Jenkins
Each major section below ends with a quick knowledge check — try it before scrolling past.
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:
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:
docker. sh 'docker build -t ${APP_NAME}:${BUILD_NUMBER} .' builds the image.
go test ./... runs. Its post { always { ... } } publishes test results whether the tests passed or failed.
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.
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?
when { expression { params.DRY_RUN == false } } evaluates false, so Jenkins never enters the stage. A skipped stage isn't a failed one: if every stage that did run succeeded, the pipeline's post { success } block still fires.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?
def variable is local to the script block that created it. Only what gets assigned to env.* — here env.APP_VERSION and env.DOCKER_TAG — is available to later stages.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?
**** instead. That masking is exactly why credentials go through withCredentials instead of being hardcoded in the Jenkinsfile.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?
runGoTests(). Every file under vars/ becomes a global pipeline function named after the file; its call() method is what runs when that function name is used, the same way deployToECS(...) works for vars/deployToECS.groovy.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.