【CDK】LAMP環境を題材に考えるスタック間のリソース共有が難しい問題を解決する

こんちゃーす。最近 CDK と格闘している PHPer です。

CDK、楽しいですよね。Typescript でインフラがかけるなんて最高です。でも、学習を始めていくとある壁にぶつかりました。

ネット上の「CDKでECS構築してみた!」系の記事を参考にすると、大抵の場合 VPCもECSもRDSも全部一つの lib/my-stack.ts に処理が書かれている」のです。サンプルコードとしてはわかりやすいのですが、いざ実務でLMAP環境を作ろうとすると⋯…

  • VPC は更新頻度が低いからスタックを分けたい
  • RDS はステートフルだから独立させたい
  • ECS はアプリケーションコードのデプロイで頻繁に更新が入るかも

とスタックを分割したくなりませんか?
でも、分割した途端に「StackA で作ったVPCのIDをどうやってStackBにわたすの?」という問題が発生します。

当記事では、現時点での私の持論である スタック間のリソース共有を紹介します。

IDではなくオブジェクトを渡す

VPC スタックとECSスタックを分けるときに、ついやってしまいがちなのが VPC IDを Props で渡す という手法です。
Terraform に慣れていると、この発想になりがちですよね。

この手法では CDK の 型安全を活かすことが出来ないので、 VPCオブジェクトそのもの を渡すのがベストだと考えています。

私の構成案:
司令塔となる エントリーポイント app がバケツリレーのようにオブジェクトを渡していくイメージです。

  1. VPCStack: VPCを作る。 public readonly vpcでVPCオブジェクトを公開する
  2. APP: VpcStack からVPCオブジェクトをもらい、 ECSStackに渡す
  3. EcsStack: 受け取ったVPC オブジェクトを使ってクラスターを作る

実際にコードを見ていきましょう。

型安全の恩恵を受ける

VPCStack ⇒ 渡す側

ここでは this.vpcをクラスのメンバ変数として公開しておきます。

// lib/vpc-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

export class VpcStack extends cdk.Stack {
  // 外部に公開するプロパティ(これが重要!)
  public readonly vpc: ec2.IVpc;

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

    this.vpc = new ec2.Vpc(this, 'LampVpc', {
      maxAzs: 2,
    });
  }
}

EcsStack ⇒ 受け取る側

ここがポイントデス。 vpcId: stringではなく、 vpc: ec2.IVpc というインターフェース型で受け取ります。

// lib/ecs-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { Construct } from 'constructs';

// Propsの定義:文字列ではなく「VPCの型」を指定する
interface EcsStackProps extends cdk.StackProps {
  vpc: ec2.IVpc;
}

export class EcsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: EcsStackProps) {
    super(scope, id, props);

    // 文字列から検索する必要なし!そのまま使える
    const cluster = new ecs.Cluster(this, 'LampCluster', {
      vpc: props.vpc, 
    });
  }
}

bin/app.ts ⇒ 繋ぐ場所

最後にエントリーポイントで紐づけます。

// bin/app.ts
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { EcsStack } from '../lib/ecs-stack';

const app = new cdk.App();

// 1. VPCを作る
const vpcStack = new VpcStack(app, 'VpcStack');

// 2. VPCオブジェクトをECSスタックに渡す
const ecsStack = new EcsStack(app, 'EcsStack', {
  vpc: vpcStack.vpc, // ここでバケツリレー
});

この方法の何が良いの?

この方法のメリットは2つあると思っています。
(一言で言うとプログラミング言語による抽象化です。)

  1. 圧倒的な型安全性
    もし間違えてS3のオブジェクトを渡そうとすると、エディタがその場でエラーを吐いてくれます。
    「デプロイしてみたらIDが違ってコケた」という悲劇が未然に防げます
  2. 記述がメチャ楽
    受け取る側で ec2.Vpc.fromLookup() のようなインポート処理を書く必要がありません。渡された瞬間から props.vpc.addInterfaceEndpoint のようにメソッドが使えます

議論したいポイント

ここまで「これが正解だ」という顔で書いてきましたが、実はこの構成には明確なデミリットもあります。それは、 スタック同士が密結合 になることです。

Cloudformation の Export/Import 機能でガッチリ紐づいてしまうため、以下のような問題が置きます。

  • スタック間の参照があるため、「VPCだけを作り直したい」が出来ない

ただし、私は上記のような問題が発生したとしても、
LAMP 環境という一つのアプリケーションにおいて、VPCとECSは「運命共同体」にあたるため問題ないと考えています。

「ECSが生きているのにVPCだけ消したい」という状況は稀ではないでしょうか。そのためこの密結合はあえて受け入れるべき仕様だと割り切っています。

ただ、もし組織の基盤となる共通のVPCを作る場合は、ライフサイクルが異なるため、疎結合なID渡しにするのが正解なのかもしれません。

皆さんの構成おしてほしいっす。


コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。