CDKでスタック間のパラメーターを受け渡す5つの方法とケース別の最適解について考えてみた

2022.09.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。CX事業本部Delivery部MADグループのきんじょーです。

皆さんは、CDKであるスタックから別のスタックの情報を参照したくなった場合、スタック間のパラメーターをどのように渡していますか?

スタック間のパラメーター受け渡しの方法は複数あります。

  1. クロススタック参照
  2. Props渡しのクロススタック参照
  3. パラメーターストア経由
  4. 独自のスクリプトで頑張る
  5. 命名規則の運用ルールで縛る

この記事では、これら5つの方法の長所短所と、ケースバイケースで何を選択すべきかを考えてみました。

先にまとめ

  • そもそもCDKのベストプラクティスではスタックを無闇に分割しない
  • クロススタック参照はスタックの依存関係が浅い場合のみ使用を推奨。深い場合は運用が詰む可能あり。
  • 迷ったらパラメーターストアを選んでおけば融通が効きやすい

これから新規にアプリケーションを構築しようと考える場合、以下のようなチャートで選択してみてはいかがでしょうか?

way-to-reference-parameters-flow-chart

前提:スタックはなるべく分けないほうがシンプル

CloudFormationでは1テンプレートが1スタックになるため、可読性のためスタックを分けることがありました。CDKではスタック内のリソースを自由にファイル分割できるため、可読性のためだけにスタックを分ける必要はありません。

CDKのベストプラクティスによると、スタックの数について以下の記載があります。

アプリケーションのStack数を決める際に、すべてのリソースを1つのStackにまとめたり、各リソースをそれぞれのStackにまとめたりするような厳密なルールはありません。最終的には、お客様のデプロイパターンに基づいて、柔軟に判断をすることになります。以下のガイドラインを参考にしてください。

  • 一般的には、できるだけ多くのリソースを同じ Stack に入れておくのが最も簡単です。あらかじめ分離したいと分かっている場合を除いて、同じStackに入れます。
  • ステートフルなリソース (データベースなど) は、ステートレスなリソースから分離しておくとよいでしょう。ステートフルリソースのあるStackの削除保護をオンにしていれば、ステートレスなリソースのある Stack を自由に破壊したり、複数のコピーを作成したりしても、データ損失のリスクは/ありません。

参考:AWS CDKでクラウドアプリケーションを開発するためのベストプラクティス

上記プラクティスに従い、最低限のスタック分割をオススメします。

各方法の比較

それでは、スタック間でパラメーターを渡す各方法について見ていきましょう。

1. クロススタック参照

クロススタック参照は、CloudFormationが提供するスタック間参照機能です。
テンプレートのOutputsでエクスポートした値を、別のスタックでインポートして参照することができます。 CDKでクロススタック参照をする場合、CfnOutput Constructを使用してエクスポートします。詳しくは以下のブログをご確認ください。

クロススタック参照の注意点

クロススタック参照はCloudFormation標準機能のためシンプルですが、スタック間の依存関係が深い、または複雑な場合、運用上致命的な状態に陥る可能性があり注意が必要です

例えば以下のような構成を考えてみます。
LambdaやFargateで構築したWebアプリケーションと、StepFunctionsなどで動くバッチ処理、そしてCloudWatchAlermによる監視を構築するイメージです。矢印の向きが参照を表しています。

stacks-in-dependencies

この状態でDatabaseスタックのDB名を変更したくなった場合、LambdaとContainer、Batchスタックからクロススタック参照されているためスタックを更新できません。

クロススタック参照の依存解決方法

方法1. 参照元スタックを削除 or 変更

参照元のスタックがステートレスかつダウンタイムを許容できるのであれば、参照元スタックを削除すれば依存は解消します。しかし、スタック数や依存関係が深くなるにつれ、順番に削除していく運用が辛くなっていきます。そして参照元が全てステートレスとは限りません。

方法2. 一時的に別のリソースを参照を切り替える

以下のように、参照先を切り替えてから古いDBを削除します。

  1. Databaseスタックに別名の新しいDBを作成
  2. Lambda、Container、Batchのスタックから参照するDB名を新に変更
  3. Databaseスタックから古いDBを削除 ※データ保持の考慮は省略

この場合も、スタック数が多いと参照先の切り替えの運用が辛くなります。

クロススタック参照を使用しても良い場合

上記方法1で運用が辛くならないケースであれば、クロススタック参照を使用しても良いと考えています。

具体的には以下のようなケースです。

  • スタック分割数が最小限
  • CloudFormationのクォータによってスタック分割が必要になる程、今後拡張が見込まれない
  • 依存関係がステートレス→フルで一方向
  • デプロイのダウンタイムが許容できる

effective-cross-stack-reference

この例でDynamoDBのDB名を変更したいと思った場合、ステートレススタックを一つ消せば、依存関係が解消します。

2. Props渡しのクロススタック参照

CDKではCfnOutput以外に、スタックの引数で必要なオブジェクトを渡してクロススタック参照する方法があります。 説明のため、cdk init sample-appで生成されるSQSとSNSを別スタックに分割しました。

import { Duration, Stack, StackProps } from "aws-cdk-lib";
import * as sns from "aws-cdk-lib/aws-sns";
import * as subs from "aws-cdk-lib/aws-sns-subscriptions";
import * as sqs from "aws-cdk-lib/aws-sqs";
import { Construct } from "constructs";

export class QueueStack extends Stack {
  // queueのプロパティを宣言して外から見えるようにする
  queue: sqs.Queue;
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    this.queue = new sqs.Queue(this, "CdkCrossStackPropsQueue", {
      visibilityTimeout: Duration.seconds(300),
    });
  }
}

// StackPropsを拡張してqueueを外から受け取る
type TopicProps = StackProps & {
  queue: sqs.Queue;
};
export class TopicStack extends Stack {
  constructor(scope: Construct, id: string, props: TopicProps) {
    super(scope, id, props);
    const topic = new sns.Topic(this, "CdkCrossStackPropsTopic");
    topic.addSubscription(new subs.SqsSubscription(props.queue));
  }
}

スタックをnewするタイミングで、引数で必要なリソースを渡します。

import * as cdk from "aws-cdk-lib";
import { QueueStack } from "../lib/cdk-cross-stack-props-stack";
import { TopicStack } from "../lib/cdk-cross-stack-props-stack";

const app = new cdk.App();
const queueStack = new QueueStack(app, "QueueStack");
// TopicStackの引数でqueueを渡します
new TopicStack(app, "TopicStack", { queue: queueStack.queue });

生成されるQueueStackのCloudFormationテンプレートに、自動的にFn::ImportValueが追加され、

QueueStack.template.yaml

Resources: 
  CdkCrossStackPropsQueue77232EF6: 
    Type: "AWS::SQS::Queue"
    Properties: 
      VisibilityTimeout: "300"
    UpdateReplacePolicy: "Delete"
    DeletionPolicy: "Delete"
    Metadata: 
      aws:cdk:path: "QueueStack/CdkCrossStackPropsQueue/Resource"
  CdkCrossStackPropsQueuePolicy80F6CE5B: 
    Type: "AWS::SQS::QueuePolicy"
    Properties: 
      PolicyDocument: 
        Statement: 
        - Action: "sqs:SendMessage"
          Condition: 
            ArnEquals: 
              aws:SourceArn: 
                Fn::ImportValue: "TopicStack:ExportsOutputRefCdkCrossStackPropsTopic4C3C9811D8ACE149"
          Effect: "Allow"
          Principal: 
            Service: "sns.amazonaws.com"
          Resource: 
            Fn::GetAtt: 
            - "CdkCrossStackPropsQueue77232EF6"
            - "Arn"
        Version: "2012-10-17"
      Queues: 
      - Ref: "CdkCrossStackPropsQueue77232EF6"
    Metadata: 
      aws:cdk:path: "QueueStack/CdkCrossStackPropsQueue/Policy/Resource"
  CdkCrossStackPropsQueueTopicStackCdkCrossStackPropsTopicE839ED603A740BD0: 
    Type: "AWS::SNS::Subscription"
    Properties: 
      Protocol: "sqs"
      TopicArn: 
        Fn::ImportValue: "TopicStack:ExportsOutputRefCdkCrossStackPropsTopic4C3C9811D8ACE149"
      Endpoint: 
        Fn::GetAtt: 
        - "CdkCrossStackPropsQueue77232EF6"
        - "Arn"
    Metadata: 
      aws:cdk:path: "QueueStack/CdkCrossStackPropsQueue/TopicStackCdkCrossStackPropsTopicE839ED60/Resource"
Parameters: 
  BootstrapVersion: 
    Type: "AWS::SSM::Parameter::Value<String>"
    Default: "/cdk-bootstrap/hnb659fds/version"
    Description: "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
Rules: 
  CheckBootstrapVersion: 
    Assertions: 
    -
      Assert: 
        Fn::Not: 
        -
          Fn::Contains: 
          -
          - "1"
          - "2"
          - "3"
          - "4"
          - "5"
          - Ref: "BootstrapVersion"
      AssertDescription: "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."

TopicStackにExportが追加されています。

TopicStack.template.yaml

Resources: 
  CdkCrossStackPropsTopic4C3C9811: 
    Type: "AWS::SNS::Topic"
    Metadata: 
      aws:cdk:path: "TopicStack/CdkCrossStackPropsTopic/Resource"
Outputs: 
  ExportsOutputRefCdkCrossStackPropsTopic4C3C9811D8ACE149: 
    Value: 
      Ref: "CdkCrossStackPropsTopic4C3C9811"
    Export: 
      Name: "TopicStack:ExportsOutputRefCdkCrossStackPropsTopic4C3C9811D8ACE149"
Parameters: 
  BootstrapVersion: 
    Type: "AWS::SSM::Parameter::Value<String>"
    Default: "/cdk-bootstrap/hnb659fds/version"
    Description: "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
Rules: 
  CheckBootstrapVersion: 
    Assertions: 
    -
      Assert: 
        Fn::Not: 
        -
          Fn::Contains: 
          -
          - "1"
          - "2"
          - "3"
          - "4"
          - "5"
          - Ref: "BootstrapVersion"
      AssertDescription: "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."

この際、CDKのコードと生成されるCloudFormationテンプレートで依存関係の向きに注意が必要です。

上記の例だと、CDKのコード上はTopicStackがQueueStackのqueueを参照していますが、

dependencies-in-cdk

CloudFormationテンプレートでは以下のように逆転しています。

dependencies-in-cfn-template

当初、CfnOutputに比べCDKのコード量が減り、リソースの依存関係もプログラミング言語の中で追えるため、Props渡しの方が便利だと考えていました。しかし、CDKのコードと実際にデプロイされるテンプレートで、依存の向きが異なるケースがあるため、気付かぬうちにスタック間で循環参照が発生しないように注意が必要です。

3. パラメーターストア経由

クロススタック参照を用いるのではなく、パラメーターストアを挟むことによって、スタック間の直接的な依存を回避する方法です。

using-parameter-store

この方法であれば、Lambdaなどのスタックは直接DatabaseStackに依存していないため、DynamoDBの削除やリネームが可能です。

CDKでパラメーターストアを参照する際、メソッドによってパラメーターを取得するタイミングや、使用されるCloudFormationの機能が異なります。 CloudFormationのParametersの機能を使用する場合、1スタックで200個までというクォータが存在するため、大量のパラメーター参照を必要とする場合はお気をつけください。

詳しくは以下のブログを参照してください。

4. 独自のスクリプトで頑張る

クロススタック参照でエクスポートできないパラメーターや、管理外の別スタックから特定の値を持ってきたい場合などに、AWS CLIやSDKを用いて、必要な情報を取得することができます。

ShellやTypeScript、Pythonなど好きな言語で、デプロイの前処理を書くことができ、汎用性は一番高いですが、秘伝のタレ化しやすいので注意が必要です。

5. 命名規則の運用ルールで縛る

こちらは、CloudFormationもパラメーターストアも使用しない方法です。

例えば「Lambda関数名はHTTPメソッドとパスを元に命名する」といったルールを予め決めておき、各Stackではルールに従ってパラメーターを指定します。

あくまでも運用ルールのみでの縛りなので、実際のスタック間に依存関係は発生しておらず、クロススタック参照の制限やCloudFormationのクォータを気にする必要がないというメリットがあります。

一方、運用ルールの徹底が必要なのと、名前被りを防ぐためにリソースを明示的に命名したくない場合(例えばS3やCloudWatchLogsのロググループ)には使えないというデメリットもあります。

パラメーター参照方法と言いつつ、実質参照していないですね。

どれを選べばいいの?

まず一番大切なのは、スタックを無闇に分けずにスタック間のパラメーター参照を減らすことです。
それでもスタック間のパラメーター参照が必要な場合、ここまでご紹介したメリット・デメリットをまとめると、以下のようなフローで選んでみてはいかがでしょうか?

way-to-reference-parameters-flow-chart

もし悩んだ場合、基本的にパラメーターストアを選んでおけば後から融通が効きやすいので一番オススメです。

最後に

スタック間のパラメーター受け渡し方法についてまとめてみました。
他にもこんな受け渡し方法や観点があるよ!というご意見があれば、是非教えて頂けると幸いです。

以上、MADグループのきんじょーでした。