By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
A Hyper-Practical, Zero-Fluff Study Guide
You’re deploying a stateful app (PostgreSQL, Redis, or a custom microservice) in Kubernetes. Your cluster runs on AWS, GCP, or bare metal. Without StorageClasses, Dynamic Provisioning, and CSI Drivers, you’re manually creating and attaching storage volumes—like a sysadmin from 2005.
Here’s the problem: - Manual provisioning = Slow, error-prone, and unscalable. You’d have to: 1. Log into your cloud provider’s console. 2. Create a disk (EBS, Persistent Disk, etc.). 3. Attach it to a node. 4. Manually reference it in a PersistentVolume (PV) YAML. 5. Hope no one deletes it accidentally.- Static provisioning = Wastes money (you pay for unused storage) and blocks auto-scaling.
PersistentVolume
Dynamic Provisioning + StorageClasses + CSI Drivers = Kubernetes automates storage on-demand.- A PersistentVolumeClaim (PVC) requests storage → Kubernetes creates a PersistentVolume (PV) automatically using the right StorageClass.- CSI Drivers let Kubernetes talk to cloud providers (AWS EBS, GCP PD, Azure Disk) or on-prem storage (Ceph, NFS, Longhorn) without vendor lock-in.- Production impact: - No downtime when scaling stateful apps (e.g., databases, message queues). - Cost control (e.g., use gp3 instead of gp2 for EBS, or standard vs. ssd in GCP). - Disaster recovery (snapshots, cross-zone replication). - Security (encryption at rest, IAM policies for storage).
PersistentVolumeClaim
StorageClass
gp3
gp2
standard
ssd
Real-world scenario:You’re migrating a monolithic app to Kubernetes. The app uses a 100GB PostgreSQL database. Your team wants: - Zero manual storage management (no more "Bob, can you create a disk for me?").- Fast failover (if a node dies, the pod reschedules with the same data).- Cost-efficient storage (don’t pay for 100GB if the app only uses 20GB).- Compliance (data must be encrypted at rest).
This guide shows you how to set this up in 30 minutes.
persistentVolumeReclaimPolicy: Delete
dev
prod
reclaimPolicy
kubeadm
Pending
kubernetes.io/aws-ebs
ebs.csi.aws.com
pd.csi.storage.gke.io
/var/lib/postgresql
RWX
Retain
Delete
VolumeSnapshotClass
aws configure
AmazonEBSCSIDriverPolicy
The CSI driver lets Kubernetes create EBS volumes dynamically.
# Add the AWS EBS CSI driver Helm repo helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver helm repo update # Install the driver (replace <CLUSTER_NAME> with your EKS cluster name) helm upgrade --install aws-ebs-csi-driver \ aws-ebs-csi-driver/aws-ebs-csi-driver \ --namespace kube-system \ --set controller.serviceAccount.create=true \ --set controller.serviceAccount.name=ebs-csi-controller-sa \ --set controller.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::<ACCOUNT_ID>:role/AmazonEKS_EBS_CSI_DriverRole
Verify installation:
kubectl get pods -n kube-system | grep ebs-csi
Expected output:
ebs-csi-controller-7d6b5c8d8f-abc12 4/4 Running 0 2m ebs-csi-node-abc12 3/3 Running 0 2m
AWS EBS has multiple volume types: - gp2 (old default, burstable) - gp3 (new default, cheaper, configurable IOPS) - io1 (high performance, expensive)
io1
We’ll use gp3 with 3000 IOPS and 125 MiB/s throughput.
# storageclass-gp3.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gp3 provisioner: ebs.csi.aws.com volumeBindingMode: WaitForFirstConsumer # Delay binding until pod is scheduled parameters: type: gp3 fsType: ext4 encrypted: "true" # Enable encryption at rest iops: "3000" # Default is 3000 for gp3 throughput: "125" # Default is 125 MiB/s reclaimPolicy: Delete # Delete PV when PVC is deleted allowVolumeExpansion: true # Allow resizing
Apply it:
kubectl apply -f storageclass-gp3.yaml
Verify:
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE gp2 (default) kubernetes.io/aws-ebs Delete Immediate false 1d gp3 ebs.csi.aws.com Delete WaitForFirstConsumer true 10s
Kubernetes uses gp2 by default (slow and expensive). Let’s change it.
# Remove the default annotation from gp2 kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' # Set gp3 as default kubectl patch storageclass gp3 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE gp2 kubernetes.io/aws-ebs Delete Immediate false 1d gp3 (default) ebs.csi.aws.com Delete WaitForFirstConsumer true 2m
We’ll deploy PostgreSQL with a PVC that triggers dynamic provisioning.
# postgres-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-pvc spec: accessModes: - ReadWriteOnce storageClassName: gp3 # Explicitly use gp3 (optional if gp3 is default) resources: requests: storage: 10Gi
# postgres-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: postgres spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:13 env: - name: POSTGRES_PASSWORD value: "mysecretpassword" ports: - containerPort: 5432 volumeMounts: - name: postgres-storage mountPath: /var/lib/postgresql/data volumes: - name: postgres-storage persistentVolumeClaim: claimName: postgres-pvc
Apply both:
kubectl apply -f postgres-pvc.yaml kubectl apply -f postgres-deployment.yaml
Check the PVC status:
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE postgres-pvc Bound pvc-12345678-1234-1234-1234-1234567890ab 10Gi RWO gp3 30s
Check the PV:
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-12345678-1234-1234-1234-1234567890ab 10Gi RWO Delete Bound default/postgres-pvc gp3 1m
Check the EBS volume in AWS:
aws ec2 describe-volumes --filters "Name=tag:kubernetes.io/created-for/pvc/name,Values=postgres-pvc"
{ "Volumes": [ { "VolumeId": "vol-1234567890abcdef0", "Size": 10, "VolumeType": "gp3", "Iops": 3000, "Throughput": 125, "Encrypted": true, "State": "in-use", "Tags": [ { "Key": "kubernetes.io/created-for/pvc/name", "Value": "postgres-pvc" } ] } ] }
Let’s resize the PVC from 10Gi to 20Gi.
kubectl patch pvc postgres-pvc -p '{"spec": {"resources": {"requests": {"storage": "20Gi"}}}}'
Check the PVC:
kubectl get pvc postgres-pvc
Expected output (may take a few minutes):
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE postgres-pvc Bound pvc-12345678-1234-1234-1234-1234567890ab 20Gi RWO gp3 5m
aws ec2 describe-volumes --volume-ids vol-1234567890abcdef0
{ "Volumes": [ { "VolumeId": "vol-1234567890abcdef0", "Size": 20, # Resized from 10Gi to 20Gi "VolumeType": "gp3", "Iops": 3000, "Throughput": 125, "Encrypted": true, "State": "in-use" } ] }
encrypted: "true"
PodSecurityPolicy
NetworkPolicy
kube-state-metrics
reclaimPolicy: Retain
kubectl delete pv <name>
WaitForFirstConsumer
app: postgres
env: prod
kubelet_volume_stats_*
kubectl logs -n kube-system <csi-pod>
volumeBindingMode
Multi-Attach
"Which StorageClass parameter controls IOPS for gp3 volumes?"
iops
iops: "3000"
Dynamic Provisioning
"A PVC is stuck in Pending. What’s the most likely cause?"
Reclaim Policy
reclaimPolicy: Delete
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.