From Serverless to Functionless,不寫 Code 也能打造縮網址服務?
(Photo by Prateek Katyal on Unsplash)
Build a URL Shortener from scratch?
大家應該都曾經因為分享方便、想要追蹤點擊等目的使用過 Bit.ly 或是已關閉的 Goo.gl 網址服務。
自行打造 URL 縮網址小工具,對於任何有一定經驗的工程師來說,都並非難事,但當我們把「要好、要快、要便宜」的標準放入考量中時,一個簡單的縮網址服務也可以從數日、數週的挑戰變成需要數月的工作項目。
人是不習慣知足的動物,有了功能之後,我們會開始要更多:
- 服務需要使用者驗證、授權。組織內的服務可能有多個,最好這些服務的都能有 Single Sign On 的整合,同時我想要讓大家可以用慣用的 Google、Facebook、GitHub 等第三方 Oauth 完成身份驗證。
- 服務的響應速度能不能更快。
- 當有突然湧入的使用者,希望服務可以瞬間擴展到足以吃下所有的需求。反之,當沒有人使用我的服務的時候,能不能降低成本到趨近為零。
- 時間應該浪費在開發新功能、與其他有價值的美好事物上頭,需要定期費心維護的東西能不能再更少。
Monolith to Microservices
回溯過去,部署服務的起手式曾經是開一台虛擬機器,在裡面安裝好所有需要的環境。隨後由於開始需要確保服務可以服務跨更多區域的使用者、增加服務的穩定、減少需要耗費大量時間逐一 Patch 虛擬機器、資料庫或是 Application 的機會、並讓服務的 Application、Database、CacheWorker 等區塊可以分別按照需要調整資源。
同時現在多數的開發團隊都會使用混合的 Techstack,在同組織、甚至同產品內就會運用多種不同的語言、框架,為了方便可以單元式的開發所需要的模組,Monolith (單體式) to Microservices (微服務) 的概念曾紅極一時。
Serverless and FaaS (Function as a Service)
Serverless
講到微服務就不得不提到 Serverless (無伺服器運算) 概念。無伺服器運算的概念被視為是 Cloud Native (雲端原生架構) 的極致展現,雖然名字可能讓人誤解,無伺服器運算事實上仍然是倚賴著雲端伺服器進行,只是我們將大多數的管理成本進一步委派由大型雲端服務商來控管。
無伺服器運算透過盡可能的免除瑣碎、非必要的架構佈署、修正、維護工作,以最大化的減低管理成本,同時提高服務調整與擴展的敏捷度。同時由於可以隨需 (On Demand) 快速的調高與調低佈建的資源大小,可以最大化的減小總體擁有成本 (Total Cost of Ownership)。
如此一來,無伺服器運算得已讓隨時都一直處在人力吃緊狀態的開發團隊得以將心力投注於開發與產品的成長,使得開發優良、高可用 (High Availability)、可擴展 (Scalable) 的產品變得比以往都還要輕鬆。
FaaS (Function as a Service)
打造微服務的主流作法,不外乎是倚靠容器化 (Container) 技術、或是倚賴 FaaS (Function as a Service) 雲端服務架構。其中 FaaS (Function as a Service) 因為可以最大幅的降低管理成本 (Admin Overhead) 及學習成本而受到歡迎,三大雲端巨頭都分別推出了對應的指標性產品,包含常見的 Google Functions、AWS Lambda、Azure Functions。
FaaS 透過將服務功能重新拆解,以 Function (函式) 為執行基礎單位,達成只需為各函式運行時間成本支付費用的夢想,同時由於 Function 必須為 Stateless,這使得彈性擴展變得彈指般容易,若妥善搭配全受管 (Fully-Managed) 服務,可以同時達成高可用服務架構的水準。
Serverless to Functionless
由於 FaaS 將管理成本交付給雲端服務營運商、可以快速調節佈建資源,並同時能達成高可用的要求,不少組織開始大量的採納 Serverless 服務。但事情總是不會如想像般美好,從 Monolith 架構轉換至擁抱 Microservices、進而採用 Serverless 架構,所需要的遠遠不只是把服務打散成 Function、搬移到雲端運營商,未經深思熟慮的貿然遷移,往往為後需服務的擴張留下不可控制的技術債。
表面上看來,Microservices 架構讓你把服務變成多個小單元、每個小單元能夠獨立的擴展、維護,好像很容易分而治之 (Divide and Conquer),然而隱含的風險是,如果多個小單元處置不善,那其實也就只是在自己的服務中佈下滿地的致命地雷。
問題出現之後,我們學著理解到無伺服器、高效的調節、低廉的成本、新潮的技術架構其實都不是核心,讓大家「專注」在能夠打造更好、更穩定的核心服務體驗,才是技術演進的價值所在。Functionless 的概念就在這樣的反思中應運而生。
Functionless 試圖回歸到服務的核心,開發者們應該盡可能將「專注」放在讓服務與需要上面、應該要盡可能用「事半功倍」的方法達成目的,可能的話,最好連 Function 都別 Provision 就能打造服務。
案例探討:AWS Samples - Functionless URL Shortener
沒有了 Function,那後端邏輯在哪、誰來負責答覆請求?讓我們先回到我們的主題:「Functionless 縮網址服務」上頭。
AWS Compute Blog 曾在 2016 年發表了《Build a Serverless, Private URL Shortener》一文,透過 Lambda、API Gateway、S3、CloudFront 等服務,就可以還算輕鬆的完成一個簡單的私有縮網址服務,如果一個月產生 1,000 個縮網址、每個縮網每月收到 1,000 個請求的話,一個月的費用不到 5 塊錢台幣。
四年過去,今年初 AWS Compute Blog 又發表了《Building a serverless URL shortener app without AWS Lambda》系列文,透過 AWS Amplify, Cognito, API Gateway, DynamoDB, S3 等重新完成縮網址服務。
在我們繼續往下以前讓我們先對 AWS 的 Serverless 生態圈重點服務有基本的認識。
AWS Serverless 無伺服器生態系
- AWS Lambda:FaaS 重點服務,可以透過 Ruby, Go, Python, Node, Java, C# 等語言撰寫 Function,透過「事件」驅動 Function 執行,可以輕易的平行運行 Function
- API Gateway: 讓你可以建立不同的 HTTP API 並連結到不同的後端
- Cognito:使用者 (User Pool) 驗證服務
- DynamoDB:Key Value NoSQL 服務
- S3:靜態資源儲存服務
- CloudFront: CDN 服務
部署 API
為了方便近距離感受 Functionless 的特性,讓我們來部署一下 AWS Samples 中的 Functionless URL Shortener 吧!Repo 的 README 中有詳細的部署指引。
- 部署之前你需要確保你的電腦中已經有 AWS CLI、以及 AWS SAM CLI
- Fork Functionless URL Shortener Repo
- 建立一組 GitHub 的 Personal Access Token,並賦予該 Token 所有 Repo Scope 底下的權限
- 將 Repo Clone 到你的電腦上並導引到對應專案目錄
- 執行
sam deploy -g
初始化部署,依據提示完成選項的選擇,並進行部署 - 由於過程涉及 CloudFront 的設定,可能會需要至多約 30 分鐘左右才能完成部署,若部署過程比想像中耗費時間,不需過於擔心
部署前端
上面的步驟幫你完成了 API 的部署,在這個 Sample App 中還包含了以 Vue.js 撰寫的前端部署,讓我們接著來完成前端介面的部署。這部分同樣在 README 中有詳細的部署指引。
- 到 Amplify Console 中找到剛剛建立的 Apps
- 依據 API 部署後的回傳訊息完成以下環境參數設定
- VUEAPPNAME
- VUEAPPCLIENT_ID
- VUEAPPAPI_ROOT
- VUEAPPAUTH_DOMAIN
- 回到 Amplify Console 針對對應的 APP 執行 Run Build 完成初次部署,整個部署就完成了!
- 試著瀏覽剛剛建立出來的 APP 建立短網址。
What’s special
讓我們已這個 2020 年 2 月發布的 Functionless URL Shortener Sample Project 與 2016 年的 Private URL Shortener 進行比較。
Apache VTL (Velocity Template Language) instead of Lambda functions
首先最大的差異就是 Functionless!綜觀整個服務中我們會看到完全沒有 Function 存在。
URL Shortener 的核心邏輯並不複雜,任何開發者對於這樣的邏輯應該都十分熟悉
- 你會有一個簡短的 URL Slug 對應至原始的 URL
- 服務的使用者應該可以快速的貼上 URL,並觸發 API
- API 觸發後,需要儲存 URL Slug 與原始目標 URL 對應
- 當其他使用者瀏覽縮短後的網址時,應該要被轉址到原始目標 URL
在這個過程中,收到的縮網址請求內容,與實際需要存進資料庫的部分並未相差太多。當使用者瀏覽短網址時,需要回覆的內容也與資料庫所儲存的資料並未差異太大,這麼簡單的 CRUD 層級的任務,難道還非得要部署幾個 Function 來解決他們?翻找一下 Repository 的內容,你還真找不到對應的 Function。
AWS 不斷的在演進自家服務的進展,原本透過 API Gateway 收到請求後,我們會想要將請求事件交由 Lambda 處理,但 API Gateway 多了對 Apache VTL (Velocity Template Language) 的支援後,我們可以透過 Apache VTL 進行簡單的請求內容轉化後,直接在 DynamoDB 建立對應的請求。
舉例來說,一個簡單的建立 URL 請求:
{
"id": "aws",
"url": http://aws.amazon.com,
"timestamp": "27/Dec/2019:21:21:17 +0000",
"owner": "ericdj@amazon.com"
}
我們可以透過在 API Gateway 建立以下的 VTL 宣告:
#set($inputRoot = $input.path('$'))
{
"id":"$inputRoot.Attributes.id.S",
"url":"$inputRoot.Attributes.url.S",
"timestamp":"$inputRoot.Attributes.timestamp.S",
"owner":"$inputRoot.Attributes.owner.S"
}
將原本的使用者請求轉換為 DynamoDB 的建立請求:
{
"id": {"S": "aws"},
"owner": {"S": "me@mydomain.com"},
"timestamp": {"S": "27/Dec/2019:21:21:17 +0000"},
"url": {"S": "http://aws.amazon.com"}
}
若遇到 Slug 重複的情形,我們可以透過在 API Gateway 建立以下的 Apache VTL 設定,將 DynamoDB 的 ConditionalCheckFailedException
錯誤,轉換成回應錯誤回應:
#set($inputRoot = $input.path('$'))
#if($inputRoot.toString().contains("ConditionalCheckFailedException"))
#set($context.responseOverride.status = 200)
{"error": true,"message": "URL link already exists"}
#end
Leverage Services instead of libraries / customized solutions
在這個 Sample 中,我們可以看到比起 2006 年的 Private URL Shortener 服務,這個服務有更好的使用者介面、更完整的驗證程序,然而實際上卻沒新增太多的程式碼,從很多角度來看反而簡化了許多需要維護的程式碼。
在新的 Sample 中以 Cognito 完成了服務驗證、以 Vue.js 完成前端部署至 S3 中,所有的服務仍然都以全受管服務打造,你不需要花太多精力在調整伺服器及其他架構,只需要專注於整個服務的核心邏輯身上。
過往我們會透過 Rails, Express, Django 這樣的全端框架來完成從 Routing, Request Handing, Database ORM, Caching, Monitoring 等功能,在這個示範架構中我們最大化的利用雲端服務來來達成所有的功能,唯一需要專注維護的程式碼只有縮網址的核心邏輯。
Architecture as Code
在這個 Sample App 的架構中,利用 AWS SAM (Serverless Application Model) 來定義需要運行服務的整個架構,任何人只需要透過簡單的 sam deploy
指令便能輕鬆複製出一模一樣的架構,架構以 YAML 檔的形式儲存,任何需要維護架構的人都不再需要進 AWS Console 花好幾天的時間想辦法追溯出整個架構的運行狀況,如果需要災難復原 (Disaster Recovery) 時也能在最短的時間內重新還原服務架構。
No waste
在這個範例中,所使用的服務全都是 Fully Managed Service,只需要告訴 AWS 你所需要的用量跟如何連結各個服務,部署的資源跟費用會隨著需要動態調節,真正的把每一毛錢都花在刀口上,不需要佈建過多不需要的容量,也不需要時時刻刻擔心突然增長的流量會不會讓服務垮掉。
結論
就以上 Sample Apps 的範例中,我們可以從中觀摩 Serverless、甚至 Functionless 架構的幾個重點訴求:
Offloading tasks,Less is more
過往我們習慣透過一個全端的 Application Framework 來包辦工作,透過 Vertical Scaling、Hortzontal Scaling 的方式來擴展佈建容量,Serverless 的思維模式讓你將關注重新放回重要的核心功能中,將可以委派的工作交給雲端服務來處理,達成「事半功倍」的成果。
Infrastructure as Code / Architecture as Code
在這個案例中,我們也見識到了透過文字 Config 來描述架構的好處,透過 Infrastructure as Code 甚至是 Architecture as Code 的方式,我們得以完整的分享架構的知識、甚至是輕鬆的重建整體服務架構。
Ensure scalability of your stacks
同時我們可以觀摩透過 AWS Lambda、DynamoDB、S3、Cognito 這些服務,我們得以完全依照流量所需來佈建所需要的資源,輕鬆就能應對突來的流量增長。
Optimize your Costs
Scale in 跟 Scale out 同樣的重要,透過 Serverless 的原則,我們可以確保當服務處於沒有人使用的時期,所消耗的資源可以自動維持在最低幅度,節省成本,將資本用在真正需要的地方。
References
- AWS Compute Blog《Build a Serverless, Private URL Shortener》
- AWS Compute Blog《Building a serverless URL shortener app without AWS Lambda》
- A Cloud Guru on Medium《Serverless is a state of mind》
- Stackery《The Future of Serverless is… Functionless?》
- Lego Engineering on Medium《Don’t wait for Functionless. Write less Functions instead》