Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

この記事は、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で連携できるため、エスカレーションの自動化、アラート発生時のサーバ情報の収集、オートスケール、などにも転用できそうです。

booklive
株式会社BookLiveは書籍、マンガ、雑誌、写真集等の人気作品を取り扱う総合電子書籍ストアを運営しています。
https://booklive.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away