Phoenixで作ったAPIサーバーをCloud Runにデプロイした手順のメモ。勉強を兼ねた備忘のための個人的なメモであり、実務上の参考とすることは意図してません。また、ところどころ省略します。
まず、miseでElixirとErlangを入れます。
[tools]
elixir = "1.20.4"
erlang = "29.1"
早速Phoenixプロジェクトを作成します。今回はEctoも含めて種々の設定を無効化しておきます。
mix phx.new . --app my_awesome_app --no-ecto --no-html --no-assets --no-gettext --no-mailer
mix setup
Phoenixプロジェクトを作成した後、適当にヘルスチェック用のAPIを生やしておきます。
# lib/my_awesome_app_web/router.ex
scope "/", MyAwesomeAppWeb do
get "/health", HealthController, :show
end
# lib/my_awesome_app_web/controllers/health_controller.ex
defmodule MyAwesomeAppWeb.HealthController do
use MyAwesomeAppWeb, :controller
def show(conn, _params) do
json(conn, %{status: "ok"})
end
end
Phoenixは、本番環境においては force_ssl によってHTTPでのリクエストをHTTPSにリダイレクトします。ところが、Cloud Runではコンテナの前段にあるGoogle Front EndでHTTPSの通信を復号してHTTPに戻します。そのため、クライアントからHTTPSでアクセスしても、PhoenixはHTTPでリクエストを受け取ってHTTPSへリダイレクトしようとするので、リダイレクトが繰り返されてしまいます。
Google Front Endはリクエストをコンテナに転送する時に X-Forwarded-Proto: https ヘッダーを付け、HTTPSでリクエストを受けたことを示します。そこで、rewrite_on: [:x_forwarded_proto] を設定して、Google Front Endが付けた X-Forwarded-Proto を見てHTTPSのリクエストとして扱うようにします。
ただし、先ほど作成したヘルスチェック用のAPIについては例外にしておきます。ヘルスチェック用のAPIは、Cloud Runへのデプロイ時にプローブのパスとして指定します。プローブはGoogle Front Endを通らずにCloud Runの内部からリクエストされるので、X-Forwarded-Proto: https ヘッダーが付きません。force_ssl の対象のままだとプローブが失敗してコンテナが起動できないと判断されてしまいます。そのため、ヘルスチェック用のAPIは force_ssl の対象から除外しておきます。
# config/prod.exs
config :my_awesome_app, MyAwesomeAppWeb.Endpoint,
force_ssl: [
rewrite_on: [:x_forwarded_proto],
exclude: [
paths: ["/health"],
# 略
]
]
ローカルでDockerイメージをビルドして起動し、ローカル環境でのAPIの応答を確認します。
mix phx.gen.release --docker
docker build -t my_awesome_app .
mix compile
docker run --rm -p 4000:8080 -e PORT=8080 -e SECRET_KEY_BASE=$(mix phx.gen.secret) -e PHX_HOST=localhost my_awesome_app
curl -i http://localhost:4000/health
なお、Apple SiliconのMacで linux/amd64 のイメージをビルドするとBEAMが起動できずに失敗するので、Dockerfileのビルド用ステージに以下の1行を足しておきます。
ENV ERL_FLAGS="+JMsingle true"
Dockerのイメージを置くためのリポジトリとしてArtifact Registryを作成します。
gcloud services enable artifactregistry.googleapis.com
gcloud artifacts repositories create my-awesome-app --repository-format=docker --location=asia-northeast1 --description="My Awesome App API server images"
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
本番用のシークレットをSecret Managerに登録します。先に mix compile しておかないと、mix phx.gen.secret の実行時に標準出力されるコンパイル時のメッセージがシークレットに混入します。
gcloud services enable secretmanager.googleapis.com
mix compile
mix phx.gen.secret | tr -d '\n' | gcloud secrets create my-awesome-app-secret-key-base --data-file=- --replication-policy=automatic
Cloud RunはデフォルトではCompute Engineのデフォルトサービスアカウントで動きますが、このサービスアカウントは権限が広いです。そのため、権限の狭い専用のサービスアカウントを作って使うようにします。このサービスアカウントにはシークレットの読み取り権限だけ与えればOKです。
gcloud iam service-accounts create my-awesome-app-runner --display-name="My Awesome App Cloud Run"
gcloud secrets add-iam-policy-binding my-awesome-app-secret-key-base --member="serviceAccount:my-awesome-app-runner@<PROJECT_ID>.iam.gserviceaccount.com" --role="roles/secretmanager.secretAccessor"
Cloud Runで動くのは linux/amd64 なので、--platform linux/amd64 を指定してビルドします。イメージにはGitのコミットハッシュをタグ付けしておきます。
export IMAGE=asia-northeast1-docker.pkg.dev/<PROJECT_ID>/my-awesome-app/my-awesome-app:$(git rev-parse --short HEAD)
docker buildx build --platform linux/amd64 -t "$IMAGE" --push .
ビルドできたらデプロイします。--service-account オプションで先ほど作成したサービスアカウントを指定してます。--set-secrets で、先ほど作成したシークレットを環境変数 SECRET_KEY_BASE として渡しています。--set-env-vars でホストを指定していますが、Cloud RunのURLは https://<サービス名>-<プロジェクト番号>.<リージョン>.run.app となるのでデプロイを待たずとも決定できます。--startup-probe=httpGet.path で /health を渡してますが、これが前述のプローブの設定です。
gcloud run deploy my-awesome-app \
--image="$IMAGE" \
--region=asia-northeast1 \
--service-account=my-awesome-app-runner@<PROJECT_ID>.iam.gserviceaccount.com \
--set-secrets=SECRET_KEY_BASE=my-awesome-app-secret-key-base:latest \
--set-env-vars=PHX_HOST=my-awesome-app-<PROJECT_NUMBER>.asia-northeast1.run.app \
--allow-unauthenticated \
--cpu=1 \
--memory=512Mi \
--min-instances=0 \
--max-instances=2 \
--startup-probe=httpGet.path=/health \
--liveness-probe=httpGet.path=/health
curlでヘルスチェックを叩いて、{"status":"ok"} が返ってくればOKです。
curl -i https://my-awesome-app-<PROJECT_NUMBER>.asia-northeast1.run.app/health