Contents

Please OCI on GitLab

Using Please and Gitlab to build OCI Images

This post walks through creating a project that uses Please to build a Go binary and package it in an OCI-compliant container image using non-Docker tools. The project uses Gitlab for Continuous Integration and a Container Registry to build and store the container image.

The completed project can be viewed here.

Please

Please is a build tool written in Go. It’s akin to Make or Bazel, in that you define targets, and the tool handles building the target and it’s dependencies. Insofar as the tools that are inspired by Blaze go (Google’s internal build tool), Please does not have quite as many supported rules. However, I’ve found it to be quite extensible when I’ve needed to leave the builtin rules. I’m not sure that it’s quite as widely used as some of it’s counterparts, which can make it difficult to troubleshoot issues with a web search. However, once you’ve used it a bit, I’ve found it to be intuitive, lightweight, and extensible. One nice thing, unlike several similar products, is that it doesn’t have a dependency on the JVM, which means getting it to run on a new machine or in a container is simple.

Open Container Initiative (OCI)

From the OCI’s website:

The Open Container Initiative is an open governance structure for the express purpose of creating open industry standards around container formats and runtimes.

If you’re not familiar with OCI, but you know of Docker, then you’ll understand OCI just fine. Docker is one of the founding organizations within the OCI governance, however their product is but one of several that implements of the specifications.

I like to keep my workstation lightweight. I’m currently running Manjaro, and I have thus far refused to install Docker, for the sake of not having another daemon running in the background. Instead, I’m making use of tools out of the container organization, such as Buildah, Podman, and Skopeo, to build OCI compliant container images. Projects out of the container organization more closely follow this point of the Unix Philosophy:

Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new “features”.

OCI in Please

One of the recently added build definitions in the pleasings repo facilitates building OCI images using these tools! At time of this writing, it’s still pretty fresh and rough around the edges, but it is functional.

The OCI Build Definition in Please is made to be relatively compatible with docker commands, by checking for relevant OCI tools in your PATH, and falling back to docker. Since my machine doesn’t have docker installed, I don’t know if this fallback really works, so I can only vouch for my own use of the OCI tools.

Rootless Builds

Rootless builds just mean that the user using docker or buildah don’t need to be root in order to use the tool. This is valuable for several reasons, but for practicality’s sake, it means that we can build an manipulate an image in GitLab CI securely. For my project, I want to be able to use GitLab’s shared runners, which means I don’t have the luxery of using the shell executor, and it’s not necessarily safe to use Docker-in-Docker or to bind the socket. So none of the options GitLab recommends are ideal in this case.

Step by Step

Let’s make a new project!

Initialize please

1
plz init

This creates three files:

  1. .gitignore. Notice that it only has two lines. These identify two fantastic features of Please. First is plz-out. Please features strong build isolation, and one aspect of that isolation is containing all of your job’s dependencies and outputs in a directory that’s local to your project rather than shared directories elsewhere on your system. The other is .plzconfig.local. The Please config files are layered, so that you can have a base config file, and overwrite any part of it with profiles. The local profile is ignored so that you can have special settings for your particular environment without affecting other users.
  2. .plzconfig. The first config layer.
  3. pleasew. This is a wrapper script around Please which will install Please on your system if it doesn’t already exist. I’ve found this nice for writing documentation with examples or CI scripts, where I can’t assume that the user running commands has installed Please to their system already.

Write some code

We need some custom code to throw into the container. Let’s keep it simple and write a Go program without any dependencies.

1
2
mkdir demo
touch demo/main.go
1
2
3
4
5
6
7
8
// demo/main.go
package main

import "fmt"

func main() {
	fmt.Println("Hello, World")
}

You can verify that the code builds and runs using normal Go tooling.

1
go run demo/main.go
Hello, World

Now build and run this using Please. We first need to create the BUILD file to define the target.

1
touch demo/BUILD
1
2
3
4
5
6
# demo/BUILD
go_binary(
    name = "hello",
    srcs = ["main.go"],
    static = True,
)

Build and run the binary using Please.

1
plz run //demo:hello

On my machine, I get an error about not finding Go in my PATH.

370ms. 1 target failed:
    //demo:_hello#lib
go not found in path /home/andrew/.please:/usr/local/bin:/usr/bin:/bin

Please is using the standard Go tooling under the hood. However, another aspect of the build isolation is having a limited set of variables available. The goal is to limit a single system’s customizations and how those can affect reproducability on other systems. So in order to tell Please where my Go executable is, define it in the .plzconfig. This may be a good use of .plzconfig.local, if there will be multiple hands on the project.

1
2
[build]
path = /usr/local/go/bin:/usr/local/bin:/usr/bin:/bin

Try running the target again, and it should print the message!

Containerize

Like with Docker, it’s going to be the most clear and reproducable to define an image with a file. The typical naming convention for this file in the OCI world is a Containerfile.

1
touch demo/Containerfile

To first make sure this will work without Please’s help, we will build the Go program ourselves so that we have a binary to include in the image.

1
2
3
cd demo
go build main.go
cd ..

This should produce a file main. You can make sure it works by calling it:

1
./main
Hello, World

Now write the image definition in the Containerfile.

1
2
3
4
# demo/Containerfile
FROM scratch
COPY . .
CMD ["./main"]

Let’s also make sure, outside of Please, that this will work as we expect it to.

1
2
3
cd demo
buildah bud -f Containerfile -t demo .
podman run --rm localhost/demo
Hello, World

It worked! We won’t need the binary around for the next steps, so let’s remove it to avoid confusion with the Please build.

1
2
rm main
cd ..
Rootless Containers

It’s worth the effort to setup your system for projects from the containers organization to be able to run rootless. Besides the extra convenience of not having to run sudo to do anything with your containers, it’s more secure, since the containers you build and run have the same permissions as your user running them. There are shortcomings, too, but it’s worth figuring out in my opinion.

Not only is this good to figure out for your local system, but later in this post I talk about building images in Gitlab CI, which is, by default on the shared runners, rootless.

Please OCI

The Please team maintains two projects related to Please. The first is the actual Please repo, which is what we’ve been using so far. This is the core project, and it contains several builtin rules, including the rule we used previously for Go. There is another repo called Pleasings which holds rules for several other languages and runtimes for whose rules are still unstable or experimental. Here is where we find the rules for both Docker and OCI. The OCI rules are newer than Docker’s, and they appear to be modeled off of Docker’s rules.

We can include the Pleasings in our project with a simple command. I have the repo pinned here to the revision I used while writing this post, so things may have changed since then, and it’s worth updating the pin (at risk of there having been breaking changes since then).

1
plz init pleasings --revision f0f280474f6d87b5146924e35872927588818340

This creates a new BUILD file in the root of your project which pulls the pleasings repo. And now any build rules from the pleasings is available with a subinclude!

1
2
3
4
5
6
#BUILD
github_repo(
  name = "pleasings",
  repo = "thought-machine/pleasings",
  revision = "f0f280474f6d87b5146924e35872927588818340",
)

Now we can add a target for the OCI rules in our BUILD file.

1
2
3
4
5
6
7
8
#demo/BUILD
subinclude("///pleasings//oci")

container_image(
    name = "image",
    srcs = [":hello"],
    containerfile = "Containerfile",
)

Notice the subinclude(). We need this in order to make use of the pleasings repo we imported in the root BUILD file. There are other ways of doing this that are more syntactically nice. See the docs for more info.

When Please builds the image, it’s going to place the Containerfile and the output from the hello target into a shared directory as a part of the build isolation. So the Containerfile’s COPY . . will still have the correct files copied into the image.

The OCI Build Definition file actually creates several rules. They’re not very well documented, so you may need to read the source yourself to get a grip on what they are and what they do. They all follow a naming convention of ${target_name}_${rule}. Here’s a few useful rules that are built for your target:

  • image - plz build //demo:image. Build the image with Buildah
  • image_run - plz run //demo:image_run. Run the image with Podman
  • image_push - plz run //demo:image_push. Push the image to a registry with Skopeo

Try it out by verifying that you can build and push your image.

1
plz run //demo:image_run
Error: executable file `./main` not found in $PATH: No such file or directory:
OCI not found

Oops! go build main.go built an executable named main, but Please automatically names the executable after the target name. So rather than calling ./main in our CMD, we need to call ./hello, which is the name of our go_binary target.

1
2
3
4
# demo/Containerfile 
FROM scratch
COPY . .
CMD ["./hello"]

Try again

1
plz run //demo:image_run
signatures
Copying blob e98ef8d80fc7 [--------------------------------------] 0.0b / 0.0b
Copying config 296f8e9515 done
Writing manifest to image destination
Storing signatures
Hello, World

Viola!

Build in Gitlab CI

Following along with some GitOps best practices, I want to be able to build my project in CI. I’m a big fan of Gitlab CI, and make the most of the free tier that I’m able to. So I want to be able to build my project on their shared runners.

If you’ve ever tried to build a docker image in Gitlab CI while following best practices, you know the hassle this can become. The shared runners, as I understand them, use only the docker executor, which means that your jobs run inside of a container. It’s not impossible to build a docker image inside of docker, but it must be done in privileged mode. Gitlab’s documentation suggests a few ways to do so, but particularly while using shared runners, they either aren’t possible or aren’t necessarily the most secure methods. Their best solution on this page is to recommend using kaniko, which is a perfectly fine tool, but means you’re now in the realm of hosting your own runner, and not using the available shared runners.

Thankfully, the projects from the containers organization are able to run rootless, and don’t have a dependency on a daemon, like docker does. This is where the decision to build OCI images with dockerless tools is going to come in handy.

The containers organization has an image built for Buildah that happens to work out of the box in Gitlab CI! quay.io/buildah/stable

This image works well for doing a typical image build. In theory, if we weren’t using Please, we would be able to just use this image as-is for our CI job and do something like a multistage build to build the Go binary and copy it into the scratch image. For the sake of consistency, though, I think it makes more sense to use Please in CI, so that we can repeat the commands we use locally. That means that our job needs to install all of the dependencies that are needed to build the project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# .gitlab-ci.yml
build:
  image: quay.io/buildah/stable
  before_script:
    - |
      dnf -y update && dnf -y install \
        @development-tools \
        findutils \
        jq \
        glibc-static \
        golang \
        podman \
        skopeo \
        xz \
      && dnf clean all      
  script:
    - ./pleasew --plain_output --verbosity info run //demo:image_run

https://gitlab.com/awyeah/demo-plz-oci/-/jobs/1321810735

The Buildah Image
For the sake of simplicity, our CI job uses the quay.io/buildah/stable image and uses dnf to install the other dependencies the system needs to build our Go file. In my experience, dnf install takes a reasonably long time to update then install the packages we need. I’ll admit that Fedora is not in my wheelhouse, and I may be missing something that would make this process faster. However, for my own projects, I create another please target that builds an image based on quay.io/buildah/stable that pre-installs all of the dependencies for my project, and I use that in the CI job. Ideally, I might use another base image like Debian and install Buildah myself, but I had trouble doing the rootless builds, and just fell back to using the prebuilt image based on Fedora. If you’re able to build this project in an image based not on Fedora (or Alpine; I don’t really want to mess with musl-libc), drop me an email!

Gitlab Container Registry

The ability to plz run //demo:image_run in CI demonstrates that we can successfully build the image and it’s dependencies in CI, but running the image in CI like this is pretty useless. What we really want is to store the image in a registry for use on some deployment platform.

I’ll admit, I am not using the Gitlab Container Registry in my personal projects. I want to use it, particularly to stay within my goal of making use of as much managed free-tier resources as possible, but I’ve had more problems trying to stay within their odd restrictions than has been worth it for a reasonably large project with many images. The one that kills me is the maximum naming depth of three levels, but also, the output from Buildah and Skopeo while trying to push an image into their registry is basically useless, since it’s aparently not JSON formatted. Those tools seem to expect a JSON response, and fail parsing when it’s not JSON, so I only get a status code. I’ve had better luck using the Google Container Registry, and that’s that.

That said, I want to demonstrate using the Please OCI rules to push our image without having to spin up any extra infrastructure, so I’ve made it work for this demo project. Read more about the Gitlab Container Registry here.

There’s a few ways to define the registry that the image should push to. If you have several images that all go to the same registry, then you can define the default registry in .plzconfig. You can also define each image to have it’s own registry in the container_image build rule, or you can overwrite the registry while running the image_push command, like this:

1
REPO=registry.gitlab.com plz run //demo:image_push

There are other things that can be overwritten in this manner. See the source for what is available as environment variables like this.

For this project, it’s probably easiest to define the default registry in the config file.

1
2
3
;.plzconfig
[buildconfig]
default-docker-repo = registry.gitlab.com/awyeah/demo-plz-oci

Then our job needs to login to the registry and push the image after building, rather than running a container post-build.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# .gitlab-ci.yml
build:
  image: quay.io/buildah/stable
  before_script:
    - |
      dnf -y update && dnf -y install \
        @development-tools \
        findutils \
        jq \
        glibc-static \
        golang \
        podman \
        skopeo \
        xz \
      && dnf clean all      
    - buildah login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - ./pleasew --plain_output --verbosity info run //demo:image_push

AW YEAH! Here’s the job that succeeded as proof (if Gitlab keeps it around perpetually). And here’s the image it built (tag f73bd51ecf2b7d2ef842e0a1b4d7b3f1a74023f12860f11a399afcee614194e1). You can use it like this to verify:

1
podman run --rm registry.gitlab.com/awyeah/demo-plz-oci/demo/image:f73bd51ecf2b7d2ef842e0a1b4d7b3f1a74023f12860f11a399afcee614194e1

Recap

This step-by-step guide allowed us to build a hello world Go project using Please, package it into an OCI-compliant image using tools from the containers project, and to build and store them using Gitlab CI and Container Registry, all without using docker.