レプリカを持つステートフルアプリケーションを実行する
このページでは、StatefulSet コントローラーを使用して、レプリカを持つステートフルアプリケーションを実行する方法を説明します。 ここでの例は、非同期レプリケーションを行う複数のスレーブを持つ、単一マスターのMySQLです。
この例は本番環境向けの構成ではないことに注意してください。 具体的には、MySQLの設定が安全ではないデフォルトのままとなっています。 これはKubernetesでステートフルアプリケーションを実行するための一般的なパターンに焦点を当てるためです。
始める前に
Kubernetesクラスターが必要、かつそのクラスターと通信するためにkubectlコマンドラインツールが設定されている必要があります。 このチュートリアルは、コントロールプレーンのホストとして動作していない少なくとも2つのノードを持つクラスターで実行することをおすすめします。 まだクラスターがない場合、minikubeを使って作成するか、 以下のいずれかのKubernetesプレイグラウンドも使用できます:
バージョンを確認するには次のコマンドを実行してください:kubectl version
.ここで使用されているPersistentVolumeClaimsの要件を満たすには、デフォルトのStorageClassを使用して動的PersistentVolumeプロビジョナーを作成するか、PersistentVolumesを静的にプロビジョニングする必要があります。
- このチュートリアルは、あなたがPersistentVolume とStatefulSet、 さらにはPod、 Service、 ConfigMapなどの 他のコアな概念に精通していることを前提としています。
- MySQLに関する知識は記事の理解に役立ちますが、 このチュートリアルは他のシステムにも役立つ一般的なパターンを提示することを目的としています。
目標
- StatefulSetコントローラーを使用して、レプリカを持つMySQLトポロジーをデプロイします。
- MySQLクライアントトラフィックを送信します。
- ダウンタイムに対する耐性を観察します。
- StatefulSetをスケールアップおよびスケールダウンします。
MySQLをデプロイする
このMySQLのデプロイの例は、1つのConfigMap、2つのService、および1つのStatefulSetから構成されます。
ConfigMap
次のYAML設定ファイルからConfigMapを作成します。
kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
labels:
app: mysql
app.kubernetes.io/name: mysql
data:
primary.cnf: |
# Apply this config only on the primary.
[mysqld]
log-bin
replica.cnf: |
# Apply this config only on replicas.
[mysqld]
super-read-only
このConfigMapは、MySQLマスターとスレーブの設定を独立して制御するために、
それぞれのmy.cnf
を上書きする内容を提供します。
この場合、マスターはスレーブにレプリケーションログを提供するようにし、
スレーブはレプリケーション以外の書き込みを拒否するようにします。
ConfigMap自体に特別なことはありませんが、ConfigMapの各部分は異なるPodに適用されます。 各Podは、StatefulSetコントローラーから提供される情報に基づいて、 初期化時にConfigMapのどの部分を見るかを決定します。
Services
以下のYAML設定ファイルからServiceを作成します。
kubectl apply -f https://k8s.io/examples/application/mysql/mysql-services.yaml
# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- name: mysql
port: 3306
clusterIP: None
selector:
app: mysql
---
# Client service for connecting to any MySQL instance for reads.
# For writes, you must instead connect to the master: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
name: mysql-read
labels:
app: mysql
spec:
ports:
- name: mysql
port: 3306
selector:
app: mysql
ヘッドレスサービスは、StatefulSetコントローラーが
StatefulSetの一部であるPodごとに作成するDNSエントリーのベースエントリーを提供します。
この例ではヘッドレスサービスの名前はmysql
なので、同じKubernetesクラスタの
同じ名前空間内の他のPodは、<pod-name>.mysql
を名前解決することでPodにアクセスできます。
mysql-read
と呼ばれるクライアントサービスは、独自のクラスタIPを持つ通常のServiceであり、
Ready状態のすべてのMySQL Podに接続を分散します。
Serviceのエンドポイントには、MySQLマスターとすべてのスレーブが含まれる可能性があります。
読み込みクエリーのみが、負荷分散されるクライアントサービスを使用できることに注意してください。 MySQLマスターは1つしかいないため、クライアントが書き込みを実行するためには、 (ヘッドレスサービス内のDNSエントリーを介して)MySQLのマスターPodに直接接続する必要があります。
StatefulSet
最後に、次のYAML設定ファイルからStatefulSetを作成します。
kubectl apply -f https://k8s.io/examples/application/mysql/mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
serviceName: mysql
replicas: 3
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- "-c"
- |
set -ex
# Generate mysql server-id from pod ordinal index.
[[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# Add an offset to avoid reserved server-id=0 value.
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# Copy appropriate conf.d files from config-map to emptyDir.
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/master.cnf /mnt/conf.d/
else
cp /mnt/config-map/slave.cnf /mnt/conf.d/
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
- name: clone-mysql
image: gcr.io/google-samples/xtrabackup:1.0
command:
- bash
- "-c"
- |
set -ex
# Skip the clone if data already exists.
[[ -d /var/lib/mysql/mysql ]] && exit 0
# Skip the clone on master (ordinal index 0).
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
[[ $ordinal -eq 0 ]] && exit 0
# Clone data from previous peer.
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
# Prepare the backup.
xtrabackup --prepare --target-dir=/var/lib/mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ALLOW_EMPTY_PASSWORD
value: "1"
ports:
- name: mysql
containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
resources:
requests:
cpu: 500m
memory: 1Gi
livenessProbe:
exec:
command: ["mysqladmin", "ping"]
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
# Check we can execute queries over TCP (skip-networking is off).
command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: gcr.io/google-samples/xtrabackup:1.0
ports:
- name: xtrabackup
containerPort: 3307
command:
- bash
- "-c"
- |
set -ex
cd /var/lib/mysql
# Determine binlog position of cloned data, if any.
if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
# XtraBackup already generated a partial "CHANGE MASTER TO" query
# because we're cloning from an existing slave. (Need to remove the tailing semicolon!)
cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
# Ignore xtrabackup_binlog_info in this case (it's useless).
rm -f xtrabackup_slave_info xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then
# We're cloning directly from master. Parse binlog position.
[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm -f xtrabackup_binlog_info xtrabackup_slave_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi
# Check if we need to complete a clone by starting replication.
if [[ -f change_master_to.sql.in ]]; then
echo "Waiting for mysqld to be ready (accepting connections)"
until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done
echo "Initializing replication from clone position"
mysql -h 127.0.0.1 \
-e "$(<change_master_to.sql.in), \
MASTER_HOST='mysql-0.mysql', \
MASTER_USER='root', \
MASTER_PASSWORD='', \
MASTER_CONNECT_RETRY=10; \
START SLAVE;" || exit 1
# In case of container restart, attempt this at-most-once.
mv change_master_to.sql.in change_master_to.sql.orig
fi
# Start a server to send backups when requested by peers.
exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
"xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
resources:
requests:
cpu: 100m
memory: 100Mi
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
次のコマンドを実行して起動の進行状況を確認できます。
kubectl get pods -l app=mysql --watch
しばらくすると、3つのPodすべてがRunning状態になるはずです。
NAME READY STATUS RESTARTS AGE
mysql-0 2/2 Running 0 2m
mysql-1 2/2 Running 0 1m
mysql-2 2/2 Running 0 1m
Ctrl+Cを押してウォッチをキャンセルします。 起動が進行しない場合は、始める前にで説明されているように、 PersistentVolumeの動的プロビジョニング機能が有効になっていることを確認してください。
このマニフェストでは、StatefulSetの一部としてステートフルなPodを管理するためにさまざまな手法を使用しています。 次のセクションでは、これらの手法のいくつかに焦点を当て、StatefulSetがPodを作成するときに何が起こるかを説明します。
ステートフルなPodの初期化を理解する
StatefulSetコントローラーは、序数インデックスの順にPodを一度に1つずつ起動します。 各PodがReady状態を報告するまで待機してから、その次のPodの起動が開始されます。
さらに、コントローラーは各Podに <statefulset-name>-<ordinal-index>
という形式の一意で不変の名前を割り当てます。
この例の場合、Podの名前はmysql-0
、mysql-1
、そしてmysql-2
となります。
上記のStatefulSetマニフェスト内のPodテンプレートは、これらのプロパティーを利用して、 MySQLレプリケーションの起動を順序正しく実行します。
構成を生成する
Podスペック内のコンテナを起動する前に、Podは最初に 初期化コンテナを定義された順序で実行します。
最初の初期化コンテナはinit-mysql
という名前で、序数インデックスに基づいて特別なMySQL設定ファイルを生成します。
スクリプトは、hostname
コマンドによって返されるPod名の末尾から抽出することによって、自身の序数インデックスを特定します。
それから、序数を(予約された値を避けるために数値オフセット付きで)MySQLのconf.d
ディレクトリーのserver-id.cnf
というファイルに保存します。
これは、StatefulSetコントローラーによって提供される一意で不変のIDを、同じ特性を必要とするMySQLサーバーIDの領域に変換します。
さらに、init-mysql
コンテナ内のスクリプトは、master.cnf
またはslave.cnf
のいずれかを、
ConfigMapから内容をconf.d
にコピーすることによって適用します。
このトポロジー例は単一のMySQLマスターと任意の数のスレーブで構成されているため、
スクリプトは単に序数の0
がマスターになるように、それ以外のすべてがスレーブになるように割り当てます。
StatefulSetコントローラーによる
デプロイ順序の保証と組み合わせると、
スレーブが作成される前にMySQLマスターがReady状態になるため、スレーブはレプリケーションを開始できます。
既存データをクローンする
一般に、新しいPodがセットにスレーブとして参加するときは、 MySQLマスターにはすでにデータがあるかもしれないと想定する必要があります。 また、レプリケーションログが期間の先頭まで全て揃っていない場合も想定する必要があります。 これらの控えめな仮定は、実行中のStatefulSetのサイズを初期サイズに固定するのではなく、 時間の経過とともにスケールアップまたはスケールダウンできるようにするために重要です。
2番目の初期化コンテナはclone-mysql
という名前で、スレーブPodが空のPersistentVolumeで最初に起動したときに、
クローン操作を実行します。
つまり、実行中の別のPodから既存のデータをすべてコピーするので、
そのローカル状態はマスターからレプリケーションを開始するのに十分な一貫性があります。
MySQL自体はこれを行うためのメカニズムを提供していないため、この例ではPercona XtraBackupという人気のあるオープンソースツールを使用しています。
クローンの実行中は、ソースとなるMySQLサーバーのパフォーマンスが低下する可能性があります。
MySQLマスターへの影響を最小限に抑えるために、スクリプトは各Podに序数インデックスが自分より1低いPodからクローンするように指示します。
StatefulSetコントローラーは、N+1
のPodを開始する前には必ずN
のPodがReady状態であることを保証するので、この方法が機能します。
レプリケーションを開始する
初期化コンテナが正常に完了すると、通常のコンテナが実行されます。
MySQLのPodは実際にmysqld
サーバーを実行するmysql
コンテナと、
サイドカー
として機能するxtrabackup
コンテナから成ります。
xtrabackup
サイドカーはクローンされたデータファイルを見て、
スレーブ上でMySQLレプリケーションを初期化する必要があるかどうかを決定します。
もし必要がある場合、mysqld
が準備できるのを待ってから、
XtraBackupクローンファイルから抽出されたレプリケーションパラメーターでCHANGE MASTER TO
とSTART SLAVE
コマンドを実行します。
スレーブがレプリケーションを開始すると、スレーブはMySQLマスターを記憶し、
サーバーが再起動した場合または接続が停止した場合に、自動的に再接続します。
また、スレーブはその不変のDNS名(mysql-0.mysql
)でマスターを探すため、
再スケジュールされたために新しいPod IPを取得したとしても、自動的にマスターを見つけます。
最後に、レプリケーションを開始した後、xtrabackup
コンテナはデータのクローンを要求する他のPodからの接続を待ち受けます。
StatefulSetがスケールアップした場合や、次のPodがPersistentVolumeClaimを失ってクローンをやり直す必要がある場合に備えて、
このサーバーは無期限に起動したままになります。
クライアントトラフィックを送信する
テストクエリーをMySQLマスター(ホスト名 mysql-0.mysql
)に送信するには、
mysql:5.7
イメージを使って一時的なコンテナを実行し、mysql
クライアントバイナリーを実行します。
kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
mysql -h mysql-0.mysql <<EOF
CREATE DATABASE test;
CREATE TABLE test.messages (message VARCHAR(250));
INSERT INTO test.messages VALUES ('hello');
EOF
Ready状態を報告したいずれかのサーバーにテストクエリーを送信するには、ホスト名mysql-read
を使用します。
kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
mysql -h mysql-read -e "SELECT * FROM test.messages"
次のような出力が得られるはずです。
Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
+---------+
| message |
+---------+
| hello |
+---------+
pod "mysql-client" deleted
mysql-read
サービスがサーバー間で接続を分散させることを実証するために、
ループでSELECT @@server_id
を実行することができます。
kubectl run mysql-client-loop --image=mysql:5.7 -i -t --rm --restart=Never --\
bash -ic "while sleep 1; do mysql -h mysql-read -e 'SELECT @@server_id,NOW()'; done"
接続の試行ごとに異なるエンドポイントが選択される可能性があるため、
報告される@@server_id
はランダムに変更されるはずです。
+-------------+---------------------+
| @@server_id | NOW() |
+-------------+---------------------+
| 100 | 2006-01-02 15:04:05 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW() |
+-------------+---------------------+
| 102 | 2006-01-02 15:04:06 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW() |
+-------------+---------------------+
| 101 | 2006-01-02 15:04:07 |
+-------------+---------------------+
ループを止めたいときはCtrl+Cを押すことができますが、別のウィンドウで実行したままにしておくことで、 次の手順の効果を確認できます。
PodとNodeのダウンタイムをシミュレーションする
単一のサーバーではなくスレーブのプールから読み取りを行うことによって可用性が高まっていることを実証するため、
Podを強制的にReadyではない状態にする間、上記のSELECT @@server_id
ループを実行したままにしてください。
Readiness Probeを壊す
mysql
コンテナに対する
readiness probe
は、mysql -h 127.0.0.1 -e 'SELECT 1'
コマンドを実行することで、サーバーが起動していてクエリーが実行できることを確認します。
このreadiness probeを失敗させる1つの方法は、そのコマンドを壊すことです。
kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off
ここでは、mysql-2
Podの実際のコンテナのファイルシステムにアクセスし、
mysql
コマンドの名前を変更してreadiness probeがコマンドを見つけられないようにしています。
数秒後、Podはそのコンテナの1つがReadyではないと報告するはずです。以下を実行して確認できます。
kubectl get pod mysql-2
READY
列の1/2
を見てください。
NAME READY STATUS RESTARTS AGE
mysql-2 1/2 Running 0 3m
この時点で、SELECT @@server_id
ループは実行され続け、しかしもう102
が報告されないことが確認できるはずです。
init-mysql
スクリプトがserver-id
を100+$ordinal
として定義したことを思い出して下さい。
そのため、サーバーID102
はPodのmysql-2
に対応します。
それではPodを修復しましょう。すると数秒後にループ出力に再び現れるはずです。
kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql.off /usr/bin/mysql
Podを削除する
StatefulSetは、Podが削除された場合にPodを再作成します。 これはReplicaSetがステートレスなPodに対して行うのと同様です。
kubectl delete pod mysql-2
StatefulSetコントローラーはmysql-2
Podがもう存在しないことに気付き、
同じ名前で同じPersistentVolumeClaimにリンクされた新しいPodを作成します。
サーバーID102
がしばらくの間ループ出力から消えて、また元に戻るのが確認できるはずです。
ノードをdrainする
Kubernetesクラスタに複数のノードがある場合は、 drainを発行して ノードのダウンタイム(例えばノードのアップグレード時など)をシミュレートできます。
まず、あるMySQL Podがどのノード上にいるかを確認します。
kubectl get pod mysql-2 -o wide
ノード名が最後の列に表示されるはずです。
NAME READY STATUS RESTARTS AGE IP NODE
mysql-2 2/2 Running 0 15m 10.244.5.27 kubernetes-minion-group-9l2t
その後、次のコマンドを実行してノードをdrainします。
これにより、新しいPodがそのノードにスケジュールされないようにcordonされ、そして既存のPodは強制退去されます。
<node-name>
は前のステップで確認したノードの名前に置き換えてください。
この操作はノード上の他のアプリケーションに影響を与える可能性があるため、 テストクラスタでのみこの操作を実行するのが最善です。
kubectl drain <node-name> --force --delete-local-data --ignore-daemonsets
Podが別のノードに再スケジュールされる様子を確認しましょう。
kubectl get pod mysql-2 -o wide --watch
次のような出力が見られるはずです。
NAME READY STATUS RESTARTS AGE IP NODE
mysql-2 2/2 Terminating 0 15m 10.244.1.56 kubernetes-minion-group-9l2t
[...]
mysql-2 0/2 Pending 0 0s <none> kubernetes-minion-group-fjlm
mysql-2 0/2 Init:0/2 0 0s <none> kubernetes-minion-group-fjlm
mysql-2 0/2 Init:1/2 0 20s 10.244.5.32 kubernetes-minion-group-fjlm
mysql-2 0/2 PodInitializing 0 21s 10.244.5.32 kubernetes-minion-group-fjlm
mysql-2 1/2 Running 0 22s 10.244.5.32 kubernetes-minion-group-fjlm
mysql-2 2/2 Running 0 30s 10.244.5.32 kubernetes-minion-group-fjlm
また、サーバーID102
がSELECT @@server_id
ループの出力からしばらくの消えて、
そして戻ることが確認できるはずです。
それでは、ノードをuncordonして正常な状態に戻しましょう。
kubectl uncordon <node-name>
スレーブの数をスケーリングする
MySQLレプリケーションでは、スレーブを追加することで読み取りクエリーのキャパシティーをスケールできます。 StatefulSetを使用している場合、単一のコマンドでこれを実行できます。
kubectl scale statefulset mysql --replicas=5
次のコマンドを実行して、新しいPodが起動してくるのを確認します。
kubectl get pods -l app=mysql --watch
新しいPodが起動すると、サーバーID103
と104
がSELECT @@server_id
ループの出力に現れます。
また、これらの新しいサーバーが、これらのサーバーが存在する前に追加したデータを持っていることを確認することもできます。
kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
mysql -h mysql-3.mysql -e "SELECT * FROM test.messages"
Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
+---------+
| message |
+---------+
| hello |
+---------+
pod "mysql-client" deleted
元の状態へのスケールダウンもシームレスに可能です。
kubectl scale statefulset mysql --replicas=3
ただし、スケールアップすると新しいPersistentVolumeClaimが自動的に作成されますが、 スケールダウンしてもこれらのPVCは自動的には削除されないことに注意して下さい。 このため、初期化されたPVCをそのまま置いておいくことで再スケールアップを速くしたり、 PVを削除する前にデータを抽出するといった選択が可能になります。
次のコマンドを実行してこのことを確認できます。
kubectl get pvc -l app=mysql
StatefulSetを3にスケールダウンしたにもかかわらず、5つのPVCすべてがまだ存在しています。
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
data-mysql-0 Bound pvc-8acbf5dc-b103-11e6-93fa-42010a800002 10Gi RWO 20m
data-mysql-1 Bound pvc-8ad39820-b103-11e6-93fa-42010a800002 10Gi RWO 20m
data-mysql-2 Bound pvc-8ad69a6d-b103-11e6-93fa-42010a800002 10Gi RWO 20m
data-mysql-3 Bound pvc-50043c45-b1c5-11e6-93fa-42010a800002 10Gi RWO 2m
data-mysql-4 Bound pvc-500a9957-b1c5-11e6-93fa-42010a800002 10Gi RWO 2m
余分なPVCを再利用するつもりがないのであれば、削除することができます。
kubectl delete pvc data-mysql-3
kubectl delete pvc data-mysql-4
クリーンアップ
SELECT @@server_id
ループを実行している端末でCtrl+Cを押すか、 別の端末から次のコマンドを実行して、ループをキャンセルします。kubectl delete pod mysql-client-loop --now
StatefulSetを削除します。これによってPodの終了も開始されます。
kubectl delete statefulset mysql
Podが消えたことを確認します。 Podが終了処理が完了するのには少し時間がかかるかもしれません。
kubectl get pods -l app=mysql
上記のコマンドから以下の出力が戻れば、Podが終了したことがわかります。
No resources found.
ConfigMap、Services、およびPersistentVolumeClaimを削除します。
kubectl delete configmap,service,pvc -l app=mysql
PersistentVolumeを手動でプロビジョニングした場合は、それらを手動で削除し、 また、下層にあるリソースも解放する必要があります。 動的プロビジョニング機能を使用した場合は、PersistentVolumeClaimを削除すれば、自動的にPersistentVolumeも削除されます。 一部の動的プロビジョナー(EBSやPDなど)は、PersistentVolumeを削除すると同時に下層にあるリソースも解放します。
次の項目
- その他のステートフルアプリケーションの例は、Helm Charts repositoryを見てください。