TT Lab
Get started
Learn Learning paths Courses

CNPE — Cloud Native Platform Engineer

Admitted, but nowhere to run: resource boundaries

Continue in TT Lab

Goal

On a personal k3s, you work directly through changing defaults, a quota rejection, a Pending caused by insufficient CPU, recovery, and per-team API permissions.

Why it matters

The platform having accepted a request is a separate matter from there being room to run it. ResourceQuota limits a team's total requests but does not reserve a place to run on any particular node. Even when LimitRange fills in empty requests, it does not modify Pods that already exist. If you mix these two up, the operator keeps raising the quota while the user waits in front of a Pending screen. You practice part of the CNPE multitenancy and capacity judgment, and this does not replace the official exam questions or the full exam scope.

What is prepared

There is an Ubuntu personal VM, k3s v1.36.4+k3s1, a real busybox container, and five dedicated Namespaces. You need kubectl resource queries, JSON, jq, and RBAC basics. KUBECONFIG is /etc/rancher/k3s/k3s.yaml, and all relative file paths are under /root/cnpe-capacity. baseline.json and baseline-origin.json hold the initial UID, node capacity, and old data, and you must not modify them. new-pod.json is a Pod with resources omitted, and oversized-pod.json is the 1-core Pod material you will modify. The old, memory-only, first, second, and sample Pods are started in advance. Starting from an empty RBAC setup, add only the permissions you need. Do not put in the kubeconfig or real credentials of an external cluster. Make all changes inside the personal VM.

This is a 75-minute lab, so extend the default 60-minute session with the +Time control. When the session ends, the VM and files disappear. Keep any report you need somewhere else before it ends. The first setup can take several minutes.

Steps

  1. Keep the existing Pod's ID card — Query the old Pod in kubectl get nodes and cnpe-cap-defaults. Save the old object in baseline.json to before.json. Check the UID and the request of 25m·32Mi, and do not delete or recreate old.
  2. Change only the defaults for new arrivals — Modify the LimitRange defaults in cnpe-cap-defaults. Put only one type Container rule in limits. defaultRequest is cpu 50m·memory 48Mi, and default is cpu 100m·memory 64Mi. The existing old must keep its request and UID unchanged.
  3. Is it Guaranteed if only memory matches? — Submit the prepared new-pod.json without any resources entry to create cnpe-cap-defaults/new. Wait for Ready and check the new request of 50m·48Mi. cnpe-cap-memory/memory-only has memory request=limit=64Mi and no CPU. Record each Pod's status.qosClass under new and memory_only in qos.json.
  4. Reject the third guest at the door — Create the ResourceQuota budget in cnpe-cap-quota from quota-manifest.json. hard is requests.cpu 100m, requests.memory 256Mi, pods 4. The existing first and second each request 50m. When status.used.requests.cpu reaches 100m, run collect quota. The third 50m request must be rejected by the CPU quota and the object must not exist.
  5. A server that has a ticket but no seat — In the too-large Pod of oversized-pod.json, change both the CPU request and limit to wanted_cpu in baseline.json. This is the node's allocatable core count rounded up plus 1. Leave the existing quota in cnpe-cap-schedule as it is and create the Pod. Confirm PodScheduled=False, Unschedulable, Insufficient cpu, and that it is not placed, and when the quota's used CPU becomes the request amount, run collect pending.
  6. Recover without raising the quota — In recovery-pod.json, keep the same Pod name, Namespace, and security settings, and write requests cpu 25m·memory 32Mi and limits cpu 25m·memory 64Mi. Delete only too-large and recreate it from this file. After you confirm a new UID and Ready, run collect recovered. Preserve the pending data and the existing schedule quota.
  7. Give teammates read permission only — Write and apply the Role reader and RoleBinding reader for cnpe-cap-quota in role.json and binding.json. Allow only pods get·list in the core API for the existing ServiceAccount tenant. With collect rbac, collect the successful read of its own first, the rejected read of cnpe-cap-other/sample, and the rejected change to budget. Do not add a ClusterRoleBinding or wildcard permissions.
  8. Separate what you proved from what you do not know — In report.json, record node_allocatable_m, pending_request_m, quota_hard_m, and recovered_request_m as millicore integers. old_pod_recreated is a boolean from comparing the initial and current UID, and memory_only_qos is the actual QoS. Record recovery_proves as placement_only, isolation_proves as api_permissions_only, and cost_slo_verified as false. Use only these nine fields, and keep the final recovery and permissions in place.

Reference

The collection format is python3 /opt/fixtures/cnpe-capacity-lab.py collect <단계> (the placeholder stands for the step name). The steps are quota, pending, recovered, and rbac. The collector saves the test requests and observation data to each step's .json and -origin.json. quota tests the rejection of creating a small Pod, and if it is allowed, it reclaims only that test Pod. rbac tests the tenant's read and a PATCH with the same cap value. A successful file save is not success of the required state. Check up to the grading result. Grading reads the data and the current API and does not modify answers or permissions. Because the past Pending data is kept separately, step 5 can be graded again even after recovery. If you collected under a wrong condition, you must fix that condition and collect again. Do not delete any initial Pod other than old.

The comparison against the preserved copy prevents accidental edits and is not tamper protection against the student's root. This lab does not measure real usage, latency, cost, SLO, or network blocking. Do not conclude that Ready is a performance guarantee or that separating API permissions is complete tenant isolation.

Official documentation

Keep the existing Pod's ID card

Query the old Pod in kubectl get nodes and cnpe-cap-defaults. Save the old object in baseline.json to before.json. Check the UID and the request of 25m·32Mi, and do not delete or recreate old.

A Pod name can be reused, but the UID changes when the Pod is recreated. Extract the nested object with .old in jq.

Change only the defaults for new arrivals

Modify the LimitRange defaults in cnpe-cap-defaults. Put only one type Container rule in limits. defaultRequest is cpu 50m·memory 48Mi, and default is cpu 100m·memory 64Mi. The existing old must keep its request and UID unchanged.

The API fills in defaults at the admission stage. Changing the LimitRange does not make existing Pods go through admission again.

Is it Guaranteed if only memory matches?

Submit the prepared new-pod.json without any resources entry to create cnpe-cap-defaults/new. Wait for Ready and check the new request of 50m·48Mi. cnpe-cap-memory/memory-only has memory request=limit=64Mi and no CPU. Record each Pod's status.qosClass under new and memory_only in qos.json.

For the per-container resources in this lab, Guaranteed looks at both the CPU and memory conditions. Running and QoS are different fields.

Reject the third guest at the door

Create the ResourceQuota budget in cnpe-cap-quota from quota-manifest.json. hard is requests.cpu 100m, requests.memory 256Mi, pods 4. The existing first and second each request 50m. When status.used.requests.cpu reaches 100m, run collect quota. The third 50m request must be rejected by the CPU quota and the object must not exist.

Even if you have not used up the 4 Pods, the sum of requested CPU reaches the limit. This is not a step that measures CPU utilization.

A server that has a ticket but no seat

In the too-large Pod of oversized-pod.json, change both the CPU request and limit to wanted_cpu in baseline.json. This is the node's allocatable core count rounded up plus 1. Leave the existing quota in cnpe-cap-schedule as it is and create the Pod. Confirm PodScheduled=False, Unschedulable, Insufficient cpu, and that it is not placed, and when the quota's used CPU becomes the request amount, run collect pending.

Check separately whether the API created the object and whether the scheduler placed it on a node. describe pod and the ResourceQuota status are different pieces of evidence.

Recover without raising the quota

In recovery-pod.json, keep the same Pod name, Namespace, and security settings, and write requests cpu 25m·memory 32Mi and limits cpu 25m·memory 64Mi. Delete only too-large and recreate it from this file. After you confirm a new UID and Ready, run collect recovered. Preserve the pending data and the existing schedule quota.

Here you recover by recreating. Do not conclude that service performance is sufficient merely because the Pod was placed after you reduced the request.

Give teammates read permission only

Write and apply the Role reader and RoleBinding reader for cnpe-cap-quota in role.json and binding.json. Allow only pods get·list in the core API for the existing ServiceAccount tenant. With collect rbac, collect the successful read of its own first, the rejected read of cnpe-cap-other/sample, and the rejected change to budget. Do not add a ClusterRoleBinding or wildcard permissions.

The collector impersonates the tenant and sends real requests. Even a PATCH with the same value must be rejected if there is no permission to change.

Separate what you proved from what you do not know

In report.json, record node_allocatable_m, pending_request_m, quota_hard_m, and recovered_request_m as millicore integers. old_pod_recreated is a boolean from comparing the initial and current UID, and memory_only_qos is the actual QoS. Record recovery_proves as placement_only, isolation_proves as api_permissions_only, and cost_slo_verified as false. Use only these nine fields, and keep the final recovery and permissions in place.

1000m is 1 core. A CPU request is not usage, and an API rejection is not proof of network isolation.