Raspberry Pi で Kubernetes を使う場合に知っておくと役立つ情報を紹介したいと思います.
使用機材
今だと,普通に使うなら使用機材は次の二択となります.
- Raspberry Pi 4 Model B
- Raspberry Pi Compute Module 4
メモリ
4GB 以上がオススメです.
メモリは 2GB でも動きはすると思いますが,Python プログラムを動かす Pod を1つ配置しているノードでも,下記のように 2GB 近く消費するためです.
1 2 3 |
$ kubectl top node rasp-cooler-1 NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% rasp-cooler-1 436m 10% 1814Mi 23% |
沢山 Pod を配置しているから,と思われるかもですが動いている Pod はこんな感じで,必要最低限です.
1 2 3 4 5 6 7 8 9 10 |
$ kubectl describe -n hems nodes rasp-cooler-1 (snip) Non-terminated Pods: (5 in total) Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age --------- ---- ------------ ---------- --------------- ------------- --- hems unit-cooler-actuator-d4946b687-kpvpc 0 (0%) 0 (0%) 256Mi (3%) 512Mi (6%) 135m kube-system calico-node-7dxp9 250m (6%) 0 (0%) 0 (0%) 0 (0%) 18d kube-system kube-proxy-l4gz4 0 (0%) 0 (0%) 0 (0%) 0 (0%) 18d monitor node-exporter-79nkn 0 (0%) 0 (0%) 0 (0%) 0 (0%) 18d monitor promtail-s4wzk 0 (0%) 0 (0%) 0 (0%) 0 (0%) 14d |
「unit-cooler-actuator-*」は IO-Link 通信および GPIO 制御を行う Python プログラムを動かしているものです.「node-exporter-*」と「promtail-*」はそれぞれ Prometheus によるシステム監視と Promtail によるログ収集を行うものです.無くても良いとは思いますが,実用性を考えると配置したくなると思います.
SD カード容量
必要用容量の観点から 16GB 以上の容量を準備するのがオススメです.
8GB でもなんとか動かせるのですが,実際に 8GB で運用すると下記のようにディスクの使用量が 95% を越えてくるので(/dev/mmcblk0p2 の行),結構厳しいです.ノードの DiskPressure
が True になって,Pod が意図通りに配置されなくなることが起りがちです.
1 2 3 4 5 6 7 8 9 |
$ df Filesystem 1K-blocks Used Available Use% Mounted on tmpfs 188928 23136 165792 13% /run /dev/mmcblk0p2 7176488 6575456 258352 97% / tmpfs 944628 0 944628 0% /dev/shm tmpfs 5120 0 5120 0% /run/lock tmpfs 944628 8 944620 1% /tmp tmpfs 944628 47716 896912 6% /var/log /dev/mmcblk0p1 258095 151391 106704 59% /boot/firmware |
小さい容量のデバイスを使う場合の対処
しかし,少ない容量のデバイスを使わざるを得ない場合もあると思います.その場合,containerd の設定ファイル /etc/containerd/config.toml
において, discard_unpacked_layers=true
を指定すると,ディスク消費を大きく抑制することができます.
1 2 3 4 5 |
[plugins."io.containerd.grpc.v1.cri".containerd] default_runtime_name = "runc" disable_snapshot_annotations = true - discard_unpacked_layers = false + discard_unpacked_layers = true |
また,問題の根本解決にはなりませんが,/var/lib/kubelet/config.yaml
の末尾に下記を追記することで,DiskPressure の閾値を変更するのも助けになるかもしれません.
1 2 3 |
evictionHard: imagefs.available: "1%" nodefs.available: "1%" |
寿命の観点
これは余談です.実用性という面では SD カードの寿命も気になると思います.
実使用における SD カードの寿命を見積もるのは色々と難しいのですが,参考になりそうな資料として下記があります.
この資料の p.10 には 8GB の SD カードに対してどのくらい書き込みを行うとエラーが発生したか示されています.メーカや書き込みパターンによって幅があるものの,ざっくり20~90TB 程度で寿命に達するようです.
一方,約1年 Kubernetes のノードとして運用したノードの書き込み量を調べてみたところ,ざっくり 30GB 程度でした.
1 2 |
$ sudo dumpe2fs -h /dev/mmcblk0p2 | grep Life Lifetime writes: 29 GB |
従って,寿命観点では 8GB でも問題はなさそうです.
また,効果のほどは確認できていませんが,/etc/sysctl.conf
に下記のように設定して,書き込み頻度やを押さえ手おくと良いかもしれないです.
1 2 3 4 |
# フラッシュする間隔 (30分) vm.dirty_writeback_centisecs = 180000 # フラッシュされるまでの最大遅延 (1時間) vm.dirty_expire_centisecs = 360000 |
定性的には,WAF(Write Amplification Factor) を下げられると思われるためです.
ちなみに,eMMC 容量が 8GB の Raspberry Pi Compute Module 4 で,上記の設定の元で 1年運用した後でも,Pre-end of life information(EXT_CSD_PRE_EOL_INFO
) は「0x01: 定常状態(問題無し)」でした.
1 2 3 4 |
$ sudo mmc extcsd read /dev/mmcblk0 eMMC Life Time Estimation A [EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_A]: 0x01 eMMC Life Time Estimation B [EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_B]: 0x01 eMMC Pre EOL information [EXT_CSD_PRE_EOL_INFO]: 0x01 |
マルチプラットフォームイメージ
慣れてくると,Raspberry Pi に配置するイメージを,普段使っている PC でビルドしたくなってくると思います.そんなとき便利なのが buildx です.これを使うと,複数のアーキテクチャで動作する,イメージを簡単に作成できます.
buildx の使い方
導入は簡単で,下記のようにするだけです.
1 2 3 4 |
$ sudo apt install -y qemu qemu-user-static binfmt-support debootstrap $ mkdir -p ~/.docker/cli-plugins/ $ curl -Lo ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.10.5/buildx-v0.10.5.linux-amd64 $ chmod +x ~/.docker/cli-plugins/docker-buildx |
あとは,次のようにすれば,マルチプラットフォームイメージをビルドして,レジストリに登録できます.NAME
や REGISTRY
は適当に置き換えてください.
1 2 3 4 5 6 7 8 9 10 |
BUILDER=arm_builder NAME=outdoor_unit_cooler REGISTRY=registry.green-rabbit.net:5000/kimata docker buildx create --name ${BUILDER} --node ${BUILDER}0 --use docker buildx use arm_builder docker buildx build --platform linux/amd64,linux/arm64/v8 \ --cache-from type=registry,ref=${REGISTRY}/${NAME}:cache \ --cache-to type=registry,ref=${REGISTRY}/${NAME}:cache \ --push --tag ${REGISTRY}/${NAME} . |
レジストリがプライベートCA のルート証明書使っていて TLS 周りのトラブルが出る場合は,次の内容で buildkitd.toml
というファイルを作成し,「docker buildx build」する際に --config buildkitd.toml
オプションを付与すればエラーが出なくなると思います.
1 2 3 |
[registry."registry.green-rabbit.net:5000"] http = true insecure = true |
GitLab CI とかで,イメージを自動的に作りたい場合,次のような Dockerfile を作って,ビルド用のイメージを用意しておくと良いと思います.
1 2 3 4 5 6 7 8 9 10 11 |
FROM ubuntu:22.04 RUN apt-get update && apt-get install -y \ docker.io git curl \ qemu qemu-user-static binfmt-support debootstrap \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN mkdir -p ~/.docker/cli-plugins/ RUN curl -Lo ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.10.5/buildx-v0.10.5.linux-amd64 RUN chmod +x ~/.docker/cli-plugins/docker-buildx |
プラットフォーム毎の Dockerfile カスタマイズ
マルチプラットフォーム対応のイメージが作れるようになると,次にプラットフォーム毎に Dockerfile の内容を切り替えたくなってくると思います.例えば,Rapsberry Pi で動かすイメージには python3-rpi.gpi
をインストールしたいけど,それ以外ではインストールしたくない,とか.
こういった場合に使えるのが TARGETPLATFORM
です.Dockerfile にARG TARGETPLATFORM
と記述すると,ビルドしているアーキテークチャの種類が $TARGETPLATFORM
で参照できるようになります.
例えば,先ほどの python3-rpi.gpi
のケースは Dockerfile に次のようにして対応できます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
FROM ubuntu:22.04 ARG TARGETPLATFORM ENV TZ=Asia/Tokyo ENV DEBIAN_FRONTEND=noninteractive RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then GPIO_LIB="python3-rpi.gpio"; fi; \ apt-get update && apt-get install -y \ language-pack-ja \ python3 python3-pip \ ${GPIO_LIB} \ (snip) && apt-get clean \ && rm -rf /va/rlib/apt/lists/* |
GPIO,SPI通信,UART通信
Raspberry Pi を使うと言うことは,各種 I/O を使ってなにか制御したいケースが多いと思います.そういった場合は,ホスト側の次のデバイスファイルを Pod にマウントしてやれば OK です.
- /dev/gpiomem
- /dev/spidev0.0
- /dev/ttyAMA0
具体的には,Kubernetes 向けの Deployment の設定ファイルに次のように記載します.「privileged: true」がないとパーミッションエラーになります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
apiVersion: apps/v1 kind: Deployment metadata: name: unit-cooler-actuator namespace: hems labels: app: unit-cooler-actuator spec: replicas: 1 selector: matchLabels: app: unit-cooler-actuator template: metadata: labels: app: unit-cooler-actuator spec: volumes: - name: dev-gpio hostPath: path: /dev/gpiomem - name: dev-spidev hostPath: path: /dev/spidev0.0 - name: dev-tty hostPath: path: /dev/ttyAMA0 containers: - name: unit-cooler-actuator (snip) volumeMounts: - mountPath: /dev/gpiomem name: dev-gpio - mountPath: /dev/spidev0.0 name: dev-spidev - mountPath: /dev/ttyAMA0 name: dev-tty securityContext: privileged: true |
ログ収集
せっかく Kubernetes を導入するので,問題発生時も Raspberry Pi にログインせずに,動作ログ等を確認できるようにしておくと便利です.私は,Promtail + Grafana Loki の組み合わせを使っていますが,何でも良いと思います.
Promtail では,下記のような設定を使っています.この設定では,namespace が bot,panel,hems,sensor のいずれかの Pod のログを http://proxy.green-rabbit.net:3100/loki/api/v1/push
に送り付けるようになっています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
apiVersion: v1 kind: ConfigMap metadata: labels: app: promtail name: promtail-config namespace: monitor data: config.yml: | clients: - url: http://proxy.green-rabbit.net:3100/loki/api/v1/push backoff_config: max_period: 10m max_retries: 100 min_period: 500ms batchsize: 1048576 batchwait: 15s timeout: 5s positions: filename: /tmp/positions.yml scrape_configs: - job_name: kubernetes-pod pipeline_stages: - regex: expression: "^(?s)(?P<time>\\\\S+?) (?P<stream>stdout|stderr) (?P<flags>\\\\S+?) (?P<service>\\\\S+?) \\\\d{4}-\\\\d{2}-\\\\d{2} \\\\d{2}:\\\\d{2}:\\\\d{2} (?P<level>DEBUG|INFO|ERROR|WARNING) (?P<content>.*)$" - labels: stream: service: level: - timestamp: source: time format: RFC3339Nano - output: source: content static_configs: - targets: - localhost labels: job: kubernetes-pod node_name: ${NODE_NAME} __path__: /var/log/pods/{bot,panel,hems,sensor}*/*/*.log --- kind: DaemonSet apiVersion: apps/v1 metadata: name: promtail namespace: monitor labels: app: promtail spec: selector: matchLabels: app: promtail template: metadata: labels: app: promtail spec: volumes: - name: config configMap: name: promtail-config - name: log hostPath: path: /var/log/pods containers: - name: promtail image: grafana/promtail:2.6.1 args: - '-config.file=/etc/promtail/config.yml' - '-config.expand-env' volumeMounts: - name: config mountPath: /etc/promtail - name: log mountPath: /var/log/pods env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName dnsPolicy: Default affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role.kubernetes.io/server operator: Exists - matchExpressions: - key: node-role.kubernetes.io/sensor operator: Exists - matchExpressions: - key: node-role.kubernetes.io/hems operator: Exists |
この設定さえしておけば,何か問題が起こった際は,下記のように直感的な画面を使ってささっとログが辿れます.
コメント