Serverless Symfony - Getting started
This guide helps you run Symfony applications on AWS Lambda using Bref. These instructions are kept up to date to target the latest Symfony version.
A demo application is available on GitHub at github.com/brefphp/examples (opens in a new tab).
Setup
First, follow the Setup guide to create an AWS account and install the necessary tools.
Next, in an existing Symfony project, install Bref and the Symfony Bridge package (opens in a new tab).
composer require bref/bref bref/symfony-bridge --update-with-dependencies
If you are using Symfony Flex (opens in a new tab), it will automatically run the bref/symfony-bridge recipe (opens in a new tab) 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 (opens in a new tab) 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
:
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
{
// ...
Deployment
Let's deploy the application to AWS Lambda:
serverless deploy
When finished, the deploy
command will show the URL of the application.
Deploying for production
At the moment, we deployed our local codebase to Lambda. When deploying for production, we probably don't want to deploy:
- development dependencies,
- our local
.env
files, - or any other dev artifact.
Follow the deployment guide for more details.
Separately, on cold starts, Symfony will boot with an empty cache directory. It will build the cache on the fly, which can take a few seconds depending on the complexity of the application.
To optimize cold starts, you can deploy the application with a warm cache. In a simple application it means that the deployment script should include cache:warmup
to look something like this:
# Install dependencies
composer install --classmap-authoritative --no-dev --no-scripts
# Warmup the cache
bin/console cache:clear --env=prod
# Disable use of Dotenv component
echo "<?php return [];" > .env.local.php
serverless deploy
Optimizing caches
When running Symfony on Lambda you should avoid writing to the filesystem. If you pre-warm the cache before deploying you are mostly fine. But you should also make sure you never write to a filesystem cache like cache.system
or use a pool like:
framework:
cache:
pools:
my_pool:
adapter: cache.adapter.filesystem
If you don't write to such cache pool you can optimize your setup by not copying the var/cache/pools
directory. The change below will make sure to symlink the pools
directory.
class Kernel extends BrefKernel
{
// ...
+ protected function getWritableCacheDirectories(): array
+ {
+ return [];
+ }
}
Troubleshooting
In case your application is showing a blank page after being deployed, have a look at the logs.
Website assets
Have a look at the Website guide to learn how to deploy a website with assets.
Symfony Console
As you may have noticed, we define a function named "console" in serverless.yml
. That function is using the Console runtime, which lets us run the Symfony Console on AWS Lambda.
For example, to execute an bin/console
command on Lambda, run the command below:
serverless bref:cli --args="<console command and options>"
For example:
serverless bref:cli --args="doctrine:migrations:migrate"
For more details follow the "Console" guide.
Logs
By default, Symfony logs to stderr
. That is great because Bref automatically forwards stderr
to AWS CloudWatch.
However, if your application is using Monolog, you need to configure it to log to stderr
as well:
monolog:
handlers:
# ...
nested:
type: stream
path: php://stderr
Trust API Gateway
When hosting a website on Lambda, API Gateway acts as a proxy between the client and your Lambda 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.
Add the following lines to 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).
You can get more details in the Symfony documentation (opens in a new tab).
Be careful with these settings if your app is also executed outside a Lambda environment (for example on a public server).
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 (opens in a new tab)
in trusted_proxies
.
The kernel.terminate
event
The kernel.terminate
event (opens in a new tab) 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 Symfony Messenger instead.