Rails7でViteを使う - 本番ビルド編

2025/01/25に公開

概要

以下の続き
https://zenn.dev/k0kishima/articles/b9840d4b9a9450

目的

  • 前編でHMRは実現できたが、Viteは開発環境ではesbuildというgo製のビルドツールを、本番環境などではrollupというJavaScript製のバンドラーを利用しているので各々の環境で確認するのがベター
  • 前項の理由で、ここでは本番環境でのビルドがうまくかを確認する

TODO:

  • 加えて、本番環境でよく用いられる CDN によるアセットの配信も実現する

目標

  • Terraform でインフラをAWS上に構築し、そこに Rails アプリケーションをデプロイできること

TODO:

  • アセットはS3にアップロードすること
  • S3の前段にCloudFrontを配置し、アセットを高速に読み込めるようにすること

環境

  • デプロイ先は AWS
  • インフラは IaC (Terraform) で管理

ビルドのトラブルシューティング

CommonJS や AMD Module などを利用したコードを含むプロジェクトでのビルド

Aside: select2 や jQuery など古いパッケージを利用している場合

当方が参画している案件では select2 などの古いパッケージが利用されていた

これは、esbuild では以下のような書き方でワークしていた

import select2 from "select2"
select2()

しかし、テスト実行時のビルドでは app/frontend/entrypoints/application.js (10:7): "default" is not exported by "node_modules/select2/dist/js/select2.js", imported by "app/frontend/entrypoints/application.js". のように実行時エラーになってしまった

これはrollupでのCommonJSでの扱いが関係していると考えられる
この問題に対応するため、 @rollup/plugin-commonjs を利用できる

以下でインストールを実行する

docker compose exec app npm install --save-dev @rollup/plugin-commonjs

続いて、 vite.config.ts を以下のように編集する

vite.config.ts
+import commonjs from '@rollup/plugin-commonjs'
 
 export default defineConfig({
   plugins: [
     RubyPlugin(),
+    commonjs(),
   ],
+  build: {
+    commonjsOptions: { include: [] },
+  }
 })

正直詳しいことはわかっていないがこれでビルドは通るようにはなった

この件に関しては、以下を参考にした
https://github.com/vitejs/vite/issues/2679
https://github.com/vitejs/vite/discussions/17403

本番環境でのビルドとコンパイルされたアセットの参照

冒頭の目標の章で少し触れたが、Viteは開発環境ではesbuildというgo製のビルドツールを、本番環境ではrollupというJavaScript製のバンドラーを利用している
そのため、開発環境ではうまく動いていたがビルドが失敗するなどの問題も起こりうる

ここではAWS上のサービス群を本番環境のデプロイ先として、コンパイルされたアセットを参照できるかを確認する

インフラ構成

簡便のため以下のようにする

  • 権限は本来であれば最小で設定すべきだが動作確認後すぐに消すことを前提にざっくり設定する
  • SSLは導入しない
    • なので、払い出されたDNS名をブラウザのアドレスバーに入力した際にプロトコルが https になったらアクセスできないので注意
  • ALBをリバースプロキシとして機能させ、このバックエンドにRailsのアプリケーションサーバーを配置
    • 通常、Nginxなどを置くが今回は配置しない
  • リモートバックエンドは構築しない

以下に Terraform による実装を用意した

https://github.com/k0kishima/rails_vite_example/tree/main/infra

AWSアカウントやプロファイルを用意しこれを terraform apply すればデプロイに必要なインフラ基盤ができる

⚠️ 用が済んだら terrafom destroy を忘れずに

デプロイ用のワークフローの実装

コンテナのもととなるイメージは実行環境以外にもソースコードもワンパッケージとなったものであるため、一度作成したイメージを使い回すというよりは改修毎に新しいリビジョンができることになる。
一般的にはブランチへのマージなどを契機にイメージを作るのがいいが、今回は便宜上任意のタイミングでECSのコンテナにデプロイできるような以下のワークフローを作成した

.github/workflows/deploy.yml
name: Deploy to ECS

permissions:
  contents: read
  id-token: write
  actions: read

on:
  workflow_dispatch:

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4.0.2
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.PROJECT_NAME }}-github-actions-role
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Login to AWS ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push Docker image
        id: build-image
        uses: docker/build-push-action@v4
        with:
          context: ./
          file: ./deploy/Dockerfile
          push: true
          tags: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.PROJECT_NAME }}-ecr-repo:latest

      - name: Render task definition from template
        id: render-task-def
        run: envsubst < deploy/taskdef-template.json > taskdef.json
        env:
          IMAGE_URI: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.PROJECT_NAME }}-ecr-repo:latest
          PROJECT_NAME: ${{ secrets.PROJECT_NAME }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
          SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}

      - name: Register ECS task definition
        id: register-task-def
        run: |
          TASK_DEF_ARN=$(aws ecs register-task-definition \
            --family ${{ secrets.PROJECT_NAME }}-ecs-task-rails \
            --execution-role-arn arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.PROJECT_NAME }}-ecs-task-execution-role \
            --cli-input-json file://taskdef.json \
            --query 'taskDefinition.taskDefinitionArn' --output text)
          echo "TASK_DEF_ARN=$TASK_DEF_ARN" >> $GITHUB_ENV

      - name: Update ECS service
        run: |
          aws ecs update-service \
            --cluster ${{ secrets.PROJECT_NAME }}-ecs-cluster \
            --service ${{ secrets.PROJECT_NAME }}-ecs-service-rails \
            --task-definition ${{ env.TASK_DEF_ARN }} \
            --force-new-deployment

以下のシークレットを Github Actions 用にセットし、ワークフローを実行すると先程作成したAWSのインフラにデプロイが可能になる

  • AWS_ACCOUNT_ID
  • AWS_REGION
  • PROJECT_NAME
  • SECRET_KEY_BASE
    • docker compose exec app bin/rails secret で出せる

デプロイ時のトラブルシューティング

#16 3.195 Error: Cannot find module @rollup/rollup-linux-x64-gnu. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try npm i again after removing both package-lock.json and node_modules directory.

上記のようなエラーがデプロイ時に発生した場合は、 package.jsonoptionalDependencies として @rollup/rollup-linux-x64-gnu を追加することで、依存関係の解決ができる

package.json
  "optionalDependencies": {
    "@rollup/rollup-linux-x64-gnu": "4.6.1"
  }

Discussion