<?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: Chigozirim Eke</title>
    <description>The latest articles on DEV Community by Chigozirim Eke (@chigo_e).</description>
    <link>https://dev.to/chigo_e</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3827548%2Ffa4c449d-6e13-4024-9a75-271e3f45d6ec.jpg</url>
      <title>DEV Community: Chigozirim Eke</title>
      <link>https://dev.to/chigo_e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chigo_e"/>
    <language>en</language>
    <item>
      <title>RAG Powered Apps with Amazon Bedrock, Part 2: Automating the RAG Pipeline with Terraform</title>
      <dc:creator>Chigozirim Eke</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:47:33 +0000</pubDate>
      <link>https://dev.to/chigo_e/rag-powered-apps-with-amazon-bedrock-part-2-automating-the-rag-pipeline-with-terraform-4po4</link>
      <guid>https://dev.to/chigo_e/rag-powered-apps-with-amazon-bedrock-part-2-automating-the-rag-pipeline-with-terraform-4po4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before you start:&lt;/strong&gt; This picks up where Part 1 left off. In Part 1, we walked through setting up a Bedrock Knowledge Base manually through the AWS Console.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction &amp;amp; Motivation
&lt;/h2&gt;

&lt;p&gt;I started this project with a simple goal: build a reusable Terraform setup that could deploy the infrastructure behind a "Chat with PDF" application end to end.&lt;/p&gt;

&lt;p&gt;When Amazon Bedrock launched, I immediately started experimenting. Like many early GenAI workflows, my first few prototypes were built directly through the AWS Console. The console experience is great for quickly understanding how the pieces fit together, but once I started iterating, I wanted something more repeatable.&lt;/p&gt;

&lt;p&gt;I wanted to be able to spin environments up, tear them down, and quickly test different architecture decisions. For example, I wanted to compare the cost differences between using S3 Vectors and OpenSearch as the vector store without manually rebuilding the infrastructure every time.&lt;/p&gt;

&lt;p&gt;At the time, I couldn't find a Terraform module that covered the complete RAG pipeline I was looking for: S3, Bedrock Knowledge Bases, OpenSearch Serverless, Lambda, IAM, and all the glue in between.&lt;/p&gt;

&lt;p&gt;So I built my own. Partly because I needed it, and partly because understanding how these services connect together is the best way to learn.&lt;/p&gt;




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

&lt;p&gt;A couple of terraform modules to automate everything we clicked through manually in Part 1. One &lt;code&gt;terraform apply&lt;/code&gt; brings up the full stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 Bucket&lt;/strong&gt;: your document store. Encrypted at rest, versioning on, zero public access.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSearch Serverless&lt;/strong&gt;: the vector database. Stores the embeddings Bedrock generates during ingestion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock Knowledge Base&lt;/strong&gt;: orchestrates the chunking, embedding, and storage of documents, and retrieval at query time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion Lambda&lt;/strong&gt;: triggered automatically when you upload a file to S3. Starts a Bedrock ingestion job so documents are chunked, embedded, and indexed without ClickOps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query Lambda&lt;/strong&gt;: accepts a natural language question, calls &lt;code&gt;RetrieveAndGenerate&lt;/code&gt;, and returns an answer with source citations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full source code + ReadMe: &lt;a href="https://github.com/coolchigi/rag-bedrock-project" rel="noopener noreferrer"&gt;Bedrock Project&lt;/a&gt;. If you run into issues or want to extend the module, feel free to open an issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flreeyqbqfifppubabz83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flreeyqbqfifppubabz83.png" alt="Bedrock RAG Architecture Diagram" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rag-bedrock-project/
├── main.tf
├── variables.tf
├── outputs.tf
├── backend.tf
├── terraform.tfvars.example
├── bootstrap/
└── modules/
    ├── storage/
    ├── opensearch/
    ├── bedrock/
    └── lambda/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each module owns one piece of the infrastructure and exposes only what other modules need through outputs. The root &lt;code&gt;main.tf&lt;/code&gt; connects everything by passing outputs from one module as inputs to another.&lt;/p&gt;

&lt;p&gt;Multiple modules might feel like overkill, but I learned early in my development journey that clear boundaries make systems easier to reason about. Each module owns one responsibility, which makes debugging much easier when something breaks.&lt;/p&gt;

&lt;p&gt;The Lambda module does not need to know how OpenSearch is configured. It only receives the IDs and values it needs through variables.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Bootstrap Remote State First
&lt;/h3&gt;

&lt;p&gt;Before running &lt;code&gt;terraform apply&lt;/code&gt; on anything, you need somewhere to store your Terraform state.&lt;/p&gt;

&lt;p&gt;If you're new to Terraform, state is how Terraform keeps track of the infrastructure it manages. Every resource it creates is recorded in a &lt;code&gt;terraform.tfstate&lt;/code&gt;file so future runs know what already exists.&lt;/p&gt;

&lt;p&gt;Keeping state locally ties it to your machine. This project uses S3 remote state so Terraform has a durable, shared source of truth. With Terraform 1.10, native S3 state locking removes the need for a separate DynamoDB table.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bootstrap/&lt;/code&gt; directory provisions the resources needed for Terraform's remote backend and deployment permissions. Run the Terraform commands below once before deploying the main stack.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All commands use &lt;a href="https://github.com/99designs/aws-vault" rel="noopener noreferrer"&gt;aws-vault&lt;/a&gt;, which stores AWS credentials in your OS keychain and injects temporary credentials at runtime. The &lt;code&gt;--no-session&lt;/code&gt; flag skips STS session tokens, which some IAM operations reject. Ignore this if you're not using aws-vault&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bootstrap init

aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bootstrap apply &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"project_name=my-rag"&lt;/span&gt; &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"environment=dev"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bootstrap creates two things: the S3 state bucket and a &lt;strong&gt;scoped deployer IAM policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The initial bootstrap requires &lt;code&gt;AdministratorAccess&lt;/code&gt; because the deployer policy does not exist yet. Once bootstrap completes, attach the generated policy to your IAM user and remove &lt;code&gt;AdministratorAccess&lt;/code&gt;. From that point forward, deployments run with least-privilege permissions.&lt;/p&gt;

&lt;p&gt;The bootstrap step outputs the S3 backend configuration and deployer policy ARN. Copy the backend configuration into &lt;code&gt;backend.tf&lt;/code&gt;, then initialize Terraform again to migrate state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then swap to the scoped policy (this can also be done through the AWS Console):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Attach the deployer policy&lt;/span&gt;
aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; aws iam attach-user-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-name&lt;/span&gt; YOUR_IAM_USER &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; YOUR_DEPLOYER_POLICY_ARN

&lt;span class="c"&gt;# Drop AdministratorAccess&lt;/span&gt;
aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; aws iam detach-user-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-name&lt;/span&gt; YOUR_IAM_USER &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::aws:policy/AdministratorAccess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 2: Configure Your Variables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;terraform.tfvars.example terraform.tfvars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module only requires a few inputs. The setting worth paying attention to is &lt;code&gt;embedding_dimensions&lt;/code&gt; because it controls the size of the vectors stored in OpenSearch.&lt;/p&gt;

&lt;p&gt;For this development environment, we use &lt;code&gt;512&lt;/code&gt; dimensions. It reduces storage costs while keeping retrieval quality acceptable. We'll revisit this choice when looking at the Bedrock Knowledge Base configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;embedding_dimensions&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;  &lt;span class="c1"&gt;# 256 or 512, half the storage cost vs 1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;First time using Bedrock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bedrock subscribes your account to a foundation model automatically the first time you invoke it, but only if the identity making that first call has the AWS Marketplace permissions (&lt;code&gt;aws-marketplace:ViewSubscriptions&lt;/code&gt; and &lt;code&gt;aws-marketplace:Subscribe&lt;/code&gt;) to finish the subscription. There is a setup window of up to 15 minutes where your calls may even succeed while the subscription is still being finalized in the background.&lt;/p&gt;

&lt;p&gt;Here is the trap I fell into. My first invocation came from the scoped Lambda role, which does not carry those Marketplace permissions. Calls worked for a few minutes, then every request started failing with an access error once the subscription failed to complete. The fix is a one-time step: subscribe the model once using an identity that does have Marketplace permissions, such as your admin user, either by accepting the model agreement in the Bedrock console or by invoking the model once. The subscription is account-wide, so you do it once rather than per region. After that, the Lambda role needs only &lt;code&gt;bedrock:InvokeModel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One extra step for Anthropic models like Claude: before that first invocation you also have to submit a one-time First Time Use form describing your use case. You do it once per account, or once at the organization's management account, from the model catalog in the Bedrock console or with the &lt;code&gt;PutUseCaseForModelAccess&lt;/code&gt; API. Access is granted as soon as the form goes through.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Lambda The Query Handler and the Trigger
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/coolchigi/rag-bedrock-project/tree/main/modules/lambda" rel="noopener noreferrer"&gt;Lambda Module&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lambda connects the application workflow together. There are two functions with both running Node.js 20 on ARM64. ARM64 (Graviton) is cheaper to run than &lt;code&gt;x86&lt;/code&gt; for the same memory allocation, and since these functions are mostly waiting on AWS services rather than doing heavy computation, there was no reason to pay the x86 premium.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;ingestion handler&lt;/strong&gt; (&lt;code&gt;ingest.mjs&lt;/code&gt;) handles the document ingestion flow. It is triggered by S3 uploads and starts a Bedrock ingestion job so new documents are processed and indexed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lambda/src/ingest.mjs&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;StartIngestionJobCommand&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;KNOWLEDGE_BASE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dataSourceId&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;DATA_SOURCE_ID&lt;/span&gt;&lt;span class="p"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;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;command&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;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&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;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;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ingestionJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ingestionJobId&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="k"&gt;if &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="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ConflictException&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="c1"&gt;// An ingestion job is already running. That's fine, the new file will be picked up.&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;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&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;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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ingestion already in progress&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="k"&gt;throw&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtle issue: Bedrock only allows one ingestion job at a time. If several files are uploaded together, multiple S3 events can trigger Lambda invocations, and subsequent requests receive a &lt;code&gt;ConflictException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We treat this as expected behavior and return &lt;code&gt;202&lt;/code&gt;, because the existing ingestion job will pick up the newly uploaded files.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;query handler&lt;/strong&gt; (&lt;code&gt;query.mjs&lt;/code&gt;) handles retrieval and generation. It accepts a natural language question and uses Bedrock Knowledge Bases to retrieve relevant context and generate an answer with citations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lambda/src/query.mjs&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&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;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Parses event.body.query (API Gateway proxy format)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseQuery&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;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;query&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="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;KNOWLEDGE_BASE_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;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="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Retries once on throttle/5xx if there's &amp;gt; 15 seconds left in the timeout budget&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;response&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;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;command&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isRetryable&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;hasTimeForRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;response&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;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;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Service temporarily unavailable&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;buildResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&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="nx"&gt;text&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="nx"&gt;citations&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;For observability, logging follows a strict rule: &lt;strong&gt;structured JSON, no PII&lt;/strong&gt;. We log request IDs, durations, and error types, but never store the actual user query or generated response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Lambda dependencies are locked in &lt;code&gt;package-lock.json&lt;/code&gt;. No floating version ranges means deployments are reproducible and less likely to unexpectedly pull in a compromised dependency.&lt;/p&gt;




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

&lt;p&gt;The Bedrock and OpenSearch modules have a circular dependency. The Bedrock Knowledge Base requires the AOSS collection endpoint, while the OpenSearch data access policy requires the Bedrock KB role ARN. On a fresh deployment, Terraform cannot resolve both resources at the same time.&lt;/p&gt;

&lt;p&gt;The solution is to break the cycle by hand: create the collection first, tell the rest of the stack where it lives, then apply everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create just the AOSS collection&lt;/span&gt;
aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; terraform apply &lt;span class="nt"&gt;-target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module.opensearch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it exists, grab its endpoint and paste it into &lt;code&gt;terraform.tfvars&lt;/code&gt; so Bedrock knows where to point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;opensearch_collection_endpoint&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://your-collection-id.us-east-1.aoss.amazonaws.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now deploy the rest. This first full run also pulls in the OpenSearch Terraform provider, so it needs an &lt;code&gt;init&lt;/code&gt; in front of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; terraform init
aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first AOSS deployment takes a while (~10 minutes). After that the endpoint is captured in your state and tfvars, so day-to-day changes are just &lt;code&gt;terraform apply&lt;/code&gt;. The README has the exact commands if you want to follow along line by line.&lt;/p&gt;

&lt;p&gt;One thing worth knowing if you're using &lt;code&gt;aws-vault&lt;/code&gt;: some IAM operations reject the session tokens it generates by default. If you encounter an &lt;code&gt;InvalidClientTokenId&lt;/code&gt; error during deployment, retry with &lt;code&gt;--no-session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/coolchigi/rag-bedrock-project" rel="noopener noreferrer"&gt;README&lt;/a&gt; contains the complete deployment reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Test It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upload, check status, and invoke&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Upload a document. The ingestion Lambda triggers automatically through the S3 event:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; ./my-document.pdf &lt;span class="se"&gt;\&lt;/span&gt;
  s3://&lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; document_bucket_name&lt;span class="si"&gt;)&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Before querying, verify that Bedrock has finished processing the document:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws bedrock-agent list-ingestion-jobs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--knowledge-base-id&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; knowledge_base_id&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-source-id&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; data_source_id&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Once the ingestion job is &lt;code&gt;COMPLETE&lt;/code&gt;, invoke the query Lambda:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws lambda invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;terraform output &lt;span class="nt"&gt;-raw&lt;/span&gt; query_function_name&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--payload&lt;/span&gt; &lt;span class="s1"&gt;'{"body":"{\"query\":\"What is the document about?\"}","requestContext":{"requestId":"test-1"},"headers":{}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cli-binary-format&lt;/span&gt; raw-in-base64-out &lt;span class="se"&gt;\&lt;/span&gt;
  /tmp/response.json &amp;amp;amp&lt;span class="p"&gt;;&lt;/span&gt;&amp;amp;amp&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/response.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The payload uses the API Gateway format expected by the Lambda handler, which is why the query is nested inside &lt;code&gt;body&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A successful response looks like:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;answer&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;The document covers...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;citations&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[...]}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;If you get an empty answer, check the ingestion status first. The Knowledge Base cannot retrieve documents until the ingestion job completes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Cleanup
&lt;/h3&gt;

&lt;p&gt;Before running &lt;code&gt;terraform destroy&lt;/code&gt;, there is one thing to update. The Bedrock data source uses a &lt;code&gt;data_deletion_policy&lt;/code&gt; that defaults to &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During teardown, Bedrock attempts to remove vectors from OpenSearch as part of deleting the data source. If the AOSS collection is destroyed in the same operation, Bedrock can no longer reach it and the deletion can get stuck.&lt;/p&gt;

&lt;p&gt;Set the policy to &lt;code&gt;RETAIN&lt;/code&gt; first, apply the change, then destroy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# modules/bedrock/main.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_data_source"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.config.environment}-${var.config.project_name}-s3-source"&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;main&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;data_deletion_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RETAIN"&lt;/span&gt;

  &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Full cleanup: tear down the stack&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To also remove the bootstrap resources, first empty the versioned Terraform state bucket, then destroy the bootstrap stack:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove all object versions from the state bucket&lt;/span&gt;
aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws s3api delete-objects &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; YOUR_STATE_BUCKET_NAME &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    aws s3api list-object-versions &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; YOUR_STATE_BUCKET_NAME &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'{Objects: Versions[].{Key:Key,VersionId:VersionId}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Destroy bootstrap&lt;/span&gt;
aws-vault &lt;span class="nb"&gt;exec &lt;/span&gt;YOUR_PROFILE &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  terraform &lt;span class="nt"&gt;-chdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bootstrap destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotchas and Cost
&lt;/h2&gt;

&lt;p&gt;AOSS is the biggest cost driver in this stack. Even the minimum capacity configuration can add hundreds of dollars per month for a continuously running collection. If you are experimenting, destroy the stack when you are done.&lt;/p&gt;

&lt;p&gt;Common issues:&lt;/p&gt;

&lt;h3&gt;
  
  
  AOSS collection won't create
&lt;/h3&gt;

&lt;p&gt;You likely hit the policies-first timing issue. Make sure encryption, network, and access policies are included in &lt;code&gt;depends_on&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  KB ingestion fails
&lt;/h3&gt;

&lt;p&gt;Verify that the KB role has &lt;code&gt;s3:GetObject&lt;/code&gt; access to the bucket and &lt;code&gt;aoss:APIAccessAll&lt;/code&gt; access to the collection. Also confirm the AOSS collection is in an &lt;code&gt;ACTIVE&lt;/code&gt; state before starting ingestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda returns 400 (&lt;code&gt;query is required&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The query Lambda expects &lt;code&gt;event.body.query&lt;/code&gt;. Make sure the payload wraps the query inside a &lt;code&gt;body&lt;/code&gt; field as shown in Step 5.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda returns 503
&lt;/h3&gt;

&lt;p&gt;The function collapses any Bedrock failure into a generic 503, so the real reason is in CloudWatch, not the response body. Two things cause it in practice.&lt;/p&gt;

&lt;p&gt;The boring one is throttling. The function retries once automatically, and sustained traffic may need backoff or a limit increase.&lt;/p&gt;

&lt;p&gt;The one that actually cost me time is IAM. This project generates with Claude Sonnet 4.5, which you reach through a cross-region inference profile rather than a plain model ID. An inference profile is really a router: it forwards your request to the underlying model in whichever US region has capacity. So IAM checks you twice, once on the profile and again on the foundation model it lands on. Grant &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; on only the profile and the call fails with an &lt;code&gt;AccessDeniedException&lt;/code&gt; on &lt;code&gt;GetInferenceProfile&lt;/code&gt;, which the Lambda dutifully turns into a 503. The role needs &lt;code&gt;InvokeModel&lt;/code&gt; and &lt;code&gt;GetInferenceProfile&lt;/code&gt; on the profile, plus &lt;code&gt;InvokeModel&lt;/code&gt; on the foundation model in each region the profile can route to (&lt;code&gt;us-east-1&lt;/code&gt;, &lt;code&gt;us-east-2&lt;/code&gt;, &lt;code&gt;us-west-2&lt;/code&gt;). The Terraform in the repo already wires this up, but it is worth knowing why all those grants are there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Empty citations array
&lt;/h3&gt;

&lt;p&gt;The document format may not be supported well by Bedrock chunking. Plain text, PDF, Markdown, and HTML generally work best.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;terraform destroy&lt;/code&gt; gets stuck
&lt;/h3&gt;

&lt;p&gt;The default &lt;code&gt;data_deletion_policy = "DELETE"&lt;/code&gt; can cause teardown issues when the AOSS collection is destroyed at the same time. Set it to &lt;code&gt;RETAIN&lt;/code&gt;, apply, and then destroy.&lt;/p&gt;

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

&lt;p&gt;The Knowledge Base is live and queryable from the CLI. In Part 3, we're putting an API Gateway in front of the query Lambda and wiring up a React frontend. The query Lambda's response shape already works with API Gateway's proxy integration, and the only thing left is CORS headers and the API Gateway resource itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Disclaimer
&lt;/h2&gt;

&lt;p&gt;This is strictly for educational purposes. You will be charged for the resources created when you follow along. Remember to clean up after use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-getting-started.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-getting-started.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/bedrockagent_knowledge_base" rel="noopener noreferrer"&gt;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/bedrockagent_knowledge_base&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/language/backend/s3" rel="noopener noreferrer"&gt;https://developer.hashicorp.com/terraform/language/backend/s3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>terraform</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
