🗂

cdk migrateを使ってみる

2023/11/18に公開

cdk migrateを使ってみる

cdk migrate自体の詳しくはこちらをご参照ください。

※本ページ記載の内容は2023年11月18日時点での挙動です。

今回サンプルとするテンプレート

実際にcdk migrateの対象とするスタック作成の元となるテンプレートは、過去のテンプレからリージョンを跨いで連携してるようなものを適当に選びました。

TGWとかRAMがリソースとして入っていますが、そこは今回重要でなく、サンプルですので気にせずでOKです。

一応参照元(開かず飛ばして問題なしです)

今回cdk migrateで取り込んでみるテンプレは↓のような経緯で作成したものから適当に選びました。
https://zenn.dev/devcamp/articles/757693dad0fbf8

TGWアカウント挟んでアカウントAとアカウントBがアカウント跨ぎリージョン跨ぎで通信出来るようにするテンプレートのうち、TGWアカウントで作成する東京とヴァージニアのテンプレートを引っ張ってきただけのものです。(図2の①と②のテンプレート)

※これらは工程の一部で使っていたものでこれだけでは特段意味をなしません。

[図1]

[図2]

リージョンが違えばスタック名は重複可能な為、どちらも"sample"という名前でスタックを作成してみます。

サンプルテンプレA(ap-northeast-1に作成する)
AWSTemplateFormatVersion: "2010-09-09"

Parameters: 

  PJPrefix: # リソース名やNameタグ値の接頭辞に利用
    Type: String

  Environment: # 同上(環境名)
    Type: String
    Default: prd

  AccountAId:
    Type: String

Resources: 
  # ------------------------------------------------------------#
  # RAM
  # ------------------------------------------------------------#
  ResourceShare:
    Type: AWS::RAM::ResourceShare
    DependsOn: 
      - TransitGateway
    Properties: 
      Name: !Sub ${PJPrefix}-${Environment}-ram
      ResourceArns:
        - !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/${TransitGateway}
      Principals:
        - !Ref AccountAId
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-${Environment}-ram-for-account-a
  # ------------------------------------------------------------#
  # TransitGateway
  # ------------------------------------------------------------#
  TransitGateway:
    Type: AWS::EC2::TransitGateway
    Properties:
      AmazonSideAsn: 64512 # デフォルト値
      AutoAcceptSharedAttachments: enable
      DefaultRouteTableAssociation: enable
      DefaultRouteTablePropagation: enable
      DnsSupport: enable
      VpnEcmpSupport: enable
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-${Environment}-transitgateway-for-account-a

Outputs:
  TransitGatewayId:
    Value: !Ref TransitGateway

  PeerRegion:
    Value: !Ref AWS::Region
  
  PeerTransitGatewayId:
    Value: !Ref TransitGateway

サンプルテンプレB(us-east-1に作成する)
AWSTemplateFormatVersion: "2010-09-09"

Parameters: 

  PJPrefix: # リソース名やNameタグ値の接頭辞に利用
    Type: String

  Environment: # 同上(環境名)
    Type: String
    Default: prd

  AccountBId: 
    Type: String

  PeerRegion: # 1stリージョンのスタックの同名出力値を入力
    Type: String

  PeerTransitGatewayId: # 1stリージョンのスタックの同名出力値を入力
    Type: String

Resources: 
  # ------------------------------------------------------------#
  # RAM
  # ------------------------------------------------------------#
  ResourceShare:
    Type: AWS::RAM::ResourceShare
    DependsOn: 
      - TransitGateway
    Properties: 
      Name: !Sub ${PJPrefix}-${Environment}-ram
      ResourceArns:
        - !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:transit-gateway/${TransitGateway}
      Principals:
        - !Ref AccountBId
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-${Environment}-ram-for-account-b
  # ------------------------------------------------------------#
  # TransitGateway
  # ------------------------------------------------------------#
  TransitGateway:
    Type: AWS::EC2::TransitGateway
    Properties:
      AmazonSideAsn: 64512 # デフォルト値
      AutoAcceptSharedAttachments: enable
      DefaultRouteTableAssociation: enable
      DefaultRouteTablePropagation: enable
      DnsSupport: enable
      VpnEcmpSupport: enable
      Tags:
        - Key: Name
          Value: !Sub ${PJPrefix}-${Environment}-transitgateway-for-account-b

  TransitGatewayPeeringAttachment:
    Type: AWS::EC2::TransitGatewayPeeringAttachment
    Properties:
      PeerAccountId: !Ref AWS::AccountId
      PeerRegion: !Ref PeerRegion
      PeerTransitGatewayId: !Ref PeerTransitGatewayId
      TransitGatewayId: !Ref TransitGateway

Outputs:
  TransitGatewayId:
    Value: !Ref TransitGateway


やってみる

cdk migrateが動作するように最新バージョンにしておきます。(migrate機能が追加される前のヴァージョンのまま先に進まないように)

$ npm install -g aws-cdk

changed 2 packages in 2s


フォルダを作成し、中に入っておきます。

$ mkdir cdk_migrate_test && cd cdk_migrate_test

サンプルテンプレAから作ったスタックをcdk migrate
cdk migrate --stack-name sample --language typescript --from-stack --account [アカウントのID] --region ap-northeast-1
This is an experimental feature and development on it is still in progress. We make no guarantees about the outcome or stability of the functionality.
 ⏳  Generating CDK app for sample...
Applying project template app for typescript
Executing npm install...
✅ All done!
サンプルテンプレBから作ったスタックをcdk migrate
cdk migrate --stack-name sample --language typescript --from-stack --account [アカウントのID] --region us-east-1
This is an experimental feature and development on it is still in progress. We make no guarantees about the outcome or stability of the functionality.
 ⏳  Generating CDK app for sample...
Applying project template app for typescript
Executing npm install...
✅ All done!

ローカルの同ディレクトリに同名スタックをcdk migrateした場合

あえて同じスタック名で試したのはこれがどうなるか確認したかっただけなのですが、
cdk migrateを実行したローカルのディレクトリで
・ap-northeast-1にあるsampleスタックをcdk migrateした後
・us-east-1にあるsampleスタックをcdk migrateした場合、
同じ名前のフォルダが既にある為エラーを表示せず、そのまま上書きされるのを確認しました。

ヘルプコマンドを確認しましたが、このようなスタック名の重複時、生成するフォルダ(ディレクトリ)名自体を意図的に変更するコマンドや、重複する場合エラーを起こすかどうかや、名前を自動的に(1)等にする事を選択出来るものはなさそうに感じました。

私が実行した時点で且つざっと見た感じですので念の為、ご確認いただければと思います。

※これ以降アカウント、リージョン跨ぎでスタック名が重複している問題には注力しませんので、一旦us-east-1リージョンのスタック名はsample2である前提で進めます。


migrateコマンド実行結果

binとlib直下のtsだけですが、結果を畳みました。

当たり前ですが、作成されたスタックのjsonから読み取っているのだと思うので、CloudFormationでスタックを作成する時点で既にコメントアウトしていた部分はcdk migrateではlib/⚪︎⚪︎-stack.tsには反映されない事が確認出来ました。

テンプレの記述を補足するコメントをcdk migrateで吸い出す事は出来なさそうです。

テンプレAのmigrate結果
bin/sample.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { SampleStack } from '../lib/sample-stack';

const app = new cdk.App();
new SampleStack(app, 'sample', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
lib/sample-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ram from 'aws-cdk-lib/aws-ram';

export interface SampleStackProps extends cdk.StackProps {
  /**
   */
  readonly pjPrefix: string;
  /**
   * @default 'prd'
   */
  readonly environment?: string;
  /**
   */
  readonly accountAId: string;
}

export class SampleStack extends cdk.Stack {
  public readonly transitGatewayId;
  public readonly peerRegion;
  public readonly peerTransitGatewayId;

  public constructor(scope: cdk.App, id: string, props: SampleStackProps) {
    super(scope, id, props);

    // Applying default props
    props = {
      ...props,
      environment: props.environment ?? 'prd',
    };

    // Resources
    const transitGateway = new ec2.CfnTransitGateway(this, 'TransitGateway', {
      amazonSideAsn: 64512,
      autoAcceptSharedAttachments: 'enable',
      defaultRouteTableAssociation: 'enable',
      defaultRouteTablePropagation: 'enable',
      dnsSupport: 'enable',
      vpnEcmpSupport: 'enable',
      tags: [
        {
          key: 'Name',
          value: `${props.pjPrefix!}-${props.environment!}-transitgateway-for-account-a`,
        },
      ],
    });

    if (transitGateway == null) { throw new Error(`A combination of conditions caused 'transitGateway' to be undefined. Fixit.`); }
    const resourceShare = new ram.CfnResourceShare(this, 'ResourceShare', {
      name: `${props.pjPrefix!}-${props.environment!}-ram`,
      resourceArns: [
        `arn:aws:ec2:${this.region}:${this.account}:transit-gateway/${transitGateway.ref}`,
      ],
      principals: [
        props.accountAId!,
      ],
      tags: [
        {
          key: 'Name',
          value: `${props.pjPrefix!}-${props.environment!}-ram-for-account-a`,
        },
      ],
    });
    resourceShare.addDependency(transitGateway);

    // Outputs
    this.transitGatewayId = transitGateway.ref;
    new cdk.CfnOutput(this, 'TransitGatewayId', {
      value: this.transitGatewayId!.toString(),
    });
    this.peerRegion = this.region;
    new cdk.CfnOutput(this, 'PeerRegion', {
      value: this.peerRegion!.toString(),
    });
    this.peerTransitGatewayId = transitGateway.ref;
    new cdk.CfnOutput(this, 'PeerTransitGatewayId', {
      value: this.peerTransitGatewayId!.toString(),
    });
  }
}
テンプレBのmigrate結果
bin/sample.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { SampleStack } from '../lib/sample-stack';

const app = new cdk.App();
new SampleStack(app, 'sample', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
lib/sample-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ram from 'aws-cdk-lib/aws-ram';

export interface SampleStackProps extends cdk.StackProps {
  /**
   */
  readonly pjPrefix: string;
  /**
   * @default 'prd'
   */
  readonly environment?: string;
  /**
   */
  readonly accountBId: string;
  /**
   */
  readonly peerRegion: string;
  /**
   */
  readonly peerTransitGatewayId: string;
}

export class SampleStack extends cdk.Stack {
  public readonly transitGatewayId;

  public constructor(scope: cdk.App, id: string, props: SampleStackProps) {
    super(scope, id, props);

    // Applying default props
    props = {
      ...props,
      environment: props.environment ?? 'prd',
    };

    // Resources
    const transitGateway = new ec2.CfnTransitGateway(this, 'TransitGateway', {
      amazonSideAsn: 64512,
      autoAcceptSharedAttachments: 'enable',
      defaultRouteTableAssociation: 'enable',
      defaultRouteTablePropagation: 'enable',
      dnsSupport: 'enable',
      vpnEcmpSupport: 'enable',
      tags: [
        {
          key: 'Name',
          value: `${props.pjPrefix!}-${props.environment!}-transitgateway-for-account-b`,
        },
      ],
    });

    if (transitGateway == null) { throw new Error(`A combination of conditions caused 'transitGateway' to be undefined. Fixit.`); }
    const resourceShare = new ram.CfnResourceShare(this, 'ResourceShare', {
      name: `${props.pjPrefix!}-${props.environment!}-ram`,
      resourceArns: [
        `arn:aws:ec2:${this.region}:${this.account}:transit-gateway/${transitGateway.ref}`,
      ],
      principals: [
        props.accountBId!,
      ],
      tags: [
        {
          key: 'Name',
          value: `${props.pjPrefix!}-${props.environment!}-ram-for-account-b`,
        },
      ],
    });
    resourceShare.addDependency(transitGateway);

    if (transitGateway == null) { throw new Error(`A combination of conditions caused 'transitGateway' to be undefined. Fixit.`); }
    const transitGatewayPeeringAttachment = new ec2.CfnTransitGatewayPeeringAttachment(this, 'TransitGatewayPeeringAttachment', {
      peerAccountId: this.account,
      peerRegion: props.peerRegion!,
      peerTransitGatewayId: props.peerTransitGatewayId!,
      transitGatewayId: transitGateway.ref,
    });

    // Outputs
    this.transitGatewayId = transitGateway.ref;
    new cdk.CfnOutput(this, 'TransitGatewayId', {
      value: this.transitGatewayId!.toString(),
    });
  }
}

スタックが残存している状況で、先ほどmigrateしたものをcdk deployしてみる

環境変数部分など多少の書き換えをした後、試しに同じプロジェクトのlib直下に
・sample-stack.ts
・sample2-stack.ts
を配置し、
bin内のsample.tsの内容(リージョン部分等)整合をつけ

cdk deploy --all

を実行しました。
それぞれのリージョンのそれぞれのスタックの「変更セット」タブを見ると



cdkによって制御しデプロイがされている事が確認出来ました。

この時点で従前の内容から変更点がなければ、単純にCDKの統制下に置くに相当する作業かと思います。この位のテンプレートであれば問題なくcdkに書き換える事が出来るみたいです。


ローカルにあるYAML,JSONからでも作成可能

cdk migrate --stack-name sample --language typescript --from-path sample.yml

This is an experimental feature and development on it is still in progress. We make no guarantees about the outcome or stability of the functionality.
 ⏳  Generating CDK app for sample...
Applying project template app for typescript
Executing npm install...
✅ All done!

一瞬CFn慣れしてる人がYAMLで書いてcdk migrateしてCDK化出来るねって考えにも出来るかなーと思いましたが、そういう事じゃないなと。

L3が無理なら→L2、L2で無理なら→L1っていう順序で作成していく事はあっても、
L1で成立しちゃってるものを取り込んで、L2→L3で同じ事出来ないかって模索する事はないだろうというのと(CDK的な可読性の恩恵が受けられない)+CFnで物作ってちゃ開発者体験あがらないって事だと思います。(linter等の恩恵が受けられない=>デプロイ前にエラーやミスが起きる可能性を減少させられない)


以上でした

まだまだ調べたい事・試したい事はありますが、今回はここまでとして次回以降頑張りたいと思います。有難うございました。

Discussion