Secure CI/CD on Amazon EKS
Build a secure GitHub Actions pipeline for observable Amazon EKS releases.
Introduction
30 Second Summary
A software update can look successful while users are already feeling the first signs of trouble. Without a clear trail, finding the exact version behind the problem becomes guesswork.
In this project, you will build ReleaseGuard as a version-reporting health API with a secure CI/CD pipeline. Each commit becomes an immutable container image on Amazon EKS.
What You'll Build
A push to main becomes a live release whose API reports the exact commit running on Amazon EKS.
By the end of this project, you'll have:
- Run ReleaseGuard locally as a non-root Docker container. Query http://127.0.0.1:8080 to see its version, readiness state, and uptime.
- Automate each release with GitHub Actions plus OIDC-issued temporary AWS credentials. See the triggering commit become an immutable image in Amazon ECR before it rolls across two Kubernetes replicas.
- Investigate live health in Amazon CloudWatch. Use OTel Container Insights metrics plus container logs to connect rollout behavior to the Pods serving traffic.
- Secret Mission: Force a release to fail readiness. Prove the stable revision stays available. Roll back safely.
Are there any prerequisites?
You need a Linux workstation with Git, Visual Studio Code, Docker, and GitHub access plus an AWS account with broad cluster-creation permissions. This project creates paid AWS resources, so the cleanup section gives you explicit pause and delete paths.
Before We Start
Before the hands-on work begins, this is your moment to commit to what ReleaseGuard proves. You are building evidence that code can become a secure, observable, recoverable Kubernetes release.
Set Up the Linux and AWS Toolchain
ReleaseGuard starts on your Linux workstation. Its delivery path later reaches GitHub plus Amazon EKS.
Your workstation needs the AWS command-line tools before it can create the cloud environment. This step also gives every later project file a predictable home.
In this step, get ready to:
- Create the public repository structure.
- Verify the complete local toolchain.
- Configure the AWS identity used for cluster creation.
Create and clone the repository
A public repository gives ReleaseGuard a remote home for its source code. It also provides the repository metadata needed by the secure delivery pipeline later.
- Open the prefilled new repository form in your browser.
- Confirm that releaseguard appears in the Repository name field.
- Choose Public as the repository visibility.
- Leave the initialization options unset.
- Click Create repository.
- Click Code above the file list.
- Select HTTPS.
- Copy the repository URL.
- Record the copied URL here: https://github.com/your-username/releaseguard.git.
- Move your Linux terminal to the Desktop by running this command:
cd ~/Desktop
What does this command do?
This makes the Desktop your terminal's current location. The cloned releaseguard folder stays easy to find.
- Clone your public repository by running this command:
git clone [[YOUR_REPO_URL="https://github.com/your-username/releaseguard.git"]]
What does this command do?
The git clone command creates a local copy of the remote repository. Git also connects the copy to its GitHub origin.
Repository clone failed?
- Confirm that the repository URL ends with releaseguard.git.
- Confirm that the repository is public on GitHub.
- Retry the command after checking your internet connection.
Help me diagnose why my public GitHub repository will not clone.
- Enter the cloned releaseguard folder by running this command:
cd releaseguard
Where are you now?
Your terminal now works inside the local ReleaseGuard repository. Every project directory you create next lands inside this repository.
- Create the project directories by running this command:
mkdir -p k8s iam .github/workflows
What do these directories hold?
- The k8s directory holds the Kubernetes manifest.
- The iam directory holds the AWS identity policies.
- The .github/workflows directory holds the deployment workflow.
- Open the repository in VS Code by running this command:
code .
What should open?
VS Code opens the current releaseguard folder as a workspace. The Explorer sidebar becomes your map of the project.
VS Code did not open?
- Confirm that VS Code is installed on your Linux workstation.
- Confirm that the code command is available on your PATH.
- Open the releaseguard folder through the VS Code folder menu if the command remains unavailable.
Help me make the VS Code command available on my Linux PATH.
- Expand .github in the VS Code Explorer sidebar.
You should see k8s plus iam at the top level. You should also see workflows inside .github.
Verify and install the command-line tools
The local toolchain divides the cloud work across several commands. Docker handles containers while Kubernetes tools manage the cluster.
Why use official downloads?
Linux package repositories can lag behind current releases. Official installers let this project use versions compatible with Amazon EKS 1.36.
- Detect your Linux CPU architecture by running this command:
uname -m
What does this result mean?
An x86_64 result maps to the amd64 downloads. An aarch64 result maps to the arm64 downloads.
- Record the displayed architecture here: x86_64 or aarch64.
- Check the current tool versions by running these commands:
docker version
aws --version
kubectl version --client
eksctl version
What are these checks proving?
- The Docker check confirms that the container engine can answer from both its client and server.
- The AWS CLI check confirms whether major version 2 is available.
- The kubectl check confirms whether client version 1.36.4 is available.
- The eksctl check confirms whether version 0.230.0 is available.
Docker cannot reach its engine?
Start the Docker engine installed on your Linux workstation. Run the version check again after the engine is active.
Help me diagnose why Docker cannot reach its engine on Linux.
✔️ All three versions match
Your AWS CLI reports major version 2. Your kubectl client reports 1.36.4.
Your eksctl output reports 0.230.0. Your command-line tools are ready.
ⓧ I see a different version
A different version can introduce incompatible commands or cluster behavior. Reinstall the pinned toolchain to make your results consistent.
- Install the current AWS CLI v2 release by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
What does this installer do?
This downloads the official AWS CLI v2 installer. The script installs the command for your current Linux user.
AWS CLI installation failed?
- Confirm that your workstation can reach the AWS download site.
- Open a fresh terminal after the installer completes.
Help me troubleshoot the official AWS CLI v2 installer on Linux.
The remaining installs use elevated permissions for /usr/local/bin. Your terminal may request your Linux account password.
x86-64 Linux
- Install kubectl 1.36.4 for amd64 by running these commands:
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
How is kubectl verified?
The checksum comparison proves that the downloaded binary matches the published file. The final command installs the verified binary on your PATH.
kubectl checksum failed?
- Delete the downloaded kubectl files.
- Download both files again from the matching amd64 URLs.
- Stop before installation until the checksum reports success.
Help me troubleshoot a kubectl checksum failure on amd64 Linux.
- Install eksctl 0.230.0 for amd64 by running these commands:
ARCH=amd64
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_$PLATFORM.tar.gz"
curl -sL "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
sudo install -m 0755 /tmp/eksctl /usr/local/bin && rm /tmp/eksctl
How is eksctl installed?
The platform value selects the official Linux amd64 archive. The checksum gate protects the install before the binary moves onto your PATH.
eksctl installation failed?
- Confirm that the checksum line names Linux_amd64.
- Confirm that your Linux account can use elevated permissions.
Help me troubleshoot the eksctl 0.230.0 installation on amd64 Linux.
ARM64 Linux
- Install kubectl 1.36.4 for arm64 by running these commands:
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/arm64/kubectl
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/arm64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
How is kubectl verified?
The checksum comparison proves that the downloaded binary matches the published file. The final command installs the verified binary on your PATH.
kubectl checksum failed?
- Delete the downloaded kubectl files.
- Download both files again from the matching arm64 URLs.
- Stop before installation until the checksum reports success.
Help me troubleshoot a kubectl checksum failure on ARM64 Linux.
- Install eksctl 0.230.0 for arm64 by running these commands:
ARCH=arm64
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_$PLATFORM.tar.gz"
curl -sL "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
sudo install -m 0755 /tmp/eksctl /usr/local/bin && rm /tmp/eksctl
How is eksctl installed?
The platform value selects the official Linux arm64 archive. The checksum gate protects the install before the binary moves onto your PATH.
eksctl installation failed?
- Confirm that the checksum line names Linux_arm64.
- Confirm that your Linux account can use elevated permissions.
Help me troubleshoot the eksctl 0.230.0 installation on ARM64 Linux.
- Confirm the installed versions by running these commands:
aws --version
kubectl version --client
eksctl version
What should the checks confirm?
AWS CLI should report major version 2. kubectl should report 1.36.4.
eksctl should report 0.230.0. These versions align with the cluster workflow used in this project.
A command still uses the old version?
Open a fresh terminal so Linux reloads your PATH. Check whether another copy of the command appears earlier on that PATH.
Help me find why Linux is still running an older tool version.
ⓧ One or more commands are missing
One or more required commands are absent from your PATH. Install the complete pinned toolchain before continuing.
- Install AWS CLI v2 by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
What does this installer do?
This downloads the official AWS CLI v2 installer. The script installs the command for your current Linux user.
AWS CLI installation failed?
- Confirm that your workstation can reach the AWS download site.
- Open a fresh terminal after the installer completes.
Help me troubleshoot the official AWS CLI v2 installer on Linux.
The remaining installs use elevated permissions for /usr/local/bin. Your terminal may request your Linux account password.
x86-64 Linux
- Install kubectl 1.36.4 for amd64 by running these commands:
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
How is kubectl verified?
The checksum comparison proves that the downloaded binary matches the published file. The final command installs the verified binary on your PATH.
kubectl checksum failed?
- Delete the downloaded kubectl files.
- Download both files again from the matching amd64 URLs.
- Stop before installation until the checksum reports success.
Help me troubleshoot a kubectl checksum failure on amd64 Linux.
- Install eksctl 0.230.0 for amd64 by running these commands:
ARCH=amd64
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_$PLATFORM.tar.gz"
curl -sL "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
sudo install -m 0755 /tmp/eksctl /usr/local/bin && rm /tmp/eksctl
How is eksctl installed?
The platform value selects the official Linux amd64 archive. The checksum gate protects the install before the binary moves onto your PATH.
eksctl installation failed?
- Confirm that the checksum line names Linux_amd64.
- Confirm that your Linux account can use elevated permissions.
Help me troubleshoot the eksctl 0.230.0 installation on amd64 Linux.
ARM64 Linux
- Install kubectl 1.36.4 for arm64 by running these commands:
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/arm64/kubectl
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/arm64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
How is kubectl verified?
The checksum comparison proves that the downloaded binary matches the published file. The final command installs the verified binary on your PATH.
kubectl checksum failed?
- Delete the downloaded kubectl files.
- Download both files again from the matching arm64 URLs.
- Stop before installation until the checksum reports success.
Help me troubleshoot a kubectl checksum failure on ARM64 Linux.
- Install eksctl 0.230.0 for arm64 by running these commands:
ARCH=arm64
PLATFORM=$(uname -s)_$ARCH
curl -sLO "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_$PLATFORM.tar.gz"
curl -sL "https://github.com/eksctl-io/eksctl/releases/download/v0.230.0/eksctl_checksums.txt" | grep $PLATFORM | sha256sum --check
tar -xzf eksctl_$PLATFORM.tar.gz -C /tmp && rm eksctl_$PLATFORM.tar.gz
sudo install -m 0755 /tmp/eksctl /usr/local/bin && rm /tmp/eksctl
How is eksctl installed?
The platform value selects the official Linux arm64 archive. The checksum gate protects the install before the binary moves onto your PATH.
eksctl installation failed?
- Confirm that the checksum line names Linux_arm64.
- Confirm that your Linux account can use elevated permissions.
Help me troubleshoot the eksctl 0.230.0 installation on ARM64 Linux.
- Confirm the installed versions by running these commands:
aws --version
kubectl version --client
eksctl version
What should the checks confirm?
AWS CLI should report major version 2. kubectl should report 1.36.4.
eksctl should report 0.230.0. These versions align with the cluster workflow used in this project.
A command is still missing?
Open a fresh terminal so Linux reloads your PATH. Confirm that /usr/local/bin is available on that PATH.
Help me find why an installed command is missing from my Linux PATH.
Configure and verify AWS access
The AWS CLI needs a local profile before it can call AWS services. That profile decides which identity signs every command.
Credentials deserve extra care. Enter them only in the terminal prompts provided by the AWS CLI.
- Prepare the AWS access key ID approved for your account.
- Prepare its matching AWS secret access key.
- Use us-east-1 for the default Region prompt.
- Use json for the default output format prompt.
- Configure the default AWS CLI profile by running this command:
aws configure
What does AWS CLI store?
The wizard asks for your access key ID plus your secret access key. It stores those credentials in ~/.aws/credentials.
The Region plus output settings are stored in ~/.aws/config. Keep both files out of the ReleaseGuard repository.
- Confirm with your AWS administrator that this identity can create EKS resources.
- Confirm that the same identity can create EC2 resources.
- Confirm that the same identity can create VPC resources.
- Confirm that the same identity can create CloudFormation resources.
- Confirm that the same identity can create IAM resources.
- Confirm that the same identity can create ECR resources.
- Confirm that the same identity can create CloudWatch resources.
- Confirm that the same identity can create EKS add-on resources.
Before you run the final check, do you expect every command to succeed with the identity you configured?
- Verify the complete toolchain plus your AWS identity by running these commands:
docker version
aws --version
kubectl version --client
eksctl version
aws sts get-caller-identity
What proves the setup works?
- Docker prints client plus server information.
- AWS CLI prints a v2 version.
- kubectl prints client version 1.36.4.
- eksctl prints version 0.230.0.
- AWS returns the account plus ARN for your configured identity.
That is the foundation locked in. Your repository is ready while every required command can reach its next responsibility.
AWS identity check failed?
- Run the configuration wizard again if a credential was entered incorrectly.
- Confirm that the configured credentials remain active.
- Confirm that the profile uses us-east-1.
Help me troubleshoot my AWS CLI identity without exposing credentials.
Your Linux workspace can now build locally plus communicate with AWS. Next, you will turn ReleaseGuard into a containerized health API.
Build and Run the ReleaseGuard Container
Your local toolchain is ready. ReleaseGuard now needs a workload you can test before cloud orchestration enters the picture.
Separate liveness and readiness behavior lets Kubernetes distinguish a running process from a pod that can receive traffic. This step proves both signals inside a Docker container before deployment.
In this step, get ready to:
- Create a Python health API with distinct liveness and readiness routes.
- Package ReleaseGuard as a non-root container image.
- Verify the release version through each local endpoint.
Create the health API
ReleaseGuard uses Python's standard-library http.server module to expose a disposable infrastructure test workload. Each route returns a small JSON response that makes the container's state visible.
- Use the new-file icon beside the releaseguard repository folder to create app.py.
- Confirm app.py appears in the file sidebar.
- Add the response helper and server startup code to app.py by pasting this section:
import json
import os
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
STARTED_AT = time.time()
class ReleaseGuardHandler(BaseHTTPRequestHandler):
def send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
print(f"{self.address_string()} - {format % args}", flush=True)
if __name__ == "__main__":
server = ThreadingHTTPServer(("0.0.0.0", 8080), ReleaseGuardHandler)
print("ReleaseGuard listening on port 8080", flush=True)
server.serve_forever()
What does this code do?
- The imports provide JSON encoding plus environment access plus timing plus the HTTP server.
- STARTED_AT records when the application process begins.
- send_json() serializes each payload before returning an HTTP response.
- log_message() prints every request so the container logs show incoming traffic.
- ThreadingHTTPServer listens on port 8080 through every container network interface.
- Save app.py.
- Confirm the saved file contains ReleaseGuard listening on port 8080 near the bottom.
Does the Python structure look broken?
Check that send_json() and log_message() are indented inside ReleaseGuardHandler. Keep the final server block aligned at the left edge.
Ask for help with the file structure:
The request handler turns environment settings into observable health responses. The READY value controls whether the workload can receive traffic.
- In app.py find the log_message() method.
- Insert this request handler directly above log_message():
def do_GET(self):
version = os.getenv("APP_VERSION", "dev")
ready = os.getenv("READY", "true").lower() == "true"
if self.path == "/healthz":
self.send_json(200, {"status": "alive"})
return
if self.path == "/readyz":
status_code = 200 if ready else 503
status = "ready" if ready else "not-ready"
self.send_json(status_code, {"status": status})
return
if self.path == "/":
self.send_json(
200,
{
"service": "releaseguard",
"version": version,
"ready": ready,
"uptime_seconds": round(time.time() - STARTED_AT, 2),
},
)
return
self.send_json(404, {"error": "not-found"})
How do the routes behave?
- APP_VERSION identifies the release inside the running image.
- READY defaults to true so a normal container can receive traffic.
- /healthz reports whether the application process is alive.
- /readyz returns status code 200 when the workload is ready.
- / reports the service name plus release version plus readiness state plus uptime.
- Save app.py.
- Confirm the saved file contains the routes / plus /healthz plus /readyz.
Are the routes outside the handler?
Make sure do_GET() has the same indentation as send_json(). Every route condition belongs inside do_GET().
Ask for help checking the handler:
Package the Docker image
A Dockerfile makes the workload repeatable by pinning its runtime plus startup behavior. A .dockerignore file keeps unrelated repository content out of the build context.
- Use the new-file icon beside the releaseguard repository folder to create Dockerfile.
- Confirm Dockerfile appears without a file extension.
- Define the container image by pasting this code into Dockerfile:
FROM python:3.14.7-slim
WORKDIR /app
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
ENV READY=true
COPY app.py .
USER 10001
EXPOSE 8080
CMD ["python", "app.py"]
What does the Dockerfile control?
- FROM python:3.14.7-slim pins the application to the official slim Python runtime.
- ARG APP_VERSION=dev accepts a release version during the image build.
- ENV READY=true gives each normal container a ready state.
- USER 10001 runs the application without root privileges.
- CMD ["python", "app.py"] starts ReleaseGuard when the container launches.
- Save Dockerfile.
- Confirm the filename remains exactly Dockerfile.
Is Dockerfile showing as plain text?
Check that the filename begins with a capital D. Remove any automatic .txt extension.
Ask for help checking the build file:
- Use the new-file icon beside the releaseguard repository folder to create .dockerignore.
- Confirm .dockerignore appears in the file sidebar.
- Limit the Docker build context by pasting these entries into .dockerignore:
.git
.github
__pycache__
*.pyc
cluster.yaml
iam
k8s
Why exclude these paths?
The image only needs app.py to run ReleaseGuard. Excluding repository metadata plus deployment files keeps the build context focused on the application.
- Save .dockerignore.
- Confirm the file contains seven exclusion patterns.
The first build can take longer while Docker downloads the pinned Python image. Once that layer is local, later builds can reuse it.
Before you build, which release version do you expect the completed image to contain?
- Build the releaseguard:local-v1 image with its visible version by running this command in the existing VS Code terminal:
docker build --build-arg APP_VERSION=local-v1 -t releaseguard:local-v1 .
What does this build command do?
- --build-arg APP_VERSION=local-v1 passes the release identifier into the Dockerfile.
- -t releaseguard:local-v1 assigns a local repository name plus an immutable-looking version tag.
- . sends the current releaseguard folder as the build context.
- Confirm the build returns to the terminal prompt without an error.
- Confirm the final build output references releaseguard:local-v1.
Did the image fail to build?
Make sure the terminal is inside the releaseguard folder. Check that app.py sits beside Dockerfile.
Ask for help diagnosing the build:
Run and inspect the container
The image now contains a release identifier that you can observe at runtime. The container stays attached to its terminal so each incoming request becomes a visible log entry.
- Start ReleaseGuard on the local loopback address by running this command:
docker run --rm -p 127.0.0.1:8080:8080 releaseguard:local-v1
What does this run command do?
- --rm removes the stopped container automatically.
- -p 127.0.0.1:8080:8080 publishes the container port only through your local machine.
- releaseguard:local-v1 selects the image you just built.
- Wait for the terminal to show ReleaseGuard listening on port 8080.
- Leave the container running in this terminal.
Did the container stop immediately?
Compare CMD ["python", "app.py"] with your saved Dockerfile. Check the terminal output for a Python indentation or syntax problem.
Ask for help with the container:
- Use the plus button in the VS Code terminal panel to start a second terminal.
Before you query the API, do you expect all three routes to report a healthy release?
- Query the root plus liveness plus readiness endpoints by running these commands in the second terminal:
curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/healthz
curl -i http://127.0.0.1:8080/readyz
What do these checks reveal?
- -i includes each response header so you can inspect the HTTP status.
- The root route identifies the exact release running inside the container.
- The health route proves that the application process is alive.
- The readiness route proves that the workload can receive traffic.
Each response includes HTTP status 200. The root body reports "version": "local-v1" while the health bodies report alive plus ready.
- Switch back to the first terminal.
- Confirm the container printed one successful request log for each endpoint.
That is the first complete release signal working. Your image exposes its version plus its liveness state plus its readiness state.
✔️ Awesome, I've got everything!
Great work. Double check that app.py plus Dockerfile plus .dockerignore are saved.
ⓧ I'd like to double check the full code
Compare each saved file with the complete versions below.
import json
import os
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
STARTED_AT = time.time()
class ReleaseGuardHandler(BaseHTTPRequestHandler):
def send_json(self, status_code, payload):
body = json.dumps(payload).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
version = os.getenv("APP_VERSION", "dev")
ready = os.getenv("READY", "true").lower() == "true"
if self.path == "/healthz":
self.send_json(200, {"status": "alive"})
return
if self.path == "/readyz":
status_code = 200 if ready else 503
status = "ready" if ready else "not-ready"
self.send_json(status_code, {"status": status})
return
if self.path == "/":
self.send_json(
200,
{
"service": "releaseguard",
"version": version,
"ready": ready,
"uptime_seconds": round(time.time() - STARTED_AT, 2),
},
)
return
self.send_json(404, {"error": "not-found"})
def log_message(self, format, *args):
print(f"{self.address_string()} - {format % args}", flush=True)
if __name__ == "__main__":
server = ThreadingHTTPServer(("0.0.0.0", 8080), ReleaseGuardHandler)
print("ReleaseGuard listening on port 8080", flush=True)
server.serve_forever()
What should match in app.py?
The file defines the response helper plus all three routes plus request logging plus the server on port 8080.
FROM python:3.14.7-slim
WORKDIR /app
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
ENV READY=true
COPY app.py .
USER 10001
EXPOSE 8080
CMD ["python", "app.py"]
What should match in Dockerfile?
The image uses the pinned slim runtime plus numeric user 10001 plus port 8080.
.git
.github
__pycache__
*.pyc
cluster.yaml
iam
k8s
What should match in .dockerignore?
The file excludes repository metadata plus cached Python files plus deployment configuration from the image build context.
- Return to the terminal running the container.
- Press Ctrl+C to stop the local container.
ReleaseGuard is now a versioned health API inside a non-root container. Next up, you will publish it to Amazon EKS for its first managed deployment.
Deploy ReleaseGuard to Amazon EKS
Your Docker image proves that ReleaseGuard works on one Linux machine. Amazon EKS moves that workload into managed Kubernetes infrastructure.
A manual cloud deployment gives you a working release. Building a newer local version then exposes how the cluster can fall behind without delivery automation.
In this step, get ready to:
- Provision an EKS 1.36 cluster with one managed worker node.
- Publish the manual-v1 image to a private Amazon ECR repository.
- Deploy two replicas and expose the version drift with local-v2.
Provision the EKS cluster
An eksctl cluster configuration records the control plane version and worker capacity in one file. Keeping that configuration in cluster.yaml also gives you a repeatable cleanup path.
- Switch back to the releaseguard repository in the VS Code Explorer sidebar.
- Click the New File button in the Explorer toolbar.
- Enter cluster.yaml as the file name.
- Define the cluster by adding this configuration to cluster.yaml:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: releaseguard-eks
region: us-east-1
version: "1.36"
accessConfig:
authenticationMode: API_AND_CONFIG_MAP
managedNodeGroups:
- name: releaseguard-workers
instanceType: t3.large
desiredCapacity: 1
minSize: 1
maxSize: 1
volumeSize: 20
What Does This Configuration Do?
- The metadata section fixes the cluster name and AWS Region. It pins Kubernetes to 1.36.
- The API_AND_CONFIG_MAP authentication mode enables EKS access entries. It also retains compatibility with the cluster configuration map.
- The releaseguard-workers node group creates one t3.large worker with a 20 GiB volume.
- Save cluster.yaml.
- Validate the saved configuration without creating resources by running this command:
eksctl create cluster -f cluster.yaml --dry-run
What Does the Dry Run Prove?
The dry run asks eksctl to parse cluster.yaml without provisioning the cluster. A rendered plan confirms that eksctl understands the configuration.
You should see a rendered cluster plan in the terminal. No cluster resources have started yet.
Does the Dry Run Reject the File?
- Confirm that cluster.yaml is saved inside the releaseguard folder.
- Compare the indentation under metadata with the configuration above.
- Compare the indentation under managedNodeGroups with the configuration above.
Ask for help with the validation output: Help me diagnose why eksctl rejects my cluster.yaml file.
Paid Resources Start Here
Creating this cluster starts billable AWS resources. The EKS standard-support control plane costs $0.10 per cluster-hour.
The worker node and related infrastructure are billed separately. The cleanup section gives you options to pause compute or delete the cluster.
Cluster creation usually takes 15 to 20 minutes. The terminal may pause between updates while AWS builds the control plane and networking resources.
- Create the cluster from the validated configuration by running this command:
eksctl create cluster -f cluster.yaml
What Does Cluster Creation Add?
eksctl creates the EKS control plane and a VPC. It also creates CloudFormation stacks for the cluster and managed node group.
The command updates your local kubeconfig after creation succeeds. That connection lets kubectl send requests to releaseguard-eks.
When the command finishes, the cloud infrastructure is ready. The next check confirms that its worker can accept workloads.
Did Cluster Creation Stop Early?
- Review the final eksctl output for the AWS resource that failed to create.
- Confirm that your configured AWS identity can create EKS resources.
- Confirm that your configured AWS identity can create the supporting infrastructure.
- Check that your AWS account has capacity for one t3.large instance in us-east-1.
Get help interpreting the failed resource: Help me troubleshoot an eksctl cluster creation failure.
Before you check the node, do you expect its status to show that it can accept pods?
- Verify the managed worker node by running this command:
kubectl get nodes
What Does the Node Check Prove?
The command asks the EKS API for every registered worker. A Ready status proves that the managed node joined the cluster.
You should see one node with a Ready status. That worker is the compute layer where ReleaseGuard runs.
✔️ Awesome, I've got everything!
Your releaseguard-eks cluster has one ready worker. Double check that cluster.yaml is saved.
ⓧ I'd like to double check the full code
Compare your saved cluster.yaml with this complete configuration.
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: releaseguard-eks
region: us-east-1
version: "1.36"
accessConfig:
authenticationMode: API_AND_CONFIG_MAP
managedNodeGroups:
- name: releaseguard-workers
instanceType: t3.large
desiredCapacity: 1
minSize: 1
maxSize: 1
volumeSize: 20
Publish manual-v1 to Amazon ECR
Amazon Elastic Container Registry stores private container images close to your cluster. Amazon ECR gives the EKS worker a stable registry location for manual-v1.
- Create the private releaseguard repository in us-east-1 by running this command:
aws ecr create-repository --repository-name releaseguard --region us-east-1
What Does This Command Create?
The command creates a private image repository named releaseguard in the same Region as the cluster. Its repository URI contains your AWS account ID.
You should see repository details in the response. The repository URI ends with /releaseguard.
- Publish manual-v1 to your private registry by running these commands:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="$ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com"
IMAGE_URI="$REGISTRY/releaseguard:manual-v1"
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin "$REGISTRY"
docker build --build-arg APP_VERSION=manual-v1 -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
How Does the Image Reach ECR?
- The first three lines build the fully qualified image URI from your AWS account ID.
- The authentication command gives Docker temporary access to your private registry.
- The build command embeds manual-v1 as the API version.
- The final command uploads the tagged image layers to the releaseguard repository.
You should see Docker authenticate before the image build starts. The push finishes with an image digest for manual-v1.
Did the Image Fail to Publish?
- Confirm that the releaseguard repository exists in us-east-1.
- Run the AWS identity check again if the registry address contains an empty account ID.
- Confirm that Docker is running before repeating the publish commands.
Get help with the failed publish: Help me diagnose why manual-v1 will not push to Amazon ECR.
Deploy the workload and expose version drift
A Deployment keeps the requested number of ReleaseGuard pods running. Its readiness and liveness probes give Kubernetes separate signals for traffic routing and container recovery.
- Create the releaseguard namespace by running this command:
kubectl create namespace releaseguard
Why Use a Namespace?
The namespace gives ReleaseGuard a dedicated boundary inside the cluster. Later permissions can target that boundary without granting access across every namespace.
You should see confirmation that the releaseguard namespace was created.
- Switch back to the VS Code Explorer sidebar.
- Click the New Folder button in the Explorer toolbar.
- Enter k8s as the folder name.
- Select the new k8s folder.
- Click the New File button in the Explorer toolbar.
- Enter releaseguard.yaml as the file name.
- Start the Deployment definition by adding this section:
apiVersion: apps/v1
kind: Deployment
metadata:
name: releaseguard
namespace: releaseguard
spec:
replicas: 2
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: releaseguard
template:
metadata:
labels:
app: releaseguard
spec:
containers:
- name: releaseguard
image: IMAGE_URI_PLACEHOLDER
How Does the Rollout Stay Available?
- The Deployment requests two ReleaseGuard replicas.
- A rolling update with maxUnavailable: 0 keeps the existing ready replicas available during a release.
- The maxSurge: 1 setting permits one additional pod while the new revision starts.
- The image placeholder keeps the saved manifest independent from one AWS account.
- Complete the Deployment by adding this section directly below image: IMAGE_URI_PLACEHOLDER:
ports:
- name: http
containerPort: 8080
env:
- name: READY
value: "true"
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 2
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
How Does Kubernetes Judge Pod Health?
- The READY environment variable starts each container in its ready state.
- The readiness probe checks /readyz before the pod receives Service traffic.
- The liveness probe checks /healthz so Kubernetes can detect a container that needs restarting.
- The resource requests help the scheduler place each replica.
- The resource limits cap each container's CPU and memory use.
- Save k8s/releaseguard.yaml.
- Render the ECR image URI and apply the Deployment by running these commands:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_URI="$ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/releaseguard:manual-v1"
sed "s|IMAGE_URI_PLACEHOLDER|$IMAGE_URI|g" k8s/releaseguard.yaml | kubectl apply -f -
Why Render the Image at Deployment Time?
The sed command replaces the placeholder only in the content sent to Kubernetes. Your saved manifest retains IMAGE_URI_PLACEHOLDER for future deployments.
The live Deployment receives the account-specific manual-v1 image URI. The EKS worker can then pull that private image from ECR.
You should see confirmation that the ReleaseGuard Deployment was created.
Before you watch the rollout, do you expect both replicas to become available?
- Wait for both replicas to pass their health checks by running this command:
kubectl rollout status deployment/releaseguard -n releaseguard --timeout=180s
What Does Rollout Success Prove?
Rollout success means the Deployment reached its desired state before the timeout. Both replicas pulled the image and passed the readiness probe.
You should see confirmation that the Deployment completed its rollout. ReleaseGuard now has two available manual-v1 replicas.
Is the Rollout Still Waiting?
- Confirm that the published image URI ends with releaseguard:manual-v1.
- Check that the worker node still reports Ready.
- Review the ReleaseGuard pods for image-pull failures.
- Review the ReleaseGuard pods for health-probe failures.
Get help tracing the blocked rollout: Help me diagnose why the ReleaseGuard Deployment is not ready.
A Service gives the two replicas one stable internal address. Traffic can reach whichever pod is ready through an internal ClusterIP.
- Add the Service definition to the end of k8s/releaseguard.yaml:
---
apiVersion: v1
kind: Service
metadata:
name: releaseguard
namespace: releaseguard
spec:
type: ClusterIP
selector:
app: releaseguard
ports:
- name: http
port: 80
targetPort: http
How Does the Service Route Requests?
- The ClusterIP type keeps the Service reachable inside the cluster.
- The app: releaseguard selector targets the ready pods created by the Deployment.
- Port 80 forwards traffic to the container port named http.
- Save k8s/releaseguard.yaml.
- Apply the completed manifest by running these commands:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_URI="$ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/releaseguard:manual-v1"
sed "s|IMAGE_URI_PLACEHOLDER|$IMAGE_URI|g" k8s/releaseguard.yaml | kubectl apply -f -
What Changes on the Second Apply?
Kubernetes keeps the existing Deployment aligned with the manifest. It also creates the internal ReleaseGuard Service.
You should see the Deployment remain configured. You should also see confirmation that the Service was created.
Did the Service Fail to Apply?
- Confirm that the separator above the Service is exactly ---.
- Check that the Service selector matches app: releaseguard in the pod template.
- Compare the indentation under ports with the completed manifest below.
Get help with the manifest: Help me diagnose why the ReleaseGuard Service will not apply.
✔️ Awesome, I've got everything!
Your saved manifest defines the two-replica Deployment and its internal Service. Double check that k8s/releaseguard.yaml is saved.
ⓧ I'd like to double check the full code
Compare your saved k8s/releaseguard.yaml with this complete manifest.
apiVersion: apps/v1
kind: Deployment
metadata:
name: releaseguard
namespace: releaseguard
spec:
replicas: 2
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: releaseguard
template:
metadata:
labels:
app: releaseguard
spec:
containers:
- name: releaseguard
image: IMAGE_URI_PLACEHOLDER
ports:
- name: http
containerPort: 8080
env:
- name: READY
value: "true"
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 2
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: releaseguard
namespace: releaseguard
spec:
type: ClusterIP
selector:
app: releaseguard
ports:
- name: http
port: 80
targetPort: http
Port forwarding gives your Linux machine a temporary path to the internal Service. This path avoids creating a public load balancer.
- Connect local port 8080 to the ReleaseGuard Service by running this command:
kubectl port-forward service/releaseguard 8080:80 -n releaseguard
How Does Port Forwarding Work?
kubectl maintains a live connection from your local port to the internal Service. The command keeps control of this terminal while the connection is active.
You should see forwarding confirmation for local port 8080. Keep this terminal running.
- Create a second terminal in the VS Code terminal panel.
- Build the newer local image by running this command:
docker build --build-arg APP_VERSION=local-v2 -t releaseguard:local-v2 .
What Changes in local-v2?
The same application code is rebuilt with APP_VERSION=local-v2. The version gives you a visible comparison with the manually deployed cluster release.
You should see the image build complete with the releaseguard:local-v2 tag.
- Run local-v2 on local port 8081 by running this command:
docker run --rm -p 127.0.0.1:8081:8080 releaseguard:local-v2
Why Use a Second Local Port?
Port 8080 remains connected to the EKS Service. Port 8081 lets the local container run at the same time.
You should see ReleaseGuard report that it is listening on container port 8080. Keep this terminal running.
- Create a third terminal in the VS Code terminal panel.
Before you compare the endpoints, which version do you think each one reports?
- Query the cloud release and local release by running these commands:
curl http://127.0.0.1:8080/
curl http://127.0.0.1:8081/
What Does the Comparison Reveal?
Port 8080 reaches the EKS Deployment. Its response reports manual-v1.
Port 8081 reaches the newer local image. Its response reports local-v2.
That mismatch is the intended shortfall. The cluster remains healthy while its manually published release falls behind the newer local build.
Can One Endpoint Not Respond?
- Confirm that the port-forward terminal still shows an active connection.
- Confirm that the local container terminal is still running releaseguard:local-v2.
- Check that the cloud request uses port 8080.
- Check that the local request uses port 8081.
Get help comparing the endpoints: Help me diagnose why an EKS or local endpoint is not responding.
Your EKS cluster now serves two healthy manual-v1 replicas through an internal Service. Next up, you will turn the visible version drift into an automated release pipeline.
Automate Delivery with GitHub Actions
Your Amazon EKS cluster still serves manual-v1. Your local container already serves local-v2.
This step uses GitHub Actions to close that version drift. OpenID Connect gives each workflow run temporary AWS credentials.
In this step, get ready to:
- Bind an IAM role to your immutable GitHub repository identity.
- Limit the role to one Amazon ECR repository plus one Amazon EKS namespace.
- Push a workflow that deploys every successful main commit.
Bind the workflow to one repository
GitHub includes immutable owner and repository IDs in the workflow identity. AWS Identity and Access Management can use those IDs to reject tokens from a renamed copy of your repository.
The two id fields are easy to mix up. The repository ID sits at the top level of the response.
- Retrieve the public repository metadata by running this command:
curl -L -X GET https://api.github.com/repos/[[YOUR_GITHUB_OWNER="your GitHub username or organisation"]]/releaseguard
What does this request return?
The GitHub REST API returns metadata for your public releaseguard repository. The top-level id identifies the repository.
The nested owner.id identifies the account that owns it. These values remain stable if a display name changes.
- Record the owner from the repository URL as your GitHub username or organisation.
- Record the top-level id value as your releaseguard repository ID.
- Record the nested owner.id value as your GitHub owner ID.
- Reuse the AWS account ID you confirmed earlier as your 12-digit AWS account ID.
Repository metadata missing?
Confirm that the repository is public. Check that the owner in the request matches the owner shown in the repository URL.
Ask for help with the metadata response.
- In the VS Code Explorer sidebar, select the iam folder.
- Create github-oidc-provider.json inside the iam folder.
- Add the OIDC provider configuration to iam/github-oidc-provider.json using this code:
{
"Url": "https://token.actions.githubusercontent.com",
"ClientIDList": [
"sts.amazonaws.com"
]
}
What does this configuration do?
The provider URL identifies GitHub as the workflow token issuer. The client ID restricts the token audience to AWS Security Token Service.
- Save iam/github-oidc-provider.json.
- Confirm that the file shows one provider URL plus one client ID.
JSON highlighted as invalid?
Check the comma after the Url value. Confirm that every key uses double quotes.
Ask for help checking the provider JSON.
- Create github-trust-policy.json inside the iam folder.
- Add the repository-specific trust policy to iam/github-trust-policy.json using this code:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_OWNER@YOUR_GITHUB_OWNER_ID/releaseguard@YOUR_REPOSITORY_ID:ref:refs/heads/main"
}
}
}
]
}
How does the trust policy protect the role?
The federated principal accepts tokens from the GitHub OIDC provider. The audience condition requires a token intended for AWS.
The subject condition binds access to one immutable owner plus one immutable repository. It also limits access to refs/heads/main.
- Save iam/github-trust-policy.json.
- Confirm that the subject ends with :ref:refs/heads/main.
Trust policy subject looks different?
Keep each placeholder exactly as shown. Confirm that the owner ID follows the owner name.
Ask for help checking the immutable subject.
- Create github-permissions.json inside the iam folder.
- Add the least-privilege AWS permissions to iam/github-permissions.json using this code:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GetECRAuthorizationToken",
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Sid": "PushReleaseGuardImage",
"Effect": "Allow",
"Action": [
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:BatchGetImage"
],
"Resource": "arn:aws:ecr:us-east-1:YOUR_AWS_ACCOUNT_ID:repository/releaseguard"
},
{
"Sid": "DescribeReleaseGuardCluster",
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "arn:aws:eks:us-east-1:YOUR_AWS_ACCOUNT_ID:cluster/releaseguard-eks"
}
]
}
Why are these permissions narrow?
The workflow can request an Amazon ECR login token. Its image permissions target only the releaseguard repository.
The Amazon EKS permission targets only releaseguard-eks. Kubernetes deployment rights come from a separate namespace-scoped access entry.
- Save iam/github-permissions.json.
- Confirm that both resource ARNs point to us-east-1.
Permissions file marked as invalid?
Check every comma in the action array. Confirm that the final statement closes before the outer array.
Ask for help checking the permissions JSON.
An AWS account needs only one GitHub OIDC provider. The next check prevents you from trying to create a duplicate.
- Check the existing IAM OIDC providers by running this command:
aws iam list-open-id-connect-providers
What should you inspect?
The response lists provider ARNs in your AWS account. Look for an ARN ending in oidc-provider/token.actions.githubusercontent.com.
✔️ I see the GitHub provider
The shared provider is ready. Leave it unchanged before creating the repository-specific role.
ⓧ I do not see the GitHub provider
- Create the GitHub OIDC provider from your saved configuration by running this command:
aws iam create-open-id-connect-provider --cli-input-json file://iam/github-oidc-provider.json
What does this command create?
IAM registers GitHub's token issuer for this AWS account. The response includes the new provider ARN.
The provider now exists for roles that trust GitHub-issued OIDC tokens.
IAM policy changes can feel high-impact. These templates limit the role to one repository identity plus the ReleaseGuard resources.
- In iam/github-trust-policy.json, replace YOUR_AWS_ACCOUNT_ID with your 12-digit AWS account ID.
- Replace YOUR_GITHUB_OWNER with your GitHub username or organisation.
- Replace YOUR_GITHUB_OWNER_ID with your GitHub owner ID.
- Replace YOUR_REPOSITORY_ID with your releaseguard repository ID.
- In iam/github-permissions.json, replace YOUR_AWS_ACCOUNT_ID with your 12-digit AWS account ID.
- Save both IAM policy files.
- Create the role plus its inline policy by running these commands:
aws iam create-role --role-name releaseguard-github-actions --assume-role-policy-document file://iam/github-trust-policy.json
aws iam put-role-policy --role-name releaseguard-github-actions --policy-name GitHubPermissions --policy-document file://iam/github-permissions.json
What do these commands create?
The first command creates the releaseguard-github-actions role with your repository-specific trust policy. The second command adds the restricted Amazon ECR plus Amazon EKS permissions.
- Retrieve the role ARN by running this command:
aws iam get-role --role-name releaseguard-github-actions --query Role.Arn --output text
Why save the role ARN?
The ARN uniquely identifies the IAM role. Amazon EKS plus GitHub both use this exact value in the next substep.
- Record the command output as arn:aws:iam::123456789012:role/releaseguard-github-actions.
- In iam/github-trust-policy.json, restore your account ID to YOUR_AWS_ACCOUNT_ID.
- Restore your GitHub owner to YOUR_GITHUB_OWNER.
- Restore your GitHub owner ID to YOUR_GITHUB_OWNER_ID.
- Restore your repository ID to YOUR_REPOSITORY_ID.
- In iam/github-permissions.json, restore your account ID to YOUR_AWS_ACCOUNT_ID.
- Save both IAM template files.
Role creation failed?
A duplicate-role error means the role name already exists. Inspect that role before deciding whether it belongs to this project.
A malformed-policy error usually points to an unreplaced placeholder or invalid JSON. Reapply your real values before retrying the role command.
Ask for help with the IAM failure.
Scope deployment access to one namespace
The IAM policy lets the workflow discover the cluster. An Amazon EKS access entry controls what the role can change inside Kubernetes.
Namespace scope contains deployment permissions inside releaseguard. Other namespaces stay outside the workflow's access.
- Create the STANDARD access entry plus its namespace-scoped policy association by running these commands:
aws eks create-access-entry --cluster-name releaseguard-eks --principal-arn [[AWS_ROLE_ARN="arn:aws:iam::123456789012:role/releaseguard-github-actions"]] --type STANDARD
aws eks associate-access-policy --cluster-name releaseguard-eks --principal-arn [[AWS_ROLE_ARN="arn:aws:iam::123456789012:role/releaseguard-github-actions"]] --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicy --access-scope type=namespace,namespaces=releaseguard
How do the two access layers work?
The access entry connects the IAM role to the EKS authorization system. The STANDARD type represents a regular IAM principal.
The AmazonEKSEditPolicy association permits deployment changes. Its access scope names only the releaseguard namespace.
That is the core security boundary in place. The pipeline can release this workload without receiving cluster-wide edit access.
- Return to your public releaseguard repository in GitHub.
- Select Settings in the repository navigation.
- Select Secrets and variables in the sidebar.
- Select Actions.
- Select the Variables tab.
- Click New repository variable.
- Enter AWS_ROLE_ARN in the name field.
- Enter arn:aws:iam::123456789012:role/releaseguard-github-actions in the value field.
Cannot create the repository variable?
Confirm that you have write access to the repository. Check that you selected repository variables instead of repository secrets.
Ask for help locating the variable settings.
Build the immutable deployment workflow
The workflow turns each main commit into a container image tagged with github.sha. That immutable tag connects the running release to one exact commit.
- In the VS Code Explorer sidebar, select the .github/workflows folder.
- Create deploy.yml inside .github/workflows.
- Add the workflow trigger plus shared environment values to .github/workflows/deploy.yml using this code:
name: Build and deploy ReleaseGuard
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: releaseguard
EKS_CLUSTER_NAME: releaseguard-eks
What does this workflow header control?
The trigger runs the workflow for pushes to main. The permissions allow an OIDC token plus read-only repository checkout.
The environment values keep the region plus resource names consistent across later commands.
- Save .github/workflows/deploy.yml.
- Confirm that the file shows id-token: write plus contents: read.
Workflow header indented incorrectly?
Use spaces for YAML indentation. Confirm that id-token plus contents align under permissions.
Ask for help checking the workflow header.
- Add the deployment job below the EKS_CLUSTER_NAME line using this code:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure temporary AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: releaseguard-deploy
aws-region: ${{ env.AWS_REGION }}
- name: Install kubectl 1.36.4
run: |
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
How does the job authenticate?
The runner checks out your repository with actions/checkout@v7.0.1. The credentials action reads the role ARN from AWS_ROLE_ARN.
The credentials action exchanges the GitHub OIDC token for temporary AWS credentials. The runner also installs checksum-verified kubectl 1.36.4 for the EKS 1.36 cluster.
- Save .github/workflows/deploy.yml.
- Confirm that the role input reads from vars.AWS_ROLE_ARN.
Credentials step nested incorrectly?
Confirm that every step starts at the same indentation beneath steps. Keep the with values beneath the credentials action.
Ask for help checking the job structure.
- Add the image build plus deployment steps below the kubectl installation step using this code:
- name: Build and push immutable image
run: |
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
IMAGE_URI="$REGISTRY/$ECR_REPOSITORY:${{ github.sha }}"
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
docker build --build-arg APP_VERSION="${{ github.sha }}" -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
- name: Deploy and wait for readiness
run: |
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_URI="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:${{ github.sha }}"
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$AWS_REGION"
sed "s|IMAGE_URI_PLACEHOLDER|$IMAGE_URI|g" k8s/releaseguard.yaml | kubectl apply -f -
kubectl rollout status deployment/releaseguard -n releaseguard --timeout=180s
How does one commit become a release?
The build step tags the image with github.sha. It also passes that commit SHA into APP_VERSION.
The deploy step replaces IMAGE_URI_PLACEHOLDER while streaming the manifest to kubectl. Rollout status keeps the job running until the new replicas become ready.
- Save .github/workflows/deploy.yml.
- Confirm that both image URI definitions end with ${{ github.sha }}.
Workflow file still looks incomplete?
Confirm that the build step plus deploy step align beneath the earlier kubectl step. Check that the manifest path remains k8s/releaseguard.yaml.
Ask for help checking the complete workflow.
✔️ Awesome, I've got everything!
Your IAM templates plus deployment workflow are saved. The source templates still contain their documented placeholders.
ⓧ I'd like to double check the IAM files
Compare each IAM template with these complete reference files.
{
"Url": "https://token.actions.githubusercontent.com",
"ClientIDList": [
"sts.amazonaws.com"
]
}
Provider reference
This complete file contains the GitHub issuer URL plus the AWS STS audience.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_AWS_ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_OWNER@YOUR_GITHUB_OWNER_ID/releaseguard@YOUR_REPOSITORY_ID:ref:refs/heads/main"
}
}
}
]
}
Trust policy reference
This complete file preserves the account plus immutable GitHub identity placeholders.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GetECRAuthorizationToken",
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Sid": "PushReleaseGuardImage",
"Effect": "Allow",
"Action": [
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:BatchGetImage"
],
"Resource": "arn:aws:ecr:us-east-1:YOUR_AWS_ACCOUNT_ID:repository/releaseguard"
},
{
"Sid": "DescribeReleaseGuardCluster",
"Effect": "Allow",
"Action": "eks:DescribeCluster",
"Resource": "arn:aws:eks:us-east-1:YOUR_AWS_ACCOUNT_ID:cluster/releaseguard-eks"
}
]
}
Permissions reference
This complete file limits image access plus cluster discovery to the ReleaseGuard resources.
ⓧ I'd like to double check the workflow
Compare .github/workflows/deploy.yml with this complete reference file.
name: Build and deploy ReleaseGuard
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: releaseguard
EKS_CLUSTER_NAME: releaseguard-eks
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure temporary AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: releaseguard-deploy
aws-region: ${{ env.AWS_REGION }}
- name: Install kubectl 1.36.4
run: |
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl
curl -LO https://dl.k8s.io/release/v1.36.4/bin/linux/amd64/kubectl.sha256
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
- name: Build and push immutable image
run: |
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGISTRY="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
IMAGE_URI="$REGISTRY/$ECR_REPOSITORY:${{ github.sha }}"
aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
docker build --build-arg APP_VERSION="${{ github.sha }}" -t "$IMAGE_URI" .
docker push "$IMAGE_URI"
- name: Deploy and wait for readiness
run: |
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
IMAGE_URI="$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPOSITORY:${{ github.sha }}"
aws eks update-kubeconfig --name "$EKS_CLUSTER_NAME" --region "$AWS_REGION"
sed "s|IMAGE_URI_PLACEHOLDER|$IMAGE_URI|g" k8s/releaseguard.yaml | kubectl apply -f -
kubectl rollout status deployment/releaseguard -n releaseguard --timeout=180s
Workflow reference
This complete file checks out the repository. It obtains temporary credentials before building the immutable image.
It finishes by applying the rendered Kubernetes manifest. The job succeeds only after the Deployment becomes ready.
Before you push, consider which version the port-forwarded API should report after this workflow succeeds.
- Commit the project files plus push main by running these commands:
git add .
git commit -m "Automate ReleaseGuard delivery"
git push origin main
What does this push trigger?
Git records the IAM templates plus workflow in one commit. Pushing main starts the Build and deploy ReleaseGuard workflow.
- Return to the releaseguard repository in GitHub.
- Select Actions in the repository navigation.
- Select Build and deploy ReleaseGuard.
- Open the newest workflow run.
- Wait for the deploy job to show success.
- Record the successful run's commit SHA as the successful workflow commit SHA.
You have closed the version drift. That successful job built the exact commit before rolling it into both ReleaseGuard replicas.
- Switch back to the separate terminal you used to query the active port-forward.
- Query the deployed root endpoint by running this command:
curl http://127.0.0.1:8080/
What proves the release worked?
The JSON response reports the live container's version value. It should match the successful workflow commit SHA.
The ready value remains true. This confirms the commit-tagged image passed the rollout's readiness checks.
Strong finish. Your public API now identifies the exact Git commit running on Amazon EKS.
Workflow failed or the old version remains?
A credentials failure usually points to the repository variable or immutable OIDC subject. Compare the run's repository IDs with the trust policy used to create the role.
A deployment permission failure usually points to the EKS access entry. Confirm that its principal ARN matches AWS_ROLE_ARN.
Ask for help diagnosing the failed release.
Your push-to-EKS release path is working. Next up, you will add CloudWatch metrics plus container logs so runtime health is visible after every deployment.
Add OTel Container Insights
Your delivery pipeline now moves each successful commit into Kubernetes. However, a completed rollout only proves that Kubernetes accepted the release.
Runtime health needs evidence after deployment. In this step, Amazon CloudWatch OTel Container Insights collects runtime metrics. It also sends container logs to one place for investigation.
In this step, get ready to:
- Install the EKS Pod Identity Agent.
- Create the CloudWatch agent's AWS identity.
- Enable OTel Container Insights.
Install the Pod Identity agent
EKS Pod Identity gives workloads temporary AWS credentials through an agent on each node. The CloudWatch agent uses this path to send telemetry without credentials stored in Kubernetes.
- Install the Pod Identity Agent on releaseguard-eks by running this command:
aws eks create-addon \
--cluster-name releaseguard-eks \
--addon-name eks-pod-identity-agent
What does this command do?
- Amazon EKS installs the managed eks-pod-identity-agent add-on.
- The add-on runs a DaemonSet that places an agent on every worker node.
- Each agent provides temporary credentials to eligible pods on its node.
The command returns an add-on record while Amazon EKS starts the agent.
Before you check, how many agent pods do you expect from a cluster with one worker node?
- Confirm the add-on status by running these checks:
aws eks describe-addon \
--cluster-name releaseguard-eks \
--addon-name eks-pod-identity-agent \
--query "addon.status" \
--output text
kubectl get pods -n kube-system | grep 'eks-pod-identity-agent'
What do these checks prove?
- The first command asks Amazon EKS for the managed add-on status.
- The second command filters the kube-system pod list to the Pod Identity Agent.
You should see ACTIVE from the first command. Each listed agent pod should show Running.
Good progress. The cluster now has the credential bridge needed by the observability workload.
Agent pod not running?
Wait briefly if the add-on is still starting. Check that the managed node remains available if the pod cannot be scheduled.
Use the pod status to narrow down the cause. Help me diagnose why the EKS Pod Identity Agent is not running on releaseguard-eks.
Create the CloudWatch Pod Identity
The CloudWatch agent needs an AWS Identity and Access Management role that trusts EKS Pod Identity. The role receives the managed CloudWatchAgentServerPolicy policy for telemetry delivery.
This permission change is tightly scoped. The role is reserved for CloudWatch observability workloads.
- Create EKS-CloudWatch-Observability-Role with its Pod Identity trust policy by running:
aws iam create-role \
--role-name EKS-CloudWatch-Observability-Role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"]
}]
}'
How does the trust policy work?
- The service principal limits trust to EKS Pod Identity.
- The assume-role action allows EKS to obtain temporary credentials for the associated workload.
- Session tags preserve context about the workload using the role.
The output should include a role object named EKS-CloudWatch-Observability-Role.
- Attach the CloudWatch agent policy by running these commands:
aws iam attach-role-policy \
--role-name EKS-CloudWatch-Observability-Role \
--policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
aws iam list-attached-role-policies \
--role-name EKS-CloudWatch-Observability-Role \
--query 'AttachedPolicies[].PolicyArn' \
--output text
What do these commands do?
- The first command attaches AWS-managed permissions for sending observability data to CloudWatch.
- The second command lists the managed policies attached to the role.
You should see arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy in the output.
The role has permission to send telemetry. EKS still needs a mapping from that role to the CloudWatch agent's Kubernetes service account.
- Create the Pod Identity association by running these commands:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws eks create-pod-identity-association \
--cluster-name releaseguard-eks \
--namespace amazon-cloudwatch \
--service-account cloudwatch-agent \
--role-arn arn:aws:iam::$ACCOUNT_ID:role/EKS-CloudWatch-Observability-Role
How does the association connect everything?
- The first command reads your AWS account ID into ACCOUNT_ID.
- The association maps cloudwatch-agent in amazon-cloudwatch to the IAM role.
- Pods using that service account can receive temporary role credentials.
The response should describe a new Pod Identity association for releaseguard-eks.
That is the identity path connected. The CloudWatch agent can now authenticate without a stored AWS access key.
Role or association creation failed?
Confirm your current AWS identity can create IAM roles. Check that the Pod Identity Agent reached ACTIVE before creating the association.
If the role already exists from an earlier attempt, inspect its trust policy before continuing. Help me troubleshoot the CloudWatch Pod Identity role or association.
Enable and verify OTel Container Insights
The amazon-cloudwatch-observability add-on deploys the collectors that send cluster metrics to CloudWatch. Its configuration preserves source metric names for queries written in PromQL.
- Install the observability add-on with OTel Container Insights enabled by running:
aws eks create-addon \
--cluster-name releaseguard-eks \
--addon-name amazon-cloudwatch-observability \
--configuration-values '{"otelContainerInsights":{"enabled":true}}'
What does this configuration enable?
- Amazon EKS installs the managed CloudWatch observability add-on.
- The otelContainerInsights setting turns on the OpenTelemetry metrics pipeline.
- The add-on uses the Pod Identity association to deliver telemetry to CloudWatch.
Before you run the health checks, which two states would prove that the collector is ready?
- Confirm the add-on status plus the agent pod status by running:
aws eks describe-addon \
--cluster-name releaseguard-eks \
--addon-name amazon-cloudwatch-observability \
--query "addon.status" \
--output text
kubectl get pods -n amazon-cloudwatch -l app.kubernetes.io/name=cloudwatch-agent
What do these checks confirm?
- The add-on status confirms that Amazon EKS finished installing the managed integration.
- The pod list confirms that a CloudWatch agent is operating on the worker node.
You should see ACTIVE from the first command. Every CloudWatch agent pod should show Running.
CloudWatch agent not healthy?
A pod that cannot start can point to missing IAM permissions. It can also indicate insufficient node resources or blocked network access.
Confirm that CloudWatchAgentServerPolicy remains attached to the associated role. Help me diagnose an unhealthy CloudWatch agent pod.
The collectors are running. ReleaseGuard now needs fresh requests that the agents can record.
- Return to the browser tab using the existing port-forward.
- Refresh the ReleaseGuard root endpoint ten times.
Each response should still report the latest successful commit SHA. Those requests create fresh ReleaseGuard activity for the telemetry pipeline.
Telemetry takes 3 to 5 minutes to appear. That wait is normal while the agents publish their first batches to CloudWatch.
- Confirm the Container Insights log groups by running:
aws logs describe-log-groups \
--log-group-name-prefix "/aws/containerinsights/releaseguard-eks" \
--query "logGroups[].logGroupName" \
--output table
What does this log check prove?
The command lists CloudWatch log groups whose names begin with the ReleaseGuard cluster prefix. Seeing those names proves that cluster telemetry has reached CloudWatch Logs.
You should see one or more log-group names beginning with /aws/containerinsights/releaseguard-eks.
- Open the Amazon CloudWatch console in your browser.
- Choose Query Studio in the navigation pane.
- Select PromQL in the query editor.
- Enter this metric query in the editor:
container_cpu_usage_seconds_total
Why query this metric?
This metric records container CPU usage with its original source name. A returned time series proves that OTel Container Insights is publishing queryable runtime data.
Before you choose Run, do you expect the query to return a time series yet?
- Choose Run in Query Studio.
You should see a time series for container_cpu_usage_seconds_total. The result confirms that CloudWatch can query CPU telemetry from the cluster.
No metric data yet?
Wait until the full 3 to 5 minute telemetry window has passed. Generate more ReleaseGuard requests before running the query again.
If the query stays empty, confirm that the add-on remains ACTIVE. Confirm that every agent pod remains Running.
Check the Pod Identity role association if both health checks pass. Help me trace missing OTel Container Insights metrics.
That is the operational proof your pipeline was missing. You can now connect a deployed commit to live cluster metrics plus container logs.
Secret mission
Block and Recover a Bad Release
Stage a release that can run but cannot become ready. You will prove that Kubernetes protects the stable revision, trace the failure through cluster state and CloudWatch logs, then recover the previous release.
Clean Up Your Resources
Amazon Elastic Kubernetes Service standard Kubernetes version support costs $0.10 per cluster per hour. Worker nodes create separate Amazon EC2 charges.
EBS volumes can add charges. Amazon ECR storage can add charges. Amazon CloudWatch ingestion or storage can add charges. AWS credits may offset eligible usage according to your specific credit terms.
Nice Work!
Nice Work!
You made it! ReleaseGuard now carries each commit through secure CI/CD into a healthy Kubernetes release on Amazon EKS.
You've learned how to:
- Built a version-reporting health API with separate readiness and liveness endpoints. Packaged it as a non-root Docker image. Ran two replicas with rolling-update safeguards and health probes.
- Published commit-tagged images to Amazon ECR through GitHub Actions. Used OIDC to exchange workflow identity for short-lived AWS credentials. Restricted deployment permissions to the releaseguard namespace.
- Added OpenTelemetry metrics and container logs with OTel Container Insights. Verified workload health in Amazon CloudWatch. Connected blocked readiness responses to the failed rollout.
- Secret Mission: Forced a new release to fail readiness. Proved the stable revision stayed available through the Kubernetes Service. Diagnosed the blocked rollout with Deployment state plus CloudWatch logs. Restored two healthy replicas through a safe rollback.
Ready to quiz yourself?