Runs an Azure PowerShell script as part of a deployment, in a container instance Azure creates and then cleans up, and returns whatever the script emits. Targets
hashicorp/azurerm ~> 4.0.
- π₯οΈ Runs an Azure PowerShell script during a deployment, in a transient container instance.
- πͺͺ Executes as one or more user-assigned managed identities β which is the real blast radius, not the script text.
- π€ Returns the script's outputs as a JSON string.
- π Marks the environment-variable set and the storage-account block sensitive, and unwraps the reviewable parts back out.
- π§ Rejects the
environment_variablename collisions the provider's set hash silently resolves in your favour or against it. - π§Ή Defaults to omitting your storage account entirely, so no storage key exists in configuration or state.
π‘ Why it matters: this is the one resource in the library that executes code you wrote. Everything else declares desired state; this runs a shell. That changes what review means β a change here is an execution, and the identity it runs as decides what that execution can reach.
If this module saved you time:
- β Star the repository β it genuinely helps other people find it.
- πΌ Connect on LinkedIn β linkedin.com/in/microsoftexpert
- β Buy me a coffee β buymeacoffee.com/microsoftexpert
flowchart TB
RG["terraform-azurerm-resource-group"]
UAI["terraform-azurerm-user-assigned-identity"]
RA["terraform-azurerm-role-assignments"]
SA["terraform-azurerm-storage-account"]
subgraph scripts["The two deployment-script modules, one per shell"]
CLI["terraform-azurerm-resource-deployment-script-azure-cli"]
PS["terraform-azurerm-resource-deployment-script-azure-power-shell"]
end
TARGET["whatever the script touches"]
RG -->|"resource_group_name"| CLI
RG -->|"resource_group_name"| PS
RG -->|"resource_group_name"| UAI
UAI -->|"identity_ids: the script runs as this"| CLI
UAI -->|"identity_ids: the script runs as this"| PS
UAI -->|"principal_id"| RA
RA -->|"grants the permissions the script will actually have"| TARGET
SA -.->|"optional: supplying a key is the WEAKER choice"| CLI
SA -.->|"optional: supplying a key is the WEAKER choice"| PS
CLI -->|"runs arbitrary code against"| TARGET
PS -->|"runs arbitrary code against"| TARGET
style CLI fill:#0078D4,stroke:#004578,color:#ffffff
style PS fill:#0078D4,stroke:#004578,color:#ffffff
style UAI fill:#004578,stroke:#004578,color:#ffffff
style RG fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style RA fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style SA fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style TARGET fill:#F3F2F1,stroke:#8A8886,color:#201F1E
The resource group and the managed identity are the two real upstreams. Note the storage-account edge is dotted and optional in the weaker direction: supplying your own account means supplying its key, and omitting the block is the stronger default.
flowchart TB
SCRIPT["script_content OR primary_script_uri, exactly one"]
VER["version_number: one of only two places the twins differ"]
IDIN["identity_ids: the real blast radius"]
ENVIN["environment_variable set, SENSITIVE as a whole"]
SAIN["storage_account, SENSITIVE, and better omitted"]
THIS["azurerm_resource_deployment_script_azure_power_shell.this"]
ACI["a container instance plus a file share, created and then cleaned up"]
OUT["outputs: whatever the script wrote, NOT redacted"]
FACTS["names, counts and posture flags, unwrapped"]
TAGS["tags: the only field that can ever be updated"]
SCRIPT --> THIS
VER --> THIS
IDIN -->|"the script runs with THESE permissions, not yours"| THIS
ENVIN -->|"names unwrapped back out; values never"| THIS
SAIN -.->|"omit it and Azure makes a temporary one, so no key exists"| THIS
THIS --> ACI
THIS --> OUT
THIS --> FACTS
TAGS --> THIS
style THIS fill:#0078D4,stroke:#004578,color:#ffffff
style ACI fill:#004578,stroke:#004578,color:#ffffff
style ENVIN fill:#8A2B06,stroke:#5C1D04,color:#ffffff
style SAIN fill:#8A2B06,stroke:#5C1D04,color:#ffffff
style IDIN fill:#8A2B06,stroke:#5C1D04,color:#ffffff
style SCRIPT fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style VER fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style OUT fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style FACTS fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style TAGS fill:#F3F2F1,stroke:#8A8886,color:#201F1E
This diagram is shared with terraform-azurerm-resource-deployment-script-azure-cli, deliberately and with no distinction invented. The two resources are implemented from a single file upstream and their schemas are byte-identical; only the version_number grammar and the script's output convention differ, and both are called out where they apply.
| Resource | Cardinality | Notes |
|---|---|---|
azurerm_resource_deployment_script_azure_power_shell.this |
single, named this |
Named by you, so many may exist per resource group. |
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module. The caller configures provider "azurerm" { features {} }, auth and subscription. |
Schema notes that bite, each confirmed against the provider's own source at the pinned version:
- π΄
tagsis the only mutable field. The update path decodes a patch model containing tags and nothing else; every other argument is force-new, so every other change replaces the resource and re-runs the script. - π΄ The
environment_variableset hash isstrings.ToLower(name)and nothing else. Two entries whose names differ only in case collide and one is silently discarded. This module rejects that. - π΄ Neither
secure_valuenorstorage_account.keyis read back from Azure. The provider repopulates both from prior state, matching secure values by variable name β so renaming a variable silently drops its secure value. β οΈ ExactlyOneOfbindsprimary_script_uriandscript_content, and the binary schema does not carry that fact. It is enforced upstream regardless; this module re-expresses it to fail earlier and in its own words.β οΈ Thenameregex is looser than its own error message. Its character class contains an unintended range that also admits*,+and,. Azure publishes no naming rule fordeploymentScripts, so this module follows the documented set β its own judgement, stated as such.β οΈ timeoutdefaults toP1D, the maximum. A hung script holds a container instance for a full day and is billed for it.β οΈ Identity is user-assigned only βcommonschema.UserAssignedIdentityOptionalForceNew(). There is no system-assigned option, so this module takes the IDs directly rather than atypefield with one legal value.β οΈ outputsis documented as "List of script outputs" but the schema declares a plain string.jsondecodeit.- β
cleanup_preferencealready defaults to the closed choice,Always. This module keeps it rather than diverging.
| Action | Role | Scope |
|---|---|---|
| Create / delete the deployment script | Contributor, or a custom role with Microsoft.Resources/deploymentScripts/* |
The resource group |
| Create the transient container instance and file share | Contributor on Microsoft.ContainerInstance and Microsoft.Storage |
The resource group |
| Assign the script's identity | Managed Identity Operator | The user-assigned identity |
Read it for plan |
Reader | The resource group |
π΄ The permission that matters most is not in this table. The script runs with the permissions of the identities in identity_ids, and those are granted elsewhere β by a role assignment on whatever the script touches. A reviewer approving a change to this module is approving an execution with that identity's rights, so the script text and the identity's role assignments have to be read together. Neither half is sufficient alone.
β Plan access is not credential access. Neither secret on this resource is fetched from Azure during a refresh.
π The exposure is the state file, which no Azure role governs.
Microsoft.Resourcesregistered, plusMicrosoft.ContainerInstanceandMicrosoft.Storagefor the transient resources the service creates.- A region that supports Azure Container Instances.
- A user-assigned managed identity, if the script needs to touch Azure at all, with role assignments granting exactly what it needs.
- π An encrypted, access-controlled remote backend, as a stated prerequisite β
secure_valueand any storage key land in state in plaintext. - Quota for a container instance in the target region. A script cannot start without one.
terraform-azurerm-resource-deployment-script-azure-power-shell/
βββ providers.tf # required_version + pinned azurerm; no provider block
βββ variables.tf # 5 required inputs, 13 optional, 39 validations
βββ main.tf # the keystone `this` + dynamic blocks + derived locals
βββ outputs.tf # 25 outputs; id first; none sensitive
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore # the canonical library ignore set
provider "azurerm" {
features {}
}
module "migration_script" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-deployment-script-azure-power-shell.git?ref=v1.0.0"
name = "run-migration"
resource_group_name = module.rg.name
location = "eastus2"
version_number = "9.7"
retention_interval = "PT6H"
script_content = "$DeploymentScriptOutputs = @{ status = 'ok' }"
}The caller configures the provider, its authentication and the mandatory features {} block. The module declares none of them.
Consumes
| Input | Type | Source |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group β name |
location |
string |
Any region supporting Azure Container Instances |
identity_ids |
list(string) |
terraform-azurerm-user-assigned-identity β id |
storage_account |
object (sensitive) |
Out of band. Prefer omitting it. |
Emits
| Output | Description |
|---|---|
id |
The deployment script's Resource ID |
outputs |
What the script produced, as a JSON string |
environment_variable_names, secure_environment_variable_names |
Which variables were passed, and which securely |
identity_ids, identity_count, runs_with_identity |
What the script ran as |
uses_own_storage_account, storage_account_name |
Whether a caller key was involved |
uses_maximum_timeout, retains_scratch_after_failure |
Posture flags for a check block |
| 4 constant facts | Tags are the only mutable field; a re-run is a replacement; the identity is the blast radius; the secrets come from state |
1 Β· The smallest real call
module "hello" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-deployment-script-azure-power-shell.git?ref=v1.0.0"
name = "hello"
resource_group_name = module.rg.name
location = "eastus2"
version_number = "9.7"
retention_interval = "PT1H"
script_content = "$DeploymentScriptOutputs = @{ greeting = 'hello' }"
}π No identity, so the script can compute but cannot touch Azure. No storage account, so no key exists anywhere. That is the intended starting point.
2 Β· Reading the script's output
output "greeting" {
value = jsondecode(module.hello.outputs).greeting
}
β οΈ outputsis a string, despite the provider documenting it as a list.jsondecodeit before indexing.π It is not redacted. Whether that is safe depends on what your script writes β the module cannot know. Write outputs that identify things, not outputs that authenticate to them.
3 Β· Running as a managed identity
module "tagger" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-deployment-script-azure-power-shell.git?ref=v1.0.0"
name = "bulk-tagger"
resource_group_name = module.rg.name
location = "eastus2"
version_number = "9.7"
retention_interval = "PT2H"
identity_ids = [module.script_identity.id]
script_content = "Get-AzResource -Tag @{ env = 'dev' } | Select-Object -ExpandProperty ResourceId"
}π΄ This is the line that decides what the script can do.
Get-Az*cmdlets inside the body run asmodule.script_identity, with whatever roles that identity has been granted β not with your permissions, and not limited by the script text.
4 Β· Passing configuration and a secret
environment_variable = [
{ name = "TARGET_ENV", value = "staging" },
{ name = "API_TOKEN", secure_value = var.api_token },
]π Use
secure_valuefor anything secret. The whole set is marked sensitive because one field inside it is β so the variable names are redacted too, and the module restores them throughenvironment_variable_namesso a plan can still show what changed. Values are never restored.
β οΈ Every entry must supply exactly one ofvalueandsecure_value. Both are optional upstream, so an entry with neither parses cleanly and produces an empty variable; this module rejects it.
5 Β· The case-collision the provider resolves silently
# REJECTED by this module:
environment_variable = [
{ name = "PATH", value = "/a" },
{ name = "path", value = "/b" },
]π§ The provider's set hash is
strings.ToLower(name)and ignores both value fields entirely. Two entries whose names differ only in case land in the same bucket and one is silently discarded β no error, no plan difference to notice. This module rejects the collision at parse time.
6 Β· Fetching the script from a URI instead
primary_script_uri = "https://raw.githubusercontent.com/contoso/scripts/v3/migrate.ps1"
supporting_script_uris = [
"https://raw.githubusercontent.com/contoso/scripts/v3/lib.ps1",
]
β οΈ Exactly one ofprimary_script_uriandscript_contentmay be supplied. The provider declaresExactlyOneOfbetween them; this module re-expresses the rule so the error names your inputs and arrives earlier. It does not restore a check that was missing β the upstream one fires either way.π
https://only, which is this module's own rule: the fetched script is executed with your identity's permissions, so fetching it over an unencrypted channel invites tampering in transit.βΉοΈ
script_is_remotereports this, because a remote script's content is not visible in the plan β what runs is decided outside your configuration.
7 Β· Forcing a re-run
force_update_tag = "2026-08-01T09:00:00Z"
β οΈ There is no "run again" operation.force_update_tagis itself force-new, so changing it destroys and recreates the resource β which is how a re-run is expressed here.π‘ A re-run destroys the previous
outputsbefore producing new ones, so anything reading them sees the new values in the same apply.
8 Β· Keeping the container on failure, to debug
cleanup_preference = "OnSuccess"
β οΈ Useful while developing: a failed run leaves the container instance so you can read its logs. It also leaves the script body and its environment variables in place until the retention interval expires.π‘
retains_scratch_after_failurereports any value other thanAlways, which is worth assertingfalsein production.
9 Β· Bounding how long the script may run
timeout_duration = "PT30M"
retention_interval = "PT6H"
timeouts = {
create = "45m"
}
β οΈ Three different clocks, and they are easy to confuse.timeout_durationis how long the script may run.retention_intervalis how long Azure keeps the record afterwards, after which it is deleted along with its outputs.timeouts.createis how long Terraform waits β it must comfortably exceedtimeout_duration.π‘
timeout_durationdefaults toP1D, the maximum.uses_maximum_timeoutreports that, because a hung script otherwise bills a container instance for a day.
10 Β· Supplying your own storage account, and why not to
storage_account = {
name = "mydeployscripts"
key = var.storage_account_key
}π΄ Omitting this block is the stronger position and is the default. Leave it out and Azure creates a temporary account for the run, so no storage key exists in your configuration or your state at all β categorically better than protecting a key well.
π΄ A storage account key grants full control of every blob, file, queue and table in that account, with no per-container scoping and nothing in Azure RBAC limiting it. Note also that
terraform-azurerm-storage-accountdeliberately does not emit access keys, so this value cannot come from the sibling module β which is a hint, not an obstacle.βΉοΈ Supply it when policy forbids the service creating storage, or when the share must sit inside a network boundary you control.
11 Β· Naming the transient container group
container = {
container_group_name = "cg-migration-runner"
}βΉοΈ Only useful when a policy or a naming standard needs to recognise the transient container. Omit it and Azure generates a name.
12 Β· Enforcing posture with a `check` block
check "deployment_script_posture" {
assert {
condition = !module.migration_script.uses_own_storage_account
error_message = "This deployment script supplies a storage account key; let Azure create a temporary account instead."
}
assert {
condition = !module.migration_script.retains_scratch_after_failure
error_message = "cleanup_preference is not Always, so the script body and its variables outlive the run."
}
assert {
condition = module.migration_script.runs_with_identity
error_message = "This script has no managed identity, so any Azure call inside it will fail."
}
}π‘ The first two pass under this module's defaults, so the check only speaks up when somebody opts out.
13 Β· Asserting a specific variable is passed securely
check "token_is_secure" {
assert {
condition = contains(module.migration_script.secure_environment_variable_names, "API_TOKEN")
error_message = "API_TOKEN must be passed as secure_value, not as a plain value or on the command line."
}
}π‘ This works precisely because the names are unwrapped back out of the sensitive collection. Without that, the whole set would be redacted and no
checkcould read it.
β οΈ Notecommand_lineis not redacted either, so a secret passed as an argument is exposed in a way the same secret insecure_valueis not.
14 Β· What a change replaces
# Updates in place -- the only thing that does:
tags = { owner = "platform" }
# Everything else REPLACES the resource, which re-runs the script:
script_content = "..."
version_number = "9.8"
identity_ids = [var.replacement_identity_id]
timeout_duration = "PT10M"π΄ The provider's update path carries tags and nothing else. There is no such thing as editing a deployment script: treat every apply that touches a non-tag field as an execution, not an edit.
15 Β· ποΈ End-to-end composition
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-platform-tools"
location = "eastus2"
}
module "script_identity" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
name = "id-deployment-script"
resource_group_name = module.rg.name
location = "eastus2"
}
module "script_permissions" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.rg.id
role_assignments = {
reader = {
principal_id = module.script_identity.principal_id
role_definition_name = "Reader"
principal_type = "ServicePrincipal"
}
}
}
module "inventory_script" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-deployment-script-azure-power-shell.git?ref=v1.0.0"
name = "rg-inventory"
resource_group_name = module.rg.name
location = "eastus2"
version_number = "9.7"
retention_interval = "PT6H"
timeout_duration = "PT10M"
identity_ids = [module.script_identity.id]
environment_variable = [
{ name = "TARGET_RG", value = module.rg.name },
]
script_content = <<-SCRIPT
$count = (Get-AzResource -ResourceGroupName $env:TARGET_RG).Count
$DeploymentScriptOutputs = @{ count = $count }
SCRIPT
}
output "resource_count" {
value = jsondecode(module.inventory_script.outputs).count
}ποΈ Read the dependency chain in the order that matters: the identity is created, granted
Readeron the resource group, and only then handed to the script. The script's power is that role assignment, not its text.
β οΈ Terraform ordersscript_permissionsbeforeinventory_scriptonly because of themodule.script_identityreference they share β not because it understands that the script needs the role. If the assignment is slow to propagate, the first run can still fail with an authorization error; re-applying is the fix.π‘
module.rg.idis used as the role-assignment scope whilemodule.rg.namegoes to the script, which is the usual split: assignments take IDs, and this resource takes a name.
| Name | Type | Required | Summary |
|---|---|---|---|
name |
string |
yes | 1β260 chars. Force-new. |
resource_group_name |
string |
yes | Force-new. |
location |
string |
yes | Must support Azure Container Instances. Force-new. |
version_number |
string |
yes | Azure PowerShell version, X.Y. Force-new. |
retention_interval |
string |
yes | ISO 8601, PT1HβP1DT2H. Force-new. |
script_content |
string |
one of | The script body, inline. Force-new. |
primary_script_uri |
string |
one of | The script by URI, https only. Force-new. |
supporting_script_uris |
list(string) |
no | Additional files, https only. |
command_line |
string |
no | Arguments. Not redacted. |
cleanup_preference |
string |
no | Always (default) / OnSuccess / OnExpiration. |
timeout_duration |
string |
no | How long the script may run. Defaults to the maximum P1D. |
force_update_tag |
string |
no | Change it to force a re-run β which replaces the resource. |
container |
object |
no | Names the transient container group. |
environment_variable |
set(object) |
no | π Sensitive as a whole. |
identity_ids |
list(string) |
no | User-assigned identities the script runs as. |
storage_account |
object |
no | π Sensitive as a whole. Better omitted. |
tags |
map(string) |
no | The only mutable field. |
timeouts |
object |
no | How long Terraform waits. |
| Output | Description |
|---|---|
id, name, resource_group_name, location |
The resource, as stored. |
outputs |
The script's JSON output string. Not redacted. |
has_outputs |
Whether the script produced anything. |
script_is_inline, script_is_remote |
Whether what runs is visible in the plan. |
supporting_script_count |
How many extra files are fetched. |
environment_variable_names |
Names, unwrapped from the sensitive set. |
secure_environment_variable_names |
Which of them are secure values. |
environment_variable_count, secure_variable_count, uses_secure_variables |
Counts and a flag. |
runs_with_identity, identity_count, identity_ids |
What the script runs as. |
storage_account_name, uses_own_storage_account |
Whether a caller key was involved. |
uses_maximum_timeout |
Whether the timeout is the default maximum. |
retains_scratch_after_failure |
Whether scratch resources outlive the run. |
tags_are_the_only_mutable_field |
Constant true. |
rerunning_is_expressed_as_replacement |
Constant true. |
script_runs_with_the_identity_permissions_not_yours |
Constant true. |
secrets_are_carried_forward_from_state_not_read_from_azure |
Constant true. |
No output is sensitive. No secret is emitted: both secrets on this resource are inputs the caller already holds.
This resource executes code, and that reframes what a plan means. Everywhere else in this library a plan describes desired state. Here a non-tag change is an execution: the provider's update path carries tags and nothing else, so altering the script, its version, its variables, its identity or its timeout replaces the resource and runs the script again. There is no edit, and force_update_tag β the field whose whole purpose is triggering a re-run β is itself force-new. Reviewers should read a diff on this module as "we are about to run this", not "we are about to change this".
The identity is the blast radius, and it is granted somewhere else. The script runs with the permissions of whatever is in identity_ids, which this module consumes but does not create or empower. A change here and a role assignment over there combine into the actual capability, and neither file shows the whole picture. That is why the module emits identity_ids and identity_count β so a policy check can at least assert the script runs as an approved identity β and why the end-to-end example puts the role assignment in the same frame.
Two secrets, both inputs, neither read back. environment_variable.secure_value and storage_account.key are supplied by the caller and never returned by Azure; the provider repopulates them from prior state on every read. The trade cuts both ways and both directions are worth stating: plan access is not credential access, because a refresh fetches neither from Azure; and drift is invisible, because nothing will ever notice if either changes outside Terraform. There is a sharper corollary in the flatten: secure values are matched to variables by name, so renaming a variable silently drops its secure value.
Collection-level sensitivity costs different amounts in different places, and both cases appear here. Marking environment_variable protects the secure values but redacts the variable names and plain values with them, so the names are unwrapped back out β the reviewable part restored, the secrets left alone. Marking storage_account protects the key but redacts the account name, which is public, so that is unwrapped too. In both cases the unwrap needs two levels: each element drawn from a sensitive collection carries a mark, and the list a comprehension builds from those elements carries another that propagates into length(). A third trap sits alongside them β var.storage_account != null is itself a sensitive bool, and a ternary's condition contaminates its result, so even a plain presence flag needs unwrapping.
The defaults were examined one at a time rather than flipped as a group. cleanup_preference already defaults to the closed choice and is left alone. timeout_duration defaults to the maximum, which is genuinely unhelpful, but lowering it would silently kill legitimate long-running scripts and the risk is cost rather than exposure β so it is reported through uses_maximum_timeout instead of changed. storage_account defaults to absent, which is the strongest available position because it means no key exists at all rather than a key protected well.
Three clocks, easily confused. timeout_duration bounds the script, retention_interval bounds how long Azure keeps the record β deleting it, and its outputs, when it expires β and timeouts.create bounds how long Terraform waits. The last must exceed the first, or Terraform gives up while the script is still running.
| Concern | This module's position | Why |
|---|---|---|
| Caller storage account | Omitted by default | Azure creates a temporary one, so no storage key exists in configuration or state. Stronger than protecting a key. |
| Storage key | Sensitive; name unwrapped | The key is a full data-plane credential for the whole account; the name is public and belongs in a plan. |
| Environment variables | Whole set sensitive; names unwrapped | One secure_value forces marking the collection; the names are the reviewable part and are restored. |
| Case-colliding variable names | Rejected | The provider's set hash lowercases the name, so one entry is silently discarded. |
| An entry with neither value | Rejected | Both fields are optional upstream, so it parses cleanly and produces an empty variable. |
| Script transport | https only |
This module's own rule: the fetched script executes with your identity's permissions. |
cleanup_preference |
Provider default Always kept |
Already the closed choice; diverging would add nothing. |
timeout_duration |
Provider default P1D kept, and reported |
A shorter default would kill legitimate scripts. Cost, not exposure β so report, don't decide. |
outputs |
Not marked sensitive | Matches the provider; its sensitivity depends on code the module cannot see, and that is said plainly rather than guessed. |
name character set |
The documented set, not the regex's | The provider's regex admits three extra characters through an unintended range. Stated as this module's judgement. |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0, never a branch. This library is plan-only: a human applies from CI.
What the offline gate covers:
terraform validateproves the configuration parses and every type is satisfied.terraform fmt -checkproves the HCL is canonically formatted.- Feeding deliberately bad
.tfvarsthroughterraform consolefires the input validations β all 39 validation blocks in this module have been proven reachable, each by a fixture that triggers it, with zero condition-evaluation errors. - All 14 derived locals have been driven to more than one value across the good fixtures, including both the sensitive-collection unwraps.
What only a real plan or apply can exercise:
- Whether the Azure PowerShell version exists β the module checks the shape and cannot know the catalogue.
- Whether the region has container-instance quota.
- Whether the script's identity holds the permissions the script assumes.
- The script itself, which is the part that actually matters and which no static check can evaluate.
id = "/subscriptions/.../resourceGroups/rg-platform-tools/providers/Microsoft.Resources/deploymentScripts/rg-inventory"
name = "rg-inventory"
location = "eastus2"
outputs = "{\"count\":17}"
has_outputs = true
script_is_inline = true
script_is_remote = false
environment_variable_names = ["TARGET_RG"]
secure_environment_variable_names = []
environment_variable_count = 1
secure_variable_count = 0
uses_secure_variables = false
runs_with_identity = true
identity_count = 1
storage_account_name = null
uses_own_storage_account = false
uses_maximum_timeout = false
retains_scratch_after_failure = false
tags_are_the_only_mutable_field = true
| Symptom | Cause | Fix |
|---|---|---|
A Get-Az* cmdlet inside the script fails with an authorization error |
The script runs as identity_ids, not as you |
Grant the identity the role it needs, on the thing it touches |
| Terraform plans a replacement for a change that looks trivial | Everything except tags is force-new |
Expected β a non-tag change re-runs the script |
| An environment variable silently disappeared | Two names differed only in case and the set hash collided | This module now rejects that at parse time |
| A secure value stopped working after a rename | Secure values are matched to variables by name on read | Treat a rename as re-supplying the secret |
| Terraform gives up while the script is still running | timeouts.create is shorter than timeout_duration |
Raise the Terraform timeout above the script timeout |
| A hung script billed a container instance for a day | timeout_duration defaults to the maximum P1D |
Set it explicitly; uses_maximum_timeout reports the default |
| The script and its variables were still there after a failure | cleanup_preference is not Always |
Expected while debugging; assert retains_scratch_after_failure is false in production |
outputs cannot be indexed |
It is a JSON string, not a list, despite the documentation | jsondecode it first |
| The resource and its outputs vanished | retention_interval expired and Azure deleted the record |
Read outputs in the same apply, or lengthen the interval |
A name with * was accepted by the provider but rejected here |
The provider's regex admits three characters its own message excludes | Use the documented set; this is a deliberate divergence |
azurerm_resource_deployment_script_azure_power_shellβ provider resource reference.- Deployment scripts in ARM templates β what the service does and how outputs are returned.
- Azure PowerShell image tags β the canonical version list.
- Sibling modules:
terraform-azurerm-resource-deployment-script-azure-power-shell,terraform-azurerm-user-assigned-identity,terraform-azurerm-role-assignments,terraform-azurerm-resource-group. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."