Deploy a Python API to Amazon ECS

Containerize a Python API and deploy it to Amazon ECS with GitHub Actions.

Introduction

30 Second Summary

A code change can take seconds. Releasing it safely can take far longer when every update depends on manual steps.

In this project, you will ship a versioned FastAPI service inside a Docker container. GitHub Actions will deploy each push to a public Amazon ECS Express Mode endpoint without storing long-lived access keys.

What You'll Build

Your public HTTPS endpoint will report version 2.0.0 after a single push to main.

By the end of this project, you'll have:

  • An interactive local API running in a Docker container. You can open http://127.0.0.1:8000/docs to try its three routes.
  • An automated deployment that turns every push to main into a commit-tagged image in Amazon ECR. You can watch GitHub Actions deploy version 2.0.0 to the public endpoint.
  • An operational trail that connects a controlled HTTP 500 response to its matching request in Amazon CloudWatch logs. You can inspect CPUUtilization to see the service's CPU activity.
  • Secret Mission: Replace the hardcoded application version with the exact GitHub commit SHA used for the deployment.

Are there any prerequisites?

Your Windows computer must run Docker Desktop. You also need GitHub access plus AWS permission to create the listed cloud resources.

Before We Start

You are committing to an observable Container Learning API that traces each release through Docker packaging, GitHub automation, AWS deployment, and production monitoring. This automated path turns source changes into traceable releases without manual rebuilding or uploading.

Set Up the Windows Toolchain

A predictable Windows toolchain keeps installation problems separate from container or pipeline failures. Clear version checks also reveal missing PATH entries before they interrupt your project.

This step prepares the local runtime for your Container Learning API. A final check proves that every tool is available from PowerShell.

In this step, get ready to:
  • Check the Windows Linux runtime.
  • Prepare the local development toolchain.
  • Open an empty project workspace with an integrated terminal.
Check WSL 2 readiness

WSL 2 provides the Linux environment used by Docker Desktop. Keeping WSL current protects the container engine from avoidable compatibility failures.

  • Press the Windows key to open Windows Search.
  • Type PowerShell into Windows Search.
  • Press Enter to start PowerShell.
  • Check your WSL installation by running this command:
wsl --version

What does this command check?

This command asks Windows for the installed WSL version. The result tells you whether WSL is ready for Docker Desktop or needs attention first.

✔️ I see version 2.1.5 or higher

That is the runtime foundation in place. Your WSL version meets Docker Desktop's minimum requirement.

ⓧ I see an older version

An older WSL release can cause Docker Desktop compatibility problems. Update it before installing the container tools.

  • Return to Windows Search.
  • Right-click Windows PowerShell in the results.
  • Select Run as administrator.
  • Approve the Windows permission request if it appears.
  • Update WSL by running this command:
wsl --update

What does this command do?

This command downloads the current WSL update from Microsoft. It updates the runtime that Docker Desktop uses for Linux containers.

  • Restart your computer if Windows requests it.
  • Open a new PowerShell window through Windows Search.
  • Confirm the updated version by running:
wsl --version

What should this confirm?

The output should now report WSL version 2.1.5 or higher. That version is ready for the Docker Desktop backend.

Still seeing the older version?

Restart Windows before checking again. A pending WSL update may finish during the restart.

If the version stays unchanged, help me troubleshoot a WSL update that is not taking effect.

ⓧ Command not found

Windows cannot find WSL yet. Install it from an elevated PowerShell window.

  • Return to Windows Search.
  • Right-click Windows PowerShell in the results.
  • Select Run as administrator.
  • Approve the Windows permission request if it appears.
  • Install WSL by running this command:
wsl --install

What does this command do?

This command enables the required Windows features. It also installs the WSL components needed by Docker Desktop.

  • Restart your computer if Windows requests it.
  • Open a new PowerShell window through Windows Search.
  • Confirm the installation by running:
wsl --version

What should this confirm?

The command should now print WSL version information. This confirms that Windows can reach the installed runtime.

WSL installation still unavailable?

Confirm that you restarted Windows after the installation. Work-managed computers may also require administrator approval for virtualization features.

If WSL remains unavailable, help me diagnose why WSL is not installing on Windows.

Why WSL sits beneath Docker

Linux containers need a Linux kernel environment. WSL 2 supplies that environment inside Windows without requiring a separate virtual machine workflow.

Docker Desktop manages the connection between Windows commands and that Linux environment. A healthy WSL installation gives the container engine a stable base.

Install and verify the desktop tools

Visual Studio Code holds the project workspace. Its integrated terminal keeps commands beside the files they affect.

Git for Windows records each release as source history. Docker Desktop packages that source into a repeatable container image.

  • Close the current PowerShell window.
  • Open a new PowerShell window through Windows Search.
  • Check whether all three tools are available by running these commands:
docker version
git --version
code --version

What do these checks prove?

  • The docker check confirms that the Docker command is available. A connection message is expected until Docker Desktop starts.
  • The git check confirms that Git for Windows is available on PATH.
  • The code check confirms that Visual Studio Code is available on PATH.

✔️ All three commands show version information

Your editor and command-line tools are installed. The fresh PowerShell window can reach every command required for the project.

ⓧ One or more commands are unavailable

Install only the tools whose commands are unavailable. Each installer adds its command to PATH for new terminal sessions.

winget install --id Git.Git -e --source winget

What does this command install?

Windows Package Manager finds the official Git for Windows package by its exact identifier. The installation adds Git to your local toolchain.

  • Complete any installer prompts that appear.
  • Close the current PowerShell window after the installations finish.
  • Open a new PowerShell window through Windows Search.
  • Recheck the toolchain by running:
docker version
git --version
code --version

Why use a new window?

A new PowerShell session reloads PATH after installation. Each command should now print version information instead of an unavailable-command message.

A command is still unavailable?

Restart Windows if a newly installed command remains unavailable. This refreshes system PATH changes that have not reached new terminal sessions.

If the problem continues, help me troubleshoot a Windows tool that is installed but missing from PATH.

Start Docker and open the workspace

Docker Desktop must be running before the Docker command can reach its container engine. The recommended WSL 2 backend connects that engine to the runtime you just verified.

  • Press the Windows key to open Windows Search.
  • Type Docker Desktop into Windows Search.
  • Press Enter to start Docker Desktop.
  • Select Accept if Docker Desktop displays its subscription agreement.
  • Wait until Docker Desktop reports that it is running.

Docker Desktop is now managing a local container engine. The next check confirms that PowerShell can reach it.

  • Select Settings in Docker Desktop.
  • Select General.
  • Confirm Use the WSL 2 based engine is selected if the setting is visible.
  • Select Use the WSL 2 based engine if the checkbox is clear.
  • Select Apply after changing the setting.

Setting not visible

Docker Desktop can hide the WSL 2 setting when the supported backend is already active. Continue to the engine check if the option does not appear.

Before you run the check, predict whether Docker can reach both the local command and the running engine.

  • Return to the new PowerShell window from earlier.
  • Confirm that the Docker engine responds by running:
docker version

What should this prove?

The output should include version information for the Docker command and the running engine. This proves that Docker Desktop is active through the Windows terminal.

Docker cannot reach the engine?

Return to Docker Desktop and confirm that it reports a running state. Apply the WSL 2 backend setting before checking again.

If the engine still does not respond, help me troubleshoot Docker Desktop on the WSL 2 backend.

Your workspace gives every upcoming file a predictable home. Visual Studio Code opens its integrated terminal at the folder currently shown in the Explorer.

  • Press the Windows key to open Windows Search.
  • Type Visual Studio Code into Windows Search.
  • Press Enter to start Visual Studio Code.

You should now see an empty Visual Studio Code window. Create the project workspace from its folder picker.

  • Select File from the menu bar.
  • Select Open Folder....
  • Select Desktop as the folder location.
  • Select New Folder.
  • Type container-learning-api as the folder name.
  • Select Select Folder.
  • Approve the folder as trusted if Visual Studio Code asks.

The Explorer should now show the empty container-learning-api folder. That folder becomes the home for every API and container file in the next step.

  • Select View from the menu bar.
  • Select Terminal.
  • Confirm that the terminal profile displays PowerShell.
  • Select the terminal profile dropdown if another shell opens.
  • Select PowerShell from the profile list.

Before you run the final check, predict whether the integrated terminal can now reach all three development tools.

  • Verify the complete toolchain from the integrated PowerShell terminal by running:
docker version
git --version
code --version

What should you see?

Each command should print version information without an unavailable-command error. Docker should also return information from its running engine.

That is the setup complete. Your Windows workspace can now edit source code, record releases, and run containers from one place.

Your Windows toolchain and empty workspace are ready. Next, you will build the first API release and run it inside Docker.

Build and Run the Python API

Your Windows toolchain is ready. The empty workspace can now become a running API.

Packaging the first API response inside a Docker image shows exactly what Docker captures inside a container. It also removes the need for a separate local Python installation.

In this step, get ready to:
  • Create a versioned FastAPI service with three GET routes.
  • Pin the application dependencies. Filter development files from Git and Docker.
  • Build the Docker image. Run the API in a named container.
Create the FastAPI service

FastAPI maps Python functions to HTTP paths. The app object holds those route mappings.

  • Select the new-file control in Visual Studio Code's left sidebar.
  • Enter main.py as the file name.
  • Add the application setup plus the root route by pasting this code into main.py:
from fastapi import FastAPI

APP_VERSION = "1.0.0"

app = FastAPI(
    title="Container Learning API",
    version=APP_VERSION,
)


@app.get("/")
def read_root() -> dict[str, str]:
    return {
        "message": "Container Learning API is running",
        "version": APP_VERSION,
    }

What does this code do?

  • The FastAPI import provides the application class.
  • The APP_VERSION variable gives this release the value 1.0.0.
  • The app object sets the API title.
  • The @app.get("/") decorator connects the root path to read_root.
  • The root response reports the running release.

The type dict[str, str] documents the response shape. Every value returned by read_root is a string.

  • Save main.py.
  • Confirm the file sidebar lists main.py inside container-learning-api.

Cannot find main.py?

Check that the file sits directly inside container-learning-api. Check that its name ends with .py.

Help me check the location and name of main.py in Visual Studio Code.

A load balancer needs a lightweight way to confirm that the service can answer requests. A controlled failure route also gives the project a predictable event for later log inspection.

  • Place the cursor below the read_root function.
  • Add the health route plus the controlled-error route by pasting:
@app.get("/health")
def health_check() -> dict[str, str]:
    return {"status": "healthy"}


@app.get("/simulate-error")
def simulate_error() -> None:
    raise RuntimeError("Simulated monitoring event")

What do these routes do?

  • The /health route returns the healthy status used by service checks.
  • The health_check function produces a normal successful response.
  • The /simulate-error route raises a controlled runtime error.
  • The simulate_error function creates an event that can appear in application logs.
  • Save main.py.
  • Confirm the editor now shows the /. /health. /simulate-error paths.

Seeing route syntax problems?

Check that each decorator starts with @app.get. Keep each function aligned with its decorator.

Help me compare my FastAPI routes with the required main.py code.

✔️ Awesome, I've got everything!

Your saved main.py should now contain the application plus all three routes.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

APP_VERSION = "1.0.0"

app = FastAPI(
    title="Container Learning API",
    version=APP_VERSION,
)


@app.get("/")
def read_root() -> dict[str, str]:
    return {
        "message": "Container Learning API is running",
        "version": APP_VERSION,
    }


@app.get("/health")
def health_check() -> dict[str, str]:
    return {"status": "healthy"}


@app.get("/simulate-error")
def simulate_error() -> None:
    raise RuntimeError("Simulated monitoring event")
Pin dependencies and filter files

Exact dependency pins keep image builds consistent. Ignore files prevent local development data from entering source control or the Docker build context.

  • Select the new-file control in Visual Studio Code's left sidebar.
  • Enter requirements.txt as the file name.
  • Pin the two application packages by pasting:
fastapi==0.141.1
uvicorn==0.53.0

What do these pins control?

  • FastAPI 0.141.1 provides the API framework.
  • Uvicorn 0.53.0 serves the application inside the container.
  • The exact pins make future image builds use the same package releases.
  • Save requirements.txt.
  • Confirm the file sidebar lists requirements.txt.

Package pins look different?

Check that each dependency uses two equals signs. Keep one dependency on each line.

Help me check the dependency pins in requirements.txt.

✔️ Awesome, I've got everything!

Your dependency file now contains both exact package pins.

ⓧ I'd like to double check the full code

fastapi==0.141.1
uvicorn==0.53.0
  • Select the new-file control in Visual Studio Code's left sidebar.
  • Enter .gitignore as the file name.
  • Keep local Python artifacts out of Git by pasting:
.venv/
__pycache__/
*.py[cod]

What does Git ignore?

  • The .venv/ pattern excludes a local virtual environment.
  • The __pycache__/ pattern excludes Python's cache folders.
  • The *.py[cod] pattern excludes compiled Python files.
  • Save .gitignore.
  • Confirm the file sidebar lists .gitignore.

Cannot see .gitignore?

Check that the name starts with a period. Remove any extra file extension added after .gitignore.

Help me check whether my .gitignore file has the correct name and location.

✔️ Awesome, I've got everything!

Your Git ignore rules now cover the local Python artifacts.

ⓧ I'd like to double check the full code

.venv/
__pycache__/
*.py[cod]
  • Select the new-file control in Visual Studio Code's left sidebar.
  • Enter .dockerignore as the file name.
  • Limit the Docker build context by pasting:
.git
.github
.venv
__pycache__
*.py[cod]

What stays outside the image build?

  • The .git entry excludes repository history.
  • The .github entry excludes repository automation files.
  • The remaining patterns exclude local Python development artifacts.
  • Save .dockerignore.
  • Confirm the file sidebar lists .dockerignore.

Git reads .gitignore when tracking source files. Docker reads .dockerignore when sending files into an image build.

Cannot see .dockerignore?

Check that the name starts with a period. Remove any extra file extension added after .dockerignore.

Help me verify the name and location of my .dockerignore file.

✔️ Awesome, I've got everything!

Your Docker build context now excludes repository data plus local Python artifacts.

ⓧ I'd like to double check the full code

.git
.github
.venv
__pycache__
*.py[cod]
Build and run the Docker image

A Dockerfile records the recipe for an image. Its instructions install the pinned packages before adding the application source.

Uvicorn listens on port 8080 inside the container. Docker publishes that internal port through 127.0.0.1:8000 on your computer.

  • Select the new-file control in Visual Studio Code's left sidebar.
  • Enter Dockerfile as the file name.
  • Define the image build by pasting:
FROM python:3.14.7-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .

EXPOSE 8080

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

How does this image recipe work?

  • The FROM instruction starts from python:3.14.7-slim.
  • The WORKDIR instruction sets /app as the container's application directory.
  • The first COPY instruction adds the dependency file.
  • The RUN instruction installs the pinned dependencies.
  • The second COPY instruction adds main.py.
  • The EXPOSE instruction documents port 8080.
  • The CMD instruction starts the API server.

Copying requirements.txt before main.py lets Docker reuse the dependency layer when only the application source changes.

  • Save Dockerfile.
  • Confirm the file sidebar lists Dockerfile.

Dockerfile saved with an extension?

The file name must be exactly Dockerfile. Remove any .txt extension.

Help me verify the Dockerfile name and contents.

✔️ Awesome, I've got everything!

Your workspace now contains the complete image recipe.

ⓧ I'd like to double check the full code

FROM python:3.14.7-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .

EXPOSE 8080

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
  • Build the version 1.0.0 image from the open workspace by running this command in the integrated PowerShell terminal:
docker build -t container-learning-api:1.0.0 .

What does this command do?

  • The docker build command processes the Dockerfile.
  • The -t option names the image container-learning-api:1.0.0.
  • The final period uses the current container-learning-api workspace as the build context.
  • Confirm the terminal reports that the image build completed successfully.

That is the packaging milestone complete. Your API now has a repeatable image tagged as release 1.0.0.

Image build failed?

Check that the integrated terminal is still using the open container-learning-api workspace. Confirm that all five project files appear in the file sidebar.

Help me diagnose my Container Learning API image build.

Before you start the container, consider whether host port 8000 can reach the API listening on container port 8080.

  • Start the named container with localhost port publishing by running:
docker run --name container-learning-api -d -p 127.0.0.1:8000:8080 container-learning-api:1.0.0

How is the container started?

  • The --name option gives the container the name container-learning-api.
  • The -d option runs the container in the background.
  • The -p option publishes container port 8080 through host port 8000.
  • Binding to 127.0.0.1 keeps the local API available only from your computer.

You'll see a container identifier in the terminal. That value confirms Docker created the named container in the background.

Container did not start?

Confirm Docker Desktop is still running. Check whether another local process already uses port 8000.

Help me diagnose why the container-learning-api container did not start.

Before you load the API documentation, consider which three paths should appear from the route decorators in main.py.

  • Open a new browser tab.
  • Enter http://127.0.0.1:8000/docs in the address bar.
  • Expand the route for /.
  • Send one request from the route's interactive panel.

You'll see Swagger UI list /. /health. /simulate-error.

The root response contains Container Learning API is running plus version 1.0.0. Your first API response is now running from inside Docker.

Before you inspect the logs, consider what evidence should confirm that the server started inside the container.

  • Inspect the named container's logs by running:
docker logs container-learning-api

What do these logs prove?

  • The startup entries confirm Uvicorn launched inside the container.
  • The request entry confirms the browser reached the Dockerized API.
  • The logs provide a direct record of application activity.

You'll see startup information plus the request generated from the documentation page. The image build, container runtime, published port, API routes, and logs now form one working local path.

Your local API now runs from a repeatable Docker image. Next, you'll test what happens when the source changes after a container is already running.

Prove Container Immutability

Your 1.0.0 API now runs through Docker inside a container. That gives you a working release to test.

A source edit creates a critical release question. Does changing main.py to 2.0.0 update the process that is already serving requests?

In this step, get ready to:
  • Change the source release from 1.0.0 to 2.0.0.
  • Compare the changed source with the existing container response.
  • Publish the updated project to a public GitHub repository on main.
Change the source version

The APP_VERSION constant feeds the root response. FastAPI also uses it as API metadata.

  • In the Visual Studio Code Explorer sidebar, select main.py.
  • Find APP_VERSION = "1.0.0".
  • Replace 1.0.0 with 2.0.0.
  • Save main.py.

Your source now identifies itself as version 2.0.0. Both routes that read APP_VERSION now use that value.

✔️ Awesome, I've got everything!

Great. Double-check that you saved main.py.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

APP_VERSION = "2.0.0"

app = FastAPI(
    title="Container Learning API",
    version=APP_VERSION,
)


@app.get("/")
def read_root() -> dict[str, str]:
    return {
        "message": "Container Learning API is running",
        "version": APP_VERSION,
    }


@app.get("/health")
def health_check() -> dict[str, str]:
    return {"status": "healthy"}


@app.get("/simulate-error")
def simulate_error() -> None:
    raise RuntimeError("Simulated monitoring event")

Before you check the running API, which version do you think it serves now?

  • Open http://127.0.0.1:8000/ in a new browser tab.

You'll still see version 1.0.0 in the response.

You just exposed the release gap. Your saved source says 2.0.0 while the running container continues to serve 1.0.0.

What Stayed Frozen?

A Docker image captures the files available during its build. The running container reads the main.py copy from that image.

Saving the workspace changes the host file. A new release needs a newly built image plus a replacement container.

Create the public release repository

A public GitHub repository gives your changed source a remote home. Starting empty lets this workspace provide the first files plus the first history.

  • Sign in to GitHub in your browser.
  • From GitHub's home page, start creating a new repository.
  • Enter container-learning-api as the repository name.
  • Set the repository visibility to public.
  • Leave the README option disabled.
  • Leave the license option disabled.
  • Leave the Git ignore option disabled.
  • Submit the repository creation form.

Your empty public repository is ready. It can now receive the project history from your existing workspace.

A Git commit records the changed source as a stable checkpoint. This commit becomes the first release snapshot in your repository.

  • Return to the integrated PowerShell terminal from earlier.
  • Initialize the repository on main and create the first commit by running these commands:
git init -b main
git add .
git commit -m "Build container learning API"

What Did Git Record?

  • The first command creates local repository history on branch main.
  • The second command stages the source plus Docker files for the snapshot.
  • The third command saves the staged snapshot with the message Build container learning API.

Your terminal prints a commit summary that lists the project files. That is your first recorded release checkpoint.

Did the Commit Stop?

If Git needs author details, follow the configuration guidance printed in the terminal. Run the commit command again after saving those details.

Ask for help with configuring Git author details for this commit. Share non-sensitive terminal output with the NextWork community if you still need support.

Connect and publish the repository

A remote connects your local commit history to the empty GitHub repository. The name origin gives that destination a short local reference.

  • Copy the repository's HTTPS URL here: your GitHub repository URL.
  • Return to the integrated PowerShell terminal from earlier.
  • Connect the local repository to your saved GitHub URL by replacing REMOTE-URL before you run this command:
git remote add origin REMOTE-URL

Why Use Origin?

The name origin points to your GitHub repository from this workspace. Future push commands can use that short name instead of repeating the full URL.

  • Confirm the remote connection by running:
git remote -v

You'll see your GitHub repository URL associated with origin for fetching plus pushing.

The first push may open a browser sign-in through Git Credential Manager. The credential stays outside your project files.

Before you push, what do you expect the empty GitHub repository to show after it receives your commit?

  • Publish branch main by running:
git push -u origin main

This push publishes your local commit. It also links local branch main to the matching branch on origin.

  • Complete browser authentication if Git Credential Manager requests it.

After authentication succeeds, your terminal confirms that branch main was published.

Did the Push Fail?

Check that the saved repository URL belongs to the empty container-learning-api repository. Confirm that your browser authentication used the GitHub account that owns it.

Ask for help with publishing this repository through Git Credential Manager. Share redacted terminal output with the NextWork community if the push still fails.

  • Return to the GitHub repository page.
  • Refresh the repository page.
  • Open main.py from branch main.

You'll see APP_VERSION set to 2.0.0 in the published source.

  • Switch back to http://127.0.0.1:8000/ in the browser from earlier.
  • Refresh the local endpoint.

You'll still receive version 1.0.0 from the running container.

You now have direct evidence that source changes require a new container release. Next, you'll build the cloud path that can make each commit deployable.

Prepare AWS and GitHub OIDC

Your repository now contains version 2.0.0. The running local container still serves version 1.0.0.

A cloud release needs a registry that stores each new image. It also needs an Amazon ECS Express Mode service that runs the selected image.

Your GitHub Actions workflow also needs temporary AWS access. OpenID Connect provides that access without storing long-lived AWS keys in GitHub.

In this step, get ready to:
  • Create an immutable Amazon ECR repository for release images.
  • Launch a bootstrap ECS Express Mode service with a public Application URL.
  • Configure GitHub OIDC trust with deployment permissions and repository variables.
Create the AWS release foundation

The image registry gives each release a permanent home. The bootstrap service proves that your AWS networking can serve a container before automation replaces the temporary image.

Why use ECS Express Mode?

ECS Express Mode creates the load balancer, networking, scaling configuration, logging path, and public HTTPS endpoint around a stateless API. This keeps the project focused on the container release path.

A standard ECS service gives you more infrastructure controls. Express Mode supplies the supporting resources this API needs with fewer manual configuration screens.

This Starts Billable Resources

Creating the service starts usage-based charges for AWS Fargate, its load balancer, public IPv4 addresses, logs, and image storage. A light deployment usually costs about $0.05 to $0.10 per running hour in US East (N. Virginia).

The final cleanup section shows you how to remove these resources. Keeping both task-count limits at 1 controls the footprint during this project.

  • Sign in to the AWS Management Console with an account that can create IAM, ECR, ECS, load-balancing, and related resources.
  • Select US East (N. Virginia) from the Region selector.
  • Open the account menu in the console header.
  • Record the displayed account ID here: your 12-digit AWS account ID.
  • Use the console search bar to open Amazon VPC.
  • Confirm that the Region contains a default VPC with public subnets.

You should see a default VPC available in US East (N. Virginia). Its public subnets give the Express Mode load balancer an internet-facing network path.

No Default VPC Available?

  • Use the Amazon VPC console option for creating a default VPC in US East (N. Virginia).
  • Return to the VPC list after creation completes.
  • Confirm that the new default VPC includes public subnets.

If your account policy blocks this action, ask an AWS administrator to restore the default VPC or grant permission to create the required networking resources.

Get help with preparing the default VPC for ECS Express Mode.

  • Use the console search bar to open Amazon Elastic Container Registry.
  • Select Private repositories in the Amazon ECR navigation pane.
  • Select Create repository.
  • Enter container-learning-api in the Repository name field.
  • Select Immutable under Image tag immutability.
  • Select Create.

The repository list should now show container-learning-api in us-east-1. Its image tags are immutable.

Why Make Image Tags Immutable?

The workflow uses a unique Git commit SHA as each image tag. Tag immutability prevents a later push from replacing the image associated with an existing release identifier.

A commit tag can therefore point to one image throughout its lifetime. That makes production behavior easier to trace.

  • Use the console search bar to open Amazon Elastic Container Service.
  • Select Express mode in the Amazon ECS navigation pane.
  • Enter public.ecr.aws/nginx/nginx:latest in the Image URI field.
  • Enter container-learning-api in the Name field.

Bootstrap Configuration Check

The setup summary should now identify the Nginx image and the container-learning-api service. This temporary image gives you a visible AWS result before the workflow deploys your API.

  • Expand Additional configurations.
  • Set Container port to 80.
  • Set Health check path to /.

The container configuration should now route load-balancer traffic to port 80. Health checks target the Nginx root path.

  • Set CPU to 0.25 vCPU.
  • Set Memory to 0.5 GB.

The compute summary should now show 256 CPU units with 512 MiB of memory.

  • Set Minimum number of tasks to 1.
  • Set Maximum number of tasks to 1.

The auto-scaling summary should now keep exactly one task running. This gives the bootstrap service a predictable cost footprint.

  • Choose Create new role for Task execution role.
  • Choose Create new role for Infrastructure role.

Provisioning can take several minutes because Express Mode creates networking, load-balancing, logging, certificate, and service resources. A quiet progress screen during this wait is expected.

  • Select Create to start the bootstrap deployment.
  • Wait until the deployment status reports that the service is available.

The service details should list ecsTaskExecutionRole as the task execution role. They should list ecsInfrastructureRoleForExpressServices as the infrastructure role.

Before you open the Application URL, what page do you expect from the temporary image?

  • Open the public Application URL from the service details.

You should see the Nginx welcome page over HTTPS. That visible page proves the bootstrap container is reachable through the Express Mode endpoint.

That is the AWS foundation working. Your next deployment can replace this temporary container without rebuilding its surrounding infrastructure by hand.

Application URL Not Loading?

  • Confirm that the service deployment has finished before opening the URL.
  • Confirm that the container port is 80.
  • Confirm that the health path is /.

Get help with diagnosing the ECS Express Mode bootstrap deployment.

Build the OIDC trust boundary

The deployment workflow needs permission to publish images and update the service. AWS Identity and Access Management separates who may assume the role from what that role may do.

The trust policy accepts tokens from one repository branch. The permissions policy limits the resulting temporary session to the release operations required by this project.

  • Use the console search bar to open IAM.
  • Select Identity providers in the IAM navigation pane.
  • Select Add provider.
  • Select OpenID Connect under Provider type.
  • Enter https://token.actions.githubusercontent.com in the Provider URL field.
  • Enter sts.amazonaws.com in the Audience field.
  • Select Add provider.

✔️ The Provider Was Added

The provider details should show token.actions.githubusercontent.com with audience sts.amazonaws.com.

ⓧ The Provider Already Exists

  • Return to Identity providers in IAM.
  • Select token.actions.githubusercontent.com.
  • Confirm that its audience includes sts.amazonaws.com.

Keep the existing provider. Multiple GitHub repositories can use one account-level provider through separate role trust policies.

New GitHub repositories use immutable numeric IDs in their OIDC subjects. Those IDs keep the trust relationship attached to this repository even if its visible name changes.

  • Return to the public container-learning-api repository in GitHub.
  • Record the repository owner from its URL here: your GitHub owner name.
  • Build the repository metadata URL here: https://api.github.com/repos/OWNER/container-learning-api.
  • Replace OWNER in that URL with your GitHub owner name.
  • Paste the completed repository metadata URL into a new browser tab.

You should see a GitHub repository metadata response. The top-level id identifies the repository.

  • Record the top-level repository id here: your GitHub numeric repository ID.
  • Find the id inside the owner object.
  • Record the owner id here: your GitHub numeric owner ID.
  • Build the immutable OIDC subject here: repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/main.
  • Replace each named section in the subject with the corresponding GitHub value you recorded.

How Does the Immutable Subject Work?

The subject combines the visible owner name with the immutable owner ID. It applies the same pattern to the repository.

The final branch reference restricts access to main. A workflow from another repository or branch receives a token with a different subject.

  • Return to IAM.
  • Select Roles in the navigation pane.
  • Select Create role.
  • Select Web identity as the trusted entity type.

The role form should now display fields for the identity provider, audience, GitHub organization, repository, and branch.

  • Choose token.actions.githubusercontent.com as the Identity provider.
  • Choose sts.amazonaws.com as the Audience.
  • Enter your GitHub owner name in the GitHub organization field.
  • Enter container-learning-api in the GitHub repository field.
  • Enter main in the GitHub branch field.
  • Select Next.

The permissions page should show the GitHub OIDC provider as the trusted identity. The role has no AWS permissions yet.

  • Leave the permission policy list unselected.
  • Select Next.
  • Enter github-actions-ecs-role in the Role name field.
  • Select Create role.

The IAM role list should now include github-actions-ecs-role.

  • Select the github-actions-ecs-role role.
  • Select the Trust relationships tab.
  • Select Edit trust policy.
  • Find the condition named token.actions.githubusercontent.com:sub.
  • Replace its value with repo:OWNER@OWNER-ID/REPO@REPO-ID:ref:refs/heads/main.
  • Confirm that token.actions.githubusercontent.com:aud equals sts.amazonaws.com.
  • Confirm that the trust action is sts:AssumeRoleWithWebIdentity.
  • Save the trust policy.

The saved trust relationship should now show the immutable owner ID, repository ID, and main branch in one subject condition.

Use This Verified Permission Set

The deployment role needs authorization, repository, ECS deployment, and role-delegation permissions. Select the following actions in the IAM visual policy editor.

  • Allow ecr:GetAuthorizationToken for registry authentication.
  • Allow ecr:BatchCheckLayerAvailability for layer checks.
  • Allow ecr:GetDownloadUrlForLayer for layer access.
  • Allow ecr:GetRepositoryPolicy for repository inspection.
  • Allow ecr:DescribeRepositories for repository discovery.
  • Allow ecr:ListImages for image discovery.
  • Allow ecr:DescribeImages for image inspection.
  • Allow ecr:BatchGetImage for image retrieval.
  • Allow ecr:InitiateLayerUpload for upload creation.
  • Allow ecr:UploadLayerPart for layer uploads.
  • Allow ecr:CompleteLayerUpload for completed layers.
  • Allow ecr:PutImage for image publication.
  • Allow ecs:CreateCluster for cluster preparation.
  • Allow ecs:RegisterTaskDefinition for release task definitions.
  • Allow ecs:CreateExpressGatewayService for Express service creation.
  • Allow ecs:UpdateExpressGatewayService for Express service updates.
  • Allow ecs:DescribeExpressGatewayService for Express service inspection.
  • Allow ecs:DescribeClusters for cluster inspection.
  • Allow ecs:DescribeServices for service inspection.
  • Allow ecs:ListServiceDeployments for deployment discovery.
  • Allow ecs:DescribeServiceDeployments for deployment inspection.
  • Allow ecs:TagResource for deployment tags.
  • Allow ecs:UntagResource for tag removal.
  • Allow iam:PassRole for the two ECS service roles.
  • Select the Permissions tab for github-actions-ecs-role.
  • Select Add permissions.
  • Select Create inline policy.
  • Select the Visual policy editor.
  • Add an Amazon Elastic Container Registry permission block for ecr:GetAuthorizationToken on all resources.
  • Add an Amazon Elastic Container Registry permission block containing the remaining ECR actions from the verified list.
  • Scope the repository permission block to container-learning-api in us-east-1 under your 12-digit AWS account ID.

The visual editor should now show one account-wide ECR authentication block. It should show one repository-scoped ECR block.

  • Add an Amazon Elastic Container Service permission block containing every ECS action from the verified list.
  • Apply the ECS permission block to all resources.
  • Add an IAM permission block for iam:PassRole.
  • Scope the IAM block to ecsTaskExecutionRole.
  • Add ecsInfrastructureRoleForExpressServices to the same IAM block.

The editor should now show four permission blocks. The role can authenticate to ECR, publish to one repository, manage the Express deployment, and pass the two ECS roles.

  • Select Next.
  • Enter github-actions-ecs-deployment-policy as the inline policy name.
  • Select Create policy.

The access boundary is now complete. GitHub can request a short-lived session only when the token matches this repository and branch.

How Do the Two Policies Differ?

The trust policy answers who can assume the role. It checks the OIDC provider, audience, repository identity, and branch.

The permission policy answers what an accepted session can do. It limits image publication to one ECR repository while granting the ECS operations required for deployment.

Policy Validation Blocking the Role?

  • Confirm that the trust condition uses the numeric owner ID and repository ID from the GitHub metadata response.
  • Confirm that the audience is sts.amazonaws.com.
  • Confirm that both ECS role names match the roles created by Express Mode.

Get help with checking the GitHub OIDC trust and deployment permissions.

Add deployment variables and verify access

The workflow should keep reusable deployment settings outside the workflow file. GitHub repository variables provide these non-sensitive values to every run.

  • Return to the container-learning-api repository in GitHub.
  • Select Settings under the repository name.
  • Select Secrets and variables in the Security section.
  • Select Actions.
  • Select the Variables tab.

The repository variables page should now display controls for adding non-sensitive configuration values.

  • Create AWS_REGION with the value us-east-1.
  • Create AWS_ACCOUNT_ID with the value your 12-digit AWS account ID.
  • Create ECR_REPOSITORY with the value container-learning-api.
  • Create ECS_SERVICE with the value container-learning-api.
  • Create ECS_CLUSTER with the value default.

You should now see five repository variables. Their names should match the workflow inputs exactly.

Variables Stay Non-Sensitive

These values identify the deployment destination. None of them grants access by itself.

OIDC supplies temporary credentials during each workflow run. You do not need to create or store AWS access keys in GitHub.

Before you perform the final check, which repository identity and branch should the IAM trust condition show?

  • Return to the github-actions-ecs-role role in IAM.
  • Select the Trust relationships tab.
  • Confirm that the audience condition equals sts.amazonaws.com.
  • Confirm that the subject condition contains your immutable GitHub owner ID.
  • Confirm that the subject condition contains your immutable GitHub repository ID.
  • Confirm that the subject condition ends with the main branch reference.
  • Return to the GitHub repository variables page.
  • Confirm that all five deployment variable names are listed.

The trust policy should identify one immutable repository on main. GitHub should list AWS_REGION, AWS_ACCOUNT_ID, ECR_REPOSITORY, ECS_SERVICE, and ECS_CLUSTER.

  • Return to the container-learning-api service in the default ECS cluster.
  • Open its public Application URL.

You should still see the Nginx page. The bootstrap service is live while its immutable registry, OIDC role, deployment permissions, and GitHub variables wait for the automated release.

Your cloud release path is ready. Next, you will turn a push to main into a commit-tagged image and a coordinated ECS deployment.

Automate Deployment and Monitoring

Your GitHub repository now contains version 2.0.0 of the FastAPI service. The running Docker container still serves 1.0.0.

This step closes that release gap with a GitHub Actions CI/CD pipeline that authenticates through OIDC.

Application logs reveal requests and failures. Infrastructure metrics reveal service resource use.

In this step, get ready to:
  • Create a GitHub Actions workflow that builds and deploys each release.
  • Publish a commit-tagged image to Amazon ECR and verify the public HTTPS endpoint.
  • Trace a controlled failure across application logs, service metrics, and deployment status.
Create the deployment workflow

A YAML workflow defines what happens after a push reaches main. The first section sets the trigger, minimum permissions, release values, and job runner.

  • In the Visual Studio Code file tree, select the new-file control for the container-learning-api folder.
  • Enter .github/workflows/deploy.yml as the file path.
  • Add the workflow trigger and release environment by pasting this first section:
name: Deploy Container Learning API

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
  ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
  ECS_SERVICE: ${{ vars.ECS_SERVICE }}
  ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
  IMAGE_TAG: ${{ github.sha }}

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Check out source
        uses: actions/checkout@v7.0.1

How Does This Start the Pipeline?

  • The push trigger starts the workflow when a commit reaches main.
  • The id-token: write permission allows GitHub Actions to request an OIDC token.
  • The repository variables identify the AWS Region, account, repository, service, and cluster.
  • The github.sha value gives each image an immutable release tag.
  • Save .github/workflows/deploy.yml.

The file tree should now show .github/workflows/deploy.yml beneath the two new folders.

Seeing YAML Errors?

Check that every nested line uses spaces at the same indentation level. Make sure steps: lines up beneath runs-on:.

Ask for a focused indentation check: Help me find the YAML indentation problem in this GitHub Actions workflow section.

The next section exchanges the OIDC token for temporary AWS credentials. It then builds a commit-tagged image and publishes that image to the existing private repository.

  • Place your cursor below the actions/checkout@v7.0.1 line.
  • Add the authentication and image publication steps by pasting:
      - name: Configure AWS credentials with OIDC
        uses: aws-actions/configure-aws-credentials@v6.3.0
        with:
          role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-actions-ecs-role
          aws-region: ${{ env.AWS_REGION }}
          role-session-name: GitHubActionsECSDeployment

      - name: Log in to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2.1.7

      - name: Build and push the image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build -t "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker push "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

How Does Passwordless Publication Work?

  • The credential action assumes github-actions-ecs-role through the trusted OIDC identity.
  • The ECR login action returns the private registry address.
  • The build command combines the registry, repository, and commit SHA into one image tag.
  • The push command publishes that exact image to Amazon ECR.
  • Save .github/workflows/deploy.yml.

The workflow should now list checkout, AWS credential, ECR login, and image publication steps in the editor.

Do the Steps Look Misaligned?

Each line beginning with - name: must use the same indentation. Keep each uses: or run: line nested beneath its matching name.

Get help comparing the levels: Help me align the authentication and image publication steps in my workflow.

The final section updates the existing Express Mode service with the new image. Its health check targets the API route that returns a successful response.

  • Place your cursor below the final docker push line.
  • Add the deployment and endpoint output steps by pasting:
      - name: Deploy to Amazon ECS Express Mode
        id: deploy
        uses: aws-actions/amazon-ecs-deploy-express-service@v1.2.2
        with:
          service-name: ${{ env.ECS_SERVICE }}
          image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
          execution-role-arn: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/ecsTaskExecutionRole
          infrastructure-role-arn: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/ecsInfrastructureRoleForExpressServices
          cluster: ${{ env.ECS_CLUSTER }}
          container-port: '8080'
          health-check-path: /health
          cpu: '256'
          memory: '512'
          min-task-count: '1'
          max-task-count: '1'

      - name: Print the application endpoint
        run: echo "Endpoint ${{ steps.deploy.outputs.endpoint }}"

What Does the Deployment Configure?

  • The deployment action replaces the bootstrap image with the image published by this workflow.
  • Container port 8080 matches the Uvicorn command in the Dockerfile.
  • The /health route gives the load balancer a dedicated health check.
  • The task receives 256 CPU units and 512 MiB of memory.
  • The final step prints the public application endpoint in the workflow log.
  • Save .github/workflows/deploy.yml.

✔️ Awesome, I've got everything!

Great. Double-check that .github/workflows/deploy.yml is saved before you publish it.

ⓧ I'd like to double check the full code

Compare your saved workflow with this complete file.

name: Deploy Container Learning API

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
  ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
  ECS_SERVICE: ${{ vars.ECS_SERVICE }}
  ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
  IMAGE_TAG: ${{ github.sha }}

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Check out source
        uses: actions/checkout@v7.0.1

      - name: Configure AWS credentials with OIDC
        uses: aws-actions/configure-aws-credentials@v6.3.0
        with:
          role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-actions-ecs-role
          aws-region: ${{ env.AWS_REGION }}
          role-session-name: GitHubActionsECSDeployment

      - name: Log in to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2.1.7

      - name: Build and push the image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        run: |
          docker build -t "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
          docker push "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

      - name: Deploy to Amazon ECS Express Mode
        id: deploy
        uses: aws-actions/amazon-ecs-deploy-express-service@v1.2.2
        with:
          service-name: ${{ env.ECS_SERVICE }}
          image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
          execution-role-arn: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/ecsTaskExecutionRole
          infrastructure-role-arn: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/ecsInfrastructureRoleForExpressServices
          cluster: ${{ env.ECS_CLUSTER }}
          container-port: '8080'
          health-check-path: /health
          cpu: '256'
          memory: '512'
          min-task-count: '1'
          max-task-count: '1'

      - name: Print the application endpoint
        run: echo "Endpoint ${{ steps.deploy.outputs.endpoint }}"
Push and verify the release

The workflow only becomes active after it reaches GitHub. That push becomes the release event that builds a fresh image from version 2.0.0.

  • Return to the integrated PowerShell terminal in Visual Studio Code.

Before you run the release commands, do you expect the public endpoint to keep showing Nginx or switch to your FastAPI response?

  • Publish the workflow to main by running these commands:
git add .
git commit -m "Automate ECS deployment"
git push

What Do These Commands Publish?

  • The first command stages the new workflow file.
  • The second command records the workflow in a Git commit.
  • The final command pushes that commit to main and triggers the workflow.

The deployment can take several minutes while AWS replaces the bootstrap task. A quiet wait during service stabilization is expected.

  • Return to your repository on GitHub.
  • Select Actions in the repository navigation.
  • Open the run named Deploy Container Learning API.
  • Confirm that every workflow step reaches a successful state.
  • Expand Print the application endpoint.
  • Record the printed HTTPS endpoint here: your ECS application endpoint.

Did the Workflow Fail?

  • Check the failed step name before changing any configuration.
  • Compare the repository variables with the AWS Region, account, ECR repository, ECS service, and cluster from the previous step.
  • Confirm that github-actions-ecs-role still trusts this repository's immutable IDs and main branch.

Share the failed step without credentials: Help me diagnose this GitHub Actions deployment failure from the step name and log output.

Before you open the endpoint, which version do you expect from the new cloud release?

  • Open your ECS application endpoint in a new browser tab.

You should see a JSON response from the Container Learning API. Its version field should contain 2.0.0.

  • Append /health to the application endpoint in your browser.

You should see a successful response whose status field says healthy.

Still Seeing the Nginx Page?

Confirm that the workflow completed its deployment step. Reload the endpoint after the ECS service reaches a stable state.

Ask for deployment-focused help: Help me find why my ECS Express endpoint still serves the bootstrap Nginx image.

That is the stale-release problem solved: your public ECS endpoint now serves version 2.0.0 from the pushed commit.

Trace production signals

The ECS service sends application output to Amazon CloudWatch. Its monitoring view reports infrastructure behavior separately.

Before you trigger the controlled failure, what do you expect the API response and service logs to record?

  • Replace /health with /simulate-error in the browser address bar.

You should receive an HTTP 500 response. This failure is the controlled monitoring event defined in main.py.

  • Return to the AWS Management Console from earlier.
  • Open the default cluster in Amazon ECS.
  • Select the container-learning-api service.
  • Select the Logs tab.
  • Locate the request for /simulate-error that reports status 500.

The matching entry proves that the application failure reached the service log stream.

  • Select the service Monitoring view.
  • Locate the CPUUtilization metric.

You should see the service CPU metric for the deployed task.

Three Production Signals

Application logs show individual requests and failures from the FastAPI process.

Infrastructure metrics show resource behavior such as CPU use over time.

GitHub Actions status shows whether the release process completed each build and deployment stage.

Cannot Find the Failed Request?

  • Reload the /simulate-error endpoint to generate another controlled failure.
  • Return to the service Logs tab after the new request.
  • Confirm that you selected the container-learning-api service in the default cluster.

Get help tracing the request: Help me locate my controlled HTTP 500 request in the ECS service logs.

You have closed the release loop. Every push now becomes a traceable ECS release with production evidence you can inspect.

Secret mission

Show the Deployed Commit SHA

Can your live API prove exactly which source revision produced it? Pass the GitHub commit SHA through the deployment workflow so the public `/version` endpoint identifies the running release.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the deployment available, pause its running resources, or delete the project entirely. Your AWS deployment can continue accruing charges until you remove the running service.

Cost warning

Amazon ECS Express Mode has no additional service fee. Its underlying resources continue billing while the service runs.

AWS Fargate, the Application Load Balancer, and public IPv4 addresses contribute to the running cost. At light traffic in US East (N. Virginia), the deployment is estimated at about $0.05 to $0.10 per running hour.

Amazon CloudWatch log storage can add a smaller charge. Amazon ECR image storage can also add a smaller charge.

The Pause and Delete options below stop the main running charges. Prompt cleanup keeps this project bill predictable.

Resources you used:

  • The Amazon ECS Express Mode service named container-learning-api in cluster default.
  • The managed ECS deployment resources, including the Fargate task, load balancer, public IPv4 addresses, networking, scaling configuration, certificate, task definition, and deployment alarms.
  • The private Amazon ECR repository named container-learning-api with its commit-SHA-tagged images.
  • The CloudWatch log group containing the application logs from the ECS service.
  • The AWS Identity and Access Management deployment role named github-actions-ecs-role with its repository-specific permissions.
  • The GitHub OIDC provider for token.actions.githubusercontent.com, if this project created it.
  • The ECS service roles ecsTaskExecutionRole and ecsInfrastructureRoleForExpressServices.
  • The public GitHub repository named container-learning-api with its GitHub Actions workflow and repository variables.
  • The local Docker container named container-learning-api with the local image tagged container-learning-api:1.0.0.

Keep everything running

No action is needed. Choose this if you want the public HTTPS API available for further testing or demonstrations.

  • Your ECS Express Mode service continues running one Fargate task behind its public endpoint.
  • Your AWS account continues accruing charges for the running service resources.
  • Your ECR images and CloudWatch logs remain available for release tracing.
  • Your IAM roles and OIDC provider remain ready for later GitHub Actions deployments.
  • Your GitHub repository remains the source for every future push to main.
  • Your local Docker container and image remain available in Docker Desktop.

Pause - I'll come back to this later

Pausing this project means deleting the ECS Express Mode service to stop its Fargate and load-balancer resources. Your source, images, deployment permissions, and logs stay available for a later deployment.

  • Return to Amazon ECS in the AWS Management Console.
  • Open the cluster named default.
  • Choose the Services tab.
  • Select the service named container-learning-api.
  • Choose Delete.
  • Enter delete in the confirmation field.
  • Confirm the service deletion.

The resource-deletion page can take a few minutes to settle. Its changing status means ECS is removing the managed stack.

  • Monitor the progress page until the Fargate and load-balancer resources are listed as removed.
  • Review the retained-resources list to identify anything that remains in your account.
  • Switch back to Docker Desktop from earlier.
  • Stop the local container-learning-api container if it is still running.
  • Keep the GitHub repository, ECR images, IAM resources, and any retained CloudWatch logs for your return.
  • Expect small storage charges if ECR images or CloudWatch logs remain in the account.

Delete - I don't want to use this again

Deleting this stack can feel drastic. Each layer is listed separately so you stay in control of what disappears.

  • Return to Amazon ECS in the AWS Management Console.
  • Open the cluster named default.
  • Choose the Services tab.
  • Select the service named container-learning-api.
  • Choose Delete.
  • Enter delete in the confirmation field.
  • Confirm the service deletion.

ECS may need several minutes to remove the generated resources. The progress page tracks each resource as the deletion completes.

  • Monitor the progress page until the service resources are listed as removed.
  • Review the same page for retained resources.
  • Open the linked CloudWatch log group if the page lists it as retained.
  • Remove that log group with its deletion control.
  • Remove any other project resource that the deletion page lists as retained.
  • Return to the Amazon ECR console.
  • Select Private repositories.
  • Select the repository named container-learning-api.
  • Remove the repository together with all commit-SHA-tagged images by using its repository deletion control.
  • Return to AWS Identity and Access Management.
  • Open the role named github-actions-ecs-role.
  • Remove the repository-specific ECR and ECS Express permissions from the role.
  • Delete the role named github-actions-ecs-role.
  • Delete any separate customer-managed policy that you created only for this deployment role.
  • Open Identity providers in IAM.
  • Select the provider for token.actions.githubusercontent.com.
  • Delete the provider only when no other GitHub workflow in the account uses it.
  • Delete ecsTaskExecutionRole when no remaining ECS service uses it.
  • Delete ecsInfrastructureRoleForExpressServices when no remaining ECS Express service uses it.
  • Return to the container-learning-api repository on GitHub.
  • Open the repository deletion area in its settings.
  • Permanently remove the repository to delete its workflow and repository variables.
  • Switch back to Docker Desktop from earlier.
  • Remove the local container named container-learning-api with its deletion control.
  • Select the local image tagged container-learning-api:1.0.0.
  • Remove the selected image with its deletion control.
  • Confirm that the ECS service no longer appears in cluster default.
  • Confirm that Amazon ECR no longer lists the container-learning-api repository.
  • Confirm that Docker Desktop no longer lists the local container or image.

Nice Work!

Nice Work!

You did it! Your versioned FastAPI service now reaches a public Amazon ECS Express Mode endpoint through an automated GitHub Actions release.

What you learned:

  • Packaged pinned Python dependencies inside a repeatable Docker image. You proved image immutability when changing the source to 2.0.0 left the existing 1.0.0 container unchanged.
  • Built a secure CI/CD release from each push to main. OIDC provides temporary AWS credentials without stored access keys. Each commit produces a SHA-tagged image in Amazon ECR before deployment.
  • Confirmed the live HTTPS API returned version 2.0.0. You found the controlled 500 request in Amazon CloudWatch logs. You also inspected CPUUtilization beside the service deployment status.
  • Secret Mission: Passed the exact GitHub commit SHA into APP_VERSION. The live /version endpoint now identifies the source revision running in AWS.

Ready to quiz yourself?