Kubernetes开发(7)-实现一个简单的ingress- controller

18 篇文章 3 订阅

今天来撸一个简单的ingress-controller, 用于了解ingress-controller的实现机制。
完整的代码在 simple-ingress-controller

基本逻辑

ingress说的简单点就是url 和 service的对应关系,相当于nginx的upstream,而ingress-controller就是用来管理这些“upstream”的,可以理解为是个动态的反向代理。

基于“动态反向代理”这个概念,实现一个简单的ingress-controller需要2部分,即 动态 + 反向代理,所以可以分开进行实现。

反向代理

同样使用go来写,简单的反向代理golang 有现成的方法,可以参照我之前写的golang的一个简单的webproxy的代码: 简单的webproxy

这里贴下主要代码

// 实际的代理服务功能
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 获取后端的真实服务地址
	backendURL, err := s.routingTables.GetBackend(r.Host, r.URL.Path)
	if err != nil {
		http.Error(w, "upstream server not found", http.StatusNotFound)
		return
	}

	klog.Infof("[webproxy] get proxy request from: %s%s to: %v", r.Host, r.URL.Path, backendURL)
	// 使用 NewSingleHostReverseProxy 进行代理请求
	p := httputil.NewSingleHostReverseProxy(backendURL)
	p.ServeHTTP(w, r)
}

主要使用的就是httputil.NewSingleHostReverseProxy 这个方法,进行反向代理的转发,将用户请求的url转发到真实url 上。

由于只是个简单的webproxy,所以对应的url 和转发策略都是静态的

// 初始化一个新的路由表
func NewRoutingTable() *RoutingTable {
	rt := &RoutingTable{
		//certificatesByHost: make(map[string]map[string]*tls.Certificate),
		Backends: make(map[string][]routingTableBackend),
	}
	// 真实的服务器host + 端口
	rtb, _ := newroutingTableBackend("hello", "127.0.0.1", 12345)
	rt.Backends["www.zyx.com"] = append(rt.Backends["www.zyx.com"], rtb)
	return rt
}

动态

有了一个简单的proxy,接下来就要考虑如何动态的生成 upstream,回忆之前有提到k8s的informer的概念,如何动态感知资源的变化*,而动态的upstream不就是通过监听资源变化动态加载到代理的backend里么,所以ingress-controller的动态操作可以使用informer进行实现。

	// 每10分钟去list一下
	factory := informers.NewSharedInformerFactory(w.client, 10 * time.Minute)
	secretLister := factory.Core().V1().Secrets().Lister()
	serviceLister := factory.Core().V1().Services().Lister()
	ingressLister := factory.Extensions().V1beta1().Ingresses().Lister()

定义好informer factory, 然后根据需要的资源生产监听器,然后设置事件。

	onChange := func() {
		payload := &Payload{
			TLSCertificates: make(map[string]*tls.Certificate),
		}

		// 获得所有的 Ingress
		ingresses, err := ingressLister.List(labels.Everything())
		if err != nil {
			klog.Errorf("[ingress] failed to list ingresses")
			return
		}

		for _, ingress := range ingresses {
			ingressPayload := IngressPayload{
				Ingress: ingress,
			}
			payload.Ingresses = append(payload.Ingresses, ingressPayload)

			for _, rule := range ingress.Spec.Rules {
				
				if rule.HTTP == nil {
					klog.Errorf("[ingress] http rule is nil, do not make the payload")
					continue
				}

				for _, path := range rule.HTTP.Paths {
					// 给 ingressPayload 组装数据
					addBackend(&ingressPayload, rule.Host, path.Path, path.Backend)
				}
			}

			// 证书处理
			for _, rec := range ingress.Spec.TLS {
				if rec.SecretName != "" {
					// 获取证书对应的 secret
					secret, err := secretLister.Secrets(ingress.Namespace).Get(rec.SecretName)
					if err != nil {
						klog.Errorf("[ingress] 获取secret 失败, %v", err)
						continue
					}
					// 加载证书
					cert, err := tls.X509KeyPair(secret.Data["tls.crt"], secret.Data["tls.key"])
					if err != nil {
						klog.Errorf("[ingress] 加载证书失败, %v", err)
						continue
					}

					payload.TLSCertificates[rec.SecretName] = &cert
				}
			}
		}

onChange 这个方法就是 具体的事件处理了,或者说,动态加载ingress 信息到backend 其实就是这个方法做的,考虑到这只是一个最简单的ingress-controller,所以secret、ingress、service 的 Informer都只用这个方法进行处理。

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		informer := factory.Core().V1().Secrets().Informer()
		informer.AddEventHandler(handler)
		informer.Run(ctx.Done())
		wg.Done()
	}()

	wg.Add(1)
	go func() {
		informer := factory.Extensions().V1beta1().Ingresses().Informer()
		informer.AddEventHandler(handler)
		informer.Run(ctx.Done())
		wg.Done()
	}()

	wg.Add(1)
	go func() {
		informer := factory.Core().V1().Services().Informer()
		informer.AddEventHandler(handler)
		informer.Run(ctx.Done())
		wg.Done()
	}()

	wg.Wait()

合并

上面已经有了“反向代理” 和 “动态upstream”的代码了,之后要考虑的就是合并的问题了,将“动态”的结构体指针放在“反向代理”的路由表结构体内就行了,记得加锁,避免线程安全。

watcher的结构体:

// 整体的payload,用于将证书和ingress列表做关联
type Payload struct {
	Ingresses       []IngressPayload
	TLSCertificates map[string]*tls.Certificate
}

// ingress payload, 记录了ingress本体以及他映射的端口
type IngressPayload struct {
	Ingress *extensionsv1beta1.Ingress
	Host    string
	Path    string
	SvcName string
	SvcPort int
}

backend路由表的结构体

// 对应ingress rules里的规则,一个host 会匹配多个path
type RoutingTable struct {
	CertificatesByHost map[string]map[string]*tls.Certificate
	Backends map[string][]routingTableBackend
	Lock *sync.RWMutex
}

// 初始化一个新的路由表
func NewRoutingTable(payload *watcher.Payload) *RoutingTable {
	rt := &RoutingTable{
		CertificatesByHost: make(map[string]map[string]*tls.Certificate),
		Backends: make(map[string][]routingTableBackend),
		Lock: &sync.RWMutex{},
	}
	// 第一次加载数据
	rt.init(payload)
	
	return rt
}

然后在服务启动的时候,分别启动proxy server 和 watcher 服务,分为2个协程启动

	// 初始化 proxy server 
	s := server.NewServer(port, tlsPort)

	// 初始化 watcher 进程
	w := watcher.New(client, func(payload *watcher.Payload) {s.Update(payload)})

	// 多协程启动
	var eg errgroup.Group
	eg.Go(func() error {
		return s.Run(context.TODO())
	})
	eg.Go(func() error {
		return w.Run(context.TODO())
	})
	if err := eg.Wait(); err != nil {
		klog.Fatalf("[ingress] something is wrong: %v", err.Error())
	}

至此,一个简单的ingress-controller就完成了,打包成容器镜像发布到kubernetes内部即可,但需要注意的是,由于该ingress-controller需要watch 和 list的权限,所以在部署的时候需要赋予rbac 的权限。

当然代码只是个demo, 比较粗糙,比如所有informer绑定的都是同一个onchange的方法,比如proxy和watcher 服务是拆成2个协程的,所以加了锁,影响了一点性能等,但拆开编写比较理解,通过这种结构,可以比较容易的编写和理解ingress-controller的概念和原理,实际使用可以添加自己需要的功能,比如tcp/udp的转发,比如支持https的转发,比如混合云场景的使用等。

个人公众号, 分享一些日常开发,运维工作中的日常以及一些学习感悟,欢迎大家互相学习,交流

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值