Terraforming GitLab & Heroku to deploy Dockerised apps

Philip Jones
May 21 · 3 min read

This is a short article that explains how to deploy a dockerised app to Heroku using GitLab CI and Terraform. This creates a fully automated and repeatable build system.

Terraform is used to setup CI/CD on Gitlab to Heroku.

Terraform

Rather than use the Heroku CLI tooling, we use Terraform to create and specify anything on Heroku. This ensures that we can define our infrastructure as code, rather than relying on manual actions. In addition we store the Terraform code in git, allowing a fully auditable history of changes.

To start with Terraform we need to configure providers for Heroku and GitLab,

provider "gitlab" {
token = "example" # Access token
}
provider "heroku" {
email = "${var.heroku_username}"
api_key = "${var.heroku_api_key}"
}

running terraform init should confirm these have been setup. Next we need to create a Heroku app using the container stack,

resource "heroku_app" "default" {
name = "${var.name}"
region = "eu" # Or "us"
stack = "container"
}

running terraform apply should then create this application. Finally we need to create the Gitlab repository with the relevant protected environment variables to allow the CI scripts to push to Heroku,

resource "gitlab_project" "default" {                                                                                                                                                                       
name = "${var.name}"
visibility_level = "private"
default_branch = "master"
merge_method = "ff"
only_allow_merge_if_pipeline_succeeds = true
}
resource "gitlab_project_variable" "heroku_app" {
key = "HEROKU_APP"
value = "${heroku_app.default.name}"
project = "${gitlab_project.default.id}"
protected = true
}
resource "gitlab_project_variable" "heroku_token" {
key = "HEROKU_API_KEY"
value = "${var.heroku_api_key}"
project = "${gitlab_project.default.id}"
protected = true
}
resource "gitlab_project_variable" "heroku_username" {
key = "HEROKU_USERNAME"
value = "${var.heroku_username}"
project = "${gitlab_project.default.id}"
protected = true
}

again running terraform apply should show all of these settings having been applied.

GitLab CI & CD

The GitLab CI system is used to lint and test the code before pushing the docker image to Heroku and setting the image live. It is setup via a .gitlab-ci.yml configuration file in the repository root. The lint-test (CI) stage, which lints then tests the frontend code and dockerfile works as follows,

stages:
- lint-test
- push

frontend-ci:
stage: lint-test
image: node:10-alpine

before_script:
- apk --update add yarn
- yarn install

script:
- yarn run lint
- yarn run test

docker-ci:
stage: lint-test
image: hadolint/hadolint:latest-debian

script:
- hadolint Dockerfile

note our systems have package.json scripts to lint and test the code and we use yarn rather than npm.

If the lint-test stage passes GitLab will move onto the push (CD) stage, which we have setup to build a docker image, push it to Heroku and then set that image live as a release,

heroku-cd:
stage: push
image: docker:latest

services:
- docker:dind

before_script:
- apk add --update curl

script:
- >
docker login registry.heroku.com
--username $HEROKU_USERNAME
--password $HEROKU_API_KEY
- >
docker build --build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA
-t registry.heroku.com/$HEROKU_APP/web .
- docker push registry.heroku.com/$HEROKU_APP/web
- >
curl --fail
-X PATCH "https://api.heroku.com/apps/$HEROKU_APP/formation"
-H 'Content-Type:application/json'
-H 'Accept:application/vnd.heroku+json; version=3.docker-releases'
-H "Authorization:Bearer $HEROKU_API_KEY"
-d '{"updates":[{"type":"web","docker_image":"'$(docker inspect registry.heroku.com/$HEROKU_APP/web --format={{.Id}})'"}]}'
only:
- master

note that spaces after : should be avoided, as the YAML parser will confuse them for YAML key value pairs. Also note that > indicates a multiline command with the newlines being replaced with spaces.

Dockerised app

Finally we need to Dockerise the application, to do this we have a Dockerfile that builds the code and bundles it with the correct command to run it.

FROM node:10-alpine

# hadolint ignore=DL3018
RUN apk --no-cache add yarn dumb-init

ENTRYPOINT ["dumb-init"]
CMD ["yarn", "run", "serve"]

COPY . /app/
WORKDIR /app/
RUN yarn install && yarn run build

the package.json script yarn run serve then serves the app. With Heroku it is key that the application is served over the port defined in the environment variable $PORT, via a server snippet like,

const express = require('express');
const port = process.env.PORT || 8080; // Default to 8080 in dev
const app = express();
...
app.listen(port);

Advancements

This serves as a great starter setup, it doesn’t however allow for rollbacks, canary deploys or any other useful CD tooling. All of these can be added over time as the application and deployment grow. It does however fully automate the CI and CD tool chain, requiring no manual steps to setup and start using.

Octopus Wealth

Thanks to Anthony Panagi, Oliver DS, and Fred Rivett

Philip Jones

Written by

Head of Engineering at Octopus Wealth. Maintainer of Quart, Hypercorn and various other Python HTTP projects.

Octopus Wealth

Insights and updates from Octopus Wealth HQ. Covering finance, tech, culture & more…

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade