Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Implementing Serverless PHP

Rob Allen
December 21, 2017

Implementing Serverless PHP

Serverless applications have a number of benefits and JavaScript is the most common language to write serverless functions in. Why not PHP? In this talk, I will discuss how I implemented first class PHP support into the Apache OpenWhisk platform. We’ll start by looking at OpenWhisk’s architecture and what happens when you invoke a function. Then, I’ll show you how I implemented the PHP support and walk though some example PHP serverless actions.

Presented at NomadPHP, December 2017

Rob Allen

December 21, 2017
Tweet

More Decks by Rob Allen

Other Decks in Technology

Transcript

  1. Serverless? The first thing to know about serverless computing is

    that "serverless" is a pretty bad name to call it. - Brandon Butler, Network World Rob Allen ~ @akrabat
  2. AKA: Functions as a Service • A runtime to execute

    your functions • No capacity planning or load balancing; just tasks being executed. • Pay for execution, not when idle Rob Allen ~ @akrabat
  3. ThoughtWorks Technology Radar "Our teams like the serverless approach; it's

    working well for us and we consider it a valid architectural choice."" 2017 Technology Radar Rob Allen ~ @akrabat
  4. 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) Streaming Continuous data flow to be processed Rob Allen ~ @akrabat
  5. Benefits • No need to think about servers • Concentrate

    on application code • Pay only for what you use, when you use it • Language agnostic: NodeJS, Swift, Python, Java, C#, etc Rob Allen ~ @akrabat
  6. Challenges • Start up latency • Time limit • State

    is external • DevOps is still a thing Rob Allen ~ @akrabat
  7. OpenWhisk OpenSource; multiple providers: IBM RedHat Adobe (for Adobe Cloud

    Platform APIs) &, of course, self-hosted Rob Allen ~ @akrabat
  8. Hello world in JS hello.js: 1 function main(params) 2 {

    3 name = params.name || "World" 4 return {msg: 'Hello ' + name} 5 } Create action: $ wsk action create helloJS hello.js --kind nodejs:6 ok: updated action helloJS Rob Allen ~ @akrabat
  9. Hello world in JS Execute: $ wsk action invoke -r

    helloJS -p name Rob { "msg": "Hello Rob" } or: $ curl -k https://192.168.33.13/api/v1/web/guest/default/helloJS.json { "msg": "Hello World" } Rob Allen ~ @akrabat
  10. Hello world in PHP hello.php: 1 <?php 2 function main(array

    $args) : array 3 { 4 $name = $args["name"] ?? "World"; 5 return [ "msg" => "Hello $name" ]; 6 } Rob Allen ~ @akrabat
  11. The old way: Dockerise it Create a bash script called

    exec: 1 #!/bin/bash 2 3 # Install PHP 4 if [ ! -f /usr/bin/php ]; then 5 echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" \ 6 >> /etc/apk/repositories 7 apk add --update php7 php7-json 8 fi 9 10 # Run PHP action 11 /usr/bin/php -r 'require "/action/hello.php"'; 12 echo json_encode(main(json_decode($argv[1], true)));' -- "$@" Rob Allen ~ @akrabat
  12. Create & execute action 1 $ zip -r hello.zip hello.php

    exec 2 3 $ wsk action create helloPHP hello.zip --native 4 ok: updated action helloPHP 5 6 $ wsk action invoke -r helloPHP -p name Rob 7 { 8 "msg": "Hello Rob" 9 } Rob Allen ~ @akrabat
  13. Action container lifecycle • Hosts the user-written code • Controlled

    via two end points: /init & /run Rob Allen ~ @akrabat
  14. Action container API input { "value": { "name" : "helloPHP",

    "main" : "main", "binary": false, "code" : "<?php …", } } { "value": { "name" : "Rob", } } output { "OK": true} `{"msg": "Hello Rob" } Rob Allen ~ @akrabat
  15. Writing a PHP action container We need: • A container

    • Code to handle endpoints (router.php) • Execute the user code (runner.php) Rob Allen ~ @akrabat
  16. Dockerfile FROM php:7.1-alpine # copy required files ADD router.php /action

    ADD runner.php /action # Start webserver on port 8080 EXPOSE 8080 CMD [ "php", "-S", "0.0.0.0:8080", "/action/router.php" ] Rob Allen ~ @akrabat
  17. router.php 1 <?php 2 if ($_SERVER['REQUEST_URI'] == '/init') { 3

    $result = init(); 4 } elseif ($_SERVER['REQUEST_URI'] == '/run') { 5 $result = run(); 6 } 7 8 /* send response */ 9 header('Content-Type: application/json'); 10 echo json_encode((object)$result); Rob Allen ~ @akrabat
  18. router.php: init() 1 function init() 2 { 3 $post =

    file_get_contents('php://input'); 4 $data = json_decode($post, true)['value']; 5 6 file_put_contents('index.php', $data['code']); 7 8 $config = ['function' => $data['main'], 'file' => 'index.php']; 9 file_put_contents('config.json', json_encode($config)); 10 11 return ["OK" => true]; 12 } Rob Allen ~ @akrabat
  19. router.php: run() 1 function run() 2 { 3 $args =

    file_get_contents('config.json'); 4 $stdin = json_decode(file_get_contents('php://input'), true)['value']; 5 6 list($rtncode, $stdout, $stderr) = runAction($args, $stdin); 7 8 file_put_contents("php://stderr", $stderr); 9 file_put_contents("php://stdout", $stdout); 10 11 $pos = strrpos($stdout, PHP_EOL) + 1; 12 $lastLine = trim(substr($stdout, $pos)); 13 14 return $lastLine; 15 } Rob Allen ~ @akrabat
  20. router.php: run() 1 function run() 2 { 3 $args =

    file_get_contents('config.json'); 4 $stdin = json_decode(file_get_contents('php://input'), true)['value']; 5 6 list($rtncode, $stdout, $stderr) = runAction($args, $stdin); 7 8 file_put_contents("php://stderr", $stderr); 9 file_put_contents("php://stdout", $stdout); 10 11 $pos = strrpos($stdout, PHP_EOL) + 1; 12 $lastLine = trim(substr($stdout, $pos)); 13 14 return $lastLine; 15 } Rob Allen ~ @akrabat
  21. router.php: runAction() 1 function runAction(array $args, string $stdin = '')

    : array 2 { 3 $args = implode(' ', array_map('escapeshellarg', $args)); 4 5 /* execute runner and open file pointers for input/output */ 6 $pipes = []; 7 $process = proc_open( 8 '/usr/local/bin/php -f runner.php' . $args, 9 [ 10 0 => ['pipe', 'r'], /* descriptor for stdin */ 11 1 => ['pipe', 'w'], /* descriptor for stdout */ 12 2 => ['pipe', 'w'] /* descriptor for stderr */ 13 ], 14 $pipes, 15 ); Rob Allen ~ @akrabat
  22. router.php: runAction() 1 function runAction(array $args, string $stdin = '')

    : array 2 { 3 $args = implode(' ', array_map('escapeshellarg', $args)); 4 5 /* execute runner and open file pointers for input/output */ 6 $pipes = []; 7 $process = proc_open( 8 '/usr/local/bin/php -f runner.php' . $args, 9 [ 10 0 => ['pipe', 'r'], /* descriptor for stdin */ 11 1 => ['pipe', 'w'], /* descriptor for stdout */ 12 2 => ['pipe', 'w'] /* descriptor for stderr */ 13 ], 14 $pipes, 15 ); Rob Allen ~ @akrabat
  23. router.php: runAction() 1 function runAction(array $args, string $stdin = '')

    : array 2 { 3 $args = implode(' ', array_map('escapeshellarg', $args)); 4 5 /* execute runner and open file pointers for input/output */ 6 $pipes = []; 7 $process = proc_open( 8 '/usr/local/bin/php -f runner.php' . $args, 9 [ 10 0 => ['pipe', 'r'], /* descriptor for stdin */ 11 1 => ['pipe', 'w'], /* descriptor for stdout */ 12 2 => ['pipe', 'w'] /* descriptor for stderr */ 13 ], 14 $pipes, 15 ); Rob Allen ~ @akrabat
  24. router.php: runAction() 1 /* write to the process's stdin */

    2 $bytes = fwrite($pipes[0], $stdin); fclose($pipes[0]); 3 4 /* read the process's stdout */ 5 $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); 6 7 /* read the process's stderr */ 8 $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); 9 10 /* close process & get return code */ 11 $returnCode = proc_close($process); 12 13 return [$returnCode, $stdout, $stderr]; 14 } Rob Allen ~ @akrabat
  25. router.php: run() 1 function run() 2 { 3 $args =

    file_get_contents('config.json'); 4 $stdin = json_decode(file_get_contents('php://input'), true)['value']; 5 6 list($rtncode, $stdout, $stderr) = runAction($args, $stdin); 7 8 file_put_contents("php://stderr", $stderr); 9 file_put_contents("php://stdout", $stdout); 10 11 $pos = strrpos($stdout, PHP_EOL) + 1; 12 $lastLine = trim(substr($stdout, $pos)); 13 14 return $lastLine; 15 } Rob Allen ~ @akrabat
  26. router.php: run() 1 function run() 2 { 3 $args =

    file_get_contents('config.json'); 4 $stdin = json_decode(file_get_contents('php://input'), true)['value']; 5 6 list($rtncode, $stdout, $stderr) = runAction($args, $stdin); 7 8 file_put_contents("php://stderr", $stderr); 9 file_put_contents("php://stdout", $stdout); 10 11 $pos = strrpos($stdout, PHP_EOL) + 1; 12 $lastLine = trim(substr($stdout, $pos)); 13 14 return $lastLine; 15 } Rob Allen ~ @akrabat
  27. router.php: run() 1 function run() 2 { 3 $args =

    file_get_contents('config.json'); 4 $stdin = json_decode(file_get_contents('php://input'), true)['value']; 5 6 list($rtncode, $stdout, $stderr) = runAction($args, $stdin); 7 8 file_put_contents("php://stderr", $stderr); 9 file_put_contents("php://stdout", $stdout); 10 11 $pos = strrpos($stdout, PHP_EOL) + 1; 12 $lastLine = trim(substr($stdout, $pos)); 13 14 return $lastLine; 15 } Rob Allen ~ @akrabat
  28. runner.php Runs the user's code in a separate process 1

    <?php 2 $config = json_decode($argv[1], true); 3 $functionName = $config['function'] ?? 'main'; 4 5 require '/action/vendor/autoload.php'; 6 require '/action/src/index.php'; 7 $result = $functionName( 8 json_decode(file_get_contents('php://stdin') ?? [], true) 9 ); 10 11 /* last line of output is response */ 12 echo json_encode((object)$result); Rob Allen ~ @akrabat
  29. runner.php Runs the user's code in a separate process 1

    <?php 2 $config = json_decode($argv[1], true); 3 $functionName = $config['function'] ?? 'main'; 4 5 require '/action/vendor/autoload.php'; 6 require '/action/src/index.php'; 7 $result = $functionName( 8 json_decode(file_get_contents('php://stdin') ?? [], true) 9 ); 10 11 /* last line of output is response */ 12 echo json_encode((object)$result); Rob Allen ~ @akrabat
  30. runner.php Runs the user's code in a separate process 1

    <?php 2 $config = json_decode($argv[1], true); 3 $functionName = $config['function'] ?? 'main'; 4 5 require '/action/vendor/autoload.php'; 6 require '/action/src/index.php'; 7 $result = $functionName( 8 json_decode(file_get_contents('php://stdin') ?? [], true) 9 ); 10 11 /* last line of output is response */ 12 echo json_encode((object)$result); Rob Allen ~ @akrabat
  31. runner.php Runs the user's code in a separate process 1

    <?php 2 $config = json_decode($argv[1], true); 3 $functionName = $config['function'] ?? 'main'; 4 5 require '/action/vendor/autoload.php'; 6 require '/action/src/index.php'; 7 $result = $functionName( 8 json_decode(file_get_contents('php://stdin') ?? [], true) 9 ); 10 11 /* last line of output is response */ 12 echo json_encode((object)$result); Rob Allen ~ @akrabat
  32. Hello world in PHP hello.php: 1 <?php 2 function main(array

    $args) : array { 3 $name = $args["name"] ?? "World"; 4 return [ "msg" => "Hello $name" ]; 5 } Rob Allen ~ @akrabat
  33. Execute natively in OpenWhisk $ wsk action create hello hello.php

    --kind php:7.1 ok: updated action hello $ wsk action invoke -r hello -p name Rob { "msg": "Hello Rob" } Rob Allen ~ @akrabat