概要
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させる
storageClass manifest
❯ cat sc.yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: example-sc
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: Immediate
persistentVolumeClaim manifest
❯ cat pvc.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc
spec:
storageClassName: example-sc
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
persistentVolume manifest
❯ 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
❯ 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になることを確認する
❯ 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を見てみる
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を編集する
$ k edit pv example-pv
# .spec.claimRef.uid行を削除する
persistentvolume/example-pv edited
または
# 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に戻っていることを確認する
❯ 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を作る
persistentVolumeClaim-2 manifest
❯ cat pvc2.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: example-pvc2
spec:
storageClassName: example-sc
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
❯ 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されることを確認する
# 最初に作った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のコードを読んでみる
> 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リソースを制御している
つらつらと書く
// 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)
ようやく辿り着いた
// 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