-
Notifications
You must be signed in to change notification settings - Fork 10
241 lines (221 loc) · 11.1 KB
/
create-azure-self-hosted-runners.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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: choice
options:
- false
- true
required: true
description: Deallocate the runner immediately after creating it (useful for spinning up runners preemptively)
default: "false"
ephemeral:
type: choice
options:
- false
- true
required: true
description: Start the runner in ephemeral mode (i.e. unregister after running one job)
default: "true"
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 }}
EPHEMERAL_RUNNER: ${{ github.event.inputs.ephemeral }}
# 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
permissions:
id-token: write # required for Azure login via OIDC
contents: read
# The following secrets are required for this workflow to run:
# AZURE_CLIENT_ID - The Client ID of an Azure Managed Identity. It is recommended to set up a resource
# group specifically for self-hosted Actions Runners, and to add a federated identity
# to authenticate as the currently-running GitHub workflow.
# az identity create --name <managed-identity-name> -g <resource-group>
# az identity federated-credential create \
# --identity-name <managed-identity-name> \
# --resource-group <resource-group> \
# --name github-workflow \
# --issuer https://token.actions.githubusercontent.com \
# --subject repo:git-for-windows/git-for-windows-automation:ref:refs/heads/main \
# --audiences api://AzureADTokenExchange
# MSYS_NO_PATHCONV=1 \
# az role assignment create \
# --assignee <client-id-of-managed-identity> \
# --scope '/subscriptions/<subscription-id>/resourceGroups/<resource-group>' \
# --role 'Contributor'
# AZURE_TENANT_ID - The Tenant ID of the Azure Managed Identity (i.e. the Azure Active Directory in which
# the Identity lives)
# AZURE_SUBSCRIPTION_ID - The Subscription ID with which the Azure Managed Identity is associated
# (technically, this is not necessary for `az login --service-principal` with a
# managed identity, but `Azure/login` requires it anyway)
# 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
# GH_APP_ID - The ID of the GitHub App whose credentials are to be used to obtain the runner token
# GH_APP_PRIVATE_KEY - The private key of the GitHub App whose credentials are to be used to obtain the runner token
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: |
# We need to URL-encode the user name because it usually is a GitHub App, which means that
# it has the suffix `[bot]`. If un-encoded, this would cause a cURL error "bad range in URL"
# because it would mistake this for an IPv6 address or something like that.
user_pwd="$(jq -n \
--arg user '${{ github.actor }}' \
--arg pwd '${{ secrets.GITHUB_TOKEN }}' \
'$user | @uri + ":" + $pwd')"
case "$ACTIONS_RUNNER_SCOPE" in
"org-level")
ACTIONS_API_URL="https://[email protected]/repos/$ACTIONS_RUNNER_ORG/actions/runners/registration-token"
ACTIONS_RUNNER_REGISTRATION_URL="https://[email protected]/$ACTIONS_RUNNER_ORG"
;;
"repo-level")
ACTIONS_API_URL="https://[email protected]/repos/$ACTIONS_RUNNER_ORG/$ACTIONS_RUNNER_REPO/actions/runners/registration-token"
ACTIONS_RUNNER_REGISTRATION_URL="https://[email protected]/$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
# Zip up and Base64-encode the post-deployment script; We used to provide a public URL
# for that script instead, but that does not work in private repositories (and we could
# not even use the `GITHUB_TOKEN` to access the file because it lacks the necessary
# scope to read repository contents).
POST_DEPLOYMENT_SCRIPT_ZIP_BASE64="$(
cd azure-self-hosted-runners &&
zip -9 tmp.zip post-deployment-script.ps1 >&2 &&
base64 -w 0 tmp.zip
)"
PUBLIC_IP_ADDRESS_NAME1="${{ github.repository_visibility != 'private' && format('{0}-ip', steps.generate-vm-name.outputs.vm_name) || '' }}"
AZURE_ARM_PARAMETERS=$(tr '\n' ' ' <<-END
githubActionsRunnerRegistrationUrl="$ACTIONS_RUNNER_REGISTRATION_URL"
githubActionsRunnerToken="$ACTIONS_RUNNER_TOKEN"
postDeploymentScriptZipBase64="$POST_DEPLOYMENT_SCRIPT_ZIP_BASE64"
postDeploymentScriptFileName="post-deployment-script.ps1"
virtualMachineImage="$AZURE_VM_IMAGE"
virtualMachineName="${{ steps.generate-vm-name.outputs.vm_name }}"
virtualMachineSize="$AZURE_VM_TYPE"
publicIpAddressName1="$PUBLIC_IP_ADDRESS_NAME1"
adminUsername="${{ secrets.AZURE_VM_USERNAME }}"
adminPassword="${{ secrets.AZURE_VM_PASSWORD }}"
ephemeral="$EPHEMERAL_RUNNER"
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: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- 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' | sed 's/${{ secrets.GITHUB_TOKEN }}/***/g'
- 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