Introduction to Serverless PHP Rob Allen February 2019 Slides: https://akrabat.com/5703
A presentation at Sunshine PHP 2019 in February 2019 in Miami, FL, USA by Rob Allen
Introduction to Serverless PHP Rob Allen February 2019 Slides: https://akrabat.com/5703
Platform options Rob Allen ~ @akrabat
Serverless Serverless is all about composing software systems from a collection of cloud services. With serverless, you can lean on off-the-shelf cloud services resources for your application architecture, focus on business logic and application needs. Nate Taggart, CEO Stackery Rob Allen ~ @akrabat
FaaS • • • • • Your code Deployed to the cloud Runs when needed Scaled automatically Pay only for execution Rob Allen ~ @akrabat
Where are the servers? Rob Allen ~ @akrabat
Rob Allen ~ @akrabat
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
Benefits • • • • No need to maintain infrastructure Concentrate on application code Pay only for what you use, when you use it Language agnostic 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 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
Public web access Add the —web flag: $ wsk action update hello hello.php —web true Rob Allen ~ @akrabat
Public web access 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 (Open API Spec support) Custom domains Rate limiting Security (API keys, OAuth, CORS) Analytics Rob Allen ~ @akrabat
API Gateway $ wsk api create /myapp /hello GET hello ok: created API /myapp/hello GET for action /_/hello Rob Allen ~ @akrabat
Todo-Backend Rob Allen ~ @akrabat
Todo-Backend Add, edit and delete to-do items using OpenWhisk (on IBM) with PostgreSQL Rob Allen ~ @akrabat
Serverless Framework application manifest: 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 src/ ├── Todo/ │ ├── Todo.php │ ├── TodoMapper.php │ └── TodoTransformer.php ├── actions/ │ ├── addTodo.php │ ├── deleteTodo.php │ ├── editTodo.php │ ├── listTodos.php │ └── showTodo.php └── AppContainer.php Rob Allen ~ @akrabat
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
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 […] service… Functions… Packages… API Gateway definitions… Rules… Triggers & Feeds… Service Bindings… Packages… Functions… API Gateway definitions… Rob Allen ~ @akrabat
A working API! Rob Allen ~ @akrabat
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! Full details: akrabat.com/lambdaphp Rob Allen ~ @akrabat
Bootstrap file while (true) { // get next event $eventPayload = $lambdaRuntime->getEventPayload(); // execute handler $data = $function($eventPayload); // send result $lambdaRuntime->sendResult($data); } Rob Allen ~ @akrabat
Manage using Serverless Framework service: hello-lambdaphp provider: name: aws runtime: provided memorySize: 128 layers: php: path: layer/php Rob Allen ~ @akrabat
Hello World in Lambda function hello($eventData) { $data = json_decode($eventData, true, 512, JSON_THROW_ON_ERROR); $name = $data[‘name’] ?? ‘World’; return json_encode([‘msg’ => “Hello $name”]); } Rob Allen ~ @akrabat
Manage using Serverless Framework functions: hello: handler: handler.hello layers: - {Ref: PhpLambdaLayer} Rob Allen ~ @akrabat
Deploy using Serverless Framework $ sls deploy Serverless: Packaging service… Serverless: Uploading CloudFormation file to S3… … functions: hello: hello-lambdaphp-dev-hello layers: php: arn:aws:lambda:eu-west-2:661969457706:layer:php: Serverless: Removing old service artifacts from S3… Rob Allen ~ @akrabat
Invoke using Serverless Framework $ sls invoke -f hello —data=’{“name”:”Rob”}’ { “msg”: “Hello Rob” } Rob Allen ~ @akrabat
Bref Community maintained PHP runtime for AWS Lambda <?php require DIR.’/vendor/autoload.php’; lambda(function (array $event) { $name = $event[‘name’] ?? ‘World’; return [‘msg’ => “Hello $name”]; }); 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: src/actions/update.main layers: - {Ref: PhpLambdaLayer} environment: FLICKR_API_KEY: ${self:custom.FLICKR_API_KEY} FLICKR_USER_ID: ${self:custom.FLICKR_USER_ID} events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $apiKey = getEnvVar(‘P365_FLICKR_API_KEY’); $userId = getEnvVar(‘P365_FLICKR_USER_ID’); $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator($apiKey); $html = $pageCreator->update($year, $userId); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $apiKey = getEnvVar(‘P365_FLICKR_API_KEY’); $userId = getEnvVar(‘P365_FLICKR_USER_ID’); $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator($apiKey); $html = $pageCreator->update($year, $userId); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $apiKey = getEnvVar(‘P365_FLICKR_API_KEY’); $userId = getEnvVar(‘P365_FLICKR_USER_ID’); $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator($apiKey); $html = $pageCreator->update($year, $userId); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $apiKey = getEnvVar(‘P365_FLICKR_API_KEY’); $userId = getEnvVar(‘P365_FLICKR_USER_ID’); $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator($apiKey); $html = $pageCreator->update($year, $userId); $uploader = new Uploader($cloudFrontId); $uploader->uploadOne($year, $html, $s3Bucket); $uploader->invalidateCache([‘/’.$year]); } Rob Allen ~ @akrabat
main() function main(array $eventData) : array { $apiKey = getEnvVar(‘P365_FLICKR_API_KEY’); $userId = getEnvVar(‘P365_FLICKR_USER_ID’); $year = $eventData[‘year’] ?? date(‘Y’); $pageCreator = new PhotoPageCreator($apiKey); $html = $pageCreator->update($year, $userId); $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
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! https://joind.in/talk/e6b2a Rob Allen ~ @akrabat