🏍️

おや、Kuberenetes サイドカーのようすが...

に公開

はじめに

以前、TROCCO の実行ログの保存先を DB から S3 へ移行した話をしました。
https://zenn.dev/primenumber/articles/179cd19b6c9e79

TROCCO のジョブは Kubernetes の Job に、転送を行うメインコンテナである Rails と、ログを S3 に転送するサイドカーコンテナである Vector をデプロイする構成になっています。

ここで利用しているサイドカーパターンですが、K8s 1.28 で Alpha 版になった SidecarContainers 機能ではなく、コンテナを明示的に2つデプロイする構成でした。
そのため、Rails コンテナの終了が Vector コンテナに伝播せず、Vector が永遠に起動し続ける課題がありました。
(詳細は以前の記事の サイドカーの問題点 にまとまっています)
この問題の回避策として、CronJob で定期的に起動し続けている Vector を Kill して回っていました。

TROCCO は EKS で稼働しているため、Alpha 版の機能を利用することができませんでした。
しかし K8s 1.29 から Beta 版になったため、EKS でも 1.29 以上であれば SidecarContainers を利用できるようになりました。
これにより課題の解消と、構成管理の簡略化を実現できたので紹介します。

SidecarContainers とは

Kubernetes v1.28: Introducing native sidecar containers

サイドカーがネイティブでサポートされておらず先述のような不便がありましたが、それを解消する機能となっています。
TROCCO で言うと、以下のように課題が解消されました。

概要

サイドカーコンテナのプロセスの終了が課題にならない構成もあるとは思います。
例えば Nginx + PHP のような Web アプリケーションの構成が考えられます。
一般的に Web アプリケーションは K8s Deployment を利用するため、メインコンテナとサイドカーコンテナは異常終了やローリングアップデートなどで強制的に終了します。

一方で TROCCO は K8s Job を利用しているため、転送ジョブが成功し、メインコンテナが正常終了するとサイドカーが起動しっぱなしで放置されていました。
SidecarContainers を利用することで、K8s Job であってもメインコンテナの正常終了がサイドカーコンテナに伝播するため、この問題から解放されます。

使い方

これまでは spec.containers にメインコンテナとサイドカーコンテナをそれぞれ記述していました。
SidecarContainers は、spec.initContainers にサイドカーコンテナを記述することで利用できます。
この時、restartPolicyAlways に設定する必要があります。

Alpine (メインコンテナ) で logging という文字列を /var/log/stdout.txt に1秒ごとに1分間書き込み、Vector (サイドカーコンテナ) で標準出力に吐き出す例です。
それぞれのコンテナ定義箇所を抜き出しています。
全体像は長いので折りたたんでいます。

  • Alpine
apiVersion: batch/v1
kind: Job
metadata:
  name: myjob
spec:
  template:
    spec:
      containers:
        - name: myjob
          image: alpine:latest
          command: ['sh', '-c', 'i=0; while [ "$i" -lt 60 ]; do echo "logging" >> /var/log/stdout.txt; sleep 1; i=$((i+1)); done; date >> /var/log/stdout.txt; exit 0']
          volumeMounts:
            - name: log
              mountPath: /var/log
  • Vector
      initContainers:
        - name: vector
          image: "timberio/vector:0.42.0-distroless-libc"
          restartPolicy: Always
          args:
            - --config-dir
            - /etc/vector/
検証に利用したマニフェスト
apiVersion: batch/v1
kind: Job
metadata:
  name: myjob
spec:
  template:
    spec:
      containers:
        - name: myjob
          image: alpine:latest
          command: ['sh', '-c', 'i=0; while [ "$i" -lt 60 ]; do echo "logging" >> /var/log/stdout.txt; sleep 1; i=$((i+1)); done; date >> /var/log/stdout.txt; exit 0']
          volumeMounts:
            - name: log
              mountPath: /var/log
      initContainers:
        - name: vector
          image: "timberio/vector:0.42.0-distroless-libc"
          restartPolicy: Always
          args:
            - --config-dir
            - /etc/vector/
          lifecycle:
            preStop:
              sleep:
                seconds: 20
          volumeMounts:
            - name: data
              mountPath: "/vector-data-dir"
            - name: config
              mountPath: "/etc/vector/"
              readOnly: true
            - name: log
              mountPath: /var/log
      restartPolicy: Never
      volumes:
        - name: data
          emptyDir: {}
        - name: config
          configMap:
            name: native-vector
        - name: log
          emptyDir: {}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: native-vector
data:
  agent.yaml: |
    data_dir: /vector-data-dir
    api:
      enabled: false
    sources:
      file:
        type: "file"
        include: [ "/var/log/stdout.txt" ]
    sinks:
      out:
        type: "console"
        inputs:
          - "file"
        encoding:
          codec: "text"

動作確認

Kind の 1.32.2 で動かしています。

apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
name: kind-sleep-action-dev
product: kind
registry: sleep-action-registry
kindV1Alpha4Cluster:
  nodes:
    - role: control-plane
  featureGates:
    PodLifecycleSleepAction: true

先述のマニフェストを kubectl apply -f example.yaml でデプロイして、stern myjob でログを確認すると以下のように出力されていることが確認できます[1]
Alpine で1分間のループ後に時刻を吐き出していますが、その後 Alpine (myjob) のプロセスが終了し、Vector は LifecycleHook で指定した時間経過後に終了しています。

$ stern myjob-hbj9k
+ myjob-hbj9k › myjob
+ myjob-hbj9k › vector
myjob-hbj9k vector 2025-03-27T06:43:16.625203Z  INFO vector::app: Log level is enabled. level="info"
myjob-hbj9k vector 2025-03-27T06:43:16.626164Z  INFO vector::app: Loading configs. paths=["/etc/vector"]
myjob-hbj9k vector 2025-03-27T06:43:16.628252Z  INFO vector::topology::running: Running healthchecks.
myjob-hbj9k vector 2025-03-27T06:43:16.628304Z  INFO vector: Vector has started. debug="false" version="0.42.0" arch="aarch64" revision="3d16e34 2024-10-21 14:10:14.375255220"
myjob-hbj9k vector 2025-03-27T06:43:16.628303Z  INFO vector::topology::builder: Healthcheck passed.
myjob-hbj9k vector 2025-03-27T06:43:16.628317Z  INFO vector::app: API is disabled, enable by setting `api.enabled` to `true` and use commands like `vector top`.
myjob-hbj9k vector 2025-03-27T06:43:16.628377Z  INFO source{component_kind="source" component_id=file component_type=file}: vector::sources::file: Starting file server. include=["/var/log/stdout.txt"] exclude=[]
myjob-hbj9k vector 2025-03-27T06:43:16.628583Z  INFO source{component_kind="source" component_id=file component_type=file}:file_server: file_source::checkpointer: Attempting to read legacy checkpoint files.
myjob-hbj9k vector 2025-03-27T06:43:22.802285Z  INFO source{component_kind="source" component_id=file component_type=file}:file_server: vector::internal_events::file::source: Found new file to watch. file=/var/log/stdout.txt
myjob-hbj9k vector logging
myjob-hbj9k vector logging

(省略)

myjob-hbj9k vector logging
myjob-hbj9k vector logging
myjob-hbj9k vector Thu Mar 27 06:44:21 UTC 2025
- myjob-hbj9k › myjob
myjob-hbj9k vector 2025-03-27T06:44:41.725681Z  INFO vector::signal: Signal received. signal="SIGTERM"
myjob-hbj9k vector 2025-03-27T06:44:41.725865Z  INFO vector: Vector has stopped.
- myjob-hbj9k › vector

まとめ

SidecarContainers を利用することで、課題であったサイドカー生き残り問題が解消されました!
それに伴い、生き残ったサイドカーを消して回る CronJob が不要になり、構成管理についても簡略化されました!

おまけ

サイドカーコンテナの終了を遅延させたい

Vector は設定ファイルで指定した秒数おきにログファイルの差分を S3 にアップロードしています。
つまり、メインコンテナの終了後、少なくとも指定秒以上は Vector が生きている必要があります。
そうでないとログが欠損してしまうためです。

SidecarContainers では LifecycleHook を利用することができるため、preStop を利用して Vector の終了を遅延させています。

PodLifecycleSleepAction

LifecycleHook を利用して、指定した時間遅延させる時に真っ先に思い浮かぶのが、lifecycle.preStop.exec.commandsleep コマンドを実行する方法です。
TROCCO でも、Vector コンテナで sleep を実行することでログの欠損を回避していました。

しかし、K8s 1.30 から sleep による遅延をより手軽に行う機能が Beta 版として追加されました。
それが PodLifecycleSleepAction です。

プルリクエストのコメントで教えてもらいました。

何が嬉しいの

メリットは、コンテナに sleep コマンドが不要となることです。
これまで sleep を実行するために、Vector のイメージには Alpine をベースにしたものを利用していました。
しかし、sleep が不要になることで Distroless イメージを利用することができるようになりました。

Distroless を利用することで、不要なパッケージを最低限に減らすことができ、セキュリティリスクを軽減することができます。
また本当に僅かではありますが、Alpine よりイメージをサイズダウンできました。

さいごに

実行ログを DB から S3 へ剥がすプロジェクトのうち、サイドカーのプロセス周りは喉に刺さった小骨のようなものでした。
ただタイムリーにサイドカーと LifecycleHook 周りの機能が Beta 版としてリリースされたことで、ネイティブな機能を利用してシンプルに解決することができました。
K8s の進化は日進月歩で、PodLifecycleSleepAction などはプルリクでコメントされるまで認識していませんでした。
ただその分課題だと思っていたものもあっという間に解決することもできますし、今後もアンテナを貼って便利な機能を追いかける必要があると感じました。

ぜひこの記事を参考に、SidecarContainersPodLifecycleSleepAction をうまく活用して、サイドカーの不便に感じていた点を解消いただけると幸いです!

脚注
  1. stern は複数の pod や container のログを見ることができるツールです。 ↩︎

株式会社primeNumber

Discussion