この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
サーバーレス開発部の藤井元貴です。
サーバーレスな開発では、サーバの管理をする必要はありませんが、システム運用を行うにあたって、監視からは逃げられません。
そこで、「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サービス | 通知対象 | 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
は下記です。
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ファイルは下記です。
TestWatchServerlessApi.yaml
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を非同期実行させたあと、例外発生でエラーにします。
app.py
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(非同期実行される側)
例外発生でエラーにします。
app.py
def lambda_handler(event, context):
print('lambda2')
# 強制的にエラー発生させる
raise NotImplementedError()
notify_slack(Slack通知用)
Lambda実行時に受け取ったパラメータをそのままSlackに投げます。
ここで見やすいように加工すると親切ですね(今回はやらない)。
app.py
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
には下記を記載します。
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アラームの様子
無事に作成できました。
動作確認
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分ほど経過後、状態が「アラーム」になりました!!
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アラームの設定値はどうするのか? など、考えるべき内容はたくさんです。
下記などを参考にしていきたいです。