前回まで:
今回はKubernetes上のデータの永続化(Volume, PersistentVolumeClaim)について説明したあと、KafkaとZookeeperクラスターをKubernetes上にデプロイするManifestファイルを眺めながら、そこで出てくるリソースについて軽く説明する。
データの永続化について
Kubernetesに不向きなこと
いわゆる状態を持つことをKubernetesでもそのままstatefulという言葉で表現する。Kubernetesにおける"stateful"の明確な定義を私はまだ述べられないけれど、少なくともDBやKafkaのような、永続化されたデータを扱うタイプのPodのことをstatefulと呼んでいる。これはNginxがstatelessと呼ばれることと対比して考えるとわかりやすい。
Cloud Native DevOps with Kubernetesという書籍では、はっきりと「データベース(のようなstatefulなPod)をKubernetesで動かすことはお勧めしない」と記載されている。似たようなことを言う人はWeb上にも沢山見受けられる。例えば -> https://www.quora.com/Is-it-a-good-way-to-run-Kafka-on-Kubernetes
勘違いのないように述べておくと、Kubernetesでもデータの永続化をしたり、それを操作するDBをデプロイすることはできるし、さらに完全にプロダクションレベルでそれを動かすことはできる。
その上でお勧めしない理由としては、Manifestが非常に技巧的でまた煩雑になることにある。Kubernetesをそもそも導入したい動機の中には「インフラに構う時間を抑えて、コアな仕事に集中すること」があったはず。それを鑑みると、Kubernetesでデータベースを動かすことに多大な労力をかける価値を見いだせないかもしれない。素朴にホスト上で動かしたほうが、管理も障害時の調査も比較的に楽な可能性すらある。
Kubernetes上のアプリケーションでデータベースを使う際の代替案として挙げられているのが、クラウドプロバイダーが提供するPaaSを利用することである。
このような背景がありながらも、これからKubernetesでデータを永続化する方法についてみてゆく。
Volume
Dockerのときと同じように、Podでもホスト上のファイルやディレクトリをマウントできる。例えば以下のようにManifestファイルを記述すればよい。
apiVersion: v1
kind: Pod
metadata:
name: cent-hostpath
labels:
app: myapp
spec:
containers:
- name: myapp-container-1
image: centos:7
command: ['sleep', '3600']
volumeMounts:
- mountPath: "/tmp"
name: sample-hostpath
volumes:
- name: sample-hostpath
hostPath:
path: /tmp
type: DirectoryOrCreate
これはCentOS7のcotainerをsleep 3600
コマンドを実行しているだけの無意味なものである。spec.volumeMounts
のフィールドにあるところで、Container内の/tmp
のディレクトリをsample-hostpath
という名前のVolumeで定義されたところにマウントすることを指定している。そのあとのspec.volumes
でsample-hostpath
という名前で指定されたホストの領域を指定している。
このようにホストの領域を使うことはできるけれど、これは明らかなアンチパターンなのでこれ以上深入りはしない。
ただ以下のemptyDir
は使い道があるかもしれない。
apiVersion: v1
kind: Pod
metadata:
name: cent
labels:
app: myapp
spec:
containers:
- name: myapp-container-1
image: centos:7
command: ['sleep', '3600']
volumeMounts:
- mountPath: "/tmp"
name: sample-emptydir
- name: myapp-container-2
image: centos:7
command: ['sleep', '3600']
volumeMounts:
- mountPath: "/tmp"
name: sample-emptydir
volumes:
- name: sample-emptydir
emptyDir: {}
spec.volumes
の中にemptyDir
が定義された。これはPod内に作成される疑似的な"ホスト上のディレクトリ"で、Podが削除されたらこの領域も削除される。従ってデータの永続化はできない。とはいえ、上のようにPodの中に複数のContainerがある場合には、この領域を互いのContainerで共有できることにある。例えばPodの中でPub/Subを実現したい場合に、Pub用のContainerがこの領域に何かを書き込んで、Sub用のContainerがこの領域内のファイルをtailするような構成にすればそれができる。こうした使い方は有意義かもしれない。
SC, PV, PVC
まずKubernetesでデータを永続化する場合、データの保存場所はKubernetesクラスターの外部にあることが前提となる。Podはその保存場所にネットワーク越しにアクセスする。
データを永続化したい場合は、StorageClass(SC), **PersistentVolumeClaim(PVC)**と呼ばれる概念を知る必要がある。(今回はDynamic Provisioningを扱う。PersistentVolumeを手動で定義する方法は扱わないので、この2つの概念しか説明しない)
StorageClass
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: managed-premium-retain
provisioner: kubernetes.io/azure-disk
reclaimPolicy: Retain
parameters:
storageaccounttype: Premium_LRS
kind: Managed
- クラウドプロバイダーが提供するディスク(例えばAzureであればAzure File)をKubernetesで扱うための設定。
- この例では
managed-premium-retain
というStorageClassリソース名で「AzureのPremium_LRSというタイプのディスク(SSDのやつ)を」を宣言している。 - クラウドプロバイダー別にパラメータが変わる。Kubernetes公式ドキュメントには利用できる
provisioner
が一覧となっている => https://kubernetes.io/docs/concepts/storage/storage-classes/
PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: azure-managed-disk
spec:
accessModes:
- ReadWriteOnce
storageClassName: managed-premium-retain
resources:
requests:
storage: 5Gi
- 使用するディスクのサイズや、アクセスモード(リードオンリーや色々なPodから書き込みもできるかどうか)といったことを定義するもの。
- この例では
azure-managed-disk
というPersistentVolumeClaimリソース名で「managed-premium-retain
というディスクを使って、一つのPodからしかアクセスできず(ReadWriteOnce
)、サイズは5Gi
使う」ということを宣言している。
実際にこれを利用するには
Podの定義のspec.volumes
に、上で定義したpersistentVolumeClaim
を指定すればよい。
kind: Pod
apiVersion: v1
metadata:
name: mypod
spec:
containers:
- name: mypod
image: nginx:1.15.5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
volumeMounts:
- mountPath: "/mnt/azure"
name: volume
volumes:
- name: volume
persistentVolumeClaim:
claimName: azure-managed-disk
以上をまとめると
- PodはPersistentVolumeClaimを指定する。
- PersistentVolumeClaimはStorageClassを指定する。
となる。つまりPodからは実際になんのディスクを使っているからはわかっていない。ここでPodと実際のディスクとの疎結合が実現できている。
またStatefulSetならば、Podの定義と同じファイルにPersistentVolumeClaimを記述できる。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cent
spec:
selector:
matchLabels:
app: cent
replicas: 3
template:
metadata:
labels:
app: cent
spec:
containers:
- name: cent
image: centos:7
command: ['sleep', '3600']
volumeMounts:
- name: instatefulset
mountPath: /tmp
volumeClaimTemplates:
- metadata:
name: instatefulset
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "managed-premium-retain"
resources:
requests:
storage: 1Gi
KafkaとZookeeperクラスターをデプロイする
上記を踏まえてKafkaとZookeeperのクラスターをデプロイするyamlを眺める。
なかなか長大なManifestである。以下のことがケアされている:
- ClusterIPをつけない、HeadlessなServiceを作る。ホストに対して直接名前でアクセスできるように。前回のCNAME云々の話を参照
- StatefulSetとしてPodをデプロイしている。
- 例えばNodeのメンテナンスなどであるNodeからZookeeperを退避させないときに、動いているZookeeperの数を少なくとも2つは保っておくことを定義する ->
PodDisruptionbudget
- 同じNodeにZookeeperが立ってはならない ->
podAntiAffinity
- Podが正常に動いているかの確認 ->
readinessProbe
とlivvenessProbe
- zookeeperの
myid
やkafkaのbeokerId
を、Podのhostnameから動的につけられるようにしている。 - データの永続化