Un registre statique Docker avec Garage

Publié le 06/04/2023

Dans ce petite article, je vais vous montrer rapidement comment monter votre registre Docker avec Garage seulement. En effet, un registre Docker n’est rien d’autre qu’une spécification par dessus HTTP, et il apparait que Garage supporte pile poil le bon sous ensemble pour la distribution (c’est à dire le téléchargement). Reste à réaliser l’envoi à la main, et c’est ce que nous allons détailler ici ! Et pour faire les choses bien, on va prendre l’exemple d’une image multi-arch, qui est un poil plus complexe.

L’idée, c’est qu’à la fin de ce tuto, vous puissiez faire quelque chose comme ça, mais avec votre propre domaine !

docker run --rm -it quentin.dufour.io/garage:v0.8.2 /garage help

Inspecter un peu le registre Docker

Pour requêter le registre docker, on a besoin d’un token même en tant qu’utilisateur anonyme. Sinon on a une 401 :

$ curl -i 'https://registry.docker.com/v2/dxflrs/garage/manifests/v0.8.2'
HTTP/1.1 401 Unauthorized
content-type: application/json
docker-distribution-api-version: registry/2.0
www-authenticate: Bearer realm="https://auth.ipv6.docker.com/token",service="registry.docker.io",scope="repository:dxflrs/garage:pull"
date: Thu, 06 Apr 2023 14:55:10 GMT
content-length: 156
strict-transport-security: max-age=31536000
docker-ratelimit-source: 2a01:e0a:28f:5e60::

{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Class":"","Name":"dxflrs/garage","Action":"pull"}]}]}

La 401 nous donne toutes les informations pour récupérer notre token :

 curl -vvv 'https://auth.ipv6.docker.com/token?service=registry.docker.io&scope=repository:dxflrs/garage:pull'|jq

On peut ensuite construire notre fichier d’en-tête headers.txt qui contiendra l’authorisation et l’information qu’on est un client Docker moderne :

Accept: application/vnd.docker.distribution.manifest.list.v2+json
Authorization: Bearer eyJh...

Et puis on peut l’utiliser avec curl pour récupérer le manifest multi arch :

curl -H @headers.txt 'https://registry.docker.com/v2/dxflrs/garage/manifests/v0.8.2'

On y retrouve alors la déclaration des 4 architectures supportées :

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 428,
      "digest": "sha256:236604ea7a441f907d52129d9490fe96b64ef2efd8d4b1c1c50ef8dbae361a8e",
      "platform": {
        "architecture": "arm64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 428,
      "digest": "sha256:73a20980fd232dc7acd51d21df6c7c9964bc7c5fbcfdc098b95cfd221bf67bf6",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 428,
      "digest": "sha256:47df19e0c6333356e503258e6301c3a91848644667a0b7de4162e6841e89769a",
      "platform": {
        "architecture": "386",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 428,
      "digest": "sha256:12ec13fe92959249c52c46e97754333267ceeea22978434737978a135b7185ce",
      "platform": {
        "architecture": "arm",
        "os": "linux"
      }
    }
  ]
}

Duquel ensuite on peut inspecter l’image d’une plateforme précise, ici arm64 :

curl -H @headers.txt 'https://registry.docker.com/v2/dxflrs/garage/manifests/sha256:236604ea7a441f907d52129d9490fe96b64ef2efd8d4b1c1c50ef8dbae361a8e'|jq

Duquel enfin on récupère les informations de configuration et des différents layers :

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 459,
    "digest": "sha256:258bd4fedb7a0bd5cffd4238777b293d6c5907e5eeaad0174bae3003041c309b"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 19845930,
      "digest": "sha256:d2d5e172a714fc48876a6657e7b4b0c3baa4c1ea42e92c687bdb9d86b8dd43c4"
    }
  ]
}

En dernier lieu, on peut récupérer les différents blobs déclarés dans le manifest. Pour la configuration, on a

curl -L -H @/tmp/d.txt 'https://registry.docker.com/v2/dxflrs/garage/blobs/sha256:258bd4fedb7a0bd5cffd4238777b293d6c5907e5eeaad0174bae3003041c309b'|jq

Ce qui nous donne les informations sur l’image :

{
  "architecture": "arm64",
  "created": "2023-03-13T20:27:37.477146404Z",
  "history": [
    {
      "author": "kaniko",
      "created": "0001-01-01T00:00:00Z",
      "created_by": "COPY result-bin/bin/garage /"
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:01ff2f334b600faf0e0fc53e7fa19b4f44b1c340cd5f39ec49393b339f6e945f"
    ]
  },
  "config": {
    "Cmd": [
      "/garage",
      "server"
    ],
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "RUST_BACKTRACE=1",
      "RUST_LOG=garage=info"
    ]
  }
}

Pour le layer, étant une archive tar.gzip, on passe la sortie de curl à tar :

$  curl -L -H @/tmp/d.txt 'https://registry.docker.com/v2/dxflrs/garage/blobs/sha256:d2d5e172a714fc48876a6657e7b4b0c3baa4c1ea42e92c687bdb9d86b8dd43c4'|tar -ztvf -
tar: Suppression de « / » au début des noms des membres
drwxr-xr-x 0/0               0 2023-03-13 21:27 /
-r-xr-xr-x 0/0        50846992 2023-03-13 21:27 garage
100 18.9M  100 18.9M    0     0  15.5M      0  0:00:01  0:00:01 --:--:-- 33.1M

Et nous voilà arriver au bout de notre exploration de l’API distribution de la Open Container Initiative définie par Docker à l’origine. Par la suite, on va pas s’amuser à récupérer tout ces fichiers à la main, mais demander à skopeo de le faire pour nous. Mais avant tout il faut…

Déclarer un bucket comme registre

Rien de particulier ici, on va supposer que vous avez un bucket Garage déjà exposé comme site web. Dans ce billet, je vais utiliser directement le bucket de mon site web comme registre docker. Pour que ce dernier soit reconnu comme registre, il est de bon ton de renvoyer un petit OK sur le chemin /v2/ :

echo ok > /tmp/v2
aws s3 cp /tmp/v2 s3://quentin.dufour.io/v2/index.html

Récupérer une image depuis le Docker Hub

On va récupérer une image multi-arch de Garage depuis le Docker Hub pour se simplifier la vie dans un premier temps. Mais à la fin, on va build à la main notre image multi-arch depuis Nix, et sans jamais utiliser un daemon docker. Pratique !

Donc pour récupérer notre image multiarch, on va utiliser skopeo :

mkdir -p /tmp/garage-img-multi
skopeo --insecure-policy copy \
  --all --format v2s2 --dest-compress \
  docker://docker.io/dxflrs/garage:v0.8.2 \
  dir:/tmp/garage-img-multi
  

Et voilà, vous avez votre image dans /tmp/garage-img-multi. Si vous avez bien suivi le tutoriel, ce sont les mêmes fichiers que vu lors de notre inspection du registre Docker avec curl.

Copier l’image sur S3

Maintenant on va reconstituer cette image dans notre registre à la main. On copie d’abord le manifest multi-arch :

cd /tmp/garae-img-multi
aws s3 cp --content-type application/vnd.docker.distribution.manifest.list.v2+json \
  manifest.json \
  s3://quentin.dufour.io/v2/garage/manifests/v0.8.2

Il faut aussi que le manifest soit accessible depuis son hash sha256, pour ça il faut le calculer et ensuite l’envoyer de nouveau :

$ sha256 manifest.json
SHA256 (manifest.json) = 91af689013dd80d2ef0f4ff75038bc738b3193a11e201530d6da0fa833f55cbb
$ aws s3 cp --content-type application/vnd.docker.distribution.manifest.list.v2+json \
  manifest.json \
  s3://quentin.dufour.io/v2/garage/manifests/sha256:91af689013dd80d2ef0f4ff75038bc738b3193a11e201530d6da0fa833f55cbb

Ensuite on copie les manifestes des images des différentes architectures (ici linux/arm, linux/arm64, linux/amd64, et linux/386) :

# manifest arm
aws s3 cp --content-type application/vnd.docker.distribution.manifest.v2+json \
  12ec13fe92959249c52c46e97754333267ceeea22978434737978a135b7185ce.manifest.json \
  s3://quentin.dufour.io/v2/garage/manifests/sha256:12ec13fe92959249c52c46e97754333267ceeea22978434737978a135b7185ce

# manifest arm64
aws s3 cp --content-type application/vnd.docker.distribution.manifest.v2+json \
  236604ea7a441f907d52129d9490fe96b64ef2efd8d4b1c1c50ef8dbae361a8e.manifest.json \
  s3://quentin.dufour.io/v2/garage/manifests/sha256:236604ea7a441f907d52129d9490fe96b64ef2efd8d4b1c1c50ef8dbae361a8e

# manifest 386
aws s3 cp --content-type application/vnd.docker.distribution.manifest.v2+json \
  47df19e0c6333356e503258e6301c3a91848644667a0b7de4162e6841e89769a.manifest.json \
  s3://quentin.dufour.io/v2/garage/manifests/sha256:47df19e0c6333356e503258e6301c3a91848644667a0b7de4162e6841e89769a

# manifest amd64
aws s3 cp --content-type application/vnd.docker.distribution.manifest.v2+json \
  73a20980fd232dc7acd51d21df6c7c9964bc7c5fbcfdc098b95cfd221bf67bf6.manifest.json \
  s3://quentin.dufour.io/v2/garage/manifests/sha256:73a20980fd232dc7acd51d21df6c7c9964bc7c5fbcfdc098b95cfd221bf67bf6

Enfin, on copie les blobs, qui contiennent par exemple les layers de chaque images. Plutôt que de copier à la main, cette fois-ci je fais une boucle :

for i in $(ls | grep -P '^[a-f0-9]+$'); do 
  aws s3 cp $i "s3://quentin.dufour.io/v2/garage/blobs/sha256:$i"
done

Et voilà !

Tester notre registre

On peut d’abord essayer avec skopeo la même commande qu’on a exécuté sur le Docker Hub pour dump toutes nos images :

skopeo --insecure-policy copy \
  --all --format v2s2 --dest-compress 
  docker://quentin.dufour.io/garage:v0.8.2 
  dir:/tmp/garage-s3-registry

Et puis, plus simplement, avec Docker directement :

docker pull quentin.dufour.io/garage:v0.8.2

Et pourquoi pas tenter même de lancer notre conteneur ?

$ sudo docker run --rm -it quentin.dufour.io/garage:v0.8.2 /garage help
garage v0.8.2 [features: k2v, sled, lmdb, sqlite, consul-discovery, kubernetes-discovery, metrics, telemetry-otlp, bundled-libs]
S3-compatible object store for self-hosted geo-distributed deployments
...

Nous y voilà : nous savons créer un registre Docker statique ! Et maintenant, pourquoi ne pas construire nos images directement avec Nix ?

Construire l’image nous-même avec Nix

On va utiliser pkgs.dockerTools.buildImage pour générer une archive docker qui va ressembler à ça :

$ tar -tvf result
dr-xr-xr-x root/root         0 1980-01-01 01:00 ./
dr-xr-xr-x root/root         0 1980-01-01 01:00 4bc17b9fc1404e9543364c02ec354faee7ca6e004dc829994a61fd42935e00aa/
-r--r--r-- root/root         3 1980-01-01 01:00 4bc17b9fc1404e9543364c02ec354faee7ca6e004dc829994a61fd42935e00aa/VERSION
-r--r--r-- root/root       396 1980-01-01 01:00 4bc17b9fc1404e9543364c02ec354faee7ca6e004dc829994a61fd42935e00aa/json
-r--r--r-- root/root   9932800 1980-01-01 01:00 4bc17b9fc1404e9543364c02ec354faee7ca6e004dc829994a61fd42935e00aa/layer.tar
-r--r--r-- root/root       447 1980-01-01 01:00 e9eaf28bc5306e0390c8e3d7ec7f072933c67a5e5dadcd4b4e699cdcbee20d00.json
-r--r--r-- root/root       286 1980-01-01 01:00 manifest.json
-r--r--r-- root/root       135 1980-01-01 01:00 repositories

De cette archive, on va pas se casser la tête dans un premier temps, on va simplement demander à skopeo de nous la convertir dans le format que l’on connait :

skopeo --insecure-policy copy docker-archive:result dir:/tmp/albatros-img

Ensuite on va la copier simplement comme vu précédemment :

sha256sum manifest.json # 840b4265d58d0358a3c4183ba0e39e7bb4c3dfb78a50cac7532476ab25666def
aws s3 cp --content-type application/vnd.docker.distribution.manifest.v2+json manifest.json s3://quentin.dufour.io/v2/albatros/manifests/sha256:840b4265d58d0358a3c4183ba0e39e7bb4c3dfb78a50cac7532476ab25666def
aws s3 cp --content-type application/vnd.docker.distribution.manifest.v2+json  manifest.json s3://quentin.dufour.io/v2/albatros/manifests/v0.9
aws s3 cp ./83695be784e20268eafd08d35b47f50d689d831ae4a047750a4ed2c0a29debc7 s3://quentin.dufour.io/v2/albatros/blobs/sha256:83695be784e20268eafd08d35b47f50d689d831ae4a047750a4ed2c0a29debc7
aws s3 cp ./e9eaf28bc5306e0390c8e3d7ec7f072933c67a5e5dadcd4b4e699cdcbee20d00 s3://quentin.dufour.io/v2/albatros/blobs/sha256:e9eaf28bc5306e0390c8e3d7ec7f072933c67a5e5dadcd4b4e699cdcbee20d00

Et voilà, on peut lancer notre binaire maintenant :

$ docker run --rm -it quentin.dufour.io/albatros:v0.9
Unable to find image 'quentin.dufour.io/albatros:v0.9' locally
v0.9: Pulling from albatros
Digest: sha256:840b4265d58d0358a3c4183ba0e39e7bb4c3dfb78a50cac7532476ab25666def
Status: Downloaded newer image for quentin.dufour.io/albatros:v0.9
2023/04/06 16:31:42 unable to parse config, error: env: required environment variable "ALBATROS_URL" is not set

Alors là on a envoyé une image simple et non une image multi-arch, mais c’est tout à fait possible à faire, l’exercice est laissé à la lectrice ou au lecture que vous êtes pour le moment. Bonne chance !