えいのうにっき

a-knowの日記です

Autify → Mackerel 連携 - 継続的なE2Eテストを監視する

この記事は Autify アドベントカレンダー2021 および Mackerel アドベントカレンダー 2021 の20日目のためのブログエントリです。Autify の中の人(Autify のカスタマーサクセスエンジニアをしています)よりお届けしています。

qiita.com

qiita.com

当記事執筆現在、Autify ではテスト実行結果の通知連携先として Slack をサポートしています。

Slack連携 - Autify Help Center

その他に Autify は Webhook もサポートしていますので、Slack 以外のサービスに通知などを連携したい場合には、これを活用すると良いです。

Webhook を作成する - Autify Help Center

「Webhook」というのは、何か世界標準的なフォーマットがあるわけではなく(あったら面白そうです)、「何らかのイベントが発生した場合に、あらかじめ指定された任意のURLにPOSTリクエストを送る」、ということを指していることが殆どだと思います。つまり、送られてくるその中身は、サービスごとにバラバラです。

そのため、任意のサービスに対して Webhook を使って連携をしたい場合には、「通知元(Webhookの送り元)のサービス」と「通知先(Webhookの送り先)のサービス」の間に、「通知元サービスの Webhook リクエストの内容を、通知先のサービスが求める内容に変換する "なにか"」を挟んであげる必要があります。イメージとしては以下の図のような感じです。

f:id:a-know:20211203175751p:plain

今回のこのブログ記事では、これにトライしてみることにします。「リクエストの内容を変換する "なにか"」と「通知先のサービス」は、今回は以下の図のような構成にしてみました。

f:id:a-know:20211203175806p:plain

「Google Cloud Run」は Google が提供するクラウドプラットフォーム・Google Cloud Platform 内の1サービスで、フルマネージドなコンテナ実行環境です。

cloud.google.com

「Mackerel」は、株式会社はてな が提供する SaaS 型のサーバー監視サービスです。とはいっても Mackerel は「サーバー」だけでなく「サービス」の監視も可能なので、「Autify による、サービスに対するE2Eテストを行った結果を連携・監視する」という観点でもピッタリです。

ja.mackerel.io

ちなみに、僕が前職・はてなに所属していたときに携わっていたプロダクトでもあります。Mackerel、チョットワカル。(でもだいぶ忘れてきちゃいましたが......。)

上記の構成で実現を試みる内容は、「Autify テストプランを実行した結果のうち、"成功したテストシナリオ数" と "成功以外だったテストシナリオ数" を Mackerel に投稿する」というものです。

果たして、うまく連携できるでしょうか?続きをお楽しみください!

全体像

上記の仕組みを実現するための道のりは、ざっくり以下のような感じになります。

  1. Mackerel 側で必要な事前準備をする
  2. 「Autify からの Webhook リクエストを受け付け、その内容を取得し、Mackerel に投稿するための内容に変換、Mackerel に投稿する」ようなコードを書く
  3. 2.のコードをコンテナ(Docker)化する
  4. 3.のコンテナを、Google Cloud Build を使ってビルドし、Google Container Registry に保存する
  5. 4.のコンテナイメージを Google Cloud Run にデプロイする
  6. Autify Webhook を作成する
  7. Google Cloud Run で環境変数を設定する

以降、手順を1つずつ見ていきます。

1. Mackerel 側で必要な事前準備をする

必要な事前準備は、以下のようなものになります。

2. 「Autify からの Webhook リクエストを受け付け、その内容を取得し、Mackerel に投稿するための内容に変換、Mackerel に投稿する」ようなコードを書く

書きます。僕は今回はGo言語で以下のようなコードを書きました。Cloud Run のチュートリアルにあるものにそのまま書き足したような内容になっていて、お世辞にもキレイなコードとは言えるものではありません。......も、もちろん、アドベントカレンダーに間に合わせるために、スピードを重視したがゆえ、ですよ、やだなぁ。

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/mackerelio/mackerel-client-go"
)

type Capability struct {
    OS             string `json:"os"`
    OSVersion      string `json:"os_version"`
    Browser        string `json:"browser"`
    BrowserVersion string `json:"browser_version"`
    Device         string `json:"device"`
    Resolution     string `json:"resolution"`
}

type Scenario struct {
    Action       string     `json:"action"`
    ID           int64      `json:"id"`
    StartedAt    string     `json:"started_at"`
    FinishedAt   string     `json:"finished_at"`
    Status       string     `json:"status"`
    URL          string     `json:"url"`
    ScenarioID   int64      `json:"scenario_id"`
    ScenarioName string     `json:"scenario_name"`
    ReviewNeeded bool       `json:"review_needed"`
    TestPlanID   int64      `json:"test_plan_id"`
    Capability   Capability `json:"capability"`
}

type TestPlan struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
}

type TestPlanWebhookFromAutify struct {
    Action       string     `json:"action"`
    ID           int64      `json:"id"`
    TestPlan     TestPlan   `json:"test_plan"`
    StartedAt    string     `json:"started_at"`
    FinishedAt   string     `json:"finished_at"`
    Status       string     `json:"status"`
    ReviewNeeded bool       `json:"review_needed"`
    URL          string     `json:"url"`
    Scenarios    []Scenario `json:"scenarios"`
}

func main() {
    log.Print("starting server...")
    http.HandleFunc("/autify2mackerel", handler)

    // Determine port for HTTP service.
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("defaulting to port %s", port)
    }

    // Start HTTP server.
    log.Printf("listening on port %s", port)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatal(err)
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    var testplanWebhookFromAutify TestPlanWebhookFromAutify
    byteArray, _ := ioutil.ReadAll(r.Body)
    body := string(byteArray)
    err := json.Unmarshal(byteArray, &testplanWebhookFromAutify)
    if err != nil {
        log.Fatalln(fmt.Sprintf("something wrong. detail: %s", body))
        w.WriteHeader(http.StatusBadRequest)
        return
    } else if testplanWebhookFromAutify.Scenarios == nil {
        log.Fatalln(fmt.Sprintf("Received TestScenario webhook. detail: %s", body))
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    // calculate each status test count
    var notPassedTestCount int
    var passedTestCount int
    for _, v := range testplanWebhookFromAutify.Scenarios {
        if v.Status == "passed" {
            passedTestCount++
        } else {
            notPassedTestCount++
        }
    }

    apikey := os.Getenv("MACKEREL_APIKEY")
    if apikey == "" {
        log.Fatalln("Mackerel API Key is required.")
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    serviceName := os.Getenv("SERVICE_NAME")
    if serviceName == "" {
        serviceName = "hoge"
    }

    // post as mackerel service metrics
    client := mackerel.NewClient(apikey)
    nowUnixTime := time.Now().Unix()

    var metricsValues []*mackerel.MetricValue
    notPassedMetricValue := &mackerel.MetricValue{
        Name:  fmt.Sprintf("%s.autify.tests.not_passed", serviceName),
        Time:  nowUnixTime,
        Value: notPassedTestCount,
    }
    passedMetricValue := &mackerel.MetricValue{
        Name:  fmt.Sprintf("%s.autify.tests.passed", serviceName),
        Time:  nowUnixTime,
        Value: passedTestCount,
    }
    metricsValues = append(metricsValues, notPassedMetricValue, passedMetricValue)
    client.PostServiceMetricValues(serviceName, metricsValues)

    w.WriteHeader(http.StatusOK)
    return
}

いちおう GitHub にも置いています。

github.com

Autify の Webhook は、「テストシナリオの実行結果」と「テストプランの実行結果」、そのいずれもが飛んでくるものになりますので、それに対する考慮が必要になります。今回のこのコードでは「テストプランの実行結果」だけを対象に処理するようにしています。

3. 2. のコードをコンテナ(Docker)化する

まずは Dockerfile を作ります。これは Cloud Run のチュートリアルのものをそのまま使用しました。

github.com

4. 3. のコンテナを、Google Cloud Build を使ってビルドし、Google Container Registry に保存する

Dockerfile を含むディレクトリで以下のコマンドを実行し、Cloud Build を使用してコンテナ イメージをビルドします。

gcloud builds submit --tag gcr.io/PROJECT-ID/autify2mackerel

...あ、ここで急に gcloud とか PROJECT-ID とか出てきてますが、 gcloud コマンドについてはこちらを、プロジェクトについてはこちらなどを参照してください。

5. 4. のコンテナイメージを Google Cloud Run にデプロイする

以下のコマンドによりデプロイします。

gcloud run deploy --image gcr.io/PROJECT-ID/autify2mackerel --platform managed

これに関しての詳細は、Google Cloud Run のチュートリアルを参照してください。

正常に実行が完了できると、コマンドの実行結果として最後に Service URL: https://xxxxxx.a.run.app と表示されていると思います。この URL に /autify2mackerel を付け加えたもの(つまり https://xxxxxx.a.run.app/autify2mackerel )が、次の手順で指定する「Autify からの Webhook の送信先」となります。

6. Autify Webhook を作成する

この手順については、公式ヘルプページを参照してください。

Webhook を作成する - Autify Help Center

この手順の「Webhook URL」に、5. の URL を指定します。

7. Google Cloud Run で環境変数を設定する

Google Cloud Platform にログイン後、左メニューから「Cloud Run」を選び、

f:id:a-know:20211203180241p:plain

「変更内容」タブを開くと、以下のような表示になると思います。この画面内、「新しいリビジョンの編集とデプロイ」を押し、

f:id:a-know:20211203180253p:plain

「変数とシークレット」タブを開いて、以下の環境変数を設定します。

f:id:a-know:20211203180304p:plain

  • MACKEREL_APIKEY
    • 「値」には、1. の手順で発行しておいたAPIキーの内容を入力します。
  • SERVICE_NAME
    • 「値」には、1. の手順で作成しておいた「サービス」名を入力します。

上記の入力を終えたら、「デプロイ」ボタンを押します。これですべての準備が完了です。

試してみる

こんな↓感じで、成功するテストプラン・失敗するテストプランを何度か実行させてみます。ここではあくまで動作確認のため、1テストプラン=1テストシナリオ、としています。

f:id:a-know:20211203180315p:plain

実行完了してから、Mackerel の「サービス」から「サービスメトリック」タブを開いてみると...?

f:id:a-know:20211203180352p:plain

おおっ、「成功したテストシナリオ数( passed )」「成功しなかったテストシナリオ数( not_passed )」がそれぞれ、見事に投稿されグラフ化されていますね!チャレンジ成功です!

「Autify によるテストが失敗する」ということは、「対象のサービスが現在、期待した操作ができない状態である」ということですので、それをサーバー・サービス監視によるアラートと一緒に扱う、というのは、悪くない取り組みではないかと思っています(ユーザー影響が出ないように・出たときにすぐに対処できるように監視をしているわけなので)。それに、「監視とは、継続的なテストである」という言葉もありますし、ね!

ちなみに

ちなみに Mackerel では、このように投稿された値に対して閾値監視を設定することが可能です。

f:id:a-know:20211203180441p:plain

この閾値に抵触した際にはそれを任意の通知先に通知をすることができるのですが、監視のためのサービスということもあり、この通知先はかなり豊富です。

f:id:a-know:20211203180453p:plain

今回僕が「Autify からの Webhook を Mackerel に連携するために仕組みを構築した」のと同じように、特定の任意のサービスに対して連携したい場合でもそのためのコードを書けば実現できるわけですが、Mackerel を使えばこれだけたくさんのサービスに対して通知が設定できてしまいます。仮に通知のためのハブとしてだけに使うとしても、大変便利そうですね!

留意事項

ここで紹介したコードは、最低限の動作を確認するためのコード、といっても差し支えないほどの素朴なコードです。その問題点としては例えば、どんな POST リクエストでも Autify からの Webhook として扱ってしまう、といったものがあるでしょう。

Autify の Webhook は「Webhookシークレット」をサポートしていますので、これを検証した上で受け付けるようにするとよりセキュアに Webhook を利用することができるので、本格運用する際にはこれを利用することが望ましいでしょう。

Webhook をより安全に利用する - Autify Help Center