大家好,我是本期的微软 MVP 实验室研究员-严振范。明天我将通过代码示例为大家分享如何应用 Kubernetes API Server 编写组件,从 K8S 中获取集群的资源对象信息。

严振范——微软最有价值专家,目前正在学习微服务相干的常识,能够多交换哟~

前言

前段时间应用 C# 写了个我的项目,应用 Kubernetes API Server,获取信息以及监控 Kubernetes 资源,而后联合 Neting 做 API 网关。

体验地址 http://neting.whuanle.cn:30080/

账号 admin,明码 admin123

本篇文章次要介绍,如何通过 C# 开发基于Kubernetes 的利用,实现获取 Kubernetes 中各种资源的信息,以及实现 Conroller 的前提常识。

Kubernetes API Server

kube-apiserver 是 k8s 次要过程之一,apiserver 组件公开了 Kubernetes API (HTTP API),apiserver 是 Kubernetes 管制面的前端,咱们能够用 Go、C# 等编程语言写代码,近程调用 Kubernetes,管制集群的运行。apiserver 裸露的 endiont 端口是 6443。

为了管制集群的运行,Kubernetes 官网提供了一个名为 kubectl 的二进制命令行工具,正是 apiserver 提供了接口服务,kubectl 解析用户输出的指令后,向 apiserver 发动 HTTP 申请,再将后果反馈给用户。

kubectl 是 Kubernetes 自带的一个十分弱小的管制集群的工具,通过命令行操作去治理整个集群。
Kubernetes 有很多可视化面板,例如 Dashboard,其背地也是调用 apiserver 的 API,相当于前端调后端。

总之,咱们应用的各种治理集群的工具,其后端都是 apiserver,通过 apiserver,咱们还能够定制各种各样的治理集群的工具,例如网格管理工具 istio。腾讯云、阿里云等云平台都提供了在线的 kubernetes 服务,还有控制台可视化操作,也是利用了 apiserver。

你能够参考笔者写的 Kubernetes 电子书,理解更多:https://k8s.whuanle.cn/1.basi...

简而言之, Kubernetes API Server 是第三方操作 Kubernetes 的入口。

裸露 Kubernetes API Server

首先查看 kube-system 中运行的 Kubernetes 组件,有个 kube-apiserver-master 正在运行。

root@master:~# kubectl get pods -o wide  -n kube-systemNAME    READY   STATUS    RESTARTS         AGE   IP          NODE     NOMINATED NODE   READINESS GATES... ...kube-apiserver-master            1/1     Running   2 (76d ago)      81d   10.0.0.4    master   <none>           <none>... ...

尽管这些组件很重要,然而只会有一个实例,并且以 Pod 模式运行,而不是 Deployment,这些组件只能放在 master 节点运行。

而后查看 admin.conf 文件,能够通过 /etc/kubernetes/admin.conf 或 $HOME/.kube/config 门路查看到。

admin.conf 文件是拜访 Kubernetes API Server 的凭证,通过这个文件,咱们能够应用编程拜访 Kubernetes 的 API 接口。

然而 admin.conf 是很重要的文件,如果是开发环境开发集群,那就轻易造,如果是生产环境,请勿应用,可通过角色绑定等形式限度 API 拜访受权。

而后把 admin.conf 或 config 文件下载到本地。

你能够应用 kubectl edit pods kube-apiserver-master -n kube-system 命令,查看 Kubernetes API Server 的一些配置信息。

因为 Kubernetes API Server 默认是通过集群内拜访的,如果须要近程拜访,则须要裸露到集群外(与是否都在内网无关,与是否在集群内无关)。

将 API Server 裸露到集群外:

kubectl expose pod  kube-apiserver-master --type=NodePort --port=6443 -n kube-system

查看节点随机调配的端口:

root@master:~# kubectl get svc -n kube-systemNAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGEkube-apiserver-master   NodePort    10.101.230.138   <none>        6443:32263/TCP           25s

32263 端口是 Kubernetes 主动调配,每个人的都不一样。

而后通过 IP:32263 即可测试拜访。

如果你的集群装置了 CoreDNS,那么通过其余节点的 IP,也能够拜访到这个服务。

而后将下载的 admin.conf 或者 config 文件(请改名为 admin.conf),批改外面的 server 属性,因为咱们此时是通过近程拜访的。

连贯到 API Server

新建一个 MyKubernetes 控制台我的项目,而后将 admin.conf 文件复制放到我的项目中,随我的项目生成输入。

而后在 Nuget 中搜寻 KubernetesClient 包,笔者以后应用的是 7.0.1。

而后在我的项目中设置环境变量:

这个环境变量自身是 ASP.NET Core 自带的,控制台程序中没有。

上面写一个办法,用于实例化和获取 Kubernetes 客户端:

private static Kubernetes GetClient()    {        KubernetesClientConfiguration config;        if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")        {            // 通过配置文件            config = KubernetesClientConfiguration.BuildConfigFromConfigFile("./admin.conf");        }        else        {            // 通过默认的 Service Account 拜访,必须在 kubernetes 中运行时能力应用            config = KubernetesClientConfiguration.BuildDefaultConfig();         }        return new Kubernetes(config);    }

逻辑很简略,如果是开发环境,则应用 admin.conf 文件拜访,如果是非开发环境,则 BuildDefaultConfig() 主动获取拜访凭证,此形式只在 Pod 中运行时无效,利用 Service Account 认证。

上面测试一下,获取全副命名空间:

static async Task Main()    {        var client = GetClient();        var namespaces  = await client.ListNamespaceAsync();        foreach (var item in namespaces.Items)        {            Console.WriteLine(item.Metadata.Name);        }    }

好了!你曾经会获取 Kubernetes 资源了,关上入门的第一步!秀儿!

客户端小常识

尽管关上了入门的第一步,然而不要急着应用各种 API ,这里咱们来理解一下 Kubernetes 各种资源在客户端中的定义,和如何解析构造。

首先,在 Kubernetes Client C# 的代码中,所有 Kubernetes 资源的模型类,都在 k8s.Models 中记录。

如果咱们要在 Kubernetes 中,查看一个对象的定义,如 kube-systtem 命名空间的:

kubectl get namespace kube-system -o yaml
apiVersion: v1kind: Namespacemetadata:  creationTimestamp: "2021-11-03T13:57:10Z"  labels:    kubernetes.io/metadata.name: kube-system  name: kube-system  resourceVersion: "33"  uid: f0c1f00d-2ee4-40fb-b772-665ac2a282d7spec:  finalizers:  - kubernetesstatus:  phase: Active

C# 中,模型的构造与其截然不同:

在客户端中,模型的名称以 apiVersion 版本做前缀,并且通过 V1NamespaceList 获取这类对象的列表。

如果要获取某类资源,其接口都是以 List 结尾的,如 client.ListNamespaceAsync()、

client.ListAPIServiceAsync()、client.ListPodForAllNamespacesAsync() 等。

看来,学习曾经步入正轨了,让咱们来试验练习吧!

实际1:如何解析一个 Service

这里笔者贴心给读者筹备了一些练习,第一个练习是解析一个 Service 的信息进去。
查看后面创立的 Servicie:

kubectl get svc  kube-apiserver-master -n kube-system -o yaml

对应构造如下:

apiVersion: v1kind: Servicemetadata:  creationTimestamp: "2022-01-24T12:51:32Z"  labels:    component: kube-apiserver    tier: control-plane  name: kube-apiserver-master  namespace: kube-system  resourceVersion: "24215604"  uid: ede0e3df-8ef6-45c6-9a8d-2a2048c6cb12spec:  clusterIP: 10.101.230.138  clusterIPs:  - 10.101.230.138  externalTrafficPolicy: Cluster  internalTrafficPolicy: Cluster  ipFamilies:  - IPv4  ipFamilyPolicy: SingleStack  ports:  - nodePort: 32263    port: 6443    protocol: TCP    targetPort: 6443  selector:    component: kube-apiserver    tier: control-plane  sessionAffinity: None  type: NodePortstatus:  loadBalancer: {}

咱们在 C# 中定义一个这样的模型类:

  public class ServiceInfo    {        /// <summary>        /// SVC 名称        /// </summary>        public string Name { get; set; } = null!;        /// <summary>        /// 三种类型之一 <see cref="ServiceType"/>        /// </summary>        public string? ServiceType { get; set; }        /// <summary>        /// 命名空间        /// </summary>        public string Namespace { get; set; } = null!;        /// <summary>        /// 有些 Service 没有此选项        /// </summary>        public string ClusterIP { get; set; } = null!;        /// <summary>        /// 外网拜访 IP        /// </summary>        public string[]? ExternalAddress { get; set; }        public IDictionary<string, string>? Labels { get; set; }        public IDictionary<string, string>? Selector { get; set; }        /// <summary>        /// name,port        /// </summary>        public List<string>? Ports { get; set; }        public string[]? Endpoints { get; set; }        public DateTime? CreationTime { get; set; }        // 关联的 Pod 以及 pod 的 ip    }

上面,指定获取哪个命名空间的 Service 及其关联的 Endpoint 信息。

 static async Task Main()    {        var result = await GetServiceAsync("kube-apiserver-master","kube-system");        Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result));    }    public static async Task<ServiceInfo> GetServiceAsync(string svcName, string namespaceName)    {        var client = GetClient();        var service = await client.ReadNamespacedServiceAsync(svcName, namespaceName);        // 获取 service 自身的信息        ServiceInfo info = new ServiceInfo        {            Name = service.Metadata.Name,            Namespace = service.Metadata.NamespaceProperty,            ServiceType = service.Spec.Type,            Labels = service.Metadata.Labels,            ClusterIP = service.Spec.ClusterIP,            CreationTime = service.Metadata.CreationTimestamp,            Selector = service.Spec.Selector.ToDictionary(x => x.Key, x => x.Value),            ExternalAddress = service.Spec.ExternalIPs?.ToArray(),        };        // service -> endpoint 的信息        var endpoint = await client.ReadNamespacedEndpointsAsync(svcName, namespaceName);        List<string> address = new List<string>();        foreach (var sub in endpoint.Subsets)        {            foreach (var addr in sub.Addresses)            {                foreach (var port in sub.Ports)                {                    address.Add($"{addr.Ip}:{port.Port}/{port.Protocol}");                }            }        }        info.Endpoints = address.ToArray();        return info;    }

输入后果如下:

亲,如果你对 Kubernetes 的网络常识不太分明,请先关上 https://k8s.whuanle.cn/4.netw... 理解一下呢。

实际2:具体解析 Service 属性

咱们晓得,一个 Service 能够关联多个 Pod,为多个 Pod 提供负载平衡等性能。同时 Service 有 externalIP、clusterIP 等属性,要真正解析出一个 Service 是比拟艰难的。例如 Service 能够只有端口,没有 IP;也能够只应用 DNS 域名拜访;也能够不绑定任何 Pod,能够从 Service A DNS -> Service B IP 间接拜访 B;

Service 蕴含的状况比拟多,读者能够参考上面这个图,上面咱们通过代码,获取一个 Service 的 IP 和端口信息,而后生成对应的 IP+端口构造。

单纯获取 IP 和 端口是没用的,因为他们是离开的,你获取到的 IP 可能是 Cluter、Node、LoadBalancer 的,有可能只是 DNS 没有 IP,那么你这个端口怎么拜访呢?这个时候必须依据肯定的规定,解析信息,筛选有效数据,能力得出有用的拜访地址。
首先定义一部分枚举和模型:

 public enum ServiceType    {        ClusterIP,        NodePort,        LoadBalancer,        ExternalName    }    /// <summary>    /// Kubernetes Service 和 IP    /// </summary>    public class SvcPort    {        // LoadBalancer -> NodePort -> Port -> Target-Port        /// <summary>        /// 127.0.0.1:8080/tcp、127.0.0.1:8080/http        /// </summary>        public string Address { get; set; } = null!;        /// <summary>        /// LoadBalancer、NodePort、Cluster        /// </summary>        public string Type { get; set; } = null!;        public string IP { get; set; } = null!;        public int Port { get; set; }    }    public class SvcIpPort    {        public List<SvcPort>? LoadBalancers { get; set; }        public List<SvcPort>? NodePorts { get; set; }        public List<SvcPort>? Clusters { get; set; }        public string? ExternalName { get; set; }    }

编写解析代码:

static async Task Main()    {        var result = await GetSvcIpsAsync("kube-apiserver-master","kube-system");        Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result));    }    public static async Task<SvcIpPort> GetSvcIpsAsync(string svcName, string namespaceName)    {        var client = GetClient();        var service = await client.ReadNamespacedServiceAsync(svcName, namespaceName);        SvcIpPort svc = new SvcIpPort();        // LoadBalancer        if (service.Spec.Type == nameof(ServiceType.LoadBalancer))        {            svc.LoadBalancers = new List<SvcPort>();            var ips = svc.LoadBalancers;            // 负载均衡器 IP            var lbIP = service.Spec.LoadBalancerIP;            var ports = service.Spec.Ports.Where(x => x.NodePort != null).ToArray();            foreach (var port in ports)            {                ips.Add(new SvcPort                {                    Address = $"{lbIP}:{port.NodePort}/{port.Protocol}",                    IP = lbIP,                    Port = (int)port.NodePort!,                    Type = nameof(ServiceType.LoadBalancer)                });            }        }        if (service.Spec.Type == nameof(ServiceType.LoadBalancer) || service.Spec.Type == nameof(ServiceType.NodePort))        {            svc.NodePorts = new List<SvcPort>();            var ips = svc.NodePorts;            // 负载均衡器 IP,有些状况能够设置 ClusterIP 为 None;也能够手动设置为 None,只有有公网 IP 就行            var clusterIP = service.Spec.ClusterIP;            var ports = service.Spec.Ports.Where(x => x.NodePort != null).ToArray();            foreach (var port in ports)            {                ips.Add(new SvcPort                {                    Address = $"{clusterIP}:{port.NodePort}/{port.Protocol}",                    IP = clusterIP,                    Port = (int)port.NodePort!,                    Type = nameof(ServiceType.NodePort)                });            }        }                // 上面这部分代码是失常的,应用 {} 能够隔离局部代码,防止变量重名        // if (service.Spec.Type == nameof(ServiceType.ClusterIP))        // 如果 Service 没有 Cluster IP,可能应用了无头模式,也有可能不想呈现 ClusterIP        //if(service.Spec.ClusterIP == "None")        {            svc.Clusters = new List<SvcPort>();            var ips = svc.Clusters;            var clusterIP = service.Spec.ClusterIP;            var ports = service.Spec.Ports.ToArray();            foreach (var port in ports)            {                ips.Add(new SvcPort                {                    Address = $"{clusterIP}:{port.Port}/{port.Protocol}",                    IP = clusterIP,                    Port = port.Port,                    Type = nameof(ServiceType.ClusterIP)                });            }        }        if (!string.IsNullOrEmpty(service.Spec.ExternalName))        {            /* NAME            TYPE           CLUSTER-IP       EXTERNAL-IP          PORT(S)     AGE               myapp-svcname   ExternalName   <none>           myapp.baidu.com      <none>      1m               myapp-svcname ->  myapp-svc                拜访 myapp-svc.default.svc.cluster.local,变成 myapp.baidu.com             */            svc.ExternalName = service.Spec.ExternalName;        }        return svc;    }

规定解析比较复杂,这里就不具体解说,读者如有疑难,可分割笔者探讨。

次要规定:LoadBalancer -> NodePort -> Port -> Target-Port 。
最终后果如下:

通过这部分代码,能够解析出 Service 在 External Name、LoadBalancer、NodePort、ClusterIP 等状况下可真正拜访的地址列表。

实际3:解析 Endpoint 列表

如果对 Endpoint 不太理解,亲请关上https://k8s.whuanle.cn/4.netw... 看一下相干常识。

在 Kubernetes 中,Service 不是间接关联 Pod 的,而是通过 Endpoint 间接代理 Pod。当然除了 Service -> Pod,通过 Endpoint,也能够实现接入集群外的第三方服务。例如数据库集群不在 Kubernetes 集群中,然而想通过 Kubernetes Service 对立拜访,则能够利用 Endpoint 进行解耦。这里不多说,读者能够参考 https://k8s.whuanle.cn/4.netw... 。

这里这大节中,笔者也将会解说如何在 Kubernetes 中分页获取资源。

首先定义以下模型:

 public class SvcInfoList    {        /// <summary>        /// 分页属性,具备长期有效期,具体由 Kubernetes 确定        /// </summary>        public string? ContinueProperty { get; set; }        /// <summary>        /// 预计残余数量        /// </summary>        public int RemainingItemCount { get; set; }        /// <summary>        /// SVC 列表        /// </summary>        public List<SvcInfo> Items { get; set; } = new List<SvcInfo>();    }    public class SvcInfo    {        /// <summary>        /// SVC 名称        /// </summary>        public string Name { get; set; } = null!;        /// <summary>        /// 三种类型之一 <see cref="ServiceType"/>        /// </summary>        public string? ServiceType { get; set; }        /// <summary>        /// 有些 Service 没有 IP,值为 None        /// </summary>        public string ClusterIP { get; set; } = null!;        public DateTime? CreationTime { get; set; }        public IDictionary<string, string>? Labels { get; set; }        public IDictionary<string, string>? Selector { get; set; }        /// <summary>        /// name,port        /// </summary>        public List<string> Ports { get; set; }        public string[]? Endpoints { get; set; }    }

Kubernetes 中的分页,没有 PageNo、PageSize、Skip、Take 、Limit 这些,并且分页可能只是预计,不肯定齐全精确。

第一次拜访获取对象列表时,不能应用 ContinueProperty 属性。

第一次拜访 Kubernets 后,获取 10 条数据,那么 Kubernetes 会返回一个 ContinueProperty 令牌,和残余数量 RemainingItemCount。

那么咱们能够通过 RemainingItemCount 计算大略的分页数字。因为 Kubernetes 是不能间接分页的,而是通过相似游标的货色,记录以后拜访的地位,而后持续向下获取对象。ContinueProperty 保留了以后查问游标的令牌,然而这个令牌有效期是几分钟。

解析办法:

public static async Task<SvcInfoList> GetServicesAsync(string namespaceName,                                                            int pageSize = 1,                                                            string? continueProperty = null)    {        var client = GetClient();        V1ServiceList services;        if (string.IsNullOrEmpty(continueProperty))        {            services = await client.ListNamespacedServiceAsync(namespaceName, limit: pageSize);        }        else        {            try            {                services = await client.ListNamespacedServiceAsync(namespaceName,                                                                    continueParameter: continueProperty,                                                                    limit: pageSize);            }            catch (Microsoft.Rest.HttpOperationException ex)            {                throw ex;            }            catch            {                throw;            }        }        SvcInfoList svcList = new SvcInfoList        {            ContinueProperty = services.Metadata.ContinueProperty,            RemainingItemCount = (int)services.Metadata.RemainingItemCount.GetValueOrDefault(),            Items = new List<SvcInfo>()        };        List<SvcInfo> svcInfos = svcList.Items;        foreach (var item in services.Items)        {            SvcInfo service = new SvcInfo            {                Name = item.Metadata.Name,                ServiceType = item.Spec.Type,                ClusterIP = item.Spec.ClusterIP,                Labels = item.Metadata.Labels,                Selector = item.Spec.Selector,                CreationTime = item.Metadata.CreationTimestamp            };            // 解决端口            if (item.Spec.Type == nameof(ServiceType.LoadBalancer) || item.Spec.Type == nameof(ServiceType.NodePort))            {                service.Ports = new List<string>();                foreach (var port in item.Spec.Ports)                {                    service.Ports.Add($"{port.Port}:{port.NodePort}/{port.Protocol}");                }            }            else if (item.Spec.Type == nameof(ServiceType.ClusterIP))            {                service.Ports = new List<string>();                foreach (var port in item.Spec.Ports)                {                    service.Ports.Add($"{port.Port}/{port.Protocol}");                }            }            var endpoint = await client.ReadNamespacedEndpointsAsync(item.Metadata.Name, namespaceName);            if (endpoint != null && endpoint.Subsets.Count != 0)            {                List<string> address = new List<string>();                foreach (var sub in endpoint.Subsets)                {                    if (sub.Addresses == null) continue;                    foreach (var addr in sub.Addresses)                    {                        foreach (var port in sub.Ports)                        {                            address.Add($"{addr.Ip}:{port.Port}/{port.Protocol}");                        }                    }                }                service.Endpoints = address.ToArray();            }            svcInfos.Add(service);        }        return svcList;    }

规定解析比较复杂,这里就不具体解说,读者如有疑难,可分割笔者探讨。
调用办法:

static async Task Main()    {        var result = await GetServicesAsync("default", 2);        Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result.Items));        if (result.RemainingItemCount != 0)        {            while (result.RemainingItemCount != 0)            {                Console.WriteLine($"残余 {result.RemainingItemCount} 条数据,{result.RemainingItemCount / 3 + (result.RemainingItemCount % 3 == 0 ? 0 : 1)} 页,按下回车键持续获取!");                Console.ReadKey();                result = await GetServicesAsync("default", 2, result.ContinueProperty);                 Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result.Items));            }        }    }


下面的实际中,代码较多,倡议读者启动后进行调试,一步步调试下来,缓缓检查数据,比照 Kubernetes 中的各种对象,逐步加深了解。

下一篇中则会解说如何实现 Conroller 和 Kubernetes Operator。敬请期待!

微软最有价值专家(MVP)

微软最有价值专家是微软公司授予第三方技术专业人士的一个寰球奖项。29年来,世界各地的技术社区领导者,因其在线上和线下的技术社区中分享专业知识和教训而取得此奖项。

MVP是通过严格筛选的专家团队,他们代表着技术最精湛且最具智慧的人,是对社区投入极大的激情并乐于助人的专家。MVP致力于通过演讲、论坛问答、创立网站、撰写博客、分享视频、开源我的项目、组织会议等形式来帮忙别人,并最大水平地帮忙微软技术社区用户应用 Microsoft 技术。
更多详情请登录官方网站:
https://mvp.microsoft.com/zh-cn