Lambdaを作ってEventBridgeで定期実行させてたけど、色々環境をいじってるうちに動かなくなってたってことがあります。そんなときにすぐに気づけるようにメール通知を実装したいと思います。
#構成
サブスクリプションフィルター+Lambda+SNSで実現します。
実際に動作させるには権限周りや入れ子になっている設定が必要なのでこのようになります。(このあたりも後で解説します。)
#Lambdaコード
こちらの記事を参考にカスタマイズしました。
https://dev.classmethod.jp/articles/notification_cloudwatchlogs_subscriptoinfilter/
import base64
import json
import zlib
import datetime
import os
import boto3
from botocore.exceptions import ClientError
print('Loading function')
def lambda_handler(event, context):
data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
data_json = json.loads(data)
log_entire_json = json.loads(json.dumps(data_json["logEvents"], ensure_ascii=False))
log_entire_len = len(log_entire_json)
#メールの件名用にロググループ名を取得
logGroup = data_json['logGroup']
#ログ全体格納用
entire_message = ''
print(log_entire_json)
for i in range(log_entire_len):
log_json = json.loads(json.dumps(data_json["logEvents"][i], ensure_ascii=False))
entire_message += log_json['message']
#複数のキーワードを設定
keywords = ['error', 'timed out']
#キーワードのいずれかを検知した時点でメールを送信する
for keyword in keywords:
if keyword in entire_message.lower():
try:
sns = boto3.client('sns')
#SNS Publish
publishResponse = sns.publish(
TopicArn = os.environ['SNS_TOPIC_ARN'],
Message = entire_message,
Subject = f'[{keyword.capitalize()}] {logGroup} Log stream'
)
print(f"keyword:'{keyword}' in, send email")
except Exception as e:
print(e)
finally:
return
print("keywords not in, don't send email")
return
検知したキーワードを件名に入れ、ログ全体(対象Lambdaの開始から終了まで)をメール通知します。
最初は検知しない場合も通知するものを考えていたのですが、うっとうしかったり、Gmailのスレッド形式表示だと埋もれてしまったりするので、errorとタイムアウトのときのみ通知するようにしました。
参考にした記事もそうでしたが、よくあるのは検知したメッセージ部分だけをメール通知するというもの。でも、エラーの詳しい内容も確認できたらいいよなあと思い、全体を通知するようにしました。なので、サブスクリプションフィルターではあえてフィルターせずに全体をLambdaに送信し処理を行います。
今回は主にエラー検知を目的としていますが、正常に終了した場合でも実行過程や結果をprint出力し、それをキーワード検知してメール送信するといった使い方も視野に入れています。(エラーなしの場合も通知するバージョンも後で載せます。)
#サブスクリプションフィルター
通知を行いたいLambda(例えばEC2の起動停止Lambda)のロググループで設定を行います。フィルターパターンがこのように指定なしになっていればOKです。コンソールによる作成時に実際のログでフィルターのテストもできます。また、メール通知Lambdaへの権限も付与されます。作成後は編集できませんが、CloudFormationテンプレートも後で載せておきますので参考にしてください。
CloudWatch Logsにログが出力されていることが前提となりますので、対象のLambdaのCloudWatch Logsへのアクセス権限を確認し、一度テストで実行するなりしてログを出力しておきましょう。
#IAMロール
以下のポリシーをアタッチしてメール通知Lambdaに使います。一応、メール通知Lambda自身もCloudWatch Logsにログを送信しています。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"sns:Publish",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
#リソース作成
後はSNSトピックにメールアドレスのサブスクリプションを設定しておけば、一通り揃いますので組み合わせていくだけですが…そのへんは面倒なのでCloudFormationテンプレートを載せておきます。ターゲットや名前はパラメータにしているので、任意の値を入力して使うこともできます。前述のLambdaコードも含んでいるので少し長くなっています。
AWSTemplateFormatVersion: "2010-09-09"
Metadata:
Generator: "former2"
Description: ""
Parameters:
ManagedPolicyName:
Type: String
Default: lambda-error-send-email-policy
RoleName:
Type: String
Default: LambdaErrorSendEmailRole
TopicName:
Type: String
Default: LambdaErrorSendEmailTopic
Endpoint:
Type: String
Default: info@example.com
FunctionName:
Type: String
Default: LambdaErrorSendEmailFunction
LogGroupName:
Type: String
Default: /aws/lambda/[TargetFunctionName]
Resources:
IAMManagedPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
ManagedPolicyName: !Sub "${ManagedPolicyName}"
Path: "/"
PolicyDocument: |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"sns:Publish",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
IAMRole:
Type: "AWS::IAM::Role"
Properties:
Path: "/"
RoleName: !Sub "${RoleName}"
AssumeRolePolicyDocument: "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"lambda.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
MaxSessionDuration: 3600
ManagedPolicyArns:
- !Ref IAMManagedPolicy
Description: ""
SNSTopic:
Type: "AWS::SNS::Topic"
Properties:
DisplayName: ""
TopicName: !Sub "${TopicName}"
SNSSubscription:
Type: "AWS::SNS::Subscription"
Properties:
TopicArn: !Ref SNSTopic
Endpoint: !Sub "${Endpoint}"
Protocol: "email"
Region: !Ref AWS::Region
LambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Description: ""
Environment:
Variables:
SNS_TOPIC_ARN: !Ref SNSTopic
FunctionName: !Sub "${FunctionName}"
Handler: "index.lambda_handler"
Code:
ZipFile: |
import base64
import json
import zlib
import datetime
import os
import boto3
from botocore.exceptions import ClientError
print('Loading function')
def lambda_handler(event, context):
data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
data_json = json.loads(data)
log_entire_json = json.loads(json.dumps(data_json["logEvents"], ensure_ascii=False))
log_entire_len = len(log_entire_json)
#メールの件名用にロググループ名を取得
logGroup = data_json['logGroup']
#ログ全体格納用
entire_message = ''
print(log_entire_json)
for i in range(log_entire_len):
log_json = json.loads(json.dumps(data_json["logEvents"][i], ensure_ascii=False))
entire_message += log_json['message']
#複数のキーワードを設定
keywords = ['error', 'timed out']
#キーワードのいずれかを検知した時点でメールを送信する
for keyword in keywords:
if keyword in entire_message.lower():
try:
sns = boto3.client('sns')
#SNS Publish
publishResponse = sns.publish(
TopicArn = os.environ['SNS_TOPIC_ARN'],
Message = entire_message,
Subject = f'[{keyword.capitalize()}] {logGroup} Log stream'
)
print(f"keyword:'{keyword}' in, send email")
except Exception as e:
print(e)
finally:
return
print("keywords not in, don't send email")
return
MemorySize: 128
Role: !GetAtt IAMRole.Arn
Runtime: "python3.8"
Timeout: 60
TracingConfig:
Mode: "PassThrough"
LambdaPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt LambdaFunction.Arn
Principal: "logs.amazonaws.com"
SourceArn: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${LogGroupName}:*"
LogsSubscriptionFilter:
Type: "AWS::Logs::SubscriptionFilter"
Properties:
LogGroupName: !Sub "${LogGroupName}"
FilterPattern: ""
DestinationArn: !GetAtt LambdaFunction.Arn
#エラーなしでも通知するLambda
import base64
import json
import zlib
import datetime
import os
import boto3
from botocore.exceptions import ClientError
print('Loading function')
def lambda_handler(event, context):
data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
data_json = json.loads(data)
log_entire_json = json.loads(json.dumps(data_json["logEvents"], ensure_ascii=False))
log_entire_len = len(log_entire_json)
logGroup = data_json['logGroup']
entire_message = ''
print(log_entire_json)
for i in range(log_entire_len):
log_json = json.loads(json.dumps(data_json["logEvents"][i], ensure_ascii=False))
entire_message += log_json['message']
keywords = ['error', 'timed out']
result = 'notification'
for keyword in keywords:
if keyword in entire_message.lower():
result = keyword
break
try:
sns = boto3.client('sns')
#SNS Publish
publishResponse = sns.publish(
TopicArn = os.environ['SNS_TOPIC_ARN'],
Message = entire_message,
Subject = f'[{result.capitalize()}] {logGroup} Log stream'
)
print(f'send [{result.capitalize()}] email')
except Exception as e:
print(e)
先ほどと同様に、error、タイムアウトの際は件名に入れてメール通知しますが、それらを検知しなかった場合も件名[Notification]としてログ全体をメール通知します。正常終了した場合でも結果をメールで確認したい場合は、Lambdaコードをこちらに書き換えればOKです。