コンテナレジストリの可用性を高める取り組み

こんにちは、Necoチームの池添です。

みなさんKubernetes向けのコンテナレジストリにはどこのサービスを利用していますか?そのサービスの調子が悪くて困ったりしたことはありませんか? 今回はコンテナレジストリをKubernetesクラスタ上にセルフホストし、システムの可用性を高める取り組みについて紹介したいと思います。

セルフホストコンテナレジストリがなぜ必要か

コンテナレジストリには、Docker Hub, Red Hat Quay, GitHub Container Registry (GHCR), さらには各種パブリッククラウドベンダーによるものなど、数多くのサービスが存在します。

これらのコンテナレジストリのいずれかひとつに頼っていると、そこが単一障害点になってしまいます。 コンテナレジストリがダウンすると新しいコンテナを立ち上げる事ができなくなり、障害につながる場合もあります。

また昨年、Docker Hubの無料プランのPull Rate Limitが発表され、話題にもなりました。 Rate Limitに引っかかると、コンテナレジストリに障害が起きていなくてもコンテナイメージが取得できなくなってしまいます。

さらには、コンテナイメージのダウンロード時間や、一度に大量のノードにコンテナイメージをダウンロードする際の通信量を削減したいという要望もあるでしょう。

これらの問題を解決するため、コンテナイメージレジストリをセルフホストしたいという需要があります。

セルフホストコンテナレジストリ

コンテナレジストリをセルフホストしたい場合、HarborやDocker Registryを利用するのが一般的でしょう。

goharbor.io

docs.docker.com

Harborはマルチテナントによる利用、Web UI、セキュリティチェックなどをサポートした多機能なコンテナレジストリです。

一方のDocker Registryは、必要最低限の機能を提供するシンプルなコンテナレジストリ実装です。

また少し毛色は違いますが、P2Pでコンテナイメージを配布するhttps://d7y.io/en-usや、Upstreamのコンテナイメージの変更を検知して自動でキャッシュするtaggerのようなOSSもあります。

コンテナイメージ名の解決方法

Kubernetes上にPodをデプロイする場合、マニフェストには以下のようにコンテナイメージの名前を指定します。

spec:
  containers:
  - name: ubuntu
    image: quay.io/cybozu/ubuntu:20.04

通常、複数のコンテナレジストリが存在する場合、利用するレジストリに応じてイメージ名を変更しなければなりません。

例えば、Quay上のコンテナイメージを利用する場合はquay.io/cybozu/ubuntu:20.04、セルフホストしたレジストリのコンテナイメージを利用する場合はself-hosted-registry:5000/ubuntu:20.04を指定するといった具合です。

このイメージ名を手動で書き換えることは非常に不便です。

この問題を解決するためには、以下のような方法が考えられます。

  • ミラーリング & Pull Through Cache方式
  • Man in the Middle方式
  • Mutating Admission Webhook方式

この3つの方式を解説していきます。

ミラーリング & Pull Through Cache方式

Kubernetesで利用可能なコンテナランタイムの多くは、コンテナレジストリのミラーリングを設定することができます。

例えばcontainerdでは以下のような設定をおこないます。

[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"]
    endpoint = ["http://self-hosted-registry:5000", "https://quay.io"]

この設定により、quay.ioから始まるコンテナイメージを利用する際に、まずセルフホストしたコンテナレジストリ(http://self-hosted-registry:5000)に対してPullを試みます。

複数のコンテナレジストリを指定することができる

そこでもしコンテナイメージが見つからなかったり、コンテナレジストリに接続できなければ、アップストリームのコンテナレジストリ(https://quay.io)からPullします。

片方のコンテナレジストリが故障した場合、フォールバックされる

さて、ミラーリング設定により利用するコンテナレジストリを複数設定することが可能になりました。 このミラーリングを有効に利用するためには、セルフホストしたコンテナレジストリに、アップストリームに存在するコンテナイメージをPushしておく必要があります。 しかし、事前に必要なコンテナイメージをすべて取得しておくのは少々面倒です。

そこで、Pull Through Cacheという仕組みを利用します。(Docker Registry, Harborのどちらでも利用可能です)

この機能を利用すると、セルフホストしたコンテナレジストリからPullする際に、レジストリがすでに対象のコンテナイメージを保持していた場合はそれを返し、保持していない場合はアップストリームからイメージを取得し、それをキャッシュに保存することができます。

キャッシュにイメージが存在しない場合はUpstreamからPullする

なお、Googleが提供するGCRではDocker HubをPull Through Cacheしてくれる機能があります。 AWSのECRも将来的にサポートされるようです。

Man in the Middle方式

HTTPS_PROXY設定を書き換えて、アップストリームコンテナレジストリへのアクセスをセルフホストコンテナレジストリに変更してしまう方式です。 以下のようなOSSを利用して実現することが可能です。

rpardini/docker-registry-proxy

後述するように、コンテナランタイムにDockerを利用している場合は、Docker Hub以外のミラーリング設定をおこなうことができません。 コンテナランタイムにDockerを利用していて、Docker Hub以外のレジストリをミラーリングしたい場合は、Man in the Middle方式を検討してみてもよいかもしれません。

Mutating Admission Webhook方式

KubernetesのMutating Admission Webhook機能を利用し、Podを作成・更新するタイミングでイメージ名を書き換えてしまう方式です。

下記の実装などが参考になるでしょう。

docker-proxy-webhook

ただしこの方式の場合、セルフホストしたコンテナレジストリが障害を起こした場合にアップストリームにフォールバックするのが難しいため、可用性の観点では課題がありそうです。

注意点

コンテナレジストリをセルフホストし、ミラーリング設定をおこなう際の注意点を紹介します。

Dockerコンテナランタイムのミラーリングはdocker.ioのみ

Dockerコンテナランタイムではdocker.ioしかミラーリングをおこなうことができません。 何年も前から議論されてはいるのですが、実装は進んでいないようです。

Kubernetes 1.20でDockershimがdeprecationになったこともありますし、KubernetesのバックエンドにはDocker以外のコンテナランタイムを利用するのがよいでしょう。

HarborのPull Through CacheはDocker HubとHarbor Registryのみ

Harborのドキュメントには、以下のように記載されています。

Harbor only supports proxy caching for Docker Hub and Harbor registries. https://goharbor.io/docs/2.1.0/administration/configure-proxy-cache/

なお、Docker Registryがサポートするレジストリは明記されていませんが、我々が検証をおこなったところDocker Hub, Quay, GHCRが問題なく動作しました。

Pull Through Cacheのアップストリームは1つだけ

Docker Registry, Harborともに、Pull Through Cacheの接続先となるアップストリームは1つだけしか設定できません。

複数のレジストリに対してPull Through Cacheを利用したい場合は、それぞれのレジストリ用のミラーをセルフホストする必要があります。例えば、Docker Hub, Quay, GHCRに対してPull Through Cacheを利用したい場合は、3つのDocker Registryを立てなければなりません。

Pull Through CacheでimagePullSecretsは利用できない

Kubernetesでは、認証が必要なプライベートコンテナレジストリにアクセスするためにimagePullSecretsという仕組みがあります。

kubernetes.io

しかし、Pull Through Cacheでイメージを取得する際には、imagePullSecretsに指定した認証情報はアップストリームのコンテナレジストリに渡りません。 これはコンテナレジストリの実装に関わらず、Docker, containerd, podman, cri-oのいずれを利用しても同じです。

Pull Through Cacheでプライベートなコンテナレジストリにアクセスするためには、コンテナレジストリにユーザー名とパスワードを指定する必要があります。

ただし、セルフホストしたコンテナレジストリにアクセスできる人は誰でもプライベートコンテナを利用することが可能になります。 必要に応じて、セルフホストしたコンテナレジストリに認証をかけるなどの対策を検討してください。

また、上記でコンテナレジストリに設定できる認証情報は、Docker Registry,Harborともに1つだけのようです。 複数の認証情報を使い分けることができないため、運用でのカバーなどが必要になってくるでしょう。

我々が採用した方式

前提条件として、我々の運用しているKubernetesクラスタでは以下の技術を利用しています。

  • アップストリームコンテナレジストリとしてQuayとGHCRを利用している
  • コンテナランタイムとしてcontainerdを採用している

このことからQuayやGHCRのPull Through Cacheが必要であり、Harborの提供する多くの機能は必要ないため、セルフホストコンテナレジストリとしてDocker Registryを採用しました。

そして、各アップストリームレジストリごとにコンテナレジストリをセルフホストし、ミラーリング & Pull Through Cacheの設定をおこなっています。

興味のある方は、下記のマニフェストや設定もご覧ください。

まとめ

本記事ではコンテナレジストリの可用性を高めるための取り組みとその注意点を紹介しました。 コンテナレジストリの冗長化にはハマりどころが多いので、この記事が少しでもみなさんのお役に立てば幸いです。