Container-Optimized OS用にコマンドを定期実行するコンテナを作ってみた

突然ですが、 GCE の Container-Optimized OS で、定期実行がしたいです。より具体的には、**定期的に Let's Encrypt の certbot が動かしたい**のです。

調べたところ、

* （アプリケーションサーバとか）使う連中のどこかに cron をインストールして一緒に動かす
* cron をインストールしたコンテナを作ってがんばる
* tasker コンテナを使う

みたいなのが出てきます。一番上はちょっと…と思い、他二つをがんばってみましたが、**なんでか苦労する**のですよね。**たかが定期実行したいだけなのに…**。

だったら雑に、無限ループで `sleep` とコマンド実行をし続けるプログラムを書いちゃうほうが楽じゃないか、と思いました。

# 要件

* Docker コンテナを使って任意のコマンドを定期実行したい
    * cron のように「何時何分に実行したい」のではなく、**概ね一定間隔で実行されていれば厳密性は問わない**

全く何も考えずに書くとつまりこういうことがしたい。

```go:runner.go
package main

import (
	"fmt"
	"os/exec"
	"time"
)

func main() {
	for {
		out, err := exec.Command("echo", "hello").Output()
		fmt.Printf("%v", string(out))
		if err != nil {
			fmt.Printf("[error] %v\n", err)
		}
		time.Sleep(5 * time.Second)
	}
}
```

あとはこれがコンテナで実行できれば OK です。簡単。

せめて実行間隔や実行コマンドは外から指定したい、みたいなことを思っていくつか機能を足してからコンテナ化しました。

https://github.com/aql/interval-runner

# 使い方

こんなふうに使えます。

```
docker run hiko/interval-runner:1.0.0 --interval 5s echo Hello World
```

`--interval` オプションで実行間隔が指定できます。フォーマットは`time.ParseDuration`（[godoc](https://pkg.go.dev/time#ParseDuration)）に従います。

https://hub.docker.com/repository/docker/hiko/interval-runner

## certbot (docker)を実行したい

certbot のコンテナは、こんな感じに実行しています。

```
docker run --rm -it --name certbot \
  -v $PWD/letsencrypt/log:/var/log/letsencrypt \
  -v $PWD/letsencrypt/etc:/etc/letsencrypt \
  -v $PWD/letsencrypt/html:/var/www/html \
  certbot/certbot:v1.26.0 certonly --webroot -w /var/www/html -d example.com
```

つまり `docker` コマンドを使いたいので、インストールが必要です。

こんな感じの Dockerfile を用意して手順通りに docker-ce-cli をインストールし（最新のインストール手順は公式ドキュメントを参照してください）

```Dockerfile:Dockerfile
FROM hiko/interval-runner:1.0.0

# Install docker client
RUN apt-get update -y && \
    apt-get install -y ca-certificates curl gnupg lsb-release
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
  | tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN apt-get update && apt-get install -y docker-ce-cli
VOLUME ["/var/run/docker.sock"]
```

たとえば docker-runner としてビルドし

```
docker build -t docker-runner .
```

こんなふうに実行できます。

```
docker run --rm --name docker-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $PWD/letsencrypt/log:/var/log/letsencrypt \
  -v $PWD/letsencrypt/etc:/etc/letsencrypt \
  -v $PWD/public:/var/www/html \
  docker-runner \
    --interval 168h \
    docker run --rm --name certbot \
      -v /var/log/letsencrypt:/var/log/letsencrypt \
      -v /etc/letsencrypt:/etc/letsencrypt \
      -v /var/www/html:/var/www/html \
      certbot/certbot:v1.26.0 certonly --webroot -w /var/www/html -d example.com
```

`docker run` が多段なのでわかりにくいけれど、まあうん、よく見ればわかる。なんとか。

ちなみに docker クライアントを入れているけれど、このためにコンテナが肥大します。`run`するだけならクライアントを介さず UNIX ドメインソケットに直接書き込めば良いんじゃないかなと思います（REST APIなので、身構えなくても大丈夫です）。

コードは、絶対何か見落としがあるだろうと直感できるくらいに簡単なので、**無理してベースコンテナなどを使わず、自作しても良い**と思います。

## docker-compose に仕掛けたい

nginx をリバースプロキシにしつつ、配下にアプリケーションサーバを置き、さらに certbot の定期実行を仕掛けておきたい、というのが今回の話です。

こんな感じの `docker-compose.yml` になりました。

```yml:docker-compose.yml
version: "3"
services:
  proxy:
    image: nginx:1.21.6-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - $PWD/config/nginx.conf:/etc/nginx/conf.d/default.conf
      - $PWD/log:/var/log/nginx
      - $PWD/public:/app/public
    restart: always
  backend:
    image: webapp:latest
    ports:
      - "8001:8001"
    volumes:
      - $PWD/log:/app/log
      - $PWD/data:/app/data
    restart: always
  certbot_runner:
    image: docker-runner:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/letsencrypt/log:/var/log/letsencrypt
      - $PWD/letsencrypt/etc:/etc/letsencrypt
      - $PWD/public:/var/www/html
    command: [
      "--initial-wait", "1m",
      "--interval", "168h",
      "docker", "run", "--rm", "--name", "certbot",
      "-v", "/var/log/letsencrypt:/var/log/letsencrypt",
      "-v", "/etc/letsencrypt:/etc/letsencrypt",
      "-v", "/var/www/html:/var/www/html",
      "certbot/certbot:v1.26.0", "certonry", "--webroot", "-w", "/var/www/html", "-d", "example.com"
    ]
```

nginx よりも先に certbot が動くと困るので、 `--initial-wait` なる雑なオプションを作って少し待たせています。ちょっと使いたいだけだったため、がんばりたくなかったのです…。

一方で、 certbot が一度も動かない状態で nginx を起動しようとすると、 `letsencrypt/live/*` がなくてエラーが起きますね。まあどうせ最初は Agreement とかもありますから、初回だけは手でやってそうですが（と思ったのでスキップするオプションを使っていないですが）、注意しましょう。

# まとめ

cronで苦労するなら作っちゃうのはどうでしょう。