Lambdaを使ってMackerelのアラートをRedmineのチケットにする

この記事は、Mackerel Advent Calendar 2017の18日目の記事です。
今日はMackerel移行時のお話をしようと思います。

11月にようやく旧環境からMackerel への移行が終わりました。
この移行時、MackerelとRedmineを連携してアラート管理する必要があったのでその方法をまとめます。
方法といっても、WebHookを利用するだけです。
ただLambdaと連携できればどんな用途にも応用がききます。

参考までに、アラート管理のツールとしてRedmineを選んだ理由もまとめます。

MackerelのアラートからRedmineまでの流れ

Mackerel => WebHook => CloudFront(WAF) => API GateWay => Lambda => Redmine

介在するサービスが多いですが、一つ一つの作業量はあまり多くありません。
要点は次の通りです。

  • WebHookでイベントを発生させる
  • CloudFrontにWAFをつけて、Mackerelからのアクセスに限定する
  • LambdaでRedmineのAPIを叩く

1. Lambdaの設定

1.1 LambdaのIAMロール

VPC内のRedmineにアクセスする必要があるため、
Lambda関数のロールにネットワークインタフェースの作成削除の権限を与える必要があります。
管理ポリシーのAWSLambdaVPCAccessExecutionRole をLambdaのロールに与えます。
念のため何の権限を与えているのか確認しておくと、主にNetwork Interfaceの作成のためですね。

AWSLambdaVPCAccessExecutionRole
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface"
            ],
            "Resource": "*"
        }
    ]
}

1.2 Lambda Function

Lambdaで受けて、RedmineのAPIを叩くだけの関数です。

いろいろとべた書きしています。

lambda_function.py
# coding: UTF-8
import requests
import json
import os

redmine_ip       = os.environ["REDMINE_IP"]
redmine_host     = os.environ["REDMINE_HOST"]
redmine_apikey   = os.environ["REDMINE_APIKEY"]
redmine_project  = os.environ["REDMINE_PROJECT"]
redmine_assigned = os.environ["REDMINE_ASSIGNED"]
redmine_tracker  = os.environ["REDMINE_TRACKER"]

redmine_url      = 'http://' + redmine_ip + '/issues.json?key=' + redmine_apikey

def lambda_handler(event, context):
    parsed = json.dumps(event)
    event_obj = ''

    # 実験中,2通りのイベントデータが存在したため以下の処理を入れた
    if json.loads(parsed).has_key('body'):
        event_obj = json.loads(json.loads(parsed)['body'])
    else:
        event_obj =  json.loads(parsed)

    stats = event_obj['alert']['status']
    opend = event_obj['alert']['isOpen']

    if stats != "ok" and opend == "True":
        alarm_host   = get_alarm_host(event_obj)
        monitor_name = event_obj['alert']['monitorName'].decode('utf-8')
        subject      = stats + u': "' + alarm_host + u' ' + monitor_name
        custom_fields = [
            {'id': 11, 'name': u'モニター名', 'value': monitor_name},
            {'id': 22, 'name': u'アラートレベル', 'value': stats},
            {'id': 33, 'name': u'アラート対象ホスト名', 'value': alarm_host}
        ]
        create_ticket(subject, json.dumps(event_obj, indent=4, separators=(',', ': ')), custom_fields)      

def get_alarm_host(event_obj):
    alarm_host = ""
    if event_obj.has_key('service'):
        alarm_host = event_obj['service']['name']
    if event_obj.has_key('host'):
        alarm_host = event_obj['host']['name']
    return alarm_host

def create_ticket(subject, description, custom_fields):
    request_headers = {
        'Host': redmine_host,
        'Content-Type': 'application/json'
    }

    payload = {
        'issue': {
            'project_id': redmine_project,
            'tracker_id': redmine_tracker,
            'assigned_to_id': redmine_assigned,
            'status_id': 1,
            'priority_id': 4,
            'subject': subject,
            'description': description,
            'custom_fields': custom_fields
        }
    }
    requests.post(redmine_url, data=json.dumps(payload), headers = request_headers)

1.3 Lambdaに渡されるMackerelのイベントオブジェクト

こんな感じのオブジェクトが渡されてきます。
詳しくはドキュメントを読みましょう。

{
  "orgName": "xxxxxxxxxx",
  "host": {
    "status": "working",
    "isRetired": false,
    "name": "web1",
    "roles": [
      {
        "roleName": "web",
        "fullname": "prod: web",
        "serviceName": "prod",
        "roleUrl": "[role url]",
        "serviceUrl": "[service url]"
      }
    ],
    "url": "[host url]",
    "memo": "",
    "id": "aaaaaaaaaa"
  },
  "event": "alert",
  "alert": {
    "status": "critical",
    "metricValue": 100,
    "warningThreshold": 70,
    "isOpen": true,
    "url": "[alert url]",
    "criticalThreshold": 90,
    "metricLabel": "CPU %",
    "trigger": "monitor",
    "duration": 3,
    "monitorName": "CPU %",
    "monitorOperator": ">",
    "createdAt": 1513319978252
  }
}

1.4 Lambdaのその他の設定

  • ネットワークインタフェースを作るために時間がかかるので、タイムアウト値は20秒程度に設定しました。
  • 念のためサブネットはインターネットゲートウェイを持たないルートテーブルのサブネットを設定しました。
  • requests は pip install requests -t . したものをパッケージに含めています。
  • 環境変数は以下のように設定しています。
REDMINE_APIKEY          xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
REDMINE_IP              10.0.0.4
REDMINE_ASSIGNED        999
REDMINE_HOST            example.jp
REDMINE_TRACKER         111
REDMINE_PROJECT         alert

2. API GateWayの設定

API GateWayは HTTPリクエストでLambdaを動かすため利用します。
設定は以下の通りです。

2.1 リソース/POSTメソッドの作成

APIを作成したら、リソースの作成で、URLのパスを定義します
パスが定義できたら、メソッドの作成ができるようになるのでPOSTメソッドを作成します。
MackerelのWebHookの仕様 でPOSTメソッドで受ければよいことがわかります。
メソッド作成する際に作成したLambda関数に紐づけます。

2.2 APIキーの作成

CloudFrontからのアクセスに限定させるためにAPIキーを作成します。

apigw_03_create_apikey.png

2.3 メソッドリクエストの設定

APIキーを持たないとリクエストが通らないようにします

apigw_04_check_require_apikey.png

2.4 APIのデプロイ

まだインターネットに公開されていない状態なので、公開する環境を用意します。
その環境にAPIをデプロイすることでインターネットに公開されます。

apigw_05_create_stage01.png
apigw_05_create_stage02.png

2.5 使用量プランを定義

APIキーを割り当てるために使用量プランを作成します。
スロットリングとクォータはお好みで設定します。

apigw_06_used_plan.png
apigw_06_used_plan2.png
apigw_06_used_plan3.png

3. CloudFrontの設定

3.1 API GateWayのAPIキーをヘッダにつける

CloudFront経由したリクエストは必ずx-api-keyヘッダを付けるようにします。
API GateWayで作成したAPIキーを値としてOriginの設定を行います。

cloudfront_01.png

3.2 WAFの設定

許可すべきIPは公式の回答があります。
このページも監視対象にしておいたほうが良いかもしれません。

cloudfront_02.png

CloudFrontの設定に戻ってWAFを設定します。

cloudfront_03.png

3.3 CloudFrontのその他の設定

  • OriginDomainNameにはAPI GateWayの呼び出しURLを設定します
  • CloudFrontでキャッシュされないように設定しています

4. WebHookの設定

あとはURLにhttps://[cloudfrontのドメイン名]/[api-gatewayのパス]を入力すればアラートが発生すればチケットが作られるようになります。

webhook_01.png

チケットができた

ということで、アラートがなると、こんな感じでチケットが登録されるようになりました。
もろもろの連絡はSlackで行い、内容をこのチケットにまとめるというような利用イメージです。

mackerel8.png

【参考】インシデント管理でやりたかったこと

連携させるツールを選定する際の要件は主に以下の通りでした。

  • 振り返りができること
  • 誰が対応するかをはっきりさせること
  • アラートの傾向と対策を行うための集計
  • 対応漏れ防止
  • 1つのインシデントにn個のアラートが発生したあと、このアラートはこのインシデント、と関連付ける仕組み欲しい

【参考】インシデント管理ツールの検討

やりたいことに対し、はじめはPagerDutyかReactioを利用しようとしていました。
Mackerelの通知チャネルの連携サービスに出てきていたので、簡単に利用できそうだったからです。
しかし、以下の理由で見送りました。

■ Reactio
調べた際、2016/07/04以降ブログの更新がなく、保守されているか不安を覚え利用をやめました。
※12/4にSSL証明書の更新があり、保守はされているようです。

■ PagerDuty
大人の事情で利用を断念しました。

残念ながら外部サービスに頼ることができなさそうであったため、
手軽に実装できそうな、Redmineを利用することにしました。

Redmineだけではエスカレーションの自動化が困難です。

通常ですと、通知のタイミングと手段と相手をあらかじめ決めたポリシーに沿って、自動的に通知する仕組みを入れたいところです。
良い代替案が見つからなかったため、いったんこの自動化は見送ることにしました。

まあ、Lambdaが利用できるので、エスカレーション機能を自作できないことはありませんね...

まとめ

AWSのサービスを駆使してMackerelのアラートをRedmineのチケットにすることができました。
まだ有用な集計ができるほど期間が経っていませんが、少なくとも振り返りの資料として役立っています。

今回はRedmineとの連携に限定しましたが、MackerelとLambdaで連携できるため、エスカレーションの自動化、アラート発生時のサーバ情報の収集、オートスケール、などにも転用できそうです。