Skip to content

Azure Provider: Authenticating using managed identities for Azure resources

Terraform supports a number of different methods for authenticating to Azure:


We recommend using a service principal or a managed identity when running Terraform non-interactively (such as when running Terraform in a CI/CD pipeline), and authenticating using the Azure CLI when running Terraform locally.

What is a managed identity?

Managed identities for Azure resources can be used to authenticate to services that support Azure Active Directory (Azure AD) authentication. There are two types of managed identities: system-assigned and user-assigned. This article is based on system-assigned managed identities.

Managed identities work in conjunction with Azure Resource Manager (ARM), Azure AD, and the Azure Instance Metadata Service (IMDS). Azure resources that support managed identities expose an internal IMDS endpoint that the client can use to request an access token. No credentials are stored on the VM, and the only additional information needed to bootstrap the Terraform connection to Azure is the subscription ID and tenant ID.

Azure AD creates an AD identity when you configure an Azure resource to use a system-assigned managed identity. The configuration process is described in more detail, below. Azure AD then creates a service principal to represent the resource for role-based access control (RBAC) and access control (IAM). The lifecycle of a system-assigned identity is tied to the resource it is enabled for: it is created when the resource is created and it is automatically removed when the resource is deleted.

Before you can use the managed identity, it has to be configured. There are two steps:

  1. Assign a role for the identity, associating it with the subscription that will be used to run Terraform. This step gives the identity permission to access Azure Resource Manager (ARM) resources.
  2. Configure access control for one or more Azure resources. For example, if you use a key vault and a storage account, you will need to configure the vault and container separately.

Before you can create a resource with a managed identity and then assign an RBAC role, your account needs sufficient permissions. You need to be a member of the account Owner role, or have Contributor plus User Access Administrator roles.

Not all Azure services support managed identities, and availability varies by region. Configuration details vary slightly among services. For more information, see Services that support managed identities for Azure resources.

Configuring a VM to use a system-assigned managed identity

The (simplified) Terraform configuration below provisions a virtual machine with a system-assigned managed identity, and then grants the Contributor role to the identity.

/*Provider bindings are generated by running cdktf get.
See https://cdk.tf/provider-generation for more details.*/
import * as azurerm from "./.gen/providers/azurerm";
/*The following providers are missing schema information and might need manual adjustments to synthesize correctly: azurerm.
For a more precise conversion please use the --provider flag in convert.*/
const azurermVirtualMachineExample = new azurerm.virtualMachine.VirtualMachine(
  this,
  "example",
  {
    identity: [
      {
        type: "SystemAssigned",
      },
    ],
  }
);
const dataAzurermRoleDefinitionContributor =
  new azurerm.dataAzurermRoleDefinition.DataAzurermRoleDefinition(
    this,
    "contributor",
    {
      name: "Contributor",
    }
  );
const dataAzurermSubscriptionCurrent =
  new azurerm.dataAzurermSubscription.DataAzurermSubscription(
    this,
    "current",
    {}
  );
const azurermRoleAssignmentExample = new azurerm.roleAssignment.RoleAssignment(
  this,
  "example_3",
  {
    principal_id: `\${${azurermVirtualMachineExample.identity.fqn}[0].principal_id}`,
    role_definition_id: `\${${dataAzurermSubscriptionCurrent.id}}\${${dataAzurermRoleDefinitionContributor.id}}`,
    scope: dataAzurermSubscriptionCurrent.id,
  }
);
/*This allows the Terraform resource name to match the original name. You can remove the call if you don't need them to match.*/
azurermRoleAssignmentExample.overrideLogicalId("example");

Configuring Terraform to use a managed identity

At this point we assume that managed identity is configured on the resource (e.g. virtual machine) being used - and that permissions have been assigned via Azure's Identity and Access Management system.

Terraform can be configured to use managed identity for authentication in one of two ways: using environment variables, or by defining the fields within the provider block.

Configuring with environment variables

Setting thearmUseMsi environment variable (equivalent to provider block argument useMsi) to true tells Terraform to use a managed identity.

By default, Terraform will use the system assigned identity for authentication. To use a user assigned identity instead, you will need to specify the armClientId environment variable (equivalent to provider block argument clientId) to the client id of the identity.

By default, Terraform will use a well-known MSI endpoint to get the authentication token, which covers most use cases. In other cases where the endpoint is different (e.g. when running as an Azure Function App), you must explicitly specify the endpoint using the armMsiEndpoint environment variable (equivalent to provider block argument msiEndpoint).

!> Note: we recommend against running Terraform inside of a Function App as the low memory ceiling can lead to Terraform being terminated and data (including the State File) being lost. Instead we’d recommend considering triggering an external process, such as Terraform Cloud or a CI System to run these longer-running more intensive processes - see Terraform in Automation for more details.

In addition to a properly-configured management identity, Terraform needs to know the subscription ID and tenant ID to identify the full context for the Azure provider.

export ARM_USE_MSI=true
export ARM_SUBSCRIPTION_ID=159f2485-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ARM_TENANT_ID=72f988bf-xxxx-xxxx-xxxx-xxxxxxxxxxxx
export ARM_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # only necessary for user assigned identity
export ARM_MSI_ENDPOINT=$MSI_ENDPOINT # only necessary when the msi endpoint is different than the well-known one

A provider block is technically optional when using environment variables. Even so, we recommend defining provider blocks so that you can pin or constrain the version of the provider being used, and configure other optional settings:

/*Provider bindings are generated by running cdktf get.
See https://cdk.tf/provider-generation for more details.*/
import * as azurerm from "./.gen/providers/azurerm";
/*The following providers are missing schema information and might need manual adjustments to synthesize correctly: hashicorp/azurerm.
For a more precise conversion please use the --provider flag in convert.*/
new azurerm.provider.AzurermProvider(this, "azurerm", {
  features: [{}],
});

Configuring with the provider block

It's also possible to configure a managed identity within the provider block:

/*Provider bindings are generated by running cdktf get.
See https://cdk.tf/provider-generation for more details.*/
import * as azurerm from "./.gen/providers/azurerm";
/*The following providers are missing schema information and might need manual adjustments to synthesize correctly: hashicorp/azurerm.
For a more precise conversion please use the --provider flag in convert.*/
new azurerm.provider.AzurermProvider(this, "azurerm", {
  features: [{}],
  use_msi: true,
});

If you intend to configure a remote backend in the provider block, put useMsi outside of the backend block:

/*Provider bindings are generated by running cdktf get.
See https://cdk.tf/provider-generation for more details.*/
import * as azurerm from "./.gen/providers/azurerm";
/*The following providers are missing schema information and might need manual adjustments to synthesize correctly: hashicorp/azurerm.
For a more precise conversion please use the --provider flag in convert.*/
new azurerm.provider.AzurermProvider(this, "azurerm", {
  backend: [
    {
      azurerm: [
        {
          container_name: "tfstate",
          key: "prod.terraform.tfstate",
          storage_account_name: "abcd1234",
          subscription_id: "00000000-0000-0000-0000-000000000000",
          tenant_id: "00000000-0000-0000-0000-000000000000",
        },
      ],
    },
  ],
  features: [{}],
  use_msi: true,
});

More information on the fields supported in the provider block can be found here.