Zum Hauptinhalt springen

CI / CD

Grundaufbau

CI/CD Aufbau

Begriffe

Continuous Integration

  • Ziel: Steigerung der Software-Qualität über permanente Integration der einzelnen Bestandteile
  • früher
    • Erstellung der Software in sehr grossen Zeitabständen kurz vor Auslieferung
  • Continuous Integration
    • Erstellung und Testung in kleinen Zyklen
    • → frühzeitige Erkennung von Integrationsproblemen und fehlerhaften Tests
    • kleine Deltas zwischen einzelnen Builds → leichteres Finden und Beheben von Fehlern

Continuous Delivery

  • Ziel: Codebasis, die jederzeit in Produktivumgebung bereitgestellt werden kann
  • Freigabe produktionsreife Build an Code-Repository
  • Eigenschaften
    • mehr inkrementelle Updates für Anwendungen in Produktion → Reduzierung von Kosten, Zeitaufwand und Risiko bei Bereitstellung von Änderungen
      • Beispiel: “Nightly” Builds
    • wichtig für kontinuierliche Bereitstellung: unkomplizierter und wiederholbarer Bereitstellungsprozess
  • Bereitstellung eines Artefaktes
  • Beispiele
    • Firefox Nightly Release
    • Chrome Canary

Continuous Deployment

  • automatische Auslieferung der Software auf Produktiv-Infrastruktur
  • in Praxis: App-Änderungen eines Entwicklers können binnen weniger Minuten nach Erstellung live gehen (wenn sie die automatischen Tests bestehen)

Build

  • Aktionen
    • bezieht Quellcode aus Repository
    • stellt Verknüpfungen zu relevanten Bibliotheken, Modulen und Abhängigkeiten her
    • kompiliert/baut alle Komponenten zu ausführbaren Applikation
  • Übersetzung der Applikation in verpackte oder einsatzfähige Ausführumgebung
    • z. B. in VM oder Container

Test

  • können während oder nach erfolgreichem Abschluss des Builds stattfinden
  • Arten
    • Unit-Tests: Validierung von neuen Merkmalen und Funktionen
    • Regressionstests: stellen sicher, dass Build mit anderen Anwendungen oder Diensten funktioniert
    • Benutzerakzeptanztests: prüfen, ob Benutzer neuen Merkmale und Funktionen wie vorgesehen nutzen können

GitLab-CI

  • in GitLab integriert
  • orchestriert durch GitLab Runners
  • ausgeführt in Executors
    • Docker (bevorzugt)
    • Shell (direkt auf Servre)
    • VirtualBox / Parallels

GitLab Webseite

  • Pipeline ansehen
    • GitLab-Repository > Build >Jobs > #123456: my-job
    • möglicher Output:
      Running with gitlab-runner 18.0.3 (4e717029)
      on docker+machine jZ893qmd, system ID: r_KX7VbDpZ018a
      Resolving secrets
      Preparing the "docker+machine" executor
      00:02
      Using Docker executor with image alpine:3 ...
      Using effective pull policy of [always] for container alpine:3
      Pulling docker image alpine:3 ...
      Using docker image sha256:cea2ff433c610f5363017404ce989632e12b953114fefc6f597a58e813c15d61 for alpine:3 with digest alpine@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 ...
      Preparing environment
      00:01
      Using effective pull policy of [always] for container sha256:2e7190a90b3766c9129a0bd54a584233df5c8cd7306a49eb59e4d7f81a29eede
      Running on runner-jz893qmd-project-15290-concurrent-0 via runner-jz893qmd-gitlab-1750145039-82fd3f38...
      Getting source from Git repository
      00:01
      Fetching changes with git depth set to 20...
      Initialized empty Git repository in /builds/uek/210/sa2023_210_3/ssteil/first-pipeline/.git/
      Created fresh repository.
      Checking out fc43b596 as detached HEAD (ref is main)...
      Skipping Git submodules setup
      Executing "step_script" stage of the job script
      00:00
      Using effective pull policy of [always] for container alpine:3
      Using docker image sha256:cea2ff433c610f5363017404ce989632e12b953114fefc6f597a58e813c15d61 for alpine:3 with digest alpine@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 ...
      $ echo "This is my first GitLab-CI pipeline"
      This is my first GitLab-CI pipeline
      $ pwd
      /builds/uek/210/sa2023_210_3/ssteil/first-pipeline
      $ ls -al
      total 24
      drwxrwxrwx 3 root root 4096 Jun 17 07:41 .
      drwxrwxrwx 4 root root 4096 Jun 17 07:41 ..
      drwxrwxrwx 6 root root 4096 Jun 17 07:41 .git
      -rw-rw-rw- 1 root root 162 Jun 17 07:41 .gitlab-ci.yml
      -rw-rw-rw- 1 root root 6178 Jun 17 07:41 README.md
      $ uname -a
      Linux runner-jz893qmd-project-15290-concurrent-0 6.8.0-1030-gcp #32~22.04.1-Ubuntu SMP Tue Apr 29 23:17:09 UTC 2025 x86_64 Linux
      Cleaning up project directory and file based variables
      00:01
      Job succeeded

Konfiguration: .gitlab-ci.yml

  • GitLab Docs - CI/CD YAML syntax reference
  • mit .gitlab-ci.yml im Repository
  • Aufteilung in Stages → enthalten Jobs
    • getriggert durch Rules
  • definiert Ausführungsumgebung (z. B. Docker-Image)
  • definiert, welche Resultate (Tests, Binaries, …) als Artefakte gespeichert werden sollen

Aufbau:

  • stages
    • Gruppierung von Jobs

Rules (GitLab Docs - Specify when jobs run with rules)

Beispiel:

stages:
- build
- test
- deliver
- deploy

default:
image: alpine:3

job1:
stage: build
script:
- echo "Executed on all branches"

job2:
stage: build
rules:
- if: $CI_COMMIT_BRANCH == 'production'
script:
- echo "Executed only on the production branch"

job3:
stage: build
rules:
- if: $CI_COMMIT_BRANCH == 'main' || $CI_COMMIT_BRANCH == 'staging'
script:
- echo "Executed on the branches main and staging"

Pipeline mit Azure

  1. GitLab-Projekt erstellen

    cd existing_folder
    git init --initial-branch=main
    git remote add origin <path-to-gitlab-repository>
    git add .
    git commit -m "Initial commit"
    git push -u origin main
  2. Umgebungsvariablen in CI/CD-Einstellungen definieren

    • AZURE_CLI_APPID
    • AZURE_CLI_PASSWORD
    • AZURE_CLI_TENANT
  3. Azure Ressourcen erstellen, welche nicht im .gitlab-ci.yml erstellt werden (siehe Spring App mit Docker Compose)

    az provider register --namespace Microsoft.ContainerRegistry
    az acr create --resource-group bbcssteilContainerRegistryRG --name bbcssteilcr --sku Basic

    Docker Engine (über Docker Desktop) starten

    az acr login --name bbcssteilcr
    az acr show --name bbcssteilcr --resource-group bbcssteilContainerRegistryRG --query id
    az ad sp create-for-rbac --scopes /subscriptions/c912ae16-0c3b-47bc-a267-743415032e19/resourceGroups/bbcssteilContainerRegistryRG/providers/Microsoft.ContainerRegistry/registries/bbcssteilcr --role 'Owner'
  4. Beispiel für .gitlab-ci.yml

    variables:
    RG_NAME:
    value: "uek-210"
    LOCATION:
    value: "switzerlandnorth"
    CONTAINER_REGISTRY:
    value: "bbcssteilcr"
    IMAGE_NAME:
    value: studle

    stages:
    - build
    - deploy

    build:
    stage: build
    image: docker:latest
    services:
    - docker:dind
    script:
    - docker login -u $AZURE_CLI_APPID -p $AZURE_CLI_PASSWORD $CONTAINER_REGISTRY.azurecr.io
    - docker build -t studle .
    - docker tag studle $CONTAINER_REGISTRY.azurecr.io/studle:latest
    - docker push $CONTAINER_REGISTRY.azurecr.io/studle:latest
    - echo "docker container studle was built and pushed"
    - echo "docker container mariadb was pushed"

    deploy:
    stage: deploy
    image: mcr.microsoft.com/azure-cli:latest
    script:
    - az login --service-principal -u ${AZURE_CLI_APPID} -p ${AZURE_CLI_PASSWORD} --tenant ${AZURE_CLI_TENANT}
    - az provider register --namespace Microsoft.ContainerRegistry
    - az container create --resource-group bbcssteilContainerRegistryRG --file deploy-aci.yaml --registry-login-server ${CONTAINER_REGISTRY}.azurecr.io --registry-username ${AZURE_CLI_APPID} --registry-password ${AZURE_CLI_PASSWORD}
  5. Committen und auf GitLab pushen

Pipeline mit Azure und BICEP

  1. Bicep-Datei erstellen

    • deploy-studle-app.bicep

      param name string = 'azure-studle'
      param location string = resourceGroup().location
      param image string
      param registry string
      param username string

      @secure()
      param password string

      resource containerGroups_azure_studle_name_resource 'Microsoft.ContainerInstance/containerGroups@2022-10-01-preview' = {
      name: name
      location: location
      tags: {
      'docker-compose-application': 'docker-compose-application'
      }
      properties: {
      sku: 'Standard'
      containers: [
      {
      name: 'app'
      properties: {
      image: '${registry}/${image}:latest'
      ports: [
      {
      protocol: 'TCP'
      port: 80
      }
      ]
      environmentVariables: [
      {
      name: 'SERVER_PORT'
      value: '80'
      }
      {
      name: 'SPRING_DATASOURCE_URL'
      value: 'jdbc:mariadb://db:3306/studle'
      }
      ]
      resources: {
      requests: {
      memoryInGB: 1
      cpu: 1
      }
      limits: {
      memoryInGB: 1
      cpu: 1
      }
      }
      }
      }
      {
      name: 'db'
      properties: {
      image: 'mariadb:10.9'
      ports: []
      environmentVariables: [
      {
      name: 'MARIADB_ROOT_PASSWORD'
      value: 'password'
      }
      {
      name: 'MARIADB_DATABASE'
      value: 'studle'
      }
      ]
      resources: {
      requests: {
      memoryInGB: 1
      cpu: 1
      }
      limits: {
      memoryInGB: 1
      cpu: 1
      }
      }
      }
      }
      {
      name: 'aci--dns--sidecar'
      properties: {
      image: 'docker/aci-hostnames-sidecar:1.0'
      command: [
      '/hosts'
      'app'
      'db'
      ]

      ports: []
      environmentVariables: []
      resources: {
      requests: {
      memoryInGB: '0.1'
      cpu: '0.01'
      }
      }
      }
      }
      ]
      initContainers: []
      imageRegistryCredentials: [
      {
      server: registry
      username: username
      password: password
      }
      ]
      restartPolicy: 'Always'
      ipAddress: {
      ports: [
      {
      protocol: 'TCP'
      port: 80
      }
      ]
      type: 'Public'
      }
      osType: 'Linux'
      }
      }
  2. .gitlab-ci.yml anpassen

    • Es werden dabei zwei zusätzliche Zeilen benötigt (unteren zwei):

        script:
      - az login --service-principal -u ${AZURE_CLI_APPID} -p ${AZURE_CLI_PASSWORD} --tenant ${AZURE_CLI_TENANT}
      - az provider register --name Microsoft.ContainerInstance
      - az deployment group create --resource-group bbcssteilContainerRegistryRG --template-file deploy-studle-app.bicep --parameters image=studle registry=$CONTAINER_REGISTRY.azurecr.io username=$AZURE_CLI_APPID password=$AZURE_CLI_PASSWORD
    variables:
    RG_NAME:
    value: "uek-210"
    LOCATION:
    value: "switzerlandnorth"
    CONTAINER_REGISTRY:
    value: "bbcssteilcr"
    IMAGE_NAME:
    value: studle

    stages:
    - build
    - deploy

    build:
    stage: build
    image: docker:latest
    services:
    - docker:dind
    script:
    - docker login -u $AZURE_CLI_APPID -p $AZURE_CLI_PASSWORD $CONTAINER_REGISTRY.azurecr.io
    - docker build -t studle .
    - docker tag studle $CONTAINER_REGISTRY.azurecr.io/studle:latest
    - docker push $CONTAINER_REGISTRY.azurecr.io/studle:latest
    - echo "docker container studle was built and pushed"
    - echo "docker container mariadb was pushed"

    deploy:
    stage: deploy
    image: mcr.microsoft.com/azure-cli:latest
    script:
    - az login --service-principal -u ${AZURE_CLI_APPID} -p ${AZURE_CLI_PASSWORD} --tenant ${AZURE_CLI_TENANT}
    - az provider register --name Microsoft.ContainerInstance
    - az deployment group create --resource-group bbcssteilContainerRegistryRG --template-file deploy-studle-app.bicep --parameters image=studle registry=$CONTAINER_REGISTRY.azurecr.io username=$AZURE_CLI_APPID password=$AZURE_CLI_PASSWORD
  3. Repository committen und pushen

Infrastructure as Code mit Azure

  1. in GitLab neues Projekt ohne README.md erstellen

  2. existierenden Ordner ins GitLab-Repository pushen (siehe in GitLab-Projekt unter Push an existing folder)

    cd existing_folder
    git init --initial-branch=main
    git remote add origin <path-to-gitlab-repository>
    git add .
    git commit -m "Initial commit"
    git push -u origin main
  3. Service Principal erstellen (siehe az ad sp - Service Principal)

    az ad sp create-for-rbac --scopes /subscriptions/c912ae16-0c3b-47bc-a267-743415032e19 --role 'Owner'

    Output:

    {
    "appId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "displayName": "azure-cli-2025-06-17-12-07-29",
    "password": "9Ailu~u.2lJe2jk2f5=tQEA8o.fhq4_dQrHncIo2",
    "tenant": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
  4. GitLab-Variablen erstellen

    • AZURE_CLI_APPID
    • AZURE_CLI_PASSWORD
    • AZURE_CLI_TENANT
  5. .gitlab-ci.yml erstellen

    variables:
    RG_NAME:
    value: "uek-210"
    LOCATION:
    value: "switzerlandnorth"
    CONTAINER_REGISTRY:
    value: "ssteilregistry"
    IMAGE_NAME:
    value: studle

    stages:
    - build
    - deploy

    build:
    stage: build
    image: docker:latest
    services:
    - docker:dind
    script:
    - docker login -u $AZURE_CLI_APPID -p $AZURE_CLI_PASSWORD $CONTAINER_REGISTRY.azurecr.io
    - docker build -t studle .
    - docker tag studle $CONTAINER_REGISTRY.azurecr.io/studle:latest
    - docker push $CONTAINER_REGISTRY.azurecr.io/studle:latest
    - echo "docker container studle was built and pushed"
    - echo "docker container mariadb was pushed"

    deploy:
    stage: deploy
    image: mcr.microsoft.com/azure-cli:latest
    script:
    - az login --service-principal -u ${AZURE_CLI_APPID} -p ${AZURE_CLI_PASSWORD} --tenant ${AZURE_CLI_TENANT}
    - az provider register --name Microsoft.ContainerInstance
    - az deployment group create --resource-group bbcssteilContainerRegistryRG --template-file deploy-studle-app.bicep --parameters image=studle registry=$CONTAINER_REGISTRY.azurecr.io username=$AZURE_CLI_APPID password=$AZURE_CLI_PASSWORD
  6. Bicep-Datei erstellen

    • deploy-studle-app.bicep

      param name string = 'azure-studle'
      param location string = resourceGroup().location
      param image string
      param registry string
      param username string

      @secure()
      param password string

      resource containerGroups_azure_studle_name_resource 'Microsoft.ContainerInstance/containerGroups@2022-10-01-preview' = {
      name: name
      location: location
      tags: {
      'docker-compose-application': 'docker-compose-application'
      }
      properties: {
      sku: 'Standard'
      containers: [
      {
      name: 'app'
      properties: {
      image: '${registry}/${image}:latest'
      ports: [
      {
      protocol: 'TCP'
      port: 80
      }
      ]
      environmentVariables: [
      {
      name: 'SERVER_PORT'
      value: '80'
      }
      {
      name: 'SPRING_DATASOURCE_URL'
      value: 'jdbc:mariadb://db:3306/studle'
      }
      ]
      resources: {
      requests: {
      memoryInGB: 1
      cpu: 1
      }
      limits: {
      memoryInGB: 1
      cpu: 1
      }
      }
      }
      }
      {
      name: 'db'
      properties: {
      image: 'mariadb:10.9'
      ports: []
      environmentVariables: [
      {
      name: 'MARIADB_ROOT_PASSWORD'
      value: 'password'
      }
      {
      name: 'MARIADB_DATABASE'
      value: 'studle'
      }
      ]
      resources: {
      requests: {
      memoryInGB: 1
      cpu: 1
      }
      limits: {
      memoryInGB: 1
      cpu: 1
      }
      }
      }
      }
      {
      name: 'aci--dns--sidecar'
      properties: {
      image: 'docker/aci-hostnames-sidecar:1.0'
      command: [
      '/hosts'
      'app'
      'db'
      ]

      ports: []
      environmentVariables: []
      resources: {
      requests: {
      memoryInGB: 1
      cpu: 1
      }
      }
      }
      }
      ]
      initContainers: []
      imageRegistryCredentials: [
      {
      server: registry
      username: username
      password: password
      }
      ]
      restartPolicy: 'Always'
      ipAddress: {
      ports: [
      {
      protocol: 'TCP'
      port: 80
      }
      ]
      type: 'Public'
      }
      osType: 'Linux'
      }
      }
  7. Repository committen und pushen