Effortlessly Configure GitHub Repositories for Azure Deployment via OIDC
Scripting your Azure-Ready GitHub Repository using Azure and GitHub CLI
What if we could script the creation and configuration of a GitHub Repository so that it is ready to provision or deploy Azure resources from a GitHub Actions pipeline? We will do that in this article using the Azure CLI and GitHub CLI.
The Objective
The goal is to go from nothing to running a GitHub Actions workflow that authenticates to Azure using Open ID Connect (so without secret credentials) in a newly created GitHub repository.
The workflow we plan to run is as follows:
name: Run Azure Login with OIDC
on:
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- 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: 'Run az commands'
run: |
az account show
az group list
This workflow is an example coming from the GitHub documentation showing how to configure GitHub Actions workflow to access Azure resources protected by Microsoft Entra.
To run this workflow we will need to automate the configuration of these resources:
The Script
A word about the tools used
I will be using PowerShell which is cross-platform. However, if you prefer using a different shell, you will simply need to adjust some syntax (such as the environment variable declarations) to ensure compatibility.
To create and configure the Microsoft Entra ID resources, we will need the Azure CLI.
To create and configure the GitHub repository, we will need the GitHub CLI.
Create the repository on GitHub
Let's assume we are already in a new directory with the YAML workflow file .github\workflows\main.yml
in it.
First, we can initialize the git repository.
git init
git add .
git commit -m "Intialize repository with the GitHub Actions workflow file"
Second, we can create the GitHub repository and push the git repository we just initialized in it.
$repositoryName = "MyAzureReadyRepository"
gh repo create $repositoryName --private --source=. --push
--public
flag instead of the --private
one if you want your GitHub repository to be public.The repository's full name (containing the organization name) can be retrieved like this:
$repositoryFullName=$(gh repo view --json nameWithOwner -q ".nameWithOwner")
--json
flag converts the output format to JSON which, combined with the --q
flag can be handy for filtering or formatting a command output. More on that in the documentation.Create the Microsoft Entra ID resources
Later, we will need the subscription and the tenant identifiers. Let's retrieve them now and take this opportunity to check that we are logged in on the correct tenant with the correct subscription selected.
$subscriptionId=$(az account show --query "id" -o tsv)
$tenantId=$(az account show --query "tenantId" -o tsv)
--query
flag to filter a command output. There are also different output formats. The tsv
(tab-separated values) one is useful for capturing a value in an environment variable. If you are not very familiar with the Azure CLI, you can check my article on the topic here.To create the app registration and its associated service principal, we can execute the following commands:
$appId=$(az ad app create --display-name "GitHub Action OIDC for ${repositoryFullName}" --query "appId" -o tsv)
$servicePrincipalId=$(az ad sp create --id $appId --query "id" -o tsv)
We can now assign the contributor role to the service principal on the subscription.
az role assignment create --role contributor --subscription $subscriptionId --assignee-object-id $servicePrincipalId --assignee-principal-type ServicePrincipal --scope /subscriptions/$subscriptionId
Creating federated credentials is a bit more complex as one of the arguments needs to be an in-line JSON string.
$parametersJson = @{
name = "FederatedIdentityForWorkshop"
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:${repositoryFullName}:ref:refs/heads/main"
description = "Deployments for ${repositoryFullName}"
audiences = @(
"api://AzureADTokenExchange"
)
}
subject
property here specifies that the GitHub Actions workflow from the created repository is only authorized to authenticate to Azure when it runs on the main branch. Of course, there are other possible configurations, such as those involving pull requests or environments. Consult the documentation to learn more about these options.To make this JSON string an inline string with escaped quotes that works for the Azure CLI, we have to transform the string using a command I found in this blog article.
$parameters = $($parametersJson | ConvertTo-Json -Depth 100 -Compress).Replace("`"", "\`"")
And finally, we can create the federated credentials.
az ad app federated-credential create --id $appId --parameters $parameters
Configure the GitHub Actions and run the workflow
For the OIDC authentication to function properly, we need to set 3 GitHub Actions Secrets (could also be GitHub Actions variables as there are not really secrets):
The identifier of the Azure tenant
The identifier of the Azure subscription
The application identifier of the app registration
gh secret set AZURE_TENANT_ID --body $tenantId
gh secret set AZURE_SUBSCRIPTION_ID --body $subscriptionId
gh secret set AZURE_CLIENT_ID --body $appId
We can directly run the workflow from the GitHub CLI, and watch the run until it is completed.
gh workflow run main.yml
$runId=$(gh run list --workflow=main.yml --json databaseId -q ".[0].databaseId")
gh run watch $runId
r
Full script
# Initialize git repository with current code
# You should have added the main.yml workflow file in the `.github\workflows` directory
git init
git add .
git commit -m "Intialize repository with the GitHub Actions workflow file"
# Create a new remote private GitHub repository
$repositoryName = "MyAzureReadyRepository"
gh repo create $repositoryName --private --source=. --push
# Retrieve the repository full name (org/repo)
$repositoryFullName=$(gh repo view --json nameWithOwner -q ".nameWithOwner")
# Retrieve the current subscription and current tenant identifiers
$subscriptionId=$(az account show --query "id" -o tsv)
$tenantId=$(az account show --query "tenantId" -o tsv)
# Create an App Registration and its associated service principal
$appId=$(az ad app create --display-name "GitHub Action OIDC for ${repositoryFullName}" --query "appId" -o tsv)
$servicePrincipalId=$(az ad sp create --id $appId --query "id" -o tsv)
# Assign the contributor role to the service principal on the subscription
az role assignment create --role contributor --subscription $subscriptionId --assignee-object-id $servicePrincipalId --assignee-principal-type ServicePrincipal --scope /subscriptions/$subscriptionId
# Prepare parameters for federated credentials
$parametersJson = @{
name = "FederatedIdentityForWorkshop"
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:${repositoryFullName}:ref:refs/heads/main"
description = "Deployments for ${repositoryFullName}"
audiences = @(
"api://AzureADTokenExchange"
)
}
# Change parameters to single line string with escaped quotes to make it work with Azure CLI
# https://medium.com/medialesson/use-dynamic-json-strings-with-azure-cli-commands-in-powershell-b191eccc8e9b
$parameters = $($parametersJson | ConvertTo-Json -Depth 100 -Compress).Replace("`"", "\`"")
# Create federated credentials
az ad app federated-credential create --id $appId --parameters $parameters
# Create GitHub secrets needed for the GitHub Actions
gh secret set AZURE_TENANT_ID --body $tenantId
gh secret set AZURE_SUBSCRIPTION_ID --body $subscriptionId
gh secret set AZURE_CLIENT_ID --body $appId
# Run workflow
gh workflow run main.yml
$runId=$(gh run list --workflow=main.yml --json databaseId -q ".[0].databaseId")
gh run watch $runId
# Open the repostory in the browser
gh repo view -w
Final Thoughts
I am very glad to have scripted the creation and configuration of a GitHub repository ready to deploy to Azure. Even if I had already done the same using Pulumi, having a small script can sometimes be more convenient than having a full IaC program. In my case, I needed to automate that for a workshop, so it was easier to give participants a script to execute.
However, I must admit that developing this script proved to be much more challenging than provisioning the same resources using Pulumi. I didn't expect it to take so much time: browsing the CLI documentation, finding the correct syntax, and understanding the cause of failures. In contrast, using the GitHub and Azure Pulumi providers in my TypeScript code turned out to be a much more enjoyable experience.
Nevertheless, I was pleased to be introduced to the GitHub CLI, which I hadn't explored extensively until now. While I found it very useful, a few things bothered me. Not all commands can be used with the --json
and -q
parameters, which is not very convenient for scripting. Commands that create things (repo, workflow runs) don't return the identifier of the thing they create. I wish GitHub CLI would be more similar to Azure CLI in these matters. I have no doubt these will be improved over time.
As for Azure CLI, I am still a big fan, although a bit disappointed to have struggled with the inline JSON string.
Keep learning, keep sharing.