AWS Step Functions ワークフローの構築で AWS CDK の Hotswap deployments を使ってみた

2023.07.11

こんにちは、CX事業本部 Delivery部の若槻です。

cdk deploy で Hotswap deployments を使用すると、CloudFormation を介さずに変更をデプロイできるため、デプロイを高速化することができます。

この Hotswap では、Lambda functions や S3 Bucket Deployments だけでなく、AWS Step Functions State Machines の定義変更もサポートしています。

  • Definition changes of AWS Step Functions State Machines.

そこで今回は、AWS Step Functions ワークフローの構築で AWS CDK の Hotswap deployments を使ってみました。

試してみた

最初の CDK コード

最初に次のような定義の State Machine を作成します。

lib/cdk-sample-stack.ts

import { aws_stepfunctions, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  public readonly myFileObjectKey: string;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    new aws_stepfunctions.StateMachine(this, 'MyStateMachine', {
      stateMachineName: 'MyStateMachine',
      stateMachineType: aws_stepfunctions.StateMachineType.EXPRESS,
      definitionBody: aws_stepfunctions.ChainDefinitionBody.fromChainable(
        new aws_stepfunctions.Pass(this, 'MyPassState', {
          result: aws_stepfunctions.Result.fromObject({ myKey: 'myValue' }),
        })
      ),
    });
  }
}

CDK デプロイします。

cdk deploy

デプロイされたステートマシンを同期実行すると結果を取得できました。正常にデプロイが行われています。

$ aws stepfunctions start-sync-execution \
  --state-machine-arn ${stateMachineArn} \
  --query output | jq -r .
{"myKey":"myValue"}

通常の変更デプロイ

続いて、ワークフローの定義を変更してみます。

lib/cdk-sample-stack.ts

import { aws_stepfunctions, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  public readonly myFileObjectKey: string;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    new aws_stepfunctions.StateMachine(this, 'MyStateMachine', {
      stateMachineName: 'MyStateMachine',
      stateMachineType: aws_stepfunctions.StateMachineType.EXPRESS,
      definitionBody: aws_stepfunctions.ChainDefinitionBody.fromChainable(
        new aws_stepfunctions.Pass(this, 'MyPassState', {
          result: aws_stepfunctions.Result.fromObject({ myKey: 'myValue2' }),
        })
      ),
    });
  }
}

CDK デプロイを行うと、完了まで約 20 秒を要しました。

cdk deploy

  Synthesis time: 2.84s

CdkSampleStack:  start: Building ed610dcd08cc23e5160d1c309f5a3d26c864aded667a1aae35904e95619ebfc3:current_account-current_region
CdkSampleStack:  success: Built ed610dcd08cc23e5160d1c309f5a3d26c864aded667a1aae35904e95619ebfc3:current_account-current_region
CdkSampleStack:  start: Publishing ed610dcd08cc23e5160d1c309f5a3d26c864aded667a1aae35904e95619ebfc3:current_account-current_region
CdkSampleStack:  success: Published ed610dcd08cc23e5160d1c309f5a3d26c864aded667a1aae35904e95619ebfc3:current_account-current_region
CdkSampleStack: deploying... [1/1]
CdkSampleStack: creating CloudFormation changeset...

   CdkSampleStack

  Deployment time: 17.39s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367

  Total time: 20.23s

当然ですが、ワークフローを実行するとデプロイにより更新されていることが分かります。

$ aws stepfunctions start-sync-execution \
  --state-machine-arn ${stateMachineArn} \
  --query output | jq -r .
{"myKey":"myValue"}

--hotswap による変更デプロイ

続いて今回の本題である Hotswap deployments を試してみます。

ワークフローの定義をさらに変更します。また、Hotswap の対象とならないリソースとして、AWS SSM Parameter を追加します。

lib/cdk-sample-stack.ts

import { aws_stepfunctions, aws_ssm, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  public readonly myFileObjectKey: string;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // ステートマシン以外のリソースを定義
    new aws_ssm.StringParameter(this, 'MyParameter', {
      parameterName: 'MyParameter',
      stringValue: 'MyParameterValue',
    });

    new aws_stepfunctions.StateMachine(this, 'MyStateMachine', {
      stateMachineName: 'MyStateMachine',
      stateMachineType: aws_stepfunctions.StateMachineType.EXPRESS,
      definitionBody: aws_stepfunctions.ChainDefinitionBody.fromChainable(
        new aws_stepfunctions.Pass(this, 'MyPassState', {
          result: aws_stepfunctions.Result.fromObject({ myKey: 'myValue3' }),
        })
      ),
    });
  }
}

ローカルのスタック定義と CloudFormation スタックの差分を確認します。

$ cdk diff
Stack CdkSampleStack
Resources
[+] AWS::SSM::Parameter MyParameter MyParameter18BA547D 
[~] AWS::StepFunctions::StateMachine MyStateMachine MyStateMachine6C968CA5 
 └─ [~] DefinitionString
     ├─ [-] {"StartAt":"MyPassState","States":{"MyPassState":{"Type":"Pass","Result":{"myKey":"myValue2"},"End":true}}}
     └─ [+] {"StartAt":"MyPassState","States":{"MyPassState":{"Type":"Pass","Result":{"myKey":"myValue3"},"End":true}}}

--hotswap を使用して Hotswap deployments を行います。すると約 4 秒でデプロイされました。デプロイが高速化されていますね。

$ cdk deploy --hotswap

  Synthesis time: 3.2s

 The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments
 They should only be used for development - never use them for your production Stacks!

CdkSampleStack:  start: Building 661e5f2cafaf455ab59361db85609648b9bcdd5272f9a6366e8972228089d215:current_account-current_region
CdkSampleStack:  success: Built 661e5f2cafaf455ab59361db85609648b9bcdd5272f9a6366e8972228089d215:current_account-current_region
CdkSampleStack:  start: Publishing 661e5f2cafaf455ab59361db85609648b9bcdd5272f9a6366e8972228089d215:current_account-current_region
CdkSampleStack:  success: Published 661e5f2cafaf455ab59361db85609648b9bcdd5272f9a6366e8972228089d215:current_account-current_region
CdkSampleStack: deploying... [1/1]

 hotswapping resources:
    AWS::StepFunctions::StateMachine 'MyStateMachine'
 AWS::StepFunctions::StateMachine 'MyStateMachine' hotswapped!

   CdkSampleStack

  Deployment time: 0.96s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367

  Total time: 4.16s

再度差分を確認すると、Hotswap deployments 前と差分に変更がなく、CloudFormation API ではなく AWS SDK を使用して直接 ワークフロー定義の更新が行われていることが分かります。

$ cdk diff            
Stack CdkSampleStack
Resources
[+] AWS::SSM::Parameter MyParameter MyParameter18BA547D 
[~] AWS::StepFunctions::StateMachine MyStateMachine MyStateMachine6C968CA5 
 └─ [~] DefinitionString
     ├─ [-] {"StartAt":"MyPassState","States":{"MyPassState":{"Type":"Pass","Result":{"myKey":"myValue2"},"End":true}}}
     └─ [+] {"StartAt":"MyPassState","States":{"MyPassState":{"Type":"Pass","Result":{"myKey":"myValue3"},"End":true}}}

ワークフローを実行すると Hotswap deployments によりワークフロー定義が更新されていることが分かります。

$ aws stepfunctions start-sync-execution \
  --state-machine-arn ${stateMachineArn} \
  --query output | jq -r .
{"myKey":"myValue3"}

--hotswap-fallback による変更デプロイ

--hotswap-fallback を使用すると、Hotswap deployments をサポートしていないリソースの変更を検知した場合に、Hotswap deployments ではなく通常の変更デプロイが行われます。検知されなかった場合は Hotswap deployments となります。

先程から CDK 定義を変更せずに、--hotswap-fallback を使用してデプロイを行います。すると通常のデプロイとなりました。

$ cdk deploy --hotswap-fallback

  Synthesis time: 3.44s

 The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments
 They should only be used for development - never use them for your production Stacks!

CdkSampleStack: deploying... [1/1]

 The following non-hotswappable changes were found:
    logicalID: MyParameter18BA547D, type: AWS::SSM::Parameter, reason: resource 'MyParameter18BA547D' was created by this deployment

Could not perform a hotswap deployment, as the stack CdkSampleStack contains non-Asset changes
Falling back to doing a full deployment
CdkSampleStack: creating CloudFormation changeset...

   CdkSampleStack

  Deployment time: 18.08s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367

  Total time: 21.53s

CloudFormation によるデプロイが行われたので、スタックの差分も解消されています。

$ cdk diff                                
Stack CdkSampleStack
There were no differences

その他の検証

ワークフローの定義以外の変更があった場合

ワークフローの定義以外の変更は、Hotswap deployments の対象とならないことを確認してみます。

ワークフローの定義を変更し、またログ出力の設定を行います。

lib/cdk-sample-stack.ts

    new aws_stepfunctions.StateMachine(this, 'MyStateMachine', {
      stateMachineName: 'MyStateMachine',
      stateMachineType: aws_stepfunctions.StateMachineType.EXPRESS,
      definitionBody: aws_stepfunctions.ChainDefinitionBody.fromChainable(
        new aws_stepfunctions.Pass(this, 'MyPassState', {
          result: aws_stepfunctions.Result.fromObject({ myKey: 'myValue4' }),
        })
      ),
      logs: {
        destination: new aws_logs.LogGroup(this, 'MyStateMachineLogGroup'),
        level: aws_stepfunctions.LogLevel.ALL,
      },
    });

Hotswap deployments を行うと、rejected changes: LoggingConfiguration, reason: resource properties 'LoggingConfiguration' are not hotswappable on this resource type とありログ出力の設定は変更されず、ワークフロー定義の変更のみデプロイされました。

$ cdk deploy --hotswap

  Synthesis time: 3.52s

 The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments
 They should only be used for development - never use them for your production Stacks!

CdkSampleStack:  start: Building 0517e4b9f2d793ea4d9c11e823cdd2357ed4cae41a133006bdfc21dc894c8dbe:current_account-current_region
CdkSampleStack:  success: Built 0517e4b9f2d793ea4d9c11e823cdd2357ed4cae41a133006bdfc21dc894c8dbe:current_account-current_region
CdkSampleStack:  start: Publishing 0517e4b9f2d793ea4d9c11e823cdd2357ed4cae41a133006bdfc21dc894c8dbe:current_account-current_region
CdkSampleStack:  success: Published 0517e4b9f2d793ea4d9c11e823cdd2357ed4cae41a133006bdfc21dc894c8dbe:current_account-current_region
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬──────────┬────────┬─────────────────────────────────┬─────────────────────────────────┬───────────┐
│   │ Resource │ Effect │ Action                          │ Principal                       │ Condition │
├───┼──────────┼────────┼─────────────────────────────────┼─────────────────────────────────┼───────────┤
│ + │ *        │ Allow  │ logs:CreateLogDelivery          │ AWS:${MyStateMachine/Role}      │           │
│   │          │        │ logs:DeleteLogDelivery          │                                 │           │
│   │          │        │ logs:DescribeLogGroups          │                                 │           │
│   │          │        │ logs:DescribeResourcePolicies   │                                 │           │
│   │          │        │ logs:GetLogDelivery             │                                 │           │
│   │          │        │ logs:ListLogDeliveries          │                                 │           │
│   │          │        │ logs:PutResourcePolicy          │                                 │           │
│   │          │        │ logs:UpdateLogDelivery          │                                 │           │
└───┴──────────┴────────┴─────────────────────────────────┴─────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
CdkSampleStack: deploying... [1/1]

 The following non-hotswappable changes were found. To reconcile these using CloudFormation, specify --hotswap-fallback
    logicalID: MyStateMachine6C968CA5, type: AWS::StepFunctions::StateMachine, rejected changes: LoggingConfiguration, reason: resource properties 'LoggingConfiguration' are not hotswappable on this resource type


 hotswapping resources:
    AWS::StepFunctions::StateMachine 'MyStateMachine'
 AWS::StepFunctions::StateMachine 'MyStateMachine' hotswapped!

   CdkSampleStack

  Deployment time: 1.11s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367

  Total time: 4.63s

念のためワークフローを実行すると、変更後の値 myValue4 が出力され、変更されていることが分かります。

$ aws stepfunctions start-sync-execution \
  --state-machine-arn ${stateMachineArn} \
  --query output | jq -r .
{"myKey":"myValue4"}

ワークフローの定義の変更でワークフロー以外のリソースの変更があった場合

ワークフローの定義の変更でワークフロー以外のリソースの変更があった場合のパターンも試してみます。

ワークフローの定義で SNS Topic への Publish を行うように変更し、Publish 先の SNS Topic を定義します。

lib/cdk-sample-stack.ts

import {
  aws_stepfunctions,
  aws_stepfunctions_tasks,
  aws_sns,
  Stack,
  StackProps,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  public readonly myFileObjectKey: string;

  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const myTopic = new aws_sns.Topic(this, 'MyTopic', {
      topicName: 'MyTopic',
    });

    new aws_stepfunctions.StateMachine(this, 'MyStateMachine', {
      stateMachineName: 'MyStateMachine',
      stateMachineType: aws_stepfunctions.StateMachineType.EXPRESS,
      definitionBody: aws_stepfunctions.ChainDefinitionBody.fromChainable(
        new aws_stepfunctions_tasks.SnsPublish(this, 'MyTask', {
          topic: myTopic,
          message: aws_stepfunctions.TaskInput.fromJsonPathAt('$.message'),
        })
      ),
    });
  }
}

Hotswap deployments を行うと、rejected changes: IAM, reason: resource 'MyTopic86869434' was created by this deployment とあり、Hotswap の対象とならないリソースの解決が上手くできていないようです。

$ cdk deploy --hotswap

  Synthesis time: 3.09s

 The --hotswap and --hotswap-fallback flags deliberately introduce CloudFormation drift to speed up deployments
 They should only be used for development - never use them for your production Stacks!

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────┬────────┬─────────────┬────────────────────────────┬───────────┐
│   │ Resource   │ Effect │ Action      │ Principal                  │ Condition │
├───┼────────────┼────────┼─────────────┼────────────────────────────┼───────────┤
│ + │ ${MyTopic} │ Allow  │ sns:Publish │ AWS:${MyStateMachine/Role} │           │
└───┴────────────┴────────┴─────────────┴────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
CdkSampleStack: deploying... [1/1]

 hotswapping resources:
    AWS::StepFunctions::StateMachine 'MyStateMachine'
Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: Parameter or resource 'MyTopic86869434' could not be found for evaluation

   CdkSampleStack (no changes)

  Deployment time: 1.16s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367

  Total time: 4.26s

ワークフローの定義の変更であっても別のリソースの変更が絡む場合は Hotswap deployments が上手く動かない場合があるようです。

おわりに

AWS Step Functions ワークフローの構築で AWS CDK の Hotswap deployments を使ってみました。

ワークフロー定義の変更のデプロイを大きく高速化できることが確認できました。

一方で、ワークフロー定義の変更でワークフロー以外のリソースの変更が絡む場合は Hotswap deployments が上手く動かない場合があるようなので、そういう場合は下記のエントリで紹介している --method=direct を使用してもデプロイを十分高速化できるので併用をすると良いかと思います。

以上