# ECS on Fargate ネットワーク設計完全ガイド：awsvpc・ALB/NLB・Service Connect・VPCエンドポイントを本番品質で組む

> ECS Fargate のネットワーク設計を awsvpc の本質から ALB/NLB 接続、セキュリティグループ連鎖、VPCエンドポイント閉域化、Service Connect によるサービス間通信まで Terraform 実コードで体系化します。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: AWS, ECS, Fargate, awsvpc, ALB, Service Connect, VPC, ネットワーク, Terraform, インフラ
- URL: https://tomodahinata.com/blog/aws-ecs-fargate-networking-alb-service-connect-vpc-guide

## 要点

- Fargate は networkMode=awsvpc 固定で各タスクが専用 ENI + プライベート IP を持つため、ALB ターゲットグループは必ず target_type=ip にしなければならない
- セキュリティグループは『ALB の SG → タスクの SG（アプリポートのみ）』の連鎖で組み、タスク側は 0.0.0.0/0 を一切開けない
- 本番の定石はプライベートサブネット + NAT Gateway。VPCエンドポイント（ECR・S3・CloudWatch Logs・Secrets Manager）を整えれば NAT トラフィックを最小化または閉域化できる
- サービス間通信は ECS Service Connect（Envoy サイドカー・DNS 名・自動リトライ・メトリクス）が推奨で、内部 ALB を増やさず疎結合を保てる
- API Gateway → NLB → ALB → ECS on Fargate という 221 エンドポイント構成を実際に本番運用した経験に基づき、各レイヤの設計判断と Terraform 実装を示す

---

ECS on Fargate の本番構築で、最も詰まるのは**ネットワークレイヤ**です。タスクが起動しても ALB に繋がらない、ECR からイメージを pull できない、サービス間の通信が不安定——こうした問題の根には、Fargate 固有の `awsvpc` ネットワークモードに対する理解の不足があります。

私は経済産業大臣賞を受賞した木材流通 B2B SaaS で、`API Gateway → NLB → ALB → ECS on Fargate` という構成を設計・実装・本番運用してきました（221 エンドポイント、プライベートサブネット運用）。このスタックを安定させるうえで、**ネットワーク設計が最大の差別化要因**でした。

この記事では、awsvpc の本質から ALB/NLB 接続・セキュリティグループ連鎖・プライベートサブネット設計・VPCエンドポイント閉域化・Service Connect によるサービス間通信まで、本番品質で組むための設計判断と Terraform 実装を一気通貫で示します。Fargate の基本（タスク定義・デプロイ・コスト・セキュリティ）は [ECS on Fargate 本番運用ガイド](/blog/aws-ecs-fargate-production-guide) を先に読んでください。本稿はネットワーク層に特化します。

---

## 全体像：リクエストはどこを通るか

まず実際のリクエスト経路をアーキテクチャ図で把握します。

```text
インターネット
      │
      ▼
┌─────────────────────────────────────────────┐
│  Public Subnet (ap-northeast-1a / 1c)       │
│  ┌──────────────┐    ┌──────────────────┐   │
│  │  ALB         │    │  NAT Gateway     │   │
│  │  (SG: alb)   │    │  (プライベート    │   │
│  └──────┬───────┘    │   サブネットの    │   │
│         │           │   出口)          │   │
└─────────│───────────└──────────────────┘───┘
          │                    ▲
          │ target_type=ip     │ 外向き通信
          ▼                    │
┌─────────────────────────────────────────────┐
│  Private Subnet (ap-northeast-1a / 1c)      │
│  ┌──────────────────────────────────────┐   │
│  │  ECS Task (ENI + Private IP)         │   │
│  │  SG: task (from alb-sg:8080 only)   │   │
│  │  ┌────────────┐  ┌────────────────┐ │   │
│  │  │  app:8080  │  │ sidecar(Envoy) │ │   │
│  │  └────────────┘  └────────────────┘ │   │
│  └──────────────────────────────────────┘   │
│                                             │
│  VPC Endpoints (Interface / Gateway)        │
│  ecr.api / ecr.dkr / s3 / logs /           │
│  secretsmanager / ssmmessages              │
└─────────────────────────────────────────────┘
          │
          ▼
  AWS サービス (ECR / CloudWatch / Secrets Manager)
```

木材流通 SaaS では、この手前にさらに `API Gateway → NLB` が加わり（[lumber-industry-dx 事例](/case-studies/lumber-industry-dx) 参照）、外部公開と内部 L7 ルーティングの責務を分離しています。この記事では ALB 以降の ECS ネットワーク層を対象にします。

---

## awsvpc の本質：タスクが ENI を持つとは何を意味するか

Fargate は **`networkMode: awsvpc` 固定**です。他のネットワークモード（`bridge`・`host`）は選べません。これは制約ではなく、Fargate が提供するタスク単位の分離境界の構造的な帰結です。

> Each Fargate task has its own isolation boundary and does not share the underlying kernel, CPU resources, memory resources, or elastic network interface with another task.（— [AWS Fargate](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html)）

`awsvpc` モードでは：

- タスクごとに **専用の ENI（Elastic Network Interface）** が割り当てられる
- ENI にはサブネット内の**プライベート IP アドレス**が付与される
- **ホストポートマッピングが存在しない**。EC2 の `bridge` モードのように「ホストの 80 番を コンテナの 8080 番に転送」という概念がない

この「ホストポートマッピングなし」の帰結が ALB 設定に直結します。

### なぜ ALB は target_type="ip" でなければならないか

通常の EC2 インスタンスを ALB のターゲットに登録するとき、`target_type="instance"` を使います。これはインスタンス ID でトラフィックを送り、EC2 側のポートフォワーディングが残りをやります。

Fargate タスクにはホストの概念がありません。タスクは **ENI に紐づいたプライベート IP**としてしか存在しません。だから ALB は**IP アドレス直接**でターゲットを登録しなければなりません。これが `target_type="ip"` の理由です。

```hcl
resource "aws_lb_target_group" "app" {
  # ...
  target_type = "ip"   # Fargate では必ずこれ。"instance" は機能しない
}
```

`target_type="instance"` のままにすると ALB ターゲットの登録に失敗するか、タスクが起動してもターゲットが `unhealthy` のまま維持されます。本番でハマる代表的なミスです。

---

## ロードバランサ接続：ALB vs NLB の選び方

Fargate サービスは ALB・NLB・GWLB のいずれにも対応します。実務の判断基準は次の通りです。

| 観点 | ALB（L7） | NLB（L4） |
|------|-----------|-----------|
| プロトコル | HTTP/HTTPS | TCP / UDP / TLS |
| ルーティング | パスベース・ホストヘッダ・HTTP メソッド | IP + ポートのみ |
| SSL 終端 | ALB が担う | NLB パススルー or TLS 終端 |
| WebSocket | 対応 | 対応（TCP） |
| UDP | 非対応 | 対応（PV 1.4 以降） |
| 固定 IP | 非対応（DNS のみ） | 対応（EIP 割当可） |
| 典型ユース | REST API・Web アプリ | gRPC・ゲーム・IoT・社内 NLB→ALB 多段 |

木材流通 SaaS では `NLB → ALB → ECS` という 2 段構成を採っています。NLB に固定 IP を割り当てて API Gateway のプライベート統合エンドポイントとし、ALB で HTTP ルーティングを行うパターンです。REST API が主体のサービスなら**通常は ALB 1 段で十分**です。

### ALB 完全 Terraform：LB + ターゲットグループ + SG 連鎖 + サービス

```hcl
# ── ALB 本体 ──────────────────────────────────────────────────────────────

resource "aws_lb" "app" {
  name               = "prod-alb"
  internal           = false   # パブリック向け。内部 ALB なら true
  load_balancer_type = "application"
  subnets            = var.public_subnet_ids
  security_groups    = [aws_security_group.alb.id]

  # アクセスログを S3 へ（本番必須）
  access_logs {
    bucket  = var.alb_log_bucket
    prefix  = "alb/prod"
    enabled = true
  }
}

# ── ALB セキュリティグループ ───────────────────────────────────────────────

resource "aws_security_group" "alb" {
  name_prefix = "prod-alb-"
  vpc_id      = var.vpc_id
  lifecycle { create_before_destroy = true }
}

# インターネットから HTTPS を受ける
resource "aws_vpc_security_group_ingress_rule" "alb_https" {
  security_group_id = aws_security_group.alb.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  cidr_ipv4         = "0.0.0.0/0"
}

resource "aws_vpc_security_group_ingress_rule" "alb_https_v6" {
  security_group_id = aws_security_group.alb.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  cidr_ipv6         = "::/0"
}

# ALB → タスクへのアウトバウンド（タスクの SG と対になる）
resource "aws_vpc_security_group_egress_rule" "alb_to_tasks" {
  security_group_id            = aws_security_group.alb.id
  referenced_security_group_id = aws_security_group.task.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}

# ── タスクセキュリティグループ：ALB の SG からのみ受ける ──────────────────

resource "aws_security_group" "task" {
  name_prefix = "prod-task-"
  vpc_id      = var.vpc_id
  lifecycle { create_before_destroy = true }
}

# インバウンド：ALB の SG からアプリポートのみ。0.0.0.0/0 は開けない
resource "aws_vpc_security_group_ingress_rule" "task_from_alb" {
  security_group_id            = aws_security_group.task.id
  referenced_security_group_id = aws_security_group.alb.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}

# アウトバウンド：外向き全開（NAT 経由で ECR / CloudWatch / Secrets Manager へ）
# VPCエンドポイントを使う場合は HTTPS(443) のみに絞ってもよい
resource "aws_vpc_security_group_egress_rule" "task_egress" {
  security_group_id = aws_security_group.task.id
  ip_protocol       = "-1"
  cidr_ipv4         = "0.0.0.0/0"
}

# ── ターゲットグループ：Fargate は必ず target_type = "ip" ─────────────────

resource "aws_lb_target_group" "app" {
  name        = "prod-app"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"   # ← Fargate の核心。絶対に "instance" にしない

  # 接続ドレイン（タスク交代時の in-flight を待つ時間）
  # デフォルト 300 秒は過剰。stopTimeout と合わせて短くする
  deregistration_delay = 60

  health_check {
    path                = "/healthz"
    protocol            = "HTTP"
    port                = "traffic-port"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    interval            = 15
    timeout             = 5
    matcher             = "200"
  }
}

# ── HTTPS リスナー ─────────────────────────────────────────────────────────

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.app.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.acm_certificate_arn

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

# HTTP → HTTPS リダイレクト
resource "aws_lb_listener" "http_redirect" {
  load_balancer_arn = aws_lb.app.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# ── ECS サービス ───────────────────────────────────────────────────────────

resource "aws_ecs_service" "app" {
  name             = "web-api"
  cluster          = var.cluster_id
  task_definition  = var.task_definition_arn
  desired_count    = 2
  launch_type      = "FARGATE"
  platform_version = "LATEST"

  # デプロイ中は常に 2 タスク健全に保ち、最大 4 タスクまで起動してから旧版を落とす
  deployment_minimum_healthy_percent = 100
  deployment_maximum_percent         = 200

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  # プライベートサブネットに配置。パブリック IP は不要
  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false   # プライベートサブネット + NAT の場合は false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }

  # デプロイ直後のヘルスチェック誤検知を防ぐ猶予時間
  # コンテナの startPeriod と合わせて設定する
  health_check_grace_period_seconds = 30

  enable_execute_command = true   # ECS Exec によるブレークグラス
}
```

### `health_check_grace_period_seconds` と `deregistration_delay` の役割

この 2 つは混同されがちですが、フェーズが違います。

- **`health_check_grace_period_seconds`**：タスク起動直後に ALB がヘルスチェックを開始するまでの**猶予時間**。アプリの初期化（DB 接続確立・キャッシュウォームアップ）が完了する前に `unhealthy` と判定されて強制終了されるのを防ぐ。
- **`deregistration_delay`**：タスクを ALB ターゲットから**解除するときに in-flight の処理を待つ時間**。デプロイ・スケールイン・タスク停止のたびに発動する。デフォルトは 300 秒だが、ほとんどの API は秒単位で完了するため過剰。`stopTimeout` と揃えて 30〜60 秒が現実的です。

---

## セキュリティグループ連鎖：0.0.0.0/0 を開けない設計

Fargate ネットワークで最も重要なセキュリティ設計が**セキュリティグループの連鎖**です。

```text
[インターネット]
      │ TCP 443
      ▼
[alb-sg]  ── egress: task-sg:8080 のみ
      │ TCP 8080
      ▼
[task-sg] ── ingress: alb-sg からのみ許可
      │
[タスク ENI:8080]
```

ポイントは**シングル SG の CIDR より、SG 参照（referenced_security_group_id）を使う**ことです。CIDR ベースの許可だと IP が変わったときに設定漏れが起きますが、SG 参照は「その SG が付いたリソース」から来るトラフィックを動的に許可するため、ALB のスケールアウト（IP 追加）にも自動追従します。

**タスクの SG のインバウンドに `0.0.0.0/0` を開けてはいけません。** これをやってしまうと、プライベートサブネットにいても VPC 内の他リソースから直接アクセスできてしまいます。

サービスが複数あって相互に通信する場合も同じ原則です。「サービス A の SG → サービス B の SG（アプリポートのみ）」と連鎖させます。後述の Service Connect を使えば、この SG 設計はさらに整理できます。

---

## サブネット設計：プライベート + NAT vs パブリック直置き

### プライベートサブネット + NAT Gateway（本番の定石）

本番の定石は**プライベートサブネットにタスクを置き、外向き通信を NAT Gateway 経由にする**パターンです。

```text
プライベートサブネット
  └── ECS タスク（assign_public_ip = false）
        └── → NAT Gateway（パブリックサブネット）
              └── → インターネット（ECR / CloudWatch / Secrets Manager）
```

利点は**攻撃面の最小化**です。タスクにパブリック IP がないため、インターネットから直接到達できません。ALB を経由したリクエストのみが届きます。

### パブリック直置き（`assign_public_ip = ENABLED`）

開発環境やプロトタイプでは `assign_public_ip = true` + パブリックサブネット に置く選択もあります。タスクにパブリック IP が付くため NAT なしで ECR・CloudWatch に到達できます。NAT Gateway のコスト（データ処理料 + 時間課金）がかかりません。

ただし**本番には推奨しません**。タスクのパブリック IP が直接インターネットに露出し、SG を誤設定した瞬間にリスクが増大します。また Fargate のプラットフォームが `assign_public_ip = ENABLED` で起動するためにパブリックサブネットが必要で、VPC 設計が複雑になります。

**判断の指針**：本番はプライベート + NAT 一択。NAT のコストが気になるなら後述の VPC エンドポイントで削減する方向を取る。

---

## VPC エンドポイント：NAT を減らす・閉域化する

プライベートサブネットの Fargate タスクが ECR からイメージを pull したり CloudWatch にログを送ったりするとき、デフォルトでは NAT Gateway 経由でパブリックエンドポイントへ通信します。**VPC エンドポイント**を設置すれば、この通信を VPC 内で閉じられます。

### Fargate が必要とする VPC エンドポイント一覧

| エンドポイント | タイプ | 用途 | 必須度 |
|--------------|--------|------|--------|
| `com.amazonaws.<region>.ecr.api` | Interface | ECR API（イメージメタデータ取得） | ECR 使用時 必須 |
| `com.amazonaws.<region>.ecr.dkr` | Interface | ECR からのイメージレイヤ pull（Docker Registry API） | ECR 使用時 必須 |
| `com.amazonaws.<region>.s3` | **Gateway** | ECR イメージレイヤは S3 に格納されている。ecr.dkr だけでは不足 | ECR 使用時 必須 |
| `com.amazonaws.<region>.logs` | Interface | `awslogs` ドライバによる CloudWatch Logs への送信 | CloudWatch Logs 使用時 必須 |
| `com.amazonaws.<region>.secretsmanager` | Interface | `secrets[].valueFrom` による Secrets Manager 注入 | シークレット注入時 必須 |
| `com.amazonaws.<region>.ssmmessages` | Interface | ECS Exec（SSM Session Manager 経由） | ECS Exec 有効時 必須 |

> **注意**：ECR エンドポイント（`ecr.api`・`ecr.dkr`）だけ用意して S3 ゲートウェイエンドポイントを省くと、イメージレイヤの pull が NAT 経由になります。ECR のイメージレイヤは S3 に格納されているため、**S3 ゲートウェイは ECR エンドポイントとセットで必須**です。

> Fargate タスク自身の ECS コントロールプレーン通信（`ecs`・`ecs-agent`・`ecs-telemetry`）はエンドポイントなしで動作しますが、ECR・Secrets Manager・CloudWatch Logs は用意しないとトラフィックがパブリックエンドポイントへ流れます。

### Terraform：VPC エンドポイント一式

```hcl
# ── Interface エンドポイント用 SG ─────────────────────────────────────────

resource "aws_security_group" "vpc_endpoints" {
  name_prefix = "vpc-endpoints-"
  vpc_id      = var.vpc_id
  lifecycle { create_before_destroy = true }
}

# プライベートサブネットからのみ HTTPS を受け付ける
resource "aws_vpc_security_group_ingress_rule" "endpoint_https" {
  security_group_id = aws_security_group.vpc_endpoints.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  cidr_ipv4         = var.vpc_cidr
}

resource "aws_vpc_security_group_egress_rule" "endpoint_egress" {
  security_group_id = aws_security_group.vpc_endpoints.id
  ip_protocol       = "-1"
  cidr_ipv4         = "0.0.0.0/0"
}

# ── S3 ゲートウェイエンドポイント（ECR レイヤ pull に必須） ──────────────

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = var.vpc_id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = var.private_route_table_ids
}

# ── Interface エンドポイント群 ────────────────────────────────────────────

locals {
  interface_endpoints = [
    "ecr.api",
    "ecr.dkr",
    "logs",
    "secretsmanager",
    "ssmmessages",
  ]
}

resource "aws_vpc_endpoint" "interface" {
  for_each = toset(local.interface_endpoints)

  vpc_id              = var.vpc_id
  service_name        = "com.amazonaws.${var.region}.${each.value}"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true   # DNS 解決を VPC 内で完結させる
}
```

`private_dns_enabled = true` が重要です。これにより `ecr.ap-northeast-1.amazonaws.com` などのパブリック DNS 名が VPC 内の ENI IP に解決されるため、**アプリやタスク定義を一切変更せずに閉域化**できます。

### NAT Gateway が不要になるか

VPC エンドポイントをすべて揃えても、NAT Gateway を完全に廃止できるとは限りません。タスクが AWS 以外の外部 API（Stripe・SendGrid など）を呼ぶ場合は引き続き NAT が必要です。ユースケースが「完全に AWS サービスのみ」なら NAT なし閉域構成も可能ですが、稀なケースです。

**現実的な判断**：NAT Gateway は残しつつ VPC エンドポイントで ECR・CloudWatch・Secrets Manager のトラフィックを内部に落とし、NAT のデータ処理コストと帯域依存を減らす方向が本番での定石です。

---

## サービス間通信：ECS Service Connect（推奨）

マイクロサービス構成では、サービス A がサービス B を呼ぶ内部通信が必要になります。選択肢は複数あります。

| 方法 | 仕組み | 長所 | 短所 |
|------|--------|------|------|
| 内部 ALB | 各サービスに内部 ALB を立てる | ALB のルーティング機能が使える | リソース増・コスト・管理複雑化 |
| Cloud Map（DNS）| Route 53 プライベートホストゾーンで DNS 解決 | シンプル | リトライ・タイムアウト・メトリクスがない |
| **Service Connect** | Envoy をサイドカーとして注入、論理名で解決 | DNS + 自動リトライ + メトリクス。ECS マネージド | 同一 ECS クラスタ内での通信が前提 |

**Service Connect が推奨**です。内部 ALB を増やさずに疎結合を実現し、Envoy が接続メトリクスを自動収集して CloudWatch Namespace `AWS/ECS/ManagedScaling` に流すため、可観測性も向上します。

### Service Connect の仕組み

> Amazon ECS Service Connect provides management of service-to-service communication as Amazon ECS configuration. It builds both service discovery and a service mesh in Amazon ECS.（— [ECS Service Connect](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-connect.html)）

Service Connect は ECS がサイドカーとして **Envoy プロキシ**を自動注入します（明示的にコンテナ定義に追加する必要はない）。クライアント側のタスクは論理名（`http://order-api:8080/orders`）で呼べば、Envoy が名前解決・ロードバランシング・リトライ・タイムアウトを処理します。

### Terraform + タスク定義での設定

Service Connect の設定は ECS サービスの `service_connect_configuration` ブロックで行います。

```hcl
# ── サービス A（order-api）が Service Connect でサービスを公開する ──────────

resource "aws_ecs_service" "order_api" {
  name             = "order-api"
  cluster          = var.cluster_id
  task_definition  = var.order_api_task_definition_arn
  desired_count    = 2
  launch_type      = "FARGATE"
  platform_version = "LATEST"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false
  }

  service_connect_configuration {
    enabled   = true
    namespace = var.cloud_map_namespace_arn   # 事前に aws_service_discovery_http_namespace で作成

    # このサービスが公開するエンドポイント
    service {
      port_name      = "http"            # タスク定義の portMappings[].name と一致させる
      discovery_name = "order-api"       # DNS 名として使われる論理名
      client_alias {
        port     = 8080
        dns_name = "order-api"           # 他タスクはこれで到達できる
      }
    }
  }
}

# ── サービス B（inventory-api）が order-api を呼ぶ側 ──────────────────────

resource "aws_ecs_service" "inventory_api" {
  name             = "inventory-api"
  cluster          = var.cluster_id
  task_definition  = var.inventory_api_task_definition_arn
  desired_count    = 2
  launch_type      = "FARGATE"
  platform_version = "LATEST"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false
  }

  service_connect_configuration {
    enabled   = true
    namespace = var.cloud_map_namespace_arn

    # クライアント側は service ブロックを省略（公開しない場合）
    # 自動注入された Envoy が order-api:8080 への通信を仲介する
  }
}
```

タスク定義側では `portMappings` に `name` を付けることが必要です。

```json
{
  "containerDefinitions": [
    {
      "name": "app",
      "portMappings": [
        {
          "containerPort": 8080,
          "protocol": "tcp",
          "name": "http",
          "appProtocol": "http"
        }
      ]
    }
  ]
}
```

これで `inventory-api` 内から `http://order-api:8080/orders` に HTTP リクエストを投げると、Envoy が interceptor として動作し、接続プーリング・リトライ・タイムアウトを自動で処理します。

### Service Connect でサービス間 SG を整理する

Service Connect を使う場合でも、SG 設計は必要です。同一クラスタ内でサービス同士が通信するとき、発信側タスクの SG から受信側タスクの SG に対して当該ポートを許可します。

```hcl
# inventory-api → order-api への通信を許可
resource "aws_vpc_security_group_ingress_rule" "order_from_inventory" {
  security_group_id            = aws_security_group.order_task.id
  referenced_security_group_id = aws_security_group.inventory_task.id
  ip_protocol                  = "tcp"
  from_port                    = 8080
  to_port                      = 8080
}
```

### Cloud Map との使い分け

**Cloud Map** は Route 53 プライベートホストゾーンを使ったシンプルな DNS ベースのサービスディスカバリです。ECS サービスが起動・停止するたびに A レコードを更新します。

- リトライ・タイムアウト・メトリクスが不要な**単純な DNS 解決だけでよい**場合は Cloud Map で十分です
- **接続の可観測性・サーキットブレーカー・細かいタイムアウト制御**が欲しければ Service Connect を選ぶ

新規構築なら Service Connect を推奨します。Cloud Map より設定は増えますが、Envoy が提供するメトリクス（接続数・エラー率・レイテンシ）は本番の観測にそのまま使えます。

---

## NLB を使う場合の注意点

L4 通信（gRPC・WebSocket over TCP・UDP）や固定 IP が必要な場合は NLB を使います。Fargate + NLB 固有の注意点をまとめます。

```hcl
resource "aws_lb" "internal" {
  name               = "prod-nlb"
  internal           = true
  load_balancer_type = "network"
  subnets            = var.private_subnet_ids
}

resource "aws_lb_target_group" "nlb_app" {
  name        = "nlb-app"
  port        = 8080
  protocol    = "TCP"
  vpc_id      = var.vpc_id
  target_type = "ip"   # NLB でも Fargate は ip 必須

  deregistration_delay = 30

  health_check {
    protocol            = "HTTP"
    path                = "/healthz"
    healthy_threshold   = 2
    unhealthy_threshold = 2
    interval            = 10
  }
}
```

**UDP を使う場合は `platform_version = "LATEST"`（= 1.4.0）が必須**です。それ以前のプラットフォームバージョンでは UDP がサポートされません。

また NLB は**クライアント IP を保持する**ため、タスクの SG で NLB のサブネット CIDR からのトラフィックを許可する必要があります（ALB のように SG 参照ができないため CIDR での許可になる）。

```hcl
# NLB のクライアント IP 保持によりサブネット CIDR を許可する
resource "aws_vpc_security_group_ingress_rule" "task_from_nlb_cidr" {
  security_group_id = aws_security_group.task.id
  ip_protocol       = "tcp"
  from_port         = 8080
  to_port           = 8080
  cidr_ipv4         = var.vpc_cidr   # NLB を置いたサブネットの CIDR
}
```

WAF を使ってトラフィックをフィルタリングするには ALB との組み合わせが必要です。詳しくは [WAF 多層防御ガイド](/blog/waf-defense-in-depth-aws-waf-cloud-armor-owasp-guide) を参照してください。

---

## 設計チェックリスト

本番にリリースする前に必ず確認する項目です。

**awsvpc / ALB 基本**
- [ ] タスク定義の `networkMode` が `awsvpc`（Fargate は固定だが明示確認）
- [ ] ALB/NLB ターゲットグループの `target_type = "ip"` になっているか
- [ ] ALB と Fargate タスクが**別々の SG**を持ち、SG 参照で連鎖しているか
- [ ] タスクの SG のインバウンドに `0.0.0.0/0` がないか
- [ ] `health_check_grace_period_seconds` を設定し、アプリの初期化時間を考慮しているか
- [ ] `deregistration_delay` を `stopTimeout` と揃えて短くしているか（デフォルト 300 秒は過剰）

**サブネット / ルーティング**
- [ ] タスクを**プライベートサブネット**に配置し `assign_public_ip = false` か
- [ ] NAT Gateway が各 AZ に存在するか（シングル AZ の NAT は SPOF）
- [ ] プライベートルートテーブルに NAT Gateway へのルートがあるか

**VPCエンドポイント**
- [ ] ECR を使うなら `ecr.api`・`ecr.dkr`・**S3 ゲートウェイ**の 3 点セットが揃っているか
- [ ] CloudWatch Logs を使うなら `logs` エンドポイントがあるか
- [ ] `secrets[].valueFrom` を使うなら `secretsmanager` エンドポイントがあるか
- [ ] ECS Exec を有効にするなら `ssmmessages` エンドポイントがあるか
- [ ] Interface エンドポイントの SG がプライベートサブネット CIDR から TCP 443 を許可しているか
- [ ] `private_dns_enabled = true` になっているか

**Service Connect / サービス間通信**
- [ ] 複数サービスがある場合、内部 ALB を増やさずに Service Connect or Cloud Map で解決しているか
- [ ] Service Connect を使うなら Cloud Map namespace（HTTP namespace）が作成されているか
- [ ] タスク定義の `portMappings` に `name` と `appProtocol` が設定されているか
- [ ] サービス間の SG で相互通信が許可されているか（SG 参照で連鎖）

---

## まとめ

Fargate のネットワークは `awsvpc` の一点から全てが展開します。

- **`awsvpc` + `target_type=ip`** は最初に押さえる絶対ルール
- **SG 連鎖（ALB SG → タスク SG）** で `0.0.0.0/0` をタスクに一切開けない
- **プライベートサブネット + NAT** が本番の安全側。VPCエンドポイントで主要 AWS サービスへの通信を閉域化してコストと攻撃面を削る
- **Service Connect** でサービス間通信を疎結合にし、内部 ALB の増殖を防ぐ

私が 221 エンドポイントを抱える木材流通 SaaS を `API Gateway → NLB → ALB → ECS on Fargate` で安定運用できているのは、この設計を各レイヤで徹底しているからです。ネットワーク層を正しく組めば、アプリ層の問題とインフラ層の問題を明確に分離でき、トラブルシューティングの速度も上がります。

コスト最適化（Spot・Graviton・Savings Plans）は [ECS on Fargate コスト最適化ガイド](/blog/aws-ecs-fargate-cost-optimization-spot-graviton-savings-plans-guide) へ、タスクが停止した際の原因調査は [ECS on Fargate トラブルシューティングガイド](/blog/aws-ecs-fargate-troubleshooting-task-stopped-reasons-guide) へ。このポートフォリオ上の受賞 SaaS の全体構成は [lumber-industry-dx 事例](/case-studies/lumber-industry-dx) で詳しく紹介しています。Fargate 本番基盤の設計・構築を一緒に進めたい場合は、そちらからご相談ください。
