diff --git a/.gitignore b/.gitignore index 1dc5e7e..ccec098 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,5 @@ yarn-error.log* *.terraform* *.tfstate* *tfvars* - .terraform.lock.hcl .env diff --git a/modules/AGENTS.md b/modules/AGENTS.md index 288e01b..8f51125 100644 --- a/modules/AGENTS.md +++ b/modules/AGENTS.md @@ -55,18 +55,25 @@ aws/ ## Provider Version Strategy -**Pinning Guidelines:** -- **Use `~>` for stable APIs:** AWS (`~> 5.0`), Azure (`~> 3.116.0`) -- **Use exact versions for frequent breaking changes:** Google (`6.12.0`) -- **Review provider versions quarterly** to stay current with security patches -- **Exception:** Pin to exact versions when a specific feature is required - -**Current Latest Versions:** -- AWS Provider: `~> 5.0` -- Azure Provider: `~> 3.116.0` -- Google Provider: `6.12.0` (exact due to API volatility) -- SAP BTP Provider: `~> 1.8.0` -- Time Provider: `~> 0.11.1` +**Provider versions are module-specific, not repository-wide.** Each module should declare the minimum provider version it requires based on testing and feature needs. + +**Version Selection Criteria:** + +When choosing a provider version for a module, consider: + +1. **Feature Requirements** - Does the module need specific APIs/resources from newer versions? +2. **Testing Validation** - Which version has been tested with this module? +3. **Breaking Changes** - Are there known breaking changes to avoid? +4. **Stability** - Prefer versions with `~>` for patch updates unless there's a specific reason +5. **Backwards Compatibility** - Will this work with existing deployments? + +**Version Constraint Best Practices:** + +- **Use `~> X.Y.Z`** to allow patch updates (recommended for most cases) +- **Use exact versions** (`X.Y.Z`) only for providers with frequent breaking changes +- **Document in the module's README** why a specific version is required +- **Test against specific versions** - Each module should be validated with the provider version it declares +- **Review provider versions quarterly** to stay current with security patches and new features ## Terraform Version Requirements @@ -273,4 +280,4 @@ category: storage - [ ] Shared responsibility matrix documented - [ ] Cross-provider consistency maintained -This comprehensive guide ensures consistency and quality across all building block modules in the multi-cloud platform. \ No newline at end of file +This comprehensive guide ensures consistency and quality across all building block modules in the multi-cloud platform. diff --git a/modules/azure/aks/backplane/README.md b/modules/azure/aks/backplane/README.md new file mode 100644 index 0000000..2a6036a --- /dev/null +++ b/modules/azure/aks/backplane/README.md @@ -0,0 +1,86 @@ +# AKS Cluster + +This documentation is intended as a reference documentation for cloud foundation or platform engineers using this module. + +## Permissions + +This is a very simple building block, which means we let the SPN have access to AKS Clusters +across all subscriptions underneath a management group (typically the top-level management group for landing zones). + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.36.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azuread_application.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_application.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_application_federated_identity_credential.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | +| [azuread_application_federated_identity_credential.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | +| [azuread_application_password.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_password) | resource | +| [azuread_application_password.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_password) | resource | +| [azuread_service_principal.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azuread_service_principal.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azurerm_role_assignment.created_principal](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.created_principal_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.created_principal_hub_to_landingzone](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.created_principal_landingzone_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals_hub_to_landingzone](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals_landingzone_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_role_definition.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_role_definition.buildingblock_hub_to_landingzone](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_role_definition.buildingblock_landingzone_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [create\_hub\_service\_principal\_name](#input\_create\_hub\_service\_principal\_name) | name of a separate service principal to create for hub VNet peering (least privilege) | `string` | `null` | no | +| [create\_service\_principal\_name](#input\_create\_service\_principal\_name) | name of a service principal to create and grant permissions to deploy the building block | `string` | `null` | no | +| [existing\_hub\_principal\_ids](#input\_existing\_hub\_principal\_ids) | set of existing principal ids that will be granted permissions to peer with the hub VNet | `set(string)` | `[]` | no | +| [existing\_principal\_ids](#input\_existing\_principal\_ids) | set of existing principal ids that will be granted permissions to deploy the building block | `set(string)` | `[]` | no | +| [hub\_scope](#input\_hub\_scope) | Scope for hub VNet peering permissions (management group or subscription). Typically a hub subscription, but can be a management group containing hub resources. | `string` | n/a | yes | +| [hub\_workload\_identity\_federation](#input\_hub\_workload\_identity\_federation) | Configuration for workload identity federation for hub service principal. If not provided, an application password will be created instead. |
object({
issuer = string
subject = string
})
| `null` | no | +| [name](#input\_name) | name of the building block, used for naming resources | `string` | `"aks"` | no | +| [scope](#input\_scope) | Scope where the building block should be deployable (management group or subscription), typically the parent of all Landing Zones. | `string` | n/a | yes | +| [workload\_identity\_federation](#input\_workload\_identity\_federation) | Configuration for workload identity federation. If not provided, an application password will be created instead. |
object({
issuer = string
subject = string
})
| `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [application\_password](#output\_application\_password) | Information about the created application password (excludes the actual password value for security). | +| [created\_application](#output\_created\_application) | Information about the created Azure AD application. | +| [created\_hub\_application](#output\_created\_hub\_application) | Information about the created hub Azure AD application. | +| [created\_hub\_service\_principal](#output\_created\_hub\_service\_principal) | Information about the created hub service principal. | +| [created\_service\_principal](#output\_created\_service\_principal) | Information about the created service principal. | +| [documentation\_md](#output\_documentation\_md) | Markdown documentation with information about the AKS Building Block building block backplane | +| [hub\_application\_password](#output\_hub\_application\_password) | Information about the created hub application password (excludes the actual password value for security). | +| [hub\_role\_assignment\_ids](#output\_hub\_role\_assignment\_ids) | The IDs of the hub role assignments for all service principals. | +| [hub\_role\_assignment\_principal\_ids](#output\_hub\_role\_assignment\_principal\_ids) | The principal IDs of all service principals that have been assigned the hub role. | +| [hub\_role\_definition\_id](#output\_hub\_role\_definition\_id) | The ID of the role definition that enables deployment of the building block to the hub. | +| [hub\_role\_definition\_name](#output\_hub\_role\_definition\_name) | The name of the role definition that enables deployment of the building block to the hub. | +| [hub\_scope](#output\_hub\_scope) | The scope (management group or subscription) where VNet peering role is applied. | +| [hub\_workload\_identity\_federation](#output\_hub\_workload\_identity\_federation) | Information about the created hub workload identity federation credential. | +| [provider\_tf](#output\_provider\_tf) | Ready-to-use provider.tf configuration for buildingblock deployment | +| [role\_assignment\_ids](#output\_role\_assignment\_ids) | The IDs of the role assignments for all service principals. | +| [role\_assignment\_principal\_ids](#output\_role\_assignment\_principal\_ids) | The principal IDs of all service principals that have been assigned the role. | +| [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the building block. | +| [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the building block. | +| [scope](#output\_scope) | The scope where the role definition and role assignments are applied. | +| [workload\_identity\_federation](#output\_workload\_identity\_federation) | Information about the created workload identity federation credential. | + diff --git a/modules/azure/aks/backplane/documentation.tf b/modules/azure/aks/backplane/documentation.tf new file mode 100644 index 0000000..eb9bbbc --- /dev/null +++ b/modules/azure/aks/backplane/documentation.tf @@ -0,0 +1,18 @@ +output "documentation_md" { + value = <", formatlist("- `%s`", azurerm_role_definition.buildingblock_deploy.permissions[0].actions))} | + +EOF + description = "Markdown documentation with information about the AKS Building Block building block backplane" +} diff --git a/modules/azure/aks/backplane/main.tf b/modules/azure/aks/backplane/main.tf new file mode 100644 index 0000000..32e57da --- /dev/null +++ b/modules/azure/aks/backplane/main.tf @@ -0,0 +1,239 @@ +data "azurerm_subscription" "current" {} + +resource "azuread_application" "buildingblock_deploy" { + count = var.create_service_principal_name != null ? 1 : 0 + + display_name = "${var.name}-${var.create_service_principal_name}" +} + +resource "azuread_service_principal" "buildingblock_deploy" { + count = var.create_service_principal_name != null ? 1 : 0 + + client_id = azuread_application.buildingblock_deploy[0].client_id + app_role_assignment_required = false +} + +resource "azuread_application_federated_identity_credential" "buildingblock_deploy" { + count = var.create_service_principal_name != null && var.workload_identity_federation != null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy[0].id + display_name = var.create_service_principal_name + audiences = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = var.workload_identity_federation.subject +} + +resource "azuread_application_password" "buildingblock_deploy" { + count = var.create_service_principal_name != null && var.workload_identity_federation == null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy[0].id + display_name = "${var.create_service_principal_name}-password" +} + +resource "azuread_application" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + display_name = "${var.name}-${var.create_hub_service_principal_name}" +} + +resource "azuread_service_principal" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + client_id = azuread_application.buildingblock_deploy_hub[0].client_id + app_role_assignment_required = false +} + +resource "azuread_application_federated_identity_credential" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation != null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy_hub[0].id + display_name = var.create_hub_service_principal_name + audiences = ["api://AzureADTokenExchange"] + issuer = var.hub_workload_identity_federation.issuer + subject = var.hub_workload_identity_federation.subject +} + +resource "azuread_application_password" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation == null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy_hub[0].id + display_name = "${var.create_hub_service_principal_name}-password" +} + +resource "azurerm_role_definition" "buildingblock_deploy" { + name = "${var.name}-deploy" + scope = var.scope + description = "Enables deployment of the ${var.name} building block to subscriptions" + + permissions { + actions = [ + "Microsoft.ContainerService/managedClusters/read", + "Microsoft.ContainerService/managedClusters/write", + "Microsoft.ContainerService/managedClusters/delete", + "Microsoft.ContainerService/managedClusters/listClusterAdminCredential/action", + "Microsoft.ContainerService/managedClusters/listClusterUserCredential/action", + "Microsoft.ContainerService/managedClusters/listClusterMonitoringUserCredential/action", + "Microsoft.ContainerService/managedClusters/accessProfiles/listCredential/action", + "Microsoft.ContainerService/managedClusters/accessProfiles/read", + "Microsoft.ContainerService/managedClusters/agentPools/read", + "Microsoft.ContainerService/managedClusters/agentPools/write", + "Microsoft.ContainerService/managedClusters/agentPools/delete", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/write", + "Microsoft.Network/virtualNetworks/delete", + "Microsoft.Network/virtualNetworks/subnets/read", + "Microsoft.Network/virtualNetworks/subnets/write", + "Microsoft.Network/virtualNetworks/subnets/delete", + "Microsoft.Network/virtualNetworks/subnets/join/action", + "Microsoft.Network/virtualNetworks/join/action", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete", + "Microsoft.Network/virtualNetworks/peer/action", + "Microsoft.Network/networkInterfaces/read", + "Microsoft.Network/networkSecurityGroups/read", + "Microsoft.Network/networkSecurityGroups/write", + "Microsoft.Network/networkSecurityGroups/delete", + "Microsoft.Network/publicIPAddresses/read", + "Microsoft.Network/publicIPAddresses/write", + "Microsoft.Network/publicIPAddresses/delete", + "Microsoft.Network/loadBalancers/read", + "Microsoft.Network/loadBalancers/write", + "Microsoft.Network/loadBalancers/delete", + "Microsoft.Network/routeTables/read", + "Microsoft.Network/routeTables/write", + "Microsoft.Network/routeTables/delete", + "Microsoft.Network/routeTables/join/action", + "Microsoft.Network/privateDnsZones/read", + "Microsoft.Network/privateDnsZones/write", + "Microsoft.Network/privateDnsZones/delete", + "Microsoft.Network/privateDnsZones/virtualNetworkLinks/read", + "Microsoft.Network/privateDnsZones/virtualNetworkLinks/write", + "Microsoft.Network/privateDnsZones/virtualNetworkLinks/delete", + "Microsoft.Resources/deployments/read", + "Microsoft.Resources/deployments/write", + "Microsoft.Resources/deployments/delete", + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Resources/subscriptions/resourceGroups/write", + "Microsoft.Resources/subscriptions/resourceGroups/delete", + "Microsoft.OperationalInsights/workspaces/read", + "Microsoft.OperationalInsights/workspaces/write", + "Microsoft.OperationalInsights/workspaces/delete", + "Microsoft.OperationalInsights/workspaces/sharedKeys/action", + "Microsoft.Insights/diagnosticSettings/read", + "Microsoft.Insights/diagnosticSettings/write", + "Microsoft.Insights/diagnosticSettings/delete", + "Microsoft.Authorization/roleAssignments/read" + ] + } +} + +resource "azurerm_role_assignment" "existing_principals" { + for_each = var.existing_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_deploy.role_definition_resource_id + principal_id = each.value + scope = var.scope +} + +resource "azurerm_role_assignment" "created_principal" { + count = var.create_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_deploy.role_definition_resource_id + principal_id = azuread_service_principal.buildingblock_deploy[0].object_id + scope = var.scope +} + +resource "azurerm_role_definition" "buildingblock_deploy_hub" { + name = "${var.name}-deploy-hub" + description = "Enables deployment of the ${var.name} building block to the hub (for private cluster peering)" + scope = var.hub_scope + + permissions { + actions = [ + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete", + "Microsoft.Network/virtualNetworks/peer/action", + ] + } +} + +resource "azurerm_role_definition" "buildingblock_hub_to_landingzone" { + name = "${var.name}-hub-to-landingzone" + description = "Allows hub service principal to peer back to landing zone vnets" + scope = var.scope + + permissions { + actions = [ + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/peer/action", + ] + } +} + +resource "azurerm_role_definition" "buildingblock_landingzone_to_hub" { + name = "${var.name}-landingzone-to-hub" + description = "Allows landing zone service principal to peer to hub vnets" + scope = var.hub_scope + + permissions { + actions = [ + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/peer/action", + ] + } +} + +resource "azurerm_role_assignment" "existing_principals_hub" { + for_each = var.existing_hub_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_deploy_hub.role_definition_resource_id + description = azurerm_role_definition.buildingblock_deploy_hub.description + principal_id = each.value + scope = var.hub_scope +} + +resource "azurerm_role_assignment" "created_principal_hub" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_deploy_hub.role_definition_resource_id + description = azurerm_role_definition.buildingblock_deploy_hub.description + principal_id = azuread_service_principal.buildingblock_deploy_hub[0].object_id + scope = var.hub_scope +} + +resource "azurerm_role_assignment" "existing_principals_hub_to_landingzone" { + for_each = var.existing_hub_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_hub_to_landingzone.role_definition_resource_id + principal_id = each.value + scope = var.scope +} + +resource "azurerm_role_assignment" "created_principal_hub_to_landingzone" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_hub_to_landingzone.role_definition_resource_id + principal_id = azuread_service_principal.buildingblock_deploy_hub[0].object_id + scope = var.scope +} + +resource "azurerm_role_assignment" "existing_principals_landingzone_to_hub" { + for_each = var.existing_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_landingzone_to_hub.role_definition_resource_id + principal_id = each.value + scope = var.hub_scope +} + +resource "azurerm_role_assignment" "created_principal_landingzone_to_hub" { + count = var.create_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_landingzone_to_hub.role_definition_resource_id + principal_id = azuread_service_principal.buildingblock_deploy[0].object_id + scope = var.hub_scope +} diff --git a/modules/azure/aks/backplane/outputs.tf b/modules/azure/aks/backplane/outputs.tf new file mode 100644 index 0000000..d5113f5 --- /dev/null +++ b/modules/azure/aks/backplane/outputs.tf @@ -0,0 +1,195 @@ +output "role_definition_id" { + value = azurerm_role_definition.buildingblock_deploy.id + description = "The ID of the role definition that enables deployment of the building block." +} + +output "role_definition_name" { + value = azurerm_role_definition.buildingblock_deploy.name + description = "The name of the role definition that enables deployment of the building block." +} + +output "role_assignment_ids" { + value = concat( + [for id in azurerm_role_assignment.existing_principals : id.id], + var.create_service_principal_name != null ? [azurerm_role_assignment.created_principal[0].id] : [] + ) + description = "The IDs of the role assignments for all service principals." +} + +output "role_assignment_principal_ids" { + value = concat( + [for id in azurerm_role_assignment.existing_principals : id.principal_id], + var.create_service_principal_name != null ? [azurerm_role_assignment.created_principal[0].principal_id] : [] + ) + description = "The principal IDs of all service principals that have been assigned the role." +} + +output "created_service_principal" { + value = var.create_service_principal_name != null ? { + object_id = azuread_service_principal.buildingblock_deploy[0].object_id + client_id = azuread_service_principal.buildingblock_deploy[0].client_id + display_name = azuread_service_principal.buildingblock_deploy[0].display_name + name = var.create_service_principal_name + } : null + description = "Information about the created service principal." +} + +output "created_application" { + value = var.create_service_principal_name != null ? { + object_id = azuread_application.buildingblock_deploy[0].object_id + client_id = azuread_application.buildingblock_deploy[0].client_id + display_name = azuread_application.buildingblock_deploy[0].display_name + } : null + description = "Information about the created Azure AD application." +} + +output "workload_identity_federation" { + value = var.create_service_principal_name != null && var.workload_identity_federation != null ? { + credential_id = azuread_application_federated_identity_credential.buildingblock_deploy[0].credential_id + display_name = azuread_application_federated_identity_credential.buildingblock_deploy[0].display_name + issuer = azuread_application_federated_identity_credential.buildingblock_deploy[0].issuer + subject = azuread_application_federated_identity_credential.buildingblock_deploy[0].subject + audiences = azuread_application_federated_identity_credential.buildingblock_deploy[0].audiences + } : null + description = "Information about the created workload identity federation credential." +} + +output "application_password" { + value = var.create_service_principal_name != null && var.workload_identity_federation == null ? { + key_id = azuread_application_password.buildingblock_deploy[0].key_id + display_name = azuread_application_password.buildingblock_deploy[0].display_name + } : null + description = "Information about the created application password (excludes the actual password value for security)." + sensitive = true +} + +output "scope" { + value = var.scope + description = "The scope where the role definition and role assignments are applied." +} + +output "hub_scope" { + value = var.hub_scope + description = "The scope (management group or subscription) where VNet peering role is applied." +} + +output "hub_role_definition_id" { + value = azurerm_role_definition.buildingblock_deploy_hub.id + description = "The ID of the role definition that enables deployment of the building block to the hub." +} + +output "hub_role_definition_name" { + value = azurerm_role_definition.buildingblock_deploy_hub.name + description = "The name of the role definition that enables deployment of the building block to the hub." +} + +output "hub_role_assignment_ids" { + value = concat( + [for id in azurerm_role_assignment.existing_principals_hub : id.id], + var.create_hub_service_principal_name != null ? [azurerm_role_assignment.created_principal_hub[0].id] : [] + ) + description = "The IDs of the hub role assignments for all service principals." +} + +output "hub_role_assignment_principal_ids" { + value = concat( + [for id in azurerm_role_assignment.existing_principals_hub : id.principal_id], + var.create_hub_service_principal_name != null ? [azurerm_role_assignment.created_principal_hub[0].principal_id] : [] + ) + description = "The principal IDs of all service principals that have been assigned the hub role." +} + +output "created_hub_service_principal" { + value = var.create_hub_service_principal_name != null ? { + object_id = azuread_service_principal.buildingblock_deploy_hub[0].object_id + client_id = azuread_service_principal.buildingblock_deploy_hub[0].client_id + display_name = azuread_service_principal.buildingblock_deploy_hub[0].display_name + name = var.create_hub_service_principal_name + } : null + description = "Information about the created hub service principal." +} + +output "created_hub_application" { + value = var.create_hub_service_principal_name != null ? { + object_id = azuread_application.buildingblock_deploy_hub[0].object_id + client_id = azuread_application.buildingblock_deploy_hub[0].client_id + display_name = azuread_application.buildingblock_deploy_hub[0].display_name + } : null + description = "Information about the created hub Azure AD application." +} + +output "hub_workload_identity_federation" { + value = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation != null ? { + credential_id = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].credential_id + display_name = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].display_name + issuer = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].issuer + subject = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].subject + audiences = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].audiences + } : null + description = "Information about the created hub workload identity federation credential." +} + +output "hub_application_password" { + value = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation == null ? { + key_id = azuread_application_password.buildingblock_deploy_hub[0].key_id + display_name = azuread_application_password.buildingblock_deploy_hub[0].display_name + } : null + description = "Information about the created hub application password (excludes the actual password value for security)." + sensitive = true +} + +output "provider_tf" { + value = var.create_service_principal_name != null && var.create_hub_service_principal_name != null ? ( + var.workload_identity_federation == null && var.hub_workload_identity_federation == null ? <<-EOT + provider "azurerm" { + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy[0].client_id}" + client_secret = "${azuread_application_password.buildingblock_deploy[0].value}" + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + + provider "azurerm" { + alias = "hub" + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy_hub[0].client_id}" + client_secret = "${azuread_application_password.buildingblock_deploy_hub[0].value}" + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + EOT + : <<-EOT + terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.36.0" + } + } + } + + provider "azurerm" { + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy[0].client_id}" + use_oidc = true + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + + provider "azurerm" { + alias = "hub" + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy_hub[0].client_id}" + use_oidc = true + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + EOT + ) : null + description = "Ready-to-use provider.tf configuration for buildingblock deployment" + sensitive = true +} diff --git a/modules/azure/aks/backplane/provider.tf b/modules/azure/aks/backplane/provider.tf new file mode 100644 index 0000000..ab91b24 --- /dev/null +++ b/modules/azure/aks/backplane/provider.tf @@ -0,0 +1,3 @@ +provider "azurerm" { + features {} +} diff --git a/modules/azure/aks/backplane/variables.tf b/modules/azure/aks/backplane/variables.tf new file mode 100644 index 0000000..ec16ea5 --- /dev/null +++ b/modules/azure/aks/backplane/variables.tf @@ -0,0 +1,74 @@ +variable "name" { + type = string + nullable = false + description = "name of the building block, used for naming resources" + default = "aks" + validation { + condition = can(regex("^[-a-z0-9]+$", var.name)) + error_message = "Only alphanumeric lowercase characters and dashes are allowed" + } +} + +variable "scope" { + type = string + nullable = false + description = "Scope where the building block should be deployable (management group or subscription), typically the parent of all Landing Zones." +} + +variable "hub_scope" { + type = string + nullable = false + description = "Scope for hub VNet peering permissions (management group or subscription). Typically a hub subscription, but can be a management group containing hub resources." +} + +variable "existing_principal_ids" { + type = set(string) + default = [] + description = "set of existing principal ids that will be granted permissions to deploy the building block" +} + +variable "existing_hub_principal_ids" { + type = set(string) + default = [] + description = "set of existing principal ids that will be granted permissions to peer with the hub VNet" +} + +variable "create_service_principal_name" { + type = string + default = null + description = "name of a service principal to create and grant permissions to deploy the building block" + + validation { + condition = var.create_service_principal_name == null ? true : can(regex("^[-a-zA-Z0-9_]+$", var.create_service_principal_name)) + error_message = "Service principal name can only contain alphanumeric characters, hyphens, and underscores" + } +} + +variable "create_hub_service_principal_name" { + type = string + default = null + description = "name of a separate service principal to create for hub VNet peering (least privilege)" + + validation { + condition = var.create_hub_service_principal_name == null ? true : can(regex("^[-a-zA-Z0-9_]+$", var.create_hub_service_principal_name)) + error_message = "Service principal name can only contain alphanumeric characters, hyphens, and underscores" + } +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subject = string + }) + default = null + description = "Configuration for workload identity federation. If not provided, an application password will be created instead." +} + +variable "hub_workload_identity_federation" { + type = object({ + issuer = string + subject = string + }) + default = null + description = "Configuration for workload identity federation for hub service principal. If not provided, an application password will be created instead." +} diff --git a/modules/azure/aks/backplane/versions.tf b/modules/azure/aks/backplane/versions.tf new file mode 100644 index 0000000..fbbc570 --- /dev/null +++ b/modules/azure/aks/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.36.0" + } + } +} diff --git a/modules/azure/aks/buildingblock/APP_TEAM_README.md b/modules/azure/aks/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..5fe2303 --- /dev/null +++ b/modules/azure/aks/buildingblock/APP_TEAM_README.md @@ -0,0 +1,119 @@ +# Azure Kubernetes Service (AKS) + +## Description +This building block provides a production-grade Azure Kubernetes Service (AKS) cluster with integrated security, monitoring, and networking features. It delivers a fully managed Kubernetes environment with Azure AD authentication, workload identity support, and comprehensive observability through Log Analytics. The cluster supports both public and private deployment scenarios with optional hub-and-spoke network connectivity. + +## Usage Motivation +This building block is for application teams that need to deploy containerized applications on a secure, scalable, and managed Kubernetes platform. The AKS cluster comes pre-configured with enterprise-grade security features, eliminating the operational complexity of managing Kubernetes infrastructure while maintaining the flexibility to run any containerized workload. + +## 🚀 Usage Examples +- A development team deploys microservices-based applications using Kubernetes deployments, services, and ingress controllers. +- A data engineering team runs distributed data processing workloads using Kubernetes jobs and cron jobs. +- An operations team manages multi-tenant applications with namespace isolation and resource quotas. +- A DevOps team implements GitOps-based continuous deployment pipelines targeting the AKS cluster. + +## 🔄 Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|--------------|-----------------| +| Provisioning and configuring the AKS cluster | ✅ | ❌ | +| Managing cluster upgrades and patches | ✅ | ❌ | +| Configuring Azure AD authentication and RBAC | ✅ | ❌ | +| Setting up Log Analytics and monitoring infrastructure | ✅ | ❌ | +| Managing virtual network and subnet configuration | ✅ | ❌ | +| Managing hub network peering (for private clusters) | ✅ | ❌ | +| Deploying and managing applications and workloads | ❌ | ✅ | +| Configuring application-level resource limits and quotas | ❌ | ✅ | +| Managing Kubernetes namespaces and RBAC within the cluster | ❌ | ✅ | +| Monitoring application performance and logs | ❌ | ✅ | +| Implementing application security policies (Network Policies, Pod Security) | ❌ | ✅ | + +## 💡 Best Practices for Secure and Efficient AKS Usage + +### Security +- **Use Workload Identity**: Leverage Azure AD Workload Identity for secure authentication to Azure resources without storing credentials +- **Implement Network Policies**: Define Kubernetes Network Policies to control pod-to-pod communication +- **Enable Pod Security Standards**: Apply Kubernetes Pod Security Standards to enforce security best practices +- **Use Azure Key Vault**: Store secrets in Azure Key Vault and inject them into pods using CSI Secret Store driver +- **Scan container images**: Regularly scan container images for vulnerabilities before deployment + +### Performance & Scalability +- **Set resource requests and limits**: Always define CPU and memory requests/limits for predictable scheduling and resource management +- **Use Horizontal Pod Autoscaler (HPA)**: Implement HPA to automatically scale applications based on metrics +- **Optimize container images**: Use multi-stage builds and minimal base images to reduce image size and startup time +- **Implement health probes**: Configure liveness and readiness probes for reliable application health monitoring + +### Operations & Monitoring +- **Use structured logging**: Implement structured logging (JSON) for better log analysis in Log Analytics +- **Monitor cluster metrics**: Regularly review cluster metrics in Azure Monitor for capacity planning +- **Implement GitOps**: Use GitOps tools like Flux or ArgoCD for declarative application deployment +- **Tag resources**: Use labels and annotations consistently for resource organization and cost allocation + +### Networking +- **Use Ingress controllers**: Deploy an Ingress controller for HTTP/HTTPS routing instead of multiple LoadBalancer services +- **Implement egress control**: Use Azure Firewall or Network Security Groups to control outbound traffic +- **Enable service mesh**: Consider using a service mesh (like Istio or Linkerd) for advanced traffic management and observability + +## Cluster Features + +### Authentication & Authorization +- **Azure AD Integration**: Cluster uses Azure AD for authentication, enabling centralized identity management (optional) +- **OIDC Issuer**: Workload Identity enabled for secure pod-to-Azure-resource authentication + +### Monitoring & Logging +- **Log Analytics Workspace**: Centralized logging for cluster and application logs (optional) +- **Container Insights**: Integrated monitoring for container performance and health +- **Diagnostic Settings**: Cluster metrics and logs forwarded to Log Analytics + +### Networking +- **Flexible VNet Options**: + - Create new VNet and subnet automatically (default) + - Use existing VNet and subnet (for shared platform networking) +- **Azure CNI**: Advanced networking capabilities with pod-level networking +- **Private Cluster**: Optional private API server accessible only via private endpoint +- **Hub Connectivity**: Optional VNet peering to central hub network for on-premises connectivity + - Only created when deploying with new VNet (`vnet_name == null`) + - Use existing VNet scenario for centrally-managed peering + +### Auto-Scaling +- **Cluster Autoscaler**: Automatically adjusts node count based on resource requirements (when enabled) +- **System Node Pool**: Dedicated node pool for system workloads with optional auto-scaling + +## Configuration Variables + +### Networking Configuration + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `vnet_name` | Name of existing VNet to use. If `null`, creates new VNet. | No | `null` (creates new) | +| `existing_vnet_resource_group_name` | Resource group of existing VNet. Only used when `vnet_name` is provided. | No | Same as AKS RG | +| `subnet_name` | Name of existing subnet to use. If `null`, creates new subnet. | No | `null` (creates new) | +| `vnet_address_space` | Address space for new VNet. Only used when `vnet_name == null`. | No | `10.240.0.0/16` | +| `subnet_address_prefix` | Address prefix for new subnet. Only used when `subnet_name == null`. | No | `10.240.0.0/20` | +| `allow_gateway_transit_from_hub` | Allow gateway transit from hub for on-premises connectivity. | No | `false` | + +### Hub Connectivity (for Private Clusters) + +| Variable | Description | Required | Default | +|----------|-------------|----------|---------| +| `hub_subscription_id` | Subscription ID of hub network. Required for hub peering. | Conditional | `null` | +| `hub_resource_group_name` | Resource group of hub VNet. Required for hub peering. | Conditional | `null` | +| `hub_vnet_name` | Name of hub VNet to peer with. Set to `null` to disable peering. | No | `null` | + +**Note:** Hub peering is **only created when `vnet_name == null`** (new VNet scenario). If using an existing VNet, peering must be managed externally. + +## Getting Started + +### Public Cluster +1. **Access the cluster**: Use `az aks get-credentials --resource-group --name ` to configure kubectl access +2. **Verify connectivity**: Run `kubectl get nodes` to confirm cluster connectivity +3. **Deploy your application**: Use `kubectl apply` or Helm to deploy applications +4. **Monitor your workloads**: View logs and metrics in Azure Monitor or Log Analytics + +### Private Cluster +1. **Ensure network connectivity**: Access must be from a network peered with the AKS VNet or the hub network +2. **Access the cluster**: Use `az aks get-credentials --resource-group --name ` from a machine with network access +3. **Use Azure Bastion or VPN**: Connect via Azure Bastion, VPN Gateway, or ExpressRoute for management access +4. **Verify connectivity**: Run `kubectl get nodes` to confirm cluster connectivity +5. **Deploy your application**: Use `kubectl apply` or Helm to deploy applications +6. **Monitor your workloads**: View logs and metrics in Azure Monitor or Log Analytics diff --git a/modules/azure/aks/buildingblock/README.md b/modules/azure/aks/buildingblock/README.md new file mode 100644 index 0000000..4788a76 --- /dev/null +++ b/modules/azure/aks/buildingblock/README.md @@ -0,0 +1,272 @@ +--- +name: AKS Cluster +supportedPlatforms: + - azure +description: | + Provision a production-grade Azure Kubernetes Service (AKS) cluster with Azure AD, OIDC, Workload Identity, Log Analytics and custom VNet using Terraform. +category: compute +--- + +# AKS Building Block + +This Terraform module provisions a production-ready [Azure Kubernetes Service (AKS)](https://learn.microsoft.com/en-us/azure/aks/) cluster including: + +- Azure AD-based authentication +- Workload Identity & OIDC issuer enabled +- Flexible networking: Create new or use existing VNet/subnet +- Optional hub VNet peering for private clusters +- Log Analytics integration (Monitoring) +- Auto-scaling node pool (optional) +- System-assigned managed identity + +## 🚀 Features + +- ✅ Production-grade configuration +- 🔐 Integrated Azure AD admin group +- ☁️ Log Analytics Workspace (LAW) with comprehensive diagnostics +- 🧠 OIDC issuer & Workload Identity support +- 🌐 Flexible virtual network configuration (new or existing) +- 🔗 Optional bi-directional hub VNet peering for private clusters +- 📈 Optional auto-scaling for system node pool +- 🔧 Configurable network plugins (Azure CNI, Kubenet) and policies (Azure, Calico, Cilium) + +## Deployment Scenarios +### Scenario 1: New VNet with Hub Peering (Private AKS with On-Premises Connectivity) + +**Use Case:** Private AKS cluster with connectivity to on-premises networks via hub VNet + +```hcl +provider "azurerm" { + alias = "hub" + subscription_id = "hub-subscription-id" + # hub credentials +} + +module "aks" { + source = "./buildingblock" + + providers = { + azurerm = azurerm + azurerm.hub = azurerm.hub + } + + aks_cluster_name = "my-private-aks" + resource_group_name = "aks-rg" + location = "West Europe" + + # Private cluster settings + private_cluster_enabled = true + private_dns_zone_id = "System" + private_cluster_public_fqdn_enabled = false + + # New VNet will be created automatically + vnet_address_space = "10.240.0.0/16" + subnet_address_prefix = "10.240.0.0/20" + + # Hub connectivity (creates bi-directional peering) + hub_subscription_id = "hub-subscription-id" + hub_resource_group_name = "hub-network-rg" + hub_vnet_name = "hub-vnet" + allow_gateway_transit_from_hub = true + + # Azure AD and monitoring + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "my-law" +} +``` + +**Key Points:** +- ✅ Module creates new VNet and subnet automatically +- ✅ Bi-directional VNet peering established with hub +- ✅ Private API server accessible via hub network +- ✅ Gateway transit enabled for on-premises connectivity + +--- + +### Scenario 2: Existing Shared VNet (Reuse Platform Networking) + +**Use Case:** Deploy AKS into existing VNet managed by platform team + +```hcl +module "aks" { + source = "./buildingblock" + + aks_cluster_name = "my-shared-aks" + resource_group_name = "aks-rg" + location = "West Europe" + + # Use existing VNet and subnet + vnet_name = "platform-shared-vnet" + existing_vnet_resource_group_name = "networking-rg" + subnet_name = "aks-subnet" + + # Private cluster settings + private_cluster_enabled = true + private_dns_zone_id = "System" + private_cluster_public_fqdn_enabled = false + + # No hub peering - existing VNet owner manages peering + hub_vnet_name = null + + # Azure AD and monitoring + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "my-law" +} +``` + +**Key Points:** +- ✅ No VNet or subnet created (uses existing) +- ✅ No peering created by this module +- ✅ Platform/networking team controls VNet peering centrally +- ✅ Requires pre-existing subnet with adequate address space + +--- + +### Scenario 3: Private Isolated (No Hub Connectivity) + +**Use Case:** Standalone private AKS cluster without external connectivity + +```hcl +module "aks" { + source = "./buildingblock" + + aks_cluster_name = "my-isolated-aks" + resource_group_name = "aks-rg" + location = "West Europe" + + # Private cluster settings + private_cluster_enabled = true + private_dns_zone_id = "System" + private_cluster_public_fqdn_enabled = false + + # New VNet will be created automatically + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.0.0/20" + + # No hub connectivity + hub_vnet_name = null + hub_resource_group_name = null + hub_subscription_id = null + + # Azure AD and monitoring + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "my-law" +} +``` + +**Key Points:** +- ✅ Module creates new VNet and subnet +- ✅ No VNet peering created +- ✅ Fully isolated environment +- ✅ Suitable for dev/test environments + +--- + +### Scenario 4: Public Cluster (Default) + +**Use Case:** Public AKS cluster for non-sensitive workloads + +```hcl +module "aks" { + source = "./buildingblock" + + aks_cluster_name = "my-public-aks" + resource_group_name = "aks-rg" + location = "West Europe" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "my-law" + + # Public cluster (default settings) + private_cluster_enabled = false + + # New VNet will be created automatically + vnet_address_space = "10.240.0.0/16" + subnet_address_prefix = "10.240.0.0/20" +} +``` + +**Key Points:** +- ✅ API server publicly accessible +- ✅ Module creates new VNet and subnet +- ✅ No hub connectivity required +- ✅ Simplest deployment option + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.36.0 | +| [time](#requirement\_time) | ~> 0.11.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [azurerm_kubernetes_cluster.aks](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster) | resource | +| [azurerm_log_analytics_workspace.law](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/log_analytics_workspace) | resource | +| [azurerm_monitor_diagnostic_setting.aks_monitoring](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_diagnostic_setting) | resource | +| [azurerm_resource_group.aks](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_route_table.aks_rt](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/route_table) | resource | +| [azurerm_subnet.aks_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource | +| [azurerm_subnet_route_table_association.aks_subnet_rt](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) | resource | +| [azurerm_virtual_network.vnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network) | resource | +| [azurerm_virtual_network_peering.aks_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network_peering) | resource | +| [azurerm_virtual_network_peering.hub_to_aks](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network_peering) | resource | +| [time_sleep.wait_for_subnet](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [azurerm_resource_group.hub_rg](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group) | data source | +| [azurerm_subnet.existing_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subnet) | data source | +| [azurerm_virtual_network.existing_vnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/virtual_network) | data source | +| [azurerm_virtual_network.hub_vnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/virtual_network) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [aks\_admin\_group\_object\_id](#input\_aks\_admin\_group\_object\_id) | Object ID of the Azure AD group used for AKS admin access. If null, Azure AD RBAC will not be configured. | `string` | `null` | no | +| [aks\_cluster\_name](#input\_aks\_cluster\_name) | Name of the AKS cluster | `string` | `"prod-aks"` | no | +| [allow\_gateway\_transit\_from\_hub](#input\_allow\_gateway\_transit\_from\_hub) | Allow gateway transit from hub to spoke. Set to true if hub has a gateway and you want spoke to use it. | `bool` | `false` | no | +| [dns\_prefix](#input\_dns\_prefix) | DNS prefix for the AKS cluster | `string` | `"prodaks"` | no | +| [dns\_service\_ip](#input\_dns\_service\_ip) | IP address for Kubernetes DNS service (must be within service\_cidr) | `string` | `"10.0.0.10"` | no | +| [enable\_auto\_scaling](#input\_enable\_auto\_scaling) | Enable auto-scaling for the default node pool | `bool` | `false` | no | +| [existing\_vnet\_resource\_group\_name](#input\_existing\_vnet\_resource\_group\_name) | Resource group name of the existing VNet. Only used when vnet\_name is provided. Defaults to the AKS resource group if not specified. | `string` | `null` | no | +| [hub\_resource\_group\_name](#input\_hub\_resource\_group\_name) | Resource group name of the hub virtual network. Required when private\_cluster\_enabled is true and connecting to a hub. | `string` | `null` | no | +| [hub\_subscription\_id](#input\_hub\_subscription\_id) | Subscription ID of the hub network. Required when private\_cluster\_enabled is true and connecting to a hub. | `string` | `null` | no | +| [hub\_vnet\_name](#input\_hub\_vnet\_name) | Name of the hub virtual network to peer with. Required when private\_cluster\_enabled is true and connecting to a hub. | `string` | `null` | no | +| [kubernetes\_version](#input\_kubernetes\_version) | Kubernetes version for the AKS cluster | `string` | `"1.33.0"` | no | +| [location](#input\_location) | Azure region where resources will be deployed | `string` | `"Germany West Central"` | no | +| [log\_analytics\_workspace\_name](#input\_log\_analytics\_workspace\_name) | Name of the Log Analytics Workspace. If null, no LAW or monitoring will be created. | `string` | `null` | no | +| [log\_retention\_days](#input\_log\_retention\_days) | Number of days to retain logs in Log Analytics Workspace | `number` | `30` | no | +| [max\_node\_count](#input\_max\_node\_count) | Maximum number of nodes for auto-scaling (set to enable auto-scaling) | `number` | `null` | no | +| [min\_node\_count](#input\_min\_node\_count) | Minimum number of nodes for auto-scaling (set to enable auto-scaling) | `number` | `null` | no | +| [network\_plugin](#input\_network\_plugin) | Network plugin to use (azure or kubenet) | `string` | `"azure"` | no | +| [network\_policy](#input\_network\_policy) | Network policy to use (azure, calico, or cilium) | `string` | `"azure"` | no | +| [node\_count](#input\_node\_count) | Initial number of nodes in the default node pool | `number` | `3` | no | +| [os\_disk\_size\_gb](#input\_os\_disk\_size\_gb) | OS disk size in GB for the node pool | `number` | `100` | no | +| [private\_cluster\_enabled](#input\_private\_cluster\_enabled) | Enable private cluster (API server only accessible via private endpoint) | `bool` | `false` | no | +| [private\_cluster\_public\_fqdn\_enabled](#input\_private\_cluster\_public\_fqdn\_enabled) | Enable public FQDN for private cluster (allows public DNS resolution but API server remains private) | `bool` | `false` | no | +| [private\_dns\_zone\_id](#input\_private\_dns\_zone\_id) | Private DNS Zone ID for private cluster. Use 'System' for Azure-managed zone, or provide custom zone ID. Only used when private\_cluster\_enabled is true. | `string` | `"System"` | no | +| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group to create for the AKS cluster | `string` | `"aks-prod-rg"` | no | +| [service\_cidr](#input\_service\_cidr) | CIDR for Kubernetes services (must not overlap with VNet or subnet) | `string` | `"10.0.0.0/16"` | no | +| [subnet\_address\_prefix](#input\_subnet\_address\_prefix) | Address prefix for the AKS subnet (only used if subnet\_name is not provided) | `string` | `"10.240.0.0/20"` | no | +| [subnet\_name](#input\_subnet\_name) | Name of the subnet for AKS. If not provided, a new subnet will be created. | `string` | `null` | no | +| [tags](#input\_tags) | Tags to apply to all resources | `map(string)` | `{}` | no | +| [vm\_size](#input\_vm\_size) | Size of the virtual machines for the default node pool | `string` | `"Standard_A2_v2"` | no | +| [vnet\_address\_space](#input\_vnet\_address\_space) | Address space for the AKS virtual network (only used if vnet\_name is not provided) | `string` | `"10.240.0.0/16"` | no | +| [vnet\_name](#input\_vnet\_name) | Name of the virtual network for AKS. If not provided, a new VNet will be created. | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [aks\_identity\_client\_id](#output\_aks\_identity\_client\_id) | Client ID of the AKS system-assigned managed identity | +| [kube\_config](#output\_kube\_config) | Kubeconfig raw output | +| [law\_id](#output\_law\_id) | Log Analytics Workspace ID | +| [oidc\_issuer\_url](#output\_oidc\_issuer\_url) | OIDC issuer URL for federated identity and workload identity setup | +| [subnet\_id](#output\_subnet\_id) | Subnet ID used by AKS | + diff --git a/modules/azure/aks/buildingblock/aks.tftest.hcl b/modules/azure/aks/buildingblock/aks.tftest.hcl new file mode 100644 index 0000000..8ebfcfd --- /dev/null +++ b/modules/azure/aks/buildingblock/aks.tftest.hcl @@ -0,0 +1,410 @@ +run "scenario_1_new_vnet_with_hub_peering" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + aks_cluster_name = "test-aks-hub" + resource_group_name = "test-aks-hub-rg" + location = "Germany West Central" + dns_prefix = "testakshub" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "test-law" + + private_cluster_enabled = true + private_dns_zone_id = "System" + private_cluster_public_fqdn_enabled = false + + vnet_address_space = "10.240.0.0/16" + subnet_address_prefix = "10.240.0.0/20" + + hub_subscription_id = "5066eff7-4173-4fea-8c67-268456b4a4f7" + hub_resource_group_name = "likvid-hub-vnet-rg" + hub_vnet_name = "hub-vnet" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.private_cluster_enabled == true + error_message = "AKS cluster should be private" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 1 + error_message = "VNet should be created when vnet_name is null" + } + + assert { + condition = contains(one(azurerm_virtual_network.vnet[*].address_space), "10.240.0.0/16") + error_message = "VNet should have correct address space" + } + + assert { + condition = length(azurerm_subnet.aks_subnet) == 1 + error_message = "Subnet should be created when subnet_name is null" + } + + assert { + condition = length(azurerm_virtual_network_peering.aks_to_hub) == 1 + error_message = "Peering to hub should be created when creating new VNet" + } + + assert { + condition = length(azurerm_virtual_network_peering.hub_to_aks) == 1 + error_message = "Peering from hub should be created when creating new VNet" + } + + assert { + condition = one(azurerm_virtual_network_peering.hub_to_aks[*].allow_gateway_transit) == false + error_message = "Hub should not allow gateway transit when not configured" + } + + assert { + condition = one(azurerm_virtual_network_peering.aks_to_hub[*].use_remote_gateways) == false + error_message = "AKS VNet should not use remote gateways when not configured" + } +} + +run "scenario_2_existing_shared_vnet" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + aks_cluster_name = "test-aks-shared" + resource_group_name = "test-aks-shared-rg" + location = "Germany West Central" + dns_prefix = "testaksshared" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "test-law" + + private_cluster_enabled = true + private_dns_zone_id = "System" + private_cluster_public_fqdn_enabled = false + + vnet_name = "lz102-on-prem-nwk-vnet" + existing_vnet_resource_group_name = "connectivity" + subnet_name = "default" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.private_cluster_enabled == true + error_message = "AKS cluster should be private" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 0 + error_message = "VNet should NOT be created when vnet_name is provided" + } + + assert { + condition = length(azurerm_subnet.aks_subnet) == 0 + error_message = "Subnet should NOT be created when subnet_name is provided" + } + + assert { + condition = length(azurerm_virtual_network_peering.aks_to_hub) == 0 + error_message = "Peering to hub should NOT be created when using existing VNet" + } + + assert { + condition = length(azurerm_virtual_network_peering.hub_to_aks) == 0 + error_message = "Peering from hub should NOT be created when using existing VNet" + } + + assert { + condition = var.existing_vnet_resource_group_name == "connectivity" + error_message = "Should use VNet from different resource group" + } +} + + + +run "scenario_4_public_cluster" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + aks_cluster_name = "test-aks-public" + resource_group_name = "test-aks-public-rg" + location = "Germany West Central" + dns_prefix = "testakspublic" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "test-law" + + private_cluster_enabled = false + + vnet_address_space = "10.240.0.0/16" + subnet_address_prefix = "10.240.0.0/20" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.private_cluster_enabled == false + error_message = "AKS cluster should be public" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 1 + error_message = "VNet should be created" + } + + assert { + condition = length(azurerm_subnet.aks_subnet) == 1 + error_message = "Subnet should be created" + } + + assert { + condition = length(azurerm_virtual_network_peering.aks_to_hub) == 0 + error_message = "Peering should NOT be created for public cluster" + } + + assert { + condition = output.oidc_issuer_url != "" + error_message = "OIDC issuer URL should be available" + } +} + +run "valid_autoscaling_configuration" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-autoscale-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks-autoscale" + dns_prefix = "testaksautoscale" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + enable_auto_scaling = true + min_node_count = 2 + max_node_count = 10 + } + + assert { + condition = azurerm_kubernetes_cluster.aks.default_node_pool[0].min_count == 2 + error_message = "Min node count should match the input variable" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.default_node_pool[0].max_count == 10 + error_message = "Max node count should match the input variable" + } +} + +run "no_monitoring_when_law_null" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-no-monitoring-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks-no-monitoring" + dns_prefix = "testaksnomon" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = null + } + + assert { + condition = length(azurerm_log_analytics_workspace.law) == 0 + error_message = "Log Analytics Workspace should not be created when log_analytics_workspace_name is null" + } + + assert { + condition = length(azurerm_monitor_diagnostic_setting.aks_monitoring) == 0 + error_message = "Diagnostic settings should not be created when log_analytics_workspace_name is null" + } +} + +run "invalid_dns_prefix" { + command = plan + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks" + dns_prefix = "Invalid_DNS_Prefix!" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + } + + expect_failures = [ + var.dns_prefix + ] +} + +run "invalid_kubernetes_version" { + command = plan + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks" + dns_prefix = "testaks" + kubernetes_version = "invalid-version" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + } + + expect_failures = [ + var.kubernetes_version + ] +} + +run "invalid_admin_group_object_id" { + command = plan + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks" + dns_prefix = "testaks" + aks_admin_group_object_id = "not-a-valid-guid" + } + + expect_failures = [ + var.aks_admin_group_object_id + ] +} + +run "invalid_node_count_too_low" { + command = plan + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks" + dns_prefix = "testaks" + node_count = 0 + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + } + + expect_failures = [ + var.node_count + ] +} + +run "invalid_os_disk_size" { + command = plan + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks" + dns_prefix = "testaks" + os_disk_size_gb = 20 + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + } + + expect_failures = [ + var.os_disk_size_gb + ] +} + +run "custom_network_plugin_kubenet" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks-kubenet" + dns_prefix = "testakskube" + network_plugin = "kubenet" + network_policy = "calico" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.network_profile[0].network_plugin == "kubenet" + error_message = "Network plugin should be kubenet when specified" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.network_profile[0].network_policy == "calico" + error_message = "Network policy should be calico when specified" + } +} + +run "naming_derived_from_cluster_name" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "myapp-prod" + dns_prefix = "myappprod" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "test-law" + } + + assert { + condition = one(azurerm_virtual_network.vnet[*].name) == "myapp-prod-vnet" + error_message = "VNet name should be derived from cluster name" + } + + assert { + condition = one(azurerm_subnet.aks_subnet[*].name) == "myapp-prod-subnet" + error_message = "Subnet name should be derived from cluster name" + } + + assert { + condition = azurerm_log_analytics_workspace.law[0].name == "myapp-prod-law" + error_message = "Log Analytics Workspace name should be derived from cluster name" + } +} + +run "workload_identity_enabled" { + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + resource_group_name = "test-aks-rg" + location = "Germany West Central" + aks_cluster_name = "test-aks-wi" + dns_prefix = "testakswi" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.workload_identity_enabled == true + error_message = "Workload Identity should be enabled" + } + + assert { + condition = azurerm_kubernetes_cluster.aks.oidc_issuer_enabled == true + error_message = "OIDC issuer should be enabled" + } + + assert { + condition = output.oidc_issuer_url != "" + error_message = "OIDC issuer URL should be available" + } +} + +run "gateway_transit_configuration" { + command = plan + + variables { + subscription_id = "ffb344c9-26d7-45f5-9ba0-806a024ae697" + aks_cluster_name = "test-aks-gateway" + resource_group_name = "test-aks-gateway-rg" + location = "Germany West Central" + dns_prefix = "testaksgateway" + aks_admin_group_object_id = "12345678-1234-1234-1234-123456789012" + log_analytics_workspace_name = "test-law" + + private_cluster_enabled = true + private_dns_zone_id = "System" + private_cluster_public_fqdn_enabled = false + + vnet_address_space = "10.240.0.0/16" + subnet_address_prefix = "10.240.0.0/20" + + hub_subscription_id = "5066eff7-4173-4fea-8c67-268456b4a4f7" + hub_resource_group_name = "likvid-hub-vnet-rg" + hub_vnet_name = "hub-vnet" + allow_gateway_transit_from_hub = false + } + + assert { + condition = one(azurerm_virtual_network_peering.hub_to_aks[*].allow_gateway_transit) == false + error_message = "Hub should not allow gateway transit when disabled" + } + + assert { + condition = one(azurerm_virtual_network_peering.aks_to_hub[*].use_remote_gateways) == false + error_message = "AKS VNet should not use remote gateways when gateway transit disabled" + } +} diff --git a/modules/azure/aks/buildingblock/logo.png b/modules/azure/aks/buildingblock/logo.png new file mode 100644 index 0000000..ca396e6 Binary files /dev/null and b/modules/azure/aks/buildingblock/logo.png differ diff --git a/modules/azure/aks/buildingblock/main.tf b/modules/azure/aks/buildingblock/main.tf new file mode 100644 index 0000000..7fb98a3 --- /dev/null +++ b/modules/azure/aks/buildingblock/main.tf @@ -0,0 +1,217 @@ +# Resource Group +resource "azurerm_resource_group" "aks" { + name = var.resource_group_name + location = var.location + tags = var.tags +} + +resource "azurerm_virtual_network" "vnet" { + count = var.vnet_name == null ? 1 : 0 + name = "${var.aks_cluster_name}-vnet" + address_space = [var.vnet_address_space] + location = var.location + resource_group_name = azurerm_resource_group.aks.name + tags = var.tags + + depends_on = [azurerm_resource_group.aks] +} + +data "azurerm_virtual_network" "existing_vnet" { + count = var.vnet_name != null ? 1 : 0 + name = var.vnet_name + resource_group_name = var.existing_vnet_resource_group_name != null ? var.existing_vnet_resource_group_name : var.resource_group_name +} + +locals { + vnet_id = var.vnet_name != null ? data.azurerm_virtual_network.existing_vnet[0].id : azurerm_virtual_network.vnet[0].id + vnet_name = var.vnet_name != null ? var.vnet_name : azurerm_virtual_network.vnet[0].name +} + +resource "azurerm_subnet" "aks_subnet" { + count = var.subnet_name == null ? 1 : 0 + name = "${var.aks_cluster_name}-subnet" + resource_group_name = azurerm_resource_group.aks.name + virtual_network_name = local.vnet_name + address_prefixes = [var.subnet_address_prefix] + + depends_on = [azurerm_virtual_network.vnet] +} + +data "azurerm_subnet" "existing_subnet" { + count = var.subnet_name != null ? 1 : 0 + name = var.subnet_name + virtual_network_name = var.vnet_name + resource_group_name = var.existing_vnet_resource_group_name != null ? var.existing_vnet_resource_group_name : var.resource_group_name +} + +locals { + subnet_id = var.subnet_name != null ? data.azurerm_subnet.existing_subnet[0].id : azurerm_subnet.aks_subnet[0].id +} + +# Route table for UDR scenarios (private cluster with hub connectivity) +resource "azurerm_route_table" "aks_rt" { + count = var.private_cluster_enabled && var.hub_vnet_name != null && var.subnet_name == null ? 1 : 0 + name = "${var.aks_cluster_name}-rt" + location = var.location + resource_group_name = azurerm_resource_group.aks.name + tags = var.tags +} + +resource "azurerm_subnet_route_table_association" "aks_subnet_rt" { + count = var.private_cluster_enabled && var.hub_vnet_name != null && var.subnet_name == null ? 1 : 0 + subnet_id = azurerm_subnet.aks_subnet[0].id + route_table_id = azurerm_route_table.aks_rt[0].id +} + +resource "time_sleep" "wait_for_subnet" { + depends_on = [azurerm_subnet.aks_subnet, azurerm_subnet_route_table_association.aks_subnet_rt] + create_duration = "30s" +} + +# Log Analytics +resource "azurerm_log_analytics_workspace" "law" { + count = var.log_analytics_workspace_name != null ? 1 : 0 + name = "${var.aks_cluster_name}-law" + location = var.location + resource_group_name = azurerm_resource_group.aks.name + sku = "PerGB2018" + retention_in_days = var.log_retention_days + tags = var.tags +} + +# AKS Cluster +resource "azurerm_kubernetes_cluster" "aks" { + depends_on = [time_sleep.wait_for_subnet] + + name = var.aks_cluster_name + location = var.location + resource_group_name = azurerm_resource_group.aks.name + dns_prefix = var.dns_prefix + kubernetes_version = var.kubernetes_version + + private_cluster_enabled = var.private_cluster_enabled + private_dns_zone_id = var.private_cluster_enabled ? var.private_dns_zone_id : null + private_cluster_public_fqdn_enabled = var.private_cluster_enabled ? var.private_cluster_public_fqdn_enabled : false + + default_node_pool { + name = "system" + auto_scaling_enabled = var.enable_auto_scaling + node_count = !var.enable_auto_scaling ? var.node_count : null + min_count = var.enable_auto_scaling ? var.min_node_count : null + max_count = var.enable_auto_scaling ? var.max_node_count : null + vm_size = var.vm_size + os_disk_size_gb = var.os_disk_size_gb + vnet_subnet_id = local.subnet_id + type = "VirtualMachineScaleSets" + + upgrade_settings { + max_surge = "10%" + } + } + + identity { + type = "SystemAssigned" + } + + role_based_access_control_enabled = true + + dynamic "azure_active_directory_role_based_access_control" { + for_each = var.aks_admin_group_object_id != null ? [1] : [] + content { + admin_group_object_ids = [var.aks_admin_group_object_id] + } + } + + network_profile { + network_plugin = var.network_plugin + network_policy = var.network_policy + service_cidr = var.service_cidr + dns_service_ip = var.dns_service_ip + load_balancer_sku = "standard" + outbound_type = var.private_cluster_enabled && var.hub_vnet_name != null ? "userDefinedRouting" : "loadBalancer" + } + + oidc_issuer_enabled = true + workload_identity_enabled = true + + tags = merge( + var.tags, + { + ManagedBy = "Terraform" + } + ) + + lifecycle { + ignore_changes = [ + default_node_pool[0].node_count, + default_node_pool[0].upgrade_settings + ] + } +} + +resource "azurerm_monitor_diagnostic_setting" "aks_monitoring" { + count = var.log_analytics_workspace_name != null ? 1 : 0 + name = "${azurerm_kubernetes_cluster.aks.name}-diag" + target_resource_id = azurerm_kubernetes_cluster.aks.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.law[0].id + + enabled_log { + category = "kube-apiserver" + } + enabled_log { + category = "kube-controller-manager" + } + enabled_log { + category = "kube-scheduler" + } + enabled_log { + category = "cluster-autoscaler" + } + enabled_log { + category = "kube-audit" + } + + enabled_metric { + category = "AllMetrics" + } +} + +data "azurerm_resource_group" "hub_rg" { + count = var.private_cluster_enabled && var.hub_resource_group_name != null ? 1 : 0 + provider = azurerm.hub + name = var.hub_resource_group_name +} + +data "azurerm_virtual_network" "hub_vnet" { + count = var.private_cluster_enabled && var.hub_vnet_name != null ? 1 : 0 + provider = azurerm.hub + name = var.hub_vnet_name + resource_group_name = data.azurerm_resource_group.hub_rg[0].name +} + +resource "azurerm_virtual_network_peering" "aks_to_hub" { + count = var.private_cluster_enabled && var.vnet_name == null && var.hub_vnet_name != null ? 1 : 0 + name = "${var.aks_cluster_name}-to-hub" + resource_group_name = azurerm_resource_group.aks.name + virtual_network_name = local.vnet_name + remote_virtual_network_id = data.azurerm_virtual_network.hub_vnet[0].id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + use_remote_gateways = var.allow_gateway_transit_from_hub +} + +resource "azurerm_virtual_network_peering" "hub_to_aks" { + count = var.private_cluster_enabled && var.vnet_name == null && var.hub_vnet_name != null ? 1 : 0 + provider = azurerm.hub + name = "hub-to-${var.aks_cluster_name}" + resource_group_name = data.azurerm_resource_group.hub_rg[0].name + virtual_network_name = data.azurerm_virtual_network.hub_vnet[0].name + remote_virtual_network_id = local.vnet_id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = var.allow_gateway_transit_from_hub + use_remote_gateways = false +} diff --git a/modules/azure/aks/buildingblock/outputs.tf b/modules/azure/aks/buildingblock/outputs.tf new file mode 100644 index 0000000..d065127 --- /dev/null +++ b/modules/azure/aks/buildingblock/outputs.tf @@ -0,0 +1,25 @@ +output "kube_config" { + description = "Kubeconfig raw output" + value = azurerm_kubernetes_cluster.aks.kube_config_raw + sensitive = true +} + +output "oidc_issuer_url" { + description = "OIDC issuer URL for federated identity and workload identity setup" + value = azurerm_kubernetes_cluster.aks.oidc_issuer_url +} + +output "aks_identity_client_id" { + description = "Client ID of the AKS system-assigned managed identity" + value = azurerm_kubernetes_cluster.aks.identity[0].principal_id +} + +output "law_id" { + description = "Log Analytics Workspace ID" + value = length(azurerm_log_analytics_workspace.law) > 0 ? azurerm_log_analytics_workspace.law[0].id : null +} + +output "subnet_id" { + description = "Subnet ID used by AKS" + value = local.subnet_id +} diff --git a/modules/azure/aks/buildingblock/provider.tf b/modules/azure/aks/buildingblock/provider.tf new file mode 100644 index 0000000..ab91b24 --- /dev/null +++ b/modules/azure/aks/buildingblock/provider.tf @@ -0,0 +1,3 @@ +provider "azurerm" { + features {} +} diff --git a/modules/azure/aks/buildingblock/variables.tf b/modules/azure/aks/buildingblock/variables.tf new file mode 100644 index 0000000..595e173 --- /dev/null +++ b/modules/azure/aks/buildingblock/variables.tf @@ -0,0 +1,250 @@ +variable "resource_group_name" { + type = string + description = "Name of the resource group to create for the AKS cluster" + default = "aks-prod-rg" +} + +variable "location" { + type = string + description = "Azure region where resources will be deployed" + default = "Germany West Central" +} + +variable "aks_cluster_name" { + type = string + description = "Name of the AKS cluster" + default = "prod-aks" + + validation { + condition = length(var.aks_cluster_name) >= 1 && length(var.aks_cluster_name) <= 63 + error_message = "AKS cluster name must be between 1 and 63 characters." + } +} + +variable "dns_prefix" { + type = string + description = "DNS prefix for the AKS cluster" + default = "prodaks" + + validation { + condition = can(regex("^[a-z0-9][a-z0-9-]{0,53}[a-z0-9]$", var.dns_prefix)) + error_message = "DNS prefix must contain only lowercase letters, numbers, and hyphens, and be between 2 and 54 characters." + } +} + +variable "vnet_address_space" { + type = string + description = "Address space for the AKS virtual network (only used if vnet_name is not provided)" + default = "10.240.0.0/16" + + validation { + condition = can(cidrhost(var.vnet_address_space, 0)) + error_message = "VNet address space must be a valid CIDR block." + } +} + +variable "vnet_name" { + type = string + description = "Name of the virtual network for AKS. If not provided, a new VNet will be created." + default = null +} + +variable "existing_vnet_resource_group_name" { + type = string + description = "Resource group name of the existing VNet. Only used when vnet_name is provided. Defaults to the AKS resource group if not specified." + default = null +} + +variable "subnet_address_prefix" { + type = string + description = "Address prefix for the AKS subnet (only used if subnet_name is not provided)" + default = "10.240.0.0/20" + + validation { + condition = can(cidrhost(var.subnet_address_prefix, 0)) + error_message = "Subnet address prefix must be a valid CIDR block." + } +} + +variable "subnet_name" { + type = string + description = "Name of the subnet for AKS. If not provided, a new subnet will be created." + default = null +} + +variable "service_cidr" { + type = string + description = "CIDR for Kubernetes services (must not overlap with VNet or subnet)" + default = "10.0.0.0/16" + + validation { + condition = can(cidrhost(var.service_cidr, 0)) + error_message = "Service CIDR must be a valid CIDR block." + } +} + +variable "dns_service_ip" { + type = string + description = "IP address for Kubernetes DNS service (must be within service_cidr)" + default = "10.0.0.10" + + validation { + condition = can(regex("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$", var.dns_service_ip)) + error_message = "DNS service IP must be a valid IP address." + } +} + +variable "node_count" { + type = number + description = "Initial number of nodes in the default node pool" + default = 3 + + validation { + condition = var.node_count >= 1 && var.node_count <= 1000 + error_message = "Node count must be between 1 and 1000." + } +} + +variable "min_node_count" { + type = number + description = "Minimum number of nodes for auto-scaling (set to enable auto-scaling)" + default = null +} + +variable "max_node_count" { + type = number + description = "Maximum number of nodes for auto-scaling (set to enable auto-scaling)" + default = null +} + +variable "vm_size" { + type = string + description = "Size of the virtual machines for the default node pool" + default = "Standard_A2_v2" +} + +variable "os_disk_size_gb" { + type = number + description = "OS disk size in GB for the node pool" + default = 100 + + validation { + condition = var.os_disk_size_gb >= 30 && var.os_disk_size_gb <= 2048 + error_message = "OS disk size must be between 30 and 2048 GB." + } +} + +variable "kubernetes_version" { + type = string + description = "Kubernetes version for the AKS cluster" + default = "1.33.0" + + validation { + condition = can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.kubernetes_version)) + error_message = "Kubernetes version must be in format X.Y.Z (e.g., 1.29.2)." + } +} + +variable "aks_admin_group_object_id" { + type = string + description = "Object ID of the Azure AD group used for AKS admin access. If null, Azure AD RBAC will not be configured." + default = null + + validation { + condition = var.aks_admin_group_object_id == null || can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.aks_admin_group_object_id)) + error_message = "Admin group object ID must be a valid GUID or null." + } +} + +variable "log_analytics_workspace_name" { + type = string + description = "Name of the Log Analytics Workspace. If null, no LAW or monitoring will be created." + default = null +} + +variable "log_retention_days" { + type = number + description = "Number of days to retain logs in Log Analytics Workspace" + default = 30 + + validation { + condition = var.log_retention_days >= 30 && var.log_retention_days <= 730 + error_message = "Log retention must be between 30 and 730 days." + } +} + +variable "enable_auto_scaling" { + type = bool + description = "Enable auto-scaling for the default node pool" + default = false +} + +variable "network_plugin" { + type = string + description = "Network plugin to use (azure or kubenet)" + default = "azure" + + validation { + condition = contains(["azure", "kubenet"], var.network_plugin) + error_message = "Network plugin must be either 'azure' or 'kubenet'." + } +} + +variable "network_policy" { + type = string + description = "Network policy to use (azure, calico, or cilium)" + default = "azure" + + validation { + condition = contains(["azure", "calico", "cilium"], var.network_policy) + error_message = "Network policy must be 'azure', 'calico', or 'cilium'." + } +} + +variable "tags" { + type = map(string) + description = "Tags to apply to all resources" + default = {} +} + +variable "private_cluster_enabled" { + type = bool + description = "Enable private cluster (API server only accessible via private endpoint)" + default = false +} + +variable "private_dns_zone_id" { + type = string + description = "Private DNS Zone ID for private cluster. Use 'System' for Azure-managed zone, or provide custom zone ID. Only used when private_cluster_enabled is true." + default = "System" +} + +variable "private_cluster_public_fqdn_enabled" { + type = bool + description = "Enable public FQDN for private cluster (allows public DNS resolution but API server remains private)" + default = false +} + +variable "hub_subscription_id" { + type = string + description = "Subscription ID of the hub network. Required when private_cluster_enabled is true and connecting to a hub." + default = null +} + +variable "hub_resource_group_name" { + type = string + description = "Resource group name of the hub virtual network. Required when private_cluster_enabled is true and connecting to a hub." + default = null +} + +variable "hub_vnet_name" { + type = string + description = "Name of the hub virtual network to peer with. Required when private_cluster_enabled is true and connecting to a hub." + default = null +} + +variable "allow_gateway_transit_from_hub" { + type = bool + description = "Allow gateway transit from hub to spoke. Set to true if hub has a gateway and you want spoke to use it." + default = false +} diff --git a/modules/azure/aks/buildingblock/versions.tf b/modules/azure/aks/buildingblock/versions.tf new file mode 100644 index 0000000..d29d984 --- /dev/null +++ b/modules/azure/aks/buildingblock/versions.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.36.0" + configuration_aliases = [azurerm, azurerm.hub] + } + + time = { + source = "hashicorp/time" + version = "~> 0.11.1" + } + } +} diff --git a/modules/azure/container-registry/backplane/outputs.tf b/modules/azure/container-registry/backplane/outputs.tf index 6c0692f..8447798 100644 --- a/modules/azure/container-registry/backplane/outputs.tf +++ b/modules/azure/container-registry/backplane/outputs.tf @@ -146,7 +146,7 @@ output "provider_tf" { client_id = "${azuread_service_principal.buildingblock_deploy[0].client_id}" client_secret = "${azuread_application_password.buildingblock_deploy[0].value}" - subscription_id = "" + subscription_id = "" tenant_id = "${data.azurerm_subscription.current.tenant_id}" } @@ -175,7 +175,7 @@ output "provider_tf" { client_id = "${azuread_service_principal.buildingblock_deploy[0].client_id}" use_oidc = true - subscription_id = "" + subscription_id = "" tenant_id = "${data.azurerm_subscription.current.tenant_id}" }