Serverless PHP Applications

A presentation at Midwest PHP in April 2020 in Minneapolis, MN, USA by Rob Allen

Slide 1

Slide 1

Serverless PHP Applications Rob Allen, April 2020

Slide 2

Slide 2

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

Slide 3

Slide 3

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

Slide 4

Slide 4

Function as a Service • • • • • Your code Deployed to the cloud Runs when needed Scaled automatically Pay only for execution Rob Allen ~ @akrabat

Slide 5

Slide 5

Use-cases Rob Allen ~ @akrabat

Slide 6

Slide 6

Use-cases Synchronous Service is invoked and provides immediate response (HTTP requests: APIs, chat bots) Rob Allen ~ @akrabat

Slide 7

Slide 7

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

Slide 8

Slide 8

Challenges Rob Allen ~ @akrabat

Slide 9

Slide 9

Challenges • Start up latency Rob Allen ~ @akrabat

Slide 10

Slide 10

Challenges • Start up latency • Time limit Rob Allen ~ @akrabat

Slide 11

Slide 11

Challenges • Start up latency • Time limit • State is external Rob Allen ~ @akrabat

Slide 12

Slide 12

Challenges • • • • Start up latency Time limit State is external Different way of thinking Rob Allen ~ @akrabat

Slide 13

Slide 13

It’s about value Rob Allen ~ @akrabat

Slide 14

Slide 14

When should you use serverless? • Responding to web hooks Rob Allen ~ @akrabat

Slide 15

Slide 15

When should you use serverless? • Responding to web hooks • PWA/Static site contact form, et al. Rob Allen ~ @akrabat

Slide 16

Slide 16

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

Slide 17

Slide 17

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

Slide 18

Slide 18

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

Slide 19

Slide 19

Serverless platforms Rob Allen ~ @akrabat

Slide 20

Slide 20

Serverless platforms with PHP support Rob Allen ~ @akrabat

Slide 21

Slide 21

Slide 22

Slide 22

Concepts Rob Allen ~ @akrabat

Slide 23

Slide 23

Rob Allen ~ @akrabat

Slide 24

Slide 24

Rob Allen ~ @akrabat

Slide 25

Slide 25

Hello world in PHP Rob Allen ~ @akrabat

Slide 26

Slide 26

Hello world in PHP Rob Allen ~ @akrabat

Slide 27

Slide 27

Upload your action $ wsk action update hello hello.php ok: updated action hello Rob Allen ~ @akrabat

Slide 28

Slide 28

Run your action $ wsk action invoke hello —result { “msg”: “Hello World” } Rob Allen ~ @akrabat

Slide 29

Slide 29

Segue: How did it do this? Rob Allen ~ @akrabat

Slide 30

Slide 30

OpenWhisk’s architecture Rob Allen ~ @akrabat

Slide 31

Slide 31

Create an action Rob Allen ~ @akrabat

Slide 32

Slide 32

Invoke an action Rob Allen ~ @akrabat

Slide 33

Slide 33

Action container lifecycle • Hosts the user-written code • Controlled via two end points: /init & /run Rob Allen ~ @akrabat

Slide 34

Slide 34

Action container lifecycle • Hosts the user-written code • Controlled via two end points: /init & /run Rob Allen ~ @akrabat

Slide 35

Slide 35

End Segue Rob Allen ~ @akrabat

Slide 36

Slide 36

Turn it into an API Add the —web flag: $ wsk action update hello hello.php —web true Rob Allen ~ @akrabat

Slide 37

Slide 37

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

Slide 38

Slide 38

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

Slide 39

Slide 39

API Gateway $ wsk api create /demo /hello GET hello ok: created API /demo/hello GET for action /_/hello Rob Allen ~ @akrabat

Slide 40

Slide 40

API Gateway $ curl https://ow.akrabat.com/demo/hello?name=Rob { “message”: “Hello Rob!” } Rob Allen ~ @akrabat

Slide 41

Slide 41

A Serverless API Rob Allen ~ @akrabat

Slide 42

Slide 42

Todo-Backend An OpenWhisk PHP implementation of a to-do list API Rob Allen ~ @akrabat

Slide 43

Slide 43

Serverless Framework Deployment tooling for serverless applications serverless.yml: service: ow-todo-backend provider: name: openwhisk runtime: php plugins: - serverless-openwhisk Rob Allen ~ @akrabat

Slide 44

Slide 44

Configure action functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” Rob Allen ~ @akrabat

Slide 45

Slide 45

Configure action functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” Rob Allen ~ @akrabat

Slide 46

Slide 46

Configure action functions: edit-todo: handler: “src/actions/editTodo.main” name: “todo-backend/edit-todo” Rob Allen ~ @akrabat

Slide 47

Slide 47

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

Slide 48

Slide 48

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

Slide 49

Slide 49

Project files . ├── ├── ├── ├── ├── src/ vendor/ composer.json composer.lock serverless.yml Rob Allen ~ @akrabat

Slide 50

Slide 50

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

Slide 51

Slide 51

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

Slide 52

Slide 52

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

Slide 53

Slide 53

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

Slide 54

Slide 54

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

Slide 55

Slide 55

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

Slide 56

Slide 56

editTodo.php: Grab input $parts = explode(“/”, $args[‘__ow_path’]); $id = (int)array_pop($parts); Rob Allen ~ @akrabat

Slide 57

Slide 57

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

Slide 58

Slide 58

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

Slide 59

Slide 59

editTodo.php: Do the work $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); Rob Allen ~ @akrabat

Slide 60

Slide 60

editTodo.php: Do the work $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); Rob Allen ~ @akrabat

Slide 61

Slide 61

editTodo.php: Do the work $container = new AppContainer($args); $mapper = $container[TodoMapper::class]; $todo = $mapper->loadById($id); $mapper->update($todo, $data); Rob Allen ~ @akrabat

Slide 62

Slide 62

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

Slide 63

Slide 63

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

Slide 64

Slide 64

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

Slide 65

Slide 65

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

Slide 66

Slide 66

A working API! Rob Allen ~ @akrabat

Slide 67

Slide 67

Rob Allen ~ @akrabat

Slide 68

Slide 68

AWS Lambda with PHP Only sensibly possible since November 2018 with the introduction of layers Rob Allen ~ @akrabat

Slide 69

Slide 69

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

Slide 70

Slide 70

.

Slide 71

Slide 71

Bref • Maintained PHP runtimes for AWS Lambda • Deployment via Serverless Framework • Great documentation! Rob Allen ~ @akrabat

Slide 72

Slide 72

Bref PHP function <?php declare(strict_types=1); require DIR . ‘/vendor/autoload.php’; return function($event) { return ‘Hello ’ . ($event[‘name’] ?? ‘world’); } Rob Allen ~ @akrabat

Slide 73

Slide 73

serverless.yml service: helloapp provider: name: aws runtime: provided functions: hello: handler: index.php layers: - ${bref:layer.php-73} Rob Allen ~ @akrabat

Slide 74

Slide 74

serverless.yml service: helloapp provider: name: aws runtime: provided functions: hello: handler: index.php layers: - ${bref:layer.php-73} Rob Allen ~ @akrabat

Slide 75

Slide 75

serverless.yml service: helloapp provider: name: aws runtime: provided functions: hello: handler: index.php layers: - ${bref:layer.php-73} Rob Allen ~ @akrabat

Slide 76

Slide 76

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

Slide 77

Slide 77

Run $ serverless invoke -f hello “Hello world” Rob Allen ~ @akrabat

Slide 78

Slide 78

Run $ serverless invoke -f hello “Hello world” $ serverless invoke -f hello -d ‘{“name”: “Rob”}’ “Hello Rob” Rob Allen ~ @akrabat

Slide 79

Slide 79

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

Slide 80

Slide 80

Add AWS API Gateway serverless.yml: functions: hello: handler: index.php … events: - http: “GET /hello” - http: “GET /hi/{name}” Rob Allen ~ @akrabat

Slide 81

Slide 81

Return a PSR-15 RequestHandler class HelloHandler implements RequestHandlerInterface { public function handle(ServerRequest $request): Response { $name = ($request->getQueryParams()[‘name’] ?? ‘world’); $data = [‘msg’ => ‘Hello ’ . $name]; return new Response(200, [‘Content-Type’ => ‘text/plain’], json_encode($data)); } } Rob Allen ~ @akrabat

Slide 82

Slide 82

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

Slide 83

Slide 83

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

Slide 84

Slide 84

Project 365 My photo-a-day website Rob Allen ~ @akrabat

Slide 85

Slide 85

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

Slide 86

Slide 86

Lambda/PHP function Rob Allen ~ @akrabat

Slide 87

Slide 87

Serverless configuration functions: update: handler: index.php events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat

Slide 88

Slide 88

Serverless configuration functions: update: handler: index.php events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat

Slide 89

Slide 89

Serverless configuration functions: update: handler: index.php events: - schedule: name: project365-build rate: cron(0 */2 * * ? *) Rob Allen ~ @akrabat

Slide 90

Slide 90

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

Slide 91

Slide 91

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

Slide 92

Slide 92

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

Slide 93

Slide 93

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

Slide 94

Slide 94

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

Slide 95

Slide 95

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

Slide 96

Slide 96

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

Slide 97

Slide 97

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

Slide 98

Slide 98

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

Slide 99

Slide 99

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

Slide 100

Slide 100

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

Slide 101

Slide 101

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

Slide 102

Slide 102

Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat

Slide 103

Slide 103

Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat

Slide 104

Slide 104

Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat

Slide 105

Slide 105

Invalidate CloudFront $cft = new CloudFrontClient([ .. ]); $result = $cft->createInvalidation([ ‘DistributionId’ => $cloudFrontId, ‘InvalidationBatch’ => [ ‘CallerReference’ => date(‘YmdHis’), ‘Paths’ => [ ‘Items’ => [“/$year.html”], ‘Quantity’ => 1, ], ], ]); Rob Allen ~ @akrabat

Slide 106

Slide 106

The finished website Rob Allen ~ @akrabat

Slide 107

Slide 107

To sum up Rob Allen ~ @akrabat

Slide 108

Slide 108

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

Slide 109

Slide 109

Thank you! Rob Allen - http://akrabat.com - @akrabat