運用中の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 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関数に、特定のログを監視する機能を追加してみました。 軽易に、追加・削除ができるので、個人的には、重宝するかも知れません。