Tag obrazu to wersja: dlaczego nigdy :latest i jak wycofać wdrożenie jednym commitem
Deployment wskazujący app:latest działa do pierwszego restartu poda na innym węźle. Jak tagować obrazy wersją i skrótem commita, przypinać je po digest, chronić tagi w rejestrze i sprzątać rejestr bez ryzyka.
Przewodnik Od commita do produkcji Część 3 z 6
- Jeden szablon pipeline'u dla wszystkich projektów: include, wersje i kontrola dryfu
- Budowanie obrazów bez demona Dockera: kaniko, Buildah i BuildKit w GitLab CI
- Tag obrazu to wersja: dlaczego nigdy :latest i jak wycofać wdrożenie jednym commitem
- Sekrety w automatyzacji i CI/CD: jedno źródło, krótkie poświadczenia, zero wartości w Git
- GitOps a skrypt deployu: pętla uzgadniania, dryf i wycofanie na przykładzie Argo CD
- Kopie w Kubernetes: co zabezpieczać, gdy klaster da się odtworzyć z repozytorium
Aplikacja działała tydzień bez zmian. W nocy węzeł został zrestartowany po aktualizacji systemu, pod uruchomił się na innym węźle i pobrał obraz app:latest, który od wczoraj wskazywał na niewydaną wersję z gałęzi głównej. Rano aplikacja zachowuje się inaczej, w repozytorium wdrożeń nie ma żadnego nowego commita, a na pytanie „do czego wracamy” nie ma odpowiedzi, bo poprzedni latest już nie istnieje.
Tag obrazu powinien być wersją: niezmiennym identyfikatorem jednego builda. Wtedy manifest w Git mówi dokładnie, co działa, różne węzły uruchamiają ten sam kod, a wycofanie jest zmianą jednej linii.
Dlaczego nigdy :latest
latest nie znaczy „najnowsza stabilna wersja”. To zwykły tag, który narzędzia nadają domyślnie, gdy nie podano innego, i który można w każdej chwili przestawić na dowolny obraz. Z tego wynikają trzy problemy:
- Brak powtarzalności. Ten sam manifest na dwóch węzłach może uruchomić dwa różne obrazy, zależnie od tego, kiedy który węzeł go pobrał.
- Brak wycofania. Nie ma zapisu, na jaki obraz wskazywał
latesttydzień temu, więc nie ma do czego wrócić. - Zależność od rejestru przy każdym starcie poda. Kubernetes ustawia domyślną politykę pobierania na podstawie tagu.
| Odwołanie w manifeście | Domyślne imagePullPolicy | Skutek |
|---|---|---|
app:latest | Always | każdy start kontenera odpytuje rejestr; gdy rejestr nie odpowiada, pod nie wystartuje |
app (bez tagu) | Always | jak wyżej, bo brak tagu oznacza latest |
app:1.4.2-3f9c2a1b | IfNotPresent | obraz z węzła, jeśli już tam jest |
app@sha256:… | IfNotPresent | obraz identyfikowany treścią |
Przy Always kubelet przy każdym uruchomieniu kontenera sprawdza w rejestrze, na jaki digest wskazuje tag. Awaria rejestru albo wyczerpany limit zapytań oznacza wtedy, że restart poda kończy się błędem ErrImagePull, nawet jeśli obraz leży na węźle.
Schemat tagów: wersja i skrót commita
Dobry tag odpowiada na dwa pytania: która to wersja i z jakiego kodu powstała. Format 1.4.2-3f9c2a1b łączy wersję semantyczną (kolejność i zgodność czytelne dla ludzi) ze skrótem commita (jednoznaczne powiązanie z kodem). W GitLab CI wersja pochodzi z tagu Git v1.4.2, a skrót ze zmiennej CI_COMMIT_SHORT_SHA, która ma osiem znaków.
build-image:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.24.0-debug
entrypoint: [""]
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
script:
# logowanie do rejestru jak w poprzedniej części serii
- VERSION="${CI_COMMIT_TAG#v}"
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${VERSION}-${CI_COMMIT_SHORT_SHA}"
--destination "${CI_REGISTRY_IMAGE}:${VERSION}"
--digest-file "${CI_PROJECT_DIR}/image-digest.txt"
artifacts:
paths: [image-digest.txt]
Buildy z gałęzi roboczych, których nikt nie wdraża na produkcję, dostają tylko tag sha-3f9c2a1b. Dzięki temu po samym tagu widać, czy obraz jest wydaniem, a polityka sprzątania może je łatwo rozróżnić. Plik z --digest-file zawiera digest wypchniętego obrazu i przydaje się w kolejnym zadaniu, które zmienia manifest.
Niezmienność tagów w rejestrze
Schemat tagów nic nie daje, jeśli ktoś może wypchnąć inny obraz pod istniejący tag. Rejestry mają do tego mechanizmy:
- GitLab: reguły ochrony tagów kontenerów (dostępne także w wersji bezpłatnej) ograniczają wypychanie i usuwanie tagów pasujących do wzorca do wybranych ról. Pełna niezmienność, niezależna od roli, jest w planie Ultimate.
- Harbor: reguły niezmienności tagów w ustawieniach projektu.
- Amazon ECR: ustawienie repozytorium
imageTagMutabilitynaIMMUTABLE.
Niezależnie od rejestru pipeline może sam odmówić nadpisania. Fragment zadania uruchamianego przed budowaniem, w obrazie zawierającym skopeo:
TAG="${CI_COMMIT_TAG#v}-${CI_COMMIT_SHORT_SHA}"
if skopeo inspect --creds "${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD}" \
"docker://${CI_REGISTRY_IMAGE}:${TAG}" > /dev/null 2>&1; then
echo "Tag ${TAG} już istnieje w rejestrze, przerywam."
exit 1
fi
Przypinanie po digest
Digest (sha256:…) to skrót treści manifestu obrazu. Tag można przestawić, digestu nie: inny obraz ma inny digest. Kubernetes akceptuje zapis z tagiem i digestem jednocześnie; obraz jest wtedy identyfikowany po digeście, a tag zostaje czytelną informacją dla ludzi.
spec:
template:
spec:
containers:
- name: app
image: registry.example.com/shop/app:1.4.2-3f9c2a1b@sha256:6f1c9e…
Digest odczytuje się bez pobierania obrazu:
skopeo inspect --format '{{.Digest}}' docker://registry.example.com/shop/app:1.4.2-3f9c2a1b
Przypinanie po digest jest obowiązkowe tam, gdzie nie kontrolujesz niezmienności tagów: obrazy bazowe, obrazy narzędzi w CI, obrazy z publicznych chartów Helm. Dla własnych obrazów w rejestrze z ochroną tagów wystarczy tag z wersją i commitem; digest dodaje się dla usług najważniejszych.
Wycofanie: revert manifestu
Gdy wdrożenie odbywa się przez GitOps, każda zmiana wersji jest commitem w repozytorium manifestów. Wycofanie to cofnięcie tego commita:
git log --oneline -3 -- apps/shop/overlays/prod/
# a91c0de shop: 1.4.3-7d2e91f0
# 5be2f13 shop: 1.4.2-3f9c2a1b
git revert --no-edit a91c0de
git push origin main
Kontroler GitOps zobaczy zmianę i przywróci poprzedni obraz. Ponieważ tag 1.4.2-3f9c2a1b nadal istnieje i wskazuje na ten sam obraz, wycofanie przywraca dokładnie to, co działało wcześniej.
Polityka sprzątania rejestru
Niezmienne tagi oznaczają, że rejestr rośnie. Sprzątanie musi rozróżniać wydania od buildów roboczych. W GitLabie służy do tego polityka czyszczenia tagów w ustawieniach rejestru kontenerów projektu (Settings → Packages and registries):
| Grupa tagów | Wzorzec | Zasada |
|---|---|---|
| Wydania | \d+\.\d+\.\d+(-[0-9a-f]{8})? | zachowywane zawsze |
| Buildy robocze | sha-[0-9a-f]{8} | usuwane po 14 dniach, 10 ostatnich zostaje |
| Pamięć podręczna warstw | osobne repozytorium cache | odtwarzalna, czas życia jak w narzędziu budującym |
GitLab sam otacza wzorce kotwicami początku i końca, więc w polu nie wpisuje się ^ ani $. Usunięcie tagów nie zwalnia od razu miejsca na dysku: w instancji samodzielnie utrzymywanej robi to dopiero odśmiecanie rejestru, uruchamiane w oknie serwisowym. Zanim reguła zacznie działać, trzeba porównać ją z listą obrazów uruchomionych w klastrach: obraz, który działa, nie może zniknąć z rejestru, bo następny restart poda na innym węźle skończy się błędem pobierania.
Sprawdź u siebie
Obrazy z tagiem latest albo bez tagu, uruchomione w klastrze:
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.spec.containers[*].image}{"\n"}{end}' \
| awk '{for (i = 2; i <= NF; i++) { n = split($i, p, "/"); if (p[n] !~ /[:@]/ || p[n] ~ /:latest$/) print $1, $i }}' \
| sort -u
# Dobrze: pusty wynik. Każda linia to przestrzeń nazw i obraz do poprawy.
Obraz z manifestu i digest, który faktycznie działa:
kubectl get pods -n shop -o custom-columns='POD:.metadata.name,IMAGE:.spec.containers[*].image,IMAGEID:.status.containerStatuses[*].imageID'
# Dobrze: wszystkie repliki mają ten sam imageID.
# Źle: różne digesty dla tego samego tagu, czyli tag był nadpisany między pobraniami.
Digest i data utworzenia obrazu w rejestrze:
skopeo inspect --format '{{.Digest}} {{.Created}}' docker://registry.example.com/shop/app:1.4.2-3f9c2a1b
# Dla obrazu jednoarchitekturowego digest powinien zgadzać się z końcówką imageID z poprzedniego polecenia.
Ustawienia polityki sprzątania projektu w GitLabie:
glab api "projects/shop%2Fapp" | jq '.container_expiration_policy'
# Dobrze: enabled true i name_regex_keep obejmujący tagi wydań.
# Źle: enabled false przy rosnącym rejestrze albo brak wzorca zachowania wydań.