-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lambda): allow running a build file #30196
Changes from 11 commits
ad1bb87
0b96fbb
52d3ce4
4ddf6e1
94b6685
5412aaf
9869ea9
c3212e8
a162e59
63a1959
12134f9
46da5e1
7eef2ee
573756f
2e21017
3e89cf1
df987a9
90fa9a4
a0610f6
731c363
83b4238
f83234a
dc8f61e
7cccfd8
fa4bde2
916f92d
c0c324a
870090b
c193beb
1e59d4a
a66abc1
58d2c86
6e4b2cc
70469ea
e5de5ff
a6bcad2
26b4b15
7200961
54fe929
63d0788
2732c24
86c345b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,8 +27,12 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions { | |
/** | ||
* The name of the exported handler in the entry file. | ||
* | ||
* The handler is prefixed with `index.` unless the specified handler value contains a `.`, | ||
* in which case it is used as-is. | ||
* * If the `code` property is supplied, then you must include the `handler` property. The handler should be the name of the file | ||
* that contains the exported handler and the function that should be called when the AWS Lambda is invoked. For example, if | ||
* you had a file called `myLambda.js` and the function to be invoked was `myHandler`, then you should input `handler` property as `myLambda.myHandler`. | ||
* | ||
* * If the `code` property is not supplied and the handler input does not contain a `.`, then the handler is prefixed with `index.` (index period). Otherwise, | ||
* the handler property is not modified. | ||
* | ||
* @default handler | ||
*/ | ||
|
@@ -83,6 +87,17 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions { | |
* @default - the directory containing the `depsLockFilePath` | ||
*/ | ||
readonly projectRoot?: string; | ||
|
||
/** | ||
* The code that will be deployed to the Lambda Handler. If included, then properties related to | ||
* bundling of the code are ignored. In the constructor of NodeJsFunction, where the Super is called, | ||
* you can see which bundling properties are ignored. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's re-phrase this. Maybe just leave it to "If included, then properties related to bundling of the code are ignored". The "In the constructor of the NodeJsFunction, where the Super is called you can see which bundling properties are ignored" isn't helpful since I wouldn't necessarily expect a user to have to go into our source code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that makes sense, agreed |
||
* | ||
* * If the `code` field is specified, then you must include the `handler` property. | ||
* | ||
* @default - the code is bundled by esbuild | ||
*/ | ||
readonly code?: lambda.Code; | ||
} | ||
|
||
/** | ||
|
@@ -94,27 +109,41 @@ export class NodejsFunction extends lambda.Function { | |
throw new Error('Only `NODEJS` runtimes are supported.'); | ||
} | ||
|
||
// Entry and defaults | ||
const entry = path.resolve(findEntry(id, props.entry)); | ||
const handler = props.handler ?? 'handler'; | ||
const architecture = props.architecture ?? Architecture.X86_64; | ||
const depsLockFilePath = findLockFile(props.depsLockFilePath); | ||
const projectRoot = props.projectRoot ?? path.dirname(depsLockFilePath); | ||
const runtime = getRuntime(scope, props); | ||
|
||
super(scope, id, { | ||
...props, | ||
runtime, | ||
code: Bundling.bundle(scope, { | ||
...props.bundling ?? {}, | ||
entry, | ||
if (props.code !== undefined) { | ||
if (props.handler === undefined) { | ||
throw new Error('Cannot determine handler when `code` property is specified. Use `handler` property to specify a handler.'); | ||
} | ||
|
||
super(scope, id, { | ||
...props, | ||
runtime, | ||
architecture, | ||
depsLockFilePath, | ||
projectRoot, | ||
}), | ||
handler: handler.indexOf('.') !== -1 ? `${handler}` : `index.${handler}`, | ||
}); | ||
code: props.code, | ||
handler: props.handler!, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove the |
||
}); | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you help me understand why we need this new code block here? Why can't we just determine the handler in the same way we are doing it right now? Couldn't we just keep the existing code and do something where if // Entry and defaults
const entry = path.resolve(findEntry(id, props.entry));
const architecture = props.architecture ?? Architecture.X86_64;
const depsLockFilePath = findLockFile(props.depsLockFilePath);
const projectRoot = props.projectRoot ?? path.dirname(depsLockFilePath);
const handler = props.handler ?? 'handler';
super(scope, id, {
...props,
runtime,
code: props.code ?? Bundling.bundle(scope, {
...props.bundling ?? {},
entry,
runtime,
architecture,
depsLockFilePath,
projectRoot,
}),
handler: handler.indexOf('.') !== -1 ? `${handler}` : `index.${handler}`,
});
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what I originally had, but I think it's a worse customer experience. Because there's nothing forcing the customer to name the file containing the exported handler There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see where you're coming from but I'm not sure I agree. I think this construct has already established that the expected behavior is we will default the handler if it isn't supplied. Consider a customer that is using the nodejs function and they currently don't supply a handler and just use the default. Now, once this feature is released, they decide to use the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
My understanding is, if a customer does switch to using Code.fromCustomCommand and wants an error-free transition with default handler values, then their build script will need to create a file (either in a directory or a zip file) named There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. basically, to avoid having an outage in production, if a customer is switching to using this Code.fromCustomCommand, the customer would need to know that their build file must output a file called index.js that exports a function called handler. That's not something that I think should be hidden from the customer. By requiring handler, we don't abstract away a detail that could potentially lead to an outage There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that this is more complicated, but the alternative is to push this complexity onto the customer after they've deployed their lambda There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Happy to change my mind on this, and with the way you've explained it I agree. Doesn't make sense to let the customer fail during deploy time. |
||
// Entry and defaults | ||
const entry = path.resolve(findEntry(id, props.entry)); | ||
const architecture = props.architecture ?? Architecture.X86_64; | ||
const depsLockFilePath = findLockFile(props.depsLockFilePath); | ||
const projectRoot = props.projectRoot ?? path.dirname(depsLockFilePath); | ||
const handler = props.handler ?? 'handler'; | ||
|
||
super(scope, id, { | ||
...props, | ||
runtime, | ||
code: Bundling.bundle(scope, { | ||
...props.bundling ?? {}, | ||
entry, | ||
runtime, | ||
architecture, | ||
depsLockFilePath, | ||
projectRoot, | ||
}), | ||
handler: handler.indexOf('.') !== -1 ? `${handler}` : `index.${handler}`, | ||
}); | ||
} | ||
|
||
// Enable connection reuse for aws-sdk | ||
if (props.awsSdkConnectionReuse ?? true) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import { spawnSync } from 'child_process'; | ||
import { Construct } from 'constructs'; | ||
import * as ecr from '../../aws-ecr'; | ||
import * as ecr_assets from '../../aws-ecr-assets'; | ||
|
@@ -54,6 +55,44 @@ export abstract class Code { | |
return new AssetCode(path, options); | ||
} | ||
|
||
/** | ||
* Runs a command to build the code asset that will be used. | ||
* | ||
* @param output Where the output of the command will be directed, either a directory or a .zip file with the output Lambda code bundle | ||
* * For example, if you use the command to run a build script (e.g., [ 'node', 'bundle_code.js' ]), and the build script generates a directory `/my/lambda/code` | ||
* containing code that should be ran in a Lambda function, then output should be set to `/my/lambda/code` | ||
* @param command The command which will be executed to generate the output, for example, [ 'node', 'bundle_code.js' ] | ||
* @param assetOptions The same options that are available for `Code.fromAsset` -- but bundling options are not allowed | ||
* @param commandOptions Options that are passed to the spawned process, which determine the characteristics of the spawned process. | ||
* * See `child_process.SpawnSyncOptions` for possible inputs and defaults (https://nodejs.org/api/child_process.html#child_processspawnsynccommand-args-options). | ||
*/ | ||
public static fromCustomCommand( | ||
output: string, | ||
command: string[], | ||
commandOptions?: {[option: string]: any}, // jsii build fails if the type is SpawnSyncOptions... so best we can do is point user to those options. | ||
assetOptions?: s3_assets.AssetOptions, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I generally prefer to use interfaces where we can so that if there is ever a chance that a new argument is introduced we can just add it to the interface. What do you think about creating a new interface that just extends export interface CustomCommandOptions extends AssetOptions {
readonly commands?: { [options: string]: any },
} Then the function definition could just be: public static fromCustomCommand(output: string, command: string[], options: CustomCommandOptions = {}) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with you, that's an improvement |
||
): AssetCode { | ||
if (command.length === 0) { | ||
throw new Error('command must contain at least one argument. For example, ["node", "buildFile.js"].'); | ||
} | ||
|
||
const cmd = command[0]; | ||
const commandArguments = command.splice(1); | ||
|
||
const proc = commandOptions === undefined | ||
? spawnSync(cmd, commandArguments) // use the default spawnSyncOptions | ||
: spawnSync(cmd, commandArguments, commandOptions); | ||
|
||
if (proc.error) { | ||
throw new Error(`Failed to execute custom command: ${proc.error}`); | ||
} | ||
if (proc.status !== 0) { | ||
throw new Error(`${command.join(' ')} exited with status: ${proc.status}\n\nstdout: ${proc.stdout?.toString().trim()}\n\nstderr: ${proc.stderr?.toString().trim()}`); | ||
} | ||
|
||
return new AssetCode(output, assetOptions); | ||
} | ||
|
||
/** | ||
* Loads the function code from an asset created by a Docker build. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I'll remove this in my final revision