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 Auto Scaling 実践構成

0
Last updated at Posted at 2026-05-02

はじめに

前回の記事 では Terraform の基本操作として、VPC・EC2 の単一構成を構築しました

本記事ではその続編として、実務でよく使われる以下の構成を Terraform で構築します

  • ALB(Application Load Balancer)による負荷分散
  • 複数 AZ への EC2 配置(高可用性)
  • Auto Scaling による自動スケーリング
  • CloudWatch によるメトリクス監視

この記事でわかること

  • ALB + EC2 の冗長構成を Terraform で定義する方法
  • Launch Template を使った EC2 の起動設定
  • Auto Scaling Group による自動スケーリングの設定
  • CloudWatch アラームによる監視設定

前提条件

  • 前回の記事 の内容を理解していること
  • Terraform v1.5 以上がインストール済みであること
  • AWS CLI がインストール・設定済みであること

なお、本記事で使用するソースコードは、以下のGitHubリポジトリからダウンロードできます
terraform-alb-demo(GitHubリポジトリ)

本記事で構築するシステム

アーキテクチャ概要

Terraform で構築する ALB + EC2 Auto Scaling 実践構成.png

構成のポイント

項目 内容
サブネット パブリックサブネット ×2(AZ 分散)
ALB インターネット向け、HTTP(80)
EC2 Launch Template + Auto Scaling Group
スケーリング CPU 使用率 70% 超で自動スケールアウト
監視 CloudWatch アラーム(CPU・ALB 5xx エラー)

AZ 分散の意義

単一 AZ 構成では、その AZ で障害が発生した場合にサービスが停止します
複数 AZ にインスタンスを分散することで、一方の AZ が停止しても残りの AZ でサービスを継続できます

ALB の役割

ALB はリクエストを複数の EC2 インスタンスに振り分けます
ヘルスチェックにより、異常なインスタンスへのルーティングを自動的に停止します

プロジェクトのディレクトリ構成

terraform-alb-demo/
├── main.tf            # プロバイダー・データソース
├── versions.tf        # バージョン管理
├── variables.tf       # 変数定義
├── terraform.tfvars   # 変数値(Git 管理外推奨)
├── vpc.tf             # VPC・サブネット・IGW
├── security_groups.tf # セキュリティグループ
├── alb.tf             # ALB・ターゲットグループ・リスナー
├── ec2.tf             # Launch Template・Auto Scaling Group
├── cloudwatch.tf      # CloudWatch アラーム
└── outputs.tf         # 出力値

手順1: プロジェクトの初期化

mkdir terraform-alb-demo
cd terraform-alb-demo

# ファイルを一括作成
"main.tf","versions.tf","variables.tf","terraform.tfvars",
"vpc.tf","security_groups.tf","alb.tf","ec2.tf",
"cloudwatch.tf","outputs.tf" | % {New-Item -ItemType File -Name $_}

手順2: 基本設定ファイルの作成

2-1. versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

2-2. main.tf

provider "aws" {
  region = var.region
}

data "aws_caller_identity" "current" {}

2-3. variables.tf

variable "region" {
  description = "AWS リージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "project_name" {
  description = "プロジェクト名"
  type        = string
  default     = "alb-demo"
}

variable "ec2_instance_type" {
  description = "EC2 インスタンスタイプ"
  type        = string
  default     = "t3.micro"
}

variable "asg_min_size" {
  description = "Auto Scaling 最小インスタンス数"
  type        = number
  default     = 2
}

variable "asg_max_size" {
  description = "Auto Scaling 最大インスタンス数"
  type        = number
  default     = 4
}

variable "asg_desired_capacity" {
  description = "Auto Scaling 希望インスタンス数"
  type        = number
  default     = 2
}

2-4. terraform.tfvars

region               = "ap-northeast-1"
project_name         = "alb-demo"
ec2_instance_type    = "t3.micro"
asg_min_size         = 2
asg_max_size         = 4
asg_desired_capacity = 2

⚠️ terraform.tfvars.gitignore に追加してください

手順3: VPC とネットワークの構築(vpc.tf)

パブリックサブネットを 2 つの AZ に分散して作成します

# ==================================================
# VPC
# ==================================================
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# ==================================================
# パブリックサブネット(AZ-a)
# ==================================================
resource "aws_subnet" "public_1" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-1"
  }
}

# ==================================================
# パブリックサブネット(AZ-c)
# ==================================================
resource "aws_subnet" "public_2" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.project_name}-public-2"
  }
}

# ==================================================
# Internet Gateway
# ==================================================
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# ==================================================
# ルートテーブル
# ==================================================
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = {
    Name = "${var.project_name}-public-rt"
  }
}

resource "aws_route_table_association" "public_1" {
  subnet_id      = aws_subnet.public_1.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_2" {
  subnet_id      = aws_subnet.public_2.id
  route_table_id = aws_route_table.public.id
}

手順4: セキュリティグループの作成(security_groups.tf)

# ==================================================
# ALB 用セキュリティグループ
# ==================================================
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTP"
    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"]
  }

  tags = {
    Name = "${var.project_name}-alb-sg"
  }
}

# ==================================================
# EC2 用セキュリティグループ
# ==================================================
resource "aws_security_group" "ec2" {
  name        = "${var.project_name}-ec2-sg"
  description = "Security group for EC2 instances"
  vpc_id      = aws_vpc.main.id

  # ALB からの HTTP のみ許可
  ingress {
    description     = "HTTP from ALB"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-ec2-sg"
  }
}

EC2 へのインバウンドは ALB からのみ許可しています
SSH が必要な場合は SSM Session Manager の利用を推奨します

手順5: ALB の構築(alb.tf)

# ==================================================
# ALB 本体
# ==================================================
resource "aws_lb" "main" {
  name               = "${var.project_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.public_1.id, aws_subnet.public_2.id]

  tags = {
    Name = "${var.project_name}-alb"
  }
}

# ==================================================
# ターゲットグループ
# ==================================================
resource "aws_lb_target_group" "main" {
  name        = "${var.project_name}-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "instance"

  health_check {
    path                = "/"
    protocol            = "HTTP"
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    matcher             = "200"
  }

  tags = {
    Name = "${var.project_name}-tg"
  }
}

# ==================================================
# HTTP リスナー
# ==================================================
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

手順6: Launch Template と Auto Scaling Group(ec2.tf)

Launch Template とは

Launch Template は EC2 インスタンスの起動設定をテンプレート化したものです
AMI・インスタンスタイプ・セキュリティグループ・ユーザーデータなどを一元管理できます
Auto Scaling Group はこのテンプレートを使ってインスタンスを自動的に起動します

# ==================================================
# Amazon Linux 2023 の最新 AMI を取得
# ==================================================
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# ==================================================
# EC2 用 IAM ロール(SSM アクセス)
# ==================================================
resource "aws_iam_role" "ec2" {
  name = "${var.project_name}-ec2-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" "ec2_ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "ec2" {
  name = "${var.project_name}-ec2-profile"
  role = aws_iam_role.ec2.name
}

# ==================================================
# Launch Template
# ==================================================
resource "aws_launch_template" "main" {
  name_prefix   = "${var.project_name}-lt-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = var.ec2_instance_type

  iam_instance_profile {
    name = aws_iam_instance_profile.ec2.name
  }

  network_interfaces {
    associate_public_ip_address = true
    security_groups             = [aws_security_group.ec2.id]
  }

  # nginx のインストールと起動
  user_data = base64encode(<<-EOF
#!/bin/bash
dnf update -y

# nginx のインストールと起動
dnf install -y nginx
systemctl start nginx
systemctl enable nginx

# SSM Agent のインストールと起動
dnf install -y amazon-ssm-agent
systemctl enable amazon-ssm-agent
systemctl start amazon-ssm-agent

# インスタンス情報をページに表示
# IMDSv2トークン取得
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
   
# インスタンス情報取得
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id)

# EC2が配置されているAZを取得
AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/placement/availability-zone)
    
# HTML生成
cat > /usr/share/nginx/html/index.html <<HTML
<h1>Hello from alb-demo</h1>
<p>Instance ID: $INSTANCE_ID</p>
<p>AZ: $AZ</p>
HTML
EOF
)

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name = "${var.project_name}-ec2"
    }
  }

  lifecycle {
    create_before_destroy = true
  }
}

# ==================================================
# Auto Scaling Group
# ==================================================
resource "aws_autoscaling_group" "main" {
  name                = "${var.project_name}-asg"
  min_size            = var.asg_min_size
  max_size            = var.asg_max_size
  desired_capacity    = var.asg_desired_capacity
  vpc_zone_identifier = [aws_subnet.public_1.id, aws_subnet.public_2.id]
  target_group_arns   = [aws_lb_target_group.main.arn]
  health_check_type   = "ELB"

  launch_template {
    id      = aws_launch_template.main.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "${var.project_name}-asg"
    propagate_at_launch = true
  }
}

# ==================================================
# Auto Scaling ポリシー(CPU 使用率ベース)
# ==================================================
resource "aws_autoscaling_policy" "scale_out" {
  name                   = "${var.project_name}-scale-out"
  autoscaling_group_name = aws_autoscaling_group.main.name
  adjustment_type        = "ChangeInCapacity"
  scaling_adjustment     = 1
  cooldown               = 300
}

resource "aws_autoscaling_policy" "scale_in" {
  name                   = "${var.project_name}-scale-in"
  autoscaling_group_name = aws_autoscaling_group.main.name
  adjustment_type        = "ChangeInCapacity"
  scaling_adjustment     = -1
  cooldown               = 300
}

health_check_type = "ELB" を指定することで、ALB のヘルスチェック結果に基づいて異常インスタンスを自動的に置き換えます

手順7: CloudWatch アラームの設定(cloudwatch.tf)

CPU 使用率に応じてスケールアウト・スケールインをトリガーするアラームと、ALB の 5xx エラーを監視するアラームを設定します

# ==================================================
# CPU 使用率 高(スケールアウトトリガー)
# ==================================================
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "${var.project_name}-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 60
  statistic           = "Average"
  threshold           = 70
  alarm_description   = "CPU 使用率が 70% を超えた場合にスケールアウト"
  alarm_actions       = [aws_autoscaling_policy.scale_out.arn]

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.main.name
  }
}

# ==================================================
# CPU 使用率 低(スケールイントリガー)
# ==================================================
resource "aws_cloudwatch_metric_alarm" "cpu_low" {
  alarm_name          = "${var.project_name}-cpu-low"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 2
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = 60
  statistic           = "Average"
  threshold           = 30
  alarm_description   = "CPU 使用率が 30% を下回った場合にスケールイン"
  alarm_actions       = [aws_autoscaling_policy.scale_in.arn]

  dimensions = {
    AutoScalingGroupName = aws_autoscaling_group.main.name
  }
}

# ==================================================
# ALB 5xx エラー監視
# ==================================================
resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
  alarm_name          = "${var.project_name}-alb-5xx"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "HTTPCode_Target_5XX_Count"
  namespace           = "AWS/ApplicationELB"
  period              = 60
  statistic           = "Sum"
  threshold           = 10
  alarm_description   = "ALB の 5xx エラーが 1 分間に 10 件を超えた場合"
  treat_missing_data  = "notBreaching"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
  }
}

evaluation_periods = 2 は「2 回連続でしきい値を超えた場合にアラーム」を意味します
一時的なスパイクでスケーリングが発動しないよう調整しています

手順8: 出力値の定義(outputs.tf)

output "app_url" {
  description = "アプリケーション URL"
  value       = "http://${aws_lb.main.dns_name}"
}

output "alb_dns_name" {
  description = "ALB の DNS 名"
  value       = aws_lb.main.dns_name
}

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "asg_name" {
  description = "Auto Scaling Group 名"
  value       = aws_autoscaling_group.main.name
}

output "target_group_arn" {
  description = "ターゲットグループ ARN"
  value       = aws_lb_target_group.main.arn
}

手順9: Terraform の実行

9-1. 初期化

cd terraform-alb-demo
terraform init

9-2. 実行計画の確認

terraform plan

作成されるリソースの概要:

リソース 内容
VPC 関連 7 VPC, サブネット×2, IGW, ルートテーブル, 関連付け×2
セキュリティグループ 2 ALB 用, EC2 用
ALB 3 ALB, ターゲットグループ, HTTP リスナー
EC2 関連 6 AMI データソース, Launch Template, ASG, スケーリングポリシー×2, IAM
CloudWatch 3 CPU 高, CPU 低, ALB 5xx アラーム

9-3. リソースの作成

terraform apply

yes を入力して実行します
EC2 インスタンスの起動と nginx のセットアップに 2〜3 分かかります

9-4. 動作確認

# アプリ URL を確認
terraform output app_url

出力例:

app_url = "http://alb-demo-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com"

ブラウザで表示された URL にアクセスし、以下のような nginx ページが表示されれば成功です

Hello from alb-demo
Instance ID: i-0a1b2c3d4e5f6g7h8
AZ: ap-northeast-1a

ページをリロードするたびに異なるインスタンス ID・AZ が表示され、ロードバランシングが機能していることを確認できます

手順10: Auto Scaling の動作確認

ASG の状態確認

Auto Scaling Group の現在の状態を確認します

aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].{Min:MinSize,Max:MaxSize,Desired:DesiredCapacity,Instances:length(Instances)}' `
  --output table

image.png

項目 意味 今回の状態
Desired 現在、維持したいインスタンス数 2台
Instances 実際に稼働しているインスタンス数 2台
Min これ以上は減らない最低台数 2台
Max スケールアウト時の上限 最大4台

スケールアウトのテスト

EC2 インスタンスに SSM Session Manager で接続し、CPU 負荷をかけます

# インスタンス ID の一覧を取得
aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].Instances[*].InstanceId' `
  --output text
# SSM Session Manager で接続(インスタンス ID を指定)
aws ssm start-session --target i-xxxxxxxxxxxxxxxxx

# CPU 負荷をかける
sudo dnf install -y stress
stress --cpu 4 --timeout 300

CloudWatch アラームが ALARM 状態になり、インスタンスが自動追加されることを確認します

# ASG の状態確認
aws autoscaling describe-auto-scaling-groups `
  --auto-scaling-group-names $(terraform output -raw asg_name) `
  --query 'AutoScalingGroups[0].{Min:MinSize,Max:MaxSize,Desired:DesiredCapacity,Instances:length(Instances)}' `
  --output table

CloudWatchのCPU使用率はデフォルトでは平均値(Average)で表示されるため、複数インスタンス構成では負荷が分散され、CPU使用率が低く見える場合があります
そのため、統計値を「Maximum」に変更することで、最も負荷がかかっているインスタンスのCPU使用率を確認できます
また、期間を1分に設定することで、よりリアルタイムに変化を把握できます

AWSコンソール(CloudWatch)にて、EC2インスタンスのCPU使用率 確認手順

  1. CloudWatch のメトリクス画面で「EC2」を選択
  2. 「Auto Scaling グループ別メトリクス」をクリック
  3. 「CPUUtilization」にチェックを入れる

これにより、Auto Scaling Group 配下のEC2インスタンスのCPU使用率を確認できます

image.png

CPU負荷テストにより、CPU使用率が上昇していることが確認できます

全リソースのクリーンアップ

terraform destroy

yes を入力すると、作成したすべてのリソースが削除されます

トラブルシューティング

1. ターゲットグループのヘルスチェックが失敗する

症状: EC2 インスタンスが unhealthy になります

解決方法:

  • EC2 の nginx が起動しているか確認(SSM Session Manager で接続)
  • セキュリティグループで ALB → EC2 のポート 80 が許可されているか確認
# SSM で接続して nginx の状態確認
sudo systemctl status nginx

2. ALB の DNS 名でアクセスできない

症状: ブラウザでタイムアウトします

解決方法:

  • ALB のセキュリティグループでポート 80 が許可されているか確認
  • ターゲットグループのヘルスチェックが healthy になっているか確認
# ターゲットの状態確認
aws elbv2 describe-target-health `
  --target-group-arn $(terraform output -raw target_group_arn) `
  --query 'TargetHealthDescriptions[*].{ID:Target.Id,State:TargetHealth.State}' `
  --output table

3. Auto Scaling が動作しない

症状: CPU 負荷をかけてもインスタンスが増えません

解決方法:

  • CloudWatch アラームの状態を確認
aws cloudwatch describe-alarms `
  --alarm-names "${project_name}-cpu-high" `
  --query 'MetricAlarms[0].{State:StateValue,Reason:StateReason}' `
  --output table

まとめ

本記事では、前回の単一 EC2 構成から発展させ、実務で使われる ALB + Auto Scaling 構成を Terraform で構築しました

  • 複数 AZ への EC2 分散で高可用性を実現
  • Launch Template で EC2 起動設定を一元管理
  • Auto Scaling で負荷に応じた自動スケーリング
  • CloudWatch アラームで CPU・エラー率を監視

この構成をベースに、RDS やキャッシュ層を追加することで、より本格的なアーキテクチャへ発展させることができます

関連記事

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?