Next.jsをCodePipelineでECSに自動デプロイする

# 動機

Next.js のソースコードをプッシュしたら、自動でビルド、ECS へのデプロイを行う仕組みを CodePipeline で作りたかったのだけど、AWS の記事は UI が違っていたりして、とても苦労しました。

そこではじめて CodePipeline を触る人のためになればと思い、構築方法を残します。

自分では Next.js を使っていましたが、どのフレームワークでもほぼ同じはずです。
また環境変数のようにソースコードでも AWS 側でも設定できるものは、今回は分かりやすさのためにソースコード側で行うようにしました。

# 流れ

デプロイまでの流れは以下の通りです。

1. CodeCommit にソースコードを push する
1. CodeCommit の変更を検出して CodeBuild が Docker イメージをビルドを開始する
1. Docker イメージを ECR に push する
1. ECR から ECS にデプロイ

また CodeCommit と CodeBuild が成果物を保存するのに S3 を使っています。

# 前提

Docker や Node.js や AWS CLI はあらかじめインストールしておきます。
私は WSL2 で作業していますが、たぶんどの OS でもあまり変わらないと思います。

ここではローリングアップデートでの自動デプロイを紹介します。
ブルーグリーンデプロイとはファイルや設定が異なるので注意してください。

# React のプロジェクトを作成

## アプリのひな形を作成

ここではデフォルトの `my-app` という名前で作成します。
TypeScript、ESLint、Tailwind CSS などのオプションは全部デフォルト値のままにしました。

```sh
npx create-next-app@latest
cd my-app
```

## ファイル追加

`Dockerfile` と `buildspec.yaml` をルートディレクトリ(package.jsonと同じところ)に追加します。

### Dockerfile

とりあえず動いていますが、もっと良い書き方があるかもしれません。

```Dockerfile:Dockerfile
FROM node:20-alpine AS build

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public

EXPOSE 3000
USER node
CMD ["npm", "start"]
```

### buildspec.yaml

`REPOSITORY_NAME` には、この後出て来る ECR のリポジトリ名を入れます。
同様に `CONTAINER_NAME` には ECS で使うコンテナ名を入れます。

```yaml:buildspec.yaml
version: 0.2

env:
  variables:
    REPOSITORY_NAME: my-ecr-repo
    CONTAINER_NAME: my-container
phases:
  pre_build:
    commands:
      - echo $REPOSITORY_NAME
      - echo $CONTAINER_NAME
      - AWS_ACCOUNT_ID=$(echo ${CODEBUILD_BUILD_ARN} | cut -f 5 -d :)
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$REPOSITORY_NAME
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
    files: imagedefinitions.json
```

# CodeCommit

リポジトリを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/b178088a-0493-05e8-c7b1-b56aac8a271e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/8e48191a-45c1-0512-3101-55cc73ca9bd9.png)

リポジトリを作成したら git clone を行います。
私は HTTPS (GRC) を使っていますが、リポジトリが空だと各方式での git clone のやり方が書いてあるので、迷わないと思います。

私は多要素認証があるので一手間(get-session-token)かかっています。下記のページにあるスクリプトを利用させてもらってます。

https://qiita.com/hoto17296/items/38f8575265a514660745

```sh
aws sts get-session-token --serial-number arn:aws:iam::111111111111:mfa/mkt1234 --profile mkt1234 --token-code 112233 | source set-aws-session-token
git clone codecommit::ap-northeast-1://my-repository
```

先ほど作成した Next.js のファイルを追加し push します。
```sh
git add .
git commit -m "add app"
git push
```

アップロードされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/566f0938-8399-2781-e65c-fc56e40692a5.png)

# ECR (Elastic Container Registry)

リポジトリを作成します。作成する名前は `buildspec.yaml` に書いた `REPOSITORY_NAME` と同じものにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/d81d206f-7ccb-be8d-d3c9-671ce5a1a912.png)

# VPC

以前書いた記事を参考に作成します。
ここでは `my-vpc` という名前の VPC を作成します。
セキュリティグループのインバウンドルールに 80 番ポートを足すのを忘れないでください。

https://qiita.com/mkt1234/items/52735503624d12243cf9#vpc-%E3%81%AE%E8%A8%AD%E5%AE%9A

# ECS

下記ページを参考にクラスターとタスク定義とサービスを作成します。

https://qiita.com/mkt1234/items/52735503624d12243cf9#ecs-%E3%81%AE%E8%A8%AD%E5%AE%9A

## クラスター

`my-cluster` という名前でクラスターを作成します。

## タスク定義

タスク定義のコンテナ名を `buildspec.yaml` に書いた `CONTAINER_NAME` と同じものにします。
イメージ URI は作成した ECR のリポジトリ名の `latest` にしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/c9f96d6f-29d9-9fba-cfab-7ee2458c70bd.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/90f776b7-a204-c745-6a17-6764c594dd90.png)

## サービス

既存のクラスターを `my-cluster` にします。
サービス名は `my-service` にします。
ネットワーキングとロードバランシングについては上記のページを参考にしてください。

ここでいったんデプロイが始まり失敗しますが、ECR にイメージが無いのが原因なので気にしないでください。

# ポリシー作成

CodeBuild に最小限の権限を付与するために、IAM で新規にポリシーを作成します。
ポリシーは複雑なので JSON で作成します。

```json
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "CodeBuildPolicy",
			"Effect": "Allow",
			"Action": [
                "logs:CreateLogGroup",
				"logs:CreateLogStream",
				"ecr:GetAuthorizationToken",
				"ecr:UploadLayerPart",
				"ecr:PutImage",
				"ecr:CompleteLayerUpload",
				"ecr:InitiateLayerUpload",
				"ecr:BatchCheckLayerAvailability",
				"s3:GetObject",
				"s3:PutObject"
			],
			"Resource": "*"
		}
	]
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/e0183d07-af29-6676-1f08-dd2255d1a9cd.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/5a50a0ed-3c7a-7a5c-314a-605b745aad4f.png)

# ロール作成

CodeBuild 用のロールを作成します。

IAM でロールを作成します。先ほど作成したポリシーを使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/545a0784-8306-8a5b-3582-8d940f7c1084.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/e350f9f1-c701-82b0-20e7-39216fee79cf.png)

`my-codebuild-role` という名前を付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/8b70335c-d1ec-c572-5bdd-e41897095e62.png)

# CodePipeline

パイプラインを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/8899e555-dada-4cc3-3d0b-4ad9e69e19fb.png)

### CodeCommit

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/dbf9c2e0-1136-5ec3-6d60-6d014d752f36.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/bb755533-c4d4-aa3c-4602-779fe6cabb1b.png)

### CodeBuild

3ページ目のビルド設定はちょっと複雑です。
「プロジェクトを作成する」をクリックするといったん別のウィンドウが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/477b19c5-e1ae-050a-0b6f-1187bed089a3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/122d1268-9652-006f-166e-95f3aaff0f91.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/d82bd87d-0da8-5774-17f6-f4ca9120b224.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/0d97e515-92d8-905f-69fc-8b7050e17ff4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/9f884081-5981-fc62-d1b7-263e6746cd31.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/46bbf0dc-2dc6-c6fb-1a5b-57a6e0c2e19e.png)

### CodeDeploy

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/0b1cc6d6-6cf2-76b4-bf33-3cde5e2ae032.png)

この設定でパイプラインを作成します。

## 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1814940/f40e977a-c791-cfa9-8931-7aa27da55d5b.png)

# 更新

CodeCommit の master ブランチに変更が発生するたびに、自動的にビルド→デプロイが実行されます。