Running a serverless AI code review agent on AWS Lambda with PR-Agent and CDK Naor Peled, lead maintainer of the open-source PR-Agent project, published a guide and companion CDK repository (pr-agent-lambda-cdk) for self-hosting an AI code review agent on AWS Lambda. The setup runs PR-Agent's GitHub Lambda webhook handler behind FastAPI and Mangum, letting the agent update pull request descriptions, post code suggestions, and respond to slash commands such as /review, /improve and /ask. Configuration is handled through environment variables (e.g. GITHUB__WEBHOOK_SECRET) since Lambda has no mounted config file, and model calls route through LiteLLM so providers like OpenAI, Bedrock or local models are a config change away. Code reviews are an essential part of our day to day work. Their importance and way of execution have changed a bit in the GenAI era. In this post, I'll show you how you can self host your own code review agent. PR-Agent https://github.com/The-PR-Agent/pr-agent is an open-source project that allows you to perform code reviews using a customizable AI agent in a self-hosted way. It works with all common AI model providers and local models. The flow is the following: 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 /review , /improve or /ask what does this migration do? . The project initially started at Qodo https://qodo.ai , 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. In this post, we'll explore how to deploy it using AWS Lambda. If you prefer to just clone and run it, everything in this post is also in pr-agent-lambda-cdk https://github.com/naorpeled/pr-agent-lambda-cdk . 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 AWS Community Builder https://builder.aws.com/community/community-builders in the DevTools category and love sharing insights about modern developer workflows and cloud tooling. I'm the lead maintainer of PR-Agent and I also maintain projects like TypeORM https://github.com/typeorm/typeorm and a few others you can find on my GitHub page https://github.com/naorpeled . I currently work for groundcover https://www.groundcover.com/ , where we're building a BYOC https://www.groundcover.com/byoc , eBPF https://www.groundcover.com/ebpf powered observability solution. PR-Agent is a Python project that has two modes: You can override the default configurations by creating a configuration file https://docs.pr-agent.ai/usage-guide/configuration options/ . The configuration file needs to be named .pr agent.toml and can sit in two locations: pr-agent-settings that sits under your git provider's org. If you work on a monorepo, you can also add a .pr agent.toml 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 enable per directory settings = true under config in your root .pr agent.toml . For now it works on GitHub and GitLab. Every setting can also be set with an environment variable, using the section and the key with a double underscore between them. For example, webhook secret under github becomes GITHUB WEBHOOK SECRET . Environment variables are applied last, so they win over .pr agent.toml . We'll use them a lot, since on Lambda there's no config file to mount. Model calls go through LiteLLM https://docs.litellm.ai/ , so moving from OpenAI to Bedrock or to a local model is just a config change. For big PRs, PR-Agent uses a compression strategy https://docs.pr-agent.ai/core-abilities/compression strategy/ that fits the most relevant parts of the diff into a fixed token budget, so even big PRs fit. 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. For that I like Lambda, for a few reasons: PR-Agent already supports Lambda. This is the GitHub Lambda handler, pr agent/servers/github lambda webhook.py I removed the logging from the except : python from fastapi import FastAPI from mangum import Mangum from starlette.middleware import Middleware from starlette context.middleware import RawContextMiddleware from pr agent.servers.github app import router try: from pr agent.config loader import apply secrets manager config apply secrets manager config except Exception as e: ... falls back to environment variables middleware = Middleware RawContextMiddleware app = FastAPI middleware=middleware app.include router router handler = Mangum app, lifespan="off" def lambda handler event, context : return handler event, context It uses the same FastAPI router as the regular server, wraps it with Mangum https://github.com/Kludex/mangum , 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. 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 . 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. 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 ASYNC REVIEWS=true , and it's on by default for every provider except GitHub. PR-Agent ships with heavy Python dependencies, so a zip deployment is not going to be fun. Lambda container images https://docs.aws.amazon.com/lambda/latest/dg/images-create.html can be up to 10 GB uncompressed, compared to 250 MB unzipped for zips. Lambda pulls the image from ECR https://docs.aws.amazon.com/AmazonECR/latest/userguide/what-is-ecr.html , in the same region as the function. In front of the function we'll use a Lambda Function URL https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html , 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. It uses authType: NONE , 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. GitHub App private keys and webhook secrets go in Secrets Manager https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html and not in environment variables, where anyone with console read access can see them. PR-Agent already knows how to read them from there. For the model I'm using Amazon Bedrock https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html , which keeps everything inside your own AWS account. LiteLLM supports Bedrock, so PR-Agent does too. And all of it is defined in CDK https://docs.aws.amazon.com/cdk/v2/guide/home.html , which compiles down to CloudFormation but lets you write actual TypeScript instead of YAML. You'll need: jq and openssl . Pick a region and use it for everything. I'll use us-east-1 . 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. 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. The first call to a model that's sold through AWS Marketplace also creates a Marketplace subscription, and the caller needs aws-marketplace:Subscribe 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 AccessDeniedException , sometimes only after the first few calls worked. The model access docs https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html list what each model needs. 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. PR-Agent authenticates as a GitHub App, so let's start there. Go to Settings, Developer settings, GitHub Apps and create a new one: https://example.com 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: export WEBHOOK SECRET=$ openssl rand -hex 32 echo $WEBHOOK SECRET paste this into the GitHub App form export APP ID=123456 export PEM=~/Downloads/your-app.2026-09-27.private-key.pem Then install the App on the repos you want reviewed. The install docs https://docs.pr-agent.ai/installation/github/ have more details. 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. jq can build the whole file for us: jq -Rs --arg app id "$APP ID" --arg secret "${WEBHOOK SECRET:?run step 1 first}" \ '{"github.app id": $app id, "github.webhook secret": $secret, "github.private key": .}' \ < "$PEM" config.json That gives you: { "github.app id": "123456", "github.webhook secret": "your-webhook-secret", "github.private key": "-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----\n" } 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: aws secretsmanager create-secret \ --name pr-agent/config \ --secret-string file://config.json \ --region us-east-1 \ && rm config.json "$PEM" Make sure to pass --region . 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 Failed to get secrets from AWS Secrets Manager on cold start and continues without credentials, and every webhook after that gets a 403 with Webhook secret not configured . Two more things to know about this secret: SECTION KEY environment variable. The project publishes Lambda images to Docker Hub on every release: pragent/pr-agent: