👨🏼‍💻

NestJS + Prisma のアプリケーションを Github Actions でGCP環境にデプロイするぞ

2023/01/24に公開

はじめに

この記事では、NestJS と Prisma で作ったアプリケーションを、Github Actions を利用し、GCP 環境にデプロイするまでの流れをまとめてみました。
似たような環境を構築しようとする際に参考になれば幸いです。

この記事でやること

  • サンプルアプリケーションの準備(nestjs、prisma)
  • GCP リソースの作成(terraform)
  • Github Actions の設定

構成は以下の通りで、Cloud Run と Cloud SQL を使ったシンプルな構成の環境です。この環境を作っていきます。

構成

イメージ図

構成を考えた時に参考にしたドキュメントまとめ

サンプルアプリケーションの準備

まずはデプロイ用に、NestJS と Prisma が動くアプリケーションを用意します。

以下のリポジトリにデプロイ用のアプリケーションを準備しました。(/posts のエンドポイントにアクセスすると post のリストが返るだけの簡単なアプリケーションです。)

このアプリケーションをデプロイしていきます。

https://github.com/shimabukuromeg/nestjs-prisma-sample/tree/sample-app

動作確認

手元の環境(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 も用意します。

https://github.com/shimabukuromeg/nestjs-prisma-sample/blob/sample-app/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

参考記事

https://zenn.dev/jrsyo/articles/e42de409e62f5d

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
  ]
}

https://github.com/shimabukuromeg/nestjs-prisma-gcp-terraform

terraform の環境構築方法
$ 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 書きはじめの書き方をよく忘れるのでこちらのページを参考

https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/google-cloud-platform-build

GCP の各サービスのドキュメントのページに terraform の定義が書いてることが多いのでその辺りも参考になりました。

Github Actions の設定

続いて、ワークフローを定義していきます。

GCP 認証

Github Actions から GCP への認証に google-github-actions/auth を使います。

https://github.com/google-github-actions/auth

GitHub Actions は OpenID Connect がサポートされており、この仕組みを利用することで、サービスアカウントキーがなくても GithubActions から GCP へ認証を行うことができます。

https://christina04.hatenablog.com/entry/workload-identity-federation

ビルド

以下、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

https://docs.github.com/ja/actions/security-guides/encrypted-secrets

  • GCP のシークレットマネージャー

https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets?hl=ja

実行

秘匿情報の指定ができたら実行します。

無事通った 🎉

動作確認

デプロイしたアプリケーションがちゃんと動いてるか確認します。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 の構築を一通りやってみました。似たような環境を作ってみたいと思ってる人の参考になれば幸いです。

参考

https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-gcp

https://scrapbox.io/pokutuna/GCP_API_を_Terraform_から有効にする

https://future-architect.github.io/articles/20210910a/

GitHubで編集を提案
株式会社モニクル

Discussion