目次

Introduction

Next.jsを使用したプロジェクトでGithubActionsでCICDを構築しました。
Next.jsのビルド時に.env.localを使用した環境変数が必要みたいだったので、GCPのSecretManagerからシークレットを取得してビルドしたのですが、Next.jsのビルド時にエラーが発生しました。エラーを調べてみると環境変数が読み込まれてませんでした。
調べてみると執筆時点では、GithubActionsは改行を挟む文字列の出力を苦手みたいでした。そんなGitHubActionsと私の格闘備忘録になります。

はじめに

今回はTips記事なのでNext.jsを動かすためのCloudRunと付属するリソースのterraformコードが登場しますが、肝心なところはGithubActionsの設定ファイルと実際にビルドするところのDockerfileになります。
環境は前回のCloudRunでアプリケーションを動かすチュートリアルで作成したCloudRunにNext.jsが動いている前提で説明していきます。
今回のポイントです。

  • env.localは機密情報のためビルドされたイメージには残らないようにしたいので、DockerのSecretMountを使用
  • また、CloudRunで立ち上がる際に機密情報を参照できるように「ボリュームのマウント」とシンボリックリンクを活用
  • GithubActionsで対応できない機密情報の複数行テキストの変数をファイルに保存するために、catコマンドの入力・出力リダイレクトをうまく利用

HandOn

SecretManagerとCloudRunの用意

では、まず今回のメインである機密情報を保存するためのSecretMangerのテンプレートを作成します

resource "google_secret_manager_secret" "cloudrun-secret" {
  secret_id = "cloudrun-secret"

  replication {
    user_managed {
      replicas {
        location = "asia-northeast1"
      }
    }
  }
}
resource "google_secret_manager_secret_version" "cloudrun-secret-version" {
  secret = google_secret_manager_secret.cloudrun-secret.id

  secret_data = var.input-secret-text
}

var.input-secret-textは、複数行のテキストになります。例えば以下のような例になります。

DATABASE_PASSWORD=XXXXXXX
API_KEY=YYYYYY

次に、Next.jsが動くCloudRunのテンプレートです。前回に追記していきます

# https://github.com/GoogleCloudPlatform/terraform-google-cloud-run
module "cloudrun" {
  source  = "GoogleCloudPlatform/cloud-run/google"
  version = "~> 0.8.0"

  service_name = "${local.env}-${local.project}"
  project_id   = local.project-id
  location     = local.location
  image        = "${local.location}-docker.pkg.dev/${local.project-id}/${local.project}/${local.env}-${local.project}-cloudrun:latest"
  template_annotations = {
    "autoscaling.knative.dev/maxScale" = 1
    "autoscaling.knative.dev/minScale" = 0
  }
  ports = {
    name = "http1"
    port = 3001
  }
  limits = {
    cpu    = "1000m"
    memory = "256Mi"
  }
  members = ["allUsers"]
  volume_mounts = [
    {
      name       = "cloudrun-secret"
      mount_path = "/app/apps/web/envs"
    }
  ]
  volumes = [
    {
      name = "cloudrun-secret"
      secret = [
        {
          secret_name = google_secret_manager_secret.cloudrun-secret.secret_id
          items = {
            key  = "latest"
            path = "cloudrun-secret"
          }
        }
      ]
    }
  ]
  service_account_email = module.cloudrun_service_account.email
}

追記したところは、volume_mountsvolumesになります。

  volume_mounts = [
    {
      name       = "cloudrun-secret"
      mount_path = "/app/apps/web/envs"
    }
  ]
  volumes = [
    {
      name = "cloudrun-secret"
      secret = [
        {
          secret_name = google_secret_manager_secret.cloudrun-secret.secret_id
          items = {
            key  = "latest"
            path = "cloudrun-secret"
          }
        }
      ]
    }
  ]

アプリケーションによって変わりますが、今回の例は/app/apps/web/envs/cloudrun-secretにSecretManagerのデータをおきます。

SecretManagerの値をGithubActionsで使用できるようにする

SecretManagerの値をファイルに格納

ボリュームのマウントでSecretManagerの値をCloudRunで参照できるようになりました。 次にGithubActionsからSecretManagerのデータを取得するようにします。
前回のCloudRunでアプリケーションを動かすチュートリアルのworkflowsの設定ファイルに追記します。

name: Deploy to gcp cloudrun
on:
  workflow_dispatch:
  push:
    branches:
      - develop
permissions:
  id-token: write
  contents: read

env:
  WORKLOAD_IDENTITY_PROVIDER: projects/xxxxxx/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
  GAR_REPO: tech-blog
  DEPLOY_REGION: asia-northeast1

jobs:
  BuildAndDeploy:
    timeout-minutes: 20
    runs-on: ubuntu-22.04
    steps:
    # env
      - name: Env Each. Env Setting
        if: github.ref == 'refs/heads/develop'
        run: |
          echo NEXT_PUBLIC_SERVICE_ENV=development >> $GITHUB_ENV
          echo GCP_PROJECT_ID=development-project >> $GITHUB_ENV
          echo SERVICE_NAME=development-tech-blog >> $GITHUB_ENV          
      - name: Common. Env Setting
        run: |
          echo GCP_SERVICE_ACCOUNT=github-actions-deploy@${{ env.GCP_PROJECT_ID }}.iam.gserviceaccount.com >> $GITHUB_ENV
          echo GAR_DOCKER_REPO=${{ env.DEPLOY_REGION }}-docker.pkg.dev/${{ env.GCP_PROJECT_ID }}/${{ env.GAR_REPO }}/${{ env.SERVICE_NAME }} >> $GITHUB_ENV          

    # setting
      - name: Github 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.GCP_SERVICE_ACCOUNT }}
      - name: Setup Cloud SDK
        uses: google-github-actions/setup-gcloud@v1
      - name: Authorize Docker push
        run: gcloud auth configure-docker ${{ env.DEPLOY_REGION }}-docker.pkg.dev
      - id: secrets
        uses: 'google-github-actions/get-secretmanager-secrets@v1'
        with:
          secrets: |-
            cloudrun-secret:projects/xxxxxx/secrets/cloudrun-secret            
      - name: Create SecretDotEnv
        run: |
          cat <<EOF > .env
          ${{ steps.secrets.outputs.cloudrun-secret }}
          EOF          

    # build
      - name: Pull Latest Image
        run: docker pull ${{ env.GAR_DOCKER_REPO }}:latest || true
      - name: Build Docker Image
        run: |-
          docker build -f Dockerfile . \
            -t ${{ env.GAR_DOCKER_REPO }}:${{ github.sha }} \
            -t ${{ env.GAR_DOCKER_REPO }}:latest \
            --secret id=cloudrun-secret,src=.env \
            --cache-from ${{ env.GAR_DOCKER_REPO }}:latest          
      - name: Push
        run: docker push --all-tags ${{ env.GAR_DOCKER_REPO }}

    # deploy
      - name: Deploy to CloudRun
        run: |-
          gcloud run deploy $SERVICE_NAME \
            --project=${{ env.GCP_PROJECT_ID }} \
            --image=${{ env.GAR_DOCKER_REPO }}:${{ github.sha }} \
            --region=${{ env.DEPLOY_REGION }} \
            --service-account=${{ env.GCP_SERVICE_ACCOUNT }} \
            --allow-unauthenticated \
            --no-use-http2          

追記したところは、2箇所あります。
まず、SecretManagerから取得してその値をファイルに格納するところです。

      - id: secrets
        uses: 'google-github-actions/get-secretmanager-secrets@v1'
        with:
          secrets: |-
            cloudrun-secret:projects/xxxxxx/secrets/cloudrun-secret            
      - name: Create SecretDotEnv
        run: |
          cat <<EOF > .env
          ${{ steps.secrets.outputs.cloudrun-secret }}
          EOF          

実は、前回の記事でseviceAccountにSecretManagerにアクセスできる権限を付与しているので、これでGitHubActionsからSecretManagerの値を取得することができます。 secretManagerから値を取得するところは調べれば結構出てくると思うのですが、その値をファイルに格納するのにかなり苦労しました。通常の文字列をファイルにリダイレクトさせる

echo $cloudrun-secret > .env

だとうまくいきませんでした。調べてみるとGithubActionsは複数行を出力することが難しいようで最初の行しか出力されないようです。格闘した挙句catコマンドの入力・出力リダイレクトを使用して期待した挙動にすることができました。できるようになってしまえばなぜすぐにこの方法が出てこなかったのかと思いましたが、なかなか思いつかず苦戦しました。

SecretMount

次に注目するところは、--secret id=cloudrun-secret,src=.envです。
secretオプションを使用することでファイル形式で機密情報をビルド時に渡すことが可能になります。
secretMountで渡られたファイルは、Dockerfileで--mount=type=secret,id=cloudrun-secretと記載することにより参照されます。
実際のSecretMountで渡されたファイルを取り出すDockerfileをNextjsを例に記載します。

FROM node:20-slim AS builder

ENV HOME=/build
WORKDIR $HOME

COPY package-lock.json \
  package.json \
  $HOME/

COPY apps/web $HOME/apps/web
# SecretMountで渡ってきた.envでビルド
RUN --mount=type=secret,id=cloudrun-secret cp /run/secrets/cloudrun-secret $HOME/apps/web/.env

RUN npm ci
RUN npm run build

FROM node:20-slim AS release
ENV NODE_ENV=production
ENV HOME=/app
WORKDIR $HOME

COPY --from=builder /build/apps/web/.next/standalone $HOME/

# CloudRunで動くときにSecretManagerからMountで環境変数ファイルを参照するためにシンボリックリンクをはる
RUN ln -s $HOME/apps/web/envs/cloudrun-secret $HOME/apps/web/.env.local

CMD npm run start

これでSecretManagerにあるファイル形式の機密情報をビルド環境とCloudRunの実行環境両方で使用することができました。

終わりに

SecretMangerで.env形式で保存した機密情報をGitHubActionsとCloudRunの実行環境両方で使用する方法を記載しました。例として記載したコードはわかりにくいと思いますが、自分用なのでご了承ください。
GitHubActionsのSecretMountとCloudRunのボリュームのマウントをうまく駆使して安全にそして機密情報の単一管理を実現しました。 ビルドと実行環境のクレデンシャルを分けたり環境変数を一つずつ入れるのは管理が大変なため我ながら綺麗に纏まったかと思います。