WED Engineering Blog
🤖

Terraformで構築するMetabase on Cloud Run 〜GKEからの移行事例〜

2024/11/08に公開

はじめに

こんにちは、WED SREの kazs です。

私は「適度な」SREを提唱・伝道活動しながら、日々のサービス信頼性向上と既存システムの最適化に取り組んでいます。

そのシステム最適化の一つが、今回ご紹介するMetabaseのインフラ基盤です。
本記事では、MetabaseのインフラをGKEからCloud Runベースに移行した事例について解説します。

課題

従来のMetabaseは他のサービスと共用のGKE上で稼働していましたが、他のサービスが移行した後もMetabaseだけが取り残された状態となっていました。この状況を解消するため、インフラ基盤の刷新を行うことにしました。

全体構成

最小構成として以下のコンポーネントを利用します:

  • Google Cloud
  • Cloud Run
  • Cloud SQL
  • Terraform
  • Cloud Storage(Terraformのbackend用)

本構成における重要なポイントは以下の3点です:

  1. 再現性を重視し、Terraformによるワンコマンドでの環境構築を実現
  2. Docker Imageのビルドを避け、DockerHubの公式イメージを利用することで最新バージョンへの追従を容易に
  3. ユーザー管理、クエリ、ダッシュボードの永続化のため、専用のデータベースを用意

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実行前に以下の準備が必要です:

  1. Google Cloudプロバイダーの認証設定
  2. 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による構成管理により、環境の再現性も確保されています。

WED Engineering Blog
WED Engineering Blog

Discussion