Skip to content

Commit

Permalink
Merge pull request #5172 from ericis/ericis/containerized-devops-tasks
Browse files Browse the repository at this point in the history
feat(devops): containerized devops tasks example
  • Loading branch information
benjdlambert committed Mar 30, 2021
2 parents d13b0ce + aaa3dd2 commit 173adf6
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 0 deletions.
6 changes: 6 additions & 0 deletions contrib/make/.editorconfig
@@ -0,0 +1,6 @@
root = false

[{Makefile,makefile,**.mk}]
# Use tabs for indentation (Makefiles require tabs)
indent_style = tab
indent_size = 4
53 changes: 53 additions & 0 deletions contrib/make/Dockerfile
@@ -0,0 +1,53 @@
ARG IMAGE_TAG=14-alpine

FROM node:${IMAGE_TAG}

ARG APK_VIRTUAL_NAME=.backstage

WORKDIR /tmp

# update package sources
# `--virtual` is used to enable removal of these dependencies as a named group
# - python: required by node-gyp
# - pixman: required by node-canvas
# - pkgconfig: required by pixman
# --------------------------------------------
# ... the below are: required by node-canvas
# see: https://github.com/Automattic/node-canvas/wiki/Installation%3A-Ubuntu-and-other-Debian-based-systems
# - alpine-sdk (~build-essential for Debian)
# - cairo-dev (~libcairo2-dev for Debian)
# - pango-dev (~libpango1.0-dev for Debian)
# - jpeg-dev (~libjpeg-dev for Debian)
# - giflib-dev (~libgif-dev for Debian)
# - librsvg-dev (~librsvg2-dev for Debian)
# --------------------------------------------
# - libsecret: required by pkg-config during `yarn run lint:docs`
# --------------------------------------------
# ... the below are: required by vale
# https://github.com/errata-ai/vale/blob/2fe466e41f1b371bfac7334c2a4643cd577c0668/Dockerfile#L13
# - py3-docutils
# - asciidoctor
# --------------------------------------------
RUN apk update \
&& apk add --no-cache \
--virtual ${APK_VIRTUAL_NAME} \
python \
pixman \
pkgconfig \
alpine-sdk \
cairo-dev \
pango-dev \
jpeg-dev \
giflib-dev \
librsvg-dev \
libsecret \
py3-docutils \
asciidoctor

RUN wget -qO- https://github.com/errata-ai/vale/releases/download/v2.10.2/vale_2.10.2_Linux_64-bit.tar.gz \
| tar xvz -C /bin vale \
&& chmod +x /bin/vale

RUN mkdir /app

WORKDIR /app
56 changes: 56 additions & 0 deletions contrib/make/README.md
@@ -0,0 +1,56 @@
# Immutable Task Pattern

Whether executed locally on an engineer's machine or remotely on a server, ensuring consistent DevOps task execution improves reproducibility and the quality of an experiment.

## Context and problem

Great progress has been made in developing immutable execution environments for application deployments as well as for the build, test, and deploy environments themselves (continuous integration, deployment and release). Local development can also benefit from immutable execution environments, especially when replicating issues.

Engineers typically execute tasks directly from their machine. As a result, they have different operating systems, program and package versions, and configurations. While locking versions of application package dependencies and even tooling with modern features, like `yarn set version ...`, help, there can still be significant differences from machine to machine.

## Solution

Containers, like [Docker](https://www.docker.com/), enable an immutable definition of the task execution environment.

## Issues and considerations

- Hosted and sometimes proprietary tasks may be unavoidable, like those offered in the GitHub Actions marketplace or tasks built into products like the core capabilities of Jenkins or through plug-ins. These tasks typically require a "poke the server" approach to debugging end-to-end workflows or containerizing the installation and configuration of the products themselves to be executed as larger containerized tasks. This might introduce the need for guidance on when to define containerized tasks and when to lean on hosted and/or proprietary solutions.
- Cross-platform presents challenges. Getting consistency across Linux, macOS, and Windows requires knowledge and effort. A quick argument can be to standardize on Linux shell across these three major platforms, but even working across Linux distributions presents some unique challenges too.
- Consider the balance of developers freedom of choice in tooling with the consistency with standardized tooling that can be achieved with containerized execution environments. Empower developers to "opt-out" and swap tooling choices wherever possible to experiment and innovate.

## When to use this pattern

- To ensure task execution consistency across a variety of environments, especially across developer workstations and remote DevOps servers
- For tasks that are particularly fragile and/or complex (e.g. developers consistently reporting setup and environmental issues)
- Code editors and "Integrated Development Environments" may offer support for containerized development, like Visual Studio Code's Remote Development extension. This pattern can easily be extended to support these scenarios. One important consideration is GUI tooling support. Most task execution shouldn't require a GUI, especially for remote DevOps tasks. However, developers might use GUI tooling that could also use containerization or virtualization. So, consider how to achieve both. For example "Docker in Docker" (DiD) could enable a GUI development container to execute containerized tasks in separate containers.

## Example

Backstage is a web application with prerequisites on Node and Yarn. Engineers are encouraged to install a specific version of Node and Yarn. The version of node is partially enforced by the "engines" configuration in "package.json". And, the version of `yarn` was "locked" using the command `yarn set version ...`, resulting in the root project yarn configuration file with the setting "yarn-path" that contains a specific version of Yarn. Engineers are encouraged to deploy with Docker.

While local execution of DevOps tasks are defined in the "scripts" section of Node's "package.json" file, the DevOps tasks used during continuous integration and deployment are defined separately in GitHub workflow YAML files to be executed by GitHub Actions using separate task definitions.

Docker could be used to unite the execution of local workstation tasks and remote DevOps tasks. With a consistent container image definition, tasks can be executed with guarantees about the OS, tooling and versions and configuration.

A simple option might be to build the image as needed. Local development could build the image once and reuse it over time. However, DevOps servers would need to build the image on every execution set. Also, building ad-hoc still introduces some significant variability when tooling is installed into the container and the system is updated.

A more robust option might be to define and build the DevOps container(s) for task execution once and then reuse the container as needed. Local development could pull the exact same image that remote DevOps servers also pull.

This example will implement the simple option and leave it to project adopters to choose how to build, store and pull the image.

### Prerequisites

Because the environments are containerized, the only requirement is [Docker](https://docs.docker.com/get-docker/). Please install the latest stable version if possible. Docker is simply one of the more ubiquitous runtime options. However, you might also prefer any runtime that supports building and running docker images. If a pre-built image were build and made available using a standard like the [Open Container Initiative](https://opencontainers.org/), then you could use any OCI runtime.

[GNU Make](https://www.gnu.org/software/make/) is highly recommended to ensure the docker commands execute consistently. Microsoft Windows users can install a Linux shell with `make` installed (e.g. Windows Subsystem for Linux, Cygwin, etc.). It would also be possible to define shell scripts and remove the `make` dependency. However, GNU Make was built for this very purpose. Without it, each `make` command will need to be understood, built (i.e. manual variable substitution) and executed.

### Steps

The steps below attempt to mirror the exact same onboarding steps for a new contributor.

1. Open a shell and change to this directory: `cd contrib/make`
2. `make install` will build the container and then execute the project's "install" setup including `npm` dependencies
3. `make tsc` will compile typescript
4. `make build` will build the project

If this were a standard for implementation, all tasks, including local development and remote DevOps, would be defined in the same way. For example, GitHub Actions would rely on these commands and containers to execute the same tasks a developer could execute locally with the exception of using 3rd party tasks from the GitHub Actions marketplace.
119 changes: 119 additions & 0 deletions contrib/make/makefile
@@ -0,0 +1,119 @@
docker_tag := backstage-make
docker_name_prefix := backstage-make
frontend_port := 3000
frontend_host_port := 3000
backend_port := 7000
backend_host_port := 7000

now_date_time_tag = `date +'%Y%m%d%H%M%S'`
my_dir_path = $(dir $(CURDIR)/$(firstword $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)))
project_root_dir_path = $(my_dir_path)/../../
docker_name_timestamp_prefix = $(docker_name_prefix)-$(now_date_time_tag)

.PHONY: container
container:
@docker build \
-t $(docker_tag) \
-f ./Dockerfile \
.

.PHONY: rm-container
rm-container:
@docker rmi -f $(docker_tag)

.PHONY: clean
clean: container
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
--network host \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
rm -rf node_modules

.PHONY: install
install: container
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
--network host \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
yarn install

.PHONY: tsc
tsc: install
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
--network host \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
yarn tsc

.PHONY: build
build: tsc
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
--network host \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
yarn build

# "check" is a standard make target
# using make's terminology instead of the project's
.PHONY: check-docs
check-docs: install
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
--network host \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
yarn run lint:docs

# check-docs alias
.PHONY: lint-docs
lint-docs: check-docs

.PHONY: check-prettier
check-prettier: install
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
--network host \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
yarn run prettier:check

# BUG: the frontend seems to run on "$(backend_port)" (7000 default).
# The documentation states "This is going to start two things,
# the frontend (:3000) and the backend (:7000)."
# However, the frontend seems to end up running on 7000.
.PHONY: dev
dev:
@docker run --rm -it \
--name $(docker_name_timestamp_prefix)-$@ \
-p $(frontend_port):$(frontend_host_port) \
-p $(backend_port):$(backend_host_port) \
--env PORT=$(frontend_port) \
-v $(project_root_dir_path):/app \
-w /app \
--entrypoint "" \
$(docker_tag) \
yarn dev

# dev alias
.PHONY: start
start: dev

# dev alias
.PHONY: run
run: dev

0 comments on commit 173adf6

Please sign in to comment.