Deploy a Bedrock Chatbot on ECS Fargate
Deploy a containerized AI chatbot on ECS Fargate with Bedrock and S3 integration.
Introduction
30 Second Summary
When something breaks in production, knowing how to find and fix the problem quickly is what separates confident engineers from panicked ones. The best way to build that confidence is to break things on purpose.
In this project, you will build a containerized AI chatbot with Docker and deploy it on AWS. Then you will deliberately break it three different ways to practice diagnosing real container failures.
What You'll Build
A Python Flask chatbot deployed on Amazon ECS Fargate that calls Amazon Bedrock for AI responses and logs each conversation to S3 as a JSON artifact.
By the end of this project, you'll have:
- A containerized chatbot running on ECS Fargate that generates AI responses using Amazon Bedrock and stores conversation logs in S3.
- An ECR repository with your pushed container image and IAM roles granting your container Bedrock and S3 permissions at runtime.
- Hands-on experience diagnosing 3 real ECS failure scenarios: CannotPullContainerError, OutOfMemoryError, and AccessDeniedException.
- A load balancer with health checks fronting your chatbot service (Secret Mission).
Are there any prerequisites?
You need an AWS account with admin access, the AWS CLI installed and configured, and Docker installed on your machine. Python runs inside the container, so you do not need it installed locally.
Set Up IAM Roles and S3 Bucket
Before you can deploy a chatbot container, it needs the right permissions to talk to other AWS services.
In this step, you'll create the IAM roles your ECS tasks need, plus the S3 bucket where your chatbot will store conversation logs.
In this step, get ready to:
- Create an S3 bucket for storing chatbot conversation artifacts.
- Create an ECS task execution role that lets the ECS agent pull images and write logs.
- Create an ECS task role that grants your chatbot container access to Amazon Bedrock and S3.
Create the S3 bucket
Your chatbot will save every conversation (the user's message and the AI's response) as a JSON file in S3.
- Create the S3 bucket by running this command:
aws s3 mb s3://nextwork-chatbot-artifacts-[[YOURNAME="enter your name"]]
Why do I need my name in the bucket name?
S3 bucket names are globally unique across all AWS accounts. Including your name prevents a naming collision with another learner's bucket.
- You should see a make_bucket confirmation message in the output.
Seeing a BucketAlreadyExists error?
Someone else already has a bucket with that name. Add a few extra characters (like the current date) to make it unique.
Help me fix my S3 bucket name.
Create the ECS task execution role
ECS needs two different IAM roles to run your container. The first is the task execution role, which the ECS agent uses behind the scenes to pull your container image from ECR and send logs to CloudWatch.
Both roles need a trust policy that allows the ECS task service to assume them. You'll create this trust policy first and reuse it for both roles.
- Create the trust policy file by running this command:
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
What is a trust policy?
A trust policy defines which AWS service is allowed to "assume" (use) this role. Here, ecs-tasks.amazonaws.com means only ECS tasks can use the role.
- Confirm the file was created by running:
ls
- You should see trust-policy.json in the output.
Now you'll use this trust policy to create the task execution role and attach the AWS managed policy that gives it the right permissions.
- Create the task execution role by running this command:
aws iam create-role \
--role-name ecsTaskExecutionRole \
--assume-role-policy-document file://trust-policy.json
- You should see a JSON response containing the role's ARN and creation details.
- Attach the AmazonECSTaskExecutionRolePolicy managed policy to this role by running:
aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
What does this policy include?
The AmazonECSTaskExecutionRolePolicy managed policy grants permissions to pull container images from ECR and write log streams to CloudWatch. These are infrastructure-level permissions the ECS agent needs. Your application code never uses this role directly.
- Verify the policy is attached by running:
aws iam list-attached-role-policies --role-name ecsTaskExecutionRole
- You should see AmazonECSTaskExecutionRolePolicy listed in the output.
Seeing an EntityAlreadyExists error?
This means a role named ecsTaskExecutionRole already exists in your account. This is fine. Skip the create-role command and just run the attach-role-policy command to make sure the managed policy is attached.
Help me troubleshoot my ECS task execution role.
Create the ECS task role with Bedrock and S3 permissions
The second role is the task role. This is the role your chatbot container uses at runtime to call Amazon Bedrock and write files to S3.
What is the difference between the task role and the task execution role?
The task execution role is for the ECS agent. It handles infrastructure tasks like pulling your container image from ECR and writing logs to CloudWatch.
The task role is for your application code. It controls what AWS services your container can call while it runs. Mixing these up is one of the most common ECS permission mistakes.
- Create the task role using the same trust policy by running this command:
aws iam create-role \
--role-name chatbot-task-role \
--assume-role-policy-document file://trust-policy.json
- You should see a JSON response with the new role's ARN.
Now you need to give this role the specific permissions your chatbot needs. You'll create an inline policy that grants access to invoke the Claude Haiku 4.5 model on Bedrock and read/write objects in your S3 bucket.
- Create the task policy file by running this command:
cat > task-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::nextwork-chatbot-artifacts-[[YOURNAME="enter your name"]]/*"
}
]
}
EOF
What does this policy do?
- The first statement grants bedrock:InvokeModel permission, scoped to the Claude Haiku 4.5 foundation model only. The double colon in the ARN (::foundation-model) means the model is owned by AWS, not your account.
- The second statement grants s3:PutObject and s3:GetObject on your specific S3 bucket. The /* at the end means "any object inside this bucket."
- Attach this inline policy to the task role by running:
aws iam put-role-policy \
--role-name chatbot-task-role \
--policy-name chatbot-bedrock-s3-policy \
--policy-document file://task-policy.json
What is the difference between attach-role-policy and put-role-policy?
The attach-role-policy command attaches a reusable AWS managed policy. The put-role-policy command creates an inline policy embedded directly in the role. Inline policies are useful when the permissions are specific to one role.
- Verify the inline policy is attached by running:
aws iam get-role-policy \
--role-name chatbot-task-role \
--policy-name chatbot-bedrock-s3-policy
- You should see the full policy document printed back, confirming bedrock:InvokeModel, s3:PutObject, and s3:GetObject are all listed.
Seeing a NoSuchEntity error?
Make sure you're using the exact role name chatbot-task-role. Role names are case-sensitive. If the create-role command earlier failed, re-run it before attaching the policy.
Help me fix my task role policy.
Your AWS foundation is ready. You have an S3 bucket for conversation logs, a task execution role for the ECS agent, and a task role that gives your chatbot access to Bedrock and S3. Next up, you'll build the chatbot application and package it into a Docker image.
Build the Chatbot Application and Docker Image
Your IAM roles and S3 bucket are in place. The infrastructure knows who your chatbot is and what it's allowed to do. Now it's time to build the chatbot itself.
You'll write a Python Flask application that receives user messages, forwards them to Amazon Bedrock for AI-generated responses, and saves each conversation to S3. Then you'll package the whole thing into a Docker image so it's ready to deploy on ECS.
In this step, get ready to:
- Write the Flask chatbot application with a /chat endpoint.
- Create the Dockerfile and dependencies file.
- Build and test the Docker image locally.
Write the Flask chatbot application
The chatbot is a single Python file that exposes an HTTP endpoint. When a user sends a message, the app calls Bedrock's Claude Haiku 4.5 model and logs the full conversation to S3 as a JSON artifact.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir bedrock-chatbot
cd bedrock-chatbot
- Confirm the folder exists by running:
ls
The folder is empty for now. You'll add three files to it over the next few minutes.
Open the bedrock-chatbot folder in your preferred code editor. The examples below use VS Code, but any text editor works.
- Create a new file called app.py inside your bedrock-chatbot folder.
The first section of app.py sets up the AWS clients and constants your chatbot needs. Both the Bedrock and S3 clients are created once when the app starts, so every request reuses them.
- In app.py, paste the following imports and initialization code:
import json
import os
import uuid
from datetime import datetime, timezone
import boto3
from flask import Flask, request, jsonify
# Create the Flask app
app = Flask(__name__)
# Set up AWS clients for Bedrock (AI model) and S3 (file storage)
bedrock_client = boto3.client("bedrock-runtime", region_name=os.environ.get("AWS_REGION", "us-east-1"))
s3_client = boto3.client("s3", region_name=os.environ.get("AWS_REGION", "us-east-1"))
# Claude Haiku 4.5 model identifier and the S3 bucket for saving conversations
MODEL_ID = "anthropic.claude-haiku-4-5-20251001-v1:0"
S3_BUCKET = os.environ.get("S3_BUCKET", "my-chatbot-artifacts")
What does this code do?
- boto3.client("bedrock-runtime") creates a client for calling AI models hosted on Amazon Bedrock.
- boto3.client("s3") creates a client for reading and writing files in S3.
- MODEL_ID points to Claude Haiku 4.5, the cheapest Claude model on Bedrock at $1 per million input tokens.
- S3_BUCKET reads the bucket name from an environment variable. You'll set this when deploying to ECS.
You won't see a change yet. The next chunk adds the route that brings the chatbot to life.
Now for the core of the application. The /chat endpoint accepts a user message, sends it to Bedrock, saves the conversation to S3, and returns the AI response.
- Below the S3_BUCKET line you just added, paste the /chat endpoint and the main block:
@app.route("/chat", methods=["POST"])
def chat():
# Validate that the request includes a message
data = request.get_json()
if not data or "message" not in data:
return jsonify({"error": "Missing 'message' field in request body"}), 400
user_message = data["message"]
# Build the request body in the format Claude expects
native_request = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512,
"messages": [
{
"role": "user",
"content": user_message
}
]
}
# Call Bedrock to get the AI response
try:
response = bedrock_client.invoke_model(
modelId=MODEL_ID,
contentType="application/json",
accept="application/json",
body=json.dumps(native_request)
)
response_body = json.loads(response["body"].read())
assistant_message = response_body["content"][0]["text"]
except Exception as e:
return jsonify({"error": f"Bedrock invocation failed: {str(e)}"}), 500
# Build a conversation record with a unique ID and timestamp
conversation = {
"id": str(uuid.uuid4()),
"timestamp": datetime.now(timezone.utc).isoformat(),
"user_message": user_message,
"assistant_message": assistant_message
}
# Save the conversation to S3 as a JSON file
try:
s3_key = f"conversations/{conversation['id']}.json"
s3_client.put_object(
Bucket=S3_BUCKET,
Key=s3_key,
Body=json.dumps(conversation, indent=2),
ContentType="application/json"
)
except Exception as e:
app.logger.warning(f"Failed to save conversation to S3: {str(e)}")
# Return the AI response and conversation ID to the caller
return jsonify({
"response": assistant_message,
"conversation_id": conversation["id"]
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
- The endpoint first validates that the incoming JSON has a message field. If not, it returns a 400 error.
- It builds a request in Claude's native format. The anthropic_version field is required for all Claude models on Bedrock.
- invoke_model sends the message to Bedrock and returns the AI response.
- Each conversation is saved to S3 under a conversations/ prefix with a unique UUID filename. If the S3 write fails, the app logs a warning but still returns the response to the user.
- app.run(host="0.0.0.0", port=5000) starts the Flask development server, listening on all network interfaces so Docker can forward traffic to it.
Why use the InvokeModel API instead of the Converse API?
Bedrock offers two ways to call models: the InvokeModel API and the higher-level Converse API. InvokeModel is the most direct path. You send the model's native request format and get the raw response back. For a single-turn chatbot, this keeps the code simple and avoids abstraction you don't need.
- Save app.py.
Seeing a syntax error?
- Make sure you pasted the second block directly below the first block, with no duplicate lines. The two code chunks together form the complete file.
- Check that the f-strings use curly braces with no extra spaces inside them.
- help me find a syntax error in my app.py file
Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
I'd like to double check the full code
import json
import os
import uuid
from datetime import datetime, timezone
import boto3
from flask import Flask, request, jsonify
app = Flask(__name__)
bedrock_client = boto3.client("bedrock-runtime", region_name=os.environ.get("AWS_REGION", "us-east-1"))
s3_client = boto3.client("s3", region_name=os.environ.get("AWS_REGION", "us-east-1"))
MODEL_ID = "anthropic.claude-haiku-4-5-20251001-v1:0"
S3_BUCKET = os.environ.get("S3_BUCKET", "my-chatbot-artifacts")
@app.route("/chat", methods=["POST"])
def chat():
data = request.get_json()
if not data or "message" not in data:
return jsonify({"error": "Missing 'message' field in request body"}), 400
user_message = data["message"]
native_request = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512,
"messages": [
{
"role": "user",
"content": user_message
}
]
}
try:
response = bedrock_client.invoke_model(
modelId=MODEL_ID,
contentType="application/json",
accept="application/json",
body=json.dumps(native_request)
)
response_body = json.loads(response["body"].read())
assistant_message = response_body["content"][0]["text"]
except Exception as e:
return jsonify({"error": f"Bedrock invocation failed: {str(e)}"}), 500
conversation = {
"id": str(uuid.uuid4()),
"timestamp": datetime.now(timezone.utc).isoformat(),
"user_message": user_message,
"assistant_message": assistant_message
}
try:
s3_key = f"conversations/{conversation['id']}.json"
s3_client.put_object(
Bucket=S3_BUCKET,
Key=s3_key,
Body=json.dumps(conversation, indent=2),
ContentType="application/json"
)
except Exception as e:
app.logger.warning(f"Failed to save conversation to S3: {str(e)}")
return jsonify({
"response": assistant_message,
"conversation_id": conversation["id"]
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Create the Dockerfile and dependencies
Your app depends on two Python packages: Flask for the web server and boto3 for talking to AWS services. You need a requirements.txt file to list them, and a Dockerfile to package everything into a container image.
- Create a new file called requirements.txt in your bedrock-chatbot folder and paste the following:
flask==3.1.3
boto3==1.42.94
Why pin exact versions?
Pinning versions with == guarantees your container installs the same packages every time you build it. Without pinned versions, a newer release could introduce breaking changes and your chatbot would fail in production with no code changes on your end.
- Save requirements.txt.
- Create another new file called Dockerfile (no file extension) in your bedrock-chatbot folder and paste the following:
# Use a lightweight Python base image
FROM python:3.12-slim
# Set the working directory inside the container
WORKDIR /app
# Install dependencies first (Docker caches this layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY app.py .
# Tell Docker the container listens on port 5000
EXPOSE 5000
# Start the Flask server when the container runs
CMD ["python", "app.py"]
What does this Dockerfile do?
- FROM python:3.12-slim starts from a minimal Python 3.12 image. The slim variant strips out extras you don't need, keeping the image small.
- requirements.txt is copied and installed before app.py. This order matters for Docker layer caching. If you change app.py later, Docker reuses the cached dependency layer instead of reinstalling everything.
- EXPOSE 5000 documents which port the app listens on. ECS uses this information when setting up networking.
- CMD runs your Flask server when the container starts.
- Save Dockerfile.
- Confirm all three files exist by running:
ls
You should see app.py, Dockerfile, and requirements.txt listed in the output.
Missing a file?
- Make sure you are inside the bedrock-chatbot folder, not its parent. Run pwd to check your current directory.
- Check that Dockerfile has no file extension. Some editors add .txt automatically.
Awesome, I've got everything!
Great. Double check you've saved all three files before moving on.
I'd like to double check the full code
flask==3.1.3
boto3==1.42.94
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"]
Build and test the Docker image locally
Before pushing your image to the cloud, build it locally to catch any packaging errors early.
- Build your Docker image by running this command from the bedrock-chatbot folder:
docker build -t chatbot-app .
What does this command do?
- -t chatbot-app tags your image with the name chatbot-app so you can reference it later.
- . tells Docker to use the current directory as the build context, which is where it looks for your Dockerfile.
The build takes about a minute. You should see Docker downloading the Python base image, installing your pip dependencies, and copying app.py into the image.
Build failing?
- If you see Cannot connect to the Docker daemon, make sure Docker Desktop is running.
- If you see no such file or directory, make sure your terminal is inside the bedrock-chatbot folder where your Dockerfile lives.
- help me fix a Docker build error for my Flask chatbot
Now verify that the container starts correctly. You can't actually chat with the bot locally because there are no AWS credentials inside the container. The goal here is to confirm Flask starts without import errors or crashes.
- Start the container by running:
docker run -p 5000:5000 chatbot-app
You should see Flask's startup output, including a line like Running on http://0.0.0.0:5000. That confirms your application code, dependencies, and Dockerfile are all correct.
- Press Ctrl+C to stop the container.
Container exits immediately?
- If the container exits with an ImportError, double check that requirements.txt includes both flask and boto3, then rebuild with docker build -t chatbot-app ..
- If you see a SyntaxError, check app.py for mismatched parentheses or quotation marks.
- help me debug why my Docker container exits immediately
Your chatbot image is built and verified. Next up, you'll push it to ECR so ECS can pull it and run it in the cloud.
Push the Image to ECR and Create the ECS Cluster
Your chatbot image is built and tested locally. Now it's time to move it into AWS so your containers can run in the cloud.
You'll push the image to Amazon ECR, a managed container registry that stores Docker images. Then you'll create an ECS cluster, which is the foundation for running containers on AWS.
In this step, get ready to:
- Create an ECR repository to store your Docker image.
- Push your chatbot image to ECR.
- Create an ECS cluster for running your containers.
Create an ECR repository
ECR is a fully managed Docker container registry. It stores your images securely and makes them available for ECS to pull when launching containers.
- Create a private ECR repository by running this command:
aws ecr create-repository --repository-name chatbot-app --region us-east-1
What does this command do?
This creates a private container registry called chatbot-app in your AWS account. The output includes a repositoryUri field, which is the address you'll use to push and pull images.
- You should see JSON output containing a repositoryUri that looks like 123456789012.dkr.ecr.us-east-1.amazonaws.com/chatbot-app.
Seeing RepositoryAlreadyExistsException?
This means a repository named chatbot-app already exists in your account. You can safely continue with the existing repository, or delete it first with aws ecr delete-repository --repository-name chatbot-app --force and re-run the create command.
Help me fix my ECR repository creation error.
Push your image to ECR
Before you can push, Docker needs to authenticate with your ECR registry. You also need to tag your local image with the full ECR repository URI.
- Find your AWS account ID by running this command:
aws sts get-caller-identity --query Account --output text
What does this return?
This prints your 12-digit AWS account ID. You'll use this number in the next commands to construct your ECR repository address.
- Copy the 12-digit number from your terminal output. You'll paste it into the next commands wherever you see the account ID placeholder.
- 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
- You should see Login Succeeded in your terminal.
Login failed?
- Make sure Docker is running on your machine. Open Docker Desktop if it isn't started.
- Verify your AWS CLI is configured by running aws configure list. You should see your access key and region listed.
- Double check that you replaced the account ID placeholder with your actual 12-digit account ID.
Help me troubleshoot my ECR Docker login failure.
- Tag your local image with the ECR repository URI by running this command:
docker tag chatbot-app:latest [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/chatbot-app:latest
- Push the tagged image to ECR by running this command:
docker push [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/chatbot-app:latest
What do these commands do?
- docker tag creates a new name for your local image that includes the full ECR address. Docker uses this name to know where to push the image.
- docker push uploads each layer of your image to the ECR repository. ECS pulls from this address when it launches your container.
- You should see each layer being pushed, ending with a digest: sha256:... line followed by the image tag. This confirms your image is stored in ECR.
Push denied or timeout?
- If you see no basic auth credentials, your Docker login has expired. Re-run the aws ecr get-login-password command above to re-authenticate.
- Make sure the repository name in your tag matches the repository you created (chatbot-app).
Help me fix my Docker push to ECR.
Confirm your image is in ECR by checking the AWS console.
- Open the AWS Management Console in your browser.
- Search for ECR in the top search bar and select Elastic Container Registry.
- Click on the chatbot-app repository.
- You should see your image listed with the latest tag.
Create an ECS cluster
An ECS cluster is a logical grouping that manages your running containers. Think of it as the home base where your tasks and services live.
- Create an ECS cluster by running this command:
aws ecs create-cluster --cluster-name chatbot-cluster --region us-east-1
What does this command do?
This creates an empty ECS cluster named chatbot-cluster. The cluster itself doesn't run anything yet. In the next step, you'll define a task and launch it as a Fargate service inside this cluster.
- You should see JSON output containing "status": "ACTIVE" in the cluster object. This confirms your cluster is ready.
Cluster creation failed?
- Check that your AWS CLI is configured for the correct region by running aws configure get region.
- If you see an AccessDenied error, make sure your AWS CLI credentials have permissions to create ECS resources.
Help me troubleshoot my ECS cluster creation.
Your image is stored in ECR and your cluster is ready. Next up, you'll create a task definition and deploy your chatbot as a running Fargate service.
Deploy the Chatbot as a Fargate Service
In the previous step, you pushed your chatbot image to ECR and created an ECS cluster. Now it is time to deploy the chatbot as a live Fargate service that anyone can reach over the internet.
To launch a container on Fargate, you need two things: a task definition that describes how the container should run, and a service that keeps the task running inside your cluster. You will also need a security group that allows inbound traffic on port 5000 so you can reach your chatbot.
In this step, get ready to:
- Register a Fargate task definition for your chatbot container.
- Create a security group and a Fargate service in your cluster.
- Test the chatbot by sending a curl request to the running task.
Create the task definition JSON file
A task definition is a blueprint that tells ECS how to run your container. It specifies the container image, port mappings, resource limits, environment variables, and the IAM roles the container uses.
- In your project folder, create a file called task-definition.json and paste the following JSON into it:
{
"family": "chatbot-task",
"networkMode": "awsvpc",
"taskRoleArn": "arn:aws:iam::[[AWS_ACCOUNT_ID=\"your 12-digit AWS account ID\"]]:role/ecsTaskRole",
"executionRoleArn": "arn:aws:iam::[[AWS_ACCOUNT_ID=\"your 12-digit AWS account ID\"]]:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "chatbot",
"image": "[[ECR_IMAGE_URI=\"your ECR image URI from the previous step\"]]",
"portMappings": [
{
"containerPort": 5000,
"hostPort": 5000,
"protocol": "tcp"
}
],
"essential": true,
"environment": [
{
"name": "S3_BUCKET",
"value": "[[S3_BUCKET_NAME=\"your S3 bucket name from Step 1\"]]"
},
{
"name": "AWS_REGION",
"value": "us-east-1"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/chatbot-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
}
}
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "256",
"memory": "512"
}
What does this task definition do?
- family names this task definition group. ECS tracks revisions under this name.
- networkMode is set to awsvpc, which is required for Fargate. Each task gets its own elastic network interface with a private IP.
- taskRoleArn is the IAM role your chatbot container uses at runtime to call Bedrock and S3. This is the task role you created in Step 1.
- executionRoleArn is the IAM role that the ECS agent uses to pull your image from ECR and write logs to CloudWatch.
- cpu and memory are set to 256 (0.25 vCPU) and 512 (0.5 GB). This is the smallest Fargate task size.
- logConfiguration uses the awslogs driver to send container logs to CloudWatch. You will use these logs for troubleshooting in later steps.
Task role vs. execution role
These two roles serve different purposes and are a common source of confusion. The task execution role gives the ECS agent permission to pull images and write logs. The task role gives your container permission to call AWS services like Bedrock and S3 at runtime.
- Register the task definition by running:
aws ecs register-task-definition --cli-input-json file://task-definition.json
- Confirm the registration succeeded. You should see JSON output that includes "family": "chatbot-task" and "revision": 1.
Seeing an error about the role ARN?
- Double-check that you replaced the account ID placeholder with your actual 12-digit AWS account ID.
- Make sure the role names match exactly what you created in Step 1 (ecsTaskRole and ecsTaskExecutionRole).
- help me fix my task definition registration error
Create a security group and launch the Fargate service
Before creating the service, you need a security group that allows inbound TCP traffic on port 5000. Without this, your chatbot container will be unreachable from the internet.
- Find your default VPC ID by running:
aws ec2 describe-vpcs --filters Name=is-default,Values=true --query "Vpcs[0].VpcId" --output text
- Copy the VPC ID from the output (it looks like vpc-0abc1234def56789).
- Create a security group for your chatbot by running:
aws ec2 create-security-group \
--group-name chatbot-sg \
--description "Allow inbound traffic on port 5000 for chatbot" \
--vpc-id [[VPC_ID="your default VPC ID"]]
- Copy the GroupId from the output (it looks like sg-0abc1234def56789).
- Open port 5000 to all inbound traffic by running:
aws ec2 authorize-security-group-ingress \
--group-id [[SG_ID="your security group ID"]] \
--protocol tcp \
--port 5000 \
--cidr 0.0.0.0/0
Why 0.0.0.0/0?
The CIDR 0.0.0.0/0 allows traffic from any IP address. This is fine for a learning project, but in production you would restrict this to specific IP ranges or place the service behind a load balancer.
Now you need a public subnet ID from your default VPC to place the task in.
- Find a subnet ID by running:
aws ec2 describe-subnets \
--filters Name=vpc-id,Values=[[VPC_ID="your default VPC ID"]] \
--query "Subnets[0].SubnetId" --output text
- Copy the subnet ID from the output (it looks like subnet-0abc1234def56789).
- Create the Fargate service by running:
aws ecs create-service \
--cluster chatbot-cluster \
--service-name chatbot-service \
--task-definition chatbot-task \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[[[SUBNET_ID=\"your subnet ID\"]]],securityGroups=[[[SG_ID=\"your security group ID\"]]],assignPublicIp=ENABLED}"
What does this command do?
- --desired-count 1 tells ECS to always keep exactly one task running.
- --launch-type FARGATE means AWS manages the underlying compute. You do not need to provision any servers.
- assignPublicIp=ENABLED assigns a public IP to the task so it can pull images from ECR and be reachable from the internet.
- Wait about 1-2 minutes for the task to start, then verify the service is running:
aws ecs describe-services --cluster chatbot-cluster --services chatbot-service --query "services[0].runningCount"
- You should see 1 in the output, meaning one task is running.
Running count still showing 0?
- The task may still be pulling the image. Wait another minute and run the command again.
- Check the service events for errors by running aws ecs describe-services --cluster chatbot-cluster --services chatbot-service --query "services[0].events[:3]".
- Make sure the subnet you chose is a public subnet with an internet gateway route. Default VPC subnets are public by default.
- my ECS service running count is stuck at 0, help me debug
Test the chatbot with a curl request
Your chatbot is running on Fargate, but you need to find its public IP address to send it a request. Fargate tasks with awsvpc networking get their own elastic network interface (ENI) with a public IP.
- Get the task ARN by running:
aws ecs list-tasks --cluster chatbot-cluster --service-name chatbot-service --query "taskArns[0]" --output text
- Copy the task ARN from the output.
- Get the ENI ID from the task by running:
aws ecs describe-tasks \
--cluster chatbot-cluster \
--tasks [[TASK_ARN="your task ARN"]] \
--query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" \
--output text
- Copy the ENI ID from the output (it looks like eni-0abc1234def56789).
- Get the public IP address from the ENI by running:
aws ec2 describe-network-interfaces \
--network-interface-ids [[ENI_ID="your ENI ID"]] \
--query "NetworkInterfaces[0].Association.PublicIp" \
--output text
- Copy the public IP address from the output.
- Send a test message to your chatbot by running:
curl -X POST http://[[TASK_PUBLIC_IP="your task public IP"]]:5000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello, what is Amazon ECS?"}'
You should see a JSON response containing a response field with an AI-generated answer and a conversation_id field.
What just happened?
Your curl request hit the Flask chatbot running inside a container on Fargate. The chatbot called Amazon Bedrock to generate a response using Claude Haiku 4.5. It then saved the conversation as a JSON file in your S3 bucket under the conversations/ prefix.
Curl request timing out?
- Verify that your security group allows inbound TCP traffic on port 5000. Run aws ec2 describe-security-groups --group-ids your-sg-id and check the inbound rules.
- Make sure you are using http:// (not https://) in your curl command. Flask's development server does not support HTTPS.
- Confirm the task is actually running by checking aws ecs describe-tasks --cluster chatbot-cluster --tasks your-task-arn --query "tasks[0].lastStatus".
- my curl request to the ECS Fargate chatbot is timing out
Your chatbot is live on AWS. Next up, you will deliberately break it in three different ways and practice diagnosing each failure using real AWS troubleshooting tools.
Troubleshooting Scenario 1 - CannotPullContainer
Your chatbot is live on ECS Fargate and responding to messages. Time to break it.
Container failures are inevitable in production. The best way to build troubleshooting skills is to cause these failures yourself and practice diagnosing them.
In this step, get ready to:
- Break your task definition by pointing it at a non-existent image tag.
- Diagnose the CannotPullContainerError using the ECS stopped tasks view.
- Fix the image URI and verify your service recovers.
Break the image URI and force a new deployment
When ECS launches a Fargate task, the first thing it does is pull the container image from the registry. If the image URI in the task definition points to a tag that doesn't exist, the pull fails and the task never starts.
You'll create a new revision of your task definition with a broken image tag to simulate this failure.
- In the Amazon ECS console, click Task definitions in the left sidebar.
- Click on your task definition family name.
- Click Create new revision, then select Create new revision from the dropdown.
- Scroll down to the Container - 1 section.
- Find the Image URI field. It ends with a tag like :latest.
- Change the tag at the end of the URI from latest to broken-tag.
- Leave everything else unchanged and click Create at the bottom of the page.
What is a task definition revision?
Every time you modify a task definition, ECS creates a new numbered revision. Your original working configuration is revision 1. The broken one you just created is revision 2. ECS keeps all revisions, so you can always switch back to a working one.
Now update your service to use this broken revision.
- Click Clusters in the left sidebar, then click on your cluster name.
- In the Services tab, click on your service name.
- Click Update in the top right.
- In the Revision dropdown under Task definition, select the latest revision (the broken one you just created).
- Toggle on Force new deployment.
- Click Update at the bottom of the page.
ECS now attempts to start a new task using the broken image URI. Give it 1-2 minutes to fail.
Diagnose the CannotPullContainerError
The task you just deployed can't find the broken-tag image in your ECR repository. ECS marks it as stopped and records the reason.
- On your service page, click the Tasks tab.
- Change the status filter to Stopped to show failed tasks.
- Click on the most recent stopped task to open its details.
- Find the Stopped reason field near the top of the page.
The stopped reason shows CannotPullContainerError followed by a message about the image not being found. This confirms ECS could not pull the container image from the registry.
- Click back to your service page and check the Events tab for additional deployment messages.
What does CannotPullContainerError mean?
This error means the ECS agent tried to pull the container image from the registry but failed. The most common causes are:
- The image tag doesn't exist in the repository.
- The repository URI is misspelled.
- The task execution role lacks permission to pull from ECR.
- Network configuration prevents reaching the registry.
In this case, you used a tag (broken-tag) that doesn't exist in your repository.
Stopped tasks disappear after 1 hour
ECS only keeps stopped task details visible in the console for 1 hour. After that, they're gone. In production, you'd send these events to CloudWatch Logs or EventBridge for long-term retention.
Fix the image URI and redeploy
Now that you've identified the problem, the fix is straightforward. Create another task definition revision with the correct image tag and update the service.
- Click Task definitions in the left sidebar.
- Click on your task definition family name.
- Click Create new revision, then select Create new revision from the dropdown.
- In the Image URI field, change the tag from broken-tag back to latest.
- Click Create.
- Navigate back to your cluster and click on your service name.
- Click Update.
- In the Revision dropdown, select the latest revision (the fixed one you just created).
- Toggle on Force new deployment and click Update.
- Wait 1-2 minutes for the new task to start.
- Click the Tasks tab and confirm a task with RUNNING status appears.
Your chatbot is healthy again. When you see CannotPullContainerError, always check the image URI and ECR repository first.
Service still failing after the fix?
- Make sure you selected the correct revision (the fixed one, not the broken one) when updating the service.
- Confirm the image tag matches exactly what you pushed to ECR in Step 3.
- Check that the Force new deployment toggle was enabled. Without it, ECS may keep running the old broken task.
- My ECS service is still failing with CannotPullContainerError after fixing the image URI. How do I troubleshoot?
You've diagnosed and fixed your first ECS failure. Next, you'll starve your container of memory and learn to diagnose the dreaded exit code 137.
Troubleshooting Scenario 2 - OutOfMemoryError
In the last step, you diagnosed a CannotPullContainerError caused by a bad image URI. That failure was loud and obvious. Now let's explore a sneakier problem: what happens when your container runs out of memory.
Memory issues are tricky because your container might start up just fine, then suddenly crash when it handles a real request. In this step, you'll deliberately starve your chatbot of memory and learn to spot the telltale signs of an OOM kill.
In this step, get ready to:
- Trigger an OutOfMemoryError by reducing your container's memory limit.
- Diagnose the failure using exit code 137 and stopped task details.
- Fix the memory allocation and verify your chatbot works again.
Trigger an OutOfMemoryError
Every ECS task definition includes a container-level memory hard limit. When a container exceeds that limit, Docker kills the process immediately with no graceful shutdown.
Your chatbot currently runs with 512 MB of task-level memory and no container-level hard limit. You'll add a hard limit of 128 MB, which is far too low for a Python app running Flask and boto3.
- Open the Amazon ECS console.
- Click Task definitions in the left sidebar.
- Click on your chatbot task definition family name.
- Click Create new revision, then select Create new revision from the dropdown.
- Scroll down to the Container definitions section and click on your container name to expand its settings.
- Find the Memory hard limit field and enter 128.
- Leave all other settings unchanged and click Create.
What is a memory hard limit?
The task-level memory (512 MB) sets the total memory for the entire Fargate task. The container-level hard limit is a separate cap that Docker enforces on a single container within that task.
When a container hits its hard limit, Docker terminates it with a SIGKILL signal. The container gets no warning and no chance to shut down gracefully.
Now you need to update your service to use this new task definition revision.
- Click Clusters in the left sidebar.
- Click on your cluster name.
- Click on your service name in the Services tab.
- Click Update in the top right corner.
- Under Revision, select the latest revision you just created.
- Check the Force new deployment checkbox.
- Click Update.
Wait about one to two minutes for the new task to launch. The container might crash at startup if Python's memory footprint already exceeds 128 MB, or it might survive startup and crash when you send a request.
- Click the Tasks tab on your service page.
- Check if a task shows a Running status. If it does, copy its public IP and send a chat request to trigger the OOM by running this command:
curl -X POST http://[[TASK_IP="your task's public IP"]]:5000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Tell me a long story about cloud computing"}'
What if the task already stopped?
If you see tasks cycling through Provisioning and Stopped states, the OOM is happening at startup. That's fine. The container is crashing before it can even serve a request. Skip the curl command and move to the diagnosis substep.
Diagnose the OutOfMemoryError
Whether the container crashed at startup or after your request, the diagnosis path is the same. ECS keeps records of stopped tasks for one hour, and each record tells you exactly why the task stopped.
- On your service page, click the Tasks tab.
- Use the filter dropdown to show Stopped tasks.
- Click on a stopped task to open its details page.
You should see two critical pieces of information on this page.
- Look at the Stopped reason field. It should say OutOfMemoryError or reference a memory limit.
- Scroll down to the Containers section and check the Exit code field. It should show 137.
What does exit code 137 mean?
Exit code 137 equals 128 + 9. The 9 is the signal number for SIGKILL, which is how the Linux OOM killer terminates a process. Unlike a normal shutdown signal (SIGTERM), SIGKILL cannot be caught or ignored.
Whenever you see exit code 137 on a container, your first instinct should be to check whether the memory allocation is sufficient for the workload.
How is this different from the CannotPullContainer error?
The CannotPullContainerError in Step 5 was an infrastructure failure. ECS could not even start the container. The OutOfMemoryError is a runtime failure. The container started, ran your code, and then exceeded its memory allocation.
This distinction matters for troubleshooting. Infrastructure errors appear quickly and have clear error codes. Runtime errors require you to also check application logs in CloudWatch.
Restore memory and redeploy
The fix is straightforward. You need to remove the restrictive container memory limit so your chatbot can use the full 512 MB task-level allocation.
- Click Task definitions in the left sidebar.
- Click on your chatbot task definition family name.
- Click Create new revision, then select Create new revision from the dropdown.
- Scroll down to the Container definitions section and click on your container name.
- Clear the Memory hard limit field so it is empty. This lets the container use the full task-level memory of 512 MB.
- Click Create.
Now update your service to use this corrected revision.
- Navigate back to Clusters and click on your cluster.
- Click on your service name in the Services tab.
- Click Update.
- Under Revision, select the latest revision.
- Check Force new deployment and click Update.
- Wait one to two minutes for the new task to reach Running status.
- Click on the running task and copy its public IP from the Configuration section.
- Verify the chatbot works by sending a test request:
curl -X POST http://[[TASK_IP="your task's public IP"]]:5000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello, are you working?"}'
You should see a JSON response containing an AI-generated reply from Bedrock and a conversation_id. The OOM problem is fixed.
Still seeing tasks crash?
- Check the stopped task details again. If you still see exit code 137, make sure you cleared the Memory hard limit field completely in the container definition. Any value under 512 can still cause OOM.
- Verify you selected the correct (latest) revision when updating the service. The Revision dropdown can default to an older revision.
- Make sure you checked Force new deployment. Without it, ECS may continue running the old task.
Help me figure out why my ECS task keeps crashing with exit code 137 after I increased memory.
Pro tip: sizing memory in production
In real projects, you would monitor your container's memory usage over time using CloudWatch container metrics. Set your memory allocation to about 1.5 to 2 times the observed peak usage. This gives you a safety margin without wasting resources.
Two troubleshooting scenarios down, one to go. Next up, you'll tackle a failure that doesn't crash the container at all. Instead, it hides inside your application logs.
Troubleshooting Scenario 3 - IAM AccessDeniedException
In the first two scenarios, the container itself failed to start or crashed. Those failures showed up directly in the ECS stopped task view with clear error codes. This scenario is different.
An IAM misconfiguration means your container runs perfectly fine. The application only fails when it tries to call another AWS service at runtime. You'll learn to diagnose these failures through application logs in CloudWatch instead of the ECS console.
In this step, get ready to:
- Remove Bedrock permissions from the task role and trigger an AccessDeniedException.
- Diagnose the error using CloudWatch Logs instead of the ECS stopped task view.
- Break and fix S3 permissions to see how silent IAM failures appear only in logs.
Remove the Bedrock permission from the task role
Your task role is the IAM role your container uses at runtime. It currently has permissions for both bedrock:InvokeModel and S3 access. You'll remove the Bedrock permission to simulate a common misconfiguration.
- Open the IAM console at https://console.aws.amazon.com/iam.
- Click Roles in the left sidebar.
- Search for and click on the task role you created in Step 1 (the one with Bedrock and S3 permissions, not the task execution role).
- Find the inline policy you attached to this role.
- Click Edit on the inline policy.
- Switch to the JSON editor if it isn't already showing.
Task role vs. task execution role
The task role grants permissions to your running container (Bedrock, S3). The task execution role grants permissions to the ECS agent (pulling images from ECR, writing to CloudWatch Logs). You are editing the task role here.
- Remove the entire statement that grants bedrock:InvokeModel permission. Keep only the S3 statement.
After removing the Bedrock statement, your policy should only contain the S3 permissions. It looks something like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}
What did you just change?
You removed your container's permission to call the Bedrock API. The container will still start and run, but any attempt to call invoke_model will be rejected by AWS with an AccessDeniedException.
- Click Next, then click Save changes.
IAM policy changes take effect immediately. You do not need to redeploy the ECS service. Your currently running task picks up the new (reduced) permissions right away.
Trigger and diagnose the AccessDeniedException
With the Bedrock permission removed, your chatbot container is still running. The ECS console shows the task as healthy because the container itself has not crashed. The failure only surfaces when you send a request that triggers the Bedrock API call.
- Send a chat request to your running task by running this command:
curl -X POST http://[[TASK_IP="your task's public IP address"]]:5000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}'
You should see a 500 error response containing the AccessDeniedException message. It looks something like this:
{
"error": "Bedrock invocation failed: An error occurred (AccessDeniedException) when calling the InvokeModel operation: User: arn:aws:sts::123456789012:assumed-role/... is not authorized to perform: bedrock:InvokeModel ..."
}
Why is this scenario different from the first two?
In the CannotPullContainer and OutOfMemoryError scenarios, the task itself failed. You diagnosed those by checking the ECS stopped task view for error codes.
Here, the task is still running. ECS reports the task as healthy. The failure is at the application level, happening inside your code when it tries to call Bedrock. The only way to find it is through your application's response or its logs.
Now confirm the error shows up in CloudWatch Logs. This is the diagnostic tool you would use in production when you can't see the direct API response.
- Open the CloudWatch console at https://console.aws.amazon.com/cloudwatch.
- Click Log groups in the left sidebar under Logs.
- Click on the log group for your chatbot task definition (the one you configured in Step 4).
- Click on the most recent log stream.
- Look for a log line showing the POST /chat request with a 500 status code.
What should I see in CloudWatch Logs?
Flask's development server logs every request to stdout, which CloudWatch captures. You should see a line that includes "POST /chat HTTP/1.1" 500. This confirms your container received the request but returned an error.
In a production application, you would add explicit logging for caught exceptions. The curl response already showed the full AccessDeniedException message. CloudWatch Logs become critical when you cannot observe the API response directly.
Don't see a 500 in CloudWatch Logs?
- Make sure you are looking at the correct log group. It should match the log configuration from your task definition in Step 4.
- Click on the most recent log stream. Older streams may be from previous task runs.
- Verify your task is still running in the ECS console. If the task stopped for another reason, a new task may have started with a new log stream.
Still stuck? Help me find the AccessDeniedException in my CloudWatch Logs for my ECS Fargate chatbot task.
Fix Bedrock access, then break and fix S3 access
Time to restore the Bedrock permission. Then you'll break S3 access to see a second flavor of IAM failure. This one is sneakier because the chatbot still returns a successful response to the user.
- Go back to the IAM console and open your task role's inline policy.
- Click Edit and switch to the JSON editor.
- Add the bedrock:InvokeModel statement back, and remove the S3 statement (s3:PutObject and s3:GetObject).
Your policy should now have Bedrock access but no S3 access. It looks something like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:*::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0"
}
]
}
What will this break?
Your chatbot calls Bedrock first, then tries to save the conversation to S3. Without S3 permissions, the Bedrock call succeeds but the S3 write fails. The app catches this S3 error and logs a warning instead of crashing.
- Click Next, then click Save changes.
- Send another chat request to your chatbot by running this command:
curl -X POST http://[[TASK_IP="your task's public IP address"]]:5000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello again"}'
This time, you should get a successful response with an AI-generated message. The chatbot appears to work perfectly from the user's perspective.
- Go back to the CloudWatch console and open your chatbot's latest log stream.
- Look for a warning line containing Failed to save conversation to S3 and an AccessDeniedException mentioning the PutObject operation.
Why is this failure harder to catch?
The Bedrock error was obvious. The user got a 500 error and the curl response showed the problem. The S3 error is silent. The user gets a perfectly normal 200 response with an AI answer, but the conversation never gets saved.
In production, silent failures like this are the hardest bugs to find. CloudWatch Logs are the only way to discover them. This is why application-level logging is critical for containerized services.
Now restore all permissions so your chatbot is fully functional again.
- Go back to the IAM console and open your task role's inline policy.
- Click Edit and restore the full policy with both Bedrock and S3 permissions.
Your restored policy should include both statements. It looks something like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:InvokeModel",
"Resource": "arn:aws:bedrock:*::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}
- Click Next, then click Save changes.
- Send a final chat request to verify everything works end to end:
curl -X POST http://[[TASK_IP="your task's public IP address"]]:5000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Confirm everything works"}'
You should get a successful response with a conversation_id in the JSON body.
- Verify the conversation was saved by checking your S3 bucket. Open the S3 console and navigate to the conversations/ prefix in your bucket.
- Confirm you see a new JSON file matching the conversation_id from the curl response.
Still seeing an error in the curl response?
- Double-check that your restored policy includes both the bedrock:InvokeModel and the S3 statements. A missing comma between statements causes a JSON syntax error that IAM rejects silently.
- Verify the model ID in your Bedrock Resource field matches anthropic.claude-haiku-4-5-20251001-v1:0 exactly.
- Check that the S3 Resource ARN matches your actual bucket name and ends with /* to allow access to objects inside the bucket.
Still stuck? Help me restore my ECS task role's IAM policy for Bedrock and S3 access.
You've now completed all three troubleshooting scenarios. You diagnosed an infrastructure error (CannotPullContainer), a resource limit error (OutOfMemoryError), and an application-level IAM error (AccessDeniedException). Each required a different diagnostic path.
Ready for a bonus challenge? The Secret Mission adds a health check endpoint and an Application Load Balancer to front your chatbot.
Secret mission
Add a Load Balancer with Health Checks
Your chatbot is running on Fargate, but you are accessing it through a task's public IP that changes every time the task restarts. In production, services sit behind a load balancer that provides a stable DNS name and monitors container health. In this secret mission, you will create an Application Load Balancer, wire it to your ECS service, and configure health checks so unhealthy tasks are automatically replaced.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses several AWS services that bill based on usage, so leaving resources running will incur ongoing costs.
Cost warning
Your ECS Fargate service bills per-second for vCPU and memory while tasks are running. Even at the smallest task size (0.25 vCPU, 0.5 GB), this adds up over time. If you completed the Secret Mission, the ALB bills hourly regardless of traffic. Pause or delete your resources if you are done experimenting.
Resources you used:
- ECS Fargate service and cluster.
- ECS task definition.
- ECR private repository (stores your Docker image).
- S3 bucket for conversation log artifacts.
- IAM roles (task execution role and task role).
- CloudWatch log group.
- Application Load Balancer and target group (Secret Mission only).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the chatbot and troubleshooting scenarios.
- Your Fargate service will continue running and billing per-second for vCPU and memory. Expect a few cents per hour at the 0.25 vCPU / 0.5 GB task size.
- ECR storage, S3 storage, and CloudWatch log ingestion will continue to incur minor charges.
- If you created an ALB for the Secret Mission, it bills hourly regardless of traffic.
Pause - I'll come back to this later
Stop running tasks to eliminate per-second Fargate charges while keeping your configuration and image in place.
- Set your ECS service desired count to 0. This stops all running tasks. Run:
aws ecs update-service --cluster your-cluster-name --service your-service-name --desired-count 0
- Your cluster, task definition, ECR image, S3 bucket, IAM roles, and CloudWatch log group will remain. ECR and S3 storage costs are negligible.
- If you created an ALB for the Secret Mission, delete it now to stop hourly charges. You can recreate it later.
- To resume, set the desired count back to 1.
Delete - I don't want to use this again
Remove all project resources completely. Follow these steps in order to avoid dependency errors.
- If you created an ALB and target group (Secret Mission), delete the ALB first, then the target group.
- Delete the ECS service by setting desired count to 0, then deleting the service.
- Delete the ECS cluster.
- Deregister the task definition.
- Delete the ECR repository (use --force to remove all images at the same time).
- Empty the S3 bucket, then delete the bucket.
- Delete the IAM task role and task execution role. Remove any inline policies before deleting the roles.
- Delete the CloudWatch log group created for your ECS tasks.
Mission accomplished
That's a Wrap!
Nice work! You just deployed an AI chatbot to production on AWS and then systematically broke and fixed it three different ways.
You've learned how to:
- Build and containerize a Python Flask chatbot that calls Amazon Bedrock Claude Haiku 4.5 for AI responses and stores conversation artifacts in S3.
- Deploy to ECS Fargate with properly scoped IAM task roles for Bedrock, S3, ECR, and CloudWatch access.
- Diagnose and fix three real ECS failure scenarios: CannotPullContainerError from a bad image URI, OutOfMemoryError by reading exit code 137, and AccessDeniedException by checking CloudWatch Logs.
- Added a health check endpoint and fronted the chatbot service with an Application Load Balancer.
Ready to quiz yourself?