【Kubernetes】StatefulSetについて理解する

2023.02.23
2024.03.24
Kubernetes
StatefulSet

本ページはAmazonアフィリエイトのリンクを含みます。

はじめに

KubernetesのStatefulSetについて、どんなものなのか動かしながら理解していきたいと思います。

StatefulSetとは

StatefulSetとは、データベースなどステートフルなPodを管理するオブジェクトになります。

ざっくり特徴としては、下記のものが挙げられます。

  • 各Podは一意なインデックスを含むホスト名がつけられ、再起動した場合でも同じホスト名のPodが起動される(pod-0, pod-1, pod2など)
  • 各Podに対して一意なPVCが割り当てられ、Podが削除されてもPVCは削除されない
  • 作成されるPodはインデックスが小さい順から作成され、前のPodがReadyになるまで次のPodは作成開始されない
  • 削除されるPodはインデックスが大きい順から削除され、前のPodが削除されるまで次のPodは削除されない
  • Headless Serviceを作成する必要がある

Headless Service

Headless Serviceとは、ClusterIPタイプのServiceでClusterIPの値が"None"に設定されているServiceのことです。

StatefulSetのように各Podに一意な役割があり、負荷分散のためのIPアドレスが不要な場合に使われます。

1apiVersion: v1
2kind: Service
3metadata:
4  name: headless-svc
5  labels:
6    app: sts-app
7spec:
8  ports:
9  - port: 80
10  clusterIP: None # clusterIPをNoneにすることでHeadless Serviceになる
11  selector:
12    app: sts-app

試してみる

実際にStatefulSetを操作しながら、特徴を理解していきます。

作成するファイルは以下の通りです。

1.
2├── service.yml
3└── statefulset.yml

service.ymlstatefulset.ymlの内容は下記の通りです。

1apiVersion: v1
2kind: Service
3metadata:
4  name: myapp-svc
5  labels:
6    app: myapp
7spec:
8  ports:
9  - port: 80
10  clusterIP: None
11  selector:
12    app: myapp
1apiVersion: apps/v1
2kind: StatefulSet
3metadata:
4  name: myapp
5spec:
6  serviceName: "myapp-svc"
7  replicas: 3
8  selector:
9    matchLabels:
10      app: myapp
11  template:
12    metadata:
13      labels:
14        app: myapp
15    spec:
16      containers:
17      - name: web
18        image: nginx:1.22
19        ports:
20        - containerPort: 80
21        volumeMounts:
22        - name: html
23          mountPath: /usr/share/nginx/html
24  volumeClaimTemplates:
25  - metadata:
26      name: html
27    spec:
28      accessModes: [ "ReadWriteOnce" ]
29      resources:
30        requests:
31          storage: 1Gi

作成

まずは、StatefulSetを作成してみます。

1kubectl apply -f statefulset.yml

作成される状況を確認すると、インデックスが小さいPodから作成され、前のPodが作成されてから次のPodが作成開始されているのがわかります。

1❯ kubectl get pod -w
2NAME      READY   STATUS              RESTARTS   AGE
3myapp-0   0/1     ContainerCreating   0          2s   # 1つ目のPod起動開始
4myapp-0   1/1     Running             0          5s
5myapp-1   0/1     Pending             0          0s   # 2つ目のPod起動開始
6myapp-1   0/1     ContainerCreating   0          0s
7myapp-1   1/1     Running             0          3s
8myapp-2   0/1     Pending             0          0s   # 3つ目のPod起動開始
9myapp-2   0/1     ContainerCreating   0          0s
10myapp-2   1/1     Running             0          4s

Headless Serviceも作成してみます。

1kubectl apply -f service.yml

下記のコマンドで確認すると、ClusterIPが"None"になっていることが確認できます。

1❯ kubectl get svc
2NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
3myapp-svc    ClusterIP   None         <none>        80/TCP    5s

名前

名前についてもう少し確認してみます。

各Podは一意な名前がついています。

1❯ kubectl get pod
2NAME      READY   STATUS    RESTARTS   AGE
3myapp-0   1/1     Running   0          9m47s
4myapp-1   1/1     Running   0          9m42s
5myapp-2   1/1     Running   0          9m39s

Podからホスト名を確認してみても、同じ名前が付けられていることが確認できます。

1❯ kubectl exec myapp-1 -- hostname
2myapp-1

例え、あるPodを削除したとしても同じ名前のPodが削除され、ホスト名も同じになっています。

1❯ kubectl delete pod myapp-1
2pod "myapp-1" deleted
3
4❯ kubectl get pod
5NAME      READY   STATUS    RESTARTS   AGE
6myapp-0   1/1     Running   0          10m
7myapp-1   1/1     Running   0          13s
8myapp-2   1/1     Running   0          10m
9
10❯ kubectl exec myapp-1 -- hostname
11myapp-1

マウントするストレージ

次はマウントされているストレージ(PVC)について確認してみます。

PVCを確認すると、volumeClaimTemplatesから作成された各Podに紐づくPVCが確認できます。

1❯ kubectl get pvc
2NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
3html-myapp-0   Bound    pvc-aca73200-b48b-4617-8dd6-40832a03a812   1Gi        RWO            hostpath       3m39s
4html-myapp-1   Bound    pvc-c7444acd-52ce-4363-9217-0e299f4c822a   1Gi        RWO            hostpath       3m33s
5html-myapp-2   Bound    pvc-84c45fcc-c6ca-48ee-90ea-3a281be87b00   1Gi        RWO            hostpath       3m28s

例え、Podを削除したとしても、紐づいていたPVCは削除されずに、再起動した同じ名前のPodに再度マウントされているのが確認できます。

1❯ kubectl describe pod myapp-2 | grep -A 4 Volumes:
2Volumes:
3  html:
4    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
5    ClaimName:  html-myapp-2
6    ReadOnly:   false
7
8❯ kubectl delete pod myapp-2
9pod "myapp-2" deleted
10
11❯ kubectl get pvc
12NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
13html-myapp-0   Bound    pvc-aca73200-b48b-4617-8dd6-40832a03a812   1Gi        RWO            hostpath       46m
14html-myapp-1   Bound    pvc-c7444acd-52ce-4363-9217-0e299f4c822a   1Gi        RWO            hostpath       46m
15html-myapp-2   Bound    pvc-84c45fcc-c6ca-48ee-90ea-3a281be87b00   1Gi        RWO            hostpath       46m
16
17❯ kubectl describe pod myapp-2 | grep -A 4 Volumes:
18Volumes:
19  html:
20    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
21    ClaimName:  html-myapp-2
22    ReadOnly:   false

Podの更新

Podに更新があった場合の動きについて確認してみます。

下記コマンドでイメージの更新を行ってみます。

1kubectl patch statefulset myapp -p '{"spec":{"template":{"spec":{"containers":[{"name":"web","image":"nginx:1.23"}]}}}}'

インデックスの大きいPodから停止され更新されているのが確認できます。

1❯ kubectl get pod -w
2NAME      READY   STATUS              RESTARTS   AGE
3myapp-0   1/1     Running             0          44s
4myapp-1   1/1     Running             0          37s
5myapp-2   1/1     Terminating         0          41s # インデックスが大きいPodから停止
6myapp-2   0/1     Pending             0          0s
7myapp-2   0/1     ContainerCreating   0          0s
8myapp-2   1/1     Running             0          1s  # Podの更新が完了
9myapp-1   1/1     Terminating         0          39s # 次のPodが停止
10myapp-1   0/1     Pending             0          0s
11myapp-1   0/1     ContainerCreating   0          0s
12myapp-1   1/1     Running             0          3s  # Podの更新が完了
13myapp-0   1/1     Terminating         0          50s # 次のPodが停止
14myapp-0   0/1     Pending             0          0s
15myapp-0   0/1     ContainerCreating   0          0s
16myapp-0   1/1     Running             0          2s  # Podの更新が完了

スケール

スケールアウト、スケールインの場合の動きを見てみます。

まずはスケールアウトしてみます。

1kubectl scale --replicas=5 statefulset myapp

インデックスの続きからPodが作成されているのが確認できます。

1❯ kubectl get pod -w
2NAME      READY   STATUS              RESTARTS   AGE
3myapp-0   1/1     Running             0          9m28s
4myapp-1   1/1     Running             0          9m32s
5myapp-2   1/1     Running             0          9m34s
6myapp-3   0/1     Pending             0          1s
7myapp-3   0/1     ContainerCreating   0          2s
8myapp-3   1/1     Running             0          4s
9myapp-4   0/1     Pending             0          1s
10myapp-4   0/1     ContainerCreating   0          2s
11myapp-4   1/1     Running             0          4s

次にスケールインしてみます。

1kubectl scale --replicas=3 statefulset myapp

インデックスの大きいPodから終了して、スケールインしています。

1❯ kubectl get pod -w
2NAME      READY   STATUS        RESTARTS   AGE
3myapp-0   1/1     Running       0          10m
4myapp-1   1/1     Running       0          10m
5myapp-2   1/1     Running       0          10m
6myapp-3   1/1     Running       0          76s
7myapp-4   1/1     Terminating   0          72s
8myapp-4   0/1     Terminating   0          72s
9myapp-3   1/1     Terminating   0          76s
10myapp-3   0/1     Terminating   0          77s

削除

最後に削除について確認してみます。

StatefulSetを削除してみます。

1kubectl delete statefulset myapp

インデックスが大きいPodから削除されるはずですが、タイミングが早すぎるのかkubectlの出力的には小さいものから削除されているように見えてしまっています。

1❯ kubectl get pod -w
2NAME      READY   STATUS        RESTARTS   AGE
3myapp-0   1/1     Terminating   0          21m
4myapp-1   1/1     Terminating   0          21m
5myapp-2   1/1     Terminating   0          21m
6myapp-0   0/1     Terminating   0          21m
7myapp-0   0/1     Terminating   0          21m
8myapp-1   0/1     Terminating   0          21m
9myapp-1   0/1     Terminating   0          21m
10myapp-2   0/1     Terminating   0          21m
11myapp-2   0/1     Terminating   0          21m

試しにレプリカを10にして、削除してみましたが、出力はバラバラになっていました。

1❯ kubectl get pod -w
2NAME      READY   STATUS        RESTARTS   AGE
3myapp-0   1/1     Terminating   0          43s
4myapp-1   1/1     Terminating   0          41s
5myapp-2   1/1     Terminating   0          40s
6myapp-3   1/1     Terminating   0          25s
7myapp-4   1/1     Terminating   0          24s
8myapp-5   1/1     Terminating   0          22s
9myapp-6   1/1     Terminating   0          21s
10myapp-7   1/1     Terminating   0          17s
11myapp-8   1/1     Terminating   0          15s
12myapp-9   1/1     Terminating   0          13s
13myapp-4   0/1     Terminating   0          25s
14myapp-4   0/1     Terminating   0          25s
15myapp-3   0/1     Terminating   0          26s
16myapp-3   0/1     Terminating   0          26s
17myapp-8   0/1     Terminating   0          16s
18myapp-8   0/1     Terminating   0          16s
19myapp-9   0/1     Terminating   0          15s
20myapp-9   0/1     Terminating   0          15s
21myapp-2   0/1     Terminating   0          42s
22myapp-2   0/1     Terminating   0          43s
23myapp-7   0/1     Terminating   0          20s
24myapp-7   0/1     Terminating   0          20s
25myapp-5   0/1     Terminating   0          26s
26myapp-5   0/1     Terminating   0          26s
27myapp-0   0/1     Terminating   0          48s
28myapp-0   0/1     Terminating   0          48s
29myapp-1   0/1     Terminating   0          47s
30myapp-1   0/1     Terminating   0          47s
31myapp-6   0/1     Terminating   0          27s
32myapp-6   0/1     Terminating   0          27s

公式のドキュメントでも、インデックスの逆順から削除されると文章では書いてあるのですが、出力例は逆順にはなっていないので、kubectlの出力の問題なのかもしれません。 https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/#cascading-delete

また、StatefulSetを削除したとしても、Headless ServiceやPVCは削除されずに残っています。

1❯ kubectl get svc
2NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
3myapp-svc    ClusterIP   None         <none>        80/TCP    57m
4
5❯ kubectl get pvc
6NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
7html-myapp-0   Bound    pvc-aca73200-b48b-4617-8dd6-40832a03a812   1Gi        RWO            hostpath       62m
8html-myapp-1   Bound    pvc-c7444acd-52ce-4363-9217-0e299f4c822a   1Gi        RWO            hostpath       62m
9html-myapp-2   Bound    pvc-84c45fcc-c6ca-48ee-90ea-3a281be87b00   1Gi        RWO            hostpath       62m

参考

Support

\ この記事が役に立ったと思ったら、サポートお願いします! /

buy me a coffee
Share

Profile

author

Masa

都内のIT企業で働くエンジニア
自分が学んだことをブログでわかりやすく発信していきながらスキルアップを目指していきます!

buy me a coffee