Sharing Bicep Modules with Azure Container Registry
Tags: azure, bicep, registry, github
Published: 2022-10-13 21:31:02

---
title: Sharing Bicep Modules with Azure Container Registry
published: true
date: 2022-10-11 14:00:00 UTC
tags: azure, bicep, registry, github
canonical_url: https://paulyu.dev/article/sharing-bicep-modules/
---

One of the things I do as a Cloud Native Advocate at Microsoft is build end-to-end lab scenarios in the [https://aka.ms/oss-labs](https://aka.ms/oss-labs) repo. Most of the demo scenarios we aim to cover is in and around the container space and a majority of the labs uses [Azure Bicep](https://learn.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=bicep) to declaratively provision Azure infrastructure. As more labs get spun up, there is a potential for redundant Bicep code. You might have already guessed, there's a need for re-usable code to spin up AKS clusters.

The easy solution here is to place Bicep code into local modules, but referencing local modules presents a few challenges... what happens if someone updates a local module and doesn't update all the code that uses it? Without proper versioning in place, there is the potential to break things. If only there was a Bicep registry where we can publish and share Bicep modules... 🤔

A quick internet search helped me discover that about a year ago, the [@BicepLang](https://twitter.com/BicepLang) team added capability to [share Bicep modules using Azure Container Registry (ACR)](https://github.com/Azure/bicep/releases/tag/v0.4.1008).

This was exactly the solution I was looking for!

There's also a [Bicep public module registry](https://github.com/Azure/bicep-registry-modules) with a handful of published modules and if you'd like to contribute to this registry, please check out the [contributing guide](https://github.com/Azure/bicep-registry-modules/blob/main/CONTRIBUTING.md) to get started (*I might publish a few there myself in the near future*).

But my goal here is to demonstrate you can build your own private registry and share Bicep modules within your team/organization. 

So in this article, we'll walk through the following:

* Creating a Bicep module
* Manually publish a Bicep module to ACR
* Automating Bicep module publishing with GitHub Actions 

... and as a bonus I'll cover how GitHub Actions can use the newer OpenID Connect (OIDC) mechanism for Azure authentication

Let's go!

## Pre-requisites

Before you get started, make sure you have the following tools:

* [Visual Studio Code](https://code.visualstudio.com/Download)
* [Azure Subscription](https://azure.microsoft.com/get-started/) with Owner privileges
* [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) with [Azure Bicep](https://docs.microsoft.com/azure/azure-resource-manager/bicep/install#azure-cli) installed
* [GitHub Account](https://github.com/)
* [GitHub CLI](https://cli.github.com/manual/installation)
* [Git CLI](https://github.com/git-guides/install-git)
* Bash terminal (if you are not on Linux or macOS, try WSL or Azure Cloud Shell)

## Build a Bicep module

Since we need an ACR resource to publish modules into, let's use that as a first module to publish.

Start by creating a new repo on GitHub, cloning it on your machine, and opening it with VSCode. We'll use the GitHub CLI for this.

```bash
# random name to use as a name for the repo and azure resources
name=bicep$RANDOM

# make sure you are logged into your github account
gh auth login

# create a private repo
gh repo create $name --private

# clone the repo
gh repo clone $name

# drop into the repo directory
cd $name

# open with vscode
code .
```

I like how the files and folders are structured within the [Bicep public module registry](https://github.com/Azure/bicep-registry-modules). Let's follow a similar pattern.

```bash
# make sure you are in the root of the project directory
mkdir -p bicep/modules/azure-container-registry
cd bicep/modules/azure-container-registry
```

Add a few files in the directory that will help others consume the module.

```bash
echo "# azure-container-registry" >> README.md
touch main.bicep
touch metadata.json
```

Here is the directory structure so far:

```text
.
└── bicep
    └── modules
        └── azure-container-registry
            ├── README.md
            ├── main.bicep
            └── metadata.json
```

Here is how each of the files will be used:

* `bicep/modules/azure-container-registry/README.md` will be used to document the intent of the module and how to use it
* `bicep/modules/azure-container-registry/main.bicep` is where we'll place the Bicep code
* `bicep/modules/azure-container-registry/metadata.json` is used to maintain versioning of the Bicep modules

> Versioning modules is a best practice as it helps with regressions and allows you to introduce breaking changes as modules evolve over time.

Open the `bicep/modules/azure-container-registry/metadata.json` file and add the following JSON code.

```json
{
  "version": {
    "major": 0,
    "minor": 1
  }
}
```

> If you'd like to follow the same schema for versioning as in the Bicep public module registry, you can take a look at this [resource](https://aka.ms/bicep-registry-module-version-file-schema#). I'll use the JSON above to make things a bit simpler.

In VSCode, open the `bicep/modules/azure-container-registry/main.bicep` file and add in the following Bicep code.

```bash
param name string
param location string
param tags object

@allowed([
  'Basic'
  'Standard'
  'Premium'
])
@description('Defaults to Standard')
param sku string = 'Standard'

@allowed([
  'SystemAssigned'
  'UserAssigned'
])
@description('Two options are available: SystemAssigned or UserAssigned')
param managedIdentityType string

@description('Required when managed identity type is set to UserAssigned')
param userAssignedIdentities object = {}

param adminUserEnabled bool = false

param anonymousPullEnabled bool = false

param publicNetworkAccess bool = false

resource registry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = {
  name: name
  location: location
  tags: tags
  sku: {
    name: sku
  }
  identity: {
    type: managedIdentityType
    userAssignedIdentities: managedIdentityType != 'SystemAssigned' ? userAssignedIdentities : null
  }
  properties: {
    adminUserEnabled: adminUserEnabled
    anonymousPullEnabled: anonymousPullEnabled
    publicNetworkAccess: publicNetworkAccess ? 'Enabled' : 'Disabled'
  }
}

output name string = registry.name
```

## Deploy Bicep template

With the module code added, let's deploy it so that we have an ACR in place to publish modules into.

Head back to the root of the project directory create a new `main.bicep` file. This is where we will consume the module.

```bash
cd ../../../
touch main.bicep
```

Your directory structure should now look like this:

```text
.
├── bicep
│   └── modules
│       └── azure-container-registry
│           ├── README.md
│           ├── main.bicep
│           └── metadata.json
└── main.bicep
```

Open the `main.bicep` file from the root of the project directory and add the following Bicep code.

```bash
targetScope = 'subscription'

param name string
param location string
param tags object = {}

// Set up the resource group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: 'rg-${name}'
  location: location
  tags: tags
}

// Set up the container registry
module acr 'bicep/modules/azure-container-registry/main.bicep' = {
  scope: rg
  name: 'acrDeploy'
  params: {
    name: 'acr${toLower(name)}'
    location: location
    tags: tags
    sku: 'Basic'
  }
}
```

The code above will deploy a resource group into your subscription then use the local module to deploy an ACR resource into the resource group.

The ACR SKU we are deploying here is `Basic`. This is fine for demo purposes but for production you will want to use either the `Standard` or `Premium` SKUs. By default, ACR is private. If you'd like to share your Bicep modules to anonymous users, you will need to deploy a `Premium` SKU.

> Notice how we referenced the source for the ACR module. We used the file path `bicep/modules/azure-container-registry/main.bicep` to locally reference the module. **This will change once we publish the module to ACR.**

Run the following command to deploy.

```bash
# make sure you are logged into azure
az login

# location to deploy resources into (change this value if needed)
location=eastus

# run the deployment
az deployment sub create \
  --name "$name-deploy" \
  --location $location \
  --template-file ./main.bicep \
  --parameters name=$name location=$location
```

Validate the deployment and store the ACR `loginServer` property in a variable.

```bash
loginServer=$(az acr show -n acr${name} --query loginServer -o tsv)
```

## Publish your module (manually)

Now we are ready to publish the module. You can only publish one module at a time, so you need to pass in the module's `main.bicep` file as the value for the `--file` parameter. We'll need to use the ACR login server URL to build the value for the `--target` parameter. This value must be formatted as `br:<artifact-uri>:<tag>`.

Run this command to publish.

```bash
az bicep publish \
  --file bicep/modules/azure-container-registry/main.bicep \
  --target "br:${loginServer}/bicep/modules/azure-container-registry:v0.1"
```

Validate the module has been published to ACR.

```bash
az acr repository show-tags \
  --name acr$name \
  --repository bicep/modules/azure-container-registry
```

## Use your published module

With the module published, we can update the `main.bicep` deployment file in the root of your project directory to reference the module from ACR instead of locally.

Update the module reference to resemble this instead of using the local module path.

```bash
module acr 'br:<REPLACE_THIS_WITH_YOUR_ACR_NAME>.azurecr.io/bicep/modules/azure-container-registry:v0.1' = {
  scope: rg
  name: 'acrDeploy'
  params: {
    name: 'acr${toLower(name)}'
    location: location
    tags: tags
    sku: 'Basic'
  }
}
```

Run the deployment again to ensure Bicep is able to pull down the module from ACR.

```bash
az deployment sub create \
  --name "$name-deploy" \
  --location $location \
  --template-file ./main.bicep \
  --parameters name=$name location=$location
```

Note the format when referencing the Bicep module as an OCI artifact in ACR is `br:<artifact-uri>:<tag>`. Optionally you can configure a Bicep to use an alias instead of referencing the entire URL of your ACR server.

Add a new file called `bicepconfig.json` in the root of the project directory.

```bash
touch bicepconfig.json
```

Your project directory should now look like this:

```text
.
├── bicep
│   └── modules
│       └── azure-container-registry
│           ├── README.md
│           ├── main.bicep
│           └── metadata.json
├── bicepconfig.json
└── main.bicep
```

Add the following JSON code to the `bicepconfig.json` file.

```json
{
  "moduleAliases": {
    "ts": {},
    "br": {
      "my-acr": {
        "registry": "<REPLACE_THIS_WITH_YOUR_ACR_NAME>.azurecr.io"
      }
    }
  }
}
```

Here, I've aliased my ACR login server URL as `my-acr`, so I can reference the module using this format: `br/<module-alias>:<module-name-or-path>:<tag>`.

> Note when using a module alias `br` is followed by a slash `/` not a colon `:`.

Let's update `main.bicep` again to use an alias instead.

```bash
module acr 'br/my-acr:bicep/modules/azure-container-registry:v0.1' = {
  scope: rg
  name: 'acrDeploy'
  params: {
    name: 'acr${toLower(name)}'
    location: location
    tags: tags
    sku: 'Basic'
  }
}
```

Before we re-run the deployment, let's clear any local Bicep cache to ensure we are getting a fresh copy from the remote ACR server. The following command will force Bicep to reload any modules that are referenced in the `main.bicep` file.

```bash
az bicep restore --file main.bicep --force
```

Now run the deployment one more time to ensure it still works.

```bash
az deployment sub create \
  --name "$name-deploy" \
  --location $location \
  --template-file ./main.bicep \
  --parameters name=$name location=$location
```

## Publish your module with GitHub Actions

Great! We now have a module published to ACR and have verified the push and pull works 🎉

What if we need to make changes to a module? What happens if we have many more modules in the `bicep/modules` directory? Publishing each module individually will become unwieldy over time. So we'll implement a GitHub Action workflow to automate this process.

With GitHub Actions, we can use the [Azure Login Action](https://github.com/marketplace/actions/azure-login) to authenticate against and make changes to Azure resources, but historically the authentication mechanism relied on client secrets (e.g., passwords). This required users to store client secrets as a [GitHub encrypted secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) stored in the GitHub repo. This introduces problems such as potentially leaking credentials, forgetting to renew the client secrets, etc.

Now, there is a better way. We can now use the Azure Login action with OIDC by adding a federated credential to an Azure AD Application to enable trust between Azure and tokens issued by GitHub Actions.

Let's use Azure CLI and Microsoft Graph REST APIs to configure this.

> ⚠️ WARNING
>
> You must have the ability to create Azure AD App Registrations to proceed with the steps below.

Create an Azure AD application

```bash
appId=$(az ad app create --display-name $name --query appId -o tsv)
```

Azure AD apps on their own cannot be granted roles to do things in your Azure subscription. You need a service principal for that. So let's add a service principal to the Azure AD application.

```bash
assigneeObjectId=$(az ad sp create --id $appId --query id -o tsv)
```

We need to grant the service principal permissions to work with resources in your resource group. Put your subscription ID, tenant ID, and resource group name into variables and grant permission to it.

```bash
subscriptionId=$(az account show --query id -o tsv)
tenantId=$(az account show --query tenantId -o tsv)
resourceGroupName=rg-$name

# grant the contributor role to the service principal at the resource group scope
az role assignment create \
  --role contributor \
  --subscription $subscriptionId \
  --assignee-object-id $assigneeObjectId \
  --assignee-principal-type ServicePrincipal \
  --scope /subscriptions/$subscriptionId/resourceGroups/$resourceGroupName
```

Add the federated credential to the Azure AD app registration.

```bash
appObjectId=$(az ad app show --id $appId --query id -o tsv)
repo=<YOUR_GITHUB_ACCOUNT_NAME>/$name
credentialName=main
description=Demo

az rest \
  --method POST \
  --uri "https://graph.microsoft.com/beta/applications/${appObjectId}/federatedIdentityCredentials" \
  --body "{'name':'${credentialName}','issuer':'https://token.actions.githubusercontent.com','subject':'repo:${repo}:ref:refs/heads/main','description':'${description}','audiences':['api://AzureADTokenExchange']}"
```

Now, add some secrets to your GitHub repo so that your Action workflow can use these as variables.

```bash
gh secret set AZURE_CLIENT_ID --body "${appId}" --repos $repo
gh secret set AZURE_TENANT_ID --body "${tenantId}" --repos $repo
gh secret set AZURE_SUBSCRIPTION_ID --body "${subscriptionId}" --repos $repo
gh secret set ACR_SERVER --body "${loginServer}" --repos $repo
```

Now we're ready to code up our Action. Create a new directory and GitHub Action workflow file.

```bash
mkdir -p .github/workflows
touch .github/workflows/publish_bicep_modules.yml
```

Our directory structure should now look like this:

```text
.
├── .github
│   └── workflows
│       └── publish_bicep_modules.yml
├── bicep
│   └── modules
│       └── azure-container-registry
│           ├── README.md
│           ├── main.bicep
│           └── metadata.json
├── bicepconfig.json
└── main.bicep
```

Add the following YAML code to the `publish_bicep_modules.yml` file.

```yml
name: Publish Bicep module

on:
  push:
    branches: [ "main" ]

  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: 'Az CLI login'
        uses: azure/login@v1
        with:
            client-id: ${{ secrets.AZURE_CLIENT_ID }}
            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
            subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      
      - name: Install Bicep
        run: |
          az version
          az bicep install
      - name: Publish Bicep modules to Azure Container Registry
        run: |
          # loop through each directory and publish
          for d in */
          do
            cd $d
            version="v$(cat metadata.json | jq -r '.version.major').$(cat metadata.json | jq -r '.version.minor')"
            if [ -z "$(az acr repository show -n ${{ secrets.ACR_SERVER }} --image bicep/modules/${d::-1}:$version 2>/dev/null)" ]; 
            then 
              echo "Publishing br:${{ secrets.ACR_SERVER }}/bicep/modules/${d::-1}:$version";
              az bicep publish --file main.bicep --target "br:${{ secrets.ACR_SERVER }}/bicep/modules/${d::-1}:$version";
            else 
              echo "Skipping br:${{ secrets.ACR_SERVER }}/bicep/modules/${d::-1}:$version as it already exists";
            fi
            cd - > /dev/null
          done
        working-directory: bicep/modules
```

Here are some important elements of this workflow.

The bit of code below allows the GitHub Action to write ID tokens which will be presented to Azure AD upon authentication request.

```text
permissions:
  id-token: write
  contents: read
```

The bit of code below uses the `azure/login@v1` action to log into Azure. Notice how the `with` block here does not include a password (only the `client-id`, `tenant-id` and `subscription-id`). With the `permissions` element set above, the Azure Login Action knows to use an OAuth 2.0 authentication flow instead.

```text
- name: 'Az CLI login'
  uses: azure/login@v1
  with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
```

The bit of code below will loop through any child directory in `bicep/modules`, build a version number from `metadata.json` (for OCI artifact tagging), and publish the module if the tag does not already exist.

```text
- name: Publish Bicep modules to Azure Container Registry
  run: |
    # loop through each directory and publish
    for d in */
    do
      cd $d
      version="v$(cat metadata.json | jq -r '.version.major').$(cat metadata.json | jq -r '.version.minor')"
      if [ -z "$(az acr repository show -n ${{ secrets.ACR_SERVER }} --image bicep/modules/${d::-1}:$version 2>/dev/null)" ]; 
      then 
        echo "Publishing br:${{ secrets.ACR_SERVER }}/bicep/modules/${d::-1}:$version";
        az bicep publish --file main.bicep --target "br:${{ secrets.ACR_SERVER }}/bicep/modules/${d::-1}:$version";
      else 
        echo "Skipping br:${{ secrets.ACR_SERVER }}/bicep/modules/${d::-1}:$version as it already exists";
      fi
      cd - > /dev/null
    done
  working-directory: bicep/modules
```

> If there is a better way to parse through the files using Bash, I'd love to know 😊

With all our code in place, we can finally commit and push to the remote repo, and watch the GitHub Action do its thing.

```bash
git add .
git commit -m 'Initial commit'
git push
```

You can view the output of the workflow using the following commands

```bash
# get a list of runs
gh run view

# from the list of runs, view the log of a particular job
gh run view --log --job=<YOUR_JOB_ID>
```

You should see a message that resembles the following near the bottom of the log output.

```text
Publishing br:***/bicep/modules/azure-container-registry:v0.1
```

You can also manually trigger the Action using the `workflow_dispatch`

```bash
gh workflow run publish_bicep_modules.yml
gh run watch
```

If you view the run log again, you'll notice a different message. This time you will see the following because the version already exists in ACR.

```text
Skipping br:***/bicep/modules/azure-container-registry:v0.1 as it already exists
```

To upload a new version all you need to do is update the version number in the `metadata.json` file, commit and push the code again and the GitHub Action will take care of the rest. Easy, right? 😉

## Summary

As a recap, we created new GitHub repo and added a Bicep module to provision an ACR resource. Then we deployed the ACR resource using a local module reference. With the ACR in place, we published the module to it, updated the `main.bicep` file and verified its functionality. From there we configured an Azure AD app registration and established a federated credential with GitHub Actions so that the Actions workflow can leverage OIDC to authenticate and work within your Azure subscription. Finally, we committed the code and pushed it to the remote GitHub repo and watched the Action publish modules.

Hopefully, this walk through will help you organize your Bicep modules and share them with others on your team, organization, or publicly (HINT: you will need the Premium SKU to allow anonymous pulls).

If you found this sort of workflow helpful, or found issues with it, please let me know in the comments below or via [Twitter @pauldotyu](https://twitter.com/pauldotyu). Also, be sure to check out the linked in the Resources section below.

Cheers!

## Resources

* [What is Bicep?](https://learn.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=bicep)
* [GitHub: Azure Bicep](https://github.com/Azure/bicep)
* [Bicep Public Module Registry](https://github.com/Azure/bicep-registry-modules/)
* [Create private registry for Bicep modules](https://learn.microsoft.com/azure/azure-resource-manager/bicep/private-module-registry?tabs=azure-cli)
* [MS Learn: Share Bicep modules by using private registries](https://learn.microsoft.com/training/modules/share-bicep-modules-using-private-registries/)
* [Use GitHub Actions to connect to Azure](https://learn.microsoft.com/azure/developer/github/connect-from-azure?tabs=azure-cli%2Clinux)
* [What is GitHub Actions for Azure](https://learn.microsoft.com/azure/developer/github/github-actions)
* [Sample Code: pauldotyu/azure-bicep-modules](https://github.com/pauldotyu/azure-bicep-modules)