🦴

Terraform プロジェクトテンプレート案

2024/08/18に公開

https://github.com/ikedam/terraform-project-template

概要

  • よく使っている Terraform プロジェクトの構成をテンプレート化しました。
    • 毎回スクラッチから作るのが面倒くさくなったので、自分用コピペ元を作るというのがモチベーション。
    • Terraform テンプレートの「テンプレート」と用語が被っているので分かりづらい。
  • こんな構成:
    • docker を使用する実行環境
    • ルートモジュールの切り替えによる環境切り替え
    • make によるタスク実行
    • tflint によるスタイルチェック
  • 今後の運用通してちょくちょく更新していくつもり。

更新履歴

  • 2024/8/18 初版

docker を使用する実行環境

以下の理由から Terraform の実行には Docker を使用しています:

  • プロジェクトによって使用している Terraform のバージョンがまちまちで、Docker を使うことでバージョンを合わせる手間が省ける。
    • 特に一度安定状態に入ったプロジェクトは Terraform のバージョンを変えるのは単にリスクにしかならないので、プロジェクト間でバージョンが食い違うのは起きうる状況。
  • チームメンバーの環境構築の手間を省くことができ、Docker さえインストールされていれば実行できることが保証されているので管理・運用が簡単。

考慮事項は以下の通り:

  • 認証情報を Terraform のコンテナーに引き渡すための設定が必要です。AWS、 GCP での詳細はこちらの記事をご参照ください: ツール実行のための docker compose
  • docker プロバイダーを使って terraform apply 時にコンテナーイメージの作成も行う場合は Docker out of Docker の設定が必要です。詳細はこちらの記事をご参照ください: Terraformでコンテナイメージのビルドからデプロイまでを行う(AWS編)
  • CI/CD を構築する場合、 CI/CD ジョブの実行環境(ランナー)自体がコンテナーであることが多いので、 ランナーに Docker in Docker のセットアップを行う必要があります。
    • ただ、一度ランナーを Docker in Docker にするとそれ以降ランナーの環境変数が一切要らなくなる(プロジェクト側で適切な Docker イメージだけ用意すれば良くなる)ので、 CI/CD 環境を Docker in Docker で統一するのはオススメ。

ルートモジュールの切り替えによる環境切り替え

Terraform の複数環境への適用をルートモジュールの切り替えによって行います。

他の方法との比較については Terraform の複数環境運用の比較 をご参照ください。

総合的にはパラメーターファイルでの切り替えのほうが有利なのですが、以下の理由から多少不便なルートモジュールの切り替えを採用しています。

  • Terraform テンプレートの開発者がローカルで terraform planterraform apply を実行する想定がある。
    • CI/CD を用意しない小規模なプロジェクトでの運用
    • Terraform テンプレートの動作確認
  • ローカルで実行する場合があるため、適用先の環境と適用パラメーターが食い違う事故が起きる余地を完全に排除したい。

make によるタスク実行

各種コマンドのショートカットに make / Makefile を使用しています。
これによって、 Terraform 実行時の -chdir= の指定などを隠蔽します。

make の採用理由

make は本来タスクランナー用途のツールではないこともあり正直なところ使いづらいのですが、「多くの環境に最初からインストールされていることを期待できる」という恩恵がとても大きいので採用しています。

Windows ではインストールされていることが期待できない / インストールされていても sh との互換性や POSIX 系のツールがないのでほとんど期待通りに動作しないといった問題があります。
Windows では WSL2 環境を使用するのが良いでしょう。

make のデフォルトターゲットとヘルプ出力

Makefile のデフォルトターゲットはヘルプの出力にするのがオススメです:

$ make
set ENV variable and call targets:

        help                 Show target helps
        init                 run terraform init
        lint                 lint terraform files
        format               format terraform files
        lock                 create/update .terraform.lock.hcl files for all environments
        plan                 run terraform plan
        apply                run terraform apply
        output-%             run terraform output. Don't need `main.` prefix.
$

make 標準の機能ではなく、自分で help ターゲットを書く必要があります。
これが標準というやり方はないので毎回 Google 検索で 「makefile help」とかで検索しています。
今回は Makefileで開発効率をあげよう!#help のコードを参考に(というかコピペ)させていただきました。

make での可変パラメーターの利用

make の難点として、可変パラメーターを使えない点があります。

terraform output を make を使って簡略化しようとすると指定する output 名が可変になるため make では扱いづらいタスクになります。

これについては Makefile で第2引数を使う方法 を参考にターゲット名の末尾を可変にします
(説明のため実行するコマンド内容自体は簡略化しています):

.PHONY: output-%
output-%:
	@$(eval output := ${@:output-%=%})
	terraform -chdir="env/$(ENV)" output -raw "$(output)"

スタイルチェック・バリデーション

make lint で以下を行います:

terraform validate

terraform validate によって、静的解析による構文エラー、参照エラーなどの検知が行えます。

難点として、事前に terraform init を実行してモジュールをインストールしておく必要があります。
terraform init はステートファイルの初期化も行うため、リモートにあるステートファイルの参照のためにクラウドサービスの認証情報が必要になってしまいます。
本来 terraform validate 自体は静的解析なので認証情報不要で実行できるのに、 terraform init のために認証情報が必要になり全体として認証情報が必要になる…という運用上の不便があります。

terraform init 時に代わりに

terraform init -backend=false

-backend=false を指定することでステートファイルの初期化を行わずにモジュールのインストールだけを行うことができ、認証情報の問題は回避可能です。
一方で -backend=false の有無を制御するのが手間なので、この回避を自然に組み込むのが難しい…というのが課題事項です。

tflint の導入

tflint は Terraform テンプレートの各種スタイルチェックを行います。また、AWS や GCP などのプロバイダー向けのプラグインも提供されています。

tflint 導入のアウトラインは以下のとおりです:

  • .tflint.hcl ファイルを作成する。
  • TFLINT_PLUGIN_DIR 環境変数を設定し、 tflint --init を実行する。
    • プラグインをインストールする。
    • コンテナーイメージだと /root/.tflint.d 以下にインストールされてしまい、キャッシュが効かなくなるため、 TFLINT_PLUGIN_DIR がキャッシュが効くような場所(リポジトリー内など)を指すようにする。
      • それに伴い .gitignore.tflint.d を追加するなどの対処が必要。
    • CI/CD で使用する場合このプラグインのインストールが問題を起こすので、後述の通り対応の検討が必要。
  • TFLINT_CONFIG_FILE 環境変数を設定し、 tflint --recursive を実行する。
    • 本当はルートモジュールで tflint を実行すれば配下のモジュールにも tflint を適用してくれるはずなのだけれども、実際にはうまく動かない。
      • 原因を調べておらず、単に設定を間違っている可能性もある。
    • このため、代わりにリポジトリールートで --recursive を指定することで全モジュールを tflint の適用対象にする。
    • --recursive を使用すると各モジュールで適用される tflint の設定ファイルがデフォルトで「モジュールのディレクトリーにある .tflint.hcl を参照する」という動作になる。全部のモジュールに .tflint.hcl を設置するのは現実的でないので、 TFLINT_CONFIG_FILE 環境変数でルートの .tflint.hcl を参照するように設定する。

tflint と Ratelimit 問題

tflint --init 時の挙動は以下のようになっています:

  • 前提: プラグインの設定の source には github.com 上のリポジトリーが指定されている。
  • GitHub API を使って、リポジトリーとバージョンからアセット(つまりプラグインのバイナリー本体)の URL を取得する。
  • プラグインのバイナリーをダウンロードする。

この「GitHub API を使って ~ URL を取得する」の部分に難があり、 GitHub 側での Ratelimit が適用されてしまいます。
このため、以下の環境下だと Ratelimit に引っかかる事象が発生します:

  • CI/CD のランナーに固定 IP を使用している。
    • 業務環境だとセキュリティのために IP でのアクセス制限をしているケースはそこそこ多いと思う。
  • CI/CD でクリーンアップを行っている。
    • 普通はクリーンアップを行うので、ほとんどのケースでこれに該当。

tflint でのドキュメントでの言及: Configuring Plugins#Avoiding rate limiting

プラグインがダウンロード済みであれば再ダウンロードはしないので、プラグインディレクトリーをキャッシュすることで問題を回避できます。GitHub Actions Workflow での回避方法については公式ドキュメントでも案内されています: Setup TFLint Action#Usage

一方で、ワークフローが連続して失敗する状況だと GitHub Actions のキャッシュが有効に働かないケースはしばしばありますし、GitHub Actions Workflow 以外の CI/CD 環境を利用している場合には環境に合わせたキャッシュ方法を調べないといけなくて導入のハードルになります。GitHub のトークンによる ratelimit の向上も案内されていますが、根本解決にならないのと、ユーザーアカウント管理やシークレット管理の必要があるので導入はそれなりに面倒くさく、あまり有用な解決策になりません。

このため以下のような対処も検討が必要です:

  • CI では tflint を無効化する。
    • 正直 CI の意味が薄れるけれど。
  • プラグインを使用しない。
    • 体感的にプラグインが役立ったことがあまりないので、この選択肢はそれなりにアリ。

運用

Terraform の各コマンドへのオプション指定

CI/CD で terraform apply を実行する場合、 -auto-approve オプションの指定が必要になります。make ではコマンドへの追加引数を指定する方法がないので、このオプションの有無を変更するのは難関です。

Terraform では TF_CLI_ARGS という環境変数で追加引数を渡すことができるため、 CI/CD やスクリプトの処理ではこれを使ってオプションを指定します:

TF_CLI_ARGS="-auto-approve" make apply

ロックファイル

Terraform には .terraform.lock.hcl というファイルで、使用するプロバイダーのバージョンを固定する機構があります。一般の依存関係管理機構のロックファイルと同様、思わぬバージョンの変更によるデグレを避けることができます。

一方でこのロックファイルの扱いは面倒です:

  • terraform init などを実行しないとファイルが生成・更新されない。
    • このためロックファイルを適切にリポジトリーにコミットするためにはローカルで terraform init などの実行が必要。
    • ローカルに terraform の実行環境が必要になる。
  • ルートモジュールの切り替えによる環境切り替えの場合、環境ごとにロックファイルが作成される。
    • このため、いちいち dev, stg, prd それぞれでロックファイルの作成が必要。
    • 特に CI/CD を導入している場合、基本的にローカルで prd 環境を扱うことはなく、更新のタイミングがない。

今回は以下の方法でこれらの問題を解決しています:

  • ローカルに terraform の実行環境が必要になる。 → Docker を使用しており、環境の構築が簡単。
  • 環境ごとにロックファイルが作成される / 更新のタイミングがない → lock ファイルの生成・更新を行う terraform providers lock を全環境に対してい実行する make lock というターゲットを作成する。

また、 CI/CD では terraform init 後にリポジトリーに差分があったら CI/CD を失敗させるような措置を入れておくとよいでしょう。

バージョンの固定については、以下も念頭に置いて運用が必要です:

  • 外部モジュールのバージョンは固定されないことに注意が必要。

    • ロックファイルが固定するのはプロバイダーだけ。モジュールについては提案は出ているが、現在のところロック対象になっていない: Lock module versions, like providers, in .terraform.lock.hcl #31301

    • 外部モジュールについてはバージョンを範囲で指定せず、マイクロバージョンまで含めたバージョン一致で指定するのが安全です。

      module "vpc" {
        source = "terraform-aws-modules/vpc/aws"
        # 範囲指定でのバージョン指定は使用しない
        # version = "~> 5.13.0"
        version = "5.13.0"
      }
      
      • tflintterraform_module_version でオプションで強制することも可能です。
      • 正直なところ外部モジュールを一切使わない運用も検討の余地があると思う。
  • ロックファイルをリポジトリー管理しない場合、代わりに以下の運用を検討する:

    • required_providers に漏れがないようにする。これは tflint でチェックが可能。

    • プロバイダーのバージョンを範囲で指定せず、マイクロバージョンを含めたバージョン一致で指定する:

      terraform {
        required_providers {
          aws = {
            source = "hashicorp/aws"
            # 範囲指定でのバージョン指定は使用しない
            # version = "~> 5.63.0"
            version = "5.63.0"
          }
        }
      }
      
      • tflint でこれを強制する方法はない。

プロバイダーのバージョンの指定

Terraform ではモジュールごとに毎回バージョン指定が必要になるのでかなり不便です。
例えば以下のような指定を各モジュールに含める必要があります:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.63.0"
    }
  }
}

モジュールのバージョンを上げる場合、全モジュールに対して変更をいれる必要があり、かなり不便があります。

version フィールドはオプションなので、ルートモジュール以外ではバージョンを指定しないことも検討の余地があります (Provider Requirements#Version Constraints):

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

ドキュメントでは version の指定を行うことを推奨していたり、tflint の推奨設定では version の指定がないとエラーになったりと、バージョンの指定を外すことにリスクがありそうな雰囲気がありますが、これはモジュール機能を「Terraform テンプレートを再配布する機能」として見た場合の話ではないかと考えています。
多くのプロジェクトでは Terraform テンプレートをディレクトリーで構造化したいのだが、それができるのがモジュール機能しかないので仕方なく使用している…という位置づけであり、同一リポジトリーとして運用する以上、バージョンをいちいち指定する必要性は薄く感じます。

一方でルートモジュールではバージョンの指定は漏れないようにしたいので、 tflint にルートモジュールでのみバージョンの指定をチェックして、それ以外では無視する…としたいのですが、それが行う機能がないというのが現状となります。

Discussion