Sawit M.
Posted on March 7, 2023
เรื่องมันมีอยู่ว่า อยากได้ GitHub Self-Hosted Runners แบบที่ autoscale ได้ตาม workload ครับ และตอนที่ไม่ใช้เลยก็สามารถ scale in instance เหลือ 0 ได้ ผมค้นไปเจอ Autoscaling self hosted GitHub runner containers with Azure Container Apps (ACA) มา คิดว่าค่อนข้างตรงกับที่ต้องการ เลยเอามาลอง POC ดูครับ
จากใน link ข้างต้น จะเป็นการ run container image ของ GitHub Self-Hosted Runners ด้วย Azure Container Apps และอาศัยความสามารถของ KEDA autoscaler ในการ scale replica ตามจำนวน queue ใน Azure Storage Queue
การทำงานจะเป็นแบบนี้ครับ
- เมื่อ GitHub workflow ถูก trigger ให้ run มันจะไป push message ลงไปที่ Azure Storage Queue
- KEDA scaler ใน Azure Container Apps ตรวจพบว่ามี queue เพิ่มขึ้น จึงทำการ scale out instance ของ GitHub selfhosted runners ตามจำนวน queue
- เมื่อ scale เรียบร้อย ตัว GitHub selfhosted runners จะมารับงานจาก GitHub workflow ไป run จนเรียบร้อย
- GitHub workflow ทำการ delete message ออกจาก Azure Storage Queue
- KEDA scaler ตรวจพบว่ามี queue ลดลงจึง scale in instance ของ GitHub selfhosted runners ลง ถ้า queue เป็น 0 จำนวน instance ของ GitHub selfhosted runners ก็เป็น 0
ด้วยความคัน ผมจะเปลี่ยนนิดหน่อย ดังนี้
- ใช้เป็น Azure Service Bus Queue แทน Azure Storage Queue เพราะในกรณีที่มีการ send / receive message เยอะ Azure Service Bus Queue จะถูกกว่านิดหน่อย (อ่านเพิ่ม Storage queues and Service Bus queues - compared and contrasted)
- ปรับ workflow ให้ใช้ Workload Identity Federation เพื่อให้เป็น passwordless
- สร้าง Azure Container Apps แบบมี Virtual Network Integration เพื่อให้ interact กับ resources ที่เป็น private ได้
Diagram จะเป็นแบบนี้นะครับ
และเช่นกัน ณ เวลาที่ผมเขียน blog นี้นั้น Azure Container Apps ยังมีข้อจำกัดดังนี้
- Support แค่ Linux-based x86-64 (linux/amd64) container image.
- ยังไม่มี KEDA scalers for GitHub runners
- ยังไม่มีที่ Southeast Asia
ทีนี้ก่อนเริ่มทำเรามาดูกันก่อนว่าเราต้องมีอะไรกันบ้าง
- Azure Subscription ( ใช้เพื่อ deploy resources ต่างๆ เช่น vnet, log analytics workspace, container apps และ service bus เป็นต้น )
- Github Organization ( เราจะให้ runners เป็น oganization level runners )
- Github Repository ( ใช้เก็บและ run workflow )
ลงมือทำกันเลย
Prerequisites
- Create Azure subscription
- Sign up Github
- Create repository
- Create organization
-
Create Github Access Token โดยเลือก scope เป็น
admin:org
- Create GitHub Self-hosted Runners Container Image ดูได้จาก docker-github-actions-runner หรือ Create a Docker based Self Hosted GitHub runner Linux container แล้ว push ขึ้น container registry ของเรา
Preflight
- ทำการ install extensions ของ Azure CLI และ providers ที่ subscription
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.ServiceBus
- Define variables
POC_PREFIX="pocghaca"
LOCATION="westus3"
LOC="usw3"
RESOURCE_GROUP="rg-${POC_PREFIX}-az-${LOC}-001"
VNET_NAME="vnet-${POC_PREFIX}-az-${LOC}-001"
INFRASTRUCTURE_SUBNET="snet-infra-az-${LOC}-001"
LOG_ANALYTIC_WORKSPACE="log-${POC_PREFIX}-az-${LOC}-001"
SERVICEBUS_NAME="sbn-${POC_PREFIX}-az-${LOC}-001"
SERVICEBUS_QUEUE="gh-runner-scaler"
CONTAINERAPPS_ENVIRONMENT="cte-${POC_PREFIX}-az-${LOC}-001"
CONTAINERAPP_NAME="cta-${POC_PREFIX}-az-${LOC}-001"
CONTAINER_REGISTRY_SERVER="..<YOUR-CONTAINER-REGISTRY>.."
CONTAINER_REGISTRY_USER="..<YOUR-REGISTRY-USERNAME>.."
CONTAINER_REGISTRY_PASS="..<YOUR-REGISTRY-PASSWORD>.."
CONTAINER_IMAGE="..<YOU-CONTAINER-IMAGE>.."
PAT="..<YOUR-GITHUB-ACCESS-TOKEN>.."
GH_ORG="..<YOUR-ORGANIZATION-NAME>.."
Prerequisite Azure Resources
- Create Resource group
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION
- Create Log analytics workspaces
az monitor log-analytics workspace create \
--name $LOG_ANALYTIC_WORKSPACE \
--resource-group $RESOURCE_GROUP \
--quota 1 \
--retention-time 30 \
--sku PerGB2018
- Create Virtual network and subnet
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--location $LOCATION \
--address-prefix 10.0.0.0/16
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $INFRASTRUCTURE_SUBNET \
--address-prefixes 10.0.0.0/21
Azure Service Bus and Queue
- Create Azure service bus and queue
az servicebus namespace create \
--name $SERVICEBUS_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
az servicebus queue create \
--resource-group $RESOURCE_GROUP \
--namespace-name $SERVICEBUS_NAME \
--name $SERVICEBUS_QUEUE
- Create connection string of Azure Service Bus Queue
az servicebus queue authorization-rule create \
--resource-group $RESOURCE_GROUP \
--namespace-name $SERVICEBUS_NAME \
--queue-name $SERVICEBUS_QUEUE \
--name $CONTAINERAPP_NAME \
--rights Manage Send Listen
- Add yourself as
Azure Service Bus Data Owner
on Azure Service Bus Namespace
- Test send a message and receive/delete a message from the queue
⚠️ ถ้าทำใน bash ของ Azure Cloud Shell ใน run
az login
อีกครั้ง
# Send message
az rest \
--resource "https://servicebus.azure.net" \
--method post \
--headers BrokerProperties='{"TimeToLive":10}' \
--url "https://${SERVICEBUS_NAME}.servicebus.windows.net/${SERVICEBUS_QUEUE}/messages" \
--body "Hello from Az REST with TTL"
# Receive / Delete message
az rest \
--resource "https://servicebus.azure.net" \
--method delete \
--url "https://${SERVICEBUS_NAME}.servicebus.windows.net/${SERVICEBUS_QUEUE}/messages/head"
Create Azure Container App
- รวบรวมข้อมูลที่ใช้ในการสร้าง Azure Container App และ Azure Container Environment
INFRASTRUCTURE_SUBNET=$(az network vnet subnet show --resource-group $RESOURCE_GROUP --vnet-name $VNET_NAME --name $INFRASTRUCTURE_SUBNET --query "id" -o tsv | tr -d '[:space:]')
LOG_WORKSPACE_ID=$(az monitor log-analytics workspace show --name $LOG_ANALYTIC_WORKSPACE --resource-group $RESOURCE_GROUP --query "customerId" -o tsv | tr -d '[:space:]')
LOG_WORKSPACE_KEY=$(az monitor log-analytics workspace get-shared-keys --name $LOG_ANALYTIC_WORKSPACE --resource-group $RESOURCE_GROUP --query "primarySharedKey" -o tsv | tr -d '[:space:]')
CONNECTION_QUEUE=$(az servicebus queue authorization-rule keys list --resource-group $RESOURCE_GROUP --namespace-name $SERVICEBUS_NAME --queue-name $SERVICEBUS_QUEUE --name $CONTAINERAPP_NAME --query primaryConnectionString --output tsv | tr -d '[:space:]')
- Create Azure Container App Environment
az containerapp env create \
--name $CONTAINERAPPS_ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--infrastructure-subnet-resource-id $INFRASTRUCTURE_SUBNET \
--docker-bridge-cidr "172.17.0.1/16" \
--platform-reserved-cidr "100.64.0.0/16" \
--platform-reserved-dns-ip "100.64.0.10" \
--logs-destination "log-analytics" \
--logs-workspace-id $LOG_WORKSPACE_ID \
--logs-workspace-key $LOG_WORKSPACE_KEY \
--internal-only
- Create Azure Container App with autoscaling
az containerapp create \
--resource-group $RESOURCE_GROUP \
--name $CONTAINERAPP_NAME \
--image $CONTAINER_IMAGE \
--environment $CONTAINERAPPS_ENVIRONMENT \
--registry-server $CONTAINER_REGISTRY_SERVER \
--registry-username $CONTAINER_REGISTRY_USER \
--registry-password $CONTAINER_REGISTRY_PASS \
--secrets gh-token="$PAT" servicebus-connection-string="$CONNECTION_QUEUE" \
--env-vars \
ACCESS_TOKEN=secretref:gh-token \
RUNNER_SCOPE="org" \
ORG_NAME=$GH_ORG \
LABELS="poc-gh-aca" \
RUNNER_WORKDIR="/tmp/github-runner" \
--cpu "1.75" \
--memory "3.5Gi" \
--min-replicas 0 \
--max-replicas 3 \
--scale-rule-auth "connection=servicebus-connection-string" \
--scale-rule-name queue-scaling \
--scale-rule-type azure-servicebus \
--scale-rule-metadata \
"queueName=$SERVICEBUS_QUEUE" \
"namespace=$SERVICEBUS_NAME" \
"messageCount=1"
- ตรวจสอบ system log
az containerapp logs show \
--resource-group $RESOURCE_GROUP \
--name $CONTAINERAPP_NAME \
--type console \
--follow
- ตรวจสอบ console log
az containerapp logs show \
--resource-group $RESOURCE_GROUP \
--name $CONTAINERAPP_NAME \
--type system \
--follow
- ตรวจสอบว่า runner เกิดขึ้นใน organization ของเรา
- ลอง send และ delete message แล้วตรวจสอบ Queue ใน Azure Service Bus และ check จำนวน runner ที่เกิดขึ้นใน organization
# Send message
az rest \
--resource "https://servicebus.azure.net" \
--method post \
--headers BrokerProperties='{"TimeToLive":3600}' \
--url "https://${SERVICEBUS_NAME}.servicebus.windows.net/${SERVICEBUS_QUEUE}/messages" \
--body "Hello from Az REST with TTL"
# Receive / Delete message
az rest \
--resource "https://servicebus.azure.net" \
--method delete \
--url "https://${SERVICEBUS_NAME}.servicebus.windows.net/${SERVICEBUS_QUEUE}/messages/head"
# See console log
az containerapp logs show \
--resource-group $RESOURCE_GROUP \
--name $CONTAINERAPP_NAME \
--type console \
--follow
# See system log
az containerapp logs show \
--resource-group $RESOURCE_GROUP \
--name $CONTAINERAPP_NAME \
--type system \
--follow
Create GitHub workflow to verify our system
- ทำการ create Service Principal แล้ว federate credential ไปให้ Github Organization และ Repository ที่เราสร้างเตรียมไว้โดยเลือกเป็น branch main
รายละเอียดดูได้จาก Let Github Action Access Azure Resources without password
- Create role assignment ให้ Service Principal ของเรามีสิทธิ์
Azure Service Bus Data Receiver
และAzure Service Bus Data Sender
ที่ Queue ที่เราสร้างไว้
- Create role assignment ให้ Service Principal ของเรามีสิทธิ์
Contributor
บน subscription ของเรา
- สำหรับสาย free ถ้า GitHub repository ของคุณเป็น public ต้องมา allow ที่ Runner Groups Default ก่อน
- ต่อไปเข้าไปที่ Repository ของตัวเองแล้ว define secret และ variable เหล่านี้
- Secrets
AZURE_CLIENT_ID
AZURE_SUBSCRIPTION_ID
AZURE_TENANT_ID
- Variables
AZ_SERVICE_BUS_NAMESPACE
AZ_SERVICE_BUS_QUEUE
- Secrets
- จากนั้นทำการสร้าง file workflow
.github/workflows/demo.yaml
ใน GitHub Repository
name: Scale up selfhosted runner and run
on:
workflow_dispatch:
permissions:
id-token: write
contents: read
env:
AZ_SERVICE_BUS_NAMESPACE: ${{ vars.AZ_SERVICE_BUS_NAMESPACE }}
AZ_SERVICE_BUS_QUEUE: ${{ vars.AZ_SERVICE_BUS_QUEUE }}
jobs:
scale-out-runner:
runs-on: ubuntu-latest
steps:
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: scale out self hosted
run: |
az rest \
--resource "https://servicebus.azure.net" \
--method post \
--headers BrokerProperties='{"TimeToLive":3600}' \
--url "https://${{ env.AZ_SERVICE_BUS_NAMESPACE }}.servicebus.windows.net/${{ env.AZ_SERVICE_BUS_QUEUE }}/messages" \
--body "${{ github.run_id }}"
deploy:
runs-on: [self-hosted, poc-gh-aca]
needs: [scale-out-runner]
steps:
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: 'Run az commands'
run: |
az account show --output table
scale-in-runner:
runs-on: ubuntu-latest
needs: [scale-out-runner, deploy]
if: ${{ always() && needs.scale-out-runner.result == 'success' }}
steps:
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: scale in self hosted
run: |
az rest \
--resource "https://servicebus.azure.net" \
--method delete \
--url "https://${{ env.AZ_SERVICE_BUS_NAMESPACE }}.servicebus.windows.net/${{ env.AZ_SERVICE_BUS_QUEUE }}/messages/head"
ใน workflow จะมี 3 jobs
-
scale-out-runner
: จะเป็นการ send message เข้าไปที่ Service Bus เพื่อ ให้ container apps scale out GitHub SelfHosted Runners ขึ้นมารับงานไปทำ -
deploy
: จะเป็นส่วนงานหลักที่ pipeline ต้องทำ โดย job นี้จะทำก็ต่อเมื่อscale-out-runner
success -
scale-in-runner
: หลังจากทำงานเสร็จไม่ว่าจะ success หรือไม่ job นี้จะทำการ delete message ออกจาก Service Bus เพื่อให้ container apps ทำการ scale in GitHub SelfHosted Runners ลงไป
- ให้ลองทำการ run workflow หลายๆ ครั้ง พร้อมๆ กัน
-
ตรวจสอบดูว่าการ autoscale ทำงานได้ตามที่คาดการณไว้หรือไม่
- Azure Service Bus Queue
- Container App Replicas
- Github Organization-Level Self-hosted Runners
-
หลังจาก workflow run เรียบร้อยหมดแล้ว ลองตรวจสอบ อีกครั้ง ทุกอย่างต้องเป็น 0
- Azure Service Bus Queue
- Container App Replicas
- Github Organization-Level Self-hosted Runners
References
- KEDA | Azure Service Bus
- Scaling in Azure Container Apps
- Autoscaling self hosted GitHub runner containers with Azure Container Apps (ACA)
- Provide a virtual network to an internal Azure Container Apps environment
- Send Message to Queue
- Receive and Delete Message (Destructive Read)
- Calling Azure REST API via curl
Posted on March 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.