만약 Kubernetes API를 위한 명령줄 클라이언트를 개발해 본 적이 있다면, 특히 클라이언트를 kubectl 플러그인으로 활용할 수 있게 만들고자 했다면, 사용자에게 kubectl처럼 익숙하게 느껴지도록 어떻게 만들지 궁금했을지도 모릅니다. kubectl options의 출력물을 보면 "내가 저 모든 옵션을 다 구현해야 하나?"라는 생각에 실망했을지도 모릅니다.
걱정하지 마세요, 다른 사람들이 이미 많은 작업을 대신해 두었습니다. 실제로 Kubernetes 프로젝트는 Go 프로그램에서 kubectl 스타일의 명령줄 인수를 처리하는 데 도움이 되는 두 가지 라이브러리를 제공합니다: clientcmd와 cli-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)
// ...
이 글의 맥락에서 다음 여섯 가지 단계가 있습니다:
로딩 규칙 구성
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)")
병합된 설정 구축
병합된 설정을 구축하는 데 두 가지 함수를 사용할 수 있습니다:
clientcmd.NewInteractiveDeferredLoadingClientConfigclientcmd.NewNonInteractiveDeferredLoadingClientConfig
이름에서 알 수 있듯이, 둘의 차이점은 첫 번째는 제공된 리더를 사용하여 인증 정보를 대화식으로 요청할 수 있는 반면, 두 번째는 호출자가 제공한 정보에만 의존한다는 것입니다.
이 함수 이름에 있는 "지연된(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)
}
}
즐거운 코딩 되세요! 익숙한 사용 패턴을 가진 도구 구현에 관심을 가져주셔서 감사합니다!