AI-Powered CI/CD with GitHub Actions
Build a CI/CD pipeline with Docker, AWS ECR, and AI code review.
Introduction
30 Second Summary
Every time a developer pushes code, someone has to test it, package it, and deploy it. Doing this manually is slow, error-prone, and the number one reason teams ship broken software.
In this project, you will build an automated pipeline that tests, packages, and delivers code every time changes are merged. An AI reviewer catches security issues before anything goes live.
What You'll Build
A complete CI/CD pipeline using GitHub Actions and Amazon Q Developer that automatically builds, tests, and pushes a Docker container to AWS ECR with AI-powered code review on every pull request.
By the end of this project, you'll have:
- A containerized Python API packaged with Docker and pushed to a private container registry in AWS.
- A CI/CD pipeline that runs tests, builds your image, and deploys on every merge to main.
- An AI-powered code reviewer that scans every pull request for security vulnerabilities and enforces your custom coding standards.
- Secret Mission: Deploy your container to a live EC2 instance with automated deployments on every merge.
Do I need to know Python for this project?
No. All application code is provided for you to copy. Your job is to build the infrastructure and automation around it. This is exactly what DevOps engineers do in the real world.
Set Up Your Environment
Before you can build a pipeline, you need the tools that talk to AWS and GitHub on your behalf.
In this step, you will install the AWS CLI, create a GitHub Organization (required for Amazon Q Developer), and confirm all your tools are ready to go.
In this step, get ready to:
- Install and configure the AWS CLI to connect to your AWS account.
- Create a free GitHub Organization for Amazon Q Developer.
- Verify Docker, Python, and the AWS CLI are all working.
Install and configure the AWS CLI
The AWS CLI is a command-line tool that lets you interact with AWS services from your terminal. Your pipeline will use it to push Docker images to a container registry in AWS.
- Check if you already have the AWS CLI installed by running this command:
aws --version
✔️ I see a version number starting with aws-cli/2
You already have AWS CLI v2 installed. Skip ahead to configuring it below.
ⓧ I see aws-cli/1 or an older version
You have an older version installed. You need AWS CLI version 2. Follow the install instructions below to replace it.
macOS
- Download and install AWS CLI v2 by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
What do these commands do?
The first command downloads the official AWS CLI installer package from Amazon. The second command runs the macOS installer, which places the aws binary in /usr/local/bin/ so your terminal can find it.
Windows
- Download the AWS CLI MSI installer from this URL:
https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file and follow the on-screen instructions to complete the installation.
- Close and reopen your terminal after the installer finishes.
- Confirm the install worked by running:
aws --version
You should see output starting with aws-cli/2.
ⓧ Command not found
The AWS CLI is not installed yet. Let's install it now.
macOS
- Download and install AWS CLI v2 by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
What do these commands do?
The first command downloads the official AWS CLI installer package from Amazon. The second command runs the macOS installer, which places the aws binary in /usr/local/bin/ so your terminal can find it.
Windows
- Download the AWS CLI MSI installer from this URL:
https://awscli.amazonaws.com/AWSCLIV2.msi
- Run the downloaded AWSCLIV2.msi file and follow the on-screen instructions to complete the installation.
- Close and reopen your terminal after the installer finishes.
- Confirm the install worked by running:
aws --version
You should see output starting with aws-cli/2.
Now you need to connect the AWS CLI to your AWS account. This tells the CLI which account to create resources in.
- Run this command to start the configuration wizard:
aws configure
Where do I find my access key and secret key?
In the AWS Console, click your account name in the top right corner. Select Security credentials. Scroll down to Access keys and click Create access key.
Copy the Access Key ID and Secret Access Key that appear. Store these safely. You will not be able to see the secret key again after you close the window.
- The wizard will ask four questions. Enter these values:
- AWS Access Key ID: paste your access key.
- AWS Secret Access Key: paste your secret key.
- Default region name: enter us-east-1.
- Default output format: enter json.
Keep your credentials private
Never share your access key or secret key with anyone. Never commit them to a Git repository. These credentials give full access to your AWS account.
- Verify the configuration worked by running:
aws sts get-caller-identity
You should see a JSON response containing your AWS account ID, user ARN, and user ID.
Seeing an error about invalid credentials?
Double-check that you pasted the full access key and secret key without extra spaces. If the credentials were typed incorrectly, run aws configure again to re-enter your values.
Help me troubleshoot my AWS CLI credentials
Create a GitHub Organization
Amazon Q Developer can only be installed on a GitHub organization, not on a personal account. You need to create a free organization now so you can install it later.
Why do I need a GitHub Organization?
Amazon Q Developer's GitHub App is designed for teams. It installs at the organization level so it can review pull requests across all repositories the org owns. Creating a free organization takes 30 seconds and costs nothing.
- Open github.com in your browser and sign in.
- Click your profile picture in the top-right corner.
- Click Settings.
- In the left sidebar under Access, click Organizations.
- Next to the Organizations header, click New organization.
- Under Choose a plan, click Choose GitHub Free.
- Enter an organization name. Something like yourname-devops works well.
- Enter your email as the contact email.
- Click Next.
- Skip the optional steps (adding members) and complete the setup.
You should land on your new organization's page. You will use this organization when installing Amazon Q Developer in a later step.
Can't find the New organization button?
Make sure you are on the Organizations page under Settings, not the main GitHub homepage. The button appears next to the Organizations heading at the top of the list.
Help me find where to create a new GitHub Organization
Verify your tools
Before moving on, confirm that Docker Desktop and Python are ready alongside the AWS CLI you just configured.
- Confirm Docker Desktop is running by checking for the whale icon in your menu bar (macOS) or system tray (Windows).
- Verify Docker is accessible from your terminal by running:
docker --version
You should see a version number in the output.
- Verify Python 3 is available by running:
python3 --version
You should see a version number starting with Python 3.
Docker command not found?
Make sure Docker Desktop is running. On macOS, look for the whale icon in the top menu bar. On Windows, check the system tray in the bottom right. If Docker Desktop is not installed, download it from docker.com/products/docker-desktop.
Help me troubleshoot my Docker Desktop installation
Your environment is locked and loaded. Next up, you will create the application files that your pipeline will build and deploy.
Create the Application Files
Your environment is set up and ready to go. Now it's time to create the application that your pipeline will build and deploy throughout this project.
The app is a simple Python API built with Flask. You do not need to understand the Python code. Your job is to copy the provided files exactly and focus on the DevOps infrastructure around them.
In this step, get ready to:
- Create the project folder with app.py and requirements.txt.
- Add a tests/ folder with automated tests.
- Run the app locally to confirm it works.
Create the project folder and app files
Every DevOps project starts with an application to deploy. You'll create a project folder and add two files: the API code and a list of its dependencies.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir devops-api
cd devops-api
- Confirm you're inside the new folder by running:
pwd
You should see a path ending in devops-api.
Seeing a different path?
Make sure you ran cd ~/Desktop first, then mkdir devops-api and cd devops-api as separate commands. If your terminal shows a different location, run cd ~/Desktop/devops-api to get to the right place.
Help me find my project folder.
- Open your code editor to this folder. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code (or Cursor if that's your editor) and press Enter to open it.
- In your editor, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the devops-api folder, and click Open.
You should see devops-api in the sidebar of your editor with no files inside yet.
- Create a new file called app.py inside your devops-api folder by clicking the new file icon in the sidebar (or press Cmd+N on macOS / Ctrl+N on Windows, then save immediately with the name app.py).
- Paste the following code into app.py:
from flask import Flask, jsonify
import platform
import datetime
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Welcome to the DevOps API",
"status": "running"
})
@app.route("/health")
def health():
return jsonify({
"status": "healthy",
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"system": platform.system(),
"python_version": platform.python_version()
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
You do not need to memorize or understand this code. Here's a quick overview of what it does:
- app = Flask(__name__) creates the web application.
- @app.route("/") defines a home page that returns a simple status message.
- @app.route("/health") defines a health check endpoint that returns system info and a timestamp.
- app.run(host="0.0.0.0", port=5000) starts the server on port 5000 when you run the file directly.
- Save app.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
File not saving to the right place?
Make sure the file is named exactly app.py (lowercase, no spaces). It should appear in the sidebar directly inside the devops-api folder, not nested inside another folder.
Help me check my file location.
- Create another new file called requirements.txt in the same devops-api folder.
- Paste the following into requirements.txt:
flask==3.1.1
pytest==8.3.4
What is requirements.txt?
This file lists the exact packages and versions your project depends on. flask==3.1.1 is the web framework for the API. pytest==8.3.4 is the testing framework your pipeline will use to verify the app works before deploying.
- Save requirements.txt.
Your editor sidebar should now show two files: app.py and requirements.txt.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full app.py:
from flask import Flask, jsonify
import platform
import datetime
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Welcome to the DevOps API",
"status": "running"
})
@app.route("/health")
def health():
return jsonify({
"status": "healthy",
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"system": platform.system(),
"python_version": platform.python_version()
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Here is the full requirements.txt:
flask==3.1.1
pytest==8.3.4
Create the test file
Automated tests are what your pipeline will run to verify the app works before deploying it. If any test fails, the pipeline stops and the broken code never reaches production.
- Create a new folder called tests inside your devops-api folder. In your editor sidebar, right-click in the empty space and select New Folder.
- Name the folder tests.
- Inside the tests folder, create a new file called test_app.py.
- Paste the following code into test_app.py:
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_home(client):
response = client.get("/")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "running"
assert "message" in data
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"
assert "timestamp" in data
assert "python_version" in data
What does this test file do?
These are automated tests that your CI/CD pipeline will run on every pull request:
- @pytest.fixture creates a reusable test client so each test gets a fresh app instance.
- test_home checks that the / route returns a 200 status and the expected JSON fields.
- test_health checks that the /health route returns a 200 status with healthy, timestamp, and python_version in the response.
- Save test_app.py.
Your sidebar should now show app.py, requirements.txt, and a tests folder containing test_app.py.
Don't see the tests folder?
Make sure you created the folder named exactly tests (lowercase, plural) directly inside devops-api. The file test_app.py must be inside the tests folder, not at the top level of your project.
Help me set up my test file.
✔️ Awesome, I've got everything!
Great. Make sure test_app.py is saved before moving on.
ⓧ I'd like to double check the full code
Here is the full tests/test_app.py:
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_home(client):
response = client.get("/")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "running"
assert "message" in data
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"
assert "timestamp" in data
assert "python_version" in data
Run the app locally to see it work
Before building any pipeline, you should confirm the app works on your own machine. This is exactly what the pipeline will do later, but you'll do it manually first.
- Install the project dependencies by running this command in your terminal (make sure you're inside the devops-api folder):
pip install -r requirements.txt
What does this command do?
This reads requirements.txt and installs the exact versions of Flask and pytest listed there. Pinning versions ensures everyone working on the project (and the pipeline) uses the same dependencies.
You should see output showing Flask and pytest being downloaded and installed.
Seeing command not found?
If you see an error like zsh: command not found: pip, try using pip3 instead:
pip3 install -r requirements.txt
On macOS, Python 3 often uses pip3 as the command name. If that still doesn't work, make sure you're in the devops-api folder where requirements.txt lives.
Help me install my dependencies.
- Start the app by running:
python app.py
You should see output that includes something like Running on http://0.0.0.0:5000. The terminal will appear to hang. That means the server is running and waiting for requests.
Seeing command not found for python?
If your terminal doesn't recognize python, try python3 instead:
python3 app.py
On macOS, python3 is often the correct command for Python 3.
Help me start my app.
- Open your browser and go to http://localhost:5000/health.
You should see a JSON response with "status": "healthy" along with a timestamp, system name, and Python version.
- Go back to your terminal and press Ctrl+C to stop the server.
Why stop the server?
You've confirmed the app works locally. Stopping it frees up port 5000 for later when you run the same app inside a Docker container.
Your app files are ready and confirmed working. Next up, you'll package this application into a Docker container so it can run the same way on any machine.
Containerize with Docker
Your app works on your machine. You confirmed it by visiting http://localhost:5000/health and seeing a healthy response. But "works on my machine" is the oldest problem in software deployment.
Docker solves this by packaging your app, its dependencies, and its runtime environment into a single portable unit called a container. In this step, you will create a Dockerfile that tells Docker exactly how to build your app into a container image, then run it to prove it works the same way everywhere.
In this step, get ready to:
- Create a Dockerfile to package your app into a container image.
- Create a .dockerignore file to keep the image lean and secure.
- Build and run the container locally to verify it works.
Create the Dockerfile
A Dockerfile is a recipe that tells Docker how to build your container image. It starts from a base image, installs your dependencies, copies your code in, and defines how to run the app.
- In your code editor, create a new file called Dockerfile inside your devops-api folder. This file has no extension.
- Copy the following content into Dockerfile:
FROM python:3.12-slim
WORKDIR /app
# Copy and install dependencies first (cached layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Create a non-root user for security
RUN useradd --create-home appuser
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]
What does this Dockerfile do?
- FROM python:3.12-slim starts from a lightweight Python image. This gives your container Python without unnecessary extras.
- COPY requirements.txt is copied first so Docker can cache the dependency install step. If your code changes but dependencies don't, Docker skips re-downloading packages.
- RUN useradd --create-home appuser creates a non-root user. Running as root inside a container is a security risk because a compromised container could affect the host system.
- CMD ["python", "app.py"] tells Docker what to run when the container starts.
- Save Dockerfile.
You won't see a change yet. The next substep creates one more file, then you will build the container.
File named Dockerfile.txt instead of Dockerfile?
Some editors automatically add a .txt extension. Make sure the file is named exactly Dockerfile with no extension. On Windows, you may need to enable "Show file name extensions" in File Explorer to confirm.
Why isn't Docker finding my Dockerfile?
Create the .dockerignore file
A .dockerignore file works like a .gitignore. It tells Docker which files to exclude when copying your project into the container. This keeps the image smaller and avoids accidentally including sensitive files.
- Create a new file called .dockerignore inside your devops-api folder.
- Copy the following content into .dockerignore:
.git
__pycache__
*.pyc
.env
venv
.github
.amazonq
tests
What does this exclude?
- .git and __pycache__ are development artifacts that bloat the image.
- .env could contain secrets that should never be baked into a container image.
- tests are excluded because they run in the pipeline, not inside the production container.
- Save .dockerignore.
- Confirm both files exist by running this in your terminal:
ls
You should see Dockerfile and .dockerignore listed alongside app.py, requirements.txt, and the tests/ folder.
Don't see .dockerignore in the list?
Files starting with a dot are hidden by default on macOS and Linux. Run ls -a to see all files including hidden ones. If the file still does not appear, double check you created it inside the devops-api folder and not one level above.
Help me find my .dockerignore file
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on to the build step.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home appuser
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]
Here is the complete .dockerignore:
.git
__pycache__
*.pyc
.env
venv
.github
.amazonq
tests
Build and test the container
Now you will turn your Dockerfile into a container image and run it. The result should look identical to when you ran the app locally, but this time everything is running inside an isolated container.
- Make sure Docker Desktop is running on your machine.
- Build the container image by running this command in your terminal (from inside the devops-api folder):
docker build -t devops-api .
What does this command do?
docker build reads your Dockerfile and creates a container image. The -t devops-api flag tags the image with the name devops-api so you can reference it later. The . at the end tells Docker to use the current directory as the build context.
The build will take 30-60 seconds the first time as it downloads the Python base image. You will see output lines starting with each step from your Dockerfile.
- Once the build finishes, start the container by running:
docker run -p 5000:5000 devops-api
What does -p 5000:5000 mean?
The -p 5000:5000 flag maps port 5000 on your machine to port 5000 inside the container. Without this, the container runs in isolation and you cannot reach it from your browser.
- Open http://localhost:5000/health in your browser.
You should see the same JSON response with "status": "healthy". Same result as before, but now it is running inside a container.
- Press Ctrl+C in your terminal to stop the container.
Seeing "port is already allocated" or no response?
If port 5000 is already in use, you may still have a previous process running. On macOS, AirPlay Receiver uses port 5000 by default. Disable it in System Settings > General > AirDrop & Handoff by turning off AirPlay Receiver.
If the build itself failed, check that your terminal is inside the devops-api folder where the Dockerfile lives. Docker needs to find the Dockerfile in the current directory.
Help me fix my Docker build or run error
Your app is now containerized and ready for the cloud. Next up, you will create an AWS container registry and push this image to it so your pipeline can pull it from anywhere.
Set Up AWS ECR and Push Your First Image
In the last step, you containerized your API with Docker and confirmed it runs locally. Now you need somewhere to store that container image so your CI/CD pipeline can access it later.
That storage location is a container registry. You will use Amazon ECR (Elastic Container Registry), a private registry inside your AWS account where Docker images live. In this step, you will create a registry, authenticate Docker to it, and push your first image.
In this step, get ready to:
- Create a private ECR repository in AWS.
- Authenticate your local Docker to the registry.
- Tag and push your Docker image to ECR.
Create an ECR repository
A container registry is like a library for Docker images. Your CI/CD pipeline will pull from this registry every time it needs to deploy your app.
- Create a private ECR repository by running this command in your terminal:
aws ecr create-repository --repository-name devops-api --region us-east-1
What does this command do?
- aws ecr create-repository tells the AWS CLI to create a new private container registry.
- --repository-name devops-api names the repository so it matches your project.
- --region us-east-1 puts the repository in the US East (N. Virginia) region, which matches the region you configured earlier.
- You should see JSON output that includes a repositoryUri field. It looks like 123456789012.dkr.ecr.us-east-1.amazonaws.com/devops-api (with your own AWS account ID instead of 123456789012).
- Copy the repositoryUri value and save it somewhere (a sticky note, a text file, or your clipboard). You will need it in the next two substeps.
Seeing an error?
- If you see RepositoryAlreadyExistsException, you already created this repository. That is fine. Run aws ecr describe-repositories --repository-names devops-api --region us-east-1 to get your URI.
- If you see an access denied error, double check that aws configure is set to the correct access key and region from Step 1.
Help me fix my ECR create-repository error
Authenticate Docker to ECR
AWS does not allow anonymous pushes to your registry. You need to authenticate your local Docker client with ECR before you can push images.
- Log Docker into your ECR registry by running this command:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com
What does this command do?
- aws ecr get-login-password --region us-east-1 retrieves a temporary authentication token from AWS.
- The | pipe sends that token directly into the next command.
- docker login --username AWS --password-stdin authenticates Docker using the token, without you having to copy and paste it manually.
The token is valid for 12 hours. After that, you would need to run this command again.
- You should see Login Succeeded in your terminal output.
Login failed?
- Make sure Docker Desktop is running. The Docker daemon needs to be active for the login command to work.
- Check that you replaced the account ID placeholder with your actual 12-digit AWS account ID. You can find it by running aws sts get-caller-identity.
Help me fix my ECR Docker login error
Tag and push the image to ECR
Docker needs to know where to send your image. You do this by tagging the local image with the full ECR repository URI.
- Tag your local devops-api image with the ECR URI by running this command:
docker tag devops-api:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/devops-api:latest
What does tagging do?
Tagging does not create a copy of the image. It gives the same image a second name that includes the destination address. Think of it as writing a shipping label on a package.
- Push the tagged image to ECR by running:
docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/devops-api:latest
- You should see progress bars as each layer of the image uploads. When it finishes, you will see a digest and a latest: digest: sha256:... confirmation line.
Push denied or unauthorized?
- Run the ECR login command from the previous substep again. Your token may have expired if some time passed.
- Check that the account ID in your tag command matches the one in your login command exactly.
Help me push my Docker image to ECR
- Verify the image is in ECR by opening the AWS Console in your browser.
- Search for ECR in the top search bar and click Elastic Container Registry.
- Click Repositories in the left sidebar.
- Click devops-api. You should see one image tagged latest in the list.
What is ECR free tier?
New AWS customers get 500 MB of private repository storage free for one year. Your image is well under this limit, so there is no cost.
Your container image is now stored in the cloud. Next up, you will create a GitHub Actions pipeline that builds, tests, and pushes images to this registry automatically on every code change.
Build the GitHub Actions CI/CD Pipeline
You have a containerized application and a private container registry in AWS. The missing piece is automation. Right now, every time you change your code, you would have to manually run tests, rebuild the Docker image, and push it to ECR.
A CI/CD pipeline eliminates that manual work. In this step, you will create a GitHub Actions workflow that automatically tests your code on every pull request and builds and pushes your Docker image to ECR on every merge to main.
In this step, get ready to:
- Create a public GitHub repository and push your project code.
- Write a CI/CD workflow file that runs tests and builds your Docker image.
- Add AWS credentials as GitHub Secrets so the pipeline can authenticate securely.
Create the GitHub repository and push your code
Your code needs to live on GitHub for the pipeline to access it. You will create a new public repository in your GitHub organization and push all your project files to it.
- Open GitHub in your browser and navigate to your organization's page.
- Click the green New button (or click + in the upper-right corner and select New repository).
- In the Owner dropdown, select your organization (not your personal account).
- Type devops-api as the repository name.
- Select Public for the repository visibility.
- Leave all other options unchecked (no README, no .gitignore, no license).
- Click Create repository.
Why leave everything unchecked?
You already have files locally. If GitHub creates a README or .gitignore, it creates a commit on the remote that conflicts with your local commit. Keeping the repo empty avoids merge conflicts when you push.
GitHub shows a Quick Setup page with instructions. You will now use those commands in your terminal to push your local project.
- In your terminal, make sure you are inside the devops-api folder. Initialize git and push your code by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/[[GH_ORG="your-organization-name"]]/devops-api.git
git push -u origin main
What do these commands do?
- git init turns the folder into a Git repository.
- git add . stages all your files for commit.
- git commit saves a snapshot of all your files with a message.
- git branch -M main ensures your default branch is named main (matching GitHub's default).
- git remote add origin connects your local repo to the GitHub repo you just created.
- git push -u origin main uploads all your code to GitHub.
- Refresh your repository page on GitHub. You should see all your project files listed: app.py, requirements.txt, Dockerfile, the tests/ folder, and .dockerignore.
Push was rejected or authentication failed?
- If you see a permission error, make sure you selected your organization as the owner when creating the repo and that you have write access to the organization.
- If Git asks for credentials, GitHub requires a personal access token (not your password) for HTTPS authentication. Generate one at Settings > Developer settings > Personal access tokens on GitHub.
- If the remote URL is wrong, fix it by running git remote set-url origin followed by the correct URL from your GitHub repo page.
Why can't I push to my GitHub repository?
Create the workflow file
GitHub Actions workflows live in a special folder called .github/workflows/. Any YAML file in this folder becomes a pipeline that GitHub runs automatically based on the triggers you define.
- Create the workflow folder structure by running this command:
mkdir -p .github/workflows
- Create a new file called ci-cd.yml inside .github/workflows/ and paste the following content:
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: devops-api
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest tests/ -v
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
What does this workflow do?
This file defines two jobs that run on GitHub's servers (not your computer):
- test runs on every push and every pull request to main. It checks out the code, installs Python 3.12, installs dependencies, and runs pytest. If any test fails, the pipeline stops.
- build-and-push only runs when code is merged to main (not on PRs). It authenticates with AWS, logs into ECR, builds the Docker image, and pushes it with two tags: the commit SHA and latest.
The needs: test line means build-and-push only runs after the test job passes. This prevents broken code from ever reaching your container registry.
- Save .github/workflows/ci-cd.yml.
- Confirm the file exists by running:
ls .github/workflows/
You should see ci-cd.yml in the output.
Don't see the file?
- Make sure you created the folder with mkdir -p .github/workflows (note the dot before github). Folders starting with a dot are hidden by default in most file browsers.
- Check that you saved the file inside the workflows folder, not the .github folder directly.
Why isn't my workflow file showing up?
✔️ Awesome, I've got everything!
Great. Make sure you have saved .github/workflows/ci-cd.yml before moving on.
ⓧ I'd like to double check the full code
Here is the complete ci-cd.yml file for comparison:
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: devops-api
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest tests/ -v
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
Add AWS credentials as GitHub Secrets
Your workflow references secrets.AWS_ACCESS_KEY_ID and secrets.AWS_SECRET_ACCESS_KEY. GitHub Secrets store sensitive values encrypted so your pipeline can use them without hardcoding credentials in your code.
- On GitHub, navigate to your devops-api repository page.
- Click Settings in the top navigation bar of the repository.
- In the left sidebar under the Security section, click Secrets and variables, then click Actions.
- Click the Secrets tab.
- Click New repository secret.
- In the Name field, type AWS_ACCESS_KEY_ID.
- In the Secret field, paste your AWS access key ID (the same one you used with aws configure).
- Click Add secret.
Now repeat this process for the second and third secrets:
- Click New repository secret again.
- Name: AWS_SECRET_ACCESS_KEY. Value: your AWS secret access key.
- Click Add secret.
- Click New repository secret one more time.
- Name: AWS_REGION. Value: us-east-1.
- Click Add secret.
Why use GitHub Secrets instead of putting credentials in the YAML?
GitHub encrypts secrets and never exposes them in logs. If you put your AWS keys directly in the workflow file, anyone who can see your public repository would have access to your AWS account.
Secrets are injected at runtime only. The ${{ secrets.AWS_ACCESS_KEY_ID }} syntax tells GitHub to decrypt the value and pass it to the step securely.
- Verify your secrets are configured. You should see three secrets listed on the Actions secrets page: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION.
Don't see the Settings tab?
- Only repository owners and admins can access Settings. Make sure you created the repository under your organization (where you are the owner).
- If the tab is hidden, click the overflow menu (three dots) at the end of the repository navigation bar and look for Settings there.
Why can't I see repository Settings on GitHub?
Now push the workflow file to GitHub so the pipeline is ready to trigger.
- Commit and push the workflow file by running these commands:
git add .
git commit -m "Add CI/CD pipeline workflow"
git push
- On GitHub, click the Actions tab in your repository. You should see a workflow run triggered by your push. The test job will run and pass.
Why doesn't the build-and-push job run?
The workflow file has a condition: if: github.event_name == 'push' && github.ref == 'refs/heads/main'. Since this is a push to main, both jobs will run. If the build-and-push job fails with a credentials error, that means your secrets are not configured correctly.
Seeing a workflow failure?
- If the test job fails, click on the failed job to see the error log. Common cause: a typo in ci-cd.yml YAML indentation (YAML is very sensitive to spaces).
- If the build-and-push job fails with a credentials error, double-check that your three secrets are named exactly AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION.
- If you see a YAML parsing error, make sure your file uses spaces (not tabs) for indentation. Each level is 2 spaces.
Why is my GitHub Actions workflow failing?
Your CI/CD pipeline is live. Every push to main now triggers automatic testing, building, and pushing. Next up, you will add AI-powered code review so every pull request gets security and quality feedback before it merges.
Install Amazon Q Developer for AI Code Review
Your CI/CD pipeline can now build, test, and push containers automatically. But what catches security vulnerabilities or quality issues before code reaches production?
In this step, you will install Amazon Q Developer, an AI-powered code reviewer that automatically scans every pull request for security and quality problems. You will also define custom rules that tell the AI exactly what standards to enforce.
In this step, get ready to:
- Install the Amazon Q Developer GitHub App on your organization.
- Create custom security review rules.
- Push the rules to GitHub and verify the installation.
Install the Amazon Q Developer GitHub App
Amazon Q Developer is a free AI tool from AWS that reviews your code automatically whenever you open a pull request. It posts findings about security vulnerabilities, quality issues, and best-practice violations directly on the PR.
- Open your browser and navigate to the Amazon Q Developer app page:
https://github.com/apps/amazon-q-developer
- Click Install.
- Select your GitHub organization (the one you created in Step 1 that contains your devops-api repository).
- Under repository access, select Only select repositories.
- Choose the Select repositories dropdown and select devops-api.
- Click Install to complete the installation.
After installation, GitHub redirects you to the Amazon Q Developer overview page. You do not need to connect an AWS account for this feature to work.
Why Amazon Q Developer over other AI review tools?
Amazon Q Developer is completely free for GitHub, requires no API keys or paid subscriptions, and integrates natively with the AWS ecosystem you are already using in this project.
Other AI review tools like CodeRabbit or GPT-based reviewers require API keys with associated costs. Amazon Q Developer gives you enterprise-grade code review at zero cost.
Don't see your organization listed?
Make sure you are signed into the GitHub account that owns the organization you created in Step 1. If you have multiple GitHub accounts, sign out and sign back in with the correct one.
help me install the Amazon Q Developer GitHub App on my organization
Create custom review rules
Amazon Q Developer can enforce your team's coding standards automatically. You define these standards as Markdown files inside a special folder, and the AI includes them in every review.
- Create the .amazonq/rules/ folder inside your devops-api project folder by running these commands:
mkdir -p .amazonq/rules
- Create the security-rules.md file inside .amazonq/rules/ and paste the following content:
# Security Standards
## Rules
- Do not hardcode secrets, API keys, or credentials in source code
- All API endpoints must validate input before processing
- Use environment variables for configuration that changes between environments
- Docker containers must run as non-root users
- Dependencies must be pinned to specific versions in requirements.txt
- All endpoints must return proper HTTP status codes
- Sensitive data must not appear in log output
What do these rules do?
Each rule in this file becomes a standard that Amazon Q Developer enforces on every pull request. For example, if someone tries to hardcode an API key, the AI will flag it and suggest using an environment variable instead.
You can add, remove, or modify rules at any time. The AI picks them up automatically on the next PR review.
- Confirm the file was created by running:
ls .amazonq/rules/
You should see security-rules.md in the output.
Seeing 'No such file or directory'?
Make sure you are in the devops-api folder when you run the commands. Run pwd to check your current location. If you are somewhere else, navigate back with cd ~/Desktop/devops-api.
help me create the .amazonq/rules directory in the right location
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete content of .amazonq/rules/security-rules.md:
# Security Standards
## Rules
- Do not hardcode secrets, API keys, or credentials in source code
- All API endpoints must validate input before processing
- Use environment variables for configuration that changes between environments
- Docker containers must run as non-root users
- Dependencies must be pinned to specific versions in requirements.txt
- All endpoints must return proper HTTP status codes
- Sensitive data must not appear in log output
Push the new files and verify the installation
- Stage, commit, and push the new rules folder by running these commands:
git add .amazonq/
git commit -m "Add Amazon Q Developer security rules"
git push origin main
What does this push do?
Pushing the .amazonq/rules/ folder to your repository makes the rules available to Amazon Q Developer. The next time a PR is opened, the AI will read these rules and enforce them in its review.
- Verify the Amazon Q Developer app is installed by going to your GitHub organization's page.
- Click Settings in the top navigation bar.
- In the left sidebar, click GitHub Apps (under Third-party Access or Integrations, depending on your org layout).
- You should see Amazon Q Developer listed with access to your devops-api repository.
Don't see the app in your settings?
Make sure you are viewing your organization's settings, not your personal account settings. Click your profile picture in the top right, select Your organizations, then click Settings next to your organization name.
help me find the Amazon Q Developer app in my GitHub organization settings
Your AI code reviewer is now watching your repository. In the next step, you will open a real pull request and watch Amazon Q Developer review your code live.
Test the Full Pipeline with a PR
In the previous step, you installed Amazon Q Developer and created custom security rules for your project. Now it is time to put everything together and watch the full automation in action.
You will create a feature branch, make a small code change, and open a pull request. This single PR will trigger two automations at once: an AI-powered code review from Amazon Q Developer and a full CI/CD pipeline run from GitHub Actions.
In this step, get ready to:
- Create a feature branch and add a new API endpoint.
- Open a pull request and receive an AI code review.
- Merge the PR and watch the pipeline build, test, and push your container to ECR.
Create a feature branch and add the /info endpoint
In a real DevOps workflow, developers never push directly to the main branch. They create a feature branch, make changes there, and open a pull request for review.
- In your terminal, make sure you are inside the devops-api folder.
- Create a new feature branch by running this command:
git checkout -b add-info-endpoint
What is a feature branch?
A feature branch is a copy of your code where you can make changes without affecting the main branch. When you are happy with the changes, you open a pull request to merge them back into main.
Now you will add a new endpoint to your API. You do not need to understand the Python code. You are copying it exactly as a DevOps engineer would receive it from a developer.
- Open app.py in your code editor.
- Find this line near the bottom of the file:
if __name__ == "__main__":
- Add the following code directly above that line, with a blank line separating them:
@app.route("/info")
def info():
return jsonify({
"app": "devops-api",
"version": "1.1.0",
"environment": "production"
})
What does this code do?
This adds a new /info endpoint that returns basic information about the app: its name, version number, and environment. It follows the same pattern as the existing /health endpoint.
- Save app.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
from flask import Flask, jsonify
import platform
import datetime
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Welcome to the DevOps API",
"status": "running"
})
@app.route("/health")
def health():
return jsonify({
"status": "healthy",
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"system": platform.system(),
"python_version": platform.python_version()
})
@app.route("/info")
def info():
return jsonify({
"app": "devops-api",
"version": "1.1.0",
"environment": "production"
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Not sure where to paste the code?
The /info endpoint goes after the health() function and before the if __name__ line at the very bottom. Make sure you have a blank line between the new function and the if __name__ line.
help me place the /info endpoint in the right location in app.py
Push and open a pull request
With the code change saved, you will now commit it to your feature branch and push it to GitHub. Opening a pull request is what triggers Amazon Q Developer's automatic review.
- Stage, commit, and push your changes by running these commands:
git add .
git commit -m "Add info endpoint"
git push origin add-info-endpoint
What do these commands do?
- git add . stages all changed files for the next commit.
- git commit -m "Add info endpoint" saves the changes with a descriptive message.
- git push origin add-info-endpoint uploads the branch to your GitHub repository.
GitHub will detect the new branch and offer to create a pull request.
- Open your devops-api repository on GitHub in your browser.
- You should see a yellow banner at the top saying your branch had recent pushes. Click Compare & pull request.
- Leave the title and description as-is. Click Create pull request.
Within a minute or two, Amazon Q Developer will automatically post a review comment on your pull request with findings about security and code quality.
What triggers the AI review?
Amazon Q Developer automatically performs a code review when a new PR is opened or a closed PR is reopened. It checks your code against general best practices and your custom rules in the .amazonq/rules/ folder.
If you want to trigger another review later (for example, after pushing additional commits), type /q review as a new top-level comment on the PR.
- Wait 1-2 minutes, then refresh the pull request page.
- You should see a review comment from Amazon Q Developer with findings about your code.
Don't see the review after a few minutes?
Check that the Amazon Q Developer app is still installed on your organization. Go to your organization's Settings > GitHub Apps and confirm it has access to the devops-api repository.
You can also try commenting /q review on the PR to manually trigger a review. You need Write, Maintain, or Admin permissions on the repository to initiate reviews.
help me troubleshoot why Amazon Q Developer is not reviewing my pull request
Merge and watch the full pipeline run
The AI review is one half of your automation. Now you will trigger the other half: the build-and-push pipeline that fires when code is merged to main.
- On your pull request page, scroll down and click Merge pull request.
- Click Confirm merge.
- Click the Actions tab at the top of your repository.
You should see a new workflow run triggered by the merge. Click on it to watch the progress in real time.
What happens during the pipeline run?
Your ci-cd.yml workflow defines two jobs that run in sequence:
- test installs your dependencies and runs pytest against your test suite. If any test fails, the pipeline stops here.
- build-and-push builds the Docker image, tags it with the commit SHA and latest, and pushes both tags to ECR.
- Wait for both jobs to show green checkmarks. This usually takes 2-3 minutes.
Once both jobs are green, your CI/CD pipeline is fully operational. Every future merge to main will automatically test your code and push a fresh container to ECR.
Seeing a red X on the build-and-push job?
The most common cause is incorrect AWS credentials in your GitHub Secrets. Go to your repository's Settings > Secrets and variables > Actions and verify that AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION are all set correctly.
Click on the failed job to expand the error logs. The error message will tell you which step failed and why.
help me debug why my GitHub Actions build-and-push job is failing
Secret mission
Deploy to EC2
Your pipeline builds, tests, and pushes images to ECR. But the container is just sitting in a registry. In this secret mission, you will launch a live EC2 instance and extend your GitHub Actions workflow to automatically deploy the container every time you merge to main. Your API will be accessible on the public internet.
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 ECR free tier and free GitHub services, so there are no ongoing costs as long as your stored images stay under 500 MB.
Resources you used:
- AWS ECR repository (devops-api).
- GitHub repository (devops-api in your organization).
- Amazon Q Developer GitHub App installation.
- Local project folder (devops-api).
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to extend it with the Secret Mission.
- Your ECR repository stores images for free up to 500 MB per month for one year. Your image is well under this limit.
- Your GitHub repository is public and free with unlimited Actions minutes.
- Amazon Q Developer is free and stays installed on your organization.
- The AWS CLI remains installed on your machine.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your files and cloud resources stay intact for when you return.
- Make sure no Docker containers are still running. Stop any with docker stop $(docker ps -q).
- Quit Docker Desktop if you are not using it for other projects.
- Your ECR repository, GitHub repo, and Amazon Q Developer installation all remain active at no cost.
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 the ECR repository by running this command in your terminal:
- Delete the GitHub repository by navigating to your devops-api repository on GitHub, clicking Settings, scrolling to the Danger Zone section, and clicking Delete this repository. Confirm by typing the repository name.
- Uninstall Amazon Q Developer from your GitHub organization. Click your profile picture, then Your organizations. Click Settings next to your organization. In the sidebar under Third-party Access, click GitHub Apps. Click Configure next to Amazon Q Developer, then click Uninstall.
- Delete your local project folder.
aws ecr delete-repository --repository-name devops-api --force
Delete the local folder by running the appropriate command for your operating system:
macOS
rm -rf ~/Desktop/devops-api
Windows
rmdir /s /q %USERPROFILE%\Desktop\devops-api
Remove the Docker image from your machine by running:
docker rmi devops-api
docker image prune -f
Nice Work!
Nice Work!
You just built a professional-grade CI/CD pipeline with AI-powered code review. You did not write a single line of application code. You built the system that deploys code. That is DevOps.
You've learned how to:
- Containerized an application with Docker and pushed the image to AWS ECR as your private container registry.
- Created a CI/CD pipeline with GitHub Actions that automatically builds, tests, and deploys on every merge to main.
- Integrated Amazon Q Developer for AI-powered security scanning and code review on every pull request.
- Secret Mission: Deployed your container to a live EC2 instance with automated deployments from your pipeline.
Ready to quiz yourself?