はじめに
前回の記事 では 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リポジトリ)
本記事で構築するシステム
アーキテクチャ概要
構成のポイント
| 項目 | 内容 |
|---|---|
| サブネット | パブリックサブネット ×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
| 項目 | 意味 | 今回の状態 |
|---|---|---|
| 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使用率 確認手順
- CloudWatch のメトリクス画面で「EC2」を選択
- 「Auto Scaling グループ別メトリクス」をクリック
- 「CPUUtilization」にチェックを入れる
これにより、Auto Scaling Group 配下のEC2インスタンスのCPU使用率を確認できます
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 やキャッシュ層を追加することで、より本格的なアーキテクチャへ発展させることができます


