CloudWatch Logs Metric Filter と Alarm を CloudFormation で作成する

記事タイトルとURLをコピーする
こんにちは。技術4課の保田(ほだ)です。
GW はずっと AWS Translate と Google 翻訳を使って適当な文章を200回ぐらい再翻訳してめちゃくちゃな日本語を作って遊んでました。オススメです。
 

要約

Lambda で特定の文字列がログ出力されたらメール通知してくれる仕組みを CloudFormation で作ります。

導入

Lambda で特定の文字列がログ出力されたらメール通知してくれる仕組みがサクッと作れればいいな、って思うことはありませんか?ありますよね? Lambda 自体は SAM (Serverless Application Model) や Serverless Framework で作成するので一緒に作ってしまった方が良いですよね。設定し忘れていて通知されなかったら大変です。

本題

今回は Lambda のログに SystemFault という文字列が出力されたら CloudWatch のアラーム状態になってそれが SNS でメール通知してくれるような仕組みを SAM で作ってみます。

ディレクトリ構成はとりあえずこんな感じとしておきます。

.
├ sns.yml
├ template.yml
└src
  └handler.py

Lambda のコード( src/handler.py )は以下のものとします。

def main(event, context):
    print('↓の文字列が出力されるとメールが飛ぶ仕組み')
    print('SystemFault')
    return 'Hello, World!'

ただ実行されるだけだとエラーは起きませんが、この SystemFault をメトリクスフィルターで引っ掛けて拾ってみよう、というわけです。(参考:https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/MonitoringLogData.html

テンプレートを書く

まず、メール通知に使う SNS トピックおよびサブスクリプションを作成します。複数の Lambda が存在しても通知するトピックは共通化されているのが自然なので、実際に使用するときもテンプレートを別にすることが多いであろうという意図でテンプレートを分けています。

AWSTemplateFormatVersion: "2010-09-09"
Description: SNS for Alarm Mail Notification

Parameters:
  SNSSubscriptionEmail:
    Type: String
    Default: xxxx@example.com
    Description: Email for Error Notice SNS Subscription.

Resources:
  # SNS Topic
  MyNoticeAlertTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: my-notice-error
      DisplayName: my-notice-error
      Subscription:
        - Endpoint: !Ref SNSSubscriptionEmail
          Protocol: email

Outputs:
  MyNoticeAlertTopicArn:
    Description: Topic Arn of MyNoticeAlert
    Value: !Ref MyNoticeAlertTopic
    Export:
      Name: !Sub ${AWS::StackName}-MyNoticeAlertTopic

そしてメインの Lambda とロググループ、メトリクスフィルター、アラームについてのテンプレートですが、一気に全部載せると長いので部分ごとに分けてご説明します。

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Log Filter and Alarm Sample

Globals:
  Function:
    Runtime: python3.8
    Timeout: 30
    MemorySize: 256

Resources:
  LogFilterAlarmSampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: log-filter-alarm-sample-function
      Handler: handler.main
      CodeUri: ./src
      Role: !GetAtt LogFilterAlarmSampleFunctionRole.Arn

  LogFilterAlarmSampleFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      RoleName: LogFilterAlarmSampleFunctionRole
      Policies:
        - PolicyName: LogFilterAlarmSampleFunctionPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              -
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-filter-alarm-sample-function*:*
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/log-filter-alarm-sample-function*:*:*

まずこちら。 Lambda のソースと実行ロールです。SAM なので頭に Transform: AWS::Serverless-2016-10-31 がついています。サンプルなのでログを出力する権限しかつけていません。

通常 SAM ではロググループも自動で作成されますが、このあとメトリクスフィルターを定義する際にリソース名を指定するため、このように明示的に定義しています。(参考:https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html

  # Log Group
  LogFilterAlarmSampleFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${LogFilterAlarmSampleFunction}
      RetentionInDays: 14

そしてメトリクスフィルターです。

  # Log Filter
  LogFilterAlarmSampleFunctionMetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties:
      FilterPattern: "SystemFault"
      LogGroupName:
        !Ref LogFilterAlarmSampleFunctionLogGroup
      MetricTransformations:
        -
          MetricValue: "1"
          MetricNamespace: LogMetrics
          MetricName: !Sub ${LogFilterAlarmSampleFunction}/SystemFault

FilterPattern として SystemFault という文字列の出現回数をカウントします。(参考:https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-logs-metricfilter.html

最後にアラームです。(参考:https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-cw-alarm.html

  # Alarm
  LogFilterAlarmSampleFunctionAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: My Lambda SystemFault Alarm
      AlarmName: !Sub ${LogFilterAlarmSampleFunction}-system-fault
      AlarmActions:
        - !ImportValue SampleSNSStack-MyNoticeAlertTopic
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Threshold: 1
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: !Sub ${LogFilterAlarmSampleFunction}/SystemFault
      Namespace: LogMetrics
      Period: 60
      Statistic: Maximum

上に書いたメトリクスを MetricName で指定し、「1分以内にメトリクスフィルターで拾った出現回数の最大値が1を超えたら」つまり「1分以内にフィルターパターンに合致したログが1回でも出力されれば」アラームのメール通知が飛ぶようになっています。

冒頭のディレクトリ構成の図に書いた template.yml はこれらの4つをそのままつなげたものです。

デプロイする

デプロイします。

まず SNS です。エンドポイントのメールアドレスは必要に応じて上書きします。

$ aws cloudformation deploy \
  --template-file sns.yml \
  --stack-name SampleSNSStack \
  --parameter-overrides SNSSubscriptionEmail=yyyy@example.com

デプロイに成功するとサブスクリプションを Confirm して下さいというメールが来るので Confirm します。ちなみに 2020 年5月時点で CloudFormation のマネジメントコンソールの「リソース」タブから SNS トピックのリンクを飛ぼうとすると ARN の「:(コロン)」がエスケープされてしまって変な感じになりますが、問題ありません。

続けて Lambda 周りのテンプレートをデプロイしますが、まず Lambda のソースコードをパッケージングしてやる必要があります。バケット名には適切な既存のバケットを指定します。

$ aws cloudformation package --template-file template.yml \
  --output-template-file packaged.yml \
  --s3-bucket hoge-bucket

成功するとこんなログが出るかと思います。

Successfully packaged artifacts and wrote output template to file packaged.yml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file C:\Users\xxxx\hoge\packaged.yml --stack-name <YOUR STACK NAME>

デプロイします。ひっかけのような気がしますが、出力されたコマンドそのままでは不十分です。  IAM ロールを作成しますので --capabilities CAPABILITY_NAMED_IAM を付けてやる必要があるからです(十分な権限が付与されている前提ですが…)。

$ aws cloudformation deploy --template-file packaged.yml \
  --stack-name SampleLogFilterAlarmStack \
  --capabilities CAPABILITY_NAMED_IAM

完了したらマネジメントコンソールで確認してみましょう。

メトリクスフィルターですが、名前はマネジメントコンソールから手動で作ったときに比べてランダム文字列がサフィックスとして付きます。

アラームはこんな感じです。上で説明した通りの設定になっています。「1」という数字が各所に出ていて混乱しますがテンプレートとしてガッチリ管理してあれば安心ですね!

ちなみになぜかわからないのですが、アクションの項目を見る通知先の SNS トピックには保留中の確認という「エンドポイントは Confirm されていないよ!」というメッセージが出っぱなしになります。ちゃんと Confirm していても出るので動作としては問題ないですがちょっとアレです。

次は実際に動かしてちゃんとメール通知がくるか試してみます。

試す

デプロイが完了したら早速挙動を確かめてみます。

どういう方法でも良いので作成された Lambda を実行します。今回のサンプルでは適当でいいです。

テストイベントを作成したら実行します。

ちょっとするとメールが来ます。やったね。

まとめ

以上の方法を活用すれば、Lambda の処理中にランダムな文字列を生成してログ出力するようにしたうえで、それが事前に登録したランダムな文字列と一致したら開発者の自分にメール通知させて「おめでとうございます!一兆分の一の確率で当選しました!」という粋な遊びができますね。