---
id: SAIFTRBL0002
moved_from:
  - guides/troubleshooting/application-permissions-resource-already-exists.md
title: "Terraform apply fails: resource already exists - to be imported into the State (application_permissions)"
description: App roles, permission scopes, pre-authorized apps, API access, or delegated permission grants already exist in Entra ID, either because Terraform is replacing a resource it already tracks or because state does not track the object at all.
tags:
  - troubleshooting
  - terraform
---

# Terraform apply fails: resource already exists - to be imported into the State (application_permissions)

**One-sentence summary:** the app role, permission scope, pre-authorized app, API access, or delegated permission grant already exists in Entra ID; check the plan first, because a spurious replacement of a resource Terraform already tracks needs a module fix while a genuinely untracked object needs an import.

---

## 🚨 Symptom

`terraform apply` fails with one of these errors:

```text
Error: A resource with the ID "/applications/<app-object-id>/appRoles/<role-id>" already exists - to be imported into the State. Please see the resource documentation for "azuread_application_app_role" for more information.
```

```text
Error: A resource with the ID "/applications/<app-object-id>/permissionScopes/<scope-id>" already exists - to be imported into the State. Please see the resource documentation for "azuread_application_permission_scope" for more information.
```

The same pattern appears for the pre-authorization, API access, and consent resource types, in any combination, whenever a workspace starts managing objects that already exist in Entra:

```text
Error: A resource with the ID "<app-object-id>/preAuthorizedApplication/<authorized-app-id>" already exists - to be imported into the State. Please see the resource documentation for "azuread_application_pre_authorized" for more information.
```

```text
Error: A resource with the ID "/applications/<app-object-id>/apiAccess/<resource-app-id>" already exists - to be imported into the State. Please see the resource documentation for "azuread_application_api_access" for more information.
```

```text
Error: A resource with the ID "/oauth2PermissionGrants/<grant-id>" already exists - to be imported into the State. Please see the resource documentation for "azuread_service_principal_delegated_permission_grant" for more information.
```

This appears in Terraform Cloud apply logs when `application_permissions` tries to create a role, scope, pre-authorization, API access grant, or delegated permission grant that already exists in Entra ID.

---

## 📌 Applies to

| Aspect | Value |
| ------ | ----- |
| **Component** | `saif-application-permissions` Terraform module, and the `permissions` submodule inside the platform `application` module |
| **Forge versions** | Any version managing generated `app_roles` or `scopes` |
| **Related versions** | Generated `auth.generated.tf` older than the Forge 3.8.5 template still carries the `depends_on` that triggers cause 1. The platform half is fixed in `iac-azure-modules` `5.1.1` |

---

## 🧠 Cause

Two different problems produce this identical error, and their fixes are opposite. One needs the object left alone and a module change; the other needs an import. Run the [Diagnose](#diagnose) step before acting.

### Cause 1: Terraform is replacing a resource it already tracks

The object is in state, the plan shows it being replaced, and the replacement traces back to a deferred data source read. Terraform recreates it, the "new" resource resolves to the same Entra object, and the create call collides with the object that is still there.

The replacement itself is spurious. It comes from a coarse module-level `depends_on` sitting above the data sources the module reads:

1. Terraform cannot read a data source at plan time if a **managed resource** in its dependency closure has a pending change in the same plan. It defers the read to apply time and reports `read_because_dependency_pending`. A changed local or variable does not trigger this, and unknown values in the data source's own configuration produce a different reason, `read_because_config_unknown`.
2. Every attribute derived from that deferred read becomes unknown at plan time. Which attribute that is depends on the module. In `application_permissions` it is the application object ID from `data.azuread_application`. In the platform `application` module the application ID arrives as a direct resource reference and is never unknown; the unknowns there are the pre-authorized client lookups, `data.azuread_application.pre_authorized` and `data.azuread_service_principal.pre_authorized_sp`.
3. Those unknowns land on attributes that cannot change in place. The provider reports the change as impossible without replacement, which is what `"action_reason": "replace_because_cannot_update"` records, and Terraform plans a replacement.
4. At apply time the data sources resolve to the same objects they always did, so the replacement recreates something identical to what already exists.

Two separate `depends_on` blocks caused this in practice:

| Location | Resources affected | Observed action order | Fix |
| -------- | ------------------ | --------------------- | --- |
| `module "application_permissions"` in generated `infra/api/auth.generated.tf` | `azuread_application_app_role`, `azuread_application_permission_scope` | `["delete", "create"]`, destroy-then-create, **destructive if the run fails** | Regenerate the file from the current template (Forge 3.8.5 and later) |
| `module "permissions"` inside the platform `application` module | `azuread_application_pre_authorized`, `azuread_application_api_access`, `azuread_service_principal_delegated_permission_grant` | `["create", "delete"]`, create-before-destroy, safe for the address that errors | Move to a module version built on `iac-azure-modules` `>= 5.1.1`, which contains [iac-azure-modules#76](https://github.com/saif-corp/iac-azure-modules/pull/76) |

The trigger is "something upstream has a pending change," so this is intermittent rather than constant. A workspace can apply cleanly for months and then fail the first time an unrelated upstream resource changes, such as a new `random_string` introduced by a platform module bump. That intermittency is the tell: nothing about the app registration itself changed.

Read the action order as a diagnostic signal, not just a caveat. It tells you which `depends_on` you are looking at and whether anything is at risk:

- `["delete", "create"]` on an app role or scope points at the generated `auth.generated.tf`. Terraform destroys first, so a run that errors before the create leaves the object deleted from the registration. In the run behind this article, `App.Read`, `App.Write`, and the `User.Read` scope were planned this way and all three disappeared from state between serial 4 and serial 5. The destroy ran, the run errored, the create never happened.
- `["create", "delete"]` on a pre-authorization, API access, or delegated grant resource points at the platform `application` module. The create runs first and collides with the object that is still there, so the run errors without touching the existing object. These are the addresses that report `already exists`, and they are safe.

`action_reason` was `replace_because_cannot_update` on all nine replacements in that run, which is the ordinary "the provider cannot change this in place" reason rather than anything exotic.

Do not treat the error list as the blast radius. That run planned six replacements in the platform module and surfaced only three errors, because the apply stopped early. Check the app registration after any failed apply, not just the plan, and check the app roles and scopes first since those are the destroy-then-create half.

### Cause 2: state drift, the object is not tracked at all

Standard Terraform state drift: the role, scope, pre-authorization, or grant exists in Entra ID, but Terraform state has no record of the matching resource address. That can happen after a partial apply, manual creation, state loss, or an out-of-band delete and recreate. It can occur on its own, or immediately after resolving the deadlock in [Terraform apply fails: Provider produced inconsistent final plan](application-permissions-provider-inconsistent-final-plan.md) if you re-ran an apply while the object was mid-transition.

The recurring case is a workspace starting to manage an Entra object that already exists, and there is no single version boundary for it. The pre-authorization, API access, and consent resources arrived in `application_permissions` across four commits in late 2025, not in [3.0.12](../release-notes/3.0.12.md):

| Resource | Introduced |
| -------- | ---------- |
| `azuread_application_api_access.api_permissions`, `azuread_application_pre_authorized.pre_authorized` | `b66ce20`, 2025-10-22, with the module itself |
| `azuread_application_api_access.pre_authorized_api_access` | `f5786c1`, 2025-11-03 |
| `azuread_service_principal_delegated_permission_grant.pre_authorized_consent` | `e19e64d`, 2025-11-03 |
| `azuread_service_principal_delegated_permission_grant.scope_permissions` | `b13526a`, 2025-11-07 |

3.0.12 (`cc89c65`, 2026-01-20) added no resource types at all. It added the `configure_api_access` and `grant_admin_consent` opt-out flags, both defaulting to `true`, plus documentation comments. An app that kept the defaults saw no change in what the module manages.

So the trigger is narrower than "upgrading past a release." Expect this error when a workspace manages one of these objects for the first time: an app onboarded to a module version that already declares the resource, a `pre_authorized_applications` entry added for a client someone had already pre-authorized by hand, or `configure_api_access` or `grant_admin_consent` flipped from `false` back to `true`. Only the addresses newly brought under management collide, so the blast radius is those addresses rather than the whole module.

---

## 🔍 Diagnose

Check whether Terraform already tracks the failing address:

```powershell
terraform state list | Select-String "azuread_application_app_role|azuread_application_permission_scope|azuread_application_pre_authorized|azuread_application_api_access|azuread_service_principal_delegated_permission_grant"
```

Then route on what you find. Do not route on `must be replaced` alone. A tainted resource, an explicit `terraform apply -replace=...`, and a genuine change to an attribute that forces replacement all replace an address that is present in state, and none of them are fixed by this guide.

1. **Address is not in state.** Cause 2. Go to [Fix cause 2](#fix-cause-2-import-or-remove-the-untracked-object).
2. **Address is in state, the plan shows `must be replaced`, and the deferred-read signature below is present.** Cause 1. Go to [Fix cause 1](#fix-cause-1-remove-the-coarse-depends_on).
3. **Address is in state and replaced, but the deferred-read signature is absent.** Neither cause applies, and this guide is the wrong one. Find the real replacement trigger instead: look for `is tainted, so must be replaced` in the plan output, check the run's plan options for `-replace`, and diff your config for an actual change to a replacement-forcing attribute. Removing the `depends_on` will not help.

Confirm the cause 1 signature from the plan rather than the apply log. In a Terraform Cloud structured plan (`GET /api/v2/plans/{plan-id}/json-output`), look for all of:

- `"action_reason": "read_because_dependency_pending"` on a nearby `data.azuread_*` resource.
- A replacement on the failing address, meaning `"actions": ["create", "delete"]` or `["delete", "create"]`, normally with `"action_reason": "replace_because_cannot_update"`.
- No `"action_reason"` of `replace_because_tainted` or `replace_by_request` on that address, either of which points at step 3 instead.

List every multi-action change in the plan, not just the addresses that errored, since the action order per address tells you which `depends_on` is responsible and which objects a failed run can delete.

In human-readable plan output, look for `will be read during apply` on a data source whose configuration is entirely static. That strongly suggests an inherited `depends_on`, but it is not proof. A `depends_on` on the data block itself, an unknown `count` or `for_each`, a precondition that depends on a pending resource, or unknown values in the provider configuration all defer a read as well. Confirm a module-level `depends_on` is actually present before acting on it.

---

## ✅ Fix

### Fix cause 1: remove the coarse `depends_on`

Do **not** import. The object is already tracked correctly; the plan is wrong.

1. Compare your generated `infra/api/auth.generated.tf` against the current template at `src/templates/saif-feature-api/infra/api/auth.generated.tf`. If your copy contains `depends_on = [module.saif-appservices]`, it predates the fix. Regenerate the file, or apply the change by hand: drop the `depends_on`, raise the version constraint to `>= 3.8.5, < 4.0.0`, and pass the identity in directly.

    ```hcl
    application = {
      id        = module.saif-appservices.application_id
      client_id = module.saif-appservices.application_client_id
    }
    service_principal = {
      object_id = module.saif-appservices.application_principal_object_id
      client_id = module.saif-appservices.application_principal_client_id
    }
    ```

    Passing these as inputs creates the same ordering edges the `depends_on` was there to provide, without deferring the module's data reads.

    The [3.8.5 release notes](../release-notes/3.8.5.md) are written around a different symptom of the same `depends_on`, `Error: Cycle` on apply. For the app roles and scopes, spurious replacement and the cycle both come from the coarse module boundary, and this change clears both, so do not skip 3.8.5 because your error text does not mention a cycle. It does not cover the platform module half; see step 2.

2. For the pre-authorization, API access, and delegated grant resources, the fix is [iac-azure-modules#76](https://github.com/saif-corp/iac-azure-modules/pull/76), released in `iac-azure-modules` `5.1.1`. That is a platform module you consume indirectly: `saif-appservices` (`SAIFCorp/saif-apiservice/azure`) sits several modules above where the change lands, so bumping `saif-appservices` alone does not pick it up. You get the fix when the Forge-published module you call is itself built on `iac-azure-modules >= 5.1.1`, so check the release notes for the Forge version carrying that bump and move to it.

    Until your workspace is on a version that carries the fix, the interim path for this half is to re-run the apply after the upstream change has settled. The deferred read is no longer pending on the second run, so the replacement disappears and the apply succeeds. That is a workaround, not a fix; it recurs the next time something upstream changes. Once you are on a version containing #76, this step should stop being necessary, and a recurrence means the deferred read is coming from somewhere else.

3. Re-plan. The app role and scope replacements should disappear and `data.azuread_application` should resolve at plan time. Replacements inside the platform `application` module persist until the module you call is built on `iac-azure-modules >= 5.1.1`.

4. If a previous failed apply already destroyed app roles or scopes, the clean apply after this fix recreates them. Verify against the registration rather than assuming.

### Fix cause 2: import or remove the untracked object

1. Import the exact ID from the error message:

    ```powershell
    terraform import 'module.application_permissions.module.permissions.azuread_application_app_role.app_roles["<value>"]' "/applications/<app-object-id>/appRoles/<role-id>"
    terraform import 'module.application_permissions.module.permissions.azuread_application_permission_scope.scopes["<value>"]' "/applications/<app-object-id>/permissionScopes/<scope-id>"
    ```

    For the pre-authorization, API access, and consent resources there are five distinct addresses, not three. The module declares two `azuread_application_api_access` resources and two `azuread_service_principal_delegated_permission_grant` resources under different names, and each pair means a different thing:

    | Resource | What it represents | Whose object ID the import ID carries |
    | -------- | ------------------ | ------------------------------------- |
    | `azuread_application_api_access.api_permissions` | This API's own permissions on an upstream API such as Microsoft Graph | This API |
    | `azuread_service_principal_delegated_permission_grant.scope_permissions` | Admin consent for those upstream permissions | Grant ID only |
    | `azuread_application_pre_authorized.pre_authorized` | A client app pre-authorized against this API | This API |
    | `azuread_application_api_access.pre_authorized_api_access` | The client app's own API access entry pointing back at this API | The pre-authorized client app |
    | `azuread_service_principal_delegated_permission_grant.pre_authorized_consent` | Admin consent for the client app against this API | Grant ID only |

    Include the resource name in the address. `azuread_application_api_access["Microsoft Graph"]` without a name is not a valid Terraform address and fails immediately with `Error: Invalid address`. Use the ID exactly as it appears in the error, prefixed as shown:

    ```powershell
    terraform import 'module.saif-appservices.module.identity.module.application.module.permissions.azuread_application_api_access.api_permissions["Microsoft Graph"]' "/applications/<this-api-app-object-id>/apiAccess/<upstream-api-client-id>"
    terraform import 'module.saif-appservices.module.identity.module.application.module.permissions.azuread_service_principal_delegated_permission_grant.scope_permissions["Microsoft Graph"]' "/oauth2PermissionGrants/<grant-id>"
    terraform import 'module.saif-appservices.module.identity.module.application.module.permissions.azuread_application_pre_authorized.pre_authorized["_saif_cli"]' "<this-api-app-object-id>/preAuthorizedApplication/<client-app-client-id>"
    terraform import 'module.saif-appservices.module.identity.module.application.module.permissions.azuread_application_api_access.pre_authorized_api_access["_saif_cli"]' "/applications/<client-app-object-id>/apiAccess/<this-api-client-id>"
    terraform import 'module.saif-appservices.module.identity.module.application.module.permissions.azuread_service_principal_delegated_permission_grant.pre_authorized_consent["_saif_cli"]' "/oauth2PermissionGrants/<grant-id>"
    ```

    The two object-ID placeholders are not the same app. `<this-api-app-object-id>` is the registration this module manages. `<client-app-object-id>` is the pre-authorized client's object ID, which `pre_authorized_api_access` takes instead. They coincide only for the `_self` key, where the client is this API itself. `_saif_cli` is an example key; substitute your own `pre_authorized_applications` keys.

    The delegated permission grant import **must** be prefixed with `/oauth2PermissionGrants/`. The bare grant ID fails at `terraform plan`, harmlessly, since no state is touched. Resolve the grant IDs with:

    ```powershell
    $apiSpId = az ad sp list --filter "appId eq '<this-api-client-id>'" --query "[0].id" -o tsv
    $msGraphSpId = az ad sp show --id 00000003-0000-0000-c000-000000000000 --query id -o tsv
    $clientSpId = az ad sp list --filter "appId eq '<pre-authorized-app-client-id>'" --query "[0].id" -o tsv

    # scope_permissions: this API consuming Microsoft Graph
    az rest --method GET --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$apiSpId' and resourceId eq '$msGraphSpId'" --query "[?consentType=='AllPrincipals'].id | [0]" -o tsv

    # pre_authorized_consent: the client app consuming this API, client and resource inverted
    az rest --method GET --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$clientSpId' and resourceId eq '$apiSpId'" --query "[?consentType=='AllPrincipals'].id | [0]" -o tsv
    ```

    The two queries are not interchangeable. `pre_authorized_consent` inverts the pair, so the first query returns nothing for it, and loosening the filter until it returns something imports the wrong grant.

    `00000003-0000-0000-c000-000000000000` is the well-known Microsoft Graph application ID, resolved to the tenant's Microsoft Graph service principal object ID. A client/resource pair can have both a per-user (`Principal`) grant and a tenant-wide admin-consent (`AllPrincipals`) grant; `application_permissions` manages the `AllPrincipals` grant, so filter on `consentType` instead of taking `value[0]`. Confirm which grant type matches what Terraform actually declares before importing.

    These five are not the whole module. `application_permissions` declares ten resources; the other five are `azuread_application_app_role.app_roles`, `azuread_application_permission_scope.scopes`, and three `azuread_app_role_assignment` resources (`permissions`, `role_assignments`, `pre_authorized_app_roles`), which drift the same way. Import whichever addresses your errors actually name instead of working from a fixed count.

    Import the addresses one at a time. Each `terraform import` is independent and touches only the address you name, so run one command per address the error reports, in any order. There is no bulk path for these resources, and the commands above are the whole procedure.

2. If the object is orphaned, remove it through the Entra admin center, `az rest`, or Microsoft Graph instead of importing it.

!!! warning "Do not force a recreate to clear this error"

    [Terraform apply fails: Provider produced inconsistent final plan](application-permissions-provider-inconsistent-final-plan.md) offers a targeted destroy of the tracked app roles and scopes. That step is gated there on confirming the deferred-read signature first and on targeting exact instance keys, and it is not a remedy for this error. If [Diagnose](#diagnose) confirms cause 1, the objects are fine and only the plan is wrong, and a targeted destroy deletes working app roles and scopes to work around it. The `already exists` error itself comes from a create-before-destroy replacement that leaves the object intact; a targeted destroy removes it for real, so callers lose the authorization that role or scope carries until the recreate lands, and a failed apply can leave it deleted. The IDs survive that: they are deterministic `uuidv5` values seeded on the application ID and the role or scope value, so a recreate against an unchanged seed recomputes the identical GUID. GUIDs churn only when the seed changes, such as when the app registration itself is recreated. Fix the `depends_on` instead.

---

## 🔬 Verify

```text
terraform apply completes without the "already exists - to be imported into the State" error.
```

For cause 1, the plan no longer shows `must be replaced` on the permission resources, and no `data.azuread_*` source reports `will be read during apply`. Confirm the app registration still has every expected app role and scope, since a prior failed apply may have destroyed some.

For cause 2, `terraform state list` now includes the imported address.

---

## 📚 Related

- [Terraform apply fails: Provider produced inconsistent final plan](application-permissions-provider-inconsistent-final-plan.md)
- [saif-application-permissions README](https://github.com/saif-corp/forge/blob/main/src/terraform/saif-application-permissions/README.md)
- [3.8.5 release notes](../release-notes/3.8.5.md)
- [3.0.12 release notes](../release-notes/3.0.12.md), which added the `configure_api_access` and `grant_admin_consent` opt-out flags
- [Application permissions configuration guide](../build/identity/configuration/app-permissions.md)
