I wanted to deploy Serverless Microservices(PHP) using AWS Lambda for one of my projects. Through research, I found some existing packages that can accomplish this task.
I was curious about the process inside the lambda container. Thus, I decided to do it on my own.
So Let’s start the Adventure….
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
We will create a layer of runtime in AWS lambda which contains
And we will reference it in the lambda function.
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 the 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.
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)]
);
}
AWS_LAMBDA_RUNTIME_API is AWS lambda’s default environment variable, we don’t need to set it. I’ll explain later, why we should send a JSON response to the AWS Lambda.
Finally, use the do-while loop, which will run until AWS Lambda’s environment is 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 wanted an HTML view for my response. You can take content type according to your requirements.
$data will look 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 will look like 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();
//Send request to lambda function.
return $client->get('http://' . $_ENV['AWS_LAMBDA_RUNTIME_API'] . '/2018-06-01/runtime/invocation/next');
}
function sendResponse($invocationId, $response)
{
$client = new \GuzzleHttp\Client();
//Get response from lambda function and send it to client.
$client->post(
'http://' . $_ENV['AWS_LAMBDA_RUNTIME_API'] . '/2018-06-01/runtime/invocation/' . $invocationId . '/response',
['body' => json_encode($response)]
);
}
To execute 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.
I have built PHP binary using amazonlinux:2 docker image. You can find the Full Code of it in Dockerfile.
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.
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,
Lambda Function has a layer of PHP runtime as,
Now it's time to run the deployed Lambda function.
Open the created Lambda Function and Test it. If everything’s fine, it will show output like,
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 applications by doing some minor configurations like changing Session, Cache, and Logs drivers.
We’re Grateful to have you with us on this journey!
Suggestions and feedback are more than welcome!
Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.
Whether you need...