🧩

Terraformのmoduleを使ってソースコードの重複をなくすぞ〜

に公開

おつかれさまです。水谷です。
最近Terraformのソースコードを触る機会が増えて、moduleを覚えたのでその備忘録です。

TL;DR

  • 以前はこんな感じで内容がほとんど同じtfファイルを環境ごとに用意していたが、module化することで同一ソースコードを参照できるようになった!
  • moved を使って管理するリソースを引き継ぐことでterraform差分を作らずに移行できた!
before
(repository root)
└── src
    ├── dev
    │   ├── ecs.tf
    │   ├── main.tf
    │   └── sqs.tf
    ├── prod
    │   ├── ecs.tf  # 95%はdev/ecs.tfのコピペ
    │   ├── main.tf
    │   └── sqs.tf  # 完全にdev/sqs.tfのコピペ
    └── stg
        ├── ecs.tf  # 95%はdev/ecs.tfのコピペ
        ├── main.tf
        └── sqs.tf  # 完全にdev/sqs.tfのコピペ
after
(repository root)
└── src
    ├── _modules
    │   └── my_service  # 各環境のmain.tfからこいつが参照される
    │       ├── ecs.tf
    │       ├── sqs.tf
    │       └── variables.tf
    ├── dev
    │   └── main.tf
    ├── prod
    │   └── main.tf
    └── stg
        └── main.tf

前提

上記beforeのようにmoduleを使わない状態でのTerraformの環境は構築済みの前提で進めます。
そのため、backend.tfなどは省略しています。

環境

今回試した環境は次のとおりです。

  • Terraform 1.12.1
  • AWS

モジュールの定義

モジュール群を収める適当なディレクトリ(今回は _modules)の下にモジュール名ディレクトリ(今回は my_service)を作成しておきます。
この my_service が1モジュールの単位になります。

resourceの定義

リソースの記述方法は環境ごとにtfファイルを作成したときと基本的には同じです。resource ブロックを使って記述します。

src/_modules/my_service/ecs.tf
resource "aws_ecs_cluster" "my_service_frontend" {
  name = "my-service-frontend"
  ...
}

variableの定義:モジュールが受け取る変数

各環境ごとに同じリソースを作成するといっても、命名に環境名を入れるとか環境ごとにスペックを少し変えるとか、細かな差異はあったりするものです。
そういった値は variable ブロックを使って定義します。[1]

src/_modules/my_service/variables.tf
# プリミティブな値を1つ受け取るパターン
variable "env" {
  type        = string
  description = "env name (ex. dev)"
}

# 複数の値をオブジェクトとして一括で受け取るパターン
variable "api_gateway_config" {
  type = object({
    allowed_ip_list = list(string)
    deployment_id = string
  })
}

定義した変数は var.<変数名> という形式でモジュール内のresource定義で参照できます。

src/_modules/my_service/ecs.tf
resource "aws_ecs_cluster" "my_service_frontend" {
  name = "my-service-frontend-${var.env}"
  ...
}

モジュール使用の定義

モジュールの定義ができたら、各環境でそれを使用する定義を記述します。
各環境のmain.tfに module ブロックを作成してモジュールを使用することを宣言し、variablesとして渡す値を指定します。[2]

src/dev/main.tf
module "my_service" {
  env = "dev"
  api_gateway_config = {
    allowed_ip_list = ["aa", "bb", "cc"]
    deployment_id   = "d-1234567890"
  }
}

これでモジュールディレクトリ内にあるresourceが作成されるようになります👏👏👏

Tips

ここからはモジュールを使用する際に必須ではないが何かと便利なテクニックを紹介します。

1. variableの指定を任意にする

基本的には環境差異がある分はvariableとして各環境で管理すべきですが、いくつもある環境の中で1つだけ違うような場合にデフォルト値を使いたくなることもあると思います。

そのような場合はデフォルト値を定義できます。

variables.tf
  variable "env" {
    type        = string
+   nullable    = true
+   default     = "xxx"
  }

  # object内の場合はoptional関数を使って第2引数にデフォルト値を指定する
  variable "api_gateway_config" {
    type = object({
      allowed_ip_list = list(string)
-     deployment_id = string
+     deployment_id = optional(string, "d-default")
    })
  }

または、デフォルト値を定義せず利用時に coalesce 関数を使うこともできます。
プリミティブな固定値ではなく別のリソースのidやarnを使う場合はこちらの記述が便利です。

variables.tf
  variable "env" {
    type        = string
    nullable    = true
-   default     = "xxx"
  }

  variable "api_gateway_config" {
    type = object({
      allowed_ip_list = list(string)
-     deployment_id = optional(string, "d-default")
+     deployment_id = optional(string)
    })
  }
src/_modules/my_service/ecs.tf
resource "aws_ecs_cluster" "my_service_frontend" {
  name = "my-service-frontend-${coalesce(var.env, "xxx")}"
  ...
}
api_gateway.tf
resource "aws_api_gateway_stage" "my_service_api_v1" {
  // TODO: 暫定対応
  deployment_id = coalesce(var.api_gateway_config.deployment_id, aws_api_gateway_deployment.my_service_api_deployment.id)
  ...
}

2. モジュール側で作成したリソースの値を使う

モジュール側で作成したリソースのidやarnを他のモジュールや環境独自のリソースで参照することもできます。

まず、モジュール側にoutputs.tfを作成し、外に出す値とその名前を output ブロックで定義します。

src/_modules/my_service/outputs.tf
output "my_service_sqs_id" {
  value = aws_sqs_queue.my_service.id
}

使用する側は module.<モジュール名>.<名前> で参照できます。

src/dev/ecs-own.tf
resource "aws_ecs_task_definition" "own_service_backend" {
  container_definitions = jsonencode([{
    environment = [
      {
        name  = "SQS_URL",
        value = module.my_service.my_service_sqs_id
      }
    ]
  }])
}

3. 既存のTerraform管理リソースをmoduleに引き継ぐ

すでにTerraformを使用して各環境にリソースを作っていた場合、単にモジュールを作成して環境ごとに作成していたファイルを消すだけだと、既存リソースは削除・モジュール分は新規作成になってしまいます。
これを避けてリソースの管理を引き継ぐ場合は moved ブロックを作成してどのリソースをどのリソースに引き継ぐかを定義します。

src/dev/main.tf
moved {
  # 移管元:<src/dev配下でのリソース名>
  from = aws_ecs_cluster.my_service_frontend
  # 移管先:module.<モジュール名>.<モジュール内でのリソース名>
  to = module.my_service.aws_ecs_cluster.my_service_frontend
}

これでTerraformの状態管理にリソース管理の移譲が記録され、差分が出なくなります。[3]

脚注
  1. 変数の内容はダミーデータのためツッコミなしでお願いします😅 ↩︎

  2. moduleブロックで定義する名前は必ずしもモジュールのディレクトリ名と同じでなくても大丈夫です。が、同じにしておいた方が無難だと思います。 ↩︎

  3. 従来記述していた設定値とモジュールに記述されていた設定値が異なる場合、その差分は出ます。変更を許容できるかvariableなどを使って差分をなくすのか検討の上でapplyしてください。 ↩︎

レバテック開発部

Discussion