DeployU
Interviews / DevOps & Cloud Infrastructure / Jenkins configuration is manual and inconsistent. Implement Configuration as Code for reproducibility.

Questions

Jenkins configuration is manual and inconsistent. Implement Configuration as Code for reproducibility.

practical Configuration as Code Interactive Quiz Code Examples

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.

Wrong Approach

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.

Right Approach

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).text

Step 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_configs

Step 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 AreaJCasC SectionSecrets Handling
System settingsjenkins:Environment vars
Security realmjenkins.securityRealmVault/K8s secrets
Authorizationjenkins.authorizationStrategyN/A
Credentialscredentials:External secrets
Clouds (K8s/EC2)jenkins.cloudsService accounts
Toolstool:N/A
PluginsUse plugins.txtN/A
Global librariesunclassified.globalLibrariesGit credentials

Practice Question

What is the recommended way to handle secrets in Jenkins Configuration as Code?