Kubespray を使った Kubernetes クラスターの作り方

# はじめに

この記事では **kubespray** を使用してKubernetes クラスターをインストールする方法について説明します．

仮想マシンを使用するため，**Vagrant** および **VirtualBox** を使います．

注意：泥臭い作業やコードが多く含まれます．

## 環境

- Ubuntu 20.04
- Kubespray v2.15.1
- Ansible v2.9.20
- VirtualBox v6.1.16
- Vagrant v2.2.6


# 0. 準備

Vagrantがない場合はインストールします．Vagrant以前にVirtualBoxもない場合はインストールしてください．

## 0.1 VirtualBoxのインストール

```bash
sudo apt install virtualbox
```


## 0.2 Vagrantのインストール

```bash
sudo apt install vagrant
```

バージョンを確認

```bash
vagrant --version
```

vagrantの使い方については例えば以下を参照してください．


- [Vagrant入門から実践まで](https://qiita.com/daichi87gi/items/d5da33c76295ee32a735)





# 1. プロビジョニング

大量の物理マシンがあれば使いたいところですが，無いのでVM（仮想マシン）を使ってクラスターを構築します．

以下では7つのVM（仮想マシン）をセットアップします．

|                                         | IP                              | CPU | メモリ | 
| --------------------------------------- | ------------------------------- | --- | ------ | 
| Ansibleマスターノード（amaster）        | 100.0.0.1                       | 1   | 2GB    | 
| Kubernetesマスターノード（kmaster1〜3） | 100.0.0.2, 100.0.0.3, 100.0.0.4 | 1   | 2GB    | 
| Kubernetesワーカーノード（kworker1〜3） | 100.0.0.5, 100.0.0.6, 100.0.0.7 | 1   | 2GB    | 

もしリソースに余裕があれば，CPUを2以上にしたり，メモリを増やすことも可能です．

## 1.1 Vagrantfileの作成

適当な作業ディレクトリを作成し，その中に以下のVagratnfileを作成します．（for文で書くともっと短く書けます）

```ruby:Vagratnfile
Vagrant.configure("2") do |config|
  config.vm.define "amaster" do |amaster|
    amaster.vm.box_download_insecure = true
    amaster.vm.box = "hashicorp/bionic64"
    amaster.vm.network "private_network", ip: "100.0.0.1"
    amaster.vm.hostname = "amaster"
    amaster.vm.provider "virtualbox" do |v|
      v.name = "amaster"
      v.memory = 2048
      v.cpus = 1
    end
  end

  config.vm.define "kmaster1" do |kmaster|
    kmaster.vm.box_download_insecure = true
    kmaster.vm.box = "hashicorp/bionic64"
    kmaster.vm.network "private_network", ip: "100.0.0.2"
    kmaster.vm.hostname = "kmaster1"
    kmaster.vm.provider "virtualbox" do |v|
      v.name = "kmaster1"
      v.memory = 2048
      v.cpus = 1
    end
  end

  config.vm.define "kmaster2" do |kmaster|
    kmaster.vm.box_download_insecure = true
    kmaster.vm.box = "hashicorp/bionic64"
    kmaster.vm.network "private_network", ip: "100.0.0.3"
    kmaster.vm.hostname = "kmaster2"
    kmaster.vm.provider "virtualbox" do |v|
      v.name = "kmaster2"
      v.memory = 2048
      v.cpus = 1
    end
  end

  config.vm.define "kmaster3" do |kmaster|
    kmaster.vm.box_download_insecure = true
    kmaster.vm.box = "hashicorp/bionic64"
    kmaster.vm.network "private_network", ip: "100.0.0.4"
    kmaster.vm.hostname = "kmaster3"
    kmaster.vm.provider "virtualbox" do |v|
      v.name = "kmaster3"
      v.memory = 2048
      v.cpus = 1
    end
  end

  config.vm.define "kworker1" do |kworker|
    kworker.vm.box_download_insecure = true
    kworker.vm.box = "hashicorp/bionic64"
    kworker.vm.network "private_network", ip: "100.0.0.5"
    kworker.vm.hostname = "kworker1"
    kworker.vm.provider "virtualbox" do |v|
      v.name = "kworker1"
      v.memory = 2048
      v.cpus = 1
    end
  end

  config.vm.define "kworker2" do |kworker|
    kworker.vm.box_download_insecure = true
    kworker.vm.box = "hashicorp/bionic64"
    kworker.vm.network "private_network", ip: "100.0.0.6"
    kworker.vm.hostname = "kworker2"
    kworker.vm.provider "virtualbox" do |v|
      v.name = "kworker2"
      v.memory = 2048
      v.cpus = 1
    end
  end

  config.vm.define "kworker3" do |kworker|
    kworker.vm.box_download_insecure = true
    kworker.vm.box = "hashicorp/bionic64"
    kworker.vm.network "private_network", ip: "100.0.0.7"
    kworker.vm.hostname = "kworker3"
    kworker.vm.provider "virtualbox" do |v|
      v.name = "kworker3"
      v.memory = 2048
      v.cpus = 1
    end
  end

end
```

コマンド `vagrant up` でVMインスタンスを作成します．

```bash
vagrant up
```

数分かかります．

## 1.2 hostsファイルの編集

VMインスタンスの数ぶんのターミナルを開いて，すべてのVMインスタンスにssh接続しておきます．

```bash
vagrant ssh amaster
vagrant ssh kmaster1
vagrant ssh kmaster2
vagrant ssh kmaster3
vagrant ssh kworker1
vagrant ssh kworker2
vagrant ssh kworker3
```

次に，すべてのVMインスタンスで次のコマンドを実行します．

```bash
sudo cat <<EOF | sudo tee -a /etc/hosts
100.0.0.1 amaster
100.0.0.2 kmaster1
100.0.0.3 kmaster2
100.0.0.4 kmaster3
100.0.0.5 kworker1
100.0.0.6 kworker2
100.0.0.7 kworker3
EOF
```

これが何をやっているかというと，`/etc/hosts`ファイルに以下の内容を追記しているだけです．

```bash
100.0.0.1 amaster
100.0.0.2 kmaster1
100.0.0.3 kmaster2
100.0.0.4 kmaster3
100.0.0.5 kworker1
100.0.0.6 kworker2
100.0.0.7 kworker3
```


例えば，`amaster`ノードでのhostsファイルは以下のようになります．

```bash
$ cat /etc/hosts
127.0.0.1	localhost
127.0.1.1	amaster	amaster

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
100.0.0.1 amaster
100.0.0.2 kmaster1
100.0.0.3 kmaster2
100.0.0.4 kmaster3
100.0.0.5 kworker1
100.0.0.6 kworker2
100.0.0.7 kworker3
```

## 1.3 Ansibleを使えるようにするためのSSHまわりの準備（amasterでのみ実行）

鍵認証によるログインが必要ですので，公開鍵と秘密鍵を作成します（パスフレーズ等はセキュリティ等の理由がなければ空のままでよいです）．

```
$ ssh-keygen -t rsa

Generating public/private rsa key pair.
Generating public/private rsa key pair.
Enter file in which to save the key (/home/vagrant/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/vagrant/.ssh/id_rsa.
Your public key has been saved in /home/vagrant/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:5U39LgrybQyluHATriD5LzgAZ57qJnSDz1g1O0CnETY vagrant@amaster
The key's randomart image is:
+---[RSA 2048]----+
|   E.            |
|  .o..       .   |
|  . +     . . .  |
|. oo o  .o o.  . |
|.+.oo o.So.o.   .|
|.o=+.o. = o    . |
|.o*+...+.o.o  . .|
|ooooo . .o oo. . |
|+. . o.   ..o    |
+----[SHA256]-----+
vagrant@amaster:~$ 
```

#### 公開鍵の登録

生成した公開鍵（`id_rsa.pub`）を他のノードにコピーします．

例えば，`kmaster1`へコピーするときは以下のようにします．

```bash
$ ssh-copy-id 100.0.0.2
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/vagrant/.ssh/id_rsa.pub"
The authenticity of host '100.0.0.2 (100.0.0.2)' can't be established.
ECDSA key fingerprint is SHA256:uY6GIjFdI9qTC4QYb980QRk+WblJF9cd5glr3SmmL+w.
Are you sure you want to continue connecting (yes/no)? 
```

接続を続けますか？と聞かれるので，`yes`と入力．

```bash
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
vagrant@100.0.0.2's password: 
```

ユーザー名`vagrant`に対するパスワードを聞かれます．パスワードは`vagrant`です．

```bash
Number of key(s) added: 1

Now try logging into the machine, with:   "ssh '100.0.0.2'"
and check to make sure that only the key(s) you wanted were added.
```

公開鍵のコピー完了です．

他のノードに対しても同様の作業を行ってください：

```bash
ssh-copy-id 100.0.0.3
ssh-copy-id 100.0.0.4
ssh-copy-id 100.0.0.5
ssh-copy-id 100.0.0.6
ssh-copy-id 100.0.0.7
```

#### `ssh-copy-id`が面倒な場合

Ansibleの`authorized_key`というモジュールで公開鍵を配布することができます．

例えば次のplaybook.ymlのようなplaybookを実行すると配れます。

```yaml:playbook.yml
- hosts: all
  tasks:
    - name: add authorized_key
      authorized_key:
        user: "{{ user }}"
        key: "{{ lookup('file', '/home/{{user}}/.ssh/id_rsa.pub') }}"
```

```bash:コマンド
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i <inventory> playbook.yml -k
```

環境変数`ANSIBLE_HOST_KEY_CHECKING`はsshで新規ホスト接続時に出る，ホスト鍵追加のプロンプトを抑制するためのものです．

#### 参考

- [Ansible の実行以前の話](https://medium.com/makotows-blog/ansible-%E3%81%AE%E5%AE%9F%E8%A1%8C%E4%BB%A5%E5%89%8D%E3%81%AE%E8%A9%B1-f2fcffc94a96)
- [ansibleを使うためのssh周りの設定](http://kzdev.hatenablog.com/entry/2018/08/04/012634)


# 2. Kubesprayによるkubernetesクラスターのセットアップ

## 2.1 kubespray 実行に必要なパッケージのインストール（amasterでのみ実行）

年のため，次のコマンドを実行します（すべてのノードで実行）

```bash
sudo apt-get update
```

python3-pipをインストールします（amasterでのみ実行）


```bash
sudo apt install -y python3-pip
```

インストールされたか確認します．

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

#### kubespray のリポジトリのクローン（amasterでのみ実行）

```bash
git clone https://github.com/kubernetes-sigs/kubespray.git
```

#### `requirement.txt` パッケージをインストール（amasterでのみ実行）

「kubespray」ディレクトリに移動


```bash
cd kubespray
sudo pip3 install -r requirements.txt
```

### インベントリファイルのコピー（amasterでのみ実行）

次のコマンドを使用してインベントリファイルをコピーします．

```bash
cp -rfp inventory/sample inventory/mycluster
```

#### `host.yml`の準備（amasterでのみ実行）

他のノードのIPアドレスを格納するための変数`IPS`を宣言します．

```bash
declare -a IPS=(100.0.0.2 100.0.0.3 100.0.0.4 100.0.0.5 100.0.0.6 100.0.0.7)
CONFIG_FILE=inventory/mycluster/hosts.yml python3 contrib/inventory_builder/inventory.py ${IPS[@]}
```

上記のコマンドを実行後，`hosts.yml`を確認すると，次のようになっています．

```bash
$ cat inventory/mycluster/hosts.yml
all:
  hosts:
    node1:
      ansible_host: 100.0.0.2
      ip: 100.0.0.2
      access_ip: 100.0.0.2
    node2:
      ansible_host: 100.0.0.3
      ip: 100.0.0.3
      access_ip: 100.0.0.3
    node3:
      ansible_host: 100.0.0.4
      ip: 100.0.0.4
      access_ip: 100.0.0.4
    node4:
      ansible_host: 100.0.0.5
      ip: 100.0.0.5
      access_ip: 100.0.0.5
    node5:
      ansible_host: 100.0.0.6
      ip: 100.0.0.6
      access_ip: 100.0.0.6
    node6:
      ansible_host: 100.0.0.7
      ip: 100.0.0.7
      access_ip: 100.0.0.7
  children:
    kube_control_plane:
      hosts:
        node1:
        node2:
    kube_node:
      hosts:
        node1:
        node2:
        node3:
        node4:
        node5:
        node6:
    etcd:
      hosts:
        node1:
        node2:
        node3:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
    calico_rr:
      hosts: {}
```

このファイルはKubernetesのインストールで使用することになります．このまま使用しても良いのですが，欲しい構成と異なるため，編集しておきます．

編集の前に，yamlの構成を見ておきます．階層構造を見やすくすると，以下のようになっていることがわかります．

```
all:
  hosts:
    node1:
    node2:
    node3:
    node4:
    node5:
    node6:
  children:
    kube_control_plane:
    kube-node:
    etcd:
    k8s-cluster:
      children:
        kube-master:
        kube-node:
    calico-rr:
```

各項目を簡単に解説します．

- `hosts`: kubernetesクラスタに含まれるマシンを列挙します
- `children`: Kubernetesのマスター・ワーカーノードなどの設定を行います
    - `kube_control_plane`: マスターとなるマシンを列挙します
    - `kube-node`: ワーカーとなるマシンを列挙します
    - `etcd`: etcdをインストールするマシンを列挙します
    - `k8s-cluster`: `children`としてKubernetesを構成する`kube-master`, `kube-node`を記述します．ここは編集しなくても大丈夫です．
    - `calico-rr`は[calico](https://www.projectcalico.org/)の使用に関する設定です．Kubernetesのネットワークポリシーの実装に使用されるらしいのですが，詳しくは存じません．この記事の範疇では編集しなくて大丈夫です．

#### `host.yml`の編集（amasterでのみ実行）

いま，我々はマスターノードが３つ欲しいので，`kube_control_plane`に`node3`を追加してください:

```
...
  children:
    kube_control_plane:
      hosts:
        node1:
        node2:
        node3:   # here!
    kube_node:
      hosts:
...
```

## 2.2 ansible-playbookの実行（amasterでのみ実行）

以上で準備完了です．

以下のコマンドでKubesprayによるkubernetesのインストールを実行します．

```
ansible-playbook -i inventory/mycluster/hosts.yml --become --become-user=root cluster.yml
```

時間がかかるのでしばらくお待ちください．（ネットワーク帯域幅にも依存しますが，概ね１５〜３０分前後かかりました）


## 2.3 `kubectl`のインストール（kmasterでのみ実行）

[kubernetes公式](https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/)にしたがうならば，どのインストール方法でもよいと思われますが，私の環境でうまくいったコマンド例を掲載します．

kmaster1~3で以下を実行します．

```bash
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
```

次に`admin.conf`ファイルを`.kube`にコピーします．

```bash
sudo cp /etc/kubernetes/admin.conf /home/vagrant/config
mkdir .kube
mv config .kube/
sudo chown $(id -u):$(id -g ) $HOME/.kube/config
```

インストール後にkubectlのバージョンを確認してください

```
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.0", GitCommit:"cb303e613a121a29364f75cc67d3d580833a7479", GitTreeState:"clean", BuildDate:"2021-04-08T16:31:21Z", GoVersion:"go1.16.1", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.6", GitCommit:"8a62859e515889f07e3e3be6a1080413f17cf2c3", GitTreeState:"clean", BuildDate:"2021-04-15T03:19:55Z", GoVersion:"go1.15.10", Compiler:"gc", Platform:"linux/amd64"}
```

以上でkubesprayによるkubernetesのインストールは完了です．（超簡単ですね！）

# 3 kubernetesの動作確認

この章ではkubesprayとは関係なく，kubernetesの基本的な動作確認を行います．

まずはノードを確認（kmaster1~3で実行）

```
$ kubectl get nodes
NAME    STATUS   ROLES                  AGE     VERSION
node1   Ready    control-plane,master   9m10s   v1.20.6
node2   Ready    control-plane,master   8m23s   v1.20.6
node3   Ready    <none>                 7m21s   v1.20.6
node4   Ready    <none>                 7m21s   v1.20.6
node5   Ready    <none>                 7m21s   v1.20.6
node6   Ready    <none>                 7m21s   v1.20.6

```

Deploymentも試してみましょう．`deployment.yaml`を作成します．

```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
```

deploymentを作成して挙動を確認します．

```bash
$ kubectl apply -f deployment.yaml
deployment.apps/nginx-deployment created

$ kubectl get deployment,pod
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-deployment   3/3     3            3           21s

NAME                                   READY   STATUS    RESTARTS   AGE
pod/nginx-deployment-db749865c-h4zpd   1/1     Running   0          21s
pod/nginx-deployment-db749865c-rxk72   1/1     Running   0          21s
pod/nginx-deployment-db749865c-s7wmn   1/1     Running   0          21s
```

Deploymentは3つのPodが稼働している状態を保とうとするはずです．試しに1つ削除してみましょう．

```
$ kubectl delete pod nginx-deployment-db749865c-h4zpd
pod "nginx-deployment-db749865c-h4zpd" deleted
$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
nginx-deployment-db749865c-bkpnf   1/1     Running   0          12s
nginx-deployment-db749865c-rxk72   1/1     Running   0          67s
nginx-deployment-db749865c-s7wmn   1/1     Running   0          67s
```

完璧ですね！うまく動いているようです．

# 4. kubesprayによるクラスターの操作

ここではクラスターの基本的な操作をメモします．

詳細は[公式のGetting started](https://kubespray.io/#/docs/getting-started)を参照してください．

## 4.1 ノードの追加

`hosts.yml`に書かれているノードのうち，足りないものが自動的に追加される

```
ansible-playbook -i inventory/mycluster/hosts.yml scale.yml -b -v
```


## 4.2 ノードの削除

- `node5`のようにノード名を指定する．（`kworker2`のようにansibleにとっての識別名ではないので注意）
- 公式では`--private-key=~/.ssh/private_key`のように秘密鍵を指定していたけど，なくても実行できてしまった．なぜだか理解できていない

```
ansible-playbook -i inventory/mycluster/hosts.yml remove-node.yml -b -v \
--extra-vars "node=node5"
```

# 5. 発展的な作業

#### metalLBの導入

クラウドでのkubernetesのマネージドサービスだと，気軽に`type:LoadBalancer Service`などを使ってしまうのですが，オンプレだとロードバランサを作らないと使えません．

ベアメタル上にk8s環境を構築している場合は **MetalLB** を導入することで，service や ingress で`type:LoadBalancer`を使えるようになります．

導入に成功し次第，この記事にまとめます．

導入方法について解説されているページは以下があるようです．

- [Kubesprayで準備したk8sにMetalLBをデプロイしてみる](https://qiita.com/YasuhiroABE/items/aa15567ac61b063cfcd4)
- [Deploy MetalLB into Kubespray/Kubernetes](https://github.com/kubernetes-sigs/kubespray/blob/360aff4a571f4a8ac7603a4a68223e744fad1209/roles/kubernetes-apps/metallb/README.md)