Building a super fast serverless container deployment pipeline on Google Cloud
Tags: googlecloud, devops, python, git
Published: 2019-11-06 03:10:11

One of our driving principles for `shirtctl` is #frugalbydesign - we simply don’t want to be paying for anything that we don’t use.

Our architecture needs to balance cost alongside other core capabilities like application security 🔒, design flexibility 💪 and developer collaboration 👩‍💻👨‍💻.

In this post, we’ll be sharing the some of the details of our continuous deployment pipeline. We’ve combined BitBucket with Google's Cloud Build service, which deploys our applications onto Cloud Run in an average of 1-2 minutes per build!

For development, we’ve also created a local build workflow to:

1. Speed up local code iteration 🏎💨
2. Minimise the number of Cloud Build jobs and Cloud Run revisions (#frugalbydesign) ☁️
3. Keep our commit log tidy! 🧹

Here’s a high level view of our approach:

![shirtctl-ci-pipeline](https://thepracticaldev.s3.amazonaws.com/i/kx1pod5sz2e8pj8bes2h.jpeg)

Now let’s take a closer look at some of the major components. 🔎 

## Speedy local builds

Our MVP sign-ups API is a Python Flask app. It relies on a few various Python packages that provide the REST framework, email, storage and other capabilities. Right now it’s a simple `api.py` file and a `requirements.txt` that represents our package dependencies.

Our `Dockerfile` for local and cloud deployment is purposefully identical, so we can focus on API development.

```dockerfile
FROM python:slim

# install python dependencies
RUN python3 -m venv /app/env
COPY requirements.txt .
RUN /app/env/bin/pip install -r requirements.txt

# configure port (Cloud Run requires 8080)
ENV PORT=8080
EXPOSE $PORT

# setup application runtime
WORKDIR /app/src
ENV GOOGLE_APPLICATION_CREDENTIALS="/app/sa-key.json”

COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

COPY api.py .

CMD ["sh", "-c", "./entrypoint.sh"]
```

We have a `localbuild.sh` script that emulates Cloud Run deployment locally using Docker, which means we can iterate our development tasks very quickly without having to redeploy to Cloud Run.

```bash
#!/bin/bash
REPO=$(basename -s .git $(git config --get remote.origin.url))
BRANCH=$(git rev-parse --abbrev-ref HEAD)

gcloud iam service-accounts keys create sa-key.json \
 --iam-account service-account@project.iam.gserviceaccount.com
SA_KEY_FILE_BASE64=$(cat sa-key.json | base64)

docker build -t shirtctl-${REPO}-${BRANCH}:latest .

docker run --rm -it \
 -e K_SERVICE=localbuild \
 -e SA_KEY_FILE_BASE64 \
 -p 8080:8080 \
 -v $(pwd):/app/src \
 shirtctl-${REPO}-${BRANCH}:latest
```

We can “hot reload” 🔥 our changes to develop even faster! `entrypoint.sh` determines at run time whether to run Flask or Gunicorn depending on the value of `$K_SERVICE`. This way our Flask service restarts automatically when changes to the source code are detected:

```bash
#!/bin/bash
echo $SA_KEY_FILE_BASE64 | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS

if [ "$K_SERVICE" = "localbuild" ] ; then
	export FLASK_APP="api.py"
 	export FLASK_DEBUG=1
	/app/env/bin/flask run --host=0.0.0.0 --port=$PORT
else
	/app/env/bin/gunicorn --bind=0.0.0.0:$PORT api:app
fi
```

## BitBucket to Cloud Source Repository

Code is committed and pushed to a private BitBucket repo. Our branching structure is simple:

- ⚙️ `dev` for feature-based development (we can have as many of these as required!)
- ✅ `test` where all feature dev branches are merged to (by pull request only)
- 🚀 `prod` where `test` is released to (also by pull request only, with dual approval required)

The BitBucket repo is [automatically synced to a Cloud Source Repository](https://cloud.google.com/source-repositories/docs/mirroring-a-bitbucket-repository) of the same name and branch structure.

## Deploying with Cloud Build

Cloud Build allow [a build job to trigger on a push](https://cloud.google.com/cloud-build/docs/running-builds/automate-builds) to our repo. This runs submits the `cloudbuild.yaml` file from our repo to Cloud Build, which accomplishes the following steps for the current branch:

Pulls the previous Docker image from Google Container Registry

```bash
docker pull gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:latest
```

Builds and tags a new Docker image from our `Dockerfile` above:

```bash
docker build . \
 --cache-from gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:latest \
 -t gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:$SHORT_SHA \
 -t gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:latest
```

Pushes the latest image to Google Container Registry:

```bash
docker push gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:$SHORT_SHA
docker push gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:latest
```

Deploys the latest image to Cloud Run, and maps the appropriate domains to access the service:

```bash
gcloud beta run deploy $REPO_NAME-$BRANCH_NAME \
         --image gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:$SHORT_SHA
gcloud beta run domain-mappings create \
         --service $REPO_NAME-$BRANCH_NAME \
         --domain $BRANCH_NAME.$REPO_NAME.shirtctl.com
```

That's all for now! Keep an eye on [shirtctl.com](https://shirtctl.com) for our MVP sign-ups launch! 👕👚