はじめに
本記事では、Terraformを使って ALB + EC2 2台(2AZ)構成のシンプルなWeb環境を構築しながら、 リソースを役割ごとにファイル分割して管理する方法をまとめています。
構成自体は最小限ですが、 後から拡張しやすい・読みやすい形を意識して整理しています。
構成イメージ

本構成では EC2 インスタンスを Public Subnet に配置しています。
本来であれば、セキュリティの観点から ALB を Public Subnet、EC2 を Private Subnet に配置する構成が一般的です。しかし、Private Subnet に EC2 を配置する場合、 OSアップデートやパッケージインストールのために NAT Gateway が必要となり、学習用途としてはコストが高くなってしまいます。
そのため今回は、構成理解とコスト面を優先し、EC2 を Public Subnet に配置する最小構成
としています。
フォルダ構成
terraform-alb-ec2-2az/
├── alb.tf
├── ec2.tf
├── outputs.tf
├── provider.tf
├── scripts/
│ └── check_infra.sh
├── security_group.tf
├── terraform.tfvars
├── variables.tf
└── vpc.tf
Terraformでファイルを分けた理由
Terraformでは、全てのリソースを main.tf にまとめることもできますが、今回は以下の理由で 機能ごとにファイルを分けています。
-
見通しがよくなる
- VPC、サブネット、EC2、ALB、セキュリティグループなどを機能ごとに分けることで、「どのリソースがどこにあるか」がひと目で分かるようにしたかった
-
変更が楽
- 後からリソースを追加・変更するとき、関係するファイルだけを編集すればよい
- ALBをHTTPS対応にする場合はalb.tfのみ修正すればOK
- main.tf に全部まとめていると、変更箇所を探すのが大変になる
-
再利用しやすい
- 同じVPCやセキュリティグループ設定を別プロジェクトでも使いたい場合、
- 該当ファイルだけコピーして再利用可能
- main.tf に全部まとめてしまうと再利用が難しい
Terraformコード概要
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
}
# IGW
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
}
# Public Subnet(AZ: 1a)
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_a_cidr
availability_zone = "ap-northeast-1a"
}
# Public Subnet(AZ: 1c)
resource "aws_subnet" "public_c" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_c_cidr
availability_zone = "ap-northeast-1c"
}
# Route Table(Public用)
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
}
# Route Table と Subnet の紐付け(1a)
resource "aws_route_table_association" "a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
# Route Table と Subnet の紐付け(1c)
resource "aws_route_table_association" "c" {
subnet_id = aws_subnet.public_c.id
route_table_id = aws_route_table.public.id
}
resource "aws_lb" "alb" {
name = var.alb_name
load_balancer_type = "application"
security_groups = [
aws_security_group.alb_sg.id
]
subnets = [
aws_subnet.public_a.id,
aws_subnet.public_c.id
]
# Terraform destroy を確実に成功させるため明示
enable_deletion_protection = false
}
resource "aws_lb_target_group" "tg" {
name = "sample-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200"
interval = 30
timeout = 5
healthy_threshold = 2
unhealthy_threshold = 2
}
}
resource "aws_lb_target_group_attachment" "web_a" {
target_group_arn = aws_lb_target_group.tg.arn
target_id = aws_instance.web_a.id
port = 80
}
resource "aws_lb_target_group_attachment" "web_c" {
target_group_arn = aws_lb_target_group.tg.arn
target_id = aws_instance.web_c.id
port = 80
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.alb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.tg.arn
}
}
# ALB用 Security Group
resource "aws_security_group" "alb_sg" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
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"]
}
}
# EC2用 Security Group
resource "aws_security_group" "ec2_sg" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# IAM Role + Instance Profile for SSM
resource "aws_iam_role" "ssm_role" {
name = "ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ssm_attach" {
role = aws_iam_role.ssm_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssm_profile" {
name = "ec2-ssm-profile"
role = aws_iam_role.ssm_role.name
}
# AMI(Amazon Linux 2023)
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# EC2(AZ-a)
resource "aws_instance" "web_a" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public_a.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
associate_public_ip_address = true
# SSM管理用
iam_instance_profile = aws_iam_instance_profile.ssm_profile.name
user_data = <<EOF
#!/bin/bash
dnf install -y httpd
systemctl enable httpd
systemctl start httpd
echo "Hello from AZ-a" > /var/www/html/index.html
EOF
tags = {
Name = "web-a"
}
}
# EC2(AZ-c)
resource "aws_instance" "web_c" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public_c.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
associate_public_ip_address = true
# SSM管理用
iam_instance_profile = aws_iam_instance_profile.ssm_profile.name
user_data = <<EOF
#!/bin/bash
dnf install -y httpd
systemctl enable httpd
systemctl start httpd
echo "Hello from AZ-c" > /var/www/html/index.html
EOF
tags = {
Name = "web-c"
}
}
EC2の user_data とは?
TerraformでEC2を作るときに、user_data を使うと インスタンス起動時に自動で実行される初期設定スクリプト を書けます。
この user_data 1つで EC2 起動直後に Apache をインストール・起動・簡易ページ作成まで自動化します。
AZごとに表示メッセージを変えることで、ロードバランサ経由でアクセス確認も簡単にできるようにしています。
# AWS Region
variable "region" {
description = "AWS region"
type = string
default = "ap-northeast-1"
}
# EC2 instance type
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
# VPC CIDR
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
# Public Subnet CIDRs
variable "public_subnet_a_cidr" {
description = "CIDR block for public subnet in AZ-a"
type = string
default = "10.0.1.0/24"
}
variable "public_subnet_c_cidr" {
description = "CIDR block for public subnet in AZ-c"
type = string
default = "10.0.2.0/24"
}
# ALB name
variable "alb_name" {
description = "Name of Application Load Balancer"
type = string
default = "sample-alb"
}
# AWS Region
region = "ap-northeast-1"
# EC2
instance_type = "t3.micro"
# Network
vpc_cidr = "10.0.0.0/16"
public_subnet_a_cidr = "10.0.1.0/24"
public_subnet_c_cidr = "10.0.2.0/24"
# ALB
alb_name = "sample-alb"
terraform.tfvarsとは?
Terraformでは 変数(variables.tfで定義) に値を渡すために terraform.tfvars を使います。
これにより、コードを変更せずに環境ごとの設定を切り替えられるようになります。
terraform.tfvarsの値を変えればリージョンやサブネット、インスタンスサイズを簡単に調整できます。
作業手順
- フォルダ作成・Terraformコード配置
- terraform init
- terraform plan
- terraform apply
- scripts/check_infra.shで作成済みリソースを確認
- 不要になったら terraform destroy
まとめ
今回は、Terraformで ALB+EC2 2台の最小構成 を作る際に、機能ごとにファイルを分けて管理する方法を整理しました。
今後はさらに module化してフォルダ管理を行い、VPCやEC2、ALBなどの構成を再利用・管理しやすくしていきたいと思います。
次回は、今回作成したリソースをスクリプトで確認した結果をまとめていく予定です。
Terraformでの自動作成と、CLIによる確認を組み合わせることで、構築後の状態把握も効率化していきます。