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