GitLab CI: Zeitverzögertes Deployment
GitLab hat mit GitLab CI eine angenehme Integration von Builds in die Repositoryverwaltung.
Damit lassen sich Pipelines umsetzen, die schnelles Feedback liefern und damit helfen, die Software- und Systemqualität zu verbessern.
Die GitLab CI Runner helfen dabei, den Build zu skalieren, damit es auch hier nicht zu unnötigen Verzögerungen kommt.
Doch was, wenn der Wunsch besteht, dass genau das passiert? Es soll bis zu einem gewissen Zeitpunkt verzögert werden, da die bisherige Anwendung noch Session-basiert ist, und zu einem Zeitpunkt mit der geringsten Wahrscheinlichkeit für Störungen das Deployment erfolgen soll.
Eine möglich Lösung besteht darin, mit mehreren Pipelines zu arbeiten.
Im Beispiel gehen wir zur Vereinfachung davon aus, dass die Deploymentumgebung mit Containern arbeitet und kontinuierlich auf eine neue Version des :latest Image prüft.
Eine Variante wäre die Verwendung von Deploy freezes, damit lassen sich Zeitfenster definieren, in denen einzelne Jobs oder ganze Pipelines nicht laufen.
Es kommt jedoch Anforderung dazu, dass es jederzeit möglich sein soll, HOTFIX Releases nach Produktion zu bringen.
Gepaar mit einem Design, bei dem GitLab nicht die Deployment Tasks selbst ausführen, sondern lediglich Images bauen soll, die auch unmittelbar in einer Abnahmeumgebung genutzt werden können, kann ein anderes Verfahren hilfreich sein.
Durch geschickte Nutzung verschiedener Image-Tags muss lediglich ein entsprechendes Image(-tag) ab einem bestimmten Zeitpunkt zur Verfügung gestellt werden. Und nur das wird dann in der produktive Umgebung aktiviert.
Die Umsetzung könne dann wie folgt aussehen:
Zunächst werden Images des Projektes unter /ci mit einem eindeutigen Buildhash und :latest bereitgestellt.
Diese können unmittelbar in einer Test- oder Abnahmeumgebung genutzt werden.
Produktion ist dadurch gekennzeichnet, dass es unter /release liegt.
Zur Illustration wird dort ebenfalls stets :latest verwendet.
stages:
- build
- push
- release
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: tcp://docker:2375
IMAGE_BASE: $CI_REGISTRY/$CI_PROJECT_PATH
CI_IMAGE: $IMAGE_BASE/ci
RELEASE_IMAGE: $IMAGE_BASE/release
default:
image: docker:cli
before_script:
- echo "Logging in to GitLab Container Registry ${CI_REGISTRY}..."
- docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
push-ci-image:
stage: push
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: never
- if: '$CI_COMMIT_TAG == null'
- if: '$CI_PIPELINE_SOURCE == "push"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"'
script:
- docker build -t "$CI_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker tag "$CI_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_IMAGE:latest"
- echo "Pushing image to GitLab Container Registry..."
- docker push "$CI_IMAGE:$CI_COMMIT_SHORT_SHA"
- docker push "$CI_IMAGE:latest"
Nun gilt es die HOTFIX Images direkt nach Produktion zu bringen.
Generell soll nur ein im git nachvollziehbar getaggter Stand nach Produktion gebracht werden, ganz gleich, ob es ein reguläres oder HOTFIX Release ist.
push-hotfix-release-image:
stage: release
rules:
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9])?-HOTFIX-RELEASE$/'
script:
- |
if [[ "$CI_COMMIT_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.?[0-9]*-HOTFIX-RELEASE$ ]]; then
VERSION="${CI_COMMIT_TAG%-HOTFIX-RELEASE}"
TARGET_PATH="$RELEASE_IMAGE"
else
echo "Tag '$CI_COMMIT_TAG' does not match hotfix pattern. Skipping."
exit 0
fi
echo "Building HOTFIX release image for $TARGET_PATH:$VERSION"
docker build -t "$TARGET_PATH:$VERSION" .
docker push "$TARGET_PATH:$VERSION"
docker tag "$TARGET_PATH:$VERSION" "$TARGET_PATH:latest"
docker push "$TARGET_PATH:latest"
Als Optimierung könnte man hier auf das bereits gebautet, getaggte und gepushte /ci-Image aus dem vorherigen Teil der Pipeline zurückgreifen und dies entsprechend unter den neuen Tags unter /release pushen.
Das reguläre, zeitgesteuerte Release, wird analog zu einem HOTFIX gebaut und gepusht, allerdings wird dabei :latest ausgelassen, so dass das Release zwar erfolgt ist, aber nicht in Produktion herangezogen wird.
Dafür wird jetzt im git vermerkt, dass für das aktuelle Datum genau dieses Container-Image-Tag verwendet werden soll.
Da GitLab CI selbst keinen State halten kann, dient eine git Notiz als entsprechendes Hilfsmittel.
prepare-release:
stage: release
rules:
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+-(RELEASE)$/'
script:
- |
if [[ "$CI_COMMIT_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+-RELEASE$ ]]; then
VERSION="${CI_COMMIT_TAG%-RELEASE}"
else
echo "Tag '$CI_COMMIT_TAG' does not match release pattern. Skipping."
exit 0
fi
- git config user.name "GitLab CI Note Bot"
- git config user.email "ci@gitlab.local"
- CURRENT_DATE=$(date +%Y-%m-%d)
- NOTE_CONTENT="DATE=$CURRENT_DATE TAG=$VERSION"
- git notes show $CI_COMMIT_SHA || true
- git notes add -m "$NOTE_CONTENT" $CI_COMMIT_SHA
- "echo Added note: $NOTE_CONTENT to commit $CI_COMMIT_SHA"
- git push origin refs/notes/commits
Der zeitgesteuerte Job verwendet nun diese git Notiz und taggt das so vorgemerkte Image für Produktion.
activate-release-image:
stage: release
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- |
git fetch --tags # Fetch tags
git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*'
git fetch origin
COMMIT_SHAS_ORDERED=$(
git notes list | awk '{print $2}' | \
xargs git log --no-walk --format="%H|%ct" | \
sort -t'|' -k2,2
)
COMMIT_SHAS=$(echo "$COMMIT_SHAS_ORDERED" | awk -F'|' '{print $1}' | tac)
CURRENT_DATE=$(date +%Y-%m-%d)
TARGET_TAG=""
for COMMIT_SHA in $COMMIT_SHAS; do
NOTE_CONTENT=$(git notes show $COMMIT_SHA 2>/dev/null || true)
if echo "$NOTE_CONTENT" | grep -q "DATE=$CURRENT_DATE"; then
TARGET_TAG=$(echo "$NOTE_CONTENT" | grep "TAG=" | tail -1 | sed 's/.*TAG=//' | tr -d '[:space:]')
if [ -n "$TARGET_TAG" ]; then
echo "Extracted tag for today: $TARGET_TAG"
break
fi
fi
done
if [ -z "$TARGET_TAG" ]; then
echo "No Git Note found matching today's date ($CURRENT_DATE). Exiting."
exit 0
fi
SOURCE_PATH="$IMAGE_BASE/release:$TARGET_TAG"
TARGET_PATH="$IMAGE_BASE/release:latest"
echo "Using image $SOURCE_PATH for $TARGET_PATH"
docker pull "$SOURCE_PATH"
docker tag "$SOURCE_PATH" "$TARGET_PATH"
docker push "$TARGET_PATH"
Zeitgesteuerte Jobs werden in GitLab CI über die Pipeline-Konfiguration angelegt. Alternativ kann das auch per GitLab CI API geschehen, jedoch nicht als Teil des Pipeline YAMLs.
Damit ist eine mögliche Umsetzung für Build und Release geschaffen. Das Deployment kann anschließend auf unterschiedliche Weise gelöst werden, z.B. mit Docker oder Kubernetes.
Zu den Themen Kubernetes, Docker und Cloudarchitektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.