Lambdaが異常終了した際にアラート通知させる仕組みをAWS SAMのテンプレートで一括デプロイ

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

お疲れ様です。サーバーレス開発部の新井です。

本日は、Lambdaが異常終了した場合に、CloudWatch Alarms → SNS でアラート通知する仕組みをAWS SAMのテンプレートから一括でデプロイする方法を紹介したいと思います。現在、Lambdaの監視には

  1. CloudWatchの標準メトリクスをCloudWatch Alarmsで監視してからSNS経由で通知
  2. CloudWatchLogsのメトリクスフィルタでカスタムメトリクスを作成し、CloudWatch Alarmsで監視してSNS経由で通知
  3. CloudWatch LogsのログをLambdaへストリーミングして、Lambdaで加工してからSNSで通知
  4. CloudWatchのメトリクスをDatadogなどの3irdParty製ツールを使って定期的に取得し、アラートを設定して通知

などの方法がありますが、今回は「1. CloudWatchの標準メトリクスをCloudWatch Alarmsで監視してからSNS経由で通知」で実装していきたいと思います。

それぞれメリット・デメリットなどはありますが、Lambdaの監視って何すればいいの?って時は、導入コストも少ないので、とりあえずこれを入れとけば良いと思います。

それでは、さっそく始めていきます。

  • 参考にしたサイト
    1. https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/monitoring-functions-metrics.html
    2. https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html
    3. https://qiita.com/onooooo/items/f59c69e30dc5b477f9fd

構成図

やり方

AWS SAMでいろいろ用意

まずは、例のごとく以下のコマンドを実行

$ sam --version
SAM CLI, version 0.4.0
$ sam init --runtime python3.6

作成されたフォルダの中身はこんな感じ

$ tree -L 2
.
├── deploy.sh
├── hello_world
│   ├── app.py
│   ├── __init__.py
│   └── __pycache__
├── output.yaml
├── README.md
├── requirements.txt
├── template.yaml
└── tests
    └── unit

テンプレートの編集

必要なリソースの作成などを行っていきます。

ここでの設定のポイントとなるCloudWatch Alarmsについて少し解説します。

下記のテンプレートの設定では、期間(Period)を1分に設定しているの1分毎に1つのデータポイントが存在し、各データポイントでは1分間の合計値がプロットされています。 評価期間(EvaluationPeriods)は1に設定しているので、直近1つ目のデータポイントがしきい値(Threshold)以上であればCloudWatch Alaramsがアラーム状態に遷移します。(しきい値以下になれば、OK状態に戻ります)

つまり、毎分エラーが1回以上発生していればアラーム状態に遷移しSNSでe-mail通知するということです。また欠損データ(TreatMissingData)はしきい値内(notBreaching)として扱うように設定していますので、エラーもなにも発生してない時はOK状態として扱われます。

より詳しい内容はドキュメントをご覧ください。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-cwl-alarms

  Sample SAM Template for sam-cwl-alarms

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 10
    MemorySize: 128


Resources:

  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello_world
      Handler: app.lambda_handler
      Runtime: python3.6
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          PARAM1: VALUE
      Events:
        HelloWorldFunction:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get
      ReservedConcurrentExecutions: 1 #同時実行数の上限エラーを検知したいので1に設定

# Lambdaの標準メトリクスErrorsを監視するCloudWatch Alarms
  HelloWorldErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: "Lambda関数のErrorメトリクスを監視して、60秒間のデータポイントの合計値が1以上の場合にSNSをトリガーするアラーム"
      AlarmActions:
        - Ref: HelloWorldSNSTopic
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: '60'
      EvaluationPeriods: '1'
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: notBreaching
      Dimensions:
      - Name: FunctionName
        Value: !Ref HelloWorldFunction

# Lambdaの標準メトリクスThrottlesを監視するCloudWatch Alarms
  HelloWorldThrottlesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: "Lambda関数のThrottlesメトリクスを監視して、60秒間のデータポイントの合計値が1以上の場合にSNSをトリガーするアラーム"
      AlarmActions:
        - Ref: HelloWorldSNSTopic
      MetricName: Throttles
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: '60'
      EvaluationPeriods: '1'
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      TreatMissingData: notBreaching
      Dimensions:
      - Name: FunctionName
        Value: !Ref HelloWorldFunction

# CloudWatch AlarmsからトリガーされるSNS
  HelloWorldSNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
      - Endpoint: your_mail_address@mail.com
        Protocol: email
      TopicName: HelloWorldAlertSNS

Outputs:

  HelloWorldFunctionApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"

  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn

  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

Lambdaのソースコード編集

今回はメモリ超過やタイムアウトなどのエラーを意図的に発生させるための処理を書いていきます。

from time import sleep
import json
import logging
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    try:

        # 権限エラーを意図的に引き起こす処理
        if(event['queryStringParameters']['permissionDeny']):
            logger.info('権限エラーになる処理')
            client = boto3.client('dynamodb')
            ddb_response = client.list_tables()
            logger.info(ddb_response)

        # タイムアウトを意図的に引き起こす処理
        if(event['queryStringParameters']['timeOut']):
            logger.info('タイムアウトになる処理')
            sleep(30)

        # メモリ超過を意図的に引き起こす処理
        if(event['queryStringParameters']['memoryExceed']):
            logger.info('メモリ超過になる処理')
            some_str = ' '
            some_str *= 128000000
            logger.info(some_str)

        return {
            "statusCode": 200,
            "body": json.dumps({'message': 'hello world'})
        }

    except Exception as e:
        # キーが存在しない場合などの、例外処理
        logger.error(e)
        raise e

デプロイ

テンプレートを流して、リソースの作成を行っていく

aws s3 mb s3://<s3_bucket_name>

aws cloudformation package --template-file template.yaml --output-template-file output.yaml --s3-bucket <s3_bucket_name>

aws cloudformation deploy --template-file output.yaml --stack-name <stack_name> --capabilities CAPABILITY_IAM

アラート通知されているか確認してみる

それでは、いくつかリクエストを送ってメールに次のようなアラート通知が来ているか確認してみます。

SNSのメールの内容

CloudWatch Alarmsが状態遷移している様子

正常にレスポンスが返ってくる場合

$ curl "https://<domain>/Prod/hello?permissionDeny=&timeOut=&memoryExceed="
{"message": "hello world"}
  • メール受信
  • アラームの状態
    • OK

PythonのKeyエラーとなる場合

$ curl "https://<domain>/Prod/hello"
{"message": "Internal server error"}
  • メール受信
  • アラームの状態
    • ALARM

権限エラーとなる場合

$ curl "https://<domain>/Prod/hello?permissionDeny=true&timeOut=&memoryExceed="
{"message": "Internal server error"}
  • メール受信
  • アラームの状態
    • ALARM

タイムアウトとなる場合

$ curl "https://<domain>/Prod/hello?permissionDeny=&timeOut=true&memoryExceed="
{"message": "Internal server error"}
  • メール受信
  • アラームの状態
    • ALARM

メモリ超過となる場合

$ curl "https://<domain>/Prod/hello?permissionDeny=&timeOut=&memoryExceed=true"
{"message": "Internal server error"}
  • メール受信
  • アラームの状態
    • ALARM

同時実行数の上限となる場合

$ curl "https://<domain>/Prod/hello?permissionDeny=&timeOut=&memoryExceed=" & curl "https://<domain>/Prod/hello?permissionDeny=&timeOut=&memoryExceed="
{"message": "Internal server error"}
  • メール受信
  • アラームの状態
    • ALARM

まとめ

Lambdaのアラート通知としては最もシンプルなやり方ですが、割とこれで事足りるケースが多いのではないかと思ってます。 SNSの後ろにLambdaを置いて、周辺のログを取得してからSNSで通知などできれば良かったのですが、時間がないので今回はこれぐらいにしておきます。

お疲れ様でした。