この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Lambdaにエラーが発生したとき、CloudWatch AlarmとChatbotでエラーを通知する仕組みはよく作ります。 しかし、Lambdaのエラーだけではなく、特定のキーワードを含む内容を通知したい場合もあります。
- Lambda自体をエラーにできないが、Errorログがあるとき
- Lambda自体は正常だが、Warningログがあるとき
というわけで、Lambdaのログに特定のキーワードがあるとき、Slackに通知する仕組みを作ってみました。
おすすめの方
- Lambdaのログに特定キーワードがあるとき、Slack通知したい方
- CloudWatch Logsのサブスクリプションフィルターを使いたい方
全体構成図
CloudWatch Logsのサブスクリプションフィルターで、キーワードマッチした内容をKinesis DataStreamsに流しています。 Kinesis DataStreamsに連結しいているLambdaから、Slackに投稿しています。
CloudWatch Logsから直接Lambdaに渡しても良いのですが、下記理由により、Kinesis DataStreamsを経由しています。
- Lambdaのトリガー(サブスクリプションフィルター)の上限数が不明
- 通知したい内容が短時間に大量発生すると、Lambdaの同時実行数が心配
- 新しいLambdaを追加したとき、CloudFormation等で「Slackに通知するLambdaのトリガー」を追加するのが少し手間
LambdaのエラーをSlackに通知する仕組みを作成する
SSMパラメータストアにSlack通知先を追加
通知先URLを取得し、下記コマンドでSSM(AWS Systems Manager)のパラメータストアに追加します。
URLの先頭にhttps://
があると、AWS CLIコマンド実行に失敗するため除去しています。
aws ssm put-parameter \
--type 'String' \
--name '/Slack/Url/LambdaErrorAndWarning' \
--value 'hooks.slack.com/services/xxxxx/yyyyy/zzzzz'
SAM Init
sam init \
--runtime python3.8 \
--name Lambda-Error-Notify-Slack \
--app-template hello-world \
--package-type Zip
SAMテンプレート
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda-Error-Notify-Slack
Parameters:
NotifySlackUrl:
Type: AWS::SSM::Parameter::Value<String>
Default: /Slack/Url/LambdaErrorAndWarning
Resources:
# ErrorとWarningログを出力するLambda
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: target.lambda_handler
Runtime: python3.8
Timeout: 10
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
HelloWorldFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${HelloWorldFunction}
# Slackに通知するLambda
NotifySlackFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: hello_world/
Handler: notify.lambda_handler
Runtime: python3.8
Timeout: 10
Environment:
Variables:
NOTIFY_SLACK_URL: !Ref NotifySlackUrl
Events:
Stream:
Type: Kinesis
Properties:
Stream: !GetAtt LambdaLogStream.Arn
StartingPosition: LATEST
BatchSize: 10
NotifySlackFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${NotifySlackFunction}
# サブスクリプションフィルター
HelloWorldFunctionLogSubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
Properties:
FilterPattern: "?ERROR ?WARNING"
LogGroupName: !Ref HelloWorldFunctionLogGroup
DestinationArn: !GetAtt LambdaLogStream.Arn
RoleArn: !GetAtt LambdaLogSubscriptionFilterRole.Arn
# サブスクリプションフィルター用のIAMロール
LambdaLogSubscriptionFilterRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- !Sub logs.${AWS::Region}.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonKinesisFullAccess
# サブスクリプションフィルターを通過したログを受け取るKinesis DataStreams
LambdaLogStream:
Type: AWS::Kinesis::Stream
Properties:
ShardCount: 1
Outputs:
HelloWorldApi:
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
Lambdaコード
ログの内容を監視して、通知したいLambda
target.py
import json
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def lambda_handler(event, context):
logger.error('This is error message.')
logger.warning('This is warning message.')
logger.info('This is info message.')
logger.debug('This is debug message.')
return {
'statusCode': 200,
'body': json.dumps({
'message': 'hello world',
}),
}
Slackに通知するLambda
notify.py
import os
import json
import urllib.request
from base64 import b64decode
from gzip import GzipFile
from io import BytesIO
NOTIFY_SLACK_URL = os.environ['NOTIFY_SLACK_URL']
def lambda_handler(event, context):
for item in event['Records']:
data = json.loads(GzipFile(fileobj=BytesIO(b64decode(item['kinesis']['data']))).read())
log_group = data['logGroup']
log_stream = data['logStream']
for log_event in data['logEvents']:
message = log_event['message']
payload = make_payload(log_group, log_stream, message)
post_slack(payload)
def make_payload(log_group, log_stream, message):
base_url = 'https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:'
url = base_url + f'log-groups/log-group/{log_group.replace("/", "$252F")}/log-events/{log_stream.replace("/", "$252F")}'
return {
'blocks': [
{
'type': 'section',
'fields': [
{
'type': 'plain_text',
'text': f'Log Group: {log_group}'
},
{
'type': 'plain_text',
'text': f'Log Stream: {log_stream}'
},
{
'type': 'mrkdwn',
'text': f'<{url}|Log Link>'
}
]
},
{
'type': 'section',
'text': {
'type': 'plain_text',
'text': f'{message}'
}
}
]
}
def post_slack(payload):
try:
res = post(f'https://{NOTIFY_SLACK_URL}', data=payload)
except requests.exceptions.RequestException as e:
print(e)
raise
else:
print(res.status)
def post(url: str, data: dict, headers:dict = {}):
req = urllib.request.Request(url,
data=json.dumps(data).encode('utf-8'),
headers=headers,
method='POST')
return urllib.request.urlopen(req)
デプロイ
sam build
sam deploy \
--template-file template.yaml \
--stack-name Lambda-Error-Notify-Slack-Stack \
--s3-bucket cm-fujii.genki-deploy \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset
動作確認
Errorを発生させる
curl https://z4m5yv7tj1.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
監視しているLambdaのログ
Slackに通知された様子
Error
とWarning
のログがSlackに通知されました!
さいごに
今回は単純な内容で作成しましたが、ErrorとWarningで通知先チャンネルを分けると、もっと分かりやすいです。