Cloud Build で Prisma マイグレーション(Secret Manager利用)
Google Cloud Build でのマイグレーションについて調べる機会があり、Prismaで試してみることにしました。その記録を残します。
要点
- Prismaは、開発用コマンドでマイグレーションファイルを作成し、本番用コマンドでそれを本番DBへ適用する流れです
- Cloud Build から本番用コマンドを実行するためには、Cloud SQL Proxy を通して Cloud SQL へつなげる必要がある。ここSecret Managerを使います
- Cloud Build ではAppEngineの環境をラップしたコンテナイメージが呼べるので、それを使ってマイグレーションコマンドを実行します
バージョン情報
名前 | バージョン |
---|---|
Prisma | 3.8.1 |
Google Cloud SQL PostgreSQL | 14 |
ローカル環境でPrismaのマイグレーションファイルを生成する
Prismaは開発用コマンドでマイグレーションファイルを作成します。実際に試して、流れを追ってみましょう。
Prisma CLIをインストール
まずはPrisma CLIからです。公式ドキュメントをみながら進めます。
mkdir google-cloud-prisma-migrate
cd google-cloud-prisma-migrate
yarn init --yes
yarn add -D prisma typescript ts-node @types/node
次にtsconfig.json
を用意します。Prismaですが、データーベースのスキーマ情報から便利なTypeScriptの型を自動生成してくれます。セットでTypeScriptも使うことになるため、tsconfig.json
も一緒に用意します。
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
初期化します。
yarn prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
この時点ではデータベースへ接続するための設定ファイルを作成するだけですので、DBが起動している必要はありません。
ローカルのDBと接続する
yarn prisma init
でschema.prisma
が生成されるはずです。このファイルを正としてDBへ接続したりマイグレーションファイルを生成します。次のような記述があるはずです。
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
環境変数DATABASE_URL
を利用して接続することがわかります。ローカルDBのために、docker composeを使いましょう。
version: "3"
volumes:
db-data:
services:
db:
image: postgres:14
container_name: google-cloud-prisma-migrate-db
volumes:
- db-data:/var/lib/postgresql/google_cloud_prisma_migrate_db_development/data
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
起動します。
docker compose up
次に環境変数DATABASE_URL
をセットします。公式ドキュメントでは.env
を使う方針となっていますが、準備が楽という理由でdirenvを使います。
pwd
> google-cloud-prisma-migrate
direnv edit .
.envrc
の編集画面になるはずです。
export DATABASE_URL="postgresql://postgres:password@localhost:5432/google_cloud_prisma_migrate_db_development?schema=public"
保存後、ターミナルでdirenv allow
としてください。これで、google-cloud-prisma-migrate
ディレクトリにおいてDATABASE_URL
が設定されます。準備OKです。
マイグレーションファイルの生成
DBと接続する準備ができたら、スキーマファイルを編集します。さきほど「正になる」と話をした schema.prisma
ファイルへテーブル情報を追加してください。ブログサイトを想定して、記事情報を格納するためのテーブルとします。
+model Post {
+ id String @id @default(uuid())
+ title String
+ emoji String?
+ published Boolean? @default(false)
+ publishDate DateTime? @map("publish_date")
+ like Int @default(0)
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @updatedAt @map("updated_at")
+
+ @@map("posts")
+}
デフォルトだとテーブル名がパスカルケースに、カラム名がキャメルケースになるため、できるだけ馴染み深いRailsのモデル構造と合わせたい理由から@@map
を使って名前を変えています。詳細はスキーマリファレンスを参考にしてください。
あとは開発用マイグレーションコマンドを実行します。
yarn prisma migrate dev --name init
このコマンドでふたつの仕事が行われます。
-
prisma/migrations/20220127104449_init/migration.sql
の生成 - ローカルDB(DATABASE_URLでつながっている先)にマイグレーション実行
マイグレーションファイルは、本番適用時にこれが差分として参照されます。よって、gitなどでのバージョン管理対象です。SQLビューワでローカルDBへアクセスすると、データベースとテーブルが生成されていることがわかります。
ローカルで行うDBへの作業はこれで完了です。せっかくなので、posts
と一緒に作成されているテーブル、_prisma_migrations
についても軽く触れておきます。Ruby on Railsなどを触ったことがある方は察するところがあるかもしれません。ドキュメントによると以下のチェック用途です:
- データベースに対してマイグレーションが実行されたかどうか
- 適用されたマイグレーションが削除されたかどうか
- 適用されたマイグレーションが変更されたかどうか
というわけでマイグレーションの履歴を保持しておき、整合性を維持するのに役立っているようですね。
Cloud Build から Cloud SQL へマイグレーション実行
本題です。Cloud SQLにたてたPostgreSQLに対して、Cloud Buildからマイグレーションを実行します。次のように作業します。
- cloudbuild.ymlとDockerfileを作成
- Terraform で Cloud SQL と Artifact Registry を作成
- Cloud Build トリガーを実行
cloudbuild.ymlとDockerfileを作成
まずは本番環境に対するPrismaのマイグレーション方法を確認しましょう。
In production and testing environments, use the migrate deploy command to apply migrations:
npx prisma migrate deploy
というわけで prisma migrate deploy
コマンドを打つことで適用できるようです。シンプルですね。
次に、Google Cloud Build でDBマイグレーションを行う方法を調べます。
[cloud build migration]
およ...?ちょっとここは先人も苦労している様子?ただ、Cloud SQL Proxyを使う点は共通していそう。DBに接続できさえすればあとはprisma migrate deploy
でいけることはわかっているので、調べ方を変えます。
[cloud build sql proxy]
もう少し具体的な例が欲しいですね。DBマイグレーションといえばRailsなので、その線で探してみます。
[cloud build rails migration]
cloudbuild.yaml ファイルは、一般的なイメージ ビルドステップ(コンテナ イメージを作成して Container Registry に push する)だけでなく、Rails データベースの移行も行います。これにはデータベースへのアクセスが必要です。これは、app-engine-exec-wrapper(Cloud SQL Auth プロキシのヘルパー)を使用して実行されます。
コレダ。「Cloud Buildによる自動化」というさらりとしたパラグラフですが少し強調してもいいと思います!まさにこれですね。この記述です。
- 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"
]
secretEnv: ["RAILS_KEY"]
掘り下げたい要素がふたつあります:
-
gcr.io/google-appengine/exec-wrapper
って何 -
secretEnv: ["RAILS_KEY"]
って何
gcr.io/google-appengine/exec-wrapper
って何
AppEngineフレキシブル環境のラッパーだそうです。Google Cloud の人が、このようなマイグレーション用途で便利なイメージがあるといいよねってことで用意してくれてるみたいですね。
This is a wrapper Docker image that sets up an environment similar to the Google App Engine Flexible Environment, suitable for running scripts and maintenance tasks provided by an application deployed to App Engine. In particular, it ensures a suitable Cloud SQL Proxy is running in the environment.
Its driving use case is running production database migrations for Ruby on Rails applications, but it is also useful for Django applications, and we expect similar uses for other languages and frameworks.
READMEに書いてある利用例をみると、
steps:
- name: "gcr.io/google-appengine/exec-wrapper"
args: ["-i", "gcr.io/my-project/appengine/some-long-name",
"-e", "ENV_VARIABLE_1=value1", "-e", "ENV_2=value2",
"-s", "my-project:us-central1:my_cloudsql_instance",
"--", "bundle", "exec", "rake", "db:migrate"]
このようになっています。
-
-i
: 実行イメージを指定。この記事の場合、Prismaが実行できるコンテナイメージを用意することになりそう。 -
-e
: 環境変数を指定。Prismaを使うならば、DATABASE_URL
をここで指定することになりそう。 -
-s
: Cloud SQLのインスタンスを指定。 - 以降、マイグレーションコマンド
基本はRuby on Railsを想定しているようですが、コンテナイメージを指定するのでどんな環境でもいいはずです。最初、このラッパーを見つけるまでは大変そうな印象だったのですが、gcr.io/google-appengine/exec-wrapper
を使えばCloud SQLに接続した状況が楽に作れそうです。
secretEnv: ["RAILS_KEY"]
って何
もうひとつ明らかにしたいポイントです。RAILS_KEY
にように、マイグレーション実行時に秘匿情報が必要な状況もあります。Prismaを使う例でいくと、DATABASE_URL
にはDBのユーザー名やパスワードも含むことになりそうなので、DATABASE_URL
が秘匿情報にあたります。Cloud Build でシークレットを扱うには以下の記事が参考になります。
ポイントを抜粋すると、
シークレットを参照するためにはentrypointとしてbashを指定する必要があります。
argsは-cから始めるようにします。-cの後に続く文字列が実行コマンドとなります。
シークレット名には$$というプレフィックスをつけるようにします。
https://cloud.google.com/build/docs/securing-builds/use-secrets#configuring_builds_to_access_the_secret_from
この部分で、gcr.io/google-appengine/exec-wrapper
とSecret Managerを組み合わせるために、Google Cloud のRailsチュートリアルであったような書き方になっているのですね。
いったん今わかる範囲で、cloudbuild.yml
の成果物はこんな感じとイメージできます。
steps:
- id: "apply-migrations"
name: "gcr.io/google-appengine/exec-wrapper"
entrypoint: "bash"
args:
- "-c"
- "/buildstep/execute.sh -i Prismaが使えるコンテナイメージ -e DATABASE_URL=$$DATABASE_URL -s $_CLOUDSQL_INSTANCE_FULL_NAME -- yarn prisma migrate deploy"
secretEnv: ["DATABASE_URL"]
dir: "google-cloud-prisma-migrate"
timeout: 2000s
substitutions:
_CLOUDSQL_INSTANCE_FULL_NAME: "Cloud SQL インスタンスの名前(my-project:us-central1:my_cloudsql_instance みたいなやつ)"
_ARTIFACT_REPOSITORY_IMAGE_NAME: "Artifact Registry にあるイメージ名"
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/DATABASE_URL/versions/latest
env: DATABASE_URL
イメージが必要とわかったので、マイグレーションを実行できるDockerイメージを作ります。
Dockerfileを作成
この例ではアプリケーションを動かすわけではないので、PrismaのCLIが打てればOKです。あまり難しく考えずに以下のように定義します。
# syntax=docker/dockerfile:1
FROM node:16
# Prisma CLIをdevDependenciesでインストールしたため
ENV NODE_ENV=development
# マイグレーションで必要
RUN apt-get -qy update
RUN apt-get -qy install openssl
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
COPY prisma ./prisma
RUN yarn install
COPY . .
CMD ["yarn", "prisma", "migrate", "deploy"]
このイメージを作成する作業もcloudbuild.yml
で行います(後述)。
Terraform で Cloud SQL と Artifact Registry を作成
Google Cloud 上のリソースを作成します。Cloud SQL はいいとして、Artifact Registryが必要な理由を説明します。AppEngineのラッパーイメージであるgcr.io/google-appengine/exec-wrapper
はコマンド実行にあたりイメージを要求します。もともと、CloudRunアプリケーションを想定しているようで、任意のオペレーションコマンド(マイグレーションやRake Taskなど)を実行する手段としてこのラッパーを提供しているようです。今回はアプリケーションを動かす予定はありませんが、イメージを作成します。そして、そのイメージを保存し、Cloud Build から読み出せる場所としてArtifact Registryのリポジトリが必要となります。
準備(別記事に任せる)
Google Cloud のリソースを Terraform で扱うには準備が必要です。こちらの記事にまとめているので、plan
可能なところまで進めます。
Cloud SQL の定義
今回は Artifact Registry に加えて Cloud SQL も必要です。モジュールを定義します。
terraform {
variable "target_region" {
description = "デプロイするリージョン"
type = string
default = "us-central1"
}
# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database
resource "google_sql_database_instance" "prisma-migrate" {
name = "prisma-migrate"
database_version = "POSTGRES_14"
region = var.target_region
deletion_protection = false # 検証で作成するため、あとで消したい
settings {
tier = "db-f1-micro"
availability_type = "REGIONAL"
disk_size = "20"
disk_type = "PD_SSD"
ip_configuration {
ipv4_enabled = "true"
}
}
}
resource "google_sql_database" "google-cloud-prisma-migrate-db" {
name = "google_cloud_prisma_migrate_db"
instance = google_sql_database_instance.prisma-migrate.name
}
# ref: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance#attributes-reference
output "prisma_migrate_connection_name" {
value = google_sql_database_instance.prisma-migrate.connection_name
}
余談ですが、us-central1
リージョンでかなり費用は抑えられるものの、割高ですのであとで削除を忘れないようにしましょう。
Cloud Build の定義
cloudbuild.yml
を使ってビルドを実行するためのリソースです。正確には、ビルド自体はcloudbuild.yml
を使うので、Terraform で作成するのはCloud Build トリガー
というリソースです。
variable "github_owner" {}
variable "github_app_repo_name" {}
variable "region" {}
variable "gcp_project_id" {}
variable "app_name" {}
variable "cloudsql_instance_full_name" {}
resource "google_cloudbuild_trigger" "deploy-migrate" {
name = "deploy-migrate"
description = "prisma deployを実行する"
github {
owner = var.github_owner
name = var.github_app_repo_name
push {
branch = "^main$"
}
}
included_files = ["google-cloud-prisma-migrate/**"]
filename = "google-cloud-prisma-migrate/cloudbuild.yml"
substitutions = {
_CLOUDSQL_INSTANCE_FULL_NAME = var.cloudsql_instance_full_name
_ARTIFACT_REPOSITORY_IMAGE_NAME = "${var.region}-docker.pkg.dev/${var.gcp_project_id}/backend/${var.app_name}"
}
}
substitutionsでCloud SQLのインスタンス名とArtifact Registryにあるイメージ名を渡しています。
main.tf を修正
モジュールを定義したら、これらを main.tf
から呼びます。
terraform {
required_version = "~> 1.0.0"
backend "gcs" {
+ # 他のおためしリソースとバッティングしないよう、専用の状態管理ファイルをつくる
+ prefix = "tfstate/google-cloud-prisma-migrate"
}
}
## project ##
provider "google" {
project = var.gcp_project_id
region = var.primary_region
}
# イメージを保存する Artifact Registry のリポジトリ
module "artifact-registry" {
source = "./modules/artifact-registry"
gcp_project_id = var.gcp_project_id
artifact_registry_location = var.primary_region
}
+# Cloud SQL
+module "cloud-sql" {
+ source = "./modules/cloud-sql"
+ target_region = var.primary_region
+}
+
+# マイグレーションを実行する Cloud Build
+module "cloud-build" {
+ source = "./modules/cloud-build"
+ gcp_project_id = var.gcp_project_id
+ region = var.primary_region
+ cloudsql_instance_full_name = module.cloud-sql.prisma_migrate_connection_name
+ app_name = "google-cloud-prisma-migrte"
+ github_owner = "cm-wada-yusuke"
+ github_app_repo_name = "gql-nest-prisma-training"
+}
gcp_project_id
など、いくつか足りない変数があります。これらは作業する方に依存するいわば秘匿情報ですので、バージョン管理対象にできません。terrafrom.tfvars
を各々作成してください。
# https://www.terraform.io/cli/config/environment-variables
primary_region = "us-central1" # 検証目的ではこのリージョンが安くて無難です
gcp_project_id = "your-project-id"
Terraform のファイル作成は完了です。
apply 前に、GitHubリポジトリをマッピング
GitHubトリガーでCloud Buildを作る場合、あらかじめGitHubリポジトリと接続しておく必要があります。Cloud Build の画面から以下のようにして接続してください。
apply
ここまでできたらapplyできます。
./tf.sh init # 定義したモジュールの読み込み
./tf.sh apply
...(Cloud SQL のデプロイでかなり時間がかかります)
Artifact Registry のリポジトリ、Cloud SQL、Cloud Build トリガーが作成されていればOKです。もし、Google Cloud admin APIが必要と言われたら、指示にしたがって有効にし、再実行してください。
cloudbuild.yml を修正
いまの状態でもCloud Build トリガーは実行できますが、cloudbuild.yml
が不完全なため、失敗します。うやむやにしていたところが明らかになったので、cloudbuild.yml
を完成させましょう。
- コンテナイメージのビルド
- Artifact Registry へプッシュ
- プッシュしたイメージを使ってマイグレーション実行
この流れをビルドファイルへ反映します。
steps:
- id: build
name: "docker"
args:
- build
- --file=Dockerfile
- "--tag=$_ARTIFACT_REPOSITORY_IMAGE_NAME:latest"
- --cache-from=$_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
- .
dir: "google-cloud-prisma-migrate"
- id: push
name: "docker"
args:
- push
- --all-tags
- $_ARTIFACT_REPOSITORY_IMAGE_NAME
dir: "google-cloud-prisma-migrate"
waitFor: ["build"]
- id: "apply-migrations"
name: "gcr.io/google-appengine/exec-wrapper"
entrypoint: "bash"
args:
- "-c"
- "/buildstep/execute.sh -i $_ARTIFACT_REPOSITORY_IMAGE_NAME:latest -e DATABASE_URL=$$PRISMA_MIGRATE_DATABASE_URL -s $_CLOUDSQL_INSTANCE_FULL_NAME -- yarn prisma migrate deploy"
secretEnv: ["PRISMA_MIGRATE_DATABASE_URL"]
dir: "google-cloud-prisma-migrate"
waitFor: ["push"]
timeout: 2000s
substitutions:
_CLOUDSQL_INSTANCE_FULL_NAME: by-terraform
_ARTIFACT_REPOSITORY_IMAGE_NAME: by-terraform
availableSecrets:
secretManager:
- versionName: projects/$PROJECT_ID/secrets/PRISMA_MIGRATE_DATABASE_URL/versions/latest
env: PRISMA_MIGRATE_DATABASE_URL
# ビルド結果に生成したイメージ情報を表示する
# https://cloud.google.com/build/docs/building/build-containers
images:
- $_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
Secret Manager へ DATABASE_URL を登録
マイグレーションに必要なPRISMA_MIGRATE_DATABASE_URL
を登録します。登録方法はいろいろありますが、一般的にSecret Managerへ登録するということは秘匿情報なので手作業が安全ではなかろうかという考えです。画面から登録します。
- 名前:
PRISMA_MIGRATE_DATABASE_URL
- シークレットの値:
postgres://postgres:<postgresユーザーのパスワード>@localhost:5432/google_cloud_prisma_migrate_db?host=<CloudSQLのインスタンス名>
- 例:
postgres://postgres:password@localhost:5432/google_cloud_prisma_migrate_db?host=/cloudsql/sample-project:us-central1:prisma-migrate
- 例:
DBユーザーのパスワードは Cloud SQL の画面から変更できますので、新しいパスワードに設定した上でこちらのシークレットを作成してください。
マイグレーション実行
mainブランチのpushへ反応するようCloud Buildトリガーを作成したので、変更をプッシュします。これで、Cloud Build トリガーが起動するはずです。
完了後、ビルドログで、マイグレーションが実行されたっぽいテキスト出力を確認できれば成功です。
Cloud SQL Proxy でローカル接続
ローカルから接続して確認します。docker compose のファイルを作成します。
version: "3"
# Cloud SQL 環境のDBにアクセスするプロキシ
services:
prisma-migrate-db:
image: "gcr.io/cloudsql-docker/gce-proxy:latest"
container_name: prisma-migrate-db
command:
[
"/cloud_sql_proxy",
"-instances=$CLOUDSQL_PROXY_CONNECTION_NAME=tcp:0.0.0.0:5432",
"-credential_file=/gcp-key.google-cloud-prisma-migrate.json",
]
volumes:
- type: bind # ファイルが存在しなければエラーとする
source: "~/.config/gcloud/application_default_credentials.json"
target: "/gcp-key.google-cloud-prisma-migrate.json"
ports:
- "45432:5432" # ローカルのDBとぶつからないようにポート変更
direnv edit .
として CLOUDSQL_PROXY_CONNECTION_NAME
を追加します。設定する値は、Cloud SQL の接続名ですので、your-project-name:us-central1:prisma-migrate
のような文字列です。その後、docker compose
で起動します。
docker compose -f docker-compose.sql-proxy.yml up
このDockerコンテナを起動している間、SQLクライアントからは、ローカルDBへ接続する要領で設定できます。この場合のデータベースURLはpostgresql://localhost:45432/google_cloud_prisma_migrate_db
です。ユーザー名とパスワードはCloud SQL上のものを指定する必要があります。
Cloud SQL上のデータベースもマイグレーションが実行され、テーブルとカラムが作成されていることがわかります。
後片付け
Cloud SQL インスタンスはそれなりに高いので、検証が終わったら掃除しましょう。
./tf.sh destroy
以上で完了です。
おわりに
先人様方の知恵を集めて、Cloud Build から Prisma マイグレーションを実行できました。一度実行できたら、以降は同じ流れです。
- ローカル環境で
yarn prisma migrate dev --name modify
など開発作業 - main ブランチにマージなりプッシュなり行う
- Cloud Build トリガーが走り、
yarn prisma migrate deploy
が走る - 未適用のマイグレーションがすべてCloud SQLへ適用される
最初の設定がやや大変ですが、一度やってしまえばあとは自動でローカルDBとCloud SQLの同期がとれるので便利ですね。皆様が Cloud Build でマイグレーションを実行する際の助けになれば幸いです。
ソースコード
GitHubで公開しています。大丈夫だとは思うのですが万が一秘匿情報が含まれていたらこっそり教えてください...
Discussion