C# - kubectl 명령어 또는 REST API 대신 Kubernetes 클라이언트 라이브러리를 통해 프로그래밍으로 접근
전에 k8s 서비스와 REST API 통신을 하는 방법에 대해 알아봤는데요,
Kubernetes - kube-apiserver와 REST API 통신하는 방법 (Docker Desktop for Windows 환경)
; https://www.sysnet.pe.kr/2/0/12566
이미 고마우신 분들이 ^^ C# 전용 클라이언트를 만들어 두었기 때문에,
kubernetes-client/csharp
; https://github.com/kubernetes-client/csharp
패키지 참조 추가로 간단하게,
// .NET 5 또는 .NET Standard 2.1 이상
Install-Package KubernetesClient
// 물론 C# 뿐만 아니라 Go, Python, Java, JavaScript, Haskell 언어도 공식 클라이언트 라이브러리를 지원합니다.
//
// Access Clusters Using the Kubernetes API
// ; https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-api/
형식 안정성 있는 프로그램이 가능합니다. 예를 들어,
"service account token" 값을 가져오는 kubectl 명령어가 있었는데요,
c:\temp> kubectl get secrets
NAME TYPE DATA AGE
default-token-qnwbl kubernetes.io/service-account-token 3 50d
c:\temp> kubectl describe secret default-token-qnwbl
Name: default-token-qnwbl
Namespace: default
Labels: <none>
Annotations: kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: 6a8f5d0c-db54-4018-b060-f311583546c0
Type: kubernetes.io/service-account-token
Data
====
namespace: 7 bytes
token: eyJhbGciOiJSUzI...[생략]...lzn1lGmAh1w
ca.crt: 1025 bytes
이런 정보를 KubernetesClient 패키지를 이용해 다음과 같이 조회할 수 있습니다.
using k8s;
using System;
using System.Text;
namespace k8sutil
{
class Program
{
static void Main(string[] args)
{
string path = Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\.kube\config");
KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildConfigFromConfigFile(path);
// 또는, KubernetesClientConfiguration config = KubernetesClientConfiguration.BuildDefaultConfig();
IKubernetes client = new Kubernetes(config);
ListSecrets(client);
Console.WriteLine();
/* 혹은, secrets의 이름과 네임스페이스를 알고 있다면 곧바로 쿼리
c:\temp> kubectl get secrets
NAME TYPE DATA AGE
default-token-qnwbl kubernetes.io/service-account-token 3 50d
*/
// var v1secret = client.ReadNamespacedSecret("default-token-qnwbl", "default");
string secretName = client.ListNamespacedSecret("default").Items[0].Metadata.Name;
string namespaceProperty = "default";
var v1secret = client.ReadNamespacedSecret(secretName, namespaceProperty);
foreach (var data in v1secret.Data)
{
Console.WriteLine($"\t{data.Key} == {data.Value.Length} byte(s)");
}
}
private static void ListSecrets(IKubernetes client)
{
var list = client.ListNamespacedSecret("default");
foreach (var item in list.Items)
{
Console.WriteLine($"{nameof(item.ApiVersion)}: {item.ApiVersion}"); // (null)
Console.WriteLine($"{nameof(item.StringData)}: {item.StringData}"); // (null)
Console.WriteLine($"{nameof(item.Type)}: {item.Type}"); // kubernetes.io/service-account-token
Console.WriteLine($"{nameof(item.Kind)}: {item.Kind}"); // (null)
Console.WriteLine($"{nameof(item.Immutable)}: {item.Immutable}"); // (null)
Console.WriteLine();
Console.WriteLine("=== V1ObjectMetadata ===");
Console.WriteLine($"\t{nameof(item.Metadata.Name)}: {item.Metadata.Name}"); // default-token-qnwbl
Console.WriteLine($"\t{nameof(item.Metadata.ClusterName)}: {item.Metadata.ClusterName}"); // (null)
Console.WriteLine($"\t{nameof(item.Metadata.CreationTimestamp)}: {item.Metadata.CreationTimestamp}"); // 2021-01-25 오전 6:01:59
Console.WriteLine($"\t{nameof(item.Metadata.DeletionGracePeriodSeconds)}: {item.Metadata.DeletionGracePeriodSeconds}"); // (null)
Console.WriteLine($"\t{nameof(item.Metadata.GenerateName)}: {item.Metadata.GenerateName}"); // (null)
Console.WriteLine($"\t{nameof(item.Metadata.Generation)}: {item.Metadata.Generation}"); // (null)
Console.WriteLine($"\t{nameof(item.Metadata.NamespaceProperty)}: {item.Metadata.NamespaceProperty}"); // default
Console.WriteLine($"\t{nameof(item.Metadata.SelfLink)}: {item.Metadata.SelfLink}"); // /api/v1/namespaces/default/secrets/default-token-qnwbl
Console.WriteLine($"\t{nameof(item.Metadata.Uid)}: {item.Metadata.Uid}"); // 6c584cda-d9a9-4b5c-8489-7e4365866014
Console.WriteLine();
Console.WriteLine($"=== Annotations ({item.Metadata.Annotations.Count}) ===");
foreach (var (k, v) in item.Metadata.Annotations)
{
Console.WriteLine($"\t{k} == {v}");
}
Console.WriteLine();
Console.WriteLine($"=== Labels ({item.Metadata.Labels?.Count}) ===");
if (item.Metadata.Labels != null)
{
foreach (var (k, v) in item.Metadata.Labels)
{
Console.WriteLine($"\t{k} == {v}");
}
}
Console.WriteLine();
Console.WriteLine($"=== OwnerReferences ({item.Metadata.OwnerReferences?.Count}) ===");
if (item.Metadata.OwnerReferences != null)
{
foreach (var data in item.Metadata.OwnerReferences)
{
PrintOwnerReference(data);
}
}
Console.WriteLine();
Console.WriteLine($"=== ManagedFields ({item.Metadata.ManagedFields?.Count}) ===");
if (item.Metadata.ManagedFields != null)
{
foreach (var data in item.Metadata.ManagedFields)
{
Console.WriteLine($"\t{nameof(data.ApiVersion)}: {data.ApiVersion}"); // v1
Console.WriteLine($"\t{nameof(data.FieldsType)}: {data.FieldsType}"); // FieldsV1
Console.WriteLine($"\t{nameof(data.FieldsV1)}: {data.FieldsV1}"); // ...json...
Console.WriteLine($"\t{nameof(data.Operation)}: {data.Operation}"); // Update
Console.WriteLine($"\t{nameof(data.Manager)}: {data.Manager}"); // Kube-controller-manager
Console.WriteLine($"\t{nameof(data.Time)}: {data.Time}"); // v1
}
}
Console.WriteLine("=== Data ===");
/*
ca.crt == byte []
namespace == byte [] ("default")
token == byte []
*/
foreach (var (k, v) in item.Data)
{
Console.WriteLine($"\t{k} == {PrintValue(v)}");
}
}
}
private static void PrintOwnerReference(k8s.Models.V1OwnerReference data)
{
Console.WriteLine($"\t{nameof(data.ApiVersion)} == {data.ApiVersion}");
Console.WriteLine($"\t{nameof(data.BlockOwnerDeletion)} == {data.BlockOwnerDeletion}");
Console.WriteLine($"\t{nameof(data.Controller)} == {data.Controller}");
Console.WriteLine($"\t{nameof(data.Kind)} == {data.Kind}");
Console.WriteLine($"\t{nameof(data.Name)} == {data.Name}");
Console.WriteLine($"\t{nameof(data.Uid)} == {data.Uid}");
}
private static string PrintValue(byte[] value)
{
return Encoding.UTF8.GetString(value);
}
}
}
출력 결과를 보면,
ApiVersion:
StringData:
Type: kubernetes.io/service-account-token
Kind:
Immutable:
=== V1ObjectMetadata ===
Name: default-token-qnwbl
ClusterName:
CreationTimestamp: 2021-01-25 오전 6:01:59
DeletionGracePeriodSeconds:
GenerateName:
Generation:
NamespaceProperty: default
SelfLink: /api/v1/namespaces/default/secrets/default-token-qnwbl
Uid: 6c584cda-d9a9-4b5c-8489-7e4365866014
=== Annotations (2) ===
kubernetes.io/service-account.name == default
kubernetes.io/service-account.uid == 6a8f5d0c-db54-4018-b060-f311583546c0
=== Labels () ===
=== OwnerReferences () ===
=== ManagedFields (1) ===
ApiVersion: v1
FieldsType: FieldsV1
FieldsV1: {
"f:data": {
".": {},
"f:ca.crt": {},
"f:namespace": {},
"f:token": {}
},
"f:metadata": {
"f:annotations": {
".": {},
"f:kubernetes.io/service-account.name": {},
"f:kubernetes.io/service-account.uid": {}
}
},
"f:type": {}
}
Operation: Update
Manager: kube-controller-manager
Time: 2021-01-25 오전 6:01:59
=== Data ===
ca.crt == -----BEGIN CERTIFICATE-----
MIICyDCCAbCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
AxMKa3Vi...[생략]...pGl6vpH/zMokqw
8FBcTBqYAlRJRiDWefzIy7tEPiSkOnLqtoVhBqL8WK6Xu15xzHJ5yO2yHTs=
-----END CERTIFICATE-----
namespace == default
token == eyJhbGciOiJSUzI1NiIsImtpZCI6IkpaSndaVndMbTByU1NBNnFXZl80ekZibE9zMk5oRHJtYzZSN19aSDNUSEkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2Nv...[생략]...NHgPtK1Z14-fIbt-jouEHsGa4ZZB1J3Xagxy0A4N7GCuNS-5fvn65B1EqVaaUgfbdHtz8E6cb0ZsEtqRvlsIjQYXlHv_3HyXWyF7SINjPRLTlzn1lGmAh1w
ca.crt == 1025 byte(s)
namespace == 7 byte(s)
token == 900 byte(s)
이로부터 "kubectl get secrets" 명령어의 출력 결과와 동일한 값들을 뽑아낼 수 있을 것입니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
참고로, Yaml을 다루는 패키지도 함께 소개합니다. ^^
// aaubry/YamlDotNet
// ; https://github.com/aaubry/YamlDotNet
Install-Package YamlDotNet
자신이 정의한 yaml 규칙이 있다면 YamlDotNet을 이용해 I/O를 하는 것도 괜찮은 방법이겠습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]