AWS CDKでCloudFront Functionを使ったリダイレクト環境を構築する

Blog

ときどき、CloudFront Functionsを使ってHTTPリダイレクト環境を構築することがある。
今まではAWS Console上でAWSリソースを作成していたが、設定内容を構成管理できるようにAWS Cloud Development Kit(AWS CDK)を使ってみる。

この記事では、AWS CDKを使って次のAWSリソースを作成する。

  • S3(CloudFrontのオリジンに設定する)
  • CloudFrontディストリビューション
  • CloudFront Functions

動作を確認した環境

  • Node.js vv18.12.0
  • AWS CLI v2.9.11
  • AWS CDK v2.58.1

初回:AWS CDKのセットアップ

AWS CDKのインストール

AWS CDKをインストールする。プロジェクト単位でインストールしても良い。

$ npm install -g aws-cdk

# バージョンの確認
$ cdk --version
2.58.1 (build 3d8df57)

デプロイ⽤のS3バケットの作成

はじめてCDKでデプロイするときは、デプロイ⽤のS3バケットを作成する必要がある。
このS3バケットはリージョン単位で作成するため、リソースを作成するリージョンを変更する場合はcdk bootstrapを実行する。

$ cdk bootstrap aws://AWS_ACCOUNT_ID/AWS_REGION --profile PROFILE_NAME

  Bootstrapping environment aws://123456789012/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...
  Environment aws://123456789012/ap-northeast-1 bootstrapped.

STEP1:CDKプロジェクトの作成

cdk init appで、CDKプロジェクトを作成できる。
今回は、TypeScriptで記述したいので、languageオプションで「typescript」を設定する。

$ mkdir example && cd $_
$ cdk init app --language typescript

Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build`   compile typescript to js
* `npm run watch`   watch for changes and compile
* `npm run test`    perform the jest unit tests
* `cdk deploy`      deploy this stack to your default AWS account/region
* `cdk diff`        compare deployed stack with current state
* `cdk synth`       emits the synthesized CloudFormation template

Executing npm install...
 All done!

次のファイル群が生成される。

.
├── README.md
├── bin
   └── example.ts # CDKアプリのエントリーポイント。lib/example-stack.tsのインスタンスを生成する。
├── cdk.json # ビルド設定や環境変数を定義する
├── jest.config.js
├── lib
   └── example-stack.ts # CloudFormationスタックを定義する。
├── node_modules
   └── ...
├── package-lock.json
├── package.json
├── test
   └── example.test.ts
└── tsconfig.json

STEP2:スタックテンプレートの修正

lib/example-stack.tsを次のように修正する。

lib/example-stack.ts
import * as cdk from "aws-cdk-lib";
import {
  aws_s3 as s3,
  aws_s3_deployment as s3deploy,
  aws_cloudfront as cf,
  aws_cloudfront_origins as cfo,
  aws_iam as iam,
} from "aws-cdk-lib";

import { Construct } from "constructs";

// アカウント名やリージョンは、AWS プロファイルから設定される
const { CDK_DEFAULT_ACCOUNT, CDK_DEFAULT_REGION } = process.env;
const prefix = `example-${CDK_DEFAULT_REGION}-${CDK_DEFAULT_ACCOUNT}`;

export class ExampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // S3 バケットを作成する
    // パブリックアクセスをすべてブロックする
    const bucket = new s3.Bucket(this, "S3Bucket", {
      bucketName: `${prefix}-bucket`,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      versioned: false,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // オリジンアクセスアイデンティティを作成する
    const identity = new cf.OriginAccessIdentity(this, "OriginAccessIdentity", {
      comment: `${bucket.bucketName} access identity`,
    });

    // オリジンアクセスアイデンティティに S3 のアクセスを許可する IAM を設定する
    const bucketPolicyStatement = new iam.PolicyStatement({
      actions: ["s3:GetObject"],
      effect: iam.Effect.ALLOW,
      principals: [identity.grantPrincipal],
      resources: [`${bucket.bucketArn}/*`],
    });
    bucket.addToResourcePolicy(bucketPolicyStatement);

    // CloudFront Function で動かす、リダイレクトスクリプトを設定する
    const cfFunction = new cf.Function(this, "CloudFrontFunction", {
      code: cf.FunctionCode.fromFile({
        filePath: "assets/redirect.js",
      }),
    });

    // CloudFront のディストリビューションを作成する
    const distribution = new cf.Distribution(this, "CloudFrontDistribution", {
      comment: "cloudfront distribution",
      defaultRootObject: "index.html",
      defaultBehavior: {
        allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD,
        cachedMethods: cf.CachedMethods.CACHE_GET_HEAD,
        cachePolicy: cf.CachePolicy.CACHING_OPTIMIZED,
        viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new cfo.S3Origin(bucket, {
          originAccessIdentity: identity,
        }),
        functionAssociations: [
          {
            function: cfFunction,
            eventType: cf.FunctionEventType.VIEWER_REQUEST,
          },
        ],
      },
      priceClass: cf.PriceClass.PRICE_CLASS_100,
    });

    // S3 バケットに index.html を置く
    new s3deploy.BucketDeployment(this, "BucketDeployment", {
      sources: [
        s3deploy.Source.data(
          "/index.html",
          "<html><body><h1>Hello World</h1></body></html>"
        ),
      ],
      destinationBucket: bucket,
      distribution: distribution,
      distributionPaths: ["/*"],
    });
  }
}

STEP3:リダイレクトスクリプトの作成

プロジェクトのルートにassetsディレクトリを作成し、redirect.jsを作成する。

$ mkdir assets
$ touch assets/redirect.js

redirect.jsは、次の内容にする。
CloudFrontのURLにアクセスすると、https://new.example.comにリダイレクトするスクリプトである。

assets/redirect.js
function handler(event) {
  var newUrl = `https://new.example.com`;

  var response = {
    statusCode: 302,
    statusDescription: "Found",
    headers: { location: { value: newUrl } },
  };

  return response;
}

STEP4:AWSリソースのデプロイ

スタップテンプレートに記述したAWSリソースをデプロイするには、cdk deployを実行する。

$ cdk deploy --profile PROFILE_NAME

ExampleStack: building assets...

...
[100%] success: Published randomvalue:current_account-current_region

...

  Deployment time: ...

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/ExampleStack/12345678-1234-1234-1234-123456789012

  Total time: ...

STEP5:動作確認

AWS CLIでCloudFrontのURLを確認する。

aws cloudfront list-distributions --profile PROFILE_NAME | jq '.DistributionList.Items[].DomainName'
"foo.cloudfront.net"

CloudFrontのURLにGETリクエストを送信する。
HTTPステータス302で、new.example.comにリダイレクトしていることを確認できた。

$ curl -I https://foo.cloudfront.net

HTTP/2 302
server: CloudFront
...
content-length: 0
location: https://new.example.com
x-cache: FunctionGeneratedResponse from cloudfront
via: 1.1 example-example.cloudfront.net (CloudFront)
...

おまけ:デプロイしたAWSリソースの削除

作成したAWSリソースを削除するには、cdk destroy スタック名を実行する。

$ cdk destroy example --profile PROFILE_NAME