Search

PHP & Serverless with Bref - part 2


May 13, 2020 | 11 mins Marie Minasyan

This article is a follow-up to this first part which introduces serverless computing. In this second part, we will first see what the layers are in AWS Lambda and how to implement them. Then we will see how to use the Bref framework.

AWS Lambda

How it works

An AWS Lambda environment includes:

  • the runtime of the chosen language (Java, Go, PowerShell, Node.js, C #, Python, Ruby by default)
  • the implementation of Lambda runtime API, i.e. the lifecycle of the execution of the environment and the invocation of serverless functions

The lifecycle of a Lambda runtime consists of an initialization phase and several (as many as necessary) invocation phases.

The initialization phase represents the time between the moment when the environment starts the runtime and the moment when the code of a function is executed. This phase is performed only once during the life cycle of the environment.

After initialization, the execution environment goes into the invocation phase and will constantly check and execute tasks, until the environment shuts down.

Since November 2018, it is possible to declare your own runtimes for Lambda functions, but also to incorporate reusable components in the form of Layers.

You can implement a runtime in any language. A runtime is a program that executes the handler of a Lambda function when it is called. A runtime can be included in the function deployment package in the form of an executable file named bootstrap (we will see an example later in this article).

Layers

A Lambda function can be configured to download additional code and content as a layer. A layer is a ZIP archive that contains libraries, a custom runtime or other dependencies.

If you have already written serverless functions in Node.js, you know that you must package the entire node_modules folder for every function (since they are deployed independently from each other). This slows down the deployment process and makes the builds slow.

But now, it is possible to publish the node_modules folder as a shared and reusable layer for all our functions. This means that we could have a layer for our custom runtime, another layer which contains our dependencies and configure our functions to use these 2 layers. Note that a function has a limit of 5 layers.

Example

PHP function

Take the following simple function as an example:

// src/profession.php
function occupation()
{
    $jobs = [
        'Fireman',
        'Astronaut',
        'Super hero',
        'Pilot',
        'Professional cook',
        'Artist',
    ];

    return ['occupation' => $jobs[array_rand($jobs)]];
}

PHP layer

I am going to create a layers/php folder in my application and I will place my layer there. To create a custom runtime, we need a bootstrap file which will contain the logic of our runtime in charge of invoking our functions.

We also need a PHP executable capable of interpreting our code. I’m going to create a bin folder in my layer folder to place my php binary. To generate a binary, I recommend you read this article.

When deploying a layer, it is placed in the /opt folder in the containers. So my bootstrap file could look like this:

#!/bin/sh

#go into the source directory
cd $LAMBDA_TASK_ROOT

#execute the runtime
/opt/bin/php /opt/runtime.php

Here is an example of runtime.php inspired by the article on the AWS blog. We will use Guzzle to make HTTP calls, therefore I will first execute the following command:

composer require guzzlehttp/guzzle
<?php

// Invoke Composer's autoloader to use Guzzle
require $_ENV['LAMBDA_TASK_ROOT'] . '/vendor/autoload.php';

// Request processing loop => barring unrecoverable failure, this loop runs until the environment shuts down
do {
    // Ask the runtime API for a request to handle
    $request = getNextRequest();

    // Obtain the function name from the _HANDLER environment variable and ensure the function's code is available
    list($handlerFile, $handlerFunction) = explode(".", $_ENV['_HANDLER']);
    require_once $_ENV['LAMBDA_TASK_ROOT'] . '/src/' . $handlerFile . '.php';

    // Execute the desired function and obtain the response
    $response = $handlerFunction($request['payload']);

    // Submit the response back to the runtime API
    sendResponse($request['invocationId'], $response);
} while (true);

function getNextRequest()
{
    $client = new \GuzzleHttp\Client();
    $response = $client->get(sprintf(
        'http://%s/2018-06-01/runtime/invocation/next',
        $_ENV['AWS_LAMBDA_RUNTIME_API']
    ));

    return [
        'invocationId' => $response->getHeader('Lambda-Runtime-Aws-Request-Id')[0],
        'payload' => json_decode((string) $response->getBody(), true),
    ];
}

function sendResponse($invocationId, $response)
{
    $client = new \GuzzleHttp\Client();
    $client->post(
        sprintf(
            'http://%s/2018-06-01/runtime/invocation/%s/response',
            $_ENV['AWS_LAMBDA_RUNTIME_API'],
            $invocationId
        ),
        ['body' => $response]
    );
}

To summarize, we currently have the following file structure:

layers/
    php/
        bin/
            php #binary file
        bootstrap
        runtime.php
src/
    profession.php
vendor/
    guzzlehttp/

Deployment

I will use the serverless framework to deploy my layer and my function:

# serverless.yml
service: php-serverless
provider:
  name: aws
  runtime: provided
  region: eu-west-3
  memorySize: 512

layers:
  php:
    path: layers/php

functions:
  occupation:
    handler: profession.occupation
    layers:
      - {Ref: PhpLambdaLayer}

As we can see, in my occupation function, the handler contains the name of my file profession.php and the occupation method. This is how I configured it in runtime.php:

//...
list($handlerFile, $handlerFunction) = explode(".", $_ENV['_HANDLER']);
require_once $_ENV['LAMBDA_TASK_ROOT'] . '/src/' . $handlerFile . '.php';
$response = $handlerFunction($request['payload']);

It is therefore up to us to configure the way we name the handlers and the way to call them in the runtime.

The name of our layer PhpLambdaLayer corresponds to its CloudFormation reference. You can read the details here.

To deploy the function and the layer, execute the following command:

$ sls deploy

Serverless: Packaging service...
#...
Serverless: Stack update finished...
Service Information
service: php-serverless
stage: dev
region: eu-west-3
stack: php-serverless-dev
resources: 7
api keys:
  None
endpoints:
  None
functions:
  occupation: php-serverless-dev-occupation
layers:
  php: arn:aws:lambda:eu-west-3:087017887086:layer:php:1

Finally, let’s invoke the occupation function:

$ sls invoke -f occupation -l
{
  "occupation": "Fireman"
}
--------------------------------------------------------------------
START RequestId: d09f2191-7233-47d3-a4fe-8de2a621a608 Version: $LATEST
END RequestId: d09f2191-7233-47d3-a4fe-8de2a621a608
REPORT RequestId: d09f2191-7233-47d3-a4fe-8de2a621a608  Duration: 38.15 ms  Billed Duration: 300 ms  Memory Size: 512 MB  Max Memory Used: 59 MB  Init Duration: 191.10 ms

Summary

So we just made a working example with a layer capable of executing PHP code.

Now imagine that you have a large application, say a REST API in Symfony, that you would like to deploy on AWS Lambda. It would be necessary to develop a much more advanced runtime capable of integrating with the front controller of Symfony, and why not with the console as well. We would also have to modify the PHP layer to add all the libraries we would need and to recompile the PHP binary.

Fortunately for us, an open source solution exists to manage all of this: Bref.

Bref

Bref is an open source Composer package that allows us to deploy PHP applications on AWS Lambda. It is developed by Matthieu Napoli.

Bref provides:

  • the documentation
  • PHP runtimes for AWS Lambda
  • deployment tools
  • integration with PHP frameworks

I suggest we deploy a Symfony application on AWS Lambda using Bref.

Symfony application

To create my application:

$ composer create-project symfony / skeleton sf-serverless-example

Next, let’s modify the default controller as follows (to use the same example as above):

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;

class DefaultController
{
    public function index()
    {
        $jobs = [
            'Fireman',
            'Astronaut',
            'Super hero',
            'Pilot',
            'Professional cook',
            'Artist',
        ];

        return new JsonResponse([
            'occupation' => $jobs[array_rand($jobs)],
        ]);
    }
}

Now let’s add the Bref library:

$ composer require bref/bref

Finally, let’s configure the deployment with serverless framework:

# serverless.yml
service: php-serverless-sf-bref
provider:
  name: aws
  region: eu-west-3
  runtime: provided
  environment:
    # Symfony environment variables
    APP_ENV: prod

plugins:
  - ./vendor/bref/bref

functions:
  website:
    handler: public/index.php
    timeout: 28 # API Gateway has a timeout of 29 seconds
    layers:
      - ${bref:layer.php-74-fpm}
    events:
      - http: 'ANY /'
      - http: 'ANY /{proxy+}'
  console:
    handler: bin/console
    timeout: 120 # in seconds
    layers:
      - ${bref:layer.php-74} # PHP
      - ${bref:layer.console} # The "console" layer

The list of layers made available by Bref can be consulted here. I also recommend that you read the Bref documentation, it is very clear and provides plenty of examples that you may need.

We need to keep in mind that with most cloud providers the filesystem is read only. Hence, we need to change the logs and cache folders of our application:

public function getLogDir()
{
    // When on the lambda only /tmp is writeable
    if (getenv('LAMBDA_TASK_ROOT') !== false) {
        return '/tmp/log/';
    }

    return parent::getLogDir();
}

public function getCacheDir()
{
    // When on the lambda only /tmp is writeable
    if (getenv('LAMBDA_TASK_ROOT') !== false) {
        return '/tmp/cache/'.$this->environment;
    }

    return parent::getCacheDir();
}

Last step, deployment:

$ sls deploy

Serverless: Packaging service...
Service Information
service: php-serverless-sf-bref
stage: dev
region: eu-west-3
stack: php-serverless-sf-bref-dev
resources: 15
api keys:
  None
endpoints:
  ANY - https://maeck9uwyf.execute-api.eu-west-3.amazonaws.com/dev
  ANY - https://maeck9uwyf.execute-api.eu-west-3.amazonaws.com/dev/{proxy+}
functions:
  website: php-serverless-sf-bref-dev-website
  console: php-serverless-sf-bref-dev-console
layers:
  None

My application is available on the URL indicated in the endpoints. Here is the result:

That’s it. We just deployed a Symfony application to AWS Lambda using Bref! As you can see, it is a pretty straight forward process…

You can now enjoy deploying PHP applications to a serverless infrastructure :)

Author(s)

Marie Minasyan

Astronaute Raccoon @ *ElevenLabs*_🚀 De retour dans la Galaxie.

Site is Ready for Offline Use