Caching is one of the most effective ways to improve application performance while reducing costs. In this guide, I'll show you how to implement a cache-aside pattern using DynamoDB, ElastiCache Redis, AWS Lambda, and API Gateway - all provisioned with Terraform.
📘 Part 1: Infrastructure Setup
1. DynamoDB Table
First, let's create a Products table in DynamoDB:
resource "aws_dynamodb_table" "products" { name = "Products" billing_mode = "PAY_PER_REQUEST" hash_key = "productId" attribute { name = "productId" type = "S" } }
2. ElastiCache Redis Cluster
We'll deploy Redis inside a VPC for better security and performance:
resource "aws_elasticache_cluster" "products_cache" { cluster_id = "products-cache" engine = "redis" node_type = "cache.t3.micro" num_cache_nodes = 1 parameter_group_name = "default.redis6.x" engine_version = "6.x" port = 6379 security_group_ids = [aws_security_group.redis.id] subnet_group_name = aws_elasticache_subnet_group.redis.name } resource "aws_elasticache_subnet_group" "redis" { name = "redis-subnet-group" subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] }
3. Networking Configuration
Proper VPC setup is crucial:
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "us-east-1a" } resource "aws_security_group" "redis" { name = "redis-sg" description = "Allow access to Redis" vpc_id = aws_vpc.main.id ingress { from_port = 6379 to_port = 6379 protocol = "tcp" security_groups = [aws_security_group.lambda.id] } }
📘 Part 2: Lambda Function Logic
Here's our Python Lambda function implementing the cache-aside pattern:
import os import json import boto3 import redis from datetime import datetime # Initialize clients dynamodb = boto3.resource('dynamodb') products_table = dynamodb.Table('Products') # Redis connection redis_client = redis.Redis( host=os.environ['REDIS_HOST'], port=6379, decode_responses=True ) def lambda_handler(event, context): product_id = event['pathParameters']['productId'] cache_key = f"product:{product_id}" # Try to get from Redis first cached_product = redis_client.get(cache_key) if cached_product: print("Cache hit!") return { 'statusCode': 200, 'body': cached_product } print("Cache miss - fetching from DynamoDB") # Get from DynamoDB response = products_table.get_item(Key={'productId': product_id}) if 'Item' not in response: return {'statusCode': 404, 'body': 'Product not found'} product = response['Item'] product_json = json.dumps(product) # Cache with 5 minute TTL redis_client.setex(cache_key, 300, product_json) return { 'statusCode': 200, 'body': product_json }
📘 Part 3: API Gateway Configuration
Let's expose our Lambda through API Gateway:
resource "aws_api_gateway_rest_api" "products_api" { name = "products-api" } resource "aws_api_gateway_resource" "product" { rest_api_id = aws_api_gateway_rest_api.products_api.id parent_id = aws_api_gateway_rest_api.products_api.root_resource_id path_part = "product" } resource "aws_api_gateway_resource" "product_id" { rest_api_id = aws_api_gateway_rest_api.products_api.id parent_id = aws_api_gateway_resource.product.id path_part = "{productId}" } resource "aws_api_gateway_method" "get_product" { rest_api_id = aws_api_gateway_rest_api.products_api.id resource_id = aws_api_gateway_resource.product_id.id http_method = "GET" authorization = "NONE" } resource "aws_api_gateway_integration" "lambda" { rest_api_id = aws_api_gateway_rest_api.products_api.id resource_id = aws_api_gateway_resource.product_id.id http_method = aws_api_gateway_method.get_product.http_method integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.get_product.invoke_arn }
📘 Part 4: Monitoring and Optimization
1. Adding TTLs
We already implemented TTLs in our Lambda function (setex
with 300 seconds), but let's add CloudWatch metrics to track cache performance:
from aws_lambda_powertools import Metrics metrics = Metrics() def lambda_handler(event, context): # ... existing code ... if cached_product: metrics.add_metric(name="CacheHits", unit="Count", value=1) # ... return cached product ... else: metrics.add_metric(name="CacheMisses", unit="Count", value=1) # ... fetch from DynamoDB ...
2. Terraform for Monitoring
Add CloudWatch alarms and dashboards:
resource "aws_cloudwatch_dashboard" "cache" { dashboard_name = "cache-performance" dashboard_body = jsonencode({ widgets = [ { type = "metric" x = 0 y = 0 width = 12 height = 6 properties = { metrics = [ ["AWS/Lambda", "CacheHits", "FunctionName", aws_lambda_function.get_product.function_name], ["AWS/Lambda", "CacheMisses", "FunctionName", aws_lambda_function.get_product.function_name] ] period = 300 stat = "Sum" region = "us-east-1" title = "Cache Performance" } } ] }) }
3. Complete Terraform Workflow
For a production setup, add a CI/CD pipeline:
resource "aws_codepipeline" "deploy_pipeline" { name = "products-api-deployment" role_arn = aws_iam_role.codepipeline.arn artifact_store { location = aws_s3_bucket.artifacts.bucket type = "S3" } stage { name = "Source" action { name = "Source" category = "Source" owner = "ThirdParty" provider = "GitHub" version = "1" output_artifacts = ["source_output"] configuration = { Owner = "your-github-org" Repo = "products-api" Branch = "main" OAuthToken = var.github_token } } } stage { name = "Terraform" action { name = "Apply" category = "Build" owner = "AWS" provider = "CodeBuild" input_artifacts = ["source_output"] version = "1" configuration = { ProjectName = aws_codebuild_project.terraform.name } } } }
Results and Observations
After implementing this architecture, you should see:
- Average latency reduction from ~100ms (DynamoDB) to ~5ms (Redis) for cache hits
- Reduced DynamoDB RCU consumption (and costs) by 80-90% for frequently accessed items
- More consistent performance under load
Final Thoughts
This cache-aside pattern is just one of many ways to optimize DynamoDB performance. For production workloads, consider:
- Adding write-through caching for data modifications
- Implementing cache invalidation strategies
- Monitoring Redis memory usage and eviction policies
- Considering DAX for alternative DynamoDB caching
The complete Terraform code is available in this GitHub repo.
Have you implemented similar caching patterns? Share your experiences in the comments!
Top comments (0)