CNPA — Cloud Native Platform Engineering Associate
Building a Platform API With CRDs
Goal
Extend the Kubernetes API to build a platform API called WebService yourself, and complete a full set on a real cluster: schema validation, server-side defaults, tenant boundaries, and self-service RBAC.
Why it matters
If you build a platform API as an internal web app, you have to reimplement state storage, concurrency control, authentication and authorization, auditing, and watch from scratch. If you register a CRD, all of that comes along — which is why the Kubernetes API has become the common language of platforms. In particular, the OpenAPI schema is the cheapest device for implementing 'fast feedback', one of the three conditions for self-service. With the single line maximum: 10, a bad request is rejected immediately, in a sentence a person can read, rather than through a pipeline log 30 minutes later. default likewise lets the server enforce 'safe defaults'. And a CRD alone does not complete a platform — only when a boundary that keeps tenants from intruding on one another (namespaces, quotas, RBAC) is in place too can you grant permissions without a ticket. Every resource in this lab is a built-in Kubernetes resource, so you apply them for real and they are graded with kubectl.
Steps
- Write a CustomResourceDefinition in
/root/cnpa-platform/crd.yamland apply it —metadata.name: webservices.platform.labhub.io,spec.group: platform.labhub.io,spec.scope: Namespaced,spec.nameswith kindWebService, pluralwebservices, singularwebservice, first shortNames entryws, and versionv1alpha1withserved: trueandstorage: true. - Complete the
v1alpha1schema of the same CRD — in thespecobject,image(string, required),replicas(integer,default: 2,minimum: 1,maximum: 10), andpublic(boolean,default: false). Then add two columns toadditionalPrinterColumns:Image(jsonPath.spec.image, type string) andReplicas(jsonPath.spec.replicas, type integer). - Create the namespace
tenant-blue— with the labelsplatform.labhub.io/tenant: blueandpod-security.kubernetes.io/enforce: baseline. - Create the ResourceQuota
tenant-blue-quotaintenant-blue—requests.cpu: "2",requests.memory: 4Gi,limits.cpu: "4",limits.memory: 8Gi,pods: "10". In the same namespace, create the LimitRangetenant-blue-limits— typeContainer, withdefaultof cpu200m/ memory256Mi, anddefaultRequestof cpu100m/ memory128Mi. - Create the WebService
shopintenant-blue— write onlyspec.image: ghcr.io/labhub/shop:1.0.0and do not writereplicasorpublic. After creating it, read it back and check that both values were filled in. - Check the schema violation. Try to create a WebService
badwithspec.replicas: 20intenant-blue, and save the failure output (including standard error) to/root/cnpa-platform/reject.txt.badmust not remain in the cluster. - Create the ServiceAccount
blue-devintenant-blue, and in the same namespace create the Rolewebservice-editor(apiGroupsplatform.labhub.io, resourceswebservices, verbsget,list,watch,create,update,patch,delete) and the RoleBindingblue-devs(binding that Role to theblue-devServiceAccount). Do not grant permission to modify the quota. - Create a second tenant with the same pattern — the namespace
tenant-green(labelplatform.labhub.io/tenant: green), the ResourceQuotatenant-green-quota(includingpods: "10"), and the WebServiceapi(spec.image: ghcr.io/labhub/api:1.0.0, with replicas not specified).blue-devfromtenant-bluemust not be able to create a WebService intenant-green.
Notes
- Right after you apply the CRD, the short form must also work immediately, as in
kubectl get ws -n tenant-blue. - Check permissions with
kubectl auth can-i <verb> <resource> --as=system:serviceaccount:tenant-blue:blue-dev -n <네임스페이스>(replace the verb, resource, and namespace placeholders with actual values). - To capture the failure output in a file, you must redirect standard error as well.
- Common mistake 1: writing the CRD's
metadata.namein the singular, as inwebservice.platform.labhub.io. It must be plural. - Common mistake 2:
defaultmust go inside each property, not on thespecobject itself. Andrequiredis an array of property names.
Register the CRD — group, scope, names
Write a CustomResourceDefinition in /root/cnpa-platform/crd.yaml and apply it — metadata.name: webservices.platform.labhub.io, spec.group: platform.labhub.io, spec.scope: Namespaced, spec.names with kind WebService, plural webservices, singular webservice, first shortNames entry ws, and version v1alpha1 with served: true and storage: true.
A CRD name must be in the <복수형>.<그룹> format (the plural form, then the group). Because this is a resource that tenants create, be careful about the scope you choose.
Schema and printer columns
Complete the v1alpha1 schema of the same CRD — in the spec object, image (string, required), replicas (integer, default: 2, minimum: 1, maximum: 10), and public (boolean, default: false). Then add two columns to additionalPrinterColumns: Image (jsonPath .spec.image, type string) and Replicas (jsonPath .spec.replicas, type integer).
The OpenAPI v3 schema is attached to each version inside the versions array. Distinguish where you write each of required, minimum/maximum, and default. Printer columns are also defined per version.
Tenant namespace
Create the namespace tenant-blue — with the labels platform.labhub.io/tenant: blue and pod-security.kubernetes.io/enforce: baseline.
The namespace itself is the tenant boundary. Attach a label that shows membership together with the Pod Security Standards label.
ResourceQuota and LimitRange
Create the ResourceQuota tenant-blue-quota in tenant-blue — requests.cpu: "2", requests.memory: 4Gi, limits.cpu: "4", limits.memory: 8Gi, pods: "10". In the same namespace, create the LimitRange tenant-blue-limits — type Container, with default of cpu 200m / memory 256Mi, and defaultRequest of cpu 100m / memory 128Mi.
The two have different roles. One is the upper bound on the namespace total, and the other is the defaults and range for individual containers. You need both for a 'Pod without requests' to pass the quota.
Create a CR and see server-side defaults
Create the WebService shop in tenant-blue — write only spec.image: ghcr.io/labhub/shop:1.0.0 and do not write replicas or public. After creating it, read it back and check that both values were filled in.
Create it without writing replicas at all. When you read the stored object back, the value should be there.
Check that a schema violation is rejected
Check the schema violation. Try to create a WebService bad with spec.replicas: 20 in tenant-blue, and save the failure output (including standard error) to /root/cnpa-platform/reject.txt. bad must not remain in the cluster.
Try to create it with an out-of-range value, and keep the error message that appears in a file. You must save standard error as well.
Self-service permissions and guardrails
Create the ServiceAccount blue-dev in tenant-blue, and in the same namespace create the Role webservice-editor (apiGroups platform.labhub.io, resources webservices, verbs get,list,watch,create,update,patch,delete) and the RoleBinding blue-devs (binding that Role to the blue-dev ServiceAccount). Do not grant permission to modify the quota.
The existing RBAC applies as is to the new resource too. Check what to write in the rule's apiGroups and resources, and leave the quota out of reach.
Second tenant and proof of isolation
Create a second tenant with the same pattern — the namespace tenant-green (label platform.labhub.io/tenant: green), the ResourceQuota tenant-green-quota (including pods: "10"), and the WebService api (spec.image: ghcr.io/labhub/api:1.0.0, with replicas not specified). blue-dev from tenant-blue must not be able to create a WebService in tenant-green.
Applying the same pattern once more is what a platform is. And the first tenant's permissions must not reach the second tenant.