Automating the Forgotten Layer: Entra ID with Terraform Stacks
Most Azure practitioners are very good at automating infrastructure. Subscriptions, networks, compute, and even Azure role assignments are usually expressed cleanly in Terraform.
But there’s a pattern I see repeatedly in real-world environments: identity is still handled manually. Entra ID groups are created in the portal, memberships are tweaked by hand, and those groups are then referenced by Terraform as if they were stable, pre-existing primitives.
That identity layer is not ancillary. It is the control plane for access, and when it lives outside of code, it quietly undermines everything else you are automating. You lose repeatability, drift detection, and confidence in your permission model. With Terraform Stacks in HCP Terraform, we can pull Entra ID into the same lifecycle as the rest of Azure.
By provisioning groups inside a dedicated component, outputting their IDs, and consuming those outputs for Azure role assignments, we get an end-to-end workflow that enforces real principle of least privilege instead of just aspiring to it.
This article walks through how to set that foundation by authenticating Terraform Stacks to Entra ID, granting it the right permissions, and wiring the azuread provider into your components so identity becomes just another automated layer.
Establishing an Entra ID Identity for Terraform Stacks
To automate Entra ID, Terraform needs an identity of its own. That identity takes the form of an Entra ID application, which Terraform Stacks will authenticate as using OIDC. This single application becomes the security boundary through which your stack interacts with both the Azure control plane and Microsoft Graph.
resource "azuread_application" "main" {
display_name = "hcp-${var.organization}-${var.application_name}-${var.service_name}-stack"
lifecycle {
ignore_changes = [required_resource_access]
}
}
The ignore_changes on required_resource_access is intentional. When you manage Microsoft Graph permissions using dedicated resources, the azuread_application resource has a tendency to try to reconcile those permissions back to whatever it thinks is correct. In practice, this can result in Terraform repeatedly attempting to remove permissions that were added elsewhere in the configuration. Ignoring that attribute avoids noisy plans and unnecessary churn.
Federated Identity Credentials for Terraform Stacks
Terraform Stacks authenticate using OIDC, not client secrets. To make that work, the Entra ID application needs a federated identity credential that matches the stack and deployment names configured in HCP Terraform. Here, a reusable module is used to establish that trust.
module "dev" {
source = "Azure-Terraformer/terraform-cloud-credential/azuread//modules/stacks/core-workflow"
version = "1.0.1"
application_id = azuread_application.main.id
organization = var.organization
project = var.application_name
stack = "${var.application_name}-${var.service_name}"
deployment = "dev"
}
The values for stack and deployment must align exactly with your Terraform Stack configuration and your *.tfdeploy.hcl files. Each environment requires its own federated credential. If you want strict isolation, you can go further and create a separate Entra ID application per deployment so that dev, test, and prod each have independent identities and permission scopes.
Granting Azure Subscription Access
With the identity in place, Terraform still needs authorization. For Azure resource management, that means assigning RBAC roles at the appropriate scope. Most automation workflows require the ability to manage role assignments themselves, which usually implies Owner at the subscription level.
data "azurerm_subscription" "dev" {
subscription_id = var.dev_subscription
}
Probably want to make it an Owner so you can manage Role Assignments.
resource "azurerm_role_assignment" "dev_owner" {
provider = azurerm.dev
scope = data.azurerm_subscription.dev.id
role_definition_name = "Owner"
principal_id = azuread_service_principal.main.object_id
principal_type = "ServicePrincipal"
}
This grants Terraform full control over Azure resources in that subscription. It solves only half of the problem, though. If the stack is going to automate Entra ID itself, it also needs Microsoft Graph permissions.
Granting Microsoft Graph Permissions for Entra ID Automation
Managing Entra ID objects such as groups, users, and applications is done through Microsoft Graph, not Azure RBAC. Conceptually it serves the same purpose — authorizing actions — but the mechanics are different.
To grant Graph permissions, you first need to locate the Microsoft Graph service principal and its app role identifiers.
data "azuread_application_published_app_ids" "well_known" {}
data "azuread_service_principal" "msgraph" {
client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"]
}
With those identifiers available, you can grant application permissions directly to your Entra ID application.
resource "azuread_application_api_access" "manage_groups" {
application_id = azuread_application.main.id
api_client_id = data.azuread_application_published_app_ids.well_known.result["MicrosoftGraph"]
role_ids = [
data.azuread_service_principal.msgraph.app_role_ids["GroupMember.ReadWrite.All"],
data.azuread_service_principal.msgraph.app_role_ids["Group.ReadWrite.All"],
data.azuread_service_principal.msgraph.app_role_ids["User.Read.All"],
data.azuread_service_principal.msgraph.app_role_ids["Application.ReadWrite.OwnedBy"]
]
}
These permissions allow Terraform to create and manage groups, memberships, and related application objects in Microsoft Entra ID. This is the enabling step that turns identity into something you can safely treat as code.
Configuring the azuread Provider for Terraform Stacks
To let your stack components talk to Entra ID, you need to add the azuread provider to the component configuration. In providers.tfcomponent.hcl, declare the provider dependency:
required_providers {
azuread = {
source = "hashicorp/azuread"
version = "~> 3.5.0"
}
}
Then configure the provider to use OIDC, passing in the identity token issued to the deployment.
provider "azuread" "this" {
config {
use_oidc = true
oidc_token = var.identity_token
client_id = var.client_id
tenant_id = var.tenant_id
}
}
Because the provider now depends on deployment-specific identity details, you also need variables for them.
variable "client_id" {
type = string
}
variable "tenant_id" {
type = string
}
Wiring Identity into the Deployment
In the deployment definition, you pass the identity token along with the client and tenant identifiers for the Entra ID application.
deployment "dev" {
inputs = {
identity_token = identity_token.azurerm.jwt
client_id = "00000000-0000-0000-0000-000000000000"
tenant_id = "00000000-0000-0000-0000-000000000000"
}
}
At runtime, Terraform Stacks exchanges that token for Microsoft Graph access as the Entra ID application, without any stored secrets.
Automating Groups as a First-Class Component
The final step is passing the configured provider into the component that manages identity resources.
component "access-control" {
source = "./src/terraform/access-control"
inputs = {
application_name = var.application_name
environment_name = var.environment_name
}
providers = {
azuread = provider.azuread.this
}
}
This is where the real payoff appears. By centralizing group creation in an access-control component, you can output group IDs and feed them directly into Azure role assignments elsewhere in the stack. Groups stop being manually curated artifacts and instead become versioned, reviewable infrastructure. From there, you can layer in membership management, PIM, or environment-specific access models without breaking the flow.
Conclusion
Automating Azure while leaving Entra ID manual creates a silent gap in your operational model. Identity is the foundation of access control, and treating it as an external concern weakens your security posture and your automation story.
Terraform Stacks make it practical to close that gap. By giving Terraform a first-class Entra ID identity, granting it explicit Graph permissions, and managing groups inside dedicated components, you get a cohesive system where infrastructure and identity evolve together.
The result is not just cleaner code, but a permission model that finally matches the level of rigor you already expect from the rest of your platform.
Entra Id Terraform Azure Active Directory Devsecops DevOps