One generic, secure-by-default boundary over the
azapi_resourceprimitive β manage any Azure Resource Manager resource type with a typed contract, write-only secrets, export-nothing defaults, and embedded schema validation on. The escape hatch for preview services, brand-new api-versions, and propertiesazurermdoes not expose yet. Built for the Azureazapiprovider v2.x (~> 2.10).
This module wraps the azapi_resource primitive β the one provider resource that can manage the full
lifecycle of any ARM type β behind the secure defaults:
- 𧬠Type-is-the-contract β caller-supplied ARM
type@api-version, a native-HCLbody, and a typed behavior tail. The empty call is the safe call. - π Write-only secrets β credential material goes in
sensitive_body, merge-patched at apply and never persisted to state. - π« Export nothing by default β
response_export_values = [], so no read-only ARM property leaks into state or outputs unless you opt in per path. - π‘οΈ Embedded validation on β
schema_validation_enabled = truecatches malformed/unknown body keys at plan, before the request reaches Azure. - π Managed-identity first β system- and/or user-assigned identity, with
principal_idsurfaced for RBAC; no embedded keys. - ποΈ Full primitive surface β
locks,retry,replace_triggers_*, partial-list controls, and per-operation request headers/query parameters, all grouped and typed.
π‘ Why it matters: raw
azapi_resourceis a loaded syringe β a dynamicbody, secrets that can land in state, and read-backs that can leak. This module is the child-proof cap: it keeps the generic power while making the locked-down resource the default. When a curatedtf-mod-azapi-<service>_<resource>module exists for your type, prefer it; reach for this generic wrapper when one does not exist yet.
β οΈ This is the one module that accepts a caller-suppliedtype(and therefore a caller-chosen api-version). Every curated module pins its type/api-version instead, per this module suite's design conventions. Do not copy this relaxation into a curated module.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
tf-mod-azapi-resource/
βββ providers.tf # required_providers (Azure/azapi ~> 2.10) + required_version >= 1.12.0; no provider block
βββ variables.tf # type, placement, body, sensitive_body, identity, behavior tail, tags, timeouts, request_options
βββ main.tf # one keystone azapi_resource.this; total renderer; dynamic identity/timeouts
βββ outputs.tf # id (primary), name, output (opt-in), principal_id, tenant_id
βββ examples/
β βββ basic/ # minimal secure call (a resource group)
βββ README.md # you are here
βββ SCOPE.md # lightweight cross-module contract
flowchart LR
caller["Caller root config<br/>provider azapi + OIDC auth"]:::ext
res["tf-mod-azapi-resource<br/>THIS Β· generic keystone"]:::this
upd["tf-mod-azapi-update-resource<br/>patch existing"]:::sib
act["tf-mod-azapi-resource-action<br/>control-plane action"]:::sib
dp["tf-mod-azapi-data-plane-resource<br/>data-plane object"]:::sib
tier2["Curated tier-2 modules<br/>cognitiveservices, containerapp, roleassignment"]:::sib
arm["Azure Resource Manager<br/>any Microsoft.* type"]:::keystone
caller --> res
res -->|"id as resource_id"| upd
res -->|"id as resource_id"| act
res -->|"id as parent_id"| dp
res -. "shares secure body pattern".-> tier2
res -->|"PUT request"| arm
tier2 -->|"PUT request"| arm
classDef this fill:#0078D4,color:#fff,stroke:#004578,stroke-width:3px;
classDef keystone fill:#004578,color:#fff,stroke:#0078D4,stroke-width:2px;
classDef sib fill:#E6F2FB,color:#073B6B,stroke:#0078D4,stroke-width:1px;
classDef ext fill:#f2f2f2,color:#333333,stroke:#999999,stroke-width:1px;
βΉοΈ Blue = this module. Deep blue = Azure Resource Manager, the universal target. The other three primitives operate on resources this module (or
azurerm) creates; curated tier-2 modules reuse the same secure-body pattern for specific ARM types.
flowchart TD
subgraph inputs["Typed inputs (the contract)"]
direction TB
id_in["type @ api-version<br/>name Β· parent_id Β· location"]:::ext
body_in["body (native HCL)<br/>tags Β· identity"]:::ext
sec_in["sensitive_body<br/>(write-only)"]:::secret
beh_in["behavior tail<br/>schema_validation_enabled = true<br/>response_export_values = []"]:::ext
end
this["azapi_resource.this<br/>one keystone Β· total renderer"]:::this
out["Outputs<br/>id Β· name Β· principal_id Β· tenant_id Β· output"]:::keystone
id_in --> this
body_in --> this
sec_in -. "merge-patched, never in state".-> this
beh_in --> this
this --> out
classDef this fill:#0078D4,color:#fff,stroke:#004578,stroke-width:3px;
classDef keystone fill:#004578,color:#fff,stroke:#0078D4,stroke-width:2px;
classDef secret fill:#FCE8E6,color:#8B1A10,stroke:#D13438,stroke-width:1px;
classDef ext fill:#f2f2f2,color:#333333,stroke:#999999,stroke-width:1px;
Resource inventory (1 azapi_* block):
azapi_resource.thisβ the single keystone. Renderstype/name/parent_id/location/bodyfrom typed inputs; routes secrets through write-onlysensitive_body; maps the behavior tail ontoschema_validation_enabled/ignore_*/response_export_values/locks/retry; rendersidentityandtimeoutsas dynamic blocks.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azapi provider | ~> 2.10 (validated against v2.10.0) |
| Provider config | None inside the module β the caller configures provider "azapi" + auth. |
Schema notes that bite (verified against the azapi v2.10.0 binary schema):
- π§±
name,parent_id, and the resource-type portion oftypeare force-new β changing any destroys and recreates the resource. Changing only the api-version usually does not replace (azapi's change detection treats a functionally-equivalent body as a no-op). - π
sensitive_bodyis a write-only argument (Terraform β₯ 1.11 feature) β its value is never written to state. Pair it withsensitive_body_versionto force a re-send when a rotated secret changes. - π«
behavior.response_export_values = []means the provider exports nothing intooutput. With the provider defaultdisable_default_output = false, leaving the argument unspecified would pull read-only props into state β this module always sets it, so the secure behavior is the default. - β»οΈ
retry.multiplierandretry.randomization_factorexist in the schema but are deprecated; this module intentionally does not expose them.
Because the ARM type is caller-supplied, exact permissions are type-dependent. For whatever type
is passed, the apply identity needs, on the target parent_id scope:
Microsoft.<RP>/<resource>/write,/read,/deleteβ control-plane CRUD for that type.- Any child-action the body triggers (e.g.
Microsoft.<RP>/<resource>/<action>/action). - RP registration:
Microsoft.<RP>registered in the subscription (orMicrosoft.Resources/subscriptions/providers/register/actionifskip_provider_registration = false). - Least-privilege: a custom role scoped to exactly the above, or a service-specific built-in role
(e.g. Storage Account Contributor) on the
parent_idscope β never subscription Owner.
β οΈ Final permission scoping is a control decision β route role design to Cloud Security / IAM before granting. This module assigns no roles itself.
- The resource provider
Microsoft.<RP>for the chosentypeis registered in the subscription (az provider show -n Microsoft.<RP>). - The chosen
typeapi-version is available in the target region (Microsoft Learn /az provider show). - The
parent_idscope already exists (resource group / parent resource / extension target). - Terraform
>= 1.12; azapi~> 2.10. - Recommended provider posture (set by the caller, not this module):
enable_preflight = true, and in regulated contextsdisable_default_output = true. - Auth configured by the caller β OIDC / workload-identity federation preferred.
Smallest call that produces a real resource β a secure storage account:
module "storage_account" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Storage/storageAccounts@2025-06-01"
name = "stcaseyexample001"
parent_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-casey"
location = "eastus2"
body = {
sku = { name = "Standard_LRS" }
kind = "StorageV2"
properties = {
minimumTlsVersion = "TLS1_2"
allowBlobPublicAccess = false
allowSharedKeyAccess = false
publicNetworkAccess = "Disabled"
supportsHttpsTrafficOnly = true
}
}
tags = { environment = "prod", owner = "platform" }
}
# The CALLER configures the provider + auth (this module does not):
provider "azapi" {
enable_preflight = true
disable_default_output = true
}
β οΈ Pin the module by tag (?ref=v1.0.0), never a branch. Tags are immutable once consumers reference them; a branch can change under you.
| Input | Type | Source |
|---|---|---|
parent_id |
string (scope ARM ID) |
an RG / parent-resource module, or provider::azapi::resource_group_resource_id(...) |
identity.identity_ids |
list(string) |
a user-assigned managed identity module |
body.properties.encryption⦠(CMK) |
string (Key Vault key URI) |
a Key Vault module |
| Output | Description | Consumed by |
|---|---|---|
id |
ARM resource ID of the managed resource | role assignments, private endpoints, diagnostics, child parent_id |
name |
Resource name (may be provider-computed) | data-plane config, app settings |
principal_id |
System-assigned identity principal (null if none) |
role assignments for the resource's MI |
tenant_id |
System-assigned identity tenant (null if none) |
cross-tenant scenarios |
output |
Opt-in read-back of behavior.response_export_values ({} by default) |
downstream config referencing computed props |
1 Β· Minimal β a resource group
module "resource_group" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Resources/resourceGroups@2024-11-01"
name = "rg-casey-example"
parent_id = "/subscriptions/00000000-0000-0000-0000-000000000000"
location = "eastus2"
tags = { environment = "example", managed_by = "terraform" }
}βΉοΈ For
Microsoft.Resources/resourceGroups,parent_idis the subscription ID. It may even be omitted β it then defaults to the provider's subscription.
2 Β· Secure storage account (the hardened default body)
module "storage" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Storage/storageAccounts@2025-06-01"
name = "stcaseydata001"
parent_id = module.resource_group.id
location = "eastus2"
body = {
sku = { name = "Standard_LRS" }
kind = "StorageV2"
properties = {
minimumTlsVersion = "TLS1_2" # never below
supportsHttpsTrafficOnly = true
allowBlobPublicAccess = false # no anonymous blobs
allowSharedKeyAccess = false # Entra-only auth
publicNetworkAccess = "Disabled" # private endpoints only
networkAcls = { defaultAction = "Deny" }
}
}
}π This is the baseline: TLS 1.2, HTTPS-only, no public/anonymous access, shared-key auth off, network default-deny. Open each up deliberately, one property at a time.
3 Β· Key Vault β RBAC, purge protection, network deny
module "key_vault" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.KeyVault/vaults@2024-11-01"
name = "kv-casey-prod"
parent_id = module.resource_group.id
location = "eastus2"
body = {
properties = {
tenantId = "72f988bf-0000-0000-0000-2d7cd011db47"
sku = { family = "A", name = "standard" }
enableRbacAuthorization = true # RBAC, not access policies
enableSoftDelete = true
enablePurgeProtection = true
publicNetworkAccess = "Disabled"
networkAcls = { defaultAction = "Deny", bypass = "AzureServices" }
}
}
}4 Β· System-assigned identity β feed principal_id into RBAC
module "app" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Web/sites@2024-04-01"
name = "app-casey-orders"
parent_id = module.resource_group.id
location = "eastus2"
identity = { type = "SystemAssigned" }
body = {
properties = { httpsOnly = true }
}
}
# principal_id is NOT sensitive β RBAC needs a plain value.
module "kv_reader" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-authorization-roleassignment?ref=v1.0.0"
scope = module.key_vault.id
role = "Key Vault Secrets User"
principal_id = module.app.principal_id
}5 Β· User-assigned identity
module "app" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Web/sites@2024-04-01"
name = "app-casey-uami"
parent_id = module.resource_group.id
location = "eastus2"
identity = {
type = "UserAssigned"
identity_ids = [module.uami.id] # from a user-assigned identity module
}
body = { properties = { httpsOnly = true } }
}βΉοΈ
identity.typeis validated:SystemAssigned|UserAssigned|SystemAssigned, UserAssigned|None.identity_idsis required when the type includesUserAssigned.
6 Β· Write-only secret via sensitive_body
module "sql_server" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Sql/servers@2023-08-01-preview"
name = "sql-casey-prod"
parent_id = module.resource_group.id
location = "eastus2"
body = {
properties = {
administratorLogin = "caseyadmin"
minimalTlsVersion = "1.2"
publicNetworkAccess = "Disabled"
}
}
# The password is write-only β merge-patched at apply, NEVER stored in state.
sensitive_body = {
properties = {
administratorLoginPassword = var.sql_admin_password # mark this variable sensitive in the caller
}
}
}π Secrets go in
sensitive_body, never inbody. Withignore_missing_property = true(the default), the un-echoed password does not show as drift.
7 Β· Read back a non-secret value via response_export_values
module "storage" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Storage/storageAccounts@2025-06-01"
name = "stcaseyexport001"
parent_id = module.resource_group.id
location = "eastus2"
body = { sku = { name = "Standard_LRS" }, kind = "StorageV2" }
behavior = {
# JMESPath map β export ONLY non-secret read-only props.
response_export_values = {
blob_endpoint = "properties.primaryEndpoints.blob"
}
}
}
output "blob_endpoint" {
value = module.storage.output.blob_endpoint
}
β οΈ Never export a secret path here βoutputlands in state.azapi_resourcehas no sensitive read-back channel; for secret results usetf-mod-azapi-resource-action.
8 Β· Serialize risky ops with behavior.locks
module "subnet" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Network/virtualNetworks/subnets@2024-05-01"
name = "snet-apps"
parent_id = module.vnet.id
body = { properties = { addressPrefix = "10.10.1.0/24" } }
behavior = {
# Avoid parallel subnet writes racing on the same vnet.
locks = [module.vnet.id]
}
}9 Β· Transient-error retry policy
module "resource" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.OperationalInsights/workspaces@2023-09-01"
name = "law-casey"
parent_id = module.resource_group.id
location = "eastus2"
body = { properties = { sku = { name = "PerGB2018" } } }
behavior = {
retry = {
error_message_regex = ["ResourceGroupBeingDeleted", "TooManyRequests"]
interval_seconds = 10 # 1..120
max_interval_seconds = 120 # 1..300
}
}
}10 Β· Force replacement with replace_triggers_external_values
module "vm" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Compute/virtualMachines@2024-07-01"
name = "vm-casey"
parent_id = module.resource_group.id
location = "eastus2"
body = { properties = { hardwareProfile = { vmSize = var.vm_size } } }
# Replace the VM when the SKU changes (ARM treats vmSize as updatable, but you want a clean rebuild).
replace_triggers_external_values = [var.vm_size]
}βΉοΈ Set
replace_triggers_external_values = null("break glass") to disable the trigger.
11 Β· Subscription-scope resource (a budget)
module "budget" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Consumption/budgets@2023-11-01"
name = "budget-casey-monthly"
parent_id = "/subscriptions/00000000-0000-0000-0000-000000000000" # subscription scope
body = {
properties = {
category = "Cost"
amount = 5000
timeGrain = "Monthly"
timePeriod = { startDate = "2026-07-01T00:00:00Z" }
}
}
}βΉοΈ The deployment scope is the
parent_id: a subscription ID here, an RG ID elsewhere.
12 Β· Management-group-scope resource (a policy assignment)
module "mg_policy" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Authorization/policyAssignments@2024-04-01"
name = "require-tls12"
parent_id = "/providers/Microsoft.Management/managementGroups/casey-root" # MG scope
body = {
properties = {
displayName = "Require TLS 1.2 on storage"
policyDefinitionId = "/providers/Microsoft.Authorization/policyDefinitions/404c3081-a854-4457-ae30-26a93ef643f9"
enforcementMode = "Default"
}
}
}13 Β· Extension-scope resource (a resource lock)
module "delete_lock" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Authorization/locks@2020-05-01"
name = "no-delete"
parent_id = module.key_vault.id # EXTENSION scope: the resource being protected
body = {
properties = {
level = "CanNotDelete"
notes = "Prod Key Vault β deletion blocked by policy."
}
}
}βΉοΈ For an extension resource,
parent_idis the ID of the resource being extended.
14 Β· for_each at scale β many resources from a map
locals {
accounts = {
"stcaseyapp001" = "eastus2"
"stcaseyapp002" = "westus2"
"stcaseylogs001" = "eastus2"
}
}
module "storage" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
for_each = local.accounts
type = "Microsoft.Storage/storageAccounts@2025-06-01"
name = each.key
parent_id = module.resource_group.id
location = each.value
body = {
sku = { name = "Standard_LRS" }
kind = "StorageV2"
properties = {
minimumTlsVersion = "TLS1_2"
publicNetworkAccess = "Disabled"
}
}
}π‘
for_eachis applied by the caller on the module call β the module itself stays a single keystone. Map keys are the stable instance identity.
15 Β· ποΈ End-to-end composition (mandatory) β RG β storage β role assignment
# 1) Resource group (this module, subscription scope)
module "resource_group" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Resources/resourceGroups@2024-11-01"
name = "rg-casey-orders"
parent_id = "/subscriptions/00000000-0000-0000-0000-000000000000"
location = "eastus2"
}
# 2) Secure storage account with a system identity (this module, RG scope)
module "storage" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-resource?ref=v1.0.0"
type = "Microsoft.Storage/storageAccounts@2025-06-01"
name = "stcaseyorders001"
parent_id = module.resource_group.id # β wired from output
location = module.resource_group.location
identity = { type = "SystemAssigned" }
body = {
sku = { name = "Standard_LRS" }
kind = "StorageV2"
properties = {
minimumTlsVersion = "TLS1_2"
allowSharedKeyAccess = false
publicNetworkAccess = "Disabled"
}
}
}
# 3) Grant the storage identity a role on the vault (principal_id wired from output)
module "kv_secrets_user" {
source = "git::https://github.com/microsoftexpert/tf-mod-azapi-authorization-roleassignment?ref=v1.0.0"
scope = module.key_vault.id
role = "Key Vault Secrets User"
principal_id = module.storage.principal_id # β wired from output (not sensitive)
}ποΈ This is the contract in action:
idbecomes the next resource'sparent_id, andprincipal_idbecomes an RBAC principal. Outputs β inputs, in dependency order.
Core β type (required), name, parent_id, location.
Body β body (native HCL), sensitive_body (write-only secrets), sensitive_body_version.
Identity & metadata β identity, tags.
Behavior tail β behavior (validation/casing/missing/null/export/locks/list-controls/retry), timeouts.
Lifecycle / advanced β replace_triggers_external_values, replace_triggers_refs, request_options.
Full object schemas
type = string # "<Namespace>/<resource>@<api-version>"; validated ^.+/.+@.+$; resource-type portion force-new
name = optional(string) # force-new
parent_id = optional(string) # deployment scope (RG/sub/MG/tenant/extension/parent); force-new
location = optional(string)
body = optional(any, {}) # native HCL; camelCase ARM keys; NO secrets here
sensitive_body = optional(any) # sensitive = true; write-only; secrets only
sensitive_body_version = optional(map(string), {})
identity = optional(object({
type = string # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned" | None
identity_ids = optional(list(string)) # required when type includes UserAssigned
})) # default null
behavior = optional(object({
schema_validation_enabled = optional(bool, true) # keep true
ignore_casing = optional(bool, false)
ignore_missing_property = optional(bool, true) # provider default
ignore_null_property = optional(bool, false)
response_export_values = optional(any, []) # export NOTHING by default
locks = optional(list(string), [])
ignore_other_items_in_list = optional(list(string), [])
list_unique_id_property = optional(map(string), {})
retry = optional(object({
error_message_regex = list(string)
interval_seconds = optional(number, 10) # 1..120
max_interval_seconds = optional(number, 180) # 1..300
}))
}), {})
tags = optional(map(string), {}) # max 50
timeouts = optional(object({ create, read, update, delete = optional(string) })) # default null
replace_triggers_external_values = optional(any) # default null
replace_triggers_refs = optional(list(string), [])
request_options = optional(object({ # advanced per-operation headers/query params
create_headers/read_headers/update_headers/delete_headers = optional(map(string), {})
create_query_parameters/.../delete_query_parameters = optional(map(list(string)), {})
}), {})βΉοΈ
bodyis typedanyonly because this is the generic wrapper (the documented exception). A curated tier-2 module mirrors the body in a deeply-typedobject.
| Output | Description | Sensitive |
|---|---|---|
id |
Primary. ARM resource ID of the managed resource. | no |
name |
Resource name (may be provider-computed). | no |
output |
Opt-in read-back of behavior.response_export_values ({} by default). |
no β but may carry exported values; never export secret paths |
principal_id |
System-assigned identity principal (null if none). |
no |
tenant_id |
System-assigned identity tenant (null if none). |
no |
The generic-type exception, and when not to use it. This is the only azapi module that accepts
a caller-supplied type/api-version. That power is also its risk: there is no curated, deeply-typed
body contract for a specific resource. Prefer a curated tf-mod-azapi-<service>_<resource> when one
exists; reach for this wrapper for preview services, a newer api-version than azurerm exposes, or a
property azurerm does not surface. When you author a curated module, pin the type/api-version and
mirror the body in a typed object β do not copy the body = any relaxation.
Secrets stay out of state via sensitive_body. body and sensitive_body are merged into one
request at apply, but sensitive_body is a write-only argument β Terraform never persists it. Pair it
with sensitive_body_version (a { path β version } map) to re-send a rotated secret on demand, and
leave ignore_missing_property = true so the un-echoed value does not register as drift. Never place a
secret in body, an output, or an example.
Export control is the leak boundary. behavior.response_export_values defaults to [], so output
is an empty object and nothing read-only is pulled into state. Opt in per JMESPath path/map, and only for
non-secret values β azapi_resource has no sensitive read-back channel (that is what
tf-mod-azapi-resource-action + sensitive_response_export_values is for). In regulated contexts also set
disable_default_output = true on the provider as belt-and-suspenders.
parent_id is the deployment scope. Top-level resources take a scope ID (RG / subscription /
management group / tenant); a child resource takes its parent's ID; an extension resource takes the ID of
the resource it extends. Build it with the provider functions
(provider::azapi::resource_group_resource_id, subscription_resource_id, β¦) where it improves clarity.
Total renderer. main.tf is a pure projection of typed input onto the one azapi_resource.this β
dynamic for identity/timeouts, a null-guarded conditional for retry, and direct passthrough for
the behavior controls. An unset optional simply omits the argument; no null guard ever leaks to the
caller.
The empty call is the safe call.
| Concern | Secure default | Opt-out |
|---|---|---|
| Embedded validation | behavior.schema_validation_enabled = true |
set false (justify; only for an api-version the embedded schema lags) |
| Secrets | routed via sensitive_body (write-only, never in state) |
β (placing secrets in body is forbidden) |
| Missing-property drift | behavior.ignore_missing_property = true |
set false |
| Output exposure | behavior.response_export_values = [] (export nothing) |
pass explicit paths / a JMESPath map |
| Casing drift | behavior.ignore_casing = false |
set true |
| Concurrency | behavior.locks = [] available |
populate with ARM IDs |
| Identity | managed identity via identity (no embedded keys) |
β |
| Body shape | native HCL body (never jsonencode) |
β |
Documented generic-wrapper exceptions:
type/api-version are caller-supplied, andbodyis typedany. Both are intentional for this module only β never replicate them in a curated tier-2 module.
Authoring is plan-only (regulated-FI / PII / privacy-regulation posture) β a human applies from a controlled CI context:
cd tf-mod-azapi-resource
terraform init -backend=false # downloads azapi ~> 2.10 only; no remote state
terraform validate # type-checks variables.tf + main.tf
terraform fmt -check -recursive # style gate
# NO `terraform apply` here. plan/apply against an environment is a separate, human-reviewed CI step.
rm -rf.terraform.terraform.lock.hcl
β οΈ Consume the module pinned to a tag β?ref=v1.0.0, never a branch.
Offline proof gate (no cloud credentials required):
terraform init -backend=false # azapi ~> 2.10 only
terraform validate # "Success! The configuration is valid."
terraform fmt -check -recursive # zero formatting differencesβΉοΈ
terraform validateruns fully offline. BecausebodyisDynamic, much of the safety comes from the typedvariables.tfand the provider's embeddedschema_validation_enabledβ the latter only evaluates the body fully at plan against a real api-version.
id = "/subscriptions/.../resourceGroups/rg-casey/providers/Microsoft.Storage/storageAccounts/stcaseydata001"
name = "stcaseydata001"
principal_id = "00000000-0000-0000-0000-000000000000" # system identity, when enabled
output = {} # nothing exported by default| Symptom | Cause | Fix |
|---|---|---|
Body key rejected at plan |
schema_validation_enabled caught an unknown/malformed key for your api-version |
Fix the key (camelCase, correct nesting). Don't reflexively disable validation. |
| A secret/PII value landed in state or an output | You exported it via behavior.response_export_values |
Set it back to []; for secret read-backs use tf-mod-azapi-resource-action + sensitive_response_export_values |
| Secret shows as drift | Secret placed in body, or ignore_missing_property = false |
Move it to sensitive_body; keep ignore_missing_property = true |
Wrong scope / parent_id |
Top-level takes a scope ID; child takes the parent's ID; extension takes the target's ID | Build it with the provider functions |
| Re-apply wants to replace the resource | You changed name, parent_id, or the resource-type portion of type (all force-new) |
Expected. Changing only the api-version usually does not replace. |
type must be in the form... |
type failed the ^.+/.+@.+$ validation |
Use Microsoft.X/y@YYYY-MM-DD |
- Terraform azapi provider β
azapi_resourceresource reference - Azure Resource Manager template reference β
learn.microsoft.com/azure/templates/<RP>/<resource>for the type you target - azapi provider functions β
resource_group_resource_id,subscription_resource_id,build_resource_id, β¦ - Sibling modules β
tf-mod-azapi-update-resource,tf-mod-azapi-resource-action,tf-mod-azapi-data-plane-resource SCOPE.md(this module) β the authoritative cross-module contract
π "Infrastructure as Code should be standardized, consistent, and secure."