外部访问k8s中的Web服务方案
Service & Ingress
熟悉 k8s 的同学都知道,k8s 为了能够访问部署在其内部的服务,抽象出一个称为 Service
的对象,这个 Service
对象就好比一组 Pod
(也可理解成一组服务) 的 LoadBalance,这样就避免了每次重启容器 IP 地址变动的问题。
大多数情况,部署在 k8s 中的都是 Web 服务器,为每一个 Web 服务器都去分配一个 LoadBalance 显得过于浪费。为了解决这个问题,k8s 又抽象出一个称为 Ingress
的对象,这个 Ingress
对象可以简单看作一个 Web 反向代理,如下图,可以根据请求域名,请求 path
将请求转发至具体的 Pod
。
那么,根据文章标题「外部访问k8s中的Web服务方案」,似乎使用 Ingress
就能解决,但往往是 "理想很丰满 现实很骨感",Ingress
并不能完全兼容我们目前的情况。
1.0 手动模式
在没使用 k8s 之前,我们是通过 Nginx 作为 Web 的反向代理服务器,Nginx 的配置中有很多 rewrite
规则,lua
脚本;在使用 k8s 之后,这些 Nginx 配置不能完全地迁移到 Ingress
中,所以就用了如下图的方案:
图中 ➀ 的 Nginx 部署在 k8s 集群中,还继续使用之前的配置,不同的是将请求转发至相应的 Service
上。但这有个很大的问题就是每新增一个应用,我都需要手动地查询出 Service
对象的 IP,并增加 Nginx 的配置。随着服务的越拆越微,手动维护的成本就越来越高,自动化的方案就越来越迫切。
1.5 解析请求的方式
此方案就是重请求的 url 中解除拼接出 k8s 的 service name, 并通过 proxy_pass 直接转发。不过可以看出此方案具有一定的局限性,接口的地址必须提前约定好。
# 这里配置 kube-dns 地址
resolver x.x.x.x;
location / {
set $service '';
# 通过 lua 请求请求域名中解析并拼接出 k8s service
rewrite_by_lua '
local host = ngx.var.host;
local m = ngx.re.match(host, "(.+).aihaisi.com")
if m then
ngx.var.service = "my-svc-" .. m[1]
end
';
// 直接转发至k8s 的service name, 这里需要设置下 nginx 使用 k8s 的 kube-dns 进行解析域名
proxy_pass http://$service;
}
也可以不用 lua,用nginx 的 rewrite 正则截取
# 这里配置 kube-dns 地址
resolver x.x.x.x;
location / {
rwrite ^/([a-zA-Z0-9]*)/(.+)$ /$2 break;
proxy_pass http://$1/$2$is_args$args;
}
2.0 自动模式
如下图所示,该方案只是新增了图中 ➁ 的 Nginx Ingress
。利用 Nginx
的泛域名转发,将没有匹配到的域名全部转发给 Ingress
, 再由 Ingress
配置的规则转发至具体的后端服务中。
因为我们大多数情况下的转发配置还是很简单的,使用 Ingress
完全能满足。这样就在创建应用的自动化流程中新增一个 Ingress
资源,而不用去手动维护 Nginx 的配置了。
比如我在 ➀ 中的 Nginx 配置如下:
...
// 具体的 xx.mydomain.com 域名转发至具体的 Service
server {
listen 80;
server_name xx.mydomain.com;
location / {
proxy_pass http://xx;
}
}
// 没有匹配到的 mydomain.com 域名都转发至 ingress
server {
listen 80;
server_name ~^([\w-]+)\.mydomain\.com$;
location / {
proxy_pass http://ingress-controller;
}
}
...
Invalid CORS request
但是在迁移该方案后,在使用 HTTPS
的情况下,访问出现了 403 Invalid CORS request
的错误,出现了跨域的问题,看了后端的 Spring Boot 有个 CORS 的配置:
...
@Configuration
@EnableSpringDataWebSupport
public class WebConfig implements WebMvcConfigurer {
...
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
}
这里的 allowedOrigins
只是 http://localhost:8080
,但为什么使用 HTTP
去访问没问题呢,继续 Debug 跟踪发现 DefaultCorsProcessor
类中的 processRequest
方法有关, processRequest
会对请求做 CORS 的检测:
其中有个 isSameOrigin
方法会取 http header 中的 X-Forwarded-Proto
X-Forwarded-Host
X-Forwarded-Port
去与 header 中的 Origins
地址去对比,一致则能通过;
之前在 ➀ 中的 Nginx 就将这些相关的信息放在 header 中透传给后端了:
...
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
...
看来是 ➁ 中的 Nginx Ingress 没有把这个信息继续透传过去,继续看 Nginx Ingress 源码,发现它是根据一个 golang 的 nginx 模版 nginx.tmpl
生成 Nginx 的配置:
可见模版中对 X-Forwarded-Proto
X-Forwarded-Host
X-Forwarded-Port
进行了重新配置,但这三个变量 $best_http_host
$pass_port
$pass_access_scheme
好像不是 Nginx 的内置变量,继续看是在哪里定义的,发现在 lua_ingress.lua
中有如下定义:
从上面的代码我们可以知道,默认情况下这三个变量直接取了 Nginx 的内置变量 ngx.var.scheme
ngx.var.server_port
ngx.var.http_host
; ➀ 中的 Nginx 承接外部的流量并提供了 HTTP 和 HTTPS,但在将请求代理给 ➁ 中的 Nginx Ingress 使用了 HTTP 协议,所以当你使用 HTTPS 去访问时,Origin
中的 schema 是 HTTPS,而从 ngx.var.scheme
获取到的值是 HTTP,这就是为什么使用 HTTPS 访问会出现跨域的错误了。
所以在配置 Nginx Ingress 时,设置 use-forwarded-headers 为 true(默认是 false),就能使用 ➀ 中 Nginx 透传过来的配置了。
访问客户端真实的 IP
在之前的方案中将 ➀ 中 Nginx $remote_addr
获取到的客户端真实 IP 放在 http header X-Real-IP
传给后端:
proxy_set_header X-Real-IP $remote_addr;
但 ➁ 中的 Nginx Ingress 好像又对 X-Real-IP
做了处理,他直接使用了 $remote_addr
, 这样岂不是获取到的都是 ➀ 中 Nginx 的 IP 了。
但是情况并不是这样的,它确能够获取到真实的 IP,这里就比较奇怪了,找了很久的代码都没发现有对 $remote_addr
变量有处理,后来发现它是直接使用了 Nginx 的 ngx_http_realip 模块进行处理,简单来说就是解析 http header 中的 X-Forwareded-For
中的地址,从而获取真实的 IP,在 ➁ 中的模版中有一段关于 ngx_http_realip 模块的配置:
有关于 ngx_http_realip 模块可以参考我的另一篇文章 ngx_http_realip模块获取客户端真实IP
小结
如果用 2.0 的自动化方案,➀ 中的 Nginx 需要 http header 中传递相关信息,配置如下:
server {
listen 80;
server_name ~^([\w-]+)\.mydomain\.com$;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name ~^([\w-]+)\.mydomain\.com$;
include wildcard_ssl_conf;
location / {
proxy_http_version 1.1;
proxy_pass http://nginx-ingress;
proxy_set_header Host $host;
# spring cors check
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
# websocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# real ip
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
➁ 中的 Nginx Ingress 需要通过 ConfigMap
来配置 use-forwarded-headers:
apiVersion: v1
data:
use-forwarded-headers: "true"
kind: ConfigMap
metadata:
name: nginx-ingress-controller
namespace: nginx-ingress