はじめに
「検証環境などで、Terraformを用いて同じような構成のEC2インスタンスを複数台立ち上げたい」というケースはよくあります。
しかし、モジュールを使わずにコードを書こうとすると、コードが冗長で非常に長くなってしまいます。可読性が下がるだけでなく、修正漏れなどのミスの原因にもなりがちです。
そこで本記事では、モジュール(module)を活用してコード本体をシンプルに保ちつつ、複数のEC2を一気に作成する方法を解説します!
モジュール側のコード作成から呼び出し側のコード作成だけでなく、出力(Output)するコツまでまとめてご紹介します。
TL;DR
この記事を読めば、以下のことができるようになります。
- モジュールを活用してコード本体をシンプルにする方法
-
for_eachを用いて、1つのモジュールから複数台のEC2(および付随ネットワーク)を一括作成する方法 - モジュールの出力(Output)を呼び出し側で集約し、マップ形式で綺麗に表示する方法
前提条件
- SSH接続用のキーペアがAWS上に作成済みであること
実行環境
- Terraform: v1.15.5
詳細
1. フォルダ構造
モジュールはmodulesフォルダ配下に作成します。モジュール名は、ec2_instanceとします。
また、今回はステージング環境にEC2インスタンスを作成することを想定して、stgフォルダの下にTerraformのファイル本体を配置しています。
.
├── modules
│ └── ec2_instance
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── stg
├── main.tf
├── outputs.tf
└── variables.tf
2-1. ファイル説明(modulesフォルダ配下)
./modules/ec2_instance/main.tf
今回は、作成するEC2インスタンスごとに以下を個別で作成する実装としています。
- VPC
- サブネット
- インターネットゲートウェイ
- ルートテーブル
- セキュリティグループ
本記事では解説をシンプルにするため、1つのEC2ごとに独立したVPC/Subnetを完全新規作成する構成にしています。
実運用ではVPCを共通化するか、インスタンスごとにvpc_cidrを変更して渡す構成が一般的です。
resource "aws_instance" "my_instance" {
ami = var.ami_id
instance_type = var.instance_type
subnet_id = aws_subnet.my_subnet.id
vpc_security_group_ids = [aws_security_group.my_sg.id]
associate_public_ip_address = true
key_name = var.key_name
root_block_device {
volume_size = 8
volume_type = "gp3"
tags = {
Name = "${var.instance_name}-root-volume"
}
}
depends_on = [
aws_internet_gateway.my_igw,
aws_security_group.my_sg
]
tags = {
Name = "${var.instance_name}-instance"
}
}
resource "aws_vpc" "my_vpc" {
cidr_block = var.vpc_cidr
tags = {
Name = "${var.instance_name}-vpc"
}
}
resource "aws_subnet" "my_subnet" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = var.subnet_cidr
map_public_ip_on_launch = true
availability_zone = "ap-northeast-1a"
tags = {
Name = "${var.instance_name}-subnet"
}
}
resource "aws_internet_gateway" "my_igw" {
vpc_id = aws_vpc.my_vpc.id
tags = {
Name = "${var.instance_name}-igw"
}
}
resource "aws_route_table" "my_rt" {
vpc_id = aws_vpc.my_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.my_igw.id
}
tags = {
Name = "${var.instance_name}-rt"
}
}
resource "aws_route_table_association" "my_rta" {
subnet_id = aws_subnet.my_subnet.id
route_table_id = aws_route_table.my_rt.id
}
resource "aws_security_group" "my_sg" {
name = "${var.instance_name}-sg"
description = "Security group for ${var.instance_name}"
vpc_id = aws_vpc.my_vpc.id
ingress {
from_port = 22
to_port = 22
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 = {
Name = "${var.instance_name}-sg"
}
}
ポイントは以下の通りです。
-
EC2インスタンスの作成タイミングを制御するために
depends_onを使用する- リソース間の参照関係(
aws_subnet.my_subnet.idなど)があれば通常は自動で順番が考慮されますが、インターネットゲートウェイ経由の通信設定やセキュリティグループの反映が完全に完了してからEC2を作成したい場合、明示的にdepends_onを書くことで作成順序のトラブルを防げます
depends_on = [ aws_internet_gateway.my_igw, aws_security_group.my_sg ] - リソース間の参照関係(
./modules/ec2_instance/variables.tf
今回はシンプルな実装にしたいので、instance_nameとkey_name以外はデフォルト値を使用します。
variable "ami_id" {
description = "The AMI to use for the instance."
type = string
default = "ami-XXXXXXXXXXXXXXXXX" # 使用するAMI IDを記載
}
variable "instance_type" {
description = "The type of instance to start."
type = string
default = "t2.micro"
}
variable "instance_name" {
description = "Name of the EC2 instance"
type = string
}
variable "vpc_cidr" {
description = "The CIDR block for the VPC."
type = string
default = "10.0.0.0/16"
}
variable "subnet_cidr" {
description = "The CIDR block for the subnet."
type = string
default = "10.0.1.0/24"
}
variable "key_name" {
description = "Key name for the EC2 instance."
type = string
}
./modules/ec2_instance/outputs.tf
出力はシンプルに、インスタンスIDとPublic IPのみとします。
後でも出てきますが注意点としては、以下の通りです
- モジュール(子モジュール)側で定義した
outputs.tfは、単体ではterraform apply実行時のターミナル画面(Outputs領域)に表示されません - 呼び出し元(親モジュール)側の
outputs.tf(今回は./stg/outputs.tf)で、子モジュールの出力値を参照して再定義する必要があります
output "instance_id" {
description = "The ID of the EC2 instance."
value = aws_instance.my_instance.id
}
output "public_ip" {
description = "The public IP address of the EC2 instance."
value = aws_instance.my_instance.public_ip
}
2-2. ファイル説明(stgフォルダ配下)
./stg/main.tf
モジュールを呼び出す側のコード(本体)は、定義したモジュールを指定するだけなので非常にシンプルです。
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
Env = var.env
}
}
}
module "ec2_instances" {
source = "../modules/ec2_instance"
for_each = toset([
"terraform-test-1",
"terraform-test-2"
])
key_name = "test_key"
instance_name = each.key
}
ポイントは以下の通りです。
-
default_tagsを使用すると、作成されるすべてのリソースに強制的に指定したタグを付与できる- 今回は、タグ
Envを付与する
- 今回は、タグ
-
for_eachで作成するリソース名(instance_name)のリストを渡すことで、複数のリソースを作成できる
./stg/variables.tf
変数はシンプルにすべてのリソースに付与する環境名(env)のみを定義します。
ステージング環境を想定しているので、値はstgとしました。
variable "env" {
description = "Environment for the EC2 instance"
type = string
default = "stg"
}
./stg/outputs.tf
./modules/ec2_instance/outputs.tfの注意点でも書いたように、モジュールのoutputs.tfの結果を出力するように記載します。
output "instance_ids" {
description = "A map of instance names to their IDs."
value = { for k, v in module.ec2_instances : k => v.instance_id }
}
output "public_ips" {
description = "A map of instance names to their public IP addresses."
value = { for k, v in module.ec2_instances : k => v.public_ip }
}
3. 実行結果
stgフォルダでTerraformを実行します。
-
コマンド
$ terraform init $ terraform plan $ terraform apply
terraform applyの最後のOutputは以下のようになります。
- 実行結果
Outputs:
instance_ids = {
"terraform-test-1" = "i-XXXXXXXXXXXXXXXXX"
"terraform-test-2" = "i-XXXXXXXXXXXXXXXXX"
}
public_ips = {
"terraform-test-1" = "XXX.XXX.XXX.XXX"
"terraform-test-2" = "XXX.XXX.XXX.XXX"
}
さいごに
今回は、Terraformのモジュールを活用して、コードをシンプルに保ちながら複数台のEC2インスタンスを一気に作成する方法をご紹介しました。
モジュール化を取り入れることで、以下のメリットが得られます。
- コードの再利用性と可読性の向上(本体側は引数を渡すだけでスッキリ)
- 同じ構成のインスタンス追加が簡単(リストに名前を追加するだけ)
-
保守性の向上(構成変更が必要な際もモジュール側を一括修正するだけでOK)
- 変更内容によってはEC2インスタンスなどの「再作成(Destroy & Create)」が発生する場合があります
-
terraform applyの前に、破壊的変更が含まれていないか必ず確認するようにしましょう!
最初はファイルの分割や Output の渡し方に少し戸惑うかもしれませんが、一度作ってしまえば環境構築が劇的に楽になります。ぜひ試してみてください!
参考URL
-
HashiCorp Developer > Standard Module Structure
- 再利用可能なモジュールに推奨されるファイルとディレクトリのレイアウト