From ca036075dc441ada9edb951c5ddd259af72fd3a0 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Mon, 12 Oct 2026 12:07:46 +0200 Subject: [PATCH 01/11] feat(azure/aks-namespace): add workspace building block for AKS namespaces Creates a namespace with ResourceQuota, LimitRange and Entra group role bindings on an existing AKS cluster. The block authenticates through the backplane UAMI by workload identity federation and holds no kubeconfig. Co-Authored-By: Claude Sonnet 5.5 --- .../azure/aks-namespace/backplane/README.md | 20 ++ modules/azure/aks-namespace/backplane/main.tf | 66 ++++ .../azure/aks-namespace/backplane/outputs.tf | 7 + .../azure/aks-namespace/backplane/provider.tf | 3 + .../aks-namespace/backplane/variables.tf | 45 +++ .../azure/aks-namespace/backplane/versions.tf | 10 + .../aks-namespace/buildingblock/README.md | 68 ++++ .../aks-namespace/buildingblock/logo.png | Bin 0 -> 4662 bytes .../azure/aks-namespace/buildingblock/main.tf | 82 +++++ .../aks-namespace/buildingblock/outputs.tf | 33 ++ .../aks-namespace/buildingblock/provider.tf | 45 +++ .../aks-namespace/buildingblock/variables.tf | 75 ++++ .../aks-namespace/buildingblock/versions.tf | 16 + modules/azure/aks-namespace/e2e/main.tf | 67 ++++ modules/azure/aks-namespace/e2e/provider.tf | 6 + modules/azure/aks-namespace/e2e/terraform.tf | 12 + .../tests/azure_aks_namespace_hub.tftest.hcl | 11 + .../aks-namespace/meshstack_integration.tf | 337 ++++++++++++++++++ 18 files changed, 903 insertions(+) create mode 100644 modules/azure/aks-namespace/backplane/README.md create mode 100644 modules/azure/aks-namespace/backplane/main.tf create mode 100644 modules/azure/aks-namespace/backplane/outputs.tf create mode 100644 modules/azure/aks-namespace/backplane/provider.tf create mode 100644 modules/azure/aks-namespace/backplane/variables.tf create mode 100644 modules/azure/aks-namespace/backplane/versions.tf create mode 100644 modules/azure/aks-namespace/buildingblock/README.md create mode 100644 modules/azure/aks-namespace/buildingblock/logo.png create mode 100644 modules/azure/aks-namespace/buildingblock/main.tf create mode 100644 modules/azure/aks-namespace/buildingblock/outputs.tf create mode 100644 modules/azure/aks-namespace/buildingblock/provider.tf create mode 100644 modules/azure/aks-namespace/buildingblock/variables.tf create mode 100644 modules/azure/aks-namespace/buildingblock/versions.tf create mode 100644 modules/azure/aks-namespace/e2e/main.tf create mode 100644 modules/azure/aks-namespace/e2e/provider.tf create mode 100644 modules/azure/aks-namespace/e2e/terraform.tf create mode 100644 modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl create mode 100644 modules/azure/aks-namespace/meshstack_integration.tf diff --git a/modules/azure/aks-namespace/backplane/README.md b/modules/azure/aks-namespace/backplane/README.md new file mode 100644 index 000000000..96d2052bd --- /dev/null +++ b/modules/azure/aks-namespace/backplane/README.md @@ -0,0 +1,20 @@ +# Azure AKS Namespace — Backplane + +Creates the identity the **Azure AKS Namespace** building block runs as. + +## What it provisions + +| Resource | Purpose | +| --- | --- | +| Resource group + user-assigned managed identity | The automation principal. No client secret exists; meshStack's building block runner federates into it. | +| Federated identity credential | Trusts the building block definition's workload identity subject. | +| Custom role (`-deploy`) | Lets the identity read the AKS cluster and fetch the user credentials. | +| `Azure Kubernetes Service RBAC Cluster Admin` | Lets the identity create namespaces, quotas and role bindings. Scoped to the one cluster. | +| `AcrPull` for the kubelet identity (optional) | Set `acr_id` and every namespace's workloads can pull from that registry without an image pull secret. | + +## Prerequisites + +- The AKS cluster is Entra-integrated and has **Azure RBAC for Kubernetes authorization** enabled + (`azure_rbac_enabled = true`). `modules/azure/aks` does not set this yet; enable it on the cluster + before deploying this backplane. +- The cluster's API server is reachable from the building block runner. diff --git a/modules/azure/aks-namespace/backplane/main.tf b/modules/azure/aks-namespace/backplane/main.tf new file mode 100644 index 000000000..22570e8cc --- /dev/null +++ b/modules/azure/aks-namespace/backplane/main.tf @@ -0,0 +1,66 @@ +data "azurerm_kubernetes_cluster" "this" { + name = var.aks.cluster_name + resource_group_name = var.aks.resource_group_name +} + +resource "azurerm_resource_group" "backplane" { + name = var.name + location = var.location +} + +resource "azurerm_user_assigned_identity" "backplane" { + name = var.name + location = var.location + resource_group_name = azurerm_resource_group.backplane.name +} + +resource "azurerm_federated_identity_credential" "backplane" { + for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s } + + name = "subject-${each.key}" + user_assigned_identity_id = azurerm_user_assigned_identity.backplane.id + audience = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = each.value +} + +# Reading the cluster through the ARM provider is all the building block needs besides the +# in-cluster rights below. +resource "azurerm_role_definition" "backplane" { + name = "${var.name}-deploy" + description = "Enables the ${var.name} building block to look up the AKS cluster it creates namespaces in" + scope = var.scope + + permissions { + actions = [ + "Microsoft.ContainerService/managedClusters/read", + "Microsoft.ContainerService/managedClusters/listClusterUserCredential/action", + ] + } + + assignable_scopes = [var.scope] +} + +resource "azurerm_role_assignment" "backplane" { + scope = data.azurerm_kubernetes_cluster.this.id + role_definition_id = azurerm_role_definition.backplane.role_definition_resource_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id +} + +# Creating a namespace is a cluster-scoped write, so the identity needs cluster admin. The built-in +# role is the narrowest one Azure offers for that; it is scoped to this one cluster. +resource "azurerm_role_assignment" "cluster_admin" { + scope = data.azurerm_kubernetes_cluster.this.id + role_definition_name = "Azure Kubernetes Service RBAC Cluster Admin" + principal_id = azurerm_user_assigned_identity.backplane.principal_id +} + +resource "azurerm_role_assignment" "kubelet_acr_pull" { + lifecycle { + enabled = var.acr_id != null + } + + scope = var.acr_id + role_definition_name = "AcrPull" + principal_id = data.azurerm_kubernetes_cluster.this.kubelet_identity[0].object_id +} diff --git a/modules/azure/aks-namespace/backplane/outputs.tf b/modules/azure/aks-namespace/backplane/outputs.tf new file mode 100644 index 000000000..2b6b805dc --- /dev/null +++ b/modules/azure/aks-namespace/backplane/outputs.tf @@ -0,0 +1,7 @@ +output "identity" { + value = { + client_id = azurerm_user_assigned_identity.backplane.client_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id + tenant_id = azurerm_user_assigned_identity.backplane.tenant_id + } +} diff --git a/modules/azure/aks-namespace/backplane/provider.tf b/modules/azure/aks-namespace/backplane/provider.tf new file mode 100644 index 000000000..ab91b2481 --- /dev/null +++ b/modules/azure/aks-namespace/backplane/provider.tf @@ -0,0 +1,3 @@ +provider "azurerm" { + features {} +} diff --git a/modules/azure/aks-namespace/backplane/variables.tf b/modules/azure/aks-namespace/backplane/variables.tf new file mode 100644 index 000000000..180d885e9 --- /dev/null +++ b/modules/azure/aks-namespace/backplane/variables.tf @@ -0,0 +1,45 @@ +variable "name" { + type = string + nullable = false + description = "Name for the building block identity and role definition." + 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 the custom role definition is assignable at (management group or subscription ID). Must contain the AKS cluster." +} + +variable "location" { + type = string + nullable = false + description = "Azure region for the UAMI resource." +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subjects = list(string) + }) + nullable = false + description = "WIF issuer and subjects for federated authentication." +} + +variable "aks" { + type = object({ + cluster_name = string + resource_group_name = string + }) + nullable = false + description = "The existing AKS cluster namespaces are created in. It must have Azure RBAC for Kubernetes authorization enabled." +} + +variable "acr_id" { + type = string + default = null + description = "Resource ID of the Azure Container Registry the cluster's workloads pull images from. When set, the cluster's kubelet identity gets AcrPull on it, so no namespace needs an image pull secret." +} diff --git a/modules/azure/aks-namespace/backplane/versions.tf b/modules/azure/aks-namespace/backplane/versions.tf new file mode 100644 index 000000000..8236fc6c9 --- /dev/null +++ b/modules/azure/aks-namespace/backplane/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.11.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.65.0, < 5.0.0" + } + } +} diff --git a/modules/azure/aks-namespace/buildingblock/README.md b/modules/azure/aks-namespace/buildingblock/README.md new file mode 100644 index 000000000..fb6ec1bf6 --- /dev/null +++ b/modules/azure/aks-namespace/buildingblock/README.md @@ -0,0 +1,68 @@ +--- +name: Azure AKS Namespace +supportedPlatforms: + - azure +description: Creates a Kubernetes namespace on an existing AKS cluster with a resource quota, default container limits and Entra group access, ordered at workspace level. +--- + +# Azure AKS Namespace + +Creates one namespace on an existing AKS cluster. It is a **workspace** building block: no meshStack +project, tenant or platform is involved. The block authenticates to the cluster as the backplane's +managed identity through workload identity federation and holds no kubeconfig. + +## What it creates + +- the namespace, labelled with the owning workspace +- a `ResourceQuota` and a `LimitRange` with default container requests and limits +- a `RoleBinding` per Entra group to the `admin`, `edit` or `view` cluster role + + +## Requirements + +| Name | Version | +|------|---------| +| [azurerm](#requirement\_azurerm) | >= 4.64.0, < 5.0.0 | +| [http](#requirement\_http) | >= 3.4.0, < 4.0.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.35.1, < 3.0.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [kubernetes_limit_range_v1.this](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/limit_range_v1) | resource | +| [kubernetes_namespace_v1.this](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace_v1) | resource | +| [kubernetes_resource_quota_v1.this](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/resource_quota_v1) | resource | +| [kubernetes_role_binding_v1.group](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding_v1) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_kubernetes_cluster.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/kubernetes_cluster) | data source | +| [http_http.aks_token](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [admin\_group\_object\_ids](#input\_admin\_group\_object\_ids) | Entra group object IDs granted the `admin` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [aks\_cluster\_name](#input\_aks\_cluster\_name) | Name of the AKS cluster the namespace is created in. | `string` | n/a | yes | +| [aks\_resource\_group\_name](#input\_aks\_resource\_group\_name) | Resource group of the AKS cluster. | `string` | n/a | yes | +| [default\_container\_limits](#input\_default\_container\_limits) | LimitRange defaults for containers that declare no requests or limits of their own. |
object({
cpu_request = optional(string, "100m")
memory_request = optional(string, "128Mi")
cpu_limit = optional(string, "500m")
memory_limit = optional(string, "512Mi")
})
| `{}` | no | +| [edit\_group\_object\_ids](#input\_edit\_group\_object\_ids) | Entra group object IDs granted the `edit` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [namespace\_name](#input\_namespace\_name) | Name of the Kubernetes namespace to create. | `string` | n/a | yes | +| [oidc\_token\_file\_path](#input\_oidc\_token\_file\_path) | File the building block runner writes its federated OIDC token to. | `string` | `"/var/run/secrets/workload-identity/azure/token"` | no | +| [resource\_quota](#input\_resource\_quota) | ResourceQuota applied to the namespace. |
object({
requests_cpu = optional(string, "2")
requests_memory = optional(string, "4Gi")
limits_cpu = optional(string, "4")
limits_memory = optional(string, "8Gi")
pods = optional(string, "50")
})
| `{}` | no | +| [view\_group\_object\_ids](#input\_view\_group\_object\_ids) | Entra group object IDs granted the `view` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [workspace\_identifier](#input\_workspace\_identifier) | Identifier of the meshStack workspace that owns the namespace. Recorded as a label. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [cluster\_name](#output\_cluster\_name) | Name of the AKS cluster hosting the namespace. | +| [cluster\_resource\_group\_name](#output\_cluster\_resource\_group\_name) | Resource group of the AKS cluster hosting the namespace. | +| [namespace\_name](#output\_namespace\_name) | Name of the created Kubernetes namespace. | +| [summary](#output\_summary) | Markdown summary shown in meshPanel after the run. | + diff --git a/modules/azure/aks-namespace/buildingblock/logo.png b/modules/azure/aks-namespace/buildingblock/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..971d7de633859d8e8cec88eb0d1aa8ed6e5ff5f3 GIT binary patch literal 4662 zcmWkxcRbZ?6uQ%p(35Mw&AXtfmmp43m$Z zl|KX`3Hx8DLdsNw&Oj=EQ(X;6{Ws{&nM3QQZmbT0G$pf;9qAwt8VKCTOk3B-==WKlslcryKrIE7O9GV9fL94#5e>*Dfv-D>4mqH5GU%9xyO#`j6yeVT zG*W;oaYsCHfL1D?p9Ux-0rxXNyBwTG8qOvcr;rSqq=CvwfKe$Rg#sVv;&fBNn>=gPzmpTn>DL7kfTJ9fDO-O}_Kzt2QzV%ruxfyC zGYK@w0^7!j1JfkTBneNX%>E+lW`Kn~goH-iKY-%(7Mxx=rch3wmw;iFxZk^E=R%xC z4*32HabxdfcmG5n5%7J7tLP_ycuK~5LO>br5Tpc_@beI-Uz>B2ZSv&u@sIUmqYN;*7N?W~&aaV|HpuF! z;D-rfb|>B@4{R7B;0TnHlT#ri*gH-do+b5sBVrFu?DIj#0+4ik>Rt@K{XifPDZGim z+3SzcGD_q{0*k-NAr&}& zBslf+IISJelK_m)kz(s`=vMp=mOQa^%+b5_^6YQK1eo4`2%#mZ3m9S6Dt)$P>L(aw z;`rt+mW6-H&r6I9;bhcak_@F=G!6tvB(qP?c5Mn_Pu%m-SdrU2(s+PX^v)K`(=E*O zm|P`x?XdIt97lU=)`Nm6D<`E)!i1utJq*3X#?fPjA9q9dM8`DpntQR>ief*NWNxfl znLv{jhn#>FJfMg-_jv_@oR`wqQa1}(*s~4|cr?hP3%8+Dig&kqW7#ChRcT%$YK!rt zs}T({8J}FA8eBL1>$gCeF7)2-zJ*t5Myu&*XizgbCo{!}uq22x#YpTqaOMbxu2g93!*R7d^AT z12z^Ld4$o2I!^|7>|?}Uz1^}OI?`rfg{sF~A~YTA*n1yxzIN2$V^YvNDLt=sPlkfp zx6|3R=)Fx_yZRl*#)ZJz?}v77+aINu`pYr^LO$%4=J=GXSQga~zE_cy4tTaBxqLh< z%YYK65}a68>Ht1;Ms%16Abk5i+$O2MTf$H1@GDunXnoO#*N}Inv(!!(gF~P354LH7 zPAs*)?}IYP)0{^7N8(b%5X%n#65K!6OKs1$c^c8M$6N<`KRUIh4u8zyI7w`r+G?B; zOLQb<3JrfrZXj&g&^gqO8t9dAB=z<2ig$4d%U5}JtHnKzABl*8hB-XgmT>$yuX;`I zR1wUiYyJomZla-UK}347(8xR`Oh$5+77c_;F;{uXoTQhkLr%k$I4E0ikinuaUZzB^ z2Yy*ol^`6$@T<&+K_;?3KC^@gd29rqwoo5kc=d}&&dj;N6~bEMUTv93<3GrQxKWXktPVvgw76JxPoIOmY-pR8UYIkGf)z?f zwNmj`)z7@ReIBtTex2B?<=N(w;&}NF)!1m?tIRU-yCYWu@y=M8evXl^$HZJrZh;0P zZy;R#RD|1VwTM^4J?aD%=!RzZzIxNGXzzvk1(E!?w`r7$d_?1HLzqScB?wM0yelz8 zlWBg}$R&VR(E6(+vb5)X3lLffzx&%>MSk#MtL`4xHq{3u_Js$gf5deHU3q8xNJGD% z_EBrW=ddtmd&NoK+MhDZlP5v_J6gp;*C4R*m~bjgC5Hgfpv_OmU=O!h4T+3bKP`_; z?ChV60+FsHe!Xw4RIQg{CZZjZ<%Q{4=tHzv;KLr@s4d;(VTPrzMZGQ4=eZK;_LZqL zNVp2nBWaDhOdgVo(-5AbyQX*J$#V%ru<{*<8mz+7nAxc#8!M1aS8T&KOLXdGN4J|1 z65Q)E3C}J)_GSQnuU|Gxic?e=OW+tw&i!yc?jB{_Dg8(P3so z=fk>*h{Tv}o*tjHtC%BPbK^vMsD9E^)3Hbml=iw{t4Ii^hXel*DW{J~=5cPiEJRAq zi%RYXQGR=+2HwGUMqc9p{z6<7e+0O z=f-xNs9;?M4uu!)-_wiOeH$6re5i?0G##DK$+flJVrz6=bf29+K_pbE(LL{wm>x8fauW8yv!e-=ctD> zAC9mfet#vF!}CK9E~MJ0Q6U*QZI#A;{o=H<(MkWOw<9eIyatkL5) z%Mq~TN>?8VT2Iv0R>#1IXaLtCfnz{b_HcG#q3fjBWNLd$T1kv1Bqb8D*3C{*7hz3R zGL90lT0HkRy9+TruJW0Mw1?of$?2u%)-Io6 ziRtVU?QwPyn~R%Nm#@)u@B`gyGEm}2{eanTFGMfg(0JsS#&mt>QHqD;;?2Tu*3_Tp zvj#ILJHIaSsHs0OV0>*pB_6t4u8o9mQeUhw_k;Yt8Ma|$r_cY6KitBWOUQ3<;BgzX zgT98x-BEr={0rR$d#~TS6B^Hxj{Ynsxw`hOR5xErd_;as4`*X)3(sC3<>Yd8a6Dg| z)Fa^CJkCODQI{a(-=GN6%rjEuvB?OmKJh+sDK8$K+Vm%XLq>dTsi;dbe9}bI9Y~T; z5&6_#M;9CA9Nn({5o~kzhn$16GBkQ+u8tp8{*(%X^rX61Y0qw(-<o&x* zth5puI@32f_C$_s6{scnen5Iih)O|?rxBHf{915N>GBmFmd+7nvGA0?)U86$s;(#T z(~#l9IEMVvmaBQ!Q$HP=-pY$DJt}V@uhEm5o!Q(?g{fB~%I;mvq`}hG+(3o8)>mf9 zCixb1JR5V1#$SD6w;V)Dhv-`B`hO0W&(CKv&m7I@*;RO# zXGH|kz1J}xNl(z?=7MD(SD#yL>xB3QeNqy4c-x@XQQT{En(M}ufpvI^JfuHZ6Y5=L zh`2*b$ji-SV``uY-@Vc5|F839v@?U)!dKC3udhY(qhm^&H7CQ8Bd7}9NtO%lWVou{ z@=s?Dx{{1(_ge>$UOPkm`caf0hncKDd?S%4^r4KpLsC#ops%7nNY=OISPWOtU^f;m z8`w257T~b+Yv$`y{TGi{sp0-tq?OBlteY@)i!cOnJb9OX-@ahS(@p{$j{Wv`YJfqm zfjvoJ&3Blx*1A}v)`BlSh>MO=!d1iSMm|Nn^0}j7Qu?&_Br3wu1(PgMP%|rE1e_Yr z65rj?){-wbg(B{ausCQzA@`5l-CZJlzlUdN>4VTg7UvfTV@dmhzK)M=v_dsmy%3!~ z_9U9N!Yj_ZhdsH|&nATHdgZS?vouM2@0dh#2`ih4iI`j||5N4^)i*7cd%Jtgs&+Xm z&+E|IC)e;shO)HrV~q7)Vl{0l8>W7_zsJho>_++pOR@O%2gi-K$Bt6W7T3ko*VWQO z;%m15DsX0PaMG7~HPv)7&h(Z3sORCo303>CHvQ%F_Y|@_v>~6(wLVW04C;3-_%N{d zG(7x8Y#JCvG(vj6?4Z#o@gGy@Luc` z_p6F3+gr(Kk=kHTgnfy(F_?m=U*|8*hCl5-!DR+M8-b7uFM*&_^|xzekRg(zGIaPr6l&XSQhB#RC0Qs_b!w zqN^c94i4G%0(vy*jx|Kzu=UUD$U#M?&>~jo4i_5{=ZNtVoM|t77VP0xK9zq`vMGE` z4bSh(mIBBMnmM#rh>L7klzM!#wVZEmDTO^B+GPInQ3S-(8940oz-Yf05&s*#kOlsT(hOxbsRw??lpOrW2+tc@D0Hy5K5 zx|9|g%>F|Q>t4XJ$ASweMQTRbHZsgVwtD4hAeP2F`FW>0VOv~N*Rx)^kg?v^rdtti zK$bXpm{o$)v5!e@ES65{m^ERLu8dIj7_9VF!-2}`1=HKVg+26Lpg3b%c^mimGT9MZ zcVVpm)SjDzG}e2ty}X<6sg{5U^KS|)^=WpizMc8!dllS&xbD@{X@$_-&`3kZ4hovr zan)w2C$M%)ADXwf1h-#kl6;$i5oc?pQ^WELrMVu1tdY# z(yn3hjVZ7ub?HNZ!?zbfIqtG>YQn4jGCxHx5vkcb9DJ76i4!GVFimunY_TwFZC=;H z{Rl|u_%eby&xlsrTc|OzKeo&$&07jP8l8$pr&wNkyp|OKwBc&bxv^wzy|&e!dUQWGeRCAL_iMRBpfN o8rv3)c&QU-BF(JRkEVkddO?Q+`WbxBR#}L?_I<5-4TtFe05cg~XaE2J literal 0 HcmV?d00001 diff --git a/modules/azure/aks-namespace/buildingblock/main.tf b/modules/azure/aks-namespace/buildingblock/main.tf new file mode 100644 index 000000000..73bfc5323 --- /dev/null +++ b/modules/azure/aks-namespace/buildingblock/main.tf @@ -0,0 +1,82 @@ +locals { + group_bindings = merge([ + for cluster_role, object_ids in { + admin = var.admin_group_object_ids + edit = var.edit_group_object_ids + view = var.view_group_object_ids + } : { for id in object_ids : "${cluster_role}-${id}" => { cluster_role = cluster_role, object_id = id } } + ]...) +} + +resource "kubernetes_namespace_v1" "this" { + metadata { + name = var.namespace_name + + labels = { + "app.kubernetes.io/managed-by" = "meshstack" + "meshcloud.io/workspace" = var.workspace_identifier + } + } +} + +resource "kubernetes_resource_quota_v1" "this" { + metadata { + name = "meshstack-quota" + namespace = kubernetes_namespace_v1.this.metadata[0].name + } + + spec { + hard = { + "requests.cpu" = var.resource_quota.requests_cpu + "requests.memory" = var.resource_quota.requests_memory + "limits.cpu" = var.resource_quota.limits_cpu + "limits.memory" = var.resource_quota.limits_memory + "pods" = var.resource_quota.pods + } + } +} + +# Without defaults, a pod that declares no requests or limits is rejected once a quota on those +# resources exists. +resource "kubernetes_limit_range_v1" "this" { + metadata { + name = "meshstack-defaults" + namespace = kubernetes_namespace_v1.this.metadata[0].name + } + + spec { + limit { + type = "Container" + + default_request = { + cpu = var.default_container_limits.cpu_request + memory = var.default_container_limits.memory_request + } + default = { + cpu = var.default_container_limits.cpu_limit + memory = var.default_container_limits.memory_limit + } + } + } +} + +resource "kubernetes_role_binding_v1" "group" { + for_each = local.group_bindings + + metadata { + name = "meshstack-${each.value.cluster_role}-${substr(each.value.object_id, 0, 8)}" + namespace = kubernetes_namespace_v1.this.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = each.value.cluster_role + } + + subject { + api_group = "rbac.authorization.k8s.io" + kind = "Group" + name = each.value.object_id + } +} diff --git a/modules/azure/aks-namespace/buildingblock/outputs.tf b/modules/azure/aks-namespace/buildingblock/outputs.tf new file mode 100644 index 000000000..6657b1581 --- /dev/null +++ b/modules/azure/aks-namespace/buildingblock/outputs.tf @@ -0,0 +1,33 @@ +output "namespace_name" { + description = "Name of the created Kubernetes namespace." + value = kubernetes_namespace_v1.this.metadata[0].name +} + +output "cluster_name" { + description = "Name of the AKS cluster hosting the namespace." + value = var.aks_cluster_name +} + +output "cluster_resource_group_name" { + description = "Resource group of the AKS cluster hosting the namespace." + value = var.aks_resource_group_name +} + +output "summary" { + description = "Markdown summary shown in meshPanel after the run." + value = <<-EOT + # AKS namespace `${kubernetes_namespace_v1.this.metadata[0].name}` + + Created on cluster `${var.aks_cluster_name}` (resource group `${var.aks_resource_group_name}`). + + Fetch credentials with: + + ``` + az aks get-credentials --name ${var.aks_cluster_name} --resource-group ${var.aks_resource_group_name} + kubectl config set-context --current --namespace ${kubernetes_namespace_v1.this.metadata[0].name} + ``` + + Quota: ${var.resource_quota.requests_cpu} CPU / ${var.resource_quota.requests_memory} requested, + ${var.resource_quota.limits_cpu} CPU / ${var.resource_quota.limits_memory} limit, ${var.resource_quota.pods} pods. + EOT +} diff --git a/modules/azure/aks-namespace/buildingblock/provider.tf b/modules/azure/aks-namespace/buildingblock/provider.tf new file mode 100644 index 000000000..305b714fb --- /dev/null +++ b/modules/azure/aks-namespace/buildingblock/provider.tf @@ -0,0 +1,45 @@ +provider "azurerm" { + features {} +} + +data "azurerm_client_config" "current" {} + +data "azurerm_kubernetes_cluster" "this" { + name = var.aks_cluster_name + resource_group_name = var.aks_resource_group_name +} + +# Exchanges the runner's federated token for an Entra token for the AKS API server. This is what +# `kubelogin get-token --login workloadidentity` does; doing it here keeps the run free of any +# tool on the runner host (see module-layout.md, Pre-run Scripts). The token is valid for about an +# hour and lands in the run's state, which is why it is the only credential this block handles. +data "http" "aks_token" { + url = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/oauth2/v2.0/token" + method = "POST" + + request_headers = { + Content-Type = "application/x-www-form-urlencoded" + } + + request_body = join("&", [ + "grant_type=client_credentials", + "client_id=${data.azurerm_client_config.current.client_id}", + # Well-known application ID of AKS's Entra server app, the audience every AKS cluster accepts. + "scope=6dae42f8-4368-4678-94ff-3960e28e3630/.default", + "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_assertion=${urlencode(trimspace(file(var.oidc_token_file_path)))}", + ]) + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "Entra rejected the token exchange with HTTP ${self.status_code}. Check the backplane's federated credential." + } + } +} + +provider "kubernetes" { + host = data.azurerm_kubernetes_cluster.this.kube_config[0].host + cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.this.kube_config[0].cluster_ca_certificate) + token = sensitive(jsondecode(data.http.aks_token.response_body).access_token) +} diff --git a/modules/azure/aks-namespace/buildingblock/variables.tf b/modules/azure/aks-namespace/buildingblock/variables.tf new file mode 100644 index 000000000..17ff19d80 --- /dev/null +++ b/modules/azure/aks-namespace/buildingblock/variables.tf @@ -0,0 +1,75 @@ +variable "aks_cluster_name" { + type = string + nullable = false + description = "Name of the AKS cluster the namespace is created in." +} + +variable "aks_resource_group_name" { + type = string + nullable = false + description = "Resource group of the AKS cluster." +} + +variable "workspace_identifier" { + type = string + nullable = false + description = "Identifier of the meshStack workspace that owns the namespace. Recorded as a label." +} + +variable "namespace_name" { + type = string + nullable = false + description = "Name of the Kubernetes namespace to create." + + validation { + condition = can(regex("^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$", var.namespace_name)) + error_message = "Must be a valid DNS label: lowercase alphanumerics and dashes, at most 63 characters, starting and ending alphanumeric." + } +} + +variable "admin_group_object_ids" { + type = list(string) + default = [] + description = "Entra group object IDs granted the `admin` cluster role inside the namespace." +} + +variable "edit_group_object_ids" { + type = list(string) + default = [] + description = "Entra group object IDs granted the `edit` cluster role inside the namespace." +} + +variable "view_group_object_ids" { + type = list(string) + default = [] + description = "Entra group object IDs granted the `view` cluster role inside the namespace." +} + +variable "resource_quota" { + type = object({ + requests_cpu = optional(string, "2") + requests_memory = optional(string, "4Gi") + limits_cpu = optional(string, "4") + limits_memory = optional(string, "8Gi") + pods = optional(string, "50") + }) + default = {} + description = "ResourceQuota applied to the namespace." +} + +variable "default_container_limits" { + type = object({ + cpu_request = optional(string, "100m") + memory_request = optional(string, "128Mi") + cpu_limit = optional(string, "500m") + memory_limit = optional(string, "512Mi") + }) + default = {} + description = "LimitRange defaults for containers that declare no requests or limits of their own." +} + +variable "oidc_token_file_path" { + type = string + default = "/var/run/secrets/workload-identity/azure/token" + description = "File the building block runner writes its federated OIDC token to." +} diff --git a/modules/azure/aks-namespace/buildingblock/versions.tf b/modules/azure/aks-namespace/buildingblock/versions.tf new file mode 100644 index 000000000..81d1fb487 --- /dev/null +++ b/modules/azure/aks-namespace/buildingblock/versions.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.64.0, < 5.0.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.35.1, < 3.0.0" + } + http = { + source = "hashicorp/http" + version = ">= 3.4.0, < 4.0.0" + } + } +} diff --git a/modules/azure/aks-namespace/e2e/main.tf b/modules/azure/aks-namespace/e2e/main.tf new file mode 100644 index 000000000..6d727414c --- /dev/null +++ b/modules/azure/aks-namespace/e2e/main.tf @@ -0,0 +1,67 @@ +variable "test_context" { + type = object({ + hub_git_ref = string + workspace = string + name_suffix = string + + fixtures = object({ + azure = object({ + subscription_uuid = string + entra_tenant_id = string + + # An AKS cluster with Azure RBAC for Kubernetes enabled, which the smoke-test harness has to + # provide: no other hub module needs a cluster. + aks = object({ + cluster_name = string + resource_group_name = string + }) + }) + }) + }) + + nullable = false +} + +module "aks_namespace" { + source = "../" + + meshstack = { + owning_workspace_identifier = var.test_context.workspace + tags = {} + } + + hub = { + git_ref = var.test_context.hub_git_ref + bbd_draft = true + } + + azure_tenant_id = var.test_context.fixtures.azure.entra_tenant_id + azure_subscription_id = var.test_context.fixtures.azure.subscription_uuid + azure_scope = "/subscriptions/${var.test_context.fixtures.azure.subscription_uuid}" + azure_location = "westeurope" + aks_cluster_name = var.test_context.fixtures.azure.aks.cluster_name + aks_resource_group_name = var.test_context.fixtures.azure.aks.resource_group_name + + # Unique backplane name per test run so role definitions don't clash across concurrent/retried runs. + backplane_name = "hub-e2e-aksns-${var.test_context.name_suffix}" +} + +resource "meshstack_building_block" "this" { + depends_on = [module.aks_namespace] + + wait_for_completion = true + + spec = { + building_block_definition_version_ref = module.aks_namespace.building_block_definition.version_ref + + display_name = "smoke-test-aks-namespace-${var.test_context.name_suffix}" + target_ref = { + kind = "meshWorkspace" + name = var.test_context.workspace + } + + inputs = { + namespace_name = { value = jsonencode("e2e-${substr(var.test_context.name_suffix, 0, 10)}") } + } + } +} diff --git a/modules/azure/aks-namespace/e2e/provider.tf b/modules/azure/aks-namespace/e2e/provider.tf new file mode 100644 index 000000000..693259040 --- /dev/null +++ b/modules/azure/aks-namespace/e2e/provider.tf @@ -0,0 +1,6 @@ +provider "azurerm" { + subscription_id = var.test_context.fixtures.azure.subscription_uuid + tenant_id = var.test_context.fixtures.azure.entra_tenant_id + + features {} +} diff --git a/modules/azure/aks-namespace/e2e/terraform.tf b/modules/azure/aks-namespace/e2e/terraform.tf new file mode 100644 index 000000000..39e32bf61 --- /dev/null +++ b/modules/azure/aks-namespace/e2e/terraform.tf @@ -0,0 +1,12 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + } + azurerm = { + source = "hashicorp/azurerm" + } + } +} diff --git a/modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl b/modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl new file mode 100644 index 000000000..fdbb6bb6b --- /dev/null +++ b/modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl @@ -0,0 +1,11 @@ +run "azure_aks_namespace_hub" { + assert { + condition = meshstack_building_block.this.status.status == "SUCCEEDED" + error_message = "azure/aks-namespace hub building block expected SUCCEEDED, got ${meshstack_building_block.this.status.status}" + } + + assert { + condition = startswith(jsondecode(meshstack_building_block.this.status.outputs["namespace_name"].value), "e2e-") + error_message = "expected namespace_name to start with 'e2e-', got ${jsondecode(meshstack_building_block.this.status.outputs["namespace_name"].value)}" + } +} diff --git a/modules/azure/aks-namespace/meshstack_integration.tf b/modules/azure/aks-namespace/meshstack_integration.tf new file mode 100644 index 000000000..21e1e646f --- /dev/null +++ b/modules/azure/aks-namespace/meshstack_integration.tf @@ -0,0 +1,337 @@ +variable "azure_tenant_id" { + type = string + description = "Azure Entra tenant ID used for provider authentication." +} + +variable "azure_subscription_id" { + type = string + description = "Azure subscription that hosts the AKS cluster." +} + +variable "azure_scope" { + type = string + description = "Azure management group or subscription scope the custom role definition is assignable at. Must contain the AKS cluster." +} + +variable "azure_location" { + type = string + description = "Azure region for the backplane's managed identity." +} + +variable "aks_cluster_name" { + type = string + description = "Name of the existing AKS cluster namespaces are created in." +} + +variable "aks_resource_group_name" { + type = string + description = "Resource group of the AKS cluster." +} + +variable "acr_id" { + type = string + default = null + description = "Resource ID of the Azure Container Registry workloads pull from. When set, the cluster's kubelet identity gets AcrPull on it." +} + +variable "backplane_name" { + type = string + default = "azure-aks-namespace" + description = "Name for the backplane resources (UAMI, role definition). Must match pattern ^[-a-z0-9]+$." +} + +variable "notification_subscribers" { + type = list(string) + default = [] + description = "List of email addresses to notify on building block lifecycle events." +} + +variable "bbd_display_name" { + type = string + default = null + description = "Overrides the name of the marketplace entry application teams see in the catalog." +} + +variable "bbd_description" { + type = string + default = null + description = "Overrides the one-line description shown next to the marketplace entry." +} + +variable "bbd_readme" { + type = string + default = null + description = "Overrides the markdown readme shown in the marketplace before ordering." +} + +variable "building_block_runner_uuid" { + type = string + default = null + description = "Runs this building block on the given meshStack building block runner instead of the shared one meshStack hosts." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to building block definition metadata." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = { + git_ref = "main" + bbd_draft = true + } + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition version is kept in draft mode. + EOT +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/aks-namespace/backplane?ref=${var.hub.git_ref}" + + name = var.backplane_name + scope = var.azure_scope + location = var.azure_location + + aks = { + cluster_name = var.aks_cluster_name + resource_group_name = var.aks_resource_group_name + } + acr_id = var.acr_id + + workload_identity_federation = { + issuer = meshstack_building_block_definition.this.version_latest.workload_identity_federation.issuer + subjects = [meshstack_building_block_definition.this.version_latest.workload_identity_federation.subject] + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = coalesce(var.bbd_display_name, "Azure AKS Namespace") + description = coalesce(var.bbd_description, "A Kubernetes namespace on the shared AKS cluster, with a resource quota and access for your Entra groups.") + support_url = "mailto:support@meshcloud.io" + documentation_url = "https://hub.meshcloud.io/platforms/azure/definitions/azure-aks-namespace" + notification_subscribers = var.notification_subscribers + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/azure/aks-namespace/buildingblock/logo.png" + target_type = "WORKSPACE_LEVEL" + + readme = coalesce(var.bbd_readme, chomp(<<-EOT + Creates a **Kubernetes namespace** on the platform team's AKS cluster for your workspace. You + get an isolated place to run workloads, with a resource quota and default container limits + already in place. + + ## 🎯 When to use it + + Use this building block when you: + - need a namespace to deploy a containerised application to + - want your team's Entra groups to manage it without cluster-wide rights + + ## 💡 Usage examples + + **Example 1: A dev and a prod namespace** + Order the block twice, once as `myapp-dev` and once as `myapp-prod`, and give each a different quota. + + **Example 2: Read-only access for auditors** + Add your auditors' Entra group to **View Group Object IDs** so they can inspect workloads without changing them. + + ## 📊 Shared Responsibility + + | Responsibility | Platform Team | Application Team | + |---|:---:|:---:| + | Operate the AKS cluster and its identity | ✅ | ❌ | + | Set up the backplane identity and permissions | ✅ | ❌ | + | Choose the namespace name and quota | ❌ | ✅ | + | Decide which Entra groups get access | ❌ | ✅ | + | Deploy and operate workloads in the namespace | ❌ | ✅ | + EOT + )) + } + + version_spec = { + draft = var.hub.bbd_draft + + deletion_mode = "DELETE" + runner_ref = var.building_block_runner_uuid == null ? null : { + kind = "meshBuildingBlockRunner" + uuid = var.building_block_runner_uuid + } + + implementation = { + terraform = { + terraform_version = "1.12.5" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/azure/aks-namespace/buildingblock" + ref_name = var.hub.git_ref + use_mesh_http_backend_fallback = true + } + } + + inputs = { + ARM_CLIENT_ID = { + type = "STRING" + display_name = "ARM Client ID" + description = "Client ID of the UAMI used to authenticate with Azure." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.backplane.identity.client_id) + } + ARM_TENANT_ID = { + type = "STRING" + display_name = "ARM Tenant ID" + description = "Azure Entra tenant ID for authentication." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.azure_tenant_id) + } + ARM_SUBSCRIPTION_ID = { + type = "STRING" + display_name = "ARM Subscription ID" + description = "The Azure subscription that hosts the AKS cluster." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.azure_subscription_id) + } + ARM_USE_OIDC = { + type = "STRING" + display_name = "ARM Use OIDC" + description = "Enables OIDC-based workload identity federation for the Azure provider." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("true") + } + ARM_OIDC_TOKEN_FILE_PATH = { + type = "STRING" + display_name = "ARM OIDC Token File Path" + description = "Path to the OIDC token file used for workload identity federation authentication." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("/var/run/secrets/workload-identity/azure/token") + } + aks_cluster_name = { + type = "STRING" + display_name = "AKS Cluster Name" + description = "Name of the AKS cluster the namespace is created in." + assignment_type = "STATIC" + argument = jsonencode(var.aks_cluster_name) + } + aks_resource_group_name = { + type = "STRING" + display_name = "AKS Resource Group" + description = "Resource group of the AKS cluster." + assignment_type = "STATIC" + argument = jsonencode(var.aks_resource_group_name) + } + workspace_identifier = { + type = "STRING" + display_name = "Workspace Identifier" + description = "The meshStack workspace that owns the namespace." + assignment_type = "WORKSPACE_IDENTIFIER" + } + namespace_name = { + type = "STRING" + display_name = "Namespace Name" + description = "Name of the Kubernetes namespace, e.g. 'myapp-dev'." + assignment_type = "USER_INPUT" + value_validation_regex = "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$" + validation_regex_error_message = "Lowercase letters, digits and dashes only, at most 63 characters, starting and ending with a letter or digit." + } + admin_group_object_ids = { + type = "CODE" + display_name = "Admin Group Object IDs" + description = "Entra group object IDs that get the 'admin' role in the namespace, as a JSON list." + assignment_type = "USER_INPUT" + default_value = jsonencode([]) + } + edit_group_object_ids = { + type = "CODE" + display_name = "Edit Group Object IDs" + description = "Entra group object IDs that get the 'edit' role in the namespace, as a JSON list." + assignment_type = "USER_INPUT" + default_value = jsonencode([]) + } + view_group_object_ids = { + type = "CODE" + display_name = "View Group Object IDs" + description = "Entra group object IDs that get the 'view' role in the namespace, as a JSON list." + assignment_type = "USER_INPUT" + default_value = jsonencode([]) + } + resource_quota = { + type = "CODE" + display_name = "Resource Quota" + description = "JSON object with requests_cpu, requests_memory, limits_cpu, limits_memory and pods." + assignment_type = "USER_INPUT" + default_value = jsonencode({ + requests_cpu = "2" + requests_memory = "4Gi" + limits_cpu = "4" + limits_memory = "8Gi" + pods = "50" + }) + } + } + + outputs = { + namespace_name = { + type = "STRING" + display_name = "Namespace Name" + description = "Name of the created Kubernetes namespace." + assignment_type = "NONE" + } + cluster_name = { + type = "STRING" + display_name = "AKS Cluster Name" + description = "Name of the AKS cluster hosting the namespace." + assignment_type = "NONE" + } + cluster_resource_group_name = { + type = "STRING" + display_name = "AKS Resource Group" + description = "Resource group of the AKS cluster hosting the namespace." + assignment_type = "NONE" + } + summary = { + type = "STRING" + display_name = "Summary" + description = "Markdown summary shown in meshPanel after the run." + assignment_type = "SUMMARY" + } + } + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.26.2" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.64.0, < 5.0.0" + } + } +} From d370a35c4891814e37ac5bfa5f103a62c2b864bb Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Mon, 12 Oct 2026 12:08:20 +0200 Subject: [PATCH 02/11] feat(aks/github-connector)!: run at workspace level against a parent namespace block The connector no longer targets a meshStack tenant. It reads its namespace from the namespace_name output of the Azure AKS Namespace building block it is ordered under, so aks.namespace_definition_uuid is a new required input. Drops the empty main.tf and outputs.tf stubs. Co-Authored-By: Claude Sonnet 5.5 --- .../github-connector/buildingblock/main.tf | 1 - .../github-connector/buildingblock/outputs.tf | 1 - .../github-connector/meshstack_integration.tf | 27 +++++++++---------- 3 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 modules/aks/github-connector/buildingblock/main.tf delete mode 100644 modules/aks/github-connector/buildingblock/outputs.tf diff --git a/modules/aks/github-connector/buildingblock/main.tf b/modules/aks/github-connector/buildingblock/main.tf deleted file mode 100644 index 7faa034cb..000000000 --- a/modules/aks/github-connector/buildingblock/main.tf +++ /dev/null @@ -1 +0,0 @@ -# TODO: Implement main.tf diff --git a/modules/aks/github-connector/buildingblock/outputs.tf b/modules/aks/github-connector/buildingblock/outputs.tf deleted file mode 100644 index d3c766438..000000000 --- a/modules/aks/github-connector/buildingblock/outputs.tf +++ /dev/null @@ -1 +0,0 @@ -# TODO: Implement outputs.tf diff --git a/modules/aks/github-connector/meshstack_integration.tf b/modules/aks/github-connector/meshstack_integration.tf index 8822ff56c..0c23cd346 100644 --- a/modules/aks/github-connector/meshstack_integration.tf +++ b/modules/aks/github-connector/meshstack_integration.tf @@ -11,9 +11,10 @@ variable "github" { variable "aks" { type = object({ + namespace_definition_uuid = string connector_config_tf_base64 = string }) - description = "AKS integration configuration. `connector_config_tf_base64` is the base64-encoded `config.tf` payload passed into the run (for example: `data:application/octet-stream;base64,ZXhhbXBsZQ==`)." + description = "AKS integration configuration. `namespace_definition_uuid` is the UUID of the deployed Azure AKS Namespace building block definition; the connector wires itself to the namespace of its parent block of that definition. `connector_config_tf_base64` is the base64-encoded `config.tf` payload passed into the run (for example: `data:application/octet-stream;base64,ZXhhbXBsZQ==`)." } variable "notification_subscribers" { @@ -85,8 +86,7 @@ resource "meshstack_building_block_definition" "aks_github_connector" { display_name = coalesce(var.bbd_display_name, "GitHub Actions AKS Connector") notification_subscribers = var.notification_subscribers symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/aks/github-connector/buildingblock/logo.png" - target_type = "TENANT_LEVEL" - supported_platforms = [{ name = "AZURE_KUBERNETES_SERVICE" }] + target_type = "WORKSPACE_LEVEL" readme = coalesce(var.bbd_readme, chomp(< Date: Mon, 12 Oct 2026 12:10:50 +0200 Subject: [PATCH 03/11] feat(aks/starterkit)!: order workspace namespace blocks instead of projects and tenants The starter kit no longer creates meshStack projects, tenants or the creator's project binding. It orders one Azure AKS Namespace building block per stage and wires a GitHub Actions connector to each, all at workspace level. platform_ref, landing_zone_refs, project_tags and the creator input are gone; building_block_definition_version_refs now also needs an aks-namespace entry, and namespace_group_object_ids controls who gets access. Co-Authored-By: Claude Sonnet 5.5 --- .../aks/starterkit/buildingblock/README.md | 13 +- modules/aks/starterkit/buildingblock/main.tf | 172 +++++------------- .../aks/starterkit/buildingblock/outputs.tf | 15 +- .../aks/starterkit/buildingblock/variables.tf | 50 ++--- .../aks/starterkit/meshstack_integration.tf | 108 ++++------- 5 files changed, 102 insertions(+), 256 deletions(-) diff --git a/modules/aks/starterkit/buildingblock/README.md b/modules/aks/starterkit/buildingblock/README.md index 6d9727c62..87652feed 100644 --- a/modules/aks/starterkit/buildingblock/README.md +++ b/modules/aks/starterkit/buildingblock/README.md @@ -26,10 +26,8 @@ No modules. | Name | Type | |------|------| | [meshstack_building_block.github_actions](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block) | resource | +| [meshstack_building_block.namespace](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block) | resource | | [meshstack_building_block.repo](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block) | resource | -| [meshstack_project.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/project) | resource | -| [meshstack_project_user_binding.creator_admin](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/project_user_binding) | resource | -| [meshstack_tenant.this](https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/tenant) | resource | | [random_id.repo_suffix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource | ## Inputs @@ -38,15 +36,12 @@ No modules. |------|-------------|------|---------|:--------:| | [apps\_base\_domain](#input\_apps\_base\_domain) | Base domain used for application URLs (e.g. 'likvid-k8s.msh.host'). The app subdomain will be prefixed to this value. | `string` | `"likvid-k8s.msh.host"` | no | | [archive\_repo\_on\_destroy](#input\_archive\_repo\_on\_destroy) | Whether to archive github repository when destroying the terraform resource, or delete it. Defaults to true (archive). | `bool` | `true` | no | -| [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | Building block definition versions this starter kit creates its child building blocks from, keyed by definition name (`git-repository` and `github-actions-connector`). |
map(object({
uuid = string
}))
| n/a | yes | -| [creator](#input\_creator) | Information about the creator of the resources who will be assigned Project Admin role |
object({
type = string
identifier = string
displayName = string
username = optional(string)
email = optional(string)
euid = optional(string)
})
| n/a | yes | +| [building\_block\_definition\_version\_refs](#input\_building\_block\_definition\_version\_refs) | Building block definition versions this starter kit creates its child building blocks from, keyed by definition name (`git-repository`, `aks-namespace` and `github-actions-connector`). |
map(object({
uuid = string
}))
| n/a | yes | | [github\_org](#input\_github\_org) | GitHub organization name. Used only for display purposes. | `string` | n/a | yes | | [github\_repo\_input\_repo\_visibility](#input\_github\_repo\_input\_repo\_visibility) | Visibility of the GitHub repository (e.g., public, private). | `string` | `"private"` | no | | [github\_template\_repo\_path](#input\_github\_template\_repo\_path) | GitHub repository template to use when creating the application repository, in the format 'owner/repo'. | `string` | `"likvid-bank/aks-starterkit-template"` | no | -| [landing\_zone\_refs](#input\_landing\_zone\_refs) | Landing zone references keyed by stage (usually dev and prod). Wired in as a static building block input from the platform/backplane that owns the meshLandingZones (their `.ref` outputs). | `map(object({ name = string, kind = optional(string, "meshLandingZone") }))` | n/a | yes | -| [name](#input\_name) | This name will be used for the created projects, app subdomain and GitHub repository. | `string` | n/a | yes | -| [platform\_ref](#input\_platform\_ref) | Reference (by uuid) to the meshPlatform the tenants are created on. Wired in as a static building block input from the platform/backplane that owns the meshPlatform (its `.ref` output). Required because the meshTenant v4 API references platforms by ref. |
object({
uuid = string
kind = optional(string, "meshPlatform")
})
| n/a | yes | -| [project\_tags](#input\_project\_tags) | Tags for the created Dev/Prod projects. |
object({
dev = map(list(string))
prod = map(list(string))

owner_tag_key = optional(string, null)
})
| n/a | yes | +| [name](#input\_name) | This name will be used for the created namespaces, app subdomain and GitHub repository. | `string` | n/a | yes | +| [namespace\_group\_object\_ids](#input\_namespace\_group\_object\_ids) | Entra group object IDs granted the admin, edit and view role in every created namespace. |
object({
admin = optional(list(string), [])
edit = optional(list(string), [])
view = optional(list(string), [])
})
| `{}` | no | | [repo\_admin](#input\_repo\_admin) | GitHub handle of the user who will be assigned as the repository admin. Delete building block definition input if not needed. | `string` | `null` | no | | [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes | diff --git a/modules/aks/starterkit/buildingblock/main.tf b/modules/aks/starterkit/buildingblock/main.tf index 284789196..38ab4a439 100644 --- a/modules/aks/starterkit/buildingblock/main.tf +++ b/modules/aks/starterkit/buildingblock/main.tf @@ -5,11 +5,14 @@ locals { repo_name = "${local.identifier}-${random_id.repo_suffix.hex}" - # GitHub Actions environment name per stage. Indexed by stage key (fails on an unknown stage). + # GitHub Actions environment name per stage. Its keys are the stages the starter kit creates a + # namespace for. github_environment_names = { dev = "development" prod = "production" } + + stages = toset(keys(local.github_environment_names)) } # Generate a random suffix for the repository name to ensure uniqueness @@ -17,69 +20,6 @@ resource "random_id" "repo_suffix" { byte_length = 4 // 8 hex characters } -resource "meshstack_project" "this" { - for_each = var.landing_zone_refs - - metadata = { - name = "${local.identifier}-${each.key}" - owned_by_workspace = var.workspace_identifier - } - spec = { - display_name = "${var.name} ${title(each.key)}" - tags = merge( - var.project_tags[each.key], - var.project_tags.owner_tag_key == null ? {} : { - (var.project_tags.owner_tag_key) : [var.creator.displayName] - } - ) - } -} - -resource "meshstack_project_user_binding" "creator_admin" { - for_each = var.creator.type == "User" && var.creator.username != null ? var.landing_zone_refs : {} - - metadata = { - name = uuid() - } - - role_ref = { - name = "Project Admin" - } - - target_ref = { - owned_by_workspace = var.workspace_identifier - name = meshstack_project.this[each.key].metadata.name - } - - subject = { - name = var.creator.username - } -} - -resource "meshstack_tenant" "this" { - for_each = var.landing_zone_refs - - metadata = { - owned_by_workspace = var.workspace_identifier - owned_by_project = meshstack_project.this[each.key].metadata.name - } - - spec = { - platform_ref = var.platform_ref - landing_zone_ref = var.landing_zone_refs[each.key] - } - - # This orders the destroy, not the create. Deleted in parallel, the tenant and the binding are both - # the run's first write, so both run the same `INSERT ... ON DUPLICATE KEY UPDATE` on meshStack's - # Author table for the run's fresh ephemeral API key. The two deadlock, the loser's transaction is - # marked rollback-only, and its delete returns a 500 that fails the whole destroy run. Ordering them - # means the first delete creates the Author row and the second one finds it. - # - # A band-aid: meshStack does not retry on a deadlock. Remove it once meshStack does. This is not the - # BD-2288 create-ordering workaround that ddd5dba removed from here — that one is still fixed. - depends_on = [meshstack_project_user_binding.creator_admin] -} - resource "meshstack_building_block" "repo" { spec = { building_block_definition_version_ref = var.building_block_definition_version_refs["git-repository"] @@ -123,14 +63,53 @@ resource "meshstack_building_block" "repo" { } } +resource "meshstack_building_block" "namespace" { + for_each = local.stages + + spec = { + building_block_definition_version_ref = var.building_block_definition_version_refs["aks-namespace"] + + display_name = "${var.name} ${title(each.key)} namespace" + target_ref = { + kind = "meshWorkspace" + name = var.workspace_identifier + } + + inputs = { + namespace_name = { + value = jsonencode("${local.identifier}-${each.key}") + } + admin_group_object_ids = { + value = jsonencode(var.namespace_group_object_ids.admin) + } + edit_group_object_ids = { + value = jsonencode(var.namespace_group_object_ids.edit) + } + view_group_object_ids = { + value = jsonencode(var.namespace_group_object_ids.view) + } + } + } + + lifecycle { + postcondition { + condition = self.status.status == "SUCCEEDED" + error_message = "Building block ${self.metadata.uuid} is ${self.status.status}, not SUCCEEDED. See its run in meshPanel." + } + } +} + resource "meshstack_building_block" "github_actions" { - for_each = var.landing_zone_refs + for_each = local.stages spec = { building_block_definition_version_ref = var.building_block_definition_version_refs["github-actions-connector"] - target_ref = meshstack_tenant.this[each.key].ref - display_name = "GHA Connector ${title(each.key)}" - parent_building_block_refs = [meshstack_building_block.repo.ref] + target_ref = { + kind = "meshWorkspace" + name = var.workspace_identifier + } + display_name = "GHA Connector ${title(each.key)}" + parent_building_block_refs = [meshstack_building_block.repo.ref, meshstack_building_block.namespace[each.key].ref] inputs = { github_environment_name = { value = jsonencode(local.github_environment_names[each.key]) @@ -138,7 +117,7 @@ resource "meshstack_building_block" "github_actions" { additional_environment_variables = { value = jsonencode(jsonencode({ "DOMAIN_NAME" = each.key == "prod" ? local.identifier : "${local.identifier}-${each.key}" - "AKS_NAMESPACE_NAME" = meshstack_tenant.this[each.key].spec.platform_tenant_id + "AKS_NAMESPACE_NAME" = jsondecode(meshstack_building_block.namespace[each.key].status.outputs.namespace_name.value) })) } } @@ -151,58 +130,3 @@ resource "meshstack_building_block" "github_actions" { } } } - -# --- State address migrations (no resource recreation) --- -# The building block moves are chained: hop 1 is the resource-type migration -# (meshstack_building_block_v2 -> meshstack_building_block), hop 2 is the dev/prod -> for_each address -# change. Terraform follows the chain, so a deployed meshstack_building_block_v2.github_actions_dev -# migrates v2 -> GA -> for_each in sequence without recreation. - -# Hop 1 — resource-type migrations. -moved { - from = meshstack_building_block_v2.repo - to = meshstack_building_block.repo -} -moved { - from = meshstack_building_block_v2.github_actions_dev - to = meshstack_building_block.github_actions_dev -} -moved { - from = meshstack_building_block_v2.github_actions_prod - to = meshstack_building_block.github_actions_prod -} - -# Hop 2 — spelled-out dev/prod -> for_each keys (this refactor). Projects and bindings have no -# hop-1 migration, so their move is the only hop. -moved { - from = meshstack_tenant.dev - to = meshstack_tenant.this["dev"] -} -moved { - from = meshstack_tenant.prod - to = meshstack_tenant.this["prod"] -} -moved { - from = meshstack_project.dev - to = meshstack_project.this["dev"] -} -moved { - from = meshstack_project.prod - to = meshstack_project.this["prod"] -} -moved { - from = meshstack_project_user_binding.creator_dev_admin[0] - to = meshstack_project_user_binding.creator_admin["dev"] -} -moved { - from = meshstack_project_user_binding.creator_prod_admin[0] - to = meshstack_project_user_binding.creator_admin["prod"] -} -moved { - from = meshstack_building_block.github_actions_dev - to = meshstack_building_block.github_actions["dev"] -} -moved { - from = meshstack_building_block.github_actions_prod - to = meshstack_building_block.github_actions["prod"] -} diff --git a/modules/aks/starterkit/buildingblock/outputs.tf b/modules/aks/starterkit/buildingblock/outputs.tf index 27aec462d..6e02d6408 100644 --- a/modules/aks/starterkit/buildingblock/outputs.tf +++ b/modules/aks/starterkit/buildingblock/outputs.tf @@ -25,13 +25,11 @@ This starter kit has set up the following resources in workspace `${var.workspac @buildingblock[${meshstack_building_block.repo.metadata.uuid}] -@project[${meshstack_project.this["dev"].metadata.owned_by_workspace}.${meshstack_project.this["dev"].metadata.name}]\ -    @tenant[${meshstack_tenant.this["dev"].metadata.uuid}]\ -        @buildingblock[${meshstack_building_block.github_actions["dev"].metadata.uuid}] +@buildingblock[${meshstack_building_block.namespace["dev"].metadata.uuid}]\ +    @buildingblock[${meshstack_building_block.github_actions["dev"].metadata.uuid}] -@project[${meshstack_project.this["prod"].metadata.owned_by_workspace}.${meshstack_project.this["prod"].metadata.name}]\ -    @tenant[${meshstack_tenant.this["prod"].metadata.uuid}]\ -        @buildingblock[${meshstack_building_block.github_actions["prod"].metadata.uuid}] +@buildingblock[${meshstack_building_block.namespace["prod"].metadata.uuid}]\ +    @buildingblock[${meshstack_building_block.github_actions["prod"].metadata.uuid}] --- @@ -69,9 +67,8 @@ View deployment status: [GitHub Actions](${jsondecode(meshstack_building_block.r - Check workflow status in the [Actions tab](<${jsondecode(meshstack_building_block.repo.status.outputs.repo_html_url.value)}/actions>) ### 3. Manage Access -- Invite team members via meshStack: - - [Dev Access](#/w/${var.workspace_identifier}/p/${meshstack_project.this["dev"].metadata.name}/access-management/role-mapping) - - [Prod Access](#/w/${var.workspace_identifier}/p/${meshstack_project.this["prod"].metadata.name}/access-management/role-mapping) +- Namespace access follows the Entra groups the platform team configured for this starter kit. + Ask them to add your team's group. --- diff --git a/modules/aks/starterkit/buildingblock/variables.tf b/modules/aks/starterkit/buildingblock/variables.tf index 8aa355a0e..426044f09 100644 --- a/modules/aks/starterkit/buildingblock/variables.tf +++ b/modules/aks/starterkit/buildingblock/variables.tf @@ -4,20 +4,7 @@ variable "workspace_identifier" { variable "name" { type = string - description = "This name will be used for the created projects, app subdomain and GitHub repository." -} - -variable "platform_ref" { - type = object({ - uuid = string - kind = optional(string, "meshPlatform") - }) - description = "Reference (by uuid) to the meshPlatform the tenants are created on. Wired in as a static building block input from the platform/backplane that owns the meshPlatform (its `.ref` output). Required because the meshTenant v4 API references platforms by ref." -} - -variable "landing_zone_refs" { - type = map(object({ name = string, kind = optional(string, "meshLandingZone") })) - description = "Landing zone references keyed by stage (usually dev and prod). Wired in as a static building block input from the platform/backplane that owns the meshLandingZones (their `.ref` outputs)." + description = "This name will be used for the created namespaces, app subdomain and GitHub repository." } variable "building_block_definition_version_refs" { @@ -25,7 +12,7 @@ variable "building_block_definition_version_refs" { type = map(object({ uuid = string })) - description = "Building block definition versions this starter kit creates its child building blocks from, keyed by definition name (`git-repository` and `github-actions-connector`)." + description = "Building block definition versions this starter kit creates its child building blocks from, keyed by definition name (`git-repository`, `aks-namespace` and `github-actions-connector`)." } variable "github_repo_input_repo_visibility" { @@ -45,33 +32,12 @@ variable "github_org" { description = "GitHub organization name. Used only for display purposes." } -variable "creator" { - type = object({ - type = string - identifier = string - displayName = string - username = optional(string) - email = optional(string) - euid = optional(string) - }) - description = "Information about the creator of the resources who will be assigned Project Admin role" -} - variable "repo_admin" { type = string description = "GitHub handle of the user who will be assigned as the repository admin. Delete building block definition input if not needed." default = null } -variable "project_tags" { - type = object({ - dev = map(list(string)) - prod = map(list(string)) - - owner_tag_key = optional(string, null) - }) - description = "Tags for the created Dev/Prod projects." -} variable "github_template_repo_path" { type = string description = "GitHub repository template to use when creating the application repository, in the format 'owner/repo'." @@ -82,4 +48,14 @@ variable "apps_base_domain" { type = string description = "Base domain used for application URLs (e.g. 'likvid-k8s.msh.host'). The app subdomain will be prefixed to this value." default = "likvid-k8s.msh.host" -} \ No newline at end of file +} + +variable "namespace_group_object_ids" { + type = object({ + admin = optional(list(string), []) + edit = optional(list(string), []) + view = optional(list(string), []) + }) + description = "Entra group object IDs granted the admin, edit and view role in every created namespace." + default = {} +} diff --git a/modules/aks/starterkit/meshstack_integration.tf b/modules/aks/starterkit/meshstack_integration.tf index a19bc06e4..30ffd43db 100644 --- a/modules/aks/starterkit/meshstack_integration.tf +++ b/modules/aks/starterkit/meshstack_integration.tf @@ -1,16 +1,8 @@ -variable "platform_ref" { - type = object({ - uuid = string - kind = optional(string, "meshPlatform") - }) - description = "Reference (by uuid) to the meshPlatform tenants are created on — e.g. the `.ref` output of the meshstack_platform resource/backplane that owns it. Wired to the building block as a static input; required since the meshTenant v4 API references platforms by ref." -} - variable "building_block_definition_version_refs" { type = map(object({ uuid = string })) - description = "Building block definition versions the starter kit creates its child building blocks from, keyed by definition name (`git-repository` and `github-actions-connector`)." + description = "Building block definition versions the starter kit creates its child building blocks from, keyed by definition name (`git-repository`, `aks-namespace` and `github-actions-connector`)." } variable "github_org" { @@ -28,9 +20,18 @@ variable "apps_base_domain" { description = "Base domain used for app URLs (example: `apps.prod.example.com`)." } -variable "landing_zone_refs" { - type = map(object({ name = string, kind = optional(string, "meshLandingZone") })) - description = "Landing zone references keyed by stage (usually dev and prod), e.g. the `.ref` outputs of the meshstack_landingzone resources/backplane that owns them." +variable "namespace_group_object_ids" { + type = object({ + admin = optional(list(string), []) + edit = optional(list(string), []) + view = optional(list(string), []) + }) + default = { + admin = [] + edit = [] + view = [] + } + description = "Entra group object IDs granted the admin, edit and view role in every namespace the starter kit creates." } variable "notification_subscribers" { @@ -38,18 +39,6 @@ variable "notification_subscribers" { default = [] } -variable "project_tags" { - type = object({ - dev = map(list(string)) - prod = map(list(string)) - }) - default = { - dev = { environment = ["dev"] } - prod = { environment = ["prod"] } - } - description = "Configure project tags of starter kit, for dev and prod." -} - variable "bbd_display_name" { type = string default = null @@ -101,7 +90,7 @@ output "building_block_definition" { } locals { - name_regex = "^[a-zA-Z0-9-]{0,24}$" # underscore and dots not allowed because of K8s namespace, max length of 25 because of project character limit and suffixes added by the building block + name_regex = "^[a-zA-Z0-9-]{0,24}$" # underscore and dots not allowed because of K8s namespace, max length of 25 because of the suffixes the building block adds to the name } resource "meshstack_building_block_definition" "aks_starterkit" { @@ -137,12 +126,10 @@ This building block is ideal for teams that: This building block automates the creation of the following resources: - **GitHub Repository**: A new repository to store your application code and Dockerfile. -- **Development Project**: You, as the creator, will have access to this project and AKS namespace. - - **AKS Namespace**: A dedicated Kubernetes namespace for development. - - **GitHub Actions Connector**: Connects the GitHub repository to the development AKS namespace via GitHub Actions and deploys after every commit to the main branch. -- **Production Project**: You, as the creator, will have access to this project and AKS namespace. - - **AKS Namespace**: A dedicated Kubernetes namespace for production. - - **GitHub Actions Connector**: Connects the GitHub repository to the production AKS namespace via GitHub Actions and deploys after every commit to the release branch. +- **Development namespace**: A dedicated Kubernetes namespace for development, with a resource quota. + - **GitHub Actions Connector**: Connects the GitHub repository to the development namespace via GitHub Actions and deploys after every commit to the main branch. +- **Production namespace**: A dedicated Kubernetes namespace for production, with a resource quota. + - **GitHub Actions Connector**: Connects the GitHub repository to the production namespace via GitHub Actions and deploys after every commit to the release branch. ## Shared Responsibilities @@ -152,7 +139,7 @@ This building block automates the creation of the following resources: | Create and manage Git repository | ✅ | ❌ | | Set up GitHub Actions CI/CD pipeline | ✅ | ❌ | | Build and scan Docker images | ✅ | ❌ | -| Manage Kubernetes namespaces (dev/prod) | ✅ | ❌ | +| Create Kubernetes namespaces (dev/prod) and grant access | ✅ | ❌ | | Manage resources inside namespaces | ❌ | ✅ | | Develop and maintain application source code | ❌ | ✅ | | Maintain application configurations | ❌ | ✅ | @@ -177,34 +164,8 @@ EOT } } inputs = { - "creator" = { - assignment_type = "AUTHOR" - description = "Information about the creator of the resources who will be assigned Project Admin role" - display_name = "Creator" - is_environment = false - type = "CODE" - updateable_by_consumer = false - } - "platform_ref" = { - # jsonencode twice is correct for structured inputs, see the project_tags input below. - argument = jsonencode(jsonencode(var.platform_ref)) - assignment_type = "STATIC" - display_name = "Platform Reference" - is_environment = false - type = "CODE" - updateable_by_consumer = false - } - "landing_zone_refs" = { - # jsonencode twice is correct for structured inputs, see the project_tags input below. - argument = jsonencode(jsonencode(var.landing_zone_refs)) - assignment_type = "STATIC" - display_name = "Landing Zone References for Dev/Prod." - is_environment = false - type = "CODE" - updateable_by_consumer = false - } "building_block_definition_version_refs" = { - # jsonencode twice is correct for structured inputs, see the project_tags input below. + # jsonencode twice is correct for structured inputs, see the namespace_group_object_ids input below. argument = jsonencode(jsonencode(var.building_block_definition_version_refs)) assignment_type = "STATIC" description = "Definition versions the starter kit creates its child building blocks from." @@ -248,21 +209,23 @@ EOT } "name" = { assignment_type = "USER_INPUT" - description = "This name will be used for the created projects, AKS namespaces and GitHub repository." - display_name = "Name of the Project" + description = "This name will be used for the created AKS namespaces and GitHub repository." + display_name = "Name" is_environment = false type = "STRING" updateable_by_consumer = false value_validation_regex = local.name_regex validation_regex_error_message = "No underscore/dots/spaces are allowed. A maximum length of 25 characters is allowed." } - "project_tags" = { - # jsonencode twice is correct, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 - argument = jsonencode(jsonencode(var.project_tags)) - assignment_type = "STATIC" - description = "Tags for the created Dev/Prod projects." - display_name = "Project Tags" - type = "CODE" + "namespace_group_object_ids" = { + # jsonencode twice is correct for structured inputs, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 + argument = jsonencode(jsonencode(var.namespace_group_object_ids)) + assignment_type = "STATIC" + description = "Entra groups that get the admin, edit and view role in the created namespaces." + display_name = "Namespace Group Object IDs" + is_environment = false + type = "CODE" + updateable_by_consumer = false } "workspace_identifier" = { assignment_type = "WORKSPACE_IDENTIFIER" @@ -308,15 +271,6 @@ EOT "BUILDINGBLOCK_DELETE", "BUILDINGBLOCK_LIST", "BUILDINGBLOCK_SAVE", - "PROJECTPRINCIPALROLE_DELETE", - "PROJECTPRINCIPALROLE_LIST", - "PROJECTPRINCIPALROLE_SAVE", - "PROJECT_DELETE", - "PROJECT_LIST", - "PROJECT_SAVE", - "TENANT_DELETE", - "TENANT_LIST", - "TENANT_SAVE", ] } } From b4c2363b0d6fe58a05a7f90f13cfe8bf93ebeb14 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Mon, 12 Oct 2026 12:12:27 +0200 Subject: [PATCH 04/11] feat(azure-kubernetes)!: rebuild the reference architecture on workspace building blocks only Drops the meshStack project, tenant, platform and landing zone from the architecture and its diagram. The starterkit now orders azure/aks-namespace and aks/github-connector blocks directly in the workspace. Co-Authored-By: Claude Sonnet 5.5 --- .../azure-kubernetes/README.md | 101 ++++++------ .../azure-kubernetes/azure-kubernetes.dot | 13 +- .../azure-kubernetes/azure-kubernetes.svg | 146 +++++++----------- 3 files changed, 118 insertions(+), 142 deletions(-) diff --git a/reference-architectures/azure-kubernetes/README.md b/reference-architectures/azure-kubernetes/README.md index 8e739be8b..e31fd129e 100644 --- a/reference-architectures/azure-kubernetes/README.md +++ b/reference-architectures/azure-kubernetes/README.md @@ -1,15 +1,16 @@ --- name: Azure Kubernetes Platform description: > - A self-service Azure Kubernetes platform that gives application teams their own - AKS namespaces with GitHub repositories, GitHub Actions CI/CD pipelines, - and a container registry backed by Azure Container Registry — all orchestrated - through a single starterkit building block. + A self-service Azure Kubernetes platform built from workspace building blocks only. Application + teams order a starterkit that creates their AKS namespaces, a GitHub repository and GitHub Actions + CI/CD pipelines, without any meshStack project, tenant, platform or landing zone. cloudProviders: - azure buildingBlocks: - path: aks/starterkit role: Orchestrates the full developer onboarding experience by composing all building blocks into one self-service offering. + - path: azure/aks-namespace + role: Creates a Kubernetes namespace with a resource quota, default limits and Entra group access on an existing AKS cluster. - path: github/repository role: Creates GitHub repositories with predefined configurations and access control for application teams. - path: aks/github-connector @@ -20,18 +21,17 @@ buildingBlocks: ## Overview -The **Azure Kubernetes Platform** (AKS) reference architecture delivers a complete, self-service -Kubernetes experience on Azure. It combines three Hub building blocks into a cohesive -platform that allows application teams to go from "I need a place to run my app" to -"my code is deployed" in minutes — without filing tickets or waiting for manual -provisioning. +The **Azure Kubernetes Platform** reference architecture delivers a self-service Kubernetes +experience on Azure from four Hub building blocks. Application teams go from "I need a place to run +my app" to "my code is deployed" in minutes, without filing tickets. -It assumes the platform team already operates one or more AKS clusters. This architecture -layers developer self-service on top of those clusters. +Every building block runs at **workspace level**. There is no meshStack project, tenant, platform +or landing zone in this architecture: a namespace is a building block, not a tenant. The platform +team still operates the AKS cluster; this architecture layers developer self-service on top of it. **Target audience:** -- **Platform engineers** who already have AKS clusters and want to add self-service onboarding. +- **Platform engineers** who already have an AKS cluster and want to add self-service onboarding. - **Application teams** who need a fast, secure path to Kubernetes with built-in CI/CD. ## Architecture Diagram @@ -40,60 +40,75 @@ layers developer self-service on top of those clusters. ## How It Works -### 1. Developer Starterkit — `aks/starterkit` +### 1. Developer Starterkit: `aks/starterkit` -The starterkit is a **meshStack building block composition** that application teams -request from the self-service catalog. When a developer clicks "Order", the starterkit -automatically: +The starterkit is a **building block composition** that application teams order from the +self-service catalog. When a developer clicks "Order", it: -1. Creates a **GitHub repository** from a configurable template - (`github/repository`). -2. Creates a **dev project** with a dedicated AKS namespace and wires it to the repo - via a **GitHub Actions connector** (`aks/github-connector`). -3. Creates a **prod project** with a separate AKS namespace and connector that - triggers on the `release` branch. -4. Grants the requesting developer **Project Admin** on both projects. +1. Creates a **GitHub repository** from a configurable template (`github/repository`). +2. Creates a **dev** and a **prod** namespace (`azure/aks-namespace`). +3. Wires each namespace to the repository with a **GitHub Actions connector** + (`aks/github-connector`). Dev deploys from `main`, prod from `release`. -### 2. GitHub Repository — `github/repository` +Each connector is ordered with the repository and its namespace as parent building blocks, and +reads the namespace name from the namespace block's output. -Each application team gets its own repository with: +### 2. Namespace: `azure/aks-namespace` -- Predefined structure (Dockerfile, Kubernetes manifests, GitHub Actions workflow). -- Branch protection rules and team-based access control. -- Template-based initialization for consistent project scaffolding. +Creates the namespace on the platform team's cluster, with: -### 3. CI/CD Pipeline — `aks/github-connector` +- a `ResourceQuota` and a `LimitRange` with default container requests and limits +- `admin`, `edit` and `view` role bindings for Entra groups +- the owning workspace recorded as a label -The connector building block creates: +The block runs as a user-assigned managed identity that meshStack's building block runner federates +into. It holds no kubeconfig. The identity has cluster admin on the one cluster through Azure RBAC +for Kubernetes. If you set `acr_id` on its backplane, the cluster's kubelet identity also gets +`AcrPull` on your Azure Container Registry, so no namespace needs an image pull secret. -- A **Kubernetes service account** scoped to the target namespace. -- **GitHub Actions environment secrets** for cluster authentication and container - registry access. -- A **GitHub Actions workflow** that builds, scans, and deploys on every push. +### 3. GitHub Repository: `github/repository` -Separate connector instances for dev and prod ensure workload isolation while sharing -the same repository. +Each application team gets its own repository with a predefined structure (Dockerfile, Kubernetes +manifests, GitHub Actions workflow), branch protection and team-based access control. + +### 4. CI/CD Pipeline: `aks/github-connector` + +The connector creates a **Kubernetes service account** scoped to the target namespace, **GitHub +Actions environment secrets** for cluster and container registry access, and a workflow that +builds, scans and deploys on every push. ## Getting Started ### Prerequisites -| Requirement | Description | -|---------------------|------------------------------------------------------------------------| -| meshStack instance | With Terraform/OpenTofu IaC runtime configured. | -| AKS cluster | An existing AKS cluster with a kubeconfig available for the connector. | -| GitHub organization | With a GitHub App installed for the Terraform GitHub provider. | +| Requirement | Description | +|---|---| +| meshStack instance | With the OpenTofu building block runner configured. | +| AKS cluster | Entra-integrated, with Azure RBAC for Kubernetes authorization enabled, and reachable from the runner. | +| Azure Container Registry | Optional. Pass its resource ID to the `azure/aks-namespace` backplane. | +| GitHub organization | With a GitHub App installed for the Terraform GitHub provider. | + +### Deployment order + +1. Deploy `azure/aks-namespace` and note its `building_block_definition` output. +2. Deploy `github/repository`. +3. Deploy `aks/github-connector`, passing the namespace definition's UUID as + `aks.namespace_definition_uuid`. +4. Deploy `aks/starterkit`, passing the three definition versions in + `building_block_definition_version_refs` under the keys `git-repository`, `aks-namespace` and + `github-actions-connector`. ## Shared Responsibilities | Responsibility | Platform Team | Application Team | | --- | --- | --- | -| Provision and manage AKS cluster | ✅ | ❌ | +| Provision and manage the AKS cluster | ✅ | ❌ | | Register and maintain building block definitions | ✅ | ❌ | +| Set up the namespace backplane identity and permissions | ✅ | ❌ | | Configure GitHub App & template repositories | ✅ | ❌ | +| Decide which Entra groups get namespace access | ✅ | ❌ | | Order starterkit from the self-service catalog | ❌ | ✅ | | Develop and maintain application source code | ❌ | ✅ | | Manage Kubernetes resources inside namespaces | ❌ | ✅ | | Merge to release branch for production deployments | ❌ | ✅ | | Monitor application health and logs | ❌ | ✅ | - diff --git a/reference-architectures/azure-kubernetes/azure-kubernetes.dot b/reference-architectures/azure-kubernetes/azure-kubernetes.dot index 4d5fda290..57c266309 100644 --- a/reference-architectures/azure-kubernetes/azure-kubernetes.dot +++ b/reference-architectures/azure-kubernetes/azure-kubernetes.dot @@ -58,24 +58,19 @@ digraph azure_kubernetes { style="rounded" color="#d5dae0" margin=18 - PLAT [label="🛰️ AKS Namespace Platform" fillcolor="#ecedfb" color="#9aa2e6"] - LZ [label="🛬 Landing Zone ×2\ndev + prod" fillcolor="#ecedfb" color="#9aa2e6"] - PROJ [label="🗂️ Project ×2\ndev + prod" fillcolor="#e5f2ea" color="#85bfa0"] BBD_S [label="📦 aks/starterkit BBD\nself-service catalog entry" fillcolor="#ecedfb" color="#9aa2e6"] BBD_R [label="📦 github/repository BBD" fillcolor="#ecedfb" color="#9aa2e6"] + BBD_N [label="📦 azure/aks-namespace BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_C [label="📦 aks/github-connector BBD" fillcolor="#ecedfb" color="#9aa2e6"] - { rank=same; BBD_R -> BBD_C [style=invis] } + { rank=same; BBD_R -> BBD_N -> BBD_C [style=invis] } - PLAT -> LZ - LZ -> PROJ [xlabel="applies to"] BBD_S -> BBD_R [xlabel="composes"] + BBD_S -> BBD_N BBD_S -> BBD_C - BBD_S -> PROJ [xlabel="creates" style=dotted constraint=false] } - PLAT -> AKS [xlabel="replicates into" style=dotted constraint=false] - PROJ -> NS [xlabel="tenant namespace" style=dotted constraint=false] + BBD_N -> NS [xlabel="creates ×2" style=dotted constraint=false] BBD_R -> REPO [xlabel="provisions" style=dotted constraint=false] BBD_C -> GHA [xlabel="provisions" style=dotted constraint=false] GHA -> ACR [xlabel="pushes images" style=dotted constraint=false] diff --git a/reference-architectures/azure-kubernetes/azure-kubernetes.svg b/reference-architectures/azure-kubernetes/azure-kubernetes.svg index 58b713f0f..7f961f633 100644 --- a/reference-architectures/azure-kubernetes/azure-kubernetes.svg +++ b/reference-architectures/azure-kubernetes/azure-kubernetes.svg @@ -1,11 +1,11 @@ - + azure_kubernetes - + cluster_azure @@ -18,7 +18,7 @@ cluster_mesh - +  meshStack @@ -37,8 +37,8 @@ SUB->AKS - - + + @@ -103,9 +103,9 @@ TPL->REPO - - -cloned into + + +cloned into @@ -122,119 +122,85 @@ runs workflow - + GHA->ACR pushes images - + GHA->NS deploys to - - -PLAT - -🛰️ AKS Namespace Platform - - - -PLAT->AKS - - -replicates into - - - -LZ - -🛬 Landing Zone ×2 -dev + prod - - - -PLAT->LZ - - - - - -PROJ - -🗂️ Project ×2 -dev + prod - - - -LZ->PROJ - - -applies to - - - -PROJ->NS - - -tenant namespace - - + BBD_S - -📦 aks/starterkit BBD -self-service catalog entry - - - -BBD_S->PROJ - - -creates + +📦 aks/starterkit BBD +self-service catalog entry - + BBD_R - -📦 github/repository BBD + +📦 github/repository BBD - + BBD_S->BBD_R - - -composes + + +composes + + + +BBD_N + +📦 azure/aks-namespace BBD + + + +BBD_S->BBD_N + + - + BBD_C - -📦 aks/github-connector BBD + +📦 aks/github-connector BBD BBD_S->BBD_C - - + + - + BBD_R->REPO - - -provisions + + +provisions - + + + +BBD_N->NS + + +creates ×2 + + - + BBD_C->GHA - - -provisions + + +provisions From aca496643832346cde82bdf7300722ffe25103a0 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Mon, 12 Oct 2026 14:01:09 +0200 Subject: [PATCH 05/11] feat(azure/aks-namespace): create the namespace's Entra groups and fill them from workspace members The block creates an admin, edit and view security group per namespace, looks up the workspace members it receives through USER_PERMISSIONS, adds each to the group their workspace role maps to, and binds the groups to the matching cluster roles. Existing group IDs can still be bound alongside. The backplane gains Graph User.Read.All and Group.ReadWrite.All. Co-Authored-By: Claude Sonnet 5.5 --- .../aks/starterkit/buildingblock/README.md | 2 +- .../aks/starterkit/buildingblock/outputs.tf | 4 +- .../aks/starterkit/buildingblock/variables.tf | 2 +- .../aks/starterkit/meshstack_integration.tf | 2 +- .../azure/aks-namespace/backplane/README.md | 1 + modules/azure/aks-namespace/backplane/main.tf | 19 ++++ .../azure/aks-namespace/backplane/provider.tf | 2 + .../azure/aks-namespace/backplane/versions.tf | 4 + .../aks-namespace/buildingblock/README.md | 21 +++- .../azure/aks-namespace/buildingblock/main.tf | 95 +++++++++++++++++-- .../aks-namespace/buildingblock/outputs.tf | 20 ++++ .../aks-namespace/buildingblock/provider.tf | 2 + .../aks-namespace/buildingblock/variables.tf | 48 +++++++++- .../aks-namespace/buildingblock/versions.tf | 4 + .../aks-namespace/meshstack_integration.tf | 90 ++++++++++++++++-- .../azure-kubernetes/README.md | 9 +- 16 files changed, 293 insertions(+), 32 deletions(-) diff --git a/modules/aks/starterkit/buildingblock/README.md b/modules/aks/starterkit/buildingblock/README.md index 87652feed..77bc8b8ba 100644 --- a/modules/aks/starterkit/buildingblock/README.md +++ b/modules/aks/starterkit/buildingblock/README.md @@ -41,7 +41,7 @@ No modules. | [github\_repo\_input\_repo\_visibility](#input\_github\_repo\_input\_repo\_visibility) | Visibility of the GitHub repository (e.g., public, private). | `string` | `"private"` | no | | [github\_template\_repo\_path](#input\_github\_template\_repo\_path) | GitHub repository template to use when creating the application repository, in the format 'owner/repo'. | `string` | `"likvid-bank/aks-starterkit-template"` | no | | [name](#input\_name) | This name will be used for the created namespaces, app subdomain and GitHub repository. | `string` | n/a | yes | -| [namespace\_group\_object\_ids](#input\_namespace\_group\_object\_ids) | Entra group object IDs granted the admin, edit and view role in every created namespace. |
object({
admin = optional(list(string), [])
edit = optional(list(string), [])
view = optional(list(string), [])
})
| `{}` | no | +| [namespace\_group\_object\_ids](#input\_namespace\_group\_object\_ids) | Existing Entra group object IDs granted the admin, edit and view role in every created namespace, in addition to the groups each namespace creates for its workspace members. |
object({
admin = optional(list(string), [])
edit = optional(list(string), [])
view = optional(list(string), [])
})
| `{}` | no | | [repo\_admin](#input\_repo\_admin) | GitHub handle of the user who will be assigned as the repository admin. Delete building block definition input if not needed. | `string` | `null` | no | | [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes | diff --git a/modules/aks/starterkit/buildingblock/outputs.tf b/modules/aks/starterkit/buildingblock/outputs.tf index 6e02d6408..69a7757a2 100644 --- a/modules/aks/starterkit/buildingblock/outputs.tf +++ b/modules/aks/starterkit/buildingblock/outputs.tf @@ -67,8 +67,8 @@ View deployment status: [GitHub Actions](${jsondecode(meshstack_building_block.r - Check workflow status in the [Actions tab](<${jsondecode(meshstack_building_block.repo.status.outputs.repo_html_url.value)}/actions>) ### 3. Manage Access -- Namespace access follows the Entra groups the platform team configured for this starter kit. - Ask them to add your team's group. +- Namespace access follows your workspace roles: owners and managers get `admin`, members `edit`, + readers `view`. Change a member's workspace role in meshStack and re-run the namespace block. --- diff --git a/modules/aks/starterkit/buildingblock/variables.tf b/modules/aks/starterkit/buildingblock/variables.tf index 426044f09..eac99b21b 100644 --- a/modules/aks/starterkit/buildingblock/variables.tf +++ b/modules/aks/starterkit/buildingblock/variables.tf @@ -56,6 +56,6 @@ variable "namespace_group_object_ids" { edit = optional(list(string), []) view = optional(list(string), []) }) - description = "Entra group object IDs granted the admin, edit and view role in every created namespace." + description = "Existing Entra group object IDs granted the admin, edit and view role in every created namespace, in addition to the groups each namespace creates for its workspace members." default = {} } diff --git a/modules/aks/starterkit/meshstack_integration.tf b/modules/aks/starterkit/meshstack_integration.tf index 30ffd43db..a7503c78a 100644 --- a/modules/aks/starterkit/meshstack_integration.tf +++ b/modules/aks/starterkit/meshstack_integration.tf @@ -31,7 +31,7 @@ variable "namespace_group_object_ids" { edit = [] view = [] } - description = "Entra group object IDs granted the admin, edit and view role in every namespace the starter kit creates." + description = "Existing Entra group object IDs granted the admin, edit and view role in every namespace the starter kit creates, in addition to the groups each namespace creates for its workspace members." } variable "notification_subscribers" { diff --git a/modules/azure/aks-namespace/backplane/README.md b/modules/azure/aks-namespace/backplane/README.md index 96d2052bd..f569f2e4e 100644 --- a/modules/azure/aks-namespace/backplane/README.md +++ b/modules/azure/aks-namespace/backplane/README.md @@ -10,6 +10,7 @@ Creates the identity the **Azure AKS Namespace** building block runs as. | Federated identity credential | Trusts the building block definition's workload identity subject. | | Custom role (`-deploy`) | Lets the identity read the AKS cluster and fetch the user credentials. | | `Azure Kubernetes Service RBAC Cluster Admin` | Lets the identity create namespaces, quotas and role bindings. Scoped to the one cluster. | +| Microsoft Graph `User.Read.All` and `Group.ReadWrite.All` | Lets the identity create the namespace's Entra groups and look up the workspace members it adds to them. Needs admin consent. | | `AcrPull` for the kubelet identity (optional) | Set `acr_id` and every namespace's workloads can pull from that registry without an image pull secret. | ## Prerequisites diff --git a/modules/azure/aks-namespace/backplane/main.tf b/modules/azure/aks-namespace/backplane/main.tf index 22570e8cc..5c28371a2 100644 --- a/modules/azure/aks-namespace/backplane/main.tf +++ b/modules/azure/aks-namespace/backplane/main.tf @@ -64,3 +64,22 @@ resource "azurerm_role_assignment" "kubelet_acr_pull" { role_definition_name = "AcrPull" principal_id = data.azurerm_kubernetes_cluster.this.kubelet_identity[0].object_id } + +# The building block creates one Entra group per namespace role and fills it with workspace members, +# so the identity reads users and writes groups. Granting these app roles needs admin consent: the +# principal deploying this backplane must be allowed to assign Graph app roles. +data "azuread_service_principal" "msgraph" { + client_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph +} + +resource "azuread_app_role_assignment" "user_read_all" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["User.Read.All"] + principal_object_id = azurerm_user_assigned_identity.backplane.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} + +resource "azuread_app_role_assignment" "group_readwrite_all" { + app_role_id = data.azuread_service_principal.msgraph.app_role_ids["Group.ReadWrite.All"] + principal_object_id = azurerm_user_assigned_identity.backplane.principal_id + resource_object_id = data.azuread_service_principal.msgraph.object_id +} diff --git a/modules/azure/aks-namespace/backplane/provider.tf b/modules/azure/aks-namespace/backplane/provider.tf index ab91b2481..baca72e44 100644 --- a/modules/azure/aks-namespace/backplane/provider.tf +++ b/modules/azure/aks-namespace/backplane/provider.tf @@ -1,3 +1,5 @@ provider "azurerm" { features {} } + +provider "azuread" {} diff --git a/modules/azure/aks-namespace/backplane/versions.tf b/modules/azure/aks-namespace/backplane/versions.tf index 8236fc6c9..d581b5eed 100644 --- a/modules/azure/aks-namespace/backplane/versions.tf +++ b/modules/azure/aks-namespace/backplane/versions.tf @@ -6,5 +6,9 @@ terraform { source = "hashicorp/azurerm" version = ">= 4.65.0, < 5.0.0" } + azuread = { + source = "hashicorp/azuread" + version = ">= 3.8.0, < 4.0.0" + } } } diff --git a/modules/azure/aks-namespace/buildingblock/README.md b/modules/azure/aks-namespace/buildingblock/README.md index fb6ec1bf6..553bce4f1 100644 --- a/modules/azure/aks-namespace/buildingblock/README.md +++ b/modules/azure/aks-namespace/buildingblock/README.md @@ -15,13 +15,17 @@ managed identity through workload identity federation and holds no kubeconfig. - the namespace, labelled with the owning workspace - a `ResourceQuota` and a `LimitRange` with default container requests and limits -- a `RoleBinding` per Entra group to the `admin`, `edit` or `view` cluster role +- an Entra security group per namespace role (`admin`, `edit`, `view`), filled with the workspace + members whose workspace role maps to it (see `role_mapping`) +- a `RoleBinding` per group to the `admin`, `edit` or `view` cluster role. Existing Entra groups can + be bound too, through the `*_group_object_ids` inputs ## Requirements | Name | Version | |------|---------| +| [azuread](#requirement\_azuread) | >= 3.8.0, < 4.0.0 | | [azurerm](#requirement\_azurerm) | >= 4.64.0, < 5.0.0 | | [http](#requirement\_http) | >= 3.4.0, < 4.0.0 | | [kubernetes](#requirement\_kubernetes) | >= 2.35.1, < 3.0.0 | @@ -34,10 +38,13 @@ No modules. | Name | Type | |------|------| +| [azuread_group.namespace](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/group) | resource | +| [azuread_group_member.namespace](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/group_member) | resource | | [kubernetes_limit_range_v1.this](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/limit_range_v1) | resource | | [kubernetes_namespace_v1.this](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace_v1) | resource | | [kubernetes_resource_quota_v1.this](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/resource_quota_v1) | resource | | [kubernetes_role_binding_v1.group](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding_v1) | resource | +| [azuread_users.members](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/users) | data source | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | | [azurerm_kubernetes_cluster.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/kubernetes_cluster) | data source | | [http_http.aks_token](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | data source | @@ -46,15 +53,19 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [admin\_group\_object\_ids](#input\_admin\_group\_object\_ids) | Entra group object IDs granted the `admin` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [admin\_group\_object\_ids](#input\_admin\_group\_object\_ids) | Existing Entra group object IDs, in addition to the group this block creates, granted the `admin` cluster role inside the namespace. | `list(string)` | `[]` | no | | [aks\_cluster\_name](#input\_aks\_cluster\_name) | Name of the AKS cluster the namespace is created in. | `string` | n/a | yes | | [aks\_resource\_group\_name](#input\_aks\_resource\_group\_name) | Resource group of the AKS cluster. | `string` | n/a | yes | | [default\_container\_limits](#input\_default\_container\_limits) | LimitRange defaults for containers that declare no requests or limits of their own. |
object({
cpu_request = optional(string, "100m")
memory_request = optional(string, "128Mi")
cpu_limit = optional(string, "500m")
memory_limit = optional(string, "512Mi")
})
| `{}` | no | -| [edit\_group\_object\_ids](#input\_edit\_group\_object\_ids) | Entra group object IDs granted the `edit` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [edit\_group\_object\_ids](#input\_edit\_group\_object\_ids) | Existing Entra group object IDs, in addition to the group this block creates, granted the `edit` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [group\_name\_prefix](#input\_group\_name\_prefix) | Prepended to the display names of the Entra groups this block creates. Leave empty to omit. | `string` | `""` | no | | [namespace\_name](#input\_namespace\_name) | Name of the Kubernetes namespace to create. | `string` | n/a | yes | | [oidc\_token\_file\_path](#input\_oidc\_token\_file\_path) | File the building block runner writes its federated OIDC token to. | `string` | `"/var/run/secrets/workload-identity/azure/token"` | no | | [resource\_quota](#input\_resource\_quota) | ResourceQuota applied to the namespace. |
object({
requests_cpu = optional(string, "2")
requests_memory = optional(string, "4Gi")
limits_cpu = optional(string, "4")
limits_memory = optional(string, "8Gi")
pods = optional(string, "50")
})
| `{}` | no | -| [view\_group\_object\_ids](#input\_view\_group\_object\_ids) | Entra group object IDs granted the `view` cluster role inside the namespace. | `list(string)` | `[]` | no | +| [role\_mapping](#input\_role\_mapping) | Maps a meshStack workspace role name to the namespace role (`admin`, `edit` or `view`) its members get. A workspace role missing here gets no access. | `map(string)` | n/a | yes | +| [user\_lookup\_attribute](#input\_user\_lookup\_attribute) | Directory attribute used to match workspace members to Entra users. 'upn' matches the User Principal Name; 'email' matches the primary mail address. | `string` | `"email"` | no | +| [users](#input\_users) | Workspace members with their meshStack roles, injected by meshStack. Each is added to the group of the namespace role their workspace role maps to. |
list(object({
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}))
| `[]` | no | +| [view\_group\_object\_ids](#input\_view\_group\_object\_ids) | Existing Entra group object IDs, in addition to the group this block creates, granted the `view` cluster role inside the namespace. | `list(string)` | `[]` | no | | [workspace\_identifier](#input\_workspace\_identifier) | Identifier of the meshStack workspace that owns the namespace. Recorded as a label. | `string` | n/a | yes | ## Outputs @@ -63,6 +74,8 @@ No modules. |------|-------------| | [cluster\_name](#output\_cluster\_name) | Name of the AKS cluster hosting the namespace. | | [cluster\_resource\_group\_name](#output\_cluster\_resource\_group\_name) | Resource group of the AKS cluster hosting the namespace. | +| [group\_object\_ids](#output\_group\_object\_ids) | Map of namespace role (admin, edit, view) to the object ID of the Entra group this block created. | | [namespace\_name](#output\_namespace\_name) | Name of the created Kubernetes namespace. | | [summary](#output\_summary) | Markdown summary shown in meshPanel after the run. | +| [unresolved\_users](#output\_unresolved\_users) | Workspace members that have no matching object in the directory and were therefore not added to any group. | diff --git a/modules/azure/aks-namespace/buildingblock/main.tf b/modules/azure/aks-namespace/buildingblock/main.tf index 73bfc5323..cc7a9c6bb 100644 --- a/modules/azure/aks-namespace/buildingblock/main.tf +++ b/modules/azure/aks-namespace/buildingblock/main.tf @@ -1,11 +1,90 @@ locals { - group_bindings = merge([ - for cluster_role, object_ids in { - admin = var.admin_group_object_ids - edit = var.edit_group_object_ids - view = var.view_group_object_ids - } : { for id in object_ids : "${cluster_role}-${id}" => { cluster_role = cluster_role, object_id = id } } - ]...) + namespace_roles = toset(["admin", "edit", "view"]) + + unique_user_euids = toset([for user in var.users : user.euid]) + + user_group_assignments = { + for pair in flatten([ + for user in var.users : [ + for role in user.roles : { + key = "${user.euid}-${var.role_mapping[role]}" + euid = user.euid + role = var.role_mapping[role] + } + if contains(keys(var.role_mapping), role) + ] + ]) : pair.key => pair + } +} + +# One bulk lookup with ignore_missing, rather than a data source per member: a workspace member +# with no object in the directory (external collaborator, service account, a guest that was never +# invited) must not fail the whole run. Unresolved members are reported through the +# unresolved_users output instead. +data "azuread_users" "members" { + lifecycle { + enabled = length(local.unique_user_euids) > 0 + } + + ignore_missing = true + + mails = var.user_lookup_attribute == "email" ? sort(tolist(local.unique_user_euids)) : null + user_principal_names = var.user_lookup_attribute == "upn" ? sort(tolist(local.unique_user_euids)) : null +} + +locals { + # Directory lookups are case-insensitive, so match on a lowercased key to avoid treating a member + # whose meshStack euid differs only in case from the directory value as unresolved. + # + # `try` because the lookup is disabled when there is no member to resolve, and reading an + # attribute of a disabled data source is an error rather than an empty list. + lookup_values = { + for u in try(data.azuread_users.members.users, []) : + lower(coalesce(var.user_lookup_attribute == "email" ? u.mail : u.user_principal_name, "")) => u.object_id + if coalesce(var.user_lookup_attribute == "email" ? u.mail : u.user_principal_name, "") != "" + } + + unresolved_user_euids = sort([ + for euid in local.unique_user_euids : euid + if !contains(keys(local.lookup_values), lower(euid)) + ]) + + created_group_object_ids = { for role, g in azuread_group.namespace : role => g.object_id } + + extra_group_object_ids = { + admin = var.admin_group_object_ids + edit = var.edit_group_object_ids + view = var.view_group_object_ids + } + + # The created group's object ID is unknown until apply, so it cannot be part of a for_each key: + # that binding is keyed by its role alone. + group_bindings = merge( + { for role in local.namespace_roles : "${role}-created" => { cluster_role = role, object_id = local.created_group_object_ids[role] } }, + merge([ + for role, ids in local.extra_group_object_ids : { + for id in ids : "${role}-${id}" => { cluster_role = role, object_id = id } + } + ]...) + ) +} + +resource "azuread_group" "namespace" { + for_each = local.namespace_roles + + display_name = join(".", compact([var.group_name_prefix, var.workspace_identifier, var.namespace_name, each.value])) + security_enabled = true + mail_enabled = false +} + +resource "azuread_group_member" "namespace" { + for_each = { + for key, assignment in local.user_group_assignments : key => assignment + if contains(keys(local.lookup_values), lower(assignment.euid)) + } + + group_object_id = azuread_group.namespace[each.value.role].object_id + member_object_id = local.lookup_values[lower(each.value.euid)] } resource "kubernetes_namespace_v1" "this" { @@ -64,7 +143,7 @@ resource "kubernetes_role_binding_v1" "group" { for_each = local.group_bindings metadata { - name = "meshstack-${each.value.cluster_role}-${substr(each.value.object_id, 0, 8)}" + name = "meshstack-${each.key}" namespace = kubernetes_namespace_v1.this.metadata[0].name } diff --git a/modules/azure/aks-namespace/buildingblock/outputs.tf b/modules/azure/aks-namespace/buildingblock/outputs.tf index 6657b1581..c72dff57b 100644 --- a/modules/azure/aks-namespace/buildingblock/outputs.tf +++ b/modules/azure/aks-namespace/buildingblock/outputs.tf @@ -13,6 +13,16 @@ output "cluster_resource_group_name" { value = var.aks_resource_group_name } +output "group_object_ids" { + description = "Map of namespace role (admin, edit, view) to the object ID of the Entra group this block created." + value = local.created_group_object_ids +} + +output "unresolved_users" { + description = "Workspace members that have no matching object in the directory and were therefore not added to any group." + value = local.unresolved_user_euids +} + output "summary" { description = "Markdown summary shown in meshPanel after the run." value = <<-EOT @@ -27,6 +37,16 @@ output "summary" { kubectl config set-context --current --namespace ${kubernetes_namespace_v1.this.metadata[0].name} ``` + ## Access + + | Namespace role | Entra group | + |---|---| + | `admin` | `${azuread_group.namespace["admin"].display_name}` | + | `edit` | `${azuread_group.namespace["edit"].display_name}` | + | `view` | `${azuread_group.namespace["view"].display_name}` | + + ${length(azuread_group_member.namespace)} membership(s) assigned from ${length(local.unique_user_euids)} workspace member(s), matched on `${var.user_lookup_attribute}`.${length(local.unresolved_user_euids) == 0 ? "" : " Not found in the directory and skipped: ${join(", ", local.unresolved_user_euids)}."} + Quota: ${var.resource_quota.requests_cpu} CPU / ${var.resource_quota.requests_memory} requested, ${var.resource_quota.limits_cpu} CPU / ${var.resource_quota.limits_memory} limit, ${var.resource_quota.pods} pods. EOT diff --git a/modules/azure/aks-namespace/buildingblock/provider.tf b/modules/azure/aks-namespace/buildingblock/provider.tf index 305b714fb..3d5f71802 100644 --- a/modules/azure/aks-namespace/buildingblock/provider.tf +++ b/modules/azure/aks-namespace/buildingblock/provider.tf @@ -43,3 +43,5 @@ provider "kubernetes" { cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.this.kube_config[0].cluster_ca_certificate) token = sensitive(jsondecode(data.http.aks_token.response_body).access_token) } + +provider "azuread" {} diff --git a/modules/azure/aks-namespace/buildingblock/variables.tf b/modules/azure/aks-namespace/buildingblock/variables.tf index 17ff19d80..ed999faff 100644 --- a/modules/azure/aks-namespace/buildingblock/variables.tf +++ b/modules/azure/aks-namespace/buildingblock/variables.tf @@ -30,19 +30,19 @@ variable "namespace_name" { variable "admin_group_object_ids" { type = list(string) default = [] - description = "Entra group object IDs granted the `admin` cluster role inside the namespace." + description = "Existing Entra group object IDs, in addition to the group this block creates, granted the `admin` cluster role inside the namespace." } variable "edit_group_object_ids" { type = list(string) default = [] - description = "Entra group object IDs granted the `edit` cluster role inside the namespace." + description = "Existing Entra group object IDs, in addition to the group this block creates, granted the `edit` cluster role inside the namespace." } variable "view_group_object_ids" { type = list(string) default = [] - description = "Entra group object IDs granted the `view` cluster role inside the namespace." + description = "Existing Entra group object IDs, in addition to the group this block creates, granted the `view` cluster role inside the namespace." } variable "resource_quota" { @@ -73,3 +73,45 @@ variable "oidc_token_file_path" { default = "/var/run/secrets/workload-identity/azure/token" description = "File the building block runner writes its federated OIDC token to." } + +variable "group_name_prefix" { + type = string + default = "" + description = "Prepended to the display names of the Entra groups this block creates. Leave empty to omit." +} + +variable "user_lookup_attribute" { + type = string + default = "email" + description = "Directory attribute used to match workspace members to Entra users. 'upn' matches the User Principal Name; 'email' matches the primary mail address." + + validation { + condition = contains(["upn", "email"], var.user_lookup_attribute) + error_message = "Must be 'upn' or 'email'." + } +} + +variable "role_mapping" { + type = map(string) + nullable = false + description = "Maps a meshStack workspace role name to the namespace role (`admin`, `edit` or `view`) its members get. A workspace role missing here gets no access." + + validation { + condition = alltrue([for r in values(var.role_mapping) : contains(["admin", "edit", "view"], r)]) + error_message = "Values must be 'admin', 'edit' or 'view'." + } +} + +variable "users" { + type = list(object({ + meshIdentifier = string + username = string + firstName = string + lastName = string + email = string + euid = string + roles = list(string) + })) + default = [] + description = "Workspace members with their meshStack roles, injected by meshStack. Each is added to the group of the namespace role their workspace role maps to." +} diff --git a/modules/azure/aks-namespace/buildingblock/versions.tf b/modules/azure/aks-namespace/buildingblock/versions.tf index 81d1fb487..6aea61510 100644 --- a/modules/azure/aks-namespace/buildingblock/versions.tf +++ b/modules/azure/aks-namespace/buildingblock/versions.tf @@ -4,6 +4,10 @@ terraform { source = "hashicorp/azurerm" version = ">= 4.64.0, < 5.0.0" } + azuread = { + source = "hashicorp/azuread" + version = ">= 3.8.0, < 4.0.0" + } kubernetes = { source = "hashicorp/kubernetes" version = ">= 2.35.1, < 3.0.0" diff --git a/modules/azure/aks-namespace/meshstack_integration.tf b/modules/azure/aks-namespace/meshstack_integration.tf index 21e1e646f..cc2a19d5c 100644 --- a/modules/azure/aks-namespace/meshstack_integration.tf +++ b/modules/azure/aks-namespace/meshstack_integration.tf @@ -34,6 +34,34 @@ variable "acr_id" { description = "Resource ID of the Azure Container Registry workloads pull from. When set, the cluster's kubelet identity gets AcrPull on it." } +variable "azure_user_lookup_attribute" { + type = string + default = "email" + description = "Directory attribute used to match meshStack workspace members to Entra users. 'email' matches the user's SMTP address and is the right choice when members are B2B guests, whose user principal name is rewritten on invitation. 'upn' only works for members native to this tenant." + + validation { + condition = contains(["upn", "email"], var.azure_user_lookup_attribute) + error_message = "Must be 'upn' or 'email'." + } +} + +variable "azure_group_name_prefix" { + type = string + default = "" + description = "Prepended to the display names of the Entra groups created per namespace. Leave empty to omit." +} + +variable "workspace_role_mapping" { + type = map(string) + default = { + "Workspace Owner" = "admin" + "Workspace Manager" = "admin" + "Workspace Member" = "edit" + "Workspace Reader" = "view" + } + description = "Maps a meshStack workspace role name to the namespace role (admin, edit or view) its members get in every namespace. A workspace role missing here gets no access." +} + variable "backplane_name" { type = string default = "azure-aks-namespace" @@ -145,7 +173,10 @@ resource "meshstack_building_block_definition" "this" { Use this building block when you: - need a namespace to deploy a containerised application to - - want your team's Entra groups to manage it without cluster-wide rights + - want your workspace members to manage it without cluster-wide rights + + Ordering it creates an Entra group per namespace role (`admin`, `edit`, `view`) and adds your + workspace members to them according to their workspace role. ## 💡 Usage examples @@ -153,7 +184,8 @@ resource "meshstack_building_block_definition" "this" { Order the block twice, once as `myapp-dev` and once as `myapp-prod`, and give each a different quota. **Example 2: Read-only access for auditors** - Add your auditors' Entra group to **View Group Object IDs** so they can inspect workloads without changing them. + Give your auditors the workspace Reader role and they land in the namespace's `view` group. For a group + that already exists, add its object ID to **Additional View Group Object IDs**. ## 📊 Shared Responsibility @@ -162,7 +194,7 @@ resource "meshstack_building_block_definition" "this" { | Operate the AKS cluster and its identity | ✅ | ❌ | | Set up the backplane identity and permissions | ✅ | ❌ | | Choose the namespace name and quota | ❌ | ✅ | - | Decide which Entra groups get access | ❌ | ✅ | + | Decide who is a workspace member and with which role | ❌ | ✅ | | Deploy and operate workloads in the namespace | ❌ | ✅ | EOT )) @@ -256,24 +288,52 @@ resource "meshstack_building_block_definition" "this" { value_validation_regex = "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$" validation_regex_error_message = "Lowercase letters, digits and dashes only, at most 63 characters, starting and ending with a letter or digit." } + group_name_prefix = { + type = "STRING" + display_name = "Group Name Prefix" + description = "Prepended to the display names of the Entra groups created for the namespace." + assignment_type = "STATIC" + argument = jsonencode(var.azure_group_name_prefix) + } + user_lookup_attribute = { + type = "STRING" + display_name = "User Lookup Attribute" + description = "Directory attribute used to look up users. 'email' matches the SMTP address; 'upn' matches the User Principal Name." + assignment_type = "STATIC" + argument = jsonencode(var.azure_user_lookup_attribute) + } + role_mapping = { + type = "CODE" + display_name = "Role Mapping" + description = "Maps meshStack workspace roles to the namespace roles admin, edit and view." + assignment_type = "STATIC" + # jsonencode twice is correct for structured inputs, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 + argument = jsonencode(jsonencode(var.workspace_role_mapping)) + } + users = { + type = "CODE" + display_name = "Users" + description = "Workspace members with their roles. Injected automatically by meshStack." + assignment_type = "USER_PERMISSIONS" + } admin_group_object_ids = { type = "CODE" - display_name = "Admin Group Object IDs" - description = "Entra group object IDs that get the 'admin' role in the namespace, as a JSON list." + display_name = "Additional Admin Group Object IDs" + description = "Existing Entra group object IDs that also get the 'admin' role in the namespace, as a JSON list." assignment_type = "USER_INPUT" default_value = jsonencode([]) } edit_group_object_ids = { type = "CODE" - display_name = "Edit Group Object IDs" - description = "Entra group object IDs that get the 'edit' role in the namespace, as a JSON list." + display_name = "Additional Edit Group Object IDs" + description = "Existing Entra group object IDs that also get the 'edit' role in the namespace, as a JSON list." assignment_type = "USER_INPUT" default_value = jsonencode([]) } view_group_object_ids = { type = "CODE" - display_name = "View Group Object IDs" - description = "Entra group object IDs that get the 'view' role in the namespace, as a JSON list." + display_name = "Additional View Group Object IDs" + description = "Existing Entra group object IDs that also get the 'view' role in the namespace, as a JSON list." assignment_type = "USER_INPUT" default_value = jsonencode([]) } @@ -311,6 +371,18 @@ resource "meshstack_building_block_definition" "this" { description = "Resource group of the AKS cluster hosting the namespace." assignment_type = "NONE" } + group_object_ids = { + type = "STRING" + display_name = "Group Object IDs" + description = "JSON map of namespace role to the Entra group created for it." + assignment_type = "NONE" + } + unresolved_users = { + type = "STRING" + display_name = "Unresolved Users" + description = "JSON list of workspace members not found in the directory, and so not added to any group." + assignment_type = "NONE" + } summary = { type = "STRING" display_name = "Summary" diff --git a/reference-architectures/azure-kubernetes/README.md b/reference-architectures/azure-kubernetes/README.md index e31fd129e..656afbdca 100644 --- a/reference-architectures/azure-kubernetes/README.md +++ b/reference-architectures/azure-kubernetes/README.md @@ -58,12 +58,14 @@ reads the namespace name from the namespace block's output. Creates the namespace on the platform team's cluster, with: - a `ResourceQuota` and a `LimitRange` with default container requests and limits -- `admin`, `edit` and `view` role bindings for Entra groups +- an Entra group per namespace role (`admin`, `edit`, `view`), filled from the workspace's members + by their workspace role (owner and manager, member, reader), and bound to the matching cluster role. + Existing groups can be added alongside - the owning workspace recorded as a label The block runs as a user-assigned managed identity that meshStack's building block runner federates into. It holds no kubeconfig. The identity has cluster admin on the one cluster through Azure RBAC -for Kubernetes. If you set `acr_id` on its backplane, the cluster's kubelet identity also gets +for Kubernetes, and Microsoft Graph `User.Read.All` and `Group.ReadWrite.All` to manage the groups. If you set `acr_id` on its backplane, the cluster's kubelet identity also gets `AcrPull` on your Azure Container Registry, so no namespace needs an image pull secret. ### 3. GitHub Repository: `github/repository` @@ -106,7 +108,8 @@ builds, scans and deploys on every push. | Register and maintain building block definitions | ✅ | ❌ | | Set up the namespace backplane identity and permissions | ✅ | ❌ | | Configure GitHub App & template repositories | ✅ | ❌ | -| Decide which Entra groups get namespace access | ✅ | ❌ | +| Grant the namespace backplane its Microsoft Graph permissions (admin consent) | ✅ | ❌ | +| Manage workspace members and their roles | ❌ | ✅ | | Order starterkit from the self-service catalog | ❌ | ✅ | | Develop and maintain application source code | ❌ | ✅ | | Manage Kubernetes resources inside namespaces | ❌ | ✅ | From 73fb389fc1518c20aaf6a86ce8561ad24f9bcd63 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Mon, 12 Oct 2026 17:17:48 +0200 Subject: [PATCH 06/11] refactor(azure/aks-namespace): only manage the groups it creates, make the quota static Drops the admin, edit and view group object ID inputs from the namespace block and the starterkit's namespace_group_object_ids that fed them. The resource quota becomes a static, platform-set input instead of a user input. Co-Authored-By: Claude Sonnet 5.5 --- .../aks/starterkit/buildingblock/README.md | 1 - modules/aks/starterkit/buildingblock/main.tf | 9 --- .../aks/starterkit/buildingblock/variables.tf | 10 --- .../aks/starterkit/meshstack_integration.tf | 26 +------ .../aks-namespace/buildingblock/README.md | 6 +- .../azure/aks-namespace/buildingblock/main.tf | 22 +----- .../aks-namespace/buildingblock/variables.tf | 18 ----- .../aks-namespace/meshstack_integration.tf | 74 +++++++++---------- .../azure-kubernetes/README.md | 3 +- 9 files changed, 40 insertions(+), 129 deletions(-) diff --git a/modules/aks/starterkit/buildingblock/README.md b/modules/aks/starterkit/buildingblock/README.md index 77bc8b8ba..8c7ca3287 100644 --- a/modules/aks/starterkit/buildingblock/README.md +++ b/modules/aks/starterkit/buildingblock/README.md @@ -41,7 +41,6 @@ No modules. | [github\_repo\_input\_repo\_visibility](#input\_github\_repo\_input\_repo\_visibility) | Visibility of the GitHub repository (e.g., public, private). | `string` | `"private"` | no | | [github\_template\_repo\_path](#input\_github\_template\_repo\_path) | GitHub repository template to use when creating the application repository, in the format 'owner/repo'. | `string` | `"likvid-bank/aks-starterkit-template"` | no | | [name](#input\_name) | This name will be used for the created namespaces, app subdomain and GitHub repository. | `string` | n/a | yes | -| [namespace\_group\_object\_ids](#input\_namespace\_group\_object\_ids) | Existing Entra group object IDs granted the admin, edit and view role in every created namespace, in addition to the groups each namespace creates for its workspace members. |
object({
admin = optional(list(string), [])
edit = optional(list(string), [])
view = optional(list(string), [])
})
| `{}` | no | | [repo\_admin](#input\_repo\_admin) | GitHub handle of the user who will be assigned as the repository admin. Delete building block definition input if not needed. | `string` | `null` | no | | [workspace\_identifier](#input\_workspace\_identifier) | n/a | `string` | n/a | yes | diff --git a/modules/aks/starterkit/buildingblock/main.tf b/modules/aks/starterkit/buildingblock/main.tf index 38ab4a439..c948f99f1 100644 --- a/modules/aks/starterkit/buildingblock/main.tf +++ b/modules/aks/starterkit/buildingblock/main.tf @@ -79,15 +79,6 @@ resource "meshstack_building_block" "namespace" { namespace_name = { value = jsonencode("${local.identifier}-${each.key}") } - admin_group_object_ids = { - value = jsonencode(var.namespace_group_object_ids.admin) - } - edit_group_object_ids = { - value = jsonencode(var.namespace_group_object_ids.edit) - } - view_group_object_ids = { - value = jsonencode(var.namespace_group_object_ids.view) - } } } diff --git a/modules/aks/starterkit/buildingblock/variables.tf b/modules/aks/starterkit/buildingblock/variables.tf index eac99b21b..eccea6ab9 100644 --- a/modules/aks/starterkit/buildingblock/variables.tf +++ b/modules/aks/starterkit/buildingblock/variables.tf @@ -49,13 +49,3 @@ variable "apps_base_domain" { description = "Base domain used for application URLs (e.g. 'likvid-k8s.msh.host'). The app subdomain will be prefixed to this value." default = "likvid-k8s.msh.host" } - -variable "namespace_group_object_ids" { - type = object({ - admin = optional(list(string), []) - edit = optional(list(string), []) - view = optional(list(string), []) - }) - description = "Existing Entra group object IDs granted the admin, edit and view role in every created namespace, in addition to the groups each namespace creates for its workspace members." - default = {} -} diff --git a/modules/aks/starterkit/meshstack_integration.tf b/modules/aks/starterkit/meshstack_integration.tf index a7503c78a..0020718b0 100644 --- a/modules/aks/starterkit/meshstack_integration.tf +++ b/modules/aks/starterkit/meshstack_integration.tf @@ -20,20 +20,6 @@ variable "apps_base_domain" { description = "Base domain used for app URLs (example: `apps.prod.example.com`)." } -variable "namespace_group_object_ids" { - type = object({ - admin = optional(list(string), []) - edit = optional(list(string), []) - view = optional(list(string), []) - }) - default = { - admin = [] - edit = [] - view = [] - } - description = "Existing Entra group object IDs granted the admin, edit and view role in every namespace the starter kit creates, in addition to the groups each namespace creates for its workspace members." -} - variable "notification_subscribers" { type = list(string) default = [] @@ -165,7 +151,7 @@ EOT } inputs = { "building_block_definition_version_refs" = { - # jsonencode twice is correct for structured inputs, see the namespace_group_object_ids input below. + # jsonencode twice is correct for structured inputs, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 argument = jsonencode(jsonencode(var.building_block_definition_version_refs)) assignment_type = "STATIC" description = "Definition versions the starter kit creates its child building blocks from." @@ -217,16 +203,6 @@ EOT value_validation_regex = local.name_regex validation_regex_error_message = "No underscore/dots/spaces are allowed. A maximum length of 25 characters is allowed." } - "namespace_group_object_ids" = { - # jsonencode twice is correct for structured inputs, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 - argument = jsonencode(jsonencode(var.namespace_group_object_ids)) - assignment_type = "STATIC" - description = "Entra groups that get the admin, edit and view role in the created namespaces." - display_name = "Namespace Group Object IDs" - is_environment = false - type = "CODE" - updateable_by_consumer = false - } "workspace_identifier" = { assignment_type = "WORKSPACE_IDENTIFIER" description = "" diff --git a/modules/azure/aks-namespace/buildingblock/README.md b/modules/azure/aks-namespace/buildingblock/README.md index 553bce4f1..03c93ff0e 100644 --- a/modules/azure/aks-namespace/buildingblock/README.md +++ b/modules/azure/aks-namespace/buildingblock/README.md @@ -17,8 +17,7 @@ managed identity through workload identity federation and holds no kubeconfig. - a `ResourceQuota` and a `LimitRange` with default container requests and limits - an Entra security group per namespace role (`admin`, `edit`, `view`), filled with the workspace members whose workspace role maps to it (see `role_mapping`) -- a `RoleBinding` per group to the `admin`, `edit` or `view` cluster role. Existing Entra groups can - be bound too, through the `*_group_object_ids` inputs +- a `RoleBinding` per group to the `admin`, `edit` or `view` cluster role ## Requirements @@ -53,11 +52,9 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [admin\_group\_object\_ids](#input\_admin\_group\_object\_ids) | Existing Entra group object IDs, in addition to the group this block creates, granted the `admin` cluster role inside the namespace. | `list(string)` | `[]` | no | | [aks\_cluster\_name](#input\_aks\_cluster\_name) | Name of the AKS cluster the namespace is created in. | `string` | n/a | yes | | [aks\_resource\_group\_name](#input\_aks\_resource\_group\_name) | Resource group of the AKS cluster. | `string` | n/a | yes | | [default\_container\_limits](#input\_default\_container\_limits) | LimitRange defaults for containers that declare no requests or limits of their own. |
object({
cpu_request = optional(string, "100m")
memory_request = optional(string, "128Mi")
cpu_limit = optional(string, "500m")
memory_limit = optional(string, "512Mi")
})
| `{}` | no | -| [edit\_group\_object\_ids](#input\_edit\_group\_object\_ids) | Existing Entra group object IDs, in addition to the group this block creates, granted the `edit` cluster role inside the namespace. | `list(string)` | `[]` | no | | [group\_name\_prefix](#input\_group\_name\_prefix) | Prepended to the display names of the Entra groups this block creates. Leave empty to omit. | `string` | `""` | no | | [namespace\_name](#input\_namespace\_name) | Name of the Kubernetes namespace to create. | `string` | n/a | yes | | [oidc\_token\_file\_path](#input\_oidc\_token\_file\_path) | File the building block runner writes its federated OIDC token to. | `string` | `"/var/run/secrets/workload-identity/azure/token"` | no | @@ -65,7 +62,6 @@ No modules. | [role\_mapping](#input\_role\_mapping) | Maps a meshStack workspace role name to the namespace role (`admin`, `edit` or `view`) its members get. A workspace role missing here gets no access. | `map(string)` | n/a | yes | | [user\_lookup\_attribute](#input\_user\_lookup\_attribute) | Directory attribute used to match workspace members to Entra users. 'upn' matches the User Principal Name; 'email' matches the primary mail address. | `string` | `"email"` | no | | [users](#input\_users) | Workspace members with their meshStack roles, injected by meshStack. Each is added to the group of the namespace role their workspace role maps to. |
list(object({
meshIdentifier = string
username = string
firstName = string
lastName = string
email = string
euid = string
roles = list(string)
}))
| `[]` | no | -| [view\_group\_object\_ids](#input\_view\_group\_object\_ids) | Existing Entra group object IDs, in addition to the group this block creates, granted the `view` cluster role inside the namespace. | `list(string)` | `[]` | no | | [workspace\_identifier](#input\_workspace\_identifier) | Identifier of the meshStack workspace that owns the namespace. Recorded as a label. | `string` | n/a | yes | ## Outputs diff --git a/modules/azure/aks-namespace/buildingblock/main.tf b/modules/azure/aks-namespace/buildingblock/main.tf index cc7a9c6bb..dc4ef23cd 100644 --- a/modules/azure/aks-namespace/buildingblock/main.tf +++ b/modules/azure/aks-namespace/buildingblock/main.tf @@ -50,23 +50,6 @@ locals { ]) created_group_object_ids = { for role, g in azuread_group.namespace : role => g.object_id } - - extra_group_object_ids = { - admin = var.admin_group_object_ids - edit = var.edit_group_object_ids - view = var.view_group_object_ids - } - - # The created group's object ID is unknown until apply, so it cannot be part of a for_each key: - # that binding is keyed by its role alone. - group_bindings = merge( - { for role in local.namespace_roles : "${role}-created" => { cluster_role = role, object_id = local.created_group_object_ids[role] } }, - merge([ - for role, ids in local.extra_group_object_ids : { - for id in ids : "${role}-${id}" => { cluster_role = role, object_id = id } - } - ]...) - ) } resource "azuread_group" "namespace" { @@ -139,8 +122,9 @@ resource "kubernetes_limit_range_v1" "this" { } } +# Each group's name is its namespace role, which is also the cluster role it is bound to. resource "kubernetes_role_binding_v1" "group" { - for_each = local.group_bindings + for_each = azuread_group.namespace metadata { name = "meshstack-${each.key}" @@ -150,7 +134,7 @@ resource "kubernetes_role_binding_v1" "group" { role_ref { api_group = "rbac.authorization.k8s.io" kind = "ClusterRole" - name = each.value.cluster_role + name = each.key } subject { diff --git a/modules/azure/aks-namespace/buildingblock/variables.tf b/modules/azure/aks-namespace/buildingblock/variables.tf index ed999faff..0c704fcac 100644 --- a/modules/azure/aks-namespace/buildingblock/variables.tf +++ b/modules/azure/aks-namespace/buildingblock/variables.tf @@ -27,24 +27,6 @@ variable "namespace_name" { } } -variable "admin_group_object_ids" { - type = list(string) - default = [] - description = "Existing Entra group object IDs, in addition to the group this block creates, granted the `admin` cluster role inside the namespace." -} - -variable "edit_group_object_ids" { - type = list(string) - default = [] - description = "Existing Entra group object IDs, in addition to the group this block creates, granted the `edit` cluster role inside the namespace." -} - -variable "view_group_object_ids" { - type = list(string) - default = [] - description = "Existing Entra group object IDs, in addition to the group this block creates, granted the `view` cluster role inside the namespace." -} - variable "resource_quota" { type = object({ requests_cpu = optional(string, "2") diff --git a/modules/azure/aks-namespace/meshstack_integration.tf b/modules/azure/aks-namespace/meshstack_integration.tf index cc2a19d5c..6eae61968 100644 --- a/modules/azure/aks-namespace/meshstack_integration.tf +++ b/modules/azure/aks-namespace/meshstack_integration.tf @@ -57,11 +57,28 @@ variable "workspace_role_mapping" { "Workspace Owner" = "admin" "Workspace Manager" = "admin" "Workspace Member" = "edit" - "Workspace Reader" = "view" } description = "Maps a meshStack workspace role name to the namespace role (admin, edit or view) its members get in every namespace. A workspace role missing here gets no access." } +variable "resource_quota" { + type = object({ + requests_cpu = optional(string, "2") + requests_memory = optional(string, "4Gi") + limits_cpu = optional(string, "4") + limits_memory = optional(string, "8Gi") + pods = optional(string, "50") + }) + default = { + requests_cpu = "2" + requests_memory = "4Gi" + limits_cpu = "4" + limits_memory = "8Gi" + pods = "50" + } + description = "ResourceQuota applied to every namespace the building block creates. Application teams cannot change it." +} + variable "backplane_name" { type = string default = "azure-aks-namespace" @@ -157,7 +174,7 @@ resource "meshstack_building_block_definition" "this" { spec = { display_name = coalesce(var.bbd_display_name, "Azure AKS Namespace") - description = coalesce(var.bbd_description, "A Kubernetes namespace on the shared AKS cluster, with a resource quota and access for your Entra groups.") + description = coalesce(var.bbd_description, "A Kubernetes namespace on the shared AKS cluster, with a resource quota and access for your workspace members.") support_url = "mailto:support@meshcloud.io" documentation_url = "https://hub.meshcloud.io/platforms/azure/definitions/azure-aks-namespace" notification_subscribers = var.notification_subscribers @@ -181,11 +198,11 @@ resource "meshstack_building_block_definition" "this" { ## 💡 Usage examples **Example 1: A dev and a prod namespace** - Order the block twice, once as `myapp-dev` and once as `myapp-prod`, and give each a different quota. + Order the block twice, once as `myapp-dev` and once as `myapp-prod`. **Example 2: Read-only access for auditors** - Give your auditors the workspace Reader role and they land in the namespace's `view` group. For a group - that already exists, add its object ID to **Additional View Group Object IDs**. + Give your auditors the workspace Reader role and they land in the namespace's `view` group, which can + inspect workloads without changing them. ## 📊 Shared Responsibility @@ -193,7 +210,8 @@ resource "meshstack_building_block_definition" "this" { |---|:---:|:---:| | Operate the AKS cluster and its identity | ✅ | ❌ | | Set up the backplane identity and permissions | ✅ | ❌ | - | Choose the namespace name and quota | ❌ | ✅ | + | Set the namespace quota | ✅ | ❌ | + | Choose the namespace name | ❌ | ✅ | | Decide who is a workspace member and with which role | ❌ | ✅ | | Deploy and operate workloads in the namespace | ❌ | ✅ | EOT @@ -310,46 +328,20 @@ resource "meshstack_building_block_definition" "this" { # jsonencode twice is correct for structured inputs, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 argument = jsonencode(jsonencode(var.workspace_role_mapping)) } + resource_quota = { + type = "CODE" + display_name = "Resource Quota" + description = "ResourceQuota applied to the namespace: requests_cpu, requests_memory, limits_cpu, limits_memory and pods." + assignment_type = "STATIC" + # jsonencode twice is correct for structured inputs, see https://registry.terraform.io/providers/meshcloud/meshstack/latest/docs/resources/building_block_definition#argument-1 + argument = jsonencode(jsonencode(var.resource_quota)) + } users = { type = "CODE" display_name = "Users" description = "Workspace members with their roles. Injected automatically by meshStack." assignment_type = "USER_PERMISSIONS" } - admin_group_object_ids = { - type = "CODE" - display_name = "Additional Admin Group Object IDs" - description = "Existing Entra group object IDs that also get the 'admin' role in the namespace, as a JSON list." - assignment_type = "USER_INPUT" - default_value = jsonencode([]) - } - edit_group_object_ids = { - type = "CODE" - display_name = "Additional Edit Group Object IDs" - description = "Existing Entra group object IDs that also get the 'edit' role in the namespace, as a JSON list." - assignment_type = "USER_INPUT" - default_value = jsonencode([]) - } - view_group_object_ids = { - type = "CODE" - display_name = "Additional View Group Object IDs" - description = "Existing Entra group object IDs that also get the 'view' role in the namespace, as a JSON list." - assignment_type = "USER_INPUT" - default_value = jsonencode([]) - } - resource_quota = { - type = "CODE" - display_name = "Resource Quota" - description = "JSON object with requests_cpu, requests_memory, limits_cpu, limits_memory and pods." - assignment_type = "USER_INPUT" - default_value = jsonencode({ - requests_cpu = "2" - requests_memory = "4Gi" - limits_cpu = "4" - limits_memory = "8Gi" - pods = "50" - }) - } } outputs = { @@ -393,6 +385,8 @@ resource "meshstack_building_block_definition" "this" { } } +provider "meshstack" {} + terraform { required_version = ">= 1.12.0" diff --git a/reference-architectures/azure-kubernetes/README.md b/reference-architectures/azure-kubernetes/README.md index 656afbdca..539b4b09f 100644 --- a/reference-architectures/azure-kubernetes/README.md +++ b/reference-architectures/azure-kubernetes/README.md @@ -59,8 +59,7 @@ Creates the namespace on the platform team's cluster, with: - a `ResourceQuota` and a `LimitRange` with default container requests and limits - an Entra group per namespace role (`admin`, `edit`, `view`), filled from the workspace's members - by their workspace role (owner and manager, member, reader), and bound to the matching cluster role. - Existing groups can be added alongside + by their workspace role (owner and manager, member, reader), and bound to the matching cluster role - the owning workspace recorded as a label The block runs as a user-assigned managed identity that meshStack's building block runner federates From 5c6fef0ee2376e0ab6dd1d0257010664f832c379 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Tue, 13 Oct 2026 10:24:38 +0200 Subject: [PATCH 07/11] feat(azure-kubernetes): add the architecture as a deployable root module meshstack_integration.tf registers the namespace, repository, GitHub connector and starterkit definitions in one apply and wires them together, including the connector's backplane. Also drops a stray provider "meshstack" block from azure/aks-namespace's integration file, which must not configure providers. Co-Authored-By: Claude Sonnet 5.5 --- .../aks-namespace/meshstack_integration.tf | 2 - .../azure-kubernetes/README.md | 65 ++++- .../azure-kubernetes/meshstack_integration.tf | 238 ++++++++++++++++++ 3 files changed, 294 insertions(+), 11 deletions(-) create mode 100644 reference-architectures/azure-kubernetes/meshstack_integration.tf diff --git a/modules/azure/aks-namespace/meshstack_integration.tf b/modules/azure/aks-namespace/meshstack_integration.tf index 6eae61968..b7ad57935 100644 --- a/modules/azure/aks-namespace/meshstack_integration.tf +++ b/modules/azure/aks-namespace/meshstack_integration.tf @@ -385,8 +385,6 @@ resource "meshstack_building_block_definition" "this" { } } -provider "meshstack" {} - terraform { required_version = ">= 1.12.0" diff --git a/reference-architectures/azure-kubernetes/README.md b/reference-architectures/azure-kubernetes/README.md index 539b4b09f..de8328cc6 100644 --- a/reference-architectures/azure-kubernetes/README.md +++ b/reference-architectures/azure-kubernetes/README.md @@ -89,15 +89,62 @@ builds, scans and deploys on every push. | Azure Container Registry | Optional. Pass its resource ID to the `azure/aks-namespace` backplane. | | GitHub organization | With a GitHub App installed for the Terraform GitHub provider. | -### Deployment order - -1. Deploy `azure/aks-namespace` and note its `building_block_definition` output. -2. Deploy `github/repository`. -3. Deploy `aks/github-connector`, passing the namespace definition's UUID as - `aks.namespace_definition_uuid`. -4. Deploy `aks/starterkit`, passing the three definition versions in - `building_block_definition_version_refs` under the keys `git-repository`, `aks-namespace` and - `github-actions-connector`. +### Deploy it as code + +`meshstack_integration.tf` in this folder is a ready-to-use root module that registers all four +building block definitions in the right order and wires them together: the namespace, the +repository and the connector definitions first, then the starterkit that composes them. A +foundation repository references it like any hub module: + +```hcl +module "azure_kubernetes" { + source = "github.com/meshcloud/meshstack-hub//reference-architectures/azure-kubernetes?ref=" + + meshstack = { owning_workspace_identifier = "platform-team" } + hub = { git_ref = "", bbd_draft = false } + + azure_tenant_id = "..." + azure_subscription_id = "..." + azure_scope = "/subscriptions/..." + azure_location = "westeurope" + aks_cluster_name = "aks-prod" + aks_resource_group_name = "rg-aks-prod" + + github = { + org = "acme-platform" + app_id = "..." + app_installation_id = "..." + app_pem_file = file("github-app.pem") + } + github_template_repo_path = "acme-platform/aks-starterkit-template" + apps_base_domain = "apps.example.com" +} +``` + +`hub.git_ref` must be a literal: the nested modules are sourced from it. + +The root module declares no providers, so the caller configures them: + +| Provider | Used for | +|---|---| +| `meshstack` | The building block definitions. | +| `azurerm`, `azuread` | The namespace backplane (managed identity, role assignments, Graph permissions) and the connector backplane (container registry, registry service principal). | +| `kubernetes` | The connector backplane, which creates a cluster role the connector's service accounts bind to. Point it at the AKS cluster with admin credentials. | + +The identity running the deployment needs to create managed identities and role assignments on the +cluster, and to grant Microsoft Graph app roles (admin consent) for the namespace backplane. + +#### What it registers + +| Module | Definition | Wired to | +|---|---|---| +| `azure/aks-namespace` | Namespace with quota and Entra groups | Its backplane identity trusts the definition's runner identity. | +| `github/repository` | GitHub repository from a template | | +| `aks/github-connector` | GitHub Actions to namespace wiring | The repository and namespace definitions. | +| `aks/starterkit` | The single catalog entry for application teams | All three definitions above, by version reference. | + +Set `hub.bbd_draft = false` once the definitions are ready to be used; the starterkit then orders +released versions. ## Shared Responsibilities diff --git a/reference-architectures/azure-kubernetes/meshstack_integration.tf b/reference-architectures/azure-kubernetes/meshstack_integration.tf new file mode 100644 index 000000000..873b93355 --- /dev/null +++ b/reference-architectures/azure-kubernetes/meshstack_integration.tf @@ -0,0 +1,238 @@ +variable "azure_tenant_id" { + type = string + description = "Azure Entra tenant ID the namespace building block authenticates against." +} + +variable "azure_subscription_id" { + type = string + description = "Azure subscription that hosts the AKS cluster." +} + +variable "azure_scope" { + type = string + description = "Azure management group or subscription scope the namespace backplane's custom role is assignable at. Must contain the AKS cluster." +} + +variable "azure_location" { + type = string + description = "Azure region for the backplane resources (managed identity, container registry)." +} + +variable "azure_user_lookup_attribute" { + type = string + default = "email" + description = "Directory attribute used to match meshStack workspace members to Entra users. 'email' suits B2B guests, 'upn' only works for members native to the tenant." +} + +variable "azure_group_name_prefix" { + type = string + default = "" + description = "Prepended to the display names of the Entra groups created per namespace. Leave empty to omit." +} + +variable "aks_cluster_name" { + type = string + description = "Name of the existing AKS cluster. It must be Entra-integrated with Azure RBAC for Kubernetes authorization enabled." +} + +variable "aks_resource_group_name" { + type = string + description = "Resource group of the AKS cluster." +} + +variable "acr_id" { + type = string + default = null + description = "Resource ID of an existing Azure Container Registry. When set, the cluster's kubelet identity gets AcrPull on it." +} + +variable "workspace_role_mapping" { + type = map(string) + default = { + "Workspace Owner" = "admin" + "Workspace Manager" = "admin" + "Workspace Member" = "edit" + "Workspace Reader" = "view" + } + description = "Maps a meshStack workspace role name to the namespace role (admin, edit or view) its members get in every namespace." +} + +variable "resource_quota" { + type = object({ + requests_cpu = optional(string, "2") + requests_memory = optional(string, "4Gi") + limits_cpu = optional(string, "4") + limits_memory = optional(string, "8Gi") + pods = optional(string, "50") + }) + default = { + requests_cpu = "2" + requests_memory = "4Gi" + limits_cpu = "4" + limits_memory = "8Gi" + pods = "50" + } + description = "ResourceQuota applied to every namespace the platform creates." +} + +variable "github" { + type = object({ + org = string + app_id = string + app_installation_id = string + app_pem_file = string + }) + sensitive = true + description = "GitHub organization and the GitHub App credentials the GitHub Terraform provider authenticates with. app_pem_file is the raw PEM private key content." +} + +variable "github_template_repo_path" { + type = string + description = "Template repository (owner/repo) every application repository is created from." +} + +variable "apps_base_domain" { + type = string + description = "Base domain application URLs are built under (example: `apps.example.com`)." +} + +variable "connector_backplane_name" { + type = string + default = "bb-github-connector" + description = "Prefix for the resources the GitHub Actions connector's backplane creates (resource group, app registration, container registry). The registry name is derived from it, so it must be globally unique." +} + +variable "notification_subscribers" { + type = list(string) + default = [] + description = "List of email addresses to notify on building block lifecycle events." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to every building block definition." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = { + git_ref = "main" + bbd_draft = true + } + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition versions are kept in draft mode. + EOT +} + +output "building_block_definitions" { + description = "The building block definitions this architecture registers, by role. The starterkit is the one application teams order." + value = { + starterkit = module.starterkit.building_block_definition + namespace = module.aks_namespace.building_block_definition + repository = module.github_repository.building_block_definition + github_connector = module.github_connector.building_block_definition + } +} + +module "aks_namespace" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/aks-namespace?ref=${var.hub.git_ref}" + + azure_tenant_id = var.azure_tenant_id + azure_subscription_id = var.azure_subscription_id + azure_scope = var.azure_scope + azure_location = var.azure_location + azure_user_lookup_attribute = var.azure_user_lookup_attribute + azure_group_name_prefix = var.azure_group_name_prefix + aks_cluster_name = var.aks_cluster_name + aks_resource_group_name = var.aks_resource_group_name + acr_id = var.acr_id + workspace_role_mapping = var.workspace_role_mapping + resource_quota = var.resource_quota + notification_subscribers = var.notification_subscribers + + meshstack = var.meshstack + hub = var.hub +} + +module "github_repository" { + source = "github.com/meshcloud/meshstack-hub//modules/github/repository?ref=${var.hub.git_ref}" + + github = var.github + notification_subscribers = var.notification_subscribers + + meshstack = var.meshstack + hub = var.hub +} + +# Mints the cluster-facing credentials the connector hands to GitHub Actions: a kubeconfig stub and +# the container registry the pipelines push to. Its providers (azurerm, azuread, kubernetes) are the +# caller's to configure, see the readme. +module "github_connector_backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/aks/github-connector/backplane?ref=${var.hub.git_ref}" + + resource_prefix = var.connector_backplane_name + + aks = { + cluster_name = var.aks_cluster_name + resource_group_name = var.aks_resource_group_name + } + acr = { + location = var.azure_location + } +} + +module "github_connector" { + source = "github.com/meshcloud/meshstack-hub//modules/aks/github-connector?ref=${var.hub.git_ref}" + + github = { + repo_definition_uuid = module.github_repository.building_block_definition.uuid + org = var.github.org + app_id = var.github.app_id + app_installation_id = var.github.app_installation_id + app_pem_file = var.github.app_pem_file + } + aks = { + namespace_definition_uuid = module.aks_namespace.building_block_definition.uuid + connector_config_tf_base64 = base64encode(module.github_connector_backplane.config_tf) + } + notification_subscribers = var.notification_subscribers + + meshstack = var.meshstack + hub = var.hub +} + +module "starterkit" { + source = "github.com/meshcloud/meshstack-hub//modules/aks/starterkit?ref=${var.hub.git_ref}" + + building_block_definition_version_refs = { + "git-repository" = module.github_repository.building_block_definition.version_ref + "aks-namespace" = module.aks_namespace.building_block_definition.version_ref + "github-actions-connector" = module.github_connector.building_block_definition.version_ref + } + github_org = var.github.org + github_template_repo_path = var.github_template_repo_path + apps_base_domain = var.apps_base_domain + notification_subscribers = var.notification_subscribers + + meshstack = var.meshstack + hub = var.hub +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.26.2" + } + } +} From 0db708a921593928a386c3915645a6c88748bcbe Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Tue, 13 Oct 2026 10:57:17 +0200 Subject: [PATCH 08/11] fix(azure-kubernetes): use distinct backplane names and drop the caller's kubernetes provider The namespace and connector backplanes now default to names that do not collide with their standalone defaults, and both are settable through namespace_backplane_name and connector_backplane_name. The connector backplane configures its own kubernetes provider from the cluster it already reads, so the root module no longer needs one from the caller. Co-Authored-By: Claude Sonnet 5.5 --- modules/aks/github-connector/backplane/provider.tf | 8 ++++++++ reference-architectures/azure-kubernetes/README.md | 8 +++++++- .../azure-kubernetes/meshstack_integration.tf | 11 +++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 modules/aks/github-connector/backplane/provider.tf diff --git a/modules/aks/github-connector/backplane/provider.tf b/modules/aks/github-connector/backplane/provider.tf new file mode 100644 index 000000000..58daf53ce --- /dev/null +++ b/modules/aks/github-connector/backplane/provider.tf @@ -0,0 +1,8 @@ +# Configured from the cluster this backplane already reads, so a caller needs no kubernetes provider +# of their own. Uses the admin credentials, as the connector's config.tf output does. +provider "kubernetes" { + host = data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].host + cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].cluster_ca_certificate) + client_certificate = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_certificate) + client_key = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_key) +} diff --git a/reference-architectures/azure-kubernetes/README.md b/reference-architectures/azure-kubernetes/README.md index de8328cc6..c8f0f9fd8 100644 --- a/reference-architectures/azure-kubernetes/README.md +++ b/reference-architectures/azure-kubernetes/README.md @@ -129,7 +129,13 @@ The root module declares no providers, so the caller configures them: |---|---| | `meshstack` | The building block definitions. | | `azurerm`, `azuread` | The namespace backplane (managed identity, role assignments, Graph permissions) and the connector backplane (container registry, registry service principal). | -| `kubernetes` | The connector backplane, which creates a cluster role the connector's service accounts bind to. Point it at the AKS cluster with admin credentials. | + +The connector backplane configures its own `kubernetes` provider from the cluster's admin +credentials, so the cluster needs local accounts enabled. + +Backplane resource names are unique per subscription or tenant (resource groups, the custom role, +the container registry). Set `namespace_backplane_name` and `connector_backplane_name` when another +deployment already uses the defaults. The identity running the deployment needs to create managed identities and role assignments on the cluster, and to grant Microsoft Graph app roles (admin consent) for the namespace backplane. diff --git a/reference-architectures/azure-kubernetes/meshstack_integration.tf b/reference-architectures/azure-kubernetes/meshstack_integration.tf index 873b93355..0f5c035a0 100644 --- a/reference-architectures/azure-kubernetes/meshstack_integration.tf +++ b/reference-architectures/azure-kubernetes/meshstack_integration.tf @@ -96,10 +96,16 @@ variable "apps_base_domain" { description = "Base domain application URLs are built under (example: `apps.example.com`)." } +variable "namespace_backplane_name" { + type = string + default = "azure-kubernetes-namespace" + description = "Name of the namespace backplane's resource group, managed identity and custom role. The role definition name is unique per Entra tenant, so set a different value when another deployment already uses this one. Must match ^[-a-z0-9]+$." +} + variable "connector_backplane_name" { type = string - default = "bb-github-connector" - description = "Prefix for the resources the GitHub Actions connector's backplane creates (resource group, app registration, container registry). The registry name is derived from it, so it must be globally unique." + default = "azure-kubernetes-connector" + description = "Prefix for the resources the GitHub Actions connector's backplane creates (resource group, app registration, container registry). The registry name is this value without dashes, so it must be globally unique across Azure, 5 to 50 lowercase alphanumerics and dashes." } variable "notification_subscribers" { @@ -154,6 +160,7 @@ module "aks_namespace" { aks_cluster_name = var.aks_cluster_name aks_resource_group_name = var.aks_resource_group_name acr_id = var.acr_id + backplane_name = var.namespace_backplane_name workspace_role_mapping = var.workspace_role_mapping resource_quota = var.resource_quota notification_subscribers = var.notification_subscribers From 818c2feb48b320acd5dc4863daf55d090580b88d Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Tue, 13 Oct 2026 13:36:04 +0200 Subject: [PATCH 09/11] fix(aks/github-connector): rename the clusterissuer ClusterRole to avoid clashing with an existing one Co-Authored-By: Claude Sonnet 5.5 --- modules/aks/github-connector/backplane/main.tf | 2 +- modules/aks/github-connector/buildingblock/kubernetes.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/aks/github-connector/backplane/main.tf b/modules/aks/github-connector/backplane/main.tf index f0f7b9e27..051374635 100644 --- a/modules/aks/github-connector/backplane/main.tf +++ b/modules/aks/github-connector/backplane/main.tf @@ -58,7 +58,7 @@ resource "azurerm_role_assignment" "bb_github_connector_acr" { # The ClusterIssuer is needed so that SSL certificates can be issued for projects using the GitHub Actions Connector. resource "kubernetes_cluster_role" "clusterissuer_reader" { metadata { - name = "clusterissuer-reader" + name = "aks-github-connector-clusterissuer-reader" } rule { diff --git a/modules/aks/github-connector/buildingblock/kubernetes.tf b/modules/aks/github-connector/buildingblock/kubernetes.tf index 0c016dd69..884172c9d 100644 --- a/modules/aks/github-connector/buildingblock/kubernetes.tf +++ b/modules/aks/github-connector/buildingblock/kubernetes.tf @@ -44,7 +44,7 @@ resource "kubernetes_cluster_role_binding" "github_actions_clusterissuer_access" role_ref { api_group = "rbac.authorization.k8s.io" kind = "ClusterRole" - name = "clusterissuer-reader" # This role is created in the backplane module + name = "aks-github-connector-clusterissuer-reader" # This role is created in the backplane module } subject { From 478417fd6917561aebec1f5e5dd845995c410a6b Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Tue, 13 Oct 2026 14:18:28 +0200 Subject: [PATCH 10/11] chore: remove non-existent role --- .../azure-kubernetes/meshstack_integration.tf | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/reference-architectures/azure-kubernetes/meshstack_integration.tf b/reference-architectures/azure-kubernetes/meshstack_integration.tf index 0f5c035a0..cac482588 100644 --- a/reference-architectures/azure-kubernetes/meshstack_integration.tf +++ b/reference-architectures/azure-kubernetes/meshstack_integration.tf @@ -50,9 +50,8 @@ variable "workspace_role_mapping" { type = map(string) default = { "Workspace Owner" = "admin" - "Workspace Manager" = "admin" - "Workspace Member" = "edit" - "Workspace Reader" = "view" + "Workspace Manager" = "edit" + "Workspace Member" = "view" } description = "Maps a meshStack workspace role name to the namespace role (admin, edit or view) its members get in every namespace." } @@ -89,11 +88,13 @@ variable "github" { variable "github_template_repo_path" { type = string description = "Template repository (owner/repo) every application repository is created from." + default = "likvid-bank/aks-starterkit-template" } variable "apps_base_domain" { type = string description = "Base domain application URLs are built under (example: `apps.example.com`)." + default = "likvid-k8s.msh.host" } variable "namespace_backplane_name" { @@ -241,5 +242,13 @@ terraform { source = "meshcloud/meshstack" version = ">= 0.26.2" } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.64.0" + } } } + +provider "azurerm" { + features {} +} From 2e5f6e5364a0addd2b5d5e1339a0ee34c33c0c58 Mon Sep 17 00:00:00 2001 From: Jelle den Burger Date: Wed, 14 Oct 2026 09:58:39 +0200 Subject: [PATCH 11/11] refactor(aks/namespace): move the azure aks-namespace module under modules/aks Co-Authored-By: Claude Sonnet 5.5 --- .../namespace}/backplane/README.md | 0 .../namespace}/backplane/main.tf | 0 .../namespace}/backplane/outputs.tf | 0 .../namespace}/backplane/provider.tf | 0 .../namespace}/backplane/variables.tf | 0 .../namespace}/backplane/versions.tf | 0 .../namespace}/buildingblock/README.md | 0 .../namespace}/buildingblock/logo.png | Bin .../namespace}/buildingblock/main.tf | 0 .../namespace}/buildingblock/outputs.tf | 0 .../namespace}/buildingblock/provider.tf | 0 .../namespace}/buildingblock/variables.tf | 0 .../namespace}/buildingblock/versions.tf | 0 .../aks-namespace => aks/namespace}/e2e/main.tf | 0 .../aks-namespace => aks/namespace}/e2e/provider.tf | 0 .../namespace}/e2e/terraform.tf | 0 .../e2e/tests/azure_aks_namespace_hub.tftest.hcl | 2 +- .../namespace}/meshstack_integration.tf | 8 ++++---- reference-architectures/azure-kubernetes/README.md | 10 +++++----- .../azure-kubernetes/azure-kubernetes.dot | 2 +- .../azure-kubernetes/azure-kubernetes.svg | 2 +- .../azure-kubernetes/meshstack_integration.tf | 2 +- 22 files changed, 13 insertions(+), 13 deletions(-) rename modules/{azure/aks-namespace => aks/namespace}/backplane/README.md (100%) rename modules/{azure/aks-namespace => aks/namespace}/backplane/main.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/backplane/outputs.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/backplane/provider.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/backplane/variables.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/backplane/versions.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/README.md (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/logo.png (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/main.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/outputs.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/provider.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/variables.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/buildingblock/versions.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/e2e/main.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/e2e/provider.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/e2e/terraform.tf (100%) rename modules/{azure/aks-namespace => aks/namespace}/e2e/tests/azure_aks_namespace_hub.tftest.hcl (76%) rename modules/{azure/aks-namespace => aks/namespace}/meshstack_integration.tf (97%) diff --git a/modules/azure/aks-namespace/backplane/README.md b/modules/aks/namespace/backplane/README.md similarity index 100% rename from modules/azure/aks-namespace/backplane/README.md rename to modules/aks/namespace/backplane/README.md diff --git a/modules/azure/aks-namespace/backplane/main.tf b/modules/aks/namespace/backplane/main.tf similarity index 100% rename from modules/azure/aks-namespace/backplane/main.tf rename to modules/aks/namespace/backplane/main.tf diff --git a/modules/azure/aks-namespace/backplane/outputs.tf b/modules/aks/namespace/backplane/outputs.tf similarity index 100% rename from modules/azure/aks-namespace/backplane/outputs.tf rename to modules/aks/namespace/backplane/outputs.tf diff --git a/modules/azure/aks-namespace/backplane/provider.tf b/modules/aks/namespace/backplane/provider.tf similarity index 100% rename from modules/azure/aks-namespace/backplane/provider.tf rename to modules/aks/namespace/backplane/provider.tf diff --git a/modules/azure/aks-namespace/backplane/variables.tf b/modules/aks/namespace/backplane/variables.tf similarity index 100% rename from modules/azure/aks-namespace/backplane/variables.tf rename to modules/aks/namespace/backplane/variables.tf diff --git a/modules/azure/aks-namespace/backplane/versions.tf b/modules/aks/namespace/backplane/versions.tf similarity index 100% rename from modules/azure/aks-namespace/backplane/versions.tf rename to modules/aks/namespace/backplane/versions.tf diff --git a/modules/azure/aks-namespace/buildingblock/README.md b/modules/aks/namespace/buildingblock/README.md similarity index 100% rename from modules/azure/aks-namespace/buildingblock/README.md rename to modules/aks/namespace/buildingblock/README.md diff --git a/modules/azure/aks-namespace/buildingblock/logo.png b/modules/aks/namespace/buildingblock/logo.png similarity index 100% rename from modules/azure/aks-namespace/buildingblock/logo.png rename to modules/aks/namespace/buildingblock/logo.png diff --git a/modules/azure/aks-namespace/buildingblock/main.tf b/modules/aks/namespace/buildingblock/main.tf similarity index 100% rename from modules/azure/aks-namespace/buildingblock/main.tf rename to modules/aks/namespace/buildingblock/main.tf diff --git a/modules/azure/aks-namespace/buildingblock/outputs.tf b/modules/aks/namespace/buildingblock/outputs.tf similarity index 100% rename from modules/azure/aks-namespace/buildingblock/outputs.tf rename to modules/aks/namespace/buildingblock/outputs.tf diff --git a/modules/azure/aks-namespace/buildingblock/provider.tf b/modules/aks/namespace/buildingblock/provider.tf similarity index 100% rename from modules/azure/aks-namespace/buildingblock/provider.tf rename to modules/aks/namespace/buildingblock/provider.tf diff --git a/modules/azure/aks-namespace/buildingblock/variables.tf b/modules/aks/namespace/buildingblock/variables.tf similarity index 100% rename from modules/azure/aks-namespace/buildingblock/variables.tf rename to modules/aks/namespace/buildingblock/variables.tf diff --git a/modules/azure/aks-namespace/buildingblock/versions.tf b/modules/aks/namespace/buildingblock/versions.tf similarity index 100% rename from modules/azure/aks-namespace/buildingblock/versions.tf rename to modules/aks/namespace/buildingblock/versions.tf diff --git a/modules/azure/aks-namespace/e2e/main.tf b/modules/aks/namespace/e2e/main.tf similarity index 100% rename from modules/azure/aks-namespace/e2e/main.tf rename to modules/aks/namespace/e2e/main.tf diff --git a/modules/azure/aks-namespace/e2e/provider.tf b/modules/aks/namespace/e2e/provider.tf similarity index 100% rename from modules/azure/aks-namespace/e2e/provider.tf rename to modules/aks/namespace/e2e/provider.tf diff --git a/modules/azure/aks-namespace/e2e/terraform.tf b/modules/aks/namespace/e2e/terraform.tf similarity index 100% rename from modules/azure/aks-namespace/e2e/terraform.tf rename to modules/aks/namespace/e2e/terraform.tf diff --git a/modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl b/modules/aks/namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl similarity index 76% rename from modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl rename to modules/aks/namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl index fdbb6bb6b..d29ec522a 100644 --- a/modules/azure/aks-namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl +++ b/modules/aks/namespace/e2e/tests/azure_aks_namespace_hub.tftest.hcl @@ -1,7 +1,7 @@ run "azure_aks_namespace_hub" { assert { condition = meshstack_building_block.this.status.status == "SUCCEEDED" - error_message = "azure/aks-namespace hub building block expected SUCCEEDED, got ${meshstack_building_block.this.status.status}" + error_message = "aks/namespace hub building block expected SUCCEEDED, got ${meshstack_building_block.this.status.status}" } assert { diff --git a/modules/azure/aks-namespace/meshstack_integration.tf b/modules/aks/namespace/meshstack_integration.tf similarity index 97% rename from modules/azure/aks-namespace/meshstack_integration.tf rename to modules/aks/namespace/meshstack_integration.tf index b7ad57935..9c546305a 100644 --- a/modules/azure/aks-namespace/meshstack_integration.tf +++ b/modules/aks/namespace/meshstack_integration.tf @@ -148,7 +148,7 @@ output "building_block_definition" { } module "backplane" { - source = "github.com/meshcloud/meshstack-hub//modules/azure/aks-namespace/backplane?ref=${var.hub.git_ref}" + source = "github.com/meshcloud/meshstack-hub//modules/aks/namespace/backplane?ref=${var.hub.git_ref}" name = var.backplane_name scope = var.azure_scope @@ -176,9 +176,9 @@ resource "meshstack_building_block_definition" "this" { display_name = coalesce(var.bbd_display_name, "Azure AKS Namespace") description = coalesce(var.bbd_description, "A Kubernetes namespace on the shared AKS cluster, with a resource quota and access for your workspace members.") support_url = "mailto:support@meshcloud.io" - documentation_url = "https://hub.meshcloud.io/platforms/azure/definitions/azure-aks-namespace" + documentation_url = "https://hub.meshcloud.io/platforms/aks/definitions/azure-aks-namespace" notification_subscribers = var.notification_subscribers - symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/azure/aks-namespace/buildingblock/logo.png" + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/${var.hub.git_ref}/modules/aks/namespace/buildingblock/logo.png" target_type = "WORKSPACE_LEVEL" readme = coalesce(var.bbd_readme, chomp(<<-EOT @@ -231,7 +231,7 @@ resource "meshstack_building_block_definition" "this" { terraform = { terraform_version = "1.12.5" repository_url = "https://github.com/meshcloud/meshstack-hub.git" - repository_path = "modules/azure/aks-namespace/buildingblock" + repository_path = "modules/aks/namespace/buildingblock" ref_name = var.hub.git_ref use_mesh_http_backend_fallback = true } diff --git a/reference-architectures/azure-kubernetes/README.md b/reference-architectures/azure-kubernetes/README.md index c8f0f9fd8..687e58445 100644 --- a/reference-architectures/azure-kubernetes/README.md +++ b/reference-architectures/azure-kubernetes/README.md @@ -9,7 +9,7 @@ cloudProviders: buildingBlocks: - path: aks/starterkit role: Orchestrates the full developer onboarding experience by composing all building blocks into one self-service offering. - - path: azure/aks-namespace + - path: aks/namespace role: Creates a Kubernetes namespace with a resource quota, default limits and Entra group access on an existing AKS cluster. - path: github/repository role: Creates GitHub repositories with predefined configurations and access control for application teams. @@ -46,14 +46,14 @@ The starterkit is a **building block composition** that application teams order self-service catalog. When a developer clicks "Order", it: 1. Creates a **GitHub repository** from a configurable template (`github/repository`). -2. Creates a **dev** and a **prod** namespace (`azure/aks-namespace`). +2. Creates a **dev** and a **prod** namespace (`aks/namespace`). 3. Wires each namespace to the repository with a **GitHub Actions connector** (`aks/github-connector`). Dev deploys from `main`, prod from `release`. Each connector is ordered with the repository and its namespace as parent building blocks, and reads the namespace name from the namespace block's output. -### 2. Namespace: `azure/aks-namespace` +### 2. Namespace: `aks/namespace` Creates the namespace on the platform team's cluster, with: @@ -86,7 +86,7 @@ builds, scans and deploys on every push. |---|---| | meshStack instance | With the OpenTofu building block runner configured. | | AKS cluster | Entra-integrated, with Azure RBAC for Kubernetes authorization enabled, and reachable from the runner. | -| Azure Container Registry | Optional. Pass its resource ID to the `azure/aks-namespace` backplane. | +| Azure Container Registry | Optional. Pass its resource ID to the `aks/namespace` backplane. | | GitHub organization | With a GitHub App installed for the Terraform GitHub provider. | ### Deploy it as code @@ -144,7 +144,7 @@ cluster, and to grant Microsoft Graph app roles (admin consent) for the namespac | Module | Definition | Wired to | |---|---|---| -| `azure/aks-namespace` | Namespace with quota and Entra groups | Its backplane identity trusts the definition's runner identity. | +| `aks/namespace` | Namespace with quota and Entra groups | Its backplane identity trusts the definition's runner identity. | | `github/repository` | GitHub repository from a template | | | `aks/github-connector` | GitHub Actions to namespace wiring | The repository and namespace definitions. | | `aks/starterkit` | The single catalog entry for application teams | All three definitions above, by version reference. | diff --git a/reference-architectures/azure-kubernetes/azure-kubernetes.dot b/reference-architectures/azure-kubernetes/azure-kubernetes.dot index 57c266309..fec35d89e 100644 --- a/reference-architectures/azure-kubernetes/azure-kubernetes.dot +++ b/reference-architectures/azure-kubernetes/azure-kubernetes.dot @@ -60,7 +60,7 @@ digraph azure_kubernetes { BBD_S [label="📦 aks/starterkit BBD\nself-service catalog entry" fillcolor="#ecedfb" color="#9aa2e6"] BBD_R [label="📦 github/repository BBD" fillcolor="#ecedfb" color="#9aa2e6"] - BBD_N [label="📦 azure/aks-namespace BBD" fillcolor="#ecedfb" color="#9aa2e6"] + BBD_N [label="📦 aks/namespace BBD" fillcolor="#ecedfb" color="#9aa2e6"] BBD_C [label="📦 aks/github-connector BBD" fillcolor="#ecedfb" color="#9aa2e6"] { rank=same; BBD_R -> BBD_N -> BBD_C [style=invis] } diff --git a/reference-architectures/azure-kubernetes/azure-kubernetes.svg b/reference-architectures/azure-kubernetes/azure-kubernetes.svg index 7f961f633..79cbcc140 100644 --- a/reference-architectures/azure-kubernetes/azure-kubernetes.svg +++ b/reference-architectures/azure-kubernetes/azure-kubernetes.svg @@ -159,7 +159,7 @@ BBD_N -📦 azure/aks-namespace BBD +📦 aks/namespace BBD diff --git a/reference-architectures/azure-kubernetes/meshstack_integration.tf b/reference-architectures/azure-kubernetes/meshstack_integration.tf index cac482588..d091309b4 100644 --- a/reference-architectures/azure-kubernetes/meshstack_integration.tf +++ b/reference-architectures/azure-kubernetes/meshstack_integration.tf @@ -150,7 +150,7 @@ output "building_block_definitions" { } module "aks_namespace" { - source = "github.com/meshcloud/meshstack-hub//modules/azure/aks-namespace?ref=${var.hub.git_ref}" + source = "github.com/meshcloud/meshstack-hub//modules/aks/namespace?ref=${var.hub.git_ref}" azure_tenant_id = var.azure_tenant_id azure_subscription_id = var.azure_subscription_id