Deploy Healthcare Microservices on ECS

Containerize two Flask microservices and deploy them on AWS ECS Fargate.

Introduction

30 Second Summary

Modern healthcare applications handle everything from patient appointments to staff record management. Running all of that in a single application creates a fragile system where one failure can take down everything.

In this project, you will build two containerized Python microservices for a healthcare clinic and deploy them on AWS using ECS Fargate, an Application Load Balancer, and a shared MySQL database.

What You'll Build

A healthcare clinic platform with a Patient Portal and a Staff Dashboard running as separate Docker containers on AWS Fargate, orchestrated by Amazon ECS and load-balanced with path-based routing.

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

  • Two Flask microservices containerized with Docker and stored in Amazon ECR.
  • A fully provisioned Amazon RDS MySQL database seeded with healthcare records.
  • An Application Load Balancer with path-based routing directing traffic to the correct microservice based on URL path.
  • Secret Mission: Add CloudWatch Logs monitoring and a health check endpoint to both microservices for production-readiness.

Are there any prerequisites?

You need an AWS account (within Free Tier eligibility period), basic familiarity with Python, and comfort navigating the AWS console. Step 1 walks you through installing the AWS CLI and verifying Docker Desktop.

Set Up Your Local Environment

Before you can deploy containers to the cloud, you need a local environment that can build Docker images and talk to AWS services.

In this step, you will install the AWS CLI so your terminal can communicate with AWS. You will also verify Docker is running and create the folder structure for your two microservices.

In this step, get ready to:
  • Install and configure AWS CLI v2 with your IAM credentials.
  • Verify Docker Desktop is running.
  • Create the project folder structure for both microservices.
Install and verify AWS CLI v2

The AWS CLI lets you manage AWS resources directly from your terminal. You need version 2 to work with services like Amazon ECR and Amazon ECS later in this project.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check if the AWS CLI is already installed by running this command:
aws --version

✔️ I see aws-cli/2.x.x

Your AWS CLI v2 is already installed. Skip ahead to the configuration section below.

ⓧ I see aws-cli/1.x.x

You have an older version of the AWS CLI installed. You need to upgrade to version 2.

Uninstall version 1 first, then install version 2 using the instructions for your operating system below.

macOS

  • Download and install AWS CLI v2 by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /

Enter your Mac password when prompted. You will not see characters appear as you type.

Windows

  • Download the MSI installer from the link below and run it:

https://awscli.amazonaws.com/AWSCLIV2.msi

  • Double-click the downloaded .msi file and follow the on-screen prompts to complete installation.
  • Close your current terminal and open a new one after installation completes.

ⓧ Command not found

The AWS CLI is not installed yet. Install version 2 for your operating system.

macOS

  • Download and install AWS CLI v2 by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /

Enter your Mac password when prompted. You will not see characters appear as you type.

Windows

  • Download the MSI installer from the link below and run it:

https://awscli.amazonaws.com/AWSCLIV2.msi

  • Double-click the downloaded .msi file and follow the on-screen prompts to complete installation.
  • Close your current terminal and open a new one after installation completes.
  • After installing, verify the installation succeeded by running:
aws --version

You should see output starting with aws-cli/2. followed by a version number.

Still seeing command not found?

Close your terminal completely and open a new one. The installation adds aws to your system PATH, but existing terminal sessions will not pick up the change.

On Windows, make sure you open a brand new PowerShell window after the MSI installer finishes.

Help me troubleshoot why aws --version is not working after installation.

  • Configure the AWS CLI with your IAM credentials by running:
aws configure

What does this command do?

The aws configure command prompts you for four values one at a time. Each value is saved to a local config file so the CLI can authenticate with AWS on every future command.

  • Enter your IAM user Access Key ID when prompted and press Enter.
  • Enter your Secret Access Key when prompted and press Enter.
  • Enter us-east-1 for the default region name and press Enter.
  • Enter json for the default output format and press Enter.

Where do I find my Access Key ID and Secret Access Key?

These credentials come from your IAM user in the AWS console. If you do not have them yet, go to the IAM service, select your user, click the Security credentials tab, and create a new access key.

Never share your Secret Access Key with anyone. Treat it like a password.

  • Verify your credentials are working by running:
aws sts get-caller-identity

You should see a JSON response containing your AWS account ID, user ARN, and user ID.

Seeing an InvalidClientTokenId error?

Double-check that you pasted the correct Access Key ID and Secret Access Key. Run aws configure again and re-enter both values carefully.

Make sure there are no extra spaces before or after the keys when pasting.

Help me fix my AWS CLI credentials error.

Verify Docker Desktop is running

Docker builds and runs the container images you will push to AWS later. It needs to be running in the background for the build commands to work.

  • Check that Docker Desktop is running by looking for its icon in your system tray (Windows) or menu bar (macOS).
  • Confirm Docker is accessible from your terminal by running:
docker --version

You should see a version number confirming Docker is installed and available.

  • Verify the Docker engine is fully operational by running:
docker info

You should see detailed output about the Docker engine including server version, storage driver, and operating system information.

Seeing a connection error from docker info?

This means Docker Desktop is not running. Return to Docker Desktop and wait for it to fully start. The whale icon in your system tray or menu bar should stop animating when it is ready.

Help me get Docker Desktop running properly.

Create the project folder structure

Your project will contain two separate microservices, each in its own folder. Creating this structure now keeps things organized for when you write the application code in the next step.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir healthcare-microservices
cd healthcare-microservices
  • Create the directories for both microservices by running:
mkdir patient-service staff-service
  • Confirm the folders exist by running:
ls

You should see patient-service and staff-service in the output.

Why two separate folders?

Each microservice gets its own folder because each one becomes its own Docker image. Keeping the code separate makes it easy to build, version, and deploy each service independently.

🙋‍♀️ Don't see both folders?

Make sure you ran the mkdir command from inside the healthcare-microservices folder. Run pwd to check your current location.

Help me figure out where my folders were created.

Your environment is ready. Next up, you will write the Flask microservices that power the patient portal and staff dashboard.

Write the Flask Microservices

Your local environment is ready with the AWS CLI configured and Docker Desktop running. Now it's time to write the actual application code that will run inside your containers.

This project uses a microservices architecture, meaning you'll build two small, independent Flask applications instead of one big monolith. Each microservice handles a single responsibility and gets its own container.

In this step, get ready to:
  • Write the Patient Portal microservice with Flask routes for patients.
  • Write the Staff Dashboard microservice with Flask routes for clinic staff.
  • Create Dockerfiles to containerize both services and verify they build locally.
Create the Patient Portal microservice

The Patient Portal is the public-facing service. Patients visit it to see clinic information and look up their providers.

  • In VS Code, open the patient-service folder inside your healthcare-microservices project folder (the one you created in Step 1).
  • Create a new file called app.py inside patient-service/ and paste the following code:
import os
from flask import Flask, jsonify, render_template_string

import mysql.connector

app = Flask(__name__)

# Database connection settings pulled from environment variables
DB_CONFIG = {
    "host": os.environ.get("DB_HOST", "localhost"),
    "user": os.environ.get("DB_USER", "admin"),
    "password": os.environ.get("DB_PASSWORD", ""),
    "database": os.environ.get("DB_NAME", "clinic"),
}


def get_db_connection():
    return mysql.connector.connect(**DB_CONFIG)


@app.route("/")
def home():
    return render_template_string("""
    <html>
    <head><title>Patient Portal - HealthFirst Clinic</title></head>
    <body>
        <h1>Welcome to HealthFirst Clinic</h1>
        <p>Use this portal to view your appointments and provider information.</p>
        <a href="/patients">View Patient Directory</a>
    </body>
    </html>
    """)


@app.route("/patients")
def list_patients():
    conn = get_db_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT id, first_name, last_name, provider, appointment_date FROM patients")
    patients = cursor.fetchall()
    cursor.close()
    conn.close()
    return jsonify(patients)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

What does this code do?

This Flask app defines two routes for the patient portal:

  • DB_CONFIG reads database connection details from environment variables. This lets you configure different databases for local testing vs. production without changing code.
  • The / route returns a simple HTML homepage for the clinic.
  • The /patients route queries the patients table and returns the results as JSON.
  • The app listens on port 5000, which is what the container will expose.
  • Save patient-service/app.py.
  • Now create a file called requirements.txt inside patient-service/ and paste the following:
flask==3.1.3
mysql-connector-python==9.7.0

Why pin exact versions?

Pinning versions like flask==3.1.3 ensures every environment builds identically. Without version pins, a new release could break your container months later.

  • Save patient-service/requirements.txt.

You won't see anything running yet. The next substep creates the second microservice, and then Dockerfiles will let you build and test both.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files in the patient-service/ folder.

ⓧ I'd like to double check the full code

import os
from flask import Flask, jsonify, render_template_string

import mysql.connector

app = Flask(__name__)

DB_CONFIG = {
    "host": os.environ.get("DB_HOST", "localhost"),
    "user": os.environ.get("DB_USER", "admin"),
    "password": os.environ.get("DB_PASSWORD", ""),
    "database": os.environ.get("DB_NAME", "clinic"),
}


def get_db_connection():
    return mysql.connector.connect(**DB_CONFIG)


@app.route("/")
def home():
    return render_template_string("""
    <html>
    <head><title>Patient Portal - HealthFirst Clinic</title></head>
    <body>
        <h1>Welcome to HealthFirst Clinic</h1>
        <p>Use this portal to view your appointments and provider information.</p>
        <a href="/patients">View Patient Directory</a>
    </body>
    </html>
    """)


@app.route("/patients")
def list_patients():
    conn = get_db_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT id, first_name, last_name, provider, appointment_date FROM patients")
    patients = cursor.fetchall()
    cursor.close()
    conn.close()
    return jsonify(patients)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
flask==3.1.3
mysql-connector-python==9.7.0
Create the Staff Dashboard microservice

The Staff Dashboard is an internal tool for clinic employees. It lets staff view full patient records and add new patients to the system.

  • Create a new file called app.py inside the staff-service/ folder and paste the following code:
import os
from flask import Flask, jsonify, render_template_string, request

import mysql.connector

app = Flask(__name__)

# Database connection settings pulled from environment variables
DB_CONFIG = {
    "host": os.environ.get("DB_HOST", "localhost"),
    "user": os.environ.get("DB_USER", "admin"),
    "password": os.environ.get("DB_PASSWORD", ""),
    "database": os.environ.get("DB_NAME", "clinic"),
}


def get_db_connection():
    return mysql.connector.connect(**DB_CONFIG)


@app.route("/admin/")
def dashboard():
    conn = get_db_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT COUNT(*) as total FROM patients")
    count = cursor.fetchone()["total"]
    cursor.close()
    conn.close()
    return render_template_string("""
    <html>
    <head><title>Staff Dashboard - HealthFirst Clinic</title></head>
    <body>
        <h1>Staff Dashboard</h1>
        <p>Total patients registered: {{ count }}</p>
        <a href="/admin/patients">View All Patient Records</a>
    </body>
    </html>
    """, count=count)


@app.route("/admin/patients")
def list_patients():
    conn = get_db_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT * FROM patients")
    patients = cursor.fetchall()
    cursor.close()
    conn.close()
    return jsonify(patients)


@app.route("/admin/patients", methods=["POST"])
def add_patient():
    data = request.get_json()
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute(
        "INSERT INTO patients (first_name, last_name, date_of_birth, provider, appointment_date) VALUES (%s, %s, %s, %s, %s)",
        (data["first_name"], data["last_name"], data["date_of_birth"], data["provider"], data["appointment_date"])
    )
    conn.commit()
    new_id = cursor.lastrowid
    cursor.close()
    conn.close()
    return jsonify({"id": new_id, "message": "Patient added successfully"}), 201


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

What does this code do?

This Flask app defines three routes for the staff dashboard:

  • /admin/ shows a dashboard with the total patient count.
  • /admin/patients (GET) returns all patient records as JSON, including sensitive fields like date of birth.
  • /admin/patients (POST) accepts a JSON body and inserts a new patient into the database.

Notice all staff routes live under the /admin/ path prefix. The Application Load Balancer will use this prefix later to route traffic to the correct container.

  • Save staff-service/app.py.
  • Create a file called requirements.txt inside staff-service/ and paste the following:
flask==3.1.3
mysql-connector-python==9.7.0
  • Save staff-service/requirements.txt.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files in the staff-service/ folder.

ⓧ I'd like to double check the full code

import os
from flask import Flask, jsonify, render_template_string, request

import mysql.connector

app = Flask(__name__)

DB_CONFIG = {
    "host": os.environ.get("DB_HOST", "localhost"),
    "user": os.environ.get("DB_USER", "admin"),
    "password": os.environ.get("DB_PASSWORD", ""),
    "database": os.environ.get("DB_NAME", "clinic"),
}


def get_db_connection():
    return mysql.connector.connect(**DB_CONFIG)


@app.route("/admin/")
def dashboard():
    conn = get_db_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT COUNT(*) as total FROM patients")
    count = cursor.fetchone()["total"]
    cursor.close()
    conn.close()
    return render_template_string("""
    <html>
    <head><title>Staff Dashboard - HealthFirst Clinic</title></head>
    <body>
        <h1>Staff Dashboard</h1>
        <p>Total patients registered: {{ count }}</p>
        <a href="/admin/patients">View All Patient Records</a>
    </body>
    </html>
    """, count=count)


@app.route("/admin/patients")
def list_patients():
    conn = get_db_connection()
    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT * FROM patients")
    patients = cursor.fetchall()
    cursor.close()
    conn.close()
    return jsonify(patients)


@app.route("/admin/patients", methods=["POST"])
def add_patient():
    data = request.get_json()
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute(
        "INSERT INTO patients (first_name, last_name, date_of_birth, provider, appointment_date) VALUES (%s, %s, %s, %s, %s)",
        (data["first_name"], data["last_name"], data["date_of_birth"], data["provider"], data["appointment_date"])
    )
    conn.commit()
    new_id = cursor.lastrowid
    cursor.close()
    conn.close()
    return jsonify({"id": new_id, "message": "Patient added successfully"}), 201


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
flask==3.1.3
mysql-connector-python==9.7.0
Write Dockerfiles for both services

A Dockerfile is a recipe that tells Docker how to package your application into a container image. Both microservices use the same Dockerfile structure.

  • Create a file called Dockerfile (no extension) inside the patient-service/ folder and paste the following:
FROM python:3.12-slim

WORKDIR /app

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

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]

What does this Dockerfile do?

  • FROM python:3.12-slim starts from a lightweight Python image.
  • WORKDIR /app sets the working directory inside the container.
  • COPY requirements.txt then RUN pip install installs dependencies first. This layer gets cached, so rebuilds are fast if only app code changes.
  • EXPOSE 5000 documents that the container listens on port 5000.
  • CMD ["python", "app.py"] starts the Flask server when the container launches.
  • Save patient-service/Dockerfile.
  • Create the same Dockerfile inside the staff-service/ folder with identical contents:
FROM python:3.12-slim

WORKDIR /app

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

COPY app.py .

EXPOSE 5000

CMD ["python", "app.py"]
  • Save staff-service/Dockerfile.

Now test that both containers build and start correctly.

  • Build the Patient Portal image by running this command from your project root folder:
docker build -t patient-service ./patient-service
  • Build the Staff Dashboard image by running:
docker build -t staff-service ./staff-service
  • Run the Patient Portal container to verify it starts:
docker run -d -p 5000:5000 patient-service

You should see a container ID printed to your terminal. The Flask app starts inside the container, but it will fail to connect to the database since no RDS instance exists yet. That is expected.

  • Stop and remove the test container by running:
docker stop $(docker ps -q --filter ancestor=patient-service) && docker rm $(docker ps -aq --filter ancestor=patient-service)
  • Run the Staff Dashboard container to verify it also starts:
docker run -d -p 5001:5000 staff-service
  • Stop and remove the staff test container:
docker stop $(docker ps -q --filter ancestor=staff-service) && docker rm $(docker ps -aq --filter ancestor=staff-service)

Why do the containers fail to connect to the database?

The Flask apps try to connect to a MySQL database on startup when you hit a database route. Since you haven't created the Amazon RDS database yet, the connection will fail. That's perfectly fine at this stage.

What matters is that the container builds successfully and Flask starts listening on port 5000. You'll provision the database in Step 4.

🙋‍♀️ Build failing with an error?

  • Make sure Docker Desktop is running. Check your system tray for the Docker whale icon.
  • Verify the Dockerfile has no file extension. Some editors add .txt automatically.
  • Check that requirements.txt and app.py are in the same folder as the Dockerfile. The build context must contain all referenced files.

Help me debug my Docker build error

Both microservices are containerized and building cleanly. Next up, you'll push these images to Amazon ECR so that ECS can pull them during deployment.

Push Container Images to Amazon ECR

Your Docker images are built and running locally. For ECS to pull and run these containers, the images need to live in a container registry that AWS services can access.

Amazon ECR (Elastic Container Registry) is a private Docker registry managed by AWS. In this step, you'll create two repositories in ECR and push both microservice images there.

In this step, get ready to:
  • Create two ECR repositories for your microservices.
  • Authenticate Docker to your private ECR registry.
  • Tag and push both container images to ECR.
Create ECR repositories

Each microservice gets its own repository in ECR. Think of a repository like a named folder that stores all versions of one container image.

  • Create the patient-service repository by running this command:
aws ecr create-repository --repository-name patient-service --region us-east-1
  • Create the staff-service repository by running this command:
aws ecr create-repository --repository-name staff-service --region us-east-1

What do these commands do?

Each command creates an empty private repository in ECR in the us-east-1 region. The output includes a repositoryUri field. This is the address you'll use to tag and push images.

The URI follows the pattern <account-id>.dkr.ecr.us-east-1.amazonaws.com/<repository-name>. Copy or note down the repositoryUri from the output. You'll need it in the next substep.

  • Check the output of each command. You should see a JSON response containing a repositoryUri and repositoryName matching the names you provided.

Seeing an access denied error?

Your IAM user may not have permission to create ECR repositories. Ensure your AWS CLI is configured with credentials that have the AmazonEC2ContainerRegistryFullAccess policy attached. You can check your current identity by running aws sts get-caller-identity.

help me fix an ECR access denied error when creating a repository

Authenticate Docker to ECR and push images

ECR is a private registry. Before Docker can push images to it, you need to authenticate your local Docker client with a temporary token from AWS.

  • Retrieve your AWS account ID by running:
aws sts get-caller-identity --query Account --output text

Why do I need my account ID?

Your ECR registry address includes your account ID. The full address follows the pattern <account-id>.dkr.ecr.us-east-1.amazonaws.com. You'll substitute your 12-digit account ID into the commands below.

  • 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="123456789012"]].dkr.ecr.us-east-1.amazonaws.com

What does this command do?

This pipes a temporary authentication token from AWS into the docker login command. The token is valid for 12 hours. After it expires, you would run this command again.

You should see Login Succeeded in the terminal output.

  • Confirm you see Login Succeeded in the terminal output before continuing.

Login failed or connection refused?

Make sure Docker Desktop is running. Check your system tray for the Docker whale icon. If Docker is stopped, the docker login command cannot connect.

Also verify your account ID is correct. The 12-digit number from aws sts get-caller-identity must match what you pasted into the command.

help me troubleshoot Docker login to ECR

  • Tag the patient-service image with the ECR repository URI by running:
docker tag patient-service:latest [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/patient-service:latest
  • Push the patient-service image to ECR by running:
docker push [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/patient-service:latest

What are these commands doing?

The docker tag command creates a second name (alias) for your local image that points to the ECR repository. The docker push command then uploads the image layers to that remote address.

You should see progress bars as each layer uploads. The push is complete when all layers show Pushed and a final digest is printed.

  • Tag the staff-service image with its ECR repository URI by running:
docker tag staff-service:latest [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/staff-service:latest
  • Push the staff-service image to ECR by running:
docker push [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/staff-service:latest
  • Wait for both pushes to finish. Each push shows layer-by-layer progress and completes with a digest hash.
  • Verify the patient-service image exists in ECR by running:
aws ecr describe-images --repository-name patient-service --region us-east-1
  • Verify the staff-service image exists in ECR by running:
aws ecr describe-images --repository-name staff-service --region us-east-1
  • Both commands should return JSON with an imageDetails array. Look for latest in the imageTags field to confirm the push succeeded.

Push failed or image not showing up?

If the push hangs or fails with a timeout, check your internet connection. Large image layers can take a few minutes to upload.

If describe-images returns an empty list, double-check that your docker tag command used the exact repository URI from your account. A single typo in the account ID or region means the push went nowhere.

help me debug a failed Docker push to ECR

Your container images are now stored in the cloud. Next up, you'll provision the AWS infrastructure that will run these containers and serve traffic to users.

Provision AWS Infrastructure

Your container images are safely stored in ECR. Now they need somewhere to run and a database to connect to.

In this step, you will create the cloud infrastructure that ties everything together. You will provision an Amazon RDS MySQL database, seed it with patient data, and set up an Application Load Balancer with path-based routing so traffic reaches the correct microservice.

In this step, get ready to:
  • Create an RDS MySQL database in the default VPC.
  • Seed the database with patient records using a Python script.
  • Create an ALB with two target groups and path-based routing rules.
Create the RDS MySQL database

Your microservices need a shared database to read and write patient records. Amazon RDS manages the MySQL server for you, handling backups and patching so you can focus on your application.

  • In the AWS Management Console, search for RDS and open the RDS dashboard.
  • Confirm the region in the top-right corner is set to us-east-1.
  • In the left navigation pane, click Databases.
  • Click Create database.
  • Select Standard create as the creation method.
  • Under Engine options, choose MySQL.
  • Under Templates, choose Free tier.
  • In the Settings section, enter healthcare-db as the DB instance identifier.
  • Set Master username to admin.
  • Uncheck Auto generate a password, then enter and confirm a password you will remember.
  • In Instance configuration, confirm the class is db.t3.micro.
  • In the Connectivity section, keep the default VPC selected.
  • Set Public access to Yes. This allows your local machine to connect for seeding.
  • Expand Additional configuration and enter clinic as the Initial database name.
  • Click Create database.

The database takes 5-10 minutes to reach the Available state. While you wait, you can write the seed script in the next substep.

Why publicly accessible?

You need to connect from your local machine to seed the database with sample data. After deployment, the ECS tasks connect through the VPC's private network. In a production environment, you would keep RDS private and use a bastion host or VPN.

💡 What about the security group?

RDS creates a default security group that allows inbound MySQL traffic (port 3306). You may need to edit its inbound rules to allow connections from your local IP address. In the RDS instance details, click the security group link, choose Edit inbound rules, and add a rule for MySQL/Aurora with source My IP.

🙋‍♀️ Database stuck in Creating?

RDS provisioning can take up to 20 minutes in some cases. Check back periodically. If the status shows an error, verify you selected the Free tier template and db.t3.micro instance class.

Help me troubleshoot my RDS database creation.

Seed the database with patient records

Once the RDS instance is available, you need to create the patients table and insert sample data. You will write a Python script that connects to your RDS endpoint and runs the necessary SQL.

  • In the RDS console, click your healthcare-db instance.
  • In the Connectivity & security tab, copy the Endpoint value. It looks like healthcare-db.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com.
  • Back in VS Code, create a new file called seed-database.py in your project root (the healthcare-microservices folder) by pasting the following code:
import mysql.connector

DB_CONFIG = {
    "host": "your-rds-endpoint-here",
    "user": "admin",
    "password": "your-password-here",
    "database": "clinic",
}

conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor()

cursor.execute("""
CREATE TABLE IF NOT EXISTS patients (
    id INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    date_of_birth DATE NOT NULL,
    provider VARCHAR(100) NOT NULL,
    appointment_date DATE NOT NULL
)
""")

patients = [
    ("Sarah", "Johnson", "1985-03-15", "Dr. Emily Chen", "2026-06-25"),
    ("Michael", "Williams", "1972-08-22", "Dr. James Park", "2026-06-26"),
    ("Jessica", "Brown", "1990-11-03", "Dr. Emily Chen", "2026-06-27"),
    ("David", "Martinez", "1968-05-30", "Dr. Aisha Patel", "2026-06-28"),
    ("Amanda", "Taylor", "1995-01-17", "Dr. James Park", "2026-06-30"),
]

cursor.executemany(
    "INSERT INTO patients (first_name, last_name, date_of_birth, provider, appointment_date) VALUES (%s, %s, %s, %s, %s)",
    patients
)

conn.commit()
print(f"Inserted {cursor.rowcount} patients.")
cursor.close()
conn.close()

What does this script do?

The script connects to your RDS MySQL instance and creates the patients table with columns for name, date of birth, provider, and appointment date. It then inserts five sample patient records that both microservices will query.

  • Replace your-rds-endpoint-here with the endpoint you copied from the RDS console.
  • Replace your-password-here with the master password you set when creating the database.
  • Save seed-database.py.
  • Run the seed script by executing this command in your terminal:
python seed-database.py

You should see Inserted 5 patients. in the terminal output.

Connection refused or timed out?

This usually means the RDS security group is blocking your IP. Go to the RDS instance details, click the VPC security group link, and add an inbound rule for MySQL/Aurora (port 3306) with source set to My IP.

Also confirm the database status is Available before running the script.

Help me connect to my RDS instance from my local machine.

Create the ALB and target groups

An Application Load Balancer sits in front of your microservices and uses path-based routing to direct traffic. Requests to / go to the patient service. Requests to /admin/* go to the staff service.

You will create two target groups first, then the load balancer with its routing rules.

  • In the AWS console, search for EC2 and open the EC2 dashboard.
  • In the left navigation pane under Load Balancing, click Target Groups.
  • Click Create target group.

Configure the first target group for the patient service:

  • For Choose a target type, select IP addresses.
  • Enter patient-tg as the Target group name.
  • Set Protocol to HTTP and Port to 5000.
  • Select your default VPC.
  • Leave health check settings at their defaults.
  • Skip the Register targets step (ECS will register targets automatically later).
  • Click Create target group.

Now create the second target group for the staff service:

  • Click Create target group again.
  • Select IP addresses as the target type.
  • Enter staff-tg as the Target group name.
  • Set Protocol to HTTP and Port to 5000.
  • Select your default VPC.
  • Skip registering targets.
  • Click Create target group.

Why IP addresses instead of Instances?

ECS Fargate tasks do not run on EC2 instances you manage. Each task gets its own private IP address inside the VPC. By choosing the IP target type, the ALB can route directly to those task IPs when ECS registers them automatically.

With both target groups created, set up the load balancer:

  • In the left navigation pane under Load Balancing, click Load Balancers.
  • Click Create load balancer.
  • Under Application Load Balancer, click Create.
  • Enter healthcare-alb as the Load balancer name.
  • For Scheme, select Internet-facing.
  • For IP address type, keep IPv4.
  • Under Network mapping, select your default VPC.
  • Select at least two Availability Zone subnets (check two or more checkboxes).
  • Under Security groups, create a new security group or select one that allows inbound HTTP on port 80 from 0.0.0.0/0.
  • Under Listeners and routing, keep the default listener on HTTP port 80.
  • For Default action, select the patient-tg target group.
  • Click Create load balancer.

The ALB takes a minute or two to become Active. While it provisions, add the path-based routing rule for the staff service:

  • Click your new healthcare-alb load balancer to open its details.
  • Click the Listeners and rules tab.
  • Click the listener on port 80 to open its rules.
  • Click Manage rules, then Add rule.
  • Give the rule a name like staff-routing.
  • Under Add condition, choose Path and enter /admin/*.
  • Under Actions, choose Forward to and select staff-tg.
  • Set the priority to 1 (higher priority than the default rule).
  • Click Save.

How does path-based routing work?

The ALB evaluates listener rules in priority order. Your new rule matches any request starting with /admin/ and sends it to staff-tg. All other requests fall through to the default rule, which sends them to patient-tg.

This means a single URL (the ALB DNS name) serves both microservices based purely on the path.

  • Return to the Load Balancers list and wait until the State column shows Active for healthcare-alb.

ALB not reaching Active state?

Ensure you selected subnets in at least two different Availability Zones. The ALB requires multi-AZ coverage. Also verify the security group allows inbound traffic on port 80.

Help me troubleshoot my Application Load Balancer.

Your infrastructure is live. The database is seeded, the load balancer is active, and routing rules are in place. Next up, you will deploy your containers on ECS Fargate and connect everything together.

Deploy ECS Services on Fargate

Your infrastructure is ready. You have an ALB routing traffic based on URL paths, target groups waiting for healthy targets, and an RDS database loaded with patient data.

Now you need something to actually serve that traffic. In this step, you will deploy both microservices as containers on AWS Fargate. Fargate runs your containers without requiring you to manage any servers. You define what to run, and AWS handles the underlying compute.

In this step, get ready to:
  • Create an ECS cluster and task execution IAM role.
  • Register task definitions for both microservices.
  • Deploy ECS services attached to the ALB target groups.
Create the ECS cluster and task execution role

An Amazon ECS cluster is a logical grouping for your services and tasks. The task execution role gives ECS permission to pull container images from ECR on your behalf.

  • Open the AWS Management Console and navigate to the IAM service.
  • In the left navigation pane, click Roles.
  • Click Create role.
  • For Trusted entity type, select AWS service.
  • For Use case, select Elastic Container Service.
  • Select Elastic Container Service Task and click Next.
  • In the permissions search box, type AmazonECSTaskExecutionRolePolicy.
  • Check the box next to AmazonECSTaskExecutionRolePolicy and click Next.
  • For Role name, enter ecsTaskExecutionRole.
  • Click Create role.

What is this role for?

The ecsTaskExecutionRole gives ECS permission to pull your container images from ECR and send logs to CloudWatch. Without it, your tasks would fail to start because ECS cannot access your private container registry.

  • Now navigate to the Elastic Container Service console.
  • In the left navigation pane, click Clusters.
  • Click Create cluster.
  • For Cluster name, enter healthcare-cluster.
  • Leave all other settings at their defaults. The console automatically associates the Fargate capacity provider with the cluster.
  • Click Create.

Wait for the cluster status to show Active. This takes about 30 seconds.

Why Fargate instead of EC2?

With EC2-backed ECS, you manage the underlying servers. You choose instance types, handle patching, and scale the fleet yourself. Fargate removes all of that. You define the CPU and memory your container needs, and AWS provisions the compute on demand.

This lets you focus on container orchestration rather than server provisioning.

🙋‍♀️ Cluster creation failed?

Make sure you are in the us-east-1 region. Check the region selector in the top-right corner of the console.

If you see a permissions error, confirm your IAM user has the ecs:CreateCluster permission.

Help me troubleshoot my ECS cluster creation error

Register task definitions for both services

A task definition tells ECS exactly how to run your container. It specifies which image to pull, how much CPU and memory to allocate, which ports to expose, and what environment variables to pass in.

  • In the ECS console left navigation pane, click Task definitions.
  • Click Create new task definition.
  • For Task definition family, enter patient-service.
  • Under Infrastructure requirements, set Launch type to AWS Fargate.
  • Set CPU to .25 vCPU and Memory to .5 GB.
  • For Task execution role, select the ecsTaskExecutionRole you just created.

Now configure the container definition within this task definition.

  • For Container name, enter patient-service.
  • For Image URI, paste the URI of your ECR patient-service image. It follows the format: your-account-id.dkr.ecr.us-east-1.amazonaws.com/patient-service:latest
  • Under Port mappings, set Container port to 5000 and Protocol to TCP.
  • Expand the Environment variables section.
  • Add a variable with key DB_HOST and value set to your RDS endpoint (the endpoint you used in seed-database.py).
  • Add a variable with key DB_USER and value admin.
  • Add a variable with key DB_PASSWORD and value set to the master password you chose when creating the RDS instance.
  • Add a variable with key DB_NAME and value clinic.
  • Click Create to register the task definition.

What do these environment variables do?

Your Flask apps read DB_HOST, DB_USER, DB_PASSWORD, and DB_NAME from the environment using os.environ.get(). This is how the containers know where to find the RDS database at runtime.

Passing credentials via environment variables is acceptable for learning projects. In production, you would use AWS Secrets Manager instead.

Now repeat the process for the staff service.

  • Click Create new task definition again.
  • For Task definition family, enter staff-service.
  • Set Launch type to AWS Fargate, CPU to .25 vCPU, and Memory to .5 GB.
  • For Task execution role, select ecsTaskExecutionRole.
  • For Container name, enter staff-service.
  • For Image URI, paste the URI of your ECR staff-service image: your-account-id.dkr.ecr.us-east-1.amazonaws.com/staff-service:latest
  • Set Container port to 5000 with TCP protocol.
  • Add the same four environment variables: DB_HOST (your RDS endpoint), DB_USER (admin), DB_PASSWORD (your password), and DB_NAME (clinic).
  • Click Create.

You should now see both patient-service and staff-service listed on the Task definitions page.

Task definition creation failed?

Double-check that your Image URI matches the format shown in the ECR console. Navigate to ECR, click into your repository, and copy the URI directly from the Image URI column.

Ensure the ecsTaskExecutionRole exists and has the AmazonECSTaskExecutionRolePolicy attached. You can verify this in the IAM console under Roles.

Help me fix my ECS task definition error

Create ECS services and attach to ALB

An ECS service maintains a desired number of running tasks and integrates with your load balancer. When you attach a service to a target group, ECS automatically registers each task's IP address as a target.

  • In the ECS console, click Clusters in the left navigation pane.
  • Click healthcare-cluster to open it.
  • Select the Services tab and click Create.

Configure the patient service first.

  • Under Deployment configuration, for Task definition, select patient-service (latest revision).
  • For Service name, enter patient-service.
  • For Desired tasks, enter 1.
  • Expand the Networking section.
  • Select your default VPC and its public subnets.
  • For Security group, choose Create a new security group.
  • Name it ecs-tasks-sg and add an inbound rule: Type = Custom TCP, Port range = 5000, Source = the ALB's security group.
  • Set Public IP to Turned on.

Why allow traffic only from the ALB security group?

Setting the inbound source to the ALB's security group means only traffic routed through the load balancer can reach your containers. This prevents anyone from bypassing the ALB and hitting your tasks directly.

  • Expand the Load balancing section.
  • For Load balancer type, select Application Load Balancer.
  • Select the ALB you created in the previous step.
  • For Container, select patient-service 5000:5000.
  • Under Target group, choose Use an existing target group and select patient-tg.
  • Click Create to deploy the service.

Now create the staff service using the same process.

  • Return to healthcare-cluster and click Create under the Services tab.
  • For Task definition, select staff-service (latest revision).
  • For Service name, enter staff-service.
  • For Desired tasks, enter 1.
  • Under Networking, select the same VPC, public subnets, and the ecs-tasks-sg security group you just created.
  • Set Public IP to Turned on.
  • Expand Load balancing, select Application Load Balancer, and choose your ALB.
  • For Container, select staff-service 5000:5000.
  • Under Target group, choose Use an existing target group and select staff-tg.
  • Click Create.

Both services will take 2-3 minutes to start their tasks. Watch the Services tab in your cluster until both show a Running task count of 1.

What happens during deployment?

ECS pulls your Docker image from ECR, launches a Fargate task in a public subnet, assigns it an IP address, and registers that IP with the ALB target group. The ALB health check then confirms the task is healthy before routing traffic to it.

  • Once both services show Running, navigate to the EC2 console and click Load Balancers in the left navigation pane.
  • Select your ALB and copy the DNS name value.
  • Open a new browser tab and paste the ALB DNS name. You should see the Welcome to HealthFirst Clinic patient portal page.
  • Now add /admin/ to the end of the URL. You should see the Staff Dashboard showing the total patients registered.

Tasks stuck in PENDING or showing STOPPED?

Check the Tasks tab in your service. Click the stopped task to view its Stopped reason. Common causes:

  • Image URI typo. Verify the ECR URI matches exactly by copying it from the ECR console.
  • Security group blocking the RDS connection. Ensure your RDS security group allows inbound on port 3306 from the ECS tasks security group.
  • Wrong environment variable values. The DB_HOST must be the full RDS endpoint (not just the database name).

Help me debug why my ECS Fargate task keeps stopping

🙋‍♀️ ALB shows 503 Service Unavailable?

This means the target group has no healthy targets yet. Wait 2-3 minutes for the health checks to pass. You can check health status in the EC2 console under Target Groups by selecting your target group and viewing the Targets tab.

If targets stay unhealthy, ensure port 5000 is used in both the target group health check and your container's port mapping.

Secret mission

Add CloudWatch Logs and Health Checks

Your microservices are running, but how do you know they're healthy? In production, you need automated health monitoring and centralized logging. In this secret mission, you'll add /health endpoints, configure CloudWatch Logs, and update your ALB to use application-level health checks.

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.

Cost warning

ECS Fargate tasks are billed per vCPU-second and memory-second while running. Two minimal tasks cost approximately $0.50-1.00/day.

Your RDS instance and ALB are covered under the AWS Free Tier for 12 months (if your account is eligible). If you leave Fargate tasks running beyond this project, charges will accumulate.

Resources you used:

  • ECS Fargate services (patient-service and staff-service in healthcare-cluster).
  • Application Load Balancer with target groups (patient-tg and staff-tg).
  • RDS MySQL instance (database clinic).
  • ECR repositories (patient-service and staff-service).
  • CloudWatch log group (healthcare-microservices).
  • IAM role (ecsTaskExecutionRole).
  • Local project files (patient-service/, staff-service/, seed-database.py).

Keep everything running

No action needed. Choose this if you are still actively building on this project or plan to extend it with CI/CD.

  • Your ECS services will continue running and serving traffic through the ALB.
  • Fargate tasks will cost approximately $0.50-1.00/day for two minimal tasks.
  • RDS and ALB remain covered under the AWS Free Tier (if eligible).
  • AWS CLI and Docker Desktop remain installed on your machine.

Pause - I'll come back to this later

Shut down running compute to stop Fargate charges while keeping your images and database intact.

  • Set both ECS services to 0 desired tasks by running:
aws ecs update-service --cluster healthcare-cluster --service patient-service --desired-count 0
aws ecs update-service --cluster healthcare-cluster --service staff-service --desired-count 0
  • Stop the RDS instance from the RDS console. Select your database, choose Actions, then Stop temporarily.
  • Your ALB remains running but costs nothing within the Free Tier.
  • Your ECR images and CloudWatch logs remain stored.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps in order.

  • Delete both ECS services by running:
aws ecs delete-service --cluster healthcare-cluster --service patient-service --force
aws ecs delete-service --cluster healthcare-cluster --service staff-service --force
  • Delete the ECS cluster by running:
aws ecs delete-cluster --cluster healthcare-cluster
  • Delete both ECR repositories by running:
aws ecr delete-repository --repository-name patient-service --force --region us-east-1
aws ecr delete-repository --repository-name staff-service --force --region us-east-1
  • Delete the CloudWatch log group by running:
aws logs delete-log-group --log-group-name healthcare-microservices --region us-east-1
  • Delete the RDS instance from the RDS console. Select your database, choose Actions, then Delete. Uncheck Create final snapshot if you do not need a backup, confirm the deletion, and choose Delete.
  • Delete the Application Load Balancer from the EC2 console. Navigate to Load Balancers, select your ALB, choose Actions, then Delete load balancer.
  • Delete the target groups. Navigate to Target Groups in the EC2 console, select patient-tg and staff-tg, choose Actions, then Delete.
  • Delete the IAM role ecsTaskExecutionRole from the IAM console. Navigate to Roles, search for ecsTaskExecutionRole, select it, and choose Delete.
  • Delete local project files by running:
cd ~/Desktop
rm -rf patient-service staff-service seed-database.py

Nice Work!

Nice Work!

You just deployed a healthcare microservices platform on AWS ECS Fargate. Two containerized Flask applications are running in production, connected to a shared database, and accessible through a single load-balanced endpoint.

You've learned how to:

  • Containerized two Python Flask microservices with Docker and pushed images to Amazon ECR.
  • Deployed both containers on AWS Fargate behind an Application Load Balancer with a shared RDS MySQL database.
  • Configured path-based routing to direct traffic to the correct microservice based on URL path.
  • Secret Mission: Added CloudWatch Logs monitoring and /health endpoints for production-readiness.

Ready to quiz yourself?