A presentation at PHP fwdays’20 | Special edition by Rob Allen
Serverless A Serverless solution is one that costs you nothing to run if nobody is using it … excluding data storage costs. Paul D. Johnston Rob Allen ~ @akrabat
as a Service Storage as a Service Database as a Service Cache as a Service Auth as a Service Search as a Service Function as a Service Rob Allen ~ @akrabat
Function as a Service • • • • • Your code Deployed to the cloud Runs when needed Scaled automatically Pay only for execution Rob Allen ~ @akrabat
Use-cases Rob Allen ~ @akrabat
Use-cases Synchronous Service is invoked and provides immediate response (HTTP requests: APIs, chat bots) Rob Allen ~ @akrabat
Use-cases Synchronous Service is invoked and provides immediate response (HTTP requests: APIs, chat bots) Asynchronous Push a message which drives an action later (web hooks, timed events, database changes) Rob Allen ~ @akrabat
Challenges Rob Allen ~ @akrabat
Challenges • Start up latency Rob Allen ~ @akrabat
Challenges • Start up latency • Time limit Rob Allen ~ @akrabat
Challenges • Start up latency • Time limit • State is external Rob Allen ~ @akrabat
Challenges • • • • Start up latency Time limit State is external Different way of thinking Rob Allen ~ @akrabat
It’s about value Rob Allen ~ @akrabat
When should you use serverless? • Responding to web hooks Rob Allen ~ @akrabat
When should you use serverless? • Responding to web hooks • PWA/Static site contact form, et al. Rob Allen ~ @akrabat
When should you use serverless? • Responding to web hooks • PWA/Static site contact form, et al. • Additional features without extending current platform Rob Allen ~ @akrabat
When should you use serverless? • • • • Responding to web hooks PWA/Static site contact form, et al. Additional features without extending current platform Variable traffic levels Rob Allen ~ @akrabat
When should you use serverless? • • • • • Responding to web hooks PWA/Static site contact form, et al. Additional features without extending current platform Variable traffic levels When you want your costs to scale with traffic Rob Allen ~ @akrabat
Serverless platforms Rob Allen ~ @akrabat
Serverless platforms with PHP support Rob Allen ~ @akrabat
Concepts Rob Allen ~ @akrabat
Rob Allen ~ @akrabat
Rob Allen ~ @akrabat
Hello world in PHP Rob Allen ~ @akrabat
Hello world in PHP Rob Allen ~ @akrabat
Upload your action $ wsk action update hello hello.php ok: updated action hello Rob Allen ~ @akrabat
Run your action $ wsk action invoke hello —result { “msg”: “Hello World” } Rob Allen ~ @akrabat
Segue: How did it do this? Rob Allen ~ @akrabat
OpenWhisk’s architecture Rob Allen ~ @akrabat
Create an action Rob Allen ~ @akrabat
Invoke an action Rob Allen ~ @akrabat
Action container lifecycle • Hosts the user-written code • Controlled via two end points: /init & /run Rob Allen ~ @akrabat
Action container lifecycle • Hosts the user-written code • Controlled via two end points: /init & /run Rob Allen ~ @akrabat
End Segue Rob Allen ~ @akrabat
Turn it into an API Add the —web flag: $ wsk action update hello hello.php —web true Rob Allen ~ @akrabat
Turn it into an API Add the —web flag: $ wsk action update hello hello.php —web true $ curl https://openwhisk.ng.bluemix.net/api/v1/web/ \ 19FT_demo/default/hello.json { “msg”: “Hello World” } Rob Allen ~ @akrabat
API Gateway When you want to do more with HTTP endpoints • • • • • Route endpoint methods to actions Custom domains Rate limiting Security (API keys, OAuth, CORS) Analytics Rob Allen ~ @akrabat
API Gateway $ wsk api create /demo /hello GET hello ok: created API /demo/hello GET for action /_/hello Rob Allen ~ @akrabat
API Gateway $ curl https://ow.akrabat.com/demo/hello?name=Rob { “message”: “Hello Rob!” } Rob Allen ~ @akrabat
A Serverless API Rob Allen ~ @akrabat
Todo-Backend An OpenWhisk PHP implementation of a to-do list API Rob Allen ~ @akrabat
Serverless Framework Deployment tooling for serverless applications serverless.yml: service: ow-todo-backend provider: name: openwhisk runtime: php plugins: - serverless-openwhisk Rob Allen ~ @akrabat
Configure action functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” Rob Allen ~ @akrabat
Configure action functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” Rob Allen ~ @akrabat
Configure action functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” Rob Allen ~ @akrabat
Configure API Gateway functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” events: - http: path: /todos/{id} method: patch Rob Allen ~ @akrabat
Configure API Gateway functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” events: - http: path: /todos/{id} method: patch Rob Allen ~ @akrabat
Project files . ├── ├── ├── ├── ├── src/ vendor/ composer.json composer.lock serverless.yml Rob Allen ~ @akrabat
Project files . ├── ├── ├── ├── ├── src/ vendor/ composer.json composer.lock serverless.yml Rob Allen ~ @akrabat src/ ├── Todo/ │ ├── Todo.php │ ├── TodoMapper.php │ └── TodoTransformer.php ├── actions/ │ ├── addTodo.php │ ├── deleteTodo.php │ ├── editTodo.php │ ├── listTodos.php │ └── showTodo.php └── AppContainer.php
editTodo.php function main(array $args) : array { try { $parts = explode(“/”, $args[‘__ow_path’]); $id = (int)array_pop($parts); $data = json_decode(base64_decode($args[‘__ow_body’]), true); if (!is_array($data)) { throw new InvalidArgumentException(‘Missing body’, 400); } $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); $transformer = $container[TodoTransformer::class]; $resource = new Item($todo, $transformer, ‘todos’); $fractal = $container[Manager::class]; return [ ‘statusCode’ => 200, ‘body’ => $fractal->createData($resource)->toArray(), ]; } catch (Throwable $e) { var_dump((string)$e); $code = $e->getCode() < 400 ? $e->getCode(): 500; return [ ‘statusCode’ => $code, ‘body’ => [‘error’ => $e->getMessage()]]; } } Rob Allen ~ @akrabat
editTodo.php: Error handling function main(array $args) : array { try { // do stuff } catch (Throwable $e) { var_dump((string)$e); $code = $e->getCode() < 400 ? $e->getCode() : 500; return [ ‘statusCode’ => $code, ‘body’ => [‘error’ => $e->getMessage()]]; } } Rob Allen ~ @akrabat
editTodo.php: Error handling function main(array $args) : array { try { // do stuff } catch (Throwable $e) { var_dump((string)$e); $code = $e->getCode() < 400 ? $e->getCode() : 500; return [ ‘statusCode’ => $code, ‘body’ => [‘error’ => $e->getMessage()]]; } } Rob Allen ~ @akrabat
editTodo.php: Error handling function main(array $args) : array { try { // do stuff } catch (Throwable $e) { var_dump((string)$e); $code = $e->getCode() < 400 ? $e->getCode() : 500; return [ ‘statusCode’ => $code, ‘body’ => [‘error’ => $e->getMessage()]]; } } Rob Allen ~ @akrabat
editTodo.php: Error handling function main(array $args) : array { try { // do stuff } catch (Throwable $e) { var_dump((string)$e); $code = $e->getCode() < 400 ? $e->getCode() : 500; return [ ‘statusCode’ => $code, ‘body’ => [‘error’ => $e->getMessage()]]; } } Rob Allen ~ @akrabat
editTodo.php: Grab input $parts = explode(“/”, $args[‘__ow_path’]); $id = (int)array_pop($parts); Rob Allen ~ @akrabat
editTodo.php: Grab input $parts = explode(“/”, $args[‘__ow_path’]); $id = (int)array_pop($parts); $body = base64_decode($args[‘__ow_body’]; $data = json_decode($body), true); if (!is_array($data)) { throw new Exception(‘Missing body’, 400); } Rob Allen ~ @akrabat
editTodo.php: Grab input $parts = explode(“/”, $args[‘__ow_path’]); $id = (int)array_pop($parts); $body = base64_decode($args[‘__ow_body’]; $data = json_decode($body), true); if (!is_array($data)) { throw new Exception(‘Missing body’, 400); } Rob Allen ~ @akrabat
editTodo.php: Do the work $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); Rob Allen ~ @akrabat
editTodo.php: Do the work $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); Rob Allen ~ @akrabat
editTodo.php: Do the work $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); Rob Allen ~ @akrabat
editTodo.php: Present results $transformer = $container[TodoTransformer::class]; $resource = new Item($todo, $transformer, ‘todos’); $fractal = $container[Manager::class]; $output = $fractal->createData($resource); return [ ‘statusCode’ => 200, ‘body’ => $output->toArray(), ]; Rob Allen ~ @akrabat
editTodo.php: Present results $transformer = $container[TodoTransformer::class]; $resource = new Item($todo, $transformer, ‘todos’); $fractal = $container[Manager::class]; $output = $fractal->createData($resource); return [ ‘statusCode’ => 200, ‘body’ => $output->toArray(), ]; Rob Allen ~ @akrabat
editTodo.php: Present results $transformer = $container[TodoTransformer::class]; $resource = new Item($todo, $transformer, ‘todos’); $fractal = $container[Manager::class]; $output = $fractal->createData($resource); return [ ‘statusCode’ => 200, ‘body’ => $output->toArray(), ]; Rob Allen ~ @akrabat
Deploy $ serverless deploy Serverless: Packaging Serverless: Compiling Serverless: Compiling Serverless: Compiling Serverless: Compiling Serverless: Compiling Serverless: Compiling Serverless: Deploying Serverless: Deploying Serverless: Deploying […] Rob Allen ~ @akrabat service… Functions… Packages… API Gateway definitions… Rules… Triggers & Feeds… Service Bindings… Packages… Functions… API Gateway definitions…
A working API! Rob Allen ~ @akrabat
Rob Allen ~ @akrabat
AWS Lambda with PHP Only sensibly possible since November 2018 with the introduction of layers Rob Allen ~ @akrabat
AWS Lambda with PHP Only sensibly possible since November 2018 with the introduction of layers Process: 1. Create a layer containing: 1. the PHP executable 2. a bootstrap script 2. Write the PHP function! Rob Allen ~ @akrabat
.
Bref • Maintained PHP runtimes for AWS Lambda • Deployment via Serverless Framework • Great documentation! Rob Allen ~ @akrabat
Bref PHP function <?php declare(strict_types=1); require DIR . ‘/vendor/autoload.php’; return function($event) { return ‘Hello ’ . ($event[‘name’] ?? ‘world’); } Rob Allen ~ @akrabat
serverless.yml service: helloapp provider: name: aws runtime: provided functions: hello: handler: index.php layers: - ${bref:layer.php-73} Rob Allen ~ @akrabat
serverless.yml service: helloapp provider: name: aws runtime: provided functions: hello: handler: index.php layers: - ${bref:layer.php-73} Rob Allen ~ @akrabat
serverless.yml service: helloapp provider: name: aws runtime: provided functions: hello: handler: index.php layers: - ${bref:layer.php-73} Rob Allen ~ @akrabat
Deploy $ serverless deploy Serverless: Packaging service… … Serverless: Stack update finished… Service Information service: helloapp stage: dev stack: helloapp-dev functions: hello: helloapp-dev-hello Rob Allen ~ @akrabat
Run $ serverless invoke -f hello “Hello world” Rob Allen ~ @akrabat
Run $ serverless invoke -f hello “Hello world” $ serverless invoke -f hello -d ‘{“name”: “Rob”}’ “Hello Rob” Rob Allen ~ @akrabat
Run locally in Docker $ serverless invoke local —docker -f hello Serverless: Building Docker image… … REPORT RequestId: 6a653a94-ee51-1f3e-65c7-1f1954842f29 Init Duration: 265.93 ms Duration: 145.37 ms Billed Duration: 200 ms Memory Size: 1024 MB Max Memory Used: 27 MB “Hello world” Xdebug also works! Rob Allen ~ @akrabat
Add AWS API Gateway serverless.yml: functions: hello: handler: index.php … events: - http: “GET /hello” - http: “GET /hi/{name}” Rob Allen ~ @akrabat
Return a PSR-15 RequestHandler class HelloHandler implements RequestHandlerInterface { public function handle(ServerRequest $request): Response { $name = ($request->getQueryParams()[‘name’] ?? ‘world’); $body = ‘Hello ’ . $name; return new Response(200, [‘Content-Type’ => ‘text/plain’], $body); } } Rob Allen ~ @akrabat
Deploy $ serverless deploy Serverless: Packaging service… … Service Information service: helloapp stage: dev stack: helloapp-dev endpoints: GET - https://l1v6cz13zb.execute-api.eu-west-2 .amazonaws.com/dev/hello Rob Allen ~ @akrabat
Test $ curl -i https://l1v6cz…naws.com/dev/hello?name=Rob HTTP/2 200 content-type: text/plain content-length: 9 Hello Rob Rob Allen ~ @akrabat
Project 365 My photo-a-day website Rob Allen ~ @akrabat
Project 365 Static website to display my photo-a-day picture for each day of the year. • Hosted on S3 • CloudFront CDN • Lambda/PHP function Rob Allen ~ @akrabat
Lambda/PHP function Rob Allen ~ @akrabat
Serverless configuration functions: update: handler: index.php events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat
Serverless configuration functions: update: handler: index.php events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat
Serverless configuration functions: update: handler: index.php events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator(); $html = $pageCreator->update($year); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator(); $html = $pageCreator->update($year); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator(); $html = $pageCreator->update($year); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator(); $html = $pageCreator->update($year); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator(); $html = $pageCreator->update($year); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
Fetch photos from Flickr $url = ‘?’ . http_build_query([ ‘api_key’ => $this->flickrApiKey, ‘user_id’ => $flickrUserId, ‘extras’ => ‘url_z, date_taken, owner_name’, ‘method’ => ‘flickr.photos.search’, ‘tags’ => $year, ]); $response = $this->client->get($url); $data = json_decode($response->getBody(), true); return $data[‘photos’]; Rob Allen ~ @akrabat
Fetch photos from Flickr $url = ‘?’ . http_build_query([ ‘api_key’ => $this->flickrApiKey, ‘user_id’ => $flickrUserId, ‘extras’ => ‘url_z, date_taken, owner_name’, ‘method’ => ‘flickr.photos.search’, ‘tags’ => $year, ]); $response = $this->client->get($url); $data = json_decode($response->getBody(), true); return $data[‘photos’]; Rob Allen ~ @akrabat
Fetch photos from Flickr $url = ‘?’ . http_build_query([ ‘api_key’ => $this->flickrApiKey, ‘user_id’ => $flickrUserId, ‘extras’ => ‘url_z, date_taken, owner_name’, ‘method’ => ‘flickr.photos.search’, ‘tags’ => $year, ]); $response = $this->client->get($url); $data = json_decode($response->getBody(), true); return $data[‘photos’]; Rob Allen ~ @akrabat
Fetch photos from Flickr $url = ‘?’ . http_build_query([ ‘api_key’ => $this->flickrApiKey, ‘user_id’ => $flickrUserId, ‘extras’ => ‘url_z, date_taken, owner_name’, ‘method’ => ‘flickr.photos.search’, ‘tags’ => $year, ]); $response = $this->client->get($url); $data = json_decode($response->getBody(), true); return $data[‘photos’]; Rob Allen ~ @akrabat
Upload to S3 $s3 = new S3Client([ ‘version’ => ‘latest’, ‘region’ => getenv(‘AWS_DEFAULT_REGION’) ]); $s3->putObject([ ‘Bucket’ => $bucketName, ‘ACL’ => ‘public-read’, ‘Key’ => “/$year.html”, ‘Body’ => $data, ‘ContentType’ => ‘text/html’, ]); Rob Allen ~ @akrabat
Upload to S3 $s3 = new S3Client([ ‘version’ => ‘latest’, ‘region’ => getenv(‘AWS_DEFAULT_REGION’) ]); $s3->putObject([ ‘Bucket’ => $bucketName, ‘ACL’ => ‘public-read’, ‘Key’ => “/$year.html”, ‘Body’ => $data, ‘ContentType’ => ‘text/html’, ]); Rob Allen ~ @akrabat
Upload to S3 $s3 = new S3Client([ ‘version’ => ‘latest’, ‘region’ => getenv(‘AWS_DEFAULT_REGION’) ]); $s3->putObject([ ‘Bucket’ => $bucketName, ‘ACL’ => ‘public-read’, ‘Key’ => “/$year.html”, ‘Body’ => $data, ‘ContentType’ => ‘text/html’, ]); Rob Allen ~ @akrabat
Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat
Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat
Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat
Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat
The finished website Rob Allen ~ @akrabat
To sum up Rob Allen ~ @akrabat
Resources • https://akrabat.com • https://www.martinfowler.com/articles/serverless.html • https://github.com/akrabat/ow-php-todo-backend • https://github.com/akrabat/project365-photos-website • http://www.openwhisk.org • https://aws.amazon.com/lambda/ • https://bref.sh Rob Allen ~ @akrabat
Thank you! Rob Allen - http://akrabat.com - @akrabat
APIs and microservices are how we build modern web applications and serverless technologies make this easy. This session will show you how serverless applications are built and how you can leverage your PHP skills to build APIs of all shapes and sizes. We will cover how to use your current knowledge to build serverless applications in PHP, leveraging the API Gateway to build robust APIs quickly and easily. By the end of the session, you’ll be well placed to design and build your own microservices that take full advantage of the power of serverless technologies.
Here’s what was said about this presentation on social media.
Very inspiring talk. thanks.
— Alex Li Sin Cin (@crazy_eco) June 6, 2020
@akrabat Google is working on #faas for #php with #psr7 support: https://t.co/ZH9i2KG0i1 @phpfwdays
— Dominik Zogg (@dominikzogg) June 6, 2020