Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/aks/github-connector/backplane/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions modules/aks/github-connector/backplane/provider.tf
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion modules/aks/github-connector/buildingblock/kubernetes.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion modules/aks/github-connector/buildingblock/main.tf

This file was deleted.

1 change: 0 additions & 1 deletion modules/aks/github-connector/buildingblock/outputs.tf

This file was deleted.

27 changes: 13 additions & 14 deletions modules/aks/github-connector/meshstack_integration.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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(<<EOT
The **GitHub Actions AKS Connector** integrates a GitHub repository with an Azure Kubernetes Service (AKS) namespace. It sets up the necessary service accounts, secrets, and workflows so that GitHub Actions can build, push, and deploy container images to your AKS namespace automatically.
Expand All @@ -101,6 +101,7 @@ This building block is ideal for teams that:

## Prerequisites

- An **Azure AKS Namespace** building block in your workspace. The connector deploys into its namespace and is ordered with it as a parent.
- A `Dockerfile` must be present in the connected GitHub repository, as the workflow will build and deploy this image.

## Resources Created
Expand Down Expand Up @@ -140,7 +141,8 @@ EOT
}

dependency_refs = [
{ uuid = var.github.repo_definition_uuid }
{ uuid = var.github.repo_definition_uuid },
{ uuid = var.aks.namespace_definition_uuid },
]

inputs = {
Expand Down Expand Up @@ -186,10 +188,13 @@ EOT
}
}
"namespace" = {
assignment_type = "PLATFORM_TENANT_ID"
display_name = "AKS Namespace"
is_environment = false
type = "STRING"
argument = jsonencode("${var.aks.namespace_definition_uuid}.namespace_name")
assignment_type = "BUILDING_BLOCK_OUTPUT"
description = "Name of the AKS namespace to deploy into, taken from the parent Azure AKS Namespace building block."
display_name = "AKS Namespace"
is_environment = false
type = "STRING"
updateable_by_consumer = false
}
"github_repo" = {
argument = jsonencode("${var.github.repo_definition_uuid}.repo_name")
Expand Down Expand Up @@ -235,12 +240,6 @@ EOT
}

outputs = {}

permissions = [
"TENANT_LIST",
"TENANT_SAVE",
"TENANT_DELETE",
]
}
}

Expand Down
21 changes: 21 additions & 0 deletions modules/aks/namespace/backplane/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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 (`<name>-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

- 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.
85 changes: 85 additions & 0 deletions modules/aks/namespace/backplane/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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
}

# 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
}
7 changes: 7 additions & 0 deletions modules/aks/namespace/backplane/outputs.tf
Original file line number Diff line number Diff line change
@@ -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
}
}
5 changes: 5 additions & 0 deletions modules/aks/namespace/backplane/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
provider "azurerm" {
features {}
}

provider "azuread" {}
45 changes: 45 additions & 0 deletions modules/aks/namespace/backplane/variables.tf
Original file line number Diff line number Diff line change
@@ -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."
}
14 changes: 14 additions & 0 deletions modules/aks/namespace/backplane/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.11.0"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.65.0, < 5.0.0"
}
azuread = {
source = "hashicorp/azuread"
version = ">= 3.8.0, < 4.0.0"
}
}
}
77 changes: 77 additions & 0 deletions modules/aks/namespace/buildingblock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
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
- 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

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_azuread"></a> [azuread](#requirement\_azuread) | >= 3.8.0, < 4.0.0 |
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | >= 4.64.0, < 5.0.0 |
| <a name="requirement_http"></a> [http](#requirement\_http) | >= 3.4.0, < 4.0.0 |
| <a name="requirement_kubernetes"></a> [kubernetes](#requirement\_kubernetes) | >= 2.35.1, < 3.0.0 |

## Modules

No modules.

## Resources

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

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aks_cluster_name"></a> [aks\_cluster\_name](#input\_aks\_cluster\_name) | Name of the AKS cluster the namespace is created in. | `string` | n/a | yes |
| <a name="input_aks_resource_group_name"></a> [aks\_resource\_group\_name](#input\_aks\_resource\_group\_name) | Resource group of the AKS cluster. | `string` | n/a | yes |
| <a name="input_default_container_limits"></a> [default\_container\_limits](#input\_default\_container\_limits) | LimitRange defaults for containers that declare no requests or limits of their own. | <pre>object({<br/> cpu_request = optional(string, "100m")<br/> memory_request = optional(string, "128Mi")<br/> cpu_limit = optional(string, "500m")<br/> memory_limit = optional(string, "512Mi")<br/> })</pre> | `{}` | no |
| <a name="input_group_name_prefix"></a> [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 |
| <a name="input_namespace_name"></a> [namespace\_name](#input\_namespace\_name) | Name of the Kubernetes namespace to create. | `string` | n/a | yes |
| <a name="input_oidc_token_file_path"></a> [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 |
| <a name="input_resource_quota"></a> [resource\_quota](#input\_resource\_quota) | ResourceQuota applied to the namespace. | <pre>object({<br/> requests_cpu = optional(string, "2")<br/> requests_memory = optional(string, "4Gi")<br/> limits_cpu = optional(string, "4")<br/> limits_memory = optional(string, "8Gi")<br/> pods = optional(string, "50")<br/> })</pre> | `{}` | no |
| <a name="input_role_mapping"></a> [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 |
| <a name="input_user_lookup_attribute"></a> [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 |
| <a name="input_users"></a> [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. | <pre>list(object({<br/> meshIdentifier = string<br/> username = string<br/> firstName = string<br/> lastName = string<br/> email = string<br/> euid = string<br/> roles = list(string)<br/> }))</pre> | `[]` | no |
| <a name="input_workspace_identifier"></a> [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 |
|------|-------------|
| <a name="output_cluster_name"></a> [cluster\_name](#output\_cluster\_name) | Name of the AKS cluster hosting the namespace. |
| <a name="output_cluster_resource_group_name"></a> [cluster\_resource\_group\_name](#output\_cluster\_resource\_group\_name) | Resource group of the AKS cluster hosting the namespace. |
| <a name="output_group_object_ids"></a> [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. |
| <a name="output_namespace_name"></a> [namespace\_name](#output\_namespace\_name) | Name of the created Kubernetes namespace. |
| <a name="output_summary"></a> [summary](#output\_summary) | Markdown summary shown in meshPanel after the run. |
| <a name="output_unresolved_users"></a> [unresolved\_users](#output\_unresolved\_users) | Workspace members that have no matching object in the directory and were therefore not added to any group. |
<!-- END_TF_DOCS -->
Binary file added modules/aks/namespace/buildingblock/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading