前提条件
- 第1回のVPCネットワーク基盤構築が完了していること
- こちらからDockerコンテナでの実行環境が準備できていること
Terraform実行環境の起動
前回と同様に、Dockerコンテナ内で作業を進めます。
# コンテナを起動してbashに入る
docker-compose run --rm terraformこれ以降のコマンドは、すべてこのコンテナ内で実行します。
セキュリティ機能の基礎知識
Security Groupsとは
Security GroupsはEC2インスタンスやロードバランサーなどのリソースに適用する仮想ファイアウォールです。
WEBアプリケーション開発者の視点から見ると
- ローカル開発でポート番号を指定してアクセスを制限する感覚に似ています
- 例: localhost:3000でフロントエンド、localhost:8080でAPIサーバーを動かすような制御
- AWSでは、これをネットワークレベルで厳密に制御します
主な特徴
- ステートフル: 許可したインバウンド通信の戻りのトラフィックは自動で許可されます
- デフォルト拒否: 明示的に許可したものだけが通信可能です
- リソース単位: 各リソース(ALB、ECS、RDSなど)に専用のSecurity Groupを作成します
NAT Gatewayとは
NAT (Network Address Translation) Gatewayは、プライベートサブネット内のリソースがインターネットにアクセスするための通信経路を提供します。
なぜ必要か
- アプリケーションサーバーは外部APIを呼び出す必要があります
- システムアップデートやパッケージのダウンロードが必要です
- セキュリティを保ちながら、アウトバウンド通信を可能にします
Network ACLsとは
Network ACLs (Access Control Lists) は、サブネットレベルで動作するファイアウォールです。
Security Groupsとの違い
| 項目 | Security Groups | Network ACLs | 
|---|---|---|
| 適用レベル | インスタンス単位 | サブネット単位 | 
| ステート | ステートフル | ステートレス | 
| デフォルト | すべて拒否 | すべて許可 | 
| ルール評価 | すべてのルールを評価 | 番号順に評価 | 
今回作成するネットワーク構成
| リソース | 説明 | 数量 | 
|---|---|---|
| Security Group (ALB) | ロードバランサー用のセキュリティグループ | 1個 | 
| Security Group (ECS) | コンテナ用のセキュリティグループ | 1個 | 
| Security Group (RDS) | データベース用のセキュリティグループ | 1個 | 
| NAT Gateway | プライベートサブネットのアウトバウンド通信用 | 2個(マルチAZ) | 
| Elastic IP | NAT Gateway用の固定IP | 2個 | 
| Network ACLs | サブネットレベルのアクセス制御(カスタマイズ) | 必要に応じて | 
Terraformコードの実装
Security Groups
各リソース用のSecurity Groupを作成します。
# terraform/security.tf
# ALB用Security Group
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-${var.environment}-alb-sg"
  description = "Security group for Application Load Balancer"
  vpc_id      = aws_vpc.main.id
  ingress {
    description = "HTTP from Internet"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "HTTPS from Internet"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    description = "All traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-alb-sg"
  }
}
# ECS用Security Group
resource "aws_security_group" "ecs" {
  name        = "${var.project_name}-${var.environment}-ecs-sg"
  description = "Security group for ECS Fargate"
  vpc_id      = aws_vpc.main.id
  ingress {
    description     = "HTTP from ALB"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
  # アプリケーションが使用するポート(例: 3000)
  ingress {
    description     = "App port from ALB"
    from_port       = 3000
    to_port         = 3000
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
  egress {
    description = "All traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-ecs-sg"
  }
}
# RDS用Security Group
resource "aws_security_group" "rds" {
  name        = "${var.project_name}-${var.environment}-rds-sg"
  description = "Security group for RDS PostgreSQL"
  vpc_id      = aws_vpc.main.id
  ingress {
    description     = "PostgreSQL from ECS"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.ecs.id]
  }
  egress {
    description = "All traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-rds-sg"
  }
}NAT Gateway
プライベートサブネットからのアウトバウンド通信を可能にするため、各AZにNAT Gatewayを設置します。
# terraform/nat.tf
# Elastic IPs for NAT Gateways
resource "aws_eip" "nat" {
  count = length(var.availability_zones)
  domain = "vpc"
  tags = {
    Name = "${var.project_name}-${var.environment}-nat-eip-${substr(var.availability_zones[count.index], -1, 1)}"
  }
}
# NAT Gateways
resource "aws_nat_gateway" "main" {
  count = length(var.availability_zones)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  tags = {
    Name = "${var.project_name}-${var.environment}-nat-${substr(var.availability_zones[count.index], -1, 1)}"
  }
  depends_on = [aws_internet_gateway.main]
}
# Private Subnetからのルート設定を更新
resource "aws_route" "private_nat" {
  count = length(var.availability_zones)
  route_table_id         = aws_route_table.private[count.index].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.main[count.index].id
}Network ACLs(オプション)
デフォルトのNetwork ACLはすべての通信を許可しています。セキュリティ要件に応じてカスタマイズが可能です。
# terraform/nacl.tf
# カスタムNetwork ACL for Public Subnets (オプション)
resource "aws_network_acl" "public" {
  vpc_id = aws_vpc.main.id
  ingress {
    rule_no    = 100
    protocol   = "tcp"
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 80
    to_port    = 80
  }
  ingress {
    rule_no    = 110
    protocol   = "tcp"
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 443
    to_port    = 443
  }
  # Ephemeral ports for return traffic
  ingress {
    rule_no    = 120
    protocol   = "tcp"
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 1024
    to_port    = 65535
  }
  egress {
    rule_no    = 100
    protocol   = "-1"
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-public-nacl"
  }
}
# カスタムNetwork ACL for Private Subnets (オプション)
resource "aws_network_acl" "private" {
  vpc_id = aws_vpc.main.id
  # VPC内からの通信を許可
  ingress {
    rule_no    = 100
    protocol   = "-1"
    action     = "allow"
    cidr_block = var.vpc_cidr
    from_port  = 0
    to_port    = 0
  }
  # Ephemeral ports for return traffic
  ingress {
    rule_no    = 110
    protocol   = "tcp"
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 1024
    to_port    = 65535
  }
  egress {
    rule_no    = 100
    protocol   = "-1"
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }
  tags = {
    Name = "${var.project_name}-${var.environment}-private-nacl"
  }
}
# Network ACLとサブネットの関連付け (オプション)
# デフォルトのNetwork ACLを使用する場合はコメントアウト
# resource "aws_network_acl_association" "public" {
#   count = length(aws_subnet.public)
#
#   network_acl_id = aws_network_acl.public.id
#   subnet_id      = aws_subnet.public[count.index].id
# }
#
# resource "aws_network_acl_association" "private" {
#   count = length(aws_subnet.private)
#
#   network_acl_id = aws_network_acl.private.id
#   subnet_id      = aws_subnet.private[count.index].id
# }出力の更新
作成したリソースの情報をterraform/outputs.tfに追加します。第1回で作成した出力に加えて、以下を追記します
# terraform/outputs.tf(既存の出力に追加)
# Security Groups関連の出力
output "security_group_alb_id" {
  description = "ALB用Security GroupのID"
  value       = aws_security_group.alb.id
}
output "security_group_ecs_id" {
  description = "ECS用Security GroupのID"
  value       = aws_security_group.ecs.id
}
output "security_group_rds_id" {
  description = "RDS用Security GroupのID"
  value       = aws_security_group.rds.id
}
# NAT Gateway関連の出力
output "nat_gateway_ids" {
  description = "NAT GatewayのIDリスト"
  value       = aws_nat_gateway.main[*].id
}
output "nat_gateway_public_ips" {
  description = "NAT GatewayのElastic IPアドレスリスト"
  value       = aws_eip.nat[*].public_ip
}リソースを作成
Dockerコンテナ内で以下のコマンドを実行します。
# terraformディレクトリへ移動
cd terraform
# 実行計画の確認
terraform plan
# リソースの作成
terraform applyNAT Gatewayの作成には3〜5分程度かかる場合があります。また、NAT Gatewayは料金が発生するリソースです(1時間あたり約$0.045)。不要になったら削除することを推奨します。
作成結果の確認
# Security GroupのID確認
terraform output security_group_alb_id
terraform output security_group_ecs_id
terraform output security_group_rds_id
# NAT Gateway情報の確認
terraform output nat_gateway_ids
terraform output nat_gateway_public_ips動作確認
AWS Management Consoleでの確認
Security Groups(EC2 > セキュリティグループ)
ALB用Security Group
- インバウンドルール: HTTP(80)とHTTPS(443)が許可されていること
- ソース: 0.0.0.0/0(インターネット全体)
ECS用Security Group
- インバウンドルール: ALBのSecurity Groupからのみアクセス許可
- ポート80と3000が開放されていること
RDS用Security Group
- インバウンドルール: ECSのSecurity Groupからのみアクセス許可
- PostgreSQLポート(5432)が開放されていること
NAT Gateway(VPC > NAT ゲートウェイ)
- 各AZに1つずつ、計2つのNAT Gatewayが作成されていること
- ステータスが「Available」になっていること
- Elastic IPが関連付けられていること
Route Tables(VPC > ルートテーブル)
プライベートルートテーブルを確認し、以下のルートが追加されていることを確認
- 送信先: 0.0.0.0/0
- ターゲット: nat-xxxxx(対応するNAT Gateway)
NAT Gatewayのコスト管理
NAT Gatewayは常時料金が発生する上割高なため、特に開発環境ではコストが課題になります。ここでは、コストを管理する方法と、そもそもNAT Gatewayが必要かどうかを判断する基準を見ていきましょう。
Terraformによる切り替え
コスト削減のため、Terraformの変数を利用してNAT Gatewayの作成を制御する仕組みを導入します。
設定変数
terraform/variables.tf に、NAT Gatewayの有効/無効を切り替えるための変数を定義しましょう。
variable "enable_nat_gateway" {
  description = "NAT Gatewayを有効にするかどうか"
  type        = bool
  default     = false
}設定ファイルでの制御
terraform/terraform.tfvars ファイルで、この変数の値を変更することで、NAT Gatewayの作成を簡単に制御できます。
# terraform/terraform.tfvars
# NAT Gatewayが不要な場合は false (デフォルト)
# 外部API連携などで必要な場合は true に変更
enable_nat_gateway = false設定を変更した後は、terraform plan と terraform apply を実行することで、リソースの作成または削除が適用されます。
※ システムの要件によって、NAT Gatewayの要否は異なります。
今回のハンズオンでのバックエンドはHono経由でDBへアクセスし、レスポンスを返すAPIを作成するものです。このように、外部への通信が発生しないシステムではNAT Gatewayは不要です。
ユーザーからのリクエスト(インバウンド通信)と、ECSからRDSへのVPC内通信のみなのでケチっていきます。
ECS + NAT Gatewayの必要性:In/Out別
| 通信方向 | 用途 | NAT Gateway必要 | 備考 | 
|---|---|---|---|
| OUT | ECRからイメージpull | ✅ 必要 | プライベートサブネット配置時は必須 | 
| OUT | 外部API呼び出し | ✅ 必要 | 決済API、認証サービス等 | 
| OUT | DNS解決 | ✅ 必要 | 外部ドメイン名解決時 | 
| OUT | ログ送信 | ✅ 必要 | CloudWatch Logs等への送信 | 
| OUT | パッケージDL | ✅ 必要 | 実行時に依存関係取得する場合 | 
| OUT | VPC内通信 | ❌ 不要 | RDS、ElastiCache、内部ALB等 | 
| IN | ALB経由アクセス | ❌ 不要 | ロードバランサー経由 | 
| IN | ポートフォワード | ❌ 不要 | SSM Session Manager経由 | 
| IN | 直接アクセス | ❌ 不要 | パブリックIP使用時 | 
次のステップ
次回は、Application Load Balancer (ALB)を構築します。今回作成したSecurity Groupsを活用し、インターネットからのトラフィックを受け付ける仕組みを実装します。
- ALBの基本設定
- Target GroupとListenerの構成
- ヘルスチェックの設定
- パスベースルーティング
