AWS core services guide covering Lambda, S3, RDS, IAM, SQS/SNS, DynamoDB, CloudFront with security and cost optimization patterns
Production-ready patterns for AWS core services: Lambda handlers, S3 operations, RDS connection management, IAM policies, async messaging, DynamoDB design, CloudFront caching, and cost optimization.
BAD: Unoptimized cold starts, no reuse
// TypeScript - AWS SDK v3
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
export const handler = async (event: any) => {
const client = new DynamoDBClient({ region: 'us-east-1' }); // ❌ Created every invocation
const apiKey = 'hardcoded-key-abc123'; // ❌ Hardcoded credentials
await client.send(new PutItemCommand({
TableName: 'users',
Item: { id: { S: event.id }, data: { S: JSON.stringify(event) } }
}));
};
GOOD: Warm reuse, environment config, structured logging
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
// ✅ Instantiate outside handler for reuse across warm invocations
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const secretsClient = new SecretsManagerClient({ region: process.env.AWS_REGION });
let cachedSecret: string | null = null;
async function getSecret(): Promise<string> {
if (cachedSecret) return cachedSecret;
const response = await secretsClient.send(
new GetSecretValueCommand({ SecretId: process.env.SECRET_ARN })
);
cachedSecret = response.SecretString!;
return cachedSecret;
}
export const handler = async (event: { id: string; data: Record<string, any> }) => {
const startTime = Date.now();
try {
const secret = await getSecret();
await dynamoClient.send(new PutItemCommand({
TableName: process.env.TABLE_NAME!,
Item: {
id: { S: event.id },
data: { S: JSON.stringify(event.data) },
timestamp: { N: Date.now().toString() }
}
}));
console.log(JSON.stringify({
level: 'info',
message: 'Item stored',
id: event.id,
duration: Date.now() - startTime
}));
return { statusCode: 200, body: JSON.stringify({ success: true }) };
} catch (error) {
console.error(JSON.stringify({
level: 'error',
message: 'Failed to store item',
error: error instanceof Error ? error.message : String(error),
id: event.id
}));
throw error;
}
};
Go handler with proper error handling
package main
import (
"context"
"encoding/json"
"os"
"time"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var dynamoClient *dynamodb.Client
func init() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
log.Fatal().Err(err).Msg("Failed to load AWS config")
}
dynamoClient = dynamodb.NewFromConfig(cfg)
}
type Event struct {
ID string `json:"id"`
Data map[string]interface{} `json:"data"`
}
func handleRequest(ctx context.Context, event Event) (map[string]interface{}, error) {
start := time.Now()
dataJSON, err := json.Marshal(event.Data)
if err != nil {
return nil, err
}
_, err = dynamoClient.PutItem(ctx, &dynamodb.PutItemInput{
TableName: &os.Getenv("TABLE_NAME"),
Item: map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{Value: event.ID},
"data": &types.AttributeValueMemberS{Value: string(dataJSON)},
"timestamp": &types.AttributeValueMemberN{Value: string(time.Now().Unix())},
},
})
if err != nil {
log.Error().Err(err).Str("id", event.ID).Msg("Failed to store item")
return nil, err
}
log.Info().Str("id", event.ID).Dur("duration", time.Since(start)).Msg("Item stored")
return map[string]interface{}{"success": true}, nil
}
func main() {
lambda.Start(handleRequest)
}
BAD: Public bucket, direct uploads
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
// ❌ Public ACL, no encryption
await s3.send(new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'uploads/file.pdf',
Body: fileBuffer,
ACL: 'public-read' // ❌ Never use public ACLs
}));
GOOD: Presigned URLs, encryption, lifecycle policies
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: process.env.AWS_REGION });
// ✅ Generate presigned URL for client-side uploads
export async function generateUploadUrl(key: string, contentType: string): Promise<string> {
const command = new PutObjectCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: `uploads/${Date.now()}-${key}`,
ContentType: contentType,
ServerSideEncryption: 'AES256',
Metadata: {
uploadedBy: 'user-service',
uploadDate: new Date().toISOString()
}
});
// URL expires in 15 minutes
return getSignedUrl(s3, command, { expiresIn: 900 });
}
// ✅ Lifecycle policy (apply via IaC or AWS CLI)
const lifecyclePolicy = {
Rules: [
{
Id: 'TransitionOldUploads',
Status: 'Enabled',
Transitions: [
{ Days: 30, StorageClass: 'INTELLIGENT_TIERING' },
{ Days: 90, StorageClass: 'GLACIER_IR' }
],
NoncurrentVersionExpiration: { NoncurrentDays: 30 },
AbortIncompleteMultipartUpload: { DaysAfterInitiation: 7 }
}
]
};
BAD: Direct Lambda connections to RDS
import { Client } from 'pg';
// ❌ Creates new connection every invocation, exhausts RDS connections
export const handler = async () => {
const client = new Client({
host: 'mydb.abc123.us-east-1.rds.amazonaws.com',
user: 'admin',
password: 'hardcoded-password', // ❌ Never hardcode
database: 'prod'
});
await client.connect();
const result = await client.query('SELECT * FROM users');
await client.end();
return result.rows;
};
GOOD: RDS Proxy with IAM authentication
import { Pool } from 'pg';
import { Signer } from '@aws-sdk/rds-signer';
const signer = new Signer({
region: process.env.AWS_REGION!,
hostname: process.env.RDS_PROXY_ENDPOINT!,
port: 5432,
username: process.env.DB_USER!
});
// ✅ Connection pool reused across warm invocations
const pool = new Pool({
host: process.env.RDS_PROXY_ENDPOINT,
port: 5432,
user: process.env.DB_USER,
database: process.env.DB_NAME,
password: async () => {
return await signer.getAuthToken(); // ✅ IAM-based auth token
},
max: 2, // ✅ Low pool size for Lambda (RDS Proxy handles pooling)
ssl: { rejectUnauthorized: true }
});
export const handler = async (event: { userId: string }) => {
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM users WHERE id = $1', [event.userId]);
return result.rows[0];
} finally {
client.release(); // ✅ Return to pool, don't close
}
};
BAD: Wildcard permissions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
]
}
GOOD: Scoped permissions with conditions
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowUploadToBucket",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectAcl"],
"Resource": "arn:aws:s3:::my-upload-bucket/uploads/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
},
{
"Sid": "AllowListBucket",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-upload-bucket",
"Condition": {
"StringLike": {
"s3:prefix": ["uploads/*"]
}
}
}
]
}
Use roles for EC2/Lambda, not IAM users
// ✅ Lambda function role (attach via SAM/CDK/Terraform)
const lambdaExecutionRole = {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Principal: { Service: 'lambda.amazonaws.com' },
Action: 'sts:AssumeRole'
}]
},
ManagedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'],
Policies: [{
PolicyName: 'DynamoDBAccess',
PolicyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: ['dynamodb:PutItem', 'dynamodb:GetItem'],
Resource: 'arn:aws:dynamodb:us-east-1:123456789012:table/users'
}]
}
}]
};
BAD: Synchronous processing, no error handling
// ❌ Blocks Lambda execution waiting for email send
export const handler = async (event: { email: string; content: string }) => {
await sendEmail(event.email, event.content); // Slow, blocks return
return { statusCode: 200 };
};
GOOD: Decouple with SQS/SNS
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const sqsClient = new SQSClient({ region: process.env.AWS_REGION });
const snsClient = new SNSClient({ region: process.env.AWS_REGION });
// ✅ API handler: queue task and return immediately
export const apiHandler = async (event: { email: string; content: string }) => {
await sqsClient.send(new SendMessageCommand({
QueueUrl: process.env.EMAIL_QUEUE_URL,
MessageBody: JSON.stringify(event),
MessageAttributes: {
priority: { DataType: 'String', StringValue: 'high' }
}
}));
return { statusCode: 202, body: JSON.stringify({ message: 'Queued' }) };
};
// ✅ Worker handler: process queue messages
export const workerHandler = async (event: { Records: any[] }) => {
for (const record of event.Records) {
const { email, content } = JSON.parse(record.body);
try {
await sendEmail(email, content);
// ✅ Publish success event to SNS topic for analytics
await snsClient.send(new PublishCommand({
TopicArn: process.env.EVENTS_TOPIC_ARN,
Message: JSON.stringify({ event: 'email_sent', email }),
MessageAttributes: {
eventType: { DataType: 'String', StringValue: 'email_sent' }
}
}));
} catch (error) {
console.error('Email send failed', { email, error });
throw error; // ✅ SQS will retry based on redrive policy
}
}
};
BAD: Multiple tables, no GSIs
// ❌ Separate tables require multiple queries
await dynamoClient.send(new GetItemCommand({
TableName: 'Users',
Key: { userId: { S: '123' } }
}));
await dynamoClient.send(new QueryCommand({
TableName: 'Orders',
KeyConditionExpression: 'userId = :userId',
ExpressionAttributeValues: { ':userId': { S: '123' } }
}));
GOOD: Single table with composite keys
import { DynamoDBClient, QueryCommand, PutItemCommand } from '@aws-sdk/client-dynamodb';
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
// ✅ Single table structure: PK = USER#123, SK = PROFILE / ORDER#2024-01-15
async function putUser(userId: string, name: string, email: string) {
await dynamoClient.send(new PutItemCommand({
TableName: process.env.TABLE_NAME!,
Item: {
PK: { S: `USER#${userId}` },
SK: { S: 'PROFILE' },
GSI1PK: { S: `EMAIL#${email}` }, // ✅ GSI for email lookups
GSI1SK: { S: `USER#${userId}` },
name: { S: name },
email: { S: email },
type: { S: 'user' }
}
}));
}
async function putOrder(userId: string, orderId: string, amount: number) {
await dynamoClient.send(new PutItemCommand({
TableName: process.env.TABLE_NAME!,
Item: {
PK: { S: `USER#${userId}` },
SK: { S: `ORDER#${orderId}` },
amount: { N: amount.toString() },
type: { S: 'order' }
}
}));
}
// ✅ Single query gets user and all orders
async function getUserWithOrders(userId: string) {
const result = await dynamoClient.send(new QueryCommand({
TableName: process.env.TABLE_NAME!,
KeyConditionExpression: 'PK = :pk',
ExpressionAttributeValues: { ':pk': { S: `USER#${userId}` } }
}));
return result.Items;
}
BAD: No cache control, frequent invalidations
// ❌ Every request hits origin
const response = {
statusCode: 200,
headers: {
'Cache-Control': 'no-cache' // ❌ Defeats CloudFront purpose
},
body: JSON.stringify(data)
};
GOOD: Strategic caching with versioned assets
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
const cfClient = new CloudFrontClient({ region: 'us-east-1' });
// ✅ Lambda@Edge origin response for dynamic cache headers
export const handler = async (event: any) => {
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
// Static assets: cache for 1 year (use versioned filenames)
if (request.uri.match(/\.(js|css|png|jpg|woff2)$/)) {
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}];
}
// API responses: cache for 5 minutes
else if (request.uri.startsWith('/api/')) {
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=300, s-maxage=300'
}];
}
// HTML: cache for 1 hour, revalidate
else {
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=3600, must-revalidate'
}];
}
return response;
};
// ✅ Targeted invalidation (use sparingly, costs $0.005 per path)
async function invalidateCache(paths: string[]) {
await cfClient.send(new CreateInvalidationCommand({
DistributionId: process.env.CLOUDFRONT_DISTRIBUTION_ID,
InvalidationBatch: {
CallerReference: Date.now().toString(),
Paths: {
Quantity: paths.length,
Items: paths // e.g., ['/index.html', '/api/*']
}
}
}));
}
Key strategies
// 1. Use Lambda reserved concurrency to prevent runaway costs
const lambdaConfig = {
ReservedConcurrentExecutions: 10, // ✅ Cap max concurrent invocations
Timeout: 30, // ✅ Don't use default 3s or max 900s, tune to actual need
MemorySize: 512 // ✅ Test optimal memory (higher = faster = cheaper sometimes)
};
// 2. S3 Intelligent-Tiering for unpredictable access patterns
const s3LifecycleRule = {
Transitions: [
{ Days: 0, StorageClass: 'INTELLIGENT_TIERING' } // ✅ Auto-optimize
]
};
// 3. DynamoDB on-demand for variable traffic, provisioned for steady
const dynamoConfig = {
BillingMode: 'PAY_PER_REQUEST' // ✅ On-demand for dev/staging
// BillingMode: 'PROVISIONED', // ✅ Provisioned + auto-scaling for prod
};
// 4. Use AWS Cost Anomaly Detection
// Set up via AWS Console > Cost Management > Cost Anomaly Detection
// Alerts when spend deviates from historical patterns
// 5. Tag everything for cost allocation
const resourceTags = {
Environment: 'production',
Project: 'user-service',
CostCenter: 'engineering',
Owner: 'platform-team'
};
BAD: Hardcoded or environment variables for secrets
const apiKey = process.env.API_KEY; // ❌ Visible in Lambda console
GOOD: SSM Parameter Store with encryption
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
const ssmClient = new SSMClient({ region: process.env.AWS_REGION });
const cache = new Map<string, { value: string; expires: number }>();
async function getParameter(name: string, ttl = 300000): Promise<string> {
const cached = cache.get(name);
if (cached && cached.expires > Date.now()) {
return cached.value;
}
const response = await ssmClient.send(new GetParameterCommand({
Name: name,
WithDecryption: true // ✅ Decrypt SecureString parameters
}));
const value = response.Parameter!.Value!;
cache.set(name, { value, expires: Date.now() + ttl });
return value;
}
// ✅ Usage in handler
export const handler = async () => {
const dbPassword = await getParameter('/prod/db/password');
const apiKey = await getParameter('/prod/external-api/key');
// Use credentials...
};
Lambda cold start mitigation
S3 performance
RDS best practices
DynamoDB performance
Monitoring