Sign inSign up

simota/devcloud-lambda

By simota

•Updated about 9 hours ago

AWS Lambda-compatible endpoint for local dev and tests (devcloud, Rust). Warm starts. Port 3001.

Image
0

722

simota/devcloud-lambda repository overview

⁠devcloud-lambda

A lightweight, AWS Lambda-compatible endpoint for local development and deterministic tests. It is the Lambda service of devcloud⁠, packaged as a standalone Rust binary — no orchestrator, dashboard, or other services.

It serves the Lambda REST API on port 3001 (the port sam local start-lambda uses) and runs Python and Node.js handlers as child processes inside the container, keeping them warm between invocations like Lambda does. It is not intended for production use or full AWS parity.

⁠Quick start

docker run --rm -p 3001:3001 -v devcloud-lambda-data:/data simota/devcloud-lambda
cat > app.py <<'EOF'
def handler(event, context):
    return {"sum": event["a"] + event["b"]}
EOF
zip function.zip app.py

export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url http://localhost:3001 lambda create-function \
  --function-name sum --runtime python3.12 --handler app.handler \
  --role arn:aws:iam::000000000000:role/lambda --zip-file fileb://function.zip
aws --endpoint-url http://localhost:3001 lambda invoke \
  --function-name sum --cli-binary-format raw-in-base64-out \
  --payload '{"a":2,"b":3}' --log-type Tail out.json && cat out.json

In the default relaxed auth mode any credentials are accepted. The --role ARN is stored but not checked.

⁠Docker Compose
services:
  lambda:
    image: simota/devcloud-lambda:0.3.2
    ports:
      - "3001:3001"
    volumes:
      - lambda-data:/data
      - ./layers:/opt:ro          # optional: layer contents (see Layers)
    environment:
      # optional: credentials handed to every handler (see Credentials)
      DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID: test
      DEVCLOUD_LAMBDA_FUNCTION_SECRET_ACCESS_KEY: test
volumes:
  lambda-data:

⁠Configuration

VariableDefaultDescription
DEVCLOUD_LAMBDA_ADDR0.0.0.0:3001Listen address (host:port).
DEVCLOUD_LAMBDA_STORAGE/dataStorage root for function state and deployment packages.
DEVCLOUD_LAMBDA_ENDPOINThttp://localhost:3001Public base URL used for GetFunction Code.Location download links and function URLs. Set it when clients reach the container under another host or port.
DEVCLOUD_LAMBDA_REGIONus-east-1Region used in function ARNs and expected in SigV4 signatures.
DEVCLOUD_LAMBDA_ACCOUNT_ID000000000000Account ID used in function ARNs.
DEVCLOUD_LAMBDA_AUTH_MODErelaxedrelaxed accepts any request; signed-relaxed requires a well-formed SigV4 Authorization header without verifying it; strict verifies AWS SigV4 signatures (service lambda).
DEVCLOUD_LAMBDA_ACCESS_KEY_ID—Access key accepted in strict mode.
DEVCLOUD_LAMBDA_SECRET_ACCESS_KEY—Secret key used to verify signatures in strict mode.
DEVCLOUD_LAMBDA_S3_STORAGE—Storage root of a devcloud-s3⁠ volume. Enables Code.S3Bucket / Code.S3Key deployment packages.
DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS300How long an idle execution environment stays warm. 0 makes every invocation a cold start.
DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID / DEVCLOUD_LAMBDA_FUNCTION_SECRET_ACCESS_KEY / DEVCLOUD_LAMBDA_FUNCTION_SESSION_TOKEN—Credentials passed to every handler as AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN, standing in for the execution role. Set the key id and the secret together (the session token is optional).
DEVCLOUD_LAMBDA_OPT_DIR/optDirectory standing in for Lambda's /opt (layer contents).
DEVCLOUD_LAMBDA_URL_AUTH_MODEDEVCLOUD_LAMBDA_AUTH_MODEAuth mode for AWS_IAM function URLs only. strict verifies URL signatures (against DEVCLOUD_LAMBDA_ACCESS_KEY_ID / ..._SECRET_ACCESS_KEY) while the API stays relaxed.
DEVCLOUD_LAMBDA_LOG_INVOCATIONStruePrint every invocation's log (INIT_START/START/END/REPORT and the handler's output) to stdout, each line tagged [<function>] — a stand-in for CloudWatch Logs in docker logs.
DEVCLOUD_LAMBDA_DOCKERfalse (true in -docker tags)Run PackageType: Image functions with the Docker CLI.
DEVCLOUD_LAMBDA_DOCKER_NETWORK—Docker network this container is on; function containers join it (see Container image functions⁠).

Strict mode example:

docker run --rm -p 3001:3001 \
  -e DEVCLOUD_LAMBDA_AUTH_MODE=strict \
  -e DEVCLOUD_LAMBDA_ACCESS_KEY_ID=mykey \
  -e DEVCLOUD_LAMBDA_SECRET_ACCESS_KEY=mysecret \
  simota/devcloud-lambda

Deploy from a bucket in devcloud-s3 by sharing its volume (read-only is enough):

services:
  s3:
    image: simota/devcloud-s3:0.3.0
    ports: ["9000:9000"]
    volumes: [s3-data:/data]
  lambda:
    image: simota/devcloud-lambda:0.3.2
    ports: ["3001:3001"]
    environment:
      DEVCLOUD_LAMBDA_S3_STORAGE: /s3
    volumes:
      - lambda-data:/data
      - s3-data:/s3:ro
volumes:
  s3-data:
  lambda-data:

The container runs as a non-root user (UID 10001) under tini. Data persists in the /data volume; mount a named volume or a host directory writable by that UID. It stops cleanly on SIGTERM (docker stop), stopping every handler process first.

⁠Execution environments (warm starts)

Like Lambda, an execution environment loads the handler module once and then serves invocations one at a time, so module-level state and one-time initialization (loading a model, opening clients) carry over between invocations.

  • A cold start adds an INIT_START line, the init-phase output, and Init Duration in the REPORT line. Init gets its own budget of one function Timeout.
  • Concurrent invocations of a function get separate environments.
  • An environment is stopped after DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS without invocations, when the function's configuration or code changes or the function is deleted, after a timeout, crash, or init failure, and on shutdown.
  • Unlike Lambda, an idle environment is not frozen: threads a Python handler left running keep running (Node.js handlers are paused between invocations).
  • Handlers get an empty stdin.
  • REPORT includes Max Memory Used: the interpreter process's peak resident memory during the invocation (init included on a cold start; processes it spawns are not counted). Above MemorySize, the log warns that Lambda would have stopped the invocation — memory is not limited here.
  • Every invocation's log also goes to the container log (DEVCLOUD_LAMBDA_LOG_INVOCATIONS):
[sum] INIT_START Runtime Version: python3.12
[sum] START RequestId: 57b04d27-... Version: $LATEST
[sum] END RequestId: 57b04d27-...
[sum] REPORT RequestId: 57b04d27-...	Duration: 0.36 ms	Billed Duration: 1 ms	Memory Size: 128 MB	Max Memory Used: 34 MB	Init Duration: 392.71 ms

⁠Runtimes

Handlers run with the interpreters bundled in the image: Python 3.12 (with boto3, like Lambda's Python runtime) and Node.js 22. A function's Runtime selects the family; when the version differs (python3.13 on Python 3.12, nodejs20.x on Node.js 22), the invocation log gets a [WARNING] devcloud: runtime ... is running on ... line on a cold start, and the container log repeats it once per function and runtime.

  • python3.x: module.function handlers.
  • nodejs*: CommonJS and ES modules (.mjs, or .js under a package.json with "type": "module"), async and callback style.
  • Other runtimes (java*, dotnet*, ruby*, provided*) can be created, but invoking them returns InvalidRuntimeException.
  • boto3 is included (the version current when the image was built). The AWS SDK for JavaScript v3 is not: ship it in the zip or in /opt.

Handlers get a cleared environment: PATH, Lambda's reserved variables (AWS_LAMBDA_FUNCTION_NAME, AWS_REGION, LAMBDA_TASK_ROOT, ...), the configured function credentials, and the function's Environment.Variables. Nothing else from the container's environment leaks in. Environment variable names follow Lambda's key pattern (letters, digits, _, starting with a letter).

⁠Layers

The layer APIs are not supported; mount the layer contents at /opt instead. As on Lambda, the default PYTHONPATH is /opt/python/lib/python<version>/site-packages:/opt/python and the default NODE_PATH is /opt/nodejs/node<major>/node_modules:/opt/nodejs/node_modules, so packages placed there are importable without extra settings — from ES module handlers too. A function that sets its own PYTHONPATH / NODE_PATH replaces the default.

docker run --rm -p 3001:3001 -v "$PWD/deps:/opt/python:ro" simota/devcloud-lambda
⁠Credentials

Lambda passes the execution role's credentials to handlers; here, set DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID and DEVCLOUD_LAMBDA_FUNCTION_SECRET_ACCESS_KEY (and optionally ..._SESSION_TOKEN) and every handler receives them, so AWS SDK calls from a handler (for example to a local S3) can sign requests. As on Lambda, functions cannot set AWS_ACCESS_KEY_ID & co. themselves.

⁠Function URLs

CreateFunctionUrlConfig / GetFunctionUrlConfig / UpdateFunctionUrlConfig / DeleteFunctionUrlConfig / ListFunctionUrlConfigs are supported. Requests become payload format 2.0 events and handler results map back to HTTP responses (statusCode, headers, cookies, body, isBase64Encoded; any other JSON value is returned as a 200 JSON body).

A URL is served on the same port, three ways:

  • the returned FunctionUrl, http://<url-id>.lambda-url.<region>.localhost:3001/;
  • http://localhost:3001/_url/<url-id>/;
  • by function name, http://localhost:3001/urls/<function>/ — no id to look up and no *.localhost resolution needed.

<url-id> is derived from the account, region, and function name, so a URL stays the same when it, the function, or the /data volume is recreated.

cat > web.py <<'EOF'
def handler(event, context):
    name = (event.get("queryStringParameters") or {}).get("name", "world")
    return {"statusCode": 200, "body": f"hello {name}"}
EOF
zip web.zip web.py
aws --endpoint-url http://localhost:3001 lambda create-function \
  --function-name web --runtime python3.12 --handler web.handler \
  --role arn:aws:iam::000000000000:role/lambda --zip-file fileb://web.zip
aws --endpoint-url http://localhost:3001 lambda create-function-url-config \
  --function-name web --auth-type NONE
curl "http://localhost:3001/urls/web/?name=devcloud"   # hello devcloud
  • AuthType: AWS_IAM refuses unsigned requests in every auth mode and verifies the SigV4 signature (service lambda) in strict mode; other modes check only that the signature is well-formed. DEVCLOUD_LAMBDA_URL_AUTH_MODE=strict verifies URL signatures while the API stays relaxed. AuthType: NONE URLs are open: resource policies (AddPermission) are not evaluated.
  • Cors answers preflight requests and decorates responses. With AllowCredentials, wildcards are answered with the requested method and headers, since browsers read * literally there.
  • InvokeMode: RESPONSE_STREAM is accepted but responses are buffered.

⁠Container image functions

PackageType: Image functions run with the Docker CLI in the -docker tags (latest-docker, 0.3.2-docker), sharing the host's Docker daemon. Each execution environment is a docker run of the function's image, invoked through the Runtime Interface Emulator that every AWS base image (public.ecr.aws/lambda/*) starts, and kept warm like zip functions. MemorySize becomes the container's memory limit.

Mount the Docker socket and name the network this container is on: function containers join it and are reached by name.

services:
  lambda:
    image: simota/devcloud-lambda:0.3.2-docker
    ports: ["3001:3001"]
    environment:
      DEVCLOUD_LAMBDA_DOCKER_NETWORK: myapp_default   # <compose project>_default
    volumes:
      - lambda-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
volumes:
  lambda-data:
aws --endpoint-url http://localhost:3001 lambda create-function \
  --function-name api --package-type Image --code ImageUri=my-function:latest \
  --role arn:aws:iam::000000000000:role/lambda
  • ImageUri is any image the daemon has or can pull; the first start may pull it within a 120 s budget.
  • ImageConfig (EntryPoint, Command, WorkingDirectory) overrides apply independently, as on Lambda.
  • REPORT includes Max Memory Used: the container's cgroup memory peak since it started (page cache included, so it reads higher than a process's resident memory), with the same warning above MemorySize. It is read with docker exec <container> cat, so images without cat omit it. A cold start's log opens with INIT_START Image: <ImageUri>.
  • Mounting docker.sock gives this container root on the Docker host, so the -docker tags run as root. Use them only where that is acceptable.
  • On SIGTERM (docker stop, docker compose down) this container removes the function containers it started. If it is killed instead (docker rm -f, a crash), it removes the leftovers the next time it starts on the same /data volume: containers are labelled devcloud.lambda.owner=<instance id> (kept in /data/instance-id), and only its own are touched, so several devcloud-lambda containers can share one Docker daemon.
  • The default tags have no Docker: invoking an image function there returns 502 with an error saying so.

⁠Supported Lambda features

FeatureStatus
CreateFunction, GetFunction, GetFunctionConfiguration, ListFunctions (Marker/MaxItems), DeleteFunctionYes
UpdateFunctionConfiguration (RevisionId precondition), UpdateFunctionCodeYes
Function identifiers: name, partial ARN, full ARNYes
Invoke: RequestResponse, Event (async, 202), DryRunYes
X-Amz-Function-Error: Unhandled on handler errorsYes
X-Amz-Log-Type: Tail (last 4 KB of the INIT_START/START/END/REPORT log)Yes
Warm execution environments, Init DurationYes
Per-function TimeoutYes — the environment's whole process group is stopped
Function URLs (NONE, AWS_IAM, CORS; stable ids and /urls/<function>/)Yes
Tags (list / tag / untag), GetAccountSettingsYes
Deployment packagesPartial — zip only (ZipFile, or S3Bucket/S3Key with DEVCLOUD_LAMBDA_S3_STORAGE); no zip64. A package over 250 MB unzipped is rejected, as on Lambda (50 MB for a direct ZipFile upload)
LayersPartial — mount the contents at /opt; no layer APIs
Container images (PackageType: Image)Yes in the -docker tags, through the Runtime Interface Emulator; not in the default tags
MemorySizeReported to handlers; REPORT adds Max Memory Used (zip and image functions); not enforced for zip functions (it is the container limit for image functions)
SigV4 authYes — signatures are verified in strict mode
Versions, aliases, event source mappings, concurrency limitsNo — only $LATEST exists

⁠Notes

  • Handlers are not sandboxed. They run inside this container with the server's privileges and can read /data (and, in the -docker tags, reach the Docker socket). Only deploy code you trust.
  • Non-GET API requests carrying a non-loopback Origin header are rejected with 403, so a web page cannot create or invoke functions. SDKs and CLIs are unaffected. Function URLs are meant to be called from browsers and are not subject to this check.
  • Do not share the /data volume between multiple containers; locking is in-process only.

⁠Changelog

⁠0.3.2
  • Image functions report Max Memory Used (the container's cgroup peak since it started) with the over-MemorySize warning.
  • Image functions' cold-start log opens with INIT_START Image: <ImageUri> instead of an empty Runtime Version:.
⁠0.3.1
  • A restarted container removes the function containers a killed predecessor (same /data volume) left behind; containers are labelled devcloud.lambda.owner. Containers started by 0.3.0 are unlabelled and not cleaned up.
⁠0.3.0
  • Container image functions in the new -docker tags (Docker CLI included; DEVCLOUD_LAMBDA_DOCKER_NETWORK lets function containers join this container's network).
  • Python 3.12 with boto3 and Node.js 22 (were Python 3.11 and Node.js 18). The image grows to about 320 MB.
  • Stable function URLs: ids derive from account, region, and function name; /urls/<function>/ addresses a URL by name. URLs created by earlier versions keep their ids.
  • DEVCLOUD_LAMBDA_URL_AUTH_MODE verifies AWS_IAM URL signatures independently of the API's auth mode.
  • Invocation logs in the container log (DEVCLOUD_LAMBDA_LOG_INVOCATIONS, on by default).
  • Max Memory Used in REPORT, with a warning above MemorySize.
⁠0.2.1
  • The runtime version-mismatch warning is also written to the container log (docker logs), once per function and runtime.
  • Invoking a container image function explains that this image has no Docker CLI (instead of No such file or directory); DEVCLOUD_LAMBDA_DOCKER=true without docker on PATH is warned about at startup.
⁠0.2.0
  • Warm starts: execution environments are reused between invocations and stopped after DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS (default 300) idle. Module state now persists between invocations; set DEVCLOUD_LAMBDA_IDLE_TIMEOUT_SECONDS=0 for the previous behavior. Cold starts report Init Duration.
  • Layers via /opt: /opt/python and /opt/nodejs/node_modules are on the default PYTHONPATH / NODE_PATH, including for ES module handlers (DEVCLOUD_LAMBDA_OPT_DIR relocates /opt).
  • Function credentials: DEVCLOUD_LAMBDA_FUNCTION_ACCESS_KEY_ID / ..._SECRET_ACCESS_KEY / ..._SESSION_TOKEN are passed to handlers.
  • Function URLs with NONE / AWS_IAM auth and CORS.
  • A [WARNING] log line when a runtime's version differs from the bundled interpreter.
  • .js handlers under "type": "module" load as ES modules; handlers get an empty stdin.
  • Environment variable names must follow Lambda's key pattern; repeated request headers are combined instead of keeping only the last one.
⁠0.1.0
  • Initial standalone image on port 3001 for linux/amd64 and linux/arm64, with Python 3.11, Node.js 18, and tini.

Tag summary

Content type

Image

Digest

sha256:5d4505a67…

Size

109.9 MB

Last updated

about 9 hours ago

docker pull simota/devcloud-lambda