Building a super fast serverless container deployment pipeline on Google Cloud
Blair Hudson
Posted on November 6, 2019
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:
- Speed up local code iteration 🏎💨
- Minimise the number of Cloud Build jobs and Cloud Run revisions (#frugalbydesign) ☁️
- Keep our commit log tidy! 🧹
Here’s a high level view of our approach:
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.
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.
#!/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:
#!/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
wheretest
is released to (also by pull request only, with dual approval required)
The BitBucket repo is automatically synced to a Cloud Source Repository of the same name and branch structure.
Deploying with Cloud Build
Cloud Build allow a build job to trigger on a push 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
docker pull gcr.io/$PROJECT_ID/$REPO_NAME-$BRANCH_NAME:latest
Builds and tags a new Docker image from our Dockerfile
above:
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:
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:
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 for our MVP sign-ups launch! 👕👚
Posted on November 6, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 6, 2019