Post

Jenkins

Jenkins

Table of Contents

  1. What is Jenkins?
  2. Core Architecture
  3. Jobs and Pipelines
  4. Jenkinsfile — Declarative vs Scripted
  5. Pipeline Stages, Steps & Agents
  6. Build Triggers
  7. Credentials & Secrets Management
  8. Plugins
  9. Shared Libraries
  10. Jenkins vs Other CI/CD Tools
  11. Common Real-World Scenarios
  12. Interview Questions

What is Jenkins?

Jenkins is an open-source, self-hosted automation server written in Java. Its primary job is to automate the repetitive parts of software development — building, testing, and deploying code — whenever a change is pushed to a repository.

This practice is called CI/CD:

  • Continuous Integration (CI) — automatically build and test every code change so problems are caught early.
  • Continuous Delivery (CD) — automatically prepare and deliver that tested code to staging or production environments.

Key characteristics:

  • Self-hosted (you run it on your own infrastructure)
  • Extremely extensible via 1800+ plugins
  • Pipeline-as-code using a Jenkinsfile
  • Supports distributed builds via a Master/Agent model
  • Free and open-source (community + CloudBees enterprise support)

Core Architecture

Master (Controller)

The Jenkins master is the central server. It is responsible for:

  • Serving the web UI
  • Scheduling build jobs
  • Storing configuration, build history, and logs
  • Dispatching work to agents

The master should not run heavy build jobs itself — it should only orchestrate.

Agents (Nodes / Workers)

An agent is a machine (physical, VM, or container) that actually executes the build steps assigned by the master.

  • Agents and master communicate via SSH (master-initiated) or JNLP (agent-initiated). In both cases, the master controls job scheduling and dispatching.
  • You can have many agents — each can be a different OS, different language runtime, etc.
  • Labels are used to target specific agents (e.g. agent { label 'linux' })

Why this matters

This Master/Agent separation allows Jenkins to:

  • Scale horizontally — add more agents as build load grows
  • Isolate environments — run Node.js builds on one agent, Python on another
  • Run jobs in parallel — multiple agents work simultaneously
1
2
3
4
         [ Jenkins Master ]
        /        |         \
  [Agent 1]  [Agent 2]  [Agent 3]
  Linux/JDK  Windows    Docker

Jobs and Pipelines

Freestyle Job

The original Jenkins job type. Configured entirely through the UI — no code. You click buttons to add build steps (run a shell command, archive artifacts, etc.).

Drawbacks:

  • Configuration lives only in Jenkins, not in your repo
  • Hard to review, version, or reproduce
  • Does not scale well for complex workflows

Pipeline Job

A Pipeline job is defined entirely in code using a Jenkinsfile committed to your repository. This is the approach used in all modern Jenkins setups.

Advantages:

  • Version-controlled alongside your application code
  • Reviewable via pull requests
  • Reproducible across environments
  • Supports complex flows — parallel stages, conditionals, approvals

Multibranch Pipeline

Without Multibranch Pipeline, every branch that needs CI requires a manually created Pipeline job in Jenkins — one job for main, another for feature/login, another for bugfix/header, and so on. As teams grow, this becomes unmanageable. A Multibranch Pipeline solves this by acting as a single parent job that scans your repository and automatically creates and manages individual Pipeline jobs — one per branch. Each child Pipeline job reads the Jenkinsfile from its own branch, so jobs are also created and deleted automatically as branches are opened and closed. The underlying pipeline execution is identical to a regular Pipeline job; Multibranch simply removes the manual overhead of managing one job per branch.

Use case: You want every feature branch to have its own CI run automatically when a PR is opened.


Jenkinsfile

The Jenkinsfile is a text file placed at the root of your repository that defines the entire pipeline. Jenkins reads it and executes it.

There are two syntaxes:

Declarative Pipeline

Structured, opinionated, and easier to read. This is what you should use and what most interviews focus on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
pipeline {
    agent any

    environment {
        APP_ENV = 'staging'
    }

    stages {
        stage('Build') {
            steps {
                sh 'npm install'
                sh 'npm run build'
            }
        }
        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh './deploy.sh'
            }
        }
    }

    post {
        success {
            echo 'Pipeline passed!'
        }
        failure {
            mail to: 'team@example.com', subject: 'Build failed'
        }
    }
}

Scripted Pipeline (Legacy)

Pure Groovy code. More flexible but more complex and harder to maintain. You’ll encounter it in older Jenkins setups.

1
2
3
4
5
6
7
8
node {
    stage('Build') {
        sh 'npm install'
    }
    stage('Test') {
        sh 'npm test'
    }
}

Key Difference

  Declarative Scripted
Syntax Structured DSL Pure Groovy
Error handling Built-in post block Manual try/catch
Readability High Medium
Flexibility Medium High
Recommended? Yes Legacy only

Pipeline Stages, Steps & Agents

agent

Defines where the pipeline (or a stage) runs.

1
2
3
4
agent any              // run on any available agent
agent none             // no global agent; each stage must define its own
agent { label 'linux' } // run on agent tagged 'linux'
agent { docker 'node:18' } // spin up a Docker container as the agent

stages and stage

stages is the container for all stages. Each stage represents a logical phase.

1
2
3
4
5
stages {
    stage('Build') { ... }
    stage('Test')  { ... }
    stage('Deploy') { ... }
}

steps

The actual commands inside a stage.

1
2
3
4
5
6
7
steps {
    sh 'echo hello'              // shell command (Linux/Mac)
    bat 'echo hello'             // batch command (Windows)
    echo 'Printing a message'    // log to console
    checkout scm                 // check out source code from SCM
    archiveArtifacts 'build/'  // save build output
}

Parallel Stages

Run multiple stages simultaneously to reduce total build time.

1
2
3
4
5
6
7
8
9
10
stage('Test in parallel') {
    parallel {
        stage('Unit tests') {
            steps { sh 'npm run test:unit' }
        }
        stage('Integration tests') {
            steps { sh 'npm run test:integration' }
        }
    }
}

post

Runs after all stages, regardless of outcome. Used for notifications, cleanup, reporting.

1
2
3
4
5
6
7
post {
    always   { echo 'Runs no matter what' }
    success  { echo 'Only on success' }
    failure  { echo 'Only on failure' }
    unstable { echo 'Tests passed but with warnings' }
    cleanup  { deleteDir() }
}

when

Conditionally run a stage.

1
2
3
4
5
6
7
stage('Deploy to prod') {
    when {
        branch 'main'
        // or: expression { return env.DEPLOY == 'true' }
    }
    steps { sh './deploy-prod.sh' }
}

Build Triggers

How Jenkins knows when to start a build.

1. Poll SCM

Jenkins checks the repository on a schedule (cron syntax). Simple to set up but wastes resources — Jenkins is constantly checking even when nothing changed.

1
2
3
triggers {
    pollSCM('H/5 * * * *')  // check every 5 minutes
}

2. Webhook (Preferred)

GitHub/GitLab sends a push notification directly to Jenkins the moment code is pushed. Instant, efficient, no polling.

Setup: In GitHub → Repository Settings → Webhooks → point to http://your-jenkins/github-webhook/

3. Scheduled (Cron)

Run a build at a fixed time regardless of code changes. Useful for nightly builds or scheduled reports.

1
2
3
triggers {
    cron('0 2 * * *')  // every day at 2am
}

4. Manual

Click “Build Now” in the UI. Or trigger via Jenkins REST API.

5. Upstream Job Trigger

Start a downstream job when an upstream job finishes.

1
2
3
triggers {
    upstream(upstreamProjects: 'my-build-job', threshold: hudson.model.Result.SUCCESS)
}

Credentials & Secrets Management

Never hardcode passwords, tokens, or keys in your Jenkinsfile or pipeline scripts.

Jenkins Credentials Store

Jenkins has a built-in encrypted database where you save secrets once via the UI (Manage Jenkins → Credentials). Instead of putting the actual secret in your Jenkinsfile, you give each secret a nickname (ID) and reference only that ID in your pipeline. At runtime, Jenkins fetches the real value, injects it as a temporary environment variable, and automatically replaces it with `` in console logs so it is never exposed.

Types of credentials you can store:

  • Username + Password
  • Secret text (API token, password)
  • SSH private key
  • Certificate

Injecting Credentials in a Pipeline

There are two ways to inject credentials depending on where you need them.

Inside the environment block — available to all stages:

1
2
3
4
environment {
    // Jenkins creates two variables: DB_CREDS_USR and DB_CREDS_PSW
    DB_CREDS = credentials('database-credentials-id')
}

Inside a specific step using withCredentials — available only within that block:

1
2
3
4
5
steps {
    withCredentials([string(credentialsId: 'my-api-token', variable: 'API_TOKEN')]) {
        sh 'curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com'
    }
}

In both cases your Jenkinsfile never contains the actual secret — only the ID.

HashiCorp Vault Integration

What is Vault?

HashiCorp Vault is a dedicated secrets management tool. Instead of secrets living inside Jenkins, they live in Vault — a centralised, audited, access-controlled secrets store. Jenkins becomes just another application that requests secrets when it needs them.

Why use Vault over Jenkins Credentials Store?

The Jenkins Credentials Store works fine for small teams, but has real problems at scale:

  • Secrets are scattered across many Jenkins instances
  • No audit trail — you cannot see who used a secret and when
  • Rotating a secret means updating it in every Jenkins instance manually
  • If Jenkins is compromised, all secrets stored in it are compromised

Vault solves all of this. Secrets live in one place, every access is logged, and rotating a secret in Vault instantly applies everywhere with no Jenkinsfile changes.

How it works

Jenkins does not store the secret. At runtime it asks Vault for the secret, uses it for the duration of that one pipeline step, then discards it.

  1. Pipeline runs and hits a step that needs a secret
  2. Jenkins calls Vault: “give me the DB password at path secret/my-app/database
  3. Vault checks whether this Jenkins instance is authorised to read that path
  4. If yes, Vault returns the secret temporarily
  5. Jenkins injects it as an environment variable, masked as `` in logs
  6. Step finishes, secret is discarded — never written to disk or stored in Jenkins

Authentication — how Vault trusts Jenkins

Before Vault hands over a secret it needs to verify Jenkins is who it claims to be. Common methods:

  • AppRole — Jenkins is given a Role ID and Secret ID (like a username/password for machines). Most common in practice.
  • Token — A Vault token is stored in the Jenkins Credentials Store. Simple but the token itself becomes a secret you need to manage.
  • AWS/GCP IAM — If Jenkins runs on a cloud VM, Vault trusts the cloud provider’s identity. No credentials needed at all — the cleanest approach.

Using Vault in a Jenkinsfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pipeline {
    agent any
    stages {
        stage('Deploy') {
            steps {
                withVault(
                    vaultSecrets: [[
                        path: 'secret/my-app/database',
                        secretValues: [[
                            envVar: 'DB_PASSWORD',
                            vaultKey: 'password'
                        ]]
                    ]]
                ) {
                    sh './deploy.sh'  // DB_PASSWORD available here, masked in logs
                }
            }
        }
    }
}

Dynamic secrets — Vault’s killer feature

Unlike static passwords in the Jenkins Credentials Store, Vault can generate dynamic secrets — credentials created on demand that automatically expire.

For example, instead of a permanent database password:

  1. Jenkins asks Vault for database credentials
  2. Vault creates a brand new database user with a short TTL (e.g. 1 hour)
  3. Jenkins uses those credentials for the build
  4. After 1 hour, Vault automatically revokes them

Even if the credentials are intercepted mid-build they are useless shortly after. This is something the Jenkins Credentials Store fundamentally cannot do.


Plugins

Jenkins’ functionality is almost entirely delivered through plugins. The core is minimal — plugins add support for Git, Docker, Kubernetes, Slack, and everything else.

Essential Plugins to Know

Plugin Purpose
Git Plugin Clone repositories, checkout branches
Pipeline Plugin Enables Jenkinsfile and pipeline jobs
Credentials Plugin Secure secrets management
Blue Ocean Modern UI for visualizing pipelines
Docker Pipeline Use Docker containers as agents
GitHub Integration Webhook support, PR status updates
Slack Notification Send build status to Slack
JUnit Parse and display test results
SonarQube Scanner Code quality analysis
Kubernetes Plugin Spin up pods as dynamic build agents

Plugin Risks

  • Plugins can become outdated or unmaintained
  • Too many plugins increase attack surface and maintenance burden
  • Always pin plugin versions in production

Shared Libraries

A Shared Library is a repository of reusable Groovy code that multiple Jenkinsfiles across different projects can import. This follows the DRY (Don’t Repeat Yourself) principle.

Why it matters

Without shared libraries, every team duplicates the same boilerplate — deploy scripts, notification logic, Docker build steps. A shared library centralizes this.

Structure of a Shared Library repo

1
2
3
4
5
6
7
shared-library/
├── vars/
│   ├── buildAndPush.groovy   ← callable as a function in Jenkinsfile
│   └── notifySlack.groovy
└── src/
    └── org/example/
        └── Utils.groovy      ← reusable Groovy classes

Using a Shared Library

1
2
3
4
5
6
7
8
9
10
11
12
@Library('my-shared-library') _

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                buildAndPush('my-app', 'v1.0')
            }
        }
    }
}

Shared libraries are configured once in Manage Jenkins → Configure System → Global Pipeline Libraries.


Jenkins vs Other CI/CD Tools

This is a core interview topic. Know the trade-offs.

  Jenkins GitHub Actions GitLab CI CircleCI
Hosting Self-hosted Cloud (GitHub) Cloud / Self-hosted Cloud
Config Jenkinsfile (Groovy) YAML YAML YAML
Ecosystem 1800+ plugins GitHub Marketplace Built-in integrations Orbs
Scalability Manual (agents) Automatic Automatic Automatic
Learning curve High Low Low-Medium Low
Cost Free (infra cost) Free tier + paid Free tier + paid Free tier + paid
Best for Enterprise, complex, legacy GitHub-native projects GitLab users Simple cloud CI

When to choose Jenkins

  • You need on-premise builds (data privacy, compliance)
  • You’re in a large enterprise with complex, multi-team pipelines
  • You need deep integration with legacy systems via plugins
  • You want full control over the build infrastructure

When NOT to choose Jenkins

  • Small team with no dedicated DevOps resource (too much maintenance)
  • Your code is already on GitHub and you want simplicity (use GitHub Actions)
  • You don’t want to manage infrastructure

Common Real-World Scenarios

These are frequently asked as situational/behavioral questions.

Scenario 1: A build passes but the deployed app is broken

Approach:

  1. Check the console output for the deploy stage — was the right artifact deployed?
  2. Verify environment variables were correctly injected
  3. Check if the correct branch/tag was built
  4. Compare the artifact version in prod vs what was tested
  5. Look at post-deploy health checks — were they skipped?

Prevention: Add smoke tests as a pipeline stage after deploy. Gate the pipeline on health check success.

Scenario 2: Flaky tests causing random build failures

Approach:

  1. Use the retry step to re-run the flaky test a fixed number of times
  2. Quarantine the flaky test — mark it skipped, file a ticket, alert the team
  3. Never silently ignore or suppress the failure
1
2
3
4
5
steps {
    retry(3) {
        sh 'npm test'
    }
}

Scenario 3: You need manual approval before deploying to production

Use the input step:

1
2
3
4
5
6
stage('Deploy to Production') {
    steps {
        input message: 'Deploy to production?', ok: 'Yes, deploy'
        sh './deploy-prod.sh'
    }
}

Jenkins pauses the pipeline and waits for a human to click approve in the UI.

Scenario 4: Build times are too slow

Approaches:

  • Use parallel stages for independent test suites
  • Cache dependencies (npm cache, Maven local repo) on the agent
  • Use Docker layer caching
  • Spin up more agents to distribute load
  • Skip unchanged stages using when conditions

Scenario 5: Securing a Jenkins instance

  • Disable the “Allow users to sign up” option
  • Use role-based access control (RBAC) via the Role Strategy plugin
  • Run Jenkins behind a reverse proxy (nginx/Apache) with HTTPS
  • Regularly update Jenkins core and plugins
  • Never expose Jenkins directly to the public internet
  • Use credentials store — never pass secrets as plain environment variables

Interview Questions

Conceptual / Fundamentals

Q1. What is Jenkins and what problem does it solve? Jenkins is a self-hosted automation server that implements CI/CD. It eliminates manual, error-prone build-test-deploy workflows by triggering them automatically on every code change.


Q2. What is the difference between Continuous Integration and Continuous Delivery? CI is the practice of automatically building and testing every code change to catch bugs early. CD extends this by automatically delivering the tested build to a staging or production environment. CI is about code quality; CD is about release speed.


Q3. What is a Jenkinsfile and why should it be in your repo? A Jenkinsfile is a text file that defines the pipeline as code. Keeping it in the repo means the pipeline is version-controlled, reviewable via PRs, and always in sync with the code it builds. If Jenkins is wiped, you can restore the pipeline from the repo.


Q4. What is the difference between Declarative and Scripted pipeline? Declarative uses a structured DSL (pipeline { } block), is easier to read, has built-in error handling via post, and is the current standard. Scripted is pure Groovy — more flexible but harder to maintain. Use Declarative unless you need something Declarative can’t express.


Q5. Explain the Jenkins Master/Agent architecture. The master is the central server that hosts the UI, schedules jobs, and stores history. Agents are worker machines that execute the actual build steps. This allows Jenkins to scale horizontally (add more agents), isolate environments (different OS or runtime per agent), and run jobs in parallel.


Practical / Hands-On

Q6. How do you trigger a Jenkins build automatically when code is pushed? The preferred method is a webhook — GitHub sends a POST request to Jenkins the moment a push happens. The alternative is Poll SCM, where Jenkins checks the repo on a schedule, but this is wasteful. Webhooks are faster and more efficient.


Q7. How do you handle secrets and credentials in Jenkins? Store them in the Jenkins Credentials Store and reference them by ID. Never hardcode secrets in the Jenkinsfile. Use the credentials() binding or withCredentials block to inject them as environment variables at runtime. Jenkins automatically masks these values in logs.


Q8. How would you run tests in parallel in Jenkins? Use the parallel block inside a stage. Each parallel branch runs simultaneously on available agents, reducing total pipeline time. Useful when you have independent test suites (unit, integration, e2e).


Q9. How do you conditionally run a stage — for example, only deploy from the main branch? Use the when directive inside the stage. You can condition on branch name, environment variable, build result, or a custom Groovy expression.


Q10. What is a Shared Library and when would you use it? A Shared Library is a separate Git repo containing reusable Groovy functions that multiple pipelines can import. Use it when multiple teams repeat the same pipeline logic — deploy scripts, Docker build steps, notification functions. It avoids copy-paste and lets you update logic in one place.


Architecture / Design

Q11. How would you design a pipeline for a microservices application with 10 services? Each service lives in its own repository with its own Jenkinsfile. A Multibranch Pipeline job is created per repository — Jenkins automatically manages pipelines for every branch across all 10 services independently.

Common steps shared across all services — build, test, Docker image push, deploy — are extracted into a Shared Library. Each service’s Jenkinsfile calls these shared functions rather than duplicating logic. When the deploy logic changes, you update it in one place and all 10 services pick it up.

Each service pipeline runs and deploys independently by default. Whether a change in one service triggers pipelines in other services depends on the dependency graph between them.

If services are truly independent, a change in Service A only triggers Service A’s pipeline — no reason to test the others. If Service A calls Service B’s API, a change in Service B could break Service A even though Service A’s code did not change. In that case, Service A declares Service B as an upstream job. When Service B’s pipeline passes, Jenkins automatically triggers Service A’s pipeline.

1
2
3
4
// In Service A's Jenkinsfile
triggers {
    upstream(upstreamProjects: 'service-b/main', threshold: hudson.model.Result.SUCCESS)
}
1
2
3
4
5
6
Service B changes
       │
       ▼
[ Service B pipeline ] ── passes ──► triggers [ Service A pipeline ]
                                               [ Service C pipeline ]
                                     (because A and C depend on B)

A separate orchestration pipeline sits on top and is triggered only when a change touches shared infrastructure — API contracts, shared libraries, or base Docker images — where the impact is truly system-wide. It runs full integration tests against the combined system and coordinates the release. Running all 10 pipelines on every single change is wasteful and slows down feedback.


Q12. A build is passing in Jenkins but the deployed app is broken. How do you debug it? Check the console output for the deploy stage. Verify the correct artifact version was deployed. Confirm environment variables were injected properly. Check post-deploy health checks. Compare the build output with what’s actually running in the environment. Add smoke tests as a pipeline stage to catch this automatically next time.


Q13. Your pipelines are taking 45 minutes to complete. How do you reduce that? Parallelise independent test suites. Cache dependencies on the agent between runs. Use Docker layer caching. Increase the number of agents. Use when conditions to skip unchanged stages. Profile which stage takes longest and optimise that first.


Trade-offs / Comparison

Q14. Why would you choose Jenkins over GitHub Actions?

  • Not VCS-locked — Jenkins works with GitHub, GitLab, Bitbucket, or any private Git server. GitHub Actions only works if your code is on GitHub.
  • Complex pipeline logic — GitHub Actions is YAML. Jenkins pipelines are Groovy code — loops, functions, dynamic stage generation. YAML breaks down for complex cross-repo orchestration.
  • Legacy integrations — 1800+ plugins built over 15 years. Better coverage for older internal tools and enterprise systems.

Trade-off: Jenkins requires you to manage infrastructure, upgrades, and plugin compatibility yourself. GitHub Actions offloads all of that. Note that GitHub Actions also supports self-hosted runners, so self-hosting alone is not a reason to pick Jenkins.


Q15. What are the main disadvantages of Jenkins? High maintenance burden — you manage upgrades, security patches, and infrastructure. Configuration-as-code (Groovy DSL) has a steep learning curve. Plugin dependencies can create compatibility issues. For small teams, the overhead often isn’t worth it compared to managed alternatives like GitHub Actions.


Scenario / Behavioral

Q16. How do you prevent a bad deployment from reaching production? Gate each stage on the previous stage’s success. Run unit tests before integration tests, integration before deploy. Add a manual input approval step before production. Run post-deploy smoke tests and roll back automatically on failure. Use feature flags to decouple deployment from release.


Q17. A developer committed secrets to the Jenkinsfile by accident. What do you do? Immediately rotate the exposed secret — assume it’s compromised. Remove it from the file and rewrite Git history to scrub it (git filter-branch or BFG Repo Cleaner). Move the secret to the Jenkins Credentials Store. Add a pre-commit hook or secret scanning tool (like git-secrets or truffleHog) to prevent recurrence.


Q18. How do you handle a flaky test that randomly fails your pipeline? Use retry(n) to attempt the step multiple times before failing. Quarantine the test — disable it in the main pipeline, file a ticket, assign ownership, and route it to a separate “flaky test” pipeline so it doesn’t block the team. Never silently ignore failures or set the build status to “unstable” and merge anyway.


This post is licensed under CC BY 4.0 by the author.