Pointing the Way to Serverless Python
Takeaways
Today we’re going to look at running up the world’s simplest Flask app using Cloud Run.
You’ll learn:
- How Google Cloud Run makes hosting apps delightfully easy
- How to write a simple Dockerfile to run your application in Cloud Run
- How you can leverage Waypoint to make development simple
To skip to the end there’s a reference GitHub repository here
The What & Why of Waypoint
What is Waypoint
In October 2020, Hashicorp announced Waypoint, a new way to simplify your development process. Rather than bespoke scripting and Makefiles, you can have a consistent experience to your flow. Coupled with a UI that allows you to simply understand who did what when, it’s an intriguing proposition for modernising and harmonising across your projects.
3 Reasons Why
-
Consistent developer experience over bespoke scripting
-
A central UI for understanding who did what when
-
Spend more time writing code vs deploying code
3 Reasons Why Google Cloud Run
-
The simplest way to get your code live and accessible on the cloud
-
Containers give you high levels of environmental consistency, no more “works on my machine”
-
The first million requests a month are FREE
Prerequisies
Install Waypoint
Follow the Hashicorp instructions here
Install Google Cloud CLI
Follow the Google instructions here
Install Docker
Follow the Docker instructions here
Install Python3 and Pipenv
Follow the Python instructions here
Follow the Pipenv instructions here
Let’s Get Started
Inital Waypoint Set Up
First let’s create a new directory to host our app
mkdir serverless-python && cd serverless-python
If you haven’t already, install a waypoint
server into your Docker environment
waypoint install --platform=docker -accept-tos
Now we can initialise waypoint
waypoint init
That will have created a waypoint.hcl
file that looks like this:
# The name of your project. A project typically maps 1:1 to a VCS repository.
# This name must be unique for your Waypoint server. If you're running in
# local mode, this must be unique to your machine.
project = "my-project"
# Labels can be specified for organizational purposes.
# labels = { "foo" = "bar" }
# An application to deploy.
app "web" {
# Build specifies how an application should be deployed. In this case,
# we'll build using a Dockerfile and keeping it in a local registry.
build {
use "docker" {}
# Uncomment below to use a remote docker registry to push your built images.
#
# registry {
# use "docker" {
# image = "registry.example.com/image"
# tag = "latest"
# }
# }
}
# Deploy to Docker
deploy {
use "docker" {}
}
}
Strip out all the comments:
project = "my-project"
app "web" {
build {
use "docker" {}
}
deploy {
use "docker" {}
}
}
Update project
to be serverless-python
and app
to be api
:
project = "serverless-python"
app "api" {
build {
use "docker" {}
}
deploy {
use "docker" {}
}
}
Adding Some Python
Set up our python environment with pipenv
pipenv --three
Now install the two packages we’re going to need, flask
and gunicorn
pipenv install flask gunicorn
Create a new app.py
file
touch app.py
And fill out with a simple flask app
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
return "Hello, world!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080) # Setting host and port allow the app to be accessible outside of localhost
Containerisation
We need a Dockerfile so we can build our image and hoist it up to the cloud
FROM python:alpine3.12
WORKDIR /usr/src/app
COPY . .
RUN pip install pipenv && pipenv lock -r > requirements.txt && pip install -r requirements.txt
EXPOSE 8080
CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 app:app
A critical thing about Cloud Run is that we need to conform to Google’s container contract. For our purposes here, this means we need to listen for requests on
0.0.0.0
and port8080
, which we implement in theCMD
statement.
It’s always good practice to add a .dockerignore
file to make sure we don’t copy unnecessary files into our images, create one now with the below content
Dockerfile
README.md
*.pyc
*.pyo
*.pyd
__pycache__
.pytest_cache
.waypoint
.vscode
Now we can try building our container for the first time, first it’s worth reinitialising waypoint to make sure it’s caught up on all our changes
waypoint init
And then we can run our first build
waypoint build
You should see output of that looks something like this
➜ waypoint build
✓ Initializing Docker client...
✓ Building image...
│ ---> eb7dbc300413
│ Step 5/6 : EXPOSE 8080
│ ---> Running in da514cb73c55
│ ---> b3b71fee78ed
│ Step 6/6 : CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 ap
│ p:app
│ ---> Running in 1455e0b30f65
│ ---> e9319ce94c71
│ Successfully built e9319ce94c71
│ Successfully tagged waypoint.local/api:latest
✓ Injecting Waypoint Entrypoint...
Great Success!
Now we can run the container locally in docker by running waypoint up
You’ll get console output that looks like
The deploy was successful! A Waypoint deployment URL is shown below. This
can be used internally to check your deployment and is not meant for external
traffic. You can manage this hostname using "waypoint hostname."
Release URL: https://main-lt4ygdndkq-nw.a.run.app
Deployment URL: https://heavily-fitting-piglet--v11.waypoint.run
And if click to go to the url, you hit an error page
Thankfully it’s a simple fix
We need to add service_port = 8080
to our deploy
block
So waypoint.hcl
should look like
project = "serverless-python"
app "api" {
build {
use "docker" {}
}
deploy {
use "docker" {
service_port = 8080
}
}
}
And now after running waypoint up
again the URL works!
So we’ve tested the container works locally, but the goal is to get it up into the cloud.
Let’s make sure we’re authenticated properly and get a list of projects so we can select a target project
gcloud auth application-default login
gcloud projects list
And let’s update our waypoint.hcl
file so we deploy to Cloud Run instead of our local docker
Add this to your build
block and replace <your-project-name>
with your target project
registry {
use "docker" {
image = "gcr.io/<your-project-name>/serverless-python"
tag = "latest"
}
}
Now when we build the Docker image, we’ll automatically upload it to the Google container registry
Replace your deploy
block with and replace <your-project-name>
with your target project
deploy {
use "google-cloud-run" {
project = "<your-project-name>"
location = "europe-west2"
port = 8080
capacity {
memory = 128
cpu_count = 1
max_requests_per_container = 10
request_timeout = 300
}
auto_scaling {
max = 1
}
}
}
And when we click the URL we are again presented with an error page
If we look at the cloud console we can see that our Cloud Run service looks healthy…
Unfortunately, Google is expecting us to authenticate to hit the URL, but we can make it public by adding a release
block
release {
use "google-cloud-run" {}
}
So our waypoint.hcl
now looks like
project = "serverless-python"
app "api" {
build {
use "docker" {}
registry {
use "docker" {
image = "gcr.io/<your-project-name>/serverless-python"
tag = "latest"
}
}
}
deploy {
use "google-cloud-run" {
project = "<your-project-name>"
location = "europe-west2"
port = 8080
capacity {
memory = 128
cpu_count = 1
max_requests_per_container = 10
request_timeout = 300
}
auto_scaling {
max = 1
}
}
}
release {
use "google-cloud-run" {}
}
}
And if we run waypoint up
once more, the console now looks like
By going to the Release URL
from the waypoint output we see
Hello, world!
Recap
Cloud Run is easy
We’ve seen how Cloud Run makes it easy to host apps at record speed, and making changes to production code
Simple Dockerfile skeleton
Although the Dockerfile is not hardened for production use, we know have a skeleton that allows us to rapidly build python applications hosted in the cloud
Waypoint makes for simple development flows
We’ve only touched on the basics of waypoint here, but you can see how it provides a consistent experience that makes common tasks truly simple comapred to bespoke bash scripting
Cleanup
Run waypoint destroy
to clear up all the resources, and it’s always worth double checking in the Google Cloud console to make sure!