Kubernetes - Créer et accéder à une registry Docker privée (arm/amd64)

Vous disposez maintenant d’un cluster Kubernetes, vous créer vos applications disposées dans votre propres conteneurs. Par défaut, K8s va “se servir” sur le docker HUB. Si vous ne souhaitez pas stocker vos images chez “Docker”, la solution reste de créer votre propre “registry Docker”.

D’autres raisons peuvent vous amener à créer votre propres conteneurs, notamment, en rapport au niveau de sécurité des images que vous pouvez trouver sur le “DockerHUB” :

Le but de cette procédure sera donc de créer votre propre “registry Docker” et d’y conneter votre cluster Kubernetes.

Source de ce document : Deploy Your Private Docker Registry as a Pod in Kubernetes

Opération à réaliser sur tous les noeuds

Sur Debian 10, des paquets sont manquant pour Docker.

Réalisez cette opération sur tous les noeuds

Commencez par le master :

apt -y install gnupg2 pass

Opération à réaliser sur le serveur Master (Debian10)

Contexte : j’utilise un serveur Gluster accessible du cluster Kubernetes (voir son installation ici).

Le nom du volume Gluster exporté est : “docker-registry”. Toutes les opérations réalisées ci-dessous seront réalisées dans le namespace Kubernetes : “docker-registry”

J’ai suivi scrupuleusement la documentation de Varun Kumar G, en adaptant les opérations par “Manifests” et l’ajout de shell d’automatisation.

Création du namespace “docker-registry”

Créer le fichier “namespace.yaml” :

apiVersion: "v1"
kind: "Namespace"
metadata:
  name: "docker-registry"
  labels:
    name: "docker-registry"

Exécution : kubectl apply -f namespace.yaml

Création des fichiers secrets et authenfication

J’ai créé un shell à partir des sources de Varun Kumar G. Les “secrets” sont utilisés par Kubernetes, l’authentification sert dans le processus “docker pull” des workers. Les workers pourront ainsi, aller chercher les images dans votre registry privée en utilisant ces paramètres d’authenfication (login/password).

Créer le fichier gensecretauthmanifest.sh contenant :

#!/bin/bash
echo "Entrez le login d'authentification d'accès au registry privé Docker"
read LOGIN
echo "Entrez le mot de passe d'authentification d'accès au registry privé Docker"
read PASSWORD
mkdir -p ./registry/certs ./registry/auth
echo "Generating tls..."
openssl req -x509 -newkey rsa:4096 -days 365 -nodes -sha256 -keyout registry/certs/tls.key -out registry/certs/tls.crt -subj "/CN=docker-registry"
SECRETMANIFEST="secrets-auth.yaml"
echo "Generating tls Manifest"
cat << EOF > "$SECRETMANIFEST"
apiVersion: v1
kind: Secret
metadata:
  name: certs-secret-docker-registry
  namespace: "docker-registry"
type: kubernetes.io/tls
data:
  tls.crt: $(cat registry/certs/tls.crt | base64 --wrap=0)
  tls.key: $(cat registry/certs/tls.key | base64 --wrap=0)
EOF

echo "Generation auth file"
AUTHFILE="registry/auth/htpasswd"
docker run --rm --entrypoint htpasswd registry:2.6.2 -Bbn "$LOGIN" "$PASSWORD" > "$AUTHFILE"

# Check
echo "CHECK..."
if [ ! -f "$AUTHFILE" ];then
        echo "Le login/password n'a pas été créé par le container htpasswd"
        exit 1
fi
echo " [*] $AUTHFILE content : "
cat "$AUTHFILE"
echo "Displaying architecture created"
ls registry/*

echo "Generating auth Manifest"
echo "---" >> "$SECRETMANIFEST"
cat << EOF >> "$SECRETMANIFEST"
apiVersion: v1
kind: Secret
metadata:
  name: auth-secret-docker-registry
  namespace: "docker-registry"
type: Opaque
data:
  username: $(echo "$LOGIN" | base64 --wrap=0)
  password: $(echo "$PASSWORD" | base64 --wrap=0)
EOF

echo "---" >> "$SECRETMANIFEST"
cat << EOF >> "$SECRETMANIFEST"
apiVersion: v1
kind: Secret
metadata:
  name: auth-secret-docker-registry
  namespace: "docker-registry"
type: Opaque
data:
  htpasswd: $(cat registry/auth/htpasswd | base64 --wrap=0)
EOF

echo "Execute now : "
echo ""
echo "    kubectl: apply -f $SECRETMANIFEST"
echo ""

Ce shell généréra :

  • le fichier secrets-auth.yaml contenant les commandes pour créer les secrets.
  • un répertoire registry, contenant le certificat qui devra être disposés sur tous les workers.

A cette étape, pensez à conserver le login et mot de passe qui seront utilisés ci-après…

# Exécution du shell
bash ./gensecretauthmanifest.sh

#Ajout des secrets dans le cluster K8s
kubectl apply -f secrets-auth.yaml

Création du Persistent Volume et Claim concernant le stockage des images

Sur le serveur Gluster, j’ai créé et démarré le volume : “docker-registry”, et je travaille dans le namespace : “docker-registry”

Attention : Adapter la valeur de l’adresse IP du serveur Gluster ([server IP addr]) volumes.yaml :

apiVersion: "v1"
kind: "Service"
metadata:
  name: "glusterfs-cluster"
  namespace: "docker-registry"
spec:
  ports:
  - port: 1
---
apiVersion: v1
kind: "Endpoints"
metadata:
  name: "glusterfs-cluster"
  namespace: "docker-registry"
subsets:
- addresses:
  - ip: [server IP addr]
  ports:
  - port: 1
---
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: "gluster-pv-docker-registry"
  namespace: "docker-registry"
spec:
  capacity:
    storage: "1Gi"
  accessModes:
    - "ReadWriteOnce"
  storageClassName: "docker-registry"
  persistentVolumeReclaimPolicy: "Recycle"
  glusterfs:
    endpoints: "glusterfs-cluster"
    path: "docker-registry"
    readOnly: false
---
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
  name: "glusterfs-claim-docker-registry"
  namespace: "docker-registry"
spec:
  accessModes:
    - "ReadWriteOnce"
  storageClassName: "docker-registry"
  resources:
    requests:
      storage: "1Gi"

Pour créer le volume, exécuter la commande

kubectl apply -f volumes.yaml

Création du deployment “Registry” et de son service

Toujours dans le namespace “docker-registry”, je crée le “deployment” et le service de l’application :

deploy.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: "docker-registry-deploy"
  namespace: "docker-registry"
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: registry
  replicas: 1
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
      - name: registry
        image: registry:2.6.2
        volumeMounts:
        - name: repo-vol
          mountPath: "/var/lib/registry"
        - name: certs-vol
          mountPath: "/certs"
          readOnly: true
        - name: auth-vol
          mountPath: "/auth"
          readOnly: true
        env:
        - name: REGISTRY_AUTH
          value: "htpasswd"
        - name: REGISTRY_AUTH_HTPASSWD_REALM
          value: "Registry Realm"
        - name: REGISTRY_AUTH_HTPASSWD_PATH
          value: "/auth/htpasswd"
        - name: REGISTRY_HTTP_TLS_CERTIFICATE
          value: "/certs/tls.crt"
        - name: REGISTRY_HTTP_TLS_KEY
          value: "/certs/tls.key"
        - name: REGISTRY_STORAGE_DELETE_ENABLED
          value: "true"
      volumes:
      - name: repo-vol
        persistentVolumeClaim:
          claimName: "glusterfs-claim-docker-registry"
      - name: certs-vol
        secret:
          secretName: certs-secret-docker-registry
      - name: auth-vol
        secret:
          secretName: auth-secret-docker-registry
---
apiVersion: v1
kind: Service
metadata:
  name: docker-registry
  namespace: "docker-registry"
spec:
  selector:
    app: registry
  ports:
  - port: 5000
    targetPort: 5000

Pour déployer l’application, exécuter la commande

kubectl apply -f deploy.yaml

Vérification du démarage du container

Utilisez la commande : kubectl describe pod et la touche tab (completion automatique, ceci nécessite de l’avoir implémenté Voir vers la fin de cette procédure)

kubectl describe pod -n docker-registry [TAB]

Répéter la commande jusqu’à obtenir “Started container registry” dans ce type de résultat (le déploiement peut prendre quelques minutes) :

Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  36s   default-scheduler  Successfully assigned docker-registry/docker-registry-deploy-6467fb7ffc-8q762 to MY_SERVER
  Normal  Pulling    33s   kubelet            Pulling image "registry:2.6.2"
  Normal  Pulled     26s   kubelet            Successfully pulled image "registry:2.6.2" in 6.542667447s
  Normal  Created    10s   kubelet            Created container registry
  Normal  Started    9s    kubelet            Started container registry

Post-installation (à réaliser sur tous les noeuds)

Résolution IP du service de registry

Tous les noeuds du cluster doivent avoir accès à la registry privée. Cette registry est créée dans le cluster, donc accessible uniquement par le réseau du cluster.

Qui récupère les images auprès de la registry ? Le processus Docker du noeud choisi par le plan de control. Docker devra résoudre lui-même l’adresse de l’image, fournie dans le manifest de déploiement. Par conséquent tous les noeuds devront résoudre ce FQDN.

Notre registry écoute sur le port 5000 et sur une adresse définie lors de la création du service. Pour connaitre l’adresse IP du service créé pour la registry privée :

kubectl get svc -n docker-registry

Exemple:

NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
docker-registry     ClusterIP   10.106.2.93     <none>        5000/TCP   16h

Ce service sera accessible par l’adresse/port : 10.106.2.87:5000

Pour permettre une résolution de cette Ip par le nom “docker-registry”, j’ajoute le résultat de la commande suivante dans le fichier “/etc/hosts” de chacun des noeuds du cluster :

kubectl get svc -A | grep -E "docker-registry.*5000" | awk '{print $4"  docker-registry"}'

Exemple :

10.106.2.93 docker-registry

Ajouter cette valeur au fichier /etc/hosts de tous les noeuds

Certificat

Cette opération va consister à copier le certificat “tls.crt”, créé lors de la génération des secrets, dans la liste des certificats de confiance de docker. L’opération est à réaliser sur chaque noeud du cluster.

Master

Le plus facile, puisque c’est lui qui dispose du répertoire “registry” contenant le certificat de la registry privée.

# créer le répertoire contenant le nom hôte et son port, définis ci-avant
mkdir -p /etc/docker/certs.d/docker-registry:5000
# copie du fichier tls.crt
cp registry/certs/tls.crt /etc/docker/certs.d/docker-registry:5000/.

Sur TOUS les workers

Cette opération est à réaliser à partir du Master et sur tous les autres noeuds :

ssh root@[noeud] 'mkdir -p /etc/docker/certs.d/docker-registry:5000'
# copier le fichier registry/certs/tls.crt du master sur les noeuds worker
scp registry/certs/tls.crt root@[Noeud]:/etc/docker/certs.d/docker-registry:5000/.

PS : Pour éviter la demande du mot de passe à chaque commande, installez la clé ssh public du Master, sur tous les workers du cluster.

Test de fonctionnement

La résolution, ainsi que le certificat sont mis en place sur tous les noeuds

Donc pour s’y connecter à partir de n’importe quel noeud, nous utiliserons la commande :

docker login docker-registry:5000 -u [LOGIN] -p [PASSWORD]

LOGIN et PASSWORD ont été définis dans la première partie de cette procédure.

Le nom sera résolu par l’hôte (fichier /etc/hosts)

vous devre obtenir :

WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

Creation du secret d’authentifications Kubernetes

Nous avons donc vérifié que Docker peut accèder à la registry privée sur tous les noeuds. Les applications seront déployées dans le cluster Kubernetes et c’est kubernetes qui pilotera Docker. Et Kubernetes devra fournir à Docker le compte et mot de passe d’accès à cette registry privée.

Kubernetes utilisera dans ce cas un mécanisme basé sur les “secrets” (détails).

Les paramétres d’authentification (login/mot de passe d’accès à la registry Docker privée) doivent être créés dans le même namespace que le “pod” ou le “deployment”.

Stockons ce secret

Ce secret sera stocké dans le namespace “default”

Sur le master, connectez-vous à la registry privée :

docker login

LOGIN et PASSWORD ont été définis dans la première partie de cette procédure.

Cette action va créer la propriété “auth” dans le fichier ~/.docker/config.json et donc permettre à docker de se connecter automatiquement à la registry privée.

Utilisons ce secret pour créer le secret “regcred” dans kubernetes :

kubectl create secret generic regcred --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson

Tout est maintenant en place pour permettre un déploiement à partir de fichiers manifest.

Déploiement d’un manifest incluant une image de la registry privée

Créer votre manifest, et ajouter ce composant en indiquant bien le namespace de votre application :

---
apiVersion: v1
data:
  **secret**
kind: Secret
metadata:
  name: auth-secret-docker-registry
  namespace: [namespace utilisé]
type: kubernetes.io/dockerconfigjson
---

**secret** sera la valeur contenu dans le secret regcred et obtenu avec la commande :

kubectl get secret regcred -o jsonpath='{.data}' | sed -E 's/\{|\}|"/ /g' | xargs

exemple obtenu : .dockerconfigjson : xcfvdfsd….==

Remplacer **secret** par la valeur obtenue, et au niveau du “Deployment”, ajouter la propriété “imagePullSecrets” :

apiVersion: apps/v1
kind: Deployment
metadata:
  [...]
spec:
  [...]
  template:
    metadata:
      labels:
        [...]
    spec:
      imagePullSecrets:
      - name: auth-secret-docker-registry
      containers:
      - name: ...
        image: docker-registry:5000/[image]

Durant la phase de déploiement, l’image sera récupérée en fournissant le secret “auth-secret-docker-registry”

Suppression de la registry privée

# suppression du namespace
kubectl delete namespace docker-registry
# suppression du "Persistent Volume"
kubectl delete pv gluster-pv-docker-registry

Gérer le contenu de la registry

A partir d’un serveur d’un master du cluster

curl -k -H 'Accept: application/json' -u [login:password] https://docker-registry:5000/v2/
# retourne {}
# si problème avec l'authentification retourne :
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}

Suppression une version d’image

Pour pouvoir supprimer une image, il faut que le container dispose de la variable d’environnement : REGISTRY_STORAGE_DELETE_ENABLED="true"

  • Liste du catalogue :
curl -k -H 'Accept: application/json' -u [login:password] https://docker-registry:5000/v2/_catalog
# Retourne un contenu json présentant les images disponibles
{"repositories":["apache2-php7","backuppc","nextcloudcustom","trudesk"]}
  • Lister les références (tag) d’une image Par exemple, le résultat de la commande précédente l’image “trudesk”, listons les tag de cette image
curl -k -H 'Accept: application/json' -u [login:password] https://docker-registry:5000/v2/trudesk/tags/list
# Retourne un contenu json présentant les images disponibles
{"name":"trudesk","tags":["1.0.10"]}

L’image trudesk:1.0.10 existe dans le dépot.

  • Suppression de l’image:tag

Créer le fichier registry-delete-image.sh

## Set values
registry="docker-registry:5000"
user="docker login"
password="docker password"

IMAGENAME="$1"
tag="$2"

if [ "$1" == "" ] || [ "$2" == "" ];then
  echo "Please provide imagename and tag"
  exit 1
fi

echo "imagename:$IMAGENAME"
echo "tag:$tag"

response=$(curl -u $user:$password -sSL -I -H "Accept: application/vnd.docker.distribution.manifest.v2+json" https://${registry}/v2/${IMAGENAME}/manifests/$tag|grep "docker-content-digest:"|cut -d ":" -f 2,3|xargs)
if [ "$reponse" != "" ];then
  echo "digest:$response"
  curl -v -u $user:$password -X DELETE https://${registry}/v2/${IMAGENAME}/manifests/${response}
  echo "$1:$2 has been deleted"
else
  echo "Nothing to delete"
fi

Pour utiliser ce shell :

chmod +x registry-delete-image.sh
# Exemple pour supprimer l'image : trudesk:1.0.10
registry-delete-image.sh "trudesk" "1.0.10"

Pour plus de détails sur l’API V2