Kensio Software Blog »

@kensio/yulin v0.34.2 adds simulated Lambda

Hugh Grigg | Kensio Software | Friday 24 Jul 2026

Version v0.34.2 of the @kensio/yulin package adds a simulated Lambda service for local development and isolated testing.

Functions are created and invoked entirely in-process and in memory, with no containers and no real AWS infrastructure.

The simulator supports CreateFunctionCommand, GetFunctionCommand and InvokeCommand, including the RequestResponse, Event and DryRun invocation types.

It also supports AWS::Lambda::Function resources when deploying CloudFormation or CDK templates into the simulated AWS.

Function code can come from three places: a real in-process handler function, zip archive bytes on Code.ZipFile, or a zip object stored in sim S3.

The quickest of those is passing an ordinary function from your own process as the function code. It can be stepped through in a debugger and can close over local test state:

import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";

import { SimAws } from "@kensio/yulin";
import { makeLambdaZipFileInput } from "@kensio/yulin/lambda";

const simAws = new SimAws();
const lambda = simAws.lambda();

await lambda.createFunction(
  new CreateFunctionCommand({
    FunctionName: "example-greeter",
    Role: "arn:aws:iam::123456789012:role/ExampleGreeterRole",
    Code: {
      ZipFile: makeLambdaZipFileInput(
        (event: { name: string }) => `Hello ${event.name}`,
      ),
    },
  }),
);

const output = await lambda.invoke(
  new InvokeCommand({
    FunctionName: "example-greeter",
    Payload: JSON.stringify({ name: "Yulin" }),
  }),
);

console.log(Buffer.from(output.Payload).toString());

makeLambdaZipFileInput produces something the SDK-shaped Code.ZipFile input accepts, so the create call keeps the shape it has against the real AWS SDK.

Handlers take the real (event, context, callback) signature, and typed handlers written against the aws-lambda typings package can be passed in unchanged.

Creating a function requires an execution Role ARN, as on real AWS. A new function starts Pending and becomes Active in the background, so simAws.backgroundTasksComplete() is there for tests that assert on the state.

A handler that throws is reported AWS-style, with FunctionError: "Unhandled" and an error document payload, rather than the invoke call itself throwing.

Real zip archives work too. makeLambdaCodeZip builds real zip bytes from a source string or from a files map keyed by archive path.

That archive runs in a Node.js vm context with cold start semantics, so the module is imported once on first invocation and its state stays warm across invocations afterwards.

The runtime provides @aws-sdk/* packages the way the real Node.js runtime does, without them being bundled into the archive. Clients constructed inside function code are routed into the same simulated AWS environment.

Calls the handler makes run as the function’s execution role, so simulated IAM authorizes them just as a real execution role would. Given an execution role allowed to read from example-bucket:

import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";

import { SimAws } from "@kensio/yulin";
import { makeLambdaCodeZip } from "@kensio/yulin/lambda";

const simAws = new SimAws();

await simAws.lambda().createFunction(
  new CreateFunctionCommand({
    FunctionName: "example-reader",
    Role: readerRole.Arn,
    Handler: "index.handler",
    Code: {
      ZipFile: makeLambdaCodeZip(`
        const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
        const s3Client = new S3Client({});
        exports.handler = async (event) => {
          const output = await s3Client.send(
            new GetObjectCommand({
              Bucket: "example-bucket",
              Key: event.objectKey,
            }),
          );
          return await output.Body.transformToString();
        };
      `),
    },
  }),
);

const output = await simAws.lambda().invoke(
  new InvokeCommand({
    FunctionName: "example-reader",
    Payload: JSON.stringify({ objectKey: "public/config.json" }),
  }),
);

The require("@aws-sdk/client-s3") there resolves to the real package, with its clients pointed at the simulated AWS rather than at anything on the network.

If the execution role is not allowed to make that read, sim IAM denies it and the invocation reports the denial as a function error, in the same way a real execution role denial surfaces inside the handler.

Sim CloudFormation can create functions as well, so they can come from the same template as the rest of the infrastructure:

{
  "Resources": {
    "ExampleGreeterFunction": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "FunctionName": "example-greeter",
        "Role": "arn:aws:iam::123456789012:role/ExampleGreeterRole",
        "Handler": "index.handler",
        "Runtime": "nodejs22.x",
        "Code": {
          "ZipFile": "exports.handler = async (event) => 'Hello ' + event.name;"
        }
      }
    }
  }
}

Inline ZipFile template source is packaged and run in the vm runtime, as if it had been zipped and passed to CreateFunctionCommand.

The Role is usually an Fn::GetAtt reference to a same-stack AWS::IAM::Role, which resolves to that Role’s ARN. Ref on the function returns its name and Fn::GetAtt supports Arn.

Deploy-time bindings can back a template function with a real in-process handler instead of its template code, which is the CloudFormation counterpart of makeLambdaZipFileInput:

import { SimAws } from "@kensio/yulin";

const simAws = new SimAws();

await simAws.cloudFormation().deployTemplate({
  stackName: "example-greeter-stack",
  template: exampleTemplate,
  bindings: [
    {
      logicalId: "ExampleGreeterFunction",
      handler: (event: { name: string }): string => `Hello ${event.name}`,
    },
  ],
});

A bound function can leave out its template Code and Handler entirely, and other functions in the same template keep running their template code.

Bindings can target a function by logicalId, functionName, arn or full cdkPath, and the logical ID also matches a CDK construct ID from aws:cdk:path metadata.

Sim Lambda covers what isolated tests and local development need rather than full Lambda parity. The Lambda tests in the repository are the clearest picture of what is actually covered.

UpdateFunctionCode, DeleteFunction and function listing are not implemented yet, and versions, aliases and qualifiers are not simulated.

The vm runtime runs CommonJS function code only, so ES module source fails with a hint rather than running. Container image functions are not supported, as the simulator stays Docker-free, and Layers are not simulated.

Environment.Variables is not applied yet, and Timeout is recorded without interrupting the handler.

The vm context is a namespacing convenience rather than a security boundary. Function code runs in-process with the same trust as the test suite itself, so this is not a way to run untrusted code.

The Lambda simulator integrates with the rest of Yulin, so S3, IAM, CloudFormation and CDK can all participate in the same simulated environment for local development and system tests.

npm: https://www.npmjs.com/package/@kensio/yulin