k8s on RaspberryPiでHAクラスタ構築
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 | ノード監視・レプリカ監視・エンドポイント監視・サービスアカウント発行など、いろいろなプロセスの集合体 |
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
で設定するとし、以下のような構成で構築しました。
keepalived
はHAProxy
に比べて簡易的ですが、ノード自体にセカンダリなIPが付与されるため、純粋な分かりやすさや、管理が簡単というメリットがあります。
keepalivedによってcontrollerノードにVIPを設定し、このVIP宛にクライアントがAPIリクエストを送信/api-serverが受信できるようにします。
ansibleによるk8s構築について
ansibleでkubernetesを構築する発想は珍しくないようで、ansibleでk8sを操作するための拡張機能(openshift
に含まれる)や、Kubespray
などの製品があるみたいです。
以上より、本プロジェクトを一般的に使えるようにする意義は薄く、車輪の再発明であると判断しました。(無念...)
そのため、本記事では制作したplaybookの解説というよりは、そもそものk8s構築自体についてや、playbookを書く際に注意した点などを主に記載していきます。
構築資材について
参考までに今回構築するために作成したansible playbookは以下になります。(cloneして利用するといった具合の資材ではありません)
ansibleディレクトリ構成
基本はansibleのベストプラクティスを参考に、以下のようなディレクトリ構成を取りました。
実際はgitlab
やbind9
といったコンポーネントを含むノード(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サーバ等の設定は省略しています。
- keepalivedの設定
- 証明書の作成
- カーネル機能の設定変更
- CRI(docker)をインストール
- kubeadm,kubelet,kubectlをインストール
- kubeadmでノードをクラスタに参加
0. keepalivedの設定
k8sのセットアップ前にkeepalivedを設定しました。カーネルパラメータを一部設定する必要があるので注意が必要です。
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
を採用しました。
証明書の発行方法も上記で説明されている通りで、本環境における証明書の作成については、ansibleの実行前に実施するスクリプトという体で展開しています。
server-csr.json
)について
APIサーバの設定(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.crt
、apiserver.key
、ca.crt
、ca.key
をkubeadm
を実施する前に、ノードのディレクトリに配備しておきます。
2. カーネル機能の設定変更
Linuxノードのiptablesがブリッジを通過するトラフィックを正確に処理する要件として、net.bridge.bridge-nf-call-iptablesをsysctlの設定ファイルで1に設定してください。
k8sの要件としてnet.bridge.bridge-nf-call-iptables
とnet.bridge.bridge-nf-call-ip6tables
を設定します。タイミングは特に問われないため、playbook中では初めに設定しています。
3. CRI(docker)をインストール
k8sでPodを動作させるため、すべてのノード上CRI(Container Runtime Interface)をインストールします。
gitlab-runnerが動作するノードに関しては、CRIにDocker
を採用しました。
UbuntuでDockerを動作させる際cgroupsdriver
をsystemd
にすること、storage-driver
にoverlay2
を用いることが推奨されるため、以下のような設定をすべてのノードに入れた上で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の設定も行ってみました。
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
4. kubeadm,kubelet,kubectlをインストール
kubernetes.ioのインストール方法に準じます。
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
を採用しました。
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_VXLAN
にAlways
を指定し、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基盤を使っていく中で、引き続き理解を深めていきたいです。
参考
-
Ubuntu20.04上でリポジトリから取得したansibleでは、service_factsが利用できない
https://github.com/VSChina/vscode-ansible/issues/265 ↩︎ -
Dockerもcontainerd同様runcで動いているため、動作しなくなるといった問題はない
https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/ ↩︎
Discussion
補則事項
記事に含めるまでもない…かも?なことを書いていきます