Skip to content

Commit d2a0f19

Browse files
committed
feat: ado agent pool building block
1 parent b64cef8 commit d2a0f19

File tree

14 files changed

+1119
-0
lines changed

14 files changed

+1119
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Azure DevOps Agent Pool Backplane
2+
3+
This backplane provides the necessary Azure infrastructure and permissions for managing Azure DevOps agent pools with elastic scaling.
4+
5+
## Components
6+
7+
- **Service Principal**: Azure AD application for Azure DevOps management
8+
- **Key Vault**: Secure storage for Azure DevOps Personal Access Token (PAT)
9+
- **Custom Role**: Role definition with permissions to read VMSS and Key Vault secrets
10+
- **Role Assignment**: Assigns the custom role to the service principal
11+
12+
## Prerequisites
13+
14+
Before deploying this backplane:
15+
16+
1. Azure subscription with permissions to create:
17+
- Service principals
18+
- Key Vaults
19+
- Custom role definitions
20+
- Role assignments
21+
22+
2. Azure DevOps Personal Access Token with scopes:
23+
- Agent Pools (Read & Manage)
24+
- Project & Team (Read, optional for project authorization)
25+
26+
## Usage
27+
28+
```hcl
29+
module "agent_pool_backplane" {
30+
source = "path/to/azuredevops/agent-pool/backplane"
31+
32+
service_principal_name = "sp-azure-devops-agent-pool"
33+
key_vault_name = "kv-azdevops-terraform"
34+
resource_group_name = "rg-azdevops-terraform"
35+
location = "westeurope"
36+
scope = "/subscriptions/12345678-1234-1234-1234-123456789012"
37+
}
38+
```
39+
40+
## Manual Steps After Deployment
41+
42+
After the backplane is deployed, you must manually:
43+
44+
1. **Store the PAT in Key Vault**:
45+
```bash
46+
az keyvault secret set \
47+
--vault-name kv-azdevops-terraform \
48+
--name azure-devops-pat \
49+
--value "YOUR_PAT_TOKEN_HERE"
50+
```
51+
52+
2. **Create Service Principal Secret** (if using client credentials):
53+
```bash
54+
az ad app credential reset \
55+
--id <service-principal-client-id> \
56+
--append
57+
```
58+
59+
## Permissions Granted
60+
61+
The custom role grants the service principal:
62+
63+
- `Microsoft.KeyVault/vaults/secrets/read` - Read PAT from Key Vault
64+
- `Microsoft.Resources/subscriptions/resourceGroups/read` - List resource groups
65+
- `Microsoft.Compute/virtualMachineScaleSets/read` - Read VMSS information
66+
67+
## Security Considerations
68+
69+
- **PAT Rotation**: Rotate the PAT every 90 days minimum
70+
- **Least Privilege**: Service principal only has read access to VMSS
71+
- **Key Vault Access**: Limited to specific service principal and admin
72+
- **Scope**: Apply role at subscription or resource group level
73+
74+
## Outputs
75+
76+
- `service_principal_id`: Client ID for authentication
77+
- `service_principal_object_id`: Object ID for role assignments
78+
- `key_vault_name`: Key Vault name for PAT storage
79+
- `resource_group_name`: Resource group containing Key Vault
80+
- `role_definition_id`: Custom role definition ID
81+
82+
## Troubleshooting
83+
84+
### Service Principal Creation Failed
85+
86+
**Cause**: Insufficient Azure AD permissions
87+
88+
**Solution**: Ensure you have Application Administrator or Global Administrator role in Azure AD
89+
90+
### Key Vault Access Denied
91+
92+
**Cause**: Service principal lacks access policy
93+
94+
**Solution**: Verify access policy in Key Vault grants Get and List permissions on secrets
95+
96+
### Role Assignment Failed
97+
98+
**Cause**: Insufficient permissions at target scope
99+
100+
**Solution**: Ensure you have Owner or User Access Administrator role at the specified scope
101+
102+
<!-- BEGIN_TF_DOCS -->
103+
## Requirements
104+
105+
| Name | Version |
106+
|------|---------|
107+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
108+
| <a name="requirement_azuread"></a> [azuread](#requirement\_azuread) | ~> 2.0 |
109+
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | ~> 4.51.0 |
110+
111+
## Modules
112+
113+
No modules.
114+
115+
## Resources
116+
117+
| Name | Type |
118+
|------|------|
119+
| [azuread_application.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource |
120+
| [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource |
121+
| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource |
122+
| [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource |
123+
| [azurerm_role_assignment.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
124+
| [azurerm_role_definition.azure_devops_agent_pool_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource |
125+
| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source |
126+
| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source |
127+
128+
## Inputs
129+
130+
| Name | Description | Type | Default | Required |
131+
|------|-------------|------|---------|:--------:|
132+
| <a name="input_key_vault_name"></a> [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store Azure DevOps PAT | `string` | n/a | yes |
133+
| <a name="input_location"></a> [location](#input\_location) | Azure region for resources | `string` | `"westeurope"` | no |
134+
| <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group for Key Vault | `string` | n/a | yes |
135+
| <a name="input_scope"></a> [scope](#input\_scope) | Scope for the custom role definition (e.g., subscription ID) | `string` | n/a | yes |
136+
| <a name="input_service_principal_name"></a> [service\_principal\_name](#input\_service\_principal\_name) | Name of the service principal for Azure DevOps agent pool management | `string` | `"sp-azure-devops-agent-pool"` | no |
137+
138+
## Outputs
139+
140+
| Name | Description |
141+
|------|-------------|
142+
| <a name="output_key_vault_id"></a> [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault |
143+
| <a name="output_key_vault_name"></a> [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault |
144+
| <a name="output_resource_group_name"></a> [resource\_group\_name](#output\_resource\_group\_name) | Name of the resource group |
145+
| <a name="output_role_definition_id"></a> [role\_definition\_id](#output\_role\_definition\_id) | ID of the custom role definition |
146+
| <a name="output_service_principal_id"></a> [service\_principal\_id](#output\_service\_principal\_id) | Application (client) ID of the service principal |
147+
| <a name="output_service_principal_object_id"></a> [service\_principal\_object\_id](#output\_service\_principal\_object\_id) | Object ID of the service principal |
148+
<!-- END_TF_DOCS -->
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
data "azurerm_client_config" "current" {}
2+
3+
data "azurerm_subscription" "current" {}
4+
5+
resource "azuread_application" "azure_devops" {
6+
display_name = var.service_principal_name
7+
description = "Service principal for managing Azure DevOps agent pools"
8+
}
9+
10+
resource "azuread_service_principal" "azure_devops" {
11+
client_id = azuread_application.azure_devops.client_id
12+
}
13+
14+
resource "azurerm_resource_group" "devops" {
15+
name = var.resource_group_name
16+
location = var.location
17+
}
18+
19+
resource "azurerm_key_vault" "devops" {
20+
name = var.key_vault_name
21+
location = azurerm_resource_group.devops.location
22+
resource_group_name = azurerm_resource_group.devops.name
23+
tenant_id = data.azurerm_client_config.current.tenant_id
24+
sku_name = "standard"
25+
26+
access_policy {
27+
tenant_id = data.azurerm_client_config.current.tenant_id
28+
object_id = data.azurerm_client_config.current.object_id
29+
30+
secret_permissions = [
31+
"Get",
32+
"List",
33+
"Set",
34+
"Delete",
35+
"Recover",
36+
"Backup",
37+
"Restore"
38+
]
39+
}
40+
41+
access_policy {
42+
tenant_id = data.azurerm_client_config.current.tenant_id
43+
object_id = azuread_service_principal.azure_devops.object_id
44+
45+
secret_permissions = [
46+
"Get",
47+
"List"
48+
]
49+
}
50+
}
51+
52+
resource "azurerm_role_definition" "azure_devops_agent_pool_manager" {
53+
name = "${var.service_principal_name}-agent-pool-manager"
54+
description = "Allows management of Azure DevOps agent pools and reading VMSS information"
55+
scope = var.scope
56+
57+
permissions {
58+
actions = [
59+
"Microsoft.KeyVault/vaults/secrets/read",
60+
"Microsoft.Resources/subscriptions/resourceGroups/read",
61+
"Microsoft.Compute/virtualMachineScaleSets/read"
62+
]
63+
}
64+
}
65+
66+
resource "azurerm_role_assignment" "azure_devops_manager" {
67+
scope = var.scope
68+
role_definition_id = azurerm_role_definition.azure_devops_agent_pool_manager.role_definition_resource_id
69+
principal_id = azuread_service_principal.azure_devops.object_id
70+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
output "service_principal_id" {
2+
description = "Application (client) ID of the service principal"
3+
value = azuread_application.azure_devops.client_id
4+
}
5+
6+
output "service_principal_object_id" {
7+
description = "Object ID of the service principal"
8+
value = azuread_service_principal.azure_devops.object_id
9+
}
10+
11+
output "key_vault_id" {
12+
description = "ID of the Key Vault"
13+
value = azurerm_key_vault.devops.id
14+
}
15+
16+
output "key_vault_name" {
17+
description = "Name of the Key Vault"
18+
value = azurerm_key_vault.devops.name
19+
}
20+
21+
output "resource_group_name" {
22+
description = "Name of the resource group"
23+
value = azurerm_resource_group.devops.name
24+
}
25+
26+
output "role_definition_id" {
27+
description = "ID of the custom role definition"
28+
value = azurerm_role_definition.azure_devops_agent_pool_manager.id
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
variable "service_principal_name" {
2+
description = "Name of the service principal for Azure DevOps agent pool management"
3+
type = string
4+
default = "sp-azure-devops-agent-pool"
5+
}
6+
7+
variable "key_vault_name" {
8+
description = "Name of the Key Vault to store Azure DevOps PAT"
9+
type = string
10+
11+
validation {
12+
condition = length(var.key_vault_name) >= 3 && length(var.key_vault_name) <= 24
13+
error_message = "Key Vault name must be between 3 and 24 characters."
14+
}
15+
}
16+
17+
variable "resource_group_name" {
18+
description = "Name of the resource group for Key Vault"
19+
type = string
20+
}
21+
22+
variable "location" {
23+
description = "Azure region for resources"
24+
type = string
25+
default = "westeurope"
26+
}
27+
28+
variable "scope" {
29+
description = "Scope for the custom role definition (e.g., subscription ID)"
30+
type = string
31+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
terraform {
2+
required_version = ">= 1.3.0"
3+
4+
required_providers {
5+
azurerm = {
6+
source = "hashicorp/azurerm"
7+
version = "~> 4.51.0"
8+
}
9+
azuread = {
10+
source = "hashicorp/azuread"
11+
version = "~> 2.0"
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)