くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Node.js + Prisma + Cloud SQLなアプリをCloud Buildでマイグレーション&Cloud Runにデプロイする

Node.js + Prisma + Cloud SQLなアプリを
Cloud Buildを使って、prisma migrateして、
さらにCloud Runにデプロイするときに、
いろいろ調べたときの備忘録。

Cloud Buildを使ったCloud Runへのデプロイについては、
以前の記事でも書いたけれど、
Cloud SQLへのマイグレーションをするとなると、かなりめんどくさいよう。。

やりたいこと

  • GitHubへプッシュしたら、
  • prisma migrate deployを実行して
  • Cloud Runへデプロイ

やりたいことはこれだけなんだけど、
そのままだとCloud BuildからCloud SQLへはアクセスできない。。

ハマったポイント

いろいろ試してみたけど、うまく行かなかった点は以下な感じ。

  • CloudBuildでそのまま実行してみる
    • Cloud Build内からCloud SQLにアクセスできない...
    • Cloud Build内ではCloud Auth Proxyが用意されていない...
  • ビルドするDockerfile内でマイグレーション
    • Dockerfile内では、バックグランド実行できない...
    • => Cloud Auth Proxyを裏で立ち上げれない...
  • cloudbuild.yamlの別ビルドステップでCloud Auth Proxyを立ち上げる
    • Docker build時はVolumeを参照できない...

やったこと

以前の記事でやった内容に、
少し手を加えたシーケンシャルなやり方だと難しそう。。

そのため、

  • waitForを使って、Cloud Auth Proxyの立ち上げを並列でおこなう
  • マイグレーション用のDockerfileを用意する

というやり方で実施。

f:id:wannabe-jellyfish:20220325143905p:plain

流れとしては、以下の通り。

  • Proxyの起動とアプリとマイグレ用のイメージのビルドをそれぞれ開始
    • Proxyは起動し続けるので、waitForでIDを指定すると、開始されない
    • "gcr.io/cloudsql-docker/gce-proxy"は新しいものだとうまく動かなかった。。
  • voluemsをマイグレ用のイメージを実行する際に共有し、 UNIXソケットを使ってCloud SQLに接続
  • マイグレーションが完了したら、アプリをデプロイする
  • 同時にProxyをdocker stopで停止する

cloudbuild.yaml

実際のcloudbuild.yamlはこんな感じ。

steps:
  # [Proxy] Start Cloud SQL Proxy in container image
  - id: Proxy:Start
    name: "gcr.io/cloudsql-docker/gce-proxy:1.15"
    args:
      - "/cloud_sql_proxy"
      - "-dir=/cloudsql"
      - "-instances=$_INSTANCE_CONNECTION_NAME"
    volumes:
      - name: cloudsql
        path: /cloudsql
    waitFor: ["-"]

  # [Migrate] Build container image
  - id: Migrate:Build
    name: "docker"
    args:
      - "build"
      - "--no-cache"
      - "-f"
      - "Dockerfile.migrate"
      - "-t"
      - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME/migrate:$COMMIT_SHA"
      - "."
    waitFor: ["-"]

  # [Migrate] Run container image
  - id: Migrate:Run
    name: "docker"
    args:
      - "run"
      - "-e"
      - "DATABASE_URL=$_DATABASE_URL"
      - "-v"
      - "cloudsql:/cloudsql" # volumeはhost側はpathではなくnameを指定
      - "-w"
      - "/src"
      - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME/migrate:$COMMIT_SHA"
      - "sh"
      - "-c"
      - "npx prisma migrate deploy"
    volumes:
      - name: cloudsql
        path: /cloudsql
    waitFor: ["Migrate:Build"]

  # [App] Build the container image
  - id: App:Build
    name: docker
    args:
      - build
      - "--no-cache"
      - "-t"
      - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
      - .
      - "-f"
      - "Dockerfile"
      - "--build-arg"
      - "DATABASE_URL=$_DATABASE_URL"
    waitFor: ["-"]

  # [App] Push the container image to Container Registry
  - id: App:Push
    name: docker
    args:
      - push
      - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
    waitFor: ["Migrate:Run", "App:Build"]

  # [App] Deploy container image to Cloud Run
  - id: App:Deploy
    name: gcr.io/google.com/cloudsdktool/cloud-sdk
    entrypoint: gcloud
    args:
      - run
      - services
      - update
      - $_SERVICE_NAME
      - "--platform=$_PLATFORM"
      - "--image=$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"
      - "--region=$_DEPLOY_REGION"
      - "--quiet"
    waitFor: ["App:Push"]

  # [Proxy] Stop Cloud SQL Proxy Container image
  - id: Proxy:Stop
    name: "gcr.io/cloud-builders/docker"
    entrypoint: "sh"
    args:
      - "-c"
      - 'docker ps -q --filter ancestor="gcr.io/cloudsql-docker/gce-proxy:1.15" | xargs docker stop'
    waitFor: ["Migrate:Run"]

images:
  - "$_GCR_HOSTNAME/$PROJECT_ID/$_SERVICE_NAME:$COMMIT_SHA"

options:
  substitutionOption: ALLOW_LOOSE

substitutions:
  _DEPLOY_REGION: asia-northeast1
  _GCR_HOSTNAME: asia.gcr.io
  _PLATFORM: managed
  _SERVICE_NAME: "(Cloud Runのサービス名)"
  _DATABASE_URL: "mysql://(DB_USER):(DB_PASS)@localhost/(DB_NAME)?socket=(CloudSQLのインスタンス接続名)"
  _INSTANCE_CONNECTION_NAME: "(CloudSQLのインスタンス接続名)"

timeout: 1200s

tags:
  - gcp-cloud-build-deploy-cloud-run
  - gcp-cloud-build-deploy-cloud-run-managed
  - $_SERVICE_NAME

Dockerfile.migrate: マイグレ用のイメージ

マイグレ用のDockerfile(Dockerfile.migrate)は、こんな感じ。
nodeのイメージに、package.jsonprismaディレクトリをコピーして、
インストールしてるだけ。

FROM node:16

# コンテナ内のwork dirを設定
WORKDIR /src

# package.jsonをコピーして、パッケージのインストール
COPY package.json ./
COPY package-lock.json ./
COPY prisma/ ./prisma/
RUN npm install

Dockerfile: アプリ用のイメージ

こちらはアプリのようDockerfile。
以前の記事と大きくは変わらず。
prismaを使うので、prismaディレクトリをコピーし、
DATABASE_URLを環境変数に設定するようにしている。

ARG VERSION=latest

FROM node:16
ARG DATABASE_URL

# コンテナ内のwork dirを設定
WORKDIR /src

# 環境変数を設定し、ポートとホストを指定
ENV PORT 8080
ENV HOST 0.0.0.0
ENV DATABASE_URL $DATABASE_URL

# package.jsonをコピーして、パッケージのインストール
COPY package.json ./
COPY package-lock.json ./
COPY prisma/ ./prisma/
RUN npm install

# ソースをコピーして、ビルド
COPY . .
RUN npm run build

# コンテナが起動したら、nuxtを起動するよう指定
CMD [ "npm", "run", "start" ]

以上!!
これで、Pushするだけで、マイグレもデプロイもできるように(´ω`)

また、簡易的にDATABASE_URLはそのままにしているけど、
Secret Managerを使って設定するのがよいです〜

おまけ: プライベートIPを使ったCloud SQLへの接続

今回はCloud SQL Proxyを起動する方法にしているけど、
プライベートプールを利用して、静的IP範囲を定義しておけば、
プライベートIPでCloud SQLにも接続できるよう。

ただ、プライベートプールを使ったビルドは料金がかかるので、
今回はCloud SQL Proxyを起動する方法を調べました。

参考にしたサイト様