Terraform First: Why I Don't Start in the Azure Portal

A production NSG rule change taught me why every Terraform change goes through code first — not a two-minute click in the Azure Portal.
I needed one more open port. A health check was failing against a nonstandard port on the mini-finance VM, and the fastest way to confirm my theory was to open it, test, and close it again. Azure Portal → Networking → Add inbound port rule. Ninety seconds, and I'd have an answer.
I didn't do it. I opened main.tf instead.
The NSG that only had two rules
The whole point of that project's network security group was that it was minimal by design — inbound TCP 22 for SSH, TCP 80 for HTTP, nothing else. That wasn't an accident. It was a deliberate constraint written into the Terraform config, and the reason it stayed that way for months was that opening a rule required editing code, not clicking a button.
That's the part that's easy to miss about "infrastructure as code" as a phrase. It doesn't just mean the infrastructure is described in code. It means the code is the only path that's supposed to change it. The moment you add a rule through the portal instead, that sentence stops being true, and nothing tells you it stopped being true.
What the click actually costs
Terraform doesn't watch Azure. It reads state, compares it to config, and calculates a diff. A rule added through the portal doesn't touch state and doesn't touch config — as far as Terraform is concerned, it doesn't exist.
That produces one of two outcomes, and both are worse than the ninety seconds you saved:
Someone runs terraform plan later and it wants to delete the rule. Config says two rules; the NSG now has three. Terraform's job is to make reality match config, so the plan proposes removing the one that isn't in the .tf file — the one you added to debug something, three weeks ago, that you've since forgotten about. If that plan gets approved without someone questioning why a rule is disappearing, whatever depended on it breaks, with no error message pointing at the cause.
Nobody runs plan again for months, and the rule just sits there. No PR. No commit. No comment explaining why port 8443, or wherever, is reachable from the internet on a box that's supposed to expose 22 and 80. Six months later it shows up in a security review as an unexplained open port, and the honest answer is "I don't remember opening that."
Neither outcome is a Terraform failure. Terraform did exactly what it's supposed to do — it just wasn't the tool that made the change, so it can't be the tool that explains it.
Writing it down first is the actual discipline
The code-first version of the same fix looks like this:
resource "azurerm_network_security_rule" "debug_health_check" {
name = "temp-debug-8443"
priority = 150
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8443"
source_address_prefix = var.my_debug_ip
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.main.name
network_security_group_name = azurerm_network_security_group.main.name
}
That's slower than the portal. It has to be — writing var.my_debug_ip instead of typing an IP into a form is what forces the second question the portal never asks: whose IP is this, and does it need to stay open after I'm done? Scoping the source to a variable instead of 0.0.0.0/0, naming the rule temp-debug instead of leaving it blank, and committing it with a message that says why — that's not process for its own sake. It's the difference between a rule with a reason attached to it and a rule that's just there.
And when the debugging is done, deleting the resource block and running terraform apply closes the port the same way it was opened: as a reviewable change, not a lingering one. git log on that file is the audit trail. There's no equivalent for "someone clicked a button in the portal at 4pm on a Tuesday."
The takeaway
The portal isn't wrong to use — it's wrong to use as the first move for anything that's supposed to be managed by Terraform. Every manual click is a small, permanent gap between what your code says and what's actually running, and those gaps don't announce themselves. They show up later, as a plan that wants to delete something important, or a port nobody can explain. Writing the change in code first doesn't make you slower. It makes the two-minute shortcut visible for what it costs before you take it, instead of after.