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

Tech
January 3, 2023

概要

ときどき、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