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

Running your Laravel project on AWS Lambda

Running your Laravel project on AWS Lambda

Ever find yourself deploying your Laravel app to your EC2 boxes and wondering if you’re using your resources wisely? You’re almost certainly paying for CPU time you’re not using, and scaling PHP applications on AWS can be tricky.

Wouldn’t it be great to pay only for the resources you actually use, and if your site could scale instantly without needing to configure auto-scaling-groups, alarms, and rules? AWS Lambda gives us these features but doesn't natively support PHP.

Until now making PHP run on AWS Lambda has required a lot of hacks and workarounds. However, evolving tools such as https://bref.sh have fixed a lot of the pain for you.

In this session we’ll walk through the steps required to set up a real Laravel website running on Lambda, served over HTTPS, and all without launching a single EC2 instance or ELB.

Finally we'll look at some of the other possibilities and considerations that are now open to us as PHP engineers in a serverless world.

nealio82

June 11, 2019
Tweet

More Decks by nealio82

Other Decks in Programming

Transcript

  1. EC2

  2. What kind of lambda do you want to create? [0]

    PHP function [1] HTTP application [2] Console application >
  3. What kind of lambda do you want to create? [0]

    PHP function [1] HTTP application [2] Console application > 1
  4. [OK] Project initialized and ready to test or deploy. The

    files created were automatically added to git.
  5. AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: '' Resources: MyFunction: Type: AWS::Serverless::Function

    Properties: FunctionName: 'my-function' Description: '' CodeUri: . Handler: index.php Timeout: 30 # in seconds (API Gateway has a timeout of 30 se Runtime: provided Layers: - ‘arn:aws:lambda:eu-west-1:209497400698:layer:php-73-fp Events: # The function will match all HTTP URLs HttpRoot: Type: Api Properties: Path: / Method: ANY HttpSubPaths: Type: Api Properties: Path: /{proxy+} hello-world.php template.yaml
  6. # ... Resources: MyFunction: Type: AWS::Serverless::Function Properties: FunctionName: 'my-function' Description:

    '' CodeUri: . Handler: index.php Timeout: 30 # in seconds Runtime: provided hello-world.php template.yaml
  7. # ... Resources: MyFunction: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-hello-world' Description:

    '' CodeUri: . Handler: hello-world.php Timeout: 30 # in seconds Runtime: provided hello-world.php template.yaml
  8. # ... Layers: - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73-fpm:1' Events: # The function will

    match all HTTP URLs HttpRoot: Type: Api Properties: Path: / Method: ANY HttpSubPaths: Type: Api Properties: Path: /{proxy+} Method: ANY # ... hello-world.php template.yaml
  9. # ... Layers: - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73-fpm:1' Events: # The function will

    match all HTTP URLs HttpRoot: Type: Api Properties: Path: / Method: ANY HttpSubPaths: Type: Api Properties: Path: /{proxy+} Method: ANY # ... hello-world.php template.yaml
  10. # ... Layers: - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73-fpm:1' Events: # The function will

    match all HTTP URLs HttpRoot: Type: Api Properties: Path: / Method: ANY HttpSubPaths: Type: Api Properties: Path: /{proxy+} Method: ANY # ... hello-world.php template.yaml
  11. # ... Layers: - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73-fpm:1' Events: # The function will

    match all HTTP URLs HttpRoot: Type: Api Properties: Path: / Method: ANY HttpSubPaths: Type: Api Properties: Path: /{proxy+} Method: ANY # ... hello-world.php template.yaml
  12. Execute the following command to deploy the packaged template aws

    cloudformation deploy --template-file / Users/neal/www/hello-world/.stack.yaml --stack- name <YOUR STACK NAME>
  13. Failed to create/update the stack. Run the following command to

    fetch the list of events leading up to the failure aws cloudformation describe-stack-events -- stack-name bref-hello-world
  14. What kind of lambda do you want to create? [0]

    PHP function [1] HTTP application [2] Console application > 1
  15. # ... Resources: WebApplication: Type: AWS::Serverless::Function Properties: FunctionName: ‘bref-kittyfinder' Description:

    '' CodeUri: . Handler: public/index.php Timeout: 30 # in seconds Runtime: provided MemorySize: 1024 app config database bootstrap template.yaml public resources routes storage
  16. # ... Resources: WebApplication: Type: AWS::Serverless::Function Properties: FunctionName: ‘bref-kittyfinder' Description:

    '' CodeUri: . Handler: public/index.php Timeout: 30 # in seconds Runtime: provided MemorySize: 1024 app config database bootstrap template.yaml public resources routes storage
  17. # ... Resources: WebApplication: Type: AWS::Serverless::Function Properties: FunctionName: ‘bref-kittyfinder' Description:

    '' CodeUri: . Handler: public/index.php Timeout: 30 # in seconds Runtime: provided MemorySize: 1024 app config database bootstrap template.yaml public resources routes storage
  18. Failed to create/update the stack. Run the following command to

    fetch the list of events leading up to the failure aws cloudformation describe-stack-events -- stack-name bref-kitty-finder
  19. cp -Rf ../bref-kitty-finder/* . \ cp ../bref-kitty-finder/.env . \ &&

    rm -rf node_modules \ && composer install --optimize-autoloader --no-dev \ && sam package --output-template-file .stack.yaml --s3-bucket bref-kitty-finder-bucket \ && sam deploy --template-file .stack.yaml --stack-name bref-kitty-finder --capabilities CAPABILITY_IAM
  20. "NOTICE: PHP message: PHP Fatal error: Uncaught ErrorException: file_put_contents(/var/task/ storage/framework/views/

    a048d5da056b113da279d23ae842d71b174d2ae6.php): failed to open stream: Read-only file system in /var/task/vendor/laravel/framework/src/ Illuminate/Filesystem/Filesystem.php:122"
  21. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' Resources: WebApplication: # ... app config database bootstrap template.yaml public resources routes storage
  22. <?php /* |----------------------------------------------------------------------- | Create The Application |----------------------------------------------------------------------- | |

    The first thing we will do is create a new Laravel application instanc | which serves as the "glue" for all the components of Laravel, and is | the IoC container for the system binding all of the various parts. | */ $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); /* * Allow overriding the storage path in * production using an environment variable. */ $app->useStoragePath($_ENV['APP_STORAGE'] ?? $app->storagePath()); app config database bootstrap app.php public resources routes storage
  23. <?php /* |----------------------------------------------------------------------- | Create The Application |----------------------------------------------------------------------- | |

    The first thing we will do is create a new Laravel application instanc | which serves as the "glue" for all the components of Laravel, and is | the IoC container for the system binding all of the various parts. | */ $app = new Illuminate\Foundation\Application( $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__) ); /* * Allow overriding the storage path in * production using an environment variable. */ $app->useStoragePath($_ENV['APP_STORAGE'] ?? $app->storagePath()); app config database bootstrap public resources routes storage app.php
  24. # ... VIEW_COMPILED_PATH=/tmp/storage/framework/views # We cannot store sessions to disk:

    if you don't need sessions (e.g. API # then use `array`, else store sessions in database or cookies SESSION_DRIVER=array # Logging to stderr allows the logs to end up in Cloudwatch LOG_CHANNEL=stderr # ... app config database bootstrap .env public resources routes storage
  25. # ... VIEW_COMPILED_PATH=/tmp/storage/framework/views # We cannot store sessions to disk:

    if you don't need sessions (e.g. API # then use `array`, else store sessions in database or cookies SESSION_DRIVER=array # Logging to stderr allows the logs to end up in Cloudwatch LOG_CHANNEL=stderr # ... app config database bootstrap .env public resources routes storage
  26. # ... VIEW_COMPILED_PATH=/tmp/storage/framework/views # We cannot store sessions to disk:

    if you don't need sessions (e.g. API # then use `array`, else store sessions in database or cookies SESSION_DRIVER=array # Logging to stderr allows the logs to end up in Cloudwatch LOG_CHANNEL=stderr # ... app config database bootstrap .env public resources routes storage
  27. # ... VIEW_COMPILED_PATH=/tmp/storage/framework/views # We cannot store sessions to disk:

    if you don't need sessions (e.g. API # then use `array`, else store sessions in database or cookies SESSION_DRIVER=array # Logging to stderr allows the logs to end up in Cloudwatch LOG_CHANNEL=stderr # ... app config database bootstrap .env public resources routes storage
  28. <?php class AppServiceProvider extends ServiceProvider { // ... /** *

    Bootstrap any application services. * * @return void */ public function boot() { } } app config database Providers bootstrap AppServiceProvider.php public resources routes storage
  29. <?php class AppServiceProvider extends ServiceProvider { // ... /** *

    Bootstrap any application services. * * @return void */ public function boot() { // Make sure the directory for compiled views exist if (!is_dir(config('view.compiled'))) { mkdir(config('view.compiled'), 0755, true); } } } app config database Providers public resources routes storage bootstrap AppServiceProvider.php
  30. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' DATABASE_URL: ‘mysql://db_user:db_pass@...’ # ... app config database bootstrap template.yaml public resources routes storage
  31. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' DATABASE_URL: ‘{{resolve:ssm:kittyfinder-database-url:1}}' # ... app config database bootstrap template.yaml public resources routes storage
  32. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' DATABASE_URL: ‘{{resolve:ssm:kittyfinder-database-url:1}}' # ... app config database bootstrap template.yaml public resources routes storage
  33. app config database bootstrap filesystems.php <?php return [ 'disks' =>

    [ // ... /* |----------------------------------------------------------------- | Assets CDN |----------------------------------------------------------------- | | This is where compiled static assets will be served from */ 'asset-cdn' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'bucket' => env(‘AWS_CDN_BUCKET'), ], ], ]; public resources routes storage
  34. app config database bootstrap filesystems.php <?php return [ 'disks' =>

    [ // ... /* |----------------------------------------------------------------- | Assets CDN |----------------------------------------------------------------- | | This is where compiled static assets will be served from */ 'asset-cdn' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 'bucket' => env(‘AWS_CDN_BUCKET'), ], ], ]; public resources routes storage
  35. <?php return [ 'use_cdn' => env('USE_CDN', false), 'cdn_url' => 'https://cdn.kittyfinder.net',

    'filesystem' => [ 'disk' => 'asset-cdn', 'options' => [ 'ACL' => 'public-read', 'CacheControl' => 'max-age=31536000, public’, // ... ], 'files' => [ 'include' => [ 'paths' => [ 'js', 'css' ], // ... app config database bootstrap asset-cdn.php public resources routes storage
  36. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' DATABASE_URL: ‘{{resolve:ssm:kittyfinder-database-url:1}}' USE_CDN: true # ... app config database bootstrap template.yaml public resources routes storage
  37. <!-- Scripts --> <script src="{{ mix('js/app.js') }}" defer></script> <!-- Fonts

    --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet <!-- Styles --> <link href="{{ mix('css/app.css') }}" rel="stylesheet"> app config database bootstrap public resources routes storage views layouts app.blade.php
  38. <!-- Scripts --> <script src="{{ mix_cdn('js/app.js') }}" defer></script> <!-- Fonts

    --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet <!-- Styles --> <link href="{{ mix_cdn('css/app.css') }}" rel="stylesheet"> app config database bootstrap public resources routes storage views layouts app.blade.php
  39. <?php return [ 'use_cdn' => env('USE_CDN', false), 'cdn_url' => 'https://cdn.kittyfinder.net',

    'filesystem' => [ 'disk' => 'asset-cdn', 'options' => [ 'ACL' => 'public-read', 'CacheControl' => 'max-age=31536000, public’, // ... ], 'files' => [ 'include' => [ 'paths' => [ 'js', 'css', 'vendor' ], // ... app config database bootstrap asset-cdn.php public resources routes storage
  40. <!-- Styles --> <link rel="stylesheet" href="{{ mix_cdn('app.css', 'vendor/nova') }}"> app

    config database bootstrap public resources routes storage views vendor nova layout.blade.php
  41. <?php class AppServiceProvider extends ServiceProvider { // ... /** *

    Bootstrap any application services. * * @return void */ public function boot() { // Make sure the directory for compiled views exist if (!is_dir(config('view.compiled'))) { mkdir(config('view.compiled'), 0755, true); } if($this->app->environment('production')) { URL::forceScheme('https'); } } } app config database Providers public resources routes storage bootstrap AppServiceProvider.php
  42. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' DATABASE_URL: ‘{{resolve:ssm:kittyfinder-database-url:1}}' USE_CDN: true APP_ENV: production # ... app config database bootstrap template.yaml public resources routes storage
  43. API Gateway Data Store Sessions Sessions Handler PHP Layer Bootstrap

    Lambda function Handler PHP Layer Bootstrap
  44. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' DATABASE_URL: ‘{{resolve:ssm:kittyfinder-database-url:1}}' USE_CDN: true APP_ENV: production SESSION_DRIVER: database # ... app config database bootstrap template.yaml public resources routes storage
  45. Resources: WebApplication: # ... Console: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyfinder-console'

    CodeUri: . Handler: artisan Runtime: provided Layers: # PHP runtime - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73:1' # Console layer - 'arn:aws:lambda:eu-west-1:209497400698:layer:console:1' app config database bootstrap template.yaml public resources routes storage
  46. app config database bootstrap Resources: WebApplication: # ... Console: Type:

    AWS::Serverless::Function Properties: FunctionName: 'bref-kittyfinder-console' CodeUri: . Handler: artisan Runtime: provided Layers: # PHP runtime - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73:1' # Console layer - 'arn:aws:lambda:eu-west-1:209497400698:layer:console:1' template.yaml public resources routes storage
  47. app config database bootstrap template.yaml public resources routes storage Resources:

    WebApplication: # ... Console: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyfinder-console' CodeUri: . Handler: artisan Runtime: provided Layers: # PHP runtime - 'arn:aws:lambda:eu-west-1:209497400698:layer:php-73:1' # Console layer - 'arn:aws:lambda:eu-west-1:209497400698:layer:console:1'
  48. /** * Get the fields displayed by the resource. *

    * @param \Illuminate\Http\Request $request * @return array */ public function fields(Request $request) { return [ // ... Avatar::make('Avatar', 'profile_pic')->disk('s3'), // ... ]; } app config database bootstrap Nova Kitty.php User.php public resources routes storage
  49. app config database bootstrap Nova Kitty.php User.php /** * Get

    the fields displayed by the resource. * * @param \Illuminate\Http\Request $request * @return array */ public function fields(Request $request) { return [ // ... Avatar::make('Avatar')->disk('s3'), // ... ]; } public resources routes storage
  50. Client Server Amazon S3 Get Pre-Signed URL Get Pre-Signed URL

    One-time use URL One-time use URL PUT file to One-time use URL 200 OK
  51. Client Server Amazon S3 Get Pre-Signed URL Get Pre-Signed URL

    One-time use URL One-time use URL PUT file to One-time use URL 200 OK Update record
  52. Client Server Amazon S3 Get Pre-Signed URL Get Pre-Signed URL

    One-time use URL One-time use URL PUT file to One-time use URL 200 OK Update record Delete previous file
  53. /** * Get the fields displayed by the resource. *

    * @param \Illuminate\Http\Request $request * @return array */ public function fields(Request $request) { return [ ID::make()->sortable(), Text::make('Name') ->sortable() ->rules('required', 'max:255'), Avatar::make(‘Avatar', 'profile_pic'), Text::make('Favourite Toy'), Trix::make('Bio'), BelongsTo::make('User') ]; } app config database bootstrap public resources routes storage Nova Kitty.php User.php nova-components
  54. /** * Get the fields displayed by the resource. *

    * @param \Illuminate\Http\Request $request * @return array */ public function fields(Request $request) { return [ ID::make()->sortable(), Text::make('Name') ->sortable() ->rules('required', 'max:255'), S3FilePicker::make('Image', 'profile_pic'), Text::make('Favourite Toy'), Trix::make('Bio'), BelongsTo::make('User') ]; } app config database bootstrap public resources routes storage Nova Kitty.php User.php nova-components
  55. class ApiController extends Controller { /** * Get a pre-signed

    key * * @return Response */ public function preSignedUrl(Request $request): Response { if (null === $request->user()) { throw new UnauthorizedHttpException("You're not allowed her } return response()->json([ 'url' => $this->generatePreSignedUrl($request), 'filename' => $this->getNewFilename($request), 'path' => 'https://' . env('AWS_BUCKET') . '.s3-eu-west-1.amazonaws.com/' . $this->getNewFilename($request) ]); } // ... app config database bootstrap public resources routes storage Http ApiController.php Controllers nova-components
  56. class ApiController extends Controller { /** * Get a pre-signed

    key * * @return Response */ public function preSignedUrl(Request $request): Response { if (null === $request->user()) { throw new UnauthorizedHttpException("You're not allowed her } return response()->json([ 'url' => $this->generatePreSignedUrl($request), 'filename' => $this->getNewFilename($request), 'path' => 'https://' . env('AWS_BUCKET') . '.s3-eu-west-1.amazonaws.com/' . $this->getNewFilename($request) ]); } // ... app config database bootstrap public resources routes storage Http ApiController.php Controllers nova-components
  57. class ApiController extends Controller { // ... private function generatePreSignedUrl(Request

    $request): string { $s3Client = new S3Client([ 'region' => env('AWS_DEFAULT_REGION'), 'version' => 'latest', ]); $cmd = $s3Client->getCommand('PutObject', [ 'Bucket' => env('AWS_BUCKET'), 'Key' => $this->getNewFilename($request), 'ContentType' => $request->get('filetype') ]); $response = $s3Client->createPresignedRequest( $cmd, '+20 minutes’ ); return (string)$response->getUri(); } // ... app config database bootstrap public resources routes storage Http ApiController.php Controllers nova-components
  58. class ApiController extends Controller { // ... private function generatePreSignedUrl(Request

    $request): string { $s3Client = new S3Client([ 'region' => env('AWS_DEFAULT_REGION'), 'version' => 'latest', ]); $cmd = $s3Client->getCommand('PutObject', [ 'Bucket' => env('AWS_BUCKET'), 'Key' => $this->getNewFilename($request), 'ContentType' => $request->get('filetype') ]); $response = $s3Client->createPresignedRequest( $cmd, '+20 minutes’ ); return (string)$response->getUri(); } // ... app config database bootstrap public resources routes storage Http ApiController.php Controllers nova-components
  59. class ApiController extends Controller { // ... private function generatePreSignedUrl(Request

    $request): string { $s3Client = new S3Client([ 'region' => env('AWS_DEFAULT_REGION'), 'version' => 'latest', ]); $cmd = $s3Client->getCommand('PutObject', [ 'Bucket' => env('AWS_BUCKET'), 'Key' => $this->getNewFilename($request), 'ContentType' => $request->get('filetype') ]); $response = $s3Client->createPresignedRequest( $cmd, '+20 minutes’ ); return (string)$response->getUri(); } // ... app config database bootstrap public resources routes storage Http ApiController.php Controllers nova-components
  60. class ApiController extends Controller { // ... private function generatePreSignedUrl(Request

    $request): string { $s3Client = new S3Client([ 'region' => env('AWS_DEFAULT_REGION'), 'version' => 'latest', ]); $cmd = $s3Client->getCommand('PutObject', [ 'Bucket' => env('AWS_BUCKET'), 'Key' => $this->getNewFilename($request), 'ContentType' => $request->get('filetype') ]); $response = $s3Client->createPresignedRequest( $cmd, '+20 minutes’ ); return (string)$response->getUri(); } // ... app config database bootstrap public resources routes storage Http ApiController.php Controllers nova-components
  61. <template> <default-field :field="field" :errors="errors"> // ... <template slot="field"> <input ref="fileField"

    type="file" name="imageUpload" @change="fileChange" /> <input type="text" :id="field.name" :dusk="field.attribute" name="field.name" v-model="value" /> </template> </default-field> </template> app config database nova-components bootstrap public resources routes storage resources/js/components S3FilePicker FormField.vue
  62. app config database nova-components bootstrap public resources routes storage resources/js/components

    S3FilePicker FormField.vue /** * Update the field's internal value. */ async fileChange(event) { let path = event.target.value let fileName = path.match(/[^\\/]*$/)[0] this.fileName = fileName this.file = this.$refs.fileField.files[0] await axios.get('/nova/presignedurl?' + 'filename=' + this.fileName + '&filetype=' + this.file.type ).then(async response => { let options = { headers: { "Content-Type": this.file.type } }; await axios.put(response.data.url, this.file, options).then(result document.getElementById(‘previewImage') .setAttribute(‘src', response.data.path); this.value = response.data.filename }); }) }
  63. app config database nova-components bootstrap public resources routes storage resources/js/components

    S3FilePicker FormField.vue /** * Update the field's internal value. */ async fileChange(event) { let path = event.target.value let fileName = path.match(/[^\\/]*$/)[0] this.fileName = fileName this.file = this.$refs.fileField.files[0] await axios.get('/nova/presignedurl?' + 'filename=' + this.fileName + '&filetype=' + this.file.type ).then(async response => { let options = { headers: { "Content-Type": this.file.type } }; await axios.put(response.data.url, this.file, options).then(result document.getElementById(‘previewImage') .setAttribute(‘src', response.data.path); this.value = response.data.filename }); }) }
  64. app config database nova-components bootstrap public resources routes storage resources/js/components

    S3FilePicker FormField.vue /** * Update the field's internal value. */ async fileChange(event) { let path = event.target.value let fileName = path.match(/[^\\/]*$/)[0] this.fileName = fileName this.file = this.$refs.fileField.files[0] await axios.get('/nova/presignedurl?' + 'filename=' + this.fileName + '&filetype=' + this.file.type ).then(async response => { let options = { headers: { "Content-Type": this.file.type } }; await axios.put(response.data.url, this.file, options).then(result document.getElementById(‘previewImage') .setAttribute(‘src', response.data.path); this.value = response.data.filename }); }) }
  65. What kind of lambda do you want to create? [0]

    PHP function [1] HTTP application [2] Console application >
  66. What kind of lambda do you want to create? [0]

    PHP function [1] HTTP application [2] Console application > 0