見出し画像

dinii における Terraform の改善活動

こんにちは。Platform Teamで SWE をしている 芳賀@HagaSpa です。
dinii では IaC として Terraform を使っており、今回はその改善について取り組んだことを書きたいと思います。

TL;DR

  • 既存のインフラリソースを高速かつ安全にコード化する方法

    • import block を使って安全に既存のインフラをコード化できる

  • Terraform のコードを共通化するためにやること

    • modules を積極的に採用し moved block を使ってリファクタリングをしよう

着手前の状態

当初自分が dinii に参画した時は以下のような状態でした。

  • Terraform Plan を実行すると大量の差分が出力される。

Plan: 62 to add, 24 to change, 86 to destroy.
  • Web UI からインフラリソースを変更しており、その後の Terraform  への反映が漏れている

Terraform とインフラ環境が統一されておらず、各種差分が大量に出ていました。そのため実態として Terraform はあくまでインフラの構成を一部表現しているだけに留まっていました。技術的な問題ではなくサービス開発の速度に IaC が追いついていないのが主な原因だと認識しています。IaC はその性質としてビジネスへインパクトを出すものではないので、どうしても優先順位としては低くなり後回しになりやすいです。
そのため機能開発やインフラの拡張は進んでいたのですが、インフラの構成管理はある程度放置されていました。
この状況を解決するために以下のステップで進めていきました。

  1. 既存のインフラリソースを Terraform へ反映する

  2. Terraform コードの共通化する

既存のインフラリソースを Terraform に反映する

途中から IaC 化を始めたベンチャーなどでよくあるユースケースとして、すでに作成済みのインフラリソースを Terraform にしたい時があると思います。dinii でもこのような作業を行う必要があり、今回は import block—generated-output を使って既存のインフラリソースを自動でコード化する方法を取りました。
例えば dinii-sample-web という Cloud Run のコードを自動生成する場合は以下のようになります。

import {
	id = "dinii-sample-web"
	to = google_cloud_run_service.web
}

id にインフラリソースの id を入力し、対応する Terraform の Resource を to に定義します。
※ 実際のインフラリソースによって id のフォーマットは異なります
その後 —generate-config-out で出力先の Terraform ファイルを指定すると、自動生成されたコードが out.tf ファイルに出力されます。

terraform plan -generate-config-out=out.tf

この機能を使うことで、すでに存在してるインフラリソースのコード化を半自動的に行うことができます。dinii ではこの機能を全ての環境で利用することで、既存リソースを安全にかつ高速にコード化をすることができました。この機能はまだ Experimental ですが、実際のインフラ環境へ影響を与えるものではないので、積極的に導入してしまっていいと思います。また import block を定義したファイルは terraform apply 後に不要になるので削除しましょう。これから IaC 化を始めようと思っている方の一歩目として、とてもおすすめできる便利な機能となっています。

Terraform コードの共通化を行う

dinii では dev, stag, prod といったように環境毎に GCP プロジェクトを分けています。各環境で利用してる GCP リソースはほとんど同じなので、今回それらを modules として共通化をしました。まず初めに改善前の dinii のディレクトリ構造を共有します。

.
├── ..
├── dinii-self-develop
│   ├── google_cloud_run_dinii_backend.tf
│   └── google_cloud_run_dinii_web.tf
├── dinii-self-prod
│   ├── google_cloud_run_dinii_backend.tf
│   ├── google_cloud_run_dinii_web.tf
│   └── google_cloud_run_dinii_cache.tf
└── dinii-self-stag
│   ├── google_cloud_run_dinii_backend.tf
│   └── google_cloud_run_dinii_web.tf
└── ..

このように dinii では GCP プロジェクト毎にディレクトリを作成し各環境のリソースを管理しています。各環境のディレクトリにそれぞれ利用するリソースを定義しており、環境間でほとんどん同じ .tf ファイルがいくつも存在していました。
今回の例としては google_cloud_run_dinii_backend.tf がそれぞれの環境ディレクトリに存在していることを指しています。このような状態だと同じような設定を持った .tf ファイルが増えるため、コード量の肥大化による可読性の低下やレビュー時にレビュワーの負荷が高まります。特に同じ内容のリソースを全環境に作成する場合、リソースの一貫性が担保されていることをレビュー時に確認する必要があり、レビューコストが高くなりとても非効率です。

# cat dinii-self-develop/google_cloud_run_dinii_backend.tf
resource "google_cloud_run_service" "google_cloud_run_dinii_backend" {
  provider = google
  project  = var.project_id

  name                       = var.service_name
  location                   = var.location
	...
}

この問題を解決するために、環境毎にある程度共通してるリソースを一つの modules としてまとめました。Cloud Run を例にとってみると以下のようになります。

.
├── ..
├── dinii-self-develop
│   ├── google_cloud_run_dinii_backend.tf
│   └── google_cloud_run_dinii_web.tf
├── dinii-self-prod
│   ├── google_cloud_run_dinii_backend.tf
│   └── google_cloud_run_dinii_web.tf
├── dinii-self-stag
│   ├── google_cloud_run_dinii_backend.tf
│   └── google_cloud_run_dinii_web.tf
└── modules
    ├── cloud_run
    │   ├── README.md
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    └── ..

まず Cloud Run に関わる各種リソースを modules/cloud_run/main.tf に移動しています。dinii でいうと以下のようなリソース群になります。

  • google_cloud_run_service

  • google_cloud_run_domain_mapping

  • google_compute_region_network_endpoint_group

これらを1つの modules にまとめることで、特定の単位でリソースが統一されてることを保証できます。また各環境のディレクトリに存在していた .tf ファイルですが、このファイルは modules を呼び出すような実装に変わります。

# cat /dinii-self-develop/google_cloud_run_dinii_backend
module "google_cloud_run_dinii_backend" {
  source = "../modules/cloud_run"

  project_id   = var.project
  service_name = var.service_name
  location     = "asia-northeast1"
	...
}

modules 側で環境毎に変更したいパラメータを Input Variables として定義することで、呼び出し側でパラメータを柔軟に変更することができます。例えば環境毎に異なったスペックのリソースを作成することも可能になります。
modules の中でも特定の環境にだけ、デプロイしたいリソースがある場合は count プロパティ を動的に変更するような modules にしましょう。modules にデプロイに関するロジックを持たせたくない場合は、Root Modules の直下に配置することでも実現できます。

resource "google_compute_region_network_endpoint_group" "main" {
  count    = var.network_endopoint_group.enable ? 1 : 0
	...
}

このようなリファクタリングにおいて、とても便利なのが moved block です。こちらは terraform mv コマンドの宣言的なコードとなっています。具体的には tfstate に記録されたリソースを別のリソースへ割り当てるための機能です。今回だと GCP プロジェクト毎のディレクトリに定義されたリソースを modules の配下にあるリソースに移動したことを tfstate に教える必要があります。これをしないと modules のリソースに対して Create を行うようにTerraform は振る舞うため、同じリソースを再度作るような terraform plan の結果になってしまいます。
moved block を定義してあげることで、terraform plan 時にリソースの移動を行う実行計画が出力されるようになり、terraform apply で移動が実行されます。

moved {
  from = google_cloud_run_service.google_cloud_run_dinii_backend
  to   = module.google_cloud_run_dinii_backend.google_cloud_run_service.main
}
# google_cloud_run_service.google_cloud_run_dinii_backend has moved to module.google_cloud_run_dinii_backend.google_cloud_run_service.main
resource "google_cloud_run_service" "main" {
    ...
    # (4 unchanged attributes hidden)
    # (3 unchanged blocks hidden)
}

実際のインフラリソースへの影響は一切ないので、どんどんリファクタリングしていきましょう。

Terraform Registry

自前で modules の作成を行わなくても利用用途に合ったものが Terraform Registry に公開されていることがあります。dinii の場合は今後の運用を見越して自前で作成しましたが、可能なら採用を一度考慮してみるといいでしょう。

今後改善していきたいところ

このように Terraform においていくつかの改善をしていきましたが、まだまだやれる事はあります。

  • CD パイプラインの構築、あるいは Terraform Cloud の導入

  • Input Variables に対する Validation

  • Github に対する Terraform 管理

近い将来これらについても実現していこうと考えています。
今回はプロダクトの話ではありませんが、dinii では飲食業界のインフラを担うために色々なプロダクトを開発しています。dinii では職種問わずまだまだ人が必要なので、飲食業界を盛り上げていくために是非一緒に働いてくださる方を募集中です!

12/18 (月) にダイニー体験会をするので、是非ご興味ある方はご参加ください!

お読みくださりありがとうございました!

この記事が気に入ったらサポートをしてみませんか?