1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KEP-5339: Plugin for Credentials in ClusterProfileの紹介

Posted at

はじめに
本記事は、KEP-5339 “Plugin for Credentials in ClusterProfile”(SIG-Multicluster)の要点と、EKS を題材にした簡易的な実装例をまとめた紹介記事です。
KEP: https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/5339-clusterprofile-plugin-credentials

概要

  • ClusterProfile の status.credentialProviders[].clusterserver / CA を保持する。
  • コントローラは cp-creds.jsonexec plugin を登録し、実行時に渡される KUBERNETES_EXEC_INFO を入力としてトークンを取得する。
  • .spec.cluster.server を基点に EKS クラスタを特定し、aws eks get-token を呼び出す。
  • 入出力は ExecCredential 形式。

全体フロー(Mermaid)

やってみた

サンプル一式: https://github.com/kahirokunn/cluster-inventory-api/tree/eks-example

1) コントローラ起動

go build  controller_example.go
./controller_example -clusterprofile-provider-file ./cp-creds.json

2) cp-creds.json の中身

-clusterprofile-provider-file のフラグで渡されているjsonファイルの中身です

cp-creds.json
{
  "providers": [
    {
      "name": "eks",
      "execConfig": {
          "apiVersion": "client.authentication.k8s.io/v1beta1",
          "args": null,
          "command": "./eks-aws-auth-plugin.sh",
          "env": null,
          "provideClusterInfo": true
      }
    }
  ]
}

3) ClusterProfile をyamlで見た状態

apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ClusterProfile
metadata:
  name: my-cluster-1
spec:
  displayName: my-cluster-1
  clusterManager:
    name: EKS-Fleet
status:
  credentialProviders:
  - name: eks
    cluster:
      server: https://xxx.gr7.ap-northeast-1.eks.amazonaws.com
      certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1J...

ExecCredential と exec plugin の流れ

  • ExecCredential
    Kubernetes の exec 認証プラグインプロトコル。実行時に 入力は環境変数 KUBERNETES_EXEC_INFO出力は標準出力へ ExecCredential を返します。
    仕様: https://kubernetes.io/docs/reference/access-authn-authz/authentication/#input-and-output-formats

  • kubectl / client-go での利用
    kubeconfigusers[].exec、または本記事のように credentials providerproviders[].execConfig にコマンドを設定すると、認証時に当該コマンドが起動されます。KUBERNETES_EXEC_INFO はその際にプラグインへ渡され、プラグインの標準出力の ExecCredential を kubectl/client-go が消費します。

  • 今回の構成
    providers[].execConfig.command./eks-aws-auth-plugin.sh を指定。プラグインは KUBERNETES_EXEC_INFO.spec.cluster.server と CA を手がかりにクラスタを特定し、最終的な ExecCredential を出力します。

修正セクションだけ差し替えます。(他の章はそのままでOK)

背景:aws eks update-kubeconfig / aws eks get-token の“互換性”について

  • aws eks update-kubeconfig

    • 機能: ローカルの kubeconfig を生成・更新するコマンド。
    • 入力: --name 等でクラスタ名の明示指定が必須。KUBERNETES_EXEC_INFO(ExecCredential の spec.cluster.server / CA)を受け取る仕組みはない。
    • 出力: ExecCredential JSON を標準出力で返さない。kubeconfig ファイルを書き換える動作のみ。
      → 結論: 「入力= KUBERNETES_EXEC_INFO、出力= ExecCredential(JSON)/標準出力」という exec plugin の入出力要件と互換ではない。
  • aws eks get-token

    • 出力: ExecCredential 互換 JSON を標準出力に返す。
    • 入力: --cluster-name の明示指定が必須。KUBERNETES_EXEC_INFO を直接入力として受け取れない(server/CA だけからクラスタ名を解決する機能は持たない)。
      → 結論: 出力は互換だが、入力は互換ではない。KUBERNETES_EXEC_INFOserver/CA)→ cluster-name への解決レイヤを別途用意する必要がある。

EKS用のexec plugin実装

処理の流れ:

  1. KUBERNETES_EXEC_INFO から .spec.cluster.server を取得
  2. ホスト名から region を抽出
  3. 当該リージョンの EKS を列挙し、endpoint 完全一致でクラスタ名を特定
  4. aws eks get-token を実行
eks-aws-auth-plugin.sh
#!/usr/bin/env bash
set -euo pipefail

# -------- utils --------
err() { printf "[eks-exec-credential] %s\n" "$*" >&2; }
need() { command -v "$1" >/dev/null 2>&1 || { err "missing dependency: $1"; exit 1; }; }
normalize_host() { sed -E 's#^https?://##; s#/$##; s#:443$##'; }

need jq
need aws

# --- read ExecCredential ---
if [[ -z "${KUBERNETES_EXEC_INFO:-}" ]]; then
  err "KUBERNETES_EXEC_INFO is empty. set provideClusterInfo: true"
  exit 1
fi

REQ_API_VERSION="$(jq -r '.apiVersion // empty' <<<"$KUBERNETES_EXEC_INFO")"
SERVER="$(jq -r '.spec.cluster.server // empty' <<<"$KUBERNETES_EXEC_INFO")"
if [[ -z "$SERVER" || "$SERVER" == "null" ]]; then
  err "spec.cluster.server is missing in KUBERNETES_EXEC_INFO"
  exit 1
fi

NORM_SERVER="$(printf "%s" "$SERVER" | normalize_host)"

# --- region: infer from server hostname ---
HOST="${NORM_SERVER%%/*}"
REGION="$(printf "%s\n" "$HOST" \
  | sed -nE 's#.*\.([a-z0-9-]+)\.eks(-fips)?\.amazonaws\.com(\.cn)?$#\1#p')"
if [[ -z "$REGION" ]]; then
  err "failed to parse region from server hostname: ${SERVER}"
  err "expected something like ...<random>.<suffix>.<region>.eks.amazonaws.com"
  exit 1
fi

# --- tiny cache: endpoint -> cluster name ---
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/eks-exec-credential"
mkdir -p "$CACHE_DIR"
MAP_CACHE="$CACHE_DIR/endpoint-map-${REGION}.json"
if [[ ! -s "$MAP_CACHE" ]] || ! jq -e . >/dev/null 2>&1 <"$MAP_CACHE"; then
  echo '{}' >"$MAP_CACHE"
fi

lookup_cache() { jq -r --arg k "$NORM_SERVER" '.[$k] // empty' <"$MAP_CACHE"; }
update_cache() {
  local tmp; tmp="$(mktemp)"
  jq --arg k "$NORM_SERVER" --arg v "$1" '.[$k]=$v' "$MAP_CACHE" >"$tmp" && mv "$tmp" "$MAP_CACHE"
}

match_endpoint() {
  local name="$1"
  local ep norm_ep
  ep="$(aws eks describe-cluster --region "$REGION" --name "$name" \
        --query 'cluster.endpoint' --output text 2>/dev/null || true)"
  [[ -z "$ep" || "$ep" == "None" ]] && return 1
  norm_ep="$(printf "%s" "$ep" | normalize_host)"
  [[ "$norm_ep" == "$NORM_SERVER" ]]
}

CLUSTER_NAME=""
# 1) cache hit?
CACHED="$(lookup_cache || true)"
if [[ -n "$CACHED" ]] && match_endpoint "$CACHED"; then
  CLUSTER_NAME="$CACHED"
fi

# 2) enumerate if needed
if [[ -z "$CLUSTER_NAME" ]]; then
  err "resolving cluster in ${REGION} for ${NORM_SERVER}"
  found=""
  while IFS= read -r name; do
    [[ -z "$name" ]] && continue
    if match_endpoint "$name"; then
      found="$name"
      break
    fi
  done < <(aws eks list-clusters --region "$REGION" --output json | jq -r '.clusters[]?')

  if [[ -z "$found" ]]; then
    err "no matching EKS cluster for endpoint: ${SERVER} (region=${REGION})"
    exit 1
  fi
  CLUSTER_NAME="$found"
  update_cache "$CLUSTER_NAME" || true
fi

# --- fetch ExecCredential via aws CLI ---
TOKEN_JSON="$(aws eks get-token --region "$REGION" --cluster-name "$CLUSTER_NAME" --output json)"

printf "%s\n" "$TOKEN_JSON"

コントローラ側の利用例

rest.Config を生成して Clientset を作成するサンプルです。

controller_example.go
package main

import (
	"context"
	"encoding/base64"
	"flag"
	"log"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
	"sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
	"sigs.k8s.io/cluster-inventory-api/pkg/credentials"
)

func main() {
	credentialsProviders := credentials.SetupProviderFileFlag()
	flag.Parse()

	cpCreds, err := credentials.NewFromFile(*credentialsProviders)
	if err != nil {
		log.Fatalf("Got error reading credentials providers: %v", err)
	}

	caPEMBase64 := `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1J...`
	caPEM, err := base64.StdEncoding.DecodeString(caPEMBase64)
	if err != nil {
		log.Fatalf("CA PEM base64 decode failed: %v", err)
	}

	// normally we would get this clusterprofile from the local cluster (maybe a watch?)
	// and we would maintain the restconfigs for clusters we're interested in.
	exampleClusterProfile := v1alpha1.ClusterProfile{
		Spec: v1alpha1.ClusterProfileSpec{
			DisplayName: "My Cluster",
		},
		Status: v1alpha1.ClusterProfileStatus{
			CredentialProviders: []v1alpha1.CredentialProvider{
				{
					Name: "eks",
					Cluster: clientcmdv1.Cluster{
						Server: "https://xxx.gr7.ap-northeast-1.eks.amazonaws.com",
						CertificateAuthorityData: caPEM,
					},
				},
			},
		},
	}

	restConfigForMyCluster, err := cpCreds.BuildConfigFromCP(&exampleClusterProfile)
	if err != nil {
		log.Fatalf("Got error generating restConfig: %v", err)
	}
	log.Printf("Got credentials: %v", restConfigForMyCluster)
	// I can then use this rest.Config to build a k8s client.

	// Build a client and list Pods in the default namespace
	clientset, err := kubernetes.NewForConfig(restConfigForMyCluster)
	if err != nil {
		log.Fatalf("failed to create clientset: %v", err)
	}
	ctx := context.Background()
	pods, err := clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
	if err != nil {
		log.Fatalf("failed to list pods: %v", err)
	}
	log.Printf("default namespace has %d pods", len(pods.Items))
	for i, p := range pods.Items {
		if i >= 10 {
			log.Printf("... (truncated)")
			break
		}
		log.Printf("pod: %s", p.Name)
	}
}

トラブルシュート

  • no matching EKS clusteraws eks list-clusters --region <r> の結果・権限・プロファイルを確認
  • x509: certificate signed by unknown authoritycertificate-authority-data(base64)を確認

補足

ExecCredentialではextensionsを使う事で追加の値を渡せます。

KEP-5339: ClusterProfile の Credentials Plugin では、ExecCredential.extensions を利用する仕様は定義されていません。
関連コード: https://github.com/kubernetes-sigs/cluster-inventory-api/blob/main/pkg/credentials/config.go#L133

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?