Kubernetes · OpenShift

OpenShift Operations

Monitoring that is half switched off by default, Loki queries that return before they time out, the four security gates and which one refused you, and the upgrade that stops on one PodDisruptionBudget.

27 min read Level: core → advanced OpenShift 03 / 03
The model

WHERE OBSERVABILITY DATA ACTUALLY COMES FROM

Two Prometheus instances behind one query endpoint — and the one that scrapes your applications does not exist until you enable it.

SOURCESkubelet / cAdvisornode-exporterOperator metricsYour /metricsDISCOVERYServiceMonitorPodMonitorPLATFORMPrometheus (openshift-monitoring)WORKLOADPrometheus (user-workload)RULESPrometheusRuleAlertmanagerQUERYThanos QuerierConsole dashboardsGrafana / externalLONG TERMRemote write → Thanos / Mimir / Cortex

Thanos Querier is what the console and your dashboards talk to; it federates both instances. That is why platform metrics appear immediately and yours do not, from the same URL.

Diagrams

THREE VIEWS OF THE SAME SYSTEM

The diagram above is the high level: what the pieces are. These two are the ones you want when something is wrong — what is inside one of those boxes, and the path a request really takes through them.

Low levelWhat is inside the cluster monitoring stack?
CLUSTER MONITORINGServiceMonitorcluster operatorsalerts toreceiversmetrics APICOLLECTPrometheus (platform)cluster componentsPrometheus (userworkload)opt-in, separatenode-exporterper nodekube-state-metricsobject stateROUTEAlertmanagerdedupe, silence, routeThanos Querierone query viewretention 15d defaultnot long-term storageSURFACEconsole dashboardsPrometheusRule CRsalerts as objectsremote-writefor anything longer
Platform and user-workload monitoring are two separate Prometheus instances on purpose. Your application's ServiceMonitor is ignored by the platform one, which is the usual reason a metric never appears.
ConnectionWhat does an upgrade traverse?
pick a channelCVO checks the graphsigned releaserelease image verifiedsignatureClusterOperators, inorderone at a timeany Degraded → stopupgrade parks hereMachineConfig renderedper poolnodes drain + rebootPDB gates itversion reportedcomplete
The upgrade stops on the first Degraded ClusterOperator, which is a feature: it parks rather than proceeding into a broken cluster. A PDB that can never be satisfied parks it just as effectively at the drain hop.
Core

MONITORING, LOGGING & SECURITY

The three things you are asked for after every incident, and the default that quietly prevents each one.

OpenShift ships a complete monitoring stack, but it is deliberately split in two:

  • Platform monitoring (openshift-monitoring) — scrapes the cluster itself. Always on, not configurable by you, and it will not scrape your namespaces.
  • User workload monitoring (openshift-user-workload-monitoring) — scrapes your applications. Disabled by default.

This is the single most common "our metrics don't work" cause on OpenShift. A team writes a ServiceMonitor, applies it, sees no error — because a ServiceMonitor with nothing watching it is a perfectly valid object — and the metrics never appear. Nothing is broken; the second Prometheus simply does not exist yet.

turning it on, and confirming the target is actually scraped
$ oc -n openshift-monitoring get cm cluster-monitoring-config -o yaml
if absent, create it — this one key is the whole switch:
data:
config.yaml: |
enableUserWorkload: true
$ oc -n openshift-user-workload-monitoring get pods
prometheus-user-workload-0 6/6 Running
then verify the TARGET, not the ServiceMonitor — a valid SM can still match nothing
$ oc -n openshift-user-workload-monitoring exec prometheus-user-workload-0 -c prometheus -- \
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[].labels.job'
A ServiceMonitor matches ports by NAME

spec.endpoints[].port is the Service's port name, not its number. An unnamed port cannot be selected at all, and a mismatched name matches nothing — silently, with the ServiceMonitor still showing as healthy. If the target list is empty, check the port name before anything else.

Alerts are PrometheusRule objects. Platform rules ship with the cluster; yours live in your namespace and are picked up by user-workload Prometheus.

The design question is not "what threshold" but what makes a human get out of bed. A CPU-above-80% alert fires constantly and teaches people to ignore the pager. An SLO burn-rate alert fires when you are consuming the error budget fast enough to miss the objective — which is the same thing the user is experiencing.

Multi-window burn rate is the standard shape: a fast window catches sudden severe breakage, a slow window catches sustained mild breakage, and requiring both to be hot suppresses the one-minute blip that recovered on its own.

a burn-rate rule that means something
groups:
- name: api-slo
rules:
- alert: APIErrorBudgetBurnFast
# 14.4x burn over 1h AND 5m = 2% of a 30-day budget in an hour
expr: |
(job:slo_errors:ratio_rate1h{job="api"} > (14.4 * 0.001))
and
(job:slo_errors:ratio_rate5m{job="api"} > (14.4 * 0.001))
for: 2m
labels: { severity: critical }
and the thing that actually causes missed incidents:
$ oc -n openshift-monitoring exec alertmanager-main-0 -c alertmanager -- \
amtool silence query --alertmanager.url=http://localhost:9093
an expired-in-name-only silence from last month's maintenance window

OpenShift logging moved from Elasticsearch/Kibana to Loki with the console as the UI. The practical differences matter:

  • Loki indexes labels, not content. Queries filter by label first and then grep the stream. A query with no label selector scans everything and will time out on a busy cluster. This is the opposite of the Elasticsearch habit.
  • Three tenantsapplication, infrastructure, audit — with separate access. Audit logs are not collected by default.
  • Retention is a LokiStack setting, and the default sizing is far smaller than most teams assume. Storage pressure silently drops the oldest streams.

The collector is Vector, configured by a ClusterLogForwarder. That object is also how you ship elsewhere — Splunk, Kafka, an external Loki — and you can forward and store locally at the same time.

LogQL that returns before the timeout
bad — no label selector, scans every stream in the tenant
{} |= "OutOfMemory"
good — narrow by label, THEN filter
{kubernetes_namespace_name="prod", kubernetes_container_name="api"}
|= "OutOfMemory" | json | line_format "{{.msg}}"
rate of errors per pod, which is what you actually want during an incident
sum by (kubernetes_pod_name) (
rate({kubernetes_namespace_name="prod"} |= "level=error" [5m]))
$ oc -n openshift-logging get lokistack logging-loki -o jsonpath='{.spec.limits.global.retention}'

Four independent gates sit between a request and a running workload. Reading the refusal tells you which one, and they are not interchangeable:

GateAnswersRefusal looks like
AuthenticationWho are you?Unauthorized, 401
RBACMay you perform this verb on this resource?Forbidden: User "x" cannot get pods
SCCMay this POD have the privileges it asks for?unable to validate against any security context constraint
NetworkPolicyMay this packet arrive?Nothing. Timeout.

The last row is the operationally important one: NetworkPolicy has no error message. Every other layer tells you it refused. A policy drop is indistinguishable from a dead backend, which is why it is worth proving or excluding early rather than late.

RBAC, resolved rather than guessed

stop reading RoleBindings and ask the API
$ oc auth can-i create deployments -n prod --as=jane
no
$ oc auth can-i --list -n prod --as=jane | head
and for a service account, which is where this usually bites:
$ oc auth can-i list secrets -n prod \
--as=system:serviceaccount:prod:api-sa
$ oc adm policy who-can delete pods -n prod
the reverse question, and far quicker than auditing bindings by hand
Audit logs answer 'who did this', but only if you collect them

The API server writes audit events for every request. They are on the masters at /var/log/kube-apiserver/ and reachable with oc adm node-logs --role=master --path=kube-apiserver/audit.log. They are not forwarded to Loki unless the ClusterLogForwarder names the audit input — so the one log you need after a security question is usually the one nobody enabled.

Advanced

UPGRADES

What the cluster already knows before you start, and where the hours go.

DECIDEChannel: stable / fast / eusTarget versionVERIFYRelease signatureUpgradeable=TrueDeprecated API checkCONTROL PLANECVO applies manifests in orderetcd → apiserver → controllersOPERATORS~30 cluster operators, sequencedNODESMCO renders configcordon → drain → rebootone pool at a timeSETTLEAll co AvailableMCPs Updated

The CVO applies manifests in a fixed order and stops at the first that will not go ready, so a stalled upgrade always names its blocker in the Progressing message.

Channels decide which versions are offered:

  • stable-4.x — released and soaked. The default for production.
  • fast-4.x — released, less soak. Fine for pre-production.
  • eus-4.x — Extended Update Support, for the even-numbered releases you can stay on longer and hop between while skipping a minor.
  • candidate-4.x — pre-release. Never production.

Before any upgrade, the cluster has already formed an opinion. The Upgradeable condition on ClusterVersion is set to False by any operator that knows the upgrade will hurt — most often because a deprecated API is still in use and the next minor removes it. Upgrading anyway is how a workload disappears mid-upgrade.

the pre-upgrade checklist, as commands
$ oc get clusterversion -o jsonpath='{.items[0].status.conditions}' \
| jq -r '.[]|select(.type=="Upgradeable")|.status + ": " + .message'
False: Cluster operator kube-apiserver should not be upgraded between minor
versions: APIRemovedInNextReleaseInUse: flowcontrol.apiserver.k8s.io/v1beta2
find WHO is still calling it, before you go looking through manifests:
$ oc get apirequestcount flowschemas.v1beta2.flowcontrol.apiserver.k8s.io \
-o jsonpath='{.status.currentHour.byNode[*].byUser[*].username}'
$ oc adm upgrade # what is actually on offer
$ oc adm upgrade --to=4.16.11

The control plane phase is fast and mostly invisible. The node phase is where the hours go: the MCO cordons a node, drains it, applies the new OS config, reboots it, uncordons, and moves to the next — one at a time per pool by default.

Draining is where upgrades stop, and the cause is nearly always one of two things:

  • A PodDisruptionBudget that cannot be satisfied. A single-replica Deployment with minAvailable: 1 can never be evicted. The drain retries forever, politely, and the upgrade sits at the same percentage for hours.
  • A pod with no controller — a bare Pod, created by hand — which drain refuses to evict because nothing would recreate it.

Both are the cluster protecting availability exactly as instructed. The fix is the PDB or the workload, not forcing the drain.

finding the PDB that is blocking the upgrade
$ oc get mcp worker -o jsonpath='{.status.conditions[?(@.type=="Degraded")].message}'
failed to drain node worker-07 after 1h0m0s: error when evicting pod "legacy-api-0":
Cannot evict pod as it would violate the pod's disruption budget
$ oc get pdb -A -o custom-columns=\
NS:.metadata.namespace,NAME:.metadata.name,MIN:.spec.minAvailable,ALLOWED:.status.disruptionsAllowed
prod legacy-api-pdb 1 0 <- 0 disruptions allowed, 1 replica
the real fix is 2 replicas. The unblock-now fix is to relax the PDB:
$ oc patch pdb legacy-api-pdb -n prod --type=merge \
-p '{"spec":{"minAvailable":0}}'
maxUnavailable is the lever for a 100-node cluster

At the default of 1, a 100-node pool is 100 sequential reboots — easily a working day. Raising maxUnavailable on the MachineConfigPool to 3 or 5 cuts that proportionally, provided your workloads have enough replicas and spread to survive losing that many nodes at once. Check PDBs and topology spread before you raise it, not after.

Three Red Hat operators cover the security questions that arrive as audit findings:

  • Compliance Operator — runs OpenSCAP against profiles (CIS, PCI-DSS, NIST 800-53) and produces ComplianceCheckResult objects. Many findings ship with an auto-remediation you can apply as a MachineConfig.
  • File Integrity Operator — AIDE on every node, alerting on unexpected changes to system files.
  • Quay / Clair — image vulnerability scanning at the registry.

Independently: image signature verification. A cluster that will pull any image from anywhere has no supply chain guarantee, and this is a cluster-wide policy, not a per-workload one.

scoping the image sources a cluster will trust
$ oc edit image.config.openshift.io/cluster
spec:
registrySources:
allowedRegistries:
- quay.io
- registry.redhat.io
- image-registry.openshift-image-registry.svc:5000
CAUTION: this rolls every node via the MCO. It is a node reboot, not a config reload.
and omitting the internal registry breaks every build in the cluster.
$ oc get compliancecheckresult -n openshift-compliance \
--selector compliance.openshift.io/check-status=FAIL

Two different things get called "backup" and conflating them is how a DR test fails:

etcd snapshotApplication backup (OADP/Velero)
ContainsEvery API objectSelected namespaces + PV data
RestoresThe whole cluster, to that instantNamespaces, into this or another cluster
GranularityAll or nothingPer namespace, per label
PV contentsNoYes, via snapshots or restic
Use forControl-plane disasterNamespace deleted, migration, real DR

An etcd restore is disruptive by design: you stop the control plane, restore on one master, and rebuild the others from it. It is the right tool for "we lost quorum", and the wrong tool for "someone deleted the prod namespace" — for which OADP restores in minutes without touching anything else.

both backups, and the check that they ran
etcd — on a master, produces a snapshot plus the static pod manifests
$ oc debug node/master-0 -- chroot /host /usr/local/bin/cluster-backup.sh \
/home/core/backup
OADP — namespaces AND their volumes
$ oc get backup -n openshift-adp
NAME STATUS ERRORS ITEMS AGE
prod-daily-0911 Completed 0 1847 6h
$ oc get backup prod-daily-0911 -n openshift-adp \
-o jsonpath='{.status.progress}'
restore one namespace without touching the rest of the cluster:
$ velero restore create --from-backup prod-daily-0911 --include-namespaces prod
The DR test nobody runs is the one that matters

An etcd restore rolls every node and takes a cluster down for the duration. If it has never been rehearsed, the first rehearsal will be during an outage, under time pressure, by someone reading the docs for the first time. Schedule it on a cluster you can afford to break, and time it — the number you get is your real RTO.

In practice

ADVANCED TROUBLESHOOTING

Operations failures are mostly absences: a metric that never arrived, a log that was never collected, an alert that was silenced, an upgrade that stopped. Absences have no error message, so each one needs a positive check rather than a glance at a dashboard.

SymptomThe absence behind itPositive check
App metrics missingUser workload monitoring never enabledoc -n openshift-user-workload-monitoring get pods
ServiceMonitor exists, no dataPort name mismatch — it matched nothingPrometheus /api/v1/targets, not the ServiceMonitor
Alert never firedAn unexpired silence, or the rule is in the wrong namespaceamtool silence query; oc get prometheusrule -A
Logs stop at a dateLokiStack retention, or storage pressure dropping streamsoc get lokistack -o yaml; the ingester pod's logs
No audit trail for an incidentClusterLogForwarder never included the audit inputoc adm node-logs --role=master --path=kube-apiserver/audit.log
Upgrade at the same % for hoursA drain blocked by a PDB or a bare podoc get mcp -o yaml — the Degraded message names the pod
Upgrade refuses to startUpgradeable=False — a removed API is still in useoc get apirequestcount names the caller
Node rebooted unexpectedlyMCO applying a MachineConfig, as designedoc get mcp; the node's machineconfiguration annotations

Collecting evidence while the incident is live

Prometheus keeps platform metrics for roughly 15 days and Loki for whatever retention you set. Both are shorter than most post-incident reviews take to schedule, so capture the query results during the incident rather than planning to look later.

snapshot the evidence before it ages out
range query straight out of Thanos, into a file you keep
$ TOKEN=$(oc whoami -t); HOST=$(oc -n openshift-monitoring get route thanos-querier -o jsonpath='{.spec.host}')
$ curl -sk -H "Authorization: Bearer $TOKEN" \
"https://$HOST/api/v1/query_range?query=up{job=%22api%22}&start=...&end=...&step=60" \
> incident-up.json
and the events, which expire in ONE hour by default — always grab these first
$ oc get events -A --sort-by=.lastTimestamp -o json > incident-events.json
$ oc adm must-gather --dest-dir=./mg-$(date +%F)
Events expire in an hour

Kubernetes Events have a default TTL of 60 minutes. They hold the scheduling failures, admission rejections, image pull errors and probe failures that explain the incident — and they are gone before most postmortems begin. Capturing them is the first command of an incident, not the last.

Reference

CHEATSHEET

CommandWhat it answers
oc -n openshift-user-workload-monitoring get podsWhether your metrics are being scraped at all
oc get servicemonitor,podmonitor -AWhat has asked to be scraped
oc get prometheusrule -AEvery alert rule, platform and yours
amtool silence queryThe silence that stopped the page
oc get lokistack -n openshift-logging -o yamlRetention and storage sizing
oc get clusterlogforwarder -AWhich log tenants are collected and where they go
oc adm node-logs --role=master --path=kube-apiserver/audit.logWho did what to the API
oc auth can-i --list -n <ns> --as=<user>Effective RBAC, resolved
oc adm policy who-can <verb> <res>The reverse RBAC question
oc adm upgradeWhich versions are on offer right now
oc get apirequestcountWho is still calling a deprecated API
oc get pdb -AThe budget that will block the next drain
oc get compliancecheckresult -n openshift-complianceCurrent compliance failures
oc get backup -n openshift-adpWhether the application backup actually ran
oc get events -A --sort-by=.lastTimestampWhat just happened — expires in an hour