📝

Rails×Next.jsプロジェクトをGoogle Cloud Runデプロイする際の覚書

2023/07/05に公開

なぜAWSではなくGCP?

個人的な趣味嗜好です😎
以前にAWSのECSでコンテナ立ててデプロイするという経験をした時、
結構やること多くて大変だなーというのと、AWSの無料期間も終わり、
ただDBとECSまわり、あとELBを立てているだけで毎月8000円くらい
飛んでいくのが、個人開発のアプリでもったいないなーと思い。

よくGCPは最低インスタンス数を0にするとコールドスタート
(最初のアクセスに時間がかかる)というのがありますが、
それこそ、ステージング環境とかなら、使っていない時には
余計な費用とかかけたくないので全然OKです。
本番環境だけ最低インスタンスを1に設定すればいいかなと。

そしてFirebaseでGCPめっちゃ楽やん!って思って、GCPに
スタックを寄せたいなというのがあったのも理由の一つです👍

基本は公式のdocsを参照します。

その内容に沿った上で、自身の環境との差異で詰まった部分を記載していきます。

自身の環境

  • Ruby 3.2.2
  • Rails 7.0.4
  • React 18
  • Next.js 13
  • 認証やストレージ系はFirebase
  • フロント、apiともにDockerで構築

上のプロジェクトとの差異

  • モノレポであること
    • 以下のような構成で組んでいた
      • pj_file
        • api
          • Dockerfile
        • frontend
          • Dockerfile
        • .github(actions等の設定)
  • staging環境を用意したいということ
    • ここで、staging用の設定ファイルの是非については問いません。とりあえず検証環境を作りたい
  • github actionsで自動デプロイできるようになりたい
    • ローカル実行や、cloudbuild実行でも十分だがやはり一回構築してみたい。という理由

公式と異なる箇所で、個別で対応した箇所

CloudSQLの作成

こちらは公式だとpostgreSQLですが、今回はMySQLで作りました。
まず最初に、gcloudを最新にしておきます。

gcloud components update

その後、自分の場合はmysql8.0.32を使っていたので、そのようにコマンドを修正しました。

 gcloud sql instances create $INSTANCE_NAME \                         
    --database-version MYSQL_8_0_32 \
    --tier db-f1-micro \
    --region asia-northeast1

Secret Manager にシークレット値を保存する

公式では、1つのcredential前提で組んでいますが、各環境に分けたかったので、
こちらの記事を参考に、環境ごとにcredentialsを作成しました。

staging環境作成&デプロイ方法

staging環境の作成については様々な記事があるので割愛します。
最初は公式docsの通りにcloudbuild.ymlで実装するという前提で、
基本的な解決策は、railsやrakeコマンドにRAILS_ENV=staging追加するだけです。

steps:
  - id: "build image"
    name: "gcr.io/cloud-builders/docker"
    entrypoint: 'bash'
    args: ["-c", "docker build --build-arg MASTER_KEY=$$RAILS_KEY -t gcr.io/${PROJECT_ID}/${_SERVICE_NAME} . "]
    secretEnv: ["RAILS_KEY"]

  - id: "push image"
    name: "gcr.io/cloud-builders/docker"
    args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"]

  - id: "apply migrations"
    name: "gcr.io/google-appengine/exec-wrapper"
    entrypoint: "bash"
    args:
      [
        "-c",
        "/buildstep/execute.sh -i gcr.io/${PROJECT_ID}/${_SERVICE_NAME} -s ${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME} -e RAILS_MASTER_KEY=$$RAILS_KEY -- bundle exec rails db:migrate RAILS_ENV=staging" ← こんな感じでOK,
        "/buildstep/execute.sh -i gcr.io/${PROJECT_ID}/${_SERVICE_NAME} -s ${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME} -e RAILS_MASTER_KEY=$$RAILS_KEY -- bundle exec rails db:seed RAILS_ENV=staging"
      ]
    secretEnv: ["RAILS_KEY"]

substitutions:
  _REGION: asia-northeast1
  _SERVICE_NAME: YOUR_SERVICE_NAME
  _INSTANCE_NAME: YOUR_INSTANCE_NAME
  _SECRET_NAME: YOUR_SECRET_NAME

availableSecrets:
  secretManager:
    - versionName: projects/${PROJECT_ID}/secrets/${_SECRET_NAME}/versions/latest
      env: RAILS_KEY

images:
  - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"

GitHubActionsでのデプロイ自動化

条件としては、

  • stagingブランチにプッシュしたら発火
  • apiとfrontendのデプロイを実行
  • 各リポジトリごとにワークフロー定義

という感じで組みました。
大枠の設定に関しては
GitHub ActionsでCloud Runにデプロイする
を参考に構築しています。

この記事の中でWorkflow Identity poolの作成のattribute-conditionですが、公式での問題点が記載されています。

そのため、こちらの設定は、repository_owner_idを設定しようと思います。
repository_owner_idは公式のapiで確認可能です。

--attribute-condition="assertion.repository_owner_id=='9919'

これを設定することで、別のユーザーやリポジトリからこのactionsのデプロイ実行されることを防止できます。

以下が、実際に組んだデプロイのワークフローです。

api-deploy.yml
on:
  push:
    branches:
      - staging

name: (Api) Build and Deploy to Cloud Run for Staging
env:
  SERVICE_NAME: {cloud runにデプロイするサービス名}
  PROJECT_ID: {GCPのプロジェクトID}
  REGION: asia-northeast1
  SERVICE_ACCOUNT: {作ったサービスアカウント}
  PROVIDER: {参考記事7番で取得したリソース名}
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: 'read'
      id-token: 'write'
    defaults:
      run:
        working-directory: api
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: "google-github-actions/auth@v0"
        with:
          workload_identity_provider: ${{ env.PROVIDER }}
          service_account: ${{ env.SERVICE_ACCOUNT }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      - name: Authorize Docker push
        run: gcloud auth configure-docker

      - name: Build Docker image
        run: docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} .

      - name: Push Docker Image
        run: docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }}

      - name: Deploy to Cloud Run
        run: |-
          gcloud run deploy $SERVICE_NAME \
            --project=$PROJECT_ID \
            --image=gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \
            --region=$REGION \
            --platform=managed \
            --allow-unauthenticated

Github Actions実行時にエラー発生!どうする?

auth実行時にエラーが起きた際はここらへんが原因かも

もしも、docker-push時に権限が足りない、というようなエラーメッセージが表示された場合は、この辺が役に立つと思います。

gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/{権限}" \
  --member="serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"

フロントに環境変数を設定したい

Firebaseのconfigに必要なデータ(FIREBASE_API_KEYなど)を渡したいと思っての動機です。
他にもやり方があるし、ベストではないと思うのですが、このやり方で一旦できたので乗せておきます。

まず、Dockerfileで以下のように環境変数を受け取れるようにしておきます。

FROM node:18 AS builder

WORKDIR /app

ARG NEXT_PUBLIC_FIREBASE_API_KEY
ARG NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
ARG NEXT_PUBLIC_FIREBASE_PROJECT_ID
ARG NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
...(他にもあれば)

COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile --production=false
COPY . .
RUN yarn build

FROM node:18-alpine3.16 AS runner
ENV NODE_ENV=production

WORKDIR /app

# standalone モードを利用すると、publicと.next/staticはデフォルトでは含まれないので明示的にコピーする必要がある
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static

COPY --from=builder /app/.next/standalone ./
CMD ["node", "server.js"]

そして、github actionsのデプロイのスクリプト内で
dockerをbuildする箇所で環境変数を渡してあげます。
一部省略している部分はあるので、そこは自分の環境に合わせて設定してください。

on:
  push:
    branches:
      - staging

name: (Front) Build and Deploy to Cloud Run for Staging
env:
  FIREBASE_API_KEY: testeeeeeesssssssssssssssssst
  FIREBASE_AUTH_DOMAIN: test-dev.firebaseapp.com
  FIREBASE_PROJECT_ID: test-dev
  FIREBASE_STORAGE_BUCKET: test-dev.appspot.com
  FIREBASE_MESSAGING_SENDER_ID: testesteste
  SERVICE_NAME: test-dev-front
  PROJECT_ID: test-dev
  REGION: asia-northeast1
  SERVICE_ACCOUNT: github-action-cd@test-dev.iam.gserviceaccount.com
  PROVIDER: projects/testtest/locations/global/workloadIdentityPools/github-actionstest-pool/providers/github-actions-provider

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions: ← これを設定しないと権限エラーで落ちる
      contents: "read"
      id-token: "write"
    defaults:
      run:
        working-directory: frontend ← モノレポのプロジェクトの場合、これを設定することで、スクリプト実行時にfrontendをルートディレクトリとしてコマンド実行してくれる
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - id: "auth"
        name: "Authenticate to Google Cloud"
        uses: "google-github-actions/auth@v0"
        with:
          workload_identity_provider: ${{ env.PROVIDER }}
          service_account: ${{ env.SERVICE_ACCOUNT }}

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v1

      - name: Authorize Docker push
        run: gcloud auth configure-docker

      - name: Build Docker image
        run: |-
          docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \
            --build-arg NEXT_PUBLIC_FIREBASE_API_KEY=${{ env.FIREBASE_API_KEY }} \
            --build-arg NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ env.FIREBASE_AUTH_DOMAIN }} \
            --build-arg NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ env.FIREBASE_PROJECT_ID }} \
            --build-arg NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ env.FIREBASE_STORAGE_BUCKET }} \
            --build-arg NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ env.FIREBASE_MESSAGING_SENDER_ID }}  .

      - name: Push Docker Image
        run: docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }}

      - name: Deploy to Cloud Run
        run: |-
          gcloud run deploy $SERVICE_NAME \
            --project=$PROJECT_ID \
            --image=gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \
            --region=$REGION \
            --platform=managed \
            --allow-unauthenticated

もしまた新しい知見がみつかったら追記か新しい記事書きます。

ではみなさん、良き開発ライフを〜👋

Discussion