Running PHP on Amazon Lambda with Serverless

Submitted by nigel on Sunday 5th November 2017

For those who are still getting up to speed with Amazon's FaaS Lambda, to quote their own blurb, "AWS Lambda is a compute service that lets you run code without provisioning or managing servers. AWS Lambda executes your code only when needed and scales automatically, from a few requests per day to thousands per second. You pay only for the compute time you consume - there is no charge when your code is not running."

It's an architecture that effectively means an organisation isn't required to spend time and money on building server infrastructure, orchestration and deploy scripts. It is therefore ideal for proof of concept and API endpoint type developments, but it would not be outlandish to suggest it could also be used for hosting websites (static but perhaps dynamic too by using Amazon's DynamoDB NoSQL offering).

The architecture is frequently referred to as serverless - that's a bit of a misnomer since it doesn't mean you don't need servers; you just don't need to build them. Serverless is also a CLI framework that allows developers to build and deploy auto-scaling, pay-per-execution, event driven functions. Serverless used in conjunction with Amazon Lambda is the focus in this introductory tutorial. 

Lambda currently supports Node.js, Java, C# and Python applications. So if you are a PHP developer you are out of luck, right? Wrong! The serverless framework can be extended to use PHP libraries (and therefore PHP code) through a neat software shim developed by araines. This is appealing since PHP resource is plentiful and often cheap, opening up the world of Lambda to a very large community. 

Getting Started

A prerequisite is getting node installed on your machine. I am a Mac user so I downloaded and installed from the Node.js Download page.

Next you need to check whether you are on PHP 7 or above. If not, and you are a Mac user, follow the instructions here and install composer by using the instructions here

Once you have that working, you can install serverless on the command line with:

# npm install -g serverless
Once that has completed you can install the serverless-php example code - this requires git. Use the -n flag to specify the directory into which you want the project installed
Nigels-MacBook-Pro:Projects nigel$ serverless install --url https://github.com/araines/serverless-php -n serverless-php-demo
Serverless: Downloading and installing "serverless-php"...
Serverless: Successfully installed "serverless-php" as "serverless-php-demo"
Nigels-MacBook-Pro:Projects nigel$ cd serverless-php-demo/
Nigels-MacBook-Pro:serverless-php-demo nigel$ ls -las
total 96
0 drwxr-xr-x  16 nigel  staff   544 30 Oct 11:50 .
0 drwxr-xr-x   6 nigel  staff   204 30 Oct 11:50 ..
8 -rw-r--r--   1 nigel  staff    40 16 Apr  2017 .gitattributes
8 -rw-r--r--   1 nigel  staff   120 16 Apr  2017 .gitignore
8 -rw-r--r--   1 nigel  staff   569 16 Apr  2017 CHANGELOG.md
8 -rw-r--r--   1 nigel  staff  1103 16 Apr  2017 LICENSE
8 -rw-r--r--   1 nigel  staff  3181 16 Apr  2017 README.md
8 -rw-r--r--   1 nigel  staff   659 16 Apr  2017 buildphp.sh
8 -rw-r--r--   1 nigel  staff   430 16 Apr  2017 composer.json
0 drwxr-xr-x   3 nigel  staff   102 30 Oct 11:50 config
8 -rw-r--r--   1 nigel  staff   783 16 Apr  2017 dockerfile.buildphp
8 -rw-r--r--   1 nigel  staff  1330 16 Apr  2017 handler.js
8 -rw-r--r--   1 nigel  staff   829 16 Apr  2017 handler.php
8 -rwxr-xr-x   1 nigel  staff   133 16 Apr  2017 php
8 -rw-r--r--   1 nigel  staff  3012 30 Oct 11:50 serverless.yml
0 drwxr-xr-x   5 nigel  staff   170 30 Oct 11:50 src
Nigels-MacBook-Pro:serverless-php-demo nigel$ 
Now here's a problem - GitHub isn't good at downloading executable files - if you look carefully you'll see that the compiled PHP image is a mere 133 bytes. That certainly isn't right! You will have to go back to GitHub, click on PHP in the directory listing, then click on download to get the correct version of PHP. Once it's finished downloading, copy it into place and get a directory listing to prove it's ok.
Nigels-MacBook-Pro:serverless-php nigel$ cp ~/Downloads/php .
Nigels-MacBook-Pro:serverless-php nigel$ ls -lash
total 49232
    0 drwxr-xr-x  17 nigel  wheel   578B  6 Nov 17:20 .
    0 drwxr-xr-x   4 nigel  wheel   136B  6 Nov 17:20 ..
    0 drwxr-xr-x  12 nigel  wheel   408B  6 Nov 17:20 .git
    8 -rw-r--r--   1 nigel  wheel    40B  6 Nov 17:20 .gitattributes
    8 -rw-r--r--   1 nigel  wheel   120B  6 Nov 17:20 .gitignore
    8 -rw-r--r--   1 nigel  wheel   569B  6 Nov 17:20 CHANGELOG.md
    8 -rw-r--r--   1 nigel  wheel   1.1K  6 Nov 17:20 LICENSE
    8 -rw-r--r--   1 nigel  wheel   3.1K  6 Nov 17:20 README.md
    8 -rw-r--r--   1 nigel  wheel   659B  6 Nov 17:20 buildphp.sh
    8 -rw-r--r--   1 nigel  wheel   430B  6 Nov 17:20 composer.json
    0 drwxr-xr-x   3 nigel  wheel   102B  6 Nov 17:20 config
    8 -rw-r--r--   1 nigel  wheel   783B  6 Nov 17:20 dockerfile.buildphp
    8 -rw-r--r--   1 nigel  wheel   1.3K  6 Nov 17:20 handler.js
    8 -rw-r--r--   1 nigel  wheel   829B  6 Nov 17:20 handler.php
49144 -rwxr-xr-x@  1 nigel  wheel    24M  6 Nov 20:48 php
    8 -rw-r--r--   1 nigel  wheel   2.9K  6 Nov 17:20 serverless.yml
    0 drwxr-xr-x   5 nigel  wheel   170B  6 Nov 17:20 src
Nigels-MacBook-Pro:serverless-php nigel$
That's more like it!
Then by running composer all the project dependencies are installed
Nigels-MacBook-Pro:serverless-php-demo nigel$ composer install -o --no-dev
Loading composer repositories with package information
Updating dependencies
Package operations: 7 installs, 0 updates, 0 removals
  - Installing psr/log (1.0.2): Downloading (100%)         
  - Installing monolog/monolog (1.23.0): Downloading (100%)         
  - Installing symfony/filesystem (v3.3.10): Downloading (100%)         
  - Installing symfony/config (v3.3.10): Downloading (100%)         
  - Installing psr/container (1.0.0): Downloading (100%)         
  - Installing symfony/dependency-injection (v3.3.10): Downloading (100%)         
  - Installing symfony/yaml (v3.3.10): Downloading (100%)         
Writing lock file
Generating optimized autoload files
Nigels-MacBook-Pro:serverless-php-demo nigel$
Configuring the Function

The configuration of the function is held in serverless.yml which you'll note contains a bunch of commented out code for more advanced usage which will cover in later blogs. However it would be a good idea to make a few changes so that you gain some familiarity with the code base. For now we want to change the service name to something more meaningful i.e. from serverless-php-demo to php-demo (lol) and the provider runtime may need changing depending upon which geographical Amazon region you intend to use. I am going for Ireland so us-east-1 becomes eu-west-1.

Once edited, the top few lines of your yml file should look like this:

Nigels-MacBook-Pro:serverless-php-demo nigel$ head -n 27 serverless.yml 
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!
 
service: php-demo # NOTE: update this with your service name
 
# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"
 
provider:
  name: aws
  runtime: nodejs6.10
 
# you can overwrite defaults here
#  stage: dev
  region: eu-west-1
 
Nigels-MacBook-Pro:serverless-php-demo nigel$
Let's change the name of the function at the same time to demo.
Nigels-MacBook-Pro:serverless-php-demo nigel$ sed -i '' 's/hello/demo/g' serverless.yml 
Nigels-MacBook-Pro:serverless-php-demo nigel$
This will edit the function name as below
Nigels-MacBook-Pro:serverless-php-demo nigel$ tail -49 serverless.yml | head -9
functions:
  demo:
    handler: handler.handle
    environment:
      HANDLER: handler.demo  # This is the service name which will be used (from services.yml)
    events:
      - http:
          path: demo
          method: get
Nigels-MacBook-Pro:serverless-php-demo nigel$ 
Next we need to change the handler to demo - to do this go into the config subdirectory, edit services.yml and change the handler to handler.demo
Nigels-MacBook-Pro:serverless-php-demo nigel$ cd config/
Nigels-MacBook-Pro:config nigel$ vi services.yml 
Nigels-MacBook-Pro:config nigel$ cat services.yml | grep demo
  handler.demo:
Nigels-MacBook-Pro:config nigel$
Ok so at this point we could invoke the function locally but we really don't want relics of the original 'hello' hanging around, and if you get a directory listing of the src subdirectory you'll see a PHP class HelloHandler.php.
Nigels-MacBook-Pro:serverless-php-demo nigel$ pwd
/Users/nigel/Projects/serverless-php-demo
Nigels-MacBook-Pro:serverless-php-demo nigel$ cd src
Nigels-MacBook-Pro:src nigel$ ls -las
total 24
0 drwxr-xr-x   5 nigel  staff   170 30 Oct 14:17 .
0 drwxr-xr-x  18 nigel  staff   612 30 Oct 12:30 ..
8 -rw-r--r--   1 nigel  staff  2676 16 Apr  2017 Context.php
8 -rw-r--r--   1 nigel  staff   363 16 Apr  2017 Handler.php
8 -rw-r--r--   1 nigel  staff   418 30 Oct 14:17 HelloHandler.php
Nigels-MacBook-Pro:src nigel$ 
So rename this to DemoHandler.php and edit it so the class name is DemoHandler. You should end up with
Nigels-MacBook-Pro:src nigel$ mv HelloHandler.php DemoHandler.php 
Nigels-MacBook-Pro:src nigel$ vi DemoHandler.php 
Nigels-MacBook-Pro:src nigel$ cat DemoHandler.php | grep Demo
class DemoHandler implements Handler
Nigels-MacBook-Pro:src nigel$ 
Now go back to the config directory. Since we've renamed the PHP handler class, we need to make sure that is reflected in the class setting. Change the reference to class: Raines\Serverless\DemoHandler
Nigels-MacBook-Pro:serverless-php-demo nigel$ cd config
Nigels-MacBook-Pro:config nigel$ vi services.yml 
Nigels-MacBook-Pro:config nigel$ cat services.yml | grep Demo
    class: Raines\Serverless\DemoHandler
Nigels-MacBook-Pro:config nigel$ 
One final activity - the PHP autoloader contains class mappings which need to reflect our changes to the handler class. So navigate to the vendor/composer directory and change the HelloHandler instances.
Nigels-MacBook-Pro:config nigel$ cd ../vendor/composer/
Nigels-MacBook-Pro:composer nigel$ ls
ClassLoader.php		autoload_classmap.php	autoload_psr4.php	autoload_static.php
LICENSE			autoload_namespaces.php	autoload_real.php	installed.json
Nigels-MacBook-Pro:composer nigel$ sed -i '' 's/Hello/Demo/g' autoload_classmap.php autoload_static.php 
Nigels-MacBook-Pro:composer nigel$ 
We are now ready to try it out!!
Local invocation
Before we deploy our code to Amazon Lambda, it's wise to check it out locally. This is simplicity itself. Go to the base directory of the project and issue the following command. 
Nigels-MacBook-Pro:serverless-php-demo nigel$ pwd
/Users/nigel/Projects/serverless-php-demo
Nigels-MacBook-Pro:serverless-php-demo nigel$ serverless invoke local -f demo
Got event [] []
{
    "statusCode": 200,
    "body": "Go Serverless v1.0! Your function executed successfully!"
}
Nigels-MacBook-Pro:serverless-php-demo nigel$ 
Success!
AWS deployment

Before your code can be deployed you will need an AWS account with the Lambda service enabled. At the time of authoring this there was a free forever Lambda service available, albeit one obviously with restrictions on usage. However these restrictions were more than adequate for my projects. 

You will also need to set up permissions for your function to be uploaded to Amazon. I created a user called serverless-admin with a policy of allowing Administrator access to all Amazon services. This can obviously be honed down should you feel this is not required.  

I opted for the easiest way of enabling my Macbook access rights to upload the code - by navigating to Security Credentials (under Users) although other options are available. There is an excellent overview on the Serverless website. I set up an Access Key and a Secret Key. These can be set as environmental variables on the command line and the serverless deploy script will use them during the deploy process to authenticate. 

Ok - using the keys, lets try out the deployment. 

Nigels-MacBook-Pro:serverless-php-demo nigel$ export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXX
Nigels-MacBook-Pro:serverless-php-demo nigel$ export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXX
Nigels-MacBook-Pro:serverless-php-demo nigel$ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (921.53 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: php-demo
stage: dev
region: eu-west-1
stack: php-demo-dev
api keys:
  None
endpoints:
  GET - https://XXXXXXXX.execute-api.eu-west-1.amazonaws.com/dev/demo
functions:
  demo: php-demo-dev-demo
Serverless: Publish service to Serverless Platform...
Service successfully published! Your service details are available at:
https://platform.serverless.com/services/XXXXXXX/php-demo
Looking good! You'll also note that it will be using an s3 bucket it has created on my behalf for the artefacts.
Lambda Dashboard Functions

Now navigate to your AWS Lambda dashboard and click on Functions - you should see the function you just created. I have two - the one we've created in this tutorial (the bottom one in the screenshot) and one I created a few days earlier which mirrored exactly the Hello function we started off with. 

AWS Lambda invocation

Ok we are now good to go. We can either run the invoke on the command line, or click on the link provided by AWS Lambda when we deployed. Or better still, lets do both :) 

Nigels-MacBook-Pro:serverless-php-demo nigel$ serverless invoke -f demo 
Serverless: Load command run
Serverless: Load command config
Serverless: Load command config:credentials
Serverless: Load command create
Serverless: Load command install
Serverless: Load command package
Serverless: Load command deploy
Serverless: Load command deploy:function
Serverless: Load command deploy:list
Serverless: Load command deploy:list:functions
Serverless: Load command invoke
Serverless: Load command invoke:local
Serverless: Load command info
Serverless: Load command logs
Serverless: Load command login
Serverless: Load command logout
Serverless: Load command metrics
Serverless: Load command remove
Serverless: Load command rollback
Serverless: Load command rollback:function
Serverless: Load command slstats
Serverless: Load command plugin
Serverless: Load command plugin
Serverless: Load command plugin:install
Serverless: Load command plugin
Serverless: Load command plugin:uninstall
Serverless: Load command plugin
Serverless: Load command plugin:list
Serverless: Load command plugin
Serverless: Load command plugin:search
Serverless: Load command emit
Serverless: Load command config
Serverless: Load command config:credentials
Serverless: Load command rollback
Serverless: Load command rollback:function
Serverless: Invoke invoke
{
    "statusCode": 200,
    "body": "Go Serverless v1.0! Your function executed successfully!"
}
Nigels-MacBook-Pro:serverless-php-demo nigel$
Looking good on the command line.
Browser Output

Looking good on the browser too!

Metrics
Metrics

By clicking on the function name on the dashboard (see earlier dashboard image) it is possible to get some metrics. Click on the Monitoring tab and you'll see something similar to the above screenshot. Note that I had seven invocations in total (five of which were not part of this blog) and five of them failed. They failed purely because I had tried to use the initial corrupted download copy of PHP from GitHub. Took me a few minutes to figure out what had happened - and then I looped back and changed this blog accordingly, but all is good now :)