HomeSource & deliveryCI/CD & Jenkins
Day 18 · 19 · 20 · 21 + the end-to-end projects — how code reaches customers

CI/CD & Jenkins

CI/CD is the machine that carries your code from a developer's laptop to a customer on the other side of the world — tested, scanned, and deployed automatically. This chapter rebuilds the whole CI/CD week into a workshop: first you understand what a pipeline is and the tools that run it, then you get hands-on installing Jenkins, running pipelines on Docker agents, writing a GitHub Actions workflow, and building a full end-to-end pipeline with SonarQube and Argo CD, then you get quizzed the way an interviewer would, and finally you face the senior delivery scenarios.

① Theory② Lab③ Interview Q&A④ Scenario drills
Day 18 · What is CI/CD? Legacy vs modern Day 19 · Jenkins Zero-to-Hero (Docker agents, projects) Day 20 · GitHub Actions & Actions vs Jenkins Day 21 · CI/CD interview questions Projects · Self-hosted runners + the Ultimate CI/CD pipeline
⚡ The whole of CI/CD in one mental model

CI/CD is the automation that delivers your application from your laptop to your customer. It is two halves. Continuous Integration (CI) is everything that happens before delivery — building the code, running the tests, scanning it for bugs and security holes, and producing a deployable artifact. Continuous Delivery (CD) is deploying that artifact onto a platform your customer can actually reach. Done by hand, this takes months; automated into a pipeline, it takes minutes. A tool like Jenkins orchestrates the whole thing, and a modern alternative like GitHub Actions does the same job without any servers to maintain.

Developercommits code Git (VCS)GitHub / GitLab CI — Jenkins / Actionsbuild · test · scan→ artifact / image Registrythe image CD → Kubernetesthe customer a commit triggers the pipeline; the pipeline builds, tests, and ships the artifact
The delivery machine. A commit to Git triggers the CI pipeline, which builds, tests, and scans the code into an artifact; the CD half then deploys that artifact to the platform the customer uses.

Two big ideas carry the whole chapter. First, Jenkins is an orchestrator that integrates other tools — Maven to build, SonarQube to scan, Argo CD to deploy — and the modern way to run its work is on Docker containers as agents, created on demand and deleted afterwards, so you never pay for idle servers. Second, the CI and CD halves are decoupled: CI ends when a new image is pushed to a registry, and a GitOps tool like Argo CD then watches a Git repository of Kubernetes manifests and deploys any change it sees — which is how you deploy reliably and keep the cluster matching what is in Git.

01
Phase 1 · Theory

What a pipeline is, and the tools that run it

First what CI/CD means and the standard stages every pipeline runs, then Jenkins as an orchestrator with Docker agents, then the modern GitHub Actions alternative, and finally the GitOps model that connects continuous integration to continuous delivery.

T1

What CI/CD is, and why it exists

The simplest way to understand CI/CD is to follow how an application gets from the person who wrote it to the person who uses it.

Imagine you are an application developer working on your laptop, and your customer is sitting somewhere on the other side of the world. Your application has to travel from your laptop to that customer in a way that is efficient and reliable — which means it has to be tested, scanned for security vulnerabilities, checked against your organisation's standards, and finally deployed somewhere the customer can reach it. Every organisation, whether a startup or a huge company, goes through this same journey. CI/CD is the process that automates it.

The name has two halves. Continuous Integration (CI) is the set of tools and steps you run before delivering the application — integrating everything that has to happen to be confident the code is good. Continuous Delivery (CD) is the step that actually deploys or delivers the application onto a platform the customer can access. The reason automation matters is time. If you did all of these steps by hand — testing every single change a developer makes — it would take months for your application to reach the customer. In the modern world you are expected to deliver in weeks or even days, and the only way to do that is to have every step automated.

In practice, a developer does not write a whole feature in one go. They break it into chunks (tracked as tickets), and they push each chunk they are confident about to a Version Control System (VCS) — a tool like GitHub, GitLab, or Bitbucket. Your CI/CD process runs when code is pushed to that VCS. So the trigger is always the same: a developer commits their change, and the pipeline takes over from there.

T2

The stages of a pipeline

These exact steps vary from product to product — a mobile app or a government project might have more — but there is a standard set of stages that most organisations follow to be sure they are shipping a good application.

Unit testone function Staticcode analysis Code qualityvulnerabilities E2E testfunctional Reports Deploy
The standard stages. Test the change, analyse and secure the code, test the whole application end to end, store the reports, and finally deploy — each stage automated so a change can flow through in minutes.
  • Unit testing. You test one function in isolation. If you wrote an add(a, b) function, the unit test passes in 2 and 3 and checks that the result is 5. A developer changes code hundreds of times, so this cannot be done by hand.
  • Static code analysis. You check that the code is well formatted and correctly indented, and that it does not do wasteful things like declaring twenty variables and using only two. These are the kinds of things a human reviewer might miss.
  • Code quality and vulnerability testing. You check that the code is not exposed to any security vulnerabilities, because delivering an application with a security hole is a very bad experience for the customer.
  • Automation (end-to-end) testing. Where a unit test checks one function, functional or end-to-end testing verifies that the change you made does not break the rest of the application — for example, that your new addition feature did not break subtraction.
  • Reports. Every organisation stores reports somewhere, because someone will eventually ask what your unit-test coverage is, or whether the code quality is acceptable.
  • Deployment. Finally, you deploy the application onto a platform the customer can access. Without this step, nothing you did before matters, because the customer cannot use the application.
T3

Jenkins the orchestrator, and Docker as agent

Jenkins is the classic tool for running a pipeline. The key word for what it does is "orchestrator", and understanding both its power and its one big weakness is essential.

When a developer pushes code to GitHub, you want all of those stages — build, test, scan, deploy — to run automatically. So you deploy a CI/CD tool such as Jenkins and tell it to watch a specific repository and branch: "whenever there is a new commit or a pull request here, notify me, and I will run a set of actions." Jenkins does not do the building or scanning itself; it acts as an orchestrator that integrates and runs other tools. For a Java application it might integrate Maven to build the code and run unit tests, SonarQube for code quality, a reporting tool, and finally Kubernetes, Docker, or EC2 to deploy. The DevOps engineer configures all of these tools inside Jenkins, and the sequence of stages that runs them is called a Jenkins pipeline.

The legacy problem — and why Docker agents solve it.

Here is the weakness. Jenkins is a binary you install on one host — the master — and because hundreds of developers create too much load, and because different teams need conflicting dependencies (one team wants Java 7, another wants Java 8; one wants Python 2, another Python 3), you cannot run everything on the master. So you keep adding worker nodes — more EC2 instances — one for this team, one for that operating system. This does not scale: you end up with dozens of virtual machines, many sitting idle at weekends when no one is committing, and you can never scale to zero. The setup is both costly and a maintenance burden, because every dependency upgrade means logging into every worker node.

Legacy: VM worker nodes (many idle) Jenkins master worker (busy) worker (idle)💸 wasted worker (idle)💸 wasted Modern: Docker agents on demand Jenkins master containercreated on a job containerdeleted after No job running? Legacy = idle VMs still cost money. Modern = zero containers, zero cost. Upgrading a dependency: legacy = log into every VM · modern = change one line in the image tag
Why Docker agents win. Instead of permanent worker VMs sitting idle, Jenkins asks Docker for a container only when a job runs, executes the pipeline inside it, and deletes it afterwards — so there is nothing to pay for when idle, and upgrading a tool is a one-line image change.

The modern approach is to run each pipeline stage on a Docker container as the agent. When a job starts, Jenkins asks Docker for a container (for example, a node:16-alpine image), runs the stage inside it, and deletes the container the moment the job finishes. Containers are lightweight, created and destroyed on demand, so there is no idle cost, and upgrading a tool is as simple as changing the image tag from node:16 to node:17 — no more logging into virtual machines to update dependencies. This is the setup you should recommend, and it makes a strong project on your resume.

T4

GitHub Actions, runners, and Actions vs Jenkins

GitHub Actions is another CI/CD solution — one the instructor personally prefers over Jenkins. Understanding when to use it, and how it differs, is a common interview topic.

GitHub Actions does the same job as Jenkins, but it is built into GitHub and focused only on GitHub (just as GitLab CI is focused only on GitLab). This is its one real weakness: because it is tied to a single platform, you should not use it if your organisation might migrate to a different platform in the future — you would have to rewrite all your pipelines. The same reasoning is why teams choose Terraform over CloudFormation: portability. But for a project that lives on GitHub, Actions has big advantages, and roughly 90% of open-source projects use it.

Unlike Jenkins, there is nothing to install. You create a .github/workflows/ folder in your repository and put a YAML file in it. The file's on: key sets the trigger (on: push, or pull_request, or issues), it defines one or more jobs (the equivalent of pipelines), and each job specifies which container image to run on (runs-on: ubuntu-latest) and a list of steps. The steps use plugins from the Marketplace, such as actions/checkout@v3 to check out the code or actions/setup-python@v2 to set up Python — and crucially, the @v2 is the plugin's version, not the language version. These plugins are installed automatically, which is why an Actions workflow needs very little code.

Runners, and the advantages over Jenkins. A runner is where a job actually executes — the equivalent of a Jenkins agent. A GitHub-hosted runner is one GitHub gives you, runs your job, and then terminates; it is free for public projects. A self-hosted runner is your own machine (an EC2 instance or a container) that you register — you use one when you have a private project with security concerns, when the GitHub runners are not powerful enough (you need, say, 32 GB of RAM for heavy tests), or when you need special packages the hosted runners lack. The headline advantage of Actions over Jenkins is that there is no hosting or maintenance — no EC2 instance to run, no plugins to manage, no version upgrades — and it is free for public repositories. The verdict: for a public project, Actions wins outright; for a private project today, Jenkins is often still recommended because of its deeper orchestration, plugins, and integrations.
T5

GitOps, Argo CD, and the CI/CD split

The most misunderstood part of a real pipeline is how continuous integration hands off to continuous delivery. The modern answer is GitOps, and the key insight is that the two halves are decoupled.

In a real pipeline there are usually two Git repositories: one for the application source code, and one for the deployment manifests (the Kubernetes deployment.yaml, service.yaml, or Helm charts). The CI half — Jenkins building, testing, scanning, and pushing a new image to a registry — ends when the new image is pushed. It does not reach into the cluster to deploy. Instead, the CD half is a separate pull-based process, and that is what makes it robust.

Source repoapp code Jenkins (CI)build·test·scan Registryimage :v1.0.1 webhook push Image updateror a shell script Manifest repodeployment.yaml Argo CD (CD)watches Git Kubernetesthe cluster new tag commit watches deploys
The GitOps architecture. CI (Jenkins) ends by pushing an image. An image updater (or a shell script) commits the new tag to the manifest repo, and Argo CD — living inside the cluster — watches that repo and deploys any change, keeping the cluster in sync with Git.

GitOps is the practice of treating a Git repository as the single source of truth for what should be running. The best CD tools follow it — Argo CD is the most popular (Flux and Spinnaker are alternatives). Here is how the handoff works. When CI pushes a new image tag (say v1.0.1), a tool called Argo Image Updater — or, more simply, a shell script — notices the new tag and commits it into the manifest repository. Argo CD, which runs as a controller inside the Kubernetes cluster, continuously watches that manifest repository, and whenever it sees a change, it deploys it to the cluster. Because Argo CD is a controller, it also enforces the desired state: if someone edits the cluster by hand, Argo CD notices the drift and reverts it back to what Git says. This is why you keep manifests in Git rather than editing the cluster directly — you get review, auditing, and a self-healing cluster.

Multi-environment promotion. The same machinery moves a change through environments — typically Dev → Staging → Production. Production is the exact, richly resourced environment your customer uses; Staging is a smaller, cheaper replica (fewer servers, less RAM); Dev is smaller still, often a single node. A change is tested on Dev first (cheap), promoted to Staging (with manual or automatic approval), and only then deployed to Production. Staging is not a full copy of Production because that would be far too costly — which is the tension every promotion pipeline has to manage.
02
Phase 2 · Lab

Hands-on — from an empty server to a full delivery pipeline

Install Jenkins on an EC2 instance and expose it, run pipelines on Docker agents, build a multi-agent pipeline, write a GitHub Actions workflow, then assemble the full end-to-end pipeline: Maven, SonarQube, Docker, and Argo CD.

L1

Install Jenkins on EC2 and expose it

Jenkins is a Java application, so Java is a prerequisite. Launch an Ubuntu EC2 instance (for the full pipeline later, use a larger type such as t2.large — one CPU is not enough once SonarQube and Docker are also running), SSH in, and install:

bashsudo apt update
sudo apt install -y openjdk-17-jdk       # Java — the prerequisite for Jenkins
java -version                            # verify
# then the official Jenkins install steps (from jenkins.io) for your distro
ps -ef | grep jenkins                    # Jenkins runs on port 8080

Jenkins now runs on port 8080, but you cannot reach it yet, because an EC2 instance blocks inbound traffic by default. Open the port in the instance's security group: Security → the security group → Edit inbound rules → Add rule → Custom TCP, port 8080, source anywhere (0.0.0.0/0 for a demo; restrict to your IP in real life). Now open http://<public-ip>:8080, unlock Jenkins with the initial admin password, and finish setup:

bashsudo cat /var/lib/jenkins/secrets/initialAdminPassword   # the initial unlock password
# in the browser: paste it → Install suggested plugins → create your admin user

A note on the architecture you have just built: this EC2 instance is your Jenkins master, which in a real organisation is used mostly for scheduling. As covered in T3, the modern way to run the actual work is not to add more worker VMs but to use Docker agents — which is the next lesson.

L2

Docker as agent, and your first pipeline

To run pipelines on Docker containers, install Docker on the same machine, give the Jenkins user access to the Docker daemon, and install the Docker Pipeline plugin:

bashsudo apt install -y docker.io
sudo su -
usermod -aG docker jenkins          # let the jenkins user talk to the Docker daemon
usermod -aG docker ubuntu           # (and the ubuntu user, just in case)
systemctl restart docker
# then in Jenkins: Manage Jenkins → Plugins → install "Docker Pipeline", and restart Jenkins

Now create a job. In Jenkins, New Item offers a freestyle project and a pipeline project, among others. Do not use freestyle — it configures everything through text boxes, which cannot be version-controlled, peer-reviewed, or shared. Always use a pipeline, where you write the pipeline as code (in Groovy) and store that Jenkinsfile in Git, so it goes through the same review workflow as your application. Here is the minimal skeleton, which runs on a Docker agent and just verifies the container:

groovypipeline {
    agent { docker { image 'node:16-alpine' } }   # run the whole pipeline in this container
    stages {
        stage('Test') {
            steps {
                sh 'node --version'                # the one step in this stage
            }
        }
    }
}

The skeleton is always the same — agent, then stages, each stage containing steps — and you grow a real pipeline just by adding more stages (checkout, build with Maven, test, deploy). Rather than typing the pipeline into the Jenkins UI, point the job at your repository with "Pipeline script from SCM": give it the Git URL, the branch, and the path to the Jenkinsfile. When you click Build Now, Jenkins fetches the code, pulls the node:16-alpine image, runs the stage inside a container, and then deletes the container — so a docker ps afterwards shows nothing running. (If you forget the syntax for a step, the Pipeline Syntax generator built into Jenkins writes the Groovy for you.)

L3

Multi-stage, multi-agent pipelines

Real applications often have several tiers that each need a different environment — a front end on Node.js, a back end on Java with Maven, and a database on MySQL. You cannot run all of those on one worker without conflicts, and the elegant solution is to give each stage its own Docker agent. You set agent none at the top, and then each stage declares the image it needs:

groovypipeline {
    agent none
    stages {
        stage('Backend')  { agent { docker { image 'maven:3.8-openjdk-17' } }
                            steps { sh 'mvn --version' } }
        stage('Frontend') { agent { docker { image 'node:16-alpine' } }
                            steps { sh 'node --version' } }
        stage('Database') { agent { docker { image 'mysql:latest' } }
                            steps { sh 'echo "run DB migrations here"' } }
    }
}

When this pipeline runs, Jenkins creates a Maven container for the backend stage, a Node container for the frontend stage, and a MySQL container for the database stage — running each stage in the right environment — and deletes every container when the pipeline finishes. You can watch it happen with docker ps in another terminal as the containers appear and disappear. This is how you handle a multi-tier application without maintaining a fleet of specialised worker VMs, and it is a strong point to raise in an interview.

L4

A GitHub Actions workflow

GitHub Actions needs no server. Create a .github/workflows/ folder in your repository and add a YAML file. This one runs on every push, sets up Python on two versions at once, installs dependencies, and runs the tests:

yamlname: my first GitHub actions
on: [push]                               # trigger: any commit (or pull_request, issues)
jobs:
  build:                                 # a job = a pipeline
    runs-on: ubuntu-latest               # the container image to run on
    strategy:
      matrix:
        python-version: ["3.8", "3.9"]   # runs the job once per version
    steps:
      - uses: actions/checkout@v3        # a Marketplace plugin (@v3 = plugin version)
      - uses: actions/setup-python@v2
        with: { python-version: "${{ matrix.python-version }}" }
      - run: pip install pytest          # your own commands
      - run: pytest

The structure mirrors Jenkins: jobs are pipelines, and steps are the equivalent of stages. The big difference is that the plugins (actions/checkout, actions/setup-python) come from the Marketplace and are installed automatically, so you write very little. Two more features you will use: secrets (stored under Settings → Secrets, for things like a kubeconfig or a SonarQube token, and referenced in the workflow), and self-hosted runners (Settings → Actions → Runners → new self-hosted runner) — you run the download-and-configure commands on your own machine, then change runs-on: ubuntu-latest to runs-on: self-hosted. Remember the three reasons to go self-hosted: a private/secure project, needing more compute than the hosted runners give, or needing special packages.

L5

The Ultimate pipeline — Maven, SonarQube & Docker (the CI half)

This is the project that impresses interviewers, and one you can put on your resume. The CI half builds a Java application, scans it, and ships an image; the CD half (next lesson) deploys it with Argo CD.

First set up SonarQube for the code-quality scan. On the same EC2 instance, create a dedicated user, download and start the server (it listens on port 9000, with default login admin/admin), then generate a token in SonarQube (My Account → Security) and store it in Jenkins under Manage Credentials:

bashadduser sonarqube                        # a dedicated user for the server
# as that user: wget the SonarQube zip, unzip it, and run bin/<os>/sonar.sh start
# open http://<public-ip>:9000 → login admin/admin → generate a token → save it in Jenkins

Now the CI pipeline. Using a Docker image that already contains Maven and Docker as the agent, each stage does one job — and note the two interview-favourite details: mvn clean package builds the jar (whereas mvn clean install would also push it to a repository like Nexus), and the pom.xml is the file that declares the application's dependencies (Java's equivalent of Python's requirements.txt), which Maven downloads before building:

groovystage('Build & Test')  { steps { sh 'cd springboot-app && mvn clean package' } }
stage('Static Analysis') { steps {
    // sends the report to the SonarQube server, using the token
    sh 'cd springboot-app && mvn sonar:sonar -Dsonar.host.url=$SONAR_URL' } }
stage('Build & Push Image') { steps {
    sh 'docker build -t abhishek/ultimate-cicd:${BUILD_NUMBER} .'
    sh 'docker push abhishek/ultimate-cicd:${BUILD_NUMBER}' } }        # needs Docker Hub creds
stage('Update Manifest') { steps {
    // a shell script replaces the image tag in the manifest repo (needs a GitHub token)
    sh './update-manifest.sh ${BUILD_NUMBER}' } }

Walking the stages: Maven builds and unit-tests the application, producing a jar in the target/ folder; mvn sonar:sonar sends a static-analysis report to the SonarQube server, which shows bugs, vulnerabilities, and code smells; Docker builds an image from that jar and pushes it to a registry (Docker Hub, ECR, or Quay.io) with a new tag per build; and finally a shell script updates the manifest repository with the new tag — which is the seam where CI hands off to CD. The Docker Hub and GitHub credentials are stored in Jenkins credentials, never in the Jenkinsfile. (Do not expect the pipeline to pass on the first run — CI/CD debugging is normal; a token expires, a URL is wrong, and you fix it and re-run.)

L6

GitOps delivery with Argo CD (the CD half)

The CI pipeline ended by committing a new image tag to the manifest repository. Now Argo CD takes over. It runs inside a Kubernetes cluster (here, minikube), and the cleanest way to install a Kubernetes controller like this is through an operator, which manages its whole lifecycle. Install it from OperatorHub, then create the Argo CD instance:

bashminikube start
# install the Operator Lifecycle Manager, then the Argo CD operator (from operatorhub.io)
kubectl create -f argocd-basic.yaml           # the Argo CD custom resource → controller pods start
kubectl get pods                              # wait for the Argo CD pods to be running
# expose the Argo CD UI (edit its service to NodePort, or `minikube service`)
kubectl get secret argocd-cluster -o jsonpath='{.data.admin\.password}' | base64 -d   # the admin password

In the Argo CD UI, log in as admin with that password and create an application: point it at your manifest repository and the path to deployment.yaml, set the sync policy to automatic, and choose the target cluster. Argo CD immediately pulls the manifest and deploys your pods — you never run kubectl apply yourself. The real payoff is self-healing: if you edit the running deployment by hand (say, change the image tag to a version that does not exist), Argo CD detects that the cluster no longer matches Git, reports the app as out of sync, and reconciles it back to what the manifest says. That is the whole point of GitOps — Git is the source of truth, and the cluster is continuously corrected to match it. (Assemble the full project — the pipeline diagram and each stage — and it makes an excellent resume and LinkedIn piece.)

03
Phase 3 · Interview Q&A

The CI/CD & Jenkins questions you'll be asked

These are the scenario-based questions the instructor flags across the Jenkins and CI/CD videos, with answers built from his explanations. Most are framed around Jenkins, since that is what most companies still use. Answer each out loud, then reveal.

Q1Explain the CI/CD process in your organization.reveal ▸
Answer

Pick a language and name each tool. For a Java app: "We use Jenkins as the orchestrator, integrating Maven, SonarQube, a security scanner, and Argo CD. When a developer commits to GitHub, a webhook triggers the Jenkins pipeline. The first stage checks out the code; then Maven builds it and runs unit tests; then we do static code analysis and security scanning (SAST/DAST); then we build a Docker image and push it to a registry; and finally a shell script updates the Kubernetes manifest in a Git repository. Argo CD — a declarative, GitOps continuous-delivery tool — watches that manifest repo and deploys the new image to the cluster." Swap the tools to match your stack (npm for Node, or deploying to EC2 instead of Kubernetes).

Q2What are the different ways to trigger a Jenkins pipeline?reveal ▸
Answer

There are three: poll SCM, build triggers, and webhooks. Polling and build triggers are costly and laggy — Jenkins runs a git fetch every minute to check for changes, and a scheduled trigger can leave hours between a commit and the build. The efficient way is a webhook: instead of Jenkins watching GitHub, GitHub sends Jenkins a JSON payload the moment a chosen event happens (a commit or pull request). You configure the webhook in GitHub's settings with the Jenkins URL, and the pipeline runs immediately.

Q3Freestyle project vs pipeline — which do you use, and why?reveal ▸
Answer

Always the pipeline. A freestyle project configures everything through UI text boxes, which is easy but not declarative — it cannot be stored in Git, peer-reviewed, versioned, or shared, and to see a job's config you have to log into Jenkins and open it. A pipeline is written as code (Groovy) in a Jenkinsfile you keep in Git, so it goes through pull requests and review like any other code, and you can track its history. Even when learning, start with pipelines.

Q4Declarative vs scripted pipeline?reveal ▸
Answer

Prefer declarative pipelines. They are structured, easy to write, and easy to collaborate on, so even someone without deep programming experience can work with them. Scripted pipelines are more free-form Groovy and are only worth it if you have a strong development team that needs that flexibility. For most teams, declarative is the right default.

Q5How do you handle issues in your worker nodes?reveal ▸
Answer

You could log in, take thread dumps, and check the node's health — you might even run a small script that monitors CPU and RAM and alerts you at 80%, or configure auto-scaling. But the best answer is to avoid the problem: use Docker agents, so containers only spin up when a job runs and are deleted afterwards. That way there are no permanent worker nodes to go unhealthy, and the agents are never sitting idle.

Q6Can Jenkins build applications in multiple languages using different agents in different stages?reveal ▸
Answer

Yes, and the efficient way is Docker agents. For a three-tier app, you set agent none and give each stage its own image — a Maven/Java image for the backend, a Node image for the frontend, a database image for the data tier. Each stage runs in the right environment, and every container is deleted when the pipeline finishes, so you save compute and avoid configuring anything on a worker node.

Q7How do you handle secrets in Jenkins?reveal ▸
Answer

Never leak them into logs or the UI. Jenkins has a built-in credentials plugin to store them, but the stronger answer is to integrate a dedicated secrets tool like HashiCorp Vault — one of the most widely used, and it integrates with Jenkins, Terraform, Ansible, and more. You keep the secret in Vault and invoke it from the pipeline when a stage needs it, so the sensitive value is never hardcoded.

Q8How do you back up Jenkins?reveal ▸
Answer

Back up the .jenkins folder — a hidden folder in the Jenkins user's home directory that holds the jobs, logs, and configuration — using rsync to recursively sync it to a durable location like an EBS volume or a snapshot. If the organisation stores Jenkins data in an external database (common at large scale), back that up too, along with plugins and any user content.

Q9What are shared libraries (shared modules) in Jenkins?reveal ▸
Answer

They are a way to write a pipeline once and reuse it across many teams. If three teams all need the same "check out code, deploy to EC2" flow, rather than each writing it again, you package that pipeline as a shared library and share it. One person writes the code, and it is reused across the organisation.

Q10How do you add a worker node, or set up auto-scaling for Jenkins?reveal ▸
Answer

To add a node: Manage Jenkins → Manage Nodes and Clouds → add a new node, provide its IP and SSH keys, authenticate, and launch (Docker agents are added under "clouds"). For teams that must use VM workers and cannot use Docker agents, you handle peak load (holidays, spikes) with an auto-scaling group on the EC2 workers, which scales the instances up and down automatically so you are not paying for idle machines the rest of the time.

Q11How do you install plugins in Jenkins?reveal ▸
Answer

Two ways. Through the UI: Manage Jenkins → Manage Plugins → search the available plugins and install (as you do for the Docker Pipeline plugin). Through the CLI: a shell script using the Jenkins java -jar jenkins-cli.jar command, which is more efficient when you need to install many plugins at once. If a plugin is not yet generally available, you download its jar and upload it.

Q12What is JNLP, and why is it used in Jenkins?reveal ▸
Answer

JNLP is the mechanism that lets Jenkins agents (worker nodes) be launched and managed remotely by the Jenkins master. You download the JNLP jar from the master's UI and run it on the agent; the agent then connects back to the master and receives the build tasks to execute. In short, it is how the master and the worker nodes talk to each other.

Q13What is the latest version of Jenkins?reveal ▸
Answer

It sounds trivial, but interviewers ask it to check whether you actually use Jenkins day to day. If a company hires you specifically for a Jenkins CI/CD role and you cannot name a roughly current version, they assume you are not hands-on with it. So keep track of the current version of whatever CI/CD tool you present yourself as using.

Q14What is the advantage of GitHub Actions over Jenkins?reveal ▸
Answer

Mainly no hosting and no maintenance. With Jenkins you create an EC2 instance, install it, expose the port, install and manage plugins, and keep it patched. With GitHub Actions there is none of that — GitHub provides the runners, the plugins are auto-installed, the UI and pipeline syntax are simple, and it is free for public projects (private repos have a monthly minutes limit). The one disadvantage is that it is scoped to GitHub, so if you might migrate platforms, do not use it.

Q15When would you use a self-hosted runner instead of a GitHub-hosted one?reveal ▸
Answer

In three cases: when it is a private project with security concerns (you don't want your banking code running on a runner you know nothing about), when the GitHub-hosted runners aren't powerful enough for your workload (you need, say, 32 GB of RAM for heavy end-to-end tests), or when you need special packages or dependencies that the hosted runners don't provide. Otherwise, GitHub-hosted runners are free for public projects and simpler to use.

Q16How do the CI and CD halves communicate? (What is GitOps / Argo CD?)reveal ▸
Answer

They are decoupled. CI (Jenkins) ends by pushing a new image to a registry — it does not deploy. Then a GitOps tool takes over: a shell script or Argo Image Updater commits the new image tag into a separate manifest repository, and Argo CD — a controller running inside the Kubernetes cluster — continuously watches that repo and deploys any change it sees. Because Argo CD treats Git as the single source of truth, it also self-heals: if someone changes the cluster by hand, it reverts the change to match Git. This is more robust and auditable than baking a kubectl apply or an Ansible step into the CI pipeline.

Q17What is the difference between mvn clean package and mvn clean install?reveal ▸
Answer

Both build the application (a jar or war), but mvn clean install additionally pushes that artifact to a repository such as Nexus or Artifactory, whereas mvn clean package just produces the artifact locally. In the Ultimate pipeline we use clean package, because we don't need to publish the jar anywhere — we only use it to build the Docker image, and it's the image we push to the registry.

04
Phase 4 · Scenario drills

The senior delivery layer — CI/CD under real pressure

You can now build a pipeline, run it on Docker agents, and deploy with GitOps. These 17 scenarios from the Scenario Playbook are the senior version: designing a pipeline from scratch, speeding up a slow one, choosing a deployment strategy, rolling back a bad deploy, and securing the supply chain. Answer out loud, reveal, and mark yourself.

🔁

CI / CD

17 scenarios · 1–17

The complete CI/CD scenario set (Jenkins, GitHub Actions, GitLab), worked through in depth. This is the thinking and the story behind each one, not answers to memorise. Senior CI/CD is about pipeline design, safety, secrets, speed, and deployment strategy — not YAML syntax. Interviewers want to hear how you ship reliably and securely without downtime, and how you reason about trade-offs.

⚡ The universal CI/CD principles (the "reflex")
Build once, promote many — fast-fail ordering, security as stages CheckoutLint / UnitBuild onceScanStaging + testsProd (gated)
Cheap checks first; build the artifact once and promote the same one; the artifact you tested is the one you ship.

Unlike a live-debugging domain, CI/CD is governed by a handful of principles that answer most questions. Burn these in — nearly every scenario is an application of one:

  1. Build once, promote many. Build the artifact or image once, and promote that exact artifact through dev, staging, and prod, injecting only environment-specific config at deploy. Rebuilding per environment invalidates all your testing — the thing you tested is not the thing you shipped.
  2. Fast-fail ordering. Run the cheap checks first (lint, unit tests) before the expensive ones (build, integration, image scan), so that bad changes die in seconds rather than after a 20-minute build.
  3. Immutable artifacts with config injection. The artifact never changes across environments; only the injected config does (the twelve-factor approach). Pin image tags by digest.
  4. Shift security left. Static analysis, dependency scanning, secret scanning, infrastructure scanning, and image scanning are pipeline stages, not a gate at the very end — automated and fast, or developers will route around them.
  5. Decouple deploy from release. Deploying code is not the same as exposing it to users; feature flags let you ship in the dark and release gradually.
  6. Everything as code. The pipelines, the infrastructure, and even the CI controller's own config are versioned, reviewed, and reproducible. No UI-clicked snowflakes.
  7. Treat the delivery platform as production. If CI/CD being down means the company cannot ship or hotfix, it is a production-severity system — it needs high availability, backups, and a rehearsed break-glass path.

---

📋 The full scenario inventory (distinct — no padding)

A. Pipeline design & flow

  1. Design a CI/CD pipeline from scratch
  2. Multi-environment promotion (dev → staging → prod)
  3. Managing the pipeline for a monorepo

B. Speed & cost

  1. The pipeline is slow (30+ min) — speed it up

C. Deployment & safety

  1. Deployment strategies — rolling vs blue/green vs canary
  2. A deploy broke prod — rollback (and the database problem)

D. Secrets & supply-chain security

  1. Secrets management in pipelines
  2. A pipeline leaked secrets in its logs
  3. Supply-chain security (pinning, provenance, SLSA)

E. Reliability & quality

  1. Flaky tests blocking the pipeline
  2. Build works locally but fails in CI (hermetic builds)
  3. CI/CD itself is a single point of failure

F. Tool-specific depth

  1. Jenkins: scaling & pipeline-as-code
  2. GitHub Actions: security & efficiency
  3. GitLab CI: pipeline design (needs/DAG, cache vs artifacts)

G. Governance & runners

  1. Approvals, compliance & audit
  2. Self-hosted runners — security & management

---

1Design a CI/CD pipeline from scratchScenario

"Walk me through the pipeline you'd build for a microservice." An open design question that reveals how you actually think about delivery.

What is actually happening (the mental model).

A good pipeline is a fast-fail, security-gated conveyor that builds an artifact once and promotes it, with each stage ordered cheapest-first so that bad changes die early, and with security woven in as stages rather than bolted on at the end. The north star is build-once-promote-many: the exact artifact that passed staging is the one that reaches production.

How to reason about it (the stages).

  1. Checkout, then lint and static analysis, then unit tests — cheap and fast, so a bad change fails here in seconds.
  2. Build the artifact or image once, tag it immutably (by commit SHA or digest), and publish it to a registry.
  3. Scan — static analysis (SAST), dependency scanning (with an SBOM), and an image CVE scan — as pipeline stages that can block on critical findings.
  4. Deploy to staging, then run integration and smoke tests against the running environment.
  5. Gated production deploy — an approval or an automated canary — deploying the same artifact, with config injected.
  6. Post-deploy verification — smoke tests and health checks; never assume the deploy succeeded.

The root causes and trade-offs.

The big design decisions are how much to gate production, choosing between a manual approval and an automated canary depending on how mature your observability is, how to handle database migrations using an expand-contract approach, and whether to build once and promote that same artifact or rebuild for convenience at each stage, which should always be build once. Order the stages by their cost and by how quickly they fail, so the cheapest and fastest checks run first.

The trap that less-experienced engineers fall into.

Rebuilding the artifact per environment (so staging tested a different build than production ships), treating security as a final manual gate, and assuming a deploy succeeded without verifying it.

🎯 Interviewer follow-up questions you should expect.

  • "Why build once and promote?" Because the artifact you tested must be the one you ship — rebuilding per environment invalidates the testing.
  • "Where does security fit?" It fits in as stages — SAST, dependency scanning, image scanning — not as a final gate.
  • "How do you order the stages?" You order them cheapest and most-likely-to-fail first, so lint and unit tests run before build and integration.
  • "How do you handle config differences across environments?" You inject the config at deploy time, so the artifact itself stays immutable.
Say it like this"My north star is build-once-promote-many: I build the artifact once and promote that exact thing through dev, staging, and prod, injecting only config — because if I rebuild per environment, the thing I tested isn't the thing I ship. Stages are fast-fail ordered: cheap checks like lint and unit tests first so bad changes die in seconds, then build, then security scanning — SAST, dependency CVEs, image scanning — as pipeline stages, not an afterthought. Staging is gated behind smoke and integration tests, prod behind approval or an automated canary, and every deploy is followed by verification, not assumed successful."

---

Mark:
2Multi-environment promotion (dev → staging → prod)Scenario

"How does a change flow from dev to production?" Or a real need to design the promotion path with the right gates and parity.

What is actually happening (the mental model).

Promotion is the same immutable artifact moving through environments with progressively stronger gates, with config injected per environment (the twelve-factor approach). The failure this design prevents is "passed staging, broke prod", which comes from either rebuilding per environment or letting the environments diverge.

How to reason about it.

  1. Same artifact, injected config. Never rebuild; the promoted artifact is identical, and only the config (environment variables, secrets) differs.
  2. Progressive gates: auto-deploy to dev; run integration and smoke tests to promote to staging; require a manual approval or an automated canary to reach production.
  3. Environment parity: keep the environments as similar as possible (built by infrastructure as code from the same definitions) so that staging is representative.
  4. GitOps at scale: promotion becomes a pull request that updates a desired-state repo (Argo CD or Flux), so it is auditable, reviewable, and trivially revertable.

The root causes and trade-offs.

A manual approval gives you control, but it's slower. An automated canary is fast, but it needs genuinely good observability behind it to be trustworthy. Full parity between staging and production is often impossible at real production scale, so you compensate for that gap with canary deploys, feature flags, and strong observability.

The trap that less-experienced engineers fall into.

Rebuilding per environment, or letting the environments drift so that staging does not catch what production will — at which point "it passed staging" becomes meaningless.

🎯 Interviewer follow-up questions you should expect.

  • "How do you promote — rebuild or reuse?" You reuse the exact artifact and inject the config.
  • "How do you make staging representative?" Through environment parity built with infrastructure as code; where full parity is impossible, you compensate with canary deploys, feature flags, and observability.
  • "What's GitOps promotion?" A pull request to the desired-state repo drives the deploy, which makes it auditable and revertable.
Say it like this"The artifact never changes across environments — only injected config does, per twelve-factor. Dev deploys automatically, staging runs integration and smoke tests, and prod is gated by approval or an automated canary. I keep environments as similar as possible with IaC so staging is representative, because divergence is what produces 'passed staging, broke prod.' At scale I like GitOps: promotion becomes a pull request that changes the desired-state repo, so it's auditable, reviewable, and trivially revertable."

---

Mark:
3Managing the pipeline for a monorepoScenario

One repo with 40 services, and a naive pipeline that rebuilds and tests everything on every commit — CI time and cost exploding, queues clogged.

What is actually happening (the mental model).

Naive monorepo CI does not scale, because it treats a one-line change to one service as a reason to build all 40. The fix is change detection, or an affected-graph: build, test, and deploy only what actually changed and its dependents. The nuance a senior engineer flags is shared libraries — a change to a shared library must fan out to everything that depends on it, so the dependency graph has to be accurate.

How to reason about it.

  1. Affected-graph tooling — Nx, Bazel, Turborepo, or git diff path filters — to work out which services a change impacts, and build and test only those and their dependents.
  2. Independent versioning and deployment per service, even within one repo.
  3. Shared-dependency fan-out — a change to a shared library correctly triggers all its consumers; the accuracy of this graph is what makes it safe.
  4. Build caching (a remote cache) so unchanged targets are not rebuilt.

The root causes and trade-offs.

A monorepo gives you atomic cross-service changes and unified tooling, but it demands a genuinely sophisticated CI setup to pull off well. The trade-off is that CI complexity against the dependency drift you'd otherwise get from having many separate repositories.

The trap that less-experienced engineers fall into.

Running the full build-and-test suite for all services on every commit — so CI time and cost explode and feedback slows to a crawl — or, conversely, an inaccurate affected-graph that misses a shared-library dependent and ships a broken consumer.

🎯 Interviewer follow-up questions you should expect.

  • "Naive monorepo CI rebuilds everything — how do you fix it?" With affected-graph tooling that builds and tests only what changed and its dependents, plus remote caching.
  • "What's the risk with change detection?" An inaccurate dependency graph that misses a shared-library consumer — the graph has to be correct.
Say it like this"Naive monorepo CI that rebuilds all 40 services on every commit doesn't scale — CI time and cost explode. I use affected-graph tooling like Nx or Bazel to build and test only what changed and its dependents, with remote build caching so unchanged targets aren't rebuilt. The nuance is shared libraries: a change there must fan out to everything depending on it, so the dependency graph has to be accurate — get it right and monorepo CI is fast, get it wrong and you either rebuild the world or ship a broken consumer."

---

Mark:
4The pipeline is slow (30+ minutes) — speed it upScenario

Developers wait 30–40 minutes for CI feedback; the queue backs up; velocity suffers. "Make it faster."

What is actually happening (the mental model).

Slow pipelines almost always have a few concentrated bottlenecks, and the senior move is to profile first and optimise from evidence, not guess. The usual wins are caching, parallelism, test sharding, affected-only builds, and pulling slow non-blocking work off the critical path. Developer feedback speed directly affects delivery velocity, so this is worth real effort.

How to work through it.

  1. Profile the pipeline — which stages actually take the time? Do not optimise by vibes.
  2. Cache dependencies — layer caching (Docker BuildKit), and node_modules, .m2, or Gradle caches — so you are not re-downloading the world each run.
  3. Parallelise independent stages and jobs.
  4. Shard tests across runners (split the suite so the total wall-clock time drops).
  5. Do affected-only builds and tests in a monorepo (scenario 3).
  6. Right-size the runners (bigger or more for the bottleneck).
  7. Move slow non-blocking checks off the critical path — heavy security scans can run asynchronously rather than gating every merge.

The root causes and trade-offs.

This is usually caused by having no caching at all, so dependencies get re-downloaded on every single run, by stages running serially when they could run in parallel, by one monolithic test suite that never got split up, or by running the full pipeline on every commit regardless of what changed. Profiling the pipeline reveals which of these is actually the culprit.

The trap that less-experienced engineers fall into.

Optimising the wrong stage (guessing instead of profiling), or "speeding up" by removing checks and weakening the merge bar rather than making the same checks faster.

🎯 Interviewer follow-up questions you should expect.

  • "How do you approach a slow pipeline?" You profile it first, then apply caching, parallelism, test sharding, affected-only builds, and off-critical-path scans.
  • "How do you cut cost without lowering quality?" You cancel superseded runs with concurrency groups, use path filters and caching, while full validation still runs on merge candidates.
  • "Quantify a win you've had." For example, going from 40 minutes to under 10, through dependency caching, test sharding, and affected-only builds.
Say it like this"First I profile — I don't optimise by vibes. Usually the wins are dependency and layer caching so we're not re-downloading the world, parallelising independent jobs, sharding the test suite across runners, and in a monorepo running only what changed. I also pull non-blocking checks like heavy security scans off the merge-critical path so they run async. I've taken pipelines from 40 minutes to under 10 this way — and since feedback speed directly affects velocity, it's worth real effort."

---

Mark:
5Deployment strategies — rolling vs blue/green vs canaryScenario

"Rolling, blue/green, or canary — which and when?" A core senior question testing whether you reason about risk, cost, and observability.

Rolling gradual · mixed versions · slow rollback Blue / Green instant switch + rollback · 2× resources blue (old) green (new) Canary 1% → watch metrics → ramp / abort 5% new Pick by risk × cost × observability: rolling is cheap; blue/green buys instant rollback; canary is safest but needs automated metric analysis + rollback. Any two-versions-live strategy needs backward-compatible (expand-contract) schema changes.
Rolling vs blue/green vs canary — a risk/cost/tooling triangle; a "real" canary needs automated analysis and rollback.

What is actually happening (the mental model).

These are three points on a risk, cost, and tooling triangle, and you pick based on the change's blast radius and your observability maturity:

  • Rolling — replace instances gradually (the cheap default). But you run mixed versions simultaneously, and rollback is slow.
  • Blue/green — stand up the full new version and flip all traffic at once. Rollback is instant (flip back), but you briefly need double the resources.
  • Canary — route a small percentage of traffic to the new version, watch the golden signals, and progressively increase it or auto-abort. This has the lowest blast radius, but it is only real with good observability and automation.

How to reason about it.

Match the strategy to the risk of the change and to your tooling: rolling for low-risk routine deploys; blue/green when you want instant rollback and can afford double resources; canary for high-risk changes where you want to catch a regression at 5% of traffic. A canary is only "real" with automated metric analysis and rollback — a human watching a dashboard is theatre.

The root causes and trade-offs.

A rolling deployment is cheap, but it runs mixed versions side by side for a while and rolls back slowly. Blue-green gives you instant rollback, but at double the infrastructure cost and as an all-or-nothing switch. Canary is the safest option, but it needs real observability and automation behind it to work. And any strategy that runs two versions live at once needs backward-compatible schemas, using the expand-contract approach, underneath it.

The trap that less-experienced engineers fall into.

Calling a manual "deploy to 10% and glance at Grafana" a canary, and forgetting that having two versions live requires backward-compatible database changes.

🎯 Interviewer follow-up questions you should expect.

  • "Blue/green vs canary — trade-offs?" Blue-green gives an instant switch and rollback but needs double the resources; canary has the lowest blast radius but needs observability and automation.
  • "What makes a canary real?" Automated metric analysis and automated rollback.
  • "What must be true of the DB for any of these?" It must be backward-compatible, using expand-contract, so both versions can coexist.
Say it like this"It's a risk-cost-tooling triangle. Rolling is the cheap default but you run mixed versions and rollback is slow. Blue/green buys instant rollback at the cost of running two environments briefly. Canary is the safest for high-risk changes — 1% of traffic, watch the golden signals, auto-abort on regression — but it's only as good as your observability and automation; a human eyeballing a dashboard isn't a canary. I match the strategy to the blast radius of the change, and whichever I use, the schema has to be backward-compatible so both versions can run at once."

---

Mark:
6A deploy broke prod — rollback (and the database problem)Scenario

A new release causes errors in production. You need to recover fast, and the question of "can we even roll back?" becomes urgent — especially if a database migration went out with it.

What is actually happening (the mental model).

Rollback should be a one-click, rehearsed path that is faster and safer than fixing forward under pressure. The strategies that make rollback trivial are blue/green (flip back), immutable image tags (redeploy the previous tag), and canary (halt and revert). The hard part is always the database: if a migration was destructive or one-way, you cannot roll the code back without corrupting or losing data — which is why backward-compatible, expand-contract migrations are essential.

How to work through it.

  1. Restore service first — roll back to the last known-good state: flip blue/green back, redeploy the previous immutable tag, or run rollout undo. This is the fastest path to green.
  2. Communicate — a status page, the stakeholders, an honest ETA.
  3. The database: if you used expand-contract migrations (add the new, backfill, dual-write, switch, and later remove the old), the old code still works against the new schema, so rollback is safe. If a one-way migration shipped with the code, rollback is not possible without data recovery — a design flaw to flag before it ships.
  4. Automate rollback triggers on an SLO breach where you can.

The root causes and trade-offs.

The choice comes down to fixing forward versus rolling back, and rolling back is usually the safer option under real pressure. The database migration is the actual crux of the decision, since an expand-contract migration keeps rollback genuinely possible, while a one-way migration forecloses it entirely.

The trap that less-experienced engineers fall into.

Coupling a breaking schema change to the code deploy — so that when the code fails, they cannot roll back because the schema has already changed. And trying to debug and fix forward in production under pressure instead of rolling back to restore users first.

🎯 Interviewer follow-up questions you should expect.

  • "How do you make rollback trivial?" With a blue-green flip-back, immutable image tags, or rollout undo, rehearsed so it's a one-click action.
  • "Why can't you always roll back?" Because of a one-way database migration — the old code can't run against the new schema, and the fix is expand-contract.
  • "What's expand-contract?" You add the new schema, backfill the data, dual-write, switch the reads over, and only later remove the old schema, so the old and new code can coexist and rollback stays safe.
Say it like this"Rollback must be faster and safer than fixing forward under pressure. With blue/green I shift traffic back to the last known-good; with immutable image tags I redeploy the previous tag. The hard part is always the database — I use expand-contract migrations so the old code still works against the new schema, which means I can roll back without data loss. If a one-way migration shipped coupled to the code, I can't roll back, and that's a design flaw I'd flag before it ever ships. So I restore service first with a rollback, then diagnose."

---

Mark:
7Secrets management in pipelinesScenario

"How do you handle credentials in CI?" Or a review flags long-lived cloud keys stored in the CI system — a top breach vector.

What is actually happening (the mental model).

The best secret is one that does not exist. Instead of storing long-lived cloud keys in CI (which can leak and are a prime target), use short-lived credentials through OIDC federation — the CI system proves its identity to the cloud through a trust relationship and gets temporary credentials, so there is no static secret to steal. Where secrets must exist, they live in the platform's protected store, masked, and scoped so that untrusted contexts (such as fork pull requests) cannot reach them.

How to reason about it.

  1. OIDC federation over static keys — GitHub Actions, GitLab, or Jenkins federate to AWS, GCP, or Azure for short-lived, auto-expiring credentials, so there is no stored cloud key.
  2. Platform secret stores for unavoidable secrets — GitHub Actions secrets, GitLab masked and protected variables, or Jenkins Credentials with withCredentials.
  3. Scope to protected branches and environments — so that a pull request from a fork cannot exfiltrate production secrets.
  4. Mask in logs, rotate, and avoid secret sprawl (a vault as the source of truth).

The root causes and trade-offs.

Static keys are convenient, but they sit around as a persistent leak risk for as long as they exist. OIDC removes the static secret entirely, at the cost of some initial setup effort. Fork pull requests are the genuinely dangerous context here, because they combine untrusted code with access to secrets.

The trap that less-experienced engineers fall into.

Storing long-lived AWS keys as CI variables — the single most common cloud breach vector — and exposing secrets to fork-PR builds.

🎯 Interviewer follow-up questions you should expect.

  • "How do you avoid storing cloud keys in CI?" With OIDC federation for short-lived credentials.
  • "How do you protect secrets from fork PRs?" You scope the secrets to protected branches and environments, and never expose them to untrusted PR builds.
  • "What's the risk of static keys in CI?" They can leak, they're a prime breach target, and they never expire on their own.
Say it like this"The best secret is one that doesn't exist. Instead of storing long-lived AWS keys in CI, I use OIDC federation — the CI system gets a short-lived role via a trust relationship, so there's no static credential to leak. Where I do need secrets, they're in the platform's protected store, masked in logs, and scoped to protected environments so a fork PR can't exfiltrate them. Static cloud keys in CI are one of the most common breach vectors, so removing them is a real security win."

---

Mark:
8A pipeline leaked secrets in its logsScenario

A build printed a token, key, or password to the console — maybe from a set -x, a debug echo, or dumping an object — and it's now in the (possibly widely-visible, retained) build logs.

What is actually happening (the mental model).

This is the same discipline as any secret leak: a secret printed to a build log is compromised — assume exposure, especially if the logs are broadly visible or retained. So the order is rotate first, then prevent — deleting the log line is secondary and does not undo the exposure.

How to work through it.

  1. Rotate or revoke the secret immediately — it is leaked the moment it hit the log.
  2. Audit where else it is used, and whether it was accessed.
  3. Enable secret masking so that the platform redacts known secrets in output going forward.
  4. Find how it happened — usually a set -x, a debug echo, or logging a request or response object — and fix that.
  5. Add secret scanning on the pipeline output so that a token appearing in the logs fails the build.
  6. Restrict who can view the logs.

The root causes.

A set -x echoing commands with secrets in them, debug logging that dumps the environment or an object, or a tool that prints credentials. The build script reveals it.

How to fix it, verify it, and prevent it.

Rotate, mask, and scan. Prevent it with masking, output scanning, and log-access restrictions.

The trap that less-experienced engineers fall into.

Deleting the log or the offending line and thinking they are safe — the secret is already exposed, and rotation is the only real fix.

🎯 Interviewer follow-up questions you should expect.

  • "A token was printed to a build log — first move?" You rotate it — a logged secret is compromised, especially with retained or visible logs.
  • "How do you prevent it?" With secret masking plus automated secret scanning on the pipeline output, and by restricting log visibility.
Say it like this"Rotate first — a secret printed to a build log is compromised, especially if logs are broadly visible or retained. Then I find how it happened, usually a set -x or a debug echo, mask it going forward, and add automated secret scanning on pipeline output so a token appearing in logs fails the build. Same discipline as any secret leak: assume exposure, invalidate, then prevent."

---

Mark:
9Supply-chain security (pinning, provenance, SLSA)Scenario

A security review of the pipeline itself: are you pulling in third-party actions/images safely? Can you prove what's in your artifacts? Increasingly a first-class concern after high-profile supply-chain attacks.

What is actually happening (the mental model).

Your pipeline pulls in a lot of third-party code (actions, base images, dependencies), and each one is an attack surface. The senior posture is to pin third-party code by immutable digest (not a moving tag), generate provenance (an SBOM), sign artifacts, and verify the signatures at deploy. A tag like @v3 can be re-pointed at malicious code; a digest cannot.

How to reason about it.

  1. Pin third-party actions and images by full commit SHA or digest, not by a mutable tag — a tag can be moved to malicious code, which is a real attack vector.
  2. Dependency scanning and an SBOM — know what is in your artifact, and generate a Software Bill of Materials.
  3. Sign artifacts (with cosign) and generate provenance (SLSA) — cryptographic proof of what was built, how, and from what source.
  4. Verify signatures at admission or deploy — only run signed, trusted artifacts.
  5. Least-privilege pipeline tokens — a compromised build should not have broad access.

The root causes and trade-offs.

Mutable tags are convenient, but they're also hijackable. Without an SBOM, you genuinely can't answer the question "are we affected by this CVE?" when one comes out. And unsigned artifacts carry no provenance at all. Pinning and signing add some friction to the pipeline, but they close real attack vectors.

The trap that less-experienced engineers fall into.

Pinning third-party actions to @v3 (a moving tag) and trusting it forever, and having no way to answer "what is in this artifact, and did we build it?" when a supply-chain CVE drops.

🎯 Interviewer follow-up questions you should expect.

  • "Why pin actions to a SHA, not a tag?" Because a tag can be re-pointed at malicious code, whereas a SHA or digest is immutable.
  • "How do you know what's in your artifact?" Through an SBOM from dependency scanning, plus SLSA provenance.
  • "How do you ensure only trusted artifacts run?" You sign them with cosign and verify the signatures at admission or deploy.
Say it like this"The pipeline pulls in a lot of third-party code, so I treat it as attack surface. I pin third-party actions and base images by full commit SHA or digest, never a moving tag like @v3, because a tag can be re-pointed at malicious code — a real attack vector. I generate an SBOM so I can answer 'are we affected by this CVE', sign artifacts with cosign and generate SLSA provenance so I can prove what was built and how, and verify those signatures at deploy so only trusted artifacts run. And the pipeline's own token is least-privilege, so a compromised build can't do much."

---

Mark:
10Flaky tests blocking the pipelineScenario

Tests fail randomly; the same commit passes on a re-run. The team starts habitually re-running until green — or worse, ignoring red.

What is actually happening (the mental model).

Flaky tests are dangerous not because of the individual failures but because they erode trust in the entire pipeline — they teach the team that "red does not really mean broken", and then a real failure gets re-run into green and ships. So you treat flakiness as a first-class bug, not noise: quarantine the flaky tests (with an owner and a ticket), fix the root cause, and never let "just re-run it" become the culture.

How to work through it.

  1. Quarantine the flaky test into a separate, non-blocking suite — with a tracking ticket and an owner — so that the pipeline stays trustworthy while it is fixed. (Quarantine, do not delete.)
  2. Fix the root cause — almost always a timing or race condition, shared state between tests, dependence on test ordering, or an unmocked external dependency.
  3. Culturally, push back on "just re-run it" as a norm, because it hides real failures.

Root causes, and how to tell them apart.

A timing or async race fails under load or on slow CI; shared mutable state or order-dependence fails depending on test order (run the tests in random order to expose it); an unmocked external dependency fails on network blips. Reproduce it by running in isolation, in random order, or under load.

The trap that less-experienced engineers fall into.

Normalising "re-run until green", which trains everyone to ignore red — so a genuine regression gets re-run into a merge. Or deleting flaky tests (and losing the coverage) instead of quarantining and fixing them.

🎯 Interviewer follow-up questions you should expect.

  • "Why are flaky tests dangerous beyond the annoyance?" Because they erode trust in the pipeline, so real failures get re-run into green and shipped anyway.
  • "How do you handle them?" You quarantine them into a non-blocking suite with an owner and a ticket, fix the root cause, and never normalise re-running.
  • "Common root causes?" Timing and races, shared state, order-dependence, and unmocked external dependencies.
Say it like this"Flakiness is a silent killer because it teaches the team that red doesn't mean broken — and then a real failure gets re-run into green and ships. I quarantine flaky tests into a non-blocking suite with an owner and a ticket, so the pipeline stays trustworthy while we fix the root cause, which is almost always timing, shared state, test-ordering, or an external dependency that should be mocked. The cultural fix matters as much as the technical one: 'just re-run it' is a norm I actively push back on."

---

Mark:
11Build works locally but fails in CI (or vice-versa)Scenario

"Works on my machine" — green locally, red in CI (or the reverse). Frustrating and time-wasting.

What is actually happening (the mental model).

The root cause is environmental non-determinism: the build depends on something that differs between the laptop and CI — tool versions, uncommitted local files, environment variables, cache state, timezone or locale, or a locally-running service. The durable fix is to make the build hermetic and reproducible so that "my machine" stops being a special place.

How to work through it.

  1. Identify the difference: tool or runtime versions (are they pinned?), uncommitted files (does a fresh clone build?), environment variables that exist only on the laptop, cache state, locale or timezone, or a local dependency (such as a database) that is not present in CI.
  2. Containerise the build — run it in the same container CI uses, so the environment is identical.
  3. Pin versions — lockfiles for dependencies, and a tool-version manager (asdf or mise) for the toolchains.
  4. Make builds hermetic — a fresh checkout plus pinned dependencies is all that is needed, with no reliance on ambient state.

Root causes, and how to tell them apart.

Version drift means different tool or dependency versions; uncommitted local files mean a fresh clone fails or differs; environment-variable dependence; or a local service the build talks to. A clean, containerised build from a fresh checkout isolates it.

The trap that less-experienced engineers fall into.

Treating "works on my machine" as acceptable and debugging it case-by-case, instead of removing the environment as a variable through containerisation and version pinning.

🎯 Interviewer follow-up questions you should expect.

  • "Works locally, fails in CI — root cause?" Environmental non-determinism — version drift, uncommitted files, environment variables, or local services.
  • "How do you fix it durably?" With hermetic builds — the same container as CI, pinned versions, and reproducibility from a fresh checkout.
Say it like this"'Works on my machine' means the environment is an uncontrolled variable — usually version drift, uncommitted local files, or an env var that only exists on the dev's laptop. The durable fix is a hermetic build: run it in the same container CI uses, pin every version with lockfiles and a tool-version manager, and make sure a fresh checkout is all that's needed. The goal is that 'my machine' stops being a special place."

---

Mark:
12CI/CD itself is a single point of failureScenario

The whole org can't ship — or can't push a security hotfix — because the CI/CD system is down. "Jenkins is broken so nobody can deploy."

What is actually happening (the mental model).

If CI/CD going down means the company cannot ship or patch, then the delivery platform is a production-severity system and must be treated like one. Two things matter: making the platform resilient (high availability, backups, defined as code so it is rebuildable) and having a rehearsed break-glass path to deploy a critical fix by hand when the pipeline is down.

How to reason about it.

  1. Treat the platform as production — HA controllers, backups (the Jenkins home and config), and infrastructure as code so it can be rebuilt quickly.
  2. Monitor and alert on the pipeline itself.
  3. A break-glass path — a documented, audited way to deploy a critical fix manually when CI is down, because "we can't hotfix because Jenkins is broken" is unacceptable during an incident.
  4. Reduce the blast radius — per-team runners and controllers, so one failure does not halt everyone.

The root causes and trade-offs.

This usually comes from a single controller with no high availability or backup, no manual deploy path to fall back on, or shared infrastructure with no isolation between teams. The trade-off is the real cost of building high availability and disaster recovery for the platform, against the risk of a company-wide shipping freeze if it ever goes down.

The trap that less-experienced engineers fall into.

Treating the CI system as "just tooling" rather than production — no backups, no rebuild plan, and no break-glass path — so that a CI outage becomes an inability to ship or hotfix during a real incident.

🎯 Interviewer follow-up questions you should expect.

  • "CI is down and you need to ship a security fix — what now?" A rehearsed break-glass manual deploy path, documented and audited.
  • "How do you make the delivery platform resilient?" Through high availability, backups, the platform defined as code for a fast rebuild, monitoring, and a reduced blast radius.
Say it like this"If CI/CD going down means the whole company can't ship or patch a security hole, that's a production-severity single point of failure, and I treat it as one — HA, backups, and the platform itself defined as code so I can rebuild it fast. Just as important is a rehearsed break-glass path: a documented, audited way to deploy a critical fix by hand when the pipeline is down, because 'we can't hotfix because Jenkins is broken' is an unacceptable failure mode during an incident."

---

Mark:
13Jenkins: scaling & pipeline-as-codeScenario

A Jenkins instance that's a snowflake — jobs configured by clicking in the UI, static build agents, plugin sprawl — that doesn't scale and is terrifying to lose.

What is actually happening (the mental model).

UI-configured Jenkins jobs are unreviewable, unversioned snowflakes; a Jenkins outage means rebuilding tribal knowledge. The senior move is everything-as-code: pipelines in Jenkinsfiles, shared logic in libraries, ephemeral agents, and the controller itself managed by configuration-as-code.

How to reason about it.

  1. Declarative Jenkinsfiles — pipelines as code in the repo, versioned and reviewed, with no UI-clicked jobs.
  2. Shared libraries — factor out common pipeline logic so that 50 teams are not copy-pasting; keep it DRY.
  3. Ephemeral agents — dynamic Kubernetes or cloud agents spun up per build, instead of static "slaves" that drift and need patching.
  4. JCasC (Configuration as Code) — manage the controller's own config as code so that Jenkins itself is reproducible.
  5. Take backups, and avoid plugin sprawl (each plugin is maintenance and risk).

The root causes and trade-offs.

Jobs configured through the UI are unversioned and unreviewable. Static agents drift over time, cost money while sitting idle, and still need patching. And without JCasC, the controller itself becomes a unique, hand-configured snowflake nobody can rebuild. The trade-off is the upfront effort of codifying all of this, against long-term maintainability and disaster recovery.

The trap that less-experienced engineers fall into.

Leaving Jenkins as clicked-UI jobs and static agents — unversioned, un-rebuildable, and a bus-factor risk when the one person who knows the setup leaves.

🎯 Interviewer follow-up questions you should expect.

  • "How do you make Jenkins scalable and maintainable?" With Jenkinsfiles, shared libraries, ephemeral agents, and JCasC for the controller.
  • "Why ephemeral agents?" Because static agents drift and need patching, while per-build ephemeral agents stay clean and scale.
  • "What's JCasC?" Configuration as Code for the controller, so Jenkins itself is reproducible.
Say it like this"UI-configured Jenkins jobs are unreviewable, unversioned snowflakes. I move everything to Jenkinsfiles in the repo, factor common logic into shared libraries so teams aren't copy-pasting pipelines, and run builds on ephemeral Kubernetes agents that spin up per build instead of maintaining static slaves that drift. I even manage the controller with JCasC so Jenkins itself is reproducible from code — otherwise a Jenkins outage means rebuilding tribal knowledge."

---

Mark:
14GitHub Actions: security & efficiencyScenario

Designing GitHub Actions workflows, with the interviewer probing the two things that separate a senior: supply-chain safety and the pull_request_target footgun.

What is actually happening (the mental model).

GitHub Actions has two security landmines every senior should name. First, third-party actions pinned to a mutable tag can be hijacked, so you pin by SHA. Second, the pull_request_target trigger runs with repo secrets and write access, so combining it with checking out untrusted PR code is a classic exfiltration hole. Beyond those there are efficiency levers: a least-privilege token, OIDC, reusable workflows, concurrency, and caching.

How to reason about it.

  1. Pin third-party actions by full commit SHA, not @v3 — a tag can be moved to malicious code.
  2. The pull_request_target danger — it runs with secrets, so never combine it with executing untrusted PR code (a known exfiltration vector). Use pull_request for untrusted contributions.
  3. Least-privilege GITHUB_TOKEN through the permissions: block — default to read, and grant only what is needed.
  4. OIDC for cloud authentication instead of stored keys (scenario 7).
  5. Reusable workflows for DRY, matrix builds for parallelism, concurrency groups to cancel superseded runs (saving cost), and caching.

The root causes and trade-offs.

Mutable action tags are hijackable. pull_request_target combined with an untrusted checkout is what actually exfiltrates secrets. And over-broad token permissions widen the blast radius of anything that does go wrong. This really is security against convenience — pinning actions and applying least-privilege add a little friction, but they close real holes.

The trap that less-experienced engineers fall into.

Pinning actions to @v3 and trusting them forever; using pull_request_target with a checkout of PR code (which exfiltrates secrets); and leaving GITHUB_TOKEN with its default broad permissions.

🎯 Interviewer follow-up questions you should expect.

  • "Why pin actions to a SHA?" Because a tag can be re-pointed at malicious code.
  • "What's dangerous about pull_request_target?" It runs with secrets and write access, so combined with untrusted PR code, it becomes a secret-exfiltration vector.
  • "How do you scope the token?" Through least-privilege using the permissions: block, and OIDC for the cloud instead of stored keys.
  • "How do you cut runner cost?" With concurrency groups to cancel superseded runs, plus caching and path filters.
Say it like this"Two things I always call out. First, supply chain: I pin third-party actions to a commit SHA because a tag like @v3 can be re-pointed at malicious code. Second, pull_request_target runs with repo secrets and write access, so combining it with checking out untrusted PR code is a classic exfiltration hole — I use pull_request for untrusted contributions. Beyond that: least-privilege GITHUB_TOKEN via the permissions block, OIDC instead of stored cloud keys, reusable workflows for DRY, and concurrency groups to stop wasting runners on superseded commits."

---

Mark:
15GitLab CI: pipeline design (needs/DAG, cache vs artifacts)Scenario

Designing GitLab CI at scale, where the interviewer probes the modern features that make pipelines fast and correct — needs: DAG and the cache-vs-artifacts distinction.

What is actually happening (the mental model).

The biggest performance lever in GitLab CI is needs:, which turns rigid sequential stages into a DAG — a job runs the moment its specific dependencies finish, instead of waiting for the whole previous stage. And the correctness gotcha is cache versus artifacts: cache is a best-effort speedup (it may be missing), whereas artifacts are guaranteed hand-offs between jobs — confusing them causes flaky pipelines.

How to reason about it.

  1. needs: for a DAG — jobs start as soon as their dependencies are done, not when the whole stage completes. Big time savings.
  2. rules: (the modern approach) over legacy only/except for controlling when jobs run.
  3. include, extends, and templates for DRY across pipelines.
  4. Cache versus artifactscache is a best-effort speedup (dependencies, may be absent, so do not rely on it for correctness), whereas artifacts are guaranteed outputs passed between jobs (a build output a downstream job needs). Do not use cache where you need a guaranteed hand-off.
  5. Parent-child or multi-project pipelines for monorepos, and protected variables and environments to gate who deploys where.
  6. Right-size the runners (autoscaling, tags).

The root causes and trade-offs.

Sequential stages are slow, and the fix is expressing the real dependencies as a needs: DAG instead. Using cache for something that's actually a required hand-off between jobs is flaky, and the fix there is using artifacts instead. And the legacy only/except syntax is simply less flexible than the newer rules: syntax.

The trap that less-experienced engineers fall into.

Leaving everything in rigid sequential stages (slow), and using cache where artifacts are needed — so that a job intermittently fails because the cache was not there.

🎯 Interviewer follow-up questions you should expect.

  • "How do you speed up a GitLab pipeline?" You use needs: to turn stages into a DAG, so jobs start as soon as their dependencies finish.
  • "Cache vs artifacts?" Cache is a best-effort speedup that may be missing; artifacts are a guaranteed hand-off between jobs.
  • "How do you keep pipelines DRY?" With include, extends, and templates, and by preferring rules: over only/except.
Say it like this"The biggest lever in GitLab is needs: — it turns rigid sequential stages into a DAG, so a job runs the moment its dependencies are done instead of waiting for the whole stage, which slashes pipeline time. I use rules: instead of legacy only/except, and include and extends to keep pipelines DRY. The correctness gotcha is cache versus artifacts: cache is a best-effort speedup that might be missing, while artifacts are guaranteed hand-offs between jobs — so I never use cache where I need a guaranteed output passed downstream, or the pipeline gets flaky."

---

Mark:
16Approvals, compliance & auditScenario

A regulated environment (SOX, PCI, etc.) needs change control, segregation of duties, and audit evidence — and the question is how to bake that into the pipeline without killing velocity.

What is actually happening (the mental model).

In regulated shops, you make the pipeline itself the control, so that compliance is automatic rather than manual paperwork. The key concepts are segregation of duties (the author cannot approve their own production deploy), immutable audit logs, signed provenance, policy-as-code gates, and — crucially — making the compliant path the path of least resistance so that people do not route around it.

How to reason about it.

  1. Required approvals with segregation of duties — the person who wrote the change cannot be the one who approves it to production; the pipeline enforces this.
  2. Immutable audit logs — who deployed what, when, and with what approval — generated automatically.
  3. Signed artifacts and provenance (SLSA, cosign) for supply-chain evidence.
  4. Policy-as-code gates (OPA) — automated checks that must pass.
  5. Evidence generation for auditors — so that an audit is a query, not a fire drill.
  6. Make the compliant path the easy path — or people build shadow processes around it.

The root causes and trade-offs.

Manual compliance checking is slow, error-prone, and genuinely hated by everyone who has to do it. Pipeline-enforced compliance is automatic and produces a real audit trail on its own. The trade-off is some upfront pipeline work, against ongoing audit pain and risk if you skip it.

The trap that less-experienced engineers fall into.

Bolting on heavyweight manual approval processes that people circumvent, instead of encoding the controls into the pipeline as the easiest path.

🎯 Interviewer follow-up questions you should expect.

  • "What's segregation of duties in a pipeline?" The author can't approve their own production deploy — the pipeline enforces a separate approver.
  • "How do you make audits painless?" With automated, immutable audit logs plus evidence generation, so an audit becomes just a query.
  • "How do you keep compliance from killing velocity?" You make the compliant path the path of least resistance, and automate the controls.
Say it like this"In regulated shops I make the pipeline be the control, so compliance is automatic rather than manual paperwork. Segregation of duties means the person who wrote the change can't be the one who approves it to prod — the pipeline enforces that. I generate signed provenance, keep immutable deploy audit logs, and use policy-as-code gates so an audit is a query, not a fire drill. The trick is making the compliant path the path of least resistance, or people route around it."

---

Mark:
17Self-hosted runners — security & managementScenario

You need self-hosted runners — to reach private resources, for performance, or for special hardware — and the question is how to run them safely, because they're a genuine security minefield.

What is actually happening (the mental model).

Self-hosted runners execute pipeline code on your infrastructure with your network access, so the danger is reuse (one job leaving behind malware or credentials for the next) and running untrusted code (a public-repo pull request executing on your runner). The senior posture is ephemeral, single-use runners, never attached to untrusted PR builds, isolated from production, least-privilege, and patched and monitored like any production host.

How to reason about it.

  1. Ephemeral and single-use — spun up per job and destroyed after, so that one job cannot poison the next (by leaving behind credentials, malware, or state).
  2. Never run untrusted PR code on self-hosted runners — a fork PR would execute attacker-controlled code on your infrastructure with your network access.
  3. Isolate from production — a separate network and least-privilege IAM, so that a compromised runner has limited reach.
  4. Autoscale (with Kubernetes or ephemeral VMs), and patch and monitor them like any production host.

The root causes and trade-offs.

Reused runners can leak state and credentials between jobs that shouldn't be able to see each other. Untrusted PR code running on a self-hosted runner is effectively remote code execution on your own infrastructure. And over-privileged runners carry a broad blast radius if anything on them is compromised. The trade-off is that self-hosted runners give you access, performance, and hardware control, at the cost of taking on real security responsibility.

The trap that less-experienced engineers fall into.

Running long-lived, reused runners (so one job poisons the next) and attaching self-hosted runners to public-repo PR builds (running untrusted code on your infrastructure with your credentials and network access).

🎯 Interviewer follow-up questions you should expect.

  • "Why must self-hosted runners be ephemeral?" Because reuse lets one job leave behind malware or credentials for the next one.
  • "Why never run public PRs on self-hosted runners?" Because it executes untrusted code on your infrastructure with your network access.
  • "How do you contain a compromised runner?" With least-privilege IAM, network isolation from production, and monitoring.
Say it like this"Self-hosted runners are a security minefield if reused, because one job can leave behind malware or credentials for the next. I make them ephemeral and single-use — spun up per job, destroyed after — and I never attach them to public-repo PR builds, since that runs untrusted code on my infrastructure with my network access. They get least-privilege IAM, network isolation from prod, and the same patching and monitoring as any production host. They're infrastructure, not a convenience box."

---

Mark:
🧠 How to turn this into muscle memory
  • Drill the seven principles first (build-once-promote-many, fast-fail ordering, immutable artifacts, shift-left security, deploy is not release, everything-as-code, treat-the-platform-as-production) — nearly every question is an application of one.
  • Build and break, in a real pipeline. Set up a pipeline that builds once and promotes; deliberately ship a bad deploy and practise the one-click rollback; leak a fake secret and add scanning and masking; pin an action by SHA; write an expand-contract migration and prove you can roll back. Doing beats reading.
  • One scenario, three passes. Reason it out, then say the spoken version without looking, then explain it to a teammate — especially the deployment-strategy trade-offs and the OIDC and secrets story, which come up constantly.
  • Keep a "pipeline decision + why" log — the trade-offs you made (canary versus blue/green, the monorepo CI approach, the rollback design) become the concrete stories you tell in the behavioural round.
No scenarios match that search. Clear search
CI/CD & Jenkins — DevOps Zero → Hero. Theory, Lab & Interview rebuilt from Days 18–21 and the end-to-end project videos (Jenkins, GitHub Actions, self-hosted runners, the Ultimate CI/CD pipeline); the 17 scenario drills are stitched in from the Scenario Playbook (CI/CD domain), rewritten in plain English. ← All chapters