Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Deployment Script (Azure PowerShell) Terraform Module

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.

Terraform azurerm Module Type Resources Posture


🧩 Overview

  • πŸ–₯️ 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_variable name 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.


❀️ Support this project

If this module saved you time:


πŸ—ΊοΈ Where this fits in the family

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
Loading

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.


🧬 What this module builds

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
Loading

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.

βœ… Provider / Versions

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:

  • πŸ”΄ tags is 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_variable set hash is strings.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_value nor storage_account.key is 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.
  • ⚠️ ExactlyOneOf binds primary_script_uri and script_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.
  • ⚠️ The name regex is looser than its own error message. Its character class contains an unintended range that also admits *, + and ,. Azure publishes no naming rule for deploymentScripts, so this module follows the documented set β€” its own judgement, stated as such.
  • ⚠️ timeout defaults to P1D, 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 a type field with one legal value.
  • ⚠️ outputs is documented as "List of script outputs" but the schema declares a plain string. jsondecode it.
  • βœ… cleanup_preference already defaults to the closed choice, Always. This module keeps it rather than diverging.

πŸ”‘ Required Azure RBAC Roles / Permissions

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.


Azure Prerequisites

  • Microsoft.Resources registered, plus Microsoft.ContainerInstance and Microsoft.Storage for 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_value and any storage key land in state in plaintext.
  • Quota for a container instance in the target region. A script cannot start without one.

πŸ“ Module Structure

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

βš™οΈ Quick Start

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.


πŸ”Œ Cross-Module Contract

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

πŸ“š Example Library

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
}

⚠️ outputs is a string, despite the provider documenting it as a list. jsondecode it 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 as module.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_value for 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 through environment_variable_names so a plan can still show what changed. Values are never restored.

⚠️ Every entry must supply exactly one of value and secure_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 of primary_script_uri and script_content may be supplied. The provider declares ExactlyOneOf between 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_remote reports 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_tag is 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 outputs before 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_failure reports any value other than Always, which is worth asserting false in 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_duration is how long the script may run. retention_interval is how long Azure keeps the record afterwards, after which it is deleted along with its outputs. timeouts.create is how long Terraform waits β€” it must comfortably exceed timeout_duration.

πŸ’‘ timeout_duration defaults to P1D, the maximum. uses_maximum_timeout reports 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-account deliberately 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 check could read it.

⚠️ Note command_line is not redacted either, so a secret passed as an argument is exposed in a way the same secret in secure_value is 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 Reader on the resource group, and only then handed to the script. The script's power is that role assignment, not its text.

⚠️ Terraform orders script_permissions before inventory_script only because of the module.script_identity reference 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.id is used as the role-assignment scope while module.rg.name goes to the script, which is the usual split: assignments take IDs, and this resource takes a name.


πŸ“₯ Inputs

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.

🧾 Outputs

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.


🧠 Architecture Notes

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.


🧱 Design Principles

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.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0, never a branch. This library is plan-only: a human applies from CI.


πŸ§ͺ Testing

What the offline gate covers:

  • terraform validate proves the configuration parses and every type is satisfied.
  • terraform fmt -check proves the HCL is canonically formatted.
  • Feeding deliberately bad .tfvars through terraform console fires 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.

πŸ’¬ Example Output

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

πŸ” Troubleshooting

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

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."