Testing Infrastructure Code: Linting, Validation, and Dry-Runs
Guard infrastructure changes with four gates: lint the code, validate syntax, dry-run against real state, and gate applies on the result.
Netbay Infrastructure Team
Netbay Engineering
On this page
"Test it in production" is a meme, but infrastructure code has a worse default: test it in the same production, live, during business hours, by running it. You do not have to live that way. The same discipline that shields application code — lint, syntax-check, dry-run, then review the diff — transfers directly to Ansible and Terraform, and the tools already ship the features. This post walks the four gates and how to run them from a laptop or a pipeline.
Gate 1: Lint the Code Itself
Before anything talks to a real API, keep the style and schema honest:
yamllint playbooks/ roles/
ansible-lint playbooks/*.yml
terraform fmt -recursive -check
terraform validateyamllint catches indentation and YAML structure errors that would break parsing later. ansible-lint adds Ansible-specific rules — deprecated module names, unquoted booleans, missing meta requirements — and it runs in seconds, so run it on every change. terraform fmt -check fails when files are not formatted to canonical style, which keeps diffs readable; terraform validate catches type errors in resource arguments without contacting the provider API.
Gate 2: Syntax-Check Before You Touch Anything
The next gate confirms the whole run can be parsed and resolved, still without making changes:
ansible-playbook --syntax-check playbooks/site.yml
terraform init
terraform plan -out ci.tfplan -detailed-exitcodeFor Ansible this resolves variables, roles, and handlers far enough to catch most mistakes early. For Terraform, plan is the dry-run that matters most: it reads real state, computes the diff, and writes it to a file that becomes the review artifact. With -detailed-exitcode, exit code 0 means no changes, 1 means an error, and 2 means there is a diff — a number your CI can branch on.
Gate 3: Dry-Run Against Reality
A lint pass in a sandbox does not prove the code is safe against the real environment. --check and --diff do, because they evaluate Ansible modules against the actual host state without applying:
ansible-playbook --check --diff playbooks/site.yml
ansible-playbook --check --diff playbooks/site.yml --limit app01--check reports what each task would change; --diff shows which lines of a template or file would change. Run it for the whole fleet first, then again for one host, and read the change count before greenlighting. This is the closest prompt you get before the real apply, and it is free to run as many times as you like.
Gate 4: The Apply Itself
The final gate is the apply, but at production it should be applied from a controlled plan file reviewed in version control, not typed from memory. CI produces ci.tfplan; a human approves the merge; the apply consumes the same file, so production can never do anything the reviewed plan did not already contain. For Ansible the equivalent is the tagged, limited, reviewed run from the roles post in this series.
Takeaway
Treat the four gates as a pipeline even on a laptop: lint and validate before they leave your editor, --check or plan against the real environment, then apply only the reviewed artifact. The cost is a few seconds per change; the payoff is that infrastructure changes stop being the scariest merge request of the week. Run the whole gate chain against test instances first — Netbay lets you spin up a disposable Ubuntu box to rehearse a change on netbayhosts.in before it touches anything that matters.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance