<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Henry A</title>
    <description>The latest articles on DEV Community by Henry A (@henryaza).</description>
    <link>https://dev.to/henryaza</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3879483%2Ff62f5c98-bf60-4367-a096-65ab5d928205.png</url>
      <title>DEV Community: Henry A</title>
      <link>https://dev.to/henryaza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/henryaza"/>
    <language>en</language>
    <item>
      <title>How I Packaged 130+ Hours of AWS Infrastructure Into Reusable Templates</title>
      <dc:creator>Henry A</dc:creator>
      <pubDate>Fri, 17 Apr 2026 01:08:40 +0000</pubDate>
      <link>https://dev.to/henryaza/how-i-packaged-130-hours-of-aws-infrastructure-into-reusable-templates-37m4</link>
      <guid>https://dev.to/henryaza/how-i-packaged-130-hours-of-aws-infrastructure-into-reusable-templates-37m4</guid>
      <description>&lt;p&gt;Every new project starts the same way.&lt;/p&gt;

&lt;p&gt;You spin up an AWS account. You need a VPC. Three AZs, public and private subnets, NAT Gateways. Then you need security — CloudTrail, GuardDuty, IAM hardening. Then CI/CD pipelines. Then Terraform modules. Then Docker Compose for local dev. Then Nginx for production.&lt;/p&gt;

&lt;p&gt;Each time, you either copy-paste from your last project (hoping nothing broke) or build from scratch (knowing how long it takes).&lt;/p&gt;

&lt;p&gt;I got tired of it. So I packaged every infrastructure pattern I keep rebuilding into reusable templates. 75+ files across 7 products. This article walks through the architecture decisions behind each one, with free samples you can use right now.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Security Checklist No One Actually Follows
&lt;/h2&gt;

&lt;p&gt;Most AWS accounts ship with default settings. No audit trail. No MFA enforcement. S3 buckets one misconfiguration away from being public.&lt;/p&gt;

&lt;p&gt;The CIS AWS Foundations Benchmark exists, but it's a 300-page PDF. Nobody reads it. So I distilled it into a 50-point checklist and wrote CloudFormation templates that implement the critical controls.&lt;/p&gt;

&lt;p&gt;Here's the full checklist — free, no catch:&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Security Hardening Checklist (50 points)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Identity &amp;amp; Access Management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Enable MFA on root account&lt;/li&gt;
&lt;li&gt;[ ] Do NOT use root account for daily tasks&lt;/li&gt;
&lt;li&gt;[ ] Enable MFA for all IAM users with console access&lt;/li&gt;
&lt;li&gt;[ ] Set strong password policy (14+ chars, complexity, rotation)&lt;/li&gt;
&lt;li&gt;[ ] Remove unused IAM users&lt;/li&gt;
&lt;li&gt;[ ] Remove unused IAM credentials (access keys)&lt;/li&gt;
&lt;li&gt;[ ] Rotate access keys every 90 days&lt;/li&gt;
&lt;li&gt;[ ] Attach policies to groups, not directly to users&lt;/li&gt;
&lt;li&gt;[ ] Use IAM roles for applications, not access keys&lt;/li&gt;
&lt;li&gt;[ ] Implement least-privilege permissions&lt;/li&gt;
&lt;li&gt;[ ] Use AWS SSO for multi-account access&lt;/li&gt;
&lt;li&gt;[ ] Review IAM policies for &lt;code&gt;*&lt;/code&gt; resource permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Logging &amp;amp; Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Enable CloudTrail in all regions&lt;/li&gt;
&lt;li&gt;[ ] Enable CloudTrail log file validation&lt;/li&gt;
&lt;li&gt;[ ] Ensure CloudTrail logs are encrypted (KMS)&lt;/li&gt;
&lt;li&gt;[ ] Enable CloudTrail log delivery to S3+CloudWatch&lt;/li&gt;
&lt;li&gt;[ ] Enable AWS Config in all regions&lt;/li&gt;
&lt;li&gt;[ ] Enable VPC Flow Logs for all VPCs&lt;/li&gt;
&lt;li&gt;[ ] Enable GuardDuty&lt;/li&gt;
&lt;li&gt;[ ] Enable Security Hub&lt;/li&gt;
&lt;li&gt;[ ] Set up CloudWatch alarms for: root usage, unauthorized API calls, console sign-in failures&lt;/li&gt;
&lt;li&gt;[ ] Enable S3 access logging for critical buckets&lt;/li&gt;
&lt;li&gt;[ ] Configure SNS alerts for security findings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Networking&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] No security groups allow 0.0.0.0/0 to port 22 (SSH)&lt;/li&gt;
&lt;li&gt;[ ] No security groups allow 0.0.0.0/0 to port 3389 (RDP)&lt;/li&gt;
&lt;li&gt;[ ] Default security group restricts all traffic&lt;/li&gt;
&lt;li&gt;[ ] Use private subnets for databases and application servers&lt;/li&gt;
&lt;li&gt;[ ] Use VPC endpoints for AWS service access (S3, DynamoDB, ECR)&lt;/li&gt;
&lt;li&gt;[ ] Enable DNS query logging&lt;/li&gt;
&lt;li&gt;[ ] Use Network ACLs as secondary defense layer&lt;/li&gt;
&lt;li&gt;[ ] No public IP on EC2 instances in private subnets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Data Protection&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Enable S3 Block Public Access at account level&lt;/li&gt;
&lt;li&gt;[ ] Enable S3 default encryption on all buckets&lt;/li&gt;
&lt;li&gt;[ ] Enable S3 versioning on critical buckets&lt;/li&gt;
&lt;li&gt;[ ] Enable RDS encryption at rest&lt;/li&gt;
&lt;li&gt;[ ] Enable RDS automated backups&lt;/li&gt;
&lt;li&gt;[ ] Enable EBS encryption by default&lt;/li&gt;
&lt;li&gt;[ ] Use KMS CMKs for sensitive data (not default keys)&lt;/li&gt;
&lt;li&gt;[ ] Enable SSL/TLS for data in transit&lt;/li&gt;
&lt;li&gt;[ ] Enforce S3 bucket policies requiring SSL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Compute&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Use IMDSv2 (require HTTP tokens) on all EC2 instances&lt;/li&gt;
&lt;li&gt;[ ] Keep AMIs patched and up to date&lt;/li&gt;
&lt;li&gt;[ ] Use Systems Manager Session Manager instead of SSH&lt;/li&gt;
&lt;li&gt;[ ] Enable detailed monitoring on production instances&lt;/li&gt;
&lt;li&gt;[ ] Use Auto Scaling groups (no single points of failure)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Account &amp;amp; Organization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Enable AWS Organizations with SCPs&lt;/li&gt;
&lt;li&gt;[ ] Deny root account usage via SCP&lt;/li&gt;
&lt;li&gt;[ ] Restrict regions via SCP&lt;/li&gt;
&lt;li&gt;[ ] Prevent disabling of CloudTrail/GuardDuty/Config via SCP&lt;/li&gt;
&lt;li&gt;[ ] Enable AWS Budgets alerts (catch unexpected spend = possible compromise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to use this:&lt;/strong&gt; Score yourself. Every unchecked item is a risk. The CloudFormation templates in my &lt;a href="https://henryaza.gumroad.com/l/aws-security-hardening-kit" rel="noopener noreferrer"&gt;Security Hardening Kit&lt;/a&gt; automate items 1-23 and 32-40 with 4 &lt;code&gt;aws cloudformation deploy&lt;/code&gt; commands.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. VPC Architecture — Why Three Subnet Tiers, Not Two
&lt;/h2&gt;

&lt;p&gt;The most common VPC mistake I see: two tiers (public + private). This forces your databases into the same subnets as your application servers.&lt;/p&gt;

&lt;p&gt;The architecture that works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet Gateway
       │
   ┌───┴───┐
   │Public │ ← ALB, NAT Gateways, Bastion
   │Subnets│   (3 AZs: 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24)
   └───┬───┘
       │
   ┌───┴───┐
   │Private│ ← App servers, ECS tasks, Lambda
   │Subnets│   (3 AZs: 10.0.11.0/24, 10.0.12.0/24, 10.0.13.0/24)
   └───┬───┘
       │
   ┌───┴────┐
   │Isolated│ ← RDS, ElastiCache, Secrets
   │Subnets │   (3 AZs: 10.0.21.0/24, 10.0.22.0/24, 10.0.23.0/24)
   └────────┘
       │
   No route to internet. No NAT. Nothing gets in or out.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why three tiers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public subnets&lt;/strong&gt; have a route to the Internet Gateway. Only load balancers and NAT Gateways live here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private subnets&lt;/strong&gt; have a route to NAT Gateway (outbound internet only). App servers live here — they can pull packages and call APIs, but nothing can reach them directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolated subnets&lt;/strong&gt; have NO route to the internet. Period. Databases live here. The only way to reach them is from within the VPC. Even if an attacker compromises your app server, they can't exfiltrate data from your database subnet to the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The NAT Gateway cost trap:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NAT Gateways cost ~$32/month each. A production VPC with 3 AZs needs 3 NAT Gateways = ~$96/month just for outbound internet access in private subnets.&lt;/p&gt;

&lt;p&gt;My templates include a dev variant: single NAT Gateway, 2 AZs, ~$32/month. Same architecture, lower cost for non-production environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC Endpoints slash your NAT bill further:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every time your EC2 instance or Lambda calls S3, DynamoDB, ECR, or CloudWatch, that traffic goes through NAT Gateway by default. You're paying data processing fees for internal AWS traffic.&lt;/p&gt;

&lt;p&gt;VPC Endpoints route that traffic over AWS's private network instead. Free for gateway endpoints (S3, DynamoDB), pennies for interface endpoints. I include endpoints for the 5 services that generate the most NAT traffic.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://henryaza.gumroad.com/l/aws-vpc-cloudformation-starter-kit" rel="noopener noreferrer"&gt;VPC Starter Kit&lt;/a&gt; includes the full production VPC, dev variant, security groups, VPC endpoints, and a hardened bastion — 5 CloudFormation templates, 1,376 lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. CI/CD — Why OIDC and Why It Matters
&lt;/h2&gt;

&lt;p&gt;Here's a pattern I still see in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# DON'T DO THIS&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long-lived access keys stored as GitHub secrets. If anyone with repo access runs &lt;code&gt;echo $AWS_SECRET_ACCESS_KEY&lt;/code&gt; in a workflow step, your keys are in the logs. If GitHub gets breached, your keys are exposed. The keys never expire unless you rotate them manually.&lt;/p&gt;

&lt;p&gt;The fix is OIDC (OpenID Connect). GitHub proves its identity to AWS using a signed token, and AWS gives back temporary credentials that expire in 1 hour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;  &lt;span class="c1"&gt;# Required for OIDC&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ROLE_ARN }}&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No long-lived secrets. No keys to rotate. The IAM role's trust policy restricts which repositories and branches can assume it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trivy pattern&lt;/strong&gt; — another thing I build into every Docker workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build image (local for scanning)&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;           &lt;span class="c1"&gt;# Build locally, don't push yet&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Trivy vulnerability scan&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@master&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ECR_REPOSITORY }}:latest&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;table&lt;/span&gt;
    &lt;span class="na"&gt;exit-code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;         &lt;span class="c1"&gt;# FAIL the build&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CRITICAL,HIGH&lt;/span&gt;
    &lt;span class="na"&gt;ignore-unfixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# Don't flag unfixable CVEs&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push to ECR&lt;/span&gt;      &lt;span class="c1"&gt;# Only runs if Trivy passes&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.meta.outputs.tags }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build → scan → push. If Trivy finds a CRITICAL or HIGH vulnerability, the build fails. The image never reaches your registry. This catches vulnerable base images and dependencies before they hit production.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://henryaza.gumroad.com/l/github-actions-cicd-templates" rel="noopener noreferrer"&gt;GitHub Actions CI/CD Template Pack&lt;/a&gt; includes 5 complete workflows with OIDC, Trivy, and multi-environment approval gates.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Terraform Module Design — Keep the Interfaces Clean
&lt;/h2&gt;

&lt;p&gt;The mistake: one giant &lt;code&gt;main.tf&lt;/code&gt; with everything in it. 800 lines, impossible to reuse, breaks when you sneeze.&lt;/p&gt;

&lt;p&gt;The pattern that works: small modules with clean interfaces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Root composition — this is all you touch per environment&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/vpc"&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;az_count&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;az_count&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"ec2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/ec2"&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;
  &lt;span class="nx"&gt;private_subnets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;
  &lt;span class="nx"&gt;public_subnets&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet_ids&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"rds"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/rds"&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;
  &lt;span class="nx"&gt;db_subnets&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isolated_subnet_ids&lt;/span&gt;
  &lt;span class="nx"&gt;app_security_group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_sg_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each module has a clear input/output contract. VPC outputs subnet IDs, EC2 consumes them.&lt;/li&gt;
&lt;li&gt;Environment separation through &lt;code&gt;tfvars&lt;/code&gt;, not code duplication. Same modules, different variables:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# environments/dev/terraform.tfvars&lt;/span&gt;
&lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev"&lt;/span&gt;
&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
&lt;span class="nx"&gt;az_count&lt;/span&gt;      &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nx"&gt;multi_az_rds&lt;/span&gt;  &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="c1"&gt;# environments/production/terraform.tfvars&lt;/span&gt;
&lt;span class="nx"&gt;environment&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&lt;/span&gt;
&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.large"&lt;/span&gt;
&lt;span class="nx"&gt;az_count&lt;/span&gt;      &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="nx"&gt;multi_az_rds&lt;/span&gt;  &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Want to add a new module (say, ElastiCache)? Add it to the root composition. Existing modules don't change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My &lt;a href="https://henryaza.gumroad.com/l/terraform-aws-starter-pack" rel="noopener noreferrer"&gt;Terraform AWS Starter Pack&lt;/a&gt; includes 5 modules (VPC, EC2+ASG+ALB, RDS Multi-AZ, IAM, S3) with this exact pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Docker Compose — Health Checks Change Everything
&lt;/h2&gt;

&lt;p&gt;Most Docker Compose files I see in the wild look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem: &lt;code&gt;depends_on&lt;/code&gt; only waits for the container to &lt;strong&gt;start&lt;/strong&gt;, not for the service inside it to be &lt;strong&gt;ready&lt;/strong&gt;. Your app crashes on boot because Postgres hasn't finished initializing.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_NAME:-myapp}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_USER:-postgres}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD:-postgres}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${DB_USER:-postgres}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${APP_PORT:-3000}:3000"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;  &lt;span class="c1"&gt;# Waits for healthcheck&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-myapp}&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;healthcheck&lt;/code&gt; on Postgres uses &lt;code&gt;pg_isready&lt;/code&gt; — only reports healthy when Postgres can accept connections&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; makes the app wait for a real readiness signal&lt;/li&gt;
&lt;li&gt;Environment variables with defaults via &lt;code&gt;.env&lt;/code&gt; — no hardcoded credentials&lt;/li&gt;
&lt;li&gt;Named volume so data survives &lt;code&gt;docker compose down&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My &lt;a href="https://henryaza.gumroad.com/l/docker-compose-starter-templates" rel="noopener noreferrer"&gt;Docker Compose Templates&lt;/a&gt; include 8 stacks (Node+Postgres, Django+Celery, MERN, Go, Rails+Sidekiq, Spring Boot, Next.js, Traefik) all with health checks, volumes, and .env management.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Nginx — The SSL Config That Gets an A+
&lt;/h2&gt;

&lt;p&gt;Getting an A+ on &lt;a href="https://www.ssllabs.com/ssltest/" rel="noopener noreferrer"&gt;SSL Labs&lt;/a&gt; shouldn't take 3 hours of Googling. Here's the config pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# OCSP Stapling — proves your cert isn't revoked without the client calling the CA&lt;/span&gt;
&lt;span class="k"&gt;ssl_stapling&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_stapling_verify&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;resolver&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="s"&gt;.1.1&lt;/span&gt; &lt;span class="mf"&gt;8.8&lt;/span&gt;&lt;span class="s"&gt;.8.8&lt;/span&gt; &lt;span class="s"&gt;valid=300s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Session caching — avoids full TLS handshake on reconnection&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_tickets&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;# Disable for perfect forward secrecy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why these choices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TLS 1.2+1.3 only&lt;/strong&gt; — TLS 1.0 and 1.1 are deprecated. Drop them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ssl_prefer_server_ciphers off&lt;/code&gt;&lt;/strong&gt; — counterintuitive, but with TLS 1.3 the client picks the cipher. Setting this to &lt;code&gt;on&lt;/code&gt; only matters for TLS 1.2 and can actually result in worse cipher selection with modern clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCSP stapling&lt;/strong&gt; — your server fetches the cert status from the CA and includes it in the handshake. The client doesn't need to make a separate request to verify the cert isn't revoked. Faster and more private.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the security headers that complete the picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=63072000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;preload"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Permissions-Policy&lt;/span&gt; &lt;span class="s"&gt;"camera=(),&lt;/span&gt; &lt;span class="s"&gt;microphone=(),&lt;/span&gt; &lt;span class="s"&gt;geolocation=()"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;a href="https://henryaza.gumroad.com/l/nginx-config-pack" rel="noopener noreferrer"&gt;Nginx Config Pack&lt;/a&gt; includes 6 production configs: reverse proxy, load balancer, static site with SPA fallback, API gateway with rate limiting, SSL termination, and security headers.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Lambda — Partial Batch Failure Reporting
&lt;/h2&gt;

&lt;p&gt;This is the Lambda pattern most people get wrong with SQS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# If ANY message fails, ALL get retried
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your batch has 10 messages and message #7 fails, SQS retries all 10. Messages 1-6 get processed twice. Message 7 still fails. You get infinite retries and duplicate processing.&lt;/p&gt;

&lt;p&gt;The fix — partial batch failure reporting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;itemIdentifier&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;messageId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;batchItemFailures&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;  &lt;span class="c1"&gt;# Only retry the failed ones
&lt;/span&gt;    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;FunctionResponseTypes: [ReportBatchItemFailures]&lt;/code&gt; in your SAM template, SQS only retries the specific messages that failed. The successful ones are removed from the queue.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://henryaza.gumroad.com/l/lambda-starter-templates" rel="noopener noreferrer"&gt;Lambda Starter Templates&lt;/a&gt; include 5 SAM-based patterns: REST API + DynamoDB CRUD, S3 event processor, SQS batch worker (with partial failures), scheduled EventBridge tasks, and a custom JWT authorizer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Stack
&lt;/h2&gt;

&lt;p&gt;Each of these patterns is something I've built and rebuilt multiple times. I packaged them into 7 products:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;What's inside&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/aws-security-hardening-kit" rel="noopener noreferrer"&gt;AWS Security Hardening Kit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;4 CloudFormation stacks + 50-point checklist&lt;/td&gt;
&lt;td&gt;$19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/aws-vpc-cloudformation-starter-kit" rel="noopener noreferrer"&gt;VPC Starter Kit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5 templates — production VPC, dev VPC, security groups, endpoints, bastion&lt;/td&gt;
&lt;td&gt;$24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/github-actions-cicd-templates" rel="noopener noreferrer"&gt;GitHub Actions CI/CD&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5 workflows — Python, Java, Docker+Trivy, Terraform, multi-env&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/terraform-aws-starter-pack" rel="noopener noreferrer"&gt;Terraform Starter Pack&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5 modules — VPC, EC2+ASG+ALB, RDS, IAM, S3&lt;/td&gt;
&lt;td&gt;$29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/docker-compose-starter-templates" rel="noopener noreferrer"&gt;Docker Compose Templates&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;8 stacks — Node, Django, MERN, Go, Rails, Spring Boot, Next.js, Traefik&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/nginx-config-pack" rel="noopener noreferrer"&gt;Nginx Config Pack&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;6 configs — reverse proxy, load balancer, SSL, API gateway, security headers&lt;/td&gt;
&lt;td&gt;$13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://henryaza.gumroad.com/l/lambda-starter-templates" rel="noopener noreferrer"&gt;Lambda Starter Templates&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5 SAM patterns — API CRUD, S3 processor, SQS worker, scheduler, authorizer&lt;/td&gt;
&lt;td&gt;$19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://henryaza.gumroad.com/l/aws-devops-infrastructure-bundle" rel="noopener noreferrer"&gt;Full Bundle&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;All 7 products — 75+ templates&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$69&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every template is production-tested, fully commented, and licensed for unlimited commercial use. No subscriptions, no SaaS — download the files and they're yours.&lt;/p&gt;

&lt;p&gt;If the 50-point security checklist above was useful, the full templates go deeper. &lt;a href="https://henryaza.gumroad.com/l/aws-devops-infrastructure-bundle" rel="noopener noreferrer"&gt;Grab the bundle&lt;/a&gt; or pick the individual products you need.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions about any of the patterns? Drop a comment — happy to go deeper on any of these.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
