---
title: Build custom virtual machine images with GitHub Actions and Azure
description: Learn how to build custom virtual machine images with GitHub Actions and Azure
author: juliakm
ms.author: jukullam
ms.topic: quickstart
ms.service: azure
ms.date: 03/23/2021
ms.custom: github-actions-azure, devx-track-azurecli, mode-portal
---
# Build custom virtual machine images with GitHub Actions and Azure
Get started with the [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions) by creating a workflow to build a virtual machine image.
With GitHub Actions, you can speed up your CI/CD process by creating custom virtual machine images with artifacts from your workflows. You can both build images and distribute them to a [Shared Image Gallery](/azure/virtual-machines/shared-image-galleries).
You can then use these images to create [virtual machines](https://azure.microsoft.com/services/virtual-machines/) and [virtual machine scale sets](/azure/virtual-machine-scale-sets/overview).
The build virtual machine image action uses the [Azure Image Builder service](/azure/virtual-machines/image-builder-overview).
## Prerequisites
- An Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/?WT.mc_id=A261C142F).
- A GitHub account with an active repository. If you don't have one, sign up for [free](https://github.com/join).
- This example uses the [Java Spring PetClinic Sample Application](https://github.com/spring-projects/spring-petclinic).
- A Shared Image Gallery.
- [Create a Shared Image Gallery with the Azure CLI](/azure/virtual-machines/share-gallery?tabs=cli)
- Create an Azure Shared Image Gallery using the portal (Windows, [Linux](/azure/virtual-machines/linux/shared-images-portal))
## Workflow file overview
A workflow is defined by a YAML (.yml) file in the `/.github/workflows/` path in your repository. This definition contains the various steps and parameters that make up the workflow.
The file has three sections:
|Section |Tasks |
|---------|---------|
|**Authentication** | 1. Add a user-managed identity.
2. Define a service principal or publish profile.
3. Create a GitHub secret. |
|**Build** | 1. Set up the environment.
2. Build the app. |
|**Image** | 1. Create a VM Image.
2. Create a virtual machine. |
## Create a user-managed identity
You'll need a user-managed identity for Azure Image Builder(AIB) to distribute images. Your Azure user-assigned managed identity will be used during the image build to read and write images to a Shared Image Gallery.
1. Create a user-managed identity with [Azure CLI](/azure/active-directory/managed-identities-azure-resources/how-to-manage-ua-identity-cli) or the [Azure portal](/azure/active-directory/managed-identities-azure-resources/how-to-manage-ua-identity-portal). Write down the name of your managed identity.
1. Customize this JSON code. Replace the placeholders for `{subscriptionID}` and `{rgName}`with your subscription ID and resource group name.
```yaml
{
"properties": {
"roleName": "Image Creation Role",
"IsCustom": true,
"description": "Azure Image Builder access to create resources for the image build",
"assignableScopes": [
"/subscriptions/{subscriptionID}/resourceGroups/{rgName}"
],
"permissions": [
{
"actions": [
"Microsoft.Compute/galleries/read",
"Microsoft.Compute/galleries/images/read",
"Microsoft.Compute/galleries/images/versions/read",
"Microsoft.Compute/galleries/images/versions/write",
"Microsoft.Compute/images/write",
"Microsoft.Compute/images/read",
"Microsoft.Compute/images/delete"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
} } ```
1. Use this JSON code to create a [new custom role](/azure/role-based-access-control/custom-roles-portal#start-from-scratch#start-from-json) with JSON.
## Create a service principal and add it to GitHub secret
To use [Azure login](https://github.com/marketplace/actions/azure-login), you first need to add your Azure service principal as a secret to your GitHub repository.
In this example, you'll create a secret named `AZURE_CREDENTIALS` that you can use to authenticate with Azure.
1. If you do not have an existing application, register a [new Active Directory application](/azure/active-directory/develop/howto-create-service-principal-portal#register-an-application-with-azure-ad-and-create-a-service-principal&preserve-view=true) to use with your service principal.
```azurecli-interactive
appName="myApp"
az ad app create \
--display-name $appName \
--homepage "http://localhost/$appName" \
--identifier-uris http://localhost/$appName
```
1. [Create a new service principal](/cli/azure/create-an-azure-service-principal-azure-cli) in the Azure portal for your app.
```azurecli-interactive
az ad sp create-for-rbac --name "myApp" --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth
```
1. Copy the JSON object for your service principal.
```json
{
"clientId": "",
"clientSecret": "",
"subscriptionId": "",
"tenantId": "",
(...)
}
```
1. Open your GitHub repository and go to **Settings**.
:::image type="content" source="media/github-repo-settings.png" alt-text="Select Settings in the navigation.":::
1. Select **Secrets** and then **New Secret**.
:::image type="content" source="media/select-secrets.png" alt-text="Choose to add a secret.":::
1. Paste in your JSON object for your service principal with the name `AZURE_CREDENTIALS`.
:::image type="content" source="media/azure-secret-add.png" alt-text="Add a secret in GitHub.":::
1. Save by selecting **Add secret**.
## Use the Azure login action
Use your service principal secret with the [Azure Login action](https://github.com/Azure/login) to authenticate to Azure.
In this workflow, you authenticate using the Azure login action with the service principal details stored in `secrets.AZURE_CREDENTIALS`. Then, you run an Azure CLI action. For more information about referencing GitHub secrets in a workflow file, see [Using encrypted secrets in a workflow](https://docs.github.com/en/actions/reference/encrypted-secrets#using-encrypted-secrets-in-a-workflow) in GitHub Docs.
```yaml
on: [push]
name: Create Custom VM Image
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Log in with Azure
uses: azure/login@v1
with:
creds: '${{ secrets.AZURE_CREDENTIALS }}'
```
## Configure Java
Set up the Java environment with the [Java Setup SDK action](https://github.com/marketplace/actions/setup-java-jdk). For this example, you'll set up the environment, build with Maven, and then output an artifact.
[GitHub artifacts](https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts) are a way to share files in a workflow between jobs. You'll create an artifact to hold the JAR file and then add it to the virtual machine image.
```yaml
on: [push]
name: Create Custom VM Image
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login via Az module
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: Setup Java 1.8.x
uses: actions/setup-java@v1
with:
java-version: '1.8.x'
- name: Build Java
run: mvn --batch-mode --update-snapshots verify
- run: mkdir staging && cp target/*.jar staging
- uses: actions/upload-artifact@v2
with:
name: Package
path: staging
```
## Build your image
Use the [Build Azure Virtual Machine Image action](https://github.com/marketplace/actions/build-azure-virtual-machine-image) to create a custom virtual machine image.
Replace the placeholders for `{subscriptionID}`, `{rgName}`and `{Identity}` with your subscription ID, resource group name, and managed identity name. Replace the values of `{galleryName}` and `{imageName}` with your image gallery name and your image name.
```yaml
- name: Create App Baked Image
id: imageBuilder
uses: azure/build-vm-image@v0
with:
location: 'eastus2'
resource-group-name: '{rgName}'
managed-identity: '{Identity}' # Managed identity
source-os-type: 'windows'
source-image-type: 'platformImage'
source-image: MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest #unique identifier of source image
dist-type: 'SharedImageGallery'
dist-resource-id: '/subscriptions/{subscriptionID}/resourceGroups/{rgName}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}/versions/0.1.${{ GITHUB.RUN_ID }}' #Replace with the resource id of your shared image gallery's image definition
dist-location: 'eastus2'
```
### Virtual Machine action arguments
| Input | Required | Description |
|---|---|---|
| `resource-group-name` | Yes | The resource group used for storage and saving artifacts during the build process. |
| `image-builder-template-name` | No | The name of the image builder template resource used. |
| `location` | Yes | The location where Azure Image Builder will run. See [supported locations](/azure/virtual-machines/image-builder-overview#regions). |
| `build-timeout-in-minutes` | No | Time after which the build is canceled. Defaults to 240. |
| `vm-size` | Optional | By default, `Standard_D1_v2` will be used. See [virtual machine sizes](/azure/virtual-machines/sizes).|
| `managed-identity` | Yes | The user-managed identity you created earlier. Use the full identifier if your identity is in a different resources group. Use the name if it is in the same resource group. |
| `source-os` | Yes | The OS type of the base image (Linux or Windows) |
| `source-image-type` | Yes | The base image type that will be used for creating the custom image. |
| `source-image` | Yes | The resource identifier for base image. A source image should be present in the same Azure region set in the input value of location. |
| `customizer-source` | No | The directory where you can keep all the artifacts that need to be added to the base image for customization. By default, the value is `${{ GITHUB.WORKSPACE }}/workflow-artifacts.` |
| `customizer-destination` | No | This is the directory in the customized image where artifacts are copied to. |
| `customizer-windows-update` | No | For Windows only. Boolean value. If `true`, the image builder will run Windows update at the end of the customizations.|
| `dist-location` | No | For SharedImageGallery, this is the `dist-type`.|
| `dist-image-tags` | No | These are user-defined tags that are added to the custom image created (example: `version:beta`). |
## Create your virtual machine
As a last step, create a virtual machine from your image.
1. Replace the placeholders for `{rgName}`with your resource group name.
1. Add a GitHub secret with the virtual machine password (`VM_PWD`). Be sure to write down the password because you will not be able to see it again. The username is `myuser`.
```yaml
- name: CREATE VM
uses: azure/CLI@v1
with:
azcliversion: 2.0.72
inlineScript: |
az vm create --resource-group ghactions-vMimage --name "app-vm-${{ GITHUB.RUN_NUMBER }}" --admin-username myuser --admin-password "${{ secrets.VM_PWD }}" --location eastus2 \
--image "${{ steps.imageBuilder.outputs.custom-image-uri }}"
```
### Complete YAML
```yaml
on: [push]
name: Create Custom VM Image
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Login via Az module
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
- name: Setup Java 1.8.x
uses: actions/setup-java@v1
with:
java-version: '1.8.x'
- name: Build Java
run: mvn --batch-mode --update-snapshots verify
- run: mkdir staging && cp target/*.jar staging
- uses: actions/upload-artifact@v2
with:
name: Package
path: staging
- name: Create App Baked Image
id: imageBuilder
uses: azure/build-vm-image@v0
with:
location: 'eastus2'
resource-group-name: '{rgName}'
managed-identity: '{Identity}' # Managed identity
source-os-type: 'windows'
source-image-type: 'platformImage'
source-image: MicrosoftWindowsServer:WindowsServer:2019-Datacenter:latest #unique identifier of source image
dist-type: 'SharedImageGallery'
dist-resource-id: '/subscriptions/{subscriptionID}/resourceGroups/{rgName}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}/versions/0.1.${{ GITHUB.RUN_ID }}' #Replace with the resource id of your shared image gallery's image definition
dist-location: 'eastus2'
- name: CREATE VM
uses: azure/CLI@v1
with:
azcliversion: 2.0.72
inlineScript: |
az vm create --resource-group ghactions-vMimage --name "app-vm-${{ GITHUB.RUN_NUMBER }}" --admin-username myuser --admin-password "${{ secrets.VM_PWD }}" --location eastus2 \
--image "${{ steps.imageBuilder.outputs.custom-image-uri }}"
```
## Next steps
- Learn how to [deploy to Azure](deploy-to-azure.md).