1. Use Github Actions to Deploy a Containerized WebApp
Introduction
This tutorial shows how to setup Github Actions in order to deploy a containerized ASP.NET Core Webapp to two different Azure compute services - Azure VM and Azure Container Apps. The VM runs Ubuntu and will use what is called a self-hosted runner for the deployment. Azure Container Apps is a serverless compute service where you can host your containerized apps. The latter requires Azure login credentials in the CI/CD pipeline.
Prerequisites
In order to follow along, ensure you have the following tools installed on your machine:
- Docker (Docker Desktop) installed on your local machine. Download Docker.
- Visual Studio Code (VSCode) as your IDE. Download VSCode. docs.microsoft.com/en-us/dotnet/core/tools/).
- .NET SDK 8.0 installed to build .NET applications. Install .NET.
- Azure CLI
Method
There are many ways to implement the CI/CD pipeline. This tutorial shows some alternative approaches ranging from a very simplistic basic setup to a more feature rich setup including for instance cross-plattform builds.
We will utilize the Docker Registry provided by Github, called Github Packages.
Here are some terminology used in this solution:
- Docker, Buildx, Registry
- Github Actions, Github Packages, self-hosted runner, github hosted runner, gh secrets, sha
- Azure VM, Azure Container Apps, azure login rbac
Here is an overview of the solution
graph LR App[App] --> GIT[Github Repo] GIT --> CICD[CI/CD Pipeline - Github Actions] CICD --> VM[Azure Ubuntu VM] CICD --> CAPP[Azure Container App]
Before we begin
Before we begin implementing the CI/CD pipeline in Github Actions, we need to create what we have in both ends of the pipeline.
In the appliction end we will create an ASP.NET Core Webapp. It will be a a very simple app, where we just use the built in scaffolding commands of dotnet. This app will be attached to a Github repo.
In the hosting end we will setup two different targets. 1) An Azure VM running Ubuntu and 2) An Azure Container Apps solution. In both cases we will use the Azure CLI to provision the services.
Develop the app
Create an empty directory called
MyDemoApp
and use the scaffolding command in dotnet to create the example webapp.dotnet new webapp dotnet new gitignore
We need to create a
Dockerfile
in order to containerize the app. We can do that using Docker CLI. Follow the instructions in the terminal. It will autodetect your project, so normally you only need to confirm the alternatives by pressing<Enter>
.docker init
Verify that you can run your application in a container locally.
docker build -t mydemoapp . docker run -p 8080:8080 mydemoapp docker push
Change the repo to your own Github user name
Make sure you are logged in to github in the terminal. If not, run
gh login
Browse to
localhost:8080
and verify that it works.Create a git repo push it to Github.
git init git add . git commit -m "Initial Commit"
Use the git function in the left menu bar in VSCode to create and push your repo to Github.
Provision the hosts
Ubuntu VM on Azure
Follow this article to setup an Ubuntu VM on Azure with Azure CLI.
Azure Container Apps
To provision the Azure Container App, follow these steps:
Prepare Script Files: Create a file:
provision_vm.sh
for provisioning the App.provision_vm.sh
#!/bin/bash resource_group=DockerDemoRG location=northeurope env_name=DockerDemoEnv app_name=mydemoapp app_port=8080 image=ghcr.io/<repo>/mydemoapp az group create --location $location --name $resource_group az containerapp env create --name $env_name --resource-group $resource_group --location $location az containerapp create --name $app_name --resource-group $resource_group \ --image $image \ --environment $env_name \ --target-port $app_port \ --ingress external --query properties.configuration.ingress.fqdn
Change the repo to your own Github user name
Verify that it works by browsing to the URL.
Azure credentials: Later when we setup the pipeline we need some Azure credentials in order to communicate from the pipeline to the Azure Container Apps service.
Get the azure rbac credentials for github actions. Azure Login
az login az account list --output table az ad sp create-for-rbac --name "github-actions-deployer" --role contributor --scopes /subscriptions/<subscription> --sdk-auth
Paste the json output into gh secrets (secrets.AZURE_CREDENTIALS)
{ "clientId": "27a6...", "clientSecret": "Rh...", "subscriptionId": "ca0a77...", "tenantId": "6e...", "activeDirectoryEndpointUrl": "https://login.microsoftonline.com", "resourceManagerEndpointUrl": "https://management.azure.com/", "activeDirectoryGraphResourceId": "https://graph.windows.net/", "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/", "galleryEndpointUrl": "https://gallery.azure.com/", "managementEndpointUrl": "https://management.core.windows.net/" }
Create the CI/CD Pipeline
Alt 1: Basic Setup
- Use docker commands directly from shell
- Use
needs
to indicate dependency between jobs (build -> deploy) - Use github secrets (GITHUB_TOKEN and self-declared)
- Self-hosted and github-hosted runners
- Use permissions for the GITHUB_TOKEN secret
name: CI/CD
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build_and_push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push Docker image
run: |
docker build . -t ghcr.io/larsappel/dockerdemoapp:latest
docker push ghcr.io/larsappel/dockerdemoapp:latest
deploy_to_vm:
needs: build_and_push
runs-on: self-hosted
steps:
- name: Stop and remove existing container
run: sudo docker stop dockerdemoapp || true && sudo docker rm dockerdemoapp || true
- name: Start the new container
run: sudo docker run -d -p 80:8080 --name dockerdemoapp ghcr.io/larsappel/dockerdemoapp:latest
deploy_to_azure_container_apps:
needs: build_and_push
runs-on: ubuntu-latest
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Container Apps
run: |
az containerapp update \
--name dockerdemoapp \
--resource-group DockerDemoRG \
--image ghcr.io/larsappel/dockerdemoapp:latest
Alt 2: With commit hash, parameters and outputs
- Calculates the commit hash
- Use output between jobs
- Use environment variables
- Use the repository name as image name
name: CI/CD (with hash)
on:
# Trigger the workflow on git push
push:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
# The registry to push the image to. Set to empty for Docker Hub
REGISTRY: ghcr.io
# On the format owner/repo. Might need to be lowercase
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_push:
runs-on: ubuntu-latest
# Outputs the short commit hash and the image name to be used in the next steps
outputs:
short_hash: ${{ steps.commit_hash.outputs.hash }}
image_name: ${{ steps.lowercase_image_name.outputs.name }}
# Permissions for the GITHUB_TOKEN
permissions:
contents: read
packages: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Calculate the short commit hash
id: commit_hash
run: echo "hash=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Make sure the image name is lowercase
id: lowercase_image_name
run: echo "name=${REGISTRY}/${IMAGE_NAME,,}" >> $GITHUB_OUTPUT
- name: Login to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
- name: Build and push Docker image
run: |
docker build . -t ${{ steps.lowercase_image_name.outputs.name }}:${{ steps.commit_hash.outputs.hash }} \
-t ${{ steps.lowercase_image_name.outputs.name }}:latest
docker push --all-tags ${{ steps.lowercase_image_name.outputs.name }}
deploy_to_vm:
needs: build_and_push
runs-on: self-hosted
steps:
- name: Stop and remove existing container
run: sudo docker stop dockerdemoapp || true && sudo docker rm dockerdemoapp || true
- name: Start the new container
run: sudo docker run -d -p 80:8080 --name dockerdemoapp ${{ needs.build_and_push.outputs.image_name }}:${{ needs.build_and_push.outputs.short_hash }}
deploy_to_azure_container_apps:
needs: build_and_push
runs-on: ubuntu-latest
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Container Apps
run: |
az containerapp update \
--name dockerdemoapp \
--resource-group DockerDemoRG \
--image ${{ needs.build_and_push.outputs.image_name }}:${{ needs.build_and_push.outputs.short_hash }}
Alt 3: Actions
- Use actions from Docker to login, tag and build
- Remove actions/checkout. Done in docker/build-push-action (don’t use:
context: .
)
name: CI/CD (with Docker actions)
on:
# Trigger the workflow on git push
push:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
# The registry to push the image to. Set to empty for Docker Hub
REGISTRY: ghcr.io
# On the format owner/repo. Might need to be lowercase
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_push:
runs-on: ubuntu-latest
# Outputs the image name to be used in the next steps
outputs:
image_name: ${{ steps.image_with_sha_tag.outputs.name }}
# Permissions for the GITHUB_TOKEN. Used under the hood in docker/build-push-action
permissions:
contents: read
packages: write
steps:
- name: Login to the Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set image name and tag
uses: docker/metadata-action@v5
id: metadata
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
latest
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
- name: Set the output to the image name with the SHA tag
id: image_with_sha_tag
run: |
NAME=$(echo "${{ steps.metadata.outputs.tags }}" | tr ',' '\n' | grep 'sha')
echo "name=${NAME}" >> $GITHUB_OUTPUT
deploy_to_vm:
needs: build_and_push
runs-on: self-hosted
steps:
- name: Stop and remove existing container
run: sudo docker stop dockerdemoapp || true && sudo docker rm dockerdemoapp || true
- name: Start the new container
run: sudo docker run -d -p 80:8080 --name dockerdemoapp ${{ needs.build_and_push.outputs.image_name }}
deploy_to_azure_container_apps:
needs: build_and_push
runs-on: ubuntu-latest
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Container Apps
run: |
az containerapp update \
--name dockerdemoapp \
--resource-group DockerDemoRG \
--image ${{ needs.build_and_push.outputs.image_name }}
Alt 4: Multi-platform
- Use Qemu and Buildx to build multi plattform images
name: CI/CD (Multiplatform)
on:
# Trigger the workflow on git push
push:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
# The registry to push the image to. Set to empty for Docker Hub
REGISTRY: ghcr.io
# On the format owner/repo. Might need to be lowercase
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_push:
runs-on: ubuntu-latest
# Outputs the image name to be used in the next steps
outputs:
image_name: ${{ steps.image_with_sha_tag.outputs.name }}
# Permissions for the GITHUB_TOKEN. Used under the hood in docker/build-push-action
permissions:
contents: read
packages: write
steps:
- name: Login to the Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set image name and tag
uses: docker/metadata-action@v5
id: metadata
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
latest
type=sha
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Buildx for multi-platform builds
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
platforms: linux/amd64,linux/arm64
- name: Set the output to the image name with the SHA tag
id: image_with_sha_tag
run: |
NAME=$(echo "${{ steps.metadata.outputs.tags }}" | tr ',' '\n' | grep 'sha')
echo "name=${NAME}" >> $GITHUB_OUTPUT
deploy_to_vm:
needs: build_and_push
runs-on: self-hosted
steps:
- name: Stop and remove existing container
run: sudo docker stop dockerdemoapp || true && sudo docker rm dockerdemoapp || true
- name: Start the new container
run: sudo docker run -d -p 80:8080 --name dockerdemoapp ${{ needs.build_and_push.outputs.image_name }}
deploy_to_azure_container_apps:
needs: build_and_push
runs-on: ubuntu-latest
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Container Apps
run: |
az containerapp update \
--name dockerdemoapp \
--resource-group DockerDemoRG \
--image ${{ needs.build_and_push.outputs.image_name }}
Related Topics
Dependabot
- Automatically create pull requests to update actions to new versions with dependabot
.github/dependabot.yaml
# Set update schedule for GitHub Actions
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
Note: Automating Dependabot with GitHub Actions
Workflow gallery
Creating starter workflows for your organization
Save State
Action and workflow authors who are using save-state or set-output via stdout should update to use the new environment files. See article.
Examples
A workflow using save-state or set-output like the following
- name: Save state
run: echo "::save-state name={name}::{value}"
- name: Set output
run: echo "::set-output name={name}::{value}"
should be updated to write to the new GITHUB_STATE
and GITHUB_OUTPUT
environment files:
- name: Save state
run: echo "{name}={value}" >> $GITHUB_STATE
- name: Set output
run: echo "{name}={value}" >> $GITHUB_OUTPUT
External Links
Links
https://learn.microsoft.com/en-us/shows/containers-with-dotnet-and-docker-for-beginners/
https://docs.docker.com/engine/install/ubuntu/
https://docs.docker.com/language/dotnet/containerize/