Author Archives: pkhamdee

Unknown's avatar

About pkhamdee

Craft IT strategies to drive IT transformation and engineering of cloud-native architecture in cloud computing to build software that people love and rely on.

COSI Driver for Nutanix

COSI (Container Object Storage Interface) เป็น driver ที่ช่วย deploy และ config Object Storage ทำหน้าที่เหมือนกับ CSI (Container Storage Interface) เพื่อสร้าง volume ทั้งที่เป็น block และ file storage ให้กับ container หน้าที่ของ COSI จะสร้าง Bucket และ Access/Secret key ให้อัตโนมัติ ทำให้การใช้งาน object storage ทำได้อัตโนมัติผ่าน deployment metadata (yaml) ลดงาน operation ในการจัดการ object storage ให้กับ container application

COSI vs CSI Comparison

ConceptCSI (Block/File)COSI(Object)
Storage ClassStorageClassBucketClass
ClaimPersistentVolumeClaimBucketClaim
AccessBuilt into PVCBucketAccess + BucketAccessClass
MountVolume mount in podCredentials secret in pod
ProtocalBlock/NFS/etcS3, GCS, Azure Blob
StatusStable (v1)Alpha (v1alpha1)

Provider ที่ support COSI driver เช่น

ProviderDriver
MinIOminio.objectstorage.k8s.io
AWS S3s3.amazonaws.com
Ceph/RadosGWceph.objectstorage.k8s.io
Nutanix Objectsnutanix.objectstorage.k8s.io

เปิดใช้งาน COSI ใน NKP สามารถทำได้ผ่าน Application Catalog โดยเข้าไปที่ COSI Driver for Nutanix

เข้าไปที่ enable option และกรอกข้อมูลที่จำเป็นตามตัวอย่าง

หลังจากกดปุ่ม Save ระบบจะติดตั้ง COSI CRD และ COSI Controller ไปยัง Kubernetes Cluster ที่เลือก โดยสามารถตรวจสอบได้จากตัวอย่าง

[root@guest-bastion-server-1 nutanix]# kubectl get crds | grep objectstorage
bucketaccessclasses.objectstorage.k8s.io 2026-02-13T14:37:53Z
bucketaccesses.objectstorage.k8s.io 2026-02-13T14:37:53Z
bucketclaims.objectstorage.k8s.io 2026-02-13T14:37:53Z
bucketclasses.objectstorage.k8s.io 2026-02-13T14:37:53Z
buckets.objectstorage.k8s.io 2026-02-13T14:37:53Z
[root@guest-bastion-server-1 nutanix]# kubectl get pod -n container-object-storage-system
NAME READY STATUS RESTARTS AGE
container-object-storage-controller-667886cd7f-l658v 1/1 Running 0 12h

NKP จะติดตั้ง BucketClass ให้อัตโนมัติ โดยตรวจสอบได้จาก cli

[root@guest-bastion-server-1 nutanix]# kubectl get bucketclasses
NAME AGE
cosi-nutanix-nkp 47m
[root@guest-bastion-server-1 nutanix]# kubectl get bucketclasses cosi-nutanix-nkp -o yaml
apiVersion: objectstorage.k8s.io/v1alpha1
deletionPolicy: Delete
driverName: ntnx.objectstorage.k8s.io
kind: BucketClass
metadata:
annotations:
meta.helm.sh/release-name: cosi-resources-nutanix
meta.helm.sh/release-namespace: develop
creationTimestamp: "2026-02-14T02:16:04Z"
generation: 1
labels:
app.kubernetes.io/managed-by: Helm
helm.toolkit.fluxcd.io/name: cosi-resources-nutanix
helm.toolkit.fluxcd.io/namespace: develop
name: cosi-nutanix-nkp
resourceVersion: "642878"
uid: 58023be3-9179-40c7-be4d-24219c4a7735

ตรวจสอบว่า NKP ได้สร้าง BucketAccessClass ให้

[root@guest-bastion-server-1 nutanix]# kubectl get BucketAccessClass
NAME AGE
cosi-nutanix-nkp 50m
[root@guest-bastion-server-1 nutanix]# kubectl get BucketAccessClass cosi-nutanix-nkp -o yaml
apiVersion: objectstorage.k8s.io/v1alpha1
authenticationType: KEY
driverName: ntnx.objectstorage.k8s.io
kind: BucketAccessClass
metadata:
annotations:
meta.helm.sh/release-name: cosi-resources-nutanix
meta.helm.sh/release-namespace: develop
creationTimestamp: "2026-02-14T02:16:04Z"
generation: 1
labels:
app.kubernetes.io/managed-by: Helm
helm.toolkit.fluxcd.io/name: cosi-resources-nutanix
helm.toolkit.fluxcd.io/namespace: develop
name: cosi-nutanix-nkp
resourceVersion: "642877"
uid: 4bd6a0e8-0a16-4039-8746-66e2c7c00a87

ทำการสร้าง BucketClaim (เหมือน PVC สำหรับ object storage)

YAML
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketClaim
metadata:
name: test-bucketclaim
spec:
bucketClassName: cosi-nutanix-nkp
protocols:
- s3

สร้าง BucketAccess เพื่อให้ COSI Controller สร้าง credential สำหรับ connect ไปยัง S3 Bucket อัตโนมติ

YAML
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketAccess
metadata:
name: nkp-test-bucketaccess
spec:
bucketAccessClassName: cosi-nutanix-nkp
bucketClaimName: test-bucketclaim
credentialsSecretName: nkp-test-objectstore-credentials

ตรวจสอบว่า cosi controller ได้ทำการสร้าง secret ตามที่กำหนดใน BucketAccess metadata

[root@guest-bastion-server-1 nutanix]# kubectl get secret nkp-test-objectstore-credentials
NAME TYPE DATA AGE
nkp-test-objectstore-credentials Opaque 1 47m
[root@guest-bastion-server-1 nutanix]# k get secret nkp-test-objectstore-credentials -o yaml
apiVersion: v1
data:
BucketInfo: eyJtZXRhZGF0YSI6eyJuYW1lIjoiYmMtNTM3OTBlOGQtZDMzMC00N2QzLWE2MjAtOWI4ODVmYTEwNTQ3IiwiY3JlYXRpb25UaW1lc3RhbXAiOm51bGx9LCJzcGVjIjp7ImJ1Y2tldE5hbWUiOiJjb3NpLW51dGFuaXgtbmtwYjE5Y2EwZTItNzRjNC00YmNmLWE3YjctMmUzN2U2MDY5YWM2IiwiYXV0aGVudGljYXRpb25UeXBlIjoiS0VZIiwic2VjcmV0UzMiOnsiZW5kcG9pbnQiOiJodHRwczovLzE5Mi4xNjguMTAuNTI6NDQzIiwicmVnaW9uIjoidXMtZWFzdC0xIiwiYWNjZXNzS2V5SUQiOiJMc2pDRG9ZeTREX09wRl9sVmo5UkNQRERnUS1SOG1NXyIsImFjY2Vzc1NlY3JldEtleSI6ImVpZW45bjdUM1pHZ2oxSGtydVZvVmNOM1VuUlAybnBjIn0sInNlY3JldEF6dXJlIjpudWxsLCJwcm90b2NvbHMiOlsiIl19fQ==
kind: Secret
metadata:
creationTimestamp: "2026-02-14T03:46:44Z"
finalizers:
- cosi.objectstorage.k8s.io/secret-protection
name: nkp-test-objectstore-credentials
namespace: default
resourceVersion: "727116"
uid: 1d6c999c-6605-4580-9db4-e18c7fe1a41c
type: Opaque
[root@guest-bastion-server-1 nutanix]# kubectl get secret nkp-test-objectstore-credentials -o jsonpath='{.data.BucketInfo}' | base64 -d | jq .
{
"metadata": {
"name": "bc-53790e8d-d330-47d3-a620-9b885fa10547",
"creationTimestamp": null
},
"spec": {
"bucketName": "cosi-nutanix-nkpb19ca0e2-74c4-4bcf-a7b7-2e37e6069ac6",
"authenticationType": "KEY",
"secretS3": {
"endpoint": "https://192.168.10.52:443",
"region": "us-east-1",
"accessKeyID": "LsjCDoYy4D_OpF_lVj9RCPDDgQ-R8mM_",
"accessSecretKey": "eien9n7T3ZGgj1HkruVoVcN3UnRP2npc"
},
"secretAzure": null,
"protocols": [
""

Deploy cosi-test container เพื่อทดสอบอ่านเขียนข้อมูลลงใน bucket storage ได้ถูกต้อง

YAML
apiVersion: v1
kind: Pod
metadata:
name: cosi-test
namespace: default
spec:
containers:
- name: test
image: amazon/aws-cli:latest
command: ["/bin/sh", "-c"]
args:
- |
echo "=== COSI Bucket Test ==="
# Install jq
yum install -y jq 2>/dev/null || apk add jq 2>/dev/null
# Parse BucketInfo JSON
BUCKET_INFO=$(cat /cosi/BucketInfo)
export AWS_ACCESS_KEY_ID=$(echo $BUCKET_INFO | jq -r '.spec.secretS3.accessKeyID')
export AWS_SECRET_ACCESS_KEY=$(echo $BUCKET_INFO | jq -r '.spec.secretS3.accessSecretKey')
BUCKET_NAME=$(echo $BUCKET_INFO | jq -r '.spec.bucketName')
ENDPOINT=$(echo $BUCKET_INFO | jq -r '.spec.secretS3.endpoint')
echo "Bucket: $BUCKET_NAME"
echo "Endpoint: $ENDPOINT"
# Write test
echo "Hello from COSI!" > /tmp/testfile.txt
aws s3 cp /tmp/testfile.txt s3://$BUCKET_NAME/testfile.txt \
--endpoint-url $ENDPOINT --no-verify-ssl
# List bucket
aws s3 ls s3://$BUCKET_NAME/ --endpoint-url $ENDPOINT --no-verify-ssl
# Read test
aws s3 cp s3://$BUCKET_NAME/testfile.txt /tmp/downloaded.txt \
--endpoint-url $ENDPOINT --no-verify-ssl
cat /tmp/downloaded.txt
echo "=== Test Complete ==="
sleep 3600
volumeMounts:
- name: cosi-credentials
mountPath: /cosi
readOnly: true
volumes:
- name: cosi-credentials
secret:
secretName: nkp-test-objectstore-credentials

ตรวจสอบการทำงาน จาก log ของ pod cosi-test

[root@guest-bastion-server-1 nutanix]# kubectl logs cosi-test
=== COSI Bucket Test ===
Amazon Linux 2023 repository 23 MB/s | 54 MB 00:02
Last metadata expiration check: 0:00:09 ago on Sat Feb 14 04:46:32 2026.
Package jq-1.7.1-51.amzn2023.x86_64 is already installed.
Dependencies resolved.
Nothing to do.
Complete!
Bucket: cosi-nutanix-nkpb19ca0e2-74c4-4bcf-a7b7-2e37e6069ac6
Endpoint: https://192.168.10.52:443

Gateway API in NKP

Gateway API เป็นมาตรฐานใหม่ที่จะมาทดแทน Ingress Controller แบบเดิมในการ Expose Kubernetes service ในรูปแบบของ http url เนื่องจากความสามารถของ Ingress Controller มีจำกัดเช่นทำได้แค่ map url path ไปยัง service ภายใน kubernetes ไม่รองรับความต้องการใหม่ๆ สำหรับการเข้าถึง modern application เช่น การจัดการ http header, query parameter routing, traffic splitting เป็นต้น รวมถึงรูปแบบ routing อื่นๆ เช่น GRPC, TLS, TCP และ UDP เป็นต้น

บทความนี้จะมาลองใช้ Gateway API ที่อยู่ใน NKP เพื่อเปรียบเทียบให้เห็นความสามารถที่มากขึ้น โดยจะใช้ wordpress app เป็นตัวอย่าง

script สำหรับ deploy wordpress

YAML
apiVersion: v1
kind: Secret
metadata:
name: mysql-pass
type: Opaque
stringData:
password: nutanix
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
ports:
- port: 3306
selector:
app: wordpress
tier: mysql
clusterIP: None
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
labels:
app: wordpress
spec:
storageClassName: nutanix-files
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-mysql
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: mysql
spec:
containers:
- image: mysql:latest
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
- name: MYSQL_DATABASE
value: wordpress
- name: MYSQL_USER
value: wordpress
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: Service
metadata:
name: wordpress
labels:
app: wordpress
spec:
ports:
- port: 80
selector:
app: wordpress
tier: frontend
type: ClusterIP
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-pv-claim
labels:
app: wordpress
spec:
storageClassName: nutanix-files
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
tier: frontend
strategy:
type: Recreate
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- image: wordpress:latest
name: wordpress
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-pass
key: password
- name: WORDPRESS_DB_USER
value: wordpress
- name: WORDPRESS_DB_NAME
value: wordpress
- name: WORDPRESS_DEBUG
value: "1"
ports:
- containerPort: 80
name: wordpress
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wp-pv-claim

หลังจากติดตั้งแล้ว สร้าง tls secret สำหรับ https service สำหรับให้บริการ wordpress

Shell
# 1. First, check if a TLS secret already exists
kubectl get secrets -n default | grep tls
# 2. If no TLS secret, create a self-signed one for testing
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=*.192.168.10.42.sslip.io"
kubectl create secret tls wordpress-tls \
--cert=tls.crt --key=tls.key -n default

สร้าง Gateway สำหรับ wordpress application

YAML
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: kommander-gateway
namespace: default
spec:
gatewayClassName: traefik
listeners:
- name: web
protocol: HTTP
port: 8000
hostname: "*.192.168.10.42.sslip.io"
allowedRoutes:
namespaces:
from: All
- name: websecure
protocol: HTTPS
port: 8443
hostname: "*.192.168.10.42.sslip.io"
allowedRoutes:
namespaces:
from: All
tls:
mode: Terminate
certificateRefs:
- name: wordpress-tls

สร้าง http routing service เพื่อให้ผู้ใช้เรียก http/https service มายัง wordpress ได้ โดยจะมีความสัมพันธ์กับ gateway ที่สร้างในข้างต้น ซึ่งกำหนดใน sectionName ตามตัวอย่าง

YAML
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: wordpress
namespace: default
spec:
parentRefs:
- name: kommander-gateway
namespace: default
sectionName: web
- name: kommander-gateway
namespace: default
sectionName: websecure
hostnames:
- wordpress.192.168.10.42.sslip.io
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: wordpress
port: 80

หลังจาก apply เข้าไปยัง kubernetes แล้ว wordpress ก็จะสามารถเรียกได้จาก url : http://wordpress.192.168.10.42.sslip.io

นอกจากจะทำ path routing แล้ว Gateway API ยังสามารถทำ routing ในรูปแบบอื่นๆ ได้ดังนี้

  1. Header-Based Routing
    rules:
    - matches:
    - headers:
    - name: X-Version
    value: v2
    backendRefs:
    - name: wordpress-v2
    port: 80

    2. Method-Based Routing

    rules:
    - matches:
    - method: POST
    path:
    type: PathPrefix
    value: /api
    backendRefs:
    - name: api-write
    port: 80

    3. Query Parameter Routing

    rules:
    - matches:
    - queryParams:
    - name: env
    value: debug
    backendRefs:
    - name: debug-service
    port: 80

    4. Traffic Splitting (Canary/Blue-Green)

    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /
    backendRefs:
    - name: wordpress-v1
    port: 80
    weight: 90
    - name: wordpress-v2
    port: 80
    weight: 10

    5. Request Header Modification

    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /
    filters:
    - type: RequestHeaderModifier
    requestHeaderModifier:
    add:
    - name: X-Custom-Header
    value: my-value
    set:
    - name: Host
    value: internal-service
    remove:
    - X-Unwanted-Header
    backendRefs:
    - name: wordpress
    port: 80

    6. Response Header Modification

    filters:
    - type: ResponseHeaderModifier
    responseHeaderModifier:
    add:
    - name: X-Frame-Options
    value: DENY
    set:
    - name: Cache-Control
    value: no-cache

    7. URL Redirect

    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /old-blog
    filters:
    - type: RequestRedirect
    requestRedirect:
    scheme: https
    hostname: new-blog.example.com
    port: 443
    statusCode: 301

    8. URL Rewrite

    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /api/v1
    filters:
    - type: URLRewrite
    urlRewrite:
    hostname: backend-api.internal
    path:
    type: ReplacePrefixMatch
    replacePrefixMatch: /v1
    backendRefs:
    - name: api-service
    port: 80

    9. Request Mirroring

    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /
    filters:
    - type: RequestMirror
    requestMirror:
    backendRef:
    name: wordpress-shadow
    port: 80
    backendRefs:
    - name: wordpress
    port: 80

    10. Multiple Matches Combined

    rules:
    - matches:
    - path:
    type: PathPrefix
    value: /api
    headers:
    - name: X-Version
    value: v2
    method: POST
    backendRefs:
    - name: api-v2
    port: 80

    นอกจากนี้ยังมี Router type อื่นๆ เช่น GRPCRoute, TLSRoute, TCPRoute และ UDPRoute ตามตัวอย่าง

    1. GRPCRoute
    YAML
    apiVersion: gateway.networking.k8s.io/v1
    kind: GRPCRoute
    metadata:
    name: grpc-route
    namespace: default
    spec:
    parentRefs:
    - name: my-gateway
    sectionName: grpc
    hostnames:
    - grpc.example.com
    rules:
    # Route by service name
    - matches:
    - method:
    service: myapp.UserService
    method: GetUser
    backendRefs:
    - name: user-service
    port: 50051
    # Route by header
    - matches:
    - headers:
    - name: x-api-version
    value: v2
    backendRefs:
    - name: api-v2
    port: 50051
    # Traffic splitting for gRPC
    - matches:
    - method:
    service: myapp.OrderService
    backendRefs:
    - name: order-service-v1
    port: 50051
    weight: 80
    - name: order-service-v2
    port: 50051
    weight: 20
    ---
    # Gateway listener for gRPC
    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
    name: my-gateway
    spec:
    gatewayClassName: traefik
    listeners:
    - name: grpc
    protocol: HTTPS
    port: 8443
    hostname: "grpc.example.com"
    tls:
    mode: Terminate
    certificateRefs:
    - name: grpc-tls-secret
    allowedRoutes:
    kinds:
    - kind: GRPCRoute
    namespaces:
    from: All

    2. TLSRoute (TLS Passthrough)

    YAML
    apiVersion: gateway.networking.k8s.io/v1alpha2
    kind: TLSRoute
    metadata:
    name: tls-passthrough
    namespace: default
    spec:
    parentRefs:
    - name: my-gateway
    sectionName: tls
    hostnames:
    - secure.example.com
    rules:
    - backendRefs:
    - name: secure-backend
    port: 8443
    ---
    # Gateway listener for TLS passthrough
    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
    name: my-gateway
    spec:
    gatewayClassName: traefik
    listeners:
    - name: tls
    protocol: TLS
    port: 8443
    hostname: "secure.example.com"
    tls:
    mode: Passthrough # TLS is NOT terminated, passed directly to backend
    allowedRoutes:
    kinds:
    - kind: TLSRoute
    namespaces:
    from: All

    3. TCPRoute

    YAML
    apiVersion: gateway.networking.k8s.io/v1alpha2
    kind: TCPRoute
    metadata:
    name: tcp-database
    namespace: default
    spec:
    parentRefs:
    - name: my-gateway
    sectionName: postgres
    rules:
    - backendRefs:
    - name: postgres-service
    port: 5432
    ---
    # Another TCPRoute for Redis
    apiVersion: gateway.networking.k8s.io/v1alpha2
    kind: TCPRoute
    metadata:
    name: tcp-redis
    namespace: default
    spec:
    parentRefs:
    - name: my-gateway
    sectionName: redis
    rules:
    - backendRefs:
    - name: redis-service
    port: 6379
    ---
    # Gateway with TCP listeners
    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
    name: my-gateway
    spec:
    gatewayClassName: traefik
    listeners:
    - name: postgres
    protocol: TCP
    port: 5432
    allowedRoutes:
    kinds:
    - kind: TCPRoute
    namespaces:
    from: All
    - name: redis
    protocol: TCP
    port: 6379
    allowedRoutes:
    kinds:
    - kind: TCPRoute
    namespaces:
    from: All

    4. UDPRoute

    YAML
    apiVersion: gateway.networking.k8s.io/v1alpha2
    kind: UDPRoute
    metadata:
    name: udp-dns
    namespace: default
    spec:
    parentRefs:
    - name: my-gateway
    sectionName: dns
    rules:
    - backendRefs:
    - name: coredns
    port: 53
    ---
    # Another UDPRoute for game server
    apiVersion: gateway.networking.k8s.io/v1alpha2
    kind: UDPRoute
    metadata:
    name: udp-game
    namespace: default
    spec:
    parentRefs:
    - name: my-gateway
    sectionName: game
    rules:
    - backendRefs:
    - name: game-server
    port: 27015
    ---
    # Gateway with UDP listeners
    apiVersion: gateway.networking.k8s.io/v1
    kind: Gateway
    metadata:
    name: my-gateway
    spec:
    gatewayClassName: traefik
    listeners:
    - name: dns
    protocol: UDP
    port: 53
    allowedRoutes:
    kinds:
    - kind: UDPRoute
    namespaces:
    from: All
    - name: game
    protocol: UDP
    port: 27015
    allowedRoutes:
    kinds:
    - kind: UDPRoute
    namespaces:
    from: All

    Nutanix File Storage Class

    วิธีการ config ให้ Nutanix CSI เชื่อมต่อไปยัง File Service (NUS) ทำให้สามารถสร้าง persistent volume บน File Share ได้ ขั้นตอนการสร้าง Storage Class ทำได้ตามตัวอย่างดังนี้

    สร้าง secret เพื่อเชื่อมต่อไปยัง Nutanix File server ผ่าน Prism Central

    YAML
    apiVersion: v1
    kind: Secret
    metadata:
    name: pc-files-secret
    namespace: ntnx-system
    stringData:
    # Provide Nutanix File Server credentials which is a REST API user created on File server UI separated by colon in "files-key:".
    key: "<<PC IP Address>>:9440:<<PC userid>>:<<password>>"
    files-key: "<<File Server IP>>:<<REST API userid>>:<<password>>"

    ตัวอย่าง secret ที่สร้างจากข้อมูลที่ได้จาก lab

    YAML
    apiVersion: v1
    kind: Secret
    metadata:
    name: nutanix-csi-credentials-files
    namespace: ntnx-system
    stringData:
    # Provide Nutanix File Server credentials which is a REST API user created on File server UI separated by colon in "files-key:".
    key: "192.168.10.25:9440:admin:P@ssw0rd123!"
    files-key: "nas.nutanix.poc:tao:nutanix/4u"

    สร้าง Storage Class ด้วยข้อมูลดังนี้

    YAML
    kind: StorageClass
    apiVersion: storage.k8s.io/v1
    metadata:
    name: nfs-pc-sc
    provisioner: csi.nutanix.com
    parameters:
    csi.storage.k8s.io/node-publish-secret-name: pc-files-secret
    csi.storage.k8s.io/node-publish-secret-namespace: ntnx-system
    csi.storage.k8s.io/controller-expand-secret-name: pc-files-secret
    csi.storage.k8s.io/controller-expand-secret-namespace: ntnx-system
    dynamicProv: ENABLED
    nfsServer: <<IP address of file server>>
    nfsServerName: <<file server name>>
    csi.storage.k8s.io/provisioner-secret-name: pc-files-secret
    csi.storage.k8s.io/provisioner-secret-namespace: ntnx-system
    storageType: NutanixFiles
    squashType: root-squash
    reclaimPolicy: Delete
    volumeBindingMode: Immediate
    allowVolumeExpansion: true

    ตัวอย่างการสร้าง file storage class ด้วยการใช้ข้อมูลจาก lab

    YAML
    kind: StorageClass
    apiVersion: storage.k8s.io/v1
    metadata:
    name: nutanix-files
    provisioner: csi.nutanix.com
    parameters:
    csi.storage.k8s.io/node-publish-secret-name: nutanix-csi-credentials-files
    csi.storage.k8s.io/node-publish-secret-namespace: ntnx-system
    csi.storage.k8s.io/controller-expand-secret-name: nutanix-csi-credentials-files
    csi.storage.k8s.io/controller-expand-secret-namespace: ntnx-system
    dynamicProv: ENABLED
    nfsServer: nas.nutanix.poc
    nfsServerName: nas
    csi.storage.k8s.io/provisioner-secret-name: nutanix-csi-credentials-files
    csi.storage.k8s.io/provisioner-secret-namespace: ntnx-system
    storageType: NutanixFiles
    reclaimPolicy: Delete
    volumeBindingMode: Immediate
    allowVolumeExpansion: true

    deploy workload เพื่อทดสอบสร้าง file volume ทำการเขียนและอ่านไฟล์จาก file volume

    YAML
    ---
    # 1. PVC
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: test-fileshare-pvc
    namespace: default
    spec:
    accessModes:
    - ReadWriteMany
    storageClassName: nutanix-files # Change this
    resources:
    requests:
    storage: 1Gi
    ---
    # 2. Writer Pod - writes a file
    apiVersion: v1
    kind: Pod
    metadata:
    name: test-writer
    namespace: default
    spec:
    containers:
    - name: writer
    image: busybox
    command: ["/bin/sh", "-c"]
    args:
    - |
    echo "Hello from writer pod at $(date)" > /data/testfile.txt
    echo "Write successful!"
    cat /data/testfile.txt
    sleep 3600
    volumeMounts:
    - name: shared-vol
    mountPath: /data
    volumes:
    - name: shared-vol
    persistentVolumeClaim:
    claimName: test-fileshare-pvc
    ---
    # 3. Reader Pod - reads the same file (proves RWX works)
    apiVersion: v1
    kind: Pod
    metadata:
    name: test-reader
    namespace: default
    spec:
    containers:
    - name: reader
    image: busybox
    command: ["/bin/sh", "-c"]
    args:
    - |
    echo "Waiting for file..."
    until [ -f /data/testfile.txt ]; do sleep 2; done
    echo "Read successful!"
    cat /data/testfile.txt
    sleep 3600
    volumeMounts:
    - name: shared-vol
    mountPath: /data
    volumes:
    - name: shared-vol
    persistentVolumeClaim:
    claimName: test-fileshare-pvc

    script สำหรับทดสอบ

    Shell
    # 1. Check available storage classes
    kubectl get storageclass
    # 2. Deploy (replace <your-storage-class> first)
    kubectl apply -f test-fileshare.yaml
    # 3. Check PVC is bound
    kubectl get pvc test-fileshare-pvc
    # 4. Check both pods are running
    kubectl get pods test-writer test-reader
    # 5. Verify writer wrote the file
    kubectl logs test-writer
    # 6. Verify reader can read the same file
    kubectl logs test-reader
    # 7. Extra test - write from reader pod too
    kubectl exec test-reader -- sh -c 'echo "Hello from reader" >> /data/testfile.txt'
    kubectl exec test-writer -- cat /data/testfile.txt

    ข้อควรระวัง

    CIS hardening จะทำการ disable/masks rpcbind ทำให้ Storage Class ไม่สามารถ mount volume NFSv3 บน worker node ได้ โดยมีทางเลือกดังนี้

    Option 1 แนะนำให้ใช้ NFSv4 เนื่องจาก NFSv4 ไม่ต้องการ rpcbind หรือ rpc-statd เนื่องจากสามารถทำกระบวนการ locking file ได้ด้วยตัวเอง รวมทั้งเป็นไปตามข้อกำหนดของ CIS hardening

    Shell
    # Test NFSv4 mount
    mount -t nfs -o vers=4 files.ntnxlab.local:/test /mnt/test

    สำหรับ Nutanix CSI, ทำการ force ให้ใช้ NFSv4 ได้ตามตัวอย่าง

    YAML
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
    name: nutanix-files-nfsv4
    provisioner: csi.nutanix.com
    parameters:
    nfsServerName: nas
    storageType: NutanixFiles
    mountOptions: "vers=4"
    mountOptions:
    - vers=4
    - nolock

    Option 2 ทำการ Unmask rpcbind ซึ่งจะมีผลต่อความปลอดภัย โดยจะต้องทำการ unmask ในทุกๆ kubernetes node

    Shell
    systemctl unmask rpcbind.socket
    systemctl unmask rpcbind.service
    systemctl enable rpcbind --now

    Option 3 ทำการ mount volume ด้วย option nolock ในกรณีที่ CSI support ด้วยวิธีนี้จะข้าม rpc-statd แต่ disable file locking

    YAML
    mountOptions:
    - nolock
    - vers=3

    note – install wordpress โดยใช้ file volume

    YAML
    apiVersion: v1
    kind: Secret
    metadata:
    name: mysql-pass
    type: Opaque
    stringData:
    password: nutanix
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: wordpress-mysql
    labels:
    app: wordpress
    spec:
    ports:
    - port: 3306
    selector:
    app: wordpress
    tier: mysql
    clusterIP: None
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: mysql-pv-claim
    labels:
    app: wordpress
    spec:
    storageClassName: nutanix-files
    accessModes:
    - ReadWriteOnce
    resources:
    requests:
    storage: 20Gi
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: wordpress-mysql
    labels:
    app: wordpress
    spec:
    selector:
    matchLabels:
    app: wordpress
    tier: mysql
    strategy:
    type: Recreate
    template:
    metadata:
    labels:
    app: wordpress
    tier: mysql
    spec:
    containers:
    - image: https://10.38.16.169/docker.io/mysql:latest
    name: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
    valueFrom:
    secretKeyRef:
    name: mysql-pass
    key: password
    - name: MYSQL_DATABASE
    value: wordpress
    - name: MYSQL_USER
    value: wordpress
    - name: MYSQL_PASSWORD
    valueFrom:
    secretKeyRef:
    name: mysql-pass
    key: password
    ports:
    - containerPort: 3306
    name: mysql
    volumeMounts:
    - name: mysql-persistent-storage
    mountPath: /var/lib/mysql
    volumes:
    - name: mysql-persistent-storage
    persistentVolumeClaim:
    claimName: mysql-pv-claim
    ---
    apiVersion: v1
    kind: Service
    metadata:
    name: wordpress
    labels:
    app: wordpress
    spec:
    ports:
    - port: 80
    selector:
    app: wordpress
    tier: frontend
    type: ClusterIP
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: wp-pv-claim
    labels:
    app: wordpress
    spec:
    storageClassName: nutanix-files
    accessModes:
    - ReadWriteOnce
    resources:
    requests:
    storage: 20Gi
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: wordpress
    labels:
    app: wordpress
    spec:
    selector:
    matchLabels:
    app: wordpress
    tier: frontend
    strategy:
    type: Recreate
    template:
    metadata:
    labels:
    app: wordpress
    tier: frontend
    spec:
    containers:
    - image: wordpress:latest
    name: wordpress
    env:
    - name: WORDPRESS_DB_HOST
    value: wordpress-mysql
    - name: WORDPRESS_DB_PASSWORD
    valueFrom:
    secretKeyRef:
    name: mysql-pass
    key: password
    - name: WORDPRESS_DB_USER
    value: wordpress
    - name: WORDPRESS_DB_NAME
    value: wordpress
    - name: WORDPRESS_DEBUG
    value: "1"
    ports:
    - containerPort: 80
    name: wordpress
    volumeMounts:
    - name: wordpress-persistent-storage
    mountPath: /var/www/html
    volumes:
    - name: wordpress-persistent-storage
    persistentVolumeClaim:
    claimName: wp-pv-claim
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    name: wordpress-ingress
    spec:
    ingressClassName: kommander-traefik
    rules:
    - host: wordpress.10.55.42.19.sslip.io
    http:
    paths:
    - path: /
    pathType: Prefix
    backend:
    service:
    name: wordpress
    port:
    number: 80

    Stress Test Kubernetes

    Script สำหรับ stress test kubernetes เพื่อ consume resource ทั้งหมดของ kubernetes

    YAML
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: resource-test
    spec:
    replicas: 10
    selector:
    matchLabels:
    app: resource-test
    template:
    metadata:
    labels:
    app: resource-test
    spec:
    containers:
    - name: stress-test
    image: 10.38.53.107/docker/nginx
    resources:
    requests:
    memory: "1Gi"
    cpu: "5000m"
    limits:
    memory: "2Gi"
    cpu: "10000m"

    Install ArgoCD

    ArgoCD เป็น Tool สำหรับทำ GitOps ช่วยติดตั้ง Application Deployment (metadata fie) ที่เป็น yaml file, kustomize หรือ helm package ที่เก็บอยู่ใน Git Repository ไปยัง Kubernetes ปลายทางได้ทั้งแบบ auto deploy และ manual deploy

    Script สำหรับ install ArgoCD

    Shell
    #!/bin/bash
    # 1. Create the namespace
    kubectl create namespace argocd
    # 2. Apply the official Argo CD manifests
    echo "Installing Argo CD..."
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    # 3. Change the server service type to LoadBalancer (optional, but helpful for UI access)
    echo "Patching service to LoadBalancer..."
    kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
    # 4. Wait for the rollout to complete
    echo "Waiting for Argo CD components to be ready..."
    kubectl wait --for=condition=available --timeout=600s deployment/argocd-server -n argocd
    # 5. Retrieve the initial admin password
    PASS=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)
    echo "------------------------------------------------------"
    echo "Argo CD Installation Complete!"
    echo "Admin Username: admin"
    echo "Admin Password: $PASS"
    echo "------------------------------------------------------"
    echo "Note: If you are on a local cluster without a LoadBalancer,"
    echo "run: kubectl port-forward svc/argocd-server -n argocd 8080:443"

    หลังจาก login เข้าหน้าหลักแล้วต้องสร้าง gitrepository สำหรับเชื่อต่อ project และ gitrepository ก่อน

    เชื่อต่อ kubernetes cluster ที่ต้องการ deploy application

    ทำการสร้าง application เพื่อให้ ArgoCD ทำการ deploy application ให้อัตโนมัติ

    ArgoCD จะ sync ข้อมูลจาก gitrepository แล้ว deploy application ใน kubernetes ตามที่ config ไว้

    External Secrets Operator

    External Secrets Operator เป็น Kubernetes Operator ที่ช่วย Integrate Kubernetes Secret กับ KMS ภายนอกโดย Operator จะทำการ sync credential จาก KMS ภายนอกและทำการ update secret ของ Kubernetes อัตโนมัติ ช่วยให้การจัดการ credential สามารถจัดการแก้ไขจากระบบ KMS ได้จากที่เดียว

    สำหรับ NKP สามารถ Enable ได้จากหน้า Applications Catalog

    หลังจาก enable แล้ว เราจะต้องทำให้ Vault trust Kubernetes จากนั้นสร้าง SecretStore เพื่อเป็นสะพานเชื่อมระหว่าง Kubernetes และ Vault หลังจากนั้นสร้าง ExternalSecret เพื่อ Sync Secret ให้กับ Kubernetes

    ขั้นตอนการทำให้ Vault trust Kubernetes

    Shell
    # Exec into your vault-0 pod
    kubectl exec -it vault-0 -n vault -- sh
    vault login hvs.SMpvYTqteXW6Vjup8HWIB972
    # 1. Enable Kubernetes auth
    vault auth enable kubernetes
    # 2. Configure it to talk to the local K8s API
    vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc:443"
    # 3. Create a policy for ESO to read secrets
    vault policy write eso-read-policy - <<EOF
    path "secret/data/*" {
    capabilities = ["read"]
    }
    EOF
    # 4. Create a role that binds the ESO ServiceAccount to the policy
    # Assuming ESO is in 'external-secrets' namespace with SA name 'external-secrets'
    vault write auth/kubernetes/role/eso-role \
    bound_service_account_names=external-secrets \
    bound_service_account_namespaces=external-secrets \
    policies=eso-read-policy \
    ttl=1h

    สร้าง SecretStore ด้วย yaml file ดังนี้

    YAML
    apiVersion: external-secrets.io/v1
    kind: ClusterSecretStore
    metadata:
    name: vault-backend
    spec:
    provider:
    vault:
    server: "http://vault.vault.svc.cluster.local:8200"
    path: "secret"
    version: "v2"
    auth:
    kubernetes:
    # This must match the role name you created in Vault Step 1.4
    role: "eso-role"
    mountPath: "kubernetes"
    serviceAccountRef:
    name: "external-secrets"
    namespace: "external-secrets"

    สร้าง External Secret store สำหรับ sync secret กับ Vault มายัง Kubernetes ด้วย yaml file ดังนี้

    YAML
    apiVersion: external-secrets.io/v1
    kind: ExternalSecret
    metadata:
    name: my-app-secret
    namespace: default
    spec:
    refreshInterval: "1h" # How often to sync
    secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
    target:
    name: k8s-app-secret # The name of the resulting K8s Secret
    creationPolicy: Owner
    data:
    - secretKey: DATABASE_PASSWORD # Key in the K8s Secret
    remoteRef:
    key: my-app/db-creds # Path in Vault
    property: password # Key inside the Vault JSON

    ตรวจสอบว่ามีการสร้าง secret k8s-app-secret ที่ namespace default จากคำสั่งข้างต้นจริง

    kubectl get secret
    NAME TYPE DATA AGE
    k8s-app-secret Opaque 1 31s

    ดูข้อมูลของ secret จะพบว่ามีการ sync DATABASE_PASSWORD จาก Vault มาให้

    kubectl get secret k8s-app-secret -o yaml
    apiVersion: v1
    data:
    DATABASE_PASSWORD: cGFzc3dvcmQxMjM=
    kind: Secret
    metadata:
    annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
    {"apiVersion":"external-secrets.io/v1","kind":"ExternalSecret","metadata":{"annotations":{},"name":"my-app-secret","namespace":"default"},"spec":{"data":[{"remoteRef":{"key":"my-app/db-creds","property":"password"},"secretKey":"DATABASE_PASSWORD"}],"refreshInterval":"1h","secretStoreRef":{"kind":"ClusterSecretStore","name":"vault-backend"},"target":{"creationPolicy":"Owner","name":"k8s-app-secret"}}}
    reconcile.external-secrets.io/data-hash: 69b12d606fda3589324b709f341cf77986d7128c2fb089486fe84d76
    creationTimestamp: "2026-02-05T11:21:32Z"
    labels:
    reconcile.external-secrets.io/created-by: ea4461eb1de01e986cc8315e7fc9b7bdbdf0204590c521bfc076aa6e
    reconcile.external-secrets.io/managed: "true"
    name: k8s-app-secret
    namespace: default
    ownerReferences:
    - apiVersion: external-secrets.io/v1
    blockOwnerDeletion: true
    controller: true
    kind: ExternalSecret
    name: my-app-secret
    uid: 084595bf-cef5-417f-9a98-2f77b82512ec
    resourceVersion: "949692"
    uid: c0a8c2aa-e0c7-44c3-86bc-dcfacc7190a0
    type: Opaque

    Install Hashicorp Vault to Kubernetes

    ติดตั้ง KMS สำหรับจัดการ credential ใน Kubernetes ด้วย Hashicorp vault

    สร้าง script สำหรับติดตั้ง install-vault.sh

    Shell
    #!/bin/bash
    # 1. Add the HashiCorp Helm Repository
    helm repo add hashicorp https://helm.releases.hashicorp.com
    helm repo update
    # 2. Create a dedicated namespace
    kubectl create namespace vault
    # 3. Deploy Vault in High Availability (HA) mode with Raft storage
    # We use a custom values file for production-like settings
    cat <<EOF > vault-values.yaml
    server:
    ha:
    enabled: true
    raft:
    enabled: true
    setNodeId: true
    config: |
    ui = true
    listener "tcp" {
    tls_disable = 1
    address = "[::]:8200"
    cluster_address = "[::]:8201"
    }
    storage "raft" {
    path = "/vault/data"
    }
    service_registration "kubernetes" {}
    EOF
    helm install vault hashicorp/vault \
    --namespace vault \
    -f vault-values.yaml

    ตรวจสอบว่า Vault ได้ install และมี status Running ดังรูป โดย status จะยังเป็น 0/1 Ready เพราะว่าต้องทำการ Initial และ Unseal ก่อน pod ถึงจะทำงาน

    nutanix@harbor ~]$ kubectl get pod -n vault
    NAME READY STATUS RESTARTS AGE
    vault-0 0/1 Running 0 19m
    vault-1 0/1 Running 0 19m
    vault-2 0/1 Running 0 19m
    vault-agent-injector-5b7dd85f5c-cht5p 1/1 Running 0 19m

    เข้าไปที่ vault-0. เพื่อทำการ initial ด้วยคำสั่ง

    kubectl exec -it vault-0 -n vault -- vault operator init

    คำสั่ง init จะทำการสร้าง Unseal Keys และ Initial Root Token ซึ่งจะต้องเก็บผลลัพธ์ไว้ เพราะต้องใช้ในการ unseal ในกรณี pod restart ถ้าข้อมูลนี้หายจะไม่สามารถ unseal และดึงข้อมูลที่เก็บใน vault ได้

    Unseal Key 1: n80kTk1a0QUsd5XJIVQ+SVF+cmjDj5H0AX8HkDd4xgZg
    Unseal Key 2: pu2+W0CZWOHcl6xyD77mzip1BeUZOcq9aRW8NwXx5m10
    Unseal Key 3: 9vGGqh02ZnOvc7T7P45EpPyFeX+KMEYGhv69B/qERaBL
    Unseal Key 4: 8gbpjijpBLViLDXnfJF7me32ts2pborEyiOH5wHLIHef
    Unseal Key 5: 0YaO4FKto4I+5ZyV2VGtQrUHJFpLS73bYsQeU153J39I
    Initial Root Token: hvs.SMpvYTqteXW6Vjup8HWIB972
    Vault initialized with 5 key shares and a key threshold of 3. Please securely
    distribute the key shares printed above. When the Vault is re-sealed,
    restarted, or stopped, you must supply at least 3 of these keys to unseal it
    before it can start servicing requests.
    Vault does not store the generated root key. Without at least 3 keys to
    reconstruct the root key, Vault will remain permanently sealed!
    It is possible to generate new unseal keys, provided you have a quorum of
    existing unseal keys shares. See "vault operator rekey" for more information.

    ทำตามขั้นตอนข้างต้นกับ vault-1 และ vault-2

    ขั้นตอนการ Unseal ใช้คำสั่งดังนี้

    # Repeat this 3 times with different keys for each pod
    kubectl exec -it vault-0 -n vault -- vault operator unseal <YOUR_UNSEAL_KEY>
    kubectl exec -it vault-1 -n vault -- vault operator unseal <YOUR_UNSEAL_KEY>
    kubectl exec -it vault-2 -n vault -- vault operator unseal <YOUR_UNSEAL_KEY>

    จำต้องทำคำสั่งข้างบน ด้วยการใช้ Unseal key โดยแต่ละ pod จะต้องทำ 3 ครั้งโดยใช้ Unseal Key ที่แตกต่างกัน และเมื่อทำครบแล้วจะได้ผลลัพธ์ดังตัวอย่าง

    [nutanix@harbor ~]$ kubectl exec -it vault-2 -n vault -- vault operator unseal
    Unseal Key (will be hidden):
    Key Value
    --- -----
    Seal Type shamir
    Initialized true
    Sealed false
    Total Shares 5
    Threshold 3
    Version 1.21.2
    Build Date 2026-01-06T08:33:05Z
    Storage Type raft
    Cluster Name vault-cluster-0f5e0570
    Cluster ID b4922833-5b45-50ee-3f29-b6639ae0ef16
    Removed From Cluster false
    HA Enabled true
    HA Cluster https://vault-2.vault-internal:8201
    HA Mode active
    Active Since 2026-02-05T10:58:54.132265118Z
    Raft Committed Index 37
    Raft Applied Index 37

    และเมื่อกลับมาดู status ของ pod อีกครั้งจะพบว่า pod ได้ทำงานครบตามจำนวน

    [nutanix@harbor ~]$ kubectl get pod -n vault
    NAME READY STATUS RESTARTS AGE
    vault-0 1/1 Running 0 23m
    vault-1 1/1 Running 0 23m
    vault-2 1/1 Running 0 23m
    vault-agent-injector-5b7dd85f5c-cht5p 1/1 Running 0 23m

    คำสั่งที่ใช้สำหรับ ตรวจสอบ status, login และ access ui

    kubectl exec -it vault-0 -n vault -- vault status
    kubectl exec -it vault-0 -n vault -- vault login <ROOT_TOKEN>
    kubectl port-forward service/vault 8200:8200 -n vault (Visit http://localhost:8200)

    ตรวจสอบ status ของ cluster

    # Login first
    kubectl exec -it vault-0 -n vault -- vault login <YOUR_ROOT_TOKEN>
    # Check Raft members
    kubectl exec -it vault-0 -n vault -- vault operator raft list-peers

    สิ่งที่ควรต้องพิจารณา

    1. Storage ที่ใช้ใน Script จะต้องมี StorageClass เพื่อ provision persistent volume
    2. การ Auto-Unseal สามารถใช้ Cloud KMS (AWS KMS, GCP KMS หรือ Azure Key Vault) ให้ช่วยขั้นตอนการ unseal ได้ ในกรณีที่ pod มีการ restart โดยไม่ต้องทำแบบ manual
    3. TLS ใน script install ทำการ disable ไว้ (tls_disable =1) เพื่อความง่ายในการติดตั้ง สำหรับ production ต้อง integrate กับ cert-manager เพื่อจัดการ life cycle ของ certificate

    ตัวอย่างการสร้าง secret

    ssh เข้าไปที่ vault instance

    kubectl exec -it vault-0 -n vault -- sh

    Login ด้วย root token

    # Ensure you are logged in first
    vault login hvs.SMpvYTqteXW6Vjup8HWIB972
    # Enable the KV-v2 engine at the path 'secret'
    vault secrets enable -path=secret kv-v2

    สร้าง secret

    # vault kv put <PATH> <KEY>=<VALUE>
    vault kv put secret/my-app/db-creds password="YourSuperSecurePassword123!"

    ตรวจสอบ secret ว่าสามารถเรียกดูได้ถูกต้อง

    vault kv get secret/my-app/db-creds
    ======= Secret Path =======
    secret/data/my-app/db-creds
    ======= Metadata =======
    Key Value
    --- -----
    created_time 2026-02-05T11:13:56.470489651Z
    custom_metadata <nil>
    deletion_time n/a
    destroyed false
    version 1
    ====== Data ======
    Key Value
    --- -----
    password password123

    Cloud-Init file to create jumphost

    ตัวอย่าง file cloud-init สำหรับสร้าง jumphost โดยใช้ rocky linux os

    file นี้จะทำการสร้าง user nutanix ติดตั้ง docker, kubectl และ code server เพื่อให้สามารถใช้ shell ผ่าน vscode

    Shell
    #cloud-config
    ssh_pwauth: true
    users:
    - name: nutanix
    passwd: nutanix/4u
    groups: users,wheel
    shell: /bin/bash
    lock-passwd: false
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    chpasswd:
    list: |
    nutanix:nutanix/4u
    root:nutanix/4u
    expire: False
    runcmd:
    - echo "Configuring SSH..."
    # Ensure PasswordAuthentication is enabled
    - sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config # Delete existing line
    - echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config # Add new line
    # Ensure PermitRootLogin is set to yes
    - sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config # Delete existing line
    - echo "PermitRootLogin yes" >> /etc/ssh/sshd_config # Add new line
    # Restart SSH Service
    - systemctl restart sshd
    - echo "SSH configuration updated and service restarted."
    - '[ ! -f "/etc/yum.repos.d/nutanix_rocky9.repo" ] || mv -f /etc/yum.repos.d/nutanix_rocky9.repo /etc/yum.repos.d/nutanix_rocky9.repo.disabled'
    - dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
    - dnf -y install git docker-ce docker-ce-cli containerd.io
    - systemctl --now enable docker
    - usermod -aG docker nutanix
    - 'curl -Lo /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl'
    - chmod +x /usr/local/bin/kubectl
    - 'curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash'
    - 'su - nutanix -c "curl -fsSL https://raw.githubusercontent.com/pkhamdee/nkp-tools/main/install-tools.sh | bash"'
    - eject

    สำหรับ https://raw.githubusercontent.com/pkhamdee/nkp-tools/main/install-tools.sh จะเป็นการ deploy code server เพื่อใช้ linux terminal และจัดการไฟล์ผ่านทาง vscode ได้

    Service Mesh with ISTIO

    เพื่อที่จะใช้งาน istio ใน Nutanix Kubernetes Platform จะต้องทำการ enable service ที่เกี่ยวข้องคือ prometheus monitoring ใน menu application

    จากนั้นทำการ enable istio ตามด้วย jaeger และ kiali

    สำหรับ kiali จะต้อง copy configuration เพิ่มเติมโดยเข้าไปที่ view detail

    และ copy ส่วนที่เป็น configuration ที่ menu overview ดังรูป

    ในขั้นตอน enable ให้ใส่ configuration ก่อนที่จะ enable ดังรูป

    หลังจาก enable เสร็จแล้วให้รอจนกว่า service จะ install เรียบร้อย ตรวจสอบได้จาก cli ตามตัวอย่าง

    [nutanix@harbor ~]$ kubectl get pod -A | grep istio
    istio-helm-gateway-ns istio-helm-ingressgateway-79df9868c8-5vkzb 1/1 Running 0 102m
    istio-helm-gateway-ns istio-helm-ingressgateway-79df9868c8-6shfm 1/1 Running 0 102m
    istio-helm-system istiod-istio-helm-545d9895f9-l56hv 1/1 Running 0 102m
    istio-helm-system istiod-istio-helm-545d9895f9-p9bzq 1/1 Running 0 102m
    istio-system jaeger-jaeger-operator-759ffd786-vf7wc 1/1 Running 0 104m
    istio-system jaeger-jaeger-operator-jaeger-6cb7cfb9f-l4xph 1/1 Running 0 104m
    istio-system kiali-kiali-operator-c6649cd8f-t7k98 1/1 Running 0 102m
    kommander-default-workspace istio-helm-pre-install-kt59f 0/1 Completed 0 105m
    kube-system istio-cni-node-cwff7 1/1 Running 0 103m
    kube-system istio-cni-node-fhnz9 1/1 Running 0 103m
    kube-system istio-cni-node-h8szk 1/1 Running 0 103m
    kube-system istio-cni-node-s2qf4 1/1 Running 0 103m

    เนื่องจาก istio และ jaeger อยู่ใน workspace ที่แตกต่างกัน จะต้องมีการ update configmap เพิ่มเติม โดยใช้ cli

    kubectl edit cm istio-istio-helm -n istio-helm-system

    เพิ่มส่วนของ tracing ตามตัวอย่าง

    YAML
    apiVersion: v1
    kind: ConfigMap
    data:
    mesh: |-
    enableTracing: true
    defaultConfig:
    discoveryAddress: istiod-istio-helm.istio-helm-system.svc:15012
    # Tracing belongs here for global configuration
    tracing:
    sampling: 100.0
    zipkin:
    address: jaeger-jaeger-operator-jaeger-collector.istio-system.svc.cluster.local:9411
    defaultProviders:
    metrics:
    - prometheus
    enablePrometheusMerge: true
    rootNamespace: istio-helm-system
    trustDomain: cluster.local
    # ... rest of metadata

    ทดสอบใช้งาน istio ในแบบ sidecare auto injection จะต้องทำการ label name space ด้วย istio.io/rev=istio-helm

    ทำการสร้าง project ใน nkp โดยกรอกข้อมูลและต้องกรอกข้อมูล namespace labels จากนั้นเลือก cluster ตามตัวอย่าง

    เมื่อสร้างเสร็จแล้ว เข้าไป Continuous Deployment (CD) และกดปุ่ม Add GitOps Source

    กรอกข้อมูลตามตัวอย่าง และกรอกข้อมูล Repository URL เป็น https://github.com/pkhamdee/sockshop จากนั้นกด Save

    จากนั้น nkp จะทำการ deploy application ลงใน namespace demo (ชื่อตรงกับ project) โดย FluxCD ซึ่งเป็น service ที่ nkp ใช้สำหรับการทำ GitOps จะ download yaml file ตาม url ที่กำหนดและ deploy application ให้อัตโนมัติ

    สามารถตรวจสอบสถานะการ deploy ได้จาก cli

    [nutanix@harbor ~]$ kubectl get pod -n demo
    NAME READY STATUS RESTARTS AGE
    carts-88b4ddf98-p5gqh 2/2 Running 0 5m39s
    carts-db-7d6d697d94-5cvxk 2/2 Running 0 5m39s
    catalogue-84b5874db7-rtxkf 2/2 Running 0 5m39s
    catalogue-db-64d88d46ff-nkrvb 2/2 Running 0 5m39s
    front-end-d87486986-8n6pc 2/2 Running 0 5m39s
    orders-8587749646-8m9wb 2/2 Running 0 5m39s
    orders-db-8458b5ddb4-xtv5l 2/2 Running 0 5m38s
    payment-6b49f65444-4hq97 2/2 Running 0 5m38s
    queue-master-686b7bf644-fw2qq 2/2 Running 0 5m38s
    rabbitmq-6d679fd595-sqhs5 3/3 Running 0 5m38s
    session-db-6cfcf8985d-9hph4 2/2 Running 0 5m38s
    shipping-5b674b9d94-42n48 2/2 Running 0 5m37s
    user-57c89fbbf4-nzt4d 2/2 Running 0 5m37s
    user-db-5c748bc594-mphsx 2/2 Running 0 5m37s

    ตรวจสอบว่า envoy sidecar ทำงานปกติ ด้วย kubectl cli โดยจะต้องมี log แสดงว่า “Envoy proxy is ready”

    [nutanix@harbor ~]$ kubectl logs carts-88b4ddf98-p5gqh -c istio-proxy -n demo
    2026-01-30T14:08:29.823282Z info FLAG: --concurrency="0"
    2026-01-30T14:08:29.823327Z info FLAG: --domain="demo.svc.cluster.local"
    2026-01-30T14:08:29.823332Z info FLAG: --help="false"
    2026-01-30T14:08:29.823335Z info FLAG: --log_as_json="false"
    2026-01-30T14:08:29.823337Z info FLAG: --log_caller=""
    2026-01-30T14:08:29.823339Z info FLAG: --log_output_level="default:info"
    2026-01-30T14:08:29.823341Z info FLAG: --log_rotate=""
    2026-01-30T14:08:29.823344Z info FLAG: --log_rotate_max_age="30"
    2026-01-30T14:08:29.823346Z info FLAG: --log_rotate_max_backups="1000"
    2026-01-30T14:08:29.823348Z info FLAG: --log_rotate_max_size="104857600"
    2026-01-30T14:08:29.823350Z info FLAG: --log_stacktrace_level="default:none"
    2026-01-30T14:08:29.823356Z info FLAG: --log_target="[stdout]"
    2026-01-30T14:08:29.823358Z info FLAG: --meshConfig="./etc/istio/config/mesh"
    2026-01-30T14:08:29.823361Z info FLAG: --outlierLogPath=""
    2026-01-30T14:08:29.823363Z info FLAG: --profiling="true"
    2026-01-30T14:08:29.823365Z info FLAG: --proxyComponentLogLevel="misc:error"
    2026-01-30T14:08:29.823368Z info FLAG: --proxyLogLevel="warning"
    2026-01-30T14:08:29.823370Z info FLAG: --serviceCluster="istio-proxy"
    2026-01-30T14:08:29.823372Z info FLAG: --stsPort="0"
    2026-01-30T14:08:29.823375Z info FLAG: --templateFile=""
    2026-01-30T14:08:29.823378Z info FLAG: --tokenManagerPlugin=""
    2026-01-30T14:08:29.823387Z info FLAG: --vklog="0"
    2026-01-30T14:08:29.823392Z info Version 1.23.6-6a112a28410654328342c68f82da48920e34f062-Clean
    2026-01-30T14:08:29.823398Z info Set max file descriptors (ulimit -n) to: 1048576
    2026-01-30T14:08:29.823719Z info Proxy role ips=[192.168.2.124] type=sidecar id=carts-88b4ddf98-p5gqh.demo domain=demo.svc.cluster.local
    2026-01-30T14:08:29.823783Z info Apply proxy config from env {"discoveryAddress":"istiod-istio-helm.istio-helm-system.svc:15012","tracing":{"zipkin":{"address":"jaeger-jaeger-operator-jaeger-collector.istio-system.svc.cluster.local:9411"},"sampling":100}}
    2026-01-30T14:08:29.825955Z info cpu limit detected as 2, setting concurrency
    2026-01-30T14:08:29.827354Z info Effective config: binaryPath: /usr/local/bin/envoy
    concurrency: 2
    configPath: ./etc/istio/proxy
    controlPlaneAuthPolicy: MUTUAL_TLS
    discoveryAddress: istiod-istio-helm.istio-helm-system.svc:15012
    drainDuration: 45s
    proxyAdminPort: 15000
    serviceCluster: istio-proxy
    statNameLength: 189
    statusPort: 15020
    terminationDrainDuration: 5s
    tracing:
    sampling: 100
    zipkin:
    address: jaeger-jaeger-operator-jaeger-collector.istio-system.svc.cluster.local:9411
    2026-01-30T14:08:29.827376Z info JWT policy is third-party-jwt
    2026-01-30T14:08:29.827380Z info using credential fetcher of JWT type in cluster.local trust domain
    2026-01-30T14:08:30.028797Z info Opening status port 15020
    2026-01-30T14:08:30.028831Z info Starting default Istio SDS Server
    2026-01-30T14:08:30.028850Z info CA Endpoint istiod-istio-helm.istio-helm-system.svc:15012, provider Citadel
    2026-01-30T14:08:30.028879Z info Using CA istiod-istio-helm.istio-helm-system.svc:15012 cert with certs: var/run/secrets/istio/root-cert.pem
    2026-01-30T14:08:30.029620Z info xdsproxy Initializing with upstream address "istiod-istio-helm.istio-helm-system.svc:15012" and cluster "Kubernetes"
    2026-01-30T14:08:30.031212Z info Pilot SAN: [istiod-istio-helm.istio-helm-system.svc]
    2026-01-30T14:08:30.033350Z info sds Starting SDS grpc server
    2026-01-30T14:08:30.033374Z info sds Starting SDS server for workload certificates, will listen on "var/run/secrets/workload-spiffe-uds/socket"
    2026-01-30T14:08:30.033503Z info starting Http service at 127.0.0.1:15004
    2026-01-30T14:08:30.035641Z info Starting proxy agent
    2026-01-30T14:08:30.035691Z info Envoy command: [-c etc/istio/proxy/envoy-rev.json --drain-time-s 45 --drain-strategy immediate --local-address-ip-version v4 --file-flush-interval-msec 1000 --disable-hot-restart --allow-unknown-static-fields -l warning --component-log-level misc:error --concurrency 2]
    2026-01-30T14:08:30.112678Z warning envoy main external/envoy/source/server/server.cc:936 There is no configured limit to the number of allowed active downstream connections. Configure a limit in `envoy.resource_monitors.downstream_connections` resource monitor. thread=14
    2026-01-30T14:08:30.114850Z warning envoy main external/envoy/source/server/server.cc:843 Usage of the deprecated runtime key overload.global_downstream_max_connections, consider switching to `envoy.resource_monitors.downstream_connections` instead.This runtime key will be removed in future. thread=14
    2026-01-30T14:08:30.123541Z info xdsproxy connected to delta upstream XDS server: istiod-istio-helm.istio-helm-system.svc:15012 id=1
    2026-01-30T14:08:30.156633Z info cache generated new workload certificate resourceName=default latency=126.712884ms ttl=23h59m59.843374146s
    2026-01-30T14:08:30.156685Z info cache Root cert has changed, start rotating root cert
    2026-01-30T14:08:30.156825Z info cache returned workload trust anchor from cache ttl=23h59m59.843176367s
    2026-01-30T14:08:30.192919Z info ads ADS: new connection for node:1
    2026-01-30T14:08:30.193043Z info cache returned workload certificate from cache ttl=23h59m59.806958673s
    2026-01-30T14:08:30.193482Z info ads ADS: new connection for node:2
    2026-01-30T14:08:30.193672Z info cache returned workload trust anchor from cache ttl=23h59m59.806329146s
    2026-01-30T14:08:31.182561Z info Readiness succeeded in 1.360041593s
    2026-01-30T14:08:31.183121Z info Envoy proxy is ready

    เข้าใช้งาน application ผ่านทาง load balancer ip โดยหา load balancer ip ได้จาก cli

    [nutanix@harbor ~]$ kubectl get svc front-end -n demo
    NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
    front-end LoadBalancer 10.108.248.153 10.38.37.21 80:32763/TCP 9m19s

    สามารถ monitor service ได้จาก kiali โดยไปที่ cluster แล้วเลือกเปิด kiali ตามภาพ

    เลือก Traffic Graph และ namespace เป็น demo

    หรือสามารถดู​ service tracing จาก Jaeger โดยเข้าจากหน้า cluster หลังจากเข้ามาที่หน้าจอหลัก เลือก service ที่ต้องการ

    กดที่เหตุการที่สนใจ jaeger จะแสดงรายละเอียดของ process ดังรูปภาพ

    Rook Ceph cluster

    การใช้งาน rook ceph cluster ใน Nutanix Kubernetes Platform เพื่อให้บริการ s3 storage สำหรับ application โดย rook ceph ได้ถูกติดตั้งใน mode high availability มาให้ซึ่งต้องมีจำนวน worker node อย่างน้อย 3 worker node เพื่อให้ rook ceph cluster สามารถทำงานได้ การเปิดใช้งานให้เข้าไปที่ menu application และเลือก enable Rook Ceph และ Rook Ceph Cluster ตามลำดับ

    ตรวจสอบว่า Rook Ceph ได้ติดตั้งเรียบร้อยแล้วจาก cli โดยเลือก namespace ตาม namespace ของ kubernetes cluster ที่เราติดตั้ง

    [nutanix@harbor ~]$ kubectl get deployment -n kommander-default-workspace
    NAME READY UP-TO-DATE AVAILABLE AGE
    ceph-cosi-driver 1/1 1 1 4m29s
    gatekeeper-audit 1/1 1 1 27h
    gatekeeper-controller-manager 2/2 2 2 27h
    kommander-default-workspace-reloader-reloader 1/1 1 1 27h
    kommander-traefik 2/2 2 2 27h
    kube-oidc-proxy 1/1 1 1 27h
    rook-ceph-crashcollector-dev2-md-0-rm6kd-f4xbb-8bgsj 1/1 1 1 2m56s
    rook-ceph-crashcollector-dev2-md-0-rm6kd-f4xbb-h9zqd 1/1 1 1 2m39s
    rook-ceph-crashcollector-dev2-md-0-rm6kd-f4xbb-hrl4t 1/1 1 1 2m59s
    rook-ceph-exporter-dev2-md-0-rm6kd-f4xbb-8bgsj 1/1 1 1 2m56s
    rook-ceph-exporter-dev2-md-0-rm6kd-f4xbb-h9zqd 1/1 1 1 2m39s
    rook-ceph-exporter-dev2-md-0-rm6kd-f4xbb-hrl4t 1/1 1 1 2m59s
    rook-ceph-mgr-a 1/1 1 1 2m56s
    rook-ceph-mgr-b 1/1 1 1 2m56s
    rook-ceph-mon-a 1/1 1 1 4m53s
    rook-ceph-mon-b 1/1 1 1 4m17s
    rook-ceph-mon-c 1/1 1 1 3m39s
    rook-ceph-operator 1/1 1 1 35m
    rook-ceph-osd-0 1/1 1 1 117s
    rook-ceph-osd-1 1/1 1 1 115s
    rook-ceph-osd-2 1/1 1 1 116s
    rook-ceph-osd-3 1/1 1 1 90s
    rook-ceph-tools 1/1 1 1 5m29s
    traefik-forward-auth 1/1 1 1 27h

    เปิดหน้าจอ rook ceph clusterโดยเลือกจาก cluster

    ใช้ default user คือ admin และใช้ cli เพื่อแสดงค่า default password

    [nutanix@harbor ~]$ kubectl -n kommander-default-workspace get secret rook-ceph-dashboard-password -ojsonpath='{.data.password}' | base64 -d
    6-$9>^PZj1^E!-*XKCdw

    เมื่อ login เข้ามาแล้วจะได้หน้าจอดังนี้

    เข้าไปที่เมนู Object และสร้าง user ใหม่

    จากนั้นเลือก Bucket และ create new bucket

    เข้าไปที่ Users อีกครั้งเพื่อ copy secret key และ access key สำหรับใช้ในการใช้งาน bucket ที่สร้างขึ้น

    เลือก user และที่ Key section แล้วเลือกที่ username

    กดปุ่ม show ก็จะแสดง username , Access Key และ Secret key สำหรับใช้ในการ access bucket