A scalable, cost-efficient, and secure solution for running self-hosted GitHub Actions runners on-demand using Azure's serverless and containerization services. This project eliminates the need for persistent VMs while providing ephemeral, scalable runner infrastructure.
graph TD
A[GitHub Webhook] --> B[Azure Receiver Function]
B --> C[Azure Storage Queue]
C --> D[Azure Controller Function]
D --> E[Azure Key Vault]
D --> F[Azure Container Registry]
D --> G[Azure Container Instance]
E --> G
F --> G
G --> H[GitHub Runner]
Component | Purpose | Technology |
---|---|---|
π£ Receiver Function | Handles GitHub webhook events and queues jobs | Azure Functions (HTTP Trigger) |
π¬ Storage Queue | Buffers and decouples job requests | Azure Storage Queue |
ποΈ Controller Function | Provisions container instances for runners | Azure Functions (Queue Trigger) |
π³ Container Instances | Runs ephemeral GitHub Actions runners | Azure Container Instances (ACI) |
π Key Vault | Securely stores GitHub App credentials and ACR credentials | Azure Key Vault |
π¦ Container Registry | Hosts custom runner container images | Azure Container Registry (ACR) |
π§Ή Cleanup Function | Automatically removes terminated containers | Azure Functions (Timer Trigger) |
- π Serverless & Event-Driven: Fully serverless architecture with automatic scaling
- π° Cost-Efficient: Pay only for actual compute usage, no idle resources
- π Security-First: GitHub App authentication, managed identities, and Key Vault integration
- β‘ Auto-Scaling: Handles parallel jobs with queue-based load balancing
- π§Ή Self-Cleaning: Automatic cleanup of terminated containers
- π³ Containerized: Custom Docker images for consistent runner environments
- π Multi-Environment: Terraform-based infrastructure with environment separation
- π Observable: Application Insights integration for monitoring and logging
Before deploying this solution, ensure you have:
- Azure Subscription with appropriate permissions
- Resource Group creation permissions
- Service Principal or User-Assigned Managed Identity for GitHub Actions
- GitHub Organization (personal accounts not supported for organization runners)
- GitHub App configured for the organization with the following permissions:
- Repository permissions:
Actions: Write
,Metadata: Read
- Organization permissions:
Self-hosted runners: Write
- Repository permissions:
- Organization Admin access to install the GitHub App
gha-self-hosted/
βββ π .github/workflows/ # GitHub Actions deployment workflows
β βββ build_deploy_controller_function.yml
β βββ build_deploy_images.yml
β βββ create_azure_remote_backend.yml
β βββ deploy_azure_infra.yml
β βββ deploy_receiver_function.yml
β βββ test_deploy_controller.yml
βββ π create-azure-infra/ # Main Terraform infrastructure
β βββ main.tf # Azure resources definition
β βββ variables.tf # Input variables
β βββ outputs.tf # Output values
β βββ data.tf # Data sources
β βββ provider.tf # Provider configuration
β βββ dev.tfvars # Environment-specific values
β βββ dev-backend.config # Terraform backend configuration
βββ π create-remote-state/ # Terraform backend setup
β βββ main.tf # Remote state infrastructure
β βββ variables.tf # Backend variables
βββ π github-runner-receiver-function/ # HTTP trigger function
β βββ function_app.py # Webhook receiver logic
β βββ requirements.txt # Python dependencies
β βββ host.json # Function configuration
β βββ local.settings.json # Local development settings
β βββ .funcignore # Function ignore file
βββ π github-runner-controller-function/ # Queue trigger function
β βββ function_app.py # Container provisioning logic
β βββ requirements.txt # Python dependencies
β βββ host.json # Function configuration
β βββ local.settings.json # Local development settings
β βββ .funcignore # Function ignore file
βββ π github-runner-cleanup-function/ # Timer trigger function
β βββ function_app.py # Cleanup logic
β βββ requirements.txt # Python dependencies
β βββ host.json # Function configuration
β βββ local.settings.json # Local development settings
β βββ .funcignore # Function ignore file
βββ π github-runner-images/ # Docker container definitions
β βββ docker-compose.yml # Multi-stage build configuration
β βββ test.http # API testing file
β βββ context/
β βββ Dockerfile.base # Base Ubuntu image with tools
β βββ Dockerfile.runner # GitHub Actions runner image
β βββ Dockerfile.test # Test container image
β βββ script/
β βββ app.sh # Runner registration script
β βββ generate_jwt.py # GitHub App JWT generation
β βββ requirements.txt # Python dependencies for scripts
βββ π design_diagram.png # Architecture diagram
βββ π README.md # This file
βββ π .gitignore # Git ignore rules
git clone https://github.com/subir0071/gha-self-hosted.git
cd gha-self-hosted
Add the following secrets to your GitHub repository:
Secret Name | Description | Example |
---|---|---|
AZURE_CLIENT_ID |
Azure Service Principal Client ID | 12345678-1234-1234-1234-123456789012 |
AZURE_TENANT_ID |
Azure Tenant ID | 87654321-4321-4321-4321-210987654321 |
AZURE_SUBSCRIPTION_ID |
Azure Subscription ID | abcdef12-3456-7890-abcd-ef1234567890 |
TF_VAR_GITHUB_APP_ID |
GitHub App ID | 123456 |
TF_VAR_GITHUB_APP_INSTALLATION_ID |
GitHub App Installation ID | 12345678 |
TF_VAR_GITHUB_APP_CLIENTID |
GitHub App Client ID | Iv1.a1b2c3d4e5f6g7h8 |
TF_VAR_GITHUB_APP_PEM_FILE |
GitHub App Private Key (PEM format) | -----BEGIN RSA PRIVATE KEY-----... |
Execute the GitHub Actions workflows in this order:
-
Create Terraform Backend:
# Run via GitHub Actions gh workflow run create_azure_remote_backend.yml
-
Deploy Azure Infrastructure:
# Run via GitHub Actions gh workflow run deploy_azure_infra.yml
-
Deploy Functions and Images:
# Deploy all components gh workflow run deploy_receiver_function.yml gh workflow run build_deploy_controller_function.yml gh workflow run build_deploy_images.yml
After successful deployment:
- Get the Receiver Function URL from Azure Portal or Terraform outputs
- Configure Organization Webhook:
- Go to your GitHub Organization β Settings β Webhooks
- Add webhook with the Receiver Function URL
- Select "Workflow jobs" events
- Set content type to "application/json"
Create a test workflow in your repository:
# .github/workflows/test-runner.yml
name: Test Self-Hosted Runner
on: [workflow_dispatch]
jobs:
test:
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- name: Test Runner
run: |
echo "π Running on self-hosted Azure runner!"
echo "Runner: $(hostname)"
echo "OS: $(cat /etc/os-release | grep PRETTY_NAME)"
gha-self-hosted/
βββ π .github/workflows/ # GitHub Actions deployment workflows
β βββ build_deploy_controller_function.yml
β βββ build_deploy_images.yml
β βββ create_azure_remote_backend.yml
β βββ deploy_azure_infra.yml
β βββ deploy_receiver_function.yml
βββ π create-azure-infra/ # Main Terraform infrastructure
β βββ main.tf # Azure resources definition
β βββ variables.tf # Input variables
β βββ outputs.tf # Output values
β βββ dev.tfvars # Environment-specific values
β βββ dev-backend.config # Terraform backend configuration
βββ π create-remote-state/ # Terraform backend setup
βββ π github-runner-receiver-function/ # HTTP trigger function
β βββ function_app.py # Webhook receiver logic
β βββ requirements.txt # Python dependencies
β βββ host.json # Function configuration
βββ π github-runner-controller-function/ # Queue trigger function
β βββ function_app.py # Container provisioning logic
β βββ requirements.txt # Python dependencies
β βββ host.json # Function configuration
βββ π github-runner-cleanup-function/ # Timer trigger function
β βββ function_app.py # Cleanup logic
β βββ requirements.txt # Python dependencies
β βββ host.json # Function configuration
βββ π github-runner-images/ # Docker container definitions
β βββ docker-compose.yml # Multi-stage build configuration
β βββ context/
β β βββ Dockerfile.base # Base Ubuntu image with tools
β β βββ Dockerfile.runner # GitHub Actions runner image
β β βββ script/
β β βββ app.sh # Runner registration script
β β βββ generate_jwt.py # GitHub App JWT generation
βββ π terraform_modules/ # Reusable Terraform modules
β βββ github_manage_org/ # GitHub organization management
βββ π design_diagram.png # Architecture diagram
The solution uses environment-specific configuration through Azure Function App Settings:
QUEUE_NAME
: Azure Storage Queue name for job bufferingstorageAccountConnectionString
: Connection string for Azure Storage
AZURE_CONTAINER_REGISTRY
: ACR name for pulling runner imagesAZURE_KV_NAME
: Key Vault name for credential retrievalGH_ORG_NAME
: GitHub organization nameAZURE_SUBSCRIPTION_ID
: Target subscription for ACI deploymentAZURE_RESOURCE_GROUP
: Resource group for ACI instances
AZURE_SUBSCRIPTION_ID
: Subscription for container monitoringAZURE_RESOURCE_GROUP
: Resource group to monitor
Customize deployment through dev.tfvars
:
project = "myproject" # Project name prefix
env = "dev" # Environment identifier
location = "eastus2" # Azure region
acr_sku = "Basic" # Container Registry SKU
kv_sku_name = "standard" # Key Vault SKU
GITHUB_ORG_NAME = "my-org" # GitHub organization
All functions are integrated with Azure Application Insights for comprehensive monitoring:
- π Performance Metrics: Function execution times, success rates
- π Error Tracking: Detailed exception logging and stack traces
- π Custom Metrics: Container creation counts, queue depths
- π Distributed Tracing: End-to-end request tracking
# Check controller function logs
az logs show --resource-group myproject-dev-rg \
--resource myproject-dev-controller-function-app
# Verify Key Vault permissions
az role assignment list --assignee <function-principal-id> \
--scope /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<kv-name>
# Test receiver function directly
curl -X POST https://myproject-dev-receiver-function-app.azurewebsites.net/api/receiver_function \
-H "Content-Type: application/json" \
-d '{"action":"queued","workflow_job":{"id":"test-123","labels":["self-hosted","linux"]}}'
- Verify GitHub App permissions and installation
- Check organization webhook configuration
- Validate GitHub App credentials in Key Vault
Monitor system health through Azure Portal or CLI:
# Function app status
az functionapp show --name myproject-dev-controller-function-app \
--resource-group myproject-dev-rg --query "state"
# Queue depth monitoring
az storage queue show --name myproject-dev-queue \
--account-name myprojectdev --query "metadata"
# Container instance status
az container list --resource-group myproject-dev-rg \
--query "[].{Name:name,State:instanceView.state}" --output table
- β Managed Identity: Functions use system-assigned managed identities
- β RBAC: Least-privilege access with Azure role assignments
- β GitHub App: Secure authentication instead of personal access tokens
- β Key Vault: Encrypted storage for sensitive credentials
- π HTTPS Only: All functions enforce HTTPS communication
- π Function Keys: HTTP triggers protected with function-level keys
- π Private Endpoints: Configure for production environments
# Rotate GitHub App private key
az keyvault secret set --vault-name myproject-dev-kv \
--name myproject-dev-kv-gh-pemfile \
--value @new-private-key.pem
# Update ACR credentials
az keyvault secret set --vault-name myproject-dev-kv \
--name myproject-dev-kv-acr-pass \
--value $(az acr credential show --name myprojectdevacr --query "passwords[0].value" -o tsv)
The project includes comprehensive GitHub Actions workflows for automated deployment:
graph TD
A[create_azure_remote_backend.yml] --> B[deploy_azure_infra.yml]
B --> C[deploy_receiver_function.yml]
B --> D[build_deploy_controller_function.yml]
B --> E[build_deploy_images.yml]
C --> F[Configure GitHub Webhook]
D --> F
E --> F
For development or troubleshooting:
# Deploy individual components
cd github-runner-controller-function
zip -r ../controller-function.zip . -x "*.git*" "*/__pycache__/*" "*.pyc"
az functionapp deployment source config-zip \
--resource-group myproject-dev-rg \
--name myproject-dev-controller-function-app \
--src controller-function.zip
- Update Dependencies: Regularly update Azure Function Python packages
- Rotate Secrets: Implement automatic GitHub App credential rotation
- Monitor Costs: Set up Azure Cost Management alerts
- Update Runner Images: Keep container images updated with latest tools
# Manual cleanup of old containers
az container list --resource-group myproject-dev-rg \
--query "[?instanceView.state=='Terminated'].name" -o tsv | \
xargs -I {} az container delete --name {} --resource-group myproject-dev-rg --yes
# Remove old container images
az acr repository list --name myprojectdevacr --output tsv | \
xargs -I {} az acr repository delete --name myprojectdevacr --repository {} --yes
We welcome contributions! Please see our Contributing Guidelines for details.
# Setup local development environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -r github-runner-controller-function/requirements.txt
# Install development tools
pip install pytest black flake8 mypy
# Run unit tests
pytest tests/
# Code formatting
black github-runner-*-function/
# Type checking
mypy github-runner-*-function/
- Azure Functions Python Developer Guide
- GitHub Apps Documentation
- Azure Container Instances Documentation
- Terraform Azure Provider
This project is licensed under the MIT License - see the LICENSE file for details.
For questions, issues, or contributions:
- π Issues: GitHub Issues
- π¬ Discussions: GitHub Discussions
- π§ Email: Open an issue for private inquiries
Built with β€οΈ for the DevOps community