Kubesprayを利用してKubernetesをデプロイ・アップグレードした時のメモ

# はじめに

Kubesprayで構築したk8sクラスターをできるだけ最新に更新する作業のメモを残しておきます。

:::note warn
何等かの原因でシステムが正常にshutdown処理を完了できない可能性があるため、リモートでアップグレード作業をする場合には強制的にリセットできる手段を確保しましょう。
:::

:::note alert
Kubespray v2.24.2ではクラスターのシャットダウン・再起動後にPodから外部へのネットワーク通信ができなくなる障害が発生しました。v2.24.2はスキップしてv2.25.0を適用しています。
:::

:::note warn
Kubespray v2.27.0では破壊的な変更が多く行われています。必ず[Release Notes](https://github.com/kubernetes-sigs/kubespray/releases)を確認してください。
* 特別なカスタマイズを行っていなくても hosts.yaml のファイル名と形式に変更があります。
:::

# 履歴

ここでは次のようなバージョンアップ時の作業時のメモを残しています。

* v2.11.0(k8s 1.15.3)→v2.11.2(k8s 1.15.11)
* v2.11.2(k8s 1.15.11)→v2.12.5(k8s v1.16.8)
* v2.12.6(k8s 1.16.9)→v2.16.0(k8s v1.17.12)

ここまではRook/CephのFlex-Volumeを利用しており、これ以降はHDDを追加し、k8s自体も新規に入れ直してBluestoreを利用しています。

* v2.15.0(k8s 1.19.7)→v2.15.1(k8s 1.19.9)
* v2.15.1(k8s 1.19.9)→v2.16.0(k8s 1.20.7)
* v2.16.0(ks8 1.20.7)→v2.21.0(k8s 1.25.6)
* v2.21.0(k8s 1.25.6)→v2.27.0(k8s 1.31.4)

バージョンアップが難しい、あるいは、技術的な変更点が多い場合には、新規に構築して移行作業を実施してきました。

いまのところkubespray v2.15.0(k8s v1.19.7)からv2.25.0(v1.29.5)までバージョンアップしているクラスターを一番長く利用しています。

当初はcontainer_engineとしてdockerを長く利用してきましたが、Kubespray v2.21.0(k8s v1.25.6)を適用するタイミングで、dockerからcontainerdへの移行を実施しました。

現在では利用している全てのクラスターでdockerに代わりcontainerdを利用しています。

# 現在利用しているクラスター達

このメモを更新した時点での情報をまとめておきます。

これらのクラスターでは、そのほとんどで以下のサービスを共通で利用しています。

* Rook/ceph (PV/PVCのブロック(RWO)とファイルシステム(RWX)に利用)
* Minio (Object Storageとして利用)
* Prometheus Operator 

Kubesprayのaddons.ymlからは主に次のサービスを有効にしています。

* Helm
* Ingress
* MetalLB

この他にもk8s-cluster.ymlを通じて証明書の定期更新(v2.23以降)を有効にしたり、サービスとしてapi-serverに接続する必要があるクラスターではKube VIP(v2.24以降)を有効にしています。

## クラスター#1 (検証系・教育系)

当時最新だったv2.11.0のタグをcheckoutして、4台のTX120s3を利用してk8sクラスターを構築しました。その後、Kubernetes v1.16.8まではアップグレードをして使い続けました。

その後、Rook/Cephのディスク領域をBluestoreに切り替えるタイミングで、Kubespray(v2.15.0, kubernetes v1.19.7)を新規にインストールし、アップグレードを続けています。

Kubespray v2.21.0(k8s v1.25.6)だった時にUbuntu 20.04→22.04にOS自体もアップグレードしています。

現在ではv2.27.0(k8s v1.31.4)にアップグレードしています。

## クラスター#2 (本番系)

4台のRX100s7を利用して、クラスターを構成しています。
Kubesprayのmasterブランチを利用して、Kubernetes v.18.9を稼動させたのが最初です。

しばらくはv2.16.0(k8s v1.20.7)を新規に導入して、これをv2.18.0(k8s v1.22.5)まで更新して利用していました。

現在は常時サービスしているクラスターではないので本番運用をしない時期はN+1環境の検証用として利用しています。

いまは新規にUbuntu 22.04とv2.25.0 (k8s 1.29.5)を導入し、本番系のサービスに利用しています。

## クラスター#3 (テスト系)

4台のTX1310 M3を利用して、Kubernetes v1.16.9から現在はv2.25.0(k8s v1.29.5)が稼動しています。

当初はrook/cephを利用せず内蔵HDD 2台のみのRAID-1構成でしたが、v2.17.0(k8s v1.21.5)にする前後のタイミングでクラスターを一度削除して、boot driveとしてSSD(M.2 SATA)を追加する変更しています。

SSDを追加したため、2台のHDDはそれぞれバラバラにRook/CephのBlueStoreとして利用しています。

その後は順当にバージョンアップを重ねて、クラスター4のテスト系として同じか更新予定の少し新しいバージョンが稼動しています。

サーバー用途では内蔵SASドライブ 2台をRAID-1にすることは珍しくないですが、ニアラインSATAを使った感想としてはRAID-1にすることでかえってシステムが不安定になってしまった印象です。

## クラスター#4 (本番系)

5台のTX1320 M4を利用して、Kubernets v1.19.9のクラスターが稼動させてからアップグレードしています。

TX1320 M4では、M.2 SATAスロットを搭載していますので、OS領域にSSD(SATA M.2)を利用して、HDDはRook/Cephのbluestoreとして利用しています。

UbuntuではM.2 SATAが/dev/sdb, HDDが/dev/sdaとして認識されているので、Boot Driveは/dev/sdb側です。

HDDを2台、M.2 SATAを1台利用する場合には、M.2 SATAが/dev/sdcとして認識されるので、OSの導入をAutoinstallによって自動化する場合などには注意が必要です。

現在はv2.25.0(k8s v1.29.5)が動作しています。

## クラスター#5 (教育系)

いわゆるMiniPCを集めてK8sクラスターを構築しています。

CPUはIntel n100やAMD 3550H、5700Uなどを搭載していて、最低16GBのメモリーを搭載しており、6台で1クラスターを構成しています。

いくつかのノードに2.5inch SSDをRook/CephのBlueStoreとして利用できるよう配置しているため、基本的な実験は全てできるようになっています。

仮想マシンではレスポンスや使い勝手に違和感を感じることもあるので、コスト的には少し検討が必要ですが、実機でk8sクラスターを構築してみたい人にとっては良い方法だと思います。

ただストレージを1つしか載せられなかったり、手間がかかるMiniPCもあるので注意が必要です。

現在はv2.27.0 (k8s v1.31.4)が稼動しています。

# 参考資料

* https://github.com/kubernetes-sigs/kubespray/blob/master/docs/upgrades.md

# 作業の前に (k8sクラスター全体の再起動手順)

:::note warn
K8sクラスターは長期間無停止で運用される傾向が強いと思いますが、アップグレード前に全ノードを順番に再起動してから作業することをお勧めします。
:::

ノードを再起動する際には1台ずつdrainしてから再起動し、必ず再起動後にuncordonしてください。

```bash:control-planeノードで事前にdorainする
$ sudo kubectl cordon node4
$ sudo kubectl drain node4 --force --ignore-daemonsets
## ↑で失敗した場合はメッセージを確認して"--delete-emptydir-data"オプション付きで実行する
$ sudo kubectl drain node4 --force --ignore-daemonsets --delete-emptydir-data
```

"--delete-emptydir-data"を省略することで、このオプションの影響を受けるPodが表示されます。私の環境ではprometheus-operatorに関連するものだけだったのでこのように対処しています。

```bash:drain完了後に対象ノードにリモートログインし再起動を実施する
$ ssh node4
$ sudo shutdown -r now
```

サーバーが再起動するまで時間があるので、``shutdown``コマンドを実行してサーバーからkick offされてから``ping``コマンドなどで状況を確認します。

```bash:再起動が終るまでの間、状況をモニターする
$ ping node4

PING node4 (192.168.1.14) 56(84) bytes of data.
64 bytes from 192.168.1.14: icmp_seq=1 ttl=128 time=4.68 ms
...
From 192.168.1.14 icmp_seq=31 Destination Host Unreachable
From 192.168.1.14 icmp_seq=31 Destination Host Unreachable
...
64 bytes from 192.168.1.14: icmp_seq=56 ttl=128 time=741 ms
...
...
```

再起動が完了してkubeletが復旧していることを確認してから

```bash:再起動したことを確認してからcontrol-planeノードでuncordonを実行する
$ sudo kubectl get node
NAME    STATUS                     ROLES           AGE      VERSION
...
node4   Ready,SchedulingDisabled   control-plane   3y256d   v1.29.5
```

STATUSが``NotReady``の場合は再起動が完了していてもkubeletがまだ起動していないので、``Ready``になるまで待ちます。

```bash:kubeletが稼動している状態でクラスターに戻す
$ sudo kubectl uncordon node4
```

もちろん一連の作業は対話的でなくても良いのでansibleのcommandモジュールで実施することも可能です。

## RabbitMQ Operator配下のPodがevictできない

たまたまPodが同じノードに配置されていると3ノードの内2ノードを削除しようとして再配置されないことが原因でdrain処理に失敗します。

明示的にAnti-Affinityルールを設定しています。

```diff:01.rabbitmq-cluster.yaml
--- a/rabbitmq/01.rabbitmq-cluster.yaml
+++ b/rabbitmq/01.rabbitmq-cluster.yaml
@@ -13,3 +13,13 @@ spec:
     service:
       spec:
         loadBalancerIP: 192.168.1.239
+  affinity:
+    podAntiAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        - labelSelector:
+            matchExpressions:
+              - key: "app.kubernetes.io/name"
+                operator: In
+                values:
+                  - rabbitmq
+          topologyKey: "kubernetes.io/hostname"
```

## Postgres Operator配下のPodがevictできない

Postgresに限りませんがPod Duration Budget(pdb)の設定によっては再起動に失敗する場合があります。

```bash:pdbの設定を確認する
$ sudo kubectl get --all-namespaces pdb
```

問題が発生した場合にはとりあえず、``MinAvaialable``を0にして対応しています。

# 作業の概要

## v2.27.0以降の手順

VIPを利用している場合に、問題が発生する可能性があります。

作業前であれば``kube_conrol_plane``に設定したノードの``/etc/kubernetes/kubelet.conf``を編集し、LoadBalancerのIPではなく``server: https://127.0.0.1:6443``のようにloopbackを通じてapi-serverに接続するようにしてください。

原因や遭遇した場合の対応については[別のセクション](#v2270を適用したらcreatecontainerconfigerrorでpodが動作しない)にまとめています。

```bash:
$ cd kubespray
## コミットしていないファイルがあるとブランチの作成ができないため、untrackedやchangedなファイルがあればcommitしておく
$ git status
$ git commit -a -m 'prepare to upgrade v2.xx.x'
## まずkubesprayの最新のリポジトリに更新
$ git checkout master
$ git pull
## 次のバージョンタグを確認する
$ git tag
## v2.26.0の次のv2.27.0からブランチを作成する
$ git checkout refs/tags/v2.27.0 -b t_v2.27.0

## 新しいvenv環境を作成し、環境変数を読み込む
$ rm -rf venv
$ /usr/bin/python3 -m venv venv/k8s
$ . venv/k8s/bin/activate
$ pip install -r requirements.txt

## inventory/mycluster を作成する。(もし inventory/mycluster があれば削除する)
$ rm -rf inventory/mycluster
$ cp -rfp inventory/sample inventory/mycluster
## inventory/mycluster/inventory.iniファイルを編集するために古いhosts.yamlファイルを参照する
$ touch inventory/mycluster/hosts.yaml
$ git diff t_v2.26.0 inventory/mycluster/hosts.yaml
## inventory/mycluster/inventory.ini ファイルをv2.27.0では手動で構成する (詳細は後述)
$ vi inventory/mycluster/inventory.ini
$ rm inventory/mycluster/hosts.yaml
## .gitignore に !inventory/mycluster を追加する
$ vi .gitignore
$ git add .gitignore inventory/mycluster
$ git commit -m 'Added the inventory/mycluster config directory.'
## 一つ前のバージョン(v2.26.0)との差分を確認する
$ git diff t_v2.26.0 inventory/mycluster
## 変更が必要なファイルは group_vars/k8s-cluster/{addons.yml,k8s-cluster.yml,k8s-net-calico.yml} のみです
## container_manager: が変更になるなどの大規模な変更がないか、念のため全体を概観してください
## 変更する際には各ファイルの差分を確認しながら作業を進めていきます
$ cd inventory/mycluster/group_vars/k8s_cluster
$ git diff t_v2.26.0 addons.yml
$ vi addons.yml
$ git diff t_v2.26.0 k8s-cluster.yml
$ vi k8s-cluster.yml
$ git diff t_v2.26.0 k8s-net-calico.yml
$ vi k8s-net-calico.yml
## 変更が終ったら差分をコミットし、トップディレクトリに移動します
$ git add .
$ git commit -m 'Modified addons.yml, k8s-cluster.yml, and k8s-net-calico.yml files.'
$ cd ../../../../
## 変更方法をupgrades.mdから確認する。そのままでは古いコマンドなので適宜変更すること
$ less docs/operations/upgrades.md
## ansible.cfgを見直し、remote_userなどを環境に合わせて、適宜変更する
$ git diff t_v2.26.0 ansible.cfg
## 変更したinventory.iniが前の版から変化していないことを確認する (v2.28.0以降は一つ前と比較する)
$ git diff t_v2.27.0 inventory/mycluster/inventory.ini
## 変更したansible.cfgが正常に動くか動作を確認し、同時に"kube_node"などに対応するノードの名前と数が正しいか確認する
$ ansible kube_control_plane -i inventory/mycluster/inventory.ini -m command -a 'uname -n'
$ ansible etcd -i inventory/mycluster/inventory.ini -m command -a 'uname -n'
$ ansible kube_node -i inventory/mycluster/inventory.ini -m command -a 'uname -n'
## 正常に動作したら残りのファイルをコミットする (変更点があればinventory.iniも)
$ git add ansible.cfg inventory/mycluster/inventory.ini
$ git commit -m 'Updated the configuration for Ansible.'
## kube_version を確認する
$ grep kube_version inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml 
## 確認したkube_versionを指定して、upgrade-cluster.ymlを実行する
$ ansible-playbook upgrade-cluster.yml -b -i inventory/mycluster/inventory.ini -e kube_version=v1.31.4
```

### v2.27.0以降のinventory/mycluster/inventory.iniについて

inventory.iniファイルは古いkubesprayから含まれていて、変化していませんが、廃止された``contrib/inventory_builder/inventory.py``を使っていた場合には同じ内容になるよう構成に注意が必要です。

以下のデフォルトのサンプルでは``kube_control_plane``にはnode1からnode3まで含まれていますが、inventory.pyは4ノードで構成する場合にはnode1とnode2だけがcontrol_planeとして指定されていました。

また明示的に``etcd_member_name``も設定はしていませんでした。

```ansible:inventory/mycluster/inventory.iniの構成例
# See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html
# for tips on building your # inventory

# Configure 'ip' variable to bind kubernetes services on a different ip than the default iface
# We should set etcd_member_name for etcd cluster. The node that are not etcd members do not need to set the value,
# or can set the empty string value.
[kube_control_plane]
# node1 ansible_host=95.54.0.12  # ip=10.3.0.1 etcd_member_name=etcd1
# node2 ansible_host=95.54.0.13  # ip=10.3.0.2 etcd_member_name=etcd2
# node3 ansible_host=95.54.0.14  # ip=10.3.0.3 etcd_member_name=etcd3

[etcd:children]
kube_control_plane

[kube_node]
# node4 ansible_host=95.54.0.15  # ip=10.3.0.4
# node5 ansible_host=95.54.0.16  # ip=10.3.0.5
# node6 ansible_host=95.54.0.17  # ip=10.3.0.6
```

Upgradeする場合には、それまで自分が使用していた構成内容と違いがないよう慎重に設定する必要があります。

私が4ノード構成でVMWare上で試した場合には次のようなinventory.iniファイルを利用しました。

```ansible:実際に利用したinventory.iniファイル
[kube_control_plane]
node1 ip=10.1.1.11 ansible_host=10.1.1.11 ansible_python_interpreter=/usr/bin/python3 etcd_member_name=etcd1
node2 ip=10.1.1.12 ansible_host=10.1.1.12 ansible_python_interpreter=/usr/bin/python3 etcd_member_name=etcd2

[etcd:children]
kube_control_plane

[etcd]
node3 ip=10.1.1.13 ansible_host=10.1.1.13 ansible_python_interpreter=/usr/bin/python3 etcd_member_name=etcd3

[kube_node:children]
etcd

[kube_node]
node4 ip=10.1.1.14 ansible_host=10.1.1.14 ansible_python_interpreter=/usr/bin/python3
```

これはv2.26.0でinventory.pyが構成していたhosts.yamlと同様の内容となっています。

```yaml:v2.26.0のinventory.pyが出力したhosts.yamlファイル
all:
  hosts:
    node1:
      ansible_host: 10.1.1.11
      ip: 10.1.1.11
      access_ip: 10.1.1.11
    node2:
      ansible_host: 10.1.1.12
      ip: 10.1.1.12
      access_ip: 10.1.1.12
    node3:
      ansible_host: 10.1.1.13
      ip: 10.1.1.13
      access_ip: 10.1.1.13
    node4:
      ansible_host: 10.1.1.14
      ip: 10.1.1.14
      access_ip: 10.1.1.14
  children:
    kube_control_plane:
      hosts:
        node1:
        node2:
    kube_node:
      hosts:
        node1:
        node2:
        node3:
        node4:
    etcd:
      hosts:
        node1:
        node2:
        node3:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
    calico_rr:
      hosts: {}
```

k8s_clusterやcalico_rrは自動的に構成されるようになったので、v2.27.0以降では明示的に利用していない限りは指定する必要がありません。

### etcd_member_listの値について

etcdが稼動しているノード上で次のようなコマンドを実行すると、etcdに設定されたメンバー名が確認できます。

```bash:TLSの設定
$ sudo etcdctl --endpoints https://localhost:2379 --cacert=/etc/ssl/etcd/ssl/ca.pem --cert=/etc/ssl/etcd/ssl/member-$(uname -n).pem --key=/etc/ssl/etcd/ssl/member-$(uname -n)-key.pem member list

## 出力例
3605649efbc160dc, started, etcd1, https://192.168.100.51:2380, https://192.168.100.51:2379, false
e5048e0162ba9845, started, etcd2, https://192.168.100.52:2380, https://192.168.100.52:2379, false
6eedcd9c0acb2497, started, etcd3, https://192.168.100.53:2380, https://192.168.100.53:2379, false
```

## v2.26.0まで使用していたバージョン

kubesprayのマニュアルでは、tagを一つづつ進めていく方法が説明されています。

差分を確認すると、CentOSへの対応だけだったり、マイナーなansibleの記述の修正が行なわれていたりするので、場合によっては、ある程度のtagをスキップした方が効率が良い場合もあるようですが、安全のためにはマイナーバージョンまで含めてtagを一つずつ更新することをお勧めします。

ポイントは、inventory/mycluster/ディレクトリを毎回sampleディレクトリからコピーした上で、一つ前のバージョンと比較しながら毎回編集していく点です。面倒かもしれませんが、addons.ymlやk8s-cluster.ymlファイルの内容は毎回変更されるため、これらを使い回すことはできません。

> release-2.16 (v2.16.0) から inventory/ディレクトリ内の、k8s-cluster/ ディレクトリの名前が、k8s_cluster/ (ハイフンがアンダースコア) へ変更になりました。なお、hosts.yamlファイルの変数などもアンダースコアに変更されています。

```bash:
$ cd kubespray
## コミットしていないファイルがあるとブランチの作成ができないため、untrackedやchangedなファイルがあればcommitしておく
$ git status
$ git commit -a -m 'prepare to upgrade v2.xx.x'
## まずkubesprayの最新のリポジトリに更新
$ git checkout master
$ git pull
## 次のバージョンタグを確認する
$ git tag
## v2.24.0の次のv2.24.1からブランチを作成する
$ git checkout refs/tags/v2.24.1 -b t_v2.24.1

## 新しいvenv環境を作成し、環境変数を読み込む
$ rm -rf venv
$ /usr/bin/python3 -m venv venv/k8s
$ . venv/k8s/bin/activate
$ pip install -r requirements.txt
$ pip install ruamel.yaml

## inventory/mycluster を作成する。(もし inventory/mycluster があれば削除する)
$ rm -rf inventory/mycluster
$ cp -rfp inventory/sample inventory/mycluster
## inventory/mycluster/hosts.yaml ファイルは、バージョンアップで内容が更新される可能性があるため、毎回手動で生成します
$ declare -a IPS=(10.10.1.3 10.10.1.4 10.10.1.5)
$ CONFIG_FILE=inventory/mycluster/hosts.yaml python3 contrib/inventory_builder/inventory.py ${IPS[@]}
## .gitignore に !inventory/mycluster を追加する
$ vi .gitignore
$ git add .gitignore inventory/mycluster
$ git commit -m 'Added the inventory/mycluster config directory.'
## 一つ前のバージョン(v2.24.0)との差分を確認する
$ git diff t_v2.24.0 inventory/mycluster
## 変更が必要なファイルは group_vars/k8s-cluster/{addons.yml,k8s-cluster.yml} のみです
## container_manager: が変更になるなどの大規模な変更がないか、念のため全体を概観してください
## 変更する際には各ファイルの差分を確認しながら作業を進めていきます
$ cd inventory/mycluster/group_vars/k8s_cluster
$ git diff t_v2.24.0 addons.yml
$ vi addons.yml
$ git diff t_v2.24.0 k8s-cluster.yml
$ vi k8s-cluster.yml
$ git diff t_v2.24.0 k8s-net-calico.yml
$ vi k8s-net-calico.yml
## 変更が終ったら差分をコミットし、トップディレクトリに移動します
$ git add .
$ git commit -m 'Modified addons.yml, k8s-cluster.yml, and k8s-net-calico.yml files.'
$ cd ../../../../
## 変更方法をupgrades.mdから確認する。そのままでは古いコマンドなので適宜変更すること
$ less docs/operations/upgrades.md
## ansible.cfgを見直し、remote_userなどを環境に合わせて、適宜変更する
$ git diff t_v2.24.0 ansible.cfg
## hosts.yamlを確認する
$ vi inventory/mycluster/hosts.yaml
## 変更したhosts.yamlが前の版からどう変化したか確認する
$ git diff t_v2.24.0 inventory/mycluster/hosts.yaml
## 変更したansible.cfgが正常に動くか動作を確認する
$ ansible all -i inventory/mycluster/hosts.yaml -m command -a 'uname -a'
## 正常に動作したら残りのファイルをコミットする
$ git add ansible.cfg inventory/mycluster/hosts.yaml
$ git commit -m 'Updated the configuration for Ansible.'
## kube_version を確認する
$ grep kube_version inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml 
## 確認したkube_versionを指定して、upgrade-cluster.ymlを実行する
$ ansible-playbook upgrade-cluster.yml -b -i inventory/mycluster/hosts.yaml -e kube_version=v1.28.6
```

:::note warn
v2.24.2を適用するタイミングでcontainerdが正常に稼動しなくなったので、ansible-playbookの引き数から``-e containerd_base_runtime_spec_rlimit_nofile=1048576``の指定は削除しました。
:::

v2.16.0からinventory/mycluster/group_vars/直下にあるディレクトリが、k8s-clusterからk8s_clusterに変更になっている点に注意してください。

# 遭遇したエラーなど

## 【全バージョン対象】upgrade-cluster.ymlの実行時に遭遇するエラー

:::note warn
この手順はあらかじめansible-playbookを実行していることが前提です。v2.27.0から``--limit``を使用する時にはfactsがキャッシュされていることが前提です。いきなり``--limit``オプションを指定してansible-playbookを実行する時には、必ず ``playbook/facts.yml`` を実行してから実施してください。[Upgrading Kubernetes in Kubespray](https://github.com/kubernetes-sigs/kubespray/blob/master/docs/operations/upgrades.md) 

ドキュメント中にはカレントディレクトリの./facts.yamlを実行するような記述になっていますが、実際には存在せず、実体の``playbook/facts.yml``だけが存在しています。
:::

ノードの数が十分でないのか、パフォーマンスが悪いためか、ansible-playbookが終了した時に次のようなステータスになっている場合があります。

```bash:upgrade-cluster.ymlの実行に失敗した状態
$ kubectl get node
NAME       STATUS                     ROLES    AGE    VERSION
node1   Ready                      master   171d   v1.15.11
node2   Ready,SchedulingDisabled   master   171d   v1.15.3
node3   Ready                      <none>   171d   v1.15.3
node4   Ready                      <none>   171d   v1.15.3
```

もしnode1もSchedulingDisabledになっていればuncordonし、古いバージョンがcordonされている状態から、ansible-playbookを再度実行します。数回繰り返したところ全体がv1.15.11になりました。

エラーになっている理由は、cordonした後に想定している時間内にSchedulingDisabledとならなかった点にありました。
積極的にcordon/uncordonを利用して1台づつ処理する場合は次のような流れになります。

```bash:手動で1台づつupgrade-cluster.ymlを実行する例
## kubectlコマンドが実行できるホストに移動
$ kubectl cordon node2

## kube_version を確認する
$ grep kube_version inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
v1.27.7

## ansibleを実行するホストに移動し、--limitを利用してcordonしたノードのみにansible roleを適用する
$ ansible-playbook upgrade-cluster.yml -b -i inventory/mycluster/hosts.yaml -e kube_version=v1.27.7 --limit node2

## 再びkubectlコマンドを実行するホストに移動
$ kubectl uncordon node2
```

:::note warn
Multus関連のエラーが出て停止する場合には、**--skip-tags=multus**を--limit node2の後ろに追加してください。
:::

私の環境では以下のように設定よりも長い時間を指定してみましたが、状況は改善しませんでした。

```diff:timeout値の修正
--- a/roles/upgrade/pre-upgrade/defaults/main.yml
+++ b/roles/upgrade/pre-upgrade/defaults/main.yml
@@ -1,5 +1,5 @@
 ---
-drain_grace_period: 300
-drain_timeout: 360s
+drain_grace_period: 500
+drain_timeout: 560s
 drain_pod_selector: ""
 drain_nodes: true
```

これはissuesに、 [https://github.com/kubernetes-sigs/kubespray/issues/1453](https://github.com/kubernetes-sigs/kubespray/issues/1453) が登録されていて、timeout値を変更しても意味がないとは指摘されています。とはいえ、ここで書かれている、ignore_errors: yesの設定は、危険だと思います。

10G SFP+でLANを組んでいるXeonクラスターではこの問題は関係ないかなと考えていましたが、あまり関係ないようです。サーバーの負荷なども関係なしに発生するようですので、適宜 --limit オプションを使いながらアップグレードしています。

2023年9月にkubespray v2.19.0を適用したところ、どうしてもcordonしてもアップグレードできないノードが発生しました。顛末については後述していますが、最終的にはクラスター全体を再起動しなければ解決しない状況になること経験しています。

### cordonできないノードが発生してしまった時の対応

> このセクションは前のcordon関連の記事とマージする予定です

update-cluster.ymlを実行している時に、どうしても1つのノードだけ**Cordon node**タスクがタイムアウトして実行できないという状態になりました。

通常はCordonにタイムアウトするだけであれば、手動でcordonすれば良いというのは経験則から分かっていて、このページの先頭でもそのように記載しています。

今回はやや特殊な状況でsyslogには次のように出力されていました。

```text:
Sep  1 06:06:47 node3 kubelet[1963]: E0901 06:06:47.603666    1963 \
    kubelet_volumes.go:245] "There were many similar errors. \
    Turn up verbosity to see them." err="orphaned pod \"344699f9-2278-48ed-b832-79fcd2a88a53\" found, \
    but error not a directory occurred when trying to remove the volumes dir" numErrs=7
```

平行していくつかのPodがTerminate状態のまま停止させることができなくなっていて、describeで出力させると、次のようなログが残っていました。結果的にこれらのメッセージには対応していません。

```text:
Normal   Killing                 3m56s (x2 over 8m58s)  kubelet                  Stopping container rabbitmq
Warning  FailedKillPod           3m56s                  kubelet                  error killing pod: failed to "KillContainer" for "rabbitmq" with KillContainerError: "rpc error: code = Unknown desc = operation timeout: context deadline exceeded"
Warning  FailedPreStopHook       3m56s                  kubelet                  Exec lifecycle hook ([/bin/bash -c if [ ! -z "$(cat /etc/pod-info/skipPreStopChecks)" ]; then exit 0; fi; rabbitmq-upgrade await_online_quorum_plus_one -t 604800; rabbitmq-upgrade await_online_synchronized_mirror -t 604800; rabbitmq-upgrade drain -t 604800]) for Container "rabbitmq" in Pod "rabbitmq-server-1_rabbitmq-system(dcbeb1f9-e21e-4914-8f91-42cbf30f250f)" failed - error: command '/bin/bash -c if [ ! -z "$(cat /etc/pod-info/skipPreStopChecks)" ]; then exit 0; fi; rabbitmq-upgrade await_online_quorum_plus_one -t 604800; rabbitmq-upgrade await_online_synchronized_mirror -t 604800; rabbitmq-upgrade drain -t 604800' exited with 126: , message: "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: process_linux.go:130: executing setns process caused: exit status 1: unknown\r\n"
```

不要なディレクトリを削除するなどしましたが解決せずに、時間切れで全ノードを再起動することで対応しました。

他のk8sクラスターでも同様の症状になったため、手動でdrainして出力を確認してみました。

```bash:drainしてみる
$ sudo kubectl drain node4 --force --ignore-daemonsets 

                                                                     
evicting pod rocketchat/myrocketchat-mongodb-secondary-0
evicting pod rocketchat/myrocketchat-mongodb-arbiter-0
evicting pod rook-ceph/rook-ceph-crashcollector-node4-7f78bf6bb6-j84cp error when evicting pods/"myrocketchat-mongodb-secondary-0" -n "rocketchat" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pods/"myrocketchat-mongodb-arbiter-0" -n "rocketchat" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
```

poddisruptionbudgets.policy を確認します。

```bash:mongodb-arbiterのpoddisruptionbudgets.policyを確認する
$ sudo kubectl -n rocketchat get poddisruptionbudgets.policy myrocketchat-mongodb-arbiter
NAME                           MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE            
myrocketchat-mongodb-arbiter   N/A             N/A               1                     511d
```

editコマンドで``maxUnavailable: 1``を加えて、とりあえずevictできるようにしました。

ただ結果としてサービスが継続できなくなってしまったので、rocketchat自体は再設定が必要になり、あまり満足する結果にはなりませんでしたが、drainできない理由になるような設定だった点は少し問題だっただろうと思います。


## 【全バージョン対象】証明書の期限切れでKubernetesクラスターが停止した際の対応

最初に遭遇した時にはびっくりしましたが、次のようなメッセージがsyslogファイルに記録されていました。

```text:/var/log/syslogからの抜粋
May  6 02:06:55 localhost kubelet[3217]: I0506 02:06:55.470276    3217 server.go:773] Client rotation is on, will bootstrap in background
May  6 02:06:55 localhost kubelet[3217]: E0506 02:06:55.471971    3217 bootstrap.go:265] part of the existing bootstrap client certificate is expired: 2021-05-05 13:04:01 +0000 UTC
May  6 02:06:55 localhost kubelet[3217]: F0506 02:06:55.472002    3217 server.go:271] failed to run Kubelet: unable to load bootstrap kubeconfig: stat /etc/kubernetes/bootstrap-kubelet.conf: no such file or directory
May  6 02:06:55 localhost systemd[1]: kubelet.service: Main process exited, code=exited, status=255/n/a
May  6 02:06:55 localhost systemd[1]: kubelet.service: Failed with result 'exit-code'.
```

certificatesファイル群を再生成してあげれば良い事はメッセージから分かりますが、kubesprayを使用している時にどうするのが最善か、すぐには判断できませんでした。

githubのIssuesには次のような投稿があります。

* [After the certificate expires how use kubespray to renew certificate #5464](https://github.com/kubernetes-sigs/kubespray/issues/5464)

念のためMasterノードで証明書の期限を確認してみます。

```bash
$ sudo openssl x509 -text -in /etc/kubernetes/ssl/apiserver.crt  | head -15
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 2538597669611175608 (0x233ae92164cefeb8)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = kubernetes
        Validity
            Not Before: May 22 13:18:23 2019 GMT
            Not After : May  5 13:03:23 2021 GMT
        Subject: CN = kube-apiserver
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c2:0b:4b:d1:9c:a8:33:dd:2c:d6:9a:85:0f:f5:
```

ちょうど前日で期限が切れていることが分かります。
Masterノードの証明書だけ作り直せば、Workerノードは起動時に証明書を再生成するとのことなので(実際には10年有効なca.crtを持っているだけなので配置し直す必要がない)、2つの全Masterノードで次の作業を行ないました。

```bash:Issuesの投稿に従って各Masterノードでそれぞれ作業を行なう
## confファイルの確認 (以下の4ファイルで、これを置き換える)
$ ls /etc/kubernetes/*.conf
/etc/kubernetes/admin.conf               /etc/kubernetes/kubelet.conf
/etc/kubernetes/controller-manager.conf  /etc/kubernetes/scheduler.conf

## ファイルのバックアップ
$ sudo cp -pR /etc/kubernetes/ssl /etc/kubernetes/ssl."$(date +%Y%m%d.%H%M%S)"
$ sudo cp -p /etc/kubernetes/admin.conf /etc/kubernetes/admin.conf."$(date +%Y%m%d.%H%M%S)"
$ sudo cp -p /etc/kubernetes/controller-manager.conf /etc/kubernetes/controller-manager.conf."$(date +%Y%m%d.%H%M%S)"
$ sudo cp -p /etc/kubernetes/kubelet.conf /etc/kubernetes/kubelet.conf."$(date +%Y%m%d.%H%M%S)"
$ sudo cp -p /etc/kubernetes/scheduler.conf /etc/kubernetes/scheduler.conf."$(date +%Y%m%d.%H%M%S)"

## 証明書再生成などの一連の作業 (v1.22以前)
$ sudo kubeadm alpha certs renew apiserver-kubelet-client
$ sudo kubeadm alpha certs renew apiserver
$ sudo kubeadm alpha certs renew front-proxy-client
$ sudo kubeadm alpha kubeconfig user --client-name system:kube-controller-manager | sudo tee /etc/kubernetes/controller-manager.conf
$ sudo kubeadm alpha kubeconfig user --client-name system:kube-scheduler | sudo tee /etc/kubernetes/scheduler.conf
$ sudo kubeadm alpha kubeconfig user --client-name system:node:$(uname -n) --org system:nodes | sudo tee /etc/kubernetes/kubelet.conf

## 証明書再生成などの一連の作業 (v1.22以降)
$ sudo kubeadm certs renew apiserver-kubelet-client --config /etc/kubernetes/kubeadm-images.yaml
$ sudo kubeadm certs renew apiserver --config /etc/kubernetes/kubeadm-images.yaml
$ sudo kubeadm certs renew front-proxy-client --config /etc/kubernetes/kubeadm-images.yaml
$ sudo kubeadm kubeconfig user --client-name system:kube-controller-manager --config /etc/kubernetes/kubeadm-images.yaml | sudo tee /etc/kubernetes/controller-manager.conf
$ sudo kubeadm kubeconfig user --client-name system:kube-scheduler --config /etc/kubernetes/kubeadm-images.yaml | sudo tee /etc/kubernetes/scheduler.conf
$ sudo kubeadm kubeconfig user --client-name system:node:$(uname -n) --org system:nodes --config /etc/kubernetes/kubeadm-images.yaml | sudo tee /etc/kubernetes/kubelet.conf

## /root/.kube/config の更新 (v1.22以前)
$ sudo kubeadm alpha kubeconfig user --client-name kubernetes-admin --org system:masters | sudo tee /etc/kubernetes/admin.conf
$ sudo cp -ip /root/.kube/config /root/.kube/config."$(date +%Y%m%d.%H%M%S)"
$ sudo cp -p /etc/kubernetes/admin.conf /root/.kube/config

## /root/.kube/config の更新 (v1.22以降)
$ sudo kubeadm kubeconfig user --client-name kubernetes-admin --org system:masters --config /etc/kubernetes/kubeadm-images.yaml | sudo tee /etc/kubernetes/admin.conf
$ sudo cp -ip /root/.kube/config /root/.kube/config."$(date +%Y%m%d.%H%M%S)"
$ sudo cp -p /etc/kubernetes/admin.conf /root/.kube/config
```

> 2022/05/16 同様の現象が稼働中のProduction環境(v1.19.9)で発生し、kubectlコマンドが証明書の期限切れが原因で実行できなくなりました。既に起動していたpodには影響はありませんでしたが、certificateの期限が切れていることを確認してから、この手順を上から順番に実行し、最後まで終わると直ちにkubectlコマンドが実行可能となりました。しかし、control-planeは正常に稼動していない状況でした。kube-scheduler, kube-controller-manager, kube-apiserverの再起動で復旧するような気もしますが、全control-planeノードを1台づつ時間をかけて順番に再起動するのがお勧めです。

"$(uname -n)"コマンドの実行結果としてホスト名を指定していますが、作業前に(期限が切れた証明書情報が書き込まれた)kubelet.confファイルを確認して、ホスト名と"system:node:..."の"..."が一致するか確認しておきます。

systemctlからkubelet.serviceが自動起動に成功するはずなので、ここまでの作業が終わると自然にクラスターが復活しているはずです。

kubespray v2.15.1からは、k8s-cluster.ymlファイルに **+auto_renew_certificates:** と **auto_renew_certificates_systemd_calendar:** の設定が追加されています。

## v2.11.2で遭遇したRook/Cephに起因する不具合

> この内容はFlexVolumeを利用していることによるものです。現在のRook/CephはCSIとRaw drive(bluestore)の利用が前提となっているので、この内容は参考にしないでください。

以前もRook/Cephを導入した際に遭遇した不具合ですが、k8sをv1.15.11に変更した後に、PodからPVCがmountできない現象が発生しました。

* https://qiita.com/YasuhiroABE/items/57d8d58c1d6fb31feb14

"kubelet.kubeadm.env.j2"からファイル名が変更されていますが、問題は同じです。
Jinjaテンプレートを変更してから、再度 ansible-playbook コマンドを実行しています。

```diff:KUBELET_VOLUME_PLUGINが指定されていない事への対応例
diff --git a/roles/kubernetes/node/templates/kubelet.env.v1beta1.j2 b/roles/kubernetes/node/templates/kubelet.env.v1beta1.j2
index ddf97819..9cffdc2c 100644
--- a/roles/kubernetes/node/templates/kubelet.env.v1beta1.j2
+++ b/roles/kubernetes/node/templates/kubelet.env.v1beta1.j2
@@ -55,6 +55,7 @@ KUBELET_NETWORK_PLUGIN="--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni
 {% elif kube_network_plugin is defined and kube_network_plugin == "cloud" %}
 KUBELET_NETWORK_PLUGIN="--hairpin-mode=promiscuous-bridge --network-plugin=kubenet"
 {% endif %}
+KUBELET_VOLUME_PLUGIN="--volume-plugin-dir={{ kubelet_flexvolumes_plugins_dir }}"
 {% if cloud_provider is defined and cloud_provider in ["openstack", "azure", "vsphere", "aws"] %}
 KUBELET_CLOUDPROVIDER="--cloud-provider={{ cloud_provider }} --cloud-config={{ kube_config_dir }}/cloud_config"
 {% elif cloud_provider is defined and cloud_provider in ["external"] %}
```

kubelet_flexvolumes_plugins_dirを設定しているにも関わらず、kubeletにvolume-plugin-dirの引数が設定されない問題はkubesprayのv2.11.xに固有の問題で、v2.12.xでは改善されているため、これらの問題は発生せず、修正は不要です。

## v2.15.1でcontainerd.ioパッケージが更新されてしまう問題について

kubesprayのv2.15.0からv2.15.1にアップグレードしようとして、次のようなエラーが発生しました。

```bash:ansible-playbookの実行結果
TASK [container-engine/docker : ensure docker packages are installed]  

fatal: [node01]: FAILED! => {"attempts": 4, "cache_update_time": 1620274204, "cache_updated": true, "changed": false, "msg": "'/usr/bin/apt-get -y -o \"Dpkg::Options::=--force-confdef\" -o \"Dpkg::Options::=--force-confold\"      install 'containerd.io=1.3.9-1' 'docker-ce-cli=5:19.03.14~3-0~ubuntu-focal' 'docker-ce=5:19.03.14~3-0~ubuntu-focal'' failed: E: Packages were downgraded and -y was used without --allow-downgrades.\n", "rc": 100, "stderr": "E: Packages were downgraded and -y was used without --allow-downgrades.\n", "stderr_lines": ["E: Packages were downgraded and -y was used without --allow-downgrades."], "stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\nThe following packages were automatically installed and are no longer required:\n  docker-ce-rootless-extras docker-scan-plugin slirp4netns\nUse 'sudo apt autoremove' to remove them.\nThe following packages will be DOWNGRADED:\n  containerd.io docker-ce docker-ce-cli\n0 upgraded, 0 newly installed, 3 downgraded, 0 to remove and 0 not upgraded.\n", "stdout_lines": ["Reading package lists...", "Building dependency tree...", "Reading state information...", "The following packages were automatically installed and are no longer required:", "  docker-ce-rootless-extras docker-scan-plugin slirp4netns", "Use 'sudo apt autoremove' to remove them.", "The following packages will be DOWNGRADED:", "  containerd.io docker-ce docker-ce-cli", "0 upgraded, 0 newly installed, 3 downgraded, 0 to remove and 0 not upgraded."]}
```

以下のIssuesに関連した記載があります。

* [package containerd.io not set on hold after the installation #7276](https://github.com/kubernetes-sigs/kubespray/issues/7276)

元々はdocker-ceだけ自動更新されないようにHoldしていましたが、containerd.ioパッケージを導入するなど対象のパッケージが増えていく過程で、各パッケージのバージョンをHoldする必要があったものの、docker-ceだけHoldにしていた事などが原因で問題となっていました。

とりあえずforceオプションを有効にして、アップグレードが機能するようにします。

```diff:git-diffコマンドの出力
$ git diff
diff --git a/roles/container-engine/docker/tasks/main.yml b/roles/container-engine/docker/tasks/main.yml
index 13903e48..357ee2db 100644
--- a/roles/container-engine/docker/tasks/main.yml
+++ b/roles/container-engine/docker/tasks/main.yml
@@ -106,6 +106,7 @@
   module_defaults:
     apt:
       update_cache: true
+      force: true
     dnf:
       enablerepo: "{{ docker_package_info.enablerepo | default(omit) }}"
     yum:
```

この状態で無事にkubesprayのバージョンがv1.19.7からv1.19.9へ更新できました。

```bash:versionの状況
$ kubectl get node
NAME     STATUS   ROLES    AGE    VERSION
node01   Ready    master   103d   v1.19.9
node02   Ready    master   103d   v1.19.9
node03   Ready    <none>   103d   v1.19.9
node04   Ready    <none>   13d    v1.19.9
node05   Ready    <none>   13d    v1.19.9
```

別のクラスターでは、containerd.io パッケージを古いバージョンに入れ替えてから作業を進めましたが、どちらの方法でも特に問題は発生していません。





## v2.15.1 → v2.16.0 にアップグレードした時のメモ

以下のような変更点がありました。

### inventory/mycluster/hosts.yaml の形式の変化

masterからcontrol_planeへの変更のに、他ハイフン('-')がアンダースコア('_'）に切り替わっています。

```diff:hosts.yamlの変更箇所の抜粋
-    kube-master:
+    kube_control_plane:
       hosts:
         node1:
         node2:
-    kube-node:
+    kube_node:
```

この変更への対応は必要ありません。そのまま安全にアップグレードができます。

### 変数名のハイフン('-')がアンダースコア('_')に変更になっている

この影響で問題になるのは``git diff``によるaddons.ymlファイルのv2.15.1との差分チェックが面倒になった点です。
とりあえずv2.15.1のinventory/myclusterディレクトリ全体をコピーした上でdiffコマンドを利用しています。

## ansibleのバージョンを新しいことによる問題への対応 (venvへの移行)

ansibleのバージョンは2.9系列から2.11.0未満でないとエラーがでます。
別件で最新版のansibleパッケージを使っているので、venvを利用することにしました。

```bash:
$ python3.9 -m venv venv/k8s
$ . venv/k8s/bin/activate
(k8s) $ pip install wheel
(k8s) $ pip install -r requirements.txt
(k8s) $ ansible --version
ansible 2.9.20
  config file = /home/yasu/ansible/kubespray/ansible.cfg
  configured module search path = ['/home/yasu/ansible/kubespray/library']
  ansible python module location = /home/yasu/ansible/kubespray/venv/k8s/lib/python3.9/site-p
ackages/ansible
  executable location = /home/yasu/ansible/kubespray/venv/k8s/bin/ansible
  python version = 3.9.5 (default, Nov 23 2021, 15:27:38) [GCC 9.3.0]
```

この後は普通にansible-playbookコマンドを実行できます。
利用の度に、``. venv/k8s/bin/activate`` を実行して必要な環境変数を読み込む事を忘れないでください。

## 【v2.17.0】Ingress Controllerが古いバージョンのまま更新されない

Ingressコントローラーが更新されていないことに気がつきました。
更新時にサービス停止が発生するため、自動的に更新されないようです。

ただ、あまりにも古いのでメンテナンス・ウィンドウでupgradeすることにしました。

```text:現行バージョンのingress-nginx-controller
        image: k8s.gcr.io/ingress-nginx/controller:v0.43.0
```

さすがに古いので、kubesprayを使って最新にしていきます。
まずChangelogを確認します。

* [https://github.com/kubernetes/ingress-nginx/blob/main/Changelog.md](https://github.com/kubernetes/ingress-nginx/blob/main/Changelog.md)

1.0.0からnetworking.k8s.io/v1betaは使えなくなるといったメッセージが出ていますが、現状で作成されているIngressオブジェクトでは該当しません。

後は、0.40.0から1.0.0までの間に大きな変更はないので、問題なく1.0.0にできそうです。
しかし、実際にテスト系でeditコマンドからansible-playbookを使わずに変更すると次のようなエラーが表示されています。

ここで、1.0.0からIngressClassオブジェクトが必須になったと公式ガイドに書かれていたのですが、見落して問題が発生しました。

* [https://kubernetes.github.io/ingress-nginx/#what-is-an-ingressclass-and-why-is-it-important-for-users-of-ingress-nginx-controller-now](https://kubernetes.github.io/ingress-nginx/#what-is-an-ingressclass-and-why-is-it-important-for-users-of-ingress-nginx-controller-now)

サービスについては、他の更新は停止しているのでサービスは問題なく動作しています。

```text:logの出力からの抜粋
E0902 03:47:03.573112       7 reflector.go:138] k8s.io/client-go@v0.21.3/tools/cache/reflector.go:167: Failed to watch *v1.IngressClass: failed to list *v1.IngressClass: ingressclasses.networking.k8s.io is forbidden: User "system:serviceaccount:ingress-nginx:ingress-nginx" cannot list resource "ingressclasses" in API group "networking.k8s.io" at the cluster scope
```

この問題への対応はkubesprayから変更することで必要なCRDを設定してくれそうなので、通常の手法でアップグレードします。

* [https://github.com/kubernetes-sigs/kubespray/issues/7190](https://github.com/kubernetes-sigs/kubespray/issues/7190)

```bash:ansible-playbook経由でのingress-controllerの更新
## tagsにingress-controllerだけ指定するとバックエンドのnginx-proxyが停止したままになり、動かなくなります
$ ansible-playbook -b -i inventory/mycluster/hosts.yaml cluster.yml --tags=ingress-controller,nginx
```

もしnamespace: kube-systemのpod/nginx-proxyが停止したままになっていたら、tags=nginxを再度実行して復旧させてください。

しかし、まだ、他のエラーがでてしまいます。

```text:
I0902 03:57:40.002794       7 store.go:361] "Ignoring ingress because of error while validating ingress class" ingress="ingress-nginx/my-ingress" error="ingress does not contain a valid IngressClass"
```

ingress/my-ingress は古いので、ingressClassNameフィールドを持っていません。
何らかのIngressClassオブジェクトを登録し、ingressClassNameフィールドをIngressオブジェクトに追加します。

```bash:
$ sudo kubectl -n ingress-nginx get ingress my-ingress -o yaml | grep ingressClassName
```
まだIngressClassオブジェクトは存在しないので、デフォルト値と思われる値で作成します。

```yaml:ingressclass-nginx.yaml
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: nginx
spec:
  controller: k8s.io/ingress-nginx
```

ここで指定するcontroller:の値は、ingress-nginx-controllerのコマンドラインに依存すると思ったのですが、YAMLの定義を確認すると、特段の指定はないようにみえました。

```bash:
$ sudo kubectl -n ingress-nginx get daemonset.apps/ingress-nginx-controller -o yaml
...
   spec:                                                
      containers:                       
      - args:               
        - /nginx-ingress-controller
        - --configmap=$(POD_NAMESPACE)/ingress-nginx
        - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
        - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
        - --annotations-prefix=nginx.ingress.kubernetes.io
        env:  
...
```

ingress-nginxのコードを追ってみても、デフォルト値は空だったり、ingress-classは'nginx'がデフォルト値だというところは分かりましたが、controller-classの部分は良く分かりませんでした。

調査はここらまでで終えて、IngressClassオブジェクトを作成します。

```bash:IngressClassの登録
$ sudo kubectl apply -f ingressclass-nginx.yaml
```

次に、ingress/my-ingressに ingressClassNameを指定します。

```bash:
## .spec.ingressClassName: nginx を追加する
$ sudo kubectl -n ingress-nginx edit ingress my-ingress
```
追加されたingressClassNameの値を確認します。

```bash:
$ sudo kubectl -n ingress-nginx get ingress my-ingress -o yaml | grep ingressClassName
...
  ingressClassName: nginx
```

さいごに、既存のingress-controllerを再起動します。

```bash:pod/ingress-controller-*の停止
$ sudo kubectl -n ingress-nginx delete pod -l app.kubernetes.io/name=ingress-nginx
```

最初にingress-nginx-controllerを更新してしまうとサービス断が長く続くことになります。
本番環境では、次の手順でスムーズに移行することができます。ansible-playbookを実行しているタイミングではサービス断が発生するとのことでしたが気がつくことはありませんでした。

1. CRDは登録されているのでIngressClassオブジェクトをあらかじめ登録する
2. Ingressオブジェクトに"ingressClassName: nginx"を追加し、反映する
3. ansible-playbookでtags=nginx,ingress-controllerを指定してcluster.yamlを実行する

これで、v1.0.0に移行することができました。

## etcdctlを使ってバックアップを取得する

v2.17.1からv2.18.0に更新した際の変更点として、デフォルトのコンテナが containerd になった点があります。

合わせてetcdがdockerコンテナからhost上のプロセスにすることがデフォルト設定になっています。

次のような設定変更が必要ですが、タイトルのように etcd_deployment_type: host のまま ansible-playbook を実行してしまいました。

* container_manager: docker (inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml)
* etcd_deployment_type: docker (inventory/mycluster/group_vars/etcd.yml)

この設定を改めてetcdを再度デプロイしようと思うのですが、念のためにsnapshotでバックアップを取得します。[公式ガイド Operating etcd clusters for Kubernetes](https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/)では、次のようなコマンドが紹介されています。

```bash:etcdctlによるsnapshotの取得方法
$ ETCDCTL_API=3 etcdctl --endpoints $ENDPOINT snapshot save snapshotdb

## snapshotdbファイルの検証
$ ETCDCTL_API=3 etcdctl --write-out=table snapshot status snapshotdb
```

実際にENDPOINT環境変数に適切なURLを設定して実行すると次のようなメッセージを表示して、プロンプトが戻ってきません。

```text:etcdctlコマンドが出力するメッセージ
{"level":"info","ts":1670378921.9468956,"caller":"snapshot/v3_snapshot.go:68","msg":"created temporary db file","path":"20221207_snapshotdb.part"}
```

接続先のURLのホストにあるsyslogをみると次のようなメッセージが記録されていました。

```text:syslogファイルから抜粋
Dec  7 04:50:02 node1 etcd[1765628]: {"level":"warn","ts":"2022-12-07T04:50:02.582Z","caller":"embed/config_logging.go:169","msg":"rejected connection","remote-addr":"192.168.2.11:37746","server-name":"","error":"tls: client didn't provide a certificate"}
```

このためcertファイルなどを指定するオプションを追加して実行します。

```bash:実際にsnapshot取得に使用したコマンドライン
$ ETCDCTL_API=3 etcdctl --endpoints $ENDPOINT --cacert=/etc/ssl/etcd/ssl/ca.pem --cert=/etc/ssl/etcd/ssl/member-node1.pem --key=/etc/ssl/etcd/ssl/member-node1-key.pem snapshot save snapshotdb
```

今度は無事に1秒程度でsnapshotが出力されました。

```text:成功した時のログメッセージ
{"level":"info","ts":1670389318.9246037,"caller":"snapshot/v3_snapshot.go:68","msg":"created temporary db file","path":"20221207_snapshotdb.part"}
{"level":"info","ts":1670389318.9329438,"logger":"client","caller":"v3/maintenance.go:211","msg":"opened snapshot stream; downloading"}
{"level":"info","ts":1670389318.9329746,"caller":"snapshot/v3_snapshot.go:76","msg":"fetching snapshot","endpoint":"https://10.1.200.11:2379"}
{"level":"info","ts":1670389319.179962,"logger":"client","caller":"v3/maintenance.go:219","msg":"completed snapshot read; closing"}
{"level":"info","ts":1670389319.3646045,"caller":"snapshot/v3_snapshot.go:91","msg":"fetched snapshot","endpoint":"https://192.168.2.11:2379","size":"42 MB","took":"now"}
{"level":"info","ts":1670389319.3652825,"caller":"snapshot/v3_snapshot.go:100","msg":"saved","path":"20221207_snapshotdb"}
Snapshot saved at 20221207_snapshotdb
```

検証のコマンドラインは読み取り権限があれば、そのまま実行が可能でした。

```bash:etcdctlによるsnapshotファイルの検証と、その結果
$ ETCDCTL_API=3 etcdctl --write-out=table snapshot status 20221207_snapshotdb 
Deprecated: Use `etcdutl snapshot status` instead.

+----------+-----------+------------+------------+
|   HASH   | REVISION  | TOTAL KEYS | TOTAL SIZE |
+----------+-----------+------------+------------+
| 3105bd23 | 218538450 |       3578 |      42 MB |
+----------+-----------+------------+------------+
```

## v2.18.0でcontainer runtimeのデフォルトが変更された問題

これまでと同様に変更を加えてアップグレード作業を進めたところ、次のようなメッセージが表示されエラーとなりました。

```text:エラーメッセージ
TASK [container-engine/containerd : containerd | Remove any package manager controlled containerd package] *********************************************
fatal: [node1]: FAILED! => {"changed": false, "msg": "'apt-get remove 'containerd.io'' failed: E: Error, pkgProblemResolver::Resolve generated breaks
, this may be caused by held packages.\n", "rc": 100, "stderr": "E: Error, pkgProblemResolver::Resolve generated breaks, this may be caused by held pack
ages.\n", "stderr_lines": ["E: Error, pkgProblemResolver::Resolve generated breaks, this may be caused by held packages."], "stdout": "Reading package l
ists...\nBuilding dependency tree...\nReading state information...\nSome packages could not be installed. This may mean that you have\nrequested an impo
ssible situation or if you are using the unstable\ndistribution that some required packages have not yet been created\nor been moved out of Incoming.\nT
he following information may help to resolve the situation:\n\nThe following packages have unmet dependencies:\n docker-ce : Depends: containerd.io (>= 
1.4.1) but it is not going to be installed\n", "stdout_lines": ["Reading package lists...", "Building dependency tree...", "Reading state information...
", "Some packages could not be installed. This may mean that you have", "requested an impossible situation or if you are using the unstable", "distribut
ion that some required packages have not yet been created", "or been moved out of Incoming.", "The following information may help to resolve the situati
on:", "", "The following packages have unmet dependencies:", " docker-ce : Depends: containerd.io (>= 1.4.1) but it is not going to be installed"]}
```

この原因は、v2.18.0から、大きな変更として、デフォルトのcontainer runtimeがdockerからcontainerdに変更されています。

```diff:設定の差分
# Container runtime
 ## docker for docker, crio for cri-o and containerd for containerd.
-container_manager: docker
+## Default: containerd
+container_manager: containerd
```

このため、containered.ioパッケージのhold設定を変更しない事などが原因でエラーになります。
これに関連する議論は、[GitHub kubespray issues#8431](https://github.com/kubernetes-sigs/kubespray/issues/8431) に登録されています。

ポイントはエラーとなったパッケージが更新できないことではなく、次のようにcontainer_managerの変更は推奨されないという点にあります。

```text:issues抜粋
Note that we don't quite support transitioning from one container engine to another. 
If you are upgrading from pre-2.18 to 2.18+ you need to change your container_manager inventory variable to keep compatibility with your old version in this case set it to container_manager: docker.
```

そのため、```container_manager: docker``` に設定してから、再び update-cluster.yml ファイルを反映させています。

dockerのままでまったく問題ありませんが、以前の検証結果からcontainer runtimeによって機能に大きな違いはありませんが、イメージファイルの配置やデフォルトのリポジトリURLリストなどが違うため、runtimeの変更はサービス断や、yamlファイルの編集が必要になるなどの問題を生じる可能性があります。

## 【v2.18.2】container_manager: docker かつ etcd_deployment_type: host 設定した場合の対処

この直前のセクションで説明したように、v2.17.1からv2.18.0に更新する際にdockerをコンテナエンジンとしたまま、etcdをdockerからhostにタイプを変更したままにしていました。

本来はコメントにあるようにdockerにしておくようにコメントがあります。
一方で ansible の設定上は、**container_manager:の値が"docker"でない場合、etcd_deployment_type:の値は"docker"であってはならない**、という、当たり前のチェックしかしてくれません。

[Release Proposal v2.18](https://github.com/kubernetes-sigs/kubespray/issues/8325) から辿っていくと、v2.19ではetcdをhostにデプロイした際には自動的にdockerが導入されることはバグになったようですが、dockerを使っている場合に、etcdをhostにデプロイすると不具合が発生するという記述は見付けられませんでした。

関連するissuesは、いくつかありますが、いずれもdockerではなく、containerdなどを利用しようとして、etcdがうまく導入できないといったものでした。

* [https://github.com/kubernetes-sigs/kubespray/issues/8381](https://github.com/kubernetes-sigs/kubespray/issues/8381)

### etcd_deployment_type: を "host" から "docker" に変更した場合

そのまま update-cluster.yml を実行しても変更は反映されず、etcd は host の service として実行され続けています。

docs/upgrade.md にあるように、個別に cluster.yml を適用していきます。
このドキュメントのコマンドラインは、hosts.ini を使うような古い形式なので適宜変更します。

```bash:Upgrade etcd:
$ ansible-playbook -b -i inventory/mycluster/hosts.yml cluster.yaml --tags=etcd
```

これは結果的に update-cluster.yml と同様に何も変更は行われませんでした。
roles/etcd/tasks/main.yml を読む限りは、削除してから導入する以外にアップグレードする手段はなさそうに思えます。

そのためドキュメントに従って、removing etcd node と adding etcd node の手順を実行していきます。

```bash:ansibleホスト側での作業
$ ansible-playbook -b -i inventory/mycluster/hosts.yaml remove-node.yml -e node=node1
$ ansible-playbook -b -i inventory/mycluster/hosts.yaml cluster.yml --limit=etcd,kube_control_plane -e ignore_assert_errors=yes
```

この後はうまくetcdが動作しなくなってしまったので、etcd_deployment_typeは"host"のまま運用しています。

## v2.19.0 で "not calico_pool_conf.spec.ipipMode is defined or ..." が表示され停止する

upgrade-cluster.yml を実行した時に次のようなメッセージが表示されました。

```text:ログメッセージ
TASK [network_plugin/calico : Check if inventory match current cluster configuration]
fatal: [node1]: FAILED! => {
    "assertion": "not calico_pool_conf.spec.ipipMode is defined or calico_pool_conf.spec.ipipMode == calico_ip ip_mode",
    "changed": false,
    "evaluated_to": false,
    "msg": "Your inventory doesn't match the current cluster configuration"
}           
```

以下のRelease Proposalに次のように明記されています。

```text:ReleaseNotev2.19
[calico] Use vxlan instead of ipip as the default calico encapsulation mode. 
```

* [Release Proposal v2.19 #8885](https://github.com/kubernetes-sigs/kubespray/issues/8885)

このエラーメッセージを出したv2.19.0のk8

```yaml:v2.19.0のinventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
# IP in IP and VXLAN is mutualy exclusive modes.
# set IP in IP encapsulation mode: "Always", "CrossSubnet", "Never"
# calico_ipip_mode: 'Never'

# set VXLAN encapsulation mode: "Always", "CrossSubnet", "Never"
# calico_vxlan_mode: 'Always'
```

v2.18.2のk8s-net-calico.ymlは次のようになっています。

```yaml:v2.18.2のinventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
# IP in IP and VXLAN is mutualy exclusive modes.
# set IP in IP encapsulation mode: "Always", "CrossSubnet", "Never"
# calico_ipip_mode: 'Always'

# set VXLAN encapsulation mode: "Always", "CrossSubnet", "Never"
# calico_vxlan_mode: 'Never'
```

この部分だけではなく、calico_network_backend: がvxlanになっているので、これもbirdに変更します。

```diff:
index cb8cde067..3d0da4b4e 100644
--- a/inventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
+++ b/inventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
@@ -86,14 +86,14 @@ calico_cni_name: k8s-pod-network
 
 # Set calico network backend: "bird", "vxlan" or "none"
 # bird enable BGP routing, required for ipip and no encapsulation modes
-# calico_network_backend: vxlan
+calico_network_backend: bird
 
 # IP in IP and VXLAN is mutualy exclusive modes.
 # set IP in IP encapsulation mode: "Always", "CrossSubnet", "Never"
-# calico_ipip_mode: 'Never'
+calico_ipip_mode: 'Always'
 
 # set VXLAN encapsulation mode: "Always", "CrossSubnet", "Never"
-# calico_vxlan_mode: 'Always'
+calico_vxlan_mode: 'Never'
 
 # set VXLAN port and VNI
 # calico_vxlan_vni: 4096
```

これらの変更によって無事に v2.18.2 から v2.19.0 にアップグレードできました。

## v2.20.0 で "calico_pool_conf.spec.blockSize| ..." と表示され停止する

v1.19.1 から v2.20.0 にする際に、calicoのbackendをbirdにするなどの変更は実施したのですが、別のエラーが表示されました。

Tigeraの公式ガイドをみると、後から変更できないパラメータのデフォルト値を24から26に変更したということのようです。

* [Tigera公式ガイド Change IP pool block size](https://docs.tigera.io/calico/3.25/networking/ipam/change-block-size)

デフォルトのままv2.20.0でupgrade-cluster.ymlを実行すると、次のようなエラーメッセージが表示されます。

```text:エラーメッセージ
TASK [network_plugin/calico : Check if inventory match current cluster configuration] 
fatal: [node1]: FAILED! => {
    "assertion": "calico_pool_conf.spec.blockSize|int == (calico_pool_blocksize | default(kube_network_node_prefix) | int)",
    "changed": false,
    "evaluated_to": false,
    "msg": "Your inventory doesn't match the current cluster configuration"
}       
```

次のようにblocksizeを26から24に変更します。v2.19.1ではコメントアウトされていたので、diffの出力は消えずに、次のような差分が表示されます。

```diff:v2.19.1との差分
diff --git a/inventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml b/inventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
index 3d0da4b4e..4e888abb3 100644
--- a/inventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
+++ b/inventory/mycluster/group_vars/k8s_cluster/k8s-net-calico.yml
@@ -19,7 +19,7 @@ calico_cni_name: k8s-pod-network
 # calico_pool_name: "default-pool"
 
 # add default ippool blockSize (defaults kube_network_node_prefix)
-# calico_pool_blocksize: 24
+calico_pool_blocksize: 24
 
 # add default ippool CIDR (must be inside kube_pods_subnet, defaults to kube_pods_subnet otherwise)
 # calico_pool_cidr: 1.2.3.4/5
```

これを適用して無事にアップグレードが完了しました。

## v2.20.0に更新する際に、ingressがterminatingのまま停止する問題への対応

v2.20.0を適用した際に次のようなエラー状態となり、ingressが停止してしまいました。

```bash:
$ sudo kubectl -n ingress-nginx get pod
NAME                             READY   STATUS        RESTARTS   AGE
ingress-nginx-controller-5z4ww   0/1     Terminating   1          3h35m
ingress-nginx-controller-7rpq9   0/1     Terminating   1          3h34m
ingress-nginx-controller-dntv9   0/1     Terminating   2          3h33m
ingress-nginx-controller-zfqf6   0/1     Terminating   2          3h34m

$ sudo kubectl -n ingress-nginx descirbe pod 'ingress-nginx-controller-5z4ww'
  ...
  Normal   SandboxChanged     10m (x73 over 25m)  kubelet                   Pod sandbox changed, it will be killed and re-created.
  Warning  FailedKillPod      47s (x43 over 10m)  kubelet                   error killing pod: failed to "KillPodSandbox" for "38bd1476-53bc-4fe2-bdb4-926240f2b27a" with KillPodSandboxError: "rpc error: code = Unknown desc = networkPlugin cni failed to teardown pod \"ingress-nginx-controller-5z4ww_ingress-nginx\" network: could not retrieve port mappings: key is not found"
```

また別のシステムでは1ノードのみがRunningとなっていて、かろうじてサービスは継続できていましたが、危険な状態でした。可能であれば別系統のシステムのみにアクセスさせるなど、一時的にサービス提供を停止した状態でアップグレードするのがお勧めです。

システム全体は問題なく動いているため、強制的にPodを停止して様子をみます。

```bash:
$ sudo kubectl -n ingress-nginx delete pod/ingress-nginx-controller-5z4ww  --grace-period=0 --force
```

問題なく再起動が始まり、これを繰り返すことでエラーがなくなりました。

## v2.21.0からRook/Ceph v1.5が動作しなくなる

> Rook/Cephのv1.10のアップグレードガイドでは、k8sを1.25にする前にRook/Cephのバージョンを1.9.10以降にするようガイドされています。

Kubespray v2.21.0(k8s v1.25.6)にアップグレードすると、PodDisruptionBudget(pdb) APIのv1beta1サポートがなくなります。

Rook/Ceph v1.5で利用しているpdb APIはv1beta1です。

Rook/Cephでpdb APIのv1をサポートしているのは、1.6の途中からです。
Rook/Cephのアップグレードでは常に最新版にアップグレードすることが可能です。というか推奨されています。

幸いにもv2.21.0にアップグレードしていて、operatorが起動しないことに気がついたため、それまでv1.24までで動作していたPodは完全に動作している状態でした。

operatorのみが停止している状態で、ceph statusがHEALTH_OKであれば安全にRook/Ceph v1.5からv1.6にアップグレードすることが可能です。

その後は速やかにv1.9.13(現時点でのv1.9系の最新版patch-release)にアップグレードしました。

## v2.22.xでMetalLB 0.13.xへの移行ができない

v2.21.0まではMetalLBのバージョンは0.12.1です。

v2.22.0では、addons.ymlに記載されているMetalLBの構成について、どうやってもlayer3の設定有効を強制されます。

この仕様はv2.22.1では改善されているため、v2.22.0ではmetallbのアップグレードは行わず(metallb_server_enabled: false)にv2.22.1を適用するタイミングでaddons.ymlでmetallb_server_enabledを有効(true)にしました。

:::note
v2.22.0でmetallbをアップグレードしようとして問題に遭遇した場合には、手動でdeploy/controller定義を修正し、コンテナ(controller)定義をv0.12.1に戻すことで復旧するはずです。
:::

v2.22.1からtasks/main.ymlの構成全体がかなり変更されて個別にテンプレートを処理するようになり、BGP(layer3)のチェック処理自体がなくなりました。

ただ実際にインストールしようとすると、次のようにエラーになります。

```text:v2.22.1では別のエラーになりmetallbが導入できない
TASK [kubernetes-apps/metallb : Kubernetes Apps | Wait for MetalLB controller to be running] **********************************************
fatal: [node1]: FAILED! => {"changed": true, "cmd": ["/usr/local/bin/kubectl", "-n", "metallb-system", "wait", "--for=condition=ready","pod", "-l", "app=metallb,component=controller"], "delta": "0:00:30.179250", "end": "2023-09-14 08:36:10.124459", "msg": "non-zero return code", "rc": 1, "start": "2023-09-14 08:35:39.945209", "stderr": "error: timed out waiting for the condition on pods/controller-65c7966fd5-jcg5z", "stderr_lines": ["error: timed out waiting for the condition on pods/controller-65c7966fd5-jcg5z"], "stdout": "pod/controller-699ccdbb74-phtkz condition met", "stdout_lines": ["pod/controller-699ccdbb74-phtkz condition met"]}
```

この時点で metallb の operator が0.13.9と更新されているものの、動作に必要なリソースが定義されていないためMetalLBが正常に起動せずansible-playbookの処理自体が進みません。

この状態では全てのServiceオブジェクトに設定されていた EXTERNAL-IP が pending になり、サービスへのアクセスができなくなりました。

対応策としては次の2点が考えられます。

* cm/configオブジェクトがまだあるはずなので、deploy/controllerのimage:を0.12.1にロールバックし、改善されるまで待つ
* 手動で必要なリソース・オブジェクトを定義し、手動でMetallb 0.13.xを導入する

ここでは後者の方法で進め、手動でIPAddressPoolとL2Advertisementオブジェクトを設定します。

まずaddons.yamlを確認します。

```yaml:addons.yamlからの抜粋
# MetalLB deployment
metallb_enabled: true
metallb_speaker_enabled: "{{ metallb_enabled }}"
metallb_protocol: "layer2"
metallb_config:
  address_pools:
    primary:
      ip_range:
        - "192.168.1.160-192.168.1.199"
      auto_assign: true
  layer2:
    - primary
```

この情報を元に2つのYAMLファイルをあらかじめ準備します。

```yaml:01.IPAddressPool.yamlファイルの内容
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: primary
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.160-192.168.1.199
  autoAssign: true
  avoidBuggyIPs: true
```

```yaml:02.L2Advertisement.yamlファイルの内容
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: primary
  namespace: metallb-system
spec:
  ipAddressPools:
  - primary
```

この状態からcoronされているノードをuncordonし復帰させてから、再度upgrade-cluster.ymlを実行しました。

あらかじめYAMLファイルを準備しておけば処理が停止してから手動で適用しすることで EXTERNAL-IP が pending になる問題は解決できます。

### 自動では移行できない模様

Metallb 0.13.xからCRDsに設定をするように変更が行われ、ConfigMapは廃止されました。

本家では移行作業を進めるためConfigMapの設定内容をコンバートするツールは準備していますが、サービス停止は避けられないように読めます。(明記はされていない)

Metallb 0.12.xの状態からCRDsを先に定義して、IPAddressPoolオブジェクトなどを作成すれば良さそうですが、0.12.xのPodには対応するwebhookが存在しないためエラーになります。

0.12から0.13の間でCRDsとWebhookだけ埋め込んだバージョンをリリースしてくれていれば移行もすんなり行ったのでしょうけれど、どうやってもサービス停止は避けられません。

### サービスへの影響について

v2.23.0でもコードは本質的には変更されていないため、結果はv2.22.xと同じでした。

動的にEXTERNAL-IPを割り当てていたServiceは、そのIPアドレスが変更されてしまうことで影響が出る可能性があります。

v2.22.x(k8s v1.26.5)以降にアップグレードする場合には、その前にServiceオブジェクトの構成を見直す必要がありそうです。

## v2.23.xで"The conditional check 'groups.get('kube_control_plane')' failed."が表示される

v2.23.xに対してrequirements.txtにある通りのansibleを利用すると、ある時点から指定されていないファイルが更新されたことで以下のようなエラーメッセージが表示されます。

```text:  
TASK [kubernetes/preinstall : Stop if either kube_control_plane or kube_node group is empty]
*****************************************************************************************************
fatal: [node1]: FAILED! => {"msg": "The conditional check 'groups.get('kube_control_plane')' failed. The error was: Conditional is marked as unsafe, and cannot be evaluated."}               
```

このエラーは以下のissuesで報告されています。

* [github/kubespray Issues #10688](https://github.com/kubernetes-sigs/kubespray/issues/10688)

修正は[#10699](https://github.com/kubernetes-sigs/kubespray/pull/10699/files)にあるとおり、roles/kubernetes/preinstall/tasks/0040-verify-settings.ymlを編集するだけです。

```diff:roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
diff --git a/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml b/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
index 8cc11b6d5..dcf3a8d01 100644
--- a/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
+++ b/roles/kubernetes/preinstall/tasks/0040-verify-settings.yml
@@ -1,7 +1,7 @@
 ---
 - name: Stop if either kube_control_plane or kube_node group is empty
   assert:
-    that: "groups.get('{{ item }}')"
+    that: "groups.get( item )"
   with_items:
     - kube_control_plane
     - kube_node
```

もし停止(cordon/drain)しているノードがあれば、control-plane上で``sudo kubectl uncordon ...``で復帰させてから、作業用クライアントから ansible-playbook を実行すれば問題なく処理が進むはずです。

## v2.24.1でingressが正常に動作しない

v2.24.0でも発生するかは未確認です。

テスト系にv2.24.1を新規に導入したところIngressオブジェクトを定義してもリクエストがいつまでも正しく動作しない状態になりました。

これはServiceAccountに付与された権限が十分でない場合に発生して次のようなパッチが作成されています。

* [ [release-2.24] [ingress-nginx] Fix nginx controller leader election RBAC permissions #11219 ](https://github.com/kubernetes-sigs/kubespray/pull/11219/files)

この記事での解決策は静的にelection-idを割り当てていますが、ワークアラウンドとして``role/ingress-nginx``の**coordination.k8s.io**の権限を変更することでも対応できます。

```yaml:role/ingress-nginxの最後の方の変更箇所
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - update
  - create
```

アップグレード時には既存のクラスターには影響はないと思われますが、新規のIngressオブジェクトを作成した時には、いつまでもingress-nginxへのアドレスが割り当てられず、Webブラウザからのリクエストが処理できない状態が継続しました。

## v2.25.0を新規に導入できない

:::note
v2.24.1やv2.24.2からのバージョンアップでは問題は発生していません。ただ後述したようにmetalLBとkube-vipを有効にしている場合にはkube_vip_services_enabledをtrueにしないよう注意してください。
:::

現象は様々でkubeadmのinitに失敗してansible-playbook自体の実行に失敗したり、kube-vipが起動しなかったりしています。

kube-vip周りに問題があるような印象ですが詳細についてはよく分かっていません。

関連するトピックについては次のような情報があります。

* [github  kube-vip requires super-admin.conf with Kubernetes 1.29 #684 ](https://github.com/kube-vip/kube-vip/issues/684)
* [github  Feature Reqest: Don't bind the admin.conf user to system:masters #2414 ](https://github.com/kubernetes/kubeadm/issues/2414)
* [kube-vipをKubernetes 1.29で使う](https://www.namachan10777.dev/blog/kube-vip-with-k8s1.29/)

少なくとも次の方法ではv2.25.0が問題なく稼動しています。

1. v2.24.1を新規に導入する。その際、addons.ymlのkube-vip関連のフラグを全てfalseに設定する
1. v2.25.0にアップグレードする。その際、addons.ymlのkube-vip関連のフラグ(``kube_vip_enabled912021``, ``kube_vip_arp_enabled``, ``kube_vip_controlplane_enabled``)をtrueに設定する。ただしMetalLBを有効にしている場合には必ず``kube_vip_services_enabled: false``にする。(``kube_vip_cp_detect``, ``kube_vip_enable_node_labeling``は必要に応じて変更する)

いまのところこれ以外の方法でv2.25.0を導入しても、再現性があまりない状態になっています。admin.confと権限だけが問題であればkube-vipのPodが起動しないのはともかく、他のPod間の通信に影響があるのは別の問題でしょう。

admin.conf以外の問題については、次のセクションと別の記事で解説しています。

## v2.25.0でIngressが時々502エラーを返す

v2.25.0を導入してから通信全体の信頼性がいまいちな印象もありますが、観察可能な範囲では時々502エラーがIngressから返されています。

ログからWebブラウザのリクエストはアプリケーションロジックまでは届いていないので、パケットロスがIngressの直後からアプリのServiceぐらいの間で起っている状況です。

結果的に原因はMetalLBとkube-vipのLBサービスの両方を有効にしたことでした。

顛末については別の記事にまとめています。

* [KubernetesでMetalLBとKube-APIのLBサービスを両方有効にしてはいけない](https://qiita.com/YasuhiroABE/items/004f7c6413168dfd0792)

### 変更したものの有効ではなかったこと

結果的には意味がなかったものの一応調べたのでメモに残しておきます。

以下の情報から暫定的に net.netfilter.nf_conntrack_tcp_be_liberal に 1 を設定してみましたが余計に状況が悪化しました。

* [kube-proxy Subtleties: Debugging an Intermittent Connection Reset](https://kubernetes.io/blog/2019/03/29/kube-proxy-subtleties-debugging-an-intermittent-connection-reset/)

この変更によってRSTパケットの送出が遅れて、arp tableが更新されなくなっているようで、悪い方向に影響が出ています。

次に調べたのがingressに``proxy_next_upstream``オプションを追加する方法です。

* [gitub  Swallowing error on 502 #1488 ](https://github.com/kubernetes/ingress-nginx/issues/1488)

この他にも``service_upstream``などいくつかの候補がありますが、proxy_next_upstreamを設定して様子をみています。

これはarp tableが更新されなくなった時には意味がありませんでしたが、K8s側の設定の更新などによる異常ではない瞬間的にアクセスできない場合には有効だろうと思われます。

## v2.24.2にアップグレードしようとしたらkubeletが起動しなくなった

次のようなメッセージが/var/log/syslogに記録されており、kubesprayが**Check api is up**タスクを実行しているところで停止しました。

```text:/var/log/syslogからの抜粋
Aug 22 07:55:25 node1 kubelet[1834546]: I0822 07:55:25.393685 1834546 dynamic_cafile_content.go:157] "Starting controller" name="client-ca-bundle::/etc/kubernetes/ssl/ca.crt"
Aug 22 07:55:25 node1 kubelet[1834546]: E0822 07:55:25.395759 1834546 run.go:74] "command failed" err="failed to run Kubelet: validate service connection: validate CRI v1 runtime API for endpoint \"unix:///var/run/containerd/containerd.sock\": rpc error: code = Unimplemented desc = unknown service runtime.v1.RuntimeService"
Aug 22 07:55:25 node1 systemd[1]: kubelet.service: Main process exited, code=exited, status=1/FAILURE
Aug 22 07:55:25 node1 systemd[1]: kubelet.service: Failed with result 'exit-code'.
```

```text:ansible-playbookの実行状況
$ ansible-playbook upgrade-cluster.yml -b -i inventory/mycluster/hosts.yaml -e kube_version=v1.28.10 -e containerd_base_runtime_spec_rlimit_nofile=1048576 --limit node1
....
 _____________________________________________________________ 
< TASK [kubernetes/control-plane : Kubeadm | Check api is up] >
 -------------------------------------------------------------
ok: [node1]
Thursday 22 August 2024  16:32:25 +0900 (0:00:01.004)       0:07:59.864 *******
FAILED - RETRYING: [node1]: Kubeadm | Upgrade first master (3 retries left).
FAILED - RETRYING: [node1]: Kubeadm | Upgrade first master (2 retries left).
FAILED - RETRYING: [node1]: Kubeadm | Upgrade first master (1 retries left).
 __________________________________________________________
/ TASK [kubernetes/control-plane : Kubeadm | Upgrade first \
\ master]                                                  /
 ----------------------------------------------------------
fatal: [node1]: FAILED! => ...
```

crictlを実行すると次のようになります。

```bash:
$ sudo crictl info
FATA[0000] validate service connection: validate CRI v1 runtime API for endpoint "unix:///var/run/containerd/containerd.sock": rpc error: code = Unimplemented desc = unknown service runtime.v1.RuntimeService 
```

どうしようもないので手動でcontainerdを再起動します。

```bash:
$ sudo systemctl restart containerd.service
```

しかし問題は解決されず、システムを再起動しました。

```bash:
$ sudo shutdown -r now
```

引き続き問題が発生しているので、node2の/etc/containerd/cri-base.jsonと内容の差分をとってみます。

```bash:
$ ansible node1,node2 -i inventory/mycluster/hosts.yaml -m command -b -a "cat /etc/containerd/cri-base.json"
$ diff -u a.json.jq b.json.jq 
--- a.json.jq   2024-08-22 17:22:11.919236135 +0900
+++ b.json.jq   2024-08-22 17:22:17.609240871 +0900
@@ -59,8 +59,8 @@
     "rlimits": [
       {
         "type": "RLIMIT_NOFILE",
-        "hard": 65535,
-        "soft": 65535
+        "hard": "1048576",
+        "soft": "1048576"
       }
     ],
     "noNewPrivileges": true
```

というわけでJSONの中で数値で指定するべきところが文字列になっていたことが原因でした。ansible-playbookの引数でのrlimit値の変更は止めるようにしたいと思います。

## v2.23.2にアップグレードする時にkube_versionが1.27.9に更新されていない

k8s-cluster.ymlファイルをチェックすると、Release Noesには1.27.9がデフォルトバージョンだよといわれているのにkube_versionには1.27.7が指定されています。

* [https://github.com/kubernetes-sigs/kubespray/issues/10803](https://github.com/kubernetes-sigs/kubespray/issues/10803)

Kubespray v2.23.2の roles/download/defaults/main/checksums.yml には1.27.7も1.27.9も登録されているのでどちらでも良いことになります。

この問題はv2.23.3でも同様です。release-2.23ブランチでは1.27.10に修正されています。

GitHubのissuesではバグではなくてドキュメントの問題だとされているので、このまま1.27.7として更新して、v2.23.3でv1.27.10を適用しました。

## v2.24.2でクラスター再起動後にPodから外部への通信ができなくなった

:::note
最終的に管理しているk8sクラスターではKubespray v2.24.x (k8s v1.28.x)は運用を止めて、v2.25.0 (k8s 1.95.x)に移行しました。
:::

v2.24.2にアップグレードしてから電源の法定点検のためクラスターを2.5日停止しました。結果としてクラスター全体の起動自体には成功していて、Pod間の通信は問題なく行えたのですが、Podから外部への通信ができないことに気がつきました。

Prometheusのログに怪しい点がないためクラスター停止以前はcalico-nodeは機能していたと思われるのですが、詳細は不明です。おそらくクラスターアップグレード後にOSレベルの再起動はなかったため、iptablesなどの設定はそのまま機能していたため気がつかなかったのだろうと思われます。

症状としてはNATが機能せずに10.233.0.0/16ネットワークアドレスのまま外部に通信(DNS)しようとしている様子がFirewallのログに残っていて、calico-nodeには次のようにipsetに起因するログが残されていました。

```text:calico-nodeのログ
ipset v7.11: Kernel and userspace incompatible: settype hash:ip,port with revision 7 not supported by userspace
```

このログを手掛かりにして次のissueを発見したのですが、Calicoのバージョンをv3.27.2以降に上げるのが良さそうです。

* [Calico incompatible ipset protocol version (again)
#8372](https://github.com/projectcalico/calico/issues/8372) 

最終的にKubespray v2.24.2はスキップして、v2.25.0を適用することでCalicoのバージョンがv3.26.4からv3.27.3になり解決しました。

Ingressのように外部からk8sの方向には問題なく通信ができるため、単独で機能するサービスは問題なく動作していました。影響範囲が限られているため気がつきにくい問題だったと思います。

Prometheusでは次のようにPodのCrashLoopとして検出されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/1b5bc10e-48aa-7185-7796-af93430dce2a.png)

テスト系のクラスターでv2.24.2を再度適用することで解決しないか試しましたが、改善はみられませんでした。

## v2.27.0で途中で止まり"CreateContainerConfigError"でPodが動作しなくなった

:::note warn
これはVIPを有効にしてapi-serverをHA構成で稼動させているクラスターで発生する現象です。
:::

最初のノードを更新したタイミングでansible-playbookが停止しました。この状態そのものはtimeoutなどでplaybookが実行できなくなっただけのようにみえましたが、``sudo kubectl get node --all-namespaces -o wide``で確認するとplaybookを適用したノード上のPodがほとんど起動していない状況になりました。

``namespace/nginx-ingress``では次のようなメッセージを表示しています。

```text:kubectl get podの出力
NAME                             READY   STATUS                       RESTARTS      AGE
ingress-nginx-controller-2rvll   0/1     CreateContainerConfigError   0             8m59s
ingress-nginx-controller-7k2j7   0/1     CreateContainerConfigError   2 (14m ago)   79d
ingress-nginx-controller-q98xf   1/1     Running                      2 (45d ago)   79d
ingress-nginx-controller-rtkrr   1/1     Running                      2 (45d ago)   79d
```

気がつかずに手動で、``--limit``をつけてnode2の更新も行ったのでサービスを継続できずに大きな影響を受けています。

```text:kubectl describe pod ..のメッセージ
Events:
  Type     Reason     Age                     From               Message
  ----     ------     ----                    ----               -------
  Normal   Scheduled  7m52s                   default-scheduler  Successfully assigned ingress-nginx/ingress-nginx-controller-2rvll to u109ls01
  Warning  Failed     5m54s (x12 over 7m52s)  kubelet            Error: services have not yet been read at least once, cannot construct envvars
  Normal   Pulled     2m48s (x26 over 7m52s)  kubelet            Container image "registry.k8s.io/ingress-nginx/controller:v1.11.2" already present on machine
```

この他のnamespaceで稼動しているPodもnode1とnode2で動作するものは、api-serverなどを除いて起動しなくなりました。

この現象に該当するissuesは次のものがありました。

https://github.com/kubernetes-sigs/kubespray/issues/11887#issuecomment-2618918176

主な方法は明示的に古いバージョンのkubeletを指定する方法と、``cm/kubeadm-config``リソースを編集し、クラスターを組んでいる他のapi-serverに接続しないようにする方法です。

まずは今後のことも考えて``cm/kubeadm-config``を編集する方法を採用しました。

```yaml:cm/kubeadm-configから抜粋
apiVersion: v1
data:
  ClusterConfiguration: |
    featureGates:
      ControlPlaneKubeletLocalMode: true
```

なおオリジナルのissuesでjumpMasterが掲載しているような``etcd:``セクションの中に``featureGates``を設定することはできないので注意してください。

ただこれだけでは問題は解決せずに、最終的に``/etc/kubernetes/kubelet.conf``を直接編集し、``    server: https://127.0.0.1:6443``と接続先をloopbackに設定してから``sudo systemctl restart kubelet.service``を実行し、ansible-playbookを``--limit``付きで実行することでnode1は収束しました。

```yaml:/etc/kubernetes/kubelet.confの修正箇所
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ...
    server: https://127.0.0.1:6443
  name: cluster.local
contexts:
  ...
```

node2の方はapi-serverの健全性チェックでどうしても``ansible-playbook``が止まってしまうため、``inventory/mycluster/group_vars/all/all.yml``に``roles/kubespray-defaults/defaults/main/download.yml``の412行目から1180行目までのdownloads:セクション全体をコピーして、kubeletの箇所だけv1.30.8を指すようにissuesの記述をコピーしました。

この方法で個別にansible-playbookコマンドでupgrade-cluster.ymlを適用してから、改めて設定を戻し、クラスター全体をv1.31.4に更新しました。

最終的に全体の更新が無事に終ると手動で変更した/etc/kubernets/kubelet.confのserver:行は元の``server: https://lb-apiserver.kubernetes.local:6443``に戻っていました。

なお他のクラスターでは/etc/kubernetes/kubelet.confは最初から``server: https://127.0.0.1:6443``となっていて、更新後もそのままでした。

``ControlPlaneKubeletLocalMode: true``の構成がどの程度ちゃんと動作するのかは次回のバージョンアップなどで確認したいと思います。

:::note
いまのところ``ControlPlaneKubeletLocalMode: true``の設定は期待したようには動作していないようです。v2.27.0の適用時にVIPを動作させている場合には手動でControl-PlaneのNodeで、/etc/kubernetes/kubelet.confのserver:行のホスト名を``localhost``や``127.0.0.1``に変更することをお勧めします。
:::

# さいごに

昔からkubesprayを利用していると、生成されるhosts.ymlファイルがinventory.iniだったり、hosts.iniだったりするかもしれませんが、最新版ではinventory_builder/inventory.py が廃止されて、inventory.ini を手動で構成する方法に統一されました。

inventory.ini だけにするのは良い方法だと思いますし、昔のhosts.ymlが利用できないわけではないのですが、いずれにしても過去の設定と同じになるように構成することが重要なポイントになってきます。

この点ではansible/kubernetesの設定方法や基本的な動作について理解していると楽になるでしょう。

また、別のKubernetesクラスターで同様の作業を実施しようとした時に、Podの状態が異常だったのか、Rook/CephのBlock Storageがumountできずに、システム全体が不安定になっているノードがあり、ノードを再起動する必要がありました。

umountできない状態でのshutdownは時間がかかるので、ノード1つに15分程度かかってしまい、Block Storageが利用できなくなった事から、サービスが停止したPodもありましたが、再起動後は自動的に復旧しました。

根本原因は不明でしたが、可能であれば、作業前にノードを順次再起動するなどして、システムの健全性を確認してから、Kubesprayのバージョンアップをした方がいいと改めて感じた障害でした。