Pickleball Playability API on ECS Fargate

Containerize a Python API and deploy it to AWS ECS Fargate with CI/CD.

Introduction

30 Second Summary

You check the weather before heading to the pickleball court. Too windy? Too hot? Chance of rain? You make a quick judgment call on whether conditions are worth playing in.

In this project, you will build a containerized REST API that makes that judgment call for you. Your API fetches live weather data, returns a playability score, and deploys to the cloud with fully automated CI/CD.

What You'll Build

A Python Flask API that calculates pickleball playability from real-time weather, containerized with Docker, deployed to ECS Fargate, and automatically redeployed via GitHub Actions.

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

  • A containerized Python REST API running on AWS ECS Fargate, publicly accessible via its task IP.
  • A fully automated CI/CD pipeline that builds, pushes, and deploys your container on every push to main.
  • A GitHub repository with a production-grade project structure including a Dockerfile, task definition, and GitHub Actions workflow.
  • Secret Mission: Add CloudWatch container logging and a health endpoint for operational monitoring.

Are there any prerequisites?

You'll need an AWS account with the AWS CLI configured, Docker Desktop installed, a GitHub account, and basic familiarity with Python and Git commands (init, add, commit, push).

Step 1 will verify that everything is set up correctly before you start building.

Set Up Your Project

Before you can containerize and deploy your Pickleball Playability API, you need a solid foundation. This step gets your local tools verified, your code repository created, and your cloud image registry ready to receive Docker images.

Think of it like setting up a kitchen before cooking. You need your oven working (Docker), your ingredients prepped (Python and AWS CLI), and your serving counter ready (ECR repository).

In this step, get ready to:
  • Verify Docker, Python, and AWS CLI are installed and configured.
  • Create a GitHub repository and local project structure.
  • Create an ECR repository to store your Docker images.
Verify your tools are ready

You need three tools working before you write any code. Let's confirm each one is installed and configured correctly.

First, make sure Docker Desktop is running on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Docker Desktop and press Enter to open it.
  • Wait for the Docker icon in your system tray to show a steady state (no animation).
  • Verify Docker is running by opening a terminal and running this command:
docker --version

✔️ I see a version number

Docker is installed and the CLI is accessible. You're good to move on.

ⓧ Command not found or Docker not running

Make sure Docker Desktop is open and fully started. The Docker daemon needs to be running for the CLI to work.

  • Check your system tray for the Docker whale icon.
  • If the icon shows an animation, wait for it to finish starting.
  • If Docker Desktop is not installed, download it from docker.com/products/docker-desktop.

Next, verify that Python 3.12 is installed. This project's Docker image uses python:3.12-slim and the dependencies require Python 3.10 or higher locally.

  • Check your Python version by running this command:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.12 or higher

Python is ready. Move on to the AWS CLI check below.

ⓧ I see an older version

Your system has an older Python that won't work with this project's dependencies.

  • Download Python 3.12 from python.org/downloads.
  • Run the installer and follow the prompts.
  • On macOS, the system python3 may still point to the old version after installation. Use python3.12 --version to confirm the new install, or use a version manager like pyenv to set 3.12 as your default.
  • On Windows, check the Add Python to PATH box during installation. Then restart your terminal and run python --version again.

ⓧ Command not found

Python is not installed on your system.

  • Download Python 3.12 from python.org/downloads.
  • Run the installer for your operating system.
  • On Windows, check the Add Python to PATH box during installation.
  • Restart your terminal, then rerun the version check command.

Finally, verify your AWS CLI is configured and can reach your account.

  • Confirm your AWS identity by running this command:
aws sts get-caller-identity

✔️ I see my account ID and ARN

Your AWS CLI is configured and authenticated. You should see a JSON response with your Account, UserId, and Arn fields.

ⓧ I see an error or credentials not found

Your AWS CLI is not configured or your credentials have expired.

  • Run aws configure and enter your Access Key ID, Secret Access Key, default region (us-east-1), and output format (json).
  • If you don't have access keys, create them in the AWS Console under IAM > Users > your user > Security credentials.
  • After configuring, run aws sts get-caller-identity again to confirm it works.

Still seeing an error?

If aws sts get-caller-identity returns an "ExpiredToken" or "InvalidClientTokenId" error, your credentials may have rotated. Regenerate your access keys in the IAM console and run aws configure again.

help me fix my AWS CLI credentials error

Create your GitHub repository and project structure

Your project needs a GitHub repository so you can set up CI/CD automation later. You'll create the repo on GitHub, clone it to your Desktop, and scaffold the folder structure.

  • Open github.com in your browser and sign in.
  • Click the + icon in the top-right corner, then select New repository.
  • Enter pickleball-advisor as the Repository name.
  • Leave the repository set to Public.
  • Do NOT check any initialization options (no README, no .gitignore, no license).
  • Click Create repository.

Why leave the repo empty?

An empty repo gives you a clean start to push your local files without merge conflicts. GitHub will show you the clone URL on the next page.

Now clone the repository to your Desktop and set up the project folders.

  • Copy the HTTPS clone URL from the GitHub page (it looks like https://github.com/your-username/pickleball-advisor.git).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone your repository by running this command:
git clone [[YOUR_REPO_URL="https://github.com/your-username/pickleball-advisor.git"]]
  • Move into the project folder by running:
cd pickleball-advisor
  • Create the project folder structure by running these commands:

macOS

mkdir app
touch Dockerfile requirements.txt

Windows

mkdir app
New-Item Dockerfile -ItemType File
New-Item requirements.txt -ItemType File
  • Confirm the files exist by running:
ls

You should see app, Dockerfile, and requirements.txt in the output.

Don't see all three items?

Make sure you ran the commands from inside the pickleball-advisor folder. Run pwd (macOS) or Get-Location (Windows) to confirm your current directory ends with pickleball-advisor.

help me fix my project folder structure

Create your ECR repository

Amazon ECR (Elastic Container Registry) stores your Docker images in the cloud. ECS Fargate will pull from this registry when it runs your container.

  • Create your ECR repository by running this command:
aws ecr create-repository --repository-name pickleball-advisor --region us-east-1

What does this command do?

This creates a private container image registry named pickleball-advisor in the us-east-1 region. When you build your Docker image later, you'll push it here so ECS Fargate can pull and run it.

  • Verify the repository was created by running:
aws ecr describe-repositories --repository-names pickleball-advisor

You should see JSON output containing repositoryName: pickleball-advisor and a repositoryUri field with your account ID.

Seeing an AccessDeniedException?

Your IAM user may not have ECR permissions. Make sure your AWS credentials belong to a user with the AmazonEC2ContainerRegistryFullAccess managed policy attached.

help me fix my ECR permissions error

Your tools are verified, your repo is cloned, and your image registry is waiting. Next up, you'll build the actual Pickleball Playability API that calculates weather-based scores.

Build the Pickleball Playability API

Your project structure is ready and your ECR repository is waiting for images. Now it is time to write the actual application that will run inside your container.

The API fetches real-time weather data and calculates a pickleball playability score from 1 to 10. It uses a weighted scoring system that factors in temperature, wind speed, precipitation probability, and dewpoint. By the end of this step you will have a working API running locally that you can test in your browser.

In this step, get ready to:
  • Write the Flask application with a playability scoring endpoint.
  • Pin the project dependencies in a requirements file.
  • Test the API locally and confirm it returns playability JSON.
Write the playability scoring logic

The core of this API is a scoring algorithm that takes four weather inputs and returns a single playability number. You will write this in app/main.py along with a recommendation function that maps scores to Phineas-and-Ferb-style messages.

  • Open app/main.py in VS Code (it should be empty) and paste the following code:
import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"


def calculate_playability(temperature_f, wind_mph, precip_probability, dewpoint_f):
    """Calculate pickleball playability score (1-10) based on weather conditions."""
    # Temperature score (2x weight): peak at 72-76F, penalty outside
    if 72 <= temperature_f <= 76:
        temp_score = 10
    elif 60 <= temperature_f <= 90:
        temp_score = 10 - (min(abs(temperature_f - 74), 16) / 16) * 6
    elif 40 <= temperature_f < 60:
        temp_score = 4 - ((60 - temperature_f) / 20) * 3
    elif temperature_f > 90:
        temp_score = 4 - ((temperature_f - 90) / 23) * 3
    else:
        temp_score = 1

    # Wind score (2x weight): 10 up to 8 mph, ramp down to 1 at 25 mph
    if wind_mph <= 8:
        wind_score = 10
    elif wind_mph >= 25:
        wind_score = 1
    else:
        wind_score = 10 - ((wind_mph - 8) / 17) * 9

    # Precipitation score (2x weight): penalty starts at 15%, severe above 50%
    if precip_probability <= 15:
        precip_score = 10
    elif precip_probability >= 80:
        precip_score = 1
    else:
        precip_score = 10 - ((precip_probability - 15) / 65) * 9

    # Dewpoint/humidity score (1x weight): lower dewpoint is better
    if dewpoint_f <= 55:
        humidity_score = 10
    elif dewpoint_f >= 75:
        humidity_score = 1
    else:
        humidity_score = 10 - ((dewpoint_f - 55) / 20) * 9

    # Weighted average: temp(2x) + wind(2x) + precip(2x) + humidity(1x)
    total = (temp_score * 2 + wind_score * 2 + precip_score * 2 + humidity_score * 1) / 7
    return round(max(1, min(10, total)), 1)


def get_recommendation(score):
    """Return a Phineas-and-Ferb-style recommendation based on score."""
    if score >= 8:
        return "Ferb, I know what we're gonna do today! Perfect pickleball weather!"
    elif score >= 6:
        return "Hey Ferb, grab the paddles! Decent conditions out there."
    elif score >= 4:
        return "Ferb, maybe we should build something indoors first..."
    else:
        return "Aren't you a little young to be playing in this weather? Stay inside!"

What does this code do?

This sets up the foundation of your API. The calculate_playability function scores four weather factors on a 1-10 scale, then combines them with weights: temperature, wind, and precipitation each count double, while humidity counts once.

The get_recommendation function turns that numeric score into a fun message. Scores of 8+ get the iconic Phineas catchphrase, while low scores warn the player to stay inside.

You will not see a change yet. The next chunk adds the HTTP endpoint that calls these functions.

Add the API route handlers

Now you need the HTTP layer. The /score endpoint accepts latitude and longitude, fetches live weather from the Open-Meteo API, runs the scoring logic, and returns JSON.

  • In the same app/main.py file, paste the following code below the get_recommendation function:
@app.route("/score")
def score():
    latitude = request.args.get("latitude")
    longitude = request.args.get("longitude")

    if not latitude or not longitude:
        return jsonify({"error": "latitude and longitude query parameters are required"}), 400

    try:
        latitude = float(latitude)
        longitude = float(longitude)
    except ValueError:
        return jsonify({"error": "latitude and longitude must be numeric"}), 400

    # Fetch current weather from Open-Meteo
    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": "temperature_2m,wind_speed_10m,precipitation_probability,dew_point_2m",
        "temperature_unit": "fahrenheit",
        "wind_speed_unit": "mph",
    }

    try:
        response = requests.get(OPEN_METEO_URL, params=params, timeout=10)
        response.raise_for_status()
    except requests.RequestException as e:
        return jsonify({"error": f"Weather API request failed: {str(e)}"}), 502

    weather_data = response.json()
    current = weather_data.get("current", {})

    temperature_f = current.get("temperature_2m", 70)
    wind_mph = current.get("wind_speed_10m", 0)
    precip_probability = current.get("precipitation_probability", 0)
    dewpoint_f = current.get("dew_point_2m", 50)

    playability = calculate_playability(temperature_f, wind_mph, precip_probability, dewpoint_f)
    recommendation = get_recommendation(playability)

    return jsonify({
        "playability_score": playability,
        "recommendation": recommendation,
        "weather": {
            "temperature_f": temperature_f,
            "wind_mph": wind_mph,
            "precipitation_probability_pct": precip_probability,
            "dewpoint_f": dewpoint_f,
        },
        "location": {
            "latitude": latitude,
            "longitude": longitude,
        },
    })


@app.route("/")
def index():
    return jsonify({
        "service": "Pickleball Playability Advisor",
        "usage": "GET /score?latitude=38.89&longitude=-77.03",
    })

What does this code do?

The /score route validates the query parameters, calls the Open-Meteo forecast API for current conditions in Fahrenheit and mph, then passes those values to calculate_playability. It returns everything as structured JSON.

The / index route is a convenience endpoint that tells anyone who hits the base URL how to use the API. This is a common pattern for production APIs.

Why Open-Meteo instead of OpenWeatherMap?

Open-Meteo requires no API key, no signup, and allows 10,000 free calls per day. This removes all friction so you can focus on the Docker and deployment concepts instead of managing credentials for a weather service.

  • Save app/main.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"


def calculate_playability(temperature_f, wind_mph, precip_probability, dewpoint_f):
    """Calculate pickleball playability score (1-10) based on weather conditions."""
    if 72 <= temperature_f <= 76:
        temp_score = 10
    elif 60 <= temperature_f <= 90:
        temp_score = 10 - (min(abs(temperature_f - 74), 16) / 16) * 6
    elif 40 <= temperature_f < 60:
        temp_score = 4 - ((60 - temperature_f) / 20) * 3
    elif temperature_f > 90:
        temp_score = 4 - ((temperature_f - 90) / 23) * 3
    else:
        temp_score = 1

    if wind_mph <= 8:
        wind_score = 10
    elif wind_mph >= 25:
        wind_score = 1
    else:
        wind_score = 10 - ((wind_mph - 8) / 17) * 9

    if precip_probability <= 15:
        precip_score = 10
    elif precip_probability >= 80:
        precip_score = 1
    else:
        precip_score = 10 - ((precip_probability - 15) / 65) * 9

    if dewpoint_f <= 55:
        humidity_score = 10
    elif dewpoint_f >= 75:
        humidity_score = 1
    else:
        humidity_score = 10 - ((dewpoint_f - 55) / 20) * 9

    total = (temp_score * 2 + wind_score * 2 + precip_score * 2 + humidity_score * 1) / 7
    return round(max(1, min(10, total)), 1)


def get_recommendation(score):
    """Return a Phineas-and-Ferb-style recommendation based on score."""
    if score >= 8:
        return "Ferb, I know what we're gonna do today! Perfect pickleball weather!"
    elif score >= 6:
        return "Hey Ferb, grab the paddles! Decent conditions out there."
    elif score >= 4:
        return "Ferb, maybe we should build something indoors first..."
    else:
        return "Aren't you a little young to be playing in this weather? Stay inside!"


@app.route("/score")
def score():
    latitude = request.args.get("latitude")
    longitude = request.args.get("longitude")

    if not latitude or not longitude:
        return jsonify({"error": "latitude and longitude query parameters are required"}), 400

    try:
        latitude = float(latitude)
        longitude = float(longitude)
    except ValueError:
        return jsonify({"error": "latitude and longitude must be numeric"}), 400

    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": "temperature_2m,wind_speed_10m,precipitation_probability,dew_point_2m",
        "temperature_unit": "fahrenheit",
        "wind_speed_unit": "mph",
    }

    try:
        response = requests.get(OPEN_METEO_URL, params=params, timeout=10)
        response.raise_for_status()
    except requests.RequestException as e:
        return jsonify({"error": f"Weather API request failed: {str(e)}"}), 502

    weather_data = response.json()
    current = weather_data.get("current", {})

    temperature_f = current.get("temperature_2m", 70)
    wind_mph = current.get("wind_speed_10m", 0)
    precip_probability = current.get("precipitation_probability", 0)
    dewpoint_f = current.get("dew_point_2m", 50)

    playability = calculate_playability(temperature_f, wind_mph, precip_probability, dewpoint_f)
    recommendation = get_recommendation(playability)

    return jsonify({
        "playability_score": playability,
        "recommendation": recommendation,
        "weather": {
            "temperature_f": temperature_f,
            "wind_mph": wind_mph,
            "precipitation_probability_pct": precip_probability,
            "dewpoint_f": dewpoint_f,
        },
        "location": {
            "latitude": latitude,
            "longitude": longitude,
        },
    })


@app.route("/")
def index():
    return jsonify({
        "service": "Pickleball Playability Advisor",
        "usage": "GET /score?latitude=38.89&longitude=-77.03",
    })
Pin the project dependencies

Your API imports two third-party libraries: Flask for the web framework and requests for calling the weather API. You also need gunicorn as the production server for when you containerize later.

  • Open requirements.txt (it should be empty) and paste the following:
flask==3.1.3
requests==2.34.2
gunicorn==26.0.0

Why pin exact versions?

Pinning with == ensures your container builds are reproducible. Every build installs the exact same packages, eliminating "it works on my machine" surprises.

  • Save requirements.txt.

Not sure if you typed the versions correctly?

Double check that each line has no spaces around the == sign and matches the exact version numbers above. A typo here will cause pip install to fail in the next step.

Help me check my requirements.txt for errors.

Test the API locally

Before containerizing, you should confirm the API works by running it with Flask's built-in development server. This catches any typos or logic errors early.

  • Install the dependencies by running this command:
pip install -r requirements.txt

Seeing permission errors or pip not found?

If pip is not recognized, try pip3 instead. On some systems the command maps to a different Python version.

Help me fix my pip installation issue.

  • Start the Flask development server by running:
python -m flask --app app.main run --port 5000

What does this command do?

The --app app.main flag tells Flask to look for the application inside the app/ directory in the main.py file. The --port 5000 flag sets which port the server listens on.

You should see output like Running on http://127.0.0.1:5000 confirming the server started.

  • Open a new terminal tab (keep the server running in the first tab).
  • Test the /score endpoint by running:
curl http://localhost:5000/score?latitude=38.89&longitude=-77.03

You should see a JSON response containing a playability_score number, a recommendation message, weather data, and location coordinates. The exact score depends on the current weather in Washington, D.C.

Test in your browser too

You can also paste http://localhost:5000/score?latitude=38.89&longitude=-77.03 into your browser's address bar to see the formatted JSON response.

Getting a connection refused error?

Make sure the Flask server is still running in your other terminal tab. If you closed it or see a traceback, switch back to that tab and run the python -m flask command again.

On macOS, port 5000 may be used by AirPlay Receiver. If the server cannot start, go to System Settings, search for AirPlay Receiver, and toggle it off.

Help me debug why my Flask server won't start or respond.

  • Once you have confirmed the JSON response, stop the Flask server by pressing Ctrl+C in the terminal where it is running.

Your API is working locally. Next up, you will package it into a Docker container so it can run anywhere.

Containerize with Docker

Your API is returning live pickleball scores from a local Python process. The problem is that "works on my machine" does not help when you need to deploy to the cloud.

Docker solves this by packaging your code, dependencies, and runtime into one portable unit called a container. In this step, you'll write a Dockerfile, build an image, and prove the containerized API works identically to the local version.

In this step, get ready to:
  • Write a production Dockerfile for the Flask API.
  • Build the Docker image locally and verify it exists.
  • Run the container and confirm the /score endpoint responds.
Write the Dockerfile

A Dockerfile is a recipe that tells Docker exactly how to build your image. Each instruction creates a layer, and Docker caches layers that haven't changed to speed up future builds.

  • Open the empty Dockerfile in VS Code (it's at the root of your pickleball-advisor folder).
  • Paste the following content into the file:
# Use a minimal Python base image
FROM python:3.12-slim

# Set the working directory inside the container
WORKDIR /app

# Copy requirements first to leverage Docker layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the application code
COPY app/ ./app/

# Document which port the container listens on
EXPOSE 5000

# Start gunicorn serving the Flask app
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app.main:app"]

What does this Dockerfile do?

  • FROM python:3.12-slim starts from a lightweight Python image. The slim variant strips out compilers and docs you don't need at runtime.
  • COPY requirements.txt is copied separately before the app code. This means Docker only re-runs pip install when dependencies change, not on every code edit.
  • CMD runs gunicorn as the production server instead of Flask's built-in dev server. This is what you'd use in a real deployment.

Why gunicorn instead of Flask's dev server?

Flask's built-in server handles one request at a time and is designed for local development only. gunicorn is a production-grade WSGI server that can handle concurrent requests. Using it in your Dockerfile proves you understand the difference between dev and production serving.

  • Save Dockerfile.
  • You won't see a visible change yet. The next substep builds this file into a runnable image.

Worried about typos in the Dockerfile?

Typos in a Dockerfile only surface when you run docker build in the next substep. Double-check that COPY app/ ./app/ has the trailing slashes and that CMD uses square brackets with double quotes around each argument.

help me fix my Dockerfile syntax

✔️ Awesome, I've got everything!

Great. Make sure you've saved Dockerfile before moving on to the build step.

ⓧ I'd like to double check the full code

Here's the complete Dockerfile for comparison:

FROM python:3.12-slim

WORKDIR /app

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

COPY app/ ./app/

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app.main:app"]
Build the Docker image

Building an image executes every instruction in the Dockerfile and produces a tagged snapshot you can run as a container.

  • Make sure your terminal is in the pickleball-advisor folder (the one containing the Dockerfile).
  • Build the image by running this command:
docker build -t pickleball-advisor .

Docker downloads the python:3.12-slim base image (first time only), installs your pip packages, and copies your app code. This takes about a minute on the first run.

  • Verify the image was created by running:
docker images

You should see pickleball-advisor in the list with the latest tag and a size around 150-180 MB.

Build failed with an error?

  • If you see a "no such file or directory" error, confirm your terminal is in the pickleball-advisor root folder (not inside app/). The . at the end of docker build means "use the current directory as context."
  • If Docker cannot connect, make sure Docker Desktop is running. You can check with docker info in your terminal.
  • If pip install fails inside the build, check that requirements.txt is in the project root (same level as the Dockerfile).

help me debug my Docker build error

Run and test the container

An image is a snapshot. A container is a running instance of that image. Time to spin one up and confirm it serves your API.

  • If your local Flask server from the previous step is still running on port 5000, stop it first with Ctrl+C in that terminal.
  • Run the container by executing this command:
docker run -p 5000:5000 pickleball-advisor

What does -p 5000:5000 mean?

The -p flag maps a port on your machine (left) to a port inside the container (right). Since the container exposes port 5000, this makes it accessible at localhost:5000 on your machine.

  • Open a new terminal tab or window (the container keeps the current one busy).
  • Test the containerized API by running this command:
curl http://localhost:5000/score?latitude=38.89&longitude=-77.03

You should see the same JSON response you got when testing locally: a playability_score, recommendation, weather data, and location coordinates.

  • Once you've confirmed the response, stop the container by pressing Ctrl+C in the terminal where it's running.

Not getting a response from the container?

  • If you see "port already in use," another process is on port 5000. Stop any local Flask server first, or check for other containers with docker ps.
  • If curl returns "connection refused," wait a few seconds for gunicorn to start. You should see log output in the container terminal before testing.
  • On Windows, if curl is not available, open http://localhost:5000/score?latitude=38.89&longitude=-77.03 in your browser instead.

help me troubleshoot my Docker container

Your API is now sealed inside a portable container image. It runs the exact same way regardless of the host machine. Next up, you'll push this image to AWS and deploy it on ECS Fargate so it's accessible from the internet.

Deploy to ECS Fargate

Your container works locally. Now it needs to run in the cloud where anyone can reach it.

In this step, you will push your Docker image to Amazon ECR, then deploy it on ECS Fargate so it runs as a managed container with its own public IP address.

In this step, get ready to:
  • Push your Docker image to ECR.
  • Create an ECS cluster and task definition.
  • Launch a Fargate service and confirm your API is live in the cloud.
Push your Docker image to ECR

Before ECS can run your container, the image needs to live in a container registry that AWS can pull from. You already created the pickleball-advisor ECR repository in Step 1. Now you will authenticate Docker, tag your image, and push it.

  • Authenticate Docker to your ECR registry by running this command:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com

What does this command do?

This pipes a temporary authentication token from AWS into Docker's login command. Docker is now authorized to push and pull images from your private ECR registry for 12 hours.

  • You should see Login Succeeded in your terminal output.
  • Tag your local image with the ECR repository URI by running:
docker tag pickleball-advisor:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/pickleball-advisor:latest
  • Push the tagged image to ECR by running:
docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/pickleball-advisor:latest

What does this command do?

Docker uploads each layer of your image to your private ECR repository. You will see progress bars for each layer as it uploads.

  • Confirm the image is in ECR by running:
aws ecr describe-images --repository-name pickleball-advisor --region us-east-1
  • You should see an imageDetails array containing your image with the latest tag.

Seeing an authorization error?

Make sure Docker Desktop is running. The aws ecr get-login-password command requires both Docker and valid AWS credentials.

If you see a denied or token error, re-run aws sts get-caller-identity to confirm your credentials are valid, then retry the login command.

Help me troubleshoot my ECR push failure

Create the ECS cluster and task definition

With your image in ECR, you need the infrastructure to run it. An ECS cluster is a logical grouping of tasks, and a task definition tells ECS exactly how to run your container.

  • Create the ECS cluster by running:
aws ecs create-cluster --cluster-name pickleball-cluster

What is an ECS cluster?

A cluster is a logical boundary that groups your running containers. Think of it as a namespace. With Fargate, you do not manage any underlying servers. AWS handles the compute for you.

  • You should see a JSON response with "clusterName": "pickleball-cluster" and "status": "ACTIVE".
  • Create the task definition file. In VS Code, create a new file called task-definition.json in the root of your pickleball-advisor folder and paste the following:
{
    "family": "pickleball-advisor",
    "networkMode": "awsvpc",
    "requiresCompatibilities": ["FARGATE"],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "name": "pickleball-advisor",
            "image": "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/pickleball-advisor:latest",
            "portMappings": [
                {
                    "containerPort": 5000,
                    "hostPort": 5000,
                    "protocol": "tcp"
                }
            ],
            "essential": true
        }
    ]
}
  • Replace both instances of ACCOUNT_ID in the file with your actual 12-digit AWS account ID.

What does this task definition do?

  • "family" names this task definition so you can reference it later.
  • "requiresCompatibilities": ["FARGATE"] tells ECS to run this on serverless compute.
  • "cpu": "256" allocates 0.25 vCPU and "memory": "512" allocates 512 MB. This is the smallest Fargate configuration.
  • "networkMode": "awsvpc" gives your container its own network interface with a private IP.
  • "executionRoleArn" references the IAM role that lets ECS pull your image from ECR.

Find your account ID quickly

Run aws sts get-caller-identity --query Account --output text to print just your 12-digit account ID.

  • Save task-definition.json.

✔️ Awesome, I've got everything!

Great. Double check you have saved task-definition.json and replaced both ACCOUNT_ID placeholders.

ⓧ I'd like to double check the full code

Here is the complete task-definition.json with ACCOUNT_ID as a placeholder you need to replace:

{
    "family": "pickleball-advisor",
    "networkMode": "awsvpc",
    "requiresCompatibilities": ["FARGATE"],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "name": "pickleball-advisor",
            "image": "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/pickleball-advisor:latest",
            "portMappings": [
                {
                    "containerPort": 5000,
                    "hostPort": 5000,
                    "protocol": "tcp"
                }
            ],
            "essential": true
        }
    ]
}
Register the task definition and launch the service

You have the cluster and the task definition file. Now you will register the definition with ECS, set up networking, and create a service that keeps one container running.

  • Register the task definition with ECS by running:
aws ecs register-task-definition --cli-input-json file://task-definition.json
  • You should see a JSON response containing "taskDefinitionArn" with pickleball-advisor:1 at the end.

What does registering a task definition do?

Registering stores your task definition in ECS so that services and tasks can reference it by family name and revision number. Each time you register an update, ECS creates a new revision (e.g., pickleball-advisor:2).

Before you can create the service, ECS needs a security group that allows inbound traffic on port 5000. You also need a public subnet ID from your default VPC.

  • Find a public subnet in your default VPC by running:
aws ec2 describe-subnets --filters "Name=default-for-az,Values=true" --query "Subnets[0].SubnetId" --output text
  • Copy the subnet ID that is returned (it looks like subnet-0abc123...). You will use it in the create-service command.
  • Create a security group that will allow traffic to your container by running:
aws ec2 create-security-group --group-name pickleball-sg --description "Allow inbound on port 5000"
  • Copy the GroupId from the output (it looks like sg-0abc123...).
  • Open port 5000 on the security group by running:
aws ec2 authorize-security-group-ingress --group-id [[SG_ID="your security group ID from above"]] --protocol tcp --port 5000 --cidr 0.0.0.0/0

What does this networking setup do?

The security group acts as a firewall for your container. The ingress rule allows anyone (0.0.0.0/0) to send TCP traffic on port 5000. Without this, your container would run but reject all incoming requests.

The public subnet ensures your task gets a public IP address that is reachable from the internet.

  • Create the ECS service by running the following command, replacing the subnet ID and security group ID with your values:
aws ecs create-service \
  --cluster pickleball-cluster \
  --service-name pickleball-service \
  --task-definition pickleball-advisor \
  --launch-type FARGATE \
  --desired-count 1 \
  --network-configuration "awsvpcConfiguration={subnets=[[[SUBNET_ID="your subnet ID"]]],securityGroups=[[[SG_ID="your security group ID"]]],assignPublicIp=ENABLED}"

What does this command do?

  • --cluster pickleball-cluster tells ECS which cluster to deploy into.
  • --task-definition pickleball-advisor references the task definition you just registered.
  • --launch-type FARGATE means AWS manages the compute. No EC2 instances to configure.
  • --desired-count 1 keeps exactly one task running at all times.
  • assignPublicIp=ENABLED gives the task a public IP so you can reach it from the internet.
  • Wait for the service to stabilize by running:
aws ecs wait services-stable --cluster pickleball-cluster --services pickleball-service

What does this wait command do?

This command polls ECS every 15 seconds until your service reports that the task is running and healthy. It will return silently when the service is stable. This can take 2-3 minutes while AWS provisions the network interface and pulls your image.

  • Once the wait completes, find your task's public IP. First, get the task ARN by running:
aws ecs list-tasks --cluster pickleball-cluster --service-name pickleball-service --query "taskArns[0]" --output text
  • Copy the full task ARN from the output.
  • Get the ENI (network interface) ID attached to the task by running:
aws ecs describe-tasks --cluster pickleball-cluster --tasks [[TASK_ARN="your task ARN from above"]] --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text
  • Copy the ENI ID (it looks like eni-0abc123...).
  • Get the public IP from the ENI by running:
aws ec2 describe-network-interfaces --network-interface-ids [[ENI_ID="your ENI ID from above"]] --query "NetworkInterfaces[0].Association.PublicIp" --output text
  • You should see a public IP address printed (e.g., 3.95.12.44).
  • Test your cloud-deployed API by running:
curl http://[[TASK_PUBLIC_IP="your task public IP"]]:5000/score?latitude=38.89&longitude=-77.03
  • You should see the same playability JSON response you saw when testing locally. Your API is now running in the cloud.

Service not stabilizing or curl timing out?

If the wait command takes more than 5 minutes, check the service events for error messages by running aws ecs describe-services --cluster pickleball-cluster --services pickleball-service --query "services[0].events[:3]".

Common causes: the ecsTaskExecutionRole IAM role does not exist (create it in the IAM console with the AmazonECSTaskExecutionRolePolicy attached), the security group does not allow port 5000, or the account ID in your task definition is incorrect.

Help me debug my ECS Fargate deployment

Your API is now live on AWS. Next up, you will automate the entire build-push-deploy cycle so every code change deploys automatically.

Automate with GitHub Actions CI/CD

Your API is running on ECS Fargate, but every time you change your code you would need to manually rebuild the Docker image, push it to ECR, and update the service. That process has four or five commands and leaves room for human error.

In this step, you will set up a GitHub Actions CI/CD pipeline that automates the entire build-push-deploy cycle every time you push code to main. One push, and your changes are live.

In this step, get ready to:
  • Create an IAM user with permissions for ECR and ECS, and store its credentials in GitHub.
  • Write a GitHub Actions workflow that builds, pushes, and deploys your container automatically.
  • Push a code change and watch the full CI/CD pipeline run end to end.
Create an IAM user for GitHub Actions

GitHub Actions needs AWS credentials to push images to ECR and update your ECS service. You will create a dedicated IAM user with only the permissions the workflow requires.

  • Create a new IAM user called github-actions-deployer by running this command:
aws iam create-user --user-name github-actions-deployer
  • Create a policy file that grants ECR push/pull and ECS deploy permissions. Create a file called github-actions-policy.json in your project folder with this content:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:BatchGetImage",
                "ecr:GetDownloadUrlForLayer"
            ],
            "Resource": "arn:aws:iam::*:role/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecs:RegisterTaskDefinition",
                "ecs:UpdateService",
                "ecs:DescribeServices",
                "ecs:DescribeTaskDefinition"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*"
        }
    ]
}

What does this policy do?

The policy grants four groups of permissions:

  • ecr:GetAuthorizationToken lets the workflow authenticate to your ECR registry.
  • The ECR image actions (BatchCheckLayerAvailability, PutImage, etc.) allow pushing and pulling Docker image layers.
  • The ECS actions (RegisterTaskDefinition, UpdateService) let the workflow register new task definitions and roll out updates.
  • iam:PassRole allows the workflow to assign the ecsTaskExecutionRole to new task definitions.
  • Attach the policy to your new user by running:
aws iam put-user-policy \
    --user-name github-actions-deployer \
    --policy-name ECSDeployPolicy \
    --policy-document file://github-actions-policy.json
  • Create an access key for the user by running:
aws iam create-access-key --user-name github-actions-deployer

Save these credentials immediately

The output includes an AccessKeyId and SecretAccessKey. Copy both values now. The secret access key is shown only once and cannot be retrieved again.

Now store these credentials as GitHub repository secrets so the workflow can use them.

  • Open your pickleball-advisor repository on GitHub.
  • Click **Settings** in the top navigation bar.
  • In the **Security** section of the left sidebar, select **Secrets and variables**, then click **Actions**.
  • Click the **Secrets** tab.
  • Click **New repository secret**.
  • In the **Name** field, enter AWS_ACCESS_KEY_ID.
  • In the **Secret** field, paste your access key ID value.
  • Click **Add secret**.
  • Click **New repository secret** again.
  • In the **Name** field, enter AWS_SECRET_ACCESS_KEY.
  • In the **Secret** field, paste your secret access key value.
  • Click **Add secret**.
  • Confirm you now see both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY listed on the **Actions secrets** page.

Don't see the Settings tab?

If the **Settings** tab is not visible, click the dropdown menu (three dots) next to the tab bar and select **Settings** from there. You need repository owner or admin access to manage secrets.

help me find the Settings tab on my GitHub repository

Create the GitHub Actions workflow file

GitHub Actions workflows live in the .github/workflows/ directory. The workflow you create will trigger on every push to main and handle the full deployment pipeline automatically.

  • First, copy your task definition into the repository root so the workflow can find it. Run:
cp task-definition.json .aws/task-definition.json

Why copy the task definition?

The GitHub Actions workflow needs access to the task definition JSON inside the repository. The amazon-ecs-render-task-definition action reads it from the repo, injects the new image tag, and passes the updated definition to ECS. The workflow references it at the repo root via the ECS_TASK_DEFINITION env var set to task-definition.json.

  • Create the workflow directories by running:
mkdir -p .github/workflows
  • Create the workflow file at .github/workflows/deploy.yml and paste the following content:
name: Deploy to Amazon ECS

on:
  push:
    branches:
      - main

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: pickleball-advisor
  ECS_SERVICE: pickleball-service
  ECS_CLUSTER: pickleball-cluster
  ECS_TASK_DEFINITION: task-definition.json
  CONTAINER_NAME: pickleball-advisor

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@0e613a0980cbf65ed5b322eb7a1e075d28913a83
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@62f4f872db3836360b72999f4b87f1ff13310f3a

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

What does this workflow do?

The workflow runs six steps every time you push to main:

  • Checks out your code from the repository.
  • Configures AWS credentials using the secrets you just stored.
  • Logs into your ECR registry.
  • Builds the Docker image and tags it with the git commit SHA, then pushes it to ECR.
  • Reads your task definition JSON and injects the new image URI into it.
  • Deploys the updated task definition to ECS and waits until the service is stable.

The actions are pinned to specific commit SHAs instead of version tags. This is a security best practice recommended by GitHub to prevent supply-chain attacks.

  • Save .github/workflows/deploy.yml.

✔️ Awesome, I've got everything!

Great. Double check you have saved both task-definition.json (in the repo root) and .github/workflows/deploy.yml.

ⓧ I'd like to double check the full code

Here is the complete workflow file:

name: Deploy to Amazon ECS

on:
  push:
    branches:
      - main

env:
  AWS_REGION: us-east-1
  ECR_REPOSITORY: pickleball-advisor
  ECS_SERVICE: pickleball-service
  ECS_CLUSTER: pickleball-cluster
  ECS_TASK_DEFINITION: task-definition.json
  CONTAINER_NAME: pickleball-advisor

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@0e613a0980cbf65ed5b322eb7a1e075d28913a83
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@62f4f872db3836360b72999f4b87f1ff13310f3a

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

And here is the task definition that should be at the repo root as task-definition.json:

{
    "family": "pickleball-advisor",
    "networkMode": "awsvpc",
    "requiresCompatibilities": ["FARGATE"],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "name": "pickleball-advisor",
            "image": "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/pickleball-advisor:latest",
            "portMappings": [
                {
                    "containerPort": 5000,
                    "hostPort": 5000,
                    "protocol": "tcp"
                }
            ],
            "essential": true
        }
    ]
}
Push to main and verify the CI/CD pipeline

With the workflow in place, every push to main triggers the full pipeline. Let's push everything and watch it work.

  • Add, commit, and push all new files by running these commands:
git add .
git commit -m "Add GitHub Actions CI/CD workflow"
git push origin main

What happens after you push?

GitHub detects the push to main and reads .github/workflows/deploy.yml. It spins up an ubuntu-latest runner and executes each step in order. The entire cycle takes 3-5 minutes.

  • Open your pickleball-advisor repository on GitHub.
  • Click the **Actions** tab at the top of the repository page.
  • You should see a workflow run titled "Add GitHub Actions CI/CD workflow" with a yellow dot (in progress) or a green check (complete).
  • Click on the workflow run to see the detailed step-by-step logs.
  • Wait for all steps to show green checkmarks. This confirms the image was built, pushed to ECR, and deployed to ECS.

Workflow failing at the Deploy step?

If the workflow fails on the "Deploy Amazon ECS task definition" step, the most common cause is the workflow needs a GitHub Environment called production to be created. Go to your repository **Settings**, click **Environments** in the left sidebar, then click **New environment** and name it production. Rerun the workflow after creating it.

If the error mentions permission issues, double-check that your IAM policy includes iam:PassRole and that both secrets are stored correctly in GitHub.

help me debug why my GitHub Actions ECS deployment workflow is failing

Now verify the deployment landed on ECS.

  • Find the public IP of your updated task by running:
aws ecs list-tasks --cluster pickleball-cluster --service-name pickleball-service --query "taskArns[0]" --output text
  • Use the task ARN from the output to describe the network interface and get the public IP:
aws ecs describe-tasks --cluster pickleball-cluster --tasks <task-arn> --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text
  • Then get the public IP from the network interface:
aws ec2 describe-network-interfaces --network-interface-ids <eni-id> --query "NetworkInterfaces[0].Association.PublicIp" --output text
  • Test the live endpoint by running:
curl http://<public-ip>:5000/score?latitude=38.89&longitude=-77.03
  • You should see the playability JSON response from your cloud-deployed API.

How do I know CI/CD is actually working?

To prove the full pipeline works end to end, make a small code change. Open app/main.py and change one of the recommendation messages. For example, find the line:

return "Hey Ferb, grab the paddles! Decent conditions out there."

Change it to something new like:

return "Hey Ferb, grab the paddles! Let's hit the court!"

Then commit and push. Watch the Actions tab run the workflow again, wait for it to complete, and hit the endpoint. Your updated message confirms the full automated cycle works.

Secret mission

Add CloudWatch Logging and Health Monitoring

Your API is running in the cloud, but can you see what it's doing? Right now, if something breaks at 2am, you have zero visibility. Add operational monitoring with CloudWatch container logging and a health endpoint that ECS checks automatically.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The Fargate task costs approximately $0.01/hour while running, and ECR storage is free under 500MB.

Cost warning

Your ECS Fargate task charges approximately $0.01/hour while running at 0.25 vCPU / 512MB. If you leave it running 24/7, that adds up to roughly $7/month. Scale the service to 0 or delete it when you are done testing.

Resources you used:

  • ECS Cluster (pickleball-cluster) in us-east-1.
  • ECS Service (pickleball-service) running one Fargate task.
  • ECR Repository (pickleball-advisor) storing Docker images.
  • CloudWatch Log Group (/ecs/pickleball-advisor).
  • IAM User for GitHub Actions with ECR and ECS permissions.
  • Local project files (pickleball-advisor) on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep the API live for demo purposes during interviews.

  • Your Fargate task continues running and serving requests on port 5000.
  • GitHub Actions will redeploy automatically on every push to main.
  • The task costs approximately $0.01/hour (about $7.20/month).

Pause - I'll come back to this later

Shut down the running task to stop compute charges while keeping all resources in place. You can scale back up any time.

  • Scale the service to 0 tasks by running:
aws ecs update-service --cluster pickleball-cluster --service pickleball-service --desired-count 0
  • No compute charges while paused. ECR storage and the cluster itself are free.
  • To resume later, run the same command with --desired-count 1.

Delete - I don't want to use this again

Remove all project resources and start fresh. Run each command in order.

  • Delete the ECS service (force-delete handles running tasks):
aws ecs delete-service --cluster pickleball-cluster --service pickleball-service --force
  • Delete the ECS cluster:
aws ecs delete-cluster --cluster pickleball-cluster
  • Delete the ECR repository and all images:
aws ecr delete-repository --repository-name pickleball-advisor --force
  • Delete the CloudWatch log group:
aws logs delete-log-group --log-group-name /ecs/pickleball-advisor
  • Delete the IAM user's access keys, detach policies, and delete the user. Replace the username and access key ID with yours:
aws iam list-access-keys --user-name github-actions-ecs
aws iam delete-access-key --user-name github-actions-ecs --access-key-id YOUR_ACCESS_KEY_ID
aws iam detach-user-policy --user-name github-actions-ecs --policy-arn YOUR_POLICY_ARN
aws iam delete-user --user-name github-actions-ecs
  • Delete your local project folder:
cd ~/Desktop
rm -rf pickleball-advisor
  • Optionally delete the GitHub repository from the repository's Settings page, scroll to the Danger Zone, and click Delete this repository.

Nice Work!

Nice Work!

You just built a production-grade containerized API, deployed it to the cloud, and automated the entire delivery pipeline. That is the full container lifecycle from code to cloud.

You've learned how to:

  • Build a Python REST API that fetches live weather data and calculates a pickleball playability score using weighted factors.
  • Containerize an application with Docker and push the image to Amazon ECR for cloud storage.
  • Deploy to ECS Fargate with proper networking including awsvpc mode, public IP assignment, and security groups.
  • Automate the full build-push-deploy cycle with GitHub Actions CI/CD so every push to main triggers a new deployment.
  • Secret Mission: Added CloudWatch logging and a health check endpoint for operational monitoring and observability.

Ready to quiz yourself?