Kubernetes の sidecar で logrotate する

kuberneteslinux

ログを出力するアプリケーションと、

package main

import (
	"os"
	"time"

	"github.com/sirupsen/logrus"
)

func main() {
	logFile, err := os.OpenFile("/var/log/app/test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		logrus.Fatal("failed to open file", err)
	}

	logrus.SetOutput(logFile)

	i := 0
	ticker := time.NewTicker(1 * time.Millisecond)
	for {
		<-ticker.C
		logrus.Print(i)
		i++
	}
}

logrotate をインストールしたイメージを作り、

$ cat Dockerfile_logrotate
FROM alpine:latest

RUN apk add --no-cache logrotate

# RUN echo '/usr/sbin/logrotate /etc/logrotate.d/logrotate.conf' > /etc/periodic/daily/logrotate
# RUN chmod +x /etc/periodic/daily/logrotate
# CMD ["crond", "-f"]

volume でログを共有して実行する。

logrotateでログをローテーションする - sambaiz-net

apiVersion: v1
kind: ConfigMap
metadata:
  name: logrotate-config
data:
  logrotate.conf: |
    /var/log/app/*.log {
      size 1k
      copytruncate
      rotate 5
      compress
      delaycompress
      missingok
      notifempty
    }

---

apiVersion: v1
kind: Pod
metadata:
  name: testapp
spec:
  containers:
  - name: testapp
    image: testapp
    imagePullPolicy: Never
    volumeMounts:
    - name: log-volume
      mountPath: /var/log/app

  - name: logrotate
    image: my-logrotate
    imagePullPolicy: Never
    volumeMounts:
    - name: log-volume
      mountPath: /var/log/app
    - name: logrotate-config-volume
      mountPath: /etc/logrotate.d
    command:
    - /bin/sh
    - -c
    - |
      chmod 644 /var/log/app && 
      while true; do 
        /usr/sbin/logrotate /etc/logrotate.d/logrotate.conf; 
        sleep 10; 
      done

  volumes:
  - name: log-volume
    emptyDir: {}
  - name: logrotate-config-volume
    configMap:
      name: logrotate-config

結果次のように rotate はされているが、

$ ls -li /var/log/app
total 456
5505178 -rw-r--r-- 1 root root 139050 Mar  6 15:30 test.log
5505181 -rw-r--r-- 1 root root 261200 Mar  6 15:30 test.log.1
5505202 -rw-r--r-- 1 root root  14024 Mar  6 15:29 test.log.2.gz
5505201 -rw-r--r-- 1 root root  13760 Mar  6 15:29 test.log.3.gz
5505218 -rw-r--r-- 1 root root  13918 Mar  6 15:29 test.log.4.gz
5505205 -rw-r--r-- 1 root root  14132 Mar  6 15:29 test.log.5.gz

copytruncate する際に一部のログが失われてしまう。

$ tail test.log.1
time="2024-03-06T15:27:11Z" level=info msg=26132
time="2024-03-06T15:27:11Z" level=info msg=26133
time="2024-03-06T15:27:11Z" level=info msg=26134
time="2024-03-06T15:27:11Z" level=info msg=26135
time="2024-03-06T15:27:11Z" level=info msg=26136
time="2024-03-06T15:27:11Z" level=info msg=26137
time="2024-03-06T15:27:11Z" level=info msg=26138
time="2024-03-06T15:27:11Z" level=info msg=26139
time="2024-03-06T15:27:11Z" level=info msg=26140
time="2024-03-06T15:27:11Z" level=info msg=26141

$ head test.log
time="2024-03-06T15:27:11Z" level=info msg=26143
time="2024-03-06T15:27:11Z" level=info msg=26144
time="2024-03-06T15:27:11Z" level=info msg=26145
time="2024-03-06T15:27:11Z" level=info msg=26146
time="2024-03-06T15:27:11Z" level=info msg=26147
time="2024-03-06T15:27:11Z" level=info msg=26148
time="2024-03-06T15:27:11Z" level=info msg=26149
time="2024-03-06T15:27:11Z" level=info msg=26150
time="2024-03-06T15:27:11Z" level=info msg=26151
time="2024-03-06T15:27:11Z" level=info msg=26152

copytruncate をやめるには postrotate でアプリケーションコンテナのプロセスに SIGHUP を送るなどして書き込むファイルの inode を更新させる必要がある。 shareProcessNamespace を true にすると他のコンテナのプロセスを見られるようになり、SYS_PTRACE capabilities を付与することでシグナルを送ることができる。

apiVersion: v1
kind: ConfigMap
metadata:
  name: logrotate-config
data:
  logrotate.conf: |
    /var/log/app/*.log {
      ...
      sharedscripts
      postrotate
        kill -HUP `cat /var/run/app/app.pid`
      endscript
    }

---

apiVersion: v1
kind: Pod
metadata:
  name: testapp
spec:
  shareProcessNamespace: true
  containers:
  - name: testapp
    ...
    volumeMounts:
    - name: run-volume
      mountPath: /var/run/app
    ...
  - name: logrotate
    volumeMounts:
    - name: run-volume
      mountPath: /var/run/app
    ...
    securityContext:
      capabilities:
        add:
        - SYS_PTRACE

  volumes:
  - name: run-volume
    emptyDir: {}
  ...

PID は 1 でなくなるのでどこかに PID を保存して共有する。

#!/bin/sh

/main &
PID=$!

echo $PID > /var/run/app/app.pid

terminate() {
    kill -TERM "$PID"
    exit
}

trap terminate TERM
wait $PID

あとはアプリケーション側でシグナルを受け取ってファイルを開きなおせばよい。

sighupChan := make(chan os.Signal, 1)
signal.Notify(sighupChan, syscall.SIGHUP)

go func() {
	for {
		<-sighupChan
		newLogFile, err := os.OpenFile("/var/log/app/test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
		if err != nil {
			panic(fmt.Sprintf("failed to open file: %v", err))
		}
		logrus.SetOutput(newLogFile)
		logFile.Close()
		logFile = newLogFile
		fmt.Println("Reopened log file")
	}
}()