Skip to content

Commit

Permalink
Merge branch 'main' into chwa1-exclude-examples
Browse files Browse the repository at this point in the history
  • Loading branch information
regularfry authored Nov 18, 2024
2 parents 7a68381 + 438b8af commit 121c03e
Show file tree
Hide file tree
Showing 14 changed files with 175 additions and 201 deletions.
2 changes: 1 addition & 1 deletion docs/adr/assets/ADR-003/examples/golang/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ require (
github.com/golang-jwt/jwt v3.2.2+incompatible
)

require golang.org/x/net v0.17.0 // indirect
require golang.org/x/net v0.23.0 // indirect
4 changes: 2 additions & 2 deletions docs/adr/assets/ADR-003/examples/golang/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/assets/ADR-003/examples/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
PyJWT==2.8.0
requests==2.31.0
requests==2.32.0
114 changes: 89 additions & 25 deletions docs/developer-guides/Scripting_Docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
- [Versioning](#versioning)
- [Variables](#variables)
- [Platform architecture](#platform-architecture)
- [`Dockerignore` file](#dockerignore-file)
- [FAQ](#faq)

## Overview

This document provides instructions on how to build Docker images using our automated build process. You'll learn how to specify version tags, commit changes, and understand the build output.

Docker is a tool for developing, shipping and running applications inside containers for Serverless and Kubernetes-based workloads. It has grown in popularity due to its ability to address several challenges faced by engineers, like:

- **Consistency across environments**: One of the common challenges in software development is the "it works on my machine" problem. Docker containers ensure that applications run the same regardless of where the container is run, be it a developer's local machine, a test environment or a production server.
Expand Down Expand Up @@ -43,7 +46,6 @@ Here are some key features built into this repository's Docker module:
- Incorporates metadata through `Dockerfile` labels for enhanced documentation and to conform to standards
- Integrates a linting routine to ensure `Dockerfile` code quality
- Includes an automated test suite to validate Docker scripts
- Provides a ready-to-run example to demonstrate the module's functionality
- Incorporates a best practice guide

## Key files
Expand All @@ -61,50 +63,108 @@ Here are some key features built into this repository's Docker module:
- [`docker.test.sh`](../../scripts/docker/tests/docker.test.sh): Main file containing all the tests
- [`Dockerfile`](../../scripts/docker/tests/Dockerfile): Image definition for the test suite
- [`VERSION`](../../scripts/docker/tests/VERSION): Version patterns for the test suite
- Usage example
- Python-based example [`hello_world`](../../scripts/docker/examples/python) app showing a multi-staged build
- A set of [make targets](https://github.com/nhs-england-tools/repository-template/blob/main/scripts/docker/docker.mk#L18) to run the example

## Usage

### Quick start

Run the test suite:
The Repository Template assumes that you will want to build more than one docker image as part of your project. As such, we do not use a `Dockerfile` at the root of the project. Instead, each docker image that you create should go in its own folder under `infrastructure/images`. So, if your application has a docker image called `my-shiny-app`, you should create the file `infrastructure/images/my-shiny-app/Dockerfile`. Let's do that.

First, we need an application to package. Let's do the simplest possible thing, and create a file called `main.py` in the root of the template with a familiar command in it:

```python
print("hello world")
```

Run this command to make the directory:

```shell
mkdir -p infrastructure/images/my-shiny-app
```

Now, edit `infrastructure/images/my-shiny-app/Dockerfile` and put this into it:

```dockerfile
FROM python

COPY ./main.py .

CMD ["python", "main.py"]
```

Note the paths in the `COPY` command. The `Dockerfile` is stored in a subdirectory, but when `docker` runs it is executed in the root of the repository so that's where all paths are relative to. This is because you can't `COPY` from parent directories. `COPY ../../main.py .` wouldn't work.

The name of the folder is also significant. It should match the name of the docker image that you want to create. With that name, you can run the following `make` task to run `hadolint` over your `Dockerfile` to check for common anti-patterns:

```shell
$ DOCKER_IMAGE=my-shiny-app make docker-lint
/workdir/./infrastructure/images/my-shiny-app/Dockerfile.effective:1 DL3006 warning: Always tag the version of an image explicitly
make[1]: *** [scripts/docker/docker.mk:34: _docker] Error 1
make: *** [scripts/docker/docker.mk:20: docker-lint] Error 2
```

All the provided docker `make` tasks take the `DOCKER_IMAGE` parameter.

`hadolint` found a problem, so let's fix that. It's complaining that we've not specified which version of the `python` docker container we want. Change the first line of the `Dockerfile` to:

```dockerfile
FROM python:3.12-slim-bookworm
```

Run `DOCKER_IMAGE=my-shiny-app make docker-lint` again, and you will see that it is silent.

Now let's actually build the image. Run the following:

```shell
DOCKER_IMAGE=my-shiny-app make docker-build
```

And now we can run it:

```shell
$ DOCKER_IMAGE=my-shiny-app make docker-run
hello world
```

If you list your images, you'll see that the image name matches the directory name under `infrastructure/images`:

```shell
$ make docker-test-suite-run
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/my-shiny-app latest 6a0adeb5348c 2 hours ago 135 MB
docker.io/library/python 3.12-slim-bookworm d9f1825e4d49 5 weeks ago 135 MB
localhost/hadolint/hadolint 2.12.0-alpine 19b38dcec411 16 months ago 8.3 MB
```

test-docker-build PASS
test-docker-test PASS
test-docker-run PASS
test-docker-clean PASS
Your process might want to add specific tag formats so you can identify docker images by date-stamps, or git hashes. The Repository Template supports that with a `VERSION` file. Create a new file called `infrastructure/images/my-shiny-app/VERSION`, and put the following into it:

```text
${yyyy}${mm}${dd}-${hash}
```

Run the example:
Now, run the `docker-build` command again, and towards the end of the output you will see something that looks like this:

```shell
$ make docker-example-build
Successfully tagged localhost/my-shiny-app:20240314-07ee679
```

#0 building with "desktop-linux" instance using docker driver
...
#12 DONE 0.0s
Obviously the specific values will be different for you. See the Versioning section below for more on this.

$ make docker-example-run
It is usually the case that there is a specific image that you will most often want to build, run, and deploy. You should edit the root-level `Makefile` to document this and to provide shortcuts. Edit `Makefile`, and change the `build` task to look like this:

* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
* Running on http://172.17.0.2:8000
Press CTRL+C to quit
```make
build: # Build the project artefact @Pipeline
DOCKER_IMAGE=my-shiny-app
make docker-build
```

Now when you run `make build`, it will do the right thing. Keeping this convention consistent across projects means that new starters can be on-boarded quickly, without needing to learn a new set of conventions each time.

### Your image implementation

Always follow [Docker best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) while developing images. Start with creating your container definition for the service and store it in the `infrastructure/images` directory.
Always follow [Docker best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) while developing images.

Here is a step-by-step guide:
Here is a step-by-step guide for an image which packages a third-party tool. It is mostly similar to the example above, but demonstrates the `.tool-versions` mechanism.

1. Create `infrastructure/images/cypress/Dockerfile`

Expand Down Expand Up @@ -212,6 +272,10 @@ Set the `docker_image` or `DOCKER_IMAGE` variable for your image. Alternatively,

For cross-platform image support, the `--platform linux/amd64` flag is used to build Docker images, enabling containers to run without any changes on both `amd64` and `arm64` architectures (via emulation).

### `Dockerignore` file

If you need to exclude files from a `COPY` command, put a [`Dockerfile.dockerignore`](https://docs.docker.com/build/building/context/#filename-and-location) file next to the relevant `Dockerfile`. They do not live in the root directory. Any paths within `Dockerfile.dockerignore` must be relative to the repository root.

## FAQ

1. _We built our serverless workloads based on AWS Lambda and package them as `.zip` archives. Why do we need Docker?_
Expand Down
6 changes: 6 additions & 0 deletions scripts/config/markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# SEE: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml

# https://github.com/DavidAnson/markdownlint/blob/main/doc/md010.md
MD010: # no-hard-tabs
ignore_code_languages:
- make
- console

# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
MD013: false

Expand Down
64 changes: 53 additions & 11 deletions scripts/docker/docker.lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ function docker-build() {

version-create-effective-file
_create-effective-dockerfile
# The current directory must be changed for the image build script to access
# assets that need to be copied
current_dir=$(pwd)
cd "$dir"

tag=$(_get-effective-tag)

docker build \
--progress=plain \
--platform linux/amd64 \
Expand All @@ -43,16 +42,36 @@ function docker-build() {
--build-arg GIT_COMMIT_HASH="$(git rev-parse --short HEAD)" \
--build-arg BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S%z")" \
--build-arg BUILD_VERSION="$(_get-effective-version)" \
--tag "${DOCKER_IMAGE}:$(_get-effective-version)" \
--tag "${tag}" \
--rm \
--file "${dir}/Dockerfile.effective" \
.
cd "$current_dir"

# Tag the image with all the stated versions, see the documentation for more details
for version in $(_get-all-effective-versions) latest; do
docker tag "${DOCKER_IMAGE}:$(_get-effective-version)" "${DOCKER_IMAGE}:${version}"
if [ ! -z "$version" ]; then
docker tag "${tag}" "${DOCKER_IMAGE}:${version}"
fi
done
docker rmi --force "$(docker images | grep "<none>" | awk '{print $3}')" 2> /dev/null ||:
}

# Create the Dockerfile.effective file to bake in version info
# Arguments (provided as environment variables):
# dir=[path to the Dockerfile to use, default is '.']
function docker-bake-dockerfile() {

local dir=${dir:-$PWD}

version-create-effective-file
_create-effective-dockerfile
}

# Run hadolint over the generated Dockerfile.
# Arguments (provided as environment variables):
# dir=[path to the image directory where the Dockerfile.effective is located, default is '.']
function docker-lint() {
local dir=${dir:-$PWD}
file=${dir}/Dockerfile.effective ./scripts/docker/dockerfile-linter.sh
}

# Check test Docker image.
Expand Down Expand Up @@ -81,12 +100,13 @@ function docker-check-test() {
function docker-run() {

local dir=${dir:-$PWD}
local tag=$(dir="$dir" _get-effective-tag)

# shellcheck disable=SC2086
docker run --rm --platform linux/amd64 \
${args:-} \
"${DOCKER_IMAGE}:$(dir="$dir" _get-effective-version)" \
${cmd:-}
"${tag}" \
${DOCKER_CMD:-}
}

# Push Docker image.
Expand Down Expand Up @@ -114,7 +134,8 @@ function docker-clean() {
done
rm -f \
.version \
Dockerfile.effective
Dockerfile.effective \
Dockerfile.effective.dockerignore
}

# Create effective version from the VERSION file.
Expand Down Expand Up @@ -207,6 +228,13 @@ function _create-effective-dockerfile() {

local dir=${dir:-$PWD}

# If it exists, we need to copy the .dockerignore file to match the prefix of the
# Dockerfile.effective file, otherwise docker won't use it.
# See https://docs.docker.com/build/building/context/#filename-and-location
# If using podman, this requires v5.0.0 or later.
if [ -f "${dir}/Dockerfile.dockerignore" ]; then
cp "${dir}/Dockerfile.dockerignore" "${dir}/Dockerfile.effective.dockerignore"
fi
cp "${dir}/Dockerfile" "${dir}/Dockerfile.effective"
_replace-image-latest-by-specific-version
_append-metadata
Expand Down Expand Up @@ -276,6 +304,20 @@ function _get-effective-version() {
head -n 1 "${dir}/.version" 2> /dev/null ||:
}

# Print the effective tag for the image with the version. If you don't have a VERSION file
# then the tag will be just the image name. Otherwise it will be the image name with the version.
# Arguments (provided as environment variables):
# dir=[path to the image directory where the Dockerfile is located, default is '.']
function _get-effective-tag() {

local tag=$DOCKER_IMAGE
version=$(_get-effective-version)
if [ ! -z "$version" ]; then
tag="${tag}:${version}"
fi
echo "$tag"
}

# Print all Docker image versions.
# Arguments (provided as environment variables):
# dir=[path to the image directory where the Dockerfile is located, default is '.']
Expand Down
Loading

0 comments on commit 121c03e

Please sign in to comment.