AWS CDKでリージョン間のクロススタック参照を簡単に実現!cdk-remote-stackを試してみた

cdk-remote-stackでクロスリージョン/アカウントのクロススタック参照を簡単に実現!
2021.11.29

はじめに

CX事業本部IoT事業部の佐藤智樹です。

今回は題名の通りAWS CDKでリージョン間のクロススタック参照などが簡単に実現できる cdk-remote-stack を試してみます。JAWSのイベントで紹介している方がおり、試したところ非常に有益な機能だったので是非紹介したいと思いブログにしました。パッケージのソースはGitHub上で公開されています。

なぜこのパッケージがあると嬉しいのか

そもそもどんな問題を解決するためのパッケージなのかを解説します。実装方法だけ気になる場合は次章まで飛ばして読んでください。

AWS CDKはTypeScriptなどのプログラムコードをCloudFormationテンプレートに変換して、CloudFormationからAWSの各種リソースをデプロイします。なので、CloudFormation上に制約がある場合、CDKも同様の制約に引っかかってしまいます。CloudFormationではエクスポートした値などはリージョン固有の値となるため、基本的には別のリージョンから参照することができません。これはグローバルサービスを構築するスタック(us-east-1)と自国のリージョン(例えば ap-northeast-1など)でリソースを展開しているスタックではリソースを参照できないことを意味します。

例えば、Budget Alertと通知用のSNSを構築する際にSNSだけap-northeast-1で構築するということはできません。両方ともus-east-1で構築する必要があります。

特段厳しい要件がなければこれで良いのですが、Service Control Policie(SCP)でグローバルサービス以外は国外のリージョンにサービスを作らせないようにアカウントを設計している場合、SNSが海外のリージョンに作れず問題になります。SCPの具体的な設計については以下の記事か公式のドキュメントを参照してください。

CloudFormationやAWS CDKでリージョン間クロススタック参照を実現するには、カスタムリソースや独自のイベントなどでLambdaをキックしSSM Parametersを利用して別リージョンのスタックのリソースを取得する必要がありました。CloudFormationとSNS+Lambdaを使った実装例は以下の記事が参考になります。自分の例よりもよく起きやすい内容だと思います。

カスタムリソースを使ってAWS CDKで実装することも可能ですが、一つのリソースを取得するためだけにカスタムリソースの実装が必要になり大変に感じていました。そんなとき今回のcdk-remote-stackを知り、試したところ非常に簡単にリージョン間のクロススタック参照が実現でき、使い方を広めたいと思ったので記事にしました。

cdk-remote-stackで何ができるのか

以下の2機能のどちらかを利用してリージョン間クロススタック参照が可能になります。

  • RemoteOutputs:CloudFormationのOutputsのクロスリージョンでのスタック間参照
  • RemoteParameters:SSM Parametersのクロスリージョン/クロスアカウントでのスタック間参照

RemoteOutputsの方はクロスリージョン参照に対応しており、RemoteParametersはクロスリージョンに加えてクロスアカウントでの参照も実現しています。今回は実際に筆者が試した、RemoteParametersを利用して単一アカウントでリージョン間クロススタック参照する方法を紹介します。

実践

今回は例としてリージョンサービスをap-northeast-1で構築し、グローバルサービスからリソースを参照してみます。実際にはBudgets Alertからap-northeast-1のSNSを参照して通知を投げれるよう実装してみます。

サンプルコード

サンプルコードは以下に格納してあります。全体像が気になる場合はこちらをご確認ください。

SNS Topicの実装

まずはSNS Topicを東京リージョン用のスタックで実装します。特に今回特別なことはしていません。

cost-alert-topic-stack.ts

import * as cdk from "@aws-cdk/core";
import * as sns from "@aws-cdk/aws-sns";

export interface CostAlertTopicStackProps extends cdk.StackProps {
  costNotifyEmail: string;
}

export class CostAlertTopicStack extends cdk.Stack {
  public readonly alarmTopic: sns.Topic;

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

    const alarmTopic = new sns.Topic(this, `${id}-CostAlarmTopic`);
    new sns.Subscription(this, `${id}-CostAlarmEmail`, {
      endpoint: props.costNotifyEmail,
      protocol: sns.SubscriptionProtocol.EMAIL,
      topic: alarmTopic,
    });
    // 外部からArnを読めるように更新
    this.alarmTopic = alarmTopic;
  }
}

アプリ層でパラメータ設定やスタック間の依存関係を設定

次にスタックを呼び出すアプリ層で、スタックの内容をSSM Parametersに格納します。またスタック間の依存関係がCloudFormationやCDKの管理外になるため、明示的にSNS Topic(参照先)->Budget Alert(参照元)の順でデプロイできるようにaddDependencyを追加します。

cdk-remote-stack-example.ts

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import * as ssm from "@aws-cdk/aws-ssm";

import { CostAlertTopicStack } from "../lib/cost-alert-topic-stack";
import { CostAlertStack } from "../lib/cost-alert-stack";

const app = new cdk.App();
const envKey = app.node.tryGetContext("environment");
const envVals = app.node.tryGetContext(envKey);

// Cost Topic
const costAlertTopicStack = new CostAlertTopicStack(app, `CostAlertTopicStack`, {
  costNotifyEmail: envVals.costNotifyEmail,
});

// SSM ParametersにcostAlertTopicStackの値を設定
const parameterPath = `/${envVals.env.accountId}/${envVals.env.region}/${costAlertTopicStack.stackName}`;

new ssm.StringParameter(costAlertTopicStack, `TopicArn`, {
  parameterName: `${parameterPath}/topicArn`,
  stringValue: costAlertTopicStack.alarmTopic.topicArn,
});

// Cost Monitoring
const costAlertStack = new CostAlertStack(app, `CostAlarmStack`, {
  costAlertTopicParam: parameterPath,
  costAlertTopicRegion: envVals.env.region,
  env: { account: envVals.env.accountId, region: "us-east-1" },
});
// 先にSNS Topicのスタックが作成されるように設定
costAlertStack.addDependency(costAlertTopicStack);

Budget Alert用のスタックでSNS Topicを参照

最後にBudget Alert用のスタック(CostAlertStack)側でSNS Topicを参照します。

cost-alert-stack.ts

import * as cdk from "@aws-cdk/core";
import * as budgets from "@aws-cdk/aws-budgets";
import { RemoteParameters } from "cdk-remote-stack";

export interface CostAlertStackProps extends cdk.StackProps {
  costAlertTopicParam: string;
  costAlertTopicRegion: string;
}

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

    // カスタムリソース経由でpathに指定したSSM Paramatersを取得する(path配下をまとめて取得可能)
    const parameters = new RemoteParameters(this, `${id}-Parameters`, {
      path: props.costAlertTopicParam,
      region: props.costAlertTopicRegion,
    });
    // SSM Paramatersの中から使用したいパラメータを取得
    const alertTopicArn = parameters.get(`${props.costAlertTopicParam}/topicArn`);

    // Budgets Alert
    new budgets.CfnBudget(this, `${id}-BillingAlarmBudget`, {
      budget: {
        budgetType: "COST",
        timeUnit: "DAILY",
        budgetLimit: {
          unit: "USD",
          amount: 100,
        },
      },
      notificationsWithSubscribers: [
        {
          notification: {
            comparisonOperator: "GREATER_THAN",
            notificationType: "ACTUAL",
            threshold: 80,
            thresholdType: "PERCENTAGE",
          },
          subscribers: [
            {
              subscriptionType: "SNS",
              address: alertTopicArn,
            },
          ],
        },
      ],
    });
  }
}

上記でリージョン間のスタック間が実現できます。次に実際にデプロイできるか試してみます。

デプロイ前準備

リポジトリをクローン後、以下のコマンドでパッケージをインストールしてください。Nodeのバージョンはv14.5.0で試しています。

% npm install

デプロイ前に対象のリージョンでのスタック作成が初めてであればブートストラップを実行してください。123456789012は使用しているAWSアカウントで置き換えてください。

% npx cdk bootstrap aws://123456789012/ap-northeast-1 -c environment=dev
% npx cdk bootstrap aws://123456789012/us-east-1 -c environment=dev

デプロイ

最後にデプロイしてみます。

% npx cdk deploy --all -c environment=dev --require-approval never

デプロイで問題なければ以下のようにBudget Alertの画面を開くと、通知先としてap-northeast-1のSNS Topicが指定できてることが確認できます。

後片付け

作業が終わって不要であれば、以下のコマンドでリソースの削除を実行してください。

% npx cdk destroy --all -c environment=dev

所感

特定のリージョンでしか使えないサービスやグローバルサービスのスタックと自国のリージョンのスタックで連携する際に非常に有用な機能だと思いました。正直自分も壁に当たるまでは何に使えるのかよく分からなかったので具体的な内容にしてみました。また今回は紹介しませんでしたが、クロスリージョンかつクロスアカウントもできるようにパッケージは設計されています。もし気になる方は是非実装して試してみてください。

もしAWS CDKを極めていく際に同じような壁に当たったら参考にしてもらえると幸いです。