Putting Infrastructure Config in Version Control
Exported DSL files only pay off once they live in git. Version control adds an audit trail, code review, and one-command rollback with git revert. Apply from a protected branch in CI, attach dry-run output to every pull request, and keep secrets in environment variables instead of the repo.
- Commit every DSL file to git. The repo becomes your history, blame, and rollback tool.
- You get four wins free: audit trail, blame, revert, and review on each change.
- A mono-repo per team is the best default. One PR can change linked resources together.
- Attach dry-run output to each pull request so reviewers see real planned changes.
- Keep secrets out of the repo. Use environment variables, AWS profiles, or CI-injected roles.
- Roll back with a revert plus apply, a workflow shaped by codenize's no-state design.
Every codenize tool ends the same way. You get a plain text file. It might be a Routefile, an IAMfile, or a Schemafile. Each one describes a live service. These files only pay off once they live in version control.
This guide covers the practical mechanics. Why git changes how infra work gets done. How to lay out the repo. What a good infrastructure pull request looks like. And how to roll back at 3 a.m. without touching a console.
Why Version Control for Infrastructure Config
Config that lives only in a web console has no history. Nobody knows who lowered that TTL. Nobody knows when a security group opened port 5432 to the world. Nobody knows why an IAM policy grew a wildcard.
Put the same config in git and you get four things for free:
- Audit trail.
git log Routefileshows every DNS change, with author and time. - Blame.
git blame Groupfileanswers "who opened this port?" in one command. The commit message answers why. - Revert. A bad change becomes
git revertplus an apply, not console archaeology. - Review. Changes become pull requests a colleague approves before anything hits production.
None of this needs a platform team. The export commands produce plain text. git init and a remote are the whole setup. That low floor is why version control is usually the first step of codenization, before any CI exists. The history starts earning its keep from the first commit.
The Three Classic Wins
The original codenize.tools site summed up the payoff in three properties: idempotency, grep-ability, and VCS management.
Idempotency
Idempotency means applying the same file twice gives the same result. The tool converges the live service toward the file. So the file describes desired state, not a script of steps. Run it once or ten times and the outcome is identical. It has its own guide: idempotency in infrastructure as code.
Grep-ability
Grep-ability is underrated until you have it. When DNS records, security groups, and IAM policies are text files in one repo, "where is this IP used?" becomes a one-liner:
git grep '10.0.42.17'
git grep 'db-master.example.com'
Seconds, instead of an afternoon of console tabs across three AWS accounts.
VCS management
VCS management is everything else in this guide. History, branches, reviews, tags, and reverts. It is the toolchain your team already trusts for app code, now applied to infrastructure.
Repository Layout Patterns
Two layouts dominate in practice:
| Layout | Strengths | Watch out for |
|---|---|---|
| Mono-repo per team | Cross-service grep, atomic PRs, one CI setup | Broad write access; needs CODEOWNERS |
| Per-service repos | Separate owners and review policies per service | Cross-service changes span multiple PRs |
The mono-repo is the better default. One PR can change a security group and the DNS record that points at it. Split by service only when ownership truly differs, as it often does for IAM.
Either way, use one directory per AWS account or environment. Keep tool config next to the files it drives:
infra/
├── production/
│ ├── Routefile # Roadworker - Route 53
│ ├── IAMfile # Miam - users, groups, roles, policies
│ ├── Groupfile # Piculet - security groups
│ └── Schemafile # Ridgepole - MySQL schema
├── staging/
│ └── ...
└── .github/workflows/dry-run.yml
Match directories to credentials
The directory-per-account layout maps cleanly onto credentials. The CI job that applies production/ assumes the production role. The job for staging/ assumes the staging role. A path filter decides which jobs run, so a staging-only PR never loads production credentials.
Keep the mapping dumb and visible. Anyone reading the workflow file should be able to say which directory reaches which account. And resist clever symlinks between environments early on. Duplicated staging and production files are annoying. Applying a staging value to production is far worse.
Commit and PR Conventions
Infrastructure commits deserve more care than code commits. The diff rarely explains the intent. A TTL dropping from 300 to 60 could be cut-over prep or a typo. These conventions hold up in practice:
- One logical change per pull request. A DNS cut-over and an IAM cleanup never share a PR.
- The subject line names the service and the action:
route53: point www at the new ALB. - The body states the why and links the ticket or incident.
- The dry-run output appears in the PR, pasted by the author or, better, posted by CI.
route53: point www at the new ALB
Cut-over for the checkout migration (OPS-412).
TTL was lowered to 60 in the previous PR; the
rollback target is the prior commit of this file.
Reviewer rule: approving an infra PR means you read the dry-run output, not just the DSL diff. The two can differ when reality has drifted since the branch was cut.
If the planned changes include a delete the description does not mention, ask before approving, not after applying.
Branch Protection and Secrets Hygiene
Whatever branch CI applies from is production. So protect it:
- Require pull requests.
- Require at least one approving review.
- Require a green dry-run check before merge.
Use CODEOWNERS so IAM changes always reach a security-minded reviewer. A Miam diff that adds iam:* should never merge on one distracted click.
Keep secrets out of the repo
The DSL files are mostly secret-free. A Roadworker Routefile or a Piculet Groupfile holds nothing sensitive. But connection config can. Never commit AWS keys or database passwords.
Credentials belong in environment variables, AWS profiles, or CI-injected roles. Ridgepole's database config supports ERB for exactly this reason:
password: <%= ENV['DB_PASSWORD'] %>
The same split applies to MySQL grants managed with Gratan. The grant definitions are code and belong in the repo. The admin credential that applies them does not, and stays out.
Pull-Request-Driven Changes with Dry-Run
Here is the loop that makes all of this real. Every change is a branch. Every branch gets a dry run. The diff is visible in the PR before anyone approves. A minimal CI job:
# on: pull_request
bundle exec roadwork -a --dry-run | tee plan.txt
gh pr comment "$PR_NUMBER" --body-file plan.txt
On merge to main, a second job runs the same command without --dry-run. Applying from CI, not laptops, means one set of credentials, one audit log, and no "it worked on my machine" applies. The full pattern lives in the export, review, apply workflow.
Two details make the apply job trustworthy:
- Give it short-lived credentials. An OIDC-assumed role beats a long-lived key sitting in CI secrets.
- Add a concurrency guard so two merges cannot apply at once and interleave changes.
It also helps to have the apply job repeat the dry run first and log it. If reality moved between review and merge, the log shows what was applied, not just what was approved. That is the first question asked in any postmortem.
Tagging Releases and Incident Rollback
Tag the repo after significant applies:
git tag -a dns-cutover-2026-07-06
Now "what was DNS during the incident?" is a checkout, not a reconstruction. Some teams tag every production apply automatically from CI. That is cheap and turns the tag list into a deployment log. Others tag only milestones. Either works, as long as the convention is written down and followed.
Rolling back during an incident
Rollback is the same motion as any other change, just faster:
git revert abc1234
git push origin main # CI applies the reverted state
Because the tools are idempotent, applying the reverted file converges the service back to its previous state. One honest caveat: reverting a schema change restores the schema definition, not the data. A dropped column comes back empty.
Destructive changes need their own recovery plan before the original PR merges. That is one more reason to want reviewers who actually read dry-run output. And schedule dry runs to catch anyone who "fixed" production in the console mid-incident. See dry-run and drift detection.