Custom PHP Runtime Layer in AWS Lambda Functions
By: Sumita Canopas
Tags: AWS, Microservices, Custom Runtime, AWS Lambda, PHP

I wanted to deploy Serverless Microservices(PHP) using AWS Lambda for one of my projects. I researched and found some existing packages that can accomplish this task.

I was curious about the process happening inside the lambda container. Thus, I decided to do it on my own.

So Let’s start the Adventure….

Prerequisites :

AWS Account

Basic knowledge of AWS lambda, CloudWatch, and API gateway

We will create a layer of runtime in AWS lambda which contains

PHP binary

runtime.php

bootstrap

and we will reference it in the lambda function.

PHP Runtime:

We are using guzzlehttp for handling incoming lambda requests.

You can install it using,

composer require guzzlehttp/guzzle

In the runtime.php, add vendor dependency for guzzlehttp.

require __DIR__ . '/vendor/autoload.php';

Then, create method getNextRequest() for capturing lambda requests. In this method, we will initialize the guzzlehttp client, and send a request to AWS lambda.

function getNextRequest()

{

$client = new \GuzzleHttp\Client();

} return $client->get(' http://' . $_ENV['AWS_LAMBDA_RUNTIME_API'] . '/2018-06-01/runtime/invocation/next');

After this, add a method to send a response to the AWS lambda.

AWS_LAMBDA_RUNTIME_API is AWS lambda’s default environment variable, we don’t need to set it. I’ll explain later, why should we send a JSON response to the AWS Lambda.

Finally, use the do-while loop, which will run till the AWS Lambda’s environment shut down. Then, call both of the above methods.

do { $request = getNextRequest(); $invocationId = $request->getHeader('Lambda-Runtime-Aws-Request-Id')[0]; $payLoad = json_decode((string) $request->getBody(), true); $data = []; sendResponse($invocationId, $data); }while(true)

Here data is an empty array, so let’s prepare data to send a response to AWS Lambda.

Create an index.php file. It contains a simple PHP code that we want to run in AWS lambda. You can add your code and pass the result as $response to runtime.php.

<?php $response = "Hello World"; ?>

Now we’ll include it in runtime.php as,

require_once $_ENV['LAMBDA_TASK_ROOT'].'/index.php';

Then prepare $data as follows,

$headers['Content-type'] = "text/html; charset=UTF-8";

$statusCode = 200;

$content = $response;

I took Content-type as text/html, because I want HTML view for my response. You can take content-type as per your requirements.

$data will be like this, we will encode it using PHP's json_encode in sendResponse() to get JSON response.

$data = [

'statusCode' => $statusCode,

'isBase64Encoded' => false,

'headers' => $headers,

'body' => $content

];

Full code of runtime.php mentioned below,

<?php // This invokes Composer's autoloader so that we'll be able to use Guzzle and any other 3rd party libraries we need. require __DIR__ . '/vendor/autoload.php'; // This is the 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(); //Get invocationId and Payload from request.

$invocationId = $request->getHeader('Lambda-Runtime-Aws-Request- Id')[0]; $payLoad = json_decode((string) $request->getBody(), true); //include index.php file require_once $_ENV['LAMBDA_TASK_ROOT'].'/index.php';



$headers['Content-type'] = "text/html; charset=UTF-8";

$statusCode = 200;

$content = $response; //Prepare response for runtime API.

$data = [

'statusCode' => $statusCode,

'isBase64Encoded' => false,

'headers' => $headers,

'body' => $content

]; // Submit the response back to the runtime API.

sendResponse($invocationId, $data);

} while (true); function getNextRequest()

{

$client = new \GuzzleHttp\Client();

return $client->get('http://' . $_ENV['AWS_LAMBDA_RUNTIME_API'] . '/2018-06-01/runtime/invocation/next');

} //Send request to lambda function. function sendResponse($invocationId, $response)

{

$client = new \GuzzleHttp\Client();

$client->post(

'http://' . $_ENV['AWS_LAMBDA_RUNTIME_API'] . '/2018-06-01/runtime/invocation/' . $invocationId . '/response',

['body' => json_encode($response)]

); //Get response from lambda function and send it to client. }

For executing runtime.php in the Lambda environment, we need PHP binary. As I need a UI interface, I’ll use PHP_CGI. We can use PHP if an interface is not required.

PHP Binary :

I have built PHP binary using amazonlinux:2 docker image. You can find the Full Code of it in Dockerfile.

Bootstrap :

It contains a command to execute runtime.php

Layer’s files reside in /opt directory and Function’s files reside in /var/task directory.

#go into the source directory

cd $LAMBDA_TASK_ROOT #execute the runtime

/opt/php-cgi /opt/runtime.php

Again, LAMBDA_TASK_ROOT is Lambda’s predefined env variable valued as the root directory of lambda function which is /var/task.

Last step:

Create a Lambda layer and a lambda function using AWS-CLI and deploy PHP code. Deploy.sh contains a script of it.

I created a GitHub repo for serverless-php, which contains a lambda function example.

After deploying both layer and function, it will look like below,

AWS Lambda Layer

AWS Lambda Function

Lambda Function has a layer of PHP runtime as,

Layer Reference in AWS Lambda function

Now it's time to run the deployed Lambda function.

Open Created Lambda Function and Test it, If everything’s fine, it will show output like,

Lambda Function Response

If we want to use an API gateway with a Lambda function, As mentioned here at Method Response, the response should be in JSON format.

You can configure CloudWatch to trace the logs of PHP applications.

Our custom PHP runtime also allows the deployment of PHP Laravel and CodeIgniter Application by doing some minor configurations like changes Session, Cache, and Logs drivers.