StatefulSetとPersistentVolumeを使ってステートフルアプリケーションを動かす

2021年7月16日(金)
深見 圭介

StatefulSetの利用

StatefulSetはDeploymentに似たワークロードAPIですが、その名の通りステートフルワークロードを管理するためのさまざまな機能を備えています。重要な特徴は、管理下のPodの再スケジューリング(再作成)をまたがって同一のストレージをマウントできることです。一意のネットワーク識別子を付与されるなどほかの特徴もありますが本記事では割愛します。

以前のサンプルアプリケーションに含まれる、Articleサービスの開発用マニフェストからデータベースサービスのマニフェストを抜粋しました。さらに前回のものから変更してPV/PVCを利用するようになっています。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: articledb
  name: articledb
spec:
  replicas: 1
  serviceName: articledb
  selector:
    matchLabels:
      app: articledb
  template:
    metadata:
      labels:
        app: articledb
    spec:
      containers:
      - name: postgres
        image: postgres:13-alpine
        env:
        - name: POSTGRES_PASSWORD
          value: 'postgres'
        - name: POSTGRES_DB
          value: 'article'
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        resources: {}
        ports:
        - name: postgres
          containerPort: 5432
          protocol: TCP
        volumeMounts:
        - name: dbschema
          mountPath: /docker-entrypoint-initdb.d/schema.sql
          subPath: schema.sql
        - name: article-data
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: dbschema
        configMap:
          name: article-init-db
  volumeClaimTemplates:
  - metadata:
      name: article-data
    spec:
      accessModes:
      - ReadWriteOnce
      storageClassName: local-path
      resources:
        requests:
          storage: 1Gi

マニフェストの前半部分、volumeClaimTemplatesより前はDeploymentの場合とほとんど同じですが、1か所だけDeploymentではあり得ないところがあります。9行目のserviceNameというフィールドです。これはこのStatefulSetに対応するServiceリソースを名前で参照しています。今回のマニフェストではそれほど重要ではないので、そういうフィールドがあることだけ触れておきます。また、よく見るとvolumeMountsの指定は2つあるのにvolumesには1つしかVolumeが定義されていません。これは後述のvolumeClaimTemplatesから作成されるVolumeをStatefulSetがPodを作成するときに追加するためです。

コンテナはpostgres:13-alpineイメージを利用しており、PostgreSQLの設定はDocker Hubの説明に従って認証パスワード*1やDB名を指定しています。また、PGDATA環境変数により、後述する永続ストレージにPostgreSQLのデータベースファイルを格納するよう設定しています。

*1 ここでは開発用マシンなどクローズドな環境で利用することを想定し、脆弱なパスワードをマニフェストに直接指定しています。本番環境などではSecretリソース等を利用して設定すべきです。そもそも、マニフェストに記載してgitリポジトリ等に保存すべきではありません。

volumeClaimTemplatesにPersistentVolumeClaimのテンプレートが設定されています。このテンプレートをもとに、StatefulSetのPodごとにPVCが作成されます。ここでは名前とアクセスモード、ストレージ容量を指定して、さらにstorageClassNameでlocal-pathを指定しています。これは動的プロビジョニングを利用して後述するlocal-path-provisionerによりPVを作成するためです。先にも書いた通り、ここでPVCのテンプレートを追加したVolumeはPodTemplateのvolumesフィールドには追加せず、.metadata.nameに指定した名前でvolumeMountsフィールドからマウント先を指定します。

サンプルアプリケーションでのPV/PVCの利用

サンプルアプリケーションにPV/PVCを導入して利用することで、データベースサービスの情報が保存されることを見てみましょう。

サンプルアプリケーションにPV/PVCをを導入する前に、利用するCSI Driverを導入します。利用するのはcsi-driver-host-pathというデモ・テスト用*2のCSI Driverです。リポジトリの手順に従って導入します。

*2 リポジトリの説明にある通り、hostPathを利用しているためノード障害などでデータが失われます。デモやCSI Driverのサンプルとして実装されており、本番環境での利用は想定されていません。

まずは、kubectlコマンドが利用できる場所に当該リポジトリを取得します。

git clone 'https://github.com/kubernetes-csi/csi-driver-host-path.git'
cd csi-driver-host-path

用意されているスクリプトを実行します。

./deploy/kubernetes-latest/deploy.sh

以下のコマンドでPodが正常に稼働しているか確認できます。

$ kubectl get po -l app.kubernetes.io/name=csi-hostpathplugin
NAME                   READY   STATUS    RESTARTS   AGE
csi-hostpathplugin-0   8/8     Running   0          8m33s

次に、StorageClassを作成します。リポジトリに含まれている設定例を利用します。

kubectl apply -f examples/csi-storageclass.yaml

ここで導入されるStorageClassリソースを見てみましょう。StorageClassの定義を抜粋すると、以下のようになっています。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-hostpath-sc
provisioner: hostpath.csi.k8s.io
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true

ここで、provisioner: hostpath.csi.k8s.ioというProvisionerを指定しており、この名前はcsi-driver-host-pathのデフォルトのProvisioner名*3になっています。

*3 具体的にはソースコードにデフォルト値の定義があります。

PVCを利用するマニフェストは、前回リポジトリchapter6-use-pvcブランチで公開していますので、前回取得済みの場合は以下のコマンドでブランチを切り替えれば取得できます。

git pull
git checkout chapter6-use-pvc

まだ取得していなかったり、上記コマンドでエラーが出たりする場合*4は別のディレクトリに以下のコマンドで新しくcloneしても構いません。

*4 ファイルを変更していると発生する場合があります。学習の成果ですね!

git clone --branch chapter6-use-pvc -- https://gitlab.com/creationline/thinkit-kubernetes-sample1-manifests.git

取得したリポジトリのoverlays/develop/statefulset-articledb.yamlを確認して上記と同様の内容であることを確認したら、次のコマンドでデプロイしましょう。

kubectl apply -k overlays/develop/

もし、前回でデプロイしたアプリケーションが残っていた場合は、以下のようなエラーが出るはずです。

The StatefulSet "accesscountdb" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', and 'updateStrategy' are forbidden
The StatefulSet "articledb" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', and 'updateStrategy' are forbidden
The StatefulSet "rankdb" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', and 'updateStrategy' are forbidden

その場合は、以下のコマンドで一度アプリケーションを削除します。

kubectl delete -k overlays/develop/

削除後にPodが正しく終了しているか(kubectl get podの結果に表示されないか)を確認し、再度上記のコマンドでアプリケーションをデプロイします。

エラーなくデプロイできたら稼働状況を確認しましょう。まずはPodから見ていきます。

$ kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
pod/article-656969d8fc-qm9h9      1/1     Running   0          5m44s
pod/rank-668b486b7-vhgfn          1/1     Running   0          5m44s
pod/website-5c8ff7886b-drz6b      1/1     Running   0          5m44s
pod/accesscount-7787ffd8f-rf2fw   1/1     Running   0          5m44s
pod/articledb-0                   1/1     Running   0          5m44s
pod/accesscountdb-0               1/1     Running   0          5m44s
pod/rankdb-0                      1/1     Running   0          5m44s

正しく動作しているようです。PVとPVCも見てみましょう。

$ kubectl get pvc,pv
NAME                                                     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/accesscount-data-accesscountdb-0   Bound    pvc-b0f8e6e6-8bb3-45e1-af31-a99addf644f3   1Gi        RWO            local-path     5m44s
persistentvolumeclaim/article-data-articledb-0           Bound    pvc-c123f106-d200-4331-87e3-6b7670a12f97   1Gi        RWO            local-path     5m44s
persistentvolumeclaim/rank-data-rankdb-0                 Bound    pvc-c4a2c4ff-2f77-49f8-897a-2bbfd686075f   1Gi        RWO            local-path     5m44s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                      STORAGECLASS   REASON   AGE
persistentvolume/pvc-b0f8e6e6-8bb3-45e1-af31-a99addf644f3   1Gi        RWO            Delete           Bound    default/accesscount-data-accesscountdb-0   local-path              5m37s
persistentvolume/pvc-c123f106-d200-4331-87e3-6b7670a12f97   1Gi        RWO            Delete           Bound    default/article-data-articledb-0           local-path              5m37s
persistentvolume/pvc-c4a2c4ff-2f77-49f8-897a-2bbfd686075f   1Gi        RWO            Delete           Bound    default/rank-data-rankdb-0                 local-path              5m36s

PV/PVCのSTATUSがそれぞれBound(割り当て済み)になっており、PVCはVOLUME列に、PVはCLAIM列に割り当てられたリソース名が表示されています。

デプロイされたことが確認できたら、記事を作成してみましょう。以下のようにcurlコマンドで記事エントリを作成します。<ノードのIPアドレス>は第3回で設定したIngressによるアクセスのためのアドレスで、ワーカーノードとして利用しているマシンのグローバルIPアドレスです。

$ curl -XPOST -H 'Content-Type: application/json' --data-binary '{"title":"Some Title","body":"Interesting body is here.","author":"John, Doe"}' http:///api/articles/
{"title":"Some Title","author":"John, Doe","body":"Interesting body is here."}

作成されたエントリを確認すると、

$ curl http://<ノードのIPアドレス>/api/articles/
[{"id":1,"title":"Some Title","author":"John, Doe"}]

のように返ってきます(記事作成コマンドを複数回実行しているとその回数分だけ登録されます)。

ここで、Podが何らかの原因で再起動しなければならなくなったとしましょう。状況を再現するため、次のコマンドで同じネームスペースのPodをすべて一度終了します。

$ kubectl get pod
NAME                          READY   STATUS    RESTARTS   AGE
rank-668b486b7-fx94q          1/1     Running   0          41s
article-656969d8fc-4qkgv      1/1     Running   0          41s
website-5c8ff7886b-6pslv      1/1     Running   0          41s
accesscount-7787ffd8f-hsqbt   1/1     Running   0          41s
articledb-0                   1/1     Running   0          35s
rankdb-0                      1/1     Running   0          34s
accesscountdb-0               1/1     Running   0          33s
$ kubectl delete pod --all --wait=false
pod "rank-668b486b7-fx94q" deleted
pod "article-656969d8fc-4qkgv" deleted
pod "website-5c8ff7886b-6pslv" deleted
pod "accesscount-7787ffd8f-hsqbt" deleted
pod "articledb-0" deleted
pod "rankdb-0" deleted
pod "accesscountdb-0" deleted
$ kubectl get pod
NAME                          READY   STATUS              RESTARTS   AGE
rank-668b486b7-fx94q          1/1     Terminating         0          57s
article-656969d8fc-4qkgv      1/1     Terminating         0          57s
website-5c8ff7886b-6pslv      1/1     Terminating         0          57s
accesscount-7787ffd8f-hsqbt   1/1     Terminating         0          57s
articledb-0                   1/1     Terminating         0          51s
rankdb-0                      1/1     Terminating         0          50s
accesscountdb-0               1/1     Terminating         0          49s
website-5c8ff7886b-55zb9      0/1     ContainerCreating   0          3s
accesscount-7787ffd8f-r6lc7   0/1     ContainerCreating   0          3s
rank-668b486b7-vlvn2          1/1     Running             0          3s
article-656969d8fc-f59pw      1/1     Running             0          3s

deleteコマンドが実行された直後の状態を見るため、kubectl delete pod コマンドに--wait=falseオプションを付けて実行しています。直後のget podコマンドで一度すべてのPodがTerminatingになっていることが分かります。

少し待って正常に動作していることを確認します。

$ kubectl get pod
NAME                          READY   STATUS    RESTARTS   AGE
rank-668b486b7-vlvn2          1/1     Running   0          43s
article-656969d8fc-f59pw      1/1     Running   0          43s
website-5c8ff7886b-55zb9      1/1     Running   0          43s
accesscount-7787ffd8f-r6lc7   1/1     Running   0          43s
accesscountdb-0               1/1     Running   0          27s
articledb-0                   1/1     Running   0          27s
rankdb-0                      1/1     Running   0          26s

Podが正常に動作していることを確認後、再度エントリを確認してみます。

$ curl http://<ノードのIPアドレス>/api/articles/
[{"id":1,"title":"Some Title","author":"John, Doe"}]

articleエントリが残っていること*5が確認できました。

*5 以前のマニフェストでデプロイして同じことを行うとPV/PVCが使われていないためarticleエントリが残りません。

おわりに

今回はPV/PVCとStatefulSetを用いて、ステートフルアプリケーションを扱う方法を解説しました。ステートフルアプリケーションの管理はアプリケーションそのものの特性や要件によって、さまざまなことを考慮する必要がありますが、Kubernetesで扱えないものではないので、特徴を理解して利用しましょう。また、今現在も活発に開発が行われている部分ですので、最新の情報をチェックして活用していきましょう。

日立ソリューションズ・クリエイト
日立ソリューションズ・クリエイトに所属し、Webアプリケーション開発に従事したのち、 2019年1月からクリエーションライン株式会社と製造業向けにコンテナ/Kubernetesを活用したデータ分析基盤やIoTアプリケーション基盤の開発に従事。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています