DEV Community

Cover image for Modern Container Builds and WebSocket APIs Come to AWS SAM
Eric D Johnson for AWS

Posted on • Originally published at builder.aws.com

Modern Container Builds and WebSocket APIs Come to AWS SAM

SAM CLI has always been good at taking the grunt work out of serverless deployments. You define your functions and APIs in a template, and SAM handles the CloudFormation, the packaging, the deployment. It works.

Until recently, two things were missing. BuildKit support for container image builds. And a SAM resource type for WebSocket APIs.

Both are now shipped. Neither breaks existing templates.

Let's walk through it.

BuildKit Support for Image-Based Lambda Functions
The problem
When you run sam build for an image-based Lambda function, SAM uses the docker-py Python SDK under the hood. That SDK talks to the Docker daemon directly, but it doesn't support BuildKit. At all.

That means every sam build invocation uses the legacy Docker builder. You lose parallelized build stages. You lose efficient layer caching. You lose multi-stage build optimizations. You lose cross-architecture improvements that BuildKit has shipped over the past several years.

For simple single-stage Dockerfiles, this probably doesn't matter. For anything with multiple stages, private dependencies, or cross-compilation targets, it's a real bottleneck.

What shipped
SAM CLI v1.156.0 introduced the --use-buildkit flag on sam build. When you pass this flag, SAM bypasses docker-py entirely and shells out to the Docker CLI (or Finch CLI) directly. That gives you access to everything BuildKit offers.

sam build --use-buildkit
That's it. One flag.

SAM auto-detects which container runtime you have. It defaults to Docker, falls back to Finch if Docker isn't running, and respects any admin-configured preference. Finch supports BuildKit too, so either runtime works.

What you get
BuildKit brings real improvements over the legacy builder:

Parallel stage execution. Multi-stage Dockerfiles build independent stages concurrently instead of sequentially.
Better layer caching. BuildKit tracks dependencies at the file level, not just the layer level. Change one file, rebuild one layer.
Multi-stage build optimizations. BuildKit skips stages that don't contribute to the final output. The legacy builder runs every stage regardless.
Improved cross-architecture support. Building arm64 (Graviton2) images on an x86 machine is more reliable with BuildKit's QEMU integration. Lambda currently supports Graviton2 for arm64 workloads.
Build secrets
SAM CLI v1.159.0 added support for passing BuildKit parameters, including build-time secrets. This lets you pass credentials into a build stage without baking them into a layer. Useful when your Lambda function pulls packages from a private registry during the build.

syntax=docker/dockerfile:1

FROM public.ecr.aws/lambda/python:3.12

RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
pip install -r requirements.txt

COPY app.py ${LAMBDA_TASK_ROOT}
CMD ["app.handler"]
You configure secrets through the Metadata section of your function resource, using DockerBuildExtraParams. SAM passes these parameters straight to docker buildx under the hood.

Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
Architectures: [x86_64]
Timeout: 10
Metadata:
Dockerfile: Dockerfile
DockerContext: ./src
DockerTag: latest
DockerBuildExtraParams:
- "--secret"
- "id=pip_conf,src=$HOME/.pip/pip.conf"
The secret mounts into the build stage at the path you specify in the Dockerfile, gets used during pip install, and never appears in the final image layers. Different functions can have different build secrets, since the configuration lives in each function's Metadata block.

Tradeoffs and limitations
A few things to keep in mind.

Opt-in only. This doesn't change existing build behavior. If you don't pass --use-buildkit, nothing changes. That's intentional. SAM doesn't break working builds.

Requires Docker or Finch CLI. BuildKit support works by calling the Docker or Finch CLI directly. If your CI environment only has the Docker daemon (no CLI), this won't work. Most environments have both, but check yours.

Dockerfile syntax matters. Some BuildKit features, like secret mounts, require the # syntax=docker/dockerfile:1 parser directive at the top of your Dockerfile. Without it, the build falls back to legacy parsing and you get confusing errors.

Not for ZIP-based functions. This flag only applies to image-based Lambda functions (PackageType: Image). ZIP-based functions don't use Docker at all.

The rule of thumb I like is this: if your Dockerfile is more than a few lines, or you're building for a different architecture, turn on BuildKit. The caching alone will save you time.

WebSocket API Support
The problem
Before this release, SAM had no native resource type for WebSocket APIs. You had two options: write raw CloudFormation, or don't use SAM for that part of your stack.

Here's what a minimal WebSocket API looks like in plain CloudFormation. Three routes ($connect, $disconnect, sendMessage), each backed by a Lambda function:

Resources:
WebSocketApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: MyWebSocketApi
ProtocolType: WEBSOCKET
RouteSelectionExpression: $request.body.action

ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: $connect
Target: !Sub integrations/${ConnectIntegration}

ConnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ConnectFunction.Arn}/invocations

ConnectPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref ConnectFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*/$connect

DisconnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: $disconnect
Target: !Sub integrations/${DisconnectIntegration}

DisconnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DisconnectFunction.Arn}/invocations

DisconnectPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref DisconnectFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*/$disconnect

SendMessageRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: sendMessage
Target: !Sub integrations/${SendMessageIntegration}

SendMessageIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SendMessageFunction.Arn}/invocations

SendMessagePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref SendMessageFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*/sendMessage

Deployment:
Type: AWS::ApiGatewayV2::Deployment
DependsOn:
- ConnectRoute
- DisconnectRoute
- SendMessageRoute
Properties:
ApiId: !Ref WebSocketApi

Stage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref WebSocketApi
StageName: prod
DeploymentId: !Ref Deployment
That's twelve resources for three routes. Around 90 lines of YAML. And I left out the Lambda function definitions.

Notice the DependsOn on the Deployment resource. If you forget that, CloudFormation tries to create the deployment before the routes exist, and the stack fails. You have to manage that dependency graph yourself.

That's a lot of ceremony for "connect a WebSocket to some Lambda functions."

What shipped
The new AWS::Serverless::WebSocketApi resource type collapses all of that into this:

Resources:
MyWebSocketApi:
Type: AWS::Serverless::WebSocketApi
Properties:
RouteSelectionExpression: $request.body.action
StageName: prod
Routes:
$connect:
FunctionArn: !GetAtt ConnectFunction.Arn
$disconnect:
FunctionArn: !GetAtt DisconnectFunction.Arn
sendMessage:
FunctionArn: !GetAtt SendMessageFunction.Arn
Compare that to the CloudFormation version. Twelve resources become one. Ninety lines become twelve. The routes, integrations, Lambda permissions, deployment, stage, and resource ordering are all handled by SAM's transform.

You define the routes. SAM generates the rest.

What SAM handles automatically
For each route you declare, SAM creates:

The AWS::ApiGatewayV2::Route resource
The AWS::ApiGatewayV2::Integration wiring the route to your Lambda function
The AWS::Lambda::Permission granting API Gateway invoke access
If you add a Lambda authorizer, the authorizer permission too
SAM also creates the deployment and stage resources, with the correct dependency ordering. No DependsOn blocks to manage.

Full feature parity
This isn't a simplified subset. The new resource type supports everything API Gateway V2 WebSocket offers:

Auth: IAM authorization and Lambda authorizers, per-route or API-wide
Custom domains: Map your WebSocket API to your own domain
Route settings: Configure throttling and logging per route via RouteSettings
Models: Attach request/response models for validation
Stage variables: Pass configuration to your integration through stage variables
Globals: Share configuration across multiple WebSocket APIs using the SAM Globals section
Use cases
WebSocket APIs are the right tool when you need a persistent, bidirectional connection. Common patterns:

Chat applications. Users send and receive messages in real time.
Live dashboards. Push metric updates to connected browsers without polling.
AI/LLM streaming. Stream token-by-token responses from a model back to the client. This one is increasingly common.
IoT command channels. Send commands to devices and receive status updates on the same connection.
If your current approach is a REST API that the client polls every few seconds, a WebSocket API will give you lower latency and lower cost. Fewer requests, fewer Lambda invocations, faster updates.

Tradeoffs and limitations
No local emulation. SAM CLI doesn't support sam local start-api for WebSocket APIs. You can test individual Lambda handlers with sam local invoke, but end-to-end local WebSocket testing isn't available yet. Deploy to a dev stage for integration testing.

No sam local start-websocket. Related to the above, there's no dedicated local command for WebSocket APIs like there is for HTTP APIs with sam local start-api.

For the Agents
If you're using an AI coding agent to build SAM applications, both of these features work out of the box with agent-driven workflows. Your agent can scaffold a WebSocket API or add BuildKit to an existing image-based function without any special setup.

For Kiro, there's an official AWS SAM Power that gives your agent SAM-aware tooling. Install it and your agent gets access to sam_init, sam_build, sam_deploy, sam_logs, and sam_local_invoke as callable tools, plus opinionated project structure guidance.

Here's what a Kiro-assisted WebSocket API scaffold looks like in practice:

You: Create a WebSocket API with connect, disconnect, and sendMessage routes.
Use the SAM Power.

Kiro: [runs sam_init] → creates project structure
[updates template.yaml] → adds AWS::Serverless::WebSocketApi
[creates Lambda handlers] → connect.py, disconnect.py, send_message.py
[runs sam_build] → builds the project
[runs sam_local_invoke] → tests the connect handler locally
The SAM Power also enforces good project structure: separate Lambda handlers in infrastructure/lambda/, proper CodeUri paths, and .aws-sam in your .gitignore. It gets you from idea to deployed WebSocket API without hand-writing boilerplate.

You can install the SAM Power from the Kiro Powers marketplace or add it directly to your project's .kiro/powers/ directory.

Getting Started
Upgrade SAM CLI to get both features:

sam --version

Need v1.156.0 or later for BuildKit, latest for WebSocket APIs

Upgrade via pip

pip install --upgrade aws-sam-cli

Or via Homebrew

brew upgrade aws-sam-cli
For BuildKit, add --use-buildkit to your sam build command. No template changes needed.

For WebSocket APIs, replace your CloudFormation resources with the new AWS::Serverless::WebSocketApi type. If you're starting fresh, the SAM template above is a working starting point.

BuildKit works with sam local for local testing. WebSocket APIs currently support sam deploy for deployment and sam local invoke for testing individual handlers, but full local WebSocket emulation isn't available yet.

Two Features, Zero Breaking Changes
These two features fill gaps that have been open for a while. BuildKit support means sam build finally uses the same build engine as the rest of the container ecosystem. WebSocket API support means you can define a real-time API in SAM the same way you define a REST API. A few lines instead of a hundred.

Neither feature changes existing behavior. Both are additive. Upgrade, try them, and keep building.

Top comments (0)