← All posts
Engineering·

Infrastructure as Code: Building the AWS Backbone

VPC, RDS, SQS, AWS Batch, three Lambda workers, CloudFront, S3 — all in Pulumi Python. And the reserved env var that costs you a failed deploy.


The pipeline needed to become a SaaS. That meant reproducible, auditable infrastructure — Pulumi Python for everything. No manual console configuration. Every resource defined in code.

What Plan 1 built

  • Networking: VPC, security groups: ALB (80/443 from internet), ECS (8000 from ALB only), RDS (5432 from ECS only). Strict least-privilege at the network layer.
  • RDS: Postgres 14, db.t3.medium, private subnets only, encrypted at rest.
  • SQS: Three FIFO job queues with distinct priorities (Starter/Pro/Enterprise), one standard webhook queue, DLQs on all of them.
  • AWS Batch: Three compute environments and queues with priority weights 10/20/30 — same job definition, different priority. The queue you're in determines how fast you get a compute instance.
  • Lambda workers: job_dispatcher (SQS FIFO → Batch submit_job), webhook_delivery (SQS → HTTP POST with exponential backoff: 30s/300s/1800s, DLQ after 3 failures), data_retention (nightly cron: deletes expired S3 objects + DB records).
  • CloudFront + S3: Dashboard bucket (static), distribution with two behaviors: /api/* → ALB (with path rewrite), * → S3 (with SPA rewrite).
  • OIDC for GitHub Actions: No stored credentials. IAM role with GitHub OIDC trust policy, assumed by CI on every deploy.

The reserved env var gotcha

Lambda internally injects AWS_DEFAULT_REGION, AWS_REGION, and all credential env vars. If you set any of them explicitly in Pulumi's environment.variables, the Lambda service returns a 400 InvalidParameterValueException at deploy time.

The error message says only "reserved." It took one failed deploy to learn this permanently. The fix is removing those keys from the Pulumi config — Lambda provides them automatically.

Why Pulumi over Terraform

Python. The infrastructure uses the same language as the application code. Type checking, real loops, real conditionals, real imports — not HCL. When the infra gets complex (and it will), you want a real programming language.