Terraformのモジュールを使ってコードを再利用する
はじめに
これまでの学習で、変数を使ってTerraformコードを柔軟にすることはできるようになりました。しかし、VPCやEC2など、複数のリソースを組み合わせたインフラを毎回手動で定義するのは効率的ではありません。今回は、これらのまとまりを「部品」として再利用可能にするための重要な機能、**モジュール(Modules)**について解説します。
1. モジュールとは?
モジュールは、関連する複数のリソースを一つの論理的な単位としてカプセル化(まとめる)する機能です。これにより、複雑なインフラをシンプルに、そして再利用可能な形で管理できます。
例えば、VPCと複数のサブネット、ルートテーブル、インターネットゲートウェイといったネットワーク関連のリソース群を一つの「ネットワークモジュール」として作成し、他のプロジェクトで簡単に呼び出して使えるようになります。
モジュールの利点
- DRY原則の実現: Don't Repeat Yourself - 同じコードを何度も書く必要がない
- 抽象化: 複雑なリソース構成を単純なインターフェースで利用できる
- 標準化: 組織内でのインフラ構成パターンを統一できる
- テスト可能性: モジュール単位でのテストが可能
2. モジュールの種類
Terraformには、主に以下の種類のモジュールがあります:
-
ルートモジュール:
terraform plan
やapply
を実行するディレクトリそのものです。これまでのハンズオンで作成してきたコードは、すべてルートモジュールに属します - ローカルモジュール: 同じリポジトリ内の別ディレクトリに配置された子モジュール
- リモートモジュール: Terraform Registry、Git、HTTPなどの外部ソースから取得するモジュール
- パブリックモジュール: Terraform Registryで公開されているコミュニティ製モジュール
3. プロジェクト構成の設計
モジュールを使用する際の推奨ディレクトリ構成:
project-root/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── outputs.tf
│ ├── staging/
│ └── prod/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── compute/
│ └── security/
└── README.md
4. モジュールの作成
ステップ1:ネットワークモジュールの作成
modules/networking
ディレクトリを作成し、VPCに関連するリソースを定義します。
modules/networking/variables.tf
variable "project_name" {
description = "プロジェクト名"
type = string
}
variable "environment" {
description = "環境名(dev, staging, prod)"
type = string
}
variable "vpc_cidr" {
description = "VPCのCIDRブロック"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "有効なCIDRブロックを指定してください。"
}
}
variable "availability_zones" {
description = "使用するアベイラビリティゾーンのリスト"
type = list(string)
default = ["ap-northeast-1a", "ap-northeast-1c"]
}
variable "public_subnet_cidrs" {
description = "パブリックサブネットのCIDRブロックリスト"
type = list(string)
}
variable "private_subnet_cidrs" {
description = "プライベートサブネットのCIDRブロックリスト"
type = list(string)
}
variable "enable_nat_gateway" {
description = "NAT Gatewayを有効にするかどうか"
type = bool
default = true
}
variable "single_nat_gateway" {
description = "単一のNAT Gatewayを使用するかどうか(コスト削減用)"
type = bool
default = false
}
variable "common_tags" {
description = "すべてのリソースに適用する共通タグ"
type = map(string)
default = {}
}
modules/networking/main.tf
locals {
common_tags = merge(var.common_tags, {
Module = "networking"
Environment = var.environment
})
}
# VPCリソース
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-vpc-${var.environment}"
})
}
# インターネットゲートウェイ
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(local.common_tags, {
Name = "${var.project_name}-igw-${var.environment}"
})
}
# パブリックサブネット
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${var.project_name}-public-subnet-${count.index + 1}-${var.environment}"
Type = "Public"
})
}
# プライベートサブネット
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
tags = merge(local.common_tags, {
Name = "${var.project_name}-private-subnet-${count.index + 1}-${var.environment}"
Type = "Private"
})
}
# Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.private_subnet_cidrs)) : 0
domain = "vpc"
depends_on = [aws_internet_gateway.this]
tags = merge(local.common_tags, {
Name = "${var.project_name}-eip-nat-${count.index + 1}-${var.environment}"
})
}
# NAT Gateway
resource "aws_nat_gateway" "this" {
count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.private_subnet_cidrs)) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = merge(local.common_tags, {
Name = "${var.project_name}-nat-gateway-${count.index + 1}-${var.environment}"
})
depends_on = [aws_internet_gateway.this]
}
# パブリックルートテーブル
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-public-rt-${var.environment}"
Type = "Public"
})
}
# パブリックサブネットとルートテーブルの関連付け
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# プライベートルートテーブル
resource "aws_route_table" "private" {
count = var.enable_nat_gateway ? length(var.private_subnet_cidrs) : 1
vpc_id = aws_vpc.this.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = var.single_nat_gateway ? aws_nat_gateway.this[0].id : aws_nat_gateway.this[count.index].id
}
}
tags = merge(local.common_tags, {
Name = "${var.project_name}-private-rt-${count.index + 1}-${var.environment}"
Type = "Private"
})
}
# プライベートサブネットとルートテーブルの関連付け
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = var.enable_nat_gateway ? aws_route_table.private[var.single_nat_gateway ? 0 : count.index].id : aws_route_table.private[0].id
}
modules/networking/outputs.tf
output "vpc_id" {
description = "作成されたVPCのID"
value = aws_vpc.this.id
}
output "vpc_cidr_block" {
description = "VPCのCIDRブロック"
value = aws_vpc.this.cidr_block
}
output "public_subnet_ids" {
description = "パブリックサブネットのIDリスト"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "プライベートサブネットのIDリスト"
value = aws_subnet.private[*].id
}
output "internet_gateway_id" {
description = "インターネットゲートウェイのID"
value = aws_internet_gateway.this.id
}
output "nat_gateway_ids" {
description = "NAT GatewayのIDリスト"
value = aws_nat_gateway.this[*].id
}
output "public_route_table_id" {
description = "パブリックルートテーブルのID"
value = aws_route_table.public.id
}
output "private_route_table_ids" {
description = "プライベートルートテーブルのIDリスト"
value = aws_route_table.private[*].id
}
5. モジュールの使用
ステップ2:環境固有の設定でモジュールを呼び出し
environments/dev/main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
# ローカル変数の定義
locals {
project_name = "myapp"
environment = "dev"
common_tags = {
Project = local.project_name
Environment = local.environment
ManagedBy = "Terraform"
Owner = "DevOps Team"
}
}
# ネットワークモジュールの呼び出し
module "networking" {
source = "../../modules/networking"
project_name = local.project_name
environment = local.environment
vpc_cidr = "10.0.0.0/16"
availability_zones = ["ap-northeast-1a", "ap-northeast-1c"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # 開発環境ではコスト削減のため単一NAT Gateway
common_tags = local.common_tags
}
# EC2インスタンス(例)
resource "aws_instance" "web" {
count = 2
ami = "ami-0d52744d6551d851e" # Amazon Linux 2023
instance_type = "t3.micro"
subnet_id = module.networking.public_subnet_ids[count.index]
vpc_security_group_ids = [aws_security_group.web.id]
tags = merge(local.common_tags, {
Name = "${local.project_name}-web-${count.index + 1}-${local.environment}"
})
}
# セキュリティグループ
resource "aws_security_group" "web" {
name_prefix = "${local.project_name}-web-${local.environment}-"
vpc_id = module.networking.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.project_name}-web-sg-${local.environment}"
})
}
environments/dev/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = module.networking.vpc_id
}
output "public_subnet_ids" {
description = "パブリックサブネットID"
value = module.networking.public_subnet_ids
}
output "web_instance_ids" {
description = "Webサーバーのインスタンスid"
value = aws_instance.web[*].id
}
output "web_instance_public_ips" {
description = "WebサーバーのパブリックIP"
value = aws_instance.web[*].public_ip
}
6. 本番環境での設定
environments/prod/main.tf
# ... プロバイダ設定は同じ ...
locals {
project_name = "myapp"
environment = "prod"
common_tags = {
Project = local.project_name
Environment = local.environment
ManagedBy = "Terraform"
Owner = "DevOps Team"
}
}
# 本番環境では異なる設定を使用
module "networking" {
source = "../../modules/networking"
project_name = local.project_name
environment = local.environment
vpc_cidr = "10.1.0.0/16" # 異なるCIDR範囲
availability_zones = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"] # より多くのAZ
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
private_subnet_cidrs = ["10.1.11.0/24", "10.1.12.0/24", "10.1.13.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # 本番環境では冗長性のため複数NAT Gateway
common_tags = local.common_tags
}
# 本番環境では異なるインスタンスタイプを使用
resource "aws_instance" "web" {
count = 3 # より多くのインスタンス
ami = "ami-0d52744d6551d851e"
instance_type = "t3.small" # より大きなインスタンスタイプ
subnet_id = module.networking.private_subnet_ids[count.index] # プライベートサブネットに配置
vpc_security_group_ids = [aws_security_group.web.id]
tags = merge(local.common_tags, {
Name = "${local.project_name}-web-${count.index + 1}-${local.environment}"
})
}
# ... セキュリティグループなど ...
7. モジュールの初期化と実行
モジュールを使用する場合は、初回にterraform init
を実行してモジュールを取得する必要があります:
cd environments/dev
terraform init
terraform plan
terraform apply
モジュールを更新した場合は、terraform get -update
を実行します:
terraform get -update
8. ベストプラクティス
8.1 モジュールの設計原則
- 単一責任: 1つのモジュールは1つの目的に特化する
- 再利用性: 異なる環境で使い回せるように設計する
- 文書化: README.mdでモジュールの使用方法を明記する
- バージョン管理: モジュールのバージョンを適切に管理する
8.2 入力検証
-
validation
ブロックを使って不正な入力を防ぐ - 適切なデフォルト値を設定する
- 型を明示的に指定する
8.3 出力の設計
- 他のモジュールやリソースで必要になる可能性のある値を出力する
- 出力には適切な説明を付ける
- 機密情報は
sensitive = true
を設定する
8.4 タグ戦略
- 共通タグを変数として受け取る
- モジュール内でローカル変数を使ってタグを統一する
9. リモートモジュールの活用
Terraform Registryのパブリックモジュールも活用できます:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-1a", "ap-northeast-1c"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = true
tags = {
Environment = "dev"
ManagedBy = "Terraform"
}
}
まとめ
機能 | メリット |
---|---|
モジュール化 | コードの再利用性向上、複雑性の削減 |
パラメータ化 | 環境に応じた柔軟な設定が可能 |
標準化 | 組織内でのインフラ構成パターンの統一 |
保守性 | 変更の影響範囲を限定し、メンテナンスが容易 |
テスト性 | モジュール単位でのテストが可能 |
モジュールは、Terraformを大規模なプロジェクトで活用する上で不可欠な機能です。適切に設計されたモジュールを使用することで、コードの重複をなくし、より保守性の高いインフラ管理を実現できます。
次回は、Terraformのバックエンドと、チーム開発で必須となるステートファイルの共有について解説します。お楽しみに!