Skip to content

microsoftexpert/tf-mod-azure-nginx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ Azure NGINXaaS Terraform Module

The NGINXaaS for Azure unit of work β€” one F5/NGINX deployment plus its configuration, TLS certificates and dataplane API keys, and read-only lookups for all four, behind a single deeply-typed module boundary. Built for AzureRM v4.x.

Terraform AzureRM Module Type Resources


🧩 Overview

  • 🟒 Provisions an NGINXaaS for Azure deployment (azurerm_nginx_deployment) β€” the F5/NGINX managed reverse proxy and load balancer, fronted publicly or privately inside your VNet.
  • πŸ“ Manages the deployment's NGINX configuration (azurerm_nginx_configuration) β€” root file, inline config files, and sensitive protected files (TLS keys, secrets).
  • πŸ” Binds TLS certificates (azurerm_nginx_certificate) sourced from Key Vault secrets, keyed by a caller-supplied stable string.
  • πŸ”‘ Issues dataplane API keys (azurerm_nginx_api_key) β€” the secret_text is a sensitive input and never leaves state in plan output.
  • πŸ”Ž Exposes optional read-only lookups for existing deployments, configurations, certificates and API keys (the four data.azurerm_nginx_* sources) β€” default empty, so no reads happen unless asked.
  • 🧱 Deeply-typed object inputs, for_each over map(object) for child collections, secure-by-default WAF and stable upgrade channel.

πŸ’‘ Why it matters: the slow-to-provision deployment and its fast-churning children (certs, keys, config) live behind one typed boundary β€” rotate an API key or swap a cert without ever re-planning the deployment.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


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

flowchart LR
 RG["tf-mod-azure-resource-group<br/>(resource_group_name)"]
 VNET["tf-mod-azure-virtual-network / subnet<br/>(subnet_id)"]
 KV["tf-mod-azure-key-vault<br/>(certificate secret ids)"]
 UAI["tf-mod-azure-user-assigned-identity<br/>(identity_ids)"]
 NGINX["tf-mod-azure-nginx<br/>(this module)"]
 RBAC["tf-mod-azure-role-assignment<br/>(principal_id)"]
 PE["tf-mod-azure-private-endpoint<br/>(resource id)"]
 DIAG["tf-mod-azure-monitor-diagnostic-setting<br/>(target resource id)"]

 RG -->|"resource_group_name"| NGINX
 VNET -->|"subnet_id"| NGINX
 KV -->|"key_vault_secret_id"| NGINX
 UAI -->|"identity_ids"| NGINX
 NGINX -->|"identity_principal_id"| RBAC
 NGINX -->|"id"| PE
 NGINX -->|"id"| DIAG

 style NGINX fill:#8957E5,color:#fff
 style KV fill:#0078D4,color:#fff
Loading

This module consumes a resource group name, a delegated subnet ID, Key Vault secret IDs (for certificates) and optional user-assigned identity IDs from its siblings, and emits the deployment id, identity_principal_id, child *_ids maps and looked-up attributes for role assignments, private endpoints and diagnostics β€” see the Cross-Module Contract.


🧬 What this module builds

flowchart TD
 DEP["azurerm_nginx_deployment.this<br/>(primary Β· the NGINXaaS instance)"]
 CFG["azurerm_nginx_configuration.configuration<br/>(0-or-1 Β· root + config + protected files)"]
 CERT["azurerm_nginx_certificate.certificate<br/>(for_each Β· map keyed by string)"]
 KEY["azurerm_nginx_api_key.api_key<br/>(for_each Β· sensitive map)"]

 DEP -->|"nginx_deployment_id"| CFG
 DEP -->|"nginx_deployment_id"| CERT
 DEP -->|"nginx_deployment_id"| KEY

 style DEP fill:#8957E5,color:#fff
Loading

Resource inventory β€” what terraform apply creates:

  • azurerm_nginx_deployment.this β€” primary. The NGINXaaS for Azure instance: SKU, sizing/autoscaling, identity, public or private frontend, network interfaces, WAF.
  • azurerm_nginx_configuration.configuration β€” the deployment's NGINX configuration (at most one per deployment); rendered via a single-key for_each so the no-count rule holds.
  • azurerm_nginx_certificate.certificate β€” for_each collection of Key Vault-backed TLS certificates, keyed by a caller string.
  • azurerm_nginx_api_key.api_key β€” for_each collection of dataplane API keys; the variable is sensitive, so main.tf iterates nonsensitive(keys(...)).

Plus four optional read-only data sources β€” data.azurerm_nginx_deployment.lookup, …_configuration.lookup, …_certificate.lookup, …_api_key.lookup β€” each driven by a map(object) that defaults to {}.


βœ… Provider / Versions

  • Terraform >= 1.12.0
  • AzureRM >= 4.0, < 5.0
  • No provider {} block in the module β€” the root/spec configures the provider so the module stays composable.

⚠️ 4.x schema notes that bite (verified against the provider changelog):

  • azurerm 4.14.0 bumped the NGINX API to 2024-09-01-preview. managed_resource_group on azurerm_nginx_deployment is deprecated and no longer supported β€” this module exposes it only for backward compatibility (default null); leave it unset.
  • diagnose_support_enabled is deprecated in favour of Azure diagnostic settings; logging_storage_account is deprecated legacy log shipping. Both default to "omit" here.
  • Data source azurerm_nginx_configuration.protected_file.content is no longer populated (deprecated) β€” never depend on reading protected-file content back.

πŸ“ Module Structure

tf-mod-azure-nginx/
β”œβ”€β”€ providers.tf # Terraform & AzureRM version pins (no provider block)
β”œβ”€β”€ variables.tf # Deeply-typed inputs: deployment + child maps + lookups
β”œβ”€β”€ main.tf # Total renderer: this + for_each children + data sources
β”œβ”€β”€ outputs.tf # id, identity ids, child *_ids maps, lookup maps
β”œβ”€β”€ SCOPE.md # Cross-module contract (consumes / emits / gotchas)
└── README.md # This file

βš™οΈ Quick Start

module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-dev-001"
  resource_group_name = module.resource_group.name
  location            = "eastus2"
  sku                 = "standardv3_Monthly"

  capacity = 20

  network_interface = [{ subnet_id = module.subnet.id }]
  frontend_private  = [{ allocation_method = "Dynamic", ip_address = "", subnet_id = module.subnet.id }]

  tags = { environment = "dev", owner = "platform" }
}

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
🧱 resource_group_name string tf-mod-azure-resource-group.name
🌐 network_interface[].subnet_id / frontend_private[].subnet_id string tf-mod-azure-virtual-network / subnet module .id (delegated subnet)
πŸ” certificates[*].key_vault_secret_id string tf-mod-azure-key-vault secret .versionless_id
πŸͺͺ identity.identity_ids list(string) tf-mod-azure-user-assigned-identity.id
🌍 frontend_public.ip_address list(string) public IP module .id (optional)

Emits

Output Description Consumed by
id Resource ID of the NGINXaaS deployment tf-mod-azure-private-endpoint (resource_id), tf-mod-azure-monitor-diagnostic-setting (target_resource_id)
name Deployment name Downstream references by name
resource_group_name Resource group (passthrough) Sibling modules in the same RG
location Azure region of the deployment Sibling resources that must be co-located
ip_address Deployment IP address DNS records, ingress/backend config referencing the deployment
nginx_version Running NGINX version (computed) Inventory / drift audits
dataplane_api_endpoint Dataplane API endpoint (computed) NGINX dataplane API clients, monitoring
identity_principal_id System-assigned identity principal ID (null when not enabled) tf-mod-azure-role-assignment (principal_id) β€” e.g. granting Key Vault access
identity_tenant_id System-assigned identity tenant ID (null when not enabled) Cross-tenant identity diagnostics
configuration_id Configuration resource ID (null when no configuration managed) Audit / drift detection of the active config
certificate_ids Map of certificate key β†’ resource ID Inventory, dependent wiring
api_key_ids Map of API key key β†’ resource ID Inventory, dependent wiring
api_key_hints Map of API key key β†’ non-secret hint Support/audit tooling that needs to identify a key without the secret
deployment_lookups Map of looked-up existing deployments β†’ key attributes Cross-stack wiring decisions against a shared/existing deployment
configuration_lookups Map of looked-up existing configurations β†’ resource ID Cross-stack wiring decisions
certificate_lookups Map of looked-up existing certificates β†’ key attributes Cross-stack wiring decisions
api_key_lookups Map of looked-up existing API keys β†’ non-secret metadata (id, hint, expiry) Cross-stack wiring decisions

πŸ“š Example Library

1️⃣ Minimal β€” smallest call that produces a real deployment
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-dev-001"
  resource_group_name = "rg-fie2-dev"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 10

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
}

ℹ️ A deployment needs a delegated subnet for dataplane traffic. capacity here is a fixed NCU count.

2️⃣ Private frontend (VNet-internal)
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-prd-001"
  resource_group_name = "rg-fie2-prd"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 20

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
  frontend_private = [{
    allocation_method = "Static"
    ip_address        = "10.20.4.10"
    subnet_id         = "/subscriptions/…/subnets/snet-nginx"
  }]
}

πŸ”’ A deployment is fronted by either a public or a private frontend β€” supplying both is rejected by the service. Private is the default.

3️⃣ Public frontend (internet-facing)
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-edge-prd-001"
  resource_group_name = "rg-edge-prd"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 20

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
  frontend_public   = { ip_address = ["/subscriptions/…/publicIPAddresses/pip-nginx"] }
}

⚠️ Use an empty ip_address = [] to let the service allocate a public IP.

4️⃣ Autoscaling instead of fixed capacity
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-prd-002"
  resource_group_name = "rg-fie2-prd"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"

  # Leave capacity null when autoscaling.
  auto_scale_profile = [{
    name         = "default"
    min_capacity = 10
    max_capacity = 60
  }]

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
}

⚠️ Set either capacity (fixed) or auto_scale_profile (autoscaling) β€” not both. The API rejects autoscaling when a fixed capacity is also set. Autoscaling is SKU-gated (not on basic_Monthly).

5️⃣ Managed identity (system-assigned)
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-dev-003"
  resource_group_name = "rg-fie2-dev"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 10

  identity = { type = "SystemAssigned" }

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
}

πŸ’‘ The system-assigned principal is emitted as identity_principal_id β€” feed it to tf-mod-azure-role-assignment to grant Key Vault read access for certificates.

6️⃣ User-assigned identity
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-dev-004"
  resource_group_name = "rg-fie2-dev"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 10

  identity = {
    type         = "UserAssigned"
    identity_ids = ["/subscriptions/…/userAssignedIdentities/uai-nginx"]
  }

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
}

πŸ”’ The module validates that identity_ids is non-empty whenever type includes UserAssigned.

7️⃣ Web Application Firewall (SKU-gated)
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-waf-prd-001"
  resource_group_name = "rg-edge-prd"
  location            = "eastus2"
  sku                 = "standardv3_Monthly" # WAF-capable tier
  capacity            = 20

  web_application_firewall = { activation_state_enabled = true }

  network_interface = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]
}

⚠️ SKU-GATED: the WAF block requires a WAF-capable SKU (standardv2_Monthly or higher). It is not available on basic_Monthly.

8️⃣ NGINX configuration (root + config files)
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-dev-005"
  resource_group_name = "rg-fie2-dev"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 10
  network_interface   = [{ subnet_id = "/subscriptions/…/subnets/snet-nginx" }]

  configuration = {
    root_file = "/etc/nginx/nginx.conf"
    config_file = [{
      content      = base64encode(file("${path.module}/nginx.conf"))
      virtual_path = "/etc/nginx/nginx.conf"
    }]
  }
}

ℹ️ A deployment has at most one configuration β€” configuration is a single object, not a collection.

9️⃣ Protected files (sensitive content)
configuration = {
  root_file = "/etc/nginx/nginx.conf"
  config_file = [{
    content      = base64encode(file("${path.module}/nginx.conf"))
    virtual_path = "/etc/nginx/nginx.conf"
  }]
  protected_file = [{
    content      = base64encode(data.azurerm_key_vault_secret.tls_key.value)
    virtual_path = "/etc/nginx/certs/server.key"
  }]
}

πŸ”’ protected_file.content is treated as sensitive by the provider (masked in plan, kept out of readable state). Note the data source no longer reads it back (azurerm 4.14.0+).

πŸ”Ÿ TLS certificate from Key Vault
certificates = {
  primary = {
    name                     = "wildcard-casey"
    key_vault_secret_id      = "https://kv-fie2.vault.azure.net/secrets/wildcard-casey"
    certificate_virtual_path = "/etc/nginx/certs/fullchain.pem"
    key_virtual_path         = "/etc/nginx/certs/privkey.pem"
  }
}

πŸ’‘ The KV secret must hold the cert and key; grant the deployment's identity Key Vault Secrets User (see example 5).

1️⃣1️⃣ Dataplane API key (sensitive)
api_keys = {
  ci = {
    name          = "ci-pipeline"
    secret_text   = var.nginx_ci_api_key   # sensitive var
    end_date_time = "2027-01-01T00:00:00Z" # RFC 3339
  }
}

πŸ”’ api_keys is a sensitive variable β€” secret_text never appears in plan output. main.tf iterates nonsensitive(keys(var.api_keys)) so map keys can drive for_each without exposing the secrets.

1️⃣2️⃣ for_each at scale β€” many certs + keys from maps
certificates = {
  apex     = { name = "apex", key_vault_secret_id = local.kv["apex"], certificate_virtual_path = "/etc/nginx/certs/apex.pem", key_virtual_path = "/etc/nginx/certs/apex.key" }
  api      = { name = "api", key_vault_secret_id = local.kv["api"], certificate_virtual_path = "/etc/nginx/certs/api.pem", key_virtual_path = "/etc/nginx/certs/api.key" }
  internal = { name = "internal", key_vault_secret_id = local.kv["internal"], certificate_virtual_path = "/etc/nginx/certs/int.pem", key_virtual_path = "/etc/nginx/certs/int.key" }
}

api_keys = {
  ci      = { name = "ci-pipeline", secret_text = var.keys["ci"], end_date_time = "2027-01-01T00:00:00Z" }
  metrics = { name = "metrics", secret_text = var.keys["metrics"], end_date_time = "2027-01-01T00:00:00Z" }
}

⚠️ Map keys are identity. apex, api, internal are the Terraform addresses. Renaming a key (e.g. apex β†’ root) is a destroy + recreate, not a rename. Choose stable, meaning-bearing keys and never reorder-rename them.

1️⃣3️⃣ Security / hardening β€” -compliant variant
module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-prd-secure"
  resource_group_name = module.resource_group.name
  location            = "eastus2"
  sku                 = "standardv3_Monthly"

  automatic_upgrade_channel = "stable" # never auto-pull preview builds
  identity                  = { type = "SystemAssigned" }
  web_application_firewall  = { activation_state_enabled = true }

  auto_scale_profile = [{ name = "default", min_capacity = 10, max_capacity = 60 }]

  network_interface = [{ subnet_id = module.subnet.id }]
  frontend_private  = [{ allocation_method = "Dynamic", ip_address = "", subnet_id = module.subnet.id }]

  tags = { environment = "prod", data_classification = "internal", owner = "platform" }
}

πŸ”’ Private frontend + WAF on + stable channel + system identity for KV pulls = the locked-down default. No public IP, no preview dataplane.

1️⃣4️⃣ Read-only lookups of existing NGINX resources
module "nginx_lookup" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  # Still creates a (placeholder) deployment β€” lookups are additive reads:
  name                = "ngx-readonly-anchor"
  resource_group_name = "rg-fie2-prd"
  location            = "eastus2"
  sku                 = "standardv3_Monthly"
  capacity            = 10
  network_interface   = [{ subnet_id = module.subnet.id }]

  deployment_lookups = {
    shared = { name = "ngx-shared-prd", resource_group_name = "rg-shared-prd" }
  }
}

# module.nginx_lookup.deployment_lookups["shared"].id /.ip_address /.nginx_version

ℹ️ Lookups default to {} (no reads). They resolve existing resources for cross-stack wiring β€” they do not create anything.

πŸ—οΈ 1️⃣5️⃣ End-to-end composition (mandatory) β€” full suite wired outputs β†’ inputs
module "resource_group" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-azure-resource-group?ref=v1.0.0"
  name     = "rg-fie2-prd"
  location = "eastus2"
}

module "network" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-virtual-network?ref=v1.0.0"
  name                = "vnet-fie2-prd"
  resource_group_name = module.resource_group.name
  location            = "eastus2"
  # … delegated subnet snet-nginx …
}

module "key_vault" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-key-vault?ref=v1.0.0"
  name                = "kv-fie2-prd"
  resource_group_name = module.resource_group.name
  location            = "eastus2"
}

module "nginx" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-nginx?ref=v1.0.0"

  name                = "ngx-fie2-prd-001"
  resource_group_name = module.resource_group.name
  location            = "eastus2"
  sku                 = "standardv3_Monthly"

  identity                 = { type = "SystemAssigned" }
  web_application_firewall = { activation_state_enabled = true }
  auto_scale_profile       = [{ name = "default", min_capacity = 10, max_capacity = 60 }]

  network_interface = [{ subnet_id = module.network.subnet_ids["snet-nginx"] }]
  frontend_private  = [{ allocation_method = "Dynamic", ip_address = "", subnet_id = module.network.subnet_ids["snet-nginx"] }]

  certificates = {
    primary = {
      name                     = "wildcard-casey"
      key_vault_secret_id      = module.key_vault.secret_ids["wildcard-casey"]
      certificate_virtual_path = "/etc/nginx/certs/fullchain.pem"
      key_virtual_path         = "/etc/nginx/certs/privkey.pem"
    }
  }
}

# Grant the deployment identity read access to Key Vault secrets
module "nginx_kv_access" {
  source       = "git::https://github.com/microsoftexpert/tf-mod-azure-role-assignment?ref=v1.0.0"
  scope        = module.key_vault.id
  principal_id = module.nginx.identity_principal_id
  role         = "Key Vault Secrets User"
}

πŸ’‘ The money shot: RG β†’ VNet/Key Vault β†’ NGINX β†’ role assignment, every wire is an output β†’ input. No hand-copied IDs.


πŸ“₯ Inputs

Core (required): name, resource_group_name, location, sku.

Deployment config (optional): capacity, auto_scale_profile, automatic_upgrade_channel (stable), email, identity, frontend_public, frontend_private, network_interface, web_application_firewall.

Deprecated (omit): diagnose_support_enabled, managed_resource_group, logging_storage_account.

Child collections: configuration (single object), certificates (map), api_keys (map, sensitive).

Lookups (read-only): deployment_lookups, configuration_lookups, certificate_lookups, api_key_lookups.

Universal tail: tags, timeouts.

Full object schemas (mirrors variables.tf)
auto_scale_profile = list(object({
  name         = string
  min_capacity = number
  max_capacity = number # >= min_capacity
}))                     # default []

identity = object({
  type         = string                 # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned"
  identity_ids = optional(list(string)) # required when type includes UserAssigned
})                                      # default null

frontend_public = object({
  ip_address = optional(list(string), [])
}) # default null

frontend_private = list(object({
  allocation_method = string # Static | Dynamic
  ip_address        = string
  subnet_id         = string
})) # default []

network_interface = list(object({ subnet_id = string })) # default []

web_application_firewall = object({
  activation_state_enabled = optional(bool, true)
}) # default null β€” SKU-gated

configuration = object({ # default null (0-or-1 per deployment)
  root_file    = string
  package_data = optional(string)
  config_file = optional(list(object({
    content      = string
    virtual_path = string
  })), [])
  protected_file = optional(list(object({
    content      = string # provider-sensitive
    virtual_path = string
  })), [])
})

certificates = map(object({
  name                     = string
  key_vault_secret_id      = string
  certificate_virtual_path = string
  key_virtual_path         = string
  nginx_deployment_id      = optional(string) # advanced: attach elsewhere
}))                                           # default {}

api_keys = map(object({ # sensitive = true
  name                = string
  secret_text         = string # sensitive
  end_date_time       = string # RFC 3339
  nginx_deployment_id = optional(string)
})) # default {}

# Lookups
deployment_lookups    = map(object({ name = string, resource_group_name = string })) # default {}
configuration_lookups = map(object({ nginx_deployment_id = string }))                # default {}
certificate_lookups   = map(object({ name = string, nginx_deployment_id = string })) # default {}
api_key_lookups       = map(object({ name = string, nginx_deployment_id = string })) # default {}

🧾 Outputs

Output Description
id Resource ID of the NGINXaaS deployment
name Deployment name
resource_group_name Resource group name (passthrough)
location Azure region
ip_address Deployment IP address
nginx_version Running NGINX version (computed)
dataplane_api_endpoint Dataplane API endpoint (computed)
identity_principal_id System-assigned principal ID β€” null when identity not enabled
identity_tenant_id System-assigned tenant ID β€” null when identity not enabled
configuration_id Configuration resource ID β€” null when no configuration managed
certificate_ids Map: cert key β†’ resource ID
api_key_ids Map: API key key β†’ resource ID
api_key_hints Map: API key key β†’ non-secret hint
deployment_lookups / configuration_lookups / certificate_lookups / api_key_lookups Looked-up attributes β€” empty {} when no lookups requested

ℹ️ No output is marked sensitive: api_key_hints is a non-secret hint and secret_text is an input only β€” the secret never leaves state via outputs.


🧠 Architecture Notes

  • Primary is the deployment, not the API key. The module catalog names azurerm_nginx_api_key "primary," but the keystone every child references via nginx_deployment_id is the deployment β€” so azurerm_nginx_deployment.this is the single un-indexed this, and configuration/certificate/api_key are for_each children. This matches the composite rule (one primary this, role-named children).
  • for_each keys are identity. Children iterate map(object) keyed by a caller string. The key becomes the Terraform resource address (azurerm_nginx_certificate.certificate["primary"]). Renaming a key destroys and recreates that child β€” it is not an in-place rename. Pick stable, semantic keys up front; if you must rename, plan for a recreate (or terraform state mv deliberately).
  • nonsensitive for secret-bearing maps. api_keys carries secret_text, so the whole variable is sensitive = true. Terraform forbids for_each over a sensitive value, so main.tf uses for_each = nonsensitive(toset(keys(var.api_keys))) β€” only the keys (caller identifiers, not secrets) are unwrapped; the values stay sensitive and flow straight to the provider.
  • SKU gating & change behavior. WAF (web_application_firewall) and autoscaling (auto_scale_profile) require a WAF/scale-capable SKU (standardv2_Monthly+); basic_Monthly supports neither. Changing sku is an in-place update on the deployment (not a recreate) β€” but name, resource_group_name and location are immutable (changing any forces a new deployment). The module validates only the SKU tier prefix, so future versioned tiers (standardv4_*) keep working without a module bump.
  • capacity vs auto_scale_profile are mutually exclusive. Fixed sizing or autoscaling β€” never both. The API rejects autoscaling when a fixed capacity is also set.
  • Public XOR private frontend. A deployment is internet-facing or VNet-internal, not both. defaults to private.
  • Configuration is a singleton. One configuration per deployment, modeled as a single optional object rendered through a one-key for_each set (keeps the no-count composite rule).

🧱 Design Principles

  • Secrets stay out of plan output. api_keys is sensitive; protected_file.content is provider-sensitive.
  • Deeply-typed inputs. Every provider block is a typed object β€” a fat-fingered nested key is a plan-time type error, not a runtime surprise.
  • Secure by default. automatic_upgrade_channel = "stable", WAF enabled when its block is present, private frontend the documented default.
  • Consumes, not creates. Subnets, Key Vault secrets and identities arrive by ID β€” this module never creates them.
  • main.tf is a total renderer. dynamic blocks for every optional/repeating block, try(x, null) on optional nested fields β€” no business logic.
  • Small blast radius. Rotating a cert or key touches one for_each element; the slow deployment is untouched.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output

⚠️ Pin the version. Always source with ?ref=v1.0.0 β€” never a branch. Module tags are immutable once consumers reference them.


πŸ§ͺ Testing

The offline proof gate every change must pass before commit:

terraform fmt -check
terraform validate
tflint

This module is verified green: terraform validate β†’ Success, terraform fmt -check β†’ clean.


πŸ’¬ Example Output

id = "/subscriptions/…/resourceGroups/rg-fie2-prd/providers/Nginx.NginxPlus/nginxDeployments/ngx-fie2-prd-001"
name = "ngx-fie2-prd-001"
ip_address = "10.20.4.10"
nginx_version = "nginx-plus-r32"
identity_principal_id = "00000000-0000-0000-0000-000000000000"
certificate_ids = { "primary" = "/subscriptions/…/certificates/wildcard-casey" }
api_key_ids = { "ci" = "/subscriptions/…/apiKeys/ci-pipeline" }
api_key_hints = { "ci" = "abc…" }
deployment_lookups = {}

πŸ” Troubleshooting

  • managed_resource_group shows a perpetual diff or "not supported". azurerm 4.14.0 (Dec 2024) bumped the NGINX API to 2024-09-01-preview and deprecated managed_resource_group β€” it is no longer settable. Leave it null (the module default). Don't reference it from anywhere.
  • Reading protected_file.content back returns empty. As of azurerm 4.14.0 the azurerm_nginx_configuration data source no longer populates protected_file.content. Treat protected files as write-only; source content from Key Vault, don't round-trip it.
  • Error: Invalid for_each argument … sensitive value. You marked an api_keys-like map sensitive and passed it directly to for_each. Wrap the keys: for_each = nonsensitive(toset(keys(var.api_keys))) and index the sensitive map by each.key (this module already does).
  • Autoscaling apply fails with "scale.capacity must be empty". You set both capacity and auto_scale_profile. Remove capacity (leave it null) when using autoscaling.
  • WAF block rejected at apply. web_application_firewall is SKU-gated β€” basic_Monthly can't enable it. Move to standardv2_Monthly or higher.
  • "SKU not available in this region." Not all NGINXaaS marketplace plans exist in every region. Pick a supported region, or a plan offered there β€” check availability at create time (per the NGINXaaS quickstart).
  • Both frontends configured β†’ service error. A deployment is public-only or private-only. Supply frontend_public or frontend_private, never both.
  • Certificate apply fails reading Key Vault. The deployment's identity lacks KV access. Enable identity and grant Key Vault Secrets User on the vault (see Example 15) before the certificate is created.
  • Renaming a certificates/api_keys map key destroyed the resource. Expected β€” for_each keys are identity. Renaming = destroy + recreate. Use terraform state mv if you truly need to preserve the object under a new key.
  • sku change re-creates everything? It shouldn't β€” sku is an in-place update. If you see a recreate, check you didn't also change name/location/resource_group_name, which are immutable.

πŸ”— Related Docs

  • Terraform Registry β€” azurerm_nginx_deployment, azurerm_nginx_configuration, azurerm_nginx_certificate, azurerm_nginx_api_key (and their data sources)
  • Microsoft Learn β€” What is NGINXaaS? (Azure Native ISV Service overview)
  • Microsoft Learn β€” QuickStart: Create an NGINXaaS resource (networking & IP-type guidance)
  • Microsoft Learn β€” Azure Native NGINXaaS FAQ (regional availability, management)
  • Terraform AzureRM provider version history β€” 4.14.0 NGINX breaking changes

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

Packages

 
 
 

Contributors

Languages