oynix

于无声处听惊雷,于无色处见繁花

AWS Lambda + API Gateway 搭建全流程

API Gateway接收请求,继而传递给Lambda处理。

1. 引言

发展至今,AWS已然成为一头庞然大物,集成并提供着几千个服务,即便只是一个小需求,也很难只用其中的一种服务,而是多种组合来满足。这篇文简单说说通过AWS的Lambda和API Gateway作为主框架,来搭建一个无服务架构。

之所以会有眼花缭乱的服务,我想,应该是为了满足各种生产需求。拿车来举个例子,假如一台30万的车,买下来可以开10年,平均一年3万,如果租的话,一天大概300。很容易可以算出:当一年当中有用车需求的天数达到100天时,租车和买车的成本是一样的,少于100天时,租车更划算,反之则买车更划算。一家原本只提供卖车服务的公司,为了满足用车天数少的用户,也是为了赚更多的钱,便增加了一项租车服务,就这样,业务线从一条增加到了两条,随着用户需求的增加,业务线会越来越多。所以,服务种类的增加,根源上都是为了满足不同的需求,我想AWS应如是。

回到正题。

Lambda是AWS提供的一种FaaS(Function as a Service)服务,是继IaaS(Infrastructure)、PaaS(Platform)、SaaS(Software)和BaaS(Backend)之后的新服务,用户只需要关心业务逻辑的实现,其他的均交给Lambda来做,省去了配置云设施、监控、集群等步骤,用完付费就可以了。本打算用钱换时间,但实际上,时间成本并不会减少,因为省下来的时间都用来学习使用这些服务,甚至可能还不太够。

而API Gateway,正如名字所言,是一个网关,用来接收请求,并转发。它的功能就是这样,具体怎么使用要看用户怎么给它定位。Lambda其实是支持直接请求的,每个Lambda都可生成一个唯一的URL,对于客户端来说,这个URL就是服务器的地址,可以向这个地址发送网络请求,Lambda收到请求后,就会把请求的数据交给里面的逻辑代码处理。但是,普遍性而言,都不会暴露Lambda的URL,而是中间通过API Gateway来接收,再转发到Lambda。因为API Gateway是个独立的服务,并不是和Lambda绑定,它收到请求后,可以转发给Lambda,也可以转发给其他服务,还可以转发给不同的Lambda。Gateway的存在,极大的增加了服务器的灵活性。

2. Lambda编写和部署

现在Lambda支持了7种语言的支持,如Java、Python、Go、Node.js等,但流程都是类似的,提供一个入口函数,Lambda收到请求时,就会调用这个方法,请求相关的数据会以参数的形式传递给函数,而我们需要做的就是编写处理这些数据的逻辑代码,然后再将处理结果通过函数的返回值传递给Lambda,这样,一次调用流程就走完了。

计费方式,则是这个过程中所使用的资源,比如CPU时长、占用内存大小、占用本地存储的大小,传递数据的大小,等多个维度,用了多少就付多少的钱。每个月会送个几百万次,所以单独看一次正常的调用,可能花一分钱都不到,约等于免费。但是,是真的免费吗?一个2万的包,假设可以背2年,一天27块钱,平均一分钟一分多钱,是不是也可以约等于不花钱。

至于部署,则有多种方式,本质上也都是一样的,即,把可执行的代码上传到Lambda,以便于Lambda可以调用。可以在网页上上传zip压缩包,也可以将zip上传到S3,再从S3上传到Lambda,也可以通过AWS CLI命令行,或者其他三方工具,等等。我用的serverless,官网地址,它本身和AWS的Lambda好像是同等定位的,也是卖FaaS服务的,我没细看,只用了它提供的命令行,先写配置文件,然后通过serverless命令就可以一键部署了。这里要注意一下版本,最新版3xx竟然和Tencent跳到一条船上了,用起来很多限制,束手束脚的,如果介意,安装旧版本即可

1
npm install -g serverless@2.72.2

我选用的Go,配置文件名为serverless.yml,其中,有这么几个选项是必须的,具体写了简短解释,除此之外,还有很多可以配置的选项,比如使用的资源,触发的条件,执行的角色,数量多到吓人,如有需要,可以去文档里直接搜索,然后按照格式配置即可,这里是文档地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
service: sample-lambda
frameworkVersion: '2' # version of serverless

# provider
provider:
name: aws #
runtime: go1.x # lambda将在runtime配置的环境中执行我们上传的代码,
lambdaHashingVersion: 20201221
stage: ${opt:stage, 'dev'} # 表示从serverless命令中读取名字为stage的参数,如果没指定则使用缺省值dev
region: ap-south-1 # 部署的区域
profile: ${self:custom.profile.${self:provider.stage}} # 操作aws账户就需要权限,profile指定的就是认证信息,可通过aws configure命令配置
architecture: x86_64 # unsupported arm64 for go1.x # 指令集

# 指定需要将哪些内容打成zip压缩包
package:
patterns:
- '!./**' # 当前目录下所有文件夹都不需要
- ./bin/** # 只需要bin目录下,我把编译生成的可执行文件放在了这里

# 自定义的一些参数
custom:
version:
dev: 2
prod: 1
profile:
dev: profiledev
prod: profileprod

# function是Lambda中的调用单位,Lambda每次调用一个fuction,也就是函数,来处理收到的请求。可以部署多个function。
functions:
app: # 这里只部署了一个叫做app的function,它最终的名字是:{server_name}-{stage}-{function_name}
handler: bin/app # executable file contains main function
memorySize: 128 # MB 执行是分配的内存
timeout: 60 # sec 超时时间,如果60秒后还不给lambda返回结果,那么lambda就会给请求者一个超时回应
environment: # 环境变量,可在代码中使用
Version: ${self:custom.version.${self:provider.stage}}

部署时,只用这么一条命令即可,注意要在配置文件serverless.yml目录下执行

1
serverless deploy --stage dev

每次来了新请求时,如果没有正在跑着的虚拟机,Lambda会启动一个新的虚拟机,来执行操作,很显然,启动并配置虚拟机是需要时间的,虽然很短,毫秒级别,但如果为了追求极致的体验,可以使用预配置并发,顾名思义,提前给你启动并配置好一定数量的机器,请求一旦来了便直接处理。既然有机器在跑,那肯定是要收费的了。就像冬天想要开车,需要热个一两分钟再跑,如果你不想等这一两分钟,那就要保证车是提前热的,也就是一直没有熄火,既然没熄火那自然是要烧油的了。

此外,如果要删除Lambda,一定要把相关联的CloudFormation和S3中对应的存储桶删掉,这样才算完整删除。其中,CloudFormation负责把S3中的代码部署到Lambda。

3. 附加:压缩可执行文件

go build后出来的可执行文件很大,是可以通过压缩来减小大小,来提高传输效率。如果你坚定的选择不压缩,似乎也没什么问题。

go build默认是用静态编译,即编译后的文件中包含执行中所有需要的库,所以,即便调用了一个库里的一个函数,那么也会把这个库完整的打包进可执行文件里,有时发现只加了几行代码,但是编译后的文件却一下大了很多,原因便在此。

除了静态编译,还可以动态编译,也就是说,不把所有的依赖库都打进可执行文件里,对于一些常见的系统库,选择在执行时动态链接的方式,这样一来可以大大减少可执行文件的体积。但是,有得便有失,如果执行的环境中没有需要的库,那么便无法正确执行。

我选用的方式是,静态编译,在生成的可执行文件基础上,再进一步压缩。动态编译体积能减少很夸张,有时能达到90%以上,而upx最好也就70%的样子。相比于极致,我更追求可靠。此外,在build时加上trimpath和ldflags可以进一步减小体积,虽然有限,但苍蝇腿也是肉不是。

1
2
3
GOOS=linux CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o bin/app_origin cmd/app.go

upx -9 bin/app_origin -o bin/app

4. 附加:角色和策略

在AWS世界设定里,操作是需要一个角色来执行的。比如,你想执行Lambda,那么你就需要为这个操作指定一个角色,让这个角色来执行。打个比方,你想打开一个楼里面的102号房间,那么你就需要指派一个人,去找到102号并打开,这里的这个人,就对应AWS里的角色概念。那么什么是策略呢?策略就是为这个角色附加的权限,接着刚才那个例子,你指派完这个人之后,还需要给他102号房间的钥匙,这样等他到了102号房门口,才能用钥匙打开门。这里的钥匙,就是策略。

所以,我们要执行Lambda,就要先创建一个角色,然后再创建一个策略,为这个策略添加可以执行Lambda的权限,接着把策略赋予给角色,最后把角色指派给我们创建的Lambda。如果该角色没有执行Lambda的权限,那么在执行的时候就会报错。

AWS中所有的操作都有对应的权限。在创建一个策略时,需要给这个策略指定4个参数:服务、操作、资源和请求条件。服务,就是AWS中所有提供的功能;操作,这里的操作分的很细致,比如列表、读取、写入等等;资源,指的是该服务下的所有资源,可以指定具体到某一个,也可以某一范围;而请求条件,则是对调用者身份的验证。

举个例子,我想查看S3中名字叫Upload的桶里的avatar.png(S3是AWS的一款云存储的服务,可以简单理解成网络云盘),如果给这个操作创建一个策略的话,那么服务便是S3,操作是读取Get,资源是Upload/avatar.png,不加额外的请求条件,然后把这三个组合在一起,就是一个策略,附加了这个策略的角色,就可以执行查看avatar.png的操作了。

对于其他的行为,都是一样的道理。AWS中对操作和资源划分的粒度极小,基本不可再分割,所以可以搭配出各种权限策略。当然,一个角色可以同时附加多个策略,意味着它可以执行多个操作,创建策略的基本理念就是,只赋予必要的权限,避免过宽的权限带来的不必要问题。

5. API Gateway

API Gateway可以用来接收客户端请求,但它不做处理,而是将请求转发出去,交给别人去处理,等拿到别人返回的处理结果后,再把结果返回给客户端,这样就完成了一次网络调用。虽然直观上看这个操作很像是转发,但在API Gateway里,它不叫转发,而是叫做集成,integration。这只是一个粗糙的介绍,而实际上这里面还有诸多细节。它有多种,比如websocket类型的,http类型的,REST类型的,这里只说REST类型。

在使用REST API Gateway时,要先创建资源,比如students,然后再在这个资源上添加请求方法,比如GET,一般用来表示获取,然后再把这个方法集成到Lambda,接着还要部署,部署时要指定一个阶段stage,比如叫做dev,成功后会生成一个URL,这个时候,客户端就可以发起请求,来获取所有的学生

1
2
3
Method URL/{stage}/{resource}

GET URL/dev/students

对于URL,可以配制成固定的域名,如果没有这样的需求,可以使用AWS提供的缺省URL,它的一般格式是

1
https://{api_gateway_id}.execute-api.{region_name}.amazonaws.com

API Gateway从客户端那里接收到的请求,叫做方法请求,把转发请求包装一下,这时叫做集成请求,然后转发给处理者,等从处理者那收到响应,这个响应叫做集成响应,再包装一下,这时候叫做方法响应,最后把方法响应返回给客户端。这个过程中,一共出现了4个变量:方法请求、集成请求、集成响应和方法响应。在每一个变量上面,都可以做一些操作。

要敏感的记住这4个变量,因为下面的所有操作都是基于它们的,会频繁提到,最好可以一提到名字就能反应过来在说谁。

比如数据转换,客户端发来的数据Lambda不能处理,则需要在集成请求中转换一下,然后再发送给Lambda。或者Lambda返回的数据不是客户端想要的格式,那么需要在集成响应中做转换后再返给客户端。

此外,可以在方法请求上添加请求验证,如何客户端验证不通过,则直接返回,后面的流程不会再走,减少无效的调用,毕竟每次调用Lambda都是要花钱的,能省则省。

6. API Gateway转发至Lambda

从Gateway过去的数据,实际上就是一个JSON串,这里面有请求的Resource,请求的Method,请求的Header,请求的Body等相关的所有数据。这个时候可以使用Lambda代理集成,它会把客户端发来的所有数据,原封不动的发给Lambda,Lambda根据Resource判断客户端请求的是哪个资源,根据Method判断请求方式,根据Header、PathParameter,QueryParameter来作出相应的逻辑处理,最终返回结果数据,这和处理Http请求本质上是一样的。

Gateway发给Lambda的数据是一个JSON串,但是不同的集成配置,里面的字段是不一样的,所以要用不同的结构体来接收。当使用Lambda代理集成时,数据结构是下面这样的,Request中的数据包括了所有Http相关的字段,而Response中的字段也是和Http响应结构中的数据一一对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import (
"context"
"database/sql"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
"github.com/aws/aws-lambda-go/events"
)

func main() {
lambda.Start(handler)
}

func handler(ctx context.Context, event events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error) {
lctx, _ := lambdacontext.FromContext(ctx)

// logic handle

return response, err
}

type APIGatewayProxyRequest struct {
Resource string `json:"resource"` // The resource path defined in API Gateway
Path string `json:"path"` // The url path for the caller
HTTPMethod string `json:"httpMethod"`
Headers map[string]string `json:"headers"`
MultiValueHeaders map[string][]string `json:"multiValueHeaders"`
QueryStringParameters map[string]string `json:"queryStringParameters"`
MultiValueQueryStringParameters map[string][]string `json:"multiValueQueryStringParameters"`
PathParameters map[string]string `json:"pathParameters"`
StageVariables map[string]string `json:"stageVariables"`
RequestContext APIGatewayProxyRequestContext `json:"requestContext"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
}

type APIGatewayProxyResponse struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
MultiValueHeaders map[string][]string `json:"multiValueHeaders"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
}

7. API Gateway转发至S3

我这里还有个需求,就是上传文件,那首选肯定是S3了,那Gateway收到请求后,可以直接将请求转发至S3,而不需要多次转手,这个时候就凸出了Gateway的灵活性。在配置集成请求时,就不能再选Lambda了,而是选AWS服务,在其中找到S3即可。

S3是根据URL来操作对应的资源的,而客户端发来的请求是API中定义的资源,所以这其中就需要转换路径,将客户端的请求URL转换成操作S3中资源的URL

1
https://s3.{region_name}.amazonaws.com/{bucket_name}/{file_name}

S3的Path是由存储桶的名字和资源名字组合而成,在集成请求中覆盖路径时,就要填上对应的参数,可以在集成请求中定义变量,用变量来指定方法请求中的值,比如方法请求的URL是

1
URL/dev/{dir}/{file_name}

dir指定目录,file_name指定文件名,让客户端来决定想要向哪个目录上传什么文件

1
URL/dev/avatar/tom.png

这个请求表示向avatar目录上传一张叫tom.png的图片,对应的业务需求可能就是一个叫tom的用户更新了自己的头像。那么Gateway就可以在集成请求中定义两个变量:bucket_name和file_name,分别映射方法请求中的dir和file_name,用{bucket_name}/{file_name}来覆盖掉原Path,然后发送给S3就可以了。

注意,在集成请求中想要使用方法请求中的参数,比如Query参数、Path参数或者Header参数,方式就是在集成请求中定义变量,然后将变量映射到方法请求中的参数,这样通过变量就可以拿到方法请求中的参数值了。

上传成功时,S3返回的响应是没有Body的,但是我想把上传文件的下载路径返回给客户端,也就是我希望方法响应的Body里是文件下载路径,这个时候就需要在集成响应中做数据转换了,把原Body(空)转换成目标格式:{"link":"https://xxxxxxxxx"},这里面要用到Apache的VOLICITY语言,照着文档编写,不算太难。

8. API Gateway授权方配置

上面提到,在方法请求中可以添加授权的验证,来限制客户端的请求,而这个请求我便是加在了上传文件的方法请求上,因为并不是谁都可以上传文件,只有登录后的用户才可以,原因很简单,因为S3存储文件也是收费的,一切为了省钱。

授权有两种类型,一个是IAM,一个是授权方,这里说一说授权方。授权方的工作流程是这样的,Gateway收到客户端的请求后,将请求中的关键数据发给授权方,授权方会返回一个结果,Gateway根据返回的结果来判定客户端是否具有访问的权限。这就像伤残鉴定,我撞到了你,你说你受伤了,想要100万赔偿,结果去医院一鉴定,医生说你是装的,别说100万了,连100块都没有。

所以,要想增加权限的判定,就要先创建一个授权方。

授权方有两种类型,一种是Cognito,这个是AWS提供的一种服务,收费的。另一种是Lambda,也是收费的。既然都要花钱,那相比Cognito这个陌生的面孔,我更愿意使用Lambda这个老伙计。Cognito的流程大概是这样子的,它需要用户先行登录,不管是通过第三方登录,比如Facebook、Google,还是自己的登录功能,这些都可以,登录成功之后要获取一个和用户相关的ID,再用这个ID去Cognito那里去换一个Token,这个Token就是通行证了,用这个Token就可以通过Cognito的验证。

如果是用Lambda,那么Gateway在收到客户端请求后,会将请求Header中的Authorization拿出来,发送给Lambda,Lambda中则需要判断这个Authorization是否合法,然后将结果返回即可。

9. SES

我这里还有个发邮件的需求,所以还用到了AWS的SES服务,Simple Email Service,只要你能想到的需求,AWS都有与之对应的一个或者多个服务卖给你,这句话说的果然没错。发送邮件时,直接调用官方提供的SDK即可。既然想发邮件,那么就需要有这个操作的权限,上面说到过,权限是怎么来的呢?是根据执行角色身上所挂着的所有策略来的,因此,要给执行Lambda的角色加一个发送SES的策略,可以单独加策略,也可以直接在原策略上增加权限。

SES有两种模式,一种叫domain,一种叫email,对于收邮件的人来说,这两种的区别就在于收件人的地址。domain需要给SES配置一个自己的域名,要提前买好且确保可用,收件人看到的发件人地址就是配置的域名了,如果没有域名,就要用email模式了,这个时候收件人看到的发件人地址长到可以从这排到法国,像这样

1
01090181be5c4cf3-6ccb1da1-5068-4759-92e4-17b467f5cf7e-000000@ap-south-1.amazonses.com

没错,我用的就是这用,抛开其他原因不谈,最主要的还是因为我没有域名。配置email模式时,AWS需要你提供一个发件人的邮箱,意思就是,虽然地址是上面这一长串,但是发件人的名字却是你提供的这个邮箱的名字。假如,我提供的是ohmyladygg@gmail.com,然后收件人看到的发件人地址就是上面那一长串,发件人名称是ohmyladygg

光提供邮箱还不够,AWS还要验证一下,确保这个邮箱真实有效,并且属于你。方式就是向这个邮箱里发个链接,只要在24小时之内点开,那么就算验证通过了,简单的很。

此外,默认的SES是处于沙盒状态的,什么是沙盒状态呢?其实就是比正常状态多了几个限制,比如每24小时内只能发送200封邮件,比如只能发送给验证过的邮箱,没错,这也就是说,只能用验证过的邮箱,再发送给验证过的邮箱,自己和自己玩。如果想要解除沙盒状态,那么需要提供一些资料,然后发申请,有点麻烦。

10. RDS

关系型数据库,Relation Database Service,看吧,只要想买,AWS就有的卖。有多种可供选择,比如MySql、Oracle、Aurora等等。我常用的就是MySql,其中的Aurora就是Amazon在MySql基础之上,做了一些处理,优点就是相比于MySql性能更好一些,缺点就是更贵,当然,这不是它的缺点,而是我的缺点。当数据量特别大,需求特别高时,Aurora确实可以提供更好的服务,更佳的表现,若要平时开发,MySql表示可以再战个几年不是问题

11. VPC和子网

如果你真的把上面这些操作了一遍,你会发现到处都能看到VPC这个东西,它的全称是Virtual Private Cloud,虚拟私有云,是以地区为单位的账号隔离。乍一听可能听不懂,不像是人话,上比喻大法。

先说地区。AWS在全球多个地区都有机房,比如,美国西部有两个,加利福尼亚(us-west-1)和俄亥俄(us-west-2),亚太东部有一个,香港(ap-east-1),亚太南部有一个,孟买(ap-south-1),等等,有很多。只要你创建了账号,那么就会在每个地区给你创建一个VPC,这个VPC默认是隔离的,别人无法访问,就好比在里面单独建了一个房间给你,开启一台EC2机器实例后,将其放到这个房间里,那么它就是绝对安全的,因为外界访问不到。

再说子网。在一个地区,其实不仅仅有一个机房,至少要有两个,这样做不是因为AWS有钱,而是为了容灾,尤其是日本,动不动就地震,可能一个纵波过去,房子就没了,那机房自然也无法幸免于难,一个地区建立多个机房就是为了应对这种情况。机房选址也是有要求的,不能离的太近,也不能离的太远,机房之间用高速线路连接,延迟极低。一个地区有一个VPC,那么一个机房就对应一个子网,比如孟买,可以看到默认有三个子网,ap-south-1a、ap-south-1b和ap-south-1c,这就表明在孟买有三个机房。既然都同属于一个VPC下,那么子网之间是可以通信的,就好比虽然我们位于不同的角落,但既然在同个房间里,那么我们就可以聊天。

这些都是默认的情况,除此之外,我们还可以手动建立更多的VPC和子网,来满足各种需求。

12. 安全组和网络ACL

上面说到,默认情况下VPC是隔离的,也就是说这个房间是没有门的,外面的人进不来,里面的人也出不去,安全是安全了,但世界那么大,终归是有人想出去看看的,这个时候就需要给房间装个门,这个门就是互联网网关,还有其他种类的门,但这里只说说这种。

既然装了门,那么就有了进出的可能,为了安全,所以还要控制谁能进来,谁能出去,这件事,归安全组和网络ACL负责,进来的叫做入站流量,出去的叫做出站流量。它们两个很像,都是通过配置入站规则和出站规则来控制流量。

但还是有些区别的,不然也就不会二者都存在。网络ACL是在子网级别运行,而安全组则是在实例级别运行,就像是防火墙一般,这是最大的区别。其他的就不多说了,这里是官网的详细说明

额外多一句安全组的源/目标,源就是进站流量的IP地址,目标就是出站流量的IP地址,支持v4和v6。可以限定精确的IP地址,也可以限制范围的IP地址,用CIDR表示,如203.0.113.0/24,还可以使用前缀,这个我没用过。最后还有一种,就是指定安全组,什么意思呢,就是可以把IP限定为使用同一个安全组的实例,比如把源设定为sg-002,那么来自这个安全组的所有流量,都在这个设定范围内,这也是缺省的设置。这个有点儿不同与防火墙的概念了,原本安全组的使用定位就很接近于防火墙,但是又可以使用安全组来过滤IP。

13. 本地连接RDS

上面说完了安全组和网络ACL,接下来再说说如何从本地连接到数据库。这是个很常见的需求,但仅仅限于连开发的数据库,正规一点的,从外面是无论如何也连不上生产数据库的,如果能按这标准来执行,或许能少不少删跑路的英雄事迹。但生产数据库也是有连接的需求的,一般都是用个跳板机,然后再限制连接跳板的权限,这样安全性就提高了不少。

回到正题,接着说从本地直接连数据库。数据库本质上就是一个或者多个EC2实例,所以在创建阶段就需要选择一个VPC,且不可更改,创建时AWS会选一个可用区,在这个区启动机器,所以,创建成功之后,VPC和子网都是确定了且不能修改的。上面说到,控制访问可以通过网络ACL和安全组,网络ACL是作用在子网级别的,修改后会影响到子网内的所有机器,这个轻易不要操作,现下就要通过修改数据库所使用的安全组了。

如果没有什么特殊需求,一般操作是允许网络ACL的所有进站和出站流量,也就是不限制,然后各个实例根据自己的需求修改关联的安全组。我在这里的操作就是如此,创建一个安全组,进站流量类型限制到MySql,端口3306,IP不作限制,出站流量规则不做限制,这样就可以在本地连上数据库了,下面是测试是否可连接的命令

1
nc -zv {mysql_host_url} 3306

14. 部署前端到S3

所谓部署前端,其实就是将打包好的网页App存储到一个可供用户访问的网络位置。网页App包以其中的index.html作为入口,同时还包含需要的所有资源,如js文件、图片资源、样式文件、字体文件,等等。S3可为存储桶中的文件提供一个可公开访问的URL,这样就可以将App包上传到存储桶,将存储桶公开,通过index.html的URL即可访问。当然,这些仅限于静态网页,毕竟存储桶是个存储设备,若要做动态网页需要考虑其他方案。

存储桶支持动态网页部署的功能,可设置入口文件和错误文件,默认做法是将这两个选项都设置成index.html,因为有些请求内会返回非200,这个时候也是需要访问index.html,所以都设置成一样的即可。

15. 关于跨域

当一个URL地址为https://www.aaa.com网页中,向https://www.bbb.com发起了一个请求,这个时候就发生了跨域操作,不仅如此,当protocol、host和port中,任何一个不同时,都是跨域操作,CORS(Corss Origin Resources Sharing),这样的请求会被浏览器拦截,是发送不出去的。但是,如果通过命令行或是其他Http客户端,如PostMan之类,你会发现一切都是正常的,所以,可以明确的是,跨域这个策略policy是浏览器的行为,而不是https://www.bbb.com的拒绝提供服务。

在浏览器看来,他觉得既然你这个网页是来自a,你却向b发出请求,那么这个行为就是有安全风险的,所以要禁止。比如百度搜索的网页上搜索一个东西,百度拿到搜索关键词之后什么都不干,扭头就调用谷歌的接口,把谷歌返回的结果展示个用户,结果广告费都让百度赚了,活却都是谷歌干的。当然这只是一个不太恰当但却能说明事情的小栗子。

但是,这种跨域请求却是一个比较常见的场景,尤其是在前后端分离的架构中,前端网页部署在S3上,访问网页肯定是S3的URL地址,而服务器部署在API Gateway,向服务器发请求时肯定是Gateway提供的URL,这两个URL是不一样的。还有一些场景,如在网页上调用一些三方的服务接口,如统计、广告等,都会出现跨域的操作。

解决跨域问题有两种方式,很显然,一个是在前端网页处理,另个是在后端服务器处理。

前端网页处理时,就要设置一个代理,通过代理去访问服务器,以此来通过浏览器的检查。而后端服务器的处理方式则是告诉浏览器:嘿,你别拦截了,我允许他访问我,我们俩是一伙的。具体方式就是,在发起请求前,先向服务器发送一个OPTIONS请求,服务器把允许的Origin、Headers和Method都返回来,浏览器按照服务器提供的白名单做出检查,如果都符合则放行,否则拦截。Origin表示允许谁访问,Headers表示允许的访问头,Method则表示允许的请求方式,返回*则表示允许任何人访问。

这三个字段对应三个请求头:Access-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-Methods,不仅是OPTIONS请求,在服务器返回的所有回应中,都需要带着这三个header。

而通过在API Gateway中通过Lambda代理集成将请求转发至Lambda时,是不能修改集成响应的,这也就意味着不能载方法响应中添加header(注意区分集成响应和方法响应),AWS文档给出的解决方案是,在Lambda中手动添加,也就是在代码里添加这三个header。

16. CloudFront

这个是AWS提供的一种CDN服务,如果前端网页的用户较为分散,在多个全球的多个区,可通过CDN为访问加速。先创建一个CDN分发,然后将源配置到S3的存储桶上,这里要注意权限问题,部署之后,用户就可以通过CDN提供的URL地址访问前端网页了,首次访问时CDN会从S3请求资源,并缓存,后面再有该地区的请求时,就会直接返回CDN的缓存来提高速度。这里有个问题,据说某些地区无法访问到CloudFront,不知是真是假,如果是真的,不知道AWS解决了没。

还要再提一句收费的问题,S3是按照存储和传输收费的,而CloudFront是按照传输收费的,定价里写是这么写的,但是月底看账单时,总能发现一些小惊喜,给这平淡无奇的生活增添一点小惊喜。如果数据量小,访问S3和访问CloudFront的成本区别应是不大。

17. 总结

虽然表明上看只需要Lambda和API Gateway,但实际上里里外外用了很多服务,比如CloudWatch、RDS、SES、CloudFormation、VPC、S3、IAM、CloufFront等。但多数内容都是一次性的,也就是在初始化时配置一次,然后就不需要再管了,后面只需要关系自己的业务逻辑即可,相对来说还是比较省心。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/07/b3dc6ccd3531/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道