閉域網クラスタにイメージを持ち込む三つの道
一言でいうと
エアギャップ環境のクラスターのノードは、イメージを自分では入手できません。誰かがランタイムのイメージストアにイメージを入れてあげなければならず、入れたあとも、Podのプルポリシーが、レジストリに問い合わせに出ていかないようになっている必要があります。k3sは、このために、プライベートレジストリ、ノードごとのイメージアーカイブ、組み込みレジストリのミラーの3つの道を用意しています。
なぜ必要なのか
顧客のセキュリティ規定で、本番のクラスターはインターネットに届きません。納品の前日にマニフェストを適用すると、PodがErrImagePullを経て、ImagePullBackOffにとどまります。接続された開発環境では、一度も見たことのない状態です。そこでは、kubeletが、イメージを自動で取得してきたからです。エアギャップ環境には、その「自動で」がありません。FDEがすることは2つです。接続された場所で、何を持っていくかを決めて運び、受け取った側で、それが運ぶ前と同じものだと証明してから、ランタイムに入れることです。
どう動くのか
Podは、レジストリではなく、ノードのランタイムのイメージストアを見ます。kubeletは、コンテナを起動するとき、プルポリシーに従います。Kubernetesのドキュメントによると、IfNotPresentは、ローカルにないときだけ取得し、Neverは取得しに行かず、Alwaysは、コンテナを起動するたびに、ランタイムがレジストリに連絡して、タグをダイジェストに解決し、ないレイヤーだけを取得します。ポリシーを書かなければ、タグが:latestであるか、ないときはAlways、それ以外のタグならIfNotPresentになります。プルに失敗すると、kubeletは、指数的に増える間隔で再試行し、その間隔は300秒で止まります。そのため、持ち込んだ直後でも、Podが数分間じっとしていることがあります。
k3sの3つの道。k3sのエアギャップインストールのドキュメントは、イメージを入れる方法を3つに分けています。
사설 레지스트리 /etc/rancher/k3s/registries.yaml 로 미러·인증을 설정. 바꾸면 노드마다 k3s 재시작
노드에 직접 배포 /var/lib/rancher/k3s/agent/images/ 에 이미지 tar 를 둔다
내장 레지스트리 미러 한 노드의 containerd 저장소에 있는 이미지를 다른 노드가 받아 간다
プライベートレジストリのドキュメントには、見落としやすい文があります。containerdには、すべてのレジストリに対する暗黙のデフォルトのエンドポイントがあり、registries.yamlに別のエンドポイントを書いても、そのデフォルトのエンドポイントが、最後の手段として必ず試されます。また、レジストリを書かないイメージ名は、歴史的な理由で、docker.ioと見なされます。エアギャップ環境で、nginx:1.25のように短く書いたマニフェストが、どこに問い合わせに行くのかが、ここで決まります。
イメージのディレクトリは、いつ読まれるのか。エアギャップのドキュメントは、このディレクトリのアーカイブを、k3sが起動するたびに取り込むと書いています。削除されたり整理されたりしたイメージがあっても、常に再び揃えるためで、その代わり、すべてのアーカイブを処理するまで、kubeletが起動しないので、起動が遅くなります。そこで、2025年5月のリリース(v1.33.1+k3s1、v1.32.5+k3s1など)からは、ディレクトリに.cache.jsonを置くと、サイズと更新時刻が同じアーカイブを飛ばす、条件付きの取り込みが使えます。この場合、削除されたイメージは、ctr image importで手動で取り込み直すか、アーカイブをtouchする必要があります。一方、イメージの取り込みのドキュメントは、実行中に入れたtarも取り込む機能が、2025年1月のリリース(v1.32.0+k3s1、v1.31.5+k3s1、v1.30.9+k3s1、v1.29.13+k3s1)からで、それ以前は、ブート時にだけ取り込んでいたと書いています。顧客のクラスターのk3sのバージョンを、先に確認する必要がある理由です。同じディレクトリに、イメージ名を行ごとに書いたテキストファイルを置くと、逆にオンラインで事前に取得する用途になる点も、混同しないでください。
持ち込みは、ハッシュで終わります。運ぶ前にアーカイブのsha256を記録し、受け取った側で、同じコマンドで照合してから入れます。入れたあとは、ランタイムが報告するイメージID(設定のハッシュ)が、アーカイブの中の設定のハッシュと同じかを見ます。この2つがつながってはじめて、「顧客のクラスターで動いているものが、私たちが検証したそれだ」と言えます。
現場での姿
ラボのVMで、こう測ってみました(実測、v1.33.3+k3s1、containerd v2.0.5-k3s2)。外への送信をREJECTで塞ぐと、https://registry.k8s.io/v2/に対するcurlのステータスコードが、401から000に変わり、新しいPodは、作ってから2秒以内に、ErrImagePullを経て、ImagePullBackOffになりました。イベントの原因の行は、failed to resolve referenceで始まり、Head "https://registry.k8s.io/v2/e2e-test-images/busybox/manifests/1.36.1-1"のリクエストが、connection refusedで終わったと書いています。レイヤーを取得する前に、タグをダイジェストに解決しようとして、レジストリに尋ねる最初のリクエストで、すでに塞がれているのです。busyboxのアーカイブ(4.5MB)を、k3s ctr -n k8s.io images importで入れると、出力にsavedと、マニフェストのダイジェストが表示されました。ただし、この出力は、進捗の表示が混ざっていて、ファイルとして保存したイメージ名が、ハイフンとタグが欠けた形で残りました。持ち込みの記録は、名前よりダイジェストで照合するほうが安全です。nginxのアーカイブ(17MB)を、動いているk3sのイメージのディレクトリにコピーすると、k3sのログに、Importing images fromとImported images ... in 1.094242885sが出力され、再起動なしで、ランタイムのイメージストアに現れました。持ち込んでおいたbusyboxで、imagePullPolicy: AlwaysのPodを作ると、イメージがイメージストアにあるのに、同じHEADリクエストの失敗で、ErrImagePullになりました。
2番目に多い事故は、持ち込みを終えたあとに起きます。誰かが、マニフェストにimagePullPolicy: Alwaysを書いていたり、タグをlatestにしていたりします。イメージはイメージストアにあるのに、kubeletは、レジストリに問い合わせに出ていき、塞がれたネットワークで失敗します。このとき、PodのコンテナのimagePullPolicyは、作ったあとでは変更できないので、Podを作り直す必要があります。遮断を少し解いて通すことは、接続された環境ならできますが、顧客のエアギャップ環境には、その選択肢がありません。
エアギャップ環境を模擬するときにも、落とし穴があります。このラボのVMは、外から入ってくる採点の接続(8899)で操作されるので、外への送信を塞ぎながら、すでに確立した接続と、ループバック、クラスター内部の帯域を生かしておかないと、採点とAPIが一緒に切れます。ルールは、1つのチェーンにまとめて、何度動かしても同じ形になるようにしておかなければ、引き継いだ人が再び動かしても安全にならません。
実務で本当に大切なこと
- 持ち込みの一覧は、タグではなく、イメージ名全体(レジストリを含む)で書きます。短い名前は、docker.ioに解決されます。
- アーカイブのハッシュを、運ぶ前に記録し、入れる前に照合し、入れたあとに、ランタイムのイメージIDと照合します。
- Kubernetesが見るcontainerdのネームスペースは、k8s.ioです。別のネームスペースに入れたイメージは、Podにとって、ないのと同じです。
- マニフェストのプルポリシーとタグを、持ち込みの前に点検します。Alwaysとlatestは、エアギャップ環境で、失敗を予約します。
- イメージのディレクトリへの持ち込みが、実行中にもできるかは、k3sのバージョンによります。バージョンを記録にあわせて残します。
- 持ち込みの記録は、人が書き写さず、ランタイムとPodから取り出して作ります。
次のラボですること
接続された状態のk3sのVMで、2つのイメージをアーカイブとして取得してハッシュを記録し、iptablesで外への送信を塞いで、エアギャップ環境を作ります。塞がれた状態で、新しいイメージのPodがプルに失敗することを証拠として残し、k3s ctr images importとイメージのディレクトリの2つの経路で持ち込んで、Podを起動します。持ち込んでおいたイメージで、AlwaysのPodが失敗することを確認し、ポリシーを直したあとで、持ち込みの記録を、ランタイムと照合して残します。
参考ドキュメント: K3s Air-Gap Install、K3s Import Images、K3s Private Registry Configuration、Kubernetes Images(imagePullPolicy)