Terraform を使用するためのベスト プラクティス

このドキュメントでは、複数のチームメンバーやワーク ストリームで Terraform を使用した効果的な開発を行うためのガイドラインと推奨事項について説明します。

このガイドでは Terraform の概要は説明しません。Google Cloud で Terraform を使用する方法については、Terraform を使ってみるをご覧ください。

スタイルと構造に関する一般的なガイドライン

以下の推奨事項は、Terraform 構成の基本スタイルと構造を対象としています。この推奨事項は、再利用可能な Terraform モジュールとルート構成に適用されます。

標準のモジュール構造に従う

  • Terraform モジュールは、標準のモジュール構造に従う必要があります。
  • デフォルトでリソースが配置されている main.tf ファイルを使用してモジュールを起動します。
  • すべてのモジュールに、Markdown 形式の README.md ファイルを含めます。README.md ファイルに、モジュールに関する基本のドキュメントを含めます。
  • 例ごとに個別のサブディレクトリを使用して、例を examples/ フォルダに配置します。各例について、詳細な README.md ファイルを含めます。
  • 独自のファイルと network.tfinstances.tfloadbalancer.tf などの記述名を使用して、リソースの論理グループを作成します。
    • すべてのリソースに独自のファイルを割り当てることは避けます。共有目的別にリソースをグループ化します。たとえば、dns.tfgoogle_dns_managed_zonegoogle_dns_record_set を組み合わせます。
  • モジュールのルート ディレクトリには、Terraform(*.tf)とリポジトリ メタデータ ファイル(README.mdCHANGELOG.md など)のみを含めます。
  • 追加のドキュメントは docs/ サブディレクトリに配置します。

命名規則を採用する

  • すべての構成オブジェクトに名前を付けます。単語の区切りにはアンダースコアを使用します。これにより、リソースタイプ、データソース タイプ、その他の事前定義された値の命名規則と一貫性を保つことができます。この規則は、名前の引数には適用されません。

    推奨:

    resource "google_compute_instance" "web_server" {
      name = "web-server"
    }
    

    非推奨:

    resource "google_compute_instance" "web-server" {
      name = "web-server"
    }
    
  • リソースタイプの 1 つ(モジュール全体の単一のロードバランサなど)に対する参照を簡素化するため、リソースに main という名前を付けます。

    • some_google_resource.my_special_resource.idsome_google_resource.main.id の違いを思い出すには、追加のメンタルワークが必要です。
  • 同じタイプのリソースを区別するには(たとえば、primarysecondary)、意味のあるリソース名を指定します。

  • リソース名を単数形にします。

  • リソース名でリソースタイプを繰り返さないでください。次に例を示します。

    推奨:

    resource "google_compute_global_address" "main" { ... }
    

    非推奨:

    resource "google_compute_global_address" "main_global_address" { … }
    

変数を慎重に使用する

  • すべての変数を variables.tf で宣言します。
  • 変数には、用途や目的がすぐわかる名前を付けます。
    • 入力、ローカル変数、ディスクサイズや RAM サイズなどの数値を表す出力には、単位(ram_size_gb など)を使用する必要があります。Google Cloud APIs には標準単位がないため、変数名に単位を付けると、構成のメンテナンス担当者にとって想定される入力単位が明確になります。
    • ストレージの単位にはバイナリ単位の接頭辞(1,024 の累乗)(kibimebigibi)を使用します。他のすべての測定単位には 10 進単位の接頭辞(1,000 の累乗)(kilomegagiga)を使用します。この使用法は、Google Cloud 内の使用法と同じです。
    • 条件付きロジックを簡素化するには、ブール値変数に正の名前(enable_external_access など)を付けます。
  • 変数には説明を含める必要があります。公開モジュールの自動生成ドキュメントには、説明が自動的に含まれます。説明により、説明的な名前で提供できない追加のコンテキストを新しいデベロッパーに提供できます。
  • 変数の定義型を指定します。
  • 必要に応じて、デフォルト値を指定します。
    • 環境に依存しない値(ディスクサイズなど)を持つ変数の場合は、デフォルト値を指定します。
    • 環境に固有の値(project_id など)を持つ変数の場合、デフォルト値は指定しないでください。このように、呼び出しモジュールでは意味のある値を提供する必要があります。
  • 変数(空の文字列やリストなど)に空のデフォルトを使用するのは、変数を空のままにすることが有効な設定で、ベースとなる API に拒否されない場合に限られます。
  • 変数の使用は慎重に行ってください。インスタンスまたは環境ごとに異なる値のみをパラメータ化します。変数を公開するかどうかを決定するときは、変数を変更するための具体的なユースケースがあることを確認してください。変数が必要になる可能性がわずかしない場合は、その変数を公開しないでください。
    • デフォルト値を含む変数の追加には下位互換性があります。
    • 変数の削除には下位互換性がありません。
    • リテラルが複数の場所で再利用されている場合は、ローカル値を変数として公開せずに使用できます。

出力を公開する

  • すべての出力を outputs.tf ファイルに整理します。
  • すべての出力にわかりやすい説明を入力します。
  • 出力の説明を README.md ファイルに記述します。terraform-docs などのツールを使用して、commit の説明を自動生成します。
  • ルート モジュールが参照または共有する必要があるすべての有用な値を出力します。特にオープンソースまたは使用頻度の高いモジュールの場合は、消費される可能性があるすべての出力を公開します。
  • 入力変数を介して出力を直接渡さないでください。渡してしまうと、依存関係グラフに適切に追加できなくなります。暗黙的な依存関係を確実に作成するには、リソースから参照属性が出力されるようにします。インスタンスの入力変数を直接参照するのではなく、次のように属性を渡します。

    推奨:

    output "name" {
      description = "Name of instance"
      value       = google_compute_instance.main.name
    }
    

    非推奨:

    output "name" {
      description = "Name of instance"
      value       = var.name
    }
    

データソースを使用する

  • 参照するリソースの横にデータソースを配置します。たとえば、インスタンスの起動に使用するイメージを取得する場合は、データソースを独自のファイルに収集するのではなく、インスタンスと一緒に配置します。
  • データソース数が多い場合は、専用の data.tf ファイルに移動することを検討してください。
  • 現在の環境に関連するデータを取得するには、変数またはリソースの補間を使用します。

カスタム スクリプトの使用を制限する

  • スクリプトは必要な場合にのみ使用してください。スクリプトによって作成されたリソースの状態が、Terraform で考慮または管理されることはありません。
    • 可能であれば、カスタム スクリプトは使用しないでください。Terraform リソースが目的の動作をサポートしていない場合にのみ使用してください。
    • 使用するすべてのカスタム スクリプトは、作成の理由について(可能であれば廃止するタイミングも)明確に文書化しておく必要があります。
  • Terraform は、プロビジョナ(local-exec プロビジョナを含む)を介してカスタム スクリプトを呼び出すことができます。
  • Terraform で呼び出したカスタム スクリプトを scripts/ ディレクトリに配置します。

ヘルパー スクリプトを別のディレクトリに含める

  • Terraform による呼び出しの対象外であるヘルパー スクリプトを helpers/ ディレクトリに編成します。
  • 説明と呼び出しの例を含めて、README.md ファイル内にヘルパー スクリプトを文書化します。
  • ヘルパー スクリプトが引数を受け入れる場合は、引数チェックと --help 出力を提供します。

静的ファイルを別のディレクトリに配置する

  • Terraform が参照するだけで、実行はしない静的ファイル(Compute Engine インスタンスに読み込まれた起動スクリプトなど)は、files/ ディレクトリに配置する必要があります。
  • 長い HereDocs は、HCL とは別の外部ファイルに配置します。これらは file() 関数で参照してください。
  • Terraform の templatefile 関数を使用して読み取ったファイルには、ファイル拡張子 .tftpl を使用します。
    • テンプレートは、templates/ ディレクトリに配置する必要があります。

ステートフル リソースを保護する

データベースなどのステートフル リソースの場合は、削除保護が有効になっていることを確認します。次に例を示します。

resource "google_sql_database_instance" "main" {
  name = "primary-instance"
  settings {
    tier = "D0"
  }

  lifecycle {
    prevent_destroy = true
  }
}

組み込みの書式設定を使用する

すべての Terraform ファイルは、terraform fmt の標準に準拠している必要があります。

式の複雑さを制限する

  • 個別に補間された式の複雑さを制限します。1 つの式で多くの関数が必要な場合は、ローカル値を使用して、複数の式に分割することを検討してください。
  • 1 行に複数の 3 項演算を含めないでください。代わりに、複数のローカル値を使用してロジックを構築します。

条件値に count を使用する

条件付きでリソースをインスタンス化するには、count メタ引数を使用します。次に例を示します。

variable "readers" {
  description = "..."
  type        = list
  default     = []
}

resource "resource_type" "reference_name" {
  // Do not create this resource if the list of readers is empty.
  count = length(var.readers) == 0 ? 0 : 1
  ...
}

ユーザー指定の変数を使用してリソースの count 変数を設定する際は注意が必要です。このような変数にリソース属性(project_id など)が指定されていて、そのリソースがまだ存在しない場合、Terraform はプランを生成できません。代わりに、Terraform からエラー value of count cannot be computed が返されます。このような場合は、別の enable_x 変数を使用して条件付きロジックを計算します。

反復されるリソースに対して for_each を使用する

入力リソースに基づいてリソースの複数のコピーを作成する場合は、for_each メタ引数を使用します。

モジュールをレジストリに公開する

再利用可能なモジュール

再利用を目的としたモジュールの場合は、前述のガイドラインに加えて、次のガイドラインに従ってください。

モジュールで必要な API を有効にする

Terraform モジュールは、google_project_service リソースまたは project_services モジュールを使用して、必要なサービスを有効にできます。API の有効化を含めることで、デモが容易になります。

  • API の有効化をモジュールに含める場合は、デフォルトが trueenable_apis 変数を公開することで、API の有効化を無効にする必要があります。
  • API の有効化がモジュールに含まれている場合、API の有効化で disable_services_on_destroyfalse に設定する必要があります。これは、この属性によりモジュールのマルチ インスタンスで問題が発生するためです。

    次に例を示します。

    module "project-services" {
      source  = "terraform-google-modules/project-factory/google//modules/project_services"
      version = "~> 12.0"
    
      project_id  = var.project_id
      enable_apis = var.enable_apis
    
      activate_apis = [
        "compute.googleapis.com",
        "pubsub.googleapis.com",
      ]
      disable_services_on_destroy = false
    }
    

オーナー ファイルを含める

すべての共有モジュールについて、誰がモジュールの責任者かを明らかにする OWNERS ファイルを含めます(GitHub では CODEOWNERS)。pull リクエストがマージされる前に、オーナーが承認する必要があります。

タグ付きバージョンをリリースする

モジュールによっては互換性を損なう変更が必要になる場合があります。ユーザーが特定のバージョンに構成を固定できるよう、その影響をユーザーに通知する必要があります。

新しいバージョンがタグ付けまたはリリースされたときに、共有モジュールが必ず SemVer v2.0.0 に準拠するようにします。

モジュールを参照する場合は、バージョンの制約を使用して、メジャー バージョンに固定します。例:

module "gke" {
  source  = "terraform-google-modules/kubernetes-engine/google"
  version = "~> 20.0"
}

プロバイダやバックエンドを構成しない

共有モジュールでプロバイダやバックエンドを構成することはできません。代わりに、ルート モジュールでプロバイダとバックエンドを構成します。

共有モジュールの場合は、次のように required_providers ブロックでプロバイダの最低必要バージョンを定義します。

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.0.0"
    }
  }
}

動作しないと証明されていない限り、新しいプロバイダ バージョンは動作すると想定します。

ラベルを変数として公開する

モジュールのインターフェースを使用して、リソースのラベル付けを柔軟に行えるようにします。次のように、空のマップのデフォルト値で labels 変数を指定することを検討してください。

variable "labels" {
  description = "A map of labels to apply to contained resources."
  default     = {}
  type        = "map"
}

すべてのリソースの出力を公開する

変数と出力によって、モジュールとリソース間の依存関係を推測できます。出力がないと、ユーザーは Terraform 構成に関連してモジュールを適切に順序付けできません。

共有モジュールで定義されているリソースごとに、そのリソースを参照する出力を少なくとも 1 つ含めます。

複雑なロジックにインライン サブモジュールを使用する

  • インライン モジュールを使用すると、複雑な Terraform モジュールを小さなユニットに整理したり、共通リソースの重複を排除できます。
  • インライン モジュールは modules/$modulename に配置します。
  • インライン モジュールは、共有モジュールのドキュメントで特に明記されていない限り、外部モジュールで使用されないように非公開として扱います。
  • Terraform は、リファクタリングされたリソースを追跡しません。最上位モジュールの複数のリソースをサブモジュールに push した場合、Terraform はリファクタリングされたすべてのリソースの再作成を試みます。この問題を緩和するには、リファクタリング時に moved ブロックを使用します。
  • 内部モジュールによって定義された出力は自動的に公開されません。内部モジュールからの出力を共有するには、それらを再度エクスポートします。

Terraform のルート モジュール

ルート構成(ルート モジュール)は、Terraform CLI を実行する作業ディレクトリです。ルート構成が次の標準(該当する場合は以前の Terraform ガイドライン)に準拠していることを確認します。一般的なガイドラインよりもルート モジュールの明示的な推奨事項が優先されます。

各ルート モジュールのリソース数を最小限に抑える

同じディレクトリと状態に含まれるリソースが多くなり、1 つのルート構成が大きくならないようにすることが重要です。特定のルート構成に含まれるすべてのリソースは、Terraform が実行されるたびに更新されます。1 つの状態に含まれるリソースが多すぎる場合、実行が遅くなる可能性があります。原則として、1 つの状態に含まれるリソースは 100 個未満に制限します(理想的には数十個程度)。

アプリケーションごとに個別のディレクトリを使用する

アプリケーションとプロジェクトを個別に管理するには、各アプリケーションとプロジェクトのリソースをそれぞれの Terraform ディレクトリに配置します。サービスは、特定のアプリケーションや、共有ネットワークなどの共通サービスを表します。特定のサービスのすべての Terraform コードを 1 つのディレクトリ(サブディレクトリを含む)にネストします。

環境固有のサブディレクトリにアプリケーションを分割する

Google Cloud にサービスをデプロイする場合、サービスの Terraform 構成を 2 つの最上位ディレクトリに分割します。1 つがサービスの実際の構成が格納されている modules ディレクトリ、もう 1 つが各環境のルート構成が含まれる environments ディレクトリです。

-- SERVICE-DIRECTORY/
   -- OWNERS
   -- modules/
      -- <service-name>/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README
      -- ...other…
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf

      -- qa/
         -- backend.tf
         -- main.tf

      -- prod/
         -- backend.tf
         -- main.tf

環境ディレクトリを使用する

環境間でコードを共有するには、参照モジュールを使用します。通常、これはサービス用の共有 Terraform の基本構成を含むサービス モジュールになります。サービス モジュールでは、共通入力をハードコードし、環境固有の入力のみを変数にする必要があります。

各環境ディレクトリには、次のファイルが含まれている必要があります。

  • backend.tf ファイル。Terraform のバックエンド状態の場所(通常は Cloud Storage)を宣言します。
  • サービス モジュールをインスタンス化する main.tf ファイル

各環境ディレクトリ(devqaprod)は、デフォルトの Terraform ワークスペースに対応し、そのサービスのバージョンを環境にデプロイします。これらのワークスペースでは、環境固有のリソースが独自のコンテキストに分離されます。デフォルトのワークスペースのみを使用します。

次の理由により、環境内で複数の CLI ワークスペースを使用することはおすすめしません。

  • 各ワークスペースの構成を検査するのが難しい場合があります。
  • 環境を分離する際に共有バックエンドを使用すると単一障害点になるため、複数のワークスペースに単一の共有バックエンドを使用することはおすすめしません。
  • コードを再利用することはできますが、現在のワークスペース変数(terraform.workspace == "foo" ? this : that など)に基づいて切り替える必要があるため、コードが読みにくくなります。

詳しくは以下をご覧ください。

リモート状態から出力を公開する

ルート モジュールからモジュール インスタンスの有用な出力を公開していることを確認してください。

たとえば、次のコード スニペットは、プロジェクト ファクトリ モジュール インスタンスからのプロジェクト ID 出力をルート モジュールの出力として渡します。

# Project root module
terraform {
  backend "gcs" {
    bucket  = "BUCKET"
  }
}

module "project" {
  source  = "terraform-google-modules/project-factory/google"
  ...
}

output "project_id" {
  value       = module.project.project_id
  description = "The ID of the created project"
}

他の Terraform 環境とアプリケーションは、ルート モジュール レベルの出力のみを参照できます。

リモート状態を使用すると、ルート モジュールの出力を参照できます。他の依存アプリによる構成を許可するには、サービスのエンドポイントに関連する情報をリモート状態にエクスポートします。

# Networks root module
data "terraform_remote_state" "network_project" {
  backend = "gcs"

  config = {
    bucket = "BUCKET"
  }
}

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "~> 9.0"

  project_id   = data.terraform_remote_state.network_project.outputs.project_id
  network_name = "vpc-1"
  ...
}

環境ディレクトリから共有サービス モジュールを呼び出すときなど、次のように子モジュール全体を再度エクスポートするほうが良い場合があります。

output "service" {
  value       = module.service
  description = "The service module outputs"
}

マイナー プロバイダのバージョンに固定する

ルート モジュールで、各プロバイダを宣言し、マイナー バージョンに固定します。これにより、固定されたターゲットを維持しながら、新しいパッチリリースに自動アップグレードできます。整合性を保つために、バージョン ファイルに versions.tf という名前を付けます。

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0.0"
    }
  }
}

変数を tfvars ファイルに格納する

ルート モジュールの場合は、.tfvars 変数ファイルを使用して変数を指定します。整合性を保つために、変数ファイルに terraform.tfvars という名前を付けます。

別の var-files または var='key=val' コマンドライン オプションを使用して変数を指定しないでください。コマンドライン オプションはエフェメラルなものであり、簡単に削除できます。デフォルトの変数ファイルを使用すると、予測しやすくなります。

.terraform.lock.hcl ファイルをチェックインする

ルート モジュールの場合、.terraform.lock.hcl 依存関係ロックファイルをソース管理にチェックインする必要があります。これにより、任意の構成に対するプロバイダ選択の変更を追跡および確認できます。

構成間の通信

Terraform の使用時によく発生する問題は、複数の Terraform 構成(別のチーム管理している可能性がある)で横断的に情報を共有する方法に関するものです。一般に、構成間では情報を共有する際に単一の構成ディレクトリに保存する必要はありません(単一のリポジトリに保存する必要もありません)。

Terraform 構成間で情報を共有する場合は、リモート状態を使用して他のルート モジュールを参照することをおすすめします。優先される状態のバックエンドは Cloud Storage または Terraform Enterprise です。

Terraform で管理されていないリソースのクエリには、Google プロバイダのデータソースを使用します。たとえば、デフォルトの Compute Engine サービス アカウントは、データソースを使用して取得できます。別の Terraform 構成で管理されているリソースのクエリに、データソースを使用しないでください。これを行うと、リソース名と構造に暗黙的な依存関係が作成され、通常の Terraform オペレーションで意図しない中断が発生する可能性があります。

Google Cloud リソースの操作

Terraform を使用して Google Cloud リソースをプロビジョニングするためのベスト プラクティスは、Google が管理する Cloud Foundation Toolkit モジュールに統合されています。このセクションでは、これらのベスト プラクティスについて説明します。

仮想マシンイメージをベイクする

通常は、Packer などのツールを使用して仮想マシンイメージをベイクすることをおすすめします。Terraform で必要なのは、事前に作成したイメージを使用してマシンを起動するだけです。

事前にベイクされたイメージが使用できない場合、Terraform は provisioner ブロックを使用して、構成管理ツールに新しい仮想マシンを渡すことができます。この方法はおすすめしません。最後の手段としてのみ使用してください。インスタンスに関連付けられた古い状態をクリーンアップするには、破棄ロジックを必要とするプロビジョナが when = destroyprovisioner ブロックを使用する必要があります。

Terraform は、インスタンス メタデータを使用して VM 構成情報を構成管理に提供する必要があります。

Identity and Access Management を管理する

Terraform と IAM の関連付けをプロビジョニングする場合、いくつかのリソースを使用できます。

  • google_*_iam_policy(例: google_project_iam_policy
  • google_*_iam_binding(例: google_project_iam_binding
  • google_*_iam_member(例: google_project_iam_member

google_*_iam_policygoogle_*_iam_binding は、信頼できる IAM の関連付けを作成します。ここでは、Terraform リソースが、関連するリソースに割り当てることができる権限の唯一の正確な情報源として機能します。

Terraform の外部で権限が変更された場合、Terraform は次回の実行時にすべての権限を上書きし、構成で定義されているポリシーを反映します。これは、特定の Terraform 構成によって完全に管理されているリソースには適していますが、Google Cloud によって自動的に管理されるロールが削除されたり、一部のサービスの機能が停止する可能性があります。

これを防ぐには、google_*_iam_member リソースを直接使用するか、Google の IAM モジュールを使用することをおすすめします。

バージョン管理

他の形式のコードと同様に、インフラストラクチャ コードをバージョン管理に保存することで、履歴を保持し、ロールバックを容易に行うことができます。

デフォルトのブランチ戦略を使用する

デフォルトでは、Terraform コードを含むすべてのリポジトリで次の戦略を使用します。

  • main ブランチはプライマリ開発ブランチであり、承認された最新のコードを表します。main ブランチは保護されています。
  • 開発は、main ブランチから分岐する機能ブランチとバグ修正ブランチで行う。
    • 機能ブランチ feature/$feature_name に名前を付ける。
    • バグ修正ブランチ fix/$bugfix_name に名前を付ける。
  • 機能またはバグ修正が完了したら、そのブランチを pull リクエストで main ブランチにマージする。
  • マージの競合を防ぐには、マージする前にブランチをリベースします。

ルート構成に環境ブランチを使用する

Google Cloud に直接デプロイされるルート構成を含むリポジトリには、安全なロールアウト戦略が必要です。環境ごとに別々のブランチを設定することをおすすめします。したがって、Terraform 構成の変更は、異なるブランチ間で変更をマージすることでプロモートできます。

環境ごとに個別のブランチ

広範な公開設定を許可する

Terraform ソースコードとリポジトリを、エンジニアリング組織全体、インフラストラクチャ オーナー(SRE など)およびインフラストラクチャの関係者(デベロッパーなど)が幅広く閲覧してアクセスできるようにします。これにより、インフラストラクチャの関係者は依存するインフラストラクチャをより深く理解できます。

変更リクエスト プロセスの一環として、インフラストラクチャの関係者にマージ リクエストを送信するように促します。

シークレットを commit しない

Terraform 構成を含め、シークレットをソース管理に commit しないでください。代わりに、Secret Manager などのシステムにアップロードし、データソースを使用して参照します。

このような機密性の高い値は状態ファイルに残り、出力として公開される可能性があるので注意してください。

チームの境界に基づいてリポジトリを整理する

別々のディレクトリを使用してリソース間の論理境界を管理できますが、組織の境界とロジスティクスによってリポジトリ構造が決まります。一般に、「承認要件と管理要件が異なる構成は、異なるソース管理リポジトリに分割する」という設計の基本原則に従ってください。この原則を説明するために、次のようなリポジトリ構成が考えられます。

  • 1 つの中央リポジトリ: このモデルでは、すべての Terraform コードが 1 つのプラットフォーム チームによって一元管理されます。このモデルは、すべてのクラウド管理を専任のインフラストラクチャ チームが担当し、他のチームが要求した変更を承認する場合に最も効果的です。

  • チーム リポジトリ: このモデルでは、各チームが独自の Terraform リポジトリを担当し、所有するインフラストラクチャに関連するすべての管理を行います。たとえば、セキュリティ チームに、すべてのセキュリティ管理を管理するリポジトリがあり、アプリケーション チームごとに、アプリケーションのデプロイと管理を行う独自の Terraform リポジトリがあるとします。

    ほとんどの企業のシナリオでは、チームの境界に従ってリポジトリを整理するのが最適な構造です。

  • 分離されたリポジトリ: このモデルでは、論理的な Terraform コンポーネントがそれぞれ独自のリポジトリに分割されます。たとえば、ネットワーキングには専用のリポジトリがあり、プロジェクトの作成と管理のために個別のプロジェクト ファクトリ リポジトリが存在する場合があります。これは、チーム間で責任が頻繁に変化する、高度に分散された環境で最適に機能します。

リポジトリ構造の例

これらの原則を組み合わせることで、Terraform 構成を異なるリポジトリ タイプに分割できます。

  • 基礎
  • アプリケーションとチーム固有
基盤リポジトリ

フォルダや組織 IAM など、中心的な主要コンポーネントを含む基盤リポジトリ。このリポジトリは、中央のクラウドチームが管理できます。

  • このリポジトリには、各主要コンポーネント(フォルダ、ネットワークなど)のディレクトリを含めます。
  • コンポーネント ディレクトリで、環境ごとに個別のフォルダを指定します(前述のディレクトリ構造のガイダンスを反映します)。

基盤リポジトリ構造

アプリケーションとチーム固有のリポジトリ

アプリケーションとチーム固有のリポジトリをチームごとに個別にデプロイし、アプリケーション固有の Terraform 構成を管理できます。

アプリケーションとチームに固有のリポジトリ構造

運用

インフラストラクチャを安全に保つには、安定した安全なプロセスを用意して Terraform の更新を適用する必要があります。

常に計画してから始める

最初に Terraform の実行プランを作成します。計画を出力ファイルに保存します。インフラストラクチャ オーナーが承認した後、計画を実行します。デベロッパーが変更のローカル プロトタイピングを行う場合でも、計画を作成して、計画を適用する前に追加、変更、破棄されるリソースを確認する必要があります。

自動パイプラインを実装する

実行コンテキストの一貫性を保つために、自動ツールを介して Terraform を実行します。Jenkins などのビルドシステムがすでに使用されており、広く採用されている場合は、それを使用して terraform plan コマンドと terraform apply コマンドを自動的に実行します。使用可能な既存のシステムがない場合は、Cloud Build または Terraform Cloud のいずれかを使用します。

継続的インテグレーションにサービス アカウントの認証情報を使用する

Terraform を CI / CD パイプラインのマシンから実行する場合は、パイプラインを実行しているサービスからサービス アカウントの認証情報を継承する必要があります。CI パイプラインは可能な限り Google Cloud で実行してください。Cloud Build、Google Kubernetes Engine、Compute Engine では、サービス アカウント キーをダウンロードせずに認証情報を挿入できるためです。

Google Cloud の外部で実行されるパイプラインの場合は、Workload Identity 連携を使用して、サービス アカウント キーをダウンロードせずに認証情報を取得します。

既存のリソースのインポートを回避する

可能であれば、(terraform import を使用して)既存のリソースをインポートすることは避けてください。手動で作成したリソースの来歴や構成を完全に把握することは困難です。代わりに Terraform を使用して新しいリソースを作成し、古いリソースを削除してください。

古いリソースを削除するのに手間がかかる場合は、明示的に承認を受けて terraform import コマンドを使用します。リソースを Terraform にインポートした後、Terraform でのみリソースを管理します。

Google では、Google Cloud リソースを Terraform 状態にインポートするために使用できるツールを提供しています。詳細については、Google Cloud リソースを Terraform の状態にインポートするをご覧ください。

Terraform の状態を手動で変更しない

Terraform 状態ファイルは、Terraform 構成と Google Cloud リソース間のマッピングを維持するために重要です。破損があると、インフラストラクチャに大きな問題が発生する可能性があります。Terraform の状態の変更が必要な場合は、terraform state コマンドを使用します。

バージョンの固定状態を定期的に確認する

バージョンを固定すると安定性が保証されますが、バグ修正やその他の改善は構成に組み込まれません。そのため、Terraform、Terraform プロバイダ、モジュールのバージョン固定状態を定期的に確認してください。

このプロセスを自動化するには、Dependabot などのツールを使用します。

ローカルでの実行中にアプリケーションのデフォルト認証情報を使用する

デベロッパーが Terraform 構成をローカルで反復処理する場合、認証のために gcloud auth application-default login を実行して、アプリケーションのデフォルト認証情報を生成する必要があります。サービス アカウント キーはダウンロードしないでください。ダウンロードすると、キーの管理と保護が難しくなります。

Terraform にエイリアスを設定する

ローカルでの開発を容易にするには、コマンドシェル プロファイルにエイリアスを追加します。

  • alias tf="terraform"
  • alias terrafrom="terraform"

セキュリティ

Terraform を使用するには、対象のクラウド インフラストラクチャに対して機密性の高いアクセス権が必要です。これらのセキュリティのベスト プラクティスに従うことで、関連するリスクを最小限に抑え、クラウド全体のセキュリティを改善できます。

リモート状態を使用する

Google Cloud を使用している場合は、Cloud Storage の状態バックエンドを使用することをおすすめします。このアプローチでは、チームとして共同作業できる状態が制限されます。また、状態と潜在的な機密情報をすべてバージョン管理から分離します。

リモート環境に使用されるバケットには、ビルドシステムおよび高い権限を持つ管理者のみがアクセスできるようにします。

開発環境の状態をソース管理に誤って commit しないようにするには、Terraform 状態ファイルに gitignore を使用します。

状態を暗号化する

Google Cloud バケットは保存時に暗号化されますが、顧客指定の暗号鍵を使用して保護レイヤを追加できます。これを行うには、GOOGLE_ENCRYPTION_KEY 環境変数を使用します。状態ファイルに保存されるシークレットはないはずですが、常に追加の防御手段として状態を暗号化してください。

シークレットを状態に保持しない

Terraform には、状態ファイルに平文でシークレット値に保存するリソースやデータ プロバイダが多数あります。可能な限り、状態をシークレットに保存することは避けてください。シークレットを平文で保存するプロバイダの例を次に示します。

機密出力をマークする

手動で機密性の高い値を暗号化する代わりに、Terraform の組み込みの機密状態管理サポートを使用します。機密性の高い値を出力にエクスポートするときは、値を必ず「機密」とマークしてください。

職掌分散を確保する

ユーザーがアクセスできない自動システムから Terraform を実行できない場合は、権限とディレクトリを分離して職掌分散を確保してください。たとえば、ネットワーク プロジェクトは、そのプロジェクトにアクセスが制限されているネットワーク Terraform サービス アカウントまたはユーザーに対応します。

適用前のチェックを実行する

自動化されたパイプラインで Terraform を実行する場合、適用前に gcloud terraform vet などのツールを使用して、ポリシーに対するプランの出力を確認します。これにより、セキュリティ回帰の発生を事前に検出できます。

継続的な監査を実施する

terraform apply コマンドの実行後、自動セキュリティ チェックを実行します。これらのチェックにより、インフラストラクチャが安全な状態からずれないようにすることができます。この種のチェックでは、次のツールが有効です。

テスト

Terraform のモジュールと構成のテストは、アプリケーション コードのテストとは異なるパターンと規則に従う場合があります。アプリケーション コードのテストでは、主にアプリケーション自体のビジネス ロジックをテストしますが、インフラストラクチャ コードを完全にテストするには、本番環境の障害リスクを最小限に抑えるために、実際のクラウド リソースをデプロイする必要があります。Terraform テストを実行する際には、次の点を考慮してください。

  • Terraform テストを実行すると、実際のインフラストラクチャが作成、変更、破棄されるため、テストに時間と費用がかかる可能性があります。
  • エンドツーエンドのアーキテクチャに純粋な単体テストを行うことはできません。最良の方法は、アーキテクチャをモジュールに分割し、個別にテストすることです。このアプローチの利点は、テスト ランタイムの高速化、各テストのコストの削減、制御できない要因によるテスト失敗の可能性の低減により、反復型開発を迅速に行えることです。
  • 可能であれば、状態の再利用は避けてください。他の構成とデータを共有するような構成でテストしている場合でも、各テストを独立させ、テスト間で状態を再利用しないのが理想的です。

最初は低コストのテスト方法を使用する

Terraform のテストには複数の方法があります。次のような方法があります(費用、実行時間、深さの低い順に示します)。

  • 静的分析: コンパイラ、リンター、ドライランなどのツールを使用して、リソースをデプロイせずに構成の構文と構造をテストします。その場合は、terraform validate を使用します。
  • モジュール統合テスト: モジュールが正常に機能することを確認するために、個々のモジュールを単独でテストします。モジュールの統合テストでは、モジュールをテスト環境にデプロイし、想定されるリソースが作成されたことを確認します。次のように、テストの作成を容易にするテスト フレームワークがいくつかあります。
  • エンドツーエンドのテスト: 統合テストのアプローチを環境全体に拡張することで、複数のモジュールが連携して機能することを確認できます。このアプローチでは、アーキテクチャを構成するすべてのモジュールを新しいテスト環境にデプロイします。テスト環境が本番環境とできるだけ似ているのが理想的です。これはコストがかかりますが、変更が本番環境の中断を引き起こさない最も確実な方法です。

小規模で始める

繰り返し行えるようにテストを構成します。まず小規模なテストを実行してから、より複雑なテストを行う、フェイル ファスト アプローチを使用します。

プロジェクト ID とリソース名をランダム化する

名前の競合を避けるため、グローバルに一意のプロジェクト ID と、各プロジェクト内で重複しないリソース名が構成に含まるようにします。これを行うには、リソースに名前空間を使用します。Terraform には、これを行うためのランダム プロバイダが組み込まれています。

テストに別の環境を使用する

テストでは、多くのリソースが作成され、削除されます。リソースのクリーンアップ中に誤って削除されないように、開発環境または本番環境プロジェクトから環境を隔離します。最善のアプローチは、テストごとに新しいプロジェクトまたはフォルダを作成することです。構成ミスを回避するため、テストの実行ごとに個別のサービス アカウントを作成することをおすすめします。

すべてのリソースをクリーンアップする

インフラストラクチャ コードをテストするということは、実際のリソースをデプロイすることを意味します。課金されないようにするには、クリーンアップ手順の実装を検討してください。

特定の構成で管理されているすべてのリモート オブジェクトを破棄するには、terraform destroy コマンドを使用します。一部のテスト フレームワークには、クリーンアップ手順があらかじめ組み込まれています。たとえば、Terratest を使用している場合は、defer terraform.Destroy(t, terraformOptions) をテストに追加します。Kitchen-Terraform を使用している場合は、terraform kitchen delete WORKSPACE_NAME を使用してワークスペースを削除します。

terraform destroy コマンドを実行した後、追加のクリーンアップ手順を実行して、Terraform が破棄できなかったリソースを削除します。これを行うには、テスト実行に使用したプロジェクトを削除するか、project_cleanup モジュールなどのツールを使用します。

テスト ランタイムを最適化する

テストの実行時間を最適化するには、次の方法を使用します。

  • テストを並行して実行する。一部のテスト フレームワークでは、複数の Terraform テストの同時実行がサポートされています。
    • たとえば、Terratest を使用する場合は、テスト関数定義の後に t.Parallel() を追加します。
  • 段階的にテストする。別個にテストできる複数の独立した構成に分離します。このアプローチにより、テストの実施時にすべての段階を経る必要がなくなり、反復開発サイクルを加速化させることができます。
    • たとえば、Kitchen-Terraform では、テストを別々のスイートに分割します。反復処理では、各スイートが個別に実行されます。
    • 同様に、Terratest を使用して、テストの各ステージを stage(t, STAGE_NAME, CORRESPONDING_TESTFUNCTION) でラップします。実行するテストを示す環境変数を設定します。例: SKIPSTAGE_NAME="true"
    • ブループリント テスト フレームワークは、段階的な実行をサポートしています。

次のステップ