当我们把图片放在云端时,随着时间推移图片会越来越多,这是我们希望生产缩略图方便快速浏览,AWS为这个提供了更好的解决方案,当云端资源中发生某种特定的事件时,将触发Lambda函数。比如,将上传的图片通过异步调整图片大小。
- 配置API网关上传图片到S3
- Lambda函数响应S3事件
- 配置CloudFront让图片访问更快更方便
源代码
代码下载地址:https://pan.baidu.com/s/1RYcGMEh0W_-n_MkQlEYCAw
提取码:umci
工程说明
新建一个Bucket,并通过CloudFormation配置API网关上传图片,Bucket中配置新增对象事件(s3:ObjectCreated:*)用来通知Lambda函数,Lambda函数中收到通知获取到图片进行Copy操作。
lambda-imageresizer: 接受通知,处理图片的函数。


1. 配置API网关上传图片到S3
图片上传的方法是/users/{userid}/picture。要实现该功能我们需要四个小步骤:
1)创建一个Bucket用来存储图片
2)为API网关创建一条Role规则
3)配置REST资源即/users/{userid}/picture路径
4)创建API网关的PUT方法
1)创建一个Bucket用来存储图片。配置中的${DomainName}是在cloudformation.template文件头部定义的变量(详见源代码),该变量的值来自build.gradle,因为S3存储桶的名称必须要全局唯一。注意,后续这个配置会增加Bucket的事件通知,用来触发Lambda函数。
"ProfilePicturesBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": "${DomainName}-profilepictures"
}
}
2)为API网关创建一条Role规则。这个规则只对图片存储桶(ProfilePicturesBucket)授予s3:PutObject和s3:PutObjectAcl权限,这个权限会被添加到AWS::ApiGateway::Method方法中(UsersIdPicturePutMethod)。
//创建一个Role,API网关可以认定这条规则只对图片存储Bucket
//授权S3:PutObject和S3:PutObjectAcl权限
"ApiGatewayProxyRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"apigateway.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
},
"Path": "/",
"Policies": [
{
"PolicyName": "S3BucketPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
//具体可以执行的动作
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
//资源指定ProfilePicturesBucket
"Resource": [
{
"Fn::Sub": "arn:aws:s3:::${ProfilePicturesBucket}"
},
{
"Fn::Sub": "arn:aws:s3:::${ProfilePicturesBucket}/*"
}
]
}
]
}
}
]
}
},
3)配置REST资源即/users/{userid}/picture路径,看这个路径这边配置了三个资源/users,/{userid},/picture,其中/users资源已经在上一章配置过,下面是 /{userid},/picture两个资源的配置,需要注意配置中ParentId对应的资源Ref Id。
/{userid}资源
"UsersIdResource": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"PathPart": "{id}",
"RestApiId": {
"Ref": "RestApi"
},
"ParentId": {
"Ref": "UsersResource"
}
}
},
/picture资源
"UsersIdPictureResource": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"PathPart": "picture",
"RestApiId": {
"Ref": "RestApi"
},
"ParentId": {
"Ref": "UsersIdResource"
}
}
},
4)创建API网关的PUT方法。这里把API网关的Http请求属性映射S3 API调用,把请求包传到S3 API。具体看下面的详细备注
"UsersIdPicturePutMethod": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
//PUT方法
"HttpMethod": "PUT",
"RestApiId": {
"Ref": "RestApi"
},
//上传图片需要自定义授权
"AuthorizationType": "CUSTOM",
"AuthorizerId": {
"Ref": "ApiGatewayAuthorizer"
},
//定义资源
"ResourceId": {
"Ref": "UsersIdPictureResource"
},
//请求路径参数{userid}, 头参数Content-Type,Content-Length
"RequestParameters": {
"method.request.path.id": "True",
"method.request.header.Content-Type": "True",
"method.request.header.Content-Length": "True"
},
"Integration": {
"Type": "AWS",
"Uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:s3:path/${ProfilePicturesBucket}/uploads/{filename}"
},
"IntegrationHttpMethod": "PUT",
//集成所需的凭证
"Credentials": {
"Fn::GetAtt": [
"ApiGatewayProxyRole",
"Arn"
]
},
"RequestParameters": {
//context.requestId 是APIGateway的请求ID
//path.filename即Uri中filename,生产路径名称
"integration.request.path.filename": "context.requestId",
"integration.request.header.Content-Type": "method.request.header.Content-Type",
"integration.request.header.Content-Length": "method.request.header.Content-Length",
//客户端在Post较大数据到服务端之前,允许双方“握手”,如果匹配上了Client才开始发送较大数据
//原因是:如果客户端直接发送请求数据,但是服务器又将该请求拒绝的话,这种行为将带来很大的资源开销。
"integration.request.header.Expect": "'100-continue'",
//添加bucket权限控制,有private,public-read-write,bucket-owner-read等
"integration.request.header.x-amz-acl": "'public-read'",
//S3中获取同时 getUserMetaDataOf("user-id") 获取{id}值
"integration.request.header.x-amz-meta-user-id": "method.request.path.id"
},
"RequestTemplates": {
},
//当集成没有映射到模板的内容类型时,允许传递
"PassthroughBehavior": "WHEN_NO_TEMPLATES",
//方法的后端处理完请求后提供的响应
"IntegrationResponses": [
{
//一个匹配正则
"SelectionPattern": "4\\d{2}",
"StatusCode": "400"
},
{
"SelectionPattern": "5\\d{2}",
"StatusCode": "500"
},
{
"SelectionPattern": ".*",
"StatusCode": "202",
"ResponseTemplates": {
"application/json": {
"Fn::Sub": "{\"status\": \"pending\"}"
}
}
}
]
},
//可发送到调用方法的客户端的响应
"MethodResponses": [
{
"StatusCode": "202"
},
{
"StatusCode": "400"
},
{
"StatusCode": "500"
}
]
}
}
小细节:在S3存储上传图片会把图片文件当作UTF-8的文本文件,我们需要在API网关手动配置指定Content-Type头视为二进制文件。登陆APIGateway控制台https://console.aws.amazon.com/apigateway/home?#如图设置下即可。

发布工程
./gradlew deploy
我们就可以通过命令上传自己的图片,这里是需要授权认证的,需要确认DynamoDB中UserTable 和 TokenTable 这两个表是否存在并且Token表中含有一条severless的授权记录,这方面有疑问可以看《AWS Lambda笔记-数据持久化DynamoDB-10》。
命令中serverless.xxxx.com替换成自己的域名
curl --request PUT 'https://serverless.xxxx.com/users/042b00ef-1c50-41e0-bc9e-ff092567427f/picture' \
--header 'Authorization:Bearer serverless' \
--header 'Content-Type:image/jpeg' \
--data-binary '@/Users/lazy/Desktop/temp/timg.jpeg'
上传成功后,我们可以看到我们上传的图片信息在serverless.kkksee.com-profilepicturesBucket下的 /uploads目录下。接下来,我们需要监听这个目录的事件,当这个目录由新增事件时,通知Lambda函数,将图片copy一份。

2.Lambda函数响应S3事件
当/uploads目录下面由新增事件时,我们触发Lambda函数。主要是AWS::S3::Bucket下的NotificationConfiguration配置,定义了Bucket的事件,监听的目录,响应的Lambda函数。
Bucket事件通知主要有:新增对象事件(s3:ObjectCreated:*)、对象删除事件(s3:ObjectRemoved:*)、还原对象事件(s3:ObjectRestore:*)、复制事件(s3:Replication:*)。
注意:防止事件触发死循环,例如我们在/uploads目录中监听s3:ObjectCreated:事件,同时我们把处理完的图片又保存到/uploads目录下,这样就又触发s3:ObjectCreated:事件,导致无限循环。
Lambda函数响应S3事件具体操作步骤:
1)创建Lambda事件响应函数
2)配置Bucket事件通知Lambda函数
1)创建Lambda事件响应函数
按照目录新建工程lambda-imageresizer,并把该工程加到根目录的settings.gradle文件中。还需要导入S3的标准化事件库aws-lambda-java-events
(build.gradle添加dependencies)

根目录settings.gradle新增:
include 'lambda-imageresizer'
当前工程的build.gradle配置新增
dependencies {
compile group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '1.3.0'
}
工程中Handle.java类(响应Bucket事件)
/*
函数使用S3预置标准事件 依赖aws-lambda-java-events 库
*/
public class Handler implements RequestHandler<S3Event, Void> {
private static final Logger LOGGER = Logger.getLogger(Handler.class);
final AmazonS3 s3client;
public Handler() {
//构造时创建S3Client
s3client = new AmazonS3Client(new DefaultAWSCredentialsProviderChain());
}
//copy图片到当前Bucekt的users/{userId}/picture/small.jpg
private void resizeImage(String bucket, String key) {
LOGGER.info("Resizing s3://" + bucket + "/" + key);
//获取user-id,对应配置x-amz-meta-user-id
final String userId = s3client.getObjectMetadata(bucket, key).getUserMetaDataOf("user-id");
LOGGER.info("Image is belonging to " + userId);
final String destinationKey = "users/" + userId + "/picture/small.jpg";
//s3自带copy方法
s3client.copyObject(bucket, key,bucket, destinationKey);
LOGGER.info("Image has been copied to s3://" + bucket + "/" + destinationKey);
}
@Override
public Void handleRequest(S3Event input, Context context) {
//循环触发的事件,并调用resizeImage方法
input.getRecords().forEach(s3EventNotificationRecord ->
resizeImage(s3EventNotificationRecord.getS3().getBucket().getName(),
s3EventNotificationRecord.getS3().getObject().getKey()));
return null;
}
}
2)配置Bucket事件通知Lambda函数
我们需要在CloudFormation中定义Lambda函数及授权器,并在上面的bucket配置中增加NotificationConfiguration。
Lambada函数
"ImageResizerLambda": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "com.serverlessbook.lambda.imageresizer.Handler",
"Runtime": "java8",
"Timeout": "300",
"MemorySize": "1024",
"Description": "Test lambda",
"Role": {
"Fn::GetAtt": [
"LambdaExecutionRole",
"Arn"
]
},
"Code": {
"S3Bucket": {
"Ref": "DeploymentBucket"
},
"S3Key": {
"Fn::Sub": "artifacts/lambda-imageresizer/${ProjectVersion}/${DeploymentTime}.jar"
}
}
}
},
Lambda授权器
"ImageResizerLambdaPermisson": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "ImageResizerLambda"
},
"Principal": "s3.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:s3:::${DomainName}-profilepictures"
}
}
},
Bucket添加通知事件,在这里详细定义了Bucket监听的事件,触发条件和响应的Lambda函数。
"ProfilePicturesBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": {
"Fn::Sub": "${DomainName}-profilepictures"
},
"NotificationConfiguration": {
"LambdaConfigurations": [
{
//响应 新增事件
"Event": "s3:ObjectCreated:*",
//函数调用条件
"Filter": {
"S3Key": {
"Rules": [
{
"Name": "prefix",
"Value": "uploads/"
}
]
}
},
//触发的Lambda函数
"Function": {
"Fn::GetAtt": [
"ImageResizerLambda",
"Arn"
]
}
}
]
}
}
},
以上配置完,发布工程。我们在上传一次图片,就可以发现新建的Bucket下创建了 users/用户ID/picture目录,里面有一个small.jpg的图片。

事件触发Lambda函数的功能基本完成,为了方便我们通过自己的域名访问这张small.jpg图片,我们只需要在之前《AWS Lambda笔记-内容分发(CDN)-7》中简单的新增配置,就可以完成。
3. 配置CloudFront让图片访问更快更方便
我们需要用自己的域名访问修改的samll.jpg图片(例如:http://serverless.xxxxx.com/1234/picture/small.jpg)
1)配置CloudFront源站信息
2)配置cloudFront缓存行为
1)配置CloudFront源站信息
把刚创建的ProfilePicturesBucket的bucket作为CloudFront的源站,及在 CloudformationDistribution的Origins中增加:
{
//指定源的域名
"DomainName": {
"Fn::Sub": "${ProfilePicturesBucket}.s3.amazonaws.com"
},
//源的唯一标识符,可以自己定义,在下面的缓存行为中使用到
"Id": "PROFILE_PICTURES",
"S3OriginConfig": {}
}
CacheBehaviors中增加
"CacheBehaviors": [
{
"PathPattern": "/users/*/picture/*",
//定义的源站
"TargetOriginId": "PROFILE_PICTURES",
"Compress": true,
"AllowedMethods": [
"GET",
"HEAD",
"OPTIONS"
],
"ForwardedValues": {
"QueryString": "false",
"Cookies": {
"Forward": "none"
}
},
//TTL都为0,直接访问源
"DefaultTTL": 0,
"MinTTL": 0,
"MaxTTL": 0,
"ViewerProtocolPolicy": "redirect-to-https"
}
]
再一次发布工程./gradlew deploy
,我们可以在[CloudFront控制台],通知(https://console.aws.amazon.com/cloudfront/home)看到刚才的配置信息,同时可以输入自定义域名及图片路径访问到图片。


源代码
代码下载地址:https://pan.baidu.com/s/1RYcGMEh0W_-n_MkQlEYCAw
提取码:umci:
网友评论