Symfony on a lambda: sessions
Tags: php, symfony, lambda, serverless
Published: 2020-01-20 07:20:47

---
title: Symfony on a lambda: sessions
published: true
description: Learn how to handle sessions in a serverless environment 
tags: PHP, Symfony, Lambda, Serverless
series: Symfony on a lambda
---

#Symfony on a lambda: sessions

If you are lost, you can find the project on Github: [link](https://github.com/hyoa/symfony-on-lambda)
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.

```yaml
//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](https://aws.amazon.com/dynamodb/?nc1=h_ls).

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
<?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
<?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**:

```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**

```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** 

```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**

```yaml
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**

```yaml
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:

```yaml
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**

```yaml
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