おや、Kuberenetes サイドカーのようすが...
はじめに
以前、TROCCO の実行ログの保存先を DB から S3 へ移行した話をしました。
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 にサイドカーコンテナを記述することで利用できます。
この時、restartPolicy
は Always
に設定する必要があります。
例
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.command
で sleep
コマンドを実行する方法です。
TROCCO でも、Vector コンテナで sleep を実行することでログの欠損を回避していました。
しかし、K8s 1.30 から sleep による遅延をより手軽に行う機能が Beta 版として追加されました。
それが PodLifecycleSleepAction です。
プルリクエストのコメントで教えてもらいました。
何が嬉しいの
メリットは、コンテナに sleep
コマンドが不要となることです。
これまで sleep
を実行するために、Vector のイメージには Alpine をベースにしたものを利用していました。
しかし、sleep
が不要になることで Distroless イメージを利用することができるようになりました。
Distroless を利用することで、不要なパッケージを最低限に減らすことができ、セキュリティリスクを軽減することができます。
また本当に僅かではありますが、Alpine よりイメージをサイズダウンできました。
さいごに
実行ログを DB から S3 へ剥がすプロジェクトのうち、サイドカーのプロセス周りは喉に刺さった小骨のようなものでした。
ただタイムリーにサイドカーと LifecycleHook 周りの機能が Beta 版としてリリースされたことで、ネイティブな機能を利用してシンプルに解決することができました。
K8s の進化は日進月歩で、PodLifecycleSleepAction
などはプルリクでコメントされるまで認識していませんでした。
ただその分課題だと思っていたものもあっという間に解決することもできますし、今後もアンテナを貼って便利な機能を追いかける必要があると感じました。
ぜひこの記事を参考に、SidecarContainers
と PodLifecycleSleepAction
をうまく活用して、サイドカーの不便に感じていた点を解消いただけると幸いです!
Discussion