Kubernetes のストレージについて調べる

作業メモ。

[Kubernetes完全ガイド impress top gearシリーズ](https://www.amazon.co.jp/dp/B07HFS7TDT/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)を読みながら手元で確認した時のメモ。

# 環境

```bash
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.3", GitCommit:"2bba0127d85d5a46ab4b778548be28623b32d0b0", GitTreeState:"clean", BuildDate:"2018-05-28T20:03:09Z", GoVersion:"go1.9.3", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"11+", GitVersion:"v1.11.5-eks-6bad6d", GitCommit:"6bad6d9c768dc0864dab48a11653aa53b5a47043", GitTreeState:"clean", BuildDate:"2018-12-06T23:13:14Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}
```

Kubernetes 環境としては EKS を利用。

# Volume/PersistentVolume(PV)/PersistentVolumeClaim(PVC)の違い

## Volume

Volume は予め用意されたボリュームをマニュフェストに直接指定することで利用可能となる。そのため、Kubernetes 上で新規にボリュームを作成したり、既存のボリュームを削除することは出来ない。例えば Node 上の任意のパスをマウントすること出来たりする。

## PersistentVolume(PV)

PersistentVolume(以降 PV) は永続化領域として確保される Volume。
PV　は個別にリソースを作成して利用する必要があり、後述する PersistentVolumeClaim(以降 PVC)経由で利用する。

## PersistentVolumeClaim(PVC)

Claim(要求する)という意味の通り、作成された PV リソースからアサインするためのリソース。PV はクラスタにボリュームを登録するだけなので Pod から利用する場合には PVC を定義する必要がある。
また、Dynamic プロビジョニングを行うことで事前に PV を作成せず、PVC が作成された際に動的に PV を作成する事ができる。

以降ではそれぞれを試してみる。

# Volume

[Volumes](https://kubernetes.io/docs/concepts/storage/volumes/)

Kubernetes では Volume を抽象化して Pod と疎結合なりソースとして定義している。様々な種類が提供されており、クラウドサービスのボリュームもサポートされている。

[Types of Volumes](https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes)

今回は Pod 用の一時的なディスクとして利用可能な emptyDir とホスト(ノード）の任意のパスをマウントできる hostPath を使ってみる。

## emptyDir

[emptyDir](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir)

emptyDir は Pod 用の一時的なディスク領域として利用可能であり、Pod が terminate されると削除される。

以下のマニュフェストで試す。

```emptydir.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}
```

```bash
$kubectl apply -f emptydir.yaml
pod "test-pd" created

# エラー
$kubectl exec -it test-pd -- /bin/sh
rpc error: code = 2 desc = oci runtime error: exec failed: container_linux.go:262: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory"

command terminated with exit code 126

```

Scratch から作っているので /bin/sh もないみたい。

[ kubernetes/test/images/test-webserver/Dockerfile
](https://github.com/kubernetes/kubernetes/blob/master/test/images/test-webserver/Dockerfile)

image を nginx にしてから再度 apply.

```bash
# /cache がマウントされている
$kubectl exec -it test-pd -- df -h
Filesystem      Size  Used Avail Use% Mounted on
overlay          20G  3.4G   17G  17% /
tmpfs           1.9G     0  1.9G   0% /dev
tmpfs           1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/nvme0n1p1   20G  3.4G   17G  17% /cache
shm              64M     0   64M   0% /dev/shm
tmpfs           1.9G   12K  1.9G   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs           1.9G     0  1.9G   0% /sys/firmware

# テストファイルを作成
$ kubectl exec -it test-pd -- touch /cache/test.txt

# ファイルが存在する
$kubectl exec -it test-pd -- ls /cache/
test.txt

# Pod を削除
$kubectl delete -f emptydir.yaml
pod "test-pd" deleted

# 再作成
$kubectl apply -f emptydir.yaml
pod "test-pd" created

# ファイルはない
$kubectl exec -it test-pd -- ls /cache/
```

## hostPath

[hostPath](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath)

hostPath を利用することで Kubernetest ノード(ホスト）の任意の領域をマウントできる。
以下では `/tmp` をマウントしてみる

```hostpath.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: nginx
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /tmp
      # this field is optional
      type: Directory
```

```bash
$ kubectl apply -f hostpath.yaml
pod "test-pd" created

# /test-pd にマウント
$kubectl exec -it test-pd -- df -h
Filesystem      Size  Used Avail Use% Mounted on
overlay          20G  3.4G   17G  17% /
tmpfs           1.9G     0  1.9G   0% /dev
tmpfs           1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/nvme0n1p1   20G  3.4G   17G  17% /test-pd
shm              64M     0   64M   0% /dev/shm
tmpfs           1.9G   12K  1.9G   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs           1.9G     0  1.9G   0% /sys/firmware

# ファイルを作成
$kubectl exec -it test-pd -- touch /test-pd/test.txt

# ファイルが存在
$kubectl exec -it test-pd -- ls /test-pd/
systemd-private-857b4e69b79c4039a8d0e50db7fa36bd-chronyd.service-zdQfQx
test.txt

# Pod が動作している Node を確認
$kubectl describe pod test-pd |grep Node
Node:               ip-172-31-0-56.ap-northeast-1.compute.internal/172.31.0.56
Node-Selectors:  <none>

# Pod を削除
$ kubectl delete pod test-pd
pod "test-pd" deleted

# Node にSSH ログインして /tmp を確認するとファイルが確認できる。Pod が削除されてもファイルは消えない
$ls /tmp/
systemd-private-857b4e69b79c4039a8d0e50db7fa36bd-chronyd.service-zdQfQx
test.txt
```

上記のように Pod が削除されても Node のファイルは削除されない。
ただし、Node が複数存在する場合、再度 Pod を起動した場合に同じ Node 上で動作するかは分からない。

# PersistentVolume(PV)

[Persistent Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)

PersistentVolume(PV) は永続化領域として確保される Volume。
PV　は個別にリソースを作成して利用する必要があり、後述する PersistentVolumeClaim(PVC)経由で利用する。

今回は PV を個別に作成する。
自分の実行環境が AWS なので以下を見て試す。

[Persistent Storage Using AWS Elastic Block Store](https://docs.openshift.com/enterprise/3.1/install_config/persistent_storage/persistent_storage_aws.html)


```bash
# 予め EBS を作成
$aws ec2 create-volume --size=10 --volume-type=gp2 --availability-zone ap-northeast-1c
{
    "AvailabilityZone": "ap-northeast-1c",
    "CreateTime": "2019-01-01T21:59:55.000Z",
    "Encrypted": false,
    "Size": 5,
    "SnapshotId": "",
    "State": "creating",
    "VolumeId": "vol-0af3eacd9228e23a0",
    "Iops": 100,
    "Tags": [],
    "VolumeType": "gp2"
}
```

上記で作成した volume-id を利用してマニュフェストファイルを作成する。

```pv.yaml
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: "pv0001"
spec:
  capacity:
    storage: "5Gi"
  accessModes:
    - "ReadWriteOnce"
  awsElasticBlockStore:
    fsType: "ext4"
    volumeID: "vol-0af3eacd9228e23a0"
```

上記を apply して作成する。


```bash
# pv を作成
$ kubectl apply -f pv.yaml
persistentvolume "pv0001" created

# pv リソースが存在する
$kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
pv0001    10Gi       RWO            Retain           Available                                      6s                                     14s
```

なお、上記場合、pv リソースを削除しても EBS は削除されないので別途削除が必要。

```bash
# pv を削除
$kubectl delete pv pv0001
persistentvolume "pv0001" deleted

# pv は削除済み
$ kubectl get pv
No resources found.

# EBS は削除されない 
$aws ec2 describe-volumes --volume-ids vol-0af3eacd9228e23a0
{
    "Volumes": [
        {
            "Attachments": [],
            "AvailabilityZone": "ap-northeast-1c",
            "CreateTime": "2019-01-01T21:59:55.943Z",
            "Encrypted": false,
            "Size": 10,
            "SnapshotId": "",
            "State": "available",
            "VolumeId": "vol-0af3eacd9228e23a0",
            "Iops": 100,
            "VolumeType": "gp2"
        }
    ]
}

# 必要に応じて削除する
$aws ec2 delete-volume --volume-id vol-0af3eacd9228e23a0

# 削除確認
$ aws ec2 describe-volumes --volume-ids vol-0af3eacd9228e23a0

An error occurred (InvalidVolume.NotFound) when calling the DescribeVolumes operation: The volume 'vol-0af3eacd9228e23a0' does not exist.
```

# PersistentVolumeClaim(PVC)

[PersistentVolumeClaims](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims)

PVC は PV の要求を行うリソース。
前途の PV は PVC 経由でマニュフェストから要求され、利用する。
PVC は要求された内容(容量、ラベル)から適切な PV を探し、Volume を割り当てる。

プロビジョニングの方法として2種類あり、前途のように予め PV を作成する方法を **Static**、PVC が作成(つまり Volume の要求があった）際に動的に PV を作成する **Dynamic** の2種類がある。

Dynamic を利用することで事前に PV や必要なストレージ(EBSなど）を作る手間を省くことが出来る。
今回は上記 Dynamic のプロビジョニングを試す。

## StorageClass を作成する

[Storage Classes](https://kubernetes.io/docs/concepts/storage/storage-classes/)

PVC をマニュフェストに記載する際に StorageClass を指定する。
StorageClass　ではどの Provider(GCEPersistentDisk なのか AWSElasticBlockStore なのかなど）であるかなど利用の際の設定を記載する。

EKS では **gp2** という StorageClass が予め作成されている。

[ストレージクラス](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/storage-classes.html)

```bash
$kubectl describe storageclass gp2
Name:            gp2
IsDefaultClass:  Yes
Annotations:     kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"},"name":"gp2","namespace":""},"parameters":{"fsType":"ext4","type":"gp2"},"provisioner":"kubernetes.io/aws-ebs"}
,storageclass.kubernetes.io/is-default-class=true
Provisioner:           kubernetes.io/aws-ebs
Parameters:            fsType=ext4,type=gp2
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Delete
VolumeBindingMode:     Immediate
Events:                <none>
```

別途 StorageClass の作成なども可能だが今回は上記を利用する。

## PVC を作成する

PVC を作成してみる。

```pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi
  storageClassName: gp2
```

```bash
# pvc を作成
$kubectl apply -f pvc.yaml
persistentvolumeclaim "myclaim" created

# pvc を確認
$kubectl get pvc
NAME      STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
myclaim   Bound     pvc-9a73a51a-0ed3-11e9-b090-0ae6cc179478   8Gi        RWO            gp2            1m

# 少し時間が経つと pv も作成される
$kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM             STORAGECLASS   REASON    AGE
pvc-9a73a51a-0ed3-11e9-b090-0ae6cc179478   8Gi        RWO            Delete           Bound     default/myclaim   gp2                      1m

# describe すると詳細が分かる
$kubectl describe pv pvc-9a73a51a-0ed3-11e9-b090-0ae6cc179478
Name:            pvc-9a73a51a-0ed3-11e9-b090-0ae6cc179478
Labels:          failure-domain.beta.kubernetes.io/region=ap-northeast-1
                 failure-domain.beta.kubernetes.io/zone=ap-northeast-1c
Annotations:     kubernetes.io/createdby=aws-ebs-dynamic-provisioner
                 pv.kubernetes.io/bound-by-controller=yes
                 pv.kubernetes.io/provisioned-by=kubernetes.io/aws-ebs
Finalizers:      [kubernetes.io/pv-protection]
StorageClass:    gp2
Status:          Bound
Claim:           default/myclaim
Reclaim Policy:  Delete
Access Modes:    RWO
Capacity:        8Gi
Node Affinity:   <none>
Message:
Source:
    Type:       AWSElasticBlockStore (a Persistent Disk resource in AWS)
    VolumeID:   aws://ap-northeast-1c/vol-05129125c1e7e8696
    FSType:     ext4
    Partition:  0
    ReadOnly:   false
Events:         <none>

# volume id から EBS を確認.Tag に Kubernetes の情報が記載されている
$ aws ec2 describe-volumes --volume-ids vol-05129125c1e7e8696
{
    "Volumes": [
        {
            "Attachments": [],
            "AvailabilityZone": "ap-northeast-1c",
            "CreateTime": "2019-01-02T21:16:15.189Z",
            "Encrypted": false,
            "Size": 8,
            "SnapshotId": "",
            "State": "available",
            "VolumeId": "vol-05129125c1e7e8696",
            "Iops": 100,
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "kubernetes-dynamic-pvc-9a73a51a-0ed3-11e9-b090-0ae6cc179478"
                },
                {
                    "Key": "kubernetes.io/created-for/pvc/name",
                    "Value": "myclaim"
                },
                {
                    "Key": "kubernetes.io/created-for/pv/name",
                    "Value": "pvc-9a73a51a-0ed3-11e9-b090-0ae6cc179478"
                },
                {
                    "Key": "kubernetes.io/created-for/pvc/namespace",
                    "Value": "default"
                },
                {
                    "Key": "kubernetes.io/cluster/eks-first-cluster",
                    "Value": "owned"
                }
            ],
            "VolumeType": "gp2"
        }
    ]
}
```

## Pod から利用する

[Claims As Volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#claims-as-volumes)

Pod から PVC つまりボリュームを利用する場合、claimName を指定する。

```
kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: myfrontend
      image: nginx
      volumeMounts:
      - mountPath: "/var/www/html"
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim
```

```bash
# pod を作成
$ kubectl apply -f pod-pvc.yaml
pod "mypod" created

# Pod では /var/www/html がマウントされており、pvc で要求した 8G
$kubectl exec -it mypod -- df -h
Filesystem      Size  Used Avail Use% Mounted on
overlay          20G  3.4G   17G  17% /
tmpfs           1.9G     0  1.9G   0% /dev
tmpfs           1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/nvme0n1p1   20G  3.4G   17G  17% /etc/hosts
shm              64M     0   64M   0% /dev/shm
/dev/nvme1n1    7.8G   36M  7.7G   1% /var/www/html
tmpfs           1.9G   12K  1.9G   1% /run/secrets/kubernetes.io/serviceaccount
tmpfs           1.9G     0  1.9G   0% /sys/firmware

# pod が動いている node を確認
$ kubectl describe pod mypod |grep Node
Node:               ip-172-31-0-56.ap-northeast-1.compute.internal/172.31.0.56
Node-Selectors:  <none>

# 該当する EC2 の BlockDeviceMappings を確認.pvc より作成された volume vol-05129125c1e7e8696　が EC2 にアタッチされている
$aws ec2 describe-instances --filters "Name=network-interface.addresses.private-ip-address,Values=172.31.0.56"
(略)
                   "Architecture": "x86_64",
                    "BlockDeviceMappings": [
                        {
                            "DeviceName": "/dev/xvda",
                            "Ebs": {
                                "AttachTime": "2018-12-20T02:33:55.000Z",
                                "DeleteOnTermination": true,
                                "Status": "attached",
                                "VolumeId": "vol-0347b33bce60e7776"
                            }
                        },
                        {
                            "DeviceName": "/dev/xvdbb",
                            "Ebs": {
                                "AttachTime": "2019-01-02T21:27:04.000Z",
                                "DeleteOnTermination": false,
                                "Status": "attached",
                                "VolumeId": "vol-05129125c1e7e8696"
                            }
                        }
                    ],

```

# リソースを削除する

検証が終わったので削除しつつ、挙動を確認する。

```bash
# Pod を削除
$kubectl delete pod mypod
pod "mypod" deleted

# EBS が EC2 からデタッチされている
$ aws ec2 describe-instances --filters "Name=network-interface.addresses.private-ip-address,Values=172.31.0.56"
(略)
                    "BlockDeviceMappings": [
                        {
                            "DeviceName": "/dev/xvda",
                            "Ebs": {
                                "AttachTime": "2018-12-20T02:33:55.000Z",
                                "DeleteOnTermination": true,
                                "Status": "attached",
                                "VolumeId": "vol-0347b33bce60e7776"
                            }
                        }
                    ],

# EC2 からでタッチされているが EBS は存在する
$aws ec2 describe-volumes --volume-ids vol-05129125c1e7e8696 |grep State
            "State": "available",

# pvc を削除
$kubectl delete pvc myclaim
persistentvolumeclaim "myclaim" deleted

# pv も削除される
$ kubectl get pv
No resources found.

# EBS も削除される
$aws ec2 describe-volumes --volume-ids vol-05129125c1e7e8696

An error occurred (InvalidVolume.NotFound) when calling the DescribeVolumes operation: The volume 'vol-05129125c1e7e8696' does not exist.
```

上記のように PVC が削除された際に EBS も削除される挙動は StorageClass の ReclaimPolicy が **Delete** であるため。
ReclaimPoliyc を **Retain** にすれば EBS は削除されない。