CoursesJenkins foundations, done rightConfiguration as code (JCasC)

Configuration as code (JCasC)

A reproducible, reviewable Jenkins.

Intermediate12 min · lesson 15 of 16

A hand-built Jenkins is a house with no blueprints. You set it up by clicking through Manage Jenkins: pick a security realm on one screen, tick an authorization box on another, paste the instance URL somewhere else, then open each plugin and fill in its settings by hand. That is a hundred small decisions, none of them written down anywhere. Six months later a disk dies, and nobody can say which of those two hundred checkboxes were changed, or why. What you had was a snowflake: unique, hand-made, undocumented, impossible to rebuild. JCasC (Jenkins Configuration as Code, a plugin) ends that. Two terms before the file. A security realm is the identity source, the thing that decides who is allowed to log in at all. An authorization strategy is the rulebook for what each logged-in person may then do. JCasC writes down both of those, plus global tools, agent clouds and every plugin's settings, in one YAML (a plain-text configuration format) file you keep in version control. Jenkins then assembles itself from that file. Standing up a fresh instance stops being a day of remembering and becomes one command that reads a file.

casc/jenkins.yaml
jenkins:
systemMessage: "Managed by Configuration as Code — do not edit via the UI"
numExecutors: 0 # controller runs no builds (agents only)
securityRealm:
local: # Jenkins' own user database
allowsSignup: false # no public self-registration
users:
- id: "admin"
password: "${ADMIN_PASSWORD}" # resolved from env at boot, never inline
authorizationStrategy:
globalMatrix: # matrix authorization: a permission grid
entries:
- group:
name: "authenticated" # every logged-in user
permissions:
- "Overall/Read"
- "Job/Build"
- user:
name: "admin" # one real administrator
permissions:
- "Overall/Administer"
security:
scriptApproval:
approvedSignatures: [] # no pre-approved Groovy — start closed
unclassified:
location:
url: "https://jenkins.acme.internal/"
adminAddress: "[email protected]"

The file is organised under a handful of top-level roots, like labelled drawers in a filing cabinet. jenkins: configures the core node. The controller is Jenkins' central server, the machine that schedules work, serves the web interface and stores every secret. numExecutors: 0 gives that machine zero build slots. (An executor is one build slot, one place a single build can run.) So the controller still hands work out, but it runs none of it; the actual work goes to agents, the separate worker machines connected to it. That is a deliberate security decision. Build code should never run on the box that holds the credentials store. security: configures the security subsystem plugins, script approval among them. unclassified: is the junk drawer, the catch-all for global settings that plugins register but that do not fit a fixed section, which is where the public URL lives. Under authorizationStrategy you see globalMatrix, matrix authorization: a grid with permissions along one axis and identities along the other. Here the built-in authenticated group gets read and build access, and the admin user alone gets Overall/Administer. Naming users and groups as their own explicit entries, rather than using the older one-line short form, avoids the ambiguity warning that newer matrix-auth versions print.

Notice that the password is written as ${ADMIN_PASSWORD} and not as the real thing. This YAML lives in Git, where everyone with repository access can read it, so a literal secret in there is a leak waiting to happen. JCasC resolves ${VAR} at load time, pulling the value from an environment variable or from a mounted secrets file. The file describes the shape of your Jenkins. The actual values are injected at boot and never committed. That separation is the whole credential story for JCasC: the credentials store still holds the secrets, and the YAML only points at them by name.

Apply it at boot

You tell Jenkins where the file is with one environment variable, CASC_JENKINS_CONFIG. It can name a single file, a directory of YAML files that JCasC merges together, or even an HTTP URL. On startup the plugin reads it and applies every setting before Jenkins finishes coming up, so a brand-new container is fully configured the first moment you can reach it. Pass -Djenkins.install.runSetupWizard=false and there is no setup wizard either. No clicking at all. A clean apply is quiet, which catches people out: JCasC logs its per-source detail at FINE, so at the default INFO level you see nothing but Jenkins reporting that it is ready. To find out what actually landed, you export the live config, shown further down. Running the same file twice is safe. JCasC is idempotent, meaning it drives each value to exactly what the YAML says regardless of the current state, so a boot and a reload behave identically. Pointing at a directory is the usual production choice, because you can split security, clouds and tooling into separate files that different people review.

shell
# CASC_JENKINS_CONFIG can be a file, a directory of YAML, or an HTTP URL.
$ docker run -d --name jenkins \
-e CASC_JENKINS_CONFIG=/var/jenkins.d \
-e JAVA_OPTS="-Djenkins.install.runSetupWizard=false" \
-e ADMIN_PASSWORD="$(openssl rand -base64 24)" \
-e CASC_RELOAD_TOKEN="$(openssl rand -hex 24)" \
-v "$PWD/casc:/var/jenkins.d:ro" \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts-jdk21
$ docker logs jenkins 2>&1 | grep -i 'fully up'
console — boot output
9f3c1a2b7e4d0a6c8b5f2d1e... # container id printed by `docker run -d`
# CASC_JENKINS_CONFIG applied silently (its detail logs at FINE); INFO shows only:
INFO hudson.WebAppMain$3#run: Jenkins is fully up and running
# no "Please use the following password to proceed to installation" banner — the
# wizard is off and JCasC already created the admin user.

Reload without a restart

Configuration also applies without downtime. Once a change to jenkins.yaml lands through a pull request, you have three ways to re-apply it. Click Manage Jenkins, then Configuration as Code, then Reload existing configuration. Run the reload-jcasc-configuration command over the Jenkins CLI (command line interface). Or send an HTTP POST, the kind of request that submits data rather than fetching it, to the /reload-configuration-as-code/ endpoint with a shared secret token set through the CASC_RELOAD_TOKEN environment variable, which is what a deploy pipeline usually does. Hold on to this mental model: JCasC does not watch Git and does not poll it. It reads the file at boot, or when you explicitly ask for a reload, and at no other moment. A merged change does nothing to the running instance until one of those happens. A reload also stamps the file back over any drift, so a setting somebody quietly flipped in the web interface snaps back to whatever Git says.

shell
# jenkins.yaml changed via a merged pull request — re-apply WITHOUT a restart.
# 1) token endpoint (what a deploy pipeline calls; POST, no login, just the shared token):
$ curl -sS -X POST -o /dev/null -w 'HTTP %{http_code}\n' \
"https://jenkins.acme.internal/reload-configuration-as-code/?casc-reload-token=$CASC_RELOAD_TOKEN"
# 2) or authenticated over the Jenkins CLI (prints nothing, exit 0 on success):
$ java -jar jenkins-cli.jar -s https://jenkins.acme.internal/ \
-auth admin:$ADMIN_API_TOKEN reload-jcasc-configuration
console — reload output
HTTP 200
# curl printed only the status line above. A successful reload is silent in the
# controller log at the default level; a failure logs at SEVERE
# ("Failed to reload configuration"), so a clean HTTP 200 with no error line
# means the new YAML is now live.

Because the file is the source of truth, every configuration change turns into a code review. Loosening authorization, adding a credentials provider, trusting a new agent cloud: each one shows up as a diff that a named person approved at a recorded time. The audit trail falls out of the workflow instead of being extra paperwork. This pairs directly with the next lesson on backup and recovery. JCasC captures the configuration, which is safe to commit because the secrets in it are only interpolated references, while a JENKINS_HOME backup captures the state and the encrypted secret values. Recovery is then two moves: replay the YAML to rebuild the shape of the instance, and restore JENKINS_HOME for its history and its secrets.

Verify what actually applied

Applying is not the same as verifying, so confirm that the live state matches what you intended. The plugin can dump the running configuration back out as YAML: the Export configuration button at /configuration-as-code/, or a POST to /configuration-as-code/export when you are scripting it. That endpoint accepts POST only, and an API token in the request is exempt from the CSRF crumb (cross-site request forgery token, the anti-forgery check Jenkins normally demands on writes). Diffing that export against your committed file is how you catch a setting some plugin ignored or quietly rewrote. Expect two things. The export is fully expanded, so it includes defaults you never wrote and sorts keys alphabetically, which makes it read noisier than your source. And an invalid key never fails silently. A mistyped attribute throws a ConfiguratorException that names the offending element, and it shows up right in the boot log as your first troubleshooting clue.

shell
# dump the LIVE config (export is POST-only; the API token skips the CSRF crumb),
# then pull out just the section you care about
$ curl -sS -u admin:$ADMIN_API_TOKEN -X POST \
https://jenkins.acme.internal/configuration-as-code/export -o live.yaml
$ yq '.jenkins.authorizationStrategy' live.yaml
console — authorization section of the export
globalMatrix:
entries:
- group:
name: authenticated
permissions:
- Job/Build # exporter sorts permissions alphabetically,
- Overall/Read # so authoring order is not preserved
- user:
name: admin
permissions:
- Overall/Administer

One boundary is worth learning early. JCasC manages global configuration, not content. It sets up security, clouds and tools, but it does not create your pipeline jobs. Those come from Job DSL or, better, from Multibranch pipelines that go and find Jenkinsfiles in your repositories. And it configures only plugins that are already present. It does not install them. Keeping that line clear heads off the two most common beginner failures: waiting for a job to appear out of jenkins.yaml, and expecting the file to fetch a plugin it names.

JCasC applies configuration. It does not install the plugins that configuration needs.
Every block in the file is handled by a configurator, a small piece of code that a plugin brings with it. globalMatrix comes from the matrix-auth plugin; a kubernetes cloud comes from the kubernetes plugin. Name something in jenkins.yaml that no installed plugin understands and the apply throws, which aborts the boot. With matrix-auth missing you get 'No hudson.security.AuthorizationStrategy implementation found for globalMatrix', and the message even lists the strategies that ARE installed. Because the failure happens during boot, anything that restarts the instance for you (Kubernetes, or Compose with a restart policy) will crash-loop the container until you fix it, while a plain docker run comes up broken and stays that way. Install the plugins first: bake a plugins.txt into the image and run jenkins-plugin-cli --plugin-file plugins.txt. Then apply the config. The order is always plugins, then configuration.
Quick check
01You merge a pull request that changes jenkins.yaml, but minutes later the running Jenkins still shows the old authorization settings. What is the most likely explanation?
Incorrect — JCasC never polls Git. Waiting changes nothing.
Correct — The file is read at boot or on an explicit reload, and a merged commit is neither of those.
Incorrect — Bad YAML does not get skipped quietly. It throws a ConfiguratorException that names the offending element.
Incorrect — Nothing about a reload requires deleting JENKINS_HOME, and doing so would throw away your history and secrets.
02A JCasC jenkins.yaml sets jenkins.numExecutors: 0. What does that do, and why would you choose it on purpose for security?
Incorrect — Authentication is set by the security realm and the authorization strategy. Executor count has nothing to do with it.
Correct — The controller keeps scheduling work and stops running it, so untrusted build code never lands next to the secrets.
Incorrect — Agents bring their own executors, so builds keep running. Only the controller stops running them.
Incorrect — That is the opposite of what happens. The setting removes the controller's own build slots and pushes work out to agents.
03Your jenkins.yaml asks for authorizationStrategy: globalMatrix, but the matrix-auth plugin is not in the image. You run it on Kubernetes with CASC_JENKINS_CONFIG set, and the container starts, fails, restarts and keeps looping. What is the cause, and what is the fix?
Incorrect — JCasC never installs plugins, so there is no version for it to get wrong.
Incorrect — That token only guards the reload endpoint. Boot does not need it.
Correct — The configurator ships with the plugin, so the plugin has to be there before the file that uses it.
Incorrect — JCasC does not skip anything silently. A block it cannot handle throws and names what it could not find.

Try this

Work through “Verify what actually applied” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: jCasC applies configuration. It does not install the plugins that configuration needs. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related