Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ echo '{"message": "Hey, are you there?" }' | sam local invoke "Ratings"
sam local invoke --help
```

**Debug mode**

You can invoke your function locally in debug mode by providing a debug port. **`sam`** will start the function in a suspended state and expose the provided port on localhost. You can then connect to it using a remote debugger.

```bash
# Invoking function in debug mode on port 5005
sam local invoke "Ratings" -e event.json -d 5005
```

At the moment, this is only available for java and nodejs runtimes.

### Generate sample event source payloads

![SAM CLI Generate Event Sample](media/sam-generate-event.gif)
Expand Down Expand Up @@ -173,6 +184,18 @@ SampleAPI:
...
```

**Debug mode**

You can spawn a local API Gateway in debug mode by providing a debug port. As soon as you invoke a function by sending a request to the local API Gateway, **`sam`** will start the function in a suspended state and expose the provided port on localhost. You can then connect to it using a remote debugger.

```bash
# Invoking function in debug mode on port 5005
sam local start-api -d 5005
```

Note: The local API Gateway will expose all of your lambda functions but, since you can specify a single debug port, you can only debug one function at a time.

At the moment, this is only available for java and nodejs runtimes.

### Validate SAM templates

Expand Down
9 changes: 6 additions & 3 deletions invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,12 @@ func invoke(c *cli.Context) {
funcEnvVarsOverrides = map[string]string{}
}

basedir := filepath.Dir(c.String("template"))
runt, err := NewRuntime(basedir, function, funcEnvVarsOverrides)
runt, err := NewRuntime(NewRuntimeOpt{
Function: function,
EnvVarsOverrides: funcEnvVarsOverrides,
Basedir: filepath.Dir(c.String("template")),
DebugPort: c.String("debug-port"),
})
if err != nil {
log.Fatalf("Could not initiate %s runtime: %s\n", function.Runtime(), err)
}
Expand Down Expand Up @@ -161,5 +165,4 @@ func invoke(c *cli.Context) {

fmt.Fprintf(stderr, "\n")
runt.CleanUp()

}
15 changes: 14 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,20 @@ func main() {
Name: "env-vars, n",
Usage: "Optional. JSON file containing values for Lambda function's environment variables. ",
},
cli.StringFlag{
Name: "debug-port, d",
Usage: "Optional. When specified, Lambda function container will start in debug mode and will expose this port on localhost. "+
"At this moment, this only works for java8 and nodejs* runtimes.",
EnvVar: "SAM_DEBUG_PORT",
},
},
},
cli.Command{
Name: "invoke",
Action: invoke,
Usage: "Invokes a local Lambda function once and quits after invocation completes. \n\n" +
"Useful for developing serverless functions that handle asyncronous events (such as S3/Kinesis etc), or if you want to compose a script of test cases. Event body can be passed in either by stdin (default), or by using the --event parameter. Runtime output (logs etc) will be outputted to stderr, and the Lambda function result will be outputted to stdout.\n",
"Useful for developing serverless functions that handle asynchronous events (such as S3/Kinesis etc), or if you want to compose a script of test cases. " +
"Event body can be passed in either by stdin (default), or by using the --event parameter. Runtime output (logs etc) will be outputted to stderr, and the Lambda function result will be outputted to stdout.\n",
ArgsUsage: "<function-identifier>",
Flags: []cli.Flag{
cli.StringFlag{
Expand All @@ -94,6 +101,12 @@ func main() {
Name: "event, e",
Usage: "JSON file containing event data passed to the Lambda function during invoke",
},
cli.StringFlag{
Name: "debug-port, d",
Usage: "Optional. When specified, Lambda function container will start in debug mode and will expose this port on localhost. "+
"At this moment, this only works for java8 and nodejs* runtimes.",
EnvVar: "SAM_DEBUG_PORT",
},
},
},
cli.Command{
Expand Down
182 changes: 154 additions & 28 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ import (
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
"github.com/docker/go-connections/nat"
"github.com/fatih/color"
"github.com/imdario/mergo"
"github.com/mattn/go-colorable"
"github.com/pkg/errors"
"os/signal"
"syscall"
)

// Invoker is a simple interface to help with testing runtimes
Expand All @@ -45,6 +48,7 @@ type Runtime struct {
Cwd string
Function resources.AWSServerlessFunction
EnvVarOverrides map[string]string
DebugPort string
Context context.Context
Client *client.Client
TimeoutTimer *time.Timer
Expand All @@ -59,20 +63,43 @@ var (
ErrRuntimeNotSupported = errors.New("unsupported runtime")
)

var runtimes = map[string]string{
"nodejs": "lambci/lambda:nodejs",
"nodejs4.3": "lambci/lambda:nodejs4.3",
"nodejs6.10": "lambci/lambda:nodejs6.10",
"python2.7": "lambci/lambda:python2.7",
"python3.6": "lambci/lambda:python3.6",
"java8": "lambci/lambda:java8",
var runtimeName = struct {
nodejs string
nodejs43 string
nodejs610 string
python27 string
python36 string
java8 string
}{
nodejs: "nodejs",
nodejs43: "nodejs4.3",
nodejs610: "nodejs6.10",
python27: "python2.7",
python36: "python3.6",
java8: "java8",
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of having this as an anon struct (and map), rather than just the map that was there before?

Copy link
Author

@fmmirel fmmirel Aug 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Paul. Thank you for having a look!

The reason why I altered the previous setup is the fact that I need to use the runtime names in the switch I added in the "getDebugEntrypoint" method. Previously, the runtime names were just keys in a map so they could not be used as const values to compare against.


// NewRuntime instantiates a Lambda runtime container
func NewRuntime(basedir string, function resources.AWSServerlessFunction, envVarsOverrides map[string]string) (Invoker, error) {
var runtimeImageFor = map[string]string{
runtimeName.nodejs: "lambci/lambda:nodejs",
runtimeName.nodejs43: "lambci/lambda:nodejs4.3",
runtimeName.nodejs610: "lambci/lambda:nodejs6.10",
runtimeName.python27: "lambci/lambda:python2.7",
runtimeName.python36: "lambci/lambda:python3.6",
runtimeName.java8: "lambci/lambda:java8",
}

// NewRuntimeOpt contains parameters that are passed to the NewRuntime method
type NewRuntimeOpt struct {
Function resources.AWSServerlessFunction
EnvVarsOverrides map[string]string
Basedir string
DebugPort string
}

// NewRuntime instantiates a Lambda runtime container
func NewRuntime(opt NewRuntimeOpt) (Invoker, error) {
// Determine which docker image to use for the provided runtime
image, found := runtimes[function.Runtime()]
image, found := runtimeImageFor[opt.Function.Runtime()]
if !found {
return nil, ErrRuntimeNotSupported
}
Expand All @@ -82,17 +109,18 @@ func NewRuntime(basedir string, function resources.AWSServerlessFunction, envVar
return nil, err
}

cwd, err := getWorkingDir(basedir, function.CodeURI().String())
cwd, err := getWorkingDir(opt.Basedir, opt.Function.CodeURI().String())
if err != nil {
return nil, err
}

r := &Runtime{
Name: function.Runtime(),
Name: opt.Function.Runtime(),
Cwd: cwd,
Image: image,
Function: function,
EnvVarOverrides: envVarsOverrides,
Function: opt.Function,
EnvVarOverrides: opt.EnvVarsOverrides,
DebugPort: opt.DebugPort,
Context: context.Background(),
Client: cli,
}
Expand All @@ -107,7 +135,7 @@ func NewRuntime(basedir string, function resources.AWSServerlessFunction, envVar
return nil, err
}

log.Printf("Fetching %s image for %s runtime...\n", r.Image, function.Runtime())
log.Printf("Fetching %s image for %s runtime...\n", r.Image, opt.Function.Runtime())
progress, err := cli.ImagePull(r.Context, r.Image, types.ImagePullOptions{})
if len(images) < 0 && err != nil {
log.Fatalf("Could not fetch %s Docker image\n%s", r.Image, err)
Expand Down Expand Up @@ -176,6 +204,7 @@ func (r *Runtime) getHostConfig() (*container.HostConfig, error) {
Binds: []string{
fmt.Sprintf("%s:/var/task:ro", r.Cwd),
},
PortBindings: r.getDebugPortBindings(),
}
if err := overrideHostConfig(host); err != nil {
log.Print(err)
Expand All @@ -184,9 +213,9 @@ func (r *Runtime) getHostConfig() (*container.HostConfig, error) {
return host, nil
}

// Invoke runs a Lambda function within the runtime with the provided event payload
// and returns a pair of io.Readers for it's stdout (callback results) and
// stderr (runtime logs).
// Invoke runs a Lambda function within the runtime with the provided event
// payload and returns a pair of io.Readers for it's stdout (callback results)
// and stderr (runtime logs).
func (r *Runtime) Invoke(event string) (io.Reader, io.Reader, error) {

log.Printf("Invoking %s (%s)\n", r.Function.Handler(), r.Name)
Expand All @@ -195,10 +224,12 @@ func (r *Runtime) Invoke(event string) (io.Reader, io.Reader, error) {

// Define the container options
config := &container.Config{
WorkingDir: "/var/task",
Image: r.Image,
Tty: false,
Cmd: []string{r.Function.Handler(), event},
WorkingDir: "/var/task",
Image: r.Image,
Tty: false,
ExposedPorts: r.getDebugExposedPorts(),
Entrypoint: r.getDebugEntrypoint(),
Cmd: []string{r.Function.Handler(), event},
Env: func() []string {
result := []string{}
for k, v := range env {
Expand Down Expand Up @@ -244,6 +275,17 @@ func (r *Runtime) Invoke(event string) (io.Reader, io.Reader, error) {
return nil, nil, err
}

if len(r.DebugPort) == 0 {
r.setupTimeoutTimer(stdout, stderr)
} else {
r.setupInterruptHandler(stdout, stderr)
}

return stdout, stderr, nil

}

func (r *Runtime) setupTimeoutTimer(stdout, stderr io.ReadCloser) {
// Start a timer, we'll use this to abort the function if it runs beyond the specified timeout
timeout := time.Duration(3) * time.Second
if r.Function.Timeout() > 0 {
Expand All @@ -257,13 +299,22 @@ func (r *Runtime) Invoke(event string) (io.Reader, io.Reader, error) {
stdout.Close()
r.CleanUp()
}()
}

return stdout, stderr, nil

func (r *Runtime) setupInterruptHandler(stdout, stderr io.ReadCloser) {
iChan := make(chan os.Signal, 2)
signal.Notify(iChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-iChan
log.Printf("Execution of function %q was interrupted", r.Function.Handler())
stderr.Close()
stdout.Close()
r.CleanUp()
os.Exit(0)
}()
}

func getSessionOrDefaultCreds() map[string]string {

region := "us-east-1"
key := "defaultkey"
secret := "defaultsecret"
Expand All @@ -289,7 +340,6 @@ func getSessionOrDefaultCreds() map[string]string {
}

func getOsEnviron() map[string]string {

result := map[string]string{}
for _, value := range os.Environ() {
keyVal := strings.Split(value, "=")
Expand All @@ -299,6 +349,80 @@ func getOsEnviron() map[string]string {
return result
}

func (r *Runtime) getDebugPortBindings() nat.PortMap {
if len(r.DebugPort) == 0 {
return nil
}
return nat.PortMap{
nat.Port(r.DebugPort): []nat.PortBinding{{HostPort: r.DebugPort}},
}
}

func (r *Runtime) getDebugExposedPorts() nat.PortSet {
if len(r.DebugPort) == 0 {
return nil
}
return nat.PortSet{nat.Port(r.DebugPort): {}}
}

func (r *Runtime) getDebugEntrypoint() (overrides []string) {
if len(r.DebugPort) == 0 {
return
}
switch r.Name {
// configs from: https://github.com/lambci/docker-lambda
// to which we add the extra debug mode options
case runtimeName.java8:
overrides = []string{
"/usr/bin/java",
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=" + r.DebugPort,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where do these options come from? Can you link to a reference?

Copy link
Author

@fmmirel fmmirel Aug 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, how about the other values like heapsize, GC etc?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought you meant the debug options. There's a comment about where I got these values immediately after the switch statement:

If you go to that repository you can inspect all of the default lambda container Dockerfiles. For example: https://github.com/lambci/docker-lambda/blob/master/nodejs/run/Dockerfile

"-XX:MaxHeapSize=1336935k",
"-XX:MaxMetaspaceSize=157286k",
"-XX:ReservedCodeCacheSize=78643k",
"-XX:+UseSerialGC",
//"-Xshare:on", doesn't work in conjunction with the debug options
"-XX:-TieredCompilation",
"-jar",
"/var/runtime/lib/LambdaJavaRTEntry-1.0.jar",
}
case runtimeName.nodejs:
overrides = []string{
"/usr/bin/node",
"--debug-brk=" + r.DebugPort,
"--nolazy",
"--max-old-space-size=1229",
"--max-new-space-size=153",
"--max-executable-size=153",
"--expose-gc",
"/var/runtime/node_modules/awslambda/bin/awslambda",
}
case runtimeName.nodejs43:
overrides = []string{
"/usr/local/lib64/node-v4.3.x/bin/node",
"--debug-brk=" + r.DebugPort,
"--nolazy",
"--max-old-space-size=1229",
"--max-semi-space-size=76",
"--max-executable-size=153",
"--expose-gc",
"/var/runtime/node_modules/awslambda/index.js",
}
case runtimeName.nodejs610:
overrides = []string{
"/var/lang/bin/node",
"--debug-brk=" + r.DebugPort,
"--nolazy",
"--max-old-space-size=1229",
"--max-semi-space-size=76",
"--max-executable-size=153",
"--expose-gc",
"/var/runtime/node_modules/awslambda/index.js",
}
// TODO: also add debug mode for Python runtimes
}
return
}

/**
The Environment Variable Saga..

Expand Down Expand Up @@ -386,7 +510,9 @@ func toStringMaybe(value interface{}) (string, bool) {

// CleanUp removes the Docker container used by this runtime
func (r *Runtime) CleanUp() {
r.TimeoutTimer.Stop()
if r.TimeoutTimer != nil {
r.TimeoutTimer.Stop()
}
r.Client.ContainerKill(r.Context, r.ID, "SIGKILL")
r.Client.ContainerRemove(r.Context, r.ID, types.ContainerRemoveOptions{})
}
Expand Down Expand Up @@ -434,7 +560,7 @@ func getDockerVersion() (string, error) {

func getWorkingDir(basedir string, codeuri string) (string, error) {

// Determin which directory to mount into the runtime container.
// Determine which directory to mount into the runtime container.
// If no CodeUri is specified for this function, then use the same
// directory as the SAM template (basedir), otherwise mount the
// directory specified in the CodeUri property.
Expand Down
Loading