admin|使用 C# 获取 Kubernetes 集群资源信息

点击上方蓝字
关注我们
(本文阅读时间:15分钟)
大家好 , 我是本期的微软 MVP 实验室研究员-严振范 。 今天我将通过代码示例为大家分享如何使用 Kubernetes API Server 编写组件 , 从 K8S 中获取集群的资源对象信息 。
微软MVP实验室研究员
【admin|使用 C# 获取 Kubernetes 集群资源信息】admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

严振范
微软最有价值专家 , 目前正在学习微服务相关的知识 , 可以多交流哟~
前言
前段时间使用 C# 写了个项目 , 使用 Kubernetes API Server , 获取信息以及监控 Kubernetes 资源 , 然后结合 Neting 做 API 网关 。
体验地址 http://neting.whuanle.cn:30080/
账号 admin , 密码 admin123
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

本篇文章主要介绍 , 如何通过 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.basic/5.k8s.html
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

简而言之 ,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|使用 C# 获取 Kubernetes 集群资源信息
文章图片

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
NAMETYPE 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 文件复制放到项目中 , 随项目生成输出 。
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

然后在 Nuget 中搜索 KubernetesClient 包 , 笔者当前使用的是 7.0.1 。
然后在项目中设置环境变量:
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

这个环境变量本身是 ASP.NET Core 自带的 , 控制台程序中没有 。
下面写一个方法 , 用于实例化和获取 Kubernetes 客户端:
privatestaticKubernetes GetClient
{
KubernetesClientConfiguration config;
if(Environment.GetEnvironmentVariable( "ASPNETCORE_ENVIRONMENT") == "Development")
{
// 通过配置文件
config = KubernetesClientConfiguration.BuildConfigFromConfigFile( "./admin.conf");
}
else
{
// 通过默认的 Service Account 访问 , 必须在 kubernetes 中运行时才能使用
config = KubernetesClientConfiguration.BuildDefaultConfig;
}
returnnewKubernetes(config);
}
逻辑很简单 , 如果是开发环境 , 则使用 admin.conf 文件访问 , 如果是非开发环境 , 则 BuildDefaultConfig 自动获取访问凭证 , 此方式只在 Pod 中运行时有效 , 利用 Service Account 认证 。
下面测试一下 , 获取全部命名空间:
staticasyncTask Main
{
varclient = GetClient;
varnamespaces = awaitclient.ListNamespaceAsync;
foreach( varitem innamespaces.Items)
{
Console.WriteLine(item.Metadata.Name);
}
}
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

好了!你已经会获取 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# 中 , 模型的结构与其一模一样:
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

在客户端中 , 模型的名称以 apiVersion 版本做前缀 , 并且通过 V1NamespaceList获取这类对象的列表 。
如果要获取某类资源 , 其接口都是以 List 开头的 , 如 client.ListNamespaceAsync、client.ListAPIServiceAsync、client.ListPodForAllNamespacesAsync等 。
看来 , 学习已经步入正轨了 , 让我们来实验练习吧!
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

实践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# 中定义一个这样的模型类:
publicclassServiceInfo
{
///<summary>
///SVC 名称
///</summary>
publicstringName { get; set; } = null!;
///<summary>
///三种类型之一 <see cref="ServiceType"/>
///</summary>
publicstring? ServiceType { get; set; }
///<summary>
///命名空间
///</summary>
publicstringNamespace { get; set; } = null!;
///<summary>
///有些 Service 没有此选项
///</summary>
publicstringClusterIP { get; set; } = null!;
///<summary>
///外网访问 IP
///</summary>
publicstring[]? ExternalAddress { get; set; }
publicIDictionary< string, string>? Labels { get; set; }
publicIDictionary< string, string>? Selector { get; set; }
///<summary>
///name,port
///</summary>
publicList< string>? Ports { get; set; }
publicstring[]? Endpoints { get; set; }
publicDateTime? CreationTime { get; set; }
// 关联的 Pod 以及 pod 的 ip
}
下面 , 指定获取哪个命名空间的 Service 及其关联的 Endpoint 信息 。
staticasyncTask Main
{
varresult = awaitGetServiceAsync( "kube-apiserver-master", "kube-system");
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result));
}
publicstaticasyncTask<ServiceInfo> GetServiceAsync( stringsvcName, stringnamespaceName )
{
varclient = GetClient;
varservice = awaitclient.ReadNamespacedServiceAsync(svcName, namespaceName);
// 获取 service 本身的信息
ServiceInfo info = newServiceInfo
{
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 的信息
varendpoint = awaitclient.ReadNamespacedEndpointsAsync(svcName, namespaceName);
List< string> address = newList< string>;
foreach( varsub inendpoint.Subsets)
{
foreach( varaddr insub.Addresses)
{
foreach( varport insub.Ports)
{
address.Add( $" {addr.Ip}: {port.Port}/ {port.Protocol}" );
}
}
}
info.Endpoints = address.ToArray;
returninfo;
}
输出结果如下:
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

亲 , 如果你对 Kubernetes 的网络知识不太清楚 , 请先打开 https://k8s.whuanle.cn/4.network/1.network.html 了解一下呢 。
实践2:详细解析 Service 属性
我们知道 , 一个 Service 可以关联多个 Pod , 为多个 Pod 提供负载均衡等功能 。 同时 Service 有 externalIP、clusterIP 等属性 , 要真正解析出一个 Service 是比较困难的 。 例如 Service 可以只有端口 , 没有 IP;也可以只使用 DNS 域名访问;也可以不绑定任何 Pod , 可以从 Service A DNS -> Service B IP 间接访问 B;
Service 包含的情况比较多 , 读者可以参考下面这个图 , 下面我们通过代码 , 获取一个 Service 的 IP 和端口信息 , 然后生成对应的 IP+端口结构 。
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

单纯获取 IP 和 端口是没用的 , 因为他们是分开的 , 你获取到的 IP 可能是 Cluter、Node、LoadBalancer 的 , 有可能只是 DNS 没有 IP , 那么你这个端口怎么访问呢?这个时候必须根据一定的规则 , 解析信息 , 筛选无效数据 , 才能得出有用的访问地址 。
首先定义一部分枚举和模型:
publicenumServiceType
{
ClusterIP,
NodePort,
LoadBalancer,
ExternalName
}
///<summary>
///Kubernetes Service 和 IP
///</summary>
publicclassSvcPort
{
// LoadBalancer -> NodePort -> Port -> Target-Port
///<summary>
///127.0.0.1:8080/tcp、127.0.0.1:8080/http
///</summary>
publicstringAddress { get; set; } = null!;
///<summary>
///LoadBalancer、NodePort、Cluster
///</summary>
publicstringType { get; set; } = null!;
publicstringIP { get; set; } = null!;
publicintPort { get; set; }
}
publicclassSvcIpPort
{
publicList<SvcPort>? LoadBalancers { get; set; }
publicList<SvcPort>? NodePorts { get; set; }
publicList<SvcPort>? Clusters { get; set; }
publicstring? ExternalName { get; set; }
}
编写解析代码:
staticasyncTask Main
{
varresult = awaitGetSvcIpsAsync( "kube-apiserver-master", "kube-system");
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result));
}
publicstaticasyncTask<SvcIpPort> GetSvcIpsAsync( stringsvcName, stringnamespaceName )
{
varclient = GetClient;
varservice = awaitclient.ReadNamespacedServiceAsync(svcName, namespaceName);
SvcIpPort svc = newSvcIpPort;
// LoadBalancer
if(service.Spec.Type == nameof(ServiceType.LoadBalancer))
{
svc.LoadBalancers = newList<SvcPort>;
varips = svc.LoadBalancers;
// 负载均衡器 IP
varlbIP = service.Spec.LoadBalancerIP;
varports = service.Spec.Ports.Where(x => x.NodePort != null).ToArray;
foreach( varport inports)
{
ips.Add( newSvcPort
{
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 = newList<SvcPort>;
varips = svc.NodePorts;
// 负载均衡器 IP , 有些情况可以设置 ClusterIP 为 None;也可以手动设置为 None , 只要有公网 IP 就行
varclusterIP = service.Spec.ClusterIP;
varports = service.Spec.Ports.Where(x => x.NodePort != null).ToArray;
foreach( varport inports)
{
ips.Add( newSvcPort
{
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 = newList<SvcPort>;
varips = svc.Clusters;
varclusterIP = service.Spec.ClusterIP;
varports = service.Spec.Ports.ToArray;
foreach( varport inports)
{
ips.Add( newSvcPort
{
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;
}
returnsvc;
}
规则解析比较复杂 , 这里就不详细讲解 , 读者如有疑问 , 可联系笔者讨论 。
主要规则:LoadBalancer -> NodePort -> Port -> Target-Port 。
最终结果如下:
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

通过这部分代码 , 可以解析出 Service 在 External Name、LoadBalancer、NodePort、ClusterIP 等情况下可真正访问的地址列表 。
实践3:解析 Endpoint 列表
如果对 Endpoint 不太了解 , 亲请打开https://k8s.whuanle.cn/4.network/2.endpoint.html 看一下相关知识 。
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

在 Kubernetes 中 , Service 不是直接关联 Pod 的 , 而是通过 Endpoint 间接代理 Pod 。 当然除了 Service -> Pod , 通过 Endpoint , 也可以实现接入集群外的第三方服务 。 例如数据库集群不在 Kubernetes 集群中 , 但是想通过 Kubernetes Service 统一访问 , 则可以利用 Endpoint 进行解耦 。 这里不多说 , 读者可以参考 https://k8s.whuanle.cn/4.network/2.endpoint.html。
这里这小节中 , 笔者也将会讲解如何在 Kubernetes 中分页获取资源 。
首先定义以下模型:
publicclassSvcInfoList
{
///<summary>
///分页属性 , 具有临时有效期 , 具体由 Kubernetes 确定
///</summary>
publicstring? ContinueProperty { get; set; }
///<summary>
///预计剩余数量
///</summary>
publicintRemainingItemCount { get; set; }
///<summary>
///SVC 列表
///</summary>
publicList< SvcInfo> Items { get; set; } = newList<SvcInfo>;
}
publicclassSvcInfo
{
///<summary>
///SVC 名称
///</summary>
publicstringName { get; set; } = null!;
///<summary>
///三种类型之一 <see cref="ServiceType"/>
///</summary>
publicstring? ServiceType { get; set; }
///<summary>
///有些 Service 没有 IP , 值为 None
///</summary>
publicstringClusterIP { get; set; } = null!;
publicDateTime? CreationTime { get; set; }
publicIDictionary< string, string>? Labels { get; set; }
publicIDictionary< string, string>? Selector { get; set; }
///<summary>
///name,port
///</summary>
publicList< string> Ports { get; set; }
publicstring[]? Endpoints { get; set; }
}
Kubernetes 中的分页 , 没有 PageNo、PageSize、Skip、Take 、Limit 这些 , 并且分页可能只是预计 , 不一定完全准确 。
第一次访问获取对象列表时 , 不能使用 ContinueProperty 属性 。
第一次访问 Kubernets 后 , 获取 10 条数据 , 那么 Kubernetes 会返回一个 ContinueProperty 令牌 , 和剩余数量 RemainingItemCount 。
那么我们可以通过 RemainingItemCount 计算大概的分页数字 。 因为 Kubernetes 是不能直接分页的 , 而是通过类似游标的东西 , 记录当前访问的位置 , 然后继续向下获取对象 。 ContinueProperty 保存了当前查询游标的令牌 , 但是这个令牌有效期是几分钟 。
解析方法:
publicstaticasyncTask<SvcInfoList> GetServicesAsync( stringnamespaceName,
intpageSize = 1,
string? continueProperty = null)
{
varclient = GetClient;
V1ServiceList services;
if( string.IsNullOrEmpty(continueProperty))
{
services = awaitclient.ListNamespacedServiceAsync(namespaceName, limit: pageSize);
}
else
{
try
{
services = awaitclient.ListNamespacedServiceAsync(namespaceName,
continueParameter: continueProperty,
limit: pageSize);
}
catch (Microsoft.Rest.HttpOperationException ex)
{
throwex;
}
catch
{
throw;
}
}
SvcInfoList svcList = newSvcInfoList
{
ContinueProperty = services.Metadata.ContinueProperty,
RemainingItemCount = ( int)services.Metadata.RemainingItemCount.GetValueOrDefault,
Items = newList<SvcInfo>
};
List<SvcInfo> svcInfos = svcList.Items;
foreach( varitem inservices.Items)
{
SvcInfo service = newSvcInfo
{
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 = newList< string>;
foreach( varport initem.Spec.Ports)
{
service.Ports.Add( $" {port.Port}: {port.NodePort}/ {port.Protocol}" );
}
}
elseif(item.Spec.Type == nameof(ServiceType.ClusterIP))
{
service.Ports = newList< string>;
foreach( varport initem.Spec.Ports)
{
service.Ports.Add( $" {port.Port}/ {port.Protocol}" );
}
}
varendpoint = awaitclient.ReadNamespacedEndpointsAsync(item.Metadata.Name, namespaceName);
if(endpoint != null&& endpoint.Subsets.Count != 0)
{
List< string> address = newList< string>;
foreach( varsub inendpoint.Subsets)
{
if(sub.Addresses == null) continue;
foreach( varaddr insub.Addresses)
{
foreach( varport insub.Ports)
{
address.Add( $" {addr.Ip}: {port.Port}/ {port.Protocol}" );
}
}
}
service.Endpoints = address.ToArray;
}
svcInfos.Add(service);
}
returnsvcList;
}
规则解析比较复杂 , 这里就不详细讲解 , 读者如有疑问 , 可联系笔者讨论 。
调用方法:
staticasyncTask Main
{
varresult = awaitGetServicesAsync( "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 = awaitGetServicesAsync( "default", 2, result.ContinueProperty);
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(result.Items));
}
}
}
admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

admin|使用 C# 获取 Kubernetes 集群资源信息
文章图片

上面的实践中 , 代码较多 , 建议读者启动后进行调试 , 一步步调试下来 , 慢慢检查数据 , 对比 Kubernetes 中的各种对象 , 逐渐加深理解 。
下一篇中则会讲解如何实现 Conroller 和 Kubernetes Operator 。 敬请期待!
微软最有 价值专家(MVP)
微软最有价值专家是微软公司授予第三方技术专业人士的一个全球奖项 。 29年来 , 世界各地的技术社区领导者 , 因其在线上和线下的技术社区中分享专业知识和经验而获得此奖项 。
MVP是经过严格挑选的专家团队 , 他们代表着技术最精湛且最具智慧的人 , 是对社区投入极大的热情并乐于助人的专家 。 MVP致力于通过演讲、论坛问答、创建网站、撰写博客、分享视频、开源项目、组织会议等方式来帮助他人 , 并最大程度地帮助微软技术社区用户使用 Microsoft 技术 。
更多详情请登录官方网站:
https://mvp.microsoft.com/zh-cn
关注微软中国MSDN
微软MVP , 期待你加入

    推荐阅读