メインコンテンツへスキップ

Terraformモジュールで再利用可能なインフラを作る

·
インフラ Terraform AWS 実践
目次

今日学んだこと
#

前回の記事でファイルレイアウトによる環境分離を学んだが、ステージングと本番で同じコードを重複して書く問題が残っていた。今回はTerraformモジュールを使って、DRY原則をインフラコードに適用する方法を学んだ。

モジュール化で得られるメリット
#

メリット説明
DRY原則同じコードを複数環境で再利用
保守性1箇所の修正で全環境に反映
柔軟性入力変数で環境ごとのカスタマイズ
安全性バージョン管理でテスト済みコードのみ本番適用

モジュールとは
#

フォルダ内にあるTerraform設定ファイル(.tfファイル)の集まり = モジュール

つまり、これまで作成してきたディレクトリも「モジュール」といえる。

種類説明
ルートモジュールterraform applyを直接実行するフォルダ
再利用可能なモジュール他のモジュールから呼び出されるフォルダ

基本構文
#

module "<NAME>" {
  source = "<SOURCE>"
  # 設定(入力変数など)
}

sourceにはローカルパス、GitHub URL、Terraform Registryなどを指定できる。


ディレクトリ構成の変更
#

前回のstage/のみの構成から、モジュールを分離した構成に変更。

├── modules/                      # 再利用可能なモジュール
│   └── services/
│       └── webserver-cluster/
│           ├── main.tf
│           ├── variables.tf
│           ├── outputs.tf
│           └── user-data.sh
│
└── live/
    ├── stage/                    # ステージング環境
    │   └── services/
    │       └── webserver-cluster/
    │           └── main.tf
    └── prod/                     # 本番環境
        └── services/
            └── webserver-cluster/
                └── main.tf
ディレクトリ役割
modules/環境に依存しない再利用可能コード
live/stage/ステージング環境のルートモジュール
live/prod/本番環境のルートモジュール

モジュールの構成要素
#

プログラミングの関数と対比すると理解しやすい。

Terraformモジュールプログラミング説明
モジュール関数再利用可能なコードの塊
variable引数モジュールに値を渡す
localsローカル変数モジュール内部の計算・定数
output戻り値モジュールから値を返す

以降のセクションで、それぞれの詳細と使い方を見ていく。


入力変数(variable)
#

なぜ必要か
#

前回のコードにはハードコードされた値が多い。

ハードコード問題
instance_type = "t2.micro"prodでは大きいインスタンスが必要
min_size = 2環境ごとにスケールを変えたい
key = "stage/..."prodで使うとstageのDBを参照

定義例
#

modules/services/webserver-cluster/variables.tf

variable "cluster_name" {
  description = "クラスターリソースの名前"
  type        = string
}

variable "instance_type" {
  description = "起動するEC2タイプの種類"
  type        = string
}

variable "min_size" {
  description = "EC2インスタンスのASGの最小値"
  type        = number
}

variable "max_size" {
  description = "EC2インスタンスのASGの最大値"
  type        = number
}

variable "db_remote_state_bucket" {
  description = "S3バケットの名前(データベースのリモートステート)"
  type        = string
}

variable "db_remote_state_key" {
  description = "S3でのデータベースのリモートステートのパス"
  type        = string
}

呼び出し側での値の指定
#

live/stage/services/webserver-cluster/main.tf

provider "aws" {
  region = "ap-northeast-1"
}

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-stage"
  db_remote_state_bucket = "tf-state-backend-20251128"
  db_remote_state_key    = "stage/data-stores/mysql/terraform.tfstate"

  instance_type = "t2.micro"
  min_size      = 2
  max_size      = 2
}

live/prod/services/webserver-cluster/main.tf

module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-prod"
  db_remote_state_bucket = "tf-state-backend-20251128"
  db_remote_state_key    = "prod/data-stores/mysql/terraform.tfstate"

  instance_type = "m4.large"  # より大きいインスタンス
  min_size      = 2
  max_size      = 10          # スケールアウト可能
}

ローカル値(locals)
#

variable との違い
#

種類外部から設定用途
variable可能モジュールのAPI(外部に公開)
locals不可モジュール内部の定数・計算

ポート番号など変更されたくない値localsで定義する。

locals {
  http_port    = 80
  any_port     = 0
  any_protocol = "-1"
  tcp_protocol = "tcp"
  all_ips      = ["0.0.0.0/0"]
}

resource "aws_security_group_rule" "allow_http_inbound" {
  type              = "ingress"
  security_group_id = aws_security_group.alb.id
  from_port         = local.http_port  # 80
  to_port           = local.http_port
  protocol          = local.tcp_protocol
  cidr_blocks       = local.all_ips
}

マジックナンバー 80 より local.http_port の方が意図が明確。


出力(output)
#

なぜ必要か
#

本番環境のみにスケジュール設定を追加したい場合、モジュール内のASG名を外部から参照する必要がある。

modules/services/webserver-cluster/outputs.tf

output "asg_name" {
  value       = aws_autoscaling_group.example.name
  description = "The name of the Auto Scaling Group"
}

output "alb_security_group_id" {
  value       = aws_security_group.alb.id
  description = "ALBのセキュリティグループID"
}

live/prod/main.tf(本番のみスケジュール追加)

resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
  scheduled_action_name  = "scale-out-during-business-hours"
  min_size               = 2
  max_size               = 10
  desired_capacity       = 10
  recurrence             = "0 9 * * *"  # 毎日9時

  autoscaling_group_name = module.webserver_cluster.asg_name  # 出力を参照
}

注意点1: ファイルパス
#

問題
#

相対パス "user-data.sh"ルートモジュールからの相対パスとして解釈される。モジュール内のファイルを参照できない。

解決策: path.module
#

# Before(動かない)
user_data = base64encode(templatefile("user-data.sh", { ... }))

# After(正しく動く)
user_data = base64encode(templatefile("${path.module}/user-data.sh", { ... }))
パス参照指す場所
path.moduleモジュール定義があるディレクトリ
path.rootルートモジュールのディレクトリ
path.cwdterraform applyを実行したディレクトリ

注意点2: インラインブロック
#

問題
#

セキュリティグループのingress/egressには2つの書き方がある。

インラインブロック

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"

  ingress {  # リソース内に直接書く
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

別リソース

resource "aws_security_group" "alb" {
  name = "${var.cluster_name}-alb"
}

resource "aws_security_group_rule" "allow_http" {
  type              = "ingress"
  security_group_id = aws_security_group.alb.id
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}

モジュールでは別リソースを推奨
#

方法柔軟性理由
インラインブロック呼び出し側でルール追加不可
別リソース呼び出し側で追加ルール定義可能

別リソースにすれば、ステージング環境のみテスト用ポートを追加、といったカスタマイズが可能。

# live/stage/main.tf - テスト用ポートを追加
resource "aws_security_group_rule" "allow_testing" {
  type              = "ingress"
  security_group_id = module.webserver_cluster.alb_security_group_id
  from_port         = 12345
  to_port           = 12345
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
}

モジュールのバージョン管理
#

問題
#

ローカルパス参照では、モジュールを変更すると全環境に即座に影響する。

解決策: GitリポジトリとSemVer
#

  1. モジュールを別リポジトリに分離
  2. Gitタグでバージョンを付与
git tag -a "v0.0.1" -m "First release"
git push --follow-tags
  1. 環境ごとに異なるバージョンを参照
# stage: 新バージョンをテスト
source = "github.com/user/modules//services/webserver-cluster?ref=v0.0.2"

# prod: 安定版を維持
source = "github.com/user/modules//services/webserver-cluster?ref=v0.0.1"

GitHubでソースURL指定方法github.com/user/repo

セマンティックバージョニング
#

変更内容バージョン
バグ修正PATCH(v0.0.1 → v0.0.2)
後方互換のある機能追加MINOR(v0.0.2 → v0.1.0)
破壊的変更MAJOR(v0.1.0 → v1.0.0)

エラー対応メモ
#

terraform init の実行場所
#

Terraform initialized in an empty directory!

.tfファイルがあるディレクトリ(ルートモジュール)で実行すること。

backend設定がモジュール内に残っている
#

Error: Failed to get existing workspaces: S3 bucket does not exist.

providerterraform { backend }はルートモジュールのみに書く。再利用可能なモジュールからは削除。


モジュールに含めるべきもの
#

設定modules/live/
provider-必須
terraform { backend }-必須
resource必須任意
variable必須任意
output必須任意
locals任意任意

参照方法の比較
#

ここまで学んだ各要素の参照方法をまとめる。

種類文法
入力変数var.<NAME>var.cluster_name
ローカル値local.<NAME>local.http_port
モジュール出力module.<MODULE>.<OUTPUT>module.webserver_cluster.asg_name
パス参照path.<TYPE>path.module

まとめ
#

学んだこと内容
モジュールの基礎フォルダ = モジュール
入力変数(variable)環境ごとの違いを吸収
ローカル値(locals)変更されたくない内部定数
出力(output)外部から参照可能な値
ファイルパスpath.moduleで正しく参照
インラインブロック別リソースの方が柔軟
バージョン管理Git + SemVerで安全なデプロイ

モジュール化のメリット
#

  1. DRY原則: 同じコードを複数環境で再利用
  2. 保守性: 1箇所の修正で全環境に反映
  3. 柔軟性: 入力変数で環境ごとのカスタマイズ
  4. 安全性: バージョン管理でテスト済みコードのみ本番適用

参考
#