引用一段對於 Serverless 較爲官方的定義:「Serverless 是一種執行模型(execution model)。在這種模型中,雲服務商負責經過動態地分配資源來執行一段代碼。雲服務商僅僅收取執行這段代碼所須要資源的費用。代碼一般會被運行在一個無狀態的容器內,而且可被多種事件觸發( http 請求、數據庫事件、監控報警、文件上傳、定時任務……)。代碼經常會以函數(function)的形式被上傳到雲服務商以供執行,所以 Serverless 也會被稱做 Functions as a Service 或者 FaaS。」html
從定義中不難看出 Serverless 的出現免去了工程師在開發應用時對服務器與後端運維的考量,使工程師能夠全心全意地投入業務邏輯代碼的實現中去。再歸納一下 Serverless 老生常談的幾條優點:前端
Serverless 對於前端開發者來講主要會有如下幾個應用場景node
AWS Lambda 是由亞馬遜雲服務平臺( AWS )最先推出於 2014 年、最爲著名的 Serverless 雲計算平臺之一。相較於其餘雲服務商,AWS Lambda 以完善的設施(觸發器種類多、支持編程語言多……)和豐富的社區支持在多數評測中佔據了上風。從體驗與學習的目的出發,AWS Lambda 能夠說是咱們的不二選擇。git
冷啓動是最常被提到的問題之一,用簡單的話來講就是當你的函數一段時間未被運行後,系統就會回收運行函數的容器資源。這樣帶來負面影響就是,當下一次調用這個函數時,就須要從新啓動一個容器來運行你的函數,結果就是函數的調用會被延遲。來看一下在 AWS Lambda 上函數調用時間間隔與冷啓動機率的關係:github
那麼具體的延遲時間是多少呢?延遲時間受許多因素的影響,好比編程語言、函數的內存配置、函數的文件大小等等。可是一個較爲廣泛的建議就是 Lambda 不適合用做對延遲極其敏感的服務(< 50ms)。web
在使用 AWS Lambda 開發應用時,咱們所寫的代碼與 AWS 這個雲服務商是具備強關聯性的。儘管目前有一些框架(例以下文會應用到的 Serverless 框架)來幫助咱們抹平不一樣服務商之間的代碼差別,想從一個服務商遷移至另外一個服務商依然是一件繁重的體力勞動,甚至包含着必定的代碼重構。數據庫
函數式的開發模式註定了代碼之間的複用與共享會成爲一個難題,也註定了代碼量會隨着服務的增多而膨脹。 Serverless 會使得函數數量與代碼量呈線性增加的關係,以下圖npm
當服務達到必定數量後,維護一個無限膨脹的代碼庫所須要的額外人力與開支也是不可小視的。編程
運行 AWS Lambda 的函數依賴於許多外部的庫和應用(aws-sdk、API Gateway、DynamoDB...),所以想要在一個徹底本地的環境運行這樣的函數是十分困難的。若是咱們每次修改函數後都須要部署並依賴於 AWS CloudWatch 中輸出的運行日誌來調試與開發 Lambda 函數,那想必效率是極低的。 Serverless CLI
對於本地開發難的問題,提供了必定的插件來支持(serverless-offline、serverless-dynamodb-local...)。不使用 Serverless CLI
開發 Lambda 的用戶可能就須要研讀官方提供的 AWS-SAM 文檔來配置本地開發環境了。json
AWS Lambda 收費的最小單位是 100ms ,也就是意味着你的函數哪怕只執行了 1ms 也會看成 100ms 來計費。這種計費方式在某些狀況下甚至會致使使用高內存的函數比低內存的要便宜。咱們來看下 AWS Lambda 的計費表:
舉一個較爲極端的例子:假設咱們設置了一個內存爲 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 應用服務模版,標準化、工程化服務的開發以及一鍵部署服務至多套的環境與節點,極大地縮短了服務開發至上線的時間。
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 地址(例: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 上查看完整的代碼。
本文發佈自 網易雲音樂前端團隊,歡迎自由轉載,轉載請保留出處。咱們一直在招人,若是你剛好準備換工做,又剛好喜歡雲音樂,那就 加入咱們!