今回は Serverless Framework で StepFunctions を作成し、エラー時の Cloudwatch Alarm も併せて定義します。
構築する環境
今回構築する環境を超適当に書くとこんな感じです。
Step Functions の Map を使って後続の関数をスケールします。SQS 使わずに実装できてとても便利です。
また、Step Functions の実行がコケたときに通知するため、Alarm と SNS を実装しています。
プラグイン準備
今回は2つのプラグインを利用します。
$ npm install -g serverless-step-functions $ npm install -g serverless-prune-plugin
Serverless プロジェクト作成
こんな感じでディレクトリを作成します。
$ tree stepfunctions-tutorial/ stepfunctions-tutorial/ ├── email.yml ├── handler1.py ├── handler2.py └── serverless.yml 0 directories, 4 files
email.yml
は SNS トピックに設定するメールをブログでうっかり公開しないように serverless.yml
から外出しするために作りました。
address: hogehoge@fugafuga.com
Lambda 実装
適当に Lambda を実装します。
import json def func1(event, context): body = { "message": "Go Serverless v3.0! Your function executed successfully!", "input": event, "targets": [0,1,2] } response = {"statusCode": 200, "body": body} # json.dumps しない。 return response
import json def func2(event, context): print(event) body = { "message": "Go Serverless v3.0! Your function executed successfully!", "input": event, } response = {"statusCode": 200, "body": json.dumps(body)} return response
handler1.py
から body.targets
配列を返しています。Lambda の初期レスポンス値は json.dumps()
されているのですが、dump するとエスケープされてしまうので json.dumps()
しないように注意。
ちなみに、json.dumps()
した値が後続処理に渡ると以下のようにマッピング失敗となります。
Serverless 設定
serverless.yml
を書いていきます。
まずは全体像。
org: talkeyboid service: stepfunctions-tutorial frameworkVersion: '3' plugins: - serverless-step-functions - serverless-prune-plugin provider: name: aws runtime: python3.9 region: ap-northeast-1 memorySize: 128 timeout: 3 logRetentionInDays: 14 apiKeys: - ${self:service}-key usagePlan: quota: limit: 100 offset: 0 period: MONTH throttle: burstLimit: 3 rateLimit: 1 custom: prune: automatic: true number: 3 env: email: ${file(./email.yml)} functions: func1: handler: handler1.func1 func2: handler: handler2.func2 stepFunctions: stateMachines: myStateMachine1: name: myStateMachine1 # role: "設定する場合はここにARN" loggingConfig: level: ALL includeExecutionData: true destinations: - Fn::GetAtt: [StepFunctionLogGroup, Arn] alarms: topics: alarm: Ref: EmailTopic metrics: - executionTimedOut - executionFailed - executionsAborted - executionThrottled treatMissingData: missing events: - http: path: hoge method: POST private: true # iamRole: "設定する場合はここにARN" definition: StartAt: Task1 States: Task1: Type: Task Resource: Fn::GetAtt: [Func1LambdaFunction, Arn] Next: MapTask MapTask: Type: Map ItemsPath: "$.body.targets" Iterator: StartAt: Task2 States: Task2: Type: Task Resource: Fn::GetAtt: [Func2LambdaFunction, Arn] End: true End: true resources: Resources: EmailTopic: Type: AWS::SNS::Topic Properties: TopicName: EmailTopic EmailSubscription: Type: AWS::SNS::Subscription Properties: Protocol: email TopicArn: !Ref EmailTopic Endpoint: ${self:custom.env.email.address} StepFunctionLogGroup: Type: AWS::Logs::LogGroup DeletionPolicy: Delete Properties: LogGroupName: /aws/states/${self:service}-${self:stepFunctions.stateMachines.myStateMachine1.name}-Logs RetentionInDays: 14
ポイントをいくつか。
provider
provider: name: aws runtime: python3.9 region: ap-northeast-1 memorySize: 128 timeout: 3 logRetentionInDays: 14 apiKeys: - ${self:service}-key usagePlan: quota: limit: 100 offset: 0 period: MONTH throttle: burstLimit: 3 rateLimit: 1
provider
で メモリサイズとタイムアウト等を明示的に指定しています。serverless のデフォルト値だとオーバースペックすぎるためです。
apiKeys
, usagePlan
で API Gateway のキーと使用量プランを設定しています。これは明示的に設定しない限り作成されないので自分で書く必要があります。API キーが不要な場合は記載しなくていいです。
custom
custom: prune: automatic: true number: 3 env: email: ${file(./email.yml)}
custom.prune
で Lambda のバージョン世代保持数を指定しています。デフォルトでは無限に増殖するため、抑えないといけません。今回は検証なので特に指定する必要はなかったですが、後学のために設定しました。
また、env.email
に email.yml
ファイルを設定しています。そのため、メールアドレスを取得したければ ファイル内のプロパティと結合し、${self:custom.env.email.address}
で取得できます。
stepFunctions
今回の本題、StepFunctions の部分です。
ロググループについて
stepFunctions: stateMachines: myStateMachine1: name: myStateMachine1 # role: "設定する場合はここにARN" loggingConfig: level: ALL includeExecutionData: true destinations: - Fn::GetAtt: [StepFunctionLogGroup, Arn]
StepFunctions のロググループは明示的に設定しないと作成されないようなので、後述する resources
でロググループを作成し、それを loggingConfig.destinations
に Fn::GetAtt
で Arn を取得し設定しています。
Alarm について
alarms: topics: alarm: Ref: EmailTopic metrics: - executionTimedOut - executionFailed - executionsAborted - executionThrottled treatMissingData: missing
Cloudwatch Alarm の設定もここでできます。alarms
プロパティの中で、後述する resources
で定義した SNS トピックを設定しています。メトリクスは以下を参照。
欠損データの取り扱いについては以下を参照。
ここでは書いていませんが、スレッショルドも設定できると思うので次やるときには設定したいと思います。
API Gateway について
API Gateway の設定は Lambda の定義と同じく、events
に設定します。API キーを利用する場合、private=true
に設定します。
events: - http: path: hoge method: POST private: true # iamRole: "設定する場合はここにARN"
データフローについて
definition: StartAt: Task1 States: Task1: Type: Task Resource: Fn::GetAtt: [Func1LambdaFunction, Arn] Next: MapTask MapTask: Type: Map ItemsPath: "$.body.targets" Iterator: StartAt: Task2 States: Task2: Type: Task Resource: Fn::GetAtt: [Func2LambdaFunction, Arn] End: true End: true
StepFunctions の中身を definition
に設定します。ここはマネジメントコンソールのビジュアルエディタで作成した json の内容をほぼそのまま転記する形で記載できそうです。
ただし、どの Lambda 関数を実行するかは若干のハックが必要です。Arn を指定する必要があるため、Fn::GetAtt
をしていますが、ここへは functions
へ定義した関数名をパスカルケースにし、後ろに LambdaFunction を付けます。
functions: func1: # これをパスカルケースにする -> Func1、+ LambdaFunction = Func1LambdaFunction handler: handler1.func1 func2: # これをパスカルケースにする -> Func2、+ LambdaFunction = Func2LambdaFunction handler: handler2.func2
これは serverless の仕様で、serverless deploy(package)
するときに作成される CloudFormation テンプレートにそのように定義されるためです。
{ "AWSTemplateFormatVersion": "2010-09-09", "Description": "The AWS CloudFormation template for this Serverless application", "Resources": { ... "Func1LambdaFunction": { "Type": "AWS::Lambda::Function", "Properties": { ... "Handler": "handler1.func1", "Runtime": "python3.9", "FunctionName": "stepfunctions-tutorial-dev-func1", ... } }
resources
resources: Resources: EmailTopic: Type: AWS::SNS::Topic Properties: TopicName: EmailTopic EmailSubscription: Type: AWS::SNS::Subscription Properties: Protocol: email TopicArn: !Ref EmailTopic Endpoint: ${self:custom.env.email.address} StepFunctionLogGroup: Type: AWS::Logs::LogGroup DeletionPolicy: Delete Properties: LogGroupName: /aws/states/${self:service}-${self:stepFunctions.stateMachines.myStateMachine1.name}-Logs RetentionInDays: 14
Email 通知用の SNS トピック・サブスクリプションと、StepFunctions 用のロググループを作成しています。StepFunctions のデフォルトのロググループ名は /aws/vendedlogs/states/<statemachine name>-Logs
のようになっているため、それに合わせています。※AWSで作成しているわけではないので、vendedlogs
は抜いています。
RetentionInDays
は完全に適当です。実務で使うときには考えましょう。
実行確認
マネジメントコンソールからエンドポイントとAPI キーを取得し、Postman に設定し、実行します。
項目 | 値 |
---|---|
メソッド | POST |
URL | ステージのエンドポイント + パス(https://~/dev/hoge) |
HTTPヘッダ | x-api-key=APIキー |
私はよくルートURLの後にパスをつけ忘れて 403 になってしまうことがあります。404 なら気づきようがあるのですが、403 だと「キー設定してるのにおかしいな...」となるので備忘として残しておきます。
実行結果は以下のように返ってきます。executionArn
は ステートマシン名 + 実行ID(ランダム値)です。
{ "executionArn": "arn:aws:states:ap-northeast-1:XXXXXXXXXXXX:execution:myStateMachine1:0fbeb2e9-581c-49ed-88b8-1c70e200a609", "startDate": 1.676463213988E9 }
感想
少々ハックはありますが、簡単に実装することができました。
Alarm の確認もしようと思ったのですが、結構時間がかかったのでまた次回。