关于serverless:小心-Serverless-IDCF

4次阅读

共计 9186 个字符,预计需要花费 23 分钟才能阅读完成。

一、技术乐观主义陷阱

技术具备商品属性,这是经常被咱们疏忽的一个事实。且不谈垄断之后带来的商业利益,一方面技术依赖市场的认可来彰显它的价值,另一方面技术还须要依附公众的反馈才得以欠缺本人,所以宏大的用户群体是它凋敝的基石,它须要尽可能的为人所知。无论你是想吸引更多的我的项目和开发者退出某个社区中,还是想让某个框架解脱石破天惊乃至怀才不遇,过程都务必依赖于大量的经营流动,其中不少也要倚靠背地大厂的资源投入。从近乎死于非命的 Silverlight 到近些年大火的 Flutter,无不遵循着相似的模式。

既然是面向公众的商品,商家必然会以利益相关者的姿势为其辩护和呐喊,这无可非议。但在此影响之下,当技术人员对某项技术进行调研或者在被动接管来自行业内的更新时,失去的信息会人不知; 鬼不觉的向踊跃侧偏移,这对技术人员来说未必是好事件。因为咱们很难分辨感官里的哪一些是事实,哪一些是观点,哪一些是有条件成立,更重要的是还有哪一些是它没有通知你的。

Serverless 就是其中一个例子。

这篇文章不是对 serverless 的批评。Serverless 是云原生架构(Cloud Native)下瓜熟蒂落的必然产物,从 IaaS(Infrastructure as a Service)到 Paas(Platform as a Service)甚至再到 Saas(Software as a Service),咱们看到的是运维能力一直外包的迁徙过程,这有助于塑造精锐团队专一于交付业务价值以及灵便应答市场变动——为什么咱们要千篇一律的写登陆注册模块?如何能力将代码的保护老本降至最低?Serverless 便是在这些前提下诞生的。但 Serverless 只是其中一种解决方案(a solution),而非惟一的解决方案(the solution),更重要的是这篇文章会让你意识到它绝非是计划中的现实首选。

例如在每一篇介绍 serverless 的文章中,都肯定会提到因为冷启动缘故导致 serverless 函数具备较慢的首次响应工夫问题,但它们可能提供的信息通常到此便戛然而止了,这无奈给咱们带来任何帮忙,咱们也不会对它产生任何的警觉。如果我持续通知你不同供应商的提早各不相同,我所在我的项目中 Azure Serverless 的第一次启动提早能够长达 6 秒,那么我置信此时你会更谨慎的对待这条信息,并开始升高对于它作为 web server 的预期。

本文想强调的另一点是,尽管 serverless 看似是近几年才诞生的“新”技术,但它背地遵循最佳实际仍然是“旧”世界下人们早已达成的共识;在理论将它利用到现有产品的过程中,你须要关怀内容与前 serverless 时代也并无二致。例如在 OWASP 整顿出的无关 Serverless 排名前十的平安问题 中,我不认为有哪一则是 serverless 架构“独享”的。Serverless 与传统服务相比的劣势之一可能是前人的贵重教训被固化到了平台和产品状态之中,用以确保你不用再走弯路。

思考到通识性,本文次要应用 Azure 和 AWS 旗下的 serverless 服务对问题进行阐明

二、被鄙视的供应商锁定(vendor lock-in)

2.1 供应商的三道锁

供应商锁定在云原生架构下是无奈防止的问题,如果你抉择 Azure 作为你的云服务提供商,那么你大概率会顺带抉择 Azure Blob Storage 而不是 AWS S3 作为你的存储服务,因为来自于同一个供应商下的服务符合度更高,保护起来更容易。同样思考到老本和危险,自此之后更换服务的可能性也简直为零。

好在编程语言和编程框架仍然通用无界,加之容器化技术早已成熟,在开发惯例业务代码的方面供应商并没有给咱们造成太大的困恼。此时的供应商只充当主角,无论你是抉择 AWS EventBridge 还是 Azure Event Grid,背地 Event-Driven 的决策不会产生扭转,外围的业务代码不会受到影响,用 ExpressJS 写出的代码在不同服务商之间仍可复用。这种模式最显著的特点是业务人员能够分心开发业务代码,它们不必关怀公司购买的是哪家提供商的产品。尽管听起来有些反模式,但代码与环境的适配能够全权交给运维人员去解决的这条路是可行的。

而 serverless 模式恰恰相反,它的崛起像是一道命题作文,在概念后行的前提下不同的供应商依据本人现存基础设施优先推出本人的解决方案。对于这一点有意思的是,如果你当初去看市面上解说 serverless 的技术图书,书中谈及的概念和代码实施方案肯定是围绕某个繁多平台编写的。

serverless 中有一个很重要的概念正是这方面的体现:trigger.

顾名思义,trigger 是 function(本文的 function 泛指狭义各个云平台上 serverless 的实现代码,同时代指 Azure Function 和 AWS Lambda)的触发器,由它来负责启动 function。例如对于一个响应前端申请的 function 而言,http 申请就是它的 trigger。

但在 serverless 生态中,http 是最不重要的。你无妨回忆一下咱们最经典的 serverless 用例,离线创立略缩图:

在该流程中须要有 function 响应解决略缩图的音讯,在存储之后须要有 function 将数据更新进数据库中。其中的音讯服务和贮存服务就是 function 的 trigger。

此时不难发现当你开始编写 function 时,你须要确认你的云供应商提供这类服务的具体产品是什么,音讯服务在 Azure 中能够是 Azure Service Bus,然而到了 AWS 则变成了 Message Queuing Service。不同服务提供的 API 和模型不尽相同,同时代码与服务集成的形式也是量身定做的,这是第一层锁。

其次为了在 function 代码中拜访这类服务,裸写的代码是不被容许的,因为你须要在拜访服务时用指定的形式传递 API Key,通常解决这个问题的方法是间接集成供应商提供的 client SDK,比方 @azure/service-bus 或是 AWS SDK。事实上从接管到申请的那一刻起,代码差别就曾经注定了,尽管 Azure 和 AWS 都批准以 event handler 函数的模式来响应 trigger 的申请,但两者的函数签名差别显著,你能获得的函数所在的上下文也各有千秋。这是第二层锁。

这两者看上去仿佛把硬件和软件层面都笼罩到了,最重要的“隐形锁”却无形中被忽略了——那就是供应商的意志,即它们心愿你以什么样的形式去设计和编写 function。

以 API 架构为例,Azure 提供的服务比方 Azure Serverless 或者是 App Service 能够是互相独立的,哪怕你只购买其中的一项服务,你也能够独自为其配置 API Management, Identity 等属性。服务被容许对外裸露 HTTP 端口。在其官网给出的架构模式中,挪动端设施能够间接拜访 Azure Serverless 服务。

而在 AWS 中,服务的职责更为垂直,而非 Azure 般全能。HTTP 端点大多要被托管在 API Gateway 上,它为你提供了丰盛的性能,比方权限验证、日志监控、缓存等等。同样在 AWS 官网 给出的后端架构模式中,挪动设施的申请必须要通过 API Gateway。

在 Azure Serverless 中每一个 serverless 我的项目都有属于本人的配置文件 host.json,如果咱们想要限度 function 解决的最大申请数,你只须要批改该文件的配置项即可:

{
    "extensions": {
        "http": {
            "routePrefix": "api",
            "maxConcurrentRequests": 100,
            "customHeaders": {"X-Content-Type-Options": "nosniff"}
        }
    }
}

下面代码中的 maxConcurrentRequests 就能用来管制并发申请数。

而在在 AWS 中,对于同步的 HTTP 端申请,官网倡议你能够通过 API Gateway 限流性能(throtting)和设定 AWS WAF 规定来实现

这层锁的危害在于你必须从一开始就在供应商的框架内来设计本人的解决方案。在 AWS 中你当然能够不抉择 API Gateway 的 Lambda authorizer 性能作为 function 权限校验的解决方案,但我不确定其余路会让你绕多远。

即便你没有接触过 Lambda authorizer 也没有关系,我前面会有具体的解说。在前面的章节咱们也会看到,在埋怨它的同时咱们不得不抵赖它背地遵循的仍然是业内的最佳实际,咱们看似无路可选,但实际上咱们惟一能走的恰好是后任留下的捷径。

2.2 解“锁”

好消息是在这一层可见的危机背后咱们仍然有可能弛缓的余地。

2019 年 Thoughtworks 刚好公布了一篇对于如何防止 serverless 供应商锁定的文章 Mitigating serverless lock-in fears,文章从硬件到软件层面都给出了很多缩小迁徙老本的倡议。但在我看来其中最为实用的一则是:为程序设计一组好的架构。

尽管低入门门槛是 serverless 不争的卖点之一,然而它的天花板仍然能够达到传统技术栈程序雷同的高度,一脉相承的优良设计可给予前期保护上的便当。

例如一个对外发送邮件的用例首先采纳 Azure Serverless Function 编写,咱们在 httpTrigger 入口函数中能够间接援用 Azure SendGrid SDK 执行发送服务。

import * as SendGrid from "@sendgrid/mail";
SendGrid.setApiKey(process.env["SENDGRID_API_KEY"] as string);

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {

const email = {
  to: 'test@example.com', // Change to your recipient
  from: 'test@example.com', // Change to your verified sender
  subject: 'Sending with SendGrid is Fun',
  text: 'and easy to do anywhere, even with Node.js',
  html: '<strong>and easy to do anywhere, even with Node.js</strong>',
}

await SendGrid.send(email);
}

之后如果想将它迁徙至 AWS Lambda 的话,发送邮件局部须要齐全替换为调用 AWS 的 SES 服务:

import {SendEmailCommand}  from "@aws-sdk/client-ses";
import {sesClient} from "./libs/sesClient.js";

// Set the parameters
const params = {Destination: {},
  Message: {},};

const data = await sesClient.send(new SendEmailCommand(params));

但事实上咱们并不关怀谁在为咱们提供邮件发送服务,无论是 SendGrid 或者 SES 性能上并无差别。所以在设计这个程序时,咱们齐全能够提取一个公共的 email client,让 httpTrigger 入口函数调用 client 即可:

import emailClient from "./email-client";

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {

// ...
await emailClient.send(email);
}

那么在迁徙的过程中,入口函数简直无需改变,更改只产生在 client 中,咱们也只需对 client 从新测试验证即可。如果你应用的是 C#,咱们甚至能够将 EmailClient 形象为一个接口注入后应用。说白了咱们又回到了拆散关注点,甚至能够说是六边形架构的老路。

针对接口编程还有一个劣势——便于咱们进行组件测试。

咱们能够把下面的流程扩大一下,再被 trigger 之后首先须要从 KeyVault 中获取用于应用 SendGrid 的 API_KEY,在发送结束 SendGrid 之后再应用 Application Insights 记录日志,流程如下图所示

你可能有趣味对虚线框内整套性能进行 E2E(端到端)测试,这并非无奈实现,然而难且代价极大。它的难首先体现在 E2E 自身的测试性质上,如果你对测试金字塔还有印象的话,处于金字塔顶端的 E2E 测试无论是运行老本还是保护老本都是最高的;其次因为 serverless 第三方提供服务的差异性,你很难在每个人的本地搭建出一套线下稳固的测试环境来,由此产生的不确定和对线上环境的依赖有悖于咱们对于测试可能疾速反馈和反复执行的冀望。

所以我倡议在 serverless 中从代码中形象出服务层(Service Layer),优先针对服务层进行测试。服务层是利用的边界和对业务逻辑和用例的封装,即便产生技术栈迁徙它也应该是最不被影响的性能,它应该作为测试中的一个危险点。

而服务层打交道的对象不再是具体的供应商服务而是形象的接口,这也便于咱们在针对服务层的测试中对依赖进行 mock,优化测试流程。

三、Serverless 里的旧酒

3.1 身份验证

无论你应用什么样的技术栈,微服务、Serverless、Low-Code 等等,认证(Authentication)和受权(Authorization)始终是你无奈回避的问题。但不同技术栈下解决受权问题的模式并无不同。在这里先对立一下语言,以下用“验证”同时代指“认证”和“受权”。

以微服务架构为例,服务于接口背地的每一组微服务不可能都领有独立的验证机制。如果你执意这么做的话须要解决不仅限于以下的问题:

  • 如果每一组微服务有须要共享的验证逻辑,那么将类似的代码分布在不同的代码库中的做法会在未来带来散弹式批改的老本
  • 具体的业务发开人员须要学习它们本不应该关怀的认证逻辑,裸露进来的认证代码不免与业务代码耦合
  • 如果在拜访每一组微服务之前都要验证一次权限,势必整体会减少咱们的零碎提早以及带来反复工作。

所以通常咱们会在零碎的边界(Edge Layer)进行验证。咱们对边界外的所有调用放弃狐疑态度,对边界内的服务无条件信赖,这被公认为业内的最佳实际。

而这种边界在古代企业架构中的化身就是 API Gateway。值得强调的是身份验证只是 API Gateway 承当的其中一项职责,实际上 API Gateway 能做的远不止于此。

AWS Lambda 的官网验证机制亦是如此:

在上图中最左侧的 client 的申请必须通过 API Gateway 的验证之后才能够持续拜访后续的 Lambda 或者是 EC2 服务。

在答复了“在哪里验证”这个问题之后,借下面的流程咱们要持续答复第二个问题:如何验证。

鉴于下面阐述的每个服务 / 函数都不应该各自实现一遍验证性能,AWS API Gateway 为咱们筹备了验证机制 custom authorizer(也能够称之为 lambda authorizer,因为 authorizer 由 lambda 函数实现),它的工作原理如下:

  • 当客户端申请达到 API Gateway 时,authorizer 函数能够从申请中获取到用于验证的要害信息,比方 JWT
  • 假如客户端应用的是 Auth0 进行登陆,authorizer 则须要将 JWT 交由 Auth0 进行验证
  • 如果验证胜利,authorizer 便会返回对应的 policy,API Gateway 依据 policy 来决定时许容许拜访后续资源

从上述流程中不难看出验证通过与否决定自 authorizer 的代码实现。但无论你是利用 JWT 还是 SAML 进行验证,背地遵循的仍然是传统 OAuth 的经典流程。我不想对 OAuth 着过多笔墨,上面的流程图兴许能唤起你的不少回顾。

在上述 AWS 的身份验证流程中,当 client 在向 AWS Lambda 发送申请时,咱们首先须要向 Authorization Server 验证身份之后才容许将资源返回给 client。不难看出 authorizer 是流程图中步骤 6 的体现

我要对潜在的“谬误”做一个解释:你可能会认为 OAuth 并不适用于 AWS API Gateway 这类状况,因为 OAuth 实质上是针对“受权”操作设计的,即决定你可能拜访哪些资源;而 API Gateway 的例子像是“认证”场景,即你是否是非法用户。

你对于 OAuth 的了解是对的,借此咱们无妨持续对 OAuth 进行一次深刻阐明:OAuth 本质上是一则委托协定,它凋谢了软件程序以用户的姿势拜访第三方资源的一种可能。OAuth 中的 client 不是指领有这些资源的用户,而是被用户委托的应用程序,比方再网易相册须要拜访用户的谷歌网盘里的照片的场景中,client 其实代指的是网易相册。正因为它只关怀受权资源,所以它能够不必关怀谁以及用什么形式受权的这些资源。残暴点说网易相册只关怀它是否可能获取到容许它调用 Google API 拿到文件信息 token 而已。

回到 API Gateway 的例子中,API 所代表的资源通常是专用的,天然作为资源领有方的 AWS 无需关怀背地的 client 是谁,也无权限度用户可能受权给多少个利用,它只关怀申请里的验证凭据。从这个角度上说,lambda 的验证工作与 OAuth 不约而同。

如果对 OAuth 再做一次形象的话,咱们能够将它称之为“基于 token 的身份验证机制(token-based authentication)”。和传统的用户名明码受权验证形式相比会带来以下劣势:

  • 因为不必把用户名和明码裸露给客户,安全性失去晋升
  • 限定拜访期限,反对随时撤销拜访权限
  • 细粒度的管制用户可拜访的资源

例如 Azure Serverless 就反对存粹基于 token 的验证形式,在你将 HttpTrigger 的 authLevel 参数设置为 function 之后,须要从 UI 上获取 Function Key 值并将其放入名为 x-function-key 的 http header 中才得以让申请到达 function,否则你将失去 401 返回后果

能够看出在解决 serverless 场景下的身份验证问题时,咱们凭仗的仍然是前人留下的宝贵财富。

3.2 部署 Serverless

最初简短的提一下 Serverless 的部署问题。

灵便和轻量是 serverless 主打的卖点之一,超级便捷的部署形式便是这一系列个性的最佳体现。例如 AWS 反对通过上传 zip 文件部署 Lambda;Firebase 反对通过 CLI 部署 Cloud Function;而 Azure Serverless 则为 VSCode 开发了 Azure Functions 插件,容许你在 IDE 中开发过程中一键部署 Azure Function。

如果你对这些伎俩的改良仅仅了解为免去了繁琐的步骤便于咱们能够更疾速的将代码部署到生产环境的话,那么我倡议你还是不要应用这些伎俩为妙。因为在软件交付的过程中纯手工的部署行为是一类反模式行为:这种一步到位的手工部署意味着你必须用手工测试的形式验证性能是否失常,同时未经试运行环境的检测而间接部署到生产环境的话,会导致咱们无奈验证在开发环境中产生的假如在生产环境中是仍然成立的,甚至在产生问题之后没有配套机制保障咱们的代码回滚到上一个稳固版本。咱们能够援用《继续交付》一书中的话对现实中的继续交付进行演绎:软件公布可能(也应该)成为一个低危险、频繁、便宜、迅速且可预感的过程。

所以 serverless 的交付环节仍然须要被治理,例如配置管理、编译、自动化测试、灰度公布等等过程对 serverless 依然实用。那为什么 serverless 服务商不持续迈出一步为咱们提供更丰盛的交付解决方案呢?这个问题的答案既是必定的也是否定的。

必定答复的理由是,现有平台工具早已反对咱们达成此类指标。例如 Azure DevOps 平台反对咱们为 serverless 利用创立 pipeline 以及治理每一次构建后的 artifact;Azure Serverless 也反对灰度公布(Deployment slots),你能够抉择首先将构建后的代码首先公布到 staging 环境上,待验证无误后一键将现存的 production 代码替换。

而否定答案也同样成立的理由是因为所有这些配套设施都并非只为 serverless 精心定制,简直所有 Azure 提供的服务都能通过 Azure DevOps 平台进行部署,所有服务在 Azure DevOps 上被厚此薄彼看待。

所以不难看出 Azure DevOps 发售的并不是预置好的标准化流程,而是反对定制化的公共能力。它们之所以止步于此不是因为代编码能力无限,而是因为无奈在代码层面做进一步形象。

如果你对编码稍有教训的话,你应该明确在软件开发中最艰难的不是编码环节,而是在于后期的程序设计,以及将各类模式恰到好处的融入其中。然而你也应该了解,哪怕是耳熟能详的 MVC 模式,在不同编程语言下的含意也不尽相同,甚至在同一种编程语言下实现也能够不同(例如 Angular 的双向绑定模式之于 Backbone.js 的事件机制)。咱们无奈用变化无穷的代码精准对其定义。

继续交付常识也具备相似的性质,大部分时候咱们须要对症下药的为我的项目设计交付流程。例如为团队抉择失当的 Git 工作流,判断是否有必要为我的项目增加冒烟测试等等。这些都是无奈通过代码计算出来的,这部分工作往往也是最难的,因为你须要对我的项目进行评估以及团队沟通之后能力将计划确定下来。正是因为 DevOps 环节里存在太多不确定因素,我的项目之间千差万别,平台能做的也只能是在泛滥因素之间寻找最大公约数(所有的我的项目都须要部署,都须要灰度公布,都须要环境变量治理),又或者将把所有可能性打包作为公共能力提供给你(比方 Azure DevOps 平台)

序幕

因为篇幅的关系咱们只能议论到这里,心愿你能从上述的文字中勾画出一个无关 serverless 更清晰的轮廓。咱们很难说服本人 serverless。

起源:Thoughtworks 洞见
申明:文章取得作者受权在 IDCF 社区公众号(devopshub)转发。优质内容共享给思否平台的技术伙伴,如原作者有其余思考请分割小编删除,致谢。

玩乐高,学麻利,规模化麻利联合作战沙盘之「乌托邦打算」,12 月 25-26 日登陆深圳,将“多团队麻利协同”基因内化在研发流程中,为规模化晋升研发效力保驾护航!!🏰⛴公众号回复“乌托邦”可加入

正文完
 0