在产品开发中调用Kubernetes API接口遇到的几个问题

目录:(可以按w快捷键切换大纲视图)

URL切换,产品提供一个功能就是透传Kuernetes API接口调用,就是要把对https://xx.xx.xx.xx:xx/api/v1/xx/xx/yy/../zz接口的调用变成对https://<kubernetes-ip>:6443/yy/../zz接口(Kubernetes原生接口的调用),开发过程中遇到了一些问题,记录一下。

思路和过程:

  • step1部署Kubernetes,Kubernetes的master节点放在中心云上,worker节点放在边缘云上。中心云的master和边缘云的worker都不通公网,软件部署在有公网的云主机上,软件所在云主机不通master节点,但是可以通worker节点。现在想通过配置代理转发,可以通过访问软件部署的节点的公网去调用Kubernetes接口。
  • step1的代理转发,有5种方案可供选择。方案1: ssh隧道转发。方案2: Iptables NAT转发。方案3: Firewalld NAT转发。方案4: Haproxy转发。方案5: nginx代理。采用最简单的方案1实现公网上的任何一台机器和Kubernetes的API网络可达。
  • step1的代理转发遇到问题1:不通过代理转发https SSL认证没问题,代理转发后出现了https SSL认证问题。
  • step2转换url。转换url有两个可选方案(方案6: nginx url映射。方案7: Spring redirect)
  • step2中的方案7相对方案6更简单,不需要部署nginx和配置nginx,用几句java代码即可实现。选用方案7
  • step2中遇到了问题2: 调用Kuernetes API需要携带token,但是调用产品的接口想把token拿掉
  • 解决问题2采用方案8: 拦截器修改header,添加token字段。失败,记录为问题3。采用方案9: kubectl proxy,方案9解决问题3,不需要携带token字段
  • 方案9带来了2个问题,本地kubectl proxy有效,其他机器kubectl proxy无效,记录为问题4
  • 采用方案7的过程中遇到了问题5: GET请求 redirect没有问题,但是POST请求redirect失败

下面详细说明下采到到三个方案:方案1方案7方案9问题1~5。其他未采用的方案读者可自行网上搜索。

方案1: ssh隧道本地道转发

ssh隧道本地转发介绍

如下图,假如host3和host1、host2都同互相通信,但是host1和host2之间不能通信,如何从host1连接上host2?

对于实现ssh连接来说,实现方式很简单,从host1 ssh到host3,再ssh到host2,也就是将host3作为跳板的方式。但是如果不是ssh,而是http的80端口呢?如何让host1能访问host2的80端口?

ssh支持本地端口转发,语法格式为:

ssh -L [local_bind_addr:]local_port:remote:remote_port middle_host

以上图为例,实现方式是在host1上执行:

ssh -g -L 2222:host2:80 host3

其中”-L”选项表示本地端口转发,其工作方式为:在本地指定一个由ssh监听的转发端口(2222),将远程主机的端口(host2:80)映射为本地端口(2222),当有主机连接本地映射端口(2222)时,本地ssh就将此端口的数据包转发给中间主机(host3),然后host3再与远程主机的端口(host2:80)通信。

现在就可以通过访问host1的2222端口来达到访问host2:80的目的了。

再来解释下”-g”选项,指定该选项表示允许外界主机连接本地转发端口(2222),如果不指定”-g”,则host4将无法通过访问host1:2222达到访问host2:80的目的。甚至,host1自身也不能使用172.16.10.5:2222,而只能使用localhost:2222或127.0.0.1:2222这样的方式达到访问host2:80的目的,之所以如此,是因为本地转发端口默认绑定在回环地址上。可以使用bind_addr来改变转发端口的绑定地址,例如:

# ssh -L 172.16.10.5:2222:host2:80 host3

这样,host1自身就能通过访问172.16.10.5:2222的方式达到访问host2:80的目的。

一般来说,使用转发端口,都建议同时使用”-g”选项,否则将只有自身能访问转发端口。

ssh也支持动态代理,例如:利用socks5代理翻墙:ssh -D 1337 -f -C -q -N root@36.134.56.149 -p 33022
浏览器配置127.0.0.1:1337 socks5代理,就可以访问36.134.56.149能访问的地址了。

具体方案

遇到一个问题,上面的命令就是需要一个终端窗口一直开着,或者终端软件一直开着,可以用nohup 放在服务器后台执行,为了防止ssh被中断,可以加个参数-N,以及修改执行nohup命令的节点的ssh_config文件

nohup ssh -N -g -L 33023:192.168.22.32:6443 183.207.130.11 -p22 &

183.207.130.11 上图边缘云的worker节点ip

192.168.22.32 上图中心云的master节点ip

这样就可以让连上公网的任何位置的机器可以直接访问中心云+边缘云部署的Kubernetes API,访问https://<部署软件的云主机的ip>:33023等同于内部访问https://192.168.22.32:6443

修改/etc/ssh/ssh_config文件
添加一行参数
ServerAliveInterval 12

方案7: Spring redirect

@Slf4j
@Controller
@RequestMapping(value = "/api/v1/xxx/tenants/{tenantId}/vdcs/{vdcId}/k8s/clusters/{poolId}/", produces ="application/json;charset=UTF-8")
@Validated
@RequiresAuthentication
@Transactional
public class PassThroughController {

    private OrgService orgService;

    @Autowired
    public void setService(OrgService orgService) {
        this.orgService = orgService;
    }

    private ProjectService projectService;

    @Autowired
    public void setService(ProjectService projectService) {
        this.projectService = projectService;
    }

    private PoolTenantService poolTenantService;

    @Autowired
    public void setService(PoolTenantService poolTenantService) {
        this.poolTenantService = poolTenantService;
    }

    private K8SService k8sService;

    @Autowired
    public void setService(K8SService k8sService) {
        this.k8sService = k8sService;
    }


    /**
     * Kubernetes原生api透传接口
     * @param tenantId
     * @param vdcId
     * @param poolId
     * @param response
     * @throws IOException
     * @throws ServletException
     */
    @RequestMapping("/**")
    public RedirectView passThrough(@PathVariable String tenantId,
                            @PathVariable String vdcId,
                            @PathVariable String poolId,
                            HttpServletRequest request,
                            HttpServletResponse response) throws IOException, ServletException {

        // url是含id的格式
        if(orgService.selectById(tenantId) == null) {
            throw new BadRequestException("tenantId not found!");
        }
        if(projectService.selectById(vdcId) == null) {
            throw new BadRequestException("vdcId not found!");
        }
        if(k8sService.selectById(poolId) == null) {
            throw new BadRequestException("poolId not found!");
        }

        String url = request.getRequestURL().toString();
        String[] segments = url.split("/");
        String[] newSegments = Arrays.copyOfRange(segments, 13, segments.length);
        String newUrl = String.join("/", newSegments);
        request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, HttpStatus.TEMPORARY_REDIRECT);
        return new RedirectView("http://<软件部署所在的ip>:33024/"+ newUrl);
    }

}

方案8: 拦截器修改header,添加token字段。

/**
 * 拦截器配置
 */
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean modifyParametersFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new ModifyParametersFilter());
        registration.addUrlPatterns("/*");              // 拦截路径
        registration.setName("modifyParametersFilter"); // 拦截器名称
        registration.setOrder(1);                       // 顺序
        return registration;
    }

    /**
     * 自定义拦截器
     */
    class ModifyParametersFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 修改请求头
            Map<String, String> map = new HashMap<>();
            String token2 = "bearer eyJhbGciOiJSUzI1NiIsImtpZCd1Q1QzdTSEYtZU1EM3lqb19LUVJfNm9EY0FyMjI0U0ZhY2RvYnMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tOThiZDciLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImQ4N2U0NzM4LTQ5MzUtNDQxYS05M2Y1LTgzNzUyOGMzNjcwZCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.fhmRUpii4cOFU3ho2XYi1nLsxWMGDHEkS2ZY5RWJB34N40VrycSsq65V5GOOxMObpkn1wqVy2o9vFYTewoZ9FyRmmurnvbxgO2tE6367JQdGrBp0miXaRbF1XRV6eDnoEskoLTJWbKmhLIaTvl9Qg-WGzKLP-tWstO8mYwsEtwja7dBIoRhJE9PW5kQDYP60s9Xwu-oEv9zecXmKeHtoI29nLZ7oUimmNlPpmOqvMlj4BO-gGeiwl3Dkigo28hm9-L5cVM3V-TXwRymK3gFA_oTIFdw_3aMqkgMZxR6QC4cUrziIawlKHb1YDLVwMUeEPwrgb8iU0v3qzxYfQEC9-A";
            map.put("Authorization", token2);
            modifyHeaders(map, request);

            // 修改cookie
            ModifyHttpServletRequestWrapper requestWrapper = new ModifyHttpServletRequestWrapper(request);
            String token = request.getHeader("token");
            if (token != null && !"".equals(token)) {
                requestWrapper.putCookie("SHIROSESSIONID", token);
            }

            // finish
            filterChain.doFilter(requestWrapper, response);
        }
    }

    /**
     * 修改请求头信息
     * @param headerses
     * @param request
     */
    private void modifyHeaders(Map<String, String> headerses, HttpServletRequest request) {
        if (headerses == null || headerses.isEmpty()) {
            return;
        }
        Class<? extends HttpServletRequest> requestClass = request.getClass();
        try {
            Field request1 = requestClass.getDeclaredField("request");
            request1.setAccessible(true);
            Object o = request1.get(request);
            Field coyoteRequest = o.getClass().getDeclaredField("coyoteRequest");
            coyoteRequest.setAccessible(true);
            Object o1 = coyoteRequest.get(o);
            Field headers = o1.getClass().getDeclaredField("headers");
            headers.setAccessible(true);
            MimeHeaders o2 = (MimeHeaders)headers.get(o1);
            for (Map.Entry<String, String> entry : headerses.entrySet()) {
                o2.removeHeader(entry.getKey());
                o2.addValue(entry.getKey()).setString(entry.getValue());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 修改cookie信息
     */
    class ModifyHttpServletRequestWrapper extends HttpServletRequestWrapper {
        private Map<String, String> mapCookies;
        ModifyHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
            this.mapCookies = new HashMap<>();
        }
        public void putCookie(String name, String value) {
            this.mapCookies.put(name, value);
        }
        public Cookie[] getCookies() {
            HttpServletRequest request = (HttpServletRequest) getRequest();
            Cookie[] cookies = request.getCookies();
            if (mapCookies == null || mapCookies.isEmpty()) {
                return cookies;
            }
            if (cookies == null || cookies.length == 0) {
                List<Cookie> cookieList = new LinkedList<>();
                for (Map.Entry<String, String> entry : mapCookies.entrySet()) {
                    String key = entry.getKey();
                    if (key != null && !"".equals(key)) {
                        cookieList.add(new Cookie(key, entry.getValue()));
                    }
                }
                if (cookieList.isEmpty()) {
                    return cookies;
                }
                return cookieList.toArray(new Cookie[cookieList.size()]);
            } else {
                List<Cookie> cookieList = new ArrayList<>(Arrays.asList(cookies));
                for (Map.Entry<String, String> entry : mapCookies.entrySet()) {
                    String key = entry.getKey();
                    if (key != null && !"".equals(key)) {
                        for (int i = 0; i < cookieList.size(); i++) {
                            if(cookieList.get(i).getName().equals(key)){
                                cookieList.remove(i);
                            }
                        }
                        cookieList.add(new Cookie(key, entry.getValue()));
                    }
                }
                return cookieList.toArray(new Cookie[cookieList.size()]);
            }
        }
    }
}

方案9: kubectl proxy

采用一种简单的办法,去掉token。就是在软件部署的节点利用kubeconfig文件,然后使用kubectl proxy代理,让访问Kubernetes API改成访问kubectl proxy

使用 kubectl 代理

下列命令使 kubectl 运行在反向代理模式下。它处理 API 服务器的定位和身份认证。

像这样运行它:

kubectl proxy --port=8080 &

然后你可以通过 curl,wget,或浏览器浏览 API,像这样:

curl http://localhost:8080/api/

输出类似如下:

{
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "10.0.1.149:443"
    }
  ]
}

不使用 kubectl 代理

# 查看所有的集群,因为你的 .kubeconfig 文件中可能包含多个上下文
kubectl config view -o jsonpath='{"Cluster name\tServer\n"}{range .clusters[*]}{.name}{"\t"}{.cluster.server}{"\n"}{end}'

# 从上述命令输出中选择你要与之交互的集群的名称
export CLUSTER_NAME="some_server_name"

# 指向引用该集群名称的 API 服务器
APISERVER=$(kubectl config view -o jsonpath="{.clusters[?(@.name==\"$CLUSTER_NAME\")].cluster.server}")

# 获得令牌
TOKEN=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='default')].data.token}"|base64 -d)

# 使用令牌玩转 API
curl -X GET $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure

输出类似如下:

{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "10.0.1.149:443"
    }
  ]
}

问题1: https认证问题

不通过代理转发直接调用Kubernetes API,https SSL认证没问题,代理转发后出现了https SSL认证问题。

可以采用命令行的curl命令加上-k参数避开,java代码中调用客户端库加入下面的内容
https://github.com/fabric8io/kubernetes-client/blob/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/CredentialsExample.java
configBuilder.withTrustCerts(true).build();

kubectl可以通过加入参数--insecure-skip-tls-verify或替换kubeconfig文件的内容。

kubectl --insecure-skip-tls-verify cluster-info dump
clusters:
- cluster:
    certificate-authority-data: xxxxxx

中的certificate-authority-data: xxxxxx insecure-skip-tls-verify: true

参考kubectl的解决办法,也可以用另一种方法修改java代码中调用客户端库,修改下面代码中的变量String configYAML

public static KubernetesClient fromYamlStringGetKubernetesClient(String configYAML) throws IOException {
    Config config = io.fabric8.kubernetes.client.Config.fromKubeconfig(configYAML);
    config.setTrustCerts(true);
    return new DefaultKubernetesClient(config);
}

问题2: 调用Kuernetes API需要携带token,但是调用产品的接口想把token拿掉

通过方案9: kubectl proxy解决

问题3: 拦截器修改header,添加token字段。失败。

通过方案9: kubectl proxy解决

问题4: 本地kubectl proxy有效,其他机器kubectl proxy无效。

nohup kubectl proxy --port=8080 > /dev/null 2>&1 &
    

改成

nohup kubectl proxy --address='0.0.0.0'  --accept-hosts='^*$' --port=8080 > /dev/null 2>&1 &

后解决。

问题5: GET请求 redirect没有问题,但是POST请求redirect失败

GET请求 redirect没有问题,但是POST请求redirect失败,因为POST请求变成了GET请求
参考下面的几个文章:

后问题解决。即:
return new RedirectView("http://<软件部署所在的ip>:33024/"+ newUrl);前,加入request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, HttpStatus.TEMPORARY_REDIRECT);后问题解决。

参考

方案6: nginx url映射

反向代理适用于很多场合,负载均衡是最普遍的用法。

nginx 作为目前最流行的web服务器之一,可以很方便地实现反向代理。

nginx 反向代理官方文档: NGINX REVERSE PROXY

当在一台主机上部署了多个不同的web服务器,并且需要能在80端口同时访问这些web服务器时,可以使用 nginx 的反向代理功能: 用 nginx 在80端口监听所有请求,并依据转发规则(比较常见的是以 URI 来转发)转发到对应的web服务器上。

例如有 webmail , webcom 以及 webdefault 三个服务器分别运行在 portmail , portcom , portdefault 端口,要实现从80端口同时访问这三个web服务器,则可以在80端口运行 nginx, 然后将 /mail 下的请求转发到 webmail 服务器, 将 /com下的请求转发到 webcom 服务器, 将其他所有请求转发到 webdefault 服务器。

假设服务器域名为example.com,则对应的 nginx http配置如下:

http {
    server {
            server_name example.com;

            location /mail/ {
                    proxy_pass http://example.com:protmail/;
            }

            location /com/ {
                    proxy_pass http://example.com:portcom/main/;
            }

            location / {
                    proxy_pass http://example.com:portdefault;
            }
    }
}

以上的配置会按以下规则转发请求( GET 和 POST 请求都会转发):

http://example.com/mail/ 下的请求转发到 http://example.com:portmail/
http://example.com/com/ 下的请求转发到 http://example.com:portcom/main/
将其它所有请求转发到 http://example.com:portdefault/
需要注意的是,在以上的配置中,webdefault 的代理服务器设置是没有指定URI的,而 webmail 和 webcom 的代理服务器设置是指定了URI的(分别为 / 和 /main/)。
如果代理服务器地址中是带有URI的,此URI会替换掉 location 所匹配的URI部分。
而如果代理服务器地址中是不带有URI的,则会用完整的请求URL来转发到代理服务器。

官方文档描述:

If the URI is specified along with the address, it replaces the part of the request URI that matches the location parameter.
If the address is specified without a URI, or it is not possible to determine the part of URI to be replaced, the full request URI is passed (possibly, modified).

以上配置的转发示例:

http://example.com/mail/index.html -> http://example.com:portmail/index.html
http://example.com/com/index.html -> http://example.com:portcom/main/index.html
http://example.com/mail/static/a.jpg -> http://example.com:portmail/static/a.jpg
http://example.com/com/static/b.css -> http://example.com:portcom/main/static/b.css
http://example.com/other/index.htm -> http://example.com:portdefault/other/index.htm

转载请注明来源,欢迎指出任何有错误或不够清晰的表达。可以邮件至 backendcloud@gmail.com