RockyLinux 9 on PowerPC への Kubernetes 導入

## 目的

K8s cluster構築には kubeadm を用いるが、以下要件がある。

https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

> * 次のいずれかが動作しているマシンが必要です
>   * Ubuntu 16.04+
>   * Debian 9+
>   * CentOS 7
>   * Red Hat Enterprise Linux (RHEL) 7
>   * Fedora 25+
>   * HypriotOS v1.0.1+
>   * Container Linux (tested with 1800.6.0)
> * 1台あたり2GB以上のメモリ(2GBの場合、アプリ用のスペースはほとんどありません)
> * 2コア以上のCPU
> * クラスター内のすべてのマシン間で通信可能なネットワーク(パブリックネットワークでもプライベートネットワークでも構いません)
> * ユニークなhostname、MACアドレス、とproduct_uuidが各ノードに必要です。詳細はここを参照してください。
> * マシン内の特定のポートが開いていること。詳細はここを参照してください。
> * Swapがオフであること。kubeletが正常に動作するためにはswapは必ずオフでなければなりません。

CentOS 後継の RockyLinux は 8以上からがリリースされており、且つ PowerPC系(ここではPower9(ppc64le)を対象とする)のサポートは RockyLinux 9 以上が必要。

kubeadm標準ではサポート対象外だが、導入した際の手順・注意事項などをここに記す。

## 事前準備

RockyLinux 9特有のことは無い。

### swap off

一時的には以下のようにする。

```
$ sudo swapoff -a
```

恒久的には /etc/fstab 上の swap マウント箇所をコメントアウトすればよい。

```
$ vi /etc/fstab

# xxxxx      none                    swap    defaults        0 0

```

swap off になっているかは以下で確認できる。

```
$ cat /proc/swaps
```

空っぽなら swap off 状態になっている。


### port有効化

以下のポートを有効にしておく必要がある。
https://kubernetes.io/ja/docs/reference/networking/ports-and-protocols/

* コントロールプレーン

| プロトコル | 通信の向き | ポート範囲 | 目的 | 使用者 |
|-|-|-|-|-|
| TCP | Inbound | 6443 | Kubernetes | API | server | 全て |
| TCP | Inbound | 2379-2380 | etcd | server | client | API | kube-apiserver, | etcd |
| TCP | Inbound | 10250 | Kubelet | API | 自身, | コントロールプレーン |
| TCP | Inbound | 10259 | kube-scheduler | 自身 |
| TCP | Inbound | 10257 | kube-controller-manager | 自身 |

* ワーカーノード

| プロトコル | 通信の向き | ポート範囲 | 目的 | 使用者 |
|-|-|-|-|-|
| TCP | Inbound | 10250 | Kubelet API | 自身, コントロールプレーン |
| TCP | Inbound | 30000-32767 | NodePort Services† | 全て |

control-plane にする場合は以下でポートを有効にする。

```sh
$ sudo firewall-cmd --zone=public --add-port 6443/tcp --add-port 2379-2380/tcp --add-port 10250/tcp  --add-port 10259/tcp  --add-port 10257/tcp  --add-port 30000-32767/tcp --permanent
$ sudo firewall-cmd --reload
```

## Installing a container runtime

https://kubernetes.io/docs/setup/production-environment/container-runtimes/

### Enable IPv4 packet forwarding

```sh
# sysctl params required by setup, params persist across reboots
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF

# Apply sysctl params without reboot
$ sudo sysctl --system
```

### Disable IPv6

```sh
$ cat <<EOF | sudo tee /etc/sysctl.d/disable_ipv6.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
EOF

# Apply sysctl params without reboot
$ sudo sysctl --system
```


### build and install cri-dockerd

docker を使うため、cri-dockerd をインストールする。

https://github.com/Mirantis/cri-dockerd/releases
に対応するパッケージがあるが、PowerPC用のものが提供されてないのでビルドするしかない。

```sh
$ git clone https://github.com/Mirantis/cri-dockerd
$ cd cri-dockerd
$ git checkout v0.3.13
$ make cri-dockerd
```

https://mirantis.github.io/cri-dockerd/usage/install-manually/
に従ってcri-dockerd サービスを設定する。

```
$ sudo install -o root -g root -m 0755 cri-dockerd /usr/local/bin/cri-dockerd
$ sudo install packaging/systemd/* /etc/systemd/system
$ sudo sed -i -e 's,/usr/bin/cri-dockerd,/usr/local/bin/cri-dockerd,' /etc/systemd/system/cri-docker.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now cri-docker.socket
$ sudo systemctl start cri-docker.socket
```

## install kubeadm, kubelet, kubectl

https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/
に従って行う。

### add kubernatis yum repository

```sh
# This overwrites any existing configuration in /etc/yum.repos.d/kubernetes.repo
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
```

### install kubelet, kubeadm and kubectl

```sh
sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
```

### (Optional) Enable the kubelet service before running kubeadm:

```sh
sudo systemctl enable --now kubelet
```

## Configuring a cgroup driver

https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/configure-cgroup-driver/
によれば 'systemd' driver を推奨するとあり、かつ 1.22 以降では 'systemd' driverがデフォルトになっているとあるため、何もしない。

## Creating a cluster

### Network setup

各ノードに以下要件を満たしていること。

* default route が設定されていること
* ノード間で唯一のIPで通信しあえること

### Initializing control-plane node

#### initialize control-plane single node

kubeadm で初期化実行。

* control-plane-endpoint に DNSラウンドロビンアドレスを指定 (→ k8s-cluster とする)
* cri-socket には cri-docker を指定
* pod-network-cidr は 10.244.0.0/16 とする
  * service-cluster-ip-range のデフォルトが 10.96.0.0/12 のためoverlapしないようにする
  * network add-in に flannel を選択したが、そのデフォルト値が 10.244.0.0/16 となっており、変更しても何故か反映されない(多分、flannel のバグ)ため、デフォルト値に従う。
* HA availability のために upload-certs を指定

```sh
$ sudo -E kubeadm init \
   --control-plane-endpoint k8s-cluster \
   --cri-socket unix:///var/run/cri-dockerd.sock \
   --pod-network-cidr 10.244.0.0/16 \
   --upload-certs
```

以下のようなメッセージが出ればクラスタ構築成功。

```
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/
```

失敗した場合、いくつかのコンテナが動作した状態になっているので、'kubeadm reset' を実行して一旦リセットする。
cri-socket の指定も忘れずに。

```sh
$ sudo -E kubeadm reset --cri-socket unix:///var/run/cri-dockerd.sock
```

#### add control-plane node

`kubeadm init --upload-certs' とした場合、以下のようなメッセージが出る。

```
You can now join any number of the control-plane node running the following command on each as root:

  kubeadm join k8s-cluster:6443 --token XXXXXXXXXXXXXXXXXXX \
        --discovery-token-ca-cert-hash sha256:YYYYYYYYYYYYYYYYYYYYYY \
        --control-plane --certificate-key ZZZZZZZZZZZZZZZZZZ

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join k8s-cluster:6443 --token XXXXXXXXXXXXXXXXXXXX \
        --discovery-token-ca-cert-hash sha256:YYYYYYYYYYYYYYYYYYYYYY 
```

要するに、認証鍵が 2h間有効になっているため、その間に control-plane node を kubeadm join で追加せよ、といこと。

2h 過ぎた場合は、鍵を再生成するよう `kubeadm init phase upload-certs --upload-certs` を実行すればよい。

```sh
$ sudo kubeadm init phase upload-certs --upload-certs
[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[upload-certs] Using certificate key:
ZZZZZZZZZZZZZZZZZZZZ
```

ノード追加用トークンの再生成は以下。

```sh
$ kubeadm token create --print-join-command
kubeadm join k8s-cluster:6443 --token XXXXXXXXXXXXXXXX --discovery-token-ca-cert-hash sha256:YYYYYYYYYYYYYYYYYYYYYY 
```

追加ノードを control-plane にしたい場合は鍵再生成結果を --certificate-key に指定する。
cri-docker 指定も忘れずに。

```sh
$ sudo kubeadm join k8s-cluster:6443 \
  --token XXXXXXXXXXXXXXXXX --discovery-token-ca-cert-hash sha256:YYYYYYYYYYYYYYYYYYYY \
  --certificate-key  ZZZZZZZZZZZZZZZZZZZZZ \
  --cri-socket unix:///var/run/cri-dockerd.sock \
  --control-plane
```

#### add worker node to cluster

control-plane でないworkerノードを追加する場合は `--control-plane` の部分を抜けばよい。

```sh
$ sudo kubeadm join k8s-cluster:6443 \
  --token XXXXXXXXXXXXXXXXX --discovery-token-ca-cert-hash sha256:YYYYYYYYYYYYYYYYYYYY \
  --certificate-key  ZZZZZZZZZZZZZZZZZZZZZ \
  --cri-socket unix:///var/run/cri-dockerd.sock
```

#### delete node

ノードの削除方法は以下のように drain して関連daemonsets を消してから delete node する。

```sh
$ kubectl drain [node-name] --ignore-daemonsets --delete-emptydir-data
$ kubectl delete node [node-name]
```

## docker credential情報を使用する

https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials

docker loginしたときの認証情報をk8sにも適用したいとき。

```sh
$ kubectl create secret generic regcred \
    --from-file=.dockerconfigjson=$HOME/.docker/config.json \
    --type=kubernetes.io/dockerconfigjson
```

匿名で使う場合は pull 回数に制限があるため、認証することで回避

## Installing a Pod network add-on 

k8s pod 内のネットワークをどのように管理するかのポリシー的なアルゴリズムを入れる必要がある。

https://kubernetes.io/docs/concepts/cluster-administration/addons/

から選べるとあるが、**PowerPCの場合は Flannel 一択**。

### Flannel

マニフェストを適用すればインストールできる。

```sh
$ kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
```

flannelのデフォルト設定では CIDR 割当範囲が 10.244.0.0/16 となっているが、変更するには yaml ファイルをダウンロードしてから編集して適用する

```sh
$ wget https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
$ vi kube-flannel.yml
...
  net-conf.json: |
    {
      "Network": "192.168.49.0/24",
      "Backend": {
        "Type": "vxlan"
      }
    }
...

## 編集したflannelを適用。
$ kubectl apply -f kube-flannel.yml
```

#### firewall configuration

flannel はバックエンドとして VXLAN を用いるが、特定の UDPポートをノード間の通信に使用する。
https://github.com/flannel-io/flannel/blob/master/Documentation/backends.md

Linuxの場合は 8472/UDP を用いるため、各ノードで許可しておく。

```sh
$ sudo firewall-cmd --permanent --zone=public --add-port=8472/udp
$ sudo firewall-cmd --reload
```


### Calico

一般的には Calico の利用率が高いらしいが、残念ながら PowerPC 系には対応していない。

具体的には [calico/node-driver-registrarというコンテナ](https://hub.docker.com/r/calico/node-driver-registrar/tags) が platform  linux/ppc64le を提供しているにも関わらず "exec format error" で動かない。 

なんじゃそりゃー  (ノ｀Д´)ノ彡┻━┻

---
(後日談)
どうやら PowerPC 未対応なのは release-3.27 以前の模様。
現在 3.29 までがリリースされているが、3.28 以降であれば exec format error にならないため動きそう。

が、今更に移行する気は無いので保留。

## local-path dynamic volume provisioner

local path はdynamic provisioning に対応していないため、都度割り当てる必要がある。

https://kubernetes.io/docs/concepts/storage/storage-classes/#local
> Local volumes do not support dynamic provisioning in Kubernetes 1.30; however a StorageClass should still be created to delay volume binding until a Pod is actually scheduled to the appropriate node. This is specified by the WaitForFirstConsumer volume binding mode.

local path の dynamic provisioning の対応例に以下のものがある。
https://github.com/rancher/local-path-provisioner

だが、残念ながら PowerPC系には対応していない。
PowerPCを除外して適用するには以下のようにする。

```
$ wget https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.26/deploy/local-path-storage.yaml
$ vi local-path-storage.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-path-provisioner
  namespace: local-path-storage
spec:
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                - key: "kubernetes.io/arch"
                  operator: In
                  values: ["amd64", "arm64"]

...
```

## firewall backend の変更

istioやflannelではPod間の通信制御に iptables を使用するが、RHEL9 では iptables が deprecated になり nftables に移行した。

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html/9.0_release_notes/deprecated_functionality#deprecated-functionality_networking

> ipset and iptables-nft have been deprecated
> The ipset and iptables-nft packages have been deprecated in RHEL. The iptables-nft package contains different tools such as iptables, ip6tables, ebtables and arptables. These tools will no longer receive new features and using them for new deployments is not recommended. As a replacement, prefer using the nft command-line tool provided by the nftables package. Existing setups should migrate to nft if possible.

この仕様変更により、RockyLinux 9 では pod間の通信ができなくなってしまっている。

本来なら K8s がiptablesでなく nftables に対応してほしいところだが、従来の iptables で制御することを許可すれば当座はしのげる。

### バックエンドを iptables に変更

`/etc/firewalld/firewalld.conf`:

```
#FirewallBackend=nftables
FirewallBackend=iptables
```

以下で適用。

```
$ sudo systemctl restart firewalld
$ sudo firewall-cmd --reload
```

### iptables rule順序変更

backend を iptables に変更しても、iptables チェインが以下のようになる場合がある。

```sh
$ sudo iptables -L FORWARD --line-number

Chain FORWARD (policy ACCEPT)
num  target     prot opt source               destination         
1    KUBE-PROXY-FIREWALL  all  --  anywhere             anywhere             ctstate NEW /* kubernetes load balancer firewall */
2    KUBE-FORWARD  all  --  anywhere             anywhere             /* kubernetes forwarding rules */
3    KUBE-SERVICES  all  --  anywhere             anywhere             ctstate NEW /* kubernetes service portals */

...

17   FORWARD_direct  all  --  anywhere             anywhere            
18   FORWARD_ZONES  all  --  anywhere             anywhere            
19   DROP       all  --  anywhere             anywhere             ctstate INVALID
20   REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited
21   FLANNEL-FWD  all  --  anywhere             anywhere             /* flanneld forward */
```

21番目にある FLANNEL-FWD ルールが REJECT より後にあるため、当該ノードへの通信が遮断されてしまう。
このルールを上に上げる必要があるが、FORWARD_ZONES内の FWD_public ルール内でREJECTしているため、それより上にする必要がある。

```sh
$ sudo iptables -L FORWARD_ZONES --line-number
# Warning: iptables-legacy tables present, use iptables-legacy to see them
Chain FORWARD_ZONES (1 references)
num  target     prot opt source               destination         
1    FWD_docker  all  --  anywhere             anywhere            [goto] 
2    FWD_docker  all  --  anywhere             anywhere            [goto] 
3    FWD_public  all  --  anywhere             anywhere            [goto] 
4    FWD_public  all  --  anywhere             anywhere            [goto] 
5    FWD_public  all  --  anywhere             anywhere            [goto] 
6    FWD_public  all  --  anywhere             anywhere            [goto] 

$ sudo iptables -L FWD_public --line-number
# Warning: iptables-legacy tables present, use iptables-legacy to see them
Chain FWD_public (4 references)
num  target     prot opt source               destination         
1    FORWARD_POLICIES_pre  all  --  anywhere             anywhere            
2    FWD_public_pre  all  --  anywhere             anywhere            
3    FWD_public_log  all  --  anywhere             anywhere            
4    FWD_public_deny  all  --  anywhere             anywhere            
5    FWD_public_allow  all  --  anywhere             anywhere            
6    FWD_public_post  all  --  anywhere             anywhere            
7    FORWARD_POLICIES_post  all  --  anywhere             anywhere            
8    REJECT     all  --  anywhere             anywhere             reject-with icmp-port-unreachable
```

上記例では FORWARD チェイン内 18番目に FORWARD_ZONES があるため、FLANNEL-FWD ルールをそれより上になるようにすればよい。

```sh
### FLANNEL-FWD ルール を No.18 に挿入
$ sudo iptables -I FORWARD 18 -j FLANNEL-FWD
```

当然ながら、iptables ルールが更新されるとルールも刷新されるため、この対処は **"都度"** 必要になる。

```
### iptables が更新される
$ sudo firewall-cmd --reload
```

そもそも RHEL9 では前述のとおり iptables による制御は deprecated のため暫定的であるべきである。