<?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: hugolesta</title>
    <description>The latest articles on DEV Community by hugolesta (@hugolesta).</description>
    <link>https://dev.to/hugolesta</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%2F199721%2Fb7aa0261-c82c-4a48-9701-2c03e14d3102.jpeg</url>
      <title>DEV Community: hugolesta</title>
      <link>https://dev.to/hugolesta</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hugolesta"/>
    <language>en</language>
    <item>
      <title>Your AI Platform Needs a Gateway, Not a Credit Card</title>
      <dc:creator>hugolesta</dc:creator>
      <pubDate>Thu, 09 Jul 2026 12:28:23 +0000</pubDate>
      <link>https://dev.to/hugolesta/your-ai-platform-needs-a-gateway-not-a-credit-card-3e5a</link>
      <guid>https://dev.to/hugolesta/your-ai-platform-needs-a-gateway-not-a-credit-card-3e5a</guid>
      <description>&lt;p&gt;A team in your org hits Anthropic during a production incident. Anthropic is down. The team is blocked for 20 minutes while someone scrambles to find an alternative API key, change a config, and redeploy. There's no fallback. There was never a plan for this.&lt;/p&gt;

&lt;p&gt;Somewhere else, another team has quietly started billing their LLM usage to a personal credit card because procurement takes six weeks. A third team has hardcoded &lt;code&gt;gpt-4o&lt;/code&gt; everywhere and doesn't know there's a cheaper model that handles their workload fine.&lt;/p&gt;

&lt;p&gt;This isn't a story about AI risk. It's a story about missing infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern you've seen before
&lt;/h2&gt;

&lt;p&gt;When teams first started running services in the cloud, the ones who moved fast didn't start with a load balancer and a CDN. They spun up an EC2 instance, pointed DNS at it, and shipped. That worked — until it didn't. Until traffic spiked, until the instance went down, until three teams had three different DNS setups and nobody knew which was canonical.&lt;/p&gt;

&lt;p&gt;AI adoption is following the same curve, faster. Every team that can call an API is calling one. The platforms don't have a control plane yet. The control plane is somebody's laptop with a hardcoded key.&lt;/p&gt;

&lt;p&gt;An AI gateway is what closes that gap. It's not a product pitch — it's the same thing you built for HTTP traffic: a single ingress point with routing, fallback, observability, and policy enforcement. The LLM is the backend. The gateway is the infrastructure around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gateway actually does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Developer — MCP / SDK / REST client"] --&amp;gt; G["AI Gateway — single stable endpoint"]
    B["Business user — internal tooling"] --&amp;gt; G
    G --&amp;gt;|"primary"| P1["Anthropic Claude"]
    G --&amp;gt;|"fallback on 5xx / timeout"| P2["AWS Bedrock — Claude / Titan"]
    G --&amp;gt;|"cost-optimised route"| P3["OpenAI GPT-4o mini"]
    G --&amp;gt; OBS["Observability — spend per team — request logs — latency"]
    G --&amp;gt; POL["Policy — rate limits — budget caps — PII filter"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failover.&lt;/strong&gt; When a provider goes down, the gateway retries against the next configured backend — same request, different model, no client code change. This is the feature that looks optional until someone spends 20 minutes blocked mid-incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost routing.&lt;/strong&gt; Not every call needs a frontier model. Summarisation, classification, short completions — route those to something cheaper. The gateway makes this a config change, not a refactor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spend visibility.&lt;/strong&gt; Every request is tagged by team, application, and model. You see where the money goes before finance calls you about it. You can set hard budget caps per team so nobody accidentally runs a batch job against &lt;code&gt;claude-opus&lt;/code&gt; all night.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider abstraction.&lt;/strong&gt; When a new model ships — or when your organisation wants to experiment with providers you don't use today — teams swap the gateway config, not their application code. The SDK they're using doesn't change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logging.&lt;/strong&gt; In any regulated environment, knowing who sent what prompt to which model isn't a nice-to-have. It's table stakes for governance. The gateway is the right place to capture that, not each individual application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What self-hosting looks like in practice
&lt;/h2&gt;

&lt;p&gt;The two tools worth knowing are &lt;a href="https://litellm.ai" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt; and &lt;a href="https://bifrost.ai" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt;. Both expose an OpenAI-compatible API, which means existing applications need zero changes to the SDK call — only the base URL and API key change.&lt;/p&gt;

&lt;p&gt;LiteLLM is the more battle-tested option. It runs as a Docker container, connects to a Postgres database for request logs and virtual key management, and supports every major provider out of the box. A minimal deployment on Kubernetes looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/berriai/litellm:main-latest&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/litellm/config.yaml"&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;
              &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm-secrets&lt;/span&gt;
                  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database-url&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/litellm&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
          &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The config itself is where the routing logic lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model_list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anthropic/claude-sonnet-4-5&lt;/span&gt;
      &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/ANTHROPIC_API_KEY&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bedrock/us.anthropic.claude-sonnet-4-5-20251001-v2:0&lt;/span&gt;
      &lt;span class="na"&gt;aws_region_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-west-1&lt;/span&gt;

&lt;span class="na"&gt;router_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;routing_strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;least-busy&lt;/span&gt;
  &lt;span class="na"&gt;num_retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;fallbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claude-sonnet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;claude-sonnet&lt;/span&gt;

&lt;span class="na"&gt;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;success_callback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langfuse"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;failure_callback&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langfuse"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two entries for the same &lt;code&gt;model_name&lt;/code&gt; is the whole failover mechanism. LiteLLM tries the first, fails, retries against the second. From the client's perspective, the call either succeeds or returns an error after both backends fail — never a 20-minute block.&lt;/p&gt;

&lt;h2&gt;
  
  
  The latency argument doesn't hold here
&lt;/h2&gt;

&lt;p&gt;The pushback you'll hear is latency. "We can't add a proxy hop to every LLM call."&lt;/p&gt;

&lt;p&gt;That argument makes sense for traditional APIs where you're measuring response times in milliseconds. It doesn't apply to LLMs. Time-to-first-token on a typical Claude call is 300–800ms. A gateway adds 5–15ms. That's noise, not overhead. The conversation about "proxy latency" is a conversation that predates LLMs and doesn't transfer.&lt;/p&gt;

&lt;p&gt;The actual latency concern worth having is gateway availability — if the gateway goes down, everything goes down. The answer is the same as any other critical path: multiple replicas, a readiness probe, a PodDisruptionBudget, and a runbook. Nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enforcement problem
&lt;/h2&gt;

&lt;p&gt;One thing the gateway doesn't solve on its own: casual users who bypass it entirely. If teams can buy a Claude.ai subscription for $20/month and skip the gateway, many will — especially if the gateway requires a non-trivial onboarding step.&lt;/p&gt;

&lt;p&gt;This is a governance problem, not a technical one. The gateway has to be the path of least resistance, not just the official path. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys that work without friction (virtual keys in LiteLLM's admin UI take 30 seconds to issue)&lt;/li&gt;
&lt;li&gt;A clear internal page that says "here is your endpoint and here is your key"&lt;/li&gt;
&lt;li&gt;A procurement policy that routes all model spend through the central key, not individual accounts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The technical infrastructure is the easier part. Getting adoption is the harder part, and that's a platform team problem from day one, not something you solve after the gateway is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run the gateway in at least two replicas from day one.&lt;/strong&gt; A single-replica proxy is a single point of failure you introduced yourself. Two replicas with a PDB that disallows simultaneous evictions cost almost nothing and remove the worst failure mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM's virtual key system is the right unit of cost tracking.&lt;/strong&gt; One virtual key per team, budget cap attached, spend dashboard out of the box. Don't try to build this yourself — it's already there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failover works at the gateway level, not the application level.&lt;/strong&gt; Stop trying to handle retries in application code when you have a gateway. Put the retry and fallback logic in one place and let every team benefit from it automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag every request with &lt;code&gt;metadata&lt;/code&gt;.&lt;/strong&gt; LiteLLM forwards custom metadata to your observability backend. Use it to tag team, environment, and use case from the first day — you'll want that granularity when someone asks why the bill doubled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK compatibility means the migration is low-friction.&lt;/strong&gt; If you start with LiteLLM and later want to evaluate Bifrost or something else, the migration is a base URL change for application teams. That's the right amount of coupling.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The teams that will struggle with AI costs and reliability in 12 months are the ones treating model access like a SaaS subscription — something each team manages independently. The teams that won't are the ones who put a control plane in front of it now, before the sprawl hardens into something unauditable.&lt;/p&gt;

&lt;p&gt;The gateway isn't the exciting part of the AI platform. It's the infrastructure that makes the exciting parts sustainable.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>aws</category>
      <category>litellm</category>
    </item>
    <item>
      <title>Give Your LLM Hands: Bedrock Agents, Lambda Tools, and the MCP Pattern in Terraform</title>
      <dc:creator>hugolesta</dc:creator>
      <pubDate>Fri, 03 Jul 2026 09:14:49 +0000</pubDate>
      <link>https://dev.to/hugolesta/give-your-llm-hands-bedrock-agents-lambda-tools-and-the-mcp-pattern-in-terraform-2fdd</link>
      <guid>https://dev.to/hugolesta/give-your-llm-hands-bedrock-agents-lambda-tools-and-the-mcp-pattern-in-terraform-2fdd</guid>
      <description>&lt;p&gt;The Knowledge Base from the &lt;a href="https://www.hugolesta.nl/blog/dont-let-your-llm-wing-it-bedrock-knowledge-base" rel="noopener noreferrer"&gt;previous post&lt;/a&gt; gives the LLM memory. But memory is passive — the model can tell you what the runbook says, but it can't go and create the Confluence page, look up the Jira ticket, or ping Slack. For that you need an agent: a model that can reason over a goal, decide which tool to call, call it, inspect the result, and loop until the task is done.&lt;/p&gt;

&lt;p&gt;This is what Bedrock Agents does. The model driving it is Anthropic Claude; the "tools" it invokes are Lambda functions you own; the schema that tells the model what each function accepts and returns lives in Terraform as a &lt;code&gt;function_schema&lt;/code&gt; block. It's the Model Context Protocol pattern, just deployed as AWS-managed infrastructure instead of a local sidecar.&lt;/p&gt;

&lt;p&gt;The agent covered here — an incident reporter — takes a PagerDuty incident ID, fetches details, searches Jira for related tickets, writes a Confluence incident page, and posts a Slack notification. Four action groups, four Lambdas, one Knowledge Base, one Terraform file.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Caller — incident ID"] --&amp;gt;|"InvokeAgent"| B["Bedrock Agent — Claude Sonnet — eu cross-region"]
    B --&amp;gt;|"Retrieve context"| C[("Knowledge Base — Aurora pgvector")]
    B --&amp;gt;|"action group"| D["Lambda — pagerduty-tools — get_incident — list_incidents — get_incident_alerts"]
    B --&amp;gt;|"action group"| E["Lambda — jira-tools — search_issues — get_issue"]
    B --&amp;gt;|"action group"| F["Lambda — confluence-tools — create_page — get_page — update_page"]
    B --&amp;gt;|"action group"| G["Lambda — slack-tools — send_message — find_channel"]
    D --&amp;gt;|"PagerDuty API"| H["PagerDuty"]
    E --&amp;gt;|"Jira API"| I["Jira"]
    F --&amp;gt;|"Confluence API"| J["Confluence"]
    G --&amp;gt;|"Slack API"| K["Slack"]
    D &amp;amp; E &amp;amp; F &amp;amp; G --&amp;gt;|"tool results"| B
    B --&amp;gt;|"final response"| A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Lambda reads its API credentials from Secrets Manager at runtime — no credentials in environment variables, no secrets in Terraform state beyond the ARN.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lambda tools pattern
&lt;/h2&gt;

&lt;p&gt;Bedrock calls this concept "action groups". I find it cleaner to think of it as MCP: the agent has a set of named functions with typed parameters, it decides which ones to call based on the task, calls them, and incorporates the responses. The Lambda is the implementation; the Terraform &lt;code&gt;function_schema&lt;/code&gt; block is the schema the model reads.&lt;/p&gt;

&lt;p&gt;Every tool Lambda in this setup follows the same structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A module call that provisions the Lambda (runtime, memory, timeout, S3 source, env vars)&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;aws_lambda_permission&lt;/code&gt; that allows &lt;code&gt;bedrock.amazonaws.com&lt;/code&gt; to invoke it&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;aws_bedrockagent_agent_action_group&lt;/code&gt; that declares the function schema&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The permission and the action group are separate resources — the permission is at the Lambda level, the action group is at the agent level. Both are required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secrets handling
&lt;/h3&gt;

&lt;p&gt;Each Lambda that calls an external API gets the secret name as an environment variable, not the secret value. The Lambda's IAM role has &lt;code&gt;secretsmanager:GetSecretValue&lt;/code&gt; on that specific secret ARN. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rotating a token is a Secrets Manager operation, not a Terraform apply&lt;/li&gt;
&lt;li&gt;The secret value never appears in a plan output or state file&lt;/li&gt;
&lt;li&gt;CloudWatch logs show &lt;code&gt;PAGERDUTY_SECRET_NAME=pagerduty-api-token&lt;/code&gt;, not a token
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lambdas&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pagerduty&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock-agent-pagerduty-tools"&lt;/span&gt;
    &lt;span class="nx"&gt;jira&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock-agent-jira-tools"&lt;/span&gt;
    &lt;span class="nx"&gt;confluence&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock-agent-confluence-tools"&lt;/span&gt;
    &lt;span class="nx"&gt;slack&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock-agent-slack-tools"&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;h3&gt;
  
  
  Lambda module + permission (PagerDuty as example)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"lambda_pagerduty_tools"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-org/lambda/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.15.0"&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;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;
  &lt;span class="nx"&gt;vcs&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vcs&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;

  &lt;span class="nx"&gt;function_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambdas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagerduty&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda_function.lambda_handler"&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python3.13"&lt;/span&gt;
  &lt;span class="nx"&gt;networking_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;cloudwatch_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;memory_size&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

  &lt;span class="nx"&gt;s3_bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-artifacts-bucket"&lt;/span&gt;
  &lt;span class="nx"&gt;s3_key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"agents/${local.lambdas.pagerduty}/${local.lambdas.pagerduty}.zip"&lt;/span&gt;

  &lt;span class="nx"&gt;env_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;PAGERDUTY_SECRET_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagerduty_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_name&lt;/span&gt;
    &lt;span class="nx"&gt;LOG_LEVEL&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"INFO"&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagerduty_secret&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_permission"&lt;/span&gt; &lt;span class="s2"&gt;"bedrock_pagerduty_tools"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowBedrockInvoke"&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.project}-${local.lambdas.pagerduty}-${local.env}"&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock.amazonaws.com"&lt;/span&gt;
  &lt;span class="nx"&gt;source_arn&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:bedrock:${local.aws_region}:${local.aws_account_id}:agent/*"&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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_pagerduty_tools&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;source_arn&lt;/code&gt; uses &lt;code&gt;agent/*&lt;/code&gt; — wildcard over agents in this account and region. If you want tighter scoping, replace the wildcard with the specific agent ARN after it's created, but that creates a circular dependency you'll have to break with a two-phase apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Agent resource
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_agent"&lt;/span&gt; &lt;span class="s2"&gt;"incident_reporter"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;agent_name&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.project}-incident-reporter-agent-${local.env}"&lt;/span&gt;
  &lt;span class="nx"&gt;agent_resource_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;incident_reporter_agent&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;foundation_model&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu.anthropic.claude-sonnet-4-6"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Orchestrates PagerDuty, Jira, Confluence, and Slack for automated incident documentation"&lt;/span&gt;
  &lt;span class="nx"&gt;idle_session_ttl_in_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;

  &lt;span class="nx"&gt;instruction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
    You are an AI Incident Reporter for the Cloud Platform team.

    When given a PagerDuty incident, complete ALL of the following steps in order:

    1. FETCH INCIDENT DETAILS
       - Use the pagerduty-tools action group to get full incident details and alerts
       - Extract: title, description, severity, affected service, timeline, assignees

    2. FIND RELATED JIRA TICKETS
       - Use the jira-tools action group to search for related tickets
       - DO NOT create new Jira tickets — only retrieve existing ones

    3. CREATE CONFLUENCE INCIDENT PAGE
       - Use the confluence-tools action group to create a new incident page
       - Include: title, severity, timeline, PagerDuty link, related Jira tickets

    4. NOTIFY SLACK
       - Use the slack-tools action group to send a message to the incident channel
       - Include: summary, severity, Confluence URL, Jira links

    5. RETURN SUMMARY
       - Provide a final summary with all links created

    Guidelines:
    - Always complete all steps — do not skip any
    - If a step fails, report the error and continue with remaining steps
    - Use the knowledge base to understand infrastructure context when relevant
&lt;/span&gt;&lt;span class="no"&gt;  EOT

&lt;/span&gt;  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_tags&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_iam_role_policy_attachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter_agent&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;A few things worth noting about this resource:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;foundation_model&lt;/code&gt;&lt;/strong&gt;: the &lt;code&gt;eu.&lt;/code&gt; prefix routes through AWS's cross-region inference, which lets the agent use model capacity across EU regions (eu-west-1, eu-central-1, eu-west-3) automatically. Without this prefix you're locked to a single region's available capacity. Use it if you're in a EU region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;instruction&lt;/code&gt;&lt;/strong&gt;: this is the system prompt. It's what tells the model the task structure, step ordering, and guard rails. Keep it procedural and explicit — the model will follow numbered steps reliably. Vague instructions produce vague agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;idle_session_ttl_in_seconds&lt;/code&gt;&lt;/strong&gt;: how long a conversation session stays alive between calls. 600 seconds (10 minutes) is reasonable for a human-in-the-loop flow; drop it lower if you're doing fully automated invocations where sessions shouldn't accumulate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent IAM
&lt;/h2&gt;

&lt;p&gt;The agent's execution role needs three permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"incident_reporter_agent"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InvokeModel"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"bedrock:InvokeModelWithResponseStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&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;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KBRetrieve"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock:Retrieve"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:bedrock:${local.aws_region}:${local.aws_account_id}:knowledge-base/${var.knowledge_base_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="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InvokeLambdas"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:lambda:${local.aws_region}:${local.aws_account_id}:function:${local.project}-${local.lambdas.pagerduty}-${local.env}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:lambda:${local.aws_region}:${local.aws_account_id}:function:${local.project}-${local.lambdas.jira}-${local.env}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:lambda:${local.aws_region}:${local.aws_account_id}:function:${local.project}-${local.lambdas.confluence}-${local.env}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:lambda:${local.aws_region}:${local.aws_account_id}:function:${local.project}-${local.lambdas.slack}-${local.env}"&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;&lt;code&gt;InvokeModel&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; is required — Bedrock Agents uses this to invoke the foundation model internally, and the resource ARN for FM invocation isn't predictable at the time the role is written if you're using cross-region inference prefixes. You can scope it to &lt;code&gt;arn:aws:bedrock:*::foundation-model/*&lt;/code&gt; if you want some constraint without breaking cross-region.&lt;/p&gt;




&lt;h2&gt;
  
  
  Action groups with function schemas
&lt;/h2&gt;

&lt;p&gt;This is where the MCP analogy is most visible. The &lt;code&gt;function_schema&lt;/code&gt; block is effectively a tool manifest: the model reads it at runtime to understand what each function is named, what parameters it expects, and what they mean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_agent_action_group"&lt;/span&gt; &lt;span class="s2"&gt;"pagerduty_tools"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;agent_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_id&lt;/span&gt;
  &lt;span class="nx"&gt;agent_version&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DRAFT"&lt;/span&gt;
  &lt;span class="nx"&gt;action_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pagerduty-tools"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PagerDuty tools: get_incident, list_incidents, get_incident_alerts"&lt;/span&gt;

  &lt;span class="nx"&gt;action_group_executor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:lambda:${local.aws_region}:${local.aws_account_id}:function:${local.project}-${local.lambdas.pagerduty}-${local.env}"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;function_schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;member_functions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;functions&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;"get_incident"&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Retrieve full details of a single PagerDuty incident by its ID"&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"incident_id"&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;"string"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The PagerDuty incident ID (e.g. P1234AB)"&lt;/span&gt;
          &lt;span class="nx"&gt;required&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="nx"&gt;functions&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;"list_incidents"&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"List active PagerDuty incidents filtered by status and/or service"&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"statuses"&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;"string"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Comma-separated statuses to filter by: triggered, acknowledged, resolved"&lt;/span&gt;
          &lt;span class="nx"&gt;required&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"limit"&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;"integer"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Maximum number of incidents to return (default: 20)"&lt;/span&gt;
          &lt;span class="nx"&gt;required&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;functions&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;"get_incident_alerts"&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Get all alerts associated with a PagerDuty incident"&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"incident_id"&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;"string"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The PagerDuty incident ID"&lt;/span&gt;
          &lt;span class="nx"&gt;required&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;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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_pagerduty_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_lambda_permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock_pagerduty_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Jira action group is read-only by design — &lt;code&gt;search_issues&lt;/code&gt; and &lt;code&gt;get_issue&lt;/code&gt; only. This isn't enforced at the Lambda level (you could build a write-capable Lambda and only expose read functions here), but the intent is captured in both the &lt;code&gt;description&lt;/code&gt; field and the agent's &lt;code&gt;instruction&lt;/code&gt;. Two layers of "don't create tickets" beats one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_agent_action_group"&lt;/span&gt; &lt;span class="s2"&gt;"jira_tools"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;agent_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_id&lt;/span&gt;
  &lt;span class="nx"&gt;agent_version&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DRAFT"&lt;/span&gt;
  &lt;span class="nx"&gt;action_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"jira-tools"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Jira tools (read-only): search_issues, get_issue — no ticket creation"&lt;/span&gt;

  &lt;span class="nx"&gt;action_group_executor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:lambda:${local.aws_region}:${local.aws_account_id}:function:${local.project}-${local.lambdas.jira}-${local.env}"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;function_schema&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;member_functions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;functions&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;"search_issues"&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Search Jira issues using JQL"&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"jql"&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;"string"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"JQL query string"&lt;/span&gt;
          &lt;span class="nx"&gt;required&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"max_results"&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;"integer"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Maximum number of results to return (default: 10)"&lt;/span&gt;
          &lt;span class="nx"&gt;required&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"fields"&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;"string"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Comma-separated list of fields to include"&lt;/span&gt;
          &lt;span class="nx"&gt;required&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;functions&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;"get_issue"&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Retrieve full details of a single Jira issue by its key"&lt;/span&gt;
        &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;map_block_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"issue_key"&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;"string"&lt;/span&gt;
          &lt;span class="nx"&gt;description&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The Jira issue key (e.g. OPS-1234)"&lt;/span&gt;
          &lt;span class="nx"&gt;required&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;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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_jira_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_lambda_permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock_jira_tools&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;h2&gt;
  
  
  Attaching the Knowledge Base
&lt;/h2&gt;

&lt;p&gt;One resource to connect the Knowledge Base built in the previous post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_agent_knowledge_base_association"&lt;/span&gt; &lt;span class="s2"&gt;"incident_reporter_kb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;agent_id&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_id&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;var&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="nx"&gt;description&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Internal infrastructure and platform documentation"&lt;/span&gt;
  &lt;span class="nx"&gt;knowledge_base_state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ENABLED"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this association in place, the agent automatically retrieves relevant KB chunks before deciding which tools to call. The instruction's "use the knowledge base to understand infrastructure context when relevant" is the trigger — the model decides when to retrieve, not you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent alias and SSM parameters
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_agent_alias"&lt;/span&gt; &lt;span class="s2"&gt;"incident_reporter"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;agent_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_id&lt;/span&gt;
  &lt;span class="nx"&gt;agent_alias_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"live"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Live alias for the Incident Reporter agent"&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_tags&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_bedrockagent_agent_action_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagerduty_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_bedrockagent_agent_action_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jira_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_bedrockagent_agent_action_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confluence_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_bedrockagent_agent_action_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aws_bedrockagent_agent_knowledge_base_association&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter_kb&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_parameter"&lt;/span&gt; &lt;span class="s2"&gt;"incident_reporter_agent_id"&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;"/${local.env}/incident-reporter/agent_id"&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;"String"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_parameter"&lt;/span&gt; &lt;span class="s2"&gt;"incident_reporter_agent_alias_id"&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;"/${local.env}/incident-reporter/agent_alias_id"&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;"String"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_bedrockagent_agent_alias&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incident_reporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_alias_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The alias is required to invoke the agent from application code — you always invoke via &lt;code&gt;agentId&lt;/code&gt; + &lt;code&gt;agentAliasId&lt;/code&gt;. Publishing a named alias decouples callers from the internal version counter: when you update action groups and Bedrock creates a new draft version, you prepare a new alias version and flip it without changing any caller configuration.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; on the alias resource is load-bearing. Without it, Terraform might try to create the alias before all action groups exist, which causes Bedrock to create the alias pointing at an agent that's still missing tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Field notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;agent_version = "DRAFT"&lt;/code&gt; on all action groups.&lt;/strong&gt; Action groups always attach to &lt;code&gt;DRAFT&lt;/code&gt;. When you publish a new agent version (via alias routing), Bedrock snapshots the DRAFT config. If you try to attach action groups to a numbered version, the API rejects it. This trips up people who try to version-lock action groups for safety.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The function description matters more than you'd think.&lt;/strong&gt; The model reads &lt;code&gt;description&lt;/code&gt; fields to decide which tool to call and when. Vague descriptions ("does stuff with PagerDuty") produce agents that hallucinate which tool to invoke. Write descriptions as if you're writing API docs for a junior engineer — precise verbs, clear scope.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parameter types are limited: &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;.&lt;/strong&gt; No nested objects. If your Lambda needs structured input (a list of filter criteria, a nested config), serialize it as JSON in a &lt;code&gt;string&lt;/code&gt; parameter and document the shape in the description. Ugly but it works.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda cold starts extend agent response time noticeably.&lt;/strong&gt; Each tool call is a synchronous Lambda invocation. Four cold starts in a chain = 4× the latency penalty. Provision concurrency on the most-called tools if response time matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;depends_on&lt;/code&gt; on the alias is not optional.&lt;/strong&gt; Terraform's parallelism will race the alias creation against action group creation without it. The apply succeeds but the agent alias captures an incomplete version with missing tools. You won't notice until runtime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-region inference (&lt;code&gt;eu.&lt;/code&gt; prefix) changes the IAM picture.&lt;/strong&gt; With the cross-region prefix, the model ARN the agent resolves to at runtime isn't in your account's region — it's wherever AWS routes capacity. &lt;code&gt;InvokeModel&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; is practically required here, or you scope to &lt;code&gt;arn:aws:bedrock:*::foundation-model/*&lt;/code&gt; to at least limit to foundation models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secrets Manager reads happen at every Lambda invocation.&lt;/strong&gt; For high-throughput agents this adds latency and cost. Cache the secret in the Lambda's global scope with a TTL if invocations are frequent — just don't cache indefinitely or token rotation won't take effect.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The Knowledge Base gave the LLM a read-only window into your documentation. The agent and its Lambda tools give it hands: it can fetch, create, and notify across your tooling stack in a single invocation. The infrastructure for it is one Terraform file, four Lambda deployments, and an IAM role with three statements.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>bedrock</category>
      <category>agents</category>
    </item>
    <item>
      <title>Don't Let Your LLM Wing It: Building a Knowledge Base That Actually Knows Things</title>
      <dc:creator>hugolesta</dc:creator>
      <pubDate>Tue, 30 Jun 2026 21:08:46 +0000</pubDate>
      <link>https://dev.to/hugolesta/dont-let-your-llm-wing-it-building-a-knowledge-base-that-actually-knows-things-k2m</link>
      <guid>https://dev.to/hugolesta/dont-let-your-llm-wing-it-building-a-knowledge-base-that-actually-knows-things-k2m</guid>
      <description>&lt;p&gt;Every team eventually asks the same question: "can we make the LLM answer questions about &lt;em&gt;our&lt;/em&gt; docs instead of hallucinating something plausible?" The answer is retrieval-augmented generation (RAG), and the unglamorous truth is that RAG is 10% prompt engineering and 90% plumbing — object storage, a vector index, an embedding model, and a pipeline that keeps all three in sync whenever someone edits a markdown file.&lt;/p&gt;

&lt;p&gt;This is the plumbing. A Bedrock Knowledge Base backed by Aurora PostgreSQL with &lt;code&gt;pgvector&lt;/code&gt;, provisioned entirely in Terraform, synced automatically from a Git repo via GitHub Actions. No notebooks, no manual "let me re-upload the docs" Tuesdays.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Git repository — knowledge-base/*.md"] --&amp;gt;|"push to main or workflow_dispatch"| B["GitHub Actions — sync-knowledge-base.yml"]
    B --&amp;gt;|uploads docs| C[("S3 bucket — KMS encrypted")]
    C --&amp;gt;|start ingestion| D{"Bedrock ingestion job — hierarchical chunking"}
    D --&amp;gt;|"Titan Embed Text v2"| E[("Aurora PostgreSQL — pgvector — HNSW + GIN indexes")]
    E --&amp;gt;|"Retrieve / RetrieveAndGenerate"| F["App / LLM proxy — scoped IAM role"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two environments — &lt;code&gt;acc&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt; — run this same pipeline side by side, each with its own bucket, KB, and database, gated by branch and dispatch logic in the workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Provisioning the vector store
&lt;/h2&gt;

&lt;h3&gt;
  
  
  S3 bucket for ingestion
&lt;/h3&gt;

&lt;p&gt;Bedrock pulls documents from S3, not the other way around, so the bucket is just a private, versioned, encrypted drop zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"kb_docs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.cluster_name}-knowledge-base"&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_tags&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"kb_docs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_docs&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;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_server_side_encryption_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"kb_docs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_docs&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;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;apply_server_side_encryption_by_default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sse_algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:kms"&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"kb_docs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_docs&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;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Aurora pgvector cluster
&lt;/h3&gt;

&lt;p&gt;This is a separate Aurora cluster from any application database — different lifecycle, different access pattern, and you don't want a runaway ingestion job competing for connections with your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"kb_db"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-aws-modules/rds-aurora/aws"&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 9.0"&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;"bedrock-kb"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aurora-postgresql"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"16.6"&lt;/span&gt;
  &lt;span class="nx"&gt;instances&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;serverlessv2_scaling_configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;min_capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="nx"&gt;max_capacity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;database_name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bedrock_kb"&lt;/span&gt;
  &lt;span class="nx"&gt;master_username&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt;
  &lt;span class="nx"&gt;manage_master_user_password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;enable_http_endpoint&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_tags&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bedrock's Knowledge Base service expects credentials in &lt;code&gt;{"username": ..., "password": ...}&lt;/code&gt; JSON shape in Secrets Manager. If your Terraform module stores a bare password string (most do), mirror it into a second secret in the right shape rather than fighting the module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"kb_db_bedrock"&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;"bedrock-kb-db-credentials"&lt;/span&gt;
  &lt;span class="nx"&gt;recovery_window_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_tags&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret_version"&lt;/span&gt; &lt;span class="s2"&gt;"kb_db_bedrock"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&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_db_bedrock&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;secret_string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"root"&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_master_password&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;h3&gt;
  
  
  IAM: the Bedrock service role
&lt;/h3&gt;

&lt;p&gt;Bedrock's Knowledge Base needs to assume a role that can read from S3, describe and execute statements against Aurora via the Data API, read the credentials secret, and invoke the embedding model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"bedrock_kb_assume_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&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;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceAccount"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_account_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;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"bedrock_kb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3Read"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"${aws_s3_bucket.kb_docs.arn}/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDSDataApi"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rds-data:BatchExecuteStatement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rds-data:ExecuteStatement"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SecretsManagerRead"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_db_bedrock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BedrockEmbeddings"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/amazon.titan-embed-text-v2:0"&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;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"bedrock_kb"&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;"${local.cluster_name}-bedrock-kb"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock_kb_assume_role&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="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"bedrock_kb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock_kb&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;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bedrock_kb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Knowledge Base resource itself
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_bedrockagent_knowledge_base"&lt;/span&gt; &lt;span class="s2"&gt;"main"&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;"${local.cluster_name}-knowledge-base"&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;bedrock_kb&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;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"VECTOR"&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="s2"&gt;"arn:aws:bedrock:eu-west-1::foundation-model/amazon.titan-embed-text-v2:0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;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;resource_arn&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cluster_arn&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_db_bedrock&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="s2"&gt;"bedrock_kb"&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;"bedrock_integration.bedrock_kb"&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;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="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;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="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;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_db&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_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;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;name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.cluster_name}-knowledge-base-s3"&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_docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;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;"HIERARCHICAL"&lt;/span&gt;
      &lt;span class="nx"&gt;hierarchical_chunking_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;level_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;1500&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;level_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="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;overlap_tokens&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&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;Hierarchical chunking is worth calling out: it splits documents into large 1500-token "parent" chunks and smaller 300-token "child" chunks with a 60-token overlap. Retrieval matches on the precise child chunk but can return the broader parent context — better recall on long documents than flat fixed-size chunking, at the cost of slightly more complex ingestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-time schema bootstrap
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;aws_bedrockagent_knowledge_base&lt;/code&gt; expects the target table to already exist with the right columns and indexes — Terraform won't create the &lt;code&gt;pgvector&lt;/code&gt; extension or table for you. This is a one-time &lt;code&gt;psql&lt;/code&gt; job against the Aurora endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bedrock_integration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bedrock_integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bedrock_kb&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;chunks&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt;  &lt;span class="n"&gt;JSON&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bedrock_kb_embedding_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bedrock_integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bedrock_kb&lt;/span&gt;
  &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;bedrock_kb_chunks_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;bedrock_integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bedrock_kb&lt;/span&gt;
  &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'simple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HNSW index handles approximate nearest-neighbor search on the embedding vector; the GIN index on &lt;code&gt;chunks&lt;/code&gt; enables hybrid search (keyword + semantic) if you ever want it. &lt;code&gt;vector(1024)&lt;/code&gt; matches Titan Embed Text v2's output dimensionality — if you swap embedding models later, the column width has to match or ingestion fails outright.&lt;/p&gt;

&lt;h3&gt;
  
  
  Publishing references via SSM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_parameter"&lt;/span&gt; &lt;span class="s2"&gt;"kb_id"&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;"/${local.env}/knowledge-base/kb_id"&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;"String"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ssm_parameter"&lt;/span&gt; &lt;span class="s2"&gt;"kb_bucket"&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;"/${local.env}/knowledge-base/bucket"&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;"String"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kb_docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apps and CI pipelines read these instead of hardcoding ARNs — when you rebuild the KB in a new account or region, nothing downstream needs a code change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Syncing docs automatically with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Provisioning is the easy part. The actual day-to-day value comes from never having to think about ingestion again. Drop a markdown file in a &lt;code&gt;knowledge-base/&lt;/code&gt; folder, push, and it's searchable within minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sync Knowledge Base&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Environment to sync knowledge base&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;acc&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;knowledge-base/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sync-kb-acc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;github.event_name == 'workflow_dispatch' &amp;amp;&amp;amp; github.event.inputs.environment == 'acc' ||&lt;/span&gt;
      &lt;span class="s"&gt;github.event_name == 'push'&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org/your-pipelines-repo/.github/workflows/sync-bedrock-knowledge-base.yml@v2&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acc&lt;/span&gt;
      &lt;span class="na"&gt;knowledge-base-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.BEDROCK_KB_ID_ACC }}&lt;/span&gt;
      &lt;span class="na"&gt;data-source-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.BEDROCK_KB_DATA_SOURCE_ID_ACC }}&lt;/span&gt;
      &lt;span class="na"&gt;bucket-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.BEDROCK_KB_BUCKET_ACC }}&lt;/span&gt;
      &lt;span class="na"&gt;bucket-prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;knowledge-base&lt;/span&gt;
      &lt;span class="na"&gt;source-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;knowledge-base&lt;/span&gt;

  &lt;span class="na"&gt;sync-kb-prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'workflow_dispatch' &amp;amp;&amp;amp; github.event.inputs.environment == 'prod'&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-org/your-pipelines-repo/.github/workflows/sync-bedrock-knowledge-base.yml@v2&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
      &lt;span class="na"&gt;knowledge-base-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.BEDROCK_KB_ID_PROD }}&lt;/span&gt;
      &lt;span class="na"&gt;data-source-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.BEDROCK_KB_DATA_SOURCE_ID_PROD }}&lt;/span&gt;
      &lt;span class="na"&gt;bucket-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.BEDROCK_KB_BUCKET_PROD }}&lt;/span&gt;
      &lt;span class="na"&gt;bucket-prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;knowledge-base&lt;/span&gt;
      &lt;span class="na"&gt;source-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;knowledge-base&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gating logic is the whole trick here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;, path-filtered to `knowledge-base/&lt;/strong&gt;&lt;code&gt;** — only fires the &lt;/code&gt;acc` job. Routine doc edits land in the acc environment automatically, with zero manual steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; with &lt;code&gt;environment: acc&lt;/code&gt;&lt;/strong&gt; — also runs the acc job. Useful for re-triggering a sync without a new commit (e.g. after fixing a broken IAM policy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt; with &lt;code&gt;environment: prod&lt;/code&gt;&lt;/strong&gt; — the &lt;em&gt;only&lt;/em&gt; path that touches prod. Promotion to production is always a deliberate, manual action, never a side effect of a push.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both jobs delegate to the same reusable workflow (&lt;code&gt;sync-bedrock-knowledge-base.yml@v2&lt;/code&gt;), parameterized per environment. The reusable workflow does the actual work: sync the &lt;code&gt;source-dir&lt;/code&gt; to the S3 &lt;code&gt;bucket-name&lt;/code&gt; under &lt;code&gt;bucket-prefix&lt;/code&gt;, then call &lt;code&gt;StartIngestionJob&lt;/code&gt; against &lt;code&gt;data-source-id&lt;/code&gt;. Centralizing that logic in one reusable workflow means every team adopting this pattern gets the same sync behavior — and a fix to the sync logic ships everywhere at once instead of needing fifteen copy-pasted workflow files updated individually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Field notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Data API requires &lt;code&gt;enable_http_endpoint = true&lt;/code&gt; on the Aurora cluster.&lt;/strong&gt; Without it, Bedrock's &lt;code&gt;rds-data:ExecuteStatement&lt;/code&gt; calls fail with a confusing connectivity error that has nothing to do with security groups — you'll waste an hour checking VPC routing before you find this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector dimension mismatches fail silently at the wrong layer.&lt;/strong&gt; If &lt;code&gt;vector(1024)&lt;/code&gt; doesn't match your embedding model's output size, the table creation succeeds, the Knowledge Base resource creates fine, and ingestion just fails per-document. Check the embedding model's dimensionality before writing the DDL, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchical chunking is the right default for prose-heavy docs&lt;/strong&gt;, but if your knowledge base is mostly short, structured files (FAQs, glossaries), flat fixed-size chunking is simpler to reason about and debug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let &lt;code&gt;push&lt;/code&gt;-triggered syncs touch prod.&lt;/strong&gt; It's tempting to make prod sync automatically on a release tag, but a bad doc — wrong numbers, stale instructions — propagating into a production RAG pipeline with no human in the loop is a worse failure mode than a slightly stale prod KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;acc&lt;/code&gt;/&lt;code&gt;prod&lt;/code&gt; split needs two of everything&lt;/strong&gt; — bucket, KB, data source, Aurora cluster — not just two sets of IAM permissions on shared resources. It costs more, but it means a bad ingestion config or chunking change gets caught in acc before it can corrupt the index your production LLM proxy actually queries.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;None of this is exotic — it's a bucket, a Postgres extension, two indexes, and a YAML file with an &lt;code&gt;if:&lt;/code&gt; condition. That's the whole trick to "production RAG": treat the knowledge base like any other piece of infrastructure, version it, gate promotion to prod behind a manual step, and let the boring CI pipeline do the boring sync work. Your LLM stops winging it, and you stop being the person who manually re-uploads PDFs every time someone asks why the bot doesn't know about last week's runbook update.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>pgvector</category>
      <category>rag</category>
    </item>
    <item>
      <title>How to Upgrade EKS 1.32: Making the Switch from bootstrap.sh to nodeadm</title>
      <dc:creator>hugolesta</dc:creator>
      <pubDate>Tue, 28 Oct 2025 17:10:23 +0000</pubDate>
      <link>https://dev.to/hugolesta/how-to-upgrade-eks-132-making-the-switch-from-bootstrapsh-to-nodeadm-9lf</link>
      <guid>https://dev.to/hugolesta/how-to-upgrade-eks-132-making-the-switch-from-bootstrapsh-to-nodeadm-9lf</guid>
      <description>&lt;p&gt;Did you know staying on a deprecated version of Kubernetes in EKS can cost you six times more? At $0.60 per hour instead of the standard $0.10 per hour, you could end up paying nearly $500 per month just for running outdated clusters.&lt;/p&gt;

&lt;p&gt;However, the EKS upgrade from version 1.31 to 1.32 isn't just about avoiding extended support fees—it introduces one of the biggest under-the-hood changes in recent EKS history. Specifically, the traditional bootstrap.sh script used for years to configure worker nodes is now replaced by a new tool called &lt;code&gt;nodeadm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This architectural shift coincides with another critical change: after November 26, 2025, Amazon EKS will no longer publish EKS-optimized Amazon Linux 2 (AL2) AMIs. Furthermore, Kubernetes 1.32 will be the final version with AL2 AMI support, while version 1.33 onwards will only support Amazon Linux 2023 (AL2023) and Bottlerocket based AMIs.&lt;/p&gt;

&lt;p&gt;If your clusters currently run EKS 1.31 or earlier on Amazon Linux 2 AMIs, upgrading to 1.32 will break your node initialization unless you adapt your user-data scripts and switch to AL2023. Throughout this article, we'll walk through exactly what changes between versions 1.31 and 1.32, why &lt;code&gt;nodeadm&lt;/code&gt; is now required, and how to rewrite your user-data and Terraform templates to ensure a smooth transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why EKS 1.32 Requires a New Bootstrap Approach
&lt;/h2&gt;

&lt;p&gt;Amazon's evolution of EKS introduces significant architectural changes with version 1.32. The most critical change affects how worker nodes bootstrap and join your clusters, requiring careful attention during upgrades.&lt;/p&gt;

&lt;h2&gt;
  
  
  End of Support for bootstrap.sh in AL2023
&lt;/h2&gt;

&lt;p&gt;EKS 1.32 marks a pivotal shift as it's the &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions-standard.html" rel="noopener noreferrer"&gt;final version for which Amazon will release Amazon Linux 2 (AL2) AMIs&lt;/a&gt;. Starting with Kubernetes 1.32, AL2023 introduces a completely different node initialization process that abandons the traditional &lt;code&gt;/etc/eks/bootstrap.sh&lt;/code&gt; script. This script has been the foundation of EKS node bootstrapping since the service launched, but is now replaced entirely in the AL2023 operating system. The familiar bash-based bootstrap approach that many DevOps teams have built automation around is completely absent in the new OS version.&lt;/p&gt;

&lt;h2&gt;
  
  
  nodeadm as the New Default Bootstrap Tool
&lt;/h2&gt;

&lt;p&gt;AL2023 replaces bootstrap.sh with &lt;code&gt;nodeadm&lt;/code&gt;, a tool that uses a YAML configuration schema. Unlike the previous approach where metadata was discovered automatically through the Amazon EKS &lt;code&gt;DescribeCluster&lt;/code&gt; API call, &lt;code&gt;nodeadm&lt;/code&gt; requires explicit provision of cluster information. This fundamental change means you must now specify three critical parameters that were previously auto-discovered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apiServerEndpoint&lt;/li&gt;
&lt;li&gt;certificateAuthority&lt;/li&gt;
&lt;li&gt;service CIDR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, the format for applying parameters to kubelet has changed. Previously accomplished with &lt;code&gt;--kubelet-extra-args&lt;/code&gt;, node customization now requires using &lt;code&gt;NodeConfigSpec&lt;/code&gt;. This shift aims to reduce API throttling risks during large-scale node deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact on Existing AL2-Based Clusters
&lt;/h2&gt;

&lt;p&gt;The elimination of bootstrap.sh creates immediate backward compatibility issues. When upgrading an EKS cluster to version 1.32 while still using AL2-based node groups, &lt;a href="https://medium.com/@kalidindi-naveen/eks-ami-upgradation-journey-from-amazon-linux-2-to-amazon-linux-2023-385c1d958b27" rel="noopener noreferrer"&gt;nodes will fail to join the cluster&lt;/a&gt;. Any automation depending on bootstrap.sh will break, as files like &lt;code&gt;/etc/eks/bootstrap.sh&lt;/code&gt; and &lt;code&gt;/etc/eks/eni-max-pods.txt&lt;/code&gt; no longer exist.&lt;/p&gt;

&lt;p&gt;For organizations with self-managed node groups or custom AMI configurations, this requires extracting and explicitly providing cluster metadata that was formerly obtained automatically. Consequently, deployment scripts, Terraform modules, and CloudFormation templates must be rewritten to align with the new declarative approach before successfully migrating to EKS 1.32.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing for the Migration to nodeadm
&lt;/h2&gt;

&lt;p&gt;Before migrating to EKS 1.32 with &lt;code&gt;nodeadm&lt;/code&gt;, careful preparation is essential to ensure a smooth transition from the traditional bootstrap approach to the new paradigm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying Affected Clusters Running AL2
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/eks-ami-deprecation-faqs.html" rel="noopener noreferrer"&gt;After November 26, 2025&lt;/a&gt;, AWS will end support for EKS AL2-optimized AMIs. Kubernetes version 1.32 represents the final release where Amazon EKS will provide AL2 AMIs. This deadline necessitates prompt action, especially for organizations with multiple clusters.&lt;/p&gt;

&lt;p&gt;To identify affected clusters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the AMI type for each node group using:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws eks describe-nodegroup --cluster-name &amp;lt;cluster-name&amp;gt; --nodegroup-name &amp;lt;nodegroup-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Examine existing user-data scripts that reference &lt;code&gt;/etc/eks/bootstrap.sh&lt;/code&gt;, which won't exist in AL2023.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Choosing Between AL2023 and Bottlerocket AMIs
&lt;/h2&gt;

&lt;p&gt;Upon identifying clusters requiring migration, you must decide between AL2023 and Bottlerocket as your future node operating system.&lt;/p&gt;

&lt;p&gt;AL2023 offers several advantages over AL2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure-by-default approach with preconfigured security policies&lt;/li&gt;
&lt;li&gt;SELinux in permissive mode&lt;/li&gt;
&lt;li&gt;IMDSv2-only mode enabled by default&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/eks-ami-deprecation-faqs.html" rel="noopener noreferrer"&gt;Optimized boot times and improved package management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alternatively, Bottlerocket provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Purpose-built container-optimized design with minimal attack surface&lt;/li&gt;
&lt;li&gt;Enhanced security with read-only file systems&lt;/li&gt;
&lt;li&gt;Automatic updates&lt;/li&gt;
&lt;li&gt;Improved compliance with security standards like CIS benchmarks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose AL2023 when you need significant customizations with direct OS-level access. Opt for Bottlerocket if you prefer a container-native approach with minimal node customization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scanning for Deprecated APIs with kubent or pluto
&lt;/h2&gt;

&lt;p&gt;Prior to upgrading, scan for deprecated APIs that might break during the transition. Kubernetes frequently removes beta APIs with each new version, potentially disrupting your workloads.&lt;/p&gt;

&lt;p&gt;The kube-no-trouble tool (kubent) efficiently identifies resources using deprecated APIs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This scans all accessible namespaces and lists APIs that will be deprecated compared to your current Kubernetes version. For clusters with hundreds of applications across multiple namespaces, this tool proves invaluable in detecting potential upgrade issues beforehand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backing Up Cluster State and Node Configurations
&lt;/h2&gt;

&lt;p&gt;A comprehensive migration plan should always include thorough backups. Given that you cannot downgrade an EKS cluster after upgrading, backups become crucial.&lt;/p&gt;

&lt;p&gt;Recommended backup steps include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Taking etcd snapshots for core Kubernetes data&lt;/li&gt;
&lt;li&gt;Backing up cluster configuration using tools like Velero&lt;/li&gt;
&lt;li&gt;Documenting node-specific configurations, especially custom user-data scripts&lt;/li&gt;
&lt;li&gt;Preserving IAM role configurations that will need migration to the new nodeadm format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, establish a documented rollback procedure with well-defined testing protocols before proceeding with the upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing nodeadm and AL2023
&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%2Fuelrvrgw68lne8v0iqea.WEBP" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuelrvrgw68lne8v0iqea.WEBP" alt="How EKS cluster works" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Image Source: AWS Documentation&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging and Validating the Upgrade
&lt;/h2&gt;

&lt;p&gt;After implementing &lt;code&gt;nodeadm&lt;/code&gt; and upgrading to EKS 1.32, troubleshooting becomes essential as new components may not function perfectly on the first try. The migration introduces different debugging approaches that we need to master.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common nodeadm Errors and How to Fix Them
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;nodeadm&lt;/code&gt; debug command serves as our first line of defense when troubleshooting unhealthy or misconfigured nodes. It validates critical requirements including network access to AWS APIs, credentials for the IAM role, connectivity to the EKS Kubernetes API endpoint, and authentication with the EKS cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodeadm debug -c file://nodeConfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For configuration validation before implementation, the &lt;code&gt;nodeadm config check&lt;/code&gt; command proves invaluable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodeadm config check -c file://nodeConfig.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most permission-related issues arise from the Hybrid Nodes IAM role &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-troubleshooting.html" rel="noopener noreferrer"&gt;missing the necessary eks:DescribeCluster action&lt;/a&gt;. Other common errors include network connectivity problems, incorrect node IP configuration, and timeout issues which can be remedied by extending timeouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodeadm install K8S_VERSION --credential-provider CREDS_PROVIDER --timeout 20m0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying kubelet and containerd Startup Logs
&lt;/h2&gt;

&lt;p&gt;Examining kubelet logs offers visibility into node initialization problems. For AL2023 nodes, we can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl status kubelet
journalctl -u kubelet -o cat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moreover, checking the status helps verify successful restarts after upgrades. For deeper troubleshooting, we can connect to the node using SSH or kubectl debug and inspect the logs. My personal preference is using &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html" rel="noopener noreferrer"&gt;AWS Systems Manager Session Manager&lt;/a&gt; to enhance security, avoiding opening SSH ports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chroot /host journalctl -u kubelet -o cat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Checking Node Readiness and Taints
&lt;/h2&gt;

&lt;p&gt;To verify node status after migration, we use standard kubectl commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get nodes -o wide
kubectl describe node NODE_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The STATUS column should display "Ready" for all nodes with the updated version number visible. Untainted nodes are essential for workload scheduling. Furthermore, checking events related to nodes can reveal issues with node registration or initialization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating Add-on Compatibility Post-Upgrade
&lt;/h2&gt;

&lt;p&gt;Post-upgrade add-on validation requires checking deployment versions and ensuring pods are running correctly. For example, to verify the CoreDNS version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl describe deployment coredns -n kube-system | grep Image | cut -d ':' -f 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect add-on logs for errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl logs -n kube-system -l k8s-app=kube-dns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For networking add-ons like Amazon VPC CNI, we should create test pods to validate IP assignment. Additionally, testing CoreDNS functionality using tools like nslookup ensures proper DNS resolution. Finally, checking if the number of replicas equals the number of nodes for daemonset add-ons like vpc-cni and kube-proxy confirms proper deployment.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Migrating to EKS 1.32 represents a significant paradigm shift for Kubernetes operations on AWS. The transition from &lt;code&gt;bootstrap.sh&lt;/code&gt; to &lt;code&gt;nodeadm&lt;/code&gt; fundamentally changes how worker nodes join your clusters, requiring careful planning and execution. Additionally, the impending deprecation of Amazon Linux 2 AMIs after November 26, 2025, creates urgency for organizations to adapt their infrastructure.&lt;/p&gt;

&lt;p&gt;Throughout this upgrade journey, you must remember that &lt;code&gt;nodeadm&lt;/code&gt; demands explicit configuration through YAML rather than command-line arguments. This declarative approach actually offers better consistency and reproducibility for node configurations once implemented correctly. Undoubtedly, the initial migration might seem daunting, especially when rewriting existing automation scripts or Terraform modules.&lt;/p&gt;

&lt;p&gt;The choice between AL2023 and Bottlerocket AMIs depends largely on your specific operational requirements. AL2023 provides a familiar environment with improved security features, whereas Bottlerocket offers a container-optimized approach with minimal attack surface. Regardless of your choice, both options eliminate the traditional bootstrap files and require adaptation to the new &lt;code&gt;nodeadm&lt;/code&gt; paradigm.&lt;/p&gt;

&lt;p&gt;Before initiating any upgrade, thorough preparation becomes essential. First, identify affected clusters running AL2, then scan for deprecated APIs, and finally back up your cluster state. After implementing the necessary changes, debugging tools like &lt;code&gt;nodeadm debug&lt;/code&gt; and &lt;code&gt;nodeadm config check&lt;/code&gt; help troubleshoot any issues that arise during the migration process.&lt;/p&gt;

&lt;p&gt;The EKS 1.32 upgrade, therefore, presents both challenges and opportunities. While it requires significant changes to existing workflows, it also aligns your infrastructure with AWS's future direction. Consequently, organizations that proactively adapt their node initialization processes will avoid extended support fees and benefit from improved security and performance features of newer operating systems.&lt;/p&gt;

&lt;p&gt;Ultimately, staying current with EKS versions not only saves operational costs but also ensures compatibility with the evolving Kubernetes ecosystem. Though this particular upgrade demands more effort than typical version bumps, the long-term benefits of embracing &lt;code&gt;nodeadm&lt;/code&gt; and AL2023 far outweigh the initial investment required for migration.&lt;/p&gt;

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

&lt;p&gt;EKS 1.32 introduces the most significant architectural change in recent history, requiring organizations to abandon the traditional bootstrap.sh script in favor of nodeadm for worker node initialization.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Critical deadline approaching:&lt;/strong&gt; Amazon ends AL2 AMI support on November 26, 2025, making EKS 1.32 the final version supporting Amazon Linux 2.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Bootstrap method completely changes:&lt;/strong&gt; nodeadm replaces bootstrap.sh and requires explicit YAML configuration instead of automatic cluster metadata discovery.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Migration requires careful preparation:&lt;/strong&gt; Identify AL2-based clusters, scan for deprecated APIs with kubent, and backup cluster state before upgrading.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Choose your AMI strategy:&lt;/strong&gt; Select between AL2023 for familiar environments with enhanced security or Bottlerocket for container-optimized minimal attack surface.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Debug with new tools:&lt;/strong&gt; Use nodeadm debug and nodeadm config check commands to troubleshoot configuration issues and validate node readiness.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Avoid costly extended support:&lt;/strong&gt; Staying on deprecated Kubernetes versions costs 6x more ($0.60/hour vs $0.10/hour), potentially adding $500+ monthly per cluster.&lt;/p&gt;

&lt;p&gt;The shift to nodeadm represents AWS's commitment to more secure, declarative infrastructure management. While the initial migration requires significant effort in rewriting automation scripts and Terraform modules, organizations that proactively adapt will benefit from improved security, performance, and long-term cost savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q1. What is the main change in EKS 1.32 that affects node initialization?&lt;/strong&gt; EKS 1.32 replaces the traditional bootstrap.sh script with a new tool called nodeadm for worker node initialization. This change requires explicit YAML configuration instead of automatic cluster metadata discovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q2. When will Amazon stop supporting Amazon Linux 2 (AL2) AMIs for EKS?&lt;/strong&gt; Amazon will end support for EKS AL2-optimized AMIs after November 26, 2025. Kubernetes version 1.32 is the final release where Amazon EKS will provide AL2 AMIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q3. What are the alternatives to Amazon Linux 2 for EKS nodes?&lt;/strong&gt; The main alternatives are Amazon Linux 2023 (AL2023) and Bottlerocket AMIs. AL2023 offers improved security features and a familiar environment, while Bottlerocket provides a container-optimized design with a minimal attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q4. How can I identify and fix common nodeadm errors?&lt;/strong&gt; You can use the 'nodeadm debug' command to validate critical requirements and the 'nodeadm config check' command to validate configurations. Common issues include missing IAM permissions, network connectivity problems, and incorrect node IP configurations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q5. What are the cost implications of staying on a deprecated version of EKS?&lt;/strong&gt; Running a deprecated version of EKS can cost six times more than the standard rate. Instead of $0.10 per hour, you could end up paying $0.60 per hour, potentially adding over $500 per month for each outdated cluster.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>The $10,000 Label: How We Used Go, Clean Architecture, and AWS to Build a FinOps-Driven Cloud Tagging Engine 🏷️</title>
      <dc:creator>hugolesta</dc:creator>
      <pubDate>Mon, 29 Sep 2025 16:01:54 +0000</pubDate>
      <link>https://dev.to/hugolesta/the-10000-label-how-we-used-go-clean-architecture-and-aws-to-build-a-finops-driven-cloud-2988</link>
      <guid>https://dev.to/hugolesta/the-10000-label-how-we-used-go-clean-architecture-and-aws-to-build-a-finops-driven-cloud-2988</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why Consistent Tagging is Your Company’s Most Underrated FinOps Tool:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Business Problem:&lt;/strong&gt; Imagine your cloud bill is a massive corporate expense report. Without proper tagging—simple key-value labels like &lt;code&gt;project: crm-migration&lt;/code&gt; or &lt;code&gt;owner: finance-team&lt;/code&gt; —you're paying thousands every month for line items labeled simply “Server.” This isn't just an accounting headache; it's a direct threat to cost control and security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Bloat:&lt;/strong&gt; Orphaned or forgotten AWS resources (Shadow IT) continue to generate costs because no one is accountable for terminating them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing Disputes:&lt;/strong&gt; Finance teams struggle to attribute costs accurately, leading to friction and delayed chargebacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Risks:&lt;/strong&gt; Unmanaged resources often fall outside compliance or patch cycles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We decided to solve this with &lt;strong&gt;sys-tag-manager&lt;/strong&gt;, a powerful, automated system built in Golang that acts as our centralized "Cloud Label Printer," ensuring every AWS resource is correctly accounted for, compliant, and cost-trackable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Is a Tagging Strategy?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tagging strategy is a &lt;strong&gt;structured approach to applying metadata (tags)&lt;/strong&gt; to cloud resources. Tags are simple key–value pairs like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;owner: finance-team
project: crm-migration
environment: production

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

&lt;/div&gt;



&lt;p&gt;On their own, tags look trivial. But when applied consistently across an entire cloud estate, they form the backbone of organization, governance, and cost management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tagging strategy defines:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which tags are required&lt;/strong&gt; (e.g. owner, project, environment).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How tags should be formatted&lt;/strong&gt; (naming conventions, lowercase vs camelCase, separators).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When tags should be applied&lt;/strong&gt; (at creation time vs automated correction).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who is responsible&lt;/strong&gt; for maintaining them?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For further guidance, consult the AWS documentation: &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/tagging-best-practices/tagging-best-practices.html" rel="noopener noreferrer"&gt;Best Practices for Tagging AWS Resources&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Foundation: Go, Clean Architecture, and AWS Cost Savings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wzxfdb1qzcfncwiokda.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%2F3wzxfdb1qzcfncwiokda.png" alt="Pretty gopher doing Thumb-up" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Go Advantage: Performance Meets FinOps:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While prototyping in Python is quick, a core, mission-critical tool demands performance and reliability. We chose Golang for &lt;strong&gt;sys-tag-manager&lt;/strong&gt; because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost-Efficient Execution:&lt;/strong&gt; Go's minimal memory footprint and extremely fast startup time are critical when running as AWS Lambda functions or Kubernetes CronJobs. This translates directly into lower AWS compute costs (less time billed for execution) compared to resource-heavier languages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; Static typing and robust concurrency ensure the system can efficiently handle a rapidly growing number of AWS API calls without failure, ensuring 100% compliance coverage.&lt;/li&gt;
&lt;/ol&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%2F0m618venq5pdoydk5g1g.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%2F0m618venq5pdoydk5g1g.png" alt="The clean architecture, Domain, Adapter and User Case" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Clean Architecture: Decoupling Logic from the AWS SDK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To ensure the system remains maintainable as our cloud estate scales, we invested in &lt;strong&gt;Clean Architecture&lt;/strong&gt;. This strategic separation is key to our long-term technical debt reduction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Domain (Core Business Logic):&lt;/strong&gt; Pure, independent rules (e.g., "A resource is compliant if it has the required tags: &lt;code&gt;owner&lt;/code&gt; and &lt;code&gt;project&lt;/code&gt;"). This is highly &lt;strong&gt;testable&lt;/strong&gt; and knows nothing about AWS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Cases (Application Logic):&lt;/strong&gt; Defines the "what" (e.g., "Check compliance and apply tags").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapters (AWS Implementation):&lt;/strong&gt; Isolated logic that interacts directly with specific AWS SDK services (&lt;code&gt;EC2Tagger&lt;/code&gt;, &lt;code&gt;RDSTagger&lt;/code&gt;). This prevents vendor lock-in and allows us to add new services (S3, Lambda) without touching the core business rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Compliance Engine: Terraform, SSM, and Metadata Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1dmb67cnjqdn7m87s5jz.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%2F1dmb67cnjqdn7m87s5jz.png" alt="AWS Resource explorer is the discovery layer" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Discovery Layer: Leveraging AWS Resource Explorer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before &lt;strong&gt;sys-tag-manager&lt;/strong&gt; can fix untagged resources, it must efficiently find them across accounts and Regions. We achieved this by using &lt;strong&gt;AWS Resource Explorer&lt;/strong&gt; as our primary discovery and inventory layer.&lt;/p&gt;

&lt;p&gt;Instead of writing complex API calls to list every resource type across every Region, &lt;strong&gt;sys-tag-manager&lt;/strong&gt; utilizes Resource Explorer's unified search capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discovery:&lt;/strong&gt; &lt;strong&gt;sys-tag-manager&lt;/strong&gt; uses the Resource Explorer API to query the entire cloud estate for resources that are missing required tags (e.g., &lt;code&gt;tag:owner is absent&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; For each untagged resource found, &lt;strong&gt;sys-tag-manager&lt;/strong&gt; checks its metadata against the centralized, correct rules stored in &lt;strong&gt;AWS SSM Parameter Store&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correction:&lt;/strong&gt; The system then applies the right tags, assigning the resource to the correct owner or project, ensuring immediate compliance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design significantly streamlined the core tagging loop, ensuring we are not just efficient in applying tags (Golang), but also efficient in &lt;strong&gt;finding&lt;/strong&gt; them (Resource Explorer), saving API call costs and latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Harnessing Shared Infrastructure: The Fallback Mechanism&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While our primary goal is to enforce resource-specific tagging, we recognized that some resources are &lt;strong&gt;"Shared Infrastructure"&lt;/strong&gt; (e.g., core networking components, centralized security groups) that don't belong to a single owner. Addressing this was a critical design challenge.&lt;/p&gt;

&lt;p&gt;Our solution was a &lt;strong&gt;smart fallback mechanism&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tagging engine first checks for the required resource-specific tags.&lt;/li&gt;
&lt;li&gt;If the tags are missing, it then checks a predefined list of &lt;strong&gt;AWS ARNs&lt;/strong&gt; (Amazon Resource Names) that are designated as shared infrastructure.&lt;/li&gt;
&lt;li&gt;If an ARN matches, the system &lt;strong&gt;applies a generic, shared set of tags&lt;/strong&gt; (e.g., &lt;code&gt;owner: platform-team&lt;/code&gt;, &lt;code&gt;charge-code: shared-infra&lt;/code&gt;) instead of flagging it as non-compliant. This prevents false positives and ensures accurate cost attribution for common resources.&lt;/li&gt;
&lt;/ol&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%2Fw7mnmtvltdw9tm2yeymp.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%2Fw7mnmtvltdw9tm2yeymp.png" alt="Hashicorp and AWS hugging" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Terraform + SSM Parameter Store Synergy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The true power of &lt;strong&gt;sys-tag-manager&lt;/strong&gt; lies in its ability to dynamically enforce tagging rules based on centralized, auditable metadata.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Rule Source:&lt;/strong&gt; We leverage &lt;strong&gt;AWS Systems Manager (SSM) Parameter Store&lt;/strong&gt; to store the required tag keys, values, and compliance rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform as the Single Source of Truth:&lt;/strong&gt; The compliance rules in SSM are managed exclusively by Terraform. This means:

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Immutability:&lt;/strong&gt; Every rule change is tracked, reviewed, and deployed via a GitOps workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation:&lt;/strong&gt; When a new project is created in Terraform, the required tag values for that project are automatically pushed to SSM, immediately making those tags valid for the tag manager checks.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;This integration ensures that the FinOps rules are always aligned with the deployed infrastructure definitions, creating a clean, traceable metadata loop.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Business Impact: Quantifiable Results for FinOps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjw9s6sh98t6w6is5l7z6.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%2Fjw9s6sh98t6w6is5l7z6.png" alt="Descriptive pie charts" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before sys-tag-manager&lt;/th&gt;
&lt;th&gt;After sys-tag-manager&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Value Proposition&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compliance Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Weeks (Manual Audits)&lt;/td&gt;
&lt;td&gt;Minutes (Automated Correction)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Faster cost allocation &amp;amp; reduced risk.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Orphaned Resources&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~12% (Estimate)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt;1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Direct savings on wasted AWS spend.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FinOps Accuracy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High Friction/Disputes&lt;/td&gt;
&lt;td&gt;High Trust/Automated Showback&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Enables accurate, automated chargeback.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By investing in this system, we have fundamentally shifted from reactive tag auditing to &lt;strong&gt;proactive, automated compliance enforcement&lt;/strong&gt;. This not only saves engineering hours but directly enables our finance team to confidently utilize &lt;strong&gt;AWS Cost and Usage Reports (CUR)&lt;/strong&gt; for accurate showback and chargeback, making our entire cloud operation more accountable and &lt;strong&gt;financially efficient&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Wrapping Up: Compliance as Code, Savings as the Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sys-tag-manager&lt;/strong&gt; is more than just an automation script; it's the enforcement layer for our FinOps and security policies, ensuring that our cloud environment is self-healing and financially accountable.&lt;/p&gt;

&lt;p&gt;By embracing &lt;strong&gt;Golang&lt;/strong&gt; for performance, &lt;strong&gt;Clean Architecture&lt;/strong&gt; for maintainability, and the &lt;strong&gt;Terraform + SSM&lt;/strong&gt; synergy for centralized metadata management, we've transformed tagging from a manual burden into an automated, cost-saving asset. This shift has given us the confidence that every dollar spent on AWS is trackable, auditable, and directly attributed to a business owner or project.&lt;/p&gt;

&lt;p&gt;The result is a culture of &lt;strong&gt;Compliance as Code&lt;/strong&gt; where engineers can focus on feature delivery, knowing that the foundational governance—the tagging—is handled automatically and efficiently by &lt;strong&gt;sys-tag-manager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's Keep the Conversation Going&lt;/strong&gt; 🗣️&lt;br&gt;
We've focused on the technical core of &lt;strong&gt;sys-tag-manager&lt;/strong&gt;, but the true organizational victory was how we scaled this system across dozens of teams without friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Would you be interested in learning more about how we automated the communication of compliance status and tagging fixes to developers, FinOps, and management?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;I'd love to hear your thoughts!&lt;/strong&gt; What's the biggest tagging challenge you face in your organization? &lt;strong&gt;Share your experiences and suggestions in the comments below!&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Feel free to connect with me on LinkedIn to discuss our approach to automated communication and team onboarding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/hugo-lesta-5a058138/" rel="noopener noreferrer"&gt;Hugo Lesta&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/hugolesta" rel="noopener noreferrer"&gt;Hugo Lesta's GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>terraform</category>
      <category>go</category>
    </item>
  </channel>
</rss>
