Deploy a Flask / MySQL application on AWS - Part 1: setting up a database and an Elastic Beanstalk environment

aws flask python mysql

November 04, 2020

Even though AWS documentation is pretty comprehensive I found myself running into a few problems while creating an environment for my flask application.
In this article I'll guide you through all the required steps to deploy your flask application on a Elastic Beanstalk envrionment, from the creation of a basic Flask application to the routing of your domain name to the webserver. Everything used in this tutorial is free-tier eligible.
Note: This article is not about creating a fully fledged flask application. I'll only write the minimum amount of code needed to render a webpage that gets data from a database.
  • An AWS account within the 12 months trial period.
  • A local Python 3 environment (preferably a virtual environment but that's up to you)
  • pip package manager
  • Familiarities with the command line
  • macOS or Linux if you want to use the command line interface like I did (I guess this should also work for Windows but the commands are probably slightly different).
Creating a database instance on AWS
Let's begin by creating a database that both our local and EB environment will be able to connect to.
Once you registered an account on AWS go to the AWS Management Console and search for RDS in "Find services"
  • Now click on "Database" on the left and then on "Create a database"
  • Select MySQL, chose your version (I recommend using the default version which is at this time v8.0.17)
  • Select "Free Tier"
Give a name to your database and choose a username / password combination that we will need to connect to the database.
Note: Make sure to save your credentials before moving on.
If you selected "free-tier" you should only have one type of instance available for use: db.t2.micro. Leave it that way.
Disable autoscaling so we don't risk exceeding the free-tier limit.
  • Select "Default VPC"
  • Click on "additional connectivity configuration"
  • Set "Public acces" to "Yes" so we can connect to the database from our own machine
  • Leave the database port to 3306
  • Enable password authentication
Connectivity configuration should look like this.
Now proceed to the end of the page. Make sure you have stored the newly created database user and password combination as it will be needed to access it.
Click on "Create database"
The database creation should only take a couple of minutes.
Now we need to create a security group that will allow us to connect to the database from our own machine. Go to "Database" and click on the database you just created. In "Connectivity and security" look for "VPC and security groups" and click on the current active security group (at this time you should only have one which is the default one. From there click on "Create a security group"
Fill the name and description fields and leave the vpc selected. Add a new rule in "Inboud rules", select MYSQL/Aurora and from the source menu select "My IP". Your current machine IP address should appear in the next field. Leave "Outbound rules" on "All traffic".
Now go back to the RDS management console and select your database by clicking on the radio button. Click on "Modify" and add the security group you just created.
Apply the settings and check if the database is accessible from our machine
To make sure we configured our new security group properly let's try to access the database from our current machine
$ mysql -h -P 3306 -u user -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 3850 Server version: 8.0.17 Source distribution Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>
If the connection times out make sure you set the database to be publicly acessible and that you added the right inbound rule (check your ip address).
Note: I use a VPN (Proton VPN) and can't connect to the database no matter which server I choose and no matter if I set the database to accept any connection on port 3306. I haven't investigated any further. For now I just disable my VPN while accessing the database from my own machine.
Adding data to the database
Let's create a simple database named "flask_app":
mysql> CREATE DATABASE flask_app;
Select the database we just created and add a new table called "languages"
mysql> USE flask_app;
mysql> CREATE TABLE LANGUAGES (id MEDIUMINT NOT NULL, language varchar(255));
And add some data to the table
mysql> INSERT INTO languages VALUES (0, "C++"), (1, "Python"), (2, "C"), (3, "Javascript"), (4, "Haskell"), (5, "Objective-C);
Note: if you add data to the database from the command line and the data is not in english but in any language using special characters, make sure to set the locale to the desired language before adding any string to the database. Failure to do so might result in the text being completely messed up.
Creating a local Python virtual environment
Now that our database is up and running it's time to take care of our flask application. This article is not a tutorial on how to create a flask application (Flask documentation is pretty good and you can find a lot of tutorials online) but for illustration purposes I'm going to create a small flask app that will connect to our database. Once we are assured that our app works locally we'll be able to deploy it on a Elastic Beanstalk environment.
Install virtualenv
$ pip install virtualenv
Create an environment for your project. I usually create the virtual environment folder in my project's parent folder so it doesn't pollute flask source directory.
Create the virtual env with the following command:
$ virtualenv env
where env is the name your virtual environment.
Activate your virtual env via the following command :
$ source env/bin/activate
You should now have the name of your environment displaying at the begining of your prompt as shown below:
(env) $
Install Flask and MySQLdb:
(env) $ pip install flask
(env) $ pip install mysqlclient
There is now an official MySQL Python library but this one has been working fine for me.
A typical minimal flask application tree usually looks like this:
├── ├── static │   ├── styles.css ├── templates │   ├── index.html
Create an index route and connect to the database using the following code:
1 from flask import Flask, render_template 2 3 import MySQLdb 4 5 application = Flask(__name__) 6 7 @application.route('/', methods=["GET"]) 8 def index(): 9 10 db = MySQLdb.connect(host="db-host-name", 11 port=3306, 12 user="user", 13 passwd="password", 14 db="db", 15 autocommit=True, 16 use_unicode=True 17 ) 18 cursor = db.cursor() 19 query = """SELECT * FROM languages;""" 20 cursor.execute(query) 21 languages = cursor.fetchall() 22 23 # Convert to a list for better readability 24 languages = [list(l) for l in languages] 25 26 return render_template('index.html', languages=languages)
Where "host" is the name of your database endpoint and the user / password combination is the one you chose during the database creation process.
1 <!DOCTYPE html> 2 <html lang="fr"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6 <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> 7 <title>Some languages</title> 8 </head> 9 <body class="sm-section"> 10 <header> 11 <div id="header"> 12 <div class="nav-container"> 13 <nav> 14 <div class="left-nav"> 15 </div> 16 <div class="center-nav"> 17 <a class="nav-item" href="#">Index</a> 18 </div> 19 <div class="right-nav"> 20 </div> 21 </nav> 22 </div> 23 </div> 24 </header> 25 <div class="unique-content"> 26 <div class="languages-wrapper"> 27 <div class="languages-body"> 28 <h2>Languages I use</h2> 29 <ul> 30 {% for l in languages %} 31 {{ l[0] }} - {{ l[1] }} 32 {% endfor %} 33 </ul> 34 </div> 35 </div> 35 </div> 36 <div class="footer-wrapper"> 37 <footer class="main-footer"> 38 <div class="left-footer"> 39 </div> 40 <div class="center-footer"> 41 <a class="footer-link" href="#">guimauve</a> 42 </div> 43 <div class="right-footer"> 44 </div> 45 </footer> 46 </div> 47 </div> 48 </body> 49 </html>
index.html goes into templates/
1 html { 2 background-color: rgba(0, 102, 255, .8); 3 height: 100%; 4 } 5 6 body { 7 font-family: 'Roboto', sans-serif; 8 margin: 0; 9 min-height: 100%; 10 background-color: transparent; 11 color: rgba(241, 241, 241, 1); 12 display: grid; 13 grid-template-rows: auto 1fr auto; 14 } 15 16 .nav-container { 17 background-color: rgba(0,0,0,0.7); 18 height:50px; 19 width: 100%; 20 } 21 22 .nav-container a:link { 23 text-decoration: none; 24 color: rgba(255, 255, 255, 1); 25 } 26 27 .nav-container a:active { 28 color: rgba(255, 255, 255, 1); 29 } 30 31 .nav-container a:visited { 32 color: rgba(255, 255, 255, 1); 33 } 34 35 .nav-container a:hover { 36 color: rgba(230, 230, 230, 0.9); 37 } 38 39 nav { 40 max-width: 1268px; 41 margin: 0 auto; 42 display: grid; 43 grid-template-columns: 1fr auto 1fr; 44 } 45 46 .center-nav { 47 padding: 14px; 48 grid-column: 2; 49 text-align: center; 50 } 51 52 .languages-wrapper { 53 display: grid; 54 max-width: 1268px; 55 padding: 0 4vw; 56 margin: 20px auto; 57 grid-template-columns: 1fr auto 1fr; 58 } 59 60 .languages-body { 61 grid-column: 2; 62 display: grid; 63 margin: 10px auto 0; 64 } 65 66 .footer-wrapper { 67 grid-row-start: 3; 68 grid-row-end: 4; 69 background-color: rgba(0, 0, 0, 0.7); 70 width: 100%; 71 } 72 73 .main-footer { 74 max-width: 1268px; 75 margin: 0 auto; 76 display: grid; 77 grid-template-columns: repeat(3, calc(100%/3)); 78 } 79 80 .main-footer a:link { 81 text-decoration: none; 82 color: rgba(255, 255, 255, 1); 83 } 84 85 .main-footer a:active { 86 color: rgba(255, 255, 255, 1); 87 } 88 89 .main-footer a:visited { 90 color: rgba(255, 255, 255, 1); 91 } 92 93 .main-footer a:hover { 94 color: rgba(230, 230, 230, 0.9); 95 } 96 97 .center-footer { 98 text-align: center; 99 grid-column: 2; 100 }
styles.css goes into static/
Starting a local Flask development server
Open a new terminal window and cd into your flask application folder. Export the following environment variables:
(env) $ export FLASK_APP="" (env) $ export FLASK_ENV=development (env) $ export FLASK_DEBUG=1 # Tells the server to automatically reload source files (env) $ export FLASK_PORT=8080 # Any available port
Run the following command to start the flask server:
(env) $ python -m flask run
Now open your browser and go to the following address:
If everything went ok our index should display the html page we've just created.
Our application works fine locally. Time to let the world know and deploy it to a production server.
Before going any further we need to export the names of the dependencies that will be needed to run our application. The instance installs these dependencies via a "requirements.txt" file placed at the root of the flask project directory.
"requirements.txt" can be generated with pip using the following command:
(env) $ pip freeze > requirements.txt
It should look something like this:
... Flask==1.1.1 ... mysqlclient==1.4.6 ...
Creating an Elastic Beanstalk Python environment
Let's install awsebcli in our environment. It will basically allow us to manage our envrionment from the command line.
(env) $ pip install awsebcli
Run the following command to init your Elastic Beanstalk envrionment. This will generate a "config.yml" that will be used to create the actual instance.
(env) $ eb init
Select a region:
Select a default region 1) us-east-1 : US East (N. Virginia) ... (default is 3): 17
17 corresponds to Paris
Create a new application and give it a name:
Select an application to use 1) someApp 2) anotherApp 2) [ Create new Application ] (default is 3): 3 Enter Application Name (default is "source"): flask-app Application flask-app has been created.
Chose your Python environment version. We'll go with version 3.7 running on Amazon Linux 2.
Note: Support for Amazon Linux 1 will end on 12/31/20 so I strongly recommend going with version 2.
It appears you are using Python. Is this correct? (Y/n): y Select a platform branch. 1) Python 3.7 running on 64bit Amazon Linux 2 2) Python 3.6 running on 64bit Amazon Linux 3) Python 3.4 running on 64bit Amazon Linux (Deprecated) 4) Python 2.7 running on 64bit Amazon Linux (Deprecated) 5) Python 2.6 running on 64bit Amazon Linux (Deprecated) 6) Preconfigured Docker - Python 3.4 running on 64bit Debian (Deprecated) (default is 1): 1
CodeCommit won't be needed here but you can set it up if you want.
Cannot setup CodeCommit because there is no Source Control setup, continuing with initialization
We do want to enable SSH to access our instance.
Do you want to set up SSH for your instances? (Y/n): Select a keypair. 1) someKeyPair 2) anotherKeyPair 3) [ Create new KeyPair ] (default is 2): 3
Creating an EC2 instance acting as a web server
After having created our application and initiated an Elastic Beanstalk envrionment, we are now ready to create an EC2 instance that will act as a web server. For a small / lightweight web application we have (to keep it simple) two options when creating our instance. We can either:
  • Create a multiple, load balanced instance: If you need more than one instance, a load balancer will dispatch the requests among your instances and scale your network by automatically creating / terminating instances according to the traffic load. The configuration on the server side is easier since the load balancer will manage routing and SSL certificates on its own without having much to do. Nginx configuration will be reduced to a few lines.
  • Create a single, non load balanced instance: If you know that the trafic of your website won't be such that it will require more than one instance. It is a little more involved since we will have to manually set up the nginx configuration / SSL certificate on the instance to handle the traffic / TLS encryption. But it can save you a ton of money (a classic load balancer cost me between 20$ and 25$ a month when not included in the free tier)
Note: it seems like downgrading from a load balanced instance to a single non load balanced instance is not possible without having to terminate and create a new instance from scratch. Trying to do so will result in an error caused by a setting in the rolling updates section that can't be modified (but this need more investigation).