kubeadm / Ansible で Kubernetes HA クラスタ構築ハンズオン

# はじめに
前回 ```kubeadm``` で Kubernetes のシングルコントロールプレーンのクラスタを構築しました。

> [kubeadm / containerd で Kubernetes](https://qiita.com/t_ume/items/f0a1c1ae8f94add8f4ba)

今回は ```kubeadm``` と ```Ansible``` を使って HA クラスタを構築しようと思います。
```kubeadm``` を実行しようにも複数サーバをセットアップには時間がかかるので、Ansible を使って IaC な構築をしようと思います。
がっつりハンズオンな記事なので量がボリューミーなのはご了承ください。

# 環境情報

OS：Ubuntu 18.04.5 LTS
containerd：1.4.3
kubeadm：v1.20.2
Kubernetes：v1.20.2
kubectl：v1.20.2
Ansible：2.9.17

|Role|hostname|CPU|Memory|enp0s3（NAT）|enp0s8（Bridge）|
|:---|:---|:---|:---|:---|:---|
|ReverseProxy|rp01|1 Core|1 Gi|10.0.2.15|192.168.10.51|
|ControlPlane|cp01<br>cp02<br>cp03|2 Core|4 Gi|10.0.2.15|192.168.10.61<br>192.168.10.62<br>192.168.10.63|
|Node|nd01<br>nd02|2 Core|4 Gi|10.0.2.15|192.168.10.71<br>192.168.10.72|

あまりに気にしていなかったのですが、```Master``` ⇒ ```ControlPlane```、```Worker``` ⇒ ```Node``` と呼ばれているんですね。知らなかった・・・

> [Kubernetesのコンポーネント](https://kubernetes.io/ja/docs/concepts/overview/components/)

今回も Vagrant/VirtualBox で構築します。
参考までに下記に今回使った Vagrantfile を載せておきます。
※NIC1（enp0s3）：NAT、NIC2（enp0s8）：ブリッジアダプターで設定しています。
※``` kubectl exec ``` で支障がでるため、起動スクリプトでデフォルトゲートウェイをブリッジアダプター側（192.168.10.1）に変更しています。

```ruby:Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.define "rp01" do |server|
    server.vm.box = "ubuntu/bionic64"
    server.vm.hostname = "rp01"
    server.vm.network "public_network", ip: "192.168.10.51"    
    server.vm.synced_folder ".", "/vagrant", disabled: true
    server.vm.provider "virtualbox" do |vb|
      vb.customize [
        "modifyvm", :id,
        "--memory", "1024",
        "--cpus", "1"
      ]
    end
    server.vm.provision "shell", run: "always", inline: <<-SHELL
sudo ip route del default via 10.0.2.2
sudo ip route add default via 192.168.10.1
    SHELL
  end

  (1..3).each do |i|
    config.vm.define "cp0#{i}" do |server|
      server.vm.box = "ubuntu/bionic64"
      server.vm.hostname = "cp0#{i}"
      server.vm.network "public_network", ip: "192.168.10.6#{i}"    
      server.vm.synced_folder ".", "/vagrant", disabled: true
      server.vm.provider "virtualbox" do |vb|
        vb.customize [
          "modifyvm", :id,
          "--memory", "4096",
          "--cpus", "2"
        ]
      end
      server.vm.provision "shell", run: "always", inline: <<-SHELL
sudo ip route del default via 10.0.2.2
sudo ip route add default via 192.168.10.1
      SHELL
    end
  end

  (1..2).each do |i|
    config.vm.define "nd0#{i}" do |server|
      server.vm.box = "ubuntu/bionic64"
      server.vm.hostname = "nd0#{i}"
      server.vm.network "public_network", ip: "192.168.10.7#{i}"    
      server.vm.synced_folder ".", "/vagrant", disabled: true
      server.vm.provider "virtualbox" do |vb|
        vb.customize [
          "modifyvm", :id,
          "--memory", "4096",
          "--cpus", "2"
        ]
      end
      server.vm.provision "shell", run: "always", inline: <<-SHELL
sudo ip route del default via 10.0.2.2
sudo ip route add default via 192.168.10.1
      SHELL
    end
  end
end
```

# Ansible インストール
まずはサーバセットアップ用に Ansible をインストールしていきます。

```console:作業サーバ：rp01
# Ansible インストールのため python のパッケージマネージャ pip をインストール
$ sudo apt update && sudo apt install python3-pip -y

$ pip3 -V
pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6)

# Ansible のインストール（今回はバージョンを指定してインストール）
$ sudo pip3 install ansible==2.9.17

$ ansible --version
ansible 2.9.17
・・・
```

インストールができたので Ansible で各サーバに SSH 接続するための鍵ファイルを作って配布します。
今回は vagrant で実行しているため、vagrant ユーザに公開鍵を追加しています。適宜、作業用のユーザに設定ください。

```console:作業サーバ：rp01
# Ansible 用の SSH 鍵作成
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/vagrant/.ssh/id_rsa): ★Enter
Enter passphrase (empty for no passphrase): ★Enter
Enter same passphrase again: ★Enter
・・・

# rp01 の vagrant ユーザの公開鍵に鍵を追加
$ cat .ssh/id_rsa.pub >> .ssh/authorized_keys

# 他サーバの vagrant ユーザに公開鍵を追加するため、対象公開鍵を確認
$ cat .ssh/id_rsa.pub
ssh-rsa　xxxx root@rp01

# 上記で確認した公開鍵を、各サーバ（cp01～03、nd01～02）にログインし vagrant ユーザの公開鍵に作成した鍵を追記
# vagrant の場合、デフォルトの鍵は消すと SSH ログインできなくなるため、消さずに追記する
$ vi ~/.ssh/authorized_keys
```

各サーバへの接続準備ができました。Ansible で接続する際の設定ファイルを作成して疎通確認を行っていきます。

```console:作業サーバ：rp01
# 作業用ディレクトリ作成
$ mkdir -p kubeadm/roles && cd kubeadm

# 以降で以下の構成でファイルを作成する
kubeadm/
　├ roles/
　├ ansible.cfg  ：Ansible 設定ファイル
　└ inventory    ：接続先情報ファイル
```

初回 SSH 接続時の ```know_hosts``` 警告文の対応に Ansible の設定ファイルを作成します。
セキュアな接続を行う場合は、一度各サーバへ SSH 接続確認を行うことで以下の設定は不要です。

> 参考：[AnsibleのSSH接続エラーの回避設定](https://qiita.com/taka379sy/items/331a294d67e02e18d68d)

```ini:ansible.cfg
[defaults]
host_key_checking = False
```

接続先サーバの情報ファイルを作成します。
各サーバの IP アドレスとグループを分けて登録しました。

```ini:inventory
[rp]
rp01  ansible_host=192.168.10.51

[cp]
cp01  ansible_host=192.168.10.61
cp02  ansible_host=192.168.10.62
cp03  ansible_host=192.168.10.63

[nd]
nd01  ansible_host=192.168.10.71
nd02  ansible_host=192.168.10.72
```

準備ができたの Ansible で疎通確認を行います。

```console:作業サーバ：rp01
# ad-hoc 機能で Ansible のモジュールを直接実行し、接続確認を行う
# -i：接続先情報ファイルを指定
# -m：Ansible モジュール指定。ping モジュールは疎通確認を行う
# all：接続先の引数、inventory ファイルで記載した識別子が指定可能。今回は全てのサーバを指定する all を指定
$ ansible -i inventory -m ping all
cp03 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
・・・
nd02 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
```

ping - pong で無事に疎通確認が取れました。

# 共通設定
さて、ここから Ansible の Playbook を作成していきます。
今回は Roles を使って Playbook を取り纏めます。
Roles の簡単な説明は、以下記事を投稿しているのでご参考に。

> [ansible role 事始め](https://qiita.com/t_ume/items/f21505e27c49c90d06ba)

手始めに共通で利用する Role を作成します。

```console:作業サーバ：rp01
# 以下の構成でファイルを作成する
kubeadm/
　├ ansible.cfg
　├ inventory
　└ roles/                   ：各ロール用のディレクトリを配下に配置
　   └ common/               ：共通設定のロール
　      └ tasks/main.yml     ：共通の設定タスク

$ pwd
/root/kubeadm

# 共通設定のロールを作成
# 以下コマンドでロールのテンプレート一式が作成される
# --init-path：ロールのテンプレートを指定パス配下に作成する
$ ansible-galaxy role init --init-path roles common
- Role common was created successfully

$ ls roles/
common
```

最初の Role ができた所で Playbook を作成します。
共通で利用するパッケージのインストールを行うタスクを記述していきます。

> Ansible：[apt](https://docs.ansible.com/ansible/2.9/modules/apt_module.html)

```yaml:roles/common/tasks/main.yml
---
# tasks file for common
- name: Install https repo for apt
  apt:
    pkg:
    - apt-transport-https
    - ca-certificates
    - curl
    - software-properties-common
    update_cache: yes
    state: present
```

実際のロールの指定、実行は以降で合わせて実施します。

# ReverseProxy 構築
次に rp01 にリバースプロキシとして ```nginx``` をインストールしていきます。
```kubectl``` を使ったコントロールプレーンへのリクエストを振り分けます。

```console:作業サーバ：rp01
# 以下の構成でファイルを作成する
kubeadm/
　├ ansible.cfg
　├ inventory
　├ rp.yml                   ：リバースプロキシ実行用 Playbook
　└ roles/
　   └ reverseproxy/         ：リバースプロキシのロール
　      ├ files/nginx.conf   ：nginx 設定ファイル
　      ├ handlers/main.yml  ：nginx の設定ファルが書き換わったときの reload Playbook
　      └ tasks/main.yml     ：nginx インストール・設定 Playbook

# リバースプロキシのロール作成
$ ansible-galaxy role init --init-path roles reverseproxy
- Role reverseproxy was created successfully

$ ls roles/
common reverseproxy
```

ロールが作成できたところで、nginx インストールの Playbook を作成していきます。
Playbook のタスク内容は以下の通りです。

1. nginx のインストール
2. 設定ファイルの配置
3. nginx の起動と自動起動の設定

> Ansible：[apt](https://docs.ansible.com/ansible/2.9/modules/apt_module.html)、[copy](https://docs.ansible.com/ansible/2.9/modules/copy_module.html)、[systemd](https://docs.ansible.com/ansible/2.9/modules/systemd_module.html)

```yaml:roles/reverseproxy/tasks/main.yml
---
# tasks file for reverseproxy
- name: Install nginx
  apt:
    name: nginx
    update_cache: yes
    state: present

- name: Copy nginx conf
  copy:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes
  notify: reload_nginx

- name: Start/Enabled nginx
  systemd:
    name: nginx
    enabled: yes
    state: started
```

```nginx.conf``` が変更された際にも自動で ```reload``` されるように ```handlers``` に処理を記載します。

```yaml:roles/reverseproxy/handlers/main.yml
---
# handlers file for reverseproxy
- name: reload_nginx
  systemd:
    name: nginx
    state: reloaded
```

```task.yaml``` で指定した ```notify: reload_nginx``` はファイルの再配置をトリガーに ```handlers``` に通知し、 ```reload_nginx``` を実行する仕組みとなっています。

次に Playbook で配置する nginx の設定ファイルを作成します。今回は最低限の設定のみ記述しています。
※ k8s api リクエストを各コントロールプレーンにリバースプロキシします。

```conf:roles/reverseproxy/files/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

error_log /var/log/nginx/error.log warn;

events {
   worker_connections  1024;
}

stream {
   upstream kubernetes {
      server 192.168.10.61:6443 max_fails=2 fail_timeout=30s;
      server 192.168.10.62:6443 max_fails=2 fail_timeout=30s;
      server 192.168.10.63:6443 max_fails=2 fail_timeout=30s;
   }
   server {
      listen 6443;
      proxy_pass kubernetes;
   }
}
```

最後に作成したロールを実行するための Playbook を作成します。rp グループに所属するサーバに対し、```reverseproxy``` ロールと先ほど作成した ```common``` ロールの Playbook を実行するように作成します。

- hosts：実行対象
- gather_facts：対象サーバのメタ情報取得要否。OS の種別によって Playbook を変更する場合などの条件分岐させる際に便利。
- become：指定されたユーザでの実行要否。```yes``` を指定すると、デフォルトは ```root``` ユーザになって実行する。
- tags：Playbook 実行時に指定すると実行対象を絞り込む事ができる

```yaml:rp.yml
---
# file: rp.yml
- hosts: rp
  gather_facts: no
  become: yes
  roles:
    - role: common
      tags: cmn
    - role: reverseproxy
      tags: rp
```

準備が整ったので実行していきます。
1度目の実行で設定を適用し、2度目の実行で設定がちゃんと反映されているか確認します。
※ ```-C``` オプションで dry-run、```--diff``` オプションで変更前後の diff を出力することもできます。
※ 対象ユーザにパスワードなしの ```sudo ``` の許可設定がされていない場合はエラーが出ます。vagrant の場合は ```/etc/sudoers.d/vagrant``` に ```vagrant ALL=(ALL) NOPASSWD:ALL``` が設定されています。必要に応じて設定をお願いします。

```console:作業サーバ：rp01
$ ansible-playbook -i inventory rp.yml

PLAY [rp] ***************************************************************************************
・・・
TASK [reverseproxy : Install nginx] *************************************************************
changed: [rp01]
・・・
PLAY RECAP **************************************************************************************
rp01     : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$ ansible-playbook -i inventory rp.yml
・・・
PLAY RECAP **************************************************************************************
rp01     : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```

全て ok になり、無事に nginx のインストール、設定ファイルの配置が完了しました。
プロキシ先が無いため、とりあえずポートの疎通確認だけ確認します。

```console:作業サーバ：rp01
$ nc 192.168.10.51 -z 6443 -v
Connection to 192.168.10.51 6443 port [tcp/*] succeeded!
```

# Ansible で Kubernetes セットアップ
最初の ```Playbook``` が実行できたので今度は Kubernetes サーバに適用する Playbook を作成していきます。
まずは全てのロールを作成します。

```console:作業サーバ：rp01
# ロールのディレクトリ構成
kubeadm/
　└ roles/
　   ├ cri/          ：CRI（containerd）のロール
　   ├ k8s-server/   ：Kubernetes サーバ（kubelet / kubeadm ）のロール
　   └ k8s-client/   ：Kubernetes クライアント（kubectl）のロール

# 各ロールの作成
$ ansible-galaxy role init --init-path roles cri
- Role cri was created successfully

$ ansible-galaxy role init --init-path roles k8s-server
- Role k8s-server was created successfully

$ ansible-galaxy role init --init-path roles k8s-client
- Role k8s-client was created successfully

# ls roles/
common  cri  k8s-client  k8s-server  reverseproxy
```

## cri
まずはコンテナ実行環境（CRI）として ```containerd``` をインストールします。

```console:作業サーバ：rp01
# cri ロールのディレクトリ構成
kubeadm/
　├ cp.yml                       ：コントロールプレーン実行用 Playbook
　├ nd.yml                       ：ノード実行用 Playbook
　└ roles/
　   └ cri/                      ：CRI（containerd）のロール
        ├ files/containerd.conf  ：カーネルモジュール永続化用ファイル
　      └ tasks/main.yml         ：Playbook
```

前述より長めの Playbook ですが、やっていることは以下のとおりです。

1. カーネルモジュールのロード
2. カーネルモジュール設定の永続化用にファイル配置
3. カーネルパラメータの設定
4. リポジトリの登録
5. ```containerd``` のインストール

> Ansible：[modeprobe](https://docs.ansible.com/ansible/2.9/modules/modprobe_module.html)、[copy](https://docs.ansible.com/ansible/2.9/modules/copy_module.html)、[sysctl](https://docs.ansible.com/ansible/2.9/modules/sysctl_module.html)、[apt_key](https://docs.ansible.com/ansible/2.9/modules/apt_key_module.html)、[apt_repository](https://docs.ansible.com/ansible/2.9/modules/apt_repository_module.html)、[apt](https://docs.ansible.com/ansible/2.9/modules/apt_module.html)、[systemd](https://docs.ansible.com/ansible/2.9/modules/systemd_module.html)

```yaml:roles/cri/tasks/main.yml
---
# tasks file for cri
- name: Load kernel module
  modprobe:
    name: "{{ item }}"
    state: present
  with_items:
    - br_netfilter
    - overlay

- name: Copy kernel module load list for boot
  copy:
    src: containerd.conf
    dest: /etc/modules-load.d/containerd.conf
    owner: root
    group: root
    mode: '0644'

- name: Add sysctl conf
  sysctl:
    name: "{{ item.name }}"
    value: "{{ item.value }}"
    sysctl_file: /etc/sysctl.d/99-kubernetes-cri.conf
    state: present
  with_items:
    - name: net.ipv4.ip_forward
      value: "1"
    - name: net.bridge.bridge-nf-call-iptables
      value: "1"
    - name: net.bridge.bridge-nf-call-ip6tables
      value: "1"

- name: Add apt-key for docker
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    state: present

- name: Add apt repository for docker
  apt_repository:
    repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable
    state: present

- name: Install containerd
  apt:
    name: containerd.io
    update_cache: yes
    state: present

- name: Start/Enabled containerd
  systemd:
    name: containerd
    enabled: yes
    state: started
```

カーネルモジュールの設定の永続化は設定ファイルが必要なため、以下ファイルを作成して配置します。

```text:roles/cri/files/containerd.conf
overlay
br_netfilter
```

CRI インストールの Playbook ができたので、コントロールプレーン、ノードの実行用 Playbook を作成します。

```yaml:cp.yml
---
# file: cp.yml
- hosts: cp
  gather_facts: no
  become: yes
  roles:
    - role: common
      tags: cmn
    - role: cri
      tags: cri
```

```yaml:nd.yml
---
# file: nd.yml
- hosts: nd
  gather_facts: no
  become: yes
  roles:
    - role: common
      tags: cmn
    - role: cri
      tags: cri
```

実行用 Playbook ができたのでいざ実行です。

```console:作業サーバ：rp01
$ ansible-playbook -i inventory cp.yml
PLAY [cp] ***************************************************************************************
・・・
PLAY RECAP **************************************************************************************
cp01    : ok=8    changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
cp02    : ok=8    changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
cp03    : ok=8    changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$ ansible-playbook -i inventory nd.yml
PLAY [nd] ***************************************************************************************
・・・
PLAY RECAP **************************************************************************************
nd01    : ok=8    changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
nd02    : ok=8    changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

CRI の ```containerd``` がインストールできました。

## k8s-server
CRI がインストールできたので、今度はサーバ側で必要な Kubernetes のパッケージをインストールしていきます。
Kubernetes コンポーネントである ```kubelet``` と、インストールツールである ```kubeadm``` をインストールする Playbook を作成していきます。

```console:作業サーバ：rp01
_/_/_/ k8s-server ロールのディレクトリ構成 _/_/_/ 
kubeadm/
　└ roles/
　   └ k8s-server/               ：Kubernetes サーバ（kubelet / kubeadm ）のロール
　      └ tasks/main.yml         ：Playbook
```

インストールの流れは今までと同じような以下の流れです。

1. Kubernetes レポジトリ登録
2. ```kubelet```、```kubeadm``` のインストール

```yaml:roles/k8s-server/tasks/main.yml
---
# tasks file for k8s-server
- name: Add apt-key for kubernetes
  apt_key:
    url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
    state: present

- name: Add apt repository for kubernetes
  apt_repository:
    repo: deb https://apt.kubernetes.io/ kubernetes-xenial main
    state: present

- name: Install kubeadm, kubelet
  apt:
    pkg:
    - kubelet
    - kubeadm
    update_cache: yes
    state: present
```

先程作成した、```cp.yml``` と ```nd.yml``` にロールを追加して実行します。

```yaml:cp.yml
---
# file: cp.yml
- hosts: cp
  gather_facts: no
  become: yes
  roles:
    - role: common
      tags: cmn
    - role: cri
      tags: cri
    - role: k8s-server
      tags: k8ss
```

```yaml:nd.yml
---
# file: nd.yml
- hosts: nd
  gather_facts: no
  become: yes
  roles:
    - role: common
      tags: cmn
    - role: cri
      tags: cri
    - role: k8s-server
      tags: k8ss
```

Playbook にタグを付与したので、タグを指定して追加した Playbook のみ実行します。

```console:作業サーバ：rp01
$ ansible-playbook -i inventory -t k8ss cp.yml
PLAY [cp] ***************************************************************************************
・・・
PLAY RECAP **************************************************************************************
cp01    : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
cp02    : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
cp03    : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$ ansible-playbook -i inventory -t k8ss nd.yml
PLAY [nd] ***************************************************************************************
・・・
PLAY RECAP **************************************************************************************
nd01    : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
nd02    : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

```kubeadm``` の実行前の準備が整いました。

## k8s-client
```kubeadm``` を実行する前に もう一つ Kubenetes を操作するクライアントツール（```kubectl```）を ```rp01``` にインストールしておきます。

```
_/_/_/ k8s-client ロールのディレクトリ構成 _/_/_/ 
kubeadm/
　└ roles/
　   └ k8s-client/               ：Kubernetes クライアント（kubectl）のロール
　      └ tasks/main.yml         ：Playbook
```

Playbook では ```kubectl``` のインストールと合わせて、エイリアス（```kc```）と補完用の設定を追記します。
※```blockinfile``` モジュールの中で ```.bashrc``` に設定を追記しています。適宜 ```path``` は変更ください。

> Ansible：[blockinfile](https://docs.ansible.com/ansible/2.9/modules/blockinfile_module.html)

```yaml:roles/k8s-client/tasks/main.yml
---
# tasks file for k8s-client
- name: Add apt-key for kubernetes
  apt_key:
    url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
    state: present

- name: Add apt repository for kubernetes
  apt_repository:
    repo: deb https://apt.kubernetes.io/ kubernetes-xenial main
    state: present

- name: Install kubectl
  apt:
    pkg:
    - kubectl
    update_cache: yes
    state: present

- name: Add kubectl alias(kc) and register completion
  blockinfile:
    path: /home/vagrant/.bashrc
    block: |
      source <(kubectl completion bash)
      alias kc=kubectl
      complete -F __start_kubectl kc
    insertafter: EOF
```

```rp.yml``` に追加した Role を追記して実行します。

```yaml:rp.yml
---
# file: rp.yml
- hosts: rp
  gather_facts: no
  become: yes
  roles:
    - role: common
      tags: cmn
    - role: reverseproxy
      tags: rp
    - role: k8s-client
      tags: k8sc
```

タグを指定して実行します。

```console:作業サーバ：rp01
$ ansible-playbook -i inventory -t k8sc rp.yml
PLAY [rp] ***************************************************************************************
・・・
PLAY RECAP **************************************************************************************
rp01     : ok=4    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

## Playbooks
今まで作成した資材を以下のとおりです。

```
kubeadm/
　├ ansible.cfg                  ：Ansible 設定ファイル
　├ inventory                    ：接続先情報ファイル
　├ rp.yml                       ：リバースプロキシ実行用 Playbook
　├ cp.yml                       ：コントロールプレーン実行用 Playbook
　├ nd.yml                       ：ノード実行用 Playbook
　└ roles/
　   ├ common/                   ：共通設定のロール
　   │  └ tasks/main.yml
　   ├ reverseproxy/             ：リバースプロキシのロール
　   │ ├ files/nginx.conf
　   │ ├ handlers/main.yml
　   │ └ tasks/main.yml
　   ├ cri/                      ：CRI（containerd）のロール
     │ ├ files/containerd.conf
　   │ └ tasks/main.yml
　   ├ k8s-server/               ：Kubernetes サーバ（kubelet / kubeadm ）のロール
　   │ └ tasks/main.yml
　   └ k8s-client/               ：Kubernetes クライアント（kubectl）のロール
　      └ tasks/main.yml
```

# kubeadm でクラスタ構築
```kubeadm``` の実行準備ができたのでコントロールプレーンから構築していきます。

先んじてエラー対応のために、```containerd``` の初期設定ファイルの削除とデーモンの再起動を行います。

> https://github.com/containerd/containerd/issues/4581

```console:作業サーバ：rp01
# file モジュールを使って設定ファイル削除
$ ansible -i inventory cp,nd -m file -a "path=/etc/containerd/config.toml state=absent" --become

nd02 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "path": "/etc/containerd/config.toml",
    "state": "absent"
}
・・・

# systemd モジュールを使ってデーモン再起動
$ ansible -i inventory cp,nd -m systemd -a "name=containerd state=restarted" --become

cp03 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "name": "containerd",
    "state": "started",
    "status": {
・・・
```

最初の一台目のコントロールプレーンを構築します。
```cp01``` にログインし、```kubeadm``` を実行します。

```console:作業サーバ：cp01
_/_/_/ 一台目のコントロールプレーンを構築 _/_/_/
# --control-plane-endpoint：コントロールプレーンのエンドポイント
# ⇒ 今回はリバースプロキシのサーバを経由してコントロールプレーンへのリクエストを受けるため指定
# --pod-network-cidr：Pod に割り振られる IP アドレスレンジ
# ⇒ デフォルトだと LAN と被ったので指定
# --apiserver-advertise-address：API サーバーが待ち受けする IP アドレス
# ⇒ Vagrant デフォルトだと NAT の NIC で動作不良起こしそうだったので、ブリッジの NIC を指定
# --upload-certs：コントロールプレーンノードで共有する必要がある証明書をクラスタにアップロード（Secret）
# ⇒ 複数コントロールプレーンノードを作成する場合は指定（手動で証明書配布する場合は不要）
# ⇒ インストール出力を見るに有効期限は2時間？
# 参考：https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/high-availability/#manual-certs

$ sudo kubeadm init \
--control-plane-endpoint "192.168.10.51:6443" \
--pod-network-cidr 172.16.0.0/16 \
--apiserver-advertise-address 192.168.10.61 \
--upload-certs

[init] Using Kubernetes version: v1.20.2
・・・
Your Kubernetes control-plane has initialized successfully!

・・・

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

  kubeadm join 192.168.10.51:6443 --token xxxx \
    --discovery-token-ca-cert-hash sha256:xxxx \
    --control-plane --certificate-key xxxx

・・・

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.10.51:6443 --token xxxx \
    --discovery-token-ca-cert-hash sha256:xxxx

```

１～３分ほどで無事にインストールができると思います。
出力内容のトークンなどはこの後利用するので、結果は保存しておきましょう。

続いてネットワークプラグイン（Project Calico）をインストールします。
インストールには ```kubectl``` を使うため、接続用の設定ファイルを cp01 から取得し rp01 に配置します。

```console:作業サーバ：rp01
# Kubernetes に接続する際の設定ファイルを取得
$ mkdir ~/.kube
$ ssh 192.168.10.61 "sudo cat /etc/kubernetes/admin.conf" > ~/.kube/config

$ kubectl get node
NAME   STATUS     ROLES                  AGE   VERSION
cp01   NotReady   control-plane,master   30m   v1.20.2

# ネットワークプラグインのデプロイ
$ kubectl apply -f https://docs.projectcalico.org/v3.17/manifests/calico.yaml
・・・
daemonset.apps/calico-node created
・・・

$ kubectl get node
NAME   STATUS   ROLES                  AGE   VERSION
cp01   Ready    control-plane,master   32m   v1.20.2
```

1台目のコントロールプレーンが無事に起動できたところで、2台目 / 3台目を追加します。
実行用のコマンドは1台目をインストールした際に出力されています。今回はそちらに ```--apiserver-advertise-address ``` オプションで IP アドレスを指定しています（以下コマンドは改行をいれて整形しています）。
```cp02```、```cp03``` にログインし実行していきます。

```console:作業サーバ：cp02/cp03
# cp02
sudo kubeadm join 192.168.10.51:6443 \
--token xxxx \
--discovery-token-ca-cert-hash sha256:xxxx \
--certificate-key xxxx \
--control-plane \
--apiserver-advertise-address 192.168.10.62

# cp03
sudo kubeadm join 192.168.10.51:6443 \
--token xxxx \
--discovery-token-ca-cert-hash sha256:xxxx \
--certificate-key xxxx \
--control-plane \
--apiserver-advertise-address 192.168.10.63
```

コントロールプレーンが HA 構成の３台で稼働できたことを確認します。

```console:作業サーバ：rp01
$ kubectl get node
NAME   STATUS   ROLES                  AGE     VERSION
cp01   Ready    control-plane,master   41m     v1.20.2
cp02   Ready    control-plane,master   2m49s   v1.20.2
cp03   Ready    control-plane,master   77s     v1.20.2
```

次にノードを追加します。こちらもアドレスを指定して実行します。実行コマンドはコントロールプレーンの1台目をインストールした時に出力されています。コントロールプレーンの2台目、3台目をインストールした時と同様に IP アドレスを指定して実行します。

```console:作業サーバ：nd01/nd02
# nd01
$ sudo kubeadm join 192.168.10.51:6443 \
--token xxxx \
--discovery-token-ca-cert-hash sha256:xxxx \
--apiserver-advertise-address 192.168.10.71

# nd02
$ sudo kubeadm join 192.168.10.51:6443 \
--token xxxx \
--discovery-token-ca-cert-hash sha256:xxxx \
--apiserver-advertise-address 192.168.10.72
```

ノードが２台構成で稼働していることを確認します。

```console:作業サーバ：rp01
$ kc get node -o wide
NAME   STATUS   ROLES                  AGE     VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION       CONTAINER-RUNTIME
cp01   Ready    control-plane,master   9m30s   v1.20.2   192.168.10.61   <none>        Ubuntu 18.04.5 LTS   4.15.0-124-generic   containerd://1.4.3
cp02   Ready    control-plane,master   5m3s    v1.20.2   192.168.10.62   <none>        Ubuntu 18.04.5 LTS   4.15.0-124-generic   containerd://1.4.3
cp03   Ready    control-plane,master   3m51s   v1.20.2   192.168.10.63   <none>        Ubuntu 18.04.5 LTS   4.15.0-124-generic   containerd://1.4.3
nd01   Ready    <none>                 2m45s   v1.20.2   192.168.10.71   <none>        Ubuntu 18.04.5 LTS   4.15.0-124-generic   containerd://1.4.3
nd02   Ready    <none>                 99s     v1.20.2   192.168.10.72   <none>        Ubuntu 18.04.5 LTS   4.15.0-124-generic   containerd://1.4.3
```

# 動作確認
クラスタが構築できたので動作確認をします。

```
# Pod 作成
$ kubectl run nginx --image=nginx --port=80
pod/nginx created

$ kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP             NODE   NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          22s   172.16.246.1   nd01   <none>           <none>

# Pod 内でバージョン確認
$ kubectl exec nginx -- nginx -v
nginx version: nginx/1.19.6

# Service（NodePort）を公開
$ kubectl expose pod nginx --type=NodePort
service/nginx exposed

# Worker の IP、Service の NodePort を取得して Pod にアクセス
$ IP=`kubectl get node nd01 -o=jsonpath='{.status.addresses[?(@.type == "InternalIP")].address}'`
$ PORT=`kubectl get svc nginx -o yaml -o=jsonpath='{.spec.ports[0].nodePort}'`

$ curl -I http://$IP:$PORT/
HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Fri, 12 Feb 2021 17:18:00 GMT
Content-Type: text/html
・・・
```

# まとめ
長いハンズオンお疲れさまでした。Ansible でコード化することで、複数サーバで実行しなければならないコマンドも一発で実行することができたかと思います。また、１度作成したコード群は追加サーバのセットアップなど、その後のクラスタ構築の時間を短縮することができます。ひいてはいつでも壊せるクラスタを構築することができるのではと思います。
今回はコントロールプレーンやノードに特別な設定を加えていませんが、OS の設定（ユーザ追加やディスク管理）や個々のサーバへの設定を追加していくこともできるので応用の幅は広いかと思います。
```kubeadm``` って本当に便利ですね、開発者に感謝です。

参考
> [kubeadmを使ってクラスターを構築する](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/)
> [kubeadmを使用した高可用性クラスターの作成](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/high-availability/)