Skip to content

DynamoDB-based session store for express-session, using the AWS SDK for JS v3, offering configurability for cost, performance, and reliability, as well as examples, cost calcs, and tips not found in other DynamoDB session stores.

Notifications You must be signed in to change notification settings

pwrdrvr/dynamodb-session-store

Repository files navigation

npm (scoped) License: MIT API Docs Build - CI Publish Docs

Overview

DynamoDB-based session store for express-session, using the AWS SDK for JS v3, offering configurability for cost, performance, and reliability not found in other DynamoDB session stores.

DynamoDB is an excellent choice for session stores because it is a fully managed service that is highly available, durable, and can scale automatically (to nearly unlimited levels) to meet demand. DynamoDB reads will typically return in 1-3 ms if capacity is set correctly and the caller is located in the same region as the Table.

Getting Started

Installation

The package is available on npm as @pwrdrvr/dynamodb-session-store

npm i @pwrdrvr/dynamodb-session-store

Importing

import { DynamoDBStore } from '@pwrdrvr/dynamodb-session-store';

Necessary IAM Policy

The following IAM permissions are required for the DynamoDB Table:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowDynamoDBAccess",
      "Effect": "Allow",
      "Action": [
        "dynamodb:DescribeTable",
        "dynamodb:GetItem",
        "dynamodb:UpdateItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/dynamodb-session-store-test"
    }
  ]
}

Example Usage

See the examples directory for more examples

import express from 'express';
import session from 'express-session';
import { DynamoDBStore } from '@pwrdrvr/dynamodb-session-store';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

const app = express();
const dynamoDBClient = new DynamoDBClient({});

app.use(
  session({
    store: new DynamoDBStore({
      tableName: 'some-table',
      dynamoDBClient,
      touchAfter: 60 * 60, // 60 minutes in seconds
    }),
    secret: 'yeah-change-this',
    cookie: {
      maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days in milliseconds
    },
    resave: false,
    saveUninitialized: false,
  }),
);

app.get('/login', (req, res) => {
  console.log(`Session ID: ${req.session?.id}`);
  req.session.user = 'test';
  res.send('Logged in');
});

app.get('/*', (req, res) => {
  res.status(200).send('Hello world');
});

app.listen(Number.parseInt(PORT, 10), () => {
  console.log(`Example app listening on port ${port}`);
});

Supported Field Types

The following field types are fully supported by the DynamoDBStore:

  • string
  • number
  • boolean
  • object

The following field types are partially supported by the DynamoDBStore:

  • Date
    • Stored as a string in ISO 8601 format
    • Will be returend as a string in ISO 8601 format
    • Cannot be automatically converted back into a Date object since it is not known which fields were originally Date objects vs date strings
    • Note: connect-dynamodb serializes Date objects to strings as well and also does not support automatic conversion back to Date objects since it serializes using JSON.stringify() and JSON.parse()

API Documentation

After installing the package review the API Documentation for detailed on each configuration option.

Features

  • Configurability of Strongly Consistent Reads
    • Off by default as Eventually Consistent Reads are less expensive and more reliable
  • Configurability of PoC-level Table creation
    • This should only be used during PoCs, else tables will be created in any accounts that developers point at using credentials that have permissions to create tables
  • Cost reduction through reducing the TTL write on every read via .touch() calls from express-session
    • This PR for express-session would have made that a configurable option for every session store, but alas, it was rejected in favor of implementing the same thing 15+ times
    • These TTL writes consumed WCUs based on the size of the entire session, not just the expires field
    • These writes were 10x more expensive than reads for under 1 KB session with ConsistentReads turned off
    • These writes were 40x more expensive than reads for 3-4 KB session with consistent reads turned off
      • Cost of 1 WCU is 5x that of 1 RCU
      • Eventually Consistent Read of 4 KB takes 0.5 RCU
      • Write of 1 KB takes 1 WCU, write of 4 KB takes 4 WCU

Configuration Tips

  • Use a Table per-region if the app is deployed in multiple regions
  • Use a Table per-environment if the app is deployed in multiple environments (e.g. dev/qa/prod)
  • Use a Table unique to the session store - do not try to overload other data into this Table as the scaling and expiration needs will not overlap well
  • For applications attached to a VPC (including Lambda's attached to a VPC), use a VPC Endpoint for DynamoDB to avoid the cost, latency, and additional reliability exposure of crossing NAT Gateway to reach DynamoDB
  • Use Provisioned Capacity with auto-scaling to avoid throttling and to achieve the lowest cost - On Demand seems nice but it is costly

Example of Pricing

Disclaimer: perform your own pricing calculation, monitor your costs during and after initial launch, and setup cost alerts to avoid unexpected charges.

Saved AWS Pricing Calculation

Assumptions

  • Using Provisioned Capacity with auto-scaling
  • Using Eventually Consistent Reads
  • 2 KB average session size
  • 100k RPM (requests per minute) average load
  • 1 million new sessions per month (~0.4 new sessions / second)
  • 8 million existing sessions
  • 2 million session updates / expirations per month (~0.8 updates / second)

Pricing Calculation

  • Storage
    • 2 KB * 8 million = 16 GB of storage
    • 16 GB * $0.25 / GB / month = $4 / month for storage
  • Reads
    • 100k RPM / 60 seconds = ~1,700 RPS (requests per second)
    • 1 RCU (read capacity unit) per item * 0.5 (eventually consistent reads) = 0.5 RCU per read
    • 1,700 RPS * 0.5 RCU per read = 850 RCUs
    • 850 RCUs / read * 720 hours / month * $0.00013 / RCU / hour = ~$80 / month for reads
  • Writes
    • 0.4 new sessions / second + 0.8 updates / second = 1.2 WPS (writes per second)
    • 1.2 WPS * 2 WCU (write capacity unit) per item = 2.4 WCUs
    • Allocate more WCUs to handle bursts
    • 100 WCUs * 720 hours / month * $0.00065 / WCU / hour = ~$50 / month for writes
  • Total
    • $4 / month for storage
    • $80 / month for reads
    • $50 / month for writes
    • $134 / month total

Running Examples

express

Source: ./examples/express.ts

  1. Create DynamoDB Table using AWS Console or any other method
    1. AWS CLI Example: aws dynamodb create-table --table-name dynamodb-session-store-test --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST
    2. Default name is dynamodb-session-store-test
    3. Default partition key is id
    4. No sort key
    5. On-demand throughput is sufficient for the example, although not suggested for high volume use
    6. Time to live can be turned on for a field named expires
      1. aws dynamodb update-time-to-live --table-name dynamodb-session-store-test --time-to-live-specification "Enabled=true, AttributeName=expires"
  2. npm run example:express
    1. If the table name was changed: TABLE_NAME=my-table-name npm run example:express
  3. Load http://localhost:3001/login in a browser
  4. Observe that a cookie is returned and does not change

cross-account

Source: ./examples/cross-account.ts

This example has the DynamoDB in one account and the express app using an IAM role from another account to access the DynamoDB Table using temporary credentials from an STS AssumeRole call (neatly encapsulated by the AWS SDK for JS v3).

This example is more involved than the others as it requires setting up an IAM role that can be assumed by the app account.

Instructions for Cross-Account DynamoDB Table Example

Session Store with DynamoDB Table in Another Account

express with dynamodb-connect module - for comparison

Source: ./examples/other.ts

  1. Create DynamoDB Table using AWS Console or any other method
    1. AWS CLI Example: aws dynamodb create-table --table-name connect-dynamodb-test --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST
    2. Default name is dynamodb-session-store-test
    3. Default partition key is id
    4. No sort key
    5. On-demand throughput is sufficient for the example, although not suggested for high volume use
    6. Time to live can be turned on for a field named expires
      1. aws dynamodb update-time-to-live --table-name connect-dynamodb-test --time-to-live-specification "Enabled=true, AttributeName=expires"
  2. npm run example:express
    1. If the table name was changed: TABLE_NAME=my-table-name npm run example:other
  3. Load http://localhost:3001/login in a browser
  4. Observe that a cookie is returned and does not change

Comparison with dynamodb-connect

Benefits of @pwrdrvr/dynamodb-session-store

  • Removes dangerous scan functionality that can make the DB completely unavailable if accidentally invoked
  • Uses eventually consistent reads on DynamoDB by default to reduce cost, increase throughput, and improve reliability (configurable)
  • Allows skipping touch calls that update the TTL on every read, which can be very expensive (configurable)
  • Serializes the sess field to a Map on DynamoDB for easier querying vs dynamodb-connect which serializes to a String
  • Migration from dynamodb-connect is easy, just set the prefix, hashKey, and table fields to the same values as dynamodb-connect - the get function will automatically deserialize the JSON sess field if found
  • Examples of how to use the module with express-session are provided

Example @pwrdrvr/dynamodb-session-store DB Record

{
  "id": "123",
  "sess": {
    "cookie": {
       "originalMaxAge": null,
       "expires": null,
       "httpOnly": true,
       "path": "/"
    },
    "name": "paul"
  },
  "expires": 1621968000
}
image

Example dynamodb-connect DB Record

{
  "id": "123",
  "sess": "{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"},\"name\":\"paul\"}",
  "expires": 1621968000
}
image

About

DynamoDB-based session store for express-session, using the AWS SDK for JS v3, offering configurability for cost, performance, and reliability, as well as examples, cost calcs, and tips not found in other DynamoDB session stores.

Topics

Resources

Stars

Watchers

Forks