AWS EC2でkubeadmを使って複数ノードKubernetes環境を作る


## はじめに
AWS EC2インスタンスを複数台立ち上げKubernetes環境を構築する。インスタンスそれぞれにワーカーノードを起動させてCluster IPによるロードバランスが行えるのかを確認する手順を以下に示す。

### 前提
EC2インスタンスを扱えること
Ubuntuを扱えること
Kubernetesをなんとなく知ってること
AWSに課金してもいい人(無料枠対象外のインスタンスを使うため)

### 構成
下図の通りマスターノード1台、ワーカーノード2台構成とする。
![kubernetes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/945324/7d71f0a1-d757-6469-a865-de0b7ba22d9e.png)

ワーカーノードにはDockerコンテナ上にnginxを走らせ、80ポートで待ち受ける。
Pod間はFlannelによりネットワークを形成ずる。
NodePortサービスにより各ノードの30080ポートで待ち受け、ClusterIPへ転送し各Podの8080ポートにロードバランスする。

## AWS EC2の設定
まずはEC2の設定を行う。

### セキュリティグループの作成
セキュリティグループの初期状態ではすべてのポートが塞がれているので必要なポートを開放する。
kubeadmを使った構築手順は書籍やネットでいくつか見かけるが、EC2を使う場合この手順がないとPod間通信ができないためだいぶハマりました。
マスターノード用の「kubemaster」とワーカーノード用の「kubeworker」の2つのセキュリティグループを作成する。
以下すべて「インバウンドルール」を指す。「ソース」欄には接続を許可する対象のセキュリティグループを指定する。
参考URL：[^1]

#### セキュリティグループ「kubemaster」

| TCP/UDP | ポート番号 | ソース | 用途 |
|:-:|:-:|:-:|:-:|
| TCP  |  6443 | kubeworker  |  Kubernetes API server |
| TCP  |  2379-2380 | kubeworker  |  kube-api server, etcd |
|  TCP | 10250  | kubeworker | Kubelet API  |
|  TCP |  10251 | kubeworker  | kube-scheduler  |
|  TCP | 10252  | kubeworker  | kube-controller-manager  |
|  TCP |  30000-32767 | kubeworker  | Nordport Service  |
|  UDP |  8285 | kubeworker  | flannel  |
|  UDP |  8472 | kubeworker  | vxlan  |
(sshはお好みで設定する)

#### セキュリティグループ「kubeworker」

| TCP/UDP | ポート番号 | ソース | 用途 |
|:-:|:-:|:-:|:-:|
|  TCP | 10250  | kubemaster, kubeworker | Kubelet API  |
|  TCP |  30000-32767 | kubemaster, kubeworker  | Nordport Service  |
|  UDP |  8285 | kubemaster, kubeworker  | flannel  |
|  UDP |  8472 | kubemaster, kubeworker  | vxlan  |
(sshはお好みで設定する)

### ノードインスタンスの立ち上げ
EC2インスタンスを「マスターノード」×1台、「ワーカーノード」×2台作成する。
Kubernetesの制約によりマスターノードは2CPU以上の構成が必要なため、t2.mediumを使用する。
**ただし「t2.medium」は無料枠対象ではないので注意すること(課金が発生する)**

|  | マスターノード×1台 | ワーカーノード×2台 |
|:-:|:-:|:-:|
| ステップ 1: Amazon マシンイメージ (AMI)  |  Ubuntu Server 20.04 LTS  |  Ubuntu Server 20.04 LTS  |
| ステップ 2: インスタンスタイプの選択  |  t2.medium **(無料枠対象外)** | t2.micro  |
| ステップ 3: インスタンスの詳細の設定  |  「サブネット」をワーカーノードと合わせる | 「サブネット」をマスターノードと合わせる  |
| ステップ 4: ストレージの追加  |  デフォルトのまま(gp2 8GB) |  デフォルトのまま(gp2 8GB) |
|  ステップ 5: タグの追加 |  デフォルトのまま |  デフォルトのまま |
| ステップ 6: セキュリティグループの設定  |  「kubemaster」を選択 | 「kubeworker」を選択  |


## ノードのセットアップ(マスターノード、ワーカーノード共通)
以下、EC2にログインして実行する。
本手順はマスターノード、ワーカーノード共通の手順を示している。
3台のノードのセットアップは以下の通り行う。

1. マスターノード：「マスター・ワーカーセットアップ共通手順」→「マスターノードセットアップ手順」
1. ワーカーノード#1：「マスター・ワーカーセットアップ共通手順」→「ワーカーノードセットアップ手順」
1. ワーカーノード#2：「マスター・ワーカーセットアップ共通手順」→「ワーカーノードセットアップ手順」

#### aptの最新化
Ubuntu20.04の初期状態なので最新化する。

```.sh
$ sudo apt update
$ sudo apt -y upgrade 
```

#### iptabesの設定
KubernentesはPodネットワークへの転送などにiptableを使用するのでその設定を行う。
参考URL：[^2]

```.sh
# iptablesがブリッジを通過するトラフィックを処理できるようにする
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
> net.bridge.bridge-nf-call-ip6tables = 1
> net.bridge.bridge-nf-call-iptables = 1
> EOF
$ sudo sysctl --system

# レガシーバイナリがインストールされていることを確認
$ sudo apt-get install -y iptables arptables ebtables

# レガシーバージョンに切り替え
$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
$ sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
$ sudo update-alternatives --set arptables /usr/sbin/arptables-legacy
$ sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy
```

#### dockerのインストール
Kubernetes上で動作するコンテナはいくつか選べるが、今回はDockerを使用する。
参考:URL：[^3]

```.sh
# リポジトリをセットアップ
$ sudo apt-get update && sudo apt-get install -y \
> apt-transport-https ca-certificates curl software-properties-common gnupg2

# Docker公式のGPG鍵を追加
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# Dockerのaptレポジトリを追加
$ sudo add-apt-repository \
>   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
>   $(lsb_release -cs) \
>   stable"

# Docker CEのインストール
$ sudo apt-get update && sudo apt-get install -y \
> containerd.io=1.2.13-2 \
> docker-ce=5:19.03.11~3-0~ubuntu-$(lsb_release -cs) \
> docker-ce-cli=5:19.03.11~3-0~ubuntu-$(lsb_release -cs)

# デーモンをセットアップ
$ cat <<EOF | sudo tee /etc/docker/daemon.json
> {
>   "exec-opts": ["native.cgroupdriver=systemd"],
>   "log-driver": "json-file",
>   "log-opts": {
>     "max-size": "100m"
>   },
>   "storage-driver": "overlay2"
> }
> EOF

# dockerサービスの作成
$ sudo mkdir -p /etc/systemd/system/docker.service.d

# dockerを再起動
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

# ブート時にdockerを有効化
$ sudo systemctl enable docker
```

#### Kubernetesのインストール
いよいよKubernetesをインストールする。とはいってもaptでインストールするだけ。
参考URL：[^4]

```.sh
$ sudo apt-get update && sudo apt-get install -y apt-transport-https curl
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
> deb https://apt.kubernetes.io/ kubernetes-xenial main
> EOF
$ sudo apt-get update
$ sudo apt-get install -y kubelet kubeadm kubectl
$ sudo apt-mark hold kubelet kubeadm kubectl
```

以上がマスターノード、ワーカーノード共通の設定手順となる。以降はマスター、ワーカーそれぞれに分けた手順を示す。

## マスターノードのセットアップ
以下の手順はマスターノードのみで行う。sshでログインすること。

### マスターノードの初期化
以下のコマンドを実行してマスターノードを初期化する。
後述するflannelのデフォルト設定より、podネットワークのCIDRは「10.244.0.0/16」と決めてしまう。

```.sh
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16
```

実行したら大量のメッセージが表示されるが、最後のほうで以下のメッセージが表示される。

```
(メッセージの最後部)
Your Kubernetes control-plane 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

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

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/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 172.31.5.34:6443 --token m42coj.g3wylraxfrhbrbfd \
    --discovery-token-ca-cert-hash sha256:0f7c9b9d211e786dfa64ac6fd6ee6c38a2aa7c6e1f2808e4510bd6457c27159e
```

メッセージに従い、以下を実行

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

以下のメッセージはワーカーノードのセットアップで使用するのでメモっておくこと。(下記tokenやhashは各自環境で異なるので必ずメモること)

```
kubeadm join 172.31.5.34:6443 --token m42coj.g3wylraxfrhbrbfd \
    --discovery-token-ca-cert-hash sha256:0f7c9b9d211e786dfa64ac6fd6ee6c38a2aa7c6e1f2808e4510bd6457c27159e
```

### Podネットワークのセットアップ

KubernetesはPod間の通信ネットワークを自由に選べる。今回はflannelを使用するので以下を実行する。
このyamlファイル内にpodネットワークのCIDRが「10.244.0.0/16」と記載されているので前述のマスターノードのセットアップ時にこのCIDRを使用した。

```.sh
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/2140ac876ef134e0ed5af15c65e414cf26827915/Documentation/kube-flannel.yml
```

マスターノードのセットアップは以上。

## ワーカーノードのセットアップ
以下の手順はワーカーノードのみで行う。
マスターノードのセットアップ時にメモった以下のコマンドにsudoを付けて実行する。(コマンド内のtokenやhashは各自の環境で異なるので以下のコマンドをそのまま実行しないこと)

```.sh
sudo kubeadm join 172.31.5.34:6443 --token m42coj.g3wylraxfrhbrbfd \
    --discovery-token-ca-cert-hash sha256:0f7c9b9d211e786dfa64ac6fd6ee6c38a2aa7c6e1f2808e4510bd6457c27159e
```

ワーカーノードのセットアップは以上。

## 動作確認

ここまででワーカーノード2台によるKubernetes環境が構築できたので、ちゃんとロードバランスされるか動作確認を行う。
以下の手順はマスターノード上で行うこと。

### ワーカーノードが立ち上がってるか確認する

以下のコマンドを実行し、各ノードのSTATUSがReadyとなっていることを確認する。

```.sh
$ kubectl get nodes
NAME              STATUS   ROLES                  AGE     VERSION
ip-172-31-1-3     Ready    <none>                 2m33s   v1.20.0
ip-172-31-13-49   Ready    <none>                 99s     v1.20.0
ip-172-31-5-34    Ready    control-plane,master   6m49s   v1.20.0
```

マスターノード1台(ROLESがcontrol-plane,masterとなっているもの)、ワーカーノード2台(ROLESがnoneとなっているもの)が立ち上がっていることが確認できる。

### ワーカーノード2台に対しnginxをデプロイする

以下のyamlファイルを作成する。nginxコンテナを2台作成するデプロイ定義となっている。

```testdeploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: testdeploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: testnginx
  template:
    metadata:
      labels:
        app: testnginx
    spec:
      containers:
        - name: testcontainer
          image: nginx
          ports:
            - containerPort: 80
```

以下を実行してワーカーノードにデプロイする。

```.sh
$ kubectl apply -f testdeploy.yaml
```

デプロイされたか以下のコマンドで確認する。STATUSがRunnningとなっていれば起動済み。Runnningとなるまで30秒ほどかかるのでちょっと待ってから実行してみること。

```.sh
$ kubectl get pods -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE              NOMINATED NODE   READINESS GATES
testdeploy-fb9d68c54-4l8fn   1/1     Running   0          25s   10.244.1.2   ip-172-31-1-3     <none>           <none>
testdeploy-fb9d68c54-w6628   1/1     Running   0          25s   10.244.2.2   ip-172-31-13-49   <none>           <none>
```

各NODEにpodが作成されていることがわかる。
次にロードバランスされていることを確認するため、以下を実行してindex.htmlをpod名に書き換える。

```.sh
$ for POD in $(kubectl get pods -o=jsonpath={.items[*].metadata.name}); 
> do 
> kubectl exec -it $POD -- sh -c "echo $POD > /usr/share/nginx/html/index.html"
> done
```

### NodePortサービスを設定する

前章で作成したpodはpodネットワーク内からのみアクセス可能なのでマスターノード上からHTTPリクエストを投げてもnginxには届かない。そこでNodePortを使って外部からアクセスできるようにする。

以下のyamlファイルを作成する。
targetPort(nginxの待ち受けポート)をport(Podの待ち受けポート)に転送し、さらにnodePort(ノードの待ち受けポート)に転送する定義となっている。

```testnodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: testnodeport
spec:
  type: NodePort
  ports:
    - name: "http"
      protocol: "TCP"
      targetPort: 80
      port: 8080
      nodePort: 30080
  selector:
    app: testnginx
```

以下を実行してサービスを作成する。

```.sh
$ kubectl apply -f testnodeport.yaml
```

作成されたか以下のコマンドで確認する。

```.sh
$ kubectl get service
NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes     ClusterIP   10.96.0.1      <none>        443/TCP          15m
testnodeport   NodePort    10.107.227.1   <none>        8080:30080/TCP   10s
```

### NodePortに対しHTTPリクエストを投げてみる

NodePortはマスターノードに対しても作成されるのでlocalhostの30080番ポートに対してHTTPリクエストを投げてみる。

```.sh
$ wget -q -O- localhost:30080
```

以下のように何度か投げてみると複数のpodから応答があり、ロードバランスされていることが確認できるだろう。

```.sh
ubuntu@ip-172-31-5-34:~$ wget -q -O- localhost:30080
testdeploy-fb9d68c54-4l8fn
ubuntu@ip-172-31-5-34:~$ wget -q -O- localhost:30080
testdeploy-fb9d68c54-w6628
ubuntu@ip-172-31-5-34:~$ wget -q -O- localhost:30080
testdeploy-fb9d68c54-4l8fn
ubuntu@ip-172-31-5-34:~$ wget -q -O- localhost:30080
testdeploy-fb9d68c54-w6628
```

## 参考
[^1]: kubeadmのインストール(https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#%E5%BF%85%E9%A0%88%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E7%A2%BA%E8%AA%8D)
[^2]: iptablesの設定(https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#iptables%E3%81%8C%E3%83%96%E3%83%AA%E3%83%83%E3%82%B8%E3%82%92%E9%80%9A%E9%81%8E%E3%81%99%E3%82%8B%E3%83%88%E3%83%A9%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%92%E5%87%A6%E7%90%86%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B)
[^3]: Dockerのインストール(https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/#docker)
[^4]: kubeadm、kubelet、kubectlのインストール(https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#kubeadm-kubelet-kubectl%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)