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.
- π’ 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) β thesecret_textis 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
objectinputs,for_eachovermap(object)for child collections, secure-by-default WAF andstableupgrade 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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
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.
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
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-keyfor_eachso the no-countrule holds.azurerm_nginx_certificate.certificateβfor_eachcollection of Key Vault-backed TLS certificates, keyed by a caller string.azurerm_nginx_api_key.api_keyβfor_eachcollection of dataplane API keys; the variable issensitive, somain.tfiteratesnonsensitive(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 {}.
- 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_grouponazurerm_nginx_deploymentis deprecated and no longer supported β this module exposes it only for backward compatibility (defaultnull); leave it unset.diagnose_support_enabledis deprecated in favour of Azure diagnostic settings;logging_storage_accountis deprecated legacy log shipping. Both default to "omit" here.- Data source
azurerm_nginx_configuration.protected_file.contentis no longer populated (deprecated) β never depend on reading protected-file content back.
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
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" }
}| 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) |
| 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 |
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.
capacityhere 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 emptyip_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 eithercapacity(fixed) orauto_scale_profile(autoscaling) β not both. The API rejects autoscaling when a fixed capacity is also set. Autoscaling is SKU-gated (not onbasic_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 totf-mod-azure-role-assignmentto 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_idsis non-empty whenevertypeincludesUserAssigned.
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_Monthlyor higher). It is not available onbasic_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 β
configurationis 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.contentis 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_keysis a sensitive variable βsecret_textnever appears in plan output.main.tfiteratesnonsensitive(keys(var.api_keys))so map keys can drivefor_eachwithout 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,internalare 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 +
stablechannel + 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.
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 {}| 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_hintsis a non-secret hint andsecret_textis an input only β the secret never leaves state via outputs.
- Primary is the deployment, not the API key. The module catalog names
azurerm_nginx_api_key"primary," but the keystone every child references vianginx_deployment_idis the deployment β soazurerm_nginx_deployment.thisis the single un-indexedthis, and configuration/certificate/api_key arefor_eachchildren. This matches the composite rule (one primarythis, role-named children). for_eachkeys are identity. Children iteratemap(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 (orterraform state mvdeliberately).nonsensitivefor secret-bearing maps.api_keyscarriessecret_text, so the whole variable issensitive = true. Terraform forbidsfor_eachover a sensitive value, somain.tfusesfor_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_Monthlysupports neither. Changingskuis an in-place update on the deployment (not a recreate) β butname,resource_group_nameandlocationare 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. capacityvsauto_scale_profileare 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_eachset (keeps the no-countcomposite rule).
- Secrets stay out of plan output.
api_keysis sensitive;protected_file.contentis 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.tfis a total renderer.dynamicblocks 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_eachelement; the slow deployment is untouched.
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.
The offline proof gate every change must pass before commit:
terraform fmt -check
terraform validate
tflintThis module is verified green: terraform validate β Success, terraform fmt -check β clean.
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 = {}
managed_resource_groupshows a perpetual diff or "not supported". azurerm 4.14.0 (Dec 2024) bumped the NGINX API to2024-09-01-previewand deprecatedmanaged_resource_groupβ it is no longer settable. Leave itnull(the module default). Don't reference it from anywhere.- Reading
protected_file.contentback returns empty. As of azurerm 4.14.0 theazurerm_nginx_configurationdata source no longer populatesprotected_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 anapi_keys-like map sensitive and passed it directly tofor_each. Wrap the keys:for_each = nonsensitive(toset(keys(var.api_keys)))and index the sensitive map byeach.key(this module already does).- Autoscaling apply fails with "scale.capacity must be empty". You set both
capacityandauto_scale_profile. Removecapacity(leave itnull) when using autoscaling. - WAF block rejected at apply.
web_application_firewallis SKU-gated βbasic_Monthlycan't enable it. Move tostandardv2_Monthlyor 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_publicorfrontend_private, never both. - Certificate apply fails reading Key Vault. The deployment's identity lacks KV access. Enable
identityand grantKey Vault Secrets Useron the vault (see Example 15) before the certificate is created. - Renaming a
certificates/api_keysmap key destroyed the resource. Expected βfor_eachkeys are identity. Renaming = destroy + recreate. Useterraform state mvif you truly need to preserve the object under a new key. skuchange re-creates everything? It shouldn't βskuis an in-place update. If you see a recreate, check you didn't also changename/location/resource_group_name, which are immutable.
- 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."