Coder Social home page Coder Social logo

lambda's Introduction

AWS Lambda for the XP Framework

Build status on GitHub XP Framework Module BSD Licence Requires PHP 7.0+ Supports PHP 8.0+ Latest Stable Version

Serverless infrastructure.

Example

Put this code in a file called Greet.class.php:

use com\amazon\aws\lambda\Handler;

class Greet extends Handler {

  /** @return callable|com.amazon.aws.lambda.Lambda|com.amazon.aws.lambda.Streaming */
  public function target() {
    return fn($event, $context) => sprintf(
      'Hello %s from PHP %s via %s @ %s',
      $event['name'],
      PHP_VERSION,
      $context->functionName,
      $context->region
    );
  }
}

The two parameters passed are $event (a value depending on where the lambda was invoked from) and $context (a Context instance, see below).

Initialization

If you need to run any initialization code, you can do so before returning the lambda from target(). This code is only run once during the init phase:

use com\amazon\aws\lambda\Handler;

class Greet extends Handler {

  /** @return callable|com.amazon.aws.lambda.Lambda|com.amazon.aws.lambda.Streaming */
  public function target() {
    $default= $this->environment->properties('task')->readString('greet', 'default');

    return fn($event, $context) => sprintf(
      'Hello %s from PHP %s via %s @ %s',
      $event['name'] ?? $default,
      PHP_VERSION,
      $context->functionName,
      $context->region
    );
  }
}

The lambda's environment accessible via $this->environment is an Environment instance, see below.

Logging

To write output to the lambda's log stream, use trace():

use com\amazon\aws\lambda\Handler;

class Greet extends Handler {

  /** @return callable|com.amazon.aws.lambda.Lambda|com.amazon.aws.lambda.Streaming */
  public function target() {
    return function($event, $context) {
      $this->environment->trace('Invoked with ', $event);

      return sprintf(/* Shortened for brevity */);
    };
  }
}

Any non-string arguments passed will be converted to string using util.Objects::stringOf(). To integrate with XP logging, pass the environment's writer to the console appender, e.g. by using $cat= Logging::all()->toConsole($this->environment->writer).

Response streaming

This library supports AWS Lambda response streaming as announced by AWS in April 2023. To use the stream, return a function(var, Stream, Context) from the handler's target() method instead of a function(var, Context):

use com\amazon\aws\lambda\{Context, Handler, Stream};

class Streamed extends Handler {

  public function target(): callable {
    return function($event, Stream $stream, Context $context) {
      $stream->use('text/plain');
      $stream->write("[".date('r')."] Hello world...\n");

      sleep(1);

      $stream->write("[".date('r')."] ...from Lambda\n");
      $stream->end();
    };
  }
}

Invoking this lambda will yield the following:

Streaming in Terminal

The Stream interface is defined as follows:

public interface com.amazon.aws.lambda.Stream extends io.streams.OutputStream, lang.Closeable {
  public function transmit(io.Channel|io.streams.InputStream $source, string $mimeType): void
  public function use(string $mimeType): void
  public function write(string $bytes): void
  public function end(): void
  public function flush(): void
  public function close(): var
}

Development

To run your lambda locally, use the following:

$ xp lambda run Greet '{"name":"Timm"}'
Hello Timm from PHP 8.2.11 via Greet @ test-local-1

This does not provide a complete lambda environment, and does not have any execution limits imposed on it! To detect this programmatically, use $this->environment->local(), which will return true.

Integration testing

To test your lambda inside a local containerized lambda environment, use the test command.

$ xp lambda test Greet '{"name":"Timm"}'
START RequestId: 9ff45cda-df9b-1b8c-c21b-5fe27c8f2d24 Version: $LATEST
END RequestId: 9ff45cda-df9b-1b8c-c21b-5fe27c8f2d24
REPORT RequestId: 9ff45cda-df9b-1b8c-c21b-5fe27c8f2d24  Init Duration: 922.19 ms...
"Hello Timm from PHP 8.2.11 via test @ us-east-1"

This functionality is provided by the AWS Lambda base images for custom runtimes. Although this also runs on your machine, $this->environment->local() will return false.

Setup

The first step is to create and publish the runtime layer:

$ xp lambda runtime
$ aws lambda publish-layer-version \
  --layer-name lambda-xp-runtime \
  --zip-file fileb://./runtime-X.X.X.zip \
  --region us-east-1

...and create a role:

$ cat > /tmp/trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
$ aws iam create-role \
  --role-name InvokeLambda \
  --path "/service-role/" \
  --assume-role-policy-document file:///tmp/trust-policy.json

After ensuring your dependencies are up-to-date using composer, create the function:

$ xp lambda package Greet.class.php
$ aws lambda create-function \
  --function-name greet \
  --handler Greet \
  --zip-file fileb://./function.zip \
  --runtime provided.al2 \
  --role "arn:aws:iam::XXXXXXXXXXXX:role/service-role/InvokeLambda" \
  --region us-east-1 \
  --layers "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:layer:lambda-xp-runtime:1"

Invocation

To invoke the function:

$ aws lambda invoke \
  --cli-binary-format raw-in-base64-out \
  --function-name greet \
  --payload '{"name":"Timm"}'
  response.json
$ cat response.json
"Hello Timm from PHP 8.0.10 via greet @ us-east-1"

Deploying changes

After having initially created your lambda, you can update its code as follows:

$ xp lambda package Greet.class.php
$ aws lambda update-function-code \
  --function-name greet \
  --zip-file fileb://./function.zip \
  --publish

Upgrading the runtime

To upgrade an existing runtime layer, build the new runtime and publish a new version by calling the following to create a new version:

$ xp lambda runtime
$ aws lambda publish-layer-version \
  --layer-name lambda-xp-runtime \
  --zip-file fileb://./runtime-X.X.X.zip \
  --region us-east-1

Now, switch the function over to use this new layer:

$ aws lambda update-function-configuration \
  --function-name greet \
  --layers "arn:aws:lambda:us-east-1:XXXXXXXXXXXX:layer:lambda-xp-runtime:2"

Using other AWS services

In order to programmatically use other AWS services use the ServiceEndpoint class:

use com\amazon\aws\{Credentials, ServiceEndpoint};
use com\amazon\aws\lambda\Handler;

class WebSockets extends Handler {

  /** @return callable|com.amazon.aws.lambda.Lambda|com.amazon.aws.lambda.Streaming */
  public function target() {
    return function($event, $context) {

      // Send message to WebSocket connection
      $this->environment->endpoint('execute-api')
        ->in($context->region)
        ->using($event['requestContext']['apiId'])
        ->resource('/{stage}/@connections/{connectionId}', $event['requestContext'])
        ->transmit(['message' => 'Reply'])
      ;
      return ['statusCode' => 200];
    };
  }
}

To test this locally, pass the necessary environment variables via -e on the command line:

$ xp lambda test -e AWS_ACCESS_KEY_ID=... -e AWS_SECRET_ACCESS_KEY=... WebSockets '{"requestContext":...}'
# ...

Context

The context object passed to the target lambda is defined as follows:

public class com.amazon.aws.lambda.Context implements lang.Value {
  public string $awsRequestId
  public string $invokedFunctionArn
  public string $traceId
  public string $clientContext
  public string $cognitoIdentity
  public string $deadline
  public string $functionName
  public string $functionVersion
  public string $memoryLimitInMB
  public string $logGroupName
  public string $logStreamName
  public string $region
  public int $payloadLength

  public function __construct(array $headers, array $environment)

  public function remainingTime(?float $now): ?float
  public function toString(): string
  public function hashCode(): string
  public function compareTo(var $value): int
}

Environment

The runtime environment is defined as follows:

public class com.amazon.aws.lambda.Environment {
  public string $root
  public [:string] $variables
  public io.streams.StringWriter $writer
  public util.PropertySource $properties

  public function __construct(string $root, ?io.streams.StringWriter $writer)

  public function taskroot(): io.Path
  public function path(string $path): io.Path
  public function tempDir(): io.Path
  public function local(): bool
  public function variable(string $name): ?string
  public function credentials(): com.amazon.aws.Credentials
  public function trace(var... $args): void
  public function properties(string $name): util.PropertyAccess
}

Interfaces

Instead of functions, a handler's target() method may also return instances implementing the Lambda or Streaming interfaces:

public interface com.amazon.aws.lambda.Lambda {
  public function process(var $event, com.amazon.aws.lambda.Context $context): var
}

public interface com.amazon.aws.lambda.Streaming {
  public function handle(
    var $event,
    com.amazon.aws.lambda.Stream $stream,
    com.amazon.aws.lambda.Context $context
  ): void
}

See also

lambda's People

Contributors

thekid avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar

lambda's Issues

Syntax error PHP 7.0...7.3: unexpected '...' (T_ELLIPSIS)

$ XP_RT=7.0 xp lambda run Greet
Uncaught exception: ParseError (syntax error, unexpected '...' (T_ELLIPSIS), expecting ']')
  at <source> [line 72 of .\src\main\php\xp\lambda\Runner.class.php]
  at <main>('xp.lambda.Runner', 'run', 'Greet') [line 0 of class-main.php]
  at lang.AbstractClassLoader->loadClass0('xp.lambda.Runner') [line 466 of lang.base.php]
  at <main>::{closure}('xp\\lambda\\Runner') [line 0 of (unknown)]
  at <main>::spl_autoload_call('xp\\lambda\\Runner') [line 0 of (unknown)]
  at <main>::is_callable(array[2]) [line 384 of class-main.php]

Increase test coverage

At least for the classes inside the com.amazon.aws.lambda package - the ones in xp.lambda heavily interact with processes and files and may be more easy to simply try out.

$ xp coverage -p src/main/php/ src/test/php
# ...
Tests:       35 passed
Memory used: 12548.45 kB (18002.38 kB peak)
Time taken:  0.052 seconds
Coverage:    30.95% lines covered (65/210)

┌──────────────────────────────────────────────────────┬─────────┬──────┐
│ Class                                                │ % Lines │  Not │
╞══════════════════════════════════════════════════════╪═════════╪══════╡
│ com.amazon.aws.lambda.Context                        │  91.43% │    3 │
│ com.amazon.aws.lambda.Environment                    │  92.86% │    1 │
│ com.amazon.aws.lambda.Handler                        │  88.89% │    1 │
│ xp.lambda.AwsRunner                                  │   0.00% │   33 │
│ xp.lambda.CreateRuntime                              │   0.00% │   23 │
│ xp.lambda.DisplayError                               │   0.00% │    3 │
│ xp.lambda.PackageLambda                              │   0.00% │   39 │
│ xp.lambda.Runner                                     │   0.00% │   20 │
│ xp.lambda.Sources                                    │ 100.00% │      │
│ xp.lambda.TestLambda                                 │   0.00% │   10 │
└──────────────────────────────────────────────────────┴─────────┴──────┘

Simplify packaging lambda

Currently, the ZIP command line utility is required to create a lambda deployment:

$ zip -r task.zip class.pth src vendor

The downsides:

  • This program must be installed
  • The command line must be remembered
  • This also bundles unnecessary test code from the src and vendor directories

While the latter could be circumvented by a combination of find and grep -v, this would make the second point even more obvious.

Idea: Extend lambda

This could be the new way of packaging:

$ xp lambda package

This would create a ZIP file exactly like above, but exclude any code in src/test/php by default. It would work with https://github.com/xp-framework/zip, and should be able to work even without ZLIB - files would simply not be compressed in this case.

Support streaming

Announced in https://aws.amazon.com/de/blogs/compute/introducing-aws-lambda-response-streaming/, April 2023 - supported in Node runtimes.

JavaScript

Code in index.js:

exports.handler = awslambda.streamifyResponse(async (event, stream, context) => {
  stream.setContentType("text/plain");
  stream.write("[" + new Date().toISOString() + "] Hello world...\n");

  await new Promise(resolve => setTimeout(resolve, 1000));

  stream.write("[" + new Date().toISOString() + "] ...from Lambda\n");
  stream.end();
});

Code in serverless.yml:

service: node-streaming

provider:
  name: aws
  region: eu-central-1
  profile: default

functions:
  func:
    handler: index.handler
    runtime: nodejs18.x
    description: 'NodeJS Streaming'
    url:
      invokeMode: RESPONSE_STREAM

Invocation:
nodejs-lambda-stream

See also

Add support for different PHP versions

Currently, version 8.0.9 is hardcoded.

# Use PHP runtime equalling that in use
$ xp lambda runtime

# Use the newest 8.0-release
$ xp lambda runtime:8.0

# Use a specific PHP version
$ xp lambda runtime:8.0.9

Reusing the docker syntax here for <image>:<tag>

Resolve symlinks

Using path repositories in Composer, I've added a library from a local filesystem path. Composer will create a symlink to this and make everything work with autoloading.

$ ls -al vendor/enbwag
total 8
drwxr-xr-x 2 thekid thekid 4096 Oct  6 17:41 .
drwxr-xr-x 7 thekid thekid 4096 Oct  6 17:41 ..
lrwxrwxrwx 1 thekid thekid   41 Oct  6 17:41 empower-foundation -> /home/thekid/devel/lib/empower-foundation

However, when packaging, we should resolve symlinks instead of following them, which results in the following ZIP file:

$ unzip -l function.zip
Archive:  function.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2023-10-06 16:29   vendor/
        0  2023-10-06 16:29   vendor/enbwag/
        0  2023-10-06 16:29   vendor/enbwag/empower-foundation/
        0  2023-10-06 16:29   ../../lib/empower-foundation/src/
# ...

PHP extensions

Currently, we compile with the following extensions enabled:

  • Core
  • bcmath
  • ctype
  • date
  • fileinfo
  • filter
  • hash
  • iconv
  • json
  • openssl
  • pcre
  • Phar
  • posix
  • Reflection
  • session
  • SPL
  • standard
  • tokenizer

Looking at https://bref.sh/docs/environment/php.html#extensions-installed-and-enabled and from scanning through use cases, the following could be interesting:

  • gd, exif - image processing
  • mbstring - prerequisite for exif anyways
  • sodium - for crypto
  • zlib - if we want to create ZIP files, HTTP compression - see #8 (comment)
  • sqlite3 - for temporary data storage
  • libxml, dom, xml, xmlreader, xmlwriter - for processing XML and HTML - see #11

The following extensions don't seem to make sense for an AWS lambda enviromment:

  • pcntl - for parallelization, you would most probably just invoke other lambdas and/or use a message queue
  • readline - this is for humans sitting in front of a terminal, not for the serverside
  • curl, ftp, mysqli / mysqlnd - the XP Framework has it's own protocol implementations
  • sockets - the standard socket library works just as well for (most - or almost all?) cases
  • opcache - we leave our code running after compiling once, caching doesn't improve this any more
  • SimpleXML - covered by XP Framework's XML APIs - used by AWS SDK, see #8 (comment)

Podman support

Podman serves as a snap-in replacement for Docker. The test and runtime subcommands should try to search for Podman if Docker is not installed.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.