AWS CDKワークショップに入門して、Step Functionsステートマシンを実装してみた
こんにちは、平野です。
巷で話題のCDK(AWS Cloud Development Kit)、試してみたいなーと思ったので試しました(自己完結)。 題材として、最近ちょっと触っているStep FunctionsでLambda関数を実行するところまでを行いました。 言語はまあまあ使い慣れているPythonで行っています。
まず手始めに、下記のワークショップを実践しました。
https://cdkworkshop.com/30-python.html
CDK気になるけど何からすればいいの?という人は、 とりあえずこれの通りにやるのが一番早いかと思います。 もちろんやってみた記事もありますので、こちらの記事もご参照ください! (こちらはTypeScriptです)
この記事では、上記ワークショップでLambda関数を作った環境をそのまま流用して Step Functionsで簡単なステートマシンを作ったという内容ですので、 試してみるか、という方はワークショップから始めてみて下さい。
なお、CDKのAPIリファレンスによると、Step Functions関連のAPIのほとんどにexperimental
と記載されていますので、
今後大きな変更がある可能性もありますのでご注意ください。
やってみた
CDKのバージョンは1.11.0
です。
出発点
出発点としては、Hello LambdaのページでLambda関数を作成し終えた所です。
この部分までワークショップを進めれば、 「ここにAWSリソースのインスタンスを作成するコードを書いて、デプロイすればリソースが作られるんだな」 というのがわかるかと思いますので、 あとは手探りでStep Functionsのリソースを作成して行きます。
また、ステートマシンを一通り作成するのに必要なパッケージは以下の2つですのでインストールしておきます。
aws_stepfunctions aws_stepfunctions_tasks
ステートマシンからLambda関数までの繋がりを確認する
まずは全体の枠としてStateMachine
のインスタンスを作ります。
リファレンスを見ると、StateMachine
のコンストラクタはのようです。
aws_cdk.aws_stepfunctions.StateMachine
__init__(scope, id, *, definition, role=None, state_machine_name=None, timeout=None)
ワークショップの流れを見れば、scope
はself
、id
はリソース名を入れれば良さそうなので、
残りで必須なものはdefinition
となります。
definition
はIChainable
インターフェイスのものが指定できるようです。
最初ここで、IChainable
って何??となってだいぶ行き詰まってしまったのですが、CDKにはJavaのリファレンスも用意されていて、
基本的に構成は同じだろうということで、Javaの方を見てみると、
IChainable
の実装としてState
などがあることがわかりました。
ということでState
インスタンスを作成しようとしたのですが、こちらは抽象クラスでした。
Task
やParallel
など、Step FunctionsでTypeとして指定するものが具象クラスになっているようです。
Lambda関数の起動のTypeはTask
なので、Task
インスタンスを作成します。
コンストラクタは
aws_cdk.aws_stepfunctions.Task
__init__(scope, id, *, task, comment=None, input_path=None, output_path=None, result_path=None, timeout=None)
なので、task
が必要です。で、これはIStepFunctionsTask
インターフェイスを指定します。
こちらはInvokeFunction
やPublishToTopic
など、下記の選択肢に相当する部分です。
今回はInvokeFunction
を使います(RunLambdaTask
って何なんだろう?)。
InvokeFunction
のコンストラクタは
aws_cdk.aws_stepfunctions_tasks.InvokeFunction
__init__(lambda_function, *, payload=None)
なので、ここにワークショップ内で作成したLambda関数のインスタンスを指定すれば良さそうです。
以上で、ステートマシンからLambda関数を呼び出す流れが繋がりました!あとはこの通りに実装すれば良さそうです。 とは言え、こんな感じでクラスの継承関係なんかを意識してプログラムを組む必要があるので、 ここはPythonだとちょっと辛いところですね。
Pythonスクリプト
出来上がったプログラムは以下のようになりました。
Taskを3つ作って直列に繋ぐステートマシンにしています。 せっかくPythonでプログラム的に作成できるので、for文でループするようにして作成しました。 またついでにCloudWatch Eventsからこのステートマシンを呼ぶ設定も入れています。
hello_stack.py
#!/usr/bin/env python3 from aws_cdk import core from hello.hello_stack import MyStack app = core.App() MyStack(app, "hello-cdk-1") app.synth()
こちらはワークショップで作成した所から変更なしです。
hello-cdk-1
というスタック名もそのまま使っていますが、実際は変えたほうが良さそうです。
hello_stack.py
from aws_cdk import ( core, aws_lambda, aws_stepfunctions_tasks as aws_tasks, aws_stepfunctions as aws_sf, aws_events_targets as aws_targets, aws_events, ) class MyStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # Lambda関数の作成(ワークショップの流用) my_lambda = aws_lambda.Function( self, 'HelloHandler', runtime=aws_lambda.Runtime.PYTHON_3_7, code=aws_lambda.Code.asset('lambda'), handler='hello.handler', ) # ステートマシン内のTaskステートを作成 # 3つのTaskを作成(Lambda関数はサボって共通) tasks = [] for i in range(3): tasks.append( aws_sf.Task( self, 'TestTask{}'.format(i), task=aws_tasks.InvokeFunction(my_lambda), )) # ステートを連結させて定義作成 definition = tasks[0].next(tasks[1].next(tasks[2])) # ステートマシン全体を作成 state_machine = aws_sf.StateMachine( self, 'TestStateMachine', definition=definition, ) # CloudWatch Eventsのルールを作成 # ステートマシンはaws_targetsを介して渡す rule = aws_events.Rule( self, 'TestRule', schedule=aws_events.Schedule.expression("cron(0 0 * * ? *)"), targets=[aws_targets.SfnStateMachine(state_machine)], )
next
メソッドでステートを連結させて、definition
としています。
こう書くとdefinition
は連結したステート全体の情報を持っていそうに見えますが、
next
メソッドの戻り値は自分自身のインスタンスなので、
先頭の(tasks[0]
の)インスタンスを指しているだけです。
デプロイについて
MFA必須のAWS環境にて検証を行っています。 下の記事でも触れられていますがCDKはまだMFAに対応していないので、 デプロイなどの作業はAssumeRoleをしてから行いました。
時間がたつとセッションが切れてしまって面倒なので、こんなスクリプトを書いておきました。
#!/usr/bin/env bash set -eu aws sts assume-role \ --role-arn arn:aws:iam::123456789012:role/cm-hirano.shigetoshi \ --role-session-name "RoleSession1" \ | jq -cr '.Credentials | {aws_access_key_id: .AccessKeyId, aws_secret_access_key: .SecretAccessKey, aws_session_token: .SessionToken}' \ | tr -d '"{}' | sed 's/,/\n/g' \ | sed 's/:/ = /'
出力をクリップボード経由で~/.aws/credentials
に貼り付けます。
これでMFAが必要なAssumeRole先でもCDKが使えますので、
profileにそのプロファイルを指定してデプロイ実行します。
cdk --profile <上記で得た認証情報のprofile> deploy
作成されたリソース
正常にデプロイされたので、各出力を見ていきます。
Stack
Lamda関数、ステートマシン、ルールの他にIAM関連のリソースも自動的に作成されていることが確認できます。
MFAが必要なロール先での実行
ルール
ステートマシン
想定通り、3つのTaskがシーケンシャルに処理されるステートマシンが出来ています。 JSONでの表示は整形が欲しいですね。。。
Lambda関数
メモリは最小の128MB、タイムアウトは3秒で、どちらもデフォルト値ですね。
実行
マネジメントコンソールから実行して、ちゃんと動きました。 Lambda関数の中は実質空っぽなので、特に何も言うことはないです、はい。
まとめ
CDKのワークショップをPythonでやってみて、 そこを出発点としてStep Functionsの中でLambda関数を呼ぶ実装を行ってみました。
同様のことは、私は今まではServerlessFrameworkで行っていましたが、 CDKでも同じことができることが確認できました。 CDKはまだまだ開発中ですが、やはりAWS公式なものというのは心強いですね。
また、ステートマシン定義に関してはオブジェクト指向でのコーディング感がかなり強かったです。 インフラを構築するという性質上、クラスがはっきりして曖昧さが少ない方がやりやすいなと感じました。 その点ではPythonは少し分が悪く、実際に大きいものを構築する際には、 TypeScriptや場合によってはJavaでの実装なんかの方が良いのかなと思いました。
何にしてもCDKでプログラム的なインフラ作成の一端が体験が出来ました。 これでどんなAWSリソースでも操れるようになったらと思うとワクワクしますね!
以上、誰かの参考になれば幸いです。
CloudFormationの出力
cdk synth
した結果。
出力が長いのでおまけ的に。
Resources: HelloHandlerServiceRole11EF7C63: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: lambda.amazonaws.com Version: "2012-10-17" ManagedPolicyArns: - Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Metadata: aws:cdk:path: hello-cdk-1/HelloHandler/ServiceRole/Resource HelloHandler2E4FBA4D: Type: AWS::Lambda::Function Properties: Code: S3Bucket: Ref: AssetParameters1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55S3BucketCD6A8AF0 S3Key: Fn::Join: - "" - - Fn::Select: - 0 - Fn::Split: - "||" - Ref: AssetParameters1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55S3VersionKey46B59819 - Fn::Select: - 1 - Fn::Split: - "||" - Ref: AssetParameters1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55S3VersionKey46B59819 Handler: hello.handler Role: Fn::GetAtt: - HelloHandlerServiceRole11EF7C63 - Arn Runtime: python3.7 DependsOn: - HelloHandlerServiceRole11EF7C63 Metadata: aws:cdk:path: hello-cdk-1/HelloHandler/Resource aws:asset:path: asset.1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55 aws:asset:property: Code TestStateMachineRole2476F720: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: Fn::Join: - "" - - states. - Ref: AWS::Region - .amazonaws.com Version: "2012-10-17" Metadata: aws:cdk:path: hello-cdk-1/TestStateMachine/Role/Resource TestStateMachineRoleDefaultPolicyB28F488D: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: lambda:InvokeFunction Effect: Allow Resource: Fn::GetAtt: - HelloHandler2E4FBA4D - Arn Version: "2012-10-17" PolicyName: TestStateMachineRoleDefaultPolicyB28F488D Roles: - Ref: TestStateMachineRole2476F720 Metadata: aws:cdk:path: hello-cdk-1/TestStateMachine/Role/DefaultPolicy/Resource TestStateMachine3C216BE3: Type: AWS::StepFunctions::StateMachine Properties: DefinitionString: Fn::Join: - "" - - '{"StartAt":"TestTask0","States":{"TestTask0":{"Next":"TestTask1","Type":"Task","Resource":"' - Fn::GetAtt: - HelloHandler2E4FBA4D - Arn - '"},"TestTask1":{"Next":"TestTask2","Type":"Task","Resource":"' - Fn::GetAtt: - HelloHandler2E4FBA4D - Arn - '"},"TestTask2":{"End":true,"Type":"Task","Resource":"' - Fn::GetAtt: - HelloHandler2E4FBA4D - Arn - '"}}}' RoleArn: Fn::GetAtt: - TestStateMachineRole2476F720 - Arn Metadata: aws:cdk:path: hello-cdk-1/TestStateMachine/Resource TestStateMachineEventsRole092D67D2: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: events.amazonaws.com Version: "2012-10-17" Metadata: aws:cdk:path: hello-cdk-1/TestStateMachine/EventsRole/Resource TestStateMachineEventsRoleDefaultPolicy3C1FFFB2: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: states:StartExecution Effect: Allow Resource: Ref: TestStateMachine3C216BE3 Version: "2012-10-17" PolicyName: TestStateMachineEventsRoleDefaultPolicy3C1FFFB2 Roles: - Ref: TestStateMachineEventsRole092D67D2 Metadata: aws:cdk:path: hello-cdk-1/TestStateMachine/EventsRole/DefaultPolicy/Resource TestRule98A50909: Type: AWS::Events::Rule Properties: ScheduleExpression: cron(0 0 * * ? *) State: ENABLED Targets: - Arn: Ref: TestStateMachine3C216BE3 Id: Target0 RoleArn: Fn::GetAtt: - TestStateMachineEventsRole092D67D2 - Arn Metadata: aws:cdk:path: hello-cdk-1/TestRule/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: Modules: aws-cdk=1.11.0,@aws-cdk/assets=1.12.0,@aws-cdk/aws-apigateway=1.12.0,@aws-cdk/aws-applicationautoscaling=1.12.0,@aws-cdk/aws-autoscaling=1.12.0,@aws-cdk/aws-autoscaling-common=1.12.0,@aws-cdk/aws-autoscaling-hooktargets=1.12.0,@aws-cdk/aws-certificatemanager=1.12.0,@aws-cdk/aws-cloudformation=1.12.0,@aws-cdk/aws-cloudfront=1.12.0,@aws-cdk/aws-cloudwatch=1.12.0,@aws-cdk/aws-codebuild=1.12.0,@aws-cdk/aws-codecommit=1.12.0,@aws-cdk/aws-codepipeline=1.12.0,@aws-cdk/aws-ec2=1.12.0,@aws-cdk/aws-ecr=1.12.0,@aws-cdk/aws-ecr-assets=1.12.0,@aws-cdk/aws-ecs=1.12.0,@aws-cdk/aws-elasticloadbalancing=1.12.0,@aws-cdk/aws-elasticloadbalancingv2=1.12.0,@aws-cdk/aws-events=1.12.0,@aws-cdk/aws-events-targets=1.12.0,@aws-cdk/aws-iam=1.12.0,@aws-cdk/aws-kms=1.12.0,@aws-cdk/aws-lambda=1.12.0,@aws-cdk/aws-logs=1.12.0,@aws-cdk/aws-route53=1.12.0,@aws-cdk/aws-route53-targets=1.12.0,@aws-cdk/aws-s3=1.12.0,@aws-cdk/aws-s3-assets=1.12.0,@aws-cdk/aws-secretsmanager=1.12.0,@aws-cdk/aws-servicediscovery=1.12.0,@aws-cdk/aws-sns=1.12.0,@aws-cdk/aws-sns-subscriptions=1.12.0,@aws-cdk/aws-sqs=1.12.0,@aws-cdk/aws-ssm=1.12.0,@aws-cdk/aws-stepfunctions=1.12.0,@aws-cdk/aws-stepfunctions-tasks=1.12.0,@aws-cdk/core=1.12.0,@aws-cdk/cx-api=1.12.0,@aws-cdk/region-info=1.12.0,jsii-runtime=Python/3.7.3 Condition: CDKMetadataAvailable Parameters: AssetParameters1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55S3BucketCD6A8AF0: Type: String Description: S3 bucket for asset "1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55" AssetParameters1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55S3VersionKey46B59819: Type: String Description: S3 key for asset version "1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55" AssetParameters1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55ArtifactHash4BEEDF62: Type: String Description: Artifact hash for asset "1b719fe464cc244bb98b1bbe1507b0006e85c1b60dd7ff12479b984d03ab8f55" Conditions: CDKMetadataAvailable: Fn::Or: - Fn::Or: - Fn::Equals: - Ref: AWS::Region - ap-east-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-2 - Fn::Equals: - Ref: AWS::Region - ap-south-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-2 - Fn::Equals: - Ref: AWS::Region - ca-central-1 - Fn::Equals: - Ref: AWS::Region - cn-north-1 - Fn::Equals: - Ref: AWS::Region - cn-northwest-1 - Fn::Equals: - Ref: AWS::Region - eu-central-1 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - eu-north-1 - Fn::Equals: - Ref: AWS::Region - eu-west-1 - Fn::Equals: - Ref: AWS::Region - eu-west-2 - Fn::Equals: - Ref: AWS::Region - eu-west-3 - Fn::Equals: - Ref: AWS::Region - me-south-1 - Fn::Equals: - Ref: AWS::Region - sa-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-2 - Fn::Equals: - Ref: AWS::Region - us-west-1 - Fn::Equals: - Ref: AWS::Region - us-west-2
その他参照情報
- AWS CDK + Pythonで、ネストした AWS StepFunctions のワークフローを作ってみた
- すでにはるかに高度なことをやられている方がいたので、大変参考になりました!