こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。
前回のエントリで、CloudFront + S3 で静的サイトホスティングをしている Web アプリケーションへのリクエスト URL に index.html
を自動追加する構成を作成しました。
そして上記実装が活用できる Web アプリケーションフレームワークとして Next.js があります。
今回は、Next.js アプリケーションでのリクエスト URL に index.html
を CloudFront Functions で追加する構成を AWS CDK で構築してみました。
試してみた
モノリポ環境作成の準備
AWS CDK と Next.js を同じリポジトリで管理するために、モノリポ環境を npm workspaces 利用します。ここではその準備を行います。
プロジェクトフォルダを作成します。
mkdir cdk-nextjs-sample && cd $_
Git の初期化を行います。
git init
touch .gitignore
echo "node_modules/" > .gitignore
npm の初期化を行います。
npm init -y
Next.js アプリケーションの初期化
次に Next.js アプリケーションの初期化を、モノリポ環境の packages/web
ディレクトリで行います。
ワークスペースを新規作成します。
npm init -w packages/web -y
rm packages/web/package.json
Next.js アプリケーションの初期化を行います。
$ (cd packages/web && npx create-next-app@latest --typescript)
✔ What is your project named? … .
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
next.config.js
を設定します。output
を export
に設定することで、next build
の生成物を out
ディレクトリに出力するようにし、AWS CDK でのコンテンツアップロードのソースパスに指定できるようにします。また、trailingSlash
を true
に設定し、各ページのファイル名が一律で index.html
になるようにします。例えば既定では about.html
と出力されるページは、この設定により /about/inde.html
に出力されるようになります。
packages/web/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export", // `next build` の生成物を `out` ディレクトリに出力する
trailingSlash: true, // ルート以外のページの出力パスを `out/<Path>/index.html` とする
};
module.exports = nextConfig;
Next.js アプリケーションのページ作成
ルートおよび /about
パスのページを作成します。
/about
ディレクトリを作成します。
mkdir packages/web/src/app/about
/about/page.tsx
ファイルを次の内容で作成します。
packages/web/src/app/about/page.tsx
import Link from 'next/link'
export default function Page() {
return (
<>
<h1>About Us</h1>
<Link href="/">Back to HOME</Link>
</>
)
}
またルートにある page.tsx
を次のように修正します。
packages/web/src/app/page.tsx
import Link from 'next/link'
export default function Page() {
return (
<>
<h1>Home</h1>
<Link href="/about">Go to AboutUs</Link>
</>
)
}
これにより両ページ間の遷移を Link
タグにより実装できました。
ここでローカルで Web アプリケーションを起動してみます。
npm run dev -w web
http://localhost:3000 にアクセスし、リンクによる遷移が正常に動作することを確認します。
AWS CDK アプリケーションの初期化
次に CDK アプリケーションの初期化を、モノリポ環境の packages/iac
ディレクトリで行います。
npm init -w packages/iac -y
rm packages/iac/package.json
CDK Init を実行します。
(cd packages/iac && cdk init sample-app --language typescript)
CDK アプリケーションの依存関係を最新バージョンにアップデートします。
npm i aws-cdk-lib@latest aws-cdk@latest -w iac
AWS CDK コードの実装
AWS CDK のスタックを定義するコードを実装します。
packages/iac/lib/iac-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 IacStack 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', {
errorResponses: [
{
ttl: Duration.minutes(5),
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/404.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.asset("./../web/out")], // Next.js のビルド生成物の出力パスを指定
});
}
}
CloudFront Functions コードの実装
リクエスト URL に index.html
を追加する CloudFront Functions のコードを実装します。
コードを配置するディレクトリを作成します。
mkdir packages/iac/src
mkdir packages/iac/src/cloudfront-function
mkdir packages/iac/src/cloudfront-function/add-index-html-to-request-url
コードの実装は次のようになります。公式ドキュメントのサンプルコードをそのまま利用しています。
packages/iac/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;
}
既定では .js
ファイルが .gitignore
の対象になっているため、上記のコードを除外するようにします。
echo "!src/cloudfront-function/**/*.js" >> packages/iac/.gitignore
デプロイ
Next.js アプリケーションをビルドします。
npm run build -w web
これにより out
ディレクトリ配下に、index.html
および about/index.html
が出力されます。
CDK デプロイを実行します。
npm run cdk -w iac -- deploy --require-approval never --method=direct
動作確認
デプロイされた Web アプリケーションにアクセスして動作を確認します。
CloudFront Distribution のドメイン名 https://xxxxxxxx.cloudfront.net にアクセスすると、ルートページが正常に表示されます。リクエスト URL への /index.html
の追加処理が行われていることが確認できます。
ルートページから <Link>
を利用した /about
パスのページへの遷移も正常に動作します。
https://xxxxxxxx.cloudfront.net/about または https://xxxxxxxx.cloudfront.net/about/ に直接アクセスした場合でも、正常にページが表示されます。この場合も /index.html
の追加処理が正常に行われていることが確認できました。
その他
index.html の自動追加を行わない場合
CloudFront Functions の関連付け設定を外して、index.html
の自動追加を行わない場合の動作を試してみます。
packages/iac/lib/iac-stack.ts
// CloudFront Destribution を作成
const distribution = new aws_cloudfront.Distribution(this, 'Distribution', {
errorResponses: [
{
ttl: Duration.minutes(5),
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/404.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,
// },
// ],
},
});
この場合は /
および /about
のいずれへのアクセスもパスが存在せずエラーとなります。
正常にアクセスするためにはリクエスト URL でファイル名 index.html
まで指定する必要があります。
trailingSlash オプションを設定しない場合
next.config.js
で trailingSlash
オプションを設定せず、デフォルトのままにした場合の動作を試してみます。
packages/web/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export", // `next build` の生成物を `out` ディレクトリに出力する
//trailingSlash: true, // ルート以外のページの出力パスを `out/<Path>/index.html` とする
};
module.exports = nextConfig;
npm run build -w web
(next build
)コマンドを実行してアプリケーションをビルドすると、out
ディレクトリへの出力は次のようになります。/about/index.html
ではなく /about.html
となっていることが確認できます。
この場合は今回は CloudFront Functions のコードでは対応できなくなります。ルート以外へのリクエスト URL に .html
を付与する処理となるようにコードを修正する必要があります。
おわりに
Next.js アプリケーションでのリクエスト URL に index.html
を CloudFront Functions で追加する構成を AWS CDK で構築してみました。
別解として HttpOrigin を利用して S3 ウェブサイトエンドポイントをオリジンにし、無理やり index.html
を付与させる方法もありますが、その場合は OAI(または OAC)による認証が使用できないなどの不都合があるので、Next.js アプリケーションを CloudFront + S3 でホスティングする場合は、今回紹介した方法が良いかと思います。
参考
以上