Blog

All posts
John Damask · 2026-03-13
devlogarchitectureaws

The Problem With Copy-Paste Infrastructure

The previous blog post ended with a lesson: "Deploy scripts need the same attention as application code." That wasn't just a platitude -- it was a preview of the next thing I had to fix.

Both deploy-prod.sh and deploy-test.sh had grown into 500+ line monoliths. Every deploy ran every step: store secrets, provision certificates, build a Docker image, package Lambda code, deploy CloudFormation, seed DynamoDB, upload the frontend, invalidate CloudFront. Changed one HTML file? Cool, enjoy the Docker build. Updated a prompt template? Here comes a full CloudFormation deploy.

Worse, every Lambda handler and static file was explicitly listed in three separate places across both scripts. When I added the takedown status page, it took three incremental commits just to get the file listed everywhere it needed to be. And since the two scripts were independent copies of the same logic, every change had to be made twice -- identically.

The Fix: One Library, Two Thin Wrappers

I extracted all the deployment logic into scripts/deploy-common.sh -- 14 functions that do the actual work. The key constraint: zero environment config. No account IDs, no domain names, no bucket names. The common file is a pure function library. Everything flows through variables set by the calling script.

The two deploy scripts shrank from 500+ lines each to about 50 lines: hardcoded config at the top, a source line to load the shared library, and a case statement to dispatch subcommands:

No args? Full deploy, same as before. Unknown subcommand? Usage message and exit.

Lambda function suffixes and frontend files are defined once as arrays at the top of the common file. Next time I add a Lambda or a page, it goes in one place.

Fixing the Prompts Annoyance

There was a long-standing pain point with prompts. The deploy scripts used attribute_not_exists as a DynamoDB condition, meaning they'd seed prompts on first deploy but never update them. Change a prompt file, deploy, and... nothing happens. The workaround was documented in CLAUDE.md as a deployment warning: manually run a raw aws dynamodb put-item command. Every time.

The refactored deploy_prompts always force-updates. Change a prompt, run ./deploy-prod.sh prompts, done. One of those fixes that makes you wonder why it wasn't always this way.

The Merge Conflict From Hell

The branch diverged from main right before a big PR landed -- the takedown follow-up emails work, which added three new Lambda handlers, four new frontend pages, email template seeding, and replaced an entire S3 bucket configuration. The merge conflict touched every file in the PR.

Resolving it was actually a good stress test for the new architecture. Instead of diffing two 500-line scripts trying to figure out which lines are new, I just updated the LAMBDA_SUFFIXES and FRONTEND_FILES arrays, added the new handler files to package_lambda_code, and dropped email template seeding into deploy_prompts. Deleted one obsolete function entirely. The modular structure made what could have been a nightmare into something methodical.

Lessons

  1. Shared logic, not shared config. Putting functions in a common file only works if the common file has zero opinions about the environment. The moment you hardcode a bucket name or account ID in the shared layer, you've created a path for cross-environment mistakes.
  2. Arrays as documentation. Defining LAMBDA_SUFFIXES and FRONTEND_FILES as arrays at the top of a file isn't just DRY -- it's a manifest. New developers (or future me) can see at a glance exactly what gets deployed.
  3. Fix the workaround, not the warning. The prompts issue was "solved" with a CLAUDE.md warning for months. The actual fix was one line: remove the condition expression. Sometimes the right answer isn't better documentation, it's deleting the thing that needs documenting.