[AWS CDK] CloudFront Functions でリクエスト URL に index.html を追加する構成を作成してみた

2024.01.16

こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。

Amazon CloudFront の CloudFront Functions を使用すると、エッジロケーションで軽量の JavaScript コードを実行して、リクエストおよびレスポンスを処理することができます。

CloudFront Functions を使用するユースケースとしては、「リクエスト URL への index.html の追加」があります。例えば Web サイトをホスティングする CloudFront のオリジンを S3 バケットとした場合、バケットへは index.html を配置することになりますが、ブラウザでのユーザーからのアクセス(ビューワーリクエスト)ではユーザービリティのためにファイル名を指定せずに /about などのディレクトリのみでアクセスさせたい場合があるためです。

そこで今回は、CloudFront Functions でリクエスト URL に index.html を追加する構成を AWS CDK で実装してみました。

実装

CloudFront Function コード

リクエスト URL に index.html を追加する CloudFront Function のコードです。

src/cloudfront-function/add-index-html-to-request-url/index.js

async function handler(event) {
  const request = event.request;
  const uri = request.uri;

  // Check whether the URI is missing a file name.
  if (uri.endsWith('/')) {
    request.uri += 'index.html';
  }
  // Check whether the URI is missing a file extension.
  else if (!uri.includes('.')) {
    request.uri += '/index.html';
  }

  return request;
}

このコードは Amazon CloudFront のドキュメントにあるサンプルコードをそのまま使用しています。

CDK コード

AWS CDK のスタック定義のコードです。JavaScript runtime 2.0 を使用した CloudFront Function を作成し、 Distribution と関連付けています。

lib/cdk-sample-stack.ts

import {
  aws_cloudfront,
  aws_s3,
  aws_s3_deployment,
  aws_cloudfront_origins,
  Stack,
  RemovalPolicy,
  Duration,
  CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // S3 バケットの作成
    const websiteBucket = new aws_s3.Bucket(this, 'WebsiteBucket', {
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // CloudFront から S3 バケットへのアクセスを許可するために、
    // Origin Access Identity を作成し、S3 バケットのアクセスポリシーに追加する
    const originAccessIdentity = new aws_cloudfront.OriginAccessIdentity(
      this,
      'OriginAccessIdentity'
    );
    websiteBucket.grantRead(originAccessIdentity);

    // CloudFront Function の作成
    const cloudFrontFunction = new aws_cloudfront.Function(
      this,
      'AddSecurityHeadersToTheResponseFunction',
      {
        code: aws_cloudfront.FunctionCode.fromFile({
          filePath: 'src/cloudfront-function/add-index-html-to-request-url/index.js',
        }),
        // JavaScript runtime 2.0 を指定
        runtime: aws_cloudfront.FunctionRuntime.JS_2_0,
      }
    );

    // CloudFront Destribution を作成
    const distribution = new aws_cloudfront.Distribution(this, 'Distribution', {
      // defaultRootObject: 'index.html', // ルートディレクトリへのアクセスへも index.html が付与されることを確認するために敢えて無効化
      errorResponses: [
        {
          ttl: Duration.minutes(5),
          httpStatus: 403,
          responseHttpStatus: 403,
          responsePagePath: '/error.html',
        },
        {
          ttl: Duration.minutes(5),
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: '/error.html',
        },
      ],
      defaultBehavior: {
        viewerProtocolPolicy:
          aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new aws_cloudfront_origins.S3Origin(websiteBucket, {
          originAccessIdentity,
        }),
        // CloudFront Function と Distribution の関連付け
        functionAssociations: [
          {
            function: cloudFrontFunction,
            eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST,
          },
        ],
      },
    });

    // CloudFront Distribution のドメイン名を出力
    new CfnOutput(this, 'DistributionUrl', {
      value: `https://${distribution.distributionDomainName}`,
    });

    // S3 バケットへのコンテンツのデプロイ、CloudFront Distribution のキャッシュ削除
    new aws_s3_deployment.BucketDeployment(this, 'WebsiteDeploy', {
      distribution,
      destinationBucket: websiteBucket,
      distributionPaths: ['/*'],
      sources: [
        aws_s3_deployment.Source.data(
          '/index.html',
          '<html><body><h1>Hello, World!</h1></body></html>'
        ),
        aws_s3_deployment.Source.data(
          '/about/index.html',
          '<html><body><h1>Abuot Me</h1></body></html>'
        ),
        aws_s3_deployment.Source.data(
          '/error.html',
          '<html><body><h1>Error!!!</h1></body></html>'
        ),
        aws_s3_deployment.Source.data('/favicon.ico', ''),
      ],
    });
  }
}

動作確認

Curl コマンドで CloudFront Distribution にアクセスしてみます。

ルートディレクトリへのアクセスで期待通りのレスポンスが返却されることを確認できました。

# 末尾スラッシュ無し、ファイル名無し -> index.html が返却される
$ curl https://d1ll42wrbl4i37.cloudfront.net
<html><body><h1>Hello, World!</h1></body></html>

# 末尾スラッシュ有り、ファイル名無し -> index.html が返却される
$ curl https://d1ll42wrbl4i37.cloudfront.net/
<html><body><h1>Hello, World!</h1></body></html>

# 末尾スファイル名有り -> index.html が返却される
$ curl https://d1ll42wrbl4i37.cloudfront.net/index.html
<html><body><h1>Hello, World!</h1></body></html>

# 不正なファイル名 -> error.html が返却される
$ curl https://d1ll42wrbl4i37.cloudfront.net/index.js        
<html><body><h1>Error!!!</h1></body></html>

/about ディレクトリへのアクセスで期待通りのレスポンスが返却されることを確認できました。

# 末尾スラッシュ無し、ファイル名無し
$ curl https://d1ll42wrbl4i37.cloudfront.net/about     
<html><body><h1>Abuot Me</h1></body></html>

# 末尾スラッシュ有り、ファイル名無し
$ curl https://d1ll42wrbl4i37.cloudfront.net/about/
<html><body><h1>Abuot Me</h1></body></html>

# 末尾ファイル名有り
$ curl https://d1ll42wrbl4i37.cloudfront.net/about/index.html
<html><body><h1>Abuot Me</h1></body></html>

次にブラウザからアクセスしてみます。

それぞれのディレクトリへのアクセスで期待通りのレスポンスが返却されることを確認できました。

リクエストが 503 エラーになった

CloudFront Functions を使用した Distribution にアクセスすると 503 エラーが返却されることがありました。

この時は誤ってイベントタイプを VIEWER_RESPONSE にしていたことが原因でした。

lib/cdk-sample-stack.ts

      defaultBehavior: {
        viewerProtocolPolicy:
          aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new aws_cloudfront_origins.S3Origin(websiteBucket, {
          originAccessIdentity,
        }),
        // CloudFront Function と Distribution の関連付け
        functionAssociations: [
          {
            function: cloudFrontFunction,
            eventType: aws_cloudfront.FunctionEventType.VIEWER_RESPONSE, // VIEWER_REQUEST を指定するのが正
          },
        ],
      },

実施したい処理によって適切なイベントタイプを選択するように注意しましょう。

おわりに

CloudFront Functions でリクエスト URL に index.html を追加する構成を AWS CDK で実装してみました。

よくあるユースケースだと思います。だからこそルートディレクトリ以外へのアクセスでも Distribution のプロパティ一つで設定可能になって欲しくはありますが、現状はこちらの方法で対応する必要があります。

参考

以上