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:
| Tag | Bedeutung | Wer pullt |
|---|---|---|
v1.4.2 | Release, unveränderlich | Prod-Cluster |
dev-<shortsha> | Jeder Mainline-Build, eindeutig | Dev-Cluster (GitOps pinnt hierauf) |
dev | Gleitender Zeiger auf den neuesten Dev-Build | Menschen, manuelles Testen |
pr-<nummer>-<shortsha> | PR-/Branch-Builds, kurzlebig | niemand 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 undimagePullPolicyist 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:
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. | Repositories | Tags | Regel |
|---|---|---|---|
| 1 | ** | v* | immer behalten |
| 2 | ** | dev,latest | immer 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
devauflö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
- Build auf Mainline.
dev-<shortsha>pushen.- Anschließend
devpushen. Die Reihenfolge ist wichtig, das gleitende Tag darf nie auf ein unvollständig hochgeladenes Artefakt zeigen. - 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):
| Variable | Beispielwert |
|---|---|
REGISTRY | harbor.dev.appcenter.de |
REGISTRY_NAMESPACE | library |
REGISTRY_USER | gituser |
REGISTRY_TOKEN | Registry 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: *promoteAnmerkungen zum Beispiel:
- Der gesamte Promote-Step steht in einem einzigen
|-Block, damitset -eufür die vollständige Sequenz gilt und die mehrzeiligen Shell-Konstrukte lesbar bleiben. SHORT_SHAwird übercutgebildet und nicht über die Bash-Substring-Syntax${BITBUCKET_COMMIT:0:7}. Das läuft auch dann, wenn der Step auf ein Image mit reinemshgesetzt wird.- Bitbucket kennt in der
pull-requests-Sektion nur Muster für den Quell-Branch. Die Einschränkung auf PRs gegenmainerfolgt deshalb im Skript überBITBUCKET_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: IfNotPresentfü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
| Symptom | Ursache |
|---|---|
| Retention läuft, Speicher bleibt gleich | GC noch nicht gelaufen |
| Policy löscht nichts | Regeln als Löschbedingungen formuliert statt als Behaltensregeln (OR-Semantik) |
| Verwaiste Manifeste bleiben liegen | Schalter für Untagged Artifacts nicht aktiviert |
| Dev-Cluster kann Image nicht mehr ziehen | Regel 2 fehlt oder falsch geschrieben, gleitendes Tag zeigt ins Leere |
| GC-Job wird nicht eingeplant | Stale Lock in Redis; JobService-Pods neu starten |