Symfony on a lambda: sessions
Matsounga Jules
Posted on January 20, 2020
Symfony on a lambda: sessions
If you are lost, you can find the project on Github: link
Each branch will match a chapter.
Sessions handling
Handling session is quite important when dealing with a website. It can be used to store many data that we will be able to use easily everywhere on the site.
Sessions are generated by the server, send to the client through a cookie that the client will reuse to authenticate on the application.
By default, Symfony save the session in memory on the server. But lambda don't last forever, so sessions stored on the lambda could disappear at any moment.
Fortunately, we can store sessions on database. In our case, DynamoDB. DynamoDB is a key-value and document database provided by AWS.
Creation of the session table
We will need new resource on our cloudformation, a table to store our sessions.
Let's edit our serverless.yml file to add this new resource.
//serverless.yml
service: [name of your project]
provider:
// ...
custom:
// ...
plugins:
// ...
functions:
// ...
resources:
Resources:
// ...
SessionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:custom.prefix}-sessionsTable
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
As I said earlier, DynamoDB is a key-value document database. That's mean we will have to define the key that we will use to retrieve our document. Unlike SQL database, we don't have to define a entire structure as we will store document. See more information here.
We create a new resource of type: AWS::DynamoDB::Table
. This table will have the name sessionsTable prefixed by the custom prefix we created on the previous chapter.
Then we define our HASH key that we will use to get our document: id
and say it is of type string S
.
We also define the ProvisionedThroughput, that define the number of read by second that we can achieve. As it's only a exercise, 1 is enough. (More information here: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html)
We can now create our new table on AWS using serverless deploy
command.
Storing session in the table
Now that our table is created, let's change the way Symfony store our session.
At first, we need to install the SDK provided by AWS to interact with the table. Type composer require aws/aws-sdk-php ^3.112
to install it.
We will then create a service that will handle our sessions.
In src/Services create a file DynamoDbSessionHandlerFactory.php that will contain the following class:
<?php
namespace App\Services;
use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\SessionHandler;
class DynamoDbSessionHandlerFactory
{
public static function build(DynamoDbClient $dbClient): SessionHandler
{
return SessionHandler::fromClient($dbClient, ['table_name' => 'sessionsTable']);
}
}
We will use this factory to create our service.
In src/Services create a file AwsClient.php that will contain the following class:
<?php
namespace App\Services;
use Aws\DynamoDb\DynamoDbClient;
use Aws\S3\S3Client;
use Aws\Sdk;
class AwsClient
{
/** @var Sdk */
protected $sdk;
public function __construct(
string $region,
string $version,
string $dynamoDbCredentialKey,
string $dynamoDbCredentialSecretKey
) {
$this->sdk = new Sdk([
'region' => $region,
'version' => $version,
'Dynamodb' => [
'credentials' => [
'key' => $dynamoDbCredentialKey,
'secret' => $dynamoDbCredentialSecretKey
]
]
]);
}
public function dynamoDbClient(): DynamoDbClient
{
return $this->sdk->createDynamoDb();
}
}
This service handle the connection to AWS and create the required client, here the client for DynamoDB.
We can now define both of this services so Symfony can use them correctly. Edit config/services.yaml:
// ...
services:
// ...
App\Services\AwsClient:
arguments:
$region: '%env(resolve:APP_AWS_REGION)%'
$version: '%env(resolve:APP_AWS_VERSION)%'
$dynamoDbCredentialKey: '%env(resolve:APP_AWS_DYNAMODB_CREDENTIAL_KEY)%'
$dynamoDbCredentialSecretKey: '%env(resolve:APP_AWS_DYNAMODB_CREDENTIAL_SECRET)%'
App\Services\SessionHandler:
factory: ['App\Services\DynamoDbSessionHandlerFactory', 'build']
arguments:
- "@=service('App\\\\Services\\\\AwsClient').dynamoDbClient()"
- '%env(resolve:APP_AWS_RESOURCE_PREFIX)%'
calls:
- method: register
Our AwsClient require some environment variables. Let's define them.
Edit .env at the root of your folder and add the following lines:
APP_AWS_REGION=''
APP_AWS_VERSION=''
APP_AWS_DYNAMODB_CREDENTIAL_KEY=''
APP_AWS_DYNAMODB_CREDENTIAL_SECRET=''
APP_AWS_RESOURCE_PREFIX=''
Do not put your credentials in this file ! As it will be committed and push to your repository. You don't want to have your secrets expose to internet.
Now create .env.local at the root of your projects and add the previous lines. This files is ignored thanks to .gitignore, so you can put your information here.
APP_AWS_RESOURCE_PREFIX
is the custom prefix define in serverless.yml
: [service-name]-[region]-[stage]
We can now edit the handler of session of Symphony. Edit config/packages/framework.yaml
framework:
secret: '%env(APP_SECRET)%'
session:
handler_id: App\Services\SessionHandler
cookie_secure: auto
cookie_samesite: lax
php_errors:
log: true
Our handler_id refer to our service created with the factory.
As a quick test to see if it's working. Edit src/Controller/HomeController.php
// ...
public function homeAction(SessionInterface $session): Response
{
$session->set('foo', 'bar');
return $this->render('home/index.html.twig');
}
Now, go in the AWS Console, search for DynamoDB, click on the link. At the left, select Tables then your session table. In elements, you should see the data stored.
Making it work on the lambda
Almost everything is already set to have it working on a lambda, except that we don't have any of the environments variables that we just added.
We will have to add them in our serverless.yml
service: // ...
provider:
// ...
environment:
// ...
APP_AWS_REGION: 'eu-west-2'
APP_AWS_VERSION: 'latest'
APP_AWS_DYNAMODB_CREDENTIAL_KEY: ''
APP_AWS_DYNAMODB_CREDENTIAL_SECRET: ''
APP_AWS_RESOURCE_PREFIX: ${self:custom.prefix}
As said earlier, we cannot store our keys on files that are committed to our repository. This one is, so we can't put our key here neither. But we still need to have this information to be passed to serverless and we can't make a .local this time.
We will use AWS Systems Manager to secure our keys. Visit the AWS Console, search for Systems Manager and select Parameter Store in the left panel.
We can now create our 2 keys, click on create parameter:
- Name: [service]/[stage]/key
- Description: Add what you want
- Tier: Standard
- Type: String
- Value: Put your key here
Then click on Create parameter
Repeat the operation for the secret (name: [service]/[stage]/secret)
We can now reference both of this parameter in serverless.yml
service: // ...
provider:
// ...
environment:
APP_ENV: prod
APP_AWS_REGION: 'eu-west-2'
APP_AWS_VERSION: 'latest'
APP_AWS_DYNAMODB_CREDENTIAL_KEY: ${ssm:/${self:service}/${self:provider.stage}/key}
APP_AWS_DYNAMODB_CREDENTIAL_SECRET: ${ssm:/${self:service}/${self:provider.stage}/secret}
APP_AWS_RESOURCE_PREFIX: ${self:custom.prefix}
ssm will interact with the Systems Manager to get the data at the path specified.
By default, a lambda cannot interact with other resource for security purpose. So will have to give it the right to interact with DynamoDB, otherwise we will get a fancy error about permission.
Still in serverless.yml, add the following lines:
service: // ...
provider:
// ...
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:[your region]:*:table/*"
This will give the lambda the ability to do the action defined in all the table. Of course, we can be more precise on which table we want to give permission and what permission we want but I prefer to keep it simple here.
With all of this, we can deploy the solution again with serverless deploy
.
If you go back in the DynamoDB console, and then you browse the application through the link in the output at the end of the deploy command, you should see a new entry in the table. Well done !
Cleanup
First, let's remove the $session->set
from HomeController.php, we don't need that anymore.
In a second time, we now have sessions working on lambda, but for local development we want to use the default storage method. We use our computer so we don't need to store our sessions in DynamoDB. To do so, we will have to override the session handling when we are in dev.
Create a file framework.yaml in config/packages/dev
framework:
session:
handler_id: null
This will prevent Symfony to use our new service and use the default method.
Now that our session are working, it's possible to deal with authentication ! See you in the next chapters
Posted on January 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.