MCO (Machine Config Operator) in OpenShift
What is MCO?
The Machine Config Operator (MCO) is an OpenShift operator that manages the operating system and runtime configuration of cluster nodes. It treats the underlying RHCOS (Red Hat CoreOS) nodes as immutable infrastructure — all node-level changes go through MCO, never manual SSH edits.
Core idea: Just as Kubernetes manages application workloads declaratively, MCO manages node OS configuration declaratively.
MCO Architecture
┌────────────────────────────────────────────────────────────┐│ MCO (Control Plane) ││ ││ ┌─────────────────────────────────────────────────────┐ ││ │ Machine Config Operator (MCO) │ ││ │ Watches MachineConfig & MachineConfigPool │ ││ └──────────────────────┬──────────────────────────────┘ ││ │ ││ ┌──────────────────────▼──────────────────────────────┐ ││ │ Machine Config Controller (MCC) │ ││ │ Renders final config, manages pool updates │ ││ └──────────────────────┬──────────────────────────────┘ ││ │ ││ ┌──────────────────────▼──────────────────────────────┐ ││ │ Machine Config Server (MCS) │ ││ │ Serves Ignition configs to new nodes on boot │ ││ └──────────────────────┬──────────────────────────────┘ │└─────────────────────────┼──────────────────────────────────┘ │ DaemonSet on every node┌─────────────────────────▼───────────────────────────────────┐│ Machine Config Daemon (MCD) ││ Applies config changes on each node, triggers reboots │└─────────────────────────────────────────────────────────────┘
Component Responsibilities
| Component | Role |
|---|---|
| MCO | Top-level operator — manages all sub-components |
| MCC (Controller) | Renders MachineConfigs, manages rolling updates per pool |
| MCS (Server) | HTTP server — serves Ignition config to bootstrapping nodes |
| MCD (Daemon) | DaemonSet on every node — applies changes, validates, reboots |
Core CRDs
1. MachineConfig (MC)
Defines the desired configuration for a node — files, systemd units, kernel arguments, etc.
apiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigmetadata: name: 99-worker-custom-config labels: machineconfiguration.openshift.io/role: worker # targets worker poolspec: config: ignition: version: 3.2.0 # --- Drop files onto nodes --- storage: files: - path: /etc/chrony.conf mode: 0644 overwrite: true contents: source: data:,server%20time.example.com%20iburst%0A # URL-encoded - path: /etc/sysctl.d/99-custom.conf mode: 0644 contents: source: data:,vm.max_map_count%3D262144%0A # --- Systemd units --- systemd: units: - name: kubelet.service enabled: true - name: my-custom-service.service enabled: true contents: | [Unit] Description=My Custom Service After=network.target [Service] ExecStart=/usr/local/bin/my-script.sh Restart=always [Install] WantedBy=multi-user.target # --- Kernel arguments --- kernelArguments: - hugepages=1024 - intel_iommu=on # --- Kernel type --- kernelType: default # default | realtime (for low-latency/NFV) # --- Extensions (RHCOS layering) --- extensions: - usbguard - kernel-devel
2. MachineConfigPool (MCP)
Defines a group of nodes and which MachineConfigs apply to them. MCO applies configs to all nodes in a pool via rolling update.
apiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigPoolmetadata: name: workerspec: # Which nodes belong to this pool machineConfigSelector: matchExpressions: - key: machineconfiguration.openshift.io/role operator: In values: - worker - custom-worker # Which nodes (by label) are in this pool nodeSelector: matchLabels: node-role.kubernetes.io/worker: "" # Pause updates (maintenance window) paused: false # Max nodes updating simultaneously maxUnavailable: 1 # Or percentage: "33%"
Built-in Pools
| Pool | Targets | Notes |
|---|---|---|
master | Control plane nodes | Changes here are critical — be careful |
worker | All worker nodes | Default pool for most changes |
| Custom pool | Specific node subset | For GPU nodes, infra nodes, etc. |
3. Rendered MachineConfig
MCO merges all MachineConfigs targeting a pool into a single rendered-<pool>-<hash> config. This is what actually gets applied to nodes.
# View rendered configsoc get mc | grep rendered# Example output:rendered-master-abc123def456 3.2.0 45mrendered-worker-xyz789ghi012 3.2.0 45m
How MCO Applies Changes — Rolling Update Flow
New/Updated MachineConfig applied │ ▼MCC renders new rendered-<pool>-<hash> │ ▼MCP detects rendered config changed │ ▼MCD on node: drain → apply → reboot → validate │ ├─ One node at a time (maxUnavailable: 1) ├─ Waits for node Ready before next └─ Pool goes Updating → Updated
# Watch rolling update progressoc get mcp# NAME CONFIG UPDATED UPDATING DEGRADED# master rendered-master-abc123 True False False# worker rendered-worker-xyz789 False True False ← updating# Watch node-by-nodeoc get nodes -w
Common MCO Use Cases
1. Add a Custom File to Nodes
apiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigmetadata: name: 99-worker-sysctl labels: machineconfiguration.openshift.io/role: workerspec: config: ignition: version: 3.2.0 storage: files: - path: /etc/sysctl.d/99-worker-performance.conf mode: 0644 overwrite: true contents: source: data:text/plain;charset=utf-8;base64,dm0ubWF4X21hcF9jb3VudD0yNjIxNDQ=
2. Add SSH Keys to Nodes
apiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigmetadata: name: 99-worker-ssh-key labels: machineconfiguration.openshift.io/role: workerspec: config: ignition: version: 3.2.0 passwd: users: - name: core sshAuthorizedKeys: - ssh-rsa AAAAB3NzaC1yc2E... admin@example.com
3. Configure Container Runtime (CRI-O)
apiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigmetadata: name: 99-worker-crio-config labels: machineconfiguration.openshift.io/role: workerspec: config: ignition: version: 3.2.0 storage: files: - path: /etc/crio/crio.conf.d/99-custom.conf mode: 0644 contents: source: data:text/plain,%5Bcrio.runtime%5D%0Alog_level%20%3D%20%22debug%22
4. Real-Time Kernel for Low-Latency Workloads
apiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigmetadata: name: 99-worker-realtime labels: machineconfiguration.openshift.io/role: workerspec: kernelType: realtime
5. Custom MachineConfigPool (Infra Nodes)
# Step 1: Create the poolapiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigPoolmetadata: name: infraspec: machineConfigSelector: matchExpressions: - key: machineconfiguration.openshift.io/role operator: In values: [worker, infra] nodeSelector: matchLabels: node-role.kubernetes.io/infra: ""---# Step 2: Label the nodes# oc label node infra-node-1 node-role.kubernetes.io/infra=# Step 3: Create infra-specific MachineConfigapiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigmetadata: name: 99-infra-config labels: machineconfiguration.openshift.io/role: infraspec: config: ignition: version: 3.2.0
Pausing a MachineConfigPool
Use during maintenance windows to prevent automatic node reboots:
# Pause updates to worker pooloc patch mcp worker --type merge -p '{"spec":{"paused":true}}'# Apply your MachineConfigs (they queue up, nothing applies yet)oc apply -f my-machineconfig.yaml# Resume — all queued changes apply nowoc patch mcp worker --type merge -p '{"spec":{"paused":false}}'
Troubleshooting MCO
Check Pool & Node Status
# Overall pool healthoc get mcp# Degraded detailsoc describe mcp worker | grep -A10 Degraded# Node MCO statusoc get node -o wideoc describe node <node> | grep -i machine
Check MCD Logs on a Node
# Find MCD pod on specific nodeoc get pod -n openshift-machine-config-operator \ -l k8s-app=machine-config-daemon \ -o wide | grep <node-name># Get logsoc logs -n openshift-machine-config-operator \ machine-config-daemon-<id> -c machine-config-daemon# Follow liveoc logs -n openshift-machine-config-operator \ machine-config-daemon-<id> -c machine-config-daemon -f
Common Errors & Fixes
| Error | Cause | Fix |
|---|---|---|
Pool Degraded: True | Config failed to apply on a node | Check MCD logs on degraded node |
Node stuck in NotReady after update | OS/config error post-reboot | SSH to node, check journalctl -u machine-config-daemon |
rendered config not found | MC label mismatch | Verify machineconfiguration.openshift.io/role label |
| Pool updating indefinitely | maxUnavailable too low or node stuck draining | Check pod disruption budgets, force-drain if safe |
| Wrong config applied | Multiple MCs with same file path | Check MC priority via name prefix (00–99) |
| Pool paused, changes not applying | Intentional or forgotten pause | oc patch mcp worker --type merge -p '{"spec":{"paused":false}}' |
MCD Node Annotation — Useful for Debugging
# Check what config a node currently has vs desiredoc get node <node> -o yaml | grep -A5 machineconfiguration# Key annotations:# machineconfiguration.openshift.io/currentConfig ← what's applied# machineconfiguration.openshift.io/desiredConfig ← what should be applied# machineconfiguration.openshift.io/state ← Done / Degraded / Working
MCO Interview Quick-Fire
- Can you SSH into RHCOS nodes and manually edit files? Technically yes, but changes will be overwritten by MCO on next update. All changes must go through MachineConfig.
- What is the naming convention for MachineConfigs? Prefix
00–99controls merge order; lower numbers apply first. e.g.00-master,99-worker-custom. - What happens if two MCs write to the same file? The one with the higher numeric prefix (applied last) wins.
- What triggers a node reboot? Any change to files, systemd units, kernel args, or kernel type managed by MCO.
- Can you apply MCO changes without a reboot? Only for changes that don’t require it (rare). Most OS-level changes require reboot.
- What is RHCOS and why does MCO require it? Red Hat CoreOS — immutable, container-optimized OS. MCO is the only supported way to configure it.
- Where are MCO components running? All in
openshift-machine-config-operatornamespace.