Using Amazon DynamoDB for Session Storage in PHP/Yii2 Application (with AWS CDK)
Tags: aws, php, container, dynamodb
Published: 2020-03-28 17:25:51

After we successfully deploy a PHP application in cluster environment, we still need some efforts to make the application to be stateless. One of them is building centralized session management. In this post, I will demonstrate how we can provision an Amazon DynamoDB table using AWS CDK and how to configure our application to securely use the created table for session storage. I will also demonstrate how to set up the local environment to test the session using Docker compose and Localstack.

This post is part of series [Building Modern PHP/Yii2 Application using AWS](https://dev.to/petrabarus/building-modern-php-yii2-application-using-aws-3ej) (or see [here for Indonesian version](https://blog.petrabarus.net/membangun-aplikasi-php-yii2-modern-dengan-aws/)).

By default, PHP will store its session into local directory, usually in `/tmp` for Linux distributions. Since we are using load balanced multiple PHP applications, this can cause problem. We cannot really guarantee a single user will access the same application that will access the same file. Yes, we can set the [Application Load Balancer to use Sticky Session](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#sticky-sessions) so the load balancer can use cookie to target same instance for the same user. But consider when container stops because autoscaling suddenly scale down or case of the container fails. This will log  users out who are still using that instance and cause bad experience.

![Sessions in Multiple App](https://dnhfy6riqbv93.cloudfront.net/images/DynamoDB-Page-3(1).png)

To solve this, we are going to use DynamoDB as a centralized session storage. Since DynamoDB is a fully managed key-value database and it can delivers within single-digit miliseconds, it is a fitting solution for this challenge.

## Using Session in Yii2

We are going to pick up from previous article where we create CI/CD Pipeline. You can access [the code here](https://github.com/petrabarus/aws-sample-modern-yii2-app/tree/part-2-ci-cd-pipeline).

In order to use session feature in Yii2, we will use predefined `session` from [Yii application components](https://www.yiiframework.com/doc/guide/2.0/en/structure-application-components). We can set a value into the component and the value will persist during the user's sessions.

The following code shows a controller where we are going to demonstrate using sessions.

{% gist https://gist.github.com/petrabarus/22ad66f6a0768ace91335bc58eb439f3 file=TestController.php %}

Controller `TestController` will be able to receive query parameter `name`. If we pass a value with the query parameter `name`, it will save into the session. Otherwise it will get the previously set sessions. The controller will output the value stored in the session and the ID of the session.

After that we will enable routing so that we can access the controller through path `/test/index`. Add the following configuration to override the default [`urlManager` component](https://www.yiiframework.com/doc/guide/2.0/en/structure-application-components)..

```php
//file: /config/web.php
//put this in the `components` list of values.

//	'components' => [
//      .....
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
            ],
        ],
```

We can try deploying the new code using CDK command `deploy`.  (I changed the stack name to `MyWebStack` to differentiate between the stack in the previous article).

```command
cdk deploy MyWebStack
```

You can test by accessing URL `<URL>/test/index`, try setting the query parameter `name` using URL `<URL>/test/index?name=foobar`, and try the URL without query paramater. Note that sometimes the `foobar` shows up in the browser, but sometimes it does not.

![Sometimes it shows](https://dnhfy6riqbv93.cloudfront.net/images/Screen+Shot+2020-03-28+at+12.50.51+PM.png)

![Another times it does not](https://dnhfy6riqbv93.cloudfront.net/images/Screen+Shot+2020-03-28+at+12.50.41+PM.png)

## DynamoDb Session Class

Fortunately for us, AWS SDK for PHP already provides an [interface to use DynamoDB as session handler for PHP](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/service_dynamodb-session-handler.html) with class [`Aws\DynamoDb\SessionHandler`](https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.DynamoDb.SessionHandler.html). This class implements the core PHP interface [SessionHandlerInterface](https://www.php.net/manual/en/class.sessionhandlerinterface.php). Yii2 also provides a nice binding with the `SessionHandlerInterface` with its class [`yii\web\Session`](https://www.yiiframework.com/doc/api/2.0/yii-web-session) which is the default class for session. We just need to pass the instance of DynamoDb `SessionHandler` to `handler` attribute of Yii2 `Session` class.

To install AWS SDK PHP composer dependencies, execute the following command.

```bash
composer require aws/aws-sdk-php
```

We then need to create a `Session` class to wrap the session access. The class will extends `yii\web\Session`, instantiate the `SessionHandler` from DynamoDB client, and pass it to `handler`.

{% gist https://gist.github.com/petrabarus/22ad66f6a0768ace91335bc58eb439f3 file=Session.php %}

## Setting Up Local Environment

Now we are going to test this in the local environment. We are going to use [Localstack](https://github.com/localstack/localstack) docker image to mock DynamoDb. Localstack provides various mocks for AWS services, but here we are going to use only DynamoDb. This way, we don't need to test in the AWS environment that can take sometime to wait.

{% gist https://gist.github.com/petrabarus/22ad66f6a0768ace91335bc58eb439f3 file=Dockerfile %}

The DynamoDb will be accessible through url `http://localstack:4569`. We will set this in the environment variables alongside the Table name. We also need to provide fake AWS credentials and region in the environment.

```yml
environment:
  AWS_REGION: 'ap-southeast-1'
  AWS_ACCESS_KEY_ID: 'fake-access-key'
  AWS_SECRET_ACCESS_KEY: 'fake-secret-key'
  DYNAMODB_SESSION_TABLE_NAME: 'Sessions'
  DYNAMODB_ENDPOINT_URL: 'http://localstack:4569'
```

To initialize the DynamoDb table for storing sessions we will use the script below. The script will create a new DynamoDb table in the localstack. The table will be called `Sessions` and will have one hash key named `id`. It should be executed after the docker compose run.

{% gist https://gist.github.com/petrabarus/22ad66f6a0768ace91335bc58eb439f3 file=localstack-setup.sh %}

## Configuring Local Access

The following code will override the default `session` component to use our `Session` class. It will obtain the environment variables `AWS_REGION`, `DYNAMODB_ENDPOINT_URL` and `DYNAMODB_SESSION_TABLE_NAME` to the class so the class can pass it to the DynamoDB client and session handler.

```php
//file: /config/web.php
//put this in the `components` list of values.

//	'components' => [
//      .....
        'session' => [
            'class' => app\components\Session::class,
            'clientConfigs' => [
                'version' => '2012-08-10',
                'region' => $_ENV['AWS_REGION'],
                'endpoint' => $_ENV['DYNAMODB_ENDPOINT_URL'] ?? null
            ],
            'sessionConfigs' => [
                'table_name' => $_ENV['DYNAMODB_SESSION_TABLE_NAME']
            ]
        ]
```

The final configuration will look like below.

{% gist https://gist.github.com/petrabarus/22ad66f6a0768ace91335bc58eb439f3 file=web.php %}

## Running in Local

We now have all the things we need to run the local environment. Execute this command to run the application.

```
docker-compose up -d
```

You may want to wait until all the containers complete the start up. Then execute this command to initialize the session table.

```
docker-compose exec localstack sh ./localstack-setup.sh
```

The site can be accessed in the `http://localhost:8080`. You can try the test controller by `http://localhost:8080/test/index` and `http://localhost:8008/test/index?name=petra`. Of course the session will be persistent since we only have 1 server to access.

## Writing CDK Code

We are going to write the CDK code to provision DynamoDB table to store the sessions. But first we need to install the [DynamoDB CDK module](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-dynamodb-readme.html) in the Node packages.

```
npm update
npm install @aws-cdk/aws-dynamodb
```

Here we only need to update the `WebApp` construct. The final code will look like this.

{% gist https://gist.github.com/petrabarus/22ad66f6a0768ace91335bc58eb439f3 file=WebApp.ts %}

Now the code explanation. 

The following code will provision a new Table with a single partition key called `id` and type String. This `id` attribute will store the session ID.

```typescript
    private createSessionTable(): dynamodb.Table {
        return new dynamodb.Table(this, 'Sessions', {
            partitionKey: {
                name: 'id',
                type: dynamodb.AttributeType.STRING,
            }
        });
    }
```

After that, the following code will grant permission for the Fargate task's [Task Role](https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html) to update and write session to DynamoDB. We only need five permissions for this `GetItem`, `UpdateItem`, `DeleteItem`, `Scan`, and `BatchWriteItem`. 

```typescript
const actions = [
    "dynamodb:GetItem",
    "dynamodb:UpdateItem",
    "dynamodb:DeleteItem",
    "dynamodb:Scan",
    "dynamodb:BatchWriteItem"
]
this.sessionTable.grant(taskDefinition.taskRole, ...actions);
```

Finally, in the following code, we will pass through the environment variable `AWS_REGION` the region for the DynamoDB client to resolve and `DYNAMO_SESSION_TABLE_NAME` for the created DynamoDB table name.

```typescript
taskImageOptions: {
    image: ecs.ContainerImage.fromAsset('.'),
    environment: {
        AWS_REGION: region,
        DYNAMODB_SESSION_TABLE_NAME: this.sessionTable.tableName,
    }
},
```

To apply this new resources and configurations, execute the following command.

```bash
cdk deploy MyWebStack
```

If you open the [DynamoDB console](https://console.aws.amazon.com/dynamodb/), you can see the created new Table.

![New Created Table](https://dnhfy6riqbv93.cloudfront.net/images/Screen+Shot+2020-03-29+at+12.10.41+AM.png)

You can see the sessions are already filling up.

![Sessions](https://dnhfy6riqbv93.cloudfront.net/images/Screen+Shot+2020-03-29+at+12.10.50+AM.png)

You can check the assigned value from the application by scanning the record ID of the row with the session ID. The image below shows that we have set `name` in the `data` attribute.

![Our Session](https://dnhfy6riqbv93.cloudfront.net/images/Screen+Shot+2020-03-29+at+12.14.51+AM.png)

## Wrapping Up

That concludes the tutorial on how to use DynamoDB for sessions storage. **NOTE** If you are still experimenting with this, please don't forget to destroy the resources by executing `cdk destroy` to avoid incurring cost.

I hope the explanation helps your development effort. I will remind you that this post is part of series [Building Modern PHP/Yii2 Application using AWS](https://dev.to/petrabarus/building-modern-php-yii2-application-using-aws-3ej). If there is any use cases that you would like to see me cover,  please don't hesitate put it in the comments. Looking forward for the next post!