使用 C# 獲取 Kubernetes 叢集資源資訊

微軟技術棧發表於2022-02-15

大家好,我是本期的微軟 MVP 實驗室研究員-嚴振範。今天我將通過程式碼示例為大家分享如何使用 Kubernetes API Server 編寫元件,從 K8S 中獲取叢集的資源物件資訊。
image.png
嚴振範——微軟最有價值專家,目前正在學習微服務相關的知識,可以多交流喲~

前言

前段時間使用 C# 寫了個專案,使用 Kubernetes API Server,獲取資訊以及監控 Kubernetes 資源,然後結合 Neting 做 API 閘道器。

體驗地址 http://neting.whuanle.cn:30080/

賬號 admin,密碼 admin123

640.webp

本篇文章主要介紹,如何通過 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...

640.webp

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

暴露 Kubernetes API Server

首先檢視 kube-system 中執行的 Kubernetes 元件,有個 kube-apiserver-master 正在執行。

root@master:~# kubectl get pods -o wide  -n kube-system
NAME    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 路徑檢視到。

640.webp

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-system
NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
kube-apiserver-master   NodePort    10.101.230.138   <none>        6443:32263/TCP           25s

32263 埠是 Kubernetes 自動分配,每個人的都不一樣。

然後通過 IP:32263 即可測試訪問。

640.webp

如果你的叢集安裝了 CoreDNS,那麼通過其他節點的 IP,也可以訪問到這個服務。

然後將下載的 admin.conf 或者 config 檔案(請改名為 admin.conf),修改裡面的 server 屬性,因為我們此時是通過遠端訪問的。

連線到 API Server

新建一個 MyKubernetes 控制檯專案,然後將 admin.conf 檔案複製放到專案中,隨專案生成輸出。

640.webp

然後在 Nuget 中搜尋 KubernetesClient 包,筆者當前使用的是 7.0.1。

然後在專案中設定環境變數:

640.webp

這個環境變數本身是 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);
        }
    }

640.webp

好了!你已經會獲取 Kubernetes 資源了,開啟入門的第一步!秀兒!

客戶端小知識

雖然開啟了入門的第一步,但是不要急著使用各種 API ,這裡我們來了解一下 Kubernetes 各種資源在客戶端中的定義,和如何解析結構。

首先,在 Kubernetes Client C# 的程式碼中,所有 Kubernetes 資源的模型類,都在 k8s.Models 中記錄。

如果我們要在 Kubernetes 中,檢視一個物件的定義,如 kube-systtem 名稱空間的:

kubectl get namespace kube-system -o yaml
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2021-11-03T13:57:10Z"
  labels:
    kubernetes.io/metadata.name: kube-system
  name: kube-system
  resourceVersion: "33"
  uid: f0c1f00d-2ee4-40fb-b772-665ac2a282d7
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

C# 中,模型的結構與其一模一樣:

640.webp

在客戶端中,模型的名稱以 apiVersion 版本做字首,並且通過 V1NamespaceList 獲取這類物件的列表。

如果要獲取某類資源,其介面都是以 List 開頭的,如 client.ListNamespaceAsync()、

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

看來,學習已經步入正軌了,讓我們來實驗練習吧!

640.webp

實踐1:如何解析一個 Service

這裡筆者貼心給讀者準備了一些練習,第一個練習是解析一個 Service 的資訊出來。
檢視前面建立的 Servicie:

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

對應結構如下:

apiVersion: v1
kind: Service
metadata:
  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-2a2048c6cb12
spec:
  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: NodePort
status:
  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;
    }

輸出結果如下:
640.webp

親,如果你對 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+埠結構。

640.webp

單純獲取 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 。
最終結果如下:
640.webp
通過這部分程式碼,可以解析出 Service 在 External Name、LoadBalancer、NodePort、ClusterIP 等情況下可真正訪問的地址列表。

實踐3:解析 Endpoint 列表

如果對 Endpoint 不太瞭解,親請開啟https://k8s.whuanle.cn/4.netw... 看一下相關知識。
640.webp
在 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)} 頁,按下Enter鍵繼續獲取!");
                Console.ReadKey();
                result = await GetServicesAsync("default", 2, result.ContinueProperty); 
                Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result.Items));
            }
        }
    }

640.webp
640.webp

上面的實踐中,程式碼較多,建議讀者啟動後進行除錯,一步步除錯下來,慢慢檢查資料,對比 Kubernetes 中的各種物件,逐漸加深理解。

下一篇中則會講解如何實現 Conroller 和 Kubernetes Operator。敬請期待!

微軟最有價值專家(MVP)

640.webp

微軟最有價值專家是微軟公司授予第三方技術專業人士的一個全球獎項。29年來,世界各地的技術社群領導者,因其線上上和線下的技術社群中分享專業知識和經驗而獲得此獎項。

MVP是經過嚴格挑選的專家團隊,他們代表著技術最精湛且最具智慧的人,是對社群投入極大的熱情並樂於助人的專家。MVP致力於通過演講、論壇問答、建立網站、撰寫部落格、分享視訊、開源專案、組織會議等方式來幫助他人,並最大程度地幫助微軟技術社群使用者使用 Microsoft 技術。
更多詳情請登入官方網站:
https://mvp.microsoft.com/zh-cn

相關文章