mybook

実例: インフラ構成の選定 — ECS vs EKS

スケールアップの岐路

モバイルアプリのリリースから2ヶ月。ユーザー数が急増し、インフラの課題が出始めた。

リョウがSlackにアラートをスクリーンショットで貼った。

🚨 「ECSタスクのCPU使用率が90%を超えた。今夜のピーク時間(20〜22時)に障害が出るかもしれない。緊急で対応が必要」

シンジはすぐにインシデント対応を始めた。ECSタスクを手動でスケールアウト。CPUが落ち着いたのは30分後だった。

インシデントが落ち着いた翌日、シンジはインフラ戦略の議論をチームに呼びかけた。

「今のECS設定でスケールアップはできる。でも今後の成長に対応できるか真剣に議論しよう。特にKubernetesへの移行も選択肢として検討したい」

「昨日のインシデント、原因は何だったの?」とマイが聞いた。

「ECSのオートスケーリング設定の問題だ。トリガーの閾値が75%で、スケールアウトに5分かかる。その間に負荷が積み上がった。ECSの設定を改善すれば解決できる問題ではある」

「じゃあEKSは必要ないんじゃ?」

「それも含めて評価する。ECSの設定改善で今は凌げても、将来の成長に追いつけるかを考えたい」

現在のインフラ構成

シンジはまず現状を整理した。

Loading diagram...

現状のスペック(Terraform管理):

# terraform/modules/ecs_service/main.tf(現状)
 
resource "aws_ecs_service" "web" {
  name            = "${var.app_name}-web"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.web.arn
  desired_count   = 5
 
  deployment_configuration {
    maximum_percent         = 200
    minimum_healthy_percent = 100
  }
}
 
# 問題のあるオートスケーリング設定
resource "aws_appautoscaling_policy" "web_cpu" {
  policy_type = "TargetTrackingScaling"
 
  target_tracking_scaling_policy_configuration {
    target_value = 75.0  # CPU 75%でスケールアウト
 
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
 
    scale_out_cooldown = 300  # 問題: 5分のクールダウンで反応が遅い
    scale_in_cooldown  = 300
  }
}

現状の課題:

  • ECSタスクのオートスケーリングが遅い(スケールアウトに3〜5分かかる)
  • CPU閾値75%では、ピーク時の急激な増加に追いつけない
  • サービス数が増えてきた(Web, Worker, Batch)
  • 将来的に「AI推薦エンジン」を別サービスとして追加する予定がある
  • ログ・メトリクスの集約(CloudWatch Logs)が大変

チームのスキルセット:

スキル習熟度経験年数
AWS ECS全員2年以上
Kubernetesリョウのみ前職で1年
Terraform全員1年半
Docker全員3年以上

ECS vs EKS の評価

チームはECS(現状継続)とEKS(Kubernetes移行)を比較した。

ECS Fargateの評価

メリット:

# ECSのシンプルなタスク定義
resource "aws_ecs_task_definition" "web" {
  family                   = "${var.app_name}-web"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 1024
  memory                   = 2048
 
  container_definitions = jsonencode([{
    name      = "web"
    image     = "${var.ecr_repository_url}:${var.image_tag}"
    essential = true
 
    portMappings = [{
      containerPort = 3000
      protocol      = "tcp"
    }]
 
    environment = [
      { name = "RAILS_ENV", value = "production" },
      { name = "DATABASE_URL", value = var.database_url }
    ]
 
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"  = "/ecs/${var.app_name}/web"
        "awslogs-region" = "ap-northeast-1"
      }
    }
  }])
}
  • チーム全員が使い慣れている(学習コストゼロ)
  • AWSマネージドで運用負荷が低い(パッチ適用自動)
  • Fargateでサーバーのプロビジョニングが不要
  • IAM/VPCとの統合がネイティブ(セキュリティグループがそのまま使える)
  • 小〜中規模には十分なスケーラビリティ

デメリット:

  • AWSに完全依存(ベンダーロックイン)
  • Kubernetesエコシステムのツール群が使えない(ArgoCD, Istio, Prometheus等)
  • 複雑なルーティング(カナリアリリース等)の実現が難しい
  • マルチクラウド戦略が取りにくい

EKSの評価

メリット:

# Kubernetesのデプロイメント定義(EKSの場合)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: 123456789.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:latest
        ports:
        - containerPort: 3000
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "1000m"
            memory: "1024Mi"
        env:
        - name: RAILS_ENV
          value: "production"
---
# HPA: カスタムメトリクスでの高度なオートスケーリング
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 3
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60
  - type: External
    external:
      metric:
        name: requests_per_second
        selector:
          matchLabels:
            app: web
      target:
        type: AverageValue
        averageValue: "100"
  • Kubernetesの豊富なエコシステム(Istio, ArgoCD, Prometheus, Grafana)
  • カナリアリリース、A/Bテストが柔軟に実現できる
  • ポータビリティが高い(他クラウドへの移行も可能)
  • HPAでカスタムメトリクス(RPS等)でのスケーリングが可能

デメリット:

  • 学習コストが高い(Kubernetes自体の複雑さ、YAML管理)
  • EKSのControl Planeが月$72(固定コスト)
  • チームの現状スキルではキャッチアップに2〜3ヶ月必要
  • デバッグが複雑(kubectl describe pod から始まる調査)

コスト比較(詳細)

項目ECS FargateEKS + Fargate
コンピュート(現状と同規模)約$450/月約$450/月
Control Plane$0$73/月
ロードバランサー現状のALB流用現状のALB流用
学習・移行コスト(初期)$0エンジニア工数2〜3ヶ月
合計(初年度)$5,400/年$6,276/年 + 移行コスト

「Kubernetesに移行しても、技術的にできることが増えるのは間違いない。でも今、その複雑さを受け入れるべきか?」

第三の選択肢: ECS + 設定改善

リョウが別のアイデアを提案した。

「KubernetesじゃなくてもECSでできることが実は多い。今のECSの設定を改善するだけで、スケーラビリティの問題は解決できるんじゃないか?」

具体的には:

  1. オートスケーリングをTargetTrackingからStepScalingに変更
  2. スケールアウトのクールダウンを300秒→60秒に短縮
  3. Predictive Scalingで事前にスケールアウト
# terraform/modules/ecs_service/autoscaling.tf(改善案)
 
# アプリケーションAutoScalingターゲット
resource "aws_appautoscaling_target" "ecs" {
  max_capacity       = 20
  min_capacity       = 3  # 最低3タスクで可用性を確保
  resource_id        = "service/${var.cluster_name}/${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}
 
# StepScaling: CPUの増加幅に応じて段階的にスケールアウト
resource "aws_appautoscaling_policy" "cpu_step_scale_up" {
  name               = "${var.service_name}-cpu-step-scale-up"
  policy_type        = "StepScaling"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace
 
  step_scaling_policy_configuration {
    adjustment_type          = "ChangeInCapacity"
    cooldown                 = 60  # 300秒から60秒に短縮: 急激な負荷変動に対応
    metric_aggregation_type  = "Maximum"
 
    # CPU 70-80%: +2タスク(通常の負荷増加)
    step_adjustment {
      metric_interval_lower_bound = 0
      metric_interval_upper_bound = 10
      scaling_adjustment          = 2
    }
 
    # CPU 80%以上: +4タスク(急激なスパイクに対応)
    step_adjustment {
      metric_interval_lower_bound = 10
      scaling_adjustment          = 4
    }
  }
}
 
resource "aws_appautoscaling_policy" "cpu_step_scale_down" {
  name               = "${var.service_name}-cpu-step-scale-down"
  policy_type        = "StepScaling"
  resource_id        = aws_appautoscaling_target.ecs.resource_id
  scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
  service_namespace  = aws_appautoscaling_target.ecs.service_namespace
 
  step_scaling_policy_configuration {
    adjustment_type          = "ChangeInCapacity"
    cooldown                 = 300  # スケールインは300秒クールダウン(過度な削減を防ぐ)
    metric_aggregation_type  = "Maximum"
 
    step_adjustment {
      metric_interval_upper_bound = 0
      scaling_adjustment          = -1
    }
  }
}
 
# CloudWatchアラーム: スケールアップトリガー
resource "aws_cloudwatch_metric_alarm" "cpu_scale_up" {
  alarm_name          = "${var.service_name}-cpu-high"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 2          # 2回連続で閾値超えたら発火
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = 60         # 1分ごとに評価(デフォルト5分から短縮)
  statistic           = "Maximum"  # 最大値で評価(平均だと急激なスパイクを見逃す)
  threshold           = 70.0
 
  dimensions = {
    ClusterName = var.cluster_name
    ServiceName = var.service_name
  }
 
  alarm_actions = [aws_appautoscaling_policy.cpu_step_scale_up.arn]
}

「これで今の問題(スケールアウトが遅い)は解決できる」とリョウは続けた。「KubernetesはAI推薦エンジンや他のマイクロサービスを追加したとき、本当に複雑になってから考えればいい」

シンジはうなずいた。「根本原因を確認しよう。昨日のインシデントの原因は何だった?」

リョウが答えた。「CPU閾値75%、クールダウン300秒の設定が、急激なスパイクに対応できなかった。EKS固有の問題じゃない」

「つまり、EKSに移行しても同じ設定のままなら同じことが起きる。問題はオーケストレーターじゃなくてスケーリング設定だ」

ADRの決定

シンジはチームの議論を整理してADRを書いた。

# ADR-015: コンテナオーケストレーションをECS Fargateで継続する(EKS移行は見送る)
 
## Status
Accepted
 
## Context
2024年5月、ECSタスクのCPU使用率が90%を超えるインシデントが発生した。
インシデントの詳細: INC-031(PostmortemはConfluenceのINC-031を参照)
 
インシデントの根本原因:
- ECSオートスケーリングのクールダウンが300秒と長く、急激なスパイクに対応できなかった
- CPU閾値75%は低負荷時には適切だが、短時間の急激な増加を捉えられない
- TargetTrackingScalingは反応速度が遅い(インスタンス起動含め5分かかる)
 
この機会にEKS(Kubernetes)への移行を検討した背景:
- 将来のマイクロサービス化への備え(AI推薦エンジン追加予定)
- より高度なオートスケーリング(RPS, カスタムメトリクス)
- DevOpsとして業界標準ツールへの適応
 
現在のシステム規模:
- DAU: 約80,000(モバイルアプリ追加後、2ヶ月で2倍に成長)
- ECSタスク: Webタスク x5, Workerタスク x3
- 月間デプロイ回数: 約15回
- サービス数: 3(Web, Worker, Batch)
 
チームのスキルセット:
- ECS経験: 全員(2年以上)
- Kubernetes経験: リョウのみ(前職で1年)
 
将来の技術的方向性(次6ヶ月の計画):
- AI推薦エンジンのマイクロサービス化(6ヶ月後を予定)
- 機能フラグを使ったカナリアリリース(来Q予定)
 
検討した選択肢:
1. ECS Fargateを継続しオートスケーリングを最適化
2. EKS(Kubernetes)に移行
3. ECS + EKSのハイブリッド(新サービスはEKS、既存はECS)
 
## Decision
ECS Fargateを継続する。EKSへの移行は見送る。
 
当面の課題(スケールアウト速度)はStepScaling + Predictive Scalingの導入で解決する。
 
EKSを採用しない理由:
1. チームの習熟度:
   リョウ以外の全員がKubernetes未経験。
   移行に2〜3ヶ月かかり、その間のビジネス速度が落ちる。
   学習中のミスによる本番インシデントのリスクも高い。
 
2. 現規模での必要性:
   DAU 80,000はECSで十分対応できる。
   ECSの限界はDAU 100万以上のワークロードで通常発生する。
   今回のインシデントはECS自体の限界ではなく設定の問題。
 
3. コスト:
   EKSのControl Planeだけで月$73追加。
   現状のビジネス規模では費用対効果が低い。
   移行工数(3ヶ月分)は年間コストに換算すると$50,000以上。
 
4. 複雑さの増加:
   Kubernetesの学習曲線はチームに過剰な認知負荷を与える。
   デバッグ・トラブルシューティングの難易度が上がる。
 
ECSを選ぶ理由:
- 今のスケーリング問題はStepScalingで解決できる(根本原因は設定の問題)
- チームが既に習熟しており、移行コストがゼロ
- AWSネイティブの統合(IAM, VPC, CloudWatch)がそのまま使える
- 必要になればECRのコンテナイメージはEKSでも使えるため、移行は後でも可能
 
将来のマイクロサービス化について:
- AI推薦エンジンは当面、ECSの別サービスとして追加する
- 本当にKubernetesの機能(Service Mesh, 高度なカナリア)が必要になったら改めて判断
 
## Consequences
良い影響:
- オートスケーリングの改善により、スケールアウト時間を300秒→60秒に短縮
- チームの学習コストゼロで即日対応
- インフラの複雑さが増えず、デプロイの信頼性を維持
- StepScalingで急激なCPUスパイクにも対応可能
 
悪い影響・リスク:
- マルチクラウド戦略が難しい(AWS依存が続く)
- 将来のマイクロサービス化でECSが限界になった場合、その時点でEKS移行が必要
- Kubernetesの学習機会を先送りにする(チームのスキルセット停滞)
 
アクションアイテム:
- ECSのオートスケーリングをStepScalingに変更(今週中: リョウ担当)
- Predictive Scalingの設定(来週: リョウ担当)
- 6ヶ月後にEKS移行の再評価を実施(AI推薦エンジン追加後)
- リョウによる社内Kubernetesハンズオン(来月: 知識の仕込み)
 
再評価のトリガー:
- DAU が500,000を超えた場合
- マイクロサービス数が5を超えた場合
- サービス間通信が複雑になり、Service Meshが必要になった場合
- チームの過半数がKubernetesを習得した場合(移行コストが下がる)

意思決定の透明性

マイが言った。「EKSを選ばない理由まで丁寧に書いてある。採用を検討しているエンジニアが読んでも、技術的に正しい判断だったと分かる」

「それが大事なんだ」とシンジ。「『EKSを知らないからECSにした』と思われたくない。評価した上でECSを選んだことを記録に残す」

Loading diagram...

INFO

「なぜ採用しなかったか」をADRに記録することは、「なぜ採用したか」と同等に重要だ。見送った技術の評価記録は、次に同じ議論が起きたときの出発点になる。「なぜEKSにしないのか?」という質問にADRが答えてくれる。

ECSオートスケーリングの実装と効果

ADRが承認されたその日から、リョウがインフラの修正に取り掛かった。

# terraform/modules/ecs_service/autoscaling.tf(最終版)
 
locals {
  # スケーリングの設定値をわかりやすくまとめる
  scaling_config = {
    min_capacity         = 3
    max_capacity         = 20
    scale_up_threshold   = 70     # CPU 70%でスケールアップ開始
    scale_down_threshold = 40     # CPU 40%以下でスケールダウン
    scale_up_cooldown    = 60     # スケールアップのクールダウン60秒
    scale_down_cooldown  = 300    # スケールダウンのクールダウン300秒
    evaluation_periods   = 2      # 2回連続で閾値超えたら発火
    period               = 60     # 1分ごとに評価
  }
}
 
resource "aws_appautoscaling_target" "ecs" {
  max_capacity       = local.scaling_config.max_capacity
  min_capacity       = local.scaling_config.min_capacity
  resource_id        = "service/${var.cluster_name}/${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}
 
# CPUアラーム: スケールアップトリガー
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "${var.service_name}-cpu-high"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = local.scaling_config.evaluation_periods
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = local.scaling_config.period
  statistic           = "Maximum"
  threshold           = local.scaling_config.scale_up_threshold
 
  dimensions = {
    ClusterName = var.cluster_name
    ServiceName = var.service_name
  }
 
  alarm_actions = [aws_appautoscaling_policy.scale_up.arn]
  ok_actions    = [aws_appautoscaling_policy.scale_down.arn]
 
  tags = {
    Terraform   = "true"
    Environment = var.environment
    ADR         = "ADR-015"  # このリソースがどのADRに基づくかを明記
  }
}

翌日のピーク時間(20時〜22時)。CPU使用率が70%に達したとき、今度はスムーズに1分以内にタスクが追加された。

リョウがSlackに書いた。「ECSのスケーリング修正、動いてる。昨日のピーク時間、CPU最大65%で安定した。インシデントなし」

シンジは返信した。「ADR-015に実施報告を追記しておこう」

# ADR-015への追記(2024年6月)
 
## 実施報告
オートスケーリング設定の変更を2024年6月2日に適用。
 
変更前後の比較:
| 指標 | 変更前 | 変更後 |
|-----|--------|--------|
| スケールアウト反応時間 | 5分 | 1分以内 |
| ピーク時CPU最大値 | 92% | 65% |
| スケールイン判断 | 5分 | 5分(維持) |
| インシデント発生 | あり(週1回ペース) | なし(1ヶ月継続) |
 
判断の妥当性: ECSの設定改善で問題が解消された。
EKS移行を今やらない判断は正しかった。
 
次回再評価予定: 2024年11月(AI推薦エンジン追加後)

Golangでのインフラ運用ツール

AI推薦エンジンをECSのサービスとして追加する準備が始まった。Golang製のAPIサービスだ。

// cmd/health-check/main.go
// ECSタスクのヘルスチェックエンドポイント
 
package main
 
import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
 
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/ecs"
)
 
type HealthStatus struct {
    Status    string            `json:"status"`
    Timestamp time.Time         `json:"timestamp"`
    Checks    map[string]string `json:"checks"`
}
 
func healthHandler(w http.ResponseWriter, r *http.Request) {
    status := HealthStatus{
        Status:    "ok",
        Timestamp: time.Now(),
        Checks: map[string]string{
            "database": checkDatabase(),
            "redis":    checkRedis(),
        },
    }
 
    for _, check := range status.Checks {
        if check != "ok" {
            status.Status = "degraded"
            w.WriteHeader(http.StatusServiceUnavailable)
            break
        }
    }
 
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(status)
}
 
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/health", healthHandler)
 
    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }
 
    // グレースフルシャットダウン(ECSタスクの停止に対応)
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
 
    go func() {
        log.Printf("Health check server starting on :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()
 
    <-quit
    log.Println("Shutting down gracefully...")
 
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
 
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Shutdown error: %v", err)
    }
 
    log.Println("Shutdown complete")
}

ECSのタスク定義にこのヘルスチェックを追加:

# terraform/modules/ecs_service/task_definition.tf
resource "aws_ecs_task_definition" "recommendation_service" {
  family                   = "${var.app_name}-recommendation"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 512
  memory                   = 1024
 
  container_definitions = jsonencode([
    {
      name      = "recommendation-api"
      image     = "${var.ecr_url}/recommendation-service:latest"
      essential = true
 
      portMappings = [
        { containerPort = 8080, protocol = "tcp" }
      ]
 
      healthCheck = {
        command     = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
 
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"  = "/ecs/${var.app_name}/recommendation"
          "awslogs-region" = var.aws_region
          "awslogs-stream-prefix" = "ecs"
        }
      }
    }
  ])
}

6ヶ月後の再評価

シンジはADR-015に書いた「6ヶ月後に再評価」を実行した。

「ADR-015の再評価をしよう。あれから6ヶ月、状況は変わったか?」

リョウが答えた。「AI推薦エンジンのサービスを追加した。現在サービス数は4つ(Web, Worker, Batch, Recommendation)。ECSで全部動かしてる」

「問題は?」

「サービス間の通信はALBを使ってるけど、レイテンシが少し気になり始めてる。Service Meshがあれば解決できる問題かもしれない」

シンジは判断した。「まだEKSに移行するほどではない。サービス数が5を超えたら、そのときに真剣に評価しよう。今日のADR-015の評価は『継続』だ」

新しいADRは書かず、ADR-015に追記した。

# ADR-015 再評価(2024年12月)
 
## 再評価結果
現状維持(ECS継続)
 
評価時点:
- DAU: 約120,000(当初の1.5倍)
- サービス数: 4(Web, Worker, Batch, Recommendation)
- ECSインシデント: 6ヶ月でゼロ(オートスケーリング改善効果)
 
EKS移行トリガーとの比較:
- DAU 500,000: 未達(現在120,000)→ 移行不要
- サービス数 5: 未達(現在4)→ 移行不要
- Service Mesh必要性: 低い(レイテンシは許容範囲)
 
次回再評価: 2025年6月(Recommendation以外の新サービス追加後)

次の章では、ADRをGitリポジトリで管理する具体的な方法を学ぶ。adr-toolsの使い方とPRへの組み込み方、CIでのバリデーション、そして「ADRのライフサイクル管理」を解説する。