
GitHub Hosted Private Runners on Azure: My Terraform Module
GitHub’s hosted runners are great until you need them to access private Azure resources and on premise resources. The typical solution involves opening firewall ports on select resources, adding IP allow lists, making resources completely public, which nobody likes doing.
GitHub Enterprise offers private networking for hosted runners. The runners deploy directly into your Azure VNet with their own network interface card. They can access everything in that network just like any other VM.
The Problem We’re Solving
When you use regular GitHub hosted runners, they’re out there on the public internet.
Want them to access your private database?
Better make it public or set resource specific firewall rules, not fun.
Need to hit a private function app?
More firewall rules or switching to public on resource firewalls.
The setup process isn’t exactly straightforward though. GitHub’s documentation exists but it is not very detailed, and there are some gotchas that can trip you up.
What You Actually Need
First things first, this ONLY works with GitHub Enterprise. Not the Team plan, not the free tier. Enterprise.
You’ll also need some Azure infrastructure ready to go:
- A resource group
- A solution VNet with a subnet dedicated to GitHub runners
- At least a /27 subnet (32 IPs)
- Proper subnet delegation to GitHub.Network/networkSettings
Your GitHub Enterprise database ID. It’s not something you can find in the UI. You have to use their GraphQL API to get it.
Getting the Database ID
You need to make a GraphQL query to GitHub’s API. Here’s the curl command that finally worked:
curl -H "Authorization: Bearer " -X POST \
-d '{ "query": "query($slug: String!) { enterprise (slug: $slug) { slug databaseId } }" ,
"variables": {
"slug": ""
}
}' \
https://api.github.com/graphql
Replace YOUR_PAT_HERE with a personal access token and YOUR_ORG_NAME with your organization slug. The response gives you the database ID you’ll need for the Terraform module.
The Terraform Module
I built a Terraform module to handle the Azure side of this setup. It does all the heavy lifting:
- Registers the GitHub.Network resource provider
- Creates the network settings resource
- Links everything together
- Outputs the ID you need for GitHub configuration
You can run this module two ways. Old school with terraform commands, or no-code style through Terraform Cloud. Just connect your repo, set your variables, and click apply.
The Service Association Link Issue
When this module creates the GitHub network settings, Azure creates something called a Service Association Link in the background. This link is like superglue. It sticks your subnet to the GitHub service and won’t let go.
Want to delete your subnet later? Too bad. The service association link blocks it. Want to remove it through the portal? You can’t. CLI commands? Nope. API calls? Access denied.
The only way to remove these is through Azure support tickets. Each time takes days. It’s an absolute pain. Azure knows about this problem, but for now, we’re stuck with it.
The lesson? Pick your subnet for your private runners carefully.
Getting the Network Settings ID
After your Terraform module deploys, you need to grab the Network Settings ID. This part’s not immediately obvious:
- Go to your Azure Resource Group in the portal where you deployed the module
- Click on the “Deployments” tab on the left
- Find your GitHub network settings deployment (it’ll be named whatever you set for network_settings_resource_name)
- Click on it, then click “Outputs”
- Copy the github_network_settings_id value
Note: You can also grab the network settings id value from the output of the Terraform module. I thought it might be important for you to know how to retrieve it from the Azure portal side too, hence my list above, lol.
Setting Up Private Runner Groups in GitHub
Now for the GitHub side:
- Go to your GitHub Enterprise settings
- Navigate to “Hosted compute networking”
- Click “New network configuration” and select Azure
- Paste in the Network Settings Resource ID you just copied
- Give it a descriptive name (you’ll likely have many of these)
- Create a runner group and associate it with your network configuration
- Add runners to the group (works with both Linux and Windows runner types)
Updating Your Workflows
Your workflows need to know about these new runners. In your workflow YAML files, you need to specify the runner group:
jobs:
deploy:
runs-on:
group: your-private-runner-group-name
labels: ubuntu-latest # or windows-latest
Every workflow that needs private network access has to be updated. No way around it. Make sure your teams know this or you’ll get a flood of “why isn’t my workflow running?” tickets.
Enterprise Rollout Strategy Tips and Suggestions
What works:
Naming Conventions: Be consistent. Use patterns like `gh-runners-[environment]-[team]-[region]`. When you have dozens of these configurations, naming matters.
Subnet Planning: Standardize your subnet sizes. /27 (32 IPs) for most teams, /26 (64 IPs) for larger ones. Document everything.
Terraform Workspaces: If using Terraform Cloud, create a cloud workspace per network configuration. Teams can manage their own deployments while using your base module.
Documentation:
- Which team owns each configuration
- Which repositories use which runners
- Which Azure resources each can access
- Who to contact when things break
Network Architecture Options
You basically have two choices for network design:
- Same VNet Strategy: Deploy runners into a subnet within the same VNet as your target resources.
This gives direct access with minimal latency. - Dedicated VNet Strategy: Create a separate VNet specifically for GitHub runners, then use VNet
peering to connect to your application VNets. This provides better isolation and centralized runner
management across multiple environments.
Why This Matters
This seems like a lot of work just to run some CI/CD jobs. But security breaches happen when we take shortcuts. Every firewall rule you don’t have to create is one less attack vector. Every public IP you don’t expose is one less target.
Plus, once this is set up, it just works. Your developers can run workflows against private resources without thinking about it. No VPN connections, no firewall requests, no waiting for security approvals. The runners are just there, in your network, ready to go.
Both Linux and Windows runners work with this setup. I’ve tested both. Pick whatever your workflows need.
The Gotchas
Beyond the service association link issue, watch out for:
Database ID: You can grab this with the GraphQL query above.
Subnet Delegation: Has to be done before deploying the module. Be sure to add a delegation for GitHub before beginning module deployment. That is done outside of this module’s scope.
Region Limitations: GitHub doesn’t support all Azure regions. Check their docs (linked below) for the current list.
Workflow Updates: Every single workflow file needs updating that has a need for private Azure network access. Communicate this to your teams.
What’s Next
This setup provides private “on VNet” CI/CD in Azure via GitHub Workflows. Your runners can access private resources without compromising security. No firewall rules, no public endpoints, just secure, private networking.
The module is available on GitHub and in the Terraform Cloud public registry if you want to use it. Feel free to fork it, adapt it to your needs, or just use it as a reference for your own implementation.
If you run into issues, especially with the service association link, prepare for an Azure support ticket. It’s annoying.
Thanks for sticking through this whole thing. Now go secure your runners before your security team finds out they’re running on the public internet.
-Rudy
Relevant Links:
https://github.com/RCFromCLE/terraform-azurerm-github-private-hosted-runner
https://registry.terraform.io/modules/RCFromCLE/github-private-hosted-runner/azurerm/latest
https://docs.github.com/en/enterprise-cloud@latest/admin/configuring-settings/configuring-private-networking-for-hosted-compute-products/about-azure-private-networking-for-github-hosted-runners-in-your-enterprise