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

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

はじめに

前回は、アプリケーションのマニフェスト管理でkustomizeを使って複数環境のマニフェストを整理しました。

ところで、デプロイしたアプリケーションはデータベースにPostgreSQLを稼働しています。データベースのように、システムの状態を保存する役割を持ったアプリケーションを「ステートフルアプリケーション」と呼びます。今回はステートフルアプリケーションをKubernetes上で利用するための仕組みである「StatefulSet」と「PersistentVolume」などを紹介します。

Kubernetes上での
ステートフルアプリケーションについて

ステートフルアプリケーションは、状態としてのデータを保持することが大きな責務です。細かな要件はシステムによって異なりますが、データを確実に保存し、失わないために、ステートレスアプリケーションよりも考慮事項が多くなります。

ステートフルアプリケーションは、ステートレスアプリケーションに比べると、要件によって変わる部分が多く、可用性・整合性など特に高い要件を満たそうとすると十分なアプリケーションやエコシステムがそろっているとは言えない部分もあり、公開された知見も少なくなります。既にKubernetesを運用している組織でも、ステートフルなアプリケーションは実績のあるベアメタルサーバーやVM、マネージドサービスを利用しているケースは多いです。

ただ、検証用途や高いレベルでの可用性を求めないのであれば、コンテナアプリケーションとして稼働することで、製品を「とりあえず使える」状態で立ち上げることは簡単にできます。検証で「主目的である検証を早く実施したい(冗長構成の構築やチューニングは後回しにしたい)」という状況はよくあると思いますが、そういった場合にコンテナアプリケーションが利用できれば最適でしょう。

現状としては以上の通りですが、今回はKubernetesで用意されているステートフルアプリケーションのための仕組みを確認しながら、前回デプロイした開発用マニフェストに含まれるPostgreSQLのマニフェストを修正してPodを再起動してもデータが保持されるように変更してみます。

Pod-Lifecycleと紐づいた
Volumeとその課題

ステートフルアプリケーションを実行するにあたって、状態を保存するストレージの利用は必須です。ホストファイルシステムやネットワークストレージをPodから利用する方法として「Volume」という仕組みがあります。まず、簡単にVolumeを紹介し、最後にVolumeではステートフルアプリケーションにとっての永続ストレージとして不十分であることを説明します。

以下に、Volumeとしてホスト上の領域をPod/コンテナにマウントするマニフェストの例を示します。

apiVersion: v1
kind: Pod
metadata:
  name: postgres
spec:
  containers:
  - name: db
    image: postgres:latest
    volumeMounts:
    - name: data
      mountPath: /mnt/data/
  volumes:
  - name: data
    emptyDir: {}

このマニフェストにより、ホスト上に確保された空のディレクトリがPodに割り当てられ、Pod内のdbコンテナの/mnt/data/にマウントされます。

コンテナ内のファイルシステムはコンテナが再起動すると初期状態に戻ります。しかし、コンテナよりも長いライフサイクルを持つPodからすると、Podのライフサイクルに合わせたストレージが欲しくなることがあります。例えばPod内の複数のコンテナ間でファイルを共有するとき、一部のコンテナの再起動により共有ファイルがリセットされてしまうと、ほかのアプリケーションは(必ずしもとは言えませんが)たいてい混乱することになるでしょう。こういった場合にVolumeを利用して共有するファイルのライフタイムをPodのライフタイムまで延長します。

では、Volumeを使えば永続ストレージとして十分でしょうか。残念ながら、Volumeは基本的にそのライフタイムをPodと共有しており、PodはKubernetes上でEphemeralなリソースと考えられているため、永続ストレージとして使われるべきではありません。

emptyDirではPodが削除されたときにその内容が破棄されます。つまりPodのライフサイクルを超えてVolume上のデータを利用することは物理的にできません。また、ネットワークファイルシステムやクラウドストレージを利用すると内容を破棄しないようにすることもできますが、Podが削除されると利用していたストレージそのものの情報がKubernetes上から消えてしまうため、ストレージ管理で重要なデータの破棄や、以前使われていたデータが引き継がれていることがKubernetes API上で保証・確認できません。

  • (問題点1) VolumeはPodのライフタイムに依存しており、ストレージのライフサイクル管理ができない

また、Volume設定ではNFS(Network File System)やクラウドストレージを具体的に指定して利用するため、クラスタ管理者が適切に各ストレージを管理する必要があります。したがって、管理者は開発チームなどが必要になったときに割り当てのための管理作業を手動で行う必要が発生し、迅速な割り当てが難しくなります。

  • (問題点2) 具体的なストレージの情報を直接扱うため、ストレージとストレージの種類の抽象化が難しく、ライフサイクル管理の自動化ができない

こういった問題を解決するために、Kubernetes上で提供されているリソースを次節以降で見ていきます。

永続ストレージをコンテナに提供する
PersistentVolume/PersistentVolumeClaim

Kubernetesでは、永続ストレージを利用する場合にPersistentVolume(以下、PV)とPersistentVolumeClaim(以下、PVC)を用いてPodにストレージを紐づけます。PVは具体的なストレージを表しており、ストレージ容量や、ネットワークファイルシステムであればアクセスするためのアドレスなども持っています。

NFSを利用する場合のPVの例を見ていきましょう。

apiVersion: v1
kind: PersistentVolume
metadata:
  labels:
    release: stable
  name: pv0003
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: slow
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: /tmp
    server: 172.17.0.2
  • ストレージの種類によらない設定:
    release: stable:リソースのグルーピングのためのラベル
    storage: 5Gi:利用したいストレージ容量
    volumeMode: Filesystem:ボリュームの利用方法
    accessModes::このPVをマウントできるアクセスモード。アクセスモードには3種類がある(詳細)
    persistentVolumeReclaimPolicy: Recycle:PVCにバインドされ、その後PVCが(利用を終了して)削除された後の扱いを指定(詳細)
    storageClassName: slow:このボリュームが所属するStorageClassの名前を指定。StorageClassについては次節で解説
  • ストレージの種類による設定:
    mountOptions::ストレージの種類によって必要なオプションを指定(詳細)
    nfs::NFSで提供されるストレージを指す。NFS固有の設定としてNFSサーバのアドレスとサーバ上のパスを指定

PVCはどのようなPVを必要とするかを記述するためのリソースであり、開発チームなどが抽象的な要求(容量・共有アクセスの可否・ボリュームの種類など)を記述します。KubernetesはPVCが要求した内容に対して最終的にPVをバインド(割り当て)することにより、PodでPVを利用できるようになります。

実際のPVCの例を見てみましょう。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 8Gi
  storageClassName: slow
  selector:
    matchLabels:
      release: "stable"
  • 以下は、バインドするPVが提供するべき機能について指定しており、PVの対応するフィールドが要求を満たせる値である必要があります。
    accessModes:volumeMode: Filesystem
    storageClassName: slow
  • resources::バインドするPVが提供するべきストレージ容量を設定。要求量であるため、ここで指定したものより大きいものが割り当てられる可能性もある
  • selector::このフィールドでPVのラベルに関する条件を指定することで、より柔軟にPVを絞り込む

PVやPVCはPodとは独立したリソースとしてKubernetes上で管理されるため、前節の(問題点1)が解消され、ストレージそのものの抽象化もPVという形で表現されているため、(問題点2)も一部解消されています。

では、PVCが作成されたときにバインドされるPVはどこから来るのでしょうか。1つの方法は、クラスタ管理者がバックエンドとなるストレージを準備し、それに対応するPVを手動でKubernetes上に作成しておく方法です。事前に作成されたPVの中からPVCの要求を満たすPVが自動的にKubernetesによって選択されバインドされます。

しかし、この方法には(問題点2)と同じ問題があります。すなわちPVの作成とストレージのプロビジョニングのために人手の作業を必要としており、まだ十分に自動化されていません。

永続ストレージのプロビジョニングを自動化する
ProvisionerとStorageClass

前節で説明した「クラスタ管理者が事前に必要なPVを設定し、利用時はPVCを通じてPodに割り当てる」というプロビジョニングの手順を「Static Provisioning」(静的プロビジョニング)と呼びます。対して、プロビジョニングを自動化して「クラスタ管理者はプロビジョニングに必要な情報だけを事前に設定して利用時にPVCに応じて必要なPVを作成し、PVCに割り当てる」という手順を「Dynamic Provisioning」(動的プロビジョニング)と呼びます。

動的プロビジョニングにおいてさまざまなストレージに対応し、実際にストレージのプロビジョニングを行うため、Kubernetesは「Provisioner」というコンポーネントを持っています。Provisionerには大別して2種類あります。1つは「in-tree provisioner」と呼ばれる、Kubernetesのコアリポジトリ内で開発され、Kubernetesのコンポーネントに組み込まれたProvisionerです。もう1つはContainer Storage Interface(CSI)というインターフェース仕様に基づいてKubernetesコミュニティ以外でも開発されているCSI Driverを用いたProvisionerです。CSI仕様はプロビジョニング以外の機能も標準化しているためCSI Driverを用いるほうが高機能ですが、in-tree provisionerのほうが導入に手間が少なく、比較的手軽に利用できます。

Provisionerは利用するストレージによってさまざまありますが、実際にクラスタ管理者が提供するストレージの詳細は「StorageClass」というKubernetes上のリソースで表現します。StorageClassリソースとして、AWSの「Amazon EBS」というクラウドストレージサービスを利用する例を挙げます(AWS上で稼働するKubernetesクラスタを前提とする例)。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: slow
provisioner: kubernetes.io/aws-ebs
parameters:
  type: io1
  iopsPerGB: "10"
  fsType: ext4
  • name: slow:iopsPerGB(後述)などで、それほど高くない性能を指定しているため分かりやすい名前を設定。 name: low-costなどでも良い
  • provisioner: kubernetes.io/aws-ebs:Amazon EBSにアクセスして必要なストレージのプロビジョニングを行うin-tree provisionerを指定。当然、異なるストレージを使う場合はそれに合わせたProvisionerを指定する必要があるが、同じ種類のストレージを利用する場合でもCSI Driverを使うときは異なるProvisionerを指定することがある
  • parameters::この配下はProvisionerによって異なるパラメータが設定される。今回の例ではAmazon EBSに固有のパラメータで性能やファイルシステムを指定している
    type: io1:Amazon EBSが提供するストレージの種類を指定
    iopsPerGB: "10" / fsType: ext4:Amazon EBSが提供するストレージの性能やファイルシステムの種類などを指定。ストレージの種類やProvisionerによっても指定できるパラメータや指定方法が変わることがある

動的プロビジョニングのまとめ

ここで、一度永続ストレージと動的プロビジョニングに関する部分をまとめておきます。関係するリソース/コンポーネントは以下の通りです。

  • PersistentVolume(PV)
    ・Podリソースに直接依存せずにストレージを表すリソース
    ・PVCにバインドされてPodから利用する
    ・静的プロビジョニングではクラスタ管理者が作成する
    ・動的プロビジョニングではStorageClassの設定とPersistenceVolumeClaimの要求に応じてProvisionerが作成する
  • PersistentVolumeClaim(PVC)
    ・必要なストレージの要求を表現するためのリソース
    ・主に抽象的な機能の指定を行う
    ・PVが自動的にバインドされて利用可能になる
    ・動的プロビジョニングを利用する場合は管理者が作成したStorageClassを参照することで利用するストレージの詳細を指定する
  • Provisioner
    ・要求に対して自動的にストレージのプロビジョニングを行うKubernetesのコントローラ
  • StorageClass
    ・クラスタにおいて自動プロビジョニングによって提供できるストレージを表すリソース
    ・動的プロビジョニングを利用するためにクラスタ管理者が作成する

動的プロビジョニングによってPodにストレージがマウントされるまで流れの簡単に追うと、以下の通りです。

  1. クラスタ管理者が実際のストレージ装置や利用するためのStorageClassリソースを用意する。必要に応じてCSI Driver等もセットアップする
  2. ストレージ利用
    1. Pod/PVCリソースを利用者が作成
    2. ProvisionerがPVCを検知する
    3. Provisionerがストレージ領域を作成する
    4. Provisionerがストレージ利用に必要なPVを作成する
    5. 作成したPVにPVCをバインドする
    6. PodがPVCをマウントしてストレージを利用する
日立ソリューションズ・クリエイト
日立ソリューションズ・クリエイトに所属し、Webアプリケーション開発に従事したのち、 2019年1月からクリエーションライン株式会社と製造業向けにコンテナ/Kubernetesを活用したデータ分析基盤やIoTアプリケーション基盤の開発に従事。

連載バックナンバー

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

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

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

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