From a diagram to validated Terraform: inside the Infra designer
How the OpsNexus Infra designer turns a form-based AWS design into a JSON spec, checks it against AWS rules, generates readable Terraform and deploys it through your own build agents.
Most infrastructure diagrams are drawn after the fact, and most Terraform is written by copying the last project's modules. The diagram and the code drift apart, and the first real check happens when terraform apply fails halfway through.
The Infra designer in OpsNexus starts from the other end. You describe the infrastructure once, as a stack. OpsNexus validates it, draws it and writes the Terraform from that single description. Then it can plan and apply it on your own build agents, with an approval in between. This post walks through each step and the reasoning behind it.
One source of truth: the spec
Everything in a stack is stored as a JSON document, the spec. The diagram, the Terraform and the forms in the UI are all generated from it. Here is a small one:
{
"format": "opsnexus.infra/v1",
"provider": "aws",
"region": "ap-south-1",
"name": "payments",
"resources": [
{"id": "main", "type": "vpc", "name": "payments-vpc",
"props": {"cidr": "10.20.0.0/16", "az_count": 2, "database_subnets": true, "nat_gateway": "single"}},
{"id": "web_sg", "type": "security_group", "name": "web",
"props": {"vpc": {"ref": "main"},
"ingress": [{"from_port": 443, "to_port": 443, "protocol": "tcp", "cidr": "0.0.0.0/0"}]}},
{"id": "web", "type": "ec2_instance", "name": "web",
"props": {"instance_type": "t3.small", "ami": "al2023",
"subnet": {"ref": "main", "tier": "public", "index": 0},
"security_groups": [{"ref": "web_sg"}]}}
]
}Two design choices matter here.
Connections are references, not strings. The EC2 instance doesn't hold a subnet ID. It holds {"ref": "main", "tier": "public", "index": 0}: the first public subnet of the VPC called main. Because the designer knows what each reference points to, the forms only offer compatible choices. An EC2 subnet picker lists subnet tiers of VPCs in the stack, and a security-group rule can point at another security group instead of a CIDR. The same references become arrows in the diagram and real attribute references in Terraform.
The spec is portable. You can download it as opsnexus.json next to the Terraform, keep it in Git, and load it again later.
Validation before generation
The validator runs on every save and returns every problem at once, each tied to a resource and a field. It checks:
- IDs are valid and unique, and required properties are set;
- references exist, point to the right type and have no cycles;
- CIDR math: the VPC range is valid and every tier in every zone fits;
- AWS rules, such as S3 bucket naming, RDS engine and class shapes, ports and region codes.
As the catalog grew, so did the rules. The validator now knows that Fargate only accepts certain CPU and memory pairs, that a CloudFront certificate must live in us-east-1, that a queue's visibility timeout has to cover the Lambda timeout that reads it, and that private Fargate tasks or EKS nodes need a NAT gateway. Each message says what to change.
Problems are split in two. Blocking problems stop a deploy. Recommendations don't: a production stack without flow logs or deletion protection gets a recommendation, not an error. You can save a stack with problems and come back to it.
Smart defaults sit next to validation. When one setting changes, dependent settings move to a valid value. Pick Windows and the root volume grows to 30 GiB. Pick Aurora and you get a supported instance class. Values you set yourself are kept.
Terraform you would write by hand
The generator targets Terraform 1.6 or later and the hashicorp/aws ~> 6.0 provider. A stack becomes a module plus one folder per environment:
payments/
├── modules/stack/ # the resources (reusable)
│ ├── versions.tf
│ ├── variables.tf
│ ├── network.tf, security.tf, compute.tf, database.tf, storage.tf ...
│ └── outputs.tf
├── environments/prod/ # the root module you run Terraform in
│ ├── versions.tf
│ ├── providers.tf # region and default tags
│ ├── main.tf # module "stack" { source = "../../modules/stack" ... }
│ ├── variables.tf, terraform.tfvars
│ ├── outputs.tf
│ └── moved.tf
└── opsnexus.json # the design itselfEnvironments are free text (dev, qa, uat, prod…). Every resource gets default tags (ManagedBy = OpsNexus, plus the project and stack), and the stack's own tags are added to them. The defaults are the secure ones: encryption on, S3 public access blocked, IMDSv2 required, RDS private with deletion protection.
Some things never go into Terraform at all. RDS master passwords are managed by AWS Secrets Manager (manage_master_user_password). Secrets Manager secrets are created without their value. Generated EC2 key pairs keep the private key encrypted in OpsNexus, with a one-time .pem download.
Access is generated too. On a server, function or container you pick which buckets, tables, queues, topics, secrets and keys it may use. OpsNexus writes an IAM policy with only the actions needed, on only those ARNs, plus their KMS keys.
The catalog now has 27 resource types, from VPCs and EC2 to EKS, Lambda, Aurora, CloudFront and CloudWatch alarms. Every template and every resource option is checked with terraform init, validate, fmt and a mocked plan.
The diagram is generated, not drawn
The Architecture tab draws the stack from the same spec: AWS Cloud, then the region, the VPC, availability zones as columns and subnet tiers as rows. Instances and databases sit in their subnets, global services outside, and arrows show traffic, data, TLS, security-group and alarm relations. You can download it as SVG. Because nobody draws it, it can't drift from the code.
Plan, approve, apply on your own agents
The Deploy tab turns the design into real infrastructure. You pick an AWS access key credential and agent labels, then choose Plan, Deploy or Destroy.
Each deployment is an ordinary OpsNexus pipeline run on the project's build agents:
- Fetch a frozen copy of the stack's Terraform, so later edits don't change a run in progress.
- Plan with Terraform in Docker, and upload the plan. OpsNexus shows what will be created, updated, deleted or replaced.
- Approve. Someone with
cloud: fullapproves or rejects. Plan-only runs stop before this. - Apply, then upload the outputs.
Before step 1, OpsNexus runs a permission check. It simulates the credential's IAM permissions against what the stack needs, and stops with the missing actions and a ready policy if anything is missing. Finding that out before apply is much cheaper than finding it halfway through.
Terraform state lives in OpsNexus, behind Terraform's HTTP backend: locked during runs, encrypted at rest, with the last 20 versions kept. Each run authenticates with a one-time token that is masked in logs and stops working when the run ends. Only one deployment per stack can be active at a time.
Where to start
Open Infrastructure → Infra designer, create a stack from a template (VPC only, web app, static site with CDN, containers on Fargate, serverless API, event-driven processing or EKS), and look at the Terraform tab before changing anything. The Infra designer docs list the Terraform generated for each type and the IAM permissions the deploy credential needs.