Flask and RDS counter app

Let's walk through an end-to-end example of building a basic counting app using:

  • Flask

  • PostgreSQL

  • RDS (AWS Relational Database Service)

We'll deploy the Flask application into our Release cluster and configure a security group to allow the nodes in the cluster to communicate with the RDS database. You can use a similar method to configure access to other cloud services, such as Redshift, Elasticache, or Redis.

Our final setup will look like this:

Note how the database and node group exist in the same VPC and use a common security group to communicate.

To follow along, you should have a Release account configured with AWS, with a domain name and cluster already running. Follow the quickstart guide if you don't have these in place yet.

Set up an RDS instance

We'll create a small, free-tier RDS instance for this demonstration, but for a production application, you'll want to tweak the settings regarding the number of instances, multi-region deployments, backups, and so on. If you already have an RDS database, you'll just need to configure it to:

  • Share a VPC with your Release node group.

  • Share a security group with your Release node group.

Identify the security group of your node group in AWS

When you set up a new cluster and node group in Release, an EC2 launch template was automatically created for you with a default security group. We'll need to find this security group that your worker nodes already use so we can assign it to the database.

Log in to your Release account, and take note of your cluster context and node group name, as both of these are used in the name of the security group. You can see how to find each in the image below.

Also note which region your cluster is running in.

Now log in to your AWS account and navigate to the EC2 dashboard for the same region as your Release cluster. You should see at least one (and probably more) EC2 instances that are worker nodes for your cluster. You can identify them as they have the same cluster context and node group name that we noted in the Release control panel in their name, followed by the suffix -Node.

Select one of these worker nodes and navigate to Actions -> Security -> Change Security Groups, as shown below.

Don't make any changes here, but note the name of the security group and then press Cancel.

Finally, back on the main EC2 page, note the VPC ID of the worker node too. You can find it as shown in the image below.

Create the RDS database

Now navigate to the RDS dashboard and choose to create a new database.

The most important settings to change are:

  • The VPC - Make sure you create the database in the same VPC as your Release clusters.

  • The security group - Add the same security group as the one you noted earlier.

For this demo, we left most of the default settings for a free-tier Postgres database, as detailed below.

Leave the default database creation method as Standard create and choose PostgreSQL as the database type.

Choose Free tier, name your database something meaningful, and choose a strong password (note this somewhere safe).

Set the VPC and security group to match the settings from your Release EC2 worker node, which we noted in the previous section.

Scroll to the bottom and click Create database.

The database will take a few minutes to initialize. Once it's ready, navigate to it from the RDS dashboard and note the endpoint, as shown below.

You now have an RDS database that your Release cluster can communicate with. To complete the example, we'll build a basic Flask application that uses this database and deploy it into our cluster.

Build the application

For the demo, we'll build a very basic "counter" application that counts the number of times the Count button has been pressed. It'll look like this when it's up and running:

Our full project will have the following directory structure. We'll use a Docker Compose file even though we're only running a single container to make it easier to extend the app. We've called our app count, and placed all the application code in an app.py file and the frontend code in templates/index.html.

Create a directory for the overall project, a subdirectory called count for the Python application, and a subdirectory in the count directory called templates for the frontend. In the count directory, create blank files for app.py, Dockerfile, and requirements.txt. In the templates directory, create a blank file called index.html. In the outer project directory, create a blank file called docker-compose.yml.

Write the app.py file

Our app.py file uses Flask-SQLAlchemy as an ORM. First we get credentials from our environment (we'll set these up later), and then we define a basic counts object, which has an automatic primary key and a single field that always stores 1.

We then initialize the database and define a hello() method. When a user visits the page, we'll count the number of records in the database. If the user presses the Count button, we'll create a new count object, which will have the effect of incrementing the total.

Finally, we run the application on port 80 with debug turned on.

Add the following code to the count/app.py file:

from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy
import traceback
import os

user = os.environ.get("POSTGRES_USER")
pw = os.environ.get("POSTGRES_PASSWORD")
host = os.environ.get("POSTGRES_HOST")
db = os.environ.get("POSTGRES_DATABASE_NAME")
DB_URL = f"postgresql+psycopg2://{user}:{pw}@{host}/{db}"
print(DB_URL)

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = DB_URL
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

db = SQLAlchemy(app)


class counts(db.Model):
    id = db.Column("count_id", db.Integer, primary_key=True)
    value = db.Column(db.Integer)


try:
    db.create_all()
except Exception as e:
    print(e)
    print("couldn't initialize db")
    traceback.print_exc()


@app.route("/", methods=["POST", "GET"])
def hello():
    if request.method == "POST":
        c = request.form["countbutton"]
        count = counts(value=1)
        db.session.add(count)
        db.session.commit()

    total_counts = counts.query.count()
    return render_template("index.html", total=total_counts)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80, debug=True, threaded=True)

Note that we've taken some liberties here in terms of best practices to keep the code simple. Specifically, for production settings, you should:

  • Turn debug mode off in the last line.

  • Run db.create_all() in a separate once-off script rather than in the web application code.

  • Properly handle the exception rather than only logging the error if the database connection fails.

Write the templates/index.html file

Our frontend contains a basic form with a single button and a templated variable {{count}} to display the total. Add the following code to templates/index.html:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
        <h3>Count</h3>
        <p>Total counts: {{total}}</p>
        <form id="countform" name='form' method="POST" action="/">
          <input id="countbutton" type="submit" name="countbutton" value="Count"></input>
        </form>
  </body>
</html>

Write the Dockerfile

For our Dockerfile, we'll extend the Python3 Alpine Docker image, as this has most of the packages we need while still being very slim. Add the following code to the count/Dockerfile file:

# Using official Python runtime base image
FROM python:3-alpine

# Set the application directory
WORKDIR /app

# Install our requirements.txt
ADD requirements.txt /app/requirements.txt

RUN apk add build-base

RUN apk add --no-cache supervisor \
    && python -m pip install --upgrade pip \
    && pip install -r requirements.txt

# Copy our code from the current folder to /app inside the container
ADD . /app

# Make port 80 available for links and/or publish
EXPOSE 80

# Define our command to be run when launching the container
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"]

This brings in a few more dependencies that we need to correctly install the psycopg2binary database driver, copies in our application code, installs our Python requirements, and runs the application using Gunicorn.

Write the requirements.txt file

Our application needs Flask (our web framework), Gunicorn (to act as a gateway between Flask and our webserver), Flask-SQLAlchemy (as an ORM to easily create objects in our database), and psycopg2-binary (as a driver to talk to PostgreSQL).

Flask
gunicorn
flask-sqlalchemy
psycopg2-binary

Write the docker-compose.yaml file

Finally, we'll initialize everything using Docker Compose. This step is not essential in our case as we have only a single Docker file, but real-world applications will usually tie together a few different containers so it's useful to have it in place. Add the following code to the top-level docker-compose.yml file:

version: "1"

services:
  count:
    build: ./count
    command: 
    - python 
    - app.py
    ports:
      - "5000:80"

This creates a single service (count) and builds the Docker file in our count directory.

We now have a full-stack application running. If you have Docker and Postgres running locally, you can set the expected environment variables and run docker-compose up to test that everything is working, otherwise we can skip straight to deploying it on Release and debugging any issues there.

Deploy the application to Release

To deploy the application to Release, you need to:

  • Push the application up to GitHub or other provider that you've integrated with Release.

  • Create a new application in Release.

  • Configure the environment variables to connect to the database.

Create the application

You can find detailed steps for doing this in our create your application guide, but we'll cover the most important steps below.

In your Release dashboard, choose to create a new application.

Select the repository that holds your application code and proceed to the next step. You can leave all the defaults in the generated Application Template.

In the "Build & Runtime Configurations" section, choose to Edit "Default Environment Variables", and add in the connection settings for the database.

The bottom of the file (after the comments) should look as follows (substituting in your own database endpoint and password that you set in AWS, and username and database name if you changed those from their defaults).

defaults: []
services:
  count:
  - key: POSTGRES_DATABASE_NAME
    value: postgres
  - key: POSTGRES_USER
    value: postgres
  - key: POSTGRES_PASSWORD
    secret: true
  - key: POSTGRES_HOST
    secret: true

Note how we define these under the count service (named in our docker-compose.yml file) so that these are only available to our main application service. We've also set the secret flag on the password and host fields to true, so these will be encrypted by Release at rest.

Click Start Build & Deploy and wait for the environment to come up. Once it's ready, you can click the URL in the section "Hosted URLs" to navigate to your running application.

If everything worked as expected, you should see the basic counting application running and be able to add to the count by clicking on the button.

If you see an error or a blank page, use the log viewer to look for any errors, or use the terminal to double check that the environment variables are set correctly by running, for example, echo $POSTGRES_USER.

Last updated