【DevOps実践】RustでAPIサーバーを構築してみた③: クラウド構築編
前置き
【DevOps実践】RustでAPIサーバーを構築してみた①: 全体紹介編
【DevOps実践】RustでAPIサーバーを構築してみた②: Docker構築編
なにやってるかが分からない方は読んでみてください。
やるべきこと
いよいよクラウドの構築に入ります。
まずはやるべきことを整理してみましょう。
今回のAPIサーバ―では、リレーショナルデータベース、インメモリーデータベース、クラウドストレージを使用しているため、これらは必然としていります。
それに加えて、APIサーバ―を実行するCloudRun、DockerImageを保存するArtifactRegistryが必要です。
- ArtifactRegistry(CloudRunで使用するイメージ保管庫)
- CloudRun(APIサーバー実行環境)
- CloudSQL(リレーショナルデータベース、PostgreSQL)
- Memotystore for Redis(インメモリーデータベース)
- CloudStorage(ストレージ)
これらは今回の場合、必須項目です。
また、CloudRunからRedisに接続するには、VPCとVPCコネクターが必要です。
ちなみに、CloudRunからCloudSQLにアクセスするときは、一応パブリックIP経由でアクセスできるけど、お勧めしませんよー(小声)
VPCコネクター使って、安全・安心にデータのやり取りしましょう!
- VPC(仮想ネットワーク空間)
- VPC Connector(VPC内にあるリソースにアクセスするためのブリッジ)
インターネット
↓
[ Cloud Run ] (外部公開)
↓
❌ 直接プライベートIPへは接続不可
↓
┌────────────────────────────────────────────┐
│ Serverless VPC Access Connector(トンネル)│
└───────────────────▲────────────────────────┘
│
▼
─────────────── VPC Network ───────────────
│
├─ Cloud Memorystore(Redis)
│
├─ Cloud SQL(Private IP)
│
└─ GCE VM / その他の内部リソース
これだけでは終われません。
よりセキュアで、セキュリティも考慮すると、サービスアカウント(SA)を作成して、適宜な権限を渡す必要があります。GithubActionsでデプロイの自動化をやるので、GithubActions用のSAも必要です。
CloudRunで使用するSAは以下の権限が必要です。
- VPCコネクター使用権限
- CloudSQL接続権限
- Redis接続権限
- CloudStorageに読み取り、書き込み権限
- ログ出力権限(おまけ、あると便利)
GithubActionsで使用するSAは以下の権限が必要です。
- ArtifactRegistry書き込み権限
- CloudRun実行時、SAを指定できる権限(上記のSAを指定しないとCloudSQL,Redis,Storageにアクセスできない!)
- CloudRun実行権限
GithubActionsからGCPにアクセスするときは、OIDC認証を使うため、以下の設定も合わせて必要です。
- workload_identity_pool
- workload_identity_pool_provider
やる事いっぱいですね、順番にやっていきましょう。
準備作業
- GCP CLI インストール
- gcloudログイン
- dockerログイン
- GCP Artifact Registoryでリポジトリを作成
- デプロイするプロジェクトのイメージ化してArtifact Registoryリポジトリにプッシュする
以下の作業はホストPCで行います。(自分はWSLです。)
まずはGoogle公式ドキュメントを参考にホストPC(WSL)にGCP CLIをインストールします。
Step 1: 必要なモジュールをインストール
sudo apt-get update && sudo apt-get install apt-transport-https ca-certificates gnupg
Step 2: GoogleCloud公開鍵をインストール
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
Step 3: gcloudCLIの配布URIをパッケージソースとして追加
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
Step 4: gcloudCLIを更新してインストール
sudo apt-get update && sudo apt-get install google-cloud-cli
ログイン情報をセットします。
Step 5: gcloudログイン
# CLI用
gcloud auth login
# アプリケーション用(*任意ですが、Terraformで認証が必要なので、使う方はやっといてください。)
gcloud auth application-default login
Step 6: デフォルトでアクセスするプロジェクト(GCP)を設定する
*任意ですが、設定しないと都度指定する必要があります
gcloud config set project <プロジェクトID>
これで一旦準備作業が完了しました。
次から本題の【クラウド構築】に入ります。
クラウド構築
自分はデプロイする予定のプロジェクトに【infra】フォルダを作成し、Terraformの設定ファイルを追加してます。
ディレクトリ構造はこんな感じです。
infra
├─ main.tf
├─ backend.tf
├─ variables.tf
├─ terraform.tfvars
└─ modules
├─ iam
│ ├─ main.tf
│ ├─ variables.tf
│ └─ output.tf
├─ redis
│ ├─ main.tf
│ ├─ variables.tf
│ └─ output.tf
├─ sql
│ ├─ main.tf
│ ├─ variables.tf
│ └─ output.tf
├─ run
│ ├─ main.tf
│ └─ variables.tf
├─ storage
│ ├─ main.tf
│ └─ variables.tf
└─ vpc
├─ main.tf
├─ variables.tf
└─ output.tf
Step 1: IAMモジュール
# modules/iam/variables.tf
# ------------------------------------------------------
# サービスアカウント作成時に使用する変数を定義
# ------------------------------------------------------
variable "region" {
type = string
}
variable "project_id" {
type = string
}
variable "project_number" {
type = string
}
variable "cloud_run_sa_id" {
description = "CloudRun Service Account ID"
type = string
}
variable "github_actions_sa_id" {
description = "Github Actions Service Account ID"
type = string
}
variable "github_workload_pool_id" {
description = "Github Workload Identity Pool ID"
type = string
}
variable "github_actions_provider_id" {
description = "Github Actions IDP Provider ID"
type = string
}
variable "github_repository_name" {
description = "Github Repository Name(owner/repository形式)"
type = string
}
# modules/iam/main.tf
# ------------------------------------------------------
# CloudRun用サービスアカウント設定
# ------------------------------------------------------
resource "google_service_account" "cloud_run_sa" {
account_id = var.cloud_run_sa_id
display_name = "CloudRunで使用するサービスアカウント"
}
# VPC Connector使用権限
resource "google_project_iam_member" "vpc_access_user" {
project = var.project_id
role = "roles/vpcaccess.user"
member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
# CloudSQL接続権限
resource "google_project_iam_member" "rdb_client" {
project = var.project_id
role = "roles/cloudsql.client"
member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
# Redis情報閲覧権限
resource "google_project_iam_member" "redis_info_viewer" {
project = var.project_id
role = "roles/redis.viewer"
member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
# CloudStorage読み取り、書き込み権限
resource "google_project_iam_member" "storage_editor" {
project = var.project_id
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
# CloudRunログ出力権限
resource "google_project_iam_member" "log_writer" {
project = var.project_id
role = "roles/logging.logWriter"
member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
# ------------------------------------------------------
# GitHubActions用サービスアカウント設定
# ------------------------------------------------------
resource "google_service_account" "github_actions_sa" {
account_id = var.github_actions_sa_id
display_name = "GithubActionsで使用するサービスアカウント"
}
# Artifact Registry書き込み権限
resource "google_project_iam_member" "registry_writer" {
project = var.project_id
role = "roles/artifactregistry.writer"
member = "serviceAccount:${google_service_account.github_actions_sa.email}"
}
# CloudRun実行時、SAを指定できる権限
resource "google_project_iam_member" "sa_can_be_specified" {
project = var.project_id
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.github_actions_sa.email}"
}
# CloudRun実行権限
# 注意:このアカウントはGithubActionsで自動デプロイに使うため
# 通常の【run.invoker】だけでは権限不足です。
# デプロイはCloudRunインスタンスの作成・更新
# 環境変数の変更、トラフィック切り替えなど、さまざまの権限が必要です。
resource "google_project_iam_member" "run_admin" {
project = var.project_id
role = "roles/run.admin"
member = "serviceAccount:${google_service_account.github_actions_sa.email}"
}
# ------------------------------------------------------
# OIDC認証
# ------------------------------------------------------
# workload_identity_pool設定
# workload_identity_pool: 外部環境からGCPのリソースに
# OIDCやSAMLを用いて、安全に認証・アクセスできる仕組み
resource "google_iam_workload_identity_pool" "github_workload_pool" {
workload_identity_pool_id = var.github_workload_pool_id
display_name = "GitHub Workload Identity Pool"
# terraform destroyすると
# GCP側の復元システムによりゴミ箱に移動されるため
# 復元可能期間内では同じIDで再作成不可となる
# destroy時にエラーで弾くように設定します。
lifecycle {
prevent_destroy = true
}
}
# workload_identity_pool_provider設定
# 特定の外部 IdP との連携設定を定義するリソース
# 外部 IdP から発行されたトークンを属性マッピング (Attribute Mapping) や
# 属性条件 (Attribute Condition) を用いて検証する
resource "google_iam_workload_identity_pool_provider" "github_actions_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.github_workload_pool.workload_identity_pool_id
workload_identity_pool_provider_id = var.github_actions_provider_id
display_name = "GithubActions OIDC認証プロバイダー"
# GithubIdPから渡ってくる認証情報(右側)をGoogle(左側)の属性にマッピング
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository"
}
# 重要:セキュリティ高めるためには
# 指定のリポジトリからのみ、アクセスできるようにしましょう!
attribute_condition = "attribute.repository == \"${var.github_repository_name}\""
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
lifecycle {
prevent_destroy = true
}
}
# Githubリポジトリからのアクセスを許可
resource "google_service_account_iam_binding" "github_sa_binding" {
# 注意:Emailではなく、Name
service_account_id = google_service_account.github_actions_sa.name
role = "roles/iam.workloadIdentityUser"
# principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<WORK_IDENTITY_POOL_ID>: このPoolを通じた外部IdP(OIDC)からのアクセスを許可する
# attribute.repository/<GITHUB_REPOSITORY_NAME>: Github特定なリポジトリからのアクセスに限定する
members = "principalSet://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.github_workload_pool.workload_identity_pool_id}/attribute.repository/${var.github_repository_name}"
}
# modules/iam/outputs.tf
# ほかのモジュールで使う項目をモジュール外に出しておきます。
output "cloud_run_sa_email" {
value = google_service_account.cloud_run_sa.email
}
Step 2: VPCモジュール
# modules/vpc/variables.tf
variable "region" {
type = stirng
}
variable "vpc_name" {
type = string
}
variable "vpc_subnet_name" {
type = string
}
variable "vpc_connector_name" {
type = string
}
# modules/vpc/main.tf
# ------------------------------------------------------
# ネットワーク
# ------------------------------------------------------
resource "google_compute_network" "vpc" {
name = var.vpc_name
# 自動でサブネットを作らないように設定
auto_create_subnetworks = false
}
# ------------------------------------------------------
# サブネット
# ------------------------------------------------------
resource "google_compute_subnetwork" "vpc_subnet" {
name = var.vpc_subnet_name
# なんでこの数字になったのかは、CIDRの知識なので、気になる方は調べてみてください。
ip_cidr_range = "10.0.1.0/24"
region = var.region
network = google_compute_network.vpc.name
}
# ------------------------------------------------------
# VPCコネクター
# ------------------------------------------------------
resource "google_vpc_access_connector" "connector" {
name = var.vpc_connector_name
region = var.region
network = google_compute_network.vpc.name
ip_cidr_range = "10.8.0.0/28"
}
# modules/vpc/outputs.tf
output "vpc_id" {
value = google_compute_network.vpc.id
}
output "connector_name" {
value = google_vpc_access_connector.connector.name
}
Step 3: Redisモジュール
# modules/redis/variables.tf
variable "region" {
type = string
}
variable "vpc_id" {
type = string
}
variable "redis_name" {
type = string
}
variable "tier" {
description = "単一ノード構成。レプリカなし。"
type = string
default = "BASIC"
}
variable "memory_size" {
description = "GB"
type = number
default = 1
}
# modules/redis/main.tf
# データの永続化はRDBでやるので、単一ノードで構成します。
resource "google_redis_instance" "redis" {
region = var.region
name = var.redis_name
tier = var.tier
memory_size_gb = var.memory_size
authorized_network = var.vpc_id
}
# modules/redis/outputs.tf
output "host" {
value = google_redis_instance.redis.host
}
output "port" {
value = google_redis_instance.redis.port
}
Step 4: Sqlモジュール
# modules/sql/variables.tf
variable "sql_name" {
type = string
}
variable "region" {
type = string
}
variable "db_version" {
type = string
default = "POSTGRES_18"
}
variable "tier" {
description = "vCPU 2 / memory 16GB"
type = string
default = "db-custom-2-16384"
}
variable "disk_type" {
type = string
default = "PD_SSD"
}
variable "disk_size" {
type = number
default = 50
}
variable "vpc_id" {
type = string
}
variable "db_name" {
type = string
}
variable "db_user" {
type = string
}
variable "db_password" {
type = string
}
# modules/sql/main.tf
# ------------------------------------------------------
# CloudSQLインスタンス作成
# ------------------------------------------------------
resource "google_sql_database_instance" "postgres" {
name = var.sql_name
region = var.region
database_version = var.db_version
settings {
tier = var.tier
disk_type = var.disk_type
disk_size = var.disk_size
ip_configuration {
ipv4_enabled = false
private_network = var.vpc_id
}
backup_configuration {
enabled = true
# バックアップ開始時刻(UTC)
start_time = "17:00" # JST AM 2:00
}
}
# 削除防止をOFFにする場合は false
deletion_protection = true
}
# ------------------------------------------------------
# DB作成
# ------------------------------------------------------
resource "google_sql_database" "postgres_db" {
instance = google_sql_database_instance.postgres.name
name = var.db_name
}
# ------------------------------------------------------
# DBユーザー作成
# ------------------------------------------------------
resource "google_sql_user" "name" {
instance = google_sql_database_instance.postgres.name
name = var.db_user
password = var.db_password
}
# modules/sql/outputs.tf
output "host" {
value = google_sql_database_instance.postgres.private_ip_address
}
output "port" {
description = "固定値"
value = 5432
}
Step 5: Storageモジュール
# modules/storage/variables.tf
variable "region" {
type = string
}
variable "bucket_name" {
type = string
}
variable "storage_class" {
description = "高頻度アクセス向け"
type = string
default = "STANDARD"
}
# modules/storage/main.tf
resource "google_storage_bucket" "storage" {
location = var.region
name = var.bucket_name
storage_class = var.storage_class
# バケット単位で IAM ポリシーに統一制御する
uniform_bucket_level_access = true
}
Step 6: Runモジュール
# modules/run/variables.tf
variable "region" {
type = string
}
variable "project_id" {
type = string
}
variable "api_server_name" {
type = string
}
variable "sa_email" {
description = "CloudRunが使用するサービスアカウント"
type = string
}
variable "artifact_registry_name" {
type = string
}
variable "image_name" {
type = string
}
variable "image_tag" {
type = string
default = "latest"
}
variable "api_server_host" {
type = string
default = "0.0.0.0"
}
variable "api_server_port" {
type = number
default = 8080
}
variable "db_host" {
type = string
}
variable "db_port" {
type = string
}
variable "db_name" {
type = string
}
variable "db_user" {
type = string
}
variable "db_password" {
type = string
}
variable "redis_host" {
type = string
}
variable "redis_port" {
type = string
}
variable "vpc_connector_name" {
type = string
}
# modules/run/main.tf
# cloud_run_v2 とv1では記述方法が異なるので注意!
# ------------------------------------------------------
# CloudRunインスタンス作成
# ------------------------------------------------------
resource "google_cloud_run_v2_service" "api_server" {
project = var.project_id
location = var.region
name = var.api_server_name
template {
service_account = var.sa_email
containers {
# <RIGION>-docker.pkg.dev/<PROJECT_ID>/<ARTIFACT_REGISTRY_NAME>/<DOCKER_IMAGE_NAME>:<DOCKER_IMAGE_TAG>
image = "${var.region}-docker.pkg.dev/${var.project_id}/${var.artifact_registry_name}/${var.image_name}:${var.image_tag}"
env {
name = "HOST"
value = var.api_server_host
}
env {
name = "PORT"
value = var.api_server_port
}
env {
name = "DB_HOST"
value = var.db_host
}
env {
name = "DB_PORT"
value = var.db_port
}
env {
name = "DB_NAME"
value = var.db_name
}
env {
name = "DB_USER"
value = var.db_user
}
env {
name = "DB_PASSWORD"
value = var.db_password
}
env {
name = "REDIS_HOST"
value = var.redis_host
}
env {
name = "REDIS_PORT"
value = var.redis_port
}
ports {
container_port = var.api_server_port
}
}
vpc_access {
connector = "projects/${var.project_id}/locations/${var.region}/connectors/${var.vpc_connector_name}"
egress = "ALL_TRAFFIC" # すべてのアウトバウンド通信が VPC 経由に変更
}
}
}
# ------------------------------------------------------
# CloudRunインスタンス公開設定
# ------------------------------------------------------
# この設定が無いと、CloudRunにデプロイしても403で弾かれます。
resource "google_cloud_run_v2_service_iam_member" "public_access" {
project = var.project_id
location = google_cloud_run_v2_service.api_server.location
name = google_cloud_run_v2_service.api_server.name
role = "roles/run.invoker"
member = "allUsers"
}
Step 7: main.tf
モジュールの設定が終わって、いよいよ組み立てていきます。
# backend.tf
# TerraformのステートファイルをCloudStorageに保存します。
terraform {
backend "gcs" {
bucket = "あなたのバケット名を指定してください。"
prefix = "sample/prod" # ここで設定したディレクトリがバケットに作成されます
}
}
# variables.tf
variable "project_id" {
description = "GCPプロジェクトID"
type = string
}
variable "project_number" {
description = "GCPプロジェクトナンバー"
type = string
}
variable "region" {
description = "GCPリージョン"
type = string
default = "asia-northeast1"
}
variable "repository_name" {
description = "Github Repository Name (owner/repository)"
type = string
}
variable "cloud_run_sa_id" {
description = "CloudRunが使用するサービスアカウント"
type = string
}
variable "github_actions_sa_id" {
description = "GithubActionsが使用するサービスアカウント"
type = string
}
variable "db_name" {
type = string
}
variable "db_user" {
type = string
}
variable "db_password" {
type = string
}
variable "artifact_registry_name" {
type = string
}
variable "image_name" {
type = string
}
# main.tf
terraform {
required_providers {
google = {
resource = "hashicorp/google"
version = "~> 5.10"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
# -----------------------------
# IAMモジュール
# -----------------------------
module "user" {
source = "./modules/iam"
region = var.region
project_id = var.project_id
project_number = var.project_number
github_repository_name = var.repository_name
github_workload_pool_id = "github-actions-pool"
github_actions_provider_id = "github-actions-pool-provider"
cloud_run_sa_id = var.cloud_run_sa_id
github_actions_sa_id = var.github_actions_sa_id
}
# -------------------
# VPC、コネクター設定
# -------------------
module "network" {
source = "./modules/vpc"
region = var.region
vpc_name = "app-vpc"
vpc_subnet_name = "app-vpc-subnet"
vpc_connector_name = "app-vpc-connector"
}
# -------------------
# Redisモジュール
# -------------------
module "redis" {
source = "./modules/redis"
region = var.region
vpc_id = module.network.vpc_id
redis_name = "app-redis"
}
# -------------------
# Sqlモジュール
# -------------------
module "db" {
source = "./modules/sql"
region = var.region
vpc_id = module.network.vpc_id
sql_name = "app-db"
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
}
# -------------------
# Storageモジュール
# -------------------
module "storage" {
source = "./modules/storage"
region = var.region
bucket_name = "app-bucket"
}
# -------------------
# Runモジュール
# -------------------
module "api_server" {
source = "./modules/run"
region = var.region
project_id = var.project_id
sa_email = module.user.cloud_run_sa_email
artifact_registry_name = var.artifact_registry_name
image_name = var.image_name
vpc_connector_name = module.network.connector_name
api_server_name = "app-api-server"
db_host = module.db.host
db_port = module.db.port
db_name = var.db_name
db_user = var.db_user
db_password = var.db_password
redis_host = module.redis.host
redis_port = module.redis.port
}
# terraform.tfvars
# いよいよ最後!変数を入れて設定完了です。
project_id = ""
project_number = ""
repository_name = ""
cloud_run_sa_id = ""
github_actions_sa_id = ""
db_name = ""
db_user = ""
db_password = ""
artifact_registry_name = ""
image_name = ""
お疲れ様でした。これでクラウドの設定は一旦完了となります。
Step 8: インスタンス作成
terraform apply
ネットワーク環境にもよりますが、10分~20分くらいかかる場合もあります。
コーヒー飲んでお待ちください。
Step 9: インスタンス削除
terraform destroy
# 今回はlifecycleで消せない項目とかもあるので、destroyではなく、
# いらなくなったモジュールをコメントアウトしてapplyしましょう!
Discussion