Active Directory Domain Service
Microsoft Entra Domain Services (formerly Azure AD Domain Services, AADDS) provides a managed domain with classic Active Directory features — domain join, group policy, LDAP, and Kerberos/NTLM authentication — without you having to deploy, patch, or manage domain controllers. The active_directory_domain_service module creates and configures the managed domain in a dedicated subnet.
Note: A managed domain is a tenant-level singleton — you can create only one managed domain per Microsoft Entra tenant. The input variable is a map of objects to match the repo convention, but it should hold exactly one entry. Multi-region presence is achieved with replica sets (the same domain extended to other regions), not by deploying a second managed domain; replica sets are out of scope for this module.
Prerequisites
AADDS depends on a subscription-level registration and on tenant-level Microsoft Entra objects that this azurerm-only repository does not manage. Complete these before applying:
- Register the
Microsoft.AADresource provider — managed-domain creation fails until this is registered on the subscription: - Domain Services service principal — a service principal for the well-known Domain Services application must exist in the tenant. The portal wizard creates it automatically, but the
azurermresource does not, so create it once. The application ID is a fixed Microsoft constant —2565bd9d-da50-47d4-8b85-4c97f669dc36for Azure Global (6ba9a5d4-8456-4118-b521-9c5ca10cdf84for sovereign clouds): - AAD DC Administrators group — a security group named
AAD DC Administratorswhose members are granted administrative rights on the managed domain. Add at least one member. - A user with a synchronized password — Entra ID does not store Kerberos/NTLM-compatible hashes until AADDS is enabled, and hashes only sync forward. Any account that must authenticate against the managed domain has to reset its password after the domain is provisioned. (Deleting and recreating the domain deletes stored hashes, so this repeats each rebuild.)
- Networking — a dedicated, non-delegated subnet with an NSG, outbound connectivity, and (post-deploy) VNet DNS pointed at the domain controllers. See Networking.
See the Microsoft prerequisites documentation for details.
Basic Configuration
The module ships secure, idempotent defaults, so a working managed domain needs only three fields:
This creates a managed domain with these defaults applied automatically:
- SKU:
Standard(single-region; no replica sets) - Domain configuration type:
FullySynced(set explicitly to avoid a forced replacement on later plans — see the note under Configuration Parameters) - Security: TLS 1.2 enforced (
tls_v1_enabled = false), legacy NTLM v1 disabled, and Kerberos + NTLM password-hash sync enabled - Notifications: notify the AAD DC Administrators group on alerts (global admins not notified)
- Filtered sync: Disabled
- Location: Uses the location defined in the main configuration
Networking
The managed domain needs a correctly-shaped subnet, an NSG, outbound connectivity, and VNet DNS pointed at its domain controllers. The subnet, NSG, rules, NAT gateway, and DNS are all created by existing modules — only the wiring below is AADDS-specific.
Subnet
Use a dedicated subnet (Microsoft requires the managed domain to be alone in its subnet). It must not be delegated and not carry a custom route table that overrides the 0.0.0.0/0 route. A /24 is recommended (the domain needs 3–5 IPs; the DCs take the first two, e.g. .4 and .5).
# networks.tfvars — inside the VNet's subnets map
aadds = {
network_security_group = "aadds" # see NSG below
address_prefixes = ["10.40.46.0/24"]
nat_gateway = "hsw" # outbound — see below
}
NSG and required rule
Create a dedicated NSG for the subnet with a single required inbound rule: WinRM TCP 5986 from the AzureActiveDirectoryDomainServices service tag (used by the AADDS management plane). The Azure default rules AllowVnetInBound and AllowAzureLoadBalancerInBound cover the rest.
# rules.tfvars
AADDS_PSRemoting_Inbound = {
destination_port_ranges = ["5986"]
source_address_prefix = "AzureActiveDirectoryDomainServices"
}
# nsgs.tfvars
aadds = {
resource_group = "hsw"
rules = {
AADDS_PSRemoting_Inbound = "110"
}
}
Outbound connectivity
AADDS requires outbound 443 to several service tags (AzureActiveDirectoryDomainServices, AzureMonitor, Storage, AzureActiveDirectory, GuestAndHybridManagement). New subnets are created with default_outbound_access_enabled = false (azurerm 4.x is private-by-default), so attach a NAT gateway to the subnet (nat_gateway = "hsw" above) to guarantee outbound. A NAT gateway is supported with AADDS — it is not an NSG or UDR, so it does not trigger the unsupported-state warnings, and it affects only outbound (inbound management via the AADDS-managed load balancer is unaffected).
DNS
For VMs and apps to use the managed domain (domain-join, Kerberos/LDAP), the VNet's DNS must point at the domain controllers.
The virtual_network_dns_servers module sets this automatically from the domain service output — no hardcoded IPs. Opt a VNet in by naming the AADDS entry's key:
# networks.tfvars — on the VNet (network) entry, not the subnet
hsw = {
resource_group = "hsw"
address_space = ["10.40.44.0/22"]
active_directory_domain_service = "aadds" # -> VNet DNS = that domain's DC IPs
subnets = { ... }
}
How it works and why it's safe:
- The DC IPs are read from
module.active_directory_domain_serviceoutput, so they always track the live values. - No dependency cycle: DNS is a downstream resource (
virtual_network → subnet → domain service → dns_servers). Settingdns_serversinline on the VNet instead would cycle. - Correct ordering: because the DNS resource depends on the domain, Terraform applies it after the domain provisions, so the VNet has default DNS during creation (pointing VNet DNS at not-yet-existent DCs can otherwise block creation).
For a VNet that needs a manual DNS list instead (no AADDS), set dns_servers = ["x.x.x.x", ...] on the network entry. If both are set, the AADDS DC IPs win.
Configuration Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
resource_group |
string | Yes | - | Key of the resource group where the managed domain is deployed |
domain_name |
string | Yes* | - | The DNS domain name for the managed domain (e.g. aadds.contoso.com). Forces replacement if changed. *Not required when existing = true |
initial_replica_set |
object | Yes* | - | The initial replica set placement (see Initial Replica Set). *Not required when existing = true |
name |
string | No | Auto-generated | Display name of the managed domain resource. If omitted, uses the naming convention |
location |
string | No | Global location | Azure region where the managed domain is created |
existing |
bool | No | false |
Reference an existing managed domain via a data source instead of creating one (see Referencing an Existing Domain) |
sku |
string | No | Standard |
SKU: Standard, Enterprise, or Premium. Standard does not support replica sets |
domain_configuration_type |
string | No | FullySynced |
FullySynced or ResourceTrusting. Forces replacement if changed (see note below) |
filtered_sync_enabled |
bool | No | false |
Enables group-based (scoped) synchronization |
secure_ldap |
object | No | omitted | Secure LDAP (LDAPS) configuration (see Secure LDAP) |
notifications |
object | No | {} (defaults applied) |
Alert notification settings (see Notifications) |
security |
object | No | {} (defaults applied) |
Password sync and legacy protocol settings (see Security) |
timeouts |
object | No | omitted | Custom Terraform operation timeouts (create, read, update, delete). The azurerm default create timeout is 3 hours |
tags |
map(string) | No | {} |
Tags merged with default tags |
Initial Replica Set
The initial replica set is the first deployment of domain controllers and is required. It inherits the managed domain's location.
initial_replica_set = {
subnet = "hsw.aadds" # Required — key from the subnet variable (dot notation: <vnet_key>.<subnet_key>)
}
| Field | Type | Required | Description |
|---|---|---|---|
subnet |
string | Yes | Key of the dedicated subnet entry for the domain controllers. The module resolves the subnet ID internally |
Security
Controls password-hash synchronization and legacy protocol support.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
kerberos_armoring_enabled |
bool | No | false |
Enable Kerberos Armoring |
kerberos_rc4_encryption_enabled |
bool | No | false |
Enable Kerberos RC4 encryption |
ntlm_v1_enabled |
bool | No | false |
Enable legacy NTLM v1 support (leave disabled for security) |
sync_kerberos_passwords |
bool | No | true |
Synchronize Kerberos password hashes |
sync_ntlm_passwords |
bool | No | true |
Synchronize NTLM password hashes |
sync_on_prem_passwords |
bool | No | false |
Synchronize on-premises password hashes (enable only for hybrid/Entra Connect) |
tls_v1_enabled |
bool | No | false |
Enable legacy TLS v1. Leave false — setting true causes the platform to reject creation |
Notifications
notifications = {
additional_recipients = ["[email protected]"]
notify_dc_admins = true
notify_global_admins = false
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
additional_recipients |
list(string) | No | [] |
Additional email addresses to notify on managed-domain alerts |
notify_dc_admins |
bool | No | true |
Notify members of the AAD DC Administrators group |
notify_global_admins |
bool | No | false |
Notify all Global Administrators |
Secure LDAP
Enables LDAPS access to the managed domain. Omitted by default. Avoid enabling external_access_enabled unless internet-facing LDAPS is explicitly required.
secure_ldap = {
enabled = true
external_access_enabled = false
pfx_certificate = "<base64-encoded-pfx>"
pfx_certificate_password = "<pfx-password>"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled |
bool | Yes | - | Whether to enable secure LDAP |
external_access_enabled |
bool | No | false |
Whether to expose LDAPS over the internet |
pfx_certificate |
string | Yes | - | Base64-encoded TripleDES-SHA1 encrypted PKCS#12 (PFX) bundle |
pfx_certificate_password |
string | Yes | - | Password to decrypt the PFX bundle |
Referencing an Existing Domain
Because the managed domain is a tenant singleton, it often already exists. Set existing = true to reference it through a data source instead of creating it. Only name (resolved from the naming convention or set explicitly) and resource_group are needed in that case:
active_directory_domain_service = {
aadds = {
existing = true
name = "aadds.sapphirehealth.org"
resource_group = "hsw"
}
}
The module merges resource and data outputs, so downstream references work the same regardless of which path created the object.
Naming Convention
The managed domain display name is automatically generated using the following pattern:
For example, with the following prefixes and suffixes:
name_prefixes = {
active_directory_domain_service = "prod-"
}
name_suffixes = {
active_directory_domain_service = "-eastus2-aadds"
}
active_directory_domain_service = {
aadds = {
resource_group = "hsw"
domain_name = "aadds.sapphirehealth.org"
initial_replica_set = {
subnet = "hsw.aadds"
}
}
}
The resulting resource display name would be: prod-aadds-eastus2-aadds
Note: The display name is the ARM resource name and is independent of domain_name (the DNS namespace). To override automatic naming, specify a custom name.