Questions
Jenkins configuration is manual and inconsistent. Implement Configuration as Code for reproducibility.
The Scenario
Your Jenkins environment has grown organically:
Current Problems:
- 3 Jenkins instances (dev, staging, prod) with different configurations
- No documentation of what plugins are installed or their versions
- Configuration changes made via UI are not tracked
- Disaster recovery takes days to manually reconfigure
- "It works on the dev Jenkins" is a common complaint
A recent incident where the production Jenkins was misconfigured caused a 4-hour outage. Leadership wants reproducible, version-controlled infrastructure.
The Challenge
Implement Jenkins Configuration as Code (JCasC) to manage all Jenkins configuration declaratively, enabling version control, peer review, and automated deployment.
A junior engineer might just take screenshots of the UI configuration, use Jenkins backup plugins without version control, or partially implement JCasC without covering credentials and plugins. These approaches don't provide reproducibility, aren't version-controlled, and leave gaps in configuration management.
A senior engineer implements comprehensive JCasC covering system config, security, credentials (with secret management), plugins (with pinned versions), and jobs. They set up GitOps workflow for config changes with PR reviews and automated deployment.
Step 1: Export Current Configuration
// Script to export current configuration (run in Script Console)
import io.jenkins.plugins.casc.ConfigurationAsCode
import java.nio.file.Files
import java.nio.file.Paths
// Export current configuration to YAML
def configPath = "/tmp/jenkins-config-export.yaml"
ConfigurationAsCode.get().export(new FileOutputStream(configPath))
println "Configuration exported to: ${configPath}"
println new File(configPath).textStep 2: Create Comprehensive JCasC Configuration
# jenkins.yaml - Main configuration file
jenkins:
systemMessage: "Jenkins - Managed by Configuration as Code"
numExecutors: 0 # No builds on master
mode: EXCLUSIVE
# Security configuration
securityRealm:
ldap:
configurations:
- server: "${LDAP_SERVER}"
rootDN: "dc=company,dc=com"
userSearchBase: "ou=users"
userSearchFilter: "uid={0}"
groupSearchBase: "ou=groups"
managerDN: "${LDAP_MANAGER_DN}"
managerPasswordSecret: "${LDAP_MANAGER_PASSWORD}"
authorizationStrategy:
roleBased:
roles:
global:
- name: "admin"
permissions:
- "Overall/Administer"
entries:
- group: "jenkins-admins"
- name: "developer"
permissions:
- "Overall/Read"
- "Job/Build"
- "Job/Read"
- "Job/Cancel"
entries:
- group: "developers"
# Global properties
globalNodeProperties:
- envVars:
env:
- key: "COMPANY_REGISTRY"
value: "registry.company.com"
- key: "DEPLOY_ENV"
value: "${DEPLOY_ENVIRONMENT:-development}"
# Agent configuration
nodes:
- permanent:
name: "static-agent-01"
remoteFS: "/var/jenkins"
numExecutors: 4
launcher:
ssh:
host: "agent-01.company.com"
credentialsId: "jenkins-ssh-key"
sshHostKeyVerificationStrategy: "knownHostsFileKeyVerificationStrategy"
labelString: "linux docker"
clouds:
- kubernetes:
name: "kubernetes"
serverUrl: "${K8S_SERVER_URL}"
namespace: "jenkins"
credentialsId: "k8s-service-account"
jenkinsUrl: "http://jenkins:8080"
jenkinsTunnel: "jenkins-agent:50000"
containerCapStr: "50"
templates:
- name: "default"
label: "kubernetes"
containers:
- name: "jnlp"
image: "jenkins/inbound-agent:latest"
resourceRequestMemory: "256Mi"
resourceLimitMemory: "512Mi"
# Credentials configuration
credentials:
system:
domainCredentials:
- credentials:
- usernamePassword:
id: "github-credentials"
username: "${GITHUB_USERNAME}"
password: "${GITHUB_TOKEN}"
description: "GitHub API credentials"
- string:
id: "slack-token"
secret: "${SLACK_BOT_TOKEN}"
description: "Slack notification token"
- file:
id: "kubeconfig-prod"
fileName: "kubeconfig"
secretBytes: "${base64:${KUBECONFIG_PROD}}"
description: "Production Kubernetes config"
- basicSSHUserPrivateKey:
id: "jenkins-ssh-key"
username: "jenkins"
privateKeySource:
directEntry:
privateKey: "${SSH_PRIVATE_KEY}"
description: "SSH key for agent connections"
# Unclassified (plugin configurations)
unclassified:
location:
url: "${JENKINS_URL}"
adminAddress: "jenkins-admin@company.com"
gitHubPluginConfig:
configs:
- name: "GitHub"
apiUrl: "https://api.github.com"
credentialsId: "github-credentials"
manageHooks: true
slackNotifier:
teamDomain: "company"
tokenCredentialId: "slack-token"
room: "#jenkins-notifications"
globalLibraries:
libraries:
- name: "company-shared-library"
retriever:
modernSCM:
scm:
git:
remote: "https://github.com/company/jenkins-shared-library.git"
credentialsId: "github-credentials"
defaultVersion: "main"
implicit: true
# Tool installations
tool:
maven:
installations:
- name: "maven-3.9"
properties:
- installSource:
installers:
- maven:
id: "3.9.4"
nodejs:
installations:
- name: "node-18"
properties:
- installSource:
installers:
- nodeJSInstaller:
id: "18.17.1"
npmPackagesRefreshHours: 72
jdk:
installations:
- name: "jdk-17"
properties:
- installSource:
installers:
- adoptOpenJdkInstaller:
id: "jdk-17.0.7+7"Step 3: Manage Plugins Declaratively
# plugins.yaml - Pin plugin versions
plugins:
- artifactId: configuration-as-code
version: "1714.v09593e830cfa"
- artifactId: job-dsl
version: "1.84"
- artifactId: workflow-aggregator
version: "596.v8c21c963d92d"
- artifactId: git
version: "5.2.0"
- artifactId: github
version: "1.37.1"
- artifactId: kubernetes
version: "3900.va_dce992317b_4"
- artifactId: role-strategy
version: "633.v836e5b_3e80a_5"
- artifactId: credentials-binding
version: "631.v861c6e55c903"
- artifactId: slack
version: "664.vc9a_90f8b_c24a_"
- artifactId: blueocean
version: "1.27.5"// Dockerfile for Jenkins with pinned plugins
FROM jenkins/jenkins:lts-jdk17
# Skip setup wizard
ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"
# Install plugins from plugins.txt
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
# Copy JCasC configuration
COPY jenkins.yaml /var/jenkins_home/casc_configs/
ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc_configsStep 4: Create GitOps Workflow
# .github/workflows/jenkins-config.yaml
name: Deploy Jenkins Configuration
on:
push:
branches: [main]
paths:
- 'jenkins/**'
pull_request:
branches: [main]
paths:
- 'jenkins/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate YAML syntax
run: |
pip install yamllint
yamllint jenkins/*.yaml
- name: Validate JCasC schema
run: |
docker run --rm \
-v $(pwd)/jenkins:/workspace \
jenkins/jenkins:lts-jdk17 \
java -jar /usr/share/jenkins/jenkins.war \
--httpPort=-1 \
--argumentsRealm.passwd.admin=admin \
--argumentsRealm.roles.admin=admin \
-Dcasc.jenkins.config=/workspace/jenkins.yaml \
-Dcasc.validateOnStartup=true \
|| true
deploy-staging:
needs: validate
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to Staging Jenkins
run: |
curl -X POST \
-u "${{ secrets.JENKINS_USER }}:${{ secrets.JENKINS_TOKEN }}" \
"${{ secrets.STAGING_JENKINS_URL }}/configuration-as-code/reload"
deploy-production:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to Production Jenkins
run: |
curl -X POST \
-u "${{ secrets.JENKINS_USER }}:${{ secrets.JENKINS_TOKEN }}" \
"${{ secrets.PROD_JENKINS_URL }}/configuration-as-code/reload"Step 5: Handle Secrets Securely
# Using environment variables (injected at runtime)
jenkins:
systemMessage: "Environment: ${ENVIRONMENT}"
credentials:
system:
domainCredentials:
- credentials:
# Reference secrets from environment
- string:
id: "api-key"
secret: "${API_KEY}"
# Or from HashiCorp Vault
- vaultStringCredentialBinding:
id: "vault-secret"
path: "secret/data/jenkins/api-key"
vaultKey: "value"// Kubernetes ConfigMap and Secret for Jenkins
apiVersion: v1
kind: ConfigMap
metadata:
name: jenkins-casc-config
data:
jenkins.yaml: |
jenkins:
systemMessage: "Production Jenkins"
# ... rest of config
---
apiVersion: v1
kind: Secret
metadata:
name: jenkins-secrets
type: Opaque
data:
GITHUB_TOKEN: <base64-encoded>
SLACK_BOT_TOKEN: <base64-encoded>
LDAP_MANAGER_PASSWORD: <base64-encoded>Step 6: Configuration Reload Pipeline
// Jenkinsfile for configuration updates
pipeline {
agent any
triggers {
githubPush() // Trigger on config repo push
}
stages {
stage('Checkout Config') {
steps {
git(
url: 'https://github.com/company/jenkins-config.git',
credentialsId: 'github-credentials',
branch: 'main'
)
}
}
stage('Validate Configuration') {
steps {
script {
// Validate YAML
sh 'yamllint jenkins.yaml'
// Dry run validation
def validation = httpRequest(
url: "${JENKINS_URL}/configuration-as-code/checkNewSource",
httpMode: 'POST',
authentication: 'jenkins-api-credentials',
contentType: 'APPLICATION_JSON',
requestBody: readFile('jenkins.yaml')
)
if (validation.status != 200) {
error "Configuration validation failed"
}
}
}
}
stage('Apply Configuration') {
steps {
script {
httpRequest(
url: "${JENKINS_URL}/configuration-as-code/reload",
httpMode: 'POST',
authentication: 'jenkins-api-credentials'
)
}
}
}
stage('Verify') {
steps {
script {
// Wait for Jenkins to reload
sleep(30)
// Verify Jenkins is healthy
def health = httpRequest(
url: "${JENKINS_URL}/api/json",
authentication: 'jenkins-api-credentials'
)
if (health.status != 200) {
error "Jenkins health check failed after config reload"
}
}
}
}
}
post {
failure {
slackSend(
channel: '#jenkins-alerts',
color: 'danger',
message: "JCasC update failed: ${env.BUILD_URL}"
)
}
success {
slackSend(
channel: '#jenkins-ops',
color: 'good',
message: "JCasC configuration updated successfully"
)
}
}
} JCasC Coverage Checklist
| Configuration Area | JCasC Section | Secrets Handling |
|---|---|---|
| System settings | jenkins: | Environment vars |
| Security realm | jenkins.securityRealm | Vault/K8s secrets |
| Authorization | jenkins.authorizationStrategy | N/A |
| Credentials | credentials: | External secrets |
| Clouds (K8s/EC2) | jenkins.clouds | Service accounts |
| Tools | tool: | N/A |
| Plugins | Use plugins.txt | N/A |
| Global libraries | unclassified.globalLibraries | Git credentials |
Practice Question
What is the recommended way to handle secrets in Jenkins Configuration as Code?