GitHub Copilot for Kubernetes
This lesson has one measurement at its centre, and everything else follows from it.
A first-draft Kubernetes manifest — a Deployment and a Service for a small API, the shape you get from “give me a Deployment and a Service for this” — was run through two tools:
kubeconform -strict 2 resources found, Valid: 2, Invalid: 0, Errors: 0
checkov 69 passed, 21 failedThe manifest used a :latest image tag, set no resource requests or limits,
defined no probes, specified no securityContext, ran in the default
namespace, and exposed the service with a LoadBalancer — a public, billable
cloud resource.
Strict schema validation reported it as completely valid. That is not a defect in kubeconform. Schema validation answers “does this document match the Kubernetes API’s shape”. Safety is a different question, it needs a different tool, and if you only run the first one your CI is green on a manifest that should not ship.
Key takeaways
kubeconformchecks structure; a policy scanner checks safety. Twenty-one findings separated the two on the same file. Run both.kubectl apply --dry-run=clientis not offline. It downloads the OpenAPI schema from the API server; with no cluster it fails outright. This is a very common and wrong assumption.- Generated Deployments omit probes, resource limits and
securityContextessentially every time. Those three are the review. - A liveness probe that merely duplicates readiness turns a slow dependency into a restart loop. They answer different questions.
- Check
kubectl config current-contextbefore every command. Ambient context is how a command meant for staging reaches production.
Where Copilot helps
Manifests are YAML with a published schema and an enormous public corpus, so the structural output is good. Copilot reliably produces a syntactically valid Deployment, gets label selectors consistent, and converts between related resources — a Deployment into a StatefulSet, a Compose service into a Deployment plus a Service — with the mechanical parts correct.
It is also unusually good at the things around manifests: explaining a chart you
inherited, interpreting an events list, and turning a CrashLoopBackOff plus
kubectl describe output into a hypothesis. Kubernetes produces a great deal of
precise diagnostic text, and reading it quickly is exactly where a model earns
its place.
Where it consistently needs correcting is everything that is a policy decision rather than a structural one — and Kubernetes has an unusual number of those expressed as optional fields with permissive defaults.
The fields that are omitted by default
Four blocks are missing from essentially every generated Deployment. Each has a default, and each default is wrong for production.
Resource requests and limits. With no requests, the scheduler has no
information and will place the Pod anywhere; with no memory limit, a leak takes
the node down rather than the Pod. Requests are what scheduling uses; the memory
limit is what makes exceeding it fatal to one Pod instead of to its neighbours.
Probes. No readinessProbe means the Service sends traffic to a Pod the
moment its container starts, before the application can answer. No
livenessProbe means a wedged process is never restarted. No startupProbe
means a slow-booting application gets killed by its own liveness probe before it
finishes starting.
securityContext. The default is a container running as whatever user the
image specifies — root, unless the Dockerfile said otherwise — with a writable
root filesystem, all default capabilities, and the ability to escalate
privileges.
A namespace. Omitting it means default, which has no Pod Security
Admission labels and typically no NetworkPolicy.
Probes: three of them, answering different questions
This is the area where a plausible-looking manifest most often causes an outage, because the failure is intermittent and looks like the application’s fault.
Readiness answers should this Pod receive traffic right now. Failing it removes the Pod from the Service endpoints. Nothing is restarted. This is the probe that should check dependencies.
Liveness answers should this Pod be killed and recreated. Failing it restarts the container. It should check only whether the process itself is wedged.
Startup answers has this finished booting yet. While it is failing, the other two are suspended. It exists so a slow start does not trip liveness.
The classic generated mistake is pointing all three at the same endpoint, where that endpoint checks the database. Now a database blip fails liveness, every replica restarts simultaneously, and a brief dependency problem becomes a full outage — with the restarts themselves adding load to the recovering database.
Practical project: a reviewable Deployment
Practical example
Namespace, ConfigMap, Deployment, Service and NetworkPolicy
A manifest set that passes strict schema validation and a policy scan, with every remaining disagreement documented.
- Status
- Tested implementation
- Runtime
- kubeconform v0.8.0, Checkov 3.3.13, kubectl v1.36.4 (client only, no cluster)
- Command
kubeconform -strict -summary manifests/ && checkov -d manifests --framework kubernetes- Result
- kubeconform: 5 resources found, Valid: 5, Invalid: 0. Checkov: 90 passed, 1 failed — down from 69/21 on the draft. The single remaining failure is a documented disagreement about CPU limits.
- Run on
- August 21, 2026
Files
copilot-kubernetes-demo/ ├── manifests/ │ ├── 00-namespace.yaml Pod Security Admission: restricted │ ├── 10-configmap.yaml │ ├── 20-deployment.yaml │ ├── 30-service.yaml │ └── 40-networkpolicy.yaml default-deny ingress and egress ├── manifests-draft.yaml the first suggestion, kept for comparison ├── README.md └── .github/ └── instructions/ └── kubernetes.instructions.md
The prompt
Create a Kubernetes Deployment and ClusterIP Service for this API, in a namespace called copilot-demo.
Requirements:
- 3 replicas, RollingUpdate with maxUnavailable 0.
- CPU and memory requests, and a memory limit. No CPU limit.
- Startup, readiness and liveness probes on /healthz. Liveness must not check dependencies.
- Pod securityContext: runAsNonRoot, numeric runAsUser 10001, seccompProfile RuntimeDefault.
- Container securityContext: allowPrivilegeEscalation false, readOnlyRootFilesystem true, drop ALL capabilities.
- automountServiceAccountToken false.
- Pin the image by digest. Do not use the latest tag.
- ClusterIP, not LoadBalancer.
Ten clauses, and every one of them exists because the default is the other thing.
The Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: copilot-demo
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app.kubernetes.io/name: api
spec:
# The default ServiceAccount gets a mounted token by default. This
# workload never calls the Kubernetes API, so it should have neither.
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: ghcr.io/example/copilot-demo-api:0.1.0@sha256:0000…0000
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
capabilities:
drop: [ALL]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
# No CPU limit on purpose: a CPU limit throttles rather than
# kills, and throttling a latency-sensitive service to enforce a
# number nobody measured causes more incidents than it prevents.
memory: 512Mi
startupProbe:
httpGet: { path: /healthz, port: http }
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 5
livenessProbe:
httpGet: { path: /healthz, port: http }
periodSeconds: 15
failureThreshold: 3
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
# readOnlyRootFilesystem means anything that writes needs an explicit
# writable mount. An emptyDir for /tmp is usually all that is required.
- name: tmp
emptyDir: {}readOnlyRootFilesystem: true and the /tmp emptyDir belong together. Setting
the first without the second produces a Pod that starts and then fails the moment
anything writes a temporary file — which for a Python service is the first
request that touches the standard library’s tempfile handling.
The namespace does real work
apiVersion: v1
kind: Namespace
metadata:
name: copilot-demo
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latestThose two labels turn the securityContext fields above from a convention into
an admission requirement. A Pod that violates the restricted profile is
rejected by the API server, which means a future manifest that forgets
runAsNonRoot fails at apply time rather than running as root quietly.
This is worth asking for explicitly. It converts a review item into a mechanical one, which is the best trade available in this lesson.
The measured difference
draft final
kubeconform -strict 2 valid 5 valid
checkov 69 passed 90 passed
21 FAILED 1 FAILEDThe one remaining failure is CKV_K8S_11: CPU limits should be set, which this
manifest deliberately does not satisfy for the reason in the comment. That
disagreement is recorded rather than suppressed, because a scanner finding you
have thought about and rejected is a different thing from one you have not read.
Validation
Validate before deploying
Kubernetes: the local validation sequence
| Step | Command | Executed here |
|---|---|---|
| Schema validation | kubeconform -strict -summary manifests/ | PASS5 resources found, 5 valid, 0 invalid. The draft was also 100% valid, despite :latest, no limits and no probes |
| Policy scan | checkov -d manifests --framework kubernetes | PASS90 passed, 1 failed on the final manifests; 69 passed, 21 failed on the draft |
| Client-side dry run | kubectl apply --dry-run=client -f manifests/ | NOT RUNContrary to a common assumption, --dry-run=client is not offline: it downloads the OpenAPI schema from the API server. With no cluster it fails with a connection error. |
The dry-run assumption that is wrong
It is widely believed that kubectl apply --dry-run=client is a local check. It
is not. Running it here, with no cluster configured:
error: error validating "manifests-draft.yaml": error validating data:
failed to download openapi: Get "http://localhost:8080/openapi/v2?timeout=32s":
dial tcp 127.0.0.1:8080: connect: connection refusedkubectl needs the API server’s OpenAPI schema to validate against, so
--dry-run=client requires a reachable cluster even though it changes nothing.
Adding --validate=false does not help — it then fails resolving API groups
instead.
The practical consequence: kubectl --dry-run=client cannot be your CI
validation step unless CI has cluster credentials, which is usually a worse
trade than the check is worth. kubeconform is the offline validator, it needs
no cluster, and it is what the pipeline in
the capstone uses.
--dry-run=server is a genuinely useful check — it runs admission controllers
and webhooks, so it catches policy rejections a schema cannot — and it needs a
cluster by definition.
ConfigMaps, Secrets and how configuration reaches a Pod
Generated manifests get the mechanics right and the lifecycle wrong, and the lifecycle is where the surprises live.
A ConfigMap consumed with envFrom is read once, at container start.
Updating the ConfigMap does not change the environment of a running Pod. Nothing
warns you; the value in the cluster and the value in the process simply differ
until something restarts. Generated documentation frequently claims otherwise.
A ConfigMap mounted as a volume does update, eventually — the kubelet syncs it on its own schedule, typically within a minute or two, and the application has to notice the file changed. This is the mechanism if you want live reload, and it requires the application to cooperate.
The practical answer for most services is to treat configuration as immutable per rollout: change the ConfigMap and trigger a new rollout, so the change is versioned, observable and rollback-able like any other. A checksum annotation on the Pod template is the usual way to make that automatic, and it is worth asking for by name because no suggestion adds it unprompted.
Secrets are base64, and base64 is an encoding. kubectl get secret -o yaml
returns the value to anyone with read access to the namespace. Secrets are
encrypted at rest only if the cluster is configured for it, which is a
cluster-level decision most manifests cannot see.
Three things to check in any generated Secret handling. Is there a real value in
data: or stringData: — that is a credential in Git. Is the Secret mounted as
a volume rather than injected as an environment variable — environment variables
appear in kubectl describe pod, in crash dumps, and are inherited by every
child process. And does the ServiceAccount that can read it need to.
Rollouts, and what actually happens during a deploy
A Deployment’s update strategy is a capacity and availability decision that generated manifests leave at its default, and the default is not neutral.
RollingUpdate with the default maxUnavailable: 25% and maxSurge: 25% means
Kubernetes may take a quarter of your replicas out of service before new ones are
ready. For three replicas that rounds to one — a third of capacity — which is
fine if you are provisioned for it and an incident if you are running close to
the line at peak.
maxUnavailable: 0 with maxSurge: 1 is the conservative alternative: never
fewer than the current replica count, one extra Pod at a time. It needs headroom
for one more Pod and it is slower. That is usually the right trade for a
user-facing service, and it is what the example above uses.
Three things that interact with a rollout and that suggestions omit:
terminationGracePeriodSeconds and the preStop hook. When a Pod is
terminated, the kubelet sends SIGTERM and removes it from Service endpoints —
but those happen concurrently, and endpoint propagation across the cluster is not
instant. A Pod that exits immediately on SIGTERM drops the requests still being
routed to it. A preStop sleep of a few seconds, and an application that
finishes in-flight requests before exiting, is what makes a deploy invisible to
users. This is also why the exec form of CMD matters in
the Docker lesson — the shell form never
receives the signal at all.
PodDisruptionBudget. A rollout is a voluntary disruption; so is a node drain. Without a PDB, a cluster upgrade can evict every replica of your service at once because nothing told it not to.
revisionHistoryLimit. Defaults to 10 ReplicaSets retained, which is what
makes kubectl rollout undo possible. Suggestions sometimes set it to 0 to keep
things tidy, which removes the ability to roll back.
The commands worth knowing, and worth putting in your instructions file as the
allowed ones: kubectl rollout status deployment/api to watch, and
kubectl rollout undo deployment/api to revert to the previous ReplicaSet. The
second is the fastest remediation Kubernetes offers and it only works if the
history is there.
Helm and Kustomize, briefly
Most real manifests are not written by hand, and the two mainstream templating approaches change what “review the manifest” means.
Kustomize overlays patches onto a base. It is built into kubectl, the base
is plain YAML, and kubectl kustomize overlays/prod renders the result — which
means the thing you validate is the rendered output, not the base. That is the
key review point: run kubeconform and your policy scan on the rendered
manifests, because a patch can remove a securityContext as easily as add one.
Helm templates Go text into YAML, which is more powerful and considerably
harder to review, because the artifact is not YAML until it is rendered. The
equivalent step is helm template, and the same rule applies — validate the
render.
Copilot handles both reasonably. Two failure modes specific to them are worth
knowing. In Helm, a value referenced with .Values.something that does not exist
renders as an empty string rather than an error, so a typo silently produces
image: ":latest" or an omitted field. --strict and a values.schema.json
turn that into a failure. In Kustomize, a strategic-merge patch that targets a
list — containers, volumes, env — replaces or merges depending on the patch type
and the list’s merge key, and getting it wrong drops entries silently.
Kubernetes-specific risks
Missing probes, limits and securityContext. Covered above; the recurring three.
:latest and mutable tags. In Kubernetes this is worse than elsewhere,
because a Pod rescheduled onto a new node re-pulls the tag and may get different
code from its siblings. Pin by digest.
Liveness checking a dependency. Covered above. Turns a dependency blip into a correlated restart of every replica.
hostPath, hostNetwork, hostPID and privileged. Each punctures the
container boundary. A hostPath mount of / is root on the node; privileged: true is equivalent. Generated manifests introduce these when asked for something
that “needs access to the host”, and the right response is almost always to ask
what specifically it needs.
A LoadBalancer Service. Provisions a cloud load balancer with a public address and a recurring bill. Fine when intended; expensive when it appeared because the example it was drawn from used one.
Wildcard RBAC. verbs: ["*"] on resources: ["*"] in a Role is the same
mistake as a wildcard IAM policy, and it appears for the same reason — it makes
the tutorial work.
Secrets in manifests. A Secret is base64, not encryption. A generated
manifest with a real value in data: is a credential in Git.
maxUnavailable defaults. The default RollingUpdate allows 25% of replicas
to be unavailable during a rollout. For three replicas that is one Pod, which may
be fine — but it is a capacity decision that was made for you.
Destructive commands
Debugging
This is where Copilot is most useful in Kubernetes, and the reason is that Kubernetes produces exceptionally good diagnostic text — it is just voluminous.
The sequence worth encoding in your instructions file:
kubectl get pods— what state, and how many restarts.kubectl describe pod <name>— the events list at the bottom is usually the answer. Scheduling failures, image pull failures, probe failures and admission rejections all appear there.kubectl logs <name>— and--previousfor a container that has already restarted, which is the log you actually want in a crash loop.kubectl get events --sort-by=.lastTimestamp— cluster-level context the Pod description does not include.
Pasting the describe output and the logs into chat and asking which of the
common failure classes this is — image pull, scheduling, probe, admission,
application — is fast and reliable. The categories are distinct and the evidence
for each is in different parts of that output.
This Pod is in CrashLoopBackOff. Here is kubectl describe pod output and kubectl logs —previous.
Tell me which of these it is: image pull, scheduling, failing probe, admission rejection, or the application exiting. Quote the specific line that supports your answer.
Do not propose a kubectl command that changes anything.
“Quote the line that supports your answer” is the clause that makes the answer checkable in five seconds rather than plausible in five paragraphs.
Review workflow
- Run kubeconform -strictOffline, fast, and proves the document matches the API schema. It proves nothing else.
- Run a policy scanCheckov or kube-linter. This is the step that found 21 issues kubeconform reported as valid.
- Check the namespace and the image referenceHuman judgementNot `default`, not `:latest`, ideally a digest.
- Check probes, limits and securityContextHuman judgementThe three blocks that are absent by default. Is liveness checking a dependency?
- Check what is exposedHuman judgementClusterIP or LoadBalancer, and is there a NetworkPolicy in this namespace?
- Check the identityHuman judgementautomountServiceAccountToken, the ServiceAccount, and any RBAC the change introduces.
- kubectl diff against the target clusterAfter confirming the context. Shows what an apply would actually change.
Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.
Best practices
- Ask for probes, resources and
securityContextby name in the prompt; none appears by default. - Label namespaces with Pod Security Admission so the security fields become admission requirements rather than review items.
- Use
kubeconformin CI, notkubectl --dry-run=client. - Pair
readOnlyRootFilesystem: truewith an explicit writable mount. - Give every namespace a default-deny
NetworkPolicy. - Record scanner disagreements inline with a reason rather than suppressing them globally.
Common mistakes
- Treating a green
kubeconformrun as a passing review. - Pointing liveness and readiness at the same dependency-checking endpoint.
- Assuming
--dry-run=clientworks offline, then discovering CI needs cluster credentials. - Leaving a
LoadBalancerthat appeared because the example had one. - Committing a
Secretwith a real base64 value and believing it is encrypted.
Where to go next
GitHub Copilot for Docker covers the image these manifests reference, including why the uid has to be numeric. GitHub Copilot for Terraform covers the cluster underneath, and the capstone wires this validation into a pipeline that runs it on every pull request.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.
Was this lesson helpful?
Your answer is stored in this browser and is not sent anywhere.