Kubernetes The Easy Way（Kubeadm編） 

# 始めに
Kubernetesへの理解を深めるために役に立つ物として[Kubernetes The Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way)というチュートリアルがあります[^1]。自動化ツール無しでkubernetesクラスターを構築することでKubernetesの構造を学べます。この記事は[kubeadm](https://github.com/kubernetes/kubeadm)を使って と上記のチュートリアルと同じクラスターを立ち上げて見ることでThe Hard Wayの理解を深める事を目的とします。

基本的にはThe Hard Wayの流れに沿って記述します。なるべく元の設定に寄せますが不必要だと思う所は修正を加えた後に注意書きでお知らせします。もとのチュートリアルと同じくGoogle Cloud Platform上で3つのmasterと3つのworkerを持つクラスターを構築します。作業環境は`Google Cloud`の`Cloud Shell`を使います。

自分もただKubernetesについて勉強している途中ですので間違ってる所や無駄な部分が多々あると思います。お気づきの点などございましたら、ご指摘いただけると幸いです。

# Prerequisites
`gcloud`のcommand-line toolを使う基本的な設定を行います。違いはありません。

```bash:command-line
(
gcloud config set compute/region us-west1
gcloud config set compute/zone us-west1-c
)
```

# Installing the Client Tools
要りません。

# Provisioning Compute Resources
ネットワークのセッティングとVMインスタンスを立ち上げます。

## Networking
ファイアウォール規則がちょっと違います。

```bash:command-line
(
gcloud compute networks create kubernetes-the-easy-way --subnet-mode custom

gcloud compute networks subnets create kubernetes \
  --network kubernetes-the-easy-way \
  --range 10.240.0.0/24

gcloud compute firewall-rules create kubernetes-the-easy-way-allow-internal \
  --allow tcp,udp,icmp,ipip \
  --network kubernetes-the-easy-way \
  --source-ranges 10.240.0.0/24,10.200.0.0/16

gcloud compute firewall-rules create kubernetes-the-easy-way-allow-external \
  --allow tcp:22,tcp:6443,icmp\
  --network kubernetes-the-easy-way \
  --source-ranges 0.0.0.0/0

gcloud compute addresses create kubernetes-the-easy-way \
  --region $(gcloud config get-value compute/region)
)
```
:::note warn 
内部用のファイアウォール規則(kubernetes-the-easy-way-allow-internal)の許可項目にIPIPプロトコールを追加します。
:::

この記事ではチュートリアルと違ってCNIとしてCalicoを使います。よって内部のファイアウォールでIPIPプロトコールを許可する必要があります[^2]。


`gcloud compute firewall-rules list --filter="network:kubernetes-the-easy-way"`を通して以下のような設定を確認できます。

```bash:check
$ gcloud compute firewall-rules list --filter="network:kubernetes-the-easy-way"
NAME                                    NETWORK                  DIRECTION  PRIORITY  ALLOW                 DENY  DISABLED
kubernetes-the-easy-way-allow-external  kubernetes-the-easy-way  INGRESS    1000      tcp:22,tcp:6443,icmp        False
kubernetes-the-easy-way-allow-internal  kubernetes-the-easy-way  INGRESS    1000      tcp,udp,icmp                False
```
また、`gcloud compute addresses list --filter="name=('kubernetes-the-easy-way')"`を通してパブリックIPアドレスを確認できます。

```bash:check
$ gcloud compute addresses list --filter="name=('kubernetes-the-easy-way')"
NAME                     ADDRESS/RANGE   TYPE      PURPOSE  NETWORK  REGION    SUBNET  STATUS
kubernetes-the-easy-way  XXX.XXX.XXX.XXX  EXTERNAL                    us-west1          RESERVED
```

## Compute Instances
インスタンスを立ち上げます。違いはありません。

### Kubernetes Controllers
```bash:command-line
for i in 0 1 2; do
  gcloud compute instances create controller-${i} \
    --async \
    --boot-disk-size 200GB \
    --can-ip-forward \
    --image-family ubuntu-2004-lts \
    --image-project ubuntu-os-cloud \
    --machine-type e2-standard-2 \
    --private-network-ip 10.240.0.1${i} \
    --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring \
    --subnet kubernetes \
    --tags kubernetes-the-easy-way,controller
done
```
### Kubernetes Workers
```bash:command-line
for i in 0 1 2; do
  gcloud compute instances create worker-${i} \
    --async \
    --boot-disk-size 200GB \
    --can-ip-forward \
    --image-family ubuntu-2004-lts \
    --image-project ubuntu-os-cloud \
    --machine-type e2-standard-2 \
    --metadata pod-cidr=10.200.${i}.0/24 \
    --private-network-ip 10.240.0.2${i} \
    --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring \
    --subnet kubernetes \
    --tags kubernetes-the-easy-way,worker
done
```
`gcloud compute instances list --filter="tags.items=kubernetes-the-easy-way"`を打つと

```bash:check
$ gcloud compute instances list --filter="tags.items=kubernetes-the-easy-way"
NAME          ZONE        MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
controller-0  us-west1-c  e2-standard-2               10.240.0.10  XXX.XXX.XXX.XXX  RUNNING
controller-1  us-west1-c  e2-standard-2               10.240.0.11  XXX.XXX.XXX.XXX  RUNNING
controller-2  us-west1-c  e2-standard-2               10.240.0.12  XXX.XXX.XXX.XXX  RUNNING
worker-0      us-west1-c  e2-standard-2               10.240.0.20  XXX.XXX.XXX.XXX  RUNNING
worker-1      us-west1-c  e2-standard-2               10.240.0.21  XXX.XXX.XXX.XXX  RUNNING
worker-2      us-west1-c  e2-standard-2               10.240.0.22  XXX.XXX.XXX.XXX  RUNNING
```
六つのインスタンスが立ち上がっている事が確認できます。

# Provisioning the CA and Generating TLS Certificates
要りません。
# Generating Kubernetes Configuration Files for Authentication
要りません。
# Generating the Data Encryption Config and Key
要りません。

#※kubernetesのインストール※
元のチュートリアルではないセクションですが、`kubeadm`を使う場合、マスターノードとワーカーノードで同じ環境を使います。よって各VMに接続して[^3]以下の過程を六つのインスタンスで行います。

:::note info 
ルート権限を仮定しています。
:::

```bash:command-line
(
# Swapをオフにする。
swapoff -a
# sed to comment the swap partition in /etc/fstab
sed -i.bak -r 's/(.+ swap .+)/#\1/' /etc/fstab

# iptablesがブリッジを通過するトラフィックを処理できるようにする
cat <<EOF >  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

# iptablesをレガシーバージョンに切り替える。
apt-get update && apt-get install -y iptables arptables ebtables
update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
update-alternatives --set arptables /usr/sbin/arptables-legacy
update-alternatives --set ebtables /usr/sbin/ebtables-legacy

# Dockerをインストールする
apt-get install -y \
  apt-transport-https ca-certificates curl software-properties-common gnupg2

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -

add-apt-repository \
  "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) \
  stable"

apt-get update && apt-get install -y \
  containerd.io=1.2.13-2 \
  docker-ce=5:19.03.11~3-0~ubuntu-$(lsb_release -cs) \
  docker-ce-cli=5:19.03.11~3-0~ubuntu-$(lsb_release -cs)

apt-mark hold containerd.io docker-ce docker-ce-cli

# Dockerデーモンをセットアップ
cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

mkdir -p /etc/systemd/system/docker.service.d

systemctl daemon-reload
systemctl restart docker
systemctl enable docker

# kubernetesをインストールする
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

apt-get update && apt-get install -y kubelet kubeadm kubectl

apt-mark hold kubelet kubeadm kubectl

systemctl daemon-reload
systemctl restart kubelet
systemctl enable kubelet
)
```

`kubeadm version -o short`と`kubelet --version`でインストールが正しく行われたか確認でみます。

```bash:check
$ kubeadm version -o short
v1.22.1
$ kubelet --version
Kubernetes v1.22.1
```

# Bootstrapping the etcd Cluster & Bootstrapping the Kubernetes Control Plane
`controller-0`,`controller-1`,`controller-2`を用いて積層コントロールプレーンを構成します。

:::note info
kubeadmの積層HAクラスターはデフォルトで積層etcdトポロジーのクラスターを生成します。別途にetcdクラスターを構築する必要はありません。
:::

## Provision a Network Load Balancer
ターゲットプールではなくバックエンドサービスを使用したネットワークロードバランサを使用します。

### Enable HTTPS Health Checks
kubernetesは基本的にヘルスチェック用のHTTPSエンドポイントを持っています[^4]。即ち、新しいロードバランサを使えばHTTPSヘルスチェックでNGINXなしに直接繋げます[^5]。

```bash:command-line
(
KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-easy-way \
        --region $(gcloud config get-value compute/region) \
        --format 'value(address)')

# リージョンヘルスチェックを作成
gcloud compute health-checks create https kubernetes \
        --region=$(gcloud config get-value compute/region) \
        --description="Kubernetes Health Check" \
        --host "kubernetes.default.svc.cluster.local" \
        --request-path "/readyz" \
        --port=6443

# ネットワーク負荷分散のソースIP
gcloud compute firewall-rules create kubernetes-the-easy-way-allow-health-check \
        --network kubernetes-the-easy-way \
        --source-ranges=35.191.0.0/16,209.85.152.0/22,209.85.204.0/22 \
        --allow tcp
)
```

### Provision a Network Load Balancer
非マネージドインスタンスグループとバックエンドサービスを作りネットワークロードバランサに繋げます。まだインスタンスグループにcontrollerインスタンスは追加しないでください。

```bash:command-line
(
KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-easy-way \
        --region $(gcloud config get-value compute/region) \
        --format 'value(address)')

# 非マネージドインスタンスグループの作成
gcloud compute instance-groups unmanaged create instance-group-easy-way \
        --zone $(gcloud config get-value compute/zone)

# リージョンバックエンドサービスを作成
gcloud compute backend-services create kubernetes-network-lb-backend-service \
        --region $(gcloud config get-value compute/region) \
        --health-checks kubernetes \
        --health-checks-region $(gcloud config get-value compute/region) \
        --protocol TCP

# インスタンスグループをバックエンドとして追加
gcloud compute backend-services add-backend kubernetes-network-lb-backend-service \
   --instance-group instance-group-easy-way \
   --instance-group-zone $(gcloud config get-value compute/zone) \
   --region $(gcloud config get-value compute/region)

# 転送ルールを作成
gcloud compute forwarding-rules create kubernetes-forwarding-rule \
        --address ${KUBERNETES_PUBLIC_ADDRESS} \
        --ports 6443 \
        --region $(gcloud config get-value compute/region) \
        --backend-service kubernetes-network-lb-backend-service
)
```

## Provision the Kubernetes Control Plane
kubeadmを使ってコントロールプレーンを構成していきます。最初のcontrollerと他のcontrollerではンスタンスグループにインスタンスを追加する順番が逆なことにご注意ください。もし、順番を間違えばcontroller同士の通信ができなくなりtimeoutが発生します。

```bash:command-line
# インスタンスグループにcontroller-0を追加
gcloud compute instance-groups unmanaged add-instances instance-group-easy-way \
        --instances controller-0 \
        --zone $(gcloud config get-value compute/zone)
```

```bash:controller-0
# controller-0で実行
(
REGION=$(curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/project/attributes/google-compute-default-region)

KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-easy-way \
  --region $REGION \
  --format 'value(address)')

kubeadm init --pod-network-cidr=10.200.0.0/16 --service-cidr=10.32.0.0/24 --control-plane-endpoint "${KUBERNETES_PUBLIC_ADDRESS}:6443" --upload-certs

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config

# CNIとしてCalicoを指定
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
)
```
出力されるkubeadmのテキストを記録しておきます。上の方に書かれている`join`はcontrollerで下の`join`はworkerでクラスターに参加させる際に使います。

```bash:output
You can now join any number of the control-plane node running the following command on each as root:

kubeadm join XXX.XXX.XXX.XXX: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 XXX.XXX.XXX.XXX:6443 --token XXXXXX.xxxxxxxxxxxxxxxx \
        --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

:::note info
このトークンはデフォルトで２時間で消滅します。joinコマンドは"kubeadm token create --print-join-command"で、コントロールプレーンの為のcertificate-keyは"kubeadm init phase upload-certs --upload-certs"で再発行できます。
:::

上記のコマンドを使ってcontroller-1とcontroller-2をjoinさせていきます。

:::note warn
必ず各インスタンスでkubernetesを立ち上げた後にVMインスタンスをインスタンスグループに加えてください。
:::

もし先にインスタンスグループに加えてしまえば、ロードバランサが常にトラフィックをリクエストを送る側のVMに返してしまうため、クラスターに参加できなくなります[^6]。

```bash:controller-1
# controller-1で実行
(
kubeadm join XXX.XXX.XXX.XXX:6443 --token XXXXXX.xxxxxxxxxxxxxxxx \
        --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
        --control-plane --certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
)
```

```bash:command-line
# インスタンスグループにcontroller-1を追加
gcloud compute instance-groups unmanaged add-instances instance-group-easy-way \
        --instances controller-1 \
        --zone $(gcloud config get-value compute/zone)
```

```bash:controller-2
# controller-2で実行
(
kubeadm join XXX.XXX.XXX.XXX:6443 --token XXXXXX.xxxxxxxxxxxxxxxx \
        --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
        --control-plane --certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
)
```

```bash:command-line
# インスタンスグループにcontroller-2を追加
gcloud compute instance-groups unmanaged add-instances instance-group-easy-way \
        --instances controller-2 \
        --zone $(gcloud config get-value compute/zone)
```

各インスタンスで`kubectl get nodes`を通して積層コントロールプレーンが構築されている事が確認できます。

```bash:check
# controllerで実行
$ kubectl get nodes
NAME           STATUS   ROLES                  AGE     VERSION
controller-0   Ready    control-plane,master   10m     v1.22.2
controller-1   Ready    control-plane,master   3m30s   v1.22.2
controller-2   Ready    control-plane,master   101s    v1.22.2
```

各インスタンスで以下のコマンドを通して積層etcdクラスターが正常に動作している事が確認できます。

```bash:check
# controllerで実行
$ kubeadm config images list --kubernetes-version v1.22.2

k8s.gcr.io/kube-apiserver:v1.22.2
k8s.gcr.io/kube-controller-manager:v1.22.2
k8s.gcr.io/kube-scheduler:v1.22.2
k8s.gcr.io/kube-proxy:v1.22.2
k8s.gcr.io/pause:3.5
k8s.gcr.io/etcd:3.5.0-0
k8s.gcr.io/coredns/coredns:v1.8.4

$ docker run --rm -it \
--net host \
-v /etc/kubernetes:/etc/kubernetes k8s.gcr.io/etcd:3.5.0-0 etcdctl \
--cert /etc/kubernetes/pki/etcd/peer.crt \
--key /etc/kubernetes/pki/etcd/peer.key \
--cacert /etc/kubernetes/pki/etcd/ca.crt \
--endpoints https://10.240.0.10:2379 endpoint health --cluster

https://10.240.0.10:2379 is healthy: successfully committed proposal: took = 18.440766ms
https://10.240.0.11:2379 is healthy: successfully committed proposal: took = 21.317217ms
https://10.240.0.12:2379 is healthy: successfully committed proposal: took = 22.050581ms
```

# Bootstrapping the Kubernetes Worker Nodes
前のパートでもう環境設定は終わっています。各workerインスタンスでkubeadm joinコマンドを実行するだけでクラスターに加わります。

```bash:worker-0,worker-1,worker-2
# 各々のworkerで実行
kubeadm join XXX.XXX.XXX.XXX:6443 --token XXXXXX.xxxxxxxxxxxxxxxx \
        --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

```bash:check
# controllerで実行
$ kubectl get nodes
NAME           STATUS   ROLES                  AGE    VERSION
controller-0   Ready    control-plane,master   25m    v1.22.2
controller-1   Ready    control-plane,master   18m    v1.22.2
controller-2   Ready    control-plane,master   16m    v1.22.2
worker-0       Ready    <none>                 2m     v1.22.2
worker-1       Ready    <none>                 2m7s   v1.22.2
worker-2       Ready    <none>                 117s   v1.22.2
```

# Configuring kubectl for Remote Access
Master Nodeで`kubectl config view --raw`を実行した後、出力されるテキストを`~/.kube/config`に上書きすることでkubectlでのリモートアクセスが可能になります。もともとグローバルIPでの証明書なので問題なく作動します。

# Provisioning Pod Network Routes
Calicoを導入しているので必要ありません。

# Deploying the DNS Cluster Add-on
もう作動しています。

```bash:check
# controllerで実行
$ kubectl get pods -l k8s-app=kube-dns -n kube-system
NAME                       READY   STATUS    RESTARTS   AGE
coredns-78fcd69978-kvcjq   1/1     Running   0          49m
coredns-78fcd69978-nvhkf   1/1     Running   0          49m

$ kubectl run busybox --image=busybox:1.28 --command -- sleep 360
$ kubectl get pods -l run=busybox
NAME      READY   STATUS    RESTARTS   AGE
busybox   1/1     Running   0          3s

$ POD_NAME=$(kubectl get pods -l run=busybox -o jsonpath="{.items[0].metadata.name}")
$ kubectl exec -ti $POD_NAME -- nslookup kubernetes
Server:    10.32.0.10
Address 1: 10.32.0.10 kube-dns.kube-system.svc.cluster.local
Name:      kubernetes
Address 1: 10.32.0.1 kubernetes.default.svc.cluster.local
```

# Smoke Test
テストなので割愛します。参考までにData Encryptionセッションでは証明書を使用していますが、kubeadmが自動生成する証明書は`/etc/kubernetes/pki`に位置します[^7]。

# Cleaning Up

```bash:command-line
# インスタンスの削除
gcloud -q compute instances delete \
        controller-0 controller-1 controller-2 \
        worker-0 worker-1 worker-2 \
        --zone $(gcloud config get-value compute/zone)

# ロードバランサの削除
gcloud -q compute forwarding-rules delete kubernetes-forwarding-rule \
        --region $(gcloud config get-value compute/region)

gcloud -q compute backend-services remove-backend kubernetes-network-lb-backend-service \
        --instance-group instance-group-easy-way \
        --instance-group-zone $(gcloud config get-value compute/zone) \
        --region $(gcloud config get-value compute/region)

gcloud -q compute backend-services delete kubernetes-network-lb-backend-service \
        --region $(gcloud config get-value compute/region)

# インスタンスグループの削除
gcloud -q compute instance-groups unmanaged delete instance-group-easy-way \
        --zone $(gcloud config get-value compute/zone)

# ヘルスチェックの削除
gcloud -q compute health-checks delete kubernetes \
    --region=$(gcloud config get-value compute/region)

# ファイアウォール規則の削除
gcloud -q compute firewall-rules delete \
        kubernetes-the-easy-way-allow-internal \
        kubernetes-the-easy-way-allow-external \
        kubernetes-the-easy-way-allow-health-check

#　ネットワークの削除
gcloud -q compute networks subnets delete kubernetes
gcloud -q compute networks delete kubernetes-the-easy-way

# ip addressesの解放
gcloud -q compute addresses delete kubernetes-the-easy-way \
        --region $(gcloud config get-value compute/region)
```

[^1]: Kubernetes The Hard Way, https://github.com/kelseyhightower/kubernetes-the-hard-way
[^2]: Self-managed Kubernetes in Google Compute Engine (GCE), https://docs.projectcalico.org/getting-started/kubernetes/self-managed-public-cloud/gce
[^3]: Linux VM への SSH 接続, https://cloud.google.com/compute/docs/instances/ssh?hl=ja
[^4]: Kubernetes API health endpoints, https://kubernetes.io/docs/reference/using-api/health-checks/
[^5]: Google Cloud Load Balancing ガイド - ヘルスチェックの概要, https://cloud.google.com/load-balancing/docs/health-check-concepts?hl=ja
[^6]: Google Cloud Load Balancing ガイド - 負荷分散された VM からのリクエストの送信, https://cloud.google.com/load-balancing/docs/internal/setting-up-internal#test-from-backend-vms
[^7]: Kubernetesドキュメント ベストプラクティス - PKI証明書とその要件, https://kubernetes.io/ja/docs/setup/best-practices/certificates/