Site cover image

Site icon imageViserhaut Tech Blog

This blog is a blog that records the daily learning of an infrastructure engineer. It aims to "Viser Haut" or strive for the highest.

Terraform のベストプラクティスを活用したインフラ管理ガイド

概要

本記事では、Terraform を使用したインフラストラクチャ管理におけるベストプラクティスを紹介します。効率的で安全なインフラストラクチャの構築と管理方法について解説し、実践的な例を交えながら説明します。

この記事で伝えたいこと

  • Terraform を使用する際の効果的なコーディング手法
  • 複数環境の管理とセキュリティの確保
  • テストとポリシー適用の重要性

解決したい課題

多くの組織では、インフラストラクチャの管理が複雑化し、以下のような問題に直面しています:

  • コードの一貫性の欠如
  • 環境間の設定の不一致
  • セキュリティリスクの増大
  • インフラストラクチャの変更管理の困難さ

課題の原因

これらの課題は主に以下の要因から生じています:

  • 標準化されたコーディング規約の欠如
  • 複数環境の管理における一貫性の不足
  • セキュリティベストプラクティスの不徹底
  • 適切なテストと検証プロセスの欠如

課題を解決する技術、手法

技術、手法の概要

Terraform のベストプラクティスを適用することで、これらの課題に効果的に対処できます。主な技術と手法は以下の通りです:

Code style
  • コードをバージョン管理にコミットする前にterraform fmtterraform validateを実行する。
  • TFLint のような Linter を使用して、組織独自のコーディングのベストプラクティスを実施する。
  • コメントには # を使う。
  • リソース名には名詞を使用し、リソースタイプを名前に含めない。
  • 名前の複数の単語を区切るにはアンダースコアを使用する。リソース定義では、リソース・タイプと名前を二重引用符で囲む。
  • 参照するリソースの後に依存するリソースを定義する。
  • すべての変数に型と説明を含める。
    variable "instance_type" {
      type        = string
      description = "The type of instance to launch, such as t2.micro or t3.large."
      default     = "t2.micro"
    }
  • すべての出力に説明を含める。
    output "instance_ip" {
      value       = aws_instance.web.private_ip
      description = "The private IP address of the web server instance."
    }
  • 変数やローカル値の使いすぎに注意する。
  • 常にデフォルトのプロバイダ設定を含める。
  • countfor_eachは控えめに使用する。

Code formatting
  • terraform fmt コマンドで下記の推奨のサブセットにフォーマットできる。
  • ネストするごとにスペース 2 つあける。
  • 単一行の値を持つ複数の引数が同じネストレベルで連続した行に現れる場合、それらの等号を揃える:
  • ブロック本体の中に引数とブロックの両方が一緒に現れる場合は、引数をすべて一番上にまとめて配置し、その下にネストされたブロックを配置する。引数とブロックを区切るには空行を 1 行使う。
    resource "aws_instance" "web" {
      ami           = "ami-123456"
      instance_type = "t2.micro"
    
      network_interface {
        device_index = 0
        network_id   = "subnet-abc123"
      }
    }
  • ブロック内の引数の論理的なグループを区切るには空行を使う。
    resource "aws_instance" "web" {
      ami           = "ami-123456"
      instance_type = "t2.micro"
    
      tags = {
        Name = "web-server"
      }
    }
  • 引数と "メタ引数"(Terraform 言語セマンティクスで定義)の両方を含むブロックでは、メタ引数を最初に列挙し、空行 1 行で他の引数と区切る。メタ引数ブロックは最後に配置し、他のブロックとは空白行で区切る。
    resource "aws_instance" "web" {
      count = 2
    
      ami           = "ami-123456"
      instance_type = "t2.micro"
    
      depends_on = [aws_vpc.main]
    }
  • 最上位のブロックは常に空白行 1 行で区切らなければならない。ネストされたブロックも空白行で区切らなければならないが、同じ型の関連ブロックをグループ化する場合は例外。
  • ブロックタイプがセマンティクスで定義されてファミリーを形成している場合を除き、同じタイプの複数のブロックを異なるタイプの他のブロックとグループ化することは避ける。(例: aws_instance の root_block_device、ebs_block_device、ephemeral_block_device は、AWS ブロックデバイスを記述するブロックタイプのファミリーを形成しているため、グループ化して混在させることができる)。

Code validation

File names
  • 以下のファイル命名規則を推奨する
  • バックエンドの設定を含む backend.tf ファイル
  • すべてのリソースとデータソースブロックを含むmain.tfファイル
    • ただしコードベースが大きくなった場合は論理的なグループにファイルを分けて整理することを推奨する。
  • outputs.tf ファイル:すべての出力ブロックをアルファベット順に格納する。
  • providers.tf すべてのプロバイダブロックと設定を含むファイル
  • terraform.tf ファイル。required_version と required_providers を定義する terraform ブロックを含む。
  • variables.tfファイル。すべての変数ブロックをアルファベット順に格納する。
  • locals.tf ファイル。詳細は Local values を参照
  • 設定のオーバーライド定義を含む override.tf ファイル。Terraform はこのファイルと _override.tfで終わるすべてのファイルを最後にロードする。これらのオーバーライドはコードの推論を難しくするので、控えめに使用し、元のリソース定義にコメントを追加する。詳細は Override Files のドキュメントを参照

Linting and static code analysis
  • Terraform には組み込みの Linter が無いが、 TFLint のようなサードパーティツールを頼ることができる。
  • 自分でルールを書くこともできる。

Comments
  • 必要なときだけコメントを使う。
  • 1 行でも複数行でも # を使う。

Resource naming
  • 一貫性と読みやすさのために説明的な名詞を使用する。
  • アンダースコアで単語を区切る。
  • リソース識別子にはリソースタイプを含めない。
  • リソースタイプと名前は二重引用符で囲む。

Resource order
  • コード内のリソースやデータソースの順番は Terraform のビルド方法に影響しないので、読みやすいようにリソースを整理する。
  • データソースを参照するリソースの前にデータソースを定義する。
  • リソースパラメータは以下の順で統一する。
    1. 存在する場合は、countまたは for_eachメタ引数
    2. リソース固有の非ブロックパラメータ
    3. リソース固有のブロックパラメータ
    4. 必要であれば、ライフサイクルブロック
    5. 必要であれば depends_on パラメータ

Variables
  • 変数は使いすぎない。
  • すべての変数に型と説明を定義する。
  • 変数がオプションの場合は、妥当なデフォルト値を定義する。
  • パスワードや秘密鍵のような機密性の高い変数には、sensitive パラメータを true に設定します。詳細は secrets management を参照する。
    variable "db_password" {
      type        = string
      description = "The password for the database."
      sensitive   = true
    }
  • type validation に加えて、input variable validation を使用して変数値に対する追加のルールを作成する。
    • 次の順序を推奨する。
      1. type
      2. description
      3. Default
      4. Sensitive
      5. バリデーションブロック

Outputs
  • Output パラメータは次の順で出力する。
    • Description
    • Value
    • Sensitive

Local Values
  • ローカル値は 2 つの場所のどちらかで定義する
    1. 複数のファイルでローカル値を参照する場合は、locals.tf という名前のファイルに定義する。
    2. ローカル値が特定のファイルに限定されている場合は、そのファイルの先頭に定義する。

Provider aliasing
  • 常にデフォルトのプロバイダ構成を含め、すべてのプロバイダを同じファイルで定義する。
  • プロバイダの複数のインスタンスを定義する場合は、デフォルトを最初に定義する。
  • デフォルト以外のプロバイダの場合は、プロバイダ・ブロックの最初のパラメータとしてエイリアスを定義する。

Dynamic resource count
  • リソースがほぼ同じなら count を、整数から導出できない個別の値を必要とする場合は for_each を使用する。
    # countの例
    resource "aws_instance" "web" {
      count         = 3
      instance_type = "t2.micro"
    }
    
    # for_eachの例
    locals {
      instance_names = ["app1", "app2", "app3"]
    }
    
    resource "aws_instance" "web" {
      for_each       = toset(local.instance_names)
      instance_type  = "t2.micro"
      tags = {
        Name = each.key
      }
    }

.gitignore
  • 以下のファイルはコミットしない。
    • terraform.tfstate.* ファイルを含む、 terraform.tfstate ファイル
    • .terraform.tfstate.lock.info ファイル。terraform apply コマンドを実行すると自動的に作成・削除される。
    • .terraform ディレクトリ。Terraform がプロバイダや子モジュールをダウンロードする場所。terraform plan を実行する際に-out フラットを含めると作成される保存されたプランファイル
    • 機密情報を含む .tfvars ファイル
  • 以下は常にコミットする。
    • すべての Terraform コードファイル
    • .terraform.lock.hcl 依存ロックファイル
    • .gitignore ファイル
    • コード、入力変数、出力を記述した README.md

Workflow style

このセクションでは、予測可能でセキュアな Terraform ワークフローを可能にする、以下のような標準についてレビューする:

  • Terraform、プロバイダ、モジュールのバージョンを固定する。
  • Terraform Cloud レジストリを使用する場合、 terraform-<PROVIDER>-<NAME> の 3 つの名前を使ってモジュールリポジトリに名前を付ける。
  • ローカルモジュールは ./modules/<module_name> に保存する。
  • tfe_outputs データソースまたはプロバイダ固有のデータソースを使用して、2 つのステートファイル間でステートを共有します。
  • 動的なプロバイダー資格情報または HashiCorp Vault のようなシークレットマネージャーを使用して資格情報を保護する。
  • モジュールのテストを書く。
  • Terraform Cloud のポリシーエンフォースメントを使ってインフラ運用のガードレールを設定する。

Version pinning
  • プロバイダやモジュールのアップグレードによってインフラストラクチャに意図しない変更が加えられるのを防ぐには、バージョンの固定を使用する。
  • また、terraform ブロックのrequired_versionを使用して、Terraform バイナリの最低必要バージョンを設定することを推奨する。

Module repository names
  • モジュールリポジトリはterraform-<PROVIDER>-<NAME>という 3 つの部分からなる名前を使用しなければならない。<NAME>はモジュールが管理するインフラの種類を表し、<PROVIDER>はモジュールが使用する主なプロバイダ。<NAME>セグメントにはさらにハイフンを入れることができ、例えばterraform-google-vaultやterraform-aws-ec2-instanceのようにすることができる。

Module structure

Local modules
  • ローカルモジュールはリモートモジュールレジストリではなくローカルディスクから取得する。
  • Terraform Cloud のプライベートモジュールレジストリのようなモジュールレジストリにモジールを公開することを推奨する。
  • ./modules/<module_name>ディレクトリに子モジュールを定義することを推奨する。

Repository structure
  • 実際のインフラ構成はモジュールコードとは別に保存することを推奨する。
    /modules
      └── network
          └── main.tf
    /environments
      ├── production
      │   └── main.tf
      └── staging
          └── main.tf
  • 各モジュールは個別のリポジトリに保存する。こうすることで、各モジュールを独立してバージョン管理でき、Terraform のプライベートレジストリでモジュールを公開しやすくなる。
    GitHub Repositories:
    ├── network-module
    ├── compute-module
    └── database-module
  • 論理的に関連するリソースをグループ化したリポジトリでインフラストラクチャ構成を整理しする。例えば、コンピュート、ネットワーク、データベースのリソースを必要とする Web アプリケーションを 1 つのリポジトリで管理する。リソースをグループに分けることで、操作の障害によって影響を受ける可能性のあるリソースの数を制限できる。
    /web-app-infrastructure
      ├── network.tf
      ├── compute.tf
      └── database.tf
  • あるいはすべてのモジュールとインフラ構成を 1 つのモノリシックリポジトリ(monorepo)にグループ化する。例えば、monorepo はインフラスタックの各コンポーネントのローカルモジュールのコレクションを定義し、ルートモジュールにデプロイすることができる。
  • モノリシック・リポジトリは CI/CD の自動化を複雑にする可能性がある。コード変更はリポジトリ全体を操作するデプロイメントをトリガーするので、ワークフローは変更されたディレクトリだけをターゲットにしなければならない。また、リポジトリにアクセスできる人なら誰でもリポジトリ内のファイルを変更できるため、きめ細かなアクセス制御ができなくなる。
    /infra-monorepo
      ├── modules
      │   ├── network
      │   ├── compute
      │   └── database
      └── environments
          ├── production
          └── staging

Branching strategy
  • Terraform コードを共同開発するときは GitHub flow に従うことを推奨する。

Multiple environments
  • リポジトリの main ブランチをすべての環境の真実のソースとすることを推奨する。
    • mainブランチ: 本番環境の設定が常に最新で正しいものとする。
    • featureブランチ: 開発やテストに使用し、mainへマージされる前にレビュー。
  • Terraform Cloud と Terraform Enterprise のユーザーには、環境ごとに別々のワークスペースを使うことを推奨する。詳細は Terraform workspace and project best practicesへ。
  • Terraform Cloud や Terraform Enterprise を使わない場合は、モジュールを使って設定をカプセル化し、環境ごとにディレクトリを使い、それぞれが個別のステートファイルを持つようにすることを推奨する。

State sharing
  • State には機密情報が含まれているため、可能な限り完全な State ファイルの共有は避ける。

Secrets management
  • リモートステートストレージを設定しない場合、Terraform CLI はローカルディスクにステート全体をプレーンテキストで保存する。ステートにはパスワードや秘密鍵などの機密データが含まれる可能性がある。Terraform Cloud と Terraform Enterprise は HashiCorp Vault を通して状態の暗号化を提供する。
    terraform {
      backend "s3" {
        bucket = "my-terraform-state"
        key    = "path/to/my/key"
        region = "us-west-2"
      }
    }
  • Terraform Cloud または Terraform Enterprise を使用する場合は、以下を推奨
    • Terraform Enterprise を使用する場合は、local_exec プロビジョナーまたは外部データソースの使用を防ぐために Sentinel ポリシーを定義し、実施する。
  • Terraform Cloud または Terraform Enterprise を使用する場合、動的なプロバイダー認証情報を使用して、長期間の静的な認証情報の使用を避ける。
  • Terraform Community Edition を使用する場合は、以下を推奨
    • プロバイダ固有の環境変数を使ってプロバイダの認証情報を設定する。
    • Terraform Vault プロバイダを使って、HashiCorp Vault のようなシークレット管理システムからシークレットにアクセスする。Terraform はこれらの値をステートファイルにプレーンテキストで書き込むことに注意する。
  • カスタム CI/CD パイプラインを使用する場合は、機密値を管理するための CI/CD ツールのベストプラクティスを確認する。

Integration and unit testing

Policy
  • ポリシーは Terraform Cloud が Terraform の実行に対して強制するルール。ポリシーを使って、Terraform プランが組織のベストプラクティスに準拠しているかどうかを検証できる。例えば、以下のようなポリシーを書くことができる:
    • Web インスタンスのサイズを制限
    • 必要なリソースタグをチェック
    • 金曜日のデプロイをブロック
    • セキュリティ設定とコスト管理の強制
  • ポリシーは Terraform コードとは別の VCS リポジトリに保存することを推奨する。
  • 詳細は policy enforcement documentation や enforce policy with Sential や detect infrastructure drift and enforce OPA policies を参照する。

技術、手法の効果

これらの技術や手法を適用することで、以下の効果が期待できます:

  • コードの一貫性と可読性の向上
  • 環境間の設定の一貫性の確保
  • セキュリティリスクの低減
  • インフラストラクチャの変更管理の効率化
  • バグの早期発見と品質向上

これらの点に注意しながら、組織の規模や要件に応じてTerraformのベストプラクティスを適切に採用することで、効率的で安全なインフラストラクチャ管理を実現できます。