NestJS + Prisma のアプリケーションを Github Actions でGCP環境にデプロイするぞ
はじめに
この記事では、NestJS と Prisma で作ったアプリケーションを、Github Actions を利用し、GCP 環境にデプロイするまでの流れをまとめてみました。
似たような環境を構築しようとする際に参考になれば幸いです。
この記事でやること
- サンプルアプリケーションの準備(nestjs、prisma)
- GCP リソースの作成(terraform)
- Github Actions の設定
構成は以下の通りで、Cloud Run と Cloud SQL を使ったシンプルな構成の環境です。この環境を作っていきます。
構成
構成を考えた時に参考にしたドキュメントまとめ
-
Cloud SQL をプライベート IP で使う方法
-
CloudRun から VPC にアクセスする方法
- 図でまとまってる
- 実際の構築手順がまとまってる
-
クイックスタート: Cloud Run から Cloud SQL for PostgreSQL に接続する | Google Cloud
サンプルアプリケーションの準備
まずはデプロイ用に、NestJS と Prisma が動くアプリケーションを用意します。
以下のリポジトリにデプロイ用のアプリケーションを準備しました。(/posts
のエンドポイントにアクセスすると post のリストが返るだけの簡単なアプリケーションです。)
このアプリケーションをデプロイしていきます。
動作確認
手元の環境(Mac)で動かす場合は、以下のコマンドを実行して動かすことができます。
$ docker compose up -d
$ yarn prisma migrate reset
$ yarn start:dev
別ターミナルを開いて、posts のエンドポイントを叩くと migration reset で登録された seed のデータがレスポンスが返ってきます。
$ curl http://localhost:3000/posts
[{"id":"fa119cb6-9135-57f5-8a5a-54f28d566d0e","title":"タイトル1","createdAt":"2022-01-30T19:34:22.000Z","updatedAt":"2022-01-30T19:34:22.000Z"},{"id":"fa119cb6-9135-57f5-8a5a-54f28d566d0b","title":"タイトル2","createdAt":"2022-01-30T19:34:22.000Z","updatedAt":"2022-01-30T19:34:22.000Z"},{"id":"fa119cb6-9135-57f5-8a5a-54f28d566d0c","title":"タイトル3","createdAt":"2022-01-30T19:34:22.000Z","updatedAt":"2022-01-30T19:34:22.000Z"}]%
CloudRun にデプロイする際に必要となるイメージを作る Dockerfile も用意します。
Dockerfile
Dockerfile
#==================================================
# Build Layer
FROM --platform=linux/amd64 node:18 as build
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --non-interactive --frozen-lockfile
RUN yarn prisma generate
COPY . .
RUN yarn build
#==================================================
# Package install Layer
FROM --platform=linux/amd64 node:18 as node_modules
WORKDIR /app
COPY package.json yarn.lock ./
COPY prisma ./prisma
RUN yarn install --non-interactive --frozen-lockfile --prod
RUN yarn prisma generate
#==================================================
# Run Layer
FROM --platform=linux/amd64 node:18-slim as node
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist /app/dist
COPY --from=build /app/prisma /app/prisma
COPY --from=node_modules /app/package.json /app/yarn.lock ./
COPY --from=node_modules /app/node_modules /app/node_modules
CMD ["/usr/local/bin/yarn", "start:prod"]
.dockerignore
.git/
dist/
node_modules/
.env
参考記事
GCP のリソース作成
続いて、アプリケーションをデプロイするインフラを terraform で作成します。
CloudRun や DB、IAM、などのリソースを作成します。
作成するリソース
- Artifact Registry
- VPC
- VPC Access Connector
- CloudSQL
- CloudRun
- IAM
- Workload Identity(GitHub Actions から OIDC を使用して 認証を行う際に利用)
以下のリポジトリに上記リソースを定義した HCL を用意しました。この定義を使って環境を作ります。
メインの処理抜粋(main.tf)
- main.tf (詳細はリポジトリ参照)
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}
# APIの有効化
module "service" {
source = "./modules/service"
project_id = var.project_id
}
# Artifact Registry
module "artifact_registry" {
source = "./modules/artifact_registry"
project_id = var.project_id
location = var.region
repository_id = "my-repo"
depends_on = [
module.service
]
}
module "vpc" {
source = "./modules/vpc"
project_id = var.project_id
region = var.region
depends_on = [
module.service
]
}
module "cloud_sql" {
source = "./modules/cloud_sql"
project_id = var.project_id
region = var.region
db-network-id = module.vpc.vpc_id
depends_on = [
module.vpc,
module.service
]
}
resource "random_id" "pool_id_suffix" {
byte_length = 4
}
module "workload_identity" {
source = "./modules/workload_identity"
project_id = var.project_id
region = var.region
pool_id = "my-pool-${random_id.pool_id_suffix.hex}"
depends_on = [
module.service
]
}
module "iam" {
source = "./modules/iam"
project_id = var.project_id
region = var.region
pool_id = module.workload_identity.workload_identity_provider_pool.name
repository = var.repository
depends_on = [
module.workload_identity,
module.service
]
}
module "cloud_run" {
source = "./modules/cloud_run"
project_id = var.project_id
region = var.region
vpc_access_connector_id = module.vpc.vpc_access_connector_id
db_connection_name = module.cloud_sql.db_connection_name
service_account_name = module.iam.cloud_run_sa_service_account_name
depends_on = [
module.iam,
module.service
]
}
terraform の環境構築方法
- GCP アカウントを作成する
- GCP プロジェクトを作成する(課金を有効にする)
https://cloud.google.com/billing/docs/how-to/modify-project?hl=ja - Google Cloud CLI をインストールする
https://cloud.google.com/sdk/docs/downloads-interactive?hl=ja
$ curl https://sdk.cloud.google.com | bash
$ exec -l $SHELL
$ gcloud init
$ gcloud version
- Terraform のインストール
$ brew install tfenv
$ tfenv --version
$ tfenv install 1.3.0
$ tfenv use 1.3.0
$ terraform version
terraform.tfvars
に GCP のプロジェクト ID などの指定をしてあげたのち、以下のコマンドを実行してリソースを作成します。
# 環境変数を指定する
$ cp terraform.tfvars.example terraform.tfvars
$ terraform plan
$ terraform apply
参考記事
terraform 書きはじめの書き方をよく忘れるのでこちらのページを参考
GCP の各サービスのドキュメントのページに terraform の定義が書いてることが多いのでその辺りも参考になりました。
Github Actions の設定
続いて、ワークフローを定義していきます。
GCP 認証
Github Actions から GCP への認証に google-github-actions/auth を使います。
GitHub Actions は OpenID Connect がサポートされており、この仕組みを利用することで、サービスアカウントキーがなくても GithubActions から GCP へ認証を行うことができます。
ビルド
以下、NestJS と Prisma のイメージをビルドし、Artifact Registry に Push するまでのワークフローの定義です。
`.github/workflows/build-image.yml`
.github/workflows/build-image.yml
name: Build
on:
workflow_call:
secrets: # workflow_call の時に secrets の扱いに若干ハマった https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callsecrets
WORKLOAD_IDENTITY_PROVIDER:
required: true
SERVICE_ACCOUNT:
required: true
IMAGE_TAG_BASE:
required: true
concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
cancel-in-progress: true
env:
WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
IMAGE_HOST: "asia-northeast1-docker.pkg.dev"
IMAGE_TAG_BASE: ${{ secrets.IMAGE_TAG_BASE }} # 例. プロジェクトID/artifact_registry名/イメージ名
jobs:
build-push:
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout
uses: actions/checkout@v3
- id: "auth"
name: "Authenticate to Google Cloud"
uses: "google-github-actions/auth@v1"
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.SERVICE_ACCOUNT }}
- name: "Set up Cloud SDK"
uses: "google-github-actions/setup-gcloud@v1"
- name: Docker Setup
id: buildx
uses: docker/setup-buildx-action@v2
- name: Authorize Docker push
id: authorize-docker-push
shell: bash
run: gcloud auth configure-docker ${{ env.IMAGE_HOST }}
- name: Docker Image Build and Push
id: docker-build
uses: docker/build-push-action@v3
with:
push: true
provenance: false
tags: "${{ env.IMAGE_HOST }}/${{ env.IMAGE_TAG_BASE }}:latest"
build-args: |
APP_VERSION=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
マイグレーション & デプロイ
以下、Prisma のマイグレーションを実行し、 NestJS、Prisma のイメージを CloudRun にデプロイするまでのワークフロー定義です。
`.github/workflows/deploy-to-cloud-run.yml`
.github/workflows/deploy-to-cloud-run.yml
name: Deploy to Cloud Run
on:
workflow_call:
secrets: # workflow_call の時に secrets の扱いに若干ハマった https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callsecrets
WORKLOAD_IDENTITY_PROVIDER:
required: true
SERVICE_ACCOUNT:
required: true
RUN_SERVICE_ACCOUNT:
required: true
PROJECT_ID:
required: true
IMAGE_TAG_BASE:
required: true
DATABASE_URL:
required: true
concurrency:
group: "${{ github.workflow }}-${{ github.head_ref || github.ref }}"
cancel-in-progress: true
env:
PROJECT_ID: ${{ secrets.PROJECT_ID }}
WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }}
RUN_SERVICE_ACCOUNT: ${{ secrets.RUN_SERVICE_ACCOUNT }}
VPC_CONNECTOR: "vpc-con"
GCP_REGION: asia-northeast1
GCP_CLOUD_RUN_SERVICE_NAME: "api"
IMAGE_HOST: "asia-northeast1-docker.pkg.dev"
IMAGE_TAG_BASE: ${{ secrets.IMAGE_TAG_BASE }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- name: Checktout
uses: actions/checkout@v3
- id: "auth"
name: "Authenticate to Google Cloud"
uses: "google-github-actions/auth@v1"
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.SERVICE_ACCOUNT }}
- name: "Set up Cloud SDK"
uses: "google-github-actions/setup-gcloud@v1"
with:
install_components: "beta"
- name: "Run migration"
id: migrate
run: |
gcloud config set run/region $GCP_REGION
set +e
gcloud beta run jobs describe migration --quiet >/dev/null
job_exists_check=$?
set -e
if [ $job_exists_check -eq 0 ]; then
jobs_cmd="update"
else
jobs_cmd="create"
fi
echo ": $jobs_cmd a job"
gcloud beta run jobs $jobs_cmd migration --image="${{ env.IMAGE_HOST }}/${{ env.IMAGE_TAG_BASE }}:latest" --max-retries=1 --args="yarn,prisma,migrate,reset,-f" --service-account="$RUN_SERVICE_ACCOUNT" --vpc-connector="$VPC_CONNECTOR" --set-secrets="DATABASE_URL=DATABASE_URL:latest"
gcloud beta run jobs execute migration --wait --format=json
deploy:
runs-on: ubuntu-latest
needs:
- migrate
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout
uses: actions/checkout@v3
- id: "auth"
name: "Authenticate to Google Cloud"
uses: "google-github-actions/auth@v1"
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.SERVICE_ACCOUNT }}
- name: "Set up Cloud SDK"
uses: "google-github-actions/setup-gcloud@v1"
- name: CloudRun Deploy
id: deploy
uses: google-github-actions/deploy-cloudrun@v0
with:
project_id: ${{ env.PROJECT_ID }}
image: ${{ env.IMAGE_HOST }}/${{ env.IMAGE_TAG_BASE }}:latest
region: ${{ env.GCP_REGION }}
service: ${{ env.GCP_CLOUD_RUN_SERVICE_NAME }}
flags: |
--concurrency=200
--max-instances=10
--min-instances=1
--cpu=1
--memory=640Mi
secrets: DATABASE_URL=DATABASE_URL:latest
秘匿情報
上記ワークフローの定義では秘匿情報を以下で管理してます。
- Github Actions の secrets
- GCP のシークレットマネージャー
実行
秘匿情報の指定ができたら実行します。
無事通った 🎉
動作確認
デプロイしたアプリケーションがちゃんと動いてるか確認します。CloudRun の URL にアクセスして、DB の値が帰ってきたら OK
$ curl <CloudRunのURL>/posts
[{"id":"fa119cb6-9135-57f5-8a5a-54f28d566d0e","title":"タイトル1","createdAt":"2022-01-30T19:34:22.000Z","updatedAt":"2022-01-30T19:34:22.000Z"},{"id":"fa119cb6-9135-57f5-8a5a-54f28d566d0b","title":"タイトル2","createdAt":"2022-01-30T19:34:22.000Z","updatedAt":"2022-01-30T19:34:22.000Z"},{"id":"fa119cb6-9135-57f5-8a5a-54f28d566d0c","title":"タイトル3","createdAt":"2022-01-30T19:34:22.000Z","updatedAt":"2022-01-30T19:34:22.000Z"}]%
後片付け
最後に terraform destroy でリソースを削除します。
$ terraform destroy
おわりに
以上、NestJS + Prisma のアプリケーションを Github Actions で GCP 環境にデプロイするまでのインフラ、CI の構築を一通りやってみました。似たような環境を作ってみたいと思ってる人の参考になれば幸いです。
参考
Discussion