AWS Lambda@Edge
📖 概述
AWS Lambda@Edge 是 Amazon CloudFront 的扩展功能,允许您在全球边缘位置运行代码,以响应 CloudFront 事件。它能在更接近用户的位置执行计算,从而减少延迟并改善用户体验。
🎯 学习目标
- 掌握 Lambda@Edge 的核心概念和触发器
- 学习边缘函数的开发和部署
- 了解性能优化和限制约束
- 掌握实际应用场景和最佳实践
🏗️ Lambda@Edge 架构
1. 触发器类型
javascript
// Viewer Request - 查看器请求
exports.viewerRequest = (event, context, callback) => {
const request = event.Records[0].cf.request
const headers = request.headers
console.log('Viewer Request 触发:', {
uri: request.uri,
method: request.method,
clientIP: headers['cloudfront-viewer-address']?.[0]?.value
})
// A/B 测试路由
if (request.uri.startsWith('/app')) {
const testGroup = determineTestGroup(headers)
if (testGroup === 'beta') {
request.uri = '/beta' + request.uri
}
// 添加测试组头部
request.headers['x-test-group'] = [{ key: 'X-Test-Group', value: testGroup }]
}
// 移动设备重定向
const userAgent = headers['user-agent']?.[0]?.value || ''
if (isMobileDevice(userAgent) && !request.uri.startsWith('/m/')) {
const response = {
status: '302',
statusDescription: 'Found',
headers: {
location: [{
key: 'Location',
value: `https://m.example.com${request.uri}`
}]
}
}
callback(null, response)
return
}
// 认证检查
const authResult = checkAuthentication(headers)
if (!authResult.authenticated && requiresAuth(request.uri)) {
const response = {
status: '401',
statusDescription: 'Unauthorized',
body: JSON.stringify({
error: 'Authentication required',
loginUrl: 'https://auth.example.com/login'
}),
headers: {
'content-type': [{ key: 'Content-Type', value: 'application/json' }],
'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Bearer' }]
}
}
callback(null, response)
return
}
callback(null, request)
}
// Origin Request - 源请求
exports.originRequest = (event, context, callback) => {
const request = event.Records[0].cf.request
console.log('Origin Request 触发:', {
uri: request.uri,
origin: request.origin
})
// 动态源选择
if (request.uri.startsWith('/api/')) {
// API 请求路由到 API Gateway
request.origin = {
custom: {
domainName: 'api.example.com',
port: 443,
protocol: 'https',
path: '/prod'
}
}
// 移除路径前缀
request.uri = request.uri.replace('/api', '')
} else if (request.uri.startsWith('/static/')) {
// 静态资源路由到 S3
request.origin = {
s3: {
domainName: 'static-assets.s3.amazonaws.com',
region: 'us-east-1',
authMethod: 'origin-access-identity',
oai: 'origin-access-identity/cloudfront/ABCDEFG1234567'
}
}
}
// 添加自定义头部
request.headers['x-forwarded-host'] = [{
key: 'X-Forwarded-Host',
value: request.headers.host[0].value
}]
request.headers['x-edge-location'] = [{
key: 'X-Edge-Location',
value: process.env.AWS_REGION || 'unknown'
}]
callback(null, request)
}
// Origin Response - 源响应
exports.originResponse = (event, context, callback) => {
const request = event.Records[0].cf.request
const response = event.Records[0].cf.response
console.log('Origin Response 触发:', {
status: response.status,
uri: request.uri
})
// 错误页面处理
if (response.status === '404') {
if (request.uri.startsWith('/app/')) {
// SPA 路由处理 - 返回 index.html
response.status = '200'
response.statusDescription = 'OK'
response.body = generateSPAIndex(request.uri)
response.headers['content-type'] = [{
key: 'Content-Type',
value: 'text/html'
}]
} else {
// 自定义 404 页面
response.body = generate404Page()
response.headers['content-type'] = [{
key: 'Content-Type',
value: 'text/html'
}]
}
}
// 安全头部注入
response.headers['strict-transport-security'] = [{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload'
}]
response.headers['x-content-type-options'] = [{
key: 'X-Content-Type-Options',
value: 'nosniff'
}]
response.headers['x-frame-options'] = [{
key: 'X-Frame-Options',
value: 'DENY'
}]
// 缓存控制
if (request.uri.match(/\.(css|js|png|jpg|jpeg|gif|ico|svg)$/)) {
response.headers['cache-control'] = [{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}]
}
callback(null, response)
}
// Viewer Response - 查看器响应
exports.viewerResponse = (event, context, callback) => {
const request = event.Records[0].cf.request
const response = event.Records[0].cf.response
console.log('Viewer Response 触发:', {
status: response.status,
uri: request.uri
})
// 响应修改
if (response.headers['content-type']?.[0]?.value?.includes('text/html')) {
// HTML 内容注入
if (response.body) {
response.body = injectAnalytics(response.body, request.headers)
}
}
// 性能头部
response.headers['server-timing'] = [{
key: 'Server-Timing',
value: `edge;dur=${Date.now() - context.startTime || 0}`
}]
// CORS 头部
const origin = request.headers.origin?.[0]?.value
if (isAllowedOrigin(origin)) {
response.headers['access-control-allow-origin'] = [{
key: 'Access-Control-Allow-Origin',
value: origin
}]
}
callback(null, response)
}
// 工具函数
function determineTestGroup(headers) {
const userAgent = headers['user-agent']?.[0]?.value || ''
const cookieHeader = headers.cookie?.[0]?.value || ''
// 检查现有测试组 Cookie
const testGroupMatch = cookieHeader.match(/testGroup=([^;]+)/)
if (testGroupMatch) {
return testGroupMatch[1]
}
// 基于用户代理哈希分组
const hash = simpleHash(userAgent)
return hash % 2 === 0 ? 'control' : 'beta'
}
function isMobileDevice(userAgent) {
const mobileRegex = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i
return mobileRegex.test(userAgent)
}
function checkAuthentication(headers) {
const authHeader = headers.authorization?.[0]?.value
const cookieHeader = headers.cookie?.[0]?.value || ''
// JWT Token 验证
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7)
return validateJWTToken(token)
}
// Session Cookie 验证
const sessionMatch = cookieHeader.match(/sessionId=([^;]+)/)
if (sessionMatch) {
return validateSession(sessionMatch[1])
}
return { authenticated: false }
}
function requiresAuth(uri) {
const protectedPaths = ['/dashboard', '/profile', '/admin']
return protectedPaths.some(path => uri.startsWith(path))
}
function simpleHash(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash // 转换为 32 位整数
}
return Math.abs(hash)
}
2. 边缘认证系统
javascript
// 边缘认证管理器
class EdgeAuthManager {
constructor() {
this.jwtSecret = process.env.JWT_SECRET
this.sessionStore = new EdgeSessionStore()
}
// JWT 验证
validateJWTToken(token) {
try {
// 简化的 JWT 验证(实际应用中需要完整实现)
const parts = token.split('.')
if (parts.length !== 3) {
throw new Error('Invalid JWT format')
}
const payload = JSON.parse(atob(parts[1]))
// 检查过期时间
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token expired')
}
// 验证签名(简化版本)
const expectedSignature = this.createSignature(parts[0] + '.' + parts[1])
if (parts[2] !== expectedSignature) {
throw new Error('Invalid signature')
}
return {
authenticated: true,
user: {
id: payload.sub,
email: payload.email,
roles: payload.roles || []
}
}
} catch (error) {
console.error('JWT 验证失败:', error)
return { authenticated: false, error: error.message }
}
}
// Session 验证
async validateSession(sessionId) {
try {
const session = await this.sessionStore.get(sessionId)
if (!session) {
return { authenticated: false, error: 'Session not found' }
}
if (session.expiresAt < Date.now()) {
await this.sessionStore.delete(sessionId)
return { authenticated: false, error: 'Session expired' }
}
return {
authenticated: true,
user: session.user,
session: {
id: sessionId,
createdAt: session.createdAt,
expiresAt: session.expiresAt
}
}
} catch (error) {
console.error('Session 验证失败:', error)
return { authenticated: false, error: error.message }
}
}
// 创建认证响应
createAuthResponse(redirectUrl = '/login') {
return {
status: '302',
statusDescription: 'Found',
headers: {
location: [{
key: 'Location',
value: redirectUrl
}],
'set-cookie': [{
key: 'Set-Cookie',
value: `redirectUrl=${encodeURIComponent(redirectUrl)}; Path=/; Secure; HttpOnly`
}]
}
}
}
// 权限检查
checkPermissions(user, requiredPermissions) {
if (!user || !user.roles) {
return false
}
const userPermissions = this.getRolePermissions(user.roles)
return requiredPermissions.every(permission =>
userPermissions.includes(permission)
)
}
getRolePermissions(roles) {
const rolePermissions = {
'admin': ['read', 'write', 'delete', 'admin'],
'editor': ['read', 'write'],
'viewer': ['read']
}
const permissions = new Set()
roles.forEach(role => {
const perms = rolePermissions[role] || []
perms.forEach(perm => permissions.add(perm))
})
return Array.from(permissions)
}
createSignature(data) {
// 简化的签名创建(实际应用中应使用 crypto 库)
return btoa(data + this.jwtSecret).substring(0, 20)
}
}
// 边缘会话存储
class EdgeSessionStore {
constructor() {
// 由于 Lambda@Edge 的限制,这里使用内存存储
// 实际应用中应该使用外部存储如 DynamoDB
this.sessions = new Map()
}
async get(sessionId) {
return this.sessions.get(sessionId) || null
}
async set(sessionId, sessionData, ttl = 3600) {
const session = {
...sessionData,
createdAt: Date.now(),
expiresAt: Date.now() + (ttl * 1000)
}
this.sessions.set(sessionId, session)
// 设置清理定时器
setTimeout(() => {
this.sessions.delete(sessionId)
}, ttl * 1000)
return session
}
async delete(sessionId) {
return this.sessions.delete(sessionId)
}
async cleanup() {
const now = Date.now()
for (const [sessionId, session] of this.sessions.entries()) {
if (session.expiresAt < now) {
this.sessions.delete(sessionId)
}
}
}
}
3. 动态内容生成
javascript
// 动态内容生成器
class EdgeContentGenerator {
constructor() {
this.templates = new Map()
this.cache = new Map()
}
// 生成个性化页面
generatePersonalizedPage(request, userContext) {
const uri = request.uri
const userAgent = request.headers['user-agent']?.[0]?.value || ''
const acceptLanguage = request.headers['accept-language']?.[0]?.value || 'en-US'
// 检测设备类型
const deviceType = this.detectDeviceType(userAgent)
// 检测语言偏好
const preferredLanguage = this.detectLanguage(acceptLanguage)
// 生成页面内容
const pageContent = this.buildPageContent({
uri,
deviceType,
language: preferredLanguage,
user: userContext?.user,
timestamp: new Date().toISOString()
})
return {
status: '200',
statusDescription: 'OK',
body: pageContent,
headers: {
'content-type': [{
key: 'Content-Type',
value: 'text/html; charset=utf-8'
}],
'cache-control': [{
key: 'Cache-Control',
value: 'private, no-cache, no-store, must-revalidate'
}],
'vary': [{
key: 'Vary',
value: 'User-Agent, Accept-Language, Authorization'
}]
}
}
}
// 构建页面内容
buildPageContent(context) {
const { uri, deviceType, language, user } = context
let template = this.getTemplate(uri, deviceType)
// 个性化替换
template = template.replace(/\{\{user\.name\}\}/g, user?.name || 'Guest')
template = template.replace(/\{\{user\.email\}\}/g, user?.email || '')
template = template.replace(/\{\{device\.type\}\}/g, deviceType)
template = template.replace(/\{\{page\.language\}\}/g, language)
// 注入动态内容
template = this.injectDynamicContent(template, context)
// 注入分析代码
template = this.injectAnalytics(template, context)
return template
}
getTemplate(uri, deviceType) {
const templateKey = `${uri}-${deviceType}`
if (this.templates.has(templateKey)) {
return this.templates.get(templateKey)
}
// 基础模板
let template = `
<!DOCTYPE html>
<html lang="{{page.language}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{page.title}}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
${deviceType === 'mobile' ? 'font-size: 16px;' : 'font-size: 14px;'}
}
.container {
max-width: ${deviceType === 'mobile' ? '100%' : '1200px'};
margin: 0 auto;
}
.user-info {
background: #f0f0f0;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="user-info">
<h2>Welcome, {{user.name}}!</h2>
<p>Device: {{device.type}}</p>
<p>Language: {{page.language}}</p>
</div>
<main>
{{dynamic.content}}
</main>
</div>
{{analytics.code}}
</body>
</html>`
this.templates.set(templateKey, template)
return template
}
injectDynamicContent(template, context) {
// 根据 URI 生成不同的动态内容
let dynamicContent = '<h1>Welcome to Our Site</h1>'
if (context.uri.startsWith('/dashboard')) {
dynamicContent = this.generateDashboardContent(context.user)
} else if (context.uri.startsWith('/products')) {
dynamicContent = this.generateProductContent(context)
} else if (context.uri.startsWith('/news')) {
dynamicContent = this.generateNewsContent(context)
}
return template.replace('{{dynamic.content}}', dynamicContent)
}
generateDashboardContent(user) {
if (!user) {
return '<p>Please log in to view your dashboard.</p>'
}
return `
<h1>Dashboard</h1>
<div class="dashboard-stats">
<div class="stat-card">
<h3>Profile</h3>
<p>Email: ${user.email}</p>
<p>Roles: ${user.roles?.join(', ') || 'None'}</p>
</div>
<div class="stat-card">
<h3>Activity</h3>
<p>Last login: ${new Date().toLocaleDateString()}</p>
</div>
</div>
`
}
generateProductContent(context) {
const products = [
{ id: 1, name: 'Product A', price: '$99' },
{ id: 2, name: 'Product B', price: '$149' },
{ id: 3, name: 'Product C', price: '$199' }
]
const productCards = products.map(product => `
<div class="product-card">
<h3>${product.name}</h3>
<p>Price: ${product.price}</p>
<button onclick="addToCart(${product.id})">Add to Cart</button>
</div>
`).join('')
return `
<h1>Our Products</h1>
<div class="products-grid">
${productCards}
</div>
<script>
function addToCart(productId) {
console.log('Added product', productId, 'to cart');
// 这里可以调用 API 或触发其他事件
}
</script>
`
}
injectAnalytics(template, context) {
const analyticsCode = `
<script>
// 简化的分析代码
(function() {
const analytics = {
track: function(event, properties) {
console.log('Analytics:', event, properties);
// 发送到分析服务
}
};
// 页面加载事件
analytics.track('page_view', {
url: '${context.uri}',
device: '${context.deviceType}',
language: '${context.language}',
user_id: '${context.user?.id || 'anonymous'}',
timestamp: '${context.timestamp}'
});
window.analytics = analytics;
})();
</script>`
return template.replace('{{analytics.code}}', analyticsCode)
}
detectDeviceType(userAgent) {
if (/Mobile|Android|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(userAgent)) {
return 'mobile'
} else if (/Tablet|iPad/i.test(userAgent)) {
return 'tablet'
} else {
return 'desktop'
}
}
detectLanguage(acceptLanguage) {
const languages = acceptLanguage.split(',').map(lang => {
const parts = lang.trim().split(';')
const code = parts[0]
const quality = parts[1] ? parseFloat(parts[1].split('=')[1]) : 1.0
return { code, quality }
})
// 按质量排序
languages.sort((a, b) => b.quality - a.quality)
// 支持的语言
const supportedLanguages = ['en-US', 'zh-CN', 'es-ES', 'fr-FR', 'de-DE']
for (const lang of languages) {
if (supportedLanguages.includes(lang.code)) {
return lang.code
}
// 检查语言代码前缀
const prefix = lang.code.split('-')[0]
const match = supportedLanguages.find(supported =>
supported.startsWith(prefix)
)
if (match) {
return match
}
}
return 'en-US' // 默认语言
}
}
4. 部署配置
javascript
// serverless.yml 配置
const serverlessConfig = `
service: lambda-edge-functions
provider:
name: aws
runtime: nodejs18.x
region: us-east-1 # Lambda@Edge 必须部署在 us-east-1
stage: \${opt:stage, 'dev'}
functions:
viewerRequest:
handler: src/viewer-request.handler
timeout: 5 # Lambda@Edge 最大 5 秒
memorySize: 128 # 最大 512MB
originRequest:
handler: src/origin-request.handler
timeout: 30 # Origin 请求最大 30 秒
memorySize: 512
originResponse:
handler: src/origin-response.handler
timeout: 30
memorySize: 256
viewerResponse:
handler: src/viewer-response.handler
timeout: 5
memorySize: 128
resources:
Resources:
# CloudFront Distribution
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: Lambda@Edge Example Distribution
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt ViewerRequestLambdaFunction.Arn
- EventType: origin-request
LambdaFunctionARN: !GetAtt OriginRequestLambdaFunction.Arn
- EventType: origin-response
LambdaFunctionARN: !GetAtt OriginResponseLambdaFunction.Arn
- EventType: viewer-response
LambdaFunctionARN: !GetAtt ViewerResponseLambdaFunction.Arn
CacheBehaviors:
- PathPattern: "/api/*"
TargetOriginId: APIOrigin
ViewerProtocolPolicy: https-only
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
LambdaFunctionAssociations:
- EventType: viewer-request
LambdaFunctionARN: !GetAtt ViewerRequestLambdaFunction.Arn
- EventType: origin-request
LambdaFunctionARN: !GetAtt OriginRequestLambdaFunction.Arn
Origins:
- Id: S3Origin
DomainName: my-static-website.s3.amazonaws.com
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/\${CloudFrontOriginAccessIdentity}"
- Id: APIOrigin
DomainName: api.example.com
CustomOriginConfig:
HTTPPort: 443
OriginProtocolPolicy: https-only
DefaultRootObject: index.html
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 404
ResponsePagePath: /404.html
ErrorCachingMinTTL: 300
- ErrorCode: 500
ResponseCode: 500
ResponsePagePath: /500.html
ErrorCachingMinTTL: 0
# Origin Access Identity
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub "\${AWS::StackName} OAI"
plugins:
- serverless-plugin-lambda-edge
`
// 部署脚本
const deploymentScript = `
#!/bin/bash
echo "开始部署 Lambda@Edge 函数..."
# 1. 安装依赖
npm install
# 2. 运行测试
npm test
# 3. 构建函数
npm run build
# 4. 部署到 us-east-1
export AWS_REGION=us-east-1
serverless deploy --stage production
# 5. 获取函数 ARN
VIEWER_REQUEST_ARN=$(aws lambda get-function --function-name lambda-edge-functions-production-viewerRequest --query 'Configuration.FunctionArn' --output text)
ORIGIN_REQUEST_ARN=$(aws lambda get-function --function-name lambda-edge-functions-production-originRequest --query 'Configuration.FunctionArn' --output text)
echo "部署完成!"
echo "Viewer Request ARN: $VIEWER_REQUEST_ARN"
echo "Origin Request ARN: $ORIGIN_REQUEST_ARN"
# 6. 更新 CloudFront 分配
echo "更新 CloudFront 分配..."
aws cloudformation update-stack --stack-name lambda-edge-stack --template-body file://cloudformation.yml --parameters ParameterKey=ViewerRequestArn,ParameterValue=$VIEWER_REQUEST_ARN
echo "部署流程完成!"
`
📚 最佳实践总结
- 性能优化:保持函数轻量,避免复杂计算
- 错误处理:实现完善的错误处理和回退机制
- 缓存策略:合理使用缓存减少函数执行次数
- 安全考虑:验证输入,保护敏感数据
- 监控日志:使用 CloudWatch 监控函数性能
- 版本管理:使用版本控制管理函数更新
- 测试验证:充分测试边缘函数行为
- 成本控制:监控执行次数和成本
通过掌握这些 AWS Lambda@Edge 技术,您将能够构建高性能的全球分布式边缘应用。