Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure PIM Active Role Assignment Terraform Module

Provisions a Microsoft Entra Privileged Identity Management (PIM) active role assignment — standing (or scheduled) privileged access at a supplied scope — using hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources

🧩 Overview

  • Creates a single azurerm_pim_active_role_assignment — the keystone this.
  • Grants a principal (user, group, or service principal) an active role at a subscription, resource group, resource, or management-group scope.
  • Optionally time-bounds the assignment with a schedule (a start time plus an expiration by days, hours, or an explicit end time).
  • Optionally records a justification and a change ticket (number + system) for auditability.
  • Emits the composite resource id and the platform-resolved principal_type.

💡 Why it matters: An active PIM assignment gives a principal the role now — standing access, or access bounded to a scheduled window — while capturing the justification and change-ticket metadata that a regulated audit trail expects. It is the standing-access counterpart to the eligible (just-in-time) assignment.

❤️ Support this project

If this module saves you time, please consider supporting its continued development:

🗺️ Where this fits in the family

flowchart LR
  scope["scope: subscription / resource group / resource / management group"]
  rd["terraform-azurerm-role-definition"]
  uai["terraform-azurerm-user-assigned-identity"]
  eligible["terraform-azurerm-pim-eligible-role-assignment"]
  me["terraform-azurerm-pim-active-role-assignment"]
  v["azurerm_pim_active_role_assignment"]
  scope -->|"scope"| me
  rd -->|"role_definition_id"| me
  uai -->|"principal_id"| me
  me -->|"creates"| v
  me -.->|"just-in-time counterpart"| eligible
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
  classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
  class me me;
  class v target;
  class scope,rd,uai,eligible ext;
Loading

This module consumes a scope, a role_definition_id, and a principal_id from upstream modules (or built-in role IDs and caller-supplied principals), and it creates a single active assignment. Its just-in-time counterpart, terraform-azurerm-pim-eligible-role-assignment, grants a principal the eligibility to activate a role on demand rather than the standing role itself.

🧬 What this module builds

flowchart LR
  in_id["scope / role_definition_id / principal_id"]
  in_sched["schedule (start + expiration) / justification"]
  in_tkt["ticket (number / system)"]
  res["azurerm_pim_active_role_assignment.this"]
  out["id / principal_type"]
  in_id -->|"input"| res
  in_sched -->|"input"| res
  in_tkt -->|"input"| res
  res -->|"output"| out
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  class res me;
Loading

Resource inventory

Resource Cardinality Role
azurerm_pim_active_role_assignment.this single (keystone) The active PIM role assignment at the supplied scope.

The optional schedule, ticket, and timeouts blocks are rendered with dynamic blocks so an empty call produces a plain, permanent active assignment.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module — the caller configures provider "azurerm", including the mandatory features {} block, plus auth.

Schema notes that bite

  • Every input is force-new. scope, role_definition_id, principal_id, justification, schedule, and ticket all replace the assignment on change — there is no in-place update.
  • The lifecycle is create / read / delete only. Because nothing updates in place, the timeouts object exposes create, read, and delete only — no update.
  • schedule.expiration is exclusive. Supply at most one of duration_days, duration_hours, or end_date_time; the module rejects two or more at parse time.
  • The id is a Terraform composite, not a raw ARM ID: {scope}|{roleDefinitionId}|{principalId}.
  • PIM prerequisites. The tenant must be PIM-enabled with Microsoft Entra ID P2 (or an equivalent license); without it the request is rejected by the platform.

🔑 Required Azure RBAC Roles / Permissions

Least-privilege at the target scope:

  • Microsoft.Authorization/roleAssignmentScheduleRequests/write and /read.
  • Assigning a role requires User Access Administrator or Owner at (or above) the target scope.
  • A PIM-enabled tenant (Microsoft Entra ID P2).

Azure Prerequisites

  • A Microsoft Entra ID P2 (or equivalent) tenant with PIM enabled.
  • The role definition ID (a custom azurerm_role_definition output or a built-in role ID) and the principal object ID.
  • The Microsoft.Authorization resource provider available at the target scope.

📁 Module Structure

terraform-azurerm-pim-active-role-assignment/
├── providers.tf   # terraform{} block: required_version + pinned azurerm (no provider block)
├── variables.tf   # deeply-typed inputs: scope/role/principal, schedule, ticket, timeouts
├── main.tf        # keystone azurerm_pim_active_role_assignment.this with dynamic blocks
├── outputs.tf     # id (composite) first, then principal_type
├── README.md      # this document
├── SCOPE.md       # the cross-module contract
├── LICENSE        # MIT
└── .gitignore     # canonical library ignore set

⚙️ Quick Start

The caller configures provider "azurerm" (including features {}) and authentication; this module never does.

provider "azurerm" {
  features {}
}

module "reader_active" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
  principal_id       = "11111111-1111-1111-1111-111111111111"
}

ℹ️ Omitting schedule creates a permanent active assignment. Add a schedule to bound it in time.

🔌 Cross-Module Contract

Consumes

Input Type Source
scope string any resource ID / terraform-azurerm-resource-group / terraform-azurerm-management-group
role_definition_id string terraform-azurerm-role-definition (or a built-in role ID)
principal_id string terraform-azurerm-user-assigned-identity (principal_id) or a caller-supplied principal

Emits

Output Description Consumed by
id Composite resource ID {scope}|{roleDefinitionId}|{principalId} references
principal_type Platform-resolved principal type (User / Group / ServicePrincipal) auditing

📚 Example Library

1 · Permanent active assignment (no schedule)

The empty-schedule call: a standing role that never expires.

module "perm" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
  principal_id       = "11111111-1111-1111-1111-111111111111"
}

🔒 A permanent active assignment has no expiry — reserve it for identities that genuinely need standing access.

2 · Time-bounded with duration_days

Grant the role for a fixed number of days from the start time.

module "thirty_days" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
  principal_id       = "11111111-1111-1111-1111-111111111111"

  schedule = {
    start_date_time = "2027-01-01T00:00:00Z"
    expiration      = { duration_days = 30 }
  }
}

💡 duration_days is one of three mutually exclusive expiration forms — see example 8.

3 · Time-bounded with duration_hours

A short-lived window measured in hours.

module "eight_hours" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
  principal_id       = "11111111-1111-1111-1111-111111111111"

  schedule = {
    start_date_time = "2027-03-15T09:00:00Z"
    expiration      = { duration_hours = 8 }
  }
}
4 · Time-bounded with an explicit end_date_time

Pin the assignment to an absolute end instant.

module "until_quarter_end" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
  principal_id       = "11111111-1111-1111-1111-111111111111"

  schedule = {
    start_date_time = "2027-01-01T00:00:00Z"
    expiration      = { end_date_time = "2027-03-31T23:59:59Z" }
  }
}
5 · Scheduled future start

Provision now, but let the role become active only at a future start_date_time.

module "future_start" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
  principal_id       = "11111111-1111-1111-1111-111111111111"

  schedule = {
    start_date_time = "2027-06-01T00:00:00Z"
    expiration      = { duration_days = 90 }
  }
}

ℹ️ A schedule with a start_date_time but no expiration produces an open-ended assignment starting at that time.

6 · With a justification

Record why the access was granted.

module "justified" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
  principal_id       = "11111111-1111-1111-1111-111111111111"
  justification      = "Standing Reader for the platform monitoring service principal."
}
7 · With a change ticket

Attach change-management metadata to the request.

module "with_ticket" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
  principal_id       = "11111111-1111-1111-1111-111111111111"
  justification      = "Time-boxed Contributor for a planned migration."

  ticket = {
    number = "CHG0012345"
    system = "ServiceNow"
  }

  schedule = {
    start_date_time = "2027-02-01T00:00:00Z"
    expiration      = { duration_days = 14 }
  }
}
8 · Expiration exclusivity (validated at parse time)

schedule.expiration accepts at most one of duration_days, duration_hours, or end_date_time. Setting two fails before any API call.

# ❌ Rejected at plan time:
#   schedule.expiration must set at most one of: duration_days, duration_hours, end_date_time.
schedule = {
  start_date_time = "2027-01-01T00:00:00Z"
  expiration = {
    duration_days = 30
    end_date_time = "2027-03-31T23:59:59Z" # two forms — invalid
  }
}

⚠️ Choose exactly one expiration form. The type system catches the mistake at terraform validate, not in Azure.

9 · Resource-group scope

Narrow the grant to a single resource group.

module "rg_scoped" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe" # Storage Blob Data Contributor
  principal_id       = "11111111-1111-1111-1111-111111111111"
}
10 · Management-group scope

Assign broadly across a management group. Prefer a tight expiration at this reach.

module "mg_scoped" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/providers/Microsoft.Management/managementGroups/mg-platform"
  role_definition_id = "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
  principal_id       = "22222222-2222-2222-2222-222222222222"

  schedule = {
    start_date_time = "2027-01-01T00:00:00Z"
    expiration      = { duration_days = 7 }
  }
}

🔒 Management-group scope reaches every child subscription. Bound it with a short expiration and record a ticket.

11 · Custom timeouts (create / read / delete only)

There is no update timeout because the resource never updates in place.

module "with_timeouts" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
  principal_id       = "11111111-1111-1111-1111-111111111111"

  timeouts = {
    create = "30m"
    read   = "5m"
    delete = "30m"
  }
}
12 · for_each over multiple principals

Assign the same role to a keyed map of principals — one module instance each, so removing one never re-indexes the rest.

locals {
  operators = {
    alice = "11111111-1111-1111-1111-111111111111"
    bob   = "22222222-2222-2222-2222-222222222222"
    carol = "33333333-3333-3333-3333-333333333333"
  }
}

module "operators_active" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"
  for_each = local.operators

  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-ops"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
  principal_id       = each.value
  justification      = "Standing operator access for ${each.key}."
}
13 · Active vs. eligible — choosing the sibling

This module grants the role now. Its sibling, terraform-azurerm-pim-eligible-role-assignment, grants only the eligibility to activate the role just-in-time.

# Standing access (this module):
module "active" {
  source             = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"
  scope              = "/subscriptions/00000000-0000-0000-0000-000000000000"
  role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
  principal_id       = "11111111-1111-1111-1111-111111111111"
}

# Just-in-time eligibility (the sibling module):
# module "eligible" {
#   source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
#   ...
# }

💡 Prefer eligible (JIT) for interactive human operators; reserve active for automation identities and scheduled windows.

14 · 🏗️ End-to-end composition

Wire a resource group, a custom role definition, and a user-assigned identity into a scheduled active assignment.

provider "azurerm" {
  features {}
}

module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-platform"
  location = "eastus2"
}

module "role" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-definition.git?ref=v1.0.0"
  name   = "Platform Data Operator"
  scope  = module.rg.id
  # ... permissions ...
}

module "identity" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
  name                = "id-platform-ops"
  resource_group_name = module.rg.name
  location            = "eastus2"
}

module "active_assignment" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-active-role-assignment.git?ref=v1.0.0"

  scope              = module.rg.id
  role_definition_id = module.role.role_definition_resource_id
  principal_id       = module.identity.principal_id
  justification      = "Standing data-operator access for the platform managed identity."

  ticket = {
    number = "CHG0044556"
    system = "ServiceNow"
  }

  schedule = {
    start_date_time = "2027-01-01T00:00:00Z"
    expiration      = { duration_days = 90 }
  }
}

ℹ️ The identity's principal_id, the role's definition ID, and the resource group's id flow straight into this module — no hand-copied GUIDs.

📥 Inputs

Required

Name Type Description
scope string Target scope resource ID (subscription / resource group / resource / management group). Force-new.
role_definition_id string Role definition ID being assigned. Force-new.
principal_id string Object ID of the principal receiving the role. Force-new.

Optional

Name Type Default Description
justification string null Justification recorded with the assignment. Force-new.
schedule object null Activation schedule; null = permanent. Force-new.
ticket object null Change-ticket metadata. Force-new.
timeouts object null Per-operation timeouts (create / read / delete only).
Full nested object schemas
variable "schedule" {
  type = object({
    start_date_time = optional(string)
    expiration = optional(object({
      duration_days  = optional(number)
      duration_hours = optional(number)
      end_date_time  = optional(string)
    }))
  })
  default = null
  # validation: within expiration, at most one of duration_days / duration_hours / end_date_time.
}

variable "ticket" {
  type = object({
    number = optional(string)
    system = optional(string)
  })
  default = null
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    delete = optional(string)
  })
  default = null
}

⚠️ Every field above is force-new — changing any of them replaces the assignment.

🧾 Outputs

Output Description Kind
id Terraform's own composite ID for the assignment Passthrough
scope Scope the role was granted at, as recorded Passthrough
role_definition_id Fully qualified Resource ID of the role that was granted Passthrough
principal_id Entra object ID of the principal holding the role Passthrough
principal_type Principal type as Azure resolved it: User, Group, ServicePrincipal and so on Passthrough
justification Justification recorded with the request, as read back Passthrough
ticket_number Change-ticket number recorded with the request, or null Derived
ticket_system Change-ticket system name recorded with the request, or null Derived
role_definition_guid The role's bare GUID, taken as the last segment of role_definition_id Derived
scope_kind Which kind of scope the role was granted at, derived from the scope string: managementGroup, subscription, resourceGroup or resource Derived
subscription_id Subscription GUID parsed out of scope, or null when the grant is at a management group Derived
management_group_name Management group name parsed out of scope, or null when the grant is not at a management group Derived
schedule_start_date_time When the grant begins, as recorded Derived
schedule_end_date_time Absolute end of the grant, as recorded, or null when there is none Derived
schedule_duration_days Expiry expressed in days, re-parsed from the ISO 8601 duration Azure returns on every refresh Derived
schedule_duration_hours Expiry expressed in hours, re-parsed from the ISO 8601 duration Azure returns on every refresh Derived
is_permanent_active_assignment THE headline fact about this resource Derived
is_time_bound True when the grant carries an expiry in any of its three forms Derived
expiration_form How the expiry was expressed: duration_days, duration_hours, end_date_time, or none Derived
azure_expiration_duration The exact ISO 8601 duration string the provider builds and sends, such as P30D or PT8H, or null when the expiry is an absolute end date or the grant is permanent Derived
has_schedule True when the caller supplied a schedule block at all Derived
has_justification True when a justification was recorded Derived
has_ticket True when change-ticket metadata was recorded Derived
principal_holds_the_role_without_activating Constant true Constant
active_assignment_is_the_only_pim_option_for_non_human_principals Constant true Constant
abac_conditions_require_the_eligible_sibling Constant true Constant
changing_any_input_revokes_and_regrants_access Constant true Constant
destroy_sends_a_pim_admin_remove_request Constant true Constant
removal_is_retried_until_the_minimum_active_duration_elapses Constant true Constant
recorded_schedule_times_are_frozen_in_state Constant true Constant
durations_are_re_read_from_azure_on_every_refresh Constant true, and the exception to the field above Constant
disappearance_of_the_assignment_is_detected_as_a_recreate Constant true, and the answer to what happens when a schedule lapses Constant
only_direct_member_assignments_are_managed Constant true Constant
request_history_retention_days Constant 45 Passthrough
pim_policy_can_reject_a_request_this_module_accepts Constant true Constant
requires_entra_id_p2_licensing Constant true Constant
this_module_ships_no_tags_variable Constant true Constant

🧠 Architecture Notes

  • Everything is immutable. scope, role_definition_id, principal_id, justification, schedule, and ticket are all force-new. Editing any of them destroys and recreates the assignment — plan changes to privileged access deliberately.
  • Active vs. eligible. This module grants the role now; the sibling terraform-azurerm-pim-eligible-role-assignment grants only the eligibility to activate it just-in-time. Choose active for standing or scheduled access, eligible for on-demand human access.
  • Expiration is exclusive. Within schedule.expiration, at most one of duration_days, duration_hours, or end_date_time may be set — enforced by a validation {} block, so the error surfaces at terraform plan, offline.
  • Create / read / delete lifecycle. With no in-place update path, the timeouts object omits update entirely; the resource is created, read, or deleted, never updated.
  • PIM / Entra ID P2 prerequisite. The assignment only succeeds in a PIM-enabled tenant with Microsoft Entra ID P2 (or equivalent). Without it the platform rejects the schedule request.
  • features {} dependence. Like every module in this suite, it carries no provider {} block; the caller supplies provider "azurerm" { features {} } and authentication.

🧱 Design Principles

Principle Behavior
Fail fast schedule.expiration exclusivity is validated at parse time — a malformed schedule never reaches Azure.
Minimal lifecycle timeouts omits update because the resource has no in-place update.
No tag surface azurerm_pim_active_role_assignment does not support tags, so the universal tail is timeouts only.
Deeply typed schedule and ticket are object() schemas, not loose maps — a typo is a type error.

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the source at a tag: ?ref=v1.0.0 — never a branch.
  • Everything here is plan-only static analysis. A human applies from CI after review.

🧪 Testing

  • terraform init -backend=false resolves the pinned provider without configuring a backend.
  • terraform validate proves the type contract — including the schedule.expiration exclusivity rule — with no cloud calls.
  • terraform fmt -check proves canonical formatting.
  • What only terraform plan (against a real, PIM-enabled tenant) exercises: the actual scope/role/principal resolution and the platform's acceptance of the schedule request.

💬 Example Output

$ terraform output
id             = "/subscriptions/00000000-0000-0000-0000-000000000000|/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7|11111111-1111-1111-1111-111111111111"
principal_type = "ServicePrincipal"

🔍 Troubleshooting

Symptom Cause Fix
Request fails with a PIM / licensing error The tenant is not PIM-enabled or lacks Microsoft Entra ID P2. Enable PIM and assign the required license before applying.
schedule.expiration must set at most one of… at plan Two expiration forms set (e.g. duration_days + end_date_time). Keep exactly one of duration_days, duration_hours, end_date_time.
AuthorizationFailed on apply The caller's identity lacks User Access Administrator / Owner at the scope. Grant the least-privilege role at (or above) the target scope.
Assignment recreated on a trivial edit A force-new field (any input) changed. Expected — every input is immutable; plan privileged-access changes accordingly.

🔗 Related Docs

  • Provider resource: azurerm_pim_active_role_assignment
  • Sibling: terraform-azurerm-pim-eligible-role-assignment (the just-in-time counterpart)
  • Sibling: terraform-azurerm-role-definition (custom role definitions)
  • Sibling: terraform-azurerm-role-assignments (standard, non-PIM role assignments)
  • This module's SCOPE.md — the cross-module contract.

💙 "Infrastructure as Code should be standardized, consistent, and secure."