Using ephemeral resources with AzAPI

Using ephemeral resources with AzAPI

Avoid writing sensitive properties to state

Using ephemeral resources with AzAPI

When working with Terraform, one of the ongoing concerns for teams—especially in regulated or multi-tenant environments — is keeping sensitive data like access keys, connection strings, or tokens out of the state file.

While Terraform does offer sensitive = true, this only redacts values from CLI output — it does not prevent secrets from being written to the state file.

Ephemeral resources, introduced in Terraform v1.10, allow for properties to be dynamically fetched without persisting them. The AzAPI provider enables this by supporting a sensitive_body block, which lets you inject secret values into resources safely — without recording them in Terraform state.

A demo to illustrate

We will:

  1. Provision a resource—like a Log Analytics Workspace.

  2. Fetch its secret using an ephemeral azapi_resource_action (e.g., the workspace shared key).

  3. Inject that secret into a downstream resource — like a Container Group — using sensitive_body.

Fetch the LAW key

We’re going to use azapi_resource_action to fetch the key using a POST to the API.

A regular (non-ephemeral) version of this resource would look like:

1
2
3
4
5
6
7
resource "azapi_resource_action" "law_shared_key" {
  action                 = "sharedKeys"
  method                 = "POST"
  resource_id            = azapi_resource.log_analytics_workspace.output.id
  type                   = "Microsoft.OperationalInsights/workspaces@2020-08-01"
  response_export_values = ["primarySharedKey"]
}

However, this writes the shared key to the state file.

Switching to ephemeral avoids that:

1
2
3
4
5
6
7
ephemeral "azapi_resource_action" "law_shared_key" {
  action                 = "sharedKeys"
  method                 = "POST"
  resource_id            = azapi_resource.log_analytics_workspace.output.id
  type                   = "Microsoft.OperationalInsights/workspaces@2020-08-01"
  response_export_values = ["primarySharedKey"]
}

This fetches the key at apply time—without persisting it. Next, we need to inject it into our Container Group’s diagnostics configuration.

Updating the Container Instance

You might try this at first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
resource "azapi_resource" "container" {
  ...
  body = {
    properties = {
      diagnostics = {
        logAnalytics = {
          logType             = "ContainerInsights"
          workspaceId         = azapi_resource.log_analytics_workspace.output.properties.customerId
          workspaceResourceId = azapi_resource.log_analytics_workspace.output.id
          # the following won't work!
          workspaceKey = ephemeral.azapi_resource_action.law_shared_key.output.primarySharedKey
        }
      }
    }
  }
  ...
}

But this will fail at plan time:

1
Ephemeral values are not valid for "body", because it is not a write-only attribute and must be persisted to state.

The AzAPI provider is protecting you from accidentally writing a secret to state.

Solution: Use “sensitive_body”

Here’s how to safely inject the secret:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
resource "azapi_resource" "container" {
  ...
  body = {
    properties = {
      ...
      diagnostics = {
        logAnalytics = {
          logType             = "ContainerInsights"
          workspaceId         = azapi_resource.log_analytics_workspace.output.properties.customerId
          workspaceResourceId = azapi_resource.log_analytics_workspace.output.id
        }
      }
      ...
    }    
  }
  ...
  sensitive_body = {
    properties = {
      diagnostics = {
        logAnalytics = {
          workspaceKey = ephemeral.azapi_resource_action.law_shared_key.output.primarySharedKey
        }
      }
    }
  }
}

All that is needed is to move the workspaceKey attribute into sensitive_body. This is merged with the body properties by the AzAPI provider.

A terraform apply later, and we have:

The starter container for Azure Container Instances

A Note on AzureRM

The AzureRM provider also supports write-only secrets, but in my opinion not quite so elegantly.

Here’s an example with a KeyVault secret:

1
2
3
4
5
6
resource "azurerm_key_vault_secret" "example" {
  name         = "secret-sauce"
  # AzureRM uses an additional attribute, i.e. instead of `value`, it uses `value_wo`
  value_wo     = "szechuan" 
  key_vault_id = azurerm_key_vault.example.id
}

If we look at the Container Groups resource - we see it does not support the write only attribute yet.

However, because AzAPI uses a more generic mechanism, it works out of the box.

Just another reason why AzAPI is awesome 😺

Known Issue: Schema Validation Bug

If you use sensitive_body with numeric fields (e.g., ports, memory, CPU), the AzAPI provider currently serialises them as strings, which can trigger schema validation errors.

This bug is tracked here:

https://github.com/Azure/terraform-provider-azapi/issues/906

Example error:

1
2
│ embedded schema validation failed: the argument "body" is invalid:
│ `properties.ipAddress.ports.0.port` is invalid, expect `integer` but got `basetypes.StringValue`

Workaround

Until the bug is fixed, set schema validation = false for the affected resource:

The team are busy on fixing related issues, with more fixes due imminently the next version of the AzAPI provider (v2.5.0).

In summary

  • Use ephemeral resources to avoid secrets in Terraform state.

  • Combine with sensitive_body to inject those secrets into resources.

  • The AzAPI approach provides coverage for any attribute.

Try it yourself

This post is licensed under CC BY 4.0 by the author.