Practice — Platform Architecture & Infrastructure
Five hands-on tasks covering the exam’s Platform Architecture & Infrastructure domain, which is worth 15% of the paper. This is a smaller domain with an unusually high value per minute: the tasks are short, sharply defined and fast to verify, which makes them ideal “bank them early” material. Drill them cold — no notes, no tab of documentation open — and time-box each one to 5–7 minutes. Write the manifest, run the verification command, and only then open the worked solution. If you peeked before you attempted, you practised reading, not passing. These five are pulled from the full practice bank; the background lessons are platform architecture, the Kubernetes substrate, scaling & scheduling, multi-cluster and reference architecture.
These are the tasks about the shape of the playground itself — how much of it each team is allowed to use, where the equipment is allowed to be put, and how the playground quietly gets bigger when more kids turn up and smaller when they go home. You’re not building the swings here; you’re deciding the rules of the ground they stand on.
Do all five in one sitting with a 30-minute timer running. Attempt each task cold, stop at 7 minutes whether or not it works, and note which of the three failure kinds you hit: I didn’t know the API shape, I knew it but typed it slowly, or I couldn’t verify it. The first is a reading gap, the second is a command-reference drill, and the third is a troubleshooting-playbook gap — three completely different fixes.
Tenancy guardrails — capping what a namespace may consume
The first line of platform defence is arithmetic: a ceiling on the namespace and a sensible default on every container inside it. Both objects are quick to write and quick to prove, so this is the cheapest mark on the paper.
A1 · Cap a tenant with ResourceQuota and LimitRange, then prove enforcement
One tenant deployed a job with no resource requests, scheduled 400 pods, and starved two other teams off the nodes. You are adding tenant-level guardrails so a single namespace cannot do that again — and so pods without requests get sensible ones automatically.
Your task:
- Create a
ResourceQuotain namespacetenant-acapping requests to 4 CPU / 8Gi, limits to 8 CPU / 16Gi, and pods to 20. - Create a
LimitRangegiving containers a default request of 100m/128Mi and a maximum of 2 CPU / 4Gi. - Prove both: show a pod inheriting the defaults, and show a request that exceeds the quota being rejected.
Done when: kubectl -n tenant-a describe quota shows non-zero used, a pod created with no resources stanza shows requests.cpu: 100m, and scaling a deployment past the 4 CPU ceiling is rejected with an exceeded quota error.
Show the worked solution
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-quota
namespace: tenant-a
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: tenant-a-limits
namespace: tenant-a
spec:
limits:
- type: Container
default: # applied as limits when omitted
cpu: 500m
memory: 512Mi
defaultRequest: # applied as requests when omitted
cpu: 100m
memory: 128Mi
max:
cpu: "2"
memory: 4Gikubectl apply -f quota.yaml -f limitrange.yaml
kubectl -n tenant-a run demo --image=nginx
kubectl -n tenant-a get pod demo -o jsonpath='{.spec.containers[0].resources}'
# {"limits":{"cpu":"500m",...},"requests":{"cpu":"100m","memory":"128Mi"}}
kubectl -n tenant-a create deploy fat --image=nginx
kubectl -n tenant-a set resources deploy/fat --requests=cpu=1 --limits=cpu=1
kubectl -n tenant-a scale deploy/fat --replicas=10 # 10 CPU requested vs a 4 CPU ceiling
kubectl -n tenant-a describe rs -l app=fat | grep -i "exceeded quota"
# Error creating: pods "fat-..." is forbidden: exceeded quota: tenant-a-quota,
# requested: requests.cpu=1, used: requests.cpu=4, limited: requests.cpu=4
kubectl -n tenant-a describe quota tenant-a-quotaWhy: the two objects do different jobs and you need both. ResourceQuota is a namespace-wide ceiling; LimitRange sets per-container defaults and bounds. They interlock in a way that trips people: once a quota constrains requests.cpu, every pod in that namespace must specify a CPU request — the LimitRange is what supplies it automatically, so without the LimitRange, existing workloads suddenly fail to create. Also note that quota rejections surface on the ReplicaSet, not the Deployment, which is why describe rs is where you look.
Scaling & scheduling — how many, and where
Two questions the platform has to answer for every workload it hosts: how many replicas should exist right now, and which nodes are they allowed to sit on. Get the metric source right for the first and the taint/toleration split right for the second, and these are quick wins.
A2 · Autoscale on a custom metric
The ingest service is CPU-light but queue-bound: it sits at 15% CPU while a backlog of 40,000 messages builds up. A CPU-based HPA will never scale it. You have Prometheus metrics for the queue depth.
Your task:
- Create an HPA (autoscaling/v2) for
ingestscaling on a Pods or External metric — target 500 messages per replica — between 2 and 30 replicas. - Add a scale-down stabilisation window of 5 minutes so it doesn’t thrash.
- Alternatively, express the same thing as a KEDA
ScaledObject.
Done when: kubectl -n platform get hpa ingest shows a real current value against the target (not <unknown>), and replicas rise as the backlog grows.
Show the worked solution
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ingest
namespace: platform
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ingest
minReplicas: 2
maxReplicas: 30
metrics:
- type: External
external:
metric:
name: queue_messages_ready
selector: { matchLabels: { queue: ingest } }
target:
type: AverageValue
averageValue: "500" # 500 messages per replica
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies: [{ type: Percent, value: 50, periodSeconds: 60 }]
scaleUp:
stabilizationWindowSeconds: 0
policies: [{ type: Percent, value: 100, periodSeconds: 30 }]
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: ingest
namespace: platform
spec:
scaleTargetRef: { name: ingest }
minReplicaCount: 2
maxReplicaCount: 30
advanced:
horizontalPodAutoscalerConfig:
behavior: # KEDA passes this straight to the HPA it generates
scaleDown:
stabilizationWindowSeconds: 300
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitoring:9090
query: sum(queue_messages_ready{queue="ingest"})
threshold: "500"kubectl -n platform get hpa ingest -w kubectl -n platform describe hpa ingest # ScalingActive / AbleToScale conditions explain <unknown> kubectl get --raw /apis/external.metrics.k8s.io/v1beta1 | jq . # is the adapter serving?
Why: autoscaling/v2 supports Resource, Pods, Object and External metrics — but the HPA controller does not talk to Prometheus, it reads the metrics APIs, so something must serve them (prometheus-adapter, or KEDA, which installs its own external metrics adapter). A target showing <unknown> almost always means that adapter chain is missing, not that your HPA is wrong. KEDA is usually the faster answer under time pressure and adds scale-to-zero, which plain HPA cannot do.
A3 · Place a platform component correctly: taints, spread and a PDB
The ingress controller keeps landing on cheap spot nodes and all three replicas once ended up in the same availability zone. During a node drain, all three went down together and the platform had an outage.
Your task:
- Taint the three dedicated system nodes and add the matching toleration plus a nodeSelector to the ingress controller.
- Add a topology spread constraint forcing at most one-replica skew across zones.
- Add a
PodDisruptionBudgetguaranteeing at least 2 replicas survive voluntary disruption, and prove a drain respects it.
Done when: kubectl get pods -o wide -l app=ingress shows the three pods on three different system nodes, kubectl get nodes -L topology.kubernetes.io/zone confirms those nodes are in three different zones, and kubectl drain on a system node blocks rather than evicting below the budget.
Show the worked solution
kubectl taint nodes sys-1 sys-2 sys-3 dedicated=system:NoSchedule kubectl label nodes sys-1 sys-2 sys-3 node-role.acme.io/system=true
# in the ingress controller's pod template
spec:
nodeSelector:
node-role.acme.io/system: "true"
tolerations:
- key: dedicated
operator: Equal
value: system
effect: NoSchedule
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app: ingress }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ingress
namespace: ingress-nginx
spec:
minAvailable: 2 # or maxUnavailable: 1 - never set both
selector:
matchLabels: { app: ingress }# zone labels live on NODES, not on pods - so read the placement in two steps kubectl get pods -l app=ingress -o custom-columns='POD:.metadata.name,NODE:.spec.nodeName' kubectl get nodes -L topology.kubernetes.io/zone,node-role.acme.io/system kubectl get pdb ingress -n ingress-nginx # ALLOWED DISRUPTIONS should be 1 kubectl drain sys-1 --ignore-daemonsets --delete-emptydir-data # blocks at the budget
Why: the three mechanisms answer three different questions. A taint repels everything that hasn’t explicitly opted in, so it reserves nodes; a toleration alone doesn’t attract, which is why you also need the nodeSelector or affinity to pull the pod there. Topology spread handles “don’t put all the eggs in one zone.” A PDB constrains voluntary disruption only — node drains, cluster upgrades, descheduling — and does nothing about a node that simply catches fire. A PDB with minAvailable equal to the replica count blocks drains forever, which is a classic self-inflicted upgrade outage.
Mesh & multi-cluster — the shape of the estate
The last two tasks widen the frame: first securing traffic between workloads on one cluster, then treating several clusters as one platform from a single control plane.
A4 · Turn on strict mTLS and verify it
An audit found that service-to-service traffic inside the cluster is plaintext. The mesh is installed and sidecars are injected in payments, but the default permissive mode means nothing is actually enforced.
Your task:
- Enforce STRICT mTLS for the
paymentsnamespace. - Prove that a plaintext request from a pod without a sidecar is now rejected.
- Prove that a request from a meshed pod still succeeds and is encrypted.
Done when: a curl from a non-mesh pod to payments-api:8080 fails with a connection reset, the same call from a meshed pod returns 200, and the mesh CLI reports the traffic as mTLS.
Show the worked solution
# Istio
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT
---
# and lock down who may call it (authorization, not just encryption)
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: payments-api-allow
namespace: payments
spec:
selector:
matchLabels: { app: payments-api }
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/checkout/sa/checkout"]# verify - Istio istioctl x describe pod -n payments $(kubectl -n payments get pod -l app=payments-api -o name | head -1 | cut -d/ -f2) kubectl -n default run plain --rm -it --image=curlimages/curl --restart=Never \ -- curl -sS -m 5 http://payments-api.payments:8080/healthz # connection reset # Linkerd equivalent: strictness comes from the default inbound policy linkerd viz edges deployment -n payments # SECURED column shows √ for mTLS kubectl annotate namespace payments \ config.linkerd.io/default-inbound-policy=cluster-authenticated --overwrite
Why: a mesh in PERMISSIVE mode accepts both plaintext and mTLS — which is right for migration and worthless for compliance. STRICT is what an auditor is asking for, and the verification step is the part exams actually grade: showing the negative case (a non-mesh client is refused) proves enforcement in a way that a green dashboard does not. Note the split: PeerAuthentication is encryption and identity; AuthorizationPolicy is who may call whom. Strict mTLS with no authorization policy still lets every meshed pod reach every other one. More in networking.
A5 · Register and target a second cluster from one control plane
Acme now runs prod-eu and prod-us. Argo CD lives in the management cluster and currently only deploys in-cluster. The same platform add-ons must land on both, without maintaining two copies of anything.
Your task:
- Register both remote clusters with Argo CD and confirm they appear with the right labels.
- Create an
ApplicationSetusing the cluster generator so each registered cluster gets its ownApplication. - Restrict it to clusters labelled
env=prod.
Done when: argocd cluster list shows both remotes Successful, and kubectl -n argocd get applications shows one generated app per prod cluster, all Synced.
Show the worked solution
kubectl config get-contexts argocd cluster add prod-eu --name prod-eu --label env=prod --label region=eu argocd cluster add prod-us --name prod-us --label env=prod --label region=us argocd cluster list
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-addons
namespace: argocd
spec:
goTemplate: true
generators:
- clusters:
selector:
matchLabels:
env: prod # excludes the in-cluster/management destination
template:
metadata:
name: 'addons-{{.name}}'
spec:
project: platform
source:
repoURL: https://github.com/acme/platform-config.git
targetRevision: main
path: 'infrastructure/{{index .metadata.labels "region"}}'
destination:
server: '{{.server}}'
namespace: platform-system
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true, ServerSideApply=true ]Why: argocd cluster add creates a ServiceAccount in the target cluster and stores its credentials as a labelled Secret in argocd — those labels are exactly what the cluster generator selects on, which is why labelling at registration time saves you a rewrite later. This is the hub-and-spoke pattern: one management control plane, many workload clusters, one repo. Its failure mode is that the hub becomes a single point of failure, so the alternative — an Argo CD per cluster, all reading the same repo — is worth knowing too. See multi-cluster.
Where to go next
When all five land inside the time-box without notes, this 15% is banked. Go back to the full practice bank for the other four domains, or take the weak spots back into the lessons: scaling & scheduling for A2 and A3, networking for A4, multi-cluster and GitOps workflows for A5. Keep the command reference next to you while you build speed, and use the troubleshooting playbook whenever the verification step is what defeated you rather than the manifest. The domain weightings and the full blueprint live in the exam guide.
1. Once a ResourceQuota constrains requests.cpu, what happens to a pod submitted with no resources stanza and no LimitRange in the namespace? 2. Where does a quota rejection actually appear — on the Deployment, the ReplicaSet, or the Pod? 3. Your HPA target reads <unknown>. What is almost always missing? 4. Why isn’t a toleration on its own enough to keep the ingress controller on the system nodes? 5. What kind of disruption does a PodDisruptionBudget protect against — and what does it not touch? 6. In Istio, which object gives you encryption and identity, and which one decides who may call whom?
Check your answers
- It is rejected — a quota on
requests.cpumakes a CPU request mandatory for every pod in the namespace, so without a LimitRange to supply a default, previously-working workloads suddenly fail to create. - On the ReplicaSet — which is why
kubectl describe rs, notdescribe deploy, is where you find theexceeded quotamessage. - The metrics adapter chain. The HPA controller reads the metrics APIs, not Prometheus, so something (prometheus-adapter, or KEDA’s own external metrics adapter) has to serve
external.metrics.k8s.io. - A toleration only says the pod may sit on a tainted node; it doesn’t attract it there. You need a nodeSelector (or node affinity) to actually pull the pod onto those nodes.
- It protects against voluntary disruption only — drains, cluster upgrades, descheduling. It does nothing about an involuntary failure such as a node dying. And
minAvailableset equal to the replica count blocks drains forever. PeerAuthentication(withmode: STRICT) gives encryption and identity;AuthorizationPolicydecides who may call whom. Strict mTLS alone still lets every meshed pod reach every other one.