Kubernetes: Topology Awareなデプロイの検証

# 概要
Topology-Aware Volume Provisioningが、Kubernetes 1.12で追加されました。この機能により、ZoneやRegionを指定し、Podとそれに関連するPersistentVolume(PV)を指定したZoneやRegionにて作成し割り当てることができるようになります。この機能をつかうためには、KubernetesのNodeだけでなくストレージも対応している必要があります。
2019/3時点では以下のストレージが対応しています。

- AWS EBS
- Azure Disk
- GCE PD
- CSI(alpha:https://kubernetes-csi.github.io/docs/topology.html)

# Region, Zoneとは
Region, Zoneに馴染みの薄い人もいるかと思うので、Region, Zoneについて簡単に説明します。
Regionは、データセンターの地域のことです。例えば東京リージョン(Region)だと東京にあるデータセンター、大阪リージョンだと大阪にあるデータセンターということになります。Multi Region、つまり複数地域にわかれたデータセンターを構築・運用することで地震などの災害でもサービスを継続できるようにします。
Zoneは、Availability Zone(AZ)とも呼ばれ、データセンター内で起こる電源障害や空調、ラック障害などが発生した場合でもサービスが継続できるように設計された区画のことです。Zoneの切り方は、データセンターの施設設備にも依存するため、データセンターごとに様々です。たとえば、電源が2系統あり、それぞれ分電盤(ブレーカー)が分かれているデータセンターでは、Zoneを2つにわけ設計することもあります。

![スクリーンショット 2019-03-10 12.18.11.png](https://qiita-image-store.s3.amazonaws.com/0/229880/277d781f-7786-e833-ea26-3b3527005cf1.png)

このようにRegion, Zoneは災害などが発生しても永続的にサービスを継続するためのデータセンターの機器配置の枠組みになります。ただし、データセンターの機器だけがRegion, Zoneを考慮して構築されていても意味がありません。データセンターで動くサービス、ソフトウェアがRegion, Zoneを意識してデプロイし、クラスタリングされていないと意味がありません。
この検証では、複数Zoneを意識してデプロイする方法を検証します。

# 動作検証
Topology Awareなデプロイでは、StorageClass(SC)のallowedTopologies, Podのspec.affinity.nodeAffinity, spec.podAntiAffinityを使いZoneを意識したデプロイを実現します。本検証では、これらの値の動作検証を行います。

## 検証環境
本検証では、Topology-Aware VolumeをサポートしているGCE PDが利用できるGKE(1.12.5-gke.5)を使用します。

## GKE(Multi Zone)の準備
検証環境のGKEにてKubernetesクラスタをMulti Zone構成で構築します。
まず、[GKE](https://cloud.google.com/kubernetes-engine/)のコンソールを開き、Kubernetesクラスタを作成します。
作成では、`ロケーションタイプ`の`リージョン`を選択します。さらに、`ノードプール`のdefault-poolの設定で、各Zoneに何個のNodeを構築するかを指定します。
今回の検証で利用する`us-central1`のRegionは、3つのZoneで構成されているため、各Zoneに1個づつNodeを配置します。
以下にMulti Zone構成でKubernetesクラスタを構築の設定例を示します。

<img width="476" alt="スクリーンショット 2019-03-10 12.39.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/229880/5c319f3a-5c90-5df5-3817-6b8eb6010172.png">

上記の設定でKubernetesクラスタを構築します。
次に、CLIでアクセスし確認します。GKEを利用するためのCLIのセットアップについては、[GKEのクイックスタート](https://cloud.google.com/kubernetes-engine/docs/quickstart)を参考にすると良いでしょう。

```
$ gcloud config set compute/region us-central1
Updated property [compute/region].

$ gcloud container clusters list
NAME                LOCATION     MASTER_VERSION  MASTER_IP     MACHINE_TYPE   NODE_VERSION  NUM_NODES  STATUS
standard-cluster-1  us-central1  1.12.5-gke.5    35.232.92.87  n1-standard-1  1.12.5-gke.5  3          RUNNING

$ gcloud container clusters get-credentials　standard-cluster-1 --region us-central1

$ kubectl get node
NAME                                                STATUS   ROLES    AGE     VERSION
gke-standard-cluster-1-default-pool-9e01aaeb-h3xz   Ready    <none>   5h15m   v1.12.5-gke.5
gke-standard-cluster-1-default-pool-a2000bd8-tf05   Ready    <none>   5h15m   v1.12.5-gke.5
gke-standard-cluster-1-default-pool-b4867b14-d6lr   Ready    <none>   5h15m   v1.12.5-gke.5

$ kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/region}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}'
gke-standard-cluster-1-default-pool-9e01aaeb-h3xz	us-central1	us-central1-f
gke-standard-cluster-1-default-pool-a2000bd8-tf05	us-central1	us-central1-b
gke-standard-cluster-1-default-pool-b4867b14-d6lr	us-central1	us-central1-a
```

その結果、Region`us-central1`に、Zone`us-central1-a`, `us-central1-b`, `us-central1-f`にそれぞれ1個づつNodeが構築されているのがわかります。
以降、本Kubernetesクラスタを使い検証を行います。

## 検証1: Topology指定なし
まず、はじめにTopologyを指定しない場合の動作を確認します。
使用するSCのManifestを以下に示します。

```sc.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: topology-aware-standard
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
volumeBindingMode: WaitForFirstConsumer
```

`sc.yaml`をデプロイします。

```
$ kubectl create -f sc.yaml
storageclass.storage.k8s.io/topology-aware-standard created
```

次に、デプロイするnginxのManifest(`nginx.yaml`)を以下に示します。

```nginx.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  serviceName: nginx
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: topology-aware-standard
      resources:
        requests:
          storage: 100Mi
```

`nginx.yaml`をデプロイし、Pod,PVC(PersistentVolumeClaim),PV(PersistentVolume)およびPod, PVのZoneを確認します。

```
$ kubectl create -f nginx.yaml
statefulset.apps/nginx created

$ kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE    IP           NODE                                                NOMINATED NODE
nginx-0   1/1     Running   0          118m   10.40.2.26   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-1   1/1     Running   0          118m   10.40.2.27   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-2   1/1     Running   0          117m   10.40.2.28   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-3   1/1     Running   0          117m   10.40.0.10   gke-standard-cluster-1-default-pool-a2000bd8-tf05   <none>
nginx-4   1/1     Running   0          116m   10.40.1.11   gke-standard-cluster-1-default-pool-9e01aaeb-h3xz   <none>

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS              AGE
data-nginx-0   Bound    pvc-867683ec-3f17-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   118m
data-nginx-1   Bound    pvc-98b67561-3f17-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   118m
data-nginx-2   Bound    pvc-a67a911b-3f17-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   117m
data-nginx-3   Bound    pvc-af77e940-3f17-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   117m
data-nginx-4   Bound    pvc-c0bfa854-3f17-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   117m

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS              REASON   AGE
pvc-867683ec-3f17-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-0   topology-aware-standard            118m
pvc-98b67561-3f17-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-1   topology-aware-standard            118m
pvc-a67a911b-3f17-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-2   topology-aware-standard            117m
pvc-af77e940-3f17-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-3   topology-aware-standard            117m
pvc-c0bfa854-3f17-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-4   topology-aware-standard            117m

$ for i in `kubectl get pod -o wide| awk 'NR>1 {print $1 "," $7}'`; do p=`echo $i |awk -F "," '{print $1}'`; n=`echo $i |awk -F "," '{print $2}'`; z=`kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}' | grep $n`; echo $p $z; done
nginx-0 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-1 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-2 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-3 gke-standard-cluster-1-default-pool-a2000bd8-tf05 us-central1-b
nginx-4 gke-standard-cluster-1-default-pool-9e01aaeb-h3xz us-central1-f

$ kubectl get pv -o=jsonpath='{range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}'
data-nginx-0	us-central1-a
data-nginx-1	us-central1-a
data-nginx-2	us-central1-a
data-nginx-3	us-central1-b
data-nginx-4	us-central1-f
```

Zoneを指定していないため、5つのPod,PVともバラバラのZoneにデプロイされていいます。
[クリーンアップ](#クリーンアップ)に従い、デプロイしたリソースを削除します。

## 検証2: SCでZoneを制限する
次に、ZoneをSCで制限します。
allowedTopologiesを指定したSCのManifest(`sc2.yaml`)を以下に示します。

```sc2.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: topology-aware-standard
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
  - key: failure-domain.beta.kubernetes.io/zone
    values:
    - us-central1-a
    - us-central1-b
```

このSCでは、allowedTopologiesにて`us-central1-a`, `us-central1-b`のZoneのみPV作成を許可します。
`sc2.yaml`をデプロイします。

```
$ kubectl create -f sc2.yaml
storageclass.storage.k8s.io/topology-aware-standard created
```

検証1で利用した`nginx.yaml`をデプロイし、Pod,PVC,PVおよびPVのZoneを確認します。

```
$ kubectl apply -f nginx.yaml
statefulset.apps/nginx created

$ kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE    IP           NODE                                                NOMINATED NODE
nginx-0   1/1     Running   0          110s   10.40.2.29   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-1   1/1     Running   0          95s    10.40.2.30   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-2   1/1     Running   0          79s    10.40.0.11   gke-standard-cluster-1-default-pool-a2000bd8-tf05   <none>
nginx-3   1/1     Running   0          56s    10.40.2.31   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-4   1/1     Running   0          32s    10.40.2.32   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS              AGE
data-nginx-0   Bound    pvc-a775786d-3f28-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   117s
data-nginx-1   Bound    pvc-b0d80943-3f28-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   102s
data-nginx-2   Bound    pvc-ba118e49-3f28-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   86s
data-nginx-3   Bound    pvc-c7f4149d-3f28-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   63s
data-nginx-4   Bound    pvc-d5f866f1-3f28-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   39s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS              REASON   AGE
pvc-a775786d-3f28-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-0   topology-aware-standard            116s
pvc-b0d80943-3f28-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-1   topology-aware-standard            100s
pvc-ba118e49-3f28-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-2   topology-aware-standard            84s
pvc-c7f4149d-3f28-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-3   topology-aware-standard            62s
pvc-d5f866f1-3f28-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-4   topology-aware-standard            38s

$ for i in `kubectl get pod -o wide| awk 'NR>1 {print $1 "," $7}'`; do p=`echo $i |awk -F "," '{print $1}'`; n=`echo $i |awk -F "," '{print $2}'`; z=`kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}' | grep $n`; echo $p $z; done
nginx-0 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-1 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-2 gke-standard-cluster-1-default-pool-a2000bd8-tf05 us-central1-b
nginx-3 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-4 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a

$ kubectl get pv -o=jsonpath='{range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}'
data-nginx-0	us-central1-a
data-nginx-1	us-central1-a
data-nginx-2	us-central1-b
data-nginx-3	us-central1-a
data-nginx-4	us-central1-a
```

許可されたZone`us-central1-a`, `us-central1-b`にのみデプロイされ、許可されていない`us-central1-f`にはデプロイされていないことが確認できます。
[クリーンアップ](#クリーンアップ)に従い、デプロイしたリソースを削除します。

## 検証3: PodのnodeAffinityでZoneを指定する (SCのZone制限なし)
検証1のSC(Zone制限なし)を使用します。
次に、Podのデプロイ先としてaffinity.nodeAffinityを指定したManifest(`nginx2.yaml`)を作成します。

```nginx2.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  serviceName: nginx
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: failure-domain.beta.kubernetes.io/zone
                operator: In
                values:
                - us-central1-a
                - us-central1-f
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: topology-aware-standard
      resources:
        requests:
          storage: 100Mi
```

.spec.template.spec.affinity.nodeAffinityにて、Zone`us-central1-a`, `us-central1-f`のみにデプロイされるように指定します。
`nginx2.yaml`をデプロイし、Pod,PVC,PVおよびPVのZoneを確認します。

```
$ kubectl create -f sc.yaml
storageclass.storage.k8s.io/topology-aware-standard created

$ kubectl create -f nginx2.yaml
statefulset.apps/nginx created

$ kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE    IP           NODE                                                NOMINATED NODE
nginx-0   1/1     Running   0          2m4s   10.40.2.33   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-1   1/1     Running   0          100s   10.40.2.34   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-2   1/1     Running   0          85s    10.40.2.35   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-3   1/1     Running   0          70s    10.40.1.12   gke-standard-cluster-1-default-pool-9e01aaeb-h3xz   <none>
nginx-4   1/1     Running   0          46s    10.40.2.36   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS              AGE
data-nginx-0   Bound    pvc-a9e73085-3f29-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   2m11s
data-nginx-1   Bound    pvc-b80b3ae0-3f29-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   107s
data-nginx-2   Bound    pvc-c108b79d-3f29-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   92s
data-nginx-3   Bound    pvc-ca06f562-3f29-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   77s
data-nginx-4   Bound    pvc-d7e80407-3f29-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   53s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS              REASON   AGE
pvc-a9e73085-3f29-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-0   topology-aware-standard            2m12s
pvc-b80b3ae0-3f29-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-1   topology-aware-standard            108s
pvc-c108b79d-3f29-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-2   topology-aware-standard            92s
pvc-ca06f562-3f29-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-3   topology-aware-standard            77s
pvc-d7e80407-3f29-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-4   topology-aware-standard            54s

$ for i in `kubectl get pod -o wide| awk 'NR>1 {print $1 "," $7}'`; do p=`echo $i |awk -F "," '{print $1}'`; n=`echo $i |awk -F "," '{print $2}'`; z=`kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}' | grep $n`; echo $p $z; done
nginx-0 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-1 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-2 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-3 gke-standard-cluster-1-default-pool-9e01aaeb-h3xz us-central1-f
nginx-4 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a

$ kubectl get pv -o=jsonpath='{range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}'
data-nginx-0	us-central1-a
data-nginx-1	us-central1-a
data-nginx-2	us-central1-a
data-nginx-3	us-central1-f
data-nginx-4	us-central1-a
```

結果、nodeAffinityにて指定した`us-central1-a`と`us-central1-f`のみにPod, PVが配置されているのが分かります。
[クリーンアップ](#クリーンアップ)に従い、デプロイしたリソースを削除します。

## 検証4: PodのnodeAffinityでZoneを指定する (SCのZone制限あり)
この検証では、検証2で利用したZone`us-central1-a`, `us-central1-b`のみPV作成を許可するSCを利用します。
検証3で利用したZone`us-central1-a`, `us-central1-f`へ配置するPodをデプロイします。
Pod,PVC,PVおよびPVのZoneを確認します。

```
$ kubectl create -f sc2.yaml
storageclass.storage.k8s.io/topology-aware-standard created

$ kubectl create -f nginx2.yaml
statefulset.apps/nginx created

$ kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE    IP           NODE                                                NOMINATED NODE
nginx-0   1/1     Running   0          2m3s   10.40.2.37   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-1   1/1     Running   0          106s   10.40.2.38   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-2   1/1     Running   0          91s    10.40.2.39   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-3   1/1     Running   0          77s    10.40.2.40   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-4   1/1     Running   0          62s    10.40.2.41   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>

$ kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS              AGE
data-nginx-0   Bound    pvc-6dd36b77-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   2m6s
data-nginx-1   Bound    pvc-77f836e6-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   109s
data-nginx-2   Bound    pvc-80f9d4e3-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   94s
data-nginx-3   Bound    pvc-896fd3b8-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   80s
data-nginx-4   Bound    pvc-9266b045-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   65s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS              REASON   AGE
pvc-6dd36b77-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-0   topology-aware-standard            2m7s
pvc-77f836e6-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-1   topology-aware-standard            110s
pvc-80f9d4e3-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-2   topology-aware-standard            95s
pvc-896fd3b8-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-3   topology-aware-standard            80s
pvc-9266b045-3f2a-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-4   topology-aware-standard            65s

$ for i in `kubectl get pod -o wide| awk 'NR>1 {print $1 "," $7}'`; do p=`echo $i |awk -F "," '{print $1}'`; n=`echo $i |awk -F "," '{print $2}'`; z=`kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}' | grep $n`; echo $p $z; done
nginx-0 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-1 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-2 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-3 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-4 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a

$ kubectl get pv -o=jsonpath='{range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}'
data-nginx-0	us-central1-a
data-nginx-1	us-central1-a
data-nginx-2	us-central1-a
data-nginx-3	us-central1-a
data-nginx-4	us-central1-a
```

その結果、SCとPodの両方から許可されている`us-central1-a`のみにデプロイされているのが分かります。
[クリーンアップ](#クリーンアップ)に従い、デプロイしたリソースを削除します。

## 検証5: PodのnodeAffinityとpodAntiAffinityでZoneのPod数を制限する (SCのZone制限あり)
次に、podAntiAffinityの検証を行います。
検証2で利用したZone`us-central1-a`, `us-central1-b`のみPV作成を許可したSCを利用します。
本検証で利用するpodAntiAffinityを指定したnginxのManifest(`nginx3.yaml`)を以下に示します。

```nginx3.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  serviceName: nginx
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: failure-domain.beta.kubernetes.io/zone
                operator: In
                values:
                - us-central1-a
                - us-central1-b
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nginx
            topologyKey: failure-domain.beta.kubernetes.io/zone
      containers:
      - name: nginx
        image: nginx
        volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: topology-aware-standard
      resources:
        requests:
          storage: 100Mi
```

このManifest(`nginx3.yaml`)では、.spec.template.spec.affinity.nodeAffinityにて、Zone`us-central1-a`, `us-central1-b`のみにデプロイされるように指定します。
さらに、.spec.template.spec.affinity.podAntiAffinityの、topologykeyにて`failure-domain.beta.kubernetes.io/zone`を指定しています。この指定により同じZoneに複数Podが配置されない、つまり各Zoneに1つのPodが配置されるようになります。
`nginx3.yaml`をデプロイし、Pod,PVC,PVおよびPVのZoneを確認します。

```
$ kubectl create -f sc2.yaml
storageclass.storage.k8s.io/topology-aware-standard created

$ kubectl create -f nginx3.yaml
statefulset.apps/nginx created

$ kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE     IP           NODE                                                NOMINATED NODE
nginx-0   1/1     Running   0          2m16s   10.40.2.43   gke-standard-cluster-1-default-pool-b4867b14-d6lr   <none>
nginx-1   1/1     Running   0          113s    10.40.0.12   gke-standard-cluster-1-default-pool-a2000bd8-tf05   <none>
nginx-2   0/1     Pending   0          97s     <none>       <none>                                              <none>

$ kubectl get pvc
NAME           STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS              AGE
data-nginx-0   Bound     pvc-595b8703-3f2b-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   2m20s
data-nginx-1   Bound     pvc-66fbea3c-3f2b-11e9-88f3-42010a80006d   1Gi        RWO            topology-aware-standard   117s
data-nginx-2   Pending                                                                        topology-aware-standard   101s

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                  STORAGECLASS              REASON   AGE
pvc-595b8703-3f2b-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-0   topology-aware-standard            2m24s
pvc-66fbea3c-3f2b-11e9-88f3-42010a80006d   1Gi        RWO            Delete           Bound    default/data-nginx-1   topology-aware-standard            2m1s

$ for i in `kubectl get pod -o wide| awk 'NR>1 {print $1 "," $7}'`; do p=`echo $i |awk -F "," '{print $1}'`; n=`echo $i |awk -F "," '{print $2}'`; z=`kubectl get node -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}' | grep $n`; echo $p $z; done
nginx-0 gke-standard-cluster-1-default-pool-b4867b14-d6lr us-central1-a
nginx-1 gke-standard-cluster-1-default-pool-a2000bd8-tf05 us-central1-b
nginx-2

$ kubectl get pv -o=jsonpath='{range .items[*]}{.spec.claimRef.name}{"\t"}{.metadata.labels.failure\-domain\.beta\.kubernetes\.io/zone}{"\n"}{end}'
data-nginx-0	us-central1-a
data-nginx-1	us-central1-b
```

結果、`nginx-0`, `nginx-1`とそれに接続されたPVC, PVがそれぞれZone`us-central1-a`, `us-central1-b`にデプロイされています。その後、`nginx-2`をデプロイしようとしているが、`Pending`となっています。
kubectl describeコマンドで詳細を確認します。

```
$ kubectl describe pod nginx-2
...
Events:
  Type     Reason             Age                  From                Message
  ----     ------             ----                 ----                -------
  Warning  FailedScheduling   4m4s (x2 over 4m4s)  default-scheduler   0/3 nodes are available: 1 node(s) didn't match node selector, 2 node(s) didn't match pod affinity/anti-affinity, 2 node(s) didn't satisfy existing pods anti-affinity rules.
  Normal   NotTriggerScaleUp  1s (x25 over 4m2s)   cluster-autoscaler  pod didn't trigger scale-up (it wouldn't fit if a new node is added):
```

その結果、`nginx-2`を配置しようとした際、各ZoneにはnginxのPodが既に配置されており、podAntiAffinityでZoneに1個づつ配置と指定したため、`Pending`となっています。
つまり、podAntiAffinityが正しく動作していることが分かります。
[クリーンアップ](#クリーンアップ)に従い、デプロイしたリソースを削除します。

## クリーンアップ
StatefulSetを削除する。StatefulSetを削除すると、これに関連するPodも削除されます。

```
$ kubectl delete sts/nginx
```

次に、PVCを削除します。PVCを削除すると、これに関連するPVも削除されます。

```
$ kubectl delete pvc --all
persistentvolumeclaim "data-nginx-0" deleted
persistentvolumeclaim "data-nginx-1" deleted
persistentvolumeclaim "data-nginx-2" deleted
persistentvolumeclaim "data-nginx-3" deleted
persistentvolumeclaim "data-nginx-4" deleted
```

最後に、SCを削除します。

```
$ kubectl delete sc topology-aware-standard
```

# 感想
　今回の検証では、Topology Awareなデプロイとして、Zoneを意識したデプロイを検証しました。コンテナ・Kubernetesによりアプリケーションの構築・運用が劇的に変化しました。さらには、KubernetesのセルフヒーリングによりPodに障害が発生しても自動復旧できるようになりました。しかし、Kubernetesを構築するサーバが搭載されているラックや電源が単一Zoneにのみ存在しているとラック障害や電源障害などが発生した場合、全てのKubernetesのNodeがダウンしてしまうため自動復旧できません。これらラック障害や電源障害でも落ちないサービスを作るためには、ZoneやRegionを考慮したKubernetesクラスタの構築と、それを意識したアプリケーションのデプロイが必要不可欠になります。特にデータを保存するストレージに関しては、格納されたデータを作り直すことは困難なため、デプロイするZoneやRegionを意識することが重要です。たとえば、RDBの場合、Masterのデータを複製するSlaveのデプロイ先を異なるZoneやRegionにデプロイするようにManifestの指定で実現します。
　KubernetesがTopology Awareなデプロイをサポートしたことで、コンテナ化されたアプリケーションの耐障害性をレベルアップさせた運用ができるようになりました。まだまだ対応しているストレージが少ない状況ですが、今後対応するストレージ、クラウドサービスが増えることを期待します。

# 参考情報

- [Topology-Aware Volume Provisioning in Kubernetes](https://kubernetes.io/blog/2018/10/11/topology-aware-volume-provisioning-in-kubernetes/)
- [Dynamic Volume Provisioning/Topology Awareness](https://kubernetes.io/docs/concepts/storage/dynamic-provisioning/#topology-awareness)
- [StorageClass/Allowed Topologies](https://kubernetes.io/docs/concepts/storage/storage-classes/#allowed-topologies)