逐步发展你的RESTful APIs—一个循序渐进的方法

95 阅读8分钟

设计一个直观的、用户友好的RESTful API是一项艰难的工作。如果这是你的第一次尝试,它可能已经是一个巨大的任务。规划你的API的生命周期管理可能是一个事后的想法。但无论如何,这是有可能的:在这篇文章中,我想提出一个不折不扣的方法来进化你的API,即使它没有被计划。

最初的情况

让我们考虑一个在使用时说 "你好 "的示例应用程序:

> curl http://org.apisix/hello
Hello world

> curl http://org.apisix/hello/Joe
Hello Joe

底层技术并不重要,我们将专注于API部分:

Initial situation

使用一个API网关

第一步也是最关键的一步是停止将应用程序直接暴露在互联网上,在它们之间建立一个API网关。如果你不熟悉API网关的概念,你可以把它看成是一个强化了的反向代理。维基百科提供了以下定义。

网关:作为API前端的服务器,接收API请求,执行节流和安全策略,将请求传递给后端服务,然后将响应传回给请求者。网关通常包括一个转换引擎,以协调和修改飞行中的请求和响应。网关还可以提供一些功能,如收集分析数据和提供缓存。网关可以提供支持认证、授权、安全、审计和监管的功能。

-API管理

在这篇文章中,我将使用Apache APISIX,但请随意使用你最熟悉的那个。

暴露网关而不是应用程序需要你更新你的DNS记录,以指向网关而不是应用程序,并等待它被传播到世界各地。这可能需要一些时间。为了跟踪传播情况,你可以使用像dnschecker这样的网站。

然而,你首先需要将你的HTTP请求从网关路由到你的应用程序。使用APISIX,你可以通过向网关发送HTTP请求来创建一个路由

curl http://apisix:9080/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PUT -d ' (1) (2)
{
  "name": "Direct Route to Old API",               (3)
  "methods": ["GET"],                              (4)
  "uris": ["/hello", "/hello/", "/hello/*"],       (5)
  "upstream": {                                    (6)
    "type": "roundrobin",                          (8)
    "nodes": {
      "oldapi:8081": 1                             (7)
    }
  }
}'
1APISIX可以分配一个自动生成的ID,或者使用提供的ID。在这种情况下,我们选择后者,在URL中传递它 -1 ,并使用PUT 动词
2要更新路由,我们需要传递API密钥
3命名路由并不是必须的,但它可以让我们更好地理解它的作用
4路由的HTTP方法数组
5要路由的URL数组
6上游是一个后端应用程序。在我们的例子中,它是Hello World API。
7具有各自权重的节点的哈希图。权重只有在有多个节点的情况下才有意义,而在这个简单的场景中并不是这样的。
8当你配置多个节点时,要使用的平衡算法

Use an API Gateway

在这个阶段,你可以查询网关并得到与之前相同的结果:

> curl http://org.apisix/hello
Hello world

> curl http://org.apisix/hello/Joe
Hello Joe

对API进行版本升级

API的发展意味着API的多个版本需要在某些时候并存。有三个选项可以使一个人的API版本化:

类型例子
查询参数
curl http://org.apisix/hello?version=1
curl http://org.apisix/hello?version=2

标题

curl -H 'Version: 1' http://org.apisix/hello
curl -H 'Version: 2' http://org.apisix/hello

路径

curl http://org.apisix/v1/hello
curl http://org.apisix/v2/hello

关于什么是最好的选择,已经写了很多文章。在这篇文章的范围内,我们将使用基于路径的版本管理,因为它是最广泛的。如果你想使用其他选项,APISIX也支持这些选项。

在上一节中,我们创建了一个包裹上游路由。APISIX允许我们创建一个有专用ID的上游,以便在多个路由中重复使用。

curl http://apisix:9080/apisix/admin/upstreams/1 -H 'X-API-KEY: xyz' -X PUT -d ' (1)
{
  "name": "Old API",                                                                   (2)
  "type": "roundrobin",
  "nodes": {
    "oldapi:8081": 1
  }
}'
1使用upstreams 路由
2新上游的有效载荷

我们还需要重写进入网关的查询,然后再将其转发到上游。后者知道/hello ,而不是/v1/hello 。APISIX允许通过插件进行这种转换、过滤等。让我们创建一个插件配置来重写路径。

curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PUT -d ' (1)
{
  "plugins": {
    "proxy-rewrite": {                                        (2)
      "regex_uri": ["/v1/(.*)", "/$1"]                        (3)
    }
  }
}'
1使用plugin-configs 路径
2使用proxy-rewrite插件
3移除版本前缀

我们现在可以创建版本路由,引用新创建的上游和插件配置:

curl http://apisix:9080/apisix/admin/routes/2 -H 'X-API-KEY: xyz' -X PUT -d '  (1)
{
  "name": "Versioned Route to Old API",
  "methods": ["GET"],
  "uris": ["/v1/hello", "/v1/hello/", "/v1/hello/*"],
  "upstream_id": 1,
  "plugin_config_id": 1
}'
1看,妈,一个新的路由!

Version the API

在这个阶段,我们已经配置了两条路由,一条是有版本的,另一条是无版本的:

> curl http://org.apisix/hello
Hello world

> curl http://org.apisix/v1/hello
Hello world

将用户从非版本的路径迁移到版本的路径上

我们已经对我们的API进行了版本升级,但是我们的用户可能仍然使用传统的非版本升级的API。我们想让他们迁移,但我们不能直接删除遗留路径,因为我们的用户并不知道。幸运的是,301 HTTP状态代码是我们的朋友:我们可以让用户知道,资源已经从org.apisix/hello 转到org.apisix/v1/hello。这需…](apisix.apache.org/docs/apisix…

curl http://apisix:9080/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
  "plugins": {
    "redirect": {
      "uri": "/v1$uri",
      "ret_code": 301
    }
  }
}'

Migrate users to the versioned app

结果是有趣的:

>curl http://apisix.org/hello

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty</center>
</body>
</html>

>curl -L apisix:9080/hello                     (1)
Hello world
1-L 选项跟随重定向

要么用户会透明地使用新的端点,因为他们会跟随,要么他们的整合中断,他们会注意到301状态和新的API位置的使用。

了解你的用户

你可能已经注意到,到目前为止,我们还不知道谁在使用我们的API。当我们不得不引入一个变化时,我们必须要有创造性,不要破坏用户的使用。其他的变化可能就不那么容易应付了。因此,我们应该努力了解我们的用户,以便在必要时联系他们。

让我们坦率地说;大多数开发者,包括我自己,如果可以避免的话,都不喜欢注册和提供联系信息。我想这是营销团队的错,他们不理解我们的心态--不给我打电话,我就给你打电话。然而,在这种特殊情况下,这将是有益的。

核 "选项完全不允许用户在系统中注册前调用我们的API。我更喜欢另一种选择:限制未注册用户在一个时期内的呼叫次数。如果他们达到了限制,我们将返回(不)著名的429 HTTP状态和邀请他们注册的信息。

在写这篇文章的时候,没有任何开箱即用的插件可以实现这个目标。但我们可以自己编写。APISIX位于一个Lua引擎之上,所有提供的插件都是用Lua编写的。另外,你也可以用Go、Python、WebAssembly或任何基于JVM的语言编写你的插件。

为了使事情简单,我写了一个Lua插件。由于这篇文章的目的不是为了理解Lua,所以我不会进一步深入研究。如果你对代码感兴趣,它可以在GitHub上找到。当公众准备好后,我们还有几个步骤要完成。

  1. 配置APISIX以使用该目录:

    config.yaml

    apisix:
      extra_lua_path: "/opt/apisix/?.lua"      (1)
    
    1APISIX可以使用位于/opt/apisix/ 文件夹中的任何Lua脚本
  2. 加载该插件:

    2.APISIX可以自己热重载。我们不需要重新启动它--和承受停机时间--来添加额外的插件! 2.

    curl http://apisix:9080/apisix/admin/plugins/reload -H 'X-API-KEY: xyz' -X PUT
    
  3. 对现有的插件配置进行修补:

    最后,我们需要对插件本身进行配置。由于我们创建了一个专门的插件配置,我们只需要用新的配置来更新它。

    curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
    {
      "plugins": {
        "proxy-rewrite": {                                (1)
          "regex_uri": ["/v1/(.*)", "/$1"]
        },
        "unauth-limit": {                                 (2)
          "count": 1,                                     (3)
          "time_window": 60,                              (3)
          "key_type": "var",                              (4)
          "key": "consumer_name",                         (4)
          "rejected_code": 429,
          "rejected_msg": "Please register at https://apisix.org/register to get your API token and enjoy unlimited calls"
        }
      }
    }'
    
1不幸的是,我们需要重复现有的插件配置。APISIX团队正在进行修复,所以你可以在不知道现有配置的情况下添加一个插件。
2我们的插件!
3如果用户是未认证的,该插件限制每60秒超过一次呼叫。否则,它不会限制任何东西。
4在下一节中解释

我们现在可以检查它的行为是否符合预期:

>curl apisix:9080/v1/hello
Hello world

>curl apisix:9080/v1/hello
{"error_msg":"Please register at https:\/\/apisix.org\/register to get your API token and enjoy unlimited calls"}

事实上,它确实如此。

创建用户

你可能应该开始看到你的用户访问注册页面,这取决于你对未认证使用的限制程度。注册有很多方面它可以是:

  • 自动或需要你所希望的许多手动验证步骤
  • 免费或付费
  • 简单到不需要进一步验证的电子邮件,或者复杂到需要更多的数据
  • 等等。

这取决于你的具体环境。

关于APISIX,归根结底,它转化为一个新的消费者。为了创建这样一个消费者,我们需要配置一个指定认证的插件。有几个认证插件是开箱即用的:basic、API key、JWT、OpenId、LDAP、Keycloak,等等。

在这篇文章的范围内,密钥认证插件已经足够了。让我们来配置一个由API密钥认证的消费者对象。

curl http://apisix:9080/apisix/admin/consumers -H 'X-API-KEY: xyz' -X PUT -d '
{
  "username": "johndoe",                 (1)
  "plugins": {
    "key-auth": {                        (2)
      "key": "mykey"                     (3)
    }
  }
}'
1消费者的ID
2要使用的插件
3有效的令牌是mykey

请注意,默认的标头是apikey 。可以配置另一个标头:请查看key-auth插件文档

我们现在可以测试我们的设置,并验证它是否按照我们的要求工作:

>curl -H 'apikey: mykey' apisix:9080/v1/hello
Hello world

>curl -H 'apikey: mykey' apisix:9080/v1/hello
Hello world

在生产中测试

在这个阶段,我们现在已经准备好让用户了解我们的Hello world API的改进版本。我想我们的团队对它进行了彻底的测试,但新的代码总是有风险的。部署一个现有应用程序的有错误的新版本会对API供应商的形象(和收入!)产生负面影响。

为了尽量减少风险,一个公认的策略是进行金丝雀发布。

金丝雀发布是一种在生产中引入新的软件版本的技术,在将其推广到整个基础设施并提供给所有人之前,先在一小部分用户中慢慢推出该变化,以减少风险。

-金丝雀发布

如果有什么东西失败了,它将只影响到用户群的一小部分,我们将能够在没有太大影响的情况下恢复这个变化。然而,通过API网关,我们可以在金丝雀发布之前引入一个步骤:我们将复制生产流量到新的API端点。虽然网关会丢弃响应,但我们可以在对用户没有影响的情况下发现额外的bug。

APISIX提供了代理镜像插件,将生产流量复制到其他节点。让我们来更新我们的插件配置。

curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
 "plugins": {
    "proxy-rewrite": {
      "regex_uri": ["/v1/(.*)", "/$1"]
    },
    "unauth-limit": {
      "count": 1,
      "time_window": 60,
      "key_type": "var",
      "key": "consumer_name",
      "rejected_code": 429,
      "rejected_msg": "Please register at https://apisix.org/register to get your API token and enjoy unlimited calls"
    },
    "proxy-mirror": {
      "host": "http://new.api:8082"                             (1)
    }
  }
}'
1APISIX也将向这个主机发送流量

Test in production

我们可以同时监控新的和旧的端点,以确保前者没有比后者更多的错误发生。如果没有,我们可以修复错误并再次重新部署,直到出现这种情况。现在我们准备进行金丝雀发布。

首先,我们创建一个指向新API的上游:

curl http://apisix:9080/apisix/admin/upstreams/2 -H 'X-API-KEY: xyz' -X PUT -d '
{
  "name": "New API",
  "type": "roundrobin",
  "nodes": {
    "newapi:8082": 1
  }
}'

然后,我们可以用traffic-split替换proxy-mirror 插件:

curl http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
 "plugins": {
    "proxy-rewrite": {
      "regex_uri": ["/v1/(.*)", "/$1"]
    },
    "unauth-limit": {
      "count": 1,
      "time_window": 60,
      "key_type": "var",
      "key": "consumer_name",
      "rejected_code": 429,
      "rejected_msg": "Please register at https://apisix.org/register to get your API token and enjoy unlimited calls"
    },
    "traffic-split": {
      "rules": [
        {
          "weighted_upstreams": [      (1)
            {
              "upstream_id": 2,
              "weight": 1
            },
            {
              "weight": 1
            }
          ]
        }
      ]
    }
  }
}'
1为演示目的,将50%的流量发送到新的API。在现实生活中,你可能会把起点降低很多,甚至只配置内部用户到新的端点。
curl -L -H 'apikey: mykey' apisix:9080/hello
Hello world

curl -L -H 'apikey: mykey' apisix:9080/hello
Hello world (souped-up version!)

如果一切正常,我们可以逐渐增加发送到新API的流量百分比,直到达到100%。现在我们可以取消流量分割,从默认端点重定向到v2而不是v1。

废弃遗留的版本

大多数用户可能会迁移到新版本以从中受益,但也有一部分用户会留在v1版本上。这可能有多种原因:没有合适的时间(提示:永远不会),太贵了,没有足够的动力迁移,你说是吧。但作为一个API供应商,每一个部署的版本都有明确的成本。你可能需要在某个时间点淘汰V1版本。

REST不是一个标准,但IETF有一个关于它的规范草案。更多细节,请阅读The Deprecation HTTP Header Field。顾名思义,它是基于一个特定的HTTP响应头。

在API网关的帮助下,我们可以配置路由以沟通其未来的废弃和替换。为此,APISIX提供了响应重写的功能。虽然它可以重写响应的任何部分,但我们将使用它来添加额外的弃用头信息。

curl -v http://apisix:9080/apisix/admin/plugin_configs/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
 "plugins": {
    "proxy-rewrite": {
      "regex_uri": ["/v1/(.*)", "/$1"]
    },
    "unauth-limit": {
      "count": 1,
      "time_window": 60,
      "key_type": "var",
      "key": "consumer_name",
      "rejected_code": 429,
      "rejected_msg": "Please register at https://apisix.org/register to get your API token and enjoy unlimited calls"
    },
    "response-rewrite": {
      "headers": {
        "Deprecation": "true",
        "Link": "<$scheme://apisix:$server_port/v2/hello>; rel=\"successor-version\""
      }
    }
  }
}'
curl -v -H 'apikey: mykey' apisix:9080/v1/hello

< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 11
< Connection: keep-alive
< Date: Fri, 18 Feb 2022 16:33:30 GMT
< Server: APISIX/2.12.0
< Link: <http://apisix:9080/v2/hello>; rel="successor-version"
< Deprecation: true
<
Hello world

结论

在这篇文章中,我们描述了一个简单的一步一步的过程来管理你的API的生命周期:

  1. 不要直接暴露你的API;在前面设置一个API网关。
  2. 使用路径、查询参数或请求头对现有的API进行升级
  3. 用301状态代码将用户从未版本化的端点迁移到版本化的端点上
  4. 轻轻地推动你的用户去注册
  5. 在生产中进行测试,首先复制流量,然后将一小部分用户转移到新的版本上。
  6. 正式发布新的版本
  7. 通过标准的响应头来传达旧版本的淘汰。

这篇文章的完整源代码可以在Github上找到,格式为maven。