Skip to main content

Overview

Persistent Volumes (PV) provide durable storage for Kubernetes applications. Unlike ephemeral container storage, persistent volumes retain data across pod restarts, rescheduling, and failures.

Storage Architecture

Kubernetes storage has three key components:
  1. PersistentVolume (PV): Cluster-level storage resource
  2. PersistentVolumeClaim (PVC): User’s request for storage
  3. StorageClass: Dynamic provisioner for PVs
┌─────────────┐     requests     ┌──────────────┐     provisions     ┌────────────┐
│     Pod     │ ──────────────>  │     PVC      │ ──────────────>   │     PV     │
└─────────────┘                   └──────────────┘                    └────────────┘

                                         │ uses

                                  ┌──────────────┐
                                  │ StorageClass │
                                  └──────────────┘

PersistentVolumeClaim Examples

PostgreSQL Database Storage

pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  # Access mode defines how the volume can be mounted
  accessModes:
    - ReadWriteOnce  # Can be mounted read-write by a single node
  
  # Storage capacity request
  resources:
    requests:
      storage: 5Gi
  
  # Storage class to use for dynamic provisioning
  storageClassName: standard-rwo
  
  # Volume should be created as a filesystem
  volumeMode: Filesystem

Redis Cache Storage

pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard-rwo
  volumeMode: Filesystem

Storage Classes

GKE Storage Classes

GKE provides several pre-configured storage classes:

standard-rwo (Balanced PD)

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard-rwo
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-balanced  # Balanced performance and cost
  replication-type: regional-pd  # Replicated across zones
volumeBindingMode: WaitForFirstConsumer  # Create PV when pod is scheduled
allowVolumeExpansion: true  # Allow resizing
Use cases: General purpose databases (PostgreSQL, MySQL, MongoDB), application data

premium-rwo (SSD PD)

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: premium-rwo
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd  # High-performance SSD
  replication-type: regional-pd
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Use cases: High-performance databases, latency-sensitive applications

standard (HDD PD)

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-standard  # Cost-effective HDD
  replication-type: none
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Use cases: Backups, logs, archival data

Viewing Storage Classes

# List available storage classes
kubectl get storageclass

# Describe a storage class
kubectl describe storageclass standard-rwo

# Get default storage class
kubectl get storageclass -o jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}'

Access Modes

ModeDescriptionUse Case
ReadWriteOnce (RWO)Volume can be mounted read-write by a single nodeDatabases, stateful apps (most common)
ReadOnlyMany (ROX)Volume can be mounted read-only by many nodesStatic content, configuration
ReadWriteMany (RWX)Volume can be mounted read-write by many nodesShared storage, multi-pod writes
GKE Persistent Disks support only ReadWriteOnce. For ReadWriteMany, use Filestore or other network storage solutions.

Using PVCs in Deployments

PostgreSQL Deployment with PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard-rwo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1  # Must be 1 for ReadWriteOnce volumes
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          ports:
            - containerPort: 5432
          # Mount the PVC
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
      # Define the volume from PVC
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-pvc

Redis Deployment with PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard-rwo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          command:
            - redis-server
            - --appendonly yes
            - --dir /data
          ports:
            - containerPort: 6379
          volumeMounts:
            - name: redis-storage
              mountPath: /data
      volumes:
        - name: redis-storage
          persistentVolumeClaim:
            claimName: redis-pvc

StatefulSets with Persistent Storage

For applications requiring stable storage and network identities, use StatefulSets with volumeClaimTemplates:
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
  # Automatic PVC creation per replica
  volumeClaimTemplates:
    - metadata:
        name: postgres-storage
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: standard-rwo
        resources:
          requests:
            storage: 10Gi
This creates:
  • postgres-storage-postgres-0
  • postgres-storage-postgres-1
  • postgres-storage-postgres-2
Each StatefulSet pod gets its own dedicated PVC, ensuring data persistence and stability.

Storage Management

Listing PVCs and PVs

# List PVCs in current namespace
kubectl get pvc

# List PVCs in all namespaces
kubectl get pvc --all-namespaces

# List PVs (cluster-wide)
kubectl get pv

# Describe PVC for details
kubectl describe pvc postgres-pvc

Resizing Volumes

If the storage class allows expansion (allowVolumeExpansion: true):
# Edit the PVC
kubectl edit pvc postgres-pvc

# Update the storage size
spec:
  resources:
    requests:
      storage: 10Gi  # Increased from 5Gi
For filesystem resizing:
# Delete and recreate the pod to trigger filesystem resize
kubectl delete pod <pod-name>

# Or rollout restart the deployment
kubectl rollout restart deployment postgres
You can only increase volume size, not decrease. Plan capacity accordingly.

Deleting PVCs

# Delete PVC (be careful - this deletes data!)
kubectl delete pvc postgres-pvc

# Check PV reclaim policy before deleting
kubectl get pv -o jsonpath='{.items[*].spec.persistentVolumeReclaimPolicy}'

Reclaim Policies

Controls what happens to the PV when its PVC is deleted:

Retain

PV is kept, manual cleanup required:
persistentVolumeReclaimPolicy: Retain

Delete (Default for Dynamic Provisioning)

PV and underlying storage are automatically deleted:
persistentVolumeReclaimPolicy: Delete

Recycle (Deprecated)

Basic scrub (rm -rf /volume/*) - use Retain or Delete instead.

Backup Strategies

Volume Snapshots

Create point-in-time snapshots of persistent volumes:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: postgres-snapshot
spec:
  volumeSnapshotClassName: pd-snapshot-class
  source:
    persistentVolumeClaimName: postgres-pvc
# Create snapshot
kubectl apply -f snapshot.yml

# List snapshots
kubectl get volumesnapshot

# Restore from snapshot
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc-restored
spec:
  dataSource:
    name: postgres-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard-rwo

Application-Level Backups

For databases, use native backup tools:
# PostgreSQL backup
kubectl exec postgres-pod -- pg_dump -U postgres dbname > backup.sql

# Redis backup
kubectl exec redis-pod -- redis-cli --rdb /data/dump.rdb

Best Practices

Right-Size Storage

Start with conservative estimates and use allowVolumeExpansion to grow as needed

Use Regional Storage

Enable replication-type: regional-pd for high availability across zones

Monitor Usage

Set up alerts for storage utilization to prevent out-of-space issues

Regular Backups

Implement automated backup strategies using snapshots or application-native tools

Storage Class Selection

Use standard-rwo for general workloads, premium-rwo for performance-critical apps

StatefulSets for Stateful Apps

Use StatefulSets with volumeClaimTemplates for databases and clustered applications

Performance Considerations

IOPS and Throughput

GKE persistent disk performance scales with size:
Disk TypeIOPS (Read/Write)Throughput
pd-standard0.75 per GB120-180 MB/s per TB
pd-balanced6 per GB240 MB/s per GB
pd-ssd30 per GB480 MB/s per GB
If you need higher IOPS, increase volume size or use pd-ssd storage class.

Troubleshooting

PVC Stuck in Pending

# Check PVC status
kubectl describe pvc postgres-pvc

# Common causes:
# - No storage class available
# - Insufficient quota
# - Zone mismatch (WaitForFirstConsumer)

# Check events
kubectl get events --field-selector involvedObject.name=postgres-pvc

Pod Cannot Mount Volume

# Check pod events
kubectl describe pod <pod-name>

# Verify PVC is bound
kubectl get pvc

# Check if another pod is using the volume (ReadWriteOnce)
kubectl get pods -o json | jq '.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName=="postgres-pvc") | .metadata.name'

Storage Full

# Check disk usage in pod
kubectl exec <pod-name> -- df -h

# Resize PVC (if allowed)
kubectl edit pvc postgres-pvc

# Clean up old data
kubectl exec <pod-name> -- rm -rf /data/old-data

Build docs developers (and LLMs) love