使用 C# 開發 Kubernetes 組件,獲取集群資源資訊
- 2022 年 1 月 24 日
- 筆記
- Kubernetes與Docker, 雲計算與雲原生開發
寫什麼呢
前段時間使用 C# 寫了個項目,使用 Kubernetes API Server,獲取資訊以及監控 Kubernetes 資源,然後結合 Neting 做 API 網關。
體驗地址 //neting.whuanle.cn:30080/
帳號 admin,密碼 admin123
本篇文章主要介紹,如何通過 C# 開發基於 Kuberetes 的應用,實現獲取 Kubernets 中各種資源的資訊,以及實現 Conroller 的前提知識。而在下一篇中則會講解如何實現 Conroller 和 Kubernetes Operator。
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
kubectl 是 Kubernetes 自帶的一個非常強大的控制集群的工具,通過命令行操作去管理整個集群。
Kubernetes 有很多可視化面板,例如 Dashboard,其背後也是調用 apiserver 的 API,相當於前端調後端。
總之,我們使用的各種管理集群的工具,其後端都是 apiserver,通過 apiserver,我們還可以訂製各種各樣的管理集群的工具,例如網格管理工具 istio。騰訊雲、阿里雲等雲平台都提供了在線的 kubernetes 服務,還有控制台可視化操作,也是利用了 apiserver。
你可以參考筆者寫的 Kubernetes 電子書,了解更多://k8s.whuanle.cn/1.basic/5.k8s.html
簡而言之, 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
路徑查看到。
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
即可測試訪問。
如果你的集群安裝了 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: 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# 中,模型的結構與其一模一樣:
在客戶端中,模型的名稱以 apiVersion 版本做前綴,並且通過 V1NamespaceList
獲取這類對象的列表。
如果要獲取某類資源,其介面都是以 List 開頭的,如 client.ListNamespaceAsync()
、client.ListAPIServiceAsync()
、client.ListPodForAllNamespacesAsync()
等。
看來,學習已經步入正軌了,讓我們來實驗練習吧!
如何解析一個 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;
}
輸出結果如下:
親,如果你對 Kubernetes 的網路知識不太清楚,請先打開 //k8s.whuanle.cn/4.network/1.network.html 了解一下呢。
實踐 2
我們知道,一個 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 不太了解,請打開 //k8s.whuanle.cn/4.network/2.endpoint.html 看一下相關知識。
在 Kubernetes 中,Service 不是直接關聯 Pod 的,而是通過 Endpoint 間接代理 Pod。當然除了 Service -> Pod,通過 Endpoint,也可以實現接入集群外的第三方服務。例如資料庫集群不在 Kubernetes 集群中,但是想通過 Kubernetes Service 統一訪問,則可以利用 Endpoint 進行解耦。這裡不多說,讀者可以參考 //k8s.whuanle.cn/4.network/2.endpoint.html 。
這裡這小節中,筆者也將會講解如何在 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 中的各種對象,逐漸加深理解。