はじめに
この記事は [富士通クラウドテクノロジーズ Advent Calendar 2019]
(https://qiita.com/advent-calendar/2019/fjct) の18日目です。
昨日は @sasachi1231 さんの「Botを使ってSlackで簡単#NowPlaying」でした。
企画職の方でも Bot の作成等は比較的簡単に実装できるほうだと思うので、どんどん Slack を便利ツールにしたり (面白くしたり) していってほしいですね!
改めましてこんにちは!ニフクラでコンテナ系サービスの開発をしている @aokuma と申します。なんだかんだもう 4 年目の社員になってしまいました…。はやい…。
本日は私が業務で触ることの多い、 Kubernetes に関する話を書きたいと思います。
内容は主に、ニフクラ上で Kubernetes クラスターを構築・利用するときに悩むと思われる
「他社クラウドプロバイダーの Kubernetes では普通に使えるアレがニフクラ上だと使えない!」
に対する自分なりの答えを書いていきます。
注意
今回紹介する内容はニフクラ Computing で VM を作り、その上に手動で Kubernetes クラスターを構築することが前提です。マネージド Kubernetes サービスであるニフクラ Hatoba(β)ではこの記事の方法を使うことはできません。
悩み
さて。早速ですが、個人的に初めてニフクラ上に Kubernetes クラスターを構築して遊んでいたとき (思えば v1.4 とかの頃…)、下記のようなことでよく悩みました。
- type: LoadBalancer が使えない…
- PersistentVolume をいい感じに使いたい…
細かいものを挙げるとキリがなくなるのでこれくらいで…。
では、これらに対する自分なりの解決策を書いていきます…!
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) と呼ばれるものです。
CCM では cloudprovider.Interface
(https://github.com/kubernetes/cloud-provider/blob/master/cloud.go) に定義されているメソッドを Go 言語で実装することで、クラウドプロバイダーによって差のある処理を吸収しています。(前述の Controller 内から、このインターフェースのメソッドを呼んでいます。)
例えば AWS 向けの実装 や GCE 向けの実装 はこんな感じです。
…というわけでニフクラ向け CCM を書いてみたのがこちら
動作している様子は後で紹介します!
PersistentVolume が使いたい
続いては PersistentVolume (永続化ボリューム) です。
コンテナを使う上で、アプリケーションは基本的にはステートレスなことが望ましいですが、そうもいかない場面はどうしても存在します。
そこで登場するのが PersistentVolume です。コンテナに永続的なデータの置き場を提供してくれるリソースです。
Kubernetes の機能として、以前から AWS の EBS を PersistentVolume として利用する機能等は存在していましたが、その処理は Kubernetes のソースコードに内蔵されており、簡単に拡張できるものではありませんでした。 (その処理は現在 CCM に移されているようです)
また、クラウドプロバイダーが提供してくれるストレージ以外で PersistentVolume を利用する手段として、 NFS を利用した nfs-provisiner 等がありました。
現在でも様々な場面で用いられていると思いますが、場合によっては NFS の性能に足を引っ張られてしまうこともあり、用途は限定されてしまうかもしれません。
そして、Kubernetes v1.13 からは Container Storage Interface (CSI) が GA となり、 CSI の仕様に準拠したドライバーを書くことで、 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 ドライバを書いてみたのがこちら
これによって Pod に高速タイプや SSD の増設ディスクをマウントすることができるようになり、ステートフルなアプリケーションも Kubernetes 上で動かしやすくなります!
動作している様子は次に紹介します!
動作している様子
では、ニフクラ向け CCM と CSI ドライバーを動かしてみます。
動かす環境として、先日リリースされた、ゾーン間・リージョン間のプライベートネットワークを簡単に接続することができるプライベートブリッジというサービス活用し、 3 ゾーンにまたがる高可用性クラスタを作成しました。(余談ですが、プライベートブリッジによってニフクラで実現できる幅が大幅に広がり、とても嬉しいです。)
各種情報は下記の通りです。
- リージョン: east-1
- ゾーン: east-11, east-12, east-13
- OS: Ubuntu 18.04
- Docker: 19.03.4
- Kubernetes: v1.17.0
クラスタの構築
下記のような構成のクラスタを作成します。
注意点として、ニフクラにはプライベート側でゾーン間をまたいでトラフィックをバランシングできるロードバランサーリソースが存在しないため、 HAProxy と Keepalived を組み合わせて代用しています。(今回はコストカットのため 2 ゾーンにしか配置していません)
- 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 で ens160, 192 以外を除外する (docker0 や CNI が利用するインターフェース名を除外)
- CCM が間違った IP アドレスを API から取得してしまうため
- 参考: https://www.slideshare.net/Fujitsu_CloudTechnologies/dockertips-118090943
- kubelet に
--cloud-provider=external
のオプションを渡す-
kubeadm init
する際に--config
で下記のような設定を渡すことになるはず
-
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
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
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
ファイル内容は下記です。
---
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
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
patches:
- node.yaml
type: LoadBalancer
さて、では type: LoadBalancer
な Service を作ってみましょう!
サンプルとして、下記のようなマニフェストを作成してみました。 Nginx の Pod を 3 つ、それを LoadBalancer で公開します。
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
コンパネを開き、ロードバランサーのページを見てみます。
おー!!ロードバランサーが作られている!そしてノード 3 台がバランシングターゲットに設定されているみたいです!
ブラウザでロードバランサーの VIP にアクセスすると、ちゃんと Nginx のデフォルトのページが表示されます
やりたかったことの一つ、とりあえず達成です
増設ディスクを使った PersistentVolume
さて次は CSI ドライバーを使って、増設ディスクを Pod にマウントしてみます。
サンプルとして次のようなマニフェストを作成しました。
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 が稼働し始めます。
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
ちゃんとデータが増設ディスクに書き込まれて、永続化できているようです
注意点として、増設ディスクはマウント対象のノードと同じゾーンに存在しなければならないため、今回の構成の場合どれか 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 年目ですが、過去イチのボリュームになりました…。(制作時間も…。)
というわけでニフクラ上で Kubernetes を自前で立てつつ、 Kubernetes の仕組みをもっと活用したい場合は今回紹介した内容を参考にしてみていただけると幸いです! PR も大歓迎ですので、バグ等を見つけたら修正お待ちしております
また、富士通クラウドテクノロジーズでは Kubernetes を活用したサービスや Kubernetes の Controller、 Custom API Server を書いている人たちとかも居ます。興味があったら我々と一緒に働いてみませんか?
さて、明日は @o108minmin さんがなにか書いてくれるみたいです。お楽しみに!