#Dev Log#GutPacer#AWS#Serverless#Cloud Architecture
The Architecture Was Simple. The Delivery Path Was Not.
What GutPacer taught me about S3 paths, CloudFront invalidation, Lambda zip contracts, OIDC, and treating deployment architecture as part of the product.
GutPacer’s cloud diagram fits comfortably on one screen.
Browser
-> CloudFront
-> S3 static frontend
Browser
-> Lambda API
-> DynamoDB
EventBridge
-> notifier Lambda
-> LINE Messaging API
There are no containers, clusters, service meshes, or long-running servers. For a small family care-record tool, that simplicity is a feature: low operating cost, few moving parts, and infrastructure that can remain quiet for most of the day.
The code was not the difficult part. The delivery path was.
Over several iterations, GutPacer deployed a frontend to the wrong S3 prefix, invalidated the wrong CloudFront path, nearly overwrote environment-specific configuration, packaged a Lambda under the wrong zip structure, and targeted the wrong AWS region. Each failure happened at a boundary between individually reasonable components.
This is the architecture lesson I retained: a serverless diagram is incomplete until it includes how code becomes the running system.
Static hosting still has contracts
The frontend is a single HTML application served under /gutpacer/ on veai.jp. That path looks like a small implementation detail. It is actually a contract shared by four places:
- the S3 object prefix;
- the CloudFront behaviour and origin mapping;
- the URL used by the browser;
- the invalidation path used after deployment.
Early deployment iterations disagreed about that contract. One sync command created a doubled path:
gutpacer/gutpacer/
Another uploaded to a location that did not match the prefix CloudFront served. A later workflow deployed the right files but invalidated a different path, so users could continue receiving cached content.
The fixes appeared as a sequence of small commits: change the S3 destination, change the invalidation path, remove the duplicated directory, verify the uploaded object list, and then remove temporary debug output after the contract was understood.
This is not an argument for a more complicated hosting platform. It is an argument for writing the path model down and verifying it from the user’s URL back to the stored object.
One file was deliberately outside the deployment
GutPacer’s frontend needs an API endpoint, but that endpoint is deployment-specific. The local frontend/config.js is ignored by Git and the production copy is managed separately in S3.
That creates an unusual deployment requirement: synchronize the application while preserving the existing production configuration.
deploy frontend assets
exclude config.js
retain the deployment-owned config.js already in S3
An ordinary “make the destination exactly match the repository” sync could delete or replace the file that tells the application where its API lives. Conversely, committing the production value into the public repository would couple source code to one environment and could expose configuration that does not belong there.
The workflow therefore excludes config.js, and the repository documents that exception. This is technical debt in the literal sense: a small operational obligation accepted to keep the static architecture simple. The important PM decision is not to pretend the exception does not exist. It needs an owner, a recovery procedure, and a test after deployment.
A Lambda zip has an internal architecture
The notifier Lambda is one JavaScript module. Its configured handler is:
index.handler
That means the deployment archive must contain index.mjs at its root. The original GitHub Actions command zipped the file with its directory path, producing an archive shaped more like this:
backend/notifier/index.mjs
The source was valid. The handler contract was not.
Changing the packaging command to flatten the path fixed the deployment artifact. More importantly, we stopped treating the zip file as a transparent container. Its internal layout is an architectural interface between CI and the Lambda runtime.
This class of failure is easy to miss in unit tests. A test imports the source from the repository. Lambda imports the handler from the archive. They are different execution paths unless the build verifies both.
The IAM error was really a region error
After adding automated API deployment, the workflow failed with an authorization message. The obvious response would have been to widen the deployment role.
That would have been the wrong fix.
The GutPacer Lambdas run in us-east-1, while a shared AWS region configuration pointed elsewhere. The CLI constructed a target in the wrong region, and the role correctly lacked permission for that target. The symptom appeared in IAM; the cause was environment resolution.
We made the region explicit in both API and notifier deployment commands:
aws lambda update-function-code ... --region us-east-1
This reduced flexibility on purpose. GutPacer does not need region portability during every deployment. It needs the workflow to target the known production architecture deterministically.
The incident also exposed that the notifier’s automated deployment had not been healthy since it was introduced. Adding a second workflow did not create the underlying problem; it made the delivery system observable enough to find it.
OIDC solved credential storage, not deployment correctness
GitHub Actions assumes a narrowly scoped AWS role through OIDC. There are no long-lived AWS access keys in the repository or workflow secrets. The role can update the intended GutPacer functions rather than receiving broad administrative access.
That is the right authentication design, but OIDC does not validate the rest of the release:
- it does not know whether the zip contains the handler at the root;
- it does not know whether the selected region is correct;
- it does not know whether
config.jssurvived the frontend sync; - it does not know whether CloudFront invalidated the path users request;
- it does not know whether the new code respects the user data boundary.
Security controls and delivery controls overlap, but they are not substitutes for one another.
The deployment architecture became three pipelines
GutPacer has three independently deployable surfaces:
- Frontend - static assets to S3, followed by CloudFront invalidation, while preserving deployment configuration.
- Core API - smoke tests, root-level Lambda package, explicit function and region.
- Notifier - separate path trigger and Lambda package because its schedule and failure modes differ from the request API.
Separating them limits unnecessary changes. Editing the notifier should not redeploy the browser. Editing a blog document should not update a Lambda. Each workflow has path filters and can also be triggered manually for recovery.
The current setup is not full infrastructure as code. Lambda functions, tables, EventBridge configuration, S3, and CloudFront are not yet represented as one reproducible stack. The repository contains deployment automation and operational documentation, not a claim that the entire environment can be rebuilt from zero with one command.
That limitation matters. For a one-family prototype, documenting known cloud state and automating high-frequency code changes was a proportionate step. Before a broader release, environment separation and reproducibility deserve another architecture decision.
How this changes the Definition of Done
The incidents moved deployment checks into product completion criteria.
A change is not done merely because the source passes tests. Depending on the component, evidence may also include:
- the exact archive layout;
- the resolved AWS account, region, and function name;
- the S3 destination and retained configuration file;
- the CloudFront invalidation path and status;
- a production smoke check through the same URL a caregiver opens;
- a rollback or data-recovery route.
This is where cloud architecture becomes a Technical PM capability rather than a diagramming exercise. Scope, risk, sequencing, and acceptance all depend on understanding the runtime interfaces.
Simple remains the right choice
None of these failures persuaded me to move GutPacer to Kubernetes, introduce a framework-heavy frontend, or combine every component into one deployment.
The architecture remains intentionally small because the workload is small. Lambda and DynamoDB fit the event-driven access pattern. S3 and CloudFront fit a static application. EventBridge fits a daily scheduled check. LINE fits the caregiver’s existing communication habit.
The improvement was not adding more infrastructure. It was making the existing contracts explicit:
source path -> artifact path -> runtime handler
repository asset -> S3 key -> CloudFront URL
workflow identity -> AWS role -> region-specific resource
code change -> verification -> user-visible release
GutPacer remains in development, and its cloud delivery history is visible in the public repository. The product page keeps the external status aligned with that technical reality.
My focus is technical product and project management grounded in cloud architecture: being able to move between stakeholder outcomes, serverless design, delivery pipelines, data boundaries, and operational evidence. See the broader VEAI LAB portfolio or hire-veai.com for related AWS and care-technology work.

Thanks for reading — built with care, for caregivers.