$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

Next.jsアプリケーションを動かす環境をaws-cdkを使って構築する(with CloudFront/S3/Fargate)

Next.jsをproduction環境で使うために外観を掴んでおきたいと思い、Next.jsアプリケーションを動かすAWS環境をaws-cdkを使って構築するサンプルを作ってみた。だいぶ荒削りだけど、参考になる例にはなったと思う。

https://github.com/shibayu36/nextjs-on-ecs

利用した技術

  • AWS
    • CloudFront
    • S3
    • ECR
    • ECS
  • aws-cdk
  • Docker
  • Next.js + TypeScript

今回作ったアーキテクチャ

図を書くとこんな感じ

f:id:shiba_yu36:20200219094922p:plain

初期構築方法と継続的なデプロイ方法

README.md に書いた。

初期構築は以下のようにして行う。誰でも使えるようにECR_BASEがあるせいで複雑になっているが、通常はECRのレポジトリURLはハードコードしたら良さそう。

$ yarn install
$ yarn workspace cdk run cdk bootstrap
$ yarn workspace cdk run cdk deploy VpcStack SecurityGroupStack RepositoryStack
$ ECR_BASE=<your ecr base> ./build-image-and-push.sh
$ yarn workspace cdk run cdk deploy --context application-version=$(git rev-parse --short HEAD) NextServerStack StaticStack
$ ECR_BASE=<your ecr base> ./deploy.sh

初期構築した後に、継続的にデプロイするのは以下のコマンド一発でできる。

ECR_BASE=<your ecr base> ./deploy.sh

技術的なポイント

今回構築のメインのポイントは以下の通り。

  • Next.jsアプリケーションのコンテナイメージを作る
  • Next.jsのnext buildで生成した静的ファイルをどう配信するか
  • aws cdkを使って、CloudFront + S3 + ECSの構成を作る
  • どうやってECSへ継続的にデプロイするか

Next.jsアプリケーションのコンテナイメージを作る

Next.jsアプリケーション用のDockerイメージを作る - $shibayu36->blog; に書いてあるとおり。基本はデプロイするコンテナにはpackage.json、yarn.lock、.next/を入れておき、yarn installでproductionに必要なものにみ入れておくと良い。またnext.config.jsを使っている場合はそれもコンテナに入れておくと良さそうだった。

Next.jsのnext buildで生成した静的ファイルをどう配信するか

ユーザーがブラウザでアクセスした時にダウンロードする静的ファイルはCloudFront + S3で配信したい。next buildで作った成果物の中には、SSR用サーバのためのファイル群と、ユーザーがブラウザからアクセスする際に使われる静的ファイル群が存在するので、後者をS3に配置すれば良い。

ブラウザでアクセスした時には/_next/static/*のURLで静的ファイルを取得していて(例: _next/static/8VBMV8AYTxMJOTzvzTfZj/pages/index.js)、これは.next/static/以下のファイルに対応するので

  • aws s3 sync ./.next/static s3://nextjs-on-ecs-static-bucket/_next/static のようにして、S3に静的ファイルをアップロードしておく
  • CloudFrontの設定で、/_next/static/*のpathは、S3のnextjs-on-ecs-static-bucketへ転送する

のようにしたら、CloudFront + S3配信が出来た。

aws cdkを使って、CloudFront + S3 + ECSの構成を作る

これはコードを見るのが早そう。

どうやってECSへ継続的にデプロイするか

ecspressoなどのECSへのデプロイツールを別に用意する必要もあるが、今回はaws-cdkをデプロイにも用いることにした。デプロイ時はaws-cdkのcontext機能を使って、どのイメージタグをデプロイするか決定する。

流れとしては以下のようになる。deploy.sh一発で全て動くようにしている。

  • gitのrevisionをイメージタグとして、build & ECRへpushする
  • buildしたイメージから.next/ディレクトリを取り出し、静的ファイル部分をS3にアップロードする
  • aws-cdkのcontextのapplication-versionにビルドしたイメージタグを渡して、NextServerStackを更新
  • cdk.context.jsonにapplication-versionを記録しておきたいので、sedを使って置換する
    • 今はcdk.context.jsonをいい感じに上書きする手法がなさそう?

build-image-and-push.sh

#!/bin/bash

set -ex

export DOCKER_BUILDKIT=1

: ${REVISION:="$(git rev-parse --short HEAD)"}
: ${ECR_BASE:=648803025740.dkr.ecr.ap-northeast-1.amazonaws.com}

docker build --file Dockerfile.server --target nextjs-on-ecs-server --tag "nextjs-on-ecs-server:$REVISION" --progress plain .

$(aws ecr get-login --no-include-email --region ap-northeast-1)
docker tag "nextjs-on-ecs-server:$REVISION" "$ECR_BASE/nextjs-on-ecs-server:$REVISION"
docker push "$ECR_BASE/nextjs-on-ecs-server:$REVISION"
echo "$ECR_BASE/nextjs-on-ecs-server:$REVISION"

deploy.sh

#!/bin/bash

set -ex

: ${REVISION:="$(git rev-parse --short HEAD)"}
export REVISION
export ECR_BASE

./build-image-and-push.sh

# Next.jsで作った静的ファイルをS3に上げる
rm -rf ./dist/
mkdir ./dist/
CONTAINER_ID="$(docker create "nextjs-on-ecs-server:$REVISION")"
docker cp ${CONTAINER_ID}:/app/.next ./dist/
docker rm -v ${CONTAINER_ID}
aws s3 sync ./dist/.next/static s3://nextjs-on-ecs-static-bucket/_next/static

# Nextのサーバが動くECSを更新
yarn workspace cdk run cdk deploy --context application-version=$REVISION NextServerStack

# cdk.context.jsonをデプロイしたrevisionに更新しておく
sed -i '' -E "s/\"application-version\": \"[^\"]+\"/\"application-version\": \"$REVISION\"/" ./cdk/cdk.context.json

まとめ

今回はNext.jsアプリケーションを動かすAWS環境をaws-cdkを使って構築するサンプルを作ってみた。Next.jsのデプロイ、AWSの要素技術、Dockerのマルチステージビルドやキャッシュの仕組み、aws-cdkによる構築の勉強になってかなり良かった。

感想としては

  • aws cdkによる構築はだいぶ体験が良い
    • CloudFormationで構築するのかなり辛かったので...
  • ECSのデプロイ方法は悩む。ベストプラクティスが知りたい
  • DockerのBuildKitによるキャッシュの仕組みは便利
  • Nextのデプロイに関するドキュメントがあんまりないので、勘でやってる...

参考