MiniPCでKubernetesクラスターを構築してみた

# はじめに

格安のIntel N100 CPUを搭載したMini PCを3台ほど入手したので、自分のテスト用にk8sクラスターをkubesprayを使って構築してみたので、その顛末をメモしておきます。

主な用途はCustom Controllerのテスト用なので、実用的なアプリケーションが稼動するかどうかにあまり関心はありませんが、本番機と同様にrook/cephも構成させるため、2台目のSSDが搭載できる点も機種選定の要件の1つでした。

格安Mini PCはM.2とWi-Fi用のPCIeスロットだけを備えている場合があります。M.2と2.5インチのSSDを両方利用できるかことが必要でした。

またWi-FiやBluetoothは使用しませんが、購入したEQ12には技適シールが外装に貼付されていました。電源もPSEマークに検査会社名が併記されていて、形式的にはEQ12の品質はかなり高い印象です。

数ヶ月利用した印象でも特に製品に問題がある印象はありません。ただ価格を考えると電源周りが突然壊れるといったことは想定しておくべきだとは思います。

Kubernetesのノードとしては突然停止しても問題ないので今回のような用途には向いているかもしれません。

# 環境

Workerノード用には次の機器を準備しました。(node2〜4の3台)

* Hardware: Beelink EQ12 (16GB Memory, 500GB NVMe SSD)
* OS: Ubuntu 22.04.02 Server版 (AutoInstallによる自動インストール)
* 追加Drive: 250GB (2.5inch SATA SSD)
* IP: 192.168.110.22〜24/24

Control-Planeノード用に余っていたMini PCを準備しました。(node1の1台のみ, 192.168.110.21)

* Hardware: Minisforum UM350 (32GB Memory, 500GB NVMe SSD)
* OS: Ubuntu 22.04.02 Server版 (AutoInstallによる自動インストール, Beelinkと同じイメージ)
* IP: 192.168.110.21/24

これらのノードをAnsibleを使って同じ構成にした上で、kubesprayを導入していきます。
実際にはnode2もControl-Planeとして構成しています。kubectlコマンドはnode1上のみで実行しています。

```bash:付属NVMe-SSDのパフォーマンステスト
## EQ12
$ sudo hdparm -Tt /dev/nvme0n1

/dev/nvme0n1:
 Timing cached reads:   18550 MB in  2.00 seconds = 9286.39 MB/sec
 Timing buffered disk reads: 2340 MB in  3.00 seconds = 779.46 MB/sec

## UM350
$ sudo /sbin/hdparm -Tt /dev/nvme0n1

/dev/nvme0n1: 
 Timing cached reads:   19126 MB in  1.99 seconds = 9631.33 MB/sec
 Timing buffered disk reads: 2854 MB in  3.00 seconds = 951.02 MB/sec 
```

# ネットワーク構成と準備作業

こちらの都合でAPU6を2台使って光ケーブル(MMF,OM3)で延伸していますが、先頭のAPU6でVPP(FD.io)を使ってプライベートネットワーク(192.168.110.0/24)を構成しています。

![20230624_overview-minipc-k8s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/55f7f23e-e579-2316-0a5b-e15d4b880b27.png)


apu6で稼動するdnsmasqからDHCP経由で固定IPを割り当てるように設定しています。

```text:/etc/dnsmasq.d/default.confの該当箇所
dhcp-host=1c:83:41:30:aa:aa,192.168.110.21 # node1, UM350
dhcp-host=7c:83:34:b9:bb:bb,192.168.110.22 # node2, EQ12
dhcp-host=7c:83:34:b9:cc:cc,192.168.110.23 # node3, EQ12
dhcp-host=7c:83:34:b9:dd:dd,192.168.110.24 # node4, EQ12
```

ansibleを使用してopenntpdを導入し、時刻情報を合わせています。

```bash:
$ ansible all -m command -a date
node2 | CHANGED | rc=0 >>
Fri Jun 23 09:10:54 AM UTC 2023
node3 | CHANGED | rc=0 >>
Fri Jun 23 09:10:54 AM UTC 2023
node4 | CHANGED | rc=0 >>
Fri Jun 23 09:10:54 AM UTC 2023
node1 | CHANGED | rc=0 >>
Fri Jun 23 09:10:54 AM UTC 2023
```

# kubesprayによるk8sクラスターの構成

具体的な手順は下記のドキュメントを参照してください。

* [Qiita - Kubesprayを利用してKubernetesをデプロイ・アップグレードした時のメモ](https://qiita.com/YasuhiroABE/items/3aaf7ceb314f47cd62d9)

```bash:kubespray利用時のコマンドライン
$ ansible-playbook -i inventory/mycluster/hosts.yaml --become --become-user=root うcluster.yml
```

dnsmasqが稼動するGatewayのAPU6上でkubesprayを利用して、k8sクラスターを構成しています。
この構成では実行が終わるまで約1時間かかりました。

```bash:ansible-playbookの実行結果
PLAY RECAP **************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0                   
node1                      : ok=826  changed=141  unreachable=0    failed=0    skipped=1300 rescued=0    ignored=8                   
node2                      : ok=722  changed=124  unreachable=0    failed=0    skipped=1139 rescued=0    ignored=3                  
node3                      : ok=583  changed=95   unreachable=0    failed=0    skipped=809  rescued=0    ignored=2                  
node4                      : ok=523  changed=73   unreachable=0    failed=0    skipped=777  rescued=0    ignored=1                  
                                                                                                                                    
Friday 23 June 2023  22:31:19 +0900 (0:00:01.091)       0:54:45.648 ***********                                                     
===============================================================================  
```

get nodeの出力は次のようになっています。

```bash:kubectlを実行した様子
$ sudo kubectl get node -o wide
NAME    STATUS   ROLES           AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
node1   Ready    control-plane   11h   v1.26.5   192.168.110.21   <none>        Ubuntu 22.04.2 LTS   5.15.0-75-generic   containerd://1.7.1
node2   Ready    control-plane   11h   v1.26.5   192.168.110.22   <none>        Ubuntu 22.04.2 LTS   5.15.0-75-generic   containerd://1.7.1
node3   Ready    <none>          11h   v1.26.5   192.168.110.23   <none>        Ubuntu 22.04.2 LTS   5.15.0-75-generic   containerd://1.7.1
node4   Ready    <none>          11h   v1.26.5   192.168.110.24   <none>        Ubuntu 22.04.2 LTS   5.15.0-75-generic   containerd://1.7.1
```

# k8sクラスターの構成

今回利用するK8sクラスターは、実際に利用しているk8sクラスターに対する実験用の環境なので、kubesprayの機能によって次のような機能を有効にしています。

* OpenID Connect連携 (RBAC, メールアドレスによるClusterRole, Roleの作成)
* Ingress (namespace: ingress-nginx)
* MetalLB (LoadBalancer, namespace: metallb-system)

さらに次のようなコンポーネントを有効にします。

* Rook/Ceph (Block Storage, Shared Filesystem)
* Minio (Object Store)

## Kubesprayでの追加機能の構成状況

git diffの出力は次のようになります。

```diff:diff出力
diff --git a/inventory/mycluster/group_vars/k8s_cluster/addons.yml b/inventory/mycluster/group_vars/k8s_cluster/addons.yml
index cb7868846..216633ba2 100644
--- a/inventory/mycluster/group_vars/k8s_cluster/addons.yml
+++ b/inventory/mycluster/group_vars/k8s_cluster/addons.yml
@@ -4,7 +4,7 @@
 # dashboard_enabled: false
 
 # Helm deployment
-helm_enabled: false
+helm_enabled: true
 
 # Registry deployment
 registry_enabled: false
@@ -97,7 +97,7 @@ rbd_provisioner_enabled: false
 # rbd_provisioner_reclaim_policy: Delete
 
 # Nginx ingress controller deployment
-ingress_nginx_enabled: false
+ingress_nginx_enabled: true
 # ingress_nginx_host_network: false
 ingress_publish_status_address: ""
 # ingress_nginx_nodeselector:
@@ -174,7 +174,7 @@ cert_manager_enabled: false
 #   - "--dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53"
 
 # MetalLB deployment
-metallb_enabled: false
+metallb_enabled: true
 metallb_speaker_enabled: "{{ metallb_enabled }}"
 # metallb_speaker_nodeselector:
 #   kubernetes.io/os: "linux"
@@ -199,15 +199,15 @@ metallb_speaker_enabled: "{{ metallb_enabled }}"
 #     value: ""
 #     effect: "NoSchedule"
 # metallb_version: v0.13.9
-# metallb_protocol: "layer2"
+metallb_protocol: "layer2"
 # metallb_port: "7472"
 # metallb_memberlist_port: "7946"
-# metallb_config:
-#   address_pools:
-#     primary:
-#       ip_range:
-#         - 10.5.0.0/16
-#       auto_assign: true
+metallb_config:
+  address_pools:
+    primary:
+      ip_range:
+        - 192.168.110.50-192.168.110.79
+      auto_assign: true
 #     pool1:
 #       ip_range:
 #         - 10.6.0.0/16
@@ -216,8 +216,8 @@ metallb_speaker_enabled: "{{ metallb_enabled }}"
 #       ip_range:
 #         - 10.10.0.0/16
 #       auto_assign: true
-#   layer2:
-#     - primary
+  layer2:
+    - primary
 #   layer3:
 #     defaults:
 #       peer_port: 179
diff --git a/inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml b/inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
index 2699eff2f..1036d1424 100644
--- a/inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
+++ b/inventory/mycluster/group_vars/k8s_cluster/k8s-cluster.yml
@@ -39,20 +39,20 @@ kube_log_level: 2
 credentials_dir: "{{ inventory_dir }}/credentials"
 
 ## It is possible to activate / deactivate selected authentication methods (oidc, static token auth)
-# kube_oidc_auth: false
+kube_oidc_auth: true
 # kube_token_auth: false
 
 
 ## Variables for OpenID Connect Configuration https://kubernetes.io/docs/admin/authentication/
 ## To use OpenID you have to deploy additional an OpenID Provider (e.g Dex, Keycloak, ...)
 
-# kube_oidc_url: https:// ...
-# kube_oidc_client_id: kubernetes
+kube_oidc_url: https://oidc.example.com/dex
+kube_oidc_client_id: k8s-rbac
 ## Optional settings for OIDC
 # kube_oidc_ca_file: "{{ kube_cert_dir }}/ca.pem"
-# kube_oidc_username_claim: sub
+kube_oidc_username_claim: email
 # kube_oidc_username_prefix: 'oidc:'
-# kube_oidc_groups_claim: groups
+kube_oidc_groups_claim: groups
 # kube_oidc_groups_prefix: 'oidc:'
 
 ## Variables to control webhook authn/authz
@@ -126,7 +126,7 @@ kube_proxy_mode: ipvs
 
 # configure arp_ignore and arp_announce to avoid answering ARP queries from kube-ipvs0 interface
 # must be set to true for MetalLB, kube-vip(ARP enabled) to work
-kube_proxy_strict_arp: false
+kube_proxy_strict_arp: true
 
 # A string slice of values which specify the addresses to use for NodePorts.
 # Values may be valid IP blocks (e.g. 1.2.3.0/24, 1.2.3.4/32).
```

ここまでの構成でEQ12の各ノードは10GB程度の空きメモリ、4GB程度のCacheが利用されている状況でした。

```bash:メモリの利用状況
$ ansible all -m command -a 'grep Mem /proc/meminfo'
node4 | CHANGED | rc=0 >>
MemTotal:       16141856 kB
MemFree:        11211012 kB
MemAvailable:   15146428 kB
node1 | CHANGED | rc=0 >>
MemTotal:       30744996 kB
MemFree:        24403472 kB
MemAvailable:   28995036 kB
node3 | CHANGED | rc=0 >>
MemTotal:       16141856 kB
MemFree:        10738372 kB
MemAvailable:   15065692 kB
node2 | CHANGED | rc=0 >>
MemTotal:       16141844 kB
MemFree:         9959016 kB
MemAvailable:   14665180 kB
```

## Rook/Cephの導入

Helmではなく公式ガイドに従ってgit cloneしたリポジトリのYAMLファイルを利用します。

```bash:
$ git clone https://github.com/rook/rook.git
$ cd rook
$ git checkout refs/tags/v1.11.8 -b t_v1.11.8
$ cd rook/deploy/examples
$ sudo kubectl create -f crds.yaml -f common.yaml -f operator.yaml
$ suod kubectl create -f cluster.yaml
```

ここで job.batch/rook-ceph-osd-prepare-node[1-4] が終わるまで状況を確認しながら待ちます。

```bash:
$ sudo kubectl -n rook-ceph get pod -w
```

OSDが構成されてから、Block Storageと、Shared Filesystemを構成します。

```bash:
$ sudo kubectl create -f csi/rbd/storageclass.yaml
$ sudo kubectl create -f filesystem.yaml 
$ sudo kubectl create -f csi/cephfs/storageclass.yaml 
```

StorageClassNameが登録されているかどうか確認します。

```bash:
$ sudo kubectl get sc
NAME              PROVISIONER                     RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
rook-ceph-block   rook-ceph.rbd.csi.ceph.com      Delete          Immediate           true                   2m9s
rook-cephfs       rook-ceph.cephfs.csi.ceph.com   Delete          Immediate           true                   24s
```

toolboxを登録し、``ceph status`` の出力を確認します。

```bash:ceph_statusの実行状況
$ sudo kubectl apply -f toolbox.yaml
$ sudo kubectl -n rook-ceph exec -it rook-ceph-tools-9b7967b5d-874ph -- ceph status
  cluster:
    id:     cd847450-85ed-49fa-9e82-d713932809eb
    health: HEALTH_OK
 
  services:
    mon: 3 daemons, quorum a,b,c (age 12m)
    mgr: b(active, since 5m), standbys: a
    mds: 1/1 daemons up, 1 hot standby
    osd: 3 osds: 3 up (since 6m), 3 in (since 6m)
 
  data:
    volumes: 1/1 healthy
    pools:   4 pools, 97 pgs
    objects: 37 objects, 4.0 MiB
    usage:   33 MiB used, 699 GiB / 699 GiB avail
    pgs:     97 active+clean
```



## Minioの導入

MinioはHelmを利用して導入します。2023年6月時点のドキュメントではいくつかのインストール方法が公式ガイドに掲載されていますが、今回は既存環境のテスト環境を構築したいため以前に実施した方法をそのまま利用します。

* [Qiita - Helmでminioをk8sクラスターに導入してみた時のメモ](https://qiita.com/YasuhiroABE/items/f6d4628bd747eb4550b1)

今回展開したChartは、**minio-5.0.11.tgz** でした。

導入が終わった直後は次のような状況になります。

```bash:
$ sudo kubectl -n minio get all
NAME             READY   STATUS    RESTARTS   AGE
pod/my-minio-0   1/1     Running   0          60m
pod/my-minio-1   1/1     Running   0          60m
pod/my-minio-2   1/1     Running   0          60m

NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/my-minio           ClusterIP   10.233.27.178   <none>        9000/TCP   60m
service/my-minio-console   ClusterIP   10.233.15.130   <none>        9001/TCP   60m
service/my-minio-svc       ClusterIP   None            <none>        9000/TCP   60m

NAME                        READY   AGE
statefulset.apps/my-minio   3/3     60m
```

手元のWebブラウザーからMinioコンソールにアクセスしようとするには、External-IPを割り当てるのが簡単です。ポートフォワードを利用する方法はkubectlコマンドを手元のPCで動作している場合には有効ですが、リモートホストでkubectlを実行している場合には簡単な方法とはいえません。

作業をする時にだけMinioコンソールにアクセスするためのYAMLファイルを準備しておきます。

既存の **svc/my-minio-console** の定義を確認 (``$ sudo kubectl -n minio get svc my-minio-console -o yaml``) し、type: LoadBalancerに変更したYAMLファイルを準備します。

```yaml:svc-minio-console-lb.yaml
---
apiVersion: v1
kind: Service
metadata:
  namespace: minio
  name: my-minio-console-lb
spec:
  type: LoadBalancer
  ports:
    - name: http
      port: 9001
      targetPort: 9001
      protocol: TCP
  selector:
    app: minio
    release: my-minio
```

今回のシステム構成では外部からAPU6から内部に作られた192.168.110.0/24にアクセスする手段はないため、最終的にはopenvpnを構成しています。

特権ユーザーのID, パスワードについては、次のようなコマンドで抽出することができます。

```bash:
$ sudo kubectl -n minio get secret my-minio -o jsonpath='{.data.rootUser}' | base64 -d  && echo
BQUkgp6JDGOEp9K51YRz

$ sudo kubectl -n minio get secret my-minio -o jsonpath='{.data.rootPassword}' | base64 -d  && echo
52grRijHveoO8QjQM9QUqf1ewIz5lqUqEi9IvEKV
```

## ingress-nginxの構成

kubesprayで導入したingress-nginxの構成は次のようになっています。

```bash:
$ sudo kubectl -n ingress-nginx get all

NAME                                 READY   STATUS    RESTARTS   AGE

pod/ingress-nginx-controller-6sgrq   1/1     Running   0          25h
pod/ingress-nginx-controller-7j4cl   1/1     Running   0          25h
pod/ingress-nginx-controller-cf9df   1/1     Running   0          25h
pod/ingress-nginx-controller-k5rxd   1/1     Running   0          25h

NAME                                      DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/ingress-nginx-controller   4         4         4       4            4           kubernetes.io/os=linux   25h
```

これでは外部からのingressに対するアクセスを受け付ける処理などもできないため、svcオブジェクトやingress-nginx-controllerに指示を出すためのIngressオブジェクトなどを定義していきます。

# 気になったこと

## K8sクラスターの再起動までに時間がかかる

もちろん単体での再起動に時間がかかるということではなく、k8sクラスターを構成しているとノード全体の再起動を試みた際になかなか停止せず再起動が終わらない現象に遭遇します。

全体のパフォーマンスが低い事の影響がこういうタイミングで発生していて、Minioなどが動作しているだけで自作のアプリケーションなどは稼動していない状況でも再起動を開始してから完了するまでに15分程度の時間が必要な状況が発生しています。

実際にはapi-serverが起動してから各種のReconcile処理が走るので全体が安定するまでには30分程度はかかる印象です。

## 突然に電源が落ちる

特定のメーカーなどとは無関係に、ほぼ全てのノードでこの5ヶ月ほどの間に突然電源が落ちています。

各ノード2回ぐらいで頻度は稼動時間などとは無関係ですが、6ノードあるのでサービスに長期的な影響は出ていません。

ただ10年以上使っている富士通製TX120 S3pの安定性に比べると、容積が小さいことで放熱に問題がある可能性を疑っています。

長期的に安定して使えるかというと、見た目の面白さはありますが、真面目にビジネス用途に使うことは絶対にお勧めしません。

# さいごに

テスト用の環境なので再構築もするだろうと思い、自分用のメモとして作業内容をまとめました。

APU6などでの作業はansibleのplaybookでもまとめているので、適宜ローカルのgitlabリポジトリなども活用しています。

システムの最低限の構成が終った段階では、/proc/meminfoのMemAvailableは10GB程度あるので、かなり余裕があるという印象です。DBを活用する構成のアプリケーションを複数動作させても稼動自体は問題なさそうです。

Intel J1900 CPUを搭載したMini Fanless Serverを購入したことがありましたが8GBしかメモリを搭載できないこともあって、実用的なワークロードを稼動させることはできなかったことを考えると簡単に実験ができる良い時代になったなと思います。

魅力的な製品ではありますが、MiniPCメーカーはMSIやASUSなどを除いてUEFI BIOSのアップデートには消極的です。例えばRyzenなどのAMD系CPUでは、パフォーマンスやセキュリティ関連の問題が定期的に発見されており、AGESAの定期的な更新は避けられません。

また放熱性能や製造しているメーカーが想定している用途に応じて利用するのが大切かなと思いますので、外部に提供するサービスをこのクラスターで稼動させることはないと思います。

もしいま実用的なKubernetesクラスターをできるだけ安く準備する必要があれば、ワークロードにもよりますが、中古のPC4 RDIMM ECCメモリを128GB以上搭載した10〜12コア程度のXeonサーバーを5台以上準備すると思います。


以上