Build a GitHub-to-ECR CI Pipeline
Use Jenkins on EC2 to build GitHub container images and publish them to ECR.
Introduction
30 Second Summary
A code change can look finished on your laptop while the packaged version your team needs is still missing. Manual handoffs make that gap easy to forget.
In this project, you will build a continuous integration pipeline that turns each GitHub commit into a numbered container image. Jenkins on Amazon EC2 publishes each image to Amazon ECR through an IAM instance role.
What You'll Build
Watch a page update travel from a GitHub push to a new numbered image in Amazon ECR through an automatic Jenkins build.
By the end of this project, you'll have:
- A browser-visible page you can run locally with Docker Desktop. It displays “GitHub to ECR CI Pipeline” at http://localhost:8081.
- A public GitHub repository with pipeline as code that Jenkins can run consistently for every source change.
- Traceable ECR images with numeric tags that match Jenkins build numbers. The build logs let you follow a commit to its published image.
- Secret Mission: Add a smoke-test gate that blocks a broken image before Jenkins publishes it to Amazon ECR.
Are there any prerequisites?
You need an AWS account that can create the required IAM resources, EC2 resources, security groups, and ECR repositories. Your account must also allow you to start AWS Systems Manager Session Manager sessions.
GitHub, Git, Visual Studio Code, and Docker Desktop must already be ready on Windows.
Before We Start
Before the hands-on work begins, lock in the purpose of this project. You are building the continuous integration half of a future Amazon EKS delivery system, where each Git commit becomes a traceable, build-numbered container image.
Prepare the AWS Build Environment
Your future Jenkins pipeline needs temporary cloud credentials before it can publish traceable container images safely.
The foundation has three parts: an IAM role, a private Amazon ECR repository, and an Amazon EC2 build host.
You will reach the host through AWS Systems Manager Session Manager to prove its cloud identity works.
In this step, get ready to:
- Create an EC2 service role with permission to use Session Manager and publish images.
- Create a private registry with immutable image tags.
- Launch an EC2 build host and verify its AWS identity.
Create the Jenkins IAM role
An EC2 service role supplies temporary AWS credentials to software running on the instance. Jenkins can use these credentials without storing long-term access keys.
- Sign in to the AWS Management Console in your browser.
- Use the console search bar to find IAM.
- Select IAM from the search results.
- Choose Roles.
- Choose Create role.
- Select AWS service as the trusted entity type.
- Select Amazon EC2 as the use case.
Why use an instance role?
The AmazonSSMManagedInstanceCore policy lets Systems Manager manage the instance. This provides the Session Manager connection used in this lab.
The AmazonEC2ContainerRegistryPowerUser policy permits image push operations in Amazon ECR. Jenkins receives that access through the EC2 role.
- Continue to Permissions policies.
- Search for AmazonSSMManagedInstanceCore.
- Select AmazonSSMManagedInstanceCore.
- Search for AmazonEC2ContainerRegistryPowerUser.
- Select AmazonEC2ContainerRegistryPowerUser.
- Continue to the role details.
- Enter jenkins-ec2-role as the role name.
- Choose Create role.
Your EC2 service role now has the two permissions needed for this lab.
- Open jenkins-ec2-role from the roles list.
- Confirm the role shows both managed policies under its permissions.
Can't create or verify the role?
- Confirm your signed-in AWS identity can create IAM roles.
- Check that the role name is exactly jenkins-ec2-role.
- Check that both managed policy names match the values above.
Ask for help with the role configuration.
Create the Amazon ECR repository
Amazon ECR stores the images produced by Jenkins. Immutable tags preserve each build number as a distinct image revision.
- Use the Region selector in the AWS console to choose US East (N. Virginia).
- Use the console search bar to find Amazon ECR.
- Select Amazon ECR from the search results.
- Choose Private repositories.
- Choose Create repository.
- Enter jenkins-cicd-demo in the Repository name field.
- Set Image tag immutability to Immutable.
- Keep Encryption configuration set to AES-256.
Why use immutable tags?
An immutable tag cannot be overwritten by a later image. Each Jenkins build number therefore keeps pointing to the image created by that build.
AES-256 protects the repository contents at rest. This setting stays enabled for the lab.
- Choose Create.
- Select jenkins-cicd-demo from the repository list.
- Copy the repository URI into this field: your ECR repository URI.
- Copy the 12-digit prefix from the repository URI into this field: your 12-digit AWS account ID.
The registry destination is ready. Its immutable tags make every future image revision traceable.
Repository settings look different?
- Confirm the selected Region is us-east-1.
- Confirm the repository name is exactly jenkins-cicd-demo.
- Confirm image tag immutability shows Immutable.
Ask for help checking the repository configuration.
Launch and verify the EC2 instance
An EC2 instance gives Jenkins a host for running builds. A standard x86_64 Amazon Linux 2023 image includes the AWS CLI and SSM Agent needed for the verification.
Launching this host starts billable EC2 and public IPv4 usage. The cleanup section gives you a clear stop or delete path.
- Use the AWS console search bar to find Amazon EC2.
- Select Amazon EC2 from the search results.
- Choose Launch instance.
- Enter jenkins-cicd-demo under Name and tags.
- Select the standard x86_64 Amazon Linux 2023 image under Application and OS Images (Amazon Machine Image).
- Confirm the selected image is not a minimal or Neuron image.
- Select t3.medium under Instance type.
- Leave Key pair (login) without a key pair.
Why this host configuration?
The t3.medium instance has 4.0 GiB of memory. Jenkins recommends 4 GB+ of RAM for a small team.
The standard Amazon Linux 2023 image includes AWS CLI v2 and SSM Agent. The minimal image has a different package set.
- Expand Network settings.
- Keep public IPv4 addressing enabled.
- Choose the option to create a new security group.
- Enter jenkins-lab-sg as the security group name.
- Remove any automatically added SSH inbound rule.
Why Session Manager instead of SSH?
Session Manager reaches the instance through its IAM role and SSM Agent. The lab therefore needs no inbound SSH rule or SSH key pair.
Restricting Jenkins access to your current public IP limits exposure while the server uses HTTP on port 8080.
- Add one inbound security group rule.
- Set the rule type to Custom TCP.
- Set the port range to 8080.
- Set the source to My IP.
- Set the root volume size to 16 GiB under Configure storage.
- Set the root volume type to gp3.
- Expand Advanced details.
- Select jenkins-ec2-role under IAM instance profile.
- Choose Launch instance.
AWS needs a few minutes to provision the host. The instance is ready for the connection check when its state shows Running.
- Return to Instances in the EC2 console.
- Select the jenkins-cicd-demo instance.
- Wait until the instance state shows Running.
- Choose Connect.
- Choose Session Manager.
- Choose Connect.
The Session Manager shell is now connected without an SSH rule. Before you run the checks, which AWS identity do you expect the instance to use?
- Verify the installed AWS CLI and the instance identity by running these commands:
aws --version
aws sts get-caller-identity
What do these checks prove?
The AWS CLI version check confirms the standard image includes AWS CLI v2.
The identity check asks AWS which credentials the shell is using. The returned ARN proves the instance received credentials from jenkins-ec2-role.
You will see AWS CLI v2 information. You will also see an ARN containing jenkins-ec2-role.
Session Manager or identity check not working?
- Confirm the instance is still in the Running state.
- Confirm its IAM instance profile is jenkins-ec2-role.
- Confirm the role still has AmazonSSMManagedInstanceCore attached.
Ask for help diagnosing the connection or identity result.
That connection proves your host can receive temporary AWS credentials without stored access keys.
Setup checkpoint
IAM role: jenkins-ec2-role is an EC2 service role with AmazonSSMManagedInstanceCore and AmazonEC2ContainerRegistryPowerUser attached.
ECR repository: jenkins-cicd-demo is private in us-east-1 with immutable tags and AES-256 encryption. Its URI is your ECR repository URI.
EC2 instance: jenkins-cicd-demo runs Amazon Linux 2023 x86_64 as a t3.medium with a 16 GiB gp3 root volume, a public IPv4 address, and the required IAM instance profile.
Security group: jenkins-lab-sg allows inbound Custom TCP traffic on port 8080 from My IP only. It has no SSH inbound rule.
Your AWS build foundation is locked in. Next, you will containerize the demo page locally before Jenkins builds it in the cloud.
Containerize the Demo Page
Your AWS build host is ready with the cloud identity that the pipeline needs. That gives your future builds somewhere secure to run.
A tiny local version gives you a visible success before cloud automation begins. By testing the Dockerfile with Docker on Windows first, you prove the image works independently of Jenkins.
In this step, get ready to:
- Create the demo page plus its container build files in Visual Studio Code.
- Build the image with Docker Desktop and verify the page in your browser.
- Create an empty public repository on GitHub.
Create the local project files
The local folder holds the page plus the instructions Docker uses to package it. Keeping these files together also prepares them for the GitHub repository you create later in this step.
- Press the Windows key to open the search bar.
- Type Visual Studio Code and press Enter.
Why test locally first?
A local build separates the container files from the cloud pipeline. When the page works here, later Jenkins errors can be traced to the server or pipeline configuration.
- Click File in the top menu.
- Select Open Folder.
- Select Desktop in the folder dialog.
- Click New folder.
- Enter jenkins-cicd-demo as the folder name.
- Select the new jenkins-cicd-demo folder.
- Click Select Folder.
The Explorer sidebar now shows an empty jenkins-cicd-demo folder on your Desktop.
- Click the new-file icon beside jenkins-cicd-demo in the Explorer sidebar.
- Enter index.html as the file name.
Your new index.html tab is ready for the page structure.
- Add the page structure to index.html by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitHub to ECR CI Pipeline</title>
<style>
</style>
</head>
<body>
<main>
<p>Cloud-native CI foundation</p>
<h1>GitHub to ECR CI Pipeline</h1>
<p>This page was packaged by Docker and published by a Jenkins CI/CD pipeline on AWS.</p>
<span class="badge">Version 1</span>
</main>
</body>
</html>
What does this code do?
- The HTML structure gives the browser a title plus the visible project content.
- The main element groups the content into one card that you style next.
- The badge class identifies the version label for its own visual treatment.
- Save index.html by pressing Ctrl+S.
- Right-click index.html in the Explorer sidebar.
- Select Reveal in File Explorer.
- Double-click index.html in File Explorer.
Your browser displays the page text in a plain layout. Seeing the heading proves that the page structure is valid before styling is added.
Does the page look empty?
- Confirm that index.html contains the entire page structure.
- Save the file before returning to your browser.
Help me diagnose why my local index.html page is blank.
The first CSS rules establish the page-wide colors plus the centered layout. This creates the dark canvas for the card.
- Switch back to the index.html tab in Visual Studio Code.
- Place your cursor between the opening and closing style tags.
- Add the page-wide styles by pasting this code:
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: #0f172a;
color: #e2e8f0;
}
What do these styles control?
- The :root rules set the dark color scheme plus the page font.
- The body rules fill the browser window with a dark background.
- The grid layout places the page content in the center of the window.
- Save index.html.
- Switch back to your browser.
- Refresh the page.
You should see the text centered on a dark blue background.
Still seeing a white background?
- Check that the new rules sit between the two style tags.
- Confirm that every opening brace has a matching closing brace.
Help me find why the styles in my index.html file are not appearing.
The remaining styles turn the centered content into a bordered card. They also give the heading plus version badge their own colors.
- Place your cursor after the closing brace of the body rules.
- Add the card styles by pasting this code:
main {
width: min(42rem, calc(100% - 3rem));
padding: 3rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #111827;
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.35);
}
h1 {
margin-top: 0;
color: #38bdf8;
}
.badge {
display: inline-block;
margin-top: 1rem;
padding: 0.45rem 0.8rem;
border-radius: 999px;
background: #14532d;
color: #bbf7d0;
}
What finishes the design?
- The main rules create the bordered card plus its spacing.
- The h1 rules make the pipeline title blue.
- The .badge rules turn Version 1 into a rounded green label.
- Save index.html.
- Switch back to your browser.
- Refresh the page.
You should see a dark card with a blue pipeline title plus a green Version 1 badge.
Does the card still look plain?
- Confirm that the main selector has no leading period.
- Confirm that .badge includes the leading period.
Help me compare my card styles with the expected main plus .badge selectors.
The Dockerfile describes how to assemble the page into an image. It starts with a web server image that already knows how to serve static files.
- Click the new-file icon beside jenkins-cicd-demo in the Explorer sidebar.
- Enter Dockerfile as the file name.
- Define the image by pasting this code:
FROM nginx:stable-alpine3.24
COPY index.html /usr/share/nginx/html/index.html
What does the Dockerfile do?
- The FROM instruction uses the NGINX image tagged nginx:stable-alpine3.24 as the web server foundation.
- The COPY instruction places your page at the location NGINX serves by default.
- Save Dockerfile.
- Confirm that Dockerfile appears beside index.html in the Explorer sidebar.
Does the file show a text extension?
The file name must be exactly Dockerfile. Rename it if Visual Studio Code shows an extra extension.
Help me correct the Dockerfile name in my Visual Studio Code project.
The .dockerignore file keeps repository metadata plus the future pipeline definition outside the image build context. This keeps unrelated files away from the web server image.
- Click the new-file icon beside jenkins-cicd-demo in the Explorer sidebar.
- Enter .dockerignore as the file name.
- Set the build context exclusions by pasting this code:
.git
Jenkinsfile
What does this file exclude?
- The .git entry excludes local version-control metadata.
- The Jenkinsfile entry excludes the future pipeline definition from the web image.
- Save .dockerignore.
- Confirm that index.html, Dockerfile, and .dockerignore appear in the Explorer sidebar.
Is the ignore file missing?
Check that the file starts with a period. Its exact name is .dockerignore.
Help me verify the three project files in my Visual Studio Code Explorer.
✔️ Awesome, I've got everything!
Your three local files now contain the complete page plus its container build instructions.
ⓧ I'd like to double check the full code
Compare your complete index.html with this file.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitHub to ECR CI Pipeline</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: #0f172a;
color: #e2e8f0;
}
main {
width: min(42rem, calc(100% - 3rem));
padding: 3rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #111827;
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.35);
}
h1 {
margin-top: 0;
color: #38bdf8;
}
.badge {
display: inline-block;
margin-top: 1rem;
padding: 0.45rem 0.8rem;
border-radius: 999px;
background: #14532d;
color: #bbf7d0;
}
</style>
</head>
<body>
<main>
<p>Cloud-native CI foundation</p>
<h1>GitHub to ECR CI Pipeline</h1>
<p>This page was packaged by Docker and published by a Jenkins CI/CD pipeline on AWS.</p>
<span class="badge">Version 1</span>
</main>
</body>
</html>
Compare your complete Dockerfile with this file.
FROM nginx:stable-alpine3.24
COPY index.html /usr/share/nginx/html/index.html
Compare your complete .dockerignore with this file.
.git
Jenkinsfile
Build and test the image
A container image packages the web server plus your page into one repeatable artifact. The same image-building approach later runs from the Jenkins server.
Docker Desktop can take a minute to start its engine. A short wait here is normal.
- Press the Windows key to open the search bar.
- Type Docker Desktop and press Enter.
- Wait until Docker Desktop reports that its engine is running.
- Switch back to Visual Studio Code.
- Open the integrated terminal by pressing Ctrl+`.
- Build the local image by running this command:
docker build -t jenkins-cicd-demo:local .
What does this command do?
- The docker build command reads the Dockerfile in the current folder.
- The -t option names the image jenkins-cicd-demo:local.
- The final dot sends the current jenkins-cicd-demo folder as the build context.
The terminal shows the image layers being assembled. A successful build finishes without an error.
Did the build fail?
- Confirm that the terminal path ends in jenkins-cicd-demo.
- Confirm that Dockerfile has no extra file extension.
- Confirm that Docker Desktop is still running.
Help me troubleshoot why my local Docker image does not build.
Before you start the container, what title do you expect the packaged page to display in your browser?
- Start the local test container by running this command:
docker run --name jenkins-cicd-demo -d -p 8081:80 jenkins-cicd-demo:local
What does this command do?
- The --name option gives the container the name jenkins-cicd-demo.
- The -d option keeps the container running in the background.
- The -p 8081:80 option connects port 8081 on Windows to port 80 inside the container.
The terminal prints the new container identifier. The web server is now ready to receive a browser request.
- Open a new tab in your browser.
- Enter http://localhost:8081 in the address bar.
You should see the dark demo page with the title GitHub to ECR CI Pipeline plus the green Version 1 badge.
That browser result proves the image can serve the expected application. You can now remove the temporary container while keeping the built image.
- Return to the Visual Studio Code terminal.
- Remove the temporary container by running this command:
docker rm -f jenkins-cicd-demo
What does this command do?
The command stops the named test container plus removes it from Docker. The local jenkins-cicd-demo:local image remains available.
The terminal prints jenkins-cicd-demo when the container is removed.
- Confirm that the container is gone by running this command:
docker ps
What does this check prove?
The command lists running containers. The jenkins-cicd-demo container should be absent from the table.
Port 8081 is now free. The local image remains ready for future builds.
Is the container still listed?
Run the removal command again with the exact container name jenkins-cicd-demo.
Help me remove the local test container without deleting its image.
Create the empty GitHub repository
The public repository becomes the remote source that Jenkins checks out later. Creating it empty avoids introducing a separate starting commit before the pipeline definition is ready.
- Open the new repository page in your browser.
- Select your personal account from the Owner dropdown.
- Enter jenkins-cicd-demo in the Repository name field.
- Select Public as the repository visibility.
- Leave Add README unselected.
- Leave the .gitignore option unchanged.
- Leave the license option unchanged.
- Click Create repository.
You should see setup instructions for the new public jenkins-cicd-demo repository. No files or commits should appear in it.
Why leave the repository empty?
Your local folder already contains the files that belong in the first commit. The next pipeline step adds the Jenkinsfile before publishing everything together.
Nice work. Your page now runs from a local container image with port 8081 released after testing. Next up, you turn the EC2 instance into the Jenkins build server that automates this process.
Set Up the Jenkins Build Server
Your demo page already runs through Docker Desktop on Windows. The Amazon EC2 host still lacks the automation service and container runtime needed to build it in the cloud.
Jenkins 2.568.3 LTS turns that host into your build server. Java 21 supplies its runtime.
Git retrieves your source from GitHub. Docker Engine builds the images.
AWS CLI v2 uses the instance role to reach Amazon ECR. This gives Jenkins temporary AWS credentials without storing access keys.
In this step, get ready to:
- Install the software Jenkins needs.
- Start the Jenkins and Docker services.
- Complete the Jenkins browser setup.
Install the server runtime
Amazon Linux 2023 uses DNF as its package manager. YUM points to DNF on this operating system.
- Return to the EC2 console from earlier.
- Select the running jenkins-cicd-demo instance.
- Choose Connect.
- Choose Session Manager.
- Choose Connect to return to the instance shell.
Package downloads can take several minutes, so a quiet terminal during this part is normal.
- Prepare the package base by running these commands:
sudo yum update -y
sudo dnf install -y wget
sudo dnf install -y git
What do these commands prepare?
- The package update applies current fixes from the configured repositories.
- The wget tool downloads the Jenkins repository definition.
- Git gives Jenkins the source control client it needs for later checkouts.
The shell returns to the prompt after the updates and both tools finish installing.
Package update not completing?
Confirm the EC2 instance is still running. Check that the Session Manager shell has not disconnected.
If a repository download keeps failing, help me diagnose the package installation.
Jenkins comes from its stable RPM repository. The package needs Java 21 before the service can run.
- Accept each package-manager confirmation when prompted.
- Add the stable repository and install the Jenkins runtime by running:
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/rpm-stable/jenkins.repo
sudo dnf install fontconfig java-21-openjdk
sudo dnf install jenkins
sudo systemctl daemon-reload
How does Jenkins get installed?
- The repository file directs the package manager to the Jenkins LTS packages.
- Java 21 provides the supported runtime for Jenkins 2.568.3 LTS.
- The final command refreshes systemd so it recognizes the new Jenkins service.
The installation finishes when the shell returns to the prompt without reporting a failed package.
Jenkins package not installing?
Check that the repository download completed before the Java and Jenkins commands ran. Repeat this chunk if the Session Manager connection interrupted it.
For help tracing the failed package, help me troubleshoot the Jenkins installation.
Docker Engine provides the build runtime that Jenkins uses later. The Jenkins operating system user needs access to the Docker group before the service starts.
- Install the container runtime and start both services by running:
sudo yum install -y docker
sudo service docker start
sudo systemctl enable docker
sudo usermod -a -G docker jenkins
sudo systemctl enable jenkins
sudo systemctl start jenkins
What does this service setup do?
- Docker starts now and becomes enabled for future instance boots.
- The Jenkins user joins the Docker group so pipeline stages can build container images.
- Jenkins starts now and becomes enabled for future instance boots.
The services are now installed and started. Next, you will prove that the host can build images and identify itself to AWS.
Service command failing?
Confirm the Docker package installed before its service commands ran. Confirm the Jenkins package installed before its service commands ran.
If one service still fails to start, help me inspect the service setup.
Verify the build host
A running service does not prove the whole build host is ready. These checks cover Jenkins, Docker, Git, the AWS CLI, and the instance role.
Before you run the first check, do you expect Jenkins to report an active service?
- Check the Jenkins service by running this command:
sudo systemctl status jenkins
What does this status check prove?
The status view confirms whether the Jenkins process started successfully. It also shows whether the service remains available for browser requests.
You should see that Jenkins is active. This proves the automation service is running on the EC2 host.
- Press Q to leave the status view.
- Check the remaining tools and reveal the one-time unlock value by running:
docker ps
git --version
aws --version
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
What do these checks prove?
- The Docker check prints the container table when the daemon is available.
- The Git check confirms the source control client is installed.
- The AWS check confirms AWS CLI v2 is available.
- The final command reveals the one-time value needed to unlock Jenkins.
The container table can be empty because no server-side containers are running yet. The important result is that each command completes successfully.
The unlock value grants access to the initial setup screen. Keep it private while you transfer it to the browser.
- Copy the value printed from the initialAdminPassword file.
- Verify the EC2 instance role identity by running:
aws sts get-caller-identity
Why check the caller identity?
The identity response proves that AWS CLI requests use the EC2 instance profile. Its ARN should identify jenkins-ec2-role.
That role supplies temporary credentials to Jenkins without long-term AWS access keys.
That is the server-side foundation proven. Jenkins can run, Docker can answer, and AWS recognizes the instance role.
One of the host checks failing?
- Repeat the Jenkins start command from the previous substep if the service is inactive.
- Repeat the Docker group and service commands if the daemon check reports an access problem.
- Confirm that jenkins-ec2-role remains attached to the instance if AWS cannot identify the caller.
For help connecting a failed result to its cause, help me diagnose the build host checks.
Unlock and configure Jenkins
Jenkins listens on port 8080 by default. The existing security group limits that port to your public IP.
- Return to the EC2 console from earlier.
- Select the jenkins-cicd-demo instance.
- Record its public IPv4 address here: your EC2 public IPv4 address.
- Open a new browser tab.
- Enter http:// followed by your EC2 public IPv4 address followed by :8080 in the address bar.
You should see the Jenkins unlock screen. This confirms that the service is reachable through the security group rule.
The copied unlock value is a temporary credential for this setup screen. Keep it out of screenshots and shared messages.
- Paste the copied value into Administrator password.
- Choose Continue.
- Choose Install suggested plugins.
Plugin installation can take several minutes, so progress may pause while Jenkins downloads dependencies.
- Complete the first administrator user form with credentials you choose.
- Choose Save and Finish.
- Choose Start using Jenkins.
You should reach the authenticated Jenkins Dashboard. Your administrator account now controls the build server.
Before the final check, do you expect the suggested plugin set to contain both Pipeline and Git?
- Choose Manage Jenkins from the Dashboard.
- Choose Plugins.
- Select Installed.
- Confirm that Pipeline is listed.
- Confirm that Git is listed.
You should see both plugins in the installed list. This proves Jenkins can load Pipeline definitions and retrieve Git repositories.
- Return to the Jenkins Dashboard.
- Refresh the browser page.
The authenticated Dashboard should load again at your EC2 address on port 8080. Your Jenkins build server is ready for a Pipeline job.
Jenkins page not loading?
- Confirm the EC2 instance still has the same public IPv4 address you entered.
- Confirm that jenkins-lab-sg still allows port 8080 from your current public IP.
- Return to the Session Manager shell to confirm the Jenkins service remains active.
If the browser still cannot connect, help me trace the Jenkins connection.
Your EC2 host is now an authenticated Jenkins build server with its container and AWS tools ready. Next, you will connect it to your GitHub repository and publish the first build-numbered image.
Run the First GitHub-to-ECR Pipeline
Your Amazon EC2 host now runs Jenkins with the tools it needs. The local page is also ready to leave your computer.
You will store the build instructions beside the page in GitHub. A deliberate Jenkins run will publish the first numbered image to Amazon ECR.
Starting the build yourself isolates the pipeline logic. You can inspect every stage before source changes enter the picture.
In this step, get ready to:
- Define the build workflow in a Jenkinsfile.
- Publish the project files to the public GitHub repository.
- Run a Jenkins job that publishes the first ECR image.
Create the Jenkins pipeline definition
A Jenkinsfile stores your pipeline as code. Jenkins reads this file to learn which tools to check and which image to publish.
- Switch back to the jenkins-cicd-demo folder in Visual Studio Code from earlier.
- Select the new-file icon in the Explorer sidebar.
- Enter Jenkinsfile as the filename.
You will see an empty Jenkinsfile tab. The file also appears beside index.html in the Explorer sidebar.
- Add the pipeline controls and tool checks by pasting this code into Jenkinsfile:
pipeline {
agent any
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '5'))
}
environment {
AWS_DEFAULT_REGION = 'us-east-1'
AWS_ACCOUNT_ID = 'your-aws-account-id'
ECR_REPOSITORY = 'jenkins-cicd-demo'
}
stages {
stage('Verify Tools') {
steps {
sh 'git --version'
sh 'docker ps'
sh 'aws --version'
}
}
}
}
What Does This Pipeline Define?
- The disableConcurrentBuilds() option prevents two builds from overlapping.
- The buildDiscarder option keeps five recent build records.
- AWS_DEFAULT_REGION points the pipeline to us-east-1.
- AWS_ACCOUNT_ID identifies your ECR registry.
- ECR_REPOSITORY identifies the jenkins-cicd-demo repository.
- The first shell step confirms Git is available.
- The second shell step confirms Docker can reach its daemon.
- The final shell step confirms the AWS CLI is available.
- Replace your-aws-account-id with the 12-digit account ID copied from the ECR repository URI earlier.
The AWS_ACCOUNT_ID line now contains your own 12-digit account ID.
- Save Jenkinsfile.
- Confirm the Explorer sidebar lists Jenkinsfile beside the existing project files.
Does the File Show a Syntax Warning?
Check that each opening brace has a matching closing brace. Keep the stage inside the stages block.
Check that the account ID remains inside single quotes.
Help me compare my initial Jenkinsfile structure with the required pipeline.
The next stage turns the checked-out project into a build-numbered image. It uses the EC2 IAM instance role when Docker authenticates to ECR.
- In Jenkinsfile, locate the completed stage('Verify Tools') block.
- Insert the following stage immediately below it inside the stages block:
stage('Build and Push Image') {
steps {
sh '''
set -eu
REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
IMAGE_URI="${REGISTRY}/${ECR_REPOSITORY}:${BUILD_NUMBER}"
aws ecr get-login-password --region "${AWS_DEFAULT_REGION}" \
| docker login --username AWS --password-stdin "${REGISTRY}"
docker build -t "${IMAGE_URI}" .
docker push "${IMAGE_URI}"
echo "Published ${IMAGE_URI}"
'''
}
}
What Does the Publishing Stage Do?
- REGISTRY assembles the private ECR registry address.
- IMAGE_URI combines the registry address with the repository name and Jenkins build number.
- aws ecr get-login-password provides temporary registry authentication through the instance role.
- docker build packages the project with the numbered ECR tag.
- docker push publishes that tagged image to the private repository.
- The final message prints the exact image URI that Jenkins published.
- Save Jenkinsfile.
- Confirm the file contains the Verify Tools stage.
- Confirm the file contains the Build and Push Image stage.
Is the New Stage Outside the Pipeline?
The publishing stage belongs inside the existing stages block. Place it after the closing brace for Verify Tools.
Help me place the image publishing stage correctly in my Jenkinsfile.
✔️ Awesome, I've got everything!
Your saved Jenkinsfile now defines the tool checks and the image publishing stage.
ⓧ I'd like to double check the full code
The complete Jenkinsfile at this point is shown below.
pipeline {
agent any
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '5'))
}
environment {
AWS_DEFAULT_REGION = 'us-east-1'
AWS_ACCOUNT_ID = 'your-aws-account-id'
ECR_REPOSITORY = 'jenkins-cicd-demo'
}
stages {
stage('Verify Tools') {
steps {
sh 'git --version'
sh 'docker ps'
sh 'aws --version'
}
}
stage('Build and Push Image') {
steps {
sh '''
set -eu
REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
IMAGE_URI="${REGISTRY}/${ECR_REPOSITORY}:${BUILD_NUMBER}"
aws ecr get-login-password --region "${AWS_DEFAULT_REGION}" \
| docker login --username AWS --password-stdin "${REGISTRY}"
docker build -t "${IMAGE_URI}" .
docker push "${IMAGE_URI}"
echo "Published ${IMAGE_URI}"
'''
}
}
}
}
How Does This Reference Differ?
The AWS_ACCOUNT_ID line uses the project placeholder in this reference. Your saved file contains your copied 12-digit account ID.
Publish the project to GitHub
The pipeline can only load files that exist in the configured GitHub repository. Your first commit packages the page and its CI definition into one traceable source revision.
- Press Cmd+` (macOS) or Ctrl+` (Windows) to open the integrated terminal in jenkins-cicd-demo.
- Replace your-github-username in the remote command below with your GitHub username.
- Complete the GitHub authentication prompt if Git asks you to sign in.
- Initialize the repository and publish its first commit by running these commands:
git init -b main
git add .
git commit -m "First commit"
git remote add origin https://github.com/your-github-username/jenkins-cicd-demo.git
git remote -v
git push -u origin main
What Do These Commands Do?
- git init -b main creates the local repository on the main branch.
- git add . stages the project files.
- git commit -m "First commit" records the first source revision.
- git remote add origin connects the local repository to GitHub.
- git remote -v displays the saved remote address.
- git push -u origin main publishes the branch to the empty public repository.
- Return to the public jenkins-cicd-demo repository page on GitHub.
- Refresh the repository page.
You should see the main branch with index.html, Dockerfile, .dockerignore, and Jenkinsfile.
Did the First Push Fail?
Confirm the repository URL contains your own GitHub username. Confirm the public repository is still empty before retrying the push.
If Git requests authentication, complete that prompt with the account that owns the repository.
Help me fix my first GitHub push.
Configure and run the Jenkins job
Pipeline script from SCM tells Jenkins to load Jenkinsfile from GitHub. Each job run therefore uses the build instructions stored with the application.
Why Load the Pipeline from Source Control?
An inline pipeline script lives only in Jenkins. Pipeline script from SCM keeps the workflow in the same Git history as the application.
Every commit can show which build instructions produced its image. That link makes the artifact easier to trace.
- Switch back to the authenticated Jenkins Dashboard from earlier.
- Select New Item.
- Enter jenkins-cicd-demo as the item name.
- Select Pipeline.
- Select OK.
The job configuration page opens. Its Pipeline section controls where Jenkins finds the workflow.
- Scroll to the Pipeline section.
- Set Definition to Pipeline script from SCM.
- Set SCM to Git.
- Enter https://github.com/your-github-username/jenkins-cicd-demo.git in Repository URL.
- Enter */main in Branch Specifier.
- Enter Jenkinsfile in Script Path.
- Select Save.
The job page now shows the saved jenkins-cicd-demo pipeline configuration.
Before you start the build, do you expect the ECR image tag to match the Jenkins build number?
- Select Build Now.
- Select the completed build from the run list.
- Select Console Output.
You will see the tool verification commands complete. Later lines show ECR authentication and the image push.
The final line begins with Published and ends with /jenkins-cicd-demo:1.
- Return to the Amazon ECR console from earlier.
- Select Repositories.
- Select Private.
- Select jenkins-cicd-demo.
- Confirm the image list contains tag 1.
That is the full commit-to-image path working. Jenkins has turned your GitHub source into an immutable build-numbered image in ECR.
That result appeared only after you selected Build Now. The pipeline has no automatic feedback loop yet.
Did the Pipeline Fail?
If the tool verification stage fails, return to the Session Manager shell from earlier. Confirm Docker and Jenkins are still running.
If registry authentication fails, compare the account ID in Jenkinsfile with the ECR repository URI. Confirm jenkins-ec2-role remains attached to the instance.
Help me diagnose my failed Jenkins pipeline.
Your manual pipeline now publishes traceable images from source control. Next, you will close the missing feedback loop so a source change can start a later build.
Automate Source Changes
Your manual Jenkins build already turned the GitHub source into image tag 1 in Amazon ECR. That proves your pipeline can produce a traceable container image.
The delivery loop still waits for you to click Build Now. Source polling lets Jenkins detect a later commit without that manual step.
In this step, get ready to:
- Declare source polling in Jenkinsfile.
- Publish a Version 2 source change to GitHub.
- Match an automatic Jenkins build to its new ECR image tag.
Declare source polling
The pollSCM('H/2 * * * *') trigger tells Jenkins to check the configured source on a two-minute hashed cadence. Jenkins starts a build when it finds a new commit.
- In the existing jenkins-cicd-demo project in Visual Studio Code, select Jenkinsfile from the Explorer sidebar.
- Find the boundary between the closing brace of options and the opening environment block:
}
environment {
Why this location?
A Declarative Pipeline keeps triggers at the same structural level as options and environment. This placement makes polling part of the pipeline definition.
- Insert the triggers block between those two sections so the boundary looks like this:
}
triggers {
pollSCM('H/2 * * * *')
}
environment {
What does the trigger do?
The triggers block activates scheduled source checks for this Pipeline. The hashed schedule spreads checks across the two-minute cadence.
- Save Jenkinsfile.
- Confirm the new block sits above environment.
✔️ Awesome, I've got everything!
Your Jenkinsfile now declares source polling. Keep the copied 12-digit account ID in your existing AWS_ACCOUNT_ID value.
ⓧ I'd like to double check the full code
Compare your Jenkinsfile with this complete reference. Keep your copied 12-digit account ID in place of your-aws-account-id.
pipeline {
agent any
options {
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '5'))
}
triggers {
pollSCM('H/2 * * * *')
}
environment {
AWS_DEFAULT_REGION = 'us-east-1'
AWS_ACCOUNT_ID = 'your-aws-account-id'
ECR_REPOSITORY = 'jenkins-cicd-demo'
}
stages {
stage('Verify Tools') {
steps {
sh 'git --version'
sh 'docker ps'
sh 'aws --version'
}
}
stage('Build and Push Image') {
steps {
sh '''
set -eu
REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
IMAGE_URI="${REGISTRY}/${ECR_REPOSITORY}:${BUILD_NUMBER}"
aws ecr get-login-password --region "${AWS_DEFAULT_REGION}" \
| docker login --username AWS --password-stdin "${REGISTRY}"
docker build -t "${IMAGE_URI}" .
docker push "${IMAGE_URI}"
echo "Published ${IMAGE_URI}"
'''
}
}
}
}
What changed in the full file?
The triggers block is the only new pipeline section. The existing tool checks plus the image publication stage remain unchanged.
- Commit the saved Jenkinsfile change with the existing Git workflow from earlier. Use Enable source polling as the commit message.
- Push the commit to main with the same Git workflow.
- Predict whether this manual run creates another numeric image tag or reuses tag 1.
- Return to the Jenkins Dashboard from earlier.
- Select the jenkins-cicd-demo Pipeline job.
- Choose Build Now once to load the polling configuration.
- Open the new build from the job page.
- Choose Console Output.
You should see a successful Published line whose image URI ends with the new build number. Jenkins has now loaded the polling declaration from GitHub.
Did the manual build fail?
- Confirm the polling commit appears on branch main in the GitHub repository.
- Compare the placement of triggers with the full-file reference above.
- Keep your copied 12-digit account ID in the AWS_ACCOUNT_ID value.
Help me diagnose why my Jenkins build failed after I added the polling trigger.
Publish the Version 2 change
The trigger is registered. A small visible edit now gives Jenkins a new source revision to detect.
- Switch back to index.html in the existing Visual Studio Code window.
- Find the current badge line:
<span class="badge">Version 1</span>
Why change the badge?
The badge creates an observable source revision inside the container image. Its text lets you distinguish this release from the earlier image.
- Replace Version 1 with Version 2 so the line looks like this:
<span class="badge">Version 2</span>
What does this edit prove?
The changed badge gives the release commit new application content. Jenkins packages that exact revision into the next build-numbered image.
- Save index.html.
- Confirm the badge line now contains Version 2.
✔️ Awesome, I've got everything!
Your saved index.html now identifies this source revision as Version 2.
ⓧ I'd like to double check the full code
Compare your index.html with this complete Version 2 reference.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitHub to ECR CI Pipeline</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
background: #0f172a;
color: #e2e8f0;
}
main {
width: min(42rem, calc(100% - 3rem));
padding: 3rem;
border: 1px solid #334155;
border-radius: 1rem;
background: #111827;
box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.35);
}
h1 {
margin-top: 0;
color: #38bdf8;
}
.badge {
display: inline-block;
margin-top: 1rem;
padding: 0.45rem 0.8rem;
border-radius: 999px;
background: #14532d;
color: #bbf7d0;
}
</style>
</head>
<body>
<main>
<p>Cloud-native CI foundation</p>
<h1>GitHub to ECR CI Pipeline</h1>
<p>This page was packaged by Docker and published by a Jenkins CI/CD pipeline on AWS.</p>
<span class="badge">Version 2</span>
</main>
</body>
</html>
What should match?
The styling plus the page copy remain unchanged. The badge is the only difference from the earlier source revision.
- Predict whether Jenkins detects this push without another click on Build Now.
- Publish the Version 2 release commit by running these commands in the existing project terminal:
git add .
git commit -m "Release version 2"
git push origin main
What do these commands change?
- The first command stages the saved project changes.
- The second command records the Version 2 release in local history.
- The third command publishes that commit to branch main on GitHub.
This check includes a short quiet wait because Jenkins uses a two-minute hashed polling cadence. Give it slightly more than two minutes before treating the silence as a problem.
- Return to the jenkins-cicd-demo job in Jenkins.
- Refresh the job page after the polling interval.
You should see a later build start without another click on Build Now. That automatic start proves Jenkins detected the Version 2 commit.
Still waiting for the automatic build?
- Wait slightly longer than the two-minute polling cadence.
- Confirm the Version 2 commit appears on branch main in GitHub.
- Confirm the earlier manual run completed after the polling trigger was pushed.
Help me find why Jenkins has not started a build after my Version 2 push.
Match the build number to the ECR tag
The automatic build uses its BUILD_NUMBER as the immutable image tag. Matching the console output to ECR proves the source revision produced a traceable artifact.
- Open the automatically started build from the Jenkins job page.
- Choose Console Output.
- Find the final Published line.
- Record the numeric tag at the end of the published URI: your automatic Jenkins build number.
That numeric tag is the automatic run's BUILD_NUMBER. The same value should identify its image in the registry.
- Predict which ECR image tag should match the automatic Jenkins build.
- Return to the Amazon ECR console from earlier.
- Select Private repositories.
- Select jenkins-cicd-demo.
- Refresh the image list.
You should see image tag your automatic Jenkins build number alongside the earlier numeric tags. The Published URI in Console Output ends with that same number.
You have closed the manual feedback gap. A Git push now becomes a traceable container image without another manual build click.
Secret mission
Block a Broken Image Before Publishing
Your pipeline can build an image even when the page inside it contains the wrong content. Add a smoke-test gate that stops the image before publication when the expected page text is missing.
Clean Up Your Resources
Clean Up Your Resources
This lab uses billable AWS resources. Choose the cleanup path that matches when you plan to return.
Cost warning
A running Amazon EC2 t3.medium instance costs $0.0418 per hour in US East (N. Virginia). An in-use public IPv4 address costs $0.005 per hour.
Amazon EBS storage is billed separately. Amazon ECR storage is also billed separately.
- Stop or terminate the instance as soon as you finish.
Resources you used:
- The jenkins-cicd-demo Amazon Linux 2023 instance hosting Jenkins. This resource includes a 16 GiB gp3 root volume plus one public IPv4 address.
- The private jenkins-cicd-demo Amazon ECR repository containing successful build-numbered images.
- The jenkins-lab-sg security group that allows access to Jenkins on port 8080 from your IP address.
- The jenkins-ec2-role IAM role used by the EC2 instance.
Keep everything running
No action is needed. This option keeps Jenkins available for more builds.
- Monitor AWS billing while the jenkins-cicd-demo instance remains running.
- Keep the jenkins-cicd-demo Amazon ECR repository to preserve its build-numbered images.
- Keep the public jenkins-cicd-demo GitHub repository as the source for future builds.
- Leave the jenkins-ec2-role role attached to the running instance.
- Leave the jenkins-lab-sg security group in place for Jenkins access.
Pause - I'll come back to this later
Stopping the EC2 instance pauses compute charges. Its configuration remains available for later.
- Select jenkins-cicd-demo in the Amazon EC2 instance list.
- Stop the selected instance.
- Confirm the EC2 console shows that the instance is no longer running.
Your Jenkins Dashboard is unavailable while the instance is stopped. Retained storage can still generate charges.
Delete - I don't want to use this again
Deleting the AWS lab is permanent. Your public GitHub repository remains available because the actions below do not remove it.
Delete the ECR Repository
- Choose Repositories in Amazon ECR.
- Choose Private.
- Select jenkins-cicd-demo.
- Choose Delete.
- Confirm the deletion with Delete.
- Return to the private repository list to confirm jenkins-cicd-demo is no longer listed.
That clears out the registry. Its stored images can no longer generate ECR storage charges.
Terminate the EC2 Instance
- Choose Instances in Amazon EC2.
- Select jenkins-cicd-demo.
- Open the root volume details for the selected instance.
- Confirm Delete on termination shows Yes.
- Choose Instance state.
- Choose Terminate (delete) instance.
- Confirm with Terminate (delete).
Termination can take a few minutes, so a short delay is expected.
- Refresh the instance list until jenkins-cicd-demo no longer shows as running.
The main hourly charge is now gone. Your Jenkins Dashboard is also removed with the instance.
Delete the Security Group
The security group no longer protects a running host. It can now be removed.
- Open the security groups page in Amazon EC2.
- Select jenkins-lab-sg.
- Delete the selected security group.
- Confirm the deletion.
- Return to the security groups list to confirm jenkins-lab-sg is no longer listed.
Delete the IAM Role
The instance role has no host left to serve. Removing it closes the final AWS permission resource from this lab.
- Choose Roles in IAM.
- Select jenkins-ec2-role.
- Choose Delete.
- Enter jenkins-ec2-role as the role name.
- Confirm with Delete.
- Return to the roles list to confirm jenkins-ec2-role is no longer listed.
Your paid AWS lab resources are now removed. The public GitHub repository remains available for future projects.
Nice Work!
Nice Work!
You did it! You built a working CI/CD pipeline that turns a GitHub source change into a tested container image in Amazon ECR through Jenkins on Amazon EC2.
You've learned how to:
- Package a browser-visible page as a Docker image from a version-pinned base image.
- Run a Jenkins Pipeline on Amazon EC2 with temporary AWS credentials supplied by an IAM instance role.
- Turn GitHub commits into immutable build-numbered images through automatic source polling. Each successful build leaves a traceable image tag in Amazon ECR.
- Secret Mission: Added a smoke-test quality gate that blocks a broken image before publication. The failed build leaves no matching tag in Amazon ECR.
Ready to quiz yourself?