[client-go] GoでEKSにSTSを使ってアクセスする

:::note info
この記事は[ZOZO AdventCalender 2024](https://qiita.com/advent-calendar/2024/zozo)シリーズ5の17日目の記事です。
:::

GoでEKSにアクセスする際、localのkubeconfigを使う方法はよく知られていますが、Lambdaなどのserverless環境の場合はkubeconfigを用意するのが面倒です。  
本記事では、EKSにアクセスする際に、STSを使って一時的なクレデンシャルを取得する方法を紹介します。  
STSを使ってアクセスすることで、kubeconfigを準備する必要がなくなり、Lambdaなどのserverless環境でもclient-goを使ったk8s操作が実行しやすくなります。  

# client-goによるEKS接続
STSを使ったアクセス方法を説明する前に、まずはclient-goでkubeconfigを使ってEKSにアクセスする方法を説明します。  
またCluster内で動作するPodからEKSのリソースにアクセスする場合のconfig取得方法についても触れたいと思います。

## kubeconfigを使ったEKS接続
まずは、kubeconfigを使ってEKSにアクセスする方法を説明します。  
事前に操作したいk8sクラスタへ接続可能なkubeconfigを準備しておきます。  
あとは、コード内でkubeconfigを読み込んでclient-goのclientsetを作成するだけです。  
```go:main.go
package main

import (
  "fmt"
  "os"
  "path/filepath"

  "k8s.io/client-go/kubernetes"
  "k8s.io/client-go/tools/clientcmd"
  "k8s.io/client-go/util/homedir"
)

func main() {
	kubeconfig := os.Getenv("KUBECONFIG")
	if kubeconfig == "" {
		// KUBECONFIGが設定されていない場合はデフォルトのkubeconfigファイルのパスを使用
		kubeconfig = filepath.Join(homedir.HomeDir(), ".kube", "config")
	}
	
	// Kubernetesクライアントのセットアップ
	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		return nil, fmt.Errorf("failed to build config from flags: %w", err)
	}
	
	clientset := kubernetes.NewForConfig(config)
}
```

## Cluster内で動作するPodからk8sリソースにアクセス
Cluster内からk8sリソースにアクセスする場合、`InClusterConfig`を使ってclientsetを作成します。  
```go:main.go
package main

import (
  "k8s.io/client-go/rest"
  "k8s.io/client-go/kubernetes"
)

func main() {
	config, err := rest.InClusterConfig()
	if err != nil {
	  // error処理
	}
	
	clientset := kubernetes.NewForConfig(config)
}
```
[InClusterConfigの実装](https://github.com/kubernetes/client-go/blob/4154d814be868b4bcf610332288a913fca8f4dc1/rest/config.go#L508-L542)は、割とシンプルで環境変数や固定のpathから必要な情報を取得して、client-goのconfigを作成しています。  
権限はマウントされたServiceAccountのものが使われます。

# client-go with STS
次に、STSを使ってEKSにアクセスする方法を説明します。  

## STSとは？
AWS STS (Security Token Service) は、一時的なセキュリティ認証情報（トークン）を生成するAWSサービスです。  
AWS STSを使用すると、以下の3つを提供する一時的な認証情報を取得できます
- アクセスキーID(AccessKeyId)
- シークレットアクセスキー(SecretAccessKey)
- セッショントークン(SessionToken)

参考: [IAMの一時的な認証情報](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_temp.html)

## 必要なIAM権限
前提事項として、適切な権限を持ったAWS Profileの設定が必要です。  
トークンを取得する対象のEKSクラスターの詳細を取得するために、以下のIAM権限が必要になります。  
- eks:DescribeCluster

Lambdaなどのserverless環境で実行する場合は、IAM Roleを作成し、Lambda関数にアタッチすることで権限を付与します。  

## tokenの取得
はじめにawsコマンドを使って、トークンを取得する例を示します。
```bash
$ aws eks get-token --cluster-name <cluster_name>
{
    "kind": "ExecCredential",
    "apiVersion": "client.authentication.k8s.io/v1beta1",
    "spec": {},
    "status": {
        "expirationTimestamp": "2024-12-13T11:56:58Z",
        "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
}
```
適切な権限を持ったAWS Profileを設定している場合、`aws eks get-token`コマンドでトークンを取得できます。  

次にGoでトークンを取得するには、[aws-iam-authenticator](https://github.com/kubernetes-sigs/aws-iam-authenticator)を使ってトークンを取得します。  
`aws-iam-authenticator`は、内部でAWS STSを使ってEKSのトークンを取得します。
```go
package main

import (
  "fmt"
  "log"
  "sigs.k8s.io/aws-iam-authenticator/pkg/token"
)

func main()  {
  g, _ := token.NewGenerator(false, false)
  tk, err := g.Get("<cluster_name>")
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(tk)
}
```

※ これらのコマンドとコードは[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/eks/latest/best-practices/identity-and-access-management.html)にも記載されているので、そちらも参考にしてください。

## client-goでトークンを使ってEKS操作
最後に本題のclient-goでトークンを使ってEKSにアクセスする方法を説明します。  
基本的には、clientsetを作成する際のConfigでトークンを設定するだけですが、HostやCAFileの設定も必要になります。  

client-goによる接続の前に、必要な情報を取得するためのコードを例示します。  
cluster情報の取得する関数定義で、AWS SDKを使って実装しています。  
```go:aws.go
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/eks"
	ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
)

type AWSClient struct {
	eks *eks.Client

	ctx context.Context
}

func NewAWSClient() (*AWSClient, error) {
	cfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion("ap-northeast-1"))
	if err != nil {
		return nil, errors.Join(err, fmt.Errorf("failed to load AWS config"))
	}
	return &AWSClient{
		eks: eks.NewFromConfig(cfg),
		ctx: context.Background(),
	}, nil
}

// cluster情報の取得
func (c *AWSClient) DescriceEKSCluster(clusterName string) (*ekstypes.Cluster, error) {
	out, err := c.eks.DescribeCluster(c.ctx, &eks.DescribeClusterInput{
		Name: aws.String(clusterName),
	})
	if err != nil {
		return nil, errors.Join(err, errors.New("failed to describe eks cluster"))
	}
	return out.Cluster, nil
}
```

次に、client-goでトークンを使ってClientsetを作成する関数を定義します。
```go:k8s.go
package main

import (
	"context"
	"encoding/base64"
	"errors"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/aws-iam-authenticator/pkg/token"
)

func NewClientset(clusterInfo *ekstypes.Cluster) (*kubernetes.Clientset, error) {
	gen, err := token.NewGenerator(false, false)
	if err != nil {
		return nil, errors.Join(err, errors.New("failed to create k8s token generator"))
	}
	token, err := gen.Get(*clusterInfo.Name)
	if err != nil {
		return nil, errors.Join(err, errors.New("failed to get k8s token"))
	}
	ca, err := base64.StdEncoding.DecodeString(*clusterInfo.CertificateAuthority.Data)
	if err != nil {
		return nil, errors.Join(err, errors.New("failed to decode certificate authority"))
	}
	clientset, err := kubernetes.NewForConfig(&rest.Config{
		Host:            *clusterInfo.Endpoint,
		BearerToken:     token.Token,
		TLSClientConfig: rest.TLSClientConfig{CAData: ca},
	})
	if err != nil {
		return nil, errors.Join(err, errors.New("failed to create k8s clientset"))
	}
	return clientset, nil
}
```

最後に、これらの処理を使ってEKSにPodを作成するコードを例示します。  
```go:main.go
package main

func main() {
  awsClient, err := NewAWSClient()
  if err != nil {
    log.Fatal(err)
  }
  clusterInfo, err := awsClient.DescriceEKSCluster("<cluster_name>")
  if err != nil {
    log.Fatal(err)
  }
  clientset, err := NewClientset(clusterInfo)
  if err != nil {
    log.Fatal(err)
  }

  // Podを作成する
  pod := &corev1.Pod{
    // Podの設定
  }
  podsClient := clientset.CoreV1().Pods("<namespace>")
  _, err = podsClient.Create(context.Background(), pod, metav1.CreateOptions{})
  if err != nil {
    log.Fatal(err)
  }
}
```

# まとめ
本記事では、GoでEKSにアクセスする際に、STSを使って一時的なクレデンシャルを取得する方法を紹介しました。  
STSを使ってアクセスすることで、kubeconfigを準備する必要がなくなり、Lambdaなどのserverless環境でもclient-goを使ったk8s操作が実行しやすくなります。  
セキュリティ的にも期限付きの一時的な認証情報を使うことで、漏洩リスクを軽減できるのでオススメです。