Introduction
Dalec is a tool for producing container images by first building packages targeting the linux distribution used by the container image. The final output image is a "distroless" container image with the package and all its dependencies installed.
Additionally other outputs can be produced such as source and binary packages, buildroots, and more.
Spec
The dalec spec is a yaml file that describes the package to be built and any customizations to the output image. It includes package metadata like name, version, packager, and other things typically found in a system package. It also includes a list of build and runtime dependencies, how to build the project to be packaged, and what files are included in the package.
In addition to building a traditional package that installs binaries and other files you can also create a "virtual" package, which is a package that references other packages but doesn't install any files itself. This is useful for creating a package that is just a collection of dependencies.
Example
In this example we'll build a virtual package that just installs other packages as dependencies.
# syntax=ghcr.io/azure/dalec/frontend:latest
name: my-package
version: 1.0.0
revision: "1"
packager: Contoso
vendor: Contoso
license: MIT
description: A virtual package that, when installed, triggers other packages to be installed
website: http://contoso.com
dependencies:
runtime:
- my-package-foo
- my-package-bar
You can build it with:
$ docker build -t my-package-image:1.0.0 --target=mariner2 -f my-package.yml .
Note: The syntax
line tells docker the parser to use so it can understand the dalec spec format.
Note: You could also pass the dalec spec file via stdin docker build -t my-package-image:1.0.0 -< my-package.yml
Note: See docker's documentation for more details on how you can pass the spec file to docker.
This will produce a container image named my-package-image:1.0.0
that has the
my-package
virtual package installed along with its runtime dependencies. The
produced container image is a "distroless" container image that only contains
the package and its dependencies. You can customize the base image to use for
the prooduced container. Below is an example that uses the mariner "core" image
as the base image which includes a shell and other tools.
# syntax=ghcr.io/azure/dalec/frontend:latest
name: my-package
version: 1.0.0
revision: "1"
packager: Contoso
vendor: Contoso
license: MIT
description: A virtual package that, when installed, triggers other packages to be installed
website: http://contoso.com
dependencies:
runtime:
- my-package-foo
- my-package-bar
targets:
mariner2:
image:
base: mcr.microsoft.com/cbl-mariner/base/core:2.0
You can also set other image settings like entrypoint/cmd, environment variables, working directory, labels, and more. For now, the best place to find what all is available to set is to look at the code.
# syntax=ghcr.io/azure/dalec/frontend:latest
name: my-package
version: 1.0.0
packager: Contoso
vendor: Contoso
license: MIT
description: A virtual package that, when installed, triggers other packages to be installed
website: http://contoso.com
dependencies:
runtime:
- my-package-foo
- my-package-bar
image:
entrypoint: /bin/sh -c
Note how this is at the top level of the spec and not under a build target. This means that it applies to all targets, but can also be customized per target by adding it under a target.
targets:
mariner2:
image:
entrypoint: /bin/sh -c
Building from source(s)
Virtual packages are helpful but we need to build packages from source too. To do this we'll need a few things:
- A list of sources to pull from
- A build script to build the sources
- A list of artifacts to include in the package
Here we'll pull from a github repo.
It will use the go-md2man
repo and build the go-md2man
from the v2.0.3 tag in the repo.
Note: See the full example from examples/go-md2man.yml
# syntax=ghcr.io/azure/dalec/frontend:latest
name: go-md2man
version: 2.0.3
revision: "1"
packager: Dalec Example
vendor: Dalec Example
license: MIT
description: A tool to convert markdown into man pages (roff).
website: https://github.com/cpuguy83/go-md2man
sources:
src:
git:
url: https://github.com/cpuguy83/go-md2man.git
commit: "v2.0.3"
dependencies:
build:
golang:
build:
env:
CGO_ENABLED: "0"
steps:
- command: |
cd src
go build -o go-md2man .
artifacts:
binaries:
src/go-md2man:
image:
entrypoint: go-md2man
cmd: --help
In the sources
section there is a single source called src
that references
the github repo at tag v2.0.3. The name src
is arbitrary, however this is
where the source will be checked out to in the build phase. You can add
multiple sources, and in the build phase they will be checked out to the name
you give them.
One thing to note, in many build systems you will not have access to the
internet while building the package, and indeed that is the case with the
mariner2
target.
As such, this build will fail because go build
will try to download the go modules.
What is actually happening with sources
is the source is fetched and stored
such that it can be packed up into a "source package". What a source package
entails is dependent on the system. For rpm based systems this is an srpm
or
.src.rpm
, on debian based systems this is a .dsc
. These packages contain
everything needed to build the package (aside from dependencies on other
packages). Source packages can be published to a package repository and then
another system can download the source package and build it.
In the case of the above example, we need to include the go modules in the
list of sources. We'll accomplish this by add a source which will run go mod download
in a docker image with the src
source mounted and then extract the
go modules from the resulting filesystem.
Note: See the full example from examples/go-md2man.yml
# syntax=ghcr.io/azure/dalec/frontend:latest
name: go-md2man
version: 2.0.3
revision: "1"
packager: Dalec Example
vendor: Dalec Example
license: MIT
description: A tool to convert markdown into man pages (roff).
website: https://github.com/cpuguy83/go-md2man
sources:
src:
git:
url: https://github.com/cpuguy83/go-md2man.git
commit: "v2.0.3"
gomods: # This is required when the build environment does not allow network access. This downloads all the go modules.
path: /build/gomodcache # This is the path we will be extracing after running the command below.
image:
ref: mcr.microsoft.com/oss/go/microsoft/golang:1.21
cmd:
dir: /build/src
mounts:
# Mount a source (inline, under `spec`), so our command has access to it.
- dest: /build/src
spec:
git:
url: https://github.com/cpuguy83/go-md2man.git
commit: "v2.0.3"
steps:
- command: go mod download
env:
# This variable controls where the go modules are downloaded to.
GOMODCACHE: /build/gomodcache
dependencies:
build:
golang:
build:
env:
CGO_ENABLED: "0"
steps:
- command: |
export GOMODCACHE="$(pwd)/gomods"
cd src
go build -o go-md2man .
artifacts:
binaries:
src/go-md2man:
image:
entrypoint: go-md2man
cmd: --help
The nice thing about this is you can see exactly what's being done to generate the go modules. The downside is its extremely verbose and even requires you to do things outside of the toolchain for the targeted distribution. The below example does something similar but in a more concise way:
name: go-md2man
version: 2.0.3
revision: "1"
packager: Dalec Example
vendor: Dalec Example
license: MIT
description: A tool to convert markdown into man pages (roff).
website: https://github.com/cpuguy83/go-md2man
sources:
src:
generate:
- gomod: {}
git:
url: https://github.com/cpuguy83/go-md2man.git
commit: "v2.0.3"
dependencies:
build:
golang:
build:
env:
CGO_ENABLED: "0"
steps:
- command: |
cd src
go build -o go-md2man .
artifacts:
binaries:
src/go-md2man:
image:
entrypoint: go-md2man
cmd: --help
The above "gomod" generator takes care of fetching the module dependencies for any and all sources that have the generator set. The module dependencies are managed by Dalec and injected into the build sources. The neccessary go environment variables are set in the build environment so that the go build toolchain knows where to find the modules.
Finally, we can add a test case to the spec file which helps ensure the package is assembled as expected.
The following test will make sure /usr/bin/go-md2man
is installed and has the expected permissions.
These tests are automatically executed when building the container image.
This can get added to the spec file like so:
tests:
- name: Check bin
files:
/usr/bin/go-md2man:
permissions: 0755
Targets
So far we've only really built a spec file asusming a single target distro (mariner2).
However many things, such as package dependencies and base images are specific to a distro or a subset of distros (e.g. Debian and Ubuntu).
The dalec spec allows you to move these distro specific things into a target
.
Instead of specifying a package dependency at the root of the spec, you can specify it under a target. This allows you to include different packages for different targets.
targets:
mariner2:
dependencies:
build:
- golang
Dalec can never hope to support every distro, so it allows you to specify a custom builder image for a target that the build will be forwarded to.
This lets you keep the same spec file for all targets and use one # syntax=
directive to build the package for any target.
It also allows you to replace the built-in targets with your own custom builder.
targets:
mariner2:
frontend:
image: docker.io/my/custom:mariner2
Metadata
You can include client-side metadata in the spec file. This may be useful when you want to parse the spec file and do something with your own tooling.
Any field at the top-level that begins with x-
will be ignored by the dalec parser.
Any unknown fields besides those that start with x-
will cause the parser to fail.
soruces:
src:
http:
url: https://example.com/foo.tar.gz
x-my-custom-field: "foo"
As an example use-case, you may want to use this to store the targeted image name for a CI/CD pipeline.
x-image-name: "my-package-image:1.0.0"
Your CI/CD tooling can then parse the spec file and use the x-image-name
field to tag the built image.
To re-itterate: the x-
fields are ignored by the dalec parser and are only for client-side use.
Additional Reading
- Details on editor support in editor-support.md
- More in-depth documentation for testing can be found in testing.md.
- Explanation of sources and how to use them can be found in sources.md.