
運用中のLambdaのログに特定のワードが出現した時、Slackに通知させる仕組みをAWS CDKで作ってみました
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
1 はじめに
CS事業本部の平内(SIN)です。
Lambdaファンクションでエラーなどがログに出力された時に、これを通知する仕組みは結構重要です。
CloudWatch Logsでサブスクリプションを追加してLambdaと連携すれば、Slackなど任意の宛先に通知することは簡単です。そして、作成当初から組み込んむ場合は特に問題ないでしょう。
しかし、既に運用されているLambdaにこれを仕掛けるとなると、ちょっと、一撃とは行きません。
今回は、AWS CDK(AWS Cloud Development Kit)を使用して、この作業を軽易に行うプロジェクトを作成してみました。 (注:CFnでも、同じ仕組みを作成可能です。AWS CDKでなければ出来ないという意味ではありません)
2 使用方法
使用方法は以下のとおりです。
- SlackにWebhookを設定
- プロジェクトのダウンロード
- ターゲット関数の設定
- デプロイ
(1) SlackにWebhookを設定

Slackにアプリ追加でIncoming Webgookを追加し、Webhook URLをコピーして下さい。

Incoming Webgookの設置について、不明な場合は、下記の記事をご参照下さい。
SNSでSlackにメッセージ送信する #slack
(2) プロジェクトのダウンロード
プロジェクトは、GitHubにあります。clone若しくは、ダウンロードして下さい。
$ git clone https://github.com/furuya02/NotificationToSlack.git $ cd NotificationToSlack $ yarn && cd lambda/slackNotificationFunction && yarn && cd ../..
(3) ターゲット関数の設定
bin/notification_to_slack.tsに、以下の3つを設定して下さい。
- 対象のLambda関数
- Slackの通知先(Incoming WebgookのWebhook URL)
- 検出する文字列
ハンドラ関数名は、デフォルトでターゲット関数 + "-slack-notification" となっており、重複等がない場合は、そのまま変更する必要はありません。
//******************************************************************** */ // 設定 //******************************************************************** */ // 対象のLambda関数 const targetFunction = 'productFunction'; // Slackの通知先 const webhook = '/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx'; // 検出する文字列 const filterPattern = '?Error ?error' // ハンドラ関数名 const slackNotificationFunctionName = targetFunction+ '-slack-notification' //******************************************************************** */
(4) デプロイ
以下のコマンドでデプロイが完了します。
$ cdk bootstrap //(当該アカウントで初めてAWS CDKを使用する場合、これが必要です) $ tsc $ cdk synth $ cdk deploy Do you wish to deploy these changes (y/n)? <= y
3 デプロイ後の状態
- ターゲットのLambda関数名に -slack-notification が付加された関数が追加されます。

- ターゲットのLambdaのログが出力されるロググループには、サブスクリプションが追加されます。
 

- 追加された関数に Cloudwatch Logsからのパーミッションが付与されています。

- ターゲットの関数は、以下のように、入力値をログ出力するだけの簡単なものですので、テストからヒットする文字列を送ってみます。
productFunction
exports.handler = async (event) => {
    console.log(event);
};

- 以下のようにSlackに通知されることが確認できます。設置するだけなら、ここまでで作業は完了です。

- スタックを削除すれば、通知のしくみは、削除されます。

4 AWS CDK
以下、作成したAWS CDKのコードを紹介させて下さい。
既存のLambdaをターゲットにすると、既にロググループが出来ている事になります。
基本的に、CFnで既存のリソースの設定を変更することが出来ないので、ロググループへのサブスクリプションの追加(削除)は、カスタムリソースのLambdaを使用しています。
カスタムリソースで利用されるLambdaには、、サブスクリプションの追加(削除)を行えるように権限を付与しています。
bin/notification_to_slack.ts
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import * as lambda from '@aws-cdk/aws-lambda';
import * as iam from '@aws-cdk/aws-iam';
import * as cfn from '@aws-cdk/aws-cloudformation';
import fs = require('fs');
//******************************************************************** */
// 設定
//******************************************************************** */
// エラーを検出する対象のLambda関数
const targetFunction = 'productFunction';
// Slackの通知先
const webhook = '/services/xxxxxxxx/xxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxx';
// 検出する文字列
const filterPattern = '?Error ?error'
// ハンドラ関数名
const slackNotificationFunctionName = targetFunction+ '-slack-notification'
//******************************************************************** */
export class NotificationToSlackStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // Slackへ通知するLambda関数
    const  slackNotificationFunction = new lambda.Function(this, 'slackNotification-function', {
      functionName:  slackNotificationFunctionName,
      code: lambda.Code.asset('lambda/slackNotificationFunction'),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_10_X,
      environment : {
        "TZ": "Asia/Tokyo",
        "INCOMING_WEBHOOK": webhook,
      }
    });
    // Logsからのパーミッションを追加
    slackNotificationFunction.addPermission("primition-from-logs", {
      principal: new iam.ServicePrincipal("logs.amazonaws.com"),
      sourceArn: 'arn:aws:logs:'+ this.region + ':' + this.account + ':log-group:/aws/lambda/' + targetFunction + ':*',
    })
    //サブスクリプションを追加するLambda関数
    const singletomFunction = new lambda.SingletonFunction(this, 'singleton-function', {
        uuid: '9ea82db1-be7e-f174-fbba-90c55957fd5b',
        code: new lambda.InlineCode(fs.readFileSync('lambda/createSubscription/index.js', { encoding: 'utf-8' })),
        handler: 'index.handler',
        timeout: cdk.Duration.seconds(300),
        runtime: lambda.Runtime.NODEJS_8_10,
    });
    // サブスクリプションの追加削除の権限付与
    singletomFunction.addToRolePolicy(new iam.PolicyStatement({
        resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/lambda/${targetFunction}:*`],
        actions: ['logs:PutSubscriptionFilter',
                  'logs:DeleteSubscriptionFilter'] }
    ));
    // カスタムリソース
    new cfn.CustomResource(this, 'custom-resource', {
        provider: cfn.CustomResourceProvider.lambda(singletomFunction),
        properties: {
            "FILTER_PATTERN": filterPattern,
            "TARGET_FUNCTION_NAME": targetFunction,
            "SLACK_NOTIFICATION_FUNCTION_NAME": slackNotificationFunctionName,
            "REGION": this.region,
            "ACCOUNT": this.account
        }
    });
  }
}
const app = new cdk.App();
new NotificationToSlackStack(app, 'NotificationToSlackStack');
カスタムリソースのLambdaです。RequestType が Createの時に、サブスクリプションを追加し、Delete で削除しています。なお、Updateでも、cfn-responseでsend()を返すことを忘れないようにしなければなりません。
lambda/createSubscription/index.js
"use strict";
const AWS = require('aws-sdk');
const cfnresponse = require('cfn-response');
exports.handler = (event, context) => {
    console.log(JSON.stringify(event));
    console.log(JSON.stringify(context));
    const requestType = event['RequestType']; // Create, Delete, Update
    const resourcePropertie = event['ResourceProperties']; // Papameters from CDK
    const account = resourcePropertie.ACCOUNT;
    const slackNotificationFunctionName = resourcePropertie.SLACK_NOTIFICATION_FUNCTION_NAME;
    const targetFunctionName = resourcePropertie.TARGET_FUNCTION_NAME;
    const filterPattern = resourcePropertie.FILTER_PATTERN;
    const region = resourcePropertie.REGION;
    const logs = new AWS.CloudWatchLogs();
    let params = {
        logGroupName: '/aws/lambda/' + targetFunctionName,
        filterName: 'notification filter',
    }
    if (requestType == 'Create') {
        params.filterPattern = filterPattern;
        params.destinationArn = `arn:aws:lambda:${region}:${account}:function:${slackNotificationFunctionName}`;
        logs.putSubscriptionFilter(params, (err, data) => {
            send(event, context, err, data);
        });
    } else if (requestType == 'Delete') {
        logs.deleteSubscriptionFilter(params, (err, data) => {
            send(event, context, err, data);
        });
    } else {
      send(event, context, null, {});
    } 
};
function send(event, context, err, data) {
    if(err){
        console.log(err);
        cfnresponse.send(event, context, cfnresponse.FAILED, {});
    } else {
        cfnresponse.send(event, context, cfnresponse.SUCCESS, data);
    }
}
5 最後に
今回は、既に運用中であるLambda関数に、特定のログを監視する機能を追加してみました。 軽易に、追加・削除ができるので、個人的には、重宝するかも知れません。












