コンテナ間でlogrotateしたいpart2

# はじめに

本記事では、nginx(openresty)のログファイルを別のlogrotate用コンテナ（以下logrotateコンテナ）を用いてログローテーションを実装する方法について記述します。

※この記事はpart2です。[part1](https://qiita.com/toyo_mura/items/4e8b0849df2cb8208f8c)から読んでいただけるとわかりやすいかと思います。
part1: https://qiita.com/toyo_mura/items/4e8b0849df2cb8208f8c

# part1の復習
* logrotateコンテナはできた
* ローテーション後のファイルにログを出力してくれない問題が発生
* コンテナ間でのローテーションを完成させるには、logrotateを実行したあと、nginxにログファイルを開きなおさせる(以下: reopen)かnginxの再起動をする必要がある。

# ゴール

複数のコンテナで吐き出されているnginxのログをlogrotateコンテナで一括管理・ローテーションできるようになることをゴールとします。

# 検討したログファイルの開きなおし(reopen)＆nginxの再起動
**前提**
・コンテナ間でのローテーションを完成させるには、logrotateを実行したあと、nginxにログファイルをreopenさせるかnginxの再起動をする必要がある
・具体的にはnginxコンテナで`openresty -s reopen`を実行またはnginxコンテナの再起動をする必要がある

これを実現させるために検討したのが以下です。


* **docker.sockをlogrotateコンテナにマウントしてdockerコマンドを使う**
セキュリティ的に問題あり
参考:https://qiita.com/YuasaJunki/items/8da1ef708a6f826f5915


* **dockerをリモートから制御できるようにして、logrotateコンテナからdockerコマンドを使う**
セキュリティ的に問題あり
参考:[リモートサーバーの中のDockerにローカルから接続する](https://www.yasunaga-lab.bio.kyutech.ac.jp/EosJ/index.php/%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AE%E4%B8%AD%E3%81%AEDocker%E3%81%AB%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%8B%E3%82%89%E6%8E%A5%E7%B6%9A%E3%81%99%E3%82%8B)
https://qiita.com/YuasaJunki/items/8da1ef708a6f826f5915

* **nginxコンテナにsshdを起動してlogrotateコンテナからsshで実施**
若干セキュリティ的に問題あり
参考:https://postd.cc/docker-ssh-considered-evil/
これだけのためにsshdを起動するのは無駄かも

* **LUAを使ってAPI(CGI)を実装（nginxにファイルをreopenさせるAPI）**
作るのが少しめんどくさい
既存コンテナの変更が必要

* **pythonを使ってAPI(CGI)を実装（nginxにファイルをreopenさせるAPI）**
作るのが少しめんどくさい
既存コンテナの変更が必要
LUAよりも汎用性がありそう

上記のように数個検討しました。
docker.sockをマウントする方法だったり、sshdを起動したりする方法が実装するのは簡単そうでしたが、セキュリティ的に問題があるため、運用していく上でそれではいけないということになり、選択肢から外れました。

結果LUAかpythonかの2択になったときに、「luaはopenrestyには入っているがnginxには入っていない・他の環境でもpythonならだいたい入っている・pythonならアプリに関係なくcgiサーバを起動するだけでいい」などの理由からpythonのほうが汎用性がありそうだったのでpythonでAPIが良さそうという結論に至りました。

想定動作としては以下です。
nginxコンテナ上でreopenのAPI(CGI)を常時動かしておきます。
logrotateコンテナがローテーションを完了したタイミングでlogrotateの機能を使ってnginxコンテナにreopen用のシグナルを乗せたhttpリクエストを送信します。
するとnginxコンテナで動いているCGIサーバ*がCGIを動かします。
CGIがhttpリクエストから取得したシグナルをnginxに送信し、ファイルがreopenされるという仕組みです。
※CGIサーバはpythonのビルトインサーバです。

# 実装の流れ
1. ローテーションしたいファイルのあるコンテナ（以下nginxコンテナ）にディレクトリをマウント（コンテナ起動オプション変更）
1. logrotateコンテナ作成
1. 動作確認（途中経過）
1. logrotateコンテナとnginxコンテナ通信用docker network作成 **←この記事ではここから**
1. nginxがファイルをreopenするためのAPIを作成
1. nginxコンテナの設定変更
1. ホストの設定変更
1. logrotateコンテナの設定変更
1. 動作確認

# 環境
* ホスト
  * OS: CentOS 7.9.2009
  * Docker: 20.10.9
  * HOSTNAME: development
* nginxコンテナ
  * OS: CentOS 7.6.1810
  * nginx: openresty/1.19.3.1
  * HOSTNAME: development_c
* logrotateコンテナ
  * OS: ubuntu 20.04
  * HOSTNAME: 5d76504e84c1

# logrotateコンテナとnginxコンテナ通信用docker-network作成
とりあえずlogrotateコンテナとnginxコンテナが通信できるようにするために`docker network create`コマンドでdocker-networkを作成します。
この作業をしないとlogrotateコンテナからnginxコンテナにコンテナ名でcurlコマンドを送信できないので大切な作業です。
このコマンドはコンテナ内ではなく、ホストで行います。

~~~
[root@development ~]# docker network create openresty-network
~~~

作成できたか確認します。

~~~
[root@development ~]# docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
2a90d0faac26   bridge              bridge    local
e091384cb914   host                host      local
9151cbd0142e   none                null      local
0a1ad870469e   openresty-network   bridge    local
~~~

問題なく作成できているようです。

# nginxがファイルをreopenするためのAPIを作成
次にCGIプログラムを作成します。今回はPython2（nginxコンテナにデフォルトで入ってたやつ）で実装します。
ファイル名はなんでもいいですが、今回はわかりやすいように`reopen.py`としています。

CGIファイルの中身は以下です。
（汎用性をもたせるために、今回のreopenの機能には必要のない部分もあります。）

~~~python
#!/usr/bin/env python

import os
import signal
import cgi

p_pid = None
p_sig = None
s_sig = None

form = cgi.FieldStorage()

if form.has_key("pid"):
    p_pid = form["pid"].value

if form.has_key("sig"):
    p_sig = form["sig"].value

sigs = {
    "USR1": signal.SIGUSR1,
    "HUP" : signal.SIGHUP
}

if sigs.has_key(p_sig):
    s_sig = sigs[p_sig]

if p_pid != None and int(p_pid) > 0 and s_sig != None:
    print("Content-Type: text/plain\n")
    print("pid: " + p_pid)
    print("sig: " + p_sig)
    print("signal: " + str(s_sig))
    try:
        os.kill(int(p_pid), s_sig)
        print("res: OK")
    except OSError as e:
        print("res: " + str(e))
else:
    print("Status: 400 Bad Request\n")
    print("param error")
~~~

参考:
[cgi.FieldStorage](https://docs.python.org/ja/3/library/cgi.html?highlight=fieldstorage)
[has_key](http://www.w3big.com/ja/python/att-dictionary-has_key.html)
[CGI](https://docs.python.org/ja/3.6/library/cgi.html)
[ビルトインサーバーだと200固定](https://stackoverflow.com/questions/25114486/python-redirect-not-working/32079589#32079589)
[ビルトインサーバー](https://symfoware.blog.fc2.com/blog-entry-1977.html)

全体を通しての動きを説明します。

* logrotateコンテナからcurlで`pid=1`と`sig=USR1`を乗せたhttpリクエストを送信
（USR1はnginxにログファイルをreopenさせるシグナル）
* nginxコンテナで起動しているCGIサーバがhttpリクエストを受け付けて、`pid=1`と`sig=USR1`を`reopen.py`に渡す
* 受け取ったプログラムは、pidが1のプロセス（ここではnginx）にUSR1のシグナルを送信
* これによってnginxのログがreopenされる

参考:[nginx に USR1 シグナルを送ってログを開き直す](https://qiita.com/white_aspara25/items/7c0352cb2c24e5d7268b)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295232/8fcf133f-4159-6504-fcbf-117e18bdbdfe.png)

プログラムの動きを簡単に説明します。

* `pid=1`と`sig=USR1`を受け取る（`cgi.FieldStorage`のおかげでクライアントから送られるフォームの内容を取得できる）
* 受け取った値が`p_pid`,`s_sig`にセットされる
* `os.kill(int(p_pid),s_sig)`の部分でnginxにUSR1シグナルを送信
* パーミッションエラーなどの場合はエラーを吐く

このような動作になっています。

また、`sigs = {...}`の部分で受け付けるシグナルを限定しています。

参考:https://qiita.com/pythonista/items/ef1cbbf8991e3a5921ff


# nginxコンテナの設定変更
次はnginxコンテナの設定を変更していきます。

CGIプログラムを作成したはいいものの、このままでは期待通りの動作をしません。
nginxコンテナの起動ユーザがrootのままCGIを動かすと、nginxにreopenのシグナルを送ってもパーミッションエラーとなります。
pythonをrootで動かしてもCGI実行のユーザがrootじゃないのが理由です。
nginxユーザで起動すれば問題なく動作します。
そのため、nginxコンテナの起動ユーザをrootからnginxに変更する必要があります。

というわけでnginxコンテナの方に変更を加えていきます。

作業内容は以下です。

* nginxコンテナのDockerfileを編集
* entrypoint.shを編集
* コンテナ起動時のオプション変更

## nginxコンテナのDockerfileを編集
nginxコンテナをnginxユーザで起動するためにDockerfileを編集します。
nginxユーザで起動するにあたって、ディレクトリのパーミッション等も変更しないといけないのでその設定も記述します。

nginxコンテナのDockerfileは人によって様々だと思いますが、ここでは私が追加した内容を記述します。
下記の項目を追記します。

~~~Dockerfile
:
COPY entrypoint.sh /tmp/
COPY reopen.py /var/tmp/cgi-bin/

RUN chmod 707 /etc/nginx/conf
RUN chmod 707 /usr/local/openresty/nginx/logs
RUN chmod 707 /usr/local/openresty/nginx
RUN chmod 707 /usr/local/openresty/nginx/logs/access
RUN chmod 707 /usr/local/openresty/nginx/logs/error
RUN chmod 755 /var/tmp/cgi-bin/reopen.py

RUN useradd -m nginx

ENTRYPOINT ["sh","/tmp/entrypoint.sh"]
USER nginx
CMD ["/usr/bin/openresty", "-g", "daemon off;"]
~~~

差分
~~~
[root@development nginx]# diff Dockerfile_old Dockerfile
4a5,6
> COPY entrypoint.sh /tmp/
> COPY reopen.py /var/tmp/cgi-bin/
13a16,26
> RUN chmod 707 /etc/nginx/conf
> RUN chmod 707 /usr/local/openresty/nginx/logs
> RUN chmod 707 /usr/local/openresty/nginx
> RUN chmod 707 /usr/local/openresty/nginx/logs/access
> RUN chmod 707 /usr/local/openresty/nginx/logs/error
> RUN chmod 755 /var/tmp/cgi-bin/reopen.py
> 
> RUN useradd -m nginx
> 
> ENTRYPOINT ["sh","/tmp/entrypoint.sh"]
> USER nginx
~~~


## entrypoint.shを編集
次にentrypoint.shを編集します。
entrypoint.shはDockerfileの中、`ENTRYPOINT`のコマンドで使用してます。
記述内容は以下です。

~~~sh
#!/bin/sh

cd /var/tmp/
nohup python -m CGIHTTPServer &

exec "$@"
~~~

`nohup python -m CGIHTTPServer &`でnginxコンテナ上でCGIサーバーを常時起動しています。

## コンテナ起動時のオプション変更
nginxコンテナの起動ユーザがrootではなくなったので、1024番より小さい値のポートが使えません。
そのため、ポートを変更します。
また、先程作成したdocker-networkを利用するためのオプションも追記します。
以下のように記述します。

~~~sh
docker run -d \
        -p 80:8080 \　#80:80だと使えない
:
        -v /hogehoge/nginx/access/:/usr/local/openresty/nginx/logs/access/ \
        -v /hogehoge/nginx/error/:/usr/local/openresty/nginx/logs/error/ \
:
        --network openresty-network \
        --name nginx1 toyo_mura/nginx:latest
~~~

~~~
[root@development nginx]# diff run.sh_old run.sh
17c17
<       -p 80:80 \
---
>       -p 80:8080 \　#80:80だと使えない
30a31
>       --network openresty-network \
~~~

# ホストの設定変更
ホストの設定を少し変更します。
変更点は以下です

* ログが出力されるディレクトリのパーミッション変更

## ログが出力されるディレクトリのパーミッション変更
ログが出力されるホストのディレクトリのパーミッションを変更しないとログを書き出せないので変更します。

~~~
[root@development ~]# chmod 707 /hogehoge/nginx/access
[root@development ~]# chmod 707 /hogehoge/nginx/error
~~~

# logrotateコンテナの設定変更

logrotateコンテナの設定がpart1で設定した内容のままだと動かないので追記を行います。
作業内容は以下です。

* logrotateコンテナのDockerfileにcurlコマンドインストール行を追記
* /etc/logrotate.d/に持っていっているnginxのログ用logrotate定義ファイルにcurlコマンドを追記
* コンテナ起動時のオプション変更


## logrotateコンテナのDockerfileにcurlコマンドインストール行を追記
logrotateコンテナからnginxコンテナにcurlコマンドでhttpリクエストを送信したいのでlogrotateコンテナのDockerfileを下記のようにします。

~~~Dockerfile
FROM ubuntu:20.04

COPY entrypoint.sh /tmp/

##### rsyslog settings #####
RUN apt-get update
RUN apt-get -y install rsyslog
RUN sed -i -e "s/^#cron/cron/" /etc/rsyslog.d/50-default.conf

##### vim settings #####
RUN apt-get -y install vim
RUN echo 'set encoding=utf-8\n\
set fileencodings=iso-2022-jp,euc-jp,sjis,utf-8\n\
set fileformats=unix,dos,mac' >> ~/.vimrc

##### cron setting #####
RUN apt-get -y install cron

##### logrotate settings #####
RUN apt-get -y install logrotate
COPY ./nginx /etc/logrotate.d/

##### curl setting #####
RUN apt-get -y install curl

##### timezone settings #####
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y install tzdata
ENV TZ Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

ENTRYPOINT ["sh","/tmp/entrypoint.sh"]
CMD ["cron", "-f"]
~~~

差分
~~~
[root@development logrotate]# diff Dockerfile_old Dockerfile
22a23,25
> ##### curl setting #####
> RUN apt-get -y install curl
> 
~~~

## /etc/logrotate.d/にCOPYしているnginxのログ用logrotate定義ファイルにcurlコマンドを追記
logrotate終了後にcurlコマンドを自動で叩いてほしいので、/etc/logrotate.d/に持っていっているnginxのログ用logrotate定義ファイルを下記のように編集します。

~~~logrotate
/var/log/nginx/access/*.log
/var/log/nginx/error/*.log {
    ifempty
    dateext
    missingok
    compress
    delaycompress
    daily
    rotate 10
    lastaction
        curl -v "nginx1:8000/cgi-bin/reopen.py?pid=1&sig=USR1"
    endscript
}
~~~

差分
~~~
[root@development logrotate]# diff nginx_old nginx
9a10,12
>     lastaction
>         curl -v "nginx1:8000/cgi-bin/reopen.py?pid=1&sig=USR1"
>     endscript
~~~

lastaction/endscriptはすべてのログがローテーションされた後に、一度だけ実行されるスクリプトです。
参考:https://hackers-high.com/linux/man-jp-logrotate/#lastactionendscript
ここのcurlの部分でnginxコンテナに`pid=1`と`sig=USR1`シグナルを乗せたhttpリクエストを送信しています。
curlの行を増やすことで複数のコンテナにhttpリクエストを投げることも可能です。
また、送信ポートが8000なのはpythonのCGIHTTPServerの待受ポートが8000だからです。

（nginxコンテナとlogrotateコンテナは`openresty-network`で疎通できているので、特にポート開放等の作業はいらないです）

## コンテナ起動時のオプション変更
先程作成したdocker-networkを利用するためのオプションを追記します。
以下のように記述します。

~~~
[root@development logrotate]# diff run.sh_old run.sh
6a7
>       --network openresty-network \
~~~


# 動作確認
まず、nginxコンテナとlogrotateコンテナ両方`docker build`、`docker run`してコンテナを作り直しましょう。（編集した設定を反映させるため）

コンテナを作り直したらnginxコンテナでCGIサーバが動いているはずなので本当に動いているか確認します。

~~~
[root@development logrotate]# docker exec nginx1 ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
nginx         1      0  0 15:10 ?        00:00:01 nginx: master process /usr/bin/openresty -g daemon off;
nginx         9      1  0 15:10 ?        00:00:02 python -m CGIHTTPServer
nginx        37      1  0 15:11 ?        00:00:00 nginx: worker process
nginx        38      0  0 18:34 ?        00:00:00 ps -ef
~~~

PID9番で動いているようです。

ちなみにnginxコンテナでssコマンドを叩くと、CGIサーバが8000番ポートで待ち受けていることがわかります。

```
[root@development logrotate]# docker exec nginx1 ss -nltp
:
LISTEN     0      5            *:8000                     *:*                   users:(("python",pid=9,fd=3))
:
```

次に、`reopen.py`が正常に動作するか確認します。
logrotateコンテナからcurlでシグナルを乗せたhttpリクエストを送信してみます。

logrotateコンテナ
~~~
root@5d76504e84c1:/# curl -v "nginx1:8000/cgi-bin/reopen.py?pid=1&sig=USR1"
*   Trying 172.21.0.3:8000...
* TCP_NODELAY set
* Connected to nginx1 (172.21.0.3) port 8000 (#0)
> GET /cgi-bin/reopen.py?pid=1&sig=USR1 HTTP/1.1
> Host: nginx1:8000
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 Script output follows
< Server: SimpleHTTP/0.6 Python/2.7.5
< Date: Fri, 18 Mar 2022 06:20:51 GMT
< Content-Type: text/plain
< 
pid: 1
sig: USR1
signal: 10
res: OK
* Closing connection 0
root@5d76504e84c1:/# 
~~~

問題なく`reopen.py`は動作しているようです。

そしたら、実際にログローテーションができてかつ、ログファイルのreopenができているかの最終確認を行います。
nginxコンテナは再起動しておきましょう。

part1までの実装で、ログファイルをローテーションしてくれるようにはなったはずです。
ここでは、ローテーション後のファイルにしっかり書き込めるようになっているか確認します。

まずはpart1と同様、ローテーションの確認をします。

~~~
[root@development logrotate]# docker exec -it logrotate bash
root@5d76504e84c1:/# ls /var/log/nginx/access/                  
hogehoge.com.log              fugafuga.com.log               piyopiyo.com.log
hogehoge.com.log-20220226.gz  fugafuga.com.log-20220226.gz   piyopiyocom.log-20220226.gz
hogehoge.com.log-20220227.gz  fugafuga.com.log-20220227.gz   piyopiyo.com.log-20220227.gz
hogehoge.com.log-20220228.gz  fugafuga.com.log-20220228.gz   piyopiyo.com.log-20220228.gz
hogehoge.com.log-20220301.gz  fugafuga.com.log-20220301.gz   piyopiyo.com.log-20220301.gz
hogehoge.com.log-20220302.gz  fugafuga.com.log-20220302.gz   piyopiyo.com.log-20220302.gz
hogehoge.com.log-20220303.gz  fugafuga.com.log-20220303.gz   piyopiyo.com.log-20220303.gz
hogehoge.com.log-20220304.gz  fugafuga.com.log-20220304.gz   piyopiyo.com.log-20220304.gz
hogehoge.com.log-20220305.gz  fugafuga.com.log-20220305.gz   piyopiyo.com.log-20220305.gz
hogehoge.com.log-20220306.gz  fugafuga.com.log-20220306.gz   piyopiyo.com.log-20220306.gz
hogehoge.com.log-20220307     fugafuga.com.log-20220307      piyopiyo.com.log-20220307
~~~

問題なくローテーションできているようです。
次に、ローテーション後のファイルを掴んでいるか確認します。
（このチェックで、同時にlogrotateのlastaction/endscliptの動作チェックも行っています）

~~~
[root@development logrotate]# docker exec nginx1 ls -l /proc/1/fd 
total 0
lrwx------ 1 nginx nginx 64 Jan 14 15:37 0 -> /dev/null
l-wx------ 1 nginx nginx 64 Jan 14 15:37 1 -> pipe:[25985041]
l-wx------ 1 nginx nginx 64 Mar  7 14:55 10 -> /usr/local/openresty/nginx/logs/access/hogehoge.com.log
l-wx------ 1 nginx nginx 64 Mar  7 14:55 11 -> /usr/local/openresty/nginx/logs/error/hogehoge.com.log
:
~~~

問題なくローテーション後のファイルを掴んでいるようです。
本当にログが出力されるか確認します。

~~~
[root@development logrotate]# curl -I 127.0.0.1
:
[root@development logrotate]# docker exec -it nginx1 cat /usr/local/openresty/nginx/logs/access/hogehoge.com.log
172.21.0.1 - - [10/Mar/2022:17:00:05 +0900] "HEAD / HTTP/1.1" 403 0 "-" "curl/7.29.0" "-"
~~~

ログが出力されていることが確認できました。

ついでにnginxコンテナ2を立ててみて、複数コンテナでも動作するか確認します。
/etc/logrotate.d/にCOPYしているnginxのログ用logrotate定義ファイルに下記を追加します。

~~~
[root@development logrotate]# diff nginx_old nginx
12a13
>         curl -v "nginx2:8000/cgi-bin/reload.py?pid=1&sig=USR1"
~~~

ローテーションの確認は飛ばします。
ローテーション後のファイルを掴んでいるか確認します。

~~~
[root@development logrotate]# docker exec nginx2 ls -l /proc/1/fd
total 0
lrwx------ 1 nginx nginx 64 Mar 28 15:11 0 -> /dev/null
l-wx------ 1 nginx nginx 64 Mar 28 15:11 1 -> pipe:[50709858]
l-wx------ 1 nginx nginx 64 Mar 28 18:10 10 -> /usr/local/openresty/nginx/logs/access/fugafuga.com.log
l-wx------ 1 nginx nginx 64 Mar 28 18:10 11 -> /usr/local/openresty/nginx/logs/error/fugafuga.com.log
:
~~~

最後に本当にログが出力されているか確認します。

~~~
[root@development logrotate]# curl -I 127.0.0.1:8080
:
[root@development logrotate]# docker exec -it nginx2 cat /usr/local/openresty/nginx/logs/access/fugafuga.com.log
172.21.0.1 - - [28/Mar/2022:16:14:02 +0900] "HEAD / HTTP/1.1" 403 0 "-" "curl/7.29.0" "-"
~~~

複数コンテナでも問題なくログが出力されていることが確認できました。

# まとめ
* logrotateコンテナとnginxコンテナ間でlogrotateを実装することができた
* logrotateを実装するためには既存のnginxコンテナに変更を加える必要あり

# おわりに
part1,part2とここまで読んでいただきありがとうございます。
誰かの役に立ったなら幸いです。
またどこかでお会いしましょう。