[Kubernetes] ReleasedなPVは削除しないで使い回そう

# 概要
Status: ReleasedなPVを再クレームしたい時はPVを削除して再作成しようねという記事を見かけるが、「PVは使いまわせるよ」と言いたくて執筆しました。

# TL;DR
「PVCを削除したらPVがReleased Statusになった。同じPVCに再度Bindしたい」
という時は`PV.spec.claimRef.uid`を削除すると要件を満たせます。
PVのステータスが`Released -> Available`に遷移し、前にBindしていたPVC専用(予約済み)のPVとなります。
また、`.spec.claimRef`ごと削除すると未予約なAvailable PVになります。
# 動作検証

## ToDo
- storageClass,PV,PVC-1を作成してBoundさせる
- PVC-1を削除するとPVのステータスがReleasedになることを確認する
- PVのclaimRefを見てみる
- PVのclaimRefを編集する
- PVがAvailableに戻っていることを確認する <- 検証したいこと1
- 同じstorageClassを参照するPVC-2を作る
- (PVはPVC-1専用になっているため)PVC-2にはBindされないことを確認する <- 検証したいこと2
- PVC-1を作成すると、PVがBindされることを確認する <- 検証したいこと3

## storageClass,PV-1,PVC-1を作成してBoundさせる

<details><summary>storageClass manifest</summary>

```yaml
❯ cat sc.yaml 
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: example-sc
provisioner: kubernetes.io/no-provisioner 
reclaimPolicy: Retain
volumeBindingMode: Immediate
```
</details>
<details><summary>persistentVolumeClaim manifest</summary>

```yaml
❯ cat pvc.yaml 
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-pvc
spec:
  storageClassName: example-sc
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Mi
```
</details>

<details><summary>persistentVolume manifest</summary>

```yaml
❯ cat pv.yaml 
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  storageClassName: example-sc
  capacity:
    storage: 10Mi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  local:
    path: /mnt/localdisk
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - nwk01
```
</details>

```bash
❯ ls        
pv.yaml  pvc.yaml  sc.yaml

❯ k apply -f .
persistentvolume/example-pv created
persistentvolumeclaim/example-pvc created
storageclass.storage.k8s.io/example-sc created

❯ k get sc,pv,pvc
NAME                                     PROVISIONER                    RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
storageclass.storage.k8s.io/example-sc   kubernetes.io/no-provisioner   Retain          Immediate           false                  58s

NAME                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
persistentvolume/example-pv   10Mi       RWO            Retain           Bound    exam/example-pvc   example-sc              58s

NAME                                STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/example-pvc   Bound    example-pv   10Mi       RWO            example-sc     58s
```

## PVC-1を削除するとPVのステータスがReleasedになることを確認する

```bash
❯ k delete pvc example-pvc 
persistentvolumeclaim "example-pvc" deleted

❯ k get pv,pvc
NAME                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM              STORAGECLASS   REASON   AGE
persistentvolume/example-pv   10Mi       RWO            Retain           Released   exam/example-pvc   example-sc              3m8s
```

## PVのclaimRefを見てみる
```bash
apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
  ...
spec:
  ...
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: example-pvc
    namespace: exam
    resourceVersion: "32159631"
    uid: f6915120-f26f-4ab8-9bfc-f26d48c35391
```

PVを作成した時には存在しなかった`claimRef`という項目が生えています。
kubernetes(kube-controller-manager)はこの項目を閲覧・操作することでPVとPVCの紐付けを管理しています。

## PVのclaimRefを編集する
```bash
$ k edit pv example-pv
# .spec.claimRef.uid行を削除する
persistentvolume/example-pv edited
```
または
```bash
# json formatで取得しつつ、削除したいリソースパスを確認
❯ k get pv example-pv -ojson | jq ".spec.claimRef.uid" 
"f6915120-f26f-4ab8-9bfc-f26d48c35391"

❯ k patch pv example-pv --type json -p '[{"op": "remove", "path": "/spec/claimRef/uid"}]'
persistentvolume/example-pv patched
```

## PVがAvailableに戻っていることを確認する
```bash
❯ k get pv                 
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM              STORAGECLASS   REASON   AGE
example-pv   10Mi       RWO            Retain           Available   exam/example-pvc   example-sc              27m
```

Availableになったので再利用できそうです。
しかしClaimには`exam/example-pvc`の名前が残っていますね。

## 同じstorageClassを参照するPVC-2を作る
</details>
<details><summary>persistentVolumeClaim-2 manifest</summary>

```yaml
❯ cat pvc2.yaml 
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-pvc2
spec:
  storageClassName: example-sc
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Mi
```
</details>

```bash
❯ k apply -f pvc2.yaml 
persistentvolumeclaim/example-pvc2 created

❯ k get pvc,pv
NAME                                 STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/example-pvc2   Pending                                      example-sc     3m47s

NAME                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM              STORAGECLASS   REASON   AGE
persistentvolume/example-pv   10Mi       RWO            Retain           Available   exam/example-pvc   example-sc              33m
```

`example-pv`はAvailableであり、`example-pv`と`example-pvc2`は同じstorageClassを参照しているため、通常であればBindされます。
しかし、3分経ってもBindされません。`example-pv`は`example-pvc`に予約されているためです。

## PVC-1を作成すると、PVがBindされることを確認する

```bash
# 最初に作ったpvc.yamlをそのまま適用する
❯ k apply -f pvc.yaml 
persistentvolumeclaim/example-pvc created

❯ k get pv,pvc
NAME                          CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS   REASON   AGE
persistentvolume/example-pv   10Mi       RWO            Retain           Bound    exam/example-pvc   example-sc              39m

NAME                                 STATUS    VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/example-pvc    Bound     example-pv   10Mi       RWO            example-sc     6s
persistentvolumeclaim/example-pvc2   Pending                                          example-sc     10m
```

想定通り、`example-pvc2`を差し置いて`example-pvc`と`example-pv`がBindされました。検証完了！

--- 
ここから下はオマケ

# PV Controllerのコードを読んでみる

```bash
> git clone https://github.com/kubernetes/kubernetes --depth 1
# https://github.com/kubernetes/kubernetes/tree/2c5105e7b80f97ab2da0d47a6db6875aec38331c
```
## PV Controllerとは？
- kubernetesコアコンポーネントの1つに`kube-controller-manager`というものがある
- `kube-controller-manager`が複数のコントローラを実行し、クラスタ内のリソースをいい感じに制御している
- そのうちの1つにPV Controllerというものがあり、PV,PVCリソースを制御している

## つらつらと書く

```go
// cmd/kube-controller-manager/app/core.go#L35
func main() {
	command := app.NewControllerManagerCommand() // メイン処理が記載されている
	code := cli.Run(command)
	os.Exit(code)
}


// cmd/kube-controller-manager/app/controllermanager.go#L104
func NewControllerManagerCommand() *cobra.Command {
    ...
	cmd := &cobra.Command{
		RunE: func(cmd *cobra.Command, args []string) error {
            ...
  			c, err := s.Config(KnownControllers(), ControllersDisabledByDefault(), ControllerAliases()) // ここでコントローラ名一覧(40個以上)を取得する
            ...
			return Run(context.Background(), c.Complete()) // <-- メイン処理


// cmd/kube-controller-manager/app/controllermanager.go#L182
func Run(ctx context.Context, c *config.CompletedConfig) error {
    ...
	run := func(ctx context.Context, controllerDescriptors map[string]*ControllerDescriptor) {
        ...
        if err := StartControllers(ctx, controllerContext, controllerDescriptors, unsecuredMux, healthzHandler); err != nil { // main

            
// cmd/kube-controller-manager/app/controllermanager.go#L626
func StartControllers(ctx context.Context, controllerCtx ControllerContext, controllerDescriptors map[string]*ControllerDescriptor,
	unsecuredMux *mux.PathRecorderMux, healthzHandler *controllerhealthz.MutableHealthzHandler) error {
        ...
		check, err := StartController(ctx, controllerCtx, serviceAccountTokenControllerDescriptor, unsecuredMux) // main


// cmd/kube-controller-manager/app/controllermanager.go#L680
func StartController(ctx context.Context, controllerCtx ControllerContext, controllerDescriptor *ControllerDescriptor,
	unsecuredMux *mux.PathRecorderMux) (healthz.HealthChecker, error) {
    ...
	initFunc := controllerDescriptor.GetInitFunc() // ここで取得したinitFuncで各コントローラを実行している
	ctrl, started, err := initFunc(klog.NewContext(ctx, klog.LoggerWithName(logger, controllerName)), controllerCtx, controllerName)

// cmd/kube-controller-manager/app/core.go#L329
// newPersistentVolumeBinderControllerDescriptorというdescriptorを通じて、initFuncとして呼び出される
func startPersistentVolumeBinderController(ctx context.Context, controllerContext ControllerContext, controllerName string) (controller.Interface, bool, error) {
    // ここで取得したInformerを利用して各リソースの変更を監視・検知する
	params := persistentvolumecontroller.ControllerParameters{
		KubeClient:                controllerContext.ClientBuilder.ClientOrDie("persistent-volume-binder"),
		SyncPeriod:                controllerContext.ComponentConfig.PersistentVolumeBinderController.PVClaimBinderSyncPeriod.Duration,
		VolumePlugins:             plugins,
		Cloud:                     controllerContext.Cloud,
		ClusterName:               controllerContext.ComponentConfig.KubeCloudShared.ClusterName,
		VolumeInformer:            controllerContext.InformerFactory.Core().V1().PersistentVolumes(),
		ClaimInformer:             controllerContext.InformerFactory.Core().V1().PersistentVolumeClaims(),
		ClassInformer:             controllerContext.InformerFactory.Storage().V1().StorageClasses(),
		PodInformer:               controllerContext.InformerFactory.Core().V1().Pods(),
		NodeInformer:              controllerContext.InformerFactory.Core().V1().Nodes(),
		EnableDynamicProvisioning: controllerContext.ComponentConfig.PersistentVolumeBinderController.VolumeConfiguration.EnableDynamicProvisioning,
	}
	volumeController, volumeControllerErr := persistentvolumecontroller.NewController(ctx, params)
    ...
	go volumeController.Run(ctx)


↑ここまではkube-controller-managerの話
======
↓ここからPV controllerの話


// pkg/controller/volume/persistentvolume/pv_controller_base.go#L302
func (ctrl *PersistentVolumeController) Run(ctx context.Context) {
    ...
    // PVを管理するvolumeWorkerとPVCを管理するclaimWorkerを起動する
	go wait.UntilWithContext(ctx, ctrl.volumeWorker, time.Second) 
	go wait.UntilWithContext(ctx, ctrl.claimWorker, time.Second)

// pkg/controller/volume/persistentvolume/pv_controller_base.go#L492
func (ctrl *PersistentVolumeController) volumeWorker(ctx context.Context) {
	logger := klog.FromContext(ctx)
	workFunc := func(ctx context.Context) bool {
    keyObj, quit := ctrl.volumeQueue.Get() // queueからkeyを取得する
       ...
       volume, err := ctrl.volumeLister.Get(name) // kubernetes上のVolumeリソースを取得する
       if err == nil {
			ctrl.updateVolume(ctx, volume) // 同期処理
	}
    // ひたすらworkFuncを実行し続ける
	for {
		if quit := workFunc(ctx); quit {
			logger.Info("Volume worker queue shutting down")
			return
		}
	}
}

// pkg/controller/volume/persistentvolume/pv_controller_base.go#L202
func (ctrl *PersistentVolumeController) updateVolume(ctx context.Context, volume *v1.PersistentVolume) {
    err = ctrl.syncVolume(ctx, volume)
```

ようやく辿り着いた
```go
// pkg/controller/volume/persistentvolume/pv_controller.go#L561
func (ctrl *PersistentVolumeController) syncVolume(ctx context.Context, volume *v1.PersistentVolume) error {
    if volume.Spec.ClaimRef == nil {
		if _, err := ctrl.updateVolumePhase(ctx, volume, v1.VolumeAvailable, ""); err != nil {
			return err
		}
		return nil
	} else /* pv.Spec.ClaimRef != nil */ {
		// Volume is bound to a claim.
		if volume.Spec.ClaimRef.UID == "" {
			// The PV is reserved for a PVC; that PVC has not yet been
			// bound to this PV; the PVC sync will handle it.
			logger.V(4).Info("Synchronizing PersistentVolume, volume is pre-bound to claim", "PVC", klog.KRef(volume.Spec.ClaimRef.Namespace, volume.Spec.ClaimRef.Name), "volumeName", volume.Name)
			if _, err := ctrl.updateVolumePhase(ctx, volume, v1.VolumeAvailable, ""); err != nil {
				// Nothing was saved; we will fall back into the same
				// condition in the next call to this method
				return err
			}
			return nil
		}
```

- もしvolume.Spec.ClaimRef全部がない ( --> 作成直後 or ClaimRefを削除した状態 )　ならば PVのStatusをAvailableにする
- もしvolume.Spec.ClaimRefはあるけどuidがない場合は、ClaimRef.name,ClaimRef.namespaceで指定されているPVにpre-boundした状態にする
  - StatusはAvailable