How to Deploy a PHP Symfony Application on AWS Lambda Using Bref?

May 23, 2019Thibaud Lemaire10 min read

thumbnail

Serverless is a trendy cloud-computing execution model aiming to release developers from taking care of server management and to bill on demand for the used resources. Developers just have to provide their application’s code while the cloud provider is in charge of running it in response to events.

Initially confined to machine learning tasks, expert system, or IoT, Serverless now answers to more and more varied use cases and is quite relevant to deploy web applications. It is particularly interesting to replace architecture facing high episodic loadings.

What is Bref?

Bref is an open source tool suite that helps to deploy PHP applications on Lambda functions. It provides PHP runtimes for AWS Lambda, a library to interface PHP code with Lambda API, deployment tools and rich documentation.

AWS Lambda can natively run JS, Python, C#, Go, Java or Ruby. To use Lambda with any other language, like PHP, we can add a custom runtime using its layer feature. Bref provides layers for PHP.

A layer is an overlay to the Lambda base runtime. It provides precompiled executables and can define the way Lambda functions handle trigger events.

How does Bref work?

Bref's layer makes Lambda able to run PHP code. It serves as an entry point for the Lambda function. The HTTP request is handled by AWS API Gateway and encapsulated into a trigger event. The Lambda function is invoked with this event as a payload. Bref is responsible for extracting the HTTP request and pass it to the front controller of our application. The response follows the exact inverse path.

Bref acts as a proxy that makes our application Lambda-agnostic. Contrary to popular belief, it is not necessary to split our application into several tiny functions. Bref allows to port regular PHP application without adapting its architecture. Here, the front controller (index.php) serves as the unique entry point for our application. However, if your application is large, it is probably better to split it into micro services to improve performance.

Bref architecture

How to deploy a Symfony application with Bref?

In this part, we will deploy a simple Symfony application on a Lambda function.

To deploy a web application using a Lambda function, certain constraints must be respected. First, most of the filesystem is read-only. The only directory that can be written in is /tmp. We must, therefore, ensure that our application does not try to write anywhere on the disk to avoid throwing an exception. Then, the execution time must be less than 30 seconds when the Lambda is invoked by an HTTP request. Finally, Lambda functions are not intended to serve static resources (images, js, css). They must be provided by another means, such as a CDN. We'll learn how to work around read-only constraints and where to store logs and static files.

The final architecture of our application will include:

  • The Lambda function invoked by API Gateway
  • An S3 bucket for storing the executable package
  • A CloudWatch log for storing our Logs
  • An S3 bucket for storing our static files
  • An API Gateway proxy acting as an entry point for our web application

Application architecture

Bref’s documentation is clear and gives examples of Symfony deployment. The purpose of this article is not to paraphrase the official documentation but rather to give you tips to optimize deployment and help you understand the purpose of each manipulation.

1. Installation

Start with a fresh Symfony application following the official Symfony documentation. To deploy your app on AWS Lambda using Bref, you’ll need AWS CLI and AWS Sam CLI. Install and configure these two tools following the official Bref documentation.

Tip for AWS beginners: keep the default region at first (us-east-1). Indeed AWS regions are a common source of confusion because AWS CLI uses us-east-1 by default. When you start to be comfortable you can, of course, switch to your preferred region.

Finally, install Bref using Composer :

composer require bref/bref

2. Writing the deployment template

To deploy your application, Bref uses AWS Sam. This tool provides shorthand syntax to describe serverless architecture using a YAML file. Sam helps package the application and then generates a CloudFormation stack to provision all necessary services (API Gateway, Lambda, VPC, etc.).

To deploy a Symfony application, we need two Lambdas. One to handle HTTP requests and serve our web application and the other to execute commands inside Symfony console (to run migrations for example).

Lambdas

Template version

A Sam template file begins with the template format version. Create a template.yaml file at the root of your project and add :

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Environment variables

We can define environment variables provided to Lambda instances. Let’s declare one to tell Symfony which environment to use :

Globals:
    Function:
        Environment:
            Variables:
                APP\_ENV: prod

Website Lambda

Then we can declare our first Lambda function, the one to serve our web application. Let’s name it symfony-website. This function must inherit from the PHP-FPM layer provided by Bref. You can find the layer reference (ARN) corresponding to your AWS region on runtimes.bref.sh.

Resources:
    Website:
        Type: AWS::Serverless::Function
        Properties:
            FunctionName: 'symfony-website'
            CodeUri: .
            Handler: public/index.php
            Timeout: 30 # in seconds (API Gateway has a timeout of 30 seconds)
            MemorySize: 1024
            Runtime: provided
            Layers:
                - 'arn:aws:lambda:us-east-1:209497400698:layer:php-73-fpm:6'
            Events:
                HttpRoot:
                    Type: Api
                    Properties:
                        Path: /
                        Method: ANY
                HttpSubPaths:
                    Type: Api
                    Properties:
                        Path: /{proxy+}
                        Method: ANY
  • CodeUri: . asks Sam to zip the entire project directory in your executable package.
  • Hander: public/index.php is the entry point of your application, in our case the front controller of Symfony.
  • You cannot set a Timeout higher than 30 seconds because it’s a limit set by API Gateway.
  • You can adjust the allocated memory size keeping in mind that computing power depends on it.
  • The Events key describes how your lambda can be invoked. Here we create a catch-all proxy thanks to two API Gateway routes.

Console Lambda

Following the same recipe, we can define our 'symfony-console' lambda. The two layers to inherit from are PHP and Console. You can find their ARN on runtimes.bref.sh.

Resources:    
    Website:
        # ...
    Console:
        Type: AWS::Serverless::Function
        Properties:
            FunctionName: 'symfony-console'
            CodeUri: .
            Handler: bin/console
            Timeout: 120 # in seconds
            Runtime: provided
            Layers:
                - 'arn:aws:lambda:us-east-1:209497400698:layer:php-73:6' # PHP
                - 'arn:aws:lambda:us-east-1:209497400698:layer:console:6' # The "console" layer
  • Notice the handler here, same as the command you type in your terminal
  • The timeout value can be increased up to 15 minutes because this function won’t be invoked by API Gateway.

CloudFront output

Finally, you can ask CloudFormation to display the access URL of your API. It’s a convenient way to retrieve the URL of your application.

Outputs:
    Website:
        Description: 'URL of our function in the \*Prod\* environment'
        Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'

At this point, your Sam template should look like that.

3. Updating Symfony configuration

Symfony configuration is straightforward. We just have to deal with the read-only filesystem constraint.

As the only writable directory in a Lambda runtime is /tmp we must ensure that none of the Symfony modules try to write elsewhere. To do so, let’s add the following code in your Kernel.php.

// src/Kernel.php
// ...
public function getLogDir()
    {
        // When on the lambda only /tmp is writable
        if (getenv('LAMBDA\_TASK\_ROOT') !== false) {
            return '/tmp/log/';
        }
        return $this->getProjectDir().'/var/log';
    }

As /tmp is flushed on container deletion you must use external log storage. Bref redirects stderr stream to CloudWatch. Update your Symfony configuration to make Monolog log into stderr :

# config/packages/prod/monolog.yaml
monolog:
    handlers:
        nested:
            path: 'php://stderr'

Contrary to what is written in the documentation of Bref, I recommend to keep the original cache directory and to warm it up before deployment. Indeed, if you use /tmp as cache directory, every time a new lambda container is initialized, Symfony will regenerate its cache (i.e. compile views, annotations, ORM proxies, etc.). This will increase your application’s response time by several seconds on the first request.

Keeping the default cache directory is made possible thanks to Symfony 4 best practices, that make var/cache/ directory typically read-only and warmable. For bundles that are not compliant with these new best practices, you can override the default cache directory in their configuration to make them write into the /tmp directory. It’s the case for Twig :

# config/packages/prod/twig.yaml
twig:
    cache: '/tmp/cache/twig'

If you encounter errors in your application, don’t forget to check in your logs (CloudWatch) if a Bundle is trying to write in the default cache folder. If so, change its cache destination the same way we did with Twig.

4. Serving assets

Our Lambda won’t serve the static files, it would be inefficient and expensive. We will host them in an AWS S3 bucket. To do so, let’s create an S3 bucket :

aws s3 mb s3://YOUR_ASSETS_BUCKET_NAME

Then we must change Symfony prod configuration to use this bucket as assets provider:

# config/prod/assets.yaml 
framework:
   assets:
       base\_urls:
           - 'https://YOUR\_ASSETS\_BUCKET\_NAME.s3.amazonaws.com/'

If you use Webpack Encore to process your assets, add the following to your Webpack configuration :

// webpack.config.js
// ...
if (Encore.isProduction()) { 
    Encore.setPublicPath(
        'https://YOUR\_ASSETS\_BUCKET\_NAME.s3.amazonaws.com/build'
    );
    Encore.setManifestKeyPrefix('build/');
}

5. Deployment

First, you shouldn’t use your development directory to deploy because we’ll need to clean up dev libraries before packing your application. Before the first deployment, create an S3 bucket to upload your code package in :

aws s3 mb s3://YOUR\_CODE\_BUCKET\_NAME

Deployment is straightforward and can be split into 4 steps :

  1. Clean up your project’s dependencies
  2. Warm up your cache and build your assets (if needed)
  3. Pack your project using Sam CLI and upload it into your code bucket
  4. Deploy your Lambda architecture and upload your assets

The standard deployment process is described in the documentation. You can automate it with a simple Makefile. I just added yarn commands to deal with Webpack Encore and the deployment of the assets:

all: install build package deploy

install:
    composer install --optimize-autoloader --no-dev --no-scripts
    yarn --production   # only if you use Webpack Encore

build:
    yarn build          # only if you use Webpack Encore
    php bin/console cache:clear --no-debug --no-warmup --env=prod
    php bin/console cache:warmup --env=prod

package:
    sam package \\
     --template-file template.yaml \\
     --output-template-file .stack.yaml \\
     --s3-bucket YOUR\_CODE\_BUCKET\_NAME

deploy:    
    sam deploy \\
     --template-file .stack.yaml \\
     --capabilities CAPABILITY\_IAM \\
     --region us-east-1 \\
     --stack-name YOUR\_STACK\_NAME    
    aws s3 sync public s3://YOUR\_ASSETS\_BUCKET\_NAME --delete --acl public-read

Edit this template with your AWS region, buckets and stack names then run make to deploy your project.

When the deployment is complete, connect to the CloudFormation console to retrieve the URL of your application.

Cloudformation console

Enjoy your first serverless Symfony application!

Conclusion

We learned how to define the template needed to generate a Symfony serverless stack. We also learned how to automate the deployment of our Lambda functions as well as the assets of our application.

In a coming soon post we’ll talk about connecting a serverless Symfony app to a relational database and how to make this stack production-ready in terms of security and performances. We'll talk about cold-start, security groups, custom domain names, shared cache, and sessions. To be continued...

Thibaud Lemaire

Thibaud Lemaire

Web Developer at Theodo