CloudWatch AlarmをAWS ChatbotからSlack通知する仕組みをCDKで作ってみた

AWS ChatbotでらくらくSlack連携!
2020.06.23

まだ六月とは思えないほど暑くなってきましたね。皆様いかがお過ごしでしょうか?

今日はCloudWatch Alarm が発火したらAWS Chatbot経由で Slack に通知される仕組みをCDKで作ってみたので手順をご紹介したいと思います。

作るもの

AWS CDK を使って以下のリソースを作成します。言語はTypescriptを使います。

  • CloudWatchAlarm
  • メトリクスフィルター
  • SNS Topic
  • AWS Chatbot
  • 他必要なIAM Role, Policyなど

CloudWatch にログを出すためにシンプルな Lambda も別途作成します。

CDKの基本的な使い方については本記事では説明を省きますが、詳しい使い方はこちらの記事が参考になればと思います。

後述しますが、Slack のワークスペースに AWS アカウントが通知を飛ばせるように権限を渡さなければいけないのですが、そこは CDK で対応できないので先に手動で設定してから AWS Chatbot のリソースを作成します。

環境

Typescript と AWS CDK のバージョンは以下の通りです。

tsc -v
Version 3.7.4

cdk "version"
1.45.0 (build 0cfab15)

サンプルの Lambda を作成

CloudWatch にログを出すための Lambda を作成します。

新しくディレクトリを切って CDK の雛形を作成します。 利用するモジュールは適宜インストールしてください。

mkdir cdk && cd cdk
cdk init --language typescript

# @aws-cdk/aws-lambdaをインストール
npm install @aws-cdk/aws-lambda

lib下のcdk-stack.tsを以下のように書き換えます。

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const helloworldFn = new lambda.Function(this, "HelloWorldSample", {
      code: new lambda.AssetCode("./lib/lambda"),
      handler: "hello-world.handler",
      runtime: lambda.Runtime.NODEJS_10_X,
    });
  }
}

次にlib下にlambdaというディレクトリを新規作成し、hello-world.tsというファイルを作成してhandlerの内容を記述します。

const AWS = require("aws-sdk");

export const handler = async (event: any = {}): Promise<any> => {
  console.log("HELLO, Slack!");
};

リクエストを投げれば ”HELLO, Slack!”のメッセージが標準出力されるだけのシンプルな関数です。この HELLO, Slack!が CloudWatchLogs に流れてきたら CloudWatch Alarm が発火するように後ほどフィルタリングの設定をします。

AWS Chatbot の Workspace Configuration を GUI から作成

AWS Chatbot で Slack 通知を行う際、Slack ワークスペースに AWS アカウントへアクセス権限を渡す必要があります。 この部分の設定は CDK でできない(2020.06.20 現在)ため、CDKでリソースを作成する前にGUI から手動で行います。

Client Typeに Slackを選択し、WorkSpace を選択して承認します。

AWS Chatbot Agent を追加する

通知先のチャンネルがプライベートチャンネルの場合は AWS Chatbot の agent を対象のチャンネルに追加する必要があります。

/invite @aws

SNS Topic と AWS Chatbot を作成

それぞれの定義をcdk-stack.tsに追加します。

SNS Topicを作成し、CloudWatch Alarmアクションと紐付けます。

    const topic = new sns.Topic(this, "notification-topic", {
      displayName: "ChatbotNotificationTopic",
      topicName: "ChatbotNotificationTopic",
    });

    // 通知さきのトピックを指定
    const action = new cwactions.SnsAction(topic);

AWS Chatbot は CloudWatch Logs への読み取り権限が必要なのでIAM RoleとPolicyも一緒に作成してアタッチします。

// Chatbot Role & Policy
  const chatbotRole = new iam.Role(this, "chatbot-role", {
    roleName: "chatbot-sample-role",
    assumedBy: new iam.ServicePrincipal("sns.amazonaws.com"),
  });

  chatbotRole.addToPolicy(
    new iam.PolicyStatement({
      resources: ["*"],
      actions: [
        "cloudwatch:Describe*",
        "cloudwatch:Get*",
        "cloudwatch:List*",
      ],
    })
  );

// Chatbot Slack Notification Integration
  const bot = new chatbot.CfnSlackChannelConfiguration(
    this,
    "sample-slack-notification",
    {
      configurationName: "sample-slack-notification",
      iamRoleArn: chatbotRole.roleArn,
      slackChannelId: "<YOUR_CHANNEL_ID>",
      slackWorkspaceId: "<YOUR_WS_ID>",
      snsTopicArns: [topic.topicArn],
    }
  );

メトリクスフィルターを作成

Log 内のキーワードで引っかかるようにメトリクスフィルターを設定します。 ここではfilterPattern”HELLO, Slack!”を指定していますが、応用方法は色々あるかと思います。

今回は Lambda のメトリクスにフィルタを作成するのでaddMetricFilter()関数を使ってフィルターを作成します。

const metricFilter = helloworldFn.logGroup.addMetricFilter(
      "Keyword Filter",
      {
        metricNamespace: "chatbot-sample",
        metricName: "filter-by-keyword",
        filterPattern: { logPatternString: "\"HELLO, Slack!\"" },
      }
    );

便利な関数が利用できるのも CDK の嬉しいところなので私は積極的に使う派ですが、細かいオプションの指定をしなくてもよしなにリソースを作成してくれたりするので、何を定義したか読み取りづらくなる場合は個別に作ったほうがいい場面もあるかと思います。

CloudWatch Alarm を作成

CloudWatch Alarm の定義を追加します。

import * as cloudwatch from "@aws-cdk/aws-cloudwatch";

const alarm = new cloudwatch.Alarm(this, "Alarm", {
      metric: metricFilter.metric(),
      actionsEnabled: true,
      threshold: 0,
      evaluationPeriods: 5,
      datapointsToAlarm: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });

    // アラームのアクションを設定
    alarm.addAlarmAction(action);

CDK 全体

全体はこんな感じになりました。

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as cloudwatch from "@aws-cdk/aws-cloudwatch";
import * as cwactions from "@aws-cdk/aws-cloudwatch-actions";
import * as sns from "@aws-cdk/aws-sns";
import * as iam from "@aws-cdk/aws-iam";
import * as chatbot from "@aws-cdk/aws-chatbot";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

   // Lambda
    const helloworldFn = new lambda.Function(this, "sample-lambda", {
      code: new lambda.AssetCode("./lib/lambda"),
      handler: "hello-world.handler",
      runtime: lambda.Runtime.NODEJS_10_X,
    });

    // Lambda Log Group setting
    const metricFilter = helloworldFn.logGroup.addMetricFilter(
      "Keyword Filter",
      {
        metricNamespace: "chatbot-sample",
        metricName: "filter-by-keyword",
        filterPattern: { logPatternString: "\"HELLO, Slack!\"" },
      }
    );

    // Chatbot Role & Policy
    const chatbotRole = new iam.Role(this, "chatbot-role", {
      roleName: "chatbot-sample-role",
      assumedBy: new iam.ServicePrincipal("sns.amazonaws.com"),
    });

    chatbotRole.addToPolicy(
      new iam.PolicyStatement({
        resources: ["*"],
        actions: [
          "cloudwatch:Describe*",
          "cloudwatch:Get*",
          "cloudwatch:List*",
        ],
      })
    );

    // SNS TOPIC
    const topic = new sns.Topic(this, "notification-topic", {
      displayName: "ChatbotNotificationTopic",
      topicName: "ChatbotNotificationTopic",
    });

    const alarm = new cloudwatch.Alarm(this, "Alarm", {
      metric: metricFilter.metric(),
      actionsEnabled: true,
      threshold: 0,
      evaluationPeriods: 5,
      datapointsToAlarm: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
    });

    // 通知さきのトピックを指定
    const action = new cwactions.SnsAction(topic);

    alarm.addAlarmAction(action);

    // Chatbot Slack Notification Integration
    const bot = new chatbot.CfnSlackChannelConfiguration(
      this,
      "sample-slack-notification",
    {
      configurationName: "sample-slack-notification",
      iamRoleArn: chatbotRole.roleArn,
      slackChannelId: "<YOUR_CHANNEL_ID>",
      slackWorkspaceId: "<YOUR_WS_ID>",
      snsTopicArns: [topic.topicArn],
    }
    );
  }
}

動作確認

リソースのデプロイが完了したらコンソールからLambdaを実行してCloudWatch Alarmが発火し、Slackへ通知が飛ぶかを検証します。

Lambdaを実行

マネジメントコンソールからLambdaを実行してCloudWatchにログが出ていることを確認します。

Alarm発火&通知

Alarmが発火してSlackへ通知されれば完了です。

最後に

AWS Chatbotが登場する前はLambdaからSlack連携する方法が一般的でしたが、Chatbotを利用することでコードを記述することなくSlack通知が手軽にできるようになりました。

対応しているサービスなど考慮事項はありますが、機会があればLambdaの代わりにChatbotを試してみてはいかがでしょう。この記事が誰かの参考になれば嬉しいです。

References