kubeadmを使ってオンプレミスのプロキシ環境下にKubernetesクラスタをデプロイする(CentOS 7.3)

クラスタのデプロイ・管理が大変なのでほんとはk8sのマネージドサービス使いたい...GKEとか。
でもオンプレミスで運用せざるを得ないことってありますよね。しかもプロキシ環境下で。

そんな状況下でk8sクラスタを構築する方法の備忘録です。

## kubeadm

[公式](https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/)が提供しているk8sクラスタを構築するためのツールです。現在のバージョンはまだBeta版ですが、2018年内にGA予定のようです。

この記事の内容は基本的には公式ドキュメントの焼き直し+α(プロキシ設定など)です。

Beta版ということもあり、クラスタデータを保持するetcdの冗長化、およびMasterコンポーネントが複数存在するようなHigh Availability構成のクラスタは[単純にkubeadmを使うだけでは](https://kubernetes.io/docs/setup/independent/high-availability/)構築できません。
今回はHA構成までは考えず、今回利用したkubeadm v1.9がカバーする範囲である単一Master構成のクラスタを構築します。

このあたりはGA時点では対応されるんでしょうか。

## 環境

* CentOS 7.3(master, worker)
* プロキシ経由でインターネットに接続可能な環境

## k8sクラスタ構成

* master: 単一構成
* podネットワーク: flannel
* コンテナランタイム: docker

## バージョン

* kubernetes v1.9.0
* kubeadm v1.9.0
* docker v1.12.6
* flannel v0.9.1

## 準備

kubeadmでk8sクラスタを構築するための準備です。各master/worker nodeすべてで実施します。rootで実施する前提です。

### /etc/hostsの設定

以下のように新しい行にホスト名に対応するIPアドレスを追記します。

```bash
[ip_address] [hostname]
```

以下のコマンド実行で追記できます(単一NIC、イーサネットの前提です)。

```bash
echo $(ip a | grep 'en\|eth0' | grep "inet" | cut -d' ' -f6 | cut -d/ -f1) $(hostname) >> /etc/hosts
```

### yumのプロキシ設定

kubeadmはじめdocker, kubeletなどのk8sの動作に必要なパッケージはyumでインストールします。
プロキシ経由でyumが利用できるよう/etc/yum.confに以下を設定しておきます。

```bash
proxy = http://[proxy_host]:[proxy_port]
```

以下のコマンド実行で設定できます。

```bash
echo proxy = http://[proxy_host]:[proxy_port] >> /etc/yum.conf
```

* プロキシで認証が必要な場合は以下も追加します。

    ```bash
    echo proxy_username = [proxy_user] >> /etc/yum.conf
    echo proxy_password = [proxy_pass] >> /etc/yum.conf
    ```

### firewall無効化

```bash
systemctl disable firewalld
systemctl stop firewalld
```

### SELinux無効化

```bash
setenforce 0
```

/etc/selinux/configを編集しdisableにしておきます。

```bash
SELINUX=disabled
```

以下のコマンドでdisableに変更できます。

```bash
sed -e "/^SELINUX=enforcing$/s/SELINUX=enforcing/SELINUX=disabled/" -i.bak /etc/selinux/config
```

### MACアドレスおよびproduct_uuidがすべてのノード間で重複していないことの確認

公式ドキュメントに
> Unique hostname, MAC address, and product_uuid for every node

とあるので確認しておきます。

* MACアドレス確認

    ```bash
    ip link
    ```
* product_uuid確認

    ```bash
    cat /sys/class/dmi/id/product_uuid
    ```

### swapを無効化

これも公式ドキュメントに
> Swap disabled. You **MUST** disable swap in order for the kubelet to work properly.

とあるので無効にしておきます。

```bash
swapoff -a
sed -e "/^UUID=[a-z0-9-]* swap/s/^/# /" -i.bak /etc/fstab
cat /etc/fstab
```

### Kubernetesコンポーネントが使用するポートが他プロセスで使用されていないか確認

k8sの各コンポーネントが利用するポートが使用されていないことを確認します。

* Master node

    | Protocol       | Direction | Port Range | Purpose                 |
    | -------------- | --------- | ---------- | ----------------------- |
    | TCP            | Inbound   | 6443*      | Kubernetes API server   |
    | TCP            | Inbound   | 2379-2380  | etcd server client API  |
    | TCP            | Inbound   | 10250      | Kubelet API             |
    | TCP            | Inbound   | 10251      | kube-scheduler          |
    | TCP            | Inbound   | 10252      | kube-controller-manager |
    | TCP            | Inbound   | 10255      | Read-only Kubelet API   |

* Worker nodes

    | Protocol | Direction | Port Range  | Purpose               |
    | -------- | --------- | ----------- | --------------------- |
    | TCP      | Inbound   | 10250       | Kubelet API           |
    | TCP      | Inbound   | 10255       | Read-only Kubelet API |
    | TCP      | Inbound   | 30000-32767 | NodePort Services*    |

*部分は設定により変更可能です。今回はk8s master/node上でk8s管理外のアプリケーションプロセスが動作することは想定していないため、デフォルト設定とします。

### docker v1.12.6インストール

Kubernetesではv1.12が推奨(1.13や17.03でも動作確認済みとのこと)されているため、  v1.12.6をインストールします。

dockerインストール手順もほぼ[公式ドキュメント](https://docs.docker.com/v1.13/engine/installation/linux/centos/)の焼き直しです。

#### 必要なyumリポジトリの有効化

```bash
yum update -y
yum install -y yum-utils
yum-config-manager --add-repo https://docs.docker.com/v1.13/engine/installation/linux/repo_files/centos/docker.repo
yum makecache fast
```

#### dockerパッケージのインストール

```bash
yum install -y docker-engine-1.12.6-1.el7.centos
```

#### dockerのproxy設定

Docker Hubなどインターネット上のコンテナイメージリポジトリからイメージを取得するため、プロキシ経由で接続可能な設定をする必要があります。

```bash
mkdir -p /etc/systemd/system/docker.service.d
cat <<EOF > /etc/systemd/system/docker.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://[proxy_user:proxy_pass@][proxy_host]:[proxy_port]" "HTTPS_PROXY=http://[proxy_user:proxy_pass@][proxy_host]:[proxy_port]" "NO_PROXY=localhost,127.0.0.1,[master_node_ip],[worker_nodes_ip],10.96.0.0/12,10.244.0.0/16"
EOF
```

NO_PROXYにはすべてのk8s master/worker nodeのIPアドレスを指定します。また、k8sの各Serviceに割り振られる仮想IP(10.96.0.0/12)、および後述するpodネットワークプラグインが展開するOverlayネットワークのIPアドレス範囲(10.244.0.0/16)をCIDRで記載しておきます。

これをしないとkubeadmでクラスタ構築するときに、preflightチェックで異なるホスト間のPodおよびServiceの通信がプロキシ経由でルーティングされちゃうよ！という旨のWarningが出力され、実際にうまく通信してくれませんでした。

#### proxy設定の反映

```bash
systemctl daemon-reload
```

#### dockerを起動&自動起動設定

```bash
systemctl enable docker && systemctl start docker
```

#### proxy設定が反映されていることを確認

```bash
$ docker info | grep Proxy
Http Proxy: http://[proxy_host]:[proxy_port]
Https Proxy: http://[proxy_host]:[proxy_port]
No Proxy: localhost,127.0.0.1,[master_node_ip],[worker_nodes_ip],10.96.0.0/12,10.244.0.0/16
```

#### dockerの動作確認

docker単体で動作可能なことを確認します。確認できたらもう利用しないのでイメージは消しておきます。

```bash
docker run --rm hello-world
docker rmi docker.io/library/hello-world
```

#### 一般ユーザからdockerを利用可能にする

```bash
groupadd docker
usermod -aG docker [user_name]
systemctl restart docker
```

ログアウト, 再ログインを実施し設定を反映します。

ここまででdockerのインストールが完了しました。
dockerはコンテナイメージの取得、コンテナの起動、削除をこれから構築するk8sクラスタからの指示で行うことになります。

#### 本番環境で利用する場合の設定(ストレージドライバ)

CentOS/RHELではdockerコンテナ/イメージごとのストレージを変更差分ごとに管理するため、デフォルトでdevicemapperというストレージドライバを利用します。

devicemapperはデフォルト設定の場合`loop-lvm`というモードで動作しています。このモードはコンテナ利用時のディスクI/Oのパフォーマンスが良くないので、プロダクション環境で運用する場合は`direct-lvm`モードに変更することを公式が推奨しています。

詳細は[公式ドキュメント](https://docs.docker.com/v1.13/engine/userguide/storagedriver/device-mapper-driver/)に記載がありますが、ホストマシン上にLVMとthin poolを作成する必要があります。docker v17.06以降だとdaemon.jsonに設定を記載すると自動で作ってくれるみたいですね。

## kubeadmインストール、kubenetesクラスタの構築

### kubadmのプロキシ設定

設定内容はdockerのプロキシ設定時と同様です。ホスト上の環境変数として指定します。

環境変数が常に有効となるように`.bash_profile`に追記し反映します。

```bash
cat <<EOF >> ~/.bash_profile
PROXY_PORT=[proxy_port]
PROXY_HOST=[proxy_host]
http_proxy=http://[proxy_user:proxy_pass@]\$PROXY_HOST:\$PROXY_PORT
HTTP_PROXY=\$http_proxy
https_proxy=\$http_proxy
HTTPS_PROXY=\$http_proxy
no_proxy="localhost,127.0.0.1,[master_node_ip],[worker_nodes_ip],10.96.0.0/12,10.244.0.0/16"
EOF

source ~/.bash_profile
```

### kubeadm, kubelet, kubectlパッケージリポジトリを追加

kubeadmとk8sの動作に必要なパッケージをインストールするためのyumリポジトリを追加します。

以下、簡単な説明です。下記以外のk8sコンポーネントはkubeadmがコンテナとして起動してくれます。

* kubelet: pod, コンテナを起動する役割を持つ、クラスタ内のすべてのホストマシンに存在するコンポーネント
* kubectl: K8sクラスタと対話するためのCLIユーティリティ

```bash
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
EOF
```

### kubeadm, kubelet, kubectlパッケージインストール(v1.9.0)

```bash
yum install -y kubelet-1.9.0-0 kubeadm-1.9.0-0 kubectl-1.9.0-0
```

### K8sの通信が正しくルーティングされるよう設定

k8sはiptablesを更新することでクラスタ内の各Pod間のルーティングを実現しています。

RHEL/CentOSの場合以下の設定をしないと通信が正しくルーティングされないようです。

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

### kubeletを一度起動し、Cgroup Driverがdockerとkubelet間で一致していることを確認

CentOSデフォルトだとkubeletは`systemd`、Dockerは`cgroupfs`を使うはずなので、利用するドライバの指定はDockerインストール時に先に実施しておいてもいいです。

```bash
systemctl enable kubelet && systemctl start kubelet
cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf | grep KUBELET_CGROUP_ARG
docker info | grep Cgroup
systemctl stop kubelet
```

どちらもcgroup-driver=systemdとなっていればOK

* 異なる場合
    以下を実施後再度確認します。

    ```bash
    cat << EOF > /etc/docker/daemon.json
    {
    "exec-opts": ["native.cgroupdriver=systemd"]
    }
    EOF"
    systemctl restart docker
    ```

### masterでのみ実施する作業

#### masterの初期化

##### まずはdry-runしてみる

`--kubernetes-version`でk8sのバージョンを指定しています。また、`--apiserver-advertise-address`でmasterのIPアドレスを指定します。

kubeadmはworker node追加時の認証のためにトークンを払い出すのですが、利用期限が決まっておりデフォルトで24時間有効です。今回は検証用途であり、あとでworker nodeを追加したりする可能性があるため、`--token-ttl 0`を指定し認証トークンの利用期限を無期限にしています。

`--pod-network-cidr`にはプロキシ設定でも指定したOverlayネットワークのIPアドレス範囲(10.244.0.0/16)をCIDR形式で指定します。

```bash
kubeadm init --kubernetes-version 1.9.0 --apiserver-advertise-address=$(ip a | grep 'en\|eth0' | grep "inet" | cut -d' ' -f6 | cut -d/ -f1) --pod-network-cidr=10.244.0.0/16 --token-ttl 0 --dry-run
```

以下が出力されれば問題なく実行できているので、実際に初期化実行します。

```bash
[dryrun]?Finished dry-running successfully. Above are the resources that would be created.
```

##### 初期化

```bash
kubeadm init --kubernetes-version 1.9.0 --apiserver-advertise-address=$(ip a | grep 'en\|eth0' | grep "inet" | cut -d' ' -f6 | cut -d/ -f1) --pod-network-cidr=10.244.0.0/16 --token-ttl 0
```

以下が出力されれば初期化に成功しています。`kubeadm join`以降の出力はworker nodeを追加する際に実行するコマンドです。

```bash
Your Kubernetes master has initialized successfully!

To start using your cluster, you need to run (as a regular user):

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

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  http://kubernetes.io/docs/admin/addons/

You can now join any number of machines by running the following on each node
as root:

  kubeadm join --token [token] [K8s_master_host]:[port] --discovery-token-ca-cert-hash sha256:[hash]
```

#### kube-apiserverと対話可能にする設定

初期化時に出力されていた以下のコマンドを実行します。

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

#### Podネットワークアドオンのデプロイ

kubeadmはOverlayネットワークを展開するところまでは面倒見てくれないので、別途Podネットワークプラグインをクラスタにデプロイします。これをしないとずっとmasterのステータスが`Not Ready`状態となりk8sが機能しません。

今回はflannelを利用します。
master初期化時の`--pod-network-cidr`やプロキシ設定で指定したIPアドレス範囲(10.244.0.0/16)はこのflannelが展開するOverlayネットワークで利用します。

```bash
kubectl apply -f  https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml
```

#### master上でkube-dns PodがRunningになっていることを確認

Podネットワークが動作しているとしばらくしてkube-dnsがRunning状態になるはずなので確認します。

```bash
$ kubectl get pods -n kube-system
NAME                                               READY     STATUS    RESTARTS   AGE
etcd-[k8s_master_host]                             1/1       Running   0          10m
kube-apiserver-[k8s_master_host]                   1/1       Running   0          10m
kube-controller-manager-[k8s_master_host]          1/1       Running   0          10m
kube-dns-6f4fd4bdf-b7kw9                           3/3       Running   0          10m
kube-flannel-ds-c2r9b                              1/1       Running   0          10m
kube-proxy-s9krr                                   1/1       Running   0          10m
kube-scheduler-[k8s_master_host]                   1/1       Running   0          10m
```

#### masterのstatusがReadyになっていることを確認

masterの状態が`Ready`となり、k8sクラスタとして利用できる状態になっていることを確認します。

```bash
$ kubectl get nodes
NAME                       STATUS    ROLES     AGE       VERSION
[k8s_master_host]          Ready     master    10m       v1.9.0
```

#### masterでもPod起動がスケジュールされるようにmasterへのPod配置を制限するtaintを削除

デフォルトではmasterへPodが配置されないよう、taintという機能によりmasterへのPodスケジューリングが制限されています。

リソースが不足していたりmaster単体で動かす場合は、master nodeに設定されているtaintを削除します。

```bash
kubectl taint nodes --all node-role.kubernetes.io/master-
```

### worker nodeでのみ実施する作業

#### worker nodeの追加

masterの初期化時に出力されたtoken, hash値を引数に指定します。

```bash
kubeadm join --token [token] [K8s_master_host]:6443 --discovery-token-ca-cert-hash sha256:[hash]
```

以下が出れば成功

```bash
Node join complete:
* Certificate signing request sent to master and response
  received.
* Kubelet informed of new secure connection details.

Run 'kubectl get nodes' on the master to see this machine join.
```

#### K8sクラスタにworker nodeが追加され`Ready`になっていることをmasterで確認

```bash
$ kubectl get nodes
NAME                       STATUS    ROLES     AGE       VERSION
[k8s_master_host]          Ready     master    10m       v1.9.0
[k8s_worker_host]          Ready     master    10m       v1.9.0
```

以上でオンプレミス、プロキシ環境下のホスト上でk8sクラスタを動作させることができました。

これでプロキシ環境下でもkubectl applyやhelmなどを利用してアプリケーションをクラスタ上に展開していくことができます。

## 参考サイト

* [Kubernetes Documentation](https://kubernetes.io/docs/home/)
* [Docker Documentation(v1.13)](https://docs.docker.com/v1.13/)