実例: インフラ構成の選定 — ECS vs EKS
スケールアップの岐路
モバイルアプリのリリースから2ヶ月。ユーザー数が急増し、インフラの課題が出始めた。
リョウがSlackにアラートをスクリーンショットで貼った。
🚨 「ECSタスクのCPU使用率が90%を超えた。今夜のピーク時間(20〜22時)に障害が出るかもしれない。緊急で対応が必要」
シンジはすぐにインシデント対応を始めた。ECSタスクを手動でスケールアウト。CPUが落ち着いたのは30分後だった。
インシデントが落ち着いた翌日、シンジはインフラ戦略の議論をチームに呼びかけた。
「今のECS設定でスケールアップはできる。でも今後の成長に対応できるか真剣に議論しよう。特にKubernetesへの移行も選択肢として検討したい」
「昨日のインシデント、原因は何だったの?」とマイが聞いた。
「ECSのオートスケーリング設定の問題だ。トリガーの閾値が75%で、スケールアウトに5分かかる。その間に負荷が積み上がった。ECSの設定を改善すれば解決できる問題ではある」
「じゃあEKSは必要ないんじゃ?」
「それも含めて評価する。ECSの設定改善で今は凌げても、将来の成長に追いつけるかを考えたい」
現在のインフラ構成
シンジはまず現状を整理した。
現状のスペック(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 Fargate | EKS + Fargate |
|---|---|---|
| コンピュート(現状と同規模) | 約$450/月 | 約$450/月 |
| Control Plane | $0 | $73/月 |
| ロードバランサー | 現状のALB流用 | 現状のALB流用 |
| 学習・移行コスト(初期) | $0 | エンジニア工数2〜3ヶ月 |
| 合計(初年度) | $5,400/年 | $6,276/年 + 移行コスト |
「Kubernetesに移行しても、技術的にできることが増えるのは間違いない。でも今、その複雑さを受け入れるべきか?」
第三の選択肢: ECS + 設定改善
リョウが別のアイデアを提案した。
「KubernetesじゃなくてもECSでできることが実は多い。今のECSの設定を改善するだけで、スケーラビリティの問題は解決できるんじゃないか?」
具体的には:
- オートスケーリングをTargetTrackingからStepScalingに変更
- スケールアウトのクールダウンを300秒→60秒に短縮
- 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を選んだことを記録に残す」
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のライフサイクル管理」を解説する。