kind (Kubernetes IN Docker)におけるローカルイメージpull失敗とimagePullPolicyについて


[kind](https://github.com/kubernetes-sigs/kind) (Kubernetes IN Docker)クラスタでローカルdockerイメージ利用する際にちょっとハマってしまった。 結論からいうと、[imagePullPolicy](https://kubernetes.io/docs/concepts/containers/images/#updating-images)の理解が足りていなかっただけなのだが、同じ様にハマってしまう人も多いのではないかと思うので、設定・確認につかったコマンドを含めて記録として残しておく。

## kindクラスタの作成

まずは、1 x control planeノードと2 x workerノードなkindクラスタを作成したいので、次のようなkindクラスタ構成ファイルを作成する

```bash
cat << EOF | > kind-cluster.yaml 
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4 
nodes:
- role: control-plane
- role: worker
- role: worker
EOF
```

続いて、kind cliコマンドで、k8s v1.19.11のノードイメージと上記で作成したkind構成ファイルを指定してkindクラスタを作成する

```bash
K8S_NODE_IMAGE=v1.19.11
kind create cluster --name my-kind-cluster \
--image=kindest/node:${K8S_NODE_IMAGE} \
--config kind-cluster.yaml
```
作成されたkindクラスタを確認する

```bash
# kind クラスタ一覧を表示
kind get clusters

> my-kind-cluster

# kubectlでノード一覧を表示
kubectl get nodes

NAME                            STATUS   ROLES    AGE     VERSION
my-kind-cluster-control-plane   Ready    master   3m32s   v1.19.11
my-kind-cluster-worker          Ready    <none>   2m59s   v1.19.11
my-kind-cluster-worker2         Ready    <none>   2m59s   v1.19.11
```

## サンプルアプリ用イメージの作成

サンプルイメージ用のDockerfileを作成

```bash
cat << EOF | > Dockerfile
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EOF
```

作成したDockerfileでサンプルイメージをビルド

```bash
docker build -t sample-app .
```

`sample-app:latest`が作成されていることを確認

```bash
docker images

REPOSITORY                     TAG                 IMAGE ID       CREATED          SIZE
sample-app                     latest              e31ba19f294d   23 minutes ago   22.9MB
```

## latestタグの付いたdockerイメージをkindクラスタノードにロード

`kind load docker-image`コマンドで作成したサンプルイメージをkindクラスタノードにロードする。

```bash
kind load docker-image sample-app:latest --name my-kind-cluster

Image: "sample-app:latest" with ID "sha256:e31ba19f294d720f3d5dfeaa421d6ca84a25fa0f324824df7608370a67c8ead6" not yet present on node "my-kind-cluster-worker2", loading...
Image: "sample-app:latest" with ID "sha256:e31ba19f294d720f3d5dfeaa421d6ca84a25fa0f324824df7608370a67c8ead6" not yet present on node "my-kind-cluster-control-plane", loading...
Image: "sample-app:latest" with ID "sha256:e31ba19f294d720f3d5dfeaa421d6ca84a25fa0f324824df7608370a67c8ead6" not yet present on node "my-kind-cluster-worker", loading...
```

上記出力内容からcontrol planeノードとworkerノード x 2 全てにイメージがロードされたことが分かる。

さらに、各ノードにロードされているイメージ一覧を確認してみる。これは各ノード用に立ち上がったdockerコンテナ中にロードされたイメージ一覧を見ればよい。

まずは、次のコマンドで各ノード用に立ち上がったdockerコンテナ一覧を確認。

```bash
docker ps

CONTAINER ID   IMAGE                   COMMAND                  CREATED          STATUS          PORTS                       NAMES
87e9f413cb38   kindest/node:v1.19.11   "/usr/local/bin/entr…"   28 minutes ago   Up 28 minutes                               my-kind-cluster-worker2
3140c46409fc   kindest/node:v1.19.11   "/usr/local/bin/entr…"   28 minutes ago   Up 28 minutes   127.0.0.1:60149->6443/tcp   my-kind-cluster-control-plane
ef0eba3986d9   kindest/node:v1.19.11   "/usr/local/bin/entr…"   28 minutes ago   Up 28 minutes                               my-kind-cluster-worker
```

それでは、この中の`my-kind-cluster-worker`という名前のworkerノード用コンテナにロードされたイメージ一覧を確認する。

kindのノード内部ではコンテナランタイムにdockerdではなくCRI互換のcontainerdを使っている。　よって、デーモンとのやり取りにはdockerコマンドではなく[crictl](https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md) (Container Runtime Interface CLI)を利用する。 以下のように`my-kind-cluster-worker`コンテナ内のイメージ一覧をcrictlコマンドで取得する。

```bash
docker exec -it my-kind-cluster-worker crictl images

IMAGE                                      TAG                  IMAGE ID            SIZE
docker.io/kindest/kindnetd                 v20210326-1e038dc5   6de166512aa22       54MB
docker.io/library/sample-app               latest               e31ba19f294d7       24.5MB
docker.io/rancher/local-path-provisioner   v0.0.14              e422121c9c5f9       13.4MB
k8s.gcr.io/build-image/debian-base         v2.1.0               c7c6c86897b63       21.1MB
k8s.gcr.io/coredns                         1.7.0                bfe3a36ebd252       14MB
k8s.gcr.io/etcd                            3.4.13-0             0369cf4303ffd       86.7MB
k8s.gcr.io/kube-apiserver                  v1.19.11             2fb26b7ebd23a       120MB
k8s.gcr.io/kube-controller-manager         v1.19.11             1af1564e1890d       112MB
k8s.gcr.io/kube-proxy                      v1.19.11             d2c2a8b355f56       120MB
k8s.gcr.io/kube-scheduler                  v1.19.11             120d339f7d67a       47.7MB
k8s.gcr.io/pause                           3.5                  ed210e3e4a5ba       301kB
```

sample-app:latestがworkerノードにロードされていることが確認できる。

## サンプルイメージ（sample-app:latest）のpullに失敗

それではサンプルイメージ(sample-app:latest)を指定したKubernetes deploymentリソースを作成して、kindクラスタにデプロイしてみる。

```bash
cat << EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  selector:
    matchLabels:
      app: sample-app
  replicas: 1
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: sample-app
        image: sample-app:latest
        ports:
        - containerPort: 80
EOF

kubectl apply -f deployment.yaml
```

すると、ImagePullBackOffで、起動に失敗する。

```
kubectl get pod

NAMESPACE            NAME                                                    READY   STATUS             RESTARTS   AGE
default              sample-app-6cb94bb4b8-8xvsr                             0/1     ImagePullBackOff   0          88s
```

kubectt describeでEvents情報を参照すると、どうやらworkerノードのローカルに`sample-app:latest`がロードされているにもかかわらず、リモート（`docker.io/library/sample-app:latest`）イメージをpullしようとして失敗していたことが分かる。

```
kubectl describe pod sample-app-6cb94bb4b8-8xvsr

Events:
  Type     Reason     Age                     From               Message
  ----     ------     ----                    ----               -------
  Normal   Scheduled  8m50s                   default-scheduler  Successfully assigned default/sample-app-6cb94bb4b8-8xvsr to my-kind-cluster-worker2
  Normal   Pulling    7m15s (x4 over 8m50s)   kubelet            Pulling image "sample-app:latest"
  Warning  Failed     7m13s (x4 over 8m48s)   kubelet            Failed to pull image "sample-app:latest": rpc error: code = Unknown desc = failed to pull and unpack image "docker.io/library/sample-app:latest": failed to resolve reference "docker.io/library/sample-app:latest": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
  Warning  Failed     7m13s (x4 over 8m48s)   kubelet            Error: ErrImagePull
  Normal   BackOff    6m47s (x7 over 8m47s)   kubelet            Back-off pulling image "sample-app:latest"
  Warning  Failed     3m47s (x20 over 8m47s)  kubelet            Error: ImagePullBackOff
```

## imagePullPolicyについて

行き詰まってしまったのでググってみると、[kind公式ドキュメント](https://kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster)にしっかりとNOTEが書いてあった。

![スクリーンショット 2021-10-01 1.50.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103420/2ae7690a-024b-bc41-6cf8-69eff4e90b25.png)

雑にサマると、これはそもそも[imagePullPolicy](https://kubernetes.io/docs/concepts/containers/images/#updating-images)の動作仕様の話であり、latestイメージタグを使わないか、imagePullPolicyを`IfNotPresent`か`Never`に設定せよということである。

- イメージタグがlatestもしくは省略されている時（デフォルトでlatest）の場合はimagePullPolicyは`Alway`として認識される。つまり、毎回kubeletはコンテナレジストリに同じ名前のイメージを問い合わせる（ただ、同一イメージダイジェストがキャッシュされていればそのローカルキャッシュを使用する）

- イメージタグがlatestではない場合はimagePullPolicyは`IfNotPresent`として認識される。つまり、ローカルに無いときにのみコンテナレジストリに同じ名前のイメージを問い合わせる

ということで、ここでは`latest`タグを使うことをやめ、すなわち`IfNotPresent`なimagePullPolicyとして設定されるようにして、Workerノードのローカルイメージが使われるようにした。

## 新しいタグのサンプルイメージをデプロイ

まずは、さきほどロードしたsample-app:latestイメージを消しておく（別にこれはやる必要はないけど、不要なものは消しておきたい性格なので）

```
docker exec -it my-kind-cluster-worker crictl images

IMAGE                                      TAG                  IMAGE ID            SIZE
docker.io/kindest/kindnetd                 v20210326-1e038dc5   6de166512aa22       54MB
docker.io/library/sample-app               latest               e31ba19f294d7       24.5MB

docker exec -it my-kind-cluster-worker crictl rmi e31ba19f294d7

Deleted: docker.io/library/sample-app:latest
```

つぎに、新しいタグ（ここでは0.0.1）を付与したサンプルイメージをkindクラスターにロードする。

```bash
# sample-appに0.0.1タグを付与
docker tag sample-app sample-app:0.0.1

# sample-app:0.0.1イメージをkindクラスタにロード
kind load docker-image sample-app:0.0.1 --name my-kind-cluster
```

さきほどと同じようにmy-kind-cluster-workerコンテナ内のイメージ一覧をcrictlコマンドで取得して、workerノードにsample-app:0.0.1がロードされたことを確認する。

```
docker exec -it my-kind-cluster-worker crictl images

IMAGE                                      TAG                  IMAGE ID            SIZE
docker.io/kindest/kindnetd                 v20210326-1e038dc5   6de166512aa22       54MB
docker.io/library/sample-app               0.0.1               　　d2c2a8b355f56       24.5MB
```

それではさきほど作成したKubernetes deployment(sample-app)にサンプルイメージ (`sample-app:０．０．１`)を指定してみる

```bash
kubectl set image deployment sample-app sample-app=sample-app:0.0.1
```

ところが、 latestタグでないにも関わらずさきほどと同じ用にImagePullBackOffで起動に失敗する。

```
kubectl describe pod sample-app-856987d994-ws7kh

Events:
  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  93s                default-scheduler  Successfully assigned default/sample-app-856987d994-ws7kh to my-kind-cluster-worker
  Normal   Pulling    45s (x3 over 91s)  kubelet            Pulling image "sample-app:0.0.1"
  Warning  Failed     44s (x3 over 89s)  kubelet            Failed to pull image "sample-app:0.0.1": rpc error: code = Unknown desc = failed to pull and unpack image "docker.io/library/sample-app:0.0.1": failed to resolve reference "docker.io/library/sample-app:0.0.1": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
  Warning  Failed     44s (x3 over 89s)  kubelet            Error: ErrImagePull
  Normal   BackOff    6s (x5 over 89s)   kubelet            Back-off pulling image "sample-app:0.0.1"
  Warning  Failed     6s (x5 over 89s)   kubelet            Error: ImagePullBackOff
```

どうしたものかと思いdeploymentの内容を確認してみると、最初のデプロイで作成されたdeploymentのpodテンプレートで、imagePullPolicyがAlwaysのままだったという残念なミスだった。

```yaml 
spec:
...
  template:
...
    spec:
      containers:
      - image: sample-app:0.0.1
        imagePullPolicy: Always   # <<<< ここ
```

そこで、imagePullPolicyを`IfNotPresent`にして更新してみたところ無事ローカルイメージが使われ、次のようにサンプルイメージの起動が成功した。

```
kubectl get pod

NAMESPACE            NAME                                                    READY   STATUS    RESTARTS   AGE
default              sample-app-58fdfdbc4d-4bnwf                             1/1     Running   0          1m
```

なお、これは単に私がkubectl set image deploymentでイメージのみを更新してしまったことが原因であり、次のように新しくdeploymentから作り直せばimagePullPolicyはIfNotPresentとして設定されるのでこのような問題は起こらない。

```bash
kubectl delete -f deployment.yaml

cat << EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
spec:
  selector:
    matchLabels:
      app: sample-app
  replicas: 1
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: sample-app
        image: sample-app:0.0.1
        ports:
        - containerPort: 80
EOF

kubectl apply -f deployment.yaml
```

## 結論

ここでは　kindクラスタへのデプロイメントを題材としているが、 結局のところkindは関係なく、[imagePullPolicy](https://kubernetes.io/docs/concepts/containers/images/#updating-images
)を理解していれば回避できた問題だった。 latestタグなイメージにおけるデフォルトのimagePullPolicyはAlwaysということで。