Serverless Symfony applications

This guide helps you run Symfony applications on AWS Lambda using Bref. These instructions are kept up-to-date to be compatible with the latest Symfony version.

Multiple demo applications are available on GitHub at github.com/brefphp/examples/Symfony.

Setup

First, follow the Installation guide to create an AWS account and install the necessary tools.

Next, in an existing Symfony project, install Bref and the Symfony Bridge package.

composer require bref/bref bref/symfony-bridge

If you are using Symfony Flex, it will automatically run the bref/symfony-bridge recipe which will perform the following tasks:

  • Create a serverless.yml configuration file optimized for Symfony.
  • Add the .serverless folder to the .gitignore file.

Otherwise, you can create the serverless.yml file manually at the root of the project. Take a look at the default configuration provided by the recipe.

You still have a few modifications to do on the application to make it compatible with AWS Lambda.

Since the filesystem is readonly except for /tmp we need to customize where the cache and logs are stored in the src/Kernel.php file. This is automatically done by the bridge, you just need to use the BrefKernel class instead of the default BaseKernel:

// src/Kernel.php

namespace App;

+ use Bref\SymfonyBridge\BrefKernel;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;

- class Kernel extends BaseKernel
+ class Kernel extends BrefKernel
{
    // ...

Deploy

The application is now ready to be deployed. Follow the deployment guide.

For better performance in production, warmup the Symfony cache before deploying:

php bin/console cache:warmup --env=prod

Console

As you may have noticed, we define a function of type "console" in serverless.yml. That function is using the Console runtime, which lets us run the Symfony Console on AWS Lambda.

To use it follow the "Console" guide.

Logs

By default, Symfony logs in stderr. That is great because Bref automatically forwards stderr to AWS CloudWatch.

However, if the application is using Monolog you need to configure it to log into stderr as well:

# config/packages/prod/monolog.yaml

monolog:
  handlers:
    # ...
    nested:
      type: stream
      path: php://stderr

Environment variables

Since Symfony 4, the production parameters are configured through environment variables. You can define them in serverless.yml.

# serverless.yml

provider:
  environment:
    APP_ENV: prod

The secrets (e.g. database passwords) must however not be committed in this file.

To learn more about all this, read the environment variables documentation.

Trust API Gateway

When hosting your site on Lambda, API Gateway will act as a proxy between the client and your function.

By default, Symfony doesn't trust proxies for security reasons, but it's safe to do it when using API Gateway and Lambda.

This is needed because otherwise, Symfony will not be able to generate URLs properly.

You should add the following lines to config/packages/framework.yaml

# config/packages/framework.yaml

framework:
  # trust the remote address because API Gateway has no fixed IP or CIDR range that we can target
  trusted_proxies: '127.0.0.1'
  # trust "X-Forwarded-*" headers coming from API Gateway
  trusted_headers: [ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port' ]

Note that API Gateway doesn't set the X-Forwarded-Host header, so we don't trust it by default. You should only whitelist this header if you set it manually, for example in your CloudFront configuration (this is done automatically in the Cloudfront distribution deployed by Lift).

Be careful with these settings if your app will not be executed only in a Lambda environment.

You can get more details in the Symfony documentation.

Getting the user IP

When using CloudFront on top of API Gateway, you will not be able to retrieve the client IP address, and you will instead get one of Cloudfront's IP when calling Request::getClientIp(). If you really need this, you will need to whitelist every CloudFront IP in trusted_proxies.

Assets

To deploy Symfony websites, assets need to be served from AWS S3. The easiest approach is to use the Server-side website construct of the Lift plugin.

This will deploy a Cloudfront distribution that will act as a proxy: it will serve static files directly from S3 and will forward everything else to Lambda. This is very close to how traditional web servers like Apache or Nginx work, which means your application doesn't need to change! For more details, see the official documentation.

First install the plugin

serverless plugin install -n serverless-lift

Then add this configuration to your serverless.yml file.

...
service: symfony

provider:
  ...

plugins:
  - ./vendor/bref/bref
  - serverless-lift

functions:
  ...

constructs:
  website:
    type: server-side-website
    assets:
      '/bundles/*': public/bundles
      '/build/*': public/build
      '/favicon.ico': public/favicon.ico
      '/robots.txt': public/robots.txt
      # add here any file or directory that needs to be served from S3

Because this construct sets the X-Forwarded-Host header by default, you should add it in your trusted_headers config, otherwise Symfony might generate wrong URLs.

# config/packages/framework.yaml

-   trusted_headers: [ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port' ]
+   trusted_headers: [ 'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-host' ]

Then, you can compile assets for production in the public directory

php bin/console assets:install --env prod
# if using Webpack Encore, additionally run
yarn encore production

Now run serverless deploy, Lift will automatically create the S3 bucket, a Cloudfront distribution and upload all specified files and directories to the bucket.

If you are not using Flex, update the serverless.yml file to exclude assets from the deployment (see the recipe)

For more details, see the Websites section of this documentation and the official Lift documentation.

Assets in templates

For the above configuration to work, assets must be referenced in templates via the asset() helper as recommended by Symfony:

- <img src="/images/logo.png">
+ <img src="{{ asset('images/logo.png') }}">

Symfony Messenger

It is possible to run Symfony Messenger workers on AWS Lambda.

A dedicated Bref package is available for this: bref/symfony-messenger.

Caching

As mentioned above the filesystem is readonly, so if you need a persistent cache it must be stored somewhere else (such as Redis, an RDBMS, or DynamoDB).

Using DynamoDB for cache

A Symfony bundle is available to use AWS DynamoDB as cache store: rikudou/psr6-dynamo-db-bundle

First install the bundle

composer require rikudou/psr6-dynamo-db-bundle

Thanks to Symfony Flex, the bundle comes pre-configured to run in Lambda.

Now, you can follow this section of the documentation to deploy your DynamoDB table using the Serverless Framework.

The kernel.terminate Event

The kernel.terminate event runs synchronously on Lambda.

That means that if you use this event, its listeners will be executed before the Lambda function returns its response. That will add latency to your response.

To run asynchronous tasks, use the Messenger component instead.