Nginx Ingress TCP代理实现

一 前言

一般使用ingress都是代理http流量,但是有些场景希望代理tcp流量,例如:不想占用过多的公网IP。

开源的ingress对tcp支持不是很好,主要原因在于k8s的Ingress没有给tcp留下插入点,可以通过ingress定义 kubectl explain ingress.spec.rules 证实。

ingress http代理简单来说,暴露一个http服务,根据host和path转发用户请求到真正的svc(用户请求带有host)。tpc代理就是暴露一堆端口号,不同的端口对应不同的后端svc。

二 nginx ingress使用

官网 暴露TCP服务 章节,介绍可以通过--tcp-services-configmap暴露tcp服务,具体怎么使用没有实践之前一直不是很理解。

2.1 启动参数

通过chart安装包可以获取nginx-ingress-controller deployment启动配置。

cat <<EOF | kubectl apply -f -
kind: Pod
apiVersion: v1
metadata:
  name: apple-app
  labels:
    app: apple
spec:
  containers:
    - name: apple-app
      image: hashicorp/http-echo
      args:
        - "-text=apple"
---
kind: Service
apiVersion: v1
metadata:
  name: apple-service
spec:
  selector:
    app: apple
  ports:
    - port: 5678
EOF

helm repo add k8s https://kubernetes-charts.storage.googleapis.com
cat <<EOF > tmpconfig.yaml
tcp:
  8080: "default/apple-service:5678"
EOF
#helm template tcpproxy k8s/nginx-ingress -f tmpconfig.yaml
#安装
helm install  tcpproxy k8s/nginx-ingress -f tmpconfig.yaml

deploy脚本示例

# Source: nginx-ingress/templates/controller-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-ingress
    chart: nginx-ingress-1.34.2
    heritage: Helm
    release: tcpproxy
    app.kubernetes.io/component: controller
  name: tcpproxy-nginx-ingress-controller
  annotations:
    {}
spec:
  selector:
    matchLabels:
      app: nginx-ingress
      release: tcpproxy
  replicas: 1
  revisionHistoryLimit: 10
  strategy:
    {}
  minReadySeconds: 0
  template:
    metadata:
      labels:
        app: nginx-ingress
        release: tcpproxy
        app.kubernetes.io/component: controller
    spec:
      dnsPolicy: ClusterFirst
      containers:
        - name: nginx-ingress-controller
          image: "quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0"
          imagePullPolicy: "IfNotPresent"
          args:
            - /nginx-ingress-controller
            - --default-backend-service=default/tcpproxy-nginx-ingress-default-backend
            - --election-id=ingress-controller-leader
            - --ingress-class=nginx
            - --configmap=default/tcpproxy-nginx-ingress-controller
            - --tcp-services-configmap=default/tcpproxy-nginx-ingress-tcp

2.2 查看ConfigMap配置

tcp的相关配置通过configmap存储,需要注意data属性,controller会解析它。

[root@test ~]# kubectl get cm tcpproxy-nginx-ingress-tcp -o yaml
apiVersion: v1
data:
  "8080": default/apple-service:5678
kind: ConfigMap
metadata:
  creationTimestamp: "2020-03-24T02:24:29Z"
  labels:
    app: nginx-ingress
    chart: nginx-ingress-1.34.2
    component: controller
    heritage: Helm
    release: tcpproxy
  name: tcpproxy-nginx-ingress-tcp

查看nginx配置

登陆controller的Pod,直接查看nginx.conf,在最后一行,可以看到nginx代理配置。直接通过curl localhost:8080,可以正常访问服务。

kubectl exec -it tcpproxy-nginx-ingress-controller-7ff6b85d96-h58ww  /bin/sh

nginx.conf示例

    # TCP services
    
    server {
        preread_by_lua_block {
            ngx.var.proxy_upstream_name="tcp-default-apple-service-5678";
        }
        
        listen                  8080;
        
        proxy_timeout           600s;
        proxy_pass              upstream_balancer;
        
    }
    
    # UDP services
    
}

三 实现分析

整体架构可以参考
https://blog.csdn.net/shida_csdn/article/details/84032019

3.1 同步

NGINXController有个channel,所有更新事件通过watch传到这个channel;同时channel通过queue绑定NGINXController的syncIngress,用于处理变更事件。

func (n *NGINXController) syncIngress(interface{}) error {
    n.syncRateLimiter.Accept()

    if n.syncQueue.IsShuttingDown() {
        return nil
    }

    ings := n.store.ListIngresses(nil)
    // pcfg里包含tcpendpoints
    hosts, servers, pcfg := n.getConfiguration(ings)

func (n *NGINXController) getConfiguration(ingresses []*ingress.Ingress) (sets.String, []*ingress.Server, *ingress.Configuration) {
    upstreams, servers := n.getBackendServers(ingresses)
    var passUpstreams []*ingress.SSLPassthroughBackend

    hosts := sets.NewString()
    // ...
    return hosts, servers, &ingress.Configuration{
        Backends:              upstreams,
        Servers:               servers,
        //获取tcp的代理服务
        TCPEndpoints:          n.getStreamServices(n.cfg.TCPConfigMapName, apiv1.ProtocolTCP),
        UDPEndpoints:          n.getStreamServices(n.cfg.UDPConfigMapName, apiv1.ProtocolUDP),
        PassthroughBackends:   passUpstreams,
        BackendConfigChecksum: n.store.GetBackendConfiguration().Checksum,
        ControllerPodsCount:   n.store.GetRunningControllerPodsCount(),
    }
}
func (n *NGINXController) getStreamServices(configmapName string, proto apiv1.Protocol) []ingress.L4Service {
    // 禁止业务上使用的端口,防止跟ingress的内部服务冲突
    rp := []int{
        n.cfg.ListenPorts.HTTP,
        n.cfg.ListenPorts.HTTPS,
        n.cfg.ListenPorts.SSLProxy,
        n.cfg.ListenPorts.Health,
        n.cfg.ListenPorts.Default,
        nginx.ProfilerPort,
        nginx.StatusPort,
        nginx.StreamPort,
    }

    reserverdPorts := sets.NewInt(rp...)
    // 解析tcp configmap的data字段
    for port, svcRef := range configmap.Data {
        externalPort, err := strconv.Atoi(port)
        //。。。
        if reserverdPorts.Has(externalPort) {
            klog.Warningf("Port %d cannot be used for %v stream services. It is reserved for the Ingress controller.", externalPort, proto)
            continue
        }
        nsSvcPort := strings.Split(svcRef, ":")
        //...
        // 获取ns
        svcNs, svcName, err := k8s.ParseNameNS(nsName)
      // 获取svc
        svc, err := n.store.GetService(nsName)
        var endps []ingress.Endpoint
        targetPort, err := strconv.Atoi(svcPort)
        
        svcs = append(svcs, ingress.L4Service{
            Port: externalPort,
            Backend: ingress.L4Backend{
                Name:          svcName,
                Namespace:     svcNs,
                Port:          intstr.FromString(svcPort),
                Protocol:      proto,
                ProxyProtocol: svcProxyProtocol,
            },
            Endpoints: endps,
            Service:   svc,
        })
    }
    // Keep upstream order sorted to reduce unnecessary nginx config reloads.
    sort.SliceStable(svcs, func(i, j int) bool {
        return svcs[i].Port < svcs[j].Port
    })
    return svcs
}

3.2 监听

有关watch的初始化在store.go中实现,当key的名称为tpcconfigmap时,会触发更新。
internal/ingress/controller/store/store.go

    // tcp = tcpproxy-nginx-ingress-tcp
    changeTriggerUpdate := func(name string) bool {
        return name == configmap || name == tcp || name == udp
    }
    
    handleCfgMapEvent := func(key string, cfgMap *corev1.ConfigMap, eventName string) {
        // updates to configuration configmaps can trigger an update
        triggerUpdate := false
        if changeTriggerUpdate(key) {
        // 设置触发更新
            triggerUpdate = true
            recorder.Eventf(cfgMap, corev1.EventTypeNormal, eventName, fmt.Sprintf("ConfigMap %v", key))
            if key == configmap {
                store.setConfig(cfgMap)
            }
        }

        ings := store.listers.IngressWithAnnotation.List()
        for _, ingKey := range ings {
            key := k8s.MetaNamespaceKey(ingKey)
            ing, err := store.getIngress(key)
            if err != nil {
                klog.Errorf("could not find Ingress %v in local store: %v", key, err)
                continue
            }

            if parser.AnnotationsReferencesConfigmap(ing) {
                store.syncIngress(ing)
                continue
            }
            // 触发同步
            if triggerUpdate {
                store.syncIngress(ing)
            }
        }

        if triggerUpdate {
            updateCh.In() <- Event{
                Type: ConfigurationEvent,
                Obj:  cfgMap,
            }
        }
    }

四 问题

1)在tcp configmap手动新增配置,ingress contorller svc会不会动态改变?

  • 更改configmap后,nginx.conf会更新,当然服务也可以访问通。
  • svc,pod不会动态新增端口。

更多文章见:http://huiwq1990.github.io/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容