
Deploying Flask Apps on AWS
A beginner's guide following my experience deploying a Flask app, including a database, on AWS.
I recently decided to deploy my SimpleChat app publicly and robustly (previously being ran manually on my VPS). I had limited experience with cloud computing (mainly Google Cloud Platform), so I decided to learn more about AWS due to its large market share, and to compare.
The App Itself
SimpleChat is a real-time chatroom application built with Flask (Python) and Socket.IO. It uses a database for storing messages and user information, which can be either SQLite (for local development) or PostgreSQL (for production). The app is containerized with Docker, making for easy deployment. Exact code is not the focus of this post, but the source code is available on GitHub.
Containerization
Containerization is the process of packaging an application into a container, which can be run on any system that supports the container software (e.g. Docker). This allows for easy deployment and consistency across different environments, similar to a virtual machine, but much more lightweight as it shares the host's system resources directly.
This tutorial uses Docker, which is the most popular container software. You can find installation instructions for your OS on Docker's website, or this handy guide for installing on Linux servers.
Once Docker is installed, you want to create a Dockerfile
in the root of your project. This file contains the
instructions for building the container image. SimpleChat's `Dockerfile is below:
# Specify the base image, in this case an image with Python 3.13 installed
FROM python:3.13
# Set the working directory inside the container, host OS is unaffected
WORKDIR /app
# Copy requirements (in root of project) and install them, including Gunicorn for running in production
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install gunicorn
# Copy the rest of the application code into the container and make the boot script executable
COPY . .
RUN chmod a+x boot.sh
# Set environment variable for Flask app
ENV FLASK_APP=simplechat:create_app
# Expose port 5000 for the app to be accessible outside the container
EXPOSE 5000
# Specify the command (in this case, script) to run when the container starts
CMD ["./boot.sh"]
A few things to note:
- Requirements are installed first, as they are less likely to change than the application code. This allows for Docker's caching to speed up subsequent rebuilds if only the application code changes.
- As the app will be going into production, Flask is run with Gunicorn, as opposed to the built-in Flask development server. Gunicorn is production-ready and can handle multiple requests concurrently, running much more efficiently and securely than the development server.
- The
boot.sh
script is used to run the app with Gunicorn, as it allows for easily changing the command without modifying theDockerfile
. The script is a simple, updating any database schema (if using flask-sqlalchemy) and starting Gunicorn with the appropriate options:
#!/bin/bash
flask db upgrade
exec gunicorn -b :5000 --access-logfile - --error-logfile - --worker-class eventlet -w 1 run:app
With the Dockerfile
and boot.sh
script created, you can build the container image with the following command
(in the root of your project, where the Dockerfile
is located):
docker build -t simplechat:latest .
This command builds the image and tags it with the name simplechat
and the tag latest
. You can verify the image was
built successfully with docker images
, which lists all locally available images.
Running Locally
To ensure your image works, you can run the container locally with the following command:
docker run --name simplechat -p 5000:5000 -d --rm simplechat:latest
This command runs the container in detached mode (-d
), mapping port 5000 on the host to port 5000 in the container
(-p 5000:5000
), and removes the container (not the image) when it stops (--rm
).
Amazon Web Services (AWS)
Here is where things get more complex in my opinion. AWS is a massive platform with many services, and it can be overwhelming to navigate. For this tutorial, we will be using the following services:
- ECR (Elastic Container Registry): A managed Docker container registry for storing and managing Docker container images.
- ECS (Elastic Container Service): A container orchestration service that makes it easy to deploy, manage, and scale containerized applications.
- RDS (Relational Database Service): A relational database service (who knew) that hosts and manages databases such as PostgreSQL.
- IAM (Identity and Access Management): A service for managing access to AWS resources securely.
- AWS Certificate Manager (ACM): A service for provisioning, managing, and deploying SSL/TLS certificates for use within AWS services.
- Secrets Manager: A service for securely storing and managing sensitive information such as database credentials.
Creating an RDS Instance
To create a PostgreSQL database for your app, navigate to the RDS service in the AWS Management Console and click "Create database". Select "Easy Create", then select "PostgreSQL" as the engine type. Settings can largely be left as default, however you will need to select "Publicly accessible" under Connectivity > Additional configuration. The rest can be changed as desired, such as instance name, username, and password. Do make note of the username and password, however. After a few minutes, your database should be created and available. Try connecting to it using a database client to verify.
You will want to modify your app's configuration to use the RDS database, likely through environment variables. For
SimpleChat, I check for an environment variable DATABASE_URI
, defaulting to a local SQLite database if not found.
Creating Secrets! Shhh!
Don't add secrets such as database credentials, Flask SECRET_KEY, etc. to your code, Dockerfile, or Docker image (since all files in your directory will be added to the container).
Instead, use AWS Secrets Manager to store your secrets as plaintext. In the later ECS Task Defining, you will be able to use these secrets as environment variables in your container.
Navigate to Secrets Manager in the AWS Management Console and click "Store a new secret". Select "Other type of secret", then add either the key-value pairs for your secrets, or just plaintext for one secret. Copy the created secrets' ARNs, since these will be used next, and in the ECS task definition step.
Allowing ECS Access to Secrets
To allow your ECS tasks to access the secrets, you will need to create a new IAM role. Navigate to the IAM service in
the AWS Management Console and click "Roles" in the sidebar, finding the ecsTaskExecutionRole
role, and add a new
permission with an inline policy. Use JSON and copy and paste the following policy, modifying the "Resource"
field to
match your secrets' ARNs:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"(ARN_1)",
"(ARN_2)"
]
}
]
}
Creating an ECR Repository
Setting Up AWS CLI
To push your Docker container images, you need to set up the AWS CLI (Command Line Interface) on your local machine. This allows you to interact with AWS services from the command line. You can find installation instructions on AWS's website.
Once installed, you need to configure the CLI with your AWS credentials, as found on the setup instructions.
Creating the Repository
One the CLI is set up, you can create an ECR repository on the AWS Management Console by searching for "ECR." You can then click "Create repository" and give it a name (e.g. chat) in a namespace (e.g. nommensen) for your repository.
Pushing your Images
AWS provides the copyable commands to push your image to ECR inside the repository page. Click on "View push commands" to see the commands. You will need to run these commands in the terminal you installed AWS CLI in, in the directory of your project (if not already built). Once pushed, refresh your repository to see the new image.
Creating an ECS Cluster and Task Definition
Once your image is pushed to ECR, you can navigate to the ECS service. Create a new cluster, making sure to select "AWS Fargate" as the cluster template. You can leave most settings as default, however you may want to change the cluster name and Monitoring > Container Insights > Observability to enhanced.
Once created, you must define what will run on your cluster, in what is called a Task Definition. Click on Task definitions on the sidebar, and create a new Task Definition. Again, select "Fargate" as the launch type. Specify the OS/resources necessary for your app (SimpleChat is so simple, I selected just 0.25 vCPU and 0.5 GB memory). You will need to select a task role.
Next, you will need to add a container to the task definition. Give it a name (e.g. chat) and specify the image URI from
ECR (found on the repository page). You will need to specify the memory and CPU units again, this time for the container
specifically (as opposed to the whole task). You will also need to specify the port mapping, which for a Flask app is
likely 5000
(unless you changed it, which isn't usually necessary).
In the Environment variables section, add the keys for the environment variables your app needs, such as SECRET_KEY
,
and if they are stored in Secrets Manager, select "ValueFrom" and paste the secret's ARN. You can also add non-secret
environment variables here as well.
Deploying the Container
To finally create the running process, you need to create a Service from the Task Definition. This will create a long-running process based on your Task Definition.
Most settings can be left as default, however you will want to enable a Public IP in the Networking section, and enable load balancing.
Using load balancing will allow Amazon Elastic Load Balancer (ELB) to distribute the incoming traffic to your Flask
app's port 5000, and run continual health checks to ensure the app is running. Here, you want to change the target group
port to 5000
to match the port your app is running on.
Once you create the service, you may need to wait a few minutes for all the resources to be created and started. Once the service shows up as active (hopefully!) in Clusters view open the Service and click on the Load Balancer link. In the Security tab, click the security group and edit inbound rules to add a new rule allowing HTTP traffic (port 80) from any source.
Now you can finally access your Flask app by going to the Load Balancer's DNS name (found on the load balancer's page)!
Custom Domain and HTTPS
If you have your own domain (you should, they're quite cheap and easy to mess with), you can set up a CNAME record to point to your load balancer's DNS name. This will allow you to access your app using your own (sub)domain.
You may notice you are unable to reach your app using your custom domain in your browser, but may be able to via curl
(.dev domains are HTTPS-only), or get a security warning. This is because you need to set up HTTPS for your domain. You
can do this using AWS Certificate Manager (ACM) to create a free SSL/TLS certificate for your domain.
Navigate to AWS Certificate Manager and request a public certificate for your domain (e.g. chat.nommensen.dev
), then
complete the verification process (I used DNS verification, which involves adding a CNAME record to your domain's DNS
settings as shown below).
Once your CNAME record is added, you may need to wait a few minutes for the DNS to propagate and for AWS to verify. Once verified, head back to your load balancer's page and edit the listener to add a new rule for HTTPS (port 443). You will need to select the certificate you just created in ACM.
Now you should be able to access your app using your custom domain with HTTPS!
CI/CD with GitHub Actions
If you would like to automate the deployment process on every new push to main (or when a new tag is created), you can use GitHub Actions to create a CI/CD pipeline to push the new image to ECR and update the ECS service.
GitHub has a (somewhat) handy guide on how to do this, however you need to do/know a few things first:
- You will need to create a new IAM user with programmatic access and appropriate permissions. Don't ask me the permissions, I couldn't figure it out and just gave it admin access 🫣
- You will need to add the IAM user's access key ID and secret access key as GitHub secrets in your GH repo.
- You will need to know your AWS region, ECR repository name, and ECS cluster and service names.
- You will need your task definition JSON file, specifically the creation definition. You can get this by going to your task definition and selecting "Create new revision", then copying the JSON from the "JSON" tab. For some reason (likely) the old version of the Action in the tutorial), you have to remove the enableFaultInjection entry.
Once you have all that, you can create a new GitHub Actions workflow in your repo (e.g. .github/workflows/deploy.yml
)
and place the task definition JSON file in a .aws folder in your repo (e.g. .aws/task-definition.json
)
This may take a few tries to get right, but you can check the SimpleChat repo for reference. I also found the Amazon Q bot quite useful for troubleshooting if the issue is with the AWS configuration.
Conclusion
Deploying on AWS can be a very daunting experience, especially for beginners. However, with some patience you should be able to get your Flask app up and running! This process took me a few days of trial and error, including writing this post, but I learned a lot about AWS and cloud computing, and gained an appreciation (the hard way) for the:
- Reliability: My app kept running a working build despite many errors and crashes
- Security: The granularity of permissions and roles, while complex and daunting, can be very secure and allows for great control to follow the principle of least privilege.
- Customization: The ability to add logging, monitoring, scaling, load balancing, etc. along every step of the way shows the power of a mature cloud platform with tons of services and features.
If you have any questions or comments, feel free to reach out to me on the links in the footer, I'd be happy to chat!
Happy deploying (and good luck)!