<?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: Naor Peled</title>
    <description>The latest articles on DEV Community by Naor Peled (@naorpeled).</description>
    <link>https://dev.to/naorpeled</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%2F1098583%2F93056eec-5107-43b8-8d74-354d5f5e370e.jpeg</url>
      <title>DEV Community: Naor Peled</title>
      <link>https://dev.to/naorpeled</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naorpeled"/>
    <language>en</language>
    <item>
      <title>Running a serverless AI code review agent on AWS Lambda with PR-Agent and CDK</title>
      <dc:creator>Naor Peled</dc:creator>
      <pubDate>Sun, 27 Sep 2026 13:26:09 +0000</pubDate>
      <link>https://dev.to/aws-builders/running-a-serverless-ai-code-review-agent-on-aws-lambda-with-pr-agent-and-cdk-40gd</link>
      <guid>https://dev.to/aws-builders/running-a-serverless-ai-code-review-agent-on-aws-lambda-with-pr-agent-and-cdk-40gd</guid>
      <description>&lt;p&gt;Code reviews are an essential part of our day to day work.&lt;br&gt;
Their importance and way of execution have changed a bit in the GenAI era.&lt;/p&gt;

&lt;p&gt;In this post, I'll show you how you can self host your own code review agent.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is PR-Agent
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/The-PR-Agent/pr-agent" rel="noopener noreferrer"&gt;PR-Agent&lt;/a&gt; is an open-source project that allows you to perform code reviews using a customizable AI agent in a self-hosted way.&lt;br&gt;
It works with all common AI model providers and local models.&lt;/p&gt;

&lt;p&gt;The flow is the following:&lt;br&gt;
You open a pull request, and PR-Agent updates the description and posts a set of code suggestions. You can also talk to it in the PR with slash commands like &lt;code&gt;/review&lt;/code&gt;, &lt;code&gt;/improve&lt;/code&gt; or &lt;code&gt;/ask what does this migration do?&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The project initially started at &lt;a href="https://qodo.ai" rel="noopener noreferrer"&gt;Qodo&lt;/a&gt;, which works on an enterprise code review solution, and was donated to the community last year. It now lives in its own GitHub org and is maintained by the community.&lt;/p&gt;

&lt;p&gt;In this post, we'll explore how to deploy it using AWS Lambda.&lt;/p&gt;

&lt;p&gt;If you prefer to just clone and run it, everything in this post is also in &lt;a href="https://github.com/naorpeled/pr-agent-lambda-cdk" rel="noopener noreferrer"&gt;pr-agent-lambda-cdk&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  A few words about me
&lt;/h2&gt;

&lt;p&gt;Hi, I'm Naor, a software engineer for the past 7 years professionally and an open source contributor/maintainer for about 6 years. I am also an &lt;a href="https://builder.aws.com/community/community-builders" rel="noopener noreferrer"&gt;AWS Community Builder&lt;/a&gt; in the DevTools category and love sharing insights about modern developer workflows and cloud tooling.&lt;/p&gt;

&lt;p&gt;I'm the lead maintainer of PR-Agent and I also maintain projects like &lt;a href="https://github.com/typeorm/typeorm" rel="noopener noreferrer"&gt;TypeORM&lt;/a&gt; and a few others you can find on &lt;a href="https://github.com/naorpeled" rel="noopener noreferrer"&gt;my GitHub page&lt;/a&gt;. I currently work for &lt;a href="https://www.groundcover.com/" rel="noopener noreferrer"&gt;groundcover&lt;/a&gt;, where we're building a &lt;a href="https://www.groundcover.com/byoc" rel="noopener noreferrer"&gt;BYOC&lt;/a&gt;, &lt;a href="https://www.groundcover.com/ebpf" rel="noopener noreferrer"&gt;eBPF&lt;/a&gt; powered observability solution.&lt;/p&gt;
&lt;h2&gt;
  
  
  A small overview on how PR-Agent can be used
&lt;/h2&gt;

&lt;p&gt;PR-Agent is a Python project that has two modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CLI.&lt;/li&gt;
&lt;li&gt;A FastAPI server that listens to requests from different git providers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can override the default configurations by creating a &lt;a href="https://docs.pr-agent.ai/usage-guide/configuration_options/" rel="noopener noreferrer"&gt;configuration file&lt;/a&gt;.&lt;br&gt;
The configuration file needs to be named &lt;code&gt;.pr_agent.toml&lt;/code&gt; and can sit in two locations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your repo's root dir.&lt;/li&gt;
&lt;li&gt;In a repo called &lt;code&gt;pr-agent-settings&lt;/code&gt; that sits under your git provider's org.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work on a monorepo, you can also add a &lt;code&gt;.pr_agent.toml&lt;/code&gt; inside sub-directories, for example one per service. PR-Agent applies the ones on the path of the files changed in the PR, and the closest one wins. These files can change things like the model, the ignored files and the settings of each tool, while some settings can only be set in the root file. It's off by default, to turn it on set &lt;code&gt;enable_per_directory_settings = true&lt;/code&gt; under &lt;code&gt;[config]&lt;/code&gt; in your root &lt;code&gt;.pr_agent.toml&lt;/code&gt;. For now it works on GitHub and GitLab.&lt;/p&gt;

&lt;p&gt;Every setting can also be set with an environment variable, using the section and the key with a double underscore between them. For example, &lt;code&gt;webhook_secret&lt;/code&gt; under &lt;code&gt;[github]&lt;/code&gt; becomes &lt;code&gt;GITHUB__WEBHOOK_SECRET&lt;/code&gt;. Environment variables are applied last, so they win over &lt;code&gt;.pr_agent.toml&lt;/code&gt;. We'll use them a lot, since on Lambda there's no config file to mount.&lt;/p&gt;

&lt;p&gt;Model calls go through &lt;a href="https://docs.litellm.ai/" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt;, so moving from OpenAI to Bedrock or to a local model is just a config change. For big PRs, PR-Agent uses a &lt;a href="https://docs.pr-agent.ai/core-abilities/compression_strategy/" rel="noopener noreferrer"&gt;compression strategy&lt;/a&gt; that fits the most relevant parts of the diff into a fixed token budget, so even big PRs fit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Lambda
&lt;/h2&gt;

&lt;p&gt;The GitHub Action is the quickest way to get started, and for a single repo it works great. Once you have several repos, a git provider other than GitHub, or you don't want model credentials in your CI, it's easier to have one deployment that everything talks to.&lt;/p&gt;

&lt;p&gt;For that I like Lambda, for a few reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A review only happens when something happens on a PR, and there's nothing to do in between. Running a container 24/7 for something that fires a few dozen times a day means paying for idle time.&lt;/li&gt;
&lt;li&gt;Every setting can be passed as an environment variable, so there's no config file or volume to mount.&lt;/li&gt;
&lt;li&gt;The token budget also limits how long a review can take, so it finishes well within Lambda's 15 minute limit.&lt;/li&gt;
&lt;li&gt;Nothing has to be kept between invocations. PR-Agent fetches everything it needs from the git provider's API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PR-Agent already supports Lambda. This is the GitHub Lambda handler, &lt;code&gt;pr_agent/servers/github_lambda_webhook.py&lt;/code&gt; (I removed the logging from the &lt;code&gt;except&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mangum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mangum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Middleware&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette_context.middleware&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RawContextMiddleware&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pr_agent.servers.github_app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pr_agent.config_loader&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;apply_secrets_manager_config&lt;/span&gt;
    &lt;span class="nf"&gt;apply_secrets_manager_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# falls back to environment variables
&lt;/span&gt;
&lt;span class="n"&gt;middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RawContextMiddleware&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include_router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mangum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses the same FastAPI router as the regular server, wraps it with &lt;a href="https://github.com/Kludex/mangum" rel="noopener noreferrer"&gt;Mangum&lt;/a&gt;, which translates Lambda events into ASGI requests, and loads the config from Secrets Manager on cold start. So the regular server and the Lambda deployment share the same code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The review runs inside the invocation
&lt;/h3&gt;

&lt;p&gt;On Lambda, PR-Agent runs the whole review before it responds to the webhook. That's actually what we want, because Lambda freezes the function once it returns, so a review running in the background would get stuck. It does mean the function needs a long timeout (the docs recommend at least 3 minutes).&lt;/p&gt;

&lt;p&gt;The side effect is that GitHub shows every delivery as timed out, since it only waits 10 seconds. The comments are still posted, so you can ignore it. GitLab.com is stricter, and disables the webhook after a few timeouts in a row.&lt;/p&gt;

&lt;p&gt;The fix is a small function in front of PR-Agent that calls it asynchronously and responds right away. I left it out of this post to keep it short, but the companion repo has it (&lt;code&gt;ASYNC_REVIEWS=true&lt;/code&gt;), and it's on by default for every provider except GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AWS pieces
&lt;/h2&gt;

&lt;p&gt;PR-Agent ships with heavy Python dependencies, so a zip deployment is not going to be fun. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/images-create.html" rel="noopener noreferrer"&gt;Lambda container images&lt;/a&gt; can be up to 10 GB uncompressed, compared to 250 MB unzipped for zips. Lambda pulls the image from &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html" rel="noopener noreferrer"&gt;ECR&lt;/a&gt;, in the same region as the function.&lt;/p&gt;

&lt;p&gt;In front of the function we'll use a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" rel="noopener noreferrer"&gt;Lambda Function URL&lt;/a&gt;, a dedicated HTTPS endpoint without API Gateway. A webhook receiver doesn't need routing, stages or usage plans, but the main reason is the timeout. API Gateway times out after 29 seconds by default, and as we saw, the review runs inside the request. A Function URL waits for the function's own timeout.&lt;/p&gt;

&lt;p&gt;It uses &lt;code&gt;authType: NONE&lt;/code&gt;, since GitHub can't sign requests with SigV4, and PR-Agent checks the webhook's HMAC signature itself. The tradeoff is no WAF, no usage plans, and no custom domain unless you put CloudFront in front of it.&lt;/p&gt;

&lt;p&gt;GitHub App private keys and webhook secrets go in &lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" rel="noopener noreferrer"&gt;Secrets Manager&lt;/a&gt; and not in environment variables, where anyone with console read access can see them. PR-Agent already knows how to read them from there.&lt;/p&gt;

&lt;p&gt;For the model I'm using &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, which keeps everything inside your own AWS account. LiteLLM supports Bedrock, so PR-Agent does too.&lt;/p&gt;

&lt;p&gt;And all of it is defined in &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html" rel="noopener noreferrer"&gt;CDK&lt;/a&gt;, which compiles down to CloudFormation but lets you write actual TypeScript instead of YAML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6uc1opq3kmertbnvha9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd6uc1opq3kmertbnvha9.png" alt="AWS architecture diagram: a developer opens a pull request, the git provider posts a webhook to a Lambda Function URL, the Lambda function runs a container image pulled from Amazon ECR, reads credentials from AWS Secrets Manager on cold start, calls Amazon Bedrock through its execution role, writes to CloudWatch Logs, and posts the review back on the pull request." width="799" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Before you start
&lt;/h3&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account with the CLI v2 configured, and credentials that can create IAM roles.&lt;/li&gt;
&lt;li&gt;Docker running locally.&lt;/li&gt;
&lt;li&gt;Node 20 or newer, &lt;code&gt;jq&lt;/code&gt; and &lt;code&gt;openssl&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick a region and use it for everything. I'll use &lt;code&gt;us-east-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then pick a model and make sure you have access to it before you build anything. Otherwise you'll end up debugging an IAM policy that's actually fine.&lt;/p&gt;

&lt;p&gt;I'll use Claude Sonnet 4.5, with Haiku 4.5 as the fallback. Anthropic models need a First Time Use form, which you submit from the model catalog in the Bedrock console, either in each account or once from your org's management account. Without it, calls fail with an error saying the use case details weren't submitted.&lt;/p&gt;

&lt;p&gt;The first call to a model that's sold through AWS Marketplace also creates a Marketplace subscription, and the caller needs &lt;code&gt;aws-marketplace:Subscribe&lt;/code&gt; for that. The Lambda's role doesn't have it, so invoke each model once from the Bedrock console playground as an admin. If you skip this you'll get &lt;code&gt;AccessDeniedException&lt;/code&gt;, sometimes only after the first few calls worked. The &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html" rel="noopener noreferrer"&gt;model access docs&lt;/a&gt; list what each model needs.&lt;/p&gt;

&lt;p&gt;Any model LiteLLM can call on Bedrock works. After the stack, I'll show how to switch to DeepSeek V3.2, which doesn't need either of these steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the GitHub App
&lt;/h3&gt;

&lt;p&gt;PR-Agent authenticates as a GitHub App, so let's start there. Go to Settings, Developer settings, GitHub Apps and create a new one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Permissions: &lt;strong&gt;Pull requests&lt;/strong&gt; and &lt;strong&gt;Issues&lt;/strong&gt; read and write, &lt;strong&gt;Contents&lt;/strong&gt; and &lt;strong&gt;Metadata&lt;/strong&gt; read only.&lt;/li&gt;
&lt;li&gt;Events: &lt;strong&gt;Pull request&lt;/strong&gt;, &lt;strong&gt;Pull request review&lt;/strong&gt;, &lt;strong&gt;Pull request review comment&lt;/strong&gt; and &lt;strong&gt;Issue comment&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under Webhook, tick &lt;strong&gt;Active&lt;/strong&gt;, put &lt;code&gt;https://example.com&lt;/code&gt; as the URL for now, and set a webhook secret. GitHub won't save an active webhook without a URL, and we'll only get the real one in step 6. Generate the secret in the same shell you'll use for the rest of the steps, since the next commands read it from there:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;
   &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$WEBHOOK_SECRET&lt;/span&gt;   &lt;span class="c"&gt;# paste this into the GitHub App form&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Generate a private key, and copy the App ID from the top of the settings page:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;123456
   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PEM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/Downloads/your-app.2026-09-27.private-key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then install the App on the repos you want reviewed. The &lt;a href="https://docs.pr-agent.ai/installation/github/" rel="noopener noreferrer"&gt;install docs&lt;/a&gt; have more details.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Put the credentials in Secrets Manager
&lt;/h3&gt;

&lt;p&gt;PR-Agent reads a single JSON secret, where the config keys are flattened into dotted strings. The private key has to be a single JSON string with escaped newlines. &lt;code&gt;jq&lt;/code&gt; can build the whole file for us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-Rs&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; app_id &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; secret &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WEBHOOK_SECRET&lt;/span&gt;:?run&lt;span class="p"&gt; step 1 first&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"github.app_id": $app_id, "github.webhook_secret": $secret, "github.private_key": .}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PEM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"github.app_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"github.webhook_secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-webhook-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"github.private_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-----BEGIN RSA PRIVATE KEY-----&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;MIIE...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;-----END RSA PRIVATE KEY-----&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store it, and then delete the local copies. You don't need them anymore, and GitHub can generate a new key whenever you need one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws secretsmanager create-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; pr-agent/config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-string&lt;/span&gt; file://config.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;config.json &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PEM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to pass &lt;code&gt;--region&lt;/code&gt;. On Lambda, the secret is looked up in the function's region, so a secret in another region won't be found. When that happens, PR-Agent logs &lt;code&gt;Failed to get secrets from AWS Secrets Manager&lt;/code&gt; on cold start and continues without credentials, and every webhook after that gets a 403 with &lt;code&gt;Webhook secret not configured&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two more things to know about this secret:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only keys with exactly one dot are read. Anything deeper is silently ignored.&lt;/li&gt;
&lt;li&gt;A key is only applied if the setting is unset or empty. So the secret is only for credentials. To change a setting that has a default, use a &lt;code&gt;SECTION__KEY&lt;/code&gt; environment variable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Copy the image into ECR
&lt;/h3&gt;

&lt;p&gt;The project publishes Lambda images to Docker Hub on every release: &lt;code&gt;pragent/pr-agent:&amp;lt;version&amp;gt;-github_lambda&lt;/code&gt;, a rolling &lt;code&gt;pragent/pr-agent:github_lambda&lt;/code&gt; tag, and the same for GitLab. So there's no need to build one yourself. I'm using 0.46.0, which is the latest at the time of writing.&lt;/p&gt;

&lt;p&gt;You do need to copy it into your own ECR, because that's the only registry Lambda pulls from. The published tags are multi-arch (amd64 and arm64), and Lambda &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/images-create.html" rel="noopener noreferrer"&gt;doesn't accept multi-arch images&lt;/a&gt;, so pull a single platform. I'd also pin a version and not use the rolling tag, so a redeploy a few months from now doesn't quietly pick up a different build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ACCOUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; Account &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
&lt;span class="nv"&gt;TAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.46.0-github_lambda

aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; pr-agent &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$REGION&lt;/span&gt;

aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT&lt;/span&gt;.dkr.ecr.&lt;span class="nv"&gt;$REGION&lt;/span&gt;.amazonaws.com

docker pull &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64 pragent/pr-agent:&lt;span class="nv"&gt;$TAG&lt;/span&gt;
docker tag pragent/pr-agent:&lt;span class="nv"&gt;$TAG&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT&lt;/span&gt;.dkr.ecr.&lt;span class="nv"&gt;$REGION&lt;/span&gt;.amazonaws.com/pr-agent:&lt;span class="nv"&gt;$TAG&lt;/span&gt;
docker push &lt;span class="nv"&gt;$ACCOUNT&lt;/span&gt;.dkr.ecr.&lt;span class="nv"&gt;$REGION&lt;/span&gt;.amazonaws.com/pr-agent:&lt;span class="nv"&gt;$TAG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use Docker Desktop with the containerd image store, &lt;code&gt;docker push&lt;/code&gt; might push the multi-arch index anyway. Run &lt;code&gt;docker manifest inspect&lt;/code&gt; on the ECR tag. If you see a &lt;code&gt;layers&lt;/code&gt; array you're good, if you see &lt;code&gt;manifests&lt;/code&gt;, Lambda will reject it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The CDK stack
&lt;/h3&gt;

&lt;p&gt;The stack looks up the secret from step 2 and the ECR repo from step 3 by name, and the image has to exist when you deploy. That's why we did those first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;pr-agent-infra &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;pr-agent-infra
npx cdk init app &lt;span class="nt"&gt;--language&lt;/span&gt; typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cdk init&lt;/code&gt; installs the dependencies and names things after the directory, so the stack goes in &lt;code&gt;lib/pr-agent-infra-stack.ts&lt;/code&gt;. Replace what's there with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ecr&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-ecr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;secretsmanager&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-secretsmanager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-logs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;constructs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// the tag you pushed in step 3&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.46.0-github_lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// cross-region inference profile ids, the "us." prefix is what makes them profiles&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-sonnet-4-5-20250929-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us.anthropic.claude-haiku-4-5-20251001-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PrAgentInfraStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&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;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ecr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromRepositoryName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PrAgentRepo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pr-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;secretsmanager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromSecretNameV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PrAgentConfig&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pr-agent/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DockerImageFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PrAgentFunction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;code&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="nx"&gt;DockerImageCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEcr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tagOrDigest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IMAGE_TAG&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="c1"&gt;// has to match the platform you pulled&lt;/span&gt;
      &lt;span class="na"&gt;architecture&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="nx"&gt;Architecture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;X86_64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// memory buys CPU on Lambda, and 2 GB is a reasonable spot for this&lt;/span&gt;
      &lt;span class="na"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// the review runs inside the invocation, and a big PR runs describe,&lt;/span&gt;
      &lt;span class="c1"&gt;// review and improve back to back. you only pay for the time used&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="c1"&gt;// caps the blast radius when someone merges a train of 30 PRs.&lt;/span&gt;
      &lt;span class="c1"&gt;// new accounts start on a reduced concurrency quota, often 10. there,&lt;/span&gt;
      &lt;span class="c1"&gt;// drop this line: Lambda keeps 100 unreserved, so reserving fails&lt;/span&gt;
      &lt;span class="na"&gt;reservedConcurrentExecutions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Lambda log groups never expire by default&lt;/span&gt;
      &lt;span class="na"&gt;logGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LogGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PrAgentLogs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RetentionDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ONE_MONTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// env vars can't contain dots, and Dynaconf reads SECTION__KEY into section.key&lt;/span&gt;
        &lt;span class="na"&gt;CONFIG__GIT_PROVIDER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;CONFIG__PUBLISH_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;CONFIG__MODEL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bedrock/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;CONFIG__FALLBACK_MODELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`["bedrock/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FALLBACK_MODEL&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="c1"&gt;// the default of 32000 is a fraction of what current models take&lt;/span&gt;
        &lt;span class="na"&gt;CONFIG__MAX_MODEL_TOKENS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;128000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;CONFIG__SECRET_PROVIDER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws_secrets_manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;AWS_SECRETS_MANAGER__SECRET_ARN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// read straight from the process env by the model layer. on Lambda it&lt;/span&gt;
        &lt;span class="c1"&gt;// makes boto3 resolve the execution role credentials the runtime&lt;/span&gt;
        &lt;span class="c1"&gt;// injects, so there are no static keys anywhere&lt;/span&gt;
        &lt;span class="na"&gt;AWS_USE_IMDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;AWS_REGION_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// /tmp is the only writable path in a Lambda container&lt;/span&gt;
        &lt;span class="na"&gt;AZURE_DEVOPS_CACHE_DIR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. the cross-region inference profile, in your account&lt;/span&gt;
    &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock:InvokeModel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock:InvokeModelWithResponseStream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&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:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:inference-profile/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;`arn:aws:bedrock:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:inference-profile/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FALLBACK_MODEL&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="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. the underlying foundation models, in every region the profile can route to.&lt;/span&gt;
    &lt;span class="c1"&gt;//    foundation-model ARNs have an empty account field. skip this and calls fail.&lt;/span&gt;
    &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToRolePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock:InvokeModel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bedrock:InvokeModelWithResponseStream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;`arn:aws:bedrock:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;::foundation-model/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;us|eu|apac|global&lt;/span&gt;&lt;span class="se"&gt;)\.&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;`arn:aws:bedrock:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;::foundation-model/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FALLBACK_MODEL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;us|eu|apac|global&lt;/span&gt;&lt;span class="se"&gt;)\.&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addFunctionUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FunctionUrlAuthType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NONE&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FunctionUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebhookUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;api/v1/github_webhooks`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LogGroup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logGroupName&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;cdk init&lt;/code&gt; also generates &lt;code&gt;bin/pr-agent-infra.ts&lt;/code&gt; with the &lt;code&gt;env&lt;/code&gt; block commented out, which makes the stack region-agnostic. The region list in the second policy statement isn't, so set the region explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrAgentInfraStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PrAgentInfraStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CDK_DEFAULT_ACCOUNT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why two IAM statements
&lt;/h3&gt;

&lt;p&gt;Sonnet 4.5 can only be called through a cross-region inference profile (the &lt;code&gt;us.&lt;/code&gt; prefix), which sends each request to one of a few US regions. AWS &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-prereq.html" rel="noopener noreferrer"&gt;requires permissions&lt;/a&gt; on both the profile and the model in each of those regions, so the stack has two statements.&lt;/p&gt;

&lt;p&gt;The region list above works for &lt;code&gt;us-east-1&lt;/code&gt;, &lt;code&gt;us-east-2&lt;/code&gt; and &lt;code&gt;us-west-2&lt;/code&gt;. In Europe, use the &lt;code&gt;eu.&lt;/code&gt; prefix and your EU regions, and you can check the exact list with &lt;code&gt;aws bedrock get-inference-profile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For a simpler setup, you can use a model that runs in your own region, like DeepSeek V3.2 below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using a different model
&lt;/h3&gt;

&lt;p&gt;DeepSeek V3.2 runs in your own region, and it doesn't need the use case form or a Marketplace subscription. Here I use it as both the main model and the fallback, and the region list only needs your region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deepseek.v3.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deepseek.v3.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// and in the second policy statement: ['us-east-1']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first policy statement will then grant access to an inference profile that doesn't exist, which is harmless. &lt;code&gt;aws bedrock list-foundation-models&lt;/code&gt; shows what's available in your region, but not what your account is allowed to call, so the first real call is what tells you.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cdk bootstrap   &lt;span class="c"&gt;# once per account/region&lt;/span&gt;
npx cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server has a health check at &lt;code&gt;/&lt;/code&gt;, so you can check that the image, the Function URL and the cold start all work before connecting GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="nt"&gt;--stack-name&lt;/span&gt; PrAgentInfraStack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='FunctionUrl'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get &lt;code&gt;{"status":"ok"}&lt;/code&gt;. If you call the &lt;code&gt;WebhookUrl&lt;/code&gt; output instead, you'll get a 405, because that path only accepts POST.&lt;/p&gt;

&lt;p&gt;A healthy response doesn't mean the secret was found. Tail the logs and look for &lt;code&gt;Applied AWS Secrets Manager configuration&lt;/code&gt; on cold start. If you see &lt;code&gt;Failed to get secrets from AWS Secrets Manager&lt;/code&gt; instead, the secret is in another region or has a different name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws logs &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="nt"&gt;--stack-name&lt;/span&gt; PrAgentInfraStack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Stacks[0].Outputs[?OutputKey=='LogGroup'].OutputValue"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Connect the webhook
&lt;/h3&gt;

&lt;p&gt;Take the &lt;code&gt;WebhookUrl&lt;/code&gt; output and paste it into your GitHub App's webhook settings instead of &lt;code&gt;example.com&lt;/code&gt;. Keep the same secret. Then open a test PR.&lt;/p&gt;

&lt;p&gt;The App's "Recent Deliveries" tab shows what happened. Timed out is expected, as we saw earlier. A 403 means the signature check failed, and the response body tells you why: &lt;code&gt;Request signatures didn't match!&lt;/code&gt; means the secret in the App is different, and &lt;code&gt;Webhook secret not configured&lt;/code&gt; means the secret from step 2 wasn't found. For anything else, check the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cdk destroy&lt;/code&gt; only removes what CDK created, so the secret and the ECR repo need to be deleted separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cdk destroy
aws ecr delete-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; pr-agent &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws secretsmanager delete-secret &lt;span class="nt"&gt;--secret-id&lt;/span&gt; pr-agent/config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force-delete-without-recovery&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then delete the GitHub App, or at least uninstall it, so it stops sending webhooks to a URL that doesn't exist anymore.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;--force-delete-without-recovery&lt;/code&gt;, the secret stays in a 30 day recovery window. You aren't billed for it, but you can't create a new secret with the same name until the window ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Lambda is the cheap part. Its &lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;free tier&lt;/a&gt; includes 1 million requests a month, and enough compute time for over 3,000 one minute reviews with 2 GB of memory. After that, each review costs about a fifth of a cent. On top of that you pay $0.40 a month for the secret and about $0.10 per GB-month for the image in ECR. Most of the cost is the model, and it depends on how big your PRs are, so I'd run it for a week on a real repo and check the &lt;a href="https://aws.amazon.com/bedrock/pricing/" rel="noopener noreferrer"&gt;Bedrock pricing page&lt;/a&gt; before estimating.&lt;/p&gt;

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

&lt;p&gt;PR-Agent is community owned and we're always looking for contributors. If you deploy this and run into issues, feel free to &lt;a href="https://github.com/The-PR-Agent/pr-agent/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;, that feedback really helps the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need guidance or just want to talk?
&lt;/h2&gt;

&lt;p&gt;Feel free to reach out to me over &lt;a href="https://www.linkedin.com/in/naorpeled" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/thehecticbyte" rel="noopener noreferrer"&gt;x&lt;/a&gt; or email me at &lt;a href="mailto:me@naor.dev"&gt;me@naor.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you enjoyed this post and want to follow along with my open source journey, feel free to follow me here and on &lt;a href="https://github.com/naorpeled" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

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

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why open source matters more now, and how to get started</title>
      <dc:creator>Naor Peled</dc:creator>
      <pubDate>Tue, 07 Jul 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/why-open-source-matters-more-now-and-how-to-get-started-4n6o</link>
      <guid>https://dev.to/aws-builders/why-open-source-matters-more-now-and-how-to-get-started-4n6o</guid>
      <description>&lt;p&gt;Open source is one of the things that helped me grow significantly in my career over the last few years.&lt;/p&gt;

&lt;p&gt;Lately, I've been thinking a lot about its importance in this GenAI era. I see a lot of value in it and think it matters even more now than it used to.&lt;br&gt;
In this post, I will share my journey and point of view.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is OSS and why should you get into it?
&lt;/h2&gt;

&lt;p&gt;OSS is short for open source software, meaning software that is open to the world, where anyone can suggest changes, share thoughts on its internals, and help shape its roadmap.&lt;/p&gt;

&lt;p&gt;On a personal and professional level, I feel that the ROI has been incredible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I met amazing and talented people who became my close friends and taught me a lot of things that helped me grow.&lt;/li&gt;
&lt;li&gt;I learned to embrace imperfection. We try our best to avoid mistakes, but at scale, being agile and letting go of perfection are necessary to move the needle.&lt;/li&gt;
&lt;li&gt;I get a feeling of fulfillment from knowing I'm helping other people improve their software.&lt;/li&gt;
&lt;li&gt;I've connected with people I might not have otherwise reached.&lt;/li&gt;
&lt;li&gt;I gained experience with technologies I didn't get to work with at work in a meaningful way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I think it matters even more now
&lt;/h2&gt;

&lt;p&gt;A few reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A lot of AI coding models are trained on public repositories. So when you contribute today, you're not just helping the people using the project now, your contribution helps shape the tools the next generation of engineers will build with.&lt;/li&gt;
&lt;li&gt;When AI can generate a first draft of almost anything, the skills that stand out shift towards judgment, reviewing code you didn't write, and finding your way around large unfamiliar codebases. Open source is one of the best places to practice exactly those, on real projects with real feedback.&lt;/li&gt;
&lt;li&gt;When you're just starting, you hit the classic catch-22: you need experience to get a job, but you need a job to get experience. Open source is one of the few ways that helps you get out of it, you get to work on real projects, with real users and real maintainers. That's always been true, but it matters even more now that junior roles are harder to get into.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A few words about me
&lt;/h2&gt;

&lt;p&gt;Hi, I'm Naor, a software engineer for the past 7 years professionally and an open source contributor/maintainer for about 6 years. I am also an &lt;a href="https://builder.aws.com/community/community-builders" rel="noopener noreferrer"&gt;AWS Community Builder&lt;/a&gt; in the DevTools category and love sharing insights about modern developer workflows and cloud tooling.&lt;/p&gt;

&lt;p&gt;Some of the projects I maintain are used by tens of millions of developers weekly. You can check them out on &lt;a href="https://github.com/naorpeled" rel="noopener noreferrer"&gt;my GitHub page&lt;/a&gt;.&lt;br&gt;
I'm all about community and I try to go to as many meetups and conferences as possible to learn more and meet amazing people who I can potentially collaborate with.&lt;/p&gt;

&lt;p&gt;I currently work for &lt;a href="http://groundcover.com/" rel="noopener noreferrer"&gt;groundcover&lt;/a&gt;, where we're building a &lt;a href="https://www.groundcover.com/byoc" rel="noopener noreferrer"&gt;BYOC&lt;/a&gt;, &lt;a href="https://www.groundcover.com/ebpf" rel="noopener noreferrer"&gt;eBPF&lt;/a&gt; powered observability solution. If you're into cloud native engineering or modern infrastructure, I highly recommend checking us out 😎&lt;/p&gt;

&lt;h2&gt;
  
  
  How my journey began
&lt;/h2&gt;

&lt;p&gt;My first PRs were for fixing typos and adding translations in docs for Vue and React. Fixing a typo or correcting docs is one of the quickest and easiest ways to start contributing to open source.&lt;/p&gt;

&lt;p&gt;In parallel to these contributions, I joined a local community for people who want to contribute to OSS. There, I found a project that had some open tasks. I asked to be assigned to one of them, and my code change ended up getting merged.&lt;/p&gt;

&lt;p&gt;In the following year, in the company I worked for, we used several products and projects that were open source.&lt;br&gt;
While working on things, I created code change requests that fixed some bugs and got merged.&lt;/p&gt;

&lt;p&gt;In mid 2022, while working with &lt;a href="https://aws.amazon.com/dynamodb" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt;, we used a project called &lt;code&gt;dynamodb-toolbox&lt;/code&gt; that helps manage entities and query DynamoDB. As we relied on the project heavily, I wanted to take part in it and opened an issue where I asked if I could help maintain the library. After talking to the author, Jeremy, for a bit, I started co-maintaining it along with other projects that Jeremy created.&lt;br&gt;
I would say that after that, the snowball effect started, and after I gained more experience co-maintaining these projects, I joined more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first contribution(s)
&lt;/h2&gt;

&lt;p&gt;In order to find projects to work on, I would first suggest to go for technologies you're already working with, because then you might already have pain points you would want to improve for others and yourself.&lt;br&gt;
Also, I personally recommend trying to find a local open source community, where you can find people to collaborate with and get started.&lt;/p&gt;

&lt;p&gt;Each project works differently, so I would suggest starting by looking for a &lt;strong&gt;&lt;code&gt;CONTRIBUTING.md&lt;/code&gt;&lt;/strong&gt; and reading the &lt;strong&gt;&lt;code&gt;README&lt;/code&gt;&lt;/strong&gt;. These are standard docs that provide pointers on how the project works and how it is maintained.&lt;/p&gt;

&lt;p&gt;Once you have a bit of context on the project, I would start by looking for &lt;strong&gt;&lt;code&gt;good first issue&lt;/code&gt;&lt;/strong&gt; labeled issues, which are issues that are suited for newcomers to start with.&lt;/p&gt;

&lt;p&gt;If you don't find any, it's okay, it doesn't mean you can't contribute at all. It just means that you would need to go through the rest of the issues and try to see if you find anything that interests you personally.&lt;/p&gt;

&lt;p&gt;Remember that maintainers are also human beings, and their goal is for the project to succeed, so if you have any questions, opening an issue or commenting on an issue is definitely okay, you gotta start somewhere. Just be respectful, and you should be good to go.&lt;/p&gt;

&lt;p&gt;After you find an issue or several issues that interest you, you should comment on the issue and ask to be assigned. While you're at it, feel free to ask for guidance (e.g., where to start or how to run the project locally). Open source is all about communication, so try to be as communicative as possible.&lt;/p&gt;

&lt;p&gt;Another great way to contribute is simply by creating issues. When using a technology, feedback is always important, that's how projects grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  My tips as a maintainer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Be patient, sometimes it takes time to be assigned or get a response, not all maintainers have a lot of time and open source is something we do during our free time, unless it's our part/full time job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Be nice, it goes a long way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Communicate when you're stuck, we want to help you, by helping you we're helping both sides.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The beginning might be a bit scary, BUT trust me that once you get the hang of it, it's very fun and teaches you a lot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nowadays, it's easy to generate code with AI, and I personally am okay with people opening PRs using it, I also use AI in my day to day. However, please make sure to thoroughly review the code you generate and ask questions if you aren't sure about anything. At the end of the day, a human maintainer is allocating their free time to review your work, so ensuring it actually makes sense is a matter of respect.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Everybody starts somewhere. In open source it's all about community, learning and growing.&lt;/p&gt;

&lt;p&gt;Shoutout to all the amazing people I met along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need guidance or just want to talk?
&lt;/h2&gt;

&lt;p&gt;Feel free to reach out to me over &lt;a href="https://www.linkedin.com/in/naorpeled" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/thehecticbyte" rel="noopener noreferrer"&gt;x&lt;/a&gt; or email me at &lt;a href="mailto:me@naor.dev"&gt;me@naor.dev&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;If you enjoyed this post and want to follow along with my open source journey, feel free to follow me here and on &lt;a href="https://github.com/naorpeled" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading this! I wish you the best of luck in reaching your goals.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>community</category>
      <category>ai</category>
      <category>career</category>
    </item>
  </channel>
</rss>
