これは、このセクションの複数ページの印刷可能なビューです。 印刷するには、ここをクリックしてください.
ステートフルアプリケーション
- 1: StatefulSetの基本
- 2: 例: Persistent Volumeを使用したWordpressとMySQLをデプロイする
- 3: 例: StatefulSetを使用したCassandraのデプロイ
- 4: 分散システムコーディネーターZooKeeperの実行
1 - StatefulSetの基本
このチュートリアルでは、StatefulSetを使用したアプリケーションを管理するための基本を説明します。StatefulSetのPodを作成、削除、スケール、そして更新する方法について紹介します。
始める前に
このチュートリアルを始める前に、以下のKubernetesの概念について理解しておく必要があります。
- Pod
- Cluster DNS
- Headless Service
- PersistentVolume
- PersistentVolumeのプロビジョニング
- StatefulSet
- kubectlコマンドラインツール
備考:
このチュートリアルでは、クラスターがPersistentVolumeの動的なプロビジョニングが行われるように設定されていることを前提としています。クラスターがそのように設定されていない場合、チュートリアルを始める前に1GiBのボリュームを2つ手動でプロビジョニングする必要があります。目標
StatefulSetはステートフルアプリケーションや分散システムで使用するために存在します。しかし、Kubernetes上のステートフルアプリケーションや分散システムは、広範で複雑なトピックです。StatefulSetの基本的な機能を示すという目的のため、また、ステートフルアプリケーションを分散システムと混同しないようにするために、ここでは、Statefulsetを使用する単純なウェブアプリケーションのデプロイを行います。
このチュートリアルを終えると、以下のことが理解できるようになります。
- StatefulSetの作成方法
- StatefulSetがどのようにPodを管理するのか
- StatefulSetの削除方法
- StatefulSetのスケール方法
- StatefulSetが管理するPodの更新方法
StatefulSetを作成する
はじめに、以下の例を使ってStatefulSetを作成しましょう。これは、コンセプトのStatefulSetのページで使ったものと同じような例です。nginxというheadless Serviceを作成し、webというStatefulSet内のPodのIPアドレスを公開します。
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
上の例をダウンロードして、web.yamlという名前で保存します。
ここでは、ターミナルウィンドウを2つ使う必要があります。1つ目のターミナルでは、kubectl getを使って、StatefulSetのPodの作成を監視します。
kubectl get pods -w -l app=nginx
2つ目のターミナルでは、kubectl applyを使って、web.yamlに定義されたheadless ServiceとStatefulSetを作成します。
kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created
上のコマンドを実行すると、2つのPodが作成され、それぞれのPodでNGINXウェブサーバーが実行されます。nginxServiceを取得してみましょう。
kubectl get service nginx
NAME      TYPE         CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     ClusterIP    None         <none>        80/TCP    12s
そして、webStatefulSetを取得して、2つのリソースの作成が成功したことも確認します。
kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         1         20s
順序付きPodの作成
n 個のレプリカを持つStatefulSetは、Podをデプロイするとき、1つずつ順番に作成し、 {0..n-1} という順序付けを行います。1つ目のターミナルでkubectl getコマンドの出力を確認しましょう。最終的に、以下の例のような出力が表示されるはずです。
kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         19s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s
web-0Podが Running (Pod Phaseを参照)かつ Ready (Pod Conditionsのtypeを参照)の状態になるまでは、web-1Podが起動していないことに注目してください。
StatefulSet内のPod
StatefulSet内のPodは、ユニークな順序インデックスと安定したネットワーク識別子を持ちます。
Podの順序インデックスを確かめる
StatefulSetのPodを取得します。
kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          1m
web-1     1/1       Running   0          1m
StatefulSetのコンセプトで説明したように、StatefulSet内のPodは安定したユニークな識別子を持ちます。この識別子は、StatefulSetコントローラーによって各Podに割り当てられる、ユニークな順序インデックスに基づいて付けられます。Podの名前は、<statefulsetの名前>-<順序インデックス>という形式です。webStatefulSetは2つのレプリカを持つため、web-0とweb-1という2つのPodを作成します。
安定したネットワーク識別子の使用
各Podは、順序インデックスに基づいた安定したホスト名を持ちます。kubectl execを使用して、各Pod内でhostnameコマンドを実行してみましょう。
for i in 0 1; do kubectl exec "web-$i" -- sh -c 'hostname'; done
web-0
web-1
kubectl runを使用して、dnsutilsパッケージのnslookupコマンドを提供するコンテナを実行します。Podのホスト名に対してnslookupを実行すると、クラスター内のDNSアドレスが確認できます。
kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
これにより、新しいシェルが起動します。新しいシェルで、次のコマンドを実行します。
# このコマンドは、dns-testコンテナのシェルで実行してください
nslookup web-0.nginx
出力は次のようになります。
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-0.nginx
Address 1: 10.244.1.6
nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-1.nginx
Address 1: 10.244.2.6
(コンテナのシェルを終了するために、exitコマンドを実行してください。)
headless serviceのCNAMEは、SRVレコードを指しています(1つのレコードがRunningかつReadyのPodに対応します)。SRVレコードは、PodのIPアドレスを含むAレコードを指します。
1つ目のターミナルで、StatefulSetのPodを監視します。
kubectl get pod -w -l app=nginx
2つ目のターミナルで、kubectl deleteを使用して、StatefulSetのすべてのPodを削除します。
kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
StatefulSetがPodを再起動して、2つのPodがRunningかつReadyの状態に移行するのを待ちます。
kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s
kubectl execとkubectl runコマンドを使用して、Podのホスト名とクラスター内DNSエントリーを確認します。まず、Podのホスト名を見てみましょう。
for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1
その後、次のコマンドを実行します。
kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
これにより、新しいシェルが起動します。新しいシェルで、次のコマンドを実行します。
# このコマンドは、dns-testコンテナのシェルで実行してください
nslookup web-0.nginx
出力は次のようになります。
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-0.nginx
Address 1: 10.244.1.7
nslookup web-1.nginx
Server:    10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name:      web-1.nginx
Address 1: 10.244.2.8
(コンテナのシェルを終了するために、exitコマンドを実行してください。)
Podの順序インデックス、ホスト名、SRVレコード、そしてAレコード名は変化していませんが、Podに紐付けられたIPアドレスは変化する可能性があります。このチュートリアルで使用しているクラスターでは、IPアドレスは変わりました。このようなことがあるため、他のアプリケーションがStatefulSet内のPodに接続するときには、IPアドレスで指定しないことが重要です。
StatefulSetの有効なメンバーを探して接続する必要がある場合は、headless ServiceのCNAME(nginx.default.svc.cluster.local)をクエリしなければなりません。CNAMEに紐付けられたSRVレコードには、StatefulSet内のRunningかつReadyなPodだけが含まれます。
アプリケーションがlivenessとreadinessをテストするコネクションのロジックをすでに実装している場合、PodのSRVレコード(web-0.nginx.default.svc.cluster.local、web-1.nginx.default.svc.cluster.local)をPodが安定しているものとして使用できます。PodがRunning and Readyな状態に移行すれば、アプリケーションはPodのアドレスを発見できるようになります。
安定したストレージへの書き込み
web-0およびweb-1のためのPersistentVolumeClaimを取得しましょう。
kubectl get pvc -l app=nginx
出力は次のようになります。
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           48s
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           48s
StatefulSetコントローラーは、2つのPersistentVolumeにバインドされた2つのPersistentVolumeClaimを作成しています。
このチュートリアルで使用しているクラスターでは、PersistentVolumeの動的なプロビジョニングが設定されているため、PersistentVolumeが自動的に作成されてバインドされています。
デフォルトでは、NGINXウェブサーバーは/usr/share/nginx/html/index.htmlに置かれたindexファイルを配信します。StatefulSetのspec内のvolumeMountsフィールドによって、/usr/share/nginx/htmlディレクトリがPersistentVolume上にあることが保証されます。
Podのホスト名をindex.htmlファイルに書き込むことで、NGINXウェブサーバーがホスト名を配信することを検証しましょう。
for i in 0 1; do kubectl exec "web-$i" -- sh -c 'echo "$(hostname)" > /usr/share/nginx/html/index.html'; done
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
備考:
上記のcurlコマンドに対して代わりに403 Forbiddenというレスポンスが返ってくる場合、volumeMountsでマウントしたディレクトリのパーミッションを修正する必要があります(これは、hostPathボリュームを使用したときに起こるバグが原因です)。この問題に対処するには、上のcurlコマンドを再実行する前に、次のコマンドを実行します。
for i in 0 1; do kubectl exec web-$i -- chmod 755 /usr/share/nginx/html; done
1つ目のターミナルで、StatefulSetのPodを監視します。
kubectl get pod -w -l app=nginx
2つ目のターミナルで、StatefulSetのすべてのPodを削除します。
kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
1つ目のターミナルでkubectl getコマンドの出力を確認して、すべてのPodがRunningかつReadyの状態に変わるまで待ちます。
kubectl get pod -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     0/1       ContainerCreating   0          0s
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         34s
ウェブサーバーがホスト名を配信し続けていることを確認します。
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
もしweb-0およびweb-1が再スケジュールされたとしても、Podは同じホスト名を配信し続けます。これは、PodのPersistentVolumeClaimに紐付けられたPersistentVolumeが、PodのvolumeMountsに再マウントされるためです。web-0とweb-1がどんなノードにスケジュールされたとしても、PodのPersistentVolumeは適切なマウントポイントにマウントされます。
StatefulSetをスケールする
StatefulSetのスケールとは、レプリカ数を増減することを意味します。これは、replicasフィールドを更新することによって実現できます。StatefulSetのスケールには、kubectl scaleと
kubectl patchのどちらも使用できます。
スケールアップ
1つ目のターミナルで、StatefulSet内のPodを監視します。
kubectl get pods -w -l app=nginx
2つ目のターミナルで、kubectl scaleを使って、レプリカ数を5にスケールします。
kubectl scale sts web --replicas=5
statefulset.apps/web scaled
1つ目のターミナルのkubectl getコマンドの出力を確認して、3つの追加のPodがRunningかつReadyの状態に変わるまで待ちます。
kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          2h
web-1     1/1       Running   0          2h
NAME      READY     STATUS    RESTARTS   AGE
web-2     0/1       Pending   0          0s
web-2     0/1       Pending   0         0s
web-2     0/1       ContainerCreating   0         0s
web-2     1/1       Running   0         19s
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         0s
web-3     0/1       ContainerCreating   0         0s
web-3     1/1       Running   0         18s
web-4     0/1       Pending   0         0s
web-4     0/1       Pending   0         0s
web-4     0/1       ContainerCreating   0         0s
web-4     1/1       Running   0         19s
StatefulSetコントローラーはレプリカ数をスケールします。 StatefulSetを作成するで説明したように、StatefulSetコントローラーは各Podを順序インデックスに従って1つずつ作成し、次のPodを起動する前に、1つ前のPodがRunningかつReadyの状態になるまで待ちます。
スケールダウン
1つ目のターミナルで、StatefulSetのPodを監視します。
kubectl get pods -w -l app=nginx
2つ目のターミナルで、kubectl patchコマンドを使用して、StatefulSetを3つのレプリカにスケールダウンします。
kubectl patch sts web -p '{"spec":{"replicas":3}}'
statefulset.apps/web patched
web-4およびweb-3がTerminatingの状態になるまで待ちます。
kubectl get pods -w -l app=nginx
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          3h
web-1     1/1       Running             0          3h
web-2     1/1       Running             0          55s
web-3     1/1       Running             0          36s
web-4     0/1       ContainerCreating   0          18s
NAME      READY     STATUS    RESTARTS   AGE
web-4     1/1       Running   0          19s
web-4     1/1       Terminating   0         24s
web-4     1/1       Terminating   0         24s
web-3     1/1       Terminating   0         42s
web-3     1/1       Terminating   0         42s
順序付きPodを削除する
コントローラーは、順序インデックスの逆順に1度に1つのPodを削除し、次のPodを削除する前には、各Podが完全にシャットダウンするまで待機しています。
StatefulSetのPersistentVolumeClaimを取得しましょう。
kubectl get pvc -l app=nginx
NAME        STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
www-web-0   Bound     pvc-15c268c7-b507-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-1   Bound     pvc-15c79307-b507-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-2   Bound     pvc-e1125b27-b508-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-3   Bound     pvc-e1176df6-b508-11e6-932f-42010a800002   1Gi        RWO           13h
www-web-4   Bound     pvc-e11bb5f8-b508-11e6-932f-42010a800002   1Gi        RWO           13h
まだ、5つのPersistentVolumeClaimと5つのPersistentVolumeが残っています。安定したストレージへの書き込みを読むと、StatefulSetのPodが削除されても、StatefulSetのPodにマウントされたPersistentVolumeは削除されないと書かれています。このことは、StatefulSetのスケールダウンによってPodが削除された場合にも当てはまります。
StatefulSetsを更新する
Kubernetes 1.7以降では、StatefulSetコントローラーは自動アップデートをサポートしています。使われる戦略は、StatefulSet APIオブジェクトのspec.updateStrategyフィールドによって決まります。この機能はコンテナイメージのアップグレード、リソースのrequestsやlimits、ラベル、StatefulSet内のPodのアノテーションの更新時に利用できます。有効なアップデートの戦略は、RollingUpdateとOnDeleteの2種類です。
RollingUpdateは、StatefulSetのデフォルトのアップデート戦略です。
RollingUpdate
RollingUpdateアップデート戦略は、StatefulSetの保証を尊重しながら、順序インデックスの逆順にStatefulSet内のすべてのPodをアップデートします。
webStatefulSetにpatchを当てて、RollingUpdateアップデート戦略を適用しましょう。
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate"}}}'
statefulset.apps/web patched
1つ目のターミナルで、webStatefulSetに再度patchを当てて、コンテナイメージを変更します。
kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.8"}]'
statefulset.apps/web patched
2つ目のターミナルで、StatefulSet内のPodを監視します。
kubectl get pod -l app=nginx -w
出力は次のようになります。
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          7m
web-1     1/1       Running   0          7m
web-2     1/1       Running   0          8m
web-2     1/1       Terminating   0         8m
web-2     1/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Terminating   0         8m
web-2     0/1       Pending   0         0s
web-2     0/1       Pending   0         0s
web-2     0/1       ContainerCreating   0         0s
web-2     1/1       Running   0         19s
web-1     1/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Terminating   0         8m
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         6s
web-0     1/1       Terminating   0         7m
web-0     1/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Terminating   0         7m
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         10s
StatefulSet内のPodは、順序インデックスの逆順に更新されました。StatefulSetコントローラーは各Podを終了させ、次のPodを更新する前に、新しいPodがRunningかつReadyの状態に変わるまで待機します。ここで、StatefulSetコントローラーは順序インデックスの前のPodがRunningかつReadyの状態になるまで次のPodの更新を始めず、現在の状態へのアップデートに失敗したPodがあった場合、そのPodをリストアすることに注意してください。
すでにアップデートを受け取ったPodは、アップデートされたバージョンにリストアされます。まだアップデートを受け取っていないPodは、前のバージョンにリストアされます。このような方法により、もし途中で失敗が起こっても、コントローラーはアプリケーションが健全な状態を保ち続けられるようにし、更新が一貫したものになるようにします。
Podを取得して、コンテナイメージを確認してみましょう。
for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
registry.k8s.io/nginx-slim:0.8
registry.k8s.io/nginx-slim:0.8
registry.k8s.io/nginx-slim:0.8
現在、StatefulSet内のすべてのPodは、前のコンテナイメージを実行しています。
備考:
kubectl rollout status sts/<name>を使って、StatefulSetへのローリングアップデートの状態を確認することもできます。ステージングアップデート
RollingUpdateアップデート戦略にpartitionパラメーターを使用すると、StatefulSetへのアップデートをステージングすることができます。ステージングアップデートを利用すれば、StatefulSet内のすべてのPodを現在のバージョンにしたまま、StatefulSetの.spec.templateを変更することが可能になります。
webStatefulSetにpatchを当てて、updateStrategyフィールドにpartitionを追加しましょう。
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}'
statefulset.apps/web patched
StatefulSetに再度patchを当てて、コンテナイメージを変更します。
kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"registry.k8s.io/nginx-slim:0.7"}]'
statefulset.apps/web patched
StatefulSet内のPodを削除します。
kubectl delete pod web-2
pod "web-2" deleted
PodがRunningかつReadyになるまで待ちます。
kubectl get pod -l app=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running   0         18s
Podのコンテナイメージを取得します。
kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.8
アップデート戦略がRollingUpdateであっても、StatefulSetが元のコンテナを持つPodをリストアしたことがわかります。これは、Podの順序インデックスがupdateStrategyで指定したpartitionより小さいためです。
カナリア版をロールアウトする
ステージングアップデートのときに指定したpartitionを小さくすることで、変更をテストするためのカナリア版をロールアウトできます。
StatefulSetにpatchを当てて、partitionを小さくします。
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'
statefulset.apps/web patched
web-2がRunningかつReadyの状態になるまで待ちます。
kubectl get pod -l app=nginx -w
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          4m
web-1     1/1       Running             0          4m
web-2     0/1       ContainerCreating   0          11s
web-2     1/1       Running   0         18s
Podのコンテナを取得します。
kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.7
partitionを変更すると、StatefulSetコントローラーはPodを自動的に更新します。Podの順序インデックスがpartition以上の値であるためです。
web-1Podを削除します。
kubectl delete pod web-1
pod "web-1" deleted
web-1PodがRunningかつReadyになるまで待ちます。
kubectl get pod -l app=nginx -w
出力は次のようになります。
NAME      READY     STATUS        RESTARTS   AGE
web-0     1/1       Running       0          6m
web-1     0/1       Terminating   0          6m
web-2     1/1       Running       0          2m
web-1     0/1       Terminating   0         6m
web-1     0/1       Terminating   0         6m
web-1     0/1       Terminating   0         6m
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       ContainerCreating   0         0s
web-1     1/1       Running   0         18s
web-1Podのコンテナイメージを取得します。
kubectl get pod web-1 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
registry.k8s.io/nginx-slim:0.8
Podの順序インデックスがpartitionよりも小さいため、web-1は元の設定のコンテナイメージにリストアされました。partitionを指定すると、StatefulSetの.spec.templateが更新されたときに、順序インデックスがそれ以上の値を持つすべてのPodがアップデートされます。partitionよりも小さな順序インデックスを持つPodが削除されたり終了されたりすると、元の設定のPodにリストアされます。
フェーズロールアウト
カナリア版をロールアウトするのと同じような方法でパーティションされたローリングアップデートを使用すると、フェーズロールアウト(例: 線形、幾何級数的、指数関数的ロールアウト)を実行できます。フェーズロールアウトを実行するには、コントローラーがアップデートを途中で止めてほしい順序インデックスをpartitionに設定します。
現在、partitionは2に設定されています。partitionを0に設定します。
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'
statefulset.apps/web patched
StatefulSet内のすべてのPodがRunningかつReadyの状態になるまで待ちます。
kubectl get pod -l app=nginx -w
出力は次のようになります。
NAME      READY     STATUS              RESTARTS   AGE
web-0     1/1       Running             0          3m
web-1     0/1       ContainerCreating   0          11s
web-2     1/1       Running             0          2m
web-1     1/1       Running   0         18s
web-0     1/1       Terminating   0         3m
web-0     1/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Terminating   0         3m
web-0     0/1       Pending   0         0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         3s
StatefulSet内のPodのコンテナイメージの詳細を取得します。
for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
registry.k8s.io/nginx-slim:0.7
registry.k8s.io/nginx-slim:0.7
registry.k8s.io/nginx-slim:0.7
partitionを0に移動することで、StatefulSetがアップデート処理を続けられるようにできます。
OnDelete
OnDeleteアップデート戦略は、(1.6以前の)レガシーな動作を実装しています。このアップデート戦略を選択すると、StatefulSetの.spec.templateフィールドへ変更を加えても、StatefulSetコントローラーが自動的にPodを更新しなくなります。この戦略を選択するには、.spec.template.updateStrategy.typeにOnDeleteを設定します。
StatefulSetを削除する
StatefulSetは、非カスケードな削除とカスケードな削除の両方をサポートしています。非カスケードな削除では、StatefulSetが削除されても、StatefulSet内のPodは削除されません。カスケードな削除では、StatefulSetとPodが一緒に削除されます。
非カスケードな削除
1つ目のターミナルで、StatefulSet内のPodを監視します
kubectl get pods -w -l app=nginx
kubectl deleteを使用して、StatefulSetを削除します。このとき、--cascade=orphanパラメーターをコマンドに与えてください。このパラメーターは、Kubernetesに対して、StatefulSetだけを削除して配下のPodは削除しないように指示します。
kubectl delete statefulset web --cascade=orphan
statefulset.apps "web" deleted
Podを取得して、ステータスを確認します。
kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          6m
web-1     1/1       Running   0          7m
web-2     1/1       Running   0          5m
webが削除されても、すべてのPodはまだRunningかつReadyの状態のままです。web-0を削除します。
kubectl delete pod web-0
pod "web-0" deleted
StatefulSetのPodを取得します。
kubectl get pods -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-1     1/1       Running   0          10m
web-2     1/1       Running   0          7m
webStatefulSetはすでに削除されているため、web-0は再起動しません。
1つ目のターミナルで、StatefulSetのPodを監視します。
kubectl get pods -w -l app=nginx
2つ目のターミナルで、StatefulSetを再作成します。もしnginxServiceを削除しなかった場合(この場合は削除するべきではありませんでした)、Serviceがすでに存在することを示すエラーが表示されます。
kubectl apply -f web.yaml
statefulset.apps/web created
service/nginx unchanged
このエラーは無視してください。このメッセージは、すでに存在する nginx というheadless Serviceを作成しようと試みたということを示しているだけです。
1つ目のターミナルで、kubectl getコマンドの出力を確認します。
kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-1     1/1       Running   0          16m
web-2     1/1       Running   0          2m
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         18s
web-2     1/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
web-2     0/1       Terminating   0         3m
webStatefulSetが再作成されると、最初にweb-0を再実行します。web-1はすでにRunningかつReadyの状態であるため、web-0がRunningかつReadyの状態に移行すると、StatefulSetは単純にこのPodを選びます。StatefulSetをreplicasを2にして再作成したため、一度web-0が再作成されて、web-1がすでにRunningかつReadyの状態であることが判明したら、web-2は停止されます。
Podのウェブサーバーが配信しているindex.htmlファイルのコンテンツをもう一度見てみましょう。
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
たとえStatefulSetとweb-0Podの両方が削除されても、Podは最初にindex.htmlファイルに書き込んだホスト名をまだ配信しています。これは、StatefulSetがPodに紐付けられたPersistentVolumeを削除しないためです。StatefulSetを再作成してweb-0を再実行すると、元のPersistentVolumeが再マウントされます。
カスケードな削除
1つ目のターミナルで、StatefulSet内のPodを監視します。
kubectl get pods -w -l app=nginx
2つ目のターミナルで、StatefulSetをもう一度削除します。今回は、--cascade=orphanパラメーターを省略します。
kubectl delete statefulset web
statefulset.apps "web" deleted
1つ目のターミナルで実行しているkubectl getコマンドの出力を確認し、すべてのPodがTerminatingの状態に変わるまで待ちます。
kubectl get pods -w -l app=nginx
NAME      READY     STATUS    RESTARTS   AGE
web-0     1/1       Running   0          11m
web-1     1/1       Running   0          27m
NAME      READY     STATUS        RESTARTS   AGE
web-0     1/1       Terminating   0          12m
web-1     1/1       Terminating   0         29m
web-0     0/1       Terminating   0         12m
web-0     0/1       Terminating   0         12m
web-0     0/1       Terminating   0         12m
web-1     0/1       Terminating   0         29m
web-1     0/1       Terminating   0         29m
web-1     0/1       Terminating   0         29m
スケールダウンのセクションで見たように、順序インデックスの逆順に従って、Podは一度に1つずつ終了します。StatefulSetコントローラーは、次のPodを終了する前に、前のPodが完全に終了するまで待ちます。
備考:
カスケードな削除ではStatefulSetがPodとともに削除されますが、StatefulSetと紐付けられたheadless Serviceは削除されません。そのため、nginxServiceは手動で削除する必要があります。kubectl delete service nginx
service "nginx" deleted
さらにもう一度、StatefulSetとheadless Serviceを再作成します。
kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created
StatefulSet上のすべてのPodがRunningかつReadyの状態に変わったら、Pod上のindex.htmlファイルのコンテンツを取得します。
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
StatefulSetを完全に削除して、すべてのPodが削除されたとしても、PersistentVolumeがマウントされたPodが再生成されて、web-0とweb-1はホスト名の配信を続けます。
最後に、webStatefulSetを削除します。
kubectl delete service nginx
service "nginx" deleted
そして、nginxServiceも削除します。
kubectl delete statefulset web
statefulset "web" deleted
Pod管理ポリシー
分散システムによっては、StatefulSetの順序の保証が不必要であったり望ましくない場合もあります。こうしたシステムでは、一意性と同一性だけが求められます。この問題に対処するために、Kubernetes 1.7でStatefulSet APIオブジェクトに.spec.podManagementPolicyが導入されました。
OrderedReadyのPod管理
OrderedReadyのPod管理はStatefulSetのデフォルトの設定です。StatefulSetコントローラーに対して、これまでに紹介したような順序の保証を尊重するように指示します。
ParallelのPod管理
ParallelのPod管理では、StatefulSetコントローラーに対して、PodがRunningかつReadyの状態や完全に停止するまで待たないように指示し、すべてのPodを並列に起動または停止させるようにします。
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  podManagementPolicy: "Parallel"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
上の例をダウンロードして、web-parallel.yamlという名前でファイルに保存してください。
このマニフェストは、.spec.podManagementPolicyがParallelに設定されている以外は、前にダウンロードしたwebStatefulSetと同一です。
1つ目のターミナルで、StatefulSet内のPodを監視します。
kubectl get pod -l app=nginx -w
2つ目のターミナルで、マニフェスト内のStatefulSetとServiceを作成します。
kubectl apply -f web-parallel.yaml
service/nginx created
statefulset.apps/web created
1つ目のターミナルで実行したkubectl getコマンドの出力を確認します。
kubectl get pod -l app=nginx -w
NAME      READY     STATUS    RESTARTS   AGE
web-0     0/1       Pending   0          0s
web-0     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-1     0/1       Pending   0         0s
web-0     0/1       ContainerCreating   0         0s
web-1     0/1       ContainerCreating   0         0s
web-0     1/1       Running   0         10s
web-1     1/1       Running   0         10s
StatefulSetコントローラーはweb-0とweb-1を同時に起動しています。
2つ目のターミナルで、StatefulSetをスケールしてみます。
kubectl scale statefulset/web --replicas=4
statefulset.apps/web scaled
kubectl getコマンドを実行しているターミナルの出力を確認します。
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         0s
web-3     0/1       Pending   0         7s
web-3     0/1       ContainerCreating   0         7s
web-2     1/1       Running   0         10s
web-3     1/1       Running   0         26s
StatefulSetが2つのPodを実行し、1つ目のPodがRunningかつReadyの状態になるのを待たずに2つ目のPodを実行しているのがわかります。
クリーンアップ
2つのターミナルが開かれているはずなので、クリーンアップの一部としてkubectlコマンドを実行する準備ができています。
kubectl delete sts web
# stsは、statefulsetの略です。
kubectl getを監視すると、Podが削除されていく様子を確認できます。
kubectl get pod -l app=nginx -w
web-3     1/1       Terminating   0         9m
web-2     1/1       Terminating   0         9m
web-3     1/1       Terminating   0         9m
web-2     1/1       Terminating   0         9m
web-1     1/1       Terminating   0         44m
web-0     1/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-3     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-1     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-2     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-2     0/1       Terminating   0         9m
web-1     0/1       Terminating   0         44m
web-1     0/1       Terminating   0         44m
web-1     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-0     0/1       Terminating   0         44m
web-3     0/1       Terminating   0         9m
web-3     0/1       Terminating   0         9m
web-3     0/1       Terminating   0         9m
削除の間、StatefulSetはすべてのPodを並列に削除し、順序インデックスが1つ前のPodが停止するのを待つことはありません。
kubectl getコマンドを実行しているターミナルを閉じて、nginxServiceを削除します。
kubectl delete svc nginx
備考:
このチュートリアルで使用したPersistentVolumeのための永続ストレージも削除する必要があります。
すべてのストレージが再利用できるようにするために、環境、ストレージの設定、プロビジョニング方法に基づいて必要な手順に従ってください。
2 - 例: Persistent Volumeを使用したWordpressとMySQLをデプロイする
このチュートリアルでは、WordPressのサイトとMySQLデータベースをMinikubeを使ってデプロイする方法を紹介します。2つのアプリケーションとも、データを保存するためにPersistentVolumeとPersistentVolumeClaimを使用します。
PersistentVolume(PV)とは、管理者が手動でプロビジョニングを行うか、StorageClassを使ってKubernetesによって動的にプロビジョニングされた、クラスター内のストレージの一部です。PersistentVolumeClaim(PVC)は、PVによって満たすことができる、ユーザーによるストレージへのリクエストのことです。PersistentVolumeとPersistentVolumeClaimは、Podのライフサイクルからは独立していて、Podの再起動、Podの再スケジューリング、さらにはPodの削除が行われたとしても、その中のデータは削除されずに残ります。
警告:
シングルインスタンスのWordPressとMySQLのPodを使用しているため、ここで行うデプロイは本番のユースケースには適しません。WordPressを本番環境にデプロイするときは、WordPress Helm Chartを使用することを検討してください。備考:
このチュートリアルで提供されるファイルは、GAとなっているDeployment APIを使用しているため、Kubernetesバージョン1.9以降のためのものになっています。もしこのチュートリアルを古いバージョンのKubernetesで使いたい場合は、APIのバージョンを適切にアップデートするか、このチュートリアルの古いバージョンを参照してください。目標
- PersistentVolumeClaimとPersistentVolumeを作成する
- 以下を含むkustomization.yamlを作成する- Secret generator
- MySQLリソースの設定
- WordPressリソースの設定
 
- kustomizationディレクトリをkubectl apply -k ./で適用する
- クリーンアップする
始める前に
Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:
バージョンを確認するには次のコマンドを実行してください:  kubectl version.
kubectl 1.14以降で動作します。
以下の設定ファイルをダウンロードします。
PersistentVolumeClaimとPersistentVolumeを作成する
MySQLとWordpressはそれぞれ、データを保存するためのPersistentVolumeを必要とします。各PersistentVolumeClaimはデプロイの段階で作成されます。
多くのクラスター環境では、デフォルトのStorageClassがインストールされています。StorageClassがPersistentVolumeClaim中で指定されていなかった場合、クラスターのデフォルトのStorageClassが代わりに使われます。
PersistentVolumeClaimが作成されるとき、StorageClassの設定に基づいてPersistentVolumeが動的にプロビジョニングされます。
警告:
ローカルのクラスターでは、デフォルトのStorageClassにはhostPathプロビジョナーが使われます。hostPathボリュームは開発およびテストにのみ適しています。hostPathボリュームでは、データはPodがスケジュールされたノード上の/tmp内に保存されます。そのため、もしPodが死んだり、クラスター上の他のノードにスケジュールされたり、ノードが再起動すると、データは失われます。備考:
hostPathプロビジョナーを使用する必要があるクラスターを立ち上げたい場合は、--enable-hostpath-provisionerフラグを controller-manager コンポーネントで設定する必要があります。備考:
Google Kubernetes Engine上で動作するKubernetesクラスターを使っている場合は、このガイドに従ってください。kustomization.yamlを作成する
Secret generatorを追加する
Secretとは、パスワードやキーのような機密性の高いデータ片を保存するためのオブジェクトです。バージョン1.14からは、kubectlがkustomizationファイルを使用したKubernetesオブジェクトの管理をサポートしています。kustomization.yaml内のgeneratorによってSecretを作成することができます。
以下のコマンドを実行して、kustomization.yamlの中にSecret generatorを追加します。YOUR_PASSWORDの部分を使いたいパスワードに置換してください。
cat <<EOF >./kustomization.yaml
secretGenerator:
- name: mysql-pass
  literals:
  - password=YOUR_PASSWORD
EOF
MySQLとWordPressのためのリソースの設定を追加する
以下のマニフェストには、シングルインスタンスのMySQLのDeploymentが書かれています。MySQLコンテナはPersistentVolumeを/var/lib/mysqlにマウントします。MYSQL_ROOT_PASSWORD環境変数には、Secretから得られたデータベースのパスワードが設定されます。
apiVersion: v1
kind: Service
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  ports:
    - port: 3306
  selector:
    app: wordpress
    tier: mysql
  clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-mysql
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: mysql
    spec:
      containers:
      - image: mysql:8.0
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim
以下のマニフェストには、シングルインスタンスのWordPressのDeploymentが書かれています。WordPressコンテナはPersistentVolumeをウェブサイトのデータファイルのために/var/www/htmlにマウントします。WORDPRESS_DB_HOST環境変数に上で定義したMySQLのServiceの名前を設定すると、WordPressはServiceによってデータベースにアクセスします。WORDPRESS_DB_PASSWORD環境変数には、kustomizeが生成したSecretから得たデータベースのパスワードが設定されます。
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  ports:
    - port: 80
  selector:
    app: wordpress
    tier: frontend
  type: LoadBalancer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wp-pv-claim
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: wordpress
        tier: frontend
    spec:
      containers:
      - image: wordpress:4.8-apache
        name: wordpress
        env:
        - name: WORDPRESS_DB_HOST
          value: wordpress-mysql
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password
        ports:
        - containerPort: 80
          name: wordpress
        volumeMounts:
        - name: wordpress-persistent-storage
          mountPath: /var/www/html
      volumes:
      - name: wordpress-persistent-storage
        persistentVolumeClaim:
          claimName: wp-pv-claim
- 
MySQLのDeploymentの設定ファイルをダウンロードします。 curl -LO https://k8s.io/examples/application/wordpress/mysql-deployment.yaml
- 
WordPressの設定ファイルをダウンロードします。 curl -LO https://k8s.io/examples/application/wordpress/wordpress-deployment.yaml
- 
これらを kustomization.yamlファイルに追加します。
cat <<EOF >>./kustomization.yaml
resources:
  - mysql-deployment.yaml
  - wordpress-deployment.yaml
EOF
適用と確認
kustomization.yamlには、WordPressのサイトとMySQLデータベースのためのすべてのリソースが含まれています。次のコマンドでこのディレクトリを適用できます。
kubectl apply -k ./
これで、すべてのオブジェクトが存在していることを確認できます。
- 
次のコマンドを実行して、Secretが存在していることを確認します。 kubectl get secrets結果は次のようになるはずです。 NAME TYPE DATA AGE mysql-pass-c57bb4t7mf Opaque 1 9s
- 
次のコマンドを実行して、PersistentVolumeが動的にプロビジョニングされていることを確認します。 kubectl get pvc備考:PVがプロビジョニングされてバインドされるまでに、最大で数分かかる場合があります。結果は次のようになるはずです。 NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mysql-pv-claim Bound pvc-8cbd7b2e-4044-11e9-b2bb-42010a800002 20Gi RWO standard 77s wp-pv-claim Bound pvc-8cd0df54-4044-11e9-b2bb-42010a800002 20Gi RWO standard 77s
- 
次のコマンドを実行して、Podが実行中であることを確認します。 kubectl get pods備考:PodのStatusが`Running`の状態になる前に、最大で数分かかる場合があります。結果は次のようになるはずです。 NAME READY STATUS RESTARTS AGE wordpress-mysql-1894417608-x5dzt 1/1 Running 0 40s
- 
次のコマンドを実行して、Serviceが実行中であることを確認します。 kubectl get services wordpress結果は次のようになるはずです。 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE wordpress LoadBalancer 10.0.0.89 <pending> 80:32406/TCP 4m備考:MinikubeではServiceを`NodePort`経由でしか公開できません。EXTERNAL-IPは常にpendingのままになります。
- 
次のコマンドを実行して、WordPress ServiceのIPアドレスを取得します。 minikube service wordpress --url結果は次のようになるはずです。 http://1.2.3.4:32406
- 
IPアドレスをコピーして、ブラウザーで読み込み、サイトを表示しましょう。 WordPressによりセットアップされた次のスクリーンショットのようなページが表示されるはずです。  
警告:
WordPressのインストールをこのページのまま放置してはいけません。もしほかのユーザーがこのページを見つけた場合、その人はインスタンス上にウェブサイトをセットアップして、悪意のあるコンテンツの配信に利用できてしまいます。ユーザー名とパスワードを決めてWordPressをインストールするか、このインスタンスを削除してください。
クリーンアップ
- 
次のコマンドを実行して、Secret、Deployment、Service、およびPersistentVolumeClaimを削除します。 kubectl delete -k ./
次の項目
- イントロスペクションとデバッグについてさらに学ぶ
- Jobについてさらに学ぶ
- Portフォワーディングについてさらに学ぶ
- コンテナへのシェルを取得する方法について学ぶ
3 - 例: StatefulSetを使用したCassandraのデプロイ
このチュートリアルでは、Apache CassandraをKubernetes上で実行する方法を紹介します。 データベースの一種であるCassandraには、データの耐久性(アプリケーションの 状態)を提供するために永続ストレージが必要です。 この例では、カスタムのCassandraのseed providerにより、Cassandraクラスターに参加した新しいCassandraインスタンスを検出できるようにします。
StatefulSetを利用すると、ステートフルなアプリケーションをKubernetesクラスターにデプロイするのが簡単になります。 このチュートリアルで使われている機能のより詳しい情報は、StatefulSetを参照してください。
備考:
CassandraとKubernetesは、ともにクラスターのメンバーを表すために ノード という用語を使用しています。このチュートリアルでは、StatefulSetに属するPodはCassandraのノードであり、Cassandraクラスター( ring と呼ばれます)のメンバーでもあります。これらのPodがKubernetesクラスター内で実行されるとき、Kubernetesのコントロールプレーンは、PodをKubernetesのNode上にスケジュールします。
Cassandraノードが開始すると、 シードリスト を使ってring上の他のノードの検出が始まります。 このチュートリアルでは、Kubernetesクラスター内に現れた新しいCassandra Podを検出するカスタムのCassandraのseed providerをデプロイします。
目標
- Cassandraのheadless Serviceを作成して検証する。
- StatefulSetを使用してCassandra ringを作成する。
- StatefulSetを検証する。
- StatefulSetを編集する。
- StatefulSetとPodを削除する。
始める前に
Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:
このチュートリアルを完了するには、Pod、Service、StatefulSetの基本についてすでに知っている必要があります。
Minikubeのセットアップに関する追加の設定手順
注意:
Minikubeは、デフォルトでは1024MiBのメモリと1CPUに設定されます。 デフォルトのリソース設定で起動したMinikubeでは、このチュートリアルの実行中にリソース不足のエラーが発生してしまいます。このエラーを回避するためにはMinikubeを次の設定で起動してください:
minikube start --memory 5120 --cpus=4
Cassandraのheadless Serviceを作成する
Kubernetesでは、Serviceは同じタスクを実行するPodの集合を表します。
以下のServiceは、Cassandra Podとクラスター内のクライアント間のDNSルックアップに使われます:
apiVersion: v1
kind: Service
metadata:
  labels:
    app: cassandra
  name: cassandra
spec:
  clusterIP: None
  ports:
  - port: 9042
  selector:
    app: cassandra
cassandra-service.yamlファイルから、Cassandra StatefulSetのすべてのメンバーをトラッキングするServiceを作成します。
kubectl apply -f https://k8s.io/examples/application/cassandra/cassandra-service.yaml
検証 (オプション)
Cassandra Serviceを取得します。
kubectl get svc cassandra
結果は次のようになります。
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
cassandra   ClusterIP   None         <none>        9042/TCP   45s
cassandraという名前のServiceが表示されない場合、作成に失敗しています。よくある問題のトラブルシューティングについては、Serviceのデバッグを読んでください。
StatefulSetを使ってCassandra ringを作成する
以下に示すStatefulSetマニフェストは、3つのPodからなるCassandra ringを作成します。
備考:
この例ではMinikubeのデフォルトのプロビジョナーを使用しています。 クラウドを使用している場合、StatefulSetを更新してください。apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cassandra
  labels:
    app: cassandra
spec:
  serviceName: cassandra
  replicas: 3
  selector:
    matchLabels:
      app: cassandra
  template:
    metadata:
      labels:
        app: cassandra
    spec:
      terminationGracePeriodSeconds: 500
      containers:
      - name: cassandra
        image: gcr.io/google-samples/cassandra:v13
        imagePullPolicy: Always
        ports:
        - containerPort: 7000
          name: intra-node
        - containerPort: 7001
          name: tls-intra-node
        - containerPort: 7199
          name: jmx
        - containerPort: 9042
          name: cql
        resources:
          limits:
            cpu: "500m"
            memory: 1Gi
          requests:
            cpu: "500m"
            memory: 1Gi
        securityContext:
          capabilities:
            add:
              - IPC_LOCK
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - nodetool drain
        env:
          - name: MAX_HEAP_SIZE
            value: 512M
          - name: HEAP_NEWSIZE
            value: 100M
          - name: CASSANDRA_SEEDS
            value: "cassandra-0.cassandra.default.svc.cluster.local"
          - name: CASSANDRA_CLUSTER_NAME
            value: "K8Demo"
          - name: CASSANDRA_DC
            value: "DC1-K8Demo"
          - name: CASSANDRA_RACK
            value: "Rack1-K8Demo"
          - name: POD_IP
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
        readinessProbe:
          exec:
            command:
            - /bin/bash
            - -c
            - /ready-probe.sh
          initialDelaySeconds: 15
          timeoutSeconds: 5
        # These volume mounts are persistent. They are like inline claims,
        # but not exactly because the names need to match exactly one of
        # the stateful pod volumes.
        volumeMounts:
        - name: cassandra-data
          mountPath: /cassandra_data
  # These are converted to volume claims by the controller
  # and mounted at the paths mentioned above.
  # do not use these in production until ssd GCEPersistentDisk or other ssd pd
  volumeClaimTemplates:
  - metadata:
      name: cassandra-data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: fast
      resources:
        requests:
          storage: 1Gi
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: fast
provisioner: k8s.io/minikube-hostpath
parameters:
  type: pd-ssd
cassandra-statefulset.yamlファイルから、CassandraのStatefulSetを作成します:
# cassandra-statefulset.yaml を編集せずにapplyできる場合は、このコマンドを使用してください
kubectl apply -f https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml
クラスターに合わせてcassandra-statefulset.yamlを編集する必要がある場合、 https://k8s.io/examples/application/cassandra/cassandra-statefulset.yaml をダウンロードして、修正したバージョンを保存したフォルダからマニフェストを適用してください。
# cassandra-statefulset.yaml をローカルで編集する必要がある場合、このコマンドを使用してください
kubectl apply -f cassandra-statefulset.yaml
CassandraのStatefulSetを検証する
- 
CassandraのStatefulSetを取得します kubectl get statefulset cassandra結果は次のようになるはずです: NAME DESIRED CURRENT AGE cassandra 3 0 13sStatefulSetリソースがPodを順番にデプロイします。
- 
Podを取得して順序付きの作成ステータスを確認します kubectl get pods -l="app=cassandra"結果は次のようになるはずです: NAME READY STATUS RESTARTS AGE cassandra-0 1/1 Running 0 1m cassandra-1 0/1 ContainerCreating 0 8s3つすべてのPodのデプロイには数分かかる場合があります。デプロイが完了すると、同じコマンドは次のような結果を返します: NAME READY STATUS RESTARTS AGE cassandra-0 1/1 Running 0 10m cassandra-1 1/1 Running 0 9m cassandra-2 1/1 Running 0 8m
- 
1番目のPodの中でCassandraのnodetoolを実行して、ringのステータスを表示します。 kubectl exec -it cassandra-0 -- nodetool status結果は次のようになるはずです: Datacenter: DC1-K8Demo ====================== Status=Up/Down |/ State=Normal/Leaving/Joining/Moving -- Address Load Tokens Owns (effective) Host ID Rack UN 172.17.0.5 83.57 KiB 32 74.0% e2dd09e6-d9d3-477e-96c5-45094c08db0f Rack1-K8Demo UN 172.17.0.4 101.04 KiB 32 58.8% f89d6835-3a42-4419-92b3-0e62cae1479c Rack1-K8Demo UN 172.17.0.6 84.74 KiB 32 67.1% a6a1e8c2-3dc5-4417-b1a0-26507af2aaad Rack1-K8Demo
CassandraのStatefulSetを変更する
kubectl editを使うと、CassandraのStatefulSetのサイズを変更できます。
- 
次のコマンドを実行します。 kubectl edit statefulset cassandraこのコマンドを実行すると、ターミナルでエディタが起動します。変更が必要な行は replicasフィールドです。 以下の例は、StatefulSetファイルの抜粋です:# Please edit the object below. Lines beginning with a '#' will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: apps/v1 kind: StatefulSet metadata: creationTimestamp: 2016-08-13T18:40:58Z generation: 1 labels: app: cassandra name: cassandra namespace: default resourceVersion: "323" uid: 7a219483-6185-11e6-a910-42010a8a0fc0 spec: replicas: 3
- 
レプリカ数を4に変更し、マニフェストを保存します。 これで、StatefulSetが4つのPodを実行するようにスケールされました。 
- 
CassandraのStatefulSetを取得して、変更を確かめます: kubectl get statefulset cassandra結果は次のようになるはずです: NAME DESIRED CURRENT AGE cassandra 4 4 36m
クリーンアップ
StatefulSetを削除したりスケールダウンしても、StatefulSetに関係するボリュームは削除されません。 StatefulSetに関連するすべてのリソースを自動的に破棄するよりも、データの方がより貴重であるため、安全のためにこのような設定になっています。
警告:
ストレージクラスやreclaimポリシーによっては、PersistentVolumeClaimを削除すると、関連するボリュームも削除される可能性があります。PersistentVolumeClaimの削除後にもデータにアクセスできるとは決して想定しないでください。- 
次のコマンドを実行して(単一のコマンドにまとめています)、CassandraのStatefulSetに含まれるすべてのリソースを削除します: grace=$(kubectl get pod cassandra-0 -o=jsonpath='{.spec.terminationGracePeriodSeconds}') \ && kubectl delete statefulset -l app=cassandra \ && echo "Sleeping ${grace} seconds" 1>&2 \ && sleep $grace \ && kubectl delete persistentvolumeclaim -l app=cassandra
- 
次のコマンドを実行して、CassandraをセットアップしたServiceを削除します: kubectl delete service -l app=cassandra
Cassandraコンテナの環境変数
このチュートリアルのPodでは、Googleのコンテナレジストリのgcr.io/google-samples/cassandra:v13イメージを使用しました。このDockerイメージはdebian-baseをベースにしており、OpenJDK 8が含まれています。
このイメージには、Apache Debianリポジトリの標準のCassandraインストールが含まれます。
環境変数を利用すると、cassandra.yamlに挿入された値を変更できます。
| 環境変数 | デフォルト値 | 
|---|---|
| CASSANDRA_CLUSTER_NAME | 'Test Cluster' | 
| CASSANDRA_NUM_TOKENS | 32 | 
| CASSANDRA_RPC_ADDRESS | 0.0.0.0 | 
次の項目
- StatefulSetのスケールを行う方法を学ぶ。
- KubernetesSeedProviderについてもっと学ぶ。
- カスタムのSeed Providerの設定についてもっと学ぶ。
4 - 分散システムコーディネーターZooKeeperの実行
このチュートリアルでは、StatefulSet、PodDisruptionBudgets、Podアンチアフィニティを使って、Kubernetes上でのApache Zookeeperの実行をデモンストレーションします。
始める前に
このチュートリアルを始める前に、以下のKubernetesの概念について理解しておく必要があります。
- Pod
- クラスターDNS
- Headless Service
- PersistentVolume
- PersistentVolume Provisioning
- StatefulSet
- PodDisruptionBudgets
- Podアンチアフィニティ
- kubectl CLI
少なくとも4つのノードのクラスターが必要で、各ノードは少なくとも2つのCPUと4GiBのメモリが必須です。このチュートリアルでは、クラスターのノードをcordonおよびdrainします。 つまり、クラスターがそのノードの全てのPodを終了して退去させ、ノードが一時的にスケジュールできなくなる、ということです。 このチュートリアル専用のクラスターを使うか、起こした破壊がほかのテナントに干渉しない確証を得ることをお勧めします。
このチュートリアルでは、クラスターがPersistentVolumeの動的なプロビジョニングが行われるように設定されていることを前提としています。 クラスターがそのように設定されていない場合、チュートリアルを始める前に20GiBのボリュームを3つ、手動でプロビジョニングする必要があります。
目標
このチュートリアルを終えると、以下の知識を得られます。
- StatefulSetを使ってZooKeeperアンサンブルをデプロイする方法。
- アンサンブルを一貫して設定する方法。
- ZooKeeperサーバーのデプロイをアンサンブルに広げる方法。
- 計画されたメンテナンス中もサービスが利用可能であることを保証するためにPodDisruptionBudgetsを使う方法。
ZooKeeper
Apache ZooKeeperは、分散アプリケーションのための、分散型オープンソースコーディネーションサービスです。 ZooKeeperでは、データの読み書き、および更新の監視ができます。 データは階層化されてファイルシステム内に編成され、アンサンブル(ZooKeeperサーバーのセット)内の全てのZooKeeperサーバーに複製されます。 データへの全ての操作はアトミックかつ逐次的に一貫性を持ちます。 ZooKeeperは、アンサンブル内の全てのサーバー間でステートマシンを複製するためにZab合意プロトコルを使ってこれを保証します。
アンサンブルはリーダーを選出するのにZabプロトコルを使い、選出が完了するまでデータを書き出しません。 完了すると、アンサンブルは複製するのにZabを使い、書き込みが承認されてクライアントに可視化されるより前に、全ての書き込みをクォーラムに複製することを保証します。 重み付けされたクォーラムでなければ、クォーラムは現在のリーダーを含むアンサンブルの過半数を占めるコンポーネントです。 例えばアンサンブルが3つのサーバーを持つ時、リーダーとそれ以外のもう1つのサーバーを含むコンポーネントが、クォーラムを構成します。 アンサンブルがクォーラムに達しない場合、アンサンブルはデータを書き出せません。
ZooKeeperサーバー群はそれらの全てのステートマシンをメモリに保持し、それぞれの変化をストレージメディア上の永続的なWAL(Write Ahead Log)に書き出します。 サーバーがクラッシュした時には、WALをリプレーすることで以前のステートに回復できます。 WALを際限のない増加から防ぐために、ZooKeeperサーバーは、メモリステートにあるものをストレージメディアに定期的にスナップショットします。 これらのスナップショットはメモリに直接読み込むことができ、スナップショットより前の全てのWALエントリは破棄され得ます。
ZooKeeperアンサンブルの作成
以下のマニフェストはHeadless Service、Service、PodDisruptionBudget、StatefulSetを含んでいます。
apiVersion: v1
kind: Service
metadata:
  name: zk-hs
  labels:
    app: zk
spec:
  ports:
  - port: 2888
    name: server
  - port: 3888
    name: leader-election
  clusterIP: None
  selector:
    app: zk
---
apiVersion: v1
kind: Service
metadata:
  name: zk-cs
  labels:
    app: zk
spec:
  ports:
  - port: 2181
    name: client
  selector:
    app: zk
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  selector:
    matchLabels:
      app: zk
  maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: zk
spec:
  selector:
    matchLabels:
      app: zk
  serviceName: zk-hs
  replicas: 3
  updateStrategy:
    type: RollingUpdate
  podManagementPolicy: OrderedReady
  template:
    metadata:
      labels:
        app: zk
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: "app"
                    operator: In
                    values:
                    - zk
              topologyKey: "kubernetes.io/hostname"
      containers:
      - name: kubernetes-zookeeper
        imagePullPolicy: Always
        image: "registry.k8s.io/kubernetes-zookeeper:1.0-3.4.10"
        resources:
          requests:
            memory: "1Gi"
            cpu: "0.5"
        ports:
        - containerPort: 2181
          name: client
        - containerPort: 2888
          name: server
        - containerPort: 3888
          name: leader-election
        command:
        - sh
        - -c
        - "start-zookeeper \
          --servers=3 \
          --data_dir=/var/lib/zookeeper/data \
          --data_log_dir=/var/lib/zookeeper/data/log \
          --conf_dir=/opt/zookeeper/conf \
          --client_port=2181 \
          --election_port=3888 \
          --server_port=2888 \
          --tick_time=2000 \
          --init_limit=10 \
          --sync_limit=5 \
          --heap=512M \
          --max_client_cnxns=60 \
          --snap_retain_count=3 \
          --purge_interval=12 \
          --max_session_timeout=40000 \
          --min_session_timeout=4000 \
          --log_level=INFO"
        readinessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        livenessProbe:
          exec:
            command:
            - sh
            - -c
            - "zookeeper-ready 2181"
          initialDelaySeconds: 10
          timeoutSeconds: 5
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/zookeeper
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
  - metadata:
      name: datadir
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi
ターミナルを開き、マニフェストを作成するために
kubectl applyコマンドを使います。
kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml
これはzk-hs Headless Service、zk-cs Service、zk-pdb PodDisruptionBudget、 zk StatefulSetを作成します。
service/zk-hs created
service/zk-cs created
poddisruptionbudget.policy/zk-pdb created
statefulset.apps/zk created
StatefulSetのPodを作成するStatefulSetコントローラーを監視するため、kubectl getを使います。
kubectl get pods -w -l app=zk
zk-2 PodがRunningおよびReadyになったら、CTRL-Cでkubectlを終了してください。
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s
StatefulSetコントローラーが3つのPodを作成し、各PodはZooKeeperサーバー付きのコンテナを持ちます。
リーダーの選出のファシリテート
匿名のネットワークにおいてリーダー選出を終了するアルゴリズムがないので、Zabはリーダー選出のための明示的なメンバーシップ設定を要します。 アンサンブルの各サーバーはユニーク識別子を持つ必要があり、全てのサーバーは識別子のグローバルセットを知っている必要があり、各識別子はネットワークアドレスと関連付けられている必要があります。
zk StatefulSetのPodのホスト名を取得するためにkubectl execを使います。
for i in 0 1 2; do kubectl exec zk-$i -- hostname; done
StatefulSetコントローラーは各Podに、その順序インデックスに基づくユニークなホスト名を提供します。
ホスト名は<statefulset名>-<順序インデックス>という形をとります。
zk StatefulSetのreplicasフィールドが3にセットされているので、このセットのコントローラーは、ホスト名にそれぞれzk-0、zk-1、zk-2が付いた3つのPodを作成します。
zk-0
zk-1
zk-2
ZooKeeperアンサンブルのサーバーは、ユニーク識別子として自然数を使い、それぞれのサーバーの識別子をサーバーのデータディレクトリ内のmyidというファイルに格納します。
各サーバーのmyidファイルの内容を調べるには、以下のコマンドを使います。
for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done
識別子が自然数で順序インデックスは正の整数なので、順序に1を加算することで識別子を生成できます。
myid zk-0
1
myid zk-1
2
myid zk-2
3
zk StatefulSet内の各Podの完全修飾ドメイン名(FQDN)を取得するには、以下のコマンドを使います。
for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done
zk-hs Serviceは、全Podのためのドメインzk-hs.default.svc.cluster.localを作成します。
zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local
Kubernetes DNSのAレコードは、FQDNをPodのIPアドレスに解決します。 KubernetesがPodを再スケジュールした場合、AレコードはPodの新しいIPアドレスに更新されますが、Aレコードの名前は変更されません。
ZooKeeperはそのアプリケーション設定をzoo.cfgという名前のファイルに格納します。
zk-0 Pod内のzoo.cfgファイルの内容を見るには、kubectl execを使います。
kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg
ファイル末尾にあるserver.1、server.2、server.3のプロパティの、1、2、3はZooKeeperサーバーのmyidファイル内の識別子に対応します。
これらはzk StatefulSet内のPodのFQDNにセットされます。
clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInterval=0
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888
合意形成
合意(consensus)プロトコルは、各参加者の識別子がユニークであることを要件としています。 Zabプロトコル内で同じユニーク識別子を主張する2つの参加者はないものとします。 これは、システム内のプロセスが、どのプロセスがどのデータをコミットしたかを同意できるようにするために必須です。 2つのPodが同じ順序値で起動されたなら、2つのZooKeeperサーバーはどちらもそれら自身を同じサーバーとして認識してしまうでしょう。
kubectl get pods -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s
各PodのAレコードは、PodがReadyになった時に記入されます。そのため、ZooKeeperサーバー群のFQDNはある1つのエンドポイント、すなわちmyidファイルで設定された識別子を主張するユニークなZooKeeperサーバーに解決されます。
zk-0.zk-hs.default.svc.cluster.local
zk-1.zk-hs.default.svc.cluster.local
zk-2.zk-hs.default.svc.cluster.local
これは、ZooKeeperのzoo.cfgファイル内のserversプロパティが正しく設定されたアンサンブルを表していることを保証します。
server.1=zk-0.zk-hs.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-hs.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-hs.default.svc.cluster.local:2888:3888
サーバーが値のコミットを試みるためにZabプロトコルを使う時、(リーダー選出が成功していて、少なくともPodのうちの2つがRunningおよびReadyならば)それぞれのサーバーは双方の合意をとって値をコミット、あるいは、(もし双方の状態が合わなければ)それを行うことに失敗します。 あるサーバーが別のサーバーを代行して書き込みを承認する状態は発生しません。
アンサンブルの健全性テスト
最も基本的な健全性テストは、データを1つのZooKeeperサーバーに書き込み、そのデータを別のサーバーで読み取ることです。
以下のコマンドは、worldをアンサンブル内のzk-0 Podのパス/helloに書き込むのに、zkCli.shスクリプトを実行します。
kubectl exec zk-0 -- zkCli.sh create /hello world
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
Created /hello
zk-1 Podからデータを取得するには、以下のコマンドを使います。
kubectl exec zk-1 -- zkCli.sh get /hello
zk-0に作成したデータは、アンサンブル内の全てのサーバーで利用できます。
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
永続的なストレージの提供
ZooKeeperの概要のセクションで言及したように、 ZooKeeperは全てのエントリを永続的なWALにコミットし、定期的にメモリ状態のスナップショットをストレージメディアに書き出します。 永続性を提供するためにWALを使用するのは、複製されたステートマシンを立てるために合意プロトコルを使うアプリケーションでよくあるテクニックです。
zk StatefulSetを削除するために、kubectl deleteコマンドを使います。
kubectl delete statefulset zk
statefulset.apps "zk" deleted
StatefulSet内のPodの終了を観察します。
kubectl get pods -w -l app=zk
zk-0が完全に終了したら、CTRL-Cでkubectlを終了します。
zk-2      1/1       Terminating   0         9m
zk-0      1/1       Terminating   0         11m
zk-1      1/1       Terminating   0         10m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-2      0/1       Terminating   0         9m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-1      0/1       Terminating   0         10m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m
zk-0      0/1       Terminating   0         11m
zookeeper.yamlのマニフェストを再適用します。
kubectl apply -f https://k8s.io/examples/application/zookeeper/zookeeper.yaml
これはzk StatefulSetオブジェクトを作成しますが、マニフェストのその他のAPIオブジェクトはすでに存在しているので変更されません。
StatefulSetコントローラーがStatefulSetのPodを再作成するのを見てみます。
kubectl get pods -w -l app=zk
zk-2 PodがRunningおよびReadyになったら、CTRL-Cでkubectlを終了します。
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         19s
zk-0      1/1       Running   0         40s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       ContainerCreating   0         0s
zk-1      0/1       Running   0         18s
zk-1      1/1       Running   0         40s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         19s
zk-2      1/1       Running   0         40s
健全性テストで入力した値をzk-2 Podから取得するには、以下のコマンドを使います。
kubectl exec zk-2 zkCli.sh get /hello
zk StatefulSet内の全てのPodを終了して再作成したにもかかわらず、アンサンブルは元の値をなおも供給します。
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x100000002
ctime = Thu Dec 08 15:13:30 UTC 2016
mZxid = 0x100000002
mtime = Thu Dec 08 15:13:30 UTC 2016
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
zk StatefulSetのspecのvolumeClaimTemplatesフィールドは、各PodにプロビジョニングされるPersistentVolumeを指定します。
volumeClaimTemplates:
  - metadata:
      name: datadir
      annotations:
        volume.alpha.kubernetes.io/storage-class: anything
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 20Gi
StatefulSetコントローラーは、StatefulSet内の各PodのためにPersistentVolumeClaimを生成します。
StatefulSetのPersistentVolumeClaimsを取得するために、以下のコマンドを使います。
kubectl get pvc -l app=zk
StatefulSetがそのPodを再作成した時、StatefulSetはPodのPersistentVolumeを再マウントします。
NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
datadir-zk-0   Bound     pvc-bed742cd-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-1   Bound     pvc-bedd27d2-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
datadir-zk-2   Bound     pvc-bee0817e-bcb1-11e6-994f-42010a800002   20Gi       RWO           1h
StatefulSetのコンテナtemplateのvolumeMountsセクションは、ZooKeeperサーバーのデータディレクトリにあるPersistentVolumeをマウントします。
volumeMounts:
- name: datadir
  mountPath: /var/lib/zookeeper
zk StatefulSet内のPodが(再)スケジュールされると、ZooKeeperサーバーのデータディレクトリにマウントされた同じPersistentVolumeを常に得ます。
Podが再スケジュールされたとしても、全ての書き込みはZooKeeperサーバーのWALおよび全スナップショットに行われ、永続性は残ります。
一貫性のある設定の保証
リーダーの選出のファシリテートおよび合意形成のセクションで記したように、ZooKeeperのアンサンブル内のサーバー群は、リーダーを選出しクォーラムを形成するための一貫性のある設定を必要とします。 また、プロトコルがネットワーク越しで正しく動作するために、Zabプロトコルの一貫性のある設定も必要です。 この例では、設定を直接マニフェストに埋め込むことで一貫性のある設定を達成します。
zk StatefulSetを取得しましょう。
kubectl get sts zk -o yaml
…
command:
      - sh
      - -c
      - "start-zookeeper \
        --servers=3 \
        --data_dir=/var/lib/zookeeper/data \
        --data_log_dir=/var/lib/zookeeper/data/log \
        --conf_dir=/opt/zookeeper/conf \
        --client_port=2181 \
        --election_port=3888 \
        --server_port=2888 \
        --tick_time=2000 \
        --init_limit=10 \
        --sync_limit=5 \
        --heap=512M \
        --max_client_cnxns=60 \
        --snap_retain_count=3 \
        --purge_interval=12 \
        --max_session_timeout=40000 \
        --min_session_timeout=4000 \
        --log_level=INFO"
…
このcommandでは、ZooKeeperサーバーを開始するためにコマンドラインパラメータで設定を渡しています。 設定をアンサンブルへ渡すのには環境変数を使うこともできます。
ログの設定
zkGenConfig.shスクリプトで生成されたファイルの1つは、ZooKeeperのログを制御します。
ZooKeeperはLog4jを使い、デフォルトではログの設定に基づいて、ログ設定に時間およびサイズベースでのローリングファイルアペンダー(ログのローテーション)を使用します。
zk StatefulSet内のPodの1つからログ設定を取得するには、以下のコマンドを使います。
kubectl exec zk-0 cat /usr/etc/zookeeper/log4j.properties
以下のログ設定は、ZooKeeperにログの全てを標準出力ファイルストリームに書き出す処理をさせます。
zookeeper.root.logger=CONSOLE
zookeeper.console.threshold=INFO
log4j.rootLogger=${zookeeper.root.logger}
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold}
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n
これはログコンテナ内のログを安全にとるための、最もシンプルと思われる方法です。 アプリケーションはログを標準出力に書き出し、Kubernetesがログのローテーションを処理してくれます。 Kubernetesは、標準出力と標準エラー出力に書き出されるアプリケーションのログがローカルストレージメディアを使い尽くさないことを保証する、健全維持ポリシーも実装しています。
Podの1つから末尾20行を取得するために、kubectl logsを使ってみます。
kubectl logs zk-0 --tail 20
kubectl logsを利用するか、Kubernetes Dashboardから、標準出力または標準エラーに書き出されたアプリケーションログを参照できます。
2016-12-06 19:34:16,236 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52740
2016-12-06 19:34:16,237 [myid:1] - INFO  [Thread-1136:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52740 (no session established for client)
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52749
2016-12-06 19:34:26,155 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52749
2016-12-06 19:34:26,156 [myid:1] - INFO  [Thread-1137:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52749 (no session established for client)
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52750
2016-12-06 19:34:26,222 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52750
2016-12-06 19:34:26,226 [myid:1] - INFO  [Thread-1138:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52750 (no session established for client)
2016-12-06 19:34:36,151 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52760
2016-12-06 19:34:36,152 [myid:1] - INFO  [Thread-1139:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52760 (no session established for client)
2016-12-06 19:34:36,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52761
2016-12-06 19:34:36,231 [myid:1] - INFO  [Thread-1140:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52761 (no session established for client)
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52767
2016-12-06 19:34:46,149 [myid:1] - INFO  [Thread-1141:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52767 (no session established for client)
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@192] - Accepted socket connection from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@827] - Processing ruok command from /127.0.0.1:52768
2016-12-06 19:34:46,230 [myid:1] - INFO  [Thread-1142:NIOServerCnxn@1008] - Closed socket connection for client /127.0.0.1:52768 (no session established for client)
Kubernetesは多くのログソリューションを統合しています。 クラスターおよびアプリケーションに最も適合するログソリューションを選べます。 クラスターレベルのロギングとアグリゲーションとして、ログをローテートおよび輸送するためのサイドカーコンテナをデプロイすることを検討してください。
非特権ユーザーの設定
コンテナ内で特権ユーザーとしての実行をアプリケーションに許可するベストプラクティスは、議論の的です。 アプリケーションが非特権ユーザーとして動作することを組織で必須としているなら、エントリポイントがそのユーザーとして実行できるユーザーを制御するセキュリティコンテキストを利用できます。
zk StatefulSetのPod templateは、SecurityContextを含んでいます。
securityContext:
  runAsUser: 1000
  fsGroup: 1000
Podのコンテナ内で、UID 1000はzookeeperユーザーに、GID 1000はzookeeperグループにそれぞれ相当します。
zk-0 PodからのZooKeeperプロセス情報を取得してみましょう。
kubectl exec zk-0 -- ps -elf
securityContextオブジェクトのrunAsUserフィールドが1000にセットされているとおり、ZooKeeperプロセスは、rootとして実行される代わりにzookeeperユーザーとして実行されています。
F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S zookeep+     1     0  0  80   0 -  1127 -      20:46 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
0 S zookeep+    27     1  0  80   0 - 1155556 -    20:46 ?        00:00:19 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg
デフォルトでは、PodのPersistentVolumeがZooKeeperサーバーのデータディレクトリにマウントされている時、rootユーザーのみがそこにアクセス可能です。 この設定はZooKeeperのプロセスがそのWALに書き込んだりスナップショットに格納したりするのを妨げることになります。
zk-0 PodのZooKeeperデータディレクトリのファイルパーミッションを取得するには、以下のコマンドを使います。
kubectl exec -ti zk-0 -- ls -ld /var/lib/zookeeper/data
securityContextオブジェクトのfsGroupフィールドが1000にセットされているので、PodのPersistentVolumeの所有権はzookeeperグループにセットされ、ZooKeeperのプロセスがそのデータを読み書きできるようになります。
drwxr-sr-x 3 zookeeper zookeeper 4096 Dec  5 20:45 /var/lib/zookeeper/data
ZooKeeperプロセスの管理
ZooKeeperドキュメントでは、「You will want to have a supervisory process that manages each of your ZooKeeper server processes (JVM).(各ZooKeeperサーバープロセス(JVM)を管理する監督プロセスを持ちたくなります)」と述べています。 分散型システム内で失敗したプロセスを再起動するのにwatchdog(監督プロセス)を使うのは、典型的パターンです。 アプリケーションをKubernetesにデプロイする時には、監督プロセスのような外部ユーティリティを使うよりもむしろ、アプリケーションのwatchdogとしてKubernetesを使うべきです。
アンサンブルのアップデート
zk StatefulSetはRollingUpdateアップデート戦略を使うように設定されています。
サーバーに割り当てられるcpusの数を更新するのに、kubectl patchを利用できます。
kubectl patch sts zk --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"0.3"}]'
statefulset.apps/zk patched
更新の状況を見るには、kubectl rollout statusを使います。
kubectl rollout status sts/zk
waiting for statefulset rolling update to complete 0 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 1 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
waiting for statefulset rolling update to complete 2 pods at revision zk-5db4499664...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
statefulset rolling update complete 3 pods at revision zk-5db4499664...
これはPod群を終了し、逆の順番で1つずつそれらを新しい設定で再作成します。 これはクォーラムがローリングアップデート中に維持されることを保証します。
履歴や過去の設定を見るには、kubectl rollout historyコマンドを使います。
kubectl rollout history sts/zk
出力は次のようになります:
statefulsets "zk"
REVISION
1
2
変更をロールバックするには、kubectl rollout undoコマンドを使います。
kubectl rollout undo sts/zk
出力は次のようになります:
statefulset.apps/zk rolled back
プロセスの失敗の取り扱い
再起動ポリシーは、Pod内のコンテナのエントリポイントへのプロセスの失敗をKubernetesがどのように取り扱うかを制御します。
StatefulSet内のPodにおいて唯一妥当なRestartPolicyはAlwaysで、これはデフォルト値です。
ステートフルなアプリケーションでは、このデフォルトポリシーの上書きは絶対にすべきではありません。
zk-0 Pod内で実行されているZooKeeperサーバーのプロセスツリーを調査するには、以下のコマンドを使います。
kubectl exec zk-0 -- ps -ef
コンテナのエントリポイントとして使われるコマンドはPID 1、エントリポイントの子であるZooKeeperプロセスはPID 27となっています。
UID        PID  PPID  C STIME TTY          TIME CMD
zookeep+     1     0  0 15:03 ?        00:00:00 sh -c zkGenConfig.sh && zkServer.sh start-foreground
zookeep+    27     1  0 15:03 ?        00:00:03 /usr/lib/jvm/java-8-openjdk-amd64/bin/java -Dzookeeper.log.dir=/var/log/zookeeper -Dzookeeper.root.logger=INFO,CONSOLE -cp /usr/bin/../build/classes:/usr/bin/../build/lib/*.jar:/usr/bin/../share/zookeeper/zookeeper-3.4.9.jar:/usr/bin/../share/zookeeper/slf4j-log4j12-1.6.1.jar:/usr/bin/../share/zookeeper/slf4j-api-1.6.1.jar:/usr/bin/../share/zookeeper/netty-3.10.5.Final.jar:/usr/bin/../share/zookeeper/log4j-1.2.16.jar:/usr/bin/../share/zookeeper/jline-0.9.94.jar:/usr/bin/../src/java/lib/*.jar:/usr/bin/../etc/zookeeper: -Xmx2G -Xms2G -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=false org.apache.zookeeper.server.quorum.QuorumPeerMain /usr/bin/../etc/zookeeper/zoo.cfg
別のターミナルで、以下のコマンドを使ってzk StatefulSet内のPodを見てみます。
kubectl get pod -w -l app=zk
別のターミナルで、以下のコマンドを使ってPod zk-0内のZooKeeperプロセスを終了します。
kubectl exec zk-0 -- pkill java
ZooKeeperプロセスの終了は、その親プロセスの終了を引き起こします。
コンテナのRestartPolicyはAlwaysなので、親プロセスが再起動(restart)されます。
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          21m
zk-1      1/1       Running   0          20m
zk-2      1/1       Running   0          19m
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Error     0          29m
zk-0      0/1       Running   1         29m
zk-0      1/1       Running   1         29m
アプリケーションが、そのビジネスロジックを実装するプロセスを立ち上げるのにスクリプト(zkServer.shなど)を使っている場合、スクリプトは子プロセスとともに終了する必要があります。
これは、Kubernetesがアプリケーションのコンテナを、そのビジネスロジックを実装しているプロセスが失敗した時に再起動することを保証します。
生存性(liveness)テスト
失敗したプロセスを再起動するための設定をアプリケーションに施すのは、分散型システムの健全さを保つのに十分ではありません。 システムのプロセスが生きていることもあれば無反応なこともあり、あるいはそうでなく不健全という状況もあります。 アプリケーションのプロセスが不健全で再起動すべきであることをKubernetesに通知するには、liveness probeを使うのがよいでしょう。
zk StatefulSetのPod templateでliveness probeを指定します。
  livenessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5
プローブはサーバーの健全さをテストするのに、ZooKeeperのruok 4文字コマンドを使うbashスクリプトを呼び出します。
OK=$(echo ruok | nc 127.0.0.1 $1)
if [ "$OK" == "imok" ]; then
    exit 0
else
    exit 1
fi
ターミナルウィンドウで、zk StatefulSet内のPodを見るのに以下のコマンドを使います。
kubectl get pod -w -l app=zk
別のウィンドウで、Pod zk-0のファイルシステムからzookeeper-readyスクリプトを削除するために以下のコマンドを使います。
kubectl exec zk-0 -- rm /opt/zookeeper/bin/zookeeper-ready
ZooKeeperプロセスの失敗のためにliveness probeを使う時、アンサンブル内の不健全なプロセスが再起動されることを保証するために、Kubernetesは自動的にプロセスを再起動します。
kubectl get pod -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Running   0          1h
zk-0      0/1       Running   1         1h
zk-0      1/1       Running   1         1h
準備性(readiness)テスト
準備性は生存性と同じではありません。 プロセスが生きているのであれば、スケジュールされ健全です。 プロセスの準備ができたら、入力を処理できます。 生存性はなくてはならないものですが、準備性の状態には十分ではありません。 プロセスは生きてはいるが準備はできていない時、特に初期化および終了の間がそのケースに相当します。
readiness probeを指定するとKubernetesは、準備性チェックに合格するまで、アプリケーションのプロセスがネットワークトラフィックを受け取らないことを保証します。
ZooKeeperサーバーにとって、健全性は準備性を意味します。
そのため、zookeeper.yamlマニフェストからのreadiness probeは、liveness probeと同一です。
  readinessProbe:
    exec:
      command:
      - sh
      - -c
      - "zookeeper-ready 2181"
    initialDelaySeconds: 15
    timeoutSeconds: 5
liveness probeとreadiness probeが同一だとしても、両方を指定することが重要です。 これは、ZooKeeperアンサンブル内の健全なサーバーだけがネットワークトラフィックを受け取ることを保証します。
ノードの失敗の許容
ZooKeeperはデータの変更を正しくコミットするのにサーバーのクォーラムを必要とします。 3つのサーバーのアンサンブルにおいては、書き込みの成功のために2つのサーバーは健全でなければなりません。 クォーラムベースのシステムにおいて、可用性を保証するために、メンバーは障害ドメインにデプロイされます。 個々のマシンの損失による障害を避けるためのベストプラクティスは、同じマシン上でアプリケーションの複数のインスタンスがコロケート(同じ場所に配置)されないようにすることです。
デフォルトでKubernetesは、同じノードのStatefulSetにPodをコロケートします。
3つのサーバーアンサンブルを作成していたとして、2つのサーバーが同じノードにあり、そのノードが障害を起こした場合、ZooKeeperサービスのクライアントは、少なくともPodの1つが再スケジュールされるまで障害に見舞われることになります。
クリティカルシステムのプロセスがノードの失敗イベントで再スケジュールできるよう、追加のキャパシティを常にプロビジョンしておくべきです。
そうしておけば、障害は単にKubernetesのスケジューラーがZooKeeperのサーバーの1つを再スケジュールするまでの辛抱です。
ただし、ダウンタイムなしでノードの障害への耐性をサービスに持たせたいなら、podAntiAffinityをセットすべきです。
zk StatefulSet内のPodのノードを取得するには、以下のコマンドを使います。
for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done
zk StatefulSet内の全てのPodは、別々のノードにデプロイされます。
kubernetes-node-cxpk
kubernetes-node-a5aq
kubernetes-node-2g2d
これはzk StatefulSet内のPodにPodAntiAffinityの指定があるからです。
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: "app"
              operator: In
              values:
                - zk
        topologyKey: "kubernetes.io/hostname"
requiredDuringSchedulingIgnoredDuringExecutionフィールドは、topologyKeyで定義されたドメイン内でappラベルの値がzkの2つのPodが絶対にコロケートすべきでないことを、Kubernetes Schedulerに指示します。
topologyKeyのkubernetes.io/hostnameは、ドメインが固有ノードであることを示しています。
異なるルール、ラベル、セレクターを使って、物理・ネットワーク・電源といった障害ドメイン全体に広がるアンサンブルにこのテクニックを広げることができます。
メンテナンス時の存続
このセクションでは、ノードをcordon(スケジュール不可化)およびdorain(解放)します。もし共有クラスターでこのチュートリアルを試しているのであれば、これがほかのテナントに有害な影響を及ぼさないことを確認してください。
前のセクションでは、計画外のノード障害に備えてどのようにPodをノード全体に広げるかを示しましたが、計画されたメンテナンスのため引き起こされる一時的なノード障害に対して計画する必要もあります。
クラスター内のノードを取得するために、以下のコマンドを使います。
kubectl get nodes
このチュートリアルでは、4つのノードのあるクラスターを仮定しています。
クラスターが4つよりも多くある場合には、4つのノード以外全てをcordonするためにkubectl cordonを使ってください。
ノードを4つに制約することで、以下のメンテナンスシミュレーションにおいてzookeeper Podをスケジュールした時に、KubernetesがアフィニティとPodDisruptionBudget制約に遭遇することを保証します。
kubectl cordon <ノード名>
zk-pdbのPodDisruptionBudgetを取得するために、以下のコマンドを使います。
kubectl get pdb zk-pdb
max-unavailableフィールドは、zk StatefulSetの最大で1つのPodがいつでも利用できなくなる可能性があるということを、Kubernetesに指示します。
NAME      MIN-AVAILABLE   MAX-UNAVAILABLE   ALLOWED-DISRUPTIONS   AGE
zk-pdb    N/A             1                 1
1つ目のターミナルで、zk StatefulSet内のPodを見るのに以下のコマンドを使います。
kubectl get pods -w -l app=zk
次に別のターミナルで、Podが現在スケジュールされているノードを取得するために、以下のコマンドを使います。
for i in 0 1 2; do kubectl get pod zk-$i --template {{.spec.nodeName}}; echo ""; done
出力は次のようになります:
kubernetes-node-pb41
kubernetes-node-ixsl
kubernetes-node-i4c4
zk-0 Podがスケジュールされているノードをcordonおよびdrainするには、kubectl drainを使います。
kubectl drain $(kubectl get pod zk-0 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data
出力は次のようになります:
node "kubernetes-node-pb41" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-pb41, kube-proxy-kubernetes-node-pb41; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-o5elz
pod "zk-0" deleted
node "kubernetes-node-pb41" drained
クラスターに4つのノードがあるので、kubectl drainは成功し、zk-0が別のノードに再スケジュールされます。
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
最初のターミナルでStatefulSetのPodを見守り、zk-1がスケジュールされたノードをdrainします。
kubectl drain $(kubectl get pod zk-1 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data
出力は次のようになります:
"kubernetes-node-ixsl" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-ixsl, kube-proxy-kubernetes-node-ixsl; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-voc74
pod "zk-1" deleted
node "kubernetes-node-ixsl" drained
zk StatefulSetがPodのコロケーションを抑止するPodAntiAffinityルールを含んでいるので、zk-1 Podはスケジュールされず、またスケジュール可能なのは2つのノードだけなので、PodはPendingの状態のままになっています。
kubectl get pods -w -l app=zk
出力は次のようになります:
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
StatefulSetのPodを見続け、zk-2がスケジュールされているノードをdrainします。
kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data
出力は次のようになります:
node "kubernetes-node-i4c4" cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
WARNING: Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog; Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4
There are pending pods when an error occurred: Cannot evict pod as it would violate the pod's disruption budget.
pod/zk-2
kubectlを終了するためにCTRL-Cを押します。
zk-2を退去させるとzk-budget違反になってしまうので、3つ目のノードはdrainできません。ただし、ノードはcordonされたままとなります。
健全性テスト中に入力した値をzk-0から取得するには、zkCli.shを使います。
kubectl exec zk-0 zkCli.sh get /hello
PodDisruptionBudgetが遵守されているので、サービスはまだ利用可能です。
WatchedEvent state:SyncConnected type:None path:null
world
cZxid = 0x200000002
ctime = Wed Dec 07 00:08:59 UTC 2016
mZxid = 0x200000002
mtime = Wed Dec 07 00:08:59 UTC 2016
pZxid = 0x200000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
最初のノードをuncordon(スケジュール可能化)するには、kubectl uncordonを使います。
kubectl uncordon kubernetes-node-pb41
出力は次のようになります:
node "kubernetes-node-pb41" uncordoned
zk-1はこのノードで再スケジュールされます。zk-1がRunningおよびReadyになるまで待ちます。
kubectl get pods -w -l app=zk
出力は次のようになります:
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   2          1h
zk-1      1/1       Running   0          1h
zk-2      1/1       Running   0          1h
NAME      READY     STATUS        RESTARTS   AGE
zk-0      1/1       Terminating   2          2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Terminating   2         2h
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       ContainerCreating   0         0s
zk-0      0/1       Running   0         51s
zk-0      1/1       Running   0         1m
zk-1      1/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Terminating   0         2h
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         0s
zk-1      0/1       Pending   0         12m
zk-1      0/1       ContainerCreating   0         12m
zk-1      0/1       Running   0         13m
zk-1      1/1       Running   0         13m
試しにzk-2がスケジュールされているノードをdrainしてみます。
kubectl drain $(kubectl get pod zk-2 --template {{.spec.nodeName}}) --ignore-daemonsets --force --delete-emptydir-data
出力は次のようになります:
node "kubernetes-node-i4c4" already cordoned
WARNING: Deleting pods not managed by ReplicationController, ReplicaSet, Job, or DaemonSet: fluentd-cloud-logging-kubernetes-node-i4c4, kube-proxy-kubernetes-node-i4c4; Ignoring DaemonSet-managed pods: node-problem-detector-v0.1-dyrog
pod "heapster-v1.2.0-2604621511-wht1r" deleted
pod "zk-2" deleted
node "kubernetes-node-i4c4" drained
今度はkubectl drainは成功しました。
zk-2の再スケジュールができるように、2つ目のノードをuncordonします。
kubectl uncordon kubernetes-node-ixsl
出力は次のようになります:
node "kubernetes-node-ixsl" uncordoned
サービスがメンテナンス中も利用可能なままであることを保証するために、PodDisruptionBudgetsとあわせてkubectl drainを利用できます。
メンテナンスでノードがオフラインになる前にノードをcordonして、Podを退去させるのにdrainが使われている場合、Disruption Budget(停止状態の予算)を表すサービスは遵守すべきバジェットを持ちます。
クリティカルサービスでは、Podをすぐに再スケジュールできるよう、追加のキャパティを常に割り当てておくべきです。
クリーンアップ
- クラスターの全てのノードをuncordonするために、kubectl uncordonを実行してください。
- このチュートリアルで使ったPersistentVolumeの永続的なストレージメディアを削除する必要があります。 全てのストレージが回収されたことを確実とするために、お使いの環境、ストレージ設定、プロビジョニング方法に基いて必要な手順に従ってください。