🥧

k8s on RaspberryPiでHAクラスタ構築

commits16 min read 1

RaspberryPiでkubernetes(以下k8s)基盤、HA(High-Availability 高可用性)構成でクラスタを構築しました。また、今後の管理を楽にするためにansibleを用いて作成しました。

High-Availability構成とは

k8sにおいて、controller(master)ノードを複数設定することで、ノードがダウンした場合にも整合性を保ちつつ稼働を止めない、いわゆる冗長化を行うことができます。

HA構成で冗長化されるコンポーネント

k8sの機能としてのHA構築では、以下がHA化します。

コンポーネント 機能
kube-apiserver kubectlなどを受信しk8sを制御するRESTサーバ
kube-scheduler Podの監視を行い、どのノードでPodをデプロイするか決める機能
etcd k8s上のすべてのクラスタの情報を保存しているキーバリューストア(DB)
kube-controller-manager ノード監視・レプリカ監視・エンドポイント監視・サービスアカウント発行など、いろいろなプロセスの集合体

https://kubernetes.io/ja/docs/concepts/overview/components/

kube-apiserverの冗長化

kubeの機能におけるエンドポイントへのアクセスについては冗長化対象外となっています。

kube-apiserver自体は冗長化されますが、kubectlでアクセスする先はconfig記載のアドレス対象になるため、configを発行したノードが潰れるとアクセスできないという事態が発生します。

このため、kubeadmとは別でエンドポイントを用意し、エンドポイントへのRESTをkube-apiserverが受信できるような構成が必要になります。

概要

作業環境

AnsibleはDockerコンテナで動作させました。

  • 実行ホスト: Docker desktop for Mac: Ubuntu 20.04
  • Ansible version: 2.11.5
  • サーバーOS: Ubuntu 20.04.3 LTS (RaspberryPi 8台)

構成図

LB用にノードを確保できる環境ではHAProxyがよく用いられますが、今回はエンドポイントをkeepalivedで設定するとし、以下のような構成で構築しました。

keepalivedHAProxyに比べて簡易的ですが、ノード自体にセカンダリなIPが付与されるため、純粋な分かりやすさや、管理が簡単というメリットがあります。

keepalivedによってcontrollerノードにVIPを設定し、このVIP宛にクライアントがAPIリクエストを送信/api-serverが受信できるようにします。

ansibleによるk8s構築について

ansibleでkubernetesを構築する発想は珍しくないようで、ansibleでk8sを操作するための拡張機能(openshiftに含まれる)や、Kubesprayなどの製品があるみたいです。

https://docs.ansible.com/ansible/latest/collections/community/kubernetes/k8s_module.html

https://kubespray.io/#/

以上より、本プロジェクトを一般的に使えるようにする意義は薄く、車輪の再発明であると判断しました。(無念...)

そのため、本記事では制作したplaybookの解説というよりは、そもそものk8s構築自体についてや、playbookを書く際に注意した点などを主に記載していきます。

構築資材について

参考までに今回構築するために作成したansible playbookは以下になります。(cloneして利用するといった具合の資材ではありません)

https://github.com/nkte8/labo/tree/2021-10-10-r01/ansible

ansibleディレクトリ構成

基本はansibleのベストプラクティスを参考に、以下のようなディレクトリ構成を取りました。

実際はgitlabbind9といったコンポーネントを含むノード(infra)や、ストレージバックエンドを設定するためのplaybookが含まれます。

ansible実行方法

ansible-playbookさえ実行できれば良いため、コンテナで実施します。
ansibleは最新版を利用したい[1]ため、pipを用いてインストールしました。

FROM ubuntu:20.04

RUN apt-get update && \
apt-get install -y openssh-client golang-cfssl python3 python3-pip && \
apt-get clean

RUN pip3 install --upgrade pip && \
pip3 install "ansible"

RUN mkdir /root/ansible
WORKDIR /root/ansible

EXPOSE 22

/root/ansibleに作業ディレクトリ、ホスト同様の.sshディレクトリのマウントを行うことで、コンテナ内からsshできるようにしています。

cd ./ansible
docker run --rm -v ~/.ssh:/root/.ssh -v ${PWD}:/root/ansible \ 
    -it ansible ansible-playbook ./site.yaml -i ./hosts.yaml

構築方法

作業の流れ

gitlabやストレージバックエンド、DNSサーバ等の設定は省略しています。

  1. keepalivedの設定
  2. 証明書の作成
  3. カーネル機能の設定変更
  4. CRI(docker)をインストール
  5. kubeadm,kubelet,kubectlをインストール
  6. kubeadmでノードをクラスタに参加

0. keepalivedの設定

k8sのセットアップ前にkeepalivedを設定しました。カーネルパラメータを一部設定する必要があるので注意が必要です。

https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html/load_balancer_administration/s1-initial-setup-forwarding-vsa

ansible上ではsysctlモジュールを用いて設定します。

- name: sysctl net.ipv4.ip_nonlocal_bind
  sysctl:
    name: net.ipv4.ip_nonlocal_bind
    value: "1"
    state: present
    sysctl_file: /etc/sysctl.d/k8s.conf
- name: sysctl net.ipv4.ip_forward
  sysctl:
    name: net.ipv4.ip_forward
    value: "1"
    state: present
    sysctl_file: /etc/sysctl.d/k8s.conf

1. 証明書の作成

k8sの各通信はTLS暗号化されています。kubeadmでは各種証明書を自動発行、または証明書を手動作成してセットアップが可能です。

実運用を考える場合は、信頼機関から発行されるのが想定されるルート証明書と、エンドポイントであるapi-serverの証明書が必要十分条件になっています。

今回はJsonとして設定ファイルとして情報を残せることから、発行にcfsslを採用しました。

https://kubernetes.io/ja/docs/concepts/cluster-administration/certificates/#cfssl

証明書の発行方法も上記で説明されている通りで、本環境における証明書の作成については、ansibleの実行前に実施するスクリプトという体で展開しています。

APIサーバの設定(server-csr.json)について

APIサーバ向けの証明書にはSAN(Subject Alternative Name)が含まれますが、ロードバランサを使う場合、ロードバランサのIPアドレスやDNS名を含めないと認証エラーになるため、これを含めた設定を作成します。

今回はVIPに対して名前はつけていないため登録していませんが、kube-apiserverにDNS名前解決を行いたい場合は、hostsの中に名前を追加してください。

{
    "CN": "kubernetes",
    "hosts": [
        "127.0.0.1",
        "10.96.0.1", // kubernetesサービスの最初のIPアドレス
        "192.168.3.10", // ← keepaliveで払い出されるVIP
        "192.168.3.11", // ← masterノードがmaster自身にアクセスするために必須
        "192.168.3.12",
        "192.168.3.13",
        "master01", // 名前解決する場合は記述
        "master02",
        "master03",
        "kubernetes", // 以下はkube-dnsが参照する内容のため、編集禁止
        "kubernetes.default",
        "kubernetes.default.svc",
        "kubernetes.default.svc.cluster",
        "kubernetes.default.svc.cluster.local"
    ],
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "JP",
            "L": "Tokyo"
        }
    ]
}

cfsslコマンドで生成されるファイルのうちapiserver.crtapiserver.keyca.crtca.keykubeadmを実施する前に、ノードのディレクトリに配備しておきます。

2. カーネル機能の設定変更

Linuxノードのiptablesがブリッジを通過するトラフィックを正確に処理する要件として、net.bridge.bridge-nf-call-iptablesをsysctlの設定ファイルで1に設定してください。

https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#iptablesがブリッジを通過するトラフィックを処理できるようにする

k8sの要件としてnet.bridge.bridge-nf-call-iptablesnet.bridge.bridge-nf-call-ip6tablesを設定します。タイミングは特に問われないため、playbook中では初めに設定しています。

3. CRI(docker)をインストール

k8sでPodを動作させるため、すべてのノード上CRI(Container Runtime Interface)をインストールします。

https://v1-21.docs.kubernetes.io/docs/concepts/overview/components/#container-runtime

gitlab-runnerが動作するノードに関しては、CRIにDockerを採用しました。

UbuntuでDockerを動作させる際cgroupsdriversystemdにすること、storage-driveroverlay2を用いることが推奨されるため、以下のような設定をすべてのノードに入れた上でdocker-daemonを起動しておきます。

{
    "exec-opts": [
        "native.cgroupdriver=systemd"
    ],
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "100m"
    },
    "storage-driver": "overlay2",
}

insecure-registryを使用する場合は上記に"insecure-registries"プロパティを追加し、カンマ区切りでエントリを追加します。

{
  // ...省略
    "insecure-registries": [
        "registry.neko.lab:5005"
    ]
}

備考: containerdを利用する場合

Dockerである必要のないノードで、containerdの設定も行ってみました。

https://v1-21.docs.kubernetes.io/ja/docs/setup/production-environment/container-runtimes/#containerd

containerd config defaultで作成できるデフォルト設定に加え、insecure-registryを設定する場合、/etc/containerd/config.tomlに以下のような設定が必要です。

### ...省略
    [plugins."io.containerd.grpc.v1.cri".registry]
      [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
        [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
          endpoint = ["https://registry-1.docker.io"]
    ### エンドポイントを記載
        [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.neko.lab:5005"]
          endpoint = ["http://registry.neko.lab:5005"]
    ### insecureであることを明記
      [plugins."io.containerd.grpc.v1.cri".registry.configs]
        [plugins."io.containerd.grpc.v1.cri".registry.configs."registry.neko.lab:5005".tls]
          insecure_skip_verify = true

https://github.com/containerd/containerd/blob/main/docs/cri/registry.md

4. kubeadm,kubelet,kubectlをインストール

kubernetes.ioのインストール方法に準じます。

https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#kubeadm-kubelet-kubectlのインストール

ansibleではapt_keyモジュールやapt_repositoryモジュールを用いてキー追加・リポジトリ追加を行ってインストールします。

    - name: add kubernetes gpg-key
      apt_key:
        url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
        state: present
    - name: add kubernetes-xenial repository
      apt_repository:
        repo: deb https://apt.kubernetes.io/ kubernetes-xenial main
        state: present
        filename: kubernetes
...
    - name: install k8s compornent
      apt:
        update_cache: yes
        name: "{{ item }}=1.21.4-00"
      with_items: "{{ k8s_compornent }}"

5. kubeadmでノードをクラスタに参加

kubeadm initについて

kubeadmでは、初期化時にconfigを設定することができます。HAクラスタ構築の際には次のような設定が必要になります。

apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: v1.21.4
controlPlaneEndpoint: "192.168.3.10:6443" # loadbalancer
apiServer:
  certSANs:
    - "192.168.3.10"
    - "192.168.3.11"
    - "192.168.3.12"
    - "192.168.3.13"
    - "master01"
    - "master02"
    - "master03"
  networking:
    podSubnet: 10.244.0.0/16

controlPlaneEndpointに指定したエンドポイントがkube-apiserverのアクセス先に設定されます。
また、certSANsにはエンドポイントからのアクセスで認証されるSANs情報を登録します。

podSubnetはPod間通信で使用されるセグメントを指定します。今回はデフォルトの10.244.0.0/16を採用しました。

CNIの登録

Project Calicoの利用

ノードを追加する前にCNIを設定する必要があります。今回はCalicoを採用しました。

https://docs.projectcalico.org/getting-started/kubernetes/self-managed-onprem/onpremises

CalicoはBGPモード(デフォルト)とオーバーレイネットワークで構成されるVXlanモードから選択できます。

VXlanはflannel互換、BGPモードは環境を選びますが、純粋なL3ネットワークとして扱えるためシンプルな構築が可能です。

設定ファイルの修正

構築に利用するcalico.yamlで、calico-nodeコンテナ内に適応されるCALICO_IPV4POOL_CIDRを、先ほどkubeadm設定ファイルで設定したpodSubnetの値と合わせます。(calico側のデフォルトは192.168.0.0/16になっています。)

また、VXlanモードを使う場合はCALICO_IPV4POOL_VXLANAlwaysを指定し、healthcheckからbirdを除外します。

kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: calico-node
  namespace: kube-system
spec:
  template:
    spec:
      containers:
        - name: calico-node
          env:
            - name: CALICO_IPV4POOL_CIDR
              value: "10.244.0.0/16"
# ... VXlanモードを使う場合は以下を設定
            - name: CALICO_IPV4POOL_IPIP
              value: "Never"
            - name: CALICO_IPV4POOL_VXLAN
              value: "Always"
# ...省略
          livenessProbe:
            exec:
              command:
                - /bin/calico-node
                - -felix-live
                # - -bird-ready  ## コメントアウト
          readinessProbe:
            exec:
              command:
                - /bin/calico-node
                - -felix-ready
                # - -bird-ready  ## コメントアウト

kubeadm joinについて

CNIを設定したら、k8sにノードを追加していきます。

ansibleで実行する際には、kubeadm joinが並行実施しないように注意する必要があります。次のようにthrottle: 1オプションを付与することで、タスク単位で並行処理を制限することが可能です。

    - block:
        - name: kubeadm join other master
          throttle: 1
          shell: |-
            {{ hostvars[first_master].master_join }} && \
            sleep 100s
      when:
        - inventory_hostname != first_master
        - "'master' in group_names"
    - block:
        - name: kubeadm join by all worker
          throttle: 1
          shell: |-
            {{ hostvars[first_master].worker_join }} && \
            sleep 100s
      when:
        - "'worker' in group_names"

(ホスト変数のmaster_joinおよび、worker_joinには、kubeadm token createなどで発行された`kubeadm join"コマンドが格納されています。)

構築後の確認

以上でクラスタが構築されました。kube-apiserverのエンドポイントが冗長化され、ノードが停止した場合も操作できるかを確認します。

エンドポイントへのアクセスの確認

controller(master)ノードに存在する/etc/kubernetes/admin.confを取得し、~/.kube/configに保存します。

中身を参照すると、アクセス先がエンドポイントになっていることがわかります。

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: XXXXXX..... #...(省略)
    server: https://192.168.3.10:6443
  name: kubernetes
contexts:
- context:
# ...(省略)

configが使用可能かを判断するために、kubectlをパッケージマネージャーからインストールするか、以下のようなkubectl実施用のコンテナを作成します。以下はRaspberryPiのアーキテクチャに合わせたDockerfileになります。

FROM ubuntu:20.04

RUN apt-get update && \
apt-get install -y wget && \
apt-get clean

## URLは https://storage.googleapis.com/kubernetes-release/release/<kube version>/bin/<OS>/<archtecture>/kubectl なので、実施するクライアントにより変更してください。
RUN wget https://storage.googleapis.com/kubernetes-release/release/v1.21.4/bin/linux/arm64/kubectl && \
mv kubectl /usr/local/bin && \
chmod +x /usr/local/bin/kubectl

EXPOSE 6443

kubectl versionコマンドをコンテナ越しに実施し、Server Versionが取得できれば疎通が可能です。

~ $ sudo docker run --rm -v /root/.kube:/root/.kube -it registry.neko.lab:5005/root/labo/kubectl kubectl version
Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.4", GitCommit:"3cce4a82b44f032d0cd1a1790e6d2f5a55d20aae", GitTreeState:"clean", BuildDate:"2021-08-11T18:16:05Z", GoVersion:"go1.16.7", Compiler:"gc", Platform:"linux/arm64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.4", GitCommit:"3cce4a82b44f032d0cd1a1790e6d2f5a55d20aae", GitTreeState:"clean", BuildDate:"2021-08-11T18:10:22Z", GoVersion:"go1.16.7", Compiler:"gc", Platform:"linux/arm64"}

コンテナで作った場合はalias kubectl="sudo docker run --rm -v /root/.kube:/root/.kube -v <作業ディレクトリ>:/workdir -it <コンテナ名> kubectl"などとしbashに登録しておけば、通常のkubectlのように扱うことができます。

冗長化の確認

keepalivedによってVIPが付与されているアドレスを確認します。

ubuntu@master01:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether dc:a6:32:8e:f7:b0 brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.11/24 brd 192.168.3.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 192.168.3.10/24 scope global secondary eth0
       valid_lft forever preferred_lft forever
    inet6 2409:11:2180:f00:dea6:32ff:fe8e:f7b0/64 scope global mngtmpaddr noprefixroute
       valid_lft forever preferred_lft forever
    inet6 fe80::dea6:32ff:fe8e:f7b0/64 scope link
       valid_lft forever preferred_lft forever

VIPが移動するかを確認します。master01をシャットダウンし、kubectl get nodeを実施します。

nkte8@Nekobook ~ % kubectl get node
NAME       STATUS     ROLES                  AGE   VERSION
master01   NotReady   control-plane,master   43m   v1.21.4
master02   Ready      control-plane,master   41m   v1.21.4
master03   Ready      control-plane,master   38m   v1.21.4
node01     Ready      <none>                 34m   v1.21.4
node02     Ready      <none>                 32m   v1.21.4
node03     Ready      <none>                 29m   v1.21.4
node04     Ready      <none>                 36m   v1.21.4

master01がダウン中もkube-apiserverにアクセスが可能であることが確認できます。

今後の課題

今回、k8sクラスタの構築をansibleで実施しました。ansibleの学習や、kubeadmの設定内容、apiserverの冗長化や証明書の仕組みなど、本記事では記述し切れていませんが多くの学びがありました。

一方、完成させた物を作るにあたり、いくらかやり残したこともあり、今後の課題として残されています。

  • ansible観点
    • best practiceの遵守
    • shell部分のモジュール化
    • glusterfs・gitlabの構築自動化
  • kubernetes観点
    • dockerのCRI利用非推奨化に伴うcontainerdへの移行[2]
    • CNIの有効利用(Podへの直接通信)
    • etcdの外出し・バックアップの仕組みの作成

今後k8s基盤を使っていく中で、引き続き理解を深めていきたいです。

kubernetes on raspberrypi

参考

https://kubernetes.io/ja/docs/home/
https://docs.ansible.com/ansible/2.9_ja/user_guide/playbooks_best_practices.html
https://developers.cyberagent.co.jp/blog/archives/3132/
https://docs.projectcalico.org/about/about-calico
脚注
  1. Ubuntu20.04上でリポジトリから取得したansibleでは、service_factsが利用できない
    https://github.com/VSChina/vscode-ansible/issues/265 ↩︎

  2. Dockerもcontainerd同様runcで動いているため、動作しなくなるといった問題はない
    https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/ ↩︎

GitHubで編集を提案

Discussion

補則事項

記事に含めるまでもない…かも?なことを書いていきます

  • RaspberryPiである理由
    • ちいさくてかわいい
    • 数を揃えやすい...
  • 電源問題
    • PoEで一応5V3A対応してる
    • basspower bootのHDD1枚が限界
  • 写真に7台しか写ってない
    • wifi越しに別の場所で動いてる(4B 8GB)
    • 監視カメラ用なので3A+に乗り換えたい
ログインするとコメントできます