Terraformで、同じ構成を複数プロビジョニングしたい: Module編

2022.08.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

先日 HashiTalks Japanで「シングルテナント構成のSaaSのIaCにTerraform Workspacesを導入してみた」というビデオ登壇をしました。その中で時間の都合でご紹介できなかった、「Workspacesを使う以外の、同じ構成(リソースセット)を複数個プロビジョニングする方法案」を、複数回に分けてご紹介していきます。

関連エントリ

今回はモジュールを使うパターンのご紹介です。

詳細

dest_aとdest_bという展開先があるとします。

ディレクトリ構成

.
├── modules
│   └── base
│       ├──・ 
│       └──・
├── dest_a.tf
├── dest_b.tf
├── outputs.tf
├── variables.tf
└── versions.tf

dest_a.tf

provider "aws" {
  alias = "dest_a"
  
  assume_role {
    role_arn = "arn:aws:iam::111111111111:role/terraform"
  }
}

module "dest_a_base" {
  source = "./modules/base"

  providers = {
    aws = aws.dest_a
  }
}

dest_b.tf

provider "aws" {
  alias = "dest_b"
  
  assume_role {
    role_arn = "arn:aws:iam::222222222222:role/terraform"
  }
}

module "dest_b_base" {
  source = "./modules/base"

  providers = {
    aws = aws.dest_b
  }
}

dev/stg/prodのように複数環境が必要な場合は以下のようにし、 env/(dev|stg|prod)で各種terraform コマンドを実行します。

.
├── env
│   ├── dev
│   │   ├── dest_a.tf
│   │   ├── dest_b.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── prod
│   │   ├── ・
│   │   └── ・
│   └── stg
│       ├── ・
│       └── ・
└── modules
    └── base
        ├── ・
        └── ・

プロビジョニングするリソースの数が多い場合はモジュールを複数個に分けることも考えましょう。

.
├── env
│   ├── dev
│   │   ├── dest_a.tf
│   │   ├── dest_b.tf
│   │   ├── outputs.tf
│   │   ├── variables.tf
│   │   └── versions.tf
│   ├── prod
│   │   ├── ・
│   │   └── ・
│   └── stg
│       ├── ・
│       └── ・
└── modules
    ├── app
    │   ├── ・
    │   └── ・
    ├── db
    │   ├── ・
    │   └── ・
    ├── network
    │   ├── ・
    │   └── ・
    └── web
        ├── ・
        └── ・

この構成の良い点

シンプル

一度のterraform applyだけで全展開先にプロビジョニングできるのでシンプルです。CDパイプラインを作る際もシンプルにできるでしょう。

展開先間の差分に対応しやすい

共通部分をモジュール化しているだけなので、展開先間で差分がある場合に対応しやすいです。

使うモジュールを変える

Aではsubモジュールを使うけどBでは使わない例です。

dest_a.tf

module "dest_a_main" {
  source = "./modules/main"

  providers = {
    aws = aws.dest_a
  }
}

module "dest_a_sub" {
  source = "./modules/sub"

  providers = {
    aws = aws.dest_a
  }
}

dest_b.tf

module "dest_b_main" {
  source = "./modules/main"

  providers = {
    aws = aws.dest_b
  }
}

variableを使ってモジュール内の挙動を変える

dest_a.tf

module "dest_a_base" {
  source = "./modules/base"

  providers = {
    aws = aws.dest_a
  }

  enable_nat_gateway = true
}

dest_b.tf

module "dest_b_base" {
  source = "./modules/base"

  providers = {
    aws = aws.dest_b
  }

  enable_nat_gateway = false
}

直接リソース書いてしまっても良い

dest_a.tf

module "dest_a_base" {
  source = "./modules/base"

  providers = {
    aws = aws.dest_a
  }
}

resource "aws_s3_bucket" "dest_a" {
  bucket_prefix = "dest_a"
}

この構成のイマイチな点

実行時間が長くなる

メリットに書いた「一度に全展開先にプロビジョニングできるシンプルさ」と引き換えに、planやapplyなど各terraformコマンドの実行時間は長くなります。そのため展開先が非常にたくさんある場合には適さないでしょう。

この構成を採る場合は、並列実行数を上げてterraformコマンド実行を高速化することを検討するのが良いでしょう。

展開先の情報がコードに出る、Gitに残る

ここまでご紹介したサンプルコードがそうであるように、展開先毎にproviderの設定とmodule呼び出しのコードを書く必要があります。通常それはGitを始めとするVCSに記録されるでしょう。(何らかの方法で動的にコード生成すれば回避できるかもしれませんが…)

要件次第ではこれは好ましいことですが、そうでない場合もあるでしょう。例えばシングルテナント構成のSaaSを作るとなった場合、テナントの情報をコードやGitに残すことはしたくないと思います。

ユースケース

DRとして、メインリージョンとは別にホットスタンバイで別リージョンにも同じリソースセットを用意したい、といった場合にはハマるんじゃないかと思います。あまり展開先が増えない要件が適していると言えるでしょう。