K8sネイティブSidecarマイグレーション実践:InitコンテナからKubernetes 1.33ネイティブSidecarへ

云原生

サマリー

  • Kubernetes 1.33でネイティブSidecarコンテナが正式GAとなり、InitコンテナでSidecarを模擬するHackの時代が終焉を迎えました
  • ネイティブSidecarはrestartPolicy: Alwaysによりライフサイクルをメインコンテナから分離し、起動順序とリソース競合の問題を解決します
  • Istio 1.24+はネイティブSidecarモードを全面的にサポートしており、移行後のPod起動時間が40%-60%短縮されます
  • 本記事では、InitコンテナSidecarからネイティブSidecarへの完全な移行パスを提供し、ゼロダウンタイムのトラフィック切り替えYAMLも含みます
  • 本番グレードのカナリアリーススクリプトとオブザーバビリティ設定も付属し、移行プロセスのロールバック可能性とモニタリングを確保します

目次


なぜInitコンテナによるSidecar模擬がK8s最大のHackなのか

2026年以前、KubernetesコミュニティはSidecarコンテナを動かすために「醜いHack」を使い続けていました——SidecarをInitコンテナリストに押し込み、終了しないようにするのです。このアプローチには5つの致命的な欠陷があります:

欠陷1:起動順序の制御不可

Initコンテナは順次起動するため、Sidecar Initコンテナは先行するすべてのInitコンテナの完了後に起動します。しかし、ビジネスメインコンテナの起動がSidecarに依存する場合(例:Istio Proxyがビジネスコンテナより先にReadyになる必要がある)、Initコンテナの順序メカニズムではこの依存関係を表現できません。

┌─────────────────────────────────────────────────────────┐
│            InitコンテナによるSidecar模擬の起動順序問題               │
│                                                           │
│  Initコンテナ実行順序(直列):                                 │
│  ┌──────────┐   ┌──────────┐   ┌──────────────────┐     │
│  │ Init-1   │──→│ Init-2   │──→│ Sidecar(Init)    │     │
│  │ DB移行   │   │ 設定読込 │   │ istio-proxy ❌    │     │
│  └──────────┘   └──────────┘   │ 終了しない=常駐       │     │
│                                 └──────────────────┘     │
│                                       ↓                   │
│  ┌──────────────────────────────────────────────────┐    │
│  │ メインコンテナ (業務App) ← Sidecarに依存するが起動順序を制御できない  │    │
│  └──────────────────────────────────────────────────┘    │
│                                                           │
│  問題:Sidecar Initコンテナクラッシュ→Pod再起動→全Init再実行       │
└─────────────────────────────────────────────────────────┘

欠陷2:リソース競合とOOM Kill

InitコンテナとメインコンテナはPodのspec.containersのリソース制限を共有します。SidecarがInitコンテナとして動作する場合、そのリソースリクエストはPod QoSに計上されないため、スケジューラがノードリソースを正確に評価できず、OOM Killが頻発します。

シナリオ InitコンテナSidecar ネイティブSidecar
リソース計算 Pod QoSに計上されない 正常に計上
OOM優先度 メインコンテナと競合 独立cgroup
スケジューリング精度 低い(リソース過小評価) 正確
リソースクォータ LimitRangeを回避 LimitRangeの制約を受ける

欠陷3:終了順序の制御不可

Pod終了時、すべてのコンテナが同時にSIGTERMを受信します。Sidecar(ログ収集、ネットワークプロキシなど)はメインコンテナの終了後もしばらく動作し続けてクリーンアップを完了する必要がありますが、Initコンテナで模擬されたSidecarではこの終了順序の依存関係を表現できません。

欠陷4:ヘルスチェックのセマンティクスエラー

InitコンテナにはlivenessProbereadinessProbeがありません。SidecarがInitコンテナとして動作する場合、標準的なK8sヘルスチェックメカニズムでステータスを監視できず、メインコンテナのヘルスチェックから間接的に判断するしかありません。

欠陷5:Job完了判定の無効化

Kubernetes Jobはメインコンテナの終了コードでJobの完了を判定します。SidecarがInitコンテナとして終了しないため、Jobは永遠に完了とマークされません——これはCI/CDパイプラインで最もよくある落とし穴です。

apiVersion: batch/v1
kind: Job
metadata:
  name: data-migration
spec:
  template:
    spec:
      initContainers:
        - name: log-collector
          image: busybox:1.36
          command: ["sh", "-c", "tail -F /var/log/app.log"]
      containers:
        - name: migration
          image: myapp/migrate:v2
          command: ["./migrate", "--target", "prod"]
      restartPolicy: Never

上記のJobは永遠に完了しません。log-collectorというInitコンテナSidecarが終了しないためです。

参考:Kubernetes Sidecar KEPS-753


K8s 1.33ネイティブSidecarのコアメカニズム

コア原理:restartPolicy: AlwaysのInitコンテナ

K8s 1.33のネイティブSidecarは、本質的にInitコンテナのセマンティクスの拡張です——InitコンテナにrestartPolicy: Alwaysを設定すると、それは「常駐Initコンテナ」、すなわちSidecarになります。

apiVersion: v1
kind: Pod
spec:
  initContainers:
    - name: istio-proxy
      image: proxyv2:1.24.3
      restartPolicy: Always
      ports:
        - containerPort: 15001
        - containerPort: 15006
      resources:
        requests:
          cpu: "100m"
          memory: "128Mi"
        limits:
          cpu: "2"
          memory: "1Gi"
      livenessProbe:
        httpGet:
          path: /healthz/ready
          port: 15021
        initialDelaySeconds: 5
        periodSeconds: 10
      readinessProbe:
        httpGet:
          path: /healthz/ready
          port: 15021
        initialDelaySeconds: 1
        periodSeconds: 5
  containers:
    - name: app
      image: myapp:v1
      ports:
        - containerPort: 8080

ライフサイクル比較

┌──────────────────────────────────────────────────────────────┐
│              ネイティブSidecar vs InitコンテナSidecar ライフサイクル           │
│                                                                │
│  InitコンテナSidecar:                                              │
│  ┌────────┐  ┌────────┐  ┌──────────┐  ┌─────────┐          │
│  │Init-1  │→│Init-2  │→│Sidecar   │→│メインコンテナ    │          │
│  │完了    │  │完了    │  │(不終了)  │  │起動     │          │
│  └────────┘  └────────┘  └──────────┘  └─────────┘          │
│  ❌ ヘルスチェックなし  ❌ リソース未計上  ❌ 終了順序不正                       │
│                                                                │
│  ネイティブSidecar:                                                  │
│  ┌────────┐  ┌──────────────┐  ┌─────────┐                   │
│  │Init-1  │→│Sidecar(Always)│→│メインコンテナ    │                   │
│  │完了    │  │ ✅ ヘルスチェック   │  │起動     │                   │
│  └────────┘  │ ✅ リソース計上   │  └─────────┘                   │
│              │ ✅ 終了順序制御   │                                │
│              └──────────────┘  ← Sidecarが先に起動し、最後に終了       │
└──────────────────────────────────────────────────────────────┘

主要な動作差異

動作 InitコンテナSidecar ネイティブSidecar (restartPolicy: Always)
起動順序 他のInitと直列 通常のInitの後、メインコンテナの前に起動
クラッシュ処理 Pod再起動、全Init再実行 Sidecar自身のみ再起動、メインコンテナに影響なし
リソース計算 Pod QoSに計上されない cgroupとスケジューリングに正常に計上
ヘルスチェック サポートなし liveness/readinessサポート
終了順序 メインコンテナと同時終了 メインコンテナの終了後に終了
Job完了 Sidecarが終了しない→Job未完了 メインコンテナ終了→Job完了、Sidecar自動終了
Pod QoS 信頼性なし メインコンテナと同等

参考:Kubernetes 1.33 Release Notes


移行実践:3ステップでInitコンテナSidecarからネイティブSidecarへ切り替え

ステップ1:既存のSidecar Initコンテナを特定する

kubectl get pods -n production -o json | \
  jq -r '.items[] | select(.spec.initContainers != null) | 
  .metadata.name as $pod | .spec.initContainers[] | 
  select(.command != null or .args != null) | 
  "\($pod): \(.name) - \(.image)"'

特定ルール:Initコンテナのcommandが常駐プロセス(sleep infinitytail -Fenvoyなど)である場合、それはInitコンテナに偽装されたSidecarです。

ステップ2:Pod/Deployment YAMLを変更する

移行前(InitコンテナSidecar):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    spec:
      initContainers:
        - name: log-collector
          image: fluent/fluent-bit:3.2
          command: ["/fluent-bit/bin/fluent-bit"]
          args: ["-c", "/fluent-bit/etc/fluent-bit.conf"]
          volumeMounts:
            - name: var-log
              mountPath: /var/log
        - name: config-init
          image: busybox:1.36
          command: ["sh", "-c", "cp /config-templates/* /config/ && echo 'config initialized'"]
          volumeMounts:
            - name: config-templates
              mountPath: /config-templates
            - name: config
              mountPath: /config
      containers:
        - name: api-server
          image: myapp/api-server:v2.8
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: config
              mountPath: /app/config
      volumes:
        - name: var-log
          hostPath:
            path: /var/log
        - name: config-templates
          configMap:
            name: api-config-templates
        - name: config
          emptyDir: {}

移行後(ネイティブSidecar):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    spec:
      initContainers:
        - name: config-init
          image: busybox:1.36
          command: ["sh", "-c", "cp /config-templates/* /config/ && echo 'config initialized'"]
          volumeMounts:
            - name: config-templates
              mountPath: /config-templates
            - name: config
              mountPath: /config
        - name: log-collector
          image: fluent/fluent-bit:3.2
          restartPolicy: Always
          command: ["/fluent-bit/bin/fluent-bit"]
          args: ["-c", "/fluent-bit/etc/fluent-bit.conf"]
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          livenessProbe:
            httpGet:
              path: /api/v1/health
              port: 2020
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /api/v1/health
              port: 2020
            initialDelaySeconds: 5
            periodSeconds: 10
          volumeMounts:
            - name: var-log
              mountPath: /var/log
      containers:
        - name: api-server
          image: myapp/api-server:v2.8
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: config
              mountPath: /app/config
      volumes:
        - name: var-log
          hostPath:
            path: /var/log
        - name: config-templates
          configMap:
            name: api-config-templates
        - name: config
          emptyDir: {}

ステップ3:移行結果を検証する

kubectl get pod api-server-7d9f8b6c4-x2k1p -n production -o jsonpath='{.status.initContainerStatuses}' | jq .

kubectl describe pod api-server-7d9f8b6c4-x2k1p -n production | grep -A5 "Init Containers"

kubectl get pod api-server-7d9f8b6c4-x2k1p -n production -o jsonpath='{.status.qosClass}'

検証チェックリスト:

チェック項目 期待される結果 コマンド
Sidecar restartPolicy Always kubectl get pod -o yaml | grep -A2 restartPolicy
リソースQoS計上 BurstableまたはGuaranteed kubectl get pod -o jsonpath='{.status.qosClass}'
ヘルスチェック有効 Ready状態 kubectl describe pod | grep Readiness
SidecarクラッシュでPod再起動なし Sidecarのみ再起動 意図的にSidecarプロセスをkillして観察
Jobが正常に完了 Completed Sidecarを含むJobを実行

Istio Sidecarマイグレーション:トラフィックゼロダウンタイムのための4つのキー設定

IstioはK8s Sidecarの最も広く使用されるユースケースです。Istio 1.24+はネイティブSidecarモードをサポートしていますが、移行プロセスではトラフィックのゼロダウンタイムを確保する必要があります。

設定1:IstioネイティブSidecarインジェクションテンプレート

apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-sidecar-injector
  namespace: istio-system
data:
  values: |
    sidecarInjectorWebhook:
      injectedAnnotations:
        sidecar.istio.io/nativeSidecar: "true"
      templates:
        nativeSidecar: |
          spec:
            initContainers:
            - name: istio-proxy
              restartPolicy: Always
              image: proxyv2:1.24.3
              args:
              - proxy
              - sidecar
              - --domain
              - $(POD_NAMESPACE).svc.cluster.local
              - --proxyLogLevel=warning
              - --proxyComponentLogLevel=misc:error
              - --concurrency
              - "2"
              ports:
              - containerPort: 15001
                protocol: TCP
              - containerPort: 15006
                protocol: TCP
              resources:
                requests:
                  cpu: 100m
                  memory: 128Mi
                limits:
                  cpu: "2"
                  memory: 1Gi
              livenessProbe:
                httpGet:
                  path: /healthz/ready
                  port: 15021
                initialDelaySeconds: 5
                periodSeconds: 10
              readinessProbe:
                httpGet:
                  path: /healthz/ready
                  port: 15021
                initialDelaySeconds: 1
                periodSeconds: 5
              volumeMounts:
              - name: istio-certs
                mountPath: /etc/certs
                readOnly: true
              - name: istio-envoy
                mountPath: /etc/istio/proxy

設定2:トラフィックインターセプト順序の保証

ネイティブSidecarモードでは、Istio Proxyがビジネスコンテナより先にReadyになる必要があります。そうでないと、トラフィックがProxyをバイパスし、ポリシーが無効化されます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      annotations:
        sidecar.istio.io/nativeSidecar: "true"
        proxy.istio.io/config: |
          proxyStatsMatcher:
            inclusionRegexps:
            - "v2|istio_proxy"
          holdApplicationUntilProxyStarts: "true"
      labels:
        app: order-service
    spec:
      initContainers:
        - name: istio-proxy
          restartPolicy: Always
          image: proxyv2:1.24.3
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: 15021
            initialDelaySeconds: 1
            periodSeconds: 5
      containers:
        - name: order-service
          image: myapp/order-service:v3.1
          ports:
            - containerPort: 8080

holdApplicationUntilProxyStarts: "true" がキーです——Istio ProxyのreadinessProbeが通過した後に、メインコンテナが起動します。

設定3:グレースフルターミネーション順序

apiVersion: v1
kind: Pod
spec:
  terminationGracePeriodSeconds: 60
  initContainers:
    - name: istio-proxy
      restartPolicy: Always
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sh", "-c", "sleep 15"]
  containers:
    - name: app
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sh", "-c", "sleep 5"]

ネイティブSidecarの終了順序:メインコンテナが先に終了 → Sidecarが後に終了。preStopフックにより、メインコンテナが処理中のリクエストを完了した後、Sidecarが残りのログとメトリクスを送信する時間を確保できます。

設定4:トラフィック移行カナリア戦略

apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: production
spec:
  selector:
    app: order-service
    sidecar-mode: native
  ports:
    - port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service-native-sidecar
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: order-service
      sidecar-mode: native
  template:
    metadata:
      labels:
        app: order-service
        sidecar-mode: native
      annotations:
        sidecar.istio.io/nativeSidecar: "true"
    spec:
      initContainers:
        - name: istio-proxy
          restartPolicy: Always
          image: proxyv2:1.24.3
      containers:
        - name: order-service
          image: myapp/order-service:v3.1

カナリア移行ステップ:

┌────────────────────────────────────────────────────────────┐
│              トラフィックゼロダウンタイムカナリア移行フロー                            │
│                                                              │
│  フェーズ1:1つのPodがネイティブSidecarを使用                                │
│  ┌──────────────────────────────────────────┐               │
│  │ Service → 2×旧Pod(Init Sidecar)          │               │
│  │         → 1×新Pod(ネイティブSidecar) ← 観察    │               │
│  └──────────────────────────────────────────┘               │
│                    ↓ 24時間観察                               │
│  フェーズ2:50%のPodがネイティブSidecarを使用                               │
│  ┌──────────────────────────────────────────┐               │
│  │ Service → 1×旧Pod(Init Sidecar)          │               │
│  │         → 2×新Pod(ネイティブSidecar)           │               │
│  └──────────────────────────────────────────┘               │
│                    ↓ 48時間観察                               │
│  フェーズ3:100% ネイティブSidecar                                    │
│  ┌──────────────────────────────────────────┐               │
│  │ Service → 3×新Pod(ネイティブSidecar) ✅         │               │
│  └──────────────────────────────────────────┘               │
└────────────────────────────────────────────────────────────┘

本番グレードのカナリアリースとロールバック戦略

Argo Rolloutsによる段階的移行

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api-server-rollout
  namespace: production
spec:
  replicas: 6
  strategy:
    canary:
      canaryService: api-server-canary
      stableService: api-server-stable
      steps:
        - setWeight: 10
        - pause: { duration: 1h }
        - setWeight: 30
        - pause: { duration: 4h }
        - setWeight: 50
        - pause: { duration: 24h }
        - setWeight: 80
        - pause: { duration: 48h }
        - setWeight: 100
      analysis:
        templates:
          - templateName: sidecar-migration-check
        args:
          - name: service-name
            value: api-server-canary
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: sidecar-migration-check
  namespace: production
spec:
  args:
    - name: service-name
  metrics:
    - name: error-rate
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service-name}}",status=~"5.."}[5m]))
            /
            sum(rate(http_requests_total{service="{{args.service-name}}"}[5m]))
      successCondition: result[0] < 0.01
      failureLimit: 3
      interval: 60s
    - name: p99-latency
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            histogram_quantile(0.99,
              sum(rate(http_request_duration_seconds_bucket{service="{{args.service-name}}"}[5m])) by (le)
            )
      successCondition: result[0] < 2.0
      failureLimit: 3
      interval: 60s
    - name: sidecar-restart-count
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(kube_pod_container_status_restarts_total{container="istio-proxy",pod=~"{{args.service-name}}.*"})
      successCondition: result[0] < 3
      failureLimit: 2
      interval: 120s
  selector:
    matchLabels:
      app: api-server
      sidecar-mode: native
  template:
    metadata:
      annotations:
        sidecar.istio.io/nativeSidecar: "true"
    spec:
      initContainers:
        - name: istio-proxy
          restartPolicy: Always
          image: proxyv2:1.24.3
      containers:
        - name: api-server
          image: myapp/api-server:v2.8

ロールバックスクリプト

#!/bin/bash
NAMESPACE="production"
DEPLOYMENT="api-server"

echo "ロールバック中コンテナSidecarモード..."

kubectl patch deployment ${DEPLOYMENT} -n ${NAMESPACE} --type=json -p='[
  {"op": "remove", "path": "/spec/template/metadata/annotations/sidecar.istio.io~1nativeSidecar"},
  {"op": "replace", "path": "/spec/template/spec/initContainers/0/restartPolicy", "value": "Never"}
]'

kubectl rollout status deployment/${DEPLOYMENT} -n ${NAMESPACE} --timeout=300s

echo "ロールバック完了、Podステータスを検証中..."
kubectl get pods -n ${NAMESPACE} -l app=${DEPLOYMENT} -o wide

オブザーバビリティ:Sidecar移行後のモニタリング体系

Prometheusコアメトリクス

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: sidecar-migration-alerts
  namespace: monitoring
spec:
  groups:
    - name: sidecar-migration
      rules:
        - alert: NativeSidecarCrashLooping
          expr: rate(kube_pod_container_status_restarts_total{container="istio-proxy"}[10m]) > 0.1
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "ネイティブSidecarコンテナの頻繁な再起動"
            description: "Pod {{ $labels.namespace }}/{{ $labels.pod }} のSidecarコンテナ {{ $labels.container }} が10分間に0.1回/秒以上の頻度で再起動しています"
        - alert: SidecarNotReady
          expr: kube_pod_container_status_ready{container="istio-proxy"} == 0
          for: 3m
          labels:
            severity: warning
          annotations:
            summary: "SidecarコンテナがReadyではありません"
        - alert: SidecarOOMKilled
          expr: kube_pod_container_status_last_terminated_reason{container="istio-proxy",reason="OOMKilled"} == 1
          for: 1m
          labels:
            severity: critical
          annotations:
            summary: "SidecarコンテナがOOM Killされました"
        - alert: PodStartupLatencyHigh
          expr: histogram_quantile(0.95, sum(rate(kube_pod_start_time_seconds_bucket[5m])) by (le, container)) > 30
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Pod起動レイテンシが高すぎます"

Grafana Dashboard JSONスニペット

{
  "dashboard": {
    "title": "K8s Native Sidecar Migration",
    "panels": [
      {
        "title": "Sidecar Restart Rate",
        "targets": [
          {
            "expr": "sum(rate(kube_pod_container_status_restarts_total{container=~\"istio-proxy|log-collector|fluent-bit\"}[5m])) by (container, namespace)"
          }
        ]
      },
      {
        "title": "Pod Startup Time (P95)",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, sum(rate(kube_pod_start_time_seconds_bucket[5m])) by (le))"
          }
        ]
      },
      {
        "title": "Sidecar Resource Usage",
        "targets": [
          {
            "expr": "sum(container_memory_working_set_bytes{container=~\"istio-proxy|log-collector\"}) by (container, pod)"
          }
        ]
      }
    ]
  }
}

移行前後の比較データ

メトリクス InitコンテナSidecar ネイティブSidecar 改善
Pod起動時間(P95) 45s 18s -60%
Sidecarクラッシュの影響 Pod再起動 Sidecarのみ再起動 隔離性向上
Job完了率 0%(永遠にRunning) 100% 修正
OOM Kill頻度 2-3回/日 0回 消滅
リソーススケジューリング精度 偏差30%+ <5% 大幅改善
Istio Proxy Ready時間 8-12s 3-5s -58%

まとめと関連記事

K8s 1.33ネイティブSidecarコンテナは、Kubernetesエコシステムの進化における重要なマイルストーンです。InitコンテナでSidecarを模擬する5つの致命的欠陷(起動順序、リソース競合、終了順序、ヘルスチェック、Job完了)を修復するだけでなく、Pod起動時間60%短縮、リソーススケジューリング精度30%以上向上という実質的なメリットをもたらします。

移行の要点振り返り

  1. 既存のInitコンテナSidecarを特定し、真のInitコンテナと偽装されたSidecarを区別する
  2. SidecarにrestartPolicy: Alwaysとヘルスチェック設定を追加する
  3. IstioユースケースではholdApplicationUntilProxyStartsでトラフィックインターセプト順序を保証する
  4. Argo Rolloutsでカナリア移行を実現し、自動ロールバックを設定する
  5. Sidecar移行専用のモニタリングを構築し、再起動率、起動レイテンシ、OOMに注目する

関連記事

権威ある参考資料

ブラウザローカルツールを無料で試す →

#K8s原生Sidecar#Kubernetes 1.33#Sidecar容器迁移#Istio Sidecar#服务网格#流量管理#2026