Changing Kernel Parameters on OpenShift Nodes: The MachineConfig Way
You need to change a kernel parameter on all your worker nodes. net.ipv4.ip_forward=1, maybe. Or increase the fs.inotify.max_user_watches limit because your application is watching too many files. Or load a custom kernel module.
How?
On a traditional Linux server, you SSH in, edit /etc/sysctl.conf, run sysctl -p, and you’re done. But you can’t SSH into OpenShift nodes (well, you shouldn’t). They’re managed by the platform. You’re not supposed to touch them directly.
This is where MachineConfig comes in.
What is a MachineConfig?
A MachineConfig is an OpenShift resource that declares what a set of nodes should look like. Files, systemd units, kernel arguments, and more. The API lives under machineconfiguration.openshift.io/v1.
# List existing machine configs
oc get mc
# Check machine config pools (groups of nodes)
oc get mcp
NAME CONFIG UPDATED UPDATING DEGRADED MACHINECOUNT READYMACHINECOUNT UPDATEDMACHINECOUNT DEGRADEDMACHINECOUNT AGE
master rendered-master-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 True False False 3 3 3 0 45d
worker rendered-worker-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 True False False 6 6 6 0 45d
Machine Config Pools (MCPs) group nodes together. By default, you have master and worker. You can create custom pools for specialized nodes (GPU workers, high-memory nodes, etc.).
Changing a kernel parameter
Let’s say you need to increase fs.inotify.max_user_watches. Here’s the MachineConfig:
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfig
metadata:
labels:
machineconfiguration.openshift.io/role: worker
name: 99-worker-inotify-tuning
spec:
config:
ignition:
version: 3.2.0
storage:
files:
- path: /etc/sysctl.d/99-inotify.conf
mode: 0644
contents:
source: data:,fs.inotify.max_user_watches%3D524288%0Afs.inotify.max_user_instances%3D1024%0A
A few things to notice:
- The label
machineconfiguration.openshift.io/role: workertargets worker nodes. Usemasterfor control plane nodes. - The name starts with
99-. Machine configs are applied in alphabetical order. Names starting with higher numbers override lower ones. The convention isXX-<role>-<description>. - Ignition is the config format. OpenShift uses Ignition (originally from CoreOS) to configure nodes at boot.
- The file contents are a data URL. The
%3Dand%0Aare URL-encoded=and newlines.
When you apply this MachineConfig, the Machine Config Operator (MCO) will:
- Mark the worker MCP as “Updating”
- Drain one node at a time
- Apply the new configuration
- Reboot the node
- Wait for the node to come back
- Move to the next node
This is a rolling update. Your workloads keep running on the other nodes while each one reboots.
Butane: a friendlier format
Writing Ignition configs by hand is painful. The URL-encoded data URIs are unreadable. Enter Butane (formerly called FCC, Fedora CoreOS Config).
Butane is a YAML-based format that gets compiled into Ignition configs. Install it:
# Download butane
curl -LO https://github.com/coreos/butane/releases/download/v0.19.0/butane-x86_64-unknown-linux-gnu
chmod +x butane-x86_64-unknown-linux-gnu
mv butane-x86_64-unknown-linux-gnu /usr/local/bin/butane
Write your config in Butane format:
# 99-worker-inotify-tuning.bu
variant: openshift
version: 4.14.0
metadata:
labels:
machineconfiguration.openshift.io/role: worker
name: 99-worker-inotify-tuning
storage:
files:
- path: /etc/sysctl.d/99-inotify.conf
mode: 0644
contents:
inline: |
fs.inotify.max_user_watches=524288
fs.inotify.max_user_instances=1024
Then compile it:
butane -d . 99-worker-inotify-tuning.bu -o 99-worker-inotify-tuning.yaml
The output is a valid MachineConfig YAML with properly encoded Ignition content. Much more readable source.
More useful MachineConfigs
Loading a kernel module:
variant: openshift
version: 4.14.0
metadata:
labels:
machineconfiguration.openshift.io/role: worker
name: 99-worker-load-br-netfilter
storage:
files:
- path: /etc/modules-load.d/br-netfilter.conf
mode: 0644
contents:
inline: |
br-netfilter
Adding a custom CA certificate:
variant: openshift
version: 4.14.0
metadata:
labels:
machineconfiguration.openshift.io/role: worker
name: 99-worker-custom-ca
storage:
files:
- path: /etc/pki/ca-trust/source/anchors/internal-ca.crt
mode: 0644
contents:
local: internal-ca.crt
(With -d . flag, Butane reads internal-ca.crt from the current directory and embeds it.)
Setting a custom container runtime config (cgroup v2, for example):
apiVersion: machineconfiguration.openshift.io/v1
kind: ContainerRuntimeConfig
metadata:
name: enable-cgroupsv2
spec:
machineConfigPoolSelector:
matchLabels:
pools.operator.machineconfiguration.openshift.io/worker: ""
containerRuntimeConfig:
logLevel: debug
The drain and reboot cycle
When you apply a MachineConfig, nodes drain and reboot. This is disruptive. Plan for it.
The MCO respects PodDisruptionBudgets. If a PDB says “at least 2 replicas must be available,” the MCO won’t drain a node if it would violate that. This is why PDBs matter beyond just kubectl drain.
Control how many nodes update at once with maxUnavailable on the MachineConfigPool:
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineConfigPool
metadata:
name: worker
spec:
maxUnavailable: 2
machineConfigSelector:
matchLabels:
pools.operator.machineconfiguration.openshift.io/worker: ""
nodeSelector:
matchLabels:
node-role.kubernetes.io/worker: ""
By default, maxUnavailable is 1. Setting it to 2 means two nodes will drain and reboot simultaneously. Faster updates, but more capacity loss at once.
Checking the status
# Overall pool status
oc get mcp
# Detailed status of a specific pool
oc describe mcp worker
# Check which MachineConfig a node is using
oc get node worker-01 -o jsonpath='{.metadata.machineconfiguration\.openshift\.io/currentConfig}'
# See if an update is in progress
oc get mcp worker -o jsonpath='{.status.conditions[?(@.type=="Updating")].status}'
A healthy update cycle looks like: UPDATING=True then UPDATING=False with DEGRADED=False. If DEGRADED=True, something went wrong. Check the MCO logs:
oc logs -n openshift-machine-config-operator deployment/machine-config-operator
What goes wrong
Don’t name your MachineConfig after an existing one. If you create a file that overrides a system-managed file, you’ll break things. Use high-numbered prefixes (99-) and descriptive names.
MachineConfigs are immutable for a node. You can’t edit a MachineConfig and have nodes pick up the change. You create a new MachineConfig with different content. The MCO applies the combined state of all matching MachineConfigs.
The reboot is real. Nodes actually reboot. This takes 5-10 minutes per node. On a 6-node worker pool with maxUnavailable: 1, that’s 30-60 minutes for the full rollout. Schedule this during maintenance windows.
Custom MachineConfigPools need careful labeling. If you create a custom pool (like a GPU pool), make sure nodes can only belong to one pool. Overlapping pools cause unpredictable behavior.
The broader picture
MachineConfig is OpenShift’s answer to “how do I manage nodes as code?” It’s the same philosophy as Terraform or Ansible, but integrated into the Kubernetes API. You declare what nodes should look like, and the operator makes it happen.
In my current project, we use MachineConfigs for everything from sysctl tuning to custom CA certificates to container runtime configuration. The beauty is that it’s declarative. The node state lives in Git as MachineConfig YAMLs. If someone manually changes a node file, the MCO reverts it on the next sync.
It’s not perfect. The reboot cycle is slow, and you can’t do hot-reload of kernel parameters (the node must reboot for most changes). But it’s reliable, it’s auditable, and it keeps our nodes consistent. For a regulated environment, that matters.
Related Posts
OpenShift Switched to OVN-Kubernetes. Here's What Changed.
OpenShift switched from OpenShift SDN to OVN-Kubernetes. Here's what changed and what you need to know.
How Your Developers Log Into OpenShift (And Why kubeadmin Isn't Enough)
How do your developers log into the OpenShift console? If the answer is kubeadmin, we need to talk.
Installing Operators on OpenShift: Skip the Helm Chart, Use OperatorHub
You need to install a database operator. In vanilla Kubernetes, you'd use Helm. In OpenShift, there's a better way.