KubernetesをAWSで運用するときの前提条件を満たす


EKSが発表された今、AWSでのKubernetesを見直したい。

## AWSで運用するときの前提条件

KubernetesをAWSで運用しようというとき、比較的忘れがちなのが以下の条件です。

* [m4インスタンスよりm3インスタンスを推奨](https://kubernetes.io/docs/admin/cluster-large/)
* Docker Storageでdevicemapper/ループバックデバイスは非推奨

m4よりm3というのは、EBSよりアクセスが安定していて高速なインスタンスストアが利用できるからですね。
インスタンス終了とともに消えてしまうインスタンスストアですが、コンテナは揮発性なので、それが乗っかるホストのディスクも揮発性でいいでしょという…[^1]

[^1]: ただ、以前は上記のようなことがkubernetesのドキュメントに書いてましたが今は消えてるので、インスタンスストアにこだわる必要性はあまりないのかもしれません。既にm5インスタンスが利用可能になっている時代ですし…

また、Dockerのストレージドライバでdevicemapperを使用している際、`loop-lvm`モードは本番環境で推奨されていません。
http://docs.docker.jp/engine/userguide/storagedriver/device-mapper-driver.html

ループバックデバイスを使っていると、コンテナの展開は遅くなり、コンテナ内のファイル読み書き速度も劣化するはずですが、RHEL系のOSだと多くの場合、デフォルトの設定が`devicemapper`で`loop-lvm`です[^2]。

https://www.school.ctc-g.co.jp/columns/nakai/nakai68.html

[^2]: [Docker v1.13.1からデフォルトのストレージドライバが`overlay`になっている](https://github.com/moby/moby/releases/tag/v1.13.1)ので、最新のDockerをインストールしている場合は`overlay`ドライバを使用していると思います。普通に`yum install`でDockerを入れた場合1.12.6がインストールされました。


そういうわけで、この記事では、

* インスタンスストアのある`m3.large`でKubernetesクラスタを構築し、
* インスタンスストアにDockerストレージを置き`devicemapper`、`direct-lvm`モードを使う

という条件でKubernetesのクラスタを構築してみます。クラスタ構築には`kubeadm`を使用します。

## デフォルトのストレージドライバ

とはいえ、よく使われている構成ツールの場合はDockerストレージをループバックデバイスに作ることはないようです。

`kubeadm`や手作業でクラスタを構築する場合、dockerインストールは自分でやる必要があるので、RHEL系で普通にセットアップすると`docker info`の結果が以下のようになります。

```bash
# CentOSに普通にdockerをyumでインストールした場合

$ sudo docker info
Storage Driver: devicemapper
 Pool Name: docker-202:1-4210412-pool
 Pool Blocksize: 65.54 kB
 ...
 Deferred Deleted Device Count: 0
 Data loop file: /var/lib/docker/devicemapper/devicemapper/data
 WARNING: Usage of loopback devices is strongly discouraged for production use. Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.
```

WARNINGで怒られてますね。これを正しくセットアップしようというのがこの記事の主旨になります。

一方、メジャーな構成ツールである`kops`でk8sクラスタを構築すると以下の通りです。

```bash
# kopsで構成した場合のDocker Storage

Server Version: 1.13.1
Storage Driver: overlay
 Backing Filesystem: xfs
```

`kube-aws`はContainer Linuxを使用していますが、以下の様にContainer LinuxはデフォルトでDockerストレージが正しく設定されているので大丈夫そうです。

```bash
# Container Linux

Server Version: 1.12.6
Storage Driver: overlay
 Backing Filesystem: extfs
```

`kubespray`も、インストールプロセスでエラーが出たので実際の挙動は見てませんが、コード上だと`docker-storge-setup`というDockerストレージのセットアップ実行コードがあったので大丈夫そうですね。

プロダクション運用を考えた際`overlay2`や`overlay`を使用するのが本当は良さそうですが、`overlay2`にカーネルv4.0以降が必要なことやCentOSで`devicemapper`が推奨されていること、Dockerの公式ページであまり`overlay`を推奨してないことから、今回は`devicemapper`で`direct-lvm`をセットアップします。

https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/
https://docs.docker.com/engine/userguide/storagedriver/selectadriver/#docker-ce

(あと、別のディスクにDockerストレージをセットアップすることで、Dockerストレージの使用率が増えてきたときもOS、Kubernetesの動作に与える影響が少なく、安定することが期待できそう)

## m3系のインスタンスを作成し、インスタンスストアをアタッチする

まず、AWSでインスタンスを作成して、インスタンスストアをアタッチします。
今回はmaster(&etcd)1台、worker1台で構成するので2つインスタンスを立ち上げます。
パブリックサブネットに、通常通り`m3.large`のインスタンスを立ち上げますが、以下のようにインスタンスストアもアタッチしておきます。

![スクリーンショット 2017-12-13 12.04.33.png](https://qiita-image-store.s3.amazonaws.com/0/91422/ff2bcfb3-0fdf-f0ce-1962-0591097e4f65.png "スクリーンショット 2017-12-13 12.04.33.png")


セキュリティグループは、面倒だったので今回はインスタンス間全通しで行きます。
![スクリーンショット 2017-12-13 12.10.53.png](https://qiita-image-store.s3.amazonaws.com/0/91422/5da1ca2a-7414-5742-3796-250fa642fd6a.png "スクリーンショット 2017-12-13 12.10.53.png")


## Dockerのセットアップ

`kubeadm`は、実行前にクラスタのノードに`docker`や`kubelet`をインストールしておく必要があります。

https://kubernetes.io/docs/setup/independent/install-kubeadm/

ここで`docker`をインストールした際に、Dockerストレージのセットアップも実行しておきます。

```bash
$ ssh centos@k8s-master

$ sudo su -

$ yum install -y docker
```

awsのインスタンスストアは以下のようにマウント済みなので、そのままDockerストレージとしてセットアップしようとすると失敗します。
そのため、前もってアンマウントします。


```bash
$ lsblk

NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0   8G  0 disk 
└─xvda1 202:1    0   8G  0 part /
xvdb    202:16   0  30G  0 disk /mnt

$ umount /mnt
$ lsblk

NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0   8G  0 disk 
└─xvda1 202:1    0   8G  0 part /
xvdb    202:16   0  30G  0 disk 
```

インスタンスストアの`/dev/xvdb`デバイスをDockerストレージとしてセットアップします。

```bash
# 設定ファイルの作成
$ cat << EOT > /etc/sysconfig/docker-storage-setup
DEVS=/dev/xvdb
VG=docker-vg
WIPE_SIGNATURES=true
EOT

# 以下コマンドでDockerストレージをセットアップ
$ docker-storage-setup

INFO: Volume group backing root filesystem could not be determined
INFO: Wipe Signatures is set to true. Any signatures on /dev/xvdb will be wiped.
/dev/xvdb: 2 bytes were erased at offset 0x00000438 (ext3): 53 ef
INFO: Device node /dev/xvdb1 exists.
  Physical volume "/dev/xvdb1" successfully created.
  Volume group "docker-vg" successfully created
  Using default stripesize 64.00 KiB.
  Rounding up size to full physical extent 32.00 MiB
  Thin pool volume with chunk size 512.00 KiB can address at most 126.50 TiB of data.
  Logical volume "docker-pool" created.
  Logical volume docker-vg/docker-pool changed.
  
$ lsblk

NAME                              MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda                              202:0    0   8G  0 disk 
└─xvda1                           202:1    0   8G  0 part /
xvdb                              202:16   0  30G  0 disk 
└─xvdb1                           202:17   0  30G  0 part 
  ├─docker--vg-docker--pool_tmeta 253:0    0  32M  0 lvm  
  │ └─docker--vg-docker--pool     253:2    0  12G  0 lvm  
  └─docker--vg-docker--pool_tdata 253:1    0  12G  0 lvm  
    └─docker--vg-docker--pool     253:2    0  12G  0 lvm  
```

Dockerストレージがセットアップできました。`docker`サービスを起動します。

```bash
$ systemctl enable docker && systemctl start docker
$ docker info

Storage Driver: devicemapper
 Pool Name: docker--vg-docker--pool
 Pool Blocksize: 524.3 kB
 Base Device Size: 10.74 GB
 Backing Filesystem: xfs
 Data file: 
 Metadata file: 
 Data Space Used: 19.92 MB
 Data Space Total: 12.81 GB
 Data Space Available: 12.79 GB
 Metadata Space Used: 45.06 kB
 Metadata Space Total: 33.55 MB
 Metadata Space Available: 33.51 MB
 Thin Pool Minimum Free Space: 1.281 GB
 Udev Sync Supported: true
 Deferred Removal Enabled: true
 Deferred Deletion Enabled: true
 Deferred Deleted Device Count: 0
 Library Version: 1.02.140-RHEL7 (2017-05-03)
```

このように、ループバックデバイスに関するWARNINGが消えていることが確認できました。

同様の処理をworkerに対しても実施します。

## kubeadmによるクラスタの初期化
https://kubernetes.io/docs/setup/independent/install-kubeadm/

ドキュメントを参考に、`kubeadm`でクラスタを初期化します。

`kubeadm`はまだβ段階で、プロダクション運用にはまだ早いですが、簡単にクラスタを構築できるのでここでは`kubeadm`でクラスタを構築してみます。

### kubernetesのmasterを初期化

`kubeadm init`コマンドでmasterを初期化します。
なお、`kubeadm init`コマンドが成功した際に表示される、

```
kubeadm join --token xxxx
```

というコマンドは後でWorkerを追加する際に必要になるので控えておきます。

```bash
# yumリポジトリの追加
$ 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関連パッケージの追加
yum install -y kubelet kubeadm kubectl

# kubeletの開始
systemctl enable kubelet && systemctl start kubelet

# kubeadmのセットアップはselinuxを無効化する必要があるようです
$ setenforce 0

# kubeadmでmasterを初期化
$ kubeadm init --pod-network-cidr=10.244.0.0/16

# 以下コマンドが表示されるので控えておく(Worker追加の際に使用する)
# kubeadm join --token xxx

# kubectlをAPIサーバーへアクセスできるよう設定
$ mkdir -p $HOME/.kube
$ cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ chown $(id -u):$(id -g) $HOME/.kube/config

# podネットワーク用のadd-onを追加(今回はflannelを使用)
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/v0.9.1/Documentation/kube-flannel.yml

# masterがReadyになるのを待つ
watch kubectl get nodes
```

### kubernetesのworkerを追加

worker用に作成したec2インスタンスにsshログインし、上記で初期化したクラスタにJOINさせます。

```bash
$ ssh centos@k8s-worker
$ sudo su -

# yumリポジトリの追加
$ 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のセットアップはselinuxを無効化する必要があるようです
$ setenforce 0

# k8s関連パッケージの追加
yum install -y kubelet kubeadm kubectl

# kubeletの開始
systemctl enable kubelet && systemctl start kubelet

# kubeadmでWorkerを追加する。
# kubeadm initコマンドの完了時に表示されたコマンドをworkerインスタンスで実行する
$ kubeadm join --token xxxx 10.0.157.43:6443 --discovery-token-ca-cert-hash xxx

kubeadm join --token 2f80ec.3db07de32fde6f62 10.0.157.43:6443 --discovery-token-ca-cert-hash sha256:5db1f5a2fb1694bd9ad986a8367351632499d9668f30373797d1b74d85456b40
[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters.
[preflight] Running pre-flight checks
[preflight] WARNING: kubelet service is not enabled, please run 'systemctl enable kubelet.service'
[preflight] Starting the kubelet service
[discovery] Trying to connect to API Server "10.0.157.43:6443"
[discovery] Created cluster-info discovery client, requesting info from "https://10.0.157.43:6443"
[discovery] Requesting info from "https://10.0.157.43:6443" again to validate TLS against the pinned public key
[discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server "10.0.157.43:6443"
[discovery] Successfully established connection with API Server "10.0.157.43:6443"
[bootstrap] Detected server version: v1.8.5
[bootstrap] The server supports the Certificates API (certificates.k8s.io/v1beta1)

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.
```

master側で、workerがjoinしたことを確認します。

```bash
$ ssh centos@k8s-master
$ sudo su -

$ watch kubectl get nodes

NAME                                  STATUS    ROLES     AGE	VERSION
ip-x.ap-northeast-1.compute.internal  Ready     <none>    45m   v1.8.5
ip-x.ap-northeast-1.compute.internal  Ready     master    1h    v1.8.5
```

nginxでも走らせてみますか。

```bash
# masterで
$ kubectl run my-nginx --image=nginx --replicas=2 --port=80
$ kubectl expose deployment my-nginx --type=NodePort
$ kubectl describe services my-nginx

Name:                     my-nginx
Namespace:                default
Labels:                   run=my-nginx
Annotations:              <none>
Selector:                 run=my-nginx
Type:                     NodePort
IP:                       10.99.129.58
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  32017/TCP
Endpoints:                10.244.1.2:80,10.244.1.3:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

# NodePortでアクセスして表示されることを確認
$ curl http://$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4):32017
```

というわけで、Dockerストレージを`direct-lvm`モードで動かしてkubernetesクラスタを動かすことができました。

# まとめ

Kubernetesを使用していて、コンテナのデプロイが遅いとか、コンテナ内ファイルアクセスが遅いとか、そういった問題を感じるようでしたら一度Dockerストレージの設定を見直してみてはいかがでしょうか。