🤖
Terraformで構築するMetabase on Cloud Run 〜GKEからの移行事例〜
はじめに
こんにちは、WED SREの kazs です。
私は「適度な」SREを提唱・伝道活動しながら、日々のサービス信頼性向上と既存システムの最適化に取り組んでいます。
そのシステム最適化の一つが、今回ご紹介するMetabaseのインフラ基盤です。
本記事では、MetabaseのインフラをGKEからCloud Runベースに移行した事例について解説します。
課題
従来のMetabaseは他のサービスと共用のGKE上で稼働していましたが、他のサービスが移行した後もMetabaseだけが取り残された状態となっていました。この状況を解消するため、インフラ基盤の刷新を行うことにしました。
全体構成
最小構成として以下のコンポーネントを利用します:
- Google Cloud
- Cloud Run
- Cloud SQL
- Terraform
- Cloud Storage(Terraformのbackend用)
本構成における重要なポイントは以下の3点です:
- 再現性を重視し、Terraformによるワンコマンドでの環境構築を実現
- Docker Imageのビルドを避け、DockerHubの公式イメージを利用することで最新バージョンへの追従を容易に
- ユーザー管理、クエリ、ダッシュボードの永続化のため、専用のデータベースを用意
Terraformコードの構成
各コンポーネントのコード構成について説明します。
基本設定ファイル
└── terraform
├── _provider.tf
├── _variables.tf
├── .terraform-version
├── apis.tf
├── vpc.tf
├── iam.tf
├── secret_manager.tf
├── cloudsql.tf
└── cloudrun.tf
プロバイダー設定(_provider.tf)
Google Cloud Platformとの連携に必要な設定を定義します:
terraform {
backend "gcs" {
bucket = "YOUR_TERRAFORM_STATE_BUCKET"
prefix = "terraform/state/metabase"
}
}
terraform {
required_version = ">= 1.9.8"
required_providers {
google = {
source = "hashicorp/google"
version = "6.10.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = "6.10.0"
}
}
}
変数定義(_variables.tf)
環境固有の値を変数として定義します:
1.9.8
Terraformバージョン指定(.terraform-version)
利用するTerraformのバージョンを固定します:
locals {
project_id = YOUR_PROJECT_ID # 所持のProjectIDに変更
us_region = "us-central1" # 必要に応じて変更
us_central1_main_range = "192.168.0.0/24" # 必要に応じて変更
database_private_range = "192.168.200.0" # /22 # 必要に応じて変更
}
variable "description" {
type = string
default = "Managed by Terraform"
}
リソース定義ファイル
API有効化(apis.tf)
必要最小限のGoogle Cloud APIを有効化します:
resource "google_project_service" "compute" {
project = local.project_id
service = "compute.googleapis.com"
}
resource "google_project_service" "cloudsql" {
project = local.project_id
service = "sql-component.googleapis.com"
}
resource "google_project_service" "cloudsql_admin" {
project = local.project_id
service = "sqladmin.googleapis.com"
}
resource "google_project_service" "cloudstroage" {
project = local.project_id
service = "storage-component.googleapis.com"
}
resource "google_project_service" "run" {
project = local.project_id
service = "run.googleapis.com"
}
resource "google_project_service" "service_networking" {
project = local.project_id
service = "servicenetworking.googleapis.com"
}
resource "google_project_service" "serverless_integrations" {
project = local.project_id
service = "runapps.googleapis.com"
}
resource "google_project_service" "secret_manager" {
project = local.project_id
service = "secretmanager.googleapis.com"
}
IAM設定(iam.tf)
Cloud Run用のサービスアカウントを作成します:
resource "google_service_account" "cloudrun_sa" {
project = local.project_id
account_id = "cloudrun"
display_name = "Cloud Run Service Account"
description = var.description
}
resource "google_project_iam_member" "cloudrun_roles" {
for_each = toset([
"roles/run.developer",
"roles/compute.loadBalancerAdmin",
"roles/compute.viewer",
"roles/secretmanager.secretAccessor",
"roles/iam.serviceAccountUser",
"roles/storage.admin",
"roles/logging.bucketWriter"
])
project = local.project_id
role = each.key
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
depends_on = [google_service_account.cloudrun_sa]
}
ネットワーク設定(vpc.tf)
VPCネットワークとその関連リソースを構築します:
resource "google_compute_network" "vpc" {
project = local.project_id
name = local.project_id
routing_mode = "GLOBAL"
auto_create_subnetworks = false
depends_on = [google_project_service.compute]
}
resource "google_compute_global_address" "database" {
project = local.project_id
name = "${local.project_id}-private-ip-range"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
address = local.database_private_range
prefix_length = 22
network = google_compute_network.vpc.self_link
depends_on = [
google_project_service.compute,
google_compute_network.vpc
]
}
resource "google_service_networking_connection" "database" {
network = google_compute_network.vpc.self_link
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.database.name]
depends_on = [google_compute_global_address.database]
}
resource "google_compute_address" "us_central1_1" {
project = local.project_id
name = "us-central1-nat-1"
region = local.us_region
depends_on = [google_project_service.compute]
}
resource "google_compute_router_nat" "us_central1" {
project = local.project_id
name = "us-central1-nat"
region = local.us_region
router = google_compute_router.us_central1.name
nat_ip_allocate_option = "MANUAL_ONLY"
nat_ips = [
google_compute_address.us_central1_1.self_link,
]
min_ports_per_vm = 10000
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
log_config {
enable = true
filter = "ERRORS_ONLY"
}
depends_on = [google_compute_router.us_central1]
}
resource "google_compute_router" "us_central1" {
project = local.project_id
name = "us-central-router"
region = local.us_region
network = google_compute_network.vpc.self_link
depends_on = [google_compute_network.vpc]
}
resource "google_compute_subnetwork" "us_central1_main" {
project = local.project_id
name = "us-central1-main"
region = local.us_region
network = google_compute_network.vpc.self_link
private_ip_google_access = "true"
ip_cidr_range = local.us_central1_main_range
depends_on = [google_compute_network.vpc]
}
シークレット管理(secret_manager.tf)
PostgreSQLのパスワードをSecret Managerで安全に管理します:
resource "google_secret_manager_secret" "metabase_db_password" {
project = local.project_id
secret_id = "metabase_db_password"
replication {
auto {}
}
depends_on = [google_project_service.secret_manager]
}
resource "google_secret_manager_secret_version" "metabase_db_password_version" {
secret = google_secret_manager_secret.metabase_db_password.id
secret_data = "YOUR_PASSWORD"
depends_on = [google_secret_manager_secret.metabase_db_password]
}
データベース設定(cloudsql.tf)
Cloud SQLでPostgreSQLインスタンスを構築します:
resource "google_sql_database_instance" "metabase" {
project = local.project_id
name = "metabase"
database_version = "POSTGRES_16"
region = local.us_region
settings {
edition = "ENTERPRISE"
tier = "db-f1-micro"
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.vpc.id
enable_private_path_for_google_cloud_services = true
}
database_flags {
name = "cloudsql.iam_authentication"
value = "on"
}
insights_config {
query_insights_enabled = true
query_plans_per_minute = 5
query_string_length = 1024
record_application_tags = false
record_client_address = false
}
}
deletion_protection = "true"
depends_on = [
google_project_service.cloudsql,
google_project_service.cloudsql_admin,
google_service_networking_connection.database
]
}
resource "google_sql_database" "metabase" {
name = "metabase"
instance = google_sql_database_instance.metabase.name
project = local.project_id
charset = "UTF8"
collation = "en_US.UTF8"
depends_on = [google_sql_database_instance.metabase]
}
data "google_secret_manager_secret_version" "metabase_db_password" {
secret = google_secret_manager_secret.metabase_db_password.id
version = "latest"
depends_on = [
google_secret_manager_secret.metabase_db_password,
google_secret_manager_secret_version.metabase_db_password_version
]
}
resource "google_sql_user" "metabase" {
name = "metabase"
instance = google_sql_database_instance.metabase.name
password = data.google_secret_manager_secret_version.metabase_db_password.secret_data
project = local.project_id
type = "BUILT_IN"
depends_on = [
google_sql_database.metabase,
google_secret_manager_secret_version.metabase_db_password_version,
google_sql_database_instance.metabase
]
}
アプリケーション環境(cloudrun.tf)
Cloud Runサービスを設定します:
resource "google_cloud_run_service" "metabase" {
project = local.project_id
name = "metabase"
location = local.us_region
template {
spec {
service_account_name = google_service_account.cloudrun_sa.email
containers {
image = "metabase/metabase:v0.50.25"
env {
name = "MB_DB_TYPE"
value = "postgres"
}
env {
name = "MB_DB_DBNAME"
value = "metabase"
}
env {
name = "MB_DB_PORT"
value = "5432"
}
env {
name = "MB_DB_USER"
value = "metabase"
}
env {
name = "MB_DB_PASS"
value_from {
secret_key_ref {
name = google_secret_manager_secret.metabase_db_password.secret_id
key = google_secret_manager_secret_version.metabase_db_password_version.version
}
}
}
env {
name = "MB_DB_HOST"
value = google_sql_database_instance.metabase.private_ip_address
}
ports {
container_port = 3000
}
resources {
limits = {
cpu = "2000m"
memory = "4Gi"
}
}
}
}
metadata {
annotations = {
"autoscaling.knative.dev/minScale" = "1"
"autoscaling.knative.dev/maxScale" = "2"
"run.googleapis.com/cloudsql-instances" = google_sql_database_instance.metabase.connection_name
"run.googleapis.com/client-name" = "terraform"
"run.googleapis.com/cpu-throttling" = "false"
"run.googleapis.com/execution-environment" = "gen2"
"run.googleapis.com/network-interfaces" = jsonencode(
[
{
network = google_compute_network.vpc.name
subnetwork = google_compute_subnetwork.us_central1_main.name
},
]
)
"run.googleapis.com/startup-cpu-boost" = "true"
"run.googleapis.com/vpc-access-egress" = "private-ranges-only"
}
}
}
autogenerate_revision_name = true
lifecycle {
ignore_changes = [
metadata[0].annotations,
]
}
depends_on = [google_sql_database_instance.metabase]
}
resource "google_cloud_run_service_iam_binding" "noauth" {
project = google_cloud_run_service.metabase.project
location = google_cloud_run_service.metabase.location
service = google_cloud_run_service.metabase.name
role = "roles/run.invoker"
members = [
"allUsers"
]
depends_on = [google_cloud_run_service.metabase]
}
output "metabase_url" {
value = google_cloud_run_service.metabase.status[0].url
description = "URL of the deployed Metabase service"
}
デプロイ準備
Terraform実行前に以下の準備が必要です:
- Google Cloudプロバイダーの認証設定
- Terraformの初期化(terraform init)
# GCPの認証
gcloud auth application-default login
# Terraformの初期化
terraform init
成果物
まとめ
本記事では、MetabaseのインフラをGKEから、よりシンプルで管理のしやすいCloud Runベースの構成へと移行した事例を紹介しました。
達成したこと
- 共用GKEクラスタに取り残されていたMetabaseを独立した環境へ移行
- Terraformによる完全なインフラのコード化を実現
- DockerHubの公式イメージを活用し、メンテナンス性を向上
- Cloud SQL採用による永続データの安全な管理
構成のポイント
- Google Cloudのマネージドサービスを最大限活用
- 最小限のコンポーネントで運用負荷を軽減
- Infrastructure as Codeによる再現性の確保
- セキュアな認証情報管理(Secret Manager活用)
本構成により、インフラの管理負荷を大幅に削減しつつ、安定したMetabase環境を実現することができました。また、Terraformによる構成管理により、環境の再現性も確保されています。
Discussion