17 min de lecture

Objectifs d'apprentissage

À la fin de ce module, vous serez en mesure de:

  • Provisionner un Container Registry IONOS CLOUD et ses jetons d'accès avec Terraform en utilisant les ressources `ionoscloud_container_registry` et `ionoscloud_container_registry_token`
  • Authentifier le CLI Docker vers un registre à l'aide d'un `docker login` à portée de jeton et pousser des images de production multi-étapes
  • Construire un pipeline d'images reproductible qui étiquette les images avec le SHA Git et les pousse vers le point de terminaison du registre
  • Intégrer les identifiants du registre dans GitHub Actions et GitLab CI en tant que secrets et exécuter `docker login` dans un pipeline
  • Configurer la collecte des ordures et l'analyse des vulnérabilités, et travailler dans le cadre du modèle de jetons par registre du registre

Unité 3.1 : Container Registry et pipelines d'images

Introduction

Vous êtes sur le point de containeriser le service API et le front-end web de TaskBoard, et vous avez besoin d'un registre pour y pousser les images avant que Managed Kubernetes ne puisse les récupérer. IONOS CLOUD Container Registry vous fournit un ou plusieurs registres dédiés et authentifiés, qui prennent en charge la Docker Registry HTTP API V2 et acceptent tout artefact conforme à la norme OCI. Tout ce que vous y effectuez, à savoir le provisionnement, la création de jetons, la connexion, la poussée d'images et la collecte des orphelins, est piloté par Terraform, l'API REST et l'outil de ligne de commande Docker.

Cette unité commence au niveau du code. Vous allez provisionner un registre en tant qu'Infrastructure as Code, générer un jeton à portée limitée, authentifier Docker et construire le pipeline build-tag-push sur lequel le reste du Module 3 dépend. Deux contraintes spécifiques à IONOS CLOUD façonnent chaque exemple présenté ici : l'authentification est exclusivement basée sur des jetons, sans contrôle d'accès basé sur les rôles ni listes de contrôle d'accès par dépôt, et le nom d'hôte du registre n'existe pas tant que le registre n'atteint pas l'état Running. Ces deux points vous poseront des problèmes en CI si vous les ignorez, ils sont donc intégrés aux modèles de code ci-dessous.

1. Provisionnement du registre avec Terraform

Le registre est une ressource Terraform de premier ordre. Vous déclarez un nom et un emplacement, appliquez la configuration, puis attendez la fin du provisionnement asynchrone avant que le nom d'hôte ne soit alloué. Les champs name et location sont immuables après la création, ce qui en fait une décision irréversible pour chaque registre.

Le nom du registre doit comporter au maximum 63 caractères. Si vous tentez de créer un registre avec un nom déjà utilisé sur la plateforme, l'API renvoie HTTP 409 Conflict avec le message already exists: name is not available. Considérez le nom comme un identifiant global, et non comme un identifiant par compte, et préfixez-le avec votre projet pour éviter les collisions.

1.1 La ressource ionoscloud_container_registry

L'emplacement doit être un emplacement pris en charge par Container Registry. Container Registry est disponible dans de/fra (DE/FRA), donc votre valeur location est de/fra.

terraform {
  required_providers {
    ionoscloud = {
      source  = "ionos-cloud/ionoscloud"
      version = ">= 6.0.0"
    }
  }
}

provider "ionoscloud" {
  # token read from IONOS_TOKEN env var
}

resource "ionoscloud_container_registry" "taskboard" {
  name     = "taskboard-prod"
  location = "de/fra"

  garbage_collection_schedule {
    days = ["Saturday", "Sunday"]
    time = "19:30:00+00:00"
  }
}

output "registry_hostname" {
  value = ionoscloud_container_registry.taskboard.hostname
}

La sortie hostname suit le modèle {registry-name}.cr.{location-with-dash}.ionos.com. Elle reste vide jusqu'à ce que le registre atteigne Running, de sorte que toute étape en aval qui la lit doit s'exécuter après la complétion totale de apply. Le registre ne possède que deux états de cycle de vie, New et Running, et seul Running est utilisable.

1.2 Provisionnement via l'API REST

Si vous effectuez le provisionnement en dehors de Terraform, le registre est créé par un POST vers le point d'accès des registres. Les mêmes propriétés garbageCollectionSchedule, location et name s'appliquent.

curl --location \
  --request POST 'https://api.ionos.com/containerregistries/registries' \
  --header "Authorization: Bearer ${IONOS_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "properties": {
      "garbageCollectionSchedule": {
        "days": ["Saturday", "Sunday"],
        "time": "19:30:00+00:00"
      },
      "location": "de/fra",
      "name": "taskboard-prod"
    }
  }'

La réponse 201 n'inclut pas d'hôte, car celui-ci n'est pas encore alloué. Interrogez GET /containerregistries/registries/{registryId} jusqu'à ce que l'état soit Running avant de lire le nom d'hôte. Les appels de liste sont paginés par jeton : passez limit en tant que paramètre de requête et suivez nextPageToken pour parcourir les grandes collections.

2. Jetons et authentification

L'authentification repose uniquement sur des jetons docker login. Il n'y a pas de RBAC, pas de dépôts par équipe, et pas d'accès anonyme ou aux dépôts publics. Un jeton est l'unité d'accès, et vous le limitez par registre ou par chemin de dépôt à l'aide d'un ensemble d'actions autorisées.

2.1 Création d'un jeton avec Terraform

Un jeton possède un nom, un statut, une date d'expiration facultative et un ou plusieurs périmètres. Chaque périmètre a un type Registry ou Repository, un chemin et une liste d'actions tirée de pull, push et delete. Le nom du jeton doit comporter de 3 à 63 caractères, commencer par une lettre de a à z, se terminer par un caractère alphanumérique et ne contenir que des caractères alphanumériques et des tirets. Il est immuable après sa création.

resource "ionoscloud_container_registry_token" "ci_push" {
  container_registry_id = ionoscloud_container_registry.taskboard.id
  name                  = "ci-push"
  status                = "enabled"

  # minimum expiry is 1 hour in the future; on expiry the token is DELETED
  expiry_date = "2027-01-01T00:00:00Z"

  scopes {
    type    = "Repository"
    name    = "taskboard/*"
    actions = ["push", "pull"]
  }
}

output "ci_push_password" {
  value     = ionoscloud_container_registry_token.ci_push.password
  sensitive = true
}

Deux comportements sont essentiels pour l'automatisation. Le mot de passe du jeton est affiché une seule fois, il convient donc de le récupérer immédiatement depuis l'état Terraform ou la réponse de l'API, puis de le stocker dans votre gestionnaire de secrets. Lorsque la date d'expiration est atteinte, le jeton est supprimé, et non désactivé. Par conséquent, une tâche de rotation doit créer le jeton de remplacement avant l'expiration de l'ancien, sous peine de perte d'accès de votre pipeline en cours de déploiement.

2.2 Création d'un jeton via l'API et connexion

Le point d'entrée du jeton est POST /containerregistries/registries/{registryId}/tokens. Les jetons existent en deux types : un jeton d'accès permanent au registre et un jeton temporaire limité par expiryDate. L'expiration minimale est une heure dans le futur.

curl --location \
  --request POST "https://api.ionos.com/containerregistries/registries/${REGISTRY_ID}/tokens" \
  --header "Authorization: Bearer ${IONOS_TOKEN}" \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "properties": {
      "name": "ci-push",
      "status": "enabled",
      "scopes": [
        { "type": "Repository", "name": "taskboard/*", "actions": ["push", "pull"] }
      ]
    }
  }'

La réponse contient les identifiants une seule fois. Utilisez le nom du jeton comme nom d'utilisateur et le mot de passe du jeton comme mot de passe pour docker login avec le nom d'hôte du registre.

echo "${CR_TOKEN_PASSWORD}" | docker login taskboard-prod.cr.de-fra.ionos.com \
  --username ci-push \
  --password-stdin

Un jeton se trouve dans l'un des trois états suivants : enabled, disabled ou deleted. Désactivez un jeton pour révoquer immédiatement l'accès sans le supprimer ; supprimez-le lorsqu'il n'est plus nécessaire.

2.3 Choisir la méthode de création et d'utilisation des identifiants

Le tableau suivant compare les chemins d'accès pour les identifiants du registre afin que vous puissiez choisir le bon pour chaque flux de travail.

Approche Idéal pour Outils Durée de vie de l'identifiant
Ressource de jeton Terraform Identités de service CI provisionnées en tant que code HCL Jusqu'à expiry_date (supprimé à l'expiration)
Création de jeton API Tâches de rotation scriptées curl/HTTP Jusqu'à expiry_date ou suppression manuelle
docker login Pousse/tirage de développement local Docker CLI Session, adossée à un jeton

Étant donné que les plages d'application sont définies par registre ou par chemin de dépôt et qu'il n'y a pas de RBAC, modélisez un jeton par pipeline CI et un par développeur plutôt que de partager un seul jeton aux permissions étendues. Limitez les jetons CI à push et à pull sur les chemins de dépôt qu'ils gèrent, et limitez les jetons de déploiement en lecture seule à pull uniquement.

3. Construction et envoi d'images

Avec un registre en cours d'exécution et un jeton en main, le flux de travail des images est un Docker standard utilisant le nom d'hôte du registre. La rigueur qui le rend de qualité de production réside dans les constructions multi-étapes pour des images de petite taille et les étiquettes Git-SHA immuables pour des déploiements traçables.

3.1 Dockerfile multi-étapes pour l'API TaskBoard

Une construction multi-étapes compile ou installe les dépendances dans une étape de construction volumineuse, puis ne copie que les artefacts d'exécution dans une image finale allégée. Les images plus petites sont récupérées plus rapidement sur Kubernetes et contiennent moins de packages pour que l'analyseur de vulnérabilités puisse les signaler.

FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "taskboard.api:app"]

3.2 Créer une étiquette avec le SHA Git et pousser

Étiquetez chaque build avec le SHA Git immuable afin qu'une image déployée corresponde à un commit exact. Une étiquette latest est acceptable en tant que pointeur pratique, mais ne déployez jamais à partir de celle-ci ; déployez à partir du SHA.

REGISTRY=taskboard-prod.cr.de-fra.ionos.com
GIT_SHA=$(git rev-parse --short HEAD)

docker build -t "${REGISTRY}/taskboard/api:${GIT_SHA}" \
             -t "${REGISTRY}/taskboard/api:latest" .

docker push "${REGISTRY}/taskboard/api:${GIT_SHA}"
docker push "${REGISTRY}/taskboard/api:latest"

Le modèle de pipeline consiste à construire, à étiqueter avec le SHA Git, à pousser dans le registre, puis à référencer l'étiquette SHA dans le manifeste Kubernetes (traité dans l'unité 3.2). Notez qu'un jeton accordant push nécessite également pull : la poussée d'une image exige la lecture des couches existantes, de sorte qu'une portée limitée à la poussée est insuffisante et qu'un jeton push doit également porter pull.

4. Collecte des ordures et analyse des vulnérabilités

Deux fonctionnalités du registre s'exécutent côté serveur et méritent d'être configurées tôt. La collecte des ordures réclame le stockage des couches non référencées, et l'analyse des vulnérabilités fait apparaître les CVE dans vos artefacts poussés.

4.1 Collecte des ordures

La collecte des ordures libère le stockage occupé par les données de couches non plus référencées par aucune étiquette. Elle est désactivée par défaut, car le bon calendrier dépend de votre cadence de poussée et de suppression, vous devez donc la configurer explicitement. Vous l'avez déjà configurée dans la ressource Terraform de la section 1.1 via le bloc garbage_collection_schedule avec days et un time.

garbage_collection_schedule {
  days = ["Saturday", "Sunday"]
  time = "19:30:00+00:00"
}

Pendant la collecte des ordures, le registre passe en mode lecture seule : les opérations de récupération (pulls) continuent de fonctionner, mais les opérations d'envoi (pushes) sont bloquées jusqu'à la fin de l'exécution. Il est donc recommandé de planifier cette opération pendant une période de faible trafic afin d'éviter d'interrompre les envois et déploiements CI/CD. Notez que l'API Docker Registry V2 ne permet pas de supprimer un dépôt entier ; pour supprimer un dépôt, vous devez appeler directement l'API IONOS CLOUD Container Registry avec DELETE /containerregistries/registries/{id}/repositories/{url-encoded-name}, et le nom du dépôt dans l'URL doit être encodé en URL, par exemple taskboard/api devient taskboard%2Fapi, sinon l'appel renvoie une erreur 404.

4.2 Analyse des vulnérabilités

L'analyse des vulnérabilités examine les artefacts envoyés afin de détecter des CVE connues. Il s'agit d'une fonctionnalité additionnelle, et une fois activée sur un registre, elle ne peut plus être désactivée ; il convient donc de l'activer de manière réfléchie. Les résultats de l'analyse sont signalés par artefact et par dépôt, y compris les constats pour lesquels des correctifs connus existent, ce qui vous permet de conditionner la promotion d'une image à ses résultats d'analyse dans CI.

Considérez l'analyse comme une porte de qualité : après l'étape d'envoi, interrogez les résultats de l'analyse pour l'artefact étiqueté par SHA et faites échouer le pipeline si les constats dépassent votre seuil, avant que l'étape de déploiement ne se poursuive.

5. Intégration CI

En CI, vous n'exécutez pas une commande docker login interactive. Vous stockez les identifiants du jeton en tant que secrets de pipeline et les transmettez à docker login --password-stdin. Le nom d'hôte du registre, le nom du jeton et le mot de passe du jeton sont les trois valeurs dont votre pipeline a besoin.

5.1 GitHub Actions

Stockez CR_HOSTNAME, CR_USERNAME et CR_PASSWORD en tant que secrets de dépôt ou d'environnement, puis connectez-vous et poussez dans le workflow.

name: build-and-push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to IONOS CLOUD Container Registry
        run: |
          echo "${{ secrets.CR_PASSWORD }}" | docker login "${{ secrets.CR_HOSTNAME }}" \
            --username "${{ secrets.CR_USERNAME }}" --password-stdin

      - name: Build and push
        run: |
          SHA=$(git rev-parse --short HEAD)
          IMG="${{ secrets.CR_HOSTNAME }}/taskboard/api:${SHA}"
          docker build -t "$IMG" .
          docker push "$IMG"

5.2 GitLab CI

Le pipeline GitLab équivalent utilise les variables CI/CD pour les trois mêmes valeurs et le service Docker-in-Docker pour la construction.

build-and-push:
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_HOST: tcp://docker:2376
  script:
    - echo "$CR_PASSWORD" | docker login "$CR_HOSTNAME" --username "$CR_USERNAME" --password-stdin
    - SHA=$(echo "$CI_COMMIT_SHA" | cut -c1-7)
    - docker build -t "$CR_HOSTNAME/taskboard/api:$SHA" .
    - docker push "$CR_HOSTNAME/taskboard/api:$SHA"
  only:
    - main

Étant donné que les jetons constituent le seul modèle d'authentification, faites tourner le jeton CI selon un planning défini et mettez à jour le secret de manière synchronisée. Chaque client est limité à 60 requêtes par seconde et 100 connexions simultanées, ce qui est suffisant pour un pipeline unique, mais à prendre en compte si vous déclenchez des builds matriciels parallèles qui poussent tous en même temps.

Fiche rapide de référence de l'API

Points de terminaison API clés pour Container Registry :

Méthode Point de terminaison Description
GET /containerregistries/registries Lister les registres (pagination via limit + nextPageToken)
POST /containerregistries/registries Créer un registre
GET /containerregistries/registries/{registryId} Obtenir les détails du registre (et état/nom d'hôte)
POST /containerregistries/registries/{registryId}/tokens Créer un jeton d'accès
DELETE /containerregistries/registries/{id}/repositories/{url-encoded-name} Supprimer un dépôt entier (l'API V2 ne le permet pas)

URL de base : https://api.ionos.com Authentification : Authorization: Bearer <token>

Atelier de code

Objectif : Provisionner un Container Registry, générer un jeton à portée limitée, construire l'image de l'API TaskBoard et la pousser avec une étiquette Git-SHA.

Prérequis :

  • Compte IONOS CLOUD avec un jeton API exporté sous la forme IONOS_TOKEN
  • Terraform et Docker installés localement
  • Un Dockerfile pour l'API TaskBoard (voir la section 3.1)

Étape 1 : Provisionner le registre et le jeton

export IONOS_TOKEN="your-api-token"
terraform init
terraform apply -auto-approve

Sortie attendue :

ionoscloud_container_registry.taskboard: Creation complete
Outputs:
registry_hostname = "taskboard-prod.cr.de-fra.ionos.com"

Étape 2 : Lire le nom d'hôte du registre et le mot de passe du jeton

REGISTRY=$(terraform output -raw registry_hostname)
CR_PASSWORD=$(terraform output -raw ci_push_password)
echo "$REGISTRY"

Sortie attendue :

taskboard-prod.cr.de-fra.ionos.com

Étape 3 : Connexion au registre

echo "$CR_PASSWORD" | docker login "$REGISTRY" --username ci-push --password-stdin

Sortie attendue :

Login Succeeded

Étape 4 : Construire l'image

docker build -t "${REGISTRY}/taskboard/api:$(git rev-parse --short HEAD)" .

Sortie attendue :

=> exporting to image
=> => naming to taskboard-prod.cr.de-fra.ionos.com/taskboard/api:a1b2c3d

Étape 5 : Pousser l'image

docker push "${REGISTRY}/taskboard/api:$(git rev-parse --short HEAD)"

Sortie attendue :

a1b2c3d: digest: sha256:... size: 1786

Étape 6 : Vérification via l'API

REGISTRY_ID=$(terraform output -raw registry_id)
curl -s --request GET \
  "https://api.ionos.com/containerregistries/registries/${REGISTRY_ID}/repositories" \
  --header "Authorization: Bearer ${IONOS_TOKEN}" | jq '.items[].id'

Sortie attendue :

"taskboard/api"

Liste de contrôle de validation :

  • [ ] Le registre a atteint Running et a exposé un nom d'hôte
  • [ ] docker login a renvoyé Login Succeeded
  • [ ] Image poussée avec une étiquette Git-SHA et visible via l'API des dépôts

Nettoyage :

terraform destroy -auto-approve

Erreurs courantes

Erreurs des développeurs à éviter avec Container Registry :

  1. Lecture du nom d'hôte avant que le registre ne soit en état « Running »

    • Problème : La commande docker login de votre pipeline échoue avec une erreur de résolution DNS juste après terraform apply.
    • Cause : Le nom d'hôte reste vide tant que le registre ne quitte pas l'état New et n'atteint pas l'état Running. Une lecture trop précoce renvoie une valeur vide ou non résoluble.
    • Correction : Subordonnez l'étape de connexion à l'état Running. Interrogez le registre jusqu'à ce qu'il soit prêt avant de vous connecter :
    until [ "$(curl -s -H "Authorization: Bearer $IONOS_TOKEN" \
      https://api.ionos.com/containerregistries/registries/$REGISTRY_ID \
      | jq -r '.metadata.state')" = "Running" ]; do sleep 5; done
    
  2. Jeton de push sans autorisation de pull

    • Problème : docker push échoue avec une erreur d'autorisation, bien que le jeton dispose de l'action push.
    • Cause : Le push lit les couches existantes pour les dédoublonner, ce qui nécessite une autorisation de pull. Une portée limitée à push uniquement est refusée.
    • Correction : Incluez toujours pull en plus de push dans la portée :
    actions = ["push", "pull"]
    
  3. Laisser un jeton expirer en cours de déploiement

    • Problème : Un déploiement CI qui fonctionnait hier échoue désormais à l'authentification, et le jeton a tout simplement disparu.
    • Cause : Lorsque la date d'expiration d'un jeton est atteinte, celui-ci est supprimé, et non désactivé. Il n'existe donc ni période de grâce ni possibilité de le réactiver.
    • Solution : Effectuez la rotation avant l'expiration. Créez le jeton de remplacement, mettez à jour le secret CI, puis laissez l'ancien jeton expirer. Ne définissez jamais une durée d'expiration plus courte que votre cadence de livraison, et rappelez-vous que la durée d'expiration minimale est d'une heure dans le futur.

Résumé

Vous pouvez désormais provisionner un IONOS CLOUD Container Registry en tant qu'Infrastructure as Code, générer des jetons d'accès à portée limitée, authentifier la CLI Docker et les pipelines CI contre celui-ci, et pousser des images multi-étapes de qualité production étiquetées par SHA Git. Vous savez également comment configurer la collecte des ordures pour récupérer du stockage et activer l'analyse des vulnérabilités comme une porte d'avancement. Ce registre est la source de vérité pour les images que l'Unité 3.2 déploiera sur Managed Kubernetes.

Le modèle d'accès par jeton uniquement du registre, sans RBAC et sans ACL par dépôt, détermine la manière dont vous organisez les identifiants : un jeton à portée limitée par pipeline et par développeur, renouvelé avant son expiration, car les jetons expirés sont supprimés sans autre forme de procès. Gardez à l'esprit la contrainte d'hôte après l'état Running pour chaque flux de travail automatisé, et le pipeline de construction, d'étiquetage et de poussée que vous avez construit ici devient la première étape du flux CI/CD complet de l'Unité 3.3.

Points clés :

  • Provisionnez les registres avec ionoscloud_container_registry ; name et location sont immuables et le nom est unique au niveau global (un doublon renvoie HTTP 409)
  • L'hôte suit {name}.cr.{location-with-dash}.ionos.com et est vide jusqu'à ce que le registre soit Running
  • L'authentification est basée sur des jetons docker login uniquement, avec des portées de Registry ou Repository et des actions pull, push, delete ; push nécessite pull
  • Les mots de passe des jetons sont affichés une seule fois et les jetons expirés sont supprimés, et non désactivés, il faut donc les capturer et les renouveler de manière proactive
  • La collecte des ordures est désactivée par défaut et configurée selon un planning ; l'analyse des vulnérabilités est un module additionnel qui ne peut pas être désactivé une fois activé

Terminologie importante :

  • Jeton d'accès au registre : L'identifiant pour docker login, à portée par registre ou par chemin de dépôt ; disponible en types permanents et temporaires (bornés par une date d'expiration)
  • Portée : L'octroi de permission d'un jeton, un type (Registry ou Repository), un chemin et un ensemble d'actions parmi pull, push, delete
  • Collecte des ordures : Récupération côté serveur des couches d'image non référencées, désactivée par défaut et exécutée selon un planning de jour et d'heure configuré
  • Analyse des vulnérabilités : Un module additionnel qui analyse les artefacts poussés pour les CVE par artefact et par dépôt ; irréversible une fois activé
  • Étiquette Git-SHA : Une étiquette d'image immuable définie sur le SHA de la commit, afin qu'une image déployée corresponde à une commit exacte

Prochaines étapes

Continuer l'apprentissage : Unité 3.2 : Déploiement et exploitation de Kubernetes

Sujets connexes :