Google Cloudによる安全な継続的デプロイ

# 継続的デプロイに挑戦した背景

こんにちは。[Papillon6814](https://github.com/Papillon6814)といいます。

GCP のような大型クラウドベンダーでサービスをホスティングしている人の中で、ベンダーの提供しているサービスでできるだけ一元的にインフラ環境を整えたいと考えている人の数はそう少なくないと思います。そこには人それぞれ様々な背景や動機があると思いますが、僕は「自分が今利用しているベンダーにできるだけ詳しくなりたい」という動機から、今回 Google Cloud で継続的デプロイの設定に挑戦しました。
継続的デプロイを設定したことのない人や、Google Cloud の環境整備に興味のある人にとって役立つような記事になればいいなと思っています。

**Cloud Build**や**Artifact Registry**などの様々なサービスを扱う際、Google Cloud サポートの Lai さんに大変お世話になりました。たくさんの粗末な質問にご丁寧にお答えしていただいてありがとうございました。

# 使用技術

## サーバーサイド開発

- Elixir 1.13.4
- Phoenix 1.6.6

書いていて楽しい、最強の言語とそのフレームワークです。

## インフラ（Google Cloud Platform）

### App Engine

`Dockerfile`を用意して簡単にホスティングすることができるので選びましたが、機会があれば Cloud Run に乗り換えようと思っています。Elixir の環境を整備するためにフレキシブル環境を選択しています。

### Cloud SQL

本番環境の PostgreSQL のために用意しています。

### Cloud Build

Github の main ブランチへの push をトリガーにして継続的デプロイを実行する、今回の要となるサービスです。

### Artifact Registry

Docker のイメージを管理するためのレジストリです。Google Cloud はもともと Container Registry というサービスを提供していましたが、新しく Artifact Registry というサービスを提供し始めたのでこちらを利用しています。

### Secret Manager

GCP で機密情報を扱うためのサービスです。

その他色々な細かいサービスを使っていますが、割愛させていただきます。

# 注意事項

デプロイの際に`Dockerfile`と`cloudbuild.yml`を使用しているのですが、事情があって`Dockerfile`は中身全体を公開していません。
せっかく記事にしているのと、**Elixir**のプロジェクトを Google Cloud にデプロイしているサンプルがインターネット上に多くないことから、本当はできるだけ多くの情報を公開したいのですが、すみません。

では、ここから本題に入ろうと思います。

# 継続的デプロイの際、シークレット変数の管理をどうするか

## シークレット変数を Dockerfile で管理する

サービスのデプロイを行う際、僕たちは本番環境用データベースへの認証情報や URL など、公開したくない情報を扱わなければならないことがあります。そういった情報は git のリモートリポジトリにも上げずに、自分たちで

例えば僕は**App Engine**でサービスを運用していて、継続的デプロイを設定する前は次のように機密情報を管理していました。

- プログラム内で機密情報を記述しなければならない箇所には、`System.fetch_env!/1`（環境変数を取得する関数です）を用いる。
- 本番環境用の環境変数を`Dockerfile`に記載する。
- `Dockerfile`を`.gitignore`に登録しておき、リモートリポジトリに環境変数のデータが入らないようにする。

複雑でない方法なのでとっつきやすく、さらにデプロイの際は`gcloud app deploy`コマンドを使って手動で行っていたので、この管理方法でも問題なく運用することができていました。

## Dockerfile での管理に行き詰まりを感じる

そう短くない間上記の方式で運用をしていましたが、サービスのユーザーが増え始めると要望の数も増え、同じくエンジニアの負担も増大していきました。なのでできるだけエンジニアの負担を軽減するために、僕たちはデプロイのような自動化できる手続きを自動化することを意識し始めました。すると機密情報の管理方式は、この方法のままだと自動化の導入に行き詰まってしまうことがわかりました。

App Engine のフレキシブル環境のデプロイを自動で実行するためには、デプロイを実施するサービス（Google Cloud でいう Cloud Build です）に`Dockerfile`を自動で与えてあげられるようにしなければなりません。したがって、**`Dockerfile`をリモートリポジトリに上げる必要**がありました。しかし先述の通り、Dockerfile には機密情報が含まれています。この管理方法のままでは継続的デプロイの設定をすることができないので、僕たちは新しい管理方法を見つける必要がありました。

# 継続的デプロイの処理の手順

ではここで一度、継続的デプロイの処理が具体的にどのような手順で実行されていくかを整理してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352824/10744317-417a-a99d-a969-a293b6d8dc5d.png)

1. Github の main ブランチへの push をトリガーとして Cloud Build が起動します。Cloud Build は`cloudbuild.yml`に記載されたステップを実行してくれます。
2. Cloud Build が Dockerfile の内容を元にイメージのビルドを開始します。
3. ビルドしたイメージを Artifact Registry にアップロードします。
4. Artifact Registry にアップロードしたイメージを使用して`gcloud app deploy`を Cloud Build 上で実行します。

そして、これらのステップで使用する環境変数は Secret Manager を使って読み込みます。以上の流れを`cloudbuild.yml`で記述すると次のようになります。

```yml:cloudbuild.yml
steps:
  - id: "build the container image (latest)"
    name: "gcr.io/cloud-builders/docker"
    entrypoint: 'bash'
    args: ['-c', 'docker build -t asia-northeast1-docker.pkg.dev/${PROJECT_ID}/prof/proj:latest --build-arg CLOUD_SQL_HOST="$$CLOUD_SQL_HOST" --build-arg CLOUD_SQL_PASSWORD="$$CLOUD_SQL_PASSWORD" --build-arg DATABASE_URL="$$DATABASE_URL" --build-arg SECRET_KEY_BASE="$$SECRET_KEY_BASE" --build-arg SERVICE_JSON="$$SERVICE_JSON" .']
    secretEnv: ['CLOUD_SQL_HOST', 'CLOUD_SQL_PASSWORD', 'DATABASE_URL', 'SECRET_KEY_BASE', 'SERVICE_JSON']

  - id: "build the container image (commit sha)"
    name: "gcr.io/cloud-builders/docker"
    entrypoint: 'bash'
    args: ['-c', 'docker build -t asia-northeast1-docker.pkg.dev/${PROJECT_ID}/proj/proj:${COMMIT_SHA} --build-arg CLOUD_SQL_HOST="$$CLOUD_SQL_HOST" --build-arg CLOUD_SQL_PASSWORD="$$CLOUD_SQL_PASSWORD" --build-arg DATABASE_URL="$$DATABASE_URL" --build-arg SECRET_KEY_BASE="$$SECRET_KEY_BASE" --build-arg SERVICE_JSON="$$SERVICE_JSON" .']
    secretEnv: ['CLOUD_SQL_HOST', 'CLOUD_SQL_PASSWORD', 'DATABASE_URL', 'SECRET_KEY_BASE', 'SERVICE_JSON']

  - id: "push container image (latest)"
    name: "gcr.io/cloud-builders/docker"
    args: ["push", "asia-northeast1-docker.pkg.dev/${PROJECT_ID}/proj/proj:latest"]

  - id: "push container image (commit sha)"
    name: "gcr.io/cloud-builders/docker"
    args: ["push", "asia-northeast1-docker.pkg.dev/${PROJECT_ID}/proj/proj:${COMMIT_SHA}"]

  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: 'bash'
    args:
    - '-eEuo'
    - 'pipefail'
    - '-c'
    - 'gcloud config set app/cloud_build_timeout 1600 && gcloud app deploy --image-url=asia-northeast1-docker.pkg.dev/${PROJECT_ID}/proj/proj:latest'
timeout: 3600s

availableSecrets:
  secretManager:
  - versionName: projects/xxx/secrets/CLOUD_SQL_HOST/versions/1
    env: 'CLOUD_SQL_HOST'
  - versionName: projects/xxx/secrets/CLOUD_SQL_PASSWORD/versions/1
    env: 'CLOUD_SQL_PASSWORD'
  - versionName: projects/xxx/secrets/DATABASE_URL/versions/1
    env: 'DATABASE_URL'
  - versionName: projects/xxx/secrets/SECRET_KEY_BASE/versions/1
    env: 'SECRET_KEY_BASE'
  - versionName: projects/xxx/secrets/E_SERVER_SERVICE_JSON/versions/2
    env: 'SERVICE_JSON'
```

これらの手順を踏まえて、GCP のセットアップをしていきましょう。

# Google Cloud の準備

主に Cloud Build 関連の準備を行います。

## Secret Manager

Cloud Build が扱う環境変数を設定するために Secret Manager に環境変数を登録していきます。
`シークレットを作成`を押します。
![](https://storage.googleapis.com/zenn-user-upload/a80f21f9d9ed-20220828.png)

本番環境で使用するシークレット変数を登録します。直感的に登録することができるようになっています。
![](https://storage.googleapis.com/zenn-user-upload/2a0615a73c21-20220828.png)

## Cloud Build

次に Cloud Build でトリガーを設定します。
トリガーは main ブランチへの push イベントで起こるようにし、リポジトリなどの設定もしていきます。
![](https://storage.googleapis.com/zenn-user-upload/63997e6df37e-20220828.png)

## Dockerfile と cloudbuild.yml の準備

デプロイのための`Dockerfile`と`cloudbuild.yml`を用意します。`Dockerfile`では環境変数をそのまま書かずに、次のようにします。

```Dockerfile
ARG CLOUD_SQL_HOST
ARG CLOUD_SQL_PASSWORD
ENV CLOUD_SQL_HOST=$CLOUD_SQL_HOST
ENV CLOUD_SQL_PASSWORD=$CLOUD_SQL_PASSWORD
```

また、シークレット変数に JSON を登録している場合は次のような処理を入れて、JSON ファイルを書き出すようにします。

```Dockerfule
ARG SERVICE_JSON
ENV SERVICE_JSON=$SERVICE_JSON
...
echo $SERVICE_JSON >> /app/service_account.json
```

これで Dockerfile 側のシークレット変数の準備は OK です。
`cloudbuild.yml`は上に記載した通りです。

GCP での設定は以上です。

# main ブランチに push してデプロイ

これで継続的デプロイの設定が完了しました。
プルリクエストをマージすると Cloud Build でデプロイが走るようになっています。また、Artifact Registry にイメージが自動で push されるようにも設定しています。
ビルドの履歴は Cloud Build で閲覧することができます。