はじめに
仕事でCDKを触っていまして,CDKスタックに統合できるWebアプリが必要になりました。
この記事は,Next.jsで作るWebアプリをCDKでビルド・デプロイまでやってしまうための記事です。
githubのリポジトリをcloneしてcdk deploy
すればNext.jsアプリが立ち上がるようになります。また,cdk watch
した状態でファイルを変更するとCDKスタックやアプリのビルドが走り自動デプロイされます。
コードは以下のgithubリポジトリにあります:
https://github.com/liveinvalley/cdk-next-app
AWS Route53で独自ドメインのHostedZoneを管理していれば,lib/cdk-next-app-stack.ts
のdomainName
とhostName
を書き換えるだけでcreate-next-app
した状態のNext.jsが立ち上がります。
要求事項
Webアプリは独自ドメインで動かし,かつSSL対応させる必要があります。
速度は必要ありませんが,費用を抑える必要があります。
スタックの管理はごく少人数で行うので,cdk deploy
でAWSリソース作成やWebアプリのビルド,証明書やDNS登録を行えるようにします。
構成の方針
Next.jsのサーバレス化のために,Lambda Web Adapter を使います。
Webアプリのコンテナイメージを作り,これを動かすLambdaを作成します。
証明書を取得し,API Gateway にカスタムドメインを設定します。
Route53を設定して,カスタムドメインでAPI Gatewayにアクセスできるようにします。
前提条件
CDKコマンドを使える状態にしておく必要があります。また,Docker が動いている必要があります。
スタック外でAWSに事前設定が必要なリソースはHostedZone
です。
AWSでドメインを取得するか,HostedZoneを作成してネームサーバを所有ドメインのレジストラに設定しておきます。
作成手順
CDKスタック作成
以下のコマンドでスタックを作成します。cdk
コマンドはディレクトリを作らないので,先に作っておきます。
[localhost:~] mkdir cdk-next-app && cd cdk-next-app
[localhost:~/cdk-next-app] 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
* `npx cdk deploy` deploy this stack to your default AWS account/region
* `npx cdk diff` compare deployed stack with current state
* `npx cdk synth` emits the synthesized CloudFormation template
Initializing a new git repository...
warning: in the working copy of '.gitignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.npmignore', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'bin/cdk-next-app.ts', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'cdk.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'jest.config.js', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'lib/cdk-next-app-stack.ts', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'package.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'test/cdk-next-app.test.ts', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'tsconfig.json', LF will be replaced by CRLF the next time Git touches it
Executing npm install...
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
✅ All done!
bin
ディレクトリ内にあるtypescriptファイル(上の例だとbin/cdk-next-app.ts
)でアカウントとリージョンを設定します。ここでは,awsコマンドの設定に合わせています。
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CdkNextAppStack } from '../lib/cdk-next-app-stack';
const app = new cdk.App();
new CdkNextAppStack(app, 'CdkNextAppStack', {
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */
/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});
カスタムタグを付与したコンテナイメージのビルド・デプロイができるように,cdk-docker-image-deployment
をCDKスタックに追加します。
[localhost:~/cdk-next-app] npm install cdk-docker-image-deployment
added 1 package, and audited 382 packages in 8s
50 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Next.js アプリ作成
スタックのディレクトリ内でNext.jsアプリを作成します。create-next-app
は指定したプロジェクト名でディレクトリを作ってくれます(この例だとwebapp
)。アプリの設定はお好みです。
[localhost:~/cdk-next-app] npx create-next-app@latest
√ What is your project named? ... webapp
√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like your code inside a `src/` directory? ... Yes
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to use Turbopack for `next dev`? ... Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No
Creating a new Next.js app in /home/user/cdk-next-app/webapp.
Using npm.
Initializing project with template: app-tw
Installing dependencies:
- react
- react-dom
- next
Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- postcss
- tailwindcss
- eslint
- eslint-config-next
- @eslint/eslintrc
added 369 packages, and audited 370 packages in 38s
139 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Success! Created webapp at /home/user/cdk-next-app/webapp
コンテナのサイズを小さくするために,next.config.ts
を編集します。
nextConfig
でstandalone設定を有効にします。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
};
export default nextConfig;
ここまでで,一度 Next.jsアプリを起動してみます(webapp
ディレクトリ内で)。
[localhost:~/cdk-next-app/webapp] npm run dev
npm run dev
> webapp@0.1.0 dev
> next dev --turbopack
▲ Next.js 15.1.2 (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.1.6:3000
✓ Starting...
✓ Ready in 1693ms
このアプリを,CDKでビルド・デプロイできるようにしていきます。
最初に,キャッシュフォルダを作ってからnodeを起動するスクリプトを作ります。
#!/bin/bash -x
[ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache
exec node server.js
次に,Next.js アプリをコンテナ化するためのDockerfile
を作ります。
Lambda Web Adapter を組み込むことで,Next.js の出力をAPI Gateway(Lambdaプロキシ統合)の期待するフォーマットに合わせます。
FROM public.ecr.aws/docker/library/node:22.11.0-slim as builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM public.ecr.aws/docker/library/node:22.11.0-slim as runner
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 /lambda-adapter /opt/extensions/lambda-adapter
ENV PORT=3000 NODE_ENV=production
ENV AWS_LWA_ENABLE_COMPRESSION=true
WORKDIR /app
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/run.sh ./run.sh
RUN ln -s /tmp/cache ./.next/cache
RUN chmod +x ./run.sh
CMD exec ./run.sh
ここまでで,Next.jsアプリをコンテナ化する準備ができました。
コンテナ化とLambdaへのデプロイは,CDKで行います。
CDKスタックに組み込み
ドメイン名をdomainName
に設定します。
また,Next.jsアプリに設定したい名前をhostName
に設定します。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53_targets from 'aws-cdk-lib/aws-route53-targets';
import * as deployment from 'cdk-docker-image-deployment'
import * as path from 'path'
export class CdkNextAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ホスト名・ドメイン
const domainName = 'example.com';
const hostName = 'www';
const fqdn = hostName + '.' + domainName;
// ECRにプッシュするイメージにつけるタグ
const tag = 'code-server-custom';
// 既存のRoute53 HostedZoneをルックアップ
const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
domainName: domainName,
});
// ACMで証明書を作成
// Route53のHostedZoneを指定して自動取得
const certificate = new cdk.aws_certificatemanager.Certificate(this, "Certificate", {
domainName: fqdn,
validation: cdk.aws_certificatemanager.CertificateValidation.fromDns(hostedZone),
});
// ECRリポジトリ作成
// スタック削除時に自動削除
const repository = new cdk.aws_ecr.Repository(this, 'Repository', {
imageTagMutability: cdk.aws_ecr.TagMutability.MUTABLE,
removalPolicy: cdk.RemovalPolicy.DESTROY,
emptyOnDelete: true,
});
// webapp ディレクトリでイメージをビルドしてECRにデプロイ
// カスタムタグを付与
const imageDeployment = new deployment.DockerImageDeployment(this, 'DockerImageDeployment', {
source: deployment.Source.directory(path.join('.', 'webapp')),
destination: deployment.Destination.ecr(repository, {
tag: tag,
}),
});
// デプロイしたイメージをもとにLambda作成
// イメージがプッシュされていないとエラーになるので,DockerImageDeploymentに依存させる
const imageFunction = new lambda.DockerImageFunction(this, 'DockerImageFunction', {
code: lambda.DockerImageCode.fromEcr(repository, {
tagOrDigest: tag,
}),
});
imageFunction.node.addDependency(imageDeployment);
// API Gateway作成(Lambda統合プロキシ)
// カスタムドメインとその証明書を付与
const api = new apigateway.LambdaRestApi(this, 'LambdaRestApi', {
handler: imageFunction,
domainName: {
certificate: certificate,
domainName: fqdn,
},
binaryMediaTypes: [
'*/*',
],
});
// Route53 にAレコード作成
const aRecord = new route53.ARecord(this, 'ARecord', {
zone: hostedZone,
recordName: hostName,
target: route53.RecordTarget.fromAlias(new route53_targets.ApiGateway(api)),
});
}
}
スタックでしていることは以下の通りです:
- HostedZoneをルックアップして取得。証明書取得やAレコード登録に使います。
- 証明書作成
- コンテナリポジトリ作成
- コンテナイメージデプロイ。webappフォルダを指定して,イメージのビルドとリポジトリへのプッシュを行います。
- Lambda作成。ビルドしたイメージを動かします。
- API Gateway作成。Lambdaを起動します。カスタムドメインを設定します。binaryMediaTypesの指定は改善したほうがいいかもしれません。
- Aレコード作成。API Gatewayに設定したカスタムドメインと同じ名前でDNSクエリに応答できるようになります。
CDKスタックデプロイ
以下のコマンドを実行します。アプリのビルドやコンテナイメージの作成は自動で行われます。初回実行時はコンテナイメージのキャッシュがないのですごく遅い(10~20分くらいかかるかも)ですが2回目から早くなります。
[localhost:~/cdk-next-app] cdk deploy
コマンドが無事に終了すると,指定したFQDNでNext.jsアプリが動いているはずです。
また,以下のコマンドを実行すると,ファイル変更をウォッチして自動デプロイしてくれます。
[localhost:~/cdk-next-app] cdk watch
Node.jsアプリだけだとローカルで動かすのが速いですが,デプロイ結果を見たくて色々触っているときは便利ですね。
参考資料
Web上にたくさんの方々が参考になる資料をあげておられます。