My Note

自己理解のためのブログ

Mackerel監視設定の管理をServerlessで自動化

目的

mackerelの監視設定を作成、削除、更新するときに mkrコマンドを利用して運用していました。 そこでの課題感が、

  • 監視設定を調べてjsonを書く
  • 監視項目を追加した際は監視idが自動で割り振られるため、jsonで定義してからmkr pushして監視idを反映するためにmkr pullしてその結果をgithubにpushする

多くの項目を一気に設定するにはとてもいいんですが、ちょっとした変更等であればWeb管理画面でポチッと変更したほうが楽だし効率的かもしれない🤔 また、誰がどの監視設定を変更したかの記録も残したいという気持ちがありました。そこで調べていたら、下記のはてなさんの公式ブログを見つけました。

developer.hatenastaff.com

本ブログでは、この記事を参考にやったことについて書いていきます。

アーキテクチャについて

f:id:yhidetoshi:20190815075225p:plain

■ 処理の流れ

f:id:yhidetoshi:20190815223702p:plain

  1. 管理者がwebコンソールで監視設定作業
  2. MackerelがwebhookでAPIGatewayにjsonペイロードをPost
  3. APIGatewayからLambdaをコール
  4. Lambdaでwebhookのjsonペイロードをパースして環境変数を付与してCodeBuildを実行
  5. CodeBuildでmkrで最新設定情報を取得して、GitHubにpush

実装について

Lambda ( Go )

コード以外の部分を先に作っておきます。

( Goのソースコードは後ほど記載します。)

f:id:yhidetoshi:20190816165146p:plain

  • 一から作成 を選択
  • ランタイム: Go 1.X を選択
  • ロールを選択 ( あらかじめ以下の権限で作成する )
    • マネージドポリシー AWSCodeBuildDeveloperAccess
    • 以下のポリシーを付与
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
  • ハンドラ: main

これら以外はデフォルト値。

APIGateway

APIを作成する

f:id:yhidetoshi:20190816174229p:plain

リソースを作成する。

f:id:yhidetoshi:20190816095049p:plain

f:id:yhidetoshi:20190816095125p:plain

  • 作成したリソースに対してメソッドをPostで作成する。
    • 統合タイプ: Lambda関数
    • Lambda プロキシ統合の使用: 有効
    • Lambdaリージョン: ap-northeast-1
    • Lambda関数を指定

f:id:yhidetoshi:20190816095154p:plain

f:id:yhidetoshi:20190816095222p:plain

f:id:yhidetoshi:20190816095711p:plain

f:id:yhidetoshi:20190816095256p:plain

f:id:yhidetoshi:20190816095309p:plain

3.1 統合リクエストの設定 - 統合タイプ: Lambda関数

■ リソースポリシーの設定する

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:<REGION>:<AWS_ACCOUNT-ID>:<APIGW-ID>/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "52.193.111.118/32",
                        "52.196.125.133/32",
                        "13.113.213.40/32",
                        "52.197.186.229/32",
                        "52.198.79.40/32",
                        "13.114.12.29/32",
                        "13.113.240.89/32",
                        "52.68.245.9/32",
                        "13.112.142.176/32"
                    ]
                }
            }
        }
    ]
}

■ 許可されていないIPアドレスからAPIGatewayを実行してみたときのログはこんな感じに。

{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:ap-northeast-1:********:<api-id>/stg/POST/monitor"}

→ ちゃんと失敗してます。

注意) リソースポリシーの設定を反映させるためには、APIのデプロイ を実施する必要があります。

Lambdaを指定すると以下のようにLambdaとAPIGatewayが連携されます。

f:id:yhidetoshi:20190816175227p:plain

Mackerel

通知チャンネルを追加する

最初にWebhookのURLにセットする値を確認する

f:id:yhidetoshi:20190816224015p:plain

Mackerelの管理画面から設定する。

Channels --> 通知グループを追加 --> Webhook

f:id:yhidetoshi:20190816223442p:plain

メモリ監視の設定を変更したときのWebhookのjsonペイロード例。(lambdaでjsonを取得した結果)

{
    "orgName": "ABC", 
    "event": "monitorUpdate", 
    "monitor": 
    {
        "duration": 3, 
        "maxCheckAttempts": 1, 
        "isMute": "False", 
        "metric": "memory%", 
        "excludeScopes": [], 
        "name": "Memory %", 
        "warning": 80, 
        "memo": "", 
        "id": "ABCDEFGHIJK", 
        "scopes": ["stg"], 
        "type": "host",
        "operator": ">"
    }, 
    "user": 
        {
            "id": "ABCDEFGHIJK", 
            "screenName": "example@com"
        }
}

LambdaのGoコード

Goの処理としてやりたいことは、mackerelから受け取るwebhookのjsonをパースして

  • screenName
  • event
  • name(monitor)

をCodeBuildに環境変数としてセットし、実行させることです。

監視項目Memory %閾値を変更したときに取得した環境変数にセットする値です。

{“name”:“Memory %“,”event”:“monitorUpdate”,“screenName”:“example@com”}

Goのコード (main.go )

package main

import (
    "encoding/json"
    "fmt"
    "strings"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/codebuild"
)

var (
    projectName = "mackerel_monitor"
    config       = aws.Config{Region: aws.String("ap-northeast-1")}
    svcCodeBuild = codebuild.New(session.New(&config))
)

// DataRequest set from json
type DataRequest struct {
    Monitor Monitor `json:"monitor"`
    User    User    `json:"user"`
    Event   string  `json:"event"`
}

// User set from json
type User struct {
    ScreenName string `json:"screenName"`
}

// Monitor set from json
type Monitor struct {
    Name string `json:"name"`
}

// APIResponse return apigw
type APIResponse struct {
    Name       string `json:"name"`
    Event      string `json:"event"`
    ScreenName string `json:"screenName"`
}

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

// Handler aws lambda hadoler
func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    reqBody := request.Body
    jsonBytes := []byte(reqBody)
    mackerelReq := &DataRequest{}

    // JSON parse. json to struct
    if err := json.Unmarshal(jsonBytes, mackerelReq); err != nil {
        fmt.Println(err)
    }

    // Setup CodeBuild
    var ptrProjectName *string
    ptrProjectName = &projectName

    // ENV MONITOR_NAME trim '{' and '}'
    repMonitor := aws.String(strings.Replace(fmt.Sprint(mackerelReq.Monitor), "{", "", -1))
    repMonitor = aws.String(strings.Replace(*repMonitor, "}", "", -1))

    // ENV EMAIL trim '{' and '}'
    trimUserEmail := aws.String(strings.Replace(fmt.Sprint(mackerelReq.User), "{", "", -1))
    trimUserEmail = aws.String(strings.Replace(*trimUserEmail, "}", "", -1))

    // Set Env when codebuild start
    params := &codebuild.StartBuildInput{
        ProjectName: ptrProjectName,
        EnvironmentVariablesOverride: []*codebuild.EnvironmentVariable{
            {
                Name:  aws.String("MONITOR_NAME"),
                Value: repMonitor,
            },
            {
                Name:  aws.String("EVENT"),
                Value: aws.String(fmt.Sprint(mackerelReq.Event)),
            },
            {
                Name:  aws.String("MKR_USER_EMAIL"),
                Value: trimUserEmail,
            },
        }}

    // Start CodeBuild
    req, _ := svcCodeBuild.StartBuildRequest(params)
    err := req.Send()

    if err == nil {
        fmt.Println("no error codebuild request")
    }

    // Response APIGW
    event := aws.String(fmt.Sprint(mackerelReq.Event))
    name := repMonitor
    screenName := trimUserEmail

    apiRes := APIResponse{
        Name:       *name,
        Event:      *event,
        ScreenName: *screenName,
    }

    // Create json. struct to json
    resJSONBytes, _ := json.Marshal(apiRes)

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

CodeBuildに先程説明した3つを環境変数として渡し、gitのコミットメッセージで利用する。

環境変数 ( CodeBuild ) webhook(json)
MKR_USER_EMAIL screenName
EVENT event
MONITOR_NAME name(monitor)

CodeBuild

■ CodeBuildを作成する

f:id:yhidetoshi:20190817075250p:plain

プロジェクト名は goのコードの中で定義しているので、今回は mackerel_monitor 。 Lambdaの環境変数で定義したほうが扱い易いので今後修正する予定。

■ 実行環境を設定する

f:id:yhidetoshi:20190817080410p:plain

コンテナイメージはDockerHubのものを指定。今回指定したのでは、はてなさんの公式ブログに記載のあった DockerHub imageのイメージを利用させていただきました。このイメージを利用したのは必要なパッケージが事前にインストールされているのでCodeBuildの実行時間が短くなるためです。

■ Buildspecを設定する

f:id:yhidetoshi:20190817081321p:plain

今回はCodePipelineを使わないのでCodeBuildの処理定義は ビルドコマンドの挿入 を選択し、エディタに切り替え をクリックしてエディタを表示させて以下のようにbuildsepc.ymlのコードを定義します。

buildspec.yml

version: 0.2

env:
  variables:
    GITHUB_USER: "yhidetoshi"
    GIT_BRANCH: "mod_mackerel_monitor"
  parameter-store:
    GITHUB_TOKEN: "github_token"
    GITHUB_REPO: "mackerel_repo"
    MACKEREL_APIKEY: "mackerel_apikey"

phases:
  pre_build:
    commands: 
      - export MACKEREL_APIKEY=${MACKEREL_APIKEY}
      - mkr monitors pull
      - git clone -b ${GIT_BRANCH} --depth 1 https://${GITHUB_USER}:${GITHUB_TOKEN}@github.com/${GITHUB_USER}/${GITHUB_REPO}.git
      - cd ${GITHUB_REPO}/mackerel
      - git config --global user.name \"${MKR_USER_EMAIL}\" && git config --global user.email \"${MKR_USER_EMAIL}\"

  build:
    commands:
      - mv -f ../../monitors.json .
      - git add monitors.json && git diff --cached --exit-code --quiet || git commit -m \""${EVENT} ${MONITOR_NAME} by ${MKR_USER_EMAIL}"\"

  post_build:
    commands:
      - git push --force-with-lease origin HEAD

■ CodeBuildに付与するIAMロールにアタッチするポリシーは以下にしました。

  • AmazonSSMReadOnlyAccess
    • SSMパラメータストアに機密情報(Githubトークンやmackerelのapikey等)を参照するため
  • CloudWatchLogsFullAccess
    • CodeBuildのログを保存するため

機密情報の扱い

今回、機密情報の取得はSSMパラメータストアから参照するようにしました。

  • buildspec.yml (一部抜粋)
env:
  ・・・
  parameter-store:
    GITHUB_TOKEN: "github_token"
    MACKEREL_APIKEY: "mackerel_apikey"
変数 ( SSMパラメータストア ) webhook(json)
GITHUB_TOKEN GitHubに接続するため
MACKEREL_APIKEY mkrでmackerelから監視設定を取得するため

[参考]

AnsibleでSSMパラメータストアを管理する方法を以前のブログにまとめています。 yhidetoshi.hatenablog.com

Lambdaで作成した環境変数を利用

CodeBuildの処理の中でmackerelから受けるwebhookのjsonペイロードからLambda (Go) で以下の環境変数を作成して利用しています。 gitのコメントに付与して、GitHubのPullRequestを作成するときに どのユーザが何の監視項目をどうしたのか を表示させるために利用しています。

  • ${MONITOR_NAME}
  • ${EVENT}
  • ${MKR_USER_EMAIL}

monitorUpdate Memory % by example@com

デプロイについて

今回、LambdaとAPIGatewayのデプロイを Serverless Framework を利用したデプロイについても記載しておきます。 CodeBuildのデプロイについては、上記で記載した方法で実施。

serverless.com

Goコードのコンパイルは以下のMakefileで行います。

  • Makefile
GOCMD=go
GOBUILD=$(GOCMD) build
GOGET=$(GOCMD) get

.PHONY: setup
## Install dependencies
setup:
  $(GOGET) github.com/mitchellh/gox
  $(GOGET) -d -t ./...

.PHONY: cross-build
## Cross build binaries
cross-build:
  rm -rf ./main
  gox -os=linux -arch=amd64 -output=./main -ldflags "-s -w"
  • serverspec.yml
service: mackerel
frameworkVersion: ">=1.48.0"

provider:
  name: aws
  stage: stg
  runtime: go1.x
  region: ap-northeast-1

  endpointType: regional
  resourcePolicy:
    - Effect: Allow
      Principal: '*'
      Action: execute-api:Invoke
      Resource:
        - arn:aws:execute-api:ap-northeast-1:${opt:account}:*/*
      Condition:
        IpAddress:
          aws:SourceIp:
            - "52.193.111.118/32"
            - "52.196.125.133/32"
            - "13.113.213.40/32"
            - "52.197.186.229/32"
            - "52.198.79.40/32"
            - "13.114.12.29/32"
            - "13.113.240.89/32"
            - "52.68.245.9/32"
            - "13.112.142.176/32"

functions:
  monitor:
    handler: main
    role: mackerelMonitorLambda
    timeout: 30
    description: mackerel montiro
    memorySize: 256
    events:
      - http:
          path: /monitor
          method: post
          integration: lambda-proxy

resources:
  Resources:
    mackerelMonitorLambda:
      Type: AWS::IAM::Role
      Properties:
        RoleName: mackerelMonitorLambda
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess
        Policies:
          - PolicyName: mackerelMonitorLambda
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - "logs:CreateLogGroup"
                    - "logs:CreateLogStream"
                    - "logs:PutLogEvents"
                  Resource: "*"

■ デプロイコマンド

- Goコンパイル
  - $ make setup cross-build

- ServerlessFrameworkでデプロイ
  - $ sls deploy --aws-profile <PROFILE> --account <AWS_ACCOUNT_ID>

まとめ

今回はMackerel監視設定の運用を効率的にする方法を調べて実際に実装してみました。 APIGatewayで簡単にAPIが作れて、バックエンドはLambda ( Go )で実装し、mkrやgitの処理をCodeBuildで行いました。 APIGatewayとLambdaはServerlessFrameworkでデプロイしました。