Skip to content

Optionally keep self-hosted-runners allocated after creating them #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 47 additions & 29 deletions .github/workflows/create-azure-self-hosted-runners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ name: create-azure-self-hosted-runners
on:
workflow_dispatch:
inputs:
amount_of_runners:
description: 'Amount of runners to set up'
required: true
default: 1
runner_scope:
type: choice
required: true
Expand All @@ -23,14 +19,26 @@ on:
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:
AMOUNT_OF_RUNNERS: ${{ github.event.inputs.amount_of_runners }}
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_name }}/azure-self-hosted-runners/post-deployment-script.ps1
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 8 stands for 8 CPU-cores.
# For a convenient overview of all arm64 VM types, see e.g. https://azureprice.net/?_cpuArchitecture=Arm64
AZURE_VM_TYPE: Standard_D8plds_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

# 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
Expand All @@ -42,24 +50,8 @@ env:
# 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-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.create-matrix.outputs.matrix }}
steps:
- name: Create matrix for setting up runners in parallel
id: create-matrix
run: |
echo "Going to create $AMOUNT_OF_RUNNERS runners"
MATRIX="matrix={\"runner_index\":[$(seq -s "," 1 $AMOUNT_OF_RUNNERS)]}"
echo "Going to use this matrix: $MATRIX"
echo $MATRIX >> $GITHUB_OUTPUT
create-runners:
name: create-runner-${{ matrix.runner_index }}
needs: create-matrix
create-runner:
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.create-matrix.outputs.matrix) }}
outputs:
vm_name: ${{ steps.generate-vm-name.outputs.vm_name }}
steps:
Expand Down Expand Up @@ -103,12 +95,12 @@ jobs:
run: |
case "$ACTIONS_RUNNER_SCOPE" in
"org-level")
ACTIONS_API_URL="https://api.github.com/repos/${{ env.ACTIONS_RUNNER_ORG }}/actions/runners/registration-token"
echo ACTIONS_RUNNER_REGISTRATION_URL="https://github.com/${{ env.ACTIONS_RUNNER_ORG }}" >> $GITHUB_ENV
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/${{ env.ACTIONS_RUNNER_ORG }}/${{ env.ACTIONS_RUNNER_REPO }}/actions/runners/registration-token"
echo ACTIONS_RUNNER_REGISTRATION_URL="https://github.com/${{ env.ACTIONS_RUNNER_ORG }}/${{ env.ACTIONS_RUNNER_REPO }}" >> $GITHUB_ENV
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"
Expand All @@ -124,7 +116,32 @@ jobs:
$ACTIONS_API_URL \
| jq --raw-output .token)
echo "::add-mask::$ACTIONS_RUNNER_TOKEN"
echo ACTIONS_RUNNER_TOKEN=$ACTIONS_RUNNER_TOKEN >> $GITHUB_ENV

# 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"
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really possible to put multi-line values into GITHUB_ENV? I was under the impression that that's impossible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you need a <<EOF trick: see e.g. this blog post.

Copy link
Contributor

@dennisameling dennisameling Jan 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it locally, and AZURE_ARM_PARAMETERS=$(cat <<-END actually lets Bash turn it into a single-line, space-separated string, so then it can be written into the $GITHUB_ENV without having to do the <<EOF trick you mentioned 🙏🏼

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that the $(...) aren't enclosed in double-quotes. Excellent!


- name: Azure Login
uses: azure/login@v1
Expand All @@ -135,9 +152,10 @@ jobs:
with:
resourceGroupName: ${{ secrets.AZURE_RESOURCE_GROUP }}
template: ./azure-self-hosted-runners/azure-arm-template.json
parameters: ./azure-self-hosted-runners/azure-arm-template-example-parameters.json githubActionsRunnerRegistrationUrl="${{ env.ACTIONS_RUNNER_REGISTRATION_URL }}" githubActionsRunnerToken="${{ env.ACTIONS_RUNNER_TOKEN }}" postDeploymentPsScriptUrl="${{ env.POST_DEPLOYMENT_SCRIPT_URL }}" virtualMachineName=${{ steps.generate-vm-name.outputs.vm_name }} virtualMachineSize=Standard_D8pls_v5 publicIpAddressName1=${{ steps.generate-vm-name.outputs.vm_name }}-ip adminUsername=${{ secrets.AZURE_VM_USERNAME }} adminPassword=${{ secrets.AZURE_VM_PASSWORD }}
parameters: ./azure-self-hosted-runners/azure-arm-template-example-parameters.json ${{ env.AZURE_ARM_PARAMETERS }}

- name: Deallocate the VM for later use
if: env.DEALLOCATE_IMMEDIATELY == 'true'
uses: azure/CLI@v1
with:
azcliversion: 2.43.0
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/delete-self-hosted-runner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: delete azure-self-hosted-runner
run-name: Delete ${{ inputs.runner_name }}

on:
workflow_dispatch:
inputs:
runner_name:
type: string
required: true
description: The name of the runner that needs to be deleted

env:
ACTIONS_RUNNER_NAME: ${{ github.event.inputs.runner_name }}

# 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
jobs:
delete-runner:
runs-on: ubuntu-latest
steps:
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Delete VM '${{ env.ACTIONS_RUNNER_NAME }}'
uses: azure/CLI@v1
with:
azcliversion: 2.43.0
inlineScript: |
az vm delete -n "$ACTIONS_RUNNER_NAME" -g ${{ secrets.AZURE_RESOURCE_GROUP }} --yes
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, kind of. But it leaves two resources behind, the network security group and the virtual network:

image

@dennisameling you mentioned previously that we could avoid that by reusing a single resource? Or is there a way to pass one or two deleteOptions so that these resources are deleted, too, when the VM is deleted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see whether 4c2ba65 works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is a known issue: Azure/azure-cli#22158

We might need 1-2 additional az CLI calls to find the NSG/Vnet that are attached to the VM and delete them in follow-up calls.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it does not work.

We might need 1-2 additional az CLI calls to find the NSG/Vnet that are attached to the VM and delete them in follow-up calls.

I guess we need 2 additional CLI calls, but we know the exact name i.e. we do not have to identify the resources via a separate call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we need 2 additional CLI calls, but we know the exact name i.e. we do not have to identify the resources via a separate call.

Yep - looks like that should be pretty straightforward:

https://learn.microsoft.com/en-us/cli/azure/network/nsg?view=azure-cli-latest#az-network-nsg-delete

az network nsg delete -g MyResourceGroup -n MyNsg

https://learn.microsoft.com/en-us/cli/azure/network/vnet?view=azure-cli-latest#az-network-vnet-delete

az network vnet delete -g MyResourceGroup -n myVNet

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

az network nsg delete -n "$ACTIONS_RUNNER_NAME"-nsg -g ${{ secrets.AZURE_RESOURCE_GROUP }}
az network vnet delete -n "$ACTIONS_RUNNER_NAME"-vnet -g ${{ secrets.AZURE_RESOURCE_GROUP }}
az network public-ip delete -n "$ACTIONS_RUNNER_NAME"-ip -g ${{ secrets.AZURE_RESOURCE_GROUP }}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"value": "westeurope"
},
"enableAcceleratedNetworking": {
"value": true
},
Expand Down Expand Up @@ -50,7 +47,7 @@
"value": "Standard"
},
"pipDeleteOption": {
"value": "Detach"
"value": "Delete"
},
"osDiskType": {
"value": "Premium_LRS"
Expand All @@ -59,7 +56,7 @@
"value": "Delete"
},
"nicDeleteOption": {
"value": "Detach"
"value": "Delete"
},
"patchMode": {
"value": "AutomaticByOS"
Expand Down
14 changes: 13 additions & 1 deletion azure-self-hosted-runners/azure-arm-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
"description": "GitHub Actions Runner registration token for the org/repo. Note that these tokens are only valid for one hour after creation!"
}
},
"githubActionsRunnerPath": {
"type": "string",
"metadata": {
"description": "Path to the Actions Runner. Keep this path short to prevent Long Path issues, e.g. D:\\a"
}
},
"postDeploymentPsScriptUrl": {
"type": "string",
"minLength": 6,
Expand All @@ -30,6 +36,12 @@
"description": "Windows Computer Name. Can be maximum 15 characters."
}
},
"stopService": {
"type": "string",
"metadata": {
"description": "(optional) Whether to stop the service immediately. Useful for spinning up runners preemptively."
}
},
"location": {
"type": "string"
},
Expand Down Expand Up @@ -250,7 +262,7 @@
"value": "[parameters('location')]"
},
"arguments": {
"value": "[concat('-GitHubActionsRunnerToken ', parameters('githubActionsRunnerToken'), ' -GithubActionsRunnerRegistrationUrl ', parameters('githubActionsRunnerRegistrationUrl'), ' -GithubActionsRunnerName ', parameters('virtualMachineName'))]"
"value": "[concat('-GitHubActionsRunnerToken ', parameters('githubActionsRunnerToken'), ' -GithubActionsRunnerRegistrationUrl ', parameters('githubActionsRunnerRegistrationUrl'), ' -GithubActionsRunnerName ', parameters('virtualMachineName'), ' -StopService ', parameters('stopService'), ' -GitHubActionsRunnerPath ', parameters('githubActionsRunnerPath'))]"
}
}
},
Expand Down
23 changes: 19 additions & 4 deletions azure-self-hosted-runners/post-deployment-script.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ param (

# Actions Runner name. Needs to be unique in the org/repo
[Parameter(Mandatory=$true)]
[string]$GithubActionsRunnerName
[string]$GithubActionsRunnerName,

# Stop Service immediately (useful for spinning up runners preemptively)
[Parameter(Mandatory=$false)]
[ValidateSet('true', 'false')]
[string]$StopService = 'true',

# Path to the Actions Runner. Keep this path short to prevent Long Path issues, e.g. D:\a
[Parameter(Mandatory=$true)]
[string]$GitHubActionsRunnerPath
)

Write-Output "Starting post-deployment script."
Expand All @@ -28,8 +37,6 @@ $GitHubActionsRunnerVersion = "2.300.2"
$GithubActionsRunnerArch = "arm64"
$GithubActionsRunnerHash = "9409e50d9ad33d8031355ed079b8f56cf3699f35cf5d0ca51e54deed432758ef"
$GithubActionsRunnerLabels = "self-hosted,Windows,ARM64"
# Keep this path short to prevent Long Path issues
$GitHubActionsRunnerPath = "C:\a"

# ======================
# WINDOWS DEVELOPER MODE
Expand Down Expand Up @@ -106,6 +113,12 @@ if((Get-FileHash -Path ${GitHubActionsRunnerPath}\actions-runner-win-${GithubAct
Write-Output "Installing GitHub Actions runner ${GitHubActionsRunnerVersion} as a Windows service with labels ${GithubActionsRunnerLabels}..."

Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::ExtractToDirectory("${GitHubActionsRunnerPath}\actions-runner-win-${GithubActionsRunnerArch}-${GitHubActionsRunnerVersion}.zip", $GitHubActionsRunnerPath)

Write-Output "Configuring the runner to shut down automatically after running"
Set-Content -Path "${GitHubActionsRunnerPath}\shut-down.ps1" -Value "shutdown -s -t 60 -d p:4:0 -c `"workflow job is done`""
[System.Environment]::SetEnvironmentVariable("ACTIONS_RUNNER_HOOK_JOB_COMPLETED", "${GitHubActionsRunnerPath}\shut-down.ps1", [System.EnvironmentVariableTarget]::Machine)

Write-Output "Configuring the runner"
cmd.exe /c "${GitHubActionsRunnerPath}\config.cmd" --unattended --ephemeral --name ${GithubActionsRunnerName} --runasservice --labels ${GithubActionsRunnerLabels} --url ${GithubActionsRunnerRegistrationUrl} --token ${GitHubActionsRunnerToken}

# Ensure that the service was created. If not, exit with error code.
Expand All @@ -116,6 +129,8 @@ if ($MatchedServices.count -eq 0) {
}

# Immediately stop the service as we want to leave the VM in a deallocated state for later use. The service will automatically be started when Windows starts.
Stop-Service -Name "actions.runner.*" -Verbose
if (${StopService} -eq 'true') {
Stop-Service -Name "actions.runner.*" -Verbose
}

Write-Output "Finished installing GitHub Actions runner."