オンプレでもk8sを楽しみたい！とりあえず(ちょっと欲張りな)k8sクラスタを立てる

最近kubernetesを触りたい欲が上がってきていますが、試せる環境がありません！！
~~（クラウド？GKE？クラウド破産怖い！）~~
ということでオンプレk8sを構築しよう！となったのですが、なかなか試す気力がなく、連休のタイミングで頑張りました。
とりあえず立てたいだけでしたが、せっかくなのでちょっと欲張ってみました。

**2022/9/26更新**
@superbrothers さんのコメントをもとに一部修正、加筆しました。
（コメントありがとうございます！）


# TL;DR (にしたかったけど長すぎる。)

今回は下記のようなことをしています。
- コントロールプレーンをHA（高可用性）構成、HAProxyを使用
- ポッド間通信(CNI)はCalicoを使用
- MetalLBでLoadBalancerに対応
- Horizontal Pod Autoscaler対応のためにMetrics serverを導入
- Ubuntu 20.04（Ubuntu 22.04はそのうちリベンジします。~~Ubuntu 22.04は辛かった…~~）

プロキシx1、コントロールプレーンノードx3、ワーカーノードx3の計7台分の仮想マシンを使用しています。


## プロキシ用
```
# HAProxyのインストール
sudo apt install -y haproxy

# HAProxyの設定
# IPアドレス(192.168.0.21など)はコントロールプレーンノードのIPアドレスに合わせてください。
sudo mv /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.old
cat << EOF | sudo tee /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    log /dev/log local1 notice
    daemon

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 1
    timeout http-request    10s
    timeout queue           20s
    timeout connect         5s
    timeout client          60m
    timeout server          60m
    timeout http-keep-alive 10s
    timeout check           10s

frontend apiserver
    bind *:6443
    mode tcp
    option tcplog
    default_backend apiserver

backend apiserver
    option httpchk GET /healthz
    http-check expect status 200
    mode tcp
    option ssl-hello-chk
    balance     roundrobin
        server kubec1 192.168.0.21:6443 check
        server kubec2 192.168.0.22:6443 check
        server kubec3 192.168.0.23:6443 check
EOF

# 設定反映
sudo systemctl restart haproxy
```


## コントロールプレーンノード/ワーカーノード共通(6台全てで行う)
```
# host名を変更(今回はコントロールプレーンの1 -> c1)
sudo hostnamectl set-hostname kubec1


# iptablesのための設定
cat <<EOF | sudo tee -a /etc/modules
br_netfilter
EOF
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system


# nftablesを使用しないように設定
sudo apt-get install -y iptables arptables ebtables
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
sudo update-alternatives --set arptables /usr/sbin/arptables-legacy
sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy


# Containerdのインストール
sudo apt install -y containerd


# kubernetesパッケージのインストール
sudo apt-get update && sudo apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
```


## 1台目のコントロールプレーンノード
```
# 初めのコントロールプレーンを作成
# pod-network-cidrのアドレスは使用していない範囲でかつ/16の範囲で指定、
# control-plane-endpointのアドレスはプロキシサーバのIPアドレスを指定する。
sudo kubeadm init --pod-network-cidr=10.20.0.0/16 --upload-certs --control-plane-endpoint "192.168.0.11:6443"

# 最後に下記の様なものが表示されるのでコピーしておく。
# You can now join any number of the control-plane node running the following command on each as root:
#
#   kubeadm join 192.168.0.11:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
#         --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
#         --control-plane --certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#
# 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 192.168.0.11:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
#         --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 


# kubectlのコンフィグを用意する
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# Calicoをインストール
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.24.1/manifests/tigera-operator.yaml

# 設定ファイルをダウンロードして修正する
# cidrのIPアドレスを--pod-network-cidrで指定したものと同じにする
wget https://raw.githubusercontent.com/projectcalico/calico/v3.24.1/manifests/custom-resources.yaml
vi custom-resources.yaml
# ...
# apiVersion: operator.tigera.io/v1
# kind: Installation
# metadata:
#   name: default
# spec:
#   # Configures Calico networking.
#   calicoNetwork:
#     # Note: The ipPools section cannot be modified post-install.
#     ipPools:
#     - blockSize: 26
#       cidr: 10.20.0.0/16
#       encapsulation: VXLANCrossSubnet
#       natOutgoing: Enabled
#       nodeSelector: all()
# ...

# 設定を反映する
kubectl create -f custom-resources.yaml


```


## 2台目以降のコントロールプレーンノード
2台目以降のコントロールプレーンの追加は上記で`kubeadm init`してから2時間以内に行ってください。
それ以上時間が経ってしまった場合は1台目のコントロールプレーンで`kubeadm init phase upload-certs --upload-certs`を実行し、表示されるIDを--certificate-keyに指定して行ってください。

```
# 先ほどメモをしていたうちの--control-planeオプションが入っている方のコマンドを実行する
sudo kubeadm join 192.168.0.11:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
 --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
 --control-plane --certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


# kubectlのコンフィグを用意する
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config


# 少ししてから下記コマンドを実行するとノードが追加されていることが確認できる
kubectl get nodes
# NAME     STATUS   ROLES           AGE   VERSION
# kubec1   Ready    control-plane   21m   v1.25.2
# kubec2   Ready    control-plane   7m    v1.25.2
```

## ワーカーノード
```
# 先ほどメモをしていたうちの--control-planeオプションが入っている方のコマンドを実行する
sudo kubeadm join 192.168.0.11:6443 --token xxxxxx.xxxxxxxxxxxxxxxx \
 --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 


# 少ししてから下記コマンドをコントロールプレーンノードで実行するとノードが追加されていることが確認できる
kubectl get nodes
# NAME     STATUS   ROLES           AGE   VERSION
# kubec1   Ready    control-plane   32m   v1.25.2
# kubec2   Ready    control-plane   18m   v1.25.2
# kubec3   Ready    control-plane   13m   v1.25.2
# kubew1   Ready    control-plane   5m    v1.25.2
```

## 諸々のインストール
ワーカーノードがないとデプロイできないため後半に持ってきています。

```
# MetalLBをインストール
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.5/config/manifests/metallb-native.yaml

# 設定ファイルを作成する
# IPアドレスは使用できてかつ空いているIPアドレスを指定する(下記は192.168.10.0/24が使用できる場合)
cat << EOF > metallb-conf.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.10.1-192.168.10.254
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool
EOF

# 反映する
kubectl apply -f metallb-conf.yaml


# Metrics Serverをインストール
# そのままだと動かないので修正
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
vi components.yaml

# ...
# ---
# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   labels:
#     k8s-app: metrics-server
#   name: metrics-server
#   namespace: kube-system
# spec:
#   selector:
#     matchLabels:
#       k8s-app: metrics-server
#   strategy:
#     rollingUpdate:
#       maxUnavailable: 0
#   template:
#     metadata:
#       labels:
#         k8s-app: metrics-server
#     spec:
#       containers:
#       - args:
#         - --cert-dir=/tmp
#         - --secure-port=4443
#         - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
#         - --kubelet-use-node-status-port
#         - --metric-resolution=15s
#         - --kubelet-insecure-tls        # ******************* ここを追加 *******************
#         image: k8s.gcr.io/metrics-server/metrics-server:v0.6.1
#         imagePullPolicy: IfNotPresent
#         livenessProbe:
#           failureThreshold: 3
#           httpGet:
#             path: /livez
#             port: https
# ...

kubectl apply -f components.yaml
```


# 解説
各コマンドが何をしているかは上記である程度コメントを入れましたのでいくつか抜粋して解説します。

## プロキシ
今回はHA Proxyを使用してコントロールプレーンをHA構成にしています。
これで1台ぐらい落ちても大丈夫？（まだ試していません。）
他にもkeepalivedを使用することもできるようです。（HAで言うとこちらを使うのが本当は正解。）
一般的にはHA ProxyでLoadBalanceをしてkeepalivedで可用性を確保するというのが正解のようです。
（今回はとりあえずなので…。いつかちゃんとした構成にします。）
設定等は公式の資料から拝借しました。

https://github.com/kubernetes/kubeadm/blob/main/docs/ha-considerations.md#options-for-software-load-balancing

## iptables
Podの通信をいい感じにするためiptablesが使用されます。
最近はnftablesがデフォルトになっていますが、k8sが対応していないためiptablesに切り替えています。

## containerd
コンテナの実行環境ですね。
以前のバージョンではdockerが使われていましたが、最近はcontainerdがデフォルトになっています。
ちなみにトラブルシューティングのために試してみたところcrictlコマンドでdockerと同じように使えそうです。

## 1台目のコントロールプレーンノードの作成
1台目のコントロールプレーンノードの作成が一番大変です。ここが終われば90％終わったと言っても過言ではありません。
1台目の`kubeadm init`では

- ポッドが使用するネットワークのアドレス空間を指定(--pod-network-cidr)
- 証明書を共有するできるように指定(--upload-certs)
- プロキシ経由でAPIサーバにアクセスするように指定（--control-plane-endpoint）

しています。

Pod間通信をするため、今回はCalicoをCNIとして使用しています。（一応こちらがkubeadmプロジェクトで検証済みのCNIのようです。）
https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network
Podで使用するネットワークを合わせるため(spec.calicoNetwork.ipPools[].cidr)、yamlファイルを修正しています。

クラウドサービスではServiceのTypeに`LoadBalancer`を指定するとグローバルIPアドレスを振ってくれる仕組みがあります。
MetalLBはグローバルIPアドレスではないですが、プライベートIPアドレスを振ってくれるのでローカルでの使用で便利です。
せっかくならLoadBalancerも使えるようにしたいということで今回追加しました。
こちらも割り振りするIPアドレスを指定する必要があるため、コンフィグファイルを作成しています。

Metrics Serverは負荷に応じてレプリカ数を調整する`Horizontal Pod Autoscaler`を使用するために追加しています。
個人レベルではまず使用することはないと思いますが、これもオートスケールしたいという単なる欲張りです。

## 2台目以降のコントロールプレーンノードの追加
特に難しいことはありませんが、一つ注意すべき点があります。
コントロールプレーンで共有している証明書は`kubeadm init`を実行してから2時間以内に取得しないといけない点です。
2時間経つとシークレットから削除されるというセキュリティ機能ですが、2時間過ぎてしまった場合にコントロールプレーンノードを追加したい場合は再度シークレットにアップロードする必要があります。
そのコマンドが、`kubeadm init phase upload-certs --upload-certs`になります。


# 構築したクラスタで遊んでみる
下記のような内容のyamlファイルを用意してください

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 0%
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 100m
          requests:
            cpu: 100m
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
  - port: 80
  type: LoadBalancer
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx
spec:
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - resource:
      name: cpu
      target:
        averageUtilization: 10
        type: Utilization
    type: Resource
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx
```

このファイルを`kubectl apply -f nginx.yaml`のようにして適用します。
`kubectl get deploy -w`、`kubectl get pods -w`あたりを見るとポッドが生成されていく様子が見られて面白いかもしれません。

`kubectl get svc`をすると外部からアクセスできるようにIPアドレスが付与されていることもわかります。
実際にブラウザでアクセスしてみるとnginxのデフォルトのページが確認できます。

続いて`kubectl get hpa -w`で負荷の状態を見ながらポッドに負荷をかけます。
F5アタック程度では大した負荷にならないので、直接ポッドに入って負荷のかかるコマンドを実行します。
`kubectl get pod`でnginxのポッドを探し、`kubectl exec -it ngix-xxx-xxx -- bash`でポッドに入り、`yes > /dev/null`を実行します。
しばらくすると`TARGETS`のところで負荷が上がり、レプリカ数が増えます。
上記のコマンドを終了するとしばらくしてからレプリカ数が戻ります。


# あとがき
こだわりが強かったのか、ちょっとやってみたつもりがガッツリあちこちハマりまくりで大変だったのでまとめてみました。
（予想以上にハマりどころ満載で1週間ぐらいかかってしまった気がします。）
仮想マシンを作っては壊しで合計20台ぐらい使って検証したのでこれでおそらくうまくいくと思います。
といろいろ言いましたが、k8sのいい勉強になるのでみなさんもハマってください（？）
誤字、脱字、間違い等があればご指摘いただければ幸いです。



# Reference
- https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/high-availability/
- https://projectcalico.docs.tigera.io/getting-started/kubernetes/quickstart
- https://github.com/kubernetes/kubeadm/blob/main/docs/ha-considerations.md#options-for-software-load-balancing
- https://metallb.universe.tf/configuration/
- https://blog.framinal.life/entry/2020/04/16/022042#MetalLBのインストール
- https://github.com/kubernetes-sigs/metrics-server
- https://qiita.com/nakamasato/items/c49cf322cd5d7770b465