Yes, you read that right. You can run PHP on AWS Lambda. But you need to use Mathieu Napoli’s “Bref” package (link), which packages your application into a binary that’s actually run in a Node.js script. Clever, right?

This guide will show you the steps to package your code into AWS Lambda functions, that can act as fully-functioning web services in themselves, with custom domain names and SSL. My intention is not to show you how to develop an application, but how to package your existing application code as a serverless service in the cloud.

My Book Launch

For my book’s launch in November 2018 I needed to set up a simple web page, where readers of the book could claim some bonus materials I’d written specially for them. They needed to be able to click the link, see the website, and enter their email address. Then I’d send them their bonus materials.

I already have a personal email service set up within AWS Lambda that handles all of my email newsletters. This separation of concerns means that I can focus on handling the front end without needing to rewrite boilerplate code for sending out emails.

So therefore all I needed was a simple email form, and a POST route inside the application that would link to my personal email service. So I didn’t see the need for a kitchen-sink solution like Laravel or Symfony, even though I love those PHP frameworks.

I actually ended up using the Slim framework because of its, well, slimness. In about 50 lines I wrote a local script that could handle the single homepage and the POST route to take the email address and pass it onto my email service.

Installing Bref

Bref is a composer package that you can easily install into your project:

composer require mnapoli/bref

However, it has an NPM dependency called “Serverless” which will actually handle the API calls to AWS Lambda:

npm i -g serverless

Then, you’ll need to configure your system to make those same API calls to AWS Lambda. In your AWS Management Console, create an IAM user, and make a note of the access key ID, and the secret key.

apt install python-pip

pip install awscli

aws configure

When the last command prompts you to enter your access key ID and the secret key, paste them in. If you get problems, then Google is your friend!

Now we’re finally ready to initialise Bref:

./vendor/bin/bref init

This will create a serverless.yml file containing some configuration that we can tweak, and most importantly a bref.php file. This PHP file is what actually gets executed by the Node.js script, and by default it looks like this:

<?php



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



λ(function (array $event) {

return [

'hello' => $event['name'] ?? 'world',

];

});

Once deployed, all this AWS Lambda will do is return ‘hello world’. Not particularly useful — but think about how powerful this is. We can deploy any code we want inside this Lambda function! Mathieu Napoli has allowed us to run serverless applications with PHP.

But I Need a Website!

While this is awesome, it doesn’t exactly fit my needs. I need an application that can respond to HTTP requests and give responses. Luckily, Bref can handle this too. I set up my bref.php file with its Slim framework adapter:

<?php



use Bref\Bridge\Slim\SlimAdapter;



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



$slim = new Slim\App;

$slim->get('/dev', function ($request, $response) {

$response->getBody()->write('Hello world!');

return $response;

});



$app = new \Bref\Application;

$app->httpHandler(new SlimAdapter($slim));

$app->run();

This app will respond to the /dev URI and return “Hello world!” So rather than just being a simple function, it’s an HTTP endpoint that can respond to requests.

I will caution you, however, that Lambda is intended for small, one-off scripts that act as functions, not for full websites. Each Lambda function fulfils a single role, like a micro-service. It’s not designed to contain an entire monolithic application, because for one thing it can’t handle persistent sessions without the aid of an external data storage service like Elasticache.

Luckily, all my simple web page application needs is a / route for the homepage and a /download post route, so we’re fulfilling this basic requirement. An application with full user authentication, session handling, and database calls requires a lot more work and should be separated out into different microservices. My bref.php file ended up looking roughly like this:

<?php



use Bref\Bridge\Slim\SlimAdapter;



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



$slim = new Slim\App;

$slim->get('/', function ($request, $response) {

$response->getBody()->write(/* ... */);

return $response;

});

$slim->post('/download', $download = function ($request, $response) {

$email = $request->getParsedBody()['email'];



if (!$email) {

return;

} /* Send API request to email service... */

}) $app = new \Bref\Application;

$app->httpHandler(new SlimAdapter($slim));

$app->run();

Testing this simple web application is easy in a local environment by using the following server command and entering ‘127.0.0.1:8000’ into the web browser:

php -S 127.0.0.1:8000 bref.php

Deploying Our Application

Bref offers a simple command to deploy the application to AWS Lambda:

./vendor/bin/bref deploy [--dry-run for testing]

This command will attempt to create the Lambda function, an API Gateway endpoint, and an S3 bucket. So ensure your locally configured IAM user has all the requisite permissions to do this. For local development you can use an admin user, but I really don’t recommend that if you’re not locking that IAM user account down by IP address.

If all goes well, you should get a success message that spits out your API Gateway endpoint, something like the following:



Serverless: Packaging service...

Serverless: Excluding development dependencies...

Serverless: Uploading CloudFormation file to S3...

Serverless: Uploading artifacts...

Serverless: Uploading service .zip file to S3 (116.75 MB)...

Serverless: Validating template...

Serverless: Updating Stack...

Serverless: Checking Stack update progress...

..

Serverless: Stack update finished...

Service Information

service: test

stage: dev

region: us-east-1

stack: test-dev

api keys:

None

endpoints:

ANY - https://xxx.execute-api.us-east-1.amazonaws.com/dev

ANY -

functions:

main: test-dev-main

Serverless: Removing old service artifacts from S3...

Deployment success

8/8 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 59 secs⏎ root@ubuntu ~/d/w/test# ./vendor/bin/bref deployServerless: Packaging service...Serverless: Excluding development dependencies...Serverless: Uploading CloudFormation file to S3...Serverless: Uploading artifacts...Serverless: Uploading service .zip file to S3 (116.75 MB)...Serverless: Validating template...Serverless: Updating Stack...Serverless: Checking Stack update progress.....Serverless: Stack update finished...Service Informationservice: teststage: devregion: us-east-1stack: test-devapi keys:Noneendpoints:ANY - https://xxx.execute-api.us-east-1.amazonaws.com/dev/{proxy+ functions:main: test-dev-mainServerless: Removing old service artifacts from S3...Deployment success8/8 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 59 secs⏎

Copy and paste the first endpoint URL into the browser, and it should show the output of your application.

The AWS API Gateway always adds a /dev prefix to your URIs because it separates your endpoints into stages, ‘dev’ being one of them. In my earlier bref.php example, I initially needed to duplicate all the Slim routes with /dev/... prefixes so they’d show up with this endpoint.

There are endless ways to customise this to your liking, and I can’t possibly hope to cover them all in this guide. So I encourage you to tinker with your own bref.php file and codebase to get this working the way you want it to.

Setting a Custom Domain with Your API Gateway / Lambda Application

Okay great, but we can’t go and publish this ugly URL to the world, we need a professional looking custom domain name. Setting this was actually the most time-consuming part of the process, and the least documented. So hopefully my guide will help you avoid a lot of the time I spent tearing my hair out!

In my case, I wanted my entire domain to point to this API Gateway endpoint. So in the AWS Management Console, I needed to go to the AWS Certificate Manager Service to create two certificates:

*.24lawsofstorytelling.com 24lawsofstorytelling.com

This separation is crucial — I could get www.24lawsofstorytelling.com to work fine, but the bare domain kept giving me “Invalid Certificate” errors in Google Chrome. It turns out that the wildcard certificate is rejected when used with the bare domain, so you need an individual second certificate against the bare domain.

Simply request a public certificate for the wildcard and the bare domain:

It offers you the choice of validating your ownership of the domain via DNS or Email. I tried the DNS option and it took over three hours to validate, while the email option is instant, so I recommend you go for that one. You’ll need access to the ‘admin@…’ email address, though. The DNS option on the other hand requires that you add a CNAME record to your domain’s DNS.

Once your certificates are issued, go back to the API Gateway and on the left-hand menu, select ‘Custom Domain Names’. You’ll see a blue button with ‘+ Create Custom Domain Name’, which takes you to a form where you can add your service’s intended domain names and link them to the certificates you just created.

When it asked me about ‘Base Path Mapping’, I had already created a new stage in my API Gateway called ‘prod’ — short for ‘production’ — and pointed my domain at this stage. This means that www.24lawsofstorytelling.com would point to xxx.execute-api.us-east-1.amazonaws.com/prod . This is considered good practice since you can separate out these endpoints for live and dev environments.

Once again I must stress — for the ‘www.’ subdomain I linked this to the wildcard certificate, but for the bare domain I needed to link it to the bare domain certificate, not the wildcard one. This took me ages to work out! Here’s the finished article in my case:

We still have one step left to do. We need to now point our domain name at the ‘Target Domain Name’ in the screenshot above. This involves creating ‘A-records’ in our DNS to this new name. For me, this took hours of working out how to do, because I’d always thought that A-records were for IPv4 addresses only.

The AWS docs are not very clear on the fact that you need to use ‘A-record aliasing’ in your DNS. Now, this is something that only seems to be available on AWS’s Route 53 DNS service. I was hosting the DNS for my domain on Cloudflare, and while it may be possible to achieve this with Cloudflare, I kept getting ‘Too Many Redirects’ errors in my browser when trying to access it.

So, I popped over to Route 53 in AWS and added the ‘Hosted Zone’ for 24lawsofstorytelling.com. This is just like setting up the DNS for any other domain, so I needed to re-add my MX records, CNAMEs, etc. from before:

I copied all four nameservers from the NS row (under Type in the above screenshot) and copied them into the custom nameserver list in GoDaddy, where I bought the domain name from.

Once that had eventually propagated, the website then worked! I had a fully-functioning single page web application running purely in AWS Lambda, all set up and working for my book’s readers. It’s automatically set up to scale and serve internationally, so I don’t have to worry about any server maintenance — how cool is that!

Behold the final result in all its glory, a web page where my book’s readers can grab their bonus materials, the 18 books and pieces of content I used to write The 24 Laws of Storytelling.

The website’s live URL: https://www.24lawsofstorytelling.com/