<?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: Anthony Uketui</title>
    <description>The latest articles on DEV Community by Anthony Uketui (@tony_uketui_6cca68c7eba02).</description>
    <link>https://dev.to/tony_uketui_6cca68c7eba02</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2786119%2F8360bb2b-6352-4543-a439-c55338fd1596.jpg</url>
      <title>DEV Community: Anthony Uketui</title>
      <link>https://dev.to/tony_uketui_6cca68c7eba02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tony_uketui_6cca68c7eba02"/>
    <language>en</language>
    <item>
      <title>Designing Active-Active Multi-Region for a Payment Gateway: An Architecture Decision Record</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Thu, 02 Jul 2026 18:21:22 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/designing-active-active-multi-region-for-a-payment-gateway-an-architecture-decision-record-51hh</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/designing-active-active-multi-region-for-a-payment-gateway-an-architecture-decision-record-51hh</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I designed a multi-region Active-Active architecture for a fintech payment gateway to survive full AWS region failures. This ADR documents why we chose Active-Active over Active-Passive, the technical approach using Route 53 + ECS Fargate + Aurora Global Database, and the trade-offs we accepted.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Context: Why This Decision Matters
&lt;/h2&gt;

&lt;p&gt;Our payment gateway is a critical-path service. A prolonged outage — especially a full AWS region failure — directly impacts revenue and customer trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our posture at the time:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Largely single-region&lt;/li&gt;
&lt;li&gt;Reactive: we recover from incidents, but aren't designed to withstand a full regional failure without manual intervention&lt;/li&gt;
&lt;li&gt;Production in one region, staging in another&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we needed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Payment gateway stays available during an AWS regional outage&lt;/li&gt;
&lt;li&gt;Recovery time and data loss reduced to defined RTO/RPO targets&lt;/li&gt;
&lt;li&gt;Improved latency by serving users from geographically closer regions&lt;/li&gt;
&lt;li&gt;Fits within our existing tooling: Terraform/Terragrunt, ECS, Aurora&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. The Two Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: Active-Passive (Disaster Recovery)
&lt;/h3&gt;

&lt;p&gt;A secondary region exists but is idle or minimally used. Failover is manual or semi-automated.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lower cost (secondary region is idle)&lt;/td&gt;
&lt;td&gt;Downtime during failover (minutes to hours)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simpler to implement&lt;/td&gt;
&lt;td&gt;Manual intervention required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Less operational complexity&lt;/td&gt;
&lt;td&gt;Risk of "cold start" failures in untested DR region&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Option B: Active-Active (High Availability) ← Chosen
&lt;/h3&gt;

&lt;p&gt;Multiple regions actively serve traffic. If one region fails, others continue serving with minimal or no downtime.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minimal/zero downtime on failover&lt;/td&gt;
&lt;td&gt;Higher cost (duplicate resources)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lower latency (serve from nearest region)&lt;/td&gt;
&lt;td&gt;Increased operational complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protection against full regional failures&lt;/td&gt;
&lt;td&gt;Requires stronger CI/CD and observability discipline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Decision:&lt;/strong&gt; Given the criticality of the payment gateway and the business impact of downtime, we chose &lt;strong&gt;Active-Active&lt;/strong&gt; despite its higher complexity and cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Architecture Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Traffic Routing — Route 53
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency-Based Routing&lt;/strong&gt; for the payment gateway's public API DNS record&lt;/li&gt;
&lt;li&gt;Each region exposes an independent endpoint (ALB) registered under the same DNS name&lt;/li&gt;
&lt;li&gt;Route 53 directs clients to the lowest-latency region&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checks&lt;/strong&gt; against a regional health endpoint (&lt;code&gt;/healthz&lt;/code&gt;) detect regional or service failure&lt;/li&gt;
&lt;li&gt;Automatic failover: 100% of traffic routes to the surviving region when one is marked unhealthy&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Compute Layer — ECS Fargate
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identical ECS clusters and services&lt;/strong&gt; in each target region&lt;/li&gt;
&lt;li&gt;Terraform/Terragrunt modules parameterized by region ensure infrastructure parity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same container images and versions&lt;/strong&gt; deployed via pinned ECR image digests&lt;/li&gt;
&lt;li&gt;This eliminates the "it works in Region A but not Region B" class of issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.3 Data Layer — Aurora Global Database
&lt;/h3&gt;

&lt;p&gt;This is the hardest part. Payment data must be consistent across regions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aurora Global Database&lt;/strong&gt; with a single primary write region&lt;/li&gt;
&lt;li&gt;One or more secondary read replicas in other regions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write forwarding&lt;/strong&gt; from secondary regions: reads are local, writes are forwarded to primary&lt;/li&gt;
&lt;li&gt;Replication lag: typically &amp;lt;1 second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RTO/RPO Targets:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RTO (Recovery Time Objective)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 minute&lt;/td&gt;
&lt;td&gt;Route 53 health checks + automatic failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPO (Recovery Point Objective)&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 second&lt;/td&gt;
&lt;td&gt;Aurora Global Database replication lag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  4. Key Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Not Active-Passive?
&lt;/h3&gt;

&lt;p&gt;For a payment gateway, even 5 minutes of downtime during manual failover is unacceptable. Active-Passive also carries the risk of "cold start" — the DR region hasn't been serving real traffic, so untested configurations, expired credentials, or capacity issues surface at the worst possible moment.&lt;/p&gt;

&lt;p&gt;Active-Active eliminates this: both regions are always warm, always serving traffic, always tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Aurora Global Database Over DynamoDB Global Tables?
&lt;/h3&gt;

&lt;p&gt;Our application is built on relational data models (MySQL/Aurora). Migrating to DynamoDB would require rewriting core application logic. Aurora Global Database provides multi-region replication without changing the data layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Latency-Based Routing Over Failover Routing?
&lt;/h3&gt;

&lt;p&gt;Latency-based routing provides &lt;strong&gt;two benefits&lt;/strong&gt;: performance (lowest latency) AND resilience (automatic failover via health checks). Pure failover routing only provides the second.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Implementation Phases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Database Replication (Foundation)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Set up Aurora Global Database with secondary cluster&lt;/li&gt;
&lt;li&gt;Validate replication lag and write forwarding behavior&lt;/li&gt;
&lt;li&gt;Run DR drills to measure actual RTO/RPO&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 2: Compute Symmetry
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deploy identical ECS services in secondary region via Terraform modules&lt;/li&gt;
&lt;li&gt;Validate container image parity across regions&lt;/li&gt;
&lt;li&gt;Configure ALBs and health check endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 3: Traffic Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Configure Route 53 latency-based routing&lt;/li&gt;
&lt;li&gt;Set up health checks with appropriate thresholds&lt;/li&gt;
&lt;li&gt;Gradual traffic shifting (10% → 50% → 100%)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 4: Operational Readiness
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Quarterly DR drills (mandatory)&lt;/li&gt;
&lt;li&gt;Cross-region observability (New Relic dashboards per region)&lt;/li&gt;
&lt;li&gt;Runbooks for manual failover scenarios&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Trade-offs We Accepted
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Higher Cost
&lt;/h3&gt;

&lt;p&gt;Duplicate compute and database resources across regions. We estimated roughly 1.5–1.8x the single-region cost (not 2x, because the secondary region handles real traffic that would otherwise need scaling in the primary).&lt;/p&gt;

&lt;h3&gt;
  
  
  Increased Complexity
&lt;/h3&gt;

&lt;p&gt;More moving parts = more potential failure modes. We mitigated this with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure as Code (Terraform) for consistency&lt;/li&gt;
&lt;li&gt;Pinned image digests for deployment determinism&lt;/li&gt;
&lt;li&gt;Regional health dashboards for visibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Write Forwarding Latency
&lt;/h3&gt;

&lt;p&gt;Writes from the secondary region incur cross-region latency (typically 30–100ms). For a payment gateway, this is acceptable — payment processing already involves multiple external API calls with higher latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start DR drills immediately.&lt;/strong&gt; Don't wait until the architecture is "complete." Drill with whatever you have — it exposes assumptions faster than any design review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define RTO/RPO with the business, not engineering.&lt;/strong&gt; The business cares about revenue impact per minute of downtime, not technical recovery metrics. Translate RTO/RPO into dollars.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for split-brain.&lt;/strong&gt; What happens if both regions think they're primary? Aurora Global Database handles this at the data layer, but application-level state (caches, queues) needs explicit handling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Budget for observability.&lt;/strong&gt; Multi-region is only as good as your visibility into it. Without per-region dashboards, you're flying blind.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. When Active-Active Is Overkill
&lt;/h2&gt;

&lt;p&gt;Not every service needs this. Active-Active makes sense when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The service is revenue-critical (payment processing, checkout)&lt;/li&gt;
&lt;li&gt;Downtime is measured in dollars-per-minute&lt;/li&gt;
&lt;li&gt;You have the engineering capacity to maintain two regions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For internal tools, staging environments, or low-criticality services, Active-Passive (or even single-region with good backups) is the right call.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I designed this architecture for a fintech payment gateway processing transactions across Africa. If you're building multi-region systems for financial services, I'd love to exchange notes on the data consistency challenges.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Migrating 500+ AWS Resources from ClickOps to Terraform: A Status Report</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Thu, 02 Jul 2026 18:09:31 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/migrating-500-aws-resources-from-clickops-to-terraform-a-status-report-3c9n</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/migrating-500-aws-resources-from-clickops-to-terraform-a-status-report-3c9n</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I led the migration of a fintech platform's entire AWS infrastructure — IAM roles, ECS services, networking, databases, CI/CD pipelines — from manually created "ClickOps" resources to Terraform-managed Infrastructure as Code. Here's what worked, what broke, and the framework I used to import 500+ existing resources without downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We Migrated
&lt;/h2&gt;

&lt;p&gt;Our infrastructure was created by clicking through the AWS Console over 3+ years. It worked, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No reproducibility.&lt;/strong&gt; If a region went down, we couldn't recreate the environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audit trail.&lt;/strong&gt; Who changed that security group rule? When? Why? Nobody knew.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration drift.&lt;/strong&gt; "Production" and "staging" had diverged in undocumented ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster recovery was impossible.&lt;/strong&gt; Without IaC, spinning up a new region meant weeks of manual work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-region architecture requires IaC.&lt;/strong&gt; Our Active-Active strategy was dead on arrival without Terraform.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Scale of the Problem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Resource Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM Roles &amp;amp; Policies&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS Services&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 Instances&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Groups&lt;/td&gt;
&lt;td&gt;30+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balancers&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS Databases&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Buckets&lt;/td&gt;
&lt;td&gt;15+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD Pipelines&lt;/td&gt;
&lt;td&gt;20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Functions&lt;/td&gt;
&lt;td&gt;15+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route 53 Records&lt;/td&gt;
&lt;td&gt;50+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Two regions:&lt;/strong&gt; Production in eu-west-2 (London), Staging in us-east-2 (Ohio).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Migration Framework
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Resource Inventory
&lt;/h3&gt;

&lt;p&gt;Before writing a single line of Terraform, I cataloged every AWS resource. I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI commands to list resources by service&lt;/li&gt;
&lt;li&gt;AWS Config for resource inventory&lt;/li&gt;
&lt;li&gt;Manual console review for resources that don't appear in standard APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Bulk Import with Terraformer
&lt;/h3&gt;

&lt;p&gt;For the initial heavy lifting, I used &lt;strong&gt;Terraformer&lt;/strong&gt; to pull existing resource configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraformer import aws &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;iam,ec2,ecs,alb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--regions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eu-west-2,us-east-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates &lt;code&gt;.tf&lt;/code&gt; files and &lt;code&gt;.tfstate&lt;/code&gt; that represent your current infrastructure. It's a massive time-saver but the output needs significant cleanup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Cleanup &amp;amp; Restructuring
&lt;/h3&gt;

&lt;p&gt;Terraformer's output is verbose and flat. I restructured it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed redundant auto-generated arguments and defaults&lt;/li&gt;
&lt;li&gt;Moved IAM policies into &lt;code&gt;policies/*.json&lt;/code&gt; files and referenced them via &lt;code&gt;locals&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Organized configurations into service-specific modules&lt;/li&gt;
&lt;li&gt;Standardized naming conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Manual Imports
&lt;/h3&gt;

&lt;p&gt;For resources that needed precision or were missed in bulk imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform import aws_iam_role.my_role my_role_name
terraform import aws_ecs_service.my_service my-cluster/my-service
terraform import aws_security_group.my_sg sg-xxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: State Verification
&lt;/h3&gt;

&lt;p&gt;The most critical step: &lt;strong&gt;&lt;code&gt;terraform plan&lt;/code&gt; must show no changes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the plan wants to destroy and recreate resources, the configuration doesn't match reality. I iterated until the plan was clean — no deletions, no replacements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Remote State Backend
&lt;/h3&gt;

&lt;p&gt;Migrated &lt;code&gt;.tfstate&lt;/code&gt; to S3 with DynamoDB locking:&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="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"env/production/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks"&lt;/span&gt;
  &lt;span class="p"&gt;}&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;Critical protections:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 versioning enabled (rollback state if something goes wrong)&lt;/li&gt;
&lt;li&gt;AES256 encryption at rest&lt;/li&gt;
&lt;li&gt;DynamoDB locking prevents concurrent &lt;code&gt;terraform apply&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Public access blocked on the bucket&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.terraform&lt;/code&gt; directory excluded from Git&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Migration Status by Service
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Completed ✅
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AWS Service&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM&lt;/td&gt;
&lt;td&gt;Roles, policies, instance profiles&lt;/td&gt;
&lt;td&gt;Standardized into reusable modules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Groups&lt;/td&gt;
&lt;td&gt;All production + staging&lt;/td&gt;
&lt;td&gt;Per-service isolation enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC &amp;amp; Networking&lt;/td&gt;
&lt;td&gt;Subnets, route tables, gateways&lt;/td&gt;
&lt;td&gt;Private subnet architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  In Progress ⏳
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AWS Service&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ECS&lt;/td&gt;
&lt;td&gt;Services, task definitions, clusters&lt;/td&gt;
&lt;td&gt;Complex due to frequent deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALB&lt;/td&gt;
&lt;td&gt;Load balancers, target groups, listeners&lt;/td&gt;
&lt;td&gt;Dependency on ECS completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACM&lt;/td&gt;
&lt;td&gt;Certificates&lt;/td&gt;
&lt;td&gt;Cutover risk — DNS validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route 53&lt;/td&gt;
&lt;td&gt;DNS records&lt;/td&gt;
&lt;td&gt;Cutover risk — must be atomic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Pending 📋
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AWS Service&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RDS&lt;/td&gt;
&lt;td&gt;Databases&lt;/td&gt;
&lt;td&gt;Snapshot-first approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;td&gt;Buckets&lt;/td&gt;
&lt;td&gt;Encryption + versioning policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CodePipeline&lt;/td&gt;
&lt;td&gt;CI/CD pipelines&lt;/td&gt;
&lt;td&gt;Artifact bucket dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CodeBuild&lt;/td&gt;
&lt;td&gt;Build projects&lt;/td&gt;
&lt;td&gt;IAM role dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECR&lt;/td&gt;
&lt;td&gt;Container registry&lt;/td&gt;
&lt;td&gt;Lifecycle rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch&lt;/td&gt;
&lt;td&gt;Logs, metrics, alarms&lt;/td&gt;
&lt;td&gt;Retention policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;td&gt;Functions&lt;/td&gt;
&lt;td&gt;Event source mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNS&lt;/td&gt;
&lt;td&gt;Notifications&lt;/td&gt;
&lt;td&gt;Slack/email integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What Broke (And How I Fixed It)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem 1: State Drift After Manual Changes
&lt;/h3&gt;

&lt;p&gt;Someone modified a security group via the Console after it was imported into Terraform. Next &lt;code&gt;terraform plan&lt;/code&gt; wanted to revert the change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Establish a rule: &lt;strong&gt;once a resource is in Terraform, the Console is read-only.&lt;/strong&gt; All changes go through code → PR → apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: ECS Task Definitions Are Append-Only
&lt;/h3&gt;

&lt;p&gt;ECS task definitions create new revisions on every change. Terraform wants to manage a specific revision, but ECS services reference the "latest" revision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;ignore_changes&lt;/code&gt; for task definition in the ECS service resource, and manage task definitions separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: Import Order Matters
&lt;/h3&gt;

&lt;p&gt;Importing an ALB listener rule before the ALB itself causes dependency errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Build a dependency graph and import in order: VPC → Subnets → Security Groups → ALB → Target Groups → Listener Rules → ECS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 4: The "Phantom Diff" Problem
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; showed changes for arguments that were set to AWS defaults. Terraformer exports everything, including defaults that Terraform would normally infer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Remove explicit default values from the HCL. If Terraform's default matches AWS's default, don't set it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Module Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── backend.tf                 # S3 + DynamoDB backend config
├── .terraform.lock.hcl        # Provider version lock
├── environments/
│   ├── production/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── staging/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
├── modules/
│   ├── iam/
│   ├── networking/
│   ├── ecs/
│   ├── alb/
│   ├── rds/
│   └── security/
└── policies/
    ├── codebuild-policy.json
    ├── codepipeline-policy.json
    └── ecs-execution-policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate state files per environment (production and staging can be managed independently)&lt;/li&gt;
&lt;li&gt;Shared modules parameterized by environment&lt;/li&gt;
&lt;li&gt;IAM policies as JSON files referenced by &lt;code&gt;locals&lt;/code&gt; (easier to review and audit)&lt;/li&gt;
&lt;li&gt;Standard tagging enforced via module defaults&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tagging Strategy
&lt;/h2&gt;

&lt;p&gt;Every resource gets these tags (enforced by Terraform):&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="nx"&gt;tags&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&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="c1"&gt;# production, staging&lt;/span&gt;
  &lt;span class="nx"&gt;Service&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;service_name&lt;/span&gt;   &lt;span class="c1"&gt;# payment-service, auth-service&lt;/span&gt;
  &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&lt;/span&gt;        &lt;span class="c1"&gt;# distinguishes IaC from ClickOps&lt;/span&gt;
  &lt;span class="nx"&gt;Owner&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;team&lt;/span&gt;           &lt;span class="c1"&gt;# platform, backend, frontend&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ManagedBy = terraform&lt;/code&gt; tag is crucial. It instantly tells you whether a resource is safe to modify via Console (if it's not tagged) or must be changed via code (if it is).&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Import before modify.&lt;/strong&gt; Never recreate a production resource. Always import first, verify &lt;code&gt;plan&lt;/code&gt; is clean, then start making changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with IAM.&lt;/strong&gt; Everything depends on IAM. Get roles and policies into Terraform first — they're the foundation for every other resource.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Separate state per environment.&lt;/strong&gt; A single state file for production + staging is a disaster waiting to happen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for the long tail.&lt;/strong&gt; The first 80% of resources are fast. The last 20% (Lambda event sources, CloudWatch alarms, SNS topics) take as long as the first 80%.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document what's NOT migrated.&lt;/strong&gt; Maintain a clear list of resources still managed via Console. This prevents the "is this in Terraform?" question.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Impact
&lt;/h2&gt;

&lt;p&gt;Once complete, this migration enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disaster recovery:&lt;/strong&gt; Spin up a new region from code in hours, not weeks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-region architecture:&lt;/strong&gt; Active-Active requires identical infrastructure in both regions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail:&lt;/strong&gt; Every change is a Git commit with a PR, reviewer, and timestamp&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance:&lt;/strong&gt; PCI DSS requires documented, repeatable infrastructure processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding:&lt;/strong&gt; New engineers can understand the infrastructure by reading code&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Migrating from ClickOps to Terraform isn't glamorous, but it's the foundation that makes everything else possible — multi-region, DR, compliance, and team scale. If you're staring at a Console full of manually created resources, start with IAM and work outward.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How I Built a DevOps Handover Document That Could Run Without Me.</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Thu, 02 Jul 2026 17:54:47 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/how-i-built-a-devops-handover-document-that-could-run-without-me-2mom</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/how-i-built-a-devops-handover-document-that-could-run-without-me-2mom</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; When I left my role as the sole DevOps/Platform Engineer for a fintech payment gateway, I created a handover document that could orient any engineer — from "where are the logs?" to "how do we fail over to another region?" — in under 60 seconds. Here's the framework I used and why most handover docs fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Most Handovers
&lt;/h2&gt;

&lt;p&gt;I've seen handover documents that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A brain dump of links with no context&lt;/li&gt;
&lt;li&gt;A 50-page novel nobody reads&lt;/li&gt;
&lt;li&gt;A set of credentials with zero operational guidance&lt;/li&gt;
&lt;li&gt;A Confluence page last updated 6 months ago&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these help the engineer at 2 AM when the payment gateway is down and you're unreachable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My design principle:&lt;/strong&gt; If I get hit by a bus tomorrow, can someone who's never seen this infrastructure keep the platform stable, deployable, observable, secure, and cost-controlled?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Framework: 6 Domain Runbooks + 1 Quick Start
&lt;/h2&gt;

&lt;p&gt;I structured the handover as a &lt;strong&gt;single entry point page&lt;/strong&gt; with drill-down links to domain-specific runbooks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 60-Second Quick Start
&lt;/h3&gt;

&lt;p&gt;At the top of the page, in a highlighted box:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you are new or taking over in an emergency, read this first.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core ownership:&lt;/strong&gt; AWS infrastructure, ECS services, CI/CD pipelines, security posture, observability, Terraform migration, and cost optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary environments:&lt;/strong&gt; Staging in us-east-2; production in eu-west-2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How code reaches production:&lt;/strong&gt; Source control → CodePipeline → CodeBuild → ECR → ECS service deployment behind ALB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When something breaks:&lt;/strong&gt; Check monitoring first, confirm the latest deployment, inspect ECS service events/ALB target health, then follow the Incident Ops Runbook&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top current risks:&lt;/strong&gt; Incomplete Terraform migration, shared security groups/IAM roles, remaining observability gaps&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;This is the most important section.&lt;/strong&gt; Everything else is a drill-down.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 6 Domain Runbooks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Platform &amp;amp; Infrastructure Runbook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; AWS architecture, ECS clusters, EC2 fleet, networking, security groups&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architecture diagrams (what lives where)&lt;/li&gt;
&lt;li&gt;Service maps: ECS cluster → services → load balancers → domains&lt;/li&gt;
&lt;li&gt;EC2 instance inventory with IPs, types, and purposes&lt;/li&gt;
&lt;li&gt;Security group matrix: which ports, which sources, which services&lt;/li&gt;
&lt;li&gt;How to add a new service, resize an instance, or modify networking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Critical detail:&lt;/strong&gt; I included a service map that mapped every ECS service to its load balancer, domain path, CI/CD pipeline, and operational status. When someone asks "what handles &lt;code&gt;/checkout/*&lt;/code&gt;?", the answer is one table lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CI/CD &amp;amp; Release Engineering Runbook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; CodePipeline, CodeBuild, ECR, deployment flow&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;End-to-end pipeline flow: source → build → test → deploy&lt;/li&gt;
&lt;li&gt;Buildspec template with SonarQube integration&lt;/li&gt;
&lt;li&gt;How to create a new pipeline for a new service&lt;/li&gt;
&lt;li&gt;How to roll back a bad deployment&lt;/li&gt;
&lt;li&gt;Common failure patterns and fixes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Critical detail:&lt;/strong&gt; Rolling back is ECS is simple — select the prior task definition revision and force a new deployment. But this needs to be documented, not assumed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Observability &amp;amp; Monitoring Runbook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; New Relic, CloudWatch, alerting, dashboards&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's monitored and what isn't (the gaps are as important as the coverage)&lt;/li&gt;
&lt;li&gt;How to instrument a new service&lt;/li&gt;
&lt;li&gt;Alert routing: which alerts go where, who responds&lt;/li&gt;
&lt;li&gt;Dashboard locations for each environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Security &amp;amp; Compliance Runbook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; IAM, security groups, GuardDuty, WAF, encryption&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current security posture (honest assessment of gaps)&lt;/li&gt;
&lt;li&gt;Remediation plan and priority matrix&lt;/li&gt;
&lt;li&gt;How to rotate credentials&lt;/li&gt;
&lt;li&gt;Incident response procedures&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. FinOps &amp;amp; Cost Management Runbook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; AWS billing, optimization strategies, governance&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current monthly spend and breakdown by service&lt;/li&gt;
&lt;li&gt;Active cost optimizations (staging shutdown schedules, reserved instances)&lt;/li&gt;
&lt;li&gt;Budget alerts and thresholds&lt;/li&gt;
&lt;li&gt;How to investigate a cost spike&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Terraform/IaC Runbook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covers:&lt;/strong&gt; Terraform state, modules, migration status&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key content:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's in Terraform and what's still ClickOps&lt;/li&gt;
&lt;li&gt;State file locations and backend config&lt;/li&gt;
&lt;li&gt;How to import a new resource&lt;/li&gt;
&lt;li&gt;How to plan and apply safely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Responsibility Matrix (RACI-style)
&lt;/h2&gt;

&lt;p&gt;This is the section most handovers miss. It's not enough to document &lt;em&gt;what&lt;/em&gt; — you need to document &lt;em&gt;who&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I created a table mapping each domain to:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Primary Owner&lt;/th&gt;
&lt;th&gt;Backup&lt;/th&gt;
&lt;th&gt;When to Escalate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform &amp;amp; Infra&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;Any production service instability, scaling issues, VPC/SG changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;Pipeline failures lasting &amp;gt;30min, new service onboarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;GuardDuty critical finding, suspected breach, credential exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;Monitoring gaps, alert fatigue, instrumentation requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FinOps&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;30%+ cost increase, daily spend spike, commitment purchases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;State corruption, import failures, drift detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incident Command&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;[Assign]&lt;/td&gt;
&lt;td&gt;Any customer-impacting issue, suspected production instability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Critical rule:&lt;/strong&gt; No domain should have a single named operator. Before the handover is considered complete, every domain needs at least one backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Failure Patterns (Cheat Sheet)
&lt;/h2&gt;

&lt;p&gt;I included a quick-reference troubleshooting table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check First&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ALB health check failing&lt;/td&gt;
&lt;td&gt;Health path, container port, target group port, SG rules&lt;/td&gt;
&lt;td&gt;Fix path or port mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SonarQube gate failing&lt;/td&gt;
&lt;td&gt;Token, project key, coverage threshold&lt;/td&gt;
&lt;td&gt;Rotate token, verify key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image mismatch&lt;/td&gt;
&lt;td&gt;ECR pushed tag vs. ECS task definition image URI&lt;/td&gt;
&lt;td&gt;Update task definition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task crash loop&lt;/td&gt;
&lt;td&gt;App logs, env vars, secrets access, CPU/memory limits&lt;/td&gt;
&lt;td&gt;Fix config, increase resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost spike&lt;/td&gt;
&lt;td&gt;Cost Explorer by service, check for zombie resources&lt;/td&gt;
&lt;td&gt;Terminate unused resources&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Known Gaps and Backlog
&lt;/h2&gt;

&lt;p&gt;The most honest section of any handover: &lt;strong&gt;what's not done.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I listed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform migration: pending services and resources&lt;/li&gt;
&lt;li&gt;Security: remaining shared security groups and IAM roles&lt;/li&gt;
&lt;li&gt;Observability: services not yet instrumented&lt;/li&gt;
&lt;li&gt;FinOps: Graviton migration candidates not yet evaluated&lt;/li&gt;
&lt;li&gt;Automation: edge cases in reconciliation system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; The person taking over needs to know where the landmines are, not just where the paved roads go.&lt;/p&gt;




&lt;h2&gt;
  
  
  Linked Source Pages
&lt;/h2&gt;

&lt;p&gt;Every claim in the handover links to its authoritative source page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EC2 Infrastructure Risk Assessment&lt;/li&gt;
&lt;li&gt;ECS Security Assessment Reports (per cluster)&lt;/li&gt;
&lt;li&gt;Complete CI/CD Pipeline Setup Guide&lt;/li&gt;
&lt;li&gt;Deployment Architecture Case Study&lt;/li&gt;
&lt;li&gt;SonarQube + CI/CD Integration Standard&lt;/li&gt;
&lt;li&gt;AWS Cost Optimization Initiative&lt;/li&gt;
&lt;li&gt;Cost Optimization Strategy ($3K Reduction Plan)&lt;/li&gt;
&lt;li&gt;Terraform Migration Status Report&lt;/li&gt;
&lt;li&gt;Multi-Region Architecture Decision Record&lt;/li&gt;
&lt;li&gt;ALB Health Check Troubleshooting Guide&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Makes This Different
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's a living document.&lt;/strong&gt; Not a one-time brain dump. The handover page links to source pages that are independently maintained.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It starts with the emergency.&lt;/strong&gt; The 60-second quick start is for the 2 AM incident. The domain runbooks are for the Tuesday morning onboarding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's honest about gaps.&lt;/strong&gt; Most handovers present a rosy picture. Mine included a "Known Gaps and Backlog" section because the person taking over needs to know what's fragile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It separates "what" from "who."&lt;/strong&gt; Documentation without ownership assignment is just a wiki page. The RACI matrix ensures every domain has a human responsible for it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It includes the "why."&lt;/strong&gt; Not just "we use SonarQube" but "we use SonarQube because we needed a SAST tool that scales on LOC-based pricing for a growing Java team in a cost-sensitive fintech."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Template
&lt;/h2&gt;

&lt;p&gt;If you're building your own handover, here's the skeleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# DevOps/Cloud Handover — [Your Name]&lt;/span&gt;

&lt;span class="gu"&gt;## TL;DR — Platform in 60 Seconds&lt;/span&gt;
[5-bullet emergency orientation]

&lt;span class="gu"&gt;## Responsibility Matrix&lt;/span&gt;
[Domain → Owner → Backup → Escalation triggers]

&lt;span class="gu"&gt;## Domain Runbooks&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Platform &amp;amp; Infrastructure
&lt;span class="p"&gt;2.&lt;/span&gt; CI/CD &amp;amp; Release Engineering
&lt;span class="p"&gt;3.&lt;/span&gt; Observability &amp;amp; Monitoring
&lt;span class="p"&gt;4.&lt;/span&gt; Security &amp;amp; Compliance
&lt;span class="p"&gt;5.&lt;/span&gt; FinOps &amp;amp; Cost Management
&lt;span class="p"&gt;6.&lt;/span&gt; Infrastructure as Code

&lt;span class="gu"&gt;## Common Failure Patterns&lt;/span&gt;
[Symptom → Check → Fix table]

&lt;span class="gu"&gt;## Known Gaps and Backlog&lt;/span&gt;
[Honest list of what's not done]

&lt;span class="gu"&gt;## References&lt;/span&gt;
[Links to all authoritative source pages]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;I built this handover when leaving my role as sole DevOps/Platform Engineer for a fintech payment gateway. The test of a good handover isn't whether it's comprehensive — it's whether the person reading it at 2 AM can keep the platform running. Design for the emergency first, then add depth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>devops</category>
      <category>productivity</category>
      <category>sre</category>
    </item>
    <item>
      <title>End-to-End CI/CD on AWS: From Bitbucket to ECS in 10 Steps</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Thu, 02 Jul 2026 17:28:50 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/end-to-end-cicd-on-aws-from-bitbucket-to-ecs-in-10-steps-54kp</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/end-to-end-cicd-on-aws-from-bitbucket-to-ecs-in-10-steps-54kp</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A complete, step-by-step guide to building a production-grade CI/CD pipeline on AWS — from source code in Bitbucket through CodeBuild and CodePipeline to a running ECS Fargate service. I built this for a fintech platform running 50+ microservices. Here's everything I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Bitbucket Repo]
       │
       ▼ (CodeStar Connection)
[AWS CodePipeline]
       │
       ├── Stage 1: Source
       │     └── Fetch code, store as ZIP in S3
       │
       ├── Stage 2: Build (CodeBuild)
       │     ├── Maven build → JAR
       │     ├── Docker build → Image
       │     ├── ECR push → Registry
       │     └── Generate imagedefinitions.json
       │
       └── Stage 3: Deploy (ECS)
             └── Rolling update → Zero downtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Java/Spring Boot, Maven, Docker, AWS (CodePipeline, CodeBuild, ECR, ECS Fargate, S3, IAM)&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Create ECR Repository
&lt;/h2&gt;

&lt;p&gt;ECR stores your Docker images. Every service gets its own repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr create-repository &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; my-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;YOUR_REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-scanning-configuration&lt;/span&gt; &lt;span class="nv"&gt;scanOnPush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;repositoryUri&lt;/code&gt; — you'll need it throughout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; Enable &lt;code&gt;scanOnPush=true&lt;/code&gt; to automatically scan images for CVEs when they're pushed. Free security.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Create S3 Artifact Bucket
&lt;/h2&gt;

&lt;p&gt;CodePipeline needs a bucket to store artifacts between stages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 mb s3://codepipeline-&amp;lt;YOUR_REGION&amp;gt;-&amp;lt;YOUR_ACCOUNT_ID&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;YOUR_REGION&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Block all public access (default). This bucket should never be public.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Create IAM Roles
&lt;/h2&gt;

&lt;p&gt;You need three roles. This is where most people get stuck.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1: CodePipeline Service Role
&lt;/h3&gt;

&lt;p&gt;Trust policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codepipeline.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions: S3 (read/write artifacts), CodeBuild (start builds), ECS (deploy), CodeStar Connections (Bitbucket access).&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2: CodeBuild Service Role
&lt;/h3&gt;

&lt;p&gt;Trust policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codebuild.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions: ECR (push images), S3 (read/write artifacts), CloudWatch Logs (build logs), Secrets Manager (if using SonarQube or other secrets).&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3: ECS Task Execution Role
&lt;/h3&gt;

&lt;p&gt;This is what ECS uses to pull images and write logs:&lt;/p&gt;

&lt;p&gt;Permissions: ECR (pull images), CloudWatch Logs (write container logs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical lesson:&lt;/strong&gt; Follow least-privilege. Don't use &lt;code&gt;*&lt;/code&gt; resources in production IAM policies. Scope to specific ARNs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Create the Buildspec
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;buildspec.yml&lt;/code&gt; lives in your repo root and tells CodeBuild what to do:&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;

&lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pre_build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Logging in to Amazon ECR...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com&lt;/span&gt;

  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Build started on `date`&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG&lt;/span&gt;

  &lt;span class="na"&gt;post_build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Build completed on `date`&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;printf '[{"name":"my-container","imageUri":"%s"}]' $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG &amp;gt; imagedefinitions.json&lt;/span&gt;

&lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;imagedefinitions.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key environment variables&lt;/strong&gt; (set in CodeBuild project):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt;: Your 12-digit account number&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt;: Target region&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IMAGE_REPO_NAME&lt;/code&gt;: Must match ECR repository name exactly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IMAGE_TAG&lt;/code&gt;: Usually &lt;code&gt;latest&lt;/code&gt; for staging, commit SHA for production&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 5: Write the Dockerfile
&lt;/h2&gt;

&lt;p&gt;For Java/Spring Boot services, use a multi-stage build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;maven:3.9-amazoncorretto-17&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; pom.xml .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src ./src&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;mvn clean package &lt;span class="nt"&gt;-DskipTests&lt;/span&gt;

&lt;span class="c"&gt;# Stage 2: Runtime&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; amazoncorretto:17-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/target/*.jar app.jar&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["java", "-jar", "app.jar"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Critical lessons from production:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Java version must match pom.xml.&lt;/strong&gt; Using a Java 8 base image with a Spring Boot 3.x project will silently fail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-module builds need the root pom.&lt;/strong&gt; If your service depends on internal modules, copy ALL module directories and build from root.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pin your base images.&lt;/strong&gt; &lt;code&gt;amazoncorretto:17-alpine&lt;/code&gt; not &lt;code&gt;amazoncorretto:latest&lt;/code&gt;. Unpinned tags cause "works on my machine" bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Platform matters.&lt;/strong&gt; If you build on an M1/M2 Mac, the image is ARM. ECS Fargate (usually x86) will crash. Always use &lt;code&gt;--platform linux/amd64&lt;/code&gt; in your build.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 6: Create ECS Task Definition
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs register-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--family&lt;/span&gt; my-service-task &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-mode&lt;/span&gt; awsvpc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--requires-compatibilities&lt;/span&gt; FARGATE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 512 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1024 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--execution-role-arn&lt;/span&gt; arn:aws:iam::&amp;lt;ACCOUNT_ID&amp;gt;:role/ecsTaskExecutionRole &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--container-definitions&lt;/span&gt; &lt;span class="s1"&gt;'[{
    "name": "my-container",
    "image": "&amp;lt;ACCOUNT_ID&amp;gt;.dkr.ecr.&amp;lt;REGION&amp;gt;.amazonaws.com/my-service:latest",
    "portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/my-service",
        "awslogs-region": "&amp;lt;REGION&amp;gt;",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 7: Create ECS Service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecs create-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; my-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; my-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--task-definition&lt;/span&gt; my-service-task &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--desired-count&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--launch-type&lt;/span&gt; FARGATE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-configuration&lt;/span&gt; &lt;span class="s2"&gt;"awsvpcConfiguration={subnets=[&amp;lt;SUBNET_1&amp;gt;,&amp;lt;SUBNET_2&amp;gt;],securityGroups=[&amp;lt;SG_ID&amp;gt;],assignPublicIp=ENABLED}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 8: Create CodeBuild Project
&lt;/h2&gt;

&lt;p&gt;Configure in the Console or CLI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; Bitbucket (via CodeStar Connection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment:&lt;/strong&gt; Amazon Linux 2023, Standard image&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buildspec:&lt;/strong&gt; Use the &lt;code&gt;buildspec.yml&lt;/code&gt; from the repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service role:&lt;/strong&gt; The CodeBuild role from Step 3&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 9: Create CodePipeline
&lt;/h2&gt;

&lt;p&gt;Wire everything together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source stage:&lt;/strong&gt; Bitbucket → CodeStar Connection → triggers on push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build stage:&lt;/strong&gt; CodeBuild project from Step 8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy stage:&lt;/strong&gt; Amazon ECS → select your cluster and service&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 10: Deployment Strategy — Rolling Updates
&lt;/h2&gt;

&lt;p&gt;ECS uses rolling updates by default:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ECS spins up new containers with the new image&lt;/li&gt;
&lt;li&gt;New containers must pass ALB health checks&lt;/li&gt;
&lt;li&gt;Once healthy, the load balancer drains traffic from old containers&lt;/li&gt;
&lt;li&gt;Old containers are terminated&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Zero downtime.&lt;/strong&gt; If the new containers fail health checks, the old ones keep running.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Rollback
&lt;/h3&gt;

&lt;p&gt;If something goes wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to ECS Service → Update Service&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;previous Task Definition revision&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Check "Force new deployment"&lt;/li&gt;
&lt;li&gt;ECS reverts to the last known-good version&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Common Failure Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline stuck at Source&lt;/td&gt;
&lt;td&gt;Bitbucket connection expired&lt;/td&gt;
&lt;td&gt;Re-authorize in Settings → Connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build fails (Maven)&lt;/td&gt;
&lt;td&gt;Dependency or syntax error&lt;/td&gt;
&lt;td&gt;Check CodeBuild logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build fails (Docker)&lt;/td&gt;
&lt;td&gt;Java version mismatch or missing modules&lt;/td&gt;
&lt;td&gt;Match pom.xml Java version to Dockerfile base image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy fails (ECS)&lt;/td&gt;
&lt;td&gt;Health check failing&lt;/td&gt;
&lt;td&gt;Verify ALB target group health path matches your app's health endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container crash loops&lt;/td&gt;
&lt;td&gt;Missing env vars or insufficient memory&lt;/td&gt;
&lt;td&gt;Check CloudWatch/New Relic logs, increase CPU/memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image not found&lt;/td&gt;
&lt;td&gt;ECR repo name mismatch&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;IMAGE_REPO_NAME&lt;/code&gt; matches exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Staging vs. Production
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Staging&lt;/th&gt;
&lt;th&gt;Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;us-east-2 (Ohio)&lt;/td&gt;
&lt;td&gt;eu-west-2 (London)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;Auto-deploy on merge to staging branch&lt;/td&gt;
&lt;td&gt;Manual release from console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality Gate&lt;/td&gt;
&lt;td&gt;SonarQube enforced&lt;/td&gt;
&lt;td&gt;SonarQube enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback&lt;/td&gt;
&lt;td&gt;Auto (ECS)&lt;/td&gt;
&lt;td&gt;Manual + stakeholder coordination&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with the IAM roles.&lt;/strong&gt; Every pipeline failure I debugged in the first month was a permissions issue. Get IAM right first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pin container images by digest, not tag.&lt;/strong&gt; &lt;code&gt;:latest&lt;/code&gt; is convenient but non-deterministic. For production, use &lt;code&gt;@sha256:...&lt;/code&gt; to guarantee you're deploying exactly what you tested.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add an approval stage for production.&lt;/strong&gt; A manual approval gate between staging and production prevents accidental deploys.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Integrate SAST from day one.&lt;/strong&gt; Bolting on SonarQube later means retrofitting 30+ buildspecs. Build it into the template from the start.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;This pipeline runs 50+ microservices for a fintech payment gateway. If you're building CI/CD on AWS, especially in regulated industries, feel free to connect — always happy to compare approaches.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From Zero to CI/CD: How I Built and Deployed a Containerized App to Azure</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Thu, 16 Apr 2026 19:36:27 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/from-zero-to-cicd-how-i-built-and-deployed-a-containerized-app-to-azure-k6i</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/from-zero-to-cicd-how-i-built-and-deployed-a-containerized-app-to-azure-k6i</guid>
      <description>&lt;p&gt;Setting up a cloud pipeline often involves connecting several moving parts that must work together perfectly. I recently completed a project where I moved a Python application from a local development environment to a professional cloud hosting setup. The goal was to build a system that was secure, automated, and reliable. Here is a breakdown of how I built it, the technical decisions I made, and the lessons I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Project Overview
&lt;/h2&gt;

&lt;p&gt;I built a web application using the &lt;strong&gt;FastAPI&lt;/strong&gt; framework and hosted it on &lt;strong&gt;Azure Container Apps&lt;/strong&gt;. This service allows the application to run in a managed environment that handles networking and scaling automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Containerization:&lt;/strong&gt; The application is packaged into a standardized unit that includes everything it needs to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code:&lt;/strong&gt; I used Terraform scripts to create all the Azure resources instead of setting them up manually in a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure CI/CD:&lt;/strong&gt; I configured a GitHub Actions pipeline that uses OpenID Connect (OIDC) for authentication, which eliminates the need for long-term passwords.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Interface:&lt;/strong&gt; The application features a black and gold theme for a professional look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live URL:&lt;/strong&gt; &lt;a href="https://lab1-anthony.ashyocean-d74db9c0.westus2.azurecontainerapps.io" rel="noopener noreferrer"&gt;lab1-anthony.ashyocean-d74db9c0.westus2.azurecontainerapps.io&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The live URL may be unavailable if infrastructure has been destroyed to avoid ongoing Azure charges. The entire setup can be recreated with a single &lt;code&gt;terraform apply&lt;/code&gt; command.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdmbpu01fq9gzir9guyrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdmbpu01fq9gzir9guyrg.png" alt=" " width="800" height="758"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xl88hk0bymom9r9ud9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xl88hk0bymom9r9ud9h.png" alt=" " width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The web framework used to build the application.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uvicorn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The ASGI server that handles incoming HTTP requests and passes them to FastAPI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Used to create a consistent environment for the code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure (ACR/ACA)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Used for storing the application images and hosting the live app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terraform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The tool used to define and deploy the cloud infrastructure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The automation tool that deploys code updates.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1: Application Architecture
&lt;/h2&gt;

&lt;p&gt;I designed the application to be configuration-driven. This means it does not have hardcoded names or settings. When the app starts, it retrieves its settings from environment variables provided by the host.&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;get_app_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&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;app_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APP_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cloud Lab Starter App&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;intern_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INTERN_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Replace Me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloud_platform&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLOUD_PLATFORM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Replace Me&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each variable has a fallback default value. If someone clones the repository and runs the application without configuring anything, it still works — it simply displays placeholder text as a hint to update the values.&lt;/p&gt;

&lt;p&gt;This approach allows the same code to run in a local test environment or a production cloud environment without modification. For local testing, I used a &lt;code&gt;.env&lt;/code&gt; file with &lt;code&gt;python-dotenv&lt;/code&gt; that is excluded from the code repository to keep local settings private.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Building the Docker Image
&lt;/h2&gt;

&lt;p&gt;I used Docker to package the application. To ensure the image was secure and efficient, I followed three main practices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-Root User:&lt;/strong&gt; By default, Docker containers run with full administrative privileges. I created a specific "appuser" with limited permissions and no shell access (&lt;code&gt;/bin/false&lt;/code&gt;) to run the application. This ensures that even if the app is compromised, the attacker has very limited access to the system and cannot open an interactive session.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Layer Optimization:&lt;/strong&gt; I structured the Dockerfile to install dependencies before copying the application code. Docker remembers these finished steps, so when I change a line of code, it only updates that specific part. This reduces build times from minutes to seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean Image:&lt;/strong&gt; The &lt;code&gt;.env&lt;/code&gt; file is not copied into the Docker image. All configuration is injected at runtime by the hosting platform. This means the image contains zero sensitive information and can be safely reused across different environments.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app ./app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/false appuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Platform Compatibility
&lt;/h3&gt;

&lt;p&gt;One early lesson was about CPU architecture. My development machine is an Apple Silicon Mac, which uses the ARM architecture. Azure Container Apps requires the &lt;code&gt;linux/amd64&lt;/code&gt; architecture. Without specifying the target platform during the build, Azure would reject the image entirely. The fix was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 &lt;span class="nt"&gt;-t&lt;/span&gt; lab1-starter-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih4eo5kz08t60gyc0l7w.png" alt=" " width="800" height="313"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Step 3: Pushing to Azure Container Registry
&lt;/h2&gt;

&lt;p&gt;Azure Container Registry (ACR) is a private image storage service. Before the application can be deployed, the Docker image must be uploaded to ACR so Azure can pull it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az acr login &lt;span class="nt"&gt;--name&lt;/span&gt; cogneticsregistry
docker tag lab1-starter-app cogneticsregistry.azurecr.io/lab1-starter-app:v1.4
docker push cogneticsregistry.azurecr.io/lab1-starter-app:v1.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;docker tag&lt;/code&gt; step is essential. It embeds the registry address into the image name so Docker knows where to send it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry Permissions
&lt;/h3&gt;

&lt;p&gt;I initially created the ACR with the "ABAC Repository Permissions" mode. This caused persistent authorization failures even after assigning the correct roles. The solution was to recreate the registry with standard RBAC mode, which provided the necessary permissions for image pushes without overcomplicating access control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Infrastructure as Code with Terraform
&lt;/h2&gt;

&lt;p&gt;Instead of manually creating resources in the Azure Portal, I used Terraform. This tool uses code to describe the exact setup required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources Created
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource Group&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The container that holds all Azure resources for the project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure Container Registry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stores the Docker images with admin access enabled.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Log Analytics Workspace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A central place to store and view application logs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container App Environment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The surrounding network and security boundary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container App&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The specific instance where the code runs, pulling the image from ACR.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All resources are fully managed by Terraform. One &lt;code&gt;terraform apply&lt;/code&gt; creates everything, and one &lt;code&gt;terraform destroy&lt;/code&gt; removes everything. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmx784hwgy6ipp2s5zstt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmx784hwgy6ipp2s5zstt.png" alt=" " width="800" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1jke6h7irsd24behh8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1jke6h7irsd24behh8d.png" alt=" " width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Management
&lt;/h3&gt;

&lt;p&gt;Sensitive and personal values are stored in a &lt;code&gt;terraform.tfvars&lt;/code&gt; file that is excluded from the repository. The Terraform code itself uses variable references to keep the deployment clean:&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="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"INTERN_NAME"&lt;/span&gt;
  &lt;span class="nx"&gt;value&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;intern_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The HTTPS Proxy Issue
&lt;/h3&gt;

&lt;p&gt;During this step, I encountered an issue where the application's CSS would not load. Azure Container Apps terminates HTTPS at the load balancer and forwards plain HTTP to the container. FastAPI was generating &lt;code&gt;http://&lt;/code&gt; URLs for static files, which the browser blocked as mixed content.&lt;/p&gt;

&lt;p&gt;The fix was adding &lt;code&gt;--proxy-headers&lt;/code&gt; and &lt;code&gt;--forwarded-allow-ips *&lt;/code&gt; to the Uvicorn startup command. This tells the server to trust the proxy from Azure's load balancer, allowing it to generate the correct HTTPS URLs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Automated Deployment with OIDC
&lt;/h2&gt;

&lt;p&gt;I set up a GitHub Actions pipeline so that every time I push code to the repository, the application is automatically rebuilt and deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1dstnx2ju9dblw65h3b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1dstnx2ju9dblw65h3b.png" alt=" " width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why OIDC Over Stored Credentials
&lt;/h3&gt;

&lt;p&gt;To make this secure, I used &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt;. Traditionally, you would store a client secret in GitHub. With OIDC, GitHub and Azure establish a trust relationship. When a deployment starts, Azure verifies the identity of the GitHub repository and issues a temporary token. This removes the risk of a permanent password being stolen or leaked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpzcfhm29q99h96rx9eyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpzcfhm29q99h96rx9eyf.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Image Tagging Strategy
&lt;/h3&gt;

&lt;p&gt;Instead of manual version tags, the pipeline tags each image with the git commit SHA. This means every deployed version is directly traceable to an exact commit in the code history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problems Encountered and Solutions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker push&lt;/code&gt; failed&lt;/td&gt;
&lt;td&gt;ACR was in ABAC mode&lt;/td&gt;
&lt;td&gt;Switched to standard RBAC mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture mismatch&lt;/td&gt;
&lt;td&gt;Built for ARM (Mac) instead of AMD64&lt;/td&gt;
&lt;td&gt;Used &lt;code&gt;--platform linux/amd64&lt;/code&gt; in build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS not loading&lt;/td&gt;
&lt;td&gt;HTTPS termination at load balancer&lt;/td&gt;
&lt;td&gt;Added &lt;code&gt;--proxy-headers&lt;/code&gt; to Uvicorn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MissingSubscriptionRegistration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provider not registered&lt;/td&gt;
&lt;td&gt;Ran &lt;code&gt;az provider register&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Security and Operational Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Practice&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Restricted User Account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Protects the server by limiting what the app can do.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OIDC Authentication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Replaces permanent passwords with temporary tokens.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure as Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allows for perfectly repeatable deployments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Commit SHA Tagging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Provides a clear audit trail for every deployment.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I Would Do Differently in Production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote Terraform State&lt;/strong&gt; — Store the state file in Azure Blob Storage for team collaboration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate Environments&lt;/strong&gt; — Maintain distinct dev, staging, and production configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Key Vault&lt;/strong&gt; — Store sensitive credentials in a managed vault rather than variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Scanning&lt;/strong&gt; — Use security tools like Trivy to check for vulnerabilities before deploying.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This project demonstrates how to move beyond simply "making an app work" to building a professional deployment system. By focusing on automation and security from the start, I created a workflow that is easy to manage and resistant to common security threats.&lt;/p&gt;

&lt;h3&gt;
  
  
  View the Source Code
&lt;/h3&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/Anthonyuketui/cognetiks-interns-lab1" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;




</description>
      <category>azure</category>
      <category>cicd</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Lambda + ALB 503 Error Debugging Guide</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Tue, 10 Mar 2026 11:12:04 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/lambda-alb-503-error-debugging-guide-30j7</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/lambda-alb-503-error-debugging-guide-30j7</guid>
      <description>&lt;h2&gt;
  
  
  Debugging AWS Lambda + ALB 503 Errors: A Step‑by‑Step Guide to Event Format Mismatches
&lt;/h2&gt;

&lt;p&gt;When I first put an AWS Lambda behind an Application Load Balancer (ALB), everything &lt;em&gt;looked&lt;/em&gt; correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Lambda worked perfectly when tested directly.&lt;/li&gt;
&lt;li&gt;The ALB target group was configured for Lambda.&lt;/li&gt;
&lt;li&gt;Listener rules and routing looked fine.&lt;/li&gt;
&lt;li&gt;Permissions were in place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But calling the Lambda through the ALB kept returning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Meanwhile, the same Lambda worked through a Function URL and API Gateway v2. The root cause turned out to be a subtle &lt;strong&gt;event format mismatch&lt;/strong&gt; between what the Lambda handler expected and what ALB actually sends.&lt;/p&gt;

&lt;p&gt;This post walks through the exact debugging steps, how I identified the mismatch, and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Verify Lambda Function Health
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 2–3 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Test the Lambda function directly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;strong&gt;Function URL&lt;/strong&gt; or the &lt;strong&gt;Lambda console test feature&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm that the function executes successfully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Expected Outcome:&lt;/strong&gt; Lambda works fine in isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If the Lambda works when tested directly, the issue is &lt;strong&gt;in the integration layer&lt;/strong&gt; (ALB), not the Lambda code itself.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Check ALB Target Group Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 5 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Verify your ALB target group.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target type: &lt;code&gt;lambda&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Lambda function registered as target&lt;/li&gt;
&lt;li&gt;Health checks: &lt;strong&gt;disabled&lt;/strong&gt; (normal for Lambda)&lt;/li&gt;
&lt;li&gt;Target status: &lt;code&gt;unavailable&lt;/code&gt; (expected for Lambda targets)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
“Unavailable” status with disabled health checks is &lt;strong&gt;normal for Lambda targets&lt;/strong&gt; — it is &lt;strong&gt;not&lt;/strong&gt; the cause of 503 errors.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Verify ALB Permissions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 3 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Check the Lambda resource policy to ensure ALB can invoke the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda get-policy &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-lambda-function
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ALB has &lt;code&gt;lambda:InvokeFunction&lt;/code&gt; permission.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;ALB ARN&lt;/strong&gt; is present in the policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Missing permissions would usually result in &lt;strong&gt;403&lt;/strong&gt; or &lt;strong&gt;502&lt;/strong&gt; errors, not 503. Still, it’s worth ruling out early.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4: Analyze ALB Listener Rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 10 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Check routing configuration on the ALB listener.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rule priority: &lt;code&gt;155&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Path pattern: &lt;code&gt;/plugin/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Host header: &lt;code&gt;api.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Target group: &lt;code&gt;MY-LAMBDA-TG&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
ALB routing is &lt;strong&gt;strict&lt;/strong&gt; — both &lt;strong&gt;path&lt;/strong&gt; and &lt;strong&gt;host header&lt;/strong&gt; must match exactly. A mismatch here can easily cause requests to hit the wrong target or no target at all.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5: Test with Correct Routing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 5 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Hit the ALB with the correct host and path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Host: api.example.com"&lt;/span&gt; https://my-alb-123456.eu-west-2.elb.amazonaws.com/plugin/test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; In my case, this still returned &lt;code&gt;503&lt;/code&gt;, even though routing looked correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Correct routing alone &lt;strong&gt;does not guarantee success&lt;/strong&gt;. If the Lambda works directly but fails through the ALB, start suspecting an &lt;strong&gt;event format mismatch&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 6: Compare Working vs Non‑Working Scenarios
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 5 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern Recognition:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Function URL: &lt;strong&gt;works&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;API Gateway v2: &lt;strong&gt;works&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;ALB: &lt;strong&gt;fails (503)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The working integrations (Function URL and API Gateway v2) share the &lt;strong&gt;same event format&lt;/strong&gt;, which ALB does &lt;strong&gt;not&lt;/strong&gt; use. That difference becomes important.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 7: Examine the Lambda Handler Signature
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 3 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Inspect the Lambda handler to see what type of event it expects.&lt;/p&gt;

&lt;p&gt;For example (pseudo-steps):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda get-function &lt;span class="nt"&gt;--function-name&lt;/span&gt; my-lambda-function
&lt;span class="c"&gt;# Then inspect / decompile the handler class if needed&lt;/span&gt;
javap &lt;span class="nt"&gt;-cp&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-public&lt;/span&gt; com.example.demo.LambdaRequestHandler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Handler Signature Example (Java):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LambdaRequestHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;APIGatewayV2HTTPEvent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;APIGatewayV2HTTPEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The handler here expects &lt;strong&gt;&lt;code&gt;APIGatewayV2HTTPEvent&lt;/code&gt;&lt;/strong&gt;, which is the event format used by &lt;strong&gt;API Gateway HTTP APIs and Function URLs&lt;/strong&gt; — &lt;strong&gt;not&lt;/strong&gt; the &lt;code&gt;ApplicationLoadBalancerRequestEvent&lt;/code&gt; style payload that ALB sends.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 8: Analyze CloudWatch Logs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 2 minutes  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Look for exceptions in CloudWatch Logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws logs filter-log-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; &lt;span class="s2"&gt;"/aws/lambda/my-lambda-function"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter-pattern&lt;/span&gt; &lt;span class="s2"&gt;"Exception"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-items&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example Error:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NullPointerException: Cannot invoke "String.hashCode()" because "&amp;lt;localX&amp;gt;" is null
    at com.example.demo.LambdaRequestHandler.handleRequest(LambdaRequestHandler.java:77)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Root Cause:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Because the handler expects a specific JSON structure (&lt;code&gt;APIGatewayV2HTTPEvent&lt;/code&gt;), it tries to access fields that &lt;strong&gt;don’t exist&lt;/strong&gt; in the ALB event → resulting in &lt;code&gt;NullPointerException&lt;/code&gt; and ultimately a &lt;strong&gt;503&lt;/strong&gt; from ALB.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 9: Identify the Event Format Mismatch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 2 minutes  &lt;/p&gt;

&lt;p&gt;Let’s compare the incoming event shapes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected by Lambda (API Gateway v2 / Function URL):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routeKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST /plugin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rawPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/plugin/payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"httpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sent by ALB:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"elb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"httpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/plugin/payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The &lt;strong&gt;JSON structure is different&lt;/strong&gt;. Fields your code assumes exist (e.g., &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;routeKey&lt;/code&gt;, &lt;code&gt;rawPath&lt;/code&gt;) are not present in the ALB event. Accessing them directly leads to &lt;code&gt;NullPointerException&lt;/code&gt; and a 503 at the ALB level.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 10: Verify with Test Events
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Estimated Time:&lt;/strong&gt; 3 minutes  &lt;/p&gt;

&lt;p&gt;You can simulate both event types in the Lambda console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ALB Test Event (will fail if your handler expects API Gateway v2):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"elb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"targetGroupArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:elasticloadbalancing:region:account-id:targetgroup/my-target-group/1234567890abcdef"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"httpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/plugin/payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;API Gateway v2 Test Event (will work with &lt;code&gt;APIGatewayV2HTTPEvent&lt;/code&gt; handler):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routeKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST /plugin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rawPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/plugin/payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"httpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If the API Gateway v2 event works but the ALB event fails with an exception, you’ve confirmed the &lt;strong&gt;event format mismatch&lt;/strong&gt; as the root cause.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fix: Update the Lambda Handler to Support ALB
&lt;/h2&gt;

&lt;p&gt;Once you know the issue is an event format mismatch, you have two main options:&lt;/p&gt;
&lt;h3&gt;
  
  
  Option 1: Change the Handler Input Type to ALB’s Event
&lt;/h3&gt;

&lt;p&gt;For Java, for example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (expects API Gateway v2):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.RequestHandler&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.Context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LambdaRequestHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;APIGatewayV2HTTPEvent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;APIGatewayV2HTTPEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Your existing logic&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (expects ALB event):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.RequestHandler&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.Context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LambdaRequestHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ApplicationLoadBalancerRequestEvent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ApplicationLoadBalancerRequestEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Adapt your logic to read from ALB's event shape:&lt;/span&gt;
        &lt;span class="c1"&gt;// event.getPath(), event.getHttpMethod(), event.getHeaders(), event.getBody(), etc.&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the function compatible with ALB’s event structure.&lt;/p&gt;




&lt;h3&gt;
  
  
  Option 2: Use an Adapter Layer
&lt;/h3&gt;

&lt;p&gt;If you need to support &lt;strong&gt;multiple event sources&lt;/strong&gt; (e.g., API Gateway v2 and ALB) with the same business logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accept the raw event (or use a common internal model).&lt;/li&gt;
&lt;li&gt;Detect which event type you received (ALB vs API Gateway v2).&lt;/li&gt;
&lt;li&gt;Map it into a unified internal request object.&lt;/li&gt;
&lt;li&gt;Pass that object to your core handler logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LambdaEntryPoint&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;LambdaResponse&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rawEvent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;InternalRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EventAdapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toInternalRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawEvent&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CoreHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern decouples your core business logic from AWS-specific event shapes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Diagnosis Checklist for Lambda + ALB 503 Errors
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Function Health&lt;/td&gt;
&lt;td&gt;Test Lambda directly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Confirm ALB invoke policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routing Rules&lt;/td&gt;
&lt;td&gt;Path &amp;amp; host header check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event Format&lt;/td&gt;
&lt;td&gt;Compare working vs failing integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;Check for NPE or other stack traces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Common Gotchas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignoring ALB event format.&lt;/li&gt;
&lt;li&gt;Assuming Lambda target health status matters for 503s.&lt;/li&gt;
&lt;li&gt;Missing &lt;code&gt;Host&lt;/code&gt; header or path mismatch.&lt;/li&gt;
&lt;li&gt;Testing with the wrong event data in the console.&lt;/li&gt;
&lt;li&gt;Handler tightly coupled to one event type (e.g., API Gateway v2 only).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lambda + ALB 503 errors can also happen due to event format mismatch&lt;/strong&gt;, not just networking or permissions.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;handler signature dictates the expected input&lt;/strong&gt;. If it expects &lt;code&gt;APIGatewayV2HTTPEvent&lt;/code&gt;, it won’t magically understand ALB’s event format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function URLs and API Gateway v2 share the same event format&lt;/strong&gt;, so a handler that works there may still fail behind an ALB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ALB sends a different JSON structure&lt;/strong&gt; (e.g., &lt;code&gt;requestContext.elb&lt;/code&gt;, different path fields), which can break your code if you assume API Gateway-style fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch logs reveal stack traces and NPEs&lt;/strong&gt; — always check them first when debugging 503s.&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>lambda</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
    <item>
      <title>IAM Policies vs Resource Policies: Lessons from Production (and the Gym)</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Tue, 09 Dec 2025 13:08:37 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/iam-policies-vs-resource-policies-lessons-from-production-and-the-gym-2ce9</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/iam-policies-vs-resource-policies-lessons-from-production-and-the-gym-2ce9</guid>
      <description>&lt;p&gt;Early in my AWS journey, I ran into a problem that had me scratching my head for hours.&lt;/p&gt;

&lt;p&gt;A frontend engineer came up to me:&lt;/p&gt;

&lt;p&gt;"Tony, I just want users to see our app logos and images, but it’s not working. They keep getting denied."&lt;/p&gt;

&lt;p&gt;I double-checked the IAM permissions—everything looked fine. The user had the right access. So why were people still blocked?&lt;/p&gt;

&lt;p&gt;It wasn’t IAM at all. It was the S3 bucket’s resource policy.&lt;/p&gt;

&lt;p&gt;That’s when it clicked: managing AWS access is a lot like managing a gym or sticking to a disciplined fitness routine.&lt;/p&gt;

&lt;p&gt;Before you say how...&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;

&lt;h4&gt;
  
  
  IAM Policies = Your Personal Trainer
&lt;/h4&gt;

&lt;p&gt;IAM policies are like your personal trainer. They define what you (the identity) are allowed to do:&lt;/p&gt;

&lt;p&gt;Can you lift weights today?&lt;/p&gt;

&lt;p&gt;Can you run on the treadmill?&lt;/p&gt;

&lt;p&gt;Can you skip cardio?&lt;/p&gt;

&lt;p&gt;In AWS, IAM policies attach to users, groups, or roles. They control what actions an identity can perform and on which resources.&lt;/p&gt;

&lt;p&gt;In production, I also enforced MFA for all IAM users. Think of it like adding a second checkpoint before entering a VIP gym area—you might have the keycard (password), but without the fingerprint (MFA), you can’t get in. Simple, but it drastically increases security.&lt;/p&gt;

&lt;h4&gt;
  
  
  Resource Policies = The Gym Rules
&lt;/h4&gt;

&lt;p&gt;Resource policies are like the rules posted on the gym wall. Even if your trainer gives you permission, the rules might still restrict your access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only VIP members can use certain equipment&lt;/li&gt;
&lt;li&gt;Some areas are off-limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In AWS, resource policies attach directly to resources, like S3 buckets, Lambda functions, or SQS queues. They define who can access the resource, including other AWS accounts or the public.&lt;/p&gt;

&lt;h4&gt;
  
  
  Here’s what I did in production:
&lt;/h4&gt;

&lt;p&gt;ALB Logs: Edited the bucket policy so our Application Load Balancer could send access logs safely, without extra permissions.&lt;/p&gt;

&lt;p&gt;Frontend Images/Logos: Created a bucket policy allowing users to read logos and images—but blocked access to sensitive files. Users got exactly what they needed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scaling Access: IAM vs Resource Policies
&lt;/h4&gt;

&lt;p&gt;One thing I learned quickly: scale changes everything.&lt;/p&gt;

&lt;p&gt;IAM policies scale best for identities. One policy can manage multiple users, groups, or roles. Perfect for internal teams.&lt;/p&gt;

&lt;p&gt;Resource policies scale best for resources. When sharing across accounts or publicly, resource policies give the resource control over access.&lt;/p&gt;

&lt;p&gt;Using both together is essential. IAM ensures users only do what they should, while resource policies ensure resources themselves are protected—even if IAM permissions exist.&lt;/p&gt;

&lt;p&gt;Example: Our financial reports bucket had IAM permissions for finance guys, but the resource policy restricted access to our AWS account only. Layered security prevented accidental or malicious access and gave us peace of mind.&lt;/p&gt;

&lt;p&gt;Layered Security = Layered Discipline&lt;br&gt;
OOps that sounded cringe. haha&lt;/p&gt;

&lt;p&gt;But just like weight loss or fitness, success in AWS access control comes from layers of discipline:&lt;/p&gt;

&lt;p&gt;IAM = your workout plan → defines what identities can do&lt;br&gt;
Resource policy = gym rules → defines who can access resources&lt;/p&gt;

&lt;p&gt;Both together produce sustainable results which ultimately prevents mistakes, leaks, or misuse.&lt;/p&gt;

&lt;p&gt;Ignoring one layer is like skipping cardio or not tracking calories.&lt;/p&gt;

&lt;p&gt;You might get some results, but you’ll eventually hit problems.&lt;/p&gt;

&lt;p&gt;In AWS, ignoring resource policies while relying solely on IAM can lead to “mystery denied” errors and frustrated users.&lt;/p&gt;

&lt;h4&gt;
  
  
  Takeaways from the battlefield
&lt;/h4&gt;

&lt;p&gt;Always check both IAM and resource policies. A deny in either will block access.&lt;/p&gt;

&lt;p&gt;Enforce MFA. It’s a small step with huge security payoff.&lt;/p&gt;

&lt;p&gt;Use resource policies for cross-account or public access—especially when sharing logs, images, or APIs.&lt;/p&gt;

&lt;p&gt;Test every access path. Users may have the right IAM permissions but still get blocked.&lt;/p&gt;

&lt;p&gt;Think in layers. &lt;/p&gt;

&lt;p&gt;Identity control + resource control = secure, scalable access.&lt;/p&gt;

&lt;p&gt;Cheers&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>CI/CD for Infrastructure That Actually Works.</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Sat, 13 Sep 2025 10:58:09 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/cicd-for-infrastructure-that-actually-works-41ph</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/cicd-for-infrastructure-that-actually-works-41ph</guid>
      <description>&lt;p&gt;&lt;em&gt;Moving beyond demos: secure identity and cost visibility&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Infrastructure Pipelines Still Fail
&lt;/h2&gt;

&lt;p&gt;Too often, Terraform runs still happen on laptops with static AWS keys. The result?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Credentials at risk — long-lived keys sitting in repos or local machines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Undocumented changes — no audit trail, no shared visibility&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment drift — dev, staging, and prod no longer match&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Forgotten test resources — quietly inflating cloud bills&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running IaC this way feels fast — until it breaks consistency, costs money, or exposes credentials.&lt;/p&gt;

&lt;p&gt;CI/CD fixes this, but to move beyond a “demo pipeline,” two practices make the difference in production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OIDC-based authentication&lt;/strong&gt; → no long-lived credentials&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost visibility in PRs&lt;/strong&gt; → prevent financial surprises&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The 8 Steps of CI/CD (Infrastructure Edition)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code pushed&lt;/strong&gt; → pipeline triggers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; → package artifact / generate Terraform plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; → syntax check, &lt;code&gt;terraform validate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan&lt;/strong&gt; → security/compliance checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy to dev/staging&lt;/strong&gt; → safe sandbox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests&lt;/strong&gt; → real environment validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy to prod&lt;/strong&gt; → gated promotion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; → logs, costs, drift&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same framework as app pipelines — only the artifact changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production-Ready Differentiators
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. OIDC: Secure, Short-Lived Credentials
&lt;/h3&gt;

&lt;p&gt;Instead of storing AWS keys in GitHub secrets, pipelines use OIDC to assume roles dynamically.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived credentials, nothing to leak&lt;/li&gt;
&lt;li&gt;Environment-specific IAM roles (dev/staging/prod)&lt;/li&gt;
&lt;li&gt;Every assumption logged in CloudTrail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Minimal AWS trust policy for GitHub OIDC (example):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"terraform_dev"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uketui-terraform-cicd-dev"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;assume_role_policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;jsonencode(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;Version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;Statement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;Effect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;Principal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;Federated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;aws_iam_openid_connect_provider.github.arn&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;Action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;Condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;StringEquals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"repo:Anthonyuketui/Terraform-Project-with-Multi-Environment-Support-on-AWS:environment:dev"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Cost Visibility: Infracost in PRs
&lt;/h3&gt;

&lt;p&gt;With a single step, you can post cost estimates into pull requests after &lt;code&gt;terraform plan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example PR comment:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5m3nh8243x71oyg4qubs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5m3nh8243x71oyg4qubs.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This puts financial awareness into the code review process. A financial mistake gets caught before merge, not on the next AWS bill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example Dev Pipeline (GitHub Actions)
&lt;/h2&gt;

&lt;p&gt;This workflow runs in a &lt;strong&gt;dev environment&lt;/strong&gt; for iteration. It’s intentionally lightweight, but notice how it already includes the &lt;strong&gt;production-grade practices&lt;/strong&gt; (OIDC, cost checks, environment isolation) that make the same design safe to scale into staging and prod.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Dev DevSecOps Pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cost-analysis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&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;Checkout code&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;actions/checkout@v4&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;Setup Terraform&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;hashicorp/setup-terraform@v3&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 Infracost&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;infracost/actions/setup@v2&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;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.INFRACOST_API_KEY }}&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;Generate Infracost diff&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cd environments/dev&lt;/span&gt;
          &lt;span class="s"&gt;terraform init -backend=false&lt;/span&gt;
          &lt;span class="s"&gt;infracost breakdown --path . \&lt;/span&gt;
            &lt;span class="s"&gt;--format table \&lt;/span&gt;
            &lt;span class="s"&gt;--out-file cost-estimate.txt&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;Output cost estimate&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cat environments/dev/cost-estimate.txt&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;Post cost estimate on PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;body="#### Infracost Estimate&lt;/span&gt;
          &lt;span class="s"&gt;\`\`\`&lt;/span&gt;
          &lt;span class="s"&gt;$(cat environments/dev/cost-estimate.txt)&lt;/span&gt;
          &lt;span class="s"&gt;\`\`\`"&lt;/span&gt;

          &lt;span class="s"&gt;echo "comment=${body}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cost_estimate&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;Add PR comment&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&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;actions/github-script@v6&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;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const body = `${{ steps.cost_estimate.outputs.comment }}`;&lt;/span&gt;
            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;body: body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;

  &lt;span class="na"&gt;terraform-dev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cost-analysis&lt;/span&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="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TF_IN_AUTOMATION&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;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;Checkout code&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;actions/checkout@v4&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 (OIDC)&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.TERRAFORM_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;${{ vars.AWS_REGION }}&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;Setup Terraform&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;hashicorp/setup-terraform@v3&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;Terraform Format Check&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform fmt -recursive&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;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init -backend-config=backend.tfvars&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;Terraform Validate&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform validate&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;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -var-file=terraform.tfvars -out=tfplan&lt;/span&gt;

      &lt;span class="c1"&gt;# -------------------------&lt;/span&gt;
      &lt;span class="c1"&gt;# Optional: Conftest (policy checks)&lt;/span&gt;
      &lt;span class="c1"&gt;# -------------------------&lt;/span&gt;
      &lt;span class="c1"&gt;# - name: Run Conftest&lt;/span&gt;
      &lt;span class="c1"&gt;#   uses: instrumenta/conftest-action@master&lt;/span&gt;
      &lt;span class="c1"&gt;#   with:&lt;/span&gt;
      &lt;span class="c1"&gt;#     files: environments/dev/tfplan&lt;/span&gt;
      &lt;span class="c1"&gt;#     policy: policy/&lt;/span&gt;

      &lt;span class="c1"&gt;# -------------------------&lt;/span&gt;
      &lt;span class="c1"&gt;# Optional: Security checks (uncomment for strict DevSecOps)&lt;/span&gt;
      &lt;span class="c1"&gt;# -------------------------&lt;/span&gt;
      &lt;span class="c1"&gt;# security-checks:&lt;/span&gt;
      &lt;span class="c1"&gt;#   runs-on: ubuntu-latest&lt;/span&gt;
      &lt;span class="c1"&gt;#   steps:&lt;/span&gt;
      &lt;span class="c1"&gt;#     - name: Checkout code&lt;/span&gt;
      &lt;span class="c1"&gt;#       uses: actions/checkout@v4&lt;/span&gt;
      &lt;span class="c1"&gt;#&lt;/span&gt;
      &lt;span class="c1"&gt;#     - name: Run Checkov&lt;/span&gt;
      &lt;span class="c1"&gt;#       uses: bridgecrewio/checkov-action@v12&lt;/span&gt;
      &lt;span class="c1"&gt;#       with:&lt;/span&gt;
      &lt;span class="c1"&gt;#         directory: environments/dev&lt;/span&gt;
      &lt;span class="c1"&gt;#         framework: terraform&lt;/span&gt;
      &lt;span class="c1"&gt;#         skip_check: CKV_AWS_79,CKV_AWS_126&lt;/span&gt;
      &lt;span class="c1"&gt;#&lt;/span&gt;
      &lt;span class="c1"&gt;#     - name: Run tfsec&lt;/span&gt;
      &lt;span class="c1"&gt;#       uses: aquasecurity/tfsec-action@v1.0.3&lt;/span&gt;
      &lt;span class="c1"&gt;#       with:&lt;/span&gt;
      &lt;span class="c1"&gt;#         working_directory: environments/dev&lt;/span&gt;
      &lt;span class="c1"&gt;#&lt;/span&gt;
      &lt;span class="c1"&gt;#     - name: Run Terrascan&lt;/span&gt;
      &lt;span class="c1"&gt;#       uses: tenable/terrascan-action@main&lt;/span&gt;
      &lt;span class="c1"&gt;#       with:&lt;/span&gt;
      &lt;span class="c1"&gt;#         iac_type: 'terraform'&lt;/span&gt;
      &lt;span class="c1"&gt;#         iac_dir: 'environments/dev'&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;Terraform Apply (on push only)&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push'&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply -auto-approve tfplan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For staging/prod, add approval gates and stricter IAM roles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Environment Setup
&lt;/h2&gt;

&lt;p&gt;Use separate directories and state backends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── environments/
│   ├── dev/
│   ├── staging/
│   └── prod/
├── modules/
└── .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Own &lt;code&gt;terraform.tfvars&lt;/code&gt; + &lt;code&gt;backend.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Own S3 bucket + DynamoDB lock table&lt;/li&gt;
&lt;li&gt;Role isolation via IAM&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;For engineers: this structure eliminates environment drift and makes infra deployments reproducible.&lt;br&gt;
For managers: it reduces credential risk, prevents surprise costs, and creates a clear audit trail.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Building a Production-Ready Terraform Project with Multi-Environment Support on AWS</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Fri, 29 Aug 2025 12:52:53 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/building-a-production-ready-terraform-project-with-multi-environment-support-on-aws-hia</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/building-a-production-ready-terraform-project-with-multi-environment-support-on-aws-hia</guid>
      <description>&lt;p&gt;When I started this project, my goal was simple but ambitious. I wanted to build a &lt;strong&gt;production-ready infrastructure&lt;/strong&gt; that could automatically scale based on demand. I also wanted to organize everything in a way that supports &lt;strong&gt;multiple environments&lt;/strong&gt; like &lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I’ll walk you through my full process, including the reasoning behind each step, challenges I faced, and how I resolved them.&lt;/p&gt;

&lt;p&gt;I'd also share snippets of my code&lt;/p&gt;

&lt;p&gt;But, here's the code if you want to follow along: &lt;a href="https://tinyurl.com/aedduwdm" rel="noopener noreferrer"&gt;https://tinyurl.com/aedduwdm&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Project Structure and Multi-Environment Setup
&lt;/h2&gt;

&lt;p&gt;I wanted to keep my project modular and clean. To achieve this, I structured my directory in this way:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffmforp15yjjsv10zal7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffmforp15yjjsv10zal7r.png" alt=" " width="476" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modules&lt;/strong&gt; contain reusable building blocks like VPC, ALB, EC2, RDS, and Monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environments&lt;/strong&gt; (&lt;code&gt;dev&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt;) each have their own &lt;code&gt;main.tf&lt;/code&gt; and &lt;code&gt;terraform.tfvars&lt;/code&gt; files that reference the modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This structure makes it easy to manage different configurations without duplicating code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcc87d5x5dpo9nob9jo2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcc87d5x5dpo9nob9jo2r.png" alt=" " width="465" height="578"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Defining the VPC
&lt;/h2&gt;

&lt;p&gt;My first step was to create a dedicated VPC with public and private subnets. Public subnets are for load balancers, and private subnets are for EC2 and RDS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Snippet (VPC module)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&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;tags&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.env}-vpc"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;I chose to keep the database and application instances in private subnets for &lt;strong&gt;security reasons&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg355elpmwzy6y6z2pk5a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg355elpmwzy6y6z2pk5a.png" alt=" " width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Application Load Balancer (ALB)
&lt;/h2&gt;

&lt;p&gt;The ALB distributes traffic across EC2 instances. This ensures high availability.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Snippet (ALB module)
&lt;/h3&gt;

&lt;p&gt;`&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.env}-alb"&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;
  &lt;span class="nx"&gt;subnets&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;public_subnets&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;By using public subnets, the ALB became internet-facing, while the EC2 instances behind it stayed private.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yfwkyshv3u36hha7cqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yfwkyshv3u36hha7cqn.png" alt=" " width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4: EC2 Auto Scaling Group
&lt;/h2&gt;

&lt;p&gt;Next, I set up EC2 instances with auto scaling. This way, when CPU usage is high, new instances are launched automatically.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Snippet (EC2 module)
&lt;/h3&gt;

&lt;p&gt;`&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_autoscaling_group"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;desired_capacity&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;desired_capacity&lt;/span&gt;
  &lt;span class="nx"&gt;max_size&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;max_size&lt;/span&gt;
  &lt;span class="nx"&gt;min_size&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;min_size&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_zone_identifier&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;private_subnets&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Amazon Linux 2 AMI&lt;/strong&gt; and passed in user data through a template to bootstrap the instances.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqs6rheri59pfavb2it4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqs6rheri59pfavb2it4.png" alt=" " width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5: RDS Database Setup
&lt;/h2&gt;

&lt;p&gt;For the database, I used Amazon RDS with MySQL. One challenge here was how to handle credentials securely.&lt;/p&gt;

&lt;p&gt;At first, I hardcoded them, but I quickly realized that wasn’t safe. I switched to &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; combined with a random password generator.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Snippet (RDS module)
&lt;/h3&gt;

&lt;p&gt;`&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_password"&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
  &lt;span class="nx"&gt;special&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"db_pwd"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.env}/db_password"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This allowed me to store the password securely and avoid exposing it in my code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0yq6xsxj7tiigvo00u0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0yq6xsxj7tiigvo00u0o.png" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 6: Monitoring and Alerts
&lt;/h2&gt;

&lt;p&gt;I didn’t want to stop at just provisioning resources. I also wanted &lt;strong&gt;monitoring and alerts&lt;/strong&gt; for key components like EC2, ALB, and RDS.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;CloudWatch Alarms&lt;/strong&gt; with SNS topics to send email alerts.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Snippet (Monitoring module)
&lt;/h3&gt;

&lt;p&gt;`&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_metric_alarm"&lt;/span&gt; &lt;span class="s2"&gt;"cpu_high"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.env}-cpu-high"&lt;/span&gt;
  &lt;span class="nx"&gt;metric_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CPUUtilization"&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;
  &lt;span class="nx"&gt;comparison_operator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS/EC2"&lt;/span&gt;
  &lt;span class="nx"&gt;statistic&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Average"&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
  &lt;span class="nx"&gt;evaluation_periods&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_actions&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This way, I get notified if something goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30rnzpyl97gwl0ejj1dy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30rnzpyl97gwl0ejj1dy.png" alt=" " width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7ifyflnc1894ftm2o0g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7ifyflnc1894ftm2o0g.png" alt=" " width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 7: Multi-Environment Variables
&lt;/h2&gt;

&lt;p&gt;Each environment has its own &lt;code&gt;terraform.tfvars&lt;/code&gt; file. For example, my &lt;strong&gt;dev environment&lt;/strong&gt; uses smaller instances while &lt;strong&gt;prod&lt;/strong&gt; uses larger ones.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dev &lt;code&gt;terraform.tfvars&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;`&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="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;region&lt;/span&gt;      &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&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;desired_capacity&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;db_instance_class&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;
&lt;h3&gt;
  
  
  Prod &lt;code&gt;terraform.tfvars&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;`&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="nx"&gt;environment&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="nx"&gt;region&lt;/span&gt;      &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&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.medium"&lt;/span&gt;
&lt;span class="nx"&gt;desired_capacity&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;db_instance_class&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.medium"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This separation made it easy to spin up lightweight dev infrastructure without affecting production.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dev Environment
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F278claf4eiaxqtmd92jz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F278claf4eiaxqtmd92jz.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxp6ol80nu1wp881kxhxw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxp6ol80nu1wp881kxhxw.png" alt=" " width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Prod Environment
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fxyaz34fgxax1j0rmlv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6fxyaz34fgxax1j0rmlv.png" alt=" " width="800" height="305"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsmnqrjmk4p6vjaafjl2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsmnqrjmk4p6vjaafjl2l.png" alt=" " width="800" height="183"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 8: Running the Project
&lt;/h2&gt;

&lt;p&gt;Here are the commands I used to apply everything:&lt;/p&gt;

&lt;p&gt;`&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;envs/dev
terraform init
terraform plan &lt;span class="nt"&gt;-var-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"terraform.tfvars"&lt;/span&gt;
terraform apply &lt;span class="nt"&gt;-var-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"terraform.tfvars"&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; ../prod
terraform init
terraform plan &lt;span class="nt"&gt;-var-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"terraform.tfvars"&lt;/span&gt;
terraform apply &lt;span class="nt"&gt;-var-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"terraform.tfvars"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This workflow gave me a smooth way to deploy separate environments using the same modules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges and How I Solved Them
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hardcoding DB credentials&lt;/strong&gt;: Initially I put the password directly in code. I later switched to using Secrets Manager with &lt;code&gt;random_password&lt;/code&gt; to improve security.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto scaling not attaching correctly to ALB&lt;/strong&gt;: At first, my instances weren’t registering as healthy. The fix was to ensure the security groups allowed the right ports and the target group health checks matched the app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-environment management&lt;/strong&gt;: I struggled with duplication until I split logic into modules and kept only environment-specific variables in &lt;code&gt;tfvars&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This project gave me a hands-on understanding of how to structure a Terraform project for &lt;strong&gt;scalability, security, and maintainability&lt;/strong&gt;. By modularizing infrastructure, securing credentials, and setting up monitoring, I ended up with a production-grade setup.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How I passed my AZ-104 (Associate Administrator) exams in less than 21 days.</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Sun, 13 Apr 2025 14:39:34 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/how-i-passed-my-az-104-associate-administrator-exams-in-less-than-21-days-4b6h</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/how-i-passed-my-az-104-associate-administrator-exams-in-less-than-21-days-4b6h</guid>
      <description>&lt;p&gt;The title might seem misleading, but it's the truth. To top it off, I scored an 854/1000, too. But that's by the way. I'm not her8e to brag, after all, it's just a paper. &lt;/p&gt;

&lt;p&gt;I'm writing this so you become armed enough to write this exam in one sitting.&lt;/p&gt;

&lt;p&gt;For clarity's sake, I've outlined this in steps.&lt;/p&gt;

&lt;p&gt;Ready?&lt;/p&gt;

&lt;p&gt;Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Book the exam.
&lt;/h2&gt;

&lt;p&gt;Create that pressure and set a deadline for yourself; that way, you don't end up in a vicious cycle of procrastination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Gather thy resources.
&lt;/h2&gt;

&lt;p&gt;I watched tons of videos researching for the best resources. I watched to the extent that it became a form of procrastination. Instead of reading, studying, and practicing, I was all over YouTube scrolling for "Best Az-104" resources. I don't want you wasting time, so here are the resources I used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1)Video Course&lt;br&gt;
2)Text course&lt;br&gt;
3)Labs(Hands-on Practice)&lt;br&gt;
4)Practice questions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People learn differently, but I'm sure with a mix of all things, you can never go wrong.&lt;/p&gt;

&lt;p&gt;Before I continue... Labs are really important to do. Get familiar with the portal and the cloud shell. &lt;/p&gt;

&lt;p&gt;Get your hands dirty. The exam is not so easy, and it's an associate exam. So if you have zero experience playing with Azure. &lt;/p&gt;

&lt;p&gt;Please create an Azure account and start getting your hands muddy.&lt;/p&gt;

&lt;p&gt;For the text course, I used &lt;strong&gt;Ms learn.&lt;/strong&gt; And I solidified the concepts with something visual, like a video course.&lt;/p&gt;

&lt;p&gt;For the Video course, I had access to &lt;strong&gt;Kodekloud&lt;/strong&gt;, so I took a course on Az-104 on it, and I supplemented it with an Udemy course by &lt;strong&gt;Scott Duffy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As you finish a certain topic, like compute or Storage, try to build projects related to the topic.&lt;/p&gt;

&lt;p&gt;These are some labs I did&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/MicrosoftLearning" rel="noopener noreferrer"&gt;
        MicrosoftLearning
      &lt;/a&gt; / &lt;a href="https://github.com/MicrosoftLearning/AZ-104-MicrosoftAzureAdministrator" rel="noopener noreferrer"&gt;
        AZ-104-MicrosoftAzureAdministrator
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      AZ-104 Microsoft Azure Administrator
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AZ-104: Microsoft Azure Administrator&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Welcome&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;This repository is for instructors teaching Microsoft courses. If you are in class, please ask your instructor for assistance.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://microsoftlearning.github.io/AZ-104-MicrosoftAzureAdministrator/" rel="nofollow noopener noreferrer"&gt;Link to demonstrations and labs (HTML format)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are you an MCT?&lt;/strong&gt; - Have a look at our &lt;a href="https://microsoftlearning.github.io/MCT-User-Guide/" rel="nofollow noopener noreferrer"&gt;GitHub User Guide for MCTs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;To preview this course in a self-paced format, see our &lt;strong&gt;&lt;a href="https://mslabs.cloudguides.com/guides/AZ-104%20Exam%20Guide%20-%20Microsoft%20Azure%20Administrator" rel="nofollow noopener noreferrer"&gt;interactive lab simulations&lt;/a&gt;&lt;/strong&gt;. You may find slight differences between the interactive simulations and the hosted labs, but the core concepts and ideas being demonstrated are the same.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Security Issue - April 2023&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Effective immediately, the Admin password will be removed from the JSON template parameter files. This means students will have to provide a password when the template is deployed. This affects Labs 4, 5, 6, 7, 10 and 11.  The lab instructions will be changed to reflect this change.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What are we doing?&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;To support this course, we will need to make…&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/MicrosoftLearning/AZ-104-MicrosoftAzureAdministrator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;After two weeks of study, I immersed myself in practice questions, thinking I was ready. Alas, I scored 45%. Haha.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feaj792q6rtteuzjo7p3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feaj792q6rtteuzjo7p3d.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Practice questions point out your flaws and loopholes. Things you might have avoided because you thought it was unimportant. And it also shows you how the questions would be structured.&lt;/p&gt;

&lt;p&gt;When you do this. Review your weak areas and go back to your text or video course to cement them. Go back to doing labs if it demands that. I supplemented with &lt;strong&gt;Azure documentation&lt;/strong&gt; &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flearn.microsoft.com%2Fen-us%2Fmedia%2Fopen-graph-image.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://learn.microsoft.com/en-us/azure/" rel="noopener noreferrer" class="c-link"&gt;
            Azure documentation | Microsoft Learn
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Learn how to build and manage powerful applications using Microsoft Azure cloud services. Get documentation, example code, tutorials, and more.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          learn.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
 and a Free YouTube video course playlist by &lt;strong&gt;Susanth Sutheesh&lt;/strong&gt;  &lt;iframe src="https://www.youtube.com/embed/I1zvntPHNMk?start=17699"&gt;
  &lt;/iframe&gt;


&lt;p&gt;Never try to memorize the practice questions. Strive to understand the concepts instead.&lt;/p&gt;

&lt;p&gt;Aside from the certification, you wouldn't want to be a scam when you eventually get a job and you're told to do something basic.&lt;/p&gt;

&lt;p&gt;So in a nutshell, lock in!&lt;/p&gt;

&lt;p&gt;For Practice questions, I used &lt;strong&gt;TechBlackboard&lt;/strong&gt; AZ-104 YouTube playlist  &lt;iframe src="https://www.youtube.com/embed/MaUZQtFUfog"&gt;
  &lt;/iframe&gt;
 as my go-to guide because it explained each option and why they aren't the correct one. I warn you, some answers can be wrong, so go through the comments from time to time.&lt;/p&gt;

&lt;p&gt;I also supplemented with a YouTube playlist that I found interesting. You can consume this probably a day before your exams, just to further solidify concepts  &lt;iframe src="https://www.youtube.com/embed/wYUhumwOGrM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Hopefully, this made sense to you. You can always reach out to me on LinkedIn&lt;/p&gt;

&lt;p&gt;I'm rooting for you and hope you crush your exam!&lt;/p&gt;

</description>
      <category>azure</category>
    </item>
    <item>
      <title>Building an Automated Football Match Schedule Notification System with AWS</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Thu, 13 Mar 2025 04:57:55 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/building-an-automated-football-match-schedule-notification-system-with-aws-55md</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/building-an-automated-football-match-schedule-notification-system-with-aws-55md</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this post, I’ll walk you through how I built a serverless application that automatically checks for match schedules and sends email notifications using AWS services. This project leverages AWS Lambda, Amazon SNS, and Amazon EventBridge, demonstrating a practical use case of event-driven architecture in the cloud.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Why I did this?
&lt;/h3&gt;

&lt;p&gt;Checking match schedules manually can be tedious, and missing a match can be frustrating. What if we could automate this process so that users receive timely notifications about upcoming matches?  &lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Overview
&lt;/h3&gt;

&lt;p&gt;To solve this, I created an AWS-based system that:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetches match schedules&lt;/strong&gt; using a Lambda function.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triggers email notifications&lt;/strong&gt; via Amazon SNS.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uses EventBridge as a scheduler&lt;/strong&gt; to run the Lambda function at set intervals.
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Implementation Steps&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Setting Up the SNS Topic
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Created an &lt;strong&gt;SNS topic&lt;/strong&gt; in the AWS console.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqlmma005dyuzx7x9vmz1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqlmma005dyuzx7x9vmz1.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbljna2tvttdp0ozx862j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbljna2tvttdp0ozx862j.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Added an &lt;strong&gt;email subscription&lt;/strong&gt; to receive notifications.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft6frodwelsapc4thvye4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft6frodwelsapc4thvye4.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirmed the subscription via email. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu82igrmkfrbq23gmfqta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu82igrmkfrbq23gmfqta.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fos73vk5gxjdorg51n3e8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fos73vk5gxjdorg51n3e8.png" alt=" " width="731" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also created IAM roles and policies so that our SNS topic can communicate with our lambda function&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F061a9sosxp65i5snkvez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F061a9sosxp65i5snkvez.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp58br3ckm1x1kgbni8xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp58br3ckm1x1kgbni8xu.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;2. Writing the Lambda Function&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;I Used Python to fetch match schedules from an API.
&lt;/li&gt;
&lt;li&gt;Processed the data and sent a message to SNS.
&lt;/li&gt;
&lt;li&gt;Deployed the function to AWS Lambda.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sample Lambda Code (Python):&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_football_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error fetching data from API: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;format_match_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;home_team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;homeTeam&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;away_team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;awayTeam&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;home_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullTime&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;homeTeam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;away_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullTime&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;awayTeam&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;competition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;competition&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown Competition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;match_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utcDate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FINISHED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Competition: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;competition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;home_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;away_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Final Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;home_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;away_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Date &amp;amp; Time: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;match_time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PLAY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Competition: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;competition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;home_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;away_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Current Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;home_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;away_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Match is in progress!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCHEDULED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Competition: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;competition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;home_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;away_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Scheduled Time: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;match_time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Competition: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;competition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;home_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;away_team&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_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;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOOTBALL_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sns_topic_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SNS_TOPIC_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sns_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Fetch today's matches
&lt;/span&gt;    &lt;span class="n"&gt;today_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;api_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.football-data.org/v4/matches?dateFrom=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;dateTo=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Auth-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fetching football matches for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_football_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&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;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error fetching match data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;format_match_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&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;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;final_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No matches available for today.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Publish to SNS
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sns_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TopicArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sns_topic_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;final_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Football Match Updates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Message published to SNS successfully.&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error publishing to SNS: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error publishing to SNS&lt;/span&gt;&lt;span class="sh"&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;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Football match data processed and sent to SNS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added an environment variable so our function recognizes the API key and the SNS topic URL&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgu2237vjmh3j59f4uzcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgu2237vjmh3j59f4uzcu.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;3. Scheduling Lambda with EventBridge&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Created a new &lt;strong&gt;EventBridge rule&lt;/strong&gt; to trigger the Lambda function at specific times.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1nwqguiwp4ewawvbk5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1nwqguiwp4ewawvbk5i.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configured it to run &lt;strong&gt;daily&lt;/strong&gt; or at specific match times.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr81jwmerc5qbncs6tnuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr81jwmerc5qbncs6tnuk.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linked the rule to the Lambda function.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61d6jrc2d4n5psyujqgd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61d6jrc2d4n5psyujqgd.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm405dibysr8hzsahlhd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm405dibysr8hzsahlhd.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Testing &amp;amp; Results&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deployed and tested the function manually.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqdqd74m0seks6d1g0fp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqdqd74m0seks6d1g0fp.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verified that SNS successfully delivered email notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrog1bvo9ak3tj3qegwn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrog1bvo9ak3tj3qegwn.png" alt=" " width="724" height="418"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Future Improvements&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Store match schedules in &lt;strong&gt;DynamoDB&lt;/strong&gt; for historical data.
&lt;/li&gt;
&lt;li&gt;Extend notifications to &lt;strong&gt;SMS or mobile apps&lt;/strong&gt;. &lt;/li&gt;
&lt;li&gt;Create a User Interface &lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This project showcases how AWS services can be combined to build an automated notification system. By leveraging &lt;strong&gt;Lambda, SNS, and EventBridge&lt;/strong&gt;, I’ve created a serverless solution that keeps users informed without manual effort. &lt;/p&gt;

</description>
    </item>
    <item>
      <title>Developing an API to Retrieve Football Game Schedules - Using Docker and AWS Services</title>
      <dc:creator>Anthony Uketui</dc:creator>
      <pubDate>Mon, 10 Mar 2025 07:00:08 +0000</pubDate>
      <link>https://dev.to/tony_uketui_6cca68c7eba02/developing-an-api-to-retrieve-football-game-schedules-using-docker-and-aws-services-4ok6</link>
      <guid>https://dev.to/tony_uketui_6cca68c7eba02/developing-an-api-to-retrieve-football-game-schedules-using-docker-and-aws-services-4ok6</guid>
      <description>&lt;h1&gt;
  
  
  Who Doesn't Love Football? ⚽
&lt;/h1&gt;

&lt;p&gt;Before diving into my passion for the game, let’s tackle an important question:&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do We Need an API for Football Schedules?
&lt;/h2&gt;

&lt;p&gt;Manually gathering football schedules from multiple websites is time-consuming and inefficient. Instead, an API allows us to integrate live updates directly into applications, websites, or analytics tools, making it easy to access real-time football schedules seamlessly.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛠 Breaking It Down with an Analogy
&lt;/h3&gt;

&lt;p&gt;Imagine you want to order food from a restaurant. Instead of going to the kitchen, checking ingredients, and preparing the meal yourself, you simply ask the waiter for what you want. The waiter communicates with the kitchen, retrieves the meal, and serves it to you—quick, accurate, and hassle-free.&lt;/p&gt;

&lt;p&gt;An API works like that waiter. When you need football game schedules, instead of manually collecting data from multiple sources, the API acts as a middleman, fetching the latest updates for you instantly.&lt;/p&gt;

&lt;p&gt;✅ This saves time, ensures accuracy, and eliminates manual work, making it easier for apps and websites to display real-time football schedules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;In this project, I developed an API that retrieves football game schedules using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Flask&lt;/strong&gt; (for the API)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Docker&lt;/strong&gt; (for containerization)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;AWS Services&lt;/strong&gt; (for scalability and reliability)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tech Stack Breakdown
&lt;/h3&gt;

&lt;h4&gt;
  
  
  🚀 Flask Framework
&lt;/h4&gt;

&lt;p&gt;Flask is a lightweight Python web framework that allows for rapid development. I used it to create API endpoints that handle requests for football game schedules.&lt;/p&gt;

&lt;h4&gt;
  
  
  📦 Docker
&lt;/h4&gt;

&lt;p&gt;Docker enables packaging applications into containers—ensuring that they run consistently across different environments. By containerizing the Flask app, I simplified deployment and eliminated compatibility issues.&lt;/p&gt;

&lt;h4&gt;
  
  
  ☁️ AWS Services Used
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elastic Container Registry (ECR)&lt;/strong&gt; – A managed container registry to store our Docker images securely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elastic Container Service (ECS)&lt;/strong&gt; – Used to deploy and manage the Dockerized API efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elastic Load Balancing (ELB)&lt;/strong&gt; – Distributes traffic across multiple instances to ensure high availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; – Exposes the API via a secure and scalable endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination ensures the API can handle real-time requests with high availability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Development and Deployment Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1️⃣ Developing the Flask API
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Created a Python-based Flask application to handle API requests.&lt;/li&gt;
&lt;li&gt;Ensured the application fetched and processed football schedules from &lt;a href="https://www.football-data.org/" rel="noopener noreferrer"&gt;Football-Data.org&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2️⃣ Containerizing the Application with Docker
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Wrote a &lt;code&gt;Dockerfile&lt;/code&gt; to define the environment and dependencies.&lt;/li&gt;
&lt;li&gt;Built and tested a Docker image locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3️⃣ Pushing the Docker Image to AWS ECR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Created an &lt;strong&gt;ECR repository&lt;/strong&gt; to store the Docker image.&lt;/li&gt;
&lt;li&gt;Tagged and pushed the image to &lt;strong&gt;ECR&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4️⃣ Deploying on AWS ECS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Set up an &lt;strong&gt;ECS cluster&lt;/strong&gt; to manage containerized applications.&lt;/li&gt;
&lt;li&gt;Defined a &lt;strong&gt;task definition&lt;/strong&gt; specifying the Docker image and resource requirements.&lt;/li&gt;
&lt;li&gt;Created a &lt;strong&gt;service&lt;/strong&gt; to maintain the desired number of running tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5️⃣ Configuring Networking &amp;amp; Load Balancing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Implemented an &lt;strong&gt;Application Load Balancer (ALB)&lt;/strong&gt; for traffic distribution.&lt;/li&gt;
&lt;li&gt;Configured &lt;strong&gt;security groups&lt;/strong&gt; to allow API access.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6️⃣ Exposing the API via AWS API Gateway
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Created an &lt;strong&gt;API Gateway&lt;/strong&gt; to serve as the entry point.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - Routed traffic through API Gateway to the ALB.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Developing the API with Python (Flask)
&lt;/h2&gt;

&lt;p&gt;I used Python to create the API, ensuring it could efficiently fetch and format football schedules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;p&gt;✅ Fetches live football schedules&lt;br&gt;&lt;br&gt;
✅ Formats responses in JSON&lt;br&gt;&lt;br&gt;
✅ Handles errors gracefully  &lt;/p&gt;
&lt;h3&gt;
  
  
  Flask Code:
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;FOOTBALL_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.football-data.org/v4/matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;FOOTBALL_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOOTBALL_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_api_key_here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/football&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_football_schedule&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetches upcoming football matches and returns as JSON.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
 &lt;span class="mi"&gt;8&lt;/span&gt;   &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Auth-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FOOTBALL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FOOTBALL_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matches&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No upcoming football matches found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matches&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="mi"&gt;200&lt;/span&gt;

        &lt;span class="n"&gt;formatted_matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;competition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;competition&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;home_team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;homeTeam&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;away_team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;awayTeam&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;venue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;venue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utcDate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&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;for&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Football schedule fetched successfully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;formatted_matches&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;200&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An error occurred.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}),&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To confirm the code works we test it in our local machine by running the command below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjuogxpdi5jd7t3dytomx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjuogxpdi5jd7t3dytomx.png" alt=" " width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the output. It works!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbxyxodu6xy9hm4jz8wh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbxyxodu6xy9hm4jz8wh.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Did I Containerize the Application? 🍽
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A Simple Analogy
&lt;/h3&gt;

&lt;p&gt;Using Docker is like a chef preparing a meal in a food container instead of serving it on a plate.  &lt;/p&gt;

&lt;p&gt;Without a container, the food might spill, mix with other meals, or not fit different kitchens. But with a container, the meal stays intact, portable, and works anywhere—whether in a restaurant, a food truck, or a customer's home.  &lt;/p&gt;

&lt;p&gt;Similarly, &lt;strong&gt;Docker&lt;/strong&gt; packages our app with everything it needs, ensuring it runs smoothly on any system without issues.  &lt;/p&gt;


&lt;h2&gt;
  
  
  What Does a Dockerfile Do? 📜
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Dockerfile&lt;/strong&gt; is like a recipe—it provides step-by-step instructions to build our container consistently.  &lt;/p&gt;

&lt;p&gt;Here’s my Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use a lightweight Python image  &lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.9-slim  &lt;/span&gt;

&lt;span class="c"&gt;# Set the working directory inside the container  &lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app  &lt;/span&gt;

&lt;span class="c"&gt;# Copy the requirements file into the container  &lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt requirements.txt  &lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies  &lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt  

&lt;span class="c"&gt;# Copy all files from the current directory into the container  &lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .  &lt;/span&gt;

&lt;span class="c"&gt;# Expose the port  &lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080  &lt;/span&gt;

&lt;span class="c"&gt;# Command to run the application  &lt;/span&gt;
8CMD ["python", "app.py"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deploying on AWS ☁️
&lt;/h2&gt;

&lt;p&gt;Since we're deploying on &lt;strong&gt;AWS&lt;/strong&gt;, I used &lt;strong&gt;ECR&lt;/strong&gt; instead of &lt;strong&gt;Docker Hub&lt;/strong&gt; for:  &lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Better security&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Faster performance&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Seamless AWS integration&lt;/strong&gt;  &lt;/p&gt;

&lt;h2&gt;
  
  
  Pushing the Docker Image to AWS ECR
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Step 1: Create an ECR Repository
&lt;/h2&gt;

&lt;p&gt;If you haven't already created an &lt;strong&gt;ECR repository&lt;/strong&gt;, do so with:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkj13iuflrumlk1bqhqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkj13iuflrumlk1bqhqc.png" alt=" " width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Authenticate Docker with AWS ECR
&lt;/h2&gt;

&lt;p&gt;Before pushing the image, log in to AWS ECR using the AWS CLI:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dv2fjg7ffo7d67z9asi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dv2fjg7ffo7d67z9asi.png" alt=" " width="800" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Tag the Docker Image
&lt;/h2&gt;

&lt;p&gt;Retrieve the repository URL and &lt;strong&gt;tag your Docker image&lt;/strong&gt;:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwxmbjdu8mbym3pymet6i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwxmbjdu8mbym3pymet6i.png" alt=" " width="800" height="37"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Push the Image to ECR
&lt;/h2&gt;

&lt;p&gt;Now, &lt;strong&gt;push the tagged image&lt;/strong&gt; to the repository:  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xp8c3lv6ba4tsche1vt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xp8c3lv6ba4tsche1vt.png" alt=" " width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring AWS ECS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Do We Need to Configure ECS?
&lt;/h3&gt;

&lt;p&gt;Think of ECS as &lt;strong&gt;a restaurant kitchen that runs and manages containers&lt;/strong&gt;. But before it can start serving meals (running containers), you need to:  &lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Set up the kitchen&lt;/strong&gt; (configure ECS)&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Tell the chefs what recipes to use&lt;/strong&gt; (define services and tasks)&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Assign kitchen equipment&lt;/strong&gt; (8choose EC2 or Fargate)  &lt;/p&gt;

&lt;p&gt;Without proper ECS configuration, it won’t know &lt;strong&gt;where or how to run our containers&lt;/strong&gt;.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Steps to Configure ECS
&lt;/h3&gt;

&lt;p&gt;1️⃣ &lt;strong&gt;Create a Cluster&lt;/strong&gt; – The foundation for running containers.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmsjucr8j5qstq2dd011.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmsjucr8j5qstq2dd011.png" alt=" " width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2️⃣ &lt;strong&gt;Define a Task Definition&lt;/strong&gt; – Like a recipe that specifies:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which container image to use (the image in our ECR)
&lt;/li&gt;
&lt;li&gt;CPU &amp;amp; memory allocation
&lt;/li&gt;
&lt;li&gt;Port settings and environment variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbx1s1wb7c5uv4tws6mle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbx1s1wb7c5uv4tws6mle.png" alt=" " width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3️⃣ &lt;strong&gt;Create a service&lt;/strong&gt;:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We set up how many tasks we want defined(we used 2) &lt;/li&gt;
&lt;li&gt;We set up security groups, Iam Policies(default)&lt;/li&gt;
&lt;li&gt;We configured our load balancer from here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6295mhw459mxvtxlf1xi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6295mhw459mxvtxlf1xi.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wng7txwwfpu4nojuzce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wng7txwwfpu4nojuzce.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Load Balancing
&lt;/h2&gt;

&lt;p&gt;To distribute incoming traffic evenly across multiple containers, we configured an &lt;strong&gt;Application Load Balancer (ALB)&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6yho3wcasvhuwje38nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6yho3wcasvhuwje38nt.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgse8y269f8idfgd04meh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgse8y269f8idfgd04meh.png" alt=" " width="800" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;High availability&lt;/strong&gt; – If one container goes down, traffic is redirected to another.&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Better performance&lt;/strong&gt; – Ensures no single container is overloaded.  &lt;/p&gt;

&lt;p&gt;To verify, we searched for our load balancer under &lt;strong&gt;EC2 Services&lt;/strong&gt; and accessed our API via the ALB's DNS. It worked! 🎉  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4pylyfsylmws0gpsztk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp4pylyfsylmws0gpsztk.png" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating an API Gateway Endpoint
&lt;/h2&gt;

&lt;p&gt;Instead of exposing our ECS services directly, we used &lt;strong&gt;API Gateway&lt;/strong&gt; as a front door to handle incoming requests.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Analogy: API Gateway is Like a Receptionist
&lt;/h3&gt;

&lt;p&gt;Just like a receptionist in a big company &lt;strong&gt;handles all incoming guests&lt;/strong&gt;, directs them appropriately, and ensures security, API Gateway:  &lt;/p&gt;

&lt;p&gt;📌 &lt;strong&gt;Receives incoming requests&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
📌 &lt;strong&gt;Routes them to the right backend service&lt;/strong&gt; (ECS)&lt;br&gt;&lt;br&gt;
📌 &lt;strong&gt;Ensures security and authentication&lt;/strong&gt;  &lt;/p&gt;

&lt;h3&gt;
  
  
  Steps to Set Up API Gateway
&lt;/h3&gt;

&lt;p&gt;1️⃣ &lt;strong&gt;Created a REST API&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdtdjwmqa9tpfjt1dwea9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdtdjwmqa9tpfjt1dwea9.png" alt=" " width="800" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fccj0xbreeqgbqz5oq04k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fccj0xbreeqgbqz5oq04k.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2️⃣ &lt;strong&gt;Created a resource &amp;amp; method&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60rq92aomtzew86h2vzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F60rq92aomtzew86h2vzk.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv3hh1eu1ueqhri15ovi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv3hh1eu1ueqhri15ovi1.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkga5mcj3l5w9rb5mxeis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkga5mcj3l5w9rb5mxeis.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0zzi9u6c22ilq91ay914.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0zzi9u6c22ilq91ay914.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3️⃣ &lt;strong&gt;Deployed the API&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F14jiy7o4micdaf8q6n2s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F14jiy7o4micdaf8q6n2s.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we can &lt;strong&gt;invoke our API successfully&lt;/strong&gt; to access football game schedules. 🚀 &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fckcy8uhcohagd4y0bvci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fckcy8uhcohagd4y0bvci.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Through this process, we successfully:&lt;br&gt;&lt;br&gt;
✅ Built and containerized a football schedule API&lt;br&gt;&lt;br&gt;
✅ Deployed it on AWS using ECS &amp;amp; Fargate&lt;br&gt;&lt;br&gt;
✅ Configured a load balancer for high availability&lt;br&gt;&lt;br&gt;
✅ Integrated API Gateway for security and request management  &lt;/p&gt;

&lt;p&gt;Now, users can access football schedules via our API! ⚽&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>api</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
