Jenkins

Each major section below ends with a quick knowledge check — try it before scrolling past.

0/0 checks

Architecture

graph TD
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef red fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef orange fill:#e67e22,stroke:#d35400,color:#fff
    classDef purple fill:#9b59b6,stroke:#8e44ad,color:#fff
    classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
    classDef dark fill:#2c3e50,stroke:#1a252f,color:#fff
    classDef yellow fill:#f39c12,stroke:#d68910,color:#000
    classDef k8s fill:#326ce5,stroke:#254ea8,color:#fff
    classDef aws fill:#ff9900,stroke:#cc7a00,color:#000
    CONTROLLER["Jenkins Controller (master) Orchestrates jobs, stores config, UI"]:::purple --> AGENT1["Agent 1 (docker)"]:::blue
    CONTROLLER --> AGENT2["Agent 2 (kubernetes pod)"]:::k8s
    CONTROLLER --> AGENT3["Agent 3 (EC2 spot instance)"]:::orange

    subgraph Job["Pipeline Job"]
        SCM["Poll SCM or webhook trigger"]:::orange --> CHECKOUT["Checkout from git"]:::green
        CHECKOUT --> STAGES["Run stages on agent"]:::blue
        STAGES --> ARCHIVE["Archive artifacts / publish results"]:::green
    end

Key concepts:

  • Controller — the Jenkins server: schedules jobs, stores state, serves UI. Never run builds on the controller itself.
  • Agent — worker nodes where builds actually run. Can be static (always-on EC2) or dynamic (K8s pod spawned per build, deleted when done).
  • Executor — a slot on an agent. One executor = one concurrent build on that agent.

Same flow, walked through one step at a time instead of all at once:

1. Trigger. A poll-SCM check or an incoming webhook reaches the controller for a configured job.
2. Controller dispatches to an agent. The controller matches the job against an available agent (docker container, Kubernetes pod, or EC2 spot instance) and claims a free executor on it — it never runs the build itself.
3. Checkout. The agent checks out the source from git.
4. Run stages. The pipeline's stages execute on that agent, occupying its claimed executor slot for the duration.
5. Archive & report. Artifacts are archived and results published back to the controller; the executor frees up for the next job.

Why does the controller hand jobs off to agents (docker, Kubernetes pod, EC2 spot) instead of running builds itself?


Declarative Pipeline

pipeline {
    // Where to run — any available agent, or specify label
    agent { label 'docker' }

    // Environment variables available to all stages
    environment {
        APP_NAME = "my-service"
        ECR_REPO = "123456789.dkr.ecr.us-east-1.amazonaws.com/my-service"
    }

    // Build parameters — shown in UI, passed to builds
    parameters {
        string(name: 'ENVIRONMENT', defaultValue: 'staging', description: 'Target environment')
        choice(name: 'REGION', choices: ['us-east-1', 'eu-west-1'], description: 'AWS Region')
        booleanParam(name: 'DRY_RUN', defaultValue: false, description: 'Skip actual deploy')
    }

    stages {
        stage('Build') {
            steps {
                sh 'docker build -t ${APP_NAME}:${BUILD_NUMBER} .'
            }
        }

        stage('Test') {
            steps {
                sh 'go test ./...'
            }
            post {
                always {
                    publishTestResults testResultsPattern: 'test-results/*.xml'
                }
            }
        }

        stage('Deploy') {
            when {
                expression { params.DRY_RUN == false }
            }
            steps {
                sh "deploy.sh --env ${params.ENVIRONMENT} --region ${params.REGION}"
            }
        }
    }

    post {
        success { slackSend message: "Build ${BUILD_NUMBER} succeeded" }
        failure { slackSend message: "Build ${BUILD_NUMBER} FAILED" }
    }
}

Execution order for the pipeline above, stepped through:

1. Build. Runs on the agent labeled docker. sh 'docker build -t ${APP_NAME}:${BUILD_NUMBER} .' builds the image.
2. Test. go test ./... runs. Its post { always { ... } } publishes test results whether the tests passed or failed.
3. Deploy — conditional. when { expression { params.DRY_RUN == false } } decides whether this stage runs at all. Set DRY_RUN=true and the stage is skipped outright, not failed.
4. Pipeline post block. Once every stage that was going to run has finished, post { success / failure } fires exactly one of the two and sends a Slack message either way.

A build is triggered with DRY_RUN=true. What happens to the "Deploy" stage?


Dynamic Variables and Script Block

// Set variables during pipeline execution using script block
stage('Get Version') {
    steps {
        script {
            // sh with returnStdout captures output as string
            def gitTag = sh(returnStdout: true, script: 'git describe --tags --abbrev=0').trim()
            def gitSha = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()

            // Set as env vars — available to all subsequent stages
            env.APP_VERSION = gitTag ?: "dev-${gitSha}"
            env.DOCKER_TAG  = "${env.APP_VERSION}-${env.BUILD_NUMBER}"
        }
    }
}

stage('Build Image') {
    steps {
        // Use the dynamically set variable
        sh "docker build -t ${ECR_REPO}:${env.DOCKER_TAG} ."
    }
}

gitTag is set with plain def gitTag = sh(...) inside the script block of the "Get Version" stage. Can the "Build Image" stage's steps use gitTag directly?


withCredentials — Secrets Injection

Secrets are stored in Jenkins Credentials store, never in Jenkinsfile. withCredentials injects them as env vars, auto-masked in all logs.

stage('Deploy to AWS') {
    steps {
        withCredentials([
            // Single string secret (API key, token)
            string(credentialsId: 'SLACK_WEBHOOK', variable: 'SLACK_URL'),

            // Username + password (Docker registry, DB)
            usernamePassword(
                credentialsId: 'ECR_CREDENTIALS',
                usernameVariable: 'AWS_ACCESS_KEY_ID',
                passwordVariable: 'AWS_SECRET_ACCESS_KEY'
            ),

            // SSH key (deploy to server)
            sshUserPrivateKey(
                credentialsId: 'DEPLOY_SSH_KEY',
                keyFileVariable: 'SSH_KEY_FILE'
            )
        ]) {
            sh '''
                aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ECR_REPO
                docker push $ECR_REPO:$DOCKER_TAG
            '''
            // $SLACK_URL and AWS credentials are masked in logs — shown as ****
        }
    }
}

A step accidentally runs echo $AWS_SECRET_ACCESS_KEY after it was injected via withCredentials. What shows up in the console log?


Shared Libraries

Shared libraries let you define common functions once and reuse them across all pipelines. Stored in a separate git repo.

jenkins-shared-library/
├── vars/
│   ├── deployToECS.groovy     # global function: deployToECS(...)
│   └── runGoTests.groovy      # global function: runGoTests()
└── src/
    └── org/company/
        └── Utils.groovy       # class-based helpers
// vars/deployToECS.groovy
def call(Map config) {
    def env     = config.environment ?: 'staging'
    def service = config.service
    def image   = config.image

    sh "aws ecs update-service --cluster ${env} --service ${service} --force-new-deployment"
    sh "aws ecs wait services-stable --cluster ${env} --services ${service}"
    echo "Deployed ${image} to ${service} in ${env}"
}
// Jenkinsfile in any repo — import the shared library
@Library('jenkins-shared-library') _

pipeline {
    agent any
    stages {
        stage('Deploy') {
            steps {
                // Call the shared library function
                deployToECS(
                    environment: 'production',
                    service:     'my-service',
                    image:       "${ECR_REPO}:${env.DOCKER_TAG}"
                )
            }
        }
    }
}

You add a new file vars/runGoTests.groovy with a def call() { ... } inside. How do you invoke it from any Jenkinsfile that imports the library?


Pipeline Patterns

Matrix builds (test across multiple versions)

stage('Test Matrix') {
    matrix {
        axes {
            axis { name 'GO_VERSION'; values '1.21', '1.22', '1.23' }
            axis { name 'OS'; values 'linux', 'darwin' }
        }
        stages {
            stage('Test') {
                steps {
                    sh "GOOS=${OS} go test ./..."
                }
            }
        }
    }
}

Parallel stages

stage('Test and Scan') {
    parallel {
        stage('Unit Tests') { steps { sh 'go test ./...' } }
        stage('Security Scan') { steps { sh 'trivy image ${APP_NAME}:${BUILD_NUMBER}' } }
        stage('Lint') { steps { sh 'golangci-lint run ./...' } }
    }
}

Manual approval gate

stage('Deploy to Production') {
    input {
        message "Deploy ${env.APP_VERSION} to production?"
        ok "Deploy"
        parameters {
            string(name: 'DEPLOY_NOTE', defaultValue: '', description: 'Deployment note')
        }
    }
    steps {
        sh "deploy.sh --env prod"
    }
}

Jenkins on AWS ECS: Master-Agent Architecture

For the deep-dive into running Jenkins agents as ECS tasks (Fargate + EC2 launch types), including:

  • How the inbound agent JNLP protocol works internally
  • ECS task provisioning flow (sequence diagrams)
  • Scaling mechanics (1 job = 1 ECS task)
  • Custom Docker images per job type
  • 60% cost reduction and 50% build time improvement breakdown
  • Fargate vs EC2 decision matrix, SOCI, Golden AMI, S3 caching

See ecs-agents.md.