DEV Community

Cover image for Pushing Multi-Architecture Container Images
Paul Yu for Microsoft Azure

Posted on • Originally published at paulyu.dev on

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Log into Azure
  2. Log into Azure Container Registry
  3. Build the container image using Docker Buildx
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

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 โœŒ๏ธ

References

Top comments (0)