Fluentdサイドカー付きJob Podを削除したい

問題

バッチ処理などを行う時に便利なKubernetesのJob Podに、Fluentdなどのログ収集プログラムを動かすためのサイドカーコンテナを付けることはよくあるパターンだと思う。しかし、このパターンには問題がある。それはメインコンテナの処理が終了してもサイドカーコンテナが生き続けるため、.spec.ttlSecondsAfterFinishedオプションを付けていてもJob Podがずっと残りしてまうことである。

既存の解決策

この「サイドカーがあるとJob Podが消えない問題」の解決策を調べていると大きく分けて2つの解決策があることがわかった。

  • メインコンテナの処理が終わったら共有ボリュームに適当なファイルを作成し、サイドカーコンテナではそのァイルが作成されたことを検知してプロセスを終了させるプログラムを動かす
  • Pod内のコンテナでPID Namespaceを共有し、メインコンテナの処理が終わったらサイドカーコンテナのプロセスを落とす

(また、コンテナにタイプを割り振ることでメインコンテナが終了したタイミングでサイドカーコンテナを終了させる機能が提案されていたがクローズされていた。)

今回の解決案

ログ収集にFluentdのtailプラグインを使っている場合、posファイルというものを使うとFluendはその情報をもとにログをどこまで読み込んだかを管理するので、ログの欠損や重複読み取りを防ぐことができる。ログが書き出されている間はこのposファイルも更新され続けるので、このファイルの中身を監視して一定期間変更がなければサイドカーコンテナを終了させることで、問題が解決できるのではと考えた。この方法ではサイドカーコンテナだけに手を入れれば良いので、何かしらの理由でメインコンテナには手を入れづらい場合は役に立つかもしれない。

準備

Jobを用意する。メインコンテナではバッチ処理を行なって何かしらのログを書き出している想定なので、3秒おきにログファイルに時間などを書き出すスクリプトを動かす。

apiVersion: batch/v1
kind: Job
metadata:
  name: sidecar-killer-sample
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 5
  ttlSecondsAfterFinished: 30
  template:
    spec:
      containers:
        - name: main
          image: busybox:1.28
          args: ['/bin/sh', '-c', 'for i in $(seq 20); do echo "$i: $(date)" >> /var/log/1.log && sleep 3; done']
          volumeMounts:
          - name: varlog
            mountPath: /var/log

これをデプロイするとコンテナ内の/var/log/1.logの以下のようなログが書き出される。

$ cat /var/log/1.log
1: Sat May 21 21:11:39 JST 2022
2: Sat May 21 21:11:42 JST 2022
3: Sat May 21 21:11:45 JST 2022
4: Sat May 21 21:11:48 JST 2022
5: Sat May 21 21:11:51 JST 2022
6: Sat May 21 21:11:54 JST 2022
7: Sat May 21 21:11:57 JST 2022
8: Sat May 21 21:12:00 JST 2022
9: Sat May 21 21:12:03 JST 2022
10: Sat May 21 21:12:06 JST 2022

次にFluentdサイドカーを追加して再度デプロイする。

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentconf
data:
  fluent.conf: |
    <source>
      @type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag hoge
      read_from_head true
    </source>
    <match **>
      @type stdout
    </match>    
---
apiVersion: batch/v1
kind: Job
metadata:
  name: sidecar-killer-sample
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 5
  ttlSecondsAfterFinished: 30
  template:
    spec:
      containers:
        - name: main
          image: busybox:1.28
          args: ['/bin/sh', '-c', 'for i in $(seq 20); do echo "$i: $(date)" >> /var/log/1.log && sleep 3; done']
          volumeMounts:
          - name: varlog
            mountPath: /var/log
        - name: fluentd
          image: fluent/fluentd:v1.14-1
          args: ['/bin/sh', '-c', 'fluentd -c /fluentd/etc/fluent.conf']
          securityContext:
            runAsUser: 0
          volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: fluentconf
              mountPath: /fluentd/etc
      restartPolicy: Never
      volumes:
        - name: varlog
          emptyDir: {}
        - name: fluentconf
          configMap:
            name: fluentconf
            items:
              - key: fluent.conf
                path: fluent.conf

Fluentdのコンテナのログを出力して正常に動作していることを確認する。

$ k logs sidecar-killer-sample-xxx fluentd
2022-05-21 15:16:06 +0000 [info]: parsing config file is succeeded path="/fluentd/etc/fluent.conf"
...
2022-05-21 15:16:59.756059200 +0000 hoge: {"message":"19: Sat May 21 15:16:59 UTC 2022"}
2022-05-21 15:17:02.756613500 +0000 hoge: {"message":"20: Sat May 21 15:17:02 UTC 2022"}

この状態ではメインコンテナが終了しても、サイドカーコンテナが生きているのでJobが終了しないことを確認する。

$ k get pods
NAME                          READY   STATUS     RESTARTS   AGE
sidecar-killer-sample-xxx     1/2     NotReady   0          2m18s
$ k get jobs
NAME                    COMPLETIONS   DURATION   AGE
sidecar-killer-sample   0/1           4m29s      4m29s

サイドカーを終了させるプログラムを追加する

Fluentdのposファイルを一定期間監視して、中身に変更がなければFluentdのプロセスを終了させるプログラムをサイドカーコンテナで動かす。FluentdコンテナではRubyが使えるので今回はRubyで簡単なスクリプトを書く。

def main()
  prev = nil
  puts 'start monitoring pos file'

  while true
    begin
      pos = {}
      File.foreach('/var/log/1.log.pos') do |line|
        elements = line.split(' ')
        pos[elements[0]] = elements[1] + '-' + elements[2]
      end
    rescue => e
      puts 'pos file not found'
      sleep 10
      next
    end
  
    if prev == pos
      puts 'terminate the process because pos file was unchanged for a minute'
      system('pkill -INT fluentd')
      break
    end

    prev = pos
    sleep 60
  end
end

main()

このスクリプトをサイドカーコンテナのバックグラウンドで動かす。

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentconf
data:
  fluent.conf: |
    <source>
      @type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag hoge
      read_from_head true
    </source>
    <match **>
      @type stdout
    </match>
  terminator.rb: |
    def main()
      prev = nil
      puts 'start monitoring pos file'

      while true
        begin
          pos = {}
          File.foreach('/var/log/1.log.pos') do |line|
            elements = line.split(' ')
            pos[elements[0]] = elements[1] + '-' + elements[2]
          end
        rescue => e
          puts 'pos file not found'
          sleep 10
          next
        end
      
        if prev == pos
          puts 'terminate the process pos file because unchanged for a minute'
          system('pkill -INT fluentd')
          break
        end

        prev = pos
        sleep 60
      end
    end

    main()
---
apiVersion: batch/v1
kind: Job
metadata:
  name: sidecar-killer
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 5
  ttlSecondsAfterFinished: 30
  template:
    spec:
      containers:
        - name: main
          image: busybox:1.28
          args: ['/bin/sh', '-c', 'for i in $(seq 10); do echo "$i: $(date)" >> /var/log/1.log && sleep 3; done']
          volumeMounts:
          - name: varlog
            mountPath: /var/log
        - name: fluentd
          image: fluent/fluentd:v1.14-1
          args: ['/bin/sh', '-c', 'fluentd -c /fluentd/etc/fluent.conf & ruby fluentd/etc/terminator.rb']
          securityContext:
            runAsUser: 0
          volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: fluentconf
              mountPath: /fluentd/etc
      restartPolicy: Never
      volumes:
        - name: varlog
          emptyDir: {}
        - name: fluentconf
          configMap:
            name: fluentconf
            items:
              - key: fluent.conf
                path: fluent.conf
              - key: terminator.rb
                path: terminator.rb

こうすることでposファイルに一定期間変更がない場合はFluentdのプロセスが終了し、Jobも完了状態となる。

$ k logs sidecar-killer-sample-xxx fluentd
...
2022-05-21 14:19:15.378827400 +0000 hoge: {"message":"10: Sat May 21 14:19:14 UTC 2022"}
start monitoring pos file
pos file not found
terminate the process because pos file was unchanged for a minute
$ k get pod
NAME                   READY   STATUS      RESTARTS   AGE
sidecar-killer-xxx     0/2     Completed   0          57s
$ k get jobs
NAME             COMPLETIONS   DURATION   AGE
sidecar-killer   1/1           53s        73s

まとめ

この実装だと、メインのコンテナで時間がかかる処理をしている場合も何かしらのログを吐き続けないといけなかったりと色々問題がある。ログに高い信頼性を求められる場合は別の方法を考えた方が良いだろう。このように泥臭い実装をしなくても済むように本家に機能が追加されるのを待ちつつ、もっとイケてる実装ができないか考えたいと思う。

あとがき

Pod内のコンテナでPID Namespaceを共有し、メインコンテナの処理が終わったらサイドカーコンテナのプロセスを落とパターンも時間があれば実験してみたい。