LambdaのErrorとWarningログをSlackに通知する
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テンプレート
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
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
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で通知先チャンネルを分けると、もっと分かりやすいです。