상세 컨텐츠

본문 제목

MinIO 기초적인 Tenant 배포와 EC 검증 테스트

> Tech

by Ryusstory 2025. 9. 12.

본문

개요

이번 글에서는 로컬 쿠버네티스 환경을 KinD로 구성하고, MinIO 를 설치하고 오브젝트 스토리지를 간단하게 올려서 기본적인 동작을 확인하고 정리해보려고 합니다. MinIO의 핵심 컨셉도 정리하려했으나 내용이 길어져 별도 글로로 작성했습니다.
https://ryusstory.tistory.com/entry/minio-architecture-key-concepts

kind와 MinIO Operator 설치

kind 설치

아래 스크립트를 통해 컨트롤 플레인 1개와 워커 3개로 구성된 클러스터를 생성합니다.
추가로 NodePort를 사용하기 위해 30000-30003 포트를 열어놨습니다.

kind create cluster --image kindest/node:v1.33.4 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
  - containerPort: 30002
    hostPort: 30002
  - containerPort: 30003
    hostPort: 30003
- role: worker
- role: worker
- role: worker
EOF

MinIO Operator 설치

공식 문서

MinIO Operator는 MinIO를 관리하기 위한 CRD를 설치합니다. 이 CRD를 통해 MinIO Tenent를 쿠버네티스 오브젝트로 정의하고 관리할 수 있습니다.
Operator는 기본적으로 네임스페이스 안에서 동작하며, 기본적으로 2개의 파드를 실행합니다.

  • 오퍼레이터 파드: MinIO 테넌트 배포/관리/수정/유지보수
  • 콘솔 파드: GUI 콘솔을 제공하여 테넌트관리를 쉽게 할 수 있도록 지원합니다.
helm repo add minio-operator https://operator.min.io
helm install \
  --namespace minio-operator \
  --create-namespace \
  --set operator.replicaCount=1 \
  operator minio-operator/operator

설치후에 crd를 확인해보면 policybindings.sts.min.iotenants.minio.min.io 라는 CRD가 생성된 것을 확인할 수 있습니다.

MinIO Tenant 배포

공식문서
각 Tenant는 서버 파드, PVC, Service, Secret 등을 포함하며, 이를 통해 여러 개의 MinIO 클러스터를 한 Kubernetes 환경 안에서 네임스페이스 별로 서로 분리해 운영할 수 있습니다.

MinIO 공식 문서에서는 안정성과 성능을 위해 다음을 권장하고 있습니다

  • 로컬 스토리지가 연결된 워커 노드에서 Tenant 실행
  • 스토리지가 직접 연결된 (DAC:Direct Attached Storage) 환경에서는 DirectPV CSI 드라이버 사용
  • PV는 독점적으로 접근, ReclaimPolicy=Retain, XFS 파일시스템 권장

각 테넌트에는 MinIO 파드가 배포되는데, 각 파드는 3개의 컨테이너로 구성됩니다.

  • MinIO 컨테이너: 객체 저장/조회 기능 제공
  • Init 컨테이너: 초기 실행 시 보안 설정(Secret) 처리 후 종료
  • Sidecar 컨테이너: Secret 변경 모니터링 및 자격 증명 관리

공식 문서를 참고하여 한번 테넌트를 배포해보겠습니다.
먼저 kustomize를 통해 배포할 매니페스트를 생성합니다.

kubectl kustomize https://github.com/minio/operator/examples/kustomization/base/ > tenant-base.yaml

해당 파일은 편집기로 열어서 아래 부분들을 수정해 봅니다.

Secret 중 storage-configuration
EC:2는 4개의 디스크 중 2개의 디스크가 장애가 나더라도 데이터를 복구할 수 있는 인코딩 방식입니다. (Erasure Coding)
테스트목적이기 때문에 EC:1로 설정해서 2개의 디스크 중 1개의 디스크가 장애가 나더라도 데이터를 복구할 수 있는 인코딩 방식으로 설정합니다.
유저/패스워드는 편의상 admin/adminadmin으로 설정했습니다. (MINIO_ROOT_USER length should be at least 3, and MINIO_ROOT_PASSWORD length at least 8 characters)

    export MINIO_STORAGE_CLASS_STANDARD="EC:1"
    export MINIO_ROOT_USER="admin"
    export MINIO_ROOT_PASSWORD="adminadmin"

S3 API를 통해 접속할 수 있는 콘솔의 유저/패스워드도 admin/adminadmin으로 설정합니다. YWRtaW4=는 base64로 인코딩된 admin 문자열입니다. ( echo -n "admin" | base64 )

  CONSOLE_ACCESS_KEY: YWRtaW4=
  CONSOLE_SECRET_KEY: YWRtaW5hZG1pbg==

kind: Tenant 오브젝트의 spec.pools[0] 부분을 보면 서버 수를 설정하는 servers 항목과 서버당 볼륨 수를 설정하는 volumesPerServer 항목이 있습니다.
servers는 4로 설정하고, volumesPerServer는 2로 설정합니다.

    volumesPerServer: 2

pools[0].volumeClaimTemplate.spec.resources.requests.storage 항목은 각 볼륨의 크기를 지정합니다. 1Ti로 설정되어 있는데, 테스트 목적이기 때문에 1Gi로 수정합니다.

            storage: 1Gi

그리고 마지막으로 각 yaml에 namespace가 minio-tenent로 설정되어 있는데, 그대로 배포하겠습니다.

kubectl apply -f tenant-base.yaml

배포하고 나면
kubectl get all -n minio-tenant 명령어로 리소스들이 잘 생성되기를 기다립니다

완료되고 나면 kubectl get pvc -n minio-tenant 명령어로 PVC들이 잘 생성되었는지 확인합니다. 확인해보면 4개 노드 x 2개 볼륨 으로 8개의 PVC가 생성된 것을 확인할 수 있습니다.

서비스를 확인해보면 3개의 서비스가 생성된 것을 확인할 수 있습니다.
minio와 minio-console만

$ kubectl get svc -n minio-tenant
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
minio             ClusterIP   10.96.133.240   <none>        443/TCP    29s
myminio-console   ClusterIP   10.96.35.196    <none>        9443/TCP   29s
myminio-hl        ClusterIP   None            <none>        9000/TCP   29s

콘솔 접속

기존의 ClusterIP 서비스를 NodePort로 패치해서 kind에서 열어둔 포트로 접속할 수 있도록 합니다.

kubectl patch svc minio -n minio-tenant -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "nodePort": 30001}]}}'
kubectl patch svc myminio-console -n minio-tenant -p '{"spec": {"type": "NodePort", "ports": [{"port": 9443, "nodePort": 30002}]}}'

https://localhost:30001/ 로 접속하면 콘솔 로그인 화면이 나옵니다.

앞서 설정한 admin/admin으로 로그인하면 아래와 같이 콘솔 화면이 나옵니다.

데이터 저장 및 확인 테스트

테스트를 위한 MinIO Client 설치

공식 문서의 방법대로 MinIO Client(mc)를 설치합니다.
설치하고 mc alias set 명령어로 앞서 설정한 minio 서비스에 접속할 수 있도록 설정합니다.

mc alias set local https://localhost:30001 admin adminadmin --insecure

테스트 파일 업로드

테스트를 위해 간단한 30줄짜리 텍스트를 업로드하고, 워커 노드 한대를 종료 시켜 EC:1 설정이 잘 동작하는지 확인해보겠습니다.
seq 명령어를 통해 30줄짜리 텍스트 파일을 생성합니다. 넣고 나서 확인하기 편하도록 숫자 앞에 이모지를 추가합니다.

seq 30 | sed 's/^/🟩🟩 /' > test.txt

mc 명령어로 테스트 버킷을 생성하고 파일을 업로드 합니다.

mc mb local/test-bucket --insecure
mc cp test.txt local/test-bucket/ --insecure

업로드된 파일을 확인합니다.

mc ls local/test-bucket --insecure

원본 파일과 업로드된 객체를 md5sum으로 비교하면 일치하는 것을 확인할 수 있습니다.

cat test.txt | md5sum
mc cat local/test-bucket/test.txt --insecure | md5sum

PV 저장 구조 확인

kubectl describe pod -n minio-tenant -l v1.min.io/tenant=myminio 명령어로 확인해보면 MinIO 파드는 /export0/export1 경로에 PV가 마운트되어 있습니다.
아래처럼 실행하면 모든 파드에서 /export0/export1 경로에 test-bucket/test.txt/xl.meta 파일이 존재하는 것을 확인할 수 있습니다.
즉, 객체가 업로드 되면 각 PV에 디렉토리/파일 단위로 저장되고, 확인해보면 실제 파일은 파일명 폴더 안에 xl.meta라는 메타데이터 파일로 저장되는 것을 알 수 있습니다.

kubectl get pod -n minio-tenant -l v1.min.io/tenant=myminio -oname \
  | xargs -I {} kubectl exec -n minio-tenant -c minio {} -- \
  sh -c "echo _____ {}; ls /export0/test-bucket/test.txt/xl.meta /export1/test-bucket/test.txt/xl.meta"

그리고 실제 어떻게 나눠서 저장되어 있는지 어느정도는 구경해 볼 수 있습니다. 잘 찾아보면 1부터 30까지 모두 들어가 있는 것을 확인할 수 있습니다.

kubectl get pod -n minio-tenant -l v1.min.io/tenant=myminio -oname | xargs -I {} kubectl exec -n minio-tenant -c minio {} -- sh -c "echo; echo _____ {} export0; cat /export0/test-bucket/test.txt/xl.meta; echo; echo _____ {} export1; cat /export1/test-bucket/test.txt/xl.meta"

xl.meta 파일 내용 분석 (필수X)

xl-meta 툴은 minio 디버깅 툴golang을 설치하고 아래 명령어로 xl-meta 툴을 설치합니다.

wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go 
sudo tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin:~/go/bin/' >> ~/.bashrc
source ~/.bashrc
go install github.com/minio/minio/docs/debugging/xl-meta@latest

이제 xl.meta 파일을 직접 보기 위해 먼저 pv를 확인합니다.

ryuss@R250822:~$ kc get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                             STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-0c903892-36ac-40bf-aabb-32f6efa6bfaf   1Gi        RWO            Delete           Bound    minio-tenant/1-myminio-pool-0-0   standard       <unset>                          67m

해당 pv의 실제 위치도 확인합니다.

kc describe pv pvc-0c903892-36ac-40bf-aabb-32f6efa6bfaf
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /var/local-path-provisioner/pvc-0c903892-36ac-40bf-aabb-32f6efa6bfaf_minio-tenant_1-myminio-pool-0-0

해당 파드가 어느 노드에 있는지 확인합니다.

kc get pods -nminio-tenant -owide
NAME               READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
myminio-pool-0-0   2/2     Running   0          69m   10.244.2.4   kind-worker    <none>           <none>

이제 노드에서 직접 xl.meta 파일을 확인해봅니다.

docker exec -ti kind-worker bash -c "cat /var/local-path-provisioner/pvc-0c903892-36ac-40bf-aabb-32f6efa6bfaf_minio-tenant_1-myminio-pool-0-0/test-bucket"

이제 xl.meta 파일을 받아옵니다.

docker cp kind-worker:/var/local-path-provisioner/pvc-0c903892-36ac-40bf-aabb-32f6efa6bfaf_minio-tenant_1-myminio-pool-0-0/test-bucket/test.txt/xl.meta .

xl-meta 를 사용하면 메타데이터 파일의 내용을 json 형태로 볼 수 있고, --data 옵션을 사용하면 base64로 인코딩된 실제 데이터를 볼 수 있습니다. 앞의 minio 아키텍처와 핵심정리에서 나온 내용 대부분이 나와 좀 이해가 되기도 했습니다. Part는 여기서는 단일 파트로만 이뤄줬는데,아마 multipart로 업로드 했을 경우 파트가 나눠질 것으로 보입니다. 아래는 메타데이터 중 의미있는 몇개 데이터만 정리해봤습니다.

  • Meatadata
    • CSumAlgo: 1: 체크섬 알고리즘. 1은 HIGHWAYHASH 체크섬을 의미(bit rot)
    • Algo: 1: Erasure Coding 알고리즘. 1은 Reed-Solomon을 의미
    • EcBSize: 1048576: Erasure Coding 블록 크기(바이트 단위). 1MB(1048576 바이트)로, 객체를 이 크기로 분할하여 샤드 생성
    • EcDist: [6,7,8,1,2,3,4,5]: 샤드 분배 배열. 각 숫자는 드라이브 인덱스를 나타내며, 샤드가 어떤 드라이브에 저장되는지 정의.
    • EcIndex: 2: 이 샤드의 인덱스. 현재 xl.meta가 속한 샤드의 위치
    • EcM: 6: Erasure Coding의 데이터 샤드 수(K).
    • EcN: 2: Erasure Coding의 패리티 샤드 수(M).
$ xl-meta --export
{
  "Versions": [
    {
      "Header": {
        "EcM": 6,
        "EcN": 2,
        "Flags": 6,
        "ModTime": "2025-09-12T02:45:14.531206152+09:00",
        "Signature": "c5313dfb",
        "Type": 1,
        "VersionID": "00000000000000000000000000000000"
      },
      "Idx": 0,
      "Metadata": {
        "Type": 1,
        "V2Obj": {
          "CSumAlgo": 1,
          "DDir": "z8yLlyKxSnaPjfPKqCsAcA==",
          "EcAlgo": 1,
          "EcBSize": 1048576,
          "EcDist": [
            6,
            7,
            8,
            1,
            2,
            3,
            4,
            5
          ],
          "EcIndex": 2,
          "EcM": 6,
          "EcN": 2,
          "ID": "AAAAAAAAAAAAAAAAAAAAAA==",
          "MTime": 1757612714531206152,
          "MetaSys": {
            "x-minio-internal-inline-data": "dHJ1ZQ=="
          },
          "MetaUsr": {
            "content-type": "text/plain",
            "etag": "ae3977a0ed8736953c6ce5e5c03b6ca7"
          },
          "PartASizes": [
            351
          ],
          "PartETags": null,
          "PartNums": [
            1
          ],
          "PartSizes": [
            351
          ],
          "Size": 351
        },
        "v": 1744126884
      }
    }
  ]
}


$ xl-meta --data
{
  "null": {
    "bitrot_valid": true,
    "bytes": 91,
    "data_base64": "8J+fqSA2CvCfn6nwn5+pIDcK8J+fqfCfn6kgOArwn5+p8J+fqSA5CvCfn6nwn5+pIDEwCvCfn6nwn58="

}
$ echo -n "8J+fqSA2CvCfn6nwn5+pIDcK8J+fqfCfn6kgOArwn5+p8J+fqSA5CvCfn6nwn5+pIDEwCvCfn6nwn58=" | base64 -d
🟩 6
🟩🟩 7
🟩🟩 8
🟩🟩 9
🟩🟩 10
🟩

관련글 더보기