Field notes · Infrastructure as Code & CI/CD
Choosing an IaC tool, and building a pipeline that doesn't waste your time
Which IaC tool you reach for depends on the cloud and the team — there's no single right answer. Here's the combination I default to, how I wire pipelines per environment, and the two habits (local testing, build-time triage) that save the most time over a project's life.
The right IaC tool is a function of your cloud and your team, not a universal default. Pick the tool for the constraint you actually have, then build the pipeline around it.
01Picking the tool
- AWS CDK — if you're all-in on AWS and want infrastructure defined in a programming language your team already knows (TypeScript, Python, etc.) instead of a templating DSL.
- CloudFormation — the native option when you want AWS-managed state and deep AWS-service coverage without a third-party control plane.
- Terraform — the default when you're cloud-agnostic, or likely to be: one workflow and state model across AWS, GCP, and Azure.
In practice, most of my work has landed on a combination: Terraform orchestrating CloudFormation — Terraform owns the deployment pipeline and state, while CloudFormation templates handle the AWS-native resources Terraform hands off to.
02Pipeline tooling
- GitHub Actions or Bitbucket Pipelines, depending on where the repo lives.
- A separate pipeline configuration per environment — each one defines what should run and what should trigger it when code lands on its branch.
03Feature-branch promotion flow
- Build and test the feature in Sandbox first.
- Promote via pull request through Dev → UAT → Production, in that order.
- Each hop is a real gate: run the tests, catch what breaks, fix it, before the PR moves to the next environment.
04Where the time actually goes
- Every failed pipeline run to catch a typo or a bad variable is a run you didn't need. Test locally or manually in Sandbox first, before pushing — it cuts the number of full pipeline cycles substantially.
- When you have the time, profile why builds are slow. A pipeline that takes 20 minutes instead of 5 isn't just an inconvenience — it's 15 minutes of friction on every single promotion, multiplied across every environment and every engineer.
Pick the IaC tool that matches your cloud commitment — CDK or CloudFormation for AWS-native, Terraform for multi-cloud, or Terraform-orchestrating-CloudFormation as a practical middle ground. Wire a pipeline per environment in GitHub Actions or Bitbucket Pipelines, promote through Sandbox → Dev → UAT → Production via PR, and protect your team's time by testing locally first and periodically auditing why builds are slow.
With infrastructure as code, it really depends on which cloud you're using and which tool fits how your team already works. If you're all-in on AWS and want infrastructure written in an actual programming language instead of a templating format, CDK is a strong option — it leans on the language your team already writes in. If you want to stay closer to native AWS and lean on AWS-managed state, CloudFormation is the straightforward choice. And if you're cloud-agnostic, or think you might need to be someday, Terraform is where I'd go.
There are a lot of ways to tackle this, and no single right answer. But if I'm being honest about what I've actually reached for most often, it's a combination of Terraform and CloudFormation — Terraform owns the deployment and drives the pipeline, and it deploys CloudFormation templates underneath for the pieces that make sense to hand off to native AWS.
How the pipeline actually runs
For the pipeline itself, I've used GitHub with GitHub Actions, and Bitbucket with Bitbucket Pipelines — which one depends entirely on where the team's code already lives. Either way, you need a pipeline configuration per environment: which pipeline should run, and how it should get invoked when code lands on a given branch.
The workflow I keep coming back to is a feature-branch framework. If you're building out a feature, you develop it and test it in Sandbox first. From there, you open a pull request and move it through the environments in order — Dev, UAT, Production — as you go. At each stage, you test the feature again, and that's where you catch whatever broke, fix it, and move forward.
Cutting down the number of runs
To reduce how many times you're pushing and re-deploying just to catch something small, I'd figure out if there's a way to test locally first — manually, in a sandbox environment, or right on your own machine. There's usually more than one way to do that, and every run you catch before it hits the pipeline is time your whole team gets back.
And when you have the time for it, dig into why your build runs are taking as long as they are, and look for ways to optimize that. It's easy to let pipeline duration become background noise nobody questions — but a slow build is a tax you pay on every single promotion, for every engineer, for the life of the project.