A standalone module that provisions a single hardened
azurerm_automation_account, targetinghashicorp/azurerm ~> 4.0. The empty call yields a private account with local keys disabled and a system-assigned managed identity, so runbooks authenticate under Azure RBAC rather than with stored credentials.
This module manages one Azure Automation Account and nothing else:
- 🔐 A single
azurerm_automation_accountnamedthis, secure by default. - 🪪 An inline managed
identity {}— system-assigned by default, user-assigned or both on request. - 🔑 Optional customer-managed key (CMK)
encryption {}wired to a Key Vault key. - 🚫
public_network_access_enabled = falseandlocal_authentication_enabled = falseon the empty call. - 🏷️ Full
tagssupport and optionaltimeouts.
The account's child assets — runbooks, schedules, variables, credentials, connections, modules, DSC configurations, hybrid worker groups, and webhooks — are separate modules in this suite. They consume this module's id and hybrid_service_url; this module deliberately does not own them, keeping its lifecycle clean and its blast radius small.
💡 Why it matters: An Automation Account is a control-plane foothold that can run code against your subscription. Shipping it with local keys off, public access off, and a managed identity on means the risky posture is something a caller has to type on purpose — not something they inherit by forgetting to harden a default.
If this module saves you time, please consider supporting it:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
kv["terraform-azurerm-key-vault"]
pe["terraform-azurerm-private-endpoint"]
ra["terraform-azurerm-role-assignments"]
rb["terraform-azurerm-automation-runbook"]
sc["terraform-azurerm-automation-schedule"]
va["terraform-azurerm-automation-variable-string and its typed siblings"]
hw["terraform-azurerm-automation-hybrid-runbook-worker"]
aa["terraform-azurerm-automation-account"]
rg -->|"resource_group_name"| aa
kv -->|"CMK key id"| aa
pe -->|"private access"| aa
aa -->|"identity_principal_id"| ra
aa -->|"id"| rb
aa -->|"id"| sc
aa -->|"id"| va
aa -->|"hybrid_service_url"| hw
classDef self fill:#0078D4,color:#fff,stroke:#004578,stroke-width:1px;
classDef neutral fill:#eef2f6,color:#1b1f23,stroke:#c7d0d9,stroke-width:1px;
class aa self;
class rg,kv,pe,ra,rb,sc,va,hw neutral;
Upstream siblings feed the account its resource group, an optional CMK, and optional private-link reachability; downstream siblings attach automation assets and grant the account's identity the roles its runbooks need.
flowchart LR
subgraph inputs["Caller inputs"]
ident["identity { type, identity_ids }"]
enc["encryption { key_vault_key_id }"]
flags["local_auth=false / public_access=false"]
sku["sku_name (Basic|Free)"]
end
aa["azurerm_automation_account.this"]
subgraph outputs["Outputs"]
oid["id / name"]
odsc["dsc_server_endpoint"]
ohyb["hybrid_service_url"]
opid["identity_principal_id"]
end
ident -->|"managed identity"| aa
enc -->|"CMK wrap"| aa
flags -->|"secure defaults"| aa
sku -->|"tier"| aa
aa -->|"emits"| oid
aa -->|"computed"| odsc
aa -->|"computed"| ohyb
aa -->|"identity[0]"| opid
classDef keystone fill:#004578,color:#fff,stroke:#002a44,stroke-width:1px;
classDef neutral fill:#eef2f6,color:#1b1f23,stroke:#c7d0d9,stroke-width:1px;
class aa keystone;
class ident,enc,flags,sku,oid,odsc,ohyb,opid neutral;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_automation_account.this |
1 | The keystone account, with inline identity / encryption / timeouts dynamic blocks. |
| Requirement | 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 (verified against the live provider schema):
name,resource_group_name, andlocationare force-new — changing any of the three replaces the account, taking every child asset attached by sibling modules with it.- 🔴
namehas a SIX-character floor, higher than most Azure resources: the pattern is^[0-9a-zA-Z][-0-9a-zA-Z]{4,48}[0-9a-zA-Z]$— 6 to 50 characters, starting and ending with a letter or digit, and underscores and periods are refused outright. A composed name likeaa-${each.key}is one short environment key away from failing. sku_nameis a required provider argument; this module defaults it toBasicand accepts onlyBasicorFree— there is no premium tier. Unusually for a SKU field it is not force-new, so Free → Basic is an in-place update.- 🔴 The provider defaults
local_authentication_enabledandpublic_network_access_enabledtotrue. This module defaults both tofalse, so the empty call is the closed account and each relaxation is something a caller types. Comparing this module against the registry documentation will show the difference; it is deliberate. - 🔴
encryption.key_sourceexists only on the pinned 4.x line. The provider adds it inside anif !features.FivePointOh()block and marks it Deprecated: “encryption.key_sourcehas been deprecated and will be removed in v5.0 of the AzureRM Provider. To disable encryption, omit theencryptionblock”. The registry docs describe 5.0 and do not list it. Omitting the wholeencryptionblock is how platform-managed keys are expressed — the provider sendsMicrosoft.Automationwhen the block is absent andMicrosoft.Keyvaultwhen it is present, so presence of the block is the choice. - ℹ️
encryption.key_vault_key_idaccepts a versionless key URL here. Its siblingazurerm_ai_foundrydoes not — an asymmetry worth knowing when moving between the two. dsc_primary_access_keyanddsc_secondary_access_keyare computed and sensitive. This module never emits them; retrieve them out of band if a legacy DSC agent truly needs them.identityallows at most one block. WhentypeincludesUserAssigned,identity_idsis mandatory.encryption.key_vault_key_idis required within the block; supplyingencryptionturns on CMK, and the identity used to reach the key must already have wrap/unwrap/get permission on it.public_network_access_enabledandlocal_authentication_enabledare independent posture flags — closing one does not close the other.
- Contributor at the target scope (resource group or subscription) to create and update the Automation Account, or a custom role granting
Microsoft.Automation/automationAccounts/*at the resource-group scope. - To enable CMK
encryption, the deploying identity also needs rights to read the Key Vault key, and the account's managed identity needs Key Vault Crypto Service Encryption User (or equivalent key wrap/unwrap/get) on the key. - Assigning roles to the account's managed identity (so its runbooks can act) requires User Access Administrator or Owner at the target scope — handled by the
terraform-azurerm-role-assignmentssibling, not here.
- An existing resource group in a supported US Azure region.
- The resource provider backing
azurerm_automation_account(Microsoft.Automation) registered on the target subscription. - For CMK: an existing Key Vault key (and, optionally, a user-assigned identity) whose
idyou pass in. - The caller configures the
provider "azurerm" { features {} }block, authentication, and subscription; the module declares none of these.
terraform-azurerm-automation-account/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # deeply-typed object() schemas + tags/timeouts tail
├── main.tf # keystone azurerm_automation_account.this; dynamic identity/encryption/timeouts
├── outputs.tf # id first, then name, DSC/hybrid endpoints, identity principal id
├── README.md # this document
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore # canonical library ignore set
# The caller owns the provider block, auth, and features {}.
provider "azurerm" {
features {}
}
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-platform-eastus2"
resource_group_name = "rg-automation-eastus2"
location = "eastus2"
# sku_name defaults to "Basic"; identity defaults to system-assigned;
# local auth and public access are disabled by default.
tags = {
environment = "prod"
owner = "platform-engineering"
}
}ℹ️ Pin
?ref=v1.0.0(a tag), never a branch, so a plan is reproducible. A human applies from CI.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
location |
string |
caller / terraform-azurerm-resource-group (location) |
encryption.key_vault_key_id |
string |
terraform-azurerm-key-vault (key id) |
encryption.user_assigned_identity_id / identity.identity_ids |
string / list(string) |
terraform-azurerm-user-assigned-identity (id) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Automation Account Resource ID (first) | runbook / schedule / variable / DSC / webhook siblings, diagnostics, private endpoint |
name |
Account name | diagnostics / tagging |
dsc_server_endpoint |
DSC pull-server endpoint | DSC node registration |
hybrid_service_url |
Hybrid worker registration URL | terraform-azurerm-automation-hybrid-runbook-worker |
identity_principal_id |
System-assigned identity principal ID (null if none) | terraform-azurerm-role-assignments |
identity_tenant_id |
Managed-identity tenant ID (null if none) | auditing |
private_endpoint_connection_ids |
Map of PE connection name => ID | private-link inventory |
resource_group_name / sku_name / tags |
Placement, tier, tags | child resources take NAME + group |
identity_type / has_no_managed_identity |
The identity model | security review |
local_authentication_enabled / public_network_access_enabled |
Posture | security review |
uses_customer_managed_key / cmk_uses_user_assigned_identity |
Encryption posture | key-control review |
uses_deprecated_key_source |
4.x-only argument in use | upgrade planning |
private_endpoint_connection_count |
How many private paths exist | reachability review |
the_dsc_registration_keys_are_readable_by_anyone_who_can_read_this_resource |
Constant: plan access is credential access | permissions review |
destroying_the_account_destroys_everything_inside_it |
Constant: runbooks, schedules, credentials | change windows |
this_module_creates_no_automation |
Constant: the account is a container | composition design |
jobs_are_asynchronous_and_unobservable |
Constant: apply proves nothing ran | verification expectations |
The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:
variable "hub_network_subnet_ids" {
description = "subnet ids of an existing hub network that these examples reference but do not create."
type = map(string)
}The examples read the current tenant from the provider rather than hard-coding it.
data "azurerm_client_config" "current" {}1 · Minimal secure account
The empty-ish call. Private, no local keys, system-assigned identity.
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-core-eastus2"
resource_group_name = "rg-automation-eastus2"
location = "eastus2"
}🔒
public_network_access_enabledandlocal_authentication_enabledboth default tofalse;identitydefaults toSystemAssigned.
2 · Basic SKU with tags
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-ops-centralus"
resource_group_name = "rg-ops"
location = "centralus"
sku_name = "Basic"
tags = {
environment = "prod"
cost_center = "1042"
}
}ℹ️
Basicis the standard production tier and the module default.
3 · Free tier for a sandbox
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-sandbox-westus2"
resource_group_name = "rg-sandbox"
location = "westus2"
sku_name = "Free"
}
⚠️ Freecaps monthly job run-time. Any value other thanBasicorFreeis rejected at plan time by the module's validation.
4 · User-assigned identity
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-uami-eastus"
resource_group_name = "rg-automation"
location = "eastus"
identity = {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.automation.id]
}
}💡
identity_idsis required whenevertypeincludesUserAssigned; the module fails at plan time if it is empty.
5 · System- and user-assigned together
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-dual-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
identity = {
type = "SystemAssigned, UserAssigned"
identity_ids = [azurerm_user_assigned_identity.automation.id]
}
}ℹ️ The system-assigned principal still surfaces via
identity_principal_id; the user-assigned identity is used where you reference it explicitly (for example, CMK access).
6 · CMK encryption with the system identity
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-cmk-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
encryption = {
key_vault_key_id = azurerm_key_vault_key.automation.id
}
}🔒 Grant the account's system-assigned identity wrap/unwrap/get on the key before applying, or the apply fails when the account tries to reach the key.
7 · CMK encryption via a user-assigned identity
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-cmk-uami-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
identity = {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.automation.id]
}
encryption = {
key_vault_key_id = azurerm_key_vault_key.automation.id
key_source = "Microsoft.Keyvault"
user_assigned_identity_id = azurerm_user_assigned_identity.automation.id
}
}💡 When the key is reached through a user-assigned identity, that identity must also appear in
identity.identity_ids.
8 · Legacy workload that needs local authentication
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-legacy-eastus"
resource_group_name = "rg-automation"
location = "eastus"
local_authentication_enabled = true # opt-out of the secure default
}
⚠️ Only enable local (key-based) authentication for a workload that genuinely cannot use Entra ID. Prefer the managed identity under RBAC.
9 · Public network access (explicit opt-out)
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-public-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
public_network_access_enabled = true # opt-out of the private default
}
⚠️ Exposes the account's public data-plane endpoints. Prefer a Private Endpoint (example 10) and leave thisfalse.
10 · Private access via a sibling Private Endpoint
This module keeps public_network_access_enabled = false; a sibling module attaches the endpoint against the account id.
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-private-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
}
module "automation_private_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-private-endpoint.git?ref=v1.0.0"
name = "pe-aa-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
subnet_id = var.hub_network_subnet_ids["private-endpoints"]
private_service_connection = {
name = "pe-aa-eastus2-connection"
private_connection_resource_id = module.automation_account.id
subresource_names = ["DSCAndHybridWorker"]
}
}🔒 With public access off, reach the account only through this endpoint (and, for Azure sub-resources,
Webhook/DSCAndHybridWorkergroups).
11 · Custom timeouts
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-timeouts-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
timeouts = {
create = "30m"
delete = "30m"
}
}ℹ️ Any timeout field you omit falls back to the provider default.
12 · A fleet of accounts with for_each
locals {
automation_accounts = {
prod = { location = "eastus2", sku_name = "Basic" }
stage = { location = "eastus2", sku_name = "Basic" }
dev = { location = "centralus", sku_name = "Free" }
}
}
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
for_each = local.automation_accounts
name = "aa-${each.key}"
resource_group_name = "rg-automation-${each.key}"
location = each.value.location
sku_name = each.value.sku_name
tags = { environment = each.key }
}💡 Keying the map by environment name keeps additions and removals from re-indexing the rest of the fleet.
⚠️ Mind the six-character floor when composing names. The provider's pattern is^[0-9a-zA-Z][-0-9a-zA-Z]{4,48}[0-9a-zA-Z]$— a minimum of 6 characters, which is higher than most Azure resources. Above,aa-devis exactly six and passes; a two-letter key such asqawould produceaa-qa, which is five, and the module refuses it atterraform validate. Underscores and periods are refused outright, so an environment key containing either breaks the same way.
13 · Attaching runbooks and schedules (sibling modules)
This module owns only the account; child assets are their own modules that consume the account id.
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-jobs-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
}
module "nightly_cleanup_runbook" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-runbook.git?ref=v1.0.0"
location = "eastus2"
name = "Invoke-NightlyCleanup"
resource_group_name = "rg-automation"
automation_account_name = module.automation_account.name
runbook_type = "PowerShell72"
}
module "nightly_schedule" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-schedule.git?ref=v1.0.0"
name = "nightly-0200"
resource_group_name = "rg-automation"
automation_account_name = module.automation_account.name
frequency = "Day"
}ℹ️ Runbooks, schedules, variables, DSC, and webhooks are separate modules in this suite; this account never manages them inline.
14 · Granting the identity least-privilege roles
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-runner-eastus2"
resource_group_name = "rg-automation"
location = "eastus2"
}
module "automation_roles" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = azurerm_resource_group.workload.id
role_assignments = {
vm_operator = {
principal_id = module.automation_account.identity_principal_id
role_definition_name = "Virtual Machine Contributor"
}
}
}🔒 Scope the assignment to exactly the resources the runbooks touch — never subscription
Ownerfor convenience.
15 · 🏗️ End-to-end composition
Resource group → Key Vault (CMK) → this Automation Account (private, CMK, system identity) → role assignment granting the account's identity its automation target.
provider "azurerm" {
features {}
}
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-automation-eastus2"
location = "eastus2"
}
module "key_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
tenant_id = data.azurerm_client_config.current.tenant_id
name = "kv-automation-eus2"
resource_group_name = module.resource_group.name
location = module.resource_group.location
keys = {
cmk = { name = "automation-cmk", key_type = "RSA", key_size = 3072, key_opts = ["get", "unwrapKey", "wrapKey"] }
}
}
module "automation_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"
name = "aa-platform-eastus2"
resource_group_name = module.resource_group.name
location = module.resource_group.location
sku_name = "Basic"
# Private + local keys off + system identity are the defaults; shown for clarity.
public_network_access_enabled = false
local_authentication_enabled = false
identity = {
type = "SystemAssigned"
}
encryption = {
key_vault_key_id = module.key_vault.key_ids["cmk"]
}
tags = {
environment = "prod"
managed_by = "terraform"
}
}
module "automation_roles" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.resource_group.id
role_assignments = {
automation_operator = {
principal_id = module.automation_account.identity_principal_id
role_definition_name = "Reader"
}
}
}💡 Wiring is by output: the RG feeds name/location, the vault feeds the CMK key
id, and the account'sidentity_principal_idfeeds the role assignment — no hand-copied IDs.
Identity (required, force-new): name, resource_group_name, location.
Tier: sku_name (default Basic; Basic | Free).
Posture: local_authentication_enabled (default false), public_network_access_enabled (default false).
Blocks: identity (default system-assigned), encryption (default null / platform-managed keys).
Tail: tags, timeouts.
Full variable schemas
variable "name" { type = string } # force-new
variable "resource_group_name" { type = string } # force-new
variable "location" { type = string } # force-new
variable "sku_name" {
type = string
default = "Basic" # Basic | Free
}
variable "local_authentication_enabled" {
type = bool
default = false # local keys disabled; prefer Entra ID
}
variable "public_network_access_enabled" {
type = bool
default = false # private by default
}
variable "identity" {
type = object({
type = string # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned"
identity_ids = optional(list(string))
})
default = { type = "SystemAssigned" }
}
variable "encryption" {
type = object({
key_vault_key_id = string
key_source = optional(string)
user_assigned_identity_id = optional(string)
})
default = null # platform-managed keys unless supplied
}
variable "tags" {
type = map(string)
default = {}
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
Automation Account Resource ID | emitted first |
name |
Account name | |
location |
Azure region, in the canonical form Azure uses. | Read from the resource, not var.location. |
dsc_server_endpoint |
DSC pull-server endpoint | computed |
hybrid_service_url |
Hybrid worker registration URL | computed |
identity_principal_id |
System-assigned identity principal ID | null when no system-assigned identity |
identity_tenant_id |
Managed-identity tenant ID | null when no identity |
private_endpoint_connection_ids |
Map of PE connection name => ID | computed; empty when none |
resource_group_name |
The account's group | Children take name + group, not id |
sku_name |
Basic or Free |
Only two values; not force-new, unusually |
tags |
Tags on the account | Cover the container, not its contents |
identity_type |
The identity model in use | |
has_no_managed_identity |
No identity at all | |
local_authentication_enabled |
Key-based auth permitted | 🔴 Provider defaults true; this module defaults false |
public_network_access_enabled |
Reachable from the internet | 🔴 Provider defaults true; this module defaults false |
uses_customer_managed_key |
CMK configured | Presence of the block is the choice |
cmk_uses_user_assigned_identity |
A dedicated identity reaches the key | Avoids the two-pass apply |
uses_deprecated_key_source |
encryption.key_source supplied |
🔴 4.x-only and removed in 5.0 |
private_endpoint_connection_count |
Private paths reported | Computed — unknown at plan on a first apply |
the_dsc_registration_keys_are_readable_by_anyone_who_can_read_this_resource |
Always true |
🔴 Plan access is credential access. The keys are computed, so refresh reads them into state |
destroying_the_account_destroys_everything_inside_it |
Always true |
🔴 Runbooks, schedules, variables, credentials, DSC — none in this plan |
this_module_creates_no_automation |
Always true |
A container; the automation is elsewhere |
jobs_are_asynchronous_and_unobservable |
Always true |
A clean apply proves nothing has run |
The sensitive computed
dsc_primary_access_key/dsc_secondary_access_keyare intentionally not emitted.
- Force-new trio.
name,resource_group_name, andlocationeach force replacement. Because sibling modules attach runbooks, schedules, and DSC nodes to this account byid, replacing it cascades — treat these three as immutable after the first apply. - Secure defaults are independent switches.
public_network_access_enabledandlocal_authentication_enabledare separate flags; the empty call closes both. Opening one leaves the other closed. - Identity-first design. The default system-assigned identity means runbooks can authenticate under RBAC immediately.
identity_principal_idisnullfor a pure user-assigned configuration — guard downstream role assignments accordingly. - CMK is opt-in and order-sensitive.
encryptionisnullby default (platform-managed keys). When supplied, the identity that reaches the key must already hold wrap/unwrap/get on it, or the apply fails; a user-assigned identity named inencryption.user_assigned_identity_idmust also be listed inidentity.identity_ids. - Total renderer. Every optional nested block is a
dynamicblock gated on a non-null variable withtry(...)on each optional field, so an omitted key renders as absent rather than erroring. features {}dependence. The module carries noprovider {}block; the caller'sprovider "azurerm" { features {} }is what lets it initialize.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Public network access | public_network_access_enabled = false |
set to true |
| Local (key) authentication | local_authentication_enabled = false |
set to true |
| Identity | system-assigned managed identity | set identity = null or choose user-assigned |
| Encryption | platform-managed keys (encryption = null) |
supply encryption.key_vault_key_id for CMK |
| Emitted secrets | DSC access keys never output | retrieve out of band if truly needed |
# From the module folder — plan-only, no cloud calls.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module
?ref=v1.0.0(a tag), never a branch. - No
terraform applyduring authoring or review; a human applies from CI against real credentials.
The offline proof gate is what CI runs before a human plan:
terraform init -backend=false— resolve the pinned provider without a backend.terraform validate— type-check the configuration against the pinned schema; theobject()schemas surface a mistypedidentity.type, a badsku_name, or a malformedencryptionblock here, before any Azure call.terraform fmt -check— enforce canonical formatting.
validate and fmt never reach Azure. Only terraform plan (run by a human) exercises the ARM API — for example, confirming the account's identity actually has key access for CMK, or that the resource group exists.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-automation-eastus2/providers/Microsoft.Automation/automationAccounts/aa-platform-eastus2"
name = "aa-platform-eastus2"
dsc_server_endpoint = "https://eus2-agentservice-prod-1.azure-automation.net/accounts/11111111-2222-3333-4444-555555555555"
hybrid_service_url = "https://11111111-2222-3333-4444-555555555555.jrds.eus2.azure-automation.net"
identity_principal_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
identity_tenant_id = "99999999-8888-7777-6666-555555555555"
private_endpoint_connection_ids = {}| Symptom | Cause | Fix |
|---|---|---|
| Plan wants to replace the account | Changed name, resource_group_name, or location (all force-new) |
Revert the change, or accept replacement and re-attach child assets to the new id. |
sku_name must be one of: Basic, Free |
A value outside the closed set | Use Basic or Free. |
identity.identity_ids must contain at least one ... |
type includes UserAssigned but no IDs supplied |
Provide identity_ids, or use SystemAssigned. |
| Apply fails enabling CMK | The identity lacks key wrap/unwrap/get, or the user-assigned identity is not in identity_ids |
Grant Key Vault Crypto Service Encryption User on the key and list the identity in identity.identity_ids. |
| Runbooks cannot reach a target resource | The account's identity has no role there | Assign least-privilege roles to identity_principal_id via the role-assignments sibling. |
| Cannot reach the account after apply | public_network_access_enabled = false and no Private Endpoint |
Add a Private Endpoint (example 10) or, if acceptable, set the flag to true. |
Provider configuration not present on init |
Caller has no provider "azurerm" { features {} } |
Add the provider block with features {} in the root module. |
- Terraform Registry:
azurerm_automation_account - Microsoft Learn: Azure Automation overview
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-key-vault,terraform-azurerm-user-assigned-identity,terraform-azurerm-role-assignments,terraform-azurerm-private-endpoint, and theterraform-azurerm-automation-*asset modules (runbook, schedule, variable, DSC, hybrid worker, webhook). - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."