実践!AWS CDK #27 スタック分割

題字・息子たち
2021.12.13

はじめに

今回はスタックの分割方法を紹介します。
CFn で言うと クロススタックの参照 と呼ばれる機能を CDK で実現します。

前回の記事はこちら。

スタック分割のメリット

スタックを分割することのメリットは一言でいうと 管理がしやすくなる です。
共通のライフサイクルやリソースでグルーピングできるため、他のスタックに影響を与えることなく目的のリソースを作成・更新することができます。このため複数のメンバーでスタックを作成・変更する場合もコンフリクトが発生しづらくなります。また、単一スタックだとデプロイ時にエラーが発生した場合はすべてのリソースがロールバックされてしまいますが、スタックを分割しておけばロールバックもスタック単位となります。

クロススタックの参照

まずは CFn でのクロススタック参照を説明します。

あるスタックで作成したリソースを別のスタックで参照するためには ImportValue という関数を利用します。

お試しで VPC とサブネットを別々のスタックで構築してみます。(通常はこの粒度でスタックを分割することはないと思いますが)
サブネット作成時には VPC の ID が必要となるので、この値を参照させます。

vpc_stack.yaml

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

Outputs:
  VpcId:
    Value: !Ref Vpc
    Export:
      Name: 1234567890-vpc-id

値を渡す側の VPC スタックでは Outputs セクションに Export フィールドを用意し、引き渡しのためのエクスポート名(ここでは 1234567890-vpc-id)を定義します。この名前は任意の値でいいのですが、リージョン内で一意である必要があります。

このファイルでスタックを作成すると VPC リソースが作成され、CFn の出力タブでは次のように表示されます。

1

続いてサブネットスタックの記述です。

VPC の ID を必要とする部分では VPC スタック側で定義した 1234567890-vpc-id というエクスポート名を使用します。ImportValue 関数を使用することで別のスタックで出力した値にアクセスできるという仕組みです。

subnet_stack.yaml

Resources:
  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId: !ImportValue 1234567890-vpc-id

このファイルでスタックを作成すると問題なくサブネットも作成されます。

以上が CFn でのクロススタック参照でした。
一見便利なように見えますが、以下の点がやや面倒です。

  • Outputs セクションの作成
  • エクスポート名の検討
  • スタック作成実行順の記憶
    • サブネットスタック作成前に VPC スタックを作成しなければならない

嬉しいことに CDK のクロススタック参照ではこれらはすべて自動化されます。

実装

それでは同様のクロススタック参照を CDK でやってみましょう。
今回は既存プロジェクトの更新ではなく、新たな CDK プロジェクトを作成して実装します。

以下のコマンドを実行し、サンプルプロジェクトを作成します。

$ mkdir sample
$ cd sample
$ cdk init app --language=typescript

空のプロジェクトが作成され、メインのスタッククラスは以下のようになっています。

lib/sample-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

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

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'SampleQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

まずは VPC のスタッククラスを作成します。

lib/vpc-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { CfnVPC } from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

export class VpcStack extends Stack {
    public readonly vpc: CfnVPC;

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

        this.vpc = new CfnVPC(this, 'Vpc', {
            cidrBlock: '10.0.0.0/16'
        });
    }
}

外部から値を参照できるように public 変数に CfnVPC オブジェクトを格納しています。

サブネットのスタッククラスはこちら。

lib/subnet-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { CfnSubnet, CfnVPC } from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

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

        new CfnSubnet(this, 'Subnet', {
            cidrBlock: '10.0.1.0/24',
            vpcId: vpc.ref
        });
    }
}

コンストラクタで CfnVPC オブジェクトを受け取れるように修正します。実際に値を使用しているのは 11 行目です。

最後にメインのスタッククラスで各スタッククラスを利用するよう修正します。

lib/sample-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SubnetStack } from './subnet-stack';
import { VpcStack } from './vpc-stack';

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

    const vpcStack = new VpcStack(scope, 'VpcStack', {
      stackName: 'vpc-stack'
    });

    new SubnetStack(scope, 'SubnetStack', vpcStack.vpc, {
      stackName: 'subnet-stack'
    });
  }
}

SubnetStack を生成する際に VpcStack で作成した CfnVPC オブジェクトをパラメーターとして渡しています。
このプログラムで CFn 版と同様のリソースが作成されます。

CDK のクロススタック参照は以下のような動きとなります。

  • Outputs セクションを明示的に作成する必要はない
    • スタック間で参照している値を自動検出してくれる
  • エクスポート名は自動で付与される
  • これらの複数スタックはデプロイコマンド一発で作成できる
    • 個別にデプロイすることも可能

便利ですね!

デプロイ

それではデプロイしてみましょう。

CDK プロジェクトで複数のスタックを扱っている場合、deploy コマンドや destroy コマンドでは対象のスタックを指定する必要があります。指定できるスタックのリストは cdk ls コマンドで確認可能です。

$ cdk ls

SampleStack
VpcStack
SubnetStack

今回はすべてのスタックをデプロイするので、--all を指定します。

$ cdk deploy --all

これで各スタックが順にデプロイされます。SampleStack に関しては処理が空とみなされスタック自体作成されません。

確認

CFn のマネジメントコンソールを見てみましょう。

意図したスタック vpc-stacksubnet-stack がそれぞれ作成されています。

2

自動生成されたエクスポート名はこちら。

3

CDK の実装では書いていませんが、CFn のテンプレートでは Outputs セクションが作られています。

4

サブネットのスタック側ではその値を ImportValue 関数で使用しています。

5

もちろん、VPC やサブネットのリソースも適切に作られていました。

スタックの削除

スタックを削除する場合も対象のスタックを指定します。今回はこちらも --all で。

$ cdk destroy --all

Are you sure you want to delete: SubnetStack, VpcStack, SampleStack (y/n)? y
SubnetStack (subnet-stack): destroying...

 ✅  SubnetStack (subnet-stack): destroyed
VpcStack (vpc-stack): destroying...

 ✅  VpcStack (vpc-stack): destroyed
SampleStack: destroying...

 ✅  SampleStack: destroyed

おわりに

CDK のクロススタック参照は CFn に比べて自動で対応してくれる部分が多く、簡単にスタックを分割するための手助けとなるでしょう。CFn のベストプラクティスでもスタック分割は推奨されていますので、CDK でも積極的に利用していきたいと思います。

リンク