UiB GitLab as Container Image Registry

Container images are published on servers that are called “container registries”. This terminology originated with the docker command and its ecosystem. Docker Hub is still the default registry that will be used when you reference container images with just a image name. GitLab provides an integrated container registry to store container images for each GitLab project. You can read more about this from GitLabs own documentation.

At UiB we provide two instances of GitLab; git.app.uib.no which provide access for everyone with a Feide Login (even for people outside of UiB) and itgit.app.uib.no which is private for the IT-department. The corresponding registry servers have address git.app.uib.no:4567 and itgit.app.uib.no:4567.

Example of a .gitlab-ci.yml file:

image: git.app.uib.no:4567/itpublic/dockerimages/alpine-docker/docker:cli
stages:
  - build
  - promote
.docker_login: &docker_login
    - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY
.build_container_image:
  script:
    - *docker_login
    - docker pull $CI_REGISTRY_IMAGE/$image_name:latest || true
    - docker build --file $dockerfile --cache-from $CI_REGISTRY_IMAGE/$image_name:latest
      --tag $CI_REGISTRY_IMAGE/$image_name:$CI_COMMIT_SHORT_SHA
      --tag $CI_REGISTRY_IMAGE/$image_name:latest
      --tag $CI_REGISTRY_IMAGE/$image_name:$version
      .
    - docker push $CI_REGISTRY_IMAGE/$image_name:$CI_COMMIT_SHORT_SHA
    - docker push $CI_REGISTRY_IMAGE/$image_name:latest
    - docker push $CI_REGISTRY_IMAGE/$image_name:$version
.promote:
  script:
    - *docker_login
    - docker pull $CI_REGISTRY_IMAGE/$image_name:$existing_tag
    - docker tag $CI_REGISTRY_IMAGE/$image_name:$existing_tag $CI_REGISTRY_IMAGE/$image_name:$new_tag
    - docker push $CI_REGISTRY_IMAGE/$image_name:$new_tag
build-test:
  stage: build
  variables:
    image_name: app-image-name
    version: test
    dockerfile: Dockerfile
  extends:
    - .build_container_image
promote-prod:
  stage: promote
  variables:
    image_name: app-image-name
    existing_tag: test
    new_tag: prod
  extends:
    - .promote
  when: manual

Authentication for protected and private projects

For workloads that only need to pull private images, a project deploy token is usually the simplest option. It is not tied to a user account and does not expire by default. Give it only the read_registry scope, and use its username and token value in the Docker registry secret below. You can set an expiry date when creating the token. Without one, it remains valid until revoked, so store it securely and rotate or revoke it when needed. If images come from several projects in one GitLab group, a group deploy token can cover them, and grants access across that group.

Access tokens is an alternative and can be generated at the level of an individual GitLab project, for a group or for GitLab System user that you add as a member to your gitrepo. A group token authenticates you for all projects that are part of that group.

Access tokens must have an expiry date. If automatic rotation is available in your GitLab instance, you can use it to reduce manual renewal work. GitLab does not automatically update the token stored in RAIL. If the token expires or is rotated without updating the secret, newly started Pods may fail to pull images. If automatic rotation is unavailable, renew the token and update the RAIL secret before it expires.

Take a look at https://git.app.uib.no/platform_public/rail/gitops/adm-it-ark-mirror/-/blob/main/base/git-pull-secrets/README.md for a guide to set up automatic rotation of token secrets.

To create an access token in an individual GitLab project - navigating to “Settings > Access Tokens” menu and press the “Add new token” button. In the dialog that shows give the new token a sensible name and select role to be at least “Reporter” (anything but “Guest” will do) and select the “read_registry” scope. Then create the token.

Save this token to RAIL as a docker-registry secret. This command will do this:

kubectl create secret docker-registry <secret-name> \
  --docker-server=https://git.app.uib.no:4567 \
  --docker-username=<token-name> \
  --docker-password=<token-value>

Replace with your values in the slots for <secret-name>, <token-name> and <token-value>. GitLab does not really care what the docker-username is set as, but it is customary to document the token name here. The Kubernetes documentation says that the <secret-name> should follow the rules for valid DNS host names. We suggest a name like gitapp-projectname-registry.

If you instead want to use and store this token as part of your GitOps repo, run the command:

kubectl create secret docker-registry <secret-name> \
  --docker-server=https://git.app.uib.no:4567 \
  --docker-username=<token-name> \
  --docker-password=<token-value> \
  --dry-run=client \
  -o yaml > gitapp-projectname-registry.yaml

This new yaml file needs to be encrypted before being committed to your GitOps repo - see Protect secrets with SOPS:

sops encrypt gitapp-projectname-registry.yaml --in-place

In the Pod spec you register that this secret is to be used for this server with a block like this

imagePullSecrets:
- name: <secret-name>
containers:
- name: <container-name>
  image: git.app.uib.no:4567/<project>/<image-name>:<image-tag>
  ...

That’s it!

If you look into the secret created with a command like kubectl get secret <secret-name> -o yaml you will see that it looks like this:

apiVersion: v1
kind: Secret
metadata:
  name: <secret-name>
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2dpdC5hcHAudWliLm5vOjQ1NjciOnsidXNlcm5hbWUiOiJnaXRhcHAtcHJvamVjdG5hbWUtcmVnaXN0cnkiLCJwYXNzd29yZCI6InRva2VuLXZhbHVlIiwiYXV0aCI6IloybDBZWEJ3TFhCeWIycGxZM1J1WVcxbExYSmxaMmx6ZEhKNU9uUnZhMlZ1TFhaaGJIVmwifX19

Copy the base64 encoded string by running echo eyJhdXRocyI6eyJ... | base64 -d | jq and you will get this:

{
  "auths": {
    "https://git.app.uib.no:4567": {
      "username": "gitapp-projectname-registry",
      "password": "token-value",
      "auth": "Z2l0YXBwLXByb2plY3RuYW1lLXJlZ2lzdHJ5OnRva2VuLXZhbHVl"
    }
  }
}

As you can see the type of this secret isn’t really docker-registry but actually dockerconfigjson. It’s kind of strange why these don’t match. Since this is a secret the object below .dockerconfigjson is actually Base64 encoded as well. Anyway, this is the format that imagePullSecrets expects so we just go with it.