引用一段对于 Serverless 较为官方的定义:“Serverless 是一种执行模型(execution model)。在这种模型中,云服务商负责经过动态地分配资源来执行一段代码。云服务商仅仅收取执行这段代码所须要资源的费用。代码一般会被运行在一个无状态的容器内,而且可被多种事件触发( http 请求、数据库事件、监控报警、文件上传、定时任务……)。代码经常会以函数(function)的形式被上传到云服务商以供执行,所以 Serverless 也会被称做 Functions as a Service 或者 FaaS。”html
从定义中不难看出 Serverless 的出现免去了工程师在开发应用时对服务器与后端运维的考量,使工程师能够全心全意地投入业务逻辑代码的实现中去。再归纳一下 Serverless 老生常谈的几条优点:前端
Serverless 对于前端开发者来讲主要会有如下几个应用场景
AWS Lambda 是由亚马逊云服务平台( AWS )最先推出于 2014 年、最为著名的 Serverless 云计算平台之一。相较于其余云服务商,AWS Lambda 以完善的设施(触发器种类多、支持编程语言多……)和丰富的社区支持在多数评测中占据了上风。从体验与学习的目的出发,AWS Lambda 能够说是咱们的不二选择。node
冷启动是最常被提到的问题之一,用简单的话来讲就是当你的函数一段时间未被运行后,系统就会回收运行函数的容器资源。这样带来负面影响就是,当下一次调用这个函数时,就须要从新启动一个容器来运行你的函数,结果就是函数的调用会被延迟。来看一下在 AWS Lambda 上函数调用时间间隔与冷启动几率的关系:git
那么具体的延迟时间是多少呢?延迟时间受许多因素的影响,好比编程语言、函数的内存配置、函数的文件大小等等。可是一个较为广泛的建议就是 Lambda 不适合用做对延迟极其敏感的服务(< 50ms)。github
在使用 AWS Lambda 开发应用时,咱们所写的代码与 AWS 这个云服务商是具备强关联性的。尽管目前有一些框架(例以下文会应用到的 Serverless 框架)来帮助咱们抹平不一样服务商之间的代码差别,想从一个服务商迁移至另外一个服务商依然是一件繁重的体力劳动,甚至包含着必定的代码重构。web
函数式的开发模式注定了代码之间的复用与共享会成为一个难题,也注定了代码量会随着服务的增多而膨胀。 Serverless 会使得函数数量与代码量呈线性增加的关系,以下图数据库
当服务达到必定数量后,维护一个无限膨胀的代码库所须要的额外人力与开支也是不可小视的。npm
运行 AWS Lambda 的函数依赖于许多外部的库和应用(aws-sdk、API Gateway、DynamoDB...),所以想要在一个彻底本地的环境运行这样的函数是十分困难的。若是咱们每次修改函数后都须要部署并依赖于 AWS CloudWatch 中输出的运行日志来调试与开发 Lambda 函数,那想必效率是极低的。 Serverless CLI
对于本地开发难的问题,提供了必定的插件来支持(serverless-offline、serverless-dynamodb-local...)。不使用 Serverless CLI
开发 Lambda 的用户可能就须要研读官方提供的 AWS-SAM 文档来配置本地开发环境了。编程
AWS Lambda 收费的最小单位是 100ms ,也就是意味着你的函数哪怕只执行了 1ms 也会看成 100ms 来计费。这种计费方式在某些状况下甚至会致使使用高内存的函数比低内存的要便宜。咱们来看下 AWS Lambda 的计费表:json
举一个较为极端的例子:假设咱们设置了一个内存为 448MB 的函数,它运行时间为 101ms ,那么每次执行咱们都须要支付 0.000000729 x 2 = $0.000001458 。而若是咱们将这个函数的内存提升到 512MB ,使它的运行时间下降 100ms 之内,那么每次执行咱们只须要支付 $0.000000834 。仅仅是一个设置,咱们就下降了整整 (1458 - 834) / 1458 ≈ 42.8% 的成本!
找到性价比最高的内存设置意味着额外的工做量,很难想象 AWS 在这个问题上竟然没有为客户提供一个合理的解决方案。
在使用 AWS Lambda 时几乎全部的周边服务(API Gateway、CloudWatch、DynamoDB...)都是须要额外收费的。其中一个很明显的特征就是捆绑消费,你可能很难想象 CloudWatch 是在使用 Lambda 时被强制使用的一个服务;而 API Gateway 也是在搭建 http 服务时几乎没法逃过的一个收费站,其 $3.5/百万次请求 的高额价格甚至远远高于使用 Lambda 的价格。
AWS Lambda 对于低计算复杂度、低流量的应用是有着绝对的价格优点的。可是当部署在 Lambda 上的函数复杂度与流量逐渐上升的时候,使用 Lambda 的成本是有可能在某一时间点超过传统云主机的。就比如使用 Lambda 是租车,而使用传统云主机是买车。但从另外一角度看,使用 Serverless 服务又能够节省必定的开发与运维成本。所以对于“Serverless 与传统云主机谁更节省成本”这个问题,不只与具体开发的应用类型有关,也与应用的开发模式密不可分,该问题的真正答案极可能只有经过精密的成本计算与实践才能得出。
虽然 Lambda 有以上值得权衡的问题,但它所带来对于开发效率的提升是前所未有的,它所带来对于服务器及运维层面的成本削减也是肉眼可见的。全面而且客观地了解 Lambda 的长处与短处是决定是否使用它的必要步骤。目前许多的外国企业及开发者已渐渐开始拥抱与接纳 Serverless 的开发模式,尽管国内可能对于 Serverless 应用范围并非很广,尽早地了解与熟悉 Serverless 相信对于国内开发者来讲也是百利而无一害的。
在了解了 Serverless 与 AWS Lambda 后,接下来咱们就能够着手在 AWS Lambda 上开发应用了。
接下来时间内,咱们要在 Lambda 上部署一套在应用开发中较为常见的用户服务,主要有注册、登陆、与接口访问权限校验的功能,包含如下四个接口
/api/user/signup
- 建立一个新用户并录入数据库/api/user/login
- 登陆并返回 JSON Web Token 来让用户访问私有接口/api/public
- 公共接口,无须登陆的用户也可访问/api/private
- 私有接口,只有登陆后的用户才能访问这里的 Serverless 指的是一个在 GitHub 上超 过 3 万星的 CLI 工具。经过 Serverless CLI ,咱们能够快速生成 Lambda 应用服务模版,标准化、工程化服务的开发以及一键部署服务至多套的环境与节点,极大地缩短了服务开发至上线的时间。
根据 Serverless CLI 文档完成前两步
npm install -g serverless
咱们选择的语言是 JavaScript ,数据库是 AWS 提供的 DynamoDb ,从 Serverless CLI 的示例库中很快能够找到这样的模版 aws-node-rest-api-with-dynamodb
复制模版至本地做为起步工程
serverless install -u https://github.com/serverless/examples/tree/master/aws-node-rest-api-with-dynamodb
这个工程包含了一个 Todo 列表的 CRUD 操做服务。核心文件有:
serverless.yml
定义了该服务所提供的 Lambda 函数
、触发函数的 触发器
以及运行该函数所须要的其余 AWS 资源
。package.json
定义了该服务所依赖的其余库。todos
目录下包含了全部的函数文件。咱们能够看到函数都是较为直白的,每个文件都是相似如下的结构:
const AWS = require('aws-sdk'); // 引入 AWS SDK const dynamoDb = new AWS.DynamoDB.DocumentClient(); // 建立 dynamoDb 实例 // 经过 module.exports 来导出该函数 module.exports.create = ( event, // event 对象包含了关于触发该函数事件的信息,每一个触发事件 event 包含的信息都会有所不一样,具体可参阅文档 context, // context 对象包含了有关函数调用、执行环境等信息,具体可参阅文档 callback // callback 是一个方法,经过调用 callback 能够在非异步函数中发送响应信息,关于如何定义异步函数与在异步函数内如何发送响应信息可参阅文档 ) => { const data = JSON.parse(event.body); // 解析 event 来得到 http 请求参数 /* 业务逻辑 */ callback(); // 用提供的 callback 函数来发送响应信息 }
相关文档: event 文档, context 文档, 定义异步函数
当咱们运行 npm install
与 serverless deploy
将该起步工程部署到云端后,就能够经过 API 地址(例:https://xxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos)来运行和访问这些函数。
根据这个工程原有的函数 ,建立相似的函数文件并不是难事,咱们在工程中建立如下 4 个文件:
但仅仅建立函数文件是不够的,咱们须要同时在 serverless.yml
中为这几个函数添加定义。以 signup
函数为例,在 functions
中添加如下内容:
signup: handler: user/signup.signup #定义了函数文件的路径 events: - http: #定义了函数触发器种类为 http (AWS API Gateway) path: api/user/signup #定义了请求路径 method: post #定义了请求 method 种类 cors: true #开启跨域
这样咱们就完整地定义了 4 个函数。接下来咱们来看这四个函数具体的实现方法。
GET
返回一条无须登陆便可访问的信息
public
函数是 4 个函数中最为简易的一个,由于该函数是彻底公开的,咱们不须要对该函数作任何校验。以下,简单地返回一条信息即可:
// user/public.js module.exports.public = (event, context, callback) => { const response = { statusCode: 200, body: JSON.stringify({ message: '任何人均可以阅读此条信息。' }) } return callback(null, response); };
注:
callback
第一个参数传入的为错误,第二个参数传入的为响应数据。
执行如下命令来部署 public
函数
# 部署单个函数 serverless deploy function -f public
或
# 部署全部函数 serverless deploy
在浏览器中直接输入 API 地址或用 cURL 工具执行如下命令来发送请求(替换成你的 API 地址, API 地址可在运行 serverless deploy
后的 log
中或在 AWS API Gateway控制台
- 阶段(stage)
中找到)
curl -X GET https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/public
返回数据
{ "message": "任何人均可以阅读此条信息。" }
POST
建立一个新用户并录入数据库,返回成功或失败信息
signup
函数的运行须要 DynamoDB
这一资源,因此第一步咱们须要在 serverless.yml
文件中对 resources
进行以下修改来添加所须要的数据库资源
# serverless.yml resources: Resources: UserDynamoDbTable: Type: 'AWS::DynamoDB::Table' #资源种类为 DynamoDB 表 DeletionPolicy: Retain #当删除 CloudFormation Stack(serverless remove)时保留该表 Properties: AttributeDefinitions: #定义表的属性 - AttributeName: username #属性名 AttributeType: S #属性类型为字符串 KeySchema: #描述表的主键 - AttributeName: username #键对应的属性名 KeyType: HASH #键类型为哈希 ProvisionedThroughput: #表的预置吞吐量 ReadCapacityUnits: 1 #读取量为 1 单元 WriteCapacityUnits: 1 #写入量为 1 单元 TableName: ${self:provider.environment.DYNAMODB_TABLE} # 定义表名为环境变量中的 DYNAMODB_TABLE
resources
一栏中填写的内容是使用yaml
语法写的 AWS CloudFormation 的模版 。DynamoDB 表在 CloudFormation 中更为详细定义文档请参考 连接 。
signup
是一个方法为 POST
的接口,所以须要从 http 事件的 body
中获取请求数据。
// user/signup.js module.exports.signup = (event, context, callback) => { // 获取请求数据并解析 JSON 字符串 const data = JSON.parse(event.body); const { username, password } = data; /* ... 校验 username 与 password */ }
获取完了请求数据后,咱们须要构造出新用户的数据,并把数据录入 DynamoDB
// user/signup.js // 引入 NodeJS 加密库 const crypto = require('crypto'); // 建立 dynamoDB 实例 const AWS = require('aws-sdk'); const dynamoDb = new AWS.DynamoDB.DocumentClient(); module.exports.signup = (event, context, callback) => { // ...获取并校验 username 与 password // 生成 salt 来确保哈希后密码的惟一性 const salt = crypto.randomBytes(16).toString('hex'); // 用 sha512 哈希函数加密,生成仅可单向验证的哈希密码 const hashedPassword = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex'); const timestamp = new Date().getTime(); // 生成当前时间戳 // 生成新用户的数据 const params = { TableName: process.env.DYNAMODB_TABLE, // 从环境变量中获取 DynamoDB 表名 Item: { username, // 用户名 salt, // 保存 salt 用于登陆时单向校验密码 password: hashedPassword, // 哈希密码 createdAt: timestamp, // 生成时间 updatedAt: timestamp // 更新时间 } } // 将新用户数据录入至 dynamoDb dynamoDb.put(params, (error) => { // 返回失败信息 if (error) { // log 错误信息,可在 AWS CloudWatch 服务中查看 console.error(error); callback(null, { statusCode: 500, body: JSON.stringify({ message: '建立用户失败!' }) }); } else { // 返回成功信息 callback(null, { statusCode: 200, body: JSON.stringify({ message: '建立用户成功!' }) }); } }
DynamoDB 在 NodeJS 中更详细的 CRUD 操做文档请参考 连接
执行如下命令来部署 signup
函数
# 部署单个函数 serverless deploy function -f signup
或
# 部署全部函数 serverless deploy
用 cURL
工具执行如下命令来发送请求(替换成你的API地址,API地址可在运行 serverless deploy
后的 log 中或在 AWS API Gateway控制台
- 阶段(stage)
中找到)
curl -X POST https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/user/signup --data '{ "username": "new_user", "password": "12345678" }'
返回数据
{ "message": "建立用户成功!" }
GET
校验用户名密码并返回 JSON Web Token 来让登陆用户访问私有接口
在用户调用了 Login 接口并经过验证后,咱们须要为用户返回一个 JSON Web Token
,以供用户来调用须要权限的服务。设置 JSON Web Token 须要如下几步操做:
npm install jsonwebtoken --save
安装 jsonwebtoken
库并添加至项目依赖在项目中添加一个 secret.json
文件来存放密钥,这里咱们采用对称加密的方式来定义一个私有密钥。
// secret.json { "secret": "私有密钥" }
将私有密钥定义至环境变量,以供函数访问。在 serverless.yml
的 provider
下做以下变动
# serverless.yml provider: environment: # 使用serverless变量语法将文件中的密钥赋值给环境变量PRIVATE_KEY PRIVATE_KEY: ${file(./secret.json):secret}
login
是一个方法为 GET
的接口,所以须要从触发事件的 queryStringParameters
中获取请求数据。
// user/login.js module.exports.login = (event, context, callback) => { // 获取请求数据 const { username, password } = event.queryStringParameters; /* ... 校验 username 与 password */ }
// user/login.js const crypto = require('crypto'); const jwt = require('jsonwebtoken'); const AWS = require('aws-sdk'); const dynamoDb = new AWS.DynamoDB.DocumentClient(); module.exports.login = (event, context, callback) => { // ...获取并校验 username 与 password // 验证帐号密码并返回 JSON Web Token // 构造 DynamoDB 请求数据,根据主键 username 获取数据 const params = { TableName: process.env.DYNAMODB_TABLE, Key: { username } }; // 从 DynamoDB 中获取数据 dynamoDb.get(params, (error, data) => { if (error) { // log 错误信息,可在 AWS CloudWatch 服务中查看 console.error(error); // 返回错误信息 callback(null, { statusCode: 500, body: JSON.stringify({ message: '登陆失败!' }) }); return; } // 从回调参数中获取 DynamoDB 返回的用户数据 const user = data.Item; if ( // 确认 username 存在 user && // 确认哈希密码匹配 user.password === crypto.pbkdf2Sync(password, user.salt, 10000, 512, 'sha512').toString('hex') ) { // 返回登陆成功信息 const response = { statusCode: 200, body: JSON.stringify({ username, // 返回 username // 返回 JSON Web Token token: jwt.sign( { username // 嵌入 username 数据 }, process.env.PRIVATE_KEY // 使用环境变量中的私有密钥签发 token ) }) }; callback(null, response); } else { // 当用户不存在,以及用户密码校验错误时返回错误信息 callback(null, { statusCode: 401, body: JSON.stringify({ message: '用户名或密码错误!' }) }); } }); };
执行如下命令来部署 login
函数
# 部署单个函数 serverless deploy function -f login
或
# 部署全部函数 serverless deploy
在浏览器中直接输入 API 地址或用 cURL 工具执行如下命令来发送请求(替换成你的 API 地址, API 地址可在运行 serverless deploy
后的log中或在 AWS API Gateway控制台
- 阶段(stage)
中找到)
curl -X GET 'https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/user/login?username=new_user&password=12345678'
返回数据
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5ld191c2VyIiwiaWF0IjoxNTYxODI1NTgyfQ.Iv0ulooGayulxf_MkkpBO1xEw1gilThT62ysuz-rQE0", "username": "new_user" }
校验请求中所包含的 JSON Web Token 是否有效
在编写 private
函数以前,咱们须要提供另外一个函数 auth
来校验用户提交请求中的 JSON Web Token 是否与咱们所签发的一致。
user/auth.js
文件在 serverless.yml
的 functions
中添加如下内容:
auth: handler: user/auth.auth # auth 是一个仅会在服务内被调用的函数,所以没有任何触发器
为了可以让 AWS API Gateway
触发器正确地识别函数有无权限执行,咱们必须在 auth
函数中返回一个含 IAM ( AWS 服务与权限管控系统) 权限策略信息的响应数据,来使得有权限的函数能够经过 AWS API Gateway
成功触发。在 user/auth.js
内定义一个以下的方法:
// user/auth.js const generatePolicy = (principalId, effect, resource) => { const authResponse = {}; authResponse.principalId = principalId; // 用于标记用户身份信息 if (effect && resource) { const policyDocument = {}; policyDocument.Version = '2012-10-17'; // 定义版本信息 policyDocument.Statement = []; const statementOne = {}; statementOne.Action = 'execute-api:Invoke'; // 定义操做类型,这里为 API 调用操做 statementOne.Effect = effect; // 可用值为 ALLOW 或 DENY ,用于指定该策略所产生的结果是容许仍是拒绝 statementOne.Resource = resource; // 传入 ARN( AWS 资源名)来指定操做所须要的资源 policyDocument.Statement[0] = statementOne; authResponse.policyDocument = policyDocument; // 将定义完成的策略加入响应数据 } return authResponse; };
关于 IAM 策略更为详细的配置文档请查看 连接
咱们在解析 JSON Web Token 时默认请求遵循 OAuth2.0 中的 Bearer Token 格式。
// user/auth.js const jwt = require('jsonwebtoken'); /* ...定义 generatePolicy 方法 */ module.exports.auth = (event, context, callback) => { // 获取请求头中的 Authorization const { authorizationToken } = event; if (authorizationToken) { // 解析 Authorization const split = event.authorizationToken.split(' '); if (split[0] === 'Bearer' && split.length === 2) { try { const token = split[1]; // 使用私有密钥校验 JSON Web Token const decoded = jwt.verify(token, process.env.PRIVATE_KEY); // 使用 generatePolicy 生成包含容许API调用的IAM权限策略的响应数据 const response = generatePolicy(decoded.username, 'Allow', event.methodArn); return callback(null, response); } catch (error) { // JSON Web Token 校验失败,返回错误 return callback('Unauthorized'); } } else { // Authorization 格式校验失败,返回错误 return callback('Unauthorized'); } } else { // 请求头未含 Authorzation,返回错误 return callback('Unauthorized'); } };
GET
返回一条须要登陆才可访问的信息
private
函数的实现与以前的 public
函数十分相似,惟一的区别就是咱们须要在函数的 http (AWS API Gateway)
触发器中加入刚刚定义的 auth
做为权限校验函数。
在 serverless.yml
中对先前定义的 private
函数做以下变动:
# serverless.yml functions: private: handler: user/private.private events: - http: path: api/private method: get authorizer: auth #设置 authorizer 为刚刚定义的 auth 函数 cors: true
// user/private.js module.exports.private = (event, context, callback) => { // 从触发事件中获取请求的用户信息 const username = event.requestContext.authorizer.principalId; // 返回消息 const response = { statusCode: 200, body: JSON.stringify({ message: `你好,${username}!只有登陆后的用户才能够阅读此条信息。` }) } return callback(null, response); };
执行如下命令来部署 private
函数
# 部署单个函数 serverless deploy function -f private
或
# 部署全部函数 serverless deploy
用 cURL 工具执行如下命令来发送请求(替换成你的 API 地址, API 地址可在运行 serverless deploy
后的 log 中或在 AWS API Gateway控制台
- 阶段(stage)
中找到)
curl -X GET -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5ld191c2VyIiwiaWF0IjoxNTYxODI1NTgyfQ.Iv0ulooGayulxf_MkkpBO1xEw1gilThT62ysuz-rQE0" https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/private
返回数据
{ "message": "你好,new_user!只有登陆后的用户才能够阅读此条信息。" }
若是在教程中有疑点,能够在 Github 上查看完整的代码。
本文发布自 网易云音乐前端团队,欢迎自由转载,转载请保留出处。咱们一直在招人,若是你刚好准备换工做,又刚好喜欢云音乐,那就 加入咱们!