從此之後,你不會再碰到 AWS Lambda ,他已經展開自己生命中的新篇章了。我們讓 AWS Lambda 在這邊接受最完善的照顧,享有最充裕的資源,用愛與包容的方式完成 AWS Lambda 各式各樣的設定。
「我想要 AWS Lambda。」他嘶吼著:「我想要親手完成那些設定!」
你確定你想嗎?
~節錄自《而 Netlify 的回音蕩漾》
經過了 2 篇文章的深入淺出 (?) 的討論,相信大家對於 Netlify 這個提供雲端運算的公司是越來越熟悉了,在學會使用 Netlify 各種佈署方式之後,我們也將要迎來這一個系列文章的重頭戲:佈署 Line Bot 囉。前面的介紹當中,我們都是用 Netlify 來佈署前端網頁 / 服務。然而,要讓 Line Bot 順利運作,我們需要的,卻是一個位於後端的 Webhook 伺服器 (詳見第 36 天)。Netlify 有提供能夠架設在後端的伺服器服務嗎?有的,Netlify 所給出的答案就是 Netlify Functions。
要說 Netlify Functions 是屬於位於後端的伺服器服務,似乎不太精確。正確來說,Netlify Functions 是屬於近年來相當火紅的後端服務型態:無伺服器運算服務 (Serveless Framework),或者說,就是所謂的函式即服務 (Function as a Service, FaaS)。同樣屬於後端的服務,伺服器服務跟這種新崛起的函式即服務,最大的差別就是資源的利用率以及我們程式設計師收到的帳單上面。絕大多數的情況下,和傳統的伺服器服務相比,函式即服務可以說是輕薄短小且免費。
而 Netlify 搭著這一陣函式即服務的潮流,順勢推出了以 AWS Lambda 為主體的 Netlify Functions。或許有人會好奇:什麼是 AWS Lambda?AWS Lambda 是亞馬遜雲端服務 (AWS) 所推出的函式即服務功能。那或許大家又會接著問下去:為什麼不用 AWS Lambda 就好,反而要特地跑來 Netlify,用 Netlify 包裝起來的 Netlify Functions 呢?AWS Lambda 首先是 AWS 所提出的功能,接著又被 Netlify 包裝起來,變成了 Netlify Functions。能不能給我翻譯翻譯,什麼叫做包裝?
包裝就是,從此以後,我們不用再碰 AWS 過於繁瑣複雜的設定流程和收費方式,只要用 Netlify 的規則來撰寫 / 發佈函式就行!
當然,也不是說 AWS 自身所提供的架構不好。AWS 提供了一個富有彈性,可根據需求進行擴充的完善架構。AWS Lambda 具有可調整函式使用資源 (記憶體大小、執行時間) 的自由,同時也可再連接資料庫,設定定時事件等等。有捨就有得,我們捨去了 AWS Lambda 的彈性,換得 Netlify Functions 的簡潔。
【註】
很可惜的一點是,雖然 AWS Lambda 提供以 Python 撰寫程式碼的選項,但 Netlify Functions 目前只支援 JavaScript、TypeScript、以及 Go 這三種語言。也因此《賴田捕手:番外篇》當中是以 JavaScript 來寫 LineBot。
好的,說了這麼多,讓我們試著來佈署 Netlify Functions 吧。
這邊就讓我們試著用功能完善,親切易懂的 Netlify CLI 來幫我們建立 Netlify Functions 吧!
安裝 Netlify CLI:
npm install netlify-cli -g
若各位的電腦當中,還沒安裝好 Netlify CLI,可以透過上述指令進行全域安裝。安裝完成後,可以透過netlify --version
來查看目前版本:
netlify --version
netlify-cli/3.37.17 win32-x64 node-v14.15.5
我的 Netlify CLI 版本號是 3.37.17。
netlify.toml
檔 還記得 Netlify 專屬的參數設定檔netlify.toml
嗎?假設我們目前的專案資料夾是netlify-functions-demo
,為了要告訴 Netlify 我們想要佈署成無伺服器運算的檔案放在哪裡,在專案資料夾當中的netlify.toml
需要這麼寫:
netlify-functions-demo/netlify.toml
[build]
functions="my_functions"
這樣寫的意思是,我們要作為 Netlify Functions 佈署成無伺服器服務的函式,就放在my_functions
這個資料夾當中。
整個專案資料夾的架構看起來如下:
netlify-functions-demo
└───netlify.toml
好的,前置作業都做完了。看起來很空虛是嗎?沒問題,Netlify CLI 要來幫我們變魔術了。
有了netlify.toml
檔案,Netlify 就知道該去哪邊找我們寫下的 Netlify Functions 並進一步去佈署它們了。不過等等,我們根本還沒寫下任何 Netlify Functions 啊?試著輸入指令netlify functions:create 你-Functions-的名字
:
netlify functions:create 你-Functions-的名字
◈ functions directory netlify-functions-demo\my_functions does not exist yet, creating it...
◈ functions directory netlify-functions-demo\my_functions created
看到了嗎?在讀了netlify.toml
檔案之後,Netlify CLI 很警覺的發現我們還沒有netlify-functions-demo/my_functions
這個資料夾,於是 Netlify CLI 很貼心的幫我們創造了一個。
接著,Netlify CLI 會繼續詢問我們需要的 Netlify Functions 是哪一種。根據我們的回答,Netlify CLI 會進一步幫我們創造出適合的初始化樣板。可能的選擇包括有:
node-fetch
的函式當然,Netlify CLI 所提供的選擇可能因為版本號不同而相異。不過,顯而易見,陽春版不管是哪一個版本號都應該要有的,對我們寫 Line Bot 來說也挺足夠的,因此選陽春版就可以了。
netlify functions:create 你-Functions-的名字
◈ functions directory netlify-functions-demo\my_functions does not exist yet, creating it...
◈ functions directory netlify-functions-demo\my_functions created
? Pick a template js-hello-world
◈ Creating function 你-Functions-的名字
◈ Created netlify-functions-demo\my_functions\你-Functions-的名字\你-Functions-的名字.js
現在,我們的專案資料夾看起來會像這樣:
netlify-functions-demo
├───netlify.toml
└───my_functions
└───你-Functions-的名字
└───你-Functions-的名字.js
看一下 Netlify CLI 幫我們創造的陽春版 Netlify Functions 長什麼樣子吧:
【註】
此樣板亦可能因 Netlify CLI 版本號不同而有所差別。
netlify-functions-demo/my_functions/你-Functions-的名字/你-Functions-的名字.js
// Docs on event and context https://www.netlify.com/docs/functions/#the-handler-method
const handler = async (event) => {
try {
const subject = event.queryStringParameters.name || 'World'
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello ${subject}` }),
// // more keys you can return:
// headers: { "headerName": "headerValue", ... },
// isBase64Encoded: true,
}
} catch (error) {
return { statusCode: 500, body: error.toString() }
}
}
module.exports = { handler }
對於函式即服務不太熟悉的朋友,可能不知道這些程式碼是在做什麼,又是如何取代傳統伺服器的服務。因此我稍微說明一下。一般所謂的伺服器,最主要的工作就是處理前端使用者發送過來各式各樣的請求。而無伺服器運算服務,就是改以函式取代伺服器,使用者發送過來的請求 (request) 就等同於準備輸入函式的變數,而使用者應該要拿到的回應 (response),就等同於函式根據變數內容運算的結果。因此,當使用者向某個網址發送請求時,該請求會被轉為變數,呼叫代表該網址的函式,函式根據變數內容運算,運算結果轉為回應的方式傳回給使用者。
注意到我這邊說了個「轉為」,因為跟傳統的伺服器還是有所不同,使用者發送過來的請求被「轉為」事件 (event),而事件才是真正輸入函式的變數。此外,由於 Netlify Functions 的真實身分,其實是 AWS Lambda (其實是還要再加上 Amazon API Gateway,不過這邊我們就不說的這麼複雜),所以這份程式碼的撰寫方式,要遵守 AWS Lambda 的規範。規範有哪些呢?
AWS Lambda 預設函式名稱及變數
在 AWS Lambda 當中,使用者發出請求,來到 AWS Lambda 後,會觸發 (呼叫) 的函式,其預設名稱是handler
。所以不要動到代表函式的變數名稱handler
。此外,該函式接收三個變數,詳細大家可以研究 AWS Lambda 的官方文件。但大多數的情況下,最重要的只有第一個變數,也就是包含了使用者請求資料的event
這個變數。
AWS Lambda 事件內容
那麼,包含了使用者請求資料的event
這個變數,實際上有哪些資料呢➀:
{
"path": "Path parameter",
"httpMethod": "Incoming request’s method name"
"headers": {Incoming request headers}
"queryStringParameters": {query string parameters }
"body": "A JSON string of the request payload."
"isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode"
}
path
:使用者請求的原始路徑httpMethod
:使用者請求的方法 (GET, POST 等等)headers
:使用者請求的標頭queryStringParameters
:使用者請求時的查詢字串body
:使用者請求的主要內容isBase64Encoded
:請求內容是否採用 Base64 編碼。了解了這些背景之後,我們再回頭來看一下陽春版 Netlify Functions 的預設內容:
const handler = async (event) => {
try {
const subject = event.queryStringParameters.name || 'World'
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello ${subject}` }),
// // more keys you can return:
// headers: { "headerName": "headerValue", ... },
// isBase64Encoded: true,
}
} catch (error) {
return { statusCode: 500, body: error.toString() }
}
}
module.exports = { handler }
第一行:const handler = async (event) => {
建立 Async/Await 函式handler
,並接收代表事件的event
這個函式變數。
第三行:const subject = event.queryStringParameters.name || 'World'
利用event.queryStringParameters
來取得查詢字串。若使用者請求時,有查詢字串鍵 (key) 為name
的值,將該值賦予變數subject
,若無,則將'World'
賦予變數subject
。
第五行:statusCode: 200,
若一切順利,傳回代表成功的 HTTP 狀態碼200
。
第六行:body: JSON.stringify({message:
Hello ${subject}}),
並且用 JSON 當作回應的內容。
第十二行:return { statusCode: 500, body: error.toString() }
若出了任何狀況,傳回代表伺服器出錯的 HTTP 狀態碼500
。
好啦,都理解之後,我們就可以用 Netlify Dev 來佈署看看。還記得要怎麼做嗎?
netlify dev
用netlify dev
在本機端模擬 Netlify 佈署結果。
圖一、用netlify dev
在本機端模擬 Netlify 佈署結果
咦,失敗了嗎?當然不是。我們根本就沒有做什麼可以佈署在前端的網頁,Not Found 是再正常不過的結果了。但我們至少有做一個放在後端的無伺服器運算函式,我們該向哪一個路徑發出請求,才能呼叫 (觸發) 這個函式呢?
答案是/.netlify/functions/你-Functions-的名字
!
圖二、向/.netlify/functions/你-Functions-的名字
發送請求。在這個示範當中,我幫我的 Netlify Functions 取了一個好聽的的名字helloWorld
。
這什麼奇怪的路徑?沒搞錯吧。還記的我們陽春版 Netlify Functions 有一個查詢路徑的相關把戲,讓我們用查詢字串試試。/.netlify/functions/你-Functions-的名字?name=ithomemhjao
圖三、向/.netlify/functions/你-Functions-的名字?name=你想試試看的-value
發送請求
原來是真的!我們的無伺服器服務 Netlify Functions 就放在/.netlify/functions/你-Functions-的名字
這裡了。
想必不少人覺得/.netlify/functions/你-Functions-的名字
這是什麼獵奇的路徑,居然要向這種路徑發出請求才能找到我們佈署的 Netlify Functions。當然,這可能會方便 Netlify 進行管理以及佈署,不過嘛,這樣子的路徑不符合大家的審美觀嘛。
沒有問題。還記得 Netlify 所推出路由相關的服務 Netlify Redirects 嗎。可以說 Netlify Functions 就是為此而生的。讓我們用netlify.toml
檔案來示範:
netlify-functions-demo/netlify.toml
[build]
functions="my_functions"
[[redirects]]
from = "/callback"
to = "/.netlify/functions/你-Function-的名字"
status = 200
圖四、向/callback?name=你想試試看的-value
發送請求
這樣是不是比較漂亮呢?
終於來到這裡了。既然我們連 Netlify Functions 的運作方式都清楚了,那麼就來寫一個可以放在 Netlify Functions 上面的 Line Bot 吧!
netlify-functions-demo/my_functions/lineBotWebhook/lineBotWebhook.js
const line = require('@line/bot-sdk')
const handler = async (event) => {
// 取得環境變數
const clientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET,
};
// 用 CHANNEL_ACCESS_TOKEN 和 CHANNEL_SECRET 初始化 Line Bot
const client = new line.Client(clientConfig);
// 為 Webhook 驗證做準備
const signature = event.headers['x-line-signature'];
const body = event.body;
// Line Bot 運作邏輯
const handleEvent = async (event) => {
if (event.type !== 'message' || event.message.type !== 'text') {
return Promise.resolve(null)
}
const { replyToken } = event;
const { text } = event.message;
// Create a new message.
const response = {
type: 'text',
text: `而 Netlify 的回音盪漾著:${text}~`,
};
await client.replyMessage(replyToken, response)
}
try {
// 用 CHANNEL_SECRET 來驗證 Line Bot 身分
if (!line.validateSignature(body, clientConfig.channelSecret, signature)) {
throw new line.exceptions.SignatureValidationFailed("signature validation failed", signature)
}
// 將 JSON 轉為 JavaScript 物件
const objBody = JSON.parse(body);
// 將觸發事件交給 Line Bot 做處理
await Promise.all(objBody.events.map(handleEvent))
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from Netlify" }),
}
} catch (error) {
console.log(error)
return { statusCode: 500, body: error.toString() }
}
}
module.exports = { handler }
第一行: const line = require('@line/bot-sdk')
引入需要用的模組line
。
第三行:const clientConfig = {
準備好 Line Bot 的身分證,包括CHANNEL_ACCESS_TOKEN
以及CHANNEL_SECRET
。這兩個變數,通常不會用明碼的方式直接寫在程式碼當中。後端服務的好處是,像這種不想寫在程式碼當中的資料,可以考慮放在環境變數 (Environment variables) 當中。Netlify 當然也有提供儲存環境變數的方法。只要來到各位的 Netlify 控制面板,【Deploy】分頁,【Deploy settings】,【Build & deploy】->【Environment】,如圖五、圖六,就可以輸入想要用的環境變數了。
圖五、【Build & deploy】->【Environment】
圖六、Environment variables
第七行:const client = new line.Client(clientConfig);
用 CHANNEL_ACCESS_TOKEN 和 CHANNEL_SECRET 初始化 Line Bot。
第八行:const signature = event.headers['x-line-signature'];
從event.headers
當中拿出 Line 請求所特有的標頭'x-line-signature'
。這個標頭是稍後要跟CHANNEL_SECRET
做交互對照的,既是 Line Bot 要驗明正身,同時也要確認請求來自 Line 官方平台。
第十行:const handleEvent = async (event) => {
我們 Line Bot 的運作邏輯,這邊就不詳細說明了。
第二十三行:if (!line.validateSignature(body, clientConfig.channelSecret, signature)) {
前面提過,我們除了需要先確認 Line Bot 的身分之外,也要確定請求是從 Line 官方平台發送而來。而驗證的方法,就是利用validateSignature()
這一個函式。該函式接收 3 個變數,包括body
、CHANNEL_SECRET
、以及signature
。大致上的運作方式是,body
經過CHANNEL_SECRET
的編碼之後,需要等於 Line 官方送過來的signature
。
第二十六行:const objBody = JSON.parse(body);
通過身分認證後,就將送過來的事件內容交給 Line Bot 處理。不過,首先要將 JSON 轉成 JavaScript 可以處理的物件 (Object),大致上看起來會像下面這樣,而詳細內容可以參考 Line 官方文件➁。
{
"destination":"代表 Line Bot id 的一串文字",
"events":[
{
"type":"message",
"message":{"type":"text","id":"代表訊息 id 的一串文字","text":"使用者傳來的文字內容"},
"timestamp":1624805388143,
"source":{"type":"user","userId":"代表使用者 id 的一串文字"},
"replyToken":"0477f7971ebc4b1a998e196a9323a9f1",
"mode":"active"
}
]
}
await Promise.all(objBody.events.map(handleEvent))
handleEvent
這個函式怎麼寫了。 再重新檢查一下我們的netlify.toml
檔案:
netlify-functions-demo/netlify.toml
[build]
functions="my_functions"
[[redirects]]
from = "/callback"
to = "/.netlify/functions/lineBotWebhook "
status = 200
因為我們需要安裝line
套件,所以記得要寫好package.json
檔案:
netlify-functions-demo/package.json
{
"name": "netlify-line-bot",
"version": "1.0.0",
"description": "",
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@line/bot-sdk": "^7.3.0"
}
}
"@line/bot-sdk": "^7.3.0"
這是一個非常簡單的 Line Bot。整個檔案架構看起來如下:
netlify-functions-demo
├───netlify.toml
├───package.json
└───my_functions
└───lineBotWebhook
└───lineBotWebhook.js
這樣我們就可以將這個專案資料夾佈署到 Netlify 上了。同時,也要在 Line Developers 的 Line Bot 設定面板上調整 Webhook URL,如圖七。詳細要注意哪些,可以參考第 31 天的內容。
圖七、設定好 Webhook URL
圖八、而 Netlify 的回音盪漾著:唷~
這一篇文章當中,我們了解到如何撰寫並佈署無伺服器服務 Netlify Functions,進一步透過無伺服器服務來架設我們的 Line Bot Webhook Server。文章當中提供了陽春版 Line Bot 的程式碼。剩下來的 2 篇文章,我們就要根據這個陽春版的 Line Bot,進行擴充和改裝,希望最後能夠完成我們的名片產生器 Line Bot。
➀ AWS Lambda event 官方文件
➁ Line Webhook Event Object 官方文件