ニフクラでも Kubernetes をいい感じに使いたい！

# はじめに

この記事は [富士通クラウドテクノロジーズ Advent Calendar 2019]
 (https://qiita.com/advent-calendar/2019/fjct) の18日目です。

昨日は @sasachi1231 さんの「[Botを使ってSlackで簡単#NowPlaying](https://qiita.com/sasachi1231/items/ca61b1deafd8e5baeca3)」でした。

企画職の方でも Bot の作成等は比較的簡単に実装できるほうだと思うので、どんどん Slack を便利ツールにしたり (面白くしたり) していってほしいですね！

改めましてこんにちは！ニフクラでコンテナ系サービスの開発をしている @aokuma と申します。なんだかんだもう 4 年目の社員になってしまいました…。はやい…。:innocent:
本日は私が業務で触ることの多い、 Kubernetes に関する話を書きたいと思います。

内容は主に、ニフクラ上で Kubernetes クラスターを構築・利用するときに悩むと思われる

**「他社クラウドプロバイダーの Kubernetes では普通に使えるアレがニフクラ上だと使えない！」**

に対する自分なりの答えを書いていきます。

# 注意

今回紹介する内容はニフクラ Computing で VM を作り、その上に手動で Kubernetes クラスターを構築することが前提です。マネージド Kubernetes サービスであるニフクラ Hatoba（β）ではこの記事の方法を使うことはできません。

# 悩み

さて。早速ですが、個人的に初めてニフクラ上に Kubernetes クラスターを構築して遊んでいたとき (思えば v1.4 とかの頃…)、下記のようなことでよく悩みました。

1. type: LoadBalancer が使えない… :sob:
1. PersistentVolume をいい感じに使いたい… :sob:

細かいものを挙げるとキリがなくなるのでこれくらいで…。
では、これらに対する自分なりの解決策を書いていきます…！:grin:

# type: LoadBalancer が使いたい

ここで言う `type: LoadBalancer` とは、 Kubernetes の Service のタイプのことです。 
`LoadBalancer` というタイプを Service に設定すると、クラウドプロバイダーのロードバランサーリソースを作成し、それ経由でクラスタ内のサービスを外部に公開することができます。
参考: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types

GKE 等のマネージドサービスでは `type: LoadBalancer` なサービスを作成するだけでクラウド上にロードバランサーリソースを作成してくれますが、自前で構築したクラスタではそう簡単にはできません。

Kubernetes にはクラウドリソースを管理する Controller がいくつか存在しており (cloud node, cloud node lifecycle, service, route controller)、その Controller 内の処理でクラウドプロバイダー固有の処理が発生しそうなものについては、外からプラグイン形式で挙動を差し込めるようになっています。この Controller のまとまりが [Cloud Controller Manager (CCM)](https://kubernetes.io/docs/tasks/administer-cluster/running-cloud-controller/) と呼ばれるものです。

CCM では `cloudprovider.Interface` (https://github.com/kubernetes/cloud-provider/blob/master/cloud.go) に定義されているメソッドを Go 言語で実装することで、クラウドプロバイダーによって差のある処理を吸収しています。(前述の Controller 内から、このインターフェースのメソッドを呼んでいます。)

例えば [AWS 向けの実装](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/legacy-cloud-providers/aws/aws.go) や [GCE 向けの実装](https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/legacy-cloud-providers/gce/gce.go) はこんな感じです。

…というわけでニフクラ向け CCM を書いてみたのがこちら :grin: 

https://github.com/aokumasan/nifcloud-cloud-controller-manager

動作している様子は後で紹介します！


# PersistentVolume が使いたい

続いては PersistentVolume (永続化ボリューム) です。
コンテナを使う上で、アプリケーションは基本的にはステートレスなことが望ましいですが、そうもいかない場面はどうしても存在します。
そこで登場するのが PersistentVolume です。コンテナに永続的なデータの置き場を提供してくれるリソースです。

Kubernetes の機能として、以前から AWS の EBS を PersistentVolume として利用する機能等は存在していましたが、その処理は Kubernetes のソースコードに内蔵されており、簡単に拡張できるものではありませんでした。 (その処理は現在 CCM に移されているようです)

また、クラウドプロバイダーが提供してくれるストレージ以外で PersistentVolume を利用する手段として、 NFS を利用した [nfs-provisiner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs) 等がありました。
現在でも様々な場面で用いられていると思いますが、場合によっては NFS の性能に足を引っ張られてしまうこともあり、用途は限定されてしまうかもしれません。

そして、Kubernetes v1.13 からは Container Storage Interface (CSI) が GA となり、 [CSI の仕様](https://github.com/container-storage-interface/spec)に準拠したドライバーを書くことで、 Kubernetes のストレージ周りの挙動を自由にカスタマイズすることができました。
現在、各クラウドプロバイダーの CCM にコミットされていた Volume 周りの処理は deprecated とし、 CSI に順次移行を進めているようです。

例えば

* https://github.com/kubernetes-sigs/aws-ebs-csi-driver
* https://github.com/kubernetes-sigs/gcp-compute-persistent-disk-csi-driver

この辺がその CSI ドライバー実装です。

…というわけで例によってニフクラの増設ディスク向けの CSI ドライバを書いてみたのがこちら :grin: 

https://github.com/aokumasan/nifcloud-additional-storage-csi-driver

これによって Pod に高速タイプや SSD の増設ディスクをマウントすることができるようになり、ステートフルなアプリケーションも Kubernetes 上で動かしやすくなります！

動作している様子は次に紹介します！


# 動作している様子

では、ニフクラ向け CCM と CSI ドライバーを動かしてみます。
動かす環境として、先日リリースされた、ゾーン間・リージョン間のプライベートネットワークを簡単に接続することができる[プライベートブリッジ](https://pfs.nifcloud.com/service/privatebridge.htm)というサービス活用し、 3 ゾーンにまたがる高可用性クラスタを作成しました。(余談ですが、プライベートブリッジによってニフクラで実現できる幅が大幅に広がり、とても嬉しいです。:tada:)

各種情報は下記の通りです。

* リージョン: east-1 
* ゾーン: east-11, east-12, east-13
* OS: Ubuntu 18.04
* Docker: 19.03.4
* Kubernetes: v1.17.0

## クラスタの構築

下記のような構成のクラスタを作成します。
注意点として、ニフクラにはプライベート側でゾーン間をまたいでトラフィックをバランシングできるロードバランサーリソースが存在しないため、 HAProxy と Keepalived を組み合わせて代用しています。(今回はコストカットのため 2 ゾーンにしか配置していません)

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72278/5ff95a55-32f7-3f59-016f-f78c464d652e.png)

* Bastion: 踏み台
* MasterLB: HAProxy + Keepalived を使った kube-apiserver のフロント LB
* Master: kube-apiserver, kube-controller-manager, kube-scheduler, cloud-controller-manager, etcd
* Node: docker, kubelet

構築手順は基本的には https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/high-availability/ にかかれている内容通りで問題ありませんが、 CCM を使う関係上、

* open-vm-tools の設定の [exclude-nics](https://github.com/vmware/open-vm-tools/wiki/Configuration) で ens160, 192 以外を除外する (docker0 や CNI が利用するインターフェース名を除外)
    * CCM が間違った IP アドレスを API から取得してしまうため
    * 参考: https://www.slideshare.net/Fujitsu_CloudTechnologies/dockertips-118090943
* kubelet に `--cloud-provider=external` のオプションを渡す
    * `kubeadm init` する際に `--config` で下記のような設定を渡すことになるはず

```yaml
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
nodeRegistration:
  kubeletExtraArgs:
    cloud-provider: external
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
controlPlaneEndpoint: "LOAD_BALANCER_DNS:LOAD_BALANCER_PORT"
```

の設定は必要です。

## CCM, CSI のデプロイ

基本的には README に書かれたとおりで大丈夫です。

### CCM

```sh
git clone https://github.com/aokumasan/nifcloud-cloud-controller-manager.git
cd nifcloud-cloud-controller-manager
vi manifests/nifcloud-cloud-controller-manager.yaml # 認証情報を編集
kubectl apply -f manifests/nifcloud-cloud-controller-manager.yaml
```

### CSI

```sh
git clone https://github.com/aokumasan/nifcloud-additional-storage-csi-driver.git
cd nifcloud-additional-storage-csi-driver
vi deploy/kubernetes/secret.yaml # 認証情報を編集
vi deploy/kubernetes/overlays/dev/node.yaml # ゾーン定義を削除・認証情報を渡すように定義しなおす
vi deploy/kubernetes/overlays/dev/kustomization.yaml # node.yaml を patch するように修正
kubectl apply -f deploy/kubernetes/secret.yaml
kubectl apply -k deploy/kubernetes/overlays/dev
```

ファイル内容は下記です。

```yaml:node.yaml
---
kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: nifcloud-storage-csi-node
  namespace: kube-system
spec:
  template:
    spec:
      containers:
        - name: nifcloud-storage-driver
          env:
            - name: NIFCLOUD_REGION
              value: jp-east-1
            - name: NIFCLOUD_ZONE
              value: ""
            - name: NIFCLOUD_ACCESS_KEY_ID
              valueFrom:
                secretKeyRef:
                  name: nifcloud-secret
                  key: access_key_id
            - name: NIFCLOUD_SECRET_ACCESS_KEY
              valueFrom:
                secretKeyRef:
                  name: nifcloud-secret
                  key: secret_access_key
```

```yaml:kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base

patches:
- node.yaml
```

## type: LoadBalancer

さて、では `type: LoadBalancer` な Service を作ってみましょう！
サンプルとして、下記のようなマニフェストを作成してみました。 Nginx の Pod を 3 つ、それを LoadBalancer で公開します。

```yaml:loadbalancer_test.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
  annotations:
    service.beta.kubernetes.io/nifcloud-load-balancer-accounting-type: "2"
spec:
  type: LoadBalancer
  ports:
    - port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: nginx
```

apply します。

```
root@master111:~# kubectl apply -f loadbalancer_test.yaml
deployment.apps/nginx created
service/nginx created

root@master111:~# kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-5c559d5697-9m25j   1/1     Running   0          24s
nginx-5c559d5697-b546j   1/1     Running   0          24s
nginx-5c559d5697-qqhp5   1/1     Running   0          24s

root@master111:~# kubectl get svc
NAME         TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)        AGE
kubernetes   ClusterIP      10.96.0.1     <none>            443/TCP        23h
nginx        LoadBalancer   10.96.19.93   111.xxx.xxx.xxx   80:30125/TCP   41s
```

コンパネを開き、ロードバランサーのページを見てみます。

<img width="1384" alt="lb.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72278/b40f5595-390c-bc7d-7655-731c9d571550.png">

おー！！ロードバランサーが作られている！そしてノード 3 台がバランシングターゲットに設定されているみたいです！

ブラウザでロードバランサーの VIP にアクセスすると、ちゃんと Nginx のデフォルトのページが表示されます :tada: 

<img width="845" alt="Nginx.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72278/32f4b845-9a12-89cd-6675-7f7cc577d773.png">

やりたかったことの一つ、とりあえず達成です :tada: :tada: :tada: 


## 増設ディスクを使った PersistentVolume

さて次は CSI ドライバーを使って、増設ディスクを Pod にマウントしてみます。
サンプルとして次のようなマニフェストを作成しました。

```yaml:pv_sample.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: nifcloud-additional-storage-standard
provisioner: additional-storage.csi.nifcloud.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  csi.storage.k8s.io/fstype: ext4
  type: standard
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nifcloud-as-claim
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: nifcloud-additional-storage-standard
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - name: app
        image: alpine
        command: ["tail", "-f", "/dev/null"]
        volumeMounts:
        - name: persistent-storage
          mountPath: /data
      volumes:
      - name: persistent-storage
        persistentVolumeClaim:
          claimName: nifcloud-as-claim
```

apply します。

```
root@master111:~# kubectl apply -f pv_sample.yaml
storageclass.storage.k8s.io/nifcloud-additional-storage-standard created
persistentvolumeclaim/nifcloud-as-claim created
deployment.apps/app created
```

しばらくすると、増設ディスクが作成され、 Kubernetes 上でも PersistentVolume として認識されます。
そして Pod が稼働し始めます。

<img width="1274" alt="volume.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72278/db008116-7f66-6f89-e1f6-490de0689546.png">

```
root@master111:~# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                       STORAGECLASS                           REASON   AGE
pvc-23b6462d-1edd-453e-9ea9-e49142a391ec   100Gi      RWO            Delete           Bound    default/nifcloud-as-claim   nifcloud-additional-storage-standard            110s

root@master111:~# kubectl get po
NAME                   READY   STATUS    RESTARTS   AGE
app-86764f79d4-w2g5t   1/1     Running   0          2m14s
```

Pod の中に入って、マウントしている増設ディスクの中になにかを書き込んでみます。

```
root@master111:~# kubectl exec -it app-5d568d4d98-z4rnd -- ash
/ # echo "from app-5d568d4d98-z4rnd" > /data/out.txt
/ # cat /data/out.txt
from app-5d568d4d98-z4rnd
```

ではこの Pod を再起動してみます。永続化されていなければ再起動によってデータは消えてしまいます。

```
root@master111:~# kubectl delete po app-5d568d4d98-z4rnd
pod "app-5d568d4d98-z4rnd" deleted

root@master111:~# kubectl get po
NAME                   READY   STATUS    RESTARTS   AGE
app-5d568d4d98-cfdpn   1/1     Running   0          46s

root@master111:~# kubectl exec -it app-5d568d4d98-cfdpn -- ash
/ # cat /data/out.txt
from app-5d568d4d98-z4rnd
```

ちゃんとデータが増設ディスクに書き込まれて、永続化できているようです :tada: :tada: :tada: 

注意点として、増設ディスクはマウント対象のノードと同じゾーンに存在しなければならないため、今回の構成の場合どれか 1 ノードが死ぬとその Pod が他のゾーンのノードに移動することはできません。
同一ゾーンに複数のノードが存在し、かつノードが生きていればそのノードに Pod が移動し、ディスクがマウントされ、Pod からまたアクセスできるようになります。

試しに Pod が稼働しているノードを drain してみると、 Pod はスケジューリングされずに Pending のままになってしまいます。

```
root@master111:~# kubectl get po -o wide
NAME                   READY   STATUS    RESTARTS   AGE     IP          NODE      NOMINATED NODE   READINESS GATES
app-5d568d4d98-cfdpn   1/1     Running   0          2m47s   10.34.0.2   node121   <none>           <none>

root@master111:~# kubectl drain node121
node/node121 cordoned
evicting pod "app-5d568d4d98-cfdpn"
pod/app-5d568d4d98-cfdpn evicted
node/node121 evicted

root@master111:~# kubectl get po
NAME                   READY   STATUS    RESTARTS   AGE
app-5d568d4d98-slwzx   0/1     Pending   0          40s
```

この辺は Pod の配置や Node 数の設計が大事になってきそうです。

※今回は構成上試せませんでしたが、1 ゾーンに複数ノードが存在する Kubernetes クラスターで試し、別ノードに Pod とディスクが移動することは確認済みです。

# さいごに

いかがでしたでしょうか！今年でアドベントカレンダーを書き始めて 4 年目ですが、過去イチのボリュームになりました…。(制作時間も…。:innocent:)
というわけでニフクラ上で Kubernetes を自前で立てつつ、 Kubernetes の仕組みをもっと活用したい場合は今回紹介した内容を参考にしてみていただけると幸いです！ PR も大歓迎ですので、バグ等を見つけたら修正お待ちしております :bow:

また、富士通クラウドテクノロジーズでは Kubernetes を活用したサービスや Kubernetes の Controller、 Custom API Server を書いている人たちとかも居ます。興味があったら我々と一緒に働いてみませんか？ :eyes: 

さて、明日は @o108minmin さんがなにか書いてくれるみたいです。お楽しみに！:grin: