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.