serverfull

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 29, 2023 License: Apache-2.0 Imports: 14 Imported by: 10

README

Serverfull - A Lambda Simulator For Go

GoDoc Build Status codecov.io

Overview

This projects is a toolkit for leveraging Lambda functions outside of the usual serverless offering in AWS. Bundled with the project is an HTTP server that implements the AWS Lambda Invoke API and is able to serve any function that is compatible with the lambda go sdk.

Generally speaking, if you want to use AWS Lambda then you should probably use AWS Lambda rather this simulator. This project is specialized and was conceived to enable:

  • Teams who want to adopt a serverless style of development but without full access to a serverless/FaaS provider such as those who are developing on bare metal or private cloud infrastructures.

  • Teams who are ready to migrate away from AWS Lambda but aren't ready to rewrite large portions of existing code such as those who are either changing cloud providers or moving to EC2 in order to fine-tune the performance characteristics of their runtimes.

If you're looking for a local development tool that supports AWS Lambda then you would likely be better served by using tools like docker-lambda and AWS SAM Local.

Quick Start

Start by defining a normal lambda function. For example, here is one from the AWS Go SDK project:

package main

// hello is lifted straight from the aws-lambda-go README.md file.
// This is can be called like:
//
//      curl --request POST localhost:8080/2015-03-31/functions/hello/invocations
func hello() (string, error) {
    return "Hello ƛ!", nil
}

Then attach that function to map and generate an instance of Serverfull:

package main

import (
    "context"
    "fmt"

    "github.com/asecurityteam/serverfull"
    "github.com/asecurityteam/settings"
)

func hello() (string, error) {
    return "Hello ƛ!", nil
}

func main() {
    ctx := context.Background()
    functions := map[string]serverfull.Function{
            // The keys of this map represent the function name and will be
            // accessed using the URL parameter of the Invoke API call.
            // These names are arbitrary and user defined. They do not need
            // to match the name of the code function. Think of these map keys
            // as being the ARN of a lambda.
            "hello":    serverfull.NewFunction(hello),
            // Note that we use the AWS Lambda SDK for Go here to wrap the function.
            // This is because the serverfull functions are _actual_ Lambda functions
            // and are 100% compatible with AWS Lambda.
    }
    // Create any implementation of the settings.Source interface. Here we
    // use the environment variable source.
    source, err := settings.NewEnvSource(os.Environ())
    if err != nil {
        panic(err.Error())
    }
    // Wrap the map in a static function loader. Other loading options are
    // discussed later in the docs.
    fetcher := &serverfull.StaticFetcher{Functions:functions}
    // Start the runtime.
    if err := serverfull.StartHTTP(ctx, source, fetcher); err != nil {
        panic(err.Error())
    }
}

If you run this code then you can make a request to invoke the hello function and see the result.

curl --request POST localhost:8080/2015-03-31/functions/hello/invocations

Alternatively, the AWS CLI may be used as well:

aws lambda invoke \
    --endpoint-url="http://localhost:8080" \
    --function-name="hello" \
    output.txt && \
    cat output.txt && \
    rm output.txt

Features And Limitations

HTTP API Options

The X-Amz-Invocation-Type header can be used, as described in the actual AWS Lambda Invoke API, to alter the execution behavior. The only aspects of the API that are not simulated yet are:

  • The "Tail" option for the LogType header does not cause the response to include partial logs.

  • The "Qualifier" parameter is currently ignored and the reported execution version is always "latest".

  • The "Function-Error" header is always "Unhandled" in the event of an exception.

The API is compatible enough with AWS Lambda that the AWS CLI, as well as all AWS SDKs that support Lambda features, can be used after adjusting the endpoint value.

Function Loaders

The project currently only supports using a static mapping of functions. A future feature we are considering is the addition of the CreateFunction and UpdateFunction API endpoints that would leverage S3 for persistence and loading. This would enable teams who want to continue using the AWS CLI for managing deployments to do so.

Running In Mock Mode

Mock mode inspects the signatures of each function being served and runs a version that only returns an empty version of the output and nil as the error. Systems that want to leverage mock mode will need to do something like this:

if mockMode {
    // Start the mock runtime.
    if err := serverfull.StartHTTPMock(ctx, source, fetcher); err != nil {
        panic(err.Error())
    }
    return
}
// Start the real runtime.
if err := serverfull.StartHTTP(ctx, source, fetcher); err != nil {
    panic(err.Error())
}

Building Lambda Binaries

In the same manner that you can enable mock mode you can also enable a native lambda mode. The lambda mode causes the runtime to bypass the HTTP server that implements the Invoke API and runs lambda.Start() on a target function instead. This is provided for teams who want to build binaries that are compatible with AWS Lambda. To do this you would need to have something like:

if lambdaMode {
    // Start the lambda mode runtime.
    if err := serverfull.StartLambda(ctx, source, fetcher, "myFunctionName"); err != nil {
        panic(err.Error())
    }
    return
}
// Start the serverfull mode runtime.
if err := serverfull.StartHTTP(ctx, source, fetcher); err != nil {
    panic(err.Error())
}

The native lambda build can only target a single function at a time so you must specify the target function name. The resulting binary, when executed, will use whatever loading strategy you've defined to fetch a function matching the name defined as TargetFunction and pass it to the AWS lambda SDK lambda.Start() method. This recreates exactly what a "normal" lambda build would do except that there is a loading step. To keep consistent with lambda expectations we recommend only using the static loader for this process because the loading will be done at startup time.

The lambda build mode also supports running the function in mock mode by using StartLambdaMock.

Configuration

This project uses settings for managing configuration values and runhttp to manage the runtime behaviors such as logs, metrics, and shutdown signaling when running or building in HTTP mode. All of the configuration described in the runhttp README is valid with the notable exception that this project adds a serverfull path prefix to all lookups. This means where runhttp will have a RUNTIME_LOGGING_LEVEL variable then this project will have a SERVERFULL_RUNTIME_LOGGING_LEVEL variable.

For more advanced changes we recommend you use the NewRouter and Start methods as examples of how the system is composed. To add features such as authentication, additional metrics, retries, or other features suitable as middleware we recommend you use a project like transportd or a more in-depth "service-mesh" type of proxy rather than modifying this project directly.

Status

This project is in incubation which means we are not yet operating this tool in production and the interfaces are subject to change.

Planned/Proposed Features

  • Replication of AWS CloudWatch metrics for lambda when running in HTTP mode.
  • Automated injection of the log and stat clients into the context when running in lambda mode.
  • Ability to trigger errors in mock mode.
  • Ability to provide static or random values for mock outputs instead of only zero values.

Contributing

Building And Testing

We publish a docker image called SDCLI that bundles all of our build dependencies. It is used by the included Makefile to help make building and testing a bit easier. The following actions are available through the Makefile:

  • make dep

    Install the project dependencies into a vendor directory

  • make lint

    Run our static analysis suite

  • make test

    Run unit tests and generate a coverage artifact

  • make integration

    Run integration tests and generate a coverage artifact

  • make coverage

    Report the combined coverage for unit and integration tests

License

This project is licensed under Apache 2.0. See LICENSE.txt for details.

Contributing Agreement

Atlassian requires signing a contributor's agreement before we can accept a patch. If you are an individual you can fill out the individual CLA. If you are contributing on behalf of your company then please fill out the corporate CLA.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var LambdaStartFn = lambda.StartHandler

LambdaStartFn is a reference to lambda.StartHandler that is exported for cases where a custom net/rpc server needs to run rather than the true native lambda server. For example, this project leverages this feature in our integration tests where we add some additional signal handling for testing purposes.

View Source
var LoggerFromContext = logevent.FromContext

LoggerFromContext extracts the current logger.

View Source
var StatFromContext = xstats.FromContext

StatFromContext extracts the current stat client.

Functions

func NewRouter

func NewRouter(conf *RouterConfig) *chi.Mux

NewRouter generates a mux that already has AWS Lambda API routes bound. This version returns a mux from the chi project as a convenience for cases where custom middleware or additional routes need to be configured.

func Start

func Start(ctx context.Context, s settings.Source, f Fetcher) error

Start is maintained for backwards compatibility but is deprecated. The original intent was a single entry point that would switch behaviors based on a build flag. However, build flags are not persisted after the build so the build-time selection is not actually available for switching at runtime. As a result, this function now only starts the HTTP server.

func StartHTTP

func StartHTTP(ctx context.Context, s settings.Source, f Fetcher) error

StartHTTP runs the HTTP API.

func StartHTTPMock

func StartHTTPMock(ctx context.Context, s settings.Source, f Fetcher) error

StartHTTPMock runs the HTTP API with mocked out functions.

func StartLambda

func StartLambda(ctx context.Context, s settings.Source, f Fetcher, target string) error

StartLambda runs the target function from the fetcher as a native lambda server.

func StartLambdaMock

func StartLambdaMock(ctx context.Context, s settings.Source, f Fetcher, target string) error

StartLambdaMock starts the native lambda server with a mocked out function.

Types

type Fetcher

type Fetcher interface {
	// Fetch uses some implementation of a loading strategy
	// to fetch the Handler with the given name. If a matching Handler
	// cannot be found then this component must emit a NotFoundError.
	Fetch(ctx context.Context, name string) (Function, error)
}

Fetcher is a pluggable component that enables different loading strategies functions.

type Function

type Function interface {
	lambda.Handler
	Source() interface{}
	Errors() []error
}

Function is an executable lambda function. This extends the official lambda SDK concept of a Handler in order to also provide the underlying function signature which is usually masked when converting any function to a lambda.Handler.

func NewFunction

func NewFunction(v interface{}) Function

NewFunction is a replacement for lambda.NewHandler that returns a Function.

func NewFunctionWithErrors added in v0.2.0

func NewFunctionWithErrors(v interface{}, errors ...error) Function

NewFunctionWithErrors allows for documenting the various error types that can be returned by the function. This may be used when running in mock + http build modes to trigger exceptions.

type Invoke

type Invoke struct {
	LogFn      LogFn
	StatFn     StatFn
	URLParamFn URLParamFn
	Fetcher    Fetcher
	MockMode   bool
}

Invoke implements the API of the same name from the AWS Lambda API. https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html

While the intent is to make this endpoint as similar to the Invoke API as possible, there are several features that are not yet supported:

  • The "Tail" option for the LogType header does not cause the response to include partial logs.
  • The "Qualifier" parameter is currently ignored and the reported execution version is always "latest".
  • The "Function-Error" header is always "Unhandled" in the event of an exception.

This implementation also provides one extra feature which is that sending an X-Amz-Invocation-Type header with the value "Error" and an X-Error-Type header with the value of the corresponding "errType" value of a Lambda error response will trigger that err while in mock mode. Not that this is only enabled while running in mock mode and will only work if the given error type is actually in the set of known error values returned by the function. The only known error types are the ones provided when constructing the function using NewFunctionWithErrors. A 404 is issued if the requested error is not available.

func (*Invoke) ServeHTTP

func (h *Invoke) ServeHTTP(w http.ResponseWriter, r *http.Request)

type LambdaFunction

type LambdaFunction struct {
	lambda.Handler
	// contains filtered or unexported fields
}

LambdaFunction is a small wrapper around the lambda.Handler that preserves the original signature of the function for later retrieval.

func (*LambdaFunction) Errors added in v0.2.0

func (f *LambdaFunction) Errors() []error

Errors returns a list of errors the Lambda might return. This is only populated if the function was constructed using the NewFunctionWithErrors constructor.

func (*LambdaFunction) Source

func (f *LambdaFunction) Source() interface{}

Source returns the original function signature.

type LogFn

type LogFn = func(context.Context) Logger

LogFn extracts a logger from the context.

type Logger

type Logger = logevent.Logger

Logger is an alias for the chosen project logging library which is, currently, logevent. All references in the project should be to this name rather than logevent directly.

type MockingFetcher

type MockingFetcher struct {
	Fetcher Fetcher
}

MockingFetcher sources original functions from another Fetcher and mocks out the results.

func (*MockingFetcher) Fetch

func (f *MockingFetcher) Fetch(ctx context.Context, name string) (Function, error)

Fetch calls the underlying Fetcher and mocks the results.

type NotFoundError

type NotFoundError struct {
	// ID is the key used when looking for the resource.
	ID string
}

NotFoundError represents a failed lookup for a resource.

func (NotFoundError) Error

func (e NotFoundError) Error() string

type RouterConfig

type RouterConfig struct {
	// HealthCheck defines the route on which the service will respond
	// with automatic 200s. This is here to integrate with systems that
	// poll for liveliness. The default value is /healthcheck
	HealthCheck string

	// Fetcher is the Lambda function loader that will
	// be used by the runtime. There is no default for this value.
	Fetcher Fetcher

	// LogFn is used to extract the request logger from the request
	// context. The default value is logevent.FromContext.
	LogFn LogFn
	// StatFn is used to extract the request stat client from the
	// request context. The default value is xstats.FromContext.
	StatFn StatFn
	// URLParamFn is used to extract URL parameters from the request.
	// The default value is chi.URLParam to match the usage of chi
	// as a mux in the default case.
	URLParamFn URLParamFn
	// MockMode should be set to enable mock mode features like error simulation.
	MockMode bool
}

RouterConfig is used to alter the behavior of the default router and the HTTP endpoint handlers that it manages.

type Stat

type Stat = xstats.XStater

Stat is an alias for the chosen project metrics library which is, currently, xstats. All references in the project should be to this name rather than xstats directly.

type StatFn

type StatFn = func(context.Context) Stat

StatFn extracts a metrics client from the context.

type StaticFetcher

type StaticFetcher struct {
	// Functions is the underlying static map of function names to executable
	// functions. The keys of the map will be used as the name of the Function.
	Functions map[string]Function
}

StaticFetcher is an implementation of the Fetcher that maintains a static mapping of names to Function instances. This implementation is a highly simplified form for the purposes of reducing risk in operations. Notably, runtimes that leverage this implementation do not need to perform any orchestration of external systems as all invocations of the Functions happen within the process and share the runtime's resources. Additionally, there is no "live update" feature which means there are less moving parts that might fail when attempting to start or update a Function.

The trade-off is that updates to, additions of, and removals of Functions must be accomplished by generating a new build and redeploying the runtime. There are no options for updating or adding in-place. Operators and developers who choose this feature must take care that redeployments of the system do not cause downtime as all Functions will be affected together.

func (*StaticFetcher) Fetch

func (f *StaticFetcher) Fetch(ctx context.Context, name string) (Function, error)

Fetch resolves the name using the internal mapping.

type URLParamFn

type URLParamFn func(ctx context.Context, name string) string

URLParamFn should be accepted by HTTP handlers that need to interface with the mux in use in order to extract request parameters from the URL. This defines the contract between any given mux and a handler so that the two do not need to be coupled.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL