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.

Da2d2829b89cde136392973a35b68959?s=128

nealio82

June 11, 2019
Tweet

Transcript

  1. RUNNING YOUR LARAVEL PROJECT ON AWS LAMBDA

  2. None
  3. None
  4. None
  5. WE ARE HIRING!

  6. None
  7. RUNNING YOUR LARAVEL PROJECT ON AWS LAMBDA

  8. (coupling yourself to AWS for fun & profit) COUPLING YOURSELF

    TO AWS
  9. None
  10. None
  11. WHY?

  12. None
  13. EC2

  14. EC2 Data Store

  15. EC2 Data Store

  16. EC2 Data Store Load Balancer

  17. WTF?

  18. EC2 Data Store Load Balancer

  19. EC2 Data Store Load Balancer EC2 EC2

  20. EC2 Data Store Load Balancer EC2 EC2

  21. EC2 Data Store Load Balancer EC2 EC2

  22. SAY HELLO TO FaaS

  23. Think of FaaS as a server that doesn’t exist…

  24. … until you need it …

  25. … and goes away again once the job is done

  26. None
  27. 1 million requests FREE per month

  28. $0.20 per million requests thereafter

  29. None
  30. Container

  31. Container Bootstrap Code

  32. Container Bootstrap Code

  33. Container Bootstrap Code Application Code

  34. Container Bootstrap Code Application Code

  35. Container Bootstrap Code Application Code

  36. LAYERS

  37. None
  38. None
  39. Lambda function Bootstrap PHP Layer Handler

  40. Lambda function Handler PHP Layer Bootstrap

  41. Lambda function Handler PHP Layer Bootstrap

  42. Lambda function Handler PHP Layer Bootstrap

  43. Lambda function Handler PHP Layer Bootstrap

  44. You can use up to 5 different layers in a

    Lambda function
  45. The total unzipped size of code & layers must be

    < 250 MB
  46. Default account limit of 1,000 concurrent invocations per AWS region

  47. Maximum request body size is 6 MB

  48. RUNNING PHP ON LAMBDA

  49. IS A PITA RUNNING PHP ON LAMBDA

  50. Lambda function Handler PHP Layer Bootstrap

  51. Lambda function Handler PHP Layer Bootstrap

  52. https://bref.sh

  53. BREF AIMS TO MAKE RUNNING PHP APPS SIMPLE

  54. SIMPLIFY PROBLEMS BY REMOVING CHOICES

  55. PROVIDE SIMPLE AND FAMILIAR SOLUTIONS

  56. EMPOWER BY SHARING KNOWLEDGE

  57. WHAT DOES BREF ACTUALLY DO?

  58. None
  59. None
  60. None
  61. SAM DEALS WITH DEPLOYING

  62. Lambda function Handler PHP Layer Bootstrap

  63. Lambda function Handler PHP Layer Bootstrap

  64. API Gateway Lambda function Handler PHP Layer Bootstrap

  65. API Gateway Lambda function Handler PHP Layer Bootstrap

  66. None
  67. None
  68. <?php echo '<h1>Hello, World!</h1>'; phpinfo(); hello-world.php

  69. $ composer require mnapoli/bref

  70. $ php vendor/bin/bref init

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

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

    PHP function [1] HTTP application [2] Console application > 1
  73. In which AWS region do you want to deploy your

    application? >
  74. In which AWS region do you want to deploy your

    application? > eu-west-1
  75. [OK] Project initialized and ready to test or deploy. The

    files created were automatically added to git.
  76. <?php echo '<h1>Hello, World!</h1>'; phpinfo(); hello-world.php index.php template.yaml

  77. <?php echo '<h1>Hello, World!</h1>'; phpinfo(); hello-world.php template.yaml

  78. 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
  79. # ... 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
  80. # ... 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
  81. # ... 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
  82. # ... 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
  83. # ... 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
  84. # ... 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
  85. DEPLOYING

  86. $ brew upgrade && brew update $ brew tap aws/tap

    $ brew install aws-sam-cli
  87. None
  88. $ aws s3 mb s3://bref-hello-world

  89. make_bucket: bref-hello-world

  90. $ sam package \ --output-template-file .stack.yaml \ --s3-bucket <bucket-name>

  91. $ sam package \ --output-template-file .stack.yaml \ --s3-bucket bref-hello-world

  92. Uploading to cf39403efc50314711b61effd4f5c948 2841371 / 2841371.0 (100.00%) Successfully packaged artifacts

    and wrote output template to file .stack.yaml.
  93. None
  94. 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>
  95. $ aws cloudformation deploy --template-file / Users/neal/www/hello-world/.stack.yaml --stack- name <YOUR

    STACK NAME>
  96. $ aws cloudformation deploy --template-file / Users/neal/www/hello-world/.stack.yaml --stack- name bref-hello-world

  97. Waiter encountered a terminal failure state Status: FAILED. Reason: Requires

    capabilities : [CAPABILITY_IAM]
  98. $ sam deploy \ --template-file .stack.yaml \ --capabilities CAPABILITY_IAM \

    --stack-name <stack-name>
  99. $ sam deploy \ --template-file .stack.yaml \ --capabilities CAPABILITY_IAM \

    --stack-name bref-hello-world
  100. 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
  101. $ php vendor/bin/bref deployment \ --region eu-west-1 -- bref-hello-world

  102. AWS::Lambda::Function Error occurred while GetObject. S3 Error Code: PermanentRedirect. S3

    Error Message: The bucket is in this region: us-east-1.
  103. None
  104. None
  105. $ aws s3 mb s3://bref-hello-world --region eu-west-1

  106. $ sam package \ --output-template-file .stack.yaml \ --s3-bucket bref-hello-world

  107. $ sam deploy \ --template-file .stack.yaml \ --capabilities CAPABILITY_IAM \

    --stack-name bref-hello-world
  108. Successfully created/updated stack - bref-hello-world

  109. None
  110. None
  111. API Gateway Lambda function Handler PHP Layer Bootstrap

  112. None
  113. None
  114. None
  115. None
  116. RECAP

  117. Looked how FaaS can simplify architecture & save money

  118. Installed AWS CLI

  119. Added Bref to an existing project

  120. Explored the template file

  121. Created a deployment bucket

  122. Created a deployment bucket in the correct region

  123. Packaged & deployed the application

  124. Explored the stack

  125. Tested the PHP application running on Lambda

  126. REFACTORING A FULLY FEATURED WEBSITE

  127. None
  128. None
  129. None
  130. None
  131. REMEMBER

  132. The filesystem is read-only

  133. (apart from /tmp)

  134. The total unzipped size of code & layers must be

    < 250 MB
  135. $ composer require mnapoli/bref

  136. $ php vendor/bin/bref init

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

    PHP function [1] HTTP application [2] Console application > 1
  138. In which AWS region do you want to deploy your

    application? > eu-west-1
  139. # ... 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
  140. # ... 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
  141. # ... 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
  142. $ aws s3 mb s3://bref-kitty-finder-bucket

  143. $ sam package \ --output-template-file .stack.yaml \ --s3-bucket bref-kitty-finder

  144. $ sam deploy \ --template-file .stack.yaml \ --capabilities CAPABILITY_IAM \

    --stack-name bref-kitty-finder
  145. 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
  146. Unzipped size must be smaller than 155930325 bytes

  147. $ composer install --optimize-autoloader --no-dev $ rm -rf node_modules

  148. $ composer install --optimize-autoloader --no-dev $ rm -rf node_modules $

    rm -rf .idea/* $ rm -rf .git/*
  149. 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
  150. Successfully created/updated stack - bref-kitty-finder

  151. None
  152. CLOUDWATCH HAS YOUR LOGS

  153. $ sam logs --name bref-kitty-finder

  154. "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"
  155. # ... Globals: Function: Environment: Variables: # Laravel environment variables

    APP_STORAGE: '/tmp' Resources: WebApplication: # ... app config database bootstrap template.yaml public resources routes storage
  156. <?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
  157. <?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
  158. # ... 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
  159. # ... 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
  160. # ... 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
  161. # ... 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
  162. <?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
  163. <?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
  164. None
  165. None
  166. None
  167. php extension=pdo_mysql conf.d php.ini app config database public resources routes

    storage bootstrap
  168. CONNECTING THE DATABASE

  169. API Gateway Lambda function Handler PHP Layer Bootstrap

  170. API Gateway Lambda function Handler PHP Layer Bootstrap

  171. API Gateway API Gateway Data Store Lambda function Handler PHP

    Layer Bootstrap
  172. API Gateway VPC API Gateway Data Store Lambda function Handler

    PHP Layer Bootstrap
  173. # ... 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
  174. None
  175. None
  176. # ... 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
  177. # ... 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
  178. None
  179. CONTENT DELIVERY NETWORK

  180. $ composer require arubacao/asset-cdn

  181. $ composer require league/flysystem-aws-s3-v3

  182. $ aws s3 mb s3://kittyfinder-site-assets

  183. 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
  184. 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
  185. <?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
  186. # ... 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
  187. <!-- 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
  188. <!-- 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
  189. None
  190. BRINGING YOUR OWN DOMAIN NAME

  191. None
  192. https://lzfret3054.execute-api.eu-west-1.amazonaws.com/Prod/

  193. https://lzfret3054.execute-api.eu-west-1.amazonaws.com/Prod/

  194. None
  195. None
  196. None
  197. None
  198. None
  199. None
  200. None
  201. <?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
  202. <!-- 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
  203. None
  204. None
  205. <?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
  206. # ... 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
  207. None
  208. API Gateway Data Store Lambda function Handler PHP Layer Bootstrap

  209. Lambda function Handler PHP Layer Bootstrap API Gateway Data Store

    Sessions
  210. Lambda function Handler PHP Layer Bootstrap API Gateway Data Store

    Sessions
  211. Lambda function Handler PHP Layer Bootstrap Handler PHP Layer Bootstrap

    API Gateway Data Store Sessions Sessions
  212. API Gateway Data Store Sessions Sessions Handler PHP Layer Bootstrap

    Lambda function Handler PHP Layer Bootstrap
  213. $ php artisan session:table

  214. # ... 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
  215. $ php artisan migrate

  216. CONSOLE COMMANDS

  217. 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
  218. 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
  219. 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'
  220. $ php vendor/bin/bref cli bref-kittyfinder-console -- \ migrate --force

  221. $ php vendor/bin/bref cli bref-kittyfinder-console -- \ migrate --force

  222. $ php vendor/bin/bref cli bref-kittyfinder-console -- \ migrate --force

  223. Migrating: 2019_06_06_163613_create_sessions_table Migrated: 2019_06_06_163613_create_sessions_table

  224. None
  225. API Gateway Data Store Lambda function Handler PHP Layer Bootstrap

  226. API Gateway Data Store Lambda function Handler PHP Layer Bootstrap

  227. API Gateway Data Store File Store Lambda function Handler PHP

    Layer Bootstrap
  228. # ... AWS_BUCKET=kittyfinder-uploads # ... app config database bootstrap .env

    public resources routes storage
  229. /** * 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
  230. 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
  231. None
  232. None
  233. <img src="{{asset('storage/' . $kitty->profile_pic)}}"> app config database bootstrap public resources

    routes storage views home.blade.php
  234. app config database bootstrap public resources <img src="{{Storage::disk('s3')->url($kitty->profile_pic)}}"> views home.blade.php

    routes storage
  235. None
  236. UPLOADING NEW IMAGES OF KITTIES

  237. Pre-signed URLs

  238. Client Server Amazon S3

  239. Client Server Amazon S3 Get Pre-Signed URL

  240. Client Server Amazon S3 Get Pre-Signed URL Get Pre-Signed URL

  241. Client Server Amazon S3 Get Pre-Signed URL Get Pre-Signed URL

    One-time use URL
  242. Client Server Amazon S3 Get Pre-Signed URL Get Pre-Signed URL

    One-time use URL One-time use URL
  243. 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
  244. 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
  245. 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
  246. $ php artisan nova:field kittyfinder/s3-filepicker

  247. /** * 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
  248. /** * 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
  249. 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
  250. 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
  251. 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
  252. 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
  253. 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
  254. 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
  255. <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
  256. 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 }); }) }
  257. 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 }); }) }
  258. 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 }); }) }
  259. None
  260. None
  261. None
  262. None
  263. None
  264. None
  265. None
  266. $ composer require nealio82/kittyfinder-s3filepicker

  267. None
  268. RECAP

  269. Added Bref to an existing site

  270. Fixed assets by moving them to a CDN

  271. Centralised session storage

  272. Added Bref’s console layer in order to run artisan commands

  273. Used pre-signed URLs to send file uploads directly to the

    CDN
  274. None
  275. PERFORMANCE

  276. None
  277. VPC API Gateway Data Store Lambda function Handler PHP Layer

    Bootstrap
  278. source: https://medium.freecodecamp.org/lambda-vpc-cold-starts-a-latency-killer-5408323278dd

  279. source: https://medium.freecodecamp.org/lambda-vpc-cold-starts-a-latency-killer-5408323278dd

  280. source: https://medium.freecodecamp.org/lambda-vpc-cold-starts-a-latency-killer-5408323278dd

  281. None
  282. SAVINGS

  283. None
  284. DOES IT SCALE?

  285. Custom runtime based upon Bref’s PHP-FPM layer

  286. Uses Phalcon PHP framework

  287. Added AWS application load balancer in-front of Lambda

  288. Uses heavy caching - responses served from Cloudfront where possible

  289. 40 million requests per day

  290. 90 ms average response time

  291. 2,500 concurrent invocations at peak times

  292. 25% cost saving compared to EC2*

  293. GOING FURTHER

  294. QUEUES & EVENTS

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

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

    PHP function [1] HTTP application [2] Console application > 0
  297. https://github.com/stechstudio/ laravel-bref-bridge

  298. ROLLING YOUR OWN

  299. https://github.com/stechstudio/ bref-extensions

  300. None
  301. serverless framework

  302. bref/bref

  303. JOIN US

  304. WE ARE HIRING!

  305. https://bref.sh/docs/community

  306. @nealio82 https://bref.sh/docs/community