Ravindra BagaleCourses & study guides मराठी

Chapter 23: Jenkins Fundamentals, Install and Architecture

23.2 Integration, delivery, deployment

Why: the problem

Think of Zomato. A developer fixes a bug in the payment page. Three questions come next. One: does the new code still pass all tests when it is joined with everyone else's code? Two: is there a ready, tested package that we could put live at any time? Three: does it go live by itself, or does a person press a button? These three questions have three names. Continuous Integration (CI) answers the first one. Continuous Delivery answers the second. Continuous Deployment answers the third. People mix these words up all the time, even in job interviews. The difference is not theory. It decides how your Jenkinsfile looks. A CI pipeline stops after tests. A delivery pipeline builds a package and waits for a person to approve. A deployment pipeline pushes to production with no human click. In this lesson you will write all three for the same small Python app, so you can see the difference line by line.

What we will build

You will take one small Flask app with two tests. You will write three Jenkinsfiles. The first runs tests on every push (CI). The second also builds a Docker image, pushes it to a registry, and stops at an approval button (Continuous Delivery). The third deploys to a staging server over SSH with no button, and checks a health URL (Continuous Deployment). You will compare the Stage View of all three.

What: every term in plain English

  • Integration — Joining your code with the main branch that everyone else uses.
  • Continuous Integration (CI) — Every push is built and tested automatically, many times a day.
  • Continuous Delivery — Every green build makes a ready package. A person decides when it goes live.
  • Continuous Deployment — Every green build goes live by itself, with no human click.
  • Artifact — The output of a build, like a jar, a zip, or a Docker image.
  • Approval gate — A pause in the pipeline where a person must click Proceed. In Jenkins it is the input step.
  • Staging — A copy of production used for final checks before going live.
  • Health check — A URL like /health that returns OK when the app is working.
Get the sample app Run the tests locally first Create the CI job Create the delivery job Add registry credentials Create the deployment job Run all three and compare
CI, delivery and deployment, top to bottom

How: step by step

  1. Get the sample app

    git clone https://gitlab.com/ravi-lab/flask-hello.git && cd flask-hello && ls
    

    What it does: A Flask app with app.py, test_app.py and requirements.txt.

    Output

    Dockerfile  Jenkinsfile.ci  Jenkinsfile.cd  Jenkinsfile.deploy  app.py  requirements.txt  test_app.py
    
  2. Run the tests locally first

    python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt && pytest -q
    

    What it does: If it is red on your laptop, it will be red in Jenkins too.

    Output

    ..                                         [100%]
    2 passed in 0.31s
    
  3. Create the CI job

    New Item > flask-ci > Pipeline > Pipeline script from SCM > Git > Script Path: Jenkinsfile.ci
    

    What it does: Jenkins reads the Jenkinsfile from Git, not from a text box.

    Output

    Saved
    
  4. Create the delivery job

    New Item > flask-cd > Pipeline script from SCM > Script Path: Jenkinsfile.cd
    

    What it does: Same repo, different Jenkinsfile.

    Output

    Saved
    
  5. Add registry credentials

    Manage Jenkins > Credentials > System > Global > Add > Username with password > ID: dockerhub
    

    What it does: Lets Jenkins push images without a password in the file.

    Output

    Credential dockerhub created
    
  6. Create the deployment job

    New Item > flask-deploy > Script Path: Jenkinsfile.deploy
    

    What it does: The fully automatic version.

    Output

    Saved
    
  7. Run all three and compare

    Build Now on each job, then open Stage View
    

    What it does: You see where each one stops.

    Output

    flask-ci: 2 stages | flask-cd: 4 stages, paused | flask-deploy: 5 stages, done
    

Example 1: CI only: test every push, nothing more

Stack. Jenkins, Docker agent, Python 3.12, pytest

Goal. CI only: test every push, nothing more.

Jenkinsfile.ci

pipeline {
  agent { docker { image 'python:3.12-slim' } }
  stages {
    stage('Install') {
      steps { sh 'pip install --no-cache-dir -r requirements.txt' }
    }
    stage('Test') {
      steps { sh 'pytest -q --junitxml=report.xml' }
      post { always { junit 'report.xml' } }
    }
  }
}

What each line does

  • agent { docker { image 'python:3.12-slim' } } — Runs every step inside a fresh Python container. Needs the Docker Pipeline plugin and Docker on the agent.
  • pip install --no-cache-dir — Installs packages without keeping a cache, so the container stays small.
  • pytest -q --junitxml=report.xml — Runs tests quietly and writes results in JUnit XML format.
  • junit 'report.xml' — Jenkins reads the XML and shows a test trend graph on the job page.
  • post { always { } } — Publish results even when tests fail. That is when you need them most.

Green output

[Pipeline] stage (Install)
+ pip install --no-cache-dir -r requirements.txt
Successfully installed Flask-3.0.3 pytest-8.3.2 ...
[Pipeline] stage (Test)
+ pytest -q --junitxml=report.xml
..                                         [100%]
2 passed in 0.28s
Recording test results
Finished: SUCCESS

Expected result. Two green stages and a Test Result link showing 2 passed. This is CI: fast feedback, no package, no deploy.

Example 2: Continuous Delivery: build and push an image, then wait for a human

Stack. Jenkins, Docker, Docker Hub

Goal. Continuous Delivery: build and push an image, then wait for a human.

Jenkinsfile.cd

pipeline {
  agent any
  environment { IMAGE = "ravilab/flask-hello:${BUILD_NUMBER}" }
  stages {
    stage('Test') {
      agent { docker { image 'python:3.12-slim' } }
      steps { sh 'pip install -r requirements.txt && pytest -q' }
    }
    stage('Build image') {
      steps { sh 'docker build -t $IMAGE .' }
    }
    stage('Push image') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'U', passwordVariable: 'P')]) {
          sh 'echo "$P" | docker login -u "$U" --password-stdin'
          sh 'docker push $IMAGE'
        }
      }
    }
    stage('Approve release') {
      steps {
        timeout(time: 1, unit: 'HOURS') {
          input message: "Release ${IMAGE} to production?", ok: 'Release'
        }
      }
    }
  }
}

What each line does

  • environment { IMAGE = ... } — One variable for the image name and tag. The build number makes each tag unique.
  • docker build -t $IMAGE . — Builds the image from the Dockerfile in the repo.
  • withCredentials([usernamePassword(...)]) — Takes the saved login and puts it in U and P only inside this block. Jenkins hides the values in logs.
  • docker login --password-stdin — Reads the password from a pipe, so it never appears in the process list.
  • input message: ... — Pauses the pipeline and shows a Release button to people with permission.
  • timeout(time: 1, unit: HOURS) — If nobody clicks in one hour, the build stops instead of waiting forever.

Green output

[Pipeline] stage (Build image)
+ docker build -t ravilab/flask-hello:7 .
Successfully tagged ravilab/flask-hello:7
[Pipeline] stage (Push image)
+ docker login -u ravilab --password-stdin
Login Succeeded
+ docker push ravilab/flask-hello:7
7: digest: sha256:4b1e... size: 1573
[Pipeline] stage (Approve release)
Release ravilab/flask-hello:7 to production?
Release or Abort
Input requested

Expected result. The job shows a paused stage with Release and Abort buttons. The image is ready in the registry. A person decides the time. That is delivery.

Example 3: Continuous Deployment: deploy to staging automatically and check health

Stack. Jenkins, Docker, SSH Agent plugin, Ubuntu 22.04 server

Goal. Continuous Deployment: deploy to staging automatically and check health.

Jenkinsfile.deploy

pipeline {
  agent any
  environment {
    IMAGE  = "ravilab/flask-hello:${BUILD_NUMBER}"
    SERVER = "ubuntu@10.0.1.25"
  }
  stages {
    stage('Test')  { steps { sh 'docker run --rm -v "$PWD":/app -w /app python:3.12-slim sh -c "pip install -q -r requirements.txt && pytest -q"' } }
    stage('Build') { steps { sh 'docker build -t $IMAGE .' } }
    stage('Push') {
      steps {
        withCredentials([usernamePassword(credentialsId: 'dockerhub', usernameVariable: 'U', passwordVariable: 'P')]) {
          sh 'echo "$P" | docker login -u "$U" --password-stdin && docker push $IMAGE'
        }
      }
    }
    stage('Deploy staging') {
      steps {
        sshagent(['staging-ssh']) {
          sh 'ssh -o StrictHostKeyChecking=accept-new $SERVER "docker pull $IMAGE && docker rm -f web || true && docker run -d --name web -p 80:5000 --restart unless-stopped $IMAGE"'
        }
      }
    }
    stage('Health check') {
      steps {
        retry(5) { sleep 3; sh 'curl -fsS http://10.0.1.25/health' }
      }
    }
  }
}

What each line does

  • SERVER = "ubuntu@10.0.1.25" — The staging server user and private IP address.
  • sshagent(['staging-ssh']) — Loads the SSH private key saved in Jenkins with id staging-ssh. Needs the SSH Agent plugin.
  • StrictHostKeyChecking=accept-new — Trusts the server key the first time and refuses if it changes later.
  • docker rm -f web || true — Removes the old container. || true means do not fail if it does not exist yet.
  • --restart unless-stopped — Docker starts the app again after a server reboot.
  • retry(5) { sleep 3; curl -fsS ... } — Tries the health URL up to 5 times, 3 seconds apart. -f makes curl fail on HTTP errors.

Green output

[Pipeline] stage (Deploy staging)
+ ssh ubuntu@10.0.1.25 docker pull ravilab/flask-hello:8 ...
8: Pulling from ravilab/flask-hello
Status: Downloaded newer image for ravilab/flask-hello:8
3a7c9e1d2f4b
[Pipeline] stage (Health check)
+ curl -fsS http://10.0.1.25/health
{"status":"ok","version":"8"}
Finished: SUCCESS

Expected result. No button anywhere. A push to main ends with version 8 running on staging. Big companies like Amazon and Netflix deploy this way many times a day, with strong tests and fast rollback.

Verify: did it work?

  1. CI job has test results

    curl -s -u ravi:$JENKINS_TOKEN http://localhost:8080/job/flask-ci/lastBuild/testReport/api/json?tree=passCount,failCount
    

    What it does: Reads the test counts.

    Output

    {"failCount":0,"passCount":2}
    
  2. Delivery job is waiting

    curl -s -u ravi:$JENKINS_TOKEN http://localhost:8080/job/flask-cd/lastBuild/api/json?tree=building,result
    

    What it does: building true and result null means it waits at input.

    Output

    {"building":true,"result":null}
    
  3. Staging runs the new tag

    ssh ubuntu@10.0.1.25 docker ps --format "{{.Image}} {{.Status}}"
    

    What it does: Shows which image runs now.

    Output

    ravilab/flask-hello:8 Up 2 minutes
    

When it is red 1

denied: requested access to the resource is denied

Cause. The image name does not start with your Docker Hub username, or login failed.

Fix, step by step

  1. Check the IMAGE name: it must be yourname/app.
  2. Check the dockerhub credential username.
  3. Run docker login by hand on the agent to test.

When it is red 2

Host key verification failed.

Cause. The agent has never seen the server SSH key and strict checking blocked it.

Fix, step by step

  1. Add -o StrictHostKeyChecking=accept-new once.
  2. Or add the key to known_hosts on the agent with ssh-keyscan.
  3. Run the build again.

When it is red 3

curl: (7) Failed to connect to 10.0.1.25 port 80: Connection refused

Cause. The app container did not start, or the security group blocks port 80.

Fix, step by step

  1. ssh to the server and run docker logs web.
  2. Check the security group allows port 80 from the Jenkins IP.
  3. Raise retry to 10 if the app starts slowly.

Cleanup

  1. Abort the waiting delivery build

    Open flask-cd > paused stage > Abort
    

    What it does: Stops the build at the approval gate.

    Output

    Aborted by ravi
    
  2. Stop staging app

    ssh ubuntu@10.0.1.25 docker rm -f web
    

    What it does: Removes the test container from staging.

    Output

    web
    

Ravindra Bagale's Tip

CI mhanje test, Delivery mhanje tayar package ani button, Deployment mhanje button nahi. Interview madhe farak spashta sanga. Dhyan dya!

Practice tasks

Easy Label the three pipelines

Why: Naming them right is half the interview.

What to do: Write CI, Delivery or Deployment next to each Jenkinsfile.

How:
  1. Read each file.
  2. Find the last stage.
  3. Write the label.

Check: All three labels are right.

Medium Add submitter to input

Why: Not everyone should release.

What to do: Add submitter: 'release-managers' to the input step.

How:
  1. Create the group or user.
  2. Edit Jenkinsfile.cd.
  3. Try to click Release as dev1.

Check: dev1 cannot release; the release manager can.

Hard Add automatic rollback

Why: Deployment needs a fast way back.

What to do: If the health check fails, run the previous tag again.

How:
  1. Save the old tag before deploy.
  2. Add post { failure { } } that runs the old tag.
  3. Break /health on purpose.

Check: The old version runs again after a failed deploy.

Easy Say CI vs delivery vs deployment in two lines

Why: If you can say it simply, you understand it. Juniors and interviewers want the short version first.

What to do: Write two short lines about CI vs delivery vs deployment for a fresher who knows only Linux basics.

How:
  1. Write what problem it solves.
  2. Write the one command or menu you use for it.
  3. Read it aloud to a friend.

Check: A fresher understands it without asking a question.

Easy Start a fresh lab Jenkins

Why: Practice needs a throwaway Jenkins. If you break it, you delete it and start again.

What to do: Run Jenkins LTS in Docker with a named volume.

How:
  1. docker volume create jenkins_home
  2. docker run -d --name jenkins -p 8080:8080 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk17
  3. docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Check: The Jenkins unlock page opens on localhost:8080.

Medium Break it on purpose

Why: You must know the red message by face. During an outage there is no time to read slowly.

What to do: Cause one error from the "When it is red" part of this lesson and fix it.

How:
  1. Follow the error section.
  2. Copy the exact message into your notes.
  3. Write the fix in one line.

Check: You fixed it using only the steps, without searching the web.

Medium Write a one-page runbook

Why: At 2 a.m. nobody remembers where a file lives. A short page saves the on-call person.

What to do: Write a runbook page for this lesson.

How:
  1. Title, when to use, exact command, how to check, how to undo.

Check: A teammate can follow it alone.

Medium Repeat it on a cloud VM

Why: A laptop is not a server. Real Jenkins runs on EC2 or a company VM.

What to do: Do the main steps of this lesson again on a small Ubuntu 22.04 EC2 instance.

How:
  1. Launch t3.small with 20 GB disk.
  2. Open port 22 only to your IP.
  3. Repeat the steps and compare the output.

Check: Same green output on the VM as on your laptop.

Hard Explain it like an interview

Why: Interviewers want real stories, not definitions.

What to do: Record a 60 second answer about this lesson.

How:
  1. Say the problem, the command you ran, and one error you hit.

Check: Your answer has a real command and a real error message.

Hard Draw the flow top to bottom

Why: A drawing shows who talks to whom and in what order.

What to do: Draw this lesson as boxes from top to bottom.

How:
  1. Start at the developer push, go through the controller and agent, end at the result.

Check: Each box matches a real thing you saw on screen.

Homework: try at home

Try at home

At home, draw your own team (or an imaginary one) and decide: should you stop at CI, at delivery, or go to full deployment? Write three reasons. Then change Jenkinsfile.cd so only users in the release-managers group can click Release using submitter.

Recap

  • CI means every push is built and tested.
  • Continuous Delivery adds a ready artifact and a human approval.
  • Continuous Deployment removes the human click.
  • In Jenkins the difference is visible: the input step marks delivery.
  • Deployment needs good tests, health checks and fast rollback.

FAQ

Can a company use CI without CD?

Yes. Many start with only CI. It already catches most bugs early.

Is continuous deployment risky?

Only without good tests. Teams add health checks, canary releases and quick rollback.

What is "CD" in CI/CD?

It can mean delivery or deployment. Always ask which one.

Where should the approval happen?

After the artifact is built and tested, just before production.

Does Jenkins do deployment by itself?

Jenkins runs the deploy commands. The real work is done by tools like ssh, kubectl, Helm or Ansible.

Interview questions

  1. Explain CI, continuous delivery and continuous deployment.

    CI builds and tests every change. Continuous delivery also produces a deployable artifact for every green build, released by a human. Continuous deployment releases every green build automatically.

  2. How do you add manual approval in Jenkins?

    With the input step inside a timeout, and submitter set to the allowed group.

  3. How do you keep registry passwords out of a Jenkinsfile?

    Save them as Jenkins credentials and use withCredentials or the docker.withRegistry step.

  4. How would you verify a deployment?

    Run a health check with retry, and smoke tests on key URLs. Fail the build and roll back if it fails.

  5. What makes continuous deployment safe?

    Automated tests, small changes, health checks, monitoring, feature flags and one-command rollback.

Quick quiz

Q1. Which practice stops after tests?

Continuous Integration.

Q2. What makes delivery different from deployment?

Delivery waits for a human approval; deployment goes live automatically.

Q3. Which Jenkins step creates an approval button?

input.

Q4. Why use the build number in the image tag?

Each build gets a unique, traceable tag.

Q5. Why retry the health check?

The app needs a few seconds to start after deploy.

Samajla ka?

Ravi Sir's Cards for this lesson

मराठी cards