Pushing Multi-Architecture Container Images
Introduction
My previous article, Building Multi-Architecture Container Images, covered the basics of building multi-architecture container images using Docker Buildx. In this article, we’ll explore how to push multi-architecture container images to Azure Container Registry (ACR) using GitHub Actions.
What is a GitHub Action?
GitHub Actions is a continuous integration and continuous deployment (CI/CD) platform built into GitHub. It allows you to automate, customize, and execute your software development workflows. Using GitHub Actions, you can create workflows that respond to GitHub events, such as push, issue creation, or a new release.
Workload identity federation for GitHub Actions
ACR is a private container registry in Azure. In order to push container images to ACR, you need to authorization to push into it.
There are a few ways to authenticate to ACR. One way is to enable admin user access on ACR and use the admin username and password; this is like a superuser account that can do anything within ACR. Another way is to use a service principal that has the AcrPush
Azure role assigned to it; this is a more granular approach that only allows the service principal to push container images to ACR.
Using the service principal approach is the preferred method, but we need to consider the two ways it can authenticate to Azure. One way is to create a client secret for the service principal and store the credentials in GitHub Secrets. The other way is to use a workload identity in Azure and federate it with GitHub Actions.
Workload identity federation is a relatively newer approach to authenticating to Azure and is the preferred method as it enables you to go “passwordless” and not store any passwords in GitHub Secrets 🔐
I’ve written a blog post that covers how to use workload identity to authenticate to Azure. So check that out for more info.
Create a user-assigned managed identity
In order to implement workload identity federation for GitHub Actions, we need to create a user-assigned managed identity in Azure.
We’ll continue to use the
osinfo
repository from my previous article. If you haven’t already gone through it, head back to the previous article and follow the steps to create the Azure resources and fork/clone the repository.
Run the following commands to create a new user-assigned managed identity in Azure and assign the AcrPush
roles to the managed identity:
# Set the resource group name
RG_NAME=rg-multiarch
# Set the managed identity name
MANAGED_IDENTITY_NAME=mi-multiarch
# Create the managed identity and return the service principal object id
MANAGED_IDENTITY_OBJECT_ID=$(az identity create \
--resource-group $RG_NAME \
--name $MANAGED_IDENTITY_NAME \
--query principalId -o tsv)
# Get the ACR resource id
ACR_RESOURCE_ID=$(az acr list --resource-group $RG_NAME --query "[0].id" -o tsv)
# Get the ACR name
ACR_NAME=$(az acr list --resource-group $RG_NAME --query "[0].name" -o tsv)
# Grant the managed identity access to ACR resource
az role assignment create \
--role "AcrPush" \
--assignee-object-id $MANAGED_IDENTITY_OBJECT_ID \
--assignee-principal-type ServicePrincipal \
--scope $ACR_RESOURCE_ID
You could also use an Azure Application Registration with a Service Principal but I prefer to use a user-assigned managed identity because it ends up being a resource in Azure that you can manage and delete. Some organizations have policies that prevent the creation of Azure Application Registrations, so using a user-assigned managed identity is a good alternative.
Establish a trust relationship between Azure and GitHub Actions
Next, we need to create the Federated Identity Credential in Azure to federate the managed identity with GitHub Actions.
# Set the federated credential name
FEDERATED_CRED_NAME=fc-multiarch
# Set the GitHub repository name in the format: pauldotyu/osinfo
GH_REPO=<YOUR_GITHUB_ORG_AND_REPO_NAME>
# Set the GitHub branch name
GH_BRANCH=main
# Create the federated credential
az identity federated-credential create \
--name ${FEDERATED_CRED_NAME} \
--identity-name ${MANAGED_IDENTITY_NAME} \
--resource-group ${RG_NAME} \
--issuer https://token.actions.githubusercontent.com \
--subject repo:${GH_REPO}:ref:refs/heads/${GH_BRANCH}
Here, I set GH_REPO
to pauldotyu/osinfo
so the subject will point to the pauldotyu/osinfo
repository on the main
branch which is what Azure AD will expect when negotiating the incoming token from GitHub. You can set the subject to expect a token from Environments, Pull Requests, or Tags as well.
The issuer
is the URL that GitHub Actions uses to issue the token and where Azure will send the token to be validated.
For more information on how to configure OpenID Connect in Azure, see Set up Azure Login with OpenID Connect authentication.
Prepare the GitHub repository
Last thing we need to do is set some GitHub Secrets for the repository. We’ll need to set the AZURE_CLIENT_ID
, AZURE_TENANT_ID
, and AZURE_SUBSCRIPTION_ID
secrets. We’ll use the GitHub CLI to set the secrets. These aren’t passwords, but you should treat them as sensitive information and not add them to your repository code.
If you do not have the GitHub CLI installed, you can find the installation instructions here.
Make sure you fork and clone the osinfo
repository from my previous article, open a terminal at the root of the repository, then run the following commands:
# Get the client id and set the secret
gh secret set AZURE_CLIENT_ID -b $(az identity show \
--resource-group $RG_NAME \
--name $MANAGED_IDENTITY_NAME \
--query clientId -o tsv)
``
# Get the tenant id and set the secret
gh secret set AZURE_TENANT_ID -b $(az identity show \
--resource-group $RG_NAME \
--name $MANAGED_IDENTITY_NAME \
--query tenantId -o tsv)
# Get the subscription id and set the secret
gh secret set AZURE_SUBSCRIPTION_ID -b $(az account show \
--query id -o tsv)
# Get the ACR login server and set the secret
gh secret set ACR_LOGIN_SERVER -b $(az acr show \
--resource-group $RG_NAME \
--name $ACR_NAME \
--query loginServer -o tsv)
We’re now ready to build and push our container image to ACR using GitHub Actions.
Building the GitHub Action Workflow
Overview Workflow
When building out automation workflows, I like to jot down the high-level steps of what the workflow will do. This helps me visualize the workflow and identify any potential issues.
Here’s what we want to accomplish:
- Log into Azure
- Log into Azure Container Registry
- Build the container image using Docker Buildx
- Push the container image to Azure Container Registry
There are some GitHub Actions available in the GitHub Marketplace that can help us accomplish these tasks. Here’s what I will use:
The workflow File
GitHub Action workflows are defined in a YAML file and must be stored in the .github/workflows
directory in the root of the repository.
In your terminal, run the following commands to create a new file in the .github/workflows
directory called container-image.yml
:
mkdir -p .github/workflows
touch .github/workflows/container-image.yml
Open the workflow file using VSCode so that we can start adding code to it. First thing we need to do is give it a name. We’ll call it container-image
, same as the file name.
Add the following to the top of the file:
name: container-image
You need to specify when the workflow should run. We’ll use the push
event to trigger the workflow when a commit is pushed to the main
branch. Add the following to the workflow file:
on:
push:
branches:
- 'main'
Here’s the critical part when using federated identity credentials. We need to add permissions to the workflow to allow it to write id-token
and read contents
. This is required for the workflow to be able to authenticate to Azure. Add the following to the workflow file:
permissions:
id-token: write
contents: read
Now we setup a single job called build-and-push
that runs on ubuntu-latest
which is the type of machine the workflow will run on. This job will contain all the steps to build and push the container image to ACR. We’ll use the steps
keyword to define the steps. Add the following to the workflow file:
jobs:
build-and-push:
runs-on: ubuntu-latest
Directly underneath runs-on
, we add steps for the workflow to run. These steps will be run serially. First step is to checkout the repository code. We’ll use the actions/checkout
GitHub Action to checkout the repository code. Add the following to the workflow file:
steps:
- name: Checkout code
uses: actions/checkout@v2
Azure Login
Next step is to login to Azure using the federated identity credential we created earlier. We will use the azure/login
GitHub Action to login, but notice we’re referencing the client-id
, tenant-id
, and subscription-id
using GitHub secrets we created earlier for the repository. Add the following to the workflow file:
- name: Login to Azure
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ACR Login
Using our authenticated session, we can login to ACR. We’ll use a simple az acr login
command here. Notice here, we’re referencing the ACR_LOGIN_SERVER
secret we created earlier for the repository. Add the following to the workflow file:
- name: Login to Azure Container Registry
run: az acr login --name ${{ secrets.ACR_LOGIN_SERVER }}
Setup Docker Buildx
Now we need to setup Docker Buildx. Remember from my previous article, we need to use Docker Buildx to build multi-architecture container images. We’ll use the docker/setup-buildx-action
GitHub Action to set it up. Add the following to the workflow file:
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
Docker Build and Push to ACR
Finally, we’ll use the docker/build-push-action
GitHub Action to build and push the container image to ACR.
- name: Build image and push to registry
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ secrets.ACR_LOGIN_SERVER }}/osinfo:${{ github.sha }}
Here, we’re building the container image using the Dockerfile
in the root of the repository. The container image will be built for the linux/amd64
and linux/arm64
platforms and tagged using the github.sha
which is the commit SHA of the commit that triggered the workflow.
The Final Workflow File
To see what your full workflow file should look like by now, you can reference the container-image.yml in my repo.
With the workflow file complete, we can commit the changes to the repository and push it to GitHub.
Finally, head over to the Actions tab in your repository, you should see the workflow running 🚀
Conclusion
In this article, we explored how to push multi-architecture container images to Azure Container Registry using GitHub Actions. We used workload identity federation to authenticate to Azure and used GitHub Actions to automate the building and pushing of our multi-architecture container images to ACR.
This was a very manual process to setup, but once it’s setup, it’s a set it and forget it type of thing. You can now push multi-architecture container images to ACR each time you make a change to your code!
When learning something new, I like to do it manually first so I can understand how it works. Then I’ll lean on tools to automate the process. If you haven’t checked out Draft which is an open-source tool that helps you automate the scaffolding of cloud-native applications for deploying to Kubernetes, you should check it out! This tool also includes the ability to create GitHub Actions workflows using… you guessed it, workload identity federation! 😎
For a more in-depth look at how to use Draft, check out my teammate @StevenMurawski’s blog post.
Next up, we’ll add to our GitHub Action workflow to include steps to secure our container images using open-source tools as outlined by my other teammate, @joshduffney. You should checkout his blog post too.
If you have any feedback or suggestions, please feel free to reach out to me on Twitter or LinkedIn.
Peace ✌️