EKS3年運用で月120万円の請求を半減|Kubernetes本番の痛すぎる失敗と対策
Kubernetes本番運用で実際にハマった月120万円の請求増加、OOMKill地獄、ノードプロビジョニングの失敗。教科書には載らない3年の試行錯誤から学んだ実装的な対策をまとめました。
うちのチームがKubernetes本番で痛い目を見た話
先日、開発チーム全体で「今月のEKS請求が月120万円になった」という話が出て、ようやく本気で向き合うことになったんですよ。正直、去年まではコンテナをぶち込めばどうにかなると思ってた。でも3年運用してみて気づいたのは、Kubernetesって「使える」と「運用できる」は全く別物だということ。教科書的には分からない、実務の地雷がいっぱいあるんです。
この記事では、うちのチームが実際に引っかかったKubernetes本番運用の問題と、対策として実装してきたことを正直に書きます。ネットに載ってないような「地味だけど重い問題」が大半です。
リソースリクエスト・リミットを甘く見ると月80万円が消える
去年の話なんですけど、ステージング環境で動くアプリが本番で急に落ちる現象が起きたんです。ログにはOOMKilledの文字。「メモリ不足?でもステージングでは動いてる」って、当時は本当に意味不明だった。
原因は、リソースリクエストを甘く設定しすぎてたんですよ。ステージングでは小さいワークロードばっかりだから、リクエストをmemory: 256Miとかで統一してた。本番にぶち込んだら、実際には2Gi必要なアプリまでその設定で走ってて、カーネルがOOMKillしまくったわけです。
で、その後どうしたかというと、リクエストを適切に設定し直すのと並行して、Karpenterを導入してノード自動スケーリングを全部タスクベースで動かすようにしました。今まではNode Autogroupsで「ザックリ16個のノード用意しとく」みたいな運用だったから、リソース効率が本当に悪かったんです。
# Before: 雑い設定
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# After: 本番測定ベースの設定
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "3Gi"
cpu: "2000m"
Karpenterに切り替えてから、無駄なノード上げっぱなしがなくなって、月15万円くらい浮いたんですよ。実装もシンプルで、Provisionerの定義だけで「CPU密度が90%超えたら自動でノード足す」みたいなロジックが動く。
ただ、ここで地味に引っかかるポイントがあるんです。既存のCluster Autoscalerと共存させると地獄になるんですよ。両方が競合してスケーリング判定を二重にやるから、無駄なノードが立ったり消えたり。必ず片方に統一してください。
NetworkPolicyを本番で無効化してる人、多くないですか?
“セキュリティは後付けでいい”みたいなノリで、ずっとNetworkPolicyを有効にしてなかったんです。だから本番環境なのに、Podが全方向でしゃべり放題。これが原因で、セキュリティレビューで指摘されて、急遽実装することになりました。
NetworkPolicyを一気に有効化するのは危険なので、まずはMonitoring用の設定で「違反するトラフィックがあれば検出するけど、ブロックはしない」という状態を3週間保ちました。その間、ログを監視して、アプリが実際にどのトラフィックを使ってるか把握する。この期間がないと、デプロイのたびに本番が止まるんですよ。
# Monitoring mode: ブロックなし、違反検出のみ
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: app-policy
annotations:
# Cilium(eBPF使ったCNI)の場合
cilium.io/debug-logging: "true"
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to: # DNS通信
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
これを本番に適用してから、想像以上に通信パターンが複雑なことに気づきました。サイドカープロキシが予想外の通信してたり、ヘルスチェック用の別ポートが開いてたり。ちゃんと設定しないと、デプロイのたびに本番が止まるんです。
うちはCiliumを使ってるんですけど、eBPF使ったネットワーク制御だから、ホストレベルでのフィルタリングができるんです。Flannel + Calico とかの古い構成だと、Ciliumのような柔軟性がないので、後々マイグレーションの時に苦労する可能性があります。正直、今から始めるならCilium一択だと思ってますね。
PVC・ストレージ周りでチーム全体が止まる日
ステートフルアプリケーション(データベース、キャッシュ層)をKubernetes上で動かすのは、個人的にはまだ本番では避けてます。でも、メッセージキューとか、キャッシュレイヤーとか、複製してもいいステートレスなデータストアは、Kubernetes上で動かすことが多いんです。
そこで落とし穴になるのがPVC(PersistentVolumeClaim)のライフサイクル管理なんですよ。
# Podが削除されたら、PVCも自動削除するのか、残すのか
apiVersion: v1
kind: Pod
metadata:
name: cache-pod
spec:
containers:
- name: redis
image: redis:7
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: cache-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cache-pvc
spec:
# 重要: StorageClass指定
storageClassName: ebs-gp3 # AWS EBS使用
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
うちでハマったのは、StatefulSetでRedisのレプリカを回してたんですけど、スケーリングしたときにPVが自動拡張されずに、データが詰まって本番が止まったこと。容量不足でPodがEvictされて、PVCが自動削除されずに残ってるから、復旧しようとしても前のデータが邪魔になるんです。最終的に手動でPVCを削除して、データを再構築するって流れになりました。月10万円のダウンタイムコスト。かなり痛い経験ですね。
EBS のボリューム自動拡張は、StorageClassの定義でallowVolumeExpansion: trueを明示的に設定しないと動かないんです。ドキュメントに書いてあるけど、デフォルト False なので見落としやすい。地味だけど本当に重い設定ですよ。
Ingressの複雑さで本番ぶっ壊す前に
Ingressコントローラー選びは、マジで重要です。うちは最初ALB Ingressコントローラー使ってたんですけど、複数のサービスをホスト名ベースでルーティングするようになったら、管理が地獄になったんです。
# 複数のHostがあると、ALB側でTargetGroupが増殖する
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- host: admin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 9000
これを20個ぐらいのホストでやると、ALBのTargetGroup数が100を超えるんですよ。AWS のアカウント当たりのリソース上限に引っかかったり、コスト管理が複雑になったりするんです。
Nginx Ingressに乗り替えてから、管理が劇的に簡単になったんですよ。Nginx側で全部ハンドルしてくれるから、AWSレベルではシンプルなNLBだけ。レイテンシも若干下がったし、ログのデバッグも楽になった。地味な改善ですけど、運用の負担がぐんと減ります。
本番環境でHPA(自動スケーリング)が暴走する仕組み
HPA(Horizontal Pod Autoscaler)の設定を甘く見てたんですよ。CPU使用率ベースで「80%超えたら2倍にスケール」みたいな設定。まあ、これが問題なんです。
トラフィックスパイクが起きたときを想像してみてください。急に1000req/s来たら、CPU使用率が一気に100%になる。HPAが「やばい、スケールしよう」って判定するんですけど、新しいPodが起動するまで30秒かかるんです。その間、既存のPodは過負荷状態。新しいPod起動後、トラフィックが落ち着いても、HPAの判定がラグで10分後に「あ、もう大丈夫だ」って気づいて縮小。こういった振動が起きるわけですね。
# Before: 甘い設定
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
# After: 本番向けの設定
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # より早めにスケール
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # メモリも監視
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 5分待ってから縮小
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0 # すぐにスケール
policies:
- type: Percent
value: 100
periodSeconds: 30
スケールダウンのstabilizationWindowSecondsを300秒(5分)に設定することで、トラフィックが一時的に上がったときのコスト無駄を防ぎます。スケールアップは0にしておいて、スパイクに素早く対応する。こういった細かい調整が本番では効いてくるんですよ。
あと、HPAだけじゃなく、Pod Disruption Budget(PDB)も設定しないと、ノード縮小時にアプリが止まったりするので注意です。
LoggingとMonitoringの設定が甘いと、本番障害の原因究明が1週間かかる
これはKubernetes特有の話じゃないんですけど、Container Log の扱いが小さく見られてる気がします。
うちのチームは最初、各Podのログはkubectl logs で見るだけだったんですよ。本番トラブルが起きたら、該当のPodを特定して手でログ見る。でもPodって自動で再起動されるから、ログが消えちゃう。結果、「なぜ止まったか分からない」って状況に何度もハマったんです。
今はFluentd + Elasticsearch(ELK Stack)でセントラルログを集約してます。CloudWatch Logs に直送してもいいんですけど、複数のECS・EKS・Lambda から大量に送られてくるので、直送だとコスト膨大になる。Fluentd側でフィルタリングして、エラーログだけはCloudWatch、その他は Elasticsearch に放ってます。
Monitoring(メトリクス)も同じで、Prometheus + Grafana 構成で、CPU・メモリ・リクエストレート・レスポンスタイム をダッシュボード化してます。
# Fluentd設定例
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
data:
fluent.conf: |
<source>
@type tail
path /var/log/containers/*.log
pos_file /var/log/es-containers.log.pos
tag kubernetes.*
read_from_head true
<parse>
@type json
time_format %Y-%m-%dT%H:%M:%S.%NZ
</parse>
</source>
<match kubernetes.**>
@type elasticsearch
@id output_es
@log_level info
include_tag_key true
host elasticsearch.default.svc.cluster.local
port 9200
path _bulk
<buffer>
@type file
path /var/log/fluentd-buffers/kubernetes.system.buffer
flush_mode interval
retry_type exponential_backoff
flush_interval 5s
flush_at_shutdown true
retry_forever
retry_max_interval 30
chunk_limit_size "#{ENV['FLUENT_ELASTICSEARCH_BUFFER_CHUNK_LIMIT_SIZE'] || '8M'}"
</buffer>
</match>
本番でのアラート設定も大事です。CPU が80%超えたら通知、Pod のEviction が発生したら通知、みたいなルールを Prometheus の alerting rules で定義。Slack 連携して、チーム全体に通知が行く。こういった地道な設定が、障害検知と対応速度を大きく改善するんですよ。
本番Kubernetesは「セットアップして終わり」じゃない
正直、EKS 立ち上げるだけなら簡単です。CloudFormation なり Terraform なり、IaC で定義すれば、30分で本番環境が建つ。でも、そこからが本当の運用の始まりなんですよ。
リソース管理、ネットワーク設計、ストレージ戦略、ログ・メトリクス収集、セキュリティ設定、障害対応…全部が絡み合ってる。1個の判断ミスで月100万円のコスト無駄や、本番ぶっ壊れが起きるんです。
うちのチームは3年かけてようやく「それなりに運用できてる」という段階に来ました。でも、新しいバージョンが出たり、ワークロードが増えたり、要件が変わったりするたびに、また新しい問題に直面する。その度に、設定を見直して、改善して、また本番で検証する。その繰り返しです。
関連して、インシデント対応の最新ベストプラクティス2026で本番障害時の対応フローについても書いてるので、併せて読むといいかもしれません。
まとめ
-
リソースリクエスト・リミットは測定ベース:ステージングで動く≠本番で効率的。Karpenterでタスク単位のスケーリングを実装すると無駄が減ります。
-
NetworkPolicyは後付け不可:本番で有効化する前に、必ずMonitoring モードで3週間の検証期間を。実際の通信パターンは複雑です。
-
ストレージはステートレスを優先:PVC管理の複雑さを避けるなら、キャッシュやメッセージングなど、複製してもいいデータだけをKubernetes内に置く。本物のDBはマネージドサービスで。
-
HPA設定は本番トラフィックで再検証:CPU ベースだけじゃなく、メモリ・カスタムメトリクスを組み合わせ、スケールダウンのstabilization window を長めに設定。
-
ログとメトリクスはセントラル集約必須:Pod再起動でログが消えるから、Fluentd + Elasticsearch または CloudWatch Logs で一元管理。Prometheus + Grafana でメトリクス可視化。
Kubernetes本番は、やることが多くて大変です。でも一度ちゃんと整えると、スケーリング・デプロイ・障害対応が本当に楽になるんですよ。その投資は確実に返ってきます。うちのチームも、最初は「こんな複雑で運用できるのか」って思ってたけど、今は「Kubernetes ないと本当に困る」ってレベル。
もし今Kubernetes 本番化を進めてる方なら、最初から「セキュリティとログ・メトリクスは必須」くらいの気持ちでやった方が、後での後悔がぐんと減りますよ。