Running your PHP site on AWS Lambda

Da2d2829b89cde136392973a35b68959?s=47 nealio82
February 21, 2019

Running your PHP site on AWS Lambda

Want to have immediate & easy website scaling, but also get rid of costly servers as they’re sitting idle 80% of the time waiting for visitors? Heard something about PHP and layers on AWS Lambda but have no idea what it means? You're not the only one! In this session we’ll get ourselves up-and-running with a PHP website on Lambda.

PHPUK Conference 2019

Da2d2829b89cde136392973a35b68959?s=128

nealio82

February 21, 2019
Tweet

Transcript

  1. RUNNING YOUR PHP PROJECT ON AWS LAMBDA

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

  6. None
  7. RUNNING YOUR PHP 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. Container

  28. Container Bootstrap Code

  29. Container Bootstrap Code

  30. Container Bootstrap Code Application Code

  31. Container Bootstrap Code Application Code

  32. Container Bootstrap Code Application Code

  33. WAS A PITA RUNNING PHP ON LAMBDA

  34. None
  35. Lambda function

  36. Lambda function JS Handler

  37. Lambda function JS Handler PHP Binary

  38. Lambda function JS Handler PHP Binary

  39. Lambda function JS Handler PHP Binary

  40. LAYERS

  41. None
  42. None
  43. • You can use up to 5 different layers in

    a Lambda function • The total unzipped size of code & layers must be < 250 MB • 1,000 concurrent invocations per region
  44. Lambda function JS Handler PHP Binary

  45. Lambda function Bootstrap PHP Layer Handler

  46. Lambda function Handler PHP Layer Bootstrap

  47. Lambda function Handler PHP Layer Bootstrap

  48. Lambda function Handler PHP Layer Bootstrap

  49. Lambda function Handler PHP Layer Bootstrap

  50. IS STILL A PITA RUNNING PHP ON LAMBDA

  51. https://bref.sh

  52. BREF AIMS TO MAKE RUNNING PHP APPS SIMPLE

  53. SIMPLIFY PROBLEMS BY REMOVING CHOICES

  54. PROVIDE SIMPLE AND FAMILIAR SOLUTIONS

  55. EMPOWER BY SHARING KNOWLEDGE

  56. WHAT DOES BREF ACTUALLY DO?

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

  61. Lambda function Handler PHP Layer Bootstrap

  62. Lambda function Handler PHP Layer Bootstrap

  63. API Gateway Lambda function Handler PHP Layer Bootstrap

  64. API Gateway Lambda function Handler PHP Layer Bootstrap

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

  68. $ composer require mnapoli/bref

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

  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. <?php echo '<h1>Hello, World!</h1>'; phpinfo(); hello-world.php index.php template.yaml

  74. 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:us-east-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
  75. # ... 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
  76. # ... 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
  77. # ... Layers: - 'arn:aws:lambda:us-east-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
  78. # ... 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
  79. # ... 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
  80. # ... 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
  81. TESTING
 LOCALLY

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

    $ brew install aws-sam-cli
  83. $ sam local start-api

  84. Error: Running AWS SAM projects locally requires Docker. Have you

    got it installed?
  85. $ sam local start-api

  86. * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

  87. None
  88. Invalid Layer name: arn:aws:lambda:eu-west-1:209497400698:layer:php-73-fpm

  89. $ sam local start-api --region=eu-west-1

  90. Downloading arn:aws:lambda:eu- west-1:209497400698:layer:php-73-fpm [####################################] 32842625/32842625 Building image...

  91. None
  92. DEPLOYING

  93. $ aws s3 mb s3://bref-hello-world

  94. make_bucket: bref-hello-world

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

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

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

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

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

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

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

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

    --stack-name bref-hello-world
  105. 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
  106. None
  107. Error occurred while GetObject. S3 Error Code: PermanentRedirect. S3 Error

    Message: The bucket is in this region: eu-west-2.
  108. None
  109. $ aws s3 mb s3://bref-hello-world --region eu-west-1

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

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

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

  113. None
  114. None
  115. API Gateway Lambda function Handler PHP Layer Bootstrap

  116. None
  117. None
  118. None
  119. None
  120. None
  121. CLOUDWATCH HAS YOUR LOGS

  122. None
  123. None
  124. BRINGING YOUR OWN DOMAIN NAME

  125. https://o6zwsmm0uk.execute-api.eu-west-1.amazonaws.com/Prod/

  126. https://o6zwsmm0uk.execute-api.eu-west-1.amazonaws.com/Prod/

  127. None
  128. None
  129. None
  130. None
  131. None
  132. None
  133. None
  134. None
  135. None
  136. RECAP

  137. • Looked at how FaaS could simplify our architecture &

    save us money • Installed AWS CLI • Added Bref to a project • Explored the template.yaml file • Used SAM Local to test • Created a deployment bucket • Packaged and deployed our application • Explored the stack • Tested the application on Lambda • Seen the logs in Cloudwatch • Added a custom domain name
  138. REFACTORING A FULLY FEATURED WEBSITE

  139. None
  140. None
  141. None
  142. None
  143. None
  144. REMEMBER

  145. • Only /tmp is writeable • The total unzipped size

    of code & layers must be < 250 MB
  146. $ composer require mnapoli/bref

  147. $ php vendor/bin/bref init

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

    PHP function [1] HTTP application [2] Console application > 1
  149. # ... Resources: MyFunction: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyquotes' Description:

    '' CodeUri: . Handler: public/index.php Timeout: 30 # in seconds Runtime: provided MemorySize: 1024 template.yaml bin public src templates config
  150. # ... Resources: WebApplication: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyquotes' Description:

    '' CodeUri: . Handler: public/index.php Timeout: 30 # in seconds Runtime: provided template.yaml bin public src templates config
  151. $ sam local start-api

  152. None
  153. bin public src templates config template.yaml <?php namespace App; class

    Kernel extends BaseKernel { // ... public function getCacheDir() { // When on the lambda only /tmp is writeable if (getenv('LAMBDA_TASK_ROOT') !== false) { return '/tmp/cache/' . $this->environment; } return $this->getProjectDir().'/var/cache' . $this->environment; } Kernel.php
  154. bin public src templates config template.yaml <?php namespace App; class

    Kernel extends BaseKernel { // ... public function getLogDir() { // When on the lambda only /tmp is writeable if (getenv('LAMBDA_TASK_ROOT') !== false) { return '/tmp/log/'; } return $this->getProjectDir().'/var/log'; } Kernel.php
  155. PDOException > PDOException > DriverException An exception occurred in driver:

    could not find driver
  156. None
  157. None
  158. bin public php templates config template.yaml extension=pdo_mysql src conf.d php.ini

  159. CONNECTING THE DATABASE

  160. API Gateway Lambda function Handler PHP Layer Bootstrap

  161. API Gateway Lambda function Handler PHP Layer Bootstrap

  162. API Gateway Lambda function Handler PHP Layer Bootstrap API Gateway

    Data Store
  163. API Gateway VPC Lambda function Handler PHP Layer Bootstrap API

    Gateway Data Store
  164. # ... Resources: WebApplication: Type: AWS::Serverless::Function Properties: Environment: Variables: APP_ENV:

    prod DATABASE_URL: ‘mysql://db_user:db_pass@...’ # ... template.yaml bin public src templates config
  165. None
  166. None
  167. None
  168. $ aws s3 mb s3://bref-kitty-quotes-bucket

  169. $ sam package \ --output-template-file .stack.yaml \ --s3-bucket bref-kitty-quotes-bucket

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

    --stack-name bref-kitty-quotes-app
  171. 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-quotes-app
  172. Unzipped size must be smaller than 155850286 bytes

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

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

    rm -rf var/cache $ rm -rf .idea/* $ rm -rf .git/*
  175. cp -Rf ../bref-kitty-quotes-symfony/* . \ && rm -rf var/cache/* \

    && rm -rf node_modules \ && composer install --optimize-autoloader --no-dev \ && php bin/console cache:warmup --env=prod \ && sam package --output-template-file .stack.yaml --s3-bucket bref-kitty-quotes-bucket \ && sam deploy --template-file .stack.yaml --stack-name bref-kitty-quotes-app --capabilities CAPABILITY_IAM
  176. Successfully created/updated stack - bref-kitty-quotes-app

  177. None
  178. None
  179. // ... if (Encore.isProduction()) { Encore.setPublicPath( 'https://s3-eu-west-1.amazonaws.com/' + ‘kittyquotes-site-assets’ );

    Encore.setManifestKeyPrefix('build/'); } webpack.config.js template.yaml bin public src templates config php
  180. $ yarn encore production

  181. None
  182. None
  183. None
  184. bin config framework: assets: base_urls: - 'https://s3-eu-west-1.amazonaws.com/%env(ASSETS_BUCKET_NAME)%' packages assets.yaml webpack.config.js

    template.yaml public src templates php
  185. None
  186. None
  187. API Gateway Data Store Lambda function Handler PHP Layer Bootstrap

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

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

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

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

    Layer Bootstrap Handler PHP Layer Bootstrap
  192. None
  193. bin config services: # ... Symfony\Component\HttpFoundation\Session \Storage\Handler\PdoSessionHandler: arguments: - !service

    { class: PDO, factory: > 'database_connection:getWrappedConnection' } - { lock_mode: 1 } services.yaml webpack.config.js template.yaml public src templates php
  194. bin config framework: # ... session: # ... handler_id: >

    Symfony\Component\HttpFoundation\Session \Storage\Handler\PdoSessionHandler packages framework.yaml webpack.config.js template.yaml public src templates php
  195. None
  196. bin public src templates config final class Version20180828140534 extends AbstractMigration

    { public function up(Schema $schema): void { $this->addSql("CREATE TABLE `sessions` ( `sess_id` VARCHAR(128) NOT NULL PRIMARY KEY, `sess_data` BLOB NOT NULL, `sess_time` INTEGER UNSIGNED NOT NULL, `sess_lifetime` MEDIUMINT NOT NULL ) COLLATE utf8_bin, ENGINE = InnoDB;"); } public function down(Schema $schema): void { $this->addSql('DROP TABLE sessions'); } } Migrations Version2019____.php webpack.config.js template.yaml php
  197. CONSOLE COMMANDS

  198. Globals: Function: Environment: Variables: DATABASE_URL: ‘mysql://db_user:db_pass@...’ Resources: WebApplication: # ...

    Console: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyquotes-console' CodeUri: . Handler: bin/console # or `artisan` for Laravel 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' webpack.config.js template.yaml bin public src templates config php
  199. Globals: Function: Environment: Variables: DATABASE_URL: ‘mysql://db_user:db_pass@...’ Resources: WebApplication: # ...

    Console: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyquotes-console' CodeUri: . Handler: bin/console # or `artisan` for Laravel 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' webpack.config.js template.yaml bin public src templates config php
  200. Globals: Function: Environment: Variables: DATABASE_URL: ‘mysql://db_user:db_pass@...’ Resources: WebApplication: # ...

    Console: Type: AWS::Serverless::Function Properties: FunctionName: 'bref-kittyquotes-console' CodeUri: . Handler: bin/console # or `artisan` for Laravel 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' webpack.config.js template.yaml bin public src templates config php
  201. $ php vendor/bin/bref cli bref-kittyquotes-console -- \ doctrine:migrations:migrate --force

  202. $ php vendor/bin/bref cli bref-kittyquotes-console -- \ doctrine:migrations:migrate --force

  203. $ php vendor/bin/bref cli bref-kittyquotes-console -- \ doctrine:migrations:migrate --force

  204. ++ 1 migrations executed ++ 1 sql queries

  205. None
  206. Handler PHP Layer Bootstrap Lambda function API Gateway Data Store

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

  208. API Gateway Data Store File Store Handler PHP Layer Bootstrap

    Lambda function
  209. parameters: app.path.kitty_images: ‘%env(APP_UPLOADS_BUCKET_NAME)%’ bin config services.yaml webpack.config.js template.yaml public src

    templates php
  210. # ... Resources: WebApplication: Type: AWS::Serverless::Function Properties: Environment: Variables: APP_UPLOADS_BUCKET_NAME:

    kittyquotes-uploads webpack.config.js template.yaml bin public src templates config php
  211. None
  212. bin config twig: # ... globals: kitty_uploads_bucket: '%app.path.kitty_images%' packages twig.yaml

    webpack.config.js template.yaml public src templates php
  213. bin public src templates config <!-- ... --> <img class="rounded"

    src="{{ kitty_uploads_bucket }}/{{ quote.kitty.image }}" alt="{{ quote.kitty.name }}” > <!-- ... --> template.yaml webpack.config.js quote quote-card.html.twig php
  214. None
  215. UPLOADING NEW IMAGES OF KITTIES

  216. pre-signed URLs

  217. Client Server Amazon S3

  218. Client Server Amazon S3 Get Pre-Signed URL

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

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

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

    One-time use URL One-time use URL
  222. 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
  223. 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
  224. 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
  225. bin public src config <?php namespace App\Controller; class AdminController extends

    BaseAdminController { /** * @Route("/kitty/upload-image", name="upload_kitty_image") */ public function uploadKittyImageAction() { //... } /** * @Route("/kitty/upload-image-pre-signed-url/{filename}", name="upload_kitty_image_pre_signed_url", methods={"GET"}) */ public function getPresignedUrlAction($filename) { //... } } php Controller AdminController.php templates template.yaml webpack.config.js
  226. bin public src templates config getPreSignedUrl = function (files) {

    file = files[0]; $.ajax({ url: '/admin/kitty/upload-image-pre-signed-url/' + file.name, type: "GET", dataType: "json", cache: false }) .done(function (data) { preSignedUrl = data.url; document.getElementById('kitty_image_image').value = data.filename; }); } function uploadFile() { $.ajax({ url: preSignedUrl, type: "PUT", data: file, contentType: file.type, processData: false }).done(function () { $("form[name='kitty_image']").submit(); }); } uploader.html.twig template.yaml webpack.config.js php
  227. None
  228. $ composer remove vich/uploader-bundle

  229. bin public src config Entity Kitty.php <?php namespace App\Entity; class

    Kitty { // ... /** * @Vich\UploadableField( * mapping="product_image", * fileNameProperty="imageName", * size=“imageSize" * ) * * @var File */ private $imageFile; /** * @ORM\Column(type="datetime") * * @var \DateTime */ private $updatedAt; templates template.yaml webpack.config.js php
  230. <?php namespace App\Entity; class Kitty { // ... /** *

    @param File|\ * Symfony\Component\HttpFoundation\File\UploadedFile $image */ public function setImageFile(?File $image = null): void { $this->imageFile = $image; if (null !== $image) { $this->updatedAt = new \DateTimeImmutable(); } } public function getImageFile(): ?File { return $this->imageFile; } } bin public src config Entity Kitty.php templates template.yaml webpack.config.js php
  231. None
  232. None
  233. None
  234. RECAP

  235. • Added Bref to an existing site • Fixed assets

    by moving them to a CDN • Moved session storage to the database • Ran console commands by adding another layer • Used pre-signed URLs for image uploading
  236. None
  237. PERFORMANCE

  238. None
  239. VPC Lambda function Handler PHP Layer Bootstrap API Gateway Data

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

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

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

  243. None
  244. None
  245. source: https://github.com/mnapoli/bref-benchmark JS vs PHP

  246. source: https://github.com/mnapoli/bref-benchmark JS vs PHP

  247. GOING FURTHER

  248. MICRO SERVICES

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

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

    PHP function [1] HTTP application [2] Console application > 0
  251. ROLLING YOUR OWN

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

  253. https://github.com/ mnapoli/bref

  254. runtime ./configure \ --build=x86_64-pc-linux-gnu \ --prefix=${INSTALL_DIR} \ --enable-option-checking=fatal \ --enable-maintainer-zts

    \ --with-config-file-path=${INSTALL_DIR}/etc/php \ --with-config-file-scan-dir=${INSTALL_DIR}/etc/php/conf.d:/var/tas --enable-fpm \ --disable-cgi \ --enable-cli \ --disable-phpdbg \ --disable-phpdbg-webhelper \ --with-sodium \ --with-readline \ --with-openssl \ --with-zlib=${INSTALL_DIR} \ --with-zlib-dir=${INSTALL_DIR} \ --with-curl \ --enable-exif \ --enable-ftp \ --with-gettext \ --enable-mbstring \ --with-pdo-mysql=shared,mysqlnd \ --enable-pcntl \ --enable-zip \ --with-pdo-pgsql=shared,${INSTALL_DIR} \ --enable-intl=shared \ --enable-opcache-file php php.Dockerfile
  255. $ make publish

  256. None
  257. None
  258. None
  259. None
  260. • Documentation • Speed & stability • A recommended method

    of creating your own runtimes • Better framework integrations
  261. JOIN US

  262. None
  263. https://bref.sh

  264. @nealio82 https://bref.sh