Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure SQL Server DNS Alias Terraform Module

A stable hostname that points at an Azure SQL logical server, so the server behind it can change without touching a connection string. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • πŸ”— Publishes <alias>.database.windows.net, pointing at a SQL logical server.
  • 🌍 The name is globally unique across Azure, sharing a namespace with server names β€” so a first apply can fail for reasons outside your subscription.
  • πŸ”΄ This module cannot repoint an alias without dropping it. Both arguments are force-new, so a change is a delete and a create.
  • πŸ“ Mirrors the provider's name rule including a two-character minimum its own error message does not mention.
  • πŸ” No update function exists, so only three timeouts are real β€” and delete defaults to 10 minutes, not 30.

πŸ’‘ Why it matters: the point of an alias is to survive a server change. But mssql_server_id is force-new here, so repointing it in Terraform destroys the alias and recreates it β€” the hostname stops resolving in between, during exactly the cutover the alias was meant to smooth. Azure has an acquire operation that moves an alias without dropping it; the provider does not expose it.


❀️ Support this project

If this module saved you time:


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

flowchart TB
    SRV["terraform-azurerm-mssql-server"]
    THIS["terraform-azurerm-mssql-server-dns-alias"]
    OFR["terraform-azurerm-mssql-outbound-firewall-rule"]
    DB["terraform-azurerm-mssql-database"]
    SAP["terraform-azurerm-mssql-server-security-alert-policy"]

    SRV -->|"id as mssql_server_id"| THIS
    SRV -->|"id as server_id"| OFR
    SRV -->|"id"| DB
    SRV -->|"name and resource_group_name"| SAP
    THIS -.->|"applications connect here instead of the server"| SRV

    style THIS fill:#0078D4,stroke:#004578,color:#ffffff
    style SRV fill:#004578,stroke:#004578,color:#ffffff
    style OFR fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style DB fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style SAP fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading

The two edges from the server are labelled with the argument names because they differ: mssql_server_id here, server_id on the outbound firewall rule β€” same server, same provider release. The dotted edge back is the whole purpose of the resource: applications connect through the alias rather than to the server, which is what lets the server be replaced underneath them.


🧬 What this module builds

flowchart TB
    NAME["name (force-new) -- a GLOBAL DNS label"]
    SID["mssql_server_id (force-new)"]

    ALIAS["azurerm_mssql_server_dns_alias.this"]

    OID["id"]
    OREC["dns_record, read back from Azure"]
    OHOST["server_hostname, what the alias replaces"]
    ONOMOVE["this_module_cannot_repoint_an_alias_without_dropping_it"]

    NAME --> ALIAS
    SID --> ALIAS
    ALIAS --> OID
    ALIAS --> OREC
    ALIAS --> OHOST
    ALIAS --> ONOMOVE

    style ALIAS fill:#0078D4,stroke:#004578,color:#ffffff
    style NAME fill:#004578,stroke:#004578,color:#ffffff
    style SID fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OID fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OREC fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OHOST fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style ONOMOVE fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading
Resource Count Notes
azurerm_mssql_server_dns_alias.this 1 Both arguments force-new; dns_record is computed by Azure

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module β€” the caller configures the provider, its auth, and the mandatory features {} block

Schema notes that bite:

  • πŸ”΄ Both arguments are force-new, so repointing the alias is a delete-then-create and the hostname stops resolving in between. Azure's acquire operation, which moves an alias without dropping it, is not exposed by the provider.
  • πŸ”΄ The name is a GLOBAL DNS label, in the same namespace as SQL server names β€” a name taken by anyone else's server or alias fails at apply, and nothing in a plan can foresee it.
  • πŸ”΄ The name rule enforces a two-character minimum the provider's own error message does not mention: its regex requires a leading and a trailing alphanumeric.
  • πŸ”΄ There is no update function. The schema exposes only create, read and delete timeouts β€” timeouts.update is a non-existent attribute, not an ignored one, and Terraform would discard it silently.
  • ⚠️ The parent argument is mssql_server_id here and server_id on the sibling outbound firewall rule.
  • ⚠️ The delete timeout defaults to 10 minutes, not the 30 used by most neighbours in this family.
  • ℹ️ There IS a requires-import guard on create, so a clean apply is evidence the alias did not exist in your subscription β€” but not that the global name was free.
  • ℹ️ dns_record is computed, read back from the service.
  • ℹ️ No tags, no location.

πŸ”‘ Required Azure RBAC Roles / Permissions

Permission Scope Why
Microsoft.Sql/servers/dnsAliases/read and /write the SQL server create the alias
Microsoft.Sql/servers/dnsAliases/delete the SQL server there is no update β€” a change is a delete and a create
SQL Server Contributor (or Contributor) the SQL server the built-in role carrying the above

⚠️ Whoever can delete this resource can break every connection string pointed at the alias, instantly. The permission reads like a naming concern and behaves like an availability one.

ℹ️ No permission outside the subscription is involved, even though the name is globally scoped β€” the uniqueness check happens at apply, not through RBAC.


Azure Prerequisites

  • The Microsoft.Sql resource provider registered on the subscription.
  • An existing Azure SQL logical server.
  • A globally available alias name. It shares a namespace with every SQL server name in Azure.
  • The caller configures provider "azurerm" { features {} }, auth and subscription.

πŸ“ Module Structure

terraform-azurerm-mssql-server-dns-alias/
β”œβ”€β”€ providers.tf    # required_version + pinned azurerm; no provider block
β”œβ”€β”€ variables.tf    # 3 inputs, deeply typed, with the global-name rule
β”œβ”€β”€ main.tf         # the single keystone `this`
β”œβ”€β”€ outputs.tf      # id first, then both hostnames, then the posture facts
β”œβ”€β”€ README.md       # this file
β”œβ”€β”€ SCOPE.md        # the cross-module contract
β”œβ”€β”€ LICENSE         # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id
}

Applications now connect to orders-db.database.windows.net. Read example 3 before planning a migration around it β€” this module cannot move the alias to a different server without dropping it.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
name a global DNS label the caller
mssql_server_id Resource ID terraform-azurerm-mssql-server β†’ id

Emits

Output Description
id Resource ID of the alias (first)
dns_record the hostname as the service reports it
expected_hostname the same, derived from the configuration β€” compare the two
server_hostname what the alias replaces

πŸ“š Example Library

1 Β· The alias, and what it replaces
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id
}

output "connection_hosts" {
  value = {
    connect_here     = module.sql_alias.dns_record      # orders-db.database.windows.net
    instead_of_here  = module.sql_alias.server_hostname # sql-platform-eus2.database.windows.net
  }
}

πŸ’‘ Both hostnames are emitted deliberately. Seeing them side by side is what makes the point of the resource obvious: connection strings should carry the first, never the second.

2 Β· The service's answer versus the configuration's
output "sanity_check" {
  value = {
    reported = module.sql_alias.dns_record        # read back from Azure
    expected = module.sql_alias.expected_hostname # derived from `name`
  }
}

πŸ’‘ dns_record is authoritative; expected_hostname is this module's arithmetic. They should agree, and comparing them is a cheap check that the naming convention assumed here still holds.

3 Β· The migration this module cannot do in place
# Changing `mssql_server_id` DESTROYS the alias and recreates it.
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id # <-- pointing this elsewhere: delete then create
}

output "warning" {
  value = module.sql_alias.this_module_cannot_repoint_an_alias_without_dropping_it # true
}

πŸ”΄ The hostname stops resolving in between β€” during exactly the cutover the alias exists to smooth. That is the opposite of what anyone reaching for an alias wants.

πŸ’‘ Azure has an acquire operation that moves an existing alias between servers without dropping it. The provider does not expose it, so that move happens outside Terraform and the state is reconciled afterwards. Plan the migration around that, not around a terraform apply.

4 Β· The two-character minimum nobody mentions
# REFUSED -- the provider's regex requires a leading AND a trailing alphanumeric:
#   name = "a"
#
# ACCEPTED:
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "ab"
  mssql_server_id = module.sql_server.id
}

⚠️ The provider's own error message talks only about permitted characters and hyphen placement β€” it never mentions a length floor. Its regex enforces one anyway. This module's message states it explicitly, because a one-character alias fails with a message that appears to be about something else.

5 Β· A name that is globally taken
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "sql" # almost certainly taken by someone, somewhere
  mssql_server_id = module.sql_server.id
}

πŸ”΄ The alias shares a namespace with every SQL server name in Azure. A generic name fails at apply with a conflict caused by a resource you cannot see, in a subscription you do not own.

πŸ’‘ The requires-import guard only tells you the alias does not exist in your subscription β€” a clean plan is not evidence the global name is free.

6 Β· Naming it for the application, not the server
# GOOD -- survives the server being replaced:
#   name = "orders-db"
#
# SELF-DEFEATING -- names the thing the alias exists to hide:
#   name = "sql-platform-eus2-alias"
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id
}

πŸ’‘ An alias named after its current server becomes misleading the moment the server changes β€” which is the one event the alias was created for.

7 Β· Changing the name breaks everything pointed at it
# Editing `name` destroys one alias and creates another.

πŸ”΄ The old hostname stops resolving at that moment. Anything still connecting through it fails immediately β€” which is the opposite of what the resource is for, and worth a change-control conversation rather than a routine apply.

8 Β· There is no update, and no update timeout
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id

  timeouts = {
    create = "45m"
    delete = "15m" # the provider's default here is 10, not 30
  }
}

πŸ”΄ timeouts.update does not exist on this type. Both arguments are force-new, so the provider defines no update function β€” an update key would be discarded silently by Terraform's object-type conversion, which is why this module's type declares only three.

⚠️ The delete default is 10 minutes, not the 30 most neighbours use. the_three_timeouts_that_exist emits all three with their real defaults.

9 Β· The parent argument is spelled differently on the sibling
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id # <-- mssql_server_id
}

module "sql_outbound_storage" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-outbound-firewall-rule.git?ref=v1.0.0"

  name      = "stsqlaudit0001.blob.core.windows.net"
  server_id = module.sql_server.id # <-- server_id
}

⚠️ Same server, same provider release, two spellings. A configuration copied between the two fails on the argument name β€” loudly, at parse time, which is the safe direction for a mismatch.

10 Β· Several aliases for several servers
module "sql_alias" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"
  for_each = module.sql_servers

  name            = "${each.key}-db"
  mssql_server_id = each.value.id
}

output "connection_hosts" {
  value = { for k, m in module.sql_alias : k => m.dns_record }
}

πŸ’‘ Keying by a stable name means adding or removing a server never re-indexes the rest, and the output hands every application its connection host in one map.

⚠️ Each alias name must still be globally unique β€” a per-environment prefix is usually what makes that true.

11 Β· Verifying the alias is actually being used
output "audit" {
  value = {
    should_connect_to  = module.sql_alias.dns_record
    must_not_appear_in_config = module.sql_alias.server_hostname
    point_of_the_alias = module.sql_alias.connecting_through_the_alias_is_the_whole_point
  }
}

πŸ’‘ An alias nothing connects through achieves nothing, and no Terraform resource can verify that anything does. Emitting both hostnames at least makes the check greppable β€” the server hostname should not appear in any application configuration.

12 Β· πŸ—οΈ End-to-end composition
provider "azurerm" {
  features {}
}

module "sql_server" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server.git?ref=v1.0.0"

  name                = "sql-platform-eus2"
  resource_group_name = "rg-data-platform"
  location            = "eastus2"

  azuread_administrator = {
    login_username = "sql-admins"
    object_id      = var.sql_admin_group_object_id
  }
}

module "orders_db" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-database.git?ref=v1.0.0"

  name      = "orders"
  server_id = module.sql_server.id
}

# The stable hostname applications use. Named for the application, so it
# survives the server being replaced.
module "sql_alias" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-server-dns-alias.git?ref=v1.0.0"

  name            = "orders-db"
  mssql_server_id = module.sql_server.id
}

# Outbound rules govern traffic FROM the server; the alias governs how clients
# reach it. Orthogonal, and both usually needed.
module "sql_outbound_audit" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-outbound-firewall-rule.git?ref=v1.0.0"

  name      = "stsqlaudit0001.blob.core.windows.net"
  server_id = module.sql_server.id
}

output "application_connection" {
  value = {
    host     = module.sql_alias.dns_record
    database = module.orders_db.name
    # Deliberately emitted so a reviewer can confirm it appears NOWHERE else.
    do_not_use = module.sql_alias.server_hostname
  }
}

πŸ’‘ The alias is named orders-db rather than after the server, which is what lets sql-platform-eus2 be replaced later without an application change.

πŸ”΄ But not through this module. Repointing the alias is force-new here; the in-place move needs Azure's acquire operation, which the provider does not expose.


πŸ“₯ Inputs

Identity β€” name (required, force-new, globally unique), mssql_server_id (required, force-new). Tail β€” timeouts, with three keys. (There is no tags: the resource exposes none.)

Full schemas
Name Type Default Notes
name string β€” Required, force-new. Lowercase, digits, hyphens; no leading/trailing hyphen; at least two characters. Globally unique.
mssql_server_id string β€” Required, force-new. Anchored; a managed instance ID is refused. Note the spelling.
timeouts object({create, read, delete}) null No update. Delete defaults to 10 minutes.

🧾 Outputs

Output Description
id Resource ID of the alias (first)
name the alias label
dns_record the hostname as the service reports it
expected_hostname the same, derived from the configuration
server_hostname what the alias replaces
mssql_server_id / server_name / resource_group_name / subscription_id the parent
the_alias_name_is_globally_unique_across_azure constant true
connecting_through_the_alias_is_the_whole_point constant true
this_module_cannot_repoint_an_alias_without_dropping_it constant true
changing_the_name_breaks_every_connection_string constant true
the_parent_argument_is_named_mssql_server_id_here constant true
the_name_rule_requires_at_least_two_characters constant true
this_resource_has_no_update_function constant true
the_delete_timeout_default_is_ten_minutes_not_thirty constant true
an_existing_alias_blocks_creation constant true
force_new_fields / fields_azure_returns_on_read lifecycle
the_three_timeouts_that_exist the deadlines that are real, with defaults
this_resource_supports_no_azure_resource_tags constant true

πŸ”’ No secret is accepted or emitted.


🧠 Architecture Notes

The alias exists to outlive a server, and this resource cannot move it. That tension is the module's central fact. mssql_server_id is force-new, so repointing the alias in Terraform destroys it and creates it again β€” and the hostname stops resolving in between, during precisely the cutover the alias was meant to make invisible. Azure's acquire operation moves an alias between servers without dropping it; the provider does not expose it, so that move belongs outside Terraform and is reconciled into state afterwards. The module emits this rather than letting it be discovered by a plan showing -/+.

The name is global, and a plan cannot see that. The alias shares a namespace with every SQL server name in Azure, so a first apply can fail because of a resource in someone else's subscription. The requires-import guard checks your subscription only β€” a clean plan is not evidence the name is free.

One rule is stricter than its own error message. The provider's regex requires a leading and a trailing alphanumeric, which makes a one-character alias illegal β€” while the message it prints talks only about permitted characters and hyphen placement. This module mirrors the regex exactly and says the minimum out loud, because otherwise the failure reads as being about something else.

There is genuinely no update. Both arguments are force-new, so the schema carries three timeouts rather than four. timeouts.update is not ignored here β€” it is not an attribute of the type, and an undeclared key is discarded silently by Terraform's object-type conversion. The module declares three and emits their real defaults, including a delete deadline of 10 minutes rather than the family's usual 30.

Two hostnames are emitted on purpose. dns_record is what the service says; server_hostname is what the alias replaces. Putting both in the outputs makes the substitution greppable β€” the server hostname should appear in no application configuration anywhere.


🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
The alias name no default β€” it is globally scoped and cannot be guessed β€”
Name shape the provider's rule, mirrored exactly, minimum stated β€”
Global availability not checkable offline β€” reported, never implied β€”
Repointing in place not offered by the provider β€” said plainly β€”
Secret handling none β€” this resource names a hostname not available

πŸš€ Runbook

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

Pin the module with ?ref=v1.0.0 β€” never a branch. This module is plan-only in this repository; a human applies from CI.


πŸ§ͺ Testing

terraform validate proves, offline and with no credentials: the name rule in both directions β€” uppercase, underscores, leading and trailing hyphens, a dotted hostname and the undocumented one-character case are all refused, while two-character, hyphenated and digit-bearing names pass β€” and that mssql_server_id is an anchored SQL server ID rather than a managed instance.

Only terraform plan and apply exercise: whether the server exists, whether the alias already exists in your subscription, and whether the name is available globally.

Nothing at either stage tells you that repointing will drop the hostname, or that the alias is not actually being used. That is what the constant outputs and the paired hostnames are for.

Note the asymmetry: a validation failure blocks terraform destroy as well as apply, which is why the name check mirrors the provider's regex exactly rather than adding to it.


πŸ’¬ Example Output

id                = "/subscriptions/.../resourceGroups/rg-data-platform/providers/Microsoft.Sql/servers/sql-platform-eus2/dnsAliases/orders-db"
name              = "orders-db"
dns_record        = "orders-db.database.windows.net"
expected_hostname = "orders-db.database.windows.net"
server_hostname   = "sql-platform-eus2.database.windows.net"
server_name       = "sql-platform-eus2"
force_new_fields  = ["name", "mssql_server_id"]

πŸ” Troubleshooting

Symptom Cause Fix
name must be made up of lowercase letters, numbers and hyphens uppercase, an underscore, a dot, or a leading/trailing hyphen It is a DNS label, not a hostname β€” no dots
The same message for a one-character name the provider's regex requires a leading and trailing alphanumeric Use at least two characters. The provider's own message does not mention this
mssql_server_id must be an Azure SQL SERVER Resource ID a managed instance ID, or server_id copied from the outbound-firewall-rule module Note the spelling: mssql_server_id here
Apply fails with a name conflict, and the alias does not exist in your subscription the name is taken globally β€” it shares a namespace with all SQL server names Choose a more specific name; a per-environment prefix usually helps
A plan wants to destroy and recreate after changing the server mssql_server_id is force-new Expected. Use Azure's acquire operation outside Terraform for an in-place move
Applications broke immediately after a rename the old hostname stopped resolving the moment the alias was replaced Expected. Treat a rename as a change-controlled event
timeouts.update appears to do nothing it is not an attribute of this type at all Use create and delete; there is no update function
A destroy took longer than expected to time out the delete default here is 10 minutes, not 30 Raise timeouts.delete if needed
A requires-import error on first apply the alias already exists in your subscription Expected. terraform import it

πŸ”— Related Docs


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