OpenResty:瑞士军队的无服务器代理

2020-05-26 14:34:20

在CloudFlare,Nginx是我们工作的核心。它是我们反向代理服务的底层基础的一部分。除了内置的Nginx功能之外,我们还使用一组特定于我们的基础设施的自定义C模块,包括负载平衡、监控和缓存。最近,我们一直在增加更多简单的服务。而且它们几乎都是用卢亚语写的。

不久前,我开始编写身份感知代理(IAP)来通过身份验证保护二进制文件。然而,最初的最小身份验证层随着功能的增加而增长。我逐渐意识到反向代理是做各种横切关注点的好层,比如身份验证、至少一次交付和适配。此外,我发现OpenResty提供了惊人的性能和灵活性,它几乎完美地符合无服务器范例。

具体地说,我一直致力于扩展IAP以接收和重塑来自Slake和Zapier的信号,通过预写日志(WAL)传输它们,并在它们进入我们的应用程序二进制文件之前验证它们的真实性。事实证明,在代理层进行这些集成具有巨大的技术优势。

代理的第一个胜利是作为通用适配器。您经常需要更改在独立开发的服务之间交换的JSON的形状。考虑到使用公共身份验证层包装服务的效用,这也是进行域映射的方便之处。使用OpenResty,您可以在性能良好的二进制文件中执行此操作。

第二个胜利是使用代理来最小化响应延迟。Sack坚持要求机器人在3秒内回复。如果上游是无服务器的JVM进程,那么在上游冷的时候很容易超时。我们在代理层解决了这个问题,方法是将传入的请求缓冲到托管队列中,这有点像预写日志(WAL)。这意味着一旦队列确认写入,我们就可以通过回复Slake的WebHook来降低延迟。由于OpenResty是c+lua,启动速度如此之快,我们在无服务器环境中尽了最大努力。

使用Wal,我们获得了至少一次的交付语义。将WAL放在代理层可以掩盖大量的上游可靠性问题。这意味着只要上游是幂等的,您就不需要重试上游的逻辑。这简化了应用程序开发,并扩大了上游堆栈的选择范围。特别对我们来说,这意味着无需重写启动缓慢的JVM二进制文件即可在无服务器上部署。

最后,我们可以快速验证传入消息的真实性,以便在消耗上游更昂贵的资源之前阻止潜在的攻击。同样,在这种死记硬背的任务中,OpenResty可能比应用服务器更快(因此也更便宜)。我们发现在SecretManager中存储秘密并在代理中检索它们相对容易。

总体而言,我们发现OpenResty几乎是无服务器的完美技术。它为您提供C性能(响应延迟低至5ms)、快速启动时间(在Cloud Run上冷启动400ms)和Ngnix的生产就绪,同时让您可以灵活地使用Lua脚本对基础架构边界进行自定义和添加功能。

值得一提的是,Cloud Run可伸缩为零(不像Fargate),并且支持并发操作(不像Lambda和Google Cloud函数)。OpenResty+Cloud Run将允许您在单个实例上为大量并发流量提供服务,因此我认为它在所有选项中都是最具成本效益的。虽然它的冷启动比Lambda(我们得到400ms比200ms)要高,因为它需要更少的扩展事件,但我预计对于大多数部署来说,冷启动的频率会更低。

让代理处理更多的用例(例如重试逻辑)可以将成本从应用程序二进制文件移到基础架构中最灵活的部分。您不需要Kubernetes集群来获得所有这些好处,但是如果您愿意,您可以将其部署在集群中。我们已经设法将我们所有的功能打包成一个小的无服务器服务,由麻省理工学院许可的Terraform部署。

现在我将更详细地谈一谈我们的具体发展情况,以及我们是如何解决各种战术发展问题的。对于大多数读者来说,这可能太详细了,但我已经尝试按概括性对它们进行排序。

云运行部署的缓慢阻碍了复杂的代理功能开发,因此首先要解决本地开发问题。当Cloud Run最终部署dockerfile时,我们使用docker-compose来调出我们的二进制文件以及GCE元数据仿真器。我们的dockerfile是从Terraform模板生成的,但是您可以要求Terraform使用-target标志生成本地文件,而无需部署整个项目。因此,我们可以通过在循环中对Terraform工件生成和docker-compose进行排序来创建一个半像样的开发循环,而不需要重写Terraform配方来支持本地开发!

使用上面的shell脚本,当您在shell中按CTRL+C时,二进制文件将更新并重新运行。棘手的一点是退出这个循环!如果您使用";/bin/bash test/dev.sh";调用它,那么它将被命名为bash,这样您就可以使用";KILLALL bash";退出。OAuth 2.0 redirect_uri不能与localhost一起使用,因此您需要从Prod部署复制带有/login?Token=true的Prod令牌。

要能够自信地快速响应传入请求,一般用途内部位置/WAL/…。已添加到代理中。任何转发到/wal/<;path>;的请求都假定是针对上游/<;path>;的,但会通过发布/订阅主题和订阅进行传输。这将缓冲区的持久存储卸载到具有极大延迟和耐久性保证的专用服务。

每个WAL消息实质上封装了一个HTTP请求。因此,标题、uri、方法和正文被放在一个信封中,并发送给PubSub。请求正文被映射到Base64编码的发布/订阅数据字段,我们使用发布/订阅属性来存储其余的内容。

已设置发布/订阅订阅,该订阅将把信封推送回代理位置/Wal-Playback。通过指定oidc_Token,Pub/Sub将添加可在代理中验证的ID令牌。

在OpenResty配置中,我们向Internet公开/wal-playback,但在打开信封并向上游发送之前,我们会验证传入的令牌。

对于我们的用例,我们的上游也托管在Cloud Run上。如果上游响应是状态代码429(请求太多),这意味着容器正在扩展,应该重试。类似地,状态代码500表示上游已中断,应该重试该请求。对于这些响应代码,代理向发布/订阅返回状态500,这触发其重试行为,导致至少一次交付语义。

我们希望通过向内部业务工作流引擎添加自定义斜杠命令来提高内部工作效率。创建内部bot和重新生成新命令非常简单,您只需要提供一个公共端点。

SLACK发送出站x-www-form-urlended网络挂钩。当然,我们的上游使用JSON,但是使用Resty-reqargs包进行转换是微不足道的。

由于这是一个公共端点,我们需要确保请求的真实性。SLACK使用共享对称签名密钥。因为我们不想要任何接近Terraform的秘密。我们手动将密钥复制到Google Secret Manager中。

那么我们只需要将密钥的资源ID存储在Terraform中。顺便说一句,“秘密经理”太棒了!您可以参考最新版本,这样您就可以轮换秘密,而不会影响Terraform。

在OpenResty配置中,我们使用经过身份验证的GET和Base64解码来获取秘密。我们将秘密存储在全局变量中,以供跨请求使用。

Slake文档很好地解释了如何验证请求。使用resty.hmac包时,只有几行Lua代码:

当然,SLACK的真正困难在于3秒的超时要求,因此入站SLACK命令被转发到WAL,以便使用至少一次的交付语义进行快速响应。

Zapier是另一个性价比很高的集成。一旦你有了身份感知代理,创建一个可以调用你的API的内部应用程序就变得很简单了。

创建Zapier App后,您需要添加Zapier作为授权重定向URL。

要使授权与Google Auth一起无限期工作,您需要向令牌请求添加参数以启用脱机访问。

要使刷新令牌端点正常工作,您需要添加客户端ID和密码:

为了将信号从Zapier发送到我们的内部软件,我们创建了一个名为Signal的Action,它有一个Name键和一个字符串,即变量的字符串字典。这似乎是一个最小但灵活的模式。

它非常好的Zapier可以与OAuth 2.0配合使用,它有助于验证我们自己的身份实现的正确性。

我们的内部工作流引擎正在开发中,完全开源,并获得麻省理工学院的许可。阅读更多关于我们对基于OpenAPI的数字过程自动化的愿景的信息。