一、前期准备
1.1、业务需求
假设 T 公司有一个全球性的网站,每个国家站点都有一个下载页面。公司想要去监测全球用户的下载情况,需要对下载按钮进行埋点,那我们就需要有一个接口可以监测到用户的下载情况,需要记录的数据有如下:
- country:用户来自哪个国家
- create_time:下载时间
- ip:用户的 IP
- referer:从哪个页面下载的
- site:国家站点代码
我们不可能针对每个站点都做一个链接作为借口,最好的方式就是创建一个链接,通过传递不同的参数来埋到不同的下载按钮,链接类似下面结构:
https://down.wzlinux.com?type=CN
https://down.wzlinux.com?type=US
https://down.wzlinux.com?type=IN
1.2、解决方案
看到这个需求,一个专业的开发人员可能很快可以解决,需要较高的开发技能,还需要运维购买服务器,部署等,相对来说比较麻烦,并且管理起来相对复杂。本文所展示的就是你只会简单的功能函数开发即可完成这个需求的开发部署,而且全部使用 AWS 的无服务器组件,用户不需要再关系底层硬件,来多少请求量我们就花多少钱,也可以节省不必要的支出。
我们知道在 web 里面重要的一个组合 LAMP(Linux + Apache + MySQL + PHP)。在 AWS 中呢,也有一个无服务器架构组合 Lambda + API Gateway + DynamoDB,这个组合正好满足我们的需求。API Gateway 用来接收用户的请求数据,是实现 web api 的重要组件,Lambda 用来实现逻辑处理,选择用户需要的数据,DynamoDB 用来存储 Lambda 的数据。
二、部署
整个架构有一些依赖关系,Lambda 依赖 DynamoDB,API Gateway 依赖 Lambda,所以我们先去创建 DynamoDB 表,然后去创建 Lambda 函数,最后创建 API Gateway。
所有的操作均选择在 AWS us-east-1 区域。
2.1、创建 DynamoDB
创建 DynamoDB 非常简单,在 Console 简单点几下就可以,因为 DynamoDB 需要一个主键,我们把生成的 uuid 作为主键,填写完成创建即可。
是不是很简单,就这样创建好了,如果你使用 awscli 创建的话,那更简单:
aws dynamodb create-table --table-name wzlinux-down --attribute-definitions AttributeName=id,AttributeType=S --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST --region us-east-1
就这样简单的创建好了,下面我们开始创建比较重要的 Lambda。
2.2、创建 Lambda
创建角色权限
因为 Lambda 需要有权限向 DynamoDB 写入数据,我们可以自己创建一个 Lambda 角色,赋予对呀的权限,在创建的时候,把我们所创建的 Role 赋予 Lambda,我这里给的资源范围大了一些,安全要求高的,可以给予最小的权限,权限 json 如下:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup",
"dynamodb:PutItem",
"dynamodb:Scan",
"dynamodb:Query",
"dynamodb:UpdateItem"
],
"Resource": "*"
}
]
}
console 创建
创建 Lambda 也比较简单,大部分参数选择默认即可,注意在 Permissions 里面选择我们上面创建好的 Role,运行环境我们选择 Python,当然 Lambda 也支持 .NET、Go、Java、Node.js、Ruby,甚至还支持用户自定义运行环境。
制作代码包
我这里已经把 Lambda 函数写好了,文件命名为lambda_function.py
。
import boto3
import os
import uuid
import time
from requests import get
def lambda_handler(event, context):
recordId = str(uuid.uuid4())
ctime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
sourceIP = event["headers"]["x-forwarded-for"]
url = "https://api.ipdata.co/{0}/country_name?api-key=40b58339be0ec3ba64fb936da37dbab9c9d1677f335c50910c52aeb7".format(sourceIP)
country = get(url).text
try:
referer = event["headers"]["referer"]
except:
referer = "NULL"
try:
site = event["queryStringParameters"]["type"]
except:
site = "NULL"
# Createing new record in DynamoDB table
dynamodb = boto3.resource(‘dynamodb‘)
table = dynamodb.Table(os.environ[‘DB_TABLE_NAME‘])
table.put_item(
Item={
‘id‘: recordId,
‘site‘: site,
‘create_time‘: ctime,
‘ip‘: sourceIP,
‘country‘: country,
‘referer‘ : referer
}
)
return {"statusCode": 200, "body": "OK"}
大致介绍一下函数,我们从 API Gateway 传入的参数获取用户来源 IP,然后通过一个接口把 IP 转换为国家。
再一个就是获取链接从哪个页面请求过来的,还有一个就是给我上面传入的参数 type 的取值。
Lambda 获取的参数主要通过 event 传递进来,event 是 API Gateway 传过来的参数,我们使用最简单的 HTTP API,我这边记录了传过来的 event,详细信息可以参照官方文档,这样大家就可以更加理解 Lambda 函数是怎么截取的数据。
{
"version": "2.0",
"routeKey": "GET /",
"rawPath": "/",
"rawQueryString": "type=CN",
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding": "gzip, deflate, br",
"accept-language": "zh-CN,zh;q=0.9",
"content-length": "0",
"host": "49imv2pyz1.execute-api.us-east-1.amazonaws.com",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36",
"x-amzn-trace-id": "Root=1-5f2f6066-93614fb0bb62a21428cb8508",
"x-forwarded-for": "52.221.86.56",
"x-forwarded-port": "443",
"x-forwarded-proto": "https"
},
"queryStringParameters": {
"type": "CN"
},
"requestContext": {
"accountId": "921283538843",
"apiId": "49imv2pyz1",
"domainName": "49imv2pyz1.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "49imv2pyz1",
"http": {
"method": "GET",
"path": "/",
"protocol": "HTTP/1.1",
"sourceIp": "52.221.86.56",
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36"
},
"requestId": "Q-wAChzJoAMESlQ=",
"routeKey": "GET /",
"stage": "$default",
"time": "09/Aug/2020:02:33:10 +0000",
"timeEpoch": 1596940390575
},
"isBase64Encoded": "False"
}
因为 python 函数有依赖包,我们需要把依赖包一并打包传到 Lambda,关于打包的过程如下,我们这里用到的依赖包为 requests。
- 使用
pip
的--target
选项在新的项目本地package
目录中安装库。
~/my-function$ pip install --target ./package requests
- 创建包含依赖项的 ZIP 存档。
~/my-function$ cd package
~/my-function/package$ zip -r9 ${OLDPWD}/wzlinux-down.zip .
- 将您的函数代码添加到存档中。
~/my-function/package$ cd $OLDPWD
~/my-function$ zip -g wzlinux-down.zip lambda_function.py
上传代码包
打包好的代码包wzlinux-down.zip
可以直接传到 Lambda,也可以先传到 S3,再上传到 Lambda,上传完成之后如下:
注意:因为我们代码中有环境变量,就是我们前面创建的 DynamoDB,在环境变量这一列创建好即可。
DB_TABLE_NAME:wzlinux-down
模拟测试
点击右上角的测试,我们发送测试的 json,这里我们就模拟 HTTP API,直接把上面的示例数据帖进去即可。
选择执行,查看到已经执行成功了。
然后再去查看 DynamoDB 中,是否已经有数据写入。
至此,我们的 Lambda 已经打通,同样附上使用 awscli 创建 Lambda 的命令,需要我们提前把代码和依赖库打包上传到 S3。
aws lambda create-function --function-name wzlinux-down --runtime python3.8 --role arn:aws:iam::921283538843:role/wzlinux-down-role --handler lambda_function.lambda_handler --code S3Bucket=code.wzlinux.com,S3Key=wzlinux-down.zip --environment Variables={DB_TABLE_NAME=wzlinux-down} --region us-east-1
--role:选择你创建好的 Role
--code:这里是我打包好的代码,上传到了我的 S3 中
2.3、创建 HTTP API
Console 创建
- 第一步,选择要集成的 Lambda 函数,已经 API 名称。
- 第二步,设置路由,我们这里就选择默认的根目录吧,方法选择 GET。
- 第三步,阶段方面也就选择默认吧,毕竟我们不会经常去发布新版本的 API。
访问测试
创建好之后,我们直接请求这个链接访问看看效果,当然我们可以再链接后面传递参数 type,如请求地址:
https://jat553o58l.execute-api.us-east-1.amazonaws.com/?type=CN
https://jat553o58l.execute-api.us-east-1.amazonaws.com/?type=US
同样查看一下 DynamoDB 数据库,查看数据是否入库:
自定义域名
为了方便记录,我们可以使用自己的域名,在 Custom domain names 里面进行创建。
创建好之后,我们为域名添加 CNAME 解析,解析值为 API Gateway domain name。
解析完成之后,我们需要为自定义域名配置一个 API mapping。
然后我们就可以使用我们最开始规划的地址进行访问了。
https://down.wzlinux.com?type=CN
https://down.wzlinux.com?type=US
https://down.wzlinux.com?type=IN
我们通过一个 URL,传递不同的参数,为各个国家生成了不同的接口,只需要埋到对应的页面,当用户访问的时候,我们就会捕获到数据,进而可以进行分析。
三、总结
AWS 这一套服务器架构(API Gateway + Lambda + DynamoDB),极大的减轻了我们开发 API 接口的工作量,使得只是简单懂得处理逻辑的函数开发人员即可完成 API 的开发,并且无服务器架构还有很多传统应用不具有的优势,可以预见无服务器架构会成为云计算的下一个纪元:
- 便宜:只有使用的时候才需要付费
- 可扩展:根据负载动态扩展
- 降低运维:不需要维护底层硬件和系统
- 用户体验:允许公司投入更多时间和资源来开发和改进面向客户的功能。
在第一次配置调试过程中,注定会遇到各式各样的问题,大家一定要充分利用 CloudWatch Logs 这个服务,我们的很多报错都可以在里面找到,最后,如果大家都已经学会了,那就开启你的无服务之旅吧,充分利用云原生的一些功能,把我们的应用进行现代化。