Skip to content

Image-Versionierung - Konzept

Ziel dieses Konzepts ist ein dauerhaft kleiner Storage-Footprint der Registry, ohne dass Images verloren gehen, die noch aktiv genutzt werden. Zusätzlich soll über das Tag-Schema klar erkennbar sein, welche Artefakte für den Dev-Cluster und welche für den Prod-Cluster bestimmt sind.

Wichtige Konzepte

Retention gibt keinen Speicher frei. Ein Retention-Job entfernt Tags und Artefakt-Einträge aus der Harbor-Datenbank; die zugehörigen Blobs sind danach nur noch unreferenziert. Erst die Garbage Collection (GC) löscht die Bytes im Storage-Backend. Ohne GC-Zeitplan bringt eine Retention-Policy nichts.

Retention-Regeln werden mit OR verknüpft, nicht mit AND. Ein Artefakt überlebt, wenn mindestens eine Regel darauf zutrifft. Regeln beschreiben deshalb immer, was behalten werden und nicht was gelöscht werden soll. Eine zusätzliche Regel kann eine Policy ausschließlich permissiver machen, nie restriktiver.

1. Tag-Schema

Retention-Regeln greifen auf Tag-Muster zu. Verbindlich ist folgendes Schema, da nur so die Funktion unsere Policy sicher ist:

TagBedeutungWer pullt
v1.4.2Release, unveränderlichProd-Cluster
dev-<shortsha>Jeder Mainline-Build, eindeutigDev-Cluster (GitOps pinnt hierauf)
devGleitender Zeiger auf den neuesten Dev-BuildMenschen, manuelles Testen
pr-<nummer>-<shortsha>PR-/Branch-Builds, kurzlebigniemand bzw. Preview-Umgebungen

Bei jedem Mainline-Build werden beide Dev-Tags gepusht: zuerst dev-<shortsha>, danach das gleitende dev. Das eindeutige Tag ist die Referenz in Dev-Manifesten, damit jederzeit nachvollziehbar ist, was tatsächlich läuft, und ein Rollback eindeutig ist. Das gleitende Tag existiert nur für den manuellen Komfortfall.

Der Dev-Cluster darf das gleitende dev-Tag nicht deployen. In Kombination mit Node-Caches und imagePullPolicy ist sonst genau in den kritischen Momenten unklar, welcher Digest wirklich aktiv ist.

Promotion nach Prod

Eine Promotion ist ein Retag desselben Digests und kein Rebuild. Harbor kann ein zusätzliches Tag direkt über die eigene API an ein vorhandenes Artefakt hängen, ohne Docker-Daemon und ohne Layer-Transfer:

Terminal window
curl -fsS -u "$HARBOR_USER:$HARBOR_TOKEN" \
-X POST "https://harbor.dev.appcenter.de/api/v2.0/projects/[PROJEKCT_NAME_OR_ID]/repositories/[REPO_NAME]/artifacts/[ARTIFACT]/tags" \
-H "Content-Type: application/json" \
-d '{"name":"v1.4.2"}'

Damit ist das Prod-Image bit-identisch mit dem getesteten Image, und Harbor legt keine zusätzlichen Bytes an.

2. Tag-Immutability

Projektweit unter Projekt → Policy → Tag Immutability

  • Repositories: **
  • Tags: v*

Unveränderliche Artefakte können von einem Retention-Job nicht gelöscht und von einem Push nicht überschrieben werden. Dev-Tags bleiben bewusst veränderlich, damit das gleitende dev weiterhin umgehängt werden kann.

3. Retention-Policy

Nr.RepositoriesTagsRegel
1**v*immer behalten
2**dev,latestimmer behalten
3**dev-*die 15 zuletzt gepushten behalten
4**pr-*in den letzten 7 Tagen gepusht
5****in den letzten 30 Tagen gepullt

Der Schalter für Untagged Artifacts bleibt bei allen Regeln aus. Nur so werden verwaiste Manifeste eingesammelt.

Wozu die einzelnen Regeln dienen

Projektweit unter Projekt → Policy → Tag Retention

  • Regel 1 schützt Releases dauerhaft.
  • Regel 2 hält das Artefakt, auf das die gleitenden Tags aktuell zeigen. Ohne diese Regel kann genau das Image wegfallen, das der Dev-Cluster über dev auflöst. Das Tag-Feld akzeptiert kommaseparierte Muster, daher genügt eine Regel.
  • Regel 3 Dev-Builds desselben Repositories teilen sich die Base-Layer; 15 Tags kosten also deutlich weniger als 15 × Imagegröße.
  • Regel 4 begrenzt PR-Builds zeitlich.
  • Regel 5 Aktiv gepullte Images überleben unabhängig von Alter und Position in der Push-Liste.

Alle Regeln werden als OR angewendet.

4. Garbage Collection

Systemweit unter Administration → Clean Up → Garbage Collection

  • Zeitplan: wöchentlich, einige Stunden nach dem Retention-Job. Andernfalls räumt die GC die Löschungen der Vorwoche auf statt die des aktuellen Tages.
  • Option „delete untagged artifacts“ aktivieren.
  • Vor dem ersten GC-Lauf den aktuellen Storage-Verbrauch notieren. Solange die GC nicht gelaufen ist, verändert sich der Verbrauch nicht, das ist erwartetes Verhalten und kein Fehler in der Policy.

Laufzeit und Betriebsverhalten

Die GC arbeitet nach dem Mark-and-Sweep-Prinzip: erst werden alle referenzierten Blobs ermittelt, dann wird gelöscht. Die Laufzeit skaliert daher mit der Anzahl der Blobs und mit der Löschlatenz des Storage-Backends.

Relevante Randbedingungen:

  • Die GC läuft als exklusiver Job; Harbor stellt über einen Redis-Key sicher, dass nie zwei Läufe gleichzeitig stattfinden. Bleibt ein Lock stehen, wird der Job nicht mehr eingeplant, dann helfen ein Neustart der JobService-Pods und ggf. das Entfernen des Locks.
  • Die Anzahl der GC-Worker ist konfigurierbar (Wertebereich 1-5) und parallelisiert das Löschen.
  • Der Mark-Lauf ist ein Alles-oder-nichts-Durchgang. Auf sehr großen Instanzen mit hoher Churn-Rate kann die GC dadurch sehr langsam werden oder mit OOM abbrechen. Für unseren Umfang irrelevant, aber gut zu wissen, falls die Registry wächst.
  • Harbor nutzt ein Zeitfenster von 2 Stunden vor Beginn der Garbage Collection. Alle blobs mit einem Timestamp aus diesem Fenster bleiben von der GC für diesen Run verschont.
  • Während eines GC Runs kann Harbor weiterhin normal verwendet werden.
  • Blobs, die erst kurz vor dem Lauf hochgeladen wurden, überspringt die GC bewusst, um Kollisionen mit laufenden Pushes zu vermeiden. Ein gerade gelöschtes Image kann also kurzzeitig noch Speicher belegen.

5. CI-Ablauf

  1. Build auf Mainline.
  2. dev-<shortsha> pushen.
  3. Anschließend dev pushen. Die Reihenfolge ist wichtig, das gleitende Tag darf nie auf ein unvollständig hochgeladenes Artefakt zeigen.
  4. Release: nicht neu bauen, sondern den getesteten Digest als v* retaggen.

Falls die Pipeline auch auf PRs baut, den pr-*-Push zusätzlich einschränken (z. B. über ein Label oder nur für PRs gegen main). Andernfalls dominieren diese Tags die Tag-Liste trotz der 7-Tage-Regel.

Beispiel: Bitbucket Pipelines

Die Pipeline leitet das Tag aus dem Auslöser ab: Push auf main erzeugt Dev-Tags, ein Pull Request erzeugt ein pr-*-Tag, ein Git-Tag v* löst die Promotion aus.

Benötigte Repository-Variablen (Workspace oder Repository, Token als Secured):

VariableBeispielwert
REGISTRYharbor.dev.appcenter.de
REGISTRY_NAMESPACElibrary
REGISTRY_USERgituser
REGISTRY_TOKENRegistry User Passwort
image: atlassian/default-image:4
definitions:
services:
docker:
memory: 2048
steps:
- step: &dev-build
name: Dev-Build und Push
services: [docker]
script:
- export SHORT_SHA=$(echo "$BITBUCKET_COMMIT" | cut -c1-7)
- export IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/$BITBUCKET_REPO_SLUG"
- echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
- docker build -t "$IMAGE:dev-$SHORT_SHA" .
# Reihenfolge zwingend: erst das eindeutige Tag, dann der gleitende Zeiger
- docker push "$IMAGE:dev-$SHORT_SHA"
- docker tag "$IMAGE:dev-$SHORT_SHA" "$IMAGE:dev"
- docker push "$IMAGE:dev"
- step: &pr-build
name: PR-Build und Push
services: [docker]
script:
- |
if [ "$BITBUCKET_PR_DESTINATION_BRANCH" != "main" ]; then
echo "PR zielt nicht auf main - kein Push."
exit 0
fi
- export SHORT_SHA=$(echo "$BITBUCKET_COMMIT" | cut -c1-7)
- export IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/$BITBUCKET_REPO_SLUG"
- echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
- docker build -t "$IMAGE:pr-$BITBUCKET_PR_ID-$SHORT_SHA" .
- docker push "$IMAGE:pr-$BITBUCKET_PR_ID-$SHORT_SHA"
- step: &promote
name: Release promoten
script:
- |
set -eu
SHORT_SHA=$(echo "$BITBUCKET_COMMIT" | cut -c1-7)
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
IMAGES="
[[YOUR_PROJECT]]-frontend
[[YOUR_PROJECT]]-backend
[[YOUR_PROJECT]]-backend-migrate
"
for IMAGE_NAME in $IMAGES; do
IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/$IMAGE_NAME"
SRC="$IMAGE:dev-$SHORT_SHA"
DST="$IMAGE:$BITBUCKET_TAG"
echo "Promoting ${SRC} -> ${DST}"
docker pull "${SRC}"
docker tag "${SRC}" "${DST}"
docker push "${DST}"
done
pipelines:
branches:
main:
- step: *dev-build
pull-requests:
'**':
- step: *pr-build
tags:
'v*':
- step: *promote

Anmerkungen zum Beispiel:

  • Der gesamte Promote-Step steht in einem einzigen |-Block, damit set -eu für die vollständige Sequenz gilt und die mehrzeiligen Shell-Konstrukte lesbar bleiben.
  • SHORT_SHA wird über cut gebildet und nicht über die Bash-Substring-Syntax ${BITBUCKET_COMMIT:0:7}. Das läuft auch dann, wenn der Step auf ein Image mit reinem sh gesetzt wird.
  • Bitbucket kennt in der pull-requests-Sektion nur Muster für den Quell-Branch. Die Einschränkung auf PRs gegen main erfolgt deshalb im Skript über BITBUCKET_PR_DESTINATION_BRANCH.
  • Der docker-Service bekommt in den Build-Steps 2048 MB, weil der Standardwert für viele Builds knapp ist.

6. Cluster-Referenzen (GitOps)

  • Dev-Manifeste referenzieren dev-<shortsha>.
  • Prod-Manifeste referenzieren v*.
  • Dank eindeutiger Tags imagePullPolicy: IfNotPresent für caching

7. Quota

Um ein überlasten des Servers zu vermeiden wird eine maximale Quota gestgesetzt. Diese sollte abhängig von der Clusterspeicherkappazität sein.

Aktuelles Ziel: 80-100 GiB

Bekannte Fallstricke

SymptomUrsache
Retention läuft, Speicher bleibt gleichGC noch nicht gelaufen
Policy löscht nichtsRegeln als Löschbedingungen formuliert statt als Behaltensregeln (OR-Semantik)
Verwaiste Manifeste bleiben liegenSchalter für Untagged Artifacts nicht aktiviert
Dev-Cluster kann Image nicht mehr ziehenRegel 2 fehlt oder falsch geschrieben, gleitendes Tag zeigt ins Leere
GC-Job wird nicht eingeplantStale Lock in Redis; JobService-Pods neu starten