Kubesprayを用いてGoogle Compute Engine環境（Ubuntu 16.04）にKubernetesをインストールしてみた

# この記事はなに？

関わっているプロジェクトで，社内のオンプレミス環境にKubernetesの構築をする機会がありました．
まず一度，自分の手元でテスト用環境を構築しようと思い，Google Compute Engineを使ってVMを立ち上げ，そこにKubernetesをインストールしてみたところ成功したので，手順をメモしておきます．

Kubernetes環境の構築には，Kubesprayを利用しています．
Kubesprayは，マルチノードからなるKubernetesクラスタを比較的少ない手順で構築するための，AnsibleのPlaybookとその周辺ツールです．
https://kubernetes.io/docs/setup/production-environment/tools/kubespray/
https://github.com/kubernetes-sigs/kubespray
https://kubespray.io/#/

# 環境

* Kubespray v2.11.0
* Google Compute Engine（以下，「GCE」）．ゲストOSのVMはUbuntu 16.04のイメージを利用
* Kubernetes 1.15.3

# 今回やりたいこと

図にするとこんな感じ↓

![k8s-20190910.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/4fdc6269-c2f8-99c6-1eaa-e6042d8162ea.png)


ここで，

* k8s-admin: Adminサーバ．Kubesprayを用いたKubernetesのインストール作業や，その後の作業を行うための作業用のサーバ．このサーバは今回インストールするKubernetesクラスタの外に位置することになる．
* k8s-master[1-2]: KubernetesのMasterノードとなるサーバ群．
* k8s-node[1-3]: KubernetesのWorkerノードとなるサーバ群．

と定義する．

また，etcdについては，k8s-master[1-2], k8s-node1の3台にインストールすることを想定する（本当はMasterノードを3台用意してそこにインストールするのがよさそう）．

# 手順

## GCEでのVMインスタンス構築

1. GCPのプロジェクト設定は完了しているものと仮定する．
まだなら
https://cloud.google.com/compute/docs/quickstart-linux
から設定する．
2. 「VMインスタンス」 -> 「インスタンスを作成」から，VMインスタンスを作成する．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/4deac3d1-f462-0cb6-c880-aa0788932f3f.png)

    今回は，以下のようなテンプレートを作り，これを計6台複製して作成することとする．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/5b3a891c-bdc8-0d76-7e95-576dfcdb326c.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/febd7ce8-0a5b-16df-15f3-4c33715c1b1d.png)

    こんな感じになる．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/67401dd9-3f11-1f68-ce4d-1f8f4504fb6c.png)

    ここで，「内部IP」の値については，あとで使うので控えておく．

3. 作ったVMすべてに，ローカルPCからSSHでログインできることを確かめる．

    ```bash
    [user@localhost]

    # 正しいプロジェクトに接続しているか確かめる
    gcloud config list

    # SSHでログイン
    gcloud beta compute --project "${GCP_PROJECT_NAME}" ssh --zone "us-central1-a" "${VM_INSTANCE_NAME}"
    ```

## GCEのファイアウォールの設定

今回の手順で一番ハマったところがここ．

GCEでは，デフォルトではTCP，UDP，ICMP以外のプロトコルでの通信をファイアウォールでブロックする仕様になっているらしい．
https://cloud.google.com/vpc/docs/vpc?hl=ja

今回構築するKubernetesのネットワークプラグインであるCalicoでは，（デフォルトで，）ノード間で通信を中継する際に，IPinIPというプロトコルが使われていて，そのままでは，ノード間での通信が発生する場合（例えば，自ノードに存在しないPodのIPアドレスにHTTPでアクセスしようとした時など）に，GCE内のファイアウォールでパケットがブロックされて通信ができないという事象が発生する．
https://github.com/kubernetes-sigs/kubespray/blob/master/docs/calico.md#cloud-providers-configuration
https://docs.projectcalico.org/v1.5/getting-started/docker/installation/gce
https://qiita.com/oke-py/items/395bb294d6d88c28634e

IPinIPをファイアウォールのルールで許可することで，これを回避する．

1. 「VPCネットワーク」 -> 「ファイアウォール ルール」から，ファイアウォールのルールを作成する．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/7a3e9819-0b0e-1239-98c4-6521311916cf.png)
2. 以下のように設定する．「ソースIPの範囲」については，ノードの内部IPが属するIPアドレスの範囲を指定する．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/ec20cd51-267e-c456-6e6f-392618cca91a.png)

    以下のようになる．
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46811/5c7264fd-10e3-3b75-1de1-a60b7bcaee49.png)


## SSH鍵の生成と公開鍵の配布

Kubesprayでの構築ではAnsibleを利用するため，Adminサーバからクラスタを構成するサーバへのSSHでのリモートログインができることが前提となる．
AdminサーバでSSH鍵を作成し，それを他のサーバに配布することにより，インスタンス間でのSSHができるようにする．

1. AdminサーバでSSH鍵を生成する．今回はテスト用の環境なので，暗号強度等には特にこだわらないこととする．

    ```bash
    [user@k8s-admin]

    ls -l ~/.ssh/id_rsa{,.pub}
      # 鍵が存在しないことを確認する

    ssh-keygen
      # 鍵の配置場所は~/.ssh/id_rsa{,.pub}．パスフレーズ等はすべて空文字列を指定

    ls -l ~/.ssh/id_rsa{,.pub}
      # id_rsa, id_rsa.pubが両方存在することを確認する．id_rsaについては，秘密鍵なので他のユーザが見られないようにしておく

    cat ~/.ssh/id_rsa.pub
      # 公開鍵の内容を記録する
    ```
2. 1.で記録した公開鍵の内容を，クラスタを構成するサーバ群の~/.ssh/authorized_keysに登録する．これによって，Adminサーバと他のサーバ間でのSSH鍵での疎通が取れるようになる．

    ```bash
    [user@k8s-master[1-2], k8s-node[1-3]]

    mkdir ~/.ssh/
      # ディレクトリがなければ作る

    echo ${CONTENTS_OF_PUBLIC_KEY} >> ~/.ssh/authorized_keys
    ```
3. SSHのログインができることを確かめる．GCEでは，インスタンス間での内部通信用にIPアドレス（= 内部IP）が振ってあるため，そちらを使ってログインを試みる．

    ```bash
    [user@k8s-admin]

    ssh 10.128.0.20
      # 10.128.0.20（= k8s-master1の内部IP）でk8s-master1にパスワードの入力なしでSSHログインできることを確かめる

    # 以下，すべてのサーバについて同様
    ```

    他サーバにはじめてSSH接続する際には，確認画面が表示されることがある．これを放置するとその後のplaybookが失敗するので，すべてのサーバについて"yes"を回答し，接続時に確認画面が表示されないようにしておく．

## パッケージの更新

ここからは，Adminサーバに必要なソフトウェアをインストールしていく作業になる．

まず，APTのパッケージ情報が古いと後述のPIPがうまく利用できなかったので，Adminサーバについて，パッケージを最新化しておく．
どうやらKubespray側では，apt-get upgradeまではしないようなので，ついでに全台についてやっておいた方がいい．

```bash
[user@k8s-admin, k8s-master[1-2], k8s-node[1-3]]

sudo apt-get update && sudo apt-get upgrade
```

## PIPのインストール

Kubesprayの依存ソフトウェアをインストールする際ににPIPが必要．
PythonについてはすでにVMイメージにインストールされていたので，PIPのみを追加でインストールする．

ここからの作業については，基本的にAdminサーバだけで問題ない．

```bash
[user@k8s-admin]

# インストールスクリプトのダウンロード
curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"

ls -l get-pip.py
  # 存在することを確認

# PIPのインストール
sudo python3 get-pip.py

pip -V
  # 最新版のPIPがインストールされていることを確認
```

APTを使ってPIPをインストールすると，なぜか古いバージョンがインストールされ，その後のansible-playbookコマンドが失敗することがあるので注意．

## Kubesprayのダウンロード

Githubからダウンロードする．

```bash
[user@k8s-admin]

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

cd kubespray
git checkout v2.11.0

git status
  # ソースコードがv2.11.0になっていることを確認
```

## 依存ソフトウェアをダウンロードする

1. requirements.txtに，Kubesprayが依存するソフトウェアが記されているので，これをPIPでインストールする．

    ```bash
    [user@k8s-admin]

    sudo pip install -r requirements.txt
    ```
2. 自分の環境では，1.で

    ```
    ERROR: paramiko 2.6.0 has requirement cryptography>=2.5, but you'll have cryptography 1.2.3 which is incompatible.
    ```

    のようなエラーが表示された．
    理由まで追うことができなかったが，このバージョン非互換を放置すると，後のansible-playbookコマンドが失敗するので，最新版のcryptographyを手動でインストールし直すことで回避する．

    ```bash
    [user@k8s-admin]

    # 古いバージョンをアンインストール
    sudo pip3 uninstall cryptography

    # 最新バージョンをインストール
    sudo pip3 install cryptography
    ```

ここでAnsible等もインストールされるので，これ以上追加でソフトウェアをインストールする必要はない．

## Ansibleの設定を行う

1. inventoryを設定する．Kubesprayのソースコードには，設定ファイル群のテンプレートがあるので，これをコピーしてから編集する形で進める．

    ```bash
    [user@k8s-admin]

    cd ~/kubespray

    # inventoryのテンプレートをコピーする
    cp -rfp inventory/sample inventory/mycluster

    ls -l inventory/{sample,mycluster}
      # 両方存在することを確認
    ```
2. Kubesprayには，IPアドレスの配列を入力することでサンプルのinventoryファイルを自動生成するスクリプトがあるので，それを使って雛形を作り，適宜編集する．

    ```bash
    [user@k8s-admin]

    # サンプルのinventoryファイル（hosts.yml）を作成する
    declare -a IPS=(10.128.0.20 10.128.0.21 10.128.0.22 10.128.0.23 10.128.0.24)
      # ここにはAdminサーバ以外（クラスタとなるサーバ）のVMの内部IPを記載する
    CONFIG_FILE=inventory/mycluster/hosts.yml python3 contrib/inventory_builder/inventory.py ${IPS[@]}

    vim inventory/mycluster/hosts.yml
      # 実現したいクラスタ構成に応じてinventoryファイルの内容を編集する
    ```

    今回構成したいクラスタの場合，こんな感じになるはず（etcdについてはまだ再考が必要かも）．
    
    ```yaml
    all:
      hosts:
        k8s-master1: # ここに指定する名前がKubernetesにおけるノードの名前になる．デフォルトの"node*"から変更している
          ip: 10.128.0.20
          ansible_host: 10.128.0.20
          access_ip: 10.128.0.20
        k8s-master2:
          ip: 10.128.0.21
          ansible_host: 10.128.0.21
          access_ip: 10.128.0.21
        k8s-node1:
          ip: 10.128.0.22
          ansible_host: 10.128.0.22
          access_ip: 10.128.0.22
        k8s-node2:
          ip: 10.128.0.23
          ansible_host: 10.128.0.23
          access_ip: 10.128.0.23
        k8s-node3:
          ip: 10.128.0.24
          ansible_host: 10.128.0.24
          access_ip: 10.128.0.24
      children:
        kube-master: # Masterノードとなるhostを指定
          hosts:
            k8s-master1:
            k8s-master2:
        kube-node: # Workerノードとなるhostを指定
          hosts:
            k8s-node1:
            k8s-node2:
            k8s-node3:
        etcd: # etcdを配置するhostを指定
          hosts:
            k8s-master1:
            k8s-master2:
            k8s-node1:
        k8s-cluster:
          children:
            kube-master:
            kube-node:
        calico-rr:
          hosts: {}
    ```

    inventoryの詳細については，
    https://github.com/kubernetes-sigs/kubespray/blob/master/docs/ansible.md
    を参照すること．
3. kubectlの設定を行う．Kubesprayには，kubectlを自動でインストールして，クラスタへの接続設定まで行う機能がある．これを有効にする．

    ```bash
    [user@k8s-admin]

    # 設定ファイルを編集する
    cp -ip inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.yml{,.bak}
    vim inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.yml

    diff inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.yml{,.bak}
    184c184
    < kubeconfig_localhost: true # inventory/mycluster/artifacts/admin.confにkubectlの設定ファイルをダウンロードする
    ---
    > # kubeconfig_localhost: false
    186c186
    < kubectl_localhost: true # inventory/mycluster/artifacts/kubectlにkubectlをダウンロードする
    ---
    > # kubectl_localhost: false
    ```

他にもいろいろ設定項目はあるので，カスタムの設定が行いたい場合は適宜確認すること．

## AnsibleでKubernetesクラスタを構築する

ansible-playbookを実行して，クラスタにKubernetesをインストールする．

```bash
[user@k8s-admin]

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

VM5台にインストールする場合でおよそ10~15分程度かかる．気長に待つ．
標準出力に出力されるログを見ているといくつかエラーが表示されることがあるが，Ansibleの"PLAY RECAP"にエラーがなければ，構築自体は問題ないと思ってよい．

## kubectlをインストールする

Adminサーバからkubectlを介してクラスタを操作できるようにしたい．
インストールの過程でkubectlとその設定ファイル（kubeconfig）がダウンロードされているので，これを配置することで，支障なくkubectlを利用できるようにする．

```bash
[user@k8s-admin]

cd ~

# kubectlとkubeconfigの確認
ls -l ~/kubespray/inventory/mycluster/artifacts/{kubectl,admin.conf}
  # kubectl: KubernetesのCLI．実行パーミッションも付与されているはず
  # admin.conf: 今回構築したクラスタに接続するためのkubeconfig

sudo cp -ip ~/kubespray/inventory/mycluster/artifacts/kubectl /usr/local/bin/kubectl
  # PATHの通っているディレクトリにkubectlのバイナリをコピー

# クラスタに接続するためのkubeconfigを設定
mkdir ~/.kube
cp -ip ~/kubespray/inventory/mycluster/artifacts/admin.conf ~/.kube/config
  # ~/.kube/configはkubectlが読みにいくkubeconfigのデフォルトのファイルパス
  # 既存のkubeconfigを汚したくない場合は，環境変数KUBECONFIGに上記のパスを設定するとよい

# kubectlの設定を確認する
kubectl config view

# 現在のクラスタ情報を確認する
kubectl cluster-info
  # Kubernetes master is running at https://10.128.0.20:6443
  # coredns is running at https://10.128.0.20:6443/api/v1/namespaces/kube-system/services/coredns:dns/proxy
  # kubernetes-dashboard is running at https://10.128.0.20:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy
  # のような表示がされればOK

# ノードの状態を確認する
kubectl get nodes
  # NAME          STATUS   ROLES    AGE   VERSION
  # k8s-master1   Ready    master   28h   v1.15.3
  # k8s-master2   Ready    master   28h   v1.15.3
  # k8s-node1     Ready    <none>   28h   v1.15.3
  # k8s-node2     Ready    <none>   28h   v1.15.3
  # k8s-node3     Ready    <none>   28h   v1.15.3
  # のような表示がされればOK
```

ここまでがうまくいけば，問題なくKubernetesがインストールされていることになる（正確に言うと，まだネットワークまわりの設定が確認できていない．[Network Checker Application](https://qiita.com/lethe2211/private/88df9f73a77b0c414c01#network-checker-application)を参照すること）．

## 構築ができているか検証する

試しに，今回構築したクラスタにNginxのPodをデプロイしてみる．

```bash
[user@k8s-admin]

# NginxのPodが含まれるDeploymentを登録
kubectl apply -f https://k8s.io/examples/application/deployment.yaml

# Deploymentが作成されていることを確認
kubectl describe deployment nginx-deployment

# Serviceを作成/クラスタ外部に露出し，クラスタ外部からクラスタ内のPodにアクセスできるようにする
kubectl expose deployment/nginx-deployment --type="NodePort" --port=80

# Serviceが作成されていることを確認
kubectl describe services nginx-deployment
  # Type: NodePortでServiceを露出した場合，ランダムに割り当てられるポート番号（= NodePort）で，クラスタ外部からPodへのアクセスが可能になる
  # "NodePort"の値を控えておく

# NodePortを介して，クラスタ外部からクラスタ内のPodにアクセスする
curl -v http://${INTERNAL_IP}:${NODE_PORT}/
  # ${INTERNAL_IP}の部分は，Workerノードの内部IP（いずれか）を入力する
  # ${NODE_PORT}の部分は，上で控えた"NodePort"の値を入力する
  # Nginxのデフォルトテンプレートがレスポンスとして返ってくればOK
```

# 他に調べてみたこと

## Network Checker Application

https://github.com/kubernetes-sigs/kubespray/blob/master/docs/netcheck.md

inventory/mycluster/group_vars/k8s-cluster/k8s-cluster.ymlの`deploy_netchecker`の値を"true"に変更することで，KubesprayがKubernetesクラスタ構築時にネットワーク診断用のPod/Serviceを自動でデプロイするようになる．

https://quay.io/repository/l23network/k8s-netchecker-agent?tag=latest&tab=tags
https://quay.io/repository/l23network/k8s-netchecker-server?tag=latest&tab=tags

```bash
[user@k8s-admin]

# Playbookの実行後

kubectl describe services netchecker-service
  # netchecker-serviceがtype: NodePortで作成されていることを確認
  # NodePortの値を控えておく

kubectl get pods | grep netchecker
  # netchecker-agent-*, netchecker-agent-hostnet-*というPodが各ノード1つずつ，netchecker-server-*というPodがクラスタに1つ作成されていることを確認


[user@k8s-master[1-2], k8s-node[1-3]]

# Pod間ネットワークとクラスタ内部のDNSの状態を診断する
curl -v http://${INTERNAL_IP}:${NODE_PORT}/api/v1/connectivity_check
  # ${INTERNAL_IP}の部分は，Workerノードの内部IP（いずれか）を入力する
  # ${NODE_PORT}の部分は，上で控えた"NodePort"の値を入力する
  # "{"Message":"All * pods successfully reported back to the server","Absent":null,"Outdated":null}"が表示されればOK
```

前述のファイアウォールの問題で悩んでいたとき，原因の切り分けに便利だった．

# 参考記事

* [Setting up a Kubernetes cluster with Kubespray](https://medium.com/@leonardo.bueno/setting-up-a-kubernetes-cluster-with-kubespray-1bf4ce8ccd73): Kubesprayによる構築については，基本的にこの記事の流れに沿っている．

# お借りしたもの

* Google Cloud Logo: https://cloud.google.com/press/
* Kubernetes Logo: https://github.com/kubernetes/community/tree/master/icons
* さくらのアイコンセット（SAKURA Internet Inc.）: https://knowledge.sakura.ad.jp/4724/