# Lambda **Protocol:** REST JSON **Endpoint:** `http://localhost:4566/2015-03-31/functions/...` Floci Lambda runs your function code locally inside real Docker containers - close enough as AWS Lambda does (using Firecracker micro VM). ## Supported Operations | Operation | Description | |---|---| | `CreateFunction` | Deploy a Lambda function | | `GetFunction` | Get function details and download URL | | `GetFunctionConfiguration` | Get runtime configuration | | `ListFunctions` | List all functions | | `UpdateFunctionCode` | Upload new code | | `UpdateFunctionConfiguration` | Update runtime, handler, memory, timeout, environment, architectures, tracing, layers, and more | | `DeleteFunction` | Remove a function | | `Invoke` | Invoke a function synchronously or asynchronously | | `CreateEventSourceMapping` | Connect SQS / Kinesis / DynamoDB Streams to a function | | `GetEventSourceMapping` | Get event source mapping details | | `ListEventSourceMappings` | List all event source mappings | | `UpdateEventSourceMapping` | Update a mapping | | `DeleteEventSourceMapping` | Remove a mapping | | `PublishVersion` | Publish an immutable version | | `ListVersionsByFunction` | List all published versions of a function | | `CreateAlias` | Create a named alias pointing to a version | | `GetAlias` | Get alias details | | `ListAliases` | List all aliases for a function | | `UpdateAlias` | Update an alias | | `DeleteAlias` | Delete an alias | | `AddPermission` | Add a resource-policy statement | | `GetPolicy` | Get the function resource policy | | `RemovePermission` | Remove a resource-policy statement | | `GetFunctionCodeSigningConfig` | Return code-signing config (always empty) | | `ListFunctionsByCodeSigningConfig` | Validates the ARN; no code-signing config can exist, so every well-formed ARN returns `ResourceNotFoundException` | | `CreateFunctionUrlConfig` | Provision a function URL | | `GetFunctionUrlConfig` | Read function URL config | | `UpdateFunctionUrlConfig` | Update function URL config | | `DeleteFunctionUrlConfig` | Delete function URL config | | `ListTags` | List tags on a function | | `TagResource` | Tag a function | | `UntagResource` | Untag a function | | `PutFunctionConcurrency` | Set reserved concurrent executions | | `GetFunctionConcurrency` | Get reserved concurrent executions | | `DeleteFunctionConcurrency` | Clear reserved concurrent executions | | `GetAccountSettings` | Account limits plus usage derived from the caller's stored functions | | `PutFunctionEventInvokeConfig` | Set the asynchronous invocation settings of a function, version or alias (retries, event age, destinations) | | `UpdateFunctionEventInvokeConfig` | Change some of those settings, leaving the rest as they are | | `GetFunctionEventInvokeConfig` | Read the asynchronous invocation settings | | `DeleteFunctionEventInvokeConfig` | Remove the asynchronous invocation settings | | `ListFunctionEventInvokeConfigs` | List the asynchronous invocation settings of every version and alias of a function | The event invoke configuration is stored and returned as AWS does, and `AWS::Lambda::EventInvokeConfig` provisions it from a stack. Asynchronous invocations do not yet apply its retry, event age or destination settings. ## Hot-Reloading via Reactive S3 Sync Floci supports an automatic hot-reloading mechanism when functions are deployed via S3. This follows the standard AWS behavior where S3 and Lambda interact, but is optimized for a seamless local development experience. When a Lambda function is created using an S3 bucket and key, Floci maintains a link between the function and its source object. Any subsequent update to that S3 object (e.g., via `s3:PutObject`) automatically triggers a reactive synchronization: 1. **Detection**: Floci detects the S3 update via an internal event system. 2. **Synchronization**: The new code is automatically re-extracted to the local code storage. 3. **Invalidation**: Any active "warm" containers for that function are proactively drained. 4. **Reload**: The very next invocation starts a fresh container with the updated code. This allows you to update your Lambda code by simply re-uploading your ZIP to S3, without having to manually call `UpdateFunctionCode` or restart any containers. ### Example ```bash # 1. Create a function linked to S3 aws lambda create-function \ --function-name my-function \ --code S3Bucket=my-bucket,S3Key=function.zip \ ... # 2. Invoke (starts a warm container) aws lambda invoke --function-name my-function out.json # 3. Update the code in S3 (Triggers Reactive Sync) aws s3 cp updated-function.zip s3://my-bucket/function.zip # 4. Invoke again (automatically picks up the new code) aws lambda invoke --function-name my-function out.json ``` !!! note "Standard Behavior" This mechanism requires no custom configuration or non-standard magic strings. It works with standard AWS SDKs and CLI tools, providing a "live" development feel while staying within the AWS API contract. ## Hot-Reload via Bind Mount For the tightest inner-loop development cycle, Floci supports a **bind-mount hot-reload** mode. Instead of packaging code into a ZIP and uploading it to S3, you point Floci directly at a directory on your host machine. The directory is bind-mounted read-only into `/var/task` inside the container, as `/var/task` is read-only in AWS Lambda, so every invocation runs the files as they currently exist on disk, with no upload or redeploy. This is enabled by using the magic bucket name `hot-reload` when creating a function: ```bash aws lambda create-function \ --function-name my-function \ --runtime nodejs22.x \ --role arn:aws:iam::000000000000:role/lambda-role \ --handler index.handler \ --code S3Bucket=hot-reload,S3Key=/absolute/path/to/your/code \ --endpoint-url http://localhost:4566 ``` The `S3Key` must be an **absolute path** reachable by the Docker daemon. When Floci runs in Docker Compose, this is the path on the Docker host (the machine running Docker), not the path inside the Floci container. ### How it works 1. `CreateFunction` with `S3Bucket=hot-reload` marks the function as a hot-reload function; `S3Key` is stored as the host-side path. 2. On each invocation, Floci starts a **fresh ephemeral container** with the host path bind-mounted at `/var/task`. 3. The container executes the files as they exist at invocation time. Editing a file and immediately invoking picks up the change without any API call. 4. After the invocation completes the container is stopped and removed, ensuring the next invocation always sees the current state of the directory. ### Configuration Hot-reload must be enabled explicitly. By default it is disabled so that `S3Bucket=hot-reload` is treated as a regular S3 bucket name. ```bash FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ENABLED=true # Optional: restrict which host paths may be bind-mounted (comma-separated) FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ALLOWED_PATHS=/home/user/projects,/tmp ``` An `S3Key` is accepted when it is one of the listed directories or inside one. `.` and `..` segments are resolved first, so `/home/user/projects/../secrets` and a sibling such as `/home/user/projects-old` are rejected. Symbolic links on the Docker host are not resolved by Floci. A path containing `:` is rejected. Without an allow-list any absolute host path is accepted except the host root, `/var`, `/proc`, and anything under `/run` or `/var/run`, where the Docker socket lives or can be reached. Floci logs a warning at startup when hot-reload is enabled without an allow-list. **Docker Compose setup**: enable the feature and share the Docker socket: ```yaml services: floci: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ENABLED: "true" ``` ### Limitations - The `S3Key` path is interpreted by the **Docker daemon**, not by Floci. When Floci itself runs inside Docker, the path must exist on the Docker host machine, not inside the Floci container. - Hot-reload containers are always ephemeral, so there is no warm-container reuse. Each invocation pays a cold-start penalty. - `UpdateFunctionCode` on a hot-reload function converts it back to a standard Zip function (the hot-reload bind-mount is removed). - S3 reactive sync is skipped for hot-reload functions because edits are picked up directly from disk. ### Difference from Reactive S3 Sync | | Reactive S3 Sync | Bind-Mount Hot-Reload | |---|---|---| | Trigger | Upload a new ZIP to S3 | Edit files on disk | | Cold start | Only after upload | Every invocation | | Requires upload step | Yes | No | | Works without `hot-reload` enabled | Yes | No | | Path on host required | No | Yes | !!! note "Concurrency enforcement" Reserved concurrency is enforced: invocations beyond the reserved value return `TooManyRequestsException` (HTTP 429). Functions without a reserved value share a **per-region** pool. AWS Lambda's "account-level" limit is in fact a per-account-per-region quota, and Floci mirrors that by partitioning counters on the ARN's region segment. The pool size (default 1000) is configurable via `floci.services.lambda.region-concurrency-limit` and applies independently to each region. `PutFunctionConcurrency` validates that the requested value leaves at least `floci.services.lambda.unreserved-concurrency-min` (default 100) available for unreserved functions in that region. `PutProvisionedConcurrencyConfig` and related provisioned-concurrency operations remain unimplemented. Reducing or clearing a function's reserved value does not kill invocations that are already in flight. This matches AWS, which applies changes only to new invocations. As a consequence, during the drain window `Σreserved-inflight + unreserved-inflight` can briefly exceed `region-concurrency-limit`. Function URLs are also reachable directly on `/{proxy:.*}` under the Lambda URL controller, which routes the request into the normal `Invoke` path. **Versions:** `CreateFunction` and `UpdateFunctionCode` honour `Publish`, publishing a version and reporting it in the response's `Version`. The two differ in the ARN they return, matching the live service: `CreateFunction` keeps the unqualified `FunctionArn` while `UpdateFunctionCode` returns the qualified one (`...:function:name:2`), as `PublishVersion` does. Without `Publish` both answer for `$LATEST` and create nothing. `UpdateFunctionConfiguration` has no `Publish` parameter in the AWS API and none here. `DeleteFunction` honours `Qualifier` — it removes that published version only, leaving `$LATEST`, the other versions and the function's aliases in place; without a qualifier the whole function goes. Matching the live service, deleting `$LATEST` by qualifier and naming an alias are both rejected with `InvalidParameterValueException`, a version an alias points at is a `ResourceConflictException`, and a version that does not exist is a silent success rather than a 404. **Layers:** `PublishLayerVersion`, `GetLayerVersion`, `GetLayerVersionByArn`, `ListLayerVersions`, `ListLayers`, and `DeleteLayerVersion` are implemented, with real local storage under `{lambda.codePath}/layers/{name}/{version}`. `ListLayers` and `ListLayerVersions` honour `CompatibleRuntime`, `CompatibleArchitecture`, `MaxItems` (1-50, defaulting to 50) and `Marker`, and always emit `NextMarker`, null on the last page. Under a filter, `LatestMatchingVersion` is the newest version that matches rather than the newest overall, and a layer with no matching version is omitted; a version published without `CompatibleArchitectures` matches neither architecture. `Marker` is opaque and signed with a key generated at startup, so a fabricated, edited or previous-run token is rejected with `InvalidParameterValueException` rather than applied as a cursor. One divergence: a parameter sent with an empty value (`?CompatibleRuntime=`) is treated as absent rather than rejected, because RESTEasy binds an empty query value as null. Resolution honours the ARN's account and partition: an ARN naming another account resolves to nothing rather than to a same-named layer of the caller's own, matching the live service, which answers that case with `AccessDeniedException` and never substitutes. `CreateFunction`/`UpdateFunctionConfiguration` validate each `Layers` ARN in the caller's own account eagerly against that storage, matching real AWS - an unresolvable one is rejected with `InvalidParameterValueException: Layer version ... does not exist.`, not silently accepted. An ARN naming another account or another partition is answered on the live service by the layer's resource policy: an AWS-managed public layer resolves, and everything else is `AccessDeniedException`. Measured on `CreateFunction` in ap-southeast-1, a foreign-account ARN and a cross-partition ARN return the same `AccessDeniedException`, so Floci returns that for both. Floci implements no layer permissions and cannot fetch real AWS content, so it cannot tell a public layer from a private one; refusing is the faithful default, being the answer AWS gives to every foreign ARN except a public one. Set `floci.services.lambda.accept-external-layer-arns: true` (`FLOCI_SERVICES_LAMBDA_ACCEPT_EXTERNAL_LAYER_ARNS`) to record a same-partition foreign ARN on the function instead of refusing it, which is what a stack attaching Powertools, the AppConfig extension or a vendor-published layer needs. The trade is explicit: with it on, Floci also accepts an ARN AWS would refuse with `AccessDeniedException`, so a typo or a private third-party layer passes here and fails on deploy. The content is never mounted at `/opt` either way, and a warning is logged at attach time and again at invoke; publish your own copy of the content locally under a name you control if the handler needs it at runtime. A layer ARN outside the `aws` partition is refused whatever that setting says. Partitions are isolated, so no resource policy can ever make such a layer readable, and `GetLayerVersionByArn` rejects one outright with `InvalidParameterValueException: Invalid layer version ...`. ## Not Implemented These AWS Lambda operations have no handler in Floci. Calls will return `404` or an error: - Layer permissions (`AddLayerVersionPermission`, `RemoveLayerVersionPermission`, `GetLayerVersionPolicy`) - Provisioned concurrency (`PutProvisionedConcurrencyConfig`, `GetProvisionedConcurrencyConfig`, `ListProvisionedConcurrencyConfigs`, `DeleteProvisionedConcurrencyConfig`) - `InvokeWithResponseStream` - Code signing management (only `GetFunctionCodeSigningConfig` and `ListFunctionsByCodeSigningConfig` are wired; there is no `PutFunctionCodeSigningConfig` or `CreateCodeSigningConfig`, so no code-signing config can exist and `ListFunctionsByCodeSigningConfig` reports every well-formed ARN as `ResourceNotFoundException` — a malformed ARN or an out-of-range `MaxItems` is rejected with `InvalidParameterValueException` first) ## Configuration !!! warning "Lambda container architecture" Floci uses the Docker host architecture by default. Set `FLOCI_SERVICES_LAMBDA_HONOUR_ARCHITECTURES=true` to run each function with its declared `arm64` or `x86_64` architecture. The Docker host must support the selected architecture. Foreign architectures require Docker Desktop or host emulation such as `binfmt_misc` with QEMU. Floci does not fall back to the host architecture when this setting is enabled. | Variable | Default | Description | |---|---|---| | `FLOCI_SERVICES_LAMBDA_ENABLED` | `true` | Enable or disable the service | | `FLOCI_SERVICES_LAMBDA_EPHEMERAL` | `false` | Remove containers after each invocation | | `FLOCI_SERVICES_LAMBDA_HONOUR_ARCHITECTURES` | `false` | Select each function's declared architecture for Docker image pulls and containers | | `FLOCI_SERVICES_LAMBDA_DEFAULT_MEMORY_MB` | `128` | Default function memory (MB) | | `FLOCI_SERVICES_LAMBDA_DEFAULT_TIMEOUT_SECONDS` | `3` | Default function timeout (seconds) | | `FLOCI_SERVICES_LAMBDA_RUNTIME_API_BASE_PORT` | `12000` | First port in the Lambda Runtime API range | | `FLOCI_SERVICES_LAMBDA_RUNTIME_API_MAX_PORT` | `12499` | Last port in the Lambda Runtime API range. One port is held per running container, so the range width caps concurrent executions | | `FLOCI_SERVICES_LAMBDA_CODE_PATH` | `./data/lambda-code` | Directory where Lambda ZIP files are stored | | `FLOCI_SERVICES_LAMBDA_POLL_INTERVAL_MS` | `1000` | Event-source mapping poll interval (milliseconds) | | `FLOCI_SERVICES_LAMBDA_CONTAINER_IDLE_TIMEOUT_SECONDS` | `300` | Idle container shutdown timeout (seconds) | | `FLOCI_SERVICES_LAMBDA_REGION_CONCURRENCY_LIMIT` | `1000` | Maximum concurrent executions per region | | `FLOCI_SERVICES_LAMBDA_UNRESERVED_CONCURRENCY_MIN` | `100` | Minimum unreserved capacity `PutFunctionConcurrency` must leave | | `FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ENABLED` | `false` | Enable bind-mount hot-reload via `S3Bucket=hot-reload` | | `FLOCI_SERVICES_LAMBDA_HOT_RELOAD_ALLOWED_PATHS` | *(unset)* | Comma-separated allowlist of host paths that may be bind-mounted | | `FLOCI_SERVICES_LAMBDA_DOCKER_NETWORK` | *(unset)* | Docker network to attach Lambda containers to (overrides `FLOCI_SERVICES_DOCKER_NETWORK`) | | `FLOCI_SERVICES_LAMBDA_DOCKER_FLAGS` | *(unset)* | Additional Docker flags applied to Lambda containers, including `--env`, `--volume`, `--publish`, `--add-host`, `--dns`, `--label`, `--network`, `--user`, `--privileged`, and `--platform`. Published ports support `host:container` and `127.0.0.1:host:container` forms | | `FLOCI_SERVICES_LAMBDA_EXTRA_HOSTS` | *(unset)* | Comma-separated `hostname:ip` entries added to each Lambda container's `/etc/hosts`; `ip` may be `host-gateway`, mirroring `docker run --add-host` | | `FLOCI_SERVICES_LAMBDA_DOCKER_HOST_OVERRIDE` | *(unset)* | Explicit host/IP that spawned Lambda containers use to reach Floci's Runtime API, bypassing auto-detection | | `FLOCI_SERVICES_LAMBDA_CONTAINER_NAME_PREFIX` | `floci` | Base name prefix for spawned Lambda containers and code volumes (e.g. `acme` → `acme--` containers, `acme-code--` volumes). Must be a valid Docker name segment (`[A-Za-z0-9][A-Za-z0-9_.-]*`); invalid values are ignored with a warning | | `FLOCI_SERVICES_LAMBDA_CODE_VOLUME_POPULATE_CONCURRENCY` | `max(2, cpus/2)` | Maximum concurrent first-time code-volume populates. See the note below | | `FLOCI_SERVICES_LAMBDA_EXECUTOR` | `docker` | Execution backend: `docker` (containers) or `kubernetes` (pods) | | `FLOCI_SERVICES_LAMBDA_KUBERNETES_NAMESPACE` | `default` | Namespace Lambda pods are created in | | `FLOCI_SERVICES_LAMBDA_KUBERNETES_LABELS` | *(unset)* | Extra pod labels as comma-separated `key=value` entries | | `FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS` | *(unset)* | Host/IP pods use to reach Floci; auto-detected when Floci runs in-cluster | | `FLOCI_SERVICES_LAMBDA_KUBERNETES_INIT_IMAGE` | `busybox:1.36` | Init-container image that downloads function code (needs `sh`, `wget`, `unzip`) | !!! note "Changing the container name prefix" Code volumes are resolved by name, and a Floci process only manages resources under its own prefix — deliberately, so multiple Floci processes with different prefixes can share one Docker daemon without touching each other's containers and volumes. Restarting with a different `container-name-prefix` therefore strands the code volumes (and their completion markers) created under the previous prefix: they are no longer reused and no longer part of automatic superseded-volume cleanup. They keep the prefix-independent `floci=true` label, so reclaim them at any time with: ```bash docker volume prune --filter label=floci=true ``` !!! note "Concurrent cold starts of large functions" A function whose unpacked code is at least 32 MB has that code streamed once into a read-only Docker volume, so later cold starts mount it instead of copying. Those first-time *populates* are capped — a burst of them overwhelms the Docker daemon — and the default cap is `max(2, cpus/2)`, derived from the CPU count the JVM sees. Because the JVM honours the container's cgroup CPU quota, running Floci with a small CPU allocation collapses the cap to 2, and a burst of cold starts across *distinct* large functions completes in waves of two rather than in parallel. Six such functions invoked at once take roughly three times the wall-clock of one. Raise the cap to decouple it from the CPU allocation: ```bash FLOCI_SERVICES_LAMBDA_CODE_VOLUME_POPULATE_CONCURRENCY=8 ``` Only first-time populates are gated. Warm containers, already-populated volumes, and functions under 32 MB are never serialised by this. ### Runtime API host override When a Lambda container starts, it calls back into Floci's Runtime API to fetch events and post results. Floci auto-detects the address containers should use for that callback (its own container IP on the shared network, or `host.docker.internal` when running on the host). In most setups this is correct and needs no configuration. On unusual network topologies, for example rootless Podman, auto-detection can pick an address the Lambda container cannot reach, and invocations fail with `connect ECONNREFUSED :12000`. Set `FLOCI_SERVICES_LAMBDA_DOCKER_HOST_OVERRIDE` to the host or IP that containers can actually reach Floci on, and Floci uses it verbatim instead of auto-detecting: ```bash FLOCI_SERVICES_LAMBDA_DOCKER_HOST_OVERRIDE=floci ``` See [Docker Configuration → Running on Podman (rootless)](../configuration/docker.md#running-on-podman-rootless) for a full rootless Podman walkthrough. ### Docker socket requirement With the default `docker` executor, Lambda requires the Docker socket. Mount it in your compose file: ```yaml services: floci: volumes: - /var/run/docker.sock:/var/run/docker.sock ``` If mounting the Docker socket is not acceptable (for example on a hardened Kubernetes cluster), use the [Kubernetes executor](#kubernetes-executor) instead — it needs no privileged access and no socket. ### Kubernetes executor Set `FLOCI_SERVICES_LAMBDA_EXECUTOR=kubernetes` to run each Lambda execution environment as a Kubernetes pod instead of a Docker container. This is designed for CI/CD clusters where privileged containers and `docker.sock` access are not allowed. Floci talks to the cluster through the standard Kubernetes API: when running inside the cluster it uses its ServiceAccount, and when running outside it uses your local kubeconfig. An inline static bearer `token`, a client-certificate/client-key credential with a PKCS#8 private key, or the `aws eks get-token --cluster-name [--region ]` exec plugin (what `aws eks update-kubeconfig` generates) is supported. `--role-arn`, `tokenFile`, any other exec command, and auth-provider credential plugins (gcloud, etc.) are not, and fail with a named error. How an invocation works: 1. On a cold start Floci creates a pod from the function's runtime image (`public.ecr.aws/lambda/*`, same mapping as the Docker executor). 2. An init container (`busybox` by default) downloads the function's deployment package — and any layers — from Floci's S3 over HTTP and unpacks them into shared `emptyDir` volumes at `/var/task` and `/opt`. 3. The runtime container polls Floci's Lambda Runtime API (`AWS_LAMBDA_RUNTIME_API`) exactly like a Docker container would. 4. Warm pods are reused across invocations and reaped after `FLOCI_SERVICES_LAMBDA_CONTAINER_IDLE_TIMEOUT_SECONDS` of inactivity. Pods left behind by a crashed Floci are swept on startup via the `app.kubernetes.io/managed-by=floci` label. Lambda pods connect **back** to Floci on the main port (4566) and the Runtime API port range (9200–9299), so those ports must be reachable from pods in the namespace. When Floci runs in-cluster this works out of the box (pod-to-pod traffic); when Floci runs outside the cluster, set `FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS` to an address the cluster's pods can reach (for example your machine's LAN IP for a `kind` cluster). #### Required RBAC The ServiceAccount Floci runs under needs these permissions in the Lambda namespace. The manifest below is complete, so applying it as-is (together with the Deployment in the next section) yields a working setup: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: floci namespace: floci --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: floci-lambda namespace: floci rules: - apiGroups: [""] resources: ["pods"] verbs: ["create", "get", "list", "watch", "delete", "deletecollection"] - apiGroups: [""] resources: ["pods/log"] verbs: ["get", "watch"] # Only needed when FLOCI_TLS_ENABLED=true (the CA bundle is shared via a ConfigMap) - apiGroups: [""] resources: ["configmaps"] verbs: ["create", "get", "update", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: floci-lambda namespace: floci roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: floci-lambda subjects: - kind: ServiceAccount name: floci namespace: floci ``` #### Running Floci in-cluster A minimal Deployment (namespace `floci` assumed, RBAC from above bound to the `floci` ServiceAccount): ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: floci spec: replicas: 1 selector: matchLabels: { app: floci } template: metadata: labels: { app: floci } spec: serviceAccountName: floci # Required when a Service named `floci` exists in the namespace: service # links inject FLOCI_PORT=tcp://:4566, which collides with Floci's # FLOCI_* configuration convention and fails startup. enableServiceLinks: false containers: - name: floci image: floci/floci:latest env: - name: FLOCI_SERVICES_LAMBDA_EXECUTOR value: kubernetes - name: FLOCI_SERVICES_LAMBDA_KUBERNETES_NAMESPACE value: floci ports: - containerPort: 4566 --- apiVersion: v1 kind: Service metadata: name: floci spec: selector: { app: floci } ports: - port: 4566 targetPort: 4566 ``` Lambda pods reach Floci by pod IP, so the Runtime API ports need no Service entries; only clients of the emulator itself use port 4566. #### Limitations - Hot reload (both bind-mount and Reactive S3 Sync bind variants) and `FLOCI_SERVICES_LAMBDA_AWS_CONFIG_PATH` rely on bind mounts and are not supported; hot-reload functions fail to launch with a clear error. - `Image` package type URIs are passed to the kubelet unchanged. Images in Floci's emulated ECR registry are not pullable by cluster nodes — pre-load them onto the nodes (e.g. `kind load docker-image`) or use a real registry. - Lambda pods carry no `imagePullSecrets`. If your runtime or init images come from an authenticated registry, attach the pull secret to the namespace's `default` ServiceAccount (`kubectl patch serviceaccount default -p '{"imagePullSecrets":[{"name":""}]}'`) so the kubelet uses it for every pod in the namespace. - A cold start waits up to 300 seconds for the pod to reach `Running` (broken images and failing init containers are detected and reported much earlier). - The init container downloads code and layers over plain HTTP on the emulator port even when `FLOCI_TLS_ENABLED=true`. busybox `wget` cannot complete a TLS handshake with Floci, and the Runtime API traffic on the same pod network is plain HTTP anyway. Use a NetworkPolicy if the pod network is part of your threat model. - With `FLOCI_TLS_ENABLED=true`, AWS SDK calls made from inside the function fail TLS verification when pods reach Floci by pod IP, because the self-signed certificate carries no SAN for dynamic pod IPs. Set `FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS` to a hostname covered by the certificate, or provide your own certificate via `floci.tls.cert-path`. - Prefer an IP for `FLOCI_SERVICES_LAMBDA_KUBERNETES_FLOCI_ADDRESS`. With a bare hostname, S3 SDKs inside functions may use virtual-hosted-style addressing (`bucket.`), and nothing in the cluster resolves those subdomains. - IPv6-only clusters are not supported. Floci advertises an IPv4 address to pods and fails fast when none is available. - Each Floci instance sweeps all `managed-by=floci` Lambda pods in its namespace at startup; run multiple Floci instances in separate namespaces. - Layers published by Floci versions before this feature have no stored archive; re-publish them once to make them downloadable by pods. ### S3 virtual-hosted-style addressing inside Lambda containers AWS SDKs use **virtual-hosted-style** S3 addressing by default, forming URLs like `https://my-bucket.s3.amazonaws.com/key`. Against Floci the same pattern becomes `http://my-bucket.localhost.floci.io:4566/key`. When Floci runs **inside Docker**, Lambda containers are on the same Docker network. Docker's embedded DNS resolves the exact alias `localhost.floci.io` correctly, but has no wildcard support. `my-bucket.localhost.floci.io` falls through to public DNS and resolves to the wrong IP, causing the Lambda invocation to time out. **Floci solves this automatically** by running an embedded DNS server (UDP/53) on its container IP. All Lambda containers launched by Floci are configured to use it as their DNS resolver. The embedded DNS server: - Resolves `*.localhost.floci.io` → Floci's Docker network IP - Forwards all other queries to the upstream resolver(s) from `/etc/resolv.conf`, falling back to public resolvers so **public hostnames** (e.g. `business-api.tiktok.com`) resolve from inside Lambda containers No extra configuration or `cap_add` is needed because Docker containers have `CAP_NET_BIND_SERVICE` in their default capability set, so Floci (running as a non-root user) can bind UDP/53 without any changes to your Compose file. ### VpcConfig, SnapStart and LoggingConfig All three round-trip through `CreateFunction`, `UpdateFunctionConfiguration`, `GetFunctionConfiguration`, `GetFunction`, `ListFunctions` and `PublishVersion`. The response shapes are **not** the request shapes, and Floci follows the AWS model rather than echoing the request back: | Field | Request shape | Response shape | Extra members Floci fills in | |---|---|---|---| | `VpcConfig` | `VpcConfig` | `VpcConfigResponse` | `VpcId`, resolved from the first subnet via EC2 | | `SnapStart` | `SnapStart` | `SnapStartResponse` | `OptimizationStatus` — `On` only for a published version with `ApplyOn=PublishedVersions`, `Off` for `$LATEST` | | `LoggingConfig` | `LoggingConfig` | `LoggingConfig` | — | `SnapStart` and `LoggingConfig` are always present in a response, as on AWS: an unset function reads back `SnapStart={ApplyOn: None, OptimizationStatus: Off}` and `LoggingConfig={LogFormat: Text, LogGroup: /aws/lambda/}`. With `LogFormat=JSON`, `ApplicationLogLevel` and `SystemLogLevel` are also returned, defaulting to `INFO`. Terraform treats these as `Computed` blocks, so a missing one is a permanent diff rather than a cosmetic omission. `LoggingConfig` is replaced wholesale on update, not merged — an update naming only `LogFormat` resets `LogGroup` to the default. `LogGroup` is validated against AWS's documented constraint: 1-512 characters matching `[.\-_/#A-Za-z0-9]+`. `ApplicationLogLevel` and `SystemLogLevel` are accepted with any `LogFormat` but are only ever stored — and therefore only ever returned — when the resolved format is `JSON`; supplying them with `LogFormat=Text` is not an error, it is simply a no-op. That is Floci's own call rather than probed AWS behaviour: it keeps the request path consistent with Floci's response shape, which never surfaces the levels for Text. `VpcConfig` is omitted entirely while the function is not attached to a VPC. Subnets that EC2 does not know about are still accepted and returned; only `VpcId` is left off in that case. `RuntimeVersionConfig.RuntimeVersionArn` is returned for managed (non-image) runtimes. Its value is derived from the runtime name, so it is stable across restarts. ### File system configs `FileSystemConfigs` accepts one EFS access point and mounts it under the requested `/mnt/...` path for local Lambda containers. As on AWS, the function must include VPC subnet and security group configuration. The mounted path uses the same shared-volume initialization settings as ECS EFS volumes under `floci.storage.efs`. This configuration is supported through the Lambda API, `AWS::Lambda::Function` resources, and `AWS::Serverless::Function` resources. S3 Files access points are not currently emulated and are rejected instead of being mounted as an empty local volume. !!! note "Resolving public hostnames from Lambda" A Lambda whose handler reaches a public host (`fetch()`/HTTPS to e.g. `business-api.tiktok.com`) resolves it through Floci's embedded DNS. As a safety net, Floci also appends configurable public resolvers (default `8.8.8.8`, `8.8.4.4`) after its own IP on each spawned container's DNS list, so name resolution still works if the embedded forwarder cannot answer. Tune or disable this for offline / locked-down networks where those resolvers are blocked: ```bash FLOCI_DNS_CONTAINER_FALLBACK_SERVERS=1.1.1.1,1.0.0.1 # use different resolvers FLOCI_DNS_CONTAINER_FALLBACK_ENABLED=false # inject only Floci's DNS ``` !!! tip "Docker Compose service names" If Floci runs as a Docker Compose service, set `FLOCI_HOSTNAME` to the service name, for example `FLOCI_HOSTNAME=floci`. When no explicit Lambda Docker network is configured, Floci automatically attaches Lambda containers to the current Compose network. Floci then injects `AWS_ENDPOINT_URL=http://floci:4566` into Lambda containers and returns SQS `QueueUrl` values with the same reachable host. This avoids function-side rewrites from `localhost` or `localhost.floci.io` to `floci`, and keeps normal AWS SDK clients pointed at the Docker DNS name that the Lambda container can resolve. !!! note "Path-style as a workaround" If you cannot use virtual-hosted-style (e.g. Floci is running natively on the host, not in Docker), configure the SDK client with `forcePathStyle: true` / `s3ForcePathStyle: true`. Requests will go to `http://localhost:4566/my-bucket/key` instead and work without DNS. #### Migrating from LocalStack If your Lambda functions have `AWS_ENDPOINT_URL=http://localhost.localstack.cloud:4566` hardcoded, add the LocalStack suffix to Floci's DNS resolver so it resolves to Floci's IP without any function-side changes: Via environment variable, use a comma-separated list for multiple suffixes: ```bash # Single suffix FLOCI_DNS_EXTRA_SUFFIXES=localhost.localstack.cloud # Multiple suffixes FLOCI_DNS_EXTRA_SUFFIXES=localhost.localstack.cloud,localhost.example.internal ``` ### Real AWS Credentials By default, a Lambda function whose execution role exists in Floci's IAM store receives temporary credentials for that role. SDK calls made by the function identify as `assumed-role//floci-session`, and IAM enforcement evaluates the role's policies. If the role is unknown to Floci, the container keeps the compatibility fallback described below. For hybrid local/cloud testing, where some services are emulated and others hit real AWS, you can mount your host `~/.aws` directory into Lambda containers: ```yaml services: floci: image: floci/floci:latest environment: FLOCI_SERVICES_LAMBDA_AWS_CONFIG_PATH: /Users/me/.aws volumes: - /var/run/docker.sock:/var/run/docker.sock ``` When `aws-config-path` is set: - The host path is bind-mounted **read-only** into each Lambda container at `/opt/aws-config` - `AWS_SHARED_CREDENTIALS_FILE` and `AWS_CONFIG_FILE` env vars are set so the SDK discovers credentials regardless of the container's HOME directory - No `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` env vars are injected When unset (default), Floci injects execution-role credentials for a known role. For an unknown role, Floci reads credentials from its own environment and falls back to `test`/`test`/`test`. !!! tip "Routing specific services to real AWS" To keep some services on Floci while others hit real AWS, clear the global endpoint and set service-specific overrides in your function's `--environment`: ``` AWS_ENDPOINT_URL= # clear Floci's global endpoint AWS_ENDPOINT_URL_SES=http://localhost.floci.io:4566 # SES stays on Floci AWS_ENDPOINT_URL_CLOUDWATCHLOGS=http://localhost.floci.io:4566 # CloudWatch stays on Floci ``` The AWS SDK supports `AWS_ENDPOINT_URL_` natively. Services without an override will use real AWS endpoints. !!! note "Credential passthrough without mounting" For functions whose execution role is unknown to Floci, you can pass static credentials to Floci's environment directly. When `aws-config-path` is unset, Floci forwards its own `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` env vars into those Lambda containers: ```yaml environment: AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN} ``` A known execution role takes precedence over `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` values in the function environment. Use `aws-config-path` when the function must use mounted credentials instead of its emulated execution role. Passthrough is on whenever those three variables are set in Floci's own environment, so a Floci started from a shell that exports real AWS credentials (`aws-vault exec`, a sourced credentials file, a CI runner) hands them to any function whose role it does not know. An `AWS_PROFILE` or an `aws sso login` alone does not do this: those populate config and cache files, not the environment. Floci logs a `WARN` carrying the forwarded access-key prefix the first time it happens. Give the function a role Floci knows, or set `aws-config-path`, to keep host credentials out of the container. ### Locally built images A container image function whose `ImageUri` names an image already present on the Docker daemon runs that image directly. This includes AWS-shaped `.dkr.ecr..amazonaws.com/:` URIs, which are otherwise rewritten to [Floci's emulated ECR registry](ecr.md) at pull time: build the image under the exact URI the function declares (`docker build -t .`) and no push is needed. With `FLOCI_DOCKER_IMAGE_REGISTRY_BASE` set, tag it as `/` instead, since that is the reference Floci launches. Set `FLOCI_SERVICES_ECR_PREFER_LOCAL_IMAGES=false` to always resolve AWS-shaped URIs through the emulated registry. ### Private registry authentication Container image functions (`"PackageType": "Image"`) that pull from private registries need Docker credentials. See [Docker Configuration → Private Registry Authentication](../configuration/docker.md#private-registry-authentication) for the full guide. ## Examples ```bash export AWS_ENDPOINT_URL=http://localhost:4566 # Package a simple Node.js function cat > index.mjs << 'EOF' export const handler = async (event) => { console.log("Event:", JSON.stringify(event)); return { statusCode: 200, body: JSON.stringify({ hello: "world" }) }; }; EOF zip function.zip index.mjs # Deploy the function aws lambda create-function \ --function-name my-function \ --runtime nodejs22.x \ --role arn:aws:iam::000000000000:role/lambda-role \ --handler index.handler \ --zip-file fileb://function.zip \ --endpoint-url $AWS_ENDPOINT_URL # Invoke synchronously aws lambda invoke \ --function-name my-function \ --payload '{"key":"value"}' \ --cli-binary-format raw-in-base64-out \ response.json \ --endpoint-url $AWS_ENDPOINT_URL cat response.json # Invoke asynchronously aws lambda invoke \ --function-name my-function \ --invocation-type Event \ --payload '{"key":"value"}' \ --cli-binary-format raw-in-base64-out \ /dev/null \ --endpoint-url $AWS_ENDPOINT_URL # Update code zip function.zip index.mjs aws lambda update-function-code \ --function-name my-function \ --zip-file fileb://function.zip \ --endpoint-url $AWS_ENDPOINT_URL ``` ## Event Source Mappings Connect Lambda to SQS, Kinesis, or DynamoDB Streams. Self-managed Apache Kafka event source mappings are accepted, validated, persisted, and returned on the wire, but Floci does not run an active Kafka consumer poller: For DynamoDB Streams mappings, Floci retries failed batches with exponential backoff, honors `MaximumRetryAttempts` and `MaximumRecordAgeInSeconds`, and sends discarded batches to configured SQS or SNS `DestinationConfig.OnFailure` destinations. ```bash # SQS trigger QUEUE_ARN=$(aws sqs get-queue-attributes \ --queue-url $AWS_ENDPOINT_URL/000000000000/orders \ --attribute-names QueueArn \ --query Attributes.QueueArn --output text \ --endpoint-url $AWS_ENDPOINT_URL) aws lambda create-event-source-mapping \ --function-name my-function \ --event-source-arn $QUEUE_ARN \ --batch-size 10 \ --endpoint-url $AWS_ENDPOINT_URL ``` ### MaximumBatchingWindowInSeconds (SQS) `CreateEventSourceMapping` and `UpdateEventSourceMapping` accept a `MaximumBatchingWindowInSeconds` integer between 0 and 300. `GetEventSourceMapping` and `ListEventSourceMappings` echo it back when set; responses omit the field when it was never configured. Values outside 0 to 300 are rejected with `InvalidParameterValueException`. When the window is greater than 0, the SQS poller holds an underfilled batch open, accumulating messages across polls, and invokes the function once the batch reaches `BatchSize` or the window elapses since the first buffered message, whichever comes first. A window of 0 (or an unset window) invokes as soon as any message is available, which is the previous behaviour. ```bash aws lambda create-event-source-mapping \ --function-name my-function \ --event-source-arn $QUEUE_ARN \ --batch-size 10 \ --maximum-batching-window-in-seconds 5 \ --endpoint-url $AWS_ENDPOINT_URL ``` ### ScalingConfig (SQS only) `CreateEventSourceMapping` and `UpdateEventSourceMapping` accept a `ScalingConfig.MaximumConcurrency` integer between 2 and 1000 on SQS event sources, matching the AWS wire format. `GetEventSourceMapping` and `ListEventSourceMappings` echo the value back when set; responses omit the `ScalingConfig` field entirely when no cap is configured. ```bash aws lambda create-event-source-mapping \ --function-name my-function \ --event-source-arn $QUEUE_ARN \ --scaling-config MaximumConcurrency=5 \ --endpoint-url $AWS_ENDPOINT_URL ``` Validation mirrors AWS: values outside 2–1000 are rejected with `InvalidParameterValueException`, and `ScalingConfig` on a non-SQS event source (Kinesis / DynamoDB Streams) is also rejected. Those services use `ParallelizationFactor` instead, which is a separate field. !!! note "Enforcement status" The configured `MaximumConcurrency` is persisted and returned on the wire, but the SQS poller does not yet cap concurrent invocations at this value (the poller today serializes invocations per ESM to one at a time regardless). Real parallel dispatch capped by `MaximumConcurrency` is tracked as a follow-up. ### FilterCriteria `CreateEventSourceMapping` and `UpdateEventSourceMapping` accept a `FilterCriteria` with up to 5 `Filters`, each carrying an event-pattern `Pattern`, using the same EventBridge pattern syntax as EventBridge Pipes. `GetEventSourceMapping` and `ListEventSourceMappings` echo it back when set and omit the field entirely when unset. Filters are **enforced** in the Kinesis, DynamoDB Streams, and SQS pollers: only matching records are delivered to the function. ```bash aws lambda create-event-source-mapping \ --function-name my-function \ --event-source-arn $QUEUE_ARN \ --filter-criteria '{"Filters":[{"Pattern":"{\"body\":{\"type\":[\"order\"]}}"}]}' \ --endpoint-url $AWS_ENDPOINT_URL ``` A pattern addresses each record the way its source presents it, matching AWS: | Source | Filter key | Notes | |--------|-----------|-------| | SQS | `body` (+ `messageAttributes`, etc.) | A JSON body is matched structurally; a non-JSON body cannot satisfy an object pattern and is dropped. Other top-level record fields (e.g. `messageId`, `messageAttributes`) are also addressable. | | DynamoDB Streams | `dynamodb` (+ `eventName`, etc.) | Matches the AttributeValue-wrapped image directly. Numeric operators do not apply (AttributeValue numbers are JSON strings), matching AWS. | | Kinesis | `data` (+ `partitionKey`) | `data` is matched against the **base64-decoded** payload (the delivered event still carries `data` base64-encoded); `partitionKey` is the supported metadata key. | Non-matching records are consumed, not retried: Kinesis and DynamoDB Streams advance the shard iterator past them (a fully filtered batch still advances the checkpoint, so a shard never stalls), and SQS deletes filtered-out messages from the queue. Validation mirrors AWS and runs before the mapping is stored: each `Pattern` must be a JSON object whose field values are non-empty match arrays or nested objects, at most 5 `Filters`, and each `Pattern` at most 4096 characters: violations are rejected with `InvalidParameterValueException`. A `FilterCriteria` of `{}` or with an empty `Filters` array clears any existing filters. !!! note "Supported operators" Filtering reuses Floci's shared EventBridge matcher (the same one EventBridge Pipes uses). It supports exact match on a string, number or `null`, plus `prefix`, `suffix`, `equals-ignore-case`, `exists`, `anything-but` (a string, a non-empty array of strings and numbers, or a nested `prefix`), and `numeric` comparison/value pairs using `=`, `>`, `>=`, `<`, `<=`. Patterns using only these behave as on AWS. A pattern is validated at `CreateEventSourceMapping` and `UpdateEventSourceMapping` and rejected with `InvalidParameterValueException` when the matcher could not satisfy it: an unknown operator, an operator AWS documents that Floci does not implement (`cidr`, `wildcard`), more than one operator in a single match element, a boolean literal, a wrong operand type, or a malformed `numeric` sequence such as an odd-length array or a non-numeric value. This is stricter than AWS, which accepts several of these. The trade is deliberate: the pollers checkpoint past (Kinesis, DynamoDB) or delete (SQS) any record a pattern fails to match, so a pattern that cannot match destroys records rather than being inert, and create time is the last point at which the caller can still act on it. One matcher deviation remains and is not a validation error, because it depends on the record rather than the pattern: event-value array intersection, where AWS matches when the record's own field is itself an array and any element satisfies the pattern, and Floci does not. !!! warning "Direct Lambda API only" `FilterCriteria` is carried only by the direct Lambda `CreateEventSourceMapping` / `UpdateEventSourceMapping` APIs (SDK, CLI, Terraform). CloudFormation and SAM event-source-mapping resources do not yet forward `FilterCriteria` (as they also do not forward `ScalingConfig` or `DestinationConfig`); forwarding it through those paths is tracked as a follow-up. ## Supported Runtimes Any runtime that has an official AWS Lambda container image works with Floci (e.g. `nodejs22.x`, `python3.13`, `java21`, `go1.x`, `provided.al2023`).