0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Terraform】ALB+EC2で作る最小Web構成(構築編)

Posted at

はじめに

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

構成イメージ

スクリーンショット 2025-12-27 18.06.00.png
本構成では 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 にまとめることもできますが、今回は以下の理由で 機能ごとにファイルを分けています。

  1. 見通しがよくなる
    • VPC、サブネット、EC2、ALB、セキュリティグループなどを機能ごとに分けることで、「どのリソースがどこにあるか」がひと目で分かるようにしたかった
  2. 変更が楽
    • 後からリソースを追加・変更するとき、関係するファイルだけを編集すればよい
    • ALBをHTTPS対応にする場合はalb.tfのみ修正すればOK
    • main.tf に全部まとめていると、変更箇所を探すのが大変になる
  3. 再利用しやすい
    • 同じVPCやセキュリティグループ設定を別プロジェクトでも使いたい場合、
    • 該当ファイルだけコピーして再利用可能
    • main.tf に全部まとめてしまうと再利用が難しい

Terraformコード概要

vpc.tf
# 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
}
alb.tf
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
  }
}
security_group.tf
# 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"]
  }
}
ec2.tf
# 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ごとに表示メッセージを変えることで、ロードバランサ経由でアクセス確認も簡単にできるようにしています。

variables.tf
# 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"
}
terraform.tfvars
# 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の値を変えればリージョンやサブネット、インスタンスサイズを簡単に調整できます。

作業手順

  1. フォルダ作成・Terraformコード配置
  2. terraform init
  3. terraform plan
  4. terraform apply
  5. scripts/check_infra.shで作成済みリソースを確認
  6. 不要になったら terraform destroy

まとめ

今回は、Terraformで ALB+EC2 2台の最小構成 を作る際に、機能ごとにファイルを分けて管理する方法を整理しました。
今後はさらに module化してフォルダ管理を行い、VPCやEC2、ALBなどの構成を再利用・管理しやすくしていきたいと思います。
次回は、今回作成したリソースをスクリプトで確認した結果をまとめていく予定です。
Terraformでの自動作成と、CLIによる確認を組み合わせることで、構築後の状態把握も効率化していきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?