🐕

Secret Managerと連携させてCloud RunにFastAPIサーバをデプロイしてみた

に公開

今回は先日公開した以下のSecret Managerの使い方を応用して、Cloud Runにそのシークレットを読ませてサーバを公開してみました。

https://zenn.dev/akasan/articles/f500badd1ed9bf

システム構成

今回の構成は以下のようになっています。

  • Secret Managerにてシークレットを管理
  • Artifact RegistryにてCloud RunサーバんじょDockerイメージ管理
  • Cloud RunにてFastAPIサーバをデプロイ

実装開始

IaCの実装

IaCのフォルダ構成は以下のようになっています。

main.tf
variables.tf
secret.txt
modules/
  artifact_registry/
    main.tf
    variables.tf
    outputs.tf
  secret_manager/
    main.tf
    variables.tf
    outputs.tf
  cloud_run/
    main.tf
    variables.tf
    outputs.tf

Artifact Registryのリソース定義

Artifact Registryのリソース定義は以下のようになっています。変数としてはregionとレポジトリ名を指定するためのartifact_registry_repo_nameを設定し、出力としてはレポジトリのIDをCloud Runに伝えるためにリポジトリのIDを返すようにしています。

modules/artifact_registry/variables.tf
variable "region" {
  description = "The Google Cloud region"
  type        = string
}

variable "artifact_registry_repo_name" {
  description = "Artifact Registry repo's name"
  type        = string
}
modules/artifact_registry/main.tf
resource "google_artifact_registry_repository" "fastapi_repo" {
  location      = var.region
  repository_id = var.artifact_registry_repo_name
  description   = "FastAPI Application"
  format        = "DOCKER"
}
modules/artifact_registry/outputs.tf
output "repository_id" {
  description = "Artifact Registory ID"
  value       = google_artifact_registry_repository.fastapi_repo.repository_id
}

Secret Managerのリソース定義

次はSecret Managerでシークレットを作成します。基本的な構成は最初に添付した先日の記事と同じです。出力としてCloud Runにシークレットを指定するためにsecret_idを設定しています。

modules/secret_manager/variables.tf
variable "region" {
  description = "The Google Cloud region"
  type        = string
}

variable "secret_file" {
  description = "secret file"
  type        = string
}
modules/secret_manager/main.tf
data "google_project" "project" {

}

resource "google_secret_manager_secret" "fastapi_app_secret" {
  secret_id = "fastapi-application-secret"

  replication {
    user_managed {
      replicas {
        location = var.region
      }
    }
  }
}

resource "google_secret_manager_secret_version" "fastapi_app_secret_version" {
  secret          = google_secret_manager_secret.fastapi_app_secret.id
  secret_data     = file(var.secret_file)
  deletion_policy = "DISABLE"
}

resource "google_secret_manager_secret_iam_member" "secretaccess" {
  secret_id = google_secret_manager_secret.fastapi_app_secret.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
}
modules/secret_manager/outputs.tf
output "secret_id" {
  description = "Secret ID"
  value       = google_secret_manager_secret.fastapi_app_secret.id
}

Cloud Runのリソース定義

次のリソース定義はCloud Runになります。Artifact RegistryとSecret Managerのリソース定義にて作成したリソースを元に、サーバの設定を実施します。リソース定義において今回は公開アクセスにするためにgoogle_cloud_run_v2_service_iam_memberallUsersで権限設定しています。あと、Cloud RunのリソースはArtifact RegistryにDockerイメージがないと作成できません。そこで、deploy_cloud_runフラグを利用し、最初のリソース作成はCloud Run以外を作成してその後Artifact Registryにイメージをpushし、次のdeployにてdeploy_cloud_run=trueにてリソースを作成します。

modules/cloud_run/variables.tf
variable "region" {
  description = "The Google Cloud region"
  type        = string
}

variable "secret_id" {
  description = "Secret ID"
  type        = string
}

variable "repository_id" {
  description = "Artifact Registry Id"
  type        = string
}

variable "image_name" {
  description = "Image name"
  type        = string
}

variable "deploy_cloud_run" {
  description = "Whether to deploy Cloud Run service (set to true after pushing Docker image)"
  type        = bool
}
modules/cloud_run/main.tf
data "google_project" "project" {

}

resource "google_cloud_run_v2_service" "fastapi_app" {
  count               = var.deploy_cloud_run ? 1 : 0
  name                = "fastapi-app"
  location            = var.region
  deletion_protection = false
  ingress             = "INGRESS_TRAFFIC_ALL"

  scaling {
    max_instance_count = 2
  }

  template {
    containers {
      image = "${var.region}-docker.pkg.dev/${data.google_project.project.project_id}/${var.repository_id}/${var.image_name}:latest"
      env {
        name = "SECRET_KEY"
        value_source {
          secret_key_ref {
            secret  = var.secret_id
            version = "latest"
          }
        }
      }
    }
  }
}

resource "google_cloud_run_v2_service_iam_member" "all_users_invoker" {
  count    = var.deploy_cloud_run ? 1 : 0
  location = google_cloud_run_v2_service.fastapi_app[0].location
  name     = google_cloud_run_v2_service.fastapi_app[0].name
  role     = "roles/run.invoker"
  member   = "allUsers"
}
modules/cloud_run/outputs.tf
output "cloud_run_url" {
  description = "Cloud Run's server URL"
  value = length(google_cloud_run_v2_service.fastapi_app) > 0 ? google_cloud_run_v2_service.fastapi_app[0].uri : ""
}

ルートファイルの設定

それでは先ほど作成したモジュールをよみこむルートファイルを作成します。3つのモジュールを読み込み、outputの連携などを設定しています。

secret.txt
SECRET VALUE
variables.tf
variable "project_id" {
  description = "The Google Cloud project ID"
  type        = string
}

variable "region" {
  description = "The Google Cloud region"
  type        = string
  default     = "asia-northeast1"
}

variable "secret_file" {
  description = "Secret file"
  type        = string
}

variable "artifact_registry_repo_name" {
  description = "Artifact Registry repo's name"
  type        = string
  default     = "fastapi-app"
}

variable "image_name" {
  description = "Docker image name"
  type        = string
  default     = "fastapi-app"
}

variable "deploy_cloud_run" {
  description = "Whether to deploy Cloud Run service (set to true after pushing Docker image)"
  type        = bool
  default     = false
}
main.tf
provider "google" {
  project = var.project_id
  region  = var.region
}

module "secret_manager" {
  source = "./modules/secret_manager"

  region      = var.region
  secret_file = var.secret_file
}

module "artifact_registry" {
  source = "./modules/artifact_registry/"

  region                      = var.region
  artifact_registry_repo_name = var.artifact_registry_repo_name
}

module "cloud_run" {
  source = "./modules/cloud_run"

  region           = var.region
  secret_id        = module.secret_manager.secret_id
  image_name       = var.image_name
  repository_id    = module.artifact_registry.repository_id
  deploy_cloud_run = var.deploy_cloud_run
}

この状態でterraform planを実行すると以下のようになります。

terraform plan
module.secret_manager.data.google_project.project: Reading...
module.cloud_run.data.google_project.project: Reading...
module.secret_manager.data.google_project.project: Read complete after 2s [id=projects/project_id]
module.cloud_run.data.google_project.project: Read complete after 2s [id=projects/project_id]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.artifact_registry.google_artifact_registry_repository.fastapi_repo will be created
  + resource "google_artifact_registry_repository" "fastapi_repo" {
      + create_time      = (known after apply)
      + description      = "FastAPI Application"
      + effective_labels = {
          + "goog-terraform-provisioned" = "true"
        }
      + format           = "DOCKER"
      + id               = (known after apply)
      + location         = "asia-northeast1"
      + mode             = "STANDARD_REPOSITORY"
      + name             = (known after apply)
      + project          = "project_id"
      + registry_uri     = (known after apply)
      + repository_id    = "fastapi-app"
      + terraform_labels = {
          + "goog-terraform-provisioned" = "true"
        }
      + update_time      = (known after apply)

      + vulnerability_scanning_config (known after apply)
    }

  # module.secret_manager.google_secret_manager_secret.fastapi_app_secret will be created
  + resource "google_secret_manager_secret" "fastapi_app_secret" {
      + create_time           = (known after apply)
      + deletion_protection   = false
      + effective_annotations = (known after apply)
      + effective_labels      = {
          + "goog-terraform-provisioned" = "true"
        }
      + expire_time           = (known after apply)
      + id                    = (known after apply)
      + name                  = (known after apply)
      + project               = "project_id"
      + secret_id             = "fastapi-application-secret"
      + terraform_labels      = {
          + "goog-terraform-provisioned" = "true"
        }

      + replication {
          + user_managed {
              + replicas {
                  + location = "asia-northeast1"
                }
            }
        }
    }

  # module.secret_manager.google_secret_manager_secret_iam_member.secretaccess will be created
  + resource "google_secret_manager_secret_iam_member" "secretaccess" {
      + etag      = (known after apply)
      + id        = (known after apply)
      + member    = "serviceAccount:795031300012-compute@developer.gserviceaccount.com"
      + project   = (known after apply)
      + role      = "roles/secretmanager.secretAccessor"
      + secret_id = (known after apply)
    }

  # module.secret_manager.google_secret_manager_secret_version.fastapi_app_secret_version will be created
  + resource "google_secret_manager_secret_version" "fastapi_app_secret_version" {
      + create_time            = (known after apply)
      + deletion_policy        = "DISABLE"
      + destroy_time           = (known after apply)
      + enabled                = true
      + id                     = (known after apply)
      + is_secret_data_base64  = false
      + name                   = (known after apply)
      + secret                 = (known after apply)
      + secret_data            = (sensitive value)
      + secret_data_wo_version = 0
      + version                = (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

FastAPIサーバの実装

次にシークレットの値を参照してそれを表示するFastAPサーバを実装します。

環境構築

uv init fastapi_app -p 3.12 
cd fastapi_app
uv add fastapi uvicorn

FastAPIサーバの実装

FastAPIサーバを実装します。シンプルに環境変数を読み込んで返せすAPIになります。

main.py
from fastapi import FastAPI
import os


app = FastAPI()

SECRET_VALUE = os.environ["SECRET_KEY"]

@app.get("/secret")
def index():
    return {"secret_value": SECRET_VALUE}

Dockerfileの作成

Dockerfileは以下のようになります。

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /app
COPY . /app

EXPOSE 8080
RUN uv sync

CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

ビルドとpush

今回は以下のshell scriptを利用してArtifact Registryにpushします。

build.sh
#!/bin/zsh
gcloud auth configure-docker asia-norhteast1-docker.pkg.dev
docker build -t asia-northeast1-docker.pkg.dev/project_id/fastapi-app/fastapi-app:latest --platform linux/amd64 .
docker push asia-northeast1-docker.pkg.dev/project_id/fastapi-app/fastapi-app:latest

環境構築

それでは実際に実行してみましょう。手順は以下になります。

  1. Artifact RegistryとSecret Managerリソースを作成する(deploy_cloud_run=false)
  2. FastAPIサーバをビルドしてpushする
  3. Cloud Runリソースを作成する(deploy_cloud_run=true)

この手順で作成されたCloud Runリソースは以下のようになっています。環境変数を見るとSECRET_KEYという名前でキーが設定されており、その値はSecret Managerから参照されていることが確認できます。

Cloud Runで作成された公開URIにアクセスすると、以下のようにSwagger UIが表示できました。試しに実行すると今回指定したSECRET VALUEという値が参照できることが確認できました。

動作確認が終了したのでterraform destroyでリソースは削除します。

まとめ

今回はSecret Managerで作成したシークレットをCloud Runサーバの環境変数として登録し、APIで利用できることを確認しました。以前個別に試していたサービスを実際に連携して利用できるところまで試すことができ、順調にインフラ構築に慣れてきていることを実感できました。今後はもっと複雑なアーキテクチャにもチャレンジしてみようと思います。

Discussion