| name | openshift-deployment | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| description | BC Gov Private Cloud OpenShift workload-manifest guidance: picking the right controller (Deployment, StatefulSet, DaemonSet, Job, CronJob) and shaping it for the platform — container resources and QoS, namespace LimitRange defaults, pod and container securityContext hardening (runAsNonRoot, drop ALL capabilities, no privilege escalation, read-only root filesystem) for the restricted-v2 SCC, liveness/readiness/startup probes, terminationGracePeriod and SIGTERM handling, PID-1 reaping with tini or dumb-init, PodDisruptionBudgets, horizontal scaling (HPA, VPA), and CronJob admission rules. Use when authoring or reviewing a workload manifest bound for a license-plate runtime namespace. Out of scope: NetworkPolicies, Vault/ESO, Argo CD/GitOps, Artifactory image pulls, Porter TCP exposure, storage class choice, Sysdig, ACS runtime policies, security assessment. | ||||||||
| owner | bcgov | ||||||||
| tags |
|
- Authoring or reviewing a new workload manifest (
Deployment,StatefulSet,DaemonSet,Job, orCronJob— raw, Helm chart, or Kustomize base) bound for a license-plate-dev/-test/-prodnamespace on Silver, Gold, GoldDR, or Emerald. - Picking which controller type fits a given workload (stateless API → Deployment; ordered identity + per-pod PVC → StatefulSet; one pod per node → DaemonSet; one-shot / parallel work → Job; scheduled → CronJob).
- Hardening an existing workload to meet the BC Gov platform's mandatory pod hygiene — resource requests / QoS class, the pod and container
securityContext(runAsNonRoot,allowPrivilegeEscalation: false, dropALLcapabilities,readOnlyRootFilesystem,seccompProfile), probes, SIGTERM andterminationGracePeriodSeconds, PID-1 reaping, PDB, HPA (with explicit rampup / rampdown), and pod anti-affinity /nodeAffinity/topologySpreadConstraints. - Setting the
securityContextso a pod is admitted by the namespace's defaultrestricted-v2Security Context Constraint, and adding the writableemptyDirvolumes (e.g. for/tmp) that areadOnlyRootFilesystem: truecontainer needs. - Adding
VerticalPodAutoscalerrecommendations (updateMode: "Off") on a workload with a drifting resource footprint. - Getting past a Kyverno workload-admission rejection —
CronJobcadence (≥ 5 min, ≥ 1 h with a PVC),CRON_TZ=inschedule, or the Emerald-onlyDataClasslabel rule. - Diagnosing pod-level failures on a workload you own —
CrashLoopBackOff,OOMKilled,Evicted,TerminationGracePeriodExceeded, or a defunct-PIDs platform alert.
The following concerns are each their own scope and belong in a sibling BC Gov OpenShift skill. Those siblings are planned — until they land in this repo, use the linked upstream Kubernetes / Red Hat docs or the BC Gov Private Cloud TechDocs:
- Writing or debugging
NetworkPolicyobjects (default-deny, allow-from-router, allow-from-same-namespace, SDN-vs-NSX-T egress) —openshift-networking(planned). - Sourcing secrets from Vault via External Secrets Operator, or rotating Vault data —
openshift-secrets(planned). - Choosing or configuring an image registry pull path, creating an
ArtifactoryServiceAccount, or rotating Artifactory credentials —openshift-images(planned). - Onboarding to Argo CD, authoring a
GitOpsTeamCR, or designing abcgov-c/tenant-gitops-<licenseplate>repo layout —openshift-gitops(planned). - Picking a storage class, requesting a quota tier, or designing PVC backups —
openshift-storage(planned). - Onboarding to Sysdig Monitor, building dashboards, or producing the evidence for a quota-increase request —
openshift-sysdig(planned). - Configuring Red Hat Advanced Cluster Security (ACS) runtime policies, image scanning, or producing a security-assessment artifact —
openshift-acs(planned). The pod and containersecurityContextmanifest field itself is in scope here; ACS is the cluster-side enforcement / scanning layer on top of it. - Exposing a non-HTTPS TCP port via the Porter Operator (
TransportServerClaim) —openshift-networking(planned). - Designing CI workflows (GitHub Actions chassis, OIDC, fork-gate) — sibling
github-actionsskill. - Anything on AKS, EKS, or vanilla Kubernetes outside the BC Gov Private Cloud.
- Pick the controller from the workload's identity model, not personal habit. Stateless, fungible replicas behind a Service →
Deployment. Ordered identity, stable network ID, per-pod PVC, controlled rolling restarts (databases, brokers, leader-elected services) →StatefulSet. Exactly one pod per node (node-agent, log shipper, CSI side-car) →DaemonSet. Run-to-completion batch work →Job(completions/parallelismfor fan-out). Scheduled batch →CronJob(subject to the cadence rules in step 8). AvoidDeploymentConfigfor new work — it's an OpenShift-only legacy resource and the platform's automation treats it identically toDeploymentanyway. - Set
requestson every container and pick a deliberate QoS class.Guaranteed(requests == limits, both set for CPU and memory) is what ideal production workloads should run — the scheduler evicts these last under node pressure.Burstable(requests < limits, or only request is set) is reasonable for most workloads in BCGov. If you omit a value, the namespaceLimitRangeinjectscpu: 50m/memory: 256Mirequests and a memory limit equal to the namespace memory quota (capped at 16 GiB). Those defaults are conservative cluster-wide guardrails, so override them with values that fit your workload. - Right-size requests using observed data. Build the manifest with a starting guess, deploy to
-dev, let it run a representative load (or steady idle for a week), then read CPU/memory P95 and P99 from Sysdig and adjust. AddVerticalPodAutoscalerinupdateMode: "Off"(recommend-only) for long-lived workloads that drift over time; never run VPA inAutomode alongside HPA on the same metric. - Configure liveness, readiness, and (if needed) startup probes; keep them cheap and downstream-independent. Readiness probe gates Service endpoints: it must go
not readywhen the pod can't serve traffic (warming caches, lost downstream dep, draining) andreadywhen it can. Liveness probe restarts the container: only fail it when the process is genuinely wedged (deadlocked, stuck event loop) — not when a downstream is down. Startup probe is for slow-booting apps (legacy JVMs, large model loads): set generousfailureThreshold * periodSecondsso the liveness probe doesn't restart the pod mid-boot. HTTP probes should be a dedicated/livez//readyzendpoint that's cheap and doesn't touch downstreams;execprobes work but cost a process exec each interval, so keep them simple. - Make sure PID 1 reaps zombie children. If your container
ENTRYPOINTis your app process directly (["/app/server"]), it's PID 1 — and most app runtimes don't reap. Defunct children accumulate, the platform sends a defunct-PIDs alert, and the quick fix isoc delete pod(the root cause returns next deploy). The proper fix is to set the entrypoint to an init that reaps and forwards signals:tini(ENTRYPOINT ["tini", "--", "/app/server"]),dumb-init, ors6-overlay. Distroless and most OCP base images do not include one — add it in your Dockerfile. - Trap SIGTERM, set a sane
terminationGracePeriodSeconds, and never exceed 600 s. When a pod is being evicted (rollout, drain, scale-down), Kubernetes sends SIGTERM, waits up toterminationGracePeriodSeconds, then sends SIGKILL. Apps must use that window to stop accepting new work (close listeners, deregister from the LB), finish in-flight requests, flush buffers, and exit. Use ≤ 60 s for stateless services, up to 600 s for stateful (the Platform Operations team caps it at 600 s on node drains and force-kills anything still running). Add apreStoplifecycle hook if you need a "drain" pause between the LB deregister and the actual shutdown (["/bin/sleep", "10"]is a common pattern to let the Service's endpoint propagate). - Always ship a
PodDisruptionBudgetfor any multi-replica workload. UsemaxUnavailablefor stateless (e.g.maxUnavailable: 1on a 3-replica Deployment means up to 1 can be down during a drain). UseminAvailablefor stateful where you need a quorum (e.g.minAvailable: 2on a 3-pod Postgres). Never setmaxUnavailable: 0— the Platform Operations team overrides it during node drains, so it only masks the misconfiguration until the next maintenance window. - Use
CronJobonly when it fits the platform's admission rules; otherwise run an in-pod scheduler. Kyverno admission policies reject:schedulemore frequent than every 5 minutes;schedulemore frequent than every 1 hour if the pod mounts a PVC; anyCRON_TZ=prefix inschedule(usespec.timeZoneinstead). For high-frequency cron with a PVC, convert to a long-runningDeploymentwithgo-crondor any in-pod scheduler so you pay the pod-startup cost once. Always setsuccessfulJobsHistoryLimitandfailedJobsHistoryLimit(defaults of 3 and 1 are usually fine — set to 0 only if you have external observability for the runs),concurrencyPolicy(Forbidis the safe default for tasks that hold a lock),startingDeadlineSeconds, and a sensibleactiveDeadlineSecondson the Job spec to bound stuck runs. - Always ship a
HorizontalPodAutoscalerwith explicit rampup and rampdown windows. HPA on CPU or memory is the baseline; setminReplicas ≥ 2for any production workload (one replica + a node drain = an outage); setmaxReplicasfrom your quota (one runaway HPA can exhaustcompute-long-running-quotaand starve siblings). Always setspec.behavior.scaleUp(rampup) andspec.behavior.scaleDown(rampdown) — never leave them defaulted — so traffic spikes don't get throttled by slow scale-up and so scale-down doesn't thrash mid-rollout (typical baseline:scaleUp.stabilizationWindowSeconds: 30with aPercent: 100 / periodSeconds: 60policy;scaleDown.stabilizationWindowSeconds: 300with aPercent: 50 / periodSeconds: 60policy — tune from observed traffic, but never ship without them). Pair every HPA with the PDB from step 7. - Always set pod anti-affinity for any multi-replica workload, and add node affinity when the workload must land on (or avoid) a specific node pool / zone. Use
podAntiAffinitywithtopologyKey: kubernetes.io/hostnameso replicas don't co-locate on the same node (preferredDuringSchedulingIgnoredDuringExecutionfor soft spread on small clusters;requiredDuringSchedulingIgnoredDuringExecutionfor hard spread when you have node headroom). On Gold/GoldDR addtopologySpreadConstraintswithtopologyKey: topology.kubernetes.io/zoneso replicas survive a zone loss. UsenodeAffinity(not hard-coded node names — they rotate) when the workload needs GPU, storage-class-local, or other node-pool-specific scheduling. - Set an explicit
securityContexton every container so the pod is admitted by the namespace's defaultrestricted-v2SCC and stays hardened. At the container level setallowPrivilegeEscalation: false,capabilities.drop: ["ALL"],runAsNonRoot: true,readOnlyRootFilesystem: true, andseccompProfile.type: RuntimeDefault.restricted-v2already enforces most of these, but stating them in the manifest makes intent explicit, survives an SCC change, and is portable. Do not hard-coderunAsUser,runAsGroup,fsGroup, orsupplementalGroups— OpenShift assigns a UID/GID from the namespace's allocated range, and a hard-coded value outside that range is rejected byrestricted-v2(unable to validate against any security context constraint). When you setreadOnlyRootFilesystem: true, mount a writableemptyDirfor every path the process writes to (commonly/tmp, plus any cache/run dirs); useemptyDir.medium: Memorywith asizeLimitfor small scratch space so it counts against the pod's memory, not node disk.
- Always set
requestson every container, and pick a deliberate QoS class. PreferGuaranteed(requests == limitsfor CPU and memory) for workloads where eviction is costly — databases, leader-elected services, anything stateful or otherwise critical.Burstable(requests set, CPU limit omitted or memory limit ≥ request) is the practical norm for typical BC Gov application workloads. AvoidBestEffort(no requests, no limits) outside opportunistic batch. (Why: a pod withoutrequestsisBestEffort, gives the scheduler nothing to place against, and is the first evicted under node pressure. The namespaceLimitRangedefaults ofcpu: 50m/memory: 256Miare conservative cluster-wide guardrails, not workload-tuned values. A missing memory limit risks OOM-thrashing siblings; omitting a CPU limit is the BC Gov norm so apps can burst into spare capacity, but the pod becomes throttle-prone whenever a node neighbour is busy.) - Never run a database — or anything else with persistent state — as a
Deploymentwith a singleReadWriteOncePVC. (Why: nodes are not guaranteed up for 24 h, and a Deployment+RWO+rolling-update will deadlock the second pod waiting for the volume the first pod hasn't released. Use aStatefulSetwith one PVC per replica, or an HA pattern like CrunchyDB / Patroni / Mongo ReplicaSet, three pods minimum.) - Always set
terminationGracePeriodSecondsdeliberately — ≤ 60 s for stateless services, ≤ 600 s for stateful — and ensure the app trapsSIGTERMto stop accepting new work, drain in-flight, and exit. (Why: the Platform Operations team capsterminationGracePeriodSecondsat 600 s during node drains and force-kills anything still running; ignoring SIGTERM is data-loss waiting to happen on the next drain.) - Never set
maxUnavailable: 0on aPodDisruptionBudget, and never ship a multi-replica long-running workload without one. (Why: the Platform Operations team overridesmaxUnavailable: 0during node drains, so it only masks the misconfiguration until the next maintenance window; a missing PDB lets a node drain take all replicas down simultaneously. Set a real number — e.g.maxUnavailable: 1on a 3-replica Deployment, orminAvailable: 2on a 3-pod StatefulSet — so drains are gracefully orchestrated.) - Always make readiness and liveness probes cheap and downstream-independent. (Why: a readiness probe that checks a downstream DB will cascade an outage when the DB blips; a liveness probe that fails on transient downstream errors will restart the pod loop and accelerate the failure. Dedicate
/livezand/readyzendpoints that only check in-process health.) - Always set PID 1 to an init that reaps children (
tini,dumb-init, ors6-overlay) unless the application runtime is documented to reap. (Why: defunct<defunct>children accumulate when PID 1 doesn't reap, the platform sends an automated alert to the Product Owner and Technical Lead, andoc delete podis only a band-aid — the bug returns until PID 1 is fixed.) - Never schedule a
CronJobmore often than every 5 minutes (every 1 hour if it mounts a PVC), and never putCRON_TZ=in theschedulestring. (Why: a Kyverno cluster policy denies theCronJobcreate on all three counts. Usespec.timeZonefor the timezone, reduce the cadence, or switch to a long-runningDeploymentdriven by an in-pod scheduler likego-crond.) - Always set
concurrencyPolicy: Forbid(orReplace) on aCronJobwhose work cannot safely overlap. (Why: the defaultAllowlets a slow run pile up parallel pods on the next tick — for tasks that hold a row lock, mutate shared state, or push to an external system that doesn't dedupe, this is a data-corruption incident, not a performance issue.) - Always set
minReplicas ≥ 2for any production workload — Deployment or HPA-driven. (Why: a single-replica workload becomes unavailable the moment its node is drained for patching, which happens routinely. Two replicas + a PDB is the minimum for "stays available across voluntary disruptions".) - Always ship a
HorizontalPodAutoscalerfor any long-running workload, and always set bothspec.behavior.scaleUp(rampup) andspec.behavior.scaleDown(rampdown) explicitly — never rely on defaults. (Why: HPA defaults are conservative — scale-up can lag a traffic spike by minutes and scale-down can thrash during a rollout, both of which cause user-visible incidents on this platform. Baseline:scaleUp.stabilizationWindowSeconds: 30with aPercent: 100 / periodSeconds: 60policy;scaleDown.stabilizationWindowSeconds: 300with aPercent: 50 / periodSeconds: 60policy. Tune from observed traffic, but the keys must be present.) - Always set pod anti-affinity on every multi-replica workload, and use
nodeAffinity(never hard-coded node names) when scheduling must target a specific node pool or zone. (Why: withoutpodAntiAffinitythe scheduler is free to stack all replicas on one node — one node drain or hardware failure then takes the whole workload down, regardless of the PDB. UsetopologyKey: kubernetes.io/hostnamefor per-node spread, and on Gold/GoldDR addtopologySpreadConstraintswithtopologyKey: topology.kubernetes.io/zoneso a zone outage doesn't take all replicas. Hard-coded node names rotate and break the manifest the next time the cluster is reconciled.) - Always scale horizontally (more replicas) rather than vertically (bigger pods), and pair HPA with a PDB. (Why: large single pods exhaust schedulability — a 32 GiB pod requires a node with 32 GiB of unallocated memory, which becomes rare as the cluster fills; horizontal scaling spreads load and tolerates node loss. Without a PDB, HPA's scale-down can take the workload below the budget during a drain.)
- Always set an explicit container
securityContextwithallowPrivilegeEscalation: false,capabilities.drop: ["ALL"],runAsNonRoot: true,readOnlyRootFilesystem: true, andseccompProfile.type: RuntimeDefault. (Why: these are the hardening defaults the platform expects under therestricted-v2SCC; declaring them in the manifest makes the posture explicit, portable, and resilient to an SCC change instead of relying on cluster-side defaulting.readOnlyRootFilesystem: truealso blocks a whole class of tamper-the-binary attacks — pair it with writableemptyDirmounts for the paths the app actually writes to.) - Never hard-code
runAsUser,runAsGroup,fsGroup, orsupplementalGroupsin a workload bound for a license-plate namespace. (Why: OpenShift'srestricted-v2SCC assigns a UID/GID from the namespace's pre-allocated range; a hard-coded value outside that range fails admission withunable to validate against any security context constraint, and even a value inside the range will break the next time the range is reassigned. SetrunAsNonRoot: trueand let the platform pick the UID. Build images so they run as an arbitrary non-root UID — group-writable, group0— rather than a fixed one.) - Never set
privileged: true, add Linux capabilities, or usehostNetwork/hostPID/hostPathon a workload bound for a license-plate namespace. (Why: therestricted-v2SCC rejects all of them. If you genuinely believe you need one, that is a platform-team conversation via a Public Cloud Service Request — not a manifest change.) - Never run
VerticalPodAutoscalerinupdateMode: "Auto"on a workload that already has anHorizontalPodAutoscaleron the same metric (CPU or memory). (Why: VPA changes requests, HPA reads utilization-vs-requests; the two fight and both can oscillate. Use VPA inupdateMode: "Off"for recommendations only when an HPA is in play.) - Always set
successfulJobsHistoryLimit,failedJobsHistoryLimit,startingDeadlineSeconds, and anactiveDeadlineSecondson the Job template of anyCronJob. (Why: default unbounded history fillscompute-time-bound-quotawith completed pods and blocks new runs; missingactiveDeadlineSecondslets a stuck run consume a worker slot indefinitely.)
- "Standard production HTTP API" →
Deployment,replicas: 3, container with explicitrequests == limitsfor CPU and memory (GuaranteedQoS), a hardened containersecurityContext(allowPrivilegeEscalation: false,capabilities.drop: ["ALL"],runAsNonRoot: true,readOnlyRootFilesystem: true,seccompProfile.type: RuntimeDefault) with a writableemptyDirmounted at/tmp,livenessProbeandreadinessProbeon dedicated/livezand/readyzendpoints,terminationGracePeriodSeconds: 30,preStop: { exec: { command: ["/bin/sleep", "10"] } },podAntiAffinityonkubernetes.io/hostname, a mandatoryPodDisruptionBudgetwithmaxUnavailable: 1, and a mandatoryHorizontalPodAutoscalerwithminReplicas: 3/maxReplicas: 10on CPU utilization 70% and explicitbehavior.scaleUp(stabilizationWindowSeconds: 30,Percent: 100 / periodSeconds: 60) +behavior.scaleDown(stabilizationWindowSeconds: 300,Percent: 50 / periodSeconds: 60). - "Pod rejected with
unable to validate against any security context constraint" → the manifest is asking for somethingrestricted-v2won't grant. Most often a hard-codedrunAsUser/fsGroupoutside the namespace's allocated range, aprivilegedcontainer, an added capability, orhostNetwork/hostPath. Remove the offending field; setrunAsNonRoot: trueand let OpenShift assign the UID. Confirm the range your namespace allows from its annotations —oc get project <ns> -o jsonpath='{.metadata.annotations.openshift\.io/sa\.scc\.uid-range}'(reading the clusterSecurityContextConstraintsobject itself withoc get sccis cluster-scoped and normally Forbidden for project users — you don't need it). - "HA Postgres for a production service" →
StatefulSetof 3 pods (CrunchyDB Operator or Patroni image frombcgov-docker-local),volumeClaimTemplatesfor one PVC per pod onnetapp-block-standard,PodDisruptionBudgetwithminAvailable: 2, paired with abackup-containerCronJobwriting to anetapp-file-backupPVC. Not aDeployment. - "Daily 02:00 cleanup job that mounts the PVC the app uses" →
CronJobwithschedule: "0 2 * * *",spec.timeZone: "America/Vancouver",concurrencyPolicy: Forbid,successfulJobsHistoryLimit: 3,failedJobsHistoryLimit: 1,startingDeadlineSeconds: 300, andactiveDeadlineSeconds: 3600on the Job template. ≥ 1 h cadence satisfies the Kyverno PVC rule. - "Process queue messages every 30 seconds" →
CronJobis rejected at admission (< 5 min). Convert to a long-runningDeploymentwithreplicas: 2that polls (or subscribes to) the queue in a loop and shuts down cleanly onSIGTERM; size replica count and per-replica concurrency for steady-state throughput. Still ship the mandatoryPodDisruptionBudget(maxUnavailable: 1),podAntiAffinityonkubernetes.io/hostname, and a CPU-basedHorizontalPodAutoscalerwithminReplicas: 2/maxReplicas: 6and the standardbehavior.scaleUp(stabilizationWindowSeconds: 30,Percent: 100 / periodSeconds: 60) +behavior.scaleDown(stabilizationWindowSeconds: 300,Percent: 50 / periodSeconds: 60) — queue-depth autoscaling is not in scope for this skill, but the CPU-based HPA keeps the mandatory-HPA rule satisfied and protects the workload when poll cost spikes. - "Slow-booting Java app keeps getting killed by liveness probe during startup" → add a
startupProbewithfailureThreshold: 30, periodSeconds: 10(5-minute boot budget). Liveness probe only kicks in after startup probe passes once, so the boot is no longer a restart loop. - "Defunct PIDs alert email" → container
ENTRYPOINTis the app process and the runtime doesn't reap. Switch the entrypoint totini(ENTRYPOINT ["tini", "--", "/app/server"]) ordumb-init; quick band-aid isoc delete podbut the bug returns until PID 1 is fixed. - "Pod stuck
OOMKilledon every restart" → memory request is below working set. Read Sysdig memory P99 and setrequests.memoryandlimits.memoryto that + headroom (~25%). If working set legitimately spikes, profile for leaks (pprof, JVM heap dump, etc.) before throwing more memory at it. - "Pod stuck
PendingwithFailedSchedulinginsufficient memory" → the requested memory is larger than any node's free allocatable. Either scale horizontally (more, smaller pods) or check if the namespace is sitting on a stuck terminating pod holding the request. Useoc get pods --field-selector=status.phase=Pendingto list the pending pods (the API has noTerminatingphase —Pending,Running,Succeeded,Failed, andUnknownare the only valid values), andoc get pods -o json | jq '.items[] | select(.metadata.deletionTimestamp) | .metadata.name'to list pods that are mid-terminate (the API signals that withmetadata.deletionTimestamp, not a phase).
Jobcompletes but the pod sticks around. That's intentional —Jobkeeps the terminated pod for log access. Tune withttlSecondsAfterFinishedon the Job spec (e.g.86400to garbage-collect after a day).CronJobhistory limits do this for you.StatefulSetrolling update hangs at one pod. Ordered rollout means the next pod won't start until the current one isReady. Check the failing pod's events / logs; if it's a probe failure, your readiness probe may be too strict or the new image is genuinely broken — rolling back viaoc rollout undo statefulset/<name>is safe.DaemonSetwon't schedule on a node. Likely a taint without a matching toleration, or a node selector / affinity rule.oc describe pod <ds-pod>will surface the scheduling failure; add thetolerationor relax the selector.- HPA reports
unknownfor a metric. The metric server can't read the source — forResourcemetrics (CPU/memory) the pods must have requests set on that resource (no requests means no utilization-vs-requests math). Evictionevent with reasonTerminationGracePeriodExceeded. Your app didn't exit withinterminationGracePeriodSecondsafter SIGTERM. Either bump the grace period (to the cap of 600 s) or — more often the right fix — fix the app to handle SIGTERM and drain.- A
CronJobaccidentally set toschedule: "*/1 * * * *"is rejected at admission. Kyverno denies it. Reduce the cadence (≥ 5 min, ≥ 1 h with a PVC), or convert to a long-running scheduler if you genuinely need higher frequency. - Need to pin a workload to a specific node pool / zone. Use
nodeSelectororaffinity.nodeAffinitywith the labels published on the cluster's nodes (oc get nodes --show-labels). Avoid hard-coding node names — they rotate. For DR / topology spread on Gold/GoldDR usetopologySpreadConstraintswithtopologyKey: topology.kubernetes.io/zone. - Pod stuck
CrashLoopBackOfffor more than a day. Read the prior run's stdout/stderr withoc logs <pod> --previous, push the fix, and roll out (oc rollout restart deployment/<name>); if you can't fix it immediately, scale the workload to 0 (oc scale deployment/<name> --replicas=0) so it stops consuming a worker slot while you investigate. StatefulSetPVC needs to grow. All BC Gov NetApp storage classes are resize-up-only: edit.spec.volumeClaimTemplates[*].spec.resources.requests.storage, then restart the pods (rolling update doesn't reshape existing PVCs — you may need to delete-and-recreate or use the platform's documented expansion procedure). Resize-down is not supported.readOnlyRootFilesystem: truemakes the app crash withread-only file system/EROFS. The process is writing somewhere on the root filesystem. Find the path from the error (commonly/tmp,/var/run, a framework cache, or a PID/lock file) and mount a writableemptyDirthere. For small scratch space preferemptyDir.medium: Memorywith asizeLimitso it's bounded and counts against the pod's memory rather than node disk. KeepreadOnlyRootFilesystem: true— adding the writable mount is the fix, turning the flag off is not.- App can't bind a privileged port (
< 1024) underrunAsNonRoot. A non-root UID can't bind:80/:443. Configure the app to listen on a high port (e.g.:8080) and let theServicemapport: 80 → targetPort: 8080. Do not reach forNET_BIND_SERVICEor root —restricted-v2won't grant either.
Scope of this skill: container-level and controller-level workload manifests inside an existing license-plate -dev / -test / -prod namespace. Cluster onboarding, network policy, secrets, GitOps, image-registry, storage class choice, monitoring, and security-assessment concerns live in their own sibling skills (see below).
See references/REFERENCE.md for: the namespace LimitRange and quota objects that shape what your manifests are allowed to request; a worked Deployment / StatefulSet / DaemonSet / CronJob template set; the probe, lifecycle, and preStop recipe; the restricted-v2 SCC securityContext recipe (what to set, what never to hard-code, and the writable-emptyDir pattern for readOnlyRootFilesystem); PDB sizing tables; an HPA and VPA decision matrix; the Kyverno workload-admission policy list (no-fast-cronjob, no-fast-cronjob-with-pvc, no-unsupported-timezone, plus the Emerald-only DataClass label rule that affects pod templates); the defunct-PID + PID-1 init container recipe; and a manifest-level error cheat sheet (OOMKilled, Evicted, CreateContainerConfigError, ImagePullBackOff, TerminationGracePeriodExceeded).
Sibling BC Gov OpenShift skills (each tightly scoped to one concern — planned, not yet shipped in this repo; use upstream docs in the interim):
openshift-networking—NetworkPolicystarter kit, default-deny, allow-from-router, SDN-vs-NSX-T egress, PorterTransportServerClaimfor direct TCP exposure.openshift-secrets— Vault + External Secrets Operator,SecretStoreandExternalSecretpatterns, hyphen-free key gotcha, rotation.openshift-images— Artifactory remote/local repos, prebuilt platform images,ArtifactoryServiceAccount+ pairedartifacts-*Secrets,npm login --auth-type=legacy.openshift-gitops— Argo CD shared-per-cluster,GitOpsTeamonboarding,bcgov-c/tenant-gitops-<licenseplate>repo layout, sync waves, Helm OCI through Artifactory.openshift-storage—netapp-file-standard/netapp-file-backup/netapp-block-standard, quota tiers, backup-container pattern, OCIO Object Storage Service.openshift-sysdig— Sysdig Monitor onboarding, dashboards, alerts, evidence for quota-increase requests.openshift-acs— Red Hat Advanced Cluster Security policies, image scanning, runtime alerts.
External (use upstream docs, not duplicates here):
- Kubernetes Workloads — controller reference for Deployment / StatefulSet / DaemonSet / Job / CronJob.
- Kubernetes Configure Quality of Service.
- Kubernetes Pod Lifecycle — probes, termination, init/sidecar containers.
- Kubernetes Configure a Security Context for a Pod or Container —
securityContextfields (runAsNonRoot,allowPrivilegeEscalation,capabilities,readOnlyRootFilesystem,seccompProfile). - OpenShift Managing Security Context Constraints — how
restricted-v2assigns UID/GID ranges and what it admits. - Kubernetes Specifying a Disruption Budget.
- Kubernetes HorizontalPodAutoscaler Walkthrough and VerticalPodAutoscaler.
- tini and dumb-init — PID-1 init container choices.
- Red Hat OpenShift Container Platform docs — for OCP-specific behaviour where it diverges from upstream Kubernetes.