Neuigkeiten von trion.
Immer gut informiert.

GitLab CI: Zeitverzögertes Deployment

GitLab

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.

Basis GitLab CI Pipeline Beispiel für Container Images
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.

GitLab CI Release Pipeline Beispiel für HOTFIX git Tags
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.

git Note für das spätere Deployment
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 "[email protected]"
    - 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.

Zeitgesteuerter GitLab CI Job, der git Note nutzt
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.

Zeitgesteuerter Job in GitLab CI
Abbildung 1. Zeitgesteuerter Job in GitLab CI

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.

Feedback oder Fragen zu einem Artikel – per E-Mail an (info a-t trion.de) oder über unser Kontaktformular. Wir freuen uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.