diff --git a/modules/azure-mi/.terraform-docs.yml b/modules/azure-mi/.terraform-docs.yml new file mode 100644 index 000000000..3a69365ff --- /dev/null +++ b/modules/azure-mi/.terraform-docs.yml @@ -0,0 +1,48 @@ +formatter: "markdown" + +version: "" + +header-from: docs/header.md +footer-from: docs/footer.md + +recursive: + enabled: false + path: modules + include-main: true + +sections: + hide: [] + show: [] + +content: "" + +output: + file: "README.md" + mode: inject + template: |- + + {{ .Content }} + + +output-values: + enabled: false + from: "" + +sort: + enabled: true + by: name + +settings: + anchor: true + color: true + default: true + description: false + escape: true + hide-empty: false + html: true + indent: 2 + lockfile: true + read-comments: true + required: true + sensitive: true + type: true diff --git a/modules/azure-mi/README.md b/modules/azure-mi/README.md index 8acfa8c59..adb719912 100644 --- a/modules/azure-mi/README.md +++ b/modules/azure-mi/README.md @@ -1,130 +1,165 @@ + +# **Azure User Assigned Managed Identity Terraform Module** + +## Overview + +This module creates an **Azure user-assigned managed identity** (`azurerm_user_assigned_identity`) in an existing resource group. It can attach **Azure RBAC role assignments** to arbitrary scopes, configure **federated identity credentials** for GitHub Actions, Kubernetes workload identity, or generic OIDC issuers, use **tags** from the resource group or from a `tags` map (not both at once), and grant **Key Vault access policies** for the identity on one or more vaults. + +The module does **not** create the resource group, federated issuers, or Key Vaults; it wires the identity to resources you already manage. + +## Key Features + +- **User-assigned identity**: Name, location, and tags. When `tags_from_rg` is **true**, identity tags are **only** those on the resource group (`var.tags` is ignored). When **false**, tags come from `var.tags`. +- **RBAC**: `rbac` entries flatten to `azurerm_role_assignment` resources, but the current implementation keys them by assignment `name` + individual `role`. Reusing the same `name` with the same `role` on different `scope` values is therefore **not currently supported** and will cause a duplicate-key error; use distinct assignment names in that case. +- **Federated credentials**: `federated_credentials` entries share `audience`; each entry has `type` `github`, `kubernetes`, or `other` (validated). The variable marks nested fields optional, but **`main.tf` expects real values per type** or plan/apply can fail: **`github`** — set `organization`, `repository`, and `entity` (subject suffix, e.g. `ref:refs/heads/main`); `issuer` defaults to the GitHub Actions OIDC issuer if unset. **`kubernetes`** — set `issuer`, `namespace`, and `service_account_name`. **`other`** — set `issuer` and `subject`. +- **Key Vault access policies**: Optional `access_policies` to grant the identity permissions on existing vaults by `key_vault_id`. +- **Outputs**: Identity **`id`**, **`name`**, **`client_id`**, and **`principal_id`** for use in AKS, role assignments, or application configuration. + +## Prerequisites + +- Existing **resource group** (`resource_group`) and valid **Azure region** (`location`). +- **azurerm** provider configured for your subscription (`~> 4.16.0`; see `versions.tf`). +- For **federated credentials**, issuers and subjects must match your IdP or cluster configuration. +- For **role assignments**, the deploying principal needs permission to create assignments on each `scope`. + +## Basic Usage + +### Example (identity only, no RBAC or federated credentials) + +```hcl +module "managed_identity" { + source = "git::https://github.com/prefapp/tfm.git//modules/azure-mi?ref=" + + name = "uami-myapp-dev" + resource_group = "my-resource-group" + location = "westeurope" + tags_from_rg = false + tags = { + environment = "dev" + } + + rbac = [] + federated_credentials = [] + access_policies = [] +} +``` + +### Example (RBAC role assignments) + +```hcl +module "managed_identity_with_rbac" { + source = "git::https://github.com/prefapp/tfm.git//modules/azure-mi?ref=" + + name = "uami-myapp-reader" + resource_group = "my-resource-group" + location = "westeurope" + tags_from_rg = false + tags = {} + + rbac = [ + { + name = "rg-reader" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group" + roles = ["Reader"] + }, + ] + + federated_credentials = [] + access_policies = [] +} +``` + +Replace `scope` and role names with values valid in your tenant. See the [comprehensive example](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples/comprehensive) for federated credential patterns. + +## File structure + +``` +. +├── CHANGELOG.md +├── main.tf +├── outputs.tf +├── variables.tf +├── versions.tf +├── docs +│ ├── footer.md +│ └── header.md +├── _examples +│ ├── basic +│ └── comprehensive +├── README.md +└── .terraform-docs.yml +``` + ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.7.5 | +| [terraform](#requirement\_terraform) | >= 1.7.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.16.0 | ## Providers | Name | Version | |------|---------| -| [azurerm](#provider\_azurerm) | = 4.16.0 | +| [azurerm](#provider\_azurerm) | 4.16.0 | + +## Modules + +No modules. ## Resources | Name | Type | |------|------| -| [azurerm_resource_group](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group.html) | data resource (only when `tags from resource group` is enabled) | -| [azurerm_user_assigned_identity](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) | source | -| [azurerm_role_assignment](https://registry.terraform.io/providers/hashicorp/azurerm/2.62.1/docs/resources/role_assignment) | source | -| [azurerm_federated_identity_credential](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | source (only when `federated_credentials` is not empty) | +| [azurerm_federated_identity_credential.that](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | resource | +| [azurerm_key_vault_access_policy.access_policy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy) | resource | +| [azurerm_role_assignment.that](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_user_assigned_identity.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_resource_group.resource_group](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| name | The name of the Managed Identity | `string` | n/a | yes | -| resource_group | The name of the resource group in which to create the Managed Identity | `string` | n/a | yes | -| location | The location in which to create the Managed Identity | `string` | n/a | yes | -| tags | A mapping of tags to assign to the resource | `map(string)` | n/a | no | -| tags_from_rg | If true, the tags from the resource group will be inherited exclusively | `bool` | `false` | no | -| rbac | A list of role-based access control (RBAC) policies to apply to the Managed Identity |
list(object({
name: string (required)
scope: string (required)
roles: list(string) (required)
}))
| n/a | yes | -| federated_credentials | A list of federated credentials to assign to the Managed Identity, posible types are:

**kubernetes**: `issuer`, `namespace` and `service_account_name`
- `issuer`: The cluster issuer
- `namespace`: The namespace of the service account
- `service_account_name`: The name of the service account

**github**: `issuer`, `organization`, `repository` and `entity`
- `issuer`: The github issuer
- `organization`: The github organization
- `repository`: The github repository
- `entity`: The github entity \|Optional value, if not provided, the entity will be the repository. For other scenarios, the entity should be provided:
  - environment: `environment:foo_enviroment`
  - tags: `ref:refs/tags/foo_tag`
  - branch: `ref:refs/heads/foo_branch`
  - commit: `ref:refs/commits/foo_commit`

**other**: `issuer` and `subject`
- `issuer`: The issuer
- `subject`: The subject |
list(object({
name: string (required)
type: string (required) - **kubernetes** \|\| **github** \|\| **other**
issuer: string (required only when type is **kubernetes** or **other**, when type is **github** is optional because the default is `https://token.actions.githubusercontent.com`)
namespace: string (required only when the type is **kubernetes**)
service_account_name: string (required only when the type is **kubernetes**)
organization: string (required only when the type is **github**)
repository: string (required only when the type is **github**)
entity: string (required only when the type is **github** and the entity is not the repository)
subject: string (required only when the type is **other**)
}))
| `[]` | no | -| audience | The audience of the federated identity credential | `list(string)` | `["api://AzureADTokenExchange"]` | no | +| [access\_policies](#input\_access\_policies) | List of access policies for the Key Vault |
list(object({
key_vault_id = string
key_permissions = optional(list(string), [])
secret_permissions = optional(list(string), [])
certificate_permissions = optional(list(string), [])
storage_permissions = optional(list(string), [])
}))
| `[]` | no | +| [audience](#input\_audience) | The audience for the federated identity credential. | `list(string)` |
[
"api://AzureADTokenExchange"
]
| no | +| [federated\_credentials](#input\_federated\_credentials) | A list of objects containing the federated credentials to assign to the User Assigned Identity. |
list(object({
name = string
type = string
issuer = optional(string)
namespace = optional(string)
service_account_name = optional(string)
organization = optional(string)
repository = optional(string)
entity = optional(string)
subject = optional(string)
}))
| `[]` | no | +| [location](#input\_location) | The location/region where the User Assigned Identity should be created. | `string` | n/a | yes | +| [name](#input\_name) | The name of the User Assigned Identity. | `string` | n/a | yes | +| [rbac](#input\_rbac) | A list of objects containing the RBAC roles to assign to the User Assigned Identity. |
list(object({
name = string
scope = string
roles = list(string)
}))
| n/a | yes | +| [resource\_group](#input\_resource\_group) | The name of the Resource Group. | `string` | n/a | yes | +| [tags](#input\_tags) | A mapping of tags to assign to the User Assigned Identity. | `map(string)` |
{
"name": "value"
}
| no | +| [tags\_from\_rg](#input\_tags\_from\_rg) | If true, the User Assigned Identity will inherit the tags from the Resource Group. | `bool` | `false` | no | ## Outputs | Name | Description | |------|-------------| -| [user_assigned_identity_id](#output\_user\_assigned\_identity\_id) | The ID of the User Assigned Identity. | +| [client\_id](#output\_client\_id) | The client ID (application ID) of the user-assigned managed identity. | +| [id](#output\_id) | The Azure resource ID of the user-assigned managed identity. | +| [name](#output\_name) | The name of the user-assigned managed identity. | +| [principal\_id](#output\_principal\_id) | The service principal object ID of the user-assigned managed identity (use for RBAC assignments referencing this identity). | -## Example +## Examples -### HCL -```hcl -name = "xxx" -resource_group = "xxx" -location = "xxx" -tags = { - foo = "bar" -} -# tags_from_rg = true # Will inherit the tags from the resource group exclusively -rbac = [ - { - name = "foo" - scope = "scope-foo" - roles = [ - "xxx" - ] - }, - { - name = "bar" - scope = "scope-bar" - roles = [ - "xxx", - "yyy", - "zzz" - ] - } -] - -federated_credentials = [ - { - name = "foo-github" - type = "github" - organization = "foo" - repository = "bar" - entity = "baz" - }, - { - name = "foo-kubernetes" - type = "kubernetes" - issuer = "https://kubernetes.default.svc.cluster.local" - namespace = "foo" - service_account_name = "bar" - }, - { - name = "other" - type = "other" - issuer = "https://example.com" - subject = "other" - } -] +For detailed examples, refer to the [module examples](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples): -``` +- [basic](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples/basic) — Minimal user-assigned identity with empty `rbac`, `federated_credentials`, and `access_policies` (see folder README). +- [comprehensive](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples/comprehensive) — Reference HCL and YAML for RBAC, federated GitHub/Kubernetes/OIDC credentials, and optional Key Vault access policies (`values.reference.yaml`; see folder README). -### Yaml -```yaml -name: xxx -resource_group_name: xxx -location: xxx -tags: -foo: bar -# tags_from_rg: true # Will inherit the tags from the resource group exclusively -rbac: # 1-n - - name: foo - scope: scope-foo - roles: - - xxx - - name: bar - scope: scope-bar - roles: # 1-n - - xxx - - yyy - - zzz -federated_credentials: # {} | 0-20 - - name: foo-github - type: github - organization: foo - repository: bar - entity: baz - - name: foo-kubernetes - type: kubernetes - issuer: https://kubernetes.default.svc.cluster.local - namespace: foo - service_account_name: bar - - name: other - type: other - issuer: https://example.com - subject: other -``` +## Remote resources + +Provider constraints for your workspace appear in the **Requirements** and **Providers** tables above. Resource documentation links below use the Terraform Registry **`latest`** path (see `versions.tf` for the module constraint, currently `~> 4.16.0`). Regenerate this README with `terraform-docs .` as described in [README.md generation](https://github.com/prefapp/tfm/blob/main/CONTRIBUTING.md#5-readmemd-generation). + +- **Managed identities**: [https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/) +- **azurerm\_user\_assigned\_identity**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) +- **azurerm\_role\_assignment**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) +- **azurerm\_federated\_identity\_credential**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) +- **azurerm\_key\_vault\_access\_policy**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy) +- **Terraform AzureRM provider**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest](https://registry.terraform.io/providers/hashicorp/azurerm/latest) + +## Support + +For issues, questions, or contributions related to this module, please visit the repository's issue tracker: [https://github.com/prefapp/tfm/issues](https://github.com/prefapp/tfm/issues) + \ No newline at end of file diff --git a/modules/azure-mi/_examples/basic/README.md b/modules/azure-mi/_examples/basic/README.md new file mode 100644 index 000000000..b6bed3121 --- /dev/null +++ b/modules/azure-mi/_examples/basic/README.md @@ -0,0 +1,5 @@ +# Basic example — `azure-mi` + +Creates one **user-assigned managed identity** with no RBAC assignments, no federated credentials, and no Key Vault access policies. + +Set `resource_group` to the name of an existing resource group in your subscription, and set `location` and `name` to valid values. Configure the **`azurerm`** provider before `terraform apply`. diff --git a/modules/azure-mi/_examples/basic/main.tf b/modules/azure-mi/_examples/basic/main.tf new file mode 100644 index 000000000..ecbf04f0c --- /dev/null +++ b/modules/azure-mi/_examples/basic/main.tf @@ -0,0 +1,38 @@ +# Replace resource_group, location, and name with values valid in your subscription. + +terraform { + required_version = ">= 1.7.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.16.0" + } + } +} + +provider "azurerm" { + features {} +} + +module "managed_identity" { + source = "../.." + + name = "uami-basicex01" + resource_group = "example-rg" + location = "westeurope" + + tags_from_rg = false + tags = { + example = "basic" + } + + rbac = [] + federated_credentials = [] + access_policies = [] +} + +output "principal_id" { + description = "Object ID of the managed identity (common input for RBAC elsewhere)." + value = module.managed_identity.principal_id +} diff --git a/modules/azure-mi/_examples/comprehensive/README.md b/modules/azure-mi/_examples/comprehensive/README.md new file mode 100644 index 000000000..edd0af62e --- /dev/null +++ b/modules/azure-mi/_examples/comprehensive/README.md @@ -0,0 +1,13 @@ +# Comprehensive reference — `azure-mi` + +- [`module.reference.hcl`](./module.reference.hcl) — Illustrative module block with **RBAC**, **federated credentials** (GitHub, Kubernetes, OIDC), and optional **Key Vault access policies**. +- [`values.reference.yaml`](./values.reference.yaml) — Illustrative values for most module inputs (same key names as the module variables). + +It is **not** consumed by Terraform automatically. Typical uses: + +- Copy fragments into `.tfvars` or HCL `module` arguments. +- Or load with `yamldecode(file(...))` in a root module and **map each field** to the `azure-mi` module arguments (Terraform does not support splatting a map directly into a `module` block). + +Replace subscription IDs, scopes, vault IDs, and GitHub org/repo with real values before apply. + +**Convention:** keep large YAML samples in `_examples/**` instead of embedding them in the terraform-docs `README.md` body. diff --git a/modules/azure-mi/_examples/comprehensive/module.reference.hcl b/modules/azure-mi/_examples/comprehensive/module.reference.hcl new file mode 100644 index 000000000..d1a08a427 --- /dev/null +++ b/modules/azure-mi/_examples/comprehensive/module.reference.hcl @@ -0,0 +1,55 @@ +# Reference-only: replace scopes, vault IDs, and GitHub coordinates with real values. + +module "managed_identity" { + source = "../.." + + name = "uami-myapp-ref" + resource_group = "my-resource-group" + location = "westeurope" + + tags_from_rg = true + tags = {} + + rbac = [ + { + name = "rg-reader" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group" + roles = ["Reader"] + }, + ] + + audience = ["api://AzureADTokenExchange"] + + federated_credentials = [ + { + name = "github-main" + type = "github" + organization = "my-org" + repository = "my-repo" + entity = "ref:refs/heads/main" + }, + { + name = "k8s-workload" + type = "kubernetes" + issuer = "https://oidc.prod1.azure.com/00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000000/v2.0" + namespace = "default" + service_account_name = "my-sa" + }, + { + name = "custom-oidc" + type = "other" + issuer = "https://example.com" + subject = "my-subject" + }, + ] + + access_policies = [ + { + key_vault_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/my-vault" + key_permissions = ["Get", "List"] + secret_permissions = ["Get", "List"] + certificate_permissions = [] + storage_permissions = [] + }, + ] +} diff --git a/modules/azure-mi/_examples/comprehensive/values.reference.yaml b/modules/azure-mi/_examples/comprehensive/values.reference.yaml new file mode 100644 index 000000000..f59171af3 --- /dev/null +++ b/modules/azure-mi/_examples/comprehensive/values.reference.yaml @@ -0,0 +1,38 @@ +# Reference YAML aligned with module input names (use `yamldecode(file(...))` and map each field explicitly to module arguments). +# Replace subscription IDs, scopes, vault IDs, and GitHub org/repo with real values. + +name: "uami-myapp-ref" +resource_group: "my-resource-group" +location: "westeurope" +tags_from_rg: true +tags: {} +audience: + - "api://AzureADTokenExchange" +rbac: + - name: "rg-reader" + scope: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group" + roles: + - "Reader" +federated_credentials: + - name: "github-main" + type: "github" + organization: "my-org" + repository: "my-repo" + entity: "ref:refs/heads/main" + - name: "k8s-workload" + type: "kubernetes" + issuer: "https://oidc.prod1.azure.com/00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000000/v2.0" + namespace: "default" + service_account_name: "my-sa" + - name: "custom-oidc" + type: "other" + issuer: "https://example.com" + subject: "my-subject" +# `access_policies` only works for Key Vaults using the legacy access-policy permission model. +# Do not use this block with vaults that have `enable_rbac_authorization = true`, or provider errors will occur. +access_policies: + - key_vault_id: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/my-vault" + key_permissions: ["Get", "List"] + secret_permissions: ["Get", "List"] + certificate_permissions: [] + storage_permissions: [] diff --git a/modules/azure-mi/docs/footer.md b/modules/azure-mi/docs/footer.md new file mode 100644 index 000000000..b82800650 --- /dev/null +++ b/modules/azure-mi/docs/footer.md @@ -0,0 +1,21 @@ +## Examples + +For detailed examples, refer to the [module examples](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples): + +- [basic](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples/basic) — Minimal user-assigned identity with empty `rbac`, `federated_credentials`, and `access_policies` (see folder README). +- [comprehensive](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples/comprehensive) — Reference HCL and YAML for RBAC, federated GitHub/Kubernetes/OIDC credentials, and optional Key Vault access policies (`values.reference.yaml`; see folder README). + +## Remote resources + +Provider constraints for your workspace appear in the **Requirements** and **Providers** tables above. Resource documentation links below use the Terraform Registry **`latest`** path (see `versions.tf` for the module constraint, currently `~> 4.16.0`). Regenerate this README with `terraform-docs .` as described in [README.md generation](https://github.com/prefapp/tfm/blob/main/CONTRIBUTING.md#5-readmemd-generation). + +- **Managed identities**: [https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/) +- **azurerm_user_assigned_identity**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) +- **azurerm_role_assignment**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) +- **azurerm_federated_identity_credential**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) +- **azurerm_key_vault_access_policy**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy) +- **Terraform AzureRM provider**: [https://registry.terraform.io/providers/hashicorp/azurerm/latest](https://registry.terraform.io/providers/hashicorp/azurerm/latest) + +## Support + +For issues, questions, or contributions related to this module, please visit the repository's issue tracker: [https://github.com/prefapp/tfm/issues](https://github.com/prefapp/tfm/issues) diff --git a/modules/azure-mi/docs/header.md b/modules/azure-mi/docs/header.md new file mode 100644 index 000000000..591a2bf30 --- /dev/null +++ b/modules/azure-mi/docs/header.md @@ -0,0 +1,90 @@ +# **Azure User Assigned Managed Identity Terraform Module** + +## Overview + +This module creates an **Azure user-assigned managed identity** (`azurerm_user_assigned_identity`) in an existing resource group. It can attach **Azure RBAC role assignments** to arbitrary scopes, configure **federated identity credentials** for GitHub Actions, Kubernetes workload identity, or generic OIDC issuers, use **tags** from the resource group or from a `tags` map (not both at once), and grant **Key Vault access policies** for the identity on one or more vaults. + +The module does **not** create the resource group, federated issuers, or Key Vaults; it wires the identity to resources you already manage. + +## Key Features + +- **User-assigned identity**: Name, location, and tags. When `tags_from_rg` is **true**, identity tags are **only** those on the resource group (`var.tags` is ignored). When **false**, tags come from `var.tags`. +- **RBAC**: `rbac` entries flatten to `azurerm_role_assignment` resources, but the current implementation keys them by assignment `name` + individual `role`. Reusing the same `name` with the same `role` on different `scope` values is therefore **not currently supported** and will cause a duplicate-key error; use distinct assignment names in that case. +- **Federated credentials**: `federated_credentials` entries share `audience`; each entry has `type` `github`, `kubernetes`, or `other` (validated). The variable marks nested fields optional, but **`main.tf` expects real values per type** or plan/apply can fail: **`github`** — set `organization`, `repository`, and `entity` (subject suffix, e.g. `ref:refs/heads/main`); `issuer` defaults to the GitHub Actions OIDC issuer if unset. **`kubernetes`** — set `issuer`, `namespace`, and `service_account_name`. **`other`** — set `issuer` and `subject`. +- **Key Vault access policies**: Optional `access_policies` to grant the identity permissions on existing vaults by `key_vault_id`. +- **Outputs**: Identity **`id`**, **`name`**, **`client_id`**, and **`principal_id`** for use in AKS, role assignments, or application configuration. + +## Prerequisites + +- Existing **resource group** (`resource_group`) and valid **Azure region** (`location`). +- **azurerm** provider configured for your subscription (`~> 4.16.0`; see `versions.tf`). +- For **federated credentials**, issuers and subjects must match your IdP or cluster configuration. +- For **role assignments**, the deploying principal needs permission to create assignments on each `scope`. + +## Basic Usage + +### Example (identity only, no RBAC or federated credentials) + +```hcl +module "managed_identity" { + source = "git::https://github.com/prefapp/tfm.git//modules/azure-mi?ref=" + + name = "uami-myapp-dev" + resource_group = "my-resource-group" + location = "westeurope" + tags_from_rg = false + tags = { + environment = "dev" + } + + rbac = [] + federated_credentials = [] + access_policies = [] +} +``` + +### Example (RBAC role assignments) + +```hcl +module "managed_identity_with_rbac" { + source = "git::https://github.com/prefapp/tfm.git//modules/azure-mi?ref=" + + name = "uami-myapp-reader" + resource_group = "my-resource-group" + location = "westeurope" + tags_from_rg = false + tags = {} + + rbac = [ + { + name = "rg-reader" + scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group" + roles = ["Reader"] + }, + ] + + federated_credentials = [] + access_policies = [] +} +``` + +Replace `scope` and role names with values valid in your tenant. See the [comprehensive example](https://github.com/prefapp/tfm/tree/main/modules/azure-mi/_examples/comprehensive) for federated credential patterns. + +## File structure + +``` +. +├── CHANGELOG.md +├── main.tf +├── outputs.tf +├── variables.tf +├── versions.tf +├── docs +│ ├── footer.md +│ └── header.md +├── _examples +│ ├── basic +│ └── comprehensive +├── README.md +└── .terraform-docs.yml +``` diff --git a/modules/azure-mi/main.tf b/modules/azure-mi/main.tf index 4b698f63b..229e63330 100644 --- a/modules/azure-mi/main.tf +++ b/modules/azure-mi/main.tf @@ -51,12 +51,12 @@ resource "azurerm_federated_identity_credential" "that" { ## https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy resource "azurerm_key_vault_access_policy" "access_policy" { - for_each = { for policy in var.access_policies : policy.key_vault_id => policy } - key_vault_id = each.value.key_vault_id - tenant_id = data.azurerm_client_config.current.tenant_id - object_id = azurerm_user_assigned_identity.this.principal_id - key_permissions = each.value.key_permissions - secret_permissions = each.value.secret_permissions + for_each = { for policy in var.access_policies : policy.key_vault_id => policy } + key_vault_id = each.value.key_vault_id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_user_assigned_identity.this.principal_id + key_permissions = each.value.key_permissions + secret_permissions = each.value.secret_permissions certificate_permissions = each.value.certificate_permissions - storage_permissions = each.value.storage_permissions + storage_permissions = each.value.storage_permissions } diff --git a/modules/azure-mi/outputs.tf b/modules/azure-mi/outputs.tf index 444f0df2d..25c7cbe7e 100644 --- a/modules/azure-mi/outputs.tf +++ b/modules/azure-mi/outputs.tf @@ -1,5 +1,19 @@ -## OUTPUTS SECTION -# User Assigned Identity output "id" { - value = azurerm_user_assigned_identity.this.id + description = "The Azure resource ID of the user-assigned managed identity." + value = azurerm_user_assigned_identity.this.id +} + +output "name" { + description = "The name of the user-assigned managed identity." + value = azurerm_user_assigned_identity.this.name +} + +output "client_id" { + description = "The client ID (application ID) of the user-assigned managed identity." + value = azurerm_user_assigned_identity.this.client_id +} + +output "principal_id" { + description = "The service principal object ID of the user-assigned managed identity (use for RBAC assignments referencing this identity)." + value = azurerm_user_assigned_identity.this.principal_id } diff --git a/modules/azure-mi/variables.tf b/modules/azure-mi/variables.tf index ee4977b92..bf85cf3c1 100644 --- a/modules/azure-mi/variables.tf +++ b/modules/azure-mi/variables.tf @@ -39,11 +39,11 @@ variable "rbac" { variable "access_policies" { description = "List of access policies for the Key Vault" type = list(object({ - key_vault_id = string - key_permissions = optional(list(string), []) - secret_permissions = optional(list(string), []) + key_vault_id = string + key_permissions = optional(list(string), []) + secret_permissions = optional(list(string), []) certificate_permissions = optional(list(string), []) - storage_permissions = optional(list(string), []) + storage_permissions = optional(list(string), []) })) default = [] }