Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings
Open more actions menu

Latest commit

 

History

1 Commit
1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure Automation Account Terraform Module

A standalone module that provisions a single hardened azurerm_automation_account, targeting hashicorp/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.

Terraform azurerm Module Type Resources


🧩 Overview

This module manages one Azure Automation Account and nothing else:

  • 🔐 A single azurerm_automation_account named this, 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 = false and local_authentication_enabled = false on the empty call.
  • 🏷️ Full tags support and optional timeouts.

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.


❤️ Support this project

If this module saves you time, please consider supporting it:


🗺️ Where this fits in the family

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;
Loading

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.


🧬 What this module builds

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;
Loading

Resource inventory

Resource Count Role
azurerm_automation_account.this 1 The keystone account, with inline identity / encryption / timeouts dynamic blocks.

✅ Provider / Versions

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, and location are force-new — changing any of the three replaces the account, taking every child asset attached by sibling modules with it.
  • 🔴 name has 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 like aa-${each.key} is one short environment key away from failing.
  • sku_name is a required provider argument; this module defaults it to Basic and accepts only Basic or Free — 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_enabled and public_network_access_enabled to true. This module defaults both to false, 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_source exists only on the pinned 4.x line. The provider adds it inside an if !features.FivePointOh() block and marks it Deprecated: “encryption.key_source has been deprecated and will be removed in v5.0 of the AzureRM Provider. To disable encryption, omit the encryption block”. The registry docs describe 5.0 and do not list it. Omitting the whole encryption block is how platform-managed keys are expressed — the provider sends Microsoft.Automation when the block is absent and Microsoft.Keyvault when it is present, so presence of the block is the choice.
  • ℹ️ encryption.key_vault_key_id accepts a versionless key URL here. Its sibling azurerm_ai_foundry does not — an asymmetry worth knowing when moving between the two.
  • dsc_primary_access_key and dsc_secondary_access_key are computed and sensitive. This module never emits them; retrieve them out of band if a legacy DSC agent truly needs them.
  • identity allows at most one block. When type includes UserAssigned, identity_ids is mandatory.
  • encryption.key_vault_key_id is required within the block; supplying encryption turns on CMK, and the identity used to reach the key must already have wrap/unwrap/get permission on it.
  • public_network_access_enabled and local_authentication_enabled are independent posture flags — closing one does not close the other.

🔑 Required Azure RBAC Roles / Permissions

  • 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-assignments sibling, not here.

Azure Prerequisites

  • 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 id you pass in.
  • The caller configures the provider "azurerm" { features {} } block, authentication, and subscription; the module declares none of these.

📁 Module Structure

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

⚙️ Quick Start

# 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.


🔌 Cross-Module Contract

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

📚 Example Library

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_enabled and local_authentication_enabled both default to false; identity defaults to SystemAssigned.

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"
  }
}

ℹ️ Basic is 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"
}

⚠️ Free caps monthly job run-time. Any value other than Basic or Free is 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_ids is required whenever type includes UserAssigned; 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 this false.

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 / DSCAndHybridWorker groups).

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-dev is exactly six and passes; a two-letter key such as qa would produce aa-qa, which is five, and the module refuses it at terraform 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 Owner for 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's identity_principal_id feeds the role assignment — no hand-copied IDs.


📥 Inputs

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
}

🧾 Outputs

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 ⚠️ Legal and almost always a mistake
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_key are intentionally not emitted.


🧠 Architecture Notes

  • Force-new trio. name, resource_group_name, and location each force replacement. Because sibling modules attach runbooks, schedules, and DSC nodes to this account by id, replacing it cascades — treat these three as immutable after the first apply.
  • Secure defaults are independent switches. public_network_access_enabled and local_authentication_enabled are 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_id is null for a pure user-assigned configuration — guard downstream role assignments accordingly.
  • CMK is opt-in and order-sensitive. encryption is null by 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 in encryption.user_assigned_identity_id must also be listed in identity.identity_ids.
  • Total renderer. Every optional nested block is a dynamic block gated on a non-null variable with try(...) on each optional field, so an omitted key renders as absent rather than erroring.
  • features {} dependence. The module carries no provider {} block; the caller's provider "azurerm" { features {} } is what lets it initialize.

🧱 Design Principles

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

🚀 Runbook

# 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 apply during authoring or review; a human applies from CI against real credentials.

🧪 Testing

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; the object() schemas surface a mistyped identity.type, a bad sku_name, or a malformed encryption block 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.


💬 Example Output

$ 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 = {}

🔍 Troubleshooting

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.

🔗 Related Docs

  • 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 the terraform-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."

Morty Proxy This is a proxified and sanitized view of the page, visit original site.