Statelessがその場限りの情報だけで動くアプリケーションであるのと対照的に、Statefulアプリケーションはショッピングカートなど画面遷移などの経過情報を保持するタイプのアプリケーションです。

StatefulSet Basics

k8s公式サイト StatefulSet Basics を別のウィンドウで開き見比べながら進めてください。

英語の原文は平易に書かれていますので、そちらを中心に進めて必要に応じてこのページに戻ってください。

ここでは公式チュートリアルObjectivesにも書かれているとおり、Deploymentではなくこれまで利用してこなかったStatefulSetを使用します。

Creating a StatefulSet (web.yaml)

このチュートリアルでは、YAMLファイルを自分で環境に合わせて反映させる必要があります。

公式ガイドの指示では、次のように指示されています。

  1. web.yamlをダウンロードし保存してください

  2. kubectl -n $(id -un) apply -f web.yamlで反映する (kubectlのコマンドラインはSCCP用に-n $(id -un)を加えています)

ゼミ室10の環境では、volumeClaimTemplates:の記述に問題があるためこのまま実行することはできません。

PV&PVCを参考に、ゼミ室の環境に合せて、設定を変更します。 今回は"ReadWriteOnce"が指定されているためBlockStorageを使用します。

## 前半部分を省略 ##
## Rook/Cephを利用する場合、最下行に"storageClassName: rook-ceph-block"を指定するおと
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
      storageClassName: rook-ceph-block

編集済みのweb.yamlファイルは次のリンクからダウンロードすることもできます。

編集したweb.yamlファイルの反映

公式サイトの指示では、あらかじめ下記のコマンドを実行するよう指示されています。 (SCCP用に-n $(id -un)を追加)

$ kubectl -n $(id -un) get pods -w -l app=nginx

これにより、Podが追加されたり削除されるとその変化の様子が画面に出力されます。

別の端末ウィンドウを開いて、次のコマンドを実行し、変更したweb.yamlファイルの内容を読み込ませます。

$ kubectl -n $(id -un) apply -f web.yaml
service/nginx created
statefulset.apps/web configured

先ほど$ kubectl -n $(id -un) get pods -w -l app=nginxを実行しておいた端末では、新しくPodが作成される課程が表示されているはずです。

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   0/1     ContainerCreating   0          10s
web-0   1/1     Running             0          10s
web-1   0/1     Pending             0          0s
web-1   0/1     Pending             0          0s
web-1   0/1     ContainerCreating   0          0s
web-1   0/1     ContainerCreating   0          5s
web-1   1/1     Running             0          6s

次のような方法でも、設定の状況を確認することができます。

$ kubectl -n $(id -un) get all
NAME        READY   STATUS    RESTARTS   AGE
pod/web-0   1/1     Running   0          2m3s
pod/web-1   1/1     Running   0          113s

NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/nginx   ClusterIP   None         <none>        80/TCP    2m11s

NAME                   READY   AGE
statefulset.apps/web   2/2     2m12s
## allではPVCの情報が表示されないため、個別にPVCの状況を確認する
$ kubectl -n $(id -un) get pvc
NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
www-web-0   Bound    pvc-2a45eb6c-fc4b-4ac3-a28c-5ee7d3eeb328   1Gi        RWO            rook-ceph-block   26m
www-web-1   Bound    pvc-5edf7ade-2b19-4f78-a717-4f259540e13c   1Gi        RWO            rook-ceph-block   26m

公式チュートリアルでは、StatefulSetについての説明が続きますが、kubectlコマンドを実行している部分は、kubectl -n $(id -un)で置き換えれば、問題なく実行できます。$ alias kubectl="kubectl -n $(id -un)" のようなエイリアスを設定しても構いません。

StatefulSet Basics の解説

このチュートリアルは、Webブラウザからアクセスすることを目的とはしていません。

kubectl -n $(id -un) run コマンドなどを利用して、kubectlコマンドからホスト名を確認したり、スケール・アップする方法を解説することが目的だからです。

内部DNSによる名前解決

Using stable network identitiesのセクションでは、2つのPodのホスト名を確認しています。

$ for i in 0 1; do kubectl -n $(id -un) exec "web-$i" -- sh -c 'uname -n'; done

またdns-test Podを一時的に起動することで、kuberentes内部でホスト名がどのように設定されるのか確認できるように説明されています。

$ kubectl -n $(id -un) run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
## "/ #"はプロンプト
/ # nslookup web-0.nginx

nslookup web-0.nginx を実行した結果は次のようになります。

Server:    169.254.25.10
Address 1: 169.254.25.10

Name:      web-0.nginx
Address 1: 10.233.105.219 web-0.nginx.yasu-abe.svc.cluster.local

最後に表示されている 10.233.105.219 はIPアドレスを示しますが、Kubernetes内部ではIPアドレスは頻繁に変更される可能性があるため、ホスト名を使って互いに通信を行うようにします。

FQDN(Fully Qualified Domain Name, 完全修飾名)は、web-0.nginx.yasu-abe.svc.cluster.local です。

PodのFQDNは、<Pod>.<Service>.<Namespace>.svc.cluster.local という命名規則を持っています。

web-0web-0.nginx あるいは、web-0.nginx.yasu-abe でもアクセスが可能です。yasu-abeはnamespaceが入りますので実行するユーザーに応じて変化します。

nslookupコマンドを利用して、FQDNの部分的な名前でも認識される事を確認しましょう。

/ # nslookup web-0
/ # nslookup web-0.nginx.yasu-abe
/ # nslookup web-0.nginx.yasu-abe.svc

## エラーになる例
/ # nslookup web-0.nginx.yasu-abe.svc.cluster

エラーになるか否かは設定次第ですが、最後のcluster.localでは、これを分割する用途は想定できないため設定されていないのだと思われます。

名前解決についてはPod内部から /etc/resolv.conf がどのように設定されているか確認してください。

/ # cat /etc/resolv.conf
search yasu-abe.svc.cluster.local svc.cluster.local cluster.local
nameserver 169.254.25.10
options ndots:5

これによって自身のnamespace上にあるPodを優先し、他のnamespaceが指定されている場合にはそのIPアドレスを検索するように設定されています。

また nginx とService名を検索した場合にはDNSラウンドロビンによる名前解決が行われます。

/ # nslookup nginx
/ # nslookup nginx
## 1回目
Server:    169.254.25.10
Address 1: 169.254.25.10

Name:      nginx.yasu-abe
Address 1: 10.233.105.219 web-0.nginx.yasu-abe.svc.cluster.local
Address 2: 10.233.115.79 web-1.nginx.yasu-abe.svc.cluster.local

## 2回目
Server:    169.254.25.10
Address 1: 169.254.25.10

Name:      nginx.yasu-abe
Address 1: 10.233.115.79 web-1.nginx.yasu-abe.svc.cluster.local
Address 2: 10.233.105.219 web-0.nginx.yasu-abe.svc.cluster.local

タイミングによって綺麗にAddress 1とAddress 2が入れ替わらないかもしれませんが、何回か試すと異なる順番でIPアドレスに返答することが分かるはずです。

web-0とweb-1のWebサーバーにWebブラウザからアクセスする方法

このチュートリアルではKubernetesクラスターの外から、例えばWebブラウザからのアクセス等は想定していません。

それでもWebブラウザからアクセスしたい場合にはどのようにすれば良いでしょうか。

このチュートリアルでは、web.yamlファイルに設定されているService定義は、ClusterIPを割り当てないHeadlessと呼ばれる設定になっています。現状では、Webブラウザ等からWebサーバー(app:nginx)にアクセスする方法がありません。

このチュートリアルでは次のようにホスト名を表示するindex.htmlを作成し、各Pod内部からcurlコマンドを利用してその内容にアクセスしています。

$ for i in 0 1; do kubectl -n $(id -un) exec "web-$i" -- sh -c 'echo "$(hostname)" > /usr/share/nginx/html/index.html'; done
$ for i in 0 1; do kubectl -n $(id -un) exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1

Webブラウザからアクセスするためには追加のServiceオブジェクトが必要です。

次の内容のYAMLファイル(例えば"tutorial-stateful.svc-nginx-ext.yaml")を準備して反映($ kubectl -n $(id -un) apply -f <filename>)させます。

apiVersion: v1
kind: Service
metadata:
  name: nginx-ext
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  selector:
    app: nginx
  type: LoadBalancer

このYAMLファイルは次のリンクからもダウンロードできます。

この反映が終ると、外部からWebサーバーに接続するためのIPアドレスが割り当てられます。

$ kubectl -n $(id -un) apply -f tutorial-stateful.svc-nginx-ext.yaml
$ kubectl -n $(id -un) get svc -l app=nginx
NAME        TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)        AGE
nginx       ClusterIP      None            <none>            80/TCP         2m56s
nginx-ext   LoadBalancer   10.233.39.104   192.168.100.xxx   80:31723/TCP   3m18s

この場合は、svc/nginx-ext がtype: LoadBalancer を指定したことで192.168.100.xxxが割り当てられているので、Webブラウザから、このIPを指定しコンテンツを表示させることができます。

$ browse http://192.168.100.xxx/

Pod数の増減について

Scaling a StatefulSet のセクションでは、Pod数を増減させる kubectl scale コマンドについて説明しています。

## Pod数を5に増加させるscaleコマンド
$ kubectl -n $(id -un) scale statefulsets/web --replicas=5

## Pod数が増加する様子を観察
$ kubectl -n $(id -un) get pod -w -l app=nginx

kubectl get pod -w コマンドを終了する場合には、C-c を入力します。

またチュートリアルの中ではScaling Downの中で、kubectl patch コマンドを実行する方法が紹介されています。

## kubectl scale statefulsets/web --replicas=3 と同じコマンド
$ kubectl -n $(id -un) patch statefulsets/web -p '{"spec":{"replicas":3}}'

またPodの数を増減させてもPVCは削除されません。

$ kubectl -n $(id -un) get pvc -l app=nginx
NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
www-web-0   Bound    pvc-f99b6813-007f-4e9c-b8b3-50337237bef9   1Gi        RWO            rook-ceph-block   20h
www-web-1   Bound    pvc-294097b3-cf32-4b3c-a7ce-790703a8ce2e   1Gi        RWO            rook-ceph-block   20h
www-web-2   Bound    pvc-258824e7-3bd6-4d9e-9173-0bdf75c35b95   1Gi        RWO            rook-ceph-block   83m
www-web-3   Bound    pvc-8a8811fa-07a8-49d4-b70d-4ebf5879f5e2   1Gi        RWO            rook-ceph-block   83m
www-web-4   Bound    pvc-8c7ad97f-166f-4de4-b30d-29ad0458b10c   1Gi        RWO            rook-ceph-block   82m

前のセクションで実行した$ for i in 0 1; do kubectl -n $(id -un) exec "web-$i" — sh -c 'echo "$(hostname)" > /usr/share/nginx/html/index.html'; doneでは最初の2つのPod(www-web-0, www-web-1)だけにホスト名が書かれたindex.htmlファイルが配置されています。

$ for i in 0 1 2; do kubectl -n $(id -un) exec -i -t "web-$i" -- curl http://localhost/; done

3つのPodが稼動している状況では次のような結果になります。

web-0
web-1
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.11.1</center>
</body>
</html>

DeploymentとStatefulSetの違い

今回利用した例ではweb.yamlにStatefulSetの定義が書かれているPodのname:や、volumeClaimTemplatesのname:ではwwwとしか指定されていませんが、replicas:に指定された数に応じて自動的に0..Nまでの番号が割り当てられています。

ポイントは、DeploymentSetではPodの名前はランダムで、何かしらのPVCが割り当てられますが、ホスト名とPVCとの対応はランダムになる点と比べて、StatefulSetではPodの名前とPVCの名前は1対1で対応することで、Podを削除しても、この関係は維持されます。

反面、StatefulSetは個別のPodが永続的なホスト名を持ち、Podと対応するPVCを割り当てられます。 Podを削除しても同じ名前のPodが再度作成され、以前と同じPVCが必ず割り当てられます。

データベースサーバーなどが、ホスト名を設定ファイルに書き込み、自身と他を区別する仕組みがある場合には、StatefulSetが有効です。

今回の例だけでは、この違いや使い分けを理解することは難しいかもしれませんが、Deploymentだけでは対応できない場面がある事は理解してください。

また、説明をよく読むと分かりますが、別ウィンドウを使い $ kubectl -n $(id -un) get pods -w -l app=nginxの出力を観察しながら操作し、Podが削除されても同じ名前で作成されたり、名前が連番になるように生成される様子を確認してください。

Example: Deploying WordPress and MySQL with Persistent Volumes

ここからは別のチュートリアルになります。

公式サイトのExample: Deploying WordPress and MySQL with Persistent Volumesについて説明します。

ここではkubectl -n $(id -un) apply -k .のようなコマンドを利用したいため、空の作業用のディレクトリを準備し、cdしてから作業を開始してください。

## 適当な作業用ディレクトリを作成する例
$ TD=$(date +%Y%m%d.%H%M).stateful-mysql-example
$ echo ${TD}
$ mkdir ${TD}
$ cd ${TD}

kustomization.yamlという名前ファイルを作成するために、catコマンドと、ヒアドキュメント(Here-Documents)を利用していますが、emacsなどでも良いので、次のような内容のファイルを、kustomization.yamlという名前で作成してください。

secretGenerator:
- name: mysql-pass
  literals:
  - password=secret
resources:
- mysql-deployment.yaml
- wordpress-deployment.yaml

パスワードを指定するsecretの部分は、変更することが可能です。

次にkustomization.yamlファイルを配置した場所にmysql-deployment.yamlとwordpress-deployment.yamlの2つのファイルを準備します。

$ curl -LO https://kubernetes.io/examples/application/wordpress/mysql-deployment.yaml
$ curl -LO https://kubernetes.io/examples/application/wordpress/wordpress-deployment.yaml

ここまで終えると、次のように3つのファイルだけを含むディレクトリで作業しているはずです。

$ ls -l
total 12
-rw-r--r-- 1 yasu-abe prof   68 Nov 29 07:58 kustomization.yaml
-rw-r--r-- 1 yasu-abe prof 1442 Nov 29 07:58 mysql-deployment.yaml
-rw-r--r-- 1 yasu-abe prof 1341 Nov 29 07:59 wordpress-deployment.yaml

まだ、このままでは、Apply and Verifyに進めないので、YAMLファイルを修正します。

YAMLファイルの修正

先ほどと同様にPersistentVolumeClaimはそのままでは動かないためRookに対応した修正を行ないます。

--- mysql-deployment.yaml       2019-10-09 10:22:39.434291231 +0000
+++ mysql-deployment-rook.yaml  2019-10-09 10:22:27.022010709 +0000
@@ -24,6 +24,7 @@
   resources:
     requests:
       storage: 20Gi
+  storageClassName: rook-ceph-block
 ---
 apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
 kind: Deployment

wordpress-deployment.yamlファイルにも同様の修正を行います。

--- wordpress-deployment.yaml   2019-10-09 10:26:32.099526478 +0000
+++ wordpress-deployment-rook.yaml      2019-10-09 10:27:01.144177437 +0000
@@ -24,6 +24,7 @@
   resources:
     requests:
       storage: 20Gi
+  storageClassName: rook-ceph-block
 ---
 apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
 kind: Deployment

修正済みのファイルは次のリンクからダウンロードできます。

3つのファイルがそろったら、k8s.io公式ガイド Apply and Verify に進みます。

$ kubectl -n $(id -un) apply -k ./
secret/mysql-pass-kd7m542fbt created
service/wordpress-mysql created
service/wordpress created
deployment.apps/wordpress-mysql created
deployment.apps/wordpress created
persistentvolumeclaim/mysql-pv-claim created
persistentvolumeclaim/wp-pv-claim created

これが成功すると、WebブラウザからWordpressに接続することができるようになります。 $ kubectl -n $(id -un) get svcの出力結果から、IPアドレスを確認します。

$ kubectl -n $(id -un) get svc wordpress
NAME                      TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)        AGE
service/wordpress         LoadBalancer   10.233.62.214   192.168.100.xxx   80:32336/TCP   9m59s

このように表示されたEXTERNAL-IPのアドレス(この場合は、192.168.100.xxx)にWebブラウザからアクセスしてください。

$ browse http://192.168.100.xxx

公式チュートリアルの中でminikubeコマンドを実行している部分は、このSCCPの環境では実行できないので実行しないでください。

またError establishing a database connectionのようなメッセージがWebブラウザに表示される場合があります。

この場合にはPodを再起動し、再度アクセスしてください。

$ kubectl -n $(id -un) delete pod -l app=wordpress,tier=frontend

Podが起動してからWebブラウザでアクセスし直してください。

Deploying WordPress and MySQL with Persistent Volumes の解説

公式のチュートリアルではあまり説明は多くありません。

まずこの例ではWordpressというWebアプリケーションとデータを保持するMySQLデータベースサーバーによって構成されています。

$ kubectl -n $(id -un) get pod,svc -l app=wordpress
NAME                                  READY   STATUS    RESTARTS   AGE
pod/wordpress-d6f9dbd9b-nwx7w         1/1     Running   0          54s
pod/wordpress-mysql-9c6c5b8c9-rqwrw   1/1     Running   0          54s

NAME                      TYPE           CLUSTER-IP      EXTERNAL-IP       PORT(S)        AGE
service/wordpress         LoadBalancer   10.233.46.124   192.168.100.172   80:30765/TCP   20h
service/wordpress-mysql   ClusterIP      None            <none>            3306/TCP       20h

WordpressからMySQLへの接続はホスト名wordpress-mysqlを通じて行われます。

これは wordpress-deployment.yaml の中で WORDPRESS_DB_HOST として定義されています。

また接続に利用するパスワード同じYAMLファイルで次のようにSecretオブジェクトを参照するよう設定されています。

        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-pass
              key: password

この書き方は実際には不十分な指定方法です。

deployment/wordpressオブジェクトの定義を確認しましょう。

## deploy/wordpress定義の全体を表示する方法
$ kubectl -n $(id -un) get deploy/wordpress -o yaml | jq .

## deploy/wordpress定義の一部を表示する方法
$ kubectl -n $(id -un) get deploy/wordpress -o jsonpath='{..spec.containers[0].env[?(@.name == "WORDPRESS_DB_PASSWORD")]}' | jq .

これは次のように一部を表示させた場合の出力です。

{
  "name": "WORDPRESS_DB_PASSWORD",
  "valueFrom": {
    "secretKeyRef": {
      "key": "password",
      "name": "mysql-pass-m4d885dchh"
    }
  }
}

この中で定義ファイルでは指定されていない -m4d885dchh が追加されています。

これは `kubectl apply コマンドに指定した -k フラグによって自動的に行われています。

別の方法としては手動でSecretオブジェクトを管理することもできますし、これまで指定してきた過去のパスワードの定義を保持したいなどの理由があればkustomization.yamlを利用することもできます。