ぽよメモ

レガシーシステム考古学専攻

Access KeyなしでMinIOを使う in Kubernetes with MinIO Operator

[2025-05-04 追記]
minio-operator v7.1.0より、ServiceAccount Tokenのaudienceに sts.min.io を指定しなければならなくなったので記載を修正しました。 github.com

はじめに

以下の記事を読んで、MinIOのSTSを使うとAccess Keyを毎回作って保存してとかしなくても済むことを知りました。

zenn.dev

ちょうど家にMinIO Operatorを使って構築したクラスタが居たので、上記の記事では検証されていない MinIO Operator を使ったSTSをやってみました。

STSとは

AWSに存在するSecurity Token Serviceの略です。 OpenID ConnectやSAMLなどの別のIdentity Providerへのフェデレーションを行い、 それらの認証トークンからAWSへアクセスするための短命なトークンを発行する方法が主流です。必要なときに都度リクエストして短命なトークンを生成するので、Access Keyなどを作って保存しておく必要がなくなります。

docs.aws.amazon.com

MinIO Operatorでは、KubernetesのService Account Tokenを使ってAssumeRoleWithWebIdentityを行います。Service AccountのトークンはKubernetesによって署名されており、operatorはその署名を検証してService Account Tokenの真正性を確認します。

事前にMinIO Operatorのカスタムリソースである PolicyBinding を作っておくことで、MinIOクラスタ内のアクセスポリシーとServiceAccountの紐付けを行います。operatorはService Account Tokenから得られたアカウント名に対応するPolicyBindingのアクセスポリシーを知り、MinioのAPIを使って一時的なトークンを発行します。

MinIO OperatorのSTSの流れ

そのため、Kubernetesクラスタ内でAccess KeyやAccess SecretをSecretに保存する必要はなくなります。MinioのAPIへのアクセスをするアプリケーションをデプロイする前に、対応するPolicyとPolicyBinding・ServiceAccountを用意してアプリケーションの環境変数として与えるだけでアクセスできるようになります。 正しいService Account以外からのリクエストに対してはトークンが発行されず、MinIOへのアクセスはできません。

準備

今回はkindで作ったクラスタで試します。STSのためにはTLSが必須となり、署名検証を無効にせずに使うためにはcert-managerおよびtrust-managerを用いるのがよいです。いずれもhelmで簡単にインストールできます。

k8sクラスタをセットアップ

kindで建てたクラスタにhelmを使ってcert-managerとtrust-managerをインストールします。trust-managerについて詳しくは過去記事をご覧下さい。 簡単に言うとクラスタ内にCA証明書をばらまく機能を持ったカスタムコントローラです。

poyo.hatenablog.jp

kind create cluster --name minio-sts
helm repo add jetstack https://charts.jetstack.io --force-update
helm upgrade cert-manager jetstack/cert-manager \
    --install \
    --namespace cert-manager \
    --create-namespace \
    --set crds.enabled=true \
    --wait
# テストのために secretTargets.authorizedSecretsAll=true を付けているが、
# 本来は secretTargets.authorizedSecrets で許可するsecretを指定した方が良い
helm upgrade trust-manager jetstack/trust-manager \
    --install \
    --namespace cert-manager \
    --wait
    --set secretTargets.enabled=true \
    --set secretTargets.authorizedSecretsAll=true

CAとClusterIssuerをセットアップ

このk8sクラスタ内で使えるCAをセットアップし、各種コンポーネントの証明書はこのCAを使って発行します。tls/issuer.yaml に記述します。

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsign
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-ca-2025
spec:
  isCA: true
  commonName: internal-ca-2025
  secretName: internal-ca-2025-secret
  duration: 87600h
  renewBefore: 336h # 14d
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsign
    kind: ClusterIssuer
    group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal
spec:
  ca:
    secretName: internal-ca-2025-secret
kubectl apply -f tls/issuer.yaml -n cert-manager

Operator用の証明書とCA証明書をセットアップ

先にMinIO OperatorのSTSエンドポイント用証明書を発行しておきます。このとき、secretNameは sts-tls とする必要があります*1minio-operator/cert.yaml として記述します。

---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: sts-certmanager-cert
  namespace: minio-operator
spec:
  dnsNames:
    - sts
    - sts.minio-operator.svc
    - sts.minio-operator.svc.cluster.local
  secretName: sts-tls # MUST
  issuerRef:
    kind: ClusterIssuer
    name: internal
kubectl create ns minio-operator
kubectl apply -f minio-operator/cert.yaml

今回MinIO本体でもcert-managerが発行した証明書を使うのですが、operatorからMinIOへのリクエストでも署名検証できるようにするためにCA証明書をマウントします。Operatorには特定の名前パターンでsecretを作ると自動で検知してくれる仕組み*2があるのでこれを使います。

CA証明書はtrust-managerに配布させます。tls/bundle-for-minio.yaml として記述します。

---
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  name: operator-ca-tls-internal # operator-ca-tls-*という名前にする
  namespace: cert-manager
spec:
  sources:
  # 作成したクラスタ内CAの証明書を同梱する
  - secret:
      name: "internal-ca-2025-secret"
      key: "ca.crt"
  target:
    secret:
      key: "ca.crt"
    namespaceSelector:
      # minio-operatorのnamespaceだけにインストールする
      matchLabels:
        kubernetes.io/metadata.name: minio-operator
kubectl apply -f tls/bundle-for-minio.yaml

MinIO Operatorをセットアップ

MinIO Operatorはアップストリームの手順に従ってインストールします。具体的にはリポジトリアーカイブをダウンロードし、kustomizeでマニフェストをビルド・一部パッチを当ててクラスタにapplyします。

MINIO_OPERATOR_VERSION="7.1.1"
mkdir -p minio-operator/upstream
# minio-operatorのupstreamのマニフェストをダウンロードする
wget -O minio-operator/upstream/v${MINIO_OPERATOR_VERSION}.tar.gz \
    https://github.com/minio/operator/archive/refs/tags/v${MINIO_OPERATOR_VERSION}.tar.gz
cd "minio-operator/upstream"
tar xf "v${MINIO_OPERATOR_VERSION}.tar.gz"
# リポジトリ丸ごとは要らないので、一旦素のマニフェストをビルドして書き出しておく
kustomize build operator-${MINIO_OPERATOR_VERSION}/ > "operator.yaml"

# 不要なファイルを削除する
rm -r operator-${MINIO_OPERATOR_VERSION} v${MINIO_OPERATOR_VERSION}.tar.gz
cd ../../

通常MinIO Operatorは自己署名証明書を自動的に作成しますが、これはあらゆる署名検証を無効にする必要があり面倒です。そのため、自動作成する機能を切ってcert-managerで発行した証明書を使わせます。前のセクションで証明書は既に用意したので、ここでは自動作成する機能をオフにします。minio-operator/kustomization.yaml でパッチを当てます。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: minio-operator

resources:
- upstream/operator.yaml
- cert.yaml

patches:
- patch: |-
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: minio-operator
      namespace: minio-operator
    spec:
      template:
        spec:
          containers:
            - name: minio-operator
              env:
                - name: OPERATOR_STS_AUTO_TLS_ENABLED
                  value: "off"
                - name: OPERATOR_STS_ENABLED
                  value: "on"
kustomize build minio-operator/ | kubectl apply -f -

MinIO用の証明書をセットアップ

MinIOのWebコンソールおよびS3互換APIのエンドポイントで利用される証明書を発行します。

---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: minio-tenant-cert
  namespace: objectstorage
spec:
  dnsNames:
    - "minio.objectstorage"
    - "minio.objectstorage.svc"
    - "minio.objectstorage.svc.cluster.local"
    - "*.minio-hl.objectstorage.svc.cluster.local"
    - "*.objectstorage.svc.cluster.local"
    - "*.minio.objectstorage.svc.cluster.local"
  secretName: minio-tenant-tls
  issuerRef:
    kind: ClusterIssuer
    name: internal
kubectl create ns objectstorage
kubectl apply -f objectstorage/cert.yaml

MinIOをセットアップ

MinIO Operatorに対して、MinIO本体はTenantと呼ばれます。カスタムリソースであるTenantを作ることでOpertorによってMinIOがセットアップされます。 今回はテスト用にErasure Codingの設定を限界まで緩めているので冗長性がありません。本番環境でそのまま利用しないでください。

---
apiVersion: minio.min.io/v2
kind: Tenant
metadata:
  name: minio
  namespace: objectstorage
spec:
  image: 'minio/minio:RELEASE.2025-03-12T18-04-18Z'
  # Disable default tls certificates.
  requestAutoCert: false
  # Use certificates generated by cert-manager.
  externalCertSecret:
    # 先に作っておいた証明書
    - name: minio-tenant-tls
      type: cert-manager.io/v1
  configuration:
    # 後で作る
    name: storage-configuration
  pools:
    - servers: 1
      name: pool-0
      volumesPerServer: 1
      volumeClaimTemplate:
        apiVersion: v1
        kind: persistentvolumeclaims
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi

いくつかの追加設定をsecretを使って設定します。 MINIO_STORAGE_CLASS_STANDARD でErasure Codingの設定をしています。本番環境では正しい設定をしてください*3。パスワードも同様にランダムかつ十分長い文字列を設定するようにし、リポジトリにはpushしないようにしてください。

---
apiVersion: v1
kind: Secret
metadata:
  name: storage-configuration
  namespace: objectstorage
stringData:
  config.env: |
    export MINIO_ROOT_USER="minio"
    export MINIO_ROOT_PASSWORD="password"
    export MINIO_STORAGE_CLASS_STANDARD="EC:0"
    export MINIO_BROWSER="on"

これらを適用します。

cat << 'EOF' > objectstorage/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: objectstorage
resources:
  - storage-configuration.yaml
  - tenant.yaml
  - tenant-cert.yaml
EOF
kustomize build objectstorage/ | kubectl apply -f -

最終的に以下の様にPodが立っていればOKです。

❯ kubectl get po -n objectstorage
NAME             READY   STATUS    RESTARTS   AGE
minio-pool-0-0   2/2     Running   0          6h24m

BucketとPolicyを作る

ポートフォワードしてコンソールにアクセスします。証明書のエラーが出ますが、これはクラスタ内CAの証明書がブラウザ側にはインストールされていないためです。本番環境ではコンソールを公開するためのIngressリソースなどを用意し、クラスタ外でもvalidな証明書を使いましょう。今回は単に警告を無視してアクセスします。

kubectl port-forward svc/minio-console -n objectstorage 9443:9443

https://localhost:9443 にアクセスすると設定したユーザ名・パスワードでログインできます。

MinIOのWebコンソール

Create Bucket してテスト用のbucketを作ります。

testバケットを作った様子

最後にこの test bucketのためのPolicyを作成します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:DeleteObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::test/*"
            ]
        }
    ]
}

test-rw policyを追加する様子

MinIOのクライアント用CA証明書を配布する

MinIOの証明書の署名検証のため、CA証明書をクラスタ内の全namespaceに配布します。各Podはこれをマウントすることで正しく署名検証できます。

---
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  name: internal-ca-bundle
spec:
  sources:
  - useDefaultCAs: true
  - secret:
      name: "internal-ca-2025-secret"
      key: "tls.crt"
  target:
    configMap:
      key: "trust-bundle.pem"
kubectl apply -f tls/bundle.yaml

全namespaceで internal-ca-bundle というConfigMapが作られているはずです。

❯ kubectl get cm internal-ca-bundle
NAME                 DATA   AGE
internal-ca-bundle   1      6h4m

STSを使ってMinioにアクセスする

ようやく長いセットアップが終わったので実際にSTSを使ってMinIOのAPIを呼び出します。

PolicyBindingを作る

MinIO Operatorはこのカスタムリソースを元にService Accountとそれに許可するPolicyを識別します。このPolicy BindingはMinIO Tenantと同じnamespaceに配置する必要があります。今回はdefault namespaceの test というService Accountに test-rw Policyを許可します。

---
apiVersion: sts.min.io/v1alpha1
kind: PolicyBinding
metadata:
  name: default-test
  namespace: objectstorage
spec:
  application:
    namespace: default
    serviceaccount: test
  policies:
    - test-rw
kubectl apply -f objectstorage/policybindings.yaml

awscliで操作する

基本的には環境変数を使ってアクセス先や認証方法を設定します。awscliやAWS SDKはいずれも同じ環境変数で設定を変更できるようになっています。

docs.aws.amazon.com

awscliの入ったPodを立ち上げます。

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test
---
apiVersion: v1
kind: Pod
metadata:
  name: bastion
spec:
  containers:
    - name: bastion
      # 古いバージョンでは環境変数からの設定変更をサポートしていないので注意
      image: amazon/aws-cli:latest
      command:
        - sleep
        - infinity
      volumeMounts:
        - name: internal-ca
          mountPath: /tls/internal
          readOnly: true
        # audienceを指定したトークンをマウント
        - name: minio-sts-token
          mountPath: /var/run/secrets/sts.min.io/serviceaccount
          readOnly: true
  volumes:
    - name: internal-ca
      configMap:
        name: internal-ca-bundle
    - name: minio-sts-token
      projected:
        sources:
          - serviceAccountToken:
              # audienceを指定しなければならない
              audience: "sts.min.io"
              expirationSeconds: 86400
              path: token
  serviceAccountName: test
kubectl apply -f default/bastion.yaml

kubectl exec を使って上記コンテナ内のbashを立ち上げます。

kubectl exec -it bastion -- bash

コンテナの中で以下の環境変数を設定します。

# マウントしたCA証明書のパス。STSとMinioのエンドポイントの署名検証に必要
export AWS_CA_BUNDLE=/tls/internal/trust-bundle.pem
# ServiceAccountのトークンを認証キーとして使う
export AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/sts.min.io/serviceaccount/token
# consoleではなくMinioのAPIエンドポイント
export AWS_ENDPOINT_URL_S3=https://minio.objectstorage.svc:443
# STSのエンドポイントはサービス名の末尾に `sts/テナントのnamesapce` を入れる
export AWS_ENDPOINT_URL_STS=https://sts.minio-operator.svc:4223/sts/objectstorage
# Minioでは使われないのだが、awscliはそれっぽい値を入れないとリクエストを蹴るので適当に入れる
export AWS_ROLE_ARN=arn:aws:iam::dummy:role/test

これだけでawscliを使ってMinIOのS3互換APIを利用できます。

bash-4.2# aws s3 ls s3://test/
bash-4.2# echo hello > hello.txt
bash-4.2# aws s3 cp hello.txt s3://test/
upload: ./hello.txt to s3://test/hello.txt
bash-4.2# aws s3 ls s3://test/
2025-03-21 06:14:38          6 hello.txt
bash-4.2#

AWS SDKから操作する

同じ環境変数を用いると、以下の様なコードでMinIOへの操作を記述できます。今回はGoのAWS SDK v2を使っています。

package main

import (
    "context"
    "flag"
    "fmt"
    "log/slog"
    "os"
    "os/signal"
    "syscall"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

var (
    bucketName string
    pathToList string
)

func listBucketItems() error {
    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
    defer cancel()

    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        return fmt.Errorf("failed to load configuration, %w", err)
    }
    bucket := s3.NewFromConfig(cfg, func(o *s3.Options) {
        o.UsePathStyle = true
    })
    outputs, err := bucket.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
        Bucket: &bucketName,
        Prefix: &pathToList,
    })
    if err != nil {
        return fmt.Errorf("failed to list bucket items, %w", err)
    }
    for _, item := range outputs.Contents {
        slog.Info("get", "path", item.Key, "size", item.Size, "bucket", bucketName)
    }
    return nil
}

func main() {
    flag.StringVar(&bucketName, "bucket", "", "bucket name")
    flag.StringVar(&pathToList, "path", "", "path to list")
    flag.Parse()

    if err := listBucketItems(); err != nil {
        slog.Error("failed to list bucket items", "error", err)
        os.Exit(1)
    }
}

同じようにAWS SDKを使っているアプリケーションでは同様の環境変数の設定でアクセス出来るようになるため非常に便利です。

例えば筆者のクラスタにはmocoというMySQLのオペレータがインストールされており、これはAWS SDKを使ってMySQLクラスタのバックアップをS3にアップロードする仕組みを持っています。MySQLBackupというカスタムリソースでその設定を書くのですが、以下の様に書いて適切なBucket・Policy・PolicyBindingを用意するだけでバックアップのデータをMinIOにアップロードできます。

apiVersion: moco.cybozu.com/v1beta2
kind: BackupPolicy
metadata:
  name: daily
spec:
  schedule: "5 0 * * *"
  concurrencyPolicy: Forbid
  jobConfig:
    serviceAccountName: moco-mysql
    env:
    - name: AWS_CA_BUNDLE
      value: /tls/internal/trust-bundle.pem
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/sts.min.io/serviceaccount/token
    - name: AWS_ENDPOINT_URL_STS
      value: https://sts.minio-operator.svc:4223/sts/objectstorage
    # AWS_ROLE_ARN is required by the AWS SDK, but it is not mandatory in Minio.
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::dummy:role/test
    bucketConfig:
      bucketName: moco-mysql-backup
      region: us-east-1
      endpointURL: https://minio.objectstorage.svc:443
      usePathStyle: true
    workVolume:
      ephemeral:
        volumeClaimTemplate:
          spec:
            accessModes: [ "ReadWriteOnce" ]
            resources:
              requests:
                storage: 10Gi
    volumeMounts:
      - name: internal-ca
        mountPath: /tls/internal
        readOnly: true
      - name: minio-sts-token
        mountPath: /var/run/secrets/sts.min.io/serviceaccount
        readOnly: true
    volumes:
      - name: internal-ca
        configMap:
          name: internal-ca-bundle
      - name: minio-sts-token
        projected:
          sources:
            - serviceAccountToken:
                audience: "sts.min.io"
                expirationSeconds: 86400
                path: token

気になったところ

STSのエンドポイントが /sts/{{Tenantのnamespace}} となるのですが、 Tenant リソース自体は一つのnamespaceに複数作成できそうな気がします。 すると、あるPolicyBindingがどのTenantのものなのか判別できない気がします。実用的にこれで困ることがあるかは不明です。

今回は自分しか管理する人が居ないクラスタなのでよいのですが、複数の開発者が同じクラスタを共有するマルチテナント構成の場合、PolicyBindingリソースの操作権限をどう与えるか悩ましいなと思いました。 いくらでも作成・更新できてしまうと他チームのbucketを勝手に操作する権限を付与できてしまいますし、逆に全く作れないようにしてMinIO Tenantの管理チームだけに絞ってしまうと都度依頼対応が必要になったりして手間が増えそうです。

また、Policyを宣言的に管理する方法がないのもつらいところです。どのように権限分担をするのかが悩ましい仕組みだなと思いました。

まとめ

Kubernetesのin clusterなMinIOへのアクセスでは、MinIO OperatorのSTSを利用することで面倒なAccess Keyの管理なしにMinIOへのアクセス権を付与出来ます。

MinIO OperatorのSTSはリクエストに付与されたKubernetesのService Account Tokenを元に、PolicyBindingカスタムリソースを用いてMinIO上のPolicyを識別し、MinIOから短命なトークンを発行します。

awscliやAWS SDK環境変数経由で認証方法やアクセス先をカスタマイズでき、他者のアプリケーションであってもAWS SDKを使っていればSTSを使ってMinIOにアクセス出来るます(たぶん)。