Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions v14.x/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
layer.zip
layer
test
15 changes: 15 additions & 0 deletions v14.x/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM lambci/lambda-base:build

COPY bootstrap.c bootstrap.js package.json /opt/

ARG NODE_VERSION

RUN curl -sSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | \
tar -xJ -C /opt --strip-components 1 -- node-v${NODE_VERSION}-linux-x64/bin/node && \
strip /opt/bin/node

RUN cd /opt && \
export NODE_MAJOR=$(echo $NODE_VERSION | awk -F. '{print "\""$1"\""}') && \
clang -Wall -Werror -s -O2 -D NODE_MAJOR="$NODE_MAJOR" -o bootstrap bootstrap.c && \
rm bootstrap.c && \
zip -yr /tmp/layer.zip .
42 changes: 42 additions & 0 deletions v14.x/bootstrap.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>

#ifndef NODE_MAJOR
#error Must pass NODE_MAJOR to the compiler (eg "10")
#define NODE_MAJOR ""
#endif

#define AWS_EXECUTION_ENV "AWS_Lambda_nodejs" NODE_MAJOR "_lambci"
#define NODE_PATH "/opt/nodejs/node" NODE_MAJOR "/node_modules:" \
"/opt/nodejs/node_modules:" \
"/var/runtime/node_modules:" \
"/var/runtime:" \
"/var/task"
#define MIN_MEM_SIZE 128
#define ARG_BUF_SIZE 32

int main(void) {
setenv("AWS_EXECUTION_ENV", AWS_EXECUTION_ENV, true);
setenv("NODE_PATH", NODE_PATH, true);

const char *mem_size_str = getenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE");
int mem_size = mem_size_str != NULL ? atoi(mem_size_str) : MIN_MEM_SIZE;

char max_semi_space_size[ARG_BUF_SIZE];
snprintf(max_semi_space_size, ARG_BUF_SIZE, "--max-semi-space-size=%d", mem_size * 5 / 100);

char max_old_space_size[ARG_BUF_SIZE];
snprintf(max_old_space_size, ARG_BUF_SIZE, "--max-old-space-size=%d", mem_size * 90 / 100);

execv("/opt/bin/node", (char *[]){
"node",
"--expose-gc",
max_semi_space_size,
max_old_space_size,
"/opt/bootstrap.js",
NULL});
perror("Could not execv");
return EXIT_FAILURE;
}
208 changes: 208 additions & 0 deletions v14.x/bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import http from 'http'

const RUNTIME_PATH = '/2018-06-01/runtime'

const CALLBACK_USED = Symbol('CALLBACK_USED')

const {
AWS_LAMBDA_FUNCTION_NAME,
AWS_LAMBDA_FUNCTION_VERSION,
AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
AWS_LAMBDA_LOG_GROUP_NAME,
AWS_LAMBDA_LOG_STREAM_NAME,
LAMBDA_TASK_ROOT,
_HANDLER,
AWS_LAMBDA_RUNTIME_API,
} = process.env

const [HOST, PORT] = AWS_LAMBDA_RUNTIME_API.split(':')

start()

async function start() {
let handler
try {
handler = await getHandler()
} catch (e) {
await initError(e)
return process.exit(1)
}
tryProcessEvents(handler)
}

async function tryProcessEvents(handler) {
try {
await processEvents(handler)
} catch (e) {
console.error(e)
return process.exit(1)
}
}

async function processEvents(handler) {
while (true) {
const { event, context } = await nextInvocation()

let result
try {
result = await handler(event, context)
} catch (e) {
await invokeError(e, context)
continue
}
const callbackUsed = context[CALLBACK_USED]

await invokeResponse(result, context)

if (callbackUsed && context.callbackWaitsForEmptyEventLoop) {
return process.prependOnceListener('beforeExit', () => tryProcessEvents(handler))
}
}
}

function initError(err) {
return postError(`${RUNTIME_PATH}/init/error`, err)
}

async function nextInvocation() {
const res = await request({ path: `${RUNTIME_PATH}/invocation/next` })

if (res.statusCode !== 200) {
throw new Error(`Unexpected /invocation/next response: ${JSON.stringify(res)}`)
}

if (res.headers['lambda-runtime-trace-id']) {
process.env._X_AMZN_TRACE_ID = res.headers['lambda-runtime-trace-id']
} else {
delete process.env._X_AMZN_TRACE_ID
}

const deadlineMs = +res.headers['lambda-runtime-deadline-ms']

const context = {
awsRequestId: res.headers['lambda-runtime-aws-request-id'],
invokedFunctionArn: res.headers['lambda-runtime-invoked-function-arn'],
logGroupName: AWS_LAMBDA_LOG_GROUP_NAME,
logStreamName: AWS_LAMBDA_LOG_STREAM_NAME,
functionName: AWS_LAMBDA_FUNCTION_NAME,
functionVersion: AWS_LAMBDA_FUNCTION_VERSION,
memoryLimitInMB: AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
getRemainingTimeInMillis: () => deadlineMs - Date.now(),
callbackWaitsForEmptyEventLoop: true,
}

if (res.headers['lambda-runtime-client-context']) {
context.clientContext = JSON.parse(res.headers['lambda-runtime-client-context'])
}

if (res.headers['lambda-runtime-cognito-identity']) {
context.identity = JSON.parse(res.headers['lambda-runtime-cognito-identity'])
}

const event = JSON.parse(res.body)

return { event, context }
}

async function invokeResponse(result, context) {
const res = await request({
method: 'POST',
path: `${RUNTIME_PATH}/invocation/${context.awsRequestId}/response`,
body: JSON.stringify(result === undefined ? null : result),
})
if (res.statusCode !== 202) {
throw new Error(`Unexpected /invocation/response response: ${JSON.stringify(res)}`)
}
}

function invokeError(err, context) {
return postError(`${RUNTIME_PATH}/invocation/${context.awsRequestId}/error`, err)
}

async function postError(path, err) {
const lambdaErr = toLambdaErr(err)
const res = await request({
method: 'POST',
path,
headers: {
'Content-Type': 'application/json',
'Lambda-Runtime-Function-Error-Type': lambdaErr.errorType,
},
body: JSON.stringify(lambdaErr),
})
if (res.statusCode !== 202) {
throw new Error(`Unexpected ${path} response: ${JSON.stringify(res)}`)
}
}

async function getHandler() {
const appParts = _HANDLER.split('.')

if (appParts.length !== 2) {
throw new Error(`Bad handler ${_HANDLER}`)
}

const [modulePath, handlerName] = appParts

// Let any errors here be thrown as-is to aid debugging
const importPath = `${LAMBDA_TASK_ROOT}/${modulePath}.js`
const app = await new Promise((res, rej) => import(importPath).then(res).catch(rej))

const userHandler = app[handlerName]

if (userHandler == null) {
throw new Error(`Handler '${handlerName}' missing on module '${modulePath}'`)
} else if (typeof userHandler !== 'function') {
throw new Error(`Handler '${handlerName}' from '${modulePath}' is not a function`)
}

return (event, context) => new Promise((resolve, reject) => {
context.succeed = resolve
context.fail = reject
context.done = (err, data) => err ? reject(err) : resolve(data)

const callback = (err, data) => {
context[CALLBACK_USED] = true
context.done(err, data)
}

let result
try {
result = userHandler(event, context, callback)
} catch (e) {
return reject(e)
}
if (result != null && typeof result.then === 'function') {
result.then(resolve, reject)
}
})
}

function request(options) {
options.host = HOST
options.port = PORT

return new Promise((resolve, reject) => {
const req = http.request(options, res => {
const bufs = []
res.on('data', data => bufs.push(data))
res.on('end', () => resolve({
statusCode: res.statusCode,
headers: res.headers,
body: Buffer.concat(bufs).toString(),
}))
res.on('error', reject)
})
req.on('error', reject)
req.end(options.body)
})
}

function toLambdaErr(err) {
const { name, message, stack } = err
return {
errorType: name || typeof err,
errorMessage: message || ('' + err),
stackTrace: (stack || '').split('\n').slice(1),
}
}
6 changes: 6 additions & 0 deletions v14.x/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

. ./config.sh

docker build --build-arg NODE_VERSION -t node-provided-lambda-v14.x .
docker run --rm -v "$PWD":/app node-provided-lambda-v14.x cp /tmp/layer.zip /app/
11 changes: 11 additions & 0 deletions v14.x/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash

. ./config.sh

REGIONS="$(aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions \
--query 'Parameters[].Value' --output text | tr '[:blank:]' '\n' | grep -v -e ^cn- -e ^us-gov- | sort -r)"

for region in $REGIONS; do
aws lambda list-layer-versions --region $region --layer-name $LAYER_NAME \
--query 'LayerVersions[*].[LayerVersionArn]' --output text
done
2 changes: 2 additions & 0 deletions v14.x/config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export LAYER_NAME=nodejs14
export NODE_VERSION=14.3.0
Binary file added v14.x/layer.zip
Binary file not shown.
5 changes: 5 additions & 0 deletions v14.x/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "node-custom-lambda-v14.x",
"version": "1.0.0",
"type": "module"
}
25 changes: 25 additions & 0 deletions v14.x/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

. ./config.sh

DESCRIPTION="Node.js v${NODE_VERSION} custom runtime"
FILENAME=${LAYER_NAME}-${NODE_VERSION}.zip

REGIONS="$(aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions \
--query 'Parameters[].Value' --output text | tr '[:blank:]' '\n' | grep -v -e ^cn- -e ^us-gov- -e ^ap-northeast-3 | sort -r)"

aws s3api put-object --bucket lambci --key layers/${FILENAME} --body layer.zip

for region in $REGIONS; do
aws s3api copy-object --region $region --copy-source lambci/layers/${FILENAME} \
--bucket lambci-${region} --key layers/${FILENAME} && \
aws lambda add-layer-version-permission --region $region --layer-name $LAYER_NAME \
--statement-id sid1 --action lambda:GetLayerVersion --principal '*' \
--version-number $(aws lambda publish-layer-version --region $region --layer-name $LAYER_NAME \
--content S3Bucket=lambci-${region},S3Key=layers/${FILENAME} \
--description "$DESCRIPTION" --query Version --output text) &
done

for job in $(jobs -p); do
wait $job
done
22 changes: 22 additions & 0 deletions v14.x/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

rm -rf layer && unzip layer.zip -d layer

cd test

npm ci

# Create zipfile for uploading to Lambda – we don't use this here
rm -f lambda.zip && zip -qyr lambda.zip index.js node_modules

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler2

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler3

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler4

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler5

docker run --rm -v "$PWD":/var/task -v "$PWD"/../layer:/opt lambci/lambda:provided index.handler6
45 changes: 45 additions & 0 deletions v14.x/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Test that global requires work
import aws4 from 'aws4'

const interval = setInterval(console.log, 100, 'ping')

const sleep = async (miliseconds) => await new Promise(res => setTimeout(res, miliseconds))

// Test top-level await works
await sleep(1000)

export const handler = async (event, context) => {
console.log(process.version)
console.log(process.execPath)
console.log(process.execArgv)
console.log(process.argv)
console.log(process.cwd())
console.log(process.env)
console.log(event)
console.log(context)
console.log(context.getRemainingTimeInMillis())
console.log(aws4)
return { some: 'obj!' }
}

export const handler2 = (event, context) => {
setTimeout(context.done, 100, null, { some: 'obj!' })
}

export const handler3 = (event, context) => {
setTimeout(context.succeed, 100, { some: 'obj!' })
}

export const handler4 = (event, context) => {
setTimeout(context.fail, 100, new Error('This error should be logged'))
}

export const handler5 = (event, context, cb) => {
setTimeout(cb, 100, null, { some: 'obj!' })
setTimeout(clearInterval, 100, interval)
}

export const handler6 = (event, context, cb) => {
context.callbackWaitsForEmptyEventLoop = false
setTimeout(cb, 100, null, { some: 'obj!' })
}
Loading