この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、平野です。
巷で話題の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 のワークフローを作ってみた
- すでにはるかに高度なことをやられている方がいたので、大変参考になりました!