This document describes Grapl's build system. Here you will find instructions for building the Grapl source tree, running tests, and running Grapl locally. This document also describes how the build tools are used in our Github Actions based Continuous Integration (CI) system.
Grapl uses Docker for build and test environments. All Grapl source builds happen in Docker image builds. This has the added benefit of enabling Grapl developers to easily spin up the entire Grapl stack locally for a nice interactive development experience.
- Docker Engine (version 20.10 or later)
- docker-compose
- GNU Make
Our Makefile defines a number of targets for building, testing and running
Grapl locally. A listing of helpful targets can be printed with make help
.
Examples:
- To kick off a local build (but no tests), run the following command:
make build
- To run all the unit tests, run the following command:
make test-unit
To run build and launch Grapl locally, run the following command
make up
For convenience, the Makefile imports environment variables from a .env
file.
The following environment variables can affect the build and test environments:
TAG
(default:latest
) - This is the tag we'll use for all the Docker images. For local buildslatest
is fine. Production builds should have a specific version e.g.v1.2.3
. Users may want to use a tag that includes version and/or branch information for tracking purposes (ex:v1.2.3-my_feature
).CARGO_PROFILE
(default:debug
) - Can either bedebug
orrelease
. These roughly translate to the Cargo profiles to be used for Rust builds.GRAPL_RUST_ENV_FILE
- File path to a shell script in to be sources for the Rust builds. This can be used to set and override environment variables, which can be useful for things like settings for sccache, which is used to for caching. It is passed as a Docker build secret so it should be suitable secrets like S3 credentials for use with sccache.DOCKER_BUILDX_BAKE_OPTS
- Docker images are built using docker buildx. You can pass additional arguments to thedocker buildx build
commands by setting this option (ex:--progress plain
).
By default, our builds use Mozilla's sccache to cache builds in a cache mount type. This improves performance for local development experience as Rust sources change.
Environment variables used by sccache
can be supplied via the
GRAPL_RUST_ENV_FILE
environment variable when running Make.
Examples:
- To disable
sccache
you can do the following:
$ echo "unset RUSTC_WRAPPER" > .rust_env.sh
$ export GRAPL_RUST_ENV_FILE=.rust_env.sh
$ make build
- To configure
sccache
to use S3 on a local minio server running on 172.17.0.100:8000:
$ cat <<EOF > .rust_env.sh
export SCCACHE_BUCKET=sccache
export AWS_ACCESS_KEY_ID=AKIAEXAMPLE
export AWS_SECRET_ACCESS_KEY="d2hhdCBkaWQgeW91IGV4cGVjdCB0byBmaW5kPwo="
export SCCACHE_DIR=/root/sccache
export SCCACHE_ENDPOINT="172.17.0.100:8000"
EOF
$ export GRAPL_RUST_ENV_FILE=.rust_env.sh
$ make build
Docker Compose files are used to define:
- how Docker images are to be built
- how to run tests in Docker containers
- how to run the local Grapl environment
The Makefile references Docker Compose files for each target that uses Docker (most of them).
We use Dockerfile multi-stage builds so each service can be built with a single Docker build command. Additionally, we use docker buildx bake to build multiple Docker images with a single BuildKit command, which allows us to leverage BuildKit concurrency across all stages. The Docker build arguments for each service and container are defined in various Docker Compose files.
For example, to build Grapl services we have the following Make target:
DOCKER_BUILDX_BAKE := docker buildx bake $(DOCKER_BUILDX_BAKE_OPTS)
...
.PHONY: build-services
build-services: ## Build Grapl services
$(DOCKER_BUILDX_BAKE) -f docker-compose.yml
Within docker-compose.yml, we have various services such as the Sysmon generator. The following defines how to build the Docker image.
grapl-rust-sysmon-subgraph-generator:
image: grapl/grapl-sysmon-subgraph-generator:${TAG:-latest}
build:
context: src/rust
target: sysmon-subgraph-generator-deploy
args:
- CARGO_PROFILE=${CARGO_PROFILE:-debug}
...
Similar can be seen for other Grapl services, as well as Grapl tests, which can
be found under the test
directory.
Most Grapl Dockerfiles have build targets specific for running tests. Docker Compose is used to define the containers for running tests, as well as how the image for the container should be built. The following is the definition for Rust unit tests (test/docker-compose.unit-tests-rust.yml):
version: "3.8"
# environment variable PWD is assumed to be grapl root directory
services:
grapl-rust-test:
image: grapl/rust-test-unit:${TAG:-latest}
build:
context: ${PWD}/src/rust
target: build-test-unit
args:
- CARGO_PROFILE=debug
command: cargo test
The build-test-unit
target is a Dockerfile stage
that will builds dependencies for cargo test
that wasn't done so in the
initial source build, cargo build
.
We're currently using docker compose up
to run our tests concurrently. We
have a helper script that checks the
exit code for each container (test) run. If any test exit code is non-zero, the
script will return non-zero as well. This allows us to surface non-zero exit
codes to Make.
The make up
command will build Grapl sources and launch Docker Compose to run
the Grapl environment locally.
If you'd like to skip building and run the Grapl environment locally you can run:
TAG=latest docker compose up
Note that TAG
should be set to whatever you used in your make
invocation (see previous section).
Alternatively, you can set tag to of the tags to a particular Grapl release we
have posted on our Dockerhub. At the time of this writing there are no releases
currently supported for local Grapl, however the main
tag is kept
up-to-date with the latest main
branch on GitHub for development and
testing. Example:
TAG=main docker compose up
Grapl is transitioning to Nomad as our container orchestration. This will replace both AWS Fargate and docker-compose.
Have docker and docker compose installed. Install Nomad and Consult following the instructions at https://www.nomadproject.io/downloads and https://www.consul.io/downloads respectively.
Install CNI plugins:
sudo mkdir -p /opt/cni/bin
cd /opt/cni/bin
sudo wget https://github.com/containernetworking/plugins/releases/download/v1.0.0/cni-plugins-linux-amd64-v1.0.0.tgz
sudo tar -xf cni-plugins-linux-amd64-v1.0.0.tgz
If you are on ChromeOS you will also need to run the following due to hashicorp/nomad#10902. This will allow Nomad to run bridge networks
sudo mkdir -p /lib/modules/$(uname -r)/
echo '_/bridge.ko' | sudo tee -a /lib/modules/$(uname -r)/modules.builtin
make up
We use BuildKite for automated builds, automated tests, and automated releases. There are three primary workflow definitions:
- pipeline.verify.yml -- This workflow runs on every PR, and every time a PR is updated. This runs all formatting and linting checks, runs all build and test targets and tests performs some additional analysis on the codebase (e.g. LGTM checks).
- pipeline.merge.yml -- This workflow runs every time we merge a PR into main. It builds all the release artifacts, and creates a release candidate.
- cargo-audit.yml -- Runs cargo audit to check our Rust dependencies for security vulnerabilities on every PR when Rust dependencies are changed. Also periodically runs over all Rust dependencies unconditionally.
The core values of Grapl's build system are:
- Simplicity -- It should be easy to understand what everything
does, and why. You need only to remember one thing:
make help
. - Evolvability -- It should be easy to add functionality. When adding a new Grapl service or library to the build system you just need to update a Dockerfile, and corresponding Docker Compose files.
- Orthogonality -- All the tools should be easily composed. For
example, in each of Grapl's source subtrees you will find that we
use the normal build tools for each language. So in
src/rust
you can executecargo test
to run all the Rust tests. Insrc/python/*
you can runpy.test
to execute python tests. We run these same commands in the build system.
Note that this list does not include the following:
- Cleverness -- Clever is complex. Clever is exhausting. Clever is hard to work with. We use the normal tools in the normal way. No clever hacks.
- Innovation -- Innovation is expensive. We strive to minimize innovation, and constrain it to only those areas where it's necessary. Reinventing things that have already been done better elsewhere drains value instead of adding it.