실습환경 구성

kind create cluster --name myk8s --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    
EOF

# minio-operator : https://github.com/minio/operator/blob/master/helm/operator/values.yaml
helm install --namespace minio-operator --create-namespace minio-operator minio-operator/operator --set operator.replicaCount=1
kubectl get all -n minio-operator

# tenant values : https://github.com/minio/operator/blob/master/helm/tenant/values.yaml
cat << EOF > minio-tenant-1-values.yaml
tenant:
  name: tenant1

  configSecret:
    name: tenant1-env-configuration
    accessKey: minio
    secretKey: minio123

  pools:
    - servers: 1
      name: pool-0
      volumesPerServer: 4
      size: 1Gi 
      storageClassName: standard
  env:
    - name: MINIO_STORAGE_CLASS_STANDARD
      value: "EC:1"
    - name: MINIO_PROMETHEUS_AUTH_TYPE
      value: public

  features:
    bucketDNS: true
    
  metrics:
    enabled: true
    port: 9000
    protocol: http
EOF
helm install --namespace tenant1 --create-namespace --values minio-tenant-1-values.yaml tenant1 minio-operator/tenant \
 && kubectl get tenants -A -w

#
kubectl describe tenants -n tenant1
kubectl get pvc -n tenant1
kubectl describe pvc -n tenant1
kubectl get sts,pod,svc,ep,pvc,secret -n tenant1
kubectl get pod -n tenant1 -l v1.min.io/pool=pool-0 -owide
kubectl describe pod -n tenant1 -l v1.min.io/pool=pool-0
kubectl stern -n tenant1 -l v1.min.io/pool=pool-0
kubectl exec -it -n tenant1 sts/tenant1-pool-0 -c minio -- id
kubectl exec -it -n tenant1 sts/tenant1-pool-0 -c minio -- env
kubectl exec -it -n tenant1 sts/tenant1-pool-0 -c minio -- cat /tmp/minio/config.env
kubectl get secret -n tenant1 tenant1-env-configuration -o jsonpath='{.data.config\.env}' | base64 -d ; echo
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d | openssl x509 -noout -text
     
#
kubectl patch svc -n tenant1 tenant1-console -p '{"spec": {"type": "NodePort", "ports": [{"port": 9443, "targetPort": 9443, "nodePort": 30001}]}}'
kubectl patch svc -n tenant1 minio -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 9000, "nodePort": 30002}]}}'

# 기본키(minio , minio123)
open "https://127.0.0.1:30001"

# mc alias
mc alias set k8s-tenant1 https://127.0.0.1:30002 minio minio123 --insecure
mc alias list
mc admin info k8s-tenant1 --insecure

# alias
MYALIAS=k8s-tenant1
mc ls $MYALIAS --recursive --insecure

# 신규 터미널 : 모니터링 -> mc cli 와 웹 브라우저에서 인가 출력 정보 비교
mc admin trace $MYALIAS --verbose --insecure
or
mc admin trace $MYALIAS --verbose --all --insecure

## mc cli 와 웹 브라우저에서 인가 출력 정보 비교
127.0.0.1:30002 Authorization: AWS4-HMAC-SHA256 Credential=minio/20250921/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=7a4a96a7cbe71edaa070c14843b89ce51fb24be3ab121a5f264bdba88b43c6e0

## mc cli 와 웹 브라우저에서 인가 출력 정보 비교
minio.tenant1.svc.cluster.local X-Amz-Security-Token: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJJSThUTlQ3UVFJMURPWktSTDVFTiIsImV4cCI6MTc1ODQ3MTc4NiwicGFyZW50IjoibWluaW8ifQ.vgrUYX7HJLHpZDg4bwXwZ255AQUXuOXkg30ov8kiS05olImLLiOz-pvR6RcFDPMsq0GTDhycse36iAxxmFqONg
minio.tenant1.svc.cluster.local Authorization: AWS4-HMAC-SHA256 Credential=II8TNT7QQI1DOZKRL5EN/20250921/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=a58c10ff7b9a6507a6b09d0cd20d87abef905048bed245443c5ac768d65ab9e2


# 버킷 생성 
mc mb $MYALIAS/mybucket --insecure
mc ls $MYALIAS --insecure

# object 업로드
echo hello > hello.txt
mc cp hello.txt $MYALIAS/mybucket --insecure

# 웹 콘솔에서 확인 후 trace 확인

버킷생성, 파일 업로드와 그에대한 로그

 

인증 

  • 클라이언트의 신원을 확인하는 과정.
  • MinIO는 AWS Signature Version 4 프로토콜을 사용하여 인증을 요구합니다.
  • 클라이언트는 유효한 Access Key와 Secret Key를 제시해야 하며,
    • S3 API 요청 (예: PUT, GET, DELETE)
    • MinIO 관리 API 요청
      모두 인증을 거쳐야만 접근 가능합니다.

인가 

  • 인증된 클라이언트가 어떤 작업과 리소스를 사용할 수 있는지 제한하는 과정.
  • PBAC (Policy-Based Access Control) 모델을 사용합니다.
  • 정책은 특정 사용자 또는 그룹에 부여되며,
    • 허용되는 S3 작업(action)
    • 특정 조건(conditions)
      등을 정의할 수 있습니다.
  • 기본 정책은 거부(Deny by default) 이므로, 정책에 명시되지 않은 작업이나 리소스는 접근할 수 없습니다.

ID 관리: 내장 관리 기능 vs 외부 관리 기능

  • 내장 ID 공급자(Minio Internal IDP)
    MinIO가 자체 내장한 ID 관리 기능으로, 사용자 계정을 직접 생성·관리하며, 사용자와 그룹에 명시적으로 정책을 연결해야 합니다. 내장 IDP 사용 시 정책 할당과 사용자·그룹 관리를 MinIO 명령어를 통해 수행합니다.
  • 외부 ID 공급자(External IDP)
    MinIO는 다음과 같은 외부 IDP 연동을 지원하여 ID 관리를 위임할 수 있습니다.
    • OpenID Connect (OIDC) 호환 서비스
    • Active Directory / LDAP
    • MinIO 인증 플러그인을 활용한 커스텀 외부 ID 관리자
      이를 통해 기존 조직의 인증 시스템과 통합할 수 있습니다.

접근 관리

  • MinIO는 정책 기반 접근 제어(PBAC)를 사용하며, 인증된 사용자에게 권한 있는 작업과 리소스만 접근하도록 정책으로 정의합니다.
  • PBAC 정책은 사용자 또는 그룹에 할당되며, 내장 IDP 사용 시 명령으로 직접 연결해야 합니다.
  • 외부 IDP 사용 시 정책 할당 방법은 IDP 종류에 따라 다릅니다.
  • MinIO의 PBAC 정책은 AWS IAM 정책 구문과 호환되도록 설계되어 있어, AWS IAM 정책 작성 문서를 참고해 정책을 작성할 수 있습니다.
  • 기본 원칙은 명시적으로 허용된 작업이나 리소스만 접근 가능하며, 명시되지 않은 요청은 기본적으로 거부됩니다 (Deny by default).
  • MinIO는 정책 기반 접근 제어(PBAC)를 이용해 사용자의 권한 있는 작업과 접근 리소스를 정의하며, 정책은 사용자 또는 그룹에 할당됩니다.
  • PBAC는 AWS IAM 정책 문법, 구조, 동작과 호환되며, 조건문을 통해 특정 태그가 있는 객체에만 액세스를 제한하는 태그 기반 정책 조건도 지원합니다.
  • 기본 제공 내장 정책으로는 consoleAdmin(전체 권한), readonly(읽기 전용), readwrite(읽기/쓰기), diagnostics(진단 권한), writeonly(쓰기 전용) 등이 있으며, 명시적 거부(Deny)가 허용(Allow)를 OVERRIDE합니다.

 

PBAC 정책 생성 / 삭제 실습

#기본 정책 확인

#
MYALIAS=k8s-tenant1
mc ls $MYALIAS --insecure

#
mc admin policy list $MYALIAS --insecure
readwrite
writeonly
consoleAdmin
diagnostics
readonly

#신규 정책 생성
#
mc admin policy info $MYALIAS readonly --insecure | jq
{
  "PolicyName": "readonly",
  "Policy": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "s3:GetBucketLocation",
          "s3:GetObject"
        ],
        "Resource": [
          "arn:aws:s3:::*"
        ]
      }
    ]
  }
}

# 기본 정책을 정책 부분만 파일로 저장
mc admin policy info $MYALIAS readonly --insecure --policy-file new.json

#
cat new.json| jq
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

# Action 추가
vi new.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:GetObject",
        "s3:List*"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

# 정책 생성
mc admin policy create $MYALIAS readlist new.json --insecure 

# 
mc admin policy list $MYALIAS --insecure
mc admin policy info $MYALIAS readlist --insecure | jq

기본정책 리스트

 

신규 정책 삽입된것을 확인 할 수 있다.

정책제거

#
mc admin policy remove $MYALIAS diagnostics --insecure
... # admin trace 에 출력 정보 확인
127.0.0.1:30002 [RESPONSE] [2025-09-21T15:02:56.901] [ Duration 1.213ms TTFB 1.211833ms ↑ 93 B  ↓ 303 B ]
127.0.0.1:30002 500 Internal Server Error
127.0.0.1:30002 {"Code":"InternalError","Message":"We encountered an internal error, please try again. (inbuilt policy `diagnostics` not allowed to be deleted)","Resource":"/minio/admin/v3/remove-canned-policy","RequestId":"186736EE5CE4E52F","HostId":"dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8"}

#
mc admin policy remove $MYALIAS readlist --insecure

#
mc admin policy list $MYALIAS --insecure

 

신규 유저 생성

# user 없음 확인
mc admin user list $MYALIAS --insecure

# user1 생성 : 시크릿키 mypassword
mc admin user add $MYALIAS user1 mypassword --insecure
mc admin user list $MYALIAS --insecure
enabled    user1

 

user1 에 PBAC 정책 할당

#
mc admin policy list $MYALIAS --insecure
mc admin policy info $MYALIAS readwrite --insecure | jq
...
        "Effect": "Allow",
        "Action": [
          "s3:*"
        ],
        "Resource": [
          "arn:aws:s3:::*"
...

mc admin policy attach $MYALIAS readwrite --user user1 --insecure
Attached Policies: [readwrite]
To User: user1

#
mc admin policy entities $MYALIAS --policy readwrite --insecure
Query time: 2025-09-20T06:21:14Z
Policy -> Entity Mappings:
  Policy: readwrite
    User Mappings:
      user1

user1에 정책 적용

 

 

Group 에 user 할당 및 정책 부착

#
mc admin group list $MYALIAS --insecure

#
mc admin group add $MYALIAS devteam user1 --insecure
mc admin group info $MYALIAS devteam --insecure
Group: devteam
Status: enabled
Policy:
Members: user1

#
mc admin policy attach $MYALIAS readonly --group devteam --insecure
Attached Policies: [readonly]
To Group: devteam

#
mc admin group info $MYALIAS devteam --insecure
Group: devteam
Status: enabled
Policy: readonly
Members: user1

 

현재 Group 에 두 번째 정책 추가 후 동작 확인

# 참고로 3번째 줄 정책이 위 1,2 모두 포함됨
cat << EOF > s3-list.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListAllMyBuckets",
        "s3:ListBucket",
        "s3:List*" 
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}
EOF

# lister 정책 생성
mc admin policy create $MYALIAS lister s3-list.json --insecure
mc admin policy list $MYALIAS --insecure

# devteam 그룹에 추가 부착
mc admin policy attach $MYALIAS lister --group devteam --insecure

# devteam 그룹에 2개의 정책 부착 확인
mc admin group info $MYALIAS devteam --insecure
Group: devteam
Status: enabled
Policy: lister,readonly
Members: user1

 

 

 

LDAP

 

배포할 Open LDAP 의 조직도를 LDIF로 만들예정

LDIF 구조는 다음과 같다.

# 전체 그림
## Users OU는 아직 사용자가 안 들어가 있지만 컨테이너만 생성
## Groups OU 안에 Admins/ Maintainers 그룹이 있고 사용자 DN이 멤버로 들어감
dc=minio,dc=io
├── cn=developer (사용자)
├── cn=maintainer (사용자)
├── cn=admin_root (사용자)
├── ou=Users (OU)
└── ou=Groups
    ├── cn=Admins (groupOfUniqueNames, member=admin_root)
    └── cn=Maintainers (groupOfUniqueNames, member=maintainer,developer)

    
# 사용자 엔트리들
dn: cn=developer,dc=minio,dc=io  # dn: 엔트리의 Distinguished Name(LDAP 경로)
changetype: add                  # changetype: add : 새 엔트리를 추가
objectclass: inetOrgPerson       # 속성(attribute)을 나열
cn: developer
givenname: developer
sn: Developer
displayname: Developer User
mail: developer@minio.io
userpassword: developer_pass

dn: cn=maintainer,dc=minio,dc=io
changetype: add
objectclass: inetOrgPerson       # LDAP에서 사람을 나타낼 때 쓰는 표준 객체 클래스
cn: maintainer                   # cn : Common Name (로그인 ID 같은 것)
givenname: maintainer
sn: Maintainer
displayname: Maintainer User     # displayname : LDAP UI 등에서 보여줄 이름
mail: maintainer@minio.io
userpassword: maintainer_pass    # userpassword : 사용자 비밀번호
...


# 조직 단위(OU) 생성
dn: ou=Groups,dc=minio,dc=io     # ou=Groups : 그룹들을 담는 컨테이너(Organizational Unit)
changetype: add
objectclass: organizationalUnit
ou: Groups

dn: ou=Users,dc=minio,dc=io      # ou=Users : 사용자를 담는 컨테이너
changetype: add
objectclass: organizationalUnit
ou: Users                        # 사용자들이 dc=minio,dc=io에 직접 만들어졌지만, 별도 OU로도 구분 가능


# 그룹 엔트리들
dn: cn=Admins,ou=Groups,dc=minio,dc=io
changetype: add
cn: Admins                       # cn=Admins 라는 그룹을 만들고
objectclass: groupOfUniqueNames  # objectclass: groupOfUniqueNames : 고유 멤버 DN을 갖는 그룹
uniqueMember: cn=admin_root,dc=minio,dc=io  # uniqueMember: 로 admin_root 사용자를 그룹 멤버로 추가

dn: cn=Maintainers,ou=Groups,dc=minio,dc=io
changetype: add
cn: Maintainers                  # Maintainers라는 그룹을 만들고
objectclass: groupOfUniqueNames
uniqueMember: cn=maintainer,dc=minio,dc=io # maintainer와 developer 두 명을 멤버로 지정
uniqueMember: cn=developer,dc=minio,dc=io

 

openldap 관리자 암호화 LDIF configmap 작성

#
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: openldap-admin-secret
type: Opaque
stringData:
  admin-password: "admin123"
EOF

#
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: openldap-bootstrap
  namespace: default
data:
  bootstrap.ldif: |
    dn: cn=developer,dc=minio,dc=io
    changetype: add
    objectclass: inetOrgPerson
    cn: developer
    givenname: developer
    sn: Developer
    displayname: Developer User
    mail: developer@minio.io
    userpassword: developer_pass

    dn: cn=maintainer,dc=minio,dc=io
    changetype: add
    objectclass: inetOrgPerson
    cn: maintainer
    givenname: maintainer
    sn: Maintainer
    displayname: Maintainer User
    mail: maintainer@minio.io
    userpassword: maintainer_pass

    dn: cn=admin_root,dc=minio,dc=io
    changetype: add
    objectclass: inetOrgPerson
    cn: admin_root
    givenname: admin_root
    sn: AdminRoot
    displayname: Admin User
    mail: admin_root@minio.io
    userpassword: admin_pass

    dn: ou=Groups,dc=minio,dc=io
    changetype: add
    objectclass: organizationalUnit
    ou: Groups

    dn: ou=Users,dc=minio,dc=io
    changetype: add
    objectclass: organizationalUnit
    ou: Users

    dn: cn=Admins,ou=Groups,dc=minio,dc=io
    changetype: add
    cn: Admins
    objectclass: groupOfUniqueNames
    uniqueMember: cn=admin_root,dc=minio,dc=io

    dn: cn=Maintainers,ou=Groups,dc=minio,dc=io
    changetype: add
    cn: Maintainers
    objectclass: groupOfUniqueNames
    uniqueMember: cn=maintainer,dc=minio,dc=io
    uniqueMember: cn=developer,dc=minio,dc=io
EOF

 

openldap 디플로이먼트/서비스 생성

cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openldap
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: openldap
  template:
    metadata:
      labels:
        app: openldap
    spec:
      initContainers:
        - name: copy-bootstrap
          image: busybox
          command: ['sh', '-c', 'cp /config/bootstrap.ldif /ldif/bootstrap.ldif']
          volumeMounts:
            - name: configmap-ldif
              mountPath: /config
            - name: writable-ldif
              mountPath: /ldif
      containers:
        - name: openldap
          image: osixia/openldap:1.5.0
          env:
            - name: LDAP_ORGANISATION
              value: "MinIO"
            - name: LDAP_DOMAIN
              value: "minio.io"
            - name: LDAP_ADMIN_PASSWORD
              value: "admin123"
            - name: LDAP_REMOVE_CONFIG_AFTER_SETUP
              value: "false"
          ports:
            - containerPort: 389
            - containerPort: 636
          volumeMounts:
            - name: writable-ldif
              mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom
        - name: phpldapadmin
          image: osixia/phpldapadmin:0.9.0
          env:
            - name: PHPLDAPADMIN_LDAP_HOSTS
              value: "localhost"
            - name: PHPLDAPADMIN_HTTPS
              value: "false" # 기본 HTTP로 사용, HTTPS 쓰려면 true
          ports:
            - containerPort: 80
      volumes:
        - name: configmap-ldif
          configMap:
            name: openldap-bootstrap
        - name: writable-ldif
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: openldap
  namespace: default
spec:
  selector:
    app: openldap
  ports:
    - name: ldap
      port: 389
      targetPort: 389
    - name: ldaps
      port: 636
      targetPort: 636
    - name: http
      port: 80
      targetPort: 80
EOF

 

배포 확인

#
kubectl get deploy,pod,svc,ep,cm,secret

# 컨테이너가 시작되면 /container/service/slapd/assets/config/bootstrap/ldif/custom/bootstrap.ldif 를 읽어서 자동으로 ldapadd 적용.
# 결과적으로 LDAP 서버가 처음 뜰 때 바로 사용자를 생성해 둡니다. 이후 kubectl exec로 들어가서 ldapsearch로 확인할 수 있습니다:

# 관리자 DN(암호)로 ldapsearch 확인
kubectl exec -it deploy/openldap -c openldap -- \
  ldapsearch -x -H ldap://localhost -D "cn=admin,dc=minio,dc=io" -w admin123 -b "dc=minio,dc=io"

 

AD/LDAP 액세스 관리

  • MinIO는 외부 ID 관리를 위해 단일 AD 또는 LDAP 서비스를 지원하며, 이를 활성화하면 내부 IDP는 비활성화됩니다.
  • 외부 AD/LDAP에서 관리되는 사용자의 고유 이름(DN)을 기반으로 MinIO 내 기존 정책에 매핑하고, 그룹 멤버십 역시 정책 매핑에 활용됩니다.
  • 인증 시 MinIO는 AD/LDAP 자격증명을 확인하고, DN 및 그룹 DN과 매칭되는 정책을 찾아 인증된 사용자에게 할당하며, 이를 기반으로 임시 자격증명(STS)을 생성합니다.
  • 명시된 정책이 없으면 모든 리소스 및 작업에 대한 접근이 거부되며, AD/LDAP 사용자는 정책에 연결된 액세스 키도 생성할 수 있습니다.

사용자 DN에 대한 정책 매핑

mc idp ldap policy attach myminio consoleAdmin \
  --user='cn=sisko,cn=users,dc=example,dc=com'

mc idp ldap policy attach myminio readwrite,diagnostics \
  --user='cn=dax,cn=users,dc=example,dc=com'
  • **cn=sisko,cn=users,dc=example,dc=com**MinIO는 정책 과 일치하는 DN을 가진 인증된 사용자를 할당하여 consoleAdmin MinIO 서버에 대한 완전한 액세스 권한을 부여합니다.
  • MinIO는 및 정책 cn=dax,cn=users,dc=example,dc=com모두와 일치하는 DN을 가진 인증된 사용자를 할당하여 MinIO 서버에 대한 일반적인 읽기/쓰기 액세스 권한 과 진단 관리 작업에 대한 액세스 권한을 부여합니다.
  • MinIO는 DN이 일치하는 인증된 사용자에게 정책을 할당하지 않으며 **cn=quark,cn=users,dc=example,dc=com**API 작업에 대한 모든 액세스를 거부합니다.

그룹 DN에 대한 정책 매핑

mc idp ldap policy attach myminio consoleAdmin \
  --group='cn=ops,cn=groups,dc=example,dc=com'

mc idp ldap policy attach myminio diagnostics \
  --group='cn=engineering,cn=groups,dc=example,dc=com'
  • **cn=ops,cn=groups,dc=example,dc=com**MinIO는 AD/LDAP 그룹 의 멤버십을 가진 모든 인증 사용자에게 consoleAdmin정책을 할당하여 MinIO 서버에 대한 완전한 액세스 권한을 부여합니다.
  • **cn=engineering,cn=groups,dc=example,dc=com**MinIO는 AD/LDAP 그룹 의 멤버십을 가진 모든 인증 사용자에게 diagnostics정책을 할당하여 진단 관리 작업에 대한 액세스 권한을 부여합니다.

 

 

MinIO 에 LDAP 설정

dc=minio,dc=io
├── cn=developer (사용자)
├── cn=maintainer (사용자)
├── cn=admin_root (사용자)
├── ou=Users (OU)
└── ou=Groups
    ├── cn=Admins (groupOfUniqueNames, member=admin_root)
    └── cn=Maintainers (groupOfUniqueNames, member=maintainer,developer)
    
    ##위 정보의 LDAP을 등록할 예정

 

  • Server Insecure : Enabled
  • Server Address : openldap.default.svc.cluster.local:389
  • Lookup Bind DN* : cn=admin,dc=**minio**,dc=io
  • Lookup Bind Password* : admin123
  • User DN Search Base* : **dc**=minio,dc=io
  • User DN Search Filter* : (cn=%s)

 

 

 

그룹 맵핑 검색 설정

#
mc idp ldap update $MYALIAS \
  group_search_filter="(&(objectClass=groupOfUniqueNames)(uniqueMember=%d))" \
  group_search_base_dn="dc=minio,dc=io" --insecure

mc admin service restart $MYALIAS --insecure

#
mc idp ldap info $MYALIAS --insecure
╭─────────────────────────────────────────────────────────────────────────────╮
│                enable: on                                                   │
│  group_search_base_dn: dc=minio,dc=io                                       │
│   group_search_filter: (&(objectClass=groupOfUniqueNames)(uniqueMember=%d)) │
│        lookup_bind_dn: cn=admin,dc=minio,dc=io                              │
│           server_addr: openldap.default.svc.cluster.local:389               │
│       server_insecure: on                                                   │
│user_dn_search_base_dn: dc=minio,dc=io                                       │
│ user_dn_search_filter: (cn=%s)                                              │
╰─────────────────────────────────────────────────────────────────────────────╯

 

 

그룹맵핑 정책 설정

# 전체 그림
dc=minio,dc=io
├── cn=developer (사용자)
├── cn=maintainer (사용자)
├── cn=admin_root (사용자)
├── ou=Users (OU)
└── ou=Groups
    ├── cn=Admins (groupOfUniqueNames, member=admin_root)
    └── cn=Maintainers (groupOfUniqueNames, member=maintainer,developer)

#
mc idp ldap policy attach $MYALIAS readwrite \
  --group="cn=Maintainers,ou=Groups,dc=minio,dc=io" --insecure
Attached Policies: [readwrite]
To Group: cn=Maintainers,ou=Groups,dc=minio,dc=io

#
mc admin user list $MYALIAS --insecure
enabled    cn=admin_root,dc=...  consoleAdmin

#
mc admin group list $MYALIAS --insecure
cn=Maintainers,ou=Groups,dc=minio,dc=io
devteam

    dn: cn=developer,dc=minio,dc=io
    changetype: add
    objectclass: inetOrgPerson
    cn: developer
    givenname: developer
    sn: Developer
    displayname: Developer User
    mail: developer@minio.io
    userpassword: developer_pass

 

 

SDK(실습에선 Python으로 진행)

기본 설정

#
docker exec -it myk8s-control-plane bash
-----------------------------------------
#
python3 -V

#
apt update && apt install -y python3-pip nano git tree

#
pip3 list 
pip3 install minio --break-system-packages
pip3 show minio

#
git clone https://github.com/minio/minio-py
cd minio-py/examples
tree 
|-- append_object.py
|-- bucket_exists.py
...

#
cat list_buckets.py
from minio import Minio

client = Minio(
    "play.min.io",
    access_key="Q3AM3UQ867SPQQA43P2F",
    secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
)

buckets = client.list_buckets()
for bucket in buckets:
    print(bucket.name, bucket.creation_date)

#
python3 list_buckets.py
...
-----------------------------------------

minio 제공되는 기본 버킷을 확인할 수 있다.

 

devuser 생성

# user 확인
MYALIAS=k8s-tenant1
mc admin user list $MYALIAS --insecure

# devuser 생성 : 시크릿키 devpassword
mc admin user add $MYALIAS devuser devpassword --insecure
mc admin user list $MYALIAS --insecure

# devuser 에 readwrite 정책 부착
mc admin policy attach $MYALIAS readwrite --user devuser --insecure
mc admin policy entities $MYALIAS --policy readwrite --insecure

 

 

Python SDK를 활용하여 버킷 목록 조회

# 아래는 myk8s-controller-plane 컨테이너 내부에서 실행
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d > tenant1.crt
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d | openssl x509 -noout -text
cp tenant1.crt /usr/local/share/ca-certificates/tenant1.crt
update-ca-certificates

#
echo "127.0.0.1 minio.tenant1.svc.cluster.local" >> /etc/hosts

#
cat << EOF > my_list_bucket.py
from minio import Minio

client = Minio(
    "minio.tenant1.svc.cluster.local:30002",
    access_key="devuser",
    secret_key="devpassword",
    secure=True,
    cert_check=False
)

buckets = client.list_buckets()
for bucket in buckets:
    print(bucket.name, bucket.creation_date)
EOF
cat my_list_bucket.py

# 버킷 목록 조회
python3 my_list_bucket.py
mybucket 2025-09-21 04:24:16.252000+00:00

# mc admin trace $MYALIAS --verbose --all --insecure
minio.tenant1.svc.cluster.local:30002 [REQUEST s3.ListBuckets] [2025-09-21T17:36:30.032] [Client IP: 10.244.0.1]
minio.tenant1.svc.cluster.local:30002 GET /
minio.tenant1.svc.cluster.local:30002 Proto: HTTP/1.1
minio.tenant1.svc.cluster.local:30002 Host: minio.tenant1.svc.cluster.local:30002
minio.tenant1.svc.cluster.local:30002 Authorization: AWS4-HMAC-SHA256 Credential=devuser/20250921/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=b73e560231834d88820360e3304ee9fa7ec9f9f26b75de8ed6aef389458fc86d

 

Python SDK를 활용하여 객체 업로드

# 바로 위 sample.json 에서 파일 작성 후 json 파일 확인
cat sample.json | jq

#
cat << EOF > labf.py
from urllib import request

def rprint(result):
	print(
		"Object name: {0}; last modified: {1}".format(
			result.object_name, result.last_modified
		)
	)

def opensky_stream():
	return request.urlopen(
		"https://opensky-network.org/api/states/all",
	)

def obj_head(response):
	print("Object head: {0}".format(response.read(100)))

def eprint(errors):
	for error in errors:
		print("Error occurred while deleting object", error)
EOF

#
cat << EOF > my_put_object.py
from minio import Minio
from minio.credentials import LdapIdentityProvider
from minio.commonconfig import CopySource
from minio.deleteobjects import DeleteObject
from time import sleep
import labf

# Create a client.
client = Minio(
    "minio.tenant1.svc.cluster.local:30002",
    access_key="devuser",
    secret_key="devpassword",
    secure=True,
    cert_check=False
)

result = client.fput_object("mybucket", "sample.json", "sample.json", "application/json")

labf.rprint(result)
EOF

#
python3 my_put_object.py
Object name: sample.json; last modified: None

#
mc ls $MYALIAS --recursive --insecure
[2025-09-21 17:53:11 KST] 6.1KiB STANDARD mybucket/sample.json

 

 

실습 환경을 클라우드 포메이션으로 배포

 

이 스크립트는 CloudFormation 템플릿을 이용해 EC2 기반의 MinIO 환경을 자동 배포하고 관리하는 예제입니다.
스택 생성 시 SSH 키와 접속 허용 IP, 인스턴스 타입 등을 파라미터로 지정할 수 있습니다.
배포 후에는 EC2의 퍼블릭 IP를 확인해 SSH 접속 및 Kubernetes(k3s) 환경 구성을 확인할 수 있습니다.

# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/minio-ec2-1node.yaml

# CloudFormation 스택 배포
# aws cloudformation deploy --template-file kans-7w.yaml --stack-name mylab --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region ap-northeast-2
예시) aws cloudformation deploy --template-file minio-ec2-1node.yaml --stack-name miniolab --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

## Tip. 인스턴스 타입 변경 : MyInstanceType=t3.xlarge (vCPU 4, Mem 16)
예시) aws cloudformation deploy --template-file minio-ec2-1node.yaml --stack-name miniolab --parameter-overrides MyInstanceType=t3.xlarge KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2

# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name miniolab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2

# [모니터링] CloudFormation 스택 상태 : 생성 완료 확인
while true; do 
  date
  AWS_PAGER="" aws cloudformation list-stacks \
    --stack-status-filter CREATE_IN_PROGRESS CREATE_COMPLETE CREATE_FAILED DELETE_IN_PROGRESS DELETE_FAILED \
    --query "StackSummaries[*].{StackName:StackName, StackStatus:StackStatus}" \
    --output table
  sleep 1
done

# 배포된 aws ec2 유동 공인 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output text
k3s-s   43.202.60.44    running

# EC2 SSH 접속 : 바로 접속하지 말고, 3~5분 정도 후에 접속 할 것
ssh -i ~/.ssh/kp-gasida.pem ubuntu@$(aws cloudformation describe-stacks --stack-name miniolab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2)
...
(⎈|default:N/A) root@k3s-s:~# <- kubeps 가 나오지 않을 경우 ssh logout 후 다시 ssh 접속 할 것!

 

 

kc get node -owide
NAME     STATUS   ROLES                  AGE     VERSION         INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION   CONTAINER-RUNTIME
k3s-s    Ready    control-plane,master   2m12s   v1.28.15+k3s1   192.168.10.10    <none>        Ubuntu 22.04.5 LTS   6.8.0-1029-aws   containerd://1.7.22-k3s1.28

hostnamectl 
...
 Hardware Vendor: Amazon EC2
  Hardware Model: t3.xlarge

# Install Krew
wget -P /root "https://github.com/kubernetes-sigs/krew/releases/latest/download/krew-linux_amd64.tar.gz"
tar zxvf "/root/krew-linux_amd64.tar.gz" --warning=no-unknown-keyword
./krew-linux_amd64 install krew
export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH" # export PATH="$PATH:/root/.krew/bin"
echo 'export PATH="$PATH:/root/.krew/bin:/root/go/bin"' >> /etc/profile
kubectl krew install get-all neat rolesum pexec stern
kubectl krew list

이 스크립트는 Kubernetes 플러그인 관리 도구인 Krew를 EC2(k3s 클러스터) 환경에 설치하는 과정입니다.
설치 후 PATH를 설정하여 krew와 플러그인들을 전역에서 사용할 수 있게 합니다.
마지막으로 get-all, neat, rolesum, pexec, stern 등의 유용한 kubectl 플러그인들을 설치하고 목록을 확인합니다.

 

 

#
lsblk -a -o NAME,KNAME,MAJ:MIN,SIZE,TYPE,MOUNTPOINT,FSTYPE,UUID,MODEL,SERIAL
NAME         KNAME      MAJ:MIN  SIZE TYPE MOUNTPOINT                   FSTYPE   UUID                                 MODEL                      SERIAL
...
nvme0n1      nvme0n1    259:0     30G disk                                                                            Amazon Elastic Block Store vol0833a7ec553e64b74
├─nvme0n1p1  nvme0n1p1  259:5     29G part /                            ext4     0eec2352-4b50-40ec-ae93-7ce2911392bb                            
├─nvme0n1p14 nvme0n1p14 259:6      4M part                                                                                                       
├─nvme0n1p15 nvme0n1p15 259:7    106M part /boot/efi                    vfat     2586-E57C                                                       
└─nvme0n1p16 nvme0n1p16 259:8    913M part /boot                        ext4     1eb1aa76-4a46-48d9-95d8-a2ecf2d505c2                            
nvme1n1      nvme1n1    259:1     30G disk                                                                            Amazon Elastic Block Store vol0da5a82ea4ada0efc
nvme3n1      nvme3n1    259:2     30G disk                                                                            Amazon Elastic Block Store vol079fd88ba55971938
nvme2n1      nvme2n1    259:3     30G disk                                                                            Amazon Elastic Block Store vol012134019ba9f1c8a
nvme4n1      nvme4n1    259:4     30G disk                                                                            Amazon Elastic Block Store vol04d49a7ef31a119a2

# fio 설치 되어 있음
apt install fio -y
fio -h

# 측정 기본
## IOPS: 초당 입출력 횟수
## Throughput: 초당 MB 전송량
## Block size, Queue depth, RW 패턴 (랜덤/순차, 읽기/쓰기) 조합에 따라 결과가 달라집니다.
## AWS gp3는 기본 IOPS 3000/Throughput 125MB/s

# 4k 랜덤 읽기/쓰기, iodepth=16, numjobs=4
## --rw=randrw : 랜덤 읽기/쓰기 혼합
## --rwmixread=70 : 70% 읽기 / 30% 쓰기
## --bs=4k : 4KB 블록 → IOPS 측정용
## --iodepth=16 : 큐 깊이 16
## --numjobs=4 : 4개의 병렬 job
## --time_based --runtime=60 : 60초 동안 측정
## --group_reporting : 그룹 단위 결과 요약
fio --name=randrw_test \
  --filename=/mnt/testfile \
  --size=4G \
  --rw=randrw \
  --rwmixread=70 \
  --bs=4k \
  --iodepth=16 \
  --numjobs=4 \
  --time_based \
  --runtime=60 \
  --group_reporting

## 읽기 평균 IOPS : 2388
  read: IOPS=2388, BW=9553KiB/s (9782kB/s)(560MiB/60003msec)

## 쓰기 평균 IOPS : 1030
  write: IOPS=1030, BW=4121KiB/s (4220kB/s)(241MiB/60003msec); 0 zone resets

## 디스크 활용률 : 읽기/쓰기 IO 수량(132k/49k) , 디스크 활용률 87%
Disk stats (read/write):
  nvme0n1: ios=132972/49709, sectors=1063776/420736, merge=0/8, ticks=232695/154017, in_queue=386713, util=87.09%

이 스크립트는 EC2에 연결된 EBS 디스크들의 상태를 확인한 뒤, fio를 이용해 성능을 벤치마크하는 과정입니다.
4KB 블록 단위로 랜덤 읽기·쓰기 테스트를 수행하며, iodepth=16, 병렬 4 job으로 60초간 측정합니다.
결과로 읽기/쓰기 IOPS와 전송량, 디스크 활용률을 확인하여 EBS 성능을 평가할 수 있습니다.

 

 

# mlocate or plocate
systemctl disable --now plocate-updatedb 
systemctl list-timers | grep locate

# updatedb
systemctl disable --now updatedb.timer
systemctl list-timers | grep updatedb

# auditd
systemctl disable --now auditd
systemctl list-timers | grep audit

# Crowdstrike Falcon & Antivirus software (clamav)

MinIO 최적화 튜닝

- 파일 시스템, 시스템 수준 호출 또는 커널 수준 호출을 인덱싱, 스캔 또는 감사하는 시스템 서비스를 비활성화합니다.
- 이러한 서비스는 리소스 경합이나 MinIO 작업 차단으로 인해 성능이 저하될 수 있습니다.
- MinIO는 MinIO를 실행하는 호스트에서 다음 서비스를 제거하거나 비활성화할 것을 강력히 권장합니다.

 

#
sysctl -a > before.txt


# 설치 되어 있음
apt install tuned -y

# 서비스 시작
systemctl start tuned && systemctl enable tuned
cat /usr/lib/systemd/system/tuned.service

# To see the current active profile, run:
tuned-adm active
Current active profile: throughput-performance

# To show the current profile selection mode, run:
tuned-adm profile_mode
Profile selection mode: auto

# To list all available profiles, run:
tuned-adm list
Available profiles:
- accelerator-performance     - Throughput performance based tuning with disabled higher latency STOP states
- aws                         - Optimize for aws ec2 instances
- balanced                    - General non-specialized tuned profile
- balanced-battery            - Balanced profile biased towards power savings changes for battery
- desktop                     - Optimize for the desktop use-case
- hpc-compute                 - Optimize for HPC compute workloads
- intel-sst                   - Configure for Intel Speed Select Base Frequency
- latency-performance         - Optimize for deterministic performance at the cost of increased power consumption
- network-latency             - Optimize for deterministic performance at the cost of increased power consumption, focused on low latency network performance
- network-throughput          - Optimize for streaming network throughput, generally only necessary on older CPUs or 40G+ networks
- optimize-serial-console     - Optimize for serial console use.
- powersave                   - Optimize for low power consumption
- throughput-performance      - Broadly applicable tuning that provides excellent performance across a variety of common server workloads
- virtual-guest               - Optimize for running inside a virtual guest
- virtual-host                - Optimize for running KVM guests
Current active profile: throughput-performance

# To switch to a different profile, run:
## The enabled profile is persisted into /etc/tuned/active_profile, which is read when the daemon starts or is restarted.
tuned-adm profile <profile-name>

# To disable all tunings, run:
tuned-adm off

tuned-adm active
No current active profile.

#
tuned-adm profile virtual-guest

tuned-adm active
Current active profile: virtual-guest

#
sysctl -a > after.txt

#
vi -d before.txt after.txt


# 4k 랜덤 읽기/쓰기, iodepth=16, numjobs=4
fio --name=randrw_test \
  --filename=/mnt/testfile \
  --size=4G \
  --rw=randrw \
  --rwmixread=70 \
  --bs=4k \
  --iodepth=16 \
  --numjobs=4 \
  --time_based \
  --runtime=60 \
  --group_reporting

## 읽기 평균 IOPS : 2388
  read: IOPS=2395, BW=9584KiB/s (9814kB/s)(562MiB/60003msec)

## 읽기 평균 IOPS : 1030
  write: IOPS=1033, BW=4134KiB/s (4233kB/s)(242MiB/60003msec); 0 zone resets

## 디스크 활용률 : 읽기/쓰기 IO 수량(132k/49k) , 디스크 활용률 87%
Disk stats (read/write):
  nvme0n1: ios=133343/49277, sectors=1066744/416808, merge=0/43, ticks=231976/154776, in_queue=386752, util=86.35%

tuned profiles

 

 

DirectPV

DirectPV란 ?

MinIO DirectPV는 AGPL 3.0 라이선스 하에 배포되는 Kubernetes용 Container Storage Interface(CSI) 드라이버로, 직접 연결된 스토리지(Direct Attached Storage)를 위한 분산 퍼시스턴트 볼륨 관리자입니다.

  • DirectPV는 SAN(스토리지 영역 네트워크)이나 NAS(네트워크 연결 스토리지)와 같은 스토리지 시스템 자체가 아니라, 여러 서버에 걸친 로컬 드라이브를 발견(discover), 포맷(format), 마운트(mount), 스케줄(schedule), 모니터링(monitor)하는 기능을 제공합니다.
  • Kubernetes에서 기존 hostPath나 local PV가 정적이고 제한적인 반면, DirectPV는 경량이며 수만 개의 드라이브를 확장 가능하게 관리할 수 있도록 설계되었습니다.

구성요소

  • 명령줄 인터페이스를 통해 DirectPV CSI 드라이버를 관리하기 위해 로컬 머신에 설치된 DirectPV 플러그인
  • 로컬 볼륨을 프로비저닝하는 Kubernetes 클러스터에 직접 설치된 DirectPV CSI 드라이버

 

DirectPV 설치

#
k krew install directpv
k directpv -h

# 
k directpv install
┌──────────────────────────────────────┬──────────────────────────┐
│ NAME                                 │ KIND                     │
├──────────────────────────────────────┼──────────────────────────┤
│ directpv                             │ Namespace                │
│ directpv-min-io                      │ ServiceAccount           │
│ directpv-min-io                      │ ClusterRole              │
│ directpv-min-io                      │ ClusterRoleBinding       │
│ directpv-min-io                      │ Role                     │
│ directpv-min-io                      │ RoleBinding              │
│ directpvdrives.directpv.min.io       │ CustomResourceDefinition │
│ directpvvolumes.directpv.min.io      │ CustomResourceDefinition │
│ directpvnodes.directpv.min.io        │ CustomResourceDefinition │
│ directpvinitrequests.directpv.min.io │ CustomResourceDefinition │
│ directpv-min-io                      │ CSIDriver                │
│ directpv-min-io                      │ StorageClass             │
│ node-server                          │ Daemonset                │
│ controller                           │ Deployment               │
└──────────────────────────────────────┴──────────────────────────┘

#
k get crd | grep min
directpvdrives.directpv.min.io         2025-09-13T05:23:40Z
directpvinitrequests.directpv.min.io   2025-09-13T05:23:40Z
directpvnodes.directpv.min.io          2025-09-13T05:23:40Z
directpvvolumes.directpv.min.io        2025-09-13T05:23:40Z

k get sc directpv-min-io -o yaml | yq
k get sc
NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
directpv-min-io        directpv-min-io         Delete          WaitForFirstConsumer   true                   4m17s

k get-all -n directpv
k get deploy,ds,pod -n directpv
k rolesum directpv-min-io -n directpv

k get directpvnodes.directpv.min.io
k get directpvnodes.directpv.min.io -o yaml | yq


# controller 파드 1대 정보 확인
kc describe pod -n directpv controller-75d5595d5b-bqwbs
...
Containers:
  csi-provisioner:
    ...
    Args:
      --v=3
      --timeout=300s
      --csi-address=$(CSI_ENDPOINT)
      --leader-election
      --feature-gates=Topology=true
      --strict-topology
    ...
    Environment:
      CSI_ENDPOINT:  unix:///csi/csi.sock
    Mounts:
      /csi from socket-dir (rw)
  ...
  csi-resizer:
    ...
    Args:
      --v=3
      --timeout=300s
      --csi-address=$(CSI_ENDPOINT)
      --leader-election
    ...
    Environment:
      CSI_ENDPOINT:  unix:///csi/csi.sock
    Mounts:
      /csi from socket-dir (rw)
  ...
  controller:
    ...
    Args:
      controller
      --identity=directpv-min-io
      -v=3
      --csi-endpoint=$(CSI_ENDPOINT)
      --kube-node-name=$(KUBE_NODE_NAME)
      --readiness-port=30443
    ...
    Environment:
      KUBE_NODE_NAME:   (v1:spec.nodeName)
      CSI_ENDPOINT:    unix:///csi/csi.sock
    Mounts:
      /csi from socket-dir (rw)
...
Volumes:
  socket-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/kubelet/plugins/controller-controller
    HostPathType:  DirectoryOrCreate
...

ls -l /var/lib/kubelet/plugins/controller-controller
total 0
srwxr-xr-x 1 root root 0 Sep 14 11:17 csi.sock

k get lease -n directpv -o yaml | yq
k get lease -n directpv
...

#
k get pod -n directpv -l selector.directpv.min.io.service=enabled
kc describe pod -n directpv -l selector.directpv.min.io.service=enabled
...
Containers:
  node-driver-registrar:
    ...
    Args:
      --v=3
      --csi-address=unix:///csi/csi.sock
      --kubelet-registration-path=/var/lib/kubelet/plugins/directpv-min-io/csi.sock

  node-server:
    ...
    Args:
      node-server
      -v=3
      --identity=directpv-min-io
      --csi-endpoint=$(CSI_ENDPOINT)
      --kube-node-name=$(KUBE_NODE_NAME)
      --readiness-port=30443
      --metrics-port=10443

  node-controller:
    ...
    Args:
      node-controller
      -v=3
      --kube-node-name=$(KUBE_NODE_NAME)

  liveness-probe:
    ...
    Args:
      --csi-address=/csi/csi.sock
      --health-port=9898

Volumes:
  socket-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/kubelet/plugins/directpv-min-io
    HostPathType:  DirectoryOrCreate
  mountpoint-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/kubelet/pods
    HostPathType:  DirectoryOrCreate
  registration-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/kubelet/plugins_registry
    HostPathType:  DirectoryOrCreate
  plugins-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/kubelet/plugins
    HostPathType:  DirectoryOrCreate
  directpv-common-root:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/directpv/
    HostPathType:  DirectoryOrCreate
  direct-csi-common-root:
    Type:          HostPath (bare host directory volume)
    Path:          /var/lib/direct-csi/
    HostPathType:  DirectoryOrCreate
  sysfs:
    Type:          HostPath (bare host directory volume)
    Path:          /sys
    HostPathType:  DirectoryOrCreate
  devfs:
    Type:          HostPath (bare host directory volume)
    Path:          /dev
    HostPathType:  DirectoryOrCreate
  run-udev-data-dir:
    Type:          HostPath (bare host directory volume)
    Path:          /run/udev/data
    HostPathType:  DirectoryOrCreate
...

 

DirectPV로 물리디스크 관리하기

#
k directpv info
┌─────────┬──────────┬───────────┬─────────┬────────┐
│ NODE    │ CAPACITY │ ALLOCATED │ VOLUMES │ DRIVES │
├─────────┼──────────┼───────────┼─────────┼────────┤
│ • k3s-s │ -        │ -         │ -       │ -      │
└─────────┴──────────┴───────────┴─────────┴────────┘

#
k directpv discover
 Discovered node 'k3s-s' ✔
┌─────────────────────┬───────┬─────────┬────────┬────────────┬────────────────────────────┬───────────┬─────────────┐
│ ID                  │ NODE  │ DRIVE   │ SIZE   │ FILESYSTEM │ MAKE                       │ AVAILABLE │ DESCRIPTION │
├─────────────────────┼───────┼─────────┼────────┼────────────┼────────────────────────────┼───────────┼─────────────┤
│ 259:1$Pna+Q57t+y... │ k3s-s │ nvme1n1 │ 30 GiB │ -          │ Amazon Elastic Block Store │ YES       │ -           │
│ 259:3$QURUJfcllF... │ k3s-s │ nvme2n1 │ 30 GiB │ -          │ Amazon Elastic Block Store │ YES       │ -           │
│ 259:2$/28TF0pThR... │ k3s-s │ nvme3n1 │ 30 GiB │ -          │ Amazon Elastic Block Store │ YES       │ -           │
│ 259:4$+AaiQHygVH... │ k3s-s │ nvme4n1 │ 30 GiB │ -          │ Amazon Elastic Block Store │ YES       │ -           │
└─────────────────────┴───────┴─────────┴────────┴────────────┴────────────────────────────┴───────────┴─────────────┘
Generated 'drives.yaml' successfully.

# (참고) 적용 예외 설정 시 select: "no" 설정
cat drives.yaml | yq
{
  "version": "v1",
  "nodes": [
    {
      "name": "k3s-s",
      "drives": [
        {
          "id": "259:4$+AaiQHygVHSl7AmoNWxVgP1kJsoha2CeGIzxfuoqBvw=",
          "name": "nvme4n1",
          "size": 32212254720,
          "make": "Amazon Elastic Block Store",
          "select": "yes"
        },
...

#
k directpv init drives.yaml
ERROR Initializing the drives will permanently erase existing data. Please review carefully before performing this *DANGEROUS* operation and retry this command with --dangerous flag.

# Perform initialization of drives which will permanently erase existing data
k directpv init drives.yaml --dangerous
Processed initialization request '75caf826-e374-4ff2-b4d6-bdbf1ac0e847' for node 'k3s-s' ✔

┌──────────────────────────────────────┬───────┬─────────┬─────────┐
│ REQUEST_ID                           │ NODE  │ DRIVE   │ MESSAGE │
├──────────────────────────────────────┼───────┼─────────┼─────────┤
│ 75caf826-e374-4ff2-b4d6-bdbf1ac0e847 │ k3s-s │ nvme1n1 │ Success │
│ 75caf826-e374-4ff2-b4d6-bdbf1ac0e847 │ k3s-s │ nvme2n1 │ Success │
│ 75caf826-e374-4ff2-b4d6-bdbf1ac0e847 │ k3s-s │ nvme3n1 │ Success │
│ 75caf826-e374-4ff2-b4d6-bdbf1ac0e847 │ k3s-s │ nvme4n1 │ Success │
└──────────────────────────────────────┴───────┴─────────┴─────────┘

#
k directpv list drives
┌───────┬─────────┬────────────────────────────┬────────┬────────┬─────────┬────────┐
│ NODE  │ NAME    │ MAKE                       │ SIZE   │ FREE   │ VOLUMES │ STATUS │
├───────┼─────────┼────────────────────────────┼────────┼────────┼─────────┼────────┤
│ k3s-s │ nvme1n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
│ k3s-s │ nvme2n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
│ k3s-s │ nvme3n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
│ k3s-s │ nvme4n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
└───────┴─────────┴────────────────────────────┴────────┴────────┴─────────┴────────┘

k directpv info
┌─────────┬──────────┬───────────┬─────────┬────────┐
│ NODE    │ CAPACITY │ ALLOCATED │ VOLUMES │ DRIVES │
├─────────┼──────────┼───────────┼─────────┼────────┤
│ • k3s-s │ 120 GiB  │ 0 B       │ 0       │ 4      │
└─────────┴──────────┴───────────┴─────────┴────────┘
0 B/120 GiB used, 0 volumes, 4 drives


lsblk
NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
...
nvme1n1      259:1    0   30G  0 disk /var/lib/directpv/mnt/03ab4af0-14e3-47d2-ae39-2742c986a71d
nvme3n1      259:2    0   30G  0 disk /var/lib/directpv/mnt/8d0c7b6f-07f8-4908-bca6-5c14ad068400
nvme2n1      259:3    0   30G  0 disk /var/lib/directpv/mnt/324af346-29f1-4a9b-93bf-fbae1f21302d
nvme4n1      259:4    0   30G  0 disk /var/lib/directpv/mnt/088e2e11-8708-4a6c-8466-631c89c03f5c

df -hT --type xfs
/dev/nvme1n1    xfs        30G  248M   30G   1% /var/lib/directpv/mnt/03ab4af0-14e3-47d2-ae39-2742c986a71d
/dev/nvme4n1    xfs        30G  248M   30G   1% /var/lib/directpv/mnt/088e2e11-8708-4a6c-8466-631c89c03f5c
/dev/nvme3n1    xfs        30G  248M   30G   1% /var/lib/directpv/mnt/8d0c7b6f-07f8-4908-bca6-5c14ad068400
/dev/nvme2n1    xfs        30G  248M   30G   1% /var/lib/directpv/mnt/324af346-29f1-4a9b-93bf-fbae1f21302d

tree -h /var/lib/directpv/
[4.0K]  /var/lib/directpv/
├── [4.0K]  mnt
│   ├── [  75]  03ab4af0-14e3-47d2-ae39-2742c986a71d
│   ├── [  75]  088e2e11-8708-4a6c-8466-631c89c03f5c
│   ├── [  75]  324af346-29f1-4a9b-93bf-fbae1f21302d
│   └── [  75]  8d0c7b6f-07f8-4908-bca6-5c14ad068400
└── [  40]  tmp

#
k get directpvdrives.directpv.min.io -o yaml | yq
k get directpvdrives.directpv.min.io
NAME                                   AGE
03ab4af0-14e3-47d2-ae39-2742c986a71d   5m58s
088e2e11-8708-4a6c-8466-631c89c03f5c   5m58s
324af346-29f1-4a9b-93bf-fbae1f21302d   5m58s
8d0c7b6f-07f8-4908-bca6-5c14ad068400   5m58s

#
cat /etc/fstab

 

#
df -hT --type xfs
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/nvme4n1   xfs    30G  248M   30G   1% /var/lib/directpv/mnt/ab82b195-5204-47fa-a864-a351f66b38d2
/dev/nvme2n1   xfs    30G  248M   30G   1% /var/lib/directpv/mnt/94be15e6-5d0d-49a9-a6d8-d139977a3446
/dev/nvme1n1   xfs    30G  248M   30G   1% /var/lib/directpv/mnt/dc26b422-5364-464e-bc54-a6ab6b8d4e32
/dev/nvme3n1   xfs    30G  248M   30G   1% /var/lib/directpv/mnt/246f427a-3e32-4311-b81c-fc9e45ebc6ad

lsblk
...
nvme1n1      259:0    0   30G  0 disk /var/lib/directpv/mnt/dc26b422-5364-464e-bc54-a6ab6b8d4e32
nvme3n1      259:2    0   30G  0 disk /var/lib/directpv/mnt/246f427a-3e32-4311-b81c-fc9e45ebc6ad
nvme4n1      259:3    0   30G  0 disk /var/lib/directpv/mnt/ab82b195-5204-47fa-a864-a351f66b38d2
nvme2n1      259:4    0   30G  0 disk /var/lib/directpv/mnt/94be15e6-5d0d-49a9-a6d8-d139977a3446

k get directpvdrives,directpvvolumes

#
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
spec:
  volumeMode: Filesystem
  storageClassName: directpv-min-io
  accessModes: [ "ReadWriteOnce" ]
  resources:
    requests:
      storage: 8Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  volumes:
    - name: nginx-volume
      persistentVolumeClaim:
        claimName: nginx-pvc
  containers:
    - name: nginx-container
      image: nginx:alpine
      volumeMounts:
        - mountPath: "/mnt"
          name: nginx-volume
EOF

#
k get pod,pvc,pv
NAME            READY   STATUS    RESTARTS   AGE
pod/nginx-pod   1/1     Running   0          2m27s

NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/nginx-pvc   Bound    pvc-97418e78-41db-4244-a3d5-e56b94c6c2bd   8Mi        RWO            directpv-min-io   <unset>                 2m27s

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS      VOLUMEATTRIBUTESCLASS   REASON   AGE
persistentvolume/pvc-97418e78-41db-4244-a3d5-e56b94c6c2bd   8Mi        RWO            Delete           Bound    default/nginx-pvc   directpv-min-io   <unset>                          2m27s

k exec -it nginx-pod -- df -hT -t xfs
Filesystem           Type            Size      Used Available Use% Mounted on
/dev/nvme3n1         xfs             8.0M      4.0K      8.0M   0% /mnt

k exec -it nginx-pod -- sh -c 'echo hello > /mnt/hello.txt'
k exec -it nginx-pod -- sh -c 'cat /mnt/hello.txt'
hello

#
lsblk
nvme1n1      259:0    0   30G  0 disk /var/lib/directpv/mnt/dc26b422-5364-464e-bc54-a6ab6b8d4e32
nvme4n1      259:3    0   30G  0 disk /var/lib/directpv/mnt/ab82b195-5204-47fa-a864-a351f66b38d2
nvme2n1      259:4    0   30G  0 disk /var/lib/directpv/mnt/94be15e6-5d0d-49a9-a6d8-d139977a3446
nvme3n1      259:2    0   30G  0 disk /var/lib/kubelet/pods/171f29cf-45e1-458f-b20e-082a78702e03/volumes/kubernetes.io~csi/pvc-97418e78-41db-4244-a3d5-e56b94c6c2bd/mount
                                      /var/lib/kubelet/plugins/kubernetes.io/csi/directpv-min-io/0a26dd2aeb3b456582028ecddabce74e4684252502203e78f857fa23e738649b/globalmount
                                      /var/lib/directpv/mnt/246f427a-3e32-4311-b81c-fc9e45ebc6ad

tree -a /var/lib/directpv/mnt
/var/lib/directpv/mnt
├── 246f427a-3e32-4311-b81c-fc9e45ebc6ad
│   ├── .FSUUID.246f427a-3e32-4311-b81c-fc9e45ebc6ad -> .
│   ├── .directpv
│   │   └── meta.info
│   └── pvc-97418e78-41db-4244-a3d5-e56b94c6c2bd
│       └── hello.txt
├── 94be15e6-5d0d-49a9-a6d8-d139977a3446
│   ├── .FSUUID.94be15e6-5d0d-49a9-a6d8-d139977a3446 -> .
│   └── .directpv
│       └── meta.info
├── ab82b195-5204-47fa-a864-a351f66b38d2
│   ├── .FSUUID.ab82b195-5204-47fa-a864-a351f66b38d2 -> .
│   └── .directpv
│       └── meta.info
└── dc26b422-5364-464e-bc54-a6ab6b8d4e32
    ├── .FSUUID.dc26b422-5364-464e-bc54-a6ab6b8d4e32 -> .
    └── .directpv
        └── meta.info
        
cat /var/lib/directpv/mnt/*/pvc*/hello.txt
hello

#
k get directpvvolumes
k get directpvvolumes -o yaml | yq


#
k delete pod nginx-pod
k get pvc,pv
k delete pvc nginx-pvc
k get pv

#
lsblk
tree -a /var/lib/directpv/mnt

 

특정 Drive에 볼륨 사용 테스트(label)

#
k directpv list drives
┌───────┬─────────┬────────────────────────────┬────────┬────────┬─────────┬────────┐
│ NODE  │ NAME    │ MAKE                       │ SIZE   │ FREE   │ VOLUMES │ STATUS │
├───────┼─────────┼────────────────────────────┼────────┼────────┼─────────┼────────┤
│ k3s-s │ nvme1n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
│ k3s-s │ nvme2n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
│ k3s-s │ nvme3n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
│ k3s-s │ nvme4n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │
└───────┴─────────┴────────────────────────────┴────────┴────────┴─────────┴────────┘

# Label the 'nvme1n1' drive in all nodes as 'fast' with the 'tier' key.
kubectl directpv label drives --drives=nvme1n1 tier=fast

# Verify that the labels are properly set by using the list command with the
k directpv list drives --show-labels
kubectl directpv list drives --drives /dev/nvme1n1 --show-labels
┌───────┬─────────┬────────────────────────────┬────────┬────────┬─────────┬────────┬───────────┐
│ NODE  │ NAME    │ MAKE                       │ SIZE   │ FREE   │ VOLUMES │ STATUS │ LABELS    │
├───────┼─────────┼────────────────────────────┼────────┼────────┼─────────┼────────┼───────────┤
│ k3s-s │ nvme1n1 │ Amazon Elastic Block Store │ 30 GiB │ 30 GiB │ -       │ Ready  │ tier=fast │
└───────┴─────────┴────────────────────────────┴────────┴────────┴─────────┴────────┴───────────┘

#
k get sc
vi create-storage-class.sh
chmod +x create-storage-class.sh
./create-storage-class.sh -h

#
./create-storage-class.sh fast-tier-storage 'directpv.min.io/tier: fast'
k get sc
kc describe sc fast-tier-storage
...
Parameters:            directpv.min.io/tier=fast,fstype=xfs
...

#
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
spec:
  volumeMode: Filesystem
  storageClassName: fast-tier-storage
  accessModes: [ "ReadWriteOnce" ]
  resources:
    requests:
      storage: 8Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  volumes:
    - name: nginx-volume
      persistentVolumeClaim:
        claimName: nginx-pvc
  containers:
    - name: nginx-container
      image: nginx:alpine
      volumeMounts:
        - mountPath: "/mnt"
          name: nginx-volume
EOF

# 해당 drive가 있는 node 에 pv 가 만들어지고, 파드 역시 해당 node 에 기동되었음
k get pod,pvc,pv -owide
NAME            READY   STATUS    RESTARTS   AGE   IP           NODE    NOMINATED NODE   READINESS GATES
pod/nginx-pod   1/1     Running   0          11s   10.42.0.13   k3s-s   <none>           <none>

NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS        VOLUMEATTRIBUTESCLASS   AGE   VOLUMEMODE
persistentvolumeclaim/nginx-pvc   Bound    pvc-127ee2bb-b4b6-4cc5-bfdd-d79b2e7e5957   8Mi        RWO            fast-tier-storage   <unset>                 11s   Filesystem

NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS        VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pvc-127ee2bb-b4b6-4cc5-bfdd-d79b2e7e5957   8Mi        RWO            Delete           Bound    default/nginx-pvc   fast-tier-storage   <unset>                          11s   Filesystem

kc describe pv
...
Node Affinity:
  Required Terms:
    Term 0:        directpv.min.io/identity in [directpv-min-io]
                   directpv.min.io/node in [k3s-s]
                   directpv.min.io/rack in [default]
                   directpv.min.io/region in [default]
                   directpv.min.io/zone in [default]
Message:
Source:
    Type:              CSI (a Container Storage Interface (CSI) volume source)
    Driver:            directpv-min-io
    FSType:            xfs
    VolumeHandle:      pvc-127ee2bb-b4b6-4cc5-bfdd-d79b2e7e5957
    ReadOnly:          false
    VolumeAttributes:      directpv.min.io/tier=fast
                           fstype=xfs
                           storage.kubernetes.io/csiProvisionerIdentity=1757816235335-1863-directpv-min-io

#
kc describe node
...
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/instance-type=k3s
                    beta.kubernetes.io/os=linux
                    directpv.min.io/identity=directpv-min-io
                    directpv.min.io/node=k3s-s
                    directpv.min.io/rack=default
                    directpv.min.io/region=default
                    directpv.min.io/zone=default
...

#
k get directpvnodes.directpv.min.io -o yaml | yq
kc describe directpvdrives.directpv.min.io
Name:         dc26b422-5364-464e-bc54-a6ab6b8d4e32
Namespace:
Labels:       directpv.min.io/access-tier=Default
              directpv.min.io/created-by=directpv-driver
              directpv.min.io/drive-name=nvme1n1
              directpv.min.io/node=k3s-s
              directpv.min.io/tier=fast
              directpv.min.io/version=v1beta1

# 삭제
kubectl delete pod nginx-pod && kubectl delete pvc nginx-pvc

 

 

 

라벨 활용법

#
k label nodes k3s-s directpv.min.io/rack=rack01 --overwrite
node/k3s-s labeled

kc describe node
...
                    directpv.min.io/rack=rack01

kc describe directpvdrives.directpv.min.io
...
 Topology:
   directpv.min.io/rack:      default

#
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
spec:
  volumeMode: Filesystem
  storageClassName: fast-tier-storage
  accessModes: [ "ReadWriteOnce" ]
  resources:
    requests:
      storage: 8Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  volumes:
    - name: nginx-volume
      persistentVolumeClaim:
        claimName: nginx-pvc
  containers:
    - name: nginx-container
      image: nginx:alpine
      volumeMounts:
        - mountPath: "/mnt"
          name: nginx-volume
EOF

#
k get pod,pvc,pv
NAME            READY   STATUS    RESTARTS   AGE
pod/nginx-pod   0/1     Pending   0          4s

NAME                              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS        VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/nginx-pvc   Pending                                      fast-tier-storage   <unset>                 4s

#
kc describe pod
...
Events:
  Type     Reason            Age               From               Message
  ----     ------            ----              ----               -------
  Warning  FailedScheduling  15s               default-scheduler  running PreBind plugin "VolumeBinding": binding volumes: provisioning failed for PVC "nginx-pvc"
  Warning  FailedScheduling  9s (x2 over 13s)  default-scheduler  running PreBind plugin "VolumeBinding": binding volumes: provisioning failed for PVC "nginx-pvc"

# no drive found for requested topology
kc describe pvc
...
Events:
  Type     Reason                Age                From                                                                              Message
  ----     ------                ----               ----                                                                              -------
  Normal   WaitForFirstConsumer  66s                persistentvolume-controller                                                       waiting for first consumer to be created before binding
  Normal   ExternalProvisioning  7s (x9 over 66s)   persistentvolume-controller                                                       Waiting for a volume to be created either by the external provisioner 'directpv-min-io' or manually by the system administrator. If volume creation is delayed, please verify that the provisioner is running and correctly registered.
  Normal   Provisioning          7s (x8 over 66s)   directpv-min-io_controller-75d5595d5b-bqwbs_4e34d69c-36a7-4c94-8ce0-a02df9c63fc8  External provisioner is provisioning volume for claim "default/nginx-pvc"
  Warning  ProvisioningFailed    7s (x8 over 66s)   directpv-min-io_controller-75d5595d5b-bqwbs_4e34d69c-36a7-4c94-8ce0-a02df9c63fc8  failed to provision volume with StorageClass "fast-tier-storage": rpc error: code = ResourceExhausted desc = no drive found for requested topology; requested node(s): k3s-s; requested size: 8388608 bytes

# 해결 1 : label 값 원복
k label nodes k3s-s directpv.min.io/rack=default --overwrite

# 해결 2 : directpvdrives CR에 label 값 directpv.min.io/rack=rack01 로 수정

# 확인
k get pod,pvc,pv

# 삭제
kubectl delete pod nginx-pod && kubectl delete pvc nginx-pvc

 

  Warning  ProvisioningFailed    7s (x5 over 30s)  directpv-min-io_controller-5db75d68cc-tx8vw_4d6e6fba-d62e-4a05-bd6c-82fc8a68a4c9  failed to provision volume with StorageClass "fast-tier-storage": rpc error: code = ResourceExhausted desc = no drive found for requested topology; requested node(s): k3s-s; requested size: 8388608 bytes

 

 

 

MinIO 설치  & 버킷 생성

#
helm repo add minio-operator https://operator.min.io

# https://github.com/minio/operator/blob/master/helm/operator/values.yaml
cat << EOF > minio-operator-values.yaml
operator:  
  env:
  - name: MINIO_OPERATOR_RUNTIME
    value: "Rancher"
  replicaCount: 1
EOF
helm install --namespace minio-operator --create-namespace minio-operator minio-operator/operator --values minio-operator-values.yaml

# 확인 : 참고로 현재는 오퍼레이터 관리 웹 미제공
k get-all -n minio-operator
k get pod,svc,ep -n minio-operator
k get crd
k exec -it -n minio-operator deploy/minio-operator -- env | grep MINIO
MINIO_OPERATOR_RUNTIME=Rancher

------------------------------------------------------
# If using Amazon Elastic Block Store (EBS) CSI driver : Please make sure to set xfs for "csi.storage.k8s.io/fstype" parameter under StorageClass.parameters.
k get sc directpv-min-io -o yaml | grep -i fstype
  csi.storage.k8s.io/fstype: xfs

  
# tenant values : https://github.com/minio/operator/blob/master/helm/tenant/values.yaml
cat << EOF > minio-tenant-1-values.yaml
tenant:
  name: tenant1

  configSecret:
    name: tenant1-env-configuration
    accessKey: minio
    secretKey: minio123

  pools:
    - servers: 1
      name: pool-0
      volumesPerServer: 4
      size: 10Gi 
      storageClassName: directpv-min-io
  env:
    - name: MINIO_STORAGE_CLASS_STANDARD
      value: "EC:1"

  metrics:
    enabled: true
    port: 9000
    protocol: http
EOF
helm install --namespace tenant1 --create-namespace --values minio-tenant-1-values.yaml tenant1 minio-operator/tenant \
 && kubectl get tenants -A -w

#
kubectl get tenants -n tenant1
kubectl get tenants -n tenant1 -owide -o yaml | yq
kc describe tenants -n tenant1

#
lsblk
k directpv info
k directpv list drives
k directpv list volumes
┌──────────────────────────────────────────┬──────────┬───────┬─────────┬──────────────────┬──────────────┬─────────┐
│ VOLUME                                   │ CAPACITY │ NODE  │ DRIVE   │ PODNAME          │ PODNAMESPACE │ STATUS  │
├──────────────────────────────────────────┼──────────┼───────┼─────────┼──────────────────┼──────────────┼─────────┤
│ pvc-ae274681-c458-409c-a61c-a018f26e2580 │ 10 GiB   │ k3s-s │ nvme1n1 │ myminio-pool-0-0 │ tenant-0     │ Bounded │
│ pvc-8f8df2f7-7f93-4531-b0da-1dbc8affa8df │ 10 GiB   │ k3s-s │ nvme2n1 │ myminio-pool-0-0 │ tenant-0     │ Bounded │
│ pvc-0ab32dfd-0330-405e-8902-78f917bd71ec │ 10 GiB   │ k3s-s │ nvme3n1 │ myminio-pool-0-0 │ tenant-0     │ Bounded │
│ pvc-bfc21d48-87aa-4bf0-ab3f-c1b9acd6b716 │ 10 GiB   │ k3s-s │ nvme4n1 │ myminio-pool-0-0 │ tenant-0     │ Bounded │
└──────────────────────────────────────────┴──────────┴───────┴─────────┴──────────────────┴──────────────┴─────────┘

k get directpvvolumes.directpv.min.io
k get directpvvolumes.directpv.min.io -o yaml | yq
kc describe directpvvolumes
tree -ah /var/lib/kubelet/plugins
tree -ah /var/lib/directpv/mnt
cat /var/lib/kubelet/plugins/kubernetes.io/csi/directpv-min-io/*/vol_data.json
{"driverName":"directpv-min-io","volumeHandle":"pvc-0ab32dfd-0330-405e-8902-78f917bd71ec"}
{"driverName":"directpv-min-io","volumeHandle":"pvc-8f8df2f7-7f93-4531-b0da-1dbc8affa8df"}
{"driverName":"directpv-min-io","volumeHandle":"pvc-ae274681-c458-409c-a61c-a018f26e2580"}
{"driverName":"directpv-min-io","volumeHandle":"pvc-bfc21d48-87aa-4bf0-ab3f-c1b9acd6b716"}

#
k get pvc -n tenant1
k get pvc -n tenant1 -o yaml | yq
kc describe pvc -n tenant1


#
kubectl get sts,pod,svc,ep,pvc,secret -n tenant1
kubectl get pod -n tenant1 -l v1.min.io/pool=pool-0 -owide
kc describe pod -n tenant1 -l v1.min.io/pool=pool-0
kubectl stern -n tenant1 -l v1.min.io/pool=pool-0
kubectl exec -it -n tenant1 sts/tenant1-pool-0 -c minio -- id
kubectl exec -it -n tenant1 sts/tenant1-pool-0 -c minio -- env
kubectl exec -it -n tenant1 sts/tenant1-pool-0 -c minio -- cat /tmp/minio/config.env
kubectl get secret -n tenant1 tenant1-env-configuration -o jsonpath='{.data.config\.env}' | base64 -d ; echo
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d | openssl x509 -noout -text
...
            X509v3 Subject Alternative Name:
                DNS:tenant1-pool-0-0.tenant1-hl.tenant1.svc.cluster.local, DNS:minio.tenant1.svc.cluster.local, DNS:minio.tenant1, DNS:minio.tenant1.svc, DNS:*., DNS:*.tenant1.svc.cluster.local
                
#
kubectl patch svc -n tenant1 tenant1-console -p '{"spec": {"type": "NodePort", "ports": [{"port": 9443, "targetPort": 9443, "nodePort": 30001}]}}'

# 기본키(minio , minio123)
echo "https://$(curl -s ipinfo.io/ip):30001"

#
kubectl patch svc -n tenant1 minio -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 9000, "nodePort": 30002}]}}'


# mc alias
mc alias set k8s-tenant1 https://127.0.0.1:30002 minio minio123 --insecure
mc alias list
mc admin info k8s-tenant1 --insecure

# 버킷 생성
mc mb k8s-tenant1/mybucket --insecure
mc ls k8s-tenant1 --insecure

 

 

 

 

# mc alias
mc alias set k8s-tenant1 https://127.0.0.1:30002 minio minio123 --insecure
mc alias list
...
k8s-tenant1
  URL       : https://127.0.0.1:30002
  AccessKey : minio
  SecretKey : minio123
  API       : s3v4
  Path      : auto
  Src       : /root/.mc/config.json
...

tree -a ~/.mc
/root/.mc
├── certs # certificates
│   └── CAs # Certificate Authorities
├── config.json 
├── config.json.old
└── share
    ├── downloads.json
    └── uploads.json
    
mc admin info k8s-tenant1 --insecure

# 버킷 생성
mc mb k8s-tenant1/mybucket --insecure
mc ls k8s-tenant1 --insecure

# 바로 위에서 설정한 내용
------------------------------------------
# 아래 부터 실습 진행

# [신규 터미널] 모니터링 Show HTTP call trace for all incoming and internode on MinIO
# https://docs.min.io/community/minio-object-store/reference/minio-mc-admin/mc-admin-trace.html
mc admin trace -v -a k8s-tenant1 --insecure
# mc admin trace --errors -v -a k8s-tenant1 --insecure # 4xx, 5xx 에러만 보기
     
# https://docs.min.io/community/minio-object-store/reference/minio-mc/mc-ls.html
mc ls --summarize --recursive k8s-tenant1 --insecure

# https://docs.min.io/community/minio-object-store/reference/minio-mc/mc-cat.html
echo hello > hello.txt
mc cp ./hello.txt k8s-tenant1/mybucket/ --insecure
mc cat k8s-tenant1/mybucket/hello.txt --insecure

# 100MB 파일 생성
</dev/urandom tr -dc 'A-Za-z0-9' | head -c 100M > randtext.txt
ls -alh randtext.txt
-rw-r--r-- 1 root root 100M Sep 14 15:08 randtext.txt

cat randtext.txt | md5sum

# https://docs.min.io/community/minio-object-store/reference/minio-mc/mc-cp.html
mc cp ./randtext.txt k8s-tenant1/mybucket/ --insecure
/root/randtext.txt:     100.00 MiB / 100.00 MiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 320.47 MiB/s 0s

mc cat k8s-tenant1/mybucket/randtext.txt --insecure | md5sum

# MinIO Multipart Upload는 '큰 파일을 S3 방식으로 업로드할 때 쪼개서 올리고, 다 올리면 서버가 합쳐주는' 기능
[Client] ---CreateMultipartUpload---> [MinIO]
         <--- UploadId ---------------
[Client] ---UploadPart(part1)--------> [MinIO]
[Client] ---UploadPart(part2)--------> [MinIO]
...
[Client] ---CompleteMultipartUpload--> [MinIO]
         <--- OK + Final Object -------
mc admin trace -v -a k8s-tenant1 --insecure
127.0.0.1:30002 [REQUEST s3.CompleteMultipartUpload] [2025-09-14T15:11:04.710] [Client IP: 10.42.0.1]
127.0.0.1:30002 POST /mybucket/randtext.txt?uploadId=ZTMyZjNlMjAtZDkyNi00YjcyLTlhNGItZTI4ODBiNzBmZTM1LmY3N2ZiOTY0LWM5NmItNDE5YS1iNThmLWY3ODAyM2FmY2JjM3gxNzU3ODMwMjY0NDE5MzA0NDQ1
127.0.0.1:30002 Proto: HTTP/1.1
127.0.0.1:30002 Host: 127.0.0.1:30002
127.0.0.1:30002 Content-Type: application/octet-stream
127.0.0.1:30002 User-Agent: MinIO (linux; amd64) minio-go/v7.0.90 mc/RELEASE.2025-08-13T08-35-41Z
127.0.0.1:30002 X-Amz-Content-Sha256: 159a60374060e572e313b089f1d4658285b3676e38c08cdba940c0342f7431a9
127.0.0.1:30002 X-Amz-Date: 20250914T061104Z
127.0.0.1:30002 Accept-Encoding: zstd,gzip
127.0.0.1:30002 Authorization: AWS4-HMAC-SHA256 Credential=minio/20250914/us-east-1/s3/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=3006225675b1e83ec73a4acdc49e14912ea547e075be739cc9a580ebde31a858
127.0.0.1:30002 Content-Length: 687
127.0.0.1:30002 <CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Part><PartNumber>1</PartNumber><ETag>2e7fda86cc02f78e25480c65efb94a29</ETag></Part><Part><PartNumber>2</PartNumber><ETag>e2b66d6f0368ce9a13de6ca12ed78a90</ETag></Part><Part><PartNumber>3</PartNumber><ETag>7b0ae1958b0f62992b13dfe4a7dddf07</ETag></Part><Part><PartNumber>4</PartNumber><ETag>942130b42c320b33a77ad9812416546b</ETag></Part><Part><PartNumber>5</PartNumber><ETag>81bea9efbeb94cadacb210343698806d</ETag></Part><Part><PartNumber>6</PartNumber><ETag>89c6655445c13b36c83a37b8b8e589c1</ETag></Part><Part><PartNumber>7</PartNumber><ETag>084d78f3c6126f91dc72c1933754ec46</ETag></Part></CompleteMultipartUpload>

127.0.0.1:30002 [RESPONSE] [2025-09-14T15:11:04.717] [ Duration 7.469ms TTFB 7.391942ms ↑ 793 B  ↓ 321 B ]
...
<CompleteMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Location>https://127.0.0.1:30002/mybucket/randtext.txt</Location><Bucket>mybucket</Bucket><Key>randtext.txt</Key><ETag>&#34;2ab7293624b88db302add6a2cacdd804-7&#34;</ETag></CompleteMultipartUploadResult>
# 참고) put https://docs.min.io/community/minio-object-store/reference/minio-mc/mc-put.html


#
cp randtext.txt randtext2.txt
mc cp ./randtext2.txt k8s-tenant1/mybucket/ --disable-multipart --insecure
/root/randtext2.txt:    100.00 MiB / 100.00 MiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 290.70 MiB/s 0s

#
mc ls --summarize --recursive --versions k8s-tenant1 --insecure
[2025-09-14 14:36:17 KST]     0B mybucket/
[2025-09-14 15:11:04 KST] 100MiB STANDARD null v1 PUT mybucket/randtext.txt
[2025-09-14 15:17:43 KST] 100MiB STANDARD null v1 PUT mybucket/randtext2.txt
Total Size: 200 MiB
Total Objects: 3

# 
k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- sh -c 'df -hT --type xfs'
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/nvme2n1   xfs    10G   67M   10G   1% /export0
/dev/nvme1n1   xfs    10G   67M   10G   1% /export1
/dev/nvme3n1   xfs    10G   67M   10G   1% /export2
/dev/nvme4n1   xfs    10G   67M   10G   1% /export3

k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- ls -R /export0
k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- ls -R /export1
k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- ls -R /export2
k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- ls -R /export3

#
tree -a /var/lib/directpv/mnt
...
            └── mybucket
                ├── randtext.txt
                │   ├── 13a96fc7-0dc5-48c2-b949-24ec24322d31
                │   │   ├── part.1
                │   │   ├── part.2
                │   │   ├── part.3
                │   │   ├── part.4
                │   │   ├── part.5
                │   │   ├── part.6
                │   │   └── part.7
                │   └── xl.meta
                └── randtext2.txt
                    ├── 287967c0-a38e-4734-b797-36ea1c76147a
                    │   └── part.1
                    └── xl.meta

# rm - https://docs.min.io/community/minio-object-store/reference/minio-mc/mc-rm.html
mc rm k8s-tenant1/mybucket/randtext2.txt --insecure
127.0.0.1:30002 [REQUEST s3.DeleteMultipleObjects] [2025-09-14T15:34:27.955] [Client IP: 10.42.0.1]
127.0.0.1:30002 POST /mybucket/?delete=
...

#
mc cp k8s-tenant1/mybucket/randtext.txt ./randtext3.txt  --insecure
127.0.0.1:9000  [OS os.OpenFileR] [2025-09-14T15:46:10.530] /export3/data/mybucket/randtext.txt/13a96fc7-0dc5-48c2-b949-24ec24322d31/part.4 25.794µs
127.0.0.1:9000  [OS os.OpenFileR] [2025-09-14T15:46:10.530] /export1/data/mybucket/randtext.txt/13a96fc7-0dc5-48c2-b949-24ec24322d31/part.4 32.617µs
127.0.0.1:9000  [STORAGE storage.ReadFileStream] [2025-09-14T15:46:10.530] /export3/data mybucket randtext.txt/13a96fc7-0dc5-48c2-b949-24ec24322d31/part.4 total-errs-availability=0 total-errs-timeout=0 54.316µs 5.3 MiB
127.0.0.1:9000  [STORAGE storage.ReadFileStream] [2025-09-14T15:46:10.530] /export1/data mybucket randtext.txt/13a96fc7-0dc5-48c2-b949-24ec24322d31/part.4 total-errs-timeout=0 total-errs-availability=0 63.241µs 5.3 MiB
...
127.0.0.1:30002 [REQUEST s3.GetObject] [2025-09-14T15:46:10.435] [Client IP: 10.42.0.1]
127.0.0.1:30002 GET /mybucket/randtext.txt
127.0.0.1:30002 Proto: HTTP/1.1
127.0.0.1:30002 Host: 127.0.0.1:30002
127.0.0.1:30002 Accept-Encoding: identity
127.0.0.1:30002 Authorization: AWS4-HMAC-SHA256 Credential=minio/20250914/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ae36a34bc4a7db66f08970b3f295da8084d03236448a04d23a23b79c87026710
127.0.0.1:30002 Content-Length: 0
127.0.0.1:30002 User-Agent: MinIO (linux; amd64) minio-go/v7.0.90 mc/RELEASE.2025-08-13T08-35-41Z
127.0.0.1:30002 X-Amz-Content-Sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
127.0.0.1:30002 X-Amz-Date: 20250914T064610Z
127.0.0.1:30002 <BLOB>
127.0.0.1:30002 [RESPONSE] [2025-09-14T15:46:10.645] [ Duration 209.83ms TTFB 4.247996ms ↑ 93 B  ↓ 100 MiB ]
...

mc ls, cp, rm 실습

 

파드 재기동 없이 볼륨 확장

#
kubectl get pvc -n tenant1

k directpv list drives
┌───────┬─────────┬────────────────────────────┬────────┬────────┬─────────┬────────┐
│ NODE  │ NAME    │ MAKE                       │ SIZE   │ FREE   │ VOLUMES │ STATUS │
├───────┼─────────┼────────────────────────────┼────────┼────────┼─────────┼────────┤
│ k3s-s │ nvme1n1 │ Amazon Elastic Block Store │ 30 GiB │ 20 GiB │ 1       │ Ready  │
│ k3s-s │ nvme2n1 │ Amazon Elastic Block Store │ 30 GiB │ 20 GiB │ 1       │ Ready  │
│ k3s-s │ nvme3n1 │ Amazon Elastic Block Store │ 30 GiB │ 20 GiB │ 1       │ Ready  │
│ k3s-s │ nvme4n1 │ Amazon Elastic Block Store │ 30 GiB │ 20 GiB │ 1       │ Ready  │
└───────┴─────────┴────────────────────────────┴────────┴────────┴─────────┴────────┘

k directpv info
┌─────────┬──────────┬───────────┬─────────┬────────┐
│ NODE    │ CAPACITY │ ALLOCATED │ VOLUMES │ DRIVES │
├─────────┼──────────┼───────────┼─────────┼────────┤
│ • k3s-s │ 120 GiB  │ 40 GiB    │ 4       │ 4      │
└─────────┴──────────┴───────────┴─────────┴────────┘

#
kubectl patch pvc -n tenant1 data0-tenant1-pool-0-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl patch pvc -n tenant1 data1-tenant1-pool-0-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl patch pvc -n tenant1 data2-tenant1-pool-0-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl patch pvc -n tenant1 data3-tenant1-pool-0-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

#
kubectl get pvc -n tenant1

k directpv list drives
┌───────┬─────────┬────────────────────────────┬────────┬────────┬─────────┬────────┐
│ NODE  │ NAME    │ MAKE                       │ SIZE   │ FREE   │ VOLUMES │ STATUS │
├───────┼─────────┼────────────────────────────┼────────┼────────┼─────────┼────────┤
│ k3s-s │ nvme1n1 │ Amazon Elastic Block Store │ 30 GiB │ 10 GiB │ 1       │ Ready  │
│ k3s-s │ nvme2n1 │ Amazon Elastic Block Store │ 30 GiB │ 10 GiB │ 1       │ Ready  │
│ k3s-s │ nvme3n1 │ Amazon Elastic Block Store │ 30 GiB │ 10 GiB │ 1       │ Ready  │
│ k3s-s │ nvme4n1 │ Amazon Elastic Block Store │ 30 GiB │ 10 GiB │ 1       │ Ready  │
└───────┴─────────┴────────────────────────────┴────────┴────────┴─────────┴────────┘

k directpv info
┌─────────┬──────────┬───────────┬─────────┬────────┐
│ NODE    │ CAPACITY │ ALLOCATED │ VOLUMES │ DRIVES │
├─────────┼──────────┼───────────┼─────────┼────────┤
│ • k3s-s │ 120 GiB  │ 80 GiB    │ 4       │ 4      │
└─────────┴──────────┴───────────┴─────────┴────────┘

# 아래 파드 내에서 볼륨 Size 가 20G 로 조금 시간 지나면 자동 확장 반영 된다.
k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- sh -c 'df -hT --type xfs'
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/nvme1n1   xfs    10G   34M   10G   1% /export0
/dev/nvme2n1   xfs    10G   34M   10G   1% /export1
/dev/nvme3n1   xfs    10G   34M   10G   1% /export2
/dev/nvme4n1   xfs    10G   34M   10G   1% /export3

k exec -it -n tenant1 tenant1-pool-0-0 -c minio -- sh -c 'df -hT --type xfs'
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/nvme1n1   xfs    20G   34M   20G   1% /export0
/dev/nvme2n1   xfs    20G   34M   20G   1% /export1
/dev/nvme3n1   xfs    20G   34M   20G   1% /export2
/dev/nvme4n1   xfs    20G   34M   20G   1% /export3

 

 

Perfomance & Warp

- 출시 시 저장할 테비바이트 단위의 예상 데이터 양
- 향후 최소 2년간 데이터 크기의 예상 성장률
- 평균 객체 크기별 객체 수
- 데이터의 평균 보존 시간(년)
- 배포할 사이트 수
- 예상 버킷 수

 

 

  • 네트워킹은 MinIO 성능에 가장 큰 영향을 미치는데, 호스트당 낮은 대역폭은 스토리지의 잠재적 성능을 인위적으로 제한하기 때문입니다.
  • 다음 네트워크 처리량 제약 조건 예시에서는 ~100MB/S의 지속 I/O를 제공하는 회전 디스크를 가정합니다.
    • 1GbE 네트워크 링크는 최대 125MB/s 또는 회전 디스크 1개를 지원할 수 있습니다.
    • 10GbE 네트워크는 약 1.25GB/s를 지원하여 잠재적으로 10~12개의 회전 디스크를 지원할 수 있습니다.
    • 25GbE 네트워크는 약 3.125GB/s를 지원하여 잠재적으로 약 30개의 회전 디스크를 지원할 수 있습니다.

 

1. 네트워크 (100GbE) 병목

네트워크 카드가 데이터를 주고받는 속도는 처음부터 정해진 최대치가 있습니다.
100GbE(기가비트 이더넷) 카드 한 개는 초당 약 11.4GB의 데이터를 처리할 수 있고, 두 개 포트가 있으면 이론상 22.8GB/s까지 올라갑니다.
하지만 실제로는 네트워크 프로토콜(데이터를 안전하게 보내는 여러 규칙)에도 시간이 들고, 장비에 따라 성능 차이가 있어 명목 속도보다 느릴 수 있습니다.

 

2. 네트워크 카드와 메인보드 연결(PCIe 버스) 병목

네트워크 카드는 CPU와 직접 연결되지 않고, 컴퓨터 내부의 고속 통로(PCIe 슬롯)를 이용합니다.
100GbE 듀얼 포트 카드를 이 고속 통로(PCIe 3.0 x16)에 연결하면, 이 통로가 처리할 수 있는 최대 속도(14.5GB/s)보다 카드에서 쏟아내는 데이터가 더 많아서, 실제 전체 속도가 늦어질 수 있습니다.
즉, 데이터가 교차로에서 밀려 차가 막히듯, 내부 회선에서 좁아지는 구간이 생기는 것입니다.

 

3. 프로세서(코어/클럭) 병목

NVMe SSD는 CPU와 바로 연결되어 빠릅니다.
하지만 데이터를 처리하는 건 결국 CPU(프로세서)입니다.
각 디스크나 네트워크에서 들어오는 작업을 처리할 '두뇌(코어/클럭)'가 부족하면, 아무리 빠른 SSD라도 실제 속도가 떨어집니다.
실험상, 디스크 1개당 CPU코어 2개(2.5GHz 이상)가 있어야 SSD의 최대 속도를 쓸 수 있다는 결과가 있습니다.

 

4. 프로세서 간(소켓 간) 연결 병목 (NUMA, UPI 등)

최신 서버는 두 개 이상의 CPU(소켓)를 쓰는 경우가 많습니다.
서로 다른 CPU에 연결된 메모리·디스크·네트워크 장치가 있을 때, 이들 사이를 잇는 연결선(UPI 등)을 통해 데이터가 오가야 합니다.
이 연결 과정에서 지연이 생길 수 있는데, 장치를 같은 CPU 옆에 놓고 쓰면 빠르지만, 반대쪽 CPU와 연동하는 경우 느려집니다.

 

5. PCIe 스위치 병목

NVMe SSD가 많이 장착된 서버는 PCIe 확장용 스위치(중간 연결장치)를 씁니다.
스위치 한 개가 한꺼번에 관리할 수 있는 PCIe 대역폭엔 한계가 있으므로, SSD 12개가 한 스위치로 묶이면, 모든 SSD가 풀 성능을 낼 수는 없습니다.
즉, 모두가 동시에 빠른 속도로 데이터 전송을 시도하면, 중간 허브가 '나눠쓰기'가 되어 성능이 줄어듭니다.

 

6. NVMe 드라이브와 PCIe 연결 병목

각 NVMe SSD는 PC 내부 통로(PCIe) 4개 레인으로 연결되어 있고, 이론적으로 양방향 3.6GB/s를 지원합니다.
대부분의 일반적인 작업 환경에서는 이 정도면 충분하며, SSD 한 개만 독립적으로 사용할 때는 병목이 거의 없습니다.

 

7. NVMe 드라이브 자체 병목

SSD 각각은 여러 메모리칩(Cell)이 있어, 동시에 읽고 쓰기를 할 수 있습니다.
하지만, 각 SSD가 가진 설계나 내부 구조(분할 방식)에 따라 다 같이 완벽하게 빨라지진 않습니다.
동시에 여러 작업이 몰리거나, 저장 공간이 거의 찼을 때 성능 저하가 나타날 수 있습니다.
여러 SSD가 한 PCIe 스위치를 공유하면, 위에서 언급한 스위치 병목도 함께 작동합니다.

 

 

PCIe 정보 확인

# 각 PCI 디바이스의 Link Capabilities / Link Status 섹션에 현재 Lane과 속도가 표시
lspci -vv | grep -A40 -i "nvme"
예시)
LnkCap: Port #8, Speed 16GT/s, Width x16 # LnkCap = 최대 지원 속도/레인
LnkSta: Speed 8GT/s, Width x8            # LnkSta = 현재 동작 속도/레인

00:1f.0 Non-Volatile memory controller: Amazon.com, Inc. NVMe EBS Controller (prog-if 02 [NVM Express])
	Subsystem: Amazon.com, Inc. NVMe EBS Controller
	Physical Slot: 31
	Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV+ VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
	Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
	Latency: 0
	Interrupt: pin A routed to IRQ 11
	Region 0: Memory at c0400000 (32-bit, non-prefetchable) [size=16K]
	Capabilities: [70] Express (v2) Endpoint, MSI 00
		DevCap:	MaxPayload 256 bytes, PhantFunc 0, Latency L0s unlimited, L1 unlimited
			ExtTag- AttnBtn- AttnInd- PwrInd- RBE+ FLReset- SlotPowerLimit 0W
		DevCtl:	CorrErr- NonFatalErr- FatalErr- UnsupReq-
			RlxdOrd- ExtTag- PhantFunc- AuxPwr- NoSnoop-
			MaxPayload 128 bytes, MaxReadReq 128 bytes
		DevSta:	CorrErr- NonFatalErr- FatalErr- UnsupReq- AuxPwr- TransPend-
		LnkCap:	Port #0, Speed unknown, Width x0, ASPM not supported
			ClockPM- Surprise- LLActRep- BwNot- ASPMOptComp-
		LnkCtl:	ASPM Disabled; RCB 64 bytes, Disabled- CommClk-
			ExtSynch- ClockPM- AutWidDis- BWInt- AutBWInt-
		LnkSta:	Speed unknown, Width x0
			TrErr- Train- SlotClk- DLActive- BWMgmt- ABWMgmt-
		DevCap2: Completion Timeout: Not Supported, TimeoutDis- NROPrPrP- LTR-
			 10BitTagComp- 10BitTagReq- OBFF Not Supported, ExtFmt- EETLPPrefix-
			 EmergencyPowerReduction Not Supported, EmergencyPowerReductionInit-
			 FRS- TPHComp- ExtTPHComp-
			 AtomicOpsCap: 32bit- 64bit- 128bitCAS-
		DevCtl2: Completion Timeout: 50us to 50ms, TimeoutDis- LTR- 10BitTagReq- OBFF Disabled,
			 AtomicOpsCtl: ReqEn-
		LnkSta2: Current De-emphasis Level: -6dB, EqualizationComplete- EqualizationPhase1-
			 EqualizationPhase2- EqualizationPhase3- LinkEqualizationRequest-
			 Retimer- 2Retimers- CrosslinkRes: unsupported
	Capabilities: [b0] MSI-X: Enable+ Count=3 Masked-
		Vector table: BAR=0 offset=00002000
		PBA: BAR=0 offset=000
		
# /sys/bus/pci에서 직접 확인 : sysfs 경로에 current_link_width와 current_link_speed 파일이 있음
cat /sys/bus/pci/devices/0000\:00\:1f.0/current_link_width
0

cat /sys/bus/pci/devices/0000\:00\:1f.0/current_link_speed
Unknown

#
apt install hwinfo -y
hwinfo --pci
예시)
x8
8.0 GT/s PCIe Gen3

15: PCI 1d.0: 0108 Non-Volatile memory controller (NVM Express)
  [Created at pci.386]
  Unique ID: 1GTX.vA1igerW3PA
  SysFS ID: /devices/pci0000:00/0000:00:1d.0
  SysFS BusID: 0000:00:1d.0
  Hardware Class: storage
  Model: "Amazon.com NVMe EBS Controller"
  Vendor: pci 0x1d0f "Amazon.com, Inc."
  Device: pci 0x8061 "NVMe EBS Controller"
  SubVendor: pci 0x1d0f "Amazon.com, Inc."
  SubDevice: pci 0x8061
  Driver: "nvme"
  Driver Modules: "nvme"
  Memory Range: 0xc0408000-0xc040bfff (rw,non-prefetchable)
  IRQ: 10 (no events)
  Module Alias: "pci:v00001D0Fd00008061sv00001D0Fsd00008061bc01sc08i02"
  Config Status: cfg=new, avail=yes, need=no, active=unknown

 

 

WARP란

MinIO에서 제공하는 대표적인 S3 벤치마크 도구

# Linux x86_64
wget https://github.com/minio/warp/releases/download/v1.3.0/warp_Linux_x86_64.tar.gz
tar zxvf warp_Linux_x86_64.tar.gz
chmod +x warp
mv warp /usr/local/bin
warp --version

#
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d > tenant1.crt
kubectl get secret -n tenant1 tenant1-tls -o jsonpath='{.data.public\.crt}' | base64 -d | openssl x509 -noout -text
...
            X509v3 Subject Alternative Name:
                DNS:tenant1-pool-0-0.tenant1-hl.tenant1.svc.cluster.local, DNS:minio.tenant1.svc.cluster.local, DNS:minio.tenant1, DNS:minio.tenant1.svc, DNS:*., DNS:*.tenant1.svc.cluster.local
#
cp tenant1.crt /usr/local/share/ca-certificates/tenant1.crt
update-ca-certificates

# 
echo "127.0.0.1 minio.tenant1.svc" >> /etc/hosts

# 
mc alias set k8s-tenant1 https://minio.tenant1.svc:30002 minio minio123
mc ls --summarize --recursive k8s-tenant1
 
#
export WARP_ENDPOINT="minio.tenant1.svc:30002"
export WARP_ACCESS_KEY="minio"
export WARP_SECRET_KEY="minio123"

# 신규 터미널1 : 모니터링 disk
iostat nvme1n1 nvme2n1 nvme3n1 nvme4n1 1
iostat nvme1n1 nvme2n1 nvme3n1 nvme4n1 1 -d
iostat nvme1n1 nvme2n1 nvme3n1 nvme4n1 1 -x
iostat nvme1n1 nvme2n1 nvme3n1 nvme4n1 1 -x -d

r/s	초당 읽기 I/O 요청 수 (Read IOPS)	초당 몇 번의 읽기 요청이 발생했는지
rkB/s	읽기 KB/초	초당 읽은 데이터 양
rrqm/s	읽기 merge 요청 수	디바이스에서 merge 된 read 요청 수
%rrqm	읽기 merge 비율	merge된 read 요청의 비율
r_await	읽기 평균 대기 시간(ms)	읽기 요청이 큐에서 대기한 시간 평균
rareq-sz	평균 읽기 요청 크기(kB)	I/O 요청당 데이터 크기 평균

w/s	초당 쓰기 I/O 요청 수 (Write IOPS)	초당 쓰기 요청 건수
wkB/s	쓰기 KB/초	초당 기록한 데이터 양
wrqm/s	쓰기 merge 요청 수	디바이스에서 merge 된 write 요청 수
%wrqm	쓰기 merge 비율	merge된 write 요청 비율
w_await	쓰기 평균 대기 시간(ms)	write 요청이 큐에서 대기한 평균 시간
wareq-sz	평균 쓰기 요청 크기(kB)	I/O 요청당 write 크기 평균
...
f/s	초당 플러시 요청 수	fsync 같은 flush 호출 수
f_await	플러시 평균 대기 시간(ms)	flush 요청이 큐에서 대기한 시간
aqu-sz	평균 I/O 큐 깊이	디바이스 큐에 평균 몇 건의 요청이 쌓였는지
%util	디바이스 사용률	디바이스가 바쁜 비율(100%면 완전히 포화)

# 신규 터미널2 : 모니터링 cpu
htop

# 신규 터미널3 : 모니터링 network -> 실행 후 General interface statistics 선택
apt install iptraf-ng -y
iptraf-ng

# 신규 터미널4 : 부하 실행
# 기본 부하 테스트 실행 : mybucket 버킷에 임시 객체들을 업로드하며 성능 측정 :32개의 동시 클라이언트로 1GB 객체 100개 업로드.
# 객체 업로드 실시간 확인 해보자. 해당 버킷에 기본의 객체는 실행 종료 시 삭제됨.
warp put --host $WARP_ENDPOINT \
  --access-key $WARP_ACCESS_KEY \
  --secret-key $WARP_SECRET_KEY \
  --tls \
  --obj.size 1MiB \
  --duration 2m \
  --concurrent 32 \
  --bucket mybucket

Reqs: 21625, Errs:0, Objs:21625, Bytes: 21.12GiB
 -       PUT Average: 180 Obj/s, 180.2MiB/s; Current 187 Obj/s, 187.3MiB/s, 142.8 ms/req
Reqs: 보낸 총 요청 수 (21625건)
Errs: 에러 발생 건수 (0 → 오류 없음)
Objs: 업로드된 객체 수(=요청 수와 같음)
Bytes: 총 업로드된 데이터 용량(21.12GiB)
PUT Average: 평균 업로드 속도
  180 Obj/s → 초당 평균 180개의 객체 업로드
  180.2MiB/s → 초당 평균 180MiB 데이터 전송
Current: 지금 이 시점의 속도(실시간)
  187 Obj/s / 187.3MiB/s
142.8 ms/req: 현재 요청당 평균 지연시간(ms)


Report: PUT. Concurrency: 32. Ran: 1m57s
 * Average: 180.23 MiB/s, 180.23 obj/s
 * Reqs: Avg: 182.5ms, 50%: 154.9ms, 90%: 311.0ms, 99%: 423.4ms, Fastest: 37.0ms, Slowest: 598.0ms, StdDev: 85.6ms
PUT: PUT(업로드) 테스트
Concurrency: 32: 동시에 32개의 병렬 스레드(커넥션)로 업로드
Ran: 1m57s: 전체 테스트 1분57초 동안 실행
Average: 전체 테스트 동안 평균 전송속도
  180.23 MiB/s / 180.23 obj/s
Reqs (latency): 요청 지연 시간 통계
  Avg: 182.5ms → 요청 평균 처리시간
	50%: 154.9ms → 중간값(절반의 요청이 155ms 이내)
	90%: 311.0ms → 90%가 311ms 이내
	99%: 423.4ms → 99%가 423ms 이내
	Fastest: 37.0ms / Slowest: 598.0ms
	StdDev: 85.6ms → 응답 시간 분산 정도

Throughput, split into 117 x 1s:
 * Fastest: 296.6MiB/s, 296.65 obj/s
 * 50% Median: 176.8MiB/s, 176.78 obj/s
 * Slowest: 100.2MiB/s, 100.17 obj/s
테스트 전체를 1초 단위로 쪼개서, 각 초마다의 처리량을 계산
Fastest: 가장 높은 처리량을 보인 구간(296MiB/s)
50% Median: 중앙값 구간(176MiB/s)
Slowest: 가장 낮았던 구간(100MiB/s)



# GET 테스트 + 병렬 : 동시에 32개의 워커(thread)로 GET 요청 , 병렬 처리로 최대 throughput 측정 가능
# --autoterm 시간절약을 위해 변화가 어느정도 안정되면 자동으로 종료.
warp get --host $WARP_ENDPOINT \
  --access-key $WARP_ACCESS_KEY \
  --secret-key $WARP_SECRET_KEY \
  --tls \
  --obj.size 1MiB \
  --duration 2m \
  --concurrent 32 \
  --bucket mybucket # --autoterm

# 평균 다운로드 속도: 약 498 MiB/s, 초당 객체 수 약 498개
Reqs: 8402, Errs:0, Objs:8402, Bytes: 8402.0MiB
 -       GET Average: 499 Obj/s, 499.2MiB/s; Current 515 Obj/s, 515.2MiB/s, 65.8 ms/req, TTFB: 41.5ms
Reqs: 8402	총 GET 요청 수
Errs: 0	오류 발생 건수 (0 → 모든 요청 성공)
Objs: 8402	다운로드된 객체 수
Bytes: 8402.0MiB	총 다운로드 데이터 용량
GET Average	평균 전송 속도
 499 Obj/s	초당 객체 수
 499.2 MiB/s	초당 데이터 전송량
Current	최근 측정 시점의 실시간 속도
 515 Obj/s, 515.2MiB/s	현재 초당 객체/데이터 속도
 65.8 ms/req	요청당 평균 지연 시간
TTFB: 41.5ms	Time To First Byte, 서버에서 첫 바이트를 받기까지 평균 시간

Throughput 540202802.0MiB/s within 7.500000% for 7s. Assuming stability. Terminating benchmark.

Report: GET. Concurrency: 32. Ran: 15s
 * Average: 498.48 MiB/s, 498.48 obj/s
 * Reqs: Avg: 64.8ms, 50%: 61.2ms, 90%: 101.5ms, 99%: 146.8ms, Fastest: 4.5ms, Slowest: 262.5ms, StdDev: 28.2ms
 * TTFB: Avg: 40ms, Best: 1ms, 25th: 27ms, Median: 38ms, 75th: 50ms, 90th: 64ms, 99th: 105ms, Worst: 223ms StdDev: 20ms
Latency 통계 (요청 지연 시간)
 Avg: 64.8ms → 전체 요청 평균 지연
 50%: 61.2ms → 절반의 요청은 61.2ms 이내 처리
 90%: 101.5ms, 99%: 146.8ms → 상위 퍼센타일 지연 시간
 Fastest: 4.5ms, Slowest: 262.5ms → 가장 빠르고 느린 요청
 StdDev: 28.2ms → 지연 시간 분산

TTFB(Time To First Byte)
 평균 40ms → 서버가 첫 바이트를 응답하기까지 걸린 시간
 Best: 1ms, Worst: 223ms
 퍼센타일 통계도 제공 (25th/50th/75th/90th/99th)
 TTFB가 낮으면 서버 응답이 빠르고, 네트워크가 병목이 아님을 의미

Throughput, split into 15 x 1s:
 * Fastest: 568.4MiB/s, 568.37 obj/s
 * 50% Median: 497.1MiB/s, 497.08 obj/s
 * Slowest: 428.3MiB/s, 428.27 obj/s
테스트 전체를 1초 단위로 나누어 초당 처리량 계산
 Fastest / Median / Slowest로 속도 변동 확인 가능
 예시: 한 구간에서는 568MiB/s, 다른 구간은 428MiB/s → 변동폭 존재

 

 

 

 

 

 

 

에러

2025-09-11T10:44:29 [TARGET_APPLY ]E: RetCode: SQL_ERROR SqlState: HY000 NativeError: 3144 Message: [MySQL][ODBC 8.0(w) Driver][mysqld-8.0.42]Cannot create a JSON value from a string with CHARACTER SET 'binary'. [1022502] (ar_odbc_stmt.c:2947)

 

원인

 Full Load시에는 실제 데이터베이스에 있는 데이터를 직접 가져오기 때문에 문제가 안되지만, Full Load 이후 CDC과정에서 위 에러가 발생하게 되는데, 이는 소스 디비의 컬럼 데이터 타입이 JSON일 때 발생하는 문제다.

CDC시에는 소스 디비의 binlog를 참고해서 가져오는데, 이때 소스 디비의 컬럼들중 데이터 타입이 JSON인 컬럼을 바이너리 형태로 읽어서 타겟 디비에 데이터를 입력할 때 문제가 발생한다.

 

해결법

 

위와 같이 문제 발생하는 테스크(소스 디비에 컬럼 데이터 타입이 JSON이 들어가 있는 테스크)를 변환 규칙으로 기존 data type이 json인걸 찾아서 이를 clob으로 데이터 유형을 변경해주면 해결된다.

+ Recent posts