<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Mark]]></title><description><![CDATA[Product Strategist @ FDA | LLM & Algorithmic Governance specialist. I turn ambiguous gov-tech requirements into tangible AI solutions. 10+ years leading digital transformation in high-stakes, regulated spaces (DoD, FDA). Let's build the future.]]></description><link>https://newsletter.markgingrass.com</link><image><url>https://substackcdn.com/image/fetch/$s_!CNag!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b60c820-4c60-40c2-bcb7-e7d7dd94e30c_1024x1024.jpeg</url><title>Mark</title><link>https://newsletter.markgingrass.com</link></image><generator>Substack</generator><lastBuildDate>Mon, 24 Aug 2026 05:03:06 GMT</lastBuildDate><atom:link href="https://newsletter.markgingrass.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Mark]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[markgingrass@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[markgingrass@substack.com]]></itunes:email><itunes:name><![CDATA[Mark]]></itunes:name></itunes:owner><itunes:author><![CDATA[Mark]]></itunes:author><googleplay:owner><![CDATA[markgingrass@substack.com]]></googleplay:owner><googleplay:email><![CDATA[markgingrass@substack.com]]></googleplay:email><googleplay:author><![CDATA[Mark]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Petshots: Building a Real SaaS Product on AWS]]></title><description><![CDATA[A Founder&#8217;s Guide to Cloud Architecture &#8212; and the Exam That Changed How I Think]]></description><link>https://newsletter.markgingrass.com/p/petshots-building-a-real-saas-product</link><guid isPermaLink="false">https://newsletter.markgingrass.com/p/petshots-building-a-real-saas-product</guid><dc:creator><![CDATA[Mark]]></dc:creator><pubDate>Sat, 04 Jul 2026 16:56:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6K3y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Petshots: Building a Real SaaS Product on AWS</h1><h2><strong>A Founder&#8217;s Guide to Cloud Architecture &#8212; and the Exam That Changed How I Think</strong></h2><p><em>By Mark Gingrass</em></p><div><hr></div><blockquote><p><strong>A note on how to read this guide</strong></p><p>This is not a textbook. It is a founder&#8217;s account &#8212; written first-person &#8212; of what I actually built, why I made each decision, and what I learned along the way. If you are studying for the <strong>AWS Solutions Architect Associate (SAA-C03)</strong> exam, you will find exam tips throughout. If you are a developer who just wants to understand how a modern cloud-native SaaS product is wired together, you will find that too. And if you are an aspiring cloud architect who learns best by seeing a real product evolve &#8212; rather than reading theoretical slide decks &#8212; you are exactly the reader I had in mind.</p><p>Every service name introduced in this guide is also expanded in the <strong>Glossary</strong> at the end. When I use an acronym for the first time, I spell it out in full. When I use it again, I occasionally remind you what it stands for &#8212; not because I think you forgot, but because repetition is how technical vocabulary actually sticks.</p><p>Live at: </p><p><a href="https://petshots.app">https://petshots.app</a></p></blockquote><div><hr></div><h2><strong>Foreword &#8212; From Clicking Buttons to Commanding the Cloud</strong></h2><p>Not long ago, my entire AWS experience was clicking around the console &#8212; the browser-based control panel where you can spin up a server, create a database, or launch a hundred other things by pointing and clicking. It felt powerful. It felt real. But I was always just a few wrong clicks away from not knowing what I had actually done or how to undo it.</p><p>Then I started using the <strong>AWS CLI</strong> &#8212; the Command Line Interface, a terminal program that lets you talk to AWS directly by typing commands. This changed everything. Instead of hunting through menus, I could type exactly what I wanted. More importantly, I could <em>see</em> what I was doing. A command has parameters. Parameters have names. Those names tell you what AWS is actually thinking about when it responds to your request.</p><p>Then I started building this app &#8212; Petshots &#8212; and the complexity grew faster than I expected. Multi-service architectures, IAM (Identity and Access Management) policies, VPC (Virtual Private Cloud) networking, CIDR (Classless Inter-Domain Routing) blocks, presigned URLs, JWT (JSON Web Token) authorizers... I was hitting concepts I had read about but never had a concrete reason to understand deeply.</p><p>That is when I started asking Claude &#8212; Anthropic&#8217;s AI assistant &#8212; a lot of questions. Not &#8220;write this code for me&#8221; questions, but &#8220;why does this work this way?&#8221; questions. Why does a NAT (Network Address Translation) instance cost less than a NAT Gateway but have no HA (High Availability)? What is the difference between an IAM role and an IAM user? Why does an S3 presigned POST work differently from a presigned PUT?</p><p>What I discovered was that building a real product is the best possible preparation for the SAA-C03 certification exam. The exam asks you to reason about trade-offs between services. Building a product <em>forces</em> you to make those trade-offs with real money, real users, and real consequences. When you have to choose between a NAT Gateway at $32/month and a NAT instance at $3.50 a month for a zero-revenue startup, that is not a textbook question &#8212; it is a genuine business decision, and the reasoning behind it is exactly what the exam is testing.</p><p>This guide is the companion to that journey. I will walk you through every layer of the Petshots architecture &#8212; not just <em>what</em> it is, but <em>why</em> it is that way, what the business trade-off was, and how the same concept shows up on the exam.</p><p>Let us start with the product itself.</p><div><hr></div><h2><strong>What We Built, and Why</strong></h2><h3><strong>The Problem</strong></h3><p>I have a dog named Ollie. Every time I want to take him somewhere new &#8212; a dog bar, a boarding facility, a doggie daycare &#8212; they ask for proof of vaccination. Specifically his rabies certificate. And every single time, I scramble. I dig through emails, I search PDF folders, I ask my vet&#8217;s office to resend something. It is embarrassing and it takes minutes I do not have when I am standing at the front desk with an impatient receptionist and an excited 65-pound dog.</p><p>My workaround, before building this app, was to ask Claude to search my Gmail for rabies records. That is not a product. That is a hack. And if my workaround for the problem is <em>asking an AI to grep my email</em>, the problem is real and the opportunity is concrete.</p><h3><strong>The Wedge</strong></h3><p>The <strong>last-mile retrieval moment</strong> is the product&#8217;s core: at the door, on your phone, with the front desk staff waiting &#8212; pull up proof of vaccination in under ten seconds. That is the founding promise.</p><p>Everything else &#8212; reminders, sharing with groomers, medication tracking &#8212; is built on top of that wedge. But the wedge has to work perfectly first.</p><h3><strong>What the App Does</strong></h3><p>Petshots is a <strong>SaaS application</strong> (Software as a Service &#8212; you use it through a browser without installing anything) for pet owners to:</p><ol><li><p><strong>Store vaccination records</strong> &#8212; upload PDFs or photos of your dog&#8217;s or cat&#8217;s vaccine certificates. Label them (Rabies, DHPP, Bordetella) and attach an expiry date.</p></li><li><p><strong>See at a glance what is current and what is overdue</strong> &#8212; a color-coded status system shows which vaccines are current (green), due soon within 30 days (amber), or expired (red). No spreadsheet needed.</p></li><li><p><strong>Show records at the door</strong> &#8212; one tap opens the document on your phone. There is a dedicated &#8220;Quick Show&#8221; button that pulls up your most important cert immediately.</p></li><li><p><strong>Share a pet passport</strong> &#8212; generate a QR (Quick Response) code link that anyone can scan. No login required on the recipient&#8217;s end. Facilities can verify your pet&#8217;s status without you standing there.</p></li><li><p><strong>Get reminders</strong> &#8212; email notifications before vaccines expire, so you never get caught off guard at check-in.</p></li></ol><p>Here is what that looks like in practice. The dashboard leads with the pets themselves &#8212; each portrait ringed in the color of its most urgent record &#8212; and surfaces anything that needs attention as a banner up top:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6K3y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6K3y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 424w, https://substackcdn.com/image/fetch/$s_!6K3y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 848w, https://substackcdn.com/image/fetch/$s_!6K3y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 1272w, https://substackcdn.com/image/fetch/$s_!6K3y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6K3y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png" width="780" height="1688" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1688,&quot;width&quot;:780,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:202656,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6K3y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 424w, https://substackcdn.com/image/fetch/$s_!6K3y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 848w, https://substackcdn.com/image/fetch/$s_!6K3y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 1272w, https://substackcdn.com/image/fetch/$s_!6K3y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc075721a-a05b-49ae-b38c-bf9a157d0037_780x1688.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Tapping a pet opens its record list. All four status states read at a glance &#8212; this one screen is the product&#8217;s core promise delivered:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kkFU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kkFU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 424w, https://substackcdn.com/image/fetch/$s_!kkFU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 848w, https://substackcdn.com/image/fetch/$s_!kkFU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 1272w, https://substackcdn.com/image/fetch/$s_!kkFU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kkFU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png" width="780" height="1688" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1688,&quot;width&quot;:780,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:145915,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kkFU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 424w, https://substackcdn.com/image/fetch/$s_!kkFU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 848w, https://substackcdn.com/image/fetch/$s_!kkFU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 1272w, https://substackcdn.com/image/fetch/$s_!kkFU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e1d57aa-8b97-4371-854f-684102b1da5e_780x1688.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And this is the shareable passport &#8212; the public page a groomer or boarding facility sees when they scan the QR code. No login, no account; just the records, their status, and links to the certificates:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!b7z9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!b7z9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 424w, https://substackcdn.com/image/fetch/$s_!b7z9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 848w, https://substackcdn.com/image/fetch/$s_!b7z9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 1272w, https://substackcdn.com/image/fetch/$s_!b7z9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!b7z9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png" width="780" height="1688" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1688,&quot;width&quot;:780,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:155233,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!b7z9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 424w, https://substackcdn.com/image/fetch/$s_!b7z9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 848w, https://substackcdn.com/image/fetch/$s_!b7z9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 1272w, https://substackcdn.com/image/fetch/$s_!b7z9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F194c25bc-b3af-447b-8072-525277ae0c42_780x1688.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>It supports up to three pets on the free tier. Pet photos, per-pet record tracking, and a shareable public passport page are all included.</p><p>You can see it live at </p><p>https://petshots.app</p><p> &#8212; that URL is the real production application.</p><h3><strong>Why We Built It on AWS</strong></h3><p>I chose AWS deliberately. I was working toward the AWS Solutions Architect Associate certification and I wanted the exam prep to be grounded in something real. There is a meaningful difference between understanding a service because you read a diagram and understanding it because you made a decision about it that cost or saved real money.</p><p>AWS also happens to be exactly the right choice for this kind of product: pay-per-use pricing means a zero-revenue startup pays almost nothing when nobody is using it, and the same architecture scales to millions of users without a rewrite.</p><div><hr></div><h2><strong>The Architecture in Full</strong></h2><p>Before we walk through each piece, here is the complete system laid out visually. Every box in this diagram is a real AWS service, deployed and live. The solid arrows are the request paths a user actually exercises; the dashed arrows are service-to-service trust and background work.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_yw3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_yw3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 424w, https://substackcdn.com/image/fetch/$s_!_yw3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 848w, https://substackcdn.com/image/fetch/$s_!_yw3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 1272w, https://substackcdn.com/image/fetch/$s_!_yw3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_yw3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png" width="1456" height="1208" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1208,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:337462,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_yw3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 424w, https://substackcdn.com/image/fetch/$s_!_yw3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 848w, https://substackcdn.com/image/fetch/$s_!_yw3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 1272w, https://substackcdn.com/image/fetch/$s_!_yw3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe11d5f6f-4edf-4c6d-85ad-3e9b627018ee_1880x1560.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Five CloudFormation <strong>stacks</strong> deploy all of this. A stack is the unit of deployment in AWS infrastructure-as-code: a named collection of resources that are created, updated, and destroyed together. Think of a stack the way you would think of a department in a company &#8212; self-contained, with defined interfaces to the other departments.</p><div><hr></div><h2><strong>The Network Foundation</strong></h2><h3><strong>What a VPC Actually Is</strong></h3><p>A <strong>VPC (Virtual Private Cloud)</strong> is your private network inside AWS. Picture AWS&#8217;s global infrastructure as a massive shared office building &#8212; thousands of tenants, but everyone is on their own locked floor. Your VPC is your floor. Other tenants cannot see your resources, cannot route traffic to your databases, cannot even acknowledge your servers exist. The isolation is enforced by AWS at the hardware level.</p><p>From a business standpoint, the VPC is the security perimeter for your entire product. If your VPC is not configured correctly, all the application-level security in the world can be undermined. This is why cloud architects spend so much time on networking &#8212; it is the foundation everything else rests on.</p><p>Every resource in this guide &#8212; every Lambda function, every database, every EC2 (Elastic Compute Cloud) instance &#8212; runs inside the Petshots VPC.</p><h3><strong>Understanding the Address Space</strong></h3><p>Our VPC uses the address block <code>10.0.0.0/16</code>. Let me unpack what that means, because CIDR (Classless Inter-Domain Routing) notation shows up constantly in AWS work and on the exam.</p><p>An IP (Internet Protocol) address is a 32-bit number, written in four groups of digits separated by dots: <code>10.0.0.0</code>. The <code>/16</code> after the address is a <strong>prefix length</strong> &#8212; it tells you how many of those 32 bits are fixed (the &#8220;network&#8221; part) and how many are free for you to assign to individual resources (the &#8220;host&#8221; part).</p><p>With <code>/16</code>, the first 16 bits are fixed (<code>10.0</code>). The remaining 16 bits are yours. 2^16 = 65,536 possible addresses. That is your VPC&#8217;s capacity.</p><p>When you carve subnets out of the VPC, you take a slice of those 65,536 addresses and dedicate it to a specific zone and purpose. Here are the CIDR ranges that commonly appear on the exam, with their host counts:</p><p>CIDRFixed bitsAddressesAWS usableExample use<code>/16</code>1665,53665,531Entire VPC<code>/24</code>24256251Medium subnet<code>/28</code>281611Smallest AWS allows</p><p><strong>Why does AWS subtract 5 from every subnet?</strong> AWS reserves the first four addresses and the last address in every subnet for internal purposes: the network address, the VPC router, DNS, future use, and the broadcast address. So a <code>/28</code> subnet (16 addresses) only gives you 11 usable IPs. The exam tests this &#8212; knowing that <code>/28</code> is the smallest allowed subnet and it yields only 11 usable IPs is worth remembering.</p><p><strong>Why </strong><code>10.x.x.x</code><strong>?</strong> This is private address space defined by RFC 1918 &#8212; address ranges reserved for private networks that will never appear on the public internet. The three RFC 1918 ranges are <code>10.0.0.0/8</code>, <code>172.16.0.0/12</code>, and <code>192.168.0.0/16</code>. Using private space means our internal addresses never collide with public internet routing.</p><blockquote><p><strong>Exam Tip:</strong> CIDR questions appear frequently. Know that <code>/16</code> = 65,536, <code>/24</code> = 256, <code>/28</code> = 16 (subtract 5 for AWS reservations to get usable IPs). Also know the three RFC 1918 private ranges &#8212; the exam sometimes asks which CIDR is valid for a VPC.</p></blockquote><h3><strong>Subnets and Availability Zones</strong></h3><p>A <strong>subnet</strong> is a subdivision of the VPC, pinned to exactly one <strong>AZ (Availability Zone)</strong>. An Availability Zone is a physically separate data center &#8212; meaning separate power feeds, separate cooling systems, separate fiber paths in from the internet &#8212; within a geographic region. The region <code>us-east-1</code> (Northern Virginia) has six AZs, named <code>us-east-1a</code> through <code>us-east-1f</code>.</p><p>The purpose of multiple AZs is <strong>fault isolation</strong>. If a power failure or network event takes down one data center, your resources in other AZs are completely unaffected. This is the core of <strong>HA (High Availability)</strong> design on AWS &#8212; spread your resources across at least two AZs so that no single failure takes down your entire product.</p><p>Petshots uses two AZs and three subnet tiers, giving us six subnets total (three tiers &#215; two AZs):</p><p>TierAWS CDK LabelWhat Lives HereInternet Access?Public<code>PUBLIC</code>NAT instanceDirect (via Internet Gateway)App<code>PRIVATE_WITH_EGRESS</code>EC2 / AppStack (when deployed)Outbound only (via NAT)Data<code>PRIVATE_ISOLATED</code>Aurora clusterNone &#8212; completely isolated</p><p>Let me explain each tier in plain English:</p><p><strong>Public subnets</strong> have a route to an <strong>IGW (Internet Gateway)</strong> &#8212; the door between your VPC and the public internet. Resources in the public subnet with a public IP address can send and receive traffic to and from anywhere on the internet. Our NAT instance lives here.</p><p><strong>App subnets (Private with Egress)</strong> have no direct route to the internet inbound &#8212; nothing on the internet can initiate a connection to these resources. But they <em>can</em> make outbound connections &#8212; to download a software package, to call an AWS API endpoint, to send an email through SES (Simple Email Service). They do this by sending traffic through the NAT instance in the public subnet, which forwards it out through the Internet Gateway and relays responses back. This one-way door pattern is the business logic of the App tier: your application servers can talk to the world, but the world cannot directly poke at them.</p><p><strong>Data subnets (Private Isolated)</strong> have no route to the internet in either direction. Aurora cannot initiate a connection outbound and nothing can initiate a connection inbound. The only resources that can reach the database are those in the same VPC with the correct Security Group (SG) permissions configured. This is defense-in-depth: even if an attacker somehow compromised the application tier, the database is behind an additional layer of isolation.</p><p>All of this is easier to hold in your head as a picture. Here is the actual Petshots VPC &#8212; two Availability Zone columns, three tiers deep, with the real CIDR block each subnet was assigned:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F93i!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F93i!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 424w, https://substackcdn.com/image/fetch/$s_!F93i!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 848w, https://substackcdn.com/image/fetch/$s_!F93i!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 1272w, https://substackcdn.com/image/fetch/$s_!F93i!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F93i!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png" width="1456" height="1013" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1013,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:207313,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!F93i!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 424w, https://substackcdn.com/image/fetch/$s_!F93i!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 848w, https://substackcdn.com/image/fetch/$s_!F93i!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 1272w, https://substackcdn.com/image/fetch/$s_!F93i!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc00e2c03-9df5-4448-aa13-2d704de64471_1840x1280.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Trace any packet&#8217;s options on this diagram and the tier rules become mechanical: traffic in the green tier can go both ways through the Internet Gateway; the amber tier can only go <em>out</em>, and only via the NAT box in the green tier; the red tier goes nowhere.</p><blockquote><p><strong>Exam Tip:</strong> Subnet type questions are among the most common in the VPC section. Memorize these: <code>PRIVATE_ISOLATED</code> = no internet at all. <code>PRIVATE_WITH_EGRESS</code> = outbound internet via NAT, no inbound. <code>PUBLIC</code> = two-way internet via IGW. Data tiers (databases, caches) should almost always be <code>PRIVATE_ISOLATED</code>.</p></blockquote><h3><strong>Internet Gateway vs NAT &#8212; A Critical Business Trade-off</strong></h3><p>The <strong>IGW (Internet Gateway)</strong> is a fully managed, highly available VPC component that enables two-way communication between resources in your public subnets and the public internet. It is effectively free &#8212; no hourly charge, no data processing charge. You simply attach it to your VPC and update route tables.</p><p>The <strong>NAT (Network Address Translation)</strong> device provides the one-way outbound door for private subnets. &#8220;Network Address Translation&#8221; means it takes packets from your private resources (which have private IPs that are not routable on the public internet) and rewrites them to appear as if they came from the NAT device&#8217;s own public IP &#8212; then relays the responses back. The result: your private server can call an external API, but the external API cannot initiate a connection back to your private server.</p><p>AWS offers two ways to implement NAT, and this is one of the most-tested trade-offs on the exam:</p><p>NAT GatewayNAT InstanceManaged byAWS (fully managed)You (it is an EC2 VM)HA (High Availability)Yes &#8212; HA within the AZ, but you need one per AZ for full HANo &#8212; single point of failureScalingAutomaticManual (you resize the EC2)BandwidthUp to 100 GbpsLimited by EC2 instance typeCost~$32/month for one AZ + data processing fees~$3.50/month for <code>t4g.nano</code> + EBSPatching / updatesAWS handles itYou are responsible</p><p>Petshots chose a <strong>NAT instance</strong> using the open-source <strong>fck-nat</strong> AMI (Amazon Machine Image &#8212; a pre-packaged OS image for EC2) running on a <code>t4g.nano</code> (a tiny ARM-based VM). The business reasoning: at zero revenue and essentially zero traffic, paying nine times more per month for a managed NAT Gateway delivers no meaningful benefit. The single point of failure is acceptable when the app is in early-stage growth &#8212; the cost savings matter more.</p><p>This is a classic startup vs enterprise trade-off. A bank would never choose a NAT instance. A zero-revenue SaaS startup probably should. The SAA-C03 exam tests your ability to recognize which scenario calls for which choice.</p><blockquote><p><strong>Exam Tip:</strong> When the exam says &#8220;production workload&#8221; or &#8220;HA requirement,&#8221; choose NAT Gateway. When the exam says &#8220;cost optimization&#8221; or &#8220;small workload,&#8221; a NAT instance is defensible. Know that a NAT Gateway is per-AZ: for full HA across two AZs, you need two NAT Gateways, doubling the cost.</p></blockquote><h3><strong>VPC Endpoints &#8212; Traffic That Never Leaves AWS</strong></h3><p>A <strong>VPC Endpoint</strong> lets traffic from your VPC reach an AWS service without going out to the public internet. This matters for two reasons: security (traffic stays on Amazon&#8217;s private backbone, never crossing the internet) and cost (you avoid NAT data-processing charges for traffic that would otherwise have to leave the VPC and come back in).</p><p>There are two types, and they work very differently:</p><p><strong>Gateway Endpoints</strong> &#8212; available for S3 (Simple Storage Service) and DynamoDB only. They are free. You add them to your route table, and traffic destined for those services is routed privately. We use one for S3 &#8212; every call our Lambda makes to the uploads bucket goes through the gateway endpoint, staying entirely on Amazon&#8217;s network.</p><p><strong>Interface Endpoints (PrivateLink)</strong> &#8212; available for almost every other AWS service (SSM &#8212; Systems Manager, Secrets Manager, KMS &#8212; Key Management Service, SES, etc.). They work differently: each endpoint creates an ENI (Elastic Network Interface &#8212; a virtual network card) in your subnet, giving the service a private IP address inside your VPC. They cost approximately $7&#8211;8 per endpoint per AZ per month.</p><p>Here is the business math for Petshots: if we wanted interface endpoints for SSM (three endpoints: <code>ssm</code>, <code>ec2messages</code>, <code>ssmmessages</code>) across two AZs, that would be 3 &#215; 2 &#215; $7.50 = <strong>$45/month</strong>. Our entire serverless stack costs nearly nothing idle. Adding $45/month in VPC interface endpoints purely for the theoretical benefit of not using our $3.50/month NAT instance for those calls would be irrational.</p><p>We use the NAT instance for SSM and Secrets Manager access, and reserve the free S3 gateway endpoint for S3 traffic (which we use heavily for document uploads and metadata reads).</p><blockquote><p><strong>Exam Tip:</strong> Gateway Endpoints (S3, DynamoDB) = free, add to route tables. Interface Endpoints (everything else) = ~$7/mo per endpoint per AZ, use ENIs. If the exam describes a scenario where an EC2 instance in a private subnet needs to access S3 &#8220;without going through the internet&#8221; &#8212; a Gateway Endpoint is the answer. For NAT cost reduction when traffic is primarily S3 or DynamoDB, a Gateway Endpoint is usually the right recommendation.</p></blockquote><h3><strong>Security Groups &#8212; Stateful Firewalls at the Resource Level</strong></h3><p>A <strong>Security Group (SG)</strong> is a virtual firewall that you attach to individual resources (EC2 instances, RDS clusters, Lambda functions in a VPC, etc.) to control which traffic reaches them.</p><p>The key architectural concept is <strong>stateful</strong>: if you allow a type of inbound traffic, the corresponding response traffic is automatically allowed outbound &#8212; you do not need to explicitly write a rule for the return path. This is different from a traditional corporate network firewall where you might need to manage both directions separately.</p><p>For Petshots:</p><ul><li><p>The Aurora database Security Group allows inbound on TCP port 3306 (MySQL&#8217;s port) from resources in the App-tier Security Group only.</p></li><li><p>The NAT instance Security Group allows inbound from the VPC&#8217;s private subnets and outbound to anywhere.</p></li><li><p>API Gateway does not use VPC Security Groups (it is a managed public endpoint).</p></li></ul><p>Security Groups implement the <strong>principle of least privilege</strong> at the network layer: each resource can be reached only by the resources that have a legitimate reason to reach it, on only the ports they need.</p><blockquote><p><strong>Exam Tip &#8212; Security Groups vs NACLs (Network Access Control Lists):</strong></p><p>Security GroupNACLApplied toIndividual resource (EC2, RDS, etc.)Entire subnetStateStateful (return traffic auto-allowed)Stateless (must allow both directions)Rule typesAllow onlyAllow and DenyRule evaluationAll rules consideredLowest rule number that matches wins</p><p>NACLs (Network Access Control Lists) are the subnet-level firewall. Because they are stateless, you must write rules for both inbound and outbound directions for any connection you want to permit. NACLs also support <strong>Deny rules</strong> &#8212; you can explicitly block a specific IP range, which Security Groups cannot do (Security Groups can only allow). Petshots uses only Security Groups. NACLs appear on the exam but were not needed here.</p></blockquote><div><hr></div><h2><strong>Identity and Access</strong></h2><h3><strong>Cognito User Pool vs Identity Pool</strong></h3><p>Cognito is AWS&#8217;s managed authentication and user management service. It offers two products with confusingly similar names &#8212; understanding the difference is essential for the exam and for building real applications.</p><p><strong>User Pool</strong> = a user directory with built-in authentication. It stores usernames (email addresses, in our case), hashed passwords, user attributes, and verification state. It handles the sign-up flow, email verification, password resets, and login &#8212; issuing <strong>JWT (JSON Web Token)</strong> tokens on successful authentication. Think of the User Pool as answering the question: <em>&#8220;Who is this person?&#8221;</em></p><p><strong>Identity Pool (Federated Identity)</strong> = exchanges tokens for temporary AWS credentials (access key, secret key, session token) so that users can call AWS services directly from the browser or mobile app &#8212; without your backend being in the middle. Think of the Identity Pool as answering: <em>&#8220;Now that I know who this person is, what AWS resources can they touch directly?&#8221;</em></p><p>Petshots uses only a User Pool. Users authenticate, receive a JWT, and send that JWT to our Lambda via the API. The Lambda &#8212; running with its own IAM (Identity and Access Management) role &#8212; calls S3 on their behalf. We do not need to give users direct AWS credentials because the Lambda intermediates all S3 access. This is safer: if a user&#8217;s JWT is stolen, the attacker gets access to our API, not to S3 directly with arbitrary permissions.</p><blockquote><p><strong>Exam Tip:</strong> If a question says &#8220;users need to upload files directly to S3 from the browser using their Cognito identity,&#8221; the answer involves a Cognito <strong>Identity Pool</strong> (to exchange the User Pool JWT for temporary IAM credentials). If the question says &#8220;users authenticate and call an API,&#8221; the answer is just a <strong>User Pool</strong> and a JWT authorizer &#8212; no Identity Pool needed.</p></blockquote><h3><strong>JWTs &#8212; How Authentication Works Without a Database Lookup</strong></h3><p>After a successful login, Cognito issues three tokens:</p><ul><li><p><strong>ID Token</strong> &#8212; contains claims about the user: their email address, their Cognito <code>sub</code> (subject &#8212; a unique UUID that never changes even if the email changes), and any custom attributes. Use this in your application to know <em>who</em> the user is.</p></li><li><p><strong>Access Token</strong> &#8212; contains the user&#8217;s permissions (scopes). Use this when calling protected APIs. This is what we send in the <code>Authorization: Bearer</code> header to API Gateway.</p></li><li><p><strong>Refresh Token</strong> &#8212; long-lived (default 30 days). Used silently in the background to obtain new Access and ID tokens after they expire (default expiry: 1 hour), without requiring the user to log in again.</p></li></ul><p>A <strong>JWT (JSON Web Token)</strong> is a base64url-encoded string with three dot-separated parts:</p><pre><code><code>[header].[payload].[signature]

Example (simplified):
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI2NDY4NjQwOCIsImVtYWlsIjoib2xsaWVAZXhhbXBsZS5jb20ifQ.[RSA signature]
</code></code></pre><p>The <strong>header</strong> identifies the algorithm (RS256 &#8212; RSA with SHA-256). The <strong>payload</strong> contains the claims (who you are, when the token expires, what permissions you have). The <strong>signature</strong> is created using Cognito&#8217;s private RSA key and mathematically proves the payload has not been tampered with.</p><p>Anyone with Cognito&#8217;s <em>public</em> key (published at a well-known URL called the JWKS &#8212; JSON Web Key Set &#8212; endpoint) can verify the signature without calling Cognito at all. This is the key insight of JWT-based auth: <strong>no database lookup required at verification time</strong>. The token is self-contained proof. This is why JWTs work so well for serverless APIs &#8212; there is no session store to maintain.</p><blockquote><p><strong>Exam Tip:</strong> Know which token to use where. <strong>Access Token</strong> &#8594; for calling protected APIs (our API Gateway). <strong>ID Token</strong> &#8594; for learning who the user is in your application logic. <strong>Refresh Token</strong> &#8594; for silent re-authentication. Never send a Refresh Token to an API endpoint.</p></blockquote><h3><strong>SRP &#8212; Why Your Password Never Travels on the Network</strong></h3><p>Cognito&#8217;s default login flow uses <strong>SRP (Secure Remote Password)</strong> protocol. This is worth understanding, not for the exam, but because it illustrates a core principle: you should never design a system where the password travels over the network &#8212; even encrypted.</p><p>SRP is a <strong>zero-knowledge proof</strong> of password knowledge. Both the client (your browser) and the server (Cognito) go through a mathematical exchange of derived values. At the end, both sides have confirmed they know the same password, without either side transmitting the password itself. If an attacker records every packet of the exchange, they learn nothing that helps them.</p><p>Contrast this with <code>USER_PASSWORD_AUTH</code> &#8212; an alternative Cognito flow where the client sends the password directly to Cognito over TLS. The password is encrypted in transit, but it still travels. SRP is strictly safer.</p><blockquote><p><strong>Exam Tip:</strong> The exam does not test SRP math. It tests that you know Cognito supports multiple auth flows. <code>USER_SRP_AUTH</code> is the secure default. Enabling <code>ALLOW_USER_PASSWORD_AUTH</code> on a public-facing app client is a security red flag unless you have a specific reason.</p></blockquote><h3><strong>IAM Roles &#8212; How Services Call Services</strong></h3><p>An <strong>IAM (Identity and Access Management) Role</strong> is an AWS identity that can be <em>assumed</em> by a service or resource &#8212; not a human. It has no password and no static access keys. Instead, when a Lambda function assumes its execution role, AWS&#8217;s <strong>STS (Security Token Service)</strong> &#8212; remember, STS stands for Security Token Service &#8212; issues it temporary credentials (access key, secret key, session token) that expire automatically, usually within an hour.</p><p>The Petshots API Lambda has an IAM execution role that grants it precisely:</p><ul><li><p>Read and write objects in the <code>petshots-uploads</code> S3 bucket</p></li><li><p>Read the Turnstile secret from Secrets Manager</p></li><li><p>Send email via SES</p></li></ul><p>Nothing else. Not the ability to read other buckets. Not the ability to access EC2. Not the ability to modify IAM policies. This is the <strong>principle of least privilege</strong> &#8212; a security best practice that says: grant exactly the permissions needed for the task, nothing more.</p><p>From a business risk perspective, least privilege limits your blast radius. If the Lambda&#8217;s credentials are somehow compromised (through a vulnerability in your code, a dependency, or an infrastructure misconfiguration), the attacker can only do what the role allows. They cannot pivot to other services or data.</p><blockquote><p><strong>Exam Tip:</strong> On the exam, if a Lambda or EC2 instance needs to access another AWS service, the answer is always to attach an <strong>IAM Role</strong> &#8212; never to embed access keys in code or environment variables. Roles use STS (Security Token Service) under the hood to issue auto-rotating temporary credentials. Static embedded keys are a security anti-pattern and the wrong answer.</p></blockquote><h3><strong>Secrets Manager &#8212; Protecting API Keys</strong></h3><p>The Cloudflare Turnstile secret key (used to verify CAPTCHA tokens at signup) is stored in <strong>AWS Secrets Manager</strong>, not in environment variables or source code. The Lambda fetches it at runtime via its IAM role.</p><p>There is an important implementation detail worth noting: Secrets Manager secret ARNs (Amazon Resource Names &#8212; the unique identifier for any AWS resource) come in two forms. The &#8220;friendly&#8221; form omits the random suffix at the end (like <code>petshots/turnstile-secret</code>). The full ARN includes a six-character random suffix (<code>petshots/turnstile-secret-RS5ju3</code>).</p><p>If you use the CDK method <code>Secret.fromSecretNameV2()</code> and then call <code>grantRead()</code>, CDK generates an IAM policy using the partial ARN with a wildcard. But the Lambda&#8217;s actual API call uses the full ARN &#8212; and the IAM policy&#8217;s wildcard pattern does not match. The result: <code>AccessDenied</code> at runtime, even though you think you granted access. The fix: use <code>fromSecretCompleteArn()</code> with the full ARN.</p><p>I mention this not to embarrass anyone &#8212; I made exactly this mistake &#8212; but because debugging IAM permission errors is a rite of passage for anyone building on AWS, and understanding why it failed teaches you more than having it work the first time.</p><blockquote><p><strong>Exam Tip &#8212; Secrets Manager vs Parameter Store:</strong></p><p>Secrets ManagerParameter Store (SSM)Cost~$0.40/secret/monthFree (Standard tier)Automatic rotationYes (built-in for RDS, Redshift, etc.)NoEncryptionAlways encryptedOptional (SecureString uses KMS)Best forAPI keys, DB credentials, secrets requiring rotationConfig values, feature flags, non-sensitive config</p><p>When the exam mentions <strong>automatic credential rotation</strong> for an RDS database, the answer is Secrets Manager. For non-sensitive configuration (feature flags, URLs, settings), Parameter Store is free and sufficient.</p></blockquote><h3><strong>Cognito PreSignUp Lambda &#8212; Custom Verification Logic</strong></h3><p>At signup, before Cognito creates the user account, it fires a <strong>Lambda trigger</strong> called <code>PreSignUp</code>. Our trigger calls the Cloudflare Turnstile API to verify that the token from the browser widget is valid. If it is not, the trigger throws an error, Cognito rejects the signup, and no user account is created.</p><p>This is a CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) integration &#8212; it verifies that the signup is coming from a real human using a browser, not from an automated script trying to create thousands of accounts.</p><p>Implementation note: the <code>admin-create-user</code> API (used in smoke tests to create throwaway test users) bypasses the <code>PreSignUp_SignUp</code> trigger because its trigger source is different. This means our automated tests can create users programmatically without needing a valid CAPTCHA token &#8212; the bypass is a deliberate Cognito design choice, not a security hole.</p><p>The two identity flows in this chapter &#8212; the CAPTCHA-gated signup and the SRP login that ends with three tokens in hand &#8212; look like this end to end:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IK-X!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IK-X!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 424w, https://substackcdn.com/image/fetch/$s_!IK-X!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 848w, https://substackcdn.com/image/fetch/$s_!IK-X!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 1272w, https://substackcdn.com/image/fetch/$s_!IK-X!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IK-X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png" width="1456" height="744" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:744,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:194098,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IK-X!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 424w, https://substackcdn.com/image/fetch/$s_!IK-X!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 848w, https://substackcdn.com/image/fetch/$s_!IK-X!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 1272w, https://substackcdn.com/image/fetch/$s_!IK-X!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24b4b6ad-48a7-40eb-8a62-50d9d74d87f3_1840x940.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2><strong>The Serverless API</strong></h2><h3><strong>The Architectural Decision: EC2 vs Serverless</strong></h3><p>When I started planning the backend, I had two viable options:</p><p><strong>Option A &#8212; Three-tier EC2</strong>: A traditional architecture with an ALB (Application Load Balancer) routing traffic to EC2 instances running a Node.js server, backed by a MySQL database on Aurora. This is the pattern most enterprise applications use. It is familiar, debuggable, and highly flexible. It also costs approximately $22&#8211;30/month minimum even when zero users are active &#8212; the EC2 instances and the ALB have hourly charges regardless of whether anyone is using the app.</p><p><strong>Option B &#8212; Serverless</strong>: API Gateway routing requests to a Lambda function, with S3 for both file storage and metadata storage. Cost is essentially $0 when idle &#8212; Lambda charges only for the milliseconds your code actually runs. API Gateway charges per request. S3 charges per GB stored and per request.</p><p>The business decision was straightforward: a pre-revenue startup with zero guaranteed traffic has no business paying for 24/7 compute capacity. The serverless architecture scales from zero to millions of requests with no re-architecture, and the bill at zero usage is literally pennies.</p><p>I kept the EC2 stack in the codebase (<code>AppStack</code>) as a study artifact for SAA-C03 exam preparation, but it is destroyed (not deployed) in production.</p><h3><strong>API Gateway HTTP API</strong></h3><p><strong>API Gateway</strong> is AWS&#8217;s managed service for creating, securing, and routing HTTP API calls. It sits in front of the Lambda and handles:</p><ul><li><p>Receiving HTTP requests from the internet</p></li><li><p>Running the JWT authorizer to verify the caller&#8217;s identity</p></li><li><p>Routing the request to the appropriate Lambda handler based on method + path</p></li><li><p>Returning the Lambda&#8217;s response to the caller</p></li></ul><p>There are two modern API Gateway flavors:</p><p>REST APIHTTP APIPrice$3.50/million requests$1.00/million requestsFeaturesMore (usage plans, request validation, API keys, caching)Less, but sufficient for mostLatency overhead~6ms~1msJWT/OAuth2 authorizersVia Lambda authorizer (extra cost)Native (no extra cost)</p><p>We use the <strong>HTTP API</strong> (the newer, cheaper flavor). It natively supports Cognito JWT authorization &#8212; no additional Lambda authorizer needed &#8212; and costs 70% less per request. For a startup at negligible traffic, the absolute dollar difference is tiny, but choosing the right service for the scenario is exactly what the SAA-C03 exam tests.</p><blockquote><p><strong>Exam Tip:</strong> REST API is the right answer when the question mentions <strong>usage plans, API keys, per-customer rate limiting, or request body validation</strong>. HTTP API is the right answer when the question mentions <strong>lower cost, JWT or OAuth2 authorizers, or WebSocket needs with fewer features</strong>. WebSocket APIs (real-time bidirectional communication, like a chat app) are a third separate flavor.</p></blockquote><h3><strong>Lambda &#8212; Event-Driven, Pay-Per-Millisecond Compute</strong></h3><p><strong>Lambda</strong> is AWS&#8217;s serverless compute service. You write a function. AWS manages every layer below it: the hardware, the operating system, the runtime, patching, scaling. You pay only for the time your code actually runs, billed in 1ms increments.</p><p>Key configuration decisions for Petshots:</p><p><strong>Runtime</strong>: Node.js 20 &#8212; a modern JavaScript runtime, well-suited for I/O heavy workloads like ours (most time is spent waiting for S3 responses, not computing).</p><p><strong>Architecture</strong>: ARM64 (also called Graviton3). AWS&#8217;s custom ARM-based chip is approximately 20% cheaper per millisecond than equivalent x86 (Intel/AMD) Lambda functions, with similar or better performance for most workloads. This is a pure cost optimization &#8212; same code, same behavior, lower bill.</p><p><strong>Memory</strong>: configured at 256 MB. Lambda&#8217;s CPU allocation scales proportionally to memory. At 256 MB, our function has enough CPU to handle presigned URL generation and S3 metadata operations efficiently without paying for memory we do not need.</p><p><strong>Implementation detail</strong>: The single Lambda function handles all API routes through a simple <code>if</code>/<code>else if</code> router on the <code>event.routeKey</code> field (the method + path combination API Gateway injects). This is called a <strong>monolithic Lambda</strong> or <strong>fat Lambda</strong> approach &#8212; one function handles everything, rather than one function per route. The trade-off: simpler deployment and lower cold start overhead vs slightly messier code organization as the route count grows.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;a2d7467c-a6aa-447b-acb0-ce9947ae72d2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">// Simplified from infra/lambda/api/index.ts
const routeKey = event.routeKey; // e.g. "GET /pets" or "POST /pets/{petId}/docs/upload-url"

if (routeKey === 'GET /pets') {
  return handleListPets(event, userSub);
} else if (routeKey === 'POST /pets') {
  return handleCreatePet(event, userSub);
} else if (routeKey.startsWith('POST /pets/') &amp;&amp; routeKey.endsWith('/docs/upload-url')) {
  return handleDocUploadUrl(event, userSub, petId);
}
// ... and so on
</code></pre></div><blockquote><p><strong>Exam Tip &#8212; Lambda limits that the exam tests:</strong></p><ul><li><p>Maximum execution timeout: <strong>15 minutes</strong>. If a process needs to run longer, use Step Functions, ECS (Elastic Container Service), or Batch.</p></li><li><p>Maximum synchronous response payload: <strong>6 MB</strong>. This is why file transfers go through presigned S3 URLs &#8212; the file never passes through Lambda.</p></li><li><p>Maximum deployment package: 50 MB zipped, 250 MB unzipped (10 GB for container images).</p></li><li><p>Default concurrent execution limit: <strong>1,000 per region</strong> (can be increased via a service quota request).</p></li></ul></blockquote><h3><strong>Cold Starts &#8212; The Serverless Latency Trade-off</strong></h3><p>A <strong>cold start</strong> happens when Lambda has no pre-warmed execution environment available. Lambda must provision a new environment: download your code, start the Node.js runtime, and run your initialization code (imports, global variable assignments, etc.) before handling the first request. For Node.js on ARM64, this typically adds 100&#8211;500 ms of latency to the first request.</p><p>Subsequent requests to the same warm environment incur no cold start &#8212; they run at full speed. Cold starts become a problem when:</p><ol><li><p>Traffic is very spiky (suddenly many simultaneous requests, all cold)</p></li><li><p>Latency requirements are strict (e.g., sub-100ms SLA at all times)</p></li></ol><p>For Petshots, cold starts are acceptable. Uploading a vaccine document is not a real-time operation &#8212; a half-second on the first API call after a period of inactivity is imperceptible in context.</p><blockquote><p><strong>Exam Tip:</strong> If a scenario describes &#8220;inconsistent Lambda performance&#8221; or &#8220;the first request after idle is slow,&#8221; the answer is cold starts. The fix options are: <strong>Provisioned Concurrency</strong> (Lambda keeps N environments pre-warmed, at an additional cost) or a container-based compute solution (ECS, EKS &#8212; Elastic Kubernetes Service). The exam expects you to recognize the trade-off, not to tune Lambda configuration.</p></blockquote><h3><strong>S3 as a Database &#8212; A Non-Traditional Choice</strong></h3><p>Petshots stores all metadata in S3, not in Aurora. Let me explain this decision honestly, because it is an unusual architectural choice and it has real limits.</p><p>Our data model:</p><pre><code><code>users/{userId}/pets/{petId}/pet.json          &#8592; pet name, species, passport token
users/{userId}/pets/{petId}/avatar            &#8592; photo (binary, variable extension)
users/{userId}/pets/{petId}/docs/{docId}/     &#8592; doc folder
    {encoded_label_and_expiry}/{filename}     &#8592; label + expiry in the S3 key!
</code></code></pre><p>The label and expiry date for each document are encoded directly in the S3 object key as a URL-encoded JSON string. This is not database normalization &#8212; it is a deliberate hack that eliminates the need for a database entirely.</p><p><strong>When S3-as-database works:</strong></p><ul><li><p>All access is by known, deterministic key (you always know the user&#8217;s <code>sub</code> and the <code>petId</code>). No scanning required.</p></li><li><p>No relationships or joins. Each pet&#8217;s data is self-contained.</p></li><li><p>User count is small enough that S3&#8217;s <code>ListObjectsV2</code> API (which lists objects under a prefix) completes in milliseconds.</p></li><li><p>Cost is paramount &#8212; Aurora costs at minimum a few dollars/month in storage and ACU charges even at idle, while S3 costs essentially nothing at this scale.</p></li></ul><p><strong>When it breaks down (the honest trade-off):</strong></p><ul><li><p>Cross-user queries become expensive and slow. The Reminder Lambda sends vaccine expiry emails by listing all user prefixes and reading every user&#8217;s settings file. At 100 users this takes a second. At 100,000 users, this is a real problem.</p></li><li><p>No atomic multi-document transactions. If you need &#8220;update two records or neither,&#8221; S3 cannot guarantee that.</p></li><li><p>No indexing. Finding &#8220;all pets with Rabies expiring this month&#8221; requires reading every record.</p></li></ul><p>The right migration path, when user count grows enough to make these matters urgent, is to introduce a DynamoDB table for metadata and keep S3 for the actual files. But that migration is a V2 problem &#8212; premature database introduction is its own kind of tech debt.</p><blockquote><p><strong>Exam Tip:</strong> S3 is excellent for large object storage, data lakes, and file serving. It is almost never the right exam answer for <strong>relational data, transactional workloads, or flexible query patterns</strong>. When the exam describes &#8220;lookup by known key with no relationships,&#8221; think <strong>DynamoDB</strong>. When the exam describes &#8220;SQL, joins, ACID transactions,&#8221; think <strong>RDS Aurora</strong>.</p></blockquote><h3><strong>Presigned URLs &#8212; Delegated Access Without Exposing Credentials</strong></h3><p>A <strong>presigned URL</strong> is one of the most elegant mechanisms in AWS. Here is the problem it solves: you have a private S3 bucket. A user needs to upload a file to it. You do not want to give the user AWS credentials (that would grant them access to everything the credential allows, not just this one upload). You also do not want the file to pass through your Lambda (that would violate the 6 MB Lambda payload limit and add unnecessary latency for large files).</p><p>The solution: your Lambda &#8212; which has IAM permission to write to the bucket &#8212; generates a pre-signed upload policy and returns it to the browser. The browser then uploads the file directly to S3 using that policy. S3 validates the signature and enforces the constraints embedded in the policy. The file never touches Lambda.</p><p>We use <strong>presigned POST policies</strong> (not presigned PUT URLs) for uploads because POST policies allow additional constraints that PUT does not:</p><ul><li><p><strong>content-length-range</strong>: S3 will reject the upload if the file is smaller than <code>minBytes</code> or larger than <code>maxBytes</code>. We use <code>0</code> to <code>20971520</code> bytes (20 MB). This is enforced server-side by S3 &#8212; the browser cannot bypass it.</p></li><li><p><strong>content-type</strong>: only <code>image/jpeg</code>, <code>image/png</code>, <code>image/webp</code> for avatars; any content type for documents.</p></li><li><p><strong>key prefix</strong>: the user can only upload to their own prefix (<code>users/{their-sub}/...</code>), not to arbitrary keys.</p></li></ul><p>For downloads (viewing a saved document), we generate <strong>presigned GET URLs</strong> valid for 60 minutes. After 60 minutes, the URL expires and the document is inaccessible without generating a new URL &#8212; providing time-limited access without making the bucket public.</p><blockquote><p><strong>Exam Tip &#8212; Presigned URL use cases:</strong></p><ul><li><p>&#8220;Allow browser upload to S3 without exposing credentials&#8221; &#8594; presigned URL (POST policy)</p></li><li><p>&#8220;Share a private S3 file with a partner for 24 hours&#8221; &#8594; presigned GET URL with custom expiry</p></li><li><p>&#8220;S3 bucket must stay private but CloudFront needs to read from it&#8221; &#8594; this is OAC (Origin Access Control), NOT presigned URLs. Know the difference: presigned URLs are user-facing delegations with an expiry; OAC is a service-to-service trust relationship.</p></li></ul></blockquote><h3><strong>Authorization: Per-User Data Isolation</strong></h3><p>Every API call goes through the JWT (JSON Web Token) authorizer in API Gateway. After the token is verified, the <code>sub</code> claim (the user&#8217;s unique identifier in Cognito) is injected into the Lambda event context.</p><p>Every S3 operation in the Lambda is scoped to that user&#8217;s prefix: <code>users/{sub}/...</code>. A user cannot read, write, or delete data under any other user&#8217;s prefix &#8212; not because the API checks a permission table, but because the S3 path itself is constructed from the verified <code>sub</code>. Even if a user crafted a malicious API request with someone else&#8217;s <code>petId</code>, the Lambda would look for the object under their own <code>sub</code>, which does not exist.</p><p>This is the simplest form of authorization: <strong>the identity IS the access key</strong>.</p><div><hr></div><h2><strong>The Frontend</strong></h2><h3><strong>What a SPA Is and Why We Chose It</strong></h3><p>The Petshots frontend is a <strong>SPA (Single-Page Application)</strong>. A SPA delivers a single HTML file to the browser; the JavaScript in that file handles all navigation, rendering, and user interaction without ever reloading the page. When you tap &#8220;Dashboard,&#8221; React (our UI library) updates the DOM &#8212; the browser&#8217;s in-memory representation of the page &#8212; without asking the server for a new page.</p><p>This is different from traditional <strong>server-rendered applications</strong> where every navigation triggers a round-trip to the server for a new HTML page. SPAs feel faster and more app-like; server-rendered apps are better for SEO (Search Engine Optimization &#8212; making pages findable by Google) because the HTML is fully formed when delivered. For a product like Petshots that is used after login, SEO is irrelevant &#8212; users reach the dashboard directly. SPA is the right choice.</p><p>The SPA is built with <strong>Vite</strong> (a build tool that compiles TypeScript and React into optimized JavaScript bundles), <strong>React 19</strong> (the UI library), and <strong>TypeScript</strong> (JavaScript with static typing, which catches bugs at compile time rather than at runtime). The compiled output is a handful of files: one HTML, one CSS, and a few JavaScript bundles &#8212; everything needed to run the entire application in a browser.</p><h3><strong>Hosting: Private S3 Bucket Behind CloudFront</strong></h3><p>The compiled SPA files are stored in a private S3 (Simple Storage Service) bucket named <code>petshots-frontend</code>. The bucket has no public access &#8212; there is no URL you can use to browse it directly. Instead, a CloudFront distribution sits in front of it and serves as the only access point.</p><p>This matters for two reasons:</p><ol><li><p><strong>Security</strong>: S3 buckets with public access enabled are a well-known source of data breaches. Keeping the bucket private, even for non-sensitive static files, is the right default.</p></li><li><p><strong>Performance</strong>: CloudFront has 400+ <strong>edge locations</strong> (points of presence in cities around the world). When a user in Tokyo requests <code>petshots.app</code>, CloudFront serves the cached files from the nearest edge location in Tokyo &#8212; not from our S3 bucket in <code>us-east-1</code> (Northern Virginia). The result: low latency globally without running servers in every region.</p></li></ol><h3><strong>OAC &#8212; How CloudFront Accesses a Private Bucket</strong></h3><p>If the S3 bucket is private, how does CloudFront read from it? Through <strong>OAC (Origin Access Control)</strong> &#8212; the modern mechanism for CloudFront-to-S3 trust.</p><p>OAC works at the IAM level: CloudFront signs its requests to S3 using SigV4 (AWS&#8217;s request signing protocol). The S3 bucket policy grants <code>s3:GetObject</code> permission to the CloudFront service principal. Only requests signed by your specific CloudFront distribution are allowed.</p><p>The predecessor, <strong>OAI (Origin Access Identity)</strong>, worked similarly but used a legacy Cognito-like CloudFront &#8220;user&#8221; instead of IAM signing. OAI is deprecated. New deployments should use OAC.</p><blockquote><p><strong>Exam Tip:</strong> The concept is the same for OAC and OAI: keep S3 private, let only CloudFront read it. On newer exam questions, OAC is the preferred answer. Both achieve the same result: S3 bucket is private, no presigned URLs needed for serving the SPA, CloudFront handles all access.</p></blockquote><h3><strong>How a CDN Works &#8212; Cache Hits and Misses</strong></h3><p>A <strong>CDN (Content Delivery Network)</strong> like CloudFront caches content at edge locations near users. Here is the request flow:</p><ol><li><p>User requests <code>https://petshots.app/index.js</code></p></li><li><p>DNS (Domain Name System) resolves <code>petshots.app</code> to the nearest CloudFront edge location. (Route 53 uses latency-based routing to pick the closest edge.)</p></li><li><p><strong>Cache hit</strong>: CloudFront has <code>index.js</code> cached &#8594; serve it immediately from the edge. Sub-millisecond.</p></li><li><p><strong>Cache miss</strong>: CloudFront does not have it &#8594; fetch from origin (our S3 bucket in us-east-1) &#8594; cache it &#8594; serve it. One extra round trip.</p></li></ol><p>Once cached at the edge, static files (JS bundles, CSS, images) are served with zero round trips to the origin. This is free performance.</p><p><strong>Cache invalidation</strong> is necessary when you deploy a new version of the app: old cached files at edge locations need to be evicted. We handle this two ways:</p><ol><li><p>Vite appends a content hash to every filename: <code>index-Df9iRGpu.js</code>. A new deploy produces <code>index-Kq3mNpXt.js</code>. Old and new can coexist in cache; they have different URLs. No invalidation needed for the bundles.</p></li><li><p><code>index.html</code> (which references the bundles) does need explicit invalidation. We run <code>aws cloudfront create-invalidation --paths '/*'</code> after each deploy.</p></li></ol><p>The implementation steps for a frontend deploy:</p><pre><code><code>cd frontend
npm run build            # Vite compiles TypeScript + React &#8594; dist/
aws s3 sync dist/ s3://petshots-frontend --delete   # Upload to S3
aws cloudfront create-invalidation \
  --distribution-id E132NGTOIUI26J \
  --paths '/*'           # Evict stale cache entries
</code></code></pre><blockquote><p><strong>Exam Tip:</strong> CloudFront cache invalidation costs: free for the first 1,000 paths per month, then $0.005 per path. If you use content-hashed filenames (Vite does this automatically), only <code>index.html</code> needs invalidation. A common exam scenario: &#8220;a deployed update is not showing for users&#8221; &#8212; the answer is usually &#8220;create a CloudFront invalidation.&#8221;</p></blockquote><h3><strong>ACM &#8212; Free TLS Certificates</strong></h3><p><strong>ACM (AWS Certificate Manager)</strong> provisions and automatically renews TLS (Transport Layer Security) certificates at no cost for use on CloudFront distributions, ALBs (Application Load Balancers), and API Gateway.</p><p>A TLS certificate is the cryptographic foundation of HTTPS &#8212; it proves the server you are talking to is really <code>petshots.app</code> (not an impersonator), and it encrypts all traffic between the browser and CloudFront.</p><p>Critical implementation detail: <strong>ACM certificates for CloudFront must be created in the </strong><code>us-east-1</code><strong> region</strong>, regardless of where your users or resources are located. CloudFront is a global service that pulls its configurations from <code>us-east-1</code>. If you create the certificate in <code>us-west-2</code> and try to attach it to CloudFront, it will not appear in the selection list. This is one of the most common mistakes candidates make when first setting up CloudFront. I made it.</p><blockquote><p><strong>Exam Tip:</strong> ACM + CloudFront &#8594; certificate must be in <code>us-east-1</code>. ACM + ALB &#8594; certificate must be in the same region as the ALB. Validation methods: <strong>DNS validation</strong> (add a CNAME to your hosted zone; ACM checks it periodically; renewal is automatic) is preferred for production. <strong>Email validation</strong> requires manual action at renewal. We use DNS validation with Route 53, which can add the CNAME automatically.</p></blockquote><h3><strong>Route 53 &#8212; DNS and the Apex Record Problem</strong></h3><p><strong>Route 53</strong> is AWS&#8217;s managed DNS service. &#8220;DNS&#8221; stands for Domain Name System &#8212; the global directory that translates human-readable domain names (<code>petshots.app</code>) to IP addresses that routers can use.</p><p>Route 53 manages a <strong>hosted zone</strong> for <code>petshots.app</code> &#8212; the authoritative source of truth for all DNS records under that domain.</p><p>Here is a problem that trips up many developers: the <strong>apex domain</strong> (the bare domain with no subdomain &#8212; <code>petshots.app</code> rather than <code>www.petshots.app</code>) cannot use a CNAME record. This is a fundamental DNS specification constraint, not an AWS limitation. A CNAME maps one hostname to another. But at the apex, DNS does not allow a CNAME (because the apex must also hold SOA and NS records, and CNAME records cannot coexist with other record types).</p><p>The solution: Route 53&#8217;s proprietary <strong>Alias record</strong>. An Alias record looks like an A record (IPv4 address) to the outside world but is resolved dynamically by Route 53 to the current IP addresses of the target AWS resource (CloudFront, ALB, etc.). Alias records:</p><ul><li><p>Work at the zone apex</p></li><li><p>Do not incur additional query charges (unlike CNAME resolution chains)</p></li><li><p>Automatically track IP changes of the target resource</p></li></ul><p>We have alias records for both <code>petshots.app</code> and <code>www.petshots.app</code> pointing to the CloudFront distribution.</p><blockquote><p><strong>Exam Tip:</strong> &#8220;Which DNS record type can be used at the zone apex?&#8221; &#8594; <strong>Alias record</strong> (not CNAME). &#8220;What is the difference between Alias and CNAME?&#8221; &#8594; Alias is Route 53-specific, works at apex, free; CNAME is standard, forbidden at apex, can point to any hostname.</p></blockquote><h3><strong>SPA Routing &#8212; The Custom Error Response Trick</strong></h3><p>There is one more tricky detail with SPAs on CloudFront that any architect needs to know. When a user bookmarks <code>https://petshots.app/dashboard</code> and opens it cold, here is what happens:</p><ol><li><p>CloudFront receives a request for <code>/dashboard</code></p></li><li><p>CloudFront looks for a file named <code>dashboard</code> in S3</p></li><li><p>That file does not exist &#8212; S3 returns 403 (access denied, because the bucket is private and there is no such key) or 404 (not found)</p></li><li><p>Without intervention, CloudFront returns the error to the user</p></li></ol><p>React Router never gets a chance to run, because React never loaded.</p><p>The fix: configure a <strong>CloudFront custom error response</strong>. We instruct CloudFront to intercept 403 and 404 responses from the origin and instead serve <code>/index.html</code> with HTTP status 200. React loads, React Router reads the URL (<code>/dashboard</code>), and renders the correct page.</p><p>Without this configuration, every direct URL navigation to any route other than the root will show an error to the user.</p><div><hr></div><h2><strong>Email and Background Jobs</strong></h2><h3><strong>SES &#8212; Transactional Email</strong></h3><p><strong>SES (Simple Email Service)</strong> is AWS&#8217;s managed email sending infrastructure. We use it for two purposes:</p><ol><li><p><strong>Cognito transactional email</strong> &#8212; when a user signs up, Cognito sends an email verification code. We have configured Cognito to send this through our verified SES domain identity (<code>petshots.app</code>), so the email comes from <code>no-reply@petshots.app</code> rather than Amazon&#8217;s generic verification address. This improves deliverability and looks more professional.</p></li><li><p><strong>Vaccine expiry reminders</strong> &#8212; the Reminder Lambda sends a daily digest of expiring documents to users who have enabled email notifications.</p></li></ol><p>SES starts every new account in <strong>sandbox mode</strong>: you can only send to email addresses you have explicitly verified, and you are capped at 200 emails per day. This is a fraud containment measure &#8212; before AWS knows who you are, they limit your ability to send spam at scale.</p><p>To exit sandbox mode, you request production access by explaining your use case, your sending volume, and how you handle bounces and complaints. After AWS approves the request (typically within 24 hours), you can send to any recipient up to your approved daily limit.</p><p>For Petshots, we submitted the request after deploying the CAPTCHA &#8212; AWS wants to see that you have abuse controls before they open up sending volume. We received approval for 50,000 emails per day.</p><p><strong>DKIM (DomainKeys Identified Mail)</strong> is an email authentication standard that works by adding a cryptographic signature to outgoing emails, verifiable via a DNS TXT record. When we receive an email from <code>no-reply@petshots.app</code>, a mail server can look up the DKIM public key in DNS and verify the signature. This proves the email actually came from us and has not been tampered with. DKIM significantly improves deliverability &#8212; without it, major email providers may mark your messages as spam.</p><blockquote><p><strong>Exam Tip:</strong> If a scenario describes &#8220;SES emails not being delivered to external users,&#8221; the most likely cause is that the account is still in <strong>sandbox mode</strong>. The fix is to request production access. DKIM and SPF records improve deliverability but do not remove sandbox restrictions &#8212; those are separate concerns.</p></blockquote><h3><strong>EventBridge &#8212; The Cron Scheduler</strong></h3><p><strong>EventBridge</strong> is AWS&#8217;s managed event bus. For our purposes, the most relevant feature is its <strong>scheduled rules</strong> capability: you define a cron expression, and EventBridge fires a target (our Lambda) on that schedule.</p><p>Our reminder rule fires at 9:00 AM UTC every day:</p><pre><code><code>cron(0 9 * * ? *)
</code></code></pre><p>AWS cron syntax has six fields (unlike standard Unix cron which has five):</p><pre><code><code>cron(minutes hours day-of-month month day-of-week year)
</code></code></pre><p>The <code>?</code> in the day-of-week position means &#8220;no specific value&#8221; &#8212; we do not care which day of the week it is, we just want it to run every day. AWS requires that you specify either <code>day-of-month</code> or <code>day-of-week</code> as <code>?</code> when you specify the other &#8212; you cannot specify both simultaneously.</p><blockquote><p><strong>Exam Tip:</strong> The <code>?</code> character in AWS cron is specific to EventBridge and does not exist in standard Unix cron &#8212; this is a common source of confusion. The six-field format (including the year) also differs from Unix&#8217;s five fields. When day-of-month is specified, day-of-week must be <code>?</code>, and vice versa.</p></blockquote><h3><strong>The Reminder Lambda &#8212; Fan-Out at Small Scale</strong></h3><p>The Reminder Lambda demonstrates a common serverless data-processing pattern. When EventBridge fires it:</p><ol><li><p>Call <code>S3.listObjectsV2</code> on the <code>users/</code> prefix to discover all user prefixes &#8212; one per user in the system.</p></li><li><p>For each user: read their <code>settings.json</code>, check if reminders are enabled.</p></li><li><p>If enabled: read each pet&#8217;s documents, compute which ones expire within the user&#8217;s configured windows (7, 30, 60 days, etc.).</p></li><li><p>If any documents are expiring: send one consolidated email via SES with all the expiring items listed.</p></li></ol><p>This is called a <strong>fan-out</strong> pattern: one event triggers processing across many records. Each user&#8217;s processing is independent &#8212; a failure for one user is caught and logged, but does not stop processing for other users.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NP9a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NP9a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 424w, https://substackcdn.com/image/fetch/$s_!NP9a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 848w, https://substackcdn.com/image/fetch/$s_!NP9a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 1272w, https://substackcdn.com/image/fetch/$s_!NP9a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NP9a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png" width="1456" height="522" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:522,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:119535,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NP9a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 424w, https://substackcdn.com/image/fetch/$s_!NP9a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 848w, https://substackcdn.com/image/fetch/$s_!NP9a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 1272w, https://substackcdn.com/image/fetch/$s_!NP9a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef22224f-2180-4eed-b63d-aa9674584f75_1840x660.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>At small scale (hundreds of users), doing this sequentially in one Lambda execution works fine. At large scale (hundreds of thousands of users), the Lambda would time out (remember: 15-minute maximum). The architectural migration: use <strong>SQS (Simple Queue Service)</strong> &#8212; remember, SQS stands for Simple Queue Service &#8212; to enqueue one message per user, then process messages concurrently with many Lambda invocations. This is a core SAA-C03 pattern: <strong>EventBridge &#8594; SQS &#8594; Lambda</strong> for scalable, reliable fan-out processing.</p><div><hr></div><h2><strong>Infrastructure as Code</strong></h2><h3><strong>Why Not Click? The Case Against ClickOps</strong></h3><p>When I started with AWS, I clicked everything in the console. It felt productive. You can see exactly what you are doing. But after a few sessions of building, I hit a wall: I could not reliably remember every setting I had configured, I could not reproduce the environment from scratch, and one wrong click could delete a resource I spent an hour configuring.</p><p><strong>ClickOps</strong> &#8212; managing infrastructure through the console &#8212; has fundamental business risks:</p><ul><li><p><strong>No audit trail</strong>: unless CloudTrail is capturing every action, there is no record of who changed what and when.</p></li><li><p><strong>No reproducibility</strong>: &#8220;can you spin up an identical staging environment?&#8221; is a nightmare question for a ClickOps shop.</p></li><li><p><strong>Configuration drift</strong>: dev and prod inevitably diverge, because the same set of clicks is never made exactly the same way twice.</p></li><li><p><strong>No peer review</strong>: code changes go through pull requests. Console clicks do not.</p></li></ul><p><strong>IaC (Infrastructure as Code)</strong> solves all of these. You describe your desired infrastructure in code, commit it to git, review changes as diffs, and let the tooling apply it consistently every time.</p><h3><strong>CDK &#8212; TypeScript That Compiles to Infrastructure</strong></h3><p><strong>CDK (Cloud Development Kit)</strong> is AWS&#8217;s IaC framework that lets you write infrastructure definitions in real programming languages: TypeScript, Python, Java, Go. CDK <strong>constructs</strong> are composable building blocks &#8212; a <code>Vpc</code> construct handles all the subnet creation, route tables, and gateway configuration that would take pages of raw CloudFormation YAML.</p><p>Under the hood, CDK compiles to <strong>CloudFormation</strong> &#8212; AWS&#8217;s foundational IaC service. <code>cdk synth</code> outputs the CloudFormation template; <code>cdk deploy</code> uploads it and tells CloudFormation to apply it.</p><p>The deployment workflow:</p><pre><code><code>cd infra
npx cdk synth          # Compile TypeScript &#8594; CloudFormation templates (review these!)
npx cdk diff           # Show what will change vs what is currently deployed
npx cdk deploy PetshotsApiStack   # Deploy one specific stack
npx cdk deploy --all   # Deploy all stacks (careful &#8212; check the diff first)
</code></code></pre><h3><strong>CloudFormation &#8212; The Deployment Engine</strong></h3><p><strong>CloudFormation</strong> reads your template, compares it to the currently deployed state, computes a <strong>change set</strong> (the diff of what needs to be created, modified, or deleted), and executes it in the correct dependency order.</p><p>Dependency ordering matters enormously. If Stack A exports a VPC ID and Stack B imports it, CloudFormation will:</p><ul><li><p>Deploy Stack A first</p></li><li><p>Refuse to delete Stack A while Stack B imports from it</p></li><li><p>Update Stack A&#8217;s export before touching Stack B&#8217;s import</p></li></ul><p>This prevents the class of bug where you delete a shared resource that something else depends on.</p><p><strong>Stacks</strong> are the unit of deployment. Petshots has five:</p><p>StackWhat it containsCan it be destroyed safely?NetworkStackVPC, subnets, NAT, S3 gateway endpointYes (no user data)AuthStackCognito User PoolNO &#8212; destroys all user accountsDataStackAurora clusterNO &#8212; destroys the databaseApiStackLambda, API Gateway, uploads S3 bucketNO &#8212; destroys uploaded filesFrontendStackCloudFront, SPA S3 bucket, ACM, Route 53Low risk (rebuildable SPA)</p><h3><strong>RemovalPolicy &#8212; Protecting Production Data from Accidents</strong></h3><p>By default, when you <code>cdk destroy</code> a stack, CDK tells CloudFormation to delete all the resources in it. For a Lambda or an EC2 instance, this is correct behavior &#8212; they are ephemeral. For a Cognito User Pool containing user accounts, or an S3 bucket containing users&#8217; vaccine documents, this would be catastrophic.</p><p>Setting <code>RemovalPolicy.RETAIN</code> tells CloudFormation: if this stack is ever destroyed, leave this resource in place as an orphan. The resource continues to exist and incur charges, but it is no longer managed by CloudFormation. You must delete it manually &#8212; which requires a deliberate, audited action.</p><p>For the Aurora cluster, we also set <code>deletionProtection: true</code> at the RDS (Relational Database Service) API level. This means even a direct API call to delete the cluster will fail &#8212; you must first explicitly disable deletion protection, then delete. Two-step protection against accidents.</p><blockquote><p><strong>Exam Tip:</strong> The exam tests RemovalPolicy in scenarios about data protection during stack teardown. RETAIN is the correct answer for stateful resources containing user data. Combined with RDS deletion protection and S3 bucket versioning, this provides multiple layers of protection against accidental data loss &#8212; a core reliability pattern.</p></blockquote><div><hr></div><h2><strong>The Three-Tier Architecture (SAA-C03 Exam Pattern)</strong></h2><p>Even though Petshots went serverless in production, the <strong>AppStack</strong> (ALB &#8212; remember, ALB stands for Application Load Balancer &#8594; ASG &#8212; Auto Scaling Group &#8594; Aurora) stays in the codebase as an SAA-C03 study artifact. The three-tier architecture is the most-tested pattern on the exam. You need to know it well.</p><pre><code><code>Internet
    &#9474;
    &#9660;
ALB (Application Load Balancer)    &#8592; public subnets, two AZs
    &#9474;  HTTP/HTTPS routing + health checks
    &#9660;
ASG (Auto Scaling Group)           &#8592; private (PRIVATE_WITH_EGRESS) subnets
    &#9474;  EC2 instances running the application
    &#9660;
Aurora (RDS)                       &#8592; data (PRIVATE_ISOLATED) subnets
</code></code></pre><h3><strong>ALB &#8212; Layer 7 Load Balancing</strong></h3><p>An <strong>ALB (Application Load Balancer)</strong> operates at Layer 7 of the OSI (Open Systems Interconnection) model &#8212; the HTTP application layer. This means it understands URLs, headers, cookies, and hostnames. It can route differently based on path (<code>/api/*</code> vs <code>/*</code>), hostname (<code>api.petshots.app</code> vs <code>petshots.app</code>), or headers.</p><p>Core ALB capabilities:</p><ul><li><p><strong>Load distribution</strong>: sends traffic to registered targets (EC2 instances) in a round-robin or weighted fashion.</p></li><li><p><strong>Health checks</strong>: periodically probes each instance at a configured path. If an instance fails health checks, the ALB stops sending traffic to it automatically.</p></li><li><p><strong>TLS termination</strong>: the ALB accepts HTTPS from clients, decrypts it, and sends plain HTTP to backend instances. Backend instances do not need to handle TLS.</p></li></ul><blockquote><p><strong>Exam Tip &#8212; ALB vs NLB (Network Load Balancer) vs CLB (Classic Load Balancer):</strong></p><p>ALBNLBCLBOSI Layer7 (HTTP/HTTPS)4 (TCP/UDP)4 and 7 (legacy)Use caseWeb apps, microservices, path/host routingUltra-low latency, static IP, raw TCPLegacy (avoid)Static IPNoYes (Elastic IP)NoWebSocketYesYesNo</p><p>Choose ALB for HTTP workloads (most web applications). Choose NLB when the question mentions <strong>static IP for whitelisting</strong>, <strong>ultra-low latency</strong>, or <strong>raw TCP/UDP</strong> (e.g., game servers, IoT). NLB is also the answer when you need to attach an Elastic IP (a fixed public IP address) to a load balancer &#8212; ALBs have no fixed IP.</p></blockquote><h3><strong>ASG &#8212; Auto Scaling Groups</strong></h3><p>An <strong>ASG (Auto Scaling Group)</strong> manages a fleet of identical EC2 instances, maintaining them within configured bounds:</p><ul><li><p><strong>Minimum</strong>: the floor &#8212; never fewer than this many instances (provides HA).</p></li><li><p><strong>Desired</strong>: the target count under current load.</p></li><li><p><strong>Maximum</strong>: the ceiling &#8212; never more than this many instances (cost control).</p></li></ul><p>Instances are created from a <strong>Launch Template</strong> specifying the AMI (Amazon Machine Image &#8212; the operating system + software snapshot), instance type, Security Group, IAM role, and user-data script (shell commands that run at first boot to configure the instance).</p><p>Scaling policies:</p><p>Policy TypeHow it worksWhen to useManualYou set desired count directlyPredictable, one-time changesScheduledScale to N at time TKnown recurring patterns (Mon morning traffic)Target tracking&#8221;Keep avg CPU at 50%&#8221; &#8212; ASG adjusts automaticallyRecommended defaultStep scalingDifferent adjustments at different thresholdsCustom, step-function response curvesPredictiveML model predicts demand, scales proactivelyHigh-volume apps with consistent patterns</p><blockquote><p><strong>Exam Tip:</strong> Target tracking is the recommended default for most workloads. Scheduled scaling is the answer when the scenario describes known predictable demand spikes (e.g., a media company that knows traffic spikes at 8 PM). Predictive scaling is the answer when historical data is mentioned.</p></blockquote><h3><strong>Aurora Serverless v2 &#8212; The Cost-Optimized Database</strong></h3><p><strong>Aurora Serverless v2</strong> scales compute capacity in <strong>ACU (Aurora Capacity Unit)</strong> increments &#8212; where each Aurora Capacity Unit is approximately 2 GB of RAM with proportional CPU. At <code>min ACU = 0</code> (scale-to-zero), the cluster suspends all compute when idle, charging only for storage. When a query arrives, it resumes in seconds.</p><p>For Petshots at this stage, the Aurora cluster exists in the infrastructure for exam-study purposes and potential future use &#8212; the serverless API stores all data in S3 today. But it is running (in scale-to-zero state) and retained by RemovalPolicy.</p><blockquote><p><strong>Exam Tip &#8212; Aurora vs RDS vs DynamoDB:</strong></p><p>RDS (Provisioned)Aurora Serverless v2DynamoDBModelRelational (SQL)Relational (SQL)NoSQL (key-value / document)ScalingManual + read replicasAutomatic (0 to 128 ACUs)Fully automaticCost modelHourly instance chargePer ACU-secondPer request + storageScale-to-zeroNoYes (min ACU 0)N/A (always on, but no idle charge)Multi-regionManual + read replicasAurora GlobalNative Global Tables</p><p>Choose Aurora Serverless when you need SQL with unpredictable or spiky traffic. Choose DynamoDB when you need single-digit ms latency at any scale, or a key-value / document model. Choose RDS when you have steady, predictable load and want fine-grained instance control.</p></blockquote><div><hr></div><h2><strong>Act 8 &#8212; Cost Architecture (The Business Lens)</strong></h2><p>Cost optimization is one of the six pillars of the AWS <strong>Well-Architected Framework</strong> (Operational Excellence, Security, Reliability, Performance Efficiency, Cost Optimization, and Sustainability &#8212; added in 2021). It is tested throughout the SAA-C03 exam and it is a real business concern for any product.</p><p>Here is the complete Petshots cost model, annotated with the architectural reasoning:</p><p>ComponentMonthly costWhy we made this choiceLambda + API Gateway~$0 idlePay-per-request; zero users = zero costS3 (uploads + frontend)~$0.02~20 GB stored; 23 cents/GB/monthCloudFront~$0 idlePay-per-GB transferredCognitoFreeFirst 50,000 MAUs freeSES~$0 idle$0.10/1,000 emails; we send very fewEventBridge~$0$1/million scheduled invocationsAurora (scale-to-zero)~$0.10Storage only; compute suspendedNAT instance~$3.50t4g.nano, 24/7; the biggest idle costRoute 53 hosted zone$0.50Fixed monthly per zoneSecrets Manager$0.40One secret, fixed monthlyACM$0Free for CloudFront/ALB<strong>Total~$5/month</strong></p><p>Compare this to the equivalent three-tier EC2 architecture: ALB (~$16/month)</p><ul><li><p>EC2 t4g.micro (~$6/month) + Aurora (minimum ACU charges). Even with scale-to-zero Aurora, the ALB alone would add $16/month. That is the financial case for going serverless on a pre-revenue product.</p></li></ul><blockquote><p><strong>Exam Tip &#8212; Well-Architected Framework pillars:</strong></p><ol><li><p><strong>Operational Excellence</strong> &#8212; run and monitor systems; automate changes.</p></li><li><p><strong>Security</strong> &#8212; protect data, systems, and assets.</p></li><li><p><strong>Reliability</strong> &#8212; recover from failures; meet demand.</p></li><li><p><strong>Performance Efficiency</strong> &#8212; use resources efficiently; avoid over-provisioning.</p></li><li><p><strong>Cost Optimization</strong> &#8212; avoid unnecessary cost; right-size resources.</p></li><li><p><strong>Sustainability</strong> &#8212; minimize environmental impact; maximize resource utilization.</p></li></ol><p>Exam questions often reference these pillars explicitly: &#8220;which option is MOST cost-effective while maintaining reliability?&#8221; You are being tested on your ability to balance competing pillars, not to maximize any single one.</p></blockquote><div><hr></div><h2><strong>The Full Request Lifecycle &#8212; Annotated</strong></h2><p>Here is the complete flow for a document upload, annotated with every AWS service involved. Study this walkthrough: the exam frequently describes this type of multi-service flow and asks which service is responsible for which behavior.</p><p>Start with the sequence diagram &#8212; four actors, eight steps. The one thing to burn into memory is step 7: the file travels the green arrow, straight from the browser to S3, touching neither API Gateway nor Lambda:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Eujm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Eujm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 424w, https://substackcdn.com/image/fetch/$s_!Eujm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 848w, https://substackcdn.com/image/fetch/$s_!Eujm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 1272w, https://substackcdn.com/image/fetch/$s_!Eujm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Eujm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png" width="1456" height="1045" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1045,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:177400,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://newsletter.markgingrass.com/i/205073226?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Eujm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 424w, https://substackcdn.com/image/fetch/$s_!Eujm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 848w, https://substackcdn.com/image/fetch/$s_!Eujm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 1272w, https://substackcdn.com/image/fetch/$s_!Eujm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F356aaedd-27cb-46e1-92b2-16899e212bdf_1840x1320.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Now the same flow step by step, with the details that matter:</p><pre><code><code>1. User taps "Add Record" on phone.
   &#8594; Browser: React SPA running in Safari/Chrome.

2. SPA sends: POST /pets/abc123/docs/upload-url
   Header: Authorization: Bearer eyJ... (the Cognito Access Token)
   &#8594; Network: HTTPS to petshots.app &#8594; CloudFront edge location &#8594; API Gateway.

3. API Gateway: receives the HTTP request.
   &#8594; JWT Authorizer downloads Cognito's JWKS (JSON Web Key Set, cached).
   &#8594; Verifies the JWT signature using Cognito's RSA public key.
   &#8594; Checks token expiry (the `exp` claim).
   &#8594; Extracts `sub` (user ID) from the JWT payload.
   &#8594; If valid: routes to Lambda. If invalid: returns 401 Unauthorized.
      (Lambda is not invoked; no cost incurred.)

4. Lambda (Node.js, ARM64) runs.
   &#8594; Reads the `sub` from event.requestContext.authorizer.jwt.claims.sub.
   &#8594; Calls S3 ListObjectsV2 on the user's doc prefix to count existing docs.
   &#8594; If count &gt;= MAX_DOCS: returns 409 Conflict. Upload rejected.
   &#8594; Calls S3 CreatePresignedPost with:
       - Bucket: petshots-uploads
       - Key prefix: users/{sub}/pets/{petId}/docs/{uuid}/
       - content-length-range: 0 to 20971520 bytes
       - Expiry: 5 minutes
   &#8594; Returns { url, fields } to the browser.

5. API Gateway &#8594; Browser: { url, fields } as JSON.

6. Browser: constructs a multipart/form-data POST directly to S3.
   &#8594; Uses the presigned `url` and `fields` from step 5.
   &#8594; Includes the actual file content.
   &#8594; S3 verifies the signature, checks content-length-range,
      checks content-type, accepts the upload.
   &#8594; File is now stored at:
      s3://petshots-uploads/users/{sub}/pets/{petId}/docs/{uuid}/
          {url-encoded JSON label+expiry}/{original filename}
   &#8594; S3 returns 204 No Content.

7. Browser: updates the UI optimistically. No further server calls needed.
</code></code></pre><p>On the exam:</p><ul><li><p>&#8220;How do browsers upload files to S3 without AWS credentials?&#8221; &#8594; <strong>Presigned POST policy</strong></p></li><li><p>&#8220;Which service validates user identity before Lambda runs?&#8221; &#8594; <strong>API Gateway JWT Authorizer</strong></p></li><li><p>&#8220;How does Lambda call S3 without hardcoded credentials?&#8221; &#8594; <strong>IAM execution role</strong> (STS-issued temporary credentials)</p></li><li><p>&#8220;Why did we design the upload to bypass Lambda for the file transfer?&#8221; &#8594; <strong>Lambda&#8217;s 6 MB payload limit</strong></p></li></ul><div><hr></div><h2><strong>Afterword &#8212; What Building This Taught Me</strong></h2><p>I started this project to solve a real problem. I ended it with a production SaaS application that has handled real signups, stored real vaccine documents, and sent real email reminders.</p><p>But the thing I did not expect was how much the process of building it aligned with the SAA-C03 certification path. Every decision I made &#8212; NAT instance vs NAT Gateway, HTTP API vs REST API, S3 for metadata vs Aurora, RemovalPolicy RETAIN vs DESTROY &#8212; is a variation on a question the exam asks. The difference is that when you make these decisions with real money and real users at stake, the reasoning becomes visceral rather than academic.</p><p>When you read in an exam question &#8220;a startup needs to minimize idle costs while maintaining the ability to scale,&#8221; you now know exactly what that means. You have felt the difference between a $0/month Lambda and a $22/month EC2 tier. You have debugged a Secrets Manager ARN mismatch at 11 PM. You have pushed a frontend build and forgotten to run the CloudFront invalidation, and then wondered for twenty minutes why the update was not showing up.</p><p>That is the kind of knowledge that sticks.</p><p>The app is live at </p><p><a href="https://petshots.app">https://petshots.app</a></p><p>. The code is on GitHub as <code>petshots</code>. If you are working through the SAA-C03 and you want to see how these services wire together in production, go look at the source.</p><p>And if you have a dog who needs his shots &#8212; that is what we built it for.</p><div><hr></div><p><em>Mark Gingrass &#8212; Petshots founder &#8212; July 2026</em></p><p><em>Questions, corrections, or feedback: <a href="mailto:mark.gingrass@gmail.com">mark.gingrass@gmail.com</a></em></p>]]></content:encoded></item><item><title><![CDATA[Orphaned - The Death of Your AWS Budget]]></title><description><![CDATA[The Hidden Tax: Finding Cloud Waste in Federal Infrastructure]]></description><link>https://newsletter.markgingrass.com/p/orphaned-cloud-the-death-of-your</link><guid isPermaLink="false">https://newsletter.markgingrass.com/p/orphaned-cloud-the-death-of-your</guid><dc:creator><![CDATA[Mark]]></dc:creator><pubDate>Sat, 13 Jun 2026 15:09:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CNag!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b60c820-4c60-40c2-bcb7-e7d7dd94e30c_1024x1024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>I&#8217;m Pursuing the AWS Solutions Architect Associate Certification</strong></h2><p>I&#8217;m currently working through the AWS Certified Solutions Architect &#8211; Associate (SAA-C03) curriculum. I&#8217;m about one third through with <a href="https://www.udemy.com/share/106WtA3@HCTXMwHZcrSpfOxRdBf2RhuPblL1nAnA0phU9wEPNoNyMrtYQWxldGfDAhEwG6qH/">Stephane Maarek&#8217;s Udemy course</a> and pretty much immediately I can see the value for federal IT environments. It&#8217;s eye opening.</p><h2><strong>Federal Cloud Is Here and AWS is Huge</strong></h2><p>Throughout the past decade, I&#8217;ve worked with mostly on-premises infrastructure. But the landscape has shifted. Cloud computing in the U.S. federal government is not only the norm, it&#8217;s almost considered a requirement. Even if you find savings or efficiency under the on-prem model, the policy pressure to go cloud is very real. A series of federal directives, from earlier Cloud First and Cloud Smart policies to recent OMB mandates, agencies are pushing to modernize <em>legacy</em> systems and migrate workloads to cloud environments (Don&#8217;t get me started on how they define <em>legacy</em>). For most agencies, this means AWS GovCloud or Microsoft Azure Government. Both platforms are tailored to address the compliance and security requirements of the public sector.</p><p>IT means operating in the cloud and not just reading about it, but working in it at the program management and architecture level, and with AI ramping up, it&#8217;s expected that program level managers even get into the weeds. </p><h2><strong>Finding Baked in Waste</strong></h2><p>Just a couple of weeks into the course, one thing becoming clear to me. The most immediate, practical value a solutions architect brings isn&#8217;t necessarily designing new infrastructure from scratch. It&#8217;s in auditing what already exists and identifying inefficiency. It&#8217;s everywhere!</p><p>A few concrete examples of the kinds of waste that are hiding in plain sight. You don&#8217;t have to know the specific terms to understand the waste.</p><ol><li><p>Amazon EC2 offers instance types optimized for different use cases (varying combinations of CPU, memory, storage, and networking capacity). Right-sizing is the process of matching instance types and sizes to actual workload requirements. Organizations tend to provision instances for peak theoretical load and never revisit them. Right-sizing must become an ongoing process, not a one-time exercise.</p></li></ol><div class="callout-block" data-callout="true"><p>Consider an agency running a document processing application on an <code>m5.4xlarge</code> EC2 instance with 16 vCPUs, 64 GB of RAM, provisioned during initial deployment. Fast forward two years: AWS Cost Explorer shows average CPU utilization hovering around 8% and memory utilization rarely exceeding 12%. Nobody revisited the instance type after go-live because the application was &#8220;working.&#8221; More than likely, the stakeholders want the extra bandwith for a &#8216;just-in-case&#8217; scenario. This is very common and usually the wrong move.</p><p>That single instance runs roughly <strong>$560/month</strong> on-demand. A right-sized <code>m5.xlarge</code> &#8212; 4 vCPUs, 16 GB RAM &#8212; would handle that actual workload  at around <strong>$140/month</strong>. That&#8217;s $420 saved per month on one instance. Now multiply that across 20, 50, or 100 instances and you&#8217;re looking at hundreds of thousands of dollars in waste. All without design changes, just lowering the resources.</p></div><ol start="2"><li><p>Multi-AZ deployments (replicates data across multiple locations) and read replicas come at a cost. Running in multiple zones cost more, but may be required for production high-availability workloads. Some workloads can remain in a single zone if they are non-critical. How does the government define <em>critical</em> is the real question. Getting the stakeholders agreement that your application is <strong>not</strong> critical is the hard part. Defining the application as non-critical alone would save the costs without any other work involved - it&#8217;s free!<br></p></li><li><p>S3 Lifecycle policies allow you to transition infrequently accessed data to cheaper storage tiers automatically. If data is only retrieved once or twice a year, storing it in S3 Standard is throwing money into the trash. S3 Glacier or S3 Intelligent-Tiering would be more appropriate. Amazon S3 Storage Lens can identify cost optimization opportunities, and S3 Intelligent-Tiering can automate data lifecycle management. Define <em>infrequent</em> with your stakeholders. I bet they provision based on what-if and just-in-case scenarios. This is bad practice. </p></li></ol><p>The underlying theme here is that it&#8217;s the human in the loop causing the costs to be high. Redefine what is critical. Stop worrying about edge cases. Be realistic about your needs. Behavior changes are the hardest changes to make in an organization, but they cost absolutely nothing, and can save thousands. </p><h2><strong>AI and ML Workloads</strong></h2><p>There&#8217;s another reason that makes this skillset increasingly important. AI and machine learning workloads are exploding within the government. This is driving demand for GPU compute, large-scale data pipelines, and complex storage architectures. What you need is the ability to evaluate the full infrastructure holistic view. Knowing where the data lives, how it moves, how frequently it&#8217;s accessed, what the compute pattern looks like, and whether the architecture actually matches the business need is a valuable skill to have. Don&#8217;t forget, convincing the decision makers will still be the hardest part. This soft skill can&#8217;t be underappreciated. </p><p>Understanding access patterns, retrieval frequency, data pipelines, and cost tradeoffs at the infrastructure level is where program managers with technical depth can differentiate themselves.</p><h2><strong>Where I Am and Where This Is Going</strong></h2><p>I&#8217;m inching towards the halfway point with Stephane Maarek&#8217;s SAA-C03 course and working hands-on in a live AWS environment. Certifications alone are worth absolutely nothing. I could drill TutorialsDojo practice exams, pass the SAA-C03, and still be useless in a real cloud environment. The credential is a door opener, something to compliment my other skills, nothing more. Hands-on projects, documented cost savings, architectural decisions is what you want in your portfolio.</p><p>That said, I&#8217;m aware this post stays at the conceptual level. I plan to go much deeper with the posts soon. I&#8217;m debating creating some hands-on deep dives that get into the granular mechanics of cloud cost optimization: how to actually find the waste, what the findings look like in the console (love the command line!), and what specific remediation steps produce real savings. Until then, there are plenty of other great resources for those looking to up skill. </p><p></p><p>Thank you for visiting my Substack at <a href="https://newsletter.markgingrass.com">newsletter.markgingrass.com</a>.</p>]]></content:encoded></item><item><title><![CDATA[Don't Prompt Me]]></title><description><![CDATA[We're handing off big ideas and make-or-break decisions. And just --dangerously-skip[ing]-permissions]]></description><link>https://newsletter.markgingrass.com/p/dont-prompt-me</link><guid isPermaLink="false">https://newsletter.markgingrass.com/p/dont-prompt-me</guid><dc:creator><![CDATA[Mark]]></dc:creator><pubDate>Mon, 11 May 2026 18:53:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CNag!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b60c820-4c60-40c2-bcb7-e7d7dd94e30c_1024x1024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What You Can&#8217;t Prompt</h1><p>Is there anything wrong with hand and off complete ideas? Is there anything wrong with allowing computers to make big decisions that have real effects? Perhaps it's not as bad as you think.</p><p>Here&#8217;s the history of how we got here. Memory stored on clay tablets. Spread across populations through reading. Calculation to mathematics. Then to machines that did it faster than any human could. Then to networks that scaled all of it across the entire planet.</p><p>Every step was the same story with different tools. The printing press didn&#8217;t kill writers, it made literacy the baseline and pushed writers toward deeper meaning. The spreadsheet didn&#8217;t kill bookkeepers, it killed the need for rooms full of people doing arithmetic by hand and created the field of financial analysis. GPS didn&#8217;t kill navigators, it allowed precision as a survival skill and freed up cognitive bandwidth for everything else.</p><p>The pattern was always: tool handles the rote, human handles the judgment.</p><p>What&#8217;s breaking now is that we&#8217;re outsourcing the judgment itself because the tool is impressive, the pressures are real, and some bean counter said &#8220;we should be using AI for this.&#8221;</p><h2><strong>The human remainder</strong></h2><p>Here&#8217;s what every cognitive outsourcing event in history has in common: it created a premium on whatever was left. It commodities tasks that were once dedicated to skilled tradesmen.</p><p>When writing took over sheer brain memory, the premium moved to interpretation. What does <em>this </em>(the writing) mean and what do we do about it. When calculation moved to machines, the premium moved to knowing <em>which</em> calculation to run. When search engines indexed all human knowledge, the premium moved to knowing <em>which</em> question to ask.</p><p>AI is compressing code generation, document drafting, data synthesis, and pattern recognition. So the premium is moving again. It&#8217;s moving to judgment, relationships, and the ability to define what actually matters before anyone writes a line of code or drafts a single requirement.</p><p>If you&#8217;re a program manager in a federal agency right now and your response to AI is, &#8220;Let&#8217;s automate the reporting,&#8221; you may be optimizing for the thing that is becoming worthless: the artifact. In some cases, the artifact may never have had much value in the first place.</p><p>Think about what these artifacts usually are: documentation proving something was done according to regulation. And by &#8220;proving,&#8221; I mean it was written down, stored somewhere, and almost nobody will ever audit whether the paper matches reality. That should force a basic question: What value does this actually provide in practice?</p><p>If the answer is none, then the target should not be automation. The target should be <strong>removal</strong>. Get rid of the process. Get rid of the artifact. I know federal environments are not that simple. You cannot always delete a requirement because it annoys you. But you can challenge it, negotiate it, ask why it exists, or document that a section is not applicable instead of pretending it adds value. Try it out. Ask the receiving end why they need it, or if you can get by without it. </p><h2><strong>Where AI is worth it</strong></h2><p>AI is good for things that humans waste time on, of course.</p><p>It&#8217;s good for developers. GitHub&#8217;s research found that developers using AI coding assistants completed tasks roughly 55 percent faster than those working without one. <a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>Engineers can focus on the architecture, and less on the actual code. That&#8217;s a real trade with real productivity. It&#8217;s not much different than using C++ over assembly or machine language in computer science. </p><p>It&#8217;s good for the compliance artifact stack (EPLC, IT Boards, etc.), too. Almost nobody is reading that risk register. Nobody is scrutinizing the configuration management plan before they check the box. It&#8217;s going to live in a SharePoint folder until the contract ends. </p><p>When I say &#8220;nobody,&#8221; I do not mean literally no one. I mean very few people, if any, will ever read this material closely enough for it to matter. That is a larger problem, and probably a separate newsletter article. Not here. </p><p>If AI can generate a FISMA-compliant system security plan or a FedRAMP-ready continuous monitoring strategy in a fraction of the time it used to take, take the time back. Use it for something that matters until you can rid the entire process.</p><p>It&#8217;s good for getting something real in front of users faster. A working prototype in two weeks, two days, or even two hours, instead of six months means you kill bad ideas before they become programs of record. We all know management hates to admit sunk costs. In a space where programs have consumed hundreds of millions (or billions, ECSS<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>) of dollars before a real user ever touched the product, faster feedback loops are genuinely transformative.</p><p>These are real wins. They&#8217;re just not the wins the systems integrators are selling you.</p><h2><strong>The checkbox, good in theory, terrible in practice</strong></h2><p>Most compliance artifacts exist because a regulation says they must, not because anyone will use them. Trust me, I want to believe in the system. I want to believe that having everything &#8220;checked&#8221; guarantees a successful product. But the reality is different. Checkboxes are the last thing the product needs and the last task completed before submission. Look at the nearest public restroom. Check the cleaning log by the exit door. It may show that someone checked the box. But when was it actually cleaned?</p><p>The federal government now spends more than $100 billion on IT every year covering everything from legacy system maintenance to new acquisitions.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a> That scale of investment has always attracted scrutiny: the GAO has flagged IT-related problems in its High Risk program since the early 1990s, and formally designated "IT Acquisitions and Operations" as its own High Risk area in 2015, a designation the office has renewed in every update since.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a></p><p>If an artifact is never read, never used to catch a real problem, never changes a single decision, then it&#8217;s just theater. The box gets checked. The program moves forward. The document rots in a folder no one can find.</p><p>AI is perfect for theater. Generate it. Check the box. Move on. The real cost was never the artifact, it was the human spending three weeks on something no one reads. The only thing reading AI is more AI, turtles all the way down (get it?).</p><h2><strong>The acquisition cycle is its own trap</strong></h2><p>Here&#8217;s a problem that isn&#8217;t getting nearly enough attention: AI is moving at software speed, and federal acquisition is moving at regulatory speed.</p><p>The Federal Acquisition Regulation runs to thousands of pages. A competitive procurement from solicitation to award routinely takes twelve to eighteen months. A major IDIQ or GWACs contract vehicle can lock a program into a technology approach for five to ten years. An Authority to Operate (ATO), the security approval required before most federal systems can go live, can take six to eighteen months on its own.</p><p>AI capabilities are advancing on a timescale measured in months, even weeks. By the time you finish an ATO for a specific model version, that version has a handful of successors.</p><p>This is not an AI problem. And AI won&#8217;t fix it alone, because the constraint isn&#8217;t cognitive, it&#8217;s regulatory. It&#8217;s the protest timeline.  &#8220;Other Transaction Authorities&#8221; exist to bypass some of the regulations. However, it takes takes someone with the knowledge, relationships, and institutional credibility to make the case for an exception (despite the justification written using AI, too). You still need to know who to call on.</p><p>That someone is a human. Specifically, one who has been around long enough to know when the rules bend and who needs to sign off on the bending of the rules.</p><h2><strong>What is the price tag</strong></h2><p>The pre-AI default was to acquire first, define later, lock in a contract, then spend years negotiating what the system should actually do. Agile was supposed to change that, and it's been gospel in federal IT circles for a decade. But genuine agile (iterative, user-driven, willing to kill bad ideas early) never really took hold in government. What took hold was the vocabulary and buzzwords.</p><p>Now: deploy the AI, then figure out what you need it to do (again backwards thinking).</p><p>Same trap. Faster, more expensive, with a better slide deck created by AI.</p><p>Requirements gathering <em>can</em> still be human work. Sitting across from someone and asking what they actually need. Finding the constraint they forgot to mention: the API that hasn&#8217;t been updated since 2007, the policy that blocks the entire workflow, the workaround that became standard practice three administrations ago and is now baked into every downstream system.</p><p>Healthcare.gov launched on October 1, 2013, and collapsed under load on day one.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a> The technical failures were real, but the root cause was a requirements and governance failure. Dozens of contractors, no single integrator accountable for the end-to-end system, and a launch deadline that had become a political fixed point that no one was willing to move regardless of readiness. No AI tooling fixes a governance structure where nobody owns the outcome. That decision to hold the date, to accept the fragmentation, to not escalate, was a human failure at the leadership level.</p><p>You can&#8217;t prompt your way to what your users forgot to tell you. And you definitely can&#8217;t prompt your way to the hard call nobody wanted to make.</p><h2><strong>What you can&#8217;t prompt</strong></h2><p>When you have a real problem, you don&#8217;t run a query. You call the person who has done this before.</p><p>The one who reads the org chart the way Robert Caro reads a political biography, looking not for who holds the title, but for who holds the power (I loved the LBJ Series by Robert Caro). The one who turned a hard no into a yes in a meeting two years ago, on the strength of a relationship built over a decade. </p><p>Federal agencies are not flat organizations with clean reporting lines and transparent decision rights. They are layered institutions with career staff who have survived six administrations, political appointees who have eighteen months to make their mark, informal power structures built on years of working the same committees, and institutional memory that doesn&#8217;t live in any document. Knowing how to navigate that, who to brief before the formal meeting, who needs to feel included before they&#8217;ll say yes, which deputy&#8217;s chief of staff is actually running the budget process regardless of title, that is the real product. It took years to build. It doesn&#8217;t transfer to a model. In other words, the soft skills.</p><p>Persuasion is human. Getting the people in the room who aren&#8217;t decision-makers to pull in the same direction anyway, that&#8217;s the job underneath the job. No model has skin in that game.</p><h2><strong>The right measure</strong></h2><p>&#8220;I need a chatbot for constituent services&#8221; is not a requirement. It&#8217;s already a solution. Someone decided the answer before they understood the question.</p><p>The outcomes worth fighting for don&#8217;t sound like technology. They sound like: we caught $40 million in fraud before the money left the account. We cut veterans&#8217; benefits processing time from nine months to six weeks. We gave case workers enough time back to actually see their clients instead of updating a database.</p><p>AI can contribute to all of those, and in some cases already is. The IRS has used machine learning to flag high-risk returns for audit selection. CBP uses AI to screen cargo manifests for anomalies that would take human analysts days to find. The Social Security Administration has been piloting tools to help process disability determinations faster, a backlog that has real human cost for people waiting on benefits they&#8217;re owed.</p><p>These work not because someone deployed AI at the problem, but because someone first defined what &#8220;working&#8221; actually means in each context. The outcome was specified before the tool was selected (so rare in the public space). That sequence matters. Reverse it, and you get a very impressive demo that solves a problem no one had.</p><p>You can&#8217;t prompt your way to knowing which outcome actually matters. You have to have been in the room. Talked to the caseworker. Understood what nine months actually costs a veteran waiting on a decision. AI can get you there faster once you know where you&#8217;re going. It has no idea what&#8217;s worth going there for.</p><h2><strong>What&#8217;s next</strong></h2><p>We outsourced memory. Calculation. Scale. Now we&#8217;re outsourcing whole ideas.</p><p>What&#8217;s left?</p><p>The model wasn&#8217;t in the room when that trust was built. It didn&#8217;t sit through the failed pilots, the budget kills, the vendor who promised a platform and delivered a PowerPoint.</p><p>You did. Knowing who to call, why the last approach failed, and which office will  kill this if you don&#8217;t bring them in early, that&#8217;s more than institutional knowledge. That&#8217;s survival knowledge. It lives in people who stayed, who paid attention, who built something over time that outlasted the last reorganization.</p><p>Every organization that hollows out that function to save money on a headcount line while expanding an AI license is going to rediscover, the hard way, that the tool doesn&#8217;t know what it&#8217;s for. It will generate artifacts. It will check boxes. It will produce a demo that impresses.</p><p>And then someone will ask what problem it solved. And the room will go quiet.</p><p>The people trying to replace judgment with a prompt are going to find out the hard way.</p><div><hr></div><p><em>The views expressed here are my own and do not represent any federal agency.</em></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>GitHub, &#8220;Research: Quantifying GitHub Copilot&#8217;s Impact on Developer Productivity and Happiness,&#8221; September 7, 2022. GitHub reported that developers using GitHub Copilot completed the task 55 percent faster than developers who did not use Copilot. <a href="https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-on-developer-productivity-and-happiness/">https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-on-developer-productivity-and-happiness/</a></p><p></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>The Air Force&#8217;s Expeditionary Combat Support System, or ECSS, is the obvious warning label. It was supposed to modernize Air Force logistics through a unified ERP system. Instead, after roughly $1 billion and years of work, the program was cancelled with &#8220;negligible&#8221; value delivered. <a href="https://centreforpublicimpact.org/public-impact-fundamentals/the-us-air-forces-expeditionary-combat-support-system-ecss/?utm_source=chatgpt.com">https://centreforpublicimpact.org/public-impact-fundamentals/the-us-air-forces-expeditionary-combat-support-system-ecss/?utm_source=chatgpt.com</a></p><p></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>U.S. Government Accountability Office, <em>&#8220;Federal Efforts to Update Old IT Are Years Behind Schedule,&#8221;</em> GAO WatchBlog, March 2025, gao.gov. The FY2025 federal IT budget totaled approximately $102 billion government-wide, per OMB data reported by the Congressional Research Service (<em>Information Technology Spending in the President&#8217;s Budget Submission for FY2025</em>, CRS Report R48049, 2024).</p><p></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>GAO, <em>High Risk List</em>, gao.gov/high-risk-list. The program began in 1990; IT system modernization projects appeared among flagged concerns as early as 1992. GAO formally added &#8220;Improving the Management of IT Acquisitions and Operations&#8221; as a standalone High Risk area in 2015 (<em>GAO-15-290</em>) and has retained it through its most recent 2025 update (<em>GAO-25-107743</em>).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>HHS Office of Inspector General, <em>HealthCare.gov: Case Study of CMS Management of the Federal Marketplace</em>, OEI-06-14-00350 (2016), oig.hhs.gov.</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[The AI Solution Trap]]></title><description><![CDATA[Federal agencies are spending real money on AI without asking the basic questions. The result: expensive tools layered on broken processes.]]></description><link>https://newsletter.markgingrass.com/p/the-ai-solution-trap</link><guid isPermaLink="false">https://newsletter.markgingrass.com/p/the-ai-solution-trap</guid><dc:creator><![CDATA[Mark]]></dc:creator><pubDate>Fri, 08 May 2026 20:25:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CNag!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b60c820-4c60-40c2-bcb7-e7d7dd94e30c_1024x1024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I tried to have an LLM write this post for me. It failed.</p><p>The draft was cleaner than mine, but it was hollow. Every sharp point got softened into a &#8220;balanced perspective.&#8221; Every real opinion got buried under caveats. It sounded polished, professional, and completely forgettable. Like most AI-written commentary, it used all the right words and said almost nothing.</p><p>The federal AI space has the same problem. Real money chasing solutions. No patience for basic questions: What problem are we solving? Who is this for? What changes if this actually works?</p><p>That&#8217;s how you end up spending serious money to automate confusion.</p><h2>The Speed Trap</h2><p>The program managers are usually the quietest people in the room. They see the weak requirements, the bad assumptions, and the missing data. They know what&#8217;s actually broken.</p><p>But by the time they&#8217;re in that room, the decision has already been blessed three levels up. The budget is allocated. The contract vehicle is picked. Staying quiet and buying the tool is the safest career move left.</p><p>So that&#8217;s what happens. Leadership gets movement. Vendors get paid. Agencies get a modern interface on top of a broken process.</p><p>A broken process that now demos better.</p><h2>The Process Problem</h2><p>This is where most of the waste lives.</p><p>Federal agencies are layering expensive technology onto workflows that should&#8217;ve been simplified, redesigned, or deleted years ago. Instead of asking whether a process deserves to exist, they ask how AI can be inserted into it.</p><p>One team uses AI to scrape updates from a tracker and generate a 40-slide deck. Leadership uses AI to summarize that deck into three bullets. Someone uses AI to draft a reply: &#8220;Thanks, keep going.&#8221; At every step, work is being done, time is being saved, and none of it creates value.</p><p>It&#8217;s the digital equivalent of putting a turbocharger on a shopping cart.</p><p>Automating waste doesn&#8217;t make it strategic. It just produces useless output faster and with more confidence. When something actually goes wrong, nobody trusts the polished dashboard. They call the person closest to the problem and ask what&#8217;s happening.</p><p>That should tell us everything we need to know.</p><h2>The Ownership Trap</h2><p>When agencies buy AI, they aren&#8217;t just buying software. Unlike a legacy system that fails the same way every time, an AI model degrades. It drifts. Inputs change, policies change, user behavior shifts &#8212; and a model that worked on day one quietly becomes wrong at scale.</p><p>If the contract is weak on data rights, the agency ends up paying for a capability it can use but can&#8217;t control. That&#8217;s how the duplication loop starts. One office pays to develop a model. Another office has the same need, but the implementation is locked in a vendor&#8217;s proprietary stack. It can&#8217;t be reused.</p><p>The government buys the same answer twice.</p><p>If you can&#8217;t inspect it, retrain it, or fix it without calling the original vendor, you don&#8217;t have a solution. You have a rental with a degradation clock running in the background.</p><h2>The Talent Gap</h2><p>Upskilling efforts in the federal AI space are mostly theater. The gap isn&#8217;t closed by hiring a few specialists or teaching staff to prompt a chatbot. Prompting isn&#8217;t strategy.</p><p>The people agencies need are the ones who understand how these systems behave in production &#8212; how they fail, how they drift, when to shut them off. People who can maintain a model through a recompete, an ATO renewal, a policy change, and two rounds of staffing turnover without losing the thread of what it was built to do.</p><p>That talent walks out the door. A contractor team builds something promising, leadership celebrates, and a few months later the expertise is gone. The PM is left holding a tool they can operate on a good day but can&#8217;t troubleshoot when the outputs start looking wrong.</p><p>The organization didn&#8217;t build capability. It outsourced the brain and called it progress.</p><h2>The Work Comes First</h2><p>Cut the process before you automate it. If a workflow improves when you remove two steps, those steps never needed a model &#8212; they needed a leader willing to delete them.</p><p>Define what you&#8217;re actually buying. Not &#8220;AI capability.&#8221; A result. Faster grant reviews. Fewer manual reconciliations. Shorter time from intake to decision. If the outcome can&#8217;t be stated in plain English, the acquisition is too vague to survive.</p><p>Secure real ownership. Data rights, retraining rights, documentation. If the agency needs to call the vendor to understand what the system is doing, it doesn&#8217;t own the solution. It&#8217;s just paying for access.</p><p>A prototype is easy. A production system that survives a policy change, an audit, and a change in administration is hard. That&#8217;s where the actual work gets done.</p><p>We keep buying complexity because clarity is harder to defend in a budget hearing. Saying &#8220;we deleted two approval steps and cut processing time in half&#8221; doesn&#8217;t land the same way as &#8220;we deployed an AI-enabled workflow modernization platform.&#8221; But one of those actually solved something.</p><p>The hard part isn&#8217;t buying the AI. The hard part is knowing when not to.</p><p><em>The views expressed here are my own and do not represent any federal agency.</em></p>]]></content:encoded></item><item><title><![CDATA[The Certificate Warning Nobody Reads]]></title><description><![CDATA[Federal IT security theater: how certificate click-throughs became compliance artifacts that protect auditors, not systems.]]></description><link>https://newsletter.markgingrass.com/p/the-certificate-warning-nobody-reads</link><guid isPermaLink="false">https://newsletter.markgingrass.com/p/the-certificate-warning-nobody-reads</guid><dc:creator><![CDATA[Mark]]></dc:creator><pubDate>Fri, 08 May 2026 13:36:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CNag!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8b60c820-4c60-40c2-bcb7-e7d7dd94e30c_1024x1024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You&#8217;ve seen it. The browser throws up a wall of text, &#8220;Do you trust this certificate?&#8221; followed by a string of hex digits that are meaningless to humans. You&#8217;re on a government computer. You&#8217;re inside a federal building.</p><p>You click <em>yes</em>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.markgingrass.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">This Substack is reader-supported. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Of course you click yes. Name one person who doesn&#8217;t.</p><p>This screen only exists so somebody can prove you clicked it. The ATO (Authority to Operate: the formal authorization a federal system needs before going live, issued after a security assessment package is reviewed) package has a control that says users must validate certificate trust. The system records the click and calls that a control. Nobody in that chain ever stopped to ask whether clicking yes changes anything.</p><p>That&#8217;s not security. That&#8217;s paperwork with a button. Worthless.</p><p>Click <em>yes</em> enough times and it becomes muscle memory. Which means the one time the warning actually matters, a legitimate attack could happen.</p><p>A warning everyone ignores isn&#8217;t a safeguard in any sense. It&#8217;s camouflage for bean counters. The auditor gets the screenshot. The attacker gets the goods.</p><h2>Why the Popup Exists</h2><p>Here&#8217;s how it usually goes. Someone stood up TLS inspection (<strong>Transport Layer Security</strong>: the protocol that encrypts network traffic between a client and a server). Nobody pushed the root certificate cleanly to managed devices. The browser complained. So the workaround became policy: click yes, carry on. Six months later, nobody remembers why the warning appears or whose job it is to fix it. It&#8217;s just part of the environment.</p><p>The popup isn&#8217;t adding any value.</p><h2>This Is the Standard Playbook</h2><p>The cert warning isn&#8217;t a one-off failure. It&#8217;s how federal IT security operates.</p><p>The annual security awareness training everyone clicks through in under an hour to clear the compliance flag? Same mechanic. The forced password change cycle that kept government sticky-note suppliers in business for a decade? Same mechanic. The enterprise-wide Zero Trust mandate in the strategy document while users manually bypass session hygiene every morning? Same mechanic.</p><p>In every case, a real security requirement got translated into a user interaction that nobody takes seriously. The box gets checked. The behavior doesn&#8217;t change.</p><p>It&#8217;s cheaper to make 80,000 people click through nonsense than to fix one broken enterprise service. So nobody fixes the service. Federal IT has spent two decades building for the audit and breaking for the mission. If a control generates a log, a screenshot, or a completion record, it survives budget cycles. If it requires fixing the underlying infrastructure, it gets deferred.</p><h2>What the System Rewards</h2><p>Nobody gets promoted for eliminating the popup.</p><p>The Authorizing Official signs a Risk Acceptance, documents the residual risk, and calls it managed. The control owner gets credit for the artifact. The help desk absorbs the tickets. The user absorbs the risk. The fix request dies in the intake queue.</p><p>This is what happens when control owners can pass audits without owning user behavior. The program office buys the system. Enterprise IT breaks the trust chain. Security signs off on the control language. Nobody owns what the user sees on the other end.</p><p>When the breach happens &#8212; and the logs show the warning appeared six hundred times and the user clicked through every single one &#8212; nobody is responsible. It was documented. The user was informed.</p><h2>We Blame the Wrong People</h2><p>We call it a workforce problem. Users don&#8217;t take security seriously. They click through warnings without reading them.</p><p>We designed it that way.</p><p>We built systems that produce compliance artifacts instead of secure behavior, then blamed the people who adapted to the environment we built. The annual training is forgettable by design &#8212; anything longer doesn&#8217;t clear the flag on time. The cert warning is ignorable by design &#8212; if it blocked access, it would generate more tickets than IT could handle. Every friction point that was too inconvenient got softened into a checkbox and called a control.</p><p>The workforce is doing exactly what the system trained them to do.</p><h2>The Control Was Fake From the Start</h2><p>If your security control depends on a user interpreting a certificate thumbprint, you don&#8217;t have a security control. You have a ritual.</p><p>A real control removes the decision from the user. It&#8217;s rare enough that when it fires, people notice. It doesn&#8217;t need to explain itself with a wall of hex digits no one will ever read.</p><p>The warning was fake the day it shipped. All it ever protected was the agency&#8217;s ability to say it tried.</p><p><em>The views expressed here are my own and do not represent any federal agency.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://newsletter.markgingrass.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">This Substack is reader-supported. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>