DEV Community

Cover image for Building and Deploying Container Images to Azure Container Apps with GitHub Actions
Will Velida
Will Velida

Posted on

Building and Deploying Container Images to Azure Container Apps with GitHub Actions

In a previous blog post, I talked about how we can provision an Azure Container App using Bicep and deploying our Bicep template using GitHub Actions.

We'll now turn our attention to updating the images that our Container App uses by building the new image, deploying it to Azure Container registry and then pulling the newly built image from our registry to our Container App.

As part of my infrastructure deployment, I defined a container image as part of my Bicep like so:

    template: {
      containers: [
        {
          name: todoApiContainerName
          image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
          resources: {
            cpu: '0.5'
            memory: '1Gi'
          }         
        }
      ]
      scale: {
        minReplicas: 1
        maxReplicas: 1
      }
    }
Enter fullscreen mode Exit fullscreen mode

So when my Container App was deployed, I had a public endpoint that showed the following page:

image

As we update our application, we'll need to update the image and deploy it to our Container App without performing a full infrastructure deployment. This is what we'll be focusing on in this article.

Before we start, please bear in mind that Azure Container Apps are currently in preview!. That means that this post was correct as of the time of writing! As the service reaches GA (please don't ask me when, I don't know), the methods you read here may have changed drastically. (Note to self, when that happens update this article!)

With that in mind, this is what we'll cover:

  • Ensuring that our GitHub action has sufficient permissions to run the workflow
  • Building our container image and pushing it to Azure Container Registry
  • Deploying our container image to Azure Container Apps.

If you haven't got a Azure Container Apps environment setup, please check out my article on Creating and Provisioning Azure Container Apps with Bicep.

Ensuring that our GitHub action has sufficient permissions to run the workflow

When we use GitHub Actions to deploy resources to Azure, we need to create a service principal that is authorized to do so. We can create a service principal for our GitHub Actions workflow by running the following AZ CLI command:

az ad sp create-for-rbac \
  --name <SERVICE_PRINCIPAL_NAME> \
  --role "contributor" \
  --scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME> \
  --sdk-auth
Enter fullscreen mode Exit fullscreen mode

Replace the following parameters with your own values:

Parameter Description
SERVICE_PRINCIPAL_NAME The name of your service principal. Can be anything your want
SUBSCRIPTION_ID Your Azure Subscription Id
RESOURCE_GROUP_NAME The name of your resource group that you'll be deploying to

This command assigns the contributor permission to your GitHub Action workflow over the resource group. This command should generate a JSON output that you'll need to copy. It looks similar to this:

{
    "clientId": "<GUID>",
    "clientSecret": "<GUID>",
    "subscriptionId": "<GUID>",
    "tenantId": "<GUID>",
}
Enter fullscreen mode Exit fullscreen mode

We need to save this JSON output as a Secret in our GitHub repo. You can do this by selecting Settings then Secrets and clicking on New to create the secret. We'll need to create the following secrets:

Secret Value
RESOURCE_GROUP Name of the resource group that we're deploying resources to
AZURE_CREDENTIALS The entire JSON output that was generated as part of the service principal creation step
REGISTRY_LOGIN_SERVER The login server name of your registry (all lowercase). Example: myregistry.azurecr.io
REGISTRY_USERNAME The clientId from the JSON output from the service principal creation
REGISTRY_PASSWORD The clientSecret from the JSON output from the service principal creation

We also need to grant our GitHub Actions service principal with the permissions to both pull and push images to our Azure Container Registry. We can grant this by running the following AZ CLI commands:

# Get the id of our Azure Container Registry
registryId=$(az acr show --name <registry-name> --query id --output tsv)

# Grant the 'AcrPush' role to our service principal
az role assignment create --assignee <ClientId> --scope $registryId --role AcrPush

# Grant the 'AcrPull' role to our service principal
az role assignment create --assignee <ClientId> --scope $registryId --role AcrPull
Enter fullscreen mode Exit fullscreen mode

Use the clientId of the Service Principal that was generated when you created it for the --assignee parameter.

Building our container image and pushing it to Azure Container Registry

With our secrets created, we can start working on building and pushing our container image to our Azure Container Registry. For this sample, I'm using the following Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app 
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["BookStore.Common/BookStore.Common.csproj", "BookStore.Api/"]
COPY ["BookStore.Repository/BookStore.Repository.csproj", "BookStore.Api/"]
COPY ["BookStore.Services/BookStore.Services.csproj", "BookStore.Api/"]
COPY ["BookStore.Api/BookStore.Api.csproj", "BookStore.Api/"]
RUN dotnet restore "BookStore.Api/BookStore.Api.csproj"
COPY . .
WORKDIR "/src/BookStore.Api"
RUN dotnet build "BookStore.Api.csproj" -c Release -o /app

FROM build AS publish
RUN dotnet publish "BookStore.Api.csproj" -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT [ "dotnet", "BookStore.Api.dll" ]
Enter fullscreen mode Exit fullscreen mode

In my GitHub Actions workflow, I want to run a job that will build and push this container image into my Azure Container Registry. We can do this using the following YAML:

name: Build and Deploy Container Image to Azure Container Apps

on:
  push:
    paths:
      - 'src/*'
  workflow_dispatch:

defaults:
  run:
    working-directory: ./src/BookStore

jobs:
  build-container-image:
    runs-on: ubuntu-latest
    steps:
    - name: 'Checkout GitHub Action'
      uses: actions/checkout@main

    - name: 'Login via Azure CLI'
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: 'Build and Push Image to ACR'
      uses: azure/docker-login@v1
      with:
        login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
        username: ${{ secrets.REGISTRY_USERNAME }}
        password: ${{ secrets.REGISTRY_PASSWORD }}
    - run: |
        docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bookstoreapi:${{ github.sha }}
        docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bookstoreapi:${{ github.sha }}
Enter fullscreen mode Exit fullscreen mode

Let's break down what this build-container-image job is doing:

  • We first log into Azure using the Service Principal credentials we generated for our GitHub Actions workflow. This will allow our workflow to deploy resources and make changes to our Azure environment.
  • We then log into our Azure Container Registry using our Service Principal credentials. Our Service Principal has permissions to pull and push images from our Container Registry, which we need for our deployment.
  • We then run the docker build and docker push commands in the run step. We are in the working directory for our application, so we don't need to do any funky formatting to point to our Dockerfile. For my image versioning, I'm just using the git commit hash, but you can apply whatever versioning you think you need for your app. Once our Dockerfile has successfully built, we push it to our Azure Container Registry.

Deploying our container image to Azure Container Apps.

Now that our image has been pushed to our Azure Container Registry, we can create a job in our GitHub Action workflow file to deploy our image to our Container App:

deploy-container-app:
    runs-on: ubuntu-latest
    needs: build-container-image
    steps:
    - name: 'Checkout GitHub Action'
      uses: actions/checkout@main

    - name: 'Login via Azure CLI'
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: 'Deploy Container App'
      uses: Azure/cli@v1
      with:
        inlineScript: |
          echo "Installing containerapp extension"
          az extension add --source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.0-py2.py3-none-any.whl --yes
          echo "Starting Deploying"
          az containerapp update -n velidabookstoreapi -g todocontainerapp-rg -i ${{ secrets.REGISTRY_LOGIN_SERVER }}/bookstoreapi:${{ github.sha }} --registry-login-server ${{ secrets.REGISTRY_LOGIN_SERVER }} --registry-username  ${{ secrets.REGISTRY_USERNAME }} --registry-password ${{ secrets.REGISTRY_PASSWORD }} --debug
Enter fullscreen mode Exit fullscreen mode

Again, let's break this down:

  • We log into Azure using the Service Principal credentials we generated for our GitHub Actions workflow.
  • We then run inline AZ CLI commands to deploy our image:
    • The az containerapp command is still in preview and are not part of the main AZ CLI command set yet, so we need to install an extension before we can start using those commands. We do so by running az extension add. We add the --yes parameter to install the extension without asking for confirmation. Check the docs for more details on az extension add.
    • Once that's been completed, we then run the az containerapp update command to deploy our new image. We pass in the credentials needed to authenticate to our Azure Container Registry so that we pull our image into our Container App.

Now that everything's been set up, we can run our workflow file to deploy our new image:

image

Navigating to my Container App, I can see that my new image has been deployed. So instead of the hello world example provided by the good folks on the Container Apps team, my Container App is now using my image instead:

image

Alternative approach

As part of the preview, we can work with Azure Container Apps using the Azure CLI. One of the commands at our disposal allows us to generate a GitHub Actions workflow using the following command:

az containerapp github-action add \
  --repo-url "https://github.com/<OWNER>/<REPOSITORY_NAME>" \
  --docker-file-path "./dockerfile" \
  --branch <BRANCH_NAME> \
  --name <CONTAINER_APP_NAME> \
  --resource-group <RESOURCE_GROUP> \
  --registry-url <URL_TO_CONTAINER_REGISTRY> \
  --registry-username <REGISTRY_USER_NAME> \
  --registry-password <REGISTRY_PASSWORD> \
  --service-principal-client-id <CLIENT_ID> \
  --service-principal-client-secret <CLIENT_SECRET> \
  --service-principal-tenant-id <TENANT_ID> \
  --token <YOUR_GITHUB_PERSONAL_ACCESS_TOKEN>
Enter fullscreen mode Exit fullscreen mode

With this command, this generates a GitHub Action workflow file for us that we can use as a basis to deploy our container images to Azure Container Apps as we update them!

Taking this approach means we have to generate our own GitHub personal access token (PAT). If you don't know how to do that, check out this guide.

What else can we do with az containerapp commands?

With the az containerapp extension, we have a couple of options available to us to manage our Azure Container Apps! For example, we can use the az containerapp update command to split traffic between our Container Apps revisions like so:

az containerapp update --name $APP_NAME --resource-group $RESOURCE_GROUP --traffic-weight $CURRENT_REVISION=80,latest=20
Enter fullscreen mode Exit fullscreen mode

We can also activate revisions using the following command:

az containerapp revision activate \
  --name <REVISION_NAME> \
  --app <CONTAINER_APP_NAME> \
  --resource-group <RESOURCE_GROUP_NAME>
Enter fullscreen mode Exit fullscreen mode

Check out this documentation to see what other commands you can use with the az containerapp extension

Conclusion

Even though Azure Container Apps is still in preview, we can use GitHub Actions to build and deploy our container images as we update them. As you would expect from a preview service, there are limits to what is supported (no support for managed identities yet, so we have to use a lot of admin access here). But as this service heads towards GA, I'm sure we'll more support for this (Again, please don't ask me when Container Apps are going GA, I have no idea 🤷)

If you want a reference to the code that we've written in this post, you can do so in this GitHub repository.

In a future blog post, I'll talk about the different types of changes we can make in Azure Container Apps and how that affects our Container Apps environments.

If you have any questions, feel free to reach out to me on twitter @willvelida

Until next time, Happy coding! 🤓🖥️

Top comments (0)