React + Nginx構成のコンテナをTerraformとGitHub Actionsを用いてCloud Runにデプロイする

# はじめに
こんにちは、H×Hのセンリツ大好きエンジニアです。（同担OKです😉）
今回はGoogle CloudとTerraformの勉強がてらに作ったサービスで構築したCI/CDをご紹介したいと思います！

https://domestic-stock-checker.com

基本的には**nicky_tree55**さんの記事を基にしておりますので、そちらで述べられている説明は省きたいと思います🙇

https://zenn.dev/aagfsgss/articles/80f687aee76a72

参考にした記事がとても勉強になりますので、そちらをまず閲覧していただくことをお勧めします💪

# 構成
[Eracer](https://www.eraser.io/?r=0)に作成してもらいました。
![diagram-export-2025-4-18-14_54_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653241/fb5fbea0-4c69-4d71-b1cb-9f788f62f1fd.png)

大まかに言うと、以下のようになっています。
- GitHub ActionsによるCI/CD
  - Terraformを用いて、Google Cloudの環境構築、デプロイを実行
- フロントエンドはReact、バックエンドはGo
  - Vite + Reactでビルドされた静的ファイルをNginxで配信するようにしている
- コンテナイメージはArtifact Registory、tfstateはCloud Storageに保存
- デプロイ先はCloud Run
  - マルチコンテナ構成で、バックエンドコンテナはサイドカー方式（フロントエンドコンテナにくっついて起動する）

ユーザーがアクセスする時に、Cloud DNSがリクエストをキャッチしてCloud Runへ促すようにしています。

DNSとCloud Runの連携部分のみ手動で行いました🖐️
方法はざっくりとこちらの通り

https://cloud.google.com/run/docs/mapping-custom-domains?hl=ja

## 参考元からの変更点
ほぼ一緒ですが、今回はフロントエンドでNext.jsではなくReactを採用しています。
[Lovable](https://lovable.dev/)にフロントを作ってもらったので、React + Vite構成になってたのが理由です🤫

そのため、Next.jsで内包されていた静的ファイル配信をNginxで行う必要があり、そこだけ変更しました。

変更した箇所は以下の通り
- フロントエンドのDockerfile
- nginx.conf.templateの作成

### フロントエンドのDockerfile
``` Dockerfile
# syntax = docker/dockerfile:1
# ローカルでの実行用ステージは割愛
#####################################
# build stage
#####################################
FROM oven/bun:1.2.2 AS builder
WORKDIR /app

# Install dependencies (optionally add --production to skip devDependencies if build tools are in prod deps)
COPY package.json bun.lock ./
# use --production if possible to avoid dev-only deps&#8203;:contentReference[oaicite:2]{index=2}
RUN bun install

# Copy source and build the app
COPY . .
# ensure production build optimizations
ENV NODE_ENV=production
# this runs "vite build", outputting to /app/dist
RUN bun run build

#####################################
# runner stage
#####################################
FROM nginx:alpine AS runner
RUN apk add --no-cache gettext
# Copy build output to nginx's web root
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
RUN apk add --no-cache net-tools && netstat -tuln

ENV PORT=80
CMD ["/bin/sh", "-c", "envsubst '$PORT' < /etc/nginx/templates/default.conf.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"]

```

※マルチステージにしてますが、devステージ（開発用）は割愛してます。
今回はBunを用いているため、ビルドステージでBunのインストールやビルドの実行など行っていますが、こちらはNode.jsに変えていただいても大丈夫です。

また、ランナーステージでは`nginx:alpine`というNginxの軽量イメージを使用しています。`nginx.conf.template`をコピーして、`envsubst`という環境変数展開用のライブラリを用いて設定ファイルをコンテナ内に作成しています。

※`ENV PORT=80`はCloud RunでPortの設定がされてない場合のものなので、無くても良いです。

### nginx.conf.template
``` nginx.conf.template
server {
    listen $PORT;
    server_name _;
    root /usr/share/nginx/html;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~* \.(ico|png|jpg|jpeg|svg|gif|webp|css|js)$ {
        try_files $uri =404;
    }

    location /api/ {
        proxy_pass バックエンドのパス;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
```

こちらに関しては、基本的な設定さえあれば大丈夫だと思います👌
特筆する所としては、画像の配信用に`location ~*`を追加したり、バックエンドのAPIにプロキシさせる用のリバースプロキシを追加したりくらいですかね。

後は前述の記事に書いているような手順で行なってもらうとCloud Runへのデプロイが行われ、成功すれば実際に外部公開ができるようになっていると思います！🎉
1. `terraform/init`でGoogle Cloudの環境構築（API有効化、アカウント作成や権限の付与など）
1. `terraform/bucket`で`tfstate`管理用のバケット追加
1. `terraform/application`と`.github/workflows`を追加してPR作成〜mainへのマージまで実行

# おわりに
今回は、インフラの勉強として下記の記事を参考に自分なりに少し変更してみました！

https://zenn.dev/aagfsgss/articles/80f687aee76a72

インフラの勉強が億劫だとお思いの方でもとっつき易い内容だと思いますので、是非チェックしてみてください！

最後までご覧いただきありがとうございました！