设计一个直观且用户友好的RESTful API往往是一项艰巨的工作。而对于初次尝试规划和管理API生命周期的新手开发者而言,尤为如此。下面,我将以简单示例的形式,和您探讨如何循序渐进地管理RESTful API的生命周期。
初始阶段
让我们首先来看一个典型的Hello应用代码的示例:
> curl http://org. apisix/hello
Hello world
> curl http://org. apisix/hello/Joe
Hello Joe
如下图所示,我们不必了解其底层技术,只需专注其API部分即可。
采用API网关
首先也是最关键的一步:禁止将应用直接暴露到互联网上,并在前端建立一个API网关。维基百科是这样定义API网关的:作为一个API的服务器前端,它能够收到各种API请求,并通过实施节流和安全策略,将请求传递给后端服务,进而将响应回传给请求者。
网关通常包括一个用于编排和修改请求与响应转换引擎。同时,网关还可以提供诸如:收集分析数据、提供缓存、支持身份验证、授权、安全审计以及合规检查等功能。当然,如果您不太熟悉API网关的概念,也可以直接把它理解为一个更高级别的反向代理。在此,我将使用Apache APISIX,您也可以使用自己熟悉的网关。
为了暴露网关,您需要更新指向网关的DNS记录,并向外广播。您既可以等待一段时间让其自动更新,也可以通过dnschecker来加速这个过程。
我使用APISIX创建了一个将HTTP请求发送到网关的路由(route,请参见如下代码段)。
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
}
}
}'
注释:
1. APISIX会分配一个自动生成的ID(您也可以使用现成的)。在此,我使用现成的,并使用put将它传递给URL - 1。
2. 通过API key来更新路由。
3. 虽然我们不一定需要命名路由,但它能够让我们更好地对其有所了解。
4. 针对路由的HTTP方法数组。
5. 针对路由的URL数组。
6. 指明后端应用的上游(upstream)。在本例中,即为Hello World API。
7. 各个节点的Hashmap都自带有权重。显然,权重只有存在多个节点时才有意义。
8. 针对多个节点配置均衡算法。
如上图所示,您可以通过如下方式查询网关,并得到相同的结果:
> curl http://org. apisix/hello
Hello world
> curl http://org. apisix/hello/Joe
Hello Joe
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
}'
下面展示了新路由的逻辑图。
在此,我们已配置了有版本和非版本的两个路由:
> curl http://org. apisix/hello
Hello world
> curl http://org. apisix/v1/hello
Hello world
将用户从非版本路径迁移到有版本路径
虽然我们给API编制了版本,但是用户可能仍会使用旧的、非版本的API。毕竟,我们肯定不能在用户不知情时就删除掉旧的路由,而只能通过HTTP的301状态代码,让用户知道资源已经从http://org.apisix/hello迁移到了http://org.apisix/v1/hello。如下代码段展示了如何通过配置重定向插件来初始化路由:
curl http://apisix:9080/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PATCH -d '
{
"plugins": {
"redirect": {
"uri": "/v1$uri",
"ret_code": 301
}
}
}'
其运行结果如下:
>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在被调用的过程中不会中断用户的使用,我选择了限制未注册的用户可调用的数量。如果他们的总量到达了该限制,我们将返回典型的HTTP 429状态消息,要求他们完成注册。
当然,目前尚无开箱即用的插件可以实现这一点。因此,我在APISIX中引入Lua引擎,并用Lua编写出了相应的插件。您可以通过GitHub的链接--https://github.com/nfrankel/evolve-apis/blob/master/unauth-limit-plugin/src/unauth-limit.lua,来浏览其源代码。当然,您也可以使用Python、WebAssembly或任何基于JVM的语言,来编写自己的插件。
我将通过如下步骤来完成插件的加载:
1. 配置APISIX使用的目录:
apisix: extra_lua_path:/opt/apisix/.lua ?”
注意:APISIX可以使用位于/opt/apisix/文件夹下的任何Lua脚本。
2. 加载插件:
由于APISIX支持热重载,因此它可以在无需重新启动的情况下添加额外的插件。
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团队正在修复这个bug。
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"}
实际上确实如此。
用户认证
接着,我们需要为消费者(consumer)角色配置一个专属的身份验证插件。目前,我们可以选用的此类身份验证插件有:API key、JWT、OpenId、LDAP、以及Keycloak等。在本例中,我们采用APISIX的key-auth插件。下面,让我们配置一个通过API key验证的消费者对象:
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端点上,实现在对用户几乎不产生影响的情况下,尽早发现更多的缺陷。
在此,APISIX提供了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"
},
"proxy-mirror": {
"host": "http://new. api:8082" # 1
}
}
}'
注释:
1. APISIX将发送流量到该地址上。
根据上图,我们可以监控和比较新和旧端点,一旦发生错误,我们便可以修复错误,并重新部署。在此,我们首先通过如下命令,创建一个指向新的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。最终我们可以移除traffic split,将默认端点从v1重定到v2上。
弃用旧的版本
根据IETF的草案规范,我们可以基于特定的HTTP响应标头,来弃用HTTP的标头字段 (请参见--https://tools.ietf.org/id/draft-dalal-deprecation-header-03.html)。即:在API网关的帮助下,我们可以通过配置路由,来与有待弃用和替代的版本进行通信。为此,我们将使用由APISIX提供response-rewrite,来添加额外弃用标志标头,请参见如下代码段:
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. 通过标准响应标头来弃用旧的版本
原文标题:Evolving your RESTful APIs, a step-by-step approach,作者:Nicolas Fränkel