From a49f3a46571533194592a78be252dab5f484ce46 Mon Sep 17 00:00:00 2001 From: RebeccaCalixte <262454636+Rebecca-Calixte@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:01:33 -0400 Subject: [PATCH 1/5] Add AKS Entra Kubernetes RBAC quickstart --- quickstart/101-aks-entra-k8s-rbac/README.md | 123 ++++++++++++ quickstart/101-aks-entra-k8s-rbac/main.tf | 204 ++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 quickstart/101-aks-entra-k8s-rbac/README.md create mode 100644 quickstart/101-aks-entra-k8s-rbac/main.tf diff --git a/quickstart/101-aks-entra-k8s-rbac/README.md b/quickstart/101-aks-entra-k8s-rbac/README.md new file mode 100644 index 000000000..4dbcd67a5 --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/README.md @@ -0,0 +1,123 @@ +# Use Microsoft Entra ID Groups with Kubernetes RBAC in AKS + +This template uses Microsoft Entra ID groups with Kubernetes role-based access control (Kubernetes RBAC) in an existing Azure Kubernetes Service (AKS) cluster. + +The example creates two Microsoft Entra groups and scopes each group to a namespace: + +- The `appdev` group can manage resources in the `dev` namespace. +- The `opssre` group can manage resources in the `sre` namespace. + +The AKS cluster must already have Microsoft Entra integration and Kubernetes RBAC enabled. Azure RBAC for Kubernetes Authorization must be disabled for this example. + +## Prerequisites + +- An Azure subscription +- An existing AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled +- Azure RBAC for Kubernetes Authorization disabled on the cluster +- Terraform `>= 1.6.0` installed +- Azure CLI and `kubectl` installed +- Permission to create Microsoft Entra users and groups +- Permission to assign Azure roles at the AKS cluster scope +- Permission to manage Kubernetes resources on the AKS cluster + +Sign in to Azure and select the subscription to use: + +```console +az login +az account set --subscription +``` + +## Terraform providers and variables + +This sample uses the AzureRM provider to reference the existing AKS cluster and assign Azure permissions, the AzureAD provider to create Microsoft Entra users and groups, and the Kubernetes provider to create namespaces, Roles, and RoleBindings. + +The Terraform variables require values for the existing cluster and the example users: + +```hcl +resource_group_name = "" +aks_cluster_name = "" +appdev_user_principal_name = "" +opssre_user_principal_name = "" +temporary_password = "" +``` + +`temporary_password` is a sensitive Terraform variable. Replace `` with a strong temporary password supplied through a secure `terraform.tfvars` file or another protected input method. Do not commit that value. + +## Example + +Create a `terraform.tfvars` file with values for the existing cluster and test users, then initialize, format, and validate the configuration: + +```console +terraform init +terraform fmt +terraform validate +``` + +Review and apply the configuration: + +```console +terraform plan +terraform apply +``` + +The configuration creates the following Microsoft Entra and Kubernetes resources: + +- `appdev` and `opssre` Microsoft Entra security groups +- One example Microsoft Entra user in each group +- Cluster User Role assignments for both groups +- `dev` and `sre` Kubernetes namespaces +- Namespace-scoped Kubernetes Roles and RoleBindings + +## Verify namespace access + +Get credentials for the AKS cluster: + +```console +az aks get-credentials \ + --resource-group \ + --name +``` + +Verify that both namespaces exist: + +```console +kubectl get namespaces +``` + +The output should include `dev` and `sre`. + +## Test appdev access + +Authenticate as the `appdev` test user and create a pod in the `dev` namespace: + +```console +kubectl run nginx-dev \ + --image=nginx \ + --restart=Never \ + --namespace dev +kubectl get pods --namespace dev +``` + +Listing pods across all namespaces or creating a pod in the `sre` namespace should return a `Forbidden` error because the `appdev` group is scoped to `dev`. + +## Test opssre access + +Authenticate as the `opssre` test user and create a pod in the `sre` namespace: + +```console +kubectl run nginx-sre \ + --image=nginx \ + --restart=Never \ + --namespace sre +kubectl get pods --namespace sre +``` + +Creating a pod in the `dev` namespace should return a `Forbidden` error because the `opssre` group is scoped to `sre`. + +## Clean up + +Remove the namespaces, RoleBindings, Roles, groups, users, and role assignments created by this configuration: + +```console +terraform destroy +``` diff --git a/quickstart/101-aks-entra-k8s-rbac/main.tf b/quickstart/101-aks-entra-k8s-rbac/main.tf new file mode 100644 index 000000000..1236beeae --- /dev/null +++ b/quickstart/101-aks-entra-k8s-rbac/main.tf @@ -0,0 +1,204 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.30" + } + } +} + +provider "azurerm" { + features {} +} + +provider "azuread" {} + +variable "resource_group_name" { + type = string + description = "Name of the resource group that contains the existing AKS cluster." +} + +variable "aks_cluster_name" { + type = string + description = "Name of the existing AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled." +} + +variable "appdev_user_principal_name" { + type = string + description = "User principal name for the application developer test user." +} + +variable "opssre_user_principal_name" { + type = string + description = "User principal name for the SRE test user." +} + +variable "temporary_password" { + type = string + description = "Temporary password used for the example Microsoft Entra users." + sensitive = true +} + +data "azurerm_kubernetes_cluster" "aks" { + name = var.aks_cluster_name + resource_group_name = var.resource_group_name +} + +provider "kubernetes" { + host = data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].host + 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) + cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].cluster_ca_certificate) +} + +resource "azuread_group" "appdev" { + display_name = "appdev" + security_enabled = true +} + +resource "azuread_group" "opssre" { + display_name = "opssre" + security_enabled = true +} + +resource "azuread_user" "aksdev" { + user_principal_name = var.appdev_user_principal_name + display_name = "AKS Dev" + mail_nickname = "aksdev" + password = var.temporary_password +} + +resource "azuread_user" "akssre" { + user_principal_name = var.opssre_user_principal_name + display_name = "AKS SRE" + mail_nickname = "akssre" + password = var.temporary_password +} + +resource "azuread_group_member" "appdev_member" { + group_object_id = azuread_group.appdev.object_id + member_object_id = azuread_user.aksdev.object_id +} + +resource "azuread_group_member" "opssre_member" { + group_object_id = azuread_group.opssre.object_id + member_object_id = azuread_user.akssre.object_id +} + +resource "azurerm_role_assignment" "appdev_cluster_user" { + scope = data.azurerm_kubernetes_cluster.aks.id + role_definition_name = "Azure Kubernetes Service Cluster User Role" + principal_id = azuread_group.appdev.object_id +} + +resource "azurerm_role_assignment" "opssre_cluster_user" { + scope = data.azurerm_kubernetes_cluster.aks.id + role_definition_name = "Azure Kubernetes Service Cluster User Role" + principal_id = azuread_group.opssre.object_id +} + +resource "kubernetes_namespace" "dev" { + metadata { + name = "dev" + } +} + +resource "kubernetes_namespace" "sre" { + metadata { + name = "sre" + } +} + +resource "kubernetes_role" "dev_full_access" { + metadata { + name = "dev-user-full-access" + namespace = kubernetes_namespace.dev.metadata[0].name + } + + rule { + api_groups = ["", "extensions", "apps"] + resources = ["*"] + verbs = ["*"] + } + + rule { + api_groups = ["batch"] + resources = ["jobs", "cronjobs"] + verbs = ["*"] + } +} + +resource "kubernetes_role" "sre_full_access" { + metadata { + name = "sre-user-full-access" + namespace = kubernetes_namespace.sre.metadata[0].name + } + + rule { + api_groups = ["", "extensions", "apps"] + resources = ["*"] + verbs = ["*"] + } + + rule { + api_groups = ["batch"] + resources = ["jobs", "cronjobs"] + verbs = ["*"] + } +} + +resource "kubernetes_role_binding" "dev_user_access" { + metadata { + name = "dev-user-access" + namespace = kubernetes_namespace.dev.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.dev_full_access.metadata[0].name + } + + subject { + kind = "Group" + name = azuread_group.appdev.object_id + api_group = "rbac.authorization.k8s.io" + } +} + +resource "kubernetes_role_binding" "sre_user_access" { + metadata { + name = "sre-user-access" + namespace = kubernetes_namespace.sre.metadata[0].name + } + + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "Role" + name = kubernetes_role.sre_full_access.metadata[0].name + } + + subject { + kind = "Group" + name = azuread_group.opssre.object_id + api_group = "rbac.authorization.k8s.io" + } +} + +output "appdev_group_object_id" { + value = azuread_group.appdev.object_id +} + +output "opssre_group_object_id" { + value = azuread_group.opssre.object_id +} From f44002eecfc31ecf79200f7c8b8518ee7a798480 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:09:11 +0000 Subject: [PATCH 2/5] Skip unattended E2E for AKS RBAC quickstart Co-authored-by: Rebecca-Calixte <262454636+Rebecca-Calixte@users.noreply.github.com> --- test/e2e/quickstart_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/e2e/quickstart_test.go b/test/e2e/quickstart_test.go index 696b3ec2c..96034e96a 100644 --- a/test/e2e/quickstart_test.go +++ b/test/e2e/quickstart_test.go @@ -25,6 +25,7 @@ var speicalTests = map[string]func(*testing.T){ "quickstart/202-machine-learning-moderately-secure-existing-VNet": test202machineLearningModeratelySecureExistingVnet, "quickstart/101-azure-netapp-files": test101AzureNetappFiles, "quickstart/101-azure-storage-actions-create-storage-task": test101AzureStorageActionsCreateStorageTask, + "quickstart/101-aks-entra-k8s-rbac": test101AKSEntraKubernetesRBAC, } func Test_Quickstarts(t *testing.T) { @@ -236,6 +237,10 @@ func test101AzureStorageActionsCreateStorageTask(t *testing.T) { }, nil) } +func test101AKSEntraKubernetesRBAC(t *testing.T) { + t.Skip("requires a pre-existing AKS cluster and Microsoft Entra user credentials") +} + func removeDuplicates(s []string) []string { m := make(map[string]struct{}) result := []string{} From 45cd3bf6f22a408157b4e6126067641da1d4526f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:15:13 +0000 Subject: [PATCH 3/5] Revert unattended E2E skip Co-authored-by: Rebecca-Calixte <262454636+Rebecca-Calixte@users.noreply.github.com> --- test/e2e/quickstart_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/e2e/quickstart_test.go b/test/e2e/quickstart_test.go index 96034e96a..696b3ec2c 100644 --- a/test/e2e/quickstart_test.go +++ b/test/e2e/quickstart_test.go @@ -25,7 +25,6 @@ var speicalTests = map[string]func(*testing.T){ "quickstart/202-machine-learning-moderately-secure-existing-VNet": test202machineLearningModeratelySecureExistingVnet, "quickstart/101-azure-netapp-files": test101AzureNetappFiles, "quickstart/101-azure-storage-actions-create-storage-task": test101AzureStorageActionsCreateStorageTask, - "quickstart/101-aks-entra-k8s-rbac": test101AKSEntraKubernetesRBAC, } func Test_Quickstarts(t *testing.T) { @@ -237,10 +236,6 @@ func test101AzureStorageActionsCreateStorageTask(t *testing.T) { }, nil) } -func test101AKSEntraKubernetesRBAC(t *testing.T) { - t.Skip("requires a pre-existing AKS cluster and Microsoft Entra user credentials") -} - func removeDuplicates(s []string) []string { m := make(map[string]struct{}) result := []string{} From d587d6554db01dd8716f3b1248b4b8987550c3f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:57:04 +0000 Subject: [PATCH 4/5] Make AKS Entra Kubernetes RBAC quickstart self-contained Co-authored-by: Rebecca-Calixte <262454636+Rebecca-Calixte@users.noreply.github.com> --- quickstart/101-aks-entra-k8s-rbac/README.md | 32 +++--- quickstart/101-aks-entra-k8s-rbac/main.tf | 114 ++++++++++++++------ 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/quickstart/101-aks-entra-k8s-rbac/README.md b/quickstart/101-aks-entra-k8s-rbac/README.md index 4dbcd67a5..38c7d8a2e 100644 --- a/quickstart/101-aks-entra-k8s-rbac/README.md +++ b/quickstart/101-aks-entra-k8s-rbac/README.md @@ -1,19 +1,18 @@ # Use Microsoft Entra ID Groups with Kubernetes RBAC in AKS -This template uses Microsoft Entra ID groups with Kubernetes role-based access control (Kubernetes RBAC) in an existing Azure Kubernetes Service (AKS) cluster. +This template uses Microsoft Entra ID groups with Kubernetes role-based access control (Kubernetes RBAC) in an Azure Kubernetes Service (AKS) cluster that the configuration creates. The example creates two Microsoft Entra groups and scopes each group to a namespace: - The `appdev` group can manage resources in the `dev` namespace. - The `opssre` group can manage resources in the `sre` namespace. -The AKS cluster must already have Microsoft Entra integration and Kubernetes RBAC enabled. Azure RBAC for Kubernetes Authorization must be disabled for this example. +The AKS cluster is created with Microsoft Entra integration and Kubernetes RBAC enabled, and with Azure RBAC for Kubernetes Authorization disabled, which this example requires. ## Prerequisites - An Azure subscription -- An existing AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled -- Azure RBAC for Kubernetes Authorization disabled on the cluster +- Permission to create resource groups and AKS clusters in the subscription - Terraform `>= 1.6.0` installed - Azure CLI and `kubectl` installed - Permission to create Microsoft Entra users and groups @@ -29,23 +28,21 @@ az account set --subscription ## Terraform providers and variables -This sample uses the AzureRM provider to reference the existing AKS cluster and assign Azure permissions, the AzureAD provider to create Microsoft Entra users and groups, and the Kubernetes provider to create namespaces, Roles, and RoleBindings. +This sample uses the AzureRM provider to create the resource group and the AKS cluster and to assign Azure permissions, the AzureAD provider to create Microsoft Entra users and groups, and the Kubernetes provider to create namespaces, Roles, and RoleBindings. -The Terraform variables require values for the existing cluster and the example users: +All Terraform variables have defaults, so the configuration runs without any input. Override them if you need a different location, resource group name prefix, or node count: ```hcl -resource_group_name = "" -aks_cluster_name = "" -appdev_user_principal_name = "" -opssre_user_principal_name = "" -temporary_password = "" +resource_group_location = "eastus" +resource_group_name_prefix = "rg" +node_count = 1 ``` -`temporary_password` is a sensitive Terraform variable. Replace `` with a strong temporary password supplied through a secure `terraform.tfvars` file or another protected input method. Do not commit that value. +The Microsoft Entra group names, user principal names, and the temporary password for the example users are generated, so the configuration can be applied repeatedly in the same tenant without name collisions. The generated password is never written to an output. ## Example -Create a `terraform.tfvars` file with values for the existing cluster and test users, then initialize, format, and validate the configuration: +Initialize, format, and validate the configuration: ```console terraform init @@ -60,8 +57,9 @@ terraform plan terraform apply ``` -The configuration creates the following Microsoft Entra and Kubernetes resources: +The configuration creates the following Azure, Microsoft Entra, and Kubernetes resources: +- A resource group and an AKS cluster with Microsoft Entra integration and Kubernetes RBAC - `appdev` and `opssre` Microsoft Entra security groups - One example Microsoft Entra user in each group - Cluster User Role assignments for both groups @@ -74,8 +72,8 @@ Get credentials for the AKS cluster: ```console az aks get-credentials \ - --resource-group \ - --name + --resource-group $(terraform output -raw resource_group_name) \ + --name $(terraform output -raw aks_cluster_name) ``` Verify that both namespaces exist: @@ -116,7 +114,7 @@ Creating a pod in the `dev` namespace should return a `Forbidden` error because ## Clean up -Remove the namespaces, RoleBindings, Roles, groups, users, and role assignments created by this configuration: +Remove the resource group, AKS cluster, namespaces, RoleBindings, Roles, groups, users, and role assignments created by this configuration: ```console terraform destroy diff --git a/quickstart/101-aks-entra-k8s-rbac/main.tf b/quickstart/101-aks-entra-k8s-rbac/main.tf index 1236beeae..dac8bc54c 100644 --- a/quickstart/101-aks-entra-k8s-rbac/main.tf +++ b/quickstart/101-aks-entra-k8s-rbac/main.tf @@ -14,6 +14,10 @@ terraform { source = "hashicorp/kubernetes" version = "~> 2.30" } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } } } @@ -23,66 +27,106 @@ provider "azurerm" { provider "azuread" {} -variable "resource_group_name" { +variable "resource_group_location" { type = string - description = "Name of the resource group that contains the existing AKS cluster." + default = "eastus" + description = "Location of the resource group." } -variable "aks_cluster_name" { +variable "resource_group_name_prefix" { type = string - description = "Name of the existing AKS cluster with Microsoft Entra integration and Kubernetes RBAC enabled." + default = "rg" + description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription." } -variable "appdev_user_principal_name" { - type = string - description = "User principal name for the application developer test user." +variable "node_count" { + type = number + default = 1 + description = "Number of nodes in the AKS default node pool." } -variable "opssre_user_principal_name" { - type = string - description = "User principal name for the SRE test user." +resource "random_pet" "rg_name" { + prefix = var.resource_group_name_prefix } -variable "temporary_password" { - type = string - description = "Temporary password used for the example Microsoft Entra users." - sensitive = true +resource "random_string" "suffix" { + length = 8 + numeric = false + special = false + upper = false +} + +resource "random_password" "temporary_password" { + length = 24 + min_lower = 2 + min_upper = 2 + min_numeric = 2 + min_special = 2 +} + +data "azurerm_client_config" "current" {} + +data "azuread_domains" "default" { + only_initial = true +} + +resource "azurerm_resource_group" "rg" { + name = random_pet.rg_name.id + location = var.resource_group_location } -data "azurerm_kubernetes_cluster" "aks" { - name = var.aks_cluster_name - resource_group_name = var.resource_group_name +resource "azurerm_kubernetes_cluster" "aks" { + name = "aks-${random_string.suffix.result}" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + dns_prefix = "aks-${random_string.suffix.result}" + role_based_access_control_enabled = true + + default_node_pool { + name = "default" + node_count = var.node_count + vm_size = "Standard_DS2_v2" + } + + identity { + type = "SystemAssigned" + } + + azure_active_directory_role_based_access_control { + tenant_id = data.azurerm_client_config.current.tenant_id + azure_rbac_enabled = false + } } provider "kubernetes" { - host = data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].host - 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) - cluster_ca_certificate = base64decode(data.azurerm_kubernetes_cluster.aks.kube_admin_config[0].cluster_ca_certificate) + host = azurerm_kubernetes_cluster.aks.kube_admin_config[0].host + client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_certificate) + client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_admin_config[0].client_key) + cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_admin_config[0].cluster_ca_certificate) } resource "azuread_group" "appdev" { - display_name = "appdev" + display_name = "appdev-${random_string.suffix.result}" security_enabled = true } resource "azuread_group" "opssre" { - display_name = "opssre" + display_name = "opssre-${random_string.suffix.result}" security_enabled = true } resource "azuread_user" "aksdev" { - user_principal_name = var.appdev_user_principal_name + user_principal_name = "aksdev-${random_string.suffix.result}@${data.azuread_domains.default.domains[0].domain_name}" display_name = "AKS Dev" - mail_nickname = "aksdev" - password = var.temporary_password + mail_nickname = "aksdev-${random_string.suffix.result}" + password = random_password.temporary_password.result } resource "azuread_user" "akssre" { - user_principal_name = var.opssre_user_principal_name + user_principal_name = "akssre-${random_string.suffix.result}@${data.azuread_domains.default.domains[0].domain_name}" display_name = "AKS SRE" - mail_nickname = "akssre" - password = var.temporary_password + mail_nickname = "akssre-${random_string.suffix.result}" + password = random_password.temporary_password.result } resource "azuread_group_member" "appdev_member" { @@ -96,13 +140,13 @@ resource "azuread_group_member" "opssre_member" { } resource "azurerm_role_assignment" "appdev_cluster_user" { - scope = data.azurerm_kubernetes_cluster.aks.id + scope = azurerm_kubernetes_cluster.aks.id role_definition_name = "Azure Kubernetes Service Cluster User Role" principal_id = azuread_group.appdev.object_id } resource "azurerm_role_assignment" "opssre_cluster_user" { - scope = data.azurerm_kubernetes_cluster.aks.id + scope = azurerm_kubernetes_cluster.aks.id role_definition_name = "Azure Kubernetes Service Cluster User Role" principal_id = azuread_group.opssre.object_id } @@ -202,3 +246,11 @@ output "appdev_group_object_id" { output "opssre_group_object_id" { value = azuread_group.opssre.object_id } + +output "resource_group_name" { + value = azurerm_resource_group.rg.name +} + +output "aks_cluster_name" { + value = azurerm_kubernetes_cluster.aks.name +} From f16c8ee0a3f6c86d5499c7231591d4682ed61a0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:02:57 +0000 Subject: [PATCH 5/5] Clarify generated group and user names in quickstart README Co-authored-by: Rebecca-Calixte <262454636+Rebecca-Calixte@users.noreply.github.com> --- quickstart/101-aks-entra-k8s-rbac/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickstart/101-aks-entra-k8s-rbac/README.md b/quickstart/101-aks-entra-k8s-rbac/README.md index 38c7d8a2e..c6a219d7f 100644 --- a/quickstart/101-aks-entra-k8s-rbac/README.md +++ b/quickstart/101-aks-entra-k8s-rbac/README.md @@ -38,7 +38,7 @@ resource_group_name_prefix = "rg" node_count = 1 ``` -The Microsoft Entra group names, user principal names, and the temporary password for the example users are generated, so the configuration can be applied repeatedly in the same tenant without name collisions. The generated password is never written to an output. +The Microsoft Entra group names, user principal names, and the temporary password for the example users are generated, so the configuration can be applied repeatedly in the same tenant without name collisions. The `appdev` and `opssre` group names and the example user principal names each carry the same generated suffix, and the `appdev_group_object_id` and `opssre_group_object_id` outputs report the object IDs of the two groups. The generated password is never written to an output. ## Example