nazolabo

フリーランスのWebエンジニアが近況や思ったことを発信しています。

AWS ECSのタスクが異常終了したらログURL付きでSlackに通知する

概要

AWS ECSのタスク停止はCloudWatch Eventで検知できますが、このイベントパターンはChatBotに対応していないので、Lambdaから自前でSlackに投げる必要があります。

ECSの停止状態からログを見たい時、一覧からどのタスクが何かを発掘して…とか、CloudWatch Logsから発掘して…とかは大変です。

上記の両方を満たすため、LambdaからSlack通知を行い、かつ詳細のURLを添付するようにします。

Lambda

以下の関数を作ります。IAMで ecs:DescribeTaskDefinition を allow しておく必要があります。

const AWS = require('aws-sdk');
const https = require('https');
const qs = require('querystring');

function postToSlack(message) {
  return new Promise ((resolve, reject) => {
    const postData = qs.stringify(message);
    const req = https.request({
      hostname: 'slack.com',
      path: '/api/chat.postMessage',
      method: "POST",
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData)
      }
    });
    req.on('response', res => {
      resolve(res);
    });

    req.on('error', err => {
      reject(err);
    });
    req.write(postData);
    req.end();
  });
}

exports.handler = async function(event) {
  if (event.detail.containers.find((containerEvent) => containerEvent.exitCode != 0) === undefined) return;

  const region = event.region;
  AWS.config.update({region});
  const startedAt = event.detail.startedAt;
  const stoppedAt = event.detail.stoppedAt;
  const taskDefinitionArn = event.detail.taskDefinitionArn;
  const ecs = new AWS.ECS();
  const taskDefinition = await ecs.describeTaskDefinition({taskDefinition: taskDefinitionArn}).promise();
  const ecsCluster = event.detail.clusterArn.split("/")[1];
  const ecsTaskID = event.detail.taskArn.split("/")[1];
  const detailURL = `https://${region}.console.aws.amazon.com/ecs/home?region=${region}#/clusters/${ecsCluster}/tasks/${ecsTaskID}/details`;
  let text = `Task is STOPPED : ${taskDefinitionArn}\nRunning at: ${startedAt} - ${stoppedAt}\nDetail: ${detailURL}\n`;

  taskDefinition.taskDefinition.containerDefinitions.forEach((containerDefintion) => {
    const name = containerDefintion.name;
    const conatinerEvent = event.detail.containers.find((containerEvent) => containerEvent.name == name);
    const logDriver = containerDefintion.logConfiguration.logDriver;
    text = text + `\`\`\`\nContainer: ${name}\nExitCode: ${conatinerEvent.exitCode}\n`;
    if (logDriver == "awslogs") {
      const logGroup = containerDefintion.logConfiguration.options["awslogs-group"];
      const prefix = containerDefintion.logConfiguration.options["awslogs-stream-prefix"];
      const logStream = `${prefix}/${name}/${ecsTaskID}`;
      const LogURL = `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:log-groups/log-group/${encodeURIComponent(logGroup)}/log-events/${encodeURIComponent(logStream)}`;
      text = text + `LogURL: ${LogURL}\n`;
    }
    text = text + "```\n";
  });

  const message = {
    token: process.env.SLACK_ACCESS_TOKEN,
    channel: process.env.SLACK_CHANNEL,
    text: text
  };

  await postToSlack(message);
};

環境変数 SLACK_ACCESS_TOKEN SLACK_CHANNEL の設定が必要です。

CloudWatch Event

停止だけ取ります。起動も取りたい場合は STOPPED の設定を消してください。

{
  "detail-type": [
    "ECS Task State Change"
  ],
  "source": [
    "aws.ecs"
  ],
  "detail": {
    "lastStatus": [
      "STOPPED"
    ]
  }
}

ターゲットに Lambda 関数で上で作った関数を指定します。

結果

f:id:nazone:20200621113237p:plain

このような通知が飛んでくるようになります。リンクをクリックするとタスク詳細画面やCloudWatch Logsの画面にすぐ飛ぶことができ、原因の把握に役立ちます。

タスクの変化が少ない環境なら全ての停止を通知してもいいと思いますし、より少ないなら起動と停止の両方を通知してもいいと思います。用途によってカスタマイズしてください。

今回のテストで使った CDK のソースは https://github.com/nazo/cdk-ecs-stop-task-slack になります。