Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Netapp Account Terraform Module

A hardened, standalone Terraform module that manages a single Azure NetApp Files account — the regional parent that capacity pools, volumes, and snapshots are created under — targeting hashicorp/azurerm ~> 4.0.


Terraform azurerm Module Version Type Resources


🧩 Overview

  • 🏦 Creates one azurerm_netapp_account — the keystone this — as the regional parent for Azure NetApp Files.
  • 🪪 Renders an optional managed identity block (SystemAssigned or UserAssigned) used to reach a Key Vault key for customer-managed key (CMK) encryption.
  • 🔗 Renders an optional active_directory block for SMB and dual-protocol volumes, with AES encryption and LDAP signing on by default.
  • 🔐 Keeps the account credentials-free by default: no Active Directory join, no identity, and no plaintext secret in state.
  • 🧱 Leaves capacity pools, volumes, snapshots, backup vaults/policies, diagnostics, private connectivity, CMK linkage, and RBAC to sibling modules — consumed by id.

💡 Why it matters: The NetApp account is a lightweight control-plane object, but it is where the Active Directory join and the encryption identity live. Getting AES encryption, LDAP signing, and secret handling right here is what lets every downstream volume mount securely. The empty call yields an account with no embedded credentials and no AD exposure; each relaxation is an explicit, reviewable opt-in.

❤️ Support this project

If this module saves you time, please consider supporting its continued development:

🗺️ Where this fits in the family

flowchart LR
  rg["terraform-azurerm-resource-group"]
  kv["terraform-azurerm-key-vault"]
  vnet["terraform-azurerm-virtual-network: subnet delegated to Microsoft.NetApp/volumes"]
  acct["terraform-azurerm-netapp-account"]
  enc["terraform-azurerm-netapp-account-encryption: who holds the key"]
  pool["terraform-azurerm-netapp-pool: the BILLED capacity"]
  vol["terraform-azurerm-netapp-volume: what clients mount"]
  snap["terraform-azurerm-netapp-snapshot: one on-demand, in the pool"]
  spol["terraform-azurerm-netapp-snapshot-policy: the schedule"]
  vault["terraform-azurerm-netapp-backup-vault: the destination"]
  bpol["terraform-azurerm-netapp-backup-policy: the retention"]
  vghana["terraform-azurerm-netapp-volume-group-sap-hana"]
  vgora["terraform-azurerm-netapp-volume-group-oracle"]

  rg -->|"resource_group_name, location"| acct
  kv -->|"encryption_key URI"| enc
  acct -->|"id, BY ID"| enc
  acct -->|"account_name, BY NAME"| pool
  acct -->|"account_name, BY NAME"| vault
  acct -->|"account_name, BY NAME"| bpol
  acct -->|"account_name, BY NAME"| spol
  acct -->|"account_name, BY NAME"| vghana
  acct -->|"account_name, BY NAME"| vgora
  pool -->|"pool_name, BY NAME"| vol
  pool -->|"capacity_pool_id, BY ID"| vghana
  pool -->|"capacity_pool_id, BY ID"| vgora
  vnet -->|"subnet_id, must be delegated"| vol
  vol -->|"name plus pool_name, BY NAME"| snap
  spol -->|"snapshot_policy_id, BY ID"| vol
  vault -->|"backup_vault_id, BY ID"| vol
  bpol -->|"backup_policy_id, BY ID"| vol

  classDef me fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
  classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
  class acct keystone;
  class enc,pool,vol,snap,spol,vault,bpol,vghana,vgora me;
  class rg,kv,vnet sib;
Loading

🧬 What this module builds

graph TD
  name["name"]
  rgn["resource_group_name"]
  loc["location"]
  idv["identity input"]
  adv["active_directory input"]
  tagsv["tags"]

  acct["azurerm_netapp_account.this"]
  idblk["identity block: SystemAssigned or UserAssigned"]
  adblk["active_directory block: SMB, Kerberos, LDAP"]

  oid["output: id"]
  oname["output: name"]
  opid["output: principal_id"]
  otid["output: tenant_id"]

  name -->|"required, force-new"| acct
  rgn -->|"required, force-new"| acct
  loc -->|"required, force-new"| acct
  tagsv --> acct
  idv --> idblk
  adv --> adblk
  idblk --> acct
  adblk --> acct
  acct --> oid
  acct --> oname
  acct -->|"identity[0].principal_id"| opid
  acct -->|"identity[0].tenant_id"| otid

  classDef this fill:#0078D4,color:#fff,stroke:#004578,stroke-width:2px;
  classDef keystone fill:#004578,color:#fff,stroke:#004578,stroke-width:2px;
  classDef ext fill:#eef2f7,color:#1b1f24,stroke:#c3cbd6;
  class idblk,adblk this;
  class acct keystone;
  class name,rgn,loc,idv,adv,tagsv,oid,oname,opid,otid ext;
Loading

Resource inventory

Resource Count Role
azurerm_netapp_account.this 1 Keystone; the regional NetApp Files account
identity (nested block) 0–1 Managed identity for CMK encryption
active_directory (nested block) 0–1 AD/LDAP connection for SMB and dual-protocol volumes
timeouts (nested block) 0–1 Optional operation timeouts

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider hashicorp/azurerm ~> 4.0
Provider block Not declared here — the caller configures provider "azurerm" { features {} }, auth, and subscription

Schema notes that bite (verified against the live provider schema):

  • 🔒 name, resource_group_name, and location are all force-new — changing any of them replaces the account (and orphans downstream pools/volumes if they are not re-parented).
  • 🪪 Changing identity.type from SystemAssigned to UserAssigned is supported in place, but the reverse is not supported from Terraform.
  • 🧾 identity.identity_ids is required when identity.type = "UserAssigned".
  • 🌐 Azure allows only one Active Directory connection per subscription per NetApp account.
  • 🔐 active_directory.server_root_ca_certificate and active_directory.ldap_over_tls_enabled are mutually required by presence, not by value — the provider declares RequiredWith on each pointing at the other, and the SDK checks whether the argument is present, never what it is set to. Supplying ldap_over_tls_enabled = false therefore demands a certificate exactly as true does. Omit both, or supply both.
  • 🎟️ Kerberos volumes require both active_directory.kerberos_ad_name and active_directory.kerberos_kdc_ip.
  • ♻️ On import, active_directory.password and active_directory.server_root_ca_certificate cannot be read back from Azure and must be redeclared in configuration.
  • 🧱 The account itself has no public-network-access argument — data-plane reachability is governed by the delegated subnets that volumes (sibling modules) are placed in.

🔑 Required Azure RBAC Roles / Permissions

Least-privilege, scoped to the target resource group:

  • A custom role granting Microsoft.NetApp/netAppAccounts/* (create/read/update/delete the account), or the built-in Contributor role on the resource group.
  • When a managed identity is attached for CMK, the identity additionally needs key permissions on the target Key Vault key (Key Vault Crypto Service Encryption User on the key). That grant is applied by the account-encryption sibling module, not here.

ℹ️ The role must be held by the caller's authenticated identity (the provider's principal), not by the module. The module never authenticates.

Azure Prerequisites

  • An existing resource group in a supported US Azure region (for example eastus2, westus2).
  • The Microsoft.NetApp resource provider registered on the target subscription.
  • For customer-managed keys: an existing Key Vault and key, plus a managed identity granted access to the key (wired through the account-encryption sibling module).
  • For Active Directory joins: reachable AD DNS servers and a domain-join account; for LDAP over TLS, a base64-encoded root CA certificate.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; this module declares none of them.

📁 Module Structure

terraform-azurerm-netapp-account/
├── providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf   # deeply-typed object() schemas + tags/timeouts tail
├── main.tf        # keystone azurerm_netapp_account.this; dynamic identity/active_directory/timeouts
├── outputs.tf     # id first, then name, then principal_id / tenant_id
├── README.md      # this document
├── SCOPE.md       # the cross-module contract (lightweight, standalone)
├── LICENSE        # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore     # canonical library ignore set

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-platform-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  tags = {
    environment = "prod"
    owner       = "platform-team"
  }
}

ℹ️ The caller owns the provider "azurerm" { features {} } block, authentication, and the subscription. Pin the module with ?ref=v1.0.0 — never a branch.

🔌 Cross-Module Contract

Consumes

Input Type Source
resource_group_name string terraform-azurerm-resource-group output name
location string caller / resource group location
identity.identity_ids set(string) terraform-azurerm-user-assigned-identity output id

Emits

Output Description Consumed by
id NetApp Account Resource ID (first) terraform-azurerm-monitor-diagnostic-setting, terraform-azurerm-role-assignments, and terraform-azurerm-netapp-account-encryption (netapp_account_id) — note the pool, volume, backup-vault, backup-policy and snapshot-policy modules consume name, not this
name NetApp Account name terraform-azurerm-netapp-pool, -backup-vault, -backup-policy, -snapshot-policy, -volume-group-sap-hana, -volume-group-oracle (account_name), and terraform-azurerm-netapp-volume / -snapshot — the whole family addresses the account by name; also diagnostics and tagging
principal_id System-assigned identity principal (object) ID, or null terraform-azurerm-role-assignments, Key Vault access for CMK
tenant_id Identity tenant ID, or null documentation / CMK wiring

📚 Example Library

1 · Minimal, hardened base account

The empty call: no identity, no Active Directory, no embedded secret. This is the secure baseline.

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-core-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"
}

🔒 No credentials are stored and no AD is joined until you opt in.

2 · Account with organizational tags
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-data-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  tags = {
    environment = "prod"
    cost_center = "1234"
    data_class  = "internal"
  }
}

💡 tags is near-universal on ARM resources and drives cost and governance reporting.

3 · System-assigned identity (CMK-ready)
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-cmk-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  identity = {
    type = "SystemAssigned"
  }
}

💡 The account's principal_id output is then granted access to a Key Vault key by the account-encryption sibling module.

4 · User-assigned identity for CMK
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-uai-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  identity = {
    type         = "UserAssigned"
    identity_ids = [module.netapp_identity.id]
  }
}

⚠️ identity_ids is required when type = "UserAssigned". Only one identity type is supported at a time — the combined "SystemAssigned, UserAssigned" value is not valid for NetApp Files.

5 · Active Directory join for SMB

The base AD connection needed before SMB volumes can be created. AES encryption and LDAP signing are on by default.

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-smb-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  active_directory = {
    dns_servers     = ["10.0.0.4", "10.0.0.5"]
    domain          = "corp.example.com"
    smb_server_name = "ANFSMB01"
    username        = "svc-anf-join"
    password        = var.ad_join_password # provisioned out of band
  }
}

🔒 password is sensitive; supply it from a Key Vault reference or a CI secret — never commit a literal.

6 · AD with organizational unit and site
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-smb-ou-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  active_directory = {
    dns_servers         = ["10.0.0.4"]
    domain              = "corp.example.com"
    smb_server_name     = "ANFSMB02"
    username            = "svc-anf-join"
    password            = var.ad_join_password
    organizational_unit = "OU=NetApp,OU=Servers,DC=corp,DC=example,DC=com"
    site_name           = "EastUS2"
  }
}

ℹ️ Omitting organizational_unit defaults to CN=Computers; omitting site_name defaults to Default-First-Site-Name.

7 · AES encryption plus Kerberos volumes
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-kerberos-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  active_directory = {
    dns_servers            = ["10.0.0.4"]
    domain                 = "corp.example.com"
    smb_server_name        = "ANFSMB03"
    username               = "svc-anf-join"
    password               = var.ad_join_password
    aes_encryption_enabled = true # already the module default
    kerberos_ad_name       = "ANFKRB"
    kerberos_kdc_ip        = "10.0.0.6"
  }
}

⚠️ Kerberos volumes need both kerberos_ad_name and kerberos_kdc_ip; supplying only one has no effect at volume-create time.

8 · Adjusting LDAP signing

ldap_signing_enabled defaults to true (the provider default is false). Only disable it for a directory that cannot sign LDAP traffic.

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-ldap-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  active_directory = {
    dns_servers          = ["10.0.0.4"]
    domain               = "corp.example.com"
    smb_server_name      = "ANFSMB04"
    username             = "svc-anf-join"
    password             = var.ad_join_password
    ldap_signing_enabled = false # opt-out of the secure default
  }
}

⚠️ Disabling LDAP signing weakens directory-traffic integrity; keep the default true unless a directory constraint forces otherwise.

9 · LDAP over TLS with a root CA certificate
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-ldaps-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  active_directory = {
    dns_servers                = ["10.0.0.4"]
    domain                     = "corp.example.com"
    smb_server_name            = "ANFSMB05"
    username                   = "svc-anf-join"
    password                   = var.ad_join_password
    ldap_over_tls_enabled      = true
    server_root_ca_certificate = var.ldap_root_ca # base64 PEM, provisioned out of band
  }
}

🔒 server_root_ca_certificate is required when ldap_over_tls_enabled = true and is sensitive — the module enforces this at plan time.

10 · Dual-protocol: local NFS users with LDAP
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-dual-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  active_directory = {
    dns_servers                       = ["10.0.0.4"]
    domain                            = "corp.example.com"
    smb_server_name                   = "ANFSMB06"
    username                          = "svc-anf-join"
    password                          = var.ad_join_password
    local_nfs_users_with_ldap_allowed = true
  }
}

⚠️ Enabling local_nfs_users_with_ldap_allowed widens access to local NFS users in addition to LDAP users; it defaults to false.

11 · Custom operation timeouts
module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-timeouts-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  timeouts = {
    create = "45m"
    delete = "45m"
  }
}

ℹ️ Azure defaults are 30m create, 5m read, 30m update, 30m delete. Only override what you need.

12 · Many accounts at scale with for_each

Instantiate one account per region/environment from a keyed map in the root module.

locals {
  netapp_accounts = {
    prod_eastus2 = { name = "anf-prod-e2", resource_group_name = "rg-netapp-prod-eastus2", location = "eastus2" }
    prod_westus2 = { name = "anf-prod-w2", resource_group_name = "rg-netapp-prod-westus2", location = "westus2" }
    dr_centralus = { name = "anf-dr-cus", resource_group_name = "rg-netapp-dr-centralus", location = "centralus" }
  }
}

module "netapp_account" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"
  for_each = local.netapp_accounts

  name                = each.value.name
  resource_group_name = each.value.resource_group_name
  location            = each.value.location

  tags = { environment = split("_", each.key)[0] }
}

💡 Keying by a stable string means adding or removing one account never re-plans the others.

13 · Least-privilege secret handling

Pull the AD join password from Key Vault at plan time instead of a variable literal.

data "azurerm_key_vault_secret" "ad_join" {
  name         = "anf-ad-join-password"
  key_vault_id = var.secrets_key_vault_id
}

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-secure-prod"
  resource_group_name = "rg-netapp-prod-eastus2"
  location            = "eastus2"

  identity = { type = "SystemAssigned" }

  active_directory = {
    dns_servers     = ["10.0.0.4"]
    domain          = "corp.example.com"
    smb_server_name = "ANFSMB07"
    username        = "svc-anf-join"
    password        = data.azurerm_key_vault_secret.ad_join.value
  }
}

🔒 The whole active_directory object is marked sensitive, so neither the password nor the root CA appears in plan output. They are still written to state in plaintext — and this provider cannot read either value back from Azure, so state holds the only copy. Protect the backend accordingly.

14 · Wiring the resource-group sibling
module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-netapp-prod-eastus2"
  location = "eastus2"
}

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-wired-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

💡 Feeding module.rg.name and module.rg.location establishes the implicit create-order — the resource group is created first.

15 · 🏗️ End-to-end composition

Resource group → user-assigned identity → this NetApp account (with AD + CMK-ready identity) → account-encryption → capacity pool → volume. This module owns only the account; the pool and volume are sibling modules that consume its id.

provider "azurerm" {
  features {}
}

module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-netapp-prod-eastus2"
  location = "eastus2"
}

module "netapp_identity" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
  name                = "id-anf-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

module "netapp_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account.git?ref=v1.0.0"

  name                = "anf-platform-prod"
  resource_group_name = module.rg.name
  location            = module.rg.location

  identity = {
    type         = "UserAssigned"
    identity_ids = [module.netapp_identity.id]
  }

  active_directory = {
    dns_servers     = ["10.0.0.4", "10.0.0.5"]
    domain          = "corp.example.com"
    smb_server_name = "ANFSMB00"
    username        = "svc-anf-join"
    password        = var.ad_join_password
  }

  tags = { environment = "prod" }
}

# CMK linkage — sibling module. Consumes this account BY ID (unlike the pool/volume modules,
# which take the account name) plus one of its identities.
# module "netapp_encryption" {
#   source            = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-account-encryption.git?ref=v1.0.0"
#   netapp_account_id = module.netapp_account.id
#   encryption_key    = var.key_vault_key_id
#
#   system_assigned_identity_principal_id = module.netapp_account.principal_id
#   # The identity still needs Get / Wrap Key / Unwrap Key on the key — a separate grant.
# }

# Capacity pool — sibling module (consumes this account BY NAME, not by id):
# module "netapp_pool" {
#   source          = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-pool.git?ref=v1.0.0"
#   account_name    = module.netapp_account.name
#   ...
# }

# Volume — sibling module (consumes the pool + a delegated subnet):
# module "netapp_volume" {
#   source       = "git::https://github.com/microsoftexpert/terraform-azurerm-netapp-volume.git?ref=v1.0.0"
#   account_name = module.netapp_account.name
#   ...
# }

💡 The account is the regional parent. Pools set the service level and provisioned capacity; volumes carry the mount targets and live in a delegated subnet — both are separate modules that reference this account's id / name.

📥 Inputs

Identity & placement (required)

Name Type Description
name string Account name (force-new).
resource_group_name string Existing resource group (force-new).
location string Azure region (force-new).

Optional configuration

Name Type Default Description
identity object null Managed identity (SystemAssigned or UserAssigned) for CMK.
active_directory object (sensitive) null AD/LDAP connection for SMB and dual-protocol volumes.
tags map(string) {} Tags applied to the account.
timeouts object null Optional create/read/update/delete timeouts.
Full object() schemas
variable "identity" {
  type = object({
    type         = string                   # "SystemAssigned" | "UserAssigned"
    identity_ids = optional(set(string))     # required when type = "UserAssigned"
  })
  default = null
}

variable "active_directory" {
  type = object({
    dns_servers                       = list(string)
    domain                            = string
    smb_server_name                   = string
    username                          = string
    password                          = string          # sensitive; out of band
    organizational_unit               = optional(string)
    site_name                         = optional(string)
    kerberos_ad_name                  = optional(string)
    kerberos_kdc_ip                   = optional(string)
    aes_encryption_enabled            = optional(bool, true)   # provider default: false
    ldap_signing_enabled              = optional(bool, true)   # provider default: false
    ldap_over_tls_enabled             = optional(bool, false)  # requires server_root_ca_certificate
    server_root_ca_certificate        = optional(string)       # sensitive; out of band
    local_nfs_users_with_ldap_allowed = optional(bool, false)
  })
  default   = null
  sensitive = true
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

🧾 Outputs

Output Description Kind
id The Azure Resource ID of the NetApp Account Passthrough
name The name of the NetApp Account Passthrough
resource_group_name The resource group holding the NetApp Account Passthrough
location The region the account resides in, normalized by the provider (an input of "East US" is emitted as "eastus") Passthrough
resource_group_id The Azure Resource ID of the containing resource group, derived by trimming the provider path from this account's ID Passthrough
arm_resource_type The ARM resource type this module creates Derived
tags The tags applied to the account Passthrough
tag_count The number of tag pairs on the account, against an Azure ceiling of 50 per resource Passthrough
principal_id The service principal (object) ID of the account's system-assigned managed identity, or null when no system-assigned identity is configured Derived
tenant_id The tenant ID of the account's managed identity, or null when no identity is configured Derived
identity_type The configured identity type - "SystemAssigned", "UserAssigned", or null when no identity block was rendered Derived
user_assigned_identity_ids The set of user-assigned identity Resource IDs attached to the account, or an empty set Derived
has_system_assigned_identity Whether the account carries a system-assigned managed identity, which is the only identity that produces a principal_id this module can emit Derived
user_assigned_identity_declared_without_ids True when identity.type is "UserAssigned" but no identity IDs were supplied Derived
identity_downgrade_is_not_supported Always true, and it is the one identity change that cannot be walked back Constant
active_directory_configured Whether an Active Directory connection is attached to this account Passthrough
active_directory_domain The Active Directory domain the account joins, or null Derived
smb_server_name The NetBIOS name registered as the SMB server computer account, or null Derived
active_directory_dns_server_count How many domain-controller IPv4 addresses were supplied, or 0 Derived
kerberos_volume_ready Whether BOTH kerberos_ad_name and kerberos_kdc_ip are set, which is what creating a Kerberos volume against this account requires Derived
aes_encryption_enabled Whether AES encryption is enabled for SMB communication Derived
ldap_signing_enabled Whether LDAP traffic is signed Derived
ldap_over_tls_enabled Whether LDAP traffic is secured with TLS Derived
local_nfs_users_with_ldap_allowed Whether local NFS client users may access NFS volumes alongside LDAP users Derived
has_server_root_ca_certificate Whether a root CA certificate was supplied for LDAP over TLS Derived
ad_credentials_are_never_read_back_from_azure Always true Constant
sensitive_marking_redacts_plan_output_not_state Always true, and it is the distinction most often collapsed Constant
ad_credentials_must_be_redeclared_after_import Always true Constant
ldap_tls_pairing_is_presence_based Always true, and it is the trap most likely to break a first apply Constant
force_new_arguments The arguments that destroy and recreate this account rather than updating it Derived
only_one_active_directory_connection_per_account Always true at the resource level: the provider permits at most one active_directory block on an account Constant
delete_requires_child_resources_removed Always true Constant
is_regional_and_not_migratable_between_subscriptions Always true Constant
max_capacity_pools_per_account The Azure default ceiling on capacity pools under a single NetApp account, adjustable by support request Passthrough
max_accounts_per_region_per_subscription The Azure default ceiling on NetApp accounts in one region for one subscription, adjustable by support request Passthrough
regional_capacity_quota_tib The default regional capacity quota per subscription, in TiB, from the Azure NetApp Files resource-limits table Passthrough
timeout_defaults The provider's built-in timeouts for this resource, which appear nowhere in the schema Derived
effective_timeouts The timeouts actually in force: the caller's values where supplied, the provider's defaults otherwise Derived

🔒 No secret is emitted. The AD password and root CA certificate are never exposed as outputs.

🧠 Architecture Notes

  • Force-new triad. name, resource_group_name, and location each force replacement. Because capacity pools and volumes are parented to this account by id/name, a replacement here cascades — treat these three as immutable after the first apply.
  • Identity direction is one-way. The provider supports moving identity.type from SystemAssigned to UserAssigned, but not back. Choose the target identity model deliberately.
  • Single AD per subscription. Azure permits only one Active Directory connection per subscription per NetApp account; the module renders at most one active_directory block (max 1 in the schema).
  • Secret round-trip on import. active_directory.password and active_directory.server_root_ca_certificate cannot be read back from Azure. After an import they will show as changes until redeclared — this is expected provider behavior, not module drift.
  • Sensitive-by-container. Terraform cannot mark a single nested attribute of an input object sensitive, so the entire active_directory variable is sensitive = true. This redacts the password and root CA in plan output only — both are written to state in plaintext, and neither is readable back from Azure, so state is their only copy. The cost is also redacting non-secret AD fields in plan output.
  • try() on every optional field. main.tf renders each optional nested field through try(...), so an omitted key is absent rather than an error, and the object-level optional(...) defaults supply the secure values.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block; that is expected and belongs to the root module.

🧱 Design Principles

Concern Secure default (empty call) Opt-out / opt-in (caller types it)
Embedded credentials No identity block; prefer a managed identity Set identity = { type = "SystemAssigned" } (or UserAssigned)
SMB AES encryption aes_encryption_enabled = true (provider default is false) Set false
LDAP signing ldap_signing_enabled = true (provider default is false) Set false
LDAP over TLS ldap_over_tls_enabled = false; requires a root CA to enable Set true + supply server_root_ca_certificate
Local NFS users with LDAP local_nfs_users_with_ldap_allowed = false Set true
AD join None (no active_directory block) Supply the active_directory object
Secret handling active_directory object is sensitive; secrets provisioned out of band —

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 — never a branch.
  • This library is plan-only during authoring; a human runs terraform plan / apply from CI against real credentials.

🧪 Testing

The offline proof gate exercised for this module:

  • terraform init -backend=false — resolves azurerm ~> 4.0 with no backend.
  • terraform validate — type-checks the object() schemas and the dynamic/try() rendering against the pinned provider schema. Reports Success.
  • terraform fmt -check — enforces canonical formatting (exit code 0).

What the gate does not cover: terraform validate never calls Azure. Only terraform plan (run by a human against real credentials) exercises the ARM API — for example the "single AD per subscription" and "identity type is one-way" constraints surface at plan/apply, not at validate.

💬 Example Output

$ terraform output
id           = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-netapp-prod-eastus2/providers/Microsoft.NetApp/netAppAccounts/anf-platform-prod"
name         = "anf-platform-prod"
principal_id = "66666666-7777-8888-9999-aaaaaaaaaaaa"
tenant_id    = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"

🔍 Troubleshooting

Symptom Cause Fix
Plan forces replacement of the account Changed name, resource_group_name, or location These are force-new; revert, or plan the downstream pool/volume re-parenting deliberately.
Apply fails on a UserAssigned account, with no local error first type = "UserAssigned" and no identity_ids. Nothing catches this offline. This module validates only the reverse pairing (IDs supplied without UserAssigned), and the provider's expand checks only that same direction, so the request is sent with userAssignedIdentities unset and Azure rejects it. Supply identity.identity_ids with at least one user-assigned identity Resource ID.
server_root_ca_certificate is required when ... ldap_over_tls_enabled is true LDAP over TLS enabled without a cert Provide the base64 root CA in server_root_ca_certificate.
Cannot switch identity back to SystemAssigned Provider supports only SystemAssigned → UserAssigned Recreate the account if you must revert the identity model.
Perpetual diff on password / server_root_ca_certificate after import Azure does not return these values Redeclare them in configuration; the diff clears on the next apply.
SMB volume create fails after account exists AD connection missing or misconfigured Supply a valid active_directory block before creating SMB volumes.
Kerberos volume create fails Only one of the Kerberos fields set Provide both kerberos_ad_name and kerberos_kdc_ip.

🔗 Related Docs

  • Provider resource: azurerm_netapp_account
  • Azure NetApp Files documentation: Microsoft.NetApp
  • Sibling modules: terraform-azurerm-resource-group, terraform-azurerm-user-assigned-identity, terraform-azurerm-key-vault, terraform-azurerm-netapp-pool, terraform-azurerm-netapp-volume, terraform-azurerm-netapp-snapshot, terraform-azurerm-netapp-backup-vault, terraform-azurerm-netapp-backup-policy. terraform-azurerm-netapp-account-encryption (customer-managed key linkage), terraform-azurerm-netapp-snapshot-policy, terraform-azurerm-netapp-volume-group-sap-hana, terraform-azurerm-netapp-volume-group-oracle.
  • This module's SCOPE.md (the cross-module contract).

💙 "Infrastructure as Code should be standardized, consistent, and secure."