Service & Ingress

熟悉 k8s 的同学都知道,k8s 为了能够访问部署在其内部的服务,抽象出一个称为 Service 的对象,这个 Service 对象就好比一组 Pod (也可理解成一组服务) 的 LoadBalance,这样就避免了每次重启容器 IP 地址变动的问题。

大多数情况,部署在 k8s 中的都是 Web 服务器,为每一个 Web 服务器都去分配一个 LoadBalance 显得过于浪费。为了解决这个问题,k8s 又抽象出一个称为 Ingress 的对象,这个 Ingress 对象可以简单看作一个 Web 反向代理,如下图,可以根据请求域名,请求 path 将请求转发至具体的 Pod

image.png

那么,根据文章标题「外部访问k8s中的Web服务方案」,似乎使用 Ingress 就能解决,但往往是 “理想很丰满 现实很骨感”,Ingress 并不能完全兼容我们目前的情况。

1.0 手动模式

在没使用 k8s 之前,我们是通过 Nginx 作为 Web 的反向代理服务器,Nginx 的配置中有很多 rewrite 规则,lua 脚本;在使用 k8s 之后,这些 Nginx 配置不能完全地迁移到 Ingress 中,所以就用了如下图的方案:

外部访问k8s中的web服务(旧).png

图中 ➀ 的 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/2is_argsargs;
}

2.0 自动模式

如下图所示,该方案只是新增了图中 ➁ 的 Nginx Ingress。利用 Nginx 的泛域名转发,将没有匹配到的域名全部转发给 Ingress, 再由 Ingress 配置的规则转发至具体的后端服务中。

因为我们大多数情况下的转发配置还是很简单的,使用 Ingress 完全能满足。这样就在创建应用的自动化流程中新增一个 Ingress 资源,而不用去手动维护 Nginx 的配置了。

外部访问k8s中的web服务.png

比如我在 ➀ 中的 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 的检测:

WX20200201-001207@2x.png

其中有个 isSameOrigin 方法会取 http header 中的 X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port 去与 header 中的 Origins 地址去对比,一致则能通过;

WX20200201-001710@2x.png

之前在 ➀ 中的 Nginx 就将这些相关的信息放在 header 中透传给后端了:

...
proxy_set_header X-Forwarded-Proto scheme;
proxy_set_header X-Forwarded-Hosthttp_host;
proxy_set_header X-Forwarded-Port $server_port;
...

看来是 ➁ 中的 Nginx Ingress 没有把这个信息继续透传过去,继续看 Nginx Ingress 源码,发现它是根据一个 golang 的 nginx 模版 nginx.tmpl 生成 Nginx 的配置:

WX20200201-003739@2x.png

可见模版中对 X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port 进行了重新配置,但这三个变量 $best_http_host $pass_port $pass_access_scheme 好像不是 Nginx 的内置变量,继续看是在哪里定义的,发现在 lua_ingress.lua 中有如下定义:

WX20200201-004208@2x.png

从上面的代码我们可以知道,默认情况下这三个变量直接取了 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 模块的配置:

WX20200201-005524@2x.png

有关于 ngx_http_realip 模块可以参考我的另一篇文章 ngx_http_realip模块获取客户端真实IP

小结

如果用 2.0 的自动化方案,➀ 中的 Nginx 需要 http header 中传递相关信息,配置如下:

server {
    listen 80;
    server_name ~^([w-]+).mydomain.com;
    return 301 https://hostrequest_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-Protoscheme;
        proxy_set_header X-Forwarded-Host http_host;
        proxy_set_header X-Forwarded-Portserver_port;
        # websocket
        proxy_set_header Upgrade http_upgrade;
        proxy_set_header Connectionconnection_upgrade;
        # real ip
        proxy_set_header X-Real-IP remote_addr;
        proxy_set_header X-Forwarded-Forproxy_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

文章来源于互联网:外部访问k8s中的Web服务方案

发表评论