<?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: Zied Ben Tahar</title>
    <description>The latest articles on DEV Community by Zied Ben Tahar (@zied).</description>
    <link>https://dev.to/zied</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F856077%2F99a2a3de-32df-4deb-b605-bbd90d38139d.jpeg</url>
      <title>DEV Community: Zied Ben Tahar</title>
      <link>https://dev.to/zied</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zied"/>
    <language>en</language>
    <item>
      <title>Serverless RAG Chat with AppSync Events and Bedrock Knowledge Bases</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Fri, 09 May 2025 14:44:23 +0000</pubDate>
      <link>https://dev.to/aws-builders/serverless-rag-chat-with-appsync-events-and-bedrock-knowledge-bases-4kjl</link>
      <guid>https://dev.to/aws-builders/serverless-rag-chat-with-appsync-events-and-bedrock-knowledge-bases-4kjl</guid>
      <description>&lt;p&gt;When it comes to building serverless WebSocket APIs on AWS, there’s no shortage of options: API Gateway, IoT Core, AppSync GraphQL subscriptions, and now AppSync Events. Each option comes with its own level of control and complexity. I’ve found that AppSync Events to be simplest to work with.&lt;/p&gt;

&lt;p&gt;One of the interesting features of AppSync Events is its data sources capability. It lets you directly integrate to resources like DynamoDB, OpenSearch, Bedrock and Lambda. You can interact with these data sources using AppSyncJS (appsync’s own flavor of javascript). But to be totally fair, I lean toward direct lambda integration as it gives more control and makes the development and testing workflow more familiar, standard and manageable. &lt;/p&gt;

&lt;p&gt;Currently, Bedrock data source supports only the InvokeModel and Converse APIs. So, if you want to integrate with Knowledge Bases, the viable approach is to create a custom data source using Lambda.&lt;/p&gt;

&lt;p&gt;And that’s exactly what this blog post is about, we’ll walk through how to build this RAG-based chat application with AppSync Events and bedrock knowledge based using nodejs, TypeScript and Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;Let’s take a look at how the whole setup fits together:&lt;/p&gt;

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

&lt;p&gt;The Knowledge Base is configured to use PostgreSQL as its vector store where we store the embeddings as well as the associated metadata of the documents we want to index. Using Postgres gives us control over the schema, the indexing strategy, and embedding format, all of which come in handy when fine-tuning a vector-based RAG setup.&lt;/p&gt;

&lt;p&gt;We’ve got  the &lt;code&gt;handleAppSyncEvents&lt;/code&gt; function directly integrated as a data source for the AppSync Events API. Its role is to process incoming events from AppSync and to invoke the &lt;code&gt;retrieveAndGenerate&lt;/code&gt; from the Knowledge Base. This function is configured to be asynchronous (with the &lt;code&gt;EVENT&lt;/code&gt; invocation type), which means AppSync doesn't wait for the function to complete before returning a response to the client. Once we receive a result from bedrock this function publishes a response back to the client’s response channel.&lt;/p&gt;

&lt;p&gt;AppSync Events supports multiple authorization methods to secure Event APIs, including API keys, Lambda authorizers, IAM, OpenID Connect, and Amazon Cognito user pools. In this setup, I’m using both Cognito user pools and IAM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web clients use cognito for authentication&lt;/li&gt;
&lt;li&gt;And I chose IAM over using API key for publishing events from the &lt;code&gt;handleAppSyncEvents&lt;/code&gt; function to AppSync, as it offers better security posture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I appreciate in this setup: AppSync Events supports Web ACLs. That means you can easily layer in protections like rate limiting and IP filtering. It’s a nice edge over API Gateway WebSockets, which still doesn’t offer native WAF support.&lt;/p&gt;

&lt;p&gt;And tying it all together, the browser connects via WebSocket to AppSync, giving us a real-time, bidirectional channel, ideal for sending the models responses back to users in conversational interfaces.&lt;/p&gt;

&lt;p&gt;Let’s dive into the details of the solution; but if you’d like to jump straight to the complete implementation, you can find it here 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ziedbentahar/rag-chat-with-appsync-events-and-bedrock-knowledge-bases" rel="noopener noreferrer"&gt;https://github.com/ziedbentahar/rag-chat-with-appsync-events-and-bedrock-knowledge-bases&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the knowledge base
&lt;/h2&gt;

&lt;p&gt;Let’s first take a look at how we can use Aurora PostgreSQL as a vector store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the vector store on Postgres
&lt;/h3&gt;

&lt;p&gt;When using PostgreSQL as a vector store, Knowledge Base requires an Aurora Serverless cluster with the Data API enabled. The database must include a vector table with specific columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An embedding column to store the vector representation of the content,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A chunk column for the actual text tied to each embedding,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And a metadata column that holds references, which are useful for pointing back to the original source during retrieval.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Knowledge Base keeps this table up to date automatically whenever content is synced from the source bucket.&lt;/p&gt;

&lt;p&gt;Since I like to keep everything automated, I trigger the db init script right after the database cluster is created. This script sets up everything we need: a role, schema, table, and indexes; all in one go, wrapped in a single transaction. It’s run by a function once the cluster is deployed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&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;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bedrockKnowledgeBaseCreds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock_user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generatePostgresPassword&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;knowledge_base&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;vectorTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bedrock_kb&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;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE EXTENSION IF NOT EXISTS vector`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE SCHEMA IF NOT EXISTS &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE ROLE &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bedrockKnowledgeBaseCreds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; WITH PASSWORD '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bedrockKnowledgeBaseCreds&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="s2"&gt;' LOGIN`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`GRANT ALL ON SCHEMA &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bedrockKnowledgeBaseCreds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE TABLE IF NOT EXISTS &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vectorTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (id uuid PRIMARY KEY, embedding vector(1024), chunks text, metadata json, custom_metadata jsonb)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE INDEX IF NOT EXISTS bedrock_kb_embedding_idx ON &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vectorTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; USING hnsw (embedding vector_cosine_ops) WITH (ef_construction=256)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE INDEX IF NOT EXISTS bedrock_kb_chunks_fts_idx ON &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vectorTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; USING gin (to_tsvector('simple', chunks))`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`CREATE INDEX IF NOT EXISTS bedrock_kb_custom_metadata_idx ON &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vectorTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; USING gin (custom_metadata)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; TO &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bedrockKnowledgeBaseCreds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;executeTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;databaseArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;databaseSecretArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;databaseName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bedrockKnowledgeBaseCreds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;executeTransaction&lt;/code&gt; leverages Aurora’s data api in order to execute these statements. In this function, I also set the secret containing a dedicated database user and password, which will later be used when setting up the data source for the Knowledge Base.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating The knowledge base
&lt;/h3&gt;

&lt;p&gt;Quite straightforward in terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;    &lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_knowledge_base"&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;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-kb"&lt;/span&gt;
      &lt;span class="nx"&gt;role_arn&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;kb_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

      &lt;span class="nx"&gt;knowledge_base_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;vector_knowledge_base_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;embedding_model_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedding_model_arn&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;"VECTOR"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;storage_configuration&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;"RDS"&lt;/span&gt;
        &lt;span class="nx"&gt;rds_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;credentials_secret_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_creds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
          &lt;span class="nx"&gt;database_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_rds_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database_name&lt;/span&gt;
          &lt;span class="nx"&gt;resource_arn&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_rds_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
          &lt;span class="nx"&gt;table_name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_schema&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vector_table&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;field_mapping&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;primary_key_field&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"id"&lt;/span&gt;
            &lt;span class="nx"&gt;metadata_field&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"metadata"&lt;/span&gt;
            &lt;span class="nx"&gt;text_field&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"chunks"&lt;/span&gt;
            &lt;span class="nx"&gt;vector_field&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"embedding"&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;depends_on&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_rds_cluster_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_cluster_instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;aws_lambda_invocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seed_db_function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_creds&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_data_source"&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;knowledge_base_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_knowledge_base&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;name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"kb_datasource"&lt;/span&gt;

      &lt;span class="nx"&gt;vector_ingestion_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;chunking_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;chunking_strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FIXED_SIZE"&lt;/span&gt;
          &lt;span class="nx"&gt;fixed_size_chunking_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;max_tokens&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
            &lt;span class="nx"&gt;overlap_percentage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&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;data_source_configuration&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;"S3"&lt;/span&gt;
        &lt;span class="nx"&gt;s3_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

          &lt;span class="nx"&gt;bucket_arn&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;kb_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
          &lt;span class="nx"&gt;inclusion_prefixes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_folder&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When configuring RDS as a data source there are a few key parameters you’ll need to provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The vector table and the field mappings that define which columns in the table should be used by the Knowledge Base.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The IAM role associated with the Knowledge Base must have the following permissions on the RDS cluster: rds-data:ExecuteStatement, rds-data:BatchExecuteStatement and rds:DescribeDbClusters&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Secret arn that holds the database credentials that we created in the previous step.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the knowledge base is deployed, here’s what it looks like in the console:&lt;/p&gt;

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

&lt;p&gt;We can already start testing it right from the console. In this example, I synced the knowledge base with a dataset containing texts about the Roman Empire.&lt;/p&gt;

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

&lt;p&gt;Alights, let’s see how to setup the AppSync Events integration&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up AppSync integration
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, I’ll be using Cognito User Pools as the default auth mode as well as IAM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AppSync will handle validating both subscribe and publish requests from clients, as long as they provide a valid Cognito token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IAM auth will be used &lt;strong&gt;handleAppSyncEvent&lt;/strong&gt; function as needs to publish responses back to the clients&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;    &lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"awscc_appsync_api"&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;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-events-api"&lt;/span&gt;
      &lt;span class="nx"&gt;owner_contact&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;event_config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;auth_providers&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;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AMAZON_COGNITO_USER_POOLS"&lt;/span&gt;
            &lt;span class="nx"&gt;cognito_config&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_region&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="nx"&gt;user_pool_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_pool_id&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="nx"&gt;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS_IAM"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;connection_auth_modes&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;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AMAZON_COGNITO_USER_POOLS"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;default_publish_auth_modes&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;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AMAZON_COGNITO_USER_POOLS"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;default_subscribe_auth_modes&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;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AMAZON_COGNITO_USER_POOLS"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up, I’ll need to create a data source and associated to &lt;code&gt;handleAppSyncEvents&lt;/code&gt; function. Unfortunately, this part isn’t supported in the terraform provider &lt;em&gt;yet&lt;/em&gt;, so for now, I’m using the SDK to create these resources in a function hat runs once right after the AppSync Events API resource is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&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;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nl"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;dataSourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;lambdaFunctionArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;serviceRoleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiId&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSourceName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambdaFunctionArn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceRoleArn&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelName&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SourceArn, TargetArn, RoleArn and channel name are required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppSyncClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_REGION&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createDataSourceCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateDataSourceCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSourceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS_LAMBDA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;serviceRoleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceRoleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;lambdaConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;lambdaFunctionArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambdaFunctionArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createDataSourceCommand&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;createChannelCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateChannelNamespaceCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;subscribeAuthModes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AMAZON_COGNITO_USER_POOLS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;publishAuthModes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AMAZON_COGNITO_USER_POOLS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS_IAM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;handlerConfigs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;onPublish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DIRECT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="na"&gt;dataSourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSourceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="na"&gt;lambdaConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="na"&gt;invokeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EVENT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="na"&gt;onSubscribe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DIRECT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="na"&gt;dataSourceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataSourceName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="na"&gt;lambdaConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                &lt;span class="na"&gt;invokeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EVENT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createChannelCommand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// ... handle resource update and deletion&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the direct integration of the default channel with the function. The &lt;code&gt;handleAppSyncEvents&lt;/code&gt; function is invoked directly in &lt;code&gt;EVENT&lt;/code&gt; invocation mode.&lt;/p&gt;

&lt;p&gt;Here how it looks in the console once the AppSync resources are created:&lt;/p&gt;

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

&lt;p&gt;With the Lambda function as a data source defined in this screen&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Using Lambda Powertools to handle AppSync realtime events
&lt;/h3&gt;

&lt;p&gt;Now let’s get to the interesting part: Handling events from AppSync and putting the knowledge base to work. Lambda Powertools offers a handy utility that makes it easier to integrate Lambda functions with AppSync events. It allows defining clear, dedicated handler methods for publish/subscribe interactions, so less messy if-else blocks. Routing is handled automatically based on namespaces and channel patterns, keeping the code clean and easy to maintain. &lt;/p&gt;

&lt;p&gt;Here’s how it works in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting things up and handing subscription&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We start by initializing the resolver using the Lambda Powertools Event Handler utility, then define a handler for new subscriptions with &lt;code&gt;app.onSubscribe&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppSyncEventsResolver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UnauthorizedException&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-lambda-powertools/event-handler/appsync-events&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppSyncEventsResolver&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;onSubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/chat/responses/*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You cannot subscribe to this channel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, I ensure that users can only subscribe to their own chat responses channel. This check prevents accidental or unauthorized access to other users’ channels. Since we’re using a Cognito User Pool, the subscription payload contains decoded user information, including the &lt;code&gt;sub&lt;/code&gt; (user ID) and &lt;code&gt;username&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validating and handling messages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s now see how we handle messages from users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&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;onPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/chat/request/*&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;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You cannot publish to this channel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messageSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I don't understand what you mean, your message format seems invalid&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid message payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bedrockClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RetrieveAndGenerateCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;retrieveAndGenerateConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;KNOWLEDGE_BASE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;knowledgeBaseConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;knowledgeBaseId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KB_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;modelArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KB_MODEL_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;span class="nx"&gt;sessionId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signedRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EVENTS_API_DNS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/event`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/chat/responses/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;EVENTS_API_DNS&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/event`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;processed&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with the subscribe handler, when a message is published to the &lt;code&gt;/chat/request/{userId}&lt;/code&gt; channel &lt;code&gt;app.onPublish&lt;/code&gt; gets invoked, I extract the user’s identity from the event and enforce that they can only publish to their own channel. We then validate the payload using zod to ensure it has the expected structure. If validation succeeds we then call bedrock &lt;code&gt;RetrieveAndGenerate&lt;/code&gt; endpoint. &lt;/p&gt;

&lt;p&gt;As I am using IAM auth to let the function publish the response back to the client via &lt;code&gt;chat/responses/{userId}&lt;/code&gt; channel. I need to sign the request with sigv4 and pass the signed request headers when calling AppSync event endpoint. This function needs to have &lt;code&gt;appsync:EventPublish&lt;/code&gt; permission on the api channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;☝️ Some notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In this solution we waits for the full response from the model, we’re not using bedrock’s response streaming feature. I could have used the &lt;code&gt;RetrieveAndGenerateStream&lt;/code&gt; API and then send chunks by publishing incrementally each chunk to the client. However, depending on the model’s response, this could lead to multiple calls to the Events API potentially increasing costs (since each call counts as a separate operation). One possible solution would be to buffer the response chunks and send them in batches, striking a better balance between responsiveness and cost. Handling lambda response streaming natively is a feature I’d love to see supported by AppSync in the future.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;RetrieveAndGenerate&lt;/code&gt; endpoint returns a sessionId that we’ll need to reuse in messages within the same conversation. This sessionId  is what allows Amazon Bedrock to maintain context. We simply return that sessionId along with the result, and include it in every subsequent chat message to keep the conversation context-aware.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can check out the Cognito user pool terraform resource definition at the following &lt;a href="https://github.com/ziedbentahar/rag-chat-with-appsync-events-and-bedrock-knowledge-bases/blob/main/infra/modules/auth/user-pool.tf" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the complete function code following &lt;a href="https://github.com/ziedbentahar/rag-chat-with-appsync-events-and-bedrock-knowledge-bases/blob/main/src/chat/lambda-handlers/handle-appsync-events.ts" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Client
&lt;/h2&gt;

&lt;p&gt;Here, I’m building a small React client to chat with the Knowledge Base via AppSync. I’m using Amplify because it makes it easy to connect to the AppSync API and handle authentication through the user pool to retrieve an access token.&lt;/p&gt;

&lt;p&gt;First, we need to configure Amplify by providing the AppSync Events API endpoint along with the Cognito User Pool and Identity Pool ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="nx"&gt;Amplify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;API&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;endpoint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://&amp;lt;api-id&amp;gt;.appsync-api.eu-west-1.amazonaws.com/event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;region&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eu-west-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;defaultAuthMode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userPool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Cognito&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;user-pool-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;userPoolClientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;user-pool-client-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;identityPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;identity-pool-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To handle real-time chat with AppSync Events, the client is connected to two endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;events.connect('/chat/responses/${userId}')&lt;/code&gt; to &lt;strong&gt;subscribe&lt;/strong&gt; to responses&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;events.post('/chat/request/${userId}')&lt;/code&gt; to &lt;strong&gt;send&lt;/strong&gt; user messages&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;      &lt;span class="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectToChannel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/chat/responses/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;if&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="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;botMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                  &lt;span class="na"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;content&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

                &lt;span class="p"&gt;};&lt;/span&gt;
                &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&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="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;botMessage&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                &lt;span class="nf"&gt;setSessionId&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&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="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription error&lt;/span&gt;&lt;span class="dl"&gt;"&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="k"&gt;return &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="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unsubscribe&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connection error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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;connectToChannel&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;userId&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;sendMessage&lt;/span&gt; &lt;span class="o"&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;userId&lt;/span&gt; &lt;span class="p"&gt;}:{&lt;/span&gt;&lt;span class="nl"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&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="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&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="na"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newMessage&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;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/chat/request/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newMessage&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;When the React component mounts, we use &lt;code&gt;useEffect&lt;/code&gt; to set up a connection to the &lt;code&gt;/chat/responses/{userId}&lt;/code&gt; channel. This subscribes to responses coming from AppSync Events. Once we start getting responses, we add that message to the chat and update the &lt;code&gt;sessionId&lt;/code&gt; to maintain context.&lt;/p&gt;

&lt;p&gt;To send a message, the &lt;code&gt;sendMessage&lt;/code&gt; function posts to the same &lt;code&gt;/chat/request/{userId}&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;userId&lt;/code&gt; in this example is provided by Amplify’s Authenticator component, which handles user authentication and exposes the signed-in user's details.&lt;/p&gt;

&lt;p&gt;Which gives this chat interface:&lt;/p&gt;

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

&lt;p&gt;If you look at the browser’s WebSocket connection, you’ll see real-time responses coming through as AppSync Events sends data&lt;/p&gt;

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

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

&lt;p&gt;In this post, we walked through building a serverless WebSocket API using AppSync Events, Lambda, and Bedrock Knowledge Bases. We explored how to handle real-time communication securely with Cognito and IAM. Lambda Powertools makes working with AppSync Events an absolute breeze.&lt;/p&gt;

&lt;p&gt;As usual you can find the complete repo with the solution ready to be adapted and deployed here 👉  &lt;a href="https://github.com/ziedbentahar/rag-chat-with-appsync-events-and-bedrock-knowledge-bases" rel="noopener noreferrer"&gt;https://github.com/ziedbentahar/rag-chat-with-appsync-events-and-bedrock-knowledge-bases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will make sure to update the repo once AppSync gets better terraform support 👌&lt;/p&gt;

&lt;p&gt;Thanks for making it all the way here !&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/appsync-events/" rel="noopener noreferrer"&gt;https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/appsync-events/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.VectorDB.html#AuroraPostgreSQL.VectorDB.PreparingKB" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.VectorDB.html#AuroraPostgreSQL.VectorDB.PreparingKB&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/appsync/latest/eventapi/supported-datasources.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/appsync/latest/eventapi/supported-datasources.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.ranthebuilder.cloud/post/aws-appsync-events-and-powertools-for-aws-lambda" rel="noopener noreferrer"&gt;https://www.ranthebuilder.cloud/post/aws-appsync-events-and-powertools-for-aws-lambda&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>appsync</category>
      <category>bedrock</category>
      <category>rag</category>
      <category>powertools</category>
    </item>
    <item>
      <title>Scheduled queries in Amazon Timestream for LiveAnalytics</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Sun, 16 Mar 2025 17:28:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/using-scheduled-queries-with-amazon-timestream-for-liveanalytics-fa</link>
      <guid>https://dev.to/aws-builders/using-scheduled-queries-with-amazon-timestream-for-liveanalytics-fa</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhgn8ztqbxs8pnu0fwdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqhgn8ztqbxs8pnu0fwdw.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Timestream for LiveAnalytics is a serverless, purpose-built database for processing time-series data. While it efficiently ingests and stores data, querying large raw datasets frequently can be inefficient, especially for use cases like aggregations, trend analysis, or anomaly detection.&lt;/p&gt;

&lt;p&gt;Scheduled Queries is a feature of timestream help addressing this by running SQL queries at specified intervals, rolling-up raw data into aggregated results, and storing them in a destination Timestream table. This approach improves performance on target tables, and optimises storage by retaining only the aggregated data.&lt;/p&gt;

&lt;p&gt;In this post, we’ll walk through setting up Timestream Scheduled Queries to automate data rollups. We’ll also explore how this setup helps you analyze and detect trends in your data over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we going to build ?
&lt;/h2&gt;

&lt;p&gt;As a use case, we’ll have a clickstream kinesis topic where producers can push events such as clicks, views, and user actions. These events will be ingested into a Timestream table and made available for downstream consumption. A scheduled query will run hourly to roll up raw event data, which will then be used to detect trending products.&lt;/p&gt;

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

&lt;p&gt;Let’s dive into this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time Series store&lt;/strong&gt;: With EventBridge Pipes supporting Timestream as a direct target, integrating with Kinesis stream source is simple. This eliminates much of the custom glue code needed for ingestion. In this setup, EventBridge Pipes polls the Kinesis stream, converts the received events into records, and writes them directly to the Timestream table. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A scheduled aggregation query&lt;/strong&gt; runs at predefined intervals to process raw events, performing an hourly rollup and storing the results in a dedicated table. Once it successfully completes, it publishes a notification event that triggers a function to query the table and detect the top N trending products, which are then published to a topic. We can imagine that these events can be used for various purposes: adjusting ad spend or fine-tuning A/B tests. Additionally, the rolled-up data can be fed into dashboards like QuickSight, Tableau, or Grafana for visualizing and monitoring product performance.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;☝️Note:&lt;/strong&gt; While I won't go into detail in this post, it's worth noting that a common way to reduce writes to the raw table is by pre-aggregating data in small batches, such as using a 5-minute tumbling window. This groups events into fixed, non-overlapping intervals before writing, effectively lowering the write frequency. &lt;a href="https://github.com/aws-samples/amazon-managed-service-for-apache-flink-examples/blob/main/python/GettingStarted/README.md" rel="noopener noreferrer"&gt;Managed Apache Flink&lt;/a&gt; can help achieve this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s see the code
&lt;/h2&gt;

&lt;p&gt;In this section, I will mainly focus on the integration with EventBridge Pipes as well as the scheduled query configuration. You can find the complete end-to-end solution at the following link 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ziedbentahar/scheduled-queries-with-amazom-timestreamdb" rel="noopener noreferrer"&gt;https://github.com/ziedbentahar/scheduled-queries-with-amazom-timestreamdb&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Simplifying time-series data ingestion with event bridge pipes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The events ingested into Kinesis are quite basic; they contain only the productId, pageId, eventType, and the event timestamp. Configuring a Timestream table as a target in EventBridge Pipes requires defining the mapping from the source event to the selected measurements and dimensions in the target table. This also involves specifying the time field type and format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    resource "awscc_pipes_pipe" "kinesis_to_timestream" {
      name     = "${var.application}-${var.environment}-kinesis-to-timestream"
      role_arn = awscc_iam_role.pipe.arn
      source   = var.source_stream.arn
      target   = aws_timestreamwrite_table.events_table.arn

      source_parameters = {
        kinesis_stream_parameters = {
          starting_position      = "TRIM_HORIZON"
          maximum_retry_attempts = 5
        }

        filter_criteria = {
          filters = [{
            pattern = &amp;lt;&amp;lt;EOF
    {
      "data": {
        "eventType": ["pageViewed", "productPageShared", "productInquiryRequested"]
      }
    }
    EOF
          }]
        }
      }

      target_parameters = {
        timestream_parameters = {
          timestamp_format = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
          version_value    = "1"
          time_value       = "$.data.time"
          time_field_type  = "TIMESTAMP_FORMAT"

          single_measure_mappings = [{
              measure_name      = "$.data.eventType"
              measure_value     = "$.data.value"
              measure_value_type = "BIGINT"
          }]

          dimension_mappings = [
            {
              dimension_name       = "id"
              dimension_value      = "$.data.id"
              dimension_value_type = "VARCHAR"
            },
            {
              dimension_name       = "pageId"
              dimension_value      = "$.data.pageId"
              dimension_value_type = "VARCHAR"
            },
            {
              dimension_name       = "productId"
              dimension_value      = "$.data.productId"
              dimension_value_type = "VARCHAR"
            }
          ]
        }
      }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, I am filtering and ingesting only the pageViewed, productPageShared, and productInquiryRequested events into the table from the stream.&lt;/p&gt;

&lt;p&gt;We can view the details of the deployed pipe in the console:&lt;/p&gt;

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

&lt;p&gt;You can view and edit the configured mapping for the target data model in the console… but when it comes to editing, you should always use IaC! Unless, of course, you enjoy the thrill of undocumented changes 😉&lt;/p&gt;

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

&lt;h3&gt;
  
  
  &lt;strong&gt;Automating the creation of the scheduled queries&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Creating the scheduled query requires defining the schedule configuration, the query statement, and the target mapping, ensuring that the results are properly mapped to the data model for insertion into the destination table.You also need to define an SNS topic to publish notifications about the execution status of scheduled queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    resource "aws_timestreamquery_scheduled_query" "hourly_rollup" {
      name = "${var.application}-${var.environment}-hourly-rollup"
      schedule_configuration {
        schedule_expression = "rate(1 hour)"
      }

      query_string = templatefile("${path.module}/queries/hourly-rollup.sql.tmpl", {
        table = "\"${aws_timestreamwrite_table.events_table.database_name}\".\"${aws_timestreamwrite_table.events_table.table_name}\""
      })


      target_configuration {
        timestream_configuration {
          database_name = aws_timestreamwrite_database.events_db.database_name
          table_name    = aws_timestreamwrite_table.hourly_rollup.table_name
          time_column   = "time"

          dimension_mapping {
            name                 = "pageId"
            dimension_value_type = "VARCHAR"
          }
          dimension_mapping {
            name                 = "productId"
            dimension_value_type = "VARCHAR"
          }
          measure_name_column = "eventType"
          multi_measure_mappings {
            target_multi_measure_name = "eventType"
            multi_measure_attribute_mapping {
              source_column      = "sum_measure"
              measure_value_type = "BIGINT"
            }
          }
        }
      }

      execution_role_arn = aws_iam_role.scheduled_query_role.arn

      error_report_configuration {
        s3_configuration {
          bucket_name = aws_s3_bucket.error_bucket.id
          object_key_prefix = local.hourly_rollup_error_prefix
        }
      }

      notification_configuration {
        sns_configuration {
          topic_arn = aws_sns_topic.scheduled_query_notification_topic.arn
        }
      }

      depends_on = [
        aws_lambda_invocation.seed_raw_events_table
      ]
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    SELECT
        SUM(measure_value::bigint) as sum_measure,
        measure_name as eventType,
        bin(time, 1h) as time,
        productId,
        pageId
    FROM
        ${table}
    WHERE
     time BETWEEN @scheduled_runtime - (interval '2' hour) AND @scheduled_runtime
    GROUP BY
        measure_name,
        bin(time, 1h),
        productId,
        pageId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To account for late-arriving events, it processes data in a two-hour window, ensuring delays of up to two hours are included in the roll-up.&lt;/p&gt;

&lt;p&gt;☝️ &lt;strong&gt;Note:&lt;/strong&gt; As of the time of writing, in order to create a scheduled query, the source table must contain data; otherwise, the creation will fail. To enable fully automated IaC deployment, I will add a Lambda invocation resource that triggers immediately after the source events table is created. This Lambda function will insert dummy events into the table, ensuring that the scheduled query can correctly infer the target schema during its creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    resource "aws_lambda_invocation" "seed_raw_events_table" {

      function_name = aws_lambda_function.seed_raw_table.function_name
      input         = jsonencode({})

      depends_on = [
        aws_lambda_function.seed_raw_table,
        aws_timestreamwrite_table.events_table
      ]

      lifecycle_scope = "CREATE_ONLY"

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

&lt;/div&gt;



&lt;p&gt;Once deployed, you can head straight to the Timestream schedule queries page . Here’s what it looks like:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  &lt;strong&gt;Identifying trending products&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The “&lt;a href="https://github.com/ziedbentahar/scheduled-queries-with-amazom-timestreamdb/blob/main/src/trend-analysis/lambda-handlers/handle-hourly-roll-up.ts" rel="noopener noreferrer"&gt;handle hourly roll up&lt;/a&gt;” function subscribes to the scheduled query execution events and runs a trend analysis that compares the page views per product of the last hour to the previous hour to identify the top N products that have achieved 2x views. It then publishes an event for these products.&lt;/p&gt;

&lt;p&gt;Below is how this function queries the Timestream table using the Timestream Query Client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    const getTopNProduct = async (params: { eventType: string; topN: number }): Promise&amp;lt;TrendingProducts&amp;gt; =&amp;gt; {
        const { eventType, topN } = params;

        const qs = `
    WITH LastHour AS (
        SELECT
            productId,
            sum_measure AS current_views
        FROM "${db}"."${table}"
        WHERE measure_name = '${eventType}'
            AND time &amp;gt; ago(1h)
    ),
    PreviousHour AS (
        SELECT
            productId,
            sum_measure AS previous_views
        FROM "${db}"."${table}"
        WHERE measure_name = '${eventType}'
            AND time &amp;gt; ago(2h)
            AND time &amp;lt;= ago(1h)
    )
    SELECT
        l.productId,
        l.current_views,
        COALESCE(p.previous_views, 0) AS previous_views,
        (l.current_views - COALESCE(p.previous_views, 0)) AS increase_last_hour
    FROM LastHour l
    LEFT JOIN PreviousHour p ON l.productId = p.productId
    WHERE (l.current_views &amp;gt;= COALESCE(p.previous_views, 0) * 2)
    ORDER BY increase_last_hour DESC
    LIMIT ${topN}
    `;

        const queryResult = await timestreamQueryClient.send(
            new QueryCommand({
                QueryString: qs,
            })
        );

        const result = parseQueryResult(queryResult);

        return {
            eventType,
            time: new Date().toISOString(),
            products: result.map((row) =&amp;gt; ({
                productId: row.productId,
                count: Number(row.current_views),
                increaseLastHour: Number(row.increase_last_hour),
            })),
        };
    };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Timestream SDK returns query responses in a raw format. To make them more usable, I drew heavy inspiration from &lt;a href="https://github.com/awslabs/amazon-timestream-tools/blob/master/sample_apps/js/query-example.js" rel="noopener noreferrer"&gt;this code example provided in an AWS Labs repository&lt;/a&gt; to parse the query result.&lt;/p&gt;

&lt;p&gt;For comparison, here’s the same query executed on both the raw events table and the hourly roll-up table for a synthetic dataset of 200,000 ingested events:&lt;/p&gt;

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

&lt;p&gt;Even with this relatively small dataset and its distribution, we can observe a clear difference in both the query duration and the number of bytes scanned between the two tables.&lt;/p&gt;

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

&lt;p&gt;I hope you find this article useful! We explored how to leverage Timestream for live analytics, focusing on automating data rollups to optimise query performance.&lt;/p&gt;

&lt;p&gt;As always, you can find the full code source, ready to be adapted and deployed here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ziedbentahar/scheduled-queries-with-amazom-timestreamdb" rel="noopener noreferrer"&gt;https://github.com/ziedbentahar/scheduled-queries-with-amazom-timestreamdb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading !&lt;/p&gt;

&lt;h2&gt;
  
  
  Further readings
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/fr/blogs/database/build-time-series-applications-faster-with-amazon-eventbridge-pipes-and-timestream-for-liveanalytics/" rel="noopener noreferrer"&gt;Build time-series applications faster with Amazon EventBridge Pipes and Timestream for&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://repost.aws/questions/QUZXrL3Ww8ScmZLuHcQg-8JA/how-to-simultaneously-deploy-timestream-database-table-and-scheduled-query" rel="noopener noreferrer"&gt;How to simultaneously deploy Timestream Database, Table and Scheduled Query&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>timestream</category>
      <category>timeseries</category>
      <category>scheduledqueries</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Replicate data from DynamoDB to Apache Iceberg tables using Glue Zero-ETL integration</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Mon, 30 Dec 2024 14:22:21 +0000</pubDate>
      <link>https://dev.to/aws-builders/replicate-data-from-dynamodb-to-apache-iceberg-tables-using-glue-zero-etl-integration-52h2</link>
      <guid>https://dev.to/aws-builders/replicate-data-from-dynamodb-to-apache-iceberg-tables-using-glue-zero-etl-integration-52h2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsg0xi5d4wnqouqhsux7z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsg0xi5d4wnqouqhsux7z.png" alt="Photo by Alexander Hafemann on Unsplash" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Analyzing data directly from Amazon DynamoDB can be tricky since it doesn’t come with built-in analytics features. One approach is to set up ETL pipelines to move the data into a data lake or a lakehouse. From there, services like Amazon Athena or EMR can take over for analysis and processing. Building and maintaining those ETL pipelines takes time and effort.&lt;/p&gt;

&lt;p&gt;AWS Glue Zero-ETL Integration provides an easy way to replicate data from DynamoDB to Apache Iceberg tables in Amazon S3. It’s particularly useful when your DynamoDB table schema isn’t complex. In such cases, it helps reduce operational overhead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Apache Iceberg is an open-source table format designed for high performance and large-scale analytics. It is increasingly recognized as a standard in data lake architectures providing advanced features such as schema evolution, time travel, ACID transactions, and efficient metadata handling, addressing key challenges in data lakes while offering warehouse-like capabilities.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I’ll walk you through setting up Glue Zero-ETL Integration using Terraform. Along the way, I’ll share my thoughts on using this service.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;You can find the complete code repository at this link 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ziedbentahar/glue-zero-etl-dynamodb-to-apache-iceberg-table" rel="noopener noreferrer"&gt;https://github.com/ziedbentahar/glue-zero-etl-dynamodb-to-apache-iceberg-table&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;I’ll use a hypothetical Orders table to demonstrate running analytical queries with Athena across various order-related dimensions:&lt;/p&gt;

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

&lt;p&gt;In this example, I’m using a simplified Orders model, which has the following structure:&lt;/p&gt;

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

&lt;p&gt;We’ll look at how Zero-ETL integration handles nested fields, sets, and lists of maps but first let setup the configuration.&lt;/p&gt;
&lt;h2&gt;
  
  
  Integration configuration
&lt;/h2&gt;

&lt;p&gt;Let’s walk through the steps to configure the integration.&lt;/p&gt;
&lt;h3&gt;
  
  
  1- Configuring the DynamoDb source table
&lt;/h3&gt;

&lt;p&gt;Before getting started, Point in time recovery (PITR) must be enabled on the source table:&lt;/p&gt;

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

&lt;p&gt;We also need to configure the table’s resource policy to allow the integration to export table’s point in time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_dynamodb_resource_policy" "this" {
  resource_arn = data.aws_dynamodb_table.this.arn

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "glue.amazonaws.com"
        },
        Action = [
          "dynamodb:ExportTableToPointInTime",
          "dynamodb:DescribeTable",
          "dynamodb:DescribeExport"
        ],
        Resource = "*",
        Condition = {
          StringEquals = {
            "aws:SourceAccount" = data.aws_caller_identity.current.account_id
          },
          StringLike = {
            "aws:SourceArn" = "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:integration:*"
          }
        }
      }
    ]
  })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2- Glue catalog database configuration
&lt;/h3&gt;

&lt;p&gt;An IAM role must be created for the Zero-ETL integration target to grant access to the Glue database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_glue_catalog_database" "this" {
  name         = "${var.application}${var.environment}db"
  location_uri = "s3://${aws_s3_bucket.database_bucket.bucket}/"
}

resource "aws_iam_policy" "integration_policy" {
  name = "${var.application}-${var.environment}-integration-policy"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "glue:CreateTable",
          "glue:GetTable",
          "glue:UpdateTable",
          "glue:GetTableVersion",
          "glue:GetTableVersions",
          "glue:GetResourcePolicy"
        ],
        Resource = [
          "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:catalog",
          "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:database/${aws_glue_catalog_database.this.name}",
          "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:table/${aws_glue_catalog_database.this.name}/*"
        ]
      },
      {
        Effect = "Allow",
        Action = [
          "cloudwatch:PutMetricData",
        ],
        Resource = "*",
        Condition = {
          StringEquals = {
            "cloudwatch:namespace" = "AWS/Glue/ZeroETL"
          }
        },

      },
      {
        Effect = "Allow",
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        Resource = "*"
      },
      {
        Effect = "Allow",
        Action = [
          "glue:GetDatabase",
        ],
        Resource = [
          "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:catalog",
          "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:database/${aws_glue_catalog_database.this.name}",
        ]
      },
      {
        Effect = "Allow",
        Action = [
          "s3:ListBucket"
        ],
        Resource = [
          aws_s3_bucket.database_bucket.arn,
        ]
      },
      {
        Effect = "Allow",
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject"
        ],
        Resource = [
          "${aws_s3_bucket.database_bucket.arn}/*",
        ]
      }

    ]
  })
}

resource "aws_iam_role" "integration_role" {
  name = "${var.application}-${var.environment}-integration-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "glue.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "integration" {
  role       = aws_iam_role.integration_role.name
  policy_arn = aws_iam_policy.integration_policy.arn
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3- Creating the integration
&lt;/h3&gt;

&lt;p&gt;Currently, neither CloudFormation nor the AWS Terraform provider supports Glue Zero-ETL. So, I’m using the AWS SDK to create the integration and configure table properties. To handle this, I rely on &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_invocation" rel="noopener noreferrer"&gt;aws_lambda_invocation&lt;/a&gt; to trigger a lambda function that creates or deletes the integration whenever a the database is created or deleted—pretty much like a CloudFormation custom resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { GlueClient, CreateIntegrationCommand, CreateIntegrationResourcePropertyCommand, DeleteIntegrationCommand, CreateIntegrationTablePropertiesCommand } from "@aws-sdk/client-glue";
import { SSMClient, PutParameterCommand, GetParameterCommand } from "@aws-sdk/client-ssm";

export const handler = async (event) =&amp;gt; {

    let glueClient = new GlueClient({ region: process.env.AWS_REGION });
    let paramStore = new SSMClient({ region: process.env.AWS_REGION });

    if(event.sourceArn == null || event.targetArn == null || event.roleArn == null) {
        throw new Error("SourceArn, TargetArn and RoleArn are required");
    }

    if (event.tf.action === "create") {

        const integrationResourcePropertyResult =  await glueClient.send(new CreateIntegrationResourcePropertyCommand({
            ResourceArn: event.targetArn,
            TargetProcessingProperties: {
                RoleArn: event.roleArn
            }
        }));

        const integrationResult = await glueClient.send(new CreateIntegrationCommand({
            IntegrationName : event.integrationName,
            SourceArn : event.sourceArn,
            TargetArn : event.targetArn,

        }));


        await glueClient.send(new CreateIntegrationTablePropertiesCommand({
            ResourceArn: integrationResult.IntegrationArn,
            TableName: event.tableConfig.tableName,
            TargetTableConfig: {
                PartitionSpec: event.tableConfig.partitionSpec ? event.tableConfig.partitionSpec : undefined,
                UnnestSpec: event.tableConfig.unnestSpec ? event.tableConfig.unnestSpec : undefined,
                TargetTableName: event.tableConfig.tableName ? event.tableConfig.tableName : undefined
            }

        }));

        await paramStore.send(new PutParameterCommand({
            Name: event.integrationName,
            Value: JSON.stringify({
                integrationArn:  integrationResult.IntegrationArn,
                resourcePropertyArn: integrationResourcePropertyResult.ResourceArn
            }),
            Type: "String",
            Overwrite: true
        }));

        return;
    }

    if (event.tf.action === "delete") {
        const integrationParams = await paramStore.send(new GetParameterCommand({
            Name: event.integrationName,
        }));

        const { integrationArn } = JSON.parse(integrationParams.Parameter.Value);

        await glueClient.send(new DeleteIntegrationCommand({
            IntegrationIdentifier: integrationArn
        }));

        return;
    }

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

&lt;/div&gt;



&lt;p&gt;I’m using the &lt;a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/glue/command/CreateIntegrationCommand/" rel="noopener noreferrer"&gt;@aws-sdk/client-glue&lt;/a&gt; to set up the integration, assign the target processing role, and configure table properties such as the target table name, schema unnesting options, and data partitioning for the target Apache Iceberg table. By default, the integration with DynamoDB uses the table’s primary keys.&lt;/p&gt;

&lt;p&gt;Here’s how Lambda invocation is used; I’m passing the parameters I want to use to configure the integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_lambda_invocation" "manage_zero_etl_integration" {

  function_name = aws_lambda_function.manage_zero_etl_integration_fn.function_name
  input = jsonencode({
    integrationName = "${var.application}-${var.environment}-zero-etl-integration",
    sourceArn       = data.aws_dynamodb_table.this.arn,
    targetArn       = aws_glue_catalog_database.this.arn,
    roleArn         = aws_iam_role.integration_role.arn,
    tableConfig = {
      tableName = data.aws_dynamodb_table.this.name,
      partitionSpec = [
        {
          FieldName    = "orderDate",
          FunctionSpec = "day"
        }
      ],
      unnestSpec : "FULL"
    }

  })

  lifecycle_scope = "CRUD"

  depends_on = [aws_glue_resource_policy.this]

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

&lt;/div&gt;



&lt;p&gt;Very much a happy-path solution here — just a workaround while waiting for proper IaC support. If you’d prefer not to take this route, another option is to create the integration using the &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/glue/create-integration.html" rel="noopener noreferrer"&gt;CLI&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4- Glue resource policy
&lt;/h3&gt;

&lt;p&gt;Since I’m using the Glue catalog for the integration, I made sure to include the following permissions in the glue catalog resource policy. This allows for integration between the source DynamoDB table and the target Iceberg table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "aws_iam_policy_document" "glue_resource_policy" {
  statement {
    effect = "Allow"

    principals {
      type = "AWS"
      identifiers = [
        "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root",
        aws_iam_role.manage_zero_etl_integration_role.arn
      ]
    }

    actions = [
      "glue:CreateInboundIntegration",
    ]

    resources = [
      "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:catalog",
      "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:database/${aws_glue_catalog_database.this.name}",
    ]

    condition {
      test     = "StringLike"
      variable = "aws:SourceArn"
      values   = [data.aws_dynamodb_table.this.arn]
    }
  }

  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["glue.amazonaws.com"]
    }

    actions = [
      "glue:AuthorizeInboundIntegration"
    ]

    resources = [
      "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:catalog",
      "arn:aws:glue:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:database/${aws_glue_catalog_database.this.name}",
    ]

    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [data.aws_dynamodb_table.this.arn]
    }
  }

  depends_on = [
    aws_iam_role.manage_zero_etl_integration_role,
    aws_lambda_function.manage_zero_etl_integration_fn
  ]
}


resource "aws_glue_resource_policy" "this" {
  policy = data.aws_iam_policy_document.glue_resource_policy.json
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find this configuration in the official docs &lt;a href="https://docs.aws.amazon.com/glue/latest/dg/zero-etl-prerequisites.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glue Zero-ETL in action
&lt;/h2&gt;

&lt;p&gt;Once you’ve deployed all the components, you can go straight to the Glue Zero-ETL list. Here’s what it looks like in the console:&lt;/p&gt;

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

&lt;p&gt;You can view the details. By default the refresh interval from the source DynamoDb table to the Iceberg table is set to 15 minutes, it is not editable for now:&lt;/p&gt;

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

&lt;p&gt;You can also monitor the integration operations and track the number of items inserted, updated, or deleted directly from CloudWatch Logs. The documentation for the metrics generated during each execution can be found at the following &lt;a href="https://docs.aws.amazon.com/glue/latest/dg/zero-etl-monitoring.html#zero-etl-cloudwatch-metrics" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuezitp7d0zlqlsry2rg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuezitp7d0zlqlsry2rg.png" alt="Cloudwatch Logs — number of inserted items during seed operation&amp;lt;br&amp;gt;
" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the first insert operation is successful, you can view the inferred Iceberg table schema on the data catalog page on the console:&lt;/p&gt;

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

&lt;p&gt;☝️ Note that the &lt;code&gt;shippingAddress&lt;/code&gt; was un-nested and &lt;code&gt;deliveryPreferences&lt;/code&gt; was replicated as an array. That’s very convenient. However,&lt;code&gt;items&lt;/code&gt; property was inferred as string. Since it’s a list of maps in DynamoDB, I expected it to map cleanly to a list of structs in Apache Iceberg, but it didn’t quite get the schema right.&lt;/p&gt;

&lt;p&gt;The items property ends up as a plain JSON string in this DynamoDb list format, It’s not perfect, but we can work around it by using &lt;a href="https://docs.aws.amazon.com/athena/latest/ug/extracting-data-from-JSON.html" rel="noopener noreferrer"&gt;json_extract&lt;/a&gt; in Athena to parse the data:&lt;/p&gt;

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

&lt;p&gt;Here’s an example query using Athena to get the number of orders grouped by city:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpy13syv5wo9bk7d4zq2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpy13syv5wo9bk7d4zq2j.png" alt="Example of a query: Number of orders by city&amp;lt;br&amp;gt;
" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s another example where I want to get the number of orders by city where the delivery preferences include &lt;code&gt;LeaveAtDoor&lt;/code&gt;. While this involves some extra steps with DynamoDB, it’s much easier to achieve with Iceberg tables:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1ifn5aoi5a8r2p68arj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1ifn5aoi5a8r2p68arj.png" alt="Getting the number of orders by city where delivery prefrence contains LeaveAtDoor&amp;lt;br&amp;gt;
" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  My Wishlist
&lt;/h2&gt;

&lt;p&gt;After trying out Glue Zero-ETL, I came up with a wishlist of features and improvements I'd like to see. Since it's still relatively new (at the time of writing), I'm looking forward to potential updates and enhancements over time. I'll keep this blog post updated as things evolve:&lt;/p&gt;
&lt;h3&gt;
  
  
  IaC support
&lt;/h3&gt;

&lt;p&gt;Deploying services through the console is not my preferred approach. As mentioned earlier in this post, currently, neither CloudFormation nor the AWS Terraform provider supports Glue Zero-ETL. I used the AWS SDK to create the integration and configure table properties. While this approach works for now, it’s not ideal. I expect that support for CloudFormation and Terraform will be introduced soon.&lt;/p&gt;
&lt;h3&gt;
  
  
  Handling DynamoDb List of Maps
&lt;/h3&gt;

&lt;p&gt;Lists of Maps aren’t supported (yet?). Since Apache Iceberg tables can handle lists of structs, the lack of support for this feature could complicate more advanced use cases with complex table schemas. In such cases, running a custom ETL job remains a better solution.&lt;/p&gt;
&lt;h3&gt;
  
  
  Custom partitioning configuration
&lt;/h3&gt;

&lt;p&gt;When setting up the integration, you can configure target table properties, such data partitioning as using the primary key from the DynamoDB table or specifying a custom partition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await glueClient.send(new CreateIntegrationTablePropertiesCommand({
    ResourceArn: integrationResult.IntegrationArn,
    TableName: event.tableConfig.tableName,
    TargetTableConfig: {
        PartitionSpec: event.tableConfig.partitionSpec ? event.tableConfig.partitionSpec : undefined,
        UnnestSpec: event.tableConfig.unnestSpec ? event.tableConfig.unnestSpec : undefined,
        TargetTableName: event.tableConfig.tableName ? event.tableConfig.tableName : undefined
    }
}));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, while I was able to define custom partition configuration through both the console and the AWS CLI, it didn’t seem to take effect:&lt;/p&gt;

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

&lt;p&gt;I’m not sure if this is a UI issue or if Glue Zero-ETL Integration simply doesn’t support it yet. The &lt;a href="https://docs.aws.amazon.com/glue/latest/dg/zero-etl-sources.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; isn’t very clear on this point, but hopefully, it gets updated soon!&lt;/p&gt;

&lt;h3&gt;
  
  
  Support for AWS services other than DynamoDb
&lt;/h3&gt;

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

&lt;p&gt;The Glue Zero-ETL integration currently supports a many sources, with DynamoDB being the only AWS service available at this point. While this is a great start, I would have preferred better alignment across AWS’s data integration offerings. For example, Amazon Kinesis Data Firehose already supports native CDC integration for RDS databases. It would have been ideal to see a more aligned approach, where Glue Zero-ETL could also support CDC from RDS and other AWS services.&lt;/p&gt;

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

&lt;p&gt;I hope you found this article helpful! I’ve found the Glue Zero-ETL integration to be an interesting tool to have in your toolkit, especially for offloading undifferentiated heavy lifting and focusing on what matters most. It’s also useful for teams that aren’t familiar with writing Glue Jobs, as it makes running ad-hoc analytics queries on data originally stored in DynamoDB much easier.&lt;/p&gt;

&lt;p&gt;As ususal, you can find the full code source, ready to be adapted and deployed here 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ziedbentahar/glue-zero-etl-dynamodb-to-apache-iceberg-table" rel="noopener noreferrer"&gt;https://github.com/ziedbentahar/glue-zero-etl-dynamodb-to-apache-iceberg-table&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading and may your data be clean, your queries be fast, and your pipelines never break 😉&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/zero-etl-using.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/glue/latest/dg/zero-etl-using.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/glue/create-integration.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/cli/latest/reference/glue/create-integration.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/glue/command/CreateIntegrationCommand/" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/glue/command/CreateIntegrationCommand/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>glue</category>
      <category>dynamodb</category>
      <category>iceberg</category>
      <category>zeroetl</category>
    </item>
    <item>
      <title>Building smarter RSS feeds for my newsletter subscriptions with SES and Bedrock</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Sat, 23 Nov 2024 16:38:33 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-smarter-rss-feed-for-my-newsletter-subscriptions-4nf7</link>
      <guid>https://dev.to/aws-builders/building-a-smarter-rss-feed-for-my-newsletter-subscriptions-4nf7</guid>
      <description>&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2ALrlFqriDDuwEQDbB" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2ALrlFqriDDuwEQDbB" alt="Photo by Joanna Kosinska on Unsplash" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I’ll share a tool I recently built for my personal use, driven largely by intellectual curiosity. Although I was aware of services like &lt;a href="https://kill-the-newsletter.com/" rel="noopener noreferrer"&gt;&lt;em&gt;Kill the Newsletter!&lt;/em&gt;&lt;/a&gt;, I wanted to create a service that generates a personalized RSS feeds for my newsletter subscriptions. I wanted the feed to do more than just list content — it would provide summaries of featured articles and topics shared in newsletters I am subscribed to and powered by LLM to categorize, summarize, and extract key information. This helps me stay on top of relevant updates and ensure I don’t miss out on insights from the community.&lt;/p&gt;

&lt;p&gt;To achieve this, I used a feature from Amazon Simple Email Service (SES) that allows for handling incoming emails, making it a suitable option for building event-driven automations that process messages as they arrive. In my case, I used this capability to efficiently manage the newsletters I receive, transforming them into personalized RSS feeds.&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F1%2ABLhSt1pprMHnrsfLroBoxg.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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F1%2ABLhSt1pprMHnrsfLroBoxg.png" alt="captionless image" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the smart RSS feed generator
&lt;/h2&gt;

&lt;p&gt;To start, I wanted to be able to set up email addresses that I can create on the fly whenever I want to subscribe to one or many newsletters. Each email address would correspond to a “virtual” inbox, tailored for a specific type of subscription. Although all emails are technically routed to the same location, this approach allows me to manage and organize my subscriptions as if they were separate inboxes, each based on specific interests and topics. I can create as many dedicated inboxes as needed. For example, one could be set up for &lt;strong&gt;&lt;em&gt;&lt;a href="mailto:awesome-serverless-community@my-domain.com"&gt;awesome-serverless-community@my-domain.com&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;, while another could be for &lt;strong&gt;&lt;em&gt;&lt;a href="mailto:awesome-system-design@my-domain.com"&gt;awesome-system-design@my-domain.com&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;, each handling different types of newsletters and allowing me to configure separate content filtering rules for each subscription.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fri0zcwjpswthkbu0oo1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fri0zcwjpswthkbu0oo1f.png" alt="High-level overview of smart and personalized RSS feed generation from newsletters" width="800" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whenever a newsletter arrives at one of these “virtual” inboxes, the RSS feed generation starts. The first step involves verifying whether the system should handle the email. To achieve this, I implemented an allow list of trusted newsletter senders, which can be configured as needed. This ensures that only approved sources are processed, adding an extra layer of control to the system. Next, the raw email content is converted into Markdown format. An LLM is then used to create a gist of the newsletter and filter the content based on my interests. Both the filter configurations and the allow list are stored in a dedicated table, with filters configured for each feed.&lt;/p&gt;

&lt;p&gt;Some of the newsletters I subscribe to feature valuable community content, such as links to blog posts or videos. I also use the LLM to generate a structured list of these links, along with their main topics, so I can easily access the content that’s most relevant to me.&lt;/p&gt;

&lt;p&gt;Once the gist of a newsletter is ready, it is finally stored in a dedicated table. Each email address gets its own personalized RSS feed that gets served through an API, so it can be accessed by RSS feed readers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;Let’s now have a deeper look at the solution:&lt;/p&gt;

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

&lt;p&gt;Incoming emails are stored in an S3 bucket. Each time a new email arrives, an event is sent to the default EventBridge bus, triggering a workflow to process the email. The &lt;strong&gt;&lt;code&gt;Process Email&lt;/code&gt;&lt;/strong&gt; function handles the entire workflow, including content conversion, filtering, and gist generation. It uses Amazon Bedrock with the Claude Sonnet model to create a structured newsletter gist, which is then stored in a DynamoDB table.&lt;/p&gt;

&lt;p&gt;For serving the RSS feeds, I built the api using Hono web framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some notes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  I chose to use a single Lambda function to handle the entire process of generating the newsletter gist, keeping that part self-contained. &lt;a href="https://medium.com/gitconnected/ai-powered-video-summarizer-with-amazon-bedrock-and-anthropics-claude-9f1832f397dc" rel="noopener noreferrer"&gt;In a previous article&lt;/a&gt;, I explored another approach using a Step Function to interact with an LLM, as it avoids paying for an active Lambda function while waiting for the LLM response, but it requires a more complex setup.&lt;/li&gt;
&lt;li&gt;  The API is deployed using a Function URL (FURL). I use CloudFront Origin Access Control (OAC) to restrict access to the Lambda function URL origin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution details
&lt;/h2&gt;

&lt;p&gt;I built this solution using Nodejs and typescript for functions code and terraform for IaC&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You will find the complete repo of this service here 👉&lt;a href="https://github.com/ziedbentahar/smart-feeds" rel="noopener noreferrer"&gt;&lt;strong&gt;https://github.com/ziedbentahar/smart-feeds&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1 — Email handling — Configuring SES
&lt;/h3&gt;

&lt;p&gt;To get started with SES handling incoming emails, we’ll add an MX record to our domain’s DNS configuration. Next, we’ll create an SES receipt rule to process all incoming emails sent to &lt;code&gt;@my-domain.com&lt;/code&gt;. This rule includes an action to deliver the raw email content to an S3 bucket.&lt;/p&gt;

&lt;p&gt;Here is the how to define this in terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
resource "aws_route53_record" "email_mx_records" {
  zone_id = var.subdomain_zone.id
  name    = local.email_subdomain
  type    = "MX"
  ttl     = "600"
  records = [    "10 inbound-smtp.us-east-1.amazonses.com",
    "10 inbound-smtp.us-east-1.amazonaws.com",
  ]
}
...

resource "aws_ses_receipt_rule_set" "this" {
  rule_set_name = "${var.application}-${var.environment}-newsletter-rule-set"
}
resource "aws_ses_receipt_rule" "this" {
  name          = "${var.application}-${var.environment}-to-bucket"
  rule_set_name = aws_ses_receipt_rule_set.this.rule_set_name
  recipients = ["${local.email_subdomain}"]
  enabled       = true
  scan_enabled  = true
  s3_action {
    position = 1
    bucket_name = aws_s3_bucket.email_bucket.bucket
    object_key_prefix = "${local.emails_prefix}"
  }
  depends_on = [    aws_ses_receipt_rule_set.this,
    aws_s3_bucket.email_bucket,
    aws_s3_bucket_policy.email_bucket_policy
  ]
}
resource "aws_ses_active_receipt_rule_set" "this" {
  rule_set_name = aws_ses_receipt_rule_set.this.rule_set_name
  depends_on = [aws_ses_receipt_rule_set.this]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to update the bucket policy to allow SES to write to the emails bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_s3_bucket_policy" "email_bucket_policy" {
  bucket = aws_s3_bucket.email_bucket.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [      {
        Effect    = "Allow",
        Principal = { Service = "ses.amazonaws.com" },
        Action    = "s3:PutObject",
        Resource  = "${aws_s3_bucket.email_bucket.arn}/*",
        Condition = {
          StringEquals = {
            "aws:Referer" = data.aws_caller_identity.current.account_id
          }
        }
      }
    ]
  })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deployment, you will be able to view in the console the SES email receiving receipt rule details :&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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F1%2A2Kc8xkdQXMvMrHtP7rkL_A.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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F1%2A2Kc8xkdQXMvMrHtP7rkL_A.png" alt="Receipt rule details" width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2— Generating newsletter gist
&lt;/h3&gt;

&lt;p&gt;The ‘Process Email’ lambda is invoked by an event bridge rule whenever a new object is created on the inbox bucket. Let’s have a look into the different involved steps in generating the newsletter gist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const lambdaHandler = async (event: S3ObjectCreatedNotificationEvent) =&amp;gt; {
    const rawContent = await getRawEmailContent(event.detail.object.key);
    const emailId = basename(event.detail.object.key);
    if (!rawContent) {
        throw new Error("Email content not found");
    }
    const { newsletterEmailFrom, newsletterEmailTo, html, date, subject } = await parseEmail(rawContent);
    const feedsConfigs = await getFeedConfigurationsBySenderEmail(newsletterEmailFrom);
    if (feedsConfigs.length === 0) {
        console.warn(`No feed config found for ${newsletterEmailFrom}`);
        return;
    }
    let shortenedLinks = new Map&amp;lt;string, string&amp;gt;();
    const markdown = generateMarkdown(html, {
        shortenLinks: true,
        shortener: (href) =&amp;gt; {
            let shortened = nanoid();
            shortenedLinks.set(shortened, href);
            return shortened;
        },
    });
    for (const [shortened, original] of shortenedLinks) {
        await addShortenedLink(original, shortened);
    }
    const output = await generateNewsletterGist(markdown);
    if (!output) {
        throw new Error("Failed to generate newsletter gist");
    }
    await Promise.allSettled(
        feedsConfigs.map(async (feedConfig) =&amp;gt; {
            await addNewItemToFeed(feedConfig.feedId, {
                feedId: feedConfig.feedId,
                date,
                title: subject,
                emailFrom: newsletterEmailFrom!,
                id: emailId,
                ...output,
            });
        })
    ).catch((e) =&amp;gt; {
        console.error(e);
    });

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;🔎 Let’s zoom-in:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  First, the raw email content is retrieved from the inbox S3 bucket. This content is then parsed to extract the HTML content, the sender, and other relevant details used downstream.&lt;/li&gt;
&lt;li&gt;  Once we confirm the sender is on the allow list, the &lt;code&gt;generateMarkdown&lt;/code&gt; function converts the email content into Markdown. During the transformation, unnecessary elements such as headers and styles are stripped from the email’s HTML content.&lt;/li&gt;
&lt;li&gt;  As I am interested in capturing relevant shared content, typically containing links to original sources, the &lt;code&gt;generateMarkdown&lt;/code&gt; function extracts these links and transforms them into short Ids. These Ids are used in the prompt instead of the full links, helping to reduce the input context length when invoking the model.&lt;/li&gt;
&lt;li&gt;  The short ids are stored in a table, linked to the original URLs, and used in the RSS feed items.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;generateNewsletterGist&lt;/code&gt; generates the prompt and invokes the model&lt;/li&gt;
&lt;li&gt;  And finally the &lt;code&gt;addNewsletterGistToFeed&lt;/code&gt; stores the structured output in the feeds table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find below the the details of the &lt;code&gt;generateMarkdown&lt;/code&gt; function, here I am relying on the &lt;a href="https://github.com/mixmark-io/turndown" rel="noopener noreferrer"&gt;turndown&lt;/a&gt; lib:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import TurndownService from "turndown";
const generateMarkdown = (
    html: string,
    options: 
      { shortenLinks: true; shortener: (href: string) =&amp;gt; string } |
      { shortenLinks: false }
): string =&amp;gt; {
    const turndownService = new TurndownService({});
    turndownService.addRule("styles-n-headers", {
        filter: ["style", "head", "script"],
        replacement: function (_) {
            return "";
        },
    });
    if (options.shortenLinks) {
        turndownService.addRule("shorten-links", {
            filter: "a",
            replacement: function (content, node) {
                const href = node.getAttribute("href");
                if (href) {
                    const shortened = options.shortener(href);
                    return `[${content}](${shortened})`;
                }
                return content;
            },
        });
    }
    const markdown = turndownService.turndown(html);
    return markdown;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important part is the prompt I use to generate the newsletter gist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const prompt = `
Process the provided newsletter issue content in markdown format and generate a structured JSON output by performing the following tasks and adhering to the constraints:
&amp;lt;tasks&amp;gt; 
    * Summarize the most important topics in this newsletter. 
    * Identify and extract the list of content shared in the newsletter, including: 
        * Key topics, extracted as paragraphs. 
        * Articles 
        * Tutorials. 
        * Key events
    * For shared content, extract the most relevant link. For each link, generate a summary sentence related to it. Do not create a link if one is not provided in the newsletter. 
    * Exclude any irrelevant content, such as unsubscribe links, social media links, or advertisements. 
    * Do not invent topics or content that is not present in the newsletter. 
&amp;lt;/tasks&amp;gt;
Here is the expected JSON schema for the output: 
&amp;lt;output-json-schema&amp;gt;
{{output_json_schema}}
&amp;lt;/output-json-schema&amp;gt;
Here is the newsletter content: 
&amp;lt;newsletter-content&amp;gt;
{{newsletter_content}}
&amp;lt;/newsletter-content&amp;gt;
`;
export const generatePromptForEmail = (newsletterContent: string, outputJsonSchema: string) =&amp;gt; {
    return prompt
        .replace("{{newsletter_content}}", newsletterContent)
        .replace("{{output_json_schema}}", outputJsonSchema);
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure the LLM generates the expected result in JSON, I provide the JSON schema for the output structure. Instead of hardcoding the output schema in the prompt, I define the schema of the newsletter gist using Zod and infer both the TypeScript type and the JSON schema from it. This way, any changes to the schema are also reflected in the LLM output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";
import zodToJsonSchema from "zod-to-json-schema";
const linkSchema = z.object({
    text: z.string(),
    url: z.string(),
});
t const newsletterGist = z.object({
    summary: z.string(),
    topics: z.array(z.string()),
    links: z.array(linkSchema),
});
export type NewsletterGist = z.infer&amp;lt;typeof newsletterGist&amp;gt;;
export const newsletterGistSchema = zodToJsonSchema(newsletterGist);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To invoke the model, I use Bedrock Converse API, this allows me to write code once and use it with different models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const prompt = generatePromptForEmail(markdown, JSON.stringify(newsletterGistSchema), config);
const result = await bedrockClient.send(
    new ConverseCommand({
        modelId: process.env.MODEL_ID,
        system: [{ text: "You are an advanced newsletter content extraction and summarization tool." }],
        messages: [            {
                role: "user",
                content: [                    {
                        text: prompt,
                    },
                ],
            },
            {
                role: "assistant",
                content: [                    {
                        text: "{",
                    },
                ],
            },
        ],
    })
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since I want to enforce a JSON output, I’ll need to prefill the assistant’s message with an opening &lt;strong&gt;&lt;code&gt;{&lt;/code&gt;&lt;/strong&gt;. This is specific to Claude models.&lt;/p&gt;

&lt;h3&gt;
  
  
  3 — Serving the newsletters gists as an RSS feed
&lt;/h3&gt;

&lt;p&gt;Working with &lt;a href="https://hono.dev/docs/getting-started/aws-lambda" rel="noopener noreferrer"&gt;Hono&lt;/a&gt; is a breeze. It simplifies many aspects of defining web APIs while supporting Lambda natively. This API serves multiple routes, and I chose to deploy it as a mono-lambda (AKA Lambdalith) to simplify the infrastructure definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Hono } from "hono";
import { handle } from "hono/aws-lambda";
import { feeds } from "./routes/feeds";
import { newsletters } from "./routes/newsletters";
import { links } from "./routes/links";
import { home } from "./routes/home";
export const app = new Hono();
app.route("/", home);
app.route("/feeds", feeds);
app.route("/links", links);
app.route("/newsletters", newsletters);
export const handler = handle(app);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy! The feeds route generates the RSS feed from the newsletter gists already stored in the feeds table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const feeds = new Hono().get("/:id/rss", async (c) =&amp;gt; {
    const feedId = c.req.param("id");
    const feedConfig = await getFeedConfig(feedId);
    let feedItems: NewsletterIssueGist[] = [];
    for await (const items of getFeedItems(feedId)) {
        feedItems = [...items.map((item) =&amp;gt; item.content), ...feedItems];
    }
    const rssFeedItems = feedItems.reduce(
        (acc, item) =&amp;gt; {
            acc[item.id] = {
                item: {
                    title: item.title,
                    description: html`&amp;lt;div&amp;gt;
                        &amp;lt;section&amp;gt;📩 ${item.emailFrom}&amp;lt;/section&amp;gt;
                        &amp;lt;section&amp;gt;📝 ${item.summary}&amp;lt;/section&amp;gt;
                        &amp;lt;section&amp;gt;
                            &amp;lt;div&amp;gt;📝 Topics&amp;lt;/div&amp;gt;
                            &amp;lt;ul&amp;gt;
                                ${item.topics.map((t) =&amp;gt; {
                                    return `&amp;lt;li&amp;gt;${t}&amp;lt;/li&amp;gt;`;
                                })}
                            &amp;lt;/ul&amp;gt;
                        &amp;lt;/section&amp;gt;
                        &amp;lt;section&amp;gt;
                            &amp;lt;div&amp;gt;
                                &amp;lt;a href="https://${process.env.API_HOST}/newsletters/${item.id}"
                                    &amp;gt;📰 Open newsletter content&amp;lt;/a
                                &amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;lt;/section&amp;gt;
                        &amp;lt;section&amp;gt;
                            &amp;lt;ul&amp;gt;
                                ${item.links.map((l) =&amp;gt; {
                                    return `
                                            &amp;lt;li&amp;gt;
                                                &amp;lt;a href="https://${process.env.API_HOST}/links/${l.url}"
                                                    &amp;gt;🔗 ${l.text}&amp;lt;/a
                                                &amp;gt;
                                            &amp;lt;/li&amp;gt;
                                        `;
                                })}
                            &amp;lt;/ul&amp;gt;
                        &amp;lt;/section&amp;gt;
                    &amp;lt;/div&amp;gt;`.toString(),
                    guid: item.id,
                    link: `https://${process.env.API_HOST}/newsletters/${item.id}`,
                    author: item.emailFrom,
                    pubDate: () =&amp;gt; new Date(item.date).toUTCString(),
                },
            };
            return acc;
        },
        {} as Record&amp;lt;string, unknown&amp;gt;
    );
    const feed = toXML(
        {
            _name: "rss",
            _attrs: {
                version: "2.0",
            },
            _content: {
                channel: [                    {
                        title: feedConfig?.name,
                    },
                    {
                        description: feedConfig?.description,
                    },
                    {
                        link: `https://${process.env.API_HOST}/feeds/${feedId}/rss`,
                    },
                    {
                        lastBuildDate: () =&amp;gt; new Date(),
                    },
                    {
                        pubDate: () =&amp;gt; new Date(),
                    },
                    {
                        language: "en",
                    },
                    Object.values(rssFeedItems),
                ],
            },
        },
        { header: true, indent: "  " }
    );
    return c.text(feed);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;here I am using the &lt;code&gt;[jstoxml](https://www.npmjs.com/package/jstoxml)&lt;/code&gt; package to be able to convert the newsletter gist structure to the RSS feed XML format.&lt;/p&gt;

&lt;p&gt;The other routes exposed by this API include &lt;strong&gt;&lt;code&gt;/newsletters&lt;/code&gt;&lt;/strong&gt;, which renders the HTML of the received email already stored in the inbox bucket, and &lt;strong&gt;&lt;code&gt;/links&lt;/code&gt;&lt;/strong&gt;, which redirects the caller to the original content link using the short link id.&lt;/p&gt;

&lt;p&gt;And finally, here is the CloudFront OAC configuration for the API exposed via a function URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_cloudfront_origin_access_control" "this" {
  name                              = "${var.application}-${var.environment}-api-oac"
  origin_access_control_origin_type = "lambda"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}
resource "aws_cloudfront_distribution" "this" {
  origin {
    domain_name              = replace(aws_lambda_function_url.api.function_url, "/https:\\/\\/|\\//", "")
    origin_access_control_id = aws_cloudfront_origin_access_control.this.id
    origin_id                = "api"
    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }
  enabled         = true
  is_ipv6_enabled = true
  aliases = [local.api_subdomain]
  default_cache_behavior {
    allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "api"
    cache_policy_id        = data.aws_cloudfront_cache_policy.disabled.id
    viewer_protocol_policy = "allow-all"
    min_ttl                = 0
  }
  price_class = "PriceClass_200"
  restrictions {
    geo_restriction {
      restriction_type = "none"
      locations        = []
    }
  }
  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.this.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
  depends_on = [aws_acm_certificate_validation.cert_validation]
}

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

&lt;/div&gt;



&lt;p&gt;Once we deploy the API we can access the RSS feed by using this url &lt;strong&gt;&lt;code&gt;https://&amp;lt;some domain&amp;gt;/feeds/&amp;lt;feed-id&amp;gt;/rss&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F1%2AmKvobK8MXX901ph8iL2MnQ.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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F1%2AmKvobK8MXX901ph8iL2MnQ.png" alt="Getting  raw `Awesome serverless updates` endraw  newsletters gists" width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is an example of how it renders in an RSS reader application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonhirulw7kjj5wcbz93v.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonhirulw7kjj5wcbz93v.gif" alt=" raw `Awesome serverless` endraw  newsletters gists from an RSS reader" width="500" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pretty neat, isn’t it? This way, I can follow updates from the community all in one place ! 👍&lt;/p&gt;

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

&lt;p&gt;I had fun building this tool. In my initial iteration, I intended to leverage Bedrock’s prompt management and prompt flow features. Unfortunately, at the time of writing, these services were not mature enough. But, I might explore them in the future.&lt;/p&gt;

&lt;p&gt;The email automation pattern used here isn’t limited to processing newsletters. It can be applied to a varity of other use cases, such as customer support systems or invoice and receipt handling.&lt;/p&gt;

&lt;p&gt;As always, you can find the full code source, ready to be adapted and deployed here 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ziedbentahar/smart-feeds" rel="noopener noreferrer"&gt;&lt;strong&gt;https://github.com/ziedbentahar/smart-feeds&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hope you enjoyed it !&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/receiving-email.html" rel="noopener noreferrer"&gt;How to receive emails using Amazon SES&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hono.dev/docs/getting-started/aws-lambda" rel="noopener noreferrer"&gt;Hono with AWS Lambda&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ses</category>
      <category>bedrock</category>
      <category>hono</category>
      <category>terraform</category>
    </item>
    <item>
      <title>An Alternative to Batch Jobs: Scheduling Events with EventBridge Scheduler</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Tue, 10 Sep 2024 10:54:48 +0000</pubDate>
      <link>https://dev.to/aws-builders/an-alternative-to-batch-jobs-scheduling-events-with-eventbridge-scheduler-596g</link>
      <guid>https://dev.to/aws-builders/an-alternative-to-batch-jobs-scheduling-events-with-eventbridge-scheduler-596g</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DKTl4EFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/9428/0%2AGAOT-Q6MTvfw2W2c" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DKTl4EFh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/9428/0%2AGAOT-Q6MTvfw2W2c" alt="Photo by [Karsten Füllhaas](https://unsplash.com/@karsten_fuellhaas?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a previous post, I wrote about EventBridge Scheduler and how we can use it to &lt;a href="https://levelup.gitconnected.com/using-aws-eventbridge-scheduler-to-build-a-serverless-reminder-application-ba3086cf8e" rel="noopener noreferrer"&gt;build a reminder application&lt;/a&gt;. In this article, we’ll explore how scheduling messages in the future can be also an alternative to batch jobs. This approach is sometimes overlooked; it offers advantages such as reducing system load and improving cost efficiency.&lt;/p&gt;

&lt;p&gt;To illustrate this pattern, we’ll design a system that needs to execute a task after a specified delay whenever a new item is created in a database. This task could involve operations such as resource creation or resource cleanup.&lt;/p&gt;

&lt;p&gt;Here are two approaches to solving this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A batch job is triggered on a schedule, this job selects all newly created items that match the creation time criteria and performs the required tasks for each:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcdfzxc98n7a48iziv68.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcdfzxc98n7a48iziv68.png" alt="Batch job approach" width="800" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Or an event-based approach: When an item is created, the system schedules a one-time message for that new item to be triggered in the future. At due time, the message is sent and the associated task is executed:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36c2m1rdis13jw1sblzr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36c2m1rdis13jw1sblzr.png" alt="Event-based approach" width="800" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are some downsides to using a batch job approach compared to an event-based approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Potential delays&lt;/strong&gt;: Batch jobs run on a fixed schedule, causing tasks to be processed at intervals. This can lead to delays, as some tasks will only be executed after the next batch execution. An event-based approach triggers tasks instantly when execution time criteria is met, ensuring timely execution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resource overhead and inefficiency&lt;/strong&gt;: Depending on how the data is stored, batch jobs can be resource-intensive, particularly when processing a large number of records at once. For example, if the data is stored in a DynamoDB table, it may require scanning the table to find items matching item’s creation time criteria or adjusting the design of the table. This can lead to sub-optimal resource usage. In contrast, an event-based approach distributes the workload more evenly over time and potentially lowering costs by eliminating the need for periodic processing.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;⚠️ &lt;strong&gt;A word on cancellation on the event-based approach&lt;/strong&gt;: This pattern can become tricky when handling task cancellations. Possible solutions include adding the ability to delete an existing schedule when a cancellation event occurs or validating in the ‘Run Task’ step whether the scheduled action is still eligible for execution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7ZkEB2cC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://miro.medium.com/v2/resize:fit:1400/format:webp/1%2A1PAa6pymiSSHDUXYLT2BLQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7ZkEB2cC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://miro.medium.com/v2/resize:fit:1400/format:webp/1%2A1PAa6pymiSSHDUXYLT2BLQ.png" alt="A strategy for handling task cancellation" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Possible implementations
&lt;/h2&gt;

&lt;p&gt;In the example, I’ll use DynamoDB as the storage layer. However, the same principles apply to other storage systems that support item-level change data capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Using TTL as a scheduling mechanism
&lt;/h3&gt;

&lt;p&gt;One solution is to leverage DynamoDB’s TTL item expiration. When creating the original item, add an associated item in the same transaction, which we’ll call a ‘signal’. This signal is created with a TTL that matches the desired event date. And then, configure a DynamoDB Stream and subscribe the ‘Run Task’ function to the signal &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-streams.html" rel="noopener noreferrer"&gt;item expiration event&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxy1afnrwifjqk1ybgxld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxy1afnrwifjqk1ybgxld.png" alt="Using TTL as a schedule" width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛑 There is an issue with this approach&lt;/strong&gt;: The item expiration and deletion are not guaranteed to be immediate so tasks for these new items might not run exactly at the due time. The DynamoDB TTL docs do not specify a precise timeframe; item expiration can occur within a few days:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DYMCEs-c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/3876/1%2A9O3YtdYZHHpuagLKWDKi0A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DYMCEs-c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/3876/1%2A9O3YtdYZHHpuagLKWDKi0A.png" alt="DynamoDb [TTL documentation page](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html)" width="800" height="101"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If there are no strict requirements to trigger the task at the exact expected time, this solution can be suitable. It’s a tradeoff.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ A better solution
&lt;/h3&gt;

&lt;p&gt;There is a better solution with EventBridge Scheduler and one-time schedules capability:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7ul8jixmd33zwnc15of.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7ul8jixmd33zwnc15of.png" alt="Using event bridge scheduler" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ‘Create One-Time Schedule for New Items’ function filters for newly created items in the table’s stream and creates a new schedule for each. While I could have configured the EventBridge Scheduler to directly trigger a Lambda function as the target, this approach might not be suited when dealing with a high volume of events. In some cases, it’s better to use a queue as target and control the ‘Run Task’ function concurrency. This way, the function can process messages at a manageable rate. It’s about finding the right balance between direct invocation and rate control, and depending on the concrete use case at hand.&lt;/p&gt;

&lt;p&gt;On a side note, I would have loved to see EventBridge Scheduler as a target of EventBridge Pipes; this could have simplified the solution even further.&lt;/p&gt;




&lt;p&gt;Let’s see how to implement this with EventBridge Scheduler. I’ll be using terraform for IaC and Rust for functions code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scheduling new tasks
&lt;/h2&gt;

&lt;p&gt;First, I’ll create a dedicated scheduler group to organise all scheduled tasks within a single group:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_scheduler_schedule_group" "schedule_group" {
  name = "${var.application}-${var.environment}"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The ‘Schedule Tasks’ function handles only new items created on the table. We achieve this by using DynamoDB Stream filtering capabilities provided by the event source mapping. As a result, the function will be invoked only when new items are inserted:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_lambda_event_source_mapping" "schedule_task_lambda" {
  event_source_arn        = aws_dynamodb_table.table.stream_arn
  function_name           = aws_lambda_function.schedule_task_lambda.function_name
  starting_position       = "TRIM_HORIZON"
  function_response_types = ["ReportBatchItemFailures"]

  filter_criteria {
    filter {
      pattern = jsonencode({
        eventName = ["INSERT"]
      })
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To create new one-time schedules on EventBridge Scheduler, this function requires the &lt;code&gt;scheduler:CreateSchedule&lt;/code&gt; action on the custom scheduler group, as well as the &lt;code&gt;iam:PassRole&lt;/code&gt; action for the role used by the schedule. A role that grants permission to send messages to the Tasks SQS queue:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
 {
    Effect = "Allow"
    Action = [
      "scheduler:CreateSchedule"
    ]
    Resource = "arn:aws:scheduler:${region}:${account_id}:schedule/${aws_scheduler_schedule_group.schedule_group.name}/*"
  },
  {
    Effect = "Allow"
    Action = [
      "iam:PassRole"
    ]
    Resource = aws_iam_role.scheduler_role.arn
  },
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Zooming in on the ‘Schedule Task’ function code&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For each new item, we’ll create a schedule that will trigger once, two hours after the item’s creation time:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async fn process_new_item(
    new_item: &amp;amp;SomeItem,
    scheduler_client: &amp;amp;aws_sdk_scheduler::Client,
    scheduler_group_name: &amp;amp;String,
    scheduler_target_arn: &amp;amp;String,
    scheduler_role_arn: &amp;amp;String,
) -&amp;gt; Result&amp;lt;(), Error&amp;gt; {

    // as an example, we'll configure a one-time schedule two hours after the item was created
    let now = Utc::now();
    let two_hours_later = now + Duration::hours(2);
    let two_hours_later_fmt = two_hours_later.format("%Y-%m-%dT%H:%M:%S").to_string();

    let response = scheduler_client
        .create_schedule()
        .name(format!("schedule-{}", &amp;amp;new_item.id))
        .action_after_completion(ActionAfterCompletion::Delete)
        .target(
            Target::builder()
                .input(serde_json::to_string(&amp;amp;new_item)?)
                .arn(scheduler_target_arn)
                .role_arn(scheduler_role_arn)
                .build()?,
        )
        .flexible_time_window(
            FlexibleTimeWindow::builder()
                .mode(FlexibleTimeWindowMode::Off)
                .build()?,
        )
        .group_name(scheduler_group_name)
        .schedule_expression(format!("at({})", two_hours_later_fmt))
        .client_token(nanoid!())
        .send()
        .await;

    match response {
        Ok(_) =&amp;gt; Ok(()),
        Err(e) =&amp;gt; {
            error!("Failed to create schedule: {:?}", e);
            return Err(Box::new(e));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important bits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Some parameters are required. When defining the target, you’ll need to provide the message payload and specify the role that EventBridge Scheduler will use to invoke the target as well as the target arn, which is the arn of the SQS queue in this case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We also need to ensure that the schedule is deleted after the target invocation is successful by using &lt;code&gt;ActionAfterCompletion::Delete&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And since we configured the function response type to ReportBatchItemFailures, here is how process_new_item is called by the function handler:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async fn process_records(
    event: LambdaEvent&amp;lt;Event&amp;gt;,
    scheduler_client: &amp;amp;aws_sdk_scheduler::Client,
    scheduler_group_name: &amp;amp;String,
    scheduler_target_arn: &amp;amp;String,
    scheduler_role_arn: &amp;amp;String,
) -&amp;gt; Result&amp;lt;DynamoDbEventResponse, Error&amp;gt; {
    let mut response = DynamoDbEventResponse {
        batch_item_failures: vec![],
    };

    if event.payload.records.is_empty() {
        return Ok(response);
    }

    for record in &amp;amp;event.payload.records {
        let item = record.change.new_image.clone();
        let new_item: SomeItem = serde_dynamo::from_item(item)?;

        let record_processing_result = process_new_item(
            &amp;amp;new_item,
            scheduler_client,
            &amp;amp;scheduler_group_name,
            &amp;amp;scheduler_target_arn,
            &amp;amp;scheduler_role_arn,
        )
        .await;

        if record_processing_result.is_err() {
            let error = record_processing_result.unwrap_err();
            error!("error processing item - {}", error);
            response.batch_item_failures.push(DynamoDbBatchItemFailure {
                item_identifier: record.change.sequence_number.clone(),
            });

        }
    }

    Ok(response)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Once new tasks are correctly scheduled, they will be visible on the schedules page in the console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr11ewbpqrnfhpj1md9f6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr11ewbpqrnfhpj1md9f6.png" alt="Schedules page on the Console" width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can also view the details related to the schedule’s target in the console&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8a26p2rawz58xqclx3hz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8a26p2rawz58xqclx3hz.png" alt="Schedule Target configuration" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the scheduled task time is due, the message associated with the item, will be sent to the SQS queue and then processed by the ‘Run Task’ function. That’s all 👌 !&lt;/p&gt;

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

&lt;p&gt;With one-time schedules EventBridge Scheduler enables interesting patterns that can improve application architecture by reducing the overhead associated with batch jobs. But as always, choosing between an event-driven approach or batch jobs depends on your application’s needs and complexity of the use case at hand.&lt;/p&gt;

&lt;p&gt;You can find the complete source code, this time written in Rust and Terraform, ready to adapt and deploy 👇&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/scheduling-messages-in-future-with-eventbridge-scheduler/tree/main" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/scheduling-messages-in-future-with-eventbridge-scheduler&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading — I hope you found it helpful!&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Time to Live (TTL)&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://serverlessland.com/serverless/visuals/eventbridge/eventbridge-scheduler" rel="noopener noreferrer"&gt;&lt;strong&gt;Serverless Land&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=zWgqj2OEKX8&amp;amp;t=2s" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=zWgqj2OEKX8&amp;amp;t=2s&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>eventbridge</category>
      <category>eventdriven</category>
      <category>serverless</category>
      <category>rust</category>
    </item>
    <item>
      <title>RAG on media content with Bedrock Knowledge Bases and Amazon Transcribe</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Thu, 01 Aug 2024 15:45:55 +0000</pubDate>
      <link>https://dev.to/aws-builders/using-rag-on-media-content-with-bedrock-knowledge-bases-and-amazon-transcribe-2kpf</link>
      <guid>https://dev.to/aws-builders/using-rag-on-media-content-with-bedrock-knowledge-bases-and-amazon-transcribe-2kpf</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F12000%2F0%2AIN9Eb43nUR64B1Q9" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F12000%2F0%2AIN9Eb43nUR64B1Q9" alt="Photo by [Pawel Czerwinski](https://unsplash.com/@pawel_czerwinski?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://levelup.gitconnected.com/ai-powered-video-summarizer-with-amazon-bedrock-and-anthropics-claude-9f1832f397dc" rel="noopener noreferrer"&gt;a previous article&lt;/a&gt;, I wrote about building an application capable of generating summaries from YouTube videos. It was fun to build. However, the solution was limited as it was only handling Youtube videos and relied on YouTube’s generated transcripts. I wanted to improve this solution and make it more versatile as well as supporting other media formats.&lt;/p&gt;

&lt;p&gt;In this article, I take a different approach: Transcribing media files with Amazon Transcribe and using the generated transcripts as a knowledge base, allowing for retrieval-augmented generation with Amazon Bedrock.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2A5YKjzEm6p84XeS7Bzwcyrw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2A5YKjzEm6p84XeS7Bzwcyrw.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG with Amazon Bedrock Knowledge Bases
&lt;/h2&gt;

&lt;p&gt;Retrieval-Augmented Generation (RAG) is a technique that improves model responses by combining information retrieval with prompt construction: When processing a query, the system first retrieves relevant data from custom knowledge bases. It then uses this data in the prompt, enabling the model to generate more accurate and contextually relevant answers.&lt;/p&gt;

&lt;p&gt;RAG significantly improves the model’s ability to provide informed, up-to-date answers, bridging the gap between the model’s training data and custom up-to-date information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2Aqxn1RXNCsbG7QpXKc8YlcQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2Aqxn1RXNCsbG7QpXKc8YlcQ.png" alt="Typical retrieve and generate flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How Bedrock can help ?
&lt;/h3&gt;

&lt;p&gt;Amazon Bedrock’s Knowledge Bases simplify the RAG process by handling much of the heavy lifting. This includes synchronizing content from Amazon S3, chunking, converting it into embeddings, and storing them in vector databases. It also provides endpoints that allow applications to query the knowledge base while generating responses based on the retrieved data. By handling these tasks, Bedrock allows to focus on building AI-powered applications rather than managing infrastructure.&lt;/p&gt;

&lt;p&gt;In this article, I will use RAG for media transcripts to generate responses based on audio or video content. These contents can be meeting recordings, podcasts, conference talks, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;The architecture of a typical RAG system consists of two components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Knowledge base indexing and synchronisation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retrieval and generation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F7812%2F1%2AQo8AbLkySnAPylgiT1KA5Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F7812%2F1%2AQo8AbLkySnAPylgiT1KA5Q.png" alt="Architecture overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users first request an upload link by invoking the &lt;strong&gt;Request Media upload link&lt;/strong&gt; function, which generates an S3 presigned URL. The user’s request includes the media metadata such as the topic, the link to the media and date. This metadata will be stored and used downstream by Bedrock to apply filtering during the retrieval phase. When the upload process completes in the media bucket, the &lt;strong&gt;Start transcription job&lt;/strong&gt; function is triggered by media bucket event notification.&lt;/p&gt;

&lt;p&gt;When a transcription job state changes, EventBridge will publish job completion status events (Success or Failure). The &lt;strong&gt;Handle transcription and sync knowledge base&lt;/strong&gt; function handles only successful events, extracts the transcription content, stores the extracted text transcript in the knowledge base bucket, and triggers a knowledge base sync.&lt;/p&gt;

&lt;p&gt;The vector database is an important part of a RAG system. It stores and retrieves text representations as vectors (also known as embeddings). allowing for similarity searches when given a query. Bedrock supports various vector databases, including Amazon OpenSearch, PostgreSQL with the pgvector extension, and Pinecone. Each option has its advantages. In this solution, I chose Pinecone as it is a serverless service that allows for quick and easy setup.&lt;/p&gt;

&lt;p&gt;When using Knowledge Bases, the Model used for generating embeddings can differ from the one used for response generation. For example, in this sample, I use “Amazon Titan Text embedding v2” for embedding and “Claude 3 Sonnet” for response generation.&lt;/p&gt;

&lt;p&gt;☝&lt;strong&gt;️Note&lt;/strong&gt;: In this article, I did not include the parts that handle transcription job monitoring and asynchronous job progress notifications to the requester. For insights on building an asynchronous REST APIs, you can refer to &lt;a href="https://zied-ben-tahar.medium.com/serverless-asynchronous-rest-apis-on-aws-c48156d790c1" rel="noopener noreferrer"&gt;this previous article.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alright, let’s deep dive into the implementation&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Details
&lt;/h2&gt;

&lt;p&gt;This time, I am taking a different approach compared to my previous articles: I will be using Rust for lambda code and terraform for IaC.&lt;/p&gt;

&lt;h3&gt;
  
  
  1- Creating the Pinecone vector database
&lt;/h3&gt;

&lt;p&gt;I try to do IaC whenever possible. The good news is that Pinecone offers a Terraform provider, which simplifies managing Pinecone indexes and collections as code. First we’ll need an API Key:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5820%2F1%2Aj8rC0BmI_c1GNTyBkW5zBg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5820%2F1%2Aj8rC0BmI_c1GNTyBkW5zBg.png" alt="Creating API Key from pinecone console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, I am using the serverless version of Pinecone. We need to set the &lt;code&gt;PINECONE_API_KEY&lt;/code&gt; environment variable to the API key we just created so that it can be used by the provider.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h3&gt;
  
  
  2- Creating the knowledge base
&lt;/h3&gt;

&lt;p&gt;Creating the knowledge base involves defining two key components: the vector store configuration, which points to Pinecone, and the data source.&lt;/p&gt;

&lt;p&gt;The data source dictates how the content will be ingested, including the storage configuration and the content chunking strategy.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
For the data source, I am setting the chunking strategy to &lt;code&gt;FIXED_SIZE&lt;/code&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
After deployment, you will be able to view in the console the data source configuration:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4484%2F1%2A8-etm7HYsfA8C53lqLQjaA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4484%2F1%2A8-etm7HYsfA8C53lqLQjaA.png" alt="Data source configuration on the console"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3- Requesting Media upload link
&lt;/h3&gt;

&lt;p&gt;This function is invoked by the API Gateway to generate a presigned URL for media file uploads. A unique identifier is assigned to the object, which will also serve as the transcription job name and as the reference for the knowledge base document.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
The request is validated to ensure that the media metadata properties are properly defined. I am using the &lt;code&gt;serde_valid&lt;/code&gt; crate to validate the request payload. This crate is very convenient for defining schema validations using attributes.&lt;br&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
And here is are details of the generate_presigned_request_uri&lt;br&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h3&gt;
  
  
  4- Handling media upload and starting transcription job
&lt;/h3&gt;

&lt;p&gt;This function is triggered by an S3 event whenever a new file is successfully uploaded. As a convention, I am using the media object key as the transcription job name which is the unique identifier of the task.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
This function needs &lt;code&gt;transcribe:startTranscriptionJob&lt;/code&gt; permission in order to be able to start a transcription task.

&lt;p&gt;Once the task is started, we can monitor the transcription job process in the console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4072%2F1%2A4RJVF64sPBKSHYMIL6yrQg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4072%2F1%2A4RJVF64sPBKSHYMIL6yrQg.png" alt="Transcription job details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5- Subscribing to transcription success events and syncing knowledge base
&lt;/h3&gt;

&lt;p&gt;Let’s first have a look into the event bridge rule definition in terraform:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Which translates to the following configuration in the AWS console:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4312%2F1%2AucKkyXfDBNo0zfROI1B6IQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4312%2F1%2AucKkyXfDBNo0zfROI1B6IQ.png" alt="Transcription success event bridge rule"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, the &lt;strong&gt;Handle successful transcription&lt;/strong&gt; function is invoked each time a transcription is successfully completed. I am only interested in having the transcription job name, as I will use it as the data source object key:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
This function first retrieves the transcription result content available at the &lt;code&gt;transcript_file_uri&lt;/code&gt;, extracts the important part and stores it in the knowledge base bucket as well as its metadata and then triggers a &lt;code&gt;start_ingestion_job&lt;/code&gt;. If the operation fails, it will be retried by EventBridge and eventually put into a dead letter queue.

&lt;p&gt;☝️**Note: **I opted against using a step function for this part since the transcribe output could exceed 256 KB.&lt;/p&gt;

&lt;h3&gt;
  
  
  6- Chatting with the knowledge base in the console
&lt;/h3&gt;

&lt;p&gt;Before building the function that queries the knowledge base, we can already test it from the console. I used this &lt;a href="https://www.youtube.com/watch?v=5Qe9mCgr6-Q" rel="noopener noreferrer"&gt;awesome first believe in serverless podcast episode&lt;/a&gt; as a data source:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4436%2F1%2A9DrVpZGnMj_v7-vRSvsa4Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4436%2F1%2A9DrVpZGnMj_v7-vRSvsa4Q.png" alt="Testing the knowledge base from the console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The console also provides a way to test and adjust the generation configuration, including choosing the model, using a custom prompt, and adjusting parameters like temperature, top-p. This allows you to tailor the configuration to your specific use case requirements.&lt;/p&gt;

&lt;p&gt;Alright, let’s now create the endpoint to see how we can query this knowledge base&lt;/p&gt;

&lt;h3&gt;
  
  
  7- Querying the knowledge base
&lt;/h3&gt;

&lt;p&gt;This function requires the bedrock:RetrieveAndGenerate permission for accessing the knowledge base and the bedrock:InvokeModel permission for the Claude 3 sonnet model arn used during the generation phase. It returns an output result along with the source URL associated with the retrieved chunks that contributed to the output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
The &lt;code&gt;build_retrieve_and_generate_configuration&lt;/code&gt; function prepares the necessary parameters for calling the retrieveAndGenerate endpoint.

&lt;p&gt;As an example, I am applying a retrieval filter to the topic attribute.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Et voilà ! Let’s call our api straight from postman:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A4800%2Fformat%3Awebp%2F1%2AxKuFQiw0gNHd26UBdr2-bA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A4800%2Fformat%3Awebp%2F1%2AxKuFQiw0gNHd26UBdr2-bA.png" alt="Testing the RAG endpoint from postman"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I’ve only scratched the surface when it comes to building RAG systems. Bedrock simplifies the process considerably. There’s still plenty of room for improvement, such as optimising retrieval methods and refining prompts. I had also fun building this in Rust — using &lt;a href="https://www.cargo-lambda.info/" rel="noopener noreferrer"&gt;Cargo Lambda&lt;/a&gt; makes creating Lambdas in Rust a breeze, check it-out!&lt;/p&gt;

&lt;p&gt;As always, you can find the full code source, ready to be adapted and deployed here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/rag-on-media-content-with-bedrock-and-transcribe" rel="noopener noreferrer"&gt;&lt;strong&gt;ziedbentahar/rag-on-media-content-with-bedrock-and-transcribe&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading ! Hope you enjoy it&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.pinecone.io/integrations/aws" rel="noopener noreferrer"&gt;&lt;strong&gt;Amazon Web Services (AWS) - Pinecone Docs&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.cargo-lambda.info/" rel="noopener noreferrer"&gt;&lt;strong&gt;Rust functions on AWS Lambda made simple&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-create.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Create a knowledge base&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bedrock</category>
      <category>rag</category>
      <category>rust</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Adding flexibility to your deployments with Lambda Web Adapter</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Fri, 19 Apr 2024 08:48:07 +0000</pubDate>
      <link>https://dev.to/aws-builders/adding-flexibility-to-your-deployments-with-lambda-web-adapter-42m2</link>
      <guid>https://dev.to/aws-builders/adding-flexibility-to-your-deployments-with-lambda-web-adapter-42m2</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8510%2F0%2AXWhUspOFaIMUneQD" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8510%2F0%2AXWhUspOFaIMUneQD" alt="Photo by [Lea L](https://unsplash.com/@leladesign?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/awslabs/aws-lambda-web-adapter" rel="noopener noreferrer"&gt;Lambda Web Adapter&lt;/a&gt; (LWA) is an open-source project that enables running Web apps on Lambda functions without the need to change or adapt the code.&lt;/p&gt;

&lt;p&gt;In my opinion, LWA opens up interesting pathways for architecture evolution: While it’s an interesting tool that helps lift &amp;amp; shift Web apps and APIs to Lambda functions without a lot of effort, it can also enable another migration path : Start deploying your application in Lambda as a Lambdalith and then transition to a classical container deployment when needed (&lt;em&gt;e.g.&lt;/em&gt; ECS Fargate). In some scenarios, it may happen that you don’t have enough data to decide whether it’s better to host on Lambda or on Fargate. LWA contributes by adding portability to your deployments.&lt;/p&gt;

&lt;p&gt;In this article, we’ll explore how to use LWA with CDK to simplify the deployment of your Web apps in Lambda and how to easily transition to ECS Fargate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Lambda Web adapter works ?
&lt;/h2&gt;

&lt;p&gt;LWA is a Lambda extension. It creates an independent process within the Lambda execution environment that listens for incoming events and forwards them to your HTTP server.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3964%2F1%2ANxwb4iEtSkvmTvFGzpGjYQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3964%2F1%2ANxwb4iEtSkvmTvFGzpGjYQ.png" alt="Lambda Web Adapter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LWA can integrate with invocations from Lambda function URLs (FURL), ALB, and API Gateway, converting invocation JSON payloads into HTTP requests that web frameworks like fastify or ASP.NET can handle. LWA also supports non-HTTP triggers (&lt;em&gt;e.g.&lt;/em&gt; SQS, S3 notifications), but in these cases, it acts as a pass-through without converting the invocation payload.&lt;/p&gt;

&lt;p&gt;LWA supports functions packaged as zip as well as Docker or OCI images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;Let’s have a look on how we’ll create our flexible deployment using CDK. In this example we’ll be focusing on deploying a public Web application using &lt;a href="https://fastify.dev/" rel="noopener noreferrer"&gt;fastify&lt;/a&gt; as a Web framework:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4272%2F1%2AMZgr0wk-x9Mn0QpP6SFJ4Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4272%2F1%2AMZgr0wk-x9Mn0QpP6SFJ4Q.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My objective is to create a CDK construct supporting two deployment strategies: &lt;strong&gt;Lambda&lt;/strong&gt; or &lt;strong&gt;ECSFargate&lt;/strong&gt;. Depending on the selected strategy, only the required components will be deployed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When in Lambda mode, we’ll configure the function to use LWA extension. We’ll also configure a REST API Gateway with lambda proxy integration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the deployment mode is ECSFargate, We will deploy our application in an ECS Fargate service that is exposed via an ALB.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In both of these deployment strategies, users access the Web app through CloudFront. We will associate a WAF Web ACL to restrict access to both the API Gateway and the Application Load Balancer. These origins will only respond to requests that include a custom verification header added by the CloudFront distribution. This approach prevents bypassing the CloudFront distribution to access the origin directly.&lt;/p&gt;

&lt;p&gt;☝️&lt;strong&gt;Some notes:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When deploying in Lambda, I ruled-out the use of FURL or HTTP API Gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;With FURL, while you can setup Origin Access Control (OAC) with CloudFront, at the time of writing, &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html#create-oac-overview-lambda" rel="noopener noreferrer"&gt;PUT and POST operations require the client to sign the request payload&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTTP API Gateway does not support WAF. An alternative solution would involve creating a Lambda@Edge to verify the presence of the custom header in the request.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To improve security, the custom verification header can be stored in secrets manager with &lt;a href="https://github.com/aws-samples/amazon-cloudfront-waf-secretsmanager" rel="noopener noreferrer"&gt;rotation&lt;/a&gt; enabled so that the header can be updated as well as the origin WAF and the CloudFront distribution configurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s see the code
&lt;/h2&gt;

&lt;p&gt;Here are the relevant parts of the solution:&lt;/p&gt;

&lt;h3&gt;
  
  
  1- Deploying fastify Web app on Lambda using LWA
&lt;/h3&gt;

&lt;p&gt;Creating a new fastify project is a breeze, I generally go with &lt;a href="https://fastify.dev/docs/latest/Reference/TypeScript/" rel="noopener noreferrer"&gt;typescript&lt;/a&gt;; for the purpose of this article, I will create one super basic api:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    import fastify from "fastify";

    const server = fastify();

    server.get("/health", async () =&amp;gt; {
      return "yup ! I am healthy";
    });

    server.get("/where-are-you-deployed", async () =&amp;gt; {
      return {
        "i-am-deployed-on": process.env.DEPLOYED_ON,
      };
    });

    server.listen({ host: "0.0.0.0", port: 8080 }, (err, address) =&amp;gt; {
      if (err) {
        console.error(err);
        process.exit(1);
      }
      console.log(`Server listening at ${address}`);
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I will deploy the Lambda function using zip archive and in order to use LWA as a Lambda extension, we’ll need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Attach the LWA layer to the function&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set the handler to the startup command &lt;a href="https://github.com/ziedbentahar/flexible-deployment-with-lambda-web-adapter/blob/main/src/run.sh" rel="noopener noreferrer"&gt;run.sh&lt;/a&gt; script. This script starts the fastify Web app. It will be added to the zip package after the code bundling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And lastly, define the &lt;code&gt;AWS_LAMBDA_EXEC_WRAPPER&lt;/code&gt; environment variable to &lt;code&gt;/opt/bootstrap&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApi.html" rel="noopener noreferrer"&gt;RestApi&lt;/a&gt; CDK construct simplifies exposition of the Lambda function:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;After deployment, you will be able to view in the console the layer associated with the function:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6112%2F1%2Al2-x_V0rcVbsZMb1_MXIkQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6112%2F1%2Al2-x_V0rcVbsZMb1_MXIkQ.png" alt="Lambda with LWA layer configured"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2- Defining the alternative deployment on ECS Fargate
&lt;/h3&gt;

&lt;p&gt;CDK offers an &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs_patterns.ApplicationLoadBalancedFargateService.html" rel="noopener noreferrer"&gt;L3 construct&lt;/a&gt; to deploy a load balanced ECS service. What I find interesting about this construct is that it hides all the complexity and verbosity of defining such a deployment, while allowing a level of flexibility. Another neat feature is that it can build and push &lt;a href="https://github.com/ziedbentahar/flexible-deployment-with-lambda-web-adapter/blob/main/src/Dockerfile" rel="noopener noreferrer"&gt;our container&lt;/a&gt; image.&lt;/p&gt;

&lt;p&gt;We’ll make sure to enable HTTPS, for that we’ll create a certificate and associated to the load balancer:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Here, I am using the default configuration, but you will want to adapt it to your own requirements.&lt;/p&gt;

&lt;p&gt;Once deployed, the ECS service looks like this on the AWS console&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6132%2F1%2AHxmV9l15tk2gNoajI1iJIg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6132%2F1%2AHxmV9l15tk2gNoajI1iJIg.png" alt="Solution overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the full definition of the construct &lt;a href="https://github.com/ziedbentahar/flexible-deployment-with-lambda-web-adapter/blob/main/infra/lib/WebAppOnECSFargate.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3- Handling the two different deployment strategies
&lt;/h3&gt;

&lt;p&gt;The important bit, the CDK construct that enables flexible deployments:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;This construct handles two deployment strategies &lt;code&gt;Lambda&lt;/code&gt; or &lt;code&gt;ECSFargate&lt;/code&gt;. &lt;br&gt;
For each strategy, we’ll need to provide a factory function that creates the required resources. These two functions need to be injected from a parent construct and they are lazily evaluated given the selected strategy.&lt;/p&gt;

&lt;p&gt;We’ll also make sure that the distribution cache policy is disabled for both of these two origins.&lt;/p&gt;

&lt;p&gt;As an example, the origin of the distribution, should end up looking like this when you select Lambda deployment strategy&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5172%2F1%2AkSi63nG4AUSV0HmjDg345Q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5172%2F1%2AkSi63nG4AUSV0HmjDg345Q.png" alt="Configured origin on “Lambda + RestAPI” mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally, let’s see how to define the WAF WebACL with a rule that checks the x-origin-header verification header:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;You can follow &lt;a href="https://github.com/ziedbentahar/flexible-deployment-with-lambda-web-adapter/blob/main/infra/lib/WebAppDeployment.ts" rel="noopener noreferrer"&gt;this link&lt;/a&gt; for the complete WebAppDeployment construct definition&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexible deployment in action
&lt;/h2&gt;

&lt;p&gt;Before wapping up, let’s call where-are-you-deployed endpoint defined in the sample web app for each strategy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6162%2F1%2AFe8iQkPEecXnjLdudm_V2A.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F6162%2F1%2AFe8iQkPEecXnjLdudm_V2A.png" alt="calling the “where-are-you-deployed” endpoint from Postman"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Lambda web adapter is certainly not the only tool that helps running full-fledged web apps on Lambda functions, but it simplifies their deployment while supporting architectural evolution.&lt;/p&gt;

&lt;p&gt;In this article we have seen how to build a CDK construct that offers a way to deploy the same web app on two distinct platforms, we can choose Lambda function or ECS Fargate by specifying a configuration during design time. We can extend this further by enabling the system to automatically redeploy itself, during runtime, on a specific target based on some events or CloudWatch alarms !&lt;/p&gt;

&lt;p&gt;As always, you can find the full code source, ready to be adapted and deployed here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/flexible-deployments-with-lambda-web-adapter" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/flexible-deployments-with-lambda-web-adapter&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading and hope you enjoyed it !&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/restrict-access-to-load-balancer.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Restricting access to Application Load Balancers&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/awslabs/aws-lambda-web-adapter" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - awslabs/aws-lambda-web-adapter: Run web applications on AWS Lambda&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aws-samples/amazon-cloudfront-waf-secretsmanager" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - aws-samples/amazon-cloudfront-waf-secretsmanager: Enhance Amazon CloudFront Origin…&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>lambdawebadapter</category>
      <category>lambda</category>
      <category>fargate</category>
      <category>flexiblearchitecture</category>
    </item>
    <item>
      <title>AI powered video summarizer with Amazon Bedrock and Anthropic’s Claude</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Wed, 03 Jan 2024 10:55:25 +0000</pubDate>
      <link>https://dev.to/aws-builders/ai-powered-video-summarizer-with-amazon-bedrock-and-anthropics-claude-2j04</link>
      <guid>https://dev.to/aws-builders/ai-powered-video-summarizer-with-amazon-bedrock-and-anthropics-claude-2j04</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--brAzGI_O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6942/0%2Au1aoh6IkniSqBqg8" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--brAzGI_O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6942/0%2Au1aoh6IkniSqBqg8" alt="Photo by [Andy Benham](https://unsplash.com/@benham3160?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At times, I find myself wanting to quickly get a summary of a video or capture the key points of a tech talk. Thanks to the capabilities of generative AI, achieving this is entirely possible with minimal effort.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk you through the process of creating a service that summarizes YouTube videos based their transcripts and generates audio from these summaries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--c2e9fPe3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5244/1%2Ao2c9LbDWn59VZaz-GfscIA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--c2e9fPe3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5244/1%2Ao2c9LbDWn59VZaz-GfscIA.png" alt="AI powered youtube video summarizer" width="800" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll leverage Anthropic’s Claude 2.1 foundation model through Amazon Bedrock for summary generation, and Amazon Polly to synthesize speech from these summaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;I will use a step functions to orchestrate the different steps involved in the summary and audio generation :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aHxEXjJ---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4494/1%2AvW39v-yN0WNZJoHiOVfHwA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aHxEXjJ---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4494/1%2AvW39v-yN0WNZJoHiOVfHwA.png" alt="AI powered youtube video summarizer architecture" width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔍 Let’s break this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;Get Video Transcript&lt;/strong&gt; function retrieves the transcript from a specified YouTube video URL. Upon successful retrieval, the transcript is stored in an S3 bucket, ready for processing in the next step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generate Model Parameters&lt;/strong&gt; function retrieves the transcript from the bucket and generates the prompt and inference parameters specific to Anthropic’s Claude v2 model. These parameters are then stored in the bucket for use by the Bedrock API in the subsequent step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Invoking the Bedrock API is achieved through the step functions’ AWS SDK integration, enabling the execution of the model inferences with inputs stored in the bucket. This step generates a structured JSON containing the summary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generate audio form summary&lt;/strong&gt; relies on Amazon Polly to perform speech synthesis from the summary produced in the previous step. This step returns the final output containing the video summary in text format, as well as a presigned URL for the generated audio file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The bucket serves as a state storage used across all the steps of the state machine. In fact, we don’t know the size of generated video transcript upfront; it might reach the Step Functions’ payload size limit of 256 KB in some lengthy videos.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  On using Anthoropic’s Claude 2.1
&lt;/h3&gt;

&lt;p&gt;At the time of writing, Claude 2.1 model supports 200K tokens, an estimated word count of 150K. It provides also a good accuracy over long documents, making it well-suited for summarizing lengthy video transcripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;You will find the complete source code here 👇&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/yt-video-summarizer-with-bedrock"&gt;&lt;strong&gt;GitHub - ziedbentahar/yt-video-summarizer-with-bedrock&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will use NodeJs, typescript and CDK for IaC.&lt;/p&gt;
&lt;h2&gt;
  
  
  Solution details
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1- Enabling Anthropic’s Claude v2 in your account
&lt;/h3&gt;

&lt;p&gt;Amazon Bedrock offers a range of foundational models, including Amazon Titan, Anthropic’s Claude, Meta Llama2, etc., which are accessible through Bedrock APIs. By default, these foundational models are not enabled; they must be enabled through the console before use.&lt;/p&gt;

&lt;p&gt;We’ll request access to Anthropic’s Claude models. But first we’ll need to submit a use case details:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Vaq113Dz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/3200/1%2AWwRSrEGHnqbZ2CCOh-kLeA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Vaq113Dz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/3200/1%2AWwRSrEGHnqbZ2CCOh-kLeA.png" alt="Request Anthropic’s Claude access" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2- Getting transcripts from Youtube Videos
&lt;/h3&gt;

&lt;p&gt;I will rely on &lt;a href="https://github.com/Kakulukian/youtube-transcript"&gt;this lib&lt;/a&gt; for the video transcript extraction (It feels like a cheat code 😉) ; in fact, this library makes use of an unofficial YouTube API without relying on a headless Chrome solution. For now, it yields good results on several YouTube videos, but I might explore a more robust solutions in the future :&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The extracted transcript is then stored on the s3 bucket using &lt;code&gt;${requestId}/transcript&lt;/code&gt; as a key.&lt;/p&gt;

&lt;p&gt;You can find the code for this lambda function &lt;a href="https://github.com/ziedbentahar/yt-video-summarizer-with-bedrock/blob/main/src/lambda-handlers/get-youtube-video-transcript.ts"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3- Finding the adequate prompt and generating model inference parameters
&lt;/h3&gt;

&lt;p&gt;At the time of writing, Bedrock currently only supports Claude’s Text Completions API. Prompts must be wrapped in &lt;code&gt;\n\nHuman: and \n\nAssistant:&lt;/code&gt; markers  to let Claude understand the conversation context.&lt;/p&gt;

&lt;p&gt;Here is the prompt; I find that it produces good results for our use case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    You are a video transcript summarizer.
    Summarize this transcript in a third person point of view in 10 sentences.
    Identify the speakers and the main topics of the transcript and add them in the output as well.
    Do not add or invent speaker names if you not able to identify them.
    Please output the summary JSON format conforming to this JSON schema:
    {
      "type": "object",
      "properties": {
        "speakers": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "topics": {
          "type": "string"
        },
        "summary": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    }

    &amp;lt;transcript&amp;gt;{{transcript}}&amp;lt;/transcript&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;🤖 &lt;strong&gt;Helping Claude producing  good results&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;To clearly mark to the transcript to summarize, we use &lt;strong&gt;&lt;/strong&gt; XML tags. &lt;a href="https://docs.anthropic.com/claude/docs/constructing-a-prompt#mark-different-parts-of-the-prompt"&gt;Claude will specifically focus&lt;/a&gt; on the structure encapsulated by these XML tags. I will be substituting &lt;strong&gt;&lt;code&gt;{{transcript}}&lt;/code&gt;&lt;/strong&gt; string with the actual video transcript.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To assist Claude in generating a reliable JSON output format, I include in the prompt the JSON schema that needs to be adhered to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, I also need to inform Claude that I want to generate only a concise JSON response without unnecessary chattiness, meaning without including a preamble and postscript while returning the JSON payload:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\n\nHuman:{{prompt}}\n\nAssistant:{
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note that the full prompt ends with a trailing  &lt;strong&gt;&lt;code&gt;{&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As mentioned on the section above, we will store this generated prompt as well as the model parameters in the bucket so that It can be used as an input of Bedrock API:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      const modelParameters = {
        prompt,
        max_tokens_to_sample: MAX_TOKENS_TO_SAMPLE,
        top_k: 250,
        top_p: 1,
        temperature: 0.2,
        stop_sequences: ["Human:"],
        anthropic_version: "bedrock-2023-05-31",
      };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can follow &lt;a href="https://github.com/ziedbentahar/yt-video-summarizer-with-bedrock/blob/main/src/lambda-handlers/generate-model-parameters.ts"&gt;this link&lt;/a&gt; for the full code of the generate-model-parameters lambda function.&lt;/p&gt;
&lt;h3&gt;
  
  
  4-  Invoking Claude Model
&lt;/h3&gt;

&lt;p&gt;In this step, we’ll avoid writing custom lambda function to invoke Bedrock API. Instead, we’ll use Step functions direct SDK integration. This state loads from the bucket the model inference parameters that were generated in the previous step:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;☝️ Note:&lt;/strong&gt; As we instructed Claude to generate the response in JSON format, the completion API response misses a leading &lt;strong&gt;{&lt;/strong&gt; as Claude outputs the rest of the requested JSON schema.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html"&gt;intrinsic functions&lt;/a&gt; on the state’s ResultSelector to add the missing opening curly brace and to format the state output in a well formed JSON payload :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ResultSelector: {
      "id.$": "$$.Execution.Name",
      "summaryTaskResult.$":
        "States.StringToJson(States.Format('\\{{}', $.Body.completion))",
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I have to admit, it is not ideal but this helps get by without writing a custom Lambda function.&lt;/p&gt;
&lt;h3&gt;
  
  
  5-  Generating audio from video summary
&lt;/h3&gt;

&lt;p&gt;This step is heavily inspired by this  &lt;a href="https://levelup.gitconnected.com/building-a-serverless-text-to-speech-application-with-amazon-polly-step-functions-and-websocket-56e9871730b7"&gt;previous blog post&lt;/a&gt;. Amazon Polly generates the audio from the video summary:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;Here are the details of synthesize function:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Once the audio generated, we store it on the S3 bucket and we generate a presigned Url so it can be downloaded afterwards.&lt;/p&gt;

&lt;p&gt;☝️ &lt;strong&gt;On language detection :&lt;/strong&gt; In this example, I am not performing language detection; by default, I am assuming that the video is in English. You can find in &lt;a href="https://medium.com/gitconnected/building-a-serverless-text-to-speech-application-with-amazon-polly-step-functions-and-websocket-56e9871730b7"&gt;my previous article&lt;/a&gt; how to perform such a process in speech synthesis. Alternatively, We can also leverage Claude model capabilities to detect the language of the transcript.&lt;/p&gt;

&lt;h3&gt;
  
  
  6- Defining the state machine
&lt;/h3&gt;

&lt;p&gt;Alright, let’s put it all together and let’s take a look at the CDK definition of the state machine:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;In order to be able to invoke Bedrock API, we’ll need to add this policy to the workflow’s role (And it’s important to remember granting the S3 bucket read &amp;amp; write permissions to the state machine):&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


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

&lt;p&gt;I find creating generative AI based applications to be a fun exercise, I am always impressed by how quickly we can develop such applications by combining Serverless and Gen AI.&lt;/p&gt;

&lt;p&gt;Certainly, there is room for improvement to make this solution production-grade. This workflow can be integrated into a larger process, allowing the video summary to be sent asynchronously to a client, and let’s not forget robust error handling.&lt;/p&gt;

&lt;p&gt;Follow &lt;a href="https://github.com/ziedbentahar/yt-video-summarizer-with-bedrock/tree/main"&gt;this link&lt;/a&gt; to get the source code for this article.&lt;/p&gt;

&lt;p&gt;Thanks for reading and hope you enjoyed it !&lt;/p&gt;

&lt;h2&gt;
  
  
  Further readings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.anthropic.com/claude/docs/put-words-in-claudes-mouth"&gt;&lt;strong&gt;Put words in Claude's mouth&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-claude.html"&gt;&lt;strong&gt;Anthropic Claude models&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html"&gt;&lt;strong&gt;What is Amazon Bedrock?&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bedrock</category>
      <category>claude</category>
      <category>generativeai</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Step Functions distributed map and cross account S3 access</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Mon, 23 Oct 2023 16:26:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/step-functions-distributed-map-and-cross-account-s3-access-5fg0</link>
      <guid>https://dev.to/aws-builders/step-functions-distributed-map-and-cross-account-s3-access-5fg0</guid>
      <description>&lt;h2&gt;
  
  
  Update 2025–02–13 : Cross account access is now natively possible when using distributed map. I will update this blog post soon 👌
&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%2Fcdn-images-1.medium.com%2Fmax%2F10368%2F0%2AqBec8rG1JvZXP8bt" 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%2Fcdn-images-1.medium.com%2Fmax%2F10368%2F0%2AqBec8rG1JvZXP8bt" alt="Photo by [Marcello Gennari](https://unsplash.com/@marcello54?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step Functions distributed map is a powerful feature that helps building highly parallel serverless data processing workflows. It has a good integration with S3 where it enables the processing of millions of objects in an efficient way.&lt;/p&gt;

&lt;p&gt;This feature relies on the “Distributed” mode of the Map State in order to process, in parallel, a list of S3 Objects in the bucket:&lt;/p&gt;

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

&lt;p&gt;However, at the time of writing, the ItemReader step of the Map state does not support S3 buckets that are on other or accounts or regions:&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%2Fcdn-images-1.medium.com%2Fmax%2F3820%2F1%2A_jaOHP3mpOEXLbFmqNs-mw.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%2Fcdn-images-1.medium.com%2Fmax%2F3820%2F1%2A_jaOHP3mpOEXLbFmqNs-mw.png" alt="[Link to the ItemReader documentation](https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemreader.html#itemreader-iam-policies)" width="800" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we will see how to work around this limitation. In fact, many solutions are possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Using S3 bucket replication: We can replicate the source S3 bucket and sync it with a bucket in the target account where we want to run the distributed map job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Another solution is to initiate the workflow with an initial step. This step synchronously lists the keys of objects in the source bucket and subsequently writes this list to an intermediate bucket in the target account. This file is then configured as the data source for the distributed map.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alternatively, a third solution similar to the second one, involves configuring an S3 inventory on the source bucket and using it to get the list of the keys.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article we will focus on the second solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&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.amazonaws.com%2Fuploads%2Farticles%2Fhasooj3r2oly41t33s61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhasooj3r2oly41t33s61.png" alt="Solution overview" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔍 Here are the relevant parts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Both of the Lambda functions “List objects in source bucket” and “Process objects” require cross-account access to the S3 bucket on the source account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“List objects in source bucket” Lambda function uses S3 &lt;code&gt;ListObjectsV2&lt;/code&gt; to get the list of the keys in the source bucket. It writes that list in JSON format in the “Object keys inventory bucket” in the target account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The map state is configured in «Distributed» mode and uses the JSON file containing the list as the source.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The distributed map state’s iterations run in parallel. Each iteration creates a child execution workflow that invokes the «Process objects» Lambda function with a batch of keys.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;You will find the complete source code here 👇&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/stepfunctions-distributed-map-cross-account-s3-access" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/stepfunctions-distributed-map-cross-account-s3-access&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example I will use NodeJs, typescript and CDK for IaC.&lt;/p&gt;
&lt;h2&gt;
  
  
  Let’s see the code
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1- “List objects in source bucket” and “Process objects“ Lambda functions
&lt;/h3&gt;

&lt;p&gt;The “&lt;strong&gt;List objects in source bucket&lt;/strong&gt;” Lambda function requires two parameters: A prefix, used to list only the keys starting with it, and an output file that will contain the list of keys. These parameters are supplied by the state machine.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The function &lt;code&gt;getKeysFromBucketByPrefix&lt;/code&gt; calls &lt;code&gt;ListObjectsV2&lt;/code&gt;. It iterates through all objects in the bucket that start with the given prefix. The loop continues until there are no more continuation tokens, indicating that all keys have been retrieved. The function then returns the list of keys in an array, which can be written to the "Object keys inventory bucket" by the &lt;code&gt;writeKeysAsJSONIntoBucket&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The &lt;strong&gt;“Process Objects”&lt;/strong&gt; Lambda function will be invoked by the workflow’s map execution with a batch of item keys as its input. The size of this batch is configurable on the distributed map state. In fact, by batching items we can improve performance and reduce cost for large datasets.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;We will need to update the source account’s bucket policy to allows the two Lambda function roles in the target account to perform &lt;code&gt;ListObjectsV2&lt;/code&gt; and &lt;code&gt;GetObject&lt;/code&gt; operations, respectively.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  {
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Principal": {
              "AWS": "arn:aws:iam::&amp;lt;your-account-id&amp;gt;:role/&amp;lt;list-bucket-lambda-role-name&amp;gt;"
          },
          "Action": "s3:ListBucket",
          "Resource": "arn:aws:s3:::&amp;lt;source-bucket-name&amp;gt;"
      },
      {
          "Effect": "Allow",
          "Principal": {
              "AWS": "arn:aws:iam::&amp;lt;your-account-id&amp;gt;:role/&amp;lt;process-object-lambda-role-name&amp;gt;"
          },
          "Action": "s3:GetObject",
          "Resource": "arn:aws:s3:::&amp;lt;source-bucket-name&amp;gt;/*"
      }
  ]
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;⚠️ &lt;strong&gt;️Important:&lt;/strong&gt; Using a Lambda function to list objects from an S3 bucket might not be most cost effective solution when dealing with tens of millions of items. It’s also important to keep in mind the Lambda function’s &lt;strong&gt;15 minutes execution time limit&lt;/strong&gt;. It’s worth exploring alternative solutions such as running the list objects operation as an ECS Task or, as I mentioned on the previous section, configuring and relying on the S3 source bucket inventory.&lt;/p&gt;

&lt;p&gt;You can find the complete CDK definition of these two lambda functions following &lt;a href="https://github.com/ziedbentahar/stepfunctions-distributed-map-cross-account-s3-access/blob/baa5cf7fd2090fc65555df65cc6f43d562b8a124/infra/lib/stepfunctions-distributed-map-cross-account-s3-access-stack.ts#L55" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  2- State machine definition
&lt;/h3&gt;

&lt;p&gt;Alright, let’s have a look into the workflow definition:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;Here, we use the state machine’s execution name property, provided by the context object, &lt;code&gt;$$.Execution.Name&lt;/code&gt;, as the filename to store the list of keys from the source bucket. We also pass the state machine’s input property, &lt;code&gt;$.prefix&lt;/code&gt;, to the “List objects in source bucket” Lambda function.&lt;/p&gt;

&lt;p&gt;At the time of writing, CDK does not provide a native Distributed Map state implementation. We will use &lt;code&gt;CustomState&lt;/code&gt; where we pass the ASL JSON definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We configure the &lt;code&gt;ItemProcessor&lt;/code&gt; in &lt;code&gt;Distributed&lt;/code&gt; Mode.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We set the &lt;code&gt;ItemReader&lt;/code&gt; as a JSON file in the list S3 bucket and we use the &lt;code&gt;$$.Execution.Name&lt;/code&gt; as the Key of the JSON file to read from the bucket.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;☝️ Depending on your use case, you may want to configure the maximum number of concurrent executions as well as the maximum number of items per batch. This will have an impact on the overall execution time of the process.&lt;/p&gt;

&lt;p&gt;You can find &lt;a href="https://github.com/ziedbentahar/stepfunctions-distributed-map-cross-account-s3-access/blob/baa5cf7fd2090fc65555df65cc6f43d562b8a124/infra/lib/stepfunctions-distributed-map-cross-account-s3-access-stack.ts#L194" rel="noopener noreferrer"&gt;here &lt;/a&gt;the full state machine definition.&lt;/p&gt;

&lt;p&gt;Once you execute the state machine, you can monitor the items processing status on the Map Run page:&lt;/p&gt;

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

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

&lt;p&gt;Step Functions distributed map a valuable service to include in your toolkit. In this article, we’ve seen how to use the Step Functions distributed map with S3 buckets that are not in the same account as the state machine.Hopefully, AWS will address this limitation!&lt;/p&gt;

&lt;p&gt;You can find a complete sample application repository here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/stepfunctions-distributed-map-cross-account-s3-access" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/stepfunctions-distributed-map-cross-account-s3-access&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Further readings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/use-dist-map-orchestrate-large-scale-parallel-workloads.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Using Map state in Distributed mode to orchestrate large-scale parallel workloads&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemreader.html#itemreader-iam-policies" rel="noopener noreferrer"&gt;&lt;strong&gt;ItemReader IAM Policies&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>stepfunctions</category>
      <category>distributedmap</category>
      <category>crossaccount</category>
      <category>s3</category>
    </item>
    <item>
      <title>Database schema migrations on AWS with Custom Resources and CDK</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Wed, 14 Jun 2023 18:36:19 +0000</pubDate>
      <link>https://dev.to/aws-builders/database-schema-migrations-on-aws-with-custom-resources-and-cdk-3e9d</link>
      <guid>https://dev.to/aws-builders/database-schema-migrations-on-aws-with-custom-resources-and-cdk-3e9d</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8576%2F0%2A4Yin6EjOtLJ3LYAa" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8576%2F0%2A4Yin6EjOtLJ3LYAa" alt="Photo by [Tomas Kirvėla](https://unsplash.com/ja/@tomkirvela?utm_source=medium&amp;amp;utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With AWS &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html" rel="noopener noreferrer"&gt;Custom Resources&lt;/a&gt; you can manage resources that are not natively supported by CloudFormation. You can execute application-specific provisioning logic as well as custom code during the deployment, the update or the deletion of a Stack. &lt;/p&gt;

&lt;p&gt;In this article, we will focus on using Custom Resources to handle schema migrations on an Aurora Postgres database. We will create a custom database migration resource that executes schema changes during the Stack deployment. To accomplish this, we will associate a Lambda function with our Custom Resource, this function ensures that any new changes to the database are automatically applied when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;We’ll use Aurora Serverless V2 with Postgres engine, NodeJs and typescript for the Lambda function code and CDK for the IaC:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4458%2F1%2ARaSEXAwy2Cvk0uWEbL3ouA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4458%2F1%2ARaSEXAwy2Cvk0uWEbL3ouA.png" alt="DB schema migrations using custom resources -Solution overview "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the details of this solution: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We’ll create the &lt;a href="https://github.com/ziedbentahar/db-schema-migration-with-custom-resources/blob/6d783560e1fedeadbfa7fb039d45c1b38e4485eb/lib/database.ts#L60" rel="noopener noreferrer"&gt;database Cluster&lt;/a&gt; on an isolated subnet as well as a secret that stores the credentials of the database. This secret is accessed by the DB schema migration Lambda function .&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;DB schema migration Lambda&lt;/strong&gt; &lt;strong&gt;function&lt;/strong&gt; is responsible for executing the necessary database schema changes. It uses the “&lt;a href="https://salsita.github.io/node-pg-migrate/#/" rel="noopener noreferrer"&gt;node-pg-migrate&lt;/a&gt;” tool, which allows us to run migration scripts programmatically. We’ll need to configure this lambda function to access the Aurora resource in our VPC.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migration scripts&lt;/strong&gt; files are included in the zip package of the lambda function : each file contains a set of changes to apply to the database, these files are stored in the same repository as the application. Each new set of changes need to be written into a new distinct file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DB migration custom resource&lt;/strong&gt; invokes the lambda function during the stack deployment when it detects changes on the migration scripts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;You can find the complete repo of this solution here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/db-schema-migration-with-custom-resources" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/db-schema-migration-with-custom-resources&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s see the code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1 —DB schema migration lambda
&lt;/h3&gt;

&lt;p&gt;As mentioned above, we will use “&lt;a href="https://salsita.github.io/node-pg-migrate/#/" rel="noopener noreferrer"&gt;node-pg-migrate&lt;/a&gt;” to run schema changes. &lt;/p&gt;

&lt;p&gt;One interesting aspect of this library is its flexibility in defining migration scripts: We have the option to write our migration scripts in either ES or TypeScript, allowing us to define database schemas with code. Alternatively, we can also define migration scripts as plain SQL files, providing a more traditional approach to managing database schema changes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2A0Xfd8Ez_KGdNB846gUxyrQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2A0Xfd8Ez_KGdNB846gUxyrQ.png" alt="Example of a migration script directory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This code below shows how to use node-pg-migrate to run the migration scripts from the custom resource lambda function:👇&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
☝️ &lt;strong&gt;Note&lt;/strong&gt;: This lib can handle forward and backward migrations but with  our solution we will only be supporting forward migrations.

&lt;p&gt;Here is CDK definition of this lambda function:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
This Lambda Function requires &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html" rel="noopener noreferrer"&gt;VPC access&lt;/a&gt; as it needs to access the Aurora Database. We will also make sure to place the network interface associated with the Lambda function in a &lt;code&gt;PRIVATE_WITH_EGRESS&lt;/code&gt; subnet as we need to access the secrets manager service. 

&lt;p&gt;Additionally, we’ll associate the security group of the Lambda function to the security group of the Aurora Cluster:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
📦 &lt;strong&gt;On embedding migration scripts:&lt;/strong&gt; In our example, Migration scripts need to be included in the Lambda function package. We use the &lt;a href="https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-lambda-nodejs.ICommandHooks.html" rel="noopener noreferrer"&gt;&lt;code&gt;afterBunding&lt;/code&gt;&lt;/a&gt; hook to copy the content of the migration dir to the bundle output dir:

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;afterBundling: (_, outputDir: string) =&amp;gt; {
  return [
    `mkdir -p ${outputDir}/migrations &amp;amp;&amp;amp; cp ${migrationDirectoryPath}/* ${outputDir}/migrations`,
  ];
},
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;You can find &lt;a href="https://github.com/ziedbentahar/db-schema-migration-with-custom-resources/blob/6d783560e1fedeadbfa7fb039d45c1b38e4485eb/lib/database.ts#L101" rel="noopener noreferrer"&gt;here&lt;/a&gt; the complete definition of this Lambda Function.&lt;/p&gt;
&lt;h3&gt;
  
  
  2 —Defining the custom resource:
&lt;/h3&gt;

&lt;p&gt;Straightforward to define with  CDK:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;☝️ One important note&lt;/strong&gt;: the &lt;code&gt;computeHash&lt;/code&gt; function computes a hash of the migration script directory content. This hash is passed as a property of the custom resource. During the stack deployment, whenever this hash changes, the lambda function gets invoked and the new migrations scripts are taken into account.&lt;/p&gt;

&lt;p&gt;You will find &lt;a href="https://github.com/ziedbentahar/db-schema-migration-with-custom-resources/blob/6d783560e1fedeadbfa7fb039d45c1b38e4485eb/lib/database.ts#L185" rel="noopener noreferrer"&gt;here&lt;/a&gt; the definition of the Custom::DbSchemaMigration custom resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  3-Putting all together
&lt;/h3&gt;

&lt;p&gt;And here is how we use this Database construct that supports schema migration:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Et voilà!  Once you make a deployment with new migration script files, you can see the DB schema migration in action via the Lambda CloudWatch logs:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4136%2F1%2AWPgPcsyHRol84DZYmu_PsA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4136%2F1%2AWPgPcsyHRol84DZYmu_PsA.png" alt="Db schema migration logs — Lambda logs"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In this article, we have seen how to use custom resources to run database schema migrations on AWS. CDK makes it a breeze ! You can find a complete sample application repository with the complete github action workflow here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/db-schema-migration-with-custom-resources" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/db-schema-migration-with-custom-resources&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hope you find it useful, and thanks for reading !&lt;/p&gt;

&lt;h2&gt;
  
  
  Further readings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cdk/api/v1/docs/custom-resources-readme.html" rel="noopener noreferrer"&gt;&lt;strong&gt;@aws-cdk/custom-resources module · AWS CDK&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Configuring a Lambda function to access resources in a VPC&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://salsita.github.io/node-pg-migrate/#/" rel="noopener noreferrer"&gt;&lt;strong&gt;node-pg-migrate - Postgresql database migration management tool for node.js&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cdk</category>
      <category>customresources</category>
      <category>database</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Serverless Asynchronous REST APIs on AWS</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Thu, 20 Apr 2023 10:32:59 +0000</pubDate>
      <link>https://dev.to/aws-builders/serverless-asynchronous-rest-apis-on-aws-5cji</link>
      <guid>https://dev.to/aws-builders/serverless-asynchronous-rest-apis-on-aws-5cji</guid>
      <description>&lt;p&gt;&lt;a href="https://medium.com/gitconnected/building-a-serverless-text-to-speech-application-with-amazon-polly-step-functions-and-websocket-56e9871730b7" rel="noopener noreferrer"&gt;In a previous article&lt;/a&gt;, we explored how to use the API Gateway WebSocket API to provide real-time, asynchronous feedback and responses to its clients. While WebSocket APIs can be an excellent option to build real-time asynchronous APIs between a browser and a server, there are situations where asynchronous REST APIs are a better fit. For example, if you need to ensure compatibility with existing architectures or integrating with backend systems where setting up a messaging system like a queue or an event bus is not an option.&lt;/p&gt;

&lt;p&gt;This article provides a guide on how to build a Serverless Async Request/Response REST API on AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;First, Let’s design the API&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s consider an application that lets clients perform long running tasks through HTTP requests, these tasks might require a long amount of time to complete (&lt;em&gt;e.g.&lt;/em&gt; text-to-speech or audio-to-text tasks, report generation, etc.). In order to keep clients informed about the status of their requests, we’ll need to create a separate status endpoint that allows clients to poll at regular intervals the current processing status until the task is finished.&lt;/p&gt;

&lt;p&gt;As an example, to initiate the task creation, clients &lt;code&gt;POST&lt;/code&gt; this request:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /task
{
    "someProperty": "someValue",
    "anotherPropety": "anotherValue",
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The API should then return a &lt;code&gt;HTTP 202 Accepted&lt;/code&gt; response with a body containing a URI that references the task :&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 202 (Accepted)

{
 "task": {
  "href": "/task/&amp;lt;taskId&amp;gt;",
  "id": "&amp;lt;taskId&amp;gt;"
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Clients can then poll that endpoint to retrieve the status of the task. The API should respond with a status corresponding to the current state of the task, such as &lt;code&gt;inprogress&lt;/code&gt;, &lt;code&gt;complete&lt;/code&gt; , &lt;code&gt;error&lt;/code&gt;. Additionally, the response payload should reflect to the specific status of the task.&lt;/p&gt;

&lt;p&gt;Here is an example of a request with a response containing the payload of a task that is in progress:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /task/&amp;lt;taskId&amp;gt;

HTTP 200 (OK)

{
    "status": "inProgress",
    "taskCreationDate": "2023-04-04T19:23:42",
    "requestData": {...},
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;For server-to-server scenarios, we’ll add a callback/webhook mechanism: When the task completes, the server updates the task state and sends &lt;code&gt;POST&lt;/code&gt; request to a callback URL that the client provides on the initial task creation request.&lt;/p&gt;

&lt;p&gt;One solution is to include the callback URL in the header of the request so that the API use it to send the response payload:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /task
Callback: https://foo.example.com/some-callback-resource

{
    "someProperty": "someValue",
    "anotherPropety": "anotherValue",
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; Security and reliability are important aspects to be taken into account when using callback URLs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: It is important to verify that the callback URLs belong to the client who initiated the requests. We also need to ensure that the callback destination server receives legitimate data from the source server, rather than forged information from malicious actors attempting to spoof the webhook. This can be done by requiring clients to use API Keys when making requests and on the server side associating these keys with well-known callback URLs that the clients must provide. In addition, &lt;a href="https://prismatic.io/blog/how-secure-webhook-endpoints-hmac/" rel="noopener noreferrer"&gt;HMAC signature verification&lt;/a&gt; can be used to authenticate and validate the payloads of API callbacks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failure handling&lt;/strong&gt;: It’s important to account for scenarios where the client may be unavailable or experiencing intermittent faults resulting in errors. To address this, we’ll need to implement a retry mechanism that includes a dead-letter queue. This mechanism allows failed deliveries to be stored in the queue and resent at a later time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alright, let’s see how to implement this on AWS.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;In this architecture, my goal was to use AWS integrations wherever possible to minimize the amount of glue code that needs to be written and maintained. This approach not only saves time and effort, but also helps ensure the scalability of the API:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;🔍 Breaking this down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We create direct AWS integration between the API Gateway RestAPI and the DynamoDb table. In this task table we store the context of the request: the client id, the request payload, the task status and the task result once available. This table gets updated by the task processing workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since we’re using API Gateway as a proxy for the DynamoDB table, we rely on two Lambda authorizers to authenticate API calls. The first authorizer verifies client request headers: The client’s API key and the callback URL, to authorize &lt;code&gt;POST /task&lt;/code&gt;&lt;br&gt;
requests. The second authorizer is dedicated to &lt;code&gt;GET /task/&amp;lt;taskId&amp;gt;&lt;/code&gt; route.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To trigger the task workflow, we rely on the table’s DynamoDb Streams. We connect the stream to the task’s state machine by using EventBridge Pipes. This Pipe selects only the newly created tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The task is then run and coordinated by a Step function workflow. The status of the task is also updated in this workflow by direct AWS SDK integration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once the task is complete, its state gets updated and the result payload gets written into the task table. We use another EventBridge Pipe to trigger the sending of the callback to the client from the stream of the completed tasks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;☝️ Note :&lt;/strong&gt; In this architecture, Instead of using the &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html" rel="noopener noreferrer"&gt;Event Bridge API destinations&lt;/a&gt;, we’ll write a custom lambda to send the callbacks. I did this to achieve better flexibility and control over how the task result payload is sent to the clients. For example, a client may require multiple registered callback URLs. To accomplish this, we use an EventBus as the target destination for the output Pipe.&lt;/p&gt;
&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;You will find the complete source code with the complete deployment pipeline here 👇&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/async-rest-api-on-aws" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/async-rest-api-on-aws&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example I will use nodejs, typescript and CDK for IaC.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Let’s see the code&lt;/strong&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;1- API Gateway, creating the long running task&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Let’s zoom into the &lt;code&gt;POST /task&lt;/code&gt; integration with the DynamoDb task table. We associate this route with an authorizer that requires two headers to be present in the request: &lt;code&gt;Authorization&lt;/code&gt; and &lt;code&gt;Callback&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the request is authorized, this integration maps the request payload to a task entry. We use the API gateway &lt;code&gt;$context.requestId&lt;/code&gt; as the identifier of the task. Additionally, we map &lt;code&gt;$context.authorizer.principalId&lt;/code&gt; (in our case the client Id) in order to be used down the line. And we extract the request callback URL from the header using &lt;code&gt;$util.escapeJavascript($input.params(‘callback’)&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
When the task is successfully added to the table, we return &lt;code&gt;HTTP 202 Accepted&lt;/code&gt; response with a body containing the task id as well as a reference pointing to the task: by using &lt;code&gt;$context.path/$context.requestId&lt;/code&gt; which translates to this URI &lt;code&gt;/&amp;lt;stage-name&amp;gt;/task/&amp;lt;task-id&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can find the complete CDK definition of the API Gateway here.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2- ‘Create Task’ authorizer Lambda&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;As mentioned above, this authorizer validates the API key and checks whether the callback URL is associated with the API Key that identifies the client:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
Here, I do not dive into the implementation of &lt;code&gt;validateTokenWithCallbackUrl&lt;/code&gt;, but this is something that can be delegated to another service that owns the responsibility to do the proper checks.

&lt;h3&gt;
  
  
  &lt;strong&gt;3- Defining the ‘create task’ EventBridge Pipe&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This EventBridge Pipe selects the INSERT events from the DynamoDb stream: The newly created tasks will trigger new state machine executions.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
We’ll to set the invocation type to &lt;code&gt;FIRE_AND_FORGET&lt;/code&gt; as the execution of this state machine is asynchronous.

&lt;p&gt;We’ll also need to set these target and destination policies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
When deployed, this Pipe will look like this on the AWS console:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5440%2F1%2Al2hUdKADQzqS7FAdeA9eQA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5440%2F1%2Al2hUdKADQzqS7FAdeA9eQA.png" alt="DynamoDb stream to step function Pipe"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4- Defining the ‘handle completed task’ EventBridge Pipe&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We apply the same principle as in the previous section. However, this Pipe selects the completed tasks from the DynamoDb stream, transforms them, and subsequently forwards them to the EventBus:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
We’ll need to attach this role to this Pipe:&lt;br&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
After deployment, the Pipe resources looks like this in the console:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5440%2F1%2AaFqxBtMlQXioLLXwLCx7pQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5440%2F1%2AaFqxBtMlQXioLLXwLCx7pQ.png" alt="DynamoDb stream to EventBus Pipe"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5- Sending the Callbacks to the clients&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To send callbacks, we associate an EventBridge rule with the EventBus. This rule matches all the events and defines the ‘&lt;a href="https://github.com/ziedbentahar/async-rest-api-on-aws/blob/main/src/backend/lambdas/callback-lambda.ts" rel="noopener noreferrer"&gt;send callback lambda&lt;/a&gt;’ as the target:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
In some situations, callbacks may fail to be delivered due to temporary unavailability of the client or a network error. We configure EventBridge to retry the operation up to 10 times within a 24-hour time-frame, after which, the message is directed to a dead letter queue to prevent message loss:

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5440%2F1%2AFwDc2FeOUXvjLZeHt0qyJw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5440%2F1%2AFwDc2FeOUXvjLZeHt0qyJw.png" alt="EventBridge rule configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Callback lambda&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The callback lambda is a simple function that computes the HMAC of the message to be sent and then sends a POST request to the target callback URL.&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
In the &lt;code&gt;computeHMAC&lt;/code&gt; function, the &lt;code&gt;clientId&lt;/code&gt; is used to retrieve the key necessary for generating the HMAC signature from the response payload.

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

&lt;p&gt;In this article, we've seen how to create Serverless, asynchronous REST API on AWS. By leveraging API Gateway AWS integrations and EventBridge Pipes, it is possible to build such an API that requires minimal Lambda glue code. This approach not only simplifies the development process, but it also allows greater flexibility and scalability.&lt;/p&gt;

&lt;p&gt;📝 You can find the complete source code and the deployment pipeline here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/async-rest-api-on-aws" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/async-rest-api-on-aws&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading !&lt;/p&gt;

&lt;h2&gt;
  
  
  Further readings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Event retry policy and using dead-letter queues&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply" rel="noopener noreferrer"&gt;&lt;strong&gt;Asynchronous Request-Reply pattern - Azure Architecture Center&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-dynamodb.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Amazon DynamoDB stream as a source&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>apigateway</category>
      <category>serverless</category>
      <category>async</category>
      <category>eventbridgepipe</category>
    </item>
    <item>
      <title>Building an AI powered and Serverless meal planner with OpenAI, AWS Step functions, AWS Lambda and CDK</title>
      <dc:creator>Zied Ben Tahar</dc:creator>
      <pubDate>Mon, 13 Mar 2023 08:16:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-an-ai-powered-and-serverless-meal-planner-with-openai-aws-step-functions-aws-lambda-and-cdk-2phg</link>
      <guid>https://dev.to/aws-builders/building-an-ai-powered-and-serverless-meal-planner-with-openai-aws-step-functions-aws-lambda-and-cdk-2phg</guid>
      <description>&lt;p&gt;OpenAI’s generative capabilities offer new possibilities when building applications. Combined with Serverless technologies, we can create applications faster while still maintaining the flexibility to iterate and to improve them over time.&lt;/p&gt;

&lt;p&gt;In this article, I will show you how to build an application that sends emails containing generated weekly meal plans from a set of ingredients a user provides. We will use OpenAI’s APIs along with AWS Serverless services: Step Functions, AWS Lambda, and Amazon SES.&lt;/p&gt;

&lt;p&gt;We will use NodeJs runtime and typescript for the Lambda code as well as CDK for IaC.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AMLFWDhvi-YJnkegw9fQZ0g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AMLFWDhvi-YJnkegw9fQZ0g.png" alt="logos of the services that were used"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What are we going to build ?
&lt;/h2&gt;

&lt;p&gt;We will create an application that allows users to submit via an API a request containing a set of food ingredients and an email address. It will then, asynchronously, send to the user an email containing a meal plan with detailed recipes for a whole week:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F11034%2F1%2AI-kW3EmeHuj__0br6dsRlw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F11034%2F1%2AI-kW3EmeHuj__0br6dsRlw.png" alt="AI powered meal planner"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the architecture diagram of the application we are going to build:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5648%2F1%2ArE-5meExVJx0CBIaQz4bWQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F5648%2F1%2ArE-5meExVJx0CBIaQz4bWQ.png" alt="AI powered meal planner architecture overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The relevant parts of this solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We use a step function to orchestrate the invocations of Lambda functions that send requests to OpenAI’s APIs to generate recipes from a prompt as well as an image for each recipe.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We use the S3 bucket to store the generated recipe images. These images are served via a CloudFront distribution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The generated meal plan is then sent via email using SES. We use the &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-api.html" rel="noopener noreferrer"&gt;SES templates capability&lt;/a&gt; to send personalized emails for each user.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Rest API gateway has a POST route with an &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-api-gateway.html" rel="noopener noreferrer"&gt;integration&lt;/a&gt; to the meal planner Step Function.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;You can find the full repository with its deployment pipeline here 👇&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/serverless-meal-planner-with-aws-and-openai&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Let’s deep dive into the code&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;☝️ &lt;strong&gt;Before starting&lt;/strong&gt;: In order to use OpenAI APIs, you will need to sign up and to create an API KEY. You can follow &lt;a href="https://platform.openai.com/docs/quickstart/add-your-api-key" rel="noopener noreferrer"&gt;this link&lt;/a&gt; to get started. The use of this API &lt;a href="https://openai.com/pricing" rel="noopener noreferrer"&gt;is not free&lt;/a&gt;, however new accounts get free credits (tokens) to start experimenting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AfQFzKZ0z_-uDvDxavUnenA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AfQFzKZ0z_-uDvDxavUnenA.png" alt="Creating API Keys on OpenAI account"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Defining the state machine
&lt;/h3&gt;

&lt;p&gt;The first step of this state machine is to generate a meal plan for a week. The second step involves generating a picture for each recipe and then saving it on a S3 bucket. The processing is done in parallel for each recipe using a Map state, this has the advantage to reduce the overall execution time of the state machine. And finally, the last important step is the sending of the email containing the meal plan:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2ARH1w9ZORWgleOQysKFSUew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2ARH1w9ZORWgleOQysKFSUew.png" alt="meal planner state machine"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which translates to this fluent state machine definition in CDK:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;You can find the complete CDK definition of the state machine following &lt;a href="https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai/blob/main/infrastructure/lib/state-machine.ts" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining the Lambda functions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1- Generate meal plan Lambda:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The challenging part about this step was to find the best prompt that yields good and consistent results from OpenAI &lt;a href="https://platform.openai.com/docs/api-reference/completions" rel="noopener noreferrer"&gt;completion API&lt;/a&gt;. I used the &lt;code&gt;text-davinci-003&lt;/code&gt; GPT model (also referred as GPT-3.5).&lt;/p&gt;

&lt;p&gt;When I tried out different prompts, the suggestions were quite good for producing interesting meal plans given a list of coherent ingredients. I was even able to request a structured result in JSON format ready to be processed by the Lambda function. I also experimented with parameters such as &lt;a href="https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature" rel="noopener noreferrer"&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://platform.openai.com/docs/api-reference/completions/create#completions/create-top_p" rel="noopener noreferrer"&gt;&lt;code&gt;TopP&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://platform.openai.com/docs/api-reference/completions/create#completions/create-max_tokens" rel="noopener noreferrer"&gt;&lt;code&gt;max_tokens&lt;/code&gt;&lt;/a&gt; searching for the sweet spot that gets satisfying results.&lt;/p&gt;

&lt;p&gt;This prompt produces the best results given our use case:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate a dinner meal plan for the whole week with these ingredients &amp;lt;a comma seperated list of ingredients&amp;gt; and with other random ingredients.
Result must be in json format
Each meal recipe contains a name, a five sentences for instructions and an array of ingredients
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;And here is the code of the Lambda function that handles the generation of the meal plan:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
☝️ &lt;strong&gt;Some notes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;As depicted on the diagram above, The OpenAI API key is stored on a secret. In this example we use the &lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html" rel="noopener noreferrer"&gt;AWS parameters and secrets Lambda extension&lt;/a&gt; to read the secret value from the Lambda. You can learn more about this Lambda extension &lt;a href="https://levelup.gitconnected.com/using-aws-parameters-and-secrets-lambda-extension-e61dd6a41110" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Even though the completion API was providing consistent response models in JSON, for some reason, the properties on the JSON object were not having a consistent casing as I was experimenting with the API. Hence the use of the getProperty helper function before returning the result; this function ensures getting a property value from an object regardless of its casing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2- Generate recipe image Lambda:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This Lambda function is similar to the previous one. We use the recipe name that &lt;code&gt;createCompletion&lt;/code&gt; API has generated in order to create an image from it by calling &lt;a href="https://platform.openai.com/docs/guides/images/introduction" rel="noopener noreferrer"&gt;createImage&lt;/a&gt; (this API uses &lt;a href="https://openai.com/research/dall-e" rel="noopener noreferrer"&gt;DALL-E models&lt;/a&gt; for image generation) :&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;createImage&lt;/code&gt; API returns an array of URLs, the size of this array depends on the &lt;a href="https://platform.openai.com/docs/api-reference/images/create-variation#images/create-variation-n" rel="noopener noreferrer"&gt;number of variation of the image&lt;/a&gt;s we want to generate. In our example we are interested in only one single image. The image URL expires after one hour, here is why we pass it to the &lt;a href="https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai/blob/main/src/backend/lambdas/upload-recipe-image-to-storage.ts" rel="noopener noreferrer"&gt;upload-recipe-image-to-storage&lt;/a&gt; Lambda that has the responsibility to download the image and to store it on a S3 Bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3- Send meal plan email Lambda:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sending of the email uses SES. But first, the Lambda function prepares a template data containing the necessary elements to generate the email:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
&lt;br&gt;
On the section below, we will see how to use CDK to create a new SES email identity as well as the email template that is used to send the mal plan.

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You can find the CDK definitions of these Lambda functions following &lt;a href="https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai/blob/main/infrastructure/lib/lambdas.ts" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Configuring SES
&lt;/h3&gt;

&lt;p&gt;On this example, we use a domain that is already defined in the Route53 public hosted zone; The SES email identity DNS validation is then seamless. We also create the meal plan email template on this nested stack:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;☝️ &lt;strong&gt;Note:&lt;/strong&gt; By default, an SES account is in sandbox mode. You are allowed to send emails only to verified identities and you can only send a limited number of emails per 24-hour period. Follow &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html" rel="noopener noreferrer"&gt;this link&lt;/a&gt; to understand the sandbox mode quotas and how to move out of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating the API Gateway with the Step function workflow
&lt;/h3&gt;

&lt;p&gt;Creating the RestApi with the Step Function integration is quite easy with CDK, although a bit verbose:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;We need to create a role that grants the Api Gateway to &lt;code&gt;states:StartExecution&lt;/code&gt; the Step Function. Each request gets validated with the API gateway JSON schema &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html#models-mappings-models" rel="noopener noreferrer"&gt;model validation&lt;/a&gt; before the execution of the step function.&lt;/p&gt;

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

&lt;p&gt;In this post, we have seen how combining OpenAI APIs with serverless architecture can help building AI-powered applications with minimal setup and configuration. The capabilities of both of these two worlds are great enablers for building MVPs and iterating faster.&lt;/p&gt;

&lt;p&gt;This application can be improved further by taking into account food restrictions or even by creating an AI powered weekly news letter.&lt;/p&gt;

&lt;p&gt;You can find the full source code of this application here:&lt;br&gt;
&lt;a href="https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - ziedbentahar/serverless-meal-planner-with-aws-and-openai&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Further readings&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://platform.openai.com/docs/guides/completion" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenAI API Text Competion&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://platform.openai.com/docs/guides/images" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenAI API Image generation&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-api-gateway.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Creating a Step Functions API Using API Gateway&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Working with models and mapping templates&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-advanced.html" rel="noopener noreferrer"&gt;&lt;strong&gt;Advanced email personalization&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions-readme.html" rel="noopener noreferrer"&gt;&lt;strong&gt;aws-cdk-lib.aws_stepfunctions module · AWS CDK&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>openai</category>
      <category>serverless</category>
      <category>stepfunctions</category>
      <category>cdk</category>
    </item>
  </channel>
</rss>
