Skip to content

create-azure-self-hosted-runners #707

create-azure-self-hosted-runners

create-azure-self-hosted-runners #707

name: create-azure-self-hosted-runners
on:
workflow_dispatch:
inputs:
runner_scope:
type: choice
required: true
description: Scope of the runner. On personal accounts, only "repo-level" works
options:
- org-level
- repo-level
default: repo-level
runner_org:
type: string
required: false
description: Organization or personal account to deploy the runner to (defaults to the repository owner)
runner_repo:
type: string
required: false
description: Repo to deploy the runner to. Only needed if runner_scope is set to "repo-level" (defaults to current repository)
deallocate_immediately:
type: string
required: true
description: Deallocate the runner immediately after creating it (useful for spinning up runners preemptively)
default: "false"
env:
ACTIONS_RUNNER_SCOPE: ${{ github.event.inputs.runner_scope }}
ACTIONS_RUNNER_ORG: "${{ github.event.inputs.runner_org || github.repository_owner }}"
ACTIONS_RUNNER_REPO: "${{ github.event.inputs.runner_repo || github.event.repository.name }}"
DEALLOCATE_IMMEDIATELY: ${{ github.event.inputs.deallocate_immediately }}
# This has to be a public URL that the VM can access after creation
POST_DEPLOYMENT_SCRIPT_URL: https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref }}/azure-self-hosted-runners/post-deployment-script.ps1
# Note that you'll need "p" (arm64 processor) and ideally "d" (local temp disk). The number 4 stands for 4 CPU-cores.
# For a convenient overview of all arm64 VM types, see e.g. https://azureprice.net/?_cpuArchitecture=Arm64
AZURE_VM_TYPE: Standard_D4plds_v5
# At the time of writing, "eastus", "eastus2" and "westus2" were among the cheapest region for the VM type we're using.
# For more information, see https://learn.microsoft.com/en-us/azure/virtual-machines/dplsv5-dpldsv5-series (which
# unfortunately does not have more information about price by region)
AZURE_VM_REGION: westus2
AZURE_VM_IMAGE: win11-24h2-ent
# The following secrets are required for this workflow to run:
# AZURE_CREDENTIALS - Credentials for the Azure CLI. It's recommended to set up a resource
# group specifically for self-hosted Actions Runners.
# az ad sp create-for-rbac --name "{YOUR_DESCRIPTIVE_NAME_HERE}" --role contributor \
# --scopes /subscriptions/{SUBSCRIPTION_ID_HERE}/resourceGroups/{RESOURCE_GROUP_HERE} \
# --sdk-auth
# AZURE_RESOURCE_GROUP - Resource group to create the runner(s) in
# AZURE_VM_USERNAME - Username of the VM so you can RDP into it
# AZURE_VM_PASSWORD - Password of the VM so you can RDP into it
jobs:
create-runner:
runs-on: ubuntu-latest
outputs:
vm_name: ${{ steps.generate-vm-name.outputs.vm_name }}
steps:
- name: Generate VM name
id: generate-vm-name
run: |
VM_NAME="actions-runner-$(date +%Y%m%d%H%M%S%N)"
echo "Will be using $VM_NAME as the VM name"
echo "vm_name=$VM_NAME" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4
- name: Obtain installation token
id: setup
uses: actions/github-script@v7
with:
script: |
const appId = ${{ secrets.GH_APP_ID }}
const privateKey = `${{ secrets.GH_APP_PRIVATE_KEY }}`
const getAppInstallationId = require('./get-app-installation-id')
const installationId = await getAppInstallationId(
console,
appId,
privateKey,
process.env.ACTIONS_RUNNER_ORG,
process.env.ACTIONS_RUNNER_REPO
)
const getInstallationAccessToken = require('./get-installation-access-token')
const { token: accessToken } = await getInstallationAccessToken(
console,
appId,
privateKey,
installationId
)
core.setSecret(accessToken)
core.setOutput('token', accessToken)
# We can't use the octokit/request-action as we can't properly mask the runner token with it
# https://github.com/actions/runner/issues/475
- name: Generate Actions Runner token and registration URL
run: |
case "$ACTIONS_RUNNER_SCOPE" in
"org-level")
ACTIONS_API_URL="https://api.github.com/repos/$ACTIONS_RUNNER_ORG/actions/runners/registration-token"
ACTIONS_RUNNER_REGISTRATION_URL="https://github.com/$ACTIONS_RUNNER_ORG"
;;
"repo-level")
ACTIONS_API_URL="https://api.github.com/repos/$ACTIONS_RUNNER_ORG/$ACTIONS_RUNNER_REPO/actions/runners/registration-token"
ACTIONS_RUNNER_REGISTRATION_URL="https://github.com/$ACTIONS_RUNNER_ORG/$ACTIONS_RUNNER_REPO"
;;
*)
echo "Unsupported runner scope: $ACTIONS_RUNNER_SCOPE"
exit 1
;;
esac
ACTIONS_RUNNER_TOKEN=$(curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ steps.setup.outputs.token }}"\
-H "X-GitHub-Api-Version: 2022-11-28" \
$ACTIONS_API_URL \
| jq --raw-output .token)
echo "::add-mask::$ACTIONS_RUNNER_TOKEN"
# The Azure VM type we use has blazing-fast local, temporary storage available as the D:\ drive.
# The only downside is that, after dellocation, the contents of this disk (including the Actions Runner),
# are destroyed. Let's only use it when we don't immediately deallocate the VM.
if [[ "$DEALLOCATE_IMMEDIATELY" == "true" ]]; then
ACTIONS_RUNNER_PATH="C:\a"
else
ACTIONS_RUNNER_PATH="D:\a"
fi
AZURE_ARM_PARAMETERS=$(tr '\n' ' ' <<-END
githubActionsRunnerRegistrationUrl="$ACTIONS_RUNNER_REGISTRATION_URL"
githubActionsRunnerToken="$ACTIONS_RUNNER_TOKEN"
postDeploymentPsScriptUrl="$POST_DEPLOYMENT_SCRIPT_URL"
virtualMachineImage="$AZURE_VM_IMAGE"
virtualMachineName="${{ steps.generate-vm-name.outputs.vm_name }}"
virtualMachineSize="$AZURE_VM_TYPE"
publicIpAddressName1="${{ steps.generate-vm-name.outputs.vm_name }}-ip"
adminUsername="${{ secrets.AZURE_VM_USERNAME }}"
adminPassword="${{ secrets.AZURE_VM_PASSWORD }}"
stopService="$DEALLOCATE_IMMEDIATELY"
githubActionsRunnerPath="$ACTIONS_RUNNER_PATH"
location="$AZURE_VM_REGION"
END
)
echo "AZURE_ARM_PARAMETERS=$AZURE_ARM_PARAMETERS" >> $GITHUB_ENV
- name: Azure Login
uses: ./.github/workflows/azure-login
with:
credentials: ${{ secrets.AZURE_CREDENTIALS }}
- uses: azure/arm-deploy@v2
id: deploy-arm-template
with:
resourceGroupName: ${{ secrets.AZURE_RESOURCE_GROUP }}
deploymentName: deploy-${{ steps.generate-vm-name.outputs.vm_name }}
template: ./azure-self-hosted-runners/azure-arm-template.json
parameters: ./azure-self-hosted-runners/azure-arm-template-example-parameters.json ${{ env.AZURE_ARM_PARAMETERS }}
scope: resourcegroup
- name: Show some more information on failure
if: failure()
run: |
echo "::group::VM status"
az vm get-instance-view --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --name ${{ steps.generate-vm-name.outputs.vm_name }} --query "instanceView.statuses"
az vm get-instance-view --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --name ${{ steps.generate-vm-name.outputs.vm_name }} --query "statuses"
echo "::endgroup::"
echo "::group::Deployment logs"
az group deployment show --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --name deploy-${{ steps.generate-vm-name.outputs.vm_name }}
echo "::endgroup::"
echo "::group::Extension logs"
az vm extension show --resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} --vm-name ${{ steps.generate-vm-name.outputs.vm_name }} --name CustomScriptExtension
echo "::endgroup::"
- name: Show post-deployment script output
if: always()
env:
CUSTOM_SCRIPT_OUTPUT: ${{ steps.deploy-arm-template.outputs.customScriptInstanceView }}
run: echo "$CUSTOM_SCRIPT_OUTPUT" | jq -r '.substatuses[0].message'
- name: Deallocate the VM for later use
if: env.DEALLOCATE_IMMEDIATELY == 'true'
run: az vm deallocate -n ${{ steps.generate-vm-name.outputs.vm_name }} -g ${{ secrets.AZURE_RESOURCE_GROUP }} --verbose