designs/sam_build_cmd.md
sam build commandThis is the design for a command to build a Lambda function. Build is the operation of converting the function's source code to an artifact that can be executed on AWS Lambda.
To run a function on AWS Lambda, customers need to provide a zip file containing an executable form of the code. The process of creating the executable usually involves downloading dependent libraries, optionally compiling the code on Amazon Linux, copying static assets, and arranging the files in a directory structure that AWS Lambda accepts. In some programming languages like Javascript, this process is fairly straightforward (just zip a folder), and in others like Python, it is much involved.
Customers using SAM CLI can easily create Lambda function code (using
sam init) and package their artifact as a zip file (using
sam package), but they have to handle the build process themselves.
Customers usually implement their own build scripts or adopt other's
scripts from the internet. This is where many customers fall off the
cliff.
In this proposal, we will be providing a new command, sam build, to
build Lambda functions for all programming languages that AWS Lambda
supports. The cardinality of the problem is number of programming
languages (N) times the number of package managers (M) per language.
Hence is it is nearly impossible to natively support each combination.
Instead, sam build will support an opinionated set of package managers
for all programming languages. In the future, we will provide an option
for customers to bring their own build commands or override the default
for any programming language. SAM CLI will still take care of the grunt
work of iterating through every function in the SAM template, figuring
out the source code location, creating temporary folders to store built
artifacts, running the build command and move artifacts to right
location.
function_identifier (LogicalID) is passed
to the build commandsam local and
sam package suite of commandssam local/package/deploy commands so
the Lambda functions will be automatically built as part of the
command without explicitly running the build command.buildspec.yaml, automatically run it using
CodeBuild Local.sam build --watch)aws cloudformation, suite of commandsLet's assume customers has the following SAM template:
NOTE: Currently we advice customers to set CodeUri to a folder containing built artifacts that can be readily packaged by
sam packagecommand. But to use with build command, customers need to set CodeUri to the folder containing source code and not built artifacts.
MyFunction1:
Type: AWS::Lambda::Function
Properties:
...
Code: ./source-code1
...
MyFunction2:
Type: AWS::Serverless::Function
Properties:
...
Code: ./source-code2
...
To build, package and deploy this app, customers would do the following:
1. Build: Run the following command to build all functions in the template and output a SAM template that can be run through the package command:
# Build the code and write artifacts to ./build folder
# NOTE: All arguments will have sensible defaults so users can just use `sam build`
$ sam build -t template.yaml -b ./build -o built-template.yaml
Output of the sam build command is a SAM template where CodeUri is pointing to the built artifacts. Note the values of Code properties in following output:
$ cat built-template.yaml
MyFunction1:
Type: AWS::Lambda::Function
Properties:
...
Code: ./build/MyFunction1
...
MyFunction2:
Type: AWS::Serverless::Function
Properties:
...
CodeUri: ./build/MyFunction2
...
2. Package and Deploy: Package the built artifacts by running the package command on the template output by build command
# Package the code
$ sam package --template-file built-template.yaml --s3-bucket mybucket --output-template-file packaged-template.yaml
# Deploy the app
$ sam deploy --template-file packaged-template.yaml --stack-name mystack
--native flag to the
build command. This will run the build inside a Docker container.--root=/my/folder flag to absolute path to the
folder relative to which we will resolve relative CodeUri paths.--root flag is set, we will look for manifest at
this folder. If neither locations have a manifest, we will look for
a manifest within the folder containing function code. Manifest
present within the code folder always overrides manifest at the
root..samrc.--template property of
sam local suite of commands to specify the template produced by
build command (ex: build-template.yaml)Explain the changes to command line interface, including adding new commands, modifying arguments etc
sam build.built-template.yaml to list of default template names searched
by sam local commandsAre there any breaking changes to CLI interface? Explain
No Breaking Change to CLI interface
Explain how this feature will be implemented. Highlight the components of your implementation, relationships between components, constraints, etc.
Build library provides the ability to execute build actions on each registered resource. A build action is either a built-in functionality or a custom build command provided by user. At a high level, the algorithm looks like this:
for resource in sam_template:
# Find the appropriate builder
builder = get_builder(resource.Type)
# Do the build
output_folder = make_build_folder(resource)
builder.build(resource.Code, resource.runtime, output_folder)
We will keep the implementation of build agnostic of the resource type.
This opens up the future possibility of adding build actions to any
resource types, not just Lambda functions. Initially we will start by
supporting only the resource types AWS::Serverless::Function and
AWS::Lambda::Function.
Default Location: $PKG_ROOT/build/
By default, we will create a folder called build right next to where
the SAM template is located. This will contain the built artifacts for
each resource. Customers can always override this folder location.
Built artifacts for each resource will be stored within a folder named with the LogicalID of the resource. This allows us to build separate zip files for each Lambda, so users can update one Lambda without triggering an update on another. The same model will work for building other non-Lambda resources.
Advantages:
Disadvantages:
$PKG_ROOT/build/
artifacts.json (not for MVP)
MyFunction1/
.... <build artifacts>
MyFunction2/
... <build artifacts>
MyApiGw/
... <build artifacts>
MyECRContainer/
... <build artifacts>
In the future, we will change the limitation around each folder being named after the resource's LogicalID. Instead, we will support an artifacts.json file that will map Lambda function resource's LogicalId to the path to a folder that contains built artifacts for this function. This allows us to support custom build systems that use different folder layout.
A well-known folder structure also helps "sam local" and "sam package" commands to automatically discover the built artifacts for each Lambda function and package it.
A build is defined to be stable if the built artifacts changes if and
only if the contents or metadata (ex: timestamp, ownership) on source
files changes. This is an important attribute of a build system. Since
SAM CLI relies on 3rd party package managers like NPM to do the heavy
lifting, we can only provide a "best effort" service here. By running
sam build on a build system that creates a new environment from
scratch (ex: Travis/CircleCI/CodeBuild/Jenkins etc), you can achieve
truly stable builds. For more information on why this is important,
refer to Debian's guide on reproducible
builds.
SAM CLI does the following to produce stable builds:
Build actions natively supported by SAM CLI follow a standard workflow:
Setup step is shared among all runtimes. Other steps in the workflow are implemented differently for each runtime.
Install dependencies specified by package.json and copy source files
Manifest Name: package.json
Files Excluded From Copy Source: node_modules/*
| Action | Command |
|---|---|
| Resolve | npm install |
| Compile | No Op |
| Copy Source | Copy files and exclude node_modules |
Let Maven take care of everything
Manifest Name: pom.xml
Files Excluded From Copy Source: N/A
| Action | Command |
|---|---|
| Resolve | No Op |
| Compile | mvn package |
| Copy Source | No Op |
Go's CLI will build the binary.
Manifest Name: Gopkg.toml
Files Excluded From Copy Source: N/A
| Action | Command |
|---|---|
| Resolve | dep ensure -v |
| Compile | GOOS=linux go build -ldflags="-s -w" main.go |
| Copy Source | No Op |
Manifest Name: *.csproj
Files Excluded From Copy Source: N/A
| Action | Command |
|---|---|
| Resolve | No Op |
| Compile | dotnet lambda package --configuration release --output-package $BUILD/package.zip |
| Copy Source | No Op |
Manifest Name: requirements.txt
Files Excluded From Copy Source: *.pyc, __pycache__
| Action | Command |
|---|---|
| Resolve | pip install --isolated --disable-pip-version-check -r requirements.txt -t $BUILD |
| Compile | No Op |
| Copy Source | Copy all files |
Some of the built-in build actions are implemented in the programming
language that the actions supports. For example, the Nodejs build action
will be implemented in Javascript to take advantage of language-specific
libraries. These modules are called builders. This is a reasonable
implementation choice because customers building Nodejs apps are
expected to have Node installed on their system. For languages like
Golang, we will delegate entire functionality to go tool by invoking
it as a subprocess. The SAM CLI distribution will now bundle Javascript
code within a Python package, which even though seems odd, carries
value.
Pros:
Cons:
In this implementation model, some steps in the build action are implemented natively in Python and some in a separate programming language. To complete a build operation, SAM CLI reads SAM template, prepares necessary folder structure, and invokes the appropriate builder process/command by passing necessary information through stdin as JSON-RPC. SAM CLI waits for a JSON-RPC response back through stdout of the process and depending on the status, either fails the build or proceeds to next step.
Input:
{
"jsonrpc": "2.0",
"id": "42",
// Only supported method is `resolve-dependencies`
"method": "resolve-dependencies",
"params": {
"source_dir": "/folder/where/source/files/located",
"build_dir": "/directory/for/builder/artifacts",
"runtime": "aws lambda function runtime ex. node8.10",
"template_path": "/path/to/sam/template"
}
}
Output:
{
"jsonrpc": "2.0",
"id": "42",
"result": {
// No result expected for successful execution
}
}
To build native binaries, we need to run on an architecture and operating system that is similar to AWS Lambda. We use the Docker containers provided by Docker Lambda project to run the same set of commands described above on this container. We will mount source code folder and build folder into the container so the commands have access to necessary files.
.samrc Changes (Out-of-Scope)Explain the new configuration entries, if any, you want to add to .samrc
We will add a new section to .samrc where customers can provide custom
build actions. This section will look like:
{
"build": {
"actions": {
"java8": "gradle build",
"dotnet6": "./build.sh"
}
}
}
Tip: How does this change impact security? Answer the following questions to help answer this question better:
What new dependencies (libraries/cli) does this change require?
What other Docker container images are you using?
Are you creating a new HTTP endpoint? If so explain how it will be created & used
Are you connecting to a remote API? If so explain how is this connection secured
Are you reading/writing to a temporary folder? If so, what is this used for and when do you clean up?
How do you validate new .samrc configuration?
TBD
artifacts.json now to be future-proof? Answer:
NObuild folder within a .sam folder
inside the project to provide a home for other scratch files if
necessary? Answer: Out of Scope for current implementationbuilt-template.yaml to list of default template names
searched by sam local commandssam init templates to include sam build in the
README