えいのうにっき

a-knowの日記です

AWS Lambda を使って Mackerel のアラートを Backlog にインシデント登録する

掲題の件を試してみたのでメモ。今回Lambda側の実装はGoにしてみた。Lambda で Goが使えるようになってからまだ一度も Lambda function を Go で書いてみたことがなかったので......。

以下手順。Mackerel や Backlog を使っていなくても、Lambda function の Go実装という観点でも多少参考にはなるかも?

Backlog で API Key を取得しておく

Lambda function から Backlog API を叩くことになるので、API Key が必要になる。

個人に紐付くもののようなので、「個人設定」から取得しておく。

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

Lambda function の作成

今回の連携のための function(関数)を新たに作成する。

AWSコンソールで Lambda を開くとあちこちにある↓のようなボタンから function の作成フローに入る。

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

一から作成 というメニューから、以下のように項目を埋めていく。

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

「ロール」(IAM Role)のところはそれぞれでいいかんじにしてもらえればよさそう。Lambda から各種 AWS リソースにアクセスする必要がある場合はいろいろカスタマイズする必要があると思うのだけど、今回のようなケースではここらへんをどうするのがいいのか、よくわかっていない。今回ここでは、新しくロールを作成して Basic Edge Lambda アクセス権限 のひとつだけをテンプレートから選んで付与しておいてみた。

関数の作成 を押すと↓こんなかんじに。

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

「トリガー」には、API Gateway を選ぶ。↓

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

するとその下部で「トリガーの設定」として各種項目を入力できるようになる。

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

今回はテスト的にやってみるという位置づけなので「セキュリティ」はオープンにしているけど、実際の運用での利用を考えている場合には、適切な認証を設定してあげるのがよさそう。

「追加」ボタンを押して追加完了。

function の実装を作成してアップロードする

次に Lamba function の実装をおこなう。今回のケースで必要な function の仕様としては、

  • Mackerel からのアラート通知 Webhook リクエスト(POST)を受けられる
    • それにより API Gateway Endpoint から Lambda function が起動される
  • リクエストボディをパースできる
  • パースした結果をもとに Backlog の課題登録APIをリクエストできる

というシンプルなもの。

Mackerel からのアラート通知 Webhook リクエストの仕様は以下のヘルプページに記載があるので、これをもとに function の実装を Go でおこなう。

mackerel.io

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/url"
    "os"
    "time"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    backlog "github.com/griffin-stewie/go-backlog"
)

type Roles struct {
    Fullname    string `json:"fullname"`
    ServiceName string `json:"serviceName"`
    ServiceUrl  string `json:"serviceUrl"`
    RoleName    string `json:"roleName"`
    RoleUrl     string `json:"roleUrl"`
}

type Host struct {
    Id        string  `json:"id"`
    Name      string  `json:"name"`
    Url       string  `json:"url"`
    Type      string  `json:"type"`
    Status    string  `json:"status"`
    Memo      string  `json:"memo"`
    IsRetired bool    `json:"isRetired"`
    Roles     []Roles `json:"roles"`
}

type Alert struct {
    CreatedAt         int64   `json:"createdAt"`
    WarningThreshold  float64 `json:"warningThreshold"`
    CriticalThreshold float64 `json:"criticalThreshold"`
    Duration          int     `json:"duration"`
    IsOpen            bool    `json:"isOpen"`
    MetricLabel       string  `json:"metricLabel"`
    MetricValue       float64 `json:"metricValue"`
    MonitorName       string  `json:"monitorName"`
    MonitorOperator   string  `json:"monitorOperator"`
    Status            string  `json:"status"`
    Trigger           string  `json:"trigger"`
    Url               string  `json:"url"`
}

type Notification struct {
    OrgName string `json:"orgName"`
    Event   string `json:"event"`
    Host    Host   `json:"host"`
    Alert   Alert  `json:"alert"`
}

// 今回の実装はAPI Gateway「統合リクエスト」の「Lambda プロキシ統合の使用」を利用する前提。
// see also: https://github.com/aws/aws-lambda-go/blob/master/events/README_ApiGatewayEvent.md
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var notification Notification
    json.Unmarshal([]byte(request.Body), &notification)

    // Mackerel は復旧時も通知がおこなわれる・復旧時通知はスルーする
    if notification.Alert.Status == "ok" {
        return events.APIGatewayProxyResponse{Body: "ok", StatusCode: 200}, nil
    }

    apikey := os.Getenv("BACKLOG_APIKEY")
    if apikey == "" {
        log.Fatalln("Backlog access API Key is required.")
        return events.APIGatewayProxyResponse{Body: "Backlog access API Key is required.", StatusCode: 400}, nil
    }

    // backlog.com なのか backlog.jp なのかはそれぞれごとに違うようなので注意
    URL, err := url.Parse("https://<space_id>.backlog.com")
    if err != nil {
        log.Fatalf("ERROR: %s", err.Error())
        return events.APIGatewayProxyResponse{Body: fmt.Sprintf("ERROR: %s", err.Error()), StatusCode: 500}, err
    }

    tm := time.Unix(notification.Alert.CreatedAt/1000, 0) // Alert.CreatedAt is msec

    // 課題登録 : https://developer.nulab-inc.com/ja/docs/backlog/api/2/add-issue/
    // projectId : https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-project-list/
    // issueTypeId : https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-issue-type-list/
    // priorityId : https://developer.nulab-inc.com/ja/docs/backlog/api/2/get-priority-list/
    client := backlog.NewClient(URL, apikey)
    response, err := client.Post("/api/v2/issues",
        values(
            map[string][]string{
                "projectId":   []string{"12345"},
                "summary":     []string{fmt.Sprintf("%s@%s - %s (occured at %s)", notification.Alert.Status, notification.Host.Name, notification.Alert.MonitorName, tm)},
                "issueTypeId": []string{"123456"},
                "priorityId":  []string{"2"},
                "description": []string{fmt.Sprintf("Alert URL : %s \nHost URL : %s", notification.Alert.Url, notification.Host.Url)}}))
    if err != nil {
        log.Fatalf("ERROR: %s", err.Error())
        return events.APIGatewayProxyResponse{Body: fmt.Sprintf("ERROR: %s", err.Error()), StatusCode: 500}, err
    }

    return events.APIGatewayProxyResponse{Body: string(response), StatusCode: 200}, nil
}

func main() {
    lambda.Start(handleRequest)
}

func values(kv map[string][]string) url.Values {
    pairs := url.Values{}
    for k, v := range kv {
        for _, s := range v {
            pairs.Add(k, s)
        }
    }
    return pairs
}

今回のような連携における注意点は上記実装内コメントにも表記しているが、以下のような点。

  • 今回の実装はAPI Gateway「統合リクエスト」の「Lambda プロキシ統合の使用」を利用する前提
    • ここまでの手順で API Gateway を自動で作成すると、デフォルトがその状態になっているので(多分)
    • この実装だと、Lambda コンソールを使ってのテストはできない(多分)ので注意
  • Mackerel は復旧時も通知がおこなわれることを考慮する
  • Backlog API の Base URI(ドメイン)は利用しているスペースにより異なる?
  • 課題登録APIをリクエストするにあたり必須な項目がいくつかあり、その項目はまた別にAPIで問い合わせることにより知ることができる
    • 今回は事前に手作業で把握しておいた

ちなみに、Backlog API クライアントの Go 実装が GitHub - griffin-stewie/go-backlog: Backlog API Client for Golang. としてあったので今回はそちらを使わせていただいた。ありがたや。

この実装を以下のコマンドによりビルド・zip圧縮する。

$ GOOS=linux GOARCH=amd64 go build -o m2b
$ zip handler.zip ./m2b

AWSコンソール内の「Designer」で Lambda function を選択すると↓、

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

圧縮したバイナリをアップロードできるメニューが表示されるので、↓

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

上記のようにファイル選択&ハンドラ名を入力。「ハンドル名」はバイナリビルド時に指定したバイナリファイル名っぽい。

さらにその下にある「環境変数」で Backlog API Key を設定しておくことを忘れずに。

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

そして、画面右上の「保存」ボタンでバイナリのアップロードと設定の保存が完了。

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

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

OK。

Mackerel の通知チャンネルとして設定する

ここまで準備してきた Lambda function、それを起動するトリガーとなる API Gateway のエンドポイントを、Mackerel からの Webhook 通知先として登録する。

API Gateway のエンドポイントは AWSコンソールで API Gateway を開いてダッシュボードとか開けば出てるので、それで。

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

ここで確認できる URL に、「リソース」で確認できる「メソッド」を指定するのを忘れずに。

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

https://xxx.execute-api.ap-northeast-1.amazonaws.com/mackerel2backlog-001/mackerel2backlog こんなかんじの形式になるはず。

Mackerel の通知チャンネルはチャンネル設定画面 から作ることができる。

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

画面右上の「通知グループ/通知チャンネルを追加」を押して...

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

Webhook 設定項目を追加する。

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

Mackerel では、通知チャンネルを新たに作成するとそのチャンネルは自動的に Default 通知グループにも設定される。通知設定漏れを防げて安心。

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

Mackerel 側の設定は以上。

動作確認

アラート発報によりちゃんとBacklogに課題登録がされるかどうか、動作確認してみる。

死活監視アラートを上げるために、手持ちのとあるサーバーで動作している mackerel-agent を stop させる。

$ sudo systemctl stop mackerel-agent

Mackerel では、エージェントが導入されているサーバーであれば毎分期待されるはずのリクエストが一定期間途絶えた場合、死活監視アラートを上げる。

mackerel.io

なので、しばし待つ。少しすると、slackが鳴動する。

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

きたきた。

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

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

Backlog の方を見てみる。

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

よっしゃよっしゃ。課題本文に Mackerel のアラート画面と該当ホストの URL も埋め込んでおいたおかげで、Backlog から Mackerel への遷移もスムーズで良いかんじ。

このあと一応エージェントを起動しなおしてみたけど、復旧時通知は意図通りスルーできていることも確認できた。

まとめ

「Mackerel Webhook 通知」「Backlog 課題登録API」「AWS Lambda」の3つを組み合わせて、Mackerel のアラートを Backlog に自動でインシデント登録する仕組みを構築してみた。Webhook リクエストの仕組みと Lambda のようなサーバーレスな仕組みのおかげで、少しの実装で様々なオペレーションを自動化できるようになったその利便性を、改めて実感した。そして、SaaS 側にプログラマブルな仕組みの構築を可能とするインターフェースがあること(Webhook であったり Web API であったり)の重要性も再認識した。

ちなみに今回、一番ハマったのが API Gateway の「統合リクエスト」「Lambda プロキシ統合の使用」のあたり。API Gateway や Lambda は触るその都度つまみ食いをしている状態なので、このあたりも少しきちっと学んでおいたほうがいいのかもしれない......。。