VagrantでkubeadmでKubernetesを起動する

今更ながらのVagrantの勉強を兼ね、`vagrant up`でKubernetesクラスターを簡単に起動するための`Vagrantfile`を作成したのでメモ。

## 参考資料

- [Multi Node Kubernetes Cluster with Vagrant, VirtualBox and Kubeadm](https://medium.com/@wso2tech/multi-node-kubernetes-cluster-with-vagrant-virtualbox-and-kubeadm-9d3eaac28b98)
- [Kubernetes v1.10 クラスタをVagrantで構築したメモ](https://qiita.com/MahoTakara/items/28cd766d0447140b7ae3)
- [Get Docker CE for Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/)
- [Get Docker CE for CentOS](https://docs.docker.com/install/linux/docker-ce/centos/)
- [Creating a single master cluster with kubeadm](https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/)

## Vagrantfile

Masterはシングルで複数NodeのKubernetesクラスターを作成する。
注意すべき点は以下。

- 各VMはNAT用に`10.0.2.15`という同じIP持っており、apiserverがそちらにバインドされてしまうため、`kubeadm init`で`--apiserver-advertise-address`などを引数に設定
- 同じくkubeletも`10.0.2.15`にバインドされてしまうため、kubeletの引数に`--nodeip`を指定する必要があり、そのためkubeletサービスのSystemdユニット定義から参照される`/etc/default/kubelet`（Ubuntu）または`/etc/sysconfig/kubelet`（CentOS）を修正する
- Masterノード上で生成した`kubeadm join`コマンドを、各Nodeはscpで取得して実行

（追記）以下に記載のものからKubernetesのバージョンアップへの対応などちょいちょい変更をしています。

- [Ubuntu版](https://github.com/sotoiwa/vagrant-k8s-ubuntu)
- [CentOS版](https://github.com/sotoiwa/vagrant-k8s-centos)

### Ubuntu

ネットワークプラグインはFlannelを使ってみる。

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

$configureBox = <<-SHELL

  # パッケージ更新
  apt-get update
  apt-get upgrade -y

  # Dockerの前提パッケージ
  apt-get install -y apt-transport-https ca-certificates curl software-properties-common
  # Dockerのレポジトリ追加
  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"
  # Dockerのインストール
  apt-get update
  apt-get install -y docker-ce=$(apt-cache madison docker-ce | grep 17.03 | head -1 | awk '{print $3}')
  apt-mark hold docker-ce
  # vagrantユーザーをdockerグループに追加
  usermod -aG docker vagrant

  # Flannelの場合に必要
  # デフォルトが1なのでコメントアウト
  # echo net.bridge.bridge-nf-call-iptables = 1 >> /etc/sysctl.conf
  # sysctl -p

  # スワップを無効化する
  # スワップ領域がないのでコメントアウト
  # swapoff -a
  # プロビジョニングで実行する場合はバックスラッシュのエスケープが必要なことに注意
  # sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
  # sed -i '/ swap / s/^\\(.*\\)$/#\\1/g' /etc/fstab

  # Kubernetesの前提パッケージ
  # apt-get update
  # apt-get install -y apt-transport-https curl
  # Kubernetesのレポジトリ追加
  curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
  cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
  # kubeadm、kubelet、kubectlのインストール
  apt-get update
  # apt-get install -y kubelet=1.12.2-00 kubeadm=1.12.2-00 kubectl=1.12.2-00
  apt-get install -y kubelet kubeadm kubectl
  apt-mark hold kubelet kubeadm kubectl

  # プライベートネットワークのNICのIPアドレスを変数に格納
  IPADDR=$(ip a show enp0s8 | grep inet | grep -v inet6 | awk '{print $2}' | cut -f1 -d/)
  # kubeletがプライベートネットワークのNICにバインドするように設定
  sed -i "/KUBELET_EXTRA_ARGS=/c\KUBELET_EXTRA_ARGS=--node-ip=$IPADDR" /etc/default/kubelet
  # kubeletを再起動
  systemctl daemon-reload
  systemctl restart kubelet

SHELL

$configureMaster = <<-SHELL

  echo "This is master"

  # プライベートネットワークのNICのIPアドレスを変数に格納
  IPADDR=$(ip a show enp0s8 | grep inet | grep -v inet6 | awk '{print $2}' | cut -f1 -d/)
  # ホスト名を変数に格納
  HOSTNAME=$(hostname -s)

  # kubeadm initの実行
  # Flannelの場合
  kubeadm init --apiserver-advertise-address=$IPADDR --apiserver-cert-extra-sans=$IPADDR --node-name $HOSTNAME --pod-network-cidr=10.244.0.0/16
  # Calicoの場合
  # kubeadm init --apiserver-advertise-address=$IPADDR --apiserver-cert-extra-sans=$IPADDR --node-name $HOSTNAME --pod-network-cidr=192.168.0.0/16

  # vagrantユーザーがkubectlを実行できるようにする
  sudo --user=vagrant mkdir -p /home/vagrant/.kube
  cp -i /etc/kubernetes/admin.conf /home/vagrant/.kube/config
  chown $(id -u vagrant):$(id -g vagrant) /home/vagrant/.kube/config

  # Flannelのインストール
  export KUBECONFIG=/etc/kubernetes/admin.conf
  kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml
  # Calicoのインストール
  # export KUBECONFIG=/etc/kubernetes/admin.conf
  # kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/rbac-kdd.yaml
  # kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yaml

  # kubectl joinコマンドを保存する
  kubeadm token create --print-join-command > /etc/kubeadm_join_cmd.sh
  chmod +x /etc/kubeadm_join_cmd.sh

  # sshでのパスワード認証を許可する
  sed -i "/^[^#]*PasswordAuthentication[[:space:]]no/c\PasswordAuthentication yes" /etc/ssh/sshd_config
  systemctl restart sshd

SHELL

$configureNode = <<-SHELL

  echo "This is worker"

  apt-get install -y sshpass
  sshpass -p "vagrant" scp -o StrictHostKeyChecking=no vagrant@192.168.33.11:/etc/kubeadm_join_cmd.sh .
  # sshpass -p "vagrant" scp -o StrictHostKeyChecking=no vagrant@172.16.33.11:/etc/kubeadm_join_cmd.sh .
  sh ./kubeadm_join_cmd.sh

SHELL

Vagrant.configure(2) do |config|

  (1..3).each do |i|

    if i == 1 then
      vm_name = "master"
    else
      vm_name = "node#{i-1}"
    end
      
    config.vm.define vm_name do |s|

      # ホスト名
      s.vm.hostname = vm_name
      # ノードのベースOSを指定
      s.vm.box = "ubuntu/xenial64"
      # ネットワークを指定
      # pod-network-cidrと重ならないように注意
      private_ip = "192.168.33.#{i+10}"
      # private_ip = "172.16.33.#{i+10}"
      s.vm.network "private_network", ip: private_ip

      # ノードのスペックを指定
      s.vm.provider "virtualbox" do |v|
        v.gui = false        
        if i == 1 then
          v.cpus = 2
          v.memory = 1024
        else
          v.cpus = 1
          v.memory = 1024
        end
      end

      # 共通のプロビジョニング
      s.vm.provision "shell", inline: $configureBox

      if i == 1 then
        # Masterのプロビジョニング
        s.vm.provision "shell", inline: $configureMaster
      else
        # Nodeのプロビジョニング
        s.vm.provision "shell", inline: $configureNode
      end
    
    end
  end
end
```

### CentOS

こちらはネットワークプラグインはCalicoを使ってみる。CalicoのPodネットワーク`192.168.0.0/16`と重ならないようにするため、VirtualBoxのプライベートネットワークは`172.16.33.0/24`を使用する。

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

$configureBox = <<-SHELL

  # パッケージ更新
  yum update -y

  # Dockerの前提パッケージ
  yum install -y yum-utils device-mapper-persistent-data lvm2
  # Dockerのレポジトリ追加
  yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  # Dockerのインストール
  VERSION=$(yum list docker-ce --showduplicates | sort -r | grep 17.03 | head -1 | awk '{print $2}')
  yum install -y --setopt=obsoletes=0 docker-ce-$VERSION docker-ce-selinux-$VERSION
  systemctl enable docker && systemctl start docker
  # vagrantユーザーをdockerグループに追加
  usermod -aG docker vagrant

  # スワップを無効化する
  swapoff -a
  # プロビジョニングで実行する場合はバックスラッシュのエスケープが必要なことに注意
  # sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
  sed -i '/ swap / s/^\\(.*\\)$/#\\1/g' /etc/fstab

  # Kubernetesのレポジトリ追加
  cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kube*
EOF
  
  # SELinuxを無効化
  setenforce 0
  sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
  
  # kubeadm、kubelet、kubectlのインストール
  # yum install -y kubelet-1.12.2-0 kubeadm-1.12.2-0 kubectl-1.12.2-0 --disableexcludes=kubernetes
  yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
  systemctl enable kubelet && systemctl start kubelet

  cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
  sysctl --system

  # プライベートネットワークのNICのIPアドレスを変数に格納
  IPADDR=$(ip a show eth1 | grep inet | grep -v inet6 | awk '{print $2}' | cut -f1 -d/)
  # kubeletがプライベートネットワークのNICにバインドするように設定
  sed -i "/KUBELET_EXTRA_ARGS=/c\KUBELET_EXTRA_ARGS=--node-ip=$IPADDR" /etc/sysconfig/kubelet
  # kubeletを再起動
  systemctl daemon-reload
  systemctl restart kubelet

SHELL

$configureMaster = <<-SHELL

  echo "This is master"

  # プライベートネットワークのNICのIPアドレスを変数に格納
  IPADDR=$(ip a show eth1 | grep inet | grep -v inet6 | awk '{print $2}' | cut -f1 -d/)
  # ホスト名を変数に格納
  HOSTNAME=$(hostname -s)

  # kubeadm initの実行
  # Flannel
  # kubeadm init --apiserver-advertise-address=$IPADDR --apiserver-cert-extra-sans=$IPADDR --node-name $HOSTNAME --pod-network-cidr=10.244.0.0/16
  # Calico
  kubeadm init --apiserver-advertise-address=$IPADDR --apiserver-cert-extra-sans=$IPADDR --node-name $HOSTNAME --pod-network-cidr=192.168.0.0/16

  # vagrantユーザーがkubectlを実行できるようにする
  sudo --user=vagrant mkdir -p /home/vagrant/.kube
  cp -i /etc/kubernetes/admin.conf /home/vagrant/.kube/config
  chown $(id -u vagrant):$(id -g vagrant) /home/vagrant/.kube/config

  # Flannelのインストール
  # export KUBECONFIG=/etc/kubernetes/admin.conf
  # kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml
  # Calicoのインストール
  export KUBECONFIG=/etc/kubernetes/admin.conf
  kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/rbac-kdd.yaml
  kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yaml

  # kubectl joinコマンドを保存する
  kubeadm token create --print-join-command > /etc/kubeadm_join_cmd.sh
  chmod +x /etc/kubeadm_join_cmd.sh

  # sshでのパスワード認証を許可する
  sed -i "/^[^#]*PasswordAuthentication[[:space:]]no/c\PasswordAuthentication yes" /etc/ssh/sshd_config
  systemctl restart sshd

SHELL

$configureNode = <<-SHELL

  echo "This is worker"

  yum install -y sshpass
  # sshpass -p "vagrant" scp -o StrictHostKeyChecking=no vagrant@192.168.33.11:/etc/kubeadm_join_cmd.sh .
  sshpass -p "vagrant" scp -o StrictHostKeyChecking=no vagrant@172.16.33.11:/etc/kubeadm_join_cmd.sh .
  sh ./kubeadm_join_cmd.sh

SHELL

Vagrant.configure(2) do |config|

  (1..3).each do |i|

    if i == 1 then
      vm_name = "master"
    else
      vm_name = "node#{i-1}"
    end
      
    config.vm.define vm_name do |s|

      # ホスト名
      s.vm.hostname = vm_name
      # ノードのベースOSを指定
      s.vm.box = "centos/7"
      # ネットワークを指定
      # pod-network-cidrと重ならないように注意
      # private_ip = "192.168.33.#{i+10}"
      private_ip = "172.16.33.#{i+10}"
      s.vm.network "private_network", ip: private_ip

      # ノードのスペックを指定
      s.vm.provider "virtualbox" do |v|
        v.gui = false        
        if i == 1 then
          v.cpus = 2
          v.memory = 1024
        else
          v.cpus = 1
          v.memory = 1024
        end
      end

      # 共通のプロビジョニング
      s.vm.provision "shell", inline: $configureBox

      if i == 1 then
        # Masterのプロビジョニング
        s.vm.provision "shell", inline: $configureMaster
      else
        # Nodeのプロビジョニング
        s.vm.provision "shell", inline: $configureNode
      end
    
    end
  end
end
```

## 動作確認

### Ubuntu

```shell-session
$ vagrant up
Bringing machine 'master' up with 'virtualbox' provider...
Bringing machine 'node1' up with 'virtualbox' provider...
Bringing machine 'node2' up with 'virtualbox' provider...
==> master: Importing base box 'ubuntu/xenial64'...
==> master: Matching MAC address for NAT networking...

（省略）

    node2: Run 'kubectl get nodes' on the master to see this node join the cluster.
sotoiwa@sotonombp:~/workspace/vagrant-k8s-ubuntu
$ vagrant ssh master
Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.4.0-140-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  Get cloud support with Ubuntu Advantage Cloud Guest:
    http://www.ubuntu.com/business/services/cloud

0 packages can be updated.
0 updates are security updates.

New release '18.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.


*** System restart required ***
vagrant@master:~$ kubectl get node -o wide
NAME     STATUS   ROLES    AGE     VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
master   Ready    master   3m34s   v1.13.0   192.168.33.11   <none>        Ubuntu 16.04.5 LTS   4.4.0-140-generic   docker://17.3.3
node1    Ready    <none>   2m3s    v1.13.0   192.168.33.12   <none>        Ubuntu 16.04.5 LTS   4.4.0-140-generic   docker://17.3.3
node2    Ready    <none>   37s     v1.13.0   192.168.33.13   <none>        Ubuntu 16.04.5 LTS   4.4.0-140-generic   docker://17.3.3
vagrant@master:~$ kubectl get po --all-namespaces -o wide
NAMESPACE     NAME                             READY   STATUS    RESTARTS   AGE     IP              NODE     NOMINATED NODE   READINESS GATES
kube-system   coredns-86c58d9df4-mkg9w         1/1     Running   0          3m26s   10.244.0.2      master   <none>           <none>
kube-system   coredns-86c58d9df4-rkzfp         1/1     Running   0          3m26s   10.244.0.3      master   <none>           <none>
kube-system   etcd-master                      1/1     Running   0          2m39s   192.168.33.11   master   <none>           <none>
kube-system   kube-apiserver-master            1/1     Running   0          2m41s   192.168.33.11   master   <none>           <none>
kube-system   kube-controller-manager-master   1/1     Running   0          2m24s   192.168.33.11   master   <none>           <none>
kube-system   kube-flannel-ds-amd64-27mm7      1/1     Running   0          48s     192.168.33.13   node2    <none>           <none>
kube-system   kube-flannel-ds-amd64-l8tj7      1/1     Running   0          2m14s   192.168.33.12   node1    <none>           <none>
kube-system   kube-flannel-ds-amd64-q6bhm      1/1     Running   0          3m26s   192.168.33.11   master   <none>           <none>
kube-system   kube-proxy-bqpx8                 1/1     Running   0          48s     192.168.33.13   node2    <none>           <none>
kube-system   kube-proxy-dltht                 1/1     Running   0          3m26s   192.168.33.11   master   <none>           <none>
kube-system   kube-proxy-gg6sl                 1/1     Running   0          2m14s   192.168.33.12   node1    <none>           <none>
kube-system   kube-scheduler-master            1/1     Running   0          2m48s   192.168.33.11   master   <none>           <none>
vagrant@master:~$
```

### CentOS

```shell-session
$ vagrant up
Bringing machine 'master' up with 'virtualbox' provider...
Bringing machine 'node1' up with 'virtualbox' provider...
Bringing machine 'node2' up with 'virtualbox' provider...
==> master: Importing base box 'centos/7'...
==> master: Matching MAC address for NAT networking...

（省略）

    node2: Run 'kubectl get nodes' on the master to see this node join the cluster.
sotoiwa@sotonombp:~/workspace/vagrant-k8s-centos
$ vagrant ssh master
[vagrant@master ~]$ kubectl get node -o wide
NAME     STATUS   ROLES    AGE     VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE                KERNEL-VERSION               CONTAINER-RUNTIME
master   Ready    master   10m     v1.13.0   172.16.33.11   <none>        CentOS Linux 7 (Core)   3.10.0-862.14.4.el7.x86_64   docker://17.3.3
node1    Ready    <none>   7m      v1.13.0   172.16.33.12   <none>        CentOS Linux 7 (Core)   3.10.0-862.14.4.el7.x86_64   docker://17.3.3
node2    Ready    <none>   3m51s   v1.13.0   172.16.33.13   <none>        CentOS Linux 7 (Core)   3.10.0-862.14.4.el7.x86_64   docker://17.3.3
[vagrant@master ~]$ kubectl get po --all-namespaces -o wide
NAMESPACE     NAME                             READY   STATUS    RESTARTS   AGE     IP             NODE     NOMINATED NODE   READINESS GATES
kube-system   calico-node-24jkl                2/2     Running   0          7m7s    172.16.33.12   node1    <none>           <none>
kube-system   calico-node-dnjbb                2/2     Running   0          4m4s    172.16.33.13   node2    <none>           <none>
kube-system   calico-node-hhscc                2/2     Running   0          10m     172.16.33.11   master   <none>           <none>
kube-system   coredns-86c58d9df4-hlmpt         1/1     Running   0          10m     192.168.0.2    master   <none>           <none>
kube-system   coredns-86c58d9df4-phbjg         1/1     Running   0          10m     192.168.0.3    master   <none>           <none>
kube-system   etcd-master                      1/1     Running   0          9m30s   172.16.33.11   master   <none>           <none>
kube-system   kube-apiserver-master            1/1     Running   0          9m2s    172.16.33.11   master   <none>           <none>
kube-system   kube-controller-manager-master   1/1     Running   3          9m4s    172.16.33.11   master   <none>           <none>
kube-system   kube-proxy-cmc72                 1/1     Running   0          7m7s    172.16.33.12   node1    <none>           <none>
kube-system   kube-proxy-h5z7t                 1/1     Running   0          4m4s    172.16.33.13   node2    <none>           <none>
kube-system   kube-proxy-kbgm5                 1/1     Running   0          10m     172.16.33.11   master   <none>           <none>
kube-system   kube-scheduler-master            1/1     Running   3          9m30s   172.16.33.11   master   <none>           <none>
[vagrant@master ~]$
```