🎯 やったこと
単一EC2 → ALB + 複数EC2のクラスタ構成 に進化させた!
今回はAWSのインフラ(特にネットワーク周り)についてTerraformを書いて実装しながら理解してみよう、という試みで簡単なWebクラスター構築を実施しました。以前に単一構成でやったことがあったので、今回はそれをクラスタ構成に進化させました。
本記事はその実施過程をまとめてものになります。
📦 構成の変化
Before:最小構成
インターネット → EC2(1台)
After:Webクラスタ
インターネット
↓
Application Load Balancer
↓
┌─────┴─────┐
EC2(AZ-a) EC2(AZ-c)
🌐 ネットワーク基盤の詳細解説
1. VPC (Virtual Private Cloud)
役割: AWS内の専用ネットワーク空間
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
}
重要な設定:
-
CIDR ブロック:
10.0.0.0/16- 使えるIPアドレス:
10.0.0.0〜10.0.255.255(65,536個) -
/16= サブネットマスク(上位16ビットがネットワーク部)
- 使えるIPアドレス:
- DNS有効化: EC2にホスト名が割り当てられる
ポイント:
- VPCは論理的に完全隔離された空間
- 他のAWSアカウントやVPCとは通信不可(明示的に設定しない限り)
- リージョンごとに作成(複数AZにまたがれる)
2. サブネット(Subnet)
役割: VPCをさらに細かく分割したネットワーク領域
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 1)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
}
作成されたサブネット:
subnet-1: 10.0.1.0/24 (ap-northeast-1a)
└ 使えるIP: 10.0.1.0 〜 10.0.1.255 (256個)
subnet-2: 10.0.2.0/24 (ap-northeast-1c)
└ 使えるIP: 10.0.2.0 〜 10.0.2.255 (256個)
cidrsubnet() 関数の解説:
cidrsubnet("10.0.0.0/16", 8, 1)
↑ ↑ ↑
VPCのCIDR 追加ビット数 ネットワーク番号
-
/16に 8ビット追加 →/24 - ネットワーク番号
1→10.0.1.0/24 - ネットワーク番号
2→10.0.2.0/24
パブリック vs プライベート:
| タイプ | インターネット接続 | 用途 |
|---|---|---|
| パブリック | ✅ 可能 | Webサーバー、ALB |
| プライベート | ❌ 不可 | DB、内部サービス |
今回はパブリックサブネットのみ使用
3. Internet Gateway (IGW)
役割: VPCとインターネットを繋ぐ「玄関」
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
通信フロー:
インターネット ←→ IGW ←→ VPC内リソース
重要ポイント:
- VPCごとに1つだけ作成可能
- IGWがないとインターネットに出られない
- 双方向通信をサポート(inbound/outbound)
- ステートフル(往復の通信を自動で許可)
4. ルートテーブル (Route Table)
役割: 「どこ宛の通信をどこに送るか」を定義する経路表
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 # IGW経由
}
}
ルールの読み方:
宛先: 0.0.0.0/0 → ターゲット: IGW
↓
「すべての外部向け通信はIGWに送る」
サブネットとの関連付け:
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
ポイント:
- サブネットは必ず1つのルートテーブルに関連付けられる
- 明示的に関連付けないと「メインルートテーブル」が使われる
- ルートは上から順に評価される(最長一致)
ルート評価の例:
宛先IP: 10.0.1.50
→ 10.0.1.0/24 にマッチ → ローカル(VPC内通信)
宛先IP: 8.8.8.8
→ 0.0.0.0/0 にマッチ → IGW経由
5. アベイラビリティゾーン (AZ) とマルチAZ構成
AZとは: データセンター群(物理的に分離)
ap-northeast-1 (東京リージョン)
├ ap-northeast-1a (AZ-a)
├ ap-northeast-1c (AZ-c)
└ ap-northeast-1d (AZ-d)
マルチAZ構成のメリット:
AZ-a が障害
↓
subnet-1 (AZ-a): ❌ 停止
subnet-2 (AZ-c): ✅ 継続
↓
サービス継続!
今回の構成:
- サブネット1: AZ-a
- サブネット2: AZ-c
- EC2を2台、それぞれのAZに配置
- ALBが自動的に両方のAZに配置
可用性の計算:
- 単一AZ: 99.5%
- マルチAZ: 99.99%(AWSのSLA)
🛡️ セキュリティグループの詳細解説
セキュリティグループとは
役割: EC2やALBの「ファイアウォール」
重要な特性:
- ステートフル: 往復の通信を自動で許可
- デフォルト拒否: 明示的に許可したもののみ通過
- インバウンド/アウトバウンド: 方向ごとに設定
1. ALB用セキュリティグループ
resource "aws_security_group" "alb" {
name = "alb-sg"
vpc_id = aws_vpc.main.id
# インバウンド: インターネットからのHTTP
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"]
}
}
通信フロー:
ユーザー (203.0.113.50:random) → ALB (sg-alb:80)
↓ インバウンドルールで許可
ALB受信成功
↓ ステートフルなので自動で許可
ALB (sg-alb:80) → ユーザー (203.0.113.50:random)
2. EC2用セキュリティグループ
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
# ALBからのみHTTPを許可
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id] # ALBのSGを指定
}
# SSH(メンテナンス用)
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
重要ポイント:
-
security_groups = [aws_security_group.alb.id]- これにより、ALBのSGを持つリソースからのみアクセス可能
- インターネットから直接EC2にアクセス不可
- セキュリティ向上!
通信フロー:
インターネット → EC2:80
❌ 拒否(ALBのSGがないため)
ALB (sg-alb) → EC2 (sg-web:80)
✅ 許可(ALBのSGを持っているため)
セキュリティグループの評価順序
1. すべてデフォルト拒否
2. インバウンドルールを上から順にチェック
3. どれか1つでもマッチすれば許可
4. アウトバウンドも同様
例:
ingress {
from_port = 80
cidr_blocks = ["10.0.0.0/16"] # VPC内から許可
}
ingress {
from_port = 80
cidr_blocks = ["0.0.0.0/0"] # 全世界から許可
}
→ 最終的に全世界から許可される(ORロジック)
⚖️ Application Load Balancer (ALB) の詳細
ALBの構成要素
ALB
├ リスナー(Listener)
│ └ ルール(Rule)
└ ターゲットグループ(Target Group)
└ ターゲット(EC2インスタンス)
1. ALB本体
resource "aws_lb" "main" {
name = "my-alb"
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id # 複数サブネット必須
internal = false # インターネット向け
}
重要な設定:
-
subnets: 最低2つのAZに配置が必須 -
internal = false: インターネット向け(パブリックIP付与) - ALBは自動的に各AZにエンドポイントを作成
DNS名:
my-alb-123456789.ap-northeast-1.elb.amazonaws.com
→ これが外部からのアクセスポイント
2. ターゲットグループ
resource "aws_lb_target_group" "web" {
name = "web-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2 # 2回成功で正常
unhealthy_threshold = 2 # 2回失敗で異常
timeout = 5 # タイムアウト5秒
interval = 30 # 30秒ごとにチェック
path = "/" # チェックするパス
matcher = "200" # 期待するHTTPステータス
}
}
ヘルスチェックの動作:
30秒ごと:
ALB → EC2:80 に GET / リクエスト
↓
200 OK が返る
↓
正常 (healthy) とマーク
2回連続失敗:
ALB → EC2:80 に GET / リクエスト
↓
タイムアウト or 200以外
↓
異常 (unhealthy) とマーク
↓
このEC2には振り分けない
3. ターゲットの登録
resource "aws_lb_target_group_attachment" "web" {
count = 2
target_group_arn = aws_lb_target_group.web.arn
target_id = aws_instance.web[count.index].id
port = 80
}
ターゲットグループの状態:
Target Group: web-tg
├ EC2-1 (i-xxx): healthy ← トラフィック振り分ける
└ EC2-2 (i-yyy): unhealthy ← 振り分けない
4. リスナー
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.web.arn
}
}
リスナーの役割:
ポート80で待ち受け
↓
リクエストが来たら
↓
ターゲットグループ web-tg に転送
ALBの負荷分散アルゴリズム
デフォルト: ラウンドロビン(Round Robin)
リクエスト1 → EC2-1
リクエスト2 → EC2-2
リクエスト3 → EC2-1
リクエスト4 → EC2-2
...
他のアルゴリズム:
- 最小接続数 (Least Outstanding Requests)
- スティッキーセッション (Cookie-based)
🔄 通信フローの完全図解
1. ユーザーがアクセス
ユーザー
↓ http://my-alb-xxx.ap-northeast-1.elb.amazonaws.com
DNS解決
↓ IPアドレス: 54.xxx.xxx.xxx (ALBのパブリックIP)
インターネット
↓
Internet Gateway
↓ ルートテーブルに従って転送
ALB (sg-alb)
↓ セキュリティグループチェック (80番ポート許可)
リスナー (port 80)
↓ ルールに従って転送
ターゲットグループ
↓ ヘルスチェックでhealthyなターゲットを選択
EC2-1 または EC2-2
↓ セキュリティグループチェック (ALBのSGから許可)
nginx
↓ レスポンス生成
同じ経路を逆方向
↓
ユーザーに返却
2. ネットワークレイヤーでの詳細
レイヤー7 (Application): HTTP
↓
レイヤー4 (Transport): TCP (port 80)
↓
レイヤー3 (Network): IP (10.0.1.10 → 10.0.1.50)
↓
レイヤー2 (Data Link): Ethernet
↓
レイヤー1 (Physical): 物理回線
ALBはレイヤー7で動作:
- HTTPヘッダーを読める
- パスベースルーティング可能
- Cookieベースのスティッキーセッション可能
3. パケットの実際の流れ
[ユーザーPC] 203.0.113.50:54321
↓ SYN
[IGW] 54.xxx.xxx.xxx:80 (ALBのパブリックIP)
↓ SYN/ACK
[ALB] 10.0.1.100:80 (ALB内部IP)
↓ ACK (3way handshake完了)
[ALB] → [EC2] 10.0.1.50:80
↓ 新しいTCPコネクション
[EC2] nginx でリクエスト処理
↓ HTTP 200 OK
[EC2] → [ALB]
↓
[ALB] → [IGW] → [ユーザーPC]
重要: ALBはコネクションを終端する
- ユーザー ↔ ALB: 1つのTCPコネクション
- ALB ↔ EC2: 別のTCPコネクション
💾 EC2インスタンスの詳細
User Data(起動スクリプト)
#!/bin/bash
yum update -y
yum install -y nginx
# インスタンス情報を取得
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)
AZ=$(ec2-metadata --availability-zone | cut -d " " -f 2)
# HTMLファイル作成
cat > /usr/share/nginx/html/index.html <<HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Web Cluster</title>
</head>
<body>
<h1>Instance ID: $INSTANCE_ID</h1>
<h2>AZ: $AZ</h2>
</body>
</html>
HTML
systemctl start nginx
systemctl enable nginx
User Dataの特徴:
- インスタンス起動時に1回だけ実行
- rootユーザーで実行される
- ログは
/var/log/cloud-init-output.logに記録
インスタンスの配置
resource "aws_instance" "web" {
count = 2
# サブネットをラウンドロビンで割り当て
subnet_id = aws_subnet.public[count.index % 2].id
# count.index = 0 → subnet[0] (AZ-a)
# count.index = 1 → subnet[1] (AZ-c)
}
結果:
EC2-1: subnet-1 (AZ-a)
EC2-2: subnet-2 (AZ-c)
🏷️ タグ管理の学び
試したこと
# ❌ ダメだった例
locals {
common_tags = {
CreatedAt = timestamp() # 実行のたびに変わる
}
}
エラー理由:
-
terraform plan時:2025-11-23T10:00:00Z -
terraform apply時:2025-11-23T10:00:05Z - → planとapplyで値が違う!エラー!
正しいパターン
# ✅ 良い例1: 固定値
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
# ✅ 良い例2: plantimestamp(Terraform 1.5以降)
locals {
common_tags = {
CreatedAt = plantimestamp() # planで固定される
}
}
# ✅ 良い例3: 変数で渡す
terraform apply -var="deployed_at=$(date +%Y-%m-%d)"
📊 リソース構成まとめ
作成したリソース一覧
| リソース | 数 | 用途 |
|---|---|---|
| VPC | 1 | ネットワーク基盤 |
| Internet Gateway | 1 | インターネット接続 |
| Subnet | 2 | 複数AZ配置 |
| Route Table | 1 | ルーティング |
| Security Group | 2 | ALB用、EC2用 |
| ALB | 1 | 負荷分散 |
| Target Group | 1 | EC2グループ管理 |
| Listener | 1 | HTTP待ち受け |
| EC2 | 2 | Webサーバー |
リソース間の依存関係
VPC
├─ Internet Gateway
├─ Subnet (×2)
│ └─ Route Table Association
├─ Security Group (ALB)
├─ Security Group (EC2)
└─ Route Table
└─ Route (IGW)
ALB
├─ depends on: Subnet (×2)
├─ depends on: Security Group (ALB)
└─ Listener
└─ Target Group
└─ Target Group Attachment
└─ depends on: EC2 (×2)
EC2 (×2)
├─ depends on: Subnet
└─ depends on: Security Group (EC2)
🎓 学んだ重要概念
1. ネットワークの階層構造
リージョン (ap-northeast-1)
└ VPC (10.0.0.0/16)
├ Subnet-1 (10.0.1.0/24, AZ-a)
└ Subnet-2 (10.0.2.0/24, AZ-c)
2. セキュリティの多層防御
インターネット
↓ [Network ACL] (今回は使ってない)
↓ [Security Group: ALB]
ALB
↓ [Security Group: EC2]
EC2
3. 高可用性の実現
- マルチAZ配置
- ヘルスチェック
- 自動フェイルオーバー
4. Infrastructure as Code
- 宣言的な記述
- バージョン管理
- 再現性
🔍 トラブルシューティング経験
1. タイポ
ams_vpc → aws_vpc
2. 文字化け
<!-- ❌ -->
<html>
<!-- ✅ -->
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
3. タグの動的値エラー
# ❌ timestamp() → 実行のたびに変わる
# ✅ 固定値 or plantimestamp()
🚀 次のステップ候補
レベルアップ案
-
Auto Scaling追加
- CPU使用率で自動スケール
- インスタンス数を動的に変更
-
RDS追加
- DBをプライベートサブネットに分離
- Webサーバーとの分離
-
CloudFront + S3
- 静的ファイルをCDN配信
- 動的コンテンツはALB
-
HTTPS対応
- ACMで証明書発行
- Route53でドメイン管理
-
監視・ログ
- CloudWatch Logs
- アラーム設定
📝 ベストプラクティス
1. タグ戦略
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
provider "aws" {
default_tags {
tags = local.common_tags
}
}
2. リソース命名
役割ベース: main, web, alb, db
詳細ベース: web_server, alb_external, rds_postgres
3. セキュリティ
最小権限の原則
- EC2はALBからのみアクセス可能
- SSH(22番)は本番では削除
まとめ
Terraformを使用することでクイックに実装して動作確認を行うことができました。
また、リソース削除の際もTerraformによって一括で削除できたためうち漏らしの心配なく行えたものよかったです。
マネージドコンソールの操作をコード記載に置換したことで、ネットワーク周りなど本質な事柄に集中できたようにも思えます。
今後、インフラ周りに関わる際に今回得た知識を活用していきたいです。