cdk-remote-stack と AWS Edge Networking Services を使ったクロスリージョンアプリケーション の補足 

こちらは [CDK Advent Calendar 2021] (https://qiita.com/advent-calendar/2021/aws-cdk) の 19 日目の記事です。

こんにちは。 藤原 [@twingo-b] (https://qiita.com/twingo-b) です。

2021/11/20-21 に開催された [JAWS PANKRATION 2021 ](https://jawspankration2021.jaws-ug.jp/) にて [cdk-remote-stackとAWS Edge Networking Servicesを使ったクロスリージョンアプリケーション ](https://jawspankration2021.jaws-ug.jp/sessions/60/) のセッション発表を行いました。発表時間の都合上概念説明がメインで、実際の CDK コードを交えたお話をもっとしたかったなーと思いましたので、この記事で補足します。

セッション発表の後に 「[こちら] (https://dev.classmethod.jp/articles/try-cdk-remote-stack/)」や「[こちら] (https://dev.classmethod.jp/articles/cdk-remote-stack-webacl-cloudfront/)」でも [cdk-remote-stack] (https://github.com/pahud/cdk-remote-stack) の詳しい解説が行われていますので、ぜひご参照ください。

# Amazon CloudFront を使用したクロスリージョンアプリケーション

お題のアーキテクチャ図です。
![cross-regional-archi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9216/5fc1af84-6273-bf04-5756-3790dc6b821a.png)

CDK コードのディレクトリ構成はこちらです。

```
├── README.md
├── bin
│   └── cross-region-app.ts
├── cdk.json
├── jest.config.js
├── lib
│   ├── canary
│   │   └── nodejs
│   │       └── node_modules
│   │           └── screen-canary.js
│   ├── cloudfront-stack.ts
│   ├── origin-stack.ts
│   ├── parameter-stack.ts
│   └── synthetics-stack.ts
├── package-lock.json
├── package.json
└── tsconfig.json
```

CDK で定義した Stack は以下のように分けました。

* AWS Systems Manager Parameter Store 関連の `ParameterStack`: ap-northeast-3
* Application Load Balancer, Amazon ECS on AWS Fargate 関連の `OsakaOriginStack`: ap-northeast-3, `TokyoOriginStack`: ap-northeast-1
* Amazon CloudFront, AWS WAF 関連の `CloudFrontStack`: us-east-1
* Amazon CloudWatch Synthetics 関連の `SyntheticsStack`: us-west-2


CDK コードで実現していることを改めて説明します。

## Stack 間の依存関係を定義
![add-dependencies.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9216/296bca0e-23c9-1fca-0ab7-d6a6a770e63b.png)

## `OsakaOriginStack`, `TokyoOriginStack` で出力した ALB Domain Name を `CloudFrontStack` で参照
![alb-domainname.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9216/328259b4-0a34-3c12-13d1-7f79226df780.png)

## `CloudFrontStack` で出力した CloudFront Distribution Domain Name を `SyntheticsStack` で参照
![cloudfront-distribution-domainname.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9216/51a34c48-39c8-e30f-7043-9f82d9def98f.png)

## `ParameterStack` で事前共有キーを定義し CloudFront origin custom header で追加した `x-pre-shared-key` を ALB の custom rule で評価し、 ALB へのダイレクトアクセスを拒否する
![x-pre-shard-key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9216/1662c696-504f-22c6-a34e-032bb744a0b7.png)

## `bin/cross-region-app.ts`
上記を定義したコード全体像を下に記載しています。 v2.2.0 で動作確認しています。
クロスリージョンの Stack 間の依存関係、パラメータ参照をシンプルなコードで実現できていることがわかります。

※ Stack 内での `RemoteOutputs`, `RemoteParameters` 利用方法は発表内で解説していますので [こちら] (https://speakerdeck.com/twingob/using-cdk-remote-stack-and-aws-edge-networking-services-for-cross-regional-applications) をご確認ください。

* `RemoteOutputs` を `CloudFrontStack`, `SyntheticsStack` で利用しますので、引数で依存する Stack を参照しています。
    * `CfnOutput` は Stack 内で定義するのではなく、エントリポイント側で定義したほうが全体の見通しが良いですね。
* `RemoteParameters` を `OsakaOriginStack`, `TokyoOriginStack`, `CloudFrontStack` で利用しますが、こちらは `ParameterStack` で定義したリージョンとパスを引数として渡しています。

```typescript:bin/cross-region-app.ts
#!/usr/bin/env node
import 'source-map-support/register';
import {
  App,
  CfnOutput,
} from 'aws-cdk-lib';
import { ParameterStack } from '../lib/parameter-stack';
import { OriginStack } from '../lib/origin-stack';
import { CloudFrontStack } from '../lib/cloudfront-stack';
import { SyntheticsStack } from '../lib/synthetics-stack';

const app = new App();
const appName = app.node.tryGetContext('appName');
const parameterRegion = 'ap-northeast-3';
const parameterPath = `/${appName}`;
const hostedZoneId = process.env.CDK_HOSTED_ZONE_ID ?? "";
const hostedZoneName = process.env.CDK_HOSTED_ZONE_NAME ?? "";
const cpu = 512;
const memory = 1024;

// Osaka Parameter resources
const parameterStack = new ParameterStack(app, 'ParameterStack', {
  env: {region: parameterRegion},
  appName: appName,
  parameterPath: parameterPath,
});

// Osaka Origin resources 
const osakaOriginStack = new OriginStack(app, 'OsakaOriginStack', {
  env: {region: 'ap-northeast-3'},
  appName: appName,
  parameterRegion: parameterRegion,
  parameterPath: parameterPath,
  hostedZoneId: hostedZoneId,
  hostedZoneName: hostedZoneName,
  cpu: cpu,
  memory: memory
});
new CfnOutput(osakaOriginStack, "AlbDomainName", {      
  value: osakaOriginStack.domainName
});
osakaOriginStack.addDependency(parameterStack);

// Tokyo Origin resources
const tokyoOriginStack = new OriginStack(app, 'TokyoOriginStack', {
  env: {region: 'ap-northeast-1'},
  appName: appName,
  parameterRegion: parameterRegion,
  parameterPath: parameterPath,
  hostedZoneId: hostedZoneId,
  hostedZoneName: hostedZoneName,
  cpu: cpu,
  memory: memory
});
new CfnOutput(tokyoOriginStack, "AlbDomainName", {      
  value: tokyoOriginStack.domainName
});
tokyoOriginStack.addDependency(parameterStack);

// N.Virginia CloudFront related resources
const cloudFrontStack = new CloudFrontStack(app, 'CloudFrontStack', {
  env: {region: 'us-east-1'},
  appName: appName,
  parameterRegion: parameterRegion,
  parameterPath: parameterPath,
  hostedZoneId: hostedZoneId,
  hostedZoneName: hostedZoneName,
  primaryOriginStack: osakaOriginStack,
  fallbackOriginStack: tokyoOriginStack,
});
new CfnOutput(cloudFrontStack, 'CloudFrontDistributionDomainName', {
  value: cloudFrontStack.domainName
});
cloudFrontStack.addDependency(parameterStack);
cloudFrontStack.addDependency(osakaOriginStack);
cloudFrontStack.addDependency(tokyoOriginStack);

// Oregon Synthetics resources
const syntheticsStack = new SyntheticsStack(app, 'SyntheticsStack', {
  env: {region: 'us-west-2'},
  appName: appName,
  cloudFrontStack: cloudFrontStack,
});
syntheticsStack.addDependency(cloudFrontStack);
```

# まとめ

私が cdk-remote-stack を初めて利用したときに感じたシンプルさと便利さを、皆さんにも感じていただければ幸いです。

そして、クロスリージョンアプリを作るときは、大阪リージョンをプライマリリージョンにできることを思い出してくださいね！