Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release TerriaMap using create-docker-context #681

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
NODE_VERSION: 18

permissions:
packages: write
Expand All @@ -18,32 +19,45 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Install dependencies
run: yarn install --frozen-lockfile
env:
NODE_OPTIONS: "--max_old_space_size=4096"

- name: Build TerriaMap
run: yarn gulp lint release
env:
NODE_OPTIONS: "--max_old_space_size=4096"

- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2

- name: Login to GHCR
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Tag image
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index

- name: Build and push
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
push: true
run: yarn docker-build-prod --metadata
2 changes: 1 addition & 1 deletion architecture/0001-npm-lockfiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Date: 2020-03-02

## Status

Accepted
Superseded 2021-10-09 by decision to use yarn everywhere.

## Context

Expand Down
56 changes: 56 additions & 0 deletions architecture/0002-docker-multi-arch-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 2. Docker multi-architecture build using create-docker-context

Date: 2024-07-18

## Status

Proposed

## Context

I (crispy) consider a multi-stage dockerfile to be the gold standard of
reproducible, mutli-architecture builds. Something like the following:

- Build container copies workspace, installs development dependencies and builds
the app.
- Production container copies build artifacts and installs only production
dependencies.

is ideal. This ensures only production dependencies are present, and you can run
this process on every architecture to create a multi-arch docker image. Binaries
downloaded during dependency installation will fetch the correct architecture
binary as depencies are installed separately on each architecture. But
installing dependencies and building JS is extremely slow on emulated
architectures, such as docker buildx on GitHub Actions (this can take 2.5 hours
to build the image).

If instead we can (on the build machine/VM):

1. install all dependencies
2. build the app
3. copy build artifacts and only production dependencies to the multi-arch
docker image

then **as long as production dependencies are portable**, we have very little
computation being run on emulated architectures. The `create-docker-context.js`
script allows us to do this, copying build artifacts and only production
dependencies to an intermediate "context" folder which is then used to create
the

Currently none of our production dependencies install non-portable binaries.

## Decision

While all production dependencies remain portable, we will build
multi-architecture docker images by building TerriaMap on the VM and copying
only production-necessary files and dependencies to the final docker image.

## Consequences

- We will replace current GitHub Actions release process with one using
`create-docker-context.js`.
- Our GitHub Actions TerriaMap release time will reduce from 2.5 hours to less
than 10 minutes.
- If in future TerriaMap uses a binary installed by side effect during JS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a big deal, Github provides ARM runners now so if this becomes a problem in the future it's "easy" to rectify.

dependency installation and this binary cannot be run on an architecture for
which an image is created, that image will fail when run on that architecture.
2 changes: 1 addition & 1 deletion deploy/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Docker image for the primary terria map application server
FROM node:16
FROM node:16-slim
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Dockerfile runs the node process as root unlike the Dockerfile in the root of this repository that runs it as node.


RUN mkdir -p /usr/src/app && mkdir -p /etc/config/client
WORKDIR /usr/src/app/component
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against the change per se, but there are a lot of things I find confusing.

Is it the terriajs specified in package.json that gets installed, or does this pull from the TerriaJS repository? What creates the node_modules that are copied into /usr/src/app? Is there documentation for how to rebuild the release image locally without using Github actions?

Why does this Dockerfile use /usr/src/app and the Dockerfile in the root of this repository use /app?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an ADR (architecture decision record) explaining why I'm proposing this. Please comment on that, or here if you are still confused or disagree.

This dockerfile copies from a "context" that is created by create-docker-context.js - which is a script that copies only production dependencies and build artifacts to make a docker image. It's non-standard, but gets around running dependency installation and compilation on emulated architectures. The terriajs that is used is whichever is installed at the time that yarn docker-build-prod is run. We usually run releases from GitHub Actions, or if we need to build docker images locally we use a clean clone of TerriaMap. Both methods will always be a clean install pulling the terriajs version specified in package.json.

I think it's more normal to use /usr/src/app than /app

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the description, this seems like a great idea.

Are the steps for how to build the docker image locally written down somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, ADR actually in this PR now. Sorry if you tried looking before, I forgot to push it.

As for building the docker image locally, this would be a separate command yarn docker-build-local, which doesn't do multi-arch builds. I haven't tested that one yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rebuild the release image to get security fixes and a few other local modifications, so I prefer a local build process that is as close to the release-image as possible so I don't have to re-do all the general quality assurance you have already done for the release.

I don't use multi-arch builds personally so not sure how important that is in the grand scheme of things; if I suddenly get ARM machines I can do separate builds on ARM/X86 and then stitch together the two images with crane or some other tool.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The files inside this directory are owned by uid 1001 and gid 127. Is there a reason to not use node:node as owner?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason. I'll apply the changes you made to our other dockerfile here too.

Expand Down
43 changes: 32 additions & 11 deletions deploy/docker/create-docker-context.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
#!/usr/bin/env node

// MAJOR ASSSUMPTION: build artifacts and node_modules content for all production
// dependencies is cross-platform, or care is taken to only install dependencies,
// run create-docker-context.js and create docker images on a compatible platform
// See architecture/0002-docker-multi-arch-build.md

// Based off @magda/docker-utils@2.1.0 create-docker-context-for-node-component
// Changes made:
// - The Dockerfile path is configurable in package.json
// - The Dockerfile path is configurable in package.json (I don't want a dockerfile
// intended to be used only through a script to be in the top level directory)
// - Can parse metadata from GitHub Action docker/metadata-action@v5 and add this
// to the created image

const childProcess = require("child_process");
const fse = require("fs-extra");
Expand Down Expand Up @@ -86,6 +94,12 @@ const argv = yargs
description:
"Version to cache from when building, using the --cache-from field in docker. Will use the same repository and name. Using this options causes the image to be pulled before build.",
type: "string"
},
metadata: {
description:
"Use tags and annotations from https://github.com/docker/metadata-action v5. Utilises env.DOCKER_METADATA_OUTPUT_JSON. Overrides --tag",
type: "boolean",
default: false
}
})
.help().argv;
Expand Down Expand Up @@ -166,16 +180,22 @@ if (argv.build) {
}
);

const tags = getTags(
argv.tag,
argv.local,
argv.repository,
argv.version,
argv.name
);
const tagArgs = tags
.map((tag) => ["-t", tag])
.reduce((soFar, tagArgs) => soFar.concat(tagArgs), []);
// metadata json from GitHub Action docker/metadata-action@v5
const metadata =
argv.metadata && env.DOCKER_METADATA_OUTPUT_JSON
? JSON.parse(env.DOCKER_METADATA_OUTPUT_JSON)
: undefined;

const tags = metadata
? metadata.tags
: getTags(argv.tag, argv.local, argv.repository, argv.version, argv.name);
const tagArgs = tags.flatMap((tag) => ["-t", tag]);

const annotationArgs =
metadata?.annotations?.flatMap((annotation) => [
"--annotation",
annotation
]) ?? [];

const cacheFromArgs = cacheFromImage ? ["--cache-from", cacheFromImage] : [];

Expand All @@ -186,6 +206,7 @@ if (argv.build) {
...(argv.platform ? ["buildx"] : []),
"build",
...tagArgs,
...annotationArgs,
...cacheFromArgs,
...(argv.noCache ? ["--no-cache"] : []),
...(argv.platform ? ["--platform", argv.platform, "--push"] : []),
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
]
},
"name": "terriajs-map",
"version": "0.1.2",
"version": "0.2.0-alpha.4",
"description": "Geospatial catalog explorer based on TerriaJS.",
"license": "Apache-2.0",
"engines": {
Expand Down Expand Up @@ -96,7 +96,7 @@
"scripts": {
"prepare": "husky install",
"docker-build-local": "node deploy/docker/create-docker-context.js --build --push --tag auto --local",
"docker-build-prod": "node deploy/docker/create-docker-context.js --build --push --tag auto --repository=ghcr.io/terriajs",
"docker-build-prod": "node deploy/docker/create-docker-context.js --build --push --platform=linux/amd64,linux/arm64",
"docker-build-ci": "node deploy/docker/create-docker-context.js --build",
"start": "terriajs-server --config-file serverconfig.json",
"gulp": "gulp",
Expand Down