Kubernetes環境でアプリを動かす際、プロキシやコレクターのサイドカーを同居させる設計は一般的です。Istioプロキシ、Cloud SQL Auth Proxy、Datadog Agentなど、どれも本番運用では欠かせない裏方です。
一方で、アプリ本体については「できるだけCPU Requestを小さくして、ノードの集約率を上げたい」と考えることがあります。特に軽量なAPIであれば、平常時は 25m 程度でも十分に処理できることがあり、コスト最適化の観点からRequestを小さくしたくなるのは自然です。
ところが、この2つを組み合わせたときに厄介な現象に遭遇しました。アプリコンテナは100%の負荷でカツカツになり、レスポンス遅延まで発生している。それなのに、HPA(Horizontal Pod Autoscaler)が1ミリも動かない。
最初はHPAの設定ミスを疑いました。metrics-serverがおかしいのか、メトリクスが正しく取れていないのか、HPAのtargetを間違えたのか。しかし実際には、HPAそのものは設定どおりに動いていました。問題だったのは、HPAが見ているCPU使用率と、こちらがアプリコンテナ単体で見ていたCPU使用率が違っていたことです。
この現象は、アプリコンテナのCPU Requestをコスト削減のために極小(25mCoreなど)に設定し、一方でサイドカーには100mCore程度のRequestを割り当てていると、HPAのスケーリング計算上、アプリ側の高負荷が見えにくくなることで発生します。
この記事では、実際に直面したHPAの仕様によるスケール不全の仕組みと、実務上どう対応するのが現実的なのかを整理します。
サイドカーを入れたときのHPAの計算
HPAがCPU使用率(AverageUtilization)を見てスケールアウトを判断する際、アプリコンテナだけを見ているわけではありません。**「Podに同居するすべてのコンテナのRequest合計」に対して、「すべてのコンテナの実際のCPU使用量の合計」**が何%か、を計算して判断します。
ここが今回、一番ハマったポイントでした。具体的な数字で見た方が分かりやすいので、アプリ側はかなり攻めた設定、サイドカー側は手堅い設定にしたPodを考えてみます。
リソース設定
-
アプリコンテナ(軽量なAPI)
- CPU Request:
25m(0.025コア) - CPU Limit:
50m(0.05コア)
- CPU Request:
-
インジェクトされたサイドカー(例:istio-proxy や cloud-sql-proxy)
- CPU Request:
100m(0.1コア) - CPU Limit:
2000m(2.0コア)
- CPU Request:
HPAの設定
- 目標CPU使用率(
targetCPUUtilizationPercentage):70%
この状態でアクセスが急増し、アプリコンテナがフル稼働してLimitの 50m に張り付いたとします。アプリ側から見れば完全に限界で、処理が詰まり、スロットリングが発生し、レスポンスも遅くなっている状態です。普通に考えれば「そろそろPodを増やしてくれ」と思うところです。
一方、サイドカーはそこまで高負荷ではなく、プロキシ中継処理で 20m しか消費していないと仮定します。このとき、HPAがスケーリング判定に用いる「Pod全体のCPU使用率」は以下のようになります。
- 分母(Pod全体のRequest合計)
$$ 25\text{m} (\text{アプリ}) + 100\text{m} (\text{サイドカー}) = 125\text{m} $$
- 分子(Pod全体の実際の使用量合計)
$$ 50\text{m} (\text{アプリ}) + 20\text{m} (\text{サイドカー}) = 70\text{m} $$
- HPAの算出するCPU使用率
$$\frac{70\text{m}}{125\text{m}} = 56%$$
計算結果は 56% になり、目標値である 70% に届きません。アプリコンテナ単体ではリソース上限に達して応答遅延が発生しているのに、HPAから見るとスケールアウト条件を満たしていないことになります。
アプリ側のCPUグラフだけを見ていると、「100%まで使っているのになぜ増えないのか」とかなり混乱します。しかし、HPAが見ている分母にはサイドカーの 100m も入っています。その結果、アプリ単体の逼迫がPod全体の数字の中で薄められてしまいます。
これが、サイドカー同居時に極小Requestを設定した際に発生するスケール窒息のメカニズムです。
サービスメッシュ以外のサイドカー
この話をすると、最初は「Istio特有の問題なのでは」と思うかもしれません。しかし、この計算の歪みはIstioプロキシに限った話ではありません。アプリとは別のCPU Requestを持つコンテナが同じPodに入っていれば、基本的には同じことが起こり得ます。
1. データベース接続プロキシ(cloud-sql-proxyなど)
データベースとセキュアに通信するためのプロキシを、アプリと同じPodに配置する構成があります。こうした接続プロキシにはアプリとは独立したCPU Requestを設定する必要があり、必要なCPU量は接続数やI/O量などのワークロード特性によって変わります。
軽量なアプリでは、アプリ本体よりプロキシ側のRequestが大きくなるケースもあります。たとえばアプリが 25m、プロキシが 100m の構成では、プロキシのRequestがPod全体のRequestの8割を占めます。このようなRequest比率の偏りが、Pod単位でCPU使用率を見るHPAの判断に影響します。
2. ログ・メトリクス・トレースコレクター(otel-collector / Datadog Agent)
アプリが吐き出すログやトレースデータを転送するエージェントも同じです。急なトラフィックのスパイクに耐えるため、デフォルトのRequestが 100m 程度に設定されているケースも多くあります。
アプリとコレクターのCPU使用量が常に同じ比率で上がるわけではないため、「アプリだけが高負荷」という状態は普通に起こります。その場合も、Pod全体で平均化されたCPU使用率だけを見ると、アプリ側の逼迫が見えにくくなります。
3. シークレット管理エージェント(Vault Agentなど)
起動時に認証情報をマウントするエージェントも同様です。
要するに、問題なのはIstioやCloud SQL Auth Proxyといった製品そのものではありません。アプリ側のRequestを不釣り合いに小さく設計し、同じPod内にそれより大きなRequestを持つサイドカーがいることがポイントです。
この状態になると、Pod全体のRequestに対するサイドカーの割合が相対的に大きくなり、HPAがアプリ側の高負荷を拾いづらくなります。
サイドカーのRequestを削るリスク
ここまで分かると、「それならサイドカーのRequestを 10m くらいまで下げればいいのでは」と考えたくなります。確かに数字の上ではHPAの分母を小さくできるため、開発環境レベルであればそれでも問題なく動くことがあります。
ただ、本番環境でHPAを動かすためだけにサイドカーのRequestを削るのは、あまりきれいな解決策ではありません。
1. サポートとSLAの維持
たとえばマネージドなプロキシ(GKEのCloud Service MeshやAzureのマネージドIstioなど)のリソース設定を独自に削った場合、プロキシ起因の遅延や接続断が発生した際に、クラウドベンダーのサポート窓口で「推奨スペック未満での運用」として調査が難しくなるリスクがあります。
2. 自動アップグレード時の競合
マネージドサービスのアドオンは裏で自動更新されます。サイドカーの定義を独自に書き換えていると、アップグレードで設定がデフォルトに戻されたり、バージョンアップ後に必要な最低Requestを満たせず、起動に失敗したりする運用上の手間が増えます。
結局のところ、HPAの数字を合わせるためにインフラ側のコンテナを無理に削ると、別のところで運用コストを払うことになりがちです。インフラが提供する自動化や信頼性の恩恵を受けるのであれば、サイドカーのRequestサイズは原則としてそのまま受け入れ、そのうえでアプリ側やHPA側を調整する方が現実的です。
アプリのCPU Requestを増やす対応
HPAの仕様に従いつつ、スケーリングを正常に機能させる一番分かりやすい方法は、アプリコンテナ側のRequestをある程度まで引き上げることです。
たとえば、アプリのリソース設定を以下のように変更します。
-
アプリコンテナ(変更後)
- CPU Request:
100m(0.1コア) - CPU Limit:
200m(0.2コア)
- CPU Request:
Pod全体のRequest合計は 200m(アプリ 100m + サイドカー 100m)になります。
アプリの負荷が上がって 120m を消費し、サイドカーが中継で 20m を消費した局面を計算してみます。
-
実際の使用量:
120m+20m=140m -
HPAの算出するCPU使用率:
140m / 200m = 70%
これで全体のCPU使用率が 70% に達し、アプリがLimit(200m)に達して詰まる前に、HPAがスケールアウトをトリガーします。サイドカー側の設定を無理に変更する必要がなく、古いKubernetes環境でも使えるため、対応としてはかなり手堅い方法です。
ただし、ここで別の問題が出てきます。本来は 25m で十分なアプリなのに、HPAを正常に動かすためだけに 100m のRequestを与えることになるからです。Pod数が増えれば、この差はそのままノードの収容効率に効いてきます。
安定性を取るためとはいえ、FinOpsの観点から見ると少しもったいない設計です。
ContainerResourceによる個別コンテナの評価
そこで使えるのが、ContainerResource メトリクスソースです。
Kubernetes v1.30で正式に一般提供(GA)となったこの機能を使用すれば、Pod全体ではなく、**「特定のコンテナ単体のCPU使用率」**だけを評価してHPAを動かすことができます。
今回のケースであれば、「Pod全体ではなく app コンテナだけを見てスケールしてほしい」という指定ができます。
ContainerResourceの計算モデル
ContainerResourceを設定すると、HPAはサイドカーのRequest(100m)を無視し、アプリコンテナ app のRequest(25m)だけを計算の分母として扱います。
アプリが 50m を消費した段階で、HPAが算出する使用率は以下のようになります。
$$\frac{50\text{m} (\text{実際の使用量})}{25\text{m} (\text{アプリのRequest})} = 200%$$
Pod全体で見た場合は 56% だったものが、アプリだけを見ると 200% になります。ターゲットを 70% に設定していれば、サイドカーがどれだけアイドル状態であっても、アプリのピンポイントな高負荷を検知してスケールアウトできます。
これによって、アプリのRequestを 25m に抑えてノードあたりの集約率を高めつつ、サイドカーの負荷に邪魔されないオートスケールを実現できます。
考え方としては、「Requestを増やしてHPAの計算に合わせる」のではなく、HPAが見る対象そのものを、実際にスケールさせたいコンテナに合わせるということです。
主要なマネージド環境であれば、現行バージョンですでに標準サポートされています。
ContainerResourceにもデメリットはある
ここまで見ると、Pod単位のCPU使用率をやめて、すべて ContainerResource にすればよいようにも見えます。しかし、実際にはこれも万能ではなく、運用上のトレードオフがあります。
まず、HPAが特定のコンテナ名に依存するようになります。たとえばHPAで app を指定している状態で、Deployment側のコンテナ名を application に変更した場合、HPA側も合わせて変更する必要があります。ローリングアップデート中は新旧Podが混在するため、コンテナ名を変更するような更新ではHPAとの整合性も考えなければなりません。
また、指定したコンテナが存在しないPodは、その ContainerResource の計算対象から外れます。普段から全Podが同じDeploymentテンプレートで動いている分には大きな問題になりにくいものの、ローリングアップデートなどで一時的にPodごとのコンテナ構成が異なる場合には注意が必要です。
もう一つ重要なのが、アプリコンテナだけを評価すると、今度はサイドカー側のボトルネックを見落とす可能性があることです。アプリのCPUには余裕があるのに、接続数の増加などによってプロキシ側が先に限界へ達することは十分あり得ます。
そのため、今回のような構成では app だけを見るのではなく、cloud-sql-proxy も別の ContainerResource として指定しておく方が安全です。アプリとプロキシをそれぞれ独立したメトリクスとして評価すれば、どちらかが先にボトルネックになった場合でもスケールアウトできます。
さらに、これは ContainerResource 固有の問題ではありませんが、そもそもCPU使用率がアプリのボトルネックを正しく表しているとは限りません。DBのConnection Pool、キュー長、外部API待ち、RPSなどが先に限界になるアプリでは、CPUだけをどれだけ細かく監視しても適切なタイミングでスケールできないことがあります。その場合は、Custom MetricsやExternal Metricsを使って、アプリの性質に合った指標をHPAに渡す必要があります。
つまり ContainerResource はPod単位HPAの単純な上位互換ではありません。「どのコンテナをスケール判断の主体として扱うか」が明確な構成では非常に有効ですが、そのぶんDeploymentの構成とHPAの設定をセットで管理する必要がある、というのが実際のところです。
構成の再現(YAMLマニフェスト)
以下は、アプリコンテナ(app)とデータベースプロキシ(cloud-sql-proxy)の双方を並列で評価する、ContainerResource型HPAとDeploymentのサンプルです。
アプリだけを見るのではなく、プロキシ側も別のターゲットで評価します。これによって、どちらが先に限界に達してもスケールアウトできる構成にしています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: message-shuttle-api
namespace: target-namespace
spec:
replicas: 2
selector:
matchLabels:
app: message-shuttle-api
template:
metadata:
labels:
app: message-shuttle-api
spec:
containers:
# 1. アプリケーションコンテナ(25m設定)
- name: app
image: gcr.io/your-project-id/message-shuttle-api:v1.0.0
resources:
requests:
cpu: "25m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
ports:
- containerPort: 8080
# 2. データベースプロキシサイドカー(Google Cloud SQL Auth Proxy)
- name: cloud-sql-proxy
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.0
args:
- "--port=5432"
- "your-project-id:us-central1:your-database-instance"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: message-shuttle-hpa
namespace: target-namespace
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: message-shuttle-api
minReplicas: 2
maxReplicas: 10
metrics:
# -------------------------------------------------------------
# アプリとプロキシ双方のコンテナCPUを並列で評価
# -------------------------------------------------------------
- type: ContainerResource
containerResource:
name: cpu
container: app # アプリコンテナ単体のCPU使用率をターゲット(70%)に指定
target:
type: Utilization
averageUtilization: 70
- type: ContainerResource
containerResource:
name: cpu
container: cloud-sql-proxy # プロキシサイドカーのCPU使用率も並列評価(80%)
target:
type: Utilization
averageUtilization: 80
クラウドにおけるデフォルト仕様の共通性
この仕様の罠と、ContainerResourceによる解決策は、特定のクラウドだけの話ではありません。EKS、AKS、GKEのどれであっても、Kubernetes上で同じようなPod構成を取れば基本的な考え方は共通です。
Azure(AKS + Service Mesh / サイドカープロキシ)
AKSでも、サービスメッシュのプロキシやテレメトリ収集エージェントなどをアプリと同一Podに配置する構成では、同じ問題が発生します。アプリ本体に比べてサイドカーのCPU Requestが大きい場合、Pod単位のCPU使用率ではアプリの高負荷が相対的に見えにくくなります。
この場合も、利用しているKubernetesバージョンが対応していれば ContainerResource を使い、アプリコンテナ単体のCPU使用率をHPAのスケーリング指標にすることで影響を切り離せます。
AWS(EKS + Datadog / ADOT)
ADOT(AWS Distro for OpenTelemetry)やDatadog Agentも、100m 〜 250m のRequestを必要とします。特にAWS Fargate上で動かす場合、アプリ本体よりサイドカーのRequestの方が大きい構成も珍しくありません。
そうなると、Pod全体のRequestに占めるサイドカーの比率が大きくなり、アプリ側の負荷上昇がHPAから見えにくくなります。これもContainerResourceでコンテナ単位に評価を分ければ回避できます。
まとめ
今回一番厄介だったのは、個々の設定だけを見ると、どれもそれほどおかしく見えないことでした。アプリのRequestを小さくする。サイドカーには必要なRequestを確保する。CPU使用率70%でHPAを動かす。それぞれ単体で見れば、どれも普通の設定です。
ところが、この3つを組み合わせると、**「アプリは限界なのに、HPAから見るとまだスケール条件に達していない」**という状態が生まれます。CPU Requestを単なるコストやスケジューリングのための数字として見ていると、この関係にはなかなか気づけません。
HPAで AverageUtilization を使う場合、Requestはそのままオートスケールの基準になります。そのため、サイドカーを含むPodでは「アプリのRequestをどこまで削れるか」をアプリ単体だけで決めるのではなく、Pod全体の構成まで含めて考える必要があります。
対応としては、Pod全体のRequestバランスを見ながらアプリ側をサイジングする方法と、ContainerResource を使ってアプリとサイドカーの評価を明示的に分ける方法があります。後者はコスト効率を維持しやすい一方で、コンテナ名への依存や構成変更時の運用負荷が増えるため、どちらが正解かは環境によって変わります。
大事なのは、HPAが実際に何を見てスケールを判断しているのかを理解したうえでRequestを決めることです。
アプリのCPUグラフが100%に張り付いているのにHPAが静かなままだったら、HPAそのものを疑う前に、まずPod内のサイドカーとRequestの比率を見てみる。今回のケースでは、それが原因を見つける一番の近道でした。