欢迎大家前往 腾讯云+社区 ,获取更多腾讯海量技术实践干货哦~
本文来自 云+社区翻译社 ,作者 ArrayZoneYour
Nginx往往是构建微服务中必不可缺的一部分,从本文中你可以习得如何使用Nginx作为API网关。
HTTP API是现代应用架构的核心。HTTP协议使开发者可以更快地构建应用并使应用的维护变得更加容易。HTTP API提供了一套通用的接口,这使得在任意的应用规模下,我们都可以借助HTTP API从一个基本的微服务开始构建出一个具有完备功能的整体。借助HTTP,普通的web应用程序也可以在规模巨大的互联网上提供高性能、高可用的API。
如果你还不理解API网关对微服务应用的重要性,可以参阅 Building Microservices: Using an API Gateway
作为领先的高性能、轻量级反向代理和负载均衡器解决方案,NGINX Plus具有处理API流量所需的高级HTTP处理能力。这使得NGINX Plus成为构建API网关的理想平台。在本文中,我们将使用一些常见的API网关为例展示如何配置NGINX Plus来以高效、可扩展、易维护的方式处理它们。最后我们会得到一套可作为生产环境部署基础的完整配置。
注:除特殊注明外,本文中所有的配置同时适用于NGINX和NGINX Plus。
样例API简介(以仓储背景为例)
API网关的主要功能是为不同的API分别提供单独,一致的入口点,它的实现与后端的实现与部署方式无关。实际场景中,往往不是所有的API都是以微服务的方式实现的。我们的API网关需要同时管理现有的API、巨无霸式的API( monoliths , 对与微服务相对的庞然大物的戏称)以及开始局部切换为微服务的应用等等。
在本文中,我们假想一个库存管理的API(WareHouse API)为例进行说明。我们使用实例的配置代码来说明不同的用例。我们假设的API是一个RESTful API,它接受JSON请求并生成JSON数据响应请求。虽然我们本文中是以RESTful API为例进行讲解,但是NGINX Plus作为API网关部署时并不要求或者限制JSON的使用;NGINX Plus本身并不知道API使用的架构或者数据格式。
WareHouse API 作为一组独立的微服务之一被实现并作为一个单独的API进行发布。其下的inventory 和 pricing 资源分别作为单独的服务集成并部署在不同的后端上。由此可以画出如下的API路径结构:
api
└── warehouse
├── inventory
└── pricing
举例来说,如果我们想获得仓库的库存信息,则需要通过客户端发送一个 HTTP
GET
请求到/api/warehouse/inventory
组织NGINX的配置文件
我们使用NGINX Plus作为API网关的好处是它可以同时扮演反向代理、负载均衡器以及现有HTTP流量所需的web服务器这三个角色。如果NGINX Plus已经是你的应用交付栈的一部分,那么你不需要再用它部署一个单独的API网关。不过,API网关预期的默认行为与基于浏览器的流量所期望的默认行为不同,因此我们需要将API网关配置与现存(未来)的基于浏览器所需的流量对应的配置文件分来。
为了实现上述需求,我们为配置文件创建了以下目录结构来支持多用途的NGINX Plus实例,这也为通过CI / CD 管道自动配置并部署提供了便利。
etc/
└── nginx/
├── api_conf.d/ ....................................... API配置的子目录
│ └── warehouse_api.conf ...... Warehouse API 的定义及配置
├── api_backends.conf ..................... 后端服务配置 (upstreams)
├── api_gateway.conf ........................ API网关服务器的顶级配置
├── api_json_errors.conf ............ JSON格式的HTTP错误响应配置
├── conf.d/
│ ├── ...
│ └── existing_apps.conf
└── nginx.conf
API网关配置的目录和文件名都加了**api_**前缀。上面的每个目录和文件都对应着API网关的不同功能和特性,我们在下面会逐个详细解释。
定义API网关的顶级配置
NGINX读取配置将从主配置文件
nginx.conf
开始。为了读取API网关配置,我们需要在
nginx.conf
中
http
块中添加一条指令来引用包含网关配置的文件
api_gateway.conf
(大概在28行附近)。从文件内容中我们可以看到
nginx.conf
中默认从
conf.d
子目录中读取基于浏览器的HTTP配置。本文中将广泛使用
include
命令来提高可读性并实现部分配置的自动化。
include /etc/nginx/api_gateway.conf; # 所有的API网关配置
include /etc/nginx/conf.d/*.conf; # 正常的web流量配置
api_gateway.conf 文件定义了将NGINX Plus作为API网关暴露给客户端的虚拟服务器的配置。该配置将暴露所有由API网关发布的API,入口位于 https://api.example.com/ ,用TLS协议加密保护。注意这里使用的配置文件是针对HTTPS的——并没有使用明文传输的HTTP。这代表着我们默认并要求API客户端知道正确的入口点并使用HTTPS连接。
log_format api_main '$remote_addr - $remote_user [$time_local] "$request"'
'$status $body_bytes_sent "$http_referer" "$http_user_agent"'
'"$http_x_forwarded_for" "$api_name"';
include api_backends.conf;
include api_keys.conf;
server {
set $api_name -; # Start with an undefined API name, each API will update this value
access_log /var/log/nginx/api_access.log api_main; # Each API may also log to a separate file
listen 443 ssl;
server_name api.example.com;
# TLS 配置
ssl_certificate /etc/ssl/certs/api.example.com.crt;
ssl_certificate_key /etc/ssl/private/api.example.com.key;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_protocols TLSv1.1 TLSv1.2;
# API 定义, 每个文件对应一个
include api_conf.d/*.conf;
# 错误响应
error_page 404 = @400; # 处理非法URI路径的请求
proxy_intercept_errors on; # 不将后端的错误消息发送给客户端
include api_json_errors.conf; # 定义返回给客户端的JSON响应数据
default_type application/json; # 如果不指定 content-type 则默认为 JSON
}
以上配置是静态的,表现在每个独立API的细节以及响应的后端服务是通过
include
命令引用相应的文件实现的。上面文件的最后四行负责处理默认的日志输出以及错误处理。我们将在后面的 错误响应 一节中单独讨论。
单服务 vs. 微服务 API后端
一些API可以通过单个后端实现,但是出于弹性或者负载均衡等原因,我们通常期望有不止一个后端。通过微服务的API,我们可以为每个服务定义单独的后端,将他们组合在一起就形成了完整的API。在本文中,我们的仓储API被部署为两个独立的服务,每一个都有多个后端。
upstream warehouse_inventory {
zone inventory_service 64k;
server 10.0.0.1:80;
server 10.0.0.2:80;
server 10.0.0.3:80;
}
upstream warehouse_pricing {
zone pricing_service 64k;
server 10.0.0.7:80;
server 10.0.0.8:80;
server 10.0.0.9:80;
}
由API网关发布的所有API的所有后端API服务均在 api_backends.conf 中被定义。这里我们在每个块中使用了多个IP地址-端口对来指示API代码的部署位置,我们也可以使用主机名来替换IP地址。NGINX Plus 的订阅用户还可以使用动态的DNS负载均衡功能自动地将新的后端添加至在线运行配置。
定义Warehouse API
这部分配置首先定义了Warehouse API的有效URI,然后定义了处理Warehouse API请求所用的通用策略。
# API 定义
#
location /api/warehouse/inventory {
set $upstream warehouse_inventory;
rewrite ^ /_warehouse last;
}
location /api/warehouse/pricing {
set $upstream warehouse_pricing;
rewrite ^ /_warehouse last;
}
# 策略
#
location = /_warehouse {
internal;
set $api_name "Warehouse";
# 在这里配置相应的策略 (认证, 限速, 日志记录, ...)
proxy_pass http://$upstream$request_uri;
}
Warehouse API 通过一系列配置块来定义。NGINX Plus具有灵活和高效的系统,这使得它可以将请求的URI与相应的配置块匹配。一般来说请求会通过具体的路径前缀进行匹配,
location
指令的顺序并不重要。在上面的配置中我们在第三行和第八行定义了两个路径前缀。在每个配置中,
$upstream
变量被设定为分别代表 inventory 和 pricing 的后端API服务。
此处这样配置的目的是将API的定义与API的交付逻辑分离。为了实现这一目标,我们尽量减少了API定义部分的配置内容。当我们为每个 location 确定了合适的 upstream 组之后,可以使用指令来查找相应的API策略。
rewrite
指令的结果是NGINX Plus搜索开头为**/_warehouse**的URI对应的 location 块。上面的配置中使用了 = 修饰符来进行精确匹配,这提升了处理的速度。
在这个阶段,我们的策略块内容非常简单。在配置中的 iternal 意味着客户端不能直接向它发出请求。
$api_name
变量被重新定义为匹配API的名称,以便它可以在日志文件中正常显示。最后请求会通过使用 $request_uri 变量(包含未修改的原始请求URI)代理至API定义部分中指定的 upstreame 组。
API的 宽松定义 vs. 精确定义
API的定义有两种方法——宽松的或者精确的。每个API最适合的方法取决于API的安全要求以及后端服务是否需要处理无效的URI。
在 warehouse_api.simple.conf 文件中,我们使用了宽松的方式来定义Warehouse API。这意味着任何前缀满足要求的URI都会被代理到相应的后端服务,即以下URI的API请求都会被作为有效URI进行处理:
- /api/warehouse/inventory
- /api/warehouse/inventory/
- /api/warehouse/inventory/foo
- /api/warehouse/inventoryfoo
- /api/warehouse/inventoryfoo/bar/
如果我们只需要考虑将每个请求代理到正确的后端服务,那么宽松的定义可以提供最快的处理速度和最紧凑的配置。相对地,使用精确的定义方法可以通过明确定义每个可用API资源的URI路径来了解API的完整URI空间。Warehouse API 的下列配置结合使用完全匹配 ( = ) 和正则表达式 ( ~ ) 实现了对每个URI的精确匹配。
location = /api/warehouse/inventory { # Complete inventory
set $upstream inventory_service;
rewrite ^ /_warehouse last;
}
location ~ ^/api/warehouse/inventory/shelf/[^/]*$ { # Shelf inventory
set $upstream inventory_service;
rewrite ^ /_warehouse last;
}
location ~ ^/api/warehouse/inventory/shelf/[^/]*/box/[^/]*$ { # Box on shelf
set $upstream inventory_service;
rewrite ^ /_warehouse last;
}
location ~ ^/api/warehouse/pricing/[^/]*$ { # Price for specific item
set $upstream pricing_service;
rewrite ^ /_warehouse last;
}
上面的配置虽然啰嗦一点,但是更准确地描述了后端服务实现的资源。这可以使后端服务免受恶意用户请求的影响,但是会增加额外的开销来处理正则表达式的匹配。在这种配置下,NGINX Plus会接受部分URI,其余的会被视为无效而被拒绝:
使用精确的API定义可以利用现有的API文档格式驱动API网关的配置,使OpenAPI规范(过去称为Swagger)下的NGINX Plus API定义自动化。本文配套提供了相应的 示例脚本 。
重写客户端请求
随着API的发展,有时出现的突发情况或变化要求更新客户端的请求。一个典型的例子就是原有的API资源被重命名或者移除。与web浏览器不同,API网关并不能向客户端发送带有API新的命名的重定向。不过幸运的是,我们可以通过重写客户端请求来解决这个问题。
在下面的代码中,我们可以看到在第三行的位置,
pricing
服务之前是作为
inventory
服务的一部分实现的。所以现在我们使用
rewrite
指令来将旧的
pricing
资源请求切换至了对新的
pricing
资源的请求。
# 重写规则
#
rewrite ^/api/warehouse/inventory/item/price/(.*) /api/warehouse/pricing/$1;
# API 定义
#
location /api/warehouse/inventory {
set $upstream inventory_service;
rewrite ^(.*)$ /_warehouse$1 last;
}
location /api/warehouse/pricing {
set $upstream pricing_service;
rewrite ^(.*) /_warehouse$1 last;
}
# 处理策略
#
location /_warehouse {
internal;
set $api_name "Warehouse";
# 在这里配置相应的策略 (认证, 限速, 日志记录, ...)
rewrite ^/_warehouse/(.*)$ /$1 break; # 移除 /_warehouse 前缀
proxy_pass http://$upstream; # 代理重写后的URI
}
不过使用重写URI也意味着在上面代码的倒数第二行我们处理代理请求的时候不能再使用
$request_uri
变量(像
warehouse_api_simple.conf
的第21行的做法一样)。所以我们需要在上述代码的第9行和第14行的位置使用不同的
rewrite
指令之后将URI移交给策略部分的代码块进行处理。
错误响应
基于HTTP API和浏览器的流量之间的一个关键区别是错误传递给客户端的方式。当我们配置NGINX Plus作为API网关时,我们将其配置其以最适合API客户端的方式返回错误信息。
# 错误响应
error_page 404 = @400; # 处理非法URI路径的请求
proxy_intercept_errors on; # 不将后端的错误消息发送给客户端
include api_json_errors.conf; # 定义返回给客户端的JSON响应数据
default_type application/json; # 如果不指定 content-type 则默认为 JSON
上面的代码展示了我们在顶层的API网关中关于错误响应的配置。
由于上面第二行的配置,当请求不能够匹配到任何的API定义时,我们将返回该行定义的错误而不是NGINX Plus默认的错误响应给客户端。这个可选的行为要求客户端按照满足API文档规范的方式进行请求,这避免了未经授权的用户通过API网关发现API的URI结构。
proxy_interceprt_errors
指的是后端服务生成的错误信息。原始的错误信息可能包含着错误的堆栈信息或者其他以及一些其他我们不希望客户端看到的敏感信息。打开这一配置之后,我们将错误信息标准化之后再发送给客户端,从而进一步提升信息的安全级别。
再下一行,我们通过
include
指令引入了错误响应的完整列表,下面展示了其中的前几行。如果你想采用JSON以外的其他错误格式,那么你可以修改最后一行
default_type
指定的内容。你还可以在每个API的策略块中使用
include
指令来导入列表覆盖默认的错误响应。
error_page 400 = @400;
location @400 { return 400 '{"status":400,"message":"Bad request"}\n'; }
error_page 401 = @401;
location @401 { return 401 '{"status":401,"message":"Unauthorized"}\n'; }
error_page 403 = @403;
location @403 { return 403 '{"status":403,"message":"Forbidden"}\n'; }
error_page 404 = @404;
location @404 { return