Cloud Run + Cloud Build + TerraformでCI/CDビルドに対応したFastAPI HTTPサーバを立てる
Cloud Run + Cloud Buildを使ったインフラで動くFastAPI HTTPサーバを自動構築できるよう、Terraformを使って構築する方法を調査しました。
全体フロー
サンプルコード
実装したコードを以下に保存していますので、ご参考ください。
HTTPサーバ
以下の形で、FastAPIを使って疎通確認用のAPIのみ定義しておきます。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root_api():
return {"status": "ok"}
Terraform
以下はterraform上では管理しません。GCPのコンソールから別途定義しています。
- terraform操作用のサービスアカウントおよび、そのIAM
- 当該サービスアカウントについて、以下IAMを付与しています。
- セキュリティ管理者: IAMポリシーを取得・設定できるようにする
- 編集者: リソースを取得・変更できるようにする
- 当該サービスアカウントについて、以下IAMを付与しています。
API有効化
今回のケースにおいて、サービスごとに以下APIが必要なので設定します。
- Cloud Build
cloudbuild.googleapis.com
-
cloudresourcemanager.googleapis.com
: GCPのプロジェクト・リソース情報を取得するために必要
- Cloud Run
run.googleapis.com
-
containerregistry.googleapis.com
: Cloud Run向けのDockerイメージの格納、参照に必要
variable "gcp_service_list" {
type = list(string)
default = [
"cloudbuild.googleapis.com",
"containerregistry.googleapis.com",
"run.googleapis.com",
"cloudresourcemanager.googleapis.com"
]
}
resource "google_project_service" "activate_gcp_services" {
for_each = toset(var.gcp_service_list)
service = each.key
}
Cloud Build
以下の2点を行います。
- Cloud Runデプロイ用に、Cloud Buildサービスアカウントに対しIAM付与
- Cloud Run デベロッパー
- サービス アカウント ユーザー
- Cloud Buildトリガーを定義
variable "project_id" {
description = "gcp project id"
type = number
}
resource "google_project_iam_member" "cloudbuild_iam" {
for_each = toset([
"roles/run.developer",
"roles/iam.serviceAccountUser"
])
role = each.key
member = "serviceAccount:${var.project_id}@cloudbuild.gserviceaccount.com"
project = var.project_id
}
resource "google_cloudbuild_trigger" "cloudbuild_sample_api" {
location = "us-central1"
name = "cloudbuild-sample-api"
filename = "cloudbuild.yaml"
github {
owner = "Niccari"
name = "cloud_run_http_server_terraform_example"
push {
branch = "^main$"
}
}
included_files = [
"**/*.py",
"Dockerfile",
"Pipfile*",
"requirements.txt",
]
}
Cloud Run
以下2点を設定します。
- デプロイパラメータの設定
- tfstateで変更を追わないパラメータの設定
デプロイパラメータの設定
以下の通り設定しています。
-
autogenerate_revision_name
: デプロイするとき自動的にリビジョン名を付与します。falseの場合、template.metadata.nameにてリビジョン名を都度指定する必要があります[1] -
template.spec.timeout_seconds
: リクエストのタイムアウト時間[秒] -
template.spec.container_concurrency
: 最大同時実行コンテナ数 -
template.spec.containers.resources.limits["memory"]
: 1コンテナインスタンスあたりの最大メモリ量 -
template.spec.containers.resources.limits["cpu"]
: 1コンテナインスタンスあたりのvCPU数 -
template.metadata.annotations["autoscaling.knative.dev/minScale"]
: コンテナインスタンスの最小数 -
template.metadata.annotations["autoscaling.knative.dev/maxScale"]
: コンテナインスタンスの最大数
variable "project_name" {
description = "gcp project name"
type = string
}
resource "google_cloud_run_service" "configure_cloud_run_service" {
name = "sample-api"
location = "us-central1"
autogenerate_revision_name = true # 自動でリビジョン末尾の識別文字列を入れるために必要
template {
spec {
timeout_seconds = 300
container_concurrency = 50
containers {
image = "us.gcr.io/${var.project_name}/sample-api" # すでにimageがアップロード済みでないといけない
resources {
limits = {
"memory" : "256Mi",
"cpu" : "1"
}
}
}
}
metadata {
annotations = {
"autoscaling.knative.dev/minScale" = "0"
"autoscaling.knative.dev/maxScale" = "5"
}
}
}
}
tfstateで変更を追わないパラメータを設定する
Cloud Buildないし手動デプロイ時、以下のパラメータが自動更新されます。そのため、これらパラメータに差分があったときにgoogle_cloud_run_serviceがTerraformのapply対象になってしまいます。これらパラメータをtfstateと同期させる必要はないので、tfstateの監視対象から外します[2]。
template.metadata.annotations["client.knative.dev/user-image"]
template.metadata.annotations["run.googleapis.com/client-name"]
template.metadata.annotations["run.googleapis.com/client-version"]
template.metadata.labels
template.spec.containers["image"]
resource "google_cloud_run_service" "configure_cloud_run_service" {
# ...省略...
lifecycle {
ignore_changes = [
# gcloudからデプロイしたとき、以下のパラメータが入る。変更差分として判定したくないのでチェック対象から外す
template[0].metadata[0].annotations["client.knative.dev/user-image"],
template[0].metadata[0].annotations["run.googleapis.com/client-name"],
template[0].metadata[0].annotations["run.googleapis.com/client-version"],
# Clour Buildからビルド・デプロイしたとき、以下のパラメータが入る。変更差分として判定したくないのでチェック対象から外す
template[0].metadata[0].labels,
template[0].spec[0].containers["image"]
]
}
}
(参考): 全体処理
サービスアカウントの秘密鍵を使うのは推奨されていないため、terraform操作用のサービスアカウントの権限借用(impersonate)をしています[3]。
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.40.0"
}
}
required_version = ">= 1.3"
backend "gcs" {}
}
variable "project_name" {
type = string
}
variable "project_id" {
type = number
}
provider "google" {
project = var.project_name
region = "us-central1"
impersonate_service_account = "terraform@${var.project_name}.iam.gserviceaccount.com"
}
module "api" {
source = "./module/api"
}
module "cloudbuild" {
source = "./module/cloudbuild"
depends_on = [module.api]
project_id = var.project_id
}
module "cloudrun" {
source = "./module/cloudrun"
depends_on = [module.api]
project_name = var.project_name
}
Discussion