Kubernetes on CentOS7

# はじめに

数か月前より、EC2インスタンス上で稼働していた Web アプリケーションの一部を **Google Kubernetes Engine**（以降、GKE）に移行させています。

Kubernetes（以降、K8s）のマネージドサービスである GKE は、マスターノードやロードバランサ等、K8s を運用する基盤が既に整っている状態なので、K8s のアーキテクチャにそこまで精通していなくても使い始めることが出来ましたが、一から K8s 環境を構築してみたい欲求が湧いてきました。

そこで K8s のステージング環境が欲しかったこともあり（GKE でステージング環境を準備するとコストもかかりますし）、またオンプレミスに置いている VMware サーバのリソースにも余裕があったので、そこに K8s の環境を実装することにしました。

尚、K8s 界隈では [**Rancher**](https://rancher.com/) を使ってゴニョゴニョする方法が ﾋｬｯﾌｰ な感じですが、「もう少し理解を深めたい」という目的もあって、敢えて手作業で K8s 環境を実装しましたので、これから K8s に入門したい方の参考になれば幸いです。

# ホスト構成
オンプレミスにある VMware（ESXi 6.0）2台の上に、4つの VM を立てます。

![構成図-ホスト構成.png](https://qiita-image-store.s3.amazonaws.com/0/94476/317474ef-5cab-84a4-d241-16f617f61bf5.png)


# クラスタ構成
各ノードの役割は次の通りです。

- Master Node
    - API Server や Controller、Scheduler 等、マスターコンポーネントの Pod を配置 
- Worker Node #1 & #2
    - ユーザー側で用意したアプリケーション Pod を配置
- Router Node
    - L4（MetalLB）、L7（Nginx Ingress）ロードバランサのコントローラ等を配置

![構成図-クラスタ構成.png](https://qiita-image-store.s3.amazonaws.com/0/94476/bea1da19-e371-dcaa-4574-14572b219955.png)


# VM の準備
VM 4台を準備し、K8s をインストールします。
（以降 `kubelet` を起動するところまで、4VM 全て共通の手順です）

## VM の作成
VM を作成し、CentOS7 をインストールします。
※ VMware での VM作成手順、CentOS7 のインストール方法は省略します。

## OS の初期設定
次のような CentOS7 の初期設定を行います。

- Firewalld の停止、自動起動無効化
- SELinux の無効化
- ホスト名の設定 （& 内部向け DNS サーバに A レコードを登録）
- NIC に固定IP割り当て
- タイムゾーン設定（Asia/Tokyo≒JST）
- OSを最新状態にアップデート（`yum update`）

やり方は**ググればわんさか出てくる**ので、これも省略します。

## swap 無効化
swap が有効になっていると `kubelet` の動作に影響するので無効化します。

```bash
$ sudo swapoff -a
```

更にリブートの際、swap が有効化されないよう `/etc/fstab` を編集します。 

```text:/etc/fstab
/dev/mapper/centos-swap swap                    swap    defaults        0 0
↓↓↓コメントアウト
#/dev/mapper/centos-swap swap                    swap    defaults        0 0
```

## Docker インストール
公式リポジトリから最新版の Docker をインストールし、サービスを起動します。

```bash
$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2

$ sudo yum-config-manager \
    --add-repo https://download.docker.com/linux/centos/docker-ce.repo

$ sudo yum install -y docker-ce

$ sudo systemctl enable docker && sudo systemctl start docker
```

## ルーティング設定
Pod 間通信が正しくルーティングされるよう、カーネルパラメータを追加します。

```bash
$ sudo sh -c "cat <<EOF >  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
"

$ sudo sysctl --system
```

## K8s リポジトリ準備

```bash
$ sudo sh -c "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
"
```

##  K8s インストール

```bash
$ sudo yum install -y kubelet kubeadm kubectl
```

## kubelet 起動
`kubelet` はノードエージェントの役割を持っており、Pod のスケジューリングや管理を行うため、クラスタを構成する全ノードで起動させておく必要があります。

```bash
$ sudo systemctl enable kubelet && sudo systemctl start kubelet
```

# Master ノードの作成

## Master の初期化
Master を `kubeadm init` コマンドで初期化します。

尚、パラメータの意味は次の通りです。

- --apiserver-advertise-address: API Server のListen IPアドレス、通常は Master ノードの NIC `eth0` の IP アドレスを指定
- --pod-network-cidr: Pod に割り当てる内部 IP アドレスの CIDR、後述の **Flannel** を CNI に採用する場合は `10.244.0.0/16` を指定
- --service-cidr: Service に割り当てる内部 IP アドレスの CIDR、デフォルトは `10.96.0.0/12`

```bash
$ sudo kubeadm init \
    --apiserver-advertise-address 172.16.64.48 \
    --pod-network-cidr 10.244.0.0/16 \
    --service-cidr 172.16.130.0/24
[init] using Kubernetes version: v1.12.1
[preflight] running pre-flight checks
[preflight/images] Pulling images required for setting up a Kubernetes cluster

･･･（省略）

Your Kubernetes master has initialized successfully!

To start using your cluster, you need to run the following 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:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

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

  kubeadm join 172.16.64.48:6443 --token 9deqgn.ub69e9rt8orxdjuz --discovery-token-ca-cert-hash sha256:4cd706f26bfbff6490c771f345c5c2a897c31d264eb430b2b1b4797d383b8a4e

```

`Your Kubernetes master has initialized successfully!` が表示されていれば初期化完了です。
またメッセージの最終行 `kubeadm join ...` は、Node を クラスタへ参加させる際に利用するコマンドなので控えておきます。

## kubectl 接続設定
`kubectl` コマンドでクラスタを操作するには、鍵による接続設定が必要です。
それらの設定は Master 初期化の際に自動で生成してくれているので、カレントユーザーがアクセスできる場所にコピーします。

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

## CNI プラグインのインストール
Master 初期化直後の Node と Pod の状態を確認すると、Pod にある `coredns` が `Pending` の状態から変わらず、何時まで待っても起動してくれません。

```bash
$ kubectl get node
NAME                         STATUS   ROLES    AGE   VERSION
master.k8s.holdings.local    Ready    master   6s    v1.12.1

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                                READY   STATUS    RESTARTS   AGE
kube-system   coredns-576cbf47c7-lxsvc                            0/1     Pending   0          2m9s
kube-system   coredns-576cbf47c7-vpl2s                            0/1     Pending   0          2m9s
kube-system   etcd-master.k8s.holdings.local                      1/1     Running   0          78s
kube-system   kube-apiserver-master.k8s.holdings.local            1/1     Running   0          98s
kube-system   kube-controller-manager-master.k8s.holdings.local   1/1     Running   0          93s
kube-system   kube-proxy-g47f2                                    1/1     Running   0          2m9s
kube-system   kube-scheduler-master.k8s.holdings.local            1/1     Running   0          80s
```

これはクラスタ上に **Container Network Interface**（CNI）プラグインがデプロイされていないためです。
そこで CNI プラグインのひとつである **Flannel** を、次のコマンドでデプロイします。

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

再度 Pod の状態を確認すると `kube-flannel` がデプロイされ、`coredns` も起動されていることが判ります。

```bash
$ kubectl get pod --all-namespaces
NAMESPACE     NAME                                                READY   STATUS    RESTARTS   AGE
kube-system   coredns-576cbf47c7-lxsvc                            1/1     Running   0          3m18s
kube-system   coredns-576cbf47c7-vpl2s                            1/1     Running   0          3m18s
kube-system   etcd-master.k8s.holdings.local                      1/1     Running   0          2m27s
kube-system   kube-apiserver-master.k8s.holdings.local            1/1     Running   0          2m47s
kube-system   kube-controller-manager-master.k8s.holdings.local   1/1     Running   0          2m42s
kube-system   kube-flannel-ds-amd64-5dn58                         1/1     Running   0          15s
kube-system   kube-proxy-g47f2                                    1/1     Running   0          3m18s
kube-system   kube-scheduler-master.k8s.holdings.local            1/1     Running   0          2m29s
```

以上で Master Node の準備は完了です。

# Woker（#1 & #2）、Router Node の作成

## IP転送の有効化
**IP 転送**（IP Forwarding）とは、ホスト自体に設定されている IP アドレス以外を宛先とする IP パケットを受信した場合、その宛先に IP パケットを転送する機能です。（ホスト自体がルーターとなり、パケットを中継するイメージ）
CentOS7 のデフォルトでは **IPv4 Forwarding** 機能が無効化されているので、次のコマンドでカーネルパラメータを追加して有効化します。

```bash
$ sudo sh -c "echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/k8s.conf"
$ sudo sysctl --system
```

## クラスタに参加
前述「Master の初期化」で控えておいた `kubeadm join` コマンドを、Master 以外の各ノードで実行してクラスタに追加します。

```bash
$ sudo kubeadm join 172.16.64.48:6443 \
    --token 9deqgn.ub69e9rt8orxdjuz \
    --discovery-token-ca-cert-hash sha256:4cd706f26bfbff6490c771f345c5c2a897c31d264eb430b2b1b4797d383b8a4e
```

Master Node 側のコンソールから、`kubectl` コマンドで Node と Pod の状態を確認します。

```bash
$ kubectl get node
NAME                         STATUS   ROLES    AGE   VERSION
master.k8s.holdings.local    Ready    master   6m    v1.12.1
router.k8s.holdings.local    Ready    <none>   6m    v1.12.1
worker1.k8s.holdings.local   Ready    <none>   6m    v1.12.1
worker2.k8s.holdings.local   Ready    <none>   6m    v1.12.1

$ kubectl get pod --all-namespaces -o wide
NAMESPACE     NAME                                                READY   STATUS    RESTARTS   AGE     IP             NODE                         NOMINATED NODE
kube-system   coredns-576cbf47c7-lxsvc                            1/1     Running   0          18m     10.244.0.7     master.k8s.holdings.local    <none>
kube-system   coredns-576cbf47c7-vpl2s                            1/1     Running   0          18m     10.244.0.6     master.k8s.holdings.local    <none>
kube-system   etcd-master.k8s.holdings.local                      1/1     Running   0          17m     172.16.64.48   master.k8s.holdings.local    <none>
kube-system   kube-apiserver-master.k8s.holdings.local            1/1     Running   0          17m     172.16.64.48   master.k8s.holdings.local    <none>
kube-system   kube-controller-manager-master.k8s.holdings.local   1/1     Running   0          17m     172.16.64.48   master.k8s.holdings.local    <none>
kube-system   kube-flannel-ds-amd64-5dn58                         1/1     Running   0          15m     172.16.64.48   master.k8s.holdings.local    <none>
kube-system   kube-flannel-ds-amd64-cg6m7                         1/1     Running   0          39s     172.16.64.49   worker1.k8s.holdings.local   <none>
kube-system   kube-flannel-ds-amd64-gv8jd                         1/1     Running   0          11s     172.16.64.50   worker2.k8s.holdings.local   <none>
kube-system   kube-flannel-ds-amd64-pztzg                         1/1     Running   0          3m57s   172.16.64.51   router.k8s.holdings.local    <none>
kube-system   kube-proxy-8fqbs                                    1/1     Running   0          39s     172.16.64.49   worker1.k8s.holdings.local   <none>
kube-system   kube-proxy-9lwp5                                    1/1     Running   0          3m57s   172.16.64.51   router.k8s.holdings.local    <none>
kube-system   kube-proxy-g47f2                                    1/1     Running   0          18m     172.16.64.48   master.k8s.holdings.local    <none>
kube-system   kube-proxy-nbr9w                                    1/1     Running   0          11s     172.16.64.50   worker2.k8s.holdings.local   <none>
kube-system   kube-scheduler-master.k8s.holdings.local            1/1     Running   0          17m     172.16.64.48   master.k8s.holdings.local    <none>
```

Node が４つで構成され、全ての Pod が正常に起動（STATUS=Running）されていれば完了です。

# metrics-server
クラスタのリソース（CPU/メモリ/ストレージ）利用状況を確認するコマンドに `kubectl top` があります。
このコマンドですが、クラスタ上で `metrics-server` が稼働していない状況だと、次のようなエラーメッセージが表示され利用することができません。

```bash
$ kubectl top nodes
Error from server (NotFound): the server could not find the requested resource (get services http:heapster:)
```

そのため、次の手順で `metrics-server` をデプロイします。


GitHub リポジトリからプロジェクトをダウンロードします。

```bash
$ git clone https://github.com/kubernetes-incubator/metrics-server
```

マニフェストファイル `metrics-server/deploy/1.8+/metrics-server-deployment.yaml` を編集します。

```yaml:metrics-server/deploy/1.8+/metrics-server-deployment.yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: metrics-server
  namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: metrics-server
  namespace: kube-system
  labels:
    k8s-app: metrics-server
spec:
  selector:
    matchLabels:
      k8s-app: metrics-server
  template:
    metadata:
      name: metrics-server
      labels:
        k8s-app: metrics-server
    spec:
      serviceAccountName: metrics-server
      volumes:
      # mount in tmp so we can safely use from-scratch images and/or read-only containers
      - name: tmp-dir
        emptyDir: {}
      containers:
      - name: metrics-server
        image: k8s.gcr.io/metrics-server-amd64:v0.3.1
        ###追加 - ここから
        command:
        - /metrics-server
        - --kubelet-insecure-tls
        ###追加 - ここまで
        imagePullPolicy: Always
        volumeMounts:
        - name: tmp-dir
          mountPath: /tmp
      ###追加 - ここから
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      nodeSelector:
        node-role.kubernetes.io/master: ""
      ###追加 - ここまで
```

`###追加 - ここから` から `###追加 - ここまで` の間を、所定の場所に追記すればＯＫです。

追加内容を軽く説明すると次のようになります。

`spec.template.spec.containers[].command` の部分は `metrics-server` が `kubelet` と通信する際、`kubelet` から提示された CA 証明書を検証しないようにする設定です（追加していないと証明書関連のエラーが発生し、metrics-server が動作しません）。今回はテスト環境なのでこのような設定で問題ないですが、本番環境ではこの辺りはキチンと設定した方が良さそうです。

`spec.template.spec.tolerations[]` と `spec.template.spec.nodeSelector` の部分は、`metrics-server` を Master Node にデプロイさせるようにするおまじないです。（詳しくは後述します。）

マニフェストファイルの編集が済んだら、`metrics-server` をデプロイします。

```bash
$ kubectl apply -f metrics-server/deploy/1.8+/
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
serviceaccount/metrics-server created
deployment.extensions/metrics-server created
service/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
```

Master Node に `metrics-server` がデプロイされたか Pod 情報を確認します。

```bash
$ kubectl get pod -n kube-system -o wide
NAME                              READY   STATUS    RESTARTS   AGE     IP           NODE                        NOMINATED NODE
･･･
metrics-server-787c588977-lrfhv   1/1     Running   0          5m40s   10.244.0.9   master.k8s.holdings.local   <none>
･･･
```

これでクラスタのリソース情報を `kubectl top` コマンドで確認できるようになります。

```bash
$ kubectl top nodes
NAME                         CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
master.k8s.holdings.local    177m         8%     1111Mi          63%
router.k8s.holdings.local    44m          2%     308Mi           17%
worker1.k8s.holdings.local   43m          2%     245Mi           14%
worker2.k8s.holdings.local   82m          4%     284Mi           16%

$ kubectl top pod --all-namespaces
NAMESPACE        NAME                                                CPU(cores)   MEMORY(bytes)
kube-system      coredns-576cbf47c7-lxsvc                            2m           21Mi
kube-system      coredns-576cbf47c7-vpl2s                            3m           16Mi
kube-system      etcd-master.k8s.holdings.local                      21m          58Mi
kube-system      kube-apiserver-master.k8s.holdings.local            33m          442Mi
kube-system      kube-controller-manager-master.k8s.holdings.local   33m          76Mi
kube-system      kube-flannel-ds-amd64-5dn58                         3m           20Mi
kube-system      kube-flannel-ds-amd64-cg6m7                         3m           15Mi
kube-system      kube-flannel-ds-amd64-gv8jd                         4m           11Mi
kube-system      kube-flannel-ds-amd64-pztzg                         4m           13Mi
kube-system      kube-proxy-8fqbs                                    5m           11Mi
kube-system      kube-proxy-9lwp5                                    3m           11Mi
kube-system      kube-proxy-g47f2                                    2m           18Mi
kube-system      kube-proxy-nbr9w                                    3m           11Mi
kube-system      kube-scheduler-master.k8s.holdings.local            12m          22Mi
kube-system      metrics-server-787c588977-lrfhv                     2m           13Mi
```

# Master 以外のノードに Role 設定
ノード情報を確認すると Master Node のみ `ROLES` に `master` が表示され、その他のノードは `<none>` と表示され、何も設定されていないことが判ります。

```bash
$ kubectl get node
NAME                         STATUS   ROLES    AGE    VERSION
master.k8s.holdings.local    Ready    master   6d2h   v1.12.1
router.k8s.holdings.local    Ready    <none>   6d2h   v1.12.1
worker1.k8s.holdings.local   Ready    <none>   6d2h   v1.12.1
worker2.k8s.holdings.local   Ready    <none>   6d2h   v1.12.1
```

この `ROLES` ですが、どの情報から表示されているか調べてみると、ノードの `Roles` に設定されている内容が表示されていました。（`kubectl describe node` で確認可能）
 
このまま `<none>` の状態で運用しても支障はありませんが、ノード情報を確認するだけで各ノードの役割が解りやすくなるよう `Roles` を設定します。

```bash
$ kubectl label node worker1.k8s.holdings.local node-role.kubernetes.io/worker=
$ kubectl label node worker2.k8s.holdings.local node-role.kubernetes.io/worker=
$ kubectl label node router.k8s.holdings.local node-role.kubernetes.io/router=
```
ちなみに本投稿時点では、この `Roles` は K8s では利用されておらず、何を設定しても特に問題は発生しないそうです。（将来、特別な意味を持って使われるようになる可能性はありますが）

各ノードに Role が設定されたか確認します。

```bash
$ kubectl get node
NAME                         STATUS   ROLES    AGE    VERSION
master.k8s.holdings.local    Ready    master   6d2h   v1.12.1
router.k8s.holdings.local    Ready    router   6d2h   v1.12.1
worker1.k8s.holdings.local   Ready    worker   6d2h   v1.12.1
worker2.k8s.holdings.local   Ready    worker   6d2h   v1.12.1
```

# Load Balancer（LB）コントローラの準備
GKE 等のパブリッククラウドで提供される K8s サービスでは、ベンダーがマネージド サービスを提供してくれているので、ユーザーは Service や Ingress を定義するだけで Load Balancing を実装できますが、オンプレミスの K8s ではユーザー自身で LB コントローラを実装する必要があります。

そこで今回は Router Node 上へ Layer-4（L4）の LB コントローラに **MetalLB**、Layer-7（L7）の LB コントローラに **Nginx Ingress Controller** をデプロイします。

## Router Node に Taints 設定
前述 `metrics-server` の項で「おまじない」と説明しましたが、K8s には 特定の Pod を 特定の Node に配置させる（またはさせない）方法について、次のメカニズムが用意されています。

- [Node Selector](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector)
- [Node affinity](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity)
- [Taints & Tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/)

今回はこれらメカニズムの内、最も柔軟な条件設定ができる **Taints & Tolerations** を利用し、Router Node に Taints を設定して、LB コントローラ以外はスケジュールされないようにします。
（Taints & Tolerations については、[**こちら**](https://qiita.com/sheepland/items/8fedae15e157c102757f)で詳しく説明されていました）

ちなみに Master Node には Master 初期化の際に Taints が設定されており、Torerations を指定しない限り Pod がスケジューリングされないようになっています。

```bash
$ kubectl describe node master.k8s.holdings.local | grep Taints
Taints:             node-role.kubernetes.io/master:NoSchedule
```

Router Node に Taints を設定します。

```bash
$ kubectl taint nodes router.k8s.holdings.local node-role.kubernetes.io/router=:NoSchedule
```

Taints が正しく設定されたか確認します。

```bash
$ kubectl describe node router.k8s.holdings.local | grep Taints
Taints:             node-role.kubernetes.io/router:NoSchedule
```

## MetalLB のデプロイ
**MetalLB** は Google 社が作った ベアメタル K8s クラスタ向けのロードバランサ実装です。
この MetalLB をクラスタにデプロイすることで、オンプレミス K8s 環境でも L4 ロードバランサ（≒ `type: LoadBalancer`）を利用することができます。

最初に MetalLB のマニフェストをダウンロードします。

```bash
$ wget https://raw.githubusercontent.com/google/metallb/master/manifests/metallb.yaml
```

`speaker` と `controller` が Router Node にデプロイされるよう、マニフェストファイル `metallb.yaml` を編集します。
※ `speaker` は DaemonSet なので、編集しなければ Worker Node だけにデプロイされ、Router Node にデプロイされない。
※ `Controller` は Deployment なので、編集しなければ Worker Node のどちらかにデプロイされてしまう。

```yaml:metallb.yaml
（省略）
---
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
  namespace: metallb-system
  name: speaker
  labels:
    app: metallb
    component: speaker
spec:
  selector:
    matchLabels:
      app: metallb
      component: speaker
  template:
    metadata:
      labels:
        app: metallb
        component: speaker
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "7472"
    spec:
      serviceAccountName: speaker
      terminationGracePeriodSeconds: 0
      hostNetwork: true
      containers:
      - name: speaker
        image: metallb/speaker:master
        imagePullPolicy: Always
        args:
        - --port=7472
        - --config=config
        env:
        - name: METALLB_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        ports:
        - name: monitoring
          containerPort: 7472
        resources:
          limits:
            cpu: 100m
            memory: 100Mi

        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - all
            add:
            - net_raw
      ###追加 - ここから
      tolerations:
      - key: "node-role.kubernetes.io/router"
        operator: "Equal"
        effect: "NoSchedule"
      ###追加 - ここまで
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  namespace: metallb-system
  name: controller
  labels:
    app: metallb
    component: controller
spec:
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: metallb
      component: controller
  template:
    metadata:
      labels:
        app: metallb
        component: controller
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "7472"
    spec:
      serviceAccountName: controller
      terminationGracePeriodSeconds: 0
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
      - name: controller
        image: metallb/controller:master
        imagePullPolicy: Always
        args:
        - --port=7472
        - --config=config
        ports:
        - name: monitoring
          containerPort: 7472
        resources:
          limits:
            cpu: 100m
            memory: 100Mi

        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - all
          readOnlyRootFilesystem: true
      ###追加 - ここから
      tolerations:
      - key: "node-role.kubernetes.io/router"
        operator: "Equal"
        effect: "NoSchedule"
      ###追加 - ここまで
---
```

編集したマニフェストファイルを元に、MetalLB をデプロイします。

```bash
$ kubectl apply -f metallb.yaml
```

Pod 情報を確認し、Master 以外のノードに `speaker`が、Router Node に `controller` がデプロイされていればＯＫです。

```bash
$ kubectl get pod -n metallb-system -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP             NODE                         NOMINATED NODE
controller-5cc7776b6f-mxchn   1/1     Running   0          8m1s    10.244.1.4     router.k8s.holdings.local    <none>
speaker-5c5hz                 1/1     Running   0          8m38s   172.16.64.51   router.k8s.holdings.local    <none>
speaker-fd5kq                 1/1     Running   0          8m38s   172.16.64.49   worker1.k8s.holdings.local   <none>
speaker-zt9b9                 1/1     Running   0          8m38s   172.16.64.50   worker2.k8s.holdings.local   <none>
```

MetalLB の動作モードには、**BGP モード** と **L2 モード** があります。
BGP モードはクラスタネットワークの上流に BGP 対応ネットワーク機器が必要なので、今回は L2 モードで MetalLB が動作するよう設定したマニフェストファイル `metallb-l2.yaml` を準備します。
マニフェスト内の `addresses` には、Service に割り当てる External IP のアドレス範囲を指定してください。
（`172.16.131.0/24` と指定すると、`172.16.131.0` を External IP に割り当ててしまう可能性有り）

```yaml:metallb-l2.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 172.16.131.1-172.16.131.254
```

マニフェストファイルを元に、MetalLB の動作モードを設定します。


```bash
$ kubectl apply -f metallb-l2.yaml
```

## Nginx Ingress Controller のデプロイ
**Nginx Ingress Controller** は [Kubernetes.io](https://kubernetes.io/) が提供する Nginx ベースの Ingress（L7）コントローラです。
この Ingress コントローラ を Router Node 上に配置して、L7 ロードバランサ を実装します。

最初にコントローラ周りを設定するマニフェストファイル `mandatory.yaml` をダウンロードします。

```bash
$ wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml
```

コントローラが Router Node にデプロイされるよう、`tolerations` を追加します。

```yaml:mandatory.yaml
（省略）

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/part-of: ingress-nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/part-of: ingress-nginx
      annotations:
        prometheus.io/port: "10254"
        prometheus.io/scrape: "true"
    spec:
      serviceAccountName: nginx-ingress-serviceaccount
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.20.0
          args:
            - /nginx-ingress-controller
            - --configmap=$(POD_NAMESPACE)/nginx-configuration
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx
            - --annotations-prefix=nginx.ingress.kubernetes.io
          securityContext:
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            # www-data -> 33
            runAsUser: 33
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            initialDelaySeconds: 10
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
      ###追加 - ここから
      tolerations:
      - key: "node-role.kubernetes.io/router"
        operator: "Equal"
        effect: "NoSchedule"
      ###追加 - ここまで
---
```

編集したマニフェストファイルを元に、Ingress Controller をデプロイします。

```bash
$ kubectl apply -f mandatory.yaml
```

Nginx Ingress Controller のエンドポイントとなるサービス `ingress-nginx` を作成します。

```bash
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/cloud-generic.yaml
```

Pod と Service 情報を確認します。
`nginx-ingress-controller` が Router Node にデプロイされ、`ingress-nginx` Service が作成されていれば OK です。

```bash
[centos@master ~]$ kubectl get pod,svc -n ingress-nginx -o wide
NAME                                            READY   STATUS    RESTARTS   AGE    IP           NODE                        NOMINATED NODE
pod/nginx-ingress-controller-844d47b8b6-4zhrf   1/1     Running   0          4d5h   10.244.1.8   router.k8s.holdings.local   <none>

NAME                    TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                      AGE    SELECTOR
service/ingress-nginx   LoadBalancer   172.16.130.122   172.16.131.1   80:30776/TCP,443:32665/TCP   4d5h   app.kubernetes.io/name=ingress-nginx,app.kubernetes.io/part-of=ingress-nginx
```

`ingress-nginx` Service に割当てられた IP アドレス（EXTERNAL-IP）にブラウザからアクセスして **404 Not Found** と表示されれば正常に動作しています。

![FireShot Capture 20 - Welcome to nginx! - http___172.16.131.2_.png](https://qiita-image-store.s3.amazonaws.com/0/94476/2f303259-0aaf-b814-57cb-d00ecf33face.png)


# K8s DashBoard のデプロイ

今回のクラスタはオンプレミスにあるテスト環境なので、認証せずにアクセスできる DashBoard を用意します。

最初に K8s DashBoard のマニフェストファイルをダウンロードします。

```bash
$ wget https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/alternative/kubernetes-dashboard.yaml
```

ダウンロードしたマニフェストファイル `kubernetes-dashboard.yaml` を編集し、Deployment に `nodeSelector` を追加（`tolerations` 設定だけだと Worker Node にデプロイされることがある為）、Service に `type: NodePort` を追加します。

```yaml:kubernetes-dashboard.yaml
（省略）

---
# ------------------- Dashboard Deployment ------------------- #

kind: Deployment
apiVersion: apps/v1beta2
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
spec:
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      k8s-app: kubernetes-dashboard
  template:
    metadata:
      labels:
        k8s-app: kubernetes-dashboard
    spec:
      containers:
      - name: kubernetes-dashboard
        image: k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.0
        ports:
        - containerPort: 9090
          protocol: TCP
        args:
          # Uncomment the following line to manually specify Kubernetes API server Host
          # If not specified, Dashboard will attempt to auto discover the API server and connect
          # to it. Uncomment only if the default does not work.
          # - --apiserver-host=http://my-address:port
        volumeMounts:
          # Create on-disk volume to store exec logs
        - mountPath: /tmp
          name: tmp-volume
        livenessProbe:
          httpGet:
            path: /
            port: 9090
          initialDelaySeconds: 30
          timeoutSeconds: 30
      volumes:
      - name: tmp-volume
        emptyDir: {}
      serviceAccountName: kubernetes-dashboard
      # Comment the following tolerations if Dashboard must not be deployed on master
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      ###追加 - ここから
      nodeSelector:
        node-role.kubernetes.io/master: ""
      ###追加 - ここまで
---
# ------------------- Dashboard Service ------------------- #

kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kube-system
spec:
  ###追加 - ここから
  type: NodePort
  ###追加 - ここまで
  ports:
  - port: 80
    targetPort: 9090
  selector:
    k8s-app: kubernetes-dashboard
```

DashBoard Service Account にクラスタ管理者権限を付与します。

```bash
$ cat <<EOF | kubectl create -f -
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: kubernetes-dashboard
  labels:
    k8s-app: kubernetes-dashboard
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: kubernetes-dashboard
  namespace: kube-system
EOF
```

最後に DashBoard をデプロイします。

```bash
$ kubectl apply -f kubernetes-dashboard.yaml
```

Pod と Service 情報を参照し、Master Node に `kubernetes-dashboard` がデプロイされ、Service `kubernetes-dashboard` が作成されていることを確認します。 

```bash
$ kubectl get pod,svc -n kube-system -o wide
NAME                                    READY   STATUS    RESTARTS   AGE   IP            NODE                        NOMINATED NODE
･･･
kubernetes-dashboard-6c4676f6db-65sdq   1/1     Running   0          58s   10.244.0.10   master.k8s.holdings.local   <none>
･･･

NAME                           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)         AGE       SELECTOR
･･･
service/kubernetes-dashboard   NodePort    172.16.130.215   <none>        80:30246/TCP    8m33s    k8s-app=kubernetes-dashboard
･･･
```

ブラウザから `http://master.k8s.holdings.local:30246` にアクセスし、DashBoard が表示されれば完了です。
※ポート `30246` は `kubernetes-dashboard` Service の Node Port （上記）

![FireShot Capture 19 - 概要 - Kubernetes Dashboar_ - http___master.k8s.holdings.local_30246_#!_overview.png](https://qiita-image-store.s3.amazonaws.com/0/94476/79dabf2c-e660-70ea-94cb-9bb3b0ccb770.png)


#テストしてみる
K8s テスト環境上で、実際にWebアプリケーションをデプロイして動作を確認します。

テスト用の Web アプリケーションとして Nginx の Pod をデプロイします。

```bash
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1
        ports:
        - name: http
          containerPort: 80
EOF
```

`nginx` の Pod が Worker Node にデプロイされているか確認します。

```bash
$ kubectl get pod -o wide
NAME                   READY   STATUS    RESTARTS   AGE   IP            NODE                         NOMINATED NODE
nginx-cdd8d77b-szbdf   1/1     Running   0          23s   10.244.2.13   worker1.k8s.holdings.local   <none>
```

Service を作成します。`type: LoadBalancer` と設定しているので、作成された Service は L4 ロードバランサとして動作します。

```bash
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
EOF
```

作成された Service の EXTERNAL-IP を確認します。

```bash
$ kubectl get svc
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)        AGE
nginx        LoadBalancer   172.16.130.252   172.16.131.2   80:30948/TCP   14s
```

ブラウザから EXTERNAL-IP にアクセス （`http://172.16.131.2/` ）して Nginx のデフォルトページが表示されれば正常に動作しています。

![FireShot Capture 20 - Welcome to nginx! - http___172.16.131.2_.png](https://qiita-image-store.s3.amazonaws.com/0/94476/34caaa26-7da5-9907-4904-86a7b3c563d0.png)


Ingress を作成します。作成された Ingress は L7 ロードバランサとして動作します。 

```bash
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: NodePort
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: nginx.holdings.local
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: 80
EOF
```

作成された Service（実際には前述で作成された Service の `type` が `NodePort` に変更されただけ）、Ingress を確認します。

```bash
$ kubectl get svc,ing
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/nginx        NodePort    172.16.130.252   <none>        80:30948/TCP   12m

NAME                       HOSTS                  ADDRESS   PORTS   AGE
ingress.extensions/nginx   nginx.holdings.local             80      10s
```

よく見ると Ingress の `ADDRESS` が空欄になっています。

GKE では明示的に指定された IP アドレス（プライベート or パブリック）が割り当てられ、そのアドレス が Ingress の Listen IP になりますが、Nginx Ingress では Controller をデプロイする際に作成した Service `ingress-nginx` の `EXTERNAL-IP`（今回の場合、`172.16.131.1`） が Listen IP となります。

次に Ingress のマニフェストで指定したホスト名（`nginx.holdings.local`）を `ingress-nginx` の Listern IP（`172.16.131.1`）で名前解決できるよう、内部 DNS サーバに登録します。（DNSへの登録方法は省略します）

DNS サーバへの登録が終わったら、ブラウザから `http://nginx.holdings.local` にアクセスしてみてください。
Nginx のデフォルトページが表示されれば正常に動作しています。

![FireShot Capture 20 - Welcome to nginx! - http___172.16.131.2_.png](https://qiita-image-store.s3.amazonaws.com/0/94476/8b5ff3dc-2f4c-193b-b52f-f930eb7e7a9a.png)


以上で K8s テスト環境の構築は完了です。

#さいごに
- 今回は永続ディスクについて触れませんでしたが、必要に応じ NFS サーバ用の VM を立てる等の方法で永続ディスクを実装できます。
- Master Node や Router Node ですが、今回はステージング環境なためシングルノードで構成しました。もし本番環境として実装するのであれば、それらのノードが SPOF にならないよう、冗長な構成をとる必要があると思います。
- 本投稿がお役に立てたようでしたら、そっと「いいね」をお願いします。
- 次は Knative やりたい。