背景

こんにちは。アクセンチュアのLiです。

いきなりですが、Terraformによる複数環境の管理は難しい!!!何の話かというと、システム開発では、dev(開発環境)、staging(ステージング環境)、prod(本番環境)など、リソース構成が類似するものの、差分が存在する複数環境を同時に運用しなければならないケースが多いですよね。
そのような複数環境をTerraformを活用してどのように管理すべきかは、非常に悩ましい問題です。ネットで検索すると、様々な会社に所属する方々が、ベストプラクティスを模索されているのがよくわかります。

私は、アクセンチュア転職後にTerraformを使いはじめて、TerraformのようなInfrastructure as Codeの考え方に基づいて、複数環境の管理・運用を経験してきました。
いくつかの案件を経験する中で、時にはTerraformを一生懸命コーディングしたものの思ったとおりに動かない!というような苦労をする時期もありました。最近ようやく「どの案件でも、こういった部分は共通的に実装するとよいな」というポイントのようなものが見えてきました。

本記事では、これまで現場で経験したTerraformでの環境実装や運用経験を振り返りつつ、複数環境の実装・運用時の留意点をシェアしたいと思います。

実装

本記事では、 Terraform workspaceを活用して、複数環境の管理・運用を行う方法をご紹介します。Terraformは複数のクラウドプラットフォームをサポートしていますが、本記事ではGoogle Cloud Platform(以下、GCP)を例にしています。

たとえば、GCP上に「dev」、「staging」、「prod」という3環境があるとします。devとstaging環境には、テスト用のGoogle Cloud Storage(以下、GCS)バケット(例:{環境識別子}-mybucket01{環境識別子}-mybucket02)を作成するが、prod環境に作成したくない、という要件があるとします。環境ごとに.tfファイルを作成し、terraform applyすることも可能ですが、Terraform workspaceを使えば、複数環境のリソースを同じ.tfファイルにまとめることができます。

実装上のポイントは以下のとおりです。なお、以下のサンプルコードではTerraformモジュールを使っていますが、必須ではありません。

  • TerraformのCLIを使い、workspaceを環境分(dev、staging、prod)用意する
  • どの環境でリソースを作るべきか、変数を用意して制御する
  • resourceブロックを書く時、 countでリソースを作成するか制御する

ディレクトリ構成

Terraformのディレクトリには、以下のようにファイルを配置します。

├── backend.tf
├── main.tf
├── module
│ ├── gcs.tf
│ └── variables.tf
└── provider.tf

ファイルおよびディレクトリについての説明です。

ファイル/ディレクトリ名 説明
backend.tf Terraformのステートファイルを保存する場所
main.tf 実際に作るリソース(GCS)の定義
module(ディレクトリ) Terraformモジュール関連ファイルを格納するディレクトリ
gcs.tf テスト用GCSリソースを作るresourceブロックを定義
variables.tf gcs.tfにある変数を定義
provider.tf Terraform providerを定義

主な.tfファイルの解説

.tfファイルのうち、複数環境の制御に関わるファイルについて、ファイル単位で解説します。

main.tf

locals {
   gcs_names = {
     mybucket01 = "${terraform.workspace}-mybucket01"
     mybucket02 = "${terraform.workspace}-mybucket02"
  }

   enabled = {
     dev = true
     staging = true
     prod = false
  }

   storage_class = {
     dev = "STANDARD"
     staging = "STANDARD"
     prod = "STANDARD"
  }

   location = {
     dev = "asia1"
     staging = "asia1"
     prod = "asia1"
  }
}

module "gcs_mybucket01" {
   source = "./module/"

   enabled = local.enabled[terraform.workspace]
   name = local.gcs_names["mybucket01"]
   location = local.location[terraform.workspace]
   storage_class = local.storage_class[terraform.workspace]
}

module "gcs_mybucket01" {
   source = "./module/"

   enabled = local.enabled[terraform.workspace]
   name = local.gcs_names["mybucket02"]
   location = local.location[terraform.workspace]
   storage_class = local.storage_class[terraform.workspace]
}

  • 変数説明
    • enabled = { dev=true...}
      • enabledという名の変数を用意する。enabled変数内では、キー、バリューのペアを定義する。
      • キーはTerraform workspaceの名前(今回の例では環境区分)で、バリューはそのworkspaceにリソースを作成するかを判断するために、true/falseを定義する。
    • enabled = local.enabled[terraform.workspace]
      • terraform.workspaceという固有変数で現在のworkspace名を取得して、local.enabledのキーを検索し、バリュー(true/false)をterraformモジュール(gcs.tf)に渡す。
gcs.tf

resource "google_storage_bucket" "bucket" {
   count = var.enabled ? 1 : 0
   project = var.project
   name = var.name
   location = var.location
   storage_class = var.storage_class
}

  • 変数説明
    • count = var.enabled ? 1 : 0
      • 渡された値がtrueの場合、リソースを1つ作成し、falseの場合、リソースを作成しない。
      • 参考:クエスチョンマーク(条件式)の使い方の公式ドキュメント
variables.tf

variable enabled {
   type = string
   default = "false"
   description = "リソースを作成するか"
}

variable project {
   type = string
   default = "xxxxxxxxx"
   description = "GCS作成先のGCPプロジェクトID"
}

variable name {
   type = string
   default = ""
   description = "バケット名"
}

variable location {
   type = string
   default = "asia1"
   description = "バケットのロケーション"
}

variable storage_class {
   type = string
   default = "STANDARD"
   description = "ストレージのクラス"
}

考察

上記サンプルコードのように、countterraform workspace条件式を活用することで、同じ.tfファイルで複数環境のリソースを管理できるようになります。 このようなコード設計で環境を行い、メリットと感じた点、および、ソースコードを工夫して修正した場面は以下のとおりです。

メリットと感じた点

  1. 全環境の全リソースを1つの.tfファイルで俯瞰することができ、環境への反映漏れを最小限に留められること
  2. 新環境の追加にすばやく対応できること
  3. リソースの追加は素早く各環境に展開できること

ソースコードを工夫して修正した場面

  1. リソースを制御する時、for_eachを使う時もありますが、countfor_each同時に使えませんので、for_eachも使いたいと思う場面。実際にも、countfor_eachか、また両方とも諦めて、シンプルなterraformコードを作ったケースがありました。
  2. リソースを段階的に更新していきたい場面。例えば、上記サンプルコードでは、{環境識別子}-mybucket01{環境識別子}-mybucket02の2つのバケットは同時に作成されます。ただし、staging環境で{環境識別子}-mybucket01を先に作成し、数週間後に追加で{環境識別子}-mybucket02を作成したい場合は工夫する必要があります。苦肉の策ですが、案件では、dev環境とstaging環境のブランチを分けるという方法で、この問題を回避しました。

展望

以上、Terraformで複数環境の実装・運用方法と留意点を整理してみました。

環境管理のほかの方法として、例えば「Terraform: Up & Running: Writing Infrastructure As Code」( Yevgeniy Brikman (著))では、異なる環境のリソース定義を異なるリソースセット(.tfファイル)として管理する、という方法を紹介しています。

私が参画している案件では、上記のリソース管理する方針を採用するケースが多いですが、ディレクトリを分けたり、環境依存情報を敢えてハードコードしたりするケースもあります。
どのように複数環境を管理するかは、Terraformで管理したいリソースの特徴、チーム構成、リポジトリ構成やCI/CD等を考慮に入れる必要があると思います。ビジネス要件の複雑さは、1つの方法のみで管理できるものではなく、状況に応じて最適解を常に考え続ける必要があります。

Terraformも常に進化しており、未来のバージョンでは、複数環境の管理がさらに容易に管理できるよう、機能追加がなされるものと期待しております。 また、TerraformコミュニティやTerraformのGitリポジトリもウォッチしていきたいと思います。

最後に、Terraformと似ていますが、異なるアプローチであるConfiguration as Dataの考え方についても気になっており、キャッチアップの途中です。また知見が纏まりましたら、ご紹介したいと思います!

李 自如

テクノロジー コンサルティング本部 インテリジェントソフトウェアエンジニアリングサービス グループ アナリスト

ニュースレター
Subscribe to アクセンチュア・ クラウド・ ダイアリーズ Blog Subscribe to アクセンチュア・ クラウド・ ダイアリーズ Blog