Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure Automation Connection Type Terraform Module

Defines a custom Azure Automation connection type β€” the field schema a connection is built from. It creates no connection and holds no credentials. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • ☁️ Creates one azurerm_automation_connection_type (the keystone this) β€” a reusable field schema for connections in one Automation Account.
  • 🧬 Takes field as a map keyed by field name, so duplicate names are impossible to express rather than something a validation has to catch.
  • πŸ”΄ Every argument is force-new, including the whole field set. This resource has no in-place edit and no update timeout.
  • πŸ”’ Rejects a field whose name says secret while is_encrypted is left false β€” the one half of that decision a module can see.
  • πŸ“‹ Emits the field contract (field_names, required_field_names, plaintext_field_names) so a connection can be checked against it, since Terraform cannot.

πŸ’‘ Why it matters: a connection type is a schema, not a connection. Nothing it creates authenticates to anything β€” and the connections built on it reference it by a bare string, so the one thing that must never change is its name.


❀️ Support this project

If this module saves you time:


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

flowchart TB
  rg["terraform-azurerm-resource-group"]
  aa["terraform-azurerm-automation-account"]
  mod["terraform-azurerm-automation-connection-type"]
  untyped["terraform-azurerm-automation-connection  the ONLY consumer, via type equals this name"]
  rb["terraform-azurerm-automation-runbook"]
  builtin["Built-in types Azure, AzureClassicCertificate, AzureServicePrincipal"]
  typed["The three typed connection modules  they use the built-in types, never this one"]

  rg -->|"name"| aa
  aa -->|"automation_account_name"| mod
  mod -->|"name as type, a STRING with no dependency edge"| untyped
  untyped -->|"resolved by name at execution time"| rb
  builtin -->|"hard-coded, not defined here"| typed
  typed -->|"resolved by name at execution time"| rb

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef keystone fill:#004578,stroke:#00243c,color:#ffffff;
  classDef neutral fill:#F3F2F1,stroke:#8A8886,color:#201F1E;
  class mod this;
  class aa keystone;
  class rg,untyped,rb,builtin,typed neutral;
Loading

This module sits on the producing side of the connection family. The three typed connection modules hard-code Azure's built-in types and never reference this resource; the only module that can consume what this defines is terraform-azurerm-automation-connection, the untyped form, via type = "<this name>" β€” a string, with no dependency edge for Terraform to see.


🧬 What this module builds

flowchart TB
  subgraph inputs["Inputs"]
    n["name  the type string a connection names, force-new"]
    rgn["resource_group_name  force-new"]
    aan["automation_account_name  force-new, by name not id"]
    f["field  map keyed by field name, min 1, force-new"]
    ig["is_global  force-new"]
  end

  res["azurerm_automation_connection_type.this"]

  subgraph outputs["Outputs"]
    onm["name  compose with THIS, not id"]
    ofn["field_names  the contract a connection must satisfy"]
    oreq["required_field_names"]
    oenc["encrypted_field_names"]
    opln["plaintext_field_names  scrutinise this one"]
    ouq["field_types_that_look_like_unqualified_primitives  reported, not rejected"]
    odef["this_defines_a_type_it_does_not_create_a_connection"]
    ofn2["nothing_updates_in_place_every_argument_is_force_new"]
  end

  n --> res
  rgn --> res
  aan --> res
  f --> res
  ig --> res
  res --> onm
  res --> ofn
  res --> oreq
  res --> oenc
  res --> opln
  res --> ouq
  res --> odef
  res --> ofn2

  classDef this fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef neutral fill:#F3F2F1,stroke:#8A8886,color:#201F1E;
  class res this;
  class n,rgn,aan,f,ig,onm,ofn,oreq,oenc,opln,ouq,odef,ofn2 neutral;
Loading
Resource Cardinality Purpose
azurerm_automation_connection_type.this single One custom connection type: a name, a field schema, and a global flag.

Five arguments, all five force-new. The field block is rendered from a keyed map, so the useful outputs describe the schema rather than echoing the inputs.


βœ… Provider / Versions

Item Value
Terraform >= 1.12.0
Provider hashicorp/azurerm ~> 4.0
Provider block None in this module. The caller configures provider "azurerm" { features {} }, auth and subscription.
Module type Standalone β€” one resource, no children.

Schema notes that bite:

  • πŸ”΄ Every argument is force-new β€” name, resource_group_name, automation_account_name, field and is_global. There is no in-place edit at all.
  • πŸ”΄ So the field set is a one-time decision. Adding one field replaces the connection type, and connections referencing it by name are left naming nothing during the window.
  • πŸ”΄ There is no update timeout, only create, read and delete. A four-key timeouts object silently drops update β€” object conversion discards undeclared attributes without an error.
  • πŸ”΄ field requires at least one entry (the provider sets min = 1).
  • πŸ”΄ Connections reference the type by NAME, as a string. No Terraform dependency exists in either direction.
  • ⚠️ There is no description argument β€” unlike all three typed connection resources, which take one. So there is nowhere on this resource to record why it exists.
  • ⚠️ No tags attribute either.
  • ⚠️ The provider documents is_global as "whether the connection type is global" and nothing more, so this module reports it and declines to describe behaviour it cannot confirm.
  • ⚠️ No closed set of field type values is published. Azure Automation conventionally uses .NET type names such as System.String; this module reports suspicious values rather than rejecting them.
  • ⚠️ lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy.

πŸ”‘ Required Azure RBAC Roles / Permissions

Operation Role Scope
Create or delete the connection type Contributor, or Automation Contributor the Automation Account
Read it Reader the connection type
Create connections of this type Contributor the Automation Account
Read a connection's values at runtime the runbook's own identity the Automation Account

πŸ”’ Nothing here is sensitive, because a schema holds no values. This resource declares that a field called ApiSecret exists and should be encrypted; the secret itself only ever lives in a connection built from the type. So an identity that can read this resource learns your field names and nothing more.

⚠️ is_encrypted is a promise about connections, not protection of anything here. It is the flag that decides whether Automation stores a connection's value for that field encrypted β€” set it correctly at definition time, because the field set cannot be changed afterwards.


Azure Prerequisites

  • An existing Automation Account, and its name plus resource group. Nothing here can confirm the two agree.
  • A settled list of fields, since the set is force-new and connections depend on the type by name.
  • A decision per field on is_encrypted, which the module cannot make for you.
  • A name that is not one of Azure's built-in types β€” Azure, AzureClassicCertificate and AzureServicePrincipal are rejected.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; this module declares none of these.

πŸ“ Module Structure

terraform-azurerm-automation-connection-type/
β”œβ”€β”€ providers.tf     # required_version + pinned azurerm; no provider block
β”œβ”€β”€ variables.tf     # 6 inputs: name, resource_group_name, automation_account_name,
β”‚                    #           field, is_global, timeouts
β”œβ”€β”€ main.tf          # the keystone `this` + the derived field-schema locals
β”œβ”€β”€ outputs.tf       # id first, then the field contract, then the facts that produce no error
β”œβ”€β”€ README.md        # this file
β”œβ”€β”€ SCOPE.md         # the cross-module contract
β”œβ”€β”€ LICENSE          # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "contoso_api_connection_type" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"

  name                    = "ContosoApi"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-prod"

  field = {
    ApiEndpoint = { type = "System.String" }
    ApiSecret   = { type = "System.String", is_encrypted = true }
    Retries     = { type = "System.Int32", is_optional = true }
  }
}

πŸ’‘ The map key is the field name, which is how a runbook addresses it β€” $Conn.ApiEndpoint.

⚠️ Get the field set right first time. It is force-new, and replacing the type breaks every connection that names it.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
name string caller β€” force-new, and a string interface for connections
resource_group_name string terraform-azurerm-resource-group output name β€” force-new
automation_account_name string terraform-azurerm-automation-account output name β€” force-new
field map(object({type, is_encrypted, is_optional})) caller β€” required, min 1, force-new
is_global bool caller β€” defaults false, force-new
timeouts object(...) caller β€” three keys only

Emits

Output Description Consumed by
id The Resource ID. imports and auditing β€” nothing consumes it
name The value to compose with. Force-new. terraform-azurerm-automation-connection as type
resource_group_name / automation_account_name The parent, by name. Force-new. review
is_global The effective value. Force-new. review
field_names Every field, sorted. The contract a connection must satisfy. connection review
required_field_names / optional_field_names Split by is_optional. connection review
encrypted_field_names / plaintext_field_names Split by is_encrypted. security review
field_types_in_use The distinct declared types. review
field_types_that_look_like_unqualified_primitives Reported, not rejected. design review
field_count How many fields. review
has_no_encrypted_fields Derived. security review
this_defines_a_type_it_does_not_create_a_connection Always true. design review
connections_reference_this_type_by_name_with_no_terraform_dependency Always true. change review
nothing_updates_in_place_every_argument_is_force_new Always true. change review
this_resource_carries_no_tags Always true. tagging review

πŸ“š Example Library

1 Β· Minimal call β€” one field
module "connection_type" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"

  name                    = "ContosoApi"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-prod"

  field = {
    ApiEndpoint = { type = "System.String" }
  }
}

ℹ️ One field is the provider's minimum. A connection type with none could hold nothing, so min = 1 is enforced in the schema and re-checked here with a message that says why.

2 Β· A realistic custom type β€” endpoint, secret, tuning
module "connection_type" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"

  name                    = "ContosoApi"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-prod"

  field = {
    ApiEndpoint = { type = "System.String" }
    ApiSecret   = { type = "System.String", is_encrypted = true }
    Retries     = { type = "System.Int32", is_optional = true }
  }
}

πŸ’‘ The three fields show the three decisions: a plain identifier, an encrypted credential, and an optional tuning value. is_optional defaults to false, so a field is required unless you say otherwise β€” which is the safer direction, because a missing required field fails loudly when a connection is created.

3 Β· Reading the field contract
output "connection_must_supply" {
  value = module.connection_type.required_field_names
}

output "connection_may_supply" {
  value = module.connection_type.optional_field_names
}

output "stored_encrypted" {
  value = module.connection_type.encrypted_field_names
}

output "stored_in_the_clear" {
  value = module.connection_type.plaintext_field_names
}

For the type in example 2:

connection_must_supply = ["ApiEndpoint", "ApiSecret"]
connection_may_supply  = ["Retries"]
stored_encrypted       = ["ApiSecret"]
stored_in_the_clear    = ["ApiEndpoint", "Retries"]

πŸ’‘ plaintext_field_names is the list to scrutinise, and it is stated positively on purpose β€” a reviewer should read what it is rather than infer it from what an encrypted list omits.

4 Β· What the encryption heuristic rejects
# Rejected β€” the name says secret, the flag says plaintext.
field = {
  ApiSecret = { type = "System.String" }
}

# Accepted.
field = {
  ApiSecret = { type = "System.String", is_encrypted = true }
}

ℹ️ This is the one half of the encryption decision a module can see. Whether a field holds a credential is not knowable from a schema β€” ConnectionString or Endpoint can carry one without saying so β€” but a field literally called ApiSecret stored in the clear is worth stopping for. The message says it is a name-based guess.

⚠️ The names it matches are password, secret, token, credential, apikey, api_key and passphrase, case-insensitively. Renaming the field is a legitimate fix if it genuinely holds no secret.

5 Β· Field types are reported, not rejected
# Accepted β€” and reported, because Azure Automation conventionally uses .NET type names.
field = {
  ApiEndpoint = { type = "string" }
  Port        = { type = "int" }
}
output "worth_one_look" {
  value = module.connection_type.field_types_that_look_like_unqualified_primitives
}
worth_one_look = ["int", "string"]

ℹ️ Deliberately not a validation. The provider publishes no closed set of legal field types, so a check refusing string could refuse input the API accepts β€” and this suite does not invent a constraint it cannot ground. The probable mistake is surfaced for a human instead.

πŸ’‘ Use System.String, System.Boolean, System.Int32 and similar unless you have a reason not to. If your connections work, the types were fine.

6 Β· What the name validation rejects
# Rejected β€” Azure's own built-in connection type names.
name = "Azure"
name = "AzureClassicCertificate"
name = "AzureServicePrincipal"

# Rejected β€” a Resource ID.
name = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg"

# Accepted.
name = "ContosoApi"

⚠️ The built-in names already exist and are created by the typed connection resources, not defined here. A custom type sharing one would be confusing at best β€” so the check names the three and says what this resource is for.

7 Β· What the field-map validations reject
# Rejected β€” no fields at all.
field = {}

# Rejected β€” an empty map key. The key IS the field name.
field = { "" = { type = "System.String" } }

# Rejected β€” a field name with a space cannot be read as $Conn.<name>.
field = { "Api Endpoint" = { type = "System.String" } }

# Rejected β€” an empty type, and a type that is a Resource ID.
field = { ApiEndpoint = { type = "" } }
field = { ApiEndpoint = { type = "/subscriptions/x" } }

# Accepted.
field = { ApiEndpoint = { type = "System.String" } }

πŸ’‘ A duplicate field name is not on this list, because it cannot be written. Map keys are unique, so choosing a map over a list makes the duplicate case a type-level impossibility rather than a validation β€” the schema does the work.

8 Β· Why a map rather than a list
# This module's shape: uniqueness by construction.
field = {
  ApiEndpoint = { type = "System.String" }
  ApiSecret   = { type = "System.String", is_encrypted = true }
}

The provider models field as a repeatable block, which this module renders from the map:

dynamic "field" {
  for_each = var.field
  content {
    name         = field.key
    type         = field.value.type
    is_encrypted = field.value.is_encrypted
    is_optional  = field.value.is_optional
  }
}

ℹ️ Ordering is not preserved and does not matter. Fields are addressed by name in a runbook β€” $Conn.ApiEndpoint β€” never by position, so nothing observable depends on the order the blocks are emitted in.

πŸ’‘ The trade-off is stated rather than hidden: a list would preserve the provider's ordering, and a map does not. Uniqueness was judged the more valuable property, because a duplicate field name is a real mistake and ordering is not a real signal.

9 Β· The whole field set is force-new
module "connection_type" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"

  name                    = "ContosoApi"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-prod"

  # Adding ONE field here REPLACES the connection type.
  field = {
    ApiEndpoint = { type = "System.String" }
    ApiSecret   = { type = "System.String", is_encrypted = true }
    ApiVersion  = { type = "System.String" } # <-- this is a destroy and create
  }
}

output "read_before_editing_fields" {
  value = module.connection_type.nothing_updates_in_place_every_argument_is_force_new
}

⚠️ And the replacement window is one in which connections name nothing. Connections reference the type by name, so during the destroy-and-create they refer to a type that does not exist β€” with no plan diff or warning anywhere in Terraform.

πŸ’‘ Include the fields you will need. An unused optional field costs nothing; growing the set later costs a rebuild.

10 Β· Guarding against accidental replacement
module "connection_type" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"

  name                    = "ContosoApi"
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-prod"

  field = {
    ApiEndpoint = { type = "System.String" }
  }
}

# `lifecycle` is not valid inside a `module` block, so `prevent_destroy` is
# unavailable to a caller. A lock on the parent account is the route that works.
resource "azurerm_management_lock" "automation_account" {
  name       = "no-delete-automation-assets"
  scope      = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-automation-prod/providers/Microsoft.Automation/automationAccounts/aa-prod"
  lock_level = "CanNotDelete"
  notes      = "Connection types are force-new; replacing one breaks the connections that name it."
}

πŸ”’ A lock prevents deletion, not replacement. A field change still plans a replace β€” the lock turns that into a failed apply rather than a silent break, which is the outcome worth having.

11 Β· Several custom types with for_each
variable "connection_types" {
  type = map(object({
    field = map(object({
      type         = string
      is_encrypted = optional(bool, false)
      is_optional  = optional(bool, false)
    }))
  }))
  default = {}
}

module "connection_type" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"
  for_each = var.connection_types

  name                    = each.key
  resource_group_name     = "rg-automation-prod"
  automation_account_name = "aa-prod"

  field = each.value.field
}

output "type_contracts" {
  description = "What each type's connections must supply."
  value       = { for key, mod in module.connection_type : key => mod.required_field_names }
}

ℹ️ A connection type is scoped to one Automation Account. Defining the same custom type in several accounts means several instances of this module β€” there is no tenant-wide connection type.

πŸ’‘ The output is the useful artefact, since nothing in Terraform checks a connection's values against its type.

12 Β· πŸ—οΈ End-to-end composition β€” a custom type and a connection that uses it
provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-automation-prod"
  location = "eastus2"

  tags = { environment = "prod" }
}

# 1 Β· The Automation Account. Tag here; neither asset below has tags.
module "automation_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-account.git?ref=v1.0.0"

  name                = "aa-prod"
  resource_group_name = module.rg.name
  location            = "eastus2"

  tags = { environment = "prod" }
}

# 2 Β· This module. The schema β€” no values, no credentials.
module "connection_type" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection-type.git?ref=v1.0.0"

  name                    = "ContosoApi"
  resource_group_name     = module.rg.name
  automation_account_name = module.automation_account.name

  field = {
    ApiEndpoint = { type = "System.String" }
    ApiSecret   = { type = "System.String", is_encrypted = true }
    Retries     = { type = "System.Int32", is_optional = true }
  }
}

# 3 Β· A connection of that custom type. The untyped module is the ONLY one that
#     can consume a custom type -- the three typed modules cover built-ins only.
module "contoso_api_connection" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-connection.git?ref=v1.0.0"

  name                    = "ContosoApiProd"
  resource_group_name     = module.rg.name
  automation_account_name = module.automation_account.name

  # Wiring the OUTPUT rather than repeating the literal "ContosoApi" is what
  # creates an ordering dependency -- the type has no other link to the connection.
  type = module.connection_type.name

  values = {
    ApiEndpoint = "https://api.contoso.example"
    ApiSecret   = var.contoso_api_secret
    Retries     = "3"
  }
}

# 4 Β· The runbook that reads it. Note it references the connection by NAME in its
#     own code, so there is no edge here either.
module "sync_runbook" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-automation-runbook.git?ref=v1.0.0"

  name                    = "Invoke-ContosoSync"
  resource_group_name     = module.rg.name
  location                = "eastus2"
  automation_account_name = module.automation_account.name
  runbook_type            = "PowerShell"
}

output "contract_the_connection_must_satisfy" {
  value = module.connection_type.required_field_names
}

output "fields_stored_in_the_clear" {
  value = module.connection_type.plaintext_field_names
}

Where var.contoso_api_secret is declared sensitive = true and provisioned out of band.

πŸ’‘ Step 3's type = module.connection_type.name is the only reason Terraform orders these correctly. Repeating the literal string "ContosoApi" would work at apply time and leave the two resources with no relationship β€” so a later change to the type would not order against the connection at all.

⚠️ Nothing checks that step 3's values keys match step 2's fields. The contract is real and enforced by Azure at connection creation, not by Terraform β€” which is why contract_the_connection_must_satisfy is emitted for a human to compare.

⚠️ Step 4 depends on step 3 and Terraform cannot tell. The runbook resolves the connection by name when it executes.


πŸ“₯ Inputs

Input Type Default Notes
name string β€” Required. Force-new. A string interface; built-in names rejected.
resource_group_name string β€” Required. Force-new.
automation_account_name string β€” Required. Force-new. By name, not ID.
field map(object(...)) β€” Required, min 1. Force-new. Key = field name.
is_global bool false Force-new. Provider documents nothing beyond the name.
timeouts object(...) null Three keys only β€” no update. A fourth is silently dropped.
Full schemas
# Rejects the three built-in type names and a Resource ID.
variable "name" { type = string }

variable "resource_group_name" { type = string }

variable "automation_account_name" { type = string }

# The map key is the field NAME, which makes duplicates impossible to express.
variable "field" {
  type = map(object({
    type         = string
    is_encrypted = optional(bool, false)
    is_optional  = optional(bool, false)
  }))
}

variable "is_global" {
  type    = bool
  default = false
}

# NOTE three keys, not four -- this resource has no update.
variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    delete = optional(string)
  })
  default = null
}

ℹ️ There is no tags variable and no description variable, because the resource has neither attribute.


🧾 Outputs

Output Type Notes
id string The Resource ID. Nothing consumes it.
name string Compose with this. Force-new.
resource_group_name string Force-new.
automation_account_name string Force-new.
is_global bool Force-new.
field_names list(string) Sorted. The contract a connection must satisfy.
required_field_names list(string) Sorted.
optional_field_names list(string) Sorted.
encrypted_field_names list(string) Sorted.
plaintext_field_names list(string) Sorted. Scrutinise this.
field_types_in_use list(string) Sorted, distinct.
field_types_that_look_like_unqualified_primitives list(string) Reported, not rejected.
field_count number At least 1.
has_no_encrypted_fields bool Derived.
this_defines_a_type_it_does_not_create_a_connection bool Always true.
connections_reference_this_type_by_name_with_no_terraform_dependency bool Always true.
nothing_updates_in_place_every_argument_is_force_new bool Always true.
this_resource_carries_no_tags bool Always true.

πŸ”’ Nothing is sensitive, because a schema holds no values. is_encrypted is a promise about how a connection's value will be stored, not protection of anything in this resource.


🧠 Architecture Notes

A connection type is a schema, and that is the thing most likely to be misread about it. It declares which fields a connection may carry β€” names, types, whether each is encrypted or optional β€” and nothing else. It holds no endpoint, no credential and no value, so creating one connects to nothing and authenticates to nothing. The payoff comes only when a connection is built from it.

It also sits on the opposite side of the family from the three typed connection modules. azurerm_automation_connection_certificate and its two siblings hard-code Azure's built-in types (Azure, AzureClassicCertificate, AzureServicePrincipal) and never reference this resource. The only module in this library that can use a custom type is terraform-azurerm-automation-connection, the untyped form, via type = "<name>". So this module produces what that one consumes, and the three typed modules are unrelated to both.

The link between the two is a bare string, which makes name an interface. A connection names its type; nothing in Terraform expresses that dependency. Renaming this type β€” or changing any field, since the whole set is force-new β€” replaces the resource, and the connections built on it keep naming a type that no longer exists, with no plan diff, no warning and no error. It works in reverse too: a connection can name a type that was never created. The practical mitigation is composition rather than validation β€” pass this module's name output into the connection instead of repeating the literal string, which is the only thing that gives Terraform an ordering edge.

Every argument is force-new, which is unusual enough to state plainly. name, resource_group_name, automation_account_name, field and is_global all replace the resource. There is no in-place edit, which is also why the provider gives it only three timeouts β€” and that in turn creates the one input this module accepts silently and wrongly: a four-key timeouts object drops update without an error, because object-type conversion discards attributes the type does not declare.

field is typed as a map keyed by field name rather than a list, and the reasoning is worth stating because the provider models it as a repeatable block. Field names must be unique within a connection type. A map makes a duplicate impossible to express, where a list would need a validation to catch one β€” so the type system does the work instead of an error message. The cost is that map iteration does not preserve the provider's ordering, which is acceptable here because fields are addressed by name in a runbook ($Conn.ApiEndpoint) and never by position, so nothing observable depends on order.

Encryption is a per-field decision the module cannot make, so it validates the detectable half and reports the rest. A connection type routinely mixes credentials with plain identifiers, so defaulting is_encrypted to true would hide identifiers for no benefit and defaulting it to false β€” the provider's own default β€” leaves the judgement with the caller. What is detectable is a field whose name says secret while the flag says plaintext, so ApiSecret without is_encrypted = true is rejected with a message admitting it is a name-based guess. Everything else is surfaced through plaintext_field_names, because a field called ConnectionString can carry a credential without announcing it.

Field types are reported rather than rejected, and that is a deliberate limit on validation. Azure Automation conventionally uses .NET type names such as System.String, but the provider publishes no closed set β€” so a check refusing string might refuse something the API accepts. Rather than invent that constraint, the module emits field_types_that_look_like_unqualified_primitives and leaves the judgement to a reviewer. Where a probable mistake cannot be safely rejected, surfacing it beats blocking it.

There is nowhere on this resource to record why it exists. No tags, and β€” unlike all three typed connection resources β€” no description either. Both absences are in the provider's schema rather than in this module, and the consequence is that documentation for a custom connection type has to live outside Azure.


🧱 Design Principles

Concern This module's position Why
Duplicate field names Impossible by construction β€” field is a map. The type system beats a validation; a duplicate cannot be written.
Ordering vs uniqueness Uniqueness chosen, trade-off stated. Fields are addressed by name, never by position, so order is not observable.
is_encrypted Provider default kept, detectable mistake rejected, rest reported. A connection type mixes credentials with identifiers; only the caller knows which is which.
Field type Reported, never rejected. No closed set is published, so a check could refuse legal input.
Built-in type names Rejected. They already exist and are created elsewhere; reusing one is a misunderstanding.
is_global Reported, not described. The provider documents only the name, so this module claims nothing more.
Secrets None accepted, none emitted, nothing sensitive. A schema holds no values.
The invisible connection dependency Named in an output, mitigated by composition. The link is a string; only wiring the name output creates an edge.
Force-new everywhere Named in an output, with the replacement window explained. lifecycle is unavailable to a module caller.
tags and description Both absent, and both absences stated. Schema omissions, not module ones.

πŸš€ Runbook

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

Pin the module with ?ref=v1.0.0 β€” never a branch. Plan-only; a human applies from CI.

After the apply, confirm the contract Terraform cannot check:

# What fields does the type actually declare?
az automation connection-type show \
  --automation-account-name "aa-prod" --resource-group "rg-automation-prod" \
  --name "ContosoApi" --query "fieldDefinitions"

# Do any connections name this type?
az automation connection list \
  --automation-account-name "aa-prod" --resource-group "rg-automation-prod" \
  --query "[?connectionType.name=='ContosoApi'].name"

The second command is the closest thing to a dependency check that exists β€” Terraform has none.


πŸ§ͺ Testing

What the offline gate proves: that the configuration parses against the pinned provider, that formatting is canonical, and β€” via terraform console with variables files β€” that every validation {} fires on the input it targets. Proved with a failing value each: a Resource ID in name, resource_group_name and automation_account_name; an empty string in each of those three; a built-in type name in name; an empty field map; an empty map key; an empty field type; a Resource ID as a field type; a field name containing a space; and a secret-named field left unencrypted. Proved as accepted: that same field with is_encrypted = true, and a complete three-field type.

The derived outputs were evaluated rather than assumed. A three-field type produced the expected splits β€” required = ["ApiEndpoint","ApiSecret"], optional = ["Retries"], encrypted = ["ApiSecret"], plaintext = ["ApiEndpoint","Retries"], types = ["System.Int32","System.String"] β€” and field_types_that_look_like_unqualified_primitives was empty. A separate case using string and int confirmed the report-not-reject design in both directions: zero validation errors, and ["int","string"] in the report.

What only an apply exercises: whether the Automation Account exists in that resource group, whether Azure accepts the field types, and whether the caller holds Contributor.

What no Terraform run exercises at all: whether any connection names this type, whether a connection's values keys match these fields, and whether the fields left in the clear should have been encrypted.


πŸ’¬ Example Output

Outputs:

automation_account_name = "aa-prod"
connections_reference_this_type_by_name_with_no_terraform_dependency = true
encrypted_field_names = [
  "ApiSecret",
]
field_count = 3
field_names = [
  "ApiEndpoint",
  "ApiSecret",
  "Retries",
]
field_types_in_use = [
  "System.Int32",
  "System.String",
]
field_types_that_look_like_unqualified_primitives = []
has_no_encrypted_fields = false
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-automation-prod/providers/Microsoft.Automation/automationAccounts/aa-prod/connectionTypes/ContosoApi"
is_global = false
name = "ContosoApi"
nothing_updates_in_place_every_argument_is_force_new = true
optional_field_names = [
  "Retries",
]
plaintext_field_names = [
  "ApiEndpoint",
  "Retries",
]
required_field_names = [
  "ApiEndpoint",
  "ApiSecret",
]
resource_group_name = "rg-automation-prod"
this_defines_a_type_it_does_not_create_a_connection = true
this_resource_carries_no_tags = true

πŸ” Troubleshooting

Symptom Cause Fix
name is one of Azure's BUILT-IN connection type names Azure, AzureClassicCertificate or AzureServicePrincipal. Those exist already; name the custom type after the service it connects to.
field must define at least one field An empty map. The provider requires a minimum of one.
every field's map key is its NAME An empty map key. The key is the field name, not a label.
a field name contains a space "Api Endpoint". A runbook reads $Conn.<name>, so a space cannot be addressed.
HEURISTIC: a field whose NAME suggests it holds a secret ... ApiSecret etc. with is_encrypted left false. Set is_encrypted = true, or rename the field.
Types appear in field_types_that_look_like_unqualified_primitives string/int rather than System.String. Not an error. Review and use qualified .NET type names if that was unintended.
Plan wants to replace after adding one field The whole field set is force-new. Expected. Connections naming this type break during the window.
A timeouts key is ignored update was passed; this resource has only three timeouts. Remove it. Object conversion drops it silently.
Connection creation fails on a missing field A required field was omitted from the connection's values. Compare against required_field_names.
Connection creation fails on an unknown key A values key matches no field on the type. Compare against field_names.
A runbook reads an empty property The connection omitted an optional field, or the field name differs in case. Check field_names against the runbook's $Conn.<name>.
Renaming the type broke every connection Connections reference the type by name, with no Terraform dependency. Restore the name, or recreate the connections against the new one.
No tags or description argument accepted The resource has neither attribute. Tag the Automation Account; document the type outside Azure.

πŸ”— Related Docs

  • azurerm_automation_connection_type provider reference
  • Microsoft Learn β€” Manage connections in Azure Automation
  • Parent: terraform-azurerm-automation-account β€” supplies automation_account_name.
  • terraform-azurerm-automation-connection β€” the untyped connection module, and the only consumer of a custom type: pass this module's name as its type.
  • Sibling: terraform-azurerm-automation-connection-certificate β€” the built-in Azure type. Hard-codes its type and does not use this resource.
  • Sibling: terraform-azurerm-automation-connection-service-principal β€” the built-in AzureServicePrincipal type, the one member of that trio still targeting Azure Resource Manager.
  • Sibling: terraform-azurerm-automation-connection-classic-certificate β€” the built-in AzureClassicCertificate type.
  • Sibling: terraform-azurerm-automation-runbook β€” reads a connection by name at execution time.
  • This module's SCOPE.md.

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