Mercari Microservices Platform における Terraform 0.12 対応

Platform Group, Platform Infra Team@dtan4 です。 現在メルカリ/メルペイのマイクロサービス基盤 (Microservices Platform) では、様々なクラウドリソース・SaaS を開発者に提供しています。これらのリソースを宣言的に管理するための手段として HashiCorp Terraform を利用しており、数百のマイクロサービスのインフラが Terraform によって管理されています。

1年以上前の話ですが、Terraform 0.12 がリリースされました。昨年後半に、我々が管理している Terraform リポジトリにおいて Terraform 0.12 対応作業を行いました。本記事では、我々の環境においてどのように Terraform 0.12 対応作業を行ったかについて説明します。

Terraform 0.12 対応の動機

Terraform 0.12 を使うことで得られるメリットは、configuration language の HCL2 対応と、for_eachdynamic といった便利な関数が使えるようになることです。例えば for_each を使うことで、配列からリソースを複数作る際に、resource ID が配列のインデックスではなく要素ベースで割り当てられるようになります。このおかげで、配列の途中で要素が追加/削除されても不必要な差分が生じなくなります。メルカリではこのような動的なリソース生成を多用しているため、新しい関数を用いることで実装がより容易になります。

variables "members" {
  default = [
    "aaa@example.com",
    "bbb@example.com",
  ]
}

# Terraform 0.11

resource "google_project_iam_member" "project_viewer" {
  count = "${length(var.members)}"

  project = "xxx"
  member  = "user:${element(var.members, count.index)}"
  role    = "roles/viewer"
}
# ==> google_project_iam_member.project_viewer[0]
# ==> google_project_iam_member.project_viewer[1]

# Terraform 0.12

resource "google_project_iam_member" "project_viewer" {
  for_each = toset(var.members)

  project = "xxx"
  member  = each.key
  role    = "roles/viewer"
}
# ==> google_project_iam_member.project_viewer["aaa@example.com"]
# ==> google_project_iam_member.project_viewer["bbb@example.com"]

また、昨年11月に Terraform 0.11 のサポート終了が宣言されたり、更に Terraform Provider の最新リリース (e.g. Google Provider v3) が Terraform 0.12 以降のみをサポートするようになったのも、Terraform 0.12 作業を始めた理由として挙げられます。

このように Terraform 0.12 では言語機能の大幅な強化が行われましたが、同時に(ある程度後方互換が保たれているとはいえ)既存 Terraform configuration の Terraform 0.12 対応はある程度の労力を割く必要がありました。Terraform 0.12 は開発者とプラットフォームチーム双方が導入を望んでいましたが、我々の環境で「安全に」導入していくとなるといろいろな工夫が必要となりました。

Terraform at Mercari Microservices Platform

実際の移行手順を説明する前に、まず我々がどのように Terraform configuration を管理しているかについて説明します。

microservices-terraform: Terraform configuration の一元管理リポジトリ

冒頭で述べたように、メルカリ/メルペイで合わせて数百のマイクロサービスが存在します。これらの Terraform configuration はまとめて一つのリポジトリ (microservices-terraform) で管理されています。リポジトリは一つですが Terraform の実行単位となる tfstate はサービスと環境単位 (prod/dev/lab) で分かれており、他のサービスの変更が plan/apply の結果に影響を及ぼすことのないよう作られています。また、Terraform そのものは CI 上でのみ実行されますが、このときも Push された Git branch の差分を確認し、変更のあるサービスでのみ Terraform を実行するようにしています。

これらの仕組みにより、管理するサービスがどれだけ増えようとも、それぞれのサービスが互いに影響を及ぼすことなく、開発者が独自のライフサイクルで Terraform を扱うことができています。一方で管理者の視点から見ると、monorepo 的な構成をとっていることで、全体に影響ある変更 (e.g. CI スクリプト更新、ポリシーやルールの追加) や権限管理が行いやすくなっています。サービスを追加するたびに Terraform の CI/CD を一からセットアップする必要もありません。開発者は、microservices-terraform に新しいサービスのディレクトリを作って必要な Terraform configuration を置くだけで、そのサービスのインフラを Terraform で管理できるようになります。

Microservice Starter-kit Terraform Module

microservices-terraform の中ではいくつかの Terraform Module を利用していますが、すべてのサービスが必ず利用している Module が Microservice Starter-kit です。この Module はすべてのサービスが共通して必要とするリソース (GCP Project, GCP IAMs, Kubernetes Namespace, GitHub Team, …) を含んでおり、サービス名や所属 (メルカリ JP/US, メルペイ) とそのサービスの Owner 一覧を記載することで、必要なリソース作成と権限設定が行われるものです。変更が加えられるたびに新しいバージョンが発行され、Amazon S3 にバージョンごとの tarball がアップロードされます。各サービスではこの tarball を pull します。これにより、それぞれのマイクロサービスが自分たちのペースにて Starter-kit のバージョン管理を行うことができます。Terraform 0.12 対応 では、このバージョニングされた Starter-kit が大きな役割を果たしました。

microservices-terraform や Starter-kit について詳しく知りたいという方は、@babarot の発表資料 Terraform Ops for Microservices も御覧ください。資料自体は2年前のものですが、大まかな仕組みについては現在でもほぼ同じです。

microservices-terraform における Terraform 0.12 対応に向けた課題

さて、数百のサービスが存在し、かつそれぞれが独立したライフサイクルで管理されているとなると、一度にすべてのサービスを Terraform 0.12 対応させるのは非現実的です。

前述したように各サービスは核となる Terraform Module "Starter-kit" を持っているのですが、サービスによって使っている Starter-kit のバージョンが異なります。今回は各サービスにおける Terraform 0.12 対応のトリガーをこの Starter-kit のバージョンに連動させましたが、古い Starter-kit を使っているサービスはまず Terraform 0.12 対応が可能なバージョンまで Starter-kit そのものを更新する必要があります。また、サービスによっては長い間 Terraform が実行されてないことで、configuration drift (手作業等により、Terraform configuration と実際のインフラの状態が乖離している状態)が存在することもありえます。安全に Terraform 0.12 対応を行うためには、事前にこれらの drift を解消しなければなりません。

以上より、開発者が自分たちのペースで自分たちのサービスを Terraform 0.12 対応できるような、何らかの工夫が必要となりました。

移行方法: 公式の対応手順

公式ドキュメント でも説明されていますが、Terraform 0.12 対応は以下の手順にて行うことができます。

最初の作業は Terraform 0.11 で行います。

  • terraform init
  • terraform plan
  • terraform apply
    • まず、現時点で Terraform configuration と実際のインフラの状態が一致している (terraform plan で差分が表示されない) 状態にします。
  • terraform 0.12checklist

    • 自動コード変換で対応できない箇所や、更新が必要な Terraform Provider が以下のように提示されます。
    After analyzing this configuration and working directory, we have identified some necessary steps that we recommend you take before upgrading to Terraform v0.12:
    
    - [ ] Provider "openpgp" may need to be upgraded to a newer version that supports Terraform 0.12. (Supported version information is not available for this provider.)
    
    - [ ] Upgrade provider "kubernetes" to version 1.10.0 or newer.
    
      No currently-installed version is compatible with Terraform 0.12. To upgrade, set the version constraint for this provider as follows and then run terraform init:
    
          version = "~> 1.10.0"
    
    Taking these steps before upgrading to Terraform v0.12 will simplify the upgrade process by avoiding syntax errors and other compatibility problems.

ここからは Terraform 0.12 のバイナリを使います。

  • terraform 0.12upgrade

    • 同じディレクトリにある全ての Terraform configuration を Terraform 0.12 で導入された書式に変換してくれます。
    • 一部、ツールにて一意に変換できないものについては TF-UPGRADE-TODO コメントを残すだけにします。
    • また、このディレクトリが Terraform 0.12 以上を要求することを明示し、 Terraform そのものでバージョンチェックできるようにするため、以下のような versions.tf を追加します。
    terraform {
      required_version = ">= 0.12"
    }
  • terraform plan

    • 大抵の場合差分は出ないはずですが、変換漏れ等で発生した場合はその都度修正します。
    • この時点ではまだ Terraform 0.11 以下のバージョンで tfstate を読むことはできます。例えば Terraform 0.12 対応用 Git ブランチとは別のブランチで、通常のインフラ更新作業を並行して行えます
  • terraform apply

    • この時点で tfstate も Terraform 0.12 ベースのものに置き換わるため、以降このサービスでは Terraform 0.11 以下を使っての作業はできなくなります。
    • 並行作業中のブランチがあれば、そのブランチを最新の master ブランチに追従させることで Terraform 0.12 対応する必要があります。

移行方法: メルカリの場合

Terraform Provider の Terraform 0.12 対応

Terraform 0.12 を使うためには、このガイドに書かれている通り、まず使っている Terraform Provider が Terraform 0.12 対応している必要があります。移行プロジェクトを始めた段階では、Terraform Registry で提供されている Official/Verified Provider の最新バージョンはすべて Terraform 0.12 に対応していたので、それらは適切なバージョン以上に更新することで対応できました。一方で、microservices-terraform では歴史的経緯により自作またはフォークした Provider をいくつか利用していたので、それらは以下のような内容で Terraform 0.12 対応させる必要がありました。

  • 自作 Provider
  • フォークした Provider
    • フォークする原因となったバグや機能不足がフォーク元の最新バージョンでは修正されていたものは、Terraform 0.12 対応のタイミングでフォークをやめて、元の Provider を使うようにしました。
    • e.g. Kubernetes RBAC 導入時は古い Kubernetes Provider が RBAC に対応していなかったのでフォークした Provider を使っていたが、最新の公式提供 Provider では対応されていたかつ Terraform 0.12 対応も済んでいたので、この機会にフォークをやめてそちらへ移行しました。
    • それ以外は自作 Provider と同じ手順で、自前で Terraform 0.12 対応させました。

Terraform modules の Terraform 0.12 対応

続いて、各マイクロサービスが利用している Terraform Module を Terraform 0.12 対応させました。現在 microservices-terraform では Terraform Registry に登録されている Module は利用しておらず、自作 Module のみを利用しています。これらを全て我々の手で Terraform 0.12 対応させる必要がありました。この作業は「公式の対応手順」通りに行い、対応完了した時点で新しいバージョンをリリースしました。これにより、「このバージョン以降の Starter-kit を使う場合は、Terraform 0.12 対応作業を済ませてください」という状態にしました。

周辺ツールの Terraform 0.12 対応

microservices-terraform リポジトリでは、Terraform そのものに加えて、plan/apply の結果通知や Terraform configuration の静的解析を行うため様々なツールが利用されています。Terraform 0.12 対応とそれに伴う configuration language の HCL2 移行により、これらのツールが正しく動作しないことが検証段階で判明しました。そのため、以下のような修正を行いました。

各サービスの Terraform 0.12 対応

ここまでの下準備が終われば、いよいよサービスそのものの Terraform configuration を Terraform 0.12 対応させることができます。ですが、以下の制約により、ここでも工夫が必要となりました。

まず microservices-terraform リポジトリにおいては、ローカルで Terraform を実行できる (tfstate を remote backend から pull できる) 権限を開発者に与えていません。Git でバージョン管理されていない変更を実環境に適用されるのを防ぐため、また開発者が持つクラウドプロバイダの権限を最小限にしたい、CI に集約させたいという理由です。この制約により、ローカルでは terraform init とその後の terraform 0.12upgrade コマンドを実行できないので「公式の対応手順」に書いた方法はローカル環境では行なえません。

そのため、各サービスの Terraform 0.12 対応は以下の手順で行えるようにしました。

  • Starter-kit を Terraform 0.12 対応したバージョンの「ひとつ前」まで更新すると、CI が自動で terraform 0.12upgrade の実行と Starter-kit 更新を行い、Terraform 0.12 対応用の Pull Request を作るようにしました。
    • 例えば Starter-kit v0.20 を Terraform 0.12 対応バージョンとした場合、「ひとつ前」の Starter-kit v0.19 を使っていれば、CI が terraform 0.12 対応 Pull Request を自動で作成します。
    • 特に差分が無ければ、開発者が自分で Approve -> Merge することで Terraform 0.12 対応が完了します。差分があればその Pull Request 上または別のブランチで修正し、問題なくマージできる状態にする必要があります。

常に最新の Starter-kit に追従できているサービスであればほとんど苦労なく Terraform 0.12 対応できましたが、古い Starter-kit を使い続けているサービスはまず「ひとつ前」のバージョンまで追従しなければなりません。Starter-kit の更新には特定の設定追加を要するものもあり、開発者の負担になることは予想できました。

そこで、Starter-kit を「ひとつ前のバージョンまで更新」を手軽にできるようヘルパースクリプトを提供しました。以下のコマンドを実行するだけで、対象サービスにおいて Starter-kit の更新と必要なパッチ適用が行われます。

# e.g. upgrade Starter-kit in mercari-echo-jp-prod to the latest version
script/upgrade-microservice-starter-kit /path/to/mercari-echo-jp/production

このスクリプトは導入後半年以上経った現在でも継続してメンテナンスされており、Starter-kit の更新に対する障壁を低くするのに一役買っています。

利用する Terraform バイナリの使い分け

移行作業を開始すると Terraform 0.12 対応済みのサービスとそうでないサービスが混在することになります。そのため、Terraform を実行するサービスに応じて、使う Terraform バイナリを使い分ける必要があります。同時に、最新の Provider は Terraform 0.11 以前に対応していない場合があるので、Provider の使い分けも必要です。 今回は terraform 0.12upgrade コマンドにて生成される versions.tf の有無を Terraform 実行前に確認し、存在する場合は Terraform 0.12 バイナリと Terraform 0.12 対応済みの Provider、そうでない場合は Terraform 0.11 バイナリと古いバージョンの Provider を使うようにする処理を CI に導入しました。例えば、Terraform バイナリの使い分けは以下のようなシェルスクリプトで実装されています。

#!/bin/bash

declare workdir="$PWD"

get_terraform_cmd() {
  if [[ -f "${workdir}/versions.tf" ]] && grep -q 'required_version = ">= 0.12"' "${workdir}/versions.tf" >/dev/null; then
    echo "terraform-0.12"
  else
    echo "terraform-0.11"
  fi
}

$(get_terraform_cmd) $@

Terraform 0.12 対応進捗率の管理

Microservices Platform では、DevStats の仕組みを用いて、現在運用されているサービスの数を Starter-kit のバージョン単位で見られる仕組みができていました。この Datadog Dashboard を用いて、Terraform 0.12 対応の進捗具合(特定バージョン以上の Starter-kit を利用している数)を確認していました。

ちなみに、今年頭にサービスの Terraform 0.12 対応を開始しましたが、現時点で新規サービスも含め 86% のサービスが Terraform 0.12 対応されているようです。Starter-kit そのもののサポートポリシーも含め、対応の追いついてないサービスをどのようにケアしていくかは今後の課題です。

まとめ

本記事では、メルカリ/メルペイ向けに運用している Microservices Platform の Terraform リポジトリにて、どのように Terraform 0.12 対応作業を行ったかについて解説しました。メルカリ独自の管理手法が多分に含まれるため、今回公開した方法がすべての環境にて適用できるとは言い切れないですが、少しでも参考になれば幸いです。