Build a Jenkins CI/CD Pipeline on AWS
Build a Jenkins pipeline that tests and deploys a private S3 site on AWS.
Introduction
30 Second Summary
A release can look ready until one missed check sends a broken page live. Reliable delivery gives every change the same path from source to proof.
In this project, you will build a Jenkins CI/CD pipeline on AWS that carries a static page from GitHub to Amazon S3. The pipeline tests each release before deployment through an IAM role with only the access it needs.
What You'll Build
A commit moves through five visible Jenkins stages before a temporary presigned URL opens the tested page from a private bucket.
By the end of this project, you'll have:
- A source-driven delivery pipeline that retrieves your page from GitHub whenever Jenkins detects a new commit.
- A visible build and test gate that preserves a downloadable release artifact only after the required content passes its check.
- A private S3 deployment that opens through a temporary URL without storing long-term AWS access keys in Jenkins or GitHub.
- An optional Secret Mission: commit a broken page to prove the test gate prevents the Deploy and Share stages from running.
Are there any prerequisites?
You need an AWS account with permission to create EC2, S3, security group, and IAM resources. You also need a GitHub account, with browser-based setup included if you have not created one yet.
Before We Start
You are building a delivery path that uses Jenkins to move a tested static index.html release from GitHub to a private Amazon S3 bucket. Locking in why a failed Test stage must stop deployment gives every later step a clear purpose.
Set Up GitHub and AWS Foundations
A source-driven delivery path needs a stable home for its page. Its pipeline definition belongs beside that page.
An AWS Identity and Access Management (IAM) role gives Jenkins an identity it can assume later. A reachable Amazon EC2 host gives the controller a Linux home.
In this step, get ready to:
- Publish the starter source in a public GitHub repository.
- Create an EC2-trusted IAM role with no permission policies.
- Launch a reachable Amazon Linux 2023 EC2 instance.
Create the GitHub source repository
A Jenkinsfile stores your delivery workflow as pipeline as code. Keeping it beside index.html lets each source commit carry the instructions that build it.
- Go to GitHub.com in your browser.
- Sign in to your GitHub account.
- Create a GitHub account from the sign-in page if you do not have one.
- Open the upper-right menu.
- Select New repository.
- Enter jenkins-cicd-demo in the Repository name field.
- Set the repository visibility to Public.
- Select Create repository.
You now have a public repository for the page source. The two project files come next.
- Use the double-check tab below as the source for index.html.
- Use the same tab as the source for the partial Jenkinsfile.
✔️ I added both files
Your repository should show index.html beside Jenkinsfile on main.
- Confirm the repository visibility is public.
- Confirm no AWS access keys appear in either file.
- Confirm no passwords appear in either file.
ⓧ I'd like to double check the full code
- Select Add file in jenkins-cicd-demo.
- Select Create new file.
- Enter index.html as the file name.
- Paste this exact content into index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Jenkins CI/CD Demo</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: linear-gradient(135deg, #111827, #1e3a8a);
}
main {
width: min(42rem, calc(100% - 3rem));
padding: 3rem;
border: 1px solid #60a5fa;
border-radius: 1rem;
background: rgba(17, 24, 39, 0.9);
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.35);
}
h1 {
margin-top: 0;
color: #93c5fd;
}
code {
color: #fde68a;
}
</style>
</head>
<body>
<main>
<h1>Jenkins CI/CD is live</h1>
<p>This page passed the Jenkins test gate and was deployed to private Amazon S3.</p>
<p>Delivery path: <code>GitHub -> Jenkins -> Amazon S3</code></p>
</main>
</body>
</html>
What does this file do?
- The index.html file defines the page Jenkins later packages.
- The CSS makes the deployed result easy to recognize in a browser.
- The heading supplies the phrase required by the future Test stage.
- Select Commit changes.
- Commit the file directly to main.
The repository file list now shows index.html.
Cannot find index.html?
Check that the filename includes the .html extension. Confirm that the commit targets main.
Help me fix my missing GitHub file.
- Select Add file from the repository file list.
- Select Create new file.
- Enter Jenkinsfile as the file name.
- Paste this exact partial pipeline into Jenkinsfile:
pipeline {
agent any
triggers {
pollSCM('H/5 * * * *')
}
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '5'))
}
stages {
stage('Build') {
steps {
sh 'rm -rf dist && mkdir -p dist && cp index.html dist/index.html'
}
}
stage('Test') {
steps {
sh 'grep -q "Jenkins CI/CD is live" dist/index.html'
}
}
stage('Archive') {
steps {
archiveArtifacts artifacts: 'dist/index.html', fingerprint: true
}
}
}
}
What does this pipeline do?
- The top-level pipeline block defines a Declarative Pipeline.
- The pollSCM('H/5 * * * *') trigger checks the source repository for changes.
- The options prevent overlapping runs. They retain five builds.
- The Build stage copies index.html into dist/index.html.
- The Test stage checks for the required heading.
- The Archive stage preserves the tested release candidate.
- Select Commit changes.
- Commit the file directly to main.
The repository file list now shows index.html beside Jenkinsfile.
Cannot see both files?
Return to the repository file list. Confirm the branch selector shows main.
Help me check my GitHub repository files.
Create the empty EC2 role
An EC2 role lets software on the instance receive temporary AWS credentials through an instance profile. This project begins with the identity in place while its permission policies remain empty.
- Go to the AWS Management Console in your browser.
- Sign in to your AWS account.
- Use the console search to open IAM.
- Select Roles.
- Select Create role.
- Set Trusted entity type to AWS service.
- Choose the EC2 use case.
Why use an EC2 role?
The trust relationship allows an EC2 instance to assume the role. Jenkins can then use temporary role credentials without storing long-term AWS access keys.
- Select Next to reach the permissions page.
- Leave every permission policy unselected.
- Select Next to reach the role details.
- Enter JenkinsEc2Role as the role name.
- Select Create role.
The role summary now shows EC2 as the trusted service. Its permissions section contains no attached or inline policies.
Does the role show a policy?
Remove any permission policy selected during creation. Keep the EC2 trust relationship unchanged.
Help me verify my empty EC2 role.
Launch and connect to the EC2 host
A security group controls which network traffic can reach the instance. This host needs browser-based shell access plus Jenkins traffic from your current IP address.
- Set the AWS Region selector to us-east-1.
- Use the console search to open EC2.
- Select Launch instance.
- Enter jenkins-cicd-server as the instance name.
- Select Amazon Linux 2023 under Application and OS Images.
- Select t3.small under Instance type.
- Select Proceed without a key pair (not recommended) under Key pair (login).
Why skip the key pair?
This project uses EC2 Instance Connect for browser-based terminal access. The connection relies on the instance's public IPv4 address plus the restricted TCP 22 rule.
- Expand the editing controls under Network settings.
- Configure the instance to receive a public IPv4 address.
- Set the first inbound rule to TCP port 22.
- Set the TCP 22 source to com.amazonaws.us-east-1.ec2-instance-connect.
- Add another inbound security group rule.
- Set the new rule to TCP port 8080.
- Set the TCP 8080 source to My IP.
What do these rules allow?
The TCP 22 rule admits browser-based shell connections from the regional EC2 Instance Connect service. The TCP 8080 rule limits future Jenkins access to your current public IP address.
- Expand Advanced details.
- Set IAM instance profile to JenkinsEc2Role.
Before you launch
Launching this instance starts usage-based EC2, EBS, public IPv4, and S3 charges. Eligible new AWS accounts may receive up to $200 in credits.
The cleanup section shows how to stop ongoing resource use.
- Select Launch instance.
AWS prepares the Linux host while its instance state shows as pending. Wait for the state to change to running.
- Select View all instances.
- Wait until jenkins-cicd-server shows Running.
- Select jenkins-cicd-server in the instance table.
Before you connect, ask yourself what a successful browser-based connection should show.
- Select Connect.
- Select the EC2 Instance Connect tab.
- Select Connect using a Public IP.
- Select Connect.
An in-browser terminal opens for jenkins-cicd-server. That is the foundation locked in.
Foundation checkpoint
- The public jenkins-cicd-demo repository contains index.html plus the partial Jenkinsfile on main.
- The repository contains no AWS access keys or passwords.
- The JenkinsEc2Role role trusts EC2 with no attached or inline permission policies.
- The jenkins-cicd-server instance runs Amazon Linux 2023 on t3.small in us-east-1.
- The instance has a public IPv4 address plus the JenkinsEc2Role instance profile.
- The security group allows TCP 22 from com.amazonaws.us-east-1.ec2-instance-connect.
- The security group allows TCP 8080 from your IP address.
Browser terminal not opening?
Confirm the instance state is running. Check that it has a public IPv4 address.
Verify that the TCP 22 rule uses the com.amazonaws.us-east-1.ec2-instance-connect prefix list.
Help me troubleshoot EC2 Instance Connect.
Your source, AWS identity, and Linux host are ready. Next, you will install the tools that turn this instance into a Jenkins controller.
Install and Unlock Jenkins
Your source files are ready. Your Amazon EC2 host is reachable.
The host still cannot interpret your Jenkinsfile through Jenkins. It needs a Java runtime, source-control tools, an active controller, and Pipeline plugins.
In this step, get ready to:
- Install the host toolchain.
- Enable the Jenkins service.
- Unlock the controller with its required plugins.
Check the required host tools
The Amazon Linux 2023 host may already include part of the toolchain. These checks reveal which installation path applies before you change the instance.
A missing command is an expected outcome during this check. The matching tab below installs the missing packages.
- Check for the Amazon Corretto runtime by running this command:
java -version
What does this check show?
This command reports the installed Java runtime. Jenkins requires Java 21 or later.
- Check for Git by running this command:
git --version
What does this check show?
This command reports the Git version available to Jenkins. Git retrieves the repository content used by your pipeline.
- Check whether the Jenkins service exists by running this command:
sudo systemctl status jenkins
What does this check show?
This command asks the operating system for the Jenkins service state. A missing service at this point means Jenkins still needs installation.
Choose the tab that matches the results in your EC2 Instance Connect terminal.
✔️ I see the required versions
Continue if Java reports major version 21. Git should report 2.50.1.
The Jenkins service should show active (running). Your host already has the required toolchain.
ⓧ I see an older version
Use the stable Jenkins package source to bring the host packages up to the required versions.
- Add the stable Jenkins repository and signing key by running these commands:
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/rpm-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/rpm-stable/jenkins.io-2026.key
What do these commands do?
- The first command saves the stable Jenkins package repository definition on the instance.
- The second command imports the signing key used to verify Jenkins packages.
- Confirm that both commands return to the terminal prompt without an error.
Repository setup failed?
Check that the EC2 instance can reach the internet. A failed download prevents the repository file from being saved.
Ask for help with the repository command:
- Install the current runtime, source-control tool, and Jenkins package by running these commands:
sudo yum install java-21-amazon-corretto-headless
sudo dnf install git
sudo dnf install jenkins -y
What do these commands install?
- Amazon Corretto provides the Java 21 runtime required by Jenkins.
- Git gives Jenkins a way to retrieve your repository.
- The Jenkins package installs the controller service from the stable repository.
- Confirm that each package command reports a successful installation or upgrade.
Package installation failed?
Return to the first command that reported an error. A repository download problem can prevent Jenkins from being found.
Ask for help with the failed package:
- Activate the Jenkins service by running these commands:
sudo systemctl enable jenkins
sudo systemctl start jenkins
What do these commands do?
- The enable command configures Jenkins to start when the EC2 instance boots.
- The start command launches Jenkins during the current session.
- Confirm that both service commands return without an error.
Jenkins did not start?
Confirm that the Java installation completed before the Jenkins package. Jenkins cannot start without a supported Java runtime.
Ask for help with the service output:
ⓧ A required command is missing
This is the expected starting state for a fresh host. Install the required packages through the stable Jenkins repository.
- Add the stable Jenkins repository and signing key by running these commands:
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/rpm-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/rpm-stable/jenkins.io-2026.key
What do these commands do?
- The first command saves the stable Jenkins package repository definition on the instance.
- The second command imports the signing key used to verify Jenkins packages.
- Confirm that both commands return to the terminal prompt without an error.
Repository setup failed?
Check that the EC2 instance can reach the internet. A failed download prevents the repository file from being saved.
Ask for help with the repository command:
- Install the required runtime, source-control tool, and Jenkins package by running these commands:
sudo yum install java-21-amazon-corretto-headless
sudo dnf install git
sudo dnf install jenkins -y
What do these commands install?
- Amazon Corretto provides the Java 21 runtime required by Jenkins.
- Git gives Jenkins a way to retrieve your repository.
- The Jenkins package installs the controller service from the stable repository.
- Confirm that each package command reports a successful installation.
Package installation failed?
Return to the first command that reported an error. A repository download problem can prevent Jenkins from being found.
Ask for help with the failed package:
- Activate the Jenkins service by running these commands:
sudo systemctl enable jenkins
sudo systemctl start jenkins
What do these commands do?
- The enable command configures Jenkins to start when the EC2 instance boots.
- The start command launches Jenkins during the current session.
- Confirm that both service commands return without an error.
Jenkins did not start?
Confirm that the Java installation completed before the Jenkins package. Jenkins cannot start without a supported Java runtime.
Ask for help with the service output:
Verify the installed toolchain
The version checks now prove that the host can run Jenkins and retrieve source code. Before you rerun them, do you expect every command to report a ready tool?
- Verify the Java runtime by running this command:
java -version
Read the Java result
You should see output beginning with openjdk version "21.0.13" 2026-10-26 LTS. The major version confirms that Jenkins has a supported runtime.
- Verify Git by running this command:
git --version
Read the Git result
The output should include version 2.50.1. Jenkins can now retrieve source through Git.
- Verify the AWS CLI included with Amazon Linux 2023 by running this command:
aws --version
Read the AWS CLI result
The output should include version 2.36.47. A later pipeline stage uses this tool to send the tested page to Amazon S3.
- Verify the Jenkins service by running this command:
sudo systemctl status jenkins
Read the Jenkins result
Look for active (running) in the service output. This confirms that Jenkins 2.580.2 LTS is accepting requests on the instance.
- Press q if the service status opens in a scrollable terminal view.
A verification still fails?
Rerun the installation command for the tool that failed. Check the Jenkins service again after Java reports version 21.
Ask for help with the failed check:
The host toolchain is ready. Your EC2 instance can now run Jenkins as a persistent service.
Unlock Jenkins and confirm the plugins
Jenkins protects its first browser session with a temporary administrator password. Treat this value as a credential while you unlock the controller.
- Reveal the temporary unlock password by running this command:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
What does this command reveal?
The command prints the one-time password created during Jenkins installation. Use it only on your Jenkins controller.
Keep the password out of screenshots. Do not commit it to GitHub.
- Copy the password from the terminal.
- Switch to a new browser tab.
- Enter http://your-ec2-public-ip:8080 in the address bar using the public IPv4 address from earlier.
- Paste the temporary password into the password field.
- Submit the unlock form.
- Choose Install suggested plugins.
The plugin installation can take several minutes while Jenkins downloads each component. A moving progress indicator means the controller is still working.
- Enter the details for your first administrator user.
- Store the administrator password in your password manager.
- Submit the administrator form.
- Complete the remaining setup screen until the Jenkins Dashboard is visible.
Cannot reach Jenkins?
Confirm that the URL uses the instance's current public IPv4 address. Confirm that it ends with port 8080.
Check that your current IP still matches the security group rule for port 8080.
Ask for help reaching the controller:
Before you inspect the installed plugins, do you expect the suggested set to include all three integrations needed for the partial pipeline?
- Choose Manage Jenkins from the Jenkins Dashboard.
- Select Plugins.
- Select the Installed plugins tab.
- Search for Pipeline.
- Confirm that Pipeline appears in the installed list.
- Confirm that Pipeline: Declarative appears in the installed list.
- Confirm that Git appears in the installed list.
That completes the controller setup. Jenkins is unlocked with an administrator user, and the three pipeline plugins are installed.
Your Jenkins controller is running with the tools it needs. Next, you will connect it to the existing GitHub repository and run the first source-controlled pipeline.
Run the First Pipeline
Your Jenkins controller is live with its Pipeline plugins ready. The source files are waiting in GitHub.
A source-driven job connects the controller to those files. The Jenkinsfile becomes the job's version-controlled instruction set.
Before cloud deployment, this run must prove Jenkins can retrieve the page from source control. It must preserve a tested release candidate as an artifact.
In this step, get ready to:
- Confirm that the partial Jenkinsfile defines the three local stages.
- Configure a source-driven Jenkins job using the public GitHub repository.
- Complete the first successful build through the Archive stage.
Confirm the partial pipeline
The partial Jenkinsfile uses a Declarative Pipeline. Its three stages create a release candidate without deploying it.
The repository's HTTPS address gives Jenkins a public source location. You will reuse that address when configuring the job.
- Switch back to the jenkins-cicd-demo repository main page from earlier.
- Copy the public repository URL from your browser's address bar.
- Record the copied URL here: your public HTTPS GitHub repository URL.
- Select Jenkinsfile from the repository's file list.
The control section keeps builds orderly. The stages section contains the work that Jenkins performs.
- Check that agent any appears near the top of the file.
- Check that pollSCM('H/5 * * * *') appears inside triggers.
- Check that options contains disableConcurrentBuilds() plus buildDiscarder(logRotator(numToKeepStr: '5')).
- Check that stages contains only Build, Test, plus Archive.
✔️ Awesome, I've got everything!
Your file matches the partial pipeline used for this first run.
ⓧ I'd like to double check the full code
pipeline {
agent any
triggers {
pollSCM('H/5 * * * *')
}
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '5'))
}
stages {
stage('Build') {
steps {
sh 'rm -rf dist && mkdir -p dist && cp index.html dist/index.html'
}
}
stage('Test') {
steps {
sh 'grep -q "Jenkins CI/CD is live" dist/index.html'
}
}
stage('Archive') {
steps {
archiveArtifacts artifacts: 'dist/index.html', fingerprint: true
}
}
}
}
How the Partial Pipeline Works
- The agent any directive lets Jenkins use an available executor.
- The pollSCM('H/5 * * * *') trigger checks the repository for source changes.
- The disableConcurrentBuilds() option prevents overlapping runs.
- The buildDiscarder option keeps the five most recent builds.
- The Build stage copies index.html into dist/index.html.
- The Test stage uses grep to check the required heading.
- The Archive stage stores the tested file with fingerprinting enabled.
Configure the source-driven job
Source control management (SCM) tells a Jenkins job where to retrieve its pipeline definition. This keeps the job configuration connected to the version stored in GitHub.
- Return to the Jenkins web interface from earlier.
- Sign in with your administrator account if Jenkins asks.
- Choose New Item from the Jenkins Dashboard.
- Enter jenkins-cicd-demo for the job name.
- Select Pipeline as the job type.
- Choose OK.
What Did You Create?
This job is the Jenkins entry point for your repository. Its next settings connect that entry point to the version-controlled pipeline.
- Under Pipeline, set Definition to Pipeline script from SCM.
- Set SCM to Git.
- Paste your public HTTPS GitHub repository URL into Repository URL.
- Enter refs/heads/main in Branch Specifier.
- Enter Jenkinsfile in Script Path.
- Choose Save.
Jenkins returns to the jenkins-cicd-demo job page. The job is now connected to the repository's partial pipeline.
Why SCM Polling?
SCM polling lets Jenkins check the repository for commits on the schedule in pollSCM('H/5 * * * *'). This creates a source-triggered loop without requiring an internet-reachable webhook endpoint.
Run the first build
Your job now points at a runnable pipeline. This build checks the complete path from source retrieval to artifact preservation.
Before you start the build, which three stages do you expect Jenkins to complete?
- Choose Build Now from the job page.
- Select the new build number when Jenkins lists it.
- Choose Console Output.
You'll see Jenkins check out index.html plus Jenkinsfile. The run finishes successfully after the Archive stage.
What the Green Run Proves
- The Build stage creates dist/index.html from the repository page.
- The Test stage finds the required phrase Jenkins CI/CD is live.
- The Archive stage preserves dist/index.html as a fingerprinted build artifact.
- Return to the jenkins-cicd-demo job page.
- Inspect the stage view for the completed build.
You'll see Build, Test, plus Archive marked as successful.
- Select the successful build.
- Open dist/index.html from the build's artifacts.
You'll see the same static page stored in GitHub. The archived copy proves Jenkins preserved the tested release candidate.
Build Not Turning Green?
- Compare the repository URL in the job configuration with the public repository URL you recorded.
- Confirm that Branch Specifier contains refs/heads/main.
- Confirm that Script Path contains the case-sensitive filename Jenkinsfile.
- Check that index.html still contains the phrase Jenkins CI/CD is live.
Help me diagnose this Jenkins Pipeline build.
That's your first green delivery loop complete. Jenkins can now retrieve source files, enforce the test gate, and preserve the result.
Next, you'll extend this pipeline beyond archived files and into private cloud storage.
Add Deployment and Hit AccessDenied
Your Jenkins pipeline already retrieves the source page. It also builds a tested artifact that you can download.
The artifact now needs a continuous delivery path to a private Amazon S3 bucket. The first deployment reveals what the current EC2 role can do inside AWS.
In this step, get ready to:
- Create a private S3 bucket for the deployed page.
- Add the Deploy and Share stages to Jenkinsfile.
- Observe how AWS responds to the deployment attempt.
Create a private S3 bucket
A private bucket gives your pipeline a separate deployment target. Public access remains blocked while Jenkins communicates with S3 through its instance role.
S3 storage and requests use usage-based pricing. The cleanup section shows you how to remove this bucket after the project.
Why use a private bucket?
Deploying onto the Jenkins controller would combine your automation server with its release destination. A separate bucket keeps those responsibilities apart.
The pipeline later creates a temporary presigned URL for the page. You get a visible deployment without making the bucket public.
- Return to the AWS Management Console from earlier.
- Confirm the selected region is us-east-1.
- Enter S3 in the console search bar.
- Select Amazon S3 from the search results.
- Select General purpose buckets.
- Click Create bucket.
- Keep the general purpose bucket type selected.
- Enter your-unique-bucket-name in the Bucket name field.
Make the name unique
S3 bucket names must be globally unique. Add a personal suffix if your first choice is already taken.
- Keep Bucket owner enforced selected under Object Ownership.
- Keep all four controls selected under Block Public Access settings for this bucket.
- Keep the SSE-S3 default encryption setting unchanged.
- Click Create bucket.
Good progress. Your private deployment target now exists with public access blocked.
Complete the delivery pipeline
A Declarative Pipeline can expose one bucket name to several stages through its environment block. Deploy uses that value for the upload destination.
- Return to the jenkins-cicd-demo repository in GitHub.
- Select Jenkinsfile from the repository file list.
- Select the edit control above the file.
- Find the stages { line.
- Insert the environment block immediately above that line by pasting this code:
environment {
S3_BUCKET = '[[S3_BUCKET_NAME="your-unique-bucket-name"]]'
}
What does the environment block do?
- The S3_BUCKET variable gives the delivery stages one shared destination.
- Jenkins receives temporary AWS credentials from the EC2 instance profile.
- No long-term AWS access key enters GitHub or Jenkins.
- Confirm the environment block sits between the options block and stages {.
Is the environment block misplaced?
Keep the environment block inside the top-level pipeline block. Align it with the triggers, options, and stages blocks.
Help me check the environment block placement.
- Find the closing brace for stage('Archive').
- Place your cursor after the Archive stage.
- Add the Deploy stage by pasting this code:
stage('Deploy') {
steps {
sh 'aws s3 cp dist/index.html s3://$S3_BUCKET/index.html --region us-east-1'
}
}
What does the Deploy stage do?
- The stage copies dist/index.html into the private bucket as index.html.
- The region option sends the upload request to us-east-1.
- A nonzero shell exit stops the pipeline before a later stage begins.
- Confirm the Deploy stage appears immediately after the Archive stage.
Is Deploy outside the stages block?
Align stage('Deploy') with the Build, Test, and Archive stage declarations. Keep it inside the existing stages block.
Help me check the Deploy stage braces.
- Find the closing brace for stage('Deploy').
- Place your cursor after the Deploy stage.
- Add the Share stage by pasting this code:
stage('Share') {
steps {
sh 'aws s3 presign s3://$S3_BUCKET/index.html --expires-in 600 --region us-east-1'
}
}
What does the Share stage do?
- The AWS CLI creates a presigned URL for the deployed object.
- The 600 value gives the URL a 600-second lifetime.
- The stage runs only after every earlier stage succeeds.
- Confirm the Share stage appears immediately after the Deploy stage.
Is the Share stage missing?
Keep stage('Share') inside the stages block. Place it before the final braces that close stages and pipeline.
Help me check the final Share stage placement.
Your edits now define the complete delivery path. Compare the entire file before committing it.
✔️ Awesome, I've got everything!
- Confirm your bucket name appears as the value of S3_BUCKET.
- Confirm the stage order is Build, Test, Archive, Deploy, and Share.
ⓧ I'd like to double check the full code
- Compare your edited Jenkinsfile with this complete version:
pipeline {
agent any
triggers {
pollSCM('H/5 * * * *')
}
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '5'))
}
environment {
S3_BUCKET = '[[S3_BUCKET_NAME="your-unique-bucket-name"]]'
}
stages {
stage('Build') {
steps {
sh 'rm -rf dist && mkdir -p dist && cp index.html dist/index.html'
}
}
stage('Test') {
steps {
sh 'grep -q "Jenkins CI/CD is live" dist/index.html'
}
}
stage('Archive') {
steps {
archiveArtifacts artifacts: 'dist/index.html', fingerprint: true
}
}
stage('Deploy') {
steps {
sh 'aws s3 cp dist/index.html s3://$S3_BUCKET/index.html --region us-east-1'
}
}
stage('Share') {
steps {
sh 'aws s3 presign s3://$S3_BUCKET/index.html --expires-in 600 --region us-east-1'
}
}
}
}
- Click Commit changes.
- Enter Add S3 deployment and sharing in the commit message field.
- Keep main selected as the destination branch.
- Click Commit changes in the commit dialog.
That is the delivery definition complete. GitHub now holds all five pipeline stages without any stored AWS keys.
Run the deployment with current permissions
The committed pipeline can now reach its new delivery stages. This run tests the actual authority carried by JenkinsEc2Role.
- Return to the jenkins-cicd-demo job in Jenkins.
SCM polling can take up to five minutes to detect the commit. You can start the same pipeline immediately for this check.
Before you start the build, do you think the current role can place an object in the private bucket?
- Click Build Now.
- Select the new build from the build history.
- Open Console Output.
You will see Build, Test, and Archive succeed. Deploy reaches Amazon S3 and fails with AccessDenied.
Share is skipped. This controlled failure is the intended authorization obstacle.
Why did AWS deny the upload?
The AWS Identity and Access Management (IAM) role supplies temporary credentials through the EC2 instance profile. Those credentials carry only the permissions assigned to the role.
JenkinsEc2Role has no S3 permissions. AWS rejects the upload before index.html reaches the bucket.
- Return to the private S3 bucket you created earlier.
- Confirm the bucket contains no index.html object.
The empty bucket proves that authorization stopped the release before deployment.
Did the expected failed run not appear?
Confirm the committed file contains pollSCM('H/5 * * * *'). Confirm the Jenkins job still reads Jenkinsfile from refs/heads/main.
Help me diagnose the missing failed build.
The pipeline has exposed the exact permission gap. Next, you will grant only the two object actions needed to deploy and share the page.
Grant Least-Privilege Access and Deploy
Your Jenkins pipeline reached Amazon S3. The Deploy stage stopped with AccessDenied because JenkinsEc2Role had no S3 authority.
Now you'll use AWS Identity and Access Management (IAM) to grant the role two object actions. The EC2 instance role gives Jenkins temporary credentials. Jenkins can deploy without stored AWS access keys.
In this step, get ready to:
- Add an object-scoped inline policy to JenkinsEc2Role.
- Run the five-stage Jenkins pipeline successfully.
- Open the private deployment through a temporary presigned URL.
Grant access to one S3 object
Least privilege keeps the role focused on the finished page. The policy grants one upload action. It grants one read action. Both actions apply only to index.html in your bucket.
- Switch back to the AWS Management Console from earlier.
- Use the console search bar to open IAM.
- Choose Roles from the IAM navigation.
- Select JenkinsEc2Role.
- Use the role's permissions area to start creating an inline policy.
- Select the JSON editor.
- Replace the editor contents with this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeployAndShareSite",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::your-unique-bucket-name/index.html"
}
]
}
What Does This Policy Do?
- The Version value selects the 2012-10-17 IAM policy language.
- The s3:PutObject action lets the Deploy stage upload the tested page.
- The s3:GetObject action lets the Share stage create access to the deployed page.
- The Resource value restricts both actions to one index.html object.
- Replace your-unique-bucket-name with the exact name of your private bucket.
- Continue to the policy naming screen.
- Enter JenkinsS3DeployPolicy as the policy name.
- Create the inline policy.
You'll see JenkinsS3DeployPolicy listed on JenkinsEc2Role. Its resource ARN ends with your exact bucket name followed by /index.html.
Policy Not Accepted?
- Check that you replaced your-unique-bucket-name with the bucket name from your S3 console.
- Check that the resource ARN ends with /index.html.
- Check that the policy contains both s3:PutObject and s3:GetObject.
Help me troubleshoot my IAM inline policy.
✔️ Awesome, I've got everything!
Your EC2 role now has only the two object permissions required by this pipeline. The saved resource points to index.html in your private bucket.
ⓧ I'd like to double check the full code
- Compare your inline policy with the reference below.
- Keep your exact bucket name in the saved Resource value.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeployAndShareSite",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::your-unique-bucket-name/index.html"
}
]
}
What Should Match?
This reference limits the policy to one object ARN. Your saved policy uses your real bucket name in place of your-unique-bucket-name.
Run the corrected pipeline
The role now authorizes exactly the operations that failed before. A fresh pipeline run proves the policy can deploy the tested artifact.
Before you start the build, which stage do you expect to change from failure to success?
- Switch back to the Jenkins tab from earlier.
- Select the jenkins-cicd-demo job from the Dashboard.
- Choose Build Now.
- Open the newest build after it finishes.
- Choose Console Output.
You'll see Build, Test, Archive, Deploy, and Share succeed. Deploy uploads dist/index.html as index.html.
That's the authorization gap closed. The Share stage now prints a temporary URL for the private object.
Deploy Still Failing?
- Confirm that JenkinsS3DeployPolicy is attached directly to JenkinsEc2Role.
- Confirm that the policy's bucket name matches the S3_BUCKET value in your Jenkinsfile.
- Confirm that the resource ARN targets the index.html object.
Help me fix the remaining deployment failure.
Open the private deployment
A presigned URL gives time-limited access to one private object. The bucket's public access protections remain enabled.
Before you open the link, what heading do you expect after all five stages have passed?
- Locate the URL printed by Share in the successful build's Console Output.
- Open the URL in a new browser tab.
You'll see the deployed page headed Jenkins CI/CD is live. The URL remains valid for 600 seconds.
- Switch back to the AWS Management Console tab.
- Use the console search bar to open Amazon S3.
- Choose General purpose buckets.
- Select the private bucket you created earlier.
- Check the bucket's object list for index.html.
You'll see index.html in the bucket. Its contents match the tested static page from the successful build.
- Open the bucket's permissions view.
- Confirm that all four Block Public Access settings remain enabled.
The object is deployed while the bucket stays private. Access comes through the temporary URL printed by Jenkins.
Where Did the AWS Credentials Come From?
The JenkinsEc2Role instance profile supplies temporary role credentials to Jenkins on EC2. The policy controls what those credentials can do.
This deployment uses no long-term AWS access key. Nothing was added to GitHub for AWS authentication.
You closed the authorization gap without broad permissions or stored keys. Your tested page now moves from GitHub through Jenkins into private Amazon S3.
Secret mission
Prove the Test Gate Blocks a Bad Release
Temporarily remove the release phrase from the page. Use the next run to prove the quality gate protects the last good S3 deployment. Restore the passing page to complete the recovery.
Clean Up Your Resources
Clean Up Your Resources
A running Amazon EC2 instance can use AWS credits or create charges. Choose whether to keep the project available, pause its compute, or remove its resources.
Cost warning
The EC2 instance, EBS storage, public IPv4 address, and S3 usage can continue generating usage-based charges.
Stopping the instance ends its compute usage. The EBS volume and S3 object can still generate charges.
Resources you used:
- The jenkins-cicd-server EC2 instance containing your Jenkins controller, pipeline job, and archived artifacts.
- The Amazon EBS root volume attached to the EC2 instance.
- The security group allowing EC2 Instance Connect traffic and Jenkins traffic on port 8080.
- The AWS Identity and Access Management (IAM) role JenkinsEc2Role with its inline JenkinsS3DeployPolicy policy.
- The private Amazon S3 bucket containing index.html.
- The public GitHub repository named jenkins-cicd-demo.
Keep everything running
No action is needed. Choose this if you plan to keep using the Jenkins delivery system for more experiments.
- Leave jenkins-cicd-server running with its current IAM role and security group.
- Keep the private S3 bucket available for future successful deployments.
- Monitor your current usage in AWS Billing and Cost Management to track charges or credit consumption.
- Continue using the jenkins-cicd-demo GitHub repository as the source for your Jenkins job.
Pause - I'll come back to this later
Shut down the EC2 instance to stop its compute usage while preserving the project. Your Jenkins files remain on the attached EBS volume.
- Return to the Amazon EC2 console from earlier.
- Select Instances from the navigation panel.
- Select jenkins-cicd-server in the instances table.
- Choose Instance state from the controls above the table.
- Choose Stop instance from the menu.
- Confirm the stop request when prompted.
- Confirm the instance state changes to Stopped.
Jenkins becomes unavailable while the instance is stopped. The EBS volume, private S3 object, IAM role, security group, and GitHub repository remain in place.
The public IPv4 address can change when you start the instance again. Use the current address shown on the EC2 instance details page when you return.
Delete - I don't want to use this again
Remove every project resource if you no longer need the Jenkins environment. This stops the project from continuing to consume AWS storage or compute.
Deletion is permanent
Terminating jenkins-cicd-server permanently removes the Jenkins controller, job configuration, and archived artifacts stored on that host.
Deleting the GitHub repository also removes its commit history from your account.
- Terminate jenkins-cicd-server from the Instances page in Amazon EC2 by using Instance state and Terminate (delete) instance.
- Delete any retained root volume from Elastic Block Store under Volumes by using Actions and Delete volume.
- Delete index.html from your private bucket on the S3 General purpose buckets page.
- Delete the empty private bucket by choosing Delete bucket on its details page.
- Delete JenkinsEc2Role from the IAM Roles page. This also removes its inline JenkinsS3DeployPolicy policy.
- Delete the security group from the EC2 Security Groups page after the terminated instance releases it.
- Delete jenkins-cicd-demo from the repository's Settings page under Danger Zone if you do not want to keep the source history.
Once these deletions finish, no active EC2 instance, retained EBS volume, S3 bucket, IAM role, project security group, or GitHub repository should remain.
Nice Work!
Nice Work!
You did it! Your CI/CD pipeline now carries a GitHub commit through Jenkins to a tested release in private Amazon S3.
You also proved that the delivery path protects the last good release. A broken page stopped at Test without replacing the private S3 object.
You've learned how to:
- Run a Jenkins controller on Amazon EC2. Load a version-controlled Jenkinsfile from GitHub with Declarative Pipeline syntax and SCM polling.
- Build a five-stage delivery path through Build, Test, Archive, Deploy, and Share. Preserve the tested dist/index.html as a fingerprinted build artifact.
- Resolve the intended AccessDenied failure with least-privilege IAM access. Scope JenkinsS3DeployPolicy to the private index.html object. Open the tested page through a temporary presigned URL without storing long-term access keys.
- Secret Mission: Commit a broken index.html. Prove that Test blocks Deploy and Share. Restore the passing page to complete a successful recovery build.
Ready to quiz yourself?