A free, guided curriculum
Learn Jenkins by
building a real pipeline.
Seven phases, start to finish: install Jenkins, write your first Jenkinsfile, containerize an app with Docker, provision infrastructure with Terraform, configure it with Ansible, and deploy on AWS — then lock it down like a production system. No prior DevOps experience required.
Stage View
Phase 1 of 7
Core Concepts
Install Jenkins, learn the architecture, run your first job.
Goal of this phase: Understand what Jenkins is, how it's structured, install it two ways, and run your very first build.
1. What is Jenkins? (Concept)
Jenkins is an open-source automation server. Its main job is to automate the boring, repetitive parts of software delivery:
- Pulling code from a repo (GitHub, GitLab, etc.)
- Running builds/tests
- Packaging artifacts (JARs, Docker images, zip files)
- Deploying to servers, cloud environments, or Kubernetes
It's the "orchestrator" that sits between your code and your infrastructure — very similar in spirit to how you already use Ansible to orchestrate configuration, except Jenkins orchestrates the pipeline of steps, and can call Ansible, Terraform, Docker, AWS CLI, etc. as steps inside it.
Why it matters for you
You already know Terraform (infra), Ansible (config), Docker (packaging), AWS (deployment target). Jenkins is the glue that triggers and sequences all of those automatically whenever code changes — instead of you running commands manually.
2. Jenkins Architecture (Concept)
┌─────────────────────────┐
│ Jenkins Controller │ (formerly called "Master")
│ - Web UI │
│ - Job/Pipeline configs │
│ - Scheduler │
│ - Plugin manager │
└───────────┬─────────────┘
│ dispatches work
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Agent 1 │ │ Agent 2 │ │ Agent 3 │ (formerly "Slaves")
│(executor)│ │(executor)│ │(executor)│
└──────────┘ └──────────┘ └──────────┘
| Term | Meaning |
|---|---|
| Controller (Master) | The main Jenkins server. Hosts the UI, stores job configs, schedules builds. |
| Agent (Node) | A machine (physical/VM/container) that actually executes the build steps. Controller delegates work to agents. |
| Executor | A slot on an agent that can run one build at a time. An agent can have multiple executors. |
| Job / Project | A configured task Jenkins can run (e.g., "build my app"). |
| Build | One execution/run of a job. |
| Workspace | The directory on the agent where Jenkins checks out code and runs the job. |
| Plugin | Add-on that extends Jenkins (Git integration, Docker, Slack notifications, etc.) |
For learning purposes, you'll run everything on a single machine where the controller also acts as an agent (built-in node) — that's completely normal for a lab setup.
3. Installation — Two Methods (Do Both)
You'll install Jenkins twice, since you specifically want the practice: once via Docker (fast, disposable, great for experimenting) and once natively on an EC2 instance (closer to how real production Jenkins servers are often run).
Method A: Jenkins via Docker (do this first — fastest)
Prerequisite: Docker installed on your machine (you already have this skill).
# 1. Pull and run Jenkins LTS (Long Term Support) image
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
jenkins/jenkins:lts
# 2. Check it's running
docker ps
# 3. Get the initial admin password (needed for first login)
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
- Port
8080→ Jenkins web UI - Port
50000→ used for agent-controller communication (JNLP agents) - The
-v jenkins_home:/var/jenkins_homevolume means your Jenkins data persists even if you stop/remove the container — important, don't skip this.
Now open http://localhost:8080 (or http://<your-ec2-public-ip>:8080 if running on an EC2 instance — remember to open port 8080 in the Security Group).
Method B: Jenkins natively on EC2 (Amazon Linux 2023 / RHEL-based)
Since you already work with EC2 and Red Hat Satellite, this will feel familiar.
# 1. Launch an EC2 instance (t2.medium recommended — Jenkins needs at least 2GB RAM)
# Open inbound port 8080 (Jenkins UI) and 22 (SSH) in the Security Group
# 2. SSH into the instance
ssh -i your-key.pem ec2-user@<public-ip>
# 3. Install Java (Jenkins requires Java 17 or 21 as of recent LTS versions)
sudo yum install -y java-17-amazon-corretto
# 4. Add the Jenkins repo and import the GPG key
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key
# 5. Install Jenkins
sudo yum install -y jenkins
# 6. Start and enable Jenkins as a service
sudo systemctl start jenkins
sudo systemctl enable jenkins
sudo systemctl status jenkins
# 7. Get the initial admin password
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
Open http://<ec2-public-ip>:8080 in your browser.
Compare the two methods — this comparison itself is a learning exercise:
| | Docker | Native (EC2/yum) |
|---|---|---|
| Setup speed | Fastest | Slower, more steps |
| Data location | Docker volume | /var/lib/jenkins |
| Managed as | Container | systemd service |
| Realistic for prod? | Yes (common in modern setups) | Yes (classic setup, still very common in enterprises) |
4. First-Time Setup Wizard
- Paste the initial admin password into the browser prompt.
- Choose "Install suggested plugins" (recommended for learning — installs Git, Pipeline, Credentials Binding, and other essentials automatically).
- Create your first admin user (don't skip this — set a real username/password, not the default admin account).
- Confirm the Jenkins URL (leave default:
http://localhost:8080/or your EC2 IP). - Click Start using Jenkins → you land on the Dashboard.
5. Explore the Dashboard (Concept + Hands-on)
Spend 10-15 minutes clicking through these before creating a job:
- Manage Jenkins → central hub for plugins, system config, security, nodes
- Manage Jenkins → Plugins → Available/Installed/Updates tabs
- Manage Jenkins → Nodes → see your built-in node (controller acting as agent), note the number of executors
- New Item → where you create jobs/pipelines
- People → user management
- Build History (appears once you run jobs) → left sidebar
Install these plugins now (Manage Jenkins → Plugins → Available plugins)
If not already installed via "suggested plugins": - Git plugin — pulls code from Git repos - Pipeline plugin — for Jenkinsfile-based pipelines (Phase 4) - Docker Pipeline — will need this in Phase 5 - Blue Ocean (optional) — nicer pipeline visualization UI
6. Project: Your First Freestyle Job
Objective: Create a job that pulls a public GitHub repo and runs a shell script — a "Hello World" for Jenkins.
Step-by-step
- Click New Item
- Enter name:
hello-world-freestyle - Select Freestyle project → OK
- Under Source Code Management, select Git
- Repository URL: use any public repo, e.g.
https://github.com/octocat/Hello-World.git- Leave branch as*/master - Under Build Triggers, leave unchecked for now (we'll do this manually first)
- Under Build Steps, click Add build step → Execute shell (Linux) and add:
bash echo "Hello from Jenkins!" echo "Current directory contents:" ls -la echo "Build number: $BUILD_NUMBER" echo "Job name: $JOB_NAME" - Click Save
- Click Build Now (left sidebar)
- Under Build History, click the build number (e.g.,
#1) → Console Output - Read the console output — this is your build log. Confirm you see your echoed text and the repo files listed.
What just happened: Jenkins cloned the Git repo into a workspace, ran your shell commands inside that workspace, and logged everything. This is the foundation every pipeline builds on.
Stretch goals (optional, once the above works)
- Change the build trigger to "Poll SCM" with schedule
H/5 * * * *(checks the repo every 5 minutes for changes) - Add a Parameterized Build (Boolean or String parameter) and reference it in your shell step using
$PARAM_NAME - Try "Build periodically" trigger with cron syntax, e.g.
H 9 * * *(once daily around 9 AM)
7. Checkpoint — Can You Answer These?
Before moving to Phase 2, make sure you can explain (out loud or in writing, no notes):
- What's the difference between the Jenkins controller and an agent?
- What is an executor?
- What is a workspace, and where does it live?
- What's the difference between a job and a build?
- Why would you choose Docker vs native install for Jenkins in a real company?
- What does the
initialAdminPasswordfile do, and why is it only needed once?
If you can answer all six clearly, you're ready for Phase 2: Jobs & Builds (build triggers, parameterized builds, post-build actions, artifact archiving).
Quick Reference — Useful Commands
# Docker Jenkins
docker start jenkins
docker stop jenkins
docker logs jenkins
docker exec -it jenkins bash
# Native Jenkins (systemd)
sudo systemctl restart jenkins
sudo systemctl status jenkins
sudo journalctl -u jenkins -f # live logs
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
Next: Phase 2 — Jobs & Builds (triggers, parameters, post-build actions). Ask me when you're ready and I'll build the same style of guide for it.
Phase 2 of 7
Jobs & Builds
Triggers, parameters, environment variables, post-build actions.
Goal of this phase: Learn how to make Jenkins jobs run automatically, accept inputs, and act on results — the day-to-day mechanics of running builds.
Assumes only what you learned in Phase 1 (installing Jenkins, creating a Freestyle job, running "Build Now"). No AWS/Terraform/Ansible knowledge assumed here — everything is plain Jenkins + shell commands.
1. Build Triggers (Concept)
In Phase 1, you clicked "Build Now" manually every time. In real use, you rarely do that — you want Jenkins to run builds automatically. This is what a Build Trigger controls.
| Trigger | What it does | When to use |
|---|---|---|
| Manual (Build Now) | You click the button yourself | Testing, one-off runs |
| Poll SCM | Jenkins checks the repo on a schedule; if something changed, it builds | Simple setups, no webhook access |
| GitHub hook trigger (Webhook) | GitHub pings Jenkins the instant code is pushed | Best practice — instant, efficient |
| Build periodically | Runs on a fixed schedule regardless of code changes | Nightly builds, scheduled reports/cleanup jobs |
| Trigger builds remotely (API) | Another system/script calls a URL to start the build | Chained automation, external tools |
| Build after other projects | Runs after another specific job finishes | Multi-job pipelines (before you learn real Pipelines) |
What is "SCM"?
SCM = Source Control Management — just a general term for systems like Git, GitHub, GitLab, Bitbucket that track versions of your code. When Jenkins docs say "SCM," they usually just mean "your Git repo."
Cron syntax (used in Poll SCM and Build Periodically)
Jenkins uses a 5-field cron-like format:
MINUTE HOUR DAY_OF_MONTH MONTH DAY_OF_WEEK
Examples:
- H/5 * * * * → every 5 minutes (the H means Jenkins picks a random offset to spread load — this is Jenkins-specific, not standard cron)
- H 9 * * * → once a day, around 9 AM
- H 9 * * 1-5 → once a day, around 9 AM, Monday–Friday only
- 0 0 * * 0 → exactly midnight every Sunday
2. Hands-On: Add a Real Trigger to Your Phase 1 Job
- Open your
hello-world-freestylejob → Configure - Under Build Triggers, check Poll SCM
- Enter schedule:
H/5 * * * * - Save
- Go to your GitHub repo (or the public repo you used), and just wait — or make a small change if it's your own repo — then check back in ~5 minutes to see if a new build triggered automatically. You can also check Manage Jenkins → System Log or the job's build history to confirm it polled.
Note: Poll SCM is a fine learning tool, but it's inefficient in real companies (constant polling wastes resources). The professional approach is a GitHub webhook, which we'll set up properly in Phase 3 once you have a repo you control and Jenkins is reachable from the internet.
3. Parameterized Builds (Concept)
A parameter lets you pass different input values into the same job each time you run it — instead of hardcoding everything. Think of it like passing arguments into a function.
Common parameter types:
- String Parameter — free text (e.g., a version number, a branch name)
- Choice Parameter — dropdown with fixed options (e.g., dev, staging, prod)
- Boolean Parameter — checkbox, true/false (e.g., "run tests? yes/no")
- Password Parameter — hidden text input (rarely used now — Credentials store is safer, covered in Phase 3)
Hands-On: Add Parameters
- Open
hello-world-freestyle→ Configure - Check This project is parameterized
- Click Add Parameter → Choice Parameter
- Name:
ENVIRONMENT- Choices (one per line):dev staging prod - Click Add Parameter → Boolean Parameter
- Name:
RUN_TESTS- Default value: checked - Scroll to your Execute shell build step and update it to:
bash echo "Deploying to environment: $ENVIRONMENT" if [ "$RUN_TESTS" = "true" ]; then echo "Running tests..." else echo "Skipping tests." fi - Save. Notice the job no longer has a plain "Build Now" button — it now says "Build with Parameters".
- Click it, choose an environment from the dropdown, toggle the checkbox, and run it.
- Check Console Output — confirm your choices were reflected in the log.
What just happened: You made one job flexible enough to behave differently based on input, instead of writing three separate jobs for dev/staging/prod. This is the beginning of "one pipeline, many environments" thinking.
4. Environment Variables (Concept)
Jenkins automatically injects certain variables into every build. You already saw a couple in Phase 1 ($BUILD_NUMBER, $JOB_NAME). Some other useful built-in ones:
| Variable | Meaning |
|---|---|
$BUILD_NUMBER |
Sequential number of this run (1, 2, 3...) |
$JOB_NAME |
Name of the job |
$BUILD_ID |
Same as build number (older format) |
$WORKSPACE |
Full path to the job's working directory on the agent |
$JENKINS_URL |
The base URL of your Jenkins server |
$GIT_COMMIT |
Commit hash that was checked out (only if using Git SCM) |
$GIT_BRANCH |
Branch that was checked out |
Try adding this to your shell step to explore:
echo "Workspace path: $WORKSPACE"
echo "Git commit: $GIT_COMMIT"
echo "Git branch: $GIT_BRANCH"
5. Post-Build Actions (Concept)
A post-build action runs after your main build steps finish — regardless of whether the build succeeded or failed (depending on which action you pick). Common ones:
- Archive the artifacts — saves specific output files (e.g., a zip, a log, a report) so you can download them later from the build page, instead of them disappearing when the workspace gets cleaned
- E-mail Notification — sends an email on failure/success (needs SMTP configured under Manage Jenkins → System, skip for now unless you want to set up a test mail server)
- Trigger a downstream job — start another job once this one finishes ("Build other projects")
Hands-On: Archive an Artifact
- Open
hello-world-freestyle→ Configure - In the Execute shell step, add a line that creates a file:
bash echo "Build $BUILD_NUMBER completed for $ENVIRONMENT" > result.txt - Scroll down to Post-build Actions → Add post-build action → Archive the artifacts
- In Files to archive, enter:
result.txt - Save, then Build with Parameters again
- Go to the completed build page (e.g.,
#5) — you'll now see a "Build Artifacts" section withresult.txtlisted as a downloadable file
This is exactly how, later, you'll archive things like test reports or a packaged application.
6. Build History & Workspace Management (Concept)
- Build History (left sidebar of a job) — every past run, color-coded: 🟢 blue/green = success, 🔴 red = failed, 🟡 yellow = unstable (tests failed but build didn't crash)
- Workspace — click "Workspace" on the job's sidebar to browse files currently sitting in that job's working directory on the agent. This is useful for debugging — "what files does Jenkins actually see when it runs?"
- Jenkins keeps build history/logs based on rules you can set under Discard old builds (Configure → General) — e.g., "keep only the last 10 builds" — important in real setups so disk doesn't fill up.
Hands-On: Limit Build History
- Configure → General → check Discard old builds
- Set Max # of builds to keep:
10 - Save
7. Checkpoint — Can You Answer These?
- What's the difference between Poll SCM and a webhook trigger, and why is a webhook usually better?
- What are the three parameter types you used, and what's each one good for?
- Name two built-in Jenkins environment variables and what they contain.
- What does "archiving an artifact" actually do, and why does it matter?
- Why would a company configure "Discard old builds"?
If you can answer all five without checking notes, you're ready for Phase 3: Source Control Integration (properly connecting Jenkins to GitHub with webhooks, and managing credentials securely instead of pasting tokens into scripts).
Quick Reference — This Phase
# Cron examples for triggers
H/5 * * * * # every ~5 minutes
H 9 * * * # once daily, ~9 AM
H 9 * * 1-5 # once daily, weekdays only
# Useful Jenkins built-in variables inside a shell step
$BUILD_NUMBER
$JOB_NAME
$WORKSPACE
$GIT_COMMIT
$GIT_BRANCH
Next: Phase 3 — Source Control Integration (GitHub webhooks, credentials management). Ask me when you're ready.
Phase 3 of 7
Source Control
GitHub webhooks and secure credentials management.
Goal of this phase: Connect Jenkins to a real GitHub repo you control, trigger builds instantly via webhooks instead of polling, and manage credentials the secure way instead of hardcoding secrets.
No AWS/Terraform/Ansible needed. You'll need a free GitHub account and a repo you own (not just a public repo you don't control, like in Phase 1).
1. Why This Phase Matters
So far, Jenkins has pulled from a public repo you don't own, and you've triggered builds manually or via polling. In real jobs, Jenkins needs to: - React instantly when your team pushes code (webhook, not polling) - Authenticate to private repos, Docker registries, cloud accounts — without secrets sitting in plain text anywhere
This phase fixes both.
2. Set Up a Repo You Control
- Log into GitHub (create a free account if you don't have one: github.com)
- Click New repository
- Name:
jenkins-practice- Visibility: Public (simpler for webhooks; private works too but needs extra credential setup, covered in section 5) - Check Add a README - Clone it to your machine and add a simple file:
bash git clone https://github.com/<your-username>/jenkins-practice.git cd jenkins-practice echo "echo 'Hello from my own repo!'" > build.sh git add build.sh git commit -m "Add build script" git push
You now own a repo Jenkins can watch for changes.
3. GitHub Webhooks (Concept)
Polling (Phase 2) = Jenkins repeatedly asks GitHub "anything new?" every few minutes — wasteful and slow.
Webhook = GitHub itself sends Jenkins a message ("something changed!") the instant you push code. Jenkins reacts immediately. This is the standard, professional setup.
For GitHub to reach your Jenkins server, Jenkins must be reachable from the internet (a public IP/URL). This is one reason production Jenkins runs on cloud servers rather than a home laptop.
- If you installed Jenkins on EC2 (Phase 1, Method B), it already has a public IP — perfect for this.
- If you're running Jenkins via Docker on your local machine, GitHub can't reach
localhost. You have two options: - Use your EC2-hosted Jenkins instead for this phase, or
- Use a tunneling tool like
ngrokto expose your local Jenkins temporarily (quick option, good for practice):bash # Install ngrok, then: ngrok http 8080This gives you a temporary public URL likehttps://abcd1234.ngrok-free.appthat forwards to your local Jenkins.
Hands-On: Configure the Webhook
- In GitHub, go to your
jenkins-practicerepo → Settings → Webhooks → Add webhook - Payload URL:
http://<your-jenkins-url>:8080/github-webhook/(must end in/github-webhook/exactly) - Content type:
application/json - Which events: "Just the push event" is fine for now
- Click Add webhook — GitHub will do a test ping; check it shows a green checkmark (✅) under "Recent Deliveries"
Hands-On: Configure Jenkins to Use the Webhook
- Create a new Freestyle job (or reuse an existing one):
hello-world-webhook - Under Source Code Management → Git, enter:
https://github.com/<your-username>/jenkins-practice.git - Under Build Triggers, check GitHub hook trigger for GITScm polling (note: different checkbox from "Poll SCM")
- Add a build step: Execute shell:
bash chmod +x build.sh ./build.sh - Save
- Make a small change in your repo and push it:
bash echo "# test change" >> README.md git add README.md git commit -m "Trigger webhook test" git push - Within a few seconds, check Jenkins — a new build should have started automatically, no polling delay.
If it doesn't trigger: check GitHub's webhook "Recent Deliveries" tab for the response code Jenkins sent back — a non-200 response tells you exactly what failed (wrong URL, Jenkins unreachable, etc.). This is the standard way to debug webhook issues.
4. Credentials Management (Concept)
The problem: Jobs often need secrets — a GitHub token to access a private repo, a password, an API key. Pasting these directly into shell scripts or job configs is a security risk (visible in logs, in job config XML, to anyone with Jenkins access).
The solution: Jenkins has a built-in Credentials Store — a secure vault. You store the secret once, give it an ID, and reference that ID in jobs. Jenkins masks the actual value in console output automatically.
Types of credentials Jenkins supports
| Type | Used for |
|---|---|
| Username with password | Basic auth to services |
| SSH Username with private key | Git over SSH, connecting to remote servers |
| Secret text | API tokens, single secret strings |
| Secret file | Config files, .pem keys, kubeconfig files |
| Certificate | Client certs (X.509) |
Hands-On: Store a GitHub Personal Access Token
- In GitHub: Settings (your profile, not the repo) → Developer settings → Personal access tokens → Tokens (classic) → Generate new token
- Give it
reposcope - Copy the token immediately (shown only once) - In Jenkins: Manage Jenkins → Credentials → System → Global credentials → Add Credentials
- Kind: Username with password
- Username: your GitHub username
- Password: paste the token (NOT your actual GitHub password — tokens are safer and revocable)
- ID:
github-token(this is the reference name you'll use in jobs) - Save
Hands-On: Use the Credential in a Job
- Open a job → Configure → Source Code Management → Git
- Under Credentials, select the dropdown → your
github-tokenentry should now appear - This lets Jenkins clone private repos, or push commits back, without you ever typing the token into a script
Try it: Make your jenkins-practice repo private (GitHub repo Settings → Change visibility), then confirm the job still works because it now authenticates via the stored credential instead of relying on public access.
5. Using Credentials Inside Build Steps
Sometimes you need to use a secret inside a shell command (e.g., an API key for a script), not just for Git checkout.
- Configure a job → Build Environment → check Use secret text(s) or file(s)
- Bind a credential to an environment variable, e.g., variable name
MY_TOKENbound to a Secret text credential - In your shell step, reference
$MY_TOKEN— Jenkins injects it at runtime and masks it in the console log (shows as****)
This is the safe pattern you'll reuse constantly once you get to Docker registry logins, cloud credentials, etc.
6. Checkpoint — Can You Answer These?
- Why is a webhook better than Poll SCM, in your own words?
- Why can't GitHub send a webhook to a Jenkins running on
localhostwithout something like ngrok? - What's the difference between "Username with password" and "Secret text" credential types — when would you use each?
- Why shouldn't you paste an API token directly into a shell script build step?
- What does Jenkins do automatically to protect a secret value in the console output?
If you can answer all five, you're ready for Phase 4: Jenkins Pipelines — this is the big one, where you'll write your first Jenkinsfile and move from clicking around the UI to pipeline-as-code, which is how virtually all real-world Jenkins usage works.
Quick Reference — This Phase
Webhook payload URL format:
http://<jenkins-host>:8080/github-webhook/
ngrok (expose local Jenkins temporarily):
ngrok http 8080
Credential reference pattern:
Store secret once with an ID → reference that ID in job config
→ Jenkins injects + masks it at runtime
Next: Phase 4 — Jenkins Pipelines (Jenkinsfile, declarative syntax, stages). Ask me when you're ready.
Phase 4 of 7
Pipelines
Jenkinsfile, declarative syntax, parallel stages, approval gates.
Goal of this phase: Move from clicking around the UI (Freestyle jobs) to writing pipeline-as-code with a Jenkinsfile — this is how virtually all real-world Jenkins usage works, and it's the core skill interviewers will test.
No AWS/Terraform/Ansible needed yet. Builds directly on the jenkins-practice repo and credentials you set up in Phase 3.
1. Why Pipelines Instead of Freestyle Jobs? (Concept)
Everything you did in Phases 1–3 was configured by clicking through the Jenkins UI. That configuration lives inside Jenkins itself — it's not version-controlled, not easily reviewed, and not portable.
A Pipeline flips this: you write the entire build process as code in a file called a Jenkinsfile, which you commit to your repo alongside your actual project. Benefits:
- Version-controlled (see history, diffs, who changed what)
- Code review possible (pull requests on your CI/CD process itself)
- Portable (works the same across any Jenkins instance)
- Supports much more complex logic than Freestyle jobs allow (parallel stages, conditional logic, retries)
This is the single most important shift in this whole learning path. Everything after this phase builds on it.
2. Declarative vs Scripted Pipeline (Concept)
Jenkins supports two pipeline syntaxes:
| Declarative | Scripted | |
|---|---|---|
| Syntax style | Structured, predictable blocks (pipeline { stages { ... } }) |
Full Groovy programming language, very flexible |
| Learning curve | Easier, recommended for most use cases | Steeper, needed for advanced edge cases |
| Readability | Easier for teams to read/review | Can get complex quickly |
| What you'll learn here | This one — used in ~90% of real Jenkinsfiles | Mentioned for awareness only |
We'll focus entirely on Declarative Pipeline syntax — it's what you'll use almost all the time in real jobs.
3. Anatomy of a Jenkinsfile (Concept)
pipeline {
agent any // Where this runs (which agent/node)
environment {
GREETING = "Hello" // Environment variables available to all stages
}
stages {
stage('Build') { // A named phase of the pipeline
steps {
sh 'echo "Building..."'
}
}
stage('Test') {
steps {
sh 'echo "Testing..."'
}
}
stage('Deploy') {
steps {
sh 'echo "Deploying..."'
}
}
}
post { // Runs after all stages, regardless of outcome
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
always {
echo 'This always runs.'
}
}
}
Key blocks explained
| Block | Purpose |
|---|---|
pipeline { } |
Wraps the entire thing — every declarative Jenkinsfile starts here |
agent |
Where the pipeline executes. agent any = run on any available agent. Can be scoped per-stage too. |
environment { } |
Defines variables usable throughout the pipeline via $VAR_NAME |
stages { } |
Container for all your named stages — the visual "steps" you'll see in the Jenkins UI |
stage('Name') { } |
One logical phase (Build, Test, Deploy, etc.) — shows up as a box in the pipeline visualization |
steps { } |
The actual commands/actions inside a stage |
sh |
Runs a shell command (use bat instead if you're on Windows agents) |
post { } |
Cleanup/notification logic — runs after the pipeline, with conditions like success, failure, always, unstable |
4. Hands-On: Your First Jenkinsfile
- In your
jenkins-practicerepo (from Phase 3), create a file named exactlyJenkinsfile(no extension) in the root:bash cd jenkins-practice -
Create the file with this content: ```groovy pipeline { agent any
environment { GREETING = "Hello from my first Jenkinsfile" }
stages { stage('Build') { steps { echo "${GREETING}" sh 'echo "Simulating build step..."' } } stage('Test') { steps { sh 'echo "Simulating tests..."' } } stage('Deploy') { steps { sh 'echo "Simulating deploy step..."' } } }
post { success { echo 'All stages completed successfully!' } failure { echo 'Something went wrong.' } } }
3. Commit and push:bash git add Jenkinsfile git commit -m "Add first Jenkinsfile" git push`` 4. In Jenkins, click **New Item** - Name:hello-world-pipeline- Type: **Pipeline** (not Freestyle this time) 5. Under **Pipeline** section at the bottom of the config page: - Definition: **Pipeline script from SCM** - SCM: **Git** - Repository URL: yourjenkins-practicerepo URL - Credentials: select yourgithub-tokencredential from Phase 3 if the repo is private - Script Path:Jenkinsfile` (default, matches the filename you created) 6. Save → Build Now 7. Watch the Stage View — you'll see boxes for Build/Test/Deploy light up green as each completes. This visualization is one of the biggest advantages of Pipelines over Freestyle jobs. 8. Click into the build → Console Output to see the full log, same as before.
This is "Pipeline as Code from SCM" — Jenkins is now reading its instructions from your Git repo, not from UI clicks. If you change the Jenkinsfile and push, the next build automatically uses the new logic — no need to touch Jenkins config at all.
5. Parameters, Conditionals, and when (Concept + Hands-On)
Declarative pipelines support parameters and conditional stage execution, similar to what you did in Phase 2 but as code.
Update your Jenkinsfile:
pipeline {
agent any
parameters {
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'prod'], description: 'Target environment')
booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Run tests?')
}
stages {
stage('Build') {
steps {
echo "Building for ${params.ENVIRONMENT}"
}
}
stage('Test') {
when {
expression { params.RUN_TESTS == true }
}
steps {
sh 'echo "Running tests..."'
}
}
stage('Deploy to Prod') {
when {
expression { params.ENVIRONMENT == 'prod' }
}
steps {
sh 'echo "Deploying to PRODUCTION - be careful!"'
}
}
}
}
Commit, push, then run Build with Parameters on your pipeline job. Try different combinations:
- ENVIRONMENT=dev, RUN_TESTS=false → Test stage should be skipped (shown gray in Stage View, not run)
- ENVIRONMENT=prod, RUN_TESTS=true → All stages run, including "Deploy to Prod"
This when { expression { ... } } pattern is exactly how real pipelines decide "only deploy to prod if this is the main branch" or "only run this stage on Fridays," etc.
6. Parallel Stages (Concept + Hands-On)
Sometimes stages don't depend on each other and can run at the same time to save time (e.g., running unit tests and linting simultaneously).
stage('Parallel Checks') {
parallel {
stage('Unit Tests') {
steps {
sh 'echo "Running unit tests..."'
}
}
stage('Lint Check') {
steps {
sh 'echo "Running lint check..."'
}
}
}
}
Add this as a new stage in your Jenkinsfile, push, and rebuild. In the Stage View, you'll see both boxes execute side-by-side instead of one after another — this is a real time-saver on larger real-world pipelines.
7. Manual Approval Gates with input (Concept + Hands-On)
For risky steps (like deploying to production), you often want a human to approve before Jenkins proceeds.
stage('Approval') {
steps {
input message: 'Deploy to production?', ok: 'Yes, deploy'
}
}
stage('Deploy to Prod') {
steps {
sh 'echo "Deploying to production now..."'
}
}
Add this before your prod deploy stage, push, and rebuild. The pipeline will pause and show a clickable prompt in the Jenkins UI (and on the build page) waiting for someone to click "Yes, deploy" or abort. This is a critical real-world safety pattern — you'll use it constantly once deployments matter.
8. Checkpoint — Can You Answer These?
- What is a
Jenkinsfile, and where does it live? - What's the practical difference between Declarative and Scripted pipeline syntax, and which should you default to?
- What does
agent anymean? - How do you make a stage run only under certain conditions?
- What does
parallel { }do, and when would you use it? - What does the
inputstep do, and why is it useful before a production deploy? - What does "Pipeline script from SCM" mean, and how is it different from pasting a pipeline script directly into the Jenkins UI?
If you can answer all seven, you're ready for Phase 5: Integrations with your existing stack — this is where Jenkins pipelines start calling Docker, and (once you're ready to learn them) Terraform, Ansible, and AWS. Since you said not to assume that knowledge yet, just let me know when you want Phase 5, and I'll either build it Docker-only for now, or fold in a "crash course" primer on Terraform/Ansible/AWS basics first — your call.
Quick Reference — This Phase
pipeline {
agent any
parameters {
choice(name: 'X', choices: ['a','b'], description: '')
booleanParam(name: 'Y', defaultValue: true, description: '')
}
stages {
stage('Name') {
when { expression { params.Y == true } }
steps { sh 'command' }
}
stage('Parallel Example') {
parallel {
stage('A') { steps { sh 'echo A' } }
stage('B') { steps { sh 'echo B' } }
}
}
stage('Gate') {
steps { input message: 'Proceed?', ok: 'Go' }
}
}
post {
success { echo 'OK' }
failure { echo 'Failed' }
}
}
Next: Phase 5 — Integrations with Docker (and optionally Terraform/Ansible/AWS with a primer first, your choice). Ask me when you're ready.
Phase 5 of 7
Docker
Build and push Docker images from a pipeline.
Goal of this phase: Get Jenkins building and pushing Docker images as part of a pipeline. This is the most common real-world use of Jenkins today — package an app into a container image automatically on every push.
Assumption for this phase: Docker-only, no Terraform/Ansible/AWS required. (When you're ready to add cloud deployment, let me know and I'll either give you a primer first or fold it into a later phase.)
Builds on Phase 4 — you'll extend the same jenkins-practice Jenkinsfile.
1. Why Docker + Jenkins? (Concept)
Instead of just running shell commands, most real pipelines end with: "package this app as a Docker image, then push it somewhere (Docker Hub, AWS ECR, etc.) so it can be deployed anywhere." Jenkins automates that packaging + publishing step every time code changes.
Basic flow you're building today:
Push code → Jenkins triggers → Jenkins builds Docker image → Jenkins pushes image to Docker Hub
2. Prerequisite: Give Jenkins Access to Docker
Jenkins needs the docker command available wherever the pipeline runs (the agent).
If Jenkins is running via Docker (Phase 1, Method A)
The Jenkins container needs access to the host's Docker daemon — this is done by mounting the Docker socket:
# Stop and remove the old container (data is safe in the jenkins_home volume)
docker stop jenkins
docker rm jenkins
# Re-run with Docker socket mounted so Jenkins can build/run Docker images
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-u root \
jenkins/jenkins:lts
# Install the docker CLI inside the container
docker exec -u root jenkins apt-get update
docker exec -u root jenkins apt-get install -y docker.io
Note: -u root and mounting the Docker socket directly means the Jenkins container can control the host's Docker — convenient for learning, but a known security tradeoff in production (out of scope for now, just be aware it's a shortcut).
If Jenkins is running natively on EC2 (Phase 1, Method B)
Install Docker on the same EC2 instance, and give the jenkins user permission to use it:
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
# Add jenkins user to the docker group so it can run docker commands
sudo usermod -aG docker jenkins
# Restart Jenkins so the group change takes effect
sudo systemctl restart jenkins
Verify Jenkins can see Docker
Create a quick test Freestyle job (or reuse hello-world-freestyle) with build step:
docker --version
docker ps
Run it — if you see version output and no permission errors, you're set.
3. Docker Hub Account + Credentials (Concept + Hands-On)
You need somewhere to push images. Docker Hub is the free, standard option.
- Create a free account at hub.docker.com if you don't have one
- Create an Access Token (Account Settings → Security → New Access Token) — safer than using your real password, and revocable
- In Jenkins: Manage Jenkins → Credentials → Global → Add Credentials
- Kind: Username with password
- Username: your Docker Hub username
- Password: the access token
- ID:
dockerhub-creds - Save
This follows the exact same pattern as the GitHub token credential you set up in Phase 3 — same mechanism, different service.
4. A Minimal App to Containerize
Add a tiny app to your jenkins-practice repo so there's something real to build.
cd jenkins-practice
Create app.py:
print("Hello from my containerized app!")
Create Dockerfile:
FROM python:3.12-slim
COPY app.py /app.py
CMD ["python", "/app.py"]
Commit and push:
git add app.py Dockerfile
git commit -m "Add minimal app and Dockerfile"
git push
5. Update the Jenkinsfile: Build & Push a Docker Image
Extend the Jenkinsfile from Phase 4:
pipeline {
agent any
environment {
DOCKERHUB_CREDENTIALS = credentials('dockerhub-creds')
IMAGE_NAME = "<your-dockerhub-username>/jenkins-practice"
IMAGE_TAG = "${BUILD_NUMBER}"
}
stages {
stage('Build') {
steps {
echo "Building image ${IMAGE_NAME}:${IMAGE_TAG}"
}
}
stage('Docker Build') {
steps {
sh 'docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .'
}
}
stage('Docker Login') {
steps {
sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
}
}
stage('Docker Push') {
steps {
sh 'docker push ${IMAGE_NAME}:${IMAGE_TAG}'
}
}
stage('Also tag latest') {
steps {
sh '''
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
docker push ${IMAGE_NAME}:latest
'''
}
}
}
post {
always {
sh 'docker logout'
}
success {
echo "Image pushed: ${IMAGE_NAME}:${IMAGE_TAG}"
}
failure {
echo 'Docker build/push failed.'
}
}
}
What's new here, explained
| Line | What it does |
|---|---|
credentials('dockerhub-creds') |
Pulls in the stored credential. Jenkins automatically creates DOCKERHUB_CREDENTIALS_USR and DOCKERHUB_CREDENTIALS_PSW variables from it. |
${BUILD_NUMBER} as the tag |
Every build produces a uniquely tagged image — you can always trace an image back to the exact build that made it. |
docker login ... --password-stdin |
Logs in without ever printing the password in plain text in a command (--password-stdin reads it from piped input instead of a CLI argument) |
Tagging latest too |
Common convention — latest always points to the most recent successful build, while numbered tags let you roll back to any specific past version |
post { always { docker logout } } |
Cleanup step — always log out afterward, regardless of success/failure |
Replace <your-dockerhub-username> with your actual username, commit, push, then run the pipeline.
6. Verify
- Check the Jenkins Console Output — confirm each stage succeeded
- Go to hub.docker.com → your repositories → you should see
jenkins-practicewith at least two tags (latestand a build number like3) - Optionally pull and run it yourself to prove the whole loop worked:
bash docker pull <your-dockerhub-username>/jenkins-practice:latest docker run <your-dockerhub-username>/jenkins-practice:latestYou should seeHello from my containerized app!printed.
7. Checkpoint — Can You Answer These?
- Why does the Jenkins container need the Docker socket mounted (Docker-in-Docker approach), or the
jenkinsuser added to thedockergroup (native install)? - What does
credentials('dockerhub-creds')do insideenvironment { }, and what two variables does it create automatically? - Why tag each image with
${BUILD_NUMBER}instead of only ever usinglatest? - Why use
--password-stdininstead of putting the password directly in thedocker logincommand? - What's the difference between
docker build,docker tag, anddocker push?
If you can answer all five, you're ready for Phase 6: Production-Grade Practices — distributed agents, security/RBAC, backups, and monitoring, which wraps up the core Jenkins curriculum. After that, if you'd like, we can circle back and add Terraform/Ansible/AWS deployment stages on top of everything you've built.
Quick Reference — This Phase
# Give Jenkins Docker access (Docker-based Jenkins)
-v /var/run/docker.sock:/var/run/docker.sock
# Give Jenkins Docker access (native Jenkins)
sudo usermod -aG docker jenkins
# Core Docker commands used in the pipeline
docker build -t <image>:<tag> .
docker login -u <user> --password-stdin
docker push <image>:<tag>
docker tag <image>:<old-tag> <image>:<new-tag>
docker logout
Next: Phase 6 — Production-Grade Practices (distributed agents, security, backups, monitoring). Ask me when you're ready.
Phase 6 of 7
Terraform, Ansible & AWS
Provision infrastructure and deploy your container end to end.
Goal of this phase: Learn just enough Terraform, Ansible, and AWS to wire real infrastructure provisioning and configuration into your Jenkins pipeline. This phase has two parts: (A) a focused primer on each tool standalone, then (B) integrating all three into your existing Jenkinsfile from Phase 5.
Assumes zero prior AWS/Terraform/Ansible knowledge, as requested. Builds on the jenkins-practice repo and Docker image pipeline from Phase 5.
Note on scope: This phase is intentionally denser than the others because three new tools are involved. Don't rush it — treat Part A almost like its own mini bootcamp before touching Jenkins again in Part B.
PART A: Standalone Primers
A1. AWS Basics (Primer)
What AWS actually is
AWS (Amazon Web Services) rents you computers, storage, and networking over the internet instead of you buying physical servers. You've already used one core AWS service without necessarily framing it this way: EC2 (Elastic Compute Cloud — virtual servers), which you used in Phase 1.
Core concepts you need for this phase
| Concept | Plain explanation |
|---|---|
| Region | A physical geographic location of AWS data centers (e.g., us-east-1 = Northern Virginia). Pick one and stick with it while learning. |
| EC2 Instance | A virtual server. Has a type (size, e.g., t2.micro = small/free-tier eligible), an OS image (AMI), and a public IP if configured. |
| AMI (Amazon Machine Image) | A template/snapshot used to launch an EC2 instance (e.g., "Amazon Linux 2023" is an AMI). |
| Security Group | A virtual firewall — rules for which ports/IPs can reach your instance (e.g., allow port 22 for SSH, port 8080 for Jenkins). |
| VPC (Virtual Private Cloud) | Your own isolated network inside AWS. AWS gives you a "default VPC" automatically — fine to use while learning. |
| IAM (Identity and Access Management) | Controls who (users, or programs like Jenkins) can do what in your AWS account. |
| IAM User / Access Key | A set of credentials (Access Key ID + Secret Access Key) that lets a program (like Terraform, or Jenkins) authenticate to AWS. |
| S3 (Simple Storage Service) | Object storage — think of it as a place to store files/backups (we'll use it for Terraform state later). |
Hands-On: Create an IAM User for Programmatic Access
You should never use your AWS root account credentials for tools like Terraform. Create a dedicated IAM user instead.
- Log into the AWS Console → search IAM
- Users → Create user
- Name:
terraform-jenkins-user- Do NOT enable console access (this user is only for programmatic/API access) - Attach policies directly → for learning purposes, attach
AdministratorAccess(in a real company you'd scope this much tighter — but broad access simplifies learning right now) - Create the user → go into it → Security credentials tab → Create access key - Use case: "Command Line Interface (CLI)" - Save the Access Key ID and Secret Access Key immediately — the secret is shown only once
Hands-On: Install & Configure AWS CLI
# On Amazon Linux / RHEL-based systems
sudo yum install -y awscli
# Or via pip if not available in package manager
pip install awscli --user
# Configure with your new IAM user's keys
aws configure
# AWS Access Key ID: <paste it>
# AWS Secret Access Key: <paste it>
# Default region name: us-east-1 (or your chosen region)
# Default output format: json
Verify it works:
aws sts get-caller-identity
This should print back your IAM user's account ID and ARN — confirming AWS trusts your credentials.
A2. Terraform Basics (Primer)
What Terraform actually is
Terraform is Infrastructure as Code (IaC) — instead of clicking around the AWS Console to create servers, you describe the infrastructure you want in a text file, and Terraform creates (and later, updates or destroys) it to match.
Core concepts
| Concept | Plain explanation |
|---|---|
| Provider | A plugin that lets Terraform talk to a specific platform (AWS, Azure, GCP, etc.) |
| Resource | A single piece of infrastructure you want (e.g., one EC2 instance, one S3 bucket) |
.tf file |
Where you write your infrastructure definitions, in HashiCorp Configuration Language (HCL) |
State file (terraform.tfstate) |
Terraform's record of what it has already created — critical, don't delete it manually |
| Plan | A preview: "here's what I would create/change/destroy" — nothing happens yet |
| Apply | Actually executes the plan — infrastructure gets created/changed |
| Destroy | Tears down everything Terraform created — very useful for not leaving billable resources running while learning |
Hands-On: Install Terraform
# Amazon Linux / RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum install -y terraform
# Verify
terraform -version
Hands-On: Your First Terraform Config
Create a new folder (separate from jenkins-practice for now):
mkdir terraform-practice && cd terraform-practice
Create main.tf:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1" # match the region you configured in aws configure
}
resource "aws_instance" "practice_server" {
ami = "ami-0c101f26f147fa7fd" # Amazon Linux 2023 (us-east-1) - verify current AMI ID in console if this errors
instance_type = "t2.micro"
tags = {
Name = "terraform-practice-server"
}
}
Important: AMI IDs are region-specific and change over time. Before running this, check the AWS Console → EC2 → AMI Catalog for the current Amazon Linux AMI ID in your region, and swap it in if needed.
Run the Terraform workflow:
terraform init # downloads the AWS provider plugin
terraform plan # shows what will be created - review this carefully
terraform apply # type 'yes' when prompted - actually creates the EC2 instance
Go check the AWS Console → EC2 → Instances — you should see terraform-practice-server running, created entirely from code.
When done experimenting, always clean up to avoid charges:
terraform destroy # type 'yes' when prompted
Key habit to build
Always run terraform plan before terraform apply and actually read the output. It tells you exactly what will change — this review step is what prevents accidental infrastructure damage in real jobs.
A3. Ansible Basics (Primer)
What Ansible actually is
Terraform creates infrastructure (the server itself). Ansible configures what's running inside it — installs packages, copies config files, starts services. Think: Terraform = "build the house," Ansible = "furnish and set up utilities inside it."
Core concepts
| Concept | Plain explanation |
|---|---|
| Control node | The machine running Ansible commands (often your laptop or a CI server) |
| Managed node | The remote server being configured (connects via SSH, no agent needed on the target) |
| Inventory | A file listing which servers Ansible should manage |
| Playbook | A YAML file describing the desired configuration steps ("tasks") |
| Module | A built-in unit of work (e.g., yum, copy, service, template) |
| Task | One step in a playbook, using a module |
| Idempotency | Running the same playbook twice produces the same end state — it won't "double install" things unnecessarily |
Hands-On: Install Ansible
# Amazon Linux / RHEL
sudo yum install -y ansible
# Verify
ansible --version
Hands-On: Inventory File
Create inventory.ini (point this at the EC2 instance you created with Terraform, or any test EC2 instance you have SSH access to):
[webservers]
practice_server ansible_host=<EC2_PUBLIC_IP> ansible_user=ec2-user ansible_ssh_private_key_file=~/path/to/your-key.pem
Test connectivity:
ansible webservers -i inventory.ini -m ping
A successful response looks like "ping": "pong" in green — confirms Ansible can reach and authenticate to the server.
Hands-On: Your First Playbook
Create setup.yml:
---
- name: Configure web server
hosts: webservers
become: true # run tasks with sudo
tasks:
- name: Install nginx
yum:
name: nginx
state: present
- name: Start and enable nginx
service:
name: nginx
state: started
enabled: true
- name: Deploy a simple index page
copy:
content: "<h1>Configured by Ansible!</h1>"
dest: /usr/share/nginx/html/index.html
Run it:
ansible-playbook -i inventory.ini setup.yml
Then open http://<EC2_PUBLIC_IP> in a browser (make sure port 80 is open in the instance's Security Group) — you should see "Configured by Ansible!"
Run the playbook a second time and notice the output shows tasks as "ok" (unchanged) rather than "changed" where nothing needed to change — that's idempotency in action.
PART B: Integrating Terraform, Ansible & Docker into Jenkins
Now the payoff: wire all of this into the Jenkinsfile you built in Phase 5, so a single pipeline run can provision infrastructure, configure it, and deploy your Docker image to it.
B1. Store AWS Credentials in Jenkins
Same credentials pattern as GitHub (Phase 3) and Docker Hub (Phase 5):
- Manage Jenkins → Credentials → Global → Add Credentials
- Kind: Secret text (add two separate entries)
- Secret: your AWS Access Key ID → ID:
aws-access-key-id- Secret: your AWS Secret Access Key → ID:aws-secret-access-key
B2. Install Terraform & Ansible on the Jenkins Agent
Same machine/container that runs your pipeline needs both tools available (same idea as installing Docker CLI in Phase 5).
# If Jenkins runs natively on EC2, install directly on that instance:
sudo yum install -y ansible
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum install -y terraform
# If Jenkins runs in Docker, exec into the container and install (apt-based image):
docker exec -u root jenkins bash -c "apt-get update && apt-get install -y ansible unzip curl"
docker exec -u root jenkins bash -c "curl -O https://releases.hashicorp.com/terraform/1.9.0/terraform_1.9.0_linux_amd64.zip && unzip terraform_1.9.0_linux_amd64.zip -d /usr/local/bin/"
B3. Add Terraform + Ansible Files to Your Repo
In your jenkins-practice repo, add a terraform/ folder with the main.tf from Part A2 (adjust the EC2 to also open port 80 for the app, and consider outputting the public IP — see below), and an ansible/ folder with inventory.ini and setup.yml adapted to deploy your Docker container instead of nginx directly.
Add an output to your terraform/main.tf so the pipeline can capture the new server's IP automatically:
output "instance_public_ip" {
value = aws_instance.practice_server.public_ip
}
B4. Extend the Jenkinsfile
Add these stages to your existing Jenkinsfile (after the Docker Push stage from Phase 5):
environment {
// ...existing DOCKERHUB vars from Phase 5...
AWS_ACCESS_KEY_ID = credentials('aws-access-key-id')
AWS_SECRET_ACCESS_KEY = credentials('aws-secret-access-key')
AWS_DEFAULT_REGION = 'us-east-1'
}
stage('Terraform Init & Apply') {
steps {
dir('terraform') {
sh 'terraform init'
sh 'terraform plan -out=tfplan'
input message: 'Apply this Terraform plan?', ok: 'Apply' // manual gate, from Phase 4
sh 'terraform apply -auto-approve tfplan'
}
}
}
stage('Capture Server IP') {
steps {
dir('terraform') {
script {
env.SERVER_IP = sh(
script: 'terraform output -raw instance_public_ip',
returnStdout: true
).trim()
}
echo "Provisioned server IP: ${env.SERVER_IP}"
}
}
}
stage('Ansible Configure & Deploy') {
steps {
dir('ansible') {
sh """
echo "[webservers]" > inventory.ini
echo "server ansible_host=${env.SERVER_IP} ansible_user=ec2-user" >> inventory.ini
ansible-playbook -i inventory.ini setup.yml \
--extra-vars "docker_image=${IMAGE_NAME}:${IMAGE_TAG}" \
--ssh-common-args='-o StrictHostKeyChecking=no'
"""
}
}
}
What this pipeline now does, end to end: 1. Builds your app's Docker image (Phase 5) 2. Pushes it to Docker Hub (Phase 5) 3. Uses Terraform to provision a fresh EC2 instance (or update an existing one) — with a manual approval gate before anything is actually created, since infra changes cost money and carry risk 4. Captures the new server's IP automatically from Terraform's output 5. Hands that IP to Ansible, which connects via SSH and configures the server — pulling and running your Docker image
This is the full loop described back in the original 6-phase plan: GitHub push → Jenkins → Docker build/push → Terraform provisions → Ansible configures/deploys.
Important reminder: run terraform destroy manually (or add a separate "teardown" pipeline job) when you're done practicing, so you don't leave EC2 instances running and racking up charges.
Checkpoint — Can You Answer These?
- In your own words, what's the difference in responsibility between Terraform and Ansible?
- Why create a dedicated IAM user for Terraform/Jenkins instead of using your AWS root account?
- Why should you always run
terraform planbeforeterraform apply? - What does "idempotency" mean, and why does it matter for Ansible playbooks?
- In the Jenkinsfile, how does the pipeline get the new server's IP address without you typing it in manually?
- Why is there a manual
inputgate beforeterraform applyin the pipeline?
If you can answer all six, you've now covered the entire original 6-tool integration loop. From here, Phase 7: Production-Grade Practices (distributed agents, RBAC/security, backups, monitoring) is the natural next and final step in the core curriculum.
Quick Reference — This Phase
# AWS
aws configure
aws sts get-caller-identity
# Terraform
terraform init
terraform plan
terraform apply
terraform destroy
terraform output -raw <output_name>
# Ansible
ansible <group> -i inventory.ini -m ping
ansible-playbook -i inventory.ini playbook.yml
Next: Phase 7 — Production-Grade Practices (distributed agents, security, backups, monitoring). Ask me when you're ready.
Phase 7 of 7
Production Practices
Distributed agents, access control, backups, monitoring.
Goal of this phase: Take everything you've built (Phases 1–6) and learn the practices that separate a "learning lab" Jenkins from one a company could actually trust in production — distributed agents, access control, backups, and monitoring.
This is the final phase of the core curriculum. Builds on everything before it — your jenkins-practice repo, Docker/Terraform/Ansible pipeline, and credentials store.
1. Distributed Builds: Controller + Multiple Agents (Concept)
So far, everything has run on the built-in node (the controller doing double duty as an agent). In production, this doesn't scale — you want the controller focused on orchestration/UI, while separate agent machines actually run builds. Benefits: - Parallel capacity — many builds run at once across different machines - Isolation — one heavy/broken build doesn't slow down or crash Jenkins itself - Specialization — some agents can have Docker + AWS CLI, others might have Windows-only tooling, etc.
Ways to connect an agent
| Method | How it works |
|---|---|
| SSH | Controller connects to the agent machine over SSH and launches an agent process — most common, simplest to set up |
| JNLP (inbound agent) | The agent machine initiates the connection to the controller (useful when the agent is behind a firewall/NAT) |
| Docker agent / Kubernetes agent | Agents are spun up as containers/pods on demand, then destroyed after the build — very common in modern setups, scales elastically |
Hands-On: Add an SSH Agent
- Launch a second EC2 instance (separate from your Jenkins controller) — this will be your dedicated agent
- Install Java on it (agents need Java too):
bash sudo yum install -y java-17-amazon-corretto - On the Jenkins controller: Manage Jenkins → Credentials — add the agent's SSH private key as a "SSH Username with private key" credential (same pattern you've used before)
- Manage Jenkins → Nodes → New Node
- Name:
agent-1- Type: Permanent Agent - Configure:
- Remote root directory:
/home/ec2-user/jenkins-agent(any writable path) - Labels:linux docker(tags you can target in pipelines) - Launch method: Launch agents via SSH- Host: agent's public IP
- Credentials: the SSH key you just added
- Host Key Verification Strategy: "Non verifying" (fine for learning; stricter in real prod)
- Save — Jenkins will connect and show the agent as online (green) in Manage Jenkins → Nodes
Hands-On: Target a Specific Agent in a Pipeline
In your Jenkinsfile, instead of agent any, target the label:
pipeline {
agent { label 'linux && docker' }
// ...rest of pipeline
}
Or target specific stages to specific agents:
stage('Build') {
agent { label 'docker' }
steps {
sh 'docker build -t myapp .'
}
}
Run the pipeline and confirm (in Console Output, near the top) it says it's running on agent-1 rather than the built-in node.
2. Security: Users, Roles, and Access Control (Concept)
By default, your Jenkins likely has one admin account (from initial setup). In a real team, you need multiple users with different permission levels — not everyone should be able to delete jobs or view credentials.
Key concepts
| Term | Meaning |
|---|---|
| Authentication | Who are you? (login) |
| Authorization | What are you allowed to do? (permissions) |
| Matrix-based security | Built-in Jenkins authorization strategy — grid of users/groups × permissions (Read, Build, Configure, Delete, etc.) |
| Role-Based Strategy (plugin) | More scalable — define roles (e.g., "Developer," "Viewer," "Admin") once, then assign users to roles instead of managing permissions per-user |
Hands-On: Enable Matrix-Based Security
- Install plugin if not present: Manage Jenkins → Plugins → Available → "Role-based Authorization Strategy"
- Manage Jenkins → Security → Authorization → select Role-Based Strategy
- Manage Jenkins → Manage and Assign Roles → Manage Roles
- Create a Global role called
developerwith permissions: Overall Read, Job Build, Job Read, Job Workspace - Create a Global role calledviewerwith only: Overall Read, Job Read - Manage and Assign Roles → Assign Roles — create a second test user (Manage Jenkins → Users → Create User) and assign them the
viewerrole - Log out, log in as the test user — confirm they can see jobs but cannot click "Build Now" or "Configure"
This is exactly the pattern real companies use: junior engineers get viewer or developer, senior/DevOps engineers get broader roles, and only a small admin group gets full control.
A Note on Credentials Visibility
Credentials you stored in earlier phases (GitHub token, Docker Hub, AWS keys) are never shown in plain text to any user, regardless of role — Jenkins masks them by design. Roles control who can use/reference a credential in a job, not who can view the raw secret.
3. Backup & Disaster Recovery (Concept)
Everything Jenkins knows — job configs, credentials, build history, plugins — lives under one directory: JENKINS_HOME.
- Native install: /var/lib/jenkins
- Docker install: inside the jenkins_home volume you mounted back in Phase 1
If this is lost and not backed up, you lose everything — every job, every pipeline, every credential.
Hands-On: Manual Backup (Native Install)
# Stop Jenkins first for a clean, consistent backup (avoids mid-write corruption)
sudo systemctl stop jenkins
# Create a compressed backup
sudo tar -czf jenkins-backup-$(date +%Y%m%d).tar.gz -C /var/lib jenkins
# Restart Jenkins
sudo systemctl start jenkins
# Optional: move the backup somewhere durable, e.g. an S3 bucket (you learned S3 basics in Phase 6)
aws s3 cp jenkins-backup-$(date +%Y%m%d).tar.gz s3://your-backup-bucket/jenkins/
Hands-On: Manual Backup (Docker Install)
docker stop jenkins
docker run --rm \
-v jenkins_home:/var/jenkins_home \
-v $(pwd):/backup \
busybox tar -czf /backup/jenkins-backup-$(date +%Y%m%d).tar.gz -C /var/jenkins_home .
docker start jenkins
Restore (either setup)
Stop Jenkins, extract the backup archive back into JENKINS_HOME (or the mounted volume), then restart. Test this at least once — a backup you've never restored from is not a verified backup.
Better long-term approach (awareness only for now)
In real production, this is usually automated: a scheduled cron job or a Jenkins job itself (a nice "meta" use of Jenkins — using it to back up itself) running nightly, pushing to S3 or similar durable storage with retention rules.
4. Monitoring Jenkins Health (Concept)
You want to know before Jenkins runs out of disk space or memory, not after builds start silently failing.
Built-in options
- Manage Jenkins → System Information — Java version, memory usage snapshot, environment variables
- Manage Jenkins → System Log — real-time Jenkins internal logs, useful for debugging plugin or connectivity issues
Hands-On: Install the Monitoring Plugin
- Manage Jenkins → Plugins → Available → search "Monitoring" → install
- After install, go to Manage Jenkins → Monitoring — you'll get graphs for JVM memory, HTTP request throughput, thread counts, etc.
Connecting to Your Existing AWS Knowledge
Since you now know AWS basics (Phase 6), the production-grade version of this is: install the CloudWatch agent on your Jenkins EC2 instance to ship system-level metrics (CPU, disk, memory) into AWS CloudWatch, then set CloudWatch Alarms (e.g., "alert if disk usage > 85%"). This is genuinely how many companies monitor self-hosted Jenkins — worth exploring once you're comfortable, but treat it as a stretch goal rather than a required exercise here.
5. Optional: Blue Ocean UI
Blue Ocean is a friendlier visual interface for Pipelines specifically (less useful for Freestyle jobs).
- Manage Jenkins → Plugins → Available → search "Blue Ocean" → install
- Click Open Blue Ocean in the left sidebar
- Browse your
hello-world-pipelineorjenkins-practicepipeline — notice the cleaner stage visualization, inline log viewing per-stage, and a more modern layout
Purely optional — functionally it doesn't change what Jenkins does, just how it's presented. Some teams love it, some stick with the classic UI. Worth knowing it exists.
6. Checkpoint — Can You Answer These?
- Why would a company use multiple agents instead of running all builds on the controller?
- What's the difference between authentication and authorization?
- Why can't even an admin-role user see a stored credential's raw value in Jenkins?
- What directory holds everything Jenkins needs to be restored, and why should you stop Jenkins before backing it up?
- Why is "a backup you've never restored from" considered unreliable?
- Name two things you'd want to monitor on a Jenkins server and why each matters.
If you can answer all six, you've completed the full 7-phase Jenkins curriculum — from zero, to a working end-to-end pipeline (GitHub → Jenkins → Docker → Terraform → Ansible), to the production practices real teams rely on.
Quick Reference — This Phase
# SSH agent prerequisite
sudo yum install -y java-17-amazon-corretto
# Backup (native)
sudo systemctl stop jenkins
sudo tar -czf jenkins-backup-$(date +%Y%m%d).tar.gz -C /var/lib jenkins
sudo systemctl start jenkins
# Backup (Docker)
docker stop jenkins
docker run --rm -v jenkins_home:/var/jenkins_home -v $(pwd):/backup busybox \
tar -czf /backup/jenkins-backup-$(date +%Y%m%d).tar.gz -C /var/jenkins_home .
docker start jenkins
// Targeting a labeled agent in a pipeline
pipeline {
agent { label 'linux && docker' }
...
}
What's Next (Beyond This Curriculum)
You now have the full core skillset. A few natural directions from here, whenever you're ready — just ask and I'll build a guide the same way:
- Shared Libraries — reusable Groovy pipeline code across multiple Jenkinsfiles/projects (useful once you have several repos following the same pattern)
- Kubernetes agents — dynamic, on-demand agents instead of static EC2 agents
- Multibranch Pipelines — automatically create a pipeline per Git branch/PR, common in team workflows
- Real project: put the full loop (GitHub → Jenkins → Docker → Terraform → Ansible) on your resume as a portfolio project, since you've now genuinely built it end-to-end
Congratulations on finishing the curriculum — this is a strong, resume-ready skill set for a Cloud & DevOps Engineer role.