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.
- βοΈ Creates one
azurerm_automation_connection_type(the keystonethis) β a reusable field schema for connections in one Automation Account. - 𧬠Takes
fieldas 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
updatetimeout. - π Rejects a field whose name says secret while
is_encryptedis leftfalseβ 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.
If this module saves you time:
- β Star the repository β it helps others find it.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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.
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;
| 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.
| 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,fieldandis_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
updatetimeout, onlycreate,readanddelete. A four-keytimeoutsobject silently dropsupdateβ object conversion discards undeclared attributes without an error. - π΄
fieldrequires at least one entry (the provider setsmin = 1). - π΄ Connections reference the type by NAME, as a string. No Terraform dependency exists in either direction.
β οΈ There is nodescriptionargument β unlike all three typed connection resources, which take one. So there is nowhere on this resource to record why it exists.β οΈ Notagsattribute either.β οΈ The provider documentsis_globalas "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 fieldtypevalues is published. Azure Automation conventionally uses .NET type names such asSystem.String; this module reports suspicious values rather than rejecting them.β οΈ lifecycleis not valid inside amoduleblock, so a caller cannot addprevent_destroy.
| 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
ApiSecretexists 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_encryptedis 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.
- 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,AzureClassicCertificateandAzureServicePrincipalare rejected. - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription; this module declares none of these.
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
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.
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 |
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 = 1is 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_optionaldefaults tofalse, 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_namesis 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 β
ConnectionStringorEndpointcan carry one without saying so β but a field literally calledApiSecretstored in the clear is worth stopping for. The message says it is a name-based guess.
β οΈ The names it matches arepassword,secret,token,credential,apikey,api_keyandpassphrase, 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
stringcould 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.Int32and 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
valuesagainst 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.nameis 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'svalueskeys match step 2's fields. The contract is real and enforced by Azure at connection creation, not by Terraform β which is whycontract_the_connection_must_satisfyis 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.
| 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
tagsvariable and nodescriptionvariable, because the resource has neither attribute.
| 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_encryptedis a promise about how a connection's value will be stored, not protection of anything in this resource.
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.
| 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. |
terraform init -backend=false
terraform validate
terraform fmt -checkPin 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.
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.
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
| 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. |
azurerm_automation_connection_typeprovider reference- Microsoft Learn β Manage connections in Azure Automation
- Parent:
terraform-azurerm-automation-accountβ suppliesautomation_account_name. terraform-azurerm-automation-connectionβ the untyped connection module, and the only consumer of a custom type: pass this module'snameas itstype.- Sibling:
terraform-azurerm-automation-connection-certificateβ the built-inAzuretype. Hard-codes its type and does not use this resource. - Sibling:
terraform-azurerm-automation-connection-service-principalβ the built-inAzureServicePrincipaltype, the one member of that trio still targeting Azure Resource Manager. - Sibling:
terraform-azurerm-automation-connection-classic-certificateβ the built-inAzureClassicCertificatetype. - 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."