GCPから毎月100円に満たない請求が来ていた。放っておいても困る額ではない。それでも調べて、止めた。
正体はArtifact Registryに置いていた約4GBのDockerイメージの保存料だった。無料枠0.5GBを超えた分に $0.10/GB で、月 $0.35 ほど。自宅のRaspberry Pi 5で動かしているDocker Swarmが、そこからイメージを引いていた。
なぜ100円のために動いたのか
前に、趣味のサイトでCloudflare D1の請求が2か月で$240に膨らんだことがある。あのときの教訓は「使いすぎた」ではなく、従量課金の口を開けたまま放置したことだった。
だからルールを決めた。従量課金のデータストアを新規に採用しない。既にあるものは減らす。 金額の大小ではなく、口の数を数える。
月100円のArtifact Registryは、金額としては無害だ。でも「請求が$0以外なら内訳を調べる」という運用にしていると、毎月この100円を確認するコストのほうが高い。請求を$0にしてしまえば、次から「$0以外なら異常」という一目で分かる状態になる。そのための移行だった。
イメージを使っているのは自宅のRaspberry Pi 5だけで、外からは引かない。わざわざクラウドに置く理由が、そもそも無かった。
registry:2 を立てる
Raspberry Pi 5の上に、公式の registry:2 を常駐させた。
docker run -d --restart=always --name registry -p 5000:5000 \
-v /home/omikuji/registry-data:/var/lib/registry \
-v /home/omikuji/registry-certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry:2
移行後のストレージは728MBに収まった。クラウド側で4GBあったのは、過去のタグが積み上がっていたからだ。使っているイメージだけを持てば、実際に要る容量は1/5だった。
TLS: daemon.jsonを触らずに自己署名を通す
Dockerはレジストリに対してHTTPSを要求する。自己署名証明書を使う場合、よく紹介されるのは daemon.json に insecure-registries を書く方法だが、これはdockerデーモンの再起動が要るうえ、TLS検証を丸ごと切ることになる。
再起動も不要で、検証も効かせたままにできる方法がある。 CA証明書を決められた場所に置くだけだ。
/etc/docker/certs.d/home.local:5000/ca.crt
/etc/docker/certs.d/192.168.1.141:5000/ca.crt
/etc/docker/certs.d/localhost:5000/ca.crt
ディレクトリ名が「ホスト名:ポート」そのものになる。ここが要点で、home.local:5000 で引くなら home.local:5000 というディレクトリが要る。IPアドレスで引くこともあるなら、そのIPのディレクトリも別に要る。引く側が使う表記ごとにディレクトリを作る必要がある。
証明書のSANも同じ発想で、使いうる名前を全部入れておく。
home.local / home / localhost / 192.168.1.141 / 127.0.0.1 / 172.17.0.1 / 10.37.0.3
172.17.0.1 はDockerブリッジのゲートウェイ、最後のはオーバーレイネットワーク側のアドレスだ。コンテナの中から引くときにこれらが使われることがある。あとから足すには証明書を作り直すしかないので、思いつく限り入れておくほうがいい。
有効期限は10年にした。自己署名で、閉じたLAN内で、更新の手間を増やしたくないので。期限を10年にしたなら、期限そのものを監視対象に入れておくのがセットだ。10年後の自分は絶対に覚えていない。
移行: 元のレジストリから1バイトも引かない
イメージの移行で、最初は「GCPからpullしてローカルにpushし直す」つもりだった。4GBの往復になる。
やめた。稼働中のコンテナが使っているイメージは、既にローカルのDockerキャッシュにある。 再タグしてpushすればいい。
docker tag <元のイメージ> home.local:5000/omikuji/<name>:latest
docker push home.local:5000/omikuji/<name>:latest
これで元のレジストリへのアクセスはゼロになる。ネットワーク転送も、GCPからの下り課金も発生しない。移行したのはair / prometheus / solar / thanos / watcher / mirai の6種だった。
副作用として、「いま動いているものと完全に同じイメージ」が移るという利点もある。レジストリのlatestタグが指すものと、実際に動いているものがずれている、という事故が起きない。
切り替えは1サービスずつ
Swarmのサービス定義を順次書き換えた。
docker service update --image home.local:5000/omikuji/<name>:latest <service>
対象は9件(prometheus、solar、watcher、thanosの4コンポーネント、ほか)。1件ずつ流して、収束を確認してから次に進んだ。全部終わったあと、サービス定義に元のレジストリへの参照が1つも残っていないことを確認した。
移行が終わってから、クラウド側のリポジトリを削除した。順序が逆だと、切り替えに失敗したときに戻せなくなる。
副産物: 6か月止まっていたサービスが見つかった
切り替えの過程で、Swarmのサービスが1つ、6か月前から停止していたことが分かった。
以前はネットワークエラーで落ちていて、今回の切り替えでコンテナは起動するようになった。ただし今度はSlackトークンの失効(account_inactive)で exit 1 している。レジストリとは無関係の、アプリ側の問題だ。
6か月気づかなかったということは、そのサービスが止まっていても誰も困らなかったということでもある。これはこれで情報だった。
同時に、Swarmに旧クラウドノードがDownのまま残っていることも見つかった。普段触らない層を触ると、放置されていたものが出てくる。 移行そのものより、この棚卸しのほうが価値があったかもしれない。
やってよかったか
月100円のために、作業は同日中に終わったとはいえ、時給換算すれば赤字だ。
それでも良かったと思っている理由が3つある。
- 請求が$0になった。 「$0以外なら異常」という判定が使えるようになり、毎月の確認が一瞬で終わる
- 従量課金の口が1つ減った。 金額ではなく、監視すべき対象の数が減った
- 止まっているサービスと死んだノードが見つかった。 これは副産物だが、実質これが一番の収穫だった
逆に、公開サービスを自宅に置くのは今でも勧めない。以前それをやって痛い目を見て、全部手放した記事を書いた。手放したのは「外向けのサービス」で、今も手元にあるのは「自分だけが使う道具」だ。レジストリはまさに後者で、外から引かないものをわざわざクラウドに置く理由は無かった。
外向けだけ畳んで、手元の道具は残す。 その線引きのうえなら、自宅サーバーは十分に働いてくれる。
まとめ
- 従量課金は金額ではなく口の数で管理する。$0にできるなら$0にしたほうが、その後の確認が楽になる
registry:2は数行で立つ。使っているイメージだけ持てば容量は劇的に減る(4GB → 728MB)- 自己署名証明書は
daemon.jsonを触らず/etc/docker/certs.d/<ホスト名:ポート>/ca.crtに置けば通る。デーモンの再起動も不要 - ディレクトリ名は「引く側が使う表記」ごとに要る。 ホスト名で引くならホスト名、IPで引くならIPのディレクトリを別に作る
- 証明書のSANには、Dockerブリッジやオーバーレイのアドレスまで入れておく。あとから足すには作り直すしかない
- 移行は稼働中コンテナのローカルキャッシュを再タグすれば、元のレジストリから1バイトも引かずに済む
- 元のリポジトリを消すのは、切り替えが全部終わってから
- 長い有効期限を設定したら、期限そのものを監視に入れる