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

  1. Consistent developer experience over bespoke scripting

  2. A central UI for understanding who did what when

  3. Spend more time writing code vs deploying code

3 Reasons Why Google Cloud Run

  1. The simplest way to get your code live and accessible on the cloud

  2. Containers give you high levels of environmental consistency, no more “works on my machine”

  3. 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 port 8080, which we implement in the CMD 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!