日々のあれこれφ(..)

もっぱら壁打ち

ECSで動かしているバッチのSlackへのアラート設定

はじめに

少し前に業務でやったことの忘備録です。

アジェンダ

  • 設計思想
  • 全体の流れ
  • ECS
  • lambda
  • Datadog経由でSlackに通知
  • その他考えていたこと
  • おわりに

設計思想

とあるECSクラスタで動いているバッチが5つあるのですが、コケた時にすぐに気づける仕組みがありませんでした。 各バッチそれぞれがログは吐いているものの内容は様々なので(DBへのupdateをかけているものもあれば、毎日1回社内向けにメールを飛ばすものなど)、最低限『コケたらすぐに気づける。ログを見に行けるようにする』という要件を満たせるようにアラートの設定を行いました。

本当はアラートと一緒にログを貼り付けたり、ここを確認してこういった手順を取ってくださいみたいなplaybookを貼り付けられたら親切だったのですが、5つもあって重要度もそれほど高くないので、そこまで個別のバッチに時間をかけるのはやめました。

またDatadogを咬ませているのは、社内のルール(というかMSの監視のベストプラクティス)でサービスに関わるアラートは一箇所でまとめて管理するという方針になっているからです。そうすることでプロダクトを構成しているあらゆるサービスの監視を行いやすくします。アラートの通知先を変える必要が出た時なんかも、Datadog上からSlackのチャンネルを変更するといったことが行えるので、Lambdaのコードをいじる必要が無くなります。 Datadogを使っていなければLambdaからそのままSlackにアラートを飛ばしていました。

全体の流れ

(1)ECS → (2)CloudWatch Events → (3)Lambda → (4)Datadog → (5)Slack

(1) ECSがタスクの起動や終了を行うとイベントストリームを吐き出します。イベントストリームで吐き出された情報には、定義や終了コード、クラスタなど実行されたタスクに関する情報が含まれます。

(2) CloudWatch Eventsの設定で、特定のクラスタのイベントストリームをトリガーに、Lambda関数を実行するルールを作成します。

(3) Lambda側ではDatadogにとばすメトリクスを作成します。 飛ばすメトリクスの内容は以下のようにしました。

・バッチが正常に実行されたら0

・バッチが正常に実行されなければ1 (イベントストリームに含まれるexitCodeが0以外の数字であること、またはexitCodeが存在しない。)

exitCodeはコンテナ終了ステータスです。exitCodeが存在しない時というのは、例えば存在しないimageが指定されていてpullに失敗した時などそもそもタスクのstartに失敗した時になります。

(4) Lambdaから送ったメトリクスに対して、Datadogでモニター作成とアラートの設定を行います。 失敗した時のメトリクスが検知された時アラートを飛ばすようにここから設定を行います。 これであらかじめ設定してあるSlackのチャンネルに条件に従ってアラートを飛ばすようにできます。

ECS

イベントストリームの中身は以下のようになっています。 これはAWSブログから持ってきたもので、Lambdaのtestイベントの設定で使っています。

{
  "version": "0",
  "id": "8f07966c-b005-4a0f-9ee9-63d2c41448b3",
  "detail-type": "ECS Task State Change",
  "source": "aws.ecs",
  "account": "244698725403",
  "time": "2016-10-17T20:29:14Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
  ],
  "detail": {
    "clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/eventStreamTestCluster",
    "containerInstanceArn": "arn:aws:ecs:us-east-1:123456789012:container-instance/f813de39-e42c-4a27-be3c-f32ebb79a5dd",
    "containers": [
      {
        "containerArn": "arn:aws:ecs:us-east-1:123456789012:container/4b5f2b75-7d74-4625-8dc8-f14230a6ae7e",
        "exitCode": 1,
        "lastStatus": "STOPPED",
        "name": "web",
        "networkBindings": [
          {
            "bindIP": "0.0.0.0",
            "containerPort": 80,
            "hostPort": 80,
            "protocol": "tcp"
          }
        ],
        "taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
      }
    ],
    "createdAt": "2016-10-17T20:28:53.671Z",
    "desiredStatus": "STOPPED",
    "lastStatus": "STOPPED",
    "overrides": {
      "containerOverrides": [
        {
          "name": "web"
        }
      ]
    },
    "startedAt": "2016-10-17T20:29:14.179Z",
    "stoppedAt": "2016-10-17T20:29:14.332Z",
    "stoppedReason": "Essential container in task exited",
    "updatedAt": "2016-10-17T20:29:14.332Z",
    "taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328",
    "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/wpunconfiguredfail:1",
    "version": 3
  }
}

参考 aws.amazon.com

CloudWatch Events

ルール作成時、イベントソースの設定部分で以下のようなイベントパターンを指定しています。

{
  "detail": {
    "clusterArn": [
      "arn:aws:ecs:ap-northeast-1:[ACCOUNT ID]:cluster/CLUSTER_NAME"
    ],
    "lastStatus": [
      "STOPPED"
    ]
  },
  "detail-type": [
    "ECS Task State Change"
  ],
  "source": [
    "aws.ecs"
  ]
}

ここでの懸念事項が一つありました。

最終的には正常なメトリクスもDatadog側に飛ばすようにしていますが、当初は失敗した時のみアラートを飛ばすことが目的だったので異常な時だけメトリクスを実行できるようにすればいいのではないかと考えていました。Lambdaも実行するたびにお金がかかってしまうので。

ただ上記にも書いた通り、この時点で異常終了か否かを検知することはできなそうでした。exitCodeが存在しないケースもあるためイベントパターンで定義ができなかったです。 また正常に実行されているかどうかもモニターから確認できた方が良さそうだと要件が変わったこともあり、クラスタが実行される度にLambdaを起動させることにしました。 そもそも4/5がdailyバッチなので料金が心配になる程そもそも実行しませんでした。

参考

docs.aws.amazon.com

Lambda

Pythonで書いています。

import urllib.request
from datadog_lambda.wrapper import datadog_lambda_wrapper
from datadog_lambda.metric import lambda_metric

@datadog_lambda_wrapper
def lambda_handler(event, context):
    # success=0, error=1
    batch_exec_status = 0

    if "exitCode" not in event["detail"]["containers"][0]:
        batch_exec_status = 1
    elif event["detail"]["containers"][0]["exitCode"] != 0:
        batch_exec_status = 1

    batch_name = event["detail"]["taskDefinitionArn"].split('/')[1].split(':')[0]
    result_status = lambda_metric("stg_hoge_batch.batch_exec_status", batch_exec_status, tags=['hogehoge', 'category:batch:'+batch_name])

    return batch_name+':exitCode '+str(batch_exec_status)

Lambdaのソースコードに関してはちょうどMRのコメントで修正箇所が大量に出され、それを今直している最中なのですが、処理内容的にはこんな感じです。

LambdaのLayer機能があるのですが、Datadogが公式でDatadog Layerを用意してくれていて、それを使うとメトリクスの送信(lambda_metric())が簡単に行えるので超便利です。いい時代。

exitCode以外でやっていることは、実行されたバッチの名前を取得してメトリクスのtagsに付けています。これは通知の時に表示させるのに使うためです。 バッチの名前はタスク定義のARNから抜き出しています。

参考

docs.datadoghq.com

Datadog

以下のようにメトリクスとアラートの設定をします。

↓ 3 Set alert conditionでメトリクスが1(>0)だった時にアラートが飛ぶように設定しています。

f:id:reiichii:20190630130527p:plain

f:id:reiichii:20190630130230p:plain

2 Define the metricで「Multi Alert」の設定を行なっています。これを行うと個別のアラートを発生させることができます。今回のケースだと category:batch:[[バッチ名]] のようになっているので、アラートのタイトルにバッチ名が表示されるようになります。

f:id:reiichii:20190630131932p:plain

没にした案

CloudWatch Logsのログストリームからメトリクスを作成してアラートを飛ばす方法

当初はCloudWatch Logsで各バッチが吐き出すログから、「Error」文字などでフィルターをかけてカスタムメトリクスを作成し、それを検知したらLambdaからアラートを飛ばす、というようなことを考えていました。ただこれだとバッチが5種類あってエラーログが様々なので個別に設定しなければならないことが多くなってしまうこと、そもそもタスクが起動しなかったらエラーが検知されないからアラート設定として不完全であること、といったような理由から没にしました。

本当はログのパターンが統一されているべきなんでしょうけど。

ともあれECSのタスク実行の確認をするだけなら、Logを見るよりイベントストリームの情報を確認するやり方の方が良かったなという確信はあります。

AWS SNSを挟むか

アラートの飛ばし方を調べる中で、CloudWatch EventsやAlermからSNSに送って、そこからLambdaに送るような設定も見ました。通知先が複数あったり、中身によってフィルタリングした上で通知先を決めるみたいなことをするのであればLambdaで実装するよりSNSを利用した方が良さそうでしたが、今回は通知先は一つしかないのとメトリクスの加工をする必要があったのでSNSは不要と判断しました。

最初はSNSでexitCodeが0か0以外かでSlackに通知を流すことをやろうとしていたのですが、結局それはできなかったな。。

ECSバッチを実行する方のCloduWatch Eventsのアラート

上記のアラート設定は、ECSタスクスケジューリングの失敗はカバーしていません。そもそもバッチが起動しなかったらイベントストリームは発行されません。なのでバッチを実行するCloduWatch Eventsがコケた時もアラートを出す設定を別で用意する必要があります。

ただ今回の私のケースだとDatadogの方でCloudWatch Eventsの失敗が検知されたらアラートが飛ぶように一括で設定してあるので、ルールを作った時点でそちらに含まれる形になります。Datadog良き。

おわりに

ECSのイベントストリームを知らなかったから、exitCodeでアラートを出すやり方を見つけるまでに苦労しました。

今回の設定を通して(実は苦手意識を持っていた) Datadogとも少し仲良くなれた気がします。もっと手足のように使いこなせるようになりたい。