| von Christoph Pieper

Automatisierung mit Azure DevOps am Beispiel von Databricks

Worum geht´s?

Databricks, als Unified Data Analytics Plattform, bietet eine Palette an Tools, auf welche Entwickler und Data Scientists für ihre tägliche Arbeit zurückgreifen können: Notebooks dienen als Oberfläche für ausführbaren Code, mit Delta Live Tables und Databricks Workflows lassen sich ganzheitliche Datenpipelines bauen und mit dem Hinzukommen von Databricks Repositories wurde Usern eine einfache Möglichkeit der Versionskontrolle innerhalb der Plattform gegeben.

Neben all diesen, sowie weiteren Features ist oftmals jedoch die Nutzung weiterer Applikationen und Methodiken notwendig, um die Tool-Palette zu erweitern. In IT-Projekten wird immer häufiger auf agile Methoden zurückgegriffen. Darauf basierende DevOps-Praktiken sowie Continuous Integration und Continuous Delivery/Deployment (CI/CD) verschaffen zahlreichen Projektteams sowie Endnutzern einer Anwendung in vielen Fällen großen Mehrwert.

Doch was versteht man unter DevOps und was genau ist eine CI/CD-Pipeline? Dieser Beitrag zeigt, wie mit Hilfe von Azure DevOps eine erste grundlegende CI/CD-Pipeline für Workflows in Azure Databricks aufgesetzt werden kann, und soll verdeutlichen, welches Potenzial DevOps und im Speziellen CI/CD in IT-Projekten haben können.

Was beschreibt DevOps?

DevOps

DevOps umfasst Tools und Praktiken, mit welchen Software-Applikationen schneller und verlässlicher an Endnutzer herangetragen werden können. Eine agile Arbeitsweise zielt darauf ab, die Entwicklung (Development) und den Betrieb (Operations) von Software näher zusammen zu führen. Abbildung 1 stellt die verschiedenen Phasen dar, welche in einer Art Endlosschleife den DevOps-Lebenszyklus widerspiegeln. Durch eine engere Zusammenarbeit zwischen Teams lassen sich Arbeitspakete effizienter und vor allem iterativ bereitstellen. Feedback eines jeden Beteiligten kann schneller aufgenommen und im Entwicklungsprozess berücksichtigt werden. Entwicklung und Betrieb arbeiten somit über den gesamten Lebenszyklus von Software-Applikationen zusammen.

Da DevOps einen großen Fokus darauf legt, die Arbeitsweise von Projektteams so effizient wie möglich zu gestalten, hat die Automatisierung von Prozessen einen großen Stellenwert in der DevOps-Philosophie. Werden Build-, Testing-, Deployment- und sonstige Schritte innerhalb der Software-Entwicklung automatisiert, so verkürzt sich der Entwicklungszyklus einer Anwendung und Ressourcen werden eingespart. Zudem können Fehler und Probleme schneller erkannt und behoben oder gar vermieden werden, da menschlichem Versagen, wie das Vergessen zentraler Schritte in der Entwicklung, vorgebeugt wird. Hierbei setzt DevOps auf CI/CD.

Welchen Nutzen bietet CI/CD in der Software-Entwicklung?

Mit Hilfe von CI/CD lassen sich Arbeitsschritte, welche während der Entwicklung von Software oftmals noch manuell durchgeführt werden, automatisieren. Neuer Code und neue Features können automatisch und regelmäßig getestet, und Arbeitsergebnisse ohne händisches Zutun in der gewünschten Ziel-Umgebung bereitgestellt werden. Eine CI/CD-Pipeline kann individuell gestaltet werden, indem man Schritte und Aufgaben definiert, welche die Pipeline für jedes noch so kleine neue Feature automatisiert durchführt. Die Nutzung einer CI/CD-Pipeline ermöglicht es somit, Arbeitsergebnisse iterativ, in kleineren Paketen und vor allem schneller bereitzustellen. Gleichzeitig können sich Entwickler-Teams anderen Aufgaben widmen und der Mehrwert sowohl für Projektteilnehmer als auch für den Endnutzer wird gesteigert. Da ein Praxis-Beispiel oftmals anschaulicher ist, wird im Folgenden näher erläutert, wie eine solche Pipeline konkret aussehen kann, und wie man sie für einen Azure Databricks-Usecase erstellt.

Wie lässt sich eine CI/CD-Pipeline für Azure Databricks aufsetzen?

Im Folgenden wird aufgeführt, wie Nutzer von Azure Databricks mit Hilfe von Azure DevOps eine erste grundlegende CI/CD-Pipeline erstellen können, um bestimmte Prozesse zu automatisieren.

Der Usecase, für den hier beispielhaft eine solche Pipeline aufgesetzt wird, behandelt Kunden- und Bestellinformationen. Databricks Notebooks werden verwendet, um diese Daten aufzubereiten und zusammenzuführen. Dafür angelegte Databricks Workflows (DEV und PROD) übernehmen die Orchestrierung dieser Notebooks. Der gesamte Code, wie Notebooks, Python-Skripte, Konfigurations-Files etc., ist in einem Git-Repository in Azure Repos hinterlegt. Die zwei Workflows verweisen jeweils auf Entwicklungs- und Produktions-Code-Stände des Repositorys. Der Zugriff des Databricks Workspaces auf das Repository wird durch einen in Azure DevOps generierten Personal Access Token ermöglicht.

Die CI/CD-Pipeline wird in Azure DevOps aufgezogen und soll folgende Aufgaben übernehmen:

  • Durchführung von Unittests für bestehende und neu erstellte PySpark-Funktionen
  • Anpassung des Databricks-Workflow-Templates, abhängig von der Entwicklungsumgebung
  • Aktualisierung des Databricks-Workflows basierend auf vorgenommenen Änderungen am Workflow-Template und unter Erhalt vorhandener Metadaten

Ferner soll die Pipeline automatisch angestoßen werden, wenn auf einen der Branches main oder develop committet wird, und abbrechen, wenn ein Task fehlschlägt.

Azure DevOps

Azure DevOps bietet verschiedene Möglichkeiten, eine CI/CD- oder auch Build- und Release-Pipeline zu erstellen. Azure Pipelines werden unterschieden in Pipelines und Releases. Man kann bspw. Azure Pipelines nutzen, um die Build-Pipeline aufzusetzen und diese wiederum um eine mittels Azure Releases erstellte Release-Pipeline erweitern. Über Azure Releases lassen sich Pipelines durch Zusammensetzen vordefinierter Tasks innerhalb der UI erstellen. Mit Azure Pipelines kann man darüber hinaus Pipelines durch Konfiguration einer YAML-Datei definieren. Da letztere Option die Möglichkeit bietet, die Pipeline-Konfigurationsdatei in Git einzuchecken und somit versionierbar und wiederverwendbar zu machen, wird die Nutzung von Azure Pipelines für eine ganzheitliche Build- und Release-Pipeline empfohlen.

Innerhalb des Azure DevOps-Projekts lässt sich unter Pipelines eine neue Pipeline erstellen. Da der Code in Azure Repos abliegt, wird an dieser Stelle Azure Repos Git YAML und anschließend das entsprechende Repository selektiert.

Pipeline-YAML-Editor

Im Anschluss wird für diesen Usecase das Starter Pipeline Template ausgewählt, welches eine grobe Struktur für die Pipeline beinhaltet, jedoch um einiges erweitert werden muss. Azure DevOps zeigt einem folglich die YAML-Konfigurationsdatei, welche sich gleich editieren lässt (siehe Abbildung 3). Diese Datei wird mit dem Speichern innerhalb des Git-Repositorys abgelegt. Azure DevOps greift auf das File zu und generiert den Inhalten entsprechend eine Azure Pipeline.

Die Pipeline-Konfiguration wird im Zuge der Entwicklung meist nicht in einem Stück heruntergeschrieben. Stattdessen können sich Pipelines im Entwicklungsverlauf iterativ erweitern, je nach Anforderungen und Komplexität des Projekts. Im weiteren Verlauf wird das finale Pipeline-Konfigurations-File azure-pipelines.yml abschnittsweise näher erläutert.

Die YAML-Datei lässt sich grob in zwei Blöcke aufteilen. Während im zweiten Block die Pipeline inhaltlich konfiguriert wird, mit allen Schritten und Aufgaben, die durchlaufen werden sollen, werden im ersten Block einige grundlegende Konfigurationen vorgenommen (siehe Abbildung 4). Für diese Pipeline wird ein von Microsoft gehosteter Agent verwendet, für den lediglich das Virtual Machine Image bzw. das Betriebssystem definiert werden muss – in diesem Fall ubuntu-latest. Wartung und Updates werden von Microsoft übernommen, und nach jedem Run wird die virtuelle Maschine wieder verworfen.

Als Trigger der Pipeline werden die Branches main und develop definiert. Mit jedem Commit auf einen der beiden Branches wird die Pipeline somit ausgeführt.

Darüber hinaus werden Variablen definiert, auf welche die Pipeline während der Laufzeit zugreift. Hier gibt es verschiedene Möglichkeiten: Variablen, welche mit ins Git eingecheckt werden sollen, können, wie in Abbildung 4 dargestellt, in der YAML-Datei definiert werden. Für Variablen, welche nicht eingecheckt werden sollen – wie bspw. Token oder Passwörter – bietet Azure DevOps die Möglichkeit diese Variablen im Azure DevOps-Projekt selbst zu speichern. An dieser Stelle werden Umgebungs-abhängige Variablen definiert, welche sich, je nach Branch, auf welchen committet wurde, unterscheiden. In diesem, sowie in weiteren Abschnitten der Pipeline-Konfiguration wird auf die von Azure vordefinierten Variablen zurückgegriffen (siehe dazu https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml).

Neben den hier definierten, gibt es zahlreiche weitere Optionen – wie Parameter oder Definition eines Schedules – um die Pipeline zu konfigurieren. Diese werden für die vorliegende Pipeline jedoch nicht benötigt.

variables:
  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
    environment: prod
    job_id: 992621709793159
    job_name: customer_order_workflow_prod
  ${{ elseif eq(variables['Build.SourceBranchName'], 'develop') }}:
    environment: dev
    job_id: 1110277934396267
    job_name: customer_order_workflow_dev

trigger:
  - develop
  - main

pool:
  vmImage: "ubuntu-latest"

Abbildung 4: Initiale Konfigurationen der CI/CD-Pipeline (Ausschnitt 1 aus azure-pipelines.yml)

Der zweite Block der CI/CD-Pipeline beinhaltet die eigentliche Konfiguration der Pipeline-Aufgaben. Hier wird definiert, welche Schritte die Pipeline durchläuft, um das gewünschte Ergebnis zu erzielen. Eine CI/CD-Pipeline besteht aus einer oder mehreren Stages und eine Stage wiederum aus einem oder mehreren Jobs. Ein Job beinhaltet verschiedene Steps, welche entweder vordefinierte Tasks sein können oder eigens verfasste Skripte. Der Build-Teil der Pipeline steht für die Continuous Integration, welche sich vor allem auf das Testen von Code und das Erstellen von Artefakten konzentriert. Der Release-Teil wiederum spiegelt das Continuous Delivery wider, welches vor allem das Deployment von Code und Applikationen, aber auch weiteres Testing umfassen kann.

Build und Release sind beide Teil derselben Azure Pipeline, werden jedoch durch Konfiguration mehrerer Stages voneinander getrennt. Der zweite Block der YAML-Pipeline wird daher anhand der verschiedenen Stages separat voneinander erläutert.

Die Build-Stage setzt sich zusammen aus zwei Jobs. Der erste Job führt Tests durch, der zweite nimmt Änderungen am Job-Template vor und publisht diese als Artefakt. Artefakte beschreiben einzelne oder eine Sammlung von Dateien, welche als Ergebnis eines Pipeline-Abschnitts nachfolgenden Stages zur Wiederverwendung bereitgestellt werden. Der erste Job test_code_and_publish_test_results setzt sich zusammen aus von Azure vordefinierten Tasks, sowie eigens geschriebenen Skripten (siehe Abbildung 5). In der Ansicht des YAML-File-Editors in Azure DevOps lässt sich an der rechten Seite ein Assistent ausklappen, über welchen man vordefinierte Tasks suchen und nutzen kann.

stages:
  - stage: BUILD
    displayName: "TestProjektBuild"
    jobs:
      - job: test_code_and_publish_test_results
        displayName: "Test code and publish test results"
        continueOnError: false
        steps:
        - task: UsePythonVersion@0
          displayName: "Use Python 3.8"
          inputs:
            versionSpec: "3.8"
        - script: |
            python -m pip install --upgrade pip
            python -m pip install -r requirements.txt
          displayName: "Install dependencies"
        - script: |
            echo "y
            $(DATABRICKS_ADDRESS)
            $DATABRICKS_API_TOKEN
            $(DATABRICKS_CLUSTER_ID)
            $(DATABRICKS_ORG_ID)
            $(DATABRICKS_PORT)" | databricks-connect configure
          env:
            DATABRICKS_API_TOKEN: $(DATABRICKS_API_TOKEN)
          displayName: "Configure Databricks Connect"
        - script: |
            cd test
            python unit_test.py
          displayName: "Run Python Unit Tests for library code"
        - task: PublishTestResults@2
          displayName: "Publish Test Results **/TEST-*.xml"
          inputs:
            testResultsFormat: "JUnit"
            testResultsFiles: "**/TEST-*.xml"
            failTaskOnFailedTests: true

Abbildung 5: Job zum Testen des Codes innerhalb der Build-Stage (Ausschnitt 2 aus azure-pipelines.yml)

Angefangen mit dem Task UsePythonVersion@0 und dem ersten Skript, wird zum einen die zu verwendende Python-Version definiert. Zum anderen werden die im File requirements.txt aufgeführten, notwendigen Libraries installiert. Das zweite Skript konfiguriert Databricks-Connect, wodurch eine Verbindung zu einem Databricks-Cluster hergestellt wird. Wurde Databricks-Connect erfolgreich initialisiert, so führt das nächste Skript das im Verzeichnis test abliegende File unit_test.py mit den darin enthaltenen Unittests aus. Der letzte Task des ersten Jobs PublishTestResults@2 stellt anschließend die Ergebnisse der Tests zur Ansicht in Azure DevOps zur Verfügung. Schlägt einer der hier definierten Schritte fehl, so unterbricht die Pipeline den aktuellen Run.

- job: adjust_job_config_template_and_publish
  displayName: "Adjust job config template and publish artifact"
  dependsOn: test_code_and_publish_test_results
  continueOnError: false
  steps:
  - script: |
      cd config
      echo job config framework
      jq '.' customer_order_update_job_config.json
      
      jq '.job_id = $(job_id) |
      .new_settings.name = "$(job_name)" |
      .new_settings.git_source.git_branch = "$(Build.SourceBranchName)"' \
      customer_order_update_job_config.json > tmp.$$.json && \
      mv tmp.$$.json customer_order_update_job_config.json
      
      echo updated job config
      jq '.' customer_order_update_job_config.json
    displayName: "Insert environment specific values into job config files"
  - task: PublishPipelineArtifact@1
    displayName: "Publish Pipeline Databricks Workflow Artifact"
    inputs:
      targetPath: "config"
      artifact: "workflows"
      publishLocation: "pipeline"

Abbildung 6: Job zum Bereitstellen des Artefakts innerhalb der Build-Stage (Ausschnitt 3 aus azure-pipelines.yml)

Im zweiten Job der Build-Stage adjust_job_config_template_and_publish wird das im Verzeichnis config hinterlegte Job-Konfigurations-Template angepasst (siehe Abbildung 6). Werden während der Entwicklung Änderungen an der Job-Konfiguration vorgenommen, so sollen diese mit Ausführung der Pipeline im bestehenden Databricks-Workflow übernommen werden. Abhängig von der aktuellen Entwicklungsumgebung (DEV/PROD) bzw. des Branches (develop/main), über welchen die Pipeline angestoßen wird, werden die initial definierten Variablen in die Job-Konfiguration eingefügt. Für diesen Zweck wird der command-line-Prozessor jq verwendet, welcher das JSON-Template mit der Job-ID, dem Job-Namen sowie dem relevanten Git-Branch des zu aktualisierenden Jobs befüllt. Die JSON-Konfiguration eines Jobs lässt sich in Databricks in der Job-Ansicht extrahieren. Da sich DEV- und PROD-Job in weiten Teilen gleichen, mussten lediglich die Werte der oben genannten Variablen aus der Datei entfernt werden, um das Template zu erstellen. Der Task PublishPipelineArtifact@1 stellt die angepasste Konfigurations-Datei anschließend in der UI zum Download, sowie zur Weiterverwendung in nachfolgenden Stages zur Verfügung.

- stage: RELEASE_DEV
  displayName: "TestProjektReleaseDev"
  dependsOn: BUILD
  condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'develop'))
  jobs:
    - template: release-jobs.yml

- stage: RELEASE_PROD
  displayName: "TestProjektReleaseProd"
  dependsOn: BUILD
  condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'main'))
  jobs:
    - template: release-jobs.yml

Abbildung 7: DEV- und PROD-Release-Stages (Ausschnitt 4 aus azure-pipelines.yml)

Der Release-Teil der Pipeline ist aufgeteilt in zwei Stages, welche sich inhaltlich nicht großartig unterscheiden, jedoch zur Abgrenzung von DEV- und PROD-Deployments dienen (siehe Abbildungen 7 und 8). Während beide Stages den erfolgreichen Abschluss der Build-Stage erfordern, läuft die DEV-Release-Stage lediglich mit dem Pipeline-Trigger develop und die PROD-Release-Stage mit dem Trigger main.

Da beide Stages die gleichen Tasks durchführen, werden diese in ein Template ausgelagert, um Code-Duplikationen zu vermeiden (siehe Abbildung 9). Das Template release-jobs.yml liegt, wie die Pipeline-Konfiguration azure-pipelines.yml, im Verzeichnis azure ab und definiert auf gleiche Weise die durchzuführenden Schritte. In den jeweiligen Release-Stages wird dieses Template schließlich aufgerufen, um den Job update_db_workflow durchzuführen.

Im ersten Schritt wird mit dem Task DownloadPipelineArtifact@2 das in der Build-Stage bereitgestellte Artefakt – das angepasste Job-Template – heruntergeladen und für die Release-Stage verfügbar gemacht.

CI/CD-Pipeline

Der zweite und letzte Schritt besteht letztendlich darin, den bereits vorhandenen Databricks-Workflow (DEV oder PROD) mit der neuen Job-Konfiguration zu updaten. Dafür wird auf die Databricks Jobs API zurückgegriffen. Die reset-Funktion ermöglicht es, den Workflow der Konfiguration entsprechend anzupassen, ohne dabei Metadaten vergangener Runs zu verlieren.

jobs:
  - job: update_db_workflow
    displayName: "Update Databricks workflow"
    steps:
    - task: DownloadPipelineArtifact@2
      displayName: "Download Databricks Workflow Pipeline Artifact"
      inputs:
        buildType: "current"
        artifactName: "workflows"
        targetPath: "$(Pipeline.Workspace)/workflows"
    - script: |
        cd $(Pipeline.Workspace)/workflows
        curl --header "Authorization: Bearer $(DATABRICKS_API_TOKEN)" \
        --request POST $(DATABRICKS_ADDRESS)/api/2.0/jobs/reset \
        --data @customer_order_update_job_config.json | jq .
      displayName: "Update Databricks $(environment) workflow"

Abbildung 9: Template mit ausgelagerten Task-Definitionen für die Release-Stages

Bereit einzutauchen?

Eine CI/CD-Pipeline bietet eine einfache Möglichkeit, Prozesse, welche bislang manuell durchgeführt werden, zu automatisieren. Dadurch lassen sich Ressourcen einsparen, indem für regelmäßig anfallende Tasks weniger Arbeitszeit investiert werden muss. Man bekommt schneller Rückmeldung über mögliche Fehler, welche sich an einem frühen Zeitpunkt des Entwicklungsprozesses korrigieren lassen. Zudem kann an einigen Stellen menschlichem Versagen durch eine fest definierte Pipeline vorgebeugt werden.

Das obige Praxis-Beispiel konnte verdeutlichen, dass man mit wenig Aufwand stark von CI/CD-Pipelines profitieren kann. Selbst für kleine Aufgaben, welche regelmäßig anfallen, kann es sich bereits lohnen, diese durch Nutzung einer solchen Pipeline zu automatisieren. Es bleibt am Ende nur zu empfehlen, einfach mal mit einer CI/CD-Pipeline für das eigene Projekt anzufangen, und sei sie noch so „klein“. Sie wächst mit der Zeit und den Fähigkeiten, die man nach und nach erlernt, wird stetig optimiert, und hat in jedem Projekt das Potenzial großen Mehrwert zu schaffen.

Anhang

variables:
  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
    environment: prod
    job_id: 992621709793159
    job_name: customer_order_workflow_prod
  ${{ elseif eq(variables['Build.SourceBranchName'], 'develop') }}:
    environment: dev
    job_id: 1110277934396267
    job_name: customer_order_workflow_dev

trigger:
  - develop
  - main

pool:
  vmImage: "ubuntu-latest"

stages:
  - stage: BUILD
    displayName: "TestProjektBuild"
    jobs:
      - job: test_code_and_publish_test_results
        displayName: "Test code and publish test results"
        continueOnError: false
        steps:
        - task: UsePythonVersion@0
          displayName: "Use Python 3.8"
          inputs:
            versionSpec: "3.8"
        - script: |
            python -m pip install --upgrade pip
            python -m pip install -r requirements.txt
          displayName: "Install dependencies"
        - script: |
            echo "y
            $(DATABRICKS_ADDRESS)
            $DATABRICKS_API_TOKEN
            $(DATABRICKS_CLUSTER_ID)
            $(DATABRICKS_ORG_ID)
            $(DATABRICKS_PORT)" | databricks-connect configure
          env:
            DATABRICKS_API_TOKEN: $(DATABRICKS_API_TOKEN)
          displayName: "Configure Databricks Connect"
        - script: |
            cd test
            python unit_test.py
          displayName: "Run Python Unit Tests for library code"
        - task: PublishTestResults@2
          displayName: "Publish Test Results **/TEST-*.xml"
          inputs:
            testResultsFormat: "JUnit"
            testResultsFiles: "**/TEST-*.xml"
            failTaskOnFailedTests: true
      - job: adjust_job_config_template_and_publish
        displayName: "Adjust job config template and publish artifact"
        dependsOn: test_code_and_publish_test_results
        continueOnError: false
        steps:
        - script: |
            cd config
            echo job config framework
            jq '.' customer_order_update_job_config.json
            
            jq '.job_id = $(job_id) |
            .new_settings.name = "$(job_name)" |
            .new_settings.git_source.git_branch = "$(Build.SourceBranchName)"' \
            customer_order_update_job_config.json > tmp.$$.json && \
            mv tmp.$$.json customer_order_update_job_config.json
            
            echo updated job config
            jq '.' customer_order_update_job_config.json
          displayName: "Insert environment specific values into job config files"
        - task: PublishPipelineArtifact@1
          displayName: "Publish Pipeline Databricks Workflow Artifact"
          inputs:
            targetPath: "config"
            artifact: "workflows"
            publishLocation: "pipeline"

  - stage: RELEASE_DEV
    displayName: "TestProjektReleaseDev"
    dependsOn: BUILD
    condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'develop'))
    jobs:
      - template: release-jobs.yml

  - stage: RELEASE_PROD
    displayName: "TestProjektReleaseProd"
    dependsOn: BUILD
    condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'main'))
    jobs:
      - template: release-jobs.yml

Abbildung 10: Vollständiges Konfigurations-File azure-pipelines.yml für die Azure DevOps Pipeline

Quellen:

https://www.databricks.com/blog/2021/09/20/part-1-implementing-ci-cd-on-databricks-using-databricks-notebooks-and-azure-devops.html
https://www.jetbrains.com/teamcity/ci-cd-guide/devops-ci-cd/
https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml
https://learn.microsoft.com/en-us/azure/databricks/dev-tools/ci-cd/ci-cd-azure-devops
https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml
https://weissenberg-group.de/was-ist-devops/
https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops
https://www.atlassian.com/de/devops#:~:text=Unter%20DevOps%20versteht%20man%20diverse,Kommunikation%20und%20Zusammenarbeit%20sowie%20Technologieautomatisierung

Teile diesen Artikel mit anderen

Über den Autor

Christoph ist seit Ende 2020 als Data Engineer bei Woodmark tätig. Im Rahmen seiner Kundenprojekte konzentriert er sich auf die Backend-Entwicklung. Er implementiert ganzheitliche ETL-Prozesse und beschäftigt sich intensiv mit Big Data und Cloud-Technologien.

Zur Übersicht Blogbeiträge