shimotsu tech

Webフロントエンドエンジニア @ to-R inc.

Next.js 製の静的サイトを S3 + CloudFront + Github Actions で自動デプロイする

これまで Next.js や Gatsby を使った Jamstack なサイトを作る際には Netlify や Vercel といったホスティングサービスを使うことがほとんどでした。

そこで Jamstack に関する理解を深めるため、AWS の S3 + CloudFront (デプロイは Github Actions で自動化する) という、もう少しマニュアル寄りな構成で Jamstack なサイトを構築してみました。

最終的にできたサイトは以下になります。
https://d1ocf77jr70lm2.cloudfront.net

今回は、構築のための方法を自分用にざっくりメモがてら書いていきます。詳細な手順を記すものではないことをご了承ください。

Next.js でサイトを構築

  • まず、通常通り Next.js でサイトを構築する。内容はいたってシンプル。リポジトリこちら

  • たいていの場合、コンテンツはヘッドレスCMSなどから取得するが、今回は簡単にするためリポジトリ内に設置した json ファイルからコンテンツを生成した。内容は、社員名簿的なもの。

{
  "list": [
    {
      "id": 1,
      "name": "hoge1",
      "age": 29,
      "branch": "fuga1"
    },
    {
      "id": 2,
      "name": "hoge2",
      "age": 30,
      "branch": "fuga2"
    },
    {
      "id": 3,
      "name": "hoge3",
      "age": 31,
      "branch": "fuga3"
    }
  ]
}
  • npm script で、静的サイト生成用のコマンド "export": "next build && next export -o dist" を定義する。また通常、next export を実施すると /out ディレクトリに静的ファイルは吐き出されるが、なんとなくの好みで /dist に変更した。

Github Actions 用の設定ファイルを作成

  • リポジトリ内に Github Actions 用の yml ファイルを作成する。mainブランチにpushした際にワークフロー(S3 への同期、CloudFront のキャッシュ削除)が走るようにする。
※デプロイに直接関係ない job については省略しています。

name: Deploy to AWS S3

on:
  workflow_dispatch:
  push: # push時に作動
    branches: [main] # main ブランチが対象

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout # ソースコードのチェックアウト。リポジトリ内のファイルにアクセスする
        uses: actions/checkout@v2

      - name: Setup Node # Node 環境のセットアップ
        uses: actions/setup-node@v3
        with:
          node-version: 16

      - name: Install Dependencies # 依存モジュールをインストール
        run: yarn install

      - name: Export # ビルド & 静的ファイルエクスポート
        run: yarn export

      - name: Deploy # デプロイ実行
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          echo "AWS s3 sync"
          aws s3 sync --region ap-northeast-1 ./dist s3://${{ secrets.AWS_BUCKET_NAME}} --delete
          echo "AWS CF reset"
          aws cloudfront create-invalidation --region ap-northeast-1 --distribution-id ${{ secrets.AWS_CF_ID }} --paths "/*"

S3 の設定

  • S3 で静的ファイルを格納しておくバケットを作成する。基本はこちらをベースにおこなっている。

  • バケットポリシーを以下のように設定する(CloudFront のディストリビューションドメイン名は、CloudFrontの設定時に分かるので、最初は仮で後ほど値を入力する)。各ステートメントの意味としては、"PublicList" は Github Actions で S3 にファイルをアップロードするための指定で、"PublicReadGetObject" は CloudFront を経由せず直接 S3 にアクセスするのを拒否するための指定。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicList",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::[AWSアカウントID]:user/[userネーム]"
            },
            "Action": "s3:ListBucket",
            "Resource": [
                "arn:aws:s3:::[バケットネーム]",
                "arn:aws:s3:::[バケットネーム]/*"
            ]
        },
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[バケットネーム]/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": "[CloudFront のディストリビューションドメイン名]/*"
                }
            }
        }
    ]
}

CloudFront の設定

  • S3 に引き続き、こちらを参考にディストリビューションを作成する。

  • S3 への直接アクセス拒否のためにカスタムヘッダー設定を行う。CloudFront > ディストリビューション > [ディストリビューションID] > オリジンを編集 からカスタムヘッダーを追加する。これを忘れると、CloudFront のディストリビューションドメインにアクセスしても、ずっと 403 (Access Denied)エラーになる。

  • 404 の場合はNext.js で用意されているエラーページを表示したいので、CloudFront > ディストリビューション > [ディストリビューションID] > エラーページのレスポンスを編集 で、設定する。レスポンスページのパスには、 /404/index.html を指定する。

  • この時点では、サブディレクトリにあるHTMLファイルにアクセスできないので、 CloudFront Functions を利用して参照できるようにする。スクリプトこちらを参考にした。

デプロイ実行

  • main ブランチに push すると Github Actions のワークフローが走る。

    • デプロイに成功すると、S3 にエクスポートされたファイルがアップロードされ、CloudFront のキャッシュが削除される。また、Github Actions 内に Artifact が生成される。
  • S3 および CloudFront の設定が正しく行われていれば、 S3 のバケットウェブサイトエンドポイントにアクセスした際は403が、CloudFront のディストリビューションドメインにアクセスした際は200番台が返ってくるはず。

参考

zenn.dev

qiita.com

blog.cotapon.org