A CI pipeline with a budget it can't exceed

Part of my Krabber series, a Twitter clone in Go. The full source is on GitHub.

Intro

I pay for Krabber myself, so one of my rules is that the bill has to have a known ceiling, even on a bad day. I wrote a whole post on metering the database toward that goal. But there’s a scarier way to blow a budget than a greedy query: your own automation doing something expensive. A typo in Terraform, or a compromised GitHub Action, that spins up a big instance or flips you to a premium plan. So I gave Krabber’s deploy pipeline a budget it physically can’t exceed, enforced in IAM.

I. The worry

In my keyless deploy setup, a push to main lets CI assume a role that can change AWS infrastructure. That’s necessary, it’s how deploys work. But think about what a role like that could do if something went wrong: launch a cluster of huge instances, add a NAT gateway, reserve capacity for a year, or bump CloudFront from the free plan to the $1,000-a-month premium tier. Most of those are one Terraform line away. “I trust my pipeline” is not a cost control. I wanted the account itself to refuse.

II. Deny the expensive things

IAM lets an explicit Deny override any Allow, so the deploy role carries a list of denies for the things that cost real money. The cleanest example is instance size. The role may launch EC2 instances, but only the small ones:

{
  "Effect": "Deny",
  "Action": "ec2:RunInstances",
  "Resource": "arn:aws:ec2:*:*:instance/*",
  "Condition": {
    "StringNotEquals": {
      "ec2:InstanceType": ["t4g.nano", "t4g.micro", "t4g.small"]
    }
  }
}

So if a bad change tried to launch a t4g.small, fine. A c7g.16xlarge? Denied, before a single billable second. The same idea covers the rest of the money pits: the role is denied NAT gateways, dedicated hosts, reservations and Spot, Lambda provisioned concurrency, SES dedicated IPs, and crucially any change to CloudFront’s pricing plan. It’s also denied from deleting or loosening the Terraform state bucket, so it can’t kick out the floor it stands on.

III. The pricing plan I keep out of CI’s hands

The CloudFront pricing plan is the sharpest example. Krabber runs on the flat-rate Free plan, which bundles WAF, DDoS protection, and DNS and never bills overage. Pro is $15 a month and Premium is $1,000. Because the deploy role is denied every pricing-plan write, CI can never move me up a tier, not by accident and not if it were hijacked. I subscribe to the plan myself, by hand, with one command, exactly once. The pipeline builds and ships the app all day long, but the lever that changes my monthly commitment is one it isn’t allowed to touch.

IV. Why enforce it instead of trusting it

I could have just been careful. Reviewed every Terraform diff, never fat-fingered an instance type. But “be careful” degrades over time and doesn’t survive a compromise. An explicit deny is a guarantee that holds at 2 a.m., on a rushed merge, and even if my GitHub token leaked. It turns “I hope this stays cheap” into “this can’t get expensive,” and that’s a much nicer thing to believe about something you pay for out of pocket.

Conclusion

Guardrails you can’t opt out of are worth more than good intentions. By denying the deploy role the handful of actions that actually cost money, I made the expensive mistakes impossible rather than merely unlikely, and I kept the one irreversible cost lever, the pricing plan, entirely in my own hands. If you run automation against your cloud account, write down the few actions that could wreck your bill and deny them at the role. It’s a short list, and it buys a lot of sleep. Thanks for reading, and may your pipelines stay thrifty.

cd ../blog