Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5172 from ericis/ericis/containerized-devops-tasks
feat(devops): containerized devops tasks example
- Loading branch information
Showing
4 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
root = false | ||
|
||
[{Makefile,makefile,**.mk}] | ||
# Use tabs for indentation (Makefiles require tabs) | ||
indent_style = tab | ||
indent_size = 4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |