レガシーになる python コンテナを作る方法 / debianで古いパッケージを使い続ける方法

# 冪等性をもたせる = レガシー保守の始まり

pythonアプリを作る場合、もっとも利用されるのが公式の docker imageでしょう。

https://hub.docker.com/_/python

私が引き継いだアプリには [python:3.10-slim-bullseye](https://github.com/docker-library/python/blob/7a54933c66171020473b9a4f06aec4e8ffb43a45/3.10/slim-bullseye/Dockerfile)  が使われていました。

python で使う pypi パッケージは `requirements.txt` などの形でバージョンを固定することが一般的です。しかし、我々のチームはできるだけ update をし続け、レガシー化を防ぎたいとの思いから、あえてバージョンを固定しないことも少なくありません。テストがコケて、どうしようもない場合だけバージョンを固定する感じです。それはそれで、ちょっとした改修をしようとしたら全然関係ないエラーが出ることになるので、辛いのですが・・（その流れで authlib に 1 行 PR を出した話は ↓ の記事にあるので興味がある方はどうぞ :)

https://qiita.com/uturned0/items/1ef5b1e257bf0766473d


## 定期的にコケる build

そんなアプリですが、数ヶ月おきに必ず CI build がコケる問題がありました。それは pypi ではなく、debian のパッケージが原因でした。docker build のエラーログはこんな感じです。

```rb
Step 25/34 : RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y foovar=0.0.1-9+deb11u2 && apt-get clean
 ---> Running in 76cfb8fff63b
Get:1 http://deb.debian.org/debian bullseye InRelease [116 kB]
Get:2 http://deb.debian.org/debian-security bullseye-security InRelease [48.4 kB]
Get:3 http://deb.debian.org/debian bullseye-updates InRelease [44.1 kB]
Get:4 http://deb.debian.org/debian bullseye/main amd64 Packages [8183 kB]
Get:5 http://deb.debian.org/debian-security bullseye-security/main amd64 Packages [210 kB]
Get:6 http://deb.debian.org/debian bullseye-updates/main amd64 Packages [14.6 kB]
Fetched 8616 kB in 1s (6120 kB/s)
Reading package lists...
Reading package lists...
Building dependency tree...
Reading state information...
E: Version '0.0.1-9+deb11u2' for 'foovar' was not found
The command '/bin/sh -c apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y foovar=0.0.1-9+deb11u2 && apt-get clean' returned a non-zero code: 100
exit status 100
```

これは、利用しようとしている package `foovar=0.0.1-9+deb11u2` が既にリポジトリから消えているというエラーです。このときは、最後の `u2` が `u3` になったバージョンがリリースされていました。


## Docker image の成り立ち

python の official image は、debian の official image を元にして作られています。 [python の 公式 Dockerfile](https://github.com/docker-library/python/blob/7a54933c66171020473b9a4f06aec4e8ffb43a45/3.10/slim-bullseye/Dockerfile) の一例です。

```rb:Dockerfile
FROM debian:bullseye-slim
```

python image は OS としては debian 公式のままで、そこに `python.tar.xz` を突っ込んだものになっています。見る限り、 apt が使う repository に手を加えていることはないようです。


コンテナの利点は冪等性を持った環境を作れることです。何度作り直しても同じ OS が用意できます。いい加減、勝手に build できなくなる問題に嫌気がさしたのですべてのバージョンを固定することにしました。そもそも冪等性をもたせるためにコンテナを使ってたんだった、僕たちは。


# なぜ古いバージョンを使うのか

そもそもバージョンを指定してインストールしているモチベーションは、アプリの中でそのパッケージを使ったコマンドを発行し、返ってきた stdout を神パースして利用しているところがあるからなのでした。コマンドのバージョンが上がって出力が変わると、パースできなくなる可能性があります。それを防ぐためにその debian package だけはバージョン固定していたのですが、そうするとそのバージョンが debian 側のリポジトリから消えたときに、代わりのバージョンを入れることもなくコケてしまいます。

# OS のバージョンを固定しよう

pythonのイメージは、同じpython versionでも定期的に build し直され、その都度 debian のバージョンを上げているようです。そのため、全く同じ debian がほしければ、python versionではなく、docker imageのbuildそのものを指定する必要があります。

例えば、`python:3.10-slim-bullseye` の現在の image を指すハッシュは以下です。

> DIGEST:sha256:6862d8ed663a47f649ba5aababed01e44741a032e80d5800db619f5113f65434

しかし、これはあなたがこの記事を読む頃には新しい build により異なる digest に変わっているでしょう。

https://hub.docker.com/layers/library/python/3.10-slim-bullseye/images/sha256-6862d8ed663a47f649ba5aababed01e44741a032e80d5800db619f5113f65434?context=explore

私は、アプリが前回正常に build できたバージョンに固定するために、前回の build log から抜き出した digest を使い、Dockerfileをこう変えました。

```css
FROM python:3.10-slim-bullseye

    ↓↓↓　こうします  ↓↓↓

FROM python@sha256:888807ff551bb731823f0ef193c0c47ff1eab95c522d82968cf69a185c30d25b 
```

これで同じ OS を使えるようになりましたが、build エラーは消えません。当時インストールできた `foovar` package が、もう debian の公式リポジトリにないからです。

# 古い debian package をインストールしよう

調べると、apt が使うリポジトリを archive なものに差し替えればいいらしいです。その docker image に入っている `/etc/apt/sources.list` を見てみましょう。

```py
# docker runで中を見てみる
docker run python@sha256:888807ff551bb731823f0ef193c0c47ff1eab95c522d82968cf69a185c30d25b \
 cat /etc/apt/sources.list
```

見れました。

```py:/etc/apt/sources.list
# deb http://snapshot.debian.org/archive/debian/20221004T000000Z bullseye main
deb http://deb.debian.org/debian bullseye main
# deb http://snapshot.debian.org/archive/debian-security/20221004T000000Z bullseye-security main
deb http://deb.debian.org/debian-security bullseye-security main
# deb http://snapshot.debian.org/archive/debian/20221004T000000Z bullseye-updates main
deb http://deb.debian.org/debian bullseye-updates main
```

コメントアウトされている行に `spapshot.debian.org/archive` の文字があります。おお、これはもしかして、優しい人がリポジトリの snapshot を用意してくれているのですね。

早速、コメントを真逆にしてみます。

```py:/etc/apt/sources.list
deb http://snapshot.debian.org/archive/debian/20221004T000000Z bullseye main
# deb http://deb.debian.org/debian bullseye main
deb http://snapshot.debian.org/archive/debian-security/20221004T000000Z bullseye-security main
# deb http://deb.debian.org/debian-security bullseye-security main
deb http://snapshot.debian.org/archive/debian/20221004T000000Z bullseye-updates main
# deb http://deb.debian.org/debian bullseye-updates main
```

これで、古いバージョンでも `apt-get` でインストールできるようになりました。

# Dockerfileを作ろう

OS と repository を固定する作業を Dockerfileに入れました。まず使いたい `sources.list` をファイルとして保存して、

```rb
FROM python@sha256:888807ff551bb731823f0ef193c0c47ff1eab95c522d82968cf69a185c30d25b
COPY sources.list /etc/apt/sources.list
RUN apt-get update -o Acquire::Check-Valid-Until=false  
```

このようにしました。これで、無事に build は成功し、セキュリティアップデートでコケることがなくなりました。（`apt-get update` でリポジトリの情報を読み取るので 忘れないようにしましょう）


# 冪等性 vs セキュリティ

冪等性を持たせるためとはいえ、ソフトウェアのバージョンを固定することにはリスクが伴います。特にセキュリティアップデートを拒むというのは考えものです。今回は閉じた環境で利用するアプリという特性でこうしましたが、インターネットに公開するようなものであれば常に最新版に追随できる体制を整えるべきでしょう。
このアプリは既に python のマイナーバージョンからひとつ遅れている（3.10→3.11) ので、半年に一度は最新 python に追随し、そのときに OS も update するようにしようと考えています。

レガシーにする危険性は忘れずにいましょう。


# あるあるらしい

後から調べてみると、沢山の人が `sources.list` を gist にuploadしていました。みなさんの苦労が忍ばれます。
https://www.google.com/search?q=debian+sources.list+source+github+repository&pws=0&gl=us&gws_rd=cr#gws_rd=cr&ip=1


# おわりに


あらゆるパッケージがあらゆるパッケージに依存している Open source時代。すべてのバージョンが水物で、冪等性を担保することはより困難になっていくと想像しています。上手にレガシーと付き合う旅は永遠に続きます。

それではみなさん、楽しいレガシーライフを！