<?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: Muhammad Usama Saleem</title>
    <description>The latest articles on DEV Community by Muhammad Usama Saleem (@usama_saleem_148).</description>
    <link>https://dev.to/usama_saleem_148</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%2F4106517%2F846750d1-b9e2-4504-893c-b5cfe366c85f.png</url>
      <title>DEV Community: Muhammad Usama Saleem</title>
      <link>https://dev.to/usama_saleem_148</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usama_saleem_148"/>
    <language>en</language>
    <item>
      <title>Building an Event-Driven Order Processing System with AWS Lambda, SQS, DynamoDB, EventBridge, and Terraform</title>
      <dc:creator>Muhammad Usama Saleem</dc:creator>
      <pubDate>Thu, 10 Sep 2026 18:55:03 +0000</pubDate>
      <link>https://dev.to/usama_saleem_148/building-an-event-driven-order-processing-system-with-aws-lambda-sqs-dynamodb-eventbridge-and-569l</link>
      <guid>https://dev.to/usama_saleem_148/building-an-event-driven-order-processing-system-with-aws-lambda-sqs-dynamodb-eventbridge-and-569l</guid>
      <description>&lt;p&gt;In my previous AWS projects, I worked with services such as DynamoDB, Lambda, API Gateway, S3, IAM, and Terraform.&lt;/p&gt;

&lt;p&gt;This time, I wanted to build something a little closer to a real production architecture.&lt;/p&gt;

&lt;p&gt;Instead of processing an order synchronously inside an API request, we'll build an &lt;strong&gt;event-driven order processing system&lt;/strong&gt; where the API accepts an order, stores it, places a message on Amazon SQS, and lets a separate Lambda function process it asynchronously.&lt;/p&gt;

&lt;p&gt;We'll also handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic retries&lt;/li&gt;
&lt;li&gt;Failed messages&lt;/li&gt;
&lt;li&gt;Dead-letter queues&lt;/li&gt;
&lt;li&gt;Duplicate messages&lt;/li&gt;
&lt;li&gt;Idempotent processing&lt;/li&gt;
&lt;li&gt;EventBridge events&lt;/li&gt;
&lt;li&gt;CloudWatch logging&lt;/li&gt;
&lt;li&gt;IAM permissions&lt;/li&gt;
&lt;li&gt;Infrastructure as Code with Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire infrastructure is deployed using Terraform.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;The application accepts an order through an HTTP API.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   │
   ▼
API Gateway
   │
   ▼
Order Lambda
   │
   ├──────────────► DynamoDB
   │                 Orders
   │
   └──────────────► SQS
                       │
                       ▼
                Processor Lambda
                       │
                       ├──────► DynamoDB
                       │          Update Order
                       │
                       └──────► EventBridge
                                  OrderCompleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the processor repeatedly fails, Amazon SQS moves the message to a Dead-Letter Queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQS
 │
 │ processing fails
 │
 ▼
Retry
 │
 │ fails again
 ▼
Retry
 │
 │ maxReceiveCount reached
 ▼
DLQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that the API does not need to wait for the order to finish processing.&lt;/p&gt;

&lt;p&gt;It accepts the order and returns a &lt;code&gt;PENDING&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;The actual processing happens asynchronously.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The final architecture contains the following AWS services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway&lt;/li&gt;
&lt;li&gt;AWS Lambda&lt;/li&gt;
&lt;li&gt;Amazon DynamoDB&lt;/li&gt;
&lt;li&gt;Amazon SQS&lt;/li&gt;
&lt;li&gt;Amazon EventBridge&lt;/li&gt;
&lt;li&gt;Amazon CloudWatch&lt;/li&gt;
&lt;li&gt;AWS IAM&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will use two DynamoDB tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders
  └── OrderId

Products
  └── ProductId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order lifecycle is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   ↓
PROCESSING
   ↓
COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If processing repeatedly fails, the SQS message is moved to the DLQ.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account&lt;/li&gt;
&lt;li&gt;AWS CLI&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;AWS credentials configured&lt;/li&gt;
&lt;li&gt;Basic knowledge of Lambda, DynamoDB and SQS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify Terraform with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And verify AWS authentication with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;The Terraform project looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── main.tf
├── variables.tf
├── terraform.tfvars
├── sqs.tf
├── iam.tf
├── lambda.tf
├── api_gateway.tf
├── eventbridge.tf
├── cloudwatch.tf
├── outputs.tf
└── lambda/
    ├── order/
    │   └── index.mjs
    └── processor/
        └── index.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Terraform file is responsible for a specific part of the infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Configure Terraform
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;main.tf&lt;/code&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;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.6.0"&lt;/span&gt;

  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;archive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/archive"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.7"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&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;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AWS provider allows Terraform to communicate with AWS.&lt;/p&gt;

&lt;p&gt;The archive provider is used to package the Lambda source code into ZIP files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Define Variables
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;variables.tf&lt;/code&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS region"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"order_table_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DynamoDB order table name"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"product_table_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DynamoDB product table name"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure the values in &lt;code&gt;terraform.tfvars&lt;/code&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;aws_region&lt;/span&gt;         &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast-1"&lt;/span&gt;
&lt;span class="nx"&gt;order_table_name&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Orders"&lt;/span&gt;
&lt;span class="nx"&gt;product_table_name&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Products"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using variables makes the Terraform configuration easier to reuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Create DynamoDB Tables
&lt;/h2&gt;

&lt;p&gt;The first table stores orders.&lt;/p&gt;

&lt;p&gt;The second table stores products.&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;module&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_order_table"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/dynamodb-table/aws"&lt;/span&gt;

  &lt;span class="nx"&gt;name&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;order_table_name&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"OrderId"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;

  &lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;=&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;"OrderId"&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_product_table"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/dynamodb-table/aws"&lt;/span&gt;

  &lt;span class="nx"&gt;name&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;product_table_name&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ProductId"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;

  &lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;=&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;"ProductId"&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;
    &lt;span class="p"&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;The &lt;code&gt;Orders&lt;/code&gt; table uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as its partition key.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Products&lt;/code&gt; table uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProductId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as its partition key.&lt;/p&gt;

&lt;p&gt;We use:&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;billing_mode&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because this project does not require us to manage provisioned read and write capacity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Create the SQS Queue
&lt;/h2&gt;

&lt;p&gt;Now we need a queue between the API and the processing Lambda.&lt;/p&gt;

&lt;p&gt;First, create the Dead-Letter Queue:&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_sqs_queue"&lt;/span&gt; &lt;span class="s2"&gt;"order_processing_dlq"&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;"order-processing-dlq"&lt;/span&gt;

  &lt;span class="nx"&gt;message_retention_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1209600&lt;/span&gt;

  &lt;span class="nx"&gt;sqs_managed_sse_enabled&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DLQ retains messages for 14 days.&lt;/p&gt;

&lt;p&gt;Now create the main queue:&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_sqs_queue"&lt;/span&gt; &lt;span class="s2"&gt;"order_processing"&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;"order-processing"&lt;/span&gt;

  &lt;span class="nx"&gt;visibility_timeout_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
  &lt;span class="nx"&gt;message_retention_seconds&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;345600&lt;/span&gt;
  &lt;span class="nx"&gt;receive_wait_time_seconds&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

  &lt;span class="nx"&gt;sqs_managed_sse_enabled&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;redrive_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;deadLetterTargetArn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_processing_dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;maxReceiveCount&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&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;There are a few important settings here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visibility timeout
&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;visibility_timeout_seconds&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Lambda receives a message, SQS temporarily hides that message from other consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Message retention
&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;message_retention_seconds&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;345600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main queue keeps messages for up to four days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dead-Letter Queue
&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;maxReceiveCount&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the message is received four times without being successfully processed, SQS moves it to the DLQ.&lt;/p&gt;

&lt;p&gt;This prevents a permanently broken message from being retried forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Create the Order Lambda
&lt;/h2&gt;

&lt;p&gt;The Order Lambda is responsible for accepting the API request.&lt;/p&gt;

&lt;p&gt;It performs the following operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the request&lt;/li&gt;
&lt;li&gt;Check that the product exists&lt;/li&gt;
&lt;li&gt;Generate an order ID&lt;/li&gt;
&lt;li&gt;Store the order in DynamoDB&lt;/li&gt;
&lt;li&gt;Send the order ID to SQS&lt;/li&gt;
&lt;li&gt;Return the order ID to the client&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The environment variables are:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_order_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_table_id&lt;/span&gt;
    &lt;span class="nx"&gt;PRODUCT_TABLE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_product_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_table_id&lt;/span&gt;
    &lt;span class="nx"&gt;QUEUE_URL&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_processing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;The Lambda function uses Node.js 22:&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_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"order"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"order-service"&lt;/span&gt;

  &lt;span class="nx"&gt;filename&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_path&lt;/span&gt;
  &lt;span class="nx"&gt;source_code_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_base64sha256&lt;/span&gt;

  &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nodejs22.x"&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.handler"&lt;/span&gt;

  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;timeout&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="nx"&gt;memory_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&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;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_order_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_table_id&lt;/span&gt;
      &lt;span class="nx"&gt;PRODUCT_TABLE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_product_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_table_id&lt;/span&gt;
      &lt;span class="nx"&gt;QUEUE_URL&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_processing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
    &lt;span class="p"&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;The Lambda source code uses the AWS SDK for DynamoDB and SQS.&lt;/p&gt;

&lt;p&gt;A simplified version of the request flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;await&lt;/span&gt; &lt;span class="nx"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendMessageCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;QueueUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUEUE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&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;The important thing is that the API does not process the order itself.&lt;/p&gt;

&lt;p&gt;It only creates the order and places a message on SQS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Create API Gateway
&lt;/h2&gt;

&lt;p&gt;Now we expose the Lambda through an HTTP API.&lt;/p&gt;

&lt;p&gt;The API contains two routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /orders
GET  /orders/{orderId}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The POST endpoint creates an order.&lt;/p&gt;

&lt;p&gt;The GET endpoint retrieves its current status.&lt;/p&gt;

&lt;p&gt;The architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   ↓
API Gateway
   ↓
Order Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API Gateway uses an AWS_PROXY integration, allowing the Lambda function to receive the HTTP request directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Connect SQS to the Processor Lambda
&lt;/h2&gt;

&lt;p&gt;Now we create the second Lambda.&lt;/p&gt;

&lt;p&gt;Its job is to consume messages from SQS.&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_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"processor"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"order-processor"&lt;/span&gt;

  &lt;span class="nx"&gt;filename&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processor_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_path&lt;/span&gt;
  &lt;span class="nx"&gt;source_code_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processor_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_base64sha256&lt;/span&gt;

  &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nodejs22.x"&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.handler"&lt;/span&gt;

  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processor_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;timeout&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
  &lt;span class="nx"&gt;memory_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&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;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_order_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dynamodb_table_id&lt;/span&gt;
      &lt;span class="nx"&gt;EVENT_BUS_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_event_bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&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="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;Then connect SQS to Lambda:&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_lambda_event_source_mapping"&lt;/span&gt; &lt;span class="s2"&gt;"order_processing"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event_source_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sqs_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_processing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;batch_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means Lambda automatically polls the SQS queue and invokes the processor when messages are available.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Process the Order
&lt;/h2&gt;

&lt;p&gt;The processor first reads the order from DynamoDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;If the order doesn't exist, we throw an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existingOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not exist`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not exist`&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;This is important.&lt;/p&gt;

&lt;p&gt;We want Lambda to fail in this situation.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because SQS uses the Lambda invocation result to determine whether the message was successfully processed.&lt;/p&gt;

&lt;p&gt;If Lambda throws an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda failure
      ↓
SQS message becomes available again
      ↓
Lambda retries
      ↓
Repeated failures
      ↓
DLQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 9: Prevent Duplicate Processing
&lt;/h2&gt;

&lt;p&gt;Amazon SQS provides &lt;strong&gt;at-least-once delivery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means the same message can potentially be delivered more than once.&lt;/p&gt;

&lt;p&gt;So we cannot assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 message = 1 Lambda invocation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, our processor needs to be idempotent.&lt;/p&gt;

&lt;p&gt;We store the order status in DynamoDB.&lt;/p&gt;

&lt;p&gt;Before processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processor conditionally changes it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROCESSING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UpdateItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SET #status = :processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#status = :pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PROCESSING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;The condition is important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status must currently be PENDING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only then can the processor claim the order.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 10: Handle Duplicate Messages
&lt;/h2&gt;

&lt;p&gt;After the order is already completed, another copy of the message might arrive.&lt;/p&gt;

&lt;p&gt;We check the current status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;currentStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PROCESSING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;currentStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMPLETED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Order &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has already been processed or is being processed`&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the processor idempotent.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message 1
   ↓
PENDING
   ↓
PROCESSING
   ↓
COMPLETED

Message 2
   ↓
COMPLETED
   ↓
Ignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second message is successfully consumed without processing the order again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 11: Complete the Order
&lt;/h2&gt;

&lt;p&gt;After processing, we update the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UpdateItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ORDER_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SET #status = :completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#status = :processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PROCESSING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMPLETED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;The state transition is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   ↓
PROCESSING
   ↓
COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conditional update prevents the order from being completed from an unexpected state.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 12: Publish an Event with EventBridge
&lt;/h2&gt;

&lt;p&gt;Once the order is completed, we publish an event.&lt;/p&gt;

&lt;p&gt;First, create the custom event bus:&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_event_bus"&lt;/span&gt; &lt;span class="s2"&gt;"orders"&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;"order-events"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the processor publishes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eventbridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutEventsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;EventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EVENT_BUS_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order-service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;DetailType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OrderCompleted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="na"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="nx"&gt;orderId&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;The resulting event contains information such as:&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-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;"OrderCompleted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&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;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b91eb8b4-0c80-4305-9651-518f6347b8dc"&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;This gives us another useful architectural property.&lt;/p&gt;

&lt;p&gt;The processor does not need to know what happens after an order is completed.&lt;/p&gt;

&lt;p&gt;Other systems can subscribe to the event later.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌── Email Service
                 │
OrderCompleted ──┼── Analytics
                 │
                 ├── Notification Service
                 │
                 └── Inventory Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the main benefits of event-driven architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 13: Configure IAM
&lt;/h2&gt;

&lt;p&gt;Each Lambda gets its own execution role.&lt;/p&gt;

&lt;p&gt;The Order Lambda only needs permissions required for its job.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DynamoDB
  ├── GetItem
  └── PutItem

SQS
  └── SendMessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Processor Lambda has different permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DynamoDB
  ├── GetItem
  └── UpdateItem

SQS
  ├── ReceiveMessage
  ├── DeleteMessage
  └── GetQueueAttributes

EventBridge
  └── PutEvents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the principle of least privilege.&lt;/p&gt;

&lt;p&gt;The processor does not need permission to create DynamoDB tables.&lt;/p&gt;

&lt;p&gt;The Order Lambda does not need permission to publish EventBridge events.&lt;/p&gt;

&lt;p&gt;Each role gets only the permissions required by that Lambda.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 14: Add CloudWatch Logs
&lt;/h2&gt;

&lt;p&gt;Each Lambda gets a dedicated CloudWatch log group.&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_log_group"&lt;/span&gt; &lt;span class="s2"&gt;"order_lambda"&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;"/aws/lambda/${aws_lambda_function.order.function_name}"&lt;/span&gt;

  &lt;span class="nx"&gt;retention_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&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_cloudwatch_log_group"&lt;/span&gt; &lt;span class="s2"&gt;"processor_lambda"&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;"/aws/lambda/${aws_lambda_function.processor.function_name}"&lt;/span&gt;

  &lt;span class="nx"&gt;retention_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us visibility into the application.&lt;/p&gt;

&lt;p&gt;For example, the processor logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order b91eb8b4-0c80-4305-9651-518f6347b8dc currently has status: PENDING

Started processing order b91eb8b4-0c80-4305-9651-518f6347b8dc

Order b91eb8b4-0c80-4305-9651-518f6347b8dc completed

OrderCompleted event published for order b91eb8b4-0c80-4305-9651-518f6347b8dc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 15: Initialize Terraform
&lt;/h2&gt;

&lt;p&gt;Now initialize the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then validate the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success! The configuration is valid.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 16: Review the Deployment
&lt;/h2&gt;

&lt;p&gt;Before creating anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will show the resources it intends to create.&lt;/p&gt;

&lt;p&gt;Always review the plan before applying infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 17: Deploy
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
Lambda
Lambda IAM Roles
DynamoDB
SQS
SQS DLQ
EventBridge
CloudWatch Log Groups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the deployment finishes, Terraform should report the resources it created.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 18: Seed a Product
&lt;/h2&gt;

&lt;p&gt;Before creating an order, we need a product.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws dynamodb put-item &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; Products &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--item&lt;/span&gt; &lt;span class="s1"&gt;'{
    "ProductId": {"S":"product-456"},
    "Name": {"S":"Test Laptop"},
    "Price": {"N":"999.99"}
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the order API can reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product-456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 19: Create an Order
&lt;/h2&gt;

&lt;p&gt;Our API accepts:&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;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product-456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;Send the request:&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;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://YOUR_API_ID.execute-api.ap-southeast-1.amazonaws.com/orders"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "customerId":"customer-123",
    "productId":"product-456",
    "quantity":2
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API immediately returns:&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;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b91eb8b4-0c80-4305-9651-518f6347b8dc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PENDING"&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;Notice that the API doesn't wait for the processor.&lt;/p&gt;

&lt;p&gt;The order is now waiting for asynchronous processing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 20: Check the Order
&lt;/h2&gt;

&lt;p&gt;Now request:&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="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://YOUR_API_ID.execute-api.ap-southeast-1.amazonaws.com/orders/b91eb8b4-0c80-4305-9651-518f6347b8dc"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After processing, the result is:&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;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b91eb8b4-0c80-4305-9651-518f6347b8dc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product-456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMPLETED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-10T18:28:39.336Z"&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;The complete flow is now working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
     ↓
Order Lambda
     ↓
DynamoDB
     ↓
SQS
     ↓
Processor Lambda
     ↓
DynamoDB
     ↓
COMPLETED
     ↓
EventBridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 21: Test Failure Handling
&lt;/h2&gt;

&lt;p&gt;A good distributed system should not only handle successful requests.&lt;/p&gt;

&lt;p&gt;We also need to test failures.&lt;/p&gt;

&lt;p&gt;I sent a message to SQS containing an order ID that doesn't exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs send-message &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_QUEUE_URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message-body&lt;/span&gt; &lt;span class="s1"&gt;'{"orderId":"does-not-exist"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processor detects that the order doesn't exist and throws an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR Order does-not-exist does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Lambda failed, SQS retries the message.&lt;/p&gt;

&lt;p&gt;After the configured number of failures, the message is moved to the DLQ.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQS
 ↓
Lambda
 ↓
Failure
 ↓
Retry
 ↓
Failure
 ↓
Retry
 ↓
Failure
 ↓
Retry
 ↓
Failure
 ↓
DLQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can verify the DLQ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs get-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_DLQ_URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-names&lt;/span&gt; ApproximateNumberOfMessages &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The queue reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ApproximateNumberOfMessages: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirmed that the failure path was working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 22: Test Duplicate Processing
&lt;/h2&gt;

&lt;p&gt;This is one of the most important tests.&lt;/p&gt;

&lt;p&gt;SQS provides at-least-once delivery, so we need to make sure a duplicate message does not process the order again.&lt;/p&gt;

&lt;p&gt;I manually sent the already completed order back to SQS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs send-message &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_QUEUE_URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message-body&lt;/span&gt; &lt;span class="s1"&gt;'{"orderId":"b91eb8b4-0c80-4305-9651-518f6347b8dc"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processor found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order b91eb8b4-0c80-4305-9651-518f6347b8dc currently has status: COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order b91eb8b4-0c80-4305-9651-518f6347b8dc has already been processed or is being processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda invocation succeeded without processing the order again.&lt;/p&gt;

&lt;p&gt;This proves our duplicate protection works.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Have Tested
&lt;/h2&gt;

&lt;p&gt;At this point, the system has three important paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Successful order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   ↓
PROCESSING
   ↓
COMPLETED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Failed order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda failure
   ↓
SQS retry
   ↓
SQS retry
   ↓
SQS retry
   ↓
SQS retry
   ↓
DLQ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Duplicate order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMPLETED
   ↓
Duplicate message
   ↓
Detected
   ↓
Ignored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These tests are more useful than simply checking whether Terraform created the AWS resources.&lt;/p&gt;

&lt;p&gt;They verify that the actual distributed workflow behaves as expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use SQS Here?
&lt;/h2&gt;

&lt;p&gt;Without SQS, the API could directly invoke the processing logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API
 ↓
Lambda
 ↓
Process Order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But now the API is coupled to the processing operation.&lt;/p&gt;

&lt;p&gt;With SQS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API
 ↓
SQS
 ↓
Processor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer and consumer are separated.&lt;/p&gt;

&lt;p&gt;This gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asynchronous processing&lt;/li&gt;
&lt;li&gt;Automatic retries&lt;/li&gt;
&lt;li&gt;Buffering&lt;/li&gt;
&lt;li&gt;Failure isolation&lt;/li&gt;
&lt;li&gt;DLQ support&lt;/li&gt;
&lt;li&gt;Independent scaling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the processor temporarily fails, the API can still accept orders.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use EventBridge?
&lt;/h2&gt;

&lt;p&gt;SQS is useful for &lt;strong&gt;work that needs to be processed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;EventBridge is useful for &lt;strong&gt;events that other systems may want to react to&lt;/strong&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Processor
      │
      ▼
OrderCompleted
      │
      ├── Notification
      ├── Analytics
      ├── Inventory
      └── Billing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order processor doesn't need to directly call every downstream service.&lt;/p&gt;

&lt;p&gt;It simply publishes an event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use DynamoDB for the Order State?
&lt;/h2&gt;

&lt;p&gt;DynamoDB gives us a simple way to store the current state of an order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderId
CustomerId
ProductId
Quantity
Status
CreatedAt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More importantly, DynamoDB conditional writes allow us to make state transitions safely.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING → PROCESSING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only succeeds when the order is actually &lt;code&gt;PENDING&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That becomes important when multiple Lambda invocations could potentially see the same message.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infrastructure as Code
&lt;/h2&gt;

&lt;p&gt;Everything in this project is defined using Terraform.&lt;/p&gt;

&lt;p&gt;Instead of manually creating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda
SQS
DynamoDB
API Gateway
IAM
EventBridge
CloudWatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;through the AWS Console, the infrastructure is represented as code.&lt;/p&gt;

&lt;p&gt;The workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terraform
    ↓
terraform plan
    ↓
Review
    ↓
terraform apply
    ↓
AWS Infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the environment reproducible and easier to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Important Production Consideration
&lt;/h2&gt;

&lt;p&gt;The current architecture is intentionally simple, but there are two reliability issues that I would address before calling this production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  DynamoDB → SQS
&lt;/h2&gt;

&lt;p&gt;Currently the order flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write Order to DynamoDB
        ↓
Send Message to SQS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine DynamoDB succeeds but SQS fails.&lt;/p&gt;

&lt;p&gt;The order would exist as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but there would be no message to process it.&lt;/p&gt;

&lt;p&gt;A production system could use a &lt;strong&gt;transactional outbox pattern&lt;/strong&gt; or another reliable event publication strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  DynamoDB → EventBridge
&lt;/h2&gt;

&lt;p&gt;There is a similar issue here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update Order → COMPLETED
        ↓
Publish EventBridge event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the DynamoDB update succeeds but EventBridge publishing fails, the order could remain &lt;code&gt;COMPLETED&lt;/code&gt; while the event is never published.&lt;/p&gt;

&lt;p&gt;A more robust architecture would make event publication retry-safe as well.&lt;/p&gt;

&lt;p&gt;These are important considerations once the system moves beyond a learning project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Architecture
&lt;/h2&gt;

&lt;p&gt;The complete system looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌──────────────────┐
                         │      Client      │
                         └────────┬─────────┘
                                  │
                                  ▼
                         ┌──────────────────┐
                         │   API Gateway    │
                         └────────┬─────────┘
                                  │
                                  ▼
                         ┌──────────────────┐
                         │  Order Lambda    │
                         └───────┬───┬──────┘
                                 │   │
                    ┌────────────┘   └─────────────┐
                    ▼                              ▼
             ┌─────────────┐                ┌─────────────┐
             │  DynamoDB   │                │     SQS     │
             │   Orders    │                │    Queue    │
             └─────────────┘                └──────┬──────┘
                                                   │
                                                   ▼
                                            ┌──────────────┐
                                            │  Processor   │
                                            │    Lambda    │
                                            └──────┬───────┘
                                                   │
                                      ┌────────────┴────────────┐
                                      ▼                         ▼
                               ┌─────────────┐          ┌──────────────┐
                               │  DynamoDB   │          │  EventBridge │
                               │   Orders    │          │  OrderEvents │
                               └─────────────┘          └──────────────┘

                         SQS failures
                              │
                              ▼
                       ┌─────────────┐
                       │     DLQ     │
                       └─────────────┘

                         Lambda Logs
                              │
                              ▼
                       ┌─────────────┐
                       │ CloudWatch  │
                       └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The main concepts demonstrated in this project are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;API Gateway can expose a serverless HTTP API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lambda can handle the API request without managing servers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DynamoDB can store the order state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQS can decouple order creation from order processing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQS automatically retries messages when processing fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Dead-Letter Queue provides a place to investigate repeatedly failed messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQS provides at-least-once delivery, so consumers should be designed to handle duplicates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DynamoDB conditional updates can help implement idempotent state transitions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EventBridge can publish domain events such as &lt;code&gt;OrderCompleted&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IAM roles can restrict each Lambda to only the AWS actions it needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CloudWatch provides logs for observing the serverless application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Terraform allows the entire infrastructure to be deployed as code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important lesson for me was that building an event-driven system is not just about connecting AWS services.&lt;/p&gt;

&lt;p&gt;The interesting part is handling what happens when things go wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What if processing fails?

What if the same message arrives twice?

What if an order doesn't exist?

What happens after repeated failures?

How do we prevent duplicate processing?

How do we allow other systems to react to completed orders?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those failure cases are where the architecture becomes much more interesting than simply deploying a Lambda function.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;The next step for this project is to build a small frontend that visualizes the complete order pipeline.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order

     ↓

API Gateway
     ↓
Order Lambda
     ↓
DynamoDB
     ↓
SQS
     ↓
Processor Lambda
     ↓
COMPLETED
     ↓
EventBridge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend will also be able to demonstrate successful orders, duplicate messages, processing failures, retries, and DLQ behavior.&lt;/p&gt;

&lt;p&gt;That should make the architecture much easier to understand visually.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Deploy DynamoDB with Terraform on AWS</title>
      <dc:creator>Muhammad Usama Saleem</dc:creator>
      <pubDate>Fri, 04 Sep 2026 19:24:02 +0000</pubDate>
      <link>https://dev.to/usama_saleem_148/how-to-deploy-dynamodb-with-terraform-on-aws-3gn2</link>
      <guid>https://dev.to/usama_saleem_148/how-to-deploy-dynamodb-with-terraform-on-aws-3gn2</guid>
      <description>&lt;h2&gt;
  
  
  How to Deploy DynamoDB Tables on AWS Using Terraform
&lt;/h2&gt;

&lt;p&gt;Infrastructure as Code allows us to define cloud infrastructure using configuration files instead of creating resources manually through the AWS Console.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll use &lt;strong&gt;Terraform&lt;/strong&gt; to create and manage two &lt;strong&gt;Amazon DynamoDB&lt;/strong&gt; tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders
Products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the end, you'll have both tables deployed to AWS and managed entirely through Terraform.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 What We Are Building
&lt;/h2&gt;

&lt;p&gt;The final infrastructure will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terraform
    │
    ▼
AWS Provider
    │
    ▼
DynamoDB
    ├── Orders
    │   └── OrderId
    │
    └── Products
        └── ProductId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Orders&lt;/code&gt; table will use &lt;code&gt;OrderId&lt;/code&gt; as its partition key, while the &lt;code&gt;Products&lt;/code&gt; table will use &lt;code&gt;ProductId&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account&lt;/li&gt;
&lt;li&gt;Terraform installed&lt;/li&gt;
&lt;li&gt;AWS credentials configured for Terraform&lt;/li&gt;
&lt;li&gt;An AWS identity with permissions to create and manage DynamoDB tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify Terraform is installed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should also make sure Terraform can authenticate with AWS before continuing.&lt;/p&gt;




&lt;h2&gt;
  
  
  📁 Step 1: Create the Terraform Files
&lt;/h2&gt;

&lt;p&gt;Create a directory for the Terraform configuration.&lt;/p&gt;

&lt;p&gt;Inside it, create these four files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── providers.tf
├── variables.tf
├── terraform.tfvars
└── dynamodb.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each file has a specific purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&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;code&gt;providers.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Terraform and AWS provider configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Variable definitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;terraform.tfvars&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Values for those variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dynamodb.tf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DynamoDB table configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Terraform automatically loads all &lt;code&gt;.tf&lt;/code&gt; files in the same directory, so there is no need to reference each file individually.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Step 2: Configure the AWS Provider
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;providers.tf&lt;/code&gt; and add:&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;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.6.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&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;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important parts here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform version
&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;required_version&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.6.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This specifies the Terraform versions supported by the configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS provider
&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;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 6.0"&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;The AWS provider allows Terraform to communicate with AWS APIs.&lt;/p&gt;

&lt;p&gt;The provider is what allows Terraform to create and manage AWS resources such as DynamoDB tables.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 Step 3: Define the Variables
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;variables.tf&lt;/code&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;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The AWS region to deploy resources in"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-aws-region"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"order_table_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The name of the DynamoDB order table"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Orders"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"product_table_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The name of the DynamoDB product table"&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Products"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These variables allow us to keep configuration values separate from our infrastructure definitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Step 4: Set the Variable Values
&lt;/h2&gt;

&lt;p&gt;Now open &lt;code&gt;terraform.tfvars&lt;/code&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;aws_region&lt;/span&gt;         &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-aws-region"&lt;/span&gt;
&lt;span class="nx"&gt;order_table_name&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Orders"&lt;/span&gt;
&lt;span class="nx"&gt;product_table_name&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Products"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-aws-region
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the AWS region where you want to deploy the tables.&lt;/p&gt;

&lt;p&gt;For example:&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;aws_region&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using variables makes it easier to reuse the same Terraform configuration across different environments or AWS regions.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗄️ Step 5: Define the DynamoDB Tables
&lt;/h2&gt;

&lt;p&gt;Now we can create the actual DynamoDB infrastructure.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;dynamodb.tf&lt;/code&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;module&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_order_table"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/dynamodb-table/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;name&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;order_table_name&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"OrderId"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;

  &lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;=&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;"OrderId"&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"dynamodb_product_table"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/dynamodb-table/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;name&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;product_table_name&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ProductId"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;

  &lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="p"&gt;=&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;"ProductId"&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;
    &lt;span class="p"&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;We are using the community-maintained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform-aws-modules/dynamodb-table/aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform module to simplify the DynamoDB configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Understanding the DynamoDB Configuration
&lt;/h2&gt;

&lt;p&gt;Let's break down the important parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;source&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/dynamodb-table/aws"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Terraform which module to use for creating the DynamoDB table.&lt;/p&gt;

&lt;p&gt;Modules allow us to reuse existing Terraform configuration instead of implementing every resource detail ourselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;name&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_table_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This determines the DynamoDB table name.&lt;/p&gt;

&lt;p&gt;Because we're using a variable, the name can be changed without modifying the resource configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;hash_key&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;hash_key&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"OrderId"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines the DynamoDB partition key.&lt;/p&gt;

&lt;p&gt;For the Orders table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is the partition key.&lt;/p&gt;

&lt;p&gt;For the Products table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ProductId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is the partition key.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;billing_mode&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configures DynamoDB's on-demand capacity mode.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;PAY_PER_REQUEST&lt;/code&gt;, we don't need to manually configure provisioned read and write capacity.&lt;/p&gt;

&lt;p&gt;This is convenient for workloads where traffic can vary significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;attributes&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For the Orders table:&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;attributes&lt;/span&gt; &lt;span class="err"&gt;=&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;"OrderId"&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&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;code&gt;S&lt;/code&gt; means the attribute is a string.&lt;/p&gt;

&lt;p&gt;The important thing to understand is that DynamoDB does &lt;strong&gt;not&lt;/strong&gt; require us to define every attribute that an item might contain.&lt;/p&gt;

&lt;p&gt;For example, an order could look like:&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;"OrderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CustomerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer-456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ProductId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"product-789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"OrderStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PENDING"&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;We don't need to declare &lt;code&gt;CustomerId&lt;/code&gt;, &lt;code&gt;ProductId&lt;/code&gt;, &lt;code&gt;Quantity&lt;/code&gt;, or &lt;code&gt;OrderStatus&lt;/code&gt; in the Terraform &lt;code&gt;attributes&lt;/code&gt; block unless they participate in the table's key schema or indexes.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 Step 6: Make Sure AWS Permissions Are Available
&lt;/h2&gt;

&lt;p&gt;Terraform communicates with AWS through API calls.&lt;/p&gt;

&lt;p&gt;Therefore, the AWS identity used by Terraform needs the required DynamoDB permissions.&lt;/p&gt;

&lt;p&gt;Depending on the configuration, permissions can include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dynamodb:CreateTable
dynamodb:DescribeTable
dynamodb:DeleteTable
dynamodb:UpdateTable
dynamodb:TagResource
dynamodb:ListTagsOfResource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact permissions required depend on what Terraform needs to create and manage.&lt;/p&gt;

&lt;p&gt;The important concept is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Terraform itself does not bypass AWS permissions. Every AWS operation performed by Terraform is subject to IAM authorization.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🚀 Step 7: Initialize Terraform
&lt;/h2&gt;

&lt;p&gt;Now that the configuration is ready, initialize Terraform.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will download the required AWS provider and initialize the modules used by the configuration.&lt;/p&gt;

&lt;p&gt;You should run &lt;code&gt;terraform init&lt;/code&gt; whenever you start working with a new Terraform configuration or when provider/module requirements change.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Step 8: Validate the Configuration
&lt;/h2&gt;

&lt;p&gt;Before deploying anything, validate the Terraform configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the configuration is valid, Terraform will report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Success! The configuration is valid.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation checks the configuration itself. It does not deploy anything to AWS.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔎 Step 9: Preview the Deployment
&lt;/h2&gt;

&lt;p&gt;Next, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will calculate the changes required to make the AWS environment match the configuration.&lt;/p&gt;

&lt;p&gt;You should see Terraform planning to create the two DynamoDB tables.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terraform Configuration
        ↓
    terraform plan
        ↓
Changes Terraform intends to make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is extremely important.&lt;/p&gt;

&lt;p&gt;Before allowing Terraform to modify infrastructure, review the plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Step 10: Deploy the DynamoDB Tables
&lt;/h2&gt;

&lt;p&gt;Once you're satisfied with the plan, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will display the planned changes and ask for confirmation.&lt;/p&gt;

&lt;p&gt;After confirmation, Terraform will create the DynamoDB tables in AWS.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terraform Configuration
        ↓
terraform plan
        ↓
Review Changes
        ↓
terraform apply
        ↓
AWS DynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔍 Step 11: Verify the Terraform Resources
&lt;/h2&gt;

&lt;p&gt;After the deployment completes, you can check the resources Terraform is managing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see entries corresponding to the two DynamoDB tables, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.dynamodb_order_table.aws_dynamodb_table.this[0]
module.dynamodb_product_table.aws_dynamodb_table.this[0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also verify the tables directly in the AWS DynamoDB console.&lt;/p&gt;

&lt;p&gt;You should now have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders
    Partition Key: OrderId

Products
    Partition Key: ProductId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✅ Step 12: Run Terraform Plan Again
&lt;/h2&gt;

&lt;p&gt;Finally, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;terraform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything matches the Terraform configuration, Terraform should report that there are no changes to make.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No changes. Your infrastructure matches the configuration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means Terraform has reached the desired state defined by the configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Final Result
&lt;/h2&gt;

&lt;p&gt;We have successfully defined and deployed two DynamoDB tables using Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Terraform
                        │
                        ▼
                 AWS Provider
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
          DynamoDB             DynamoDB
           Orders              Products
              │                   │
        OrderId PK          ProductId PK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Terraform files
        ↓
Configure AWS provider
        ↓
Define variables
        ↓
Define DynamoDB tables
        ↓
terraform init
        ↓
terraform validate
        ↓
terraform plan
        ↓
terraform apply
        ↓
Verify resources
        ↓
terraform plan
        ↓
No changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The important concepts from this tutorial are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terraform configuration describes the desired infrastructure.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The AWS provider allows Terraform to communicate with AWS.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terraform modules simplify resource configuration and reuse.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DynamoDB tables can be defined entirely through Terraform.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform init&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;prepares the Terraform working environment.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform validate&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;checks the configuration.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;shows what Terraform intends to change.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform apply&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;actually creates or modifies AWS infrastructure.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Terraform state tracks the infrastructure Terraform manages.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Running&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;terraform plan&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;after deployment should show no changes when the infrastructure matches the configuration.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The main idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Define your AWS infrastructure as code, review the planned changes, and let Terraform create and manage the resources for you.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>dynamodb</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Serverless CRUD API with AWS Lambda, API Gateway, and DynamoDB</title>
      <dc:creator>Muhammad Usama Saleem</dc:creator>
      <pubDate>Thu, 03 Sep 2026 16:28:24 +0000</pubDate>
      <link>https://dev.to/usama_saleem_148/building-a-serverless-crud-api-with-aws-lambda-api-gateway-and-dynamodb-2986</link>
      <guid>https://dev.to/usama_saleem_148/building-a-serverless-crud-api-with-aws-lambda-api-gateway-and-dynamodb-2986</guid>
      <description>&lt;p&gt;Building an API on AWS is one thing. Understanding what happens between the client request, API Gateway, Lambda, DynamoDB, IAM, and the deployment pipeline is another.&lt;/p&gt;

&lt;p&gt;For this hands-on project, I built a complete serverless CRUD API using Amazon API Gateway, AWS Lambda, and Amazon DynamoDB.&lt;/p&gt;

&lt;p&gt;The goal was to understand how these services work together and to build the API from the ground up rather than treating the architecture as a black box.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ What I Built
&lt;/h2&gt;

&lt;p&gt;The application follows a simple serverless architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  │
  ▼
API Gateway
  │
  ▼
AWS Lambda
  │
  ▼
DynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For deployment, GitHub Actions was added to the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
  │
  ▼
GitHub Actions
  │
  ▼
GitHub OIDC
  │
  ▼
AWS IAM
  │
  ▼
AWS Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a REST-style CRUD API without managing servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ AWS Services Used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&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;Amazon API Gateway&lt;/td&gt;
&lt;td&gt;Expose HTTP API endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;td&gt;Run the application logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon DynamoDB&lt;/td&gt;
&lt;td&gt;Store application data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS IAM&lt;/td&gt;
&lt;td&gt;Manage permissions and roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS STS&lt;/td&gt;
&lt;td&gt;Provide temporary credentials through role assumption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;Automate Lambda deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub OIDC&lt;/td&gt;
&lt;td&gt;Authenticate GitHub Actions with AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📌 API Operations
&lt;/h2&gt;

&lt;p&gt;The API supports the main CRUD operations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;HTTP Method&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;Create&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Create a new user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Retrieve one or all users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update&lt;/td&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;Update an existing user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete&lt;/td&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;Delete an existing user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /users
GET /users
GET /users/{name}
PUT /users/{name}
DELETE /users/{name}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact API Gateway routes are configured in AWS.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗄️ DynamoDB Design
&lt;/h2&gt;

&lt;p&gt;The project uses a DynamoDB table with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Partition Key: name
Type: String
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user record looks conceptually like:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ali"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ali@example.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;"followers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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;One important DynamoDB concept I worked through was the role of the primary key.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;name&lt;/code&gt; is the partition key, it uniquely identifies the item in this table.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔎 Get One vs Get All
&lt;/h2&gt;

&lt;p&gt;One of the concepts I wanted to understand properly was the difference between retrieving a specific DynamoDB item and scanning the table.&lt;/p&gt;

&lt;p&gt;For a single user, Lambda uses a &lt;code&gt;GetCommand&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For retrieving all users, Lambda uses a &lt;code&gt;ScanCommand&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users/Ali
      │
      ▼
DynamoDB GetItem
      │
      ▼
Ali
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users
      │
      ▼
DynamoDB Scan
      │
      ▼
All users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction became particularly useful while testing the API.&lt;/p&gt;




&lt;h2&gt;
  
  
  ➕ Create-Only POST Requests
&lt;/h2&gt;

&lt;p&gt;I didn't want a POST request to silently overwrite an existing user.&lt;/p&gt;

&lt;p&gt;DynamoDB normally allows a &lt;code&gt;PutItem&lt;/code&gt; operation to replace an existing item with the same primary key.&lt;/p&gt;

&lt;p&gt;To make the endpoint behave like a create operation, I added a condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;attribute_not_exists(#name)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;name&lt;/code&gt; is used as the partition key, this condition prevents an existing record from being replaced.&lt;/p&gt;

&lt;p&gt;A duplicate request therefore returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 409 Conflict
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of overwriting the existing record.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✏️ Updating Records
&lt;/h2&gt;

&lt;p&gt;The PUT endpoint uses DynamoDB's &lt;code&gt;UpdateCommand&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It updates the user's fields and returns the updated item.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT /users/Ali
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can update values such as:&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;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new-email@example.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;"followers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&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;The updated record is then returned by the API.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗑️ Deleting Records
&lt;/h2&gt;

&lt;p&gt;The DELETE endpoint first checks whether the user exists.&lt;/p&gt;

&lt;p&gt;If the user exists, Lambda removes the DynamoDB item.&lt;/p&gt;

&lt;p&gt;If the user does not exist, the API returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 404 Not Found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the API behavior predictable for the client.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Lambda
&lt;/h2&gt;

&lt;p&gt;The API logic runs inside AWS Lambda.&lt;/p&gt;

&lt;p&gt;The function handles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reading the API Gateway request&lt;/li&gt;
&lt;li&gt;Identifying the HTTP method&lt;/li&gt;
&lt;li&gt;Reading path parameters&lt;/li&gt;
&lt;li&gt;Parsing request bodies&lt;/li&gt;
&lt;li&gt;Validating input&lt;/li&gt;
&lt;li&gt;Performing DynamoDB operations&lt;/li&gt;
&lt;li&gt;Returning HTTP responses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Lambda function uses the AWS SDK for JavaScript.&lt;/p&gt;

&lt;p&gt;The project uses the modular AWS SDK v3 packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@aws-sdk/client-dynamodb
@aws-sdk/lib-dynamodb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌐 API Gateway
&lt;/h2&gt;

&lt;p&gt;Amazon API Gateway provides the HTTP interface for the Lambda function.&lt;/p&gt;

&lt;p&gt;The API exposes the CRUD routes and forwards requests to Lambda.&lt;/p&gt;

&lt;p&gt;The overall request flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Request
     │
     ▼
API Gateway
     │
     ▼
Lambda
     │
     ▼
DynamoDB
     │
     ▼
Lambda Response
     │
     ▼
API Gateway
     │
     ▼
Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested the endpoints externally through API Gateway and Postman.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 IAM Roles
&lt;/h2&gt;

&lt;p&gt;There are two different IAM responsibilities in this project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda Execution Role
&lt;/h3&gt;

&lt;p&gt;The Lambda execution role allows the function to interact with DynamoDB.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lambda
  │
  ▼
Lambda Execution Role
  │
  ▼
DynamoDB Permissions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitHub Deployment Role
&lt;/h3&gt;

&lt;p&gt;GitHub Actions uses a separate IAM role to deploy the Lambda function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions
  │
  ▼
GitHub OIDC
  │
  ▼
Deployment IAM Role
  │
  ▼
Lambda Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping these roles separate helped reinforce an important AWS principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The identity deploying a function does not need to be the same identity used by the function at runtime.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔑 GitHub OIDC Deployment
&lt;/h2&gt;

&lt;p&gt;The Lambda deployment was automated using GitHub Actions.&lt;/p&gt;

&lt;p&gt;Instead of storing long-lived AWS access keys inside GitHub, the workflow uses GitHub OIDC.&lt;/p&gt;

&lt;p&gt;The flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Actions
      │
      ▼
OIDC Token
      │
      ▼
AWS IAM
      │
      ▼
Temporary Credentials
      │
      ▼
Lambda Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was one of the main reasons I wanted to include CI/CD in the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Deployment Workflow
&lt;/h2&gt;

&lt;p&gt;The workflow performs the following steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Checkout repository
        │
        ▼
Install dependencies
        │
        ▼
Prepare Lambda package
        │
        ▼
Authenticate with AWS
        │
        ▼
Deploy Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Lambda package contains the application code and the required dependencies.&lt;/p&gt;

&lt;p&gt;I also separated the deployment package from the rest of the repository so files such as tests, documentation, and GitHub configuration are not unnecessarily uploaded to Lambda.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Testing
&lt;/h2&gt;

&lt;p&gt;The API was tested both through API Gateway and locally.&lt;/p&gt;

&lt;p&gt;The final CRUD flow was tested as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET all users
      ↓
GET specific user
      ↓
POST new user
      ↓
GET new user
      ↓
PUT user
      ↓
GET updated user
      ↓
DELETE user
      ↓
GET deleted user
      ↓
404 response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also tested the duplicate-create scenario.&lt;/p&gt;

&lt;p&gt;Attempting to create an existing user returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;409 Conflict
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which confirms that the DynamoDB condition is working as intended.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧯 Some Problems I Had to Solve
&lt;/h2&gt;

&lt;p&gt;The most useful part of the project was probably the troubleshooting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda Deployment Permissions
&lt;/h3&gt;

&lt;p&gt;The GitHub deployment role initially did not have all the Lambda permissions required by the deployment action.&lt;/p&gt;

&lt;p&gt;The missing permissions were identified from the deployment error and the IAM policy was updated accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda Package Contents
&lt;/h3&gt;

&lt;p&gt;The first deployment approach packaged the entire repository.&lt;/p&gt;

&lt;p&gt;That meant files such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;README.md
test.js
.github/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could end up inside the Lambda deployment package.&lt;/p&gt;

&lt;p&gt;I changed the workflow to create a dedicated &lt;code&gt;lambda-package&lt;/code&gt; directory containing only the Lambda code, package metadata, and required dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  ES Modules
&lt;/h3&gt;

&lt;p&gt;The Lambda code uses ES module syntax.&lt;/p&gt;

&lt;p&gt;Running the same code locally initially produced an error because Node.js was treating the project as CommonJS.&lt;/p&gt;

&lt;p&gt;Adding the appropriate module configuration to &lt;code&gt;package.json&lt;/code&gt; allowed the same code to run locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate Data
&lt;/h3&gt;

&lt;p&gt;During testing, attempting to create the same user again correctly returned a &lt;code&gt;409&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This was a useful reminder that test data can affect subsequent runs and that negative test cases are just as important as successful ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔒 Security Considerations
&lt;/h2&gt;

&lt;p&gt;The project was designed without putting AWS credentials into the GitHub repository.&lt;/p&gt;

&lt;p&gt;The main authentication approach is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub OIDC&lt;/li&gt;
&lt;li&gt;IAM roles&lt;/li&gt;
&lt;li&gt;Temporary AWS credentials&lt;/li&gt;
&lt;li&gt;Separate Lambda execution and deployment roles&lt;/li&gt;
&lt;li&gt;Specific AWS permissions for each role&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No access keys, passwords, tokens, or other credentials are included in the repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  📁 Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws-serverless-crud-api/
│
├── .github/
│   └── workflows/
│       └── deploy.yml
│
├── index.js
├── package.json
├── package-lock.json
├── test.js
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 What I Learned
&lt;/h2&gt;

&lt;p&gt;This project gave me hands-on experience with:&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Lambda
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda functions&lt;/li&gt;
&lt;li&gt;Event handling&lt;/li&gt;
&lt;li&gt;Execution roles&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Lambda deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  API Gateway
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;HTTP APIs&lt;/li&gt;
&lt;li&gt;Routes&lt;/li&gt;
&lt;li&gt;Path parameters&lt;/li&gt;
&lt;li&gt;HTTP status codes&lt;/li&gt;
&lt;li&gt;Connecting API Gateway to Lambda&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  DynamoDB
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Partition keys&lt;/li&gt;
&lt;li&gt;GetItem&lt;/li&gt;
&lt;li&gt;Scan&lt;/li&gt;
&lt;li&gt;PutItem&lt;/li&gt;
&lt;li&gt;UpdateItem&lt;/li&gt;
&lt;li&gt;DeleteItem&lt;/li&gt;
&lt;li&gt;Conditional expressions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IAM
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Trust policies&lt;/li&gt;
&lt;li&gt;Permission policies&lt;/li&gt;
&lt;li&gt;Execution roles&lt;/li&gt;
&lt;li&gt;Deployment roles&lt;/li&gt;
&lt;li&gt;Least-privilege access&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD workflows&lt;/li&gt;
&lt;li&gt;OIDC authentication&lt;/li&gt;
&lt;li&gt;AWS role assumption&lt;/li&gt;
&lt;li&gt;Lambda deployment&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Why I Built This
&lt;/h2&gt;

&lt;p&gt;I wanted to go beyond simply learning what Lambda, API Gateway, and DynamoDB are.&lt;/p&gt;

&lt;p&gt;The goal was to build something where I had to deal with the actual interactions between these services.&lt;/p&gt;

&lt;p&gt;The project also gave me the opportunity to work through IAM permissions, deployment packaging, API error handling, DynamoDB conditions, and CI/CD.&lt;/p&gt;

&lt;p&gt;That made the architecture much easier to understand because every part of it was actually configured and tested.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔮 Possible Improvements
&lt;/h2&gt;

&lt;p&gt;There are several directions this project could be taken next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add API authentication and authorization&lt;/li&gt;
&lt;li&gt;Add request validation&lt;/li&gt;
&lt;li&gt;Introduce Amazon Cognito&lt;/li&gt;
&lt;li&gt;Add CloudWatch dashboards and alarms&lt;/li&gt;
&lt;li&gt;Add infrastructure as code&lt;/li&gt;
&lt;li&gt;Add automated API tests to the CI/CD pipeline&lt;/li&gt;
&lt;li&gt;Add a frontend application&lt;/li&gt;
&lt;li&gt;Introduce API versioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These would move the project closer to a production-style serverless application.&lt;/p&gt;




&lt;h2&gt;
  
  
  📌 Final Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌──────────────────┐
                    │     Client       │
                    │ Browser / Postman│
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  API Gateway     │
                    │    HTTP API      │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │  AWS Lambda      │
                    │  CRUD Logic      │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │    DynamoDB      │
                    │   User Records   │
                    └──────────────────┘


Deployment:

GitHub
   │
   ▼
GitHub Actions
   │
   ▼
GitHub OIDC
   │
   ▼
AWS IAM
   │
   ▼
AWS Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  👨‍💻 Project
&lt;/h2&gt;

&lt;p&gt;Built as part of my hands-on AWS learning journey, with a focus on understanding serverless architecture, REST APIs, DynamoDB operations, IAM, and automated AWS deployments.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploy a Static Website to AWS S3 with GitHub Actions and OIDC</title>
      <dc:creator>Muhammad Usama Saleem</dc:creator>
      <pubDate>Wed, 02 Sep 2026 16:28:06 +0000</pubDate>
      <link>https://dev.to/usama_saleem_148/deploy-a-static-website-to-aws-s3-with-github-actions-and-oidc-3lg0</link>
      <guid>https://dev.to/usama_saleem_148/deploy-a-static-website-to-aws-s3-with-github-actions-and-oidc-3lg0</guid>
      <description>&lt;p&gt;In this guide, we'll build a simple deployment pipeline where a website stored in GitHub is automatically deployed to an Amazon S3 bucket whenever changes are pushed to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;No long-lived AWS access keys will be stored in GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ What is being built?
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a website.&lt;/li&gt;
&lt;li&gt;Store it in GitHub.&lt;/li&gt;
&lt;li&gt;Push the code.&lt;/li&gt;
&lt;li&gt;GitHub Actions starts automatically.&lt;/li&gt;
&lt;li&gt;GitHub authenticates with AWS using OIDC.&lt;/li&gt;
&lt;li&gt;AWS allows the workflow to assume an IAM role.&lt;/li&gt;
&lt;li&gt;The workflow uploads the website to S3.&lt;/li&gt;
&lt;li&gt;S3 serves the website.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
   ↓
GitHub Actions
   ↓
GitHub OIDC
   ↓
AWS IAM Role
   ↓
Amazon S3
   ↓
Static Website 🌐
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📋 What is required?
&lt;/h2&gt;

&lt;p&gt;Before starting, you'll need the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account&lt;/li&gt;
&lt;li&gt;An S3 bucket&lt;/li&gt;
&lt;li&gt;An IAM role&lt;/li&gt;
&lt;li&gt;An IAM permissions policy&lt;/li&gt;
&lt;li&gt;GitHub OIDC configured for AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GitHub
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub repository&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;main&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;GitHub Actions enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Local machine
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;A text/code editor&lt;/li&gt;
&lt;li&gt;A simple &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need a server or an EC2 instance for this project.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧰 What to prepare
&lt;/h2&gt;

&lt;p&gt;For my project, I used the following setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Region: &amp;lt;AWS_REGION&amp;gt;
S3 Bucket: &amp;lt;S3_BUCKET_NAME&amp;gt;
IAM Role: &amp;lt;IAM_ROLE_NAME&amp;gt;
GitHub Repository: &amp;lt;GITHUB_USERNAME&amp;gt;/&amp;lt;GITHUB_REPOSITORY&amp;gt;
Branch: main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, throughout this guide, replace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;AWS_REGION&amp;gt;
&amp;lt;S3_BUCKET_NAME&amp;gt;
&amp;lt;IAM_ROLE_NAME&amp;gt;
&amp;lt;GITHUB_USERNAME&amp;gt;
&amp;lt;GITHUB_REPOSITORY&amp;gt;
&amp;lt;AWS_ACCOUNT_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with your own values.&lt;/p&gt;

&lt;p&gt;We'll prepare four main things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;S3 bucket&lt;/li&gt;
&lt;li&gt;IAM role + permissions&lt;/li&gt;
&lt;li&gt;GitHub OIDC authentication&lt;/li&gt;
&lt;li&gt;GitHub Actions workflow&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1️⃣ Prepare the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;Go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Console → S3 → Create bucket&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a bucket with a name such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-aws-s3-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 S3 bucket names must be globally unique. If this name is already taken, choose another one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Choose your AWS region.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ap-southeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Encryption
&lt;/h3&gt;

&lt;p&gt;For server-side encryption, I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSE-S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means S3 manages the encryption keys for the objects stored in the bucket.&lt;/p&gt;

&lt;p&gt;Once the bucket is created, open it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Prepare Static Website Hosting
&lt;/h2&gt;

&lt;p&gt;Open:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 → Your bucket → Properties&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Find:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static website hosting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enable it and set the index document to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the configuration.&lt;/p&gt;

&lt;p&gt;At this point, S3 knows that this bucket is going to be used to serve a website.&lt;/p&gt;

&lt;p&gt;But we still need to put a website inside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ Prepare the IAM Role
&lt;/h2&gt;

&lt;p&gt;Next, we need an IAM role that GitHub Actions can use.&lt;/p&gt;

&lt;p&gt;Go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Console → IAM → Roles → Create role&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a role such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git-S3-Project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing here is that we're creating a &lt;strong&gt;role&lt;/strong&gt;, not creating an AWS access key and putting that key into GitHub.&lt;/p&gt;

&lt;p&gt;The authentication flow will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
   ↓
OIDC token
   ↓
AWS STS
   ↓
IAM Role
   ↓
Temporary credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means GitHub Actions can use temporary AWS credentials instead of storing long-lived AWS credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  4️⃣ Prepare the Trust Policy
&lt;/h2&gt;

&lt;p&gt;The role needs to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who is allowed to assume me?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what the &lt;strong&gt;trust policy&lt;/strong&gt; controls.&lt;/p&gt;

&lt;p&gt;For the GitHub repository, use a trust policy like this:&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="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="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Federated"&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:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:oidc-provider/token.actions.githubusercontent.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="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:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&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;"StringEquals"&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;"token.actions.githubusercontent.com:aud"&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.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="nl"&gt;"StringLike"&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;"token.actions.githubusercontent.com:sub"&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="s2"&gt;"repo:&amp;lt;GITHUB_USERNAME&amp;gt;/&amp;lt;GITHUB_REPOSITORY&amp;gt;:ref:refs/heads/main"&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="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;Replace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;AWS_ACCOUNT_ID&amp;gt;
&amp;lt;GITHUB_USERNAME&amp;gt;
&amp;lt;GITHUB_REPOSITORY&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with your own values.&lt;/p&gt;

&lt;p&gt;For example, this part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:&amp;lt;GITHUB_USERNAME&amp;gt;/&amp;lt;GITHUB_REPOSITORY&amp;gt;:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should point to the repository and branch that are allowed to assume the role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust policy vs permissions policy
&lt;/h3&gt;

&lt;p&gt;Keep this distinction in mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trust Policy
     ↓
Who can assume the role?

Permissions Policy
     ↓
What can the role do?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll configure the permissions policy next.&lt;/p&gt;




&lt;h2&gt;
  
  
  5️⃣ Prepare the S3 Permissions
&lt;/h2&gt;

&lt;p&gt;Now we need to tell AWS what the role is actually allowed to do.&lt;/p&gt;

&lt;p&gt;For this project, the role needs permission to upload objects to the S3 bucket.&lt;/p&gt;

&lt;p&gt;Use:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowS3"&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;"Action"&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="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObjectAcl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&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;"Resource"&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="s2"&gt;"arn:aws:s3:::&amp;lt;S3_BUCKET_NAME&amp;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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;S3_BUCKET_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with your bucket name.&lt;/p&gt;

&lt;p&gt;The permission that matters most for our deployment is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3:PutObject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because GitHub Actions needs to upload the website files into S3.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 For a production setup, review these permissions carefully and follow the principle of least privilege. The example above is intended to keep this learning project straightforward.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6️⃣ Prepare the Website
&lt;/h2&gt;

&lt;p&gt;Create an &lt;code&gt;index.html&lt;/code&gt; in your GitHub repository.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;My AWS Website&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello from AWS S3 🚀&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This website is deployed using GitHub Actions.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your repository can now look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws-s3-project/
│
├── index.html
│
└── .github/
    └── workflows/
        └── deploy.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7️⃣ Prepare GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Now we connect GitHub to AWS.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/workflows/deploy.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add:&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;Deploy to S3 Bucket&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;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;main&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;deploy&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;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;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&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;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:role/&amp;lt;IAM_ROLE_NAME&amp;gt;&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;&amp;lt;AWS_REGION&amp;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;Upload files to S3&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;aws s3 cp ./index.html s3://&amp;lt;S3_BUCKET_NAME&amp;gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;AWS_ACCOUNT_ID&amp;gt;
&amp;lt;IAM_ROLE_NAME&amp;gt;
&amp;lt;AWS_REGION&amp;gt;
&amp;lt;S3_BUCKET_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with your own values.&lt;/p&gt;

&lt;p&gt;There are three important parts here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&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;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the workflow runs when code is pushed to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  OIDC permission
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;id-token: write&lt;/code&gt; allows GitHub Actions to request an OIDC token.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; give GitHub access to S3.&lt;/p&gt;

&lt;p&gt;AWS still decides whether that identity is allowed to assume the IAM role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload
&lt;/h3&gt;

&lt;p&gt;Finally:&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 &lt;span class="nb"&gt;cp&lt;/span&gt; ./index.html s3://&amp;lt;S3_BUCKET_NAME&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This copies the file into the S3 bucket.&lt;/p&gt;




&lt;h2&gt;
  
  
  8️⃣ Prepare Public Website Access
&lt;/h2&gt;

&lt;p&gt;At this point, GitHub Actions can upload the file.&lt;/p&gt;

&lt;p&gt;But that's a different question from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can someone visiting the website read the file?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a basic S3 static website, the website objects need to be publicly readable.&lt;/p&gt;

&lt;p&gt;For this learning project, you can use a bucket policy that allows public read access to &lt;code&gt;index.html&lt;/code&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;"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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublicReadForWebsite"&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="s2"&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;"Action"&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="s2"&gt;"s3:GetObject"&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;"Resource"&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="s2"&gt;"arn:aws:s3:::&amp;lt;S3_BUCKET_NAME&amp;gt;/index.html"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;S3_BUCKET_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with your bucket name.&lt;/p&gt;

&lt;p&gt;Here:&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="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="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;means anyone can be the requester, while:&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="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;"s3:GetObject"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;allows them to read the object.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ This makes &lt;code&gt;index.html&lt;/code&gt; publicly readable. This is appropriate for demonstrating a public static website, but you should not blindly use this access model for sensitive or private data.&lt;/p&gt;

&lt;p&gt;⚠️ This example only makes &lt;code&gt;index.html&lt;/code&gt; public. If your website contains CSS, JavaScript, images, or other files, you'll need to account for those objects too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For production applications, a common approach is to keep the S3 bucket private and use Amazon CloudFront to serve the website.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Once Done — How to Check
&lt;/h2&gt;

&lt;p&gt;Now we have everything prepared.&lt;/p&gt;

&lt;p&gt;Let's test it from end to end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 1: Push the Code
&lt;/h2&gt;

&lt;p&gt;Commit and push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Deploy website to S3"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Check 2: Open GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Go to your GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actions → Deploy to S3 Bucket&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You should see a workflow run.&lt;/p&gt;

&lt;p&gt;The steps should complete successfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Checkout code
✓ Configure AWS credentials
✓ Upload files to S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If these are green, GitHub successfully authenticated with AWS and uploaded the file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 3: Check the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;Go to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Console → S3 → Your Bucket → Objects&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms the file reached S3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Check 4: Open the Website
&lt;/h2&gt;

&lt;p&gt;Go back to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 → Properties → Static website hosting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy the website endpoint shown there and open it in your browser.&lt;/p&gt;

&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from AWS S3 🚀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 Your GitHub repository is now deploying a website to S3.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔎 If Something Goes Wrong
&lt;/h2&gt;

&lt;p&gt;Here are the main things to check.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions cannot assume the role
&lt;/h3&gt;

&lt;p&gt;Check the IAM &lt;strong&gt;trust policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The GitHub repository and branch in the &lt;code&gt;sub&lt;/code&gt; condition must match the workflow.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo:&amp;lt;GITHUB_USERNAME&amp;gt;/&amp;lt;GITHUB_REPOSITORY&amp;gt;:ref:refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also make sure the GitHub Actions workflow has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  GitHub assumes the role but S3 returns &lt;code&gt;AccessDenied&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Check the IAM &lt;strong&gt;permissions policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Make sure the role has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3:PutObject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for the correct bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:aws:s3:::&amp;lt;S3_BUCKET_NAME&amp;gt;/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Deployment succeeds but website shows &lt;code&gt;AccessDenied&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;GitHub's deployment permission and visitor access are separate.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 bucket policy&lt;/li&gt;
&lt;li&gt;Static website hosting&lt;/li&gt;
&lt;li&gt;Block Public Access settings&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Presigned URL works but normal URL doesn't
&lt;/h3&gt;

&lt;p&gt;A presigned URL provides temporary authorization to access an object.&lt;/p&gt;

&lt;p&gt;It does not mean the object is publicly accessible.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal S3 URL
      ↓
Requires permission

Presigned URL
      ↓
Contains temporary authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 Final Result
&lt;/h2&gt;

&lt;p&gt;What we started with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we ended with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub
   ↓
GitHub Actions
   ↓
OIDC
   ↓
IAM Role
   ↓
S3
   ↓
Static Website 🌐
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the deployment process is now automatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change index.html
       ↓
   git push
       ↓
GitHub Actions
       ↓
 Upload to S3
       ↓
Website updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main takeaway is that GitHub Actions can authenticate with AWS using OIDC and assume an IAM role without storing long-lived AWS access keys in the repository.&lt;/p&gt;

&lt;p&gt;That's the complete setup. 🚀&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
      <category>github</category>
    </item>
  </channel>
</rss>
