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.
- π 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_idis 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.
If this module saved you time:
- β Star the repository
- πΌ Connect on LinkedIn
- β Buy me a coffee
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
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.
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
| Resource | Count | Notes |
|---|---|---|
azurerm_mssql_server_dns_alias.this |
1 | Both arguments force-new; dns_record is computed by Azure |
| 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,readanddeletetimeouts βtimeouts.updateis a non-existent attribute, not an ignored one, and Terraform would discard it silently. β οΈ The parent argument ismssql_server_idhere andserver_idon 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_recordis computed, read back from the service. - βΉοΈ No
tags, nolocation.
| 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.
- The
Microsoft.Sqlresource 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.
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
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.
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 |
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_recordis authoritative;expected_hostnameis 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.updatedoes not exist on this type. Both arguments are force-new, so the provider defines no update function β anupdatekey 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_existemits 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-dbrather than after the server, which is what letssql-platform-eus2be 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.
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. |
| 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.
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.
| 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 |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 β never a branch. This module is plan-only in this repository; a human
applies from CI.
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.
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"]
| 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 |
azurerm_mssql_server_dns_aliasMicrosoft.Sql/servers/dnsAliasestemplate reference- Azure SQL Database connectivity architecture
- Sibling modules:
terraform-azurerm-mssql-server,terraform-azurerm-mssql-outbound-firewall-rule,terraform-azurerm-mssql-database,terraform-azurerm-mssql-server-security-alert-policy - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."