Terraformモジュールを使ったCloud Spannerの設定標準化の取り組み

この記事は、Merpay Tech Openness Month 2023 の8日目の記事です。
メルペイのSREチームに所属しておりますt-nakataです。今回はメルペイでのTerraformモジュールを利用したCloud Spannerの設定標準化の取り組みについて紹介します。

Cloud Spannerの設定標準化とは?

メルペイのバックエンドではマイクロサービスアーキテクチャを採用しており、各マイクロサービスで利用するデータベースはCloud Spannerを主に利用しております。Cloud Spannerは基本的には各マイクロサービスを担当しているバックエンドエンジニアがTerraformを利用して構築し、運用します。(一部共用のインスタンスもあります。) その際に考慮する必要がある点が多々あります。たとえば、google_spanner_instancegoogle_spanner_database リソースによるCloud Spannerのインスタンス、 データベース自体の設定はもちろん、運用で必要な監視(Datadog monitor)、アプリケーション側のサービスアカウントに対するパーミッションの付与、データベースのバックアップやインスタンスの負荷に応じてProcessing Unit数をオートスケールをさせるspanner-autoscalerの導入などもあり、これらを構成するためには沢山のTerraformリソースを追加する必要があります。また、これらの実装にはいくつかの選択肢がある一方で、FinOpsの観点からコストメリットのある構成にしたいなど、推奨の構成に設定する必要があったりもします。これまで上記の対応はドキュメントを基にバックエンドエンジニアが個々に対応したり、SREへリクエストをしてもらった上でSREが対応したりしていましたが、都度対応する運用コストもかかるようになってきました。このような背景からCloud Spannerに関連するリソースを一通り構成できるようなTerraformモジュールを実装しました。以下を満たすことを目的としています。

  • マイクロサービスに必要なCloud Spannerに関連するTerraformリソースを一通り作成できるようにする
  • 可能な限り必要な設定を抽象化し、利用者が実装の詳細に立ち入らなくても構成できるようにする
  • 推奨の構成となるようモジュールのinput variableにはdefault値を持ち、カスタマイズしたいマイクロサービスに対してはinput variableで上書きできるようにする
  • モジュールを利用することにより複数選択肢のある構成を統一する

以降では各マイクロサービスが利用するTerraformのリソースが本モジュールを含めてどのように構成されているかについて触れ、そのうえで本モジュールの詳細について簡単に紹介いたします。

Terraformリソースの構成

各マイクロサービスが利用するTerraformのリソースですが、Platform Infraチームが管理しているモノレポ上にあります。(詳しくは他記事も参照してください。)本モジュールもこのモノレポ上で利用されることを前提としています。モノレポの構成の概要は以下の図のとおりとなっております。(今回の記事に関連した内容のみを抜粋しております)

  • modulesディレクトリ配下にモノレポ内で利用するTerraformモジュール定義があります。spanner-kitと記載しているものが本モジュールとなります。各マイクロサービスはsourceにバージョンとともにモジュールへのpathを指定して利用します。
  • microservicesディレクトリ配下に各マイクロサービス向けのTerraformリソースがあります。development/labolatory/productionと環境ごとにstateを持っています。
  • マイクロサービスにはstarter-kitを利用します。詳細はリンクの記事を参照していただきたいですが、Google Cloudのプロジェクト等、マイクロサービス作成に必要なものが一式定義されています。加えて、本モジュールを含め、必要なTerraformモジュール、個別のリソース定義を利用して、マイクロサービスに必要なリソースを構成します。
  • マイクロサービス内の一部のリソースは共有のプロジェクトを利用します。詳細は後述しますが、共有プロジェクトに向けたgoogle provider定義を利用して構成します。

モジュールの詳細

今回実装したモジュールのinput variableは以下のようになっております。(一部社内の具体的な実装に関わる変数については省略、変更しています)

default値を利用した通常の構成の場合

module "spanner-with-default" {
  source                  = "uri_of_module_with_version"
  environment             = "production"
  microservice_project_id = "microservice_project_id"
  instance = {
    name             = "instance-name"
    processing_units = 1000
  }
  databases = [
    {
      name          = "database_name"
      enable_backup = true
    }
  ]
  providers = {
    (略)
  }
}

input variableを全て指定した場合

module "spanner-with-all-variable" {
  source                  = "uri_of_module_with_version"
  environment             = "production"
  microservice_project_id = "microservice_project_id"
  instance = {
    name             = "instance-name"
    config           = "regional-asia-northeast1"
    processing_units = 1000
  }
  databases = [
    {
      name          = "database_name"
      enable_backup = true
    }
  ]
  spanner_autoscaler = {
    enable             = true
    service_account_id = "service_account_id"
  }
  backup = {
    backup_schedules    = ["0 */2 * * *"]
    interval_hours      = 2
    retention_days      = 7
    scheduler_location  = "asia-northeast1"
    scheduler_time_zone = "Asia/Tokyo"
    workflow_location   = "asia-northeast1"
  }
  spanner_database_role_on_app_sa {
    bind         = true
    is_read_only = false
  }
  notification = {
    slack_channel = "slack_channel"
  }
  providers = {
    (略)
  }
}

モジュール内ではTerraformリソースごとにtfファイルを持っており、現在は20ファイル程度で構成されています。つまり、モジュールは約20種類程度のTerraformリソースで構成されています。input variableの仕様はterraform-docsを利用してREADME.mdを生成し、利用者に提供しています。 input variableについてはほぼほぼ変数名通りではありますが、以降ではそれぞれについての詳細と構成されるリソースの概要について紹介します。

instance

こちらはほぼgoogle_spanner_instanceリソースに向けた変数を指定できます。本モジュールはインスタンスごとの定義となっています。

database

こちらはインスタンス内に作成するgoogle_spanner_databaseリソースに向けた変数を指定できます。また、enable_backupでデータベースごとにバックアップを構成するかどうかを指定することができます。

spanner_autoscaler

こちらはautoscalerを有効にするかどうかを指定できます。default値で有効になっています。有効にした場合はautoscaler用のサービスアカウントや必要なパーミッション等を定義します。マイグレーション向けにservice_account_idを指定した場合は、既に存在するサービスアカウントを利用するようにしています。また、autoscaler自体に対する設定についてはautoscalerの設定の実態がKubernetesのCRDであり、既にKubernetesリソースを管理するレポジトリでの資産があるため、そちらを利用してもらうようにしました。

backup

こちらはバックアップに関する詳細を指定できます。default値が推奨の値になっています。backup_schedulesでバックアップのscheduleを定義し、Cloud Schedulerによりバックアップをトリガーします。バックアップジョブはWorkflowsにより起動、終了の監視をします。interval_hoursから一定期間内にバックアップが成功しているか、失敗していないか、期間内にバックアップが終了しているかを監視するDatadog monitorを作成します。retention_daysでバックアップの保持期間を指定できます。

spanner_database_role_on_app_sa

こちらはアプリケーション側のサービスアカウントに対する権限を指定できます。大きく書き込みもするアプリケーションと読み込みのみをするアプリケーションがあり、is_read_onlygoogle_spanner_database_iam_memberリソースへのroleをroles/spanner.databaseUserroles/spanner.databaseReaderにするかを指定します。

notification

利用者への通知先を指定できます。現状はDatadog monitorの通知先としてslack_channelが指定できるようになっています。default値では共用のチャンネルになっています。

providers

module blockの仕様 通りのマイクロサービス固有のリソースで使用しているproviderを指定します。

モジュールで工夫した点

以降ではモジュールを実装した際に工夫した点について簡単に紹介します。

processing_unitsをautoscalerが有効の場合にのみignore_changesにする

autoscalerを有効にした場合はautoscalerがインスタンスのCPU Utilizationによってprocessing_unitsを更新します。この場合Terraform state側との乖離が発生してしまい、terraform applyをしてしまうと、Terraformで指定した値にprocessing_unitsが収束してしまいます。こちらの対応としてはlifecycle.ignore_changesを指定する必要があります。一方マイクロサービスによってはautoscalerを利用していないものも存在します。このため、var.spanner_autoscaler.enableによって動的にlifecycleを設定する必要がありますが、こちらは現状のTerraformの仕様上できません。代わりに以下の通り別のリソースを作成することにしました。

resource "google_spanner_instance" "spanner_instance" {
  count            = var.spanner_autoscaler.enable ? 0 : 1
  (略)
}

resource "google_spanner_instance" "spanner_instance_autoscaler" {
  count            = var.spanner_autoscaler.enable ? 1 : 0
  (略)
  lifecycle {
    ignore_changes = [processing_units, num_nodes]
  }
}

locals {
  spanner_instance  = var.spanner_autoscaler.enable ? google_spanner_instance.spanner_instance_autoscaler[0] : google_spanner_instance.spanner_instance[0]
}

リソースのname、id等のlength制限の回避

作成されるインスタンスやデータベースに紐づくリソースのnameやidにはインスタンス、データベースのnameを持たせたいです。しかしリソースによってはlength制限に該当してしまうケースがあります。例えば、google_spanner_instance.nameにはThe name must be between 6 and 30 characters in lengthとあり、google_service_account.accound_idにもmust be 6-30 characters longとあります。account_idに用途ごとのprefixをつけたい場合はインスタンスのnameによってはlengthを超えてしまうケースがあります。今回はこれを回避するために、Random Providerを使用し、制限を超える場合は一部をより短いlengthの文字列に置き換えることで回避しました。以下のような定義にしました。

resource "google_service_account" "workflow" {
  account_id   = "workflow-${random_string.id_for_spanner_instance_short_name.result}"
  (略)
}

resource "random_string" "id_for_spanner_instance_short_name" {
  (略)
}

共有のSecretを複数マイクロサービスで利用したい

こちらは本モジュール自体の内容ではありませんが紹介します。本モジュールでプロジェクトごとではないAPI key等のSecretを利用したいケースがありました。共有用のプロジェクトのSecret ManagerにSecretを保存し、Secretを利用する各マイクロサービスのサービスアカウントにroles/secretmanager.secretAccessor roleを付与することで同一のSecretを1箇所に集約して各マイクロサービスからアクセスできるようにするとよさそうです。一方、本モノレポでのCIにおけるterraform applyは、権限をマイクロサービスごとに移譲させるため、個々のマイクロサービスに存在する専用のサービスアカウントを利用するようになっています。このサービスアカウントに共有プロジェクトへの権限を直接付与するのは避けたいです。この対応として共有プロジェクトの権限を持つサービスアカウントをimpersonate_service_accountに設定し、各マイクロサービスのterraform applyをするサービスアカウントが権限を借用できるようなproviderが用意されています。以下のようなリソース定義により、各マイクロサービスから共有のプロジェクトの特定リソースに対してterraform applyができるようになっています。

# 共有リソース用のprovider定義
provider "google" {
  alias = "common"
  impersonate_service_account = "共有プロジェクトへの権限を持つサービスアカウント"
}
# モジュール定義
module "spanner" {
  (略)
  providers = {
    google        = google
    google.common = google.common
  }
}

# モジュール内の共有プロジェクトへのリソース定義
resource "google_secret_manager_secret_iam_member" "some_api_key" {
  provider  = google.common
  project   = "共用のプロジェクト"
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${サービスアカウント}"
  secret_id = "some_key"
}

現状の課題について

最後に本モジュールに関連した現状の課題について紹介します。

既存のマイクロサービスのマイグレーションについて

本モジュールを利用していないメルペイの既存のマイクロサービスに対しても、本モジュールを利用したリソース定義とするべくマイグレーションをしたいと考えております。github.com/hashicorp/hcl/v2 を利用して、既存の定義をパースし、cloud.google.com/go 配下の各パッケージを利用し既存のリソースの状態を取得することにより、本モジュールのリソース定義やstateをマイグレーションする定義を出力するスクリプトなどを実装しています。しかし、Terraform管理外の既存のバックアップ等の動作を停止させる必要があったり、Cloud Spannerという極めて重要なリソースに関するマイグレーションであったりすることから、マイクロサービスごと1件づつ対応しており、現在も継続してSREチームで対応中です。

Terraformリソース定義のvalidationについて

本モジュールにより、Cloud Spanner関連のリソース定義を集約できるようになりましたが、依然として各マイクロサービスにて固有にCloud Spanner関連のリソースを定義することができてしまいます。場合によってはベストプラクティスに則っていないものが存在してしまう可能性もあります。こちらの対応として、一通りマイグレーションが終わった後でConftestによるポリシーを追加し、メルペイのリソースに関してはポリシーによるvalidationをCIですることにより防止したいと考えています。

おわりに

簡単ではありますが、Cloud Spannerの構成を標準化するためのTerraformモジュールについて紹介させていただきました。
明日の記事は @katsukitさんです。引き続きお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加