継続的デプロイに挑戦した背景
こんにちは。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 には機密情報が含まれています。この管理方法のままでは継続的デプロイの設定をすることができないので、僕たちは新しい管理方法を見つける必要がありました。
継続的デプロイの処理の手順
ではここで一度、継続的デプロイの処理が具体的にどのような手順で実行されていくかを整理してみます。
- Github の main ブランチへの push をトリガーとして Cloud Build が起動します。Cloud Build は
cloudbuild.yml
に記載されたステップを実行してくれます。 - Cloud Build が Dockerfile の内容を元にイメージのビルドを開始します。
- ビルドしたイメージを Artifact Registry にアップロードします。
- Artifact Registry にアップロードしたイメージを使用して
gcloud app deploy
を Cloud Build 上で実行します。
そして、これらのステップで使用する環境変数は Secret Manager を使って読み込みます。以上の流れを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 に環境変数を登録していきます。
シークレットを作成
を押します。
本番環境で使用するシークレット変数を登録します。直感的に登録することができるようになっています。
Cloud Build
次に Cloud Build でトリガーを設定します。
トリガーは main ブランチへの push イベントで起こるようにし、リポジトリなどの設定もしていきます。
Dockerfile と cloudbuild.yml の準備
デプロイのためのDockerfile
とcloudbuild.yml
を用意します。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 ファイルを書き出すようにします。
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 で閲覧することができます。