Skip to content

CKA road trip — StorageClass + Provisioner end to end

Step 1 — Admin applies the StorageClass once

# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
kubectl apply -f storageclass.yaml
# storageclass.storage.k8s.io/fast-ssd created

Step 2 — Developer submits a PVC

# pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
  namespace: default
spec:
  storageClassName: fast-ssd
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi
kubectl apply -f pvc.yaml
# persistentvolumeclaim/postgres-data created

kubectl get pvc
# NAME            STATUS    VOLUME   CAPACITY   STORAGECLASS   AGE
# postgres-data   Pending                        fast-ssd       2s
# STATUS is Pending because volumeBindingMode is WaitForFirstConsumer
# it will bind when a pod uses it

Step 3 — Provisioner sees the PVC and calls AWS

The provisioner (ebs.csi.aws.com) is a program already running inside the cluster. This is what it does internally when it sees the PVC:

# provisioner sees new PVC: postgres-data, storageClassName: fast-ssd
# it matches — so it runs:

response = ec2.create_volume(
    Size=50,
    VolumeType='gp3',
    Iops=3000,
    AvailabilityZone='us-east-1a'
)
# AWS responds: { 'VolumeId': 'vol-0abc1234def5678' }

# provisioner now creates the PV automatically in Kubernetes:
pv = V1PersistentVolume(
    metadata=V1ObjectMeta(name='pvc-a1b2c3d4'),
    spec=V1PersistentVolumeSpec(
        capacity={'storage': '50Gi'},
        access_modes=['ReadWriteOnce'],
        storage_class_name='fast-ssd',
        aws_elastic_block_store=V1AWSElasticBlockStoreVolumeSource(
            volume_id='vol-0abc1234def5678',
            fs_type='ext4'
        )
    )
)
k8s.create_persistent_volume(pv)

Step 4 — PV appears automatically (nobody wrote this)

kubectl get pv
# NAME           CAPACITY  ACCESS MODES  STORAGECLASS  STATUS     CLAIM
# pvc-a1b2c3d4   50Gi      RWO           fast-ssd      Bound      default/postgres-data

This PV was never written by anyone. The provisioner created it after calling AWS.

kubectl get pvc
# NAME            STATUS  VOLUME         CAPACITY  STORAGECLASS  AGE
# postgres-data   Bound   pvc-a1b2c3d4   50Gi      fast-ssd      10s
# STATUS is now Bound

Step 5 — Developer deploys the pod using the PVC

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: postgres
spec:
  containers:
  - name: postgres
    image: postgres:15
    env:
    - name: POSTGRES_PASSWORD
      value: "password"
    volumeMounts:
    - name: data
      mountPath: /var/lib/postgresql/data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: postgres-data    # references the PVC
kubectl apply -f pod.yaml
# pod/postgres created

kubectl get pod postgres
# NAME       READY   STATUS    RESTARTS   AGE
# postgres   1/1     Running   0          15s

What just happened — full picture

Admin applied StorageClass (once, ever)
        |
Developer submitted PVC (pvc.yaml)
        |
Kubernetes told the provisioner (ebs.csi.aws.com)
        |
Provisioner called AWS API → created real disk vol-0abc1234def5678
        |
Provisioner created PV in Kubernetes pointing to that disk
        |
Kubernetes bound PVC to PV
        |
Developer deployed pod referencing the PVC
        |
Pod is running, writing data to /var/lib/postgresql/data
which maps to vol-0abc1234def5678 in AWS

The developer only wrote pvc.yaml and pod.yaml. Nobody wrote a PV. Nobody logged into AWS. The StorageClass + provisioner handled everything in between.


Compare to the lab (no-provisioner)

You wrote PV manually       ← provisioner would have done this
You wrote PVC               ← same
You wrote pod               ← same
StorageClass did nothing    ← provisioner would have called AWS here

The lab skips the automatic part entirely. That's why StorageClass looked pointless — because in that example, it was.