목록으로

Programming Notes

clientcmd를 이용한 균일한 API 서버 접근

만약 Kubernetes API를 위한 명령줄 클라이언트를 개발해 본 적이 있다면, 특히 클라이언트를 kubectl 플러그인으로 활용할 수 있게 만들고자 했다면, 사용자에게 kubectl 처럼 익숙하게 느껴지도록 어떻게 만들지 궁금했을지도 모릅니다. kubectl options...

만약 Kubernetes API를 위한 명령줄 클라이언트를 개발해 본 적이 있다면, 특히 클라이언트를 kubectl 플러그인으로 활용할 수 있게 만들고자 했다면, 사용자에게 kubectl처럼 익숙하게 느껴지도록 어떻게 만들지 궁금했을지도 모릅니다. kubectl options의 출력물을 보면 "내가 저 모든 옵션을 다 구현해야 하나?"라는 생각에 실망했을지도 모릅니다.

걱정하지 마세요, 다른 사람들이 이미 많은 작업을 대신해 두었습니다. 실제로 Kubernetes 프로젝트는 Go 프로그램에서 kubectl 스타일의 명령줄 인수를 처리하는 데 도움이 되는 두 가지 라이브러리를 제공합니다: clientcmdcli-runtime (이는 clientcmd를 사용합니다). 이 글에서는 전자를 사용하는 방법을 보여줄 것입니다.

일반적인 철학

client-go의 일부인 만큼 예상할 수 있듯이, clientcmd의 궁극적인 목적은 API 서버에 요청을 보낼 수 있는 restclient.Config 인스턴스를 제공하는 것입니다.

이는 kubectl의 다음 의미론을 따릅니다:

  • 기본값은 ~/.kube 또는 그에 상응하는 경로에서 가져옵니다.
  • KUBECONFIG 환경 변수를 사용하여 파일을 지정할 수 있습니다.
  • 위의 모든 설정은 명령줄 인수를 사용하여 다시 재정의될 수 있습니다.

--kubeconfig 명령줄 인수를 직접 설정하지는 않지만, kubectl과 일치시키기 위해 이를 설정하고 싶을 수 있습니다. 이에 대한 내용은 "플래그 바인딩" 섹션에서 살펴보겠습니다.

사용 가능한 기능

clientcmd는 프로그램이 다음을 처리할 수 있도록 합니다.

  • kubeconfig 선택 (KUBECONFIG 사용)
  • 컨텍스트 선택
  • 네임스페이스 선택
  • 클라이언트 인증서 및 개인 키
  • 사용자 가장 (impersonation)
  • HTTP 기본 인증 지원 (사용자 이름/비밀번호)

설정 병합

다양한 시나리오에서 clientcmd는 설정 병합을 지원합니다: KUBECONFIG는 여러 파일을 지정할 수 있으며, 이 파일들의 내용은 결합됩니다. 이것은 혼란스러울 수 있습니다. 설정은 구현 방식에 따라 다른 방향으로 병합되기 때문입니다. 설정이 맵(map)으로 정의되면, 첫 번째 정의가 우선하며 이후 정의는 무시됩니다. 설정이 맵으로 정의되지 않았다면, 마지막 정의가 우선합니다.

KUBECONFIG를 사용하여 설정을 가져올 때, 파일이 누락되면 경고만 발생합니다. 사용자가 명시적으로 경로를 지정하면 (--kubeconfig 스타일로), 해당 파일은 반드시 존재해야 합니다.

KUBECONFIG가 정의되지 않은 경우, 기본 설정 파일인 ~/.kube/config가 존재하면 대신 사용됩니다.

전반적인 과정

일반적인 사용 패턴은 clientcmd 패키지 문서에 간결하게 설명되어 있습니다:

loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// if you want to change the loading rules (which files in which order), you can do so here

configOverrides := &clientcmd.ConfigOverrides{}
// if you want to change override values or bind them to flags, there are methods to help you

kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
 // Do something
}
client, err := metav1.New(config)
// ...

이 글의 맥락에서 다음 여섯 가지 단계가 있습니다:

  1. 로딩 규칙 구성.
  2. 오버라이드 구성.
  3. 플래그 세트 구축.
  4. 플래그 바인딩.
  5. 병합된 설정 구축.
  6. API 클라이언트 얻기.

로딩 규칙 구성

clientcmd.NewDefaultClientConfigLoadingRules()KUBECONFIG 환경 변수의 내용 또는 기본 설정 파일 이름(~/.kube/config)을 사용하는 로딩 규칙을 구축합니다. 또한, 기본 설정 파일이 사용되는 경우, (매우) 오래된 기본 설정 파일 (~/.kube/.kubeconfig)에서 설정을 마이그레이션할 수 있습니다.

자신만의 ClientConfigLoadingRules를 구축할 수 있지만, 대부분의 경우 기본값이 적합합니다.

오버라이드 구성

clientcmd.ConfigOverrides는 로딩 규칙을 사용하여 로드된 설정 위에 적용될 오버라이드를 저장하는 struct입니다. 이 글의 맥락에서, 그 주된 목적은 명령줄 인수로부터 얻은 값을 저장하는 것입니다. 이는 Go의 flag 패키지를 대체하여 긴 이름의 이중 하이픈 인수를 지원하는 pflag 라이브러리를 사용하여 처리됩니다.

대부분의 경우 오버라이드에 설정할 것은 없으며, 단지 플래그에 바인딩할 것입니다.

플래그 세트 구축

이 맥락에서 플래그는 명령줄 인수의 표현으로, 긴 이름(--namespace 등), 짧은 이름(-n 등), 기본값, 그리고 사용 정보에 표시되는 설명을 지정합니다. 플래그는 FlagInfo 구조체 인스턴스에 저장됩니다.

다음 세 가지 플래그 세트를 사용할 수 있으며, 다음 명령줄 인수를 나타냅니다:

  • 인증 인수 (인증서, 토큰, 가장, 사용자 이름/비밀번호)
  • 클러스터 인수 (API 서버, 인증 기관, TLS 설정, 프록시, 압축)
  • 컨텍스트 인수 (클러스터 이름, kubeconfig 사용자 이름, 네임스페이스)

권장되는 선택은 명명된 컨텍스트 선택 인수와 타임아웃 인수와 함께 이 세 가지를 모두 포함합니다.

이들은 모두 Recommended...Flags 함수를 사용하여 사용할 수 있습니다. 이 함수들은 접두사를 받으며, 이 접두사는 모든 인수 긴 이름 앞에 추가됩니다.

따라서 clientcmd.RecommendedConfigOverrideFlags("")를 호출하면 --context, --namespace 등과 같은 명령줄 인수가 생성됩니다. --timeout 인수에는 0의 기본값이 주어지며, --namespace 인수에는 -n이라는 해당하는 짧은 변형이 있습니다. from-과 같은 접두사를 추가하면 --from-context, --from-namespace 등과 같은 명령줄 인수가 생성됩니다. 이는 단일 API 서버와 관련된 명령에서는 특별히 유용해 보이지 않을 수 있지만, 다중 클러스터 시나리오와 같이 여러 API 서버가 관련될 때 유용합니다.

여기서 잠재적인 함정이 있습니다: 접두사는 짧은 이름을 수정하지 않으므로, 여러 접두사가 사용될 경우 --namespace에 주의가 필요합니다. 접두사 중 하나만 -n 짧은 이름과 연결될 수 있습니다. 다른 접두사의 --namespace에 연결된 짧은 이름을 지우거나, 합리적인 -n 연결이 없다면 모든 접두사의 짧은 이름을 지워야 할 수도 있습니다. 짧은 이름은 다음과 같이 지울 수 있습니다:

kflags := clientcmd.RecommendedConfigOverrideFlags(prefix)
kflags.ContextOverrideFlags.Namespace.ShortName = ""

마찬가지로, 플래그의 긴 이름을 지워서 플래그를 완전히 비활성화할 수 있습니다:

kflags.ContextOverrideFlags.Namespace.LongName = ""

플래그 바인딩

플래그 세트가 정의되면, clientcmd.BindOverrideFlags를 사용하여 명령줄 인수를 오버라이드에 바인딩할 수 있습니다. 이를 위해서는 Go의 flag 패키지가 아닌 pflag FlagSet가 필요합니다.

--kubeconfig도 바인딩하려면, 로딩 규칙의 ExplicitPath를 바인딩하여 지금 바로 그렇게 해야 합니다:

flags.StringVarP(&loadingRules.ExplicitPath, "kubeconfig", "", "", "absolute path(s) to the kubeconfig file(s)")

병합된 설정 구축

병합된 설정을 구축하는 데 두 가지 함수를 사용할 수 있습니다:

이름에서 알 수 있듯이, 둘의 차이점은 첫 번째는 제공된 리더를 사용하여 인증 정보를 대화식으로 요청할 수 있는 반면, 두 번째는 호출자가 제공한 정보에만 의존한다는 것입니다.

이 함수 이름에 있는 "지연된(deferred)"이라는 언급은 최종 설정이 가능한 한 늦게 결정된다는 사실을 나타냅니다. 이는 명령줄 인수가 파싱되기 전에 이 함수들을 호출할 수 있으며, 그 결과로 생성되는 설정은 실제로 구성될 때까지 파싱된 모든 값을 사용한다는 것을 의미합니다.

API 클라이언트 얻기

병합된 설정은 ClientConfig 인스턴스로 반환됩니다. ClientConfig() 메서드를 호출하여 API 클라이언트를 얻을 수 있습니다.

어떤 설정도 주어지지 않으면 (KUBECONFIG가 비어 있거나 존재하지 않는 파일을 가리키고, ~/.kube/config가 존재하지 않으며, 명령줄 인수를 통해 설정이 주어지지 않은 경우), 기본 설정은 KUBERNETES_MASTER를 참조하는 모호한 오류를 반환합니다. 이는 레거시 동작입니다. 이를 제거하려는 여러 시도가 있었지만, kubectl--local--dry-run 명령줄 인수를 위해 유지되고 있습니다. clientcmd.IsEmptyConfig()를 호출하여 "빈 설정" 오류를 확인하고 더 명확한 오류 메시지를 제공해야 합니다.

Namespace() 메서드도 유용합니다. 이 메서드는 사용해야 할 네임스페이스를 반환합니다. 또한 사용자가 네임스페이스를 재정의했는지(--namespace 사용) 여부도 나타냅니다.

전체 예시

다음은 완전한 예시입니다.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/spf13/pflag"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	// 로딩 규칙, 기본 설정
	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()

	// 오버라이드 및 플래그 (명령줄 인수) 설정
	configOverrides := &clientcmd.ConfigOverrides{}
	flags := pflag.NewFlagSet("clientcmddemo", pflag.ExitOnError)
	clientcmd.BindOverrideFlags(configOverrides, flags,
		clientcmd.RecommendedConfigOverrideFlags(""))
	flags.StringVarP(&loadingRules.ExplicitPath, "kubeconfig", "", "", "absolute path(s) to the kubeconfig file(s)")
	flags.Parse(os.Args)

	// 클라이언트 구성
	kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
	config, err := kubeConfig.ClientConfig()
	if err != nil {
		if clientcmd.IsEmptyConfig(err) {
			panic("Please provide a configuration pointing to the Kubernetes API server")
		}
		panic(err)
	}
	client, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	// 어떤 네임스페이스를 사용할지 알아내는 방법
	namespace, overridden, err := kubeConfig.Namespace()
	if err != nil {
		panic(err)
	}
	fmt.Printf("Chosen namespace: %s; overridden: %t
", namespace, overridden)

	// 클라이언트를 사용해 봅시다
	nodeList, err := client.CoreV1().Nodes().List(context.TODO(), v1.ListOptions{})
	if err != nil {
		panic(err)
	}
	for _, node := range nodeList.Items {
		fmt.Println(node.Name)
	}
}

즐거운 코딩 되세요! 익숙한 사용 패턴을 가진 도구 구현에 관심을 가져주셔서 감사합니다!