Run logrotate sidecar in Kubernetes

kuberneteslinux

Develop an application outputting logs,

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++
	}
}

create image logrotate installed,

$ 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"]

share logs with volume, and run a pod.

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

As a result, rotate has been successful,

$ 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

but some logs have lost on 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

To avoid copytruncate, you need to update the inode of the file to be written by sending SIGHUP to the application container process etc. Setting shareProcessNamespace to true allows containers to see processes in other containers, and SYS_PTRACE capabilities allows them to send signals.

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: {}
  ...

Since the PID is no longer 1, save the PID somewhere and share it.

#!/bin/sh

/main &
PID=$!

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

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

trap terminate TERM
wait $PID

All that’s left to do is receive the signal on the application side and reopen the file.

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")
	}
}()