AWS Builder Center
Lambda Web Functions in Terraform: what hashicorp/aws rejects and awscc already accepts

Lambda Web Functions in Terraform: what hashicorp/aws rejects and awscc already accepts

AWS Lambda Web Functions run a plain Node.js HTTP server, and one aws lambda-web deploy command creates the bucket, role, function, revision and endpoint for you. I ran the server locally, then tried to model it in Terraform: hashicorp/aws 6.69.1 fails validation on the resource type, while awscc 1.106.0 accepts all three resources. The article covers what the CLI leaves behind after a delete and the multi-Region settings you can't change later.

Series: Infrastructure-as-code (4 articles)

  1. …
  2. 4
    Lambda Web Functions in Terraform: what hashicorp/aws rejects and awscc already accepts This article
Engineer @ DevOps area

One command, five resources, nothing in state

AWS launched Lambda Web Functions on October 12, 2026. Your Node.js server listens on a port, Lambda runs up to 128 concurrent requests per execution environment, and you pay for active CPU time instead of wall-clock duration. The demo deploy is a single line:
1
2
aws lambda-web deploy --name my-web-app --code ./my-app \
--regions us-east-1,eu-west-1,ap-northeast-1
On the first run, according to the Lambda Developer Guide, that command creates or reuses a versioned S3 bucket and an IAM execution role, then creates the Web Function, its first revision and an endpoint. None of it lands in a Terraform state file. I wanted to know how far I could get modelling it properly, without creating anything in a real account. Everything below was run on October 21, 2026 on darwin_arm64 with Terraform v1.16.6.

Step 1: the server is just a server

The runtime contract is one environment variable. Lambda sets AWS_LAMBDA_HTTP_ENDPOINT to host:port, and the server has 10 seconds after initialization to start listening there. No framework needed:
1
2
3
4
5
6
7
8
9
10
import { createServer } from 'node:http';

const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':');

createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ path: req.url, pid: process.pid }));
}).listen(Number(port), host, () => {
console.log(`listening on ${host}:${port}`);
});
Locally you set the variable yourself:
1
2
3
4
5
$ AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js
listening on 127.0.0.1:3000

$ curl -s http://127.0.0.1:3000/orders/42
{"path":"/orders/42","pid":37118}
At launch the runtime is Node.js 24 (nodejs24.x) on Arm64, in five Regions: us-east-1, us-east-2, us-west-2, ap-northeast-1 and eu-west-1. maxConcurrencyPerEnvironment accepts 1 to 128 and defaults to 64, so 128 is a ceiling you opt into.

Step 2: what the deploy command leaves behind

The "Creating a Web Function" page lists what deploy does on the first run, and the clean-up section lists what aws lambda-web delete-web-function doesn't remove:
Resourcedeploydelete-web-function
S3 bucket awscli-lambdaweb-<account-id>-<region>-an, versionedcreates or reusesstays, with all package versions
IAM role awscli-lambdaweb-<name>creates or reusesstays
Web Function, revision, endpoint devcreatesremoved
CloudWatch Logs log groupswritten by Lambdastay
Warning
In a pipeline, deploy waits for Enter before creating a new function unless you pass --create. A CI role that can run it unattended can also create IAM roles and buckets, unless you pass --bucket-name and --execution-role-arn so it reuses yours.

Step 3: hashicorp/aws doesn't know the resource yet

The resource names in the open provider PR are aws_lambdaweb_*. With the latest release, 6.69.1 from October 15:
1
2
3
4
5
6
7
8
9
10
11
12
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "6.69.1"
}
}
}

resource "aws_lambdaweb_function" "app" {
function_name = "orders-api"
}
1
2
3
4
5
6
7
8
$ terraform validate
Error: Invalid resource type

on main.tf line 10, in resource "aws_lambdaweb_function" "app":
10: resource "aws_lambdaweb_function" "app" {

The provider hashicorp/aws does not support resource type
"aws_lambdaweb_function".
It's not a naming mistake on my side. With only the required_providers block left in main.tf, the provider schema has no Lambda Web resource of any name:
1
2
3
4
5
6
7
8
$ terraform providers schema -json | python3 -c "
import json, sys
d = json.load(sys.stdin)
r = [k for p in d['provider_schemas'].values()
for k in p.get('resource_schemas', {})
if 'lambdaweb' in k or 'lambda_web' in k]
print(len(r), r)"

0 []
Issue #50429 tracks the feature and PR #50452 implements it. As of today the PR is open with review required.

Step 4: awscc already has all three

CloudFormation ships three types: AWS::Lambda::WebFunction, AWS::Lambda::WebFunctionRevision and AWS::Lambda::WebFunctionEndpoint. The awscc provider is generated from the CloudFormation registry and added them in 1.105.0 on October 7. The same schema query against awscc 1.106.0:
1
2
3
4
5
6
7
8
9
$ terraform providers schema -json | python3 -c "
import json, sys
d = json.load(sys.stdin)
[print(k) for p in d['provider_schemas'].values()
for k in sorted(p.get('resource_schemas', {}))
if 'lambda_web' in k]"

awscc_lambda_web_function
awscc_lambda_web_function_endpoint
awscc_lambda_web_function_revision
This configuration passes validation with it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
resource "awscc_lambda_web_function" "app" {
function_name = "orders-api"
}

resource "awscc_lambda_web_function_revision" "current" {
function_name = awscc_lambda_web_function.app.function_name

build_config = {
code_config = {
s3_object = {
bucket = var.code_bucket
key = var.code_key
version_id = var.code_version
}
}
runtime_config = {
runtime = "nodejs24.x"
}
}

service_config = {
execution_role_arn = var.execution_role_arn
max_concurrency_per_environment = 64
timeout_seconds = 30
}
}

resource "awscc_lambda_web_function_endpoint" "global" {
function_name = awscc_lambda_web_function.app.function_name
endpoint_name = "prod"
endpoint_type = "MultiRegion"
auth_type = "ApplicationManaged"
regions = ["us-east-1", "eu-west-1", "ap-northeast-1"]

revision_weights = [{
revision_id = awscc_lambda_web_function_revision.current.revision_id
weight = 100
}]

scaling_config = {
max_environments = 20
}
}
1
2
3
4
5
6
7
8
9
10
$ terraform init
...
- Installing hashicorp/awscc v1.106.0...
- Installed hashicorp/awscc v1.106.0 (signed by HashiCorp)
...
Terraform has been successfully initialized!
...

$ terraform validate
Success! The configuration is valid.
I validated this, I did not apply it. Treat it as a schema-checked starting point, not a tested module.

The settings you only get to choose once

In the CloudFormation schema, every revision property is immutable, and so are the endpoint's EndpointType and Regions. A code change replaces the revision resource, which is expected. A Region-list change replaces the endpoint, and the docs say a deleted endpoint's domain name is retired permanently. Pick the endpoint type and Regions before clients start calling the domain.
MultiRegion endpoints also only support the Disabled auto-deployment mode, so traffic moves only when revision weights change. Weights are integers from 1 to 100, one or two revisions, summing to exactly 100.

Honest trade-offs

Tip
The draft docs in the aws provider PR warn that a function whose inline endpoint is MultiRegion keeps serving its original revision "even though the apply succeeds and the following plan is empty". Whichever provider you use, keep the endpoint as its own resource.
Things I got wrong on the first pass, or still can't answer:
  • I assumed Terraform had nothing. It does, through awscc. The gap is only in the provider most teams already pin.
  • My awscc example pins the revision in state. If CI ships revisions on its own, the next terraform plan will want to move traffic back to the pinned one, so take revision_weights out of Terraform's hands or add ignore_changes.
  • The revision references an S3 object version. If the bucket expires noncurrent versions, old revisions move to Failed, and rollback goes with them.
  • Cost depends on utilization. Active vCPU in us-east-1 is $0.0000276944 per vCPU-second, which is about 3.1x the Fargate Arm vCPU-hour rate. My napkin break-even on the CPU line is roughly 32% average utilization, before memory and requests. That's list prices, not a benchmark.

Summary

The split I'd use: Terraform owns the bucket, the role, the function and the endpoint, including max_environments. CI uploads the zip, creates a revision, waits for it to become Active, sends a real request (Active doesn't prove the server is listening), and then moves the weights. Revisions are deploys. Functions and endpoints are infrastructure.

Series: Infrastructure-as-code (4 articles)

  1. …
  2. 4
    Lambda Web Functions in Terraform: what hashicorp/aws rejects and awscc already accepts This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article