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の作成のためですね。

```json: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を叩くだけの関数です。

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

```py: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のイベントオブジェクト

こんな感じのオブジェクトが渡されてきます。
詳しくは[ドキュメント](https://mackerel.io/ja/docs/entry/howto/alerts/webhook)を読みましょう。

```json
{
  "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 . したものをパッケージに含めています。
* 環境変数は以下のように設定しています。

```text
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の仕様](https://mackerel.io/ja/docs/entry/howto/alerts/webhook) でPOSTメソッドで受ければよいことがわかります。
メソッド作成する際に作成したLambda関数に紐づけます。

## 2.2 APIキーの作成

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

![apigw_03_create_apikey.png](https://qiita-image-store.s3.amazonaws.com/0/143313/3e4944c8-ad90-1ad9-d922-aa5146cddb8f.png)


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

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

![apigw_04_check_require_apikey.png](https://qiita-image-store.s3.amazonaws.com/0/143313/397153fa-930e-122a-c5d9-535760ba88f9.png)


## 2.4 APIのデプロイ

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

![apigw_05_create_stage01.png](https://qiita-image-store.s3.amazonaws.com/0/143313/be20ce18-6cc8-1985-87c3-749486d7611b.png)
![apigw_05_create_stage02.png](https://qiita-image-store.s3.amazonaws.com/0/143313/07d43428-1e80-ae2f-1a8d-050581a92bc0.png)


## 2.5 使用量プランを定義

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

![apigw_06_used_plan.png](https://qiita-image-store.s3.amazonaws.com/0/143313/b6b0647e-8b5a-227c-9e71-ab04c0d7c42f.png)
![apigw_06_used_plan2.png](https://qiita-image-store.s3.amazonaws.com/0/143313/0f04cc16-9aae-45dc-b7e5-2afe03dd6047.png)
![apigw_06_used_plan3.png](https://qiita-image-store.s3.amazonaws.com/0/143313/e0fff08a-04f6-19c0-d999-9dba1e5a5f49.png)

# 3. CloudFrontの設定

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

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

![cloudfront_01.png](https://qiita-image-store.s3.amazonaws.com/0/143313/74baf27d-8f1c-5ba9-8798-a74934f71ab5.png)


## 3.2 WAFの設定

許可すべきIPは[公式の回答](https://mackerel.io/ja/docs/entry/faq/spec/source-ip-addresses)があります。
このページも監視対象にしておいたほうが良いかもしれません。

![cloudfront_02.png](https://qiita-image-store.s3.amazonaws.com/0/143313/76562343-5fbf-8157-20eb-8a7fdd94c862.png)

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

![cloudfront_03.png](https://qiita-image-store.s3.amazonaws.com/0/143313/5820e08c-47ea-3149-3f31-72582c14f6d4.png)

## 3.3 CloudFrontのその他の設定

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

# 4. WebHookの設定

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

![webhook_01.png](https://qiita-image-store.s3.amazonaws.com/0/143313/5121b62c-6121-314f-4665-3bc3f4db2708.png)

# チケットができた

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

![mackerel8.png](https://qiita-image-store.s3.amazonaws.com/0/143313/b625d022-3a6c-b377-d26e-d9dccff5c25f.png)


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

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

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

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

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

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

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


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

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

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

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

# まとめ

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

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