Turning Azure virtual machines on and off from a GitHub action

3 minute read

For building Mixed Reality applications I use an Azure virtual machine, because reasons. The great thing about virtual machines is they can be deployed pretty easily without having to order real hardware - the problem with virtual machines is they can rack up quite an Azure bill when left unattended, and although my friends at Microsoft won’t mind, my credit card certainly will.

Recentely I attended a session by Barbara Forbes, on FurtureTech 2023, where she showed you could use a credential that allowed a GitHub action to login into an Azure tenant. It was actually a very tiny part of the talk, but it stuck in my mind: since I already knew you could startup and shutdown a virtual machine using the Azure Cloud Shell, I wondered: would it be possible to do that from a GitHub action as well? And yes indeed, you can.

Getting the login credential

When you select your virtual machine in the Azure portal, you will see a whopping long URL in your browser’s address bar. Something like this:

portal.azure.com/#@somenamehere.onmicrosoft.com/resource/subscriptions/
subscriptionid/resourceGroups/VMresourcegroup/providers/Microsoft.Compute/
virtualMachines/VMname/overview

You will need this part right away:

subscriptionid/resourceGroups/VMresourcegroup

and the VMName later

Open a cloud shell with this thingy top right in the browser:

And enter the following command:

az ad sp create-for-rbac --name "somenamehere" --role contributor \
    --scopes /subscriptions/subscriptionid/resourceGroups/VMresourcegroup \
    --sdk-auth

This will respond with something very much like this:

{
  "clientId": "{someguid}",
  "clientSecret": "{someguid}",
  "subscriptionId": "{someguid}",
  "tenantId": "{someguid}",
  "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/"
}

This entire things needs to be stored in a GitHub Actions secret. I called it AZURE_CREDENTIALS

Stopping the virtual machine manually and on a schedule

This requires this simple bit of YAML. The first step logs in, the second step stops the virtual machine.

name: stopvm

on:
  workflow_dispatch:
  schedule:
    - cron: '15 15 * * 1-5'
    - cron: '19 15 * * 1-5'

jobs:
  deploy:
    runs-on: ubuntu-latest

    name: shutdown
    steps: 

      - name: Azure Login
        uses: Azure/login@v1
        with:
          creds: ${{ secrets.AZURE_VM_CREDENTIALS }}  

      - name: Azure CLI script
        uses: azure/CLI@v1
        with:
          azcliversion: 2.30.0
          inlineScript: |
            az vm stop --resource-group VMresourcegroup --name VMname
            az vm deallocate --resource-group VM resource group --name VMname

All times are in UTC, so the build server is stopped at 3:15pm and at 7:15pm another attempt is done. In CEST we are now in DST so that’s 5:15pm and 9:15pm. You can also stop it manually.

Note: the virtual machine is not only stopped, but also deallocated. Otherwise, it will still be billed.

Starting the virtual machine on demand

Now this is the really funky thing. Initially, I just let the machine start at 8am. But sometimes days go by without a branch being merged to develop. So why not have the virtual machine start on demand? Turned out to be pretty easy. On top of my build file I added this extra job:

name: mybuild

on:
  push:
      branches:
        - develop

jobs:
  # First, start the vm
  startvm:
    runs-on: ubuntu-latest

    name: startvm
    steps: 

      - name: Azure Login
        uses: Azure/login@v1
        with:
          creds: ${{ secrets.AZURE_VM_CREDENTIALS }}

      - name: Azure CLI script
        uses: azure/CLI@v1
        with:
          azcliversion: 2.30.0
          inlineScript: |
            az vm start --resource-group VMresourcegroup --name VMname

  # Then, build the project

  build-project:
    runs-on: somerunner

And now the build server spins up whenever the first merge of the day is coming in. Technically, I could have it shutdown again afterwards and really pinch down on virtual machine costs, but since merges tend to come in flocks, that’s not very practical so I decided against it. But it would be prefectly possible. But I did add two stop moments, in case a merge happens after 5:15.

Since you can copy and paste all the code off this blog post, I will dispense with an actual demo project this time.