🧪

AWS Solutions ConstructsでWAF+CloudFront+S3構成を作る

2023/01/25に公開

はじめに

本記事はJAWS-UG CDK支部 #5 〜新春隠し芸大会〜 で話した内容の具体的な実装部分です。
https://jawsug-cdk.connpass.com/event/269789/

環境構築

npx, yarnは利用できるLinux環境を前提としている。(筆者環境はCloud9+AmazonLinux2)

mkdir work && cd work
npx projen new awscdk-app-ts 

以下のようにprojenの設定ファイルを設定しnpx projenを実行すると完成。
後述するが、cdkVersion ≧ 2.50.0である必要がある。

.projenrc.js
const { awscdk } = require('projen');
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.61.1',
  defaultReleaseBranch: 'main',
  name: 'work',
  deps: [
    "@aws-solutions-constructs/aws-wafwebacl-cloudfront",
    "@aws-solutions-constructs/aws-cloudfront-s3",
    "@aws-solutions-constructs/core",
  ],
});

project.synth();

Sample App

ホスティング用の資材の作り方も載せておく

npm create astro@latest
# ? Where would you like to create your new project? › website
# How would you like to setup your new project? › - Use arrow-keys. Return to submit. 
# ❯   a few best practices (recommended)
# Would you like to install npm dependencies? (recommended) › Y
# Would you like to initialize a new git repository? (optional) … no
# How would you like to setup TypeScript? › - Use arrow-keys. Return to submit.
# ❯   Strict
cd website/
npm run build

Code

Try 1

以下の2つの Constructsを組み合わせることでWAF + CloudFront + S3構成は作成可能だ。

https://docs.aws.amazon.com/solutions/latest/constructs/aws-cloudfront-s3.html

https://docs.aws.amazon.com/solutions/latest/constructs/aws-wafwebacl-cloudfront.html

ということでこう書くと失敗する。

import { App, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { WafwebaclToCloudFront } from '@aws-solutions-constructs/aws-wafwebacl-cloudfront';
import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3';

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps = {}) {
    super(scope, id, props);

    const CF2S3 = new CloudFrontToS3(this, "CF_S3", {})
    
    new WafwebaclToCloudFront(this, "WAF_CF", {
      existingCloudFrontWebDistribution: CF2S3.cloudFrontWebDistribution,
    })
    
    new BucketDeployment(this, 'DeployWithInvalidation', {
      sources: [Source.asset('./website/dist')],
      destinationBucket: CF2S3.s3BucketInterface,
      distribution: CF2S3.cloudFrontWebDistribution,
      distributionPaths: ['/*'],
    });
  }
}

// for development, use account/region from cdk cli
const devEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: "ap-northeast-3",
};

const app = new App();

new MyStack(app, 'work-dev', { env: devEnv });
// new MyStack(app, 'work-prod', { env: prodEnv });

app.synth();

NGの理由

AWS WAF v2をap-northeast-3で作ろうとしているのが原因。CloudFrontはグローバルリージョンサービスでIAMと同様に、どのリージョンでも指定できるので手癖でリリースするとハマる。

For CLOUDFRONT, you must create your WAFv2 resources in the US East (N. Virginia) Region, us-east-1.

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html

例外としてus-east-1で実行すればうまくいくと思われるが、必然的にOrigin(S3 Bucket)のリージョンもus-east-1となってしまう。例えば日本在住の筆者はオリジンを日本に配置できない制約があると困ってしまうため最後の手段にしたい。

Try 2

ややこしいので、WAF部分だけをスタック分割する。CDKv2.50以上であればクロスリージョン設定が利用できるので便利だ。※前述のprojenでの設定理由でもある。

以下のようにStackにcrossRegionReferences: trueを指定すると良い。

import { App, Stack, CfnOutput } from 'aws-cdk-lib';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { WafwebaclToCloudFront } from '@aws-solutions-constructs/aws-wafwebacl-cloudfront';
import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3';


const app = new App();

const mainStack = new Stack(app, 'mainStack', { 
  env: {region: "ap-northeast-3"},
  crossRegionReferences: true,
});

const globalStack = new Stack(app, 'globalStack', { 
  env: {region: "us-east-1"},
  crossRegionReferences: true,
});


const CF2S3 = new CloudFrontToS3(mainStack, "CF_S3", {})

new WafwebaclToCloudFront(globalStack, "WAF_CF", {
  existingCloudFrontWebDistribution: CF2S3.cloudFrontWebDistribution,
})

new BucketDeployment(mainStack, 'DeployWithInvalidation', {
  sources: [Source.asset('./website/dist')],
  destinationBucket: CF2S3.s3BucketInterface,
  distribution: CF2S3.cloudFrontWebDistribution,
  distributionPaths: ['/*'],
});

new CfnOutput(globalStack, 'Endpoint', {
  value: CF2S3.cloudFrontWebDistribution.distributionDomainName,
  exportName: 'DitributionDomainName',
});

app.synth();

※@pahud氏のツイートを参考。感謝!

https://twitter.com/pahudnet/status/1587522689740046336

NGの理由

これもまだうまくいきそうなのだが、実際デプロイすると、WAFのArnをCloudFrontとうまく連携できずにうまくいかない。

恥ずかしながら解析できていないで、以下は単なる仮説。

Try 3

ということで、以下の様にスタックを分割すると上手くいくだろうか?

  • mainStack(ap-northeast-3): Origin(S3Bucket)
  • globalStack(us-east-1): WAF+CloudFront

OriginのS3BucketがL2になってしまうのが課題なのですが、Solutions Constructsの設定値は、実は@aws-solutions-constructs/coreからimportできるので問題ありません。すごい。

import { DefaultS3Props } from '@aws-solutions-constructs/core';

const bucket = new Bucket(mainStack, "S3", DefaultS3Props())

完成

src/main.ts
import { App, Stack, CfnOutput } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
import { WafwebaclToCloudFront } from '@aws-solutions-constructs/aws-wafwebacl-cloudfront';
import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3';
import { DefaultS3Props } from '@aws-solutions-constructs/core';

const app = new App();

const mainStack = new Stack(app, 'mainStack', { 
  env: {region: "ap-northeast-3"},
  crossRegionReferences: true,
});

const globalStack = new Stack(app, 'globalStack', { 
  env: {region: "us-east-1"},
  crossRegionReferences: true,
});


const bucket = new Bucket(mainStack, "S3", DefaultS3Props())

const CF2S3 = new CloudFrontToS3(globalStack, "CF_S3", {
  existingBucketObj: bucket
})

new WafwebaclToCloudFront(globalStack, "WAF_CF", {
  existingCloudFrontWebDistribution: CF2S3.cloudFrontWebDistribution,
})

new BucketDeployment(globalStack, 'DeployWithInvalidation', {
  sources: [Source.asset('./website/dist')],
  destinationBucket: CF2S3.s3BucketInterface,
  distribution: CF2S3.cloudFrontWebDistribution,
  distributionPaths: ['/*'],
});

new CfnOutput(globalStack, 'Endpoint', {
  value: CF2S3.cloudFrontWebDistribution.distributionDomainName,
  exportName: 'DitributionDomainName',
});

app.synth();

まとめ

  • リージョン設計が重要なリソースも最近は簡潔に CDK 管理できる
  • CDK によりクロスリージョン参照が容易
  • Solutions Constructs の Core ライブラリでデフォルト設定を改善できる

という点が良いので、AWS Solutions Constructsを試してみてください。

Discussion