In this guide, we'll introduce how to use Flask, Waitress, and Docker to create a web service Living on the Edge. Our goal is to create a web application that accepts an HTTP request and returns a UUID in the body, with the geographic location of your compute container also returned in an extra header, implemented in pure python.
Note: Here we will use Docker, a popular package for building and deploying containers, though any container image that adheres to the OCI specification can be deployed onto Edge Computing.
Before we get started, let's make sure you have a few basic requirements out of the way.
You should have:
- Docker installed on your machine
- Python with virtualenv installed
- A repository to host docker images (DockerHub or Docker server)
Basic Components of the Application
- Dockerfile: Configuration file that instructs the runtime how to build your container
- requirements.txt: A list of python dependencies needed to run our code.
- .dockerignore: Files to ignore when building the container image
- app.py: Python script to respond to HTTP requests to the container
Create a Working Directory
To keep our project organized we will create a new directory for this project and change into this directory. Subsequent commands in this tutorial will be executed from this directory unless otherwise stated.
$ mkdir flask_uuid
$ cd flask_uuid
Create your VirtualEnv
So that we don't pollute the system python with the project dependencies we will use a virtual environment to execute code in when testing. Virtual Environments allow you to install multiple python modules that may otherwise conflict on the same system and allows you to maintain discrete environments for each python project you are working on. We'll create one now so that we can ensure we have the correct dependencies for our flask application.
$ virtualenv venv
The virtual environment will be created which will include a new python executable at the path venv/bin/python.
We can call python with this path, but it's simpler to just execute python. Let's activate our virtual env so that our $PATH is updated and we're only using the virtual env instance of python.
$ source venv/bin/activate
Once you've activated your virtual environment you'll see your terminal prompt change to include the virtual environment name (which in this example is venv)
(venv)$
Install Requirements
Now we have a virtual environment setup for testing lets install the requirements for our flask project. We have two requirements, flask and waitress. Create a text file named requirements.txt and add the following to it:
Flask==1.0.2
waitress==1.2.1
Once that file is saved, install the requirements inside the virtual env (remember, your prompt will update with the virtual env name to let you know that you are inside the virtual environment).
(venv)$ pip install -r requirements.txt
You may notice that extra modules were installed too, that's because flask is dependant on other python modules to be able to run and pip has satisfied these dependencies automatically.
Serve Requests
Now we have our development environment setup we can start to create our flask web application and respond to HTTP requests.
Create a file named app.py and add the following to it, this code is commented so that you can understand what is happening.
from flask import Flask # import the flask module we installed earlier
import uuid # import the uuid module, this ships with python so we didn't need to install it
app = Flask(__name__) # Initialise the flask app as app
@app.route('/') # Declare the '/' route for app using a route decorator
def get_uuid(): # Define a function to respond to the route
return str(uuid.uuid4()) # Return a uuid string as the response
That's all we need! We can now start the development server and confirm that our application responds as we expect. Start the development server with
(venv)$ FLASK_APP=app.py flask run
And you'll see the following output
(venv)$ FLASK_APP=app.py flask run
* Serving Flask app "app.py"
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Our application is being served on port 5000 using the development server. You can now load http://127.0.0.1:5000/
in a web browser to get a uuid!
Add Response Headers
Perhaps in the future, you will want to debug your application but you need to know which location is serving your request to allow you to troubleshoot further. In this instance, it would be helpful to return the container location in a response header. You can use the hostname environment variable to determine where the container is deployed and we can return this in a header in our response.
To add the location to an SP-LOCATION header update your app.py to the following, additions have been highlighted in green.
from flask import Flask
import uuid
import os
import re
app = Flask(__name__)
@app.after_request
def add_hostname_header(response):
env_host = str(os.environ.get('HOSTNAME'))
hostname = re.findall('[a-z]{3}-\d$', env_host)
if hostname:
response.headers["SP-LOCATION"] = hostname
return response
@app.route('/')
def get_uuid():
return str(uuid.uuid4())
We've added a add_hostname_header function which checks if the env variable HOSTNAME is present and that a simple regex search of the hostname can return the three-letter location name and the container number from the end of the hostname string. If these evaluate to true, we add the result of the regex to the SP-LOCATION header.
Serve Requests with Python
Now we need to enable a web server to manage and handle the requests we receive. Flask ships with a development server, however, this is not suitable for production. Instead, we'll use Waitress, which is meant to be a production quality pure python WSGI web server. We've already installed the waitress module so we simply need to add it to our code. Update your app.py file to the following:
from flask import Flask
from waitress import serve
import uuid
import os
import re
app = Flask(__name__)
@app.after_request
def add_hostname_header(response):
env_host = str(os.environ.get('HOSTNAME'))
hostname = re.findall('[a-z]{3}-\d$', env_host)
if hostname:
response.headers["SP-LOCATION"] = hostname
return response
@app.route('/')
def get_uuid():
return str(uuid.uuid4())
if __name__ == "__main__":
serve(app, listen='*:80')
We've added the waitress import and instructed waitress to serve the application, listening on port 80 on both IPv4 and IPv6 when this module is executed directly. Let's double check that this works locally before we wrap this up in a docker container.
As we've added waitress in, we need to use a different, simpler command to run the server, and we'll be shown the ports and addresses that waitress is listening on:
(venv)$ python app.py
Serving on http://[::]:80
Serving on http://0.0.0.0:80
We can also test by loading http://localhost in a browser. (We don't need to specify a port to the browser here as port 80 is the default for HTTP).
Create a Dockerfile
Now we're going to create a container for this application so we can deploy it to the edge. Create a file called 'Dockerfile' (no extension) with the following contents:
FROM python:3-alpine
WORKDIR /usr/src/flask_uuid
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python","app.py"]
For those unfamiliar with dockerfiles, a brief explanation is below:
- The python:3-alpine image is used as a base. The alpine image is used as it is extremely small and lightweight and we do not have complex dependencies.
- The WORKDIR is set to /usr/src/flask_uuid
- The contents of the build context (the current dir which contains the Dockerfile) are copied to the WORKDIR in the container
- pip install is executed against the requirements file, installing our dependencies.
- The command for the container is set, which in our instance runs our Flask app served by waitress.
Let's also create a .dockerignore file so we don't copy unrequired items to our container
Dockerfile
.dockerignore
.git
.cache
venv/
*.pyc
Now we're ready to build our container.
$ docker build -t <your name>/flask_uuid .
You can view your built container with:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
<your_repo>/flask_uuid latest 418a87009dad 3 hours ago 33MB
Deploy your Image locally
First, let's run our docker image locally to determine if it works as we expect.
$ docker run -p 49160:80 -d <your_repo>/flask_uuid
This command aliases port 80 on our container instance to port 49160 on our local machine. Access 0.0.0.0:49160 in the browser
We can also test with curl too
Notice how the SP-LOCATION header we defined is not there? That's because the hostname env variable is either a) not present, or b) the application was not able to parse the hostname with the provided regex. Once this is deployed to the Edge you will see this header.
Run docker ps to display active containers
$ docker ps
Let's shut down the container
$ docker stop <container id>
Now that we've confirmed the Docker image built successfully, let's deploy the script onto Edge Computing.
Push the new docker image by its repository name to your DockerHub or personal docker repository.
$ docker push <your_repo>/flask_uuid
Now your docker image is publicly accessible and Edge Computing can pull and deploy it.
Deploy your image on Edge Computing
- Add a new Workload
- Add a Name and add your image, including the repository that you pushed your Docker image to in the Image field. Select the Wordload type: Container.
- Next you can add environment variables, however we don't need any for this project. Enable the Anycast IP option to be provisioned one IP address that will respond from the closest edge, where you code is deployed, to the user.
- Now we can select the VPC (currently only default is available), the required public ports and if we want to execute a command in the container. We don't need to add any commands as the command we need is already in the Dockerfile, so we'll add port 80 to allow web traffic to our container.
- Select the spec you require for your containers and add the locations you require
- Check and confirm your cost estimate, then click 'Deploy Workload' to have your code deployed to your chosen edge locations.
Congratulations, your flask web application has been deployed! In less than a minute StackPath will partition IP addresses to your Pod and the web application will be live and available for your use.
Use the Application
Now it's deployed, test that your code behaves as you expect. Load your IP in a browser to get a UUID.
With cURL, check that the header has been added:
You can also test the header is correct for each location by cURL-ing each container IP instead of the Anycast IP address.
External Links
Documentation for Docker, Flask, VirtualEnv, and Waitress is available in case you would like to develop this project further.