OCI Registry einrichten

Um eigene Images zu verwalten und zu pushen, wird ein OCI Registry benötigt.

Im folgenden Beispiel wird die Installation über yaml Dateien durchgeführt, die direkt über kubectl importiert werden. Dazu werden die im folgenden aufgeführten Beispiele in Dateien gepackt und über denn Befehl kubectl apply -f $DATEINAME importiert.

Namespace

Zunächst wird ein Namespace erstellt in dem die Registry läuft. Für das Beispiel ist das der Namespace registry.

apiVersion: v1
kind: Namespace
metadata:
  name: registry

Storage

Damit die Daten der Registry auch nach einem Neustart erhalten bleiben, wird ein Storae benötigt. Über die default Storage Klasse wird dafür ein PersistentVolumeClaim erstellt. Dieser erstellt dann ein PersistentVolume sobald dieses von dem Deployment angefordert wird.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: registry
  namespace: registry
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: "5Gi"
#  storageClassName: longhorn

Wird eine andere als die default StorageClass gewünscht kann diese mit dem Parameter storageClassName angegeben werden.

Secrets

Damit die Registry nicht ohne Authentifizierung läuft muss im Vorfeld eine htpasswd Datei erstellt werden um darüber die Benutzer zu verwalten.

Zunächst wird die htpasswd Datei erstellt. Dies lässt sich am einfachsten über den httpd OCI Container durchführen. Dieser enthält das benötigte Progamm htpasswd. Im Beispiel wird podman genutzt. Der Befehl kann gegen docker getauscht werden.

user@linux $ podman run --rm --entrypoint htpasswd docker.io/httpd -Bbn \
                testuser testpassword > htpasswd

Über die Datei htpasswd wird anschließend ein Secret im Namespace registry erstellt.

user@linux $ kubectl create secret generic registry-secret --dry-run=client \
                -o yaml --type opaque --from-file htpasswd -n registry
apiVersion: v1
data:
  htpasswd: dGVzdHVzZXI6JDJ5JDA1JGVSNXliR3hoU0gxSmVtcEIxR0dZMnVwUFVJb1lFQnFTSXhaVVlEYUpYRld6SVV0UTVReDFDCgo=
kind: Secret
metadata:
  creationTimestamp: null
  name: registry-secret
  namespace: registry
type: opaque

Deployment

Über das Deployment wird die Definition des Registry Pods durchgeführt. Dieser wird dann über ein automatisch erstelltes ReplicaSet gestartet.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
      release: registry
  template:
    metadata:
      labels:
        app: registry
        release: registry
    spec:
      containers:
        env:
        - name: REGISTRY_STORAGE_DELETE_ENABLED
          value: "true"
        - name: REGISTRY_AUTH
          value: htpasswd
        - name: REGISTRY_AUTH_HTPASSWD_REALM
          value: Registry Realm
        - name: REGISTRY_AUTH_HTPASSWD_PATH
          value: /auth/htpasswd
        image: registry:2
        imagePullPolicy: ifNotPresent
        livenessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 5000
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        name: registry
        ports:
        - containerPort: 5000
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /
            port: 5000
            scheme: HTTP
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 1
        volumeMounts:
        - mountPath: /auth
          name: auth
          readOnly: true
        - mountPath: /var/lib/registry/
          name: data
      restartPolicy: Always
      securityContext:
        fsGroup: 1000
        runAsUser: 1000
      terminationGracePeriodSeconds: 30
      volumes:
      - name: auth
        secret:
          defaultMode: 420
          items:
          - key: htpasswd
            path: htpasswd
          secretName: registry-secret
      - name: data
        persistentVolumeClaim:
          claimName: registry

Service

Nach dem Einlesen des Deployments existiert der Pod registry im Namespace registry, es existiert aber noch kein Zugriff von Außen auf den Container. Dazu wird ein Service erstellt.

apiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: registry
spec:
  ports:
  - name: http-5000
    port: 5000
    protocol: TCP
    targetPort: 5000
  selector:
    app: registry
    release: registry
  type: ClusterIP

Der Service ist in der Lage den passenden Pod auf Grund des selector zu finden und diesem eine ClusterIP zu geben. Dies ClusterIP mit dem Port 5000 ist dann eine Möglichkeit auf den Dienst zuzugreifen.

Allerdings nur auf einer der Cluster Nodes. Um auch von Außen auf den Dienst zugreifen zu können macht es Sinn einen Ingress zu erstellen über den dann der Zugriff erfolgt.

Ingress

Für den Ingress wird eine DNS Domain benötigt, die auf den Cluster zeigt. Für das Beispiel ist es registry.internal. Um auch per https auf den Dienst zugreifen zu können muss ein TLS Zertifikat vorhanden sein. Diese kann wie Folgt erstellt werden:

user@linux $ openssl req -x509 -newkey rsa:4096 -keyout tls.key -nodes \
                -out tls.crt -days 365 -subj '/CN=registry.internal'

Statt des selbstzertifizierten Zertifikats kann auch ein Let’s Encrypt Zertifikat über cert-manager oder ein bestehendes Zertifikat verwendet werden.

Aus dem Zertifikat wird nun wiederum ein secret erstellt.

user@linux $ kubectl create secret tls registry-tls -n registry \
                --key=tls.key --cert=tls.crt --dry-run=client -o yaml
apiVersion: v1
data:
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZHVENDQXdHZ0F3SUJBZ0lVZC9kV1Q2TEloLzBlR2JJZXNqclA1WEQyQ0hBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0hERWFNQmdHQTFVRUF3d1JjbVZuYVhOMGNua3VhVzUwWlhKdVlXd3dIaGNOTWpVd05ERTNNVEV4TmpBMApXaGNOTWpZd05ERTNNVEV4TmpBMFdqQWNNUm93R0FZRFZRUUREQkZ5WldkcGMzUnllUzVwYm5SbGNtNWhiRENDCkFpSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnSVBBRENDQWdvQ2dnSUJBTFJwdUYxcW5OTXdUUDNrdy9SWFdMSWUKU1dIVXJ0U3dGMjJML0RSL0N3bWRCU0RvbjIvRHJFcTdqcmYwOUkrS21SMGJlRWNNczJUTDQvSVpIT2RYQkVZTgpMbzNJTTR5c1ZoRUh1ajRDekFQYzc5eW95U20vYjFSRVpjSElKd0ltSS93UjRrSlFhODU3eTZZVVR5enhUbFdyCm8zUXdLcHEwcnhwdDgxN083SkI0THp1V1dyTklqdWpJVHh2YXNXR1ozd0NuSmNIczRzWlZpZk1KUGZhV2RpUysKd2hTZDR6U1J1VnZFWFZMalFmYkk2bGtJVTF4THIrR2VPL0c5WVZ2TmFJeVZsT0JUd1Jqa0hDT2pOa0dpR1dKVAp1b2JoTE43KzNWaFdmYjZ3enQwV3BHM0NJTjVFemJNOUxQems3aVljdTk2RVJ3dlhvREp6eGRSbUF3WUZRY2twCjEzd1VoSGdFdkZUZDlvZk9HZ3graDd3cm1Mdk1mZitCRUk3amRNUEhST3UwSVpqSGxOQTVMRzA3ai9rbE5xWmsKMXdhZHNyUjNDZjVpM0tVbmlpMTYxQ29WMitjU1YyRTIyUFBrVnNsVklKYmJvT0VXZGc0c2ZNTHd5Y3ZSVk8xcQovVjl6MEVsajBhMllSTUdXMWI4cXIwNkpHUkRvVEFKSFNzc1R1dm5icmJ6ZzJOdEl0NFd0OFZpVXRFVjdTMkpTCnN6WmhwSTRUUlN2bHRwbUlneXVMWDUzVmNWWUhxRXBFY2xZOC9tSkdmdXJzWGxhdm9NSmlaYk5MMWpLaUJkL1kKNkRxMTJnMmMvc3BXOXlYeGJyQnlIWEZraVZpc21pOG5tTFYxQTFsSHpGc3ZTdnIzZTRpVWpLV0t3RklsQ2o4Rgorak42c2FFdjFmZHFwUldERExXMUFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCVElWdGo0bDdCNVczcUw1T0NOClBrWFlNWkdpcWpBZkJnTlZIU01FR0RBV2dCVElWdGo0bDdCNVczcUw1T0NOUGtYWU1aR2lxakFQQmdOVkhSTUIKQWY4RUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElDQVFBcUtWVjFHM2JwRHFMN04rTnpNTitGK0lIMwphUVd3ZGFRSGtuN2JXYmNBamQvUEZ1YXJPdCtMaUMvUmQ5VUhLWVA3MGdHZmQ5YTFmWjBGTlZDb3RZR2ZvYnVDCjNUV0RnanRXNy8wSi9ENXFNR3REWk1UVW1NVGVheVR5VlpsdUlxcTdGMklDYjU0R0k2WnR4aWRuNUw5bkhpQU8KRGFmQnZkMTFPQ2ViZ1VsRHBVR3FoaW50ZmJsVkY1NUhwUjdiSEpqV2U1WDAva3NyZi92a0RUMEVLYTlzZkhrUwpLTTZaNm9LTVlDUEdrL1d4dzA0YkF2dzU2U0pMUnFTU0NCL1pqQVQxazhyeW1ieENMekczN1NPeDJ4NWpueEF5CkZ5RldUcExHb2hOZmRURDlxOGxlSjJYMUh5L1gwVVgyM1Vpek5US2kyRjMxallDVXQ5QkJwbEx6ekM5bW1BeEgKbm9FMnNKS3lKbXdDdTk3VGhvOUlnSHpPMVZHV3Qyb2NPQ0lNdkxqY2ZoTUpHR2F5MFVoQ3JvYk9XWlBHS3VndAoyQU85VW9WaDVWMzloVDJVSkFVZFdDN0hsYi9RdFhLL3ZyczgybW5kZDVyUXFXajVrSjFEbXRNaldFak1DcVlvCjJPd0tSc1pqT2ZzcGhDSlpIOFR5eFoxY1V3Q0laMEsrVzFxeURSWDYwUkF4M3ZtMFBueU1YNGR4aFJsMWl6VmEKZi9jc1d3K09mSnVnVzZ6Zm1VdlV4by95WG0vSElHVExaL003OUlBY3NrbnJuM0pqWTBubGlmd0h4bFV1U0F6ZwoxN2ZIcjdGcjk3NHIzYWZLdWZnSU8rR1VkZnFsQWZhWkxmcGtqdGhqZ3FEV0VBRlNaSEhxcXJuWnZva2hhZURpClIyZUZVT0JRT1czaHhhVTdZQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRQzBhYmhkYXB6VE1FejkKNU1QMFYxaXlIa2xoMUs3VXNCZHRpL3cwZndzSm5RVWc2Sjl2dzZ4S3U0NjM5UFNQaXBrZEczaEhETE5reStQeQpHUnpuVndSR0RTNk55RE9NckZZUkI3bytBc3dEM08vY3FNa3B2MjlVUkdYQnlDY0NKaVA4RWVKQ1VHdk9lOHVtCkZFOHM4VTVWcTZOME1DcWF0SzhhYmZOZXp1eVFlQzg3bGxxelNJN295RThiMnJGaG1kOEFweVhCN09MR1ZZbnoKQ1QzMmxuWWt2c0lVbmVNMGtibGJ4RjFTNDBIMnlPcFpDRk5jUzYvaG5qdnh2V0ZieldpTWxaVGdVOEVZNUJ3agpvelpCb2hsaVU3cUc0U3plL3QxWVZuMitzTTdkRnFSdHdpRGVSTTJ6UFN6ODVPNG1ITHZlaEVjTDE2QXljOFhVClpnTUdCVUhKS2RkOEZJUjRCTHhVM2ZhSHpob01mb2U4SzVpN3pIMy9nUkNPNDNURHgwVHJ0Q0dZeDVUUU9TeHQKTzQvNUpUYW1aTmNHbmJLMGR3bitZdHlsSjRvdGV0UXFGZHZuRWxkaE50ano1RmJKVlNDVzI2RGhGbllPTEh6Qwo4TW5MMFZUdGF2MWZjOUJKWTlHdG1FVEJsdFcvS3E5T2lSa1E2RXdDUjByTEU3cjUyNjI4NE5qYlNMZUZyZkZZCmxMUkZlMHRpVXJNMllhU09FMFVyNWJhWmlJTXJpMStkMVhGV0I2aEtSSEpXUFA1aVJuN3E3RjVXcjZEQ1ltV3oKUzlZeW9nWGYyT2c2dGRvTm5QN0tWdmNsOFc2d2NoMXhaSWxZckpvdko1aTFkUU5aUjh4YkwwcjY5M3VJbEl5bAppc0JTSlFvL0Jmb3plckdoTDlYM2FxVVZnd3kxdFFJREFRQUJBb0lDQUNPVTlFRXNYVGpEdm5PV3NsVVhBdzNDCkxtdm1sQUtycGpzN1VDY1VaVnRraGhYcSswTUNQeEZRTTRJeGhDaHBSL0IzTWY0bFVaNVFIaWxwN1lyczNSRnAKMlNRcWQ0eEhrd1B4MTdnU09pV0s1aDNKaGo4L0c5aHRVdnBvbk5UdUs4dXp4VUdaOWVFNEJqNi9vNnZ6L2FTawo0T3h4OFgwb3BKNWNVQk1EVWFIZXFrWWd6Y3V0UkU1QjcyUkplaEdMVlBHZWhiSmRJNDdGWVJzM1YrcXgyeVE5CklTOEt3cnhqLzBCTGxySlowdkUyeUExK09GNnNLTWloT2laZjJQdFBwRTZSMDlaNGZrMkg4OHJOams1Y2g1QWUKZmx2S3ZseE92TjRHMU84T2xRWUN5TFJEQk5PQmsyWkY2d0VIcjdhQmxVWDZKYjcrcklBMjJDNGZYbnBLQUNLMApTY0Jjd3d6dzBmQTF4a3BaTk1zSVVpLzhwLzJvUjlyY0loaUNRSTd0WFlueERRSENWZ3dVNGJITjJhakdHMUJICitZTmZsK2t4QWk1WUJja0p0bHVjQXJoWWdHUFd0b1FmWnFJU3IrNFVkWEFTZ3VBOGp5aVdHVVlBWnFuUlVrWEMKYjBseTIyR0x5dWRoN2ZJSnFmQXR3Sm5ON25icmpDZy90dHFOc1FMZWgxWGpVYUNYbExvMmVTVmFlZHNZa0dJNwpKMXVwckJiWVlqQURDa1hLaDhHYnB0cEZBZmpRekU2TjEyRTd1dVF1eGdrRXRBZnp6QlIrWTh3ejhpWkFPR043ClpWcUYwTWlRV21UWmRhM3RVSnM5QnpKMDBPZDhNbzZsVHFYTlFxOXU0TkxoOThzTTRsc0dhU0lLYmFTS09RQ2MKTkpKdXc3M3R2V0ltZlNQNm5GbVRBb0lCQVFEOFVmSTlwTU52R2picjQ3Y00vZElYRk44NWFQZ2VDd2RXRXh2KwpWUG9VeVd2THp0a3BtdWRUdWZXY25aVzVZSTZRZUVtcmk0YjNkbUMySHgvS0tTL0c0a0tvdFZncFJhd3RjSUpNCnh4ck5YdTdZSjhadkg2bG84NUxHbmlQVDR2NDRNR2dHNjFBRVREVEorakUrSStwVmpnazdEKzNXQnd5RkRSbXAKaWVoSSs2THU0VDI1U3ZEYjdaZW1OT1RXbjY1N25BOURXUzUrRUtQY00yOFJid3VTdEJiMEZNTWptUFhJM25NcwpyeDBkd2lNc0wzNStOdU9qRk14VEpocnQ5RHFESUJBUGdtdUNKTUNuVHlwakdKR01DbzJZVXpZdko3U0Z0TjMvClBCd0dQb050cjFkUW4ydnpVeHluZVBTeDZydDZYUlhOeFQyTU1tUmg2dndoTUNtdkFvSUJBUUMzQzAzTU90NFgKRTVnQkxPZzc4L2ZWb2txNURCa0tBdGlPM3lJWnFHS1BXUHRnQ2x5dUpzZ012SVI3M3hoVkJ6YTJCWkMvUkNyMApkQWRRbnZXYXRhb1YyN3YzMFBzQ0xRaFU3Q2NpdWdxRlJUN1lnV0RIQk5tbkVWeHExOWYxd2pTcHlIdVl1bUkyCk5SczZCME90RHRleld3TndvWkRCZGN1VnR5cXFReDdWRFErcHRKZ290dnhtbWNRcTN4WnhQczNuWmhVazJ1VlUKL241SnhzNWFLOHpNZHRoUWpucGdOYzVFQmhEa1pjK1dyTmM1WjBtNUsvMnh5dlFsdXF6R1AzWnJYUlFFcm1VYwpOWkQrRVJxdVVuY3BJS3pMOU8yVHFKZHoySGlQOWptSUJKOTdjVUVrNHRQcTJZYnFlWHY1SGxRczk5TGtXTFFQCm9rVHp4RUthWkFQYkFvSUJBQk5yYXkxN1pOOUVNVDN1aE1Rbk5PZzUzd1JZSStDTUVNQjdNQWhmR2ZCSG9GaEwKVDBONGZKMWhEcHBETnFiUjI2Y2EydkN0Q0hJN1ZpNHVMeEFzSTFVM1ROSVhRdlhLT3ZvMFVwTCtMLzNtZEpPRwpYQ3RwVUd5WGNwQiswMXNYZVdGeHVFL3dCSFNRT08vaXBhK1dyQTV4cEJ3N21aNkRaRjlKSnRSNW5Mc2hRdVVICnlPeGQ3RFBCRVk3bE8wZVplcUJnM252Y2ZVdU9sRTM2VjI0TUVlUDBvRitneVhRUDdick1CQ2xJUVFGdFU2K3UKNkJsbXVCWnhIMWkyOGhPbVhHcURLVGxJWEdYajFrQUtROFRrdXk4QUVPM01XYnMvWHhGN1hpcXF3YTFwV2t3eQoxcEdKODVFQ3NJM2pMVklVTXVHMEVGMko5TGE1bWlnL0liQ2NPRmtDZ2dFQWNyalgrcitPZ0tJY0sraHNhVjhBCkh0cEh3UER2SkpJaURuR1ZHc1dwZURTSjRHaStLN3hNdDRiVDloc2VVZlJpZXZURUFzeURxNUNwSVFOdjVaWWsKVXV6VVEzNnNRM0hiL2ZYQWxZaEtuYnFIcUFSMmxtWFkwRmdXTnpTdXV1NE1PYmxZbGFHRGVud1FZMjg0SUZmawpJeHBRN3Ara2JVd1oyK3pnQmJScHQ1Z25EL01MUnNGaE1pRC9qT3NqVXJqTkxsR1J3UGFjczRlSnVVMnIrV0hvClN1SFZzNitJelpJTGhDRFZKNEVUK1ZpOHo5S0hwMUlHQUErMnM0cnJUSmxyNitCN2NtK25HV1lOU04rSmZHWmQKSHQ3R3psSGxNUjJDUnU0T3FoUGxmUFdBeTBweFRPQUpkL0Q4R1NzbzR6cjZBUEVXTVYzVEJOVnNKK2dDNG1iNgo3d0tDQVFFQXoxWEdMV1pILytyblozdmVkVzh2U0U0cks0N3RXQ0dwQlJsM0xSTTBia1ZlaGJQN3JLRHg4a3MwCmtqQUtXei8rb3Vud1BqSGR0T2dBZmhVcUR6cS85Q2IzOFBOQ1Q3emM5dmlnc2FXc3ZuU05aSWNjakhFV2hpT1QKcjg1L3h6ZG82WkorOE1WQzl0Wll5YjRudmNuWkhVV2dKSmljYm1PMTAxcnJxVG1uUi8xcFZwVnF2R3FwNG5lRgprbm5mS1oydnhwZUlsQjIyQ20rNGlLNzdLekgveW1oc2xGcE5nR3FNS0F3RFlqM0NHK21hc0RPMXVXUUhSWjZWCmxQYjB0WHNOVVNYLysyb1d5U2VYWlBTbnhMd1JvUDZPdFFGTjdSR0MyekN6b05tdndnV3l5RVVrNHFtVW9vOVYKSTBrSFBkNkhFVXFkcTF3SVJ5WjBrWWFMakNJRkNBPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
kind: Secret
metadata:
  creationTimestamp: null
  name: registry-tls
  namespace: registry
type: kubernetes.io/tls

Dieses secret wird in dem folgenden Ingress verwendet.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    # cert-manager.io/cluster-issuer: acme-production
    # kubernetes.io/tls-acme: "true"
    ingress.kubernetes.io/proxy-body-size: 1024m
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/proxy-body-size: 1024m
  name: registry
  namespace: registry
spec:
  rules:
  - host: registry.internal
    http:
      paths:
      - backend:
          service:
            name: registry
            port:
              number: 5000
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - registry.internal
    secretName: registry-tls

Die beiden auskommentierten Einträge sind die Verwendung von cert-manager und einem ClusterIssuer gedacht. Der ClusterIssuer muss dafür zunächst angelegt sein und die Domain von extern durch den Zertifizierungsdienst - z. B. Let’s Encrypt

  • erreichbar sein.

Nach dem Erstellen des Ingress ist ist die Registry über https://registry.internal erreichbar.