Legalscape のインフラストラクチャー管理を支える Terraform

Legalscape (リーガルスケープ) アドベントカレンダー 2021 の 12/9 (木) のエントリです。

このエントリでは Legalscape のインフラストラクチャー管理を支えている Terraform について、その導入背景と Legalscape での運用状況についてお送りいたします。

Terraform 導入に至った背景

先日のシステム構成のエントリ でもご説明したように、Legalscape ではプロダクトのβ版提供開始当初から Google Cloud Platform (GCP) を採用していました。そして当初のクラウドリソース管理は利用している GCP のサービスも少なかったため、gcloud コマンドや Web の管理コンソール (https://console.cloud.google.com) を用いて牧歌的に行われていました。

しかし時間が経過するとともに、業務委託や正社員などプロダクトの開発に関わる人員が少しずつ増え、またシステムも徐々に複雑に進化していきます。すると、それに応じて クラウドリソースの最新の構成がどのようになっているのか をキャッチアップするのに時間的なコストがかかるようになりました。また、システム構成を変更しようとする際の影響範囲の把握が難しくなるため有識者にレビューを依頼したくなるものの、システム変更の概要や手順を漏れなく詳細に記述するのが大変だしそもそもレビューがし辛い といった問題が生じるようになりました。

ここでそれなりの大きさのエンジニアリング組織であれば、インフラエンジニア的な人員を用意してクラウドリソースの管理を主に担当してもらう… という選択肢もとりえるかとは思います。しかし当時の Legalscape は (そして今も) スタートアップゆえに潤沢なエンジニアリングリソースが存在しているとは言えず、インフラエンジニア的な人員を用意するのが難しいという事情がありました。

一方で世間的には、Amazon Web Services (AWS) や GCP などのクラウドプラットフォームを利用してプロダクトを構築することが標準的となり、それに合わせてインフラストラクチャーをコードで表現して管理する、いわゆる Infrastructure as Code (IaC) を実現するツールの利用が当たり前になりつつありました。これら IaC を実現する各種ツールの登場により、インフラストラクチャーはもはやソフトウェアエンジニア自らが管理できる、むしろ管理すべきもの になってきたと感じています。そしてその IaC を実現するツールの代表格 (の一つ) である Terraform は、特にバックエンドサービスの開発に関わる現代のソフトウェアエンジニアにとって必須のスキル となりつつあるのではないでしょうか?1

そのような社内や世間の事情を考慮し、Legalscape でも Terraform を導入し利用することにしました。

Legalscape における Terraform 運用の詳細

運用方針の策定

新しいツールを導入してその価値を最大限に引き出す運用を実現するには、最初の運用設計が肝心だと僕は考えています。Terraform 導入当時の Legalscape には Terraform を扱ったことがあるメンバーがあまりいなかったため、習熟度を考慮して以下の基本的な運用方針を定めることにしました。2

  • 必ずしも常に・すべてのインフラを Terraform コンフィギュレーションで記述する必要はありません
    • 記述にかかる手間やその必要性、また開発者自身の Terraform 習熟度を考慮して適宜記述の要否を決めるものとします
  • Terraform コンフィギュレーション / ステート は小さく保つようにします
    • 「全体で一つのコンフィギュレーション / ステート」のようなモノリシック構成にするのではなく、例えばマイクロサービスの単位に分割して記述・管理することとします
  • 新しくマイクロサービスを構築する場合など多量の Terraform コンフィギュレーションを書く際は、一度にすべての Terraform コンフィギュレーションを書ききる必要はありません
    • Pull request のでのコードレビューがしやすい規模に分けて徐々に書き足していきましょう

リソースの管理単位とディレクトリ構成

Terraform を初めて導入する際に一番迷うポイントは、リソースの管理単位とそれに関連するディレクトリ構成をどのようにするか、ではないでしょうか?

すべての環境すべてのクラウドリソースを一つのコンフィギュレーションファイル (.tf) に収めるモノリシックな構成は考慮すべきことが少ないがために Terraform 導入当初にはうまくワークするかもしれません。しかし、システムを構成するリソースが増えてくると徐々に運用が辛いものになっていくことでしょう。では具体的にクラウドリソースの集合をどのように分割して管理すればいいのか? Terraform のモジュールはどのように利用するべきか? ステージング環境やプロダクション環境などの環境の違いをどのようにコンフィギュレーションで表現すればいいのか? など、考え始めると悩みは尽きません。

弊社もこのリソースの管理単位をどうするかで悩みましたが、初導入とは言え最初からモノリシックな構成にすると (不可能ではないものの) 後々のリファクタリングが辛いものになることが想像できたので、結果として以下の構成をとることにしました。

  • リソースの管理単位
    • 「一つのマイクロサービス」など、まとめて管理したいクラウドリソースの集合 (便宜上コンポーネントと呼んでいる) ごとにディレクトリを分けてコンフィギュレーションファイルを配置する
    • 加えて Terraform のステート をこのコンポーネント単位で分離することで、terraform apply の影響範囲を小さめに抑える
    • これにより、システムを構成するすべてのクラウドリソースが一つのコンフィギュレーションファイルに記述されてしまうモノリシックな構成を抑止する
  • コンポーネント内のコンフィギュレーションファイル構成
    • コンポーネント内の main.tf ファイルにはプロバイダとステートのバックエンドのみを記述する
    • 具体的なクラウドリソースの宣言は、適切なファイル名を付けた (main.tf とは別の) コンフィギュレーションファイルに記述する
    • 度々現れるクラウドリソースの組み合わせは Terraform モジュールを用意してそちらに宣言を記述する
    • これにより、コンポーネントの main.tf にすべてのクラウドリソースが記述されてしまうようなモノリシックなファイル構成をある程度抑止する
  • Terraform モジュール
    • 主に可読性と DRY (Don’t repeat yourself) 原則を守る目的で、コンポーネントよりも小さい単位で一緒に管理したいクラウドリソースを宣言する際に利用する
  • 環境ごとのコンフィギュレーション
    • 原則として同一のコンフィギュレーションをそれぞれの環境で利用することで、環境ごとのクラウドリソースの差異を小さく保つようにする
    • ステージングやプロダクションそれぞれで固有の値などの具体的な環境の差異は、Terraform の変数で表現する
    • クラウドリソースの維持コスト的にプロダクション環境とそれ以外の環境とで同一の構成をとるのが現実的ではない場合は、サブディレクトリを用意して個別のコンポーネントとして扱う
      • それぞれのコンポーネントはステージングの構成のみ、プロダクションの構成のみが記述される

具体的なディレクトリ構成・ファイル構成は以下のようになっています。3

(root)
  ├ modules/              ... マイクロサービスやコンポーネントに依存しない形で関連するリソースをモジュールとしてまとめて定義する。
  │   │                       ここに配置したモジュールは、一つ以上のマイクロサービス/コンポーネントの Terraform コンフィギュレーションで参照される。
  │   ├ some_module/
  │   │   ├ main.tf       ... ここで具体的なリソースを定義する (provider や backend の記述は、この main.tf では不要)。
  │   │   └ variables.tf  ... このモジュールが要求する変数をここで宣言する。モジュールを利用する側の Terraform コンフィギュレーションでは、利用の際にこの変数の具体的な値を定義する必要がある。
  │   └ .../
  │
  ├ environments/         ... 環境別の tfvars ファイルを保持するディレクトリ。
  │   │                       マイクロサービスやコンポーネントに依らない変数 (GCP のプロジェクト ID など) はここで定義する。
  │   ├ dev.tfvars        ... 開発環境に関する変数をこのファイルで定義する。
  │   └ prod.tfvars       ... プロダクション環境に関する変数をこのファイルで定義する。
  │
  └ components/           ... マイクロサービス/コンポーネントに依存するインフラ構成の Terraform コンフィギュレーションをここで定義する。
      ├ some_app/
      │   ├ main.tf       ... provider や backend などをこのファイルで定義する。
      │   │                   個々の都合に応じて、provider のバージョンを適宜選択して構わない。
      │   ├ some_app.tf   ... このマイクロサービス/コンポーネントに関する定義を適度にファイル分割して記述する。
      │   │                   定義済みのモジュールを利用してもいいし、または直接ここでリソースを定義してもよい。
      │   ├ variables.tf  ... このマイクロサービス/コンポーネントの変数を宣言する。ここで宣言すべき変数は、基本的には環境に依存するものとなるはずである。
      │   │                   宣言された変数は dev.tfvars/prod.tfvars にて定義する必要がある。
      │   ├ dev.tfvars    ... このマイクロサービス/コンポーネントに固有であり、かつ開発環境に依存する変数をここで定義する。
      │   └ prod.tfvars   ... プロダクション環境に依存する変数をここで定義する。
      └ .../

Terraform CLI のバージョンアップ

以前から Terraform をお使いの方はご存知かと存じますが、Terraform はバージョン 1.0 に至るまでに頻繁にマイナーバージョンアップを繰り返してきました。バージョン 1.0 より前の Terraform を利用するにあたっては、この頻繁なバージョンアップとどう付き合うかが運用上のポイントの一つになり得ました。

Legalscape が Terraform 導入に向けて実際に動き始めたのは 2020 年 8 月の上旬で、ちょうどバージョン 0.13 のリリースを控えていた時期でした。この微妙なタイミングにおいてバージョン 0.13 のリリースを待つか現行の 0.12 で行くかを検討した結果、新機能よりも安定性を優先してバージョン 0.12 を採用することにしました。

Terraform を実行する環境を整備するにあたって、弊社では当初から HashiCorp 公式の Docker イメージ を利用し、以下のラッパースクリプト4と Makefile を用意して Terraform のコマンドを実行できるようにしていました。

#!/usr/bin/env bash

COMMAND="$1"
BASE_DIR="$(cd "$(dirname "$0")"/.. || exit 1; pwd -P)"
TERRAFORM_DOCKER_IMAGE="hashicorp/terraform:0.12.31"

docker run -it --rm \
  --name terraform-cli \
  --env GOOGLE_APPLICATION_CREDENTIALS=/adc.json \
  --env TF_VAR_env="${TF_VAR_env}" \
  --mount type=bind,source="${BASE_DIR}",target=/work \
  --mount type=bind,source="${GOOGLE_APPLICATION_CREDENTIALS}",target=/adc.json \
  --workdir "/work/$2" \
  ${TERRAFORM_DOCKER_IMAGE} \
	  "${COMMAND}" "${@:3}"
TERRAFORM = TF_VAR_env=$(ENV) bin/terraform.sh
STATE_BACKEND_BUCKET = bucket-name-to-store-state

.PHONY: init plan apply

init:
	$(TERRAFORM) init $(TARGET) \
	  -reconfigure \
	  -backend-config="bucket=$(STATE_BACKEND_BUCKET)-$(ENV)"

plan: init
	$(TERRAFORM) plan $(TARGET) \
	  -var-file=/work/environments/$(ENV).tfvars \
	  -var-file=$(ENV).tfvars

apply: init
	$(TERRAFORM) apply $(TARGET) \
	  -var-file=/work/environments/$(ENV).tfvars \
	  -var-file=$(ENV).tfvars

これにより、開発者の作業環境に Terraform CLI を事前にインストールをすることなく、かつ皆が同じ Terraform バージョンを使って気軽に terraform apply 相当の操作をできるようになっています。

しかし時が過ぎ Terraform のマイナーバージョンがどんどん上がるにつれて、新たなコンポーネントを作る際にバージョン 0.13 以降で導入された機能5を使いたいという要望が出始めました。一方でこれまでに作られてきたコンポーネントのコンフィギュレーションおよびステートすべてを 0.13 以降のバージョンでも利用できるようにアップデートするのは骨の折れる作業であったため、コンポーネントごとに異なる Terraform バージョンを利用できる仕組みを導入して解消することにしました。

具体的には以下のようにラッパースクリプトを書き換え、各コンポーネントのディレクトリ直下に .tfversion ファイルが存在すればそこで指定された Terraform バージョンを利用するようにします。

#!/usr/bin/env bash

COMMAND="$1"
TARGET="$2"
BASE_DIR="$(cd "$(dirname "$0")"/.. || exit 1; pwd -P)"

TERRAFORM_VERSION="0.12.31"

if [ -f "${TARGET}/.tfversion" ]; then
  TERRAFORM_VERSION="$(cat "${TARGET}/.tfversion" | tr -d '\n')"
fi

TERRAFORM_DOCKER_IMAGE="hashicorp/terraform:${TERRAFORM_VERSION}"

docker run -it --rm \
  --name terraform-cli \
  --env GOOGLE_APPLICATION_CREDENTIALS=/adc.json \
  --env TF_VAR_env="${TF_VAR_env}" \
  --mount type=bind,source="${BASE_DIR}",target=/work \
  --mount type=bind,source="${GOOGLE_APPLICATION_CREDENTIALS}",target=/adc.json \
  --workdir "/work/${TARGET}" \
  ${TERRAFORM_DOCKER_IMAGE} \
	  "${COMMAND}" "${@:3}"

これによりすべてのコンポーネントの Terraform アップグレードをせずとも、コンポーネント単位で気軽に最新バージョンの Terraform を利用できるようになりました。

Terraform で管理されているクラウドリソースの一例

Terraform 導入当初はトライアル的に Cloud Monitoring の外部監視アラートをコンフィギュレーションで記述してみることから始めましたが、それから 1 年以上が経過して今では Legalscape のプロダクトを構成するクラウドリソースのほとんどが Terraform で管理されるまでになりました。

以下が現時点において Terraform 管理されているクラウドリソースの一例です。

  • Compute Engine インスタンス
  • Cloud SQL インスタンス
  • Cloud Load Balancer (URL マップやバックエンドサービスなどの諸々)
  • Cloud DNS
  • サービスアカウント / ロール
  • Cloud Run
  • Cloud Tasks
  • Cloud Scheduler
  • BigQuery データセット / テーブルスキーマ

得られた教訓

以上が弊社における Terraform の運用状況になります。実際に Terraform を 1 年以上運用してみて得られた教訓は以下になります。

よかったこと 👍

  • 原則として環境ごとのコンフィギュレーションを用意せずに共通化することで、環境間のシステム構成の差異を小さく留めることができる
    • これにより、開発環境で十分に検証されたコンフィギュレーションをほぼそのままプロダクション環境に展開できている
  • コンポーネント単位でクラウドリソースを管理することで…
    • 既存リソース変更時の影響範囲が把握しやすくなる
    • コンポーネントごとに自由に Terraform CLI のバージョンを指定したりアップグレードできるようになる

よくなかったこと 👎

  • 具体的な「コンポーネント」が当初想定していた「マイクロサービス」ではなく、「同種のクラウドリソースをまとめたもの」となりがち
    • 例えばサービスアカウントとそのサービスアカウントに付与する権限を宣言するだけの汎用的なコンポーネントが作られてしまった6
  • コンポーネント単位でクラウドリソースを管理することで…
    • Data source を利用する必要が時々生じ、コンポーネント間に暗黙的な依存関係が生まれてしまっている
    • 変更頻度の小さいコンポーネントの Terraform バージョンが古いまま取り残されている

終わりに

というわけで、今回は Legalscape における Terraform の導入背景と運用状況についてお送りしてきました。

「得られた教訓」にもあるように、弊社の構成がすべてにおいて良い結果になるわけではありませんが、Terraform 導入に迷っていらっしゃる方々にとってこのエントリが多少なりとも参考になれば幸いです。

そして弊社における IaC の取り組みはまだまだ道半ばです。この Terraform の運用の改善はもちろんのこと、新たなツールを導入もしくは開発して、ソフトウェアエンジニアによるインフラストラクチャーの管理をより快適にしてくれる、そんな方を我々はお待ちしております 🤗

採用情報

最後までお読みいただきありがとうございました!


  1. 個人の感想であり、所属企業の見解を代表するものではございません。 

  2. この運用方針は実際に Terraform コンフィギュレーションを管理している git リポジトリの README.md に記載されているものになります。 

  3. こちらも git リポジトリの README.md に記載されているものになります。 

  4. エントリ掲載の都合上、パラメータチェックやエラーハンドリングなどの記述を省いています。 

  5. 具体的には、モジュールを利用する際に for_each を記述できる機能 になります。 

  6. 何を隠そうこの僕がこのコンポーネントを作ってしまったわけですが… 🤦‍♂️