クラスター作りすぎでDockerのレートリミットに悩まされてる人、必見! mirror.gcr.ioで「ImagePullBackOff」にオサラバしよう! [Cluster APIにも対応]


## はじめに

Kubernetesクラスタを運用する際、特にCI/CDパイプラインやプロトタイピング中に、Docker Hubのレートリミットに遭遇することがあります。この制限は、イメージのプル失敗(`ImagePullBackOff`)などを引き起こし、非常にうっとうしいです。

これを回避するために、[mirror.gcr.io](https://cloud.google.com/container-registry/docs/pulling-cached-images) をDocker Hubイメージのプルスルーキャッシュとして利用できます。

本稿では、特に[Cluster API](https://cluster-api.sigs.k8s.io/) を用いた**管理クラスタ**と**ワークロードクラスタ**の構成で、全てのクラスターでmirror.gcr.ioをレジストリミラーとして設定し、Docker Hubのレートリミットを回避する方法を解説していきます。

## なぜmirror.gcr.ioを使うのか？

mirror.gcr.ioはDocker Hubイメージをキャッシュしてくれているので、クラスタからは直接Docker HubではなくGoogleのミラーからイメージを取得するようにすると、Docker Hubのレートリミットに悩まされなくなります!

## 前提条件

- [Docker](https://docs.docker.com/get-docker/) のインストール
- [kind](https://kind.sigs.k8s.io/)（Kubernetes IN Docker）のインストール
- [kubectl](https://kubernetes.io/docs/tasks/tools/) のインストール
- [clusterctl](https://cluster-api.sigs.k8s.io/user/quick-start.html#install-clusterctl) のインストール
- [yq](https://github.com/mikefarah/yq) のインストール（YAML処理用）

---

## ステップ1: レジストリミラー設定ファイルの準備

kindで直接起動したクラスター(管理クラスター)のcontainerdがDocker Hubのミラーとしてmirror.gcr.ioを利用できるよう、ディレクトリ構造と設定ファイルを作成します。

```bash
mkdir -p certs.d/docker.io
cat > certs.d/docker.io/hosts.toml <<EOF
server = "https://registry-1.docker.io"

[host."https://mirror.gcr.io"]
  capabilities = ["pull"]

[host."https://registry-1.docker.io"]
  capabilities = ["pull", "resolve"]
EOF
```

このファイルは、containerdにDocker Hubイメージの取得時に、

1. registry-1.docker.ioからマニフェストを取得 (resolve)
1. mirror.gcr.ioからpull

というステップを踏むような設定になっています。

このファイルはkindを実行するホストマシン(macOS)に作成しておきます。

resolveもひとつのAPIコールになるため、Docker Hubのレートリミットの消費につながります。「一切Docker Hubとやりとりしたくない」という場合は次のようにmirror.gcr.ioにresolveも任せることもできます。

```
server = "https://registry-1.docker.io"

[host."https://mirror.gcr.io"]
  capabilities = ["pull", "resolve"]
```

注意点として、`latest`タグなど頻繁に更新されるイメージが、mirror.gcr.ioでは古いままといったことが起りうるので注意してください。

## ステップ2: kindクラスタのミラー設定

以下の内容で `kind-cluster.yaml` ファイルを作成します。これにより、kind内部のcontainerdがミラーを利用し、デバッグログも有効化されます。

```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  ipFamily: dual
nodes:
  - role: control-plane
    image: kindest/node:v1.33.1
    extraMounts:
      # Cluster API Docker Provider（CAPD）がホストのDockerを操作できるようにDockerソケットをマウント
      - hostPath: /var/run/docker.sock
        containerPath: /var/run/docker.sock
      # ミラー設定をcontainerdの設定ディレクトリにマウント
      - hostPath: ./certs.d
        containerPath: /etc/containerd/certs.d
# containerdにdocker.ioとregistry.k8s.ioのイメージ取得時にmirror.gcr.ioを優先させる
containerdConfigPatches:
  - |-
    [plugins."io.containerd.grpc.v1.cri".registry]
      config_path = "/etc/containerd/certs.d"
    # mirror.gcr.ioの利用状況を確認するためデバッグモードを有効化
    # ログ確認例: `docker exec -it <cluster名>-control-plane bash -c 'journalctl -u containerd -f & crictl pull nginx'`
    [debug]
      level = "debug"
```

これもホストマシンに作成しておきます。


**各項目の詳細解説：**

- `extraMounts`:
  - CAPDがDockerを操作するためのソケットマウント。
  - ミラー設定（certs.d）をノード内のcontainerd設定ディレクトリにマウント。
- `containerdConfigPatches`:
  - containerdにカスタムレジストリ設定パスを指定。
  - デバッグログを有効化し、イメージプルやミラー利用がはっきりと解るようにする。



## ステップ3: 管理クラスタの作成

```bash
kind create cluster --name management --config kind-cluster.yaml
```

このコマンドで、カスタムレジストリ設定とデバッグログが有効な管理クラスタが作成されます。ミラーが利用されているかは、以下のコマンドで確認できます：

```bash
docker exec -it management-control-plane bash -c 'crictl pull hello-world && journalctl -u containerd' | grep fetch | grep mirror.gcr.io
```

`mirror.gcr.io`からイメージが取得されているログが表示されれば成功です。

## ステップ4: 管理クラスタへのCluster APIインストール

```bash
export CLUSTER_TOPOLOGY=true
clusterctl init --infrastructure docker
```

これで管理クラスタにCluster APIコンポーネントがインストールされます。


## ステップ5: ワークロードクラスタテンプレートへのミラーパッチ適用

Cluster APIでワークロードクラスタを作成する際、ノードにもミラー設定が反映されるよう、クラスタテンプレートに事前コマンドを注入します。

以下の内容で `patch.yaml` を作成します：

```yaml
- name: containerd-customization
  description: "containerdのミラー・デバッグ・再起動設定"
  definitions:
    - selector:
        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
        kind: KubeadmControlPlaneTemplate
        matchResources:
          controlPlane: true
      jsonPatches:
        - op: add
          path: /spec/template/spec/kubeadmConfigSpec/preKubeadmCommands
          value:
            - |
              set -e

              # 1. ミラー用hosts.toml作成
              mkdir -p /etc/containerd/certs.d/docker.io
              cat <<EOF > /etc/containerd/certs.d/docker.io/hosts.toml
              server = "https://registry-1.docker.io"

              [host."https://mirror.gcr.io"]
                capabilities = ["pull"]

              [host."https://registry-1.docker.io"]
                capabilities = ["pull", "resolve"]
              EOF

              # 2. デバッグ設定を冪等に追加
              grep -qF '[debug]' /etc/containerd/config.toml || cat <<'TOML' >> /etc/containerd/config.toml
              [debug]
                level = "debug"
              TOML

              # 3. registry config_pathを冪等に追加
              grep -qF '[plugins."io.containerd.grpc.v1.cri".registry]' /etc/containerd/config.toml || cat <<'TOML' >> /etc/containerd/config.toml

              [plugins."io.containerd.grpc.v1.cri".registry]
                config_path = "/etc/containerd/certs.d"
              TOML

              # 4. containerdを再起動して反映
              systemctl restart containerd
    - selector:
        apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
        kind: KubeadmConfigTemplate
        matchResources:
          machineDeploymentClass:
            names: ["default-worker"]
      jsonPatches:
        - op: add
          path: /spec/template/spec/preKubeadmCommands
          value:
            - |
              set -e

              # 1. ミラー用hosts.toml作成
              mkdir -p /etc/containerd/certs.d/docker.io
              cat <<EOF > /etc/containerd/certs.d/docker.io/hosts.toml
              server = "https://registry-1.docker.io"

              [host."https://mirror.gcr.io"]
                capabilities = ["pull"]

              [host."https://registry-1.docker.io"]
                capabilities = ["pull", "resolve"]
              EOF

              # 2. デバッグ設定を冪等に追加
              grep -qF '[debug]' /etc/containerd/config.toml || cat <<'TOML' >> /etc/containerd/config.toml
              [debug]
                level = "debug"
              TOML

              # 3. registry config_pathを冪等に追加
              grep -qF '[plugins."io.containerd.grpc.v1.cri".registry]' /etc/containerd/config.toml || cat <<'TOML' >> /etc/containerd/config.toml

              [plugins."io.containerd.grpc.v1.cri".registry]
                config_path = "/etc/containerd/certs.d"
              TOML

              # 4. containerdを再起動して反映
              systemctl restart containerd
```

**各項目の詳細解説：**

- `name`と`description`: パッチの目的を示すメタデータ。
- `definitions`: ノード種別ごとのパッチ定義リスト。
- 各`selector`は特定のCluster APIテンプレート種別（コントロールプレーンまたはワーカーノード）をターゲット。
- `jsonPatches`: ノードの起動プロセスにコマンドを注入するJSONパッチ操作。
- `preKubeadmCommands`内のシェルスクリプトは：
  1. **ミラー設定作成**: containerd用のhosts.tomlを作成。
  2. **デバッグログ有効化**: containerd設定にdebugセクションを追加。
  3. **registry config_path設定**: containerdにミラー設定パスを認識させる。
  4. **containerd再起動**: 設定反映のためサービス再起動。
- このパッチはコントロールプレーン・ワーカーノード両方に適用され、全ノードでミラーが利用されます。

なんだか込み入ってますが、kindで行った設定をCluster API版として焼き直したものです。

## ステップ6: ワークロードクラスタマニフェストの生成と適用

このステップでは、ワークロードクラスタのマニフェストを生成し、containerdカスタマイズパッチを適用してから管理クラスタに適用します。

```bash
clusterctl generate cluster workload \
  --flavor development \
  --kubernetes-version v1.33.1 \
  --control-plane-machine-count=1 \
  --worker-machine-count=1 \
  | yq \
  | tee workload.original.yaml \
  | yq 'select(.kind == "ClusterClass").spec.patches += load("patch.yaml")' \
  | tee workload.patched.yaml \
  | kubectl apply -f -
```

各コマンドの意味と流れは以下の通りです：

- **クラスタマニフェスト生成**: `clusterctl generate cluster workload ...` で新しいワークロードクラスタのYAMLマニフェストを生成します（トポロジー、マシンテンプレート、設定など）。
- **yqでYAML整形**: 最初の`yq`でYAMLをパースし、後続処理しやすくします。
- **オリジナル保存**: `tee workload.original.yaml`で未パッチのマニフェストを保存。
- **ClusterClassへパッチ適用**: 2つ目の`yq`で`ClusterClass`の`spec.patches`に`patch.yaml`の内容を追加。これによりクラスタ作成時にpreKubeadmCommands（ミラー設定）がノード起動時に注入されます。
- **パッチ済みマニフェスト保存**: `tee workload.patched.yaml`でパッチ済みマニフェストを保存。
- **クラスタへ適用**: `kubectl apply -f -`でパッチ済みマニフェストを管理クラスタに適用し、ワークロードクラスタが作成されます。

workload.original.yamlとworkload.patched.yamlは、パッチ適用の前後をご確認いただきやすくするために保存しています。

## ステップ7: ワークロードクラスタでミラー利用を確認

ワークロードクラスタのノードがミラーを利用しているか確認するには、以下を実行します：

```bash
for container in $(kubectl --context=kind-management get machines -o custom-columns=NAME:.metadata.name --no-headers); do
  echo
  echo === $container ===
  docker exec -it $container bash -c 'crictl pull hello-world && journalctl -u containerd' | grep fetch | grep mirror.gcr.io
done
```

各ノードで`mirror.gcr.io`からイメージが取得されているログが表示されれば成功です。

## ステップ8: (オプション) CNIプラグインのインストール

例：Calicoをインストールする場合

```bash
kubectl --context=kind-workload apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
```

## まとめ

以上のようにすると、管理クラスタ・ワークロードクラスタの両方でmirror.gcr.ioをDocker Hubイメージのプルスルーキャッシュとして利用できます。これによりDocker Hubのレートリミットを回避し、Kubernetesワークロードのイメージプルが安定します。是非試してみてください! 最後までお読みいただき、ありがとうございました!

## 参考リンク

- [Google Container Registry Mirror](https://cloud.google.com/container-registry/docs/pulling-cached-images)
- [kind Registry Configuration](https://kind.sigs.k8s.io/docs/user/local-registry/)
- [Cluster API Book](https://cluster-api.sigs.k8s.io/)