external-secrets: PushSecretを使ってKubernetesクラスター間でシークレットを共有する

本記事では、external-secretsのPushSecret機能を使用して、Kubernetesクラスター間でシークレットを共有する方法を解説します。マルチクラスター環境での実運用において、シークレットの同期は重要な課題の一つです。クラスター間でシークレットを安全に共有し、自動的に同期する仕組みを構築することで、運用の効率化とセキュリティの向上を図ることができます。

### 実現したい構成

以下のような構成を実現し、ソースクラスターのシークレットをターゲットクラスターに自動的に同期します：

```mermaid
flowchart LR
    subgraph Cluster-A["ソースクラスター"]
        Secret["Secret"]
        ESO["external-secrets<br>オペレーター"]
        ESO -.->|ウォッチ| Secret
    end

    ESO -->|"Secretをコピー"| ターゲットクラスター
```

### 前提条件

本記事の手順を実行するには、以下のツールが必要です：

- Docker
- k3d
- kubectl
- Helm
- kubectx（オプション: コンテキスト切り替えを簡単にするため）
- kubectl-view-secret (オプション: シークレットの内容確認のため)

## 環境構築

k3dで次の2つクラスターを構築します。

- ソースクラスター: `k3d-source-cluster`
- ターゲットクラスター: `k3d-target-cluster`

構築手順は本記事の本質ではないものの、長くなってしまったので折りたたんであります↓

<details>
<summary>構築手順を見る</summary>

### 共有ネットワークの作成

まず、クラスター間で通信できるように共有ネットワークを作成します。このネットワークは、後ほど作成する2つのKubernetesクラスター間の通信を可能にします。

```bash
docker network create shared-net --subnet 172.28.0.0/16 --gateway 172.28.0.1
```

このコマンドで作成されるネットワークは以下の特徴を持ちます：
- サブネット: 172.28.0.0/16
- ゲートウェイ: 172.28.0.1
- ネットワーク名: shared-net

### ソースクラスターの構築

ソースクラスターは、シークレットを提供する側のクラスターです。以下の設定で構築します。

`source-cluster.yaml`を作成し、以下の内容を記述します：

```yaml
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
  name: source-cluster
servers: 1
agents: 1
image: docker.io/rancher/k3s:v1.30.0-k3s1
kubeAPI:
  host: 0.0.0.0
  hostIP: 127.0.0.1
  hostPort: "6443"
ports:
  - port: 8080:80
    nodeFilters:
      - loadbalancer
registries:
  create:
    name: registry.localhost
    host: 127.0.0.1
    hostPort: "15000"
network: shared-net
options:
  k3d:
    wait: true
  kubeconfig:
    updateDefaultKubeconfig: true
    switchCurrentContext: true
  k3s:
    extraArgs:
      # CIDR範囲の重複回避のため、クラスターごとに異なるCIDRを指定
      - arg: "--cluster-cidr=10.42.0.0/16"
        nodeFilters:
          - server:*
      - arg: "--service-cidr=10.43.0.0/16"
        nodeFilters:
          - server:*
```

この設定ファイルの重要なポイントを解説します：

1. **ネットワーク設定**
   - KubeAPI: ポート6443でホストにバインド
   - ロードバランサー: ポート8080:80をマッピング
   - network: 先ほど作成した共有ネットワークを使用
2. **レジストリ設定**
   - ローカルレジストリを作成（名前: registry.localhost）
   - ポート15000でホストにバインド
3. **CIDR設定**
   - Cluster CIDR: 10.42.0.0/16
   - Service CIDR: 10.43.0.0/16
   （これらはクラスター間でのネットワークの重複を避けるために重要）

クラスターを作成するには、以下のコマンドを実行します：

```bash
k3d cluster create --config source-cluster.yaml
```

### ターゲットクラスターの構築

ターゲットクラスターは、シークレットを受け取る側のクラスターです。ソースクラスターとは異なる設定で構築します。

`target-cluster.yaml`を作成し、以下の内容を記述します：

```yaml
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
  name: target-cluster
servers: 1
agents: 1
image: docker.io/rancher/k3s:v1.30.0-k3s1
kubeAPI:
  host: 0.0.0.0
  hostIP: 127.0.0.1
  hostPort: "6444"  # ソースクラスターと異なるポートを使用
ports:
  - port: 8081:80   # ソースクラスターと異なるポートを使用
    nodeFilters:
      - loadbalancer
registries:
  use:
    - registry.localhost  # ソースクラスターのレジストリを使用
network: shared-net
options:
  k3d:
    wait: true
  kubeconfig:
    updateDefaultKubeconfig: true
    switchCurrentContext: true
  k3s:
    extraArgs:
      # CIDR範囲の重複回避のため、ソースクラスターと異なるCIDRを指定
      - arg: "--cluster-cidr=10.44.0.0/16"
        nodeFilters:
          - server:*
      - arg: "--service-cidr=10.45.0.0/16"
        nodeFilters:
          - server:*
```

この設定ファイルのポイントを解説します：

1. **ソースクラスターとの違い**
   - KubeAPIポート: 6444（ソースクラスターは6443）
   - ロードバランサーポート: 8081:80（ソースクラスターは8080:80）
   - CIDR範囲: 10.44.0.0/16と10.45.0.0/16（ソースクラスターとは異なる範囲）
2. **レジストリ設定**
   - ソースクラスターで作成したレジストリを使用
   - `use` セクションで既存のレジストリを指定
3. **ネットワーク設定**
   - ソースクラスターと同じ共有ネットワークを使用
   - これによりクラスター間の通信が可能に

クラスターを作成：

```bash
k3d cluster create --config target-cluster.yaml
```

## クラスター間通信の検証

### コンテキストの確認

まず、両方のクラスターが正しく作成されたことを確認します：

```bash
kubectl config get-contexts
```

出力例：
```
CURRENT   NAME                 CLUSTER              AUTHINFO                   NAMESPACE
          k3d-source-cluster   k3d-source-cluster   admin@k3d-source-cluster   
*         k3d-target-cluster   k3d-target-cluster   admin@k3d-target-cluster   
```

コンテキストの切り替え方法：
```bash
# ソースクラスターに切り替え
kubectl config use-context k3d-source-cluster

# ターゲットクラスターに切り替え
kubectl config use-context k3d-target-cluster
```

### クラスター間の疎通確認

クラスター間の通信を確認するため、デバッグ用のPodを作成します。

#### 1. デバッグ用Pod定義の作成

`debug-pod.yaml`を作成：
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: debug-pod
spec:
  containers:
  - name: debug
    image: curlimages/curl
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: auth
      mountPath: /tmp/auth
      readOnly: true
  volumes:
  - name: auth
    secret:
      secretName: debug-auth
```

このPodの特徴：
- 軽量なcurlイメージを使用
- 無限スリープで実行し続ける
- 認証情報をボリュームとしてマウント

#### 2. 認証情報の準備

`debug-auth-secret.yaml`を作成：

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: debug-auth
type: Opaque
data:
  client-certificate-data: ... # kubeconfigのtarget-clusterのBase64エンコードされたclient-certificate-data
  client-key-data: ... # kubeconfigのtarget-clusterのBase64エンコードされたclient-key-data
```

認証情報の取得方法：

```bash
# kubeconfigの内容を表示
kubectl config view --raw
```

Base64エンコードされた認証情報をコピーして、debug-auth-secret.yamlに貼り付けます。

#### 3. デバッグ環境のセットアップ

```bash
# ソースクラスターに切り替え
kubectl config use-context k3d-source-cluster

# 認証情報とデバッグPodを作成
kubectl apply -f debug-auth-secret.yaml
kubectl apply -f debug-pod.yaml

# Podの状態を確認
kubectl get pod debug-pod
```

#### 4. 疎通確認の実行

```bash
kubectl exec debug-pod -- curl -k \
  --cert /tmp/auth/client-certificate-data \
  --key /tmp/auth/client-key-data \
  https://k3d-target-cluster-server-0:6443/api/v1/namespaces
```

このコマンドで成功すると、JSONフォーマットでnamespaceのリストが返ってきます。これは以下を意味します：
- ターゲットクラスターのAPIサーバーに到達可能
- 認証が正常に機能している
- クラスター間の基本的な通信が確立されている

</details>

## external-secretsのセットアップ

クラスターの準備が確認できたら、いよいよexternal-secretsのセットアップを行います。

### ソースクラスターへのインストール

```bash
# ソースクラスターのコンテキストに切り替え
kubectl config use-context k3d-source-cluster

helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets \
   external-secrets/external-secrets \
    -n external-secrets \
    --create-namespace
```

### ターゲットクラスターの認証情報設定

ソースクラスターからターゲットクラスターへのアクセスに必要な認証情報を設定します。

**認証情報の取得**

まず、ターゲットクラスターのクライアント証明書情報を取得します：

```bash
kubectl config view --raw

# 以下の情報をメモ
# - client-certificate-data
# - client-key-data
# - certificate-authority-data
```

**認証情報の設定**

`target-cluster-credentials.yaml`を作成：

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: target-cluster-credentials
  namespace: default
type: Opaque
data:
  client-certificate-data: ... # kubeconfigから取得したBase64エンコード済みデータ
  client-key-data: ... # kubeconfigから取得したBase64エンコード済みデータ
```

認証情報をソースクラスターに適用：

```bash
# ソースクラスターのコンテキストに切り替え
kubectl config use-context k3d-source-cluster

# 認証情報の適用
kubectl apply -f target-cluster-credentials.yaml

# 作成されたSecretの確認
kubectl get secret target-cluster-credentials -o yaml
```

### SecretStoreの設定

SecretStoreは、external-secretsがシークレットを保存・取得するためのバックエンドストアを定義します。

**SecretStore定義の作成**

`secret-store.yaml`を作成：

```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: target-cluster
  namespace: default
spec:
  provider:
    kubernetes:
      remoteNamespace: default  # ターゲットクラスターのnamespace
      server:
        url: https://k3d-target-cluster-server-0:6443  # k3d内部のホスト名を使用
        # CA証明書はBase64エンコードされた値を直接指定
        caBundle: ... # kubeconfigから取得したcertificate-authority-data
      auth:
        cert:
          clientCert:
            name: target-cluster-credentials
            key: client-certificate-data
          clientKey:
            name: target-cluster-credentials
            key: client-key-data
```

**SecretStoreの適用と確認**

```bash
# SecretStoreの適用
kubectl apply -f secret-store.yaml

# 状態の確認
kubectl describe secretstore target-cluster

# 期待される出力に含まれる内容
Status:
  Conditions:
    Last Transition Time:  ...
    Message:               SecretStore validated
    Reason:               Valid
    Status:               True
    Type:                 Ready
```

## PushSecretの設定と動作確認

### PushSecretの作成

PushSecretは、ソースクラスターのシークレットをターゲットクラスターに自動的にプッシュするための設定です。

**サンプルシークレットの作成（ソースクラスター）**

```bash
# サンプルシークレットの作成
kubectl create secret generic my-secret \
  --from-literal=username=admin \
  --from-literal=password=supersecret

# 作成されたシークレットの確認
kubectl get secret my-secret -o yaml
```

**PushSecret定義の作成**

`push-secret.yaml`を作成：

```yaml
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
  name: pushsecret-example
  namespace: default
spec:
  # 同期時にプロバイダーの既存のシークレットを上書き
  updatePolicy: Replace
  # PushSecret削除時にプロバイダーのシークレットも削除
  deletionPolicy: Delete
  # 再同期の間隔
  refreshInterval: 10s
  # シークレットをプッシュするSecretStore
  secretStoreRefs:
    - name: target-cluster
      kind: SecretStore
  # 同期対象のSecret
  selector:
    secret:
      name: my-secret  # ソースクラスターでのSecretのname
  data:
    - match:
        secretKey: username  # ソースクラスターでのSecretのkey
        remoteRef:
          remoteKey: my-secret-copy  # ターゲットクラスターでのSecretのname
          property: username-copy    # ターゲットクラスターでのSecretのkey
    - match:
        secretKey: password  # ソースクラスターでのSecretのkey
        remoteRef:
          remoteKey: my-secret-copy  # ターゲットクラスターでのSecretのname
          property: password-copy    # ターゲットクラスターでのSecretのkey
```

**PushSecretの適用**

```bash
kubectl apply -f push-secret.yaml

# 状態の確認
kubectl describe pushsecret pushsecret-example
```

### 動作確認

**ターゲットクラスターでのシークレット確認**

```bash
# ターゲットクラスターに切り替え
kubectl config use-context k3d-target-cluster

# シークレットの確認
kubectl describe secret my-secret-copy

# 期待される出力
Name:         my-secret-copy
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
password-copy:  11 bytes
username-copy:  5 bytes
```

**シークレットの内容確認**

シークレットの値を確認するには、以下のようなコマンドを使用できます：

```bash
# kubectl-view-secretプラグインを使用する場合
kubectl-view-secret my-secret-copy --all

# または、直接Base64デコードする場合
kubectl get secret my-secret-copy -o jsonpath='{.data.username-copy}' | base64 -d
kubectl get secret my-secret-copy -o jsonpath='{.data.password-copy}' | base64 -d
```

期待される出力：
```
password-copy='supersecret'
username-copy='admin'
```

**自動同期の確認**

ソースクラスターでシークレットを更新し、ターゲットクラスターに反映されることを確認：

```bash
# ソースクラスターに切り替え
kubectl config use-context k3d-source-cluster

# シークレットの更新
kubectl create secret generic my-secret \
  --from-literal=username=newadmin \
  --from-literal=password=newsecret \
  --dry-run=client -o yaml | kubectl apply -f -

# ターゲットクラスターに切り替えて確認
kubectl config use-context k3d-target-cluster
kubectl-view-secret my-secret-copy --all
```

### クリーンアップ

**PushSecretの削除**

```bash
# ソースクラスターに切り替え
kubectl config use-context k3d-source-cluster

# PushSecretの削除
kubectl delete pushsecret pushsecret-example

# ターゲットクラスターでシークレットが削除されたことを確認
kubectl config use-context k3d-target-cluster
kubectl get secret my-secret-copy
# "Error from server (NotFound): secrets "my-secret-copy" not found" と表示されれば成功
```

**クラスターの削除**

```bash
# クラスターの削除
k3d cluster delete --config source-cluster.yaml
k3d cluster delete --config target-cluster.yaml

# 共有ネットワークの削除
docker network rm shared-net
```

## まとめ

本記事では、external-secretsのPushSecret機能を使用して、Kubernetesクラスター間でシークレットを共有する方法を詳しく解説しました。実運用では、この機能を活用することで、マルチクラスター環境でのシークレット管理を効率化できると思います！


## 参考リンク

- [external-secrets公式ドキュメント](https://external-secrets.io/)
- [PushSecret API Reference](https://external-secrets.io/v0.9.0/api/pushsecret/)
- [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)