この記事は Autify アドベントカレンダー2021 および Mackerel アドベントカレンダー 2021 の20日目のためのブログエントリです。Autify の中の人(Autify のカスタマーサクセスエンジニアをしています)よりお届けしています。
当記事執筆現在、Autify ではテスト実行結果の通知連携先として Slack をサポートしています。
その他に Autify は Webhook もサポートしていますので、Slack 以外のサービスに通知などを連携したい場合には、これを活用すると良いです。
Webhook を作成する - Autify Help Center
「Webhook」というのは、何か世界標準的なフォーマットがあるわけではなく(あったら面白そうです)、「何らかのイベントが発生した場合に、あらかじめ指定された任意のURLにPOSTリクエストを送る」、ということを指していることが殆どだと思います。つまり、送られてくるその中身は、サービスごとにバラバラです。
そのため、任意のサービスに対して Webhook を使って連携をしたい場合には、「通知元(Webhookの送り元)のサービス」と「通知先(Webhookの送り先)のサービス」の間に、「通知元サービスの Webhook リクエストの内容を、通知先のサービスが求める内容に変換する "なにか"」を挟んであげる必要があります。イメージとしては以下の図のような感じです。
今回のこのブログ記事では、これにトライしてみることにします。「リクエストの内容を変換する "なにか"」と「通知先のサービス」は、今回は以下の図のような構成にしてみました。
「Google Cloud Run」は Google が提供するクラウドプラットフォーム・Google Cloud Platform 内の1サービスで、フルマネージドなコンテナ実行環境です。
「Mackerel」は、株式会社はてな が提供する SaaS 型のサーバー監視サービスです。とはいっても Mackerel は「サーバー」だけでなく「サービス」の監視も可能なので、「Autify による、サービスに対するE2Eテストを行った結果を連携・監視する」という観点でもピッタリです。
ちなみに、僕が前職・はてなに所属していたときに携わっていたプロダクトでもあります。Mackerel、チョットワカル。(でもだいぶ忘れてきちゃいましたが......。)
上記の構成で実現を試みる内容は、「Autify テストプランを実行した結果のうち、"成功したテストシナリオ数" と "成功以外だったテストシナリオ数" を Mackerel に投稿する」というものです。
果たして、うまく連携できるでしょうか?続きをお楽しみください!
全体像
上記の仕組みを実現するための道のりは、ざっくり以下のような感じになります。
- Mackerel 側で必要な事前準備をする
- 「Autify からの Webhook リクエストを受け付け、その内容を取得し、Mackerel に投稿するための内容に変換、Mackerel に投稿する」ようなコードを書く
- 2.のコードをコンテナ(Docker)化する
- 3.のコンテナを、Google Cloud Build を使ってビルドし、Google Container Registry に保存する
- 4.のコンテナイメージを Google Cloud Run にデプロイする
- Autify Webhook を作成する
- 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 にも置いています。
Autify の Webhook は、「テストシナリオの実行結果」と「テストプランの実行結果」、そのいずれもが飛んでくるものになりますので、それに対する考慮が必要になります。今回のこのコードでは「テストプランの実行結果」だけを対象に処理するようにしています。
3. 2. のコードをコンテナ(Docker)化する
まずは Dockerfile を作ります。これは Cloud Run のチュートリアルのものをそのまま使用しました。
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」を選び、
「変更内容」タブを開くと、以下のような表示になると思います。この画面内、「新しいリビジョンの編集とデプロイ」を押し、
「変数とシークレット」タブを開いて、以下の環境変数を設定します。
MACKEREL_APIKEY
- 「値」には、1. の手順で発行しておいたAPIキーの内容を入力します。
SERVICE_NAME
- 「値」には、1. の手順で作成しておいた「サービス」名を入力します。
上記の入力を終えたら、「デプロイ」ボタンを押します。これですべての準備が完了です。
試してみる
こんな↓感じで、成功するテストプラン・失敗するテストプランを何度か実行させてみます。ここではあくまで動作確認のため、1テストプラン=1テストシナリオ、としています。
実行完了してから、Mackerel の「サービス」から「サービスメトリック」タブを開いてみると...?
おおっ、「成功したテストシナリオ数( passed
)」「成功しなかったテストシナリオ数( not_passed
)」がそれぞれ、見事に投稿されグラフ化されていますね!チャレンジ成功です!
「Autify によるテストが失敗する」ということは、「対象のサービスが現在、期待した操作ができない状態である」ということですので、それをサーバー・サービス監視によるアラートと一緒に扱う、というのは、悪くない取り組みではないかと思っています(ユーザー影響が出ないように・出たときにすぐに対処できるように監視をしているわけなので)。それに、「監視とは、継続的なテストである」という言葉もありますし、ね!
ちなみに
ちなみに Mackerel では、このように投稿された値に対して閾値監視を設定することが可能です。
この閾値に抵触した際にはそれを任意の通知先に通知をすることができるのですが、監視のためのサービスということもあり、この通知先はかなり豊富です。
今回僕が「Autify からの Webhook を Mackerel に連携するために仕組みを構築した」のと同じように、特定の任意のサービスに対して連携したい場合でもそのためのコードを書けば実現できるわけですが、Mackerel を使えばこれだけたくさんのサービスに対して通知が設定できてしまいます。仮に通知のためのハブとしてだけに使うとしても、大変便利そうですね!
留意事項
ここで紹介したコードは、最低限の動作を確認するためのコード、といっても差し支えないほどの素朴なコードです。その問題点としては例えば、どんな POST リクエストでも Autify からの Webhook として扱ってしまう、といったものがあるでしょう。
Autify の Webhook は「Webhookシークレット」をサポートしていますので、これを検証した上で受け付けるようにするとよりセキュアに Webhook を利用することができるので、本格運用する際にはこれを利用することが望ましいでしょう。