AWS CDKワークショップに入門して、Step Functionsステートマシンを実装してみた

こんにちは、平野です。

巷で話題のCDK(AWS Cloud Development Kit)、試してみたいなーと思ったので試しました(自己完結)。 題材として、最近ちょっと触っているStep FunctionsでLambda関数を実行するところまでを行いました。 言語はまあまあ使い慣れているPythonで行っています。

まず手始めに、下記のワークショップを実践しました。

https://cdkworkshop.com/30-python.html

CDK気になるけど何からすればいいの?という人は、 とりあえずこれの通りにやるのが一番早いかと思います。 もちろんやってみた記事もありますので、こちらの記事もご参照ください! (こちらはTypeScriptです)

【コードでインフラ定義】CDKという異次元体験をさくっとやるのに便利なAWS公式Workshopの紹介

この記事では、上記ワークショップで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)

ワークショップの流れを見れば、scopeselfidはリソース名を入れれば良さそうなので、 残りで必須なものはdefinitionとなります。 definitionIChainableインターフェイスのものが指定できるようです。 最初ここで、IChainableって何??となってだいぶ行き詰まってしまったのですが、CDKにはJavaのリファレンスも用意されていて、 基本的に構成は同じだろうということで、Javaの方を見てみると、

Interface IChainable

IChainableの実装としてStateなどがあることがわかりました。

ということでStateインスタンスを作成しようとしたのですが、こちらは抽象クラスでした。 TaskParallelなど、Step FunctionsでTypeとして指定するものが具象クラスになっているようです。

Class State

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インターフェイスを指定します。

Interface IStepFunctionsTask

こちらはInvokeFunctionPublishToTopicなど、下記の選択肢に相当する部分です。

今回は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をしてから行いました。

AWS CDK(Python)を使って繰り返し処理でEC2インスタンスを作成してみる

時間がたつとセッションが切れてしまって面倒なので、こんなスクリプトを書いておきました。

#!/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

その他参照情報