1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CDKでサーバレスNext.jsアプリを立ち上げる

Last updated at Posted at 2024-12-23

はじめに

仕事で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.tsdomainNamehostNameを書き換えるだけで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コマンドの設定に合わせています。

bin/cdk-next-app.ts
#!/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設定を有効にします。

webapp/next.config.ts
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

ちゃんと動いたようです。
image.png

このアプリを,CDKでビルド・デプロイできるようにしていきます。
最初に,キャッシュフォルダを作ってからnodeを起動するスクリプトを作ります。

webapp/run.sh
#!/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プロキシ統合)の期待するフォーマットに合わせます。

webapp/Dockerfile
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に設定します。

lib/cdk-next-app-stack.ts
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)),
    });
  }
}

スタックでしていることは以下の通りです:

  1. HostedZoneをルックアップして取得。証明書取得やAレコード登録に使います。
  2. 証明書作成
  3. コンテナリポジトリ作成
  4. コンテナイメージデプロイ。webappフォルダを指定して,イメージのビルドとリポジトリへのプッシュを行います。
  5. Lambda作成。ビルドしたイメージを動かします。
  6. API Gateway作成。Lambdaを起動します。カスタムドメインを設定します。binaryMediaTypesの指定は改善したほうがいいかもしれません。
  7. 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上にたくさんの方々が参考になる資料をあげておられます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?