dockerkubernetesgitlabgitlab-cipaketo

Use Paketo.io / CloudNativeBuildpacks (CNB) in GitLab CI with Kubernetes executor & unprivileged Runners (without pack CLI & docker)


We want to use Paketo.io / CloudNativeBuildpacks (CNB) GitLab CI in the most simple way. Our GitLab setup uses an AWS EKS cluster with unprivileged GitLab CI Runners leveraging the Kubernetes executor. We also don't want to introduce security risks by using Docker in our builds. So we don't have our host’s /var/run/docker.sock exposed nor want to use docker:dind.

We found some guides on how to use Paketo with GitLab CI like this https://tanzu.vmware.com/developer/guides/gitlab-ci-cd-cnb/ . But as described beneath the headline Use Cloud Native Buildpacks with GitLab in GitLab Build Job WITHOUT Using the GitLab Build Template, the approach relies on Docker and pack CLI. We tried to resemble this in our .gitlab-ci.yml which looks like this:

image: docker:20.10.9

stages:
  - build

before_script:
  - |
    echo "install pack CLI (see https://buildpacks.io/docs/tools/pack/)"
    apk add --no-cache curl
    (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.21.1/pack-v0.21.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)

build-image:
  stage: build
  script:
    - pack --version
    - >
      pack build $REGISTRY_GROUP_PROJECT/$CI_PROJECT_NAME:latest
      --builder paketobuildpacks/builder:base
      --path . 

But as outlined our setup does not support docker and we end up with the following error inside our logs:

...
$ echo "install pack CLI (see https://buildpacks.io/docs/tools/pack/)" # collapsed multi-line command
install pack CLI (see https://buildpacks.io/docs/tools/pack/)
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.14/community/x86_64/APKINDEX.tar.gz
(1/4) Installing brotli-libs (1.0.9-r5)
(2/4) Installing nghttp2-libs (1.43.0-r0)
(3/4) Installing libcurl (7.79.1-r0)
(4/4) Installing curl (7.79.1-r0)
Executing busybox-1.33.1-r3.trigger
OK: 12 MiB in 26 packages
pack
$ pack --version
0.21.1+git-e09e397.build-2823
$ pack build $REGISTRY_GROUP_PROJECT/$CI_PROJECT_NAME:latest --builder paketobuildpacks/builder:base --path .
ERROR: failed to build: failed to fetch builder image 'index.docker.io/paketobuildpacks/builder:base': Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Cleaning up project directory and file based variables 00:01
ERROR: Job failed: command terminated with exit code 1

Any idea on how to use Paketo Buildpacks with GitLab CI without having Docker present inside our GitLab Kubernetes runners (which seems to be kind of a best practice)? We also don't want our setup to become to complex - e.g. by adding kpack.


Solution

  • TLDR;

    Use the Buildpack's lifecycle directly inside your .gitlab-ci.yml here's a fully working example):

    image: paketobuildpacks/builder
    
    stages:
      - build
    
    # We somehow need to access GitLab Container Registry with the Paketo lifecycle
    # So we simply create ~/.docker/config.json as stated in https://stackoverflow.com/a/41710291/4964553
    before_script:
      - mkdir ~/.docker
      - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_JOB_TOKEN\"}}}" >> ~/.docker/config.json
    
    build-image:
      stage: build
      script:
        - /cnb/lifecycle/creator -app=. $CI_REGISTRY_IMAGE:latest
    

    The details: "using the lifecycle directly"

    There are ongoing discussions about this topic. Especially have a look into https://github.com/buildpacks/pack/issues/564 and https://github.com/buildpacks/pack/issues/413#issuecomment-565165832. As stated there:

    If you're looking to build images in CI (not locally), I'd encourage you to use the lifecycle directly for that, so that you don't need Docker. Here's an example:

    The link to the example is broken, but it refers to the Tekton implementation on how to use buildpacks in a Kubernetes environment. Here we can get a first glue about what Stephen Levine referred to as "to use the lifecycle directly". Inside it the crucial point is the usage of command: ["/cnb/lifecycle/creator"]. So this is the lifecycle everyone is talking about! And there's good documentaion about this command that could be found in this CNB RFC.

    Choosing a good image: paketobuildpacks/builder:base

    So how to develop a working .gitlab-ci.yml? Let's start simple. Digging into the Tekton implementation you'll see that the lifecycle command is executed inside an environment defined in BUILDER_IMAGE, which itself is documented as The image on which builds will run (must include lifecycle and compatible buildpacks). That sound's familiar! Can't we simply pick the builder image paketobuildpacks/builder:base from our pack CLI command? Let's try this locally on our workstation before commiting to much noise into our GitLab. Choose a project you want to build (I created a example Spring Boot app if you'd like at gitlab.com/jonashackt/microservice-api-spring-boot you can clone) and run:

    docker run --rm -it -v "$PWD":/usr/src/app -w /usr/src/app paketobuildpacks/builder bash
    

    Now inside the paketobuildpacks/builder image powered container try to run the Paketo lifecycle directly with:

    /cnb/lifecycle/creator -app=. microservice-api-spring-boot:latest
    

    I only used the -app parameter of the many possible parameters for the creator command, since most of them have quite good defaults. But as the default app directory path is not the default /workspace - but the current directory, I configured it. Also we need to define an <image-name> at the end, which will simply be used as the resulting container image name.

    The first .gitlab-ci.yml

    Both commands did work at my local workstation, so let's finally create a .gitlab-ci.yml using this approach (here's a fully working example .gitlab-ci.yml):

    image: paketobuildpacks/builder
    
    stages:
      - build
    
    build-image:
      stage: build
      script:
        - /cnb/lifecycle/creator -app=. $CI_REGISTRY_IMAGE:latest
    

    docker login without docker

    As we don't have docker available inside our Kubernetes Runners, we can't login into GitLab Container Registry as described in the docs. So the following error occured to me using this first approach:

    ===> ANALYZING
    ERROR: failed to get previous image: connect to repo store "gitlab.yourcompanyhere.cloud:4567/yourgroup/microservice-api-spring-boot:latest": GET https://gitlab.yourcompanyhere.cloud/jwt/auth?scope=repository%3Ayourgroup%2Fmicroservice-api-spring-boot%3Apull&service=container_registry: DENIED: access forbidden
    Cleaning up project directory and file based variables 00:01
    ERROR: Job failed: command terminated with exit code 1
    

    Using the approach described in this so answer fixed the problem. We need to create a ~/.docker/config.json containing the GitLab Container Registry login information - and then the Paketo build will pick them up, as stated in the docs:

    If CNB_REGISTRY_AUTH is unset and a docker config.json file is present, the lifecycle SHOULD use the contents of this file to authenticate with any matching registry.

    Inside our .gitlab-ci.yml this could look like:

    # We somehow need to access GitLab Container Registry with the Paketo lifecycle
    # So we simply create ~/.docker/config.json as stated in https://stackoverflow.com/a/41710291/4964553
    before_script:
      - mkdir ~/.docker
      - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_JOB_TOKEN\"}}}" >> ~/.docker/config.json
    

    Our final .gitlab-ci.yml

    As we're using the image: paketobuildpacks/builder at the top of our .gitlab-ci.yml, we can now leverage the lifecycle directly. Which is what we wanted to do in the first place. Only remember to use the correct GitLab CI variables to describe your <image-name> like this:

    /cnb/lifecycle/creator -app=. $CI_REGISTRY_IMAGE:latest
    

    Otherwise the Buildpack process analyser step will break and it finally won't get pushed to the GitLab Container Registry. So finally our .gitlab-ci.yml looks like this (here's the fully working example):

    image: paketobuildpacks/builder
    
    stages:
      - build
    
    # We somehow need to access GitLab Container Registry with the Paketo lifecycle
    # So we simply create ~/.docker/config.json as stated in https://stackoverflow.com/a/41710291/4964553
    before_script:
      - mkdir ~/.docker
      - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_JOB_TOKEN\"}}}" >> ~/.docker/config.json
    
    build-image:
      stage: build
      script:
        - /cnb/lifecycle/creator -app=. $CI_REGISTRY_IMAGE:latest
    

    Our builds should now run successfully using Paketo/Buildpacks without pack CLI and Docker:

    enter image description here

    See the full log of the example project here.