Forum Discussion

Sai2608's avatar
Sai2608
Copper Contributor
Oct 16, 2026

Design advice: Tf module to manage Azure NSG rules from YAML/CSV manifests

Hi all,

I’m building a Terraform module that lets our security team manage Azure NSG rules declaratively. They edit YAML or CSV manifest files in a /nsgrules/ folder and raise a PR, and they never touch HCL. I’d like feedback on the design before I go further.

Requirements

  • Parse YAML or CSV manifests with these fields: name, priority, direction, access, protocol, source_port, dest_port, source_prefix, dest_prefix, description
  • Apply the rules to named NSGs (both existing NSGs and new ones) across multiple Azure subscriptions
  • Validate the manifest schema before plan (priority range, unique priorities per NSG/direction, allowed enum values, CIDR format)
  • Produce a terraform plan diff that someone without Terraform knowledge can read
  • Run in Azure DevOps: plan on PR, apply on merge to main
  • Pass a Wiz IaC scan with 0 critical findings

Example manifest

yaml

nsg_name: nsg-app-prod resource_group: rg-network-prod subscription: prod rules: - name: allow-https-inbound priority: 200 direction: Inbound access: Allow protocol: Tcp source_port: "*" dest_port: "443" source_prefix: 10.10.0.0/16 dest_prefix: VirtualNetwork description: HTTPS from internal network

Current approach (simplified)

hcl

locals { yaml_files = fileset("${path.root}/nsgrules", "*.yaml") manifests = { for f in local.yaml_files : trimsuffix(f, ".yaml") => yamldecode(file("${path.root}/nsgrules/${f}")) } } resource "azurerm_network_security_group" "this" { for_each = var.nsgs name = each.value.nsg_name resource_group_name = each.value.resource_group location = each.value.location dynamic "security_rule" { for_each = each.value.rules content { name = security_rule.value.name priority = security_rule.value.priority # ...remaining fields } } }

Questions

  1. Inline security_rule dynamic blocks or a separate azurerm_network_security_rule per rule (for_each)? With inline blocks, changing one rule seems to make the plan show the whole set changing, which isn’t readable for reviewers. Separate resources give per-rule diffs, but then I can’t manage new NSGs with inline rules, and I know mixing the two styles causes conflicts. What do people use in production?
  2. Multiple subscriptions: Terraform doesn’t allow for_each over provider configurations. Is the usual pattern one provider alias per subscription plus one module call each, or a separate state/root module per subscription? Which one scales better as subscriptions get added?
  3. Validation: Manifests are read with file()/yamldecode() in locals, but variable validation blocks only apply to input variables. Is it cleanest to decode in the root module and pass the result into a child module whose variable has the validation blocks? Or should I use check blocks or precondition instead? Also, how do you validate unique priorities per NSG + direction?
  4. CSV support: csvdecode() returns every value as a string, and multi-value fields (several source prefixes) are awkward. Do you normalize CSV into the same object shape as the YAML, or just standardize on YAML?
  5. Existing NSGs: What’s the safest way to bring existing NSGs and rules under management without a destroy/recreate? Do you generate import blocks from the manifest?
  6. Readable plans for non-Terraform reviewers: Has anyone used terraform show -json with a summarizer (tf-summarize or a custom script) to post a rule-level diff as a PR comment in Azure DevOps?
  7. Guardrails: Beyond Wiz, do you also block risky rules in validation, e.g. inbound Allow from */0.0.0.0/0/Internet on 22/3389?

Environment: Terraform x.x, azurerm x.x, Azure DevOps pipelines, remote state in Azure Storage.

Thanks in advance. I’m happy to share the finished module structure once it’s working.

No RepliesBe the first to reply