運用中のLambdaのログに特定のワードが出現した時、Slackに通知させる仕組みをAWS CDKで作ってみました

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 WebgookWebhook 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です。RequestTypeCreateの時に、サブスクリプションを追加し、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関数に、特定のログを監視する機能を追加してみました。 軽易に、追加・削除ができるので、個人的には、重宝するかも知れません。