Azure DevOps in zes lessen

Gericht op data engineering, niet op applicatieontwikkeling. Van Repos en branching tot YAML-pipelines die dbt, Databricks en Fabric uitrollen, met omgevingen, approvals en veilig omgaan met geheimen.

6
Lessen
6
Praktijkoefeningen
Beginner tot gevorderd
Niveau
100%
Gratis

Les 1: De onderdelen van Azure DevOps

Beginner Met voorbeelden

Vijf diensten onder één naam

Azure DevOps is geen enkele tool maar een verzameling diensten die je los kunt gebruiken. Voor data engineering heb je er in de praktijk drie van nodig, en de andere twee kom je vooral tegen in teams die er al mee werken.

Waarom dit bij datawerk hoort

Een datawarehouse is software. Transformaties zijn code, een pipeline-definitie is code, en de inrichting van je platform kan code zijn. Zodra dat zo is, horen dezelfde gewoonten erbij als bij elke andere codebase: reviews, geautomatiseerde tests en een uitrol die reproduceerbaar is.

Organisatie, project en repository

De hiërarchie is drie lagen diep. Een organisatie is je bedrijf. Daarbinnen maak je projecten, en elk project heeft zijn eigen repositories, pipelines en boards. Rechten regel je op projectniveau.

Voor een datateam werkt één project met meerdere repositories meestal prettiger dan een project per repository: je pipelines kunnen dan makkelijk templates en variabelengroepen delen.

Let op de parallelle jobs

Een gratis organisatie krijgt één door Microsoft gehoste parallelle job, en nieuwe organisaties moeten die zelfs eerst aanvragen. Draaien er twee pipelines tegelijk, dan staat de tweede te wachten. Bij een datateam dat 's nachts meerdere laadprocessen draait, loop je daar snel tegenaan; dan is een self-hosted agent of een betaalde parallelle job de uitweg. Les 6 gaat daarover.

Oefening: wat heb je nodig?

Situatie: een datateam van vier mensen werkt met een dbt-project op Snowflake en een handvol Databricks-notebooks. Nu staat alles in een gedeelde netwerkmap en wordt er met de hand gedeployd.

Opdracht:

  1. Welke van de vijf diensten heeft dit team echt nodig, en welke kan het overslaan?
  2. Maak je één project of twee? Waar hangt dat van af?
  3. Zet je dbt en de notebooks in één repository of in twee?
  4. Wat is het eerste dat je inricht, nog vóór enige pipeline?

Richting: Repos en Pipelines zijn onmisbaar, Artifacts komt later van pas, Boards alleen als ze hun werk nu nergens anders plannen, Test Plans niet. Eén project met twee repositories: dbt en notebooks hebben een eigen levenscyclus en een eigen pipeline, maar delen wel variabelengroepen en service connections. Begin met de repository en een branch policy op main — zonder dat laatste kan iedereen alsnog rechtstreeks naar main duwen en heb je alleen een duurdere netwerkmap.

Les 2: Repos, branching en pull requests

Beginner Met voorbeelden

Een vertakkingsmodel dat bij datawerk past

De meeste datateams zijn klein en leveren vaak. Daar past trunk-based development beter bij dan een zwaar model met release- en hotfixbranches: je werkt op korte takken vanaf main en voegt dagelijks samen.

Wat je vooral wilt vermijden is een tak die weken openstaat. Bij transformatiecode leidt dat gegarandeerd tot conflicten, want iedereen raakt dezelfde modellen aan.

Naamgeving die zichzelf uitlegt

Gebruik een voorvoegsel plus het werkitemnummer: feature/1234-omzet-per-segment of fix/1240-dubbele-orders. Azure DevOps koppelt de tak dan automatisch aan het werkitem, en in je historie zie je later terug waaróm iets veranderde.

Branch policies: de knop die het verschil maakt

Een repository zonder beleid op main is een netwerkmap met versiegeschiedenis. Met branch policies dwing je af dat er niemand rechtstreeks naar main duwt en dat elke wijziging langs een pull request gaat.

Zet minimaal deze drie aan: minstens één reviewer, een build die moet slagen, en gekoppelde werkitems. De eerste zorgt dat iemand meekijkt, de tweede dat kapotte code main niet haalt, de derde dat je over een jaar nog kunt achterhalen waarom een model eruitziet zoals het eruitziet.

Shell - dagelijkse gang van zaken

# Altijd vanaf een actuele main beginnen
git checkout main
git pull

git checkout -b feature/1234-omzet-per-segment

# ... je wijzigingen ...
git add models/marts/fct_omzet_segment.sql
git commit -m "Omzet per klantsegment toegevoegd aan de gold-laag"
git push -u origin feature/1234-omzet-per-segment

# Pull request aanmaken vanaf de opdrachtregel
az repos pr create \
  --title "Omzet per klantsegment" \
  --description "Nieuwe mart voor de kwartaalrapportage. Tests toegevoegd." \
  --source-branch feature/1234-omzet-per-segment \
  --target-branch main \
  --work-items 1234
        

Kies squash merge

Bij squash merge wordt je hele tak één commit op main. Je historie blijft daarmee leesbaar: één regel per wijziging in plaats van vijftien tussencommits met berichten als "fix" en "nog een poging". Je kunt dat per repository afdwingen in de policies.

Wat hoort niet in je repository

Bij datawerk is de verleiding groot om even een CSV of een exportje mee te committen. Doe dat niet: Git is er niet voor gemaakt en je krijgt het er nooit meer uit. Een .gitignore die profielen, omgevingsbestanden en data buitensluit, hoort bij het aanmaken van de repository.

.gitignore voor een dbt-project

# Inloggegevens
profiles.yml
.env

# Door dbt gegenereerd
target/
dbt_packages/
logs/

# Data hoort niet in git
*.csv
*.parquet
*.xlsx
!seeds/*.csv          # behalve seeds: die zijn klein en bewust beheerd

# Python
__pycache__/
.venv/
        

Oefening: beleid inrichten

Situatie: een team van vier duwt rechtstreeks naar main. Vorige week ging er een model live met een syntaxfout, en niemand wist wie het had gedaan of waarom. Er staan inmiddels ook drie CSV-bestanden van 80 MB in de repository.

Opdracht:

  1. Welke drie branch policies zet je als eerste aan, en welk probleem lost elk op?
  2. Hoe voorkom je dat er nog meer CSV's in belanden?
  3. De drie bestaande CSV's zijn al gecommit. Waarom is git rm niet genoeg?
  4. Een teamlid klaagt dat pull requests het werk vertragen. Wat is je antwoord?

Richting: een verplichte reviewer vangt de menselijke blik, een verplichte build vangt de syntaxfout, en gekoppelde werkitems maken achteraf navolgbaar waarom iets veranderde. Nieuwe CSV's hou je buiten met .gitignore. Bestaande bestanden verwijderen met git rm haalt ze uit de huidige stand maar niet uit de historie — de repository blijft even groot en de data is nog steeds terug te halen; daarvoor moet je de historie herschrijven, wat afstemming met het hele team vraagt. Op de laatste vraag: een pull request die een dag blijft liggen vertraagt inderdaad, dus spreek af dat reviews binnen een paar uur gebeuren en houd takken klein. De vertraging zit in grote takken, niet in het reviewen zelf.

Les 3: YAML-pipelines

Gemiddeld Met YAML-voorbeelden

Pipelines als code

Azure DevOps kent twee soorten pipelines. De klassieke stel je samen in de webinterface; de YAML-variant staat als bestand in je repository. Kies altijd YAML. Je pipeline gaat dan mee in pull requests, je ziet in de historie wie wat wijzigde, en je kunt hem terugdraaien zoals elke andere code.

De opbouw

Een pipeline bestaat uit stages die jobs bevatten, en elke job bestaat uit steps. Stages draaien standaard na elkaar, jobs binnen een stage parallel. Elke job krijgt een schone machine, dus wat de ene job neerzet, is in de volgende weg tenzij je het als artefact doorgeeft.

YAML - azure-pipelines.yml

trigger:
  branches:
    include: [main]
  paths:
    include: [models/**, macros/**]   # alleen starten als dit wijzigt

pr:
  branches:
    include: [main]

pool:
  vmImage: ubuntu-latest

variables:
  - group: dbt-gedeeld          # variabelengroep uit Library
  - name: pythonVersie
    value: '3.11'

stages:
  - stage: Controleren
    displayName: Bouwen en testen
    jobs:
      - job: dbt_build
        steps:
          - task: UsePythonVersion@0
            inputs:
              versionSpec: $(pythonVersie)

          - script: pip install dbt-snowflake==1.8.3
            displayName: dbt installeren

          - script: dbt deps
            displayName: Pakketten ophalen

          - script: dbt build --target ci
            displayName: Modellen bouwen en testen
            env:
              SNOWFLAKE_PASSWORD: $(snowflakeWachtwoord)
        

Gebruik padfilters

Zonder paths start je pipeline ook bij een wijziging in de README. Bij één parallelle job betekent dat wachten voor niets. Beperk de trigger tot de mappen waar echte code staat.

Templates tegen kopieerwerk

Heb je drie repositories die bijna dezelfde stappen draaien, zet die stappen dan in een template. Wijzig je de dbt-versie, dan doe je dat op één plek in plaats van drie.

YAML - templates/dbt-stappen.yml

# In het template
parameters:
  - name: doelomgeving
    type: string
  - name: selectie
    type: string
    default: ''

steps:
  - script: pip install dbt-snowflake==1.8.3
    displayName: dbt installeren
  - script: dbt deps
    displayName: Pakketten ophalen
  - script: |
      dbt build --target ${{ parameters.doelomgeving }} ${{ parameters.selectie }}
    displayName: Bouwen en testen

# In de pipeline die het template gebruikt
steps:
  - template: templates/dbt-stappen.yml
    parameters:
      doelomgeving: ci
      selectie: '--select state:modified+'
        

Oefening: een pipeline opzetten

Situatie: je hebt een dbt-project in Azure Repos. Je wilt dat bij elke pull request wordt gebouwd en getest, en dat er na een merge naar main niets automatisch naar productie gaat — dat moet nog een aparte stap worden.

Opdracht:

  1. Welke triggers zet je aan, en welke bewust niet?
  2. Hoe zorg je dat een wijziging in de documentatie geen pipeline start?
  3. Waarom zet je het wachtwoord niet gewoon in het YAML-bestand?
  4. Het team heeft ook een Databricks-repository met bijna dezelfde stappen. Wat doe je?

Richting: zet pr aan op main zodat elke pull request wordt gecontroleerd, en trigger op main voor de bouwstap — maar laat de uitrol naar productie in een aparte stage met een approval, zodat er niets vanzelf live gaat. Beperk met paths tot je code-mappen. Het wachtwoord hoort in een variabelengroep of Key Vault: alles in YAML staat in je repository en is daarmee zichtbaar voor iedereen met leesrechten, en het staat voorgoed in de historie. Voor de tweede repository maak je een template met de gedeelde stappen.

Les 4: CI voor dataprojecten

Gevorderd Met YAML-voorbeelden

Wat valt er te testen aan data?

Bij applicatiecode draai je unittests. Bij transformatiecode ligt het anders: de logica zit in SQL en het resultaat hangt af van de data. Toch is er genoeg te controleren voordat iets main haalt.

Alleen bouwen wat veranderde

Een volledig project met tweehonderd modellen bouwen bij elke pull request duurt te lang; dan wacht niemand meer op de uitkomst. Met state:modified+ bouw je alleen de gewijzigde modellen en wat daarvan afhangt. Daar heb je de artefacten van de laatste productierun voor nodig, en die haal je als artefact binnen.

YAML - ci-pipeline.yml

stages:
  - stage: CI
    jobs:
      - job: controleren
        steps:
          - task: UsePythonVersion@0
            inputs:
              versionSpec: '3.11'

          - script: pip install dbt-snowflake==1.8.3 sqlfluff
            displayName: Gereedschap installeren

          - script: dbt deps
            displayName: Pakketten ophalen

          # Artefacten van de laatste productierun binnenhalen,
          # zodat we weten wat er gewijzigd is ten opzichte van productie
          - task: DownloadPipelineArtifact@2
            inputs:
              buildType: specific
              project: $(System.TeamProject)
              definition: $(prodPipelineId)
              artifactName: dbt-artifacts
              targetPath: $(Pipeline.Workspace)/prod-artifacts
            continueOnError: true

          - script: dbt compile --target ci
            displayName: Compileren

          - script: sqlfluff lint models/ --dialect snowflake
            displayName: Stijlcontrole
            continueOnError: true        # waarschuwen, niet blokkeren

          - script: |
              dbt build --target ci \
                --select state:modified+ \
                --defer --state $(Pipeline.Workspace)/prod-artifacts
            displayName: Gewijzigde modellen bouwen en testen
            env:
              SNOWFLAKE_PASSWORD: $(snowflakeWachtwoord)

          - task: PublishTestResults@2
            condition: always()          # ook publiceren als de stap faalde
            inputs:
              testResultsFormat: JUnit
              testResultsFiles: target/*.xml
        

Gebruik condition: always() bij resultaten

Faalt een stap, dan slaat Azure DevOps standaard de rest over. Juist bij een mislukte build wil je de testresultaten zien. Zonder condition: always() krijg je alleen "de pipeline is rood" zonder te weten welke test faalde.

Een schone omgeving per pull request

Laat je CI niet in hetzelfde schema bouwen als je collega's. Geef elke pipelinerun een eigen schema op basis van het build-id, en ruim het achteraf op. Dan kunnen twee pull requests tegelijk draaien zonder elkaar te overschrijven.

YAML - eigen schema per run

variables:
  - name: ciSchema
    value: 'ci_pr_$(System.PullRequest.PullRequestId)'

steps:
  - script: dbt build --target ci
    env:
      DBT_SCHEMA: $(ciSchema)

  # Opruimen, ook als de build faalde
  - script: |
      echo "DROP SCHEMA IF EXISTS $(ciSchema) CASCADE;" | snowsql -f -
    displayName: CI-schema opruimen
    condition: always()
        

Oefening: een CI die niemand gebruikt

Situatie: de CI-pipeline draait bij elke pull request en duurt 38 minuten, want hij bouwt het hele project van 210 modellen. Twee ontwikkelaars die tegelijk een pull request openen, overschrijven elkaars tabellen in het gedeelde CI-schema. Niemand wacht nog op de uitkomst; er wordt gemerged zodra er een reviewer is.

Opdracht:

  1. Welke twee oorzaken maken deze pipeline onbruikbaar?
  2. Hoe breng je die 38 minuten omlaag, en wat heb je daarvoor nodig?
  3. Hoe voorkom je dat twee runs elkaar in de weg zitten?
  4. Welke policy zorgt dat er niet gemerged kan worden voordat de build groen is?
  5. De stijlcontrole faalt vaak op oude code. Wat doe je daarmee?

Richting: te traag en niet geïsoleerd. De duur pak je aan met state:modified+ en --defer, waarvoor je de artefacten van de productierun als artefact moet publiceren en binnenhalen. De botsing los je op met een schema per pull request, opgeruimd met condition: always(). Een verplichte build in de branch policy zorgt dat groen een voorwaarde wordt in plaats van een advies. En zet de linter op continueOnError: true zolang de oude code nog niet is bijgewerkt: een waarschuwing die je negeert is beter dan een blokkade die mensen leren omzeilen.

Les 5: Omgevingen, approvals en releases

Gemiddeld Met YAML-voorbeelden

Van merge naar productie

Een merge naar main hoort niet automatisch productie te raken. Tussen die twee zit een uitrol die je wilt kunnen tegenhouden, inzien en terugdraaien. Daarvoor gebruik je environments met approvals.

Een environment in Azure DevOps is meer dan een naam: het is de plek waar je vastlegt wie moet goedkeuren, op welke momenten er niet uitgerold mag worden, en wat de geschiedenis van uitrollen is.

YAML - cd-pipeline.yml

stages:
  - stage: Acceptatie
    dependsOn: CI
    condition: succeeded()
    jobs:
      - deployment: naar_acc
        environment: acceptatie
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - script: dbt build --target acc
                  env:
                    SNOWFLAKE_PASSWORD: $(accWachtwoord)

  - stage: Productie
    dependsOn: Acceptatie
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: naar_prod
        environment: productie      # hier hangt de approval aan
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - script: dbt build --target prod
                  env:
                    SNOWFLAKE_PASSWORD: $(prodWachtwoord)

                # Artefacten bewaren, zodat de volgende CI kan vergelijken
                - publish: target/
                  artifact: dbt-artifacts
        

Die condition is belangrijk

Zonder de controle op Build.SourceBranch kan een pipeline die vanaf een feature-branch draait ook de productiestage bereiken. Leg expliciet vast dat alleen main naar productie mag.

Approvals en checks

Op een environment hang je meer dan alleen een goedkeuring. De nuttigste voor datateams:

Terugdraaien vraagt om een plan

Bij applicaties rol je terug door de vorige versie opnieuw uit te rollen. Bij data werkt dat niet altijd: een model dat je tabel heeft overschreven, draai je niet terug door oude code te deployen. Denk vooraf na over wat herstel betekent — Time Travel in Snowflake, een clone vóór de uitrol, of een incrementeel model dat je gericht kunt herbouwen.

Oefening: een uitrol inrichten

Situatie: het team wil van handmatig deployen af. Er is een acceptatieomgeving en een productieomgeving. De financiële afdeling wil dat er niets verandert tijdens de maandafsluiting, de eerste drie werkdagen van de maand. Twee keer is er een uitrol misgegaan doordat er tegelijk een nachtelijk laadproces liep.

Opdracht:

  1. Welke stages maak je, en welke voorwaarde zet je tussen acceptatie en productie?
  2. Welke check lost het probleem met het nachtelijke laadproces op?
  3. Hoe regel je de maandafsluiting? Er is geen check die "niet de eerste drie werkdagen" kent.
  4. Wat publiceer je als artefact, en waarom heb je dat later nodig?
  5. Hoe ziet terugdraaien eruit als een model de productietabel al heeft overschreven?

Richting: CI, dan acceptatie, dan productie, met op de productiestage een voorwaarde op main plus een approval. De botsing met het laadproces los je op met een exclusive lock op de omgeving, eventueel aangevuld met een REST-controle of het laadproces klaar is. Voor de maandafsluiting bestaat geen kant-en-klare check: gebruik een business-hours-venster plus een handmatige approval, en leg de afspraak vast bij de goedkeurder — of laat de pipeline zelf een variabele controleren die je tijdens de afsluiting op "nee" zet. Publiceer de map target/ als artefact; die heeft je volgende CI nodig voor --defer. En terugdraaien regel je vooraf: maak een clone van de betrokken tabellen vlak voor de uitrol, zodat herstel een hernoeming is in plaats van een herbouw.

Les 6: Geheimen, verbindingen en agents

Gevorderd Beveiliging

Waar wachtwoorden horen

Nooit in je YAML. Alles in je repository is zichtbaar voor iedereen met leesrechten en blijft voorgoed in de historie staan, ook nadat je het eruit haalt. Er zijn drie betere plekken.

YAML - geheimen gebruiken

variables:
  - group: snowflake-prod       # gekoppeld aan Key Vault

steps:
  # GOED: als omgevingsvariabele doorgeven
  - script: dbt build --target prod
    env:
      SNOWFLAKE_PASSWORD: $(snowflakeWachtwoord)

  # FOUT: in de opdrachtregel zelf. Dat belandt in de logs
  # en is zichtbaar voor iedereen die de run kan bekijken.
  # - script: dbt build --password $(snowflakeWachtwoord)
        

Waarom env en niet de opdrachtregel

Azure DevOps maskeert bekende geheimen in logs, maar dat werkt niet waterdicht: zodra een waarde door een script wordt bewerkt of in een foutmelding terechtkomt, kan hij alsnog zichtbaar worden. Via env komt hij niet in het commando terecht en loop je dat risico niet.

Service connections

Een service connection is de brug tussen je pipeline en een externe dienst: een Azure-abonnement, een Databricks-workspace, een container registry. Voor Azure gebruik je bij voorkeur workload identity federation: je pipeline wisselt dan een kortlevend token in en er is geen geheim meer om te beheren of te lekken.

Beperk wie een verbinding mag gebruiken

Een service connection staat standaard open voor alle pipelines in het project. Zet dat uit voor je productieverbinding en geef hem alleen vrij aan de pipeline die hem nodig heeft. Anders kan iedereen met rechten op een willekeurige pipeline bij je productieomgeving.

Door Microsoft gehost of zelf beheerd

Een door Microsoft gehoste agent is een schone machine die na afloop verdwijnt. Dat is prettig: geen onderhoud, geen resten van een vorige run. Er zijn twee redenen om toch een eigen agent te draaien.

De eerste is netwerktoegang. Staat je bron achter een firewall of in een virtueel netwerk, dan komt een gehoste agent er niet bij. De tweede is wachttijd: met één parallelle job staan je pipelines in de rij, en een eigen agent lost dat op.

YAML - agentkeuze

# Door Microsoft gehost
pool:
  vmImage: ubuntu-latest

# Eigen agent, bijvoorbeeld binnen je netwerk
pool:
  name: eigen-agents
  demands:
    - agent.os -equals Linux
    - dbt           # zelf gezette eigenschap

# Per stage verschillen kan ook: CI gehost, productie op eigen agent
stages:
  - stage: CI
    pool:
      vmImage: ubuntu-latest
  - stage: Productie
    pool:
      name: eigen-agents
        

Een eigen agent is onderhoud

Je bent zelf verantwoordelijk voor de machine, de updates en het opruimen tussen runs. Werkruimtes die niet worden opgeschoond, zijn de meest voorkomende oorzaak van een pipeline die "op mijn agent wel werkt". Zet workspace: clean: all in je job als je daar last van hebt.

Oefening: veilig inrichten

Situatie: in het YAML-bestand staat het Snowflake-wachtwoord hardgecodeerd, al negen maanden. De pipeline moet ook een on-premises SQL Server benaderen die achter een firewall staat. De productieverbinding met Azure is beschikbaar voor alle zestien pipelines in het project.

Opdracht:

  1. Het wachtwoord uit het bestand halen is niet genoeg. Wat moet er nog meer gebeuren?
  2. Waar zet je het in plaats daarvan, en wat is het verschil tussen de twee opties?
  3. Hoe bereikt de pipeline die SQL Server achter de firewall?
  4. Wat doe je met die productieverbinding die voor alles openstaat?
  5. Hoe voorkom je dat het wachtwoord alsnog in de logs verschijnt?

Richting: het wachtwoord staat negen maanden in je historie, dus het is gecompromitteerd: roteer het, want verwijderen uit het bestand haalt het niet uit eerdere commits. Zet het nieuwe in een variabelengroep, of beter in Key Vault zodat je voortaan kunt rouleren zonder je pipelines te raken. Voor de SQL Server heb je een self-hosted agent binnen dat netwerk nodig; een gehoste agent komt er niet bij. Beperk de productieverbinding tot de pipelines die hem echt nodig hebben. En geef geheimen altijd via env door in plaats van in de opdrachtregel, zodat ze niet in een commando of foutmelding terechtkomen.