[GitHub Actions] シークレットな複数行のテキストをビルド時に使用する方法
目次
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_mounts
とvolumes
になります。
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のボリュームのマウントをうまく駆使して安全にそして機密情報の単一管理を実現しました。
ビルドと実行環境のクレデンシャルを分けたり環境変数を一つずつ入れるのは管理が大変なため我ながら綺麗に纏まったかと思います。