<?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: Tanseer</title>
    <description>The latest articles on DEV Community by Tanseer (@tanseer).</description>
    <link>https://dev.to/tanseer</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%2F3901526%2Faae5933f-439d-4185-ad46-10b5e922d96c.jpg</url>
      <title>DEV Community: Tanseer</title>
      <link>https://dev.to/tanseer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanseer"/>
    <language>en</language>
    <item>
      <title>Amazon Textract: Extract Data from Documents with AI</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-textract-extract-data-from-documents-with-ai-16of</link>
      <guid>https://dev.to/aws-builders/amazon-textract-extract-data-from-documents-with-ai-16of</guid>
      <description>&lt;p&gt;Turn scanned documents, forms, and tables into structured data your app can use, going well beyond plain text scanning. Stop seven in the AWS Hidden Gems series.&lt;/p&gt;

&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: Amazon Textract
&lt;/h2&gt;

&lt;p&gt;Plenty of important data is trapped in documents: invoices, forms, receipts, IDs, contracts. Textract reads those documents and pulls out not just the words, but the structure. It knows which value belongs to which field on a form and how a table's rows and columns line up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Getting data out of documents has always been painful. Typing it in by hand is slow and error prone. Plain OCR, which stands for optical character recognition and means turning an image of text into machine readable text, gives you the words but throws away the layout. You get a wall of text with no idea that "Total: 42.00" means the total field holds 42.00, or which numbers sat in which table cell.&lt;/p&gt;

&lt;p&gt;Textract is OCR plus document understanding. AWS trained models that recognize forms and tables, so instead of raw text you get field and value pairs and structured tables. That is the difference between reading a document and actually using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Amazon Textract?
&lt;/h2&gt;

&lt;p&gt;Textract is a managed document analysis service. It reads text from images and PDFs, and it understands common document structures.&lt;/p&gt;

&lt;p&gt;It can pull out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raw text, line by line and word by word&lt;/li&gt;
&lt;li&gt;Form data as key and value pairs, like a field name and its answer&lt;/li&gt;
&lt;li&gt;Tables, preserving rows and columns&lt;/li&gt;
&lt;li&gt;Answers to specific questions you ask about the document, using its Queries feature&lt;/li&gt;
&lt;li&gt;Specialized data from receipts and invoices, and from identity documents
Short single images are processed instantly. Multi page PDFs stored in S3 are processed in the background, and Textract notifies you when the results are ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;An accounts team receives hundreds of supplier invoices as PDFs every month. Someone opens each one and copies the invoice number, date, and total into their system by hand.&lt;/p&gt;

&lt;p&gt;It is slow, and typos cause real accounting errors. They want the key fields pulled out of each invoice automatically and dropped into their database.&lt;/p&gt;

&lt;p&gt;Textract does exactly this. Its invoice analysis returns the fields they care about from each PDF, so the data flows in without manual typing, and the mistakes that came with it disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Finance teams pull fields from invoices and receipts into accounting systems&lt;/li&gt;
&lt;li&gt;Banks and lenders process loan and mortgage paperwork automatically&lt;/li&gt;
&lt;li&gt;Healthcare digitizes intake forms and patient records&lt;/li&gt;
&lt;li&gt;Insurance extracts data from claims documents to speed up processing&lt;/li&gt;
&lt;li&gt;Governments and HR turn paper forms into searchable digital records&lt;/li&gt;
&lt;li&gt;Any team replacing manual data entry from PDFs and scans
The pattern is documents in, structured data out, with no one retyping.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;Documents land in S3. A common pipeline: an upload triggers a Lambda function, the function calls Textract, and the structured results are stored in DynamoDB or passed on for more processing. For long PDFs, Textract sends a notification through SNS when the background job is done. Extracted text often flows next into Comprehend, the AWS text analysis service, to find meaning in it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Document uploaded] --&amp;gt; B[S3 bucket]
    B --&amp;gt;|Upload event| C[Lambda function]
    C --&amp;gt;|Analyze document| D[Textract]
    D --&amp;gt;|Fields, tables, text| C
    C --&amp;gt;|Store structured data| E[DynamoDB]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Textract is the reader. Storage and triggers use familiar services, and Textract turns the document into data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;For a single image or short document, you send it to Textract and get structured results back right away: text, form pairs, and tables. For a multi page PDF in S3, you start a job, Textract processes it in the background, and it notifies you through SNS when the results are ready to fetch. You choose what to extract by asking for features, such as forms and tables, or by using the invoice or identity document analysis for those specific cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Document] --&amp;gt; B{Size?}
    B --&amp;gt;|Single image| C[Call Textract, get results instantly]
    B --&amp;gt;|Multi page PDF| D[Start a job]
    D --&amp;gt; E[Textract processes in background]
    E --&amp;gt; F[SNS notifies you]
    F --&amp;gt; G[Fetch structured results]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;The Textract console lets you try extraction on your own document before writing code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, search for Textract, and open it. Check the region in the top right corner.&lt;/li&gt;
&lt;li&gt;In the left menu, open the Analyze Document demo.&lt;/li&gt;
&lt;li&gt;Upload a document with some structure, such as a form or an invoice, or use a provided sample.&lt;/li&gt;
&lt;li&gt;Turn on the features you want above the document, such as Forms and Tables, then let Textract analyze it.&lt;/li&gt;
&lt;li&gt;Review the results tabs. The Forms tab shows key and value pairs it found, the Tables tab shows reconstructed tables, and the Raw Text tab shows every line. Notice how it links each field to its value.&lt;/li&gt;
&lt;li&gt;To use Textract from your own code, create an IAM role with the AmazonTextractFullAccess policy, or a narrower policy allowing the Textract actions you need, plus s3:GetObject on the bucket holding your documents.&lt;/li&gt;
&lt;li&gt;To confirm the setup, run the code in the next section on a document in your S3 bucket and check that the text comes back.
Common mistakes: an access error usually means the IAM role lacks Textract permission or read access to the S3 bucket, so check both. If a scanned document extracts poorly, a clearer, higher resolution scan almost always improves the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using it from code
&lt;/h2&gt;

&lt;p&gt;This reads a document in S3 and prints its text, line by line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;textract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect_document_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3Object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-docs-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BlockType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LINE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For form fields and tables, call analyze_document instead, with FeatureTypes of ["FORMS", "TABLES"]. Its output links keys to their values through relationships, which is fiddly to walk by hand, so most people use the Textract Response Parser library (trp) to read the pairs and tables directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text detection&lt;/td&gt;
&lt;td&gt;Per page, cheapest option, plain text only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms or tables&lt;/td&gt;
&lt;td&gt;Per page, higher rate, for structured data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;Per page, for asking specific questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invoice or ID analysis&lt;/td&gt;
&lt;td&gt;Per page, for those document types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Pages per month for the first 3 months, across text and analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS AI services family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS AI Services
├── Textract      text and structured data from documents
├── Rekognition   understanding of images and video
├── Comprehend    meaning and sentiment in text
├── Transcribe    speech to text
├── Translate     language translation
└── Polly         text to speech
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Textract and Rekognition both read images, but for different goals. Rekognition, from earlier in this series, understands the scene in a photo. Textract understands the document on the page. They often pair with Comprehend, which analyzes the text once it is extracted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Textract reads documents the way you need software to: not just the words, but which value goes with which field and how tables are laid out. It replaces manual data entry from invoices, forms, and scans with a single API call. Next time data is stuck inside a PDF, you know how to get it out as structured data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop seven of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert&lt;/li&gt;
&lt;li&gt;Amazon IVS&lt;/li&gt;
&lt;li&gt;Amazon Rekognition&lt;/li&gt;
&lt;li&gt;Amazon Personalize&lt;/li&gt;
&lt;li&gt;AWS AppSync&lt;/li&gt;
&lt;li&gt;Amazon Timestream&lt;/li&gt;
&lt;li&gt;Amazon Textract (you are here)&lt;/li&gt;
&lt;li&gt;Amazon Kendra&lt;/li&gt;
&lt;li&gt;AWS DataSync&lt;/li&gt;
&lt;li&gt;AWS IoT Core
Next up is Amazon Kendra, which brings AI powered natural language search to your own content.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awstextract</category>
    </item>
    <item>
      <title>Amazon Timestream: The Database Built for Time-Series Data</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Mon, 24 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-timestream-the-database-built-for-time-series-data-fbe</link>
      <guid>https://dev.to/aws-builders/amazon-timestream-the-database-built-for-time-series-data-fbe</guid>
      <description>&lt;p&gt;A serverless database made for data that arrives with a timestamp, like sensor readings, metrics, and events. Stop six in the AWS Hidden Gems series.&lt;/p&gt;

&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: Amazon Timestream
&lt;/h2&gt;

&lt;p&gt;Some data is defined by when it happened: a temperature reading every second, a server's CPU usage over a day, a stock price tick by tick. This is time series data, and normal databases handle it poorly at scale. Timestream is a database built specifically for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Time series data has an awkward shape. It arrives constantly and in huge volumes, you almost always query it by time ranges, and older data becomes less useful but you still want to keep it cheaply. General purpose databases struggle here. A relational database gets expensive and slow under a firehose of writes, and you end up over provisioning servers and hand building ways to expire old data.&lt;/p&gt;

&lt;p&gt;Timestream is designed around this shape. It is serverless, so it scales writes automatically with no servers to size. It stores recent data in a fast tier and moves older data to a cheaper tier on its own. And its query language has time series functions built in, for things like filling gaps between readings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Amazon Timestream?
&lt;/h2&gt;

&lt;p&gt;Timestream is a serverless time series database. Time series data is a sequence of values recorded over time, each stamped with the moment it happened.&lt;/p&gt;

&lt;p&gt;A record in Timestream has three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dimensions: labels that describe the source, like a device id or a region&lt;/li&gt;
&lt;li&gt;Measures: the actual values, like temperature or CPU percentage&lt;/li&gt;
&lt;li&gt;Time: when the measurement was taken
It stores data in two tiers automatically. A memory tier holds recent data for fast queries, and a magnetic tier holds older data at lower cost. You set how long data stays in each with a retention policy, and Timestream moves data between them for you. You query it with SQL, the standard database query language, extended with functions made for time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;A company runs a few hundred temperature sensors across its warehouses, each reporting every ten seconds. That is millions of readings a day, forever.&lt;/p&gt;

&lt;p&gt;They tried storing this in their existing relational database. Writes started to lag, storage costs climbed, and queries like average temperature per warehouse over the last hour got slow. The database was not built for this kind of load.&lt;/p&gt;

&lt;p&gt;Moving the readings to Timestream fixes it. Writes keep up automatically, recent data stays fast to query, old data ages into cheap storage, and the time based queries they need are quick. Their database goes back to handling the business data it is good at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IoT platforms store sensor readings from thousands of devices reporting constantly&lt;/li&gt;
&lt;li&gt;Operations teams keep server and application metrics for monitoring and alerting&lt;/li&gt;
&lt;li&gt;Industrial systems track machine data to spot problems before a breakdown&lt;/li&gt;
&lt;li&gt;Energy and utilities record meter readings over time for analysis and billing&lt;/li&gt;
&lt;li&gt;Finance stores market data like prices and trades stamped to the moment&lt;/li&gt;
&lt;li&gt;Apps log user activity events for later analysis by time
The pattern is a high volume stream of timestamped values that you query by time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;Data usually flows in from many small producers. Devices send readings through IoT Core, or an app or stream writes records directly. Timestream stores them, and you query from a dashboard tool like Amazon Managed Grafana or Amazon QuickSight to see trends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Sensors and devices] --&amp;gt; B[IoT Core]
    B --&amp;gt;|Route readings| C[Timestream]
    D[Apps and streams] --&amp;gt;|Write records| C
    C --&amp;gt;|SQL queries| E[Grafana or QuickSight dashboards]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timestream is the store for the time series itself. Other services feed it and visualize it, and it focuses on ingesting and querying by time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;You create a database and a table, and set the table's retention: how long data stays in the fast memory tier and how long in the cheaper magnetic tier. Producers write records, each carrying dimensions, one or more measures, and a timestamp. Timestream keeps recent records in memory for fast access and ages older ones into magnetic storage on schedule. You run SQL queries over any time range, and Timestream reads from whichever tier holds that data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Create database and table] --&amp;gt; B[Set memory and magnetic retention]
    B --&amp;gt; C[Producers write timestamped records]
    C --&amp;gt; D[Recent data in fast memory tier]
    C --&amp;gt; E[Older data in cheap magnetic tier]
    D --&amp;gt; F[SQL queries by time range]
    E --&amp;gt; F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;You will create a database and table, write a couple of records, and query them. This uses Timestream for LiveAnalytics, the serverless engine.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, search for Timestream, and open it. Check the region in the top right corner.&lt;/li&gt;
&lt;li&gt;In the left menu choose Databases, click Create database, select Standard database, give it a name, and create it.&lt;/li&gt;
&lt;li&gt;Open your database, choose Tables, and click Create table. Name the table, then set the retention, for example keep 12 hours in the memory tier and 7 days in the magnetic tier. These control how long data stays fast versus cheap.&lt;/li&gt;
&lt;li&gt;To add data quickly, open the Query editor from the left menu. Timestream expects most writes to come from code, but you can confirm the table exists and is queryable here.&lt;/li&gt;
&lt;li&gt;Write a few records using the code in the next section, sending a device id as a dimension, a temperature as a measure, and the current time. This is the normal way data enters Timestream.&lt;/li&gt;
&lt;li&gt;Back in the Query editor, run a query such as selecting all rows from your table ordered by time, and confirm your records appear. Timestream gives you sample queries you can adapt.&lt;/li&gt;
&lt;li&gt;To validate a real time based query, try averaging your measure grouped by device over the last hour, and check the numbers match what you wrote.
Common mistakes: a write that is rejected often has a timestamp outside the memory tier's retention window, so make sure you are writing recent times. An access error means the IAM role is missing Timestream write or query permission, so add it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using it from code
&lt;/h2&gt;

&lt;p&gt;Timestream splits writing and querying into two clients. This writes a single temperature reading.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;write_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestream-write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# current time in milliseconds
&lt;/span&gt;
&lt;span class="n"&gt;write_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DatabaseName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sensors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Records&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sensor-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MeasureName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MeasureValue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;22.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MeasureValueType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DOUBLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wrote one reading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read the data back, use the timestream-query client and run a SQL statement like SELECT device_id, temperature, time FROM "my-database"."sensors" ORDER BY time DESC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writes&lt;/td&gt;
&lt;td&gt;Per GB of data written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory tier storage&lt;/td&gt;
&lt;td&gt;Per GB per hour, for recent fast data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Magnetic tier storage&lt;/td&gt;
&lt;td&gt;Per GB per month, cheaper, for older data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;Per GB of data scanned by the query&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Monthly write, storage, and query allowance for new accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS database family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Databases
├── Timestream    time series data like sensors and metrics
├── DynamoDB      key value and document data at any scale
├── Aurora/RDS    relational SQL databases
├── ElastiCache   in memory caching for speed
└── Neptune       graph data and relationships
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS offers a database per job rather than one for everything. Timestream is the one for timestamped streams. DynamoDB is the general NoSQL workhorse, and Aurora or RDS handle relational data. Pick Timestream when time is the main axis of your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Timestream is built for the exact shape of time series data: constant writes, time based queries, and old data you want to keep cheaply. It scales itself, tiers your data by age, and speaks SQL. Next time you are about to force sensor or metric data into a general database, reach for this instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop six of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert&lt;/li&gt;
&lt;li&gt;Amazon IVS&lt;/li&gt;
&lt;li&gt;Amazon Rekognition&lt;/li&gt;
&lt;li&gt;Amazon Personalize&lt;/li&gt;
&lt;li&gt;AWS AppSync&lt;/li&gt;
&lt;li&gt;Amazon Timestream (you are here)&lt;/li&gt;
&lt;li&gt;Amazon Textract&lt;/li&gt;
&lt;li&gt;Amazon Kendra&lt;/li&gt;
&lt;li&gt;AWS DataSync&lt;/li&gt;
&lt;li&gt;AWS IoT Core
Next up is Amazon Textract, which pulls text, forms, and tables out of scanned documents using AI.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awstimestream</category>
    </item>
    <item>
      <title>AWS AppSync: The Easiest Way to Build Real-Time Apps</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-appsync-the-easiest-way-to-build-real-time-apps-5b1c</link>
      <guid>https://dev.to/aws-builders/aws-appsync-the-easiest-way-to-build-real-time-apps-5b1c</guid>
      <description>&lt;p&gt;Build APIs that fetch exactly the data an app needs and push live updates as things change. Stop five in the AWS Hidden Gems series.&lt;/p&gt;

&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: AWS AppSync
&lt;/h2&gt;

&lt;p&gt;Modern apps often need two things that are awkward to build: letting the client ask for exactly the data it wants in one request, and pushing live updates to the screen the moment data changes. AppSync gives you both. It is a managed service for building GraphQL APIs, with real time updates included.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Two common API headaches led to AppSync. First, with traditional REST APIs, the client often calls several endpoints and gets back more data than it needs, which is slow on mobile. GraphQL fixes this by letting the client ask for precisely the fields it wants in a single request, but running a GraphQL server yourself means managing servers, connecting data sources, and writing a lot of glue.&lt;/p&gt;

&lt;p&gt;Second, live updates. Showing new messages or a changing dashboard in real time usually means managing WebSocket connections, which are long lived two way connections between client and server. Doing that at scale is fiddly.&lt;/p&gt;

&lt;p&gt;AppSync handles both. It runs the GraphQL API for you, connects it to your data with little code, and manages the real time connections so updates just arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AWS AppSync?
&lt;/h2&gt;

&lt;p&gt;AppSync is a managed GraphQL service. GraphQL is a query language for APIs where the client describes the exact data it wants, and the server returns just that, in one round trip.&lt;/p&gt;

&lt;p&gt;You define a schema, which is the shape of your data and the operations allowed on it. You connect each part of the schema to a data source, which AppSync can talk to directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DynamoDB for fast NoSQL storage&lt;/li&gt;
&lt;li&gt;Lambda for any custom logic&lt;/li&gt;
&lt;li&gt;Relational databases, HTTP endpoints, and others
AppSync supports three kinds of operations: queries to read data, mutations to change data, and subscriptions to receive live updates. When someone changes data through a mutation, AppSync automatically pushes that change to every client subscribed to it. Authentication is built in, with options including API keys and Amazon Cognito, the AWS user sign in service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;A team is building a shared task board, like a simple Trello. When one person moves a card, everyone else looking at the board should see it move immediately.&lt;/p&gt;

&lt;p&gt;Building this the usual way means a REST API for the data plus a separate WebSocket layer to broadcast changes, with code to track who is connected and who cares about which board. That is a lot of plumbing for a small team.&lt;/p&gt;

&lt;p&gt;With AppSync, they define the board and card types once, back them with DynamoDB, and add a subscription. Now a mutation to move a card is pushed to every open board automatically. The real time behavior comes almost for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Chat and messaging apps deliver new messages instantly to everyone in a conversation&lt;/li&gt;
&lt;li&gt;Collaborative tools like shared boards and documents sync changes across users live&lt;/li&gt;
&lt;li&gt;Live dashboards update metrics on screen as new data arrives&lt;/li&gt;
&lt;li&gt;Multiplayer game features share state like scores and moves between players&lt;/li&gt;
&lt;li&gt;Mobile apps fetch lean, exact payloads to stay fast on slow connections&lt;/li&gt;
&lt;li&gt;Apps that work offline sync their changes when the connection returns
The pattern is flexible data access plus live updates, without building the real time layer yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;The client app talks to a single AppSync endpoint. AppSync routes each request to the right data source, most often DynamoDB for storage or Lambda for custom logic. Cognito handles who is allowed to do what. For live updates, clients hold a subscription connection to AppSync, and AppSync pushes changes to them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Client app] --&amp;gt;|GraphQL query or mutation| B[AppSync API]
    B --&amp;gt; C[DynamoDB]
    B --&amp;gt; D[Lambda]
    B --&amp;gt;|Auth check| E[Cognito]
    B --&amp;gt;|Live updates| A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AppSync sits between your app and your data, giving one flexible endpoint and handling the real time push.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;You define a GraphQL schema with your types and operations. You attach data sources and connect each field to an action, such as read this item from DynamoDB. The client sends a query or mutation, and AppSync runs the connected action and returns exactly the requested fields. When a mutation changes data, AppSync notifies every client subscribed to that change over their live connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Define GraphQL schema] --&amp;gt; B[Connect fields to data sources]
    B --&amp;gt; C[Client sends query or mutation]
    C --&amp;gt; D[AppSync runs it against the data source]
    D --&amp;gt; E[Return exactly the requested fields]
    D --&amp;gt; F[Push updates to subscribed clients]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;You will create a GraphQL API, let AppSync build a DynamoDB backed type, and test it live.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, search for AppSync, and open it. Check the region in the top right corner.&lt;/li&gt;
&lt;li&gt;Click Create API, choose GraphQL APIs, and pick the option to design from scratch. Give the API a name and create it.&lt;/li&gt;
&lt;li&gt;Open the Schema page. Define a simple type and the operations for it, for example a Task type with an id and a title, and queries and mutations to create and list tasks.&lt;/li&gt;
&lt;li&gt;Use the Create Resources shortcut on the schema. AppSync can generate a DynamoDB table and the resolvers, which are the small pieces that connect each GraphQL field to a table action, so you do not write them by hand.&lt;/li&gt;
&lt;li&gt;Go to the Queries page, a built in tool for running operations against your API. Run a mutation to create a task, then a query to list tasks, and confirm your data comes back.&lt;/li&gt;
&lt;li&gt;To see real time updates, open a subscription in the Queries page in one browser tab, then run a create mutation in another. The new task appears in the subscription tab immediately.&lt;/li&gt;
&lt;li&gt;For your app to call the API, note the API URL and set an authorization mode under Settings. An API key is fine for testing, while Cognito is the usual choice for real users. Confirm a call works from your code using the URL and key.
Common mistakes: an unauthorized error usually means the request is missing the API key or Cognito token the API expects, so check the authorization mode and that your client sends the right credential. If a field returns null, its resolver is probably not connected to a data source yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using it from code
&lt;/h2&gt;

&lt;p&gt;With AppSync, the code is your GraphQL schema and the operations clients run against it. Here is a small schema and the three operation types.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Task&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Boolean&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&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="n"&gt;listTasks&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="n"&gt;Task&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mutation&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="n"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Task&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="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Subscription&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="n"&gt;onAddTask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;aws_subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="n"&gt;addTask&lt;/span&gt;&lt;span class="err"&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;A client adds a task and, thanks to the subscription, every other client is notified without polling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&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="n"&gt;addTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write the AppSync post"&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="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;done&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;The @aws_subscribe line is what wires the subscription to the mutation, so any addTask pushes the new task to subscribers automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries and mutations&lt;/td&gt;
&lt;td&gt;Per million operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query and mutation rate&lt;/td&gt;
&lt;td&gt;About $4.00 per million operations (US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real time updates&lt;/td&gt;
&lt;td&gt;Per million updates pushed to clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection time&lt;/td&gt;
&lt;td&gt;Per million minutes clients stay connected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;250,000 operations and 250,000 real time updates per month for 12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS API and app services family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS API and App Services
├── AppSync       managed GraphQL with real time updates
├── API Gateway   REST and WebSocket APIs you route yourself
├── Amplify       front end hosting and app building tools
└── Lambda        serverless functions behind your APIs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main choice is AppSync versus API Gateway. AppSync is built around GraphQL and real time updates, and connects to data sources with little code. API Gateway is built around REST and WebSocket routing, giving you more manual control. For live, data driven apps, AppSync usually gets you there faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;AppSync gives you a flexible GraphQL API and real time updates without running a server or building a WebSocket layer. Define a schema, point it at your data, and changes flow to every client live. Next time an app needs live, precise data access, you know the service that skips the plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop five of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert&lt;/li&gt;
&lt;li&gt;Amazon IVS&lt;/li&gt;
&lt;li&gt;Amazon Rekognition&lt;/li&gt;
&lt;li&gt;Amazon Personalize&lt;/li&gt;
&lt;li&gt;AWS AppSync (you are here)&lt;/li&gt;
&lt;li&gt;Amazon Timestream&lt;/li&gt;
&lt;li&gt;Amazon Textract&lt;/li&gt;
&lt;li&gt;Amazon Kendra&lt;/li&gt;
&lt;li&gt;AWS DataSync&lt;/li&gt;
&lt;li&gt;AWS IoT Core
Next up is Amazon Timestream, a database built specifically for time series data like sensor readings and metrics.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>awsappsync</category>
    </item>
    <item>
      <title>Amazon Personalize: Build Recommendation Systems Without ML</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Mon, 17 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-personalize-build-recommendation-systems-without-ml-2kde</link>
      <guid>https://dev.to/aws-builders/amazon-personalize-build-recommendation-systems-without-ml-2kde</guid>
      <description>&lt;p&gt;The recommendation technology behind Amazon.com, packaged as a service you can use without a data science team. Stop four in the AWS Hidden Gems series.&lt;/p&gt;

&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: Amazon Personalize
&lt;/h2&gt;

&lt;p&gt;"Customers who bought this also bought" and "recommended for you" feel simple as a user, but they run on serious machine learning. Amazon Personalize gives you that same recommendation technology as a managed service. You bring your data, it trains a private model, and your app asks it what to show each user.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Good recommendations need machine learning, and building an ML recommender from scratch is a long road. You need data scientists, pipelines to clean and prepare data, model training and tuning, and infrastructure to serve predictions in real time as users browse. Most teams cannot staff that.&lt;/p&gt;

&lt;p&gt;Personalize packages the recommendation systems Amazon developed over decades. It handles the data preparation, model training, and real time serving. You never see the algorithms or the servers. You give it your interaction history, and it gives you recommendations through an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Amazon Personalize?
&lt;/h2&gt;

&lt;p&gt;Personalize is a managed recommendation service. A recommendation system predicts what a user is most likely to want next, based on their behavior and the behavior of similar users.&lt;/p&gt;

&lt;p&gt;You give it three kinds of data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interactions: the events that happened, like a user viewing, clicking, or buying an item. This is the most important data.&lt;/li&gt;
&lt;li&gt;Items: details about the things you recommend, like category or price.&lt;/li&gt;
&lt;li&gt;Users: details about your users, like age group or location.
From that, Personalize can power several things: personalized recommendations for each user, similar items to something a user is viewing, and a personalized reordering of a list you already have. It keeps recommendations fresh by taking in new activity as it happens, so suggestions shift as a user browses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What Personalize does not do is give good results without enough data. It needs a reasonable history of interactions to learn from, so it suits products that already have some usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;A streaming video app has a growing catalog and a home screen that shows the same popular titles to everyone. Users scroll past them because nothing feels relevant to their taste.&lt;/p&gt;

&lt;p&gt;The team wants a "recommended for you" row that reflects what each person actually watches. Building that with in house machine learning would take months and specialists they do not have.&lt;/p&gt;

&lt;p&gt;With Personalize, they upload their viewing history, choose a recommendation type, let it train, and call it from the app to fill that row per user. The home screen starts reflecting real taste, and watch time goes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Streaming services build personalized "recommended for you" rows from viewing history&lt;/li&gt;
&lt;li&gt;Online stores show product recommendations and "you might also like" suggestions&lt;/li&gt;
&lt;li&gt;News and content sites reorder articles to match each reader's interests&lt;/li&gt;
&lt;li&gt;Marketplaces surface relevant listings instead of the same popular few&lt;/li&gt;
&lt;li&gt;Apps personalize search results and category pages per user&lt;/li&gt;
&lt;li&gt;Marketing teams pick the most relevant items to feature in an email to each customer
The pattern is showing each user the items they are most likely to want, drawn from their own behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;Your interaction data usually starts in your app's database or event stream. You export it as a CSV file into S3, and Personalize imports it to train. Your app sends live events to Personalize as users act, and calls it for recommendations when rendering a screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[App activity: views, clicks, buys] --&amp;gt; B[CSV in S3]
    B --&amp;gt;|Import| C[Personalize trains a model]
    C --&amp;gt; D[Deployed recommender]
    E[Your app] --&amp;gt;|Get recommendations for a user| D
    E --&amp;gt;|Send new events| C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Personalize is the recommendation engine. S3 carries the training data in, and your app talks to the trained model over an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;You start by creating a dataset group, a container for your data. You import your interactions data, and optionally items and users, from a CSV in S3. You pick a recipe, which is the algorithm for your goal, such as recommendations for a user or similar items. Personalize trains a model on your data, which takes a while. Once trained, you deploy it so your app can query it, then call it for recommendations in real time. As users keep acting, you send those events back so the model stays current.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Create dataset group] --&amp;gt; B[Import interaction data from S3]
    B --&amp;gt; C[Choose a recipe for your goal]
    C --&amp;gt; D[Personalize trains the model]
    D --&amp;gt; E[Deploy it for your app to query]
    E --&amp;gt; F[App gets recommendations per user]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;For this you need a CSV of interactions in S3 with at least three columns: a user id, an item id, and a timestamp.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, search for Amazon Personalize, and open it. Check the region in the top right corner.&lt;/li&gt;
&lt;li&gt;Click Create dataset group, give it a name, and choose the custom option so you control the steps directly.&lt;/li&gt;
&lt;li&gt;Create the interactions dataset. Define a schema that matches your CSV columns (USER_ID, ITEM_ID, and TIMESTAMP), then start an import job pointing to your CSV file in S3. Personalize needs an IAM role that can read that S3 bucket, and the console can create this role for you.&lt;/li&gt;
&lt;li&gt;Wait for the import to finish, then create a solution, which is a trained model. Pick a recipe such as User-Personalization for per user recommendations. Personalize trains on your data, and this step can take a while depending on data size.&lt;/li&gt;
&lt;li&gt;When training finishes, create a campaign. A campaign is the deployed version of your model that your app calls for real time recommendations.&lt;/li&gt;
&lt;li&gt;Test it in the console. On the campaign page, enter a user id and Personalize returns a ranked list of item ids recommended for that user.&lt;/li&gt;
&lt;li&gt;To confirm the setup end to end, call the campaign from your code as shown next and check that it returns items for a real user id.
Common mistakes: an import that fails almost always means the CSV columns do not match the schema you defined, so line them up exactly. If recommendations look random, the model may not have had enough interaction data, since Personalize needs a reasonable history to learn.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using it from code
&lt;/h2&gt;

&lt;p&gt;Once a campaign is deployed, getting recommendations is a single call with a user id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;personalize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;personalize-runtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;personalize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_recommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;campaignArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:personalize:us-east-1:123456789012:campaign/my-campaign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user-123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;numResults&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# how many items to recommend
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itemList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itemId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To send new activity so recommendations stay fresh, use the put_events call on the personalize-events client as users view or buy items.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data import&lt;/td&gt;
&lt;td&gt;Per GB of data ingested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training&lt;/td&gt;
&lt;td&gt;Per hour of model training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real time recommendations&lt;/td&gt;
&lt;td&gt;Per throughput unit per hour while deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch recommendations&lt;/td&gt;
&lt;td&gt;Per user or item processed in bulk jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;First 2 months include data processing, some training hours, and recommendation capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS AI services family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS AI Services
├── Personalize   recommendations and personalization
├── Rekognition   image and video understanding
├── Textract      data extraction from documents
├── Comprehend    text analysis and sentiment
├── Transcribe    speech to text
└── Lex           chatbots and voice assistants
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like the rest of this family, Personalize hands you a hard machine learning capability through a simple API. Rekognition, from the last article, does the same for vision. Personalize does it for recommendations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Personalize turns your own usage data into recommendations, using Amazon's recommendation technology without asking you to build or understand the models. Next time a product needs "recommended for you" done well, you know the service that gets you there without a data science team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop four of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert&lt;/li&gt;
&lt;li&gt;Amazon IVS&lt;/li&gt;
&lt;li&gt;Amazon Rekognition&lt;/li&gt;
&lt;li&gt;Amazon Personalize (you are here)&lt;/li&gt;
&lt;li&gt;AWS AppSync&lt;/li&gt;
&lt;li&gt;Amazon Timestream&lt;/li&gt;
&lt;li&gt;Amazon Textract&lt;/li&gt;
&lt;li&gt;Amazon Kendra&lt;/li&gt;
&lt;li&gt;AWS DataSync&lt;/li&gt;
&lt;li&gt;AWS IoT Core
Next up is AWS AppSync, the easiest way to build APIs that update in real time.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awspersonalize</category>
    </item>
    <item>
      <title>Amazon Rekognition: Add AI Vision to Your Applications</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Wed, 12 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-rekognition-add-ai-vision-to-your-applications-3m4f</link>
      <guid>https://dev.to/aws-builders/amazon-rekognition-add-ai-vision-to-your-applications-3m4f</guid>
      <description>&lt;p&gt;Give your app the ability to understand images and video, with no machine learning knowledge required. Stop three in the AWS Hidden Gems series.&lt;/p&gt;

&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: Amazon Rekognition
&lt;/h2&gt;

&lt;p&gt;Your app can store and show images. Rekognition lets it understand them too: what objects are in a photo, what text appears, whether a face matches, or whether an image is unsafe. You call an API and get the answer back, with no models to train.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Teaching a computer to understand images is one of the hardest problems in software. Traditionally it meant hiring machine learning engineers, collecting and labeling thousands of training images, renting GPUs, which are specialized chips for training models, and maintaining that model over time. That is out of reach for most teams.&lt;/p&gt;

&lt;p&gt;Rekognition removes all of that. AWS trained the deep learning models already, on huge datasets, and exposes them through a simple API. You send an image, you get structured results back. The hard research and training is done, and you just make a call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Amazon Rekognition?
&lt;/h2&gt;

&lt;p&gt;Rekognition is a managed computer vision service. Computer vision means software that interprets images and video the way a person visually would.&lt;/p&gt;

&lt;p&gt;It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect objects and scenes, returning labels like dog, car, or beach with a confidence score&lt;/li&gt;
&lt;li&gt;Detect and analyze faces, including expressions and an estimated age range&lt;/li&gt;
&lt;li&gt;Compare two faces, or search a face against a stored collection to find a match&lt;/li&gt;
&lt;li&gt;Read text that appears inside an image, like a sign or a label&lt;/li&gt;
&lt;li&gt;Flag unsafe or explicit content for moderation&lt;/li&gt;
&lt;li&gt;Detect safety equipment such as helmets and masks in workplace photos&lt;/li&gt;
&lt;li&gt;Analyze stored or live video for the same things across frames
For images, results come back instantly. For video, Rekognition processes the file in the background and notifies you when it is done. If the built in labels are not enough, Custom Labels lets you train it to recognize your own specific objects from a small set of example images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;A marketplace app lets users upload photos of items they want to sell. Some upload blurry pictures, some pick the wrong category, and a few upload content that breaks the rules.&lt;/p&gt;

&lt;p&gt;Reviewing every photo by hand does not scale past a few hundred a day. The team needs each upload checked automatically: is there actually a product in the photo, does it match the chosen category, and is the content safe to show.&lt;/p&gt;

&lt;p&gt;Rekognition handles all three. One call returns labels to confirm the category, and another checks for unsafe content, so only clean, correctly tagged listings go live. No human in the loop for the routine cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Marketplaces and social apps moderate uploaded images automatically for unsafe content&lt;/li&gt;
&lt;li&gt;Retail tags product photos with labels so search and filtering work without manual data entry&lt;/li&gt;
&lt;li&gt;Media companies find every clip a specific person or object appears in across a video library&lt;/li&gt;
&lt;li&gt;Security and access systems match a face at a door against a collection of approved people&lt;/li&gt;
&lt;li&gt;Manufacturing checks photos for required safety gear before allowing entry to a site&lt;/li&gt;
&lt;li&gt;Apps make images searchable by reading the text printed inside them
The pattern is turning raw pixels into structured data your app can act on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;Images and video usually live in S3. A common setup: a user uploads an image to S3, that upload triggers a Lambda function, the function calls Rekognition, and the results get stored in DynamoDB for your app to use. For faces, Rekognition keeps a collection, which is a searchable index of face data. For video, results are delivered through SNS, the AWS notification service, when the background job finishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[User uploads image] --&amp;gt; B[S3 bucket]
    B --&amp;gt;|Upload event| C[Lambda function]
    C --&amp;gt;|Analyze image| D[Rekognition]
    D --&amp;gt;|Labels and results| C
    C --&amp;gt;|Store results| E[DynamoDB]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rekognition is the vision brain you call from your own code. Everything around it, meaning storage, triggers, and results, uses services you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;For an image, the flow is one step: send the image to Rekognition, either its S3 location or the raw bytes, and it returns labels, faces, text, or moderation results as JSON. For video, it is three steps: start a job on a video in S3, Rekognition processes it in the background, and it sends a notification through SNS when the results are ready to fetch.&lt;/p&gt;

&lt;p&gt;Face search adds a setup step. First you add known faces to a collection with IndexFaces. Then, to identify someone, you search a new face against that collection and get back the closest matches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Image or video] --&amp;gt; B{Type?}
    B --&amp;gt;|Image| C[Call Rekognition, get results instantly]
    B --&amp;gt;|Video| D[Start a job]
    D --&amp;gt; E[Rekognition processes in background]
    E --&amp;gt; F[SNS notifies you]
    F --&amp;gt; G[Fetch results]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;The Rekognition console has built in demos, so you can try it on your own image before writing any code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, search for Rekognition, and open it. Check the region in the top right corner.&lt;/li&gt;
&lt;li&gt;In the left menu under Demos, click Label detection. This is the feature that identifies objects and scenes.&lt;/li&gt;
&lt;li&gt;Upload an image from your computer, or use one of the samples provided. Rekognition analyzes it right away.&lt;/li&gt;
&lt;li&gt;Look at the results. On the right you see a list of labels, each with a confidence score from 0 to 100. The image on the left highlights where objects were found. Try a few different images to see how the labels change.&lt;/li&gt;
&lt;li&gt;Explore the other demos in the left menu, such as Facial analysis, Text in image, and Content moderation, to see the range of what Rekognition returns.&lt;/li&gt;
&lt;li&gt;To use Rekognition from your own code, create an IAM user or role with the AmazonRekognitionReadOnlyAccess policy for the detection APIs, plus s3:GetObject permission on the bucket holding your images. This lets your code call Rekognition and read the images it needs to analyze.&lt;/li&gt;
&lt;li&gt;To confirm your setup works outside the console, run the code in the next section against an image in your S3 bucket and check that labels come back.
Common mistakes: an access error usually means the IAM role is missing either Rekognition permission or read access to the S3 bucket, so check both. If labels seem wrong, remember the confidence score and filter out anything below a threshold like 80 for your app.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using it from code
&lt;/h2&gt;

&lt;p&gt;This calls Rekognition on an image stored in S3 and prints the labels it finds, with their confidence scores.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;rekognition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rekognition&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rekognition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect_labels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3Object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-image-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;MaxLabels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# return at most 10 labels
&lt;/span&gt;    &lt;span class="n"&gt;MinConfidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# ignore anything the model is less than 80% sure about
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Labels&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confidence&lt;/span&gt;&lt;span class="sh"&gt;"&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;Swap detect_labels for detect_moderation_labels to check for unsafe content, or detect_text to read text in the image. The shape of the call stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Image analysis&lt;/td&gt;
&lt;td&gt;Per image processed, per feature (labels, faces, text, moderation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image rate&lt;/td&gt;
&lt;td&gt;About $1.00 per 1,000 images for common features (US, first tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video analysis&lt;/td&gt;
&lt;td&gt;Per minute of video processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Face storage&lt;/td&gt;
&lt;td&gt;Small monthly charge per 1,000 faces kept in a collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Volume discounts&lt;/td&gt;
&lt;td&gt;Lower per unit rates as monthly volume grows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;5,000 images per month for the first 12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS AI services family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS AI Services
├── Rekognition   understands images and video
├── Textract      pulls text and data from documents
├── Transcribe    turns speech into text
├── Comprehend    finds meaning and sentiment in text
├── Polly         turns text into speech
└── Translate     translates between languages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the ready to use AI services, each solving one problem through a simple API with no model training. Rekognition is the one for vision. Textract, coming later in this series, is the one built specifically for documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Rekognition gives your app eyes. Objects, faces, text, and unsafe content all come back from a single API call, with the machine learning already done for you. Next time a feature needs to understand an image, you know you do not have to build a vision model to get it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop three of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert&lt;/li&gt;
&lt;li&gt;Amazon IVS&lt;/li&gt;
&lt;li&gt;Amazon Rekognition (you are here)&lt;/li&gt;
&lt;li&gt;Amazon Personalize&lt;/li&gt;
&lt;li&gt;AWS AppSync&lt;/li&gt;
&lt;li&gt;Amazon Timestream&lt;/li&gt;
&lt;li&gt;Amazon Textract&lt;/li&gt;
&lt;li&gt;Amazon Kendra&lt;/li&gt;
&lt;li&gt;AWS DataSync&lt;/li&gt;
&lt;li&gt;AWS IoT Core
Next up is Amazon Personalize, which builds recommendation systems using the same technology behind Amazon.com, again with no machine learning required.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awsrekognition</category>
    </item>
    <item>
      <title>Amazon IVS: Build Live Streaming Without the Complexity</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/amazon-ivs-build-live-streaming-without-the-complexity-1ikj</link>
      <guid>https://dev.to/aws-builders/amazon-ivs-build-live-streaming-without-the-complexity-1ikj</guid>
      <description>&lt;p&gt;Live video in your app without running a single streaming server. Stop two in the AWS Hidden Gems series.&lt;/p&gt;

&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: Amazon IVS
&lt;/h2&gt;

&lt;p&gt;In the last article, MediaConvert handled video files that already existed. Amazon IVS, short for Interactive Video Service, is for the opposite case: video happening right now, like a live class, a product launch, or a gaming stream. It is the managed way to put live video into your app or website.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Live streaming is genuinely hard to build. You need a server to receive the incoming video, something to transcode it into different qualities, a way to package it for players, a global network to deliver it, and a video player for viewers. On top of that, live audiences are spiky. Ten viewers one minute, ten thousand the next. Getting low latency, meaning a small delay between the camera and the viewer, takes real expertise.&lt;/p&gt;

&lt;p&gt;Before IVS, teams stitched this together from separate services like MediaLive, MediaPackage, and a content delivery network, then built and tuned their own player. It worked, but it was a lot of moving parts.&lt;/p&gt;

&lt;p&gt;IVS bundles all of it into one service. You create a channel, point your camera software at it, and share a playback URL. IVS handles ingest, transcoding, delivery, and scaling, and it gives you a ready made player. It is built on the same technology that runs Twitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Amazon IVS?
&lt;/h2&gt;

&lt;p&gt;Amazon IVS is a fully managed live streaming service. It comes in two modes.&lt;/p&gt;

&lt;p&gt;Low latency streaming is for broadcasting to an audience, from one host to thousands or millions of viewers, with a delay of only a few seconds. This is the classic live stream.&lt;/p&gt;

&lt;p&gt;Real time streaming is for interactive video where the delay is under a third of a second. Think multiple hosts on screen together, audio rooms, or a guest joining a broadcast. It supports up to twelve hosts publishing at once.&lt;/p&gt;

&lt;p&gt;Both give you SDKs for web, iOS, and Android, so you can broadcast and play video from inside your own app. IVS can also record streams to S3, add live chat, and carry timed metadata, which is data synced to a moment in the video for things like polls or product highlights.&lt;/p&gt;

&lt;p&gt;What IVS does not do is process video files that already exist. That is MediaConvert, from the last article. IVS is only for live.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;A fitness startup wants live workout classes in its app. An instructor streams from a studio, and members join from their phones at home.&lt;/p&gt;

&lt;p&gt;Building this from scratch means running ingest servers, transcoding the video for different connection speeds, delivering it worldwide with a small delay, and writing a player for iOS and Android. The team is four people. None of that is their actual product.&lt;/p&gt;

&lt;p&gt;With IVS, they create a channel, give the instructor a stream key, and drop the IVS player into their app pointed at the playback URL. The hard parts are handled, and they get back to building the fitness features that make the app worth using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fitness and education run live classes with instructors streaming to members at home&lt;/li&gt;
&lt;li&gt;Gaming and creator platforms build their own Twitch style streaming without owning the infrastructure&lt;/li&gt;
&lt;li&gt;Retail and ecommerce run live shopping shows where hosts demo products and viewers buy in the moment&lt;/li&gt;
&lt;li&gt;Events stream conferences, launches, and town halls to a large remote audience&lt;/li&gt;
&lt;li&gt;Social apps add live audio rooms and multi host video using real time streaming&lt;/li&gt;
&lt;li&gt;Auctions and betting need the tiny delay of real time streaming so no viewer sees the action late
The common thread is live video inside an app, without a streaming team to run it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;The flow is simple. Broadcast software or the IVS broadcast SDK sends live video to an IVS channel using RTMPS, a secure protocol for pushing live streams. IVS transcodes the video into several qualities and delivers it through a playback URL. Viewers watch using the IVS player in your app. Optionally, IVS records the stream to S3 and sends stream events, like start and stop, to EventBridge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Host camera and OBS] --&amp;gt;|RTMPS| B[IVS channel ingest]
    B --&amp;gt; C[IVS transcode and deliver]
    C --&amp;gt;|Playback URL| D[IVS player in your app]
    C --&amp;gt;|Optional recording| E[S3 bucket]
    C --&amp;gt;|Stream events| F[EventBridge]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IVS is the whole live pipeline in one service. You bring the camera and the app, and it handles everything in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;You create a channel, which gives you three things: an ingest endpoint where video goes in, a stream key that authorizes your broadcast, and a playback URL where viewers watch. You put the ingest endpoint and stream key into your broadcast software, then start streaming. IVS ingests the video, transcodes it into multiple qualities on the fly, and serves it through the playback URL with only a few seconds of delay. Viewers open your app and the player loads the stream. When you stop broadcasting, the stream ends, and if recording is on, the file lands in S3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Create channel] --&amp;gt; B[Get ingest endpoint, stream key, playback URL]
    B --&amp;gt; C[Start broadcasting to the ingest endpoint]
    C --&amp;gt; D[IVS transcodes and delivers]
    D --&amp;gt; E[Viewers watch through the player]
    C --&amp;gt; F[Stop broadcasting ends the stream]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;You will create a channel, then stream to it using OBS Studio, a free and popular broadcast tool.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, search for IVS or Interactive Video Service, and open it. Check the region in the top right corner, since your channel lives in one region.&lt;/li&gt;
&lt;li&gt;Click Create channel. For a first test, keep the default setup. The settings that matter are the channel type, which you can leave as Standard so IVS transcodes to several qualities, and the latency, which you can leave as Low. Give the channel a name and click Create channel.&lt;/li&gt;
&lt;li&gt;On the channel page, IVS shows three things you need. Copy the Ingest server, which is an RTMPS address. Reveal and copy the Stream key, and keep it secret, since anyone who has it can stream to your channel. Copy the Playback URL as well.&lt;/li&gt;
&lt;li&gt;Install and open OBS Studio. Go to Settings, then Stream. Set Service to Custom, paste the IVS ingest server into the Server field, and paste your stream key into the Stream Key field. Click OK.&lt;/li&gt;
&lt;li&gt;In OBS, add a source so there is something to show, such as your webcam or a screen capture, then click Start Streaming.&lt;/li&gt;
&lt;li&gt;Go back to the channel page in the IVS console. Within a few seconds the channel shows as Live with a preview of your video. You can also open the Playback URL in the IVS web player to watch it exactly as a viewer would.&lt;/li&gt;
&lt;li&gt;To confirm everything works, check that the preview matches your camera and plays smoothly. Click Stop Streaming in OBS to end the stream.&lt;/li&gt;
&lt;li&gt;To save your streams, edit the channel and turn on recording to an S3 bucket. IVS stores each stream there automatically, and the console sets up the permission it needs to write to the bucket. No IAM role is needed to broadcast, because the stream key is what authorizes you.
Common mistakes: if OBS says it cannot connect, the ingest server or stream key is usually wrong, so paste them again carefully. If the console never shows Live, make sure OBS is actually streaming and that you picked the Custom service option rather than a preset like Twitch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Going live from code
&lt;/h2&gt;

&lt;p&gt;Two pieces of code make IVS practical: creating a channel on your backend, and showing the stream in your app.&lt;/p&gt;

&lt;p&gt;First, create a channel with the AWS SDK. This returns the same ingest endpoint, stream key, and playback URL you saw in the console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;ivs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ivs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ivs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-live-channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STANDARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# transcodes the stream into several qualities
&lt;/span&gt;    &lt;span class="n"&gt;latencyMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# a few seconds of delay, good for most live streams
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;stream_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ingest server:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingestEndpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Playback URL:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;playbackUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stream key:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# keep this secret
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, on the viewer side, load the playback URL in the IVS web player.&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="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://player.live-video.net/1.x/amazon-ivs-player.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"video-player"&lt;/span&gt; &lt;span class="na"&gt;playsinline&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;IVSPlayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachHTMLVideoElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video-player&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_PLAYBACK_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To call create_channel, the backend's IAM role needs IVS permissions such as ivs:CreateChannel. The player needs nothing secret, since the playback URL is safe to share with viewers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What you pay for&lt;/td&gt;
&lt;td&gt;Input hours (video you send in) plus output hours (video sent to viewers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate driver&lt;/td&gt;
&lt;td&gt;Video quality, grouped as SD, HD, and Full HD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input, HD&lt;/td&gt;
&lt;td&gt;About $2.00 per streaming hour (US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output, HD&lt;/td&gt;
&lt;td&gt;About $0.15 per viewer hour (US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost example&lt;/td&gt;
&lt;td&gt;1 hour HD stream watched by 100 viewers, about $2 input plus $15 output, near $17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real time streaming&lt;/td&gt;
&lt;td&gt;Billed separately, by participant minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recording&lt;/td&gt;
&lt;td&gt;Saved to S3, so normal S3 storage rates apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;None for IVS streaming hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS live video family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Live and Media Services
├── Amazon IVS      all in one live streaming with a built in player
├── MediaLive       broadcast grade live encoding you assemble yourself
├── MediaPackage    packaging and protection for live and on demand video
├── MediaConnect    reliable transport of live video between points
└── MediaConvert    file based transcoding for videos that already exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The choice comes down to control versus speed. IVS is the fastest way to add live video to an app, with the player included. MediaLive with MediaPackage is the build it yourself route when you need fine control over a broadcast. MediaConvert, from the last article, is for finished files rather than live video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;IVS turns live streaming from a big infrastructure project into a channel, a stream key, and a playback URL. You bring the camera and the app, and IVS handles ingest, transcoding, delivery, and the player. Next time you need live video in a product, you know the service that removes the hard parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop two of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert&lt;/li&gt;
&lt;li&gt;Amazon IVS (you are here)&lt;/li&gt;
&lt;li&gt;Amazon Rekognition&lt;/li&gt;
&lt;li&gt;Amazon Personalize&lt;/li&gt;
&lt;li&gt;AWS AppSync
Next up is Amazon Rekognition, which adds AI vision to your apps so they can understand what is in an image or video.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AWS Elemental MediaConvert: The Video Service Every Developer Should Know</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Mon, 03 Aug 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-elemental-mediaconvert-the-video-service-every-developer-should-know-3a7l</link>
      <guid>https://dev.to/aws-builders/aws-elemental-mediaconvert-the-video-service-every-developer-should-know-3a7l</guid>
      <description>&lt;h2&gt;
  
  
  About this series
&lt;/h2&gt;

&lt;p&gt;Most AWS learning stops after EC2, S3, IAM, and Lambda. But AWS has over two hundred services, and many of the most useful ones rarely appear in tutorials.&lt;/p&gt;

&lt;p&gt;AWS Hidden Gems covers those underrated services you shouldn't ignore. Each article picks one, then explains why it exists, what it does, where it fits, and how to set it up from the console. Know the four basics above and you can follow along. Everything else gets explained as it comes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's service: MediaConvert
&lt;/h2&gt;

&lt;p&gt;Almost every app has video in it, and every video hides one boring step: converting the raw file into formats that phones, laptops, TVs, and slow networks can all play. That is what MediaConvert does.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why does this service exist?
&lt;/h2&gt;

&lt;p&gt;Transcoding means rewriting a video into a different format, quality, or resolution. You need it because one file cannot serve every viewer. A weak mobile connection needs a small version. A fast home connection wants the crisp one.&lt;/p&gt;

&lt;p&gt;Before AWS, teams ran FFmpeg (a free tool that converts almost any video) on their own server fleets. Those servers were expensive, sat idle between uploads, and needed constant patching. Getting broadcast quality output took specialist knowledge most teams did not have.&lt;/p&gt;

&lt;p&gt;MediaConvert removes all of that. You hand it a file, it does the work on infrastructure you never manage, and you pay only for the minutes you process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is MediaConvert?
&lt;/h2&gt;

&lt;p&gt;MediaConvert is a fully managed, file based video transcoding service. Fully managed means AWS runs the servers for you. File based means it works on complete video files that already exist, usually in S3, not on live streams.&lt;/p&gt;

&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changes the codec, the compression method, such as H.264 or the newer H.265&lt;/li&gt;
&lt;li&gt;Changes resolution, so one 4K master becomes 1080p, 720p, and 480p&lt;/li&gt;
&lt;li&gt;Adjusts bitrate, the data used per second and the main quality versus size lever&lt;/li&gt;
&lt;li&gt;Adds captions, extra audio tracks, logo overlays, and thumbnails&lt;/li&gt;
&lt;li&gt;Packages video for streaming with HLS and DASH, the formats behind adaptive bitrate, where the player switches quality as the network changes
What it does not do is store or deliver your videos. Storage is the job of S3. Delivery is the job of CloudFront, a network that keeps copies of your files close to users.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A real world problem
&lt;/h2&gt;

&lt;p&gt;An online course startup lets instructors upload lectures from their phones. Files arrive in every format and size. A student on wifi watches fine. A student on mobile data sees the giant 4K file stall every few seconds. Some older devices cannot play certain formats at all.&lt;/p&gt;

&lt;p&gt;The startup cannot control how instructors record or how students watch. It can only fix the middle: convert every upload into a consistent set of streaming friendly versions. That is what MediaConvert is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real world use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Streaming platforms turn studio master files into every quality level for phones, browsers, and TVs&lt;/li&gt;
&lt;li&gt;Education platforms standardize instructor uploads so playback never depends on someone's recording setup&lt;/li&gt;
&lt;li&gt;Healthcare processes recorded procedures and telehealth sessions into required formats for review and archiving&lt;/li&gt;
&lt;li&gt;Finance and enterprise transcode earnings calls and training, captioned for accessibility and stored for the long term&lt;/li&gt;
&lt;li&gt;Ecommerce converts product and review clips into fast loading versions that do not slow the page&lt;/li&gt;
&lt;li&gt;Gaming and social apps normalize huge volumes of user uploaded clips into formats the app can play
The pattern is always the same. Video comes in messy, and it leaves clean and ready to play.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in AWS
&lt;/h2&gt;

&lt;p&gt;A common setup: a video lands in S3, the upload triggers a Lambda function, Lambda starts a MediaConvert job, MediaConvert reads the file and writes converted versions back to S3, and CloudFront delivers them worldwide. MediaConvert reports progress through EventBridge, the AWS service for reacting to events, and it accesses S3 using an IAM role, a set of permissions a service borrows to act for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[User uploads video] --&amp;gt; B[S3 input bucket]
    B --&amp;gt;|Upload event| C[Lambda function]
    C --&amp;gt;|Start job| D[MediaConvert]
    B --&amp;gt;|Reads source file| D
    D --&amp;gt;|Writes outputs| E[S3 output bucket]
    E --&amp;gt; F[CloudFront]
    F --&amp;gt; G[Viewers on any device]
    D --&amp;gt;|Job status| H[EventBridge]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MediaConvert is the processing engine in the middle. Everything around it is handled by services built for that job.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the workflow runs
&lt;/h2&gt;

&lt;p&gt;The unit of work is a job: one input file, the outputs you want, and the IAM role to use. A job is submitted, waits in a queue, gets processed on managed infrastructure, and writes its outputs to S3 when done. Status updates flow through EventBridge the whole time, and failures come back with an error message.&lt;/p&gt;

&lt;p&gt;Two features save repetition. Presets store output settings, so you set 1080p H.264 once and reuse it. Job templates store a whole job's settings, so your standard outputs become a one line request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Job submitted] --&amp;gt; B[Job waits in queue]
    B --&amp;gt; C[MediaConvert processes it]
    C --&amp;gt; D{Success?}
    D --&amp;gt;|Yes| E[Outputs written to S3]
    D --&amp;gt;|No| F[Job marked with error]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting it up in the AWS Console
&lt;/h2&gt;

&lt;p&gt;Before you start, upload a short sample video to an S3 bucket. Note the bucket name and the folder where you want the output. Keep the file small so your first job finishes quickly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console, type MediaConvert into the search bar at the top, and open it. Look at the region shown in the top right corner and make sure it matches the region of your S3 bucket. MediaConvert reads from and writes to S3, so keeping both in the same region avoids extra data transfer charges and keeps the job faster.&lt;/li&gt;
&lt;li&gt;Click Create job. The page splits into two halves. The left panel is where you add the parts of your job, meaning the input and the output groups. The right panel is where you edit the settings for whatever part you have selected.&lt;/li&gt;
&lt;li&gt;In the Input section on the left, paste the full S3 path to your sample video, for example s3://your-bucket-name/sample.mp4. This is the file MediaConvert will read. Check the path carefully, because a wrong path or a missing file extension is the most common reason a job fails right away.&lt;/li&gt;
&lt;li&gt;In the left panel, find Output groups and click Add. MediaConvert asks what kind of group you want. Choose File group for this first test, which produces one converted file rather than a segmented streaming package, so it is the simplest way to confirm the flow works. Open the group and set its Destination to the S3 folder for your output, for example s3://your-bucket-name/output/.&lt;/li&gt;
&lt;li&gt;Inside the output group there is an output entry. Select it and set three things on the right: the video codec to H.264, which every device supports, the resolution to 1280 by 720, and the audio settings left at their defaults. These few values are enough to produce a clean, playable file.&lt;/li&gt;
&lt;li&gt;Scroll to the Job settings section and find the service access or IAM role option. MediaConvert needs a role that lets it read your input bucket and write to your output bucket. For a first job, pick the option that lets the console create this role for you. If you prefer to build it yourself, the role needs a trust policy allowing the MediaConvert service to assume it, plus s3:GetObject on the input bucket and s3:PutObject on the output bucket.&lt;/li&gt;
&lt;li&gt;Leave the default on demand queue selected. You do not need reserved capacity to start.&lt;/li&gt;
&lt;li&gt;Click Create at the bottom. The job appears with a status of Progressing. For a small file it changes to Complete within a minute or two. Open your output folder in S3, download the converted file, and play it to confirm the transcode worked.
Common mistakes: a permissions error almost always means the IAM role cannot reach one of your buckets, so check that it covers both the input and the output. A job that fails right away usually means the input path is wrong, so confirm it exactly, including the file extension.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Starting a job with code
&lt;/h2&gt;

&lt;p&gt;Clicking through the console is fine for learning, but in a real system you want jobs to start on their own. The usual pattern is a Lambda function that runs whenever a new video lands in S3 and tells MediaConvert to start a job.&lt;/p&gt;

&lt;p&gt;One detail trips up newcomers. MediaConvert gives each account its own endpoint, which is its own address, so you fetch that endpoint once and build the client with it. To keep the code short, first save your settings from steps 4 and 5 as a job template in the console. The template holds the codec, resolution, and output destination, so the function only has to pass the input file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="c1"&gt;# MediaConvert has a per account endpoint, so fetch it once when the function loads.
&lt;/span&gt;&lt;span class="n"&gt;default_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mediaconvert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;default_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_endpoints&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Endpoints&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;mediaconvert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mediaconvert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# The S3 event tells us which file was just uploaded.
&lt;/span&gt;    &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mediaconvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::123456789012:role/MediaConvertRole&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;JobTemplate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-video-template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inputs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FileInput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Started MediaConvert job:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Job&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Job&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this run on its own, add an S3 trigger on your input bucket that invokes the function on new uploads, and give the function's role permission to call MediaConvert and to pass the MediaConvert role.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Billing unit&lt;/td&gt;
&lt;td&gt;Per minute of output video, prorated per second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billed on&lt;/td&gt;
&lt;td&gt;Output, not input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output math&lt;/td&gt;
&lt;td&gt;One 10 minute video into 3 versions = 30 output minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic tier HD&lt;/td&gt;
&lt;td&gt;About $0.015 per minute (US, on demand)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic tier SD&lt;/td&gt;
&lt;td&gt;Lower than HD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic tier 4K&lt;/td&gt;
&lt;td&gt;Higher than HD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Professional tier&lt;/td&gt;
&lt;td&gt;Higher rate, for advanced broadcast features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost example&lt;/td&gt;
&lt;td&gt;100 videos x 10 min x 2 HD versions = 2,000 min, about $30 per month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Monthly allowance of basic tier output minutes for 12 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reserved pricing&lt;/td&gt;
&lt;td&gt;Lower rate for large, predictable monthly volume&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The AWS media services family
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Media Services
├── MediaConvert   file based transcoding for existing videos
├── MediaLive      real time encoding for live broadcasts
├── MediaPackage   packages and protects streams for delivery
├── MediaConnect   reliable transport of live video
├── MediaTailor    inserts personalized ads into streams
└── Amazon IVS     managed low latency interactive live streaming
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key split is file versus live. MediaConvert handles finished files. MediaLive and IVS handle video happening right now. If your video already exists, MediaConvert is the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;MediaConvert takes video in any shape and produces clean, playable versions for every device and network, with no servers to run. Next time you build anything with video, you know the managed service that does the heavy lifting and how to wire it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series progress
&lt;/h2&gt;

&lt;p&gt;You are on stop one of AWS Hidden Gems.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Elemental MediaConvert (you are here)&lt;/li&gt;
&lt;li&gt;Amazon IVS&lt;/li&gt;
&lt;li&gt;Amazon Rekognition&lt;/li&gt;
&lt;li&gt;Amazon Personalize&lt;/li&gt;
&lt;li&gt;AWS AppSync
Next up is Amazon IVS, which picks up where MediaConvert stops, handling live and interactive video.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;Questions, corrections, or want to talk through where this fits in your own project? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>mediaconvert</category>
      <category>elementalmediaconvert</category>
    </item>
    <item>
      <title>Terraform State Locking Without DynamoDB: S3 Native Locking Explained</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Tue, 21 Jul 2026 04:53:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/terraform-state-locking-without-dynamodb-s3-native-locking-explained-448l</link>
      <guid>https://dev.to/aws-builders/terraform-state-locking-without-dynamodb-s3-native-locking-explained-448l</guid>
      <description>&lt;p&gt;A beginner friendly guide to S3 native state locking and why it makes your Terraform setup so much cleaner&lt;/p&gt;

&lt;p&gt;Setting up remote state in Terraform used to come with a small annoyance. You could not just use an S3 bucket on its own. You also had to create a DynamoDB table sitting right next to it, purely to handle locking. Two AWS services for something that felt like one job.&lt;/p&gt;

&lt;p&gt;That has changed. Starting with Terraform 1.10, S3 can handle state locking by itself. No DynamoDB table, no extra resource to provision, no extra permissions to hand out. In this post I will walk through what state locking is, why it needed DynamoDB in the first place, what changed under the hood, and the exact steps to switch to the simpler setup.&lt;/p&gt;

&lt;p&gt;This is written for people who are still new to AWS and Terraform, so I will explain each term the first time it shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick refresher on the Terraform state file
&lt;/h2&gt;

&lt;p&gt;When you run Terraform, it needs to remember what it built. It keeps this record in a file called the state file, usually named &lt;code&gt;terraform.tfstate&lt;/code&gt;. Think of it as a map that connects the resources written in your code to the real resources living in your AWS account.&lt;/p&gt;

&lt;p&gt;If you work alone on your laptop, that file can just sit on your machine. But the moment a second person joins, everyone needs to share the same map. If two people have two different copies, they will fight over what exists and what does not.&lt;/p&gt;

&lt;p&gt;The common fix is to store the state file in a shared location that the whole team points to. This shared location is called a backend. An S3 bucket is one of the most popular backends because it is cheap, durable, and simple. A bucket is just a storage container in S3 where you keep files, which AWS calls objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why state locking matters
&lt;/h2&gt;

&lt;p&gt;Here is the problem that locking solves.&lt;/p&gt;

&lt;p&gt;Say two engineers on your team both run &lt;code&gt;terraform apply&lt;/code&gt; at almost the same moment. Both runs read the current state file. Both make their changes. Both try to write the updated state back. One write lands on top of the other, and now your state file is scrambled. In the worst case Terraform gets confused about what already exists and starts creating duplicates or destroying things it should not touch.&lt;/p&gt;

&lt;p&gt;State locking stops this. Before Terraform makes any change, it grabs a lock. While that lock is held, any other run that tries to start is told to wait. Once the first run finishes, it releases the lock and the next person can go. Only one operation writes at a time, so the state file stays clean.&lt;/p&gt;

&lt;p&gt;The question is where that lock actually lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old way: a DynamoDB table just for locks
&lt;/h2&gt;

&lt;p&gt;For a long time, the S3 backend could store your state, but it could not lock it. S3 on its own had no simple way to say "only write this if nobody else is holding it." So Terraform borrowed a second service to keep track of locks: DynamoDB, which is a fast key value database from AWS.&lt;/p&gt;

&lt;p&gt;The setup looked like this. You created a DynamoDB table with a single partition key called &lt;code&gt;LockID&lt;/code&gt;. A partition key, also called a hash key, is just the main field DynamoDB uses to find a row. When Terraform started an operation, it wrote a lock item into that table. When it finished, it deleted the item. If a second run showed up and saw a lock item already there, it knew the state was busy.&lt;/p&gt;

&lt;p&gt;The Terraform code for that table usually looked like this.&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_dynamodb_table"&lt;/span&gt; &lt;span class="s2"&gt;"terraform_locks"&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;"terraform-locks"&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;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;

  &lt;span class="nx"&gt;attribute&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;"LockID"&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;And the backend block that tied it all together looked like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"global/s3/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-south-1"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;key&lt;/code&gt; is the path to the state file inside the bucket, and &lt;code&gt;encrypt = true&lt;/code&gt; tells S3 to encrypt the file at rest.&lt;/p&gt;

&lt;p&gt;None of this is hard on its own. But notice how much you are carrying. You provision a whole extra service. You manage its name and region and make sure they match the backend config. You grant IAM permissions for it, where IAM is the AWS system that decides who is allowed to do what. And you have one more thing that can silently break, like a table created in the wrong region or a name typo that only shows up when a lock fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Two things had to line up for S3 to do locking on its own.&lt;/p&gt;

&lt;p&gt;First, in August 2024, AWS added conditional writes to S3. This is the important piece. A conditional write lets you upload an object only if it does not already exist. Technically this works through an HTTP header called &lt;code&gt;If-None-Match&lt;/code&gt;. In plain terms, you can now tell S3 "create this file, but only if it is not already there, otherwise refuse." That refuse behavior is exactly what a lock needs. If the lock file is already there, your write bounces, which means someone else has the lock.&lt;/p&gt;

&lt;p&gt;Second, Terraform 1.10 (released in November 2024) added a new backend option called &lt;code&gt;use_lockfile&lt;/code&gt;. When you turn it on, Terraform uses that S3 conditional write trick to manage locks directly in the same bucket as your state. DynamoDB is no longer part of the picture.&lt;/p&gt;

&lt;p&gt;Terraform 1.11 went a step further and marked the old &lt;code&gt;dynamodb_table&lt;/code&gt; argument as deprecated, which is the maintainers' way of saying the DynamoDB path is on its way out and you should move over.&lt;/p&gt;

&lt;p&gt;One quick clarification so you do not get tripped up. S3 native state locking has nothing to do with a feature called S3 Object Lock. Object Lock is a compliance feature that stops files from being deleted or changed for a set period. That is not what we are using here. Terraform state locking uses conditional writes, and it works on a plain, normal S3 bucket with no special mode turned on.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;When you enable &lt;code&gt;use_lockfile&lt;/code&gt; and run an operation, Terraform creates a small lock file in the same bucket, right next to your state file. It reuses your state path and adds a &lt;code&gt;.tflock&lt;/code&gt; extension.&lt;/p&gt;

&lt;p&gt;So if your state file is at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global/s3/terraform.tfstate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the lock file will appear at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global/s3/terraform.tfstate.tflock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While an operation runs, that &lt;code&gt;.tflock&lt;/code&gt; object exists. When the operation ends, Terraform deletes it. If someone else runs Terraform during that window, S3 rejects their conditional write because the lock file is already there, and Terraform shows them an error instead of letting two runs collide.&lt;/p&gt;

&lt;p&gt;The error looks something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: Error acquiring the state lock

Error message: operation error S3: PutObject, https response error
StatusCode: 412, RequestID: ..., HostID: ..., api error PreconditionFailed:
At least one of the pre-conditions you specified did not hold

Lock Info:
  ID:        3f6a1c9e-...-b2d4
  Path:      my-terraform-state-bucket/global/s3/terraform.tfstate.tflock
  Operation: OperationTypeApply
  Who:       sahil@laptop
  Created:   2026-07-21 09:14:22.5 +0000 UTC
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;StatusCode: 412&lt;/code&gt; and &lt;code&gt;PreconditionFailed&lt;/code&gt;. That 412 is the HTTP status S3 returns when a conditional write is refused because the object already exists. In other words, the lock is doing its job. Everything you need to identify who holds the lock is right there in the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps to set it up
&lt;/h2&gt;

&lt;p&gt;Here is the full walkthrough for a fresh setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Check your Terraform version
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;use_lockfile&lt;/code&gt; needs Terraform 1.10.0 or newer. Check what you have.&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;If you see anything older than 1.10, upgrade before continuing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create the S3 bucket for your state
&lt;/h3&gt;

&lt;p&gt;If you already have a state bucket you can skip ahead. If not, create one with the AWS CLI, which is the command line tool for talking to AWS. Bucket names have to be globally unique across all of AWS, so pick something specific to you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api create-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-terraform-state-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-bucket-configuration&lt;/span&gt; &lt;span class="nv"&gt;LocationConstraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ap-south-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small gotcha. Every region except &lt;code&gt;us-east-1&lt;/code&gt; needs that &lt;code&gt;--create-bucket-configuration LocationConstraint&lt;/code&gt; line. If you are using &lt;code&gt;us-east-1&lt;/code&gt;, drop that last line, because that region rejects it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Turn on versioning
&lt;/h3&gt;

&lt;p&gt;Versioning tells S3 to keep old copies of a file each time it changes. For a state file this is a safety net. If something ever corrupts your state, you can roll back to an earlier version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-versioning &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-terraform-state-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Write the backend block
&lt;/h3&gt;

&lt;p&gt;Now the good part. In your Terraform project, add the backend block with &lt;code&gt;use_lockfile = true&lt;/code&gt;. Notice there is no &lt;code&gt;dynamodb_table&lt;/code&gt; line anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"global/s3/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-south-1"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;use_lockfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;use_lockfile = true&lt;/code&gt; line is the whole feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Set the IAM permissions
&lt;/h3&gt;

&lt;p&gt;Because DynamoDB is gone, your permissions get shorter too. The role or user running Terraform now only needs S3 actions, and no DynamoDB actions at all. A minimal policy looks 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;"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="s2"&gt;"s3:ListBucket"&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="s2"&gt;"arn:aws:s3:::my-terraform-state-bucket"&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;"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="s2"&gt;"s3:GetObject"&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:DeleteObject"&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="s2"&gt;"arn:aws:s3:::my-terraform-state-bucket/global/s3/terraform.tfstate*"&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;The trailing &lt;code&gt;*&lt;/code&gt; on the resource is doing quiet but important work. It matches both the state file and the &lt;code&gt;.tflock&lt;/code&gt; lock file, since the lock file shares the same path with &lt;code&gt;.tflock&lt;/code&gt; tacked on. One rule covers both.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Initialize Terraform
&lt;/h3&gt;

&lt;p&gt;Run init to wire everything up. Terraform reads your backend block and gets the bucket ready.&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;h3&gt;
  
  
  7. Test that the lock works
&lt;/h3&gt;

&lt;p&gt;Run a normal plan and apply.&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
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While the apply is running, open the S3 console or run &lt;code&gt;aws s3 ls&lt;/code&gt; on your bucket path. You will see a &lt;code&gt;terraform.tfstate.tflock&lt;/code&gt; object appear during the run and vanish once it finishes. If you want to see the lock in action, open a second terminal and start another &lt;code&gt;terraform apply&lt;/code&gt; while the first one is still going. The second one will hit that 412 lock error from earlier and refuse to run until the first is done. That is the whole point working exactly as designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving over if you already use DynamoDB
&lt;/h2&gt;

&lt;p&gt;If you have an existing setup with a DynamoDB lock table, switching is easy and you do not have to do it all in one nervous jump.&lt;/p&gt;

&lt;p&gt;The cautious approach is to keep both options set for a short transition period. You can list &lt;code&gt;use_lockfile = true&lt;/code&gt; and &lt;code&gt;dynamodb_table&lt;/code&gt; at the same time, and Terraform will acquire locks in both places. That way nothing breaks while you and your team catch up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-terraform-state-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"global/s3/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-south-1"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-locks"&lt;/span&gt;
    &lt;span class="nx"&gt;use_lockfile&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After changing the backend, run init again with the reconfigure flag so Terraform picks up the new settings.&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;span class="nt"&gt;-reconfigure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you are confident everyone is on Terraform 1.10 or newer and the S3 locking is working, remove the &lt;code&gt;dynamodb_table&lt;/code&gt; line, run &lt;code&gt;terraform init -reconfigure&lt;/code&gt; one more time, and then delete the DynamoDB table you no longer need.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a lock gets stuck
&lt;/h2&gt;

&lt;p&gt;Every so often a Terraform run crashes halfway, or someone closes their laptop mid apply. When that happens the &lt;code&gt;.tflock&lt;/code&gt; file can be left behind even though no operation is actually running. The next person then gets the lock error for a lock nobody holds.&lt;/p&gt;

&lt;p&gt;To clear it, grab the lock ID from the error output (the &lt;code&gt;ID&lt;/code&gt; field) and run force unlock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform force-unlock 3f6a1c9e-...-b2d4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this carefully. Only force unlock when you are sure no real Terraform operation is running anywhere, otherwise you are back to the exact race condition that locking was meant to prevent. If in doubt, ask your team before you unlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this genuinely reduces complexity
&lt;/h2&gt;

&lt;p&gt;Pull back and look at what you removed. You went from two services to one. There is no DynamoDB table to create, name, tag, watch, or clean up. Your lock now lives in the same bucket as your state, so there is one place to look when you want to understand what is going on.&lt;/p&gt;

&lt;p&gt;The bootstrap problem shrinks too. Every Terraform team hits the same chicken and egg puzzle at the start. To use remote state you first need the backend infrastructure to exist, but that infrastructure is itself something you want to manage. With the old approach that meant standing up a bucket and a table before anything else. Now it is just a bucket.&lt;/p&gt;

&lt;p&gt;Your permissions shrink. No DynamoDB actions to grant means a shorter IAM policy and fewer things a reviewer has to reason about. Your Terraform code shrinks, because the whole &lt;code&gt;aws_dynamodb_table&lt;/code&gt; resource can be deleted. And you cut out a category of quiet failures, like a lock table sitting in the wrong region or a name that does not match the backend config.&lt;/p&gt;

&lt;p&gt;Same safety, fewer moving parts. That is a rare and welcome trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;State locking has always been one of those things you set up once, forget about, and only remember when it saves you from corrupting shared state. The mechanics used to lean on DynamoDB because S3 could not do the job alone. Now that S3 supports conditional writes, that reason is gone, and Terraform 1.10 gives you a one line switch to take advantage of it.&lt;/p&gt;

&lt;p&gt;If you are starting a new project, reach for &lt;code&gt;use_lockfile&lt;/code&gt; from day one. If you already have a DynamoDB table doing this work, plan a calm migration, keep both settings on for a bit, then retire the table. Your future self, staring at a much simpler backend block, will thank you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let us connect
&lt;/h2&gt;

&lt;p&gt;If you have questions, ran into a snag while trying this, or just want to talk Terraform and AWS, reach out at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;. I am always happy to help someone get unstuck.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>terraform</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>Build and Deploy a 3 Tier Serverless App on AWS with Terraform Modules</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Tue, 14 Jul 2026 04:59:11 +0000</pubDate>
      <link>https://dev.to/aws-builders/build-and-deploy-a-3-tier-serverless-app-on-aws-with-terraform-modules-32pe</link>
      <guid>https://dev.to/aws-builders/build-and-deploy-a-3-tier-serverless-app-on-aws-with-terraform-modules-32pe</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Most tutorials show you how to create a Lambda function by clicking around the AWS console. That works once. Then you need a staging environment, and you click everything again. Then production. Then someone asks "wait, did staging have the 30 second timeout or was that only dev?" and nobody knows, because the infrastructure lives in memory and screenshots.&lt;/p&gt;

&lt;p&gt;This post walks through a project where the entire 3 tier stack comes from a single &lt;code&gt;terraform apply&lt;/code&gt;. Tier one is the frontend on Amplify, tier two is the API on Lambda and API Gateway, tier three is a MySQL database on RDS. Run the apply in the dev folder, you get a dev environment. Run it in prod, you get prod. Same code, zero clicking.&lt;/p&gt;

&lt;p&gt;The app itself is a small todo application. That is on purpose. The app is just a passenger. The real work is the Terraform: reusable modules, three isolated environments, remote state stored in S3, and module outputs wired into module inputs so everything builds in the correct order.&lt;/p&gt;

&lt;p&gt;The full source is here: &lt;a href="https://github.com/TanseerS/serverless-web-application" rel="noopener noreferrer"&gt;github.com/TanseerS/serverless-web-application&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick note on the word serverless before we start. It does not mean there are no servers. It means AWS manages the servers and you only bring code. You pay when the code runs, not while it sits idle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1kcdw74i1t48tyot1ky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1kcdw74i1t48tyot1ky.png" alt="Architecture diagram of the serverless web application" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The flow is simple to trace. A user opens a React app hosted on AWS Amplify (a service that builds and hosts frontends straight from a GitHub repo). The app calls a REST API on API Gateway (the AWS service that gives your code an HTTPS URL). Every route on that API points to a single Lambda function (code that runs on demand without a server you manage). Inside the Lambda, an Express app handles the routing and talks to a MySQL database on RDS (managed relational databases on AWS).&lt;/p&gt;

&lt;p&gt;Deployments are split in two. Push to GitHub and Amplify rebuilds the frontend on its own, since CI/CD comes free with Amplify. Change the backend code or any infrastructure, and &lt;code&gt;terraform apply&lt;/code&gt; ships it. Terraform even re-zips the backend folder automatically whenever the source changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repository layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;serverless-web-application/
├── frontend/            # React 18 + Vite app
├── backend/             # Express + Sequelize API, packaged for Lambda
├── assets/              # diagrams and screenshots
└── infrastructure/
    ├── bootstrap/       # run once, creates the S3 state bucket
    ├── modules/
    │   ├── database/    # RDS MySQL, security group, subnet group
    │   ├── backend/     # Lambda, API Gateway, IAM, code packaging
    │   └── frontend/    # Amplify app, branch, IAM
    └── environments/
        ├── dev/
        ├── staging/
        └── prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two ideas make this layout work.&lt;/p&gt;

&lt;p&gt;First, modules are reusable building blocks. The database module does not know or care whether it is building dev or prod. It takes a &lt;code&gt;project&lt;/code&gt; and an &lt;code&gt;environment&lt;/code&gt; variable and names everything &lt;code&gt;${var.project}-${var.environment}-...&lt;/code&gt;, so the same code stamps out &lt;code&gt;todo-app-dev-rds&lt;/code&gt;, &lt;code&gt;todo-app-staging-rds&lt;/code&gt; and &lt;code&gt;todo-app-prod-rds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Second, each environment folder is tiny. It looks up the default VPC with data sources, calls the three modules, and passes in values from its own &lt;code&gt;terraform.tfvars&lt;/code&gt; file. Promoting a change from dev to prod means changing directory and applying again. You never edit module code to deploy an environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: solve the chicken and egg problem
&lt;/h2&gt;

&lt;p&gt;Terraform keeps a state file, which is its record of everything it has created. By default that file sits on your laptop, which is fine until your laptop is not around or a teammate runs an apply and now there are two versions of the truth. The fix is remote state: store the file in an S3 bucket instead.&lt;/p&gt;

&lt;p&gt;But here is the catch. The S3 bucket has to exist before Terraform can store state in it, and we want Terraform to create the bucket. Who creates the creator?&lt;/p&gt;

&lt;p&gt;The answer is a small run once setup called the bootstrap pattern. The &lt;code&gt;infrastructure/bootstrap/&lt;/code&gt; folder is a tiny Terraform project with plain local state. Its only job is to create the state bucket. You run it once per AWS account and forget about it.&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;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&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_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-statefile-${data.aws_caller_identity.current.account_id}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the bucket name. S3 bucket names are globally unique across every AWS account on the planet, so &lt;code&gt;todo-app-statefile&lt;/code&gt; alone would probably be taken. The &lt;code&gt;aws_caller_identity&lt;/code&gt; data source reads your AWS account ID at plan time and appends it, so the name is unique without hardcoding anything.&lt;/p&gt;

&lt;p&gt;The bootstrap also turns on three protections for the bucket: versioning (every old copy of your state is kept, which is what saves you when an apply goes wrong), full public access blocking, and AES 256 encryption at rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: remote state with S3 native locking
&lt;/h2&gt;

&lt;p&gt;With the bucket in place, every environment points at it through a &lt;code&gt;backend.tf&lt;/code&gt; file. Same bucket, different key per environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"todo-app-statefile-&amp;lt;account-id&amp;gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dev/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-south-1"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;use_lockfile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because dev, staging and prod each write to their own key, an apply in dev can never touch prod. They are fully isolated.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;use_lockfile = true&lt;/code&gt; line deserves attention. State locking stops two people from running apply at the same time and corrupting the state. For years the standard answer was a DynamoDB table just for the lock. Terraform 1.10 added native S3 locking: Terraform drops a lock object into the bucket itself while an apply runs. One less resource to create, pay for and explain.&lt;/p&gt;

&lt;p&gt;Here is the bucket after all three environments have been initialized, one prefix per state file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdq4ajvkybmnaancwssu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdq4ajvkybmnaancwssu5.png" alt="S3 state bucket with dev, staging and prod prefixes, versioning and encryption enabled" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the database module
&lt;/h2&gt;

&lt;p&gt;The database module creates three things. A DB subnet group that tells RDS which subnets it can live in. A security group (a virtual firewall) allowing inbound MySQL on port 3306. And the RDS instance itself:&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_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-${var.environment}-rds"&lt;/span&gt;
  &lt;span class="nx"&gt;db_name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TodoAppDb"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mysql"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"8.0"&lt;/span&gt;
  &lt;span class="nx"&gt;allocated_storage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
  &lt;span class="nx"&gt;storage_type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gp3"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.micro"&lt;/span&gt;

  &lt;span class="nx"&gt;username&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;username&lt;/span&gt;
  &lt;span class="nx"&gt;password&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;password&lt;/span&gt;

  &lt;span class="nx"&gt;db_subnet_group_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_subnet_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;publicly_accessible&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;skip_final_snapshot&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;MySQL 8.0 on a &lt;code&gt;db.t3.micro&lt;/code&gt; with 20 GB of gp3 storage, which keeps it inside the free tier.&lt;/p&gt;

&lt;p&gt;One honest admission. The security group currently allows 3306 from &lt;code&gt;0.0.0.0/0&lt;/code&gt;, meaning anywhere on the internet, and the instance is publicly accessible. That is a demo shortcut so the Lambda can reach the database without living inside the VPC. Production would put RDS in private subnets, place the Lambda inside the VPC, and scope the security group to the Lambda alone. The repo says this out loud in its trade offs section rather than pretending it is fine.&lt;/p&gt;

&lt;p&gt;The module ends by outputting &lt;code&gt;db_endpoint&lt;/code&gt;, &lt;code&gt;db_name&lt;/code&gt; and &lt;code&gt;db_port&lt;/code&gt;. Remember those. They are about to become someone else's inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: the backend module, where most of the Terraform lives
&lt;/h2&gt;

&lt;p&gt;This module packages the code, creates the Lambda, and builds the entire REST API. A few pieces are worth slowing down for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Packaging code without a build script
&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;data&lt;/span&gt; &lt;span class="s2"&gt;"archive_file"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"zip"&lt;/span&gt;
  &lt;span class="nx"&gt;source_dir&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/../../../backend"&lt;/span&gt;
  &lt;span class="nx"&gt;output_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/../../build/function.zip"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform zips the &lt;code&gt;backend/&lt;/code&gt; folder at plan time. The Lambda resource then sets &lt;code&gt;source_code_hash = data.archive_file.this.output_base64sha256&lt;/code&gt;, which means the function only gets updated when the code actually changed. Edit a Terraform file and apply, the Lambda is untouched. Edit &lt;code&gt;routes/tasks.js&lt;/code&gt; and apply, Terraform notices the hash changed and ships the new zip. No separate deploy pipeline for the backend at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lambda function
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-${var.environment}-lambda-function"&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;"server.handler"&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;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;512&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;DB_HOST&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;db_endpoint&lt;/span&gt;
      &lt;span class="nx"&gt;DB_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;db_name&lt;/span&gt;
      &lt;span class="nx"&gt;DB_PORT&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;db_port&lt;/span&gt;
      &lt;span class="nx"&gt;DB_USER&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;username&lt;/span&gt;
      &lt;span class="nx"&gt;DB_PASSWORD&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;password&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;Node.js 22, 512 MB of memory, 30 second timeout. The database connection details arrive as environment variables, and look where they come from: &lt;code&gt;var.db_endpoint&lt;/code&gt; is the output of the database module. The environment folder wires them together like this:&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;"backend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"../../modules/backend"&lt;/span&gt;
  &lt;span class="nx"&gt;db_endpoint&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;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_endpoint&lt;/span&gt;
  &lt;span class="nx"&gt;db_name&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;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_name&lt;/span&gt;
  &lt;span class="nx"&gt;db_port&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;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_port&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 the trick that makes the whole project build in the right order. Terraform sees that the backend needs an output from the database, so it builds the database first. Nobody wrote &lt;code&gt;depends_on&lt;/code&gt; for that. The dependency graph falls out of the references themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eight API routes from one map
&lt;/h3&gt;

&lt;p&gt;The API has four paths (&lt;code&gt;/api&lt;/code&gt;, &lt;code&gt;/api/health&lt;/code&gt;, &lt;code&gt;/api/tasks&lt;/code&gt;, &lt;code&gt;/api/tasks/{id}&lt;/code&gt;) and eight method and path combinations once you count the OPTIONS methods needed for CORS preflight. Writing eight method blocks and eight integration blocks by hand would be sixteen nearly identical resources. Instead, the routes live in one &lt;code&gt;locals&lt;/code&gt; map:&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;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;health_get&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resource_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="nx"&gt;http_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;tasks_get&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resource_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="nx"&gt;http_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;tasks_post&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resource_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="nx"&gt;http_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;task_put&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resource_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task_by_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;http_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PUT"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;task_delete&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resource_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task_by_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;http_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DELETE"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# ...plus three OPTIONS entries&lt;/span&gt;
  &lt;span class="p"&gt;}&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_api_gateway_method"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;
  &lt;span class="nx"&gt;rest_api_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_rest_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;resource_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_id&lt;/span&gt;
  &lt;span class="nx"&gt;http_method&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http_method&lt;/span&gt;
  &lt;span class="nx"&gt;authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NONE"&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;for_each&lt;/code&gt; loops over the map and stamps out one resource per entry. The integrations do the same, every one an &lt;code&gt;AWS_PROXY&lt;/code&gt; integration pointing at the Lambda, which means API Gateway forwards the raw request and lets Express figure out the routing inside the function. Adding a new route later means adding one line to the map.&lt;/p&gt;

&lt;h3&gt;
  
  
  The silent API Gateway gotcha
&lt;/h3&gt;

&lt;p&gt;Here is a bug that costs beginners hours. API Gateway keeps a deployed snapshot of your API separate from its configuration. You can change methods and integrations in Terraform, apply successfully, and API Gateway will keep serving the old snapshot without a single complaint. Everything looks green. Your change is just not live.&lt;/p&gt;

&lt;p&gt;The fix is to force a redeploy whenever the routing changes:&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_api_gateway_deployment"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rest_api_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_api_gateway_rest_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;redeploy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sha1&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;aws_api_gateway_method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;aws_api_gateway_integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&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;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;create_before_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trigger is a hash of the routing config. Config changes, hash changes, Terraform creates a fresh deployment. The &lt;code&gt;create_before_destroy&lt;/code&gt; lifecycle rule makes sure the new deployment exists before the old one goes away, so the stage never has a gap where the API is down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: the frontend module
&lt;/h2&gt;

&lt;p&gt;The Amplify app connects directly to the GitHub repo using a personal access token, with the build instructions written inline:&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_amplify_app"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-${var.environment}-amplify-app"&lt;/span&gt;
  &lt;span class="nx"&gt;repository&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;repository&lt;/span&gt;
  &lt;span class="nx"&gt;access_token&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;github_access_token&lt;/span&gt;

  &lt;span class="nx"&gt;environment_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;VITE_API_URL&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;api_uri&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;That &lt;code&gt;VITE_API_URL&lt;/code&gt; line is my favorite detail in the project. The value comes from &lt;code&gt;module.backend.api_invoke_url&lt;/code&gt;, the output of the backend module. So Terraform creates the API, learns its URL, and hands it to Amplify as a build time environment variable. The React app bakes it in during &lt;code&gt;npm run build&lt;/code&gt;. The frontend learns its backend address from Terraform and nothing is ever hardcoded.&lt;/p&gt;

&lt;p&gt;The module also adds a rewrite rule so deep links like &lt;code&gt;/tasks/5&lt;/code&gt; fall back to &lt;code&gt;index.html&lt;/code&gt; instead of returning 404, which every single page app on Amplify needs, and a branch resource with auto build turned on. The branch stage is set with a conditional: &lt;code&gt;var.environment == "prod" ? "PRODUCTION" : "DEVELOPMENT"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: the backend code that makes Express work in Lambda
&lt;/h2&gt;

&lt;p&gt;A normal Express app ends with &lt;code&gt;app.listen(3000)&lt;/code&gt;. There is no port to listen on inside Lambda, so the app is wrapped instead:&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;serverlessExpress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vendia/serverless-express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// routes...&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serverlessExpress&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;app&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;@vendia/serverless-express&lt;/code&gt; package translates the event API Gateway sends into the request object Express expects, and translates the response back. Your Express code does not change at all.&lt;/p&gt;

&lt;p&gt;One more pattern worth stealing. The database table has to exist before the first query, but you do not want to create it on every request. The code caches the promise:&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;let&lt;/span&gt; &lt;span class="nx"&gt;dbReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dbReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dbReady&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;sequelize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&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;dbReady&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dbReady&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// don't cache a failed attempt&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;tasksRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first request in each Lambda container runs &lt;code&gt;sequelize.sync()&lt;/code&gt; which creates the &lt;code&gt;tasks&lt;/code&gt; table if needed. Every later request awaits the already resolved promise, which costs nothing. And if the sync fails, the cache resets so the next request tries again instead of being stuck with a failed promise forever. There is no manual schema step anywhere in the deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying it yourself
&lt;/h2&gt;

&lt;p&gt;You need an AWS account with credentials configured, Terraform 1.15 or newer, and a GitHub personal access token that can read your fork and create webhooks.&lt;/p&gt;

&lt;p&gt;First, bootstrap the state bucket, once per account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;infrastructure/bootstrap
terraform init
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, edit &lt;code&gt;backend.tf&lt;/code&gt; in each environment folder and set the bucket name to what the bootstrap printed. Backend blocks cannot take variables, so this one value is a literal.&lt;/p&gt;

&lt;p&gt;Third, deploy an environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;infrastructure/environments/dev
&lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars   &lt;span class="c"&gt;# fill in DB password, repo URL, token, branch&lt;/span&gt;
terraform init
terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RDS takes a few minutes to come up. The result is 28 resources from one command:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fftvjc3agl0tkxi7pyeyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fftvjc3agl0tkxi7pyeyf.png" alt="terraform apply output ending with Apply complete, 28 resources added" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fourth, trigger the first frontend build. Amplify only builds on new commits, so push something to the configured branch after the apply. An empty commit does the job.&lt;/p&gt;

&lt;p&gt;Finally, check it is alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://&amp;lt;api-id&amp;gt;.execute-api.ap-south-1.amazonaws.com/dev/api/health
&lt;span class="c"&gt;# {"status":"ok","database":"connected"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What actually gets created
&lt;/h2&gt;

&lt;p&gt;Everything below came from the same modules, applied once per environment folder. Three Lambda functions on Node.js 22:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fll53oflnhttymltcxgnu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fll53oflnhttymltcxgnu.png" alt="Lambda console showing the dev, staging and prod functions" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three regional REST APIs, each with a stage named after its environment:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonsrap1xpjolec43lsuw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fonsrap1xpjolec43lsuw.png" alt="API Gateway console showing the three REST APIs" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A MySQL instance per environment:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fns8s9ekye5lr0xq0lnc3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fns8s9ekye5lr0xq0lnc3.png" alt="RDS console showing the three MySQL instances" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the Amplify apps, connected to GitHub and building on every push:&lt;/p&gt;

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

&lt;p&gt;Notice what did not happen. Nobody opened the RDS console to click Create database three times. The staging environment is not "dev, but with some settings someone changed by hand last month." All three are provably identical because they came from identical code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade offs
&lt;/h2&gt;

&lt;p&gt;A few shortcuts were taken deliberately, and it is worth knowing what production would change. The open security group and public RDS would become private subnets with the Lambda inside the VPC. Database credentials would move from Lambda environment variables into Secrets Manager. The single AZ &lt;code&gt;db.t3.micro&lt;/code&gt; with &lt;code&gt;skip_final_snapshot&lt;/code&gt; would become Multi AZ with deletion protection. And the hardcoded bucket name in &lt;code&gt;backend.tf&lt;/code&gt; would be handled with partial backend config through &lt;code&gt;terraform init -backend-config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The roadmap includes a GitHub Actions pipeline that runs &lt;code&gt;terraform fmt&lt;/code&gt;, &lt;code&gt;validate&lt;/code&gt; and &lt;code&gt;plan&lt;/code&gt; on pull requests and applies on merge, plus tflint and checkov for static checks.&lt;/p&gt;

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

&lt;p&gt;The todo app is forgettable. The patterns are not. The bootstrap trick for remote state, S3 native locking without a DynamoDB table, modules whose outputs feed the next module's inputs so the build order takes care of itself, &lt;code&gt;for_each&lt;/code&gt; over a map instead of sixteen copy pasted resources, and a hash trigger that stops API Gateway from silently serving stale config. Every one of these shows up in real production Terraform, and this repo is a small enough place to learn them without drowning.&lt;/p&gt;

&lt;p&gt;Clone it, deploy dev, then run &lt;code&gt;terraform destroy&lt;/code&gt; when you are done exploring. RDS is the only always on cost here, and tearing it down is one command, the same way bringing it up was.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/TanseerS/serverless-web-application" rel="noopener noreferrer"&gt;github.com/TanseerS/serverless-web-application&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Contact
&lt;/h2&gt;

&lt;p&gt;Questions, feedback, or stuck on a step? Reach me at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;. Happy to help.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>s3</category>
    </item>
    <item>
      <title>Go Live in Minutes: Building Your First Live Stream with Amazon IVS</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Tue, 07 Jul 2026 05:08:24 +0000</pubDate>
      <link>https://dev.to/aws-builders/go-live-in-minutes-building-your-first-live-stream-with-amazon-ivs-5159</link>
      <guid>https://dev.to/aws-builders/go-live-in-minutes-building-your-first-live-stream-with-amazon-ivs-5159</guid>
      <description>&lt;h2&gt;
  
  
  The same tech that powers Twitch, minus the servers, minus the headache
&lt;/h2&gt;

&lt;p&gt;A few weeks ago I wanted to test a live streaming idea. My first thought was the usual one. I will need a media server, something like Wowza or a self hosted RTMP setup on EC2, then a CDN in front of it, then hours of tuning latency. Before I even opened the console I was already tired.&lt;/p&gt;

&lt;p&gt;Then I found Amazon IVS, and honestly, the whole thing took less time than making tea.&lt;/p&gt;

&lt;p&gt;If you are a developer who has never touched live streaming on AWS before, this post is for you. By the end of it you will know exactly what IVS is, why it is different from rolling your own setup, and how to go live in four steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What exactly is Amazon IVS
&lt;/h2&gt;

&lt;p&gt;Amazon Interactive Video Service, or IVS for short, is a fully managed live streaming service from AWS. Fully managed means AWS runs everything behind the scenes. You do not create servers, you do not install streaming software on the cloud, and you do not worry about scaling.&lt;/p&gt;

&lt;p&gt;Here is the part that made me sit up. IVS runs on the exact same infrastructure that powers Twitch. Twitch is one of the biggest live streaming platforms in the world, handling millions of concurrent viewers every single day. When you create an IVS channel, your tiny test stream rides on that same battle tested backbone.&lt;/p&gt;

&lt;p&gt;So instead of you building a smaller, buggier version of Twitch infrastructure, AWS just hands you Twitch infrastructure as an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The killer feature: ultra low latency
&lt;/h2&gt;

&lt;p&gt;If you have ever streamed with a typical setup, you know the pain. You wave at the camera, and viewers see the wave 15 to 30 seconds later. That delay is called latency, the time it takes for video to travel from your camera to the viewer's screen.&lt;/p&gt;

&lt;p&gt;IVS brings that down to under 3 seconds.&lt;/p&gt;

&lt;p&gt;Why does that matter? Because under 3 seconds, a stream stops feeling like a broadcast and starts feeling like a conversation. Viewers can react in chat and you can respond almost instantly.&lt;/p&gt;

&lt;p&gt;IVS also lets you sync interactive elements with the video. Think live chat, polls, quizzes, or even shopping buttons that appear exactly when the host says "buy this now." This is done through something called timed metadata, small pieces of data you inject into the stream that arrive on the viewer's screen in sync with the video frame. For a marketplace or live commerce product, this is gold.&lt;/p&gt;

&lt;h2&gt;
  
  
  How is it serverless
&lt;/h2&gt;

&lt;p&gt;The word serverless gets thrown around a lot, so let me be specific about what it means here.&lt;/p&gt;

&lt;p&gt;There are no servers for you to manage. No EC2 instances, no patching, no capacity planning. You create a channel and AWS handles the rest.&lt;/p&gt;

&lt;p&gt;It scales automatically. Whether 5 people show up to your stream or 5 million, you do nothing. The same channel handles both. There is no "upgrade your instance" moment at 2 AM when your stream suddenly goes viral.&lt;/p&gt;

&lt;p&gt;You pay only for what you use. IVS pricing is based on two things: hours of video you send into the service (input) and hours of video delivered to viewers (output). No stream running means no bill running. This is the same pay as you go model that makes Lambda and other serverless tools so friendly for side projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going live in 4 steps
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised me. Here is the entire setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a channel
&lt;/h3&gt;

&lt;p&gt;Log in to the AWS Console and search for Amazon IVS. Click Create channel, give it a name, and keep the default configuration for now. That is it. AWS provisions the channel in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Copy your Ingest Server and Stream Key
&lt;/h3&gt;

&lt;p&gt;Open your new channel and you will see two important values. The Ingest Server is the address your video gets sent to, something like rtmps://xxxxxx.global-contribute.live-video.net. The Stream Key is a secret token that proves the stream belongs to you. Treat the stream key like a password. Anyone who has it can broadcast on your channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Paste them into OBS Studio
&lt;/h3&gt;

&lt;p&gt;OBS Studio is free, open source broadcasting software and the de facto standard for streaming. Download it, open Settings, go to Stream, and choose Custom as the service. Paste the Ingest Server into the Server field and the Stream Key into the Stream Key field. Save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Click Start Streaming
&lt;/h3&gt;

&lt;p&gt;Back on the main OBS screen, add a source (your webcam or screen), then hit Start Streaming. Switch to the AWS Console, open your channel, and you will see yourself live in the built in preview player within seconds.&lt;/p&gt;

&lt;p&gt;Four steps. No infrastructure. You are now running on Twitch grade streaming systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;The console preview is just the beginning. IVS gives you a playback URL for every channel, and AWS provides a player SDK for web, iOS, and Android. That means you can drop your live stream into your own React app or mobile app with a few lines of code. Add the IVS chat feature and timed metadata, and you have a real interactive streaming product without touching a single server.&lt;/p&gt;

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

&lt;p&gt;Live streaming used to be a project measured in weeks. With Amazon IVS it is a project measured in minutes. You get the infrastructure behind Twitch, latency under 3 seconds, automatic scaling from a handful of viewers to millions, and a bill that only grows when your stream does. If you have been putting off a live video feature because the plumbing felt scary, this is your sign to create a channel and press Start Streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's connect
&lt;/h2&gt;

&lt;p&gt;If you tried this and got stuck somewhere, or if you built something cool with IVS, I would love to hear about it. Reach out at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>ivs</category>
    </item>
    <item>
      <title>Terraform guide to IAM Policies: The Two Cases You Need to Know</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Wed, 01 Jul 2026 11:21:21 +0000</pubDate>
      <link>https://dev.to/tanseer/terraform-guide-to-iam-policies-the-two-cases-you-need-to-know-j0b</link>
      <guid>https://dev.to/tanseer/terraform-guide-to-iam-policies-the-two-cases-you-need-to-know-j0b</guid>
      <description>&lt;h2&gt;
  
  
  When to attach a ready made policy and when to write your own
&lt;/h2&gt;

&lt;p&gt;When you first start writing IAM setup in Terraform, the examples online can feel like they contradict each other. One tutorial writes a long block called &lt;code&gt;aws_iam_policy_document&lt;/code&gt;. Another just pastes a single line with a long string inside it. Both of them work, so which one is correct?&lt;/p&gt;

&lt;p&gt;The answer is that both are correct. They simply solve two different problems. Once you can tell which situation you are in, IAM in Terraform stops feeling random and starts feeling obvious.&lt;/p&gt;

&lt;p&gt;Before we get into the two cases, here are the words you will keep seeing:&lt;/p&gt;

&lt;p&gt;IAM stands for Identity and Access Management. It is the AWS service that decides who is allowed to do what. An IAM role is an identity that an AWS service (like a Lambda function) takes on so it can act with certain permissions. An IAM policy is the document that lists which actions are allowed or denied. An ARN, short for Amazon Resource Name, is the unique address of a resource in AWS.&lt;/p&gt;

&lt;p&gt;With that out of the way, let us look at the two cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: AWS Managed Policy (attach directly)
&lt;/h2&gt;

&lt;p&gt;A managed policy is a policy that AWS already wrote and maintains for you. If AWS already provides the exact permissions you need, you do not create your own &lt;code&gt;aws_iam_policy_document&lt;/code&gt;. You just point your role at the policy AWS already made.&lt;/p&gt;

&lt;p&gt;For example, giving a Lambda function permission to write logs is a very common need, and AWS has a ready made policy for it:&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_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"logs"&lt;/span&gt; &lt;span class="p"&gt;{&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another common one is letting a Lambda function run inside a VPC (a Virtual Private Cloud, which is your own private network in AWS):&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_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what is happening here. You are not writing any permissions yourself. You are taking an existing AWS managed policy, referring to it by its ARN, and attaching it to your role. That is the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: Your own custom permissions
&lt;/h2&gt;

&lt;p&gt;Sometimes AWS does not have a managed policy that matches exactly what you need. Maybe you want a Lambda function to read and write files in one specific S3 bucket and nothing else. In that case you build the policy yourself, and it takes three small steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the policy document
&lt;/h3&gt;

&lt;p&gt;First you describe the permissions. This &lt;code&gt;aws_iam_policy_document&lt;/code&gt; is a data source, which means it does not create anything in AWS on its own. It is a way to write your permission rules in clean Terraform instead of raw JSON.&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;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"s3_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&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="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"${aws_s3_bucket.uploads.arn}/*"&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;Reading this in plain English: allow the actions of getting an object and putting an object, and only on the uploads bucket and everything inside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create the IAM policy
&lt;/h3&gt;

&lt;p&gt;Now you turn that document into a real IAM policy that exists in AWS. The &lt;code&gt;policy&lt;/code&gt; field pulls in the JSON produced by the document from Step 1.&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_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"s3_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-${var.environment}-s3-access"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&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;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_access&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Attach it
&lt;/h3&gt;

&lt;p&gt;Finally you attach the policy you just created to your role. This looks almost the same as Case 1, but the ARN now comes from your own policy instead of an AWS managed one.&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_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"s3_access"&lt;/span&gt; &lt;span class="p"&gt;{&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;The easiest way to remember the difference is this. In Case 1 the policy already exists, so you skip straight to attaching. In Case 2 you have to build the policy first, then attach it.&lt;/p&gt;

&lt;p&gt;The custom flow always moves in the same direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Policy Document
      │
      ▼
IAM Policy
      │
      ▼
Attach to Role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You write the document, the document becomes a policy, and the policy gets attached to the role. If you ever feel lost, just ask yourself one question: does AWS already have a policy for this? If yes, attach it directly. If no, build your own with the three steps above.&lt;/p&gt;

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

&lt;p&gt;IAM in Terraform is not as confusing as it first looks. There are really only two situations. When AWS already provides the exact permissions, you attach a managed policy by its ARN in a single resource. When you need something specific that AWS does not offer, you create a policy document, turn it into an IAM policy, and attach that.&lt;/p&gt;

&lt;p&gt;Keep that fork in mind and you will always know which approach the moment fits, instead of copying an example and hoping it is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let us connect
&lt;/h2&gt;

&lt;p&gt;If you have questions or want to share how you handle IAM in your own projects, reach out at &lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;. I am always happy to talk Terraform and AWS.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Lambda MicroVMs Are Kind of Insane: Tiny Virtual Machines That Boot in Milliseconds and Sleep When You Are Not Looking</title>
      <dc:creator>Tanseer</dc:creator>
      <pubDate>Tue, 30 Jun 2026 04:53:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/lambda-microvms-are-kind-of-insane-tiny-virtual-machines-that-boot-in-milliseconds-and-sleep-when-4b93</link>
      <guid>https://dev.to/aws-builders/lambda-microvms-are-kind-of-insane-tiny-virtual-machines-that-boot-in-milliseconds-and-sleep-when-4b93</guid>
      <description>&lt;p&gt;&lt;em&gt;A beginner friendly guide to the Firecracker powered microVMs behind AWS Lambda, the new suspend and resume superpower, and the big question everyone keeps asking: is this still serverless?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Imagine a virtual computer so small and so fast that it starts up in less time than it takes you to blink. Now imagine thousands of them running on a single physical server, each one completely walled off from its neighbours, and each one quietly going to sleep the moment it has nothing to do, so that you stop paying for it.&lt;/p&gt;

&lt;p&gt;That is not a science fiction pitch. That is roughly what is happening inside AWS Lambda today, and the newer microVM capabilities push the idea even further.&lt;/p&gt;

&lt;p&gt;In this post we will start from zero. We will explain what a microVM even is, meet the tiny engine that makes the whole thing possible, walk through what happens when a single request comes in, and then look at the features that make people say this is a little bit crazy. By the end you will be able to answer the question that is dividing the cloud community: after all of this, is Lambda even serverless anymore?&lt;/p&gt;

&lt;p&gt;Let us get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Exactly Is a Lambda MicroVM?
&lt;/h2&gt;

&lt;p&gt;A microVM is a tiny, lightweight virtual machine. It behaves like its own small computer with a strong security wall around it, but it throws away almost all the heavy baggage of a normal machine, so it starts in milliseconds and uses very little memory. Picture a tiny, fast, single purpose box that appears, does one job, and disappears.&lt;/p&gt;

&lt;p&gt;Here is the part that surprises most people. Lambda has always run your code inside these microVMs. Every time your function runs, AWS quietly spins up a tiny isolated machine just for you. The recent excitement is about giving those microVMs new powers, such as the ability to pause when idle and to be reached directly over the internet. So when people say "Lambda MicroVMs," they are really talking about this next chapter of an idea that has been humming away under the hood for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Firecracker, the Tiny Engine Doing All the Work
&lt;/h2&gt;

&lt;p&gt;Powering all of this is Firecracker, a technology that AWS built and then released to the public as open source. AWS created it specifically to run microVMs safely at massive scale, and it is the reason a tiny machine can boot in around 125 milliseconds while still keeping the strong security wall of a real virtual machine. This same AWS made engine quietly runs underneath both Lambda and AWS Fargate.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How It Works: The Journey of a Single Request
&lt;/h2&gt;

&lt;p&gt;Let us follow one request from start to finish so the architecture clicks.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You upload your code.&lt;/strong&gt; This can be a simple zip file or a container image. You also choose how much memory the function gets, which in turn decides how much CPU power it receives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something triggers it.&lt;/strong&gt; A user hits an endpoint, a file lands in storage, a message arrives on a queue, and Lambda decides your function needs to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A microVM appears.&lt;/strong&gt; AWS spins up a Firecracker microVM with your chosen runtime and your code inside. This little box is called the &lt;strong&gt;execution environment&lt;/strong&gt;. If a fresh one has to be created from scratch, that small delay is known as a &lt;strong&gt;cold start&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your code runs.&lt;/strong&gt; The environment initialises, your handler runs, and a response goes back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The environment is kept around.&lt;/strong&gt; Instead of throwing the microVM away immediately, Lambda often keeps it ready for a little while. If another request arrives soon, it reuses the same warm environment and skips the startup cost entirely. This is called a &lt;strong&gt;warm start&lt;/strong&gt;, and it is why the second request to a function usually feels much faster than the first.
So the architecture is really a fleet of these tiny machines being created, reused, and recycled constantly, all managed for you so you never touch a server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Each One Gets Its Own Front Door: Dedicated HTTP Endpoints
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;endpoint&lt;/strong&gt; is just a web address that something can send a request to. &lt;strong&gt;HTTP&lt;/strong&gt; is the everyday language of the web that browsers and apps use to talk to servers.&lt;/p&gt;

&lt;p&gt;For a long time, if you wanted your Lambda function to be reachable over the web, you usually had to put another service in front of it to handle the incoming traffic. The newer approach gives a function its own dedicated HTTPS endpoint, a clean and direct web address that points straight at it. No extra plumbing in between.&lt;/p&gt;

&lt;p&gt;The address looks something 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;https://your-unique-id.lambda-url.your-region.on.aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can keep it open or lock it down so only authorised callers get through. For simple use cases, such as a webhook that some other service calls, or a small backend for a single page app, this is wonderfully direct. Your request lands on a private little microVM that exists just to answer you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Magic: Suspend and Resume
&lt;/h2&gt;

&lt;p&gt;This is the feature that earns the word "crazy."&lt;/p&gt;

&lt;p&gt;To understand it, you need one more idea: a &lt;strong&gt;snapshot&lt;/strong&gt;. A snapshot is a complete saved picture of a running machine at a single moment in time, including everything sitting in its memory. If you take a snapshot and then restore it later, the machine wakes up exactly where it left off, as if no time had passed at all.&lt;/p&gt;

&lt;p&gt;Firecracker can do this with microVMs. It can freeze a running microVM, save its full state, and bring it back to life almost instantly.&lt;/p&gt;

&lt;p&gt;Now think about what that unlocks. Traditionally you had two bad choices. Either you kept a server running all the time so it was ready to respond instantly, and you paid for every idle second. Or you shut it down to save money and then suffered a slow start whenever traffic came back.&lt;/p&gt;

&lt;p&gt;Suspend and resume gives you a third option. The microVM can stay paused while nothing is happening, and the headline benefit is simple: &lt;strong&gt;you are not paying for a fully running machine while it sits idle.&lt;/strong&gt; When a request finally arrives, it wakes up quickly and carries on. You get the responsiveness of an always on server with a cost profile much closer to pay only for what you use.&lt;/p&gt;

&lt;p&gt;That is the holy grail people have chased for years. Be instantly ready, but only pay when you are actually doing work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lambda MicroVM vs EC2 vs Classic Lambda
&lt;/h2&gt;

&lt;p&gt;It helps to see where this sits between the two options most people already know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EC2&lt;/strong&gt; is a rented virtual server that you control and manage yourself. &lt;strong&gt;Classic Lambda&lt;/strong&gt; is short lived, event driven functions with a hard time limit. The microVM approach lands in the middle, borrowing the best of each.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;EC2&lt;/th&gt;
&lt;th&gt;Classic Lambda&lt;/th&gt;
&lt;th&gt;Lambda MicroVM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What you manage&lt;/td&gt;
&lt;td&gt;The whole server&lt;/td&gt;
&lt;td&gt;Just your code&lt;/td&gt;
&lt;td&gt;Just your code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How long it can run&lt;/td&gt;
&lt;td&gt;As long as you like&lt;/td&gt;
&lt;td&gt;Up to 15 minutes per run&lt;/td&gt;
&lt;td&gt;Designed for longer and more stateful work than the 15 minute limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup speed&lt;/td&gt;
&lt;td&gt;Slow, minutes&lt;/td&gt;
&lt;td&gt;Fast, with occasional cold starts&lt;/td&gt;
&lt;td&gt;Fast, with quick resume from a paused state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost when idle&lt;/td&gt;
&lt;td&gt;You keep paying while it runs&lt;/td&gt;
&lt;td&gt;Nothing, it is not running&lt;/td&gt;
&lt;td&gt;The big win is not paying for a fully running machine while idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;You set it up&lt;/td&gt;
&lt;td&gt;Automatic, scales to zero&lt;/td&gt;
&lt;td&gt;Automatic, scales to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best feeling&lt;/td&gt;
&lt;td&gt;Total control&lt;/td&gt;
&lt;td&gt;Pure simplicity&lt;/td&gt;
&lt;td&gt;Control and simplicity together&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The short version is this. EC2 gives you full control but you babysit it and pay for idle time. Classic Lambda is beautifully simple but tightly constrained. The microVM model tries to give you longer running, more flexible compute that still scales down to nothing when no one is using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is This Actually Good For?
&lt;/h2&gt;

&lt;p&gt;A new tool is only exciting if it solves real problems. Here is where this approach shines.&lt;/p&gt;

&lt;p&gt;It is great for &lt;strong&gt;long running jobs&lt;/strong&gt; that do not fit neatly into a short window, such as heavier data processing or video work that would bump into the classic 15 minute ceiling.&lt;/p&gt;

&lt;p&gt;It suits &lt;strong&gt;stateful sessions&lt;/strong&gt;, meaning work where the machine needs to remember something between requests rather than starting fresh every single time.&lt;/p&gt;

&lt;p&gt;It is a natural fit for &lt;strong&gt;AI and machine learning inference&lt;/strong&gt;, where a large model can be expensive to load. You load it once, let the machine pause when traffic is quiet, and resume instantly when the next request comes, instead of paying to keep a big GPU style box running all day.&lt;/p&gt;

&lt;p&gt;It works beautifully for &lt;strong&gt;interactive sandboxes and code execution&lt;/strong&gt;, the kind of thing where each user needs their own safe, isolated little environment that spins up fast and tears down cleanly.&lt;/p&gt;

&lt;p&gt;And it is useful for &lt;strong&gt;bursty traffic&lt;/strong&gt;, where requests come in unpredictable spikes. You get instant readiness during the spike and near zero cost during the long quiet stretches in between.&lt;/p&gt;

&lt;p&gt;When would you not bother? If your workload is genuinely tiny and event driven, classic Lambda is still the simplest possible answer. And if you need deep control over the operating system, custom networking, or hardware that must run nonstop, a traditional server like EC2 is still the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About the Price?
&lt;/h2&gt;

&lt;p&gt;Cost is usually the deciding factor, so let us compare the three billing styles in plain terms. To follow along, you need one bit of vocabulary: a &lt;strong&gt;GB second&lt;/strong&gt; is just one gigabyte of memory used for one second. It is the unit Lambda uses to measure how much work you actually consumed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EC2&lt;/strong&gt; charges you for the time the server is switched on, whether or not anyone is using it. A box running quietly overnight with zero visitors still appears on your bill. You pay for capacity, not for usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Classic Lambda&lt;/strong&gt; charges you in two parts: a tiny amount for each request, plus an amount for compute measured in those GB seconds. If nobody calls your function, it costs you nothing. You pay for usage, not for capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The microVM model&lt;/strong&gt; aims for the best of both. You get a more capable, longer lived machine, but the key promise is that you are not charged for a fully running VM while it is paused and idle. You pay mainly for the active time when it is actually doing something.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Important: cloud pricing changes often, and the exact rates for these options vary by region and configuration. Rather than trust any single number you read in a blog, including this one, always check the current AWS pricing pages before you make a decision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  So Is It Still Serverless?
&lt;/h2&gt;

&lt;p&gt;Here is the fun part, and it is genuinely a matter of debate.&lt;/p&gt;

&lt;p&gt;The word &lt;strong&gt;serverless&lt;/strong&gt; never meant there were no servers. It meant you, the developer, never had to think about them. The classic definition came with a few expectations: your code is event driven and short lived, it scales automatically all the way down to zero, you never manage infrastructure, and you only pay for what you use.&lt;/p&gt;

&lt;p&gt;Now look at what these microVMs offer. Long running. Able to hold state. Reachable through their own dedicated web address. Sitting paused and ready rather than vanishing completely. To some people that starts to look an awful lot like a small, managed server wearing a serverless costume.&lt;/p&gt;

&lt;p&gt;The case for "yes, it is still serverless" is strong. You still never patch an operating system. It still scales to zero. You still pay for value rather than for idle capacity. By the spirit of the original promise, nothing has been broken.&lt;/p&gt;

&lt;p&gt;The case for "no, the purity is gone" is also fair. The original idea of a tiny, stateless, event triggered function that lives for a moment and dies has clearly been stretched into something bigger and more persistent.&lt;/p&gt;

&lt;p&gt;The honest answer is probably this. Serverless is becoming a spectrum rather than a strict box. On one end you have pure functions that flash in and out of existence. On the other you have long lived managed machines. These microVMs live somewhere in the comfortable middle, and they are quietly redefining what the word can mean. Maybe the better question is not "is this serverless," but "does this let me ship without babysitting infrastructure." If the answer is yes, the label matters a lot less than the freedom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Lambda has always been powered by tiny, fast, isolated microVMs running on Firecracker. What is changing is how clever those microVMs are getting. They can now boot in a blink, answer the web directly, and most remarkably of all, doze off when idle so you are not paying for a machine that is doing nothing.&lt;/p&gt;

&lt;p&gt;Whether you call it serverless, managed compute, or something brand new, one thing is clear. The line between a server and a function is getting blurrier, and that is a great problem for developers to have. The more the cloud handles for us, the more time we get to spend building the things we actually care about.&lt;/p&gt;

&lt;p&gt;If you are new to AWS, the best next step is to try spinning up a simple function, give it a dedicated endpoint, and call it from your browser. Seeing your own tiny microVM answer back is the moment the whole idea finally clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let Us Connect
&lt;/h2&gt;

&lt;p&gt;If you found this helpful, have a question, or spot something you want to discuss, I would love to hear from you. Reach out at &lt;strong&gt;&lt;a href="mailto:khantanseer43@gmail.com"&gt;khantanseer43@gmail.com&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>lambda</category>
      <category>serverless</category>
      <category>stateful</category>
    </item>
  </channel>
</rss>
