Infrastructure as Code met Azure Bicep

Geschreven door Matthijs Wagemakers op

Een trend die de niet meer weg te denken is uit hedendaagse software development standaarden is Infrastructure as Code (IaC). Met IaC is het niet meer nodig om handmatig omgevingen zoals VM's, Azure Resources of servers te configureren, kan de configuratie van omgevingen gelijk worden gehouden en kunnen wijzigingen opgenomen worden in code repositories.

Het automatisch configureren van resources in Azure was al langer mogelijk via de Azure CLI, ARM templates en tools zoals Terraform. Sinds maart dit jaar is echter een nieuwe optie beschikbaar: Azure Bicep.

Met Azure Bicep kunnen op een eenvoudige manier Azure Resources geconfigureerd worden. Bicep wordt helemaal krachtig als de templates automatisch toegepast worden met een CI/CD pipeline.

In dit artikel gaan we aan de slag met Bicep in een Azure DevOps build en een deployment YAML pipeline, om automatisch en volledige met code, een Azure Web App te configureren voor een Test en Productie omgeving.

Wat is Azure Bicep

Bicep is Microsofts antwoord op tools zoals Terraform en Ansible. Bicep is een declaratieve domeinspecifieke taal dat belooft eenvoudiger te zijn dan Azure Resource Manager (ARM) templates.

Bicep template

We starten met het aanmaken van de bicep template. Een gemakkelijke manier om dit te doen is in Visual Studio Code met de Bicep extension.

Daarnaast is het handig om de Azure CLI en de Azure Bicep command group te installeren. Installeer Azure CLI door de volgende instructies te volgen: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli. Na de installatie kan de bicep command group geïnstalleerd worden met het commando:
az bicep install

Door een .bicep file aan te maken krijgen we in Visual Studio Code code completion en syntax highlighting in de bicep file waardoor het gemakkelijk is om de template samen te stellen. De extension biedt resource snippets aan waarmee laagdrempelig resources toegevoegd kunnen worden. Om een snippet te gebruiken type plan in de bicep template en kies de suggestie res-plan om een app service plan toe te voegen. Doe hetzelfde voor een app service door app te typen en res-web-app te kiezen.

De app wordt gekoppeld aan het plan en door middel van parameters kan de naam van de app service configureerbaar gemaakt worden. Het eindresultaat ziet er als volgt uit:

param appServiceName string

var appServicePlanName = toLower('plan-${appServiceName}')

resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: appServicePlanName
  location: resourceGroup().location
  sku: {
    name: 'D1'
  }
}

resource appService 'Microsoft.Web/sites@2020-06-01' = {
  name: appServiceName
  location: resourceGroup().location
  properties: {
    serverFarmId: appServicePlan.id
  }
}

Een alternatieve methode om een bicep template aan te maken is door een bestaande Azure Resource te exporteren naar een ARM (json) template, bijvoorbeeld via de Azure Portal met de optie Export template en deze ARM template te decompilen naar een bicep file met het commando:
az bicep decompile --file export.json

De template kan via de Azure CLI nu handmatig toepast worden. Dit kan net als bij ARM templates op de volgende niveau's (scopes) gedaan worden:

In dit voorbeeld wordt het template uitgevoerd op Resource Group niveau. Hiervoor moet de resource group eerst bestaan. Voer daarvoor dit Azure CLI commando uit:
az group create --location westeurope --name rg-bicepdemo-01

Hierna kan de bicep template toegepast worden:
az deployment group create --resource-group rg-bicepdemo-01 --template-file appservice.bicep

De Azure CLI zal vragen om de parameter appServiceName op te geven. Geef een unieke naam op, bijvoorbeeld: app-bicepdemo-01

Als het commando voltooid is zijn de resources zichtbaar in de Azure Portal.

Mochten we nu een aanpassing doen in de template, door bijvoorbeeld ARR affinity uit te zetten, dan zal de bestaande app service geüpdate worden. Alle andere configuratie die nog gelijk is wordt niet veranderd. Pas de app service resource aan in de template door de property httpsOnly toe te voegen:

resource appService 'Microsoft.Web/sites@2020-06-01' = {
  name: appServiceName
  location: resourceGroup().location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
  }
}

Pas de template opnieuw toe:
az deployment group create --resource-group rg-bicepdemo-01 --template-file appservice.bicep

Zie in de Azure Portal dat de app service -> TLS/SSL settings -> HTTPS Only aangepast is naar on.

Azure DevOps environments

Nu is dit al erg handig, maar het zou nog mooier zijn als we onze test en productie omgeving automatisch in sync kunnen houden bij wijzigingen. Hiervoor gaan we een build en deployment pipeline opzetten. Omdat we een YAML pipeline willen gebruiken (i.p.v. een grafische classic release pipeline) moeten er eerst environments aangemaakt worden in DevOps.

Environments worden getarget in de deployment pipeline en zorgen ervoor dat het inzichtelijk wordt welke pipeline wanneer naar de environment heeft gedeployed. Ook kan de security op environment ingeregeld worden, bijvoorbeeld als er een pre-deployment approval nodig is.

Maak een environment aan door naar Azure Devops -> Pipelines -> Environments te gaan. Maak 2 nieuwe environment met de volgende namen:

  • Bicepdemo-Infra-test
  • Bicepdemo-Infra-prod

DevOps environment

YAML pipeline

Nu de environments aangemaakt zijn kan de pipeline opgesteld worden. Om de feedback loop wat te verkorten in het geval dat de bicep template (syntax) fouten bevat gaan we de deployment opdelen in 2 stappen: build en deploy.

Voor het build gedeelte gebruiken we een powershell script wat de bicep template zal compilen:
az bicep build --file appservice.bicep

De YAML template is goed te editen vanuit de Azure DevOps pipeline editor. Maak een nieuwe pipeline aan d.m.v. deze stappen:

  1. Ga naar Pipelines
  2. Kies New pipeline
  3. Kies Azure DevOps Repos Git
  4. Kies je git repo
  5. Kies starter pipeline

We maken nu de build stage aan, waarin we de bicep template builden en daardoor controleren op syntax errors. Daarna wordt de template als pipeline artifact gepubliceerd zodat deze gebruikt kan worden in de deployment stages van onze pipeline:

variables:
  bicepSourceFile: 'appservice.bicep'

stages:
- stage: build
  displayName: Build
  jobs: 
  - job: build
    displayName: Build bicep template
    steps: 
    - task: PowerShell@2
      displayName: Build Bicep file
      inputs:
        targetType: 'inline'
        script: 'az bicep build --file $(Build.SourcesDirectory)/${{ variables.bicepSourceFile }}' 

    - task: CopyFiles@2
      inputs:
        Contents: '$(Build.SourcesDirectory)/${{ variables.bicepSourceFile }}'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'

    - publish: $(Build.ArtifactStagingDirectory)
      artifact: biceptemplate

Hierna voegen we de deployment stage voor de test omgeving toe. Deze download automatisch het artifact uit de vorige stage naar de Pipeline.Workspace map. Daarna voeren we met de Azure CLI twee commandos uit: het aanmaken van de resource group (indien deze nog niet bestaat) en daarna het publiceren van de bicep template naar deze resource group.

Hier komt tevens onze environment terug die we eerder aangemaakt hebben: Bicepdemo-Infra-test.
Ook configureren we de (Azure Resource Manager) Service Connection die gebruikt wordt om in te loggen in de Azure CLI en rechten heeft om de resources aan te maken.

We willen deze stage niet uitvoeren als de pipeline aangeroepen wordt vanuit een Pull Request build dus hiervoor wordt een condition toegevoegd.

- stage: deploy_test
  displayName: Deploy Test
  jobs:
  - deployment:
    displayName: 'Deploy App Service to Test'
    environment: Bicepdemo-Infra-test
    strategy:
      runOnce:
        deploy:
          steps:
            - task: AzureCLI@2
              inputs:
                azureSubscription: 'BicepDemoServiceConnection'
                scriptType: 'pscore'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  az group create --name rg-bicepdemo-test-01 --location westeurope
                  az deployment group create `
                    --resource-group rg-bicepdemo-test-01 `
                    --template-file $(Pipeline.Workspace)/biceptemplate/${{ variables.bicepSourceFile }} `
                    --parameters appServiceName='app-bicepdemo-test-01'
  condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))

De deployment stage voor de production omgeving ziet er vrijwel hetzelfde uit, maar heeft een andere resource group en resource naam.

Als we alles samenvoegen in een pipeline die automatisch getriggered wordt op alle commits naar de main branch dan krijgen we het volgende resultaat:

trigger:
- main

pool:
  vmImage: ubuntu-latest

variables:
  bicepSourceFile: 'appservice.bicep'

stages:
- stage: build
  displayName: Build
  jobs: 
  - job: build
    displayName: Build bicep template
    steps: 
    - task: PowerShell@2
      displayName: Build Bicep file
      inputs:
        targetType: 'inline'
        script: 'az bicep build --file $(Build.SourcesDirectory)/${{ variables.bicepSourceFile }}' 

    - task: CopyFiles@2
      inputs:
        Contents: '$(Build.SourcesDirectory)/${{ variables.bicepSourceFile }}'
        TargetFolder: '$(Build.ArtifactStagingDirectory)'

    - publish: $(Build.ArtifactStagingDirectory)
      artifact: biceptemplate
  
- stage: deploy_test
  displayName: Deploy Test
  jobs:
  - deployment:
    displayName: 'Deploy App Service to Test'
    environment: Bicepdemo-Infra-test
    strategy:
      runOnce:
        deploy:
          steps:
            - task: AzureCLI@2
              inputs:
                azureSubscription: 'BicepDemoServiceConnection'
                scriptType: 'pscore'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  az group create --name rg-bicepdemo-test-01 --location westeurope
                  az deployment group create `
                    --resource-group rg-bicepdemo-test-01 `
                    --template-file $(Pipeline.Workspace)/biceptemplate/${{ variables.bicepSourceFile }} `
                    --parameters appServiceName='app-bicepdemo-test-01'
  condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))

- stage: deploy_prod
  displayName: Deploy Prod
  jobs:
  - deployment:
    displayName: 'Deploy App Service to Prod'
    environment: Bicepdemo-Infra-prod
    strategy:
      runOnce:
        deploy:
          steps:
            - task: AzureCLI@2
              inputs:
                azureSubscription: 'BicepDemoServiceConnection'
                scriptType: 'pscore'
                scriptLocation: 'inlineScript'
                inlineScript: |
                  az group create --name rg-bicepdemo-prod-01 --location westeurope
                  az deployment group create `
                    --resource-group rg-bicepdemo-prod-01 `
                    --template-file $(Pipeline.Workspace)/biceptemplate/${{ variables.bicepSourceFile }} `
                    --parameters appServiceName='app-bicepdemo-prod-01'
  condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))

Door de pipeline nu uit te voeren wordt eerst de bicep template gevalideerd, de bicep file naar onze test omgeving uitgerold en daarna naar onze productie omgeving doorgezet.

Pipeline configuratie

Als puntje op de i kunnen we nog een extra check toevoegen: alle deployments naar productie moeten eerst goedgekeurd worden. Dit doen we in de environment Bicepdemo-Infra-prod via Approvals and checks.

Het resultaat is dat de productie deployment stage goedgekeurd moet worden:

Approvals

← Terug
XPRTZ