Easily Create Azure VM Custom Linux Images for ASP.NET Core Services with GitHub Actions and Packer

Itay Podhajcer
Microsoft Azure
Published in
4 min readJan 2, 2022

Creating Docker container images with build pipeline, whether it’s with GitHub Actions or any other such tool, is common and straight forward. But what if you can’t or don’t want to use Docker container images, and instead, you want to rely on VM images? Well, no problem, there’s a solution for you as well. In this article we’ll be covering how we can build a GitHub Actions workflow that uses HashiCorp’s Packer to create a custom image for an ASP.NET Core Web API.

Prerequisites

We will be creating a simple ASP.NET Core Web API as the service that needs to be included in the image, so the .NET SDK should be installed on the workstation. For the GitHub Actions workflow, we will need to create an Azure Active Directory service principal so Packer can interact with Azure to build the VM image, so the latest version of the Azure CLI should be installed as well.

Example Repository

A complete example with a simple ASP.NET Core service, Packer script and GitHub Actions workflow, is available in the following GitHub repository:

The Service

For the service, we will just use the basic ASP.NET Core that is created by the .NET CLI tool by running dotnet new webapi -n WebApi -o ./ inside our src directory.

The Packer Script

We will start by creating a Linux systemd template file called service.tpl:

The description and executable file name will be passed in through the Packer script and the service will run under a non-rooted user.

Next we will create the actual packer script, called main.pkr.hcl, which will first define the source image to use:

And then the build tasks, which include:

  • Uploading the service file after values have been applied to the template
  • Create the service’s directory
  • Change the ownership to our non-rooted user
  • Upload the service’s compiled binaries
  • Grand execution permissions to the service’s files
  • Now, if you need to run your service with a port below 1024, you will need to add this command at this point:
sudo setcap CAP_NET_BIND_SERVICE=+eip ${local.destination_directory}/${var.executable_name}
  • Reload the daemon
  • Enable the service
  • Start the service
  • Lastly, deprovision the VM

This entire build section should look like this:

We will also create a Packer variables file called variables.pkr.hcl for all the variables our script requires:

The Service Principal

Creating the service principal is fairly easy using the Azure CLI. The following command will create the principal associated to the Contributor role, as Packer will be deploying temporary resources to create the image and will also store the image in an existing resource group:

az ad sp create-for-rbac --name github-actions-packer --role Contributor --query "{ client_id: appId, client_secret: password, tenant_id: tenant }"

You will also need your Azure subscription ID, which can be retrieved with:

az account show --query "{ subscription_id: id }"

By the way, if you are using multiple subscriptions, and need to change the one that is being used by the CLI, you can run:

az account set --subscription [SUBSCRIPTION-ID]

We will need to keep the output from the above commands, as it will be required when we define our GitHub Actions workflow.

The Workflow

The workflow will be split into two jobs:

  1. Build the service.
  2. Build the custom Azure VM image.

For brevity, I will only go into details on the image building job, but a complete workflow file can be found as part of the above linked GitHub repository.

We will actually start not in the code, but instead, by creating secrets that will be used by the workflow. To create secrets under the repository (it can also be done at the organization level) go to:

Repository Settings => Secrets => New repository secret

The secrets we will create are:

  • CLIENT_ID — output from the Azure CLI commands above.
  • CLIENT_SECRET — output from the Azure CLI commands above.
  • RESOURCE_GROUP_NAME — the name of a pre-existing resource group.
  • SUBSCRIPTION_ID — output from the Azure CLI commands above.
  • TENANT_ID — output from the Azure CLI commands above.

Now we can move to the workflow file (needs to be placed under ./.github/workflows for GitHub to detect it), which will have a build-image job which relies on the completion of the source building job. the job will:

  • Checkout the repository, so the Packer script is available to the workflow
  • Download the artifacts of the previous source building job
  • Install the latest Packer version
  • Run the Packer script by executing the packer build command and passing variables to it using -var

The workflow should look like the following:

Testing

Testing the generated image only requires us to create a new Azure VM using that image and either opening the port the service exposes or connecting to the VM using ssh. If we connect to the VM using ssh, running systemctl status WebApi.service should show our service’s logs.

Conclusion

If you are planning a multi-cloud solution, the above solution can be adjusted, without too much effort, to create the images on multiple clouds at once. If you are only planning to run on Azure, you cloud also check the Azure Image Builder tool, developed by Microsoft and based on HashiCorp’s Packer (so you can even use the same script).

--

--

Itay Podhajcer
Microsoft Azure

Tech expert with 20+ years’ experience as CTO, Chief Architect, and Consultant. 3x Microsoft MVP award winner. Passionate blogger and open-source contributor