AWS CDKでAWS SAMのようにリソースを定義してみた

AWS CDKでもAWS SAMのようにリソースを定義したいという要望を叶えてくれるモジュールが追加されていました。その名は "aws-cdk-lib.aws_sam module"
2022.02.16

AWS CDKでもAWS SAMのようにリソースを定義したい時もある

こんにちは、のんピ(@non____97)です。

皆さんはAWS CDKを使いたい。でも、AWS SAMのようにリソースを定義したいと思ったことはありますか? 私はあります。

例えば、AWS Step Functionsのステートマシンを定義するとき、AWS CDKでは以下のようにTypeScriptやPythonなどのプログラミング言語で定義する必要があります。

AWS CDKでAWS Step Functionsのステートマシンを定義する場合

import {
  Stack,
  StackProps,
  Duration,
  aws_lambda as lambda,
  aws_stepfunctions as sfn,
  aws_stepfunctions_tasks as tasks,
} from "aws-cdk-lib";
import { Construct } from "constructs";

declare const submitLambda: lambda.Function;
declare const getStatusLambda: lambda.Function;

export class AwsSamStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const submitJob = new tasks.LambdaInvoke(this, "Submit Job", {
      lambdaFunction: submitLambda,
      // Lambda's result is in the attribute `Payload`
      outputPath: "$.Payload",
    });

    const waitX = new sfn.Wait(this, "Wait X Seconds", {
      time: sfn.WaitTime.secondsPath("$.waitSeconds"),
    });

    const getStatus = new tasks.LambdaInvoke(this, "Get Job Status", {
      lambdaFunction: getStatusLambda,
      // Pass just the field named "guid" into the Lambda, put the
      // Lambda's result in a field called "status" in the response
      inputPath: "$.guid",
      outputPath: "$.Payload",
    });

    const jobFailed = new sfn.Fail(this, "Job Failed", {
      cause: "AWS Batch Job Failed",
      error: "DescribeJob returned FAILED",
    });

    const finalStatus = new tasks.LambdaInvoke(this, "Get Final Job Status", {
      lambdaFunction: getStatusLambda,
      // Use "guid" field as input
      inputPath: "$.guid",
      outputPath: "$.Payload",
    });

    const definition = submitJob
      .next(waitX)
      .next(getStatus)
      .next(
        new sfn.Choice(this, "Job Complete?")
          // Look at the "status" field
          .when(sfn.Condition.stringEquals("$.status", "FAILED"), jobFailed)
          .when(
            sfn.Condition.stringEquals("$.status", "SUCCEEDED"),
            finalStatus
          )
          .otherwise(waitX)
      );

    new sfn.StateMachine(this, "StateMachine", {
      definition,
      timeout: Duration.minutes(5),
    });
  }
}

管理したいステートマシンのワークフローが複雑でない場合は問題ありませんが、分岐やループ、並列処理が複数あると、定義するのがなかなか大変です。

一方、AWS SAMではステートマシンを定義する際に、ASL形式で記述されたワークフローを読み込むことができます。これにより、AWS Step Functions Workflow Studioで設計したワークフローを流用することが出来ます。

AWS SAMでAWS Step Functionsのステートマシンを定義する場合

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  StateMachineName:
    Description: Please type the Step Functions StateMachine Name.
    Type: String
    Default: 'sfn-sam-app-statemachine'
  LambdaFunctionName:
    Description: Please type the Lambda Function Name.
    Type: String
    Default: 'sfn-sam-app-function'
Resources:
  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${LambdaFunctionName}
      CodeUri: functions/hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Timeout: 60
      Role: !GetAtt LambdaFunctionRole.Arn
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSStepFunctionsReadOnlyAccess
  StateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: !Sub ${StateMachineName}
      DefinitionUri: statemachine/sfn.asl.json
      DefinitionSubstitutions:
        LambdaFunction: !GetAtt LambdaFunction.Arn
      Role: !GetAtt StateMachineRole.Arn
      Logging:
        Level: ALL
        IncludeExecutionData: True
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt StateMachineLogGroup.Arn
  StateMachineLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName : !Join [ "", [ '/aws/states/', !Sub '${StateMachineName}', '-Logs' ] ]
  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess

抜粋 : Step Functionsの構築はAWS SAMを利用すると捗りそうです

そんなある時、AWS CDKでもAWS Step Functions Workflow Studioで設計したワークフローを読みこんで、ステートマシンを定義したいなと思い、AWS CDKのAPI Referenceを漁っていると、見つけてしまいました。

aws-cdk-lib.aws_sam module

そこで今回は、aws-cdk-lib.aws_sam moduleを紹介します。

aws-cdk-lib.aws_sam module とは

2021/2/16時点の最新のAWS CDK v2.12.0 ではこのモジュールはプレビューです。ご注意ください

aws-cdk-lib.aws_sam moduleはAWS SAMのConstructライブラリーです。

v2.12.0 時点ではL2 Constructはなく、L1 Constructのみとなっています。そのため、基本的にはAWS SAMのテンプレートを書くときと同じような書き味になります。

L1 ConstructとAWS SAMのTypeとの対応は以下の通りです。

L1 Construct AWS SAM Type
CfnApi AWS::Serverless::Api
CfnApplication AWS::Serverless::Application
CfnFunction AWS::Serverless::Function
CfnHttpApi AWS::Serverless::HttpApi
CfnLayerVersion AWS::Serverless::LayerVersion
CfnSimpleTable AWS::Serverless::SimpleTable
CfnStateMachine AWS::Serverless::StateMachine

やってみた

ワークフローの設計

それではaws-cdk-lib.aws_sam moduleを使って、実際にステートマシンを作成してみます。

まずはワークフローの設計を行います。

Step Functionsのコンソールより、ステートマシン - ステートマシンの作成をクリックします。

ステートマシンの作成

ワークフローを視覚的に設計を選択して、次へをクリックします。

ワークフローを視覚的に設計

AWS Step Functions Workflow Studioでステートマシンのワークフローを設計します。設計後は定義をクリックし、生成された定義(ASL形式)を控えておきます。

AWS Step Functions Workflow Studioで生成された定義(ASL形式)をコピー

生成された定義は以下になります。

{
  "Comment": "A description of my state machine",
  "StartAt": "Pass 1",
  "States": {
    "Pass 1": {
      "Type": "Pass",
      "Next": "Wait"
    },
    "Wait": {
      "Type": "Wait",
      "Seconds": 1,
      "Next": "Parallel 1"
    },
    "Parallel 1": {
      "Type": "Parallel",
      "Branches": [
        {
          "StartAt": "Pass 2",
          "States": {
            "Pass 2": {
              "Type": "Pass",
              "Next": "Parallel 2"
            },
            "Parallel 2": {
              "Type": "Parallel",
              "Branches": [
                {
                  "StartAt": "Pass 4",
                  "States": {
                    "Pass 4": {
                      "Type": "Pass",
                      "End": true
                    }
                  }
                },
                {
                  "StartAt": "Pass 5",
                  "States": {
                    "Pass 5": {
                      "Type": "Pass",
                      "End": true
                    }
                  }
                }
              ],
              "Next": "Pass 6"
            },
            "Pass 6": {
              "Type": "Pass",
              "End": true
            }
          }
        },
        {
          "StartAt": "Pass 3",
          "States": {
            "Pass 3": {
              "Type": "Pass",
              "End": true
            }
          }
        }
      ],
      "Next": "Choice"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$",
          "IsPresent": true,
          "Next": "Success"
        }
      ],
      "Default": "Fail"
    },
    "Success": {
      "Type": "Succeed"
    },
    "Fail": {
      "Type": "Fail"
    }
  }
}

AWS CDKでステートマシンをデプロイ

それでは、AWS CDKでステートマシンをデプロイします。

まず、先程生成された定義を保存します。今回は、./src/stepFunctions/workflow.asl.jsonに保存しました。

.
├── .gitignore
├── .npmignore
├── README.md
├── bin
│   └── aws-sam.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── aws-sam-stack.ts
├── package-lock.json
├── package.json
├── src
│   └── stepFunctions
│       └── workflow.asl.json ←生成された定義のファイル
├── test
│   └── aws-sam.test.ts
└── tsconfig.json

次に、生成された定義を読み込むようにステートマシンを定義します。

定義する際は、API ReferenceAWS::Serverless::StateMachineのドキュメントを確認しながら行います。

なお、生成された定義を読み込む際は、ローカルに存在する定義を直接読み込むことはできず、S3バケットに一度アップロードする必要があります。S3バケットに定義ファイルをアップロードする処理もAWS CDKで行います。

実際のコードは以下の通りです。

./lib/aws-sam-stack.ts

import {
  Fn,
  Stack,
  StackProps,
  aws_s3 as s3,
  aws_s3_deployment as s3deploy,
  aws_logs as logs,
  aws_iam as iam,
  aws_sam as sam,
} from "aws-cdk-lib";
import { Construct } from "constructs";

export class AwsSamStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const stackUniqueId = Fn.select(2, Fn.split("/", this.stackId));

    // S3 buckets to store AWS Step Function Workflow
    const sfnWorkflowBucket = new s3.Bucket(this, "SfnWorkflowBucket", {
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: new s3.BlockPublicAccess({
        blockPublicAcls: true,
        blockPublicPolicy: true,
        ignorePublicAcls: true,
        restrictPublicBuckets: true,
      }),
    });

    // Deploy AWS Step Function Workflow
    new s3deploy.BucketDeployment(this, "DeployFilesToSfnWorkflowBucket", {
      sources: [
        s3deploy.Source.asset("./src/stepFunctions/", {
          exclude: [".DS_Store"],
        }),
      ],
      destinationBucket: sfnWorkflowBucket,
    });

    // CloudWatch Logs for State Machine Logs
    const stateMachineLogGroup = new logs.LogGroup(
      this,
      "StateMachineLogGroup",
      {
        logGroupName: `/aws/vendedlogs/states/CfnStateMachine-${stackUniqueId}-Logs`,
        retention: logs.RetentionDays.TWO_WEEKS,
      }
    );

    // IAM Role for State Machine
    const stateMachineIamRole = new iam.Role(this, "StateMachineIamRole", {
      assumedBy: new iam.ServicePrincipal("states.amazonaws.com"),
      managedPolicies: [
        new iam.ManagedPolicy(this, "CloudWatchLogsDeliveryFullAccessPolicy", {
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              resources: ["*"],
              actions: [
                "logs:CreateLogDelivery",
                "logs:GetLogDelivery",
                "logs:UpdateLogDelivery",
                "logs:DeleteLogDelivery",
                "logs:ListLogDeliveries",
                "logs:PutResourcePolicy",
                "logs:DescribeResourcePolicies",
                "logs:DescribeLogGroups",
              ],
            }),
          ],
        }),
        new iam.ManagedPolicy(this, "XRayAccessPolicy", {
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              resources: ["*"],
              actions: [
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords",
                "xray:GetSamplingRules",
                "xray:GetSamplingTargets",
              ],
            }),
          ],
        }),
      ],
    });

    // AWS Step Functions State Machine
    new sam.CfnStateMachine(this, "CfnStateMachine", {
      definitionSubstitutions: {
        definitionSubstitutionsKey: "definitionSubstitutions",
      },
      definitionUri: {
        bucket: sfnWorkflowBucket.bucketName,
        key: "workflow.asl.json",
      },
      logging: {
        destinations: [
          {
            cloudWatchLogsLogGroup: {
              logGroupArn: stateMachineLogGroup.logGroupArn,
            },
          },
        ],
        includeExecutionData: true,
        level: "ALL",
      },
      name: "CfnStateMachine",
      role: stateMachineIamRole.roleArn,
      tags: {
        Name: "CfnStateMachine",
      },
      tracing: {
        enabled: true,
      },
      type: "STANDARD",
    });
  }
}

準備ができたら、npx cdk deployでステートマシンなど各種リソースをデプロイします。

Step Functionsのコンソールを確認すると、AWS Step Functions Workflow Studioで設計したワークフローのステートマシンが作成されたことが確認できます。

作成されたステートマシンとワークフローの確認

こちらのステートマシンを実行してみます。

エラーは発生せず、正常に終了しました。

ステートマシンの実行結果の確認

X-Rayトレースマップも正常に確認できます。

X-Rayトレースマップの確認

CloudWatch Logsへのログ出力も正常にされていることが確認できます。

CloudWatch Logsへのログ出力の確認

AWS CDKがますます便利になっていく

AWS CDKでもAWS SAMのようにリソースを定義したいという要望を叶えてくれるモジュール aws-cdk-lib.aws_sam module を紹介しました。

上手く使えばAWS CDKとAWS SAMの良いとこ取りのような形になるので、こちらのモジュールが早くGAになることを楽しみに待っています。

また、今回のデモで使ったコードは以下リポジトリにアップロードしています。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!