To securely deploy to a private Azure Kubernetes Service (AKS) cluster from CI/CD pipelines, public cloud-hosted runners (such as default GitHub-hosted runners or Azure DevOps Microsoft-hosted agents) cannot be used directly because they lack access to the cluster’s Private Endpoint.
Instead, you must deploy self-hosted agents or runners inside a Virtual Network (VNet) that can resolve and reach the private AKS API server.
Deployment Architecture
Self-hosted runners poll GitHub or Azure DevOps over outbound HTTPS (port 443), eliminating the need for inbound open firewall ports. Because the agents live inside the Azure VNet, they resolve the private API server DNS via the Private DNS Zone link (privatelink.<region>.azmk8s.io).
Pattern 1: Azure Container Instances (ACI) Self-Hosted Agents
Deploying runners inside Azure Container Instances (ACI) injected into a delegated subnet in your spoke VNet is a lightweight, serverless approach for managing CI/CD infrastructure.
1. Terraform for VNet-Integrated ACI Agent
This configuration deploys an Azure DevOps pipeline agent into a VNet subnet with access to the private AKS DNS:
Terraform
# User-Assigned Managed Identity for the Agentresource "azurerm_user_assigned_identity" "agent_identity" { name = "id-ado-runner-prod" location = var.location resource_group_name = var.resource_group_name}# Grant RBAC on AKS Cluster to the Agent Identityresource "azurerm_role_assignment" "agent_aks_rbac" { scope = var.aks_cluster_id role_definition_name = "Azure Kubernetes Service Cluster User" principal_id = azurerm_user_assigned_identity.agent_identity.principal_id}# Container Group running in Delegated Subnetresource "azurerm_container_group" "ado_agent" { name = "aci-ado-agent-pool" location = var.location resource_group_name = var.resource_group_name os_type = "Linux" subnet_ids = [var.agent_subnet_id] ip_address_type = "Private" identity { type = "UserAssigned" identity_ids = [azurerm_user_assigned_identity.agent_identity.id] } container { name = "azdevops-agent" image = "mcr.microsoft.com/azure-pipelines/vso-agent:latest" cpu = "2.0" memory = "4.0" environment_variables = { AZP_URL = "https://dev.azure.com/YourOrganization" AZP_POOL = "Private-AKS-Pool" AZP_AGENT_NAME = "aci-agent-01" } secure_environment_variables = { AZP_TOKEN = var.azure_devops_pat # Personal Access Token with Agent Pools (Read & Manage) scope } }}
Pattern 2: Actions Runner Controller (ARC) inside AKS
If you are using GitHub Actions, the enterprise best practice is to run Actions Runner Controller (ARC). ARC runs directly inside Kubernetes (either inside a dedicated management AKS cluster or a separate namespace in your non-prod cluster) and dynamically scales ephemeral runner pods up and down based on workflow demand.
GitHub.com ──(Long-Polling)──► ARC Listener Pod ──► Scales Runner Pods inside VNet ──► Internal Private AKS API
Steps to Deploy ARC for GitHub Actions
1.Install ARC via Helm:Step 1.
Install the Actions Runner Controller operator using Helm on a cluster residing within the target VNet:
helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controllerhelm repo update# Install the ARC Controllerhelm upgrade --install arc \ --namespace arc-systems \ --create-namespace \ actions-runner-controller/gha-runner-scale-set-controller
2.Configure GitHub Authentication:Step 2.
Create a Kubernetes secret containing your GitHub Personal Access Token (PAT) or GitHub App credentials:
kubectl create namespace arc-runnerskubectl create secret generic github-app-secret \ --namespace arc-runners \ --from-literal=github_app_id="123456" \ --from-literal=github_app_installation_id="789012" \ --from-file=github_app_private_key=./github-app.private-key.pem
3.Deploy Ephemeral Auto-Scaling Runner Scale Set:Step 3.
Deploy an autoscaling runner scale set that targets your GitHub repository or organization:
Bash
helm upgrade --install private-aks-runners \ --namespace arc-runners \ actions-runner-controller/gha-runner-scale-set \ --set githubConfigUrl="https://github.com/YourOrg/YourRepo" \ --set githubConfigSecret="github-app-secret" \ --set minRunners=1 \ --set maxRunners=10
Workflow Example: Passwordless Authentication via OIDC & Entra Workload Identity
Avoid embedding static credentials or service principal keys in your pipelines. Use OpenID Connect (OIDC) so the agent exchanges its identity token for an Azure Entra access token dynamically.
GitHub Actions Workflow (.github/workflows/deploy.yml)
name: Deploy to Private AKSon: push: branches: [ main ]jobs: deploy: runs-on: self-hosted # Directs job to your private self-hosted ARC runner permissions: id-token: write # Required for OIDC Entra authentication contents: read steps: - name: Checkout Code uses: actions/checkout@v4 - name: Log in to Azure via OIDC Federation uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Set AKS Cluster Context uses: azure/aks-set-context@v4 with: cluster-name: 'aks-corp-prod-eastus-001' resource-group: 'rg-aks-prod-001' use-kubelogin: 'true' - name: Deploy Workload to Private Cluster run: | kubectl apply -f k8s/deployment.yaml kubectl rollout status deployment/myapp-deployment
Comparison: ACI vs. ARC (K8s-Native)
| Feature | Azure Container Instances (ACI) | Actions Runner Controller (ARC) |
| Best Used For | Azure DevOps Pipelines, lightweight setups | GitHub Actions, enterprise-scale auto-scaling |
| Maintenance | Minimal (Serverless container runtime) | Managed via Helm charts on Kubernetes |
| Scaling Velocity | Moderate (Starts new ACI instance per job) | High (Pre-warmed pods start in seconds) |
| Network Footprint | Direct VNet Integration via Delegated Subnet | Runs natively inside the VNet cluster |


