Forum Discussion
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
- 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?
- 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?
- 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?
- 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?
- 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?
- 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?
- 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.