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}"
}