LambdaとAPI GatewayのCloudWatchアラームをAWS SAMで定義して監視する! Slack通知もするよ!! 〜CloudFormationの参考にもどうぞ〜

はじめに

サーバーレス開発部の藤井元貴です。

サーバーレスな開発では、サーバの管理をする必要はありませんが、システム運用を行うにあたって、監視からは逃げられません。

そこで、「CloudWatchアラームを定義し、アラーム発生時にSlackに通知する仕組み」を「AWS SAM」で作ってみました。

CloudFormationでCloudWatchアラームを定義する際の参考にもどうぞ!!!

おすすめの方

  • AWS SAMでCloudWatchアラームを設定したい
  • AWS SAMで1つのLambdaに複数のイベントトリガーを設定したい
  • AWS SAMでSwaggerを使ってAPIを定義したい
  • CloudFormationでもCloudWatchアラームを設定したい
  • LambdaからSlackにPostしたい

環境

項目 バージョン
macOS High Sierra 10.13.6
AWS CLI aws-cli/1.16.89 Python/3.6.1 Darwin/17.7.0 botocore/1.12.79
AWS SAM CLI 0.10.0
Python 3.6

構成

API GatewayとLambdaを用意し、別のLambdaを非同期実行させます。

非同期実行されたLambdaのリトライ失敗時、DLQによってSNS Topicを発行します。

また、CloudWatchアラームの動作時に、SNS Topicを発行します。

SNS Topicが発行されると起動するLambdaで、Slackに通知します。

AWS構成図

通知対象

AWSサービス 通知対象 CloudWatchメトリクス名
API Gateway 5xx系エラー発生時 5XXError
Lambda(API Gatewayの裏側) エラー発生時 Errors
Lambda(非同期実行された側) エラー発生時 Errors
Lambda(非同期実行された側) リトライ失敗時 無し(DLQのため)

Slack通知用のLambdaは、監視しません。

やってみる

Slackの設定(Incoming Webhook)

下記を参考にIncoming Webhookの設定を行い、Webhook URLをメモしておきます。

プロジェクトフォルダの作成

AWS SAMでプロジェクトフォルダを作成します。

sam init --runtime python3.6 --name TestWatchServerless

フォルダ構成

3つのLambdaを作成するため、次のようにしています。(最低限の内容のみ記載)

├── TestWatchServerlessApi.yaml
├── src
│   ├── lambda1
│   │   ├── app.py
│   │   └── requirements.txt
│   ├── lambda2
│   │   ├── app.py
│   │   └── requirements.txt
│   └── notify_slack
│       ├── app.py
│       └── requirements.txt
└── template.yaml

templateファイル

AWS SAMのtemplate.yamlは下記です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: TestWatchServerless
Globals:
  Function:
    Timeout: 3

Parameters:
  SlackWebhookUrl:
    Type: String
    Default: hoge

Resources:

  # CloudWatchアラーム用のTopic
  TestWatchServerlessAlarmTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: TestWatchServerlessAlarmTopic
      DisplayName: TestWatchServerlessAlarmTopic

  # DLQ用のTopic
  TestWatchServerlessDLQTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: TestWatchServerlessDLQTopic
      DisplayName: TestWatchServerlessDLQTopic

  # API Gatewayの定義
  HelloWorldApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: s3://cm-fujii.genki-sam-test-bucket/TestWatchServerlessApi.yaml

  # Lambda1の定義(API Gatewayの裏側)
  HelloWorldFunction1:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/lambda1
      Handler: app.lambda_handler
      Runtime: python3.6
      Environment:
        Variables:
          TARGET_LAMBDA_FUNCTION: !Ref HelloWorldFunction2
      Policies:
        LambdaInvokePolicy:
          FunctionName: !Ref HelloWorldFunction2
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref HelloWorldApi

  # Lambda2の定義(非同期実行される側)
  HelloWorldFunction2:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/lambda2
      Handler: app.lambda_handler
      Runtime: python3.6
      DeadLetterQueue:
        Type: SNS
        TargetArn: !Ref TestWatchServerlessDLQTopic

  # Lambda3の定義(Slack通知用)
  NotifySlackFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/notify_slack
      Handler: app.lambda_handler
      Runtime: python3.6
      Environment:
        Variables:
          # このURLはコミット&公開したくないため、デプロイ時にコマンドで設定する
          SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl
      Events:
        SnsAlarmTopic:
          Type: SNS
          Properties:
            Topic: !Ref TestWatchServerlessAlarmTopic
        SnsDLQTopic:
          Type: SNS
          Properties:
            Topic: !Ref TestWatchServerlessDLQTopic

  # CloudWatchアラームの定義(Lambda1のErros用)
  AlarmFunction1Errors:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: Lambda1Errors
      Namespace: AWS/Lambda
      Dimensions:
        - Name: Resource
          Value: !Ref HelloWorldFunction1
        - Name: FunctionName
          Value: !Ref HelloWorldFunction1
      MetricName: Errors
      ComparisonOperator: GreaterThanOrEqualToThreshold  # 閾値以上
      Period: 60  # 期間[s]
      EvaluationPeriods: 1  # 閾値を超えた回数
      Statistic: Maximum  # 最大
      Threshold: 1  # 閾値
      AlarmActions:
        - !Ref TestWatchServerlessAlarmTopic  # アラーム発生時のアクション

  # CloudWatchアラームの定義(Lambda2のErros用)
  AlarmFunction2Errors:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: Lambda2Errors
      Namespace: AWS/Lambda
      Dimensions:
        - Name: Resource
          Value: !Ref HelloWorldFunction2
        - Name: FunctionName
          Value: !Ref HelloWorldFunction2
      MetricName: Errors
      ComparisonOperator: GreaterThanOrEqualToThreshold  # 閾値以上
      Period: 60  # 期間[s]
      EvaluationPeriods: 1  # 閾値を超えた回数
      Statistic: Maximum  # 最大
      Threshold: 1  # 閾値
      AlarmActions:
        - !Ref TestWatchServerlessAlarmTopic  # アラーム発生時のアクション

  # CloudWatchアラームの定義(API Gatewayの5XXError用)
  AlarmApi5XXError:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: Api5XXError
      Namespace: AWS/ApiGateway
      Dimensions:
        - Name: ApiName
          Value: Test Watch Serverless API  # Swaggerファイルの title に合わせる
        - Name: Stage
          Value: Prod
      MetricName: 5XXError
      ComparisonOperator: GreaterThanOrEqualToThreshold  # 閾値以上
      Period: 60  # 期間[s]
      EvaluationPeriods: 1  # 閾値を超えた回数
      Statistic: Maximum  # 最大
      Threshold: 1  # 閾値
      AlarmActions:
        - !Ref TestWatchServerlessAlarmTopic  # アラーム発生時のアクション

Outputs:

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

Swaggerファイル

API Gatewayを定義するためのSwaggerファイルは下記です。

swagger: "2.0"
info:
  description: "SwaggerとAPI Gatewayのサンプルです。"
  version: "1.0.0"
  title: "Test Watch Serverless API"
basePath: "/Prod"
schemes:
  - "https"
paths:
  /hello:
    get:
      tags:
        - "Hello"
      summary: "This is summary"
      description: "This is description"
      consumes:
        - "application/json"
      produces:
        - "application/json"
      responses:
        200:
          description: "successful operation"
          schema:
            $ref: "#/definitions/Hello"
      x-amazon-apigateway-integration:
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction1.Arn}/invocations
        passthroughBehavior: when_no_templates
        httpMethod: POST
        type: aws_proxy
definitions:
  Hello:
    type: "object"
    required:
      - "message"
    properties:
      message:
        type: "string"

Lambda関数の作成

3つのLambdaを作成します。

Lambda1(API Gatewayの裏側)

別のLambdaを非同期実行させたあと、例外発生でエラーにします。

import os
import json
import boto3

def lambda_handler(event, context):
    client = boto3.client('lambda')

    param = {
        'hoge': 'fuga'
    }

    # 別のLambdaを非同期実行する
    client.invoke(
        FunctionName=os.getenv('TARGET_LAMBDA_FUNCTION'),
        InvocationType='Event',
        Payload=json.dumps(param)
    )

    # 強制的にエラー発生させる
    raise NotImplementedError()

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

Lambda2(非同期実行される側)

例外発生でエラーにします。

def lambda_handler(event, context):
    print('lambda2')

    # 強制的にエラー発生させる
    raise NotImplementedError()

notify_slack(Slack通知用)

Lambda実行時に受け取ったパラメータをそのままSlackに投げます。

ここで見やすいように加工すると親切ですね(今回はやらない)。

import os
import json
import requests

def lambda_handler(event, context):
    # https://api.slack.com/incoming-webhooks
    # https://api.slack.com/docs/message-formatting
    # https://api.slack.com/docs/messages/builder
    payload = {
        'text': f'```{json.dumps(event, indent=2)}```'
    }

    # http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/
    try:
        response = requests.post(os.getenv('SLACK_WEBHOOK_URL'), data=json.dumps(payload))
    except requests.exceptions.RequestException as e:
        print(e)
    else:
        print(response.status_code)

このLambda関数のコード(app.py)と同じ場所にあるrequirements.txtには下記を記載します。

requests==2.20.0

S3バケットの作成

コード等を格納するためのS3バケットを作成します。作成済みの場合は飛ばします。

aws s3 mb s3://cm-fujii.genki-sam-test-bucket

Swaggerファイルを格納

SwaggerファイルをS3バケットに格納します。

aws s3 cp TestWatchServerlessApi.yaml s3://cm-fujii.genki-sam-test-bucket/TestWatchServerlessApi.yaml

build

下記コマンドでビルドします。

sam build

package

続いてコード一式をS3バケットにアップロードします。

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-sam-test-bucket

deploy

最後にデプロイします。template.yamlの環境変数をオーバーライドし、ここでSlackのWebhook URLを設定します。

sam deploy \
    --template-file packaged.yaml \
    --stack-name TestWatchServerless \
    --capabilities CAPABILITY_IAM \
    --parameter-overrides SlackWebhookUrl=https://hooks.slack.com/services/xxxxxxxxxxxxx

CloudWatchアラームの様子

無事に作成できました。

CloudWatchアラームの様子(データ不足)

動作確認

WebAPIのURLを取得

次のコマンドでWebAPIのURLを取得します。

$ aws cloudformation describe-stacks --stack-name TestWatchServerless --query 'Stacks[].Outputs'
[
    [
        {
            "OutputKey": "HelloWorldApi",
            "OutputValue": "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/",
            "Description": "API Gateway endpoint URL for Prod stage for Hello World function"
        }
    ]
]

いざ!!!

取得したURLにGETリクエストを行います!

$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "Internal server error"}

目論見通り、Internal server errorが返ってきました。

CloudWatchアラームの様子(動作確認中)

1分ほど経過後、状態が「アラーム」になりました!!

CloudWatchアラームの様子(アラーム)

Slack通知の様子

無事に4件の通知が来ました!

(画像モザイクだらけですが……)

簡単に中身を見ます。(一部抜粋)

1件目

Lambda1(API Gatewayの裏側)のエラー通知です。

"Sns": {
    "Type": "Notification",
    "TopicArn": "arn:aws:sns:ap-northeast-1:1234567890:TestWatchServerlessAlarmTopic",
    "Subject": "ALARM: \"Lambda1Errors\" in Asia Pacific (Tokyo)",
    "Message": "略",
    "Timestamp": "2019-04-12T06:24:03.121Z",
}

2件目

API Gatewayのエラー通知です。

"Sns": {
    "Type": "Notification",
    "TopicArn": "arn:aws:sns:ap-northeast-1:1234567890:TestWatchServerlessAlarmTopic",
    "Subject": "ALARM: \"Api5XXError\" in Asia Pacific (Tokyo)",
    "Message": "略",
    "Timestamp": "2019-04-12T06:25:19.535Z",
}

3件目

Lambda2(非同期実行される側)のエラー通知です。

"Sns": {
    "Type": "Notification",
    "TopicArn": "arn:aws:sns:ap-northeast-1:1234567890:TestWatchServerlessAlarmTopic",
    "Subject": "ALARM: \"Lambda2Errors\" in Asia Pacific (Tokyo)",
    "Message": "略",
    "Timestamp": "2019-04-12T06:25:31.352Z",
}

4件目

Lambda2(非同期実行される側)のリトライ失敗の通知(DLQ)です。

"Sns": {
    "Type": "Notification",
    "TopicArn": "arn:aws:sns:ap-northeast-1:1234567890:TestWatchServerlessDLQTopic",
    "Subject": null,
    "Message": "{\"hoge\": \"fuga\"}",
    "Timestamp": "2019-04-12T06:26:38.221Z",
    "SignatureVersion": "1",
}

最後に

監視対象は何にするのか? CloudWatchアラームの設定値はどうするのか? など、考えるべき内容はたくさんです。

下記などを参考にしていきたいです。

参考