Auto-Deploy a Website with GitHub Actions
Build a CI/CD pipeline that auto-deploys your site to AWS S3 on every push.
Introduction
30 Second Summary
Every time a developer makes a change to a website, someone has to upload the new files to the server. Doing this manually every single time is slow, error-prone, and feels like busywork.
In this project, you will build a live website that updates itself automatically every time you push code to GitHub. You'll set up a CI/CD pipeline using GitHub Actions and AWS S3 so you never have to manually upload files again.
What You'll Build
You'll push a code change, watch a pipeline kick off in real time, and refresh your browser to see your live site update within seconds.
By the end of this project, you'll have:
- A live website on a public URL that anyone can visit. You'll share the link and watch people load your site from anywhere in the world.
- An automated deployment pipeline that syncs your site to AWS every time you push code. No manual uploads, no logging into the AWS console.
- Secure credentials management with GitHub Secrets so your pipeline can access AWS without exposing passwords in your code.
- Secret Mission: Add a staging environment with a separate branch and S3 bucket for testing changes before they go live.
Are there any prerequisites?
You'll need an AWS account, a GitHub account, and VS Code installed on your computer. No prior experience with CI/CD or AWS is required.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. Taking a moment to set your intention makes the hands-on work ahead more purposeful.
Set Up Git and Create Your Project
Your CI/CD pipeline needs a starting point. Git tracks every change you make to your code, and GitHub stores that code in the cloud where your pipeline can access it.
In this step, you will install Git, create a GitHub repository, and push your first website files. This gives your pipeline something to deploy in the next step.
In this step, get ready to:
- Install and configure Git on your machine.
- Create a GitHub repository for your project.
- Create website files and push them to GitHub.
Install and configure Git
Git is the version control tool that records your code changes and sends them to GitHub. Your pipeline watches GitHub for new pushes, so Git is the trigger for everything that follows.
- In VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Check if Git is already installed by running this command:
git --version
✔️ I see a version number
Git is already installed. Skip ahead to configuring your Git identity below.
ⓧ Command not found
Git is not installed yet. Follow these steps to install it.
- Open the official Git download page in your browser.
- The download starts automatically. If it does not, click the Click here to download link for the x64 version.
- Run the downloaded installer.
- Accept all the default options by clicking Next through each screen, then click Install.
- Once the installer finishes, click Finish.
- Close your VS Code terminal by clicking the trash can icon on the terminal panel.
- Open a new terminal with Ctrl+` so it picks up the new Git installation.
- Verify Git is installed by running:
git --version
You should see a version number like git version 2.55.0.windows.1 printed in the terminal.
Still not seeing a version number?
Make sure you closed the old terminal and opened a fresh one after installing. The old terminal session does not know Git was installed.
If it still does not work, restart VS Code completely, then open a new terminal and try again.
Help me troubleshoot my Git installation
Now configure your Git identity. This attaches your name and email to every commit you make.
- Set your name by running this command:
git config --global user.name [[YOURNAME="your full name"]]
- Set your email by running this command:
git config --global user.email [[YOUREMAIL="your email address"]]
Why does Git need my name and email?
Every Git commit records who made it. GitHub uses the email to link commits to your GitHub profile. Use the same email you signed up to GitHub with.
Create a GitHub repository
A repository on GitHub is where your code lives online. Your CI/CD pipeline will watch this repository and deploy automatically whenever new code arrives.
- Go to github.com and sign in to your account.
- Click the + icon in the top-right corner of the page.
- Select New repository.
- Enter my-devops-site in the Repository name field.
- Select Public for the visibility.
- Leave all checkboxes unchecked (do not add a README, .gitignore, or license).
- Click Create repository.
You should see a Quick setup page with instructions for pushing code. Keep this page open. You will use the repository URL shortly.
Why leave the repository empty?
You are about to push files from your local machine. If GitHub creates a README first, your local files and the remote files will conflict. Starting empty avoids that.
Create your website files and push to GitHub
Now you will create a simple portfolio website. These are the files your CI/CD pipeline will deploy to AWS in the next step.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir my-devops-site
cd my-devops-site
- Create the two website files by running:
touch index.html style.css
- Confirm the files exist by running:
ls
You should see index.html and style.css listed in the output.
- Open the my-devops-site folder in VS Code by running:
code .
What does code . do?
The code . command opens VS Code with the current folder as your workspace. The dot means "this folder." A new VS Code window opens with your project files in the sidebar.
- In the new VS Code window, click on index.html in the Explorer sidebar to open it.
- Paste the following HTML into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My DevOps Portfolio</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>My DevOps Portfolio</h1>
<p>Deployed automatically with CI/CD</p>
</header>
<main>
<section class="hero">
<h2>Hello, World!</h2>
<p>This site is automatically deployed to AWS S3 every time I push code to GitHub.</p>
</section>
<section class="skills">
<h2>What I'm Learning</h2>
<ul>
<li>CI/CD Pipelines</li>
<li>GitHub Actions</li>
<li>AWS S3 Static Hosting</li>
<li>IAM and Security</li>
</ul>
</section>
</main>
<footer>
<p>Built with a CI/CD pipeline using GitHub Actions</p>
</footer>
</body>
</html>
What does this HTML do?
This is a simple portfolio page with a header, a hero section, a skills list, and a footer. The <link> tag connects it to style.css for styling.
The content mentions CI/CD and GitHub Actions because this site is about your DevOps skills. You will see it live on the internet by the end of the next step.
- Save index.html with Ctrl+S.
- Right-click index.html in the Explorer sidebar and select Open In Default Browser.
You should see your page in the browser with unstyled text. The heading, skills list, and footer are all visible but plain. That is expected because style.css is still empty.
- Back in VS Code, click on style.css in the Explorer sidebar to open it.
- Paste the following CSS reset and body styles into style.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
header {
text-align: center;
padding: 2rem 0;
border-bottom: 2px solid #0066cc;
}
What does this CSS do?
The * selector resets all margins and padding to zero, giving you a clean slate. The body styles set a readable font, center the content, and cap the width at 800px. The header gets a blue underline border.
- Save style.css with Ctrl+S and refresh your browser tab.
You should see the text is now centered with a clean font, and the header has a blue bottom border.
- Add the header text styles and main content styles below your existing CSS in style.css:
header h1 {
color: #0066cc;
font-size: 2.5rem;
}
header p {
color: #666;
font-size: 1.1rem;
margin-top: 0.5rem;
}
main {
padding: 2rem 0;
}
.hero {
background: #f0f7ff;
padding: 2rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.hero h2 {
color: #0066cc;
margin-bottom: 0.5rem;
}
.skills {
padding: 1rem 0;
}
.skills h2 {
margin-bottom: 1rem;
}
What does this CSS do?
The heading turns blue and grows larger. The hero section gets a light blue background with rounded corners. The skills heading gets spacing below it for the list that follows.
- Save style.css with Ctrl+S and refresh your browser.
You should see the heading in blue, and the hero section has a light blue background box with rounded corners.
- Add the skills list styles and footer below your existing CSS in style.css:
.skills ul {
list-style: none;
}
.skills li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.skills li::before {
content: "\2713";
position: absolute;
left: 0;
color: #0066cc;
font-weight: bold;
}
footer {
text-align: center;
padding: 2rem 0;
border-top: 1px solid #eee;
color: #666;
}
What does this CSS do?
The default bullet points are removed and replaced with blue checkmarks using the ::before pseudo-element. The footer gets a subtle top border and gray text to separate it from the main content.
- Save style.css with Ctrl+S and refresh your browser.
Your page should now look polished. The skills list shows blue checkmarks instead of bullet points, and the footer has a light gray divider line.
Page looks the same after refreshing?
Your browser may be caching the old CSS. Try a hard refresh with Ctrl+Shift+R to force it to reload the stylesheet.
If styles are partially applied, check that each new chunk was pasted below the previous code, not replacing it.
Help me fix my CSS not loading in the browser
✔️ Awesome, I've got everything!
Great. Double check you have saved both index.html and style.css.
ⓧ I'd like to double check the full code
Here is the complete index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My DevOps Portfolio</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>My DevOps Portfolio</h1>
<p>Deployed automatically with CI/CD</p>
</header>
<main>
<section class="hero">
<h2>Hello, World!</h2>
<p>This site is automatically deployed to AWS S3 every time I push code to GitHub.</p>
</section>
<section class="skills">
<h2>What I'm Learning</h2>
<ul>
<li>CI/CD Pipelines</li>
<li>GitHub Actions</li>
<li>AWS S3 Static Hosting</li>
<li>IAM and Security</li>
</ul>
</section>
</main>
<footer>
<p>Built with a CI/CD pipeline using GitHub Actions</p>
</footer>
</body>
</html>
For style.css, confirm these chunks are present in this order:
- Reset (*), body, and header styles (blue bottom border).
- header h1, header p, main, .hero, .hero h2, .skills, and .skills h2 styles.
- .skills ul, .skills li, .skills li::before (checkmarks), and footer styles.
Your website looks great locally. Now initialize a Git repository and push everything to GitHub.
- Switch back to your VS Code terminal (click the Terminal panel at the bottom of VS Code).
- Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: add website files"
What do these commands do?
git init creates a hidden .git folder that tracks all changes. git add . stages every file in the folder. git commit saves a snapshot with a message describing what you did.
- Connect your local repo to GitHub and push your code by running these commands:
git branch -M main
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/my-devops-site.git
git push -u origin main
What do these commands do?
git branch -M main renames your default branch to main. git remote add origin links your local repo to the GitHub repository you just created. git push uploads your files to GitHub.
GitHub will ask you to sign in
When you push for the first time, a browser window opens asking you to sign in to GitHub and authorize access. Sign in with your GitHub account and approve the request. Your credentials are saved for future pushes.
Before you check GitHub, what do you expect to see in the repository?
- Open your repository page at github.com/your-github-username/my-devops-site in your browser.
You should see index.html and style.css listed in the repository with your commit message "Initial commit: add website files" displayed above the file list.
Don't see your files on GitHub?
If the push failed, check that the repository URL in the git remote add origin command matches your actual GitHub username and the repo name my-devops-site.
If you see "remote origin already exists", run git remote set-url origin with the correct URL instead.
Help me push my code to GitHub
Your code is now tracked by Git and stored on GitHub. Next up, you will deploy this site to AWS S3 so anyone can visit it with a public URL.
Deploy Your Site to S3 (The Manual Way)
Your code is on GitHub and ready to go. Now you need somewhere to host it so the world can actually see your website.
In this step, you will manually deploy your site to AWS S3. This is the slow, click-heavy process that every developer does before discovering automation. Pay attention to how many steps it takes. That pain is exactly what your CI/CD pipeline will eliminate later.
In this step, get ready to:
- Create an S3 bucket and enable static website hosting.
- Configure public access permissions so anyone can view the site.
- Upload your website files and verify the live URL works.
Create an S3 bucket and enable static website hosting
S3 buckets are containers that store files in the cloud. When you enable static website hosting on a bucket, AWS gives you a public URL that serves those files as a website.
- Open your browser and go to the AWS Management Console at https://console.aws.amazon.com/s3/.
- Sign in with your AWS account if prompted.
- Click Create bucket.
- In the Bucket name field, enter a globally unique name like my-devops-site- followed by your name (for example, my-devops-site-alex).
- For AWS Region, select US East (N. Virginia) us-east-1.
- Leave all other settings at their defaults and click Create bucket at the bottom of the page.
Why us-east-1?
This is AWS's oldest and most commonly used region. Choosing a consistent region keeps things simple for the rest of the project.
You should see your new bucket appear in the Buckets list. Now you need to turn on website hosting.
- Click your bucket name to open it.
- Click the Properties tab.
- Scroll down to the Static website hosting section and click Edit.
- Select Enable.
- In the Index document field, type index.html.
- Click Save changes.
After saving, scroll back down to the Static website hosting section. You should now see a Bucket website endpoint URL displayed. This is your site's future public address.
What is the endpoint URL?
The URL follows the pattern http://bucket-name.s3-website-us-east-1.amazonaws.com. Anyone with this link will be able to view your website once you finish configuring permissions.
Don't see the endpoint URL?
Make sure you clicked Save changes after enabling static website hosting. If the section still says Disabled, click Edit again and confirm Enable is selected.
Help me troubleshoot my S3 static website hosting
Configure public access and bucket policy
By default, S3 blocks all public access to your bucket. Since you want anyone on the internet to view your website, you need to disable this protection and add a policy that grants read access.
- In your bucket, click the Permissions tab.
- Under Block public access (bucket settings), click Edit.
- Uncheck Block all public access.
- Click Save changes.
- A confirmation dialog appears. Type confirm in the field and click Confirm.
The Block public access section should now show that public access is allowed. Next, add a bucket policy that tells S3 exactly what kind of access to allow.
- Still on the Permissions tab, scroll down to Bucket policy and click Edit.
- Paste the following policy into the editor, replacing the bucket name with your own:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::[[BUCKET_NAME="my-devops-site-yourname"]]/*"
}
]
}
What does this policy do?
This policy grants everyone ("Principal": "*") permission to read (s3:GetObject) any file in your bucket. The /* at the end of the Resource means "all objects inside this bucket."
Without this policy, visitors would get an Access Denied error when trying to view your website.
- Click Save changes.
You should see a banner confirming the policy was saved. The Permissions tab now shows a warning that the bucket is publicly accessible. That is exactly what you want for a public website.
Seeing an Access Denied or invalid resource error?
Double check that the bucket name in the Resource field matches your actual bucket name exactly. Even one character difference will cause the policy to fail.
If you get an error saying the policy allows public access, go back and confirm that Block all public access is unchecked. The bucket policy cannot be saved while public access is still blocked.
Help me fix my S3 bucket policy
Upload your website files and visit the live URL
Your bucket is configured and ready to serve a website. Now you need to put your actual files in it.
- Click the Objects tab in your bucket.
- Click Upload.
- Click Add files.
- Navigate to your my-devops-site folder on your Desktop and select both index.html and style.css.
- Click Open.
- Click Upload at the bottom of the page.
You should see a green success banner confirming both files were uploaded. Click Close to return to your bucket.
Now for the moment of truth. Before you open the URL, what do you think you will see?
- Click the Properties tab.
- Scroll down to Static website hosting and click the Bucket website endpoint URL to open it in a new tab.
Your website is live on the internet. You should see the blue "My DevOps Portfolio" heading, the "Hello, World!" section, and your skills list styled exactly as you built it locally.
What just happened?
You manually deployed a website by creating a bucket, configuring permissions, and uploading files through a web interface. It worked, but think about how many clicks that took. Every time you change a line of code, you would have to repeat the upload process.
That repetition is exactly the problem CI/CD solves.
Seeing a 403 Forbidden or blank page?
Make sure you uploaded index.html to the root of the bucket, not inside a folder. If your Objects list shows a folder containing the files, delete them and re-upload directly to the bucket root.
Also verify that your bucket policy was saved successfully and that Block all public access is disabled. Without both of these, visitors cannot access your files.
Help me troubleshoot my S3 website
Your site is live. Anyone with that URL can visit it right now. In the next step, you will connect GitHub to AWS so that your pipeline can deploy changes automatically instead of you doing all of this by hand.
Connect GitHub to AWS
Your website is live on S3 and anyone can visit it. But right now, the only way to update it is by manually uploading files through the AWS Console.
To automate deployments, GitHub Actions needs permission to upload files to your S3 bucket on your behalf. In this step, you will create a dedicated IAM user with limited permissions and store its credentials securely in GitHub.
In this step, get ready to:
- Create an IAM user with S3 deployment permissions.
- Generate access keys for the IAM user.
- Store AWS credentials securely as GitHub Secrets.
Create an IAM user with S3 deployment permissions
You need a dedicated IAM user whose only job is deploying your site. This follows the principle of least privilege: giving each identity only the permissions it needs and nothing more.
- In the AWS Console, type IAM in the top search bar and select IAM from the results.
- In the left navigation panel, click Users.
- Click Create user in the top right corner.
- Enter github-actions-deployer as the user name.
- Leave the Provide user access to the AWS Management Console checkbox unchecked. This user only needs programmatic access.
- Click Next.
- On the Set permissions page, select Attach policies directly.
- Do not select any policies from the list. Click Next to skip this page for now.
Why skip policies here?
You will attach a custom inline policy directly to the user after creation. Inline policies are scoped to a single user and give you precise control over exactly which bucket and actions are allowed.
- On the Review and create page, click Create user.
You should see a green success banner confirming the user was created.
Now attach the deployment permissions to this user.
- Click on the user name github-actions-deployer to open the user details.
- Select the Permissions tab.
- Click the Add permissions dropdown, then select Create inline policy.
- In the policy editor, click the JSON tab to switch to the JSON editor.
- Delete the default content and paste the following policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::[[S3_BUCKET_NAME="your-bucket-name"]]",
"arn:aws:s3:::[[S3_BUCKET_NAME="your-bucket-name"]]/*"
]
}
]
}
What does this policy do?
This policy grants exactly four permissions on your specific bucket:
- s3:PutObject allows uploading new files.
- s3:GetObject allows reading existing files.
- s3:ListBucket allows listing the bucket contents to detect what changed.
- s3:DeleteObject allows removing old files that no longer exist in the repo.
The two Resource lines scope these permissions to your specific bucket only. The first line covers the bucket itself. The second (with /*) covers all objects inside it.
- Replace your-bucket-name in both Resource lines with your actual S3 bucket name from Step 2.
- Click Next.
- Enter S3DeployPolicy as the policy name.
- Click Create policy.
You should see the inline policy appear under the Permissions tab for your user.
Policy not saving?
- Make sure both Resource ARN lines contain your exact bucket name with no extra spaces.
- Confirm you have the correct JSON syntax. Missing commas or brackets will prevent the policy from saving.
- Check that you are signed in with an account that has IAM permissions.
Need more help? Why can't I save my IAM inline policy?
Generate access keys
The IAM user exists but has no credentials yet. Access keys are what GitHub Actions will use to authenticate as this user when calling AWS APIs.
- On the github-actions-deployer user page, click the Security credentials tab.
- Scroll down to the Access keys section and click Create access key.
- On the Access key best practices & alternatives page, select Third-party service as the use case.
- Check the confirmation checkbox at the bottom of the page.
- Click Next.
- Skip the description tag and click Create access key.
Copy both keys right now
You will see both the Access key ID and the Secret access key on screen. Copy both values and paste them somewhere safe (a text file or password manager).
The secret access key is only shown once. If you close this page without copying it, you will need to delete the key and create a new one.
- Copy the Access key ID and paste it into a temporary text file.
- Click Show next to the secret access key, then copy and paste it into the same text file.
- Click Done.
Accidentally closed the page?
If you lost the secret access key, go back to the Security credentials tab, delete the access key you just created by clicking Actions > Delete, and create a new one following the same steps above.
Still stuck? Help me recreate my access key
Add secrets to GitHub
Hardcoding credentials in code is a security risk. GitHub Secrets encrypt your values and make them available to workflows without exposing them in your repository.
- Open your my-devops-site repository on github.com.
- Click Settings in the repository navigation bar.
- In the left sidebar, click Secrets and variables, then click Actions.
- Click New repository secret.
You need to add three secrets, one at a time.
- For the first secret, enter AWS_ACCESS_KEY_ID as the name.
- Paste your Access Key ID from the text file into the Secret field.
- Click Add secret.
- Click New repository secret again.
- Enter AWS_SECRET_ACCESS_KEY as the name and paste your Secret Access Key as the value.
- Click Add secret.
- Click New repository secret one more time.
- Enter S3_BUCKET_NAME as the name and paste your S3 bucket name as the value.
- Click Add secret.
Why store the bucket name as a secret?
The bucket name is not sensitive like a password. However, storing it as a secret keeps your workflow file generic and reusable. If you ever change your bucket name, you update it in one place instead of editing the workflow YAML.
Before you verify, what do you expect to see on the Actions secrets page now? How many secrets should be listed?
- Confirm you are on the Settings > Secrets and variables > Actions page.
You should see three repository secrets listed: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and S3_BUCKET_NAME. The values are masked, showing only the last updated timestamp.
Only see one or two secrets?
- Make sure you clicked Add secret after each entry. If you navigated away before saving, the secret was not stored.
- Double-check you are in the Repository secrets section, not Environment secrets.
- Verify the secret names are spelled exactly as shown: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET_NAME.
Need help? Why aren't my GitHub Secrets showing up?
GitHub now has everything it needs to authenticate with AWS on your behalf. Next up, you will write the pipeline workflow that uses these secrets to deploy your site automatically.
Write Your CI/CD Pipeline
Your GitHub repository now has secure access to AWS through the secrets you stored. It's time to write the instructions that tell GitHub exactly what to do with those credentials.
Remember uploading files to S3 by hand through the console? Repeating that process every time you change a line of code does not scale. A CI/CD pipeline automates the entire deployment so you never touch the AWS console for deployments again.
In this step, get ready to:
- Create the GitHub Actions workflow file that defines your deployment pipeline.
- Push the workflow to GitHub and confirm it runs successfully.
Create the GitHub Actions workflow file
GitHub Actions looks for workflow files inside a specific folder in your repository: .github/workflows/. Any YAML file in that folder becomes a pipeline that GitHub can run.
- Create the required folder structure by running this command in your VS Code terminal:
mkdir -p .github/workflows
- Confirm the folder was created by running:
ls .github
You should see workflows listed in the output.
Don't see the workflows folder?
If you get an error about the path not existing, make sure you are running the command from inside your my-devops-site project folder. Run pwd to check your current location.
If you are using PowerShell and mkdir -p does not work, try mkdir .github/workflows instead. PowerShell creates intermediate folders automatically.
Help me create the .github/workflows folder in my project
- Create a new file called deploy.yml inside the .github/workflows/ folder.
- In VS Code, right-click on the workflows folder in the Explorer sidebar and select New File.
- Name the file deploy.yml and press Enter.
- Paste the following workflow code into deploy.yml:
name: Deploy to S3
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6.2.3
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./ s3://${{ secrets.S3_BUCKET_NAME }}/ \
--delete \
--exclude ".git/*" \
--exclude ".github/*" \
--exclude "README.md"
What does this workflow do?
- name: Deploy to S3 gives the workflow a human-readable name that appears in the GitHub Actions tab.
- on: push: branches: [main] triggers the workflow automatically every time you push code to the main branch.
- workflow_dispatch lets you manually trigger the workflow from the GitHub UI using a Run workflow button.
- actions/checkout@v7 downloads your repository code onto the runner so the deploy step can access it.
- aws-actions/configure-aws-credentials@v6.2.3 reads the secrets you stored and sets up the AWS CLI on the runner with those credentials.
- aws s3 sync uploads your files to S3. The --delete flag removes old files from S3 that no longer exist locally. The --exclude flags prevent Git metadata and GitHub Actions config from being uploaded to your public website.
- Save deploy.yml by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Why this approach instead of AWS CodePipeline?
AWS has its own CI/CD tool called CodePipeline, but GitHub Actions is a better fit here. It is free for public repositories, requires zero additional AWS infrastructure, and lives right where your code already is.
GitHub Actions lets you focus on learning CI/CD concepts without extra AWS setup overhead.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete deploy.yml file for comparison:
name: Deploy to S3
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6.2.3
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./ s3://${{ secrets.S3_BUCKET_NAME }}/ \
--delete \
--exclude ".git/*" \
--exclude ".github/*" \
--exclude "README.md"
Push the workflow and watch it run
The workflow file only takes effect once it reaches GitHub. Pushing it to the main branch triggers the pipeline for the first time.
- Commit and push the new workflow file by running these commands:
git add .github/workflows/deploy.yml
git commit -m "Add CI/CD deployment workflow"
git push
What does this do?
git add stages the new file. git commit saves it to your local history. git push sends it to GitHub, which immediately detects the workflow file and starts running it.
Before you check the result, what do you think will happen when this push reaches GitHub?
- Open your my-devops-site repository on github.com.
- Click the Actions tab at the top of the repository page.
- You should see a workflow run named Add CI/CD deployment workflow (matching your commit message) either in progress or already complete.
- Wait for a green checkmark to appear next to the run. This confirms the pipeline deployed your files to S3 successfully.
Seeing a red X instead of a green checkmark?
Click on the failed run to see the error log. The most common causes are:
- Incorrect secret names. Go to Settings > Secrets and variables > Actions and verify the three secrets are named exactly AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and S3_BUCKET_NAME.
- Wrong bucket name in the S3_BUCKET_NAME secret. It must match your actual S3 bucket name exactly (case-sensitive).
- IAM permissions too restrictive. Make sure the inline policy on github-actions-deployer includes s3:PutObject, s3:GetObject, s3:ListBucket, and s3:DeleteObject.
Help me debug my failed GitHub Actions workflow run
- Open your S3 website endpoint URL in a new browser tab to confirm the site is still live.
Your site loads exactly as before. The difference is that this time, the files were deployed by your pipeline instead of your hands.
Your pipeline is live. Every push to main now deploys your site automatically. Next up, you'll make a real code change and watch the full cycle happen end to end.
Push a Change and Watch the Magic
Your pipeline ran successfully in the last step, proving the connection between GitHub and AWS works end to end.
Now for the real payoff. You will edit your website code, push the change, and watch your live site update automatically. No logging into AWS, no clicking upload buttons.
In this step, get ready to:
- Make a visible change to your website code.
- Push the change and watch the pipeline deploy it automatically.
Make a visible change to your website
To prove your pipeline works, you need to change something you can see on the live site. The hero heading is the most obvious element on the page, so you will swap it for something personal.
- In VS Code, open index.html from the Explorer sidebar.
- Find this line inside the hero section:
<h2>Hello, World!</h2>
- Replace Hello, World! with a new heading of your choice. For example:
<h2>I Automated This Deployment</h2>
Pick something you will recognize
Choose any text you like. The point is that when you refresh your live site later, you will immediately see that the new heading appeared without you touching AWS.
- Save index.html.
- Commit the change with a descriptive message by running these commands in your VS Code terminal:
git add index.html
git commit -m "Update hero heading to test CI/CD pipeline"
What does this command do?
The git add stages the file you changed. The git commit saves a snapshot of that change in your local Git history with a message explaining what you did.
You should see output confirming one file changed and one insertion. The change is saved locally but has not reached GitHub yet.
Push and verify the automatic deployment
This is the moment you see the entire CI/CD loop in action. One push triggers the pipeline, and your live site updates without any manual intervention.
Before you push, think about what you expect to happen. Will your live site update instantly? Will it take a few seconds? What would you see on the GitHub Actions tab?
- Push your commit to GitHub by running:
git push
- Immediately open your browser and go to your my-devops-site repository on github.com.
- Click the Actions tab at the top of your repository page.
You should see a new workflow run in progress, shown by an orange spinning circle. This is your pipeline reacting to the push you just made.
- Wait for the workflow run to finish. It typically takes 15-30 seconds.
- Confirm a green checkmark appears next to the run.
What just happened?
Your push triggered the deploy.yml workflow automatically. GitHub Actions spun up a fresh server, checked out your code, authenticated with AWS using your stored secrets, and synced your files to S3.
You did not log into AWS. You did not click upload. The pipeline handled everything.
- Open a new browser tab and navigate to your S3 website endpoint URL (the same URL you visited in Step 2).
- Refresh the page.
Your new heading is now live on the internet. You changed one line of code, pushed to GitHub, and your live website updated automatically within seconds.
Still seeing the old heading?
Try a hard refresh: press Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to bypass your browser cache.
If the heading still has not changed, go back to the Actions tab and click on the latest run. Check that all steps show green checkmarks and no error messages appear in the logs.
Help me troubleshoot why my S3 site is not showing the updated heading after a GitHub Actions deployment.
Why is this a big deal?
Think back to Step 2 when you manually uploaded files through the AWS console. That process took multiple clicks, required you to be logged into AWS, and was easy to forget or mess up.
Now you have a pipeline that does the same thing in seconds, triggered by a single git push. This is how professional teams ship code to production every day.
Secret mission
Add a Staging Environment
Real DevOps teams never deploy directly to production without testing first. In this bonus challenge, you'll create a staging environment with its own S3 bucket and branch-based deployment routing. Changes pushed to a staging branch deploy to a separate URL for previewing, while only merges to main update your live production site.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses AWS Free Tier resources, so there are no ongoing costs as long as you stay within the free tier limits.
Cost warning
AWS Free Tier for S3 covers 5 GB of storage and 20,000 GET requests per month for 12 months (legacy accounts) or is included in up to $200 of credits (new accounts created after July 15, 2025). If your free tier expires or you exceed the limits, S3 charges apply for storage and requests. Deleting the buckets removes any future cost risk.
Resources you used:
- S3 bucket (production) with static website hosting enabled.
- S3 bucket (staging) with static website hosting enabled.
- IAM user github-actions-deployer with an active access key.
- GitHub repository secrets (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_BUCKET_NAME_STAGING).
- Local project folder my-devops-site on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep your site live and auto-deploying.
- Your website stays live at its S3 endpoint URL.
- Every push to GitHub still triggers an automatic deployment.
- No ongoing cost within AWS Free Tier limits.
Pause - I'll come back to this later
There are no running compute resources to shut down. S3 only charges for storage and requests, so your costs remain negligible within the free tier. If you want to reduce usage while keeping your setup intact:
- Empty both S3 buckets (delete the objects inside) without deleting the buckets themselves. This removes the hosted files but preserves your bucket configuration for later.
- Your GitHub Actions workflow and secrets remain ready. The next time you push, the pipeline deploys your files again automatically.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
Delete both S3 buckets:
- Open the S3 console at https://console.aws.amazon.com/s3/.
- In the left navigation pane, choose General purpose buckets.
- Select the radio button next to your production bucket, then choose Empty.
- Type permanently delete in the confirmation field, then choose Empty.
- Return to the bucket list, select the same bucket, and choose Delete.
- Type the bucket name to confirm, then choose Delete bucket.
- Repeat the empty and delete steps for your staging bucket.
Delete the IAM user:
- Open the IAM console at https://console.aws.amazon.com/iam/.
- In the left navigation pane, choose Users.
- Select the checkbox next to github-actions-deployer.
- Choose Delete at the top of the page.
- Type github-actions-deployer in the confirmation field, then choose Delete.
Remove GitHub Secrets:
- In your my-devops-site GitHub repository, go to Settings.
- Select Secrets and variables, then Actions.
- Click the trash icon next to each of the four secrets (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_BUCKET_NAME_STAGING) and confirm each deletion.
Delete your local project folder:
- Delete the local folder by running this command in your terminal:
cd ~/Desktop
rm -rf my-devops-site
On Windows, use this instead:
cd ~\Desktop
Remove-Item -Recurse -Force my-devops-site
Nice Work!
Nice Work!
You just built a fully automated deployment pipeline connecting GitHub to AWS. Every time you push code, your live website updates itself without you ever touching the AWS console.
You've learned how to:
- Deployed a live website to AWS S3 accessible via a public URL, and experienced the difference between manual uploads and automated CI/CD.
- Created a GitHub Actions workflow that automatically syncs your code to S3 on every push to main.
- Configured IAM permissions following least-privilege principles and stored sensitive credentials as GitHub Secrets so no passwords live in your code.
- Added a staging environment with branch-based deployment routing so changes are tested before going live (Secret Mission).
Ready to quiz yourself?