DEV Community

Olivier Miossec
Olivier Miossec

Posted on

Using Azure Bicep with GitHub Actions

Note: this post was created just before the release of the 0.4 version of Bicep. In this version, a linter is available and reduces the need to use ARM-TTK.

Azure Bicep is ready for production. We can ask ourselves the question: How to set up a GitHub repository to deploy resources to Azure using Bicep with GitHub Actions.
We need to imagine a clear and secure workflow. The main branch is our unique source of truth. What we have in our bicep files in this branch should be the exact mirror of what we have in Azure.
In this situation, direct push to the main branch should be avoided. Instead, team members should use a dev branch to perform changes to Bicep files. Changes are then tested to see if there is anything wrong. Once tests are completed, a pull request will merge de changes to the main branch and start a deployment to Azure.

For the illustration, we will try to deploy a Windows VM with a virtual network.

To start, we need to create a branch protection rule for the master (or main) branch to ensure only pull requests are permitted and people do not push commit directly.
In Github, you can go to settings, then branches, and create a new Branch protection rule.
Click on the Add Rule button, add the name of the branch (master or main) et select Require to pull request reviews before merging (you may add include administrators if the account you use with Git is your administrator account)

Now if you try to push a commit to your main branch you should have this error
remote: error: GH006: Protected branch update failed for refs/heads/master.

Now that we cannot push our commit directly to the main branch, we need to create a new branch with our content.
We will start with a simple Linux VM with its VNET/Subnet

param vmName string = 'linuxvm'
param vnetName string = '01-bicep-decompile-vnet'
param adminUser string = 'myUser'
param vmSize  string = 'Standard_B2ms' 

var nicName_var = '${vmName}-nic'
var osVhdName = '${vmName}-vhd'
var location = 'francecentral'


resource vnetName_resource 'Microsoft.Network/virtualNetworks@2020-05-01' = {
  name: vnetName
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '172.16.5.0/24'
      ]
    }
    subnets: [
      {
        name: 'default'
        properties: {
          addressPrefix: '172.16.5.0/24'
        }
      }
    ]
    virtualNetworkPeerings: []
    enableDdosProtection: false
    enableVmProtection: false
  }
}

resource vmName_resource 'Microsoft.Compute/virtualMachines@2019-07-01' = {
  name: vmName
  location: location
  properties: {
    hardwareProfile: {
      vmSize: vmSize
    }
    storageProfile: {
      imageReference: {
        publisher: 'Canonical'
        offer: 'UbuntuServer'
        sku: '18.04-LTS'
        version: 'latest'
      }
      osDisk: {
        osType: 'Linux'
        name: osVhdName
        createOption: 'FromImage'
        caching: 'ReadWrite'
        managedDisk: {
          storageAccountType: 'Premium_LRS'
          id: resourceId('Microsoft.Compute/disks', osVhdName)
        }
        diskSizeGB: 30
      }
      dataDisks: []
    }
    osProfile: {
      computerName: vmName
      adminUsername: adminUser
      linuxConfiguration: {
        disablePasswordAuthentication: false
        provisionVMAgent: true
      }
      secrets: []
      allowExtensionOperations: true
      requireGuestProvisionSignal: true
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: nicName.id
        }
      ]
    }
  }
}

resource nicName 'Microsoft.Network/networkInterfaces@2020-05-01' = {
  name: nicName_var
  location: location
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAddress: '172.16.5.4'
          privateIPAllocationMethod: 'Dynamic'
          subnet: {
            id: vnetName_default.id
          }
          primary: true
          privateIPAddressVersion: 'IPv4'
        }
      }
    ]
    dnsSettings: {
      dnsServers: []
    }
    enableAcceleratedNetworking: false
    enableIPForwarding: false
  }
}

resource vnetName_default 'Microsoft.Network/virtualNetworks/subnets@2020-05-01' = {
  name: '${vnetName_resource.name}/default'
  properties: {
    addressPrefix: '172.16.5.0/24'
    delegations: []
    privateEndpointNetworkPolicies: 'Enabled'
    privateLinkServiceNetworkPolicies: 'Enabled'
  }
}
Enter fullscreen mode Exit fullscreen mode

No, that we have our bicep file and our branch policy we need the tool to test the bicep file.

To test our bicep file, we need to convert it into an ARM JSON template file and use ARM Template Toolkit to test it.
The best option to use these tools with GitHub Actions is to use a container image to build a custom task.
We will need, a docker file to create our image, an action.yml file to describe how the image will be used in the workflow, and a script to convert the bicep file and test it with ARM-TTK.

Let start with the docker file. We need an image with Azure Powershell module, Bicep binary, and arm-ttk module.

FROM mcr.microsoft.com/azure-powershell:5.9.0-ubuntu-18.04

ENV PSModulePath /usr/local/share/powershell/Modules:/opt/microsoft/powershell/7/Modules:/root/.local/share/powershell/Modules

RUN pwsh -c install-module -name pester -force

RUN curl -Lo arm-ttk.zip https://azurequickstartsservice.blob.core.windows.net/ttk/latest/arm-template-toolkit.zip

RUN pwsh -c Expand-Archive ./arm-ttk.zip

RUN pwsh -c copy-item  -Path ./arm-ttk/arm-ttk/ -Destination /usr/local/share/powershell/Modules/ -recurse -force 


RUN curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64

RUN chmod +x ./bicep

RUN mv ./bicep /usr/local/bin/bicep

ADD entrypoint.ps1 /entrypoint.ps1

ENTRYPOINT ["pwsh", "/entrypoint.ps1"]
Enter fullscreen mode Exit fullscreen mode

This dockerfile is created inside a folder named armtest.

Now, we need the entrypoint.ps1 file, the purpose of this script is to convert the bicep file and test the ARM template file with ARM-TTK.

[CmdletBinding()]
param(
    [Parameter(Mandatory=$true)]
    [ValidateScript({
        if( -Not ($_ | Test-Path) ){
            throw "File or folder does not exist"
        }
        return $true
    })]
    [System.IO.FileInfo]$inputBicepFile
)


try {
    & bicep build $inputBicepFile
}
catch {
    Write-Host "An error during bicep build:"
    Write-Host $_
    exit 1
}


$templateFileName = Join-Path -Path $inputBicepFile.DirectoryName -ChildPath "$($inputBicepFile.basename).json" 



try {

    get-item /usr/local/share/powershell/Modules/arm-ttk/

    import-module /usr/local/share/powershell/Modules/arm-ttk/arm-ttk.psd1

    $TestResultArray = Test-AzTemplate -TemplatePath $templateFileName -Skip Template-Should-Not-Contain-Blanks 

    if ($null -ne ($TestResultArray | Where-Object { -not $_.Passed })){

        Write-Output $TestResultArray | Where-Object { -not $_.Passed }

        throw "Error in Template"

    }
}
catch {
    Write-Host "An error while testing the template file:"
    Write-Host $_
    exit 1
}
Enter fullscreen mode Exit fullscreen mode

The script takes a param, the path of the bicep file. It tries, then, to build it, converting it into an ARM JSON template file. The second part of the script tests the template using the ARM-TTK test-aztemplate cmdlet.
To limit false positives, the script removes the tests on blank properties by using -Skip Template-Should-Not-Contain-Blanks.
If one or more tests fails, the scripts will exist

The last thing to do is to build the action metadata file. It defines how the workflow will be implemented in GitHub Action. This YAML will describe the input, the bicep file to test, and how to run the action, meaning which container to run with which arguments

name: 'AzureBicepTest'
author: 'omiossec'
description: 'Perform Bicep Tests'
branding:
  icon: 'cloud'
  color: 'blue'
inputs:
  bicepfile:
    description: 'Bicep file to test'
    default: "./vm.bicep"
    required: false
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ inputs.bicepfile }}
Enter fullscreen mode Exit fullscreen mode

Our Github action to test the file is ready. We can set up our first workflow. We want to run this workflow against all branches
To create the workflow you just need to create a ./github/workflows folder at the root of your repository and add a YAML file to describe what you want to achieve.

If ARM-TTK finds an error, the workflow will report it. If not, we can merge the branch to the main branch. At this moment we can deploy to Azure.

There is a required thing to do if we want to deploy to Azure, a service principal with the privilege to deploy objects in a resource group.

First let's create the resource group

New-AzResourceGroup -Name rg-test-bicep -Location westeurope
Enter fullscreen mode Exit fullscreen mode

Then create a service principal and assign it the role contributor on our resource group.

$servicePrincipal = New-AzADServicePrincipal -DisplayName "bicep-demo" -SkipAssignment 


New-AzRoleAssignment -RoleDefinitionName contributor -ServicePrincipalName $ServicePrincipal.ApplicationId -ResourceGroupName 'rg-test-bicep'
Enter fullscreen mode Exit fullscreen mode

We will need to extract the secret

$SecureStringBinary = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($servicePrincipal.Secret)

 $servicePrincipalSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($SecureStringBinary)
Enter fullscreen mode Exit fullscreen mode

With these elements, we can build the authentication object

$azureContext = Get-AzContext

$servicePrincipalObject = @{
    clientId        = $servicePrincipal.ApplicationId
    clientSecret    = $servicePrincipalSecret
    subscriptionId  = $azureContext.Subscription.Id
    tenantId        = $azureContext.Tenant.TenantId
}

$servicePrincipalObject | ConvertTo-Json
Enter fullscreen mode Exit fullscreen mode

The JSON data can be used in a GitHub secret that we can later use in our workflow. Simply go to the repository settings and then to secret. Add a new secret, AZURE_SP, and add the JSON data.

The secret will be used in the second workflow, triggered by a pull request on the main branch.
The task is deploying the bicep file, but first, we need to provide parameters to the bicep file during the deployment. Like in ARM templates, you can use the inline form or use a parameters file.
For the parameter file, you can use the ARM Template file format, like this.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "vmName": {
        "value": "vmtest"
      },
      "vnetName": {
        "value": "test-vnet"
      },
      "adminUser": {
        "value": "adm-bicep"
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Now we can create the workflow, to deploy resources to Azure. No need to create a custom action using Docker. We can use the GitHub Action for Azure Resource Manager (ARM) deployment.

name: bicep-deploy

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:

      # Checkout code
    - uses: actions/checkout@main

      # Log into Azure
    - uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_SP }}

      # Deploy Bicep file
    - name: deploy
      uses: azure/arm-deploy@v1
      with:
        subscriptionId: 087f441f-6200-4226-bac3-22a1dbe98fae
        resourceGroupName: 'rg-test-bicep'
        template: ./vm.bicep
        parameters: ./vm.parameters.json
Enter fullscreen mode Exit fullscreen mode

The workflow uses the azure/login@v1 action to log in to Azure with the JSON object stored in the AZURE_SP secret variable and then use the azure/arm-deploy@1 to deploy the bicep file in the subscription using the parameter file we created earlier.

This is a simple example of how you can use GitHub action to create a two steps workflow for your Bicep deployment. One step to control your modification and the other to deploy to Azure.

Top comments (0)