diff --git a/.gitignore b/.gitignore index 64221de..1970311 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ yarn-error.log* .angular/ .terraform -.terraform.lock.hcl \ No newline at end of file +.terraform.lock.hcl +.env \ No newline at end of file diff --git a/modules/AGENTS.md b/modules/AGENTS.md index ddb37cf..7876a75 100644 --- a/modules/AGENTS.md +++ b/modules/AGENTS.md @@ -132,6 +132,43 @@ aws/ - **Negative scenarios:** Invalid inputs that should fail gracefully - **Naming collision tests:** Prevent resource conflicts - **Cross-provider consistency:** Similar test patterns across clouds +- **Test Users:** Use the following test users: + - **User Tom:** + ```json + { + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] + } + ``` + - **User Daniela:** + ```json + { + meshIdentifier = "likvid-daniela-user" + username = "likvid-daniela@meshcloud.io" + firstName = "Daniela" + lastName = "Livkid" + email = "likvid-daniela@meshcloud.io" + euid = "likvid-daniela@meshcloud.io" + roles = ["user", "Workspace Manager"] + } + ``` + - **User Anna:** + ```json + { + meshIdentifier = "likvid-anna-user" + username = "likvid-anna@meshcloud.io" + firstName = "Anna" + lastName = "Livkid" + email = "likvid-anna@meshcloud.io" + euid = "likvid-anna@meshcloud.io" + roles = ["reader", "Workspace Member"] + } + ``` **Example Test Structure:** ```hcl diff --git a/modules/azure/service-principal/buildingblock/APP_TEAM_README.md b/modules/azure/service-principal/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..671c959 --- /dev/null +++ b/modules/azure/service-principal/buildingblock/APP_TEAM_README.md @@ -0,0 +1,509 @@ +# Azure Service Principal + +This building block creates a service principal in Azure Entra ID (formerly Azure Active Directory) with role-based access to your Azure subscription. Service principals are used for automated authentication and authorization in CI/CD pipelines, applications, and automation scripts. + +## 🚀 Usage Examples + +- A development team creates a service principal to **automate deployments** from their CI/CD pipelines to Azure resources. +- A DevOps engineer sets up separate service principals for **development, staging, and production** environments with appropriate permissions. +- A team configures a read-only service principal for **monitoring and compliance tools** that need to scan infrastructure without making changes. +- A team uses **workload identity federation (OIDC)** with GitHub Actions or Azure DevOps to authenticate without managing secrets. + +## 🔄 Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Create service principal | ✅ | ❌ | +| Assign Azure roles to service principal | ✅ | ❌ | +| Choose authentication method (secret vs OIDC) | ⚠️ | ⚠️ | +| Provide service principal credentials (if using secrets) | ✅ | ❌ | +| Configure federated identity (if using OIDC) | ✅ | ❌ | +| Store client secret securely (if using secrets) | ❌ | ✅ | +| Use service principal in pipelines/applications | ❌ | ✅ | +| Monitor secret expiration (if using secrets) | ❌ | ✅ | +| Request secret rotation before expiration (if using secrets) | ❌ | ✅ | +| Use least privilege roles | ⚠️ | ✅ | +| Review and audit service principal usage | ✅ | ✅ | +| Request removal of unused service principals | ❌ | ✅ | + +## 💡 Best Practices + +### Service Principal Naming + +**Why**: Clear names help identify purpose and ownership. + +**Recommended Patterns**: +- Include application/service name: `myapp-production-sp` +- Include purpose: `myapp-cicd-sp`, `myapp-monitoring-sp` +- Include environment: `myapp-dev-sp`, `myapp-prod-sp` + +**Examples**: +- ✅ `ecommerce-prod-deployment-sp` +- ✅ `analytics-monitoring-sp` +- ✅ `backup-automation-sp` +- ❌ `sp1` +- ❌ `test-service-principal` + +### Role Selection + +**Why**: Follow least privilege principle to minimize security risks. + +**When to Use Each Role**: + +**Reader**: +- Monitoring and reporting tools +- Compliance scanning +- Read-only dashboards +- Cost analysis tools + +**Contributor** (Recommended): +- CI/CD pipelines deploying resources +- Application deployments +- Infrastructure as Code +- Standard automation tasks +- **Cannot** assign roles to other principals + +**Owner** (Use Very Sparingly): +- Infrastructure as Code managing RBAC +- Creating additional service principals +- Full subscription management +- ⚠️ **Only use when absolutely necessary** +- ⚠️ **Requires strong justification** + +### Secret Rotation Strategy + +**Why**: Regular secret rotation reduces risk of credential compromise. + +**Recommended Rotation Periods**: +- **Production environments**: 90 days (default) +- **Development/test**: 180 days +- **Long-running automation**: 90-180 days +- **High-security applications**: 30-60 days + +**Important**: Plan secret rotation carefully to avoid service disruptions. + +### Secure Secret Storage + +**Never**: +- ❌ Commit secrets to version control +- ❌ Store secrets in plain text files +- ❌ Share secrets via email or chat +- ❌ Log secrets in application logs + +**Always**: +- ✅ Store in Azure Key Vault +- ✅ Use secret management systems (HashiCorp Vault, etc.) +- ✅ Use environment variables for runtime +- ✅ Rotate secrets before expiration +- ✅ Use separate service principals per environment + +## 🔐 Authentication Methods + +Service principals support two authentication methods. Choose based on your use case: + +### Method 1: Client Secret (Traditional) + +**How it works**: A password is generated and used for authentication. + +**Best for**: +- Legacy applications +- Environments without OIDC support +- Simple script-based automation + +**Limitations**: +- Secrets expire and require rotation +- Secrets must be stored securely +- Risk of secret exposure + +### Method 2: Workload Identity Federation (OIDC) - Recommended + +**How it works**: Uses OpenID Connect tokens from trusted identity providers (GitHub, Azure DevOps, etc.) without storing secrets. + +**Best for**: +- GitHub Actions workflows +- Azure DevOps pipelines +- GitLab CI/CD +- Modern cloud-native applications + +**Benefits**: +- ✅ No secrets to manage or rotate +- ✅ Automatic token rotation +- ✅ Reduced security risk +- ✅ Simplified credential management +- ✅ Audit trail tied to identity provider + +**Request from Platform Team**: "Please create a service principal with workload identity federation for GitHub Actions" (or your platform) + +## 📝 Receiving Service Principal Credentials + +### For Client Secret Authentication + +After the Platform Team creates your service principal, you'll receive: +- **Client ID** (Service Principal ID / Application ID) +- **Client Secret** (Password) +- **Tenant ID** (Azure AD Directory ID) +- **Subscription ID** (Target Azure subscription) + +**Store these values securely immediately** - the client secret cannot be retrieved again without rotation. + +### For Workload Identity Federation (OIDC) + +After the Platform Team configures your service principal, you'll receive: +- **Client ID** (Service Principal ID / Application ID) +- **Tenant ID** (Azure AD Directory ID) +- **Subscription ID** (Target Azure subscription) +- Federated credential configuration details (issuer, subject, audience) + +**No secrets to store** - authentication uses short-lived tokens from your CI/CD platform. + +## 🔐 Using Service Principal for Authentication + +### With Client Secret + +#### Azure CLI + +```bash +az login --service-principal \ + --username \ + --password \ + --tenant +``` + +#### Terraform + +```hcl +provider "azurerm" { + features {} + + client_id = var.service_principal_id + client_secret = var.client_secret + tenant_id = var.tenant_id + subscription_id = var.subscription_id +} +``` + +#### GitHub Actions (with secrets) + +```yaml +- name: Azure Login + uses: azure/login@v1 + with: + creds: | + { + "clientId": "${{ secrets.AZURE_CLIENT_ID }}", + "clientSecret": "${{ secrets.AZURE_CLIENT_SECRET }}", + "subscriptionId": "${{ secrets.AZURE_SUBSCRIPTION_ID }}", + "tenantId": "${{ secrets.AZURE_TENANT_ID }}" + } +``` + +### With Workload Identity Federation (OIDC) + +#### GitHub Actions (recommended) + +```yaml +name: Deploy to Azure + +on: + push: + branches: [main] + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Azure Login with OIDC + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Azure CLI commands + run: | + az account show + az group list +``` + +**Key differences**: +- ✅ No `client-secret` needed +- ✅ Must set `permissions: id-token: write` +- ✅ GitHub generates short-lived tokens automatically + +#### Azure DevOps Pipelines (recommended) + +```yaml +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: AzureCLI@2 + inputs: + azureSubscription: 'MyServiceConnection' # Uses OIDC + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az account show + az group list +``` + +**Service connection is configured by Platform Team with OIDC** - no secrets in Azure DevOps. + +#### Terraform with OIDC + +```hcl +provider "azurerm" { + features {} + + use_oidc = true + client_id = var.service_principal_id + tenant_id = var.tenant_id + subscription_id = var.subscription_id + + # OIDC token automatically provided by CI/CD platform +} +``` + +## 🔄 Secret Rotation Process + +**Note**: This section only applies to service principals using **client secret authentication**. Service principals using **workload identity federation (OIDC)** do not require secret rotation. + +When secrets approach expiration (you should receive alerts): + +1. **Request rotation from Platform Team**: + - Provide service principal name + - Indicate urgency (days until expiration) + - List affected services/pipelines + +2. **Receive new credentials** from Platform Team + +3. **Update credentials in all locations**: + - Azure Key Vault secrets + - CI/CD pipeline secrets + - Application configuration + - Environment variables + +4. **Test authentication** with new credentials + +5. **Verify all services** using the service principal are working + +6. **Confirm completion** with Platform Team + +**Consider migrating to OIDC** to eliminate secret rotation requirements. + +## ⚠️ Important Notes + +### Authentication Method Choice + +- **Prefer OIDC** for GitHub Actions, Azure DevOps, and modern CI/CD platforms +- **Use client secrets** only for legacy systems or when OIDC is not supported +- Discuss authentication method with Platform Team during request + +### Client Secret Authentication + +- **Save credentials immediately** - client secrets cannot be retrieved after initial provisioning +- Secret rotation must be requested from Platform Team before expiration +- Old secrets are automatically revoked after rotation + +### OIDC Authentication + +- No secrets to store or rotate +- Requires federated credential configuration by Platform Team +- Must include `permissions: id-token: write` in workflows (GitHub Actions) +- Subject claims must match your repository/project configuration + +### General + +- Service principal names should be descriptive and follow naming conventions +- Role assignments are at subscription scope +- Common roles: Owner, Contributor, Reader (request appropriate level) +- Always use separate service principals per environment (dev, staging, prod) + +## 🆘 Troubleshooting + +### Secret expired (Client Secret Authentication) + +**Cause**: Secret has passed expiration date + +**Solution**: +1. Contact Platform Team immediately to request emergency rotation +2. Provide list of affected services for impact assessment +3. Receive new credentials from Platform Team +4. Update all services using the credential as quickly as possible + +**Prevention**: Migrate to workload identity federation (OIDC) to eliminate secret expiration. + +### Service principal authentication fails + +**Cause**: Multiple possible causes + +**Solution**: + +**For Client Secret Authentication**: +1. Verify client ID, tenant ID, and secret are correct +2. Check if secret has expired (contact Platform Team) +3. Verify you're authenticating to the correct subscription +4. Ensure service principal has required role assignment +5. Contact Platform Team to verify service principal status + +**For OIDC Authentication**: +1. Verify `permissions: id-token: write` is set in workflow (GitHub Actions) +2. Confirm client ID and tenant ID are correct +3. Check federated credential configuration with Platform Team +4. Verify issuer and subject claims match your CI/CD platform +5. Ensure service principal has required role assignment +6. Review CI/CD platform logs for token generation errors + +### OIDC token validation fails + +**Cause**: Federated credential misconfiguration + +**Common Issues**: +- **GitHub**: Subject claim doesn't match repository/branch/environment +- **Azure DevOps**: Service connection not configured for workload identity federation +- **Issuer mismatch**: Federated credential issuer doesn't match token issuer + +**Solution**: +1. Contact Platform Team with error message +2. Verify workflow/pipeline configuration matches federated credential +3. Check subject claim format for your platform: + - GitHub: `repo:/:ref:refs/heads/` + - Azure DevOps: `sc:////` + +### Need to remove service principal + +**Cause**: Service principal no longer needed + +**Solution**: +1. Document all locations where credentials are used +2. Remove credentials from all pipelines and applications +3. Contact Platform Team to request service principal deletion +4. Confirm no services are affected after removal + +## 📊 Monitoring Secret Expiration + +**Note**: This section only applies to service principals using **client secret authentication**. Service principals using **workload identity federation (OIDC)** automatically rotate tokens and do not require monitoring. + +**Platform Team Responsibilities**: +- Monitor secret expiration dates +- Send alerts 30 days before expiration +- Provide rotation services + +**Your Responsibilities**: +- Respond to expiration alerts promptly +- Request rotation at least 2 weeks before expiration +- Track where credentials are used +- Update credentials in all locations after rotation +- Test services after rotation + +**Recommendation**: Maintain an inventory of where each service principal is used for quick rotation, or migrate to OIDC to eliminate this overhead. + +## 🔗 Common Integration Patterns + +### Pattern 1: GitHub Actions with OIDC (Recommended) + +**Request from Platform Team**: +1. Service principal with **workload identity federation** for GitHub +2. Contributor role on target subscription +3. Federated credential configured for your repository + +**Example Request**: "Please create a service principal with OIDC for `myorg/myrepo` repository with Contributor access" + +**Your Setup**: +1. Receive client ID, tenant ID, subscription ID +2. Add as GitHub repository secrets: + - `AZURE_CLIENT_ID` + - `AZURE_TENANT_ID` + - `AZURE_SUBSCRIPTION_ID` +3. Use in workflows (see OIDC examples above) + +**Benefits**: +- ✅ No secrets to manage +- ✅ Automatic token rotation +- ✅ GitHub's identity system provides audit trail + +### Pattern 2: Azure DevOps with OIDC (Recommended) + +**Request from Platform Team**: +1. Service principal with **workload identity federation** for Azure DevOps +2. Service connection configured with OIDC +3. Contributor role on target subscription + +**Example Request**: "Please create an Azure DevOps service connection with OIDC for `myproject` with Contributor access" + +**Your Setup**: +1. Platform Team configures service connection +2. Use service connection name in pipelines +3. No credentials to store in Azure DevOps + +**Benefits**: +- ✅ Seamless pipeline integration +- ✅ No secret management +- ✅ Azure DevOps manages token lifecycle + +### Pattern 3: Legacy CI/CD with Client Secrets + +**Request from Platform Team**: +1. Service principal with **client secret authentication** +2. Contributor role on target subscription +3. Store credentials in Key Vault (recommended) + +**Your Setup**: +1. Receive service principal credentials +2. Store in secret management system +3. Configure CI/CD platform with credentials +4. Monitor expiration and rotate before deadline + +**Use when**: +- Platform doesn't support OIDC +- Legacy automation scripts +- Temporary/prototype setups + +**Consider migrating to OIDC** when possible. + +### Pattern 4: Multi-Environment Setup + +**With OIDC (Recommended)**: +Request separate service principals per environment: +- **Development**: `myapp-dev-sp` with OIDC, Contributor role +- **Staging**: `myapp-staging-sp` with OIDC, Contributor role +- **Production**: `myapp-prod-sp` with OIDC, Contributor role + +**With Client Secrets (If OIDC Not Available)**: +- **Development**: `myapp-dev-sp` with Contributor role, 180-day rotation +- **Staging**: `myapp-staging-sp` with Contributor role, 90-day rotation +- **Production**: `myapp-prod-sp` with Contributor role, 60-day rotation + +**Benefits**: +- Isolated identities per environment +- Easier to revoke access to specific environments +- Better audit trail +- Different security policies per environment + +### Pattern 5: Read-Only Monitoring + +**Request from Platform Team**: +1. Service principal with Reader role +2. OIDC (if monitoring runs in CI/CD) or client secret (if external tool) + +**Use Cases**: +- Cost analysis dashboards +- Compliance scanning tools +- Infrastructure monitoring +- Security auditing tools + +## 📚 Related Documentation + +- [Entra ID Service Principals](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals) +- [Azure RBAC Roles](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles) +- [Best Practices for Service Principals](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal) +- [Credential Management](https://learn.microsoft.com/en-us/entra/identity-platform/howto-create-service-principal-portal#option-3-create-a-new-client-secret) diff --git a/modules/azure/service-principal/buildingblock/README.md b/modules/azure/service-principal/buildingblock/README.md new file mode 100644 index 0000000..21ff17a --- /dev/null +++ b/modules/azure/service-principal/buildingblock/README.md @@ -0,0 +1,260 @@ +--- +name: Azure Service Principal +supportedPlatforms: + - azure +description: Creates an Entra ID application registration and service principal with role assignment for automated access to Azure resources +category: security +--- + +# Azure Service Principal Building Block + +Creates and manages an Entra ID application registration, service principal, and role assignment for automated access to Azure subscriptions. + +This documentation is intended as a reference for cloud foundation or platform engineers using this module. + +## Prerequisites + +- Azure subscription with appropriate permissions +- Permissions to create Entra ID applications and service principals +- Permissions to assign roles at subscription scope + +## Features + +- Creates Entra ID application registration +- Creates service principal linked to the application +- Optional client secret generation with configurable expiration +- Supports workload identity federation (OIDC) when secrets are disabled +- Assigns Azure RBAC role at subscription scope +- Automatic secret rotation based on time interval (when enabled) +- Supports Owner, Contributor, and Reader roles + +## Usage + +### Basic Service Principal with Contributor Role + +```hcl +module "service_principal" { + source = "./buildingblock" + + display_name = "my-app-service-principal" + description = "Service principal for CI/CD pipeline" + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + azure_role = "Contributor" +} + +output "client_id" { + value = module.service_principal.service_principal_id +} + +output "client_secret" { + value = module.service_principal.client_secret + sensitive = true +} + +output "tenant_id" { + value = module.service_principal.tenant_id +} +``` + +### Service Principal with Reader Role + +```hcl +module "readonly_sp" { + source = "./buildingblock" + + display_name = "monitoring-service-principal" + description = "Read-only access for monitoring tools" + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + azure_role = "Reader" +} +``` + +### Service Principal with Custom Secret Rotation + +```hcl +module "long_lived_sp" { + source = "./buildingblock" + + display_name = "long-lived-service-principal" + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + azure_role = "Contributor" + secret_rotation_days = 180 +} +``` + +### Service Principal for Workload Identity Federation (No Secret) + +```hcl +module "oidc_sp" { + source = "./buildingblock" + + display_name = "workload-identity-service-principal" + description = "Service principal for OIDC/workload identity federation" + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + azure_role = "Contributor" + create_client_secret = false +} + +# Add federated identity credential separately +resource "azuread_application_federated_identity_credential" "github" { + application_id = module.oidc_sp.application_object_id + display_name = "github-actions-federated-credential" + audiences = ["api://AzureADTokenExchange"] + issuer = "https://token.actions.githubusercontent.com" + subject = "repo:myorg/myrepo:ref:refs/heads/main" +} +``` + +### Service Principal with Custom Owners + +```hcl +data "azuread_user" "admin" { + user_principal_name = "admin@example.com" +} + +module "managed_sp" { + source = "./buildingblock" + + display_name = "team-managed-service-principal" + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + azure_role = "Contributor" + owners = [data.azuread_user.admin.object_id] +} +``` + +## Role Options + +- **Owner**: Full access including role assignments (use sparingly) +- **Contributor**: Full management access except role assignments (recommended) +- **Reader**: Read-only access to resources + +## Authentication Methods + +### Client Secret (Default) + +When `create_client_secret = true` (default), the module creates a client secret with: +- Configurable expiration period (30-730 days) +- Automatic rotation based on time_rotating resource +- Default rotation period: 90 days + +**Note**: After secret rotation, you must retrieve the new secret from Terraform state or outputs. + +### Workload Identity Federation (OIDC) + +When `create_client_secret = false`, no client secret is created. Instead, configure federated identity credentials for: +- GitHub Actions +- Azure DevOps +- GitLab CI +- Other OIDC-compatible platforms + +**Benefits**: +- No secrets to manage or rotate +- Enhanced security with short-lived tokens +- Compliance-friendly authentication + +## Integration with Azure DevOps Service Connection (OIDC) + +```hcl +module "devops_service_principal" { + source = "./buildingblock" + + display_name = "azuredevops-deployment-sp" + description = "Service principal for Azure DevOps pipelines with OIDC" + azure_subscription_id = var.subscription_id + azure_role = "Contributor" + create_client_secret = false +} + +resource "azuread_application_federated_identity_credential" "azdo" { + application_id = module.devops_service_principal.application_object_id + display_name = "azuredevops-federated-credential" + audiences = ["api://AzureADTokenExchange"] + issuer = "https://vstoken.dev.azure.com/${var.azdo_org_id}" + subject = "sc://${var.azdo_org_name}/${var.azdo_project_name}/Azure-Production" +} + +module "azuredevops_service_connection" { + source = "../../azuredevops/service-connection-subscription/buildingblock" + + azure_devops_organization_url = var.org_url + key_vault_name = var.key_vault_name + resource_group_name = var.resource_group_name + + project_id = var.project_id + service_connection_name = "Azure-Production" + azure_subscription_id = var.subscription_id + service_principal_id = module.devops_service_principal.service_principal_id + azure_tenant_id = module.devops_service_principal.tenant_id +} +``` + +## Security Considerations + +- **Prefer workload identity federation** (set `create_client_secret = false`) when possible for enhanced security +- Store client secrets securely (Key Vault, secret management system) if using client secret authentication +- Use least privilege principle - prefer Reader or Contributor over Owner +- Monitor secret expiration dates (when using client secrets) +- Rotate secrets regularly (when using client secrets) +- Limit application owners to trusted administrators +- Review role assignments periodically + +## Limitations + +- Role assignment is at subscription scope only +- Only supports built-in Owner, Contributor, and Reader roles +- Changing display_name requires recreation of application +- Secret rotation requires Terraform apply to take effect + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | +| [time](#requirement\_time) | ~> 0.11.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuread_application.main](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_application_password.main](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_password) | resource | +| [azuread_service_principal.main](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azurerm_role_assignment.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [time_rotating.secret_rotation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_subscription.target](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_role](#input\_azure\_role) | Azure RBAC role to assign to the service principal on the subscription | `string` | `"Contributor"` | no | +| [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID where role assignments will be created | `string` | n/a | yes | +| [create\_client\_secret](#input\_create\_client\_secret) | Whether to create a client secret for the service principal (set to false for workload identity federation) | `bool` | `true` | no | +| [description](#input\_description) | Description for the Entra ID application | `string` | `"Service principal managed by Terraform"` | no | +| [display\_name](#input\_display\_name) | Display name for the Entra ID application and service principal | `string` | n/a | yes | +| [owners](#input\_owners) | List of object IDs to set as owners of the application (defaults to current user) | `list(string)` | `[]` | no | +| [secret\_rotation\_days](#input\_secret\_rotation\_days) | Number of days before the service principal secret expires (only used if create\_client\_secret is true) | `number` | `90` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [application\_id](#output\_application\_id) | Application (client) ID of the Entra ID application | +| [application\_object\_id](#output\_application\_object\_id) | Object ID of the Entra ID application | +| [authentication\_method](#output\_authentication\_method) | Authentication method for the service principal | +| [azure\_role](#output\_azure\_role) | Azure role assigned to the service principal | +| [client\_secret](#output\_client\_secret) | Client secret for the service principal (null if create\_client\_secret is false) | +| [secret\_expiration\_date](#output\_secret\_expiration\_date) | Date when the service principal secret will expire (null if create\_client\_secret is false) | +| [service\_principal\_id](#output\_service\_principal\_id) | Client ID of the service principal (same as application\_id) | +| [service\_principal\_object\_id](#output\_service\_principal\_object\_id) | Object ID of the service principal | +| [subscription\_id](#output\_subscription\_id) | Azure Subscription ID where role assignment was created | +| [tenant\_id](#output\_tenant\_id) | Entra ID Tenant ID | + diff --git a/modules/azure/service-principal/buildingblock/logo.png b/modules/azure/service-principal/buildingblock/logo.png new file mode 100644 index 0000000..3405fd3 Binary files /dev/null and b/modules/azure/service-principal/buildingblock/logo.png differ diff --git a/modules/azure/service-principal/buildingblock/logo.svg b/modules/azure/service-principal/buildingblock/logo.svg new file mode 100644 index 0000000..2e2ec3f --- /dev/null +++ b/modules/azure/service-principal/buildingblock/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/azure/service-principal/buildingblock/main.tf b/modules/azure/service-principal/buildingblock/main.tf new file mode 100644 index 0000000..3e388a4 --- /dev/null +++ b/modules/azure/service-principal/buildingblock/main.tf @@ -0,0 +1,39 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_subscription" "target" { + subscription_id = var.azure_subscription_id +} + +resource "azuread_application" "main" { + display_name = var.display_name + description = var.description + owners = length(var.owners) > 0 ? var.owners : [data.azurerm_client_config.current.object_id] +} + +resource "azuread_service_principal" "main" { + client_id = azuread_application.main.client_id + owners = length(var.owners) > 0 ? var.owners : [data.azurerm_client_config.current.object_id] +} + +resource "time_rotating" "secret_rotation" { + count = var.create_client_secret ? 1 : 0 + + rotation_days = var.secret_rotation_days +} + +resource "azuread_application_password" "main" { + count = var.create_client_secret ? 1 : 0 + + application_id = azuread_application.main.id + display_name = "Terraform-managed secret" + + rotate_when_changed = { + rotation = time_rotating.secret_rotation[0].id + } +} + +resource "azurerm_role_assignment" "main" { + scope = data.azurerm_subscription.target.id + role_definition_name = var.azure_role + principal_id = azuread_service_principal.main.object_id +} diff --git a/modules/azure/service-principal/buildingblock/outputs.tf b/modules/azure/service-principal/buildingblock/outputs.tf new file mode 100644 index 0000000..4eb83b6 --- /dev/null +++ b/modules/azure/service-principal/buildingblock/outputs.tf @@ -0,0 +1,50 @@ +output "application_id" { + description = "Application (client) ID of the Entra ID application" + value = azuread_application.main.client_id +} + +output "application_object_id" { + description = "Object ID of the Entra ID application" + value = azuread_application.main.object_id +} + +output "service_principal_id" { + description = "Client ID of the service principal (same as application_id)" + value = azuread_service_principal.main.client_id +} + +output "service_principal_object_id" { + description = "Object ID of the service principal" + value = azuread_service_principal.main.object_id +} + +output "client_secret" { + description = "Client secret for the service principal (null if create_client_secret is false)" + value = var.create_client_secret ? azuread_application_password.main[0].value : null + sensitive = true +} + +output "tenant_id" { + description = "Entra ID Tenant ID" + value = data.azurerm_client_config.current.tenant_id +} + +output "subscription_id" { + description = "Azure Subscription ID where role assignment was created" + value = data.azurerm_subscription.target.subscription_id +} + +output "azure_role" { + description = "Azure role assigned to the service principal" + value = var.azure_role +} + +output "secret_expiration_date" { + description = "Date when the service principal secret will expire (null if create_client_secret is false)" + value = var.create_client_secret ? azuread_application_password.main[0].end_date : null +} + +output "authentication_method" { + description = "Authentication method for the service principal" + value = var.create_client_secret ? "client_secret" : "workload_identity_federation" +} diff --git a/modules/azure/service-principal/buildingblock/provider.tf b/modules/azure/service-principal/buildingblock/provider.tf new file mode 100644 index 0000000..2f05966 --- /dev/null +++ b/modules/azure/service-principal/buildingblock/provider.tf @@ -0,0 +1,6 @@ +provider "azurerm" { + features {} + subscription_id = var.azure_subscription_id +} + +provider "azuread" {} diff --git a/modules/azure/service-principal/buildingblock/service-principal.tftest.hcl b/modules/azure/service-principal/buildingblock/service-principal.tftest.hcl new file mode 100644 index 0000000..16866b4 --- /dev/null +++ b/modules/azure/service-principal/buildingblock/service-principal.tftest.hcl @@ -0,0 +1,135 @@ +variables { + display_name = "test-service-principal" + azure_subscription_id = "f808fff2-adda-415a-9b77-2833c041aacf" +} + +run "valid_contributor_service_principal" { + variables { + display_name = "test-sp-contributor" + azure_role = "Contributor" + description = "Test service principal with Contributor role" + } + + assert { + condition = azuread_application.main.display_name == "test-sp-contributor" + error_message = "Application display name should match input" + } + + assert { + condition = azurerm_role_assignment.main.role_definition_name == "Contributor" + error_message = "Role assignment should be Contributor" + } +} + +run "valid_reader_service_principal" { + variables { + display_name = "test-sp-reader" + azure_role = "Reader" + } + + assert { + condition = azurerm_role_assignment.main.role_definition_name == "Reader" + error_message = "Role assignment should be Reader" + } +} + +run "valid_owner_service_principal" { + variables { + display_name = "test-sp-owner" + azure_role = "Owner" + } + + assert { + condition = azurerm_role_assignment.main.role_definition_name == "Owner" + error_message = "Role assignment should be Owner" + } +} + +run "invalid_role_validation" { + variables { + display_name = "test-sp-invalid" + azure_role = "CustomRole" + } + + expect_failures = [ + var.azure_role + ] +} + +run "custom_secret_rotation" { + variables { + display_name = "test-sp-rotation" + secret_rotation_days = 180 + create_client_secret = true + } + + assert { + condition = time_rotating.secret_rotation[0].rotation_days == 180 + error_message = "Secret rotation should be 180 days" + } +} + +run "invalid_secret_rotation_too_short" { + variables { + display_name = "test-sp-short-rotation" + secret_rotation_days = 15 + } + + expect_failures = [ + var.secret_rotation_days + ] +} + +run "invalid_secret_rotation_too_long" { + + variables { + display_name = "test-sp-long-rotation" + secret_rotation_days = 800 + } + + expect_failures = [ + var.secret_rotation_days + ] +} + +run "custom_description" { + + variables { + display_name = "test-sp-description" + description = "Custom service principal for CI/CD pipelines" + } + + assert { + condition = azuread_application.main.description == "Custom service principal for CI/CD pipelines" + error_message = "Application description should match input" + } +} + +run "service_principal_without_secret" { + + variables { + display_name = "test-sp-oidc" + create_client_secret = false + description = "Service principal for OIDC authentication" + } + + assert { + condition = azuread_application.main.display_name == "test-sp-oidc" + error_message = "Application display name should match input" + } + + assert { + condition = output.client_secret == null + error_message = "Client secret should be null when create_client_secret is false" + } + + assert { + condition = output.secret_expiration_date == null + error_message = "Secret expiration date should be null when create_client_secret is false" + } + + assert { + condition = output.authentication_method == "workload_identity_federation" + error_message = "Authentication method should be workload_identity_federation" + } +} diff --git a/modules/azure/service-principal/buildingblock/variables.tf b/modules/azure/service-principal/buildingblock/variables.tf new file mode 100644 index 0000000..cd0f19b --- /dev/null +++ b/modules/azure/service-principal/buildingblock/variables.tf @@ -0,0 +1,49 @@ +variable "display_name" { + description = "Display name for the Entra ID application and service principal" + type = string +} + +variable "description" { + description = "Description for the Entra ID application" + type = string + default = "Service principal managed by Terraform" +} + +variable "azure_subscription_id" { + description = "Azure Subscription ID where role assignments will be created" + type = string +} + +variable "azure_role" { + description = "Azure RBAC role to assign to the service principal on the subscription" + type = string + default = "Contributor" + + validation { + condition = contains(["Owner", "Contributor", "Reader"], var.azure_role) + error_message = "azure_role must be one of: Owner, Contributor, Reader" + } +} + +variable "create_client_secret" { + description = "Whether to create a client secret for the service principal (set to false for workload identity federation)" + type = bool + default = true +} + +variable "secret_rotation_days" { + description = "Number of days before the service principal secret expires (only used if create_client_secret is true)" + type = number + default = 90 + + validation { + condition = var.secret_rotation_days >= 30 && var.secret_rotation_days <= 730 + error_message = "secret_rotation_days must be between 30 and 730 days" + } +} + +variable "owners" { + description = "List of object IDs to set as owners of the application (defaults to current user)" + type = list(string) + default = [] +} diff --git a/modules/azure/service-principal/buildingblock/versions.tf b/modules/azure/service-principal/buildingblock/versions.tf new file mode 100644 index 0000000..9533cd3 --- /dev/null +++ b/modules/azure/service-principal/buildingblock/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.6.0" + } + time = { + source = "hashicorp/time" + version = "~> 0.11.1" + } + } +} diff --git a/modules/azuredevops/pipeline/backplane/README.md b/modules/azuredevops/pipeline/backplane/README.md new file mode 100644 index 0000000..5f16509 --- /dev/null +++ b/modules/azuredevops/pipeline/backplane/README.md @@ -0,0 +1,98 @@ +# Azure DevOps Pipeline Backplane + +This module provisions the infrastructure required to support the Azure DevOps Pipeline building block. + +## What It Provisions + +- **Azure AD Service Principal**: For pipeline management automation +- **Azure Key Vault**: Stores Azure DevOps Personal Access Token (PAT) +- **Custom Role Definition**: Minimal permissions for reading Key Vault secrets +- **Role Assignment**: Grants the service principal access to Key Vault + +## Prerequisites + +- Azure subscription with permissions to create: + - Azure AD applications and service principals + - Key Vault instances + - Custom role definitions and assignments +- Azure DevOps organization with Administrator access +- Azure DevOps PAT with `Build (Read & Execute)` scope + +## Usage + +```hcl +module "azuredevops_pipeline_backplane" { + source = "./backplane" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + service_principal_name = "azuredevops-pipeline-terraform" + key_vault_name = "kv-azdo-pipeline-prod" + resource_group_name = "rg-azdo-pipeline-prod" + location = "West Europe" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" +} +``` + +## Post-Deployment Steps + +1. Create an Azure DevOps PAT with `Build (Read & Execute)` scope +2. Store the PAT in the provisioned Key Vault: + ```bash + az keyvault secret set --vault-name --name azdo-pat --value + ``` + +## Security Considerations + +- Service principal has read-only access to Key Vault secrets +- PAT should be rotated regularly (recommended: every 90 days) +- Use separate backplane instances for different environments + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuread_application.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | +| [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_role_assignment.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store the Azure DevOps PAT | `string` | n/a | yes | +| [location](#input\_location) | Azure region for resources | `string` | `"West Europe"` | no | +| [resource\_group\_name](#input\_resource\_group\_name) | Resource group name for the Key Vault | `string` | n/a | yes | +| [scope](#input\_scope) | Azure scope for role definitions (subscription or management group) | `string` | n/a | yes | +| [service\_principal\_name](#input\_service\_principal\_name) | Name for the Azure DevOps service principal | `string` | `"azure-devops-terraform"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | +| [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT | +| [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT | +| [key\_vault\_uri](#output\_key\_vault\_uri) | URI of the Key Vault for storing Azure DevOps PAT | +| [resource\_group\_name](#output\_resource\_group\_name) | Name of the resource group containing the Key Vault | +| [service\_principal\_client\_id](#output\_service\_principal\_client\_id) | Client ID of the Azure DevOps service principal | +| [service\_principal\_object\_id](#output\_service\_principal\_object\_id) | Object ID of the Azure DevOps service principal | + \ No newline at end of file diff --git a/modules/azuredevops/pipeline/backplane/main.tf b/modules/azuredevops/pipeline/backplane/main.tf new file mode 100644 index 0000000..86fc5ba --- /dev/null +++ b/modules/azuredevops/pipeline/backplane/main.tf @@ -0,0 +1,69 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_subscription" "current" {} + +resource "azuread_application" "azure_devops" { + display_name = var.service_principal_name + description = "Service principal for managing Azure DevOps pipelines" +} + +resource "azuread_service_principal" "azure_devops" { + client_id = azuread_application.azure_devops.client_id +} + +resource "azurerm_resource_group" "devops" { + name = var.resource_group_name + location = var.location +} + +resource "azurerm_key_vault" "devops" { + name = var.key_vault_name + location = azurerm_resource_group.devops.location + resource_group_name = azurerm_resource_group.devops.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + secret_permissions = [ + "Get", + "List", + "Set", + "Delete", + "Recover", + "Backup", + "Restore" + ] + } + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azuread_service_principal.azure_devops.object_id + + secret_permissions = [ + "Get", + "List" + ] + } +} + +resource "azurerm_role_definition" "azure_devops_manager" { + name = "${var.service_principal_name}-manager" + description = "Allows management of Azure DevOps pipelines" + scope = var.scope + + permissions { + actions = [ + "Microsoft.KeyVault/vaults/secrets/read", + "Microsoft.Resources/subscriptions/resourceGroups/read" + ] + } +} + +resource "azurerm_role_assignment" "azure_devops_manager" { + scope = var.scope + role_definition_id = azurerm_role_definition.azure_devops_manager.role_definition_resource_id + principal_id = azuread_service_principal.azure_devops.object_id +} diff --git a/modules/azuredevops/pipeline/backplane/outputs.tf b/modules/azuredevops/pipeline/backplane/outputs.tf new file mode 100644 index 0000000..259f6bf --- /dev/null +++ b/modules/azuredevops/pipeline/backplane/outputs.tf @@ -0,0 +1,34 @@ +output "service_principal_client_id" { + description = "Client ID of the Azure DevOps service principal" + value = azuread_service_principal.azure_devops.client_id +} + +output "service_principal_object_id" { + description = "Object ID of the Azure DevOps service principal" + value = azuread_service_principal.azure_devops.object_id +} + +output "key_vault_id" { + description = "ID of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.id +} + +output "key_vault_name" { + description = "Name of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.name +} + +output "key_vault_uri" { + description = "URI of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.vault_uri +} + +output "resource_group_name" { + description = "Name of the resource group containing the Key Vault" + value = azurerm_resource_group.devops.name +} + +output "azure_devops_organization_url" { + description = "Azure DevOps organization URL" + value = var.azure_devops_organization_url +} diff --git a/modules/azuredevops/pipeline/backplane/variables.tf b/modules/azuredevops/pipeline/backplane/variables.tf new file mode 100644 index 0000000..40d6646 --- /dev/null +++ b/modules/azuredevops/pipeline/backplane/variables.tf @@ -0,0 +1,31 @@ +variable "azure_devops_organization_url" { + description = "Azure DevOps organization URL (e.g., https://dev.azure.com/myorg)" + type = string +} + +variable "service_principal_name" { + description = "Name for the Azure DevOps service principal" + type = string + default = "azure-devops-terraform" +} + +variable "key_vault_name" { + description = "Name of the Key Vault to store the Azure DevOps PAT" + type = string +} + +variable "resource_group_name" { + description = "Resource group name for the Key Vault" + type = string +} + +variable "location" { + description = "Azure region for resources" + type = string + default = "West Europe" +} + +variable "scope" { + description = "Azure scope for role definitions (subscription or management group)" + type = string +} diff --git a/modules/azuredevops/pipeline/backplane/versions.tf b/modules/azuredevops/pipeline/backplane/versions.tf new file mode 100644 index 0000000..1877588 --- /dev/null +++ b/modules/azuredevops/pipeline/backplane/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.6.0" + } + } +} diff --git a/modules/azuredevops/pipeline/buildingblock/APP_TEAM_README.md b/modules/azuredevops/pipeline/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..685e381 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/APP_TEAM_README.md @@ -0,0 +1,256 @@ +# Azure DevOps Pipeline + +This building block provides automated CI/CD pipelines in Azure DevOps to streamline your build, test, and deployment processes. Pipelines are configured via meshStack and run based on YAML definitions in your repository. + +## 🚀 Usage Examples + +- A development team sets up a pipeline to **automatically build and test** their application on every commit to the main branch. +- A DevOps engineer configures a multi-stage pipeline to **deploy to staging and production** environments with approval gates. +- A team creates separate pipelines for different environments (dev, staging, production) with environment-specific variables. + +## 🔄 Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Create Azure DevOps project | ✅ | ❌ | +| Create repository | ✅ | ❌ | +| Create pipeline configuration | ✅ | ❌ | +| Write YAML pipeline definition | ❌ | ✅ | +| Commit pipeline YAML to repository | ❌ | ✅ | +| Run and monitor pipelines | ❌ | ✅ | +| Manage pipeline variables | ⚠️ | ✅ | +| Troubleshoot pipeline failures | ❌ | ✅ | + +## 💡 Best Practices + +### Pipeline YAML Location + +**Why**: Consistent YAML file location makes pipelines predictable and easy to find. + +**Recommended Locations**: +- Root: `azure-pipelines.yml` (default) +- Dedicated folder: `ci/azure-pipelines.yml` +- Environment-specific: `pipelines/production.yml` + +**Examples**: +- ✅ `azure-pipelines.yml` +- ✅ `ci/build-and-test.yml` +- ✅ `pipelines/deploy-prod.yml` +- ❌ `random/location/pipe.yml` + +### Pipeline Naming + +**Why**: Clear names help identify purpose and environment at a glance. + +**Recommended Patterns**: +- Include app name: `myapp-ci-cd` +- Include environment: `myapp-production-deploy` +- Include purpose: `myapp-build-test` + +**Examples**: +- ✅ `customer-portal-ci` +- ✅ `payment-api-production` +- ✅ `frontend-build-test` +- ❌ `pipeline1` +- ❌ `new-pipeline` + +### Secret Management + +**Why**: Protect sensitive data and credentials from exposure. + +**Best Practices**: +- Use secret variables for sensitive data +- Link variable groups for shared secrets +- Never commit secrets to YAML files +- Rotate secrets regularly + +### Variable Groups + +**Why**: Share common variables across multiple pipelines efficiently. + +**When to Use Variable Groups**: +- Shared configuration (API endpoints, service URLs) +- Environment-specific settings +- Shared secrets (connection strings, credentials) + +### Branch Configuration + +**Default Branch Patterns**: +- Main branch: `refs/heads/main` (default) +- Development: `refs/heads/develop` +- Release: `refs/heads/release/*` + +## 📝 Creating Your Pipeline YAML + +Your repository must contain a YAML file at the specified path. Here's a starter template: + +### Basic Build Pipeline + +```yaml +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: NodeTool@0 + inputs: + versionSpec: '18.x' + displayName: 'Install Node.js' + + - script: npm install + displayName: 'Install dependencies' + + - script: npm run build + displayName: 'Build application' + + - script: npm test + displayName: 'Run tests' +``` + +### Pipeline with Variables + +```yaml +trigger: + - main + +variables: + - name: buildConfiguration + value: 'Release' + +pool: + vmImage: 'ubuntu-latest' + +steps: + - script: echo Building in $(buildConfiguration) mode + displayName: 'Show configuration' + + - script: dotnet build --configuration $(buildConfiguration) + displayName: 'Build project' +``` + +### Multi-Stage Pipeline + +```yaml +trigger: + - main + +stages: + - stage: Build + jobs: + - job: BuildJob + pool: + vmImage: 'ubuntu-latest' + steps: + - script: npm install + - script: npm run build + + - stage: Test + jobs: + - job: TestJob + pool: + vmImage: 'ubuntu-latest' + steps: + - script: npm test + + - stage: Deploy + jobs: + - job: DeployJob + pool: + vmImage: 'ubuntu-latest' + steps: + - script: echo Deploying application +``` + +## 🔍 Using Pipeline Variables + +Access configured variables in your YAML: + +```yaml +steps: + - script: echo Environment is $(environment) + displayName: 'Show environment' + + - script: echo Deploying to $(api_endpoint) + displayName: 'Deploy to endpoint' +``` + +For secret variables, map them explicitly: + +```yaml +steps: + - script: echo $(api_key) + env: + API_KEY: $(api_key) +``` + +## 🏃 Running Your Pipeline + +After pipeline creation: + +1. **Automatic Triggers**: Pipeline runs automatically on commits to configured branch +2. **Manual Runs**: Trigger from Azure DevOps web UI +3. **View Results**: Check Azure DevOps Pipelines section + +### Manual Pipeline Run + +1. Navigate to Azure DevOps project +2. Go to **Pipelines** +3. Select your pipeline +4. Click **Run pipeline** +5. (Optional) Override variables +6. Click **Run** + +## ⚠️ Important Notes + +- YAML file must exist in repository before pipeline is configured +- Pipeline triggers based on YAML configuration +- Variable group IDs must reference existing variable groups +- Secret variables are masked in logs but can be accessed in pipeline tasks + +## 🆘 Troubleshooting + +### "YAML file not found" error + +**Cause**: YAML file doesn't exist at specified path in repository + +**Solution**: Commit YAML file to repository first + +```bash +echo "trigger: [main]" > azure-pipelines.yml +git add azure-pipelines.yml +git commit -m "Add pipeline YAML" +git push +``` + +### Pipeline not triggering on commits + +**Cause**: Trigger configuration in YAML file + +**Solution**: Check YAML trigger configuration: + +```yaml +trigger: + - main +``` + +### Secret variables not working + +**Cause**: Secrets must be explicitly mapped in YAML + +**Solution**: Map secrets in your YAML: + +```yaml +steps: + - script: echo $(api_key) + env: + API_KEY: $(api_key) +``` + +## 📚 Related Documentation + +- [Azure Pipelines YAML Schema](https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/) +- [Pipeline Triggers](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/triggers) +- [Variable Groups](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups) +- [Pipeline Variables](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables) diff --git a/modules/azuredevops/pipeline/buildingblock/README.md b/modules/azuredevops/pipeline/buildingblock/README.md new file mode 100644 index 0000000..880a5e6 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/README.md @@ -0,0 +1,229 @@ +--- +name: Azure DevOps Pipeline +supportedPlatforms: + - azuredevops +description: Provides a CI/CD pipeline in Azure DevOps linked to a repository with YAML-based configuration +category: devops +--- + +# Azure DevOps Pipeline Building Block + +Creates and manages Azure DevOps pipelines (build definitions) linked to repositories with YAML-based pipeline definitions. + +## Prerequisites + +- Deployed Azure DevOps Pipeline backplane +- Azure DevOps project ID where the pipeline will be created +- Repository with a YAML pipeline definition file +- Azure DevOps PAT stored in Key Vault with `Build (Read & Execute)` scope + +## Features + +- Creates YAML-based CI/CD pipelines +- Supports multiple repository types (Azure Repos, GitHub, Bitbucket) +- Links variable groups to pipelines +- Supports pipeline-specific variables (including secrets) +- Configurable branch and YAML file path + +## Usage + +### Basic Pipeline + +```hcl +module "azuredevops_pipeline" { + source = "./buildingblock" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + key_vault_name = "kv-azdo-pipeline-prod" + resource_group_name = "rg-azdo-pipeline-prod" + pat_secret_name = "azdo-pat" + + project_id = "12345678-1234-1234-1234-123456789012" + pipeline_name = "my-app-ci-cd" + repository_id = "my-app-repo" + yaml_path = "azure-pipelines.yml" +} +``` + +### Pipeline with Variables + +```hcl +module "azuredevops_pipeline" { + source = "./buildingblock" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + key_vault_name = "kv-azdo-pipeline-prod" + resource_group_name = "rg-azdo-pipeline-prod" + + project_id = "12345678-1234-1234-1234-123456789012" + pipeline_name = "my-app-ci-cd" + repository_id = "my-app-repo" + yaml_path = "ci/azure-pipelines.yml" + + pipeline_variables = [ + { + name = "environment" + value = "production" + }, + { + name = "api_key" + value = "secret-value" + is_secret = true + } + ] + + variable_group_ids = [10, 20] +} +``` + +### GitHub Repository Pipeline + +```hcl +module "github_pipeline" { + source = "./buildingblock" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + key_vault_name = "kv-azdo-pipeline-prod" + resource_group_name = "rg-azdo-pipeline-prod" + + project_id = "12345678-1234-1234-1234-123456789012" + pipeline_name = "github-app-ci" + repository_type = "GitHub" + repository_id = "myorg/my-repo" + branch_name = "refs/heads/main" + yaml_path = ".azuredevops/pipeline.yml" +} +``` + +## Repository Types + +- **TfsGit** (default): Azure Repos Git repositories +- **GitHub**: GitHub repositories (requires service connection) +- **GitHubEnterprise**: GitHub Enterprise repositories +- **Bitbucket**: Bitbucket Cloud repositories + +## Pipeline Variables + +Variables can be defined with the following properties: + +- `name` (required): Variable name +- `value` (required): Variable value +- `is_secret` (optional): Mark as secret (default: false) +- `allow_override` (optional): Allow override at queue time (default: true) + +## Variable Groups + +Link existing variable groups to the pipeline using their IDs: + +```hcl +variable_group_ids = [10, 20, 30] +``` + +## Integration with Other Modules + +This building block works with repositories and projects: + +```hcl +module "azuredevops_project" { + source = "../project/buildingblock" + # ... project configuration +} + +module "app_repository" { + source = "../repository/buildingblock" + project_id = module.azuredevops_project.project_id + # ... repository configuration +} + +module "ci_pipeline" { + source = "./buildingblock" + project_id = module.azuredevops_project.project_id + repository_id = module.app_repository.repository_id + # ... pipeline configuration +} +``` + +## YAML Pipeline Requirements + +The pipeline expects a YAML file in the repository at the specified path. Example: + +```yaml +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - script: echo Hello, world! + displayName: 'Run a one-line script' + + - script: | + echo Add other tasks to build, test, and deploy your project. + displayName: 'Run a multi-line script' +``` + +## Security Considerations + +- Use secret variables for sensitive data +- Secret variables are masked in pipeline logs +- Link variable groups for shared secrets across pipelines +- PAT should have minimal required scopes (`Build (Read & Execute)`) +- Use service connections for external repository access + +## Limitations + +- Pipeline definition must be YAML-based (classic pipelines not supported) +- YAML file must exist in the repository before pipeline creation +- Repository must be accessible with the provided credentials + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuredevops_build_definition.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/build_definition) | resource | +| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source | +| [azurerm_key_vault_secret.azure_devops_pat](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault_secret) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | +| [branch\_name](#input\_branch\_name) | Default branch for the pipeline | `string` | `"refs/heads/main"` | no | +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault containing the Azure DevOps PAT | `string` | n/a | yes | +| [pat\_secret\_name](#input\_pat\_secret\_name) | Name of the secret in Key Vault that contains the Azure DevOps PAT | `string` | `"azdo-pat"` | no | +| [pipeline\_name](#input\_pipeline\_name) | Name of the pipeline to create | `string` | n/a | yes | +| [pipeline\_variables](#input\_pipeline\_variables) | List of pipeline variables to create |
list(object({
name = string
value = string
is_secret = optional(bool, false)
allow_override = optional(bool, true)
}))
| `[]` | no | +| [project\_id](#input\_project\_id) | Azure DevOps Project ID where the pipeline will be created | `string` | n/a | yes | +| [repository\_id](#input\_repository\_id) | Repository ID or name where the pipeline YAML file is located | `string` | n/a | yes | +| [repository\_type](#input\_repository\_type) | Type of repository. Options: TfsGit, GitHub, GitHubEnterprise, Bitbucket | `string` | `"TfsGit"` | no | +| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group containing the Key Vault | `string` | n/a | yes | +| [variable\_group\_ids](#input\_variable\_group\_ids) | List of variable group IDs to link to this pipeline | `list(number)` | `[]` | no | +| [yaml\_path](#input\_yaml\_path) | Path to the YAML pipeline definition file in the repository | `string` | `"azure-pipelines.yml"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [pipeline\_id](#output\_pipeline\_id) | ID of the created pipeline | +| [pipeline\_name](#output\_pipeline\_name) | Name of the created pipeline | +| [pipeline\_revision](#output\_pipeline\_revision) | Revision number of the pipeline | +| [pipeline\_url](#output\_pipeline\_url) | Deep link URL to the pipeline in Azure DevOps | +| [project\_id](#output\_project\_id) | Project ID where the pipeline was created | +| [repository\_id](#output\_repository\_id) | Repository ID linked to the pipeline | +| [yaml\_path](#output\_yaml\_path) | Path to the YAML pipeline definition | + \ No newline at end of file diff --git a/modules/azuredevops/pipeline/buildingblock/logo.png b/modules/azuredevops/pipeline/buildingblock/logo.png new file mode 100644 index 0000000..4dd083e Binary files /dev/null and b/modules/azuredevops/pipeline/buildingblock/logo.png differ diff --git a/modules/azuredevops/pipeline/buildingblock/logo.svg b/modules/azuredevops/pipeline/buildingblock/logo.svg new file mode 100644 index 0000000..3acc595 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/azuredevops/pipeline/buildingblock/main.tf b/modules/azuredevops/pipeline/buildingblock/main.tf new file mode 100644 index 0000000..efd30f9 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/main.tf @@ -0,0 +1,37 @@ +data "azurerm_key_vault" "devops" { + name = var.key_vault_name + resource_group_name = var.resource_group_name +} + +data "azurerm_key_vault_secret" "azure_devops_pat" { + name = var.pat_secret_name + key_vault_id = data.azurerm_key_vault.devops.id +} + +resource "azuredevops_build_definition" "main" { + project_id = var.project_id + name = var.pipeline_name + + ci_trigger { + use_yaml = true + } + + repository { + repo_type = var.repository_type + repo_id = var.repository_id + branch_name = var.branch_name + yml_path = var.yaml_path + } + + variable_groups = length(var.variable_group_ids) > 0 ? var.variable_group_ids : null + + dynamic "variable" { + for_each = var.pipeline_variables + content { + name = variable.value.name + value = variable.value.value + is_secret = lookup(variable.value, "is_secret", false) + allow_override = lookup(variable.value, "allow_override", true) + } + } +} diff --git a/modules/azuredevops/pipeline/buildingblock/outputs.tf b/modules/azuredevops/pipeline/buildingblock/outputs.tf new file mode 100644 index 0000000..925ed55 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/outputs.tf @@ -0,0 +1,34 @@ +output "pipeline_id" { + description = "ID of the created pipeline" + value = azuredevops_build_definition.main.id +} + +output "pipeline_name" { + description = "Name of the created pipeline" + value = azuredevops_build_definition.main.name +} + +output "pipeline_revision" { + description = "Revision number of the pipeline" + value = azuredevops_build_definition.main.revision +} + +output "project_id" { + description = "Project ID where the pipeline was created" + value = var.project_id +} + +output "repository_id" { + description = "Repository ID linked to the pipeline" + value = var.repository_id +} + +output "yaml_path" { + description = "Path to the YAML pipeline definition" + value = var.yaml_path +} + +output "pipeline_url" { + description = "Deep link URL to the pipeline in Azure DevOps" + value = "${var.azure_devops_organization_url}/${var.project_id}/_build?definitionId=${azuredevops_build_definition.main.id}" +} diff --git a/modules/azuredevops/pipeline/buildingblock/pipeline.tftest.hcl b/modules/azuredevops/pipeline/buildingblock/pipeline.tftest.hcl new file mode 100644 index 0000000..a86da75 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/pipeline.tftest.hcl @@ -0,0 +1,172 @@ +variables { + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" + pat_secret_name = "ado-pat" + project_id = "eece6ccc-c821-46a1-9214-80df6da9e13f" + repository_id = "e5612cf3-36f1-4db5-b9d4-6431704233f3" +} + +run "valid_pipeline_configuration" { + + variables { + pipeline_name = "test-pipeline" + } + + assert { + condition = azuredevops_build_definition.main.name == "test-pipeline" + error_message = "Pipeline name should match input variable" + } + + assert { + condition = azuredevops_build_definition.main.project_id == "eece6ccc-c821-46a1-9214-80df6da9e13f" + error_message = "Pipeline should be created in the specified project" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].repo_id == "e5612cf3-36f1-4db5-b9d4-6431704233f3" + error_message = "Pipeline should reference the correct repository" + } +} + +run "pipeline_with_custom_yaml_path" { + + variables { + pipeline_name = "custom-pipeline" + yaml_path = "ci/custom-pipeline.yml" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].yml_path == "ci/custom-pipeline.yml" + error_message = "Pipeline should use custom YAML path" + } +} + +run "pipeline_with_custom_branch" { + + variables { + pipeline_name = "develop-pipeline" + branch_name = "refs/heads/develop" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].branch_name == "refs/heads/develop" + error_message = "Pipeline should use custom branch" + } +} + +run "pipeline_with_variables" { + + variables { + pipeline_name = "var-pipeline" + + pipeline_variables = [ + { + name = "environment" + value = "production" + }, + { + name = "api_key" + value = "secret" + is_secret = true + } + ] + } + + assert { + condition = length(azuredevops_build_definition.main.variable) == 2 + error_message = "Pipeline should have 2 variables" + } +} + +run "pipeline_with_variable_groups" { + + variables { + pipeline_name = "vg-pipeline" + + variable_group_ids = [10, 20, 30] + } + + assert { + condition = length(azuredevops_build_definition.main.variable_groups) == 3 + error_message = "Pipeline should link 3 variable groups" + } +} + +run "github_repository_pipeline" { + + variables { + pipeline_name = "github-pipeline" + repository_type = "GitHub" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].repo_type == "GitHub" + error_message = "Pipeline should use GitHub repository type" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].repo_id == "e5612cf3-36f1-4db5-b9d4-6431704233f3" + error_message = "Pipeline should reference GitHub repository" + } +} + +run "tfsgit_repository_pipeline" { + + variables { + pipeline_name = "tfsgit-pipeline" + repository_type = "TfsGit" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].repo_type == "TfsGit" + error_message = "Pipeline should use TfsGit repository type" + } +} + +run "invalid_repository_type" { + + variables { + pipeline_name = "test-pipeline" + repository_type = "InvalidType" + } + + expect_failures = [ + var.repository_type + ] +} + +run "pipeline_with_default_values" { + + variables { + pipeline_name = "default-pipeline" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].repo_type == "TfsGit" + error_message = "Default repository type should be TfsGit" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].branch_name == "refs/heads/main" + error_message = "Default branch should be refs/heads/main" + } + + assert { + condition = azuredevops_build_definition.main.repository[0].yml_path == "azure-pipelines.yml" + error_message = "Default YAML path should be azure-pipelines.yml" + } +} + +run "pipeline_with_empty_variable_groups" { + + variables { + pipeline_name = "no-vg-pipeline" + variable_group_ids = [] + } + + assert { + condition = length(coalesce(azuredevops_build_definition.main.variable_groups, [])) == 0 + error_message = "Pipeline should have no variable groups" + } +} diff --git a/modules/azuredevops/pipeline/buildingblock/provider.tf b/modules/azuredevops/pipeline/buildingblock/provider.tf new file mode 100644 index 0000000..1413b11 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/provider.tf @@ -0,0 +1,8 @@ +provider "azuredevops" { + org_service_url = var.azure_devops_organization_url + personal_access_token = data.azurerm_key_vault_secret.azure_devops_pat.value +} + +provider "azurerm" { + features {} +} diff --git a/modules/azuredevops/pipeline/buildingblock/variables.tf b/modules/azuredevops/pipeline/buildingblock/variables.tf new file mode 100644 index 0000000..77b2c2f --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/variables.tf @@ -0,0 +1,75 @@ +variable "azure_devops_organization_url" { + description = "Azure DevOps organization URL (e.g., https://dev.azure.com/myorg)" + type = string +} + +variable "key_vault_name" { + description = "Name of the Key Vault containing the Azure DevOps PAT" + type = string +} + +variable "resource_group_name" { + description = "Name of the resource group containing the Key Vault" + type = string +} + +variable "pat_secret_name" { + description = "Name of the secret in Key Vault that contains the Azure DevOps PAT" + type = string + default = "azdo-pat" +} + +variable "project_id" { + description = "Azure DevOps Project ID where the pipeline will be created" + type = string +} + +variable "pipeline_name" { + description = "Name of the pipeline to create" + type = string +} + +variable "repository_type" { + description = "Type of repository. Options: TfsGit, GitHub, GitHubEnterprise, Bitbucket" + type = string + default = "TfsGit" + + validation { + condition = contains(["TfsGit", "GitHub", "GitHubEnterprise", "Bitbucket"], var.repository_type) + error_message = "repository_type must be one of: TfsGit, GitHub, GitHubEnterprise, Bitbucket" + } +} + +variable "repository_id" { + description = "Repository ID or name where the pipeline YAML file is located" + type = string +} + +variable "branch_name" { + description = "Default branch for the pipeline" + type = string + default = "refs/heads/main" +} + +variable "yaml_path" { + description = "Path to the YAML pipeline definition file in the repository" + type = string + default = "azure-pipelines.yml" +} + +variable "variable_group_ids" { + description = "List of variable group IDs to link to this pipeline" + type = list(number) + default = [] +} + +variable "pipeline_variables" { + description = "List of pipeline variables to create" + type = list(object({ + name = string + value = string + is_secret = optional(bool, false) + allow_override = optional(bool, true) + })) + default = [] +} diff --git a/modules/azuredevops/pipeline/buildingblock/versions.tf b/modules/azuredevops/pipeline/buildingblock/versions.tf new file mode 100644 index 0000000..6201b42 --- /dev/null +++ b/modules/azuredevops/pipeline/buildingblock/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuredevops = { + source = "microsoft/azuredevops" + version = "~> 1.1.1" + } + } +} diff --git a/modules/azuredevops/project/backplane/README.md b/modules/azuredevops/project/backplane/README.md index be6f84a..8ed3355 100644 --- a/modules/azuredevops/project/backplane/README.md +++ b/modules/azuredevops/project/backplane/README.md @@ -68,8 +68,8 @@ To create the PAT token, you need: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [azuread](#requirement\_azuread) | ~> 2.53.1 | -| [azurerm](#requirement\_azurerm) | ~> 3.116.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | ## Modules @@ -83,7 +83,6 @@ No modules. | [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | | [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | | [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | -| [azurerm_resource_group_template_deployment.documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group_template_deployment) | resource | | [azurerm_role_assignment.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | | [azurerm_role_definition.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | diff --git a/modules/azuredevops/project/backplane/documentation.tf b/modules/azuredevops/project/backplane/documentation.tf deleted file mode 100644 index f10d5b0..0000000 --- a/modules/azuredevops/project/backplane/documentation.tf +++ /dev/null @@ -1,45 +0,0 @@ -# Add documentation template resource -resource "azurerm_resource_group_template_deployment" "documentation" { - name = "azure-devops-documentation" - resource_group_name = azurerm_resource_group.devops.name - deployment_mode = "Incremental" - - template_content = jsonencode({ - "$schema" = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#" - contentVersion = "1.0.0.0" - parameters = {} - variables = {} - resources = [] - outputs = { - documentation = { - type = "object" - value = { - title = "Azure DevOps Project Building Block" - description = "Creates and manages Azure DevOps projects with user entitlements and group memberships" - version = "1.0.0" - backplane = { - resources = [ - "Azure AD Service Principal", - "Azure Key Vault for PAT storage", - "Custom Role Definitions", - "Role Assignments" - ] - } - buildingblock = { - resources = [ - "Azure DevOps Project", - "User Entitlements (Stakeholder licenses)", - "Custom Project Groups", - "Group Memberships" - ] - } - requirements = [ - "Azure DevOps organization", - "Personal Access Token with required scopes", - "Users must exist in Azure AD/identity provider" - ] - } - } - } - }) -} \ No newline at end of file diff --git a/modules/azuredevops/project/backplane/versions.tf b/modules/azuredevops/project/backplane/versions.tf index c94c4a7..63432c0 100644 --- a/modules/azuredevops/project/backplane/versions.tf +++ b/modules/azuredevops/project/backplane/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "~> 3.116.0" + version = "~> 4.51.0" } azuread = { source = "hashicorp/azuread" - version = "~> 2.53.1" + version = "~> 3.6.0" } } } \ No newline at end of file diff --git a/modules/azuredevops/project/buildingblock/APP_TEAM_README.md b/modules/azuredevops/project/buildingblock/APP_TEAM_README.md index d81c8f6..f5c545d 100644 --- a/modules/azuredevops/project/buildingblock/APP_TEAM_README.md +++ b/modules/azuredevops/project/buildingblock/APP_TEAM_README.md @@ -1,178 +1,175 @@ -# 🚀 Azure DevOps Project Building Block - -Create and manage Azure DevOps projects with automatic user licensing and role-based access control. - -## ✨ What You Get - -- **New Azure DevOps Project** with your chosen configuration -- **User Assignment** from authoritative system to appropriate groups -- **Role-Based Groups** mapping user roles to Azure DevOps project groups -- **Secure Authentication** through Azure Key Vault managed credentials - -## 🎯 Quick Start - -```hcl -module "my_devops_project" { - source = "path/to/azuredevops/project/buildingblock" - - # Basic project setup - project_name = "amazing-product" - - # Users provided by authoritative system - users = [ - { - meshIdentifier = "dev-001" - username = "developer" - firstName = "John" - lastName = "Developer" - email = "developer@company.com" - euid = "john.developer" - roles = ["user"] - }, - { - meshIdentifier = "mgr-001" - username = "manager" - firstName = "Jane" - lastName = "Manager" - email = "manager@company.com" - euid = "jane.manager" - roles = ["admin", "reader"] - } - ] -} -``` +# Azure DevOps Project -## 👥 User Roles Explained +This building block creates and manages Azure DevOps projects with automatic user licensing and role-based access control. Users are assigned to appropriate groups based on their roles from the authoritative system. -| Role in User.roles | Azure DevOps Group | What They Can Do | Best For | -|--------------------|------------------|-----------------|----------| -| **reader** | Readers | View project items, browse code | Stakeholders, managers | -| **user** | Contributors | Create work items, contribute code, run builds | Developers, testers | -| **admin** | Project Administrators | Full project control, manage users | Project leads, DevOps engineers | +## 🚀 Usage Examples -## 🔐 License Management +- A development team requests a new Azure DevOps project to **manage their application's repositories, pipelines, and work items** in one place. +- A project lead configures user access by **assigning team members appropriate roles** (reader, contributor, administrator) through the authoritative system. +- An organization creates multiple projects to **separate different applications** or teams with independent access control. -User licenses are managed externally by the authoritative system. This module focuses on assigning users to the appropriate Azure DevOps project groups based on their roles. +## 🔄 Shared Responsibility -## 🔄 Shared Responsibility Matrix +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Create Azure DevOps project | ✅ | ❌ | +| Manage user licensing | ✅ | ❌ | +| Assign users to project groups | ✅ | ❌ | +| Define user roles in authoritative system | ❌ | ✅ | +| Use project for development work | ❌ | ✅ | +| Manage project content (repos, pipelines, boards) | ❌ | ✅ | +| Request access changes | ❌ | ✅ | + +## 👥 User Roles Explained -| Task | Your Responsibility | Building Block Handles | -|------|-------------------|----------------------| -| **User Accounts** | Create users in Azure AD | ✅ Authoritative system manages | - | -| **User Licenses** | - | ✅ Authoritative system assigns | - | -| **User Roles** | - | ✅ Authoritative system defines | Map roles to Azure DevOps groups | -| **PAT Token** | Create & store in Key Vault | - | Retrieve & use for authentication | -| **Project Config** | Define requirements | - | Create project with settings | -| **Team Structure** | - | Provide user data with roles | Apply group memberships | -| **Ongoing Management** | Update user lists | Update user data | Apply changes automatically | +| Role in Authoritative System | Azure DevOps Group | What They Can Do | Best For | +|------------------------------|-------------------|------------------|----------| +| **reader** | Readers | View project items, browse code | Stakeholders, managers, auditors | +| **user** | Contributors | Create work items, contribute code, run builds | Developers, testers, engineers | +| **admin** | Project Administrators | Full project control, manage settings | Project leads, DevOps engineers | ## 💡 Best Practices -### 🏗️ Project Setup +### Project Setup + +**Why**: Well-configured projects improve team collaboration and organization. + +**Recommendations**: - Use descriptive project names (e.g., `mobile-app-frontend` not `project1`) - Start with `private` visibility for security -- Choose `Agile` work item template for flexibility +- Choose `Agile` work item template for flexibility (or your team's preferred methodology) + +### User Management + +**Why**: Proper access control protects sensitive code and maintains compliance. -### 👤 User Management -- **Role Assignment**: Ensure users have appropriate roles in the authoritative system -- **Review Regularly**: Audit user access and roles quarterly -- **Group Mapping**: Users are automatically assigned to Azure DevOps groups based on their roles +**Best Practices**: +- Ensure users have appropriate roles in the authoritative system +- Review user access and roles quarterly +- Follow principle of least privilege (give minimum required access) +- Users are automatically assigned to Azure DevOps groups based on their roles -### 🔐 Security -- **Rotate PAT Tokens**: Update tokens every 6 months minimum -- **Principle of Least Privilege**: Give users minimum required access -- **Monitor Access**: Regular review of user permissions +### Security -### 💸 Cost Optimization -- **License Management**: Coordinate with authoritative system for license optimization -- **Role Hygiene**: Ensure users only have necessary roles -- **Feature Control**: Disable unused project features +**Why**: Protect your codebase and development environment from unauthorized access. -## 🚨 Important Notes +**Recommendations**: +- PAT tokens should be rotated every 6 months minimum +- Monitor access logs for unusual activity +- Regular audits of user permissions +- Remove access for departing team members promptly -⚠️ **User Accounts Required**: Users must exist in your Azure AD before running this module. +### Cost Optimization -⚠️ **Organization Permissions**: You need organization-level permissions to assign licenses. +**Why**: Azure DevOps licensing can add up, especially for large teams. -⚠️ **PAT Scopes**: Ensure your Personal Access Token has the required scopes: -- Project & Team (Read, Write, & Manage) -- Member Entitlement Management (Read & Write) +**Tips**: +- Use Stakeholder licenses (free) for users who only need to view/comment +- Ensure users only have necessary roles +- Coordinate with the authoritative system for license optimization +- Disable unused project features to reduce overhead + +### Project Features + +Common features you can enable or disable: +- **Boards**: Work item tracking, backlogs, sprints +- **Repos**: Git repositories +- **Pipelines**: CI/CD automation +- **Test Plans**: Manual and exploratory testing +- **Artifacts**: Package feeds (NuGet, npm, Maven, etc.) + +## 🔐 License Management + +User licenses are managed externally by the authoritative system. This building block focuses on assigning users to the appropriate Azure DevOps project groups based on their roles. + +**Available License Types**: +- **Stakeholder** (Free): View and comment on work items, limited access +- **Basic**: Full access to repos, pipelines, boards +- **Basic + Test Plans**: Includes Test Plans feature ## 📝 Common Scenarios ### Scenario 1: Development Team -```hcl -users = [ - { - principal_name = "lead@company.com" - role = "administrator" - license_type = "basic" - }, - { - principal_name = "dev1@company.com" - role = "contributor" - license_type = "basic" - }, - { - principal_name = "dev2@company.com" - role = "contributor" - license_type = "basic" - } -] -``` + +A team with a project lead, several developers, and a manager: +- Project lead: `administrator` role → Project Administrators group +- Developers: `user` role → Contributors group +- Manager: `reader` role → Readers group (Stakeholder license) ### Scenario 2: Mixed Team with Stakeholders -```hcl -users = [ - { - principal_name = "manager@company.com" - role = "reader" - license_type = "stakeholder" # Free! - }, - { - principal_name = "developer@company.com" - role = "contributor" - license_type = "basic" - }, - { - principal_name = "stakeholder@company.com" - role = "reader" - license_type = "stakeholder" # Free! - } -] -``` + +A team with developers and business stakeholders: +- Developers: `user` role with Basic license +- Stakeholders: `reader` role with Stakeholder license (free) +- DevOps engineer: `administrator` role ### Scenario 3: Minimal Features Project -```hcl -project_features = { - testplans = "disabled" # Save costs - artifacts = "disabled" # Not needed yet -} -``` -## 🔧 Troubleshooting +A simple project that only needs repositories and pipelines: +- Disable Test Plans to save costs +- Disable Artifacts if not needed +- Keep Repos and Pipelines enabled + +## ⚠️ Important Notes + +- User accounts must exist in your Azure AD before assignment +- PAT token must have correct scopes: + - Project & Team (Read, Write, & Manage) + - Member Entitlement Management (Read & Write) +- Role changes in the authoritative system automatically update Azure DevOps group memberships +- Removing a user from the authoritative system removes their project access + +## 🆘 Troubleshooting ### "User not found" Error -1. ✅ Check user exists in Azure AD -2. ✅ Verify email address is correct -3. ✅ Ensure user is invited to Azure DevOps org + +**Cause**: User doesn't exist in Azure AD or email address is incorrect + +**Solution**: +1. Verify user exists in Azure AD +2. Check email address is correct +3. Ensure user is invited to Azure DevOps organization ### License Assignment Failed -1. ✅ Verify PAT has Member Entitlement permissions -2. ✅ Check available licenses in organization -3. ✅ Confirm organization-level access rights + +**Cause**: Insufficient available licenses or missing permissions + +**Solution**: +1. Verify PAT has Member Entitlement permissions +2. Check available licenses in organization +3. Confirm organization-level access rights ### Access Denied -1. ✅ Verify PAT scopes are correct -2. ✅ Check Key Vault access permissions -3. ✅ Ensure PAT hasn't expired -## 🎉 Success! +**Cause**: PAT expired or insufficient scopes + +**Solution**: +1. Verify PAT scopes are correct +2. Check Key Vault access permissions +3. Ensure PAT hasn't expired +4. Contact Platform Team for assistance + +### User Has Wrong Permissions + +**Cause**: Role mismatch between authoritative system and Azure DevOps + +**Solution**: +1. Verify role assignment in authoritative system +2. Wait for synchronization (may take a few minutes) +3. Check Azure DevOps group memberships in project settings + +## 🎉 Getting Started + +Once your project is deployed: + +1. **Access your project**: Navigate to `https://dev.azure.com/yourorg/yourproject` +2. **Verify access**: Ensure all team members can log in and see appropriate features +3. **Create repositories**: Start adding your code repositories +4. **Set up pipelines**: Configure CI/CD for your applications +5. **Configure boards**: Set up work item tracking for your team -Once deployed, your team will have: -- ✅ A fully configured Azure DevOps project -- ✅ Users with appropriate licenses and access -- ✅ Organized role-based security groups -- ✅ Ready-to-use development environment +## 📚 Related Documentation -Visit your project at: `https://dev.azure.com/yourorg/yourproject` \ No newline at end of file +- [Azure DevOps Projects Overview](https://learn.microsoft.com/en-us/azure/devops/organizations/projects/) +- [Project Permissions and Access](https://learn.microsoft.com/en-us/azure/devops/organizations/security/permissions) +- [Work with Projects](https://learn.microsoft.com/en-us/azure/devops/organizations/projects/about-projects) diff --git a/modules/azuredevops/project/buildingblock/README.md b/modules/azuredevops/project/buildingblock/README.md index 5642418..4fa018b 100644 --- a/modules/azuredevops/project/buildingblock/README.md +++ b/modules/azuredevops/project/buildingblock/README.md @@ -2,7 +2,8 @@ name: Azure DevOps Project supportedPlatforms: - azuredevops -description: Creates and manages Azure DevOps projects with user entitlements, stakeholder licenses, and role-based group memberships. +description: | + Creates and manages Azure DevOps projects with user entitlements, stakeholder licenses, and role-based group memberships. category: devops --- @@ -86,20 +87,6 @@ module "azure_devops_project" { } ``` -## Variables - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|----------| -| `azure_devops_organization_url` | Azure DevOps organization URL | `string` | - | yes | -| `key_vault_name` | Key Vault name containing PAT | `string` | - | yes | -| `resource_group_name` | Resource group containing Key Vault | `string` | - | yes | -| `project_name` | Name of the Azure DevOps project | `string` | - | yes | -| `project_description` | Project description | `string` | `"Managed by Terraform"` | no | -| `project_visibility` | Project visibility (private/public) | `string` | `"private"` | no | -| `work_item_template` | Work item template | `string` | `"Agile"` | no | -| `version_control` | Version control system | `string` | `"Git"` | no | -| `users` | List of users from authoritative system | `list(object)` | `[]` | no | - ## Default Project Features The building block creates projects with the following default features: @@ -120,18 +107,6 @@ Users are assigned to default Azure DevOps project groups based on their roles l Users can have multiple roles and will be assigned to all corresponding groups. - - -## Outputs - -| Name | Description | -|------|-------------| -| `project_id` | ID of the created project | -| `project_url` | URL of the project | -| `user_assignments` | Map of users and their assigned roles | -| `group_memberships` | Information about group memberships | -| `project_features` | Enabled/disabled project features | - ## Important Notes - **User Management**: Users are provided by authoritative system with pre-assigned roles. @@ -166,7 +141,7 @@ If you get permission errors: |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | | [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 | -| [azurerm](#requirement\_azurerm) | ~> 3.116.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | ## Modules @@ -195,7 +170,7 @@ No modules. | [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault containing the Azure DevOps PAT | `string` | n/a | yes | | [pat\_secret\_name](#input\_pat\_secret\_name) | n/a | `string` | `"Name of the Azure DevOps PAT Token stored in the KeyVault"` | no | | [project\_description](#input\_project\_description) | Description of the Azure DevOps project | `string` | `"Managed by Terraform"` | no | -| [project\_features](#input\_project\_features) | Project features to enable/disable |
object({
boards = optional(string, "enabled")
repositories = optional(string, "enabled")
pipelines = optional(string, "enabled")
testplans = optional(string, "disabled")
artifacts = optional(string, "enabled")
})
|
{
"artifacts": "disabled",
"boards": "enabled",
"pipelines": "disabled",
"repositories": "disabled",
"testplans": "disabled"
}
| no | +| [project\_features](#input\_project\_features) | Project features to enable/disable |
object({
boards = optional(string, "enabled")
repositories = optional(string, "enabled")
pipelines = optional(string, "enabled")
testplans = optional(string, "disabled")
artifacts = optional(string, "enabled")
})
|
{
"artifacts": "enabled",
"boards": "enabled",
"pipelines": "enabled",
"repositories": "enabled",
"testplans": "disabled"
}
| no | | [project\_name](#input\_project\_name) | Name of the Azure DevOps project | `string` | n/a | yes | | [project\_visibility](#input\_project\_visibility) | Visibility of the project (private or public) | `string` | `"private"` | no | | [resource\_group\_name](#input\_resource\_group\_name) | Resource group name containing the Key Vault | `string` | n/a | yes | @@ -207,11 +182,15 @@ No modules. | Name | Description | |------|-------------| +| [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | | [group\_memberships](#output\_group\_memberships) | Information about group memberships | +| [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault containing the Azure DevOps PAT | +| [pat\_secret\_name](#output\_pat\_secret\_name) | Name of the Azure DevOps PAT secret in Key Vault | | [project\_features](#output\_project\_features) | Enabled/disabled project features | | [project\_id](#output\_project\_id) | ID of the created Azure DevOps project | | [project\_name](#output\_project\_name) | Name of the created Azure DevOps project | | [project\_url](#output\_project\_url) | URL of the created Azure DevOps project | | [project\_visibility](#output\_project\_visibility) | Visibility of the project | +| [resource\_group\_name](#output\_resource\_group\_name) | Resource group name containing the Key Vault | | [user\_assignments](#output\_user\_assignments) | Map of users and their assigned roles | \ No newline at end of file diff --git a/modules/azuredevops/project/buildingblock/main.tf b/modules/azuredevops/project/buildingblock/main.tf index 08352ce..b8f3ebd 100644 --- a/modules/azuredevops/project/buildingblock/main.tf +++ b/modules/azuredevops/project/buildingblock/main.tf @@ -6,17 +6,18 @@ locals { readers = [ for user in var.users : user.email - if contains(user.roles, "reader") + if contains(user.roles, "reader") || contains(user.roles, "Workspace Member") ] contributors = [ for user in var.users : user.email - if contains(user.roles, "user") + if contains(user.roles, "user") || contains(user.roles, "Workspace Manager") ] administrators = [ for user in var.users : user.email - if contains(user.roles, "admin") + if contains(user.roles, "admin") || contains(user.roles, "Workspace Owner") + ] # Create a map of email to user descriptor for easy lookup user_descriptors = { @@ -47,6 +48,7 @@ resource "azuredevops_project" "main" { lifecycle { ignore_changes = [ + visibility, version_control, work_item_template ] diff --git a/modules/azuredevops/project/buildingblock/outputs.tf b/modules/azuredevops/project/buildingblock/outputs.tf index 80daa83..53b0888 100644 --- a/modules/azuredevops/project/buildingblock/outputs.tf +++ b/modules/azuredevops/project/buildingblock/outputs.tf @@ -10,7 +10,7 @@ output "project_name" { output "project_url" { description = "URL of the created Azure DevOps project" - value = "${var.azure_devops_organization_url}/${azuredevops_project.main.name}" + value = "${var.azure_devops_organization_url}/${replace(azuredevops_project.main.name, " ", "%20")}" } output "project_visibility" { @@ -55,4 +55,25 @@ output "group_memberships" { output "project_features" { description = "Enabled/disabled project features" value = var.project_features +} + +output "azure_devops_organization_url" { + description = "Azure DevOps organization URL" + value = var.azure_devops_organization_url +} + +output "key_vault_name" { + description = "Name of the Key Vault containing the Azure DevOps PAT" + value = var.key_vault_name +} + +output "resource_group_name" { + description = "Resource group name containing the Key Vault" + value = var.resource_group_name +} + +output "pat_secret_name" { + description = "Name of the Azure DevOps PAT secret in Key Vault" + value = var.pat_secret_name + sensitive = true } \ No newline at end of file diff --git a/modules/azuredevops/project/buildingblock/project.tftest.hcl b/modules/azuredevops/project/buildingblock/project.tftest.hcl index 52a556c..ebb633c 100644 --- a/modules/azuredevops/project/buildingblock/project.tftest.hcl +++ b/modules/azuredevops/project/buildingblock/project.tftest.hcl @@ -1,22 +1,24 @@ -run "valid_project_creation" { - command = plan - variables { - azure_devops_organization_url = "https://dev.azure.com/testorg" - key_vault_name = "kv-test-devops" - resource_group_name = "rg-test-devops" - project_name = "test-project" - project_description = "Test project for validation" +variables { + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" + pat_secret_name = "ado-pat" +} +run "valid_project_creation" { + variables { + project_name = "test-project" + project_description = "Test project for validation" users = [ { - meshIdentifier = "test-user-001" - username = "testuser" - firstName = "Test" - lastName = "User" - email = "test.user@example.com" - euid = "test.user" - roles = ["user"] + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] } ] } @@ -38,41 +40,36 @@ run "valid_project_creation" { } run "user_role_assignment_validation" { - command = plan variables { - azure_devops_organization_url = "https://dev.azure.com/testorg" - key_vault_name = "kv-test-devops" - resource_group_name = "rg-test-devops" - project_name = "test-project" - + project_name = "test-project" users = [ { - meshIdentifier = "reader-001" - username = "readeruser" - firstName = "Reader" - lastName = "User" - email = "reader@example.com" - euid = "reader.user" - roles = ["reader"] + meshIdentifier = "likvid-anna-user" + username = "likvid-anna@meshcloud.io" + firstName = "Anna" + lastName = "Livkid" + email = "likvid-anna@meshcloud.io" + euid = "likvid-anna@meshcloud.io" + roles = ["reader", "Workspace Member"] }, { - meshIdentifier = "dev-001" - username = "developer" - firstName = "Dev" - lastName = "User" - email = "developer@example.com" - euid = "dev.user" - roles = ["user"] + meshIdentifier = "likvid-daniela-user" + username = "likvid-daniela@meshcloud.io" + firstName = "Daniela" + lastName = "Livkid" + email = "likvid-daniela@meshcloud.io" + euid = "likvid-daniela@meshcloud.io" + roles = ["user", "Workspace Manager"] }, { - meshIdentifier = "admin-001" - username = "adminuser" - firstName = "Admin" - lastName = "User" - email = "admin@example.com" - euid = "admin.user" - roles = ["admin"] + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] } ] } @@ -93,38 +90,70 @@ run "user_role_assignment_validation" { } } -run "invalid_project_name" { - command = plan +run "invalid_project_name_empty" { expect_failures = [ var.project_name ] variables { - azure_devops_organization_url = "https://dev.azure.com/testorg" - key_vault_name = "kv-test-devops" - resource_group_name = "rg-test-devops" - project_name = "" # Invalid: empty name + project_name = "" + users = [ + { + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] + } + ] + } +} + +run "invalid_project_name_too_long" { + expect_failures = [ + var.project_name + ] + + variables { + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" + project_name = "ThisProjectNameIsWayTooLongAndExceedsTheMaximumAllowedCharacterLimitOf64Characters" + pat_secret_name = "ado-pat" + users = [ + { + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] + } + ] } } run "user_with_multiple_roles" { - command = plan variables { - azure_devops_organization_url = "https://dev.azure.com/testorg" - key_vault_name = "kv-test-devops" - resource_group_name = "rg-test-devops" + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" project_name = "test-project" + pat_secret_name = "ado-pat" users = [ { - meshIdentifier = "multi-001" - username = "multiuser" - firstName = "Multi" - lastName = "User" - email = "multi@example.com" - euid = "multi.user" - roles = ["admin", "reader", "user"] # Multiple roles + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "reader", "user", "Workspace Owner"] # Multiple roles } ] } @@ -146,23 +175,22 @@ run "user_with_multiple_roles" { } run "user_without_relevant_roles" { - command = plan variables { - azure_devops_organization_url = "https://dev.azure.com/testorg" - key_vault_name = "kv-test-devops" - resource_group_name = "rg-test-devops" + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" project_name = "test-project" - + pat_secret_name = "ado-pat" users = [ { - meshIdentifier = "norole-001" - username = "noroleuser" - firstName = "No" - lastName = "Role" - email = "norole@example.com" - euid = "no.role" - roles = ["some-other-role"] # No Azure DevOps relevant roles + meshIdentifier = "likvid-daniela-user" + username = "likvid-daniela@meshcloud.io" + firstName = "Daniela" + lastName = "Livkid" + email = "likvid-daniela@meshcloud.io" + euid = "likvid-daniela@meshcloud.io" + roles = ["NONE Exisiting Role"] # No Azure DevOps relevant roles } ] } @@ -184,13 +212,24 @@ run "user_without_relevant_roles" { } run "default_project_features" { - command = plan variables { - azure_devops_organization_url = "https://dev.azure.com/testorg" - key_vault_name = "kv-test-devops" - resource_group_name = "rg-test-devops" + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" project_name = "test-project" + pat_secret_name = "ado-pat" + users = [ + { + meshIdentifier = "likvid-tom-user" + username = "likvid-tom@meshcloud.io" + firstName = "Tom" + lastName = "Livkid" + email = "likvid-tom@meshcloud.io" + euid = "likvid-tom@meshcloud.io" + roles = ["admin", "Workspace Owner"] + } + ] } assert { @@ -199,13 +238,13 @@ run "default_project_features" { } assert { - condition = azuredevops_project.main.features.repositories == "diabled" - error_message = "Repositories should be diabled by default" + condition = azuredevops_project.main.features.repositories == "enabled" + error_message = "Repositories should be enabled by default" } assert { - condition = azuredevops_project.main.features.pipelines == "diabled" - error_message = "Pipelines should be diabled by default" + condition = azuredevops_project.main.features.pipelines == "enabled" + error_message = "Pipelines should be enabled by default" } assert { @@ -214,8 +253,8 @@ run "default_project_features" { } assert { - condition = azuredevops_project.main.features.artifacts == "diabled" - error_message = "Artifacts should be diabled by default" + condition = azuredevops_project.main.features.artifacts == "enabled" + error_message = "Artifacts should be enabled by default" } } diff --git a/modules/azuredevops/project/buildingblock/variables.tf b/modules/azuredevops/project/buildingblock/variables.tf index 0b6e646..5f8f033 100644 --- a/modules/azuredevops/project/buildingblock/variables.tf +++ b/modules/azuredevops/project/buildingblock/variables.tf @@ -24,7 +24,7 @@ variable "project_name" { type = string validation { - condition = length(var.project_name) >= 1 && length(var.project_name) <= 64 + condition = length(var.project_name) > 0 && length(var.project_name) <= 64 error_message = "Project name must be between 1 and 64 characters." } } @@ -79,10 +79,10 @@ variable "project_features" { }) default = { boards = "enabled" - repositories = "disabled" - pipelines = "disabled" + repositories = "enabled" + pipelines = "enabled" testplans = "disabled" - artifacts = "disabled" + artifacts = "enabled" } } diff --git a/modules/azuredevops/project/buildingblock/versions.tf b/modules/azuredevops/project/buildingblock/versions.tf index 33218b0..a0dd1d6 100644 --- a/modules/azuredevops/project/buildingblock/versions.tf +++ b/modules/azuredevops/project/buildingblock/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "~> 3.116.0" + version = "~> 4.51.0" } azuredevops = { source = "microsoft/azuredevops" diff --git a/modules/azuredevops/repository/backplane/README.md b/modules/azuredevops/repository/backplane/README.md new file mode 100644 index 0000000..4a173fa --- /dev/null +++ b/modules/azuredevops/repository/backplane/README.md @@ -0,0 +1,98 @@ +# Azure DevOps Repository Backplane + +This module provisions the infrastructure required to support the Azure DevOps Repository building block. + +## What It Provisions + +- **Azure AD Service Principal**: For repository management automation +- **Azure Key Vault**: Stores Azure DevOps Personal Access Token (PAT) +- **Custom Role Definition**: Minimal permissions for reading Key Vault secrets +- **Role Assignment**: Grants the service principal access to Key Vault + +## Prerequisites + +- Azure subscription with permissions to create: + - Azure AD applications and service principals + - Key Vault instances + - Custom role definitions and assignments +- Azure DevOps organization with Administrator access +- Azure DevOps PAT with `Code (Read & Write)` scope + +## Usage + +```hcl +module "azuredevops_repository_backplane" { + source = "./backplane" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + service_principal_name = "azuredevops-repo-terraform" + key_vault_name = "kv-azdo-repo-prod" + resource_group_name = "rg-azdo-repo-prod" + location = "West Europe" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" +} +``` + +## Post-Deployment Steps + +1. Create an Azure DevOps PAT with `Code (Read & Write)` scope +2. Store the PAT in the provisioned Key Vault: + ```bash + az keyvault secret set --vault-name --name azdo-pat --value + ``` + +## Security Considerations + +- Service principal has read-only access to Key Vault secrets +- PAT should be rotated regularly (recommended: every 90 days) +- Use separate backplane instances for different environments + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuread_application.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | +| [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_role_assignment.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store the Azure DevOps PAT | `string` | n/a | yes | +| [location](#input\_location) | Azure region for resources | `string` | `"West Europe"` | no | +| [resource\_group\_name](#input\_resource\_group\_name) | Resource group name for the Key Vault | `string` | n/a | yes | +| [scope](#input\_scope) | Azure scope for role definitions (subscription or management group) | `string` | n/a | yes | +| [service\_principal\_name](#input\_service\_principal\_name) | Name for the Azure DevOps service principal | `string` | `"azure-devops-terraform"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | +| [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT | +| [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT | +| [key\_vault\_uri](#output\_key\_vault\_uri) | URI of the Key Vault for storing Azure DevOps PAT | +| [resource\_group\_name](#output\_resource\_group\_name) | Name of the resource group containing the Key Vault | +| [service\_principal\_client\_id](#output\_service\_principal\_client\_id) | Client ID of the Azure DevOps service principal | +| [service\_principal\_object\_id](#output\_service\_principal\_object\_id) | Object ID of the Azure DevOps service principal | + \ No newline at end of file diff --git a/modules/azuredevops/repository/backplane/main.tf b/modules/azuredevops/repository/backplane/main.tf new file mode 100644 index 0000000..4a61927 --- /dev/null +++ b/modules/azuredevops/repository/backplane/main.tf @@ -0,0 +1,69 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_subscription" "current" {} + +resource "azuread_application" "azure_devops" { + display_name = var.service_principal_name + description = "Service principal for managing Azure DevOps repositories" +} + +resource "azuread_service_principal" "azure_devops" { + client_id = azuread_application.azure_devops.client_id +} + +resource "azurerm_resource_group" "devops" { + name = var.resource_group_name + location = var.location +} + +resource "azurerm_key_vault" "devops" { + name = var.key_vault_name + location = azurerm_resource_group.devops.location + resource_group_name = azurerm_resource_group.devops.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + secret_permissions = [ + "Get", + "List", + "Set", + "Delete", + "Recover", + "Backup", + "Restore" + ] + } + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azuread_service_principal.azure_devops.object_id + + secret_permissions = [ + "Get", + "List" + ] + } +} + +resource "azurerm_role_definition" "azure_devops_manager" { + name = "${var.service_principal_name}-manager" + description = "Allows management of Azure DevOps repositories" + scope = var.scope + + permissions { + actions = [ + "Microsoft.KeyVault/vaults/secrets/read", + "Microsoft.Resources/subscriptions/resourceGroups/read" + ] + } +} + +resource "azurerm_role_assignment" "azure_devops_manager" { + scope = var.scope + role_definition_id = azurerm_role_definition.azure_devops_manager.role_definition_resource_id + principal_id = azuread_service_principal.azure_devops.object_id +} diff --git a/modules/azuredevops/repository/backplane/outputs.tf b/modules/azuredevops/repository/backplane/outputs.tf new file mode 100644 index 0000000..259f6bf --- /dev/null +++ b/modules/azuredevops/repository/backplane/outputs.tf @@ -0,0 +1,34 @@ +output "service_principal_client_id" { + description = "Client ID of the Azure DevOps service principal" + value = azuread_service_principal.azure_devops.client_id +} + +output "service_principal_object_id" { + description = "Object ID of the Azure DevOps service principal" + value = azuread_service_principal.azure_devops.object_id +} + +output "key_vault_id" { + description = "ID of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.id +} + +output "key_vault_name" { + description = "Name of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.name +} + +output "key_vault_uri" { + description = "URI of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.vault_uri +} + +output "resource_group_name" { + description = "Name of the resource group containing the Key Vault" + value = azurerm_resource_group.devops.name +} + +output "azure_devops_organization_url" { + description = "Azure DevOps organization URL" + value = var.azure_devops_organization_url +} diff --git a/modules/azuredevops/repository/backplane/variables.tf b/modules/azuredevops/repository/backplane/variables.tf new file mode 100644 index 0000000..40d6646 --- /dev/null +++ b/modules/azuredevops/repository/backplane/variables.tf @@ -0,0 +1,31 @@ +variable "azure_devops_organization_url" { + description = "Azure DevOps organization URL (e.g., https://dev.azure.com/myorg)" + type = string +} + +variable "service_principal_name" { + description = "Name for the Azure DevOps service principal" + type = string + default = "azure-devops-terraform" +} + +variable "key_vault_name" { + description = "Name of the Key Vault to store the Azure DevOps PAT" + type = string +} + +variable "resource_group_name" { + description = "Resource group name for the Key Vault" + type = string +} + +variable "location" { + description = "Azure region for resources" + type = string + default = "West Europe" +} + +variable "scope" { + description = "Azure scope for role definitions (subscription or management group)" + type = string +} diff --git a/modules/azuredevops/repository/backplane/versions.tf b/modules/azuredevops/repository/backplane/versions.tf new file mode 100644 index 0000000..1877588 --- /dev/null +++ b/modules/azuredevops/repository/backplane/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.6.0" + } + } +} diff --git a/modules/azuredevops/repository/buildingblock/APP_TEAM_README.md b/modules/azuredevops/repository/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..5553811 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/APP_TEAM_README.md @@ -0,0 +1,257 @@ +# Azure DevOps Git Repository + +This building block creates and manages Git repositories in Azure DevOps with built-in best practices for code review and collaboration. Repositories are configured via meshStack with optional branch protection policies. + +## 🚀 Usage Examples + +- A development team creates a repository to **host their application source code** with automatic branch protection on the main branch. +- A team sets up a repository with **strict code review requirements** (minimum 2 reviewers) for production applications. +- An organization creates multiple repositories to **separate microservices** or components with independent versioning. + +## 🔄 Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Create Azure DevOps project | ✅ | ❌ | +| Create repository | ✅ | ❌ | +| Configure branch policies | ✅ | ❌ | +| Push code to repository | ❌ | ✅ | +| Create branches | ❌ | ✅ | +| Submit pull requests | ❌ | ✅ | +| Review code | ❌ | ✅ | +| Merge pull requests | ❌ | ✅ | +| Manage repository settings | ⚠️ | ⚠️ | + +## 💡 Best Practices + +### Repository Naming + +**Why**: Consistent naming makes repositories easy to find and understand. + +**Recommended Patterns**: +- Use lowercase with hyphens: `my-app-name` +- Include component type: `frontend-webapp`, `backend-api` +- Avoid special characters and spaces + +**Examples**: +- ✅ `customer-portal-frontend` +- ✅ `payment-service-api` +- ✅ `shared-components` +- ❌ `CustomerPortal_Frontend` +- ❌ `Payment Service` +- ❌ `repo1` + +### Branch Protection + +**Why**: Branch protection policies ensure code quality and prevent accidental changes to critical branches. + +**When to Enable Branch Policies**: +- ✅ Production repositories +- ✅ Shared libraries and components +- ✅ Any code deployed to customers +- ❌ Personal experimentation repositories +- ❌ Documentation-only repositories + +**Recommended Reviewer Settings**: +- Development repositories: No branch policies or 1 reviewer +- Staging/UAT repositories: 1 reviewer minimum +- Production repositories: 2 reviewers minimum + +### Repository Initialization + +**Clean Init** (Recommended): +- Creates repository with an initial commit and README +- Ready to clone and start working immediately +- Good for new projects starting from scratch + +**Uninitialized**: +- Creates empty repository with no initial commit +- Useful when migrating code from another repository +- Requires manual initialization after creation + +### Working with Branch Policies + +When branch policies are enabled, you cannot push directly to the default branch (usually `main`). Instead: + +1. **Create a feature branch**: + ```bash + git checkout -b feature/my-new-feature + ``` + +2. **Make changes and commit**: + ```bash + git add . + git commit -m "Add new feature" + git push origin feature/my-new-feature + ``` + +3. **Create a pull request** via Azure DevOps web UI + +4. **Wait for reviews** from the required number of reviewers + +5. **Link work items** to satisfy policy requirements (if configured) + +6. **Complete PR** once all policies are satisfied and reviewers approve + +### Clone URLs + +After repository creation, you'll receive multiple clone URLs: + +**HTTPS URL** (Recommended for CI/CD): +```bash +git clone https://dev.azure.com/myorg/myproject/_git/my-repo +``` + +**SSH URL** (Recommended for developers): +```bash +git clone git@ssh.dev.azure.com:v3/myorg/myproject/my-repo +``` + +## 🔍 Repository Configuration Patterns + +### Development/Sandbox Repository + +**Use Case**: Experimentation, learning, proof-of-concepts + +**Configuration**: +- No branch policies +- Any team member can push directly +- Fast iteration, minimal process + +### Team Collaboration Repository + +**Use Case**: Standard application development + +**Configuration**: +- Branch policies enabled +- Minimum 1-2 reviewers required +- Work item linking recommended +- Balance between quality and velocity + +### Production-Critical Repository + +**Use Case**: Customer-facing applications, shared libraries + +**Configuration**: +- Branch policies enabled +- Minimum 2 reviewers required +- Work item linking enforced +- Build validation policies +- Maximum code quality standards + +## 📝 Getting Started with Your Repository + +After repository creation: + +1. **Clone the repository**: + ```bash + git clone + cd + ``` + +2. **Configure your identity** (if not already done): + ```bash + git config user.name "Your Name" + git config user.email "your.email@company.com" + ``` + +3. **Create a feature branch**: + ```bash + git checkout -b feature/initial-setup + ``` + +4. **Add your code and commit**: + ```bash + git add . + git commit -m "Initial project setup" + git push origin feature/initial-setup + ``` + +5. **Create a pull request** if branch policies are enabled, or push to main if not: + ```bash + # If no branch policies: + git checkout main + git merge feature/initial-setup + git push origin main + ``` + +## ⚠️ Important Notes + +- Repository names must be unique within a project +- Branch policies apply only to the default branch (usually `main`) +- Changing initialization type after creation has no effect +- Repository content is not automatically backed up (use branch policies and review processes for protection) + +## 🆘 Troubleshooting + +### Cannot push to repository + +**Cause**: Branch policies are enabled and you're pushing to the default branch + +**Solution**: Create a feature branch and submit a pull request instead + +```bash +git checkout -b feature/my-changes +git push origin feature/my-changes +``` + +### "Repository already exists" error + +**Cause**: Repository name conflicts with existing repository in the project + +**Solution**: Choose a different repository name or delete the existing repository + +### Access denied when cloning + +**Cause**: Missing permissions or expired credentials + +**Solution**: +1. Verify you have at least Reader access to the project +2. For HTTPS: Check your PAT is valid and has Code (Read) permissions +3. For SSH: Verify your SSH key is added to Azure DevOps + +### Pull request cannot be completed + +**Cause**: Branch policies not satisfied + +**Solution**: +1. Ensure required number of reviewers have approved +2. Link work items if policy requires it +3. Resolve all merge conflicts +4. Wait for any required build validations to pass + +### "Branch policy prevents direct push" + +**Cause**: Attempting to push directly to protected branch + +**Solution**: This is expected behavior. Use feature branches and pull requests: + +```bash +# Create and switch to a feature branch +git checkout -b feature/my-work + +# Make your changes and push +git add . +git commit -m "My changes" +git push origin feature/my-work + +# Then create a PR in Azure DevOps UI +``` + +## 🎯 Next Steps + +After your repository is set up: + +1. **Add a README.md** with project information +2. **Set up .gitignore** for your language/framework +3. **Configure branch policies** if needed for quality gates +4. **Create a pipeline** to automate builds and deployments +5. **Invite team members** and assign appropriate permissions +6. **Link work items** to track features and bugs + +## 📚 Related Documentation + +- [Azure DevOps Git Documentation](https://learn.microsoft.com/en-us/azure/devops/repos/git/) +- [Branch Policies Overview](https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies) +- [Pull Request Best Practices](https://learn.microsoft.com/en-us/azure/devops/repos/git/pull-requests) +- [Git Authentication](https://learn.microsoft.com/en-us/azure/devops/repos/git/auth-overview) diff --git a/modules/azuredevops/repository/buildingblock/README.md b/modules/azuredevops/repository/buildingblock/README.md new file mode 100644 index 0000000..419b901 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/README.md @@ -0,0 +1,137 @@ +--- +name: Azure DevOps Git Repository +supportedPlatforms: + - azuredevops +description: Provides a Git repository in Azure DevOps with optional branch protection policies +category: devops +--- + +# Azure DevOps Repository Building Block + +Creates and manages Git repositories in Azure DevOps projects with optional branch protection policies. + +## Prerequisites + +- Deployed Azure DevOps Repository backplane +- Azure DevOps project ID where the repository will be created +- Azure DevOps PAT stored in Key Vault with `Code (Read & Write)` scope + +## Features + +- Creates Git repositories with configurable initialization +- Optional branch protection policies: + - Minimum number of reviewers for pull requests + - Work item linking requirements + - No self-approval on PRs +- Supports clean initialization or uninitialized repositories + +## Usage + +```hcl +module "azuredevops_repository" { + source = "./buildingblock" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + key_vault_name = "kv-azdo-repo-prod" + resource_group_name = "rg-azdo-repo-prod" + pat_secret_name = "azdo-pat" + + project_id = "12345678-1234-1234-1234-123456789012" + repository_name = "my-app-repo" + + init_type = "Clean" + enable_branch_policies = true + minimum_reviewers = 2 +} +``` + +## Branch Protection Policies + +When `enable_branch_policies` is set to `true`, the following policies are applied to the default branch: + +- **Minimum Reviewers**: Requires the specified number of reviewers +- **No Self-Approval**: Submitter cannot approve their own PR +- **Last Pusher Cannot Approve**: The last person to push changes cannot approve +- **Reset Votes on Push**: Approvals are reset when new changes are pushed +- **Work Item Linking**: PRs should link to work items (non-blocking) + +## Repository Initialization Options + +- `Clean`: Creates a repository with an initial commit and README +- `Uninitialized`: Creates an empty repository without any commits +- `Import`: Requires additional configuration for importing from another repository + +## Integration with Azure DevOps Project Module + +This building block is designed to work with repositories created in projects managed by the Azure DevOps Project building block: + +```hcl +module "azuredevops_project" { + source = "../project/buildingblock" + # ... project configuration +} + +module "app_repository" { + source = "./buildingblock" + + project_id = module.azuredevops_project.project_id + # ... repository configuration +} +``` + +## Security Considerations + +- Branch policies help enforce code review standards +- Minimum reviewers prevent unreviewed code from being merged +- Work item linking ensures traceability +- PAT should have minimal required scopes (`Code (Read & Write)`) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuredevops_branch_policy_min_reviewers.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/branch_policy_min_reviewers) | resource | +| [azuredevops_branch_policy_work_item_linking.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/branch_policy_work_item_linking) | resource | +| [azuredevops_git_repository.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/git_repository) | resource | +| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source | +| [azurerm_key_vault_secret.azure_devops_pat](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault_secret) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | +| [enable\_branch\_policies](#input\_enable\_branch\_policies) | Enable branch protection policies on the default branch | `bool` | `true` | no | +| [init\_type](#input\_init\_type) | Type of repository initialization. Options: Clean, Import, Uninitialized | `string` | `"Clean"` | no | +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault containing the Azure DevOps PAT | `string` | n/a | yes | +| [minimum\_reviewers](#input\_minimum\_reviewers) | Minimum number of reviewers required for pull requests | `number` | `2` | no | +| [pat\_secret\_name](#input\_pat\_secret\_name) | Name of the secret in Key Vault that contains the Azure DevOps PAT | `string` | `"azdo-pat"` | no | +| [project\_id](#input\_project\_id) | Azure DevOps Project ID where the repository will be created | `string` | n/a | yes | +| [repository\_name](#input\_repository\_name) | Name of the Git repository to create | `string` | n/a | yes | +| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group containing the Key Vault | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [branch\_policies\_enabled](#output\_branch\_policies\_enabled) | Whether branch policies are enabled | +| [default\_branch](#output\_default\_branch) | Default branch of the repository | +| [repository\_id](#output\_repository\_id) | ID of the created repository | +| [repository\_name](#output\_repository\_name) | Name of the created repository | +| [repository\_url](#output\_repository\_url) | URL of the created repository | +| [ssh\_url](#output\_ssh\_url) | SSH URL of the repository | +| [web\_url](#output\_web\_url) | Web URL of the repository | + \ No newline at end of file diff --git a/modules/azuredevops/repository/buildingblock/logo.png b/modules/azuredevops/repository/buildingblock/logo.png new file mode 100644 index 0000000..bac28a7 Binary files /dev/null and b/modules/azuredevops/repository/buildingblock/logo.png differ diff --git a/modules/azuredevops/repository/buildingblock/logo.svg b/modules/azuredevops/repository/buildingblock/logo.svg new file mode 100644 index 0000000..28a546b --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/logo.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/azuredevops/repository/buildingblock/main.tf b/modules/azuredevops/repository/buildingblock/main.tf new file mode 100644 index 0000000..cb576f1 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/main.tf @@ -0,0 +1,64 @@ +data "azurerm_key_vault" "devops" { + name = var.key_vault_name + resource_group_name = var.resource_group_name +} + +data "azurerm_key_vault_secret" "azure_devops_pat" { + name = var.pat_secret_name + key_vault_id = data.azurerm_key_vault.devops.id +} + +resource "azuredevops_git_repository" "main" { + project_id = var.project_id + name = var.repository_name + + initialization { + init_type = var.init_type + } + + lifecycle { + ignore_changes = [ + initialization + ] + } +} + +resource "azuredevops_branch_policy_min_reviewers" "main" { + count = var.enable_branch_policies ? 1 : 0 + + project_id = var.project_id + + enabled = true + blocking = true + + settings { + reviewer_count = var.minimum_reviewers + submitter_can_vote = false + last_pusher_cannot_approve = true + allow_completion_with_rejects_or_waits = false + on_push_reset_approved_votes = true + + scope { + repository_id = azuredevops_git_repository.main.id + repository_ref = azuredevops_git_repository.main.default_branch + match_type = "Exact" + } + } +} + +resource "azuredevops_branch_policy_work_item_linking" "main" { + count = var.enable_branch_policies ? 1 : 0 + + project_id = var.project_id + + enabled = true + blocking = false + + settings { + scope { + repository_id = azuredevops_git_repository.main.id + repository_ref = azuredevops_git_repository.main.default_branch + match_type = "Exact" + } + } +} diff --git a/modules/azuredevops/repository/buildingblock/outputs.tf b/modules/azuredevops/repository/buildingblock/outputs.tf new file mode 100644 index 0000000..639ce75 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/outputs.tf @@ -0,0 +1,34 @@ +output "repository_id" { + description = "ID of the created repository" + value = azuredevops_git_repository.main.id +} + +output "repository_name" { + description = "Name of the created repository" + value = azuredevops_git_repository.main.name +} + +output "repository_url" { + description = "URL of the created repository" + value = azuredevops_git_repository.main.url +} + +output "ssh_url" { + description = "SSH URL of the repository" + value = azuredevops_git_repository.main.ssh_url +} + +output "web_url" { + description = "Web URL of the repository" + value = azuredevops_git_repository.main.web_url +} + +output "default_branch" { + description = "Default branch of the repository" + value = azuredevops_git_repository.main.default_branch +} + +output "branch_policies_enabled" { + description = "Whether branch policies are enabled" + value = var.enable_branch_policies +} diff --git a/modules/azuredevops/repository/buildingblock/provider.tf b/modules/azuredevops/repository/buildingblock/provider.tf new file mode 100644 index 0000000..1413b11 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/provider.tf @@ -0,0 +1,8 @@ +provider "azuredevops" { + org_service_url = var.azure_devops_organization_url + personal_access_token = data.azurerm_key_vault_secret.azure_devops_pat.value +} + +provider "azurerm" { + features {} +} diff --git a/modules/azuredevops/repository/buildingblock/repository.tftest.hcl b/modules/azuredevops/repository/buildingblock/repository.tftest.hcl new file mode 100644 index 0000000..0c3b6db --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/repository.tftest.hcl @@ -0,0 +1,122 @@ +variables { + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" + pat_secret_name = "ado-pat" + project_id = "eece6ccc-c821-46a1-9214-80df6da9e13f" + repository_name = "test-repo" +} + +run "valid_repository_configuration" { + + variables { + } + + assert { + condition = azuredevops_git_repository.main.name == "test-repo" + error_message = "Repository name should match input variable" + } + + assert { + condition = azuredevops_git_repository.main.project_id == "eece6ccc-c821-46a1-9214-80df6da9e13f" + error_message = "Repository should be created in the specified project" + } +} + +run "repository_with_branch_policies" { + + variables { + enable_branch_policies = true + minimum_reviewers = 3 + } + + assert { + condition = var.enable_branch_policies == true + error_message = "Branch policies should be enabled" + } + + assert { + condition = var.minimum_reviewers == 3 + error_message = "Minimum reviewers should be 3" + } + + assert { + condition = length(azuredevops_branch_policy_min_reviewers.main) == 1 + error_message = "Branch policy should be created when enabled" + } +} + +run "repository_without_branch_policies" { + + variables { + enable_branch_policies = false + } + + assert { + condition = var.enable_branch_policies == false + error_message = "Branch policies should be disabled" + } + + assert { + condition = length(azuredevops_branch_policy_min_reviewers.main) == 0 + error_message = "No branch policy should be created when disabled" + } +} + +run "uninitialized_repository" { + + variables { + init_type = "Uninitialized" + } + + assert { + condition = azuredevops_git_repository.main.initialization[0].init_type == "Uninitialized" + error_message = "Repository initialization type should be Uninitialized" + } +} + +run "clean_initialization" { + + variables { + repository_name = "new-repo" + init_type = "Clean" + } + + assert { + condition = azuredevops_git_repository.main.initialization[0].init_type == "Clean" + error_message = "Repository initialization type should be Clean" + } +} + +run "invalid_init_type" { + + variables { + init_type = "Invalid" + } + + expect_failures = [ + var.init_type + ] +} + +run "minimum_reviewers_out_of_range" { + + variables { + minimum_reviewers = 15 + } + + expect_failures = [ + var.minimum_reviewers + ] +} + +run "minimum_reviewers_zero" { + + variables { + minimum_reviewers = 0 + } + + expect_failures = [ + var.minimum_reviewers + ] +} diff --git a/modules/azuredevops/repository/buildingblock/variables.tf b/modules/azuredevops/repository/buildingblock/variables.tf new file mode 100644 index 0000000..1c5f8e0 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/variables.tf @@ -0,0 +1,58 @@ +variable "azure_devops_organization_url" { + description = "Azure DevOps organization URL (e.g., https://dev.azure.com/myorg)" + type = string +} + +variable "key_vault_name" { + description = "Name of the Key Vault containing the Azure DevOps PAT" + type = string +} + +variable "resource_group_name" { + description = "Name of the resource group containing the Key Vault" + type = string +} + +variable "pat_secret_name" { + description = "Name of the secret in Key Vault that contains the Azure DevOps PAT" + type = string + default = "azdo-pat" +} + +variable "project_id" { + description = "Azure DevOps Project ID where the repository will be created" + type = string +} + +variable "repository_name" { + description = "Name of the Git repository to create" + type = string +} + +variable "init_type" { + description = "Type of repository initialization. Options: Clean, Import, Uninitialized" + type = string + default = "Clean" + + validation { + condition = contains(["Clean", "Import", "Uninitialized"], var.init_type) + error_message = "init_type must be one of: Clean, Import, Uninitialized" + } +} + +variable "enable_branch_policies" { + description = "Enable branch protection policies on the default branch" + type = bool + default = true +} + +variable "minimum_reviewers" { + description = "Minimum number of reviewers required for pull requests" + type = number + default = 2 + + validation { + condition = var.minimum_reviewers >= 1 && var.minimum_reviewers <= 10 + error_message = "minimum_reviewers must be between 1 and 10" + } +} diff --git a/modules/azuredevops/repository/buildingblock/versions.tf b/modules/azuredevops/repository/buildingblock/versions.tf new file mode 100644 index 0000000..6201b42 --- /dev/null +++ b/modules/azuredevops/repository/buildingblock/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuredevops = { + source = "microsoft/azuredevops" + version = "~> 1.1.1" + } + } +} diff --git a/modules/azuredevops/service-connection-subscription/backplane/README.md b/modules/azuredevops/service-connection-subscription/backplane/README.md new file mode 100644 index 0000000..a29f930 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/backplane/README.md @@ -0,0 +1,136 @@ +# Azure DevOps Service Connection (Subscription) Backplane + +This module provisions the infrastructure required to support the Azure DevOps Service Connection (Subscription) building block. + +## What It Provisions + +- **Azure AD Service Principal**: For service connection management automation +- **Azure Key Vault**: Stores Azure DevOps Personal Access Token (PAT) +- **Custom Role Definition**: Minimal permissions for reading Key Vault secrets +- **Role Assignment**: Grants the service principal access to Key Vault +- **Federated Identity Credential** (optional): For workload identity federation (OIDC) authentication + +## Prerequisites + +- Azure subscription with permissions to create: + - Azure AD applications and service principals + - Key Vault instances + - Custom role definitions and assignments +- Azure DevOps organization with Administrator access +- Azure DevOps PAT with `Service Connections (Read, Query & Manage)` scope + +## Usage + +### Basic Backplane (Service Principal Authentication) + +```hcl +module "azuredevops_service_connection_backplane" { + source = "./backplane" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + service_principal_name = "azuredevops-serviceconn-terraform" + key_vault_name = "kv-azdo-sc-prod" + resource_group_name = "rg-azdo-sc-prod" + location = "West Europe" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" +} +``` + +### Backplane with Workload Identity Federation + +```hcl +module "azuredevops_service_connection_backplane" { + source = "./backplane" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + service_principal_name = "azuredevops-serviceconn-terraform" + key_vault_name = "kv-azdo-sc-prod" + resource_group_name = "rg-azdo-sc-prod" + location = "West Europe" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000" + enable_workload_identity_federation = true + azure_devops_organization_id = "33333333-3333-3333-3333-333333333333" + azure_devops_project_name = "MyProject" + service_connection_name = "Azure-Production-Federated" +} +``` + +## Post-Deployment Steps + +1. Create an Azure DevOps PAT with `Service Connections (Read, Query & Manage)` scope +2. Store the PAT in the provisioned Key Vault: + ```bash + az keyvault secret set --vault-name --name azdo-pat --value + ``` + +## Workload Identity Federation + +When `enable_workload_identity_federation = true`, this module configures: +- **Issuer**: `https://vstoken.dev.azure.com/{organization_id}` +- **Subject**: `sc://{org_url}/{project}/{connection_name}` +- **Audience**: `api://AzureADTokenExchange` + +This eliminates the need for client secrets by using OIDC token exchange. + +## Security Considerations + +- Service principal has read-only access to Key Vault secrets +- PAT should be rotated regularly (recommended: every 90 days) +- Use separate backplane instances for different environments +- Service connections will create their own service principals for Azure access + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuread_application.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_application_federated_identity_credential.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | +| [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | +| [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_role_assignment.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.azure_devops_manager](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_devops\_organization\_id](#input\_azure\_devops\_organization\_id) | Azure DevOps organization ID (GUID) for workload identity federation | `string` | n/a | yes | +| [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | +| [azure\_devops\_project\_name](#input\_azure\_devops\_project\_name) | Azure DevOps project name for workload identity federation | `string` | n/a | yes | +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store the Azure DevOps PAT | `string` | n/a | yes | +| [location](#input\_location) | Azure region for resources | `string` | `"West Europe"` | no | +| [resource\_group\_name](#input\_resource\_group\_name) | Resource group name for the Key Vault | `string` | n/a | yes | +| [scope](#input\_scope) | Azure scope for role definitions (subscription or management group) | `string` | n/a | yes | +| [service\_connection\_name](#input\_service\_connection\_name) | Azure DevOps service connection name for workload identity federation | `string` | n/a | yes | +| [service\_principal\_name](#input\_service\_principal\_name) | Name for the Azure DevOps service principal | `string` | `"azure-devops-terraform"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | +| [federated\_credential\_issuer](#output\_federated\_credential\_issuer) | Issuer URL for workload identity federation | +| [federated\_credential\_subject](#output\_federated\_credential\_subject) | Subject identifier for workload identity federation | +| [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT | +| [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT | +| [key\_vault\_uri](#output\_key\_vault\_uri) | URI of the Key Vault for storing Azure DevOps PAT | +| [resource\_group\_name](#output\_resource\_group\_name) | Name of the resource group containing the Key Vault | +| [service\_principal\_client\_id](#output\_service\_principal\_client\_id) | Client ID of the Azure DevOps service principal | +| [service\_principal\_object\_id](#output\_service\_principal\_object\_id) | Object ID of the Azure DevOps service principal | + \ No newline at end of file diff --git a/modules/azuredevops/service-connection-subscription/backplane/main.tf b/modules/azuredevops/service-connection-subscription/backplane/main.tf new file mode 100644 index 0000000..594577f --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/backplane/main.tf @@ -0,0 +1,78 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_subscription" "current" {} + +resource "azuread_application" "azure_devops" { + display_name = var.service_principal_name + description = "Service principal for managing Azure DevOps service connections" +} + +resource "azuread_service_principal" "azure_devops" { + client_id = azuread_application.azure_devops.client_id +} + +resource "azurerm_resource_group" "devops" { + name = var.resource_group_name + location = var.location +} + +resource "azurerm_key_vault" "devops" { + name = var.key_vault_name + location = azurerm_resource_group.devops.location + resource_group_name = azurerm_resource_group.devops.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + secret_permissions = [ + "Get", + "List", + "Set", + "Delete", + "Recover", + "Backup", + "Restore" + ] + } + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azuread_service_principal.azure_devops.object_id + + secret_permissions = [ + "Get", + "List" + ] + } +} + +resource "azurerm_role_definition" "azure_devops_manager" { + name = "${var.service_principal_name}-manager" + description = "Allows management of Azure DevOps service connections" + scope = var.scope + + permissions { + actions = [ + "Microsoft.KeyVault/vaults/secrets/read", + "Microsoft.Resources/subscriptions/resourceGroups/read" + ] + } +} + +resource "azurerm_role_assignment" "azure_devops_manager" { + scope = var.scope + role_definition_id = azurerm_role_definition.azure_devops_manager.role_definition_resource_id + principal_id = azuread_service_principal.azure_devops.object_id +} + +resource "azuread_application_federated_identity_credential" "azure_devops" { + application_id = azuread_application.azure_devops.id + display_name = "${var.service_connection_name}-federated-credential" + description = "Federated identity credential for Azure DevOps service connection" + audiences = ["api://AzureADTokenExchange"] + issuer = "https://vstoken.dev.azure.com/${var.azure_devops_organization_id}" + subject = "sc://${var.azure_devops_organization_url}/${var.azure_devops_project_name}/${var.service_connection_name}" +} diff --git a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf new file mode 100644 index 0000000..94bb9a3 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf @@ -0,0 +1,44 @@ +output "service_principal_client_id" { + description = "Client ID of the Azure DevOps service principal" + value = azuread_service_principal.azure_devops.client_id +} + +output "service_principal_object_id" { + description = "Object ID of the Azure DevOps service principal" + value = azuread_service_principal.azure_devops.object_id +} + +output "key_vault_id" { + description = "ID of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.id +} + +output "key_vault_name" { + description = "Name of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.name +} + +output "key_vault_uri" { + description = "URI of the Key Vault for storing Azure DevOps PAT" + value = azurerm_key_vault.devops.vault_uri +} + +output "resource_group_name" { + description = "Name of the resource group containing the Key Vault" + value = azurerm_resource_group.devops.name +} + +output "azure_devops_organization_url" { + description = "Azure DevOps organization URL" + value = var.azure_devops_organization_url +} + +output "federated_credential_issuer" { + description = "Issuer URL for workload identity federation" + value = "https://vstoken.dev.azure.com/${var.azure_devops_organization_id}" +} + +output "federated_credential_subject" { + description = "Subject identifier for workload identity federation" + value = "sc://${var.azure_devops_organization_url}/${var.azure_devops_project_name}/${var.service_connection_name}" +} diff --git a/modules/azuredevops/service-connection-subscription/backplane/variables.tf b/modules/azuredevops/service-connection-subscription/backplane/variables.tf new file mode 100644 index 0000000..0302061 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/backplane/variables.tf @@ -0,0 +1,46 @@ +variable "azure_devops_organization_url" { + description = "Azure DevOps organization URL (e.g., https://dev.azure.com/myorg)" + type = string +} + +variable "service_principal_name" { + description = "Name for the Azure DevOps service principal" + type = string + default = "azure-devops-terraform" +} + +variable "key_vault_name" { + description = "Name of the Key Vault to store the Azure DevOps PAT" + type = string +} + +variable "resource_group_name" { + description = "Resource group name for the Key Vault" + type = string +} + +variable "location" { + description = "Azure region for resources" + type = string + default = "West Europe" +} + +variable "scope" { + description = "Azure scope for role definitions (subscription or management group)" + type = string +} + +variable "azure_devops_organization_id" { + description = "Azure DevOps organization ID (GUID) for workload identity federation" + type = string +} + +variable "azure_devops_project_name" { + description = "Azure DevOps project name for workload identity federation" + type = string +} + +variable "service_connection_name" { + description = "Azure DevOps service connection name for workload identity federation" + type = string +} diff --git a/modules/azuredevops/service-connection-subscription/backplane/versions.tf b/modules/azuredevops/service-connection-subscription/backplane/versions.tf new file mode 100644 index 0000000..1877588 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/backplane/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.6.0" + } + } +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md b/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..5d604d4 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md @@ -0,0 +1,353 @@ +# Azure DevOps Service Connection (Subscription) + +This building block connects your Azure DevOps pipelines to Azure subscriptions, enabling automated deployment and management of cloud resources. Service connections are configured via meshStack with secure authentication using workload identity federation (OIDC) - no secrets required. + +## 🚀 Usage Examples + +- A development team configures a service connection to **automatically deploy applications** to their Azure subscription via CI/CD pipelines. +- A DevOps engineer creates separate service connections for **development, staging, and production** environments with appropriate permissions. +- A team sets up a read-only service connection for **monitoring and compliance** pipelines that validate infrastructure without modifying it. + +## 🔄 Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Create Azure DevOps project | ✅ | ❌ | +| Create service principal for Azure access | ✅ | ❌ | +| Assign Azure roles to service principal | ✅ | ❌ | +| Create service connection | ✅ | ❌ | +| Provide service principal credentials | ✅ | ❌ | +| Authorize pipelines to use connection | ⚠️ | ⚠️ | +| Use service connection in pipelines | ❌ | ✅ | +| Deploy Azure resources via pipelines | ❌ | ✅ | +| Monitor deployments | ❌ | ✅ | +| Manage federated credentials | ✅ | ❌ | + +## 💡 Best Practices + +### Service Connection Naming + +**Why**: Clear names help identify environment and purpose instantly in pipeline YAML. + +**Recommended Patterns**: +- Include cloud provider: `Azure-Production` +- Include environment: `Azure-Dev`, `Azure-Staging`, `Azure-Prod` +- Include subscription purpose: `Azure-Monitoring`, `Azure-Shared-Services` + +**Examples**: +- ✅ `Azure-Production` +- ✅ `Azure-Dev-Subscription` +- ✅ `Azure-Shared-Services` +- ✅ `Azure-ReadOnly-Monitoring` +- ❌ `connection1` +- ❌ `my-connection` + +### Authorization Strategy + +**Manual Authorization** (Recommended for Production): +- Explicit approval required before pipelines can use the connection +- More secure - prevents unauthorized access +- Best for production environments and sensitive subscriptions +- Compliance-friendly + +**Automatic Authorization** (Development/Testing): +- All pipelines can immediately use the connection +- Convenient for development workflows +- Less secure but faster to use +- Best for dev/test environments only + +### Service Principal Roles + +The service principal's role determines what pipelines can do in Azure. The Platform Team assigns these roles outside this module. + +**Common Role Assignments**: + +**Reader** (Read-Only): +- View resources and configuration +- Monitoring and reporting pipelines +- Compliance checking +- Read-only validation tasks + +**Contributor** (Recommended for Deployments): +- Deploy applications +- Manage most resources (VMs, storage, networking) +- Cannot modify role assignments or permissions +- Standard CI/CD operations + +**Owner** (Use Sparingly): +- Full subscription control +- Can manage role assignments +- Infrastructure as Code managing RBAC +- Only use when absolutely necessary + +### Multi-Environment Setup + +**Best Practice**: Create separate service connections per environment, each using dedicated service principals with environment-appropriate permissions. + +**Example Structure**: +- `Azure-Development`: Auto-authorized, Contributor role, dev subscription +- `Azure-Staging`: Manual authorization, Contributor role, staging subscription +- `Azure-Production`: Manual authorization, Contributor role, production subscription + +### Authentication Method + +This service connection uses **Workload Identity Federation (OIDC)** exclusively: + +**Key Benefits**: +- **No secrets required** - uses OpenID Connect tokens instead of passwords +- **Automatic token rotation** - short-lived tokens are refreshed automatically +- **Enhanced security** - no long-lived credentials that can be compromised +- **Compliance-friendly** - meets modern security standards +- **Zero maintenance** - no credential rotation needed + +**How it works**: Azure DevOps requests a token from Azure AD, which Azure validates using the federated identity credential configured by the Platform Team. The token is short-lived and automatically refreshed. + +### Security Recommendations + +**Do**: +- Use manual authorization for production environments +- Request minimal required permissions (Reader when possible, Contributor when needed) +- Monitor pipeline activity logs regularly +- Leverage workload identity federation's automatic token rotation + +**Don't**: +- Try to manage service principal credentials (they don't exist with OIDC!) +- Store any credentials in pipeline YAML or code repositories +- Use Owner role unless absolutely necessary +- Auto-authorize production service connections + +## 📝 Using Service Connection in Pipelines + +### Azure CLI Task + +```yaml +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: AzureCLI@2 + displayName: 'Deploy Resources' + inputs: + azureSubscription: 'Azure-Production' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az group create --name myResourceGroup --location eastus + az storage account create --name mystorageaccount \ + --resource-group myResourceGroup --sku Standard_LRS +``` + +### Azure PowerShell Task + +```yaml +steps: + - task: AzurePowerShell@5 + displayName: 'Run Azure PowerShell Script' + inputs: + azureSubscription: 'Azure-Production' + ScriptType: 'InlineScript' + Inline: | + Get-AzResourceGroup + New-AzResourceGroup -Name "myRG" -Location "eastus" + azurePowerShellVersion: 'LatestVersion' +``` + +### Terraform Task + +```yaml +steps: + - task: TerraformTaskV4@4 + displayName: 'Terraform Init' + inputs: + provider: 'azurerm' + command: 'init' + backendServiceArm: 'Azure-Production' + backendAzureRmResourceGroupName: 'terraform-state-rg' + backendAzureRmStorageAccountName: 'tfstatestorage' + backendAzureRmContainerName: 'tfstate' + backendAzureRmKey: 'terraform.tfstate' + + - task: TerraformTaskV4@4 + displayName: 'Terraform Apply' + inputs: + provider: 'azurerm' + command: 'apply' + environmentServiceNameAzureRM: 'Azure-Production' +``` + +### ARM Template Deployment + +```yaml +steps: + - task: AzureResourceManagerTemplateDeployment@3 + displayName: 'Deploy ARM Template' + inputs: + azureResourceManagerConnection: 'Azure-Production' + subscriptionId: '87654321-4321-4321-4321-210987654321' + resourceGroupName: 'myResourceGroup' + location: 'East US' + templateLocation: 'Linked artifact' + csmFile: 'azuredeploy.json' + csmParametersFile: 'azuredeploy.parameters.json' + deploymentMode: 'Incremental' +``` + +## 🔐 Authorizing Pipelines + +### Manual Authorization (Recommended for Production) + +If manual authorization is configured: + +1. Run your pipeline - it will pause for authorization +2. Azure DevOps prompts: "This pipeline needs permission to access a resource" +3. Click **View** and then **Permit** +4. Pipeline continues execution + +**To authorize permanently**: +1. Go to **Project Settings** → **Service connections** +2. Select your service connection +3. Click **Security** → Authorize specific pipelines +4. Select pipelines that should always have access + +### Automatic Authorization + +If automatic authorization is configured: +- No action needed +- All pipelines can immediately use the connection + +## 🔍 Verifying Service Connection + +After creation, verify in Azure DevOps: + +1. Navigate to **Project Settings** +2. Go to **Service connections** +3. Find your service connection name +4. Verify: + - ✅ Connection status is green (verified) + - ✅ Subscription name matches expected + - ✅ Service principal is valid + +### Testing the Connection + +Create a simple test pipeline: + +```yaml +# test-connection.yml +trigger: none + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: AzureCLI@2 + inputs: + azureSubscription: 'Azure-Production' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + echo "Testing Azure connection..." + az account show + az group list --output table +``` + +Run manually to verify connectivity and permissions. + +## ⚠️ Important Notes + +- Service principal must be created and configured by Platform Team +- Service connection name is used in pipeline YAML (case-sensitive) +- Service principal permissions are managed outside this module +- Manual authorization is more secure for production environments +- No credential rotation required - workload identity federation handles authentication automatically + +## 🆘 Troubleshooting + +### "Service connection not found" in pipeline + +**Cause**: Service connection name mismatch or not authorized + +**Solution**: +1. Verify service connection name matches exactly in YAML (case-sensitive) +2. Check if manual authorization is required +3. Ensure connection exists in project settings + +### "Insufficient permissions" error + +**Cause**: Service principal lacks required permissions for the operation + +**Solution**: +1. Contact Platform Team to verify service principal role assignment +2. Verify required role for your operation: + - Resource creation/modification: Contributor + - Read-only operations: Reader + - RBAC modifications: Owner +3. Check scope of role assignment (subscription/resource group level) + +### Service connection shows as invalid + +**Cause**: Service principal or federated credential configuration issue + +**Solution**: +1. Contact Platform Team to verify: + - Service principal exists and is active + - Federated identity credential is properly configured + - Azure DevOps organization ID matches the issuer +2. Platform Team will investigate and fix the federated credential configuration + +### Cannot deploy to resource group + +**Cause**: Service principal has Reader role (read-only) + +**Solution**: Contact Platform Team to assign Contributor role to the service principal at the appropriate scope + +### Pipeline authorization keeps prompting + +**Cause**: Manual authorization required for each pipeline run + +**Solution**: +1. Authorize the pipeline permanently (see "Authorizing Pipelines" section above) +2. Or request automatic authorization if appropriate for the environment + +### "Subscription not found" error + +**Cause**: Service principal doesn't have access to the subscription + +**Solution**: Contact Platform Team to verify: +- Service principal exists +- Service principal has role assignment on the subscription +- Subscription ID is correct + +## 🔄 Credential Management + +### No Credential Rotation Required! + +This service connection uses **Workload Identity Federation (OIDC)**, which means: + +✅ **No secrets to manage** - authentication uses short-lived tokens +✅ **Automatic token rotation** - tokens expire quickly and are refreshed automatically +✅ **Zero maintenance** - no manual credential rotation needed +✅ **Better security** - no long-lived credentials that can leak or be compromised + +### What This Means for You + +**You don't need to**: +- Request credential rotation +- Worry about expiring passwords or secrets +- Schedule maintenance windows for credential updates +- Update pipeline configurations for credential changes + +**The Platform Team manages**: +- Service principal configuration +- Federated identity credential setup +- Azure role assignments +- Trust relationship between Azure DevOps and Azure AD + +## 📚 Related Documentation + +- [Azure DevOps Service Connections](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints) +- [Azure Service Principal Best Practices](https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal) +- [Azure RBAC Roles](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles) +- [Pipeline Permissions](https://learn.microsoft.com/en-us/azure/devops/pipelines/process/resources#authorize-a-resource) diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md new file mode 100644 index 0000000..1a24b1e --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -0,0 +1,219 @@ +--- +name: Azure DevOps Service Connection (Subscription) +supportedPlatforms: + - azuredevops +description: Provides an Azure subscription service connection in Azure DevOps for pipeline integration with Azure subscriptions +category: devops +--- + +# Azure DevOps Service Connection (Subscription) Building Block + +Creates and manages Azure subscription service connections in Azure DevOps projects, enabling pipelines to deploy and manage resources in Azure subscriptions. + +## Prerequisites + +- Deployed Azure DevOps Service Connection (Subscription) backplane +- Azure DevOps project ID where the service connection will be created +- Azure subscription ID to connect to +- Azure DevOps PAT stored in Key Vault with `Service Connections (Read, Query & Manage)` scope +- Existing Azure AD service principal with appropriate permissions on the target subscription +- Service principal with federated identity credential configured for Azure DevOps + +## Features + +- Configures Azure DevOps service connection using workload identity federation (OIDC) +- No client secrets required - uses secure token-based authentication +- Optional automatic authorization for all pipelines +- Enhanced security through short-lived tokens + +## Usage + +### Basic Service Connection + +```hcl +module "azuredevops_service_connection" { + source = "./buildingblock" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + key_vault_name = "kv-azdo-sc-prod" + resource_group_name = "rg-azdo-sc-prod" + pat_secret_name = "azdo-pat" + + project_id = "12345678-1234-1234-1234-123456789012" + service_connection_name = "Azure-Production" + azure_subscription_id = "87654321-4321-4321-4321-210987654321" + service_principal_id = "11111111-1111-1111-1111-111111111111" + azure_tenant_id = "22222222-2222-2222-2222-222222222222" +} +``` + +### Service Connection with Auto-Authorization + +```hcl +module "authorized_service_connection" { + source = "./buildingblock" + + azure_devops_organization_url = "https://dev.azure.com/myorg" + key_vault_name = "kv-azdo-sc-prod" + resource_group_name = "rg-azdo-sc-prod" + + project_id = "12345678-1234-1234-1234-123456789012" + service_connection_name = "Azure-Dev" + azure_subscription_id = "87654321-4321-4321-4321-210987654321" + service_principal_id = "11111111-1111-1111-1111-111111111111" + azure_tenant_id = "22222222-2222-2222-2222-222222222222" + authorize_all_pipelines = true + description = "Development environment service connection" +} +``` + +## Authentication Method + +This module exclusively uses **Workload Identity Federation (OIDC)** for enhanced security. + +### Requirements + +The service principal must: +1. Be created and configured outside this module (typically in the backplane) +2. Have appropriate role assignments on the target Azure subscription +3. Have a federated identity credential configured for Azure DevOps with: + - Issuer: `https://vstoken.dev.azure.com/{organization_id}` (GUID, not name) + - Subject: `sc://{org_name}/{project_name}/{connection_name}` + - Audience: `api://AzureADTokenExchange` + +### Benefits + +- **No client secrets** - eliminates secret management burden +- **Automatic token rotation** - tokens are short-lived and rotated automatically +- **Enhanced security** - no long-lived credentials to compromise +- **Compliance-friendly** - meets security requirements without credential storage + +## Pipeline Authorization + +**Manual Authorization** (default: `authorize_all_pipelines = false`): +- Each pipeline must be explicitly authorized to use the service connection +- More secure for production environments + +**Automatic Authorization** (`authorize_all_pipelines = true`): +- All pipelines in the project can use the service connection +- Convenient for development/testing environments + +## Integration with Other Modules + +```hcl +module "azuredevops_project" { + source = "../project/buildingblock" + # ... project configuration +} + +module "ci_pipeline" { + source = "../pipeline/buildingblock" + project_id = module.azuredevops_project.project_id + # ... pipeline configuration +} + +module "azure_connection" { + source = "./buildingblock" + + azure_devops_organization_url = module.azuredevops_project.azure_devops_organization_url + key_vault_name = module.azuredevops_project.key_vault_name + resource_group_name = module.azuredevops_project.resource_group_name + + project_id = module.azuredevops_project.project_id + service_connection_name = "Azure-Prod" + azure_subscription_id = "87654321-4321-4321-4321-210987654321" + service_principal_id = var.service_principal_id + azure_tenant_id = var.azure_tenant_id +} +``` + +## Using Service Connection in Pipelines + +Reference the service connection in your Azure Pipelines YAML: + +```yaml +trigger: + - main + +pool: + vmImage: 'ubuntu-latest' + +steps: + - task: AzureCLI@2 + inputs: + azureSubscription: 'Azure-Production' # Service connection name + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az group list +``` + +## Security Considerations + +- Service principal must be created and managed outside this module +- Workload identity federation uses short-lived tokens (no secrets stored) +- Use least privilege principle when assigning roles to the service principal +- Enable manual authorization for production service connections +- Regularly review service principal permissions +- No credential rotation needed (tokens are automatically rotated) + +## Limitations + +- Service principal must be created and managed separately +- Changing service connection name requires recreation +- Federated identity credential must be configured in the service principal (typically via backplane) +- Only workload identity federation is supported (no client secret authentication) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 | +| [azurerm](#requirement\_azurerm) | ~> 4.51.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuredevops_resource_authorization.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/resource_authorization) | resource | +| [azuredevops_serviceendpoint_azurerm.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/serviceendpoint_azurerm) | resource | +| [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source | +| [azurerm_key_vault_secret.azure_devops_pat](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault_secret) | data source | +| [azurerm_subscription.target](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [authorize\_all\_pipelines](#input\_authorize\_all\_pipelines) | Automatically authorize all pipelines to use this service connection | `bool` | `false` | no | +| [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | +| [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID to connect to | `string` | n/a | yes | +| [azure\_tenant\_id](#input\_azure\_tenant\_id) | Azure AD Tenant ID | `string` | n/a | yes | +| [description](#input\_description) | Description for the service connection | `string` | `"Azure subscription service connection managed by Terraform"` | no | +| [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault containing the Azure DevOps PAT | `string` | n/a | yes | +| [pat\_secret\_name](#input\_pat\_secret\_name) | Name of the secret in Key Vault that contains the Azure DevOps PAT | `string` | `"azdo-pat"` | no | +| [project\_id](#input\_project\_id) | Azure DevOps Project ID where the service connection will be created | `string` | n/a | yes | +| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group containing the Key Vault | `string` | n/a | yes | +| [service\_connection\_name](#input\_service\_connection\_name) | Name of the service connection to create | `string` | n/a | yes | +| [service\_principal\_id](#input\_service\_principal\_id) | Client ID of the existing Azure AD service principal | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [authentication\_method](#output\_authentication\_method) | Authentication method used | +| [authorized\_all\_pipelines](#output\_authorized\_all\_pipelines) | Whether all pipelines are authorized to use this connection | +| [azure\_subscription\_id](#output\_azure\_subscription\_id) | Azure Subscription ID connected | +| [azure\_subscription\_name](#output\_azure\_subscription\_name) | Azure Subscription name connected | +| [service\_connection\_id](#output\_service\_connection\_id) | ID of the created service connection | +| [service\_connection\_name](#output\_service\_connection\_name) | Name of the created service connection | +| [service\_principal\_id](#output\_service\_principal\_id) | Client ID of the service principal | +| [workload\_identity\_federation\_issuer](#output\_workload\_identity\_federation\_issuer) | Issuer URL for workload identity federation | +| [workload\_identity\_federation\_subject](#output\_workload\_identity\_federation\_subject) | Subject identifier for workload identity federation | + \ No newline at end of file diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/logo.png b/modules/azuredevops/service-connection-subscription/buildingblock/logo.png new file mode 100644 index 0000000..66cf4c5 Binary files /dev/null and b/modules/azuredevops/service-connection-subscription/buildingblock/logo.png differ diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/logo.svg b/modules/azuredevops/service-connection-subscription/buildingblock/logo.svg new file mode 100644 index 0000000..84dbe62 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf new file mode 100644 index 0000000..22eb7e7 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf @@ -0,0 +1,43 @@ +data "azurerm_key_vault" "devops" { + name = var.key_vault_name + resource_group_name = var.resource_group_name +} + +data "azurerm_key_vault_secret" "azure_devops_pat" { + name = var.pat_secret_name + key_vault_id = data.azurerm_key_vault.devops.id +} + +data "azurerm_subscription" "target" { + subscription_id = var.azure_subscription_id +} + +resource "azuredevops_serviceendpoint_azurerm" "main" { + project_id = var.project_id + service_endpoint_name = var.service_connection_name + description = var.description + service_endpoint_authentication_scheme = "WorkloadIdentityFederation" + + credentials { + serviceprincipalid = var.service_principal_id + } + + azurerm_spn_tenantid = var.azure_tenant_id + azurerm_subscription_id = data.azurerm_subscription.target.subscription_id + azurerm_subscription_name = data.azurerm_subscription.target.display_name + + lifecycle { + ignore_changes = [ + description + ] + } +} + +resource "azuredevops_resource_authorization" "main" { + count = var.authorize_all_pipelines ? 1 : 0 + + project_id = var.project_id + resource_id = azuredevops_serviceendpoint_azurerm.main.id + authorized = true + type = "endpoint" +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/outputs.tf b/modules/azuredevops/service-connection-subscription/buildingblock/outputs.tf new file mode 100644 index 0000000..7cf6398 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/outputs.tf @@ -0,0 +1,44 @@ +output "service_connection_id" { + description = "ID of the created service connection" + value = azuredevops_serviceendpoint_azurerm.main.id +} + +output "service_connection_name" { + description = "Name of the created service connection" + value = azuredevops_serviceendpoint_azurerm.main.service_endpoint_name +} + +output "service_principal_id" { + description = "Client ID of the service principal" + value = var.service_principal_id +} + +output "azure_subscription_id" { + description = "Azure Subscription ID connected" + value = data.azurerm_subscription.target.subscription_id +} + +output "azure_subscription_name" { + description = "Azure Subscription name connected" + value = data.azurerm_subscription.target.display_name +} + +output "authorized_all_pipelines" { + description = "Whether all pipelines are authorized to use this connection" + value = var.authorize_all_pipelines +} + +output "authentication_method" { + description = "Authentication method used" + value = "workload_identity_federation" +} + +output "workload_identity_federation_issuer" { + description = "Issuer URL for workload identity federation" + value = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_issuer +} + +output "workload_identity_federation_subject" { + description = "Subject identifier for workload identity federation" + value = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_subject +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/provider.tf b/modules/azuredevops/service-connection-subscription/buildingblock/provider.tf new file mode 100644 index 0000000..4841b51 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/provider.tf @@ -0,0 +1,10 @@ +provider "azuredevops" { + org_service_url = var.azure_devops_organization_url + personal_access_token = data.azurerm_key_vault_secret.azure_devops_pat.value +} + +provider "azurerm" { + features {} +} + +provider "azuread" {} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl new file mode 100644 index 0000000..19e7f8a --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl @@ -0,0 +1,53 @@ +variables { + azure_devops_organization_url = "https://dev.azure.com/meshcloud-prod" + key_vault_name = "ado-demo" + resource_group_name = "rg-devops" + pat_secret_name = "ado-pat" + project_id = "eece6ccc-c821-46a1-9214-80df6da9e13f" + repository_id = "e5612cf3-36f1-4db5-b9d4-6431704233f3" + + service_connection_name = "test-service-connection" + azure_subscription_id = "f808fff2-adda-415a-9b77-2833c041aacf" + service_principal_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" + azure_tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827" +} + +run "valid_service_connection_basic" { + + variables { + service_connection_name = "valid-service-connection" + authorize_all_pipelines = false + } +} + +run "valid_service_connection_with_auto_authorize" { + + variables { + service_connection_name = "auto-auth-connection" + authorize_all_pipelines = true + } +} + +run "valid_service_connection_with_description" { + + variables { + service_connection_name = "documented-connection" + description = "Service connection for production deployments" + } +} + +run "minimal_required_variables" { + + variables { + service_connection_name = "minimal-connection" + } +} + +run "service_connection_with_custom_description" { + + variables { + service_connection_name = "custom-desc-connection" + description = "Custom service connection for staging environment" + } +} + diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf new file mode 100644 index 0000000..0f0bcd6 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf @@ -0,0 +1,57 @@ +variable "azure_devops_organization_url" { + description = "Azure DevOps organization URL (e.g., https://dev.azure.com/myorg)" + type = string +} + +variable "key_vault_name" { + description = "Name of the Key Vault containing the Azure DevOps PAT" + type = string +} + +variable "resource_group_name" { + description = "Name of the resource group containing the Key Vault" + type = string +} + +variable "pat_secret_name" { + description = "Name of the secret in Key Vault that contains the Azure DevOps PAT" + type = string + default = "azdo-pat" +} + +variable "project_id" { + description = "Azure DevOps Project ID where the service connection will be created" + type = string +} + +variable "service_connection_name" { + description = "Name of the service connection to create" + type = string +} + +variable "description" { + description = "Description for the service connection" + type = string + default = "Azure subscription service connection managed by Terraform" +} + +variable "azure_subscription_id" { + description = "Azure Subscription ID to connect to" + type = string +} + +variable "service_principal_id" { + description = "Client ID of the existing Azure AD service principal" + type = string +} + +variable "azure_tenant_id" { + description = "Azure AD Tenant ID" + type = string +} + +variable "authorize_all_pipelines" { + description = "Automatically authorize all pipelines to use this service connection" + type = bool + default = false +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf b/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf new file mode 100644 index 0000000..6201b42 --- /dev/null +++ b/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.51.0" + } + azuredevops = { + source = "microsoft/azuredevops" + version = "~> 1.1.1" + } + } +} diff --git a/modules/ionos/user-management/buildingblock/scripts/README.md b/modules/ionos/user-management/buildingblock/scripts/README.md deleted file mode 100644 index 48d7573..0000000 --- a/modules/ionos/user-management/buildingblock/scripts/README.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: IONOS User Management Scripts -supportedPlatforms: - - ionos -description: Helper scripts for detecting existing users and managing IONOS Cloud user creation with smart detection capabilities. -category: utilities ---- - -# User Management Scripts - -This directory contains helper scripts for the IONOS user management module. - -## Scripts - -### `check_user_exists.sh` -Checks if a user exists in IONOS Cloud by email address. - -**Usage:** -```bash -./check_user_exists.sh "user@example.com" -``` - -**Requirements:** -- `IONOS_TOKEN` environment variable set -- `curl` command available -- `jq` command available - -**Output:** -Returns JSON with user existence information: -```json -{ - "exists": "true|false", - "user_id": "user-id-if-exists", - "error": "error-message-if-any" -} -``` - -### Installing jq - -**macOS:** -```bash -brew install jq -``` - -**Ubuntu/Debian:** -```bash -sudo apt-get install jq -``` - -**CentOS/RHEL:** -```bash -sudo yum install jq -``` - -## Environment Variables - -- `IONOS_TOKEN` - Your IONOS API token with user management permissions \ No newline at end of file