Kubernetes ile uygulama deploy etmeye başladığınızda, ilk başta her şey güzel görünür. Pod’lar ayağa kalkar, servisler birbirleriyle konuşur, hayat güzeldir. Ta ki pod’unuz restart edip tüm verilerini kaybedene kadar. İşte bu noktada PersistentVolume ve PVC kavramlarıyla tanışmak zorunda kalırsınız. Bu yazıda, Kubernetes’te kalıcı depolama yönetimini gerçek dünya senaryolarıyla ve pratik örneklerle ele alacağız.
Kalıcı Depolama Neden Gerekli?
Kubernetes pod’ları özünde geçici yapılardır. Bir pod öldüğünde, yeniden başladığında ya da farklı bir node’a taşındığında, container içindeki tüm veriler uçup gider. Bu durum stateless uygulamalar için sorun değildir ama bir MySQL veritabanı, bir Elasticsearch cluster’ı ya da kullanıcı upload’larını tutan bir uygulama söz konusu olduğunda durum tamamen farklıdır.
Kubernetes bu sorunu çözmek için birkaç katmanlı bir soyutlama sistemi kullanır. Bu sistemi anlamak, başlangıçta biraz kafa karıştırıcı olabilir ama mantığı kavradıktan sonra ne kadar zarif bir tasarım olduğunu göreceksiniz.
Temel Kavramlar
Kalıcı depolama konusunda üç ana kavramla karşılaşırsınız:
- PersistentVolume (PV): Cluster yöneticisi tarafından sağlanan, fiziksel depolama kaynağını temsil eden nesnedir. NFS, iSCSI, cloud provider disk’leri veya local disk olabilir.
- PersistentVolumeClaim (PVC): Kullanıcı ya da uygulama tarafından yapılan depolama talebidir. “Bana 10GB depolama ver” gibi düşünebilirsiniz.
- StorageClass: Dinamik provisioning için kullanılan şablon yapısıdır. PVC oluşturulduğunda otomatik olarak PV yaratır.
Bu ilişkiyi şöyle düşünebilirsiniz: PV bir depo rafıdır, PVC ise o raftan yer talep etmektir. StorageClass ise depoda boş raf olmadığında otomatik yeni raf sipariş eden sistemdir.
PersistentVolume Oluşturma
Önce manuel PV oluşturmayı görelim. Bu yöntem genellikle “static provisioning” olarak adlandırılır ve özellikle on-premise ortamlarda sıkça kullanılır.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-nfs-pv
labels:
type: nfs
environment: production
spec:
capacity:
storage: 50Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nfs:
server: 192.168.1.100
path: "/exports/kubernetes/data"
EOF
Burada birkaç önemli parametreyi açıklayalım:
- capacity.storage: PV’nin toplam kapasitesi
- volumeMode:
Filesystem(dizin olarak mount) veyaBlock(ham blok cihaz) seçenekleri vardır - accessModes: Erişim modunu belirler
- persistentVolumeReclaimPolicy: PVC silindiğinde ne yapılacağını belirler
- storageClassName: Bu PV’nin hangi storage class’a ait olduğunu tanımlar
Access mode’lar için üç seçenek vardır:
- ReadWriteOnce (RWO): Tek bir node tarafından okunup yazılabilir
- ReadOnlyMany (ROX): Birden fazla node tarafından sadece okunabilir
- ReadWriteMany (RWX): Birden fazla node tarafından okunup yazılabilir
Reclaim policy seçenekleri ise şunlardır:
- Retain: PVC silinince PV korunur, veriler kaybolmaz ama PV tekrar kullanılamaz hale gelir
- Delete: PVC silinince PV ve arkasındaki depolama otomatik silinir
- Recycle: Eski bir yöntemdir, genellikle artık önerilmez
PersistentVolumeClaim Oluşturma
PV hazır olduğuna göre şimdi PVC oluşturalım:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-app-pvc
namespace: production
spec:
accessModes:
- ReadWriteMany
volumeMode: Filesystem
resources:
requests:
storage: 20Gi
storageClassName: manual
selector:
matchLabels:
type: nfs
environment: production
EOF
PVC oluşturduktan sonra durumunu kontrol edelim:
kubectl get pvc -n production
kubectl describe pvc my-app-pvc -n production
# PV durumunu da kontrol edelim
kubectl get pv my-nfs-pv
PVC durumu Bound ise her şey yolundadır. Pending durumunda kalıyorsa, genellikle eşleşen bir PV bulunamadığı anlamına gelir. Lost durumu ise bağlı olduğu PV’nin artık mevcut olmadığını gösterir.
Pod’a PVC Bağlama
Şimdi bu PVC’yi gerçek bir pod’a nasıl bağlayacağımıza bakalım. Bir PostgreSQL deployment örneği üzerinden gidelim:
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-db
namespace: production
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: my-app-pvc
EOF
Dikkat etmeniz gereken önemli bir nokta var: PostgreSQL için PGDATA environment variable’ını mount edilen dizinin bir alt dizinine ayarlıyoruz. Bu, PostgreSQL’in mount noktasında “lost+found” gibi dizinler gördüğünde şikayet etmesini önler.
StorageClass ve Dinamik Provisioning
Gerçek dünya Kubernetes ortamlarında genellikle manuel PV oluşturmazsınız. Bunun yerine StorageClass kullanarak dinamik provisioning yaparsınız. Cloud provider’larda bu genellikle kutudan çıkar şekilde gelir ama on-premise ortamlarda biraz daha fazla kurulum gerekir.
AWS EBS kullanan bir StorageClass örneği:
cat <<EOF | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
allowVolumeExpansion: true
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
EOF
- provisioner: Hangi CSI driver’ının kullanılacağını belirtir
- volumeBindingMode:
Immediate(PVC oluşturulunca anında PV yarat) veyaWaitForFirstConsumer(pod schedule edilene kadar bekle) seçenekleri vardır - allowVolumeExpansion: Sonradan boyut büyütmeye izin verir
- reclaimPolicy: PVC silinince ne yapılacağı
WaitForFirstConsumer modu özellikle multi-zone cluster’larda kritiktir. Bu mod sayesinde PV, pod’un schedule edildiği zone’da oluşturulur. Aksi takdirde farklı zone’larda PV ve pod oluşabilir ve pod volume’a erişemez.
Dinamik provisioning kullanan bir PVC örneği:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elasticsearch-data
namespace: logging
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
storageClassName: fast-ssd
EOF
Bu PVC’yi oluşturduğunuzda (ya da bunu kullanan bir pod schedule edildiğinde, binding mode’a göre), Kubernetes otomatik olarak 100GB’lık bir EBS volume oluşturacak ve PV ile eşleştirecektir.
StatefulSet ile PVC Kullanımı
StatefulSet’ler, özellikle veritabanları ve dağıtık sistemler için tasarlanmıştır. Her pod için otomatik olarak ayrı PVC oluşturmaları en önemli özellikleridir. Bir Redis cluster örneği:
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
namespace: caching
spec:
serviceName: redis-headless
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
command:
- redis-server
- /etc/redis/redis.conf
volumeMounts:
- name: redis-data
mountPath: /data
- name: redis-config
mountPath: /etc/redis
volumes:
- name: redis-config
configMap:
name: redis-configuration
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 10Gi
EOF
StatefulSet’in volumeClaimTemplates bölümü tam burada devreye girer. Her pod için (redis-cluster-0, redis-cluster-1, redis-cluster-2) ayrı PVC’ler otomatik oluşturulur. Adlandırma şeması şöyledir: redis-data-redis-cluster-0, redis-data-redis-cluster-1 ve böyle devam eder.
Bu PVC’lerin önemli bir özelliği, StatefulSet silinse bile PVC’lerin korunmasıdır. Bu, yanlışlıkla StatefulSet silseniz bile verilerinizin güvende kalacağı anlamına gelir. Tabii bu aynı zamanda manuel temizlik yapmanız gerektiği anlamına da gelir.
Volume Snapshot ile Yedekleme
Production ortamında volume snapshot’ları kritik öneme sahiptir. Kubernetes’te VolumeSnapshot API’si bu iş için kullanılır:
# Önce VolumeSnapshotClass tanımlayalım
cat <<EOF | kubectl apply -f -
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshot-class
annotations:
snapshot.storage.kubernetes.io/is-default-class: "true"
driver: ebs.csi.aws.com
deletionPolicy: Delete
EOF
# Snapshot alalım
cat <<EOF | kubectl apply -f -
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-backup-$(date +%Y%m%d)
namespace: production
spec:
volumeSnapshotClassName: ebs-snapshot-class
source:
persistentVolumeClaimName: my-app-pvc
EOF
Bu snapshot’tan yeni bir PVC oluşturmak için:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-restored
namespace: production
spec:
dataSource:
name: postgres-backup-20240115
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
EOF
Gerçek Dünya Senaryosu: Production Sorun Giderme
Bir gece alarm geldi: Production’daki MySQL pod’u CrashLoopBackOff durumunda. Kontrol etmeye başlıyorsunuz:
# Pod durumunu kontrol et
kubectl get pods -n production -l app=mysql
# PVC durumunu kontrol et
kubectl get pvc -n production
# Olayları incele
kubectl describe pod mysql-deployment-7d9f8b6c-xk2nl -n production
# PV'nin gerçekten bağlı olup olmadığını kontrol et
kubectl get pv | grep mysql
# Node'daki mount durumunu kontrol et (node'a SSH ile bağlandıktan sonra)
# Hangi node'da çalıştığını öğren
kubectl get pod mysql-deployment-7d9f8b6c-xk2nl -n production -o wide
# Node üzerinde
mount | grep kubernetes
df -h | grep kubernetes
# PVC'nin event log'larına bak
kubectl get events -n production --field-selector involvedObject.name=mysql-pvc --sort-by='.lastTimestamp'
Sık karşılaştığım sorunlar şunlardır:
- FailedMount: Volume’un node’a mount edilememesi. Genellikle CSI driver sorunu veya cloud provider permission problemidir.
- Insufficient storage: PV boyutunun yetersiz kalması.
allowVolumeExpansion: trueayarlıysa PVC’yi genişletebilirsiniz. - Multi-attach error: RWO volume’un birden fazla node’a mount edilmeye çalışılması. Bu genellikle node failure sonrası eski pod hala terminating durumundayken yeni pod başlatılmaya çalışıldığında olur.
Multi-attach sorununu çözmek için:
# Eski pod'u zorla sil
kubectl delete pod <stuck-pod-name> -n production --grace-period=0 --force
# PV'yi describe ederek hangi node'a bağlı olduğunu gör
kubectl describe pv <pv-name>
PVC Boyutu Genişletme
Uygulamanız büyüdükçe disk ihtiyacı da büyür. StorageClass’ınızda allowVolumeExpansion: true ayarlıysa, PVC boyutunu büyütmek oldukça basittir:
# Mevcut PVC boyutunu kontrol et
kubectl get pvc my-app-pvc -n production
# PVC'yi edit ile genişlet
kubectl edit pvc my-app-pvc -n production
# spec.resources.requests.storage değerini artırın
# Veya patch ile
kubectl patch pvc my-app-pvc -n production -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
# İşlemin tamamlanmasını bekle ve kontrol et
kubectl get pvc my-app-pvc -n production -w
Bazı volume türleri için filesystem’i genişletmek amacıyla pod’u restart etmeniz gerekebilir. Kubernetes genellikle bunu pod yeniden başladığında otomatik yapar, ancak bazı durumlarda pod içinden manuel müdahale gerekebilir.
PV Recycle ve Manuel Temizlik
Production’da sık karşılaşılan bir durum: PVC silindi ama PV hala “Released” durumunda ve tekrar kullanmak istiyorsunuz. Retain policy kullandıysanız PV’yi elle temizlemeniz gerekir:
# Released durumundaki PV'leri listele
kubectl get pv | grep Released
# PV'nin claimRef'ini temizle (bu PV'yi tekrar Available yapar)
kubectl patch pv my-nfs-pv -p '{"spec":{"claimRef": null}}'
# Veya doğrudan edit ile
kubectl edit pv my-nfs-pv
# spec.claimRef bölümünü silin
# Durumu kontrol et
kubectl get pv my-nfs-pv
Bunu yapmadan önce disk üzerindeki verileri backup almayı unutmayın. PV’yi tekrar Available yaptığınızda, yeni bir PVC bu PV’ye bağlanabilir ve eski veriler görünür olabilir.
StorageClass Bazlı İzleme ve Kapasite Yönetimi
Production’da PV/PVC durumlarını düzenli takip etmek önemlidir:
# Tüm PV'lerin özetini gör
kubectl get pv -o custom-columns='NAME:.metadata.name,CAPACITY:.spec.capacity.storage,STATUS:.status.phase,CLAIM:.spec.claimRef.name,STORAGECLASS:.spec.storageClassName'
# Namespace bazında PVC kullanımını listele
kubectl get pvc --all-namespaces -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CAPACITY:.status.capacity.storage,STORAGECLASS:.spec.storageClassName'
# Orphaned PVC'leri bul (hiçbir pod tarafından kullanılmayan)
kubectl get pvc --all-namespaces -o json | jq '.items[] | select(.status.phase == "Bound") | {namespace: .metadata.namespace, name: .metadata.name}'
# StorageClass bazında kullanım özetini çıkar
kubectl get pv -o json | jq -r '.items[] | "(.spec.storageClassName) (.spec.capacity.storage)"' | sort | uniq -c
Bu komutları cron job olarak çalıştırıp çıktılarını bir monitoring sistemine besleyebilirsiniz. Prometheus ve Grafana kullanıyorsanız, kube-state-metrics PV/PVC durumlarını otomatik olarak expose eder.
Güvenlik Konuları
PV/PVC kullanımında güvenlik genellikle göz ardı edilir. Dikkat etmeniz gereken noktalar:
- fsGroup: Pod güvenlik bağlamında
fsGroupayarlayarak volume’a erişim yetkisini belirleyin - readOnly mount: Yalnızca okuma gereken volume’ları
readOnly: trueile mount edin - Encryption: StorageClass parametrelerinde şifrelemeyi etkinleştirin
- RBAC: PVC oluşturma yetkisini namespace bazında kısıtlayın
Güvenli bir pod spec örneği:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: data-volume
mountPath: /app/data
- name: config-volume
mountPath: /app/config
readOnly: true
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: app-data-pvc
- name: config-volume
persistentVolumeClaim:
claimName: app-config-pvc
readOnly: true
EOF
Sonuç
Kubernetes’te kalıcı depolama yönetimi, ilk bakışta karmaşık görünse de mantığını kavradıktan sonra oldukça sistematik bir yapıya sahip olduğunu görürsünüz. PV, PVC ve StorageClass üçlüsü birlikte çalışarak depolama altyapısını uygulama geliştiricilerinden soyutlar ve yönetimini kolaylaştırır.
Production ortamında dikkat etmeniz gereken en kritik noktaları özetleyecek olursam:
- Mutlaka Retain policy kullanın önemli veriler için. Delete policy yanlış bir silme işleminde sizi ağlatabilir.
- StorageClass seçimine dikkat edin. Fast SSD her şey için gerekli değildir, doğru depolama tipini seçmek hem performans hem de maliyet açısından önemlidir.
- Volume snapshot’larını otomatize edin. Manuel backup’a güvenmek risklidir.
WaitForFirstConsumerbinding mode’unu tercih edin multi-zone cluster’larda. Zone affinity sorunlarının önüne geçer.- PVC boyutlarını başlangıçta biraz cömert tutun. Sonradan büyütmek mümkün ama küçültmek (çoğu durumda) imkansızdır.
- Orphaned PV ve PVC’leri düzenli temizleyin. Hem maliyet hem de yönetim karmaşıklığı açısından önemlidir.
Stateful uygulamalarla çalışmak her zaman biraz daha dikkat ve planlama gerektirir. Ama doğru yapılandırıldığında, Kubernetes’in storage yönetimi gerçekten güçlü ve esnek bir altyapı sağlar. Verileriniz güvende, uygulamalarınız mutlu olsun.