สัปดาห์นี้ก็ Store Data ล้วนๆ
สำหรับ part ก่อนหน้า
- The Cloud Camp Week#06 (K8S Part1:Overview) | naiwaen@DebuggingSoft
- The Cloud Camp Week#07 (K8S Part2:K8S Object) | naiwaen@DebuggingSoft
Volumes
ก่อนที่จะมาเรื่องใหม่ต้องมา Recap ก่อนจาก Part 1 ตัว Pod มันเก็บข้อมูลได้นะ แต่จะอยู่ชั่วคราว (ephemeral) เท่านั้น ดังนั้นเราจะพบ non-trivial problems ของ App ได้ ดังนี้
- ถ้า Pod ตาย Data ที่อยู่ในนั้นจะหายไปด้วย
- ถ้า Pod นั้นมี Container 2 ตัว จะแชร์ Data ระหว่าง Container อย่างไร ?
default pod ไม่ได้แชร์ไฟล์กันระหว่าง container นะ
ดังนั้น Volume จะมาช่วยแก้ปัญหาพวกนี้ แต่ควรมาเข้าใจตัว YAML Pattern กันก่อน
YAML Pattern
- Pod Level เชื่อม Volume
volumes:
- name: cache-volume #ชื่อ Volume
hostPath: #ชนิดของ Volume ในที่นี้จะเป็น hostPath
#ส่วนข้างล่างนี้จะเป็น Config ของแต่ละ Type ที่ทำได้ตาม Spec เช่น hostPath มี path/type
#หรือ emptyDir จะมี เช่น sizeLimit / medium
#หรือ persistentVolumeClaim เช่น claimName
path: /tmp
type: Directory- Container Level เอา Volume จาก Pod มาใช้งาน
volumeMounts:
- mountPath: /tmp/cache #Path ใน Container
name: cache-volume #ชื่อ Volume ที่ประกาศไว้ใน Pod LevelVolume Type ที่เด่นๆ ดังนี้
- hostPath
แบบนี้จะคล้ายกับตัว docker volume โดย Volume แบบ hostPath จะเอา path ของ node ที่ pod Deploy อยู่มา map กับ pod
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: mcss
name: mcss
spec:
containers:
- image: nginx
name: mcss
resources: {}
volumeMounts:
- mountPath: /tmp/cache
name: cache-volume
- image: pingkunga/service-test
name: c2
volumeMounts:
- mountPath: /tmp/cache
name: cache-volume
dnsPolicy: ClusterFirst
restartPolicy: Always
volumes:
- name: cache-volume
hostPath:
# directory location on host
path: /tmp
# this field is optional
type: Directory
status: {}- Key Config
- Path: ที่อยู่จริงๆบน Node ที่เราจะเชื่อมเข้ามา
- Type: มันจะมีหลายแบบต้องระวังการเรียกใช้
>> FileOrCreate: มันจะไม่สร้าง Directory มาให้นะ
>> File: ต้องมีไฟล์นั้นจริงๆ
>> DirectoryOrCreate / Directory ล้อกันมา และะมีแบบอื่นๆตาม Link
Ref: Volumes-HostPath | Kubernetes - Sample Use-Case
- ถ้าใช้ docker อาจจะต้องเข้าไป Config Path /var/lib/docker
- เปิดช่องให้ใน Tools monitor เข้ามาดูข้อมูล อย่างตัว google/cadvisor: Analyzes resource usage and performance characteristics of running containers. (github.com)
- สร้าง Path หลอก ในกรณีที่ Container มันต้องการใช้ตอน Start - Limitation
- ถ้า pod ตาย แล้ว kube-scheduler ไปสร้าง pod ใหม่ที่ node data จะหาย อารมณ์ประมาณวิญญาณเข้าผิดร่าง
แก้โดยใช้ตัว network path มา map แทน nfs
- Security Issue: ถ้าจำเป้นต้องใช้ hostPath ให้ Scope ที่เล็ก file / directory และ readOnly access ด้วย //มันแก้จาก node มาใส่ได้
- emptyDir
สร้าง Volume เปล่าๆ ผูกกับ pod (เกิด และตายไปด้วยกัน) ไม่จำเป็นต้องใช้ Path ของ Node แบบ hostPath อาจจะใช้ memory มา config ก็ได้ โดยจะมาแก้ปัญหาการแชร์ Data ระหว่าง Container ภายใน Pod
Note: emptyDir จัดการโดย kubelet
emptyDir can read and write the same files / mounted at the same or different paths in each container
- YAML
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: mcss
name: mcss
spec:
containers:
- image: nginx
name: mcss
resources: {}
volumeMounts:
- mountPath: /tmp/cache
name: cache-volume
- image: pingkunga/service-test
name: c2
volumeMounts:
- mountPath: /tmp/cache
name: cache-volume
dnsPolicy: ClusterFirst
restartPolicy: Always
volumes:
- name: cache-volume
emptyDir:
sizeLimit: 500Mi- Sample Use-Case
- scratch space เป็นพื้นที่ทด เช่น
>> merge sort Json เยอะๆ
>> พักงานที่ consume จาก queue เข้ามาก่อนประมวลผลต่อ
- checkpointing กรณีที่เรามีงานขนาดใหญ่ และ compute intensive (ใช้ CPU) เยอะ ถ้าพังระหว่างทางเสียเวลา Process ใหม่อีก เลยเอาตัว emptyDir มาเป็นกระดาษทด ทำไปแล้วเท่าไหร่ ในกรณีที่ pod มันค้าง แล้วตายไป อีก pod ที่จับอยู่ จะได้เอางานมาทำต่อจากจุดล่าสุด ไม่ต้อง process ใหม่ทั้งหมด แต่ตัว app ต้องปรับด้วยนะ
- local content cache - cache แบบ ถ้าในพวก WordPress ทำแบบ WP Super Cache ทำ เอามาเป็นพวก Static HTML แต่ที่จะไป Render PHP ใหม่ทุกรอบ
พอเห็น Sample Use-Case เข้าใจเลยว่าทำไมใช้ memory
volumes:
- name: cache-volume
emptyDir:
sizeLimit: 500Mi
medium: Memory- downwardAPI
เอาข้อมูล Pod Information มาใช้งาน เช่น เอามาลง Log หรือ เขียนเป็น File
- Key Config
metadata:
creationTimestamp: null
#0. Initial Metadata พวก labels / annotations / namespace
labels:
run: nginx
release: stable
environment: dev
tier: frontend
...
downwardAPI:
items:
- path: "namespace.txt" #1-2 อ่านมาแล้ว Save เป็นไฟล์ชื่อ namespace.txt
fieldRef:
fieldPath: metadata.labels #1-1 อ่านจากอะไร อันนี้จะมีใน doc จะอ่านทั้งหมด
- path: "appinfo/release" #2-2 อ่านมาแล้ว Save ไฟล์ชื่อ release อยู่ appinfo
fieldRef:
fieldPath: metadata.labels['release'] #2-1.อ่านเฉพาะ Label ชื่อ release - Sample Use-Case
- ข้อมูล Pod Information จะได้ไม่ต้องตัดจากตัว kubectl get pod -o your prooerty
- Expose Pod Information to Containers Through Files | Kubernetes - YAML
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
release: stable
environment: prod
tier: backend
app.kubernetes.io/name: "inv-app"
app.kubernetes.io/version: "8.8.12.0"
annotations:
apf.kubernetes.io/autoupdate-spec: "true"
applyset.kubernetes.io/additional-namespaces: "inv-app,reg-app"
applyset.kubernetes.io/tooling: "kubectl/v1.28"
name: nginxping
namespace: appbackend
spec:
containers:
- image: nginx
name: nginx
resources: {}
volumeMounts:
- name: podinfo
mountPath: /opt/app/labels
dnsPolicy: ClusterFirst
restartPolicy: Always
volumes:
- name: podinfo
downwardAPI:
items:
- path: "release"
fieldRef:
fieldPath: metadata.labels['release']
- path: "environment"
fieldRef:
fieldPath: metadata.labels['environment']
- path: "tier"
fieldRef:
fieldPath: metadata.labels['tier']
- path: "app.kubernetes.io/name"
fieldRef:
fieldPath: metadata.labels['app.kubernetes.io/name']
- path: "app.kubernetes.io/version"
fieldRef:
fieldPath: metadata.labels['app.kubernetes.io/version']
- path: "apf.kubernetes.io/autoupdate-spec"
fieldRef:
fieldPath: metadata.annotations['apf.kubernetes.io/autoupdate-spec']
- path: "applyset.kubernetes.io/additional-namespaces"
fieldRef:
fieldPath: metadata.annotations['applyset.kubernetes.io/additional-namespaces']
- path: "namespace"
fieldRef:
fieldPath: metadata.namespace
status: {}Ref: Downward API | Kubernetes
Ephemeral Volumes
Volume ที่ใช้เก็บข้อมูลชั่วคราว แก้เรื่อง Data หาย หรือ ทำ Cache (จริงๆมองอีก View ของการแบ่ง Volume)
- emptyDir: empty at Pod startup,
- configMap, downwardAPI, secret: inject different kinds of Kubernetes data into a Pod
- CSI ephemeral volumes: เป็น Volume ค่ายๆอื่นๆ ของที่พัฒนาตามมาตรฐาน CSI เช่น พวก Volume ของ Cloud ค่ายต่างๆ
- generic ephemeral volumes พวก PV/ PVC
Persistent Volume Family

- Storage Class (Class)
เอาไว้ทำ disk profile ก่อนนำมาใช้งานใน PV / PVC อาทิ เช่น SPEED (MEM / SSD / HDD / NETWORK) หรือ พวก Backup Policy เป็นต้น
- Key Property
- provisioner บอกว่าจะใช้ของค่ายไหน
Internal ของ K8S มีมาให้เองจะขึ้นต้นด้วย kubernetes.io เช่น Local (no-provisioner)
External ของ Cloud หรือ On-Premise ค่ายๆ อื่นๆ เช่น rancher.io/local-path
Note: หลัง K8S 1.28 Storage ของ Cloud จากเดิมที่ Internal จะแยกเป็น External คุยผ่าน CSI Driver
- parameters ล้อตาม provisioner ที่เลือก
- reclaimPolicy retain(เก็บไว้) vs delete
- allowVolumeExpansion - dynamically provisioned มีผลกับพวก pv / pvc ที่ on top อีกที
- mountOptions
- volumeBindingMode unset, Immediate, WaitForFirstConsumer (ที่เห็นใน Lab อันนี้) - Sample YAML
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-storage provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer
- List Storage Class ใช้กรณีทีเราไม่ได้ลง K8S เอง จะได้มา List Storage Class ที่มี Profile ไว้ สำหรับตอนสร้าง PV / PVC ต่อไป
kubectl get StorageClass #Sample Result NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 16d
Ref: Storage Classes | Kubernetes
- Persistent Volume (PV)
Persistent Volume เป็นการจองพื้นที่ตาม Profile ที่กำหนดไว้ใน Storage Class
- Key Config
- capacity > storage บอกขนาดของ Disk ที่จองหน่วย Gi
- volumeMode
>> FileSystem - พวก Folder ที่เราคุ้นเคย
>> Blob - เห็นเนื่อที่เท่าๆกัน ถ้าดูจะได้ Binary
- storageClassName - บอก Profile ของ Disk
NOTE: สังเกตุ YAML อันถัดไปมันจะมี HostPath ด้วย ใน doc k8s for test
Ref: Configure a Pod to Use a PersistentVolume for Storage | Kubernetes
- accessModes
- persistentVolumeReclaimPolicy
>> Delete - Pod ตายลบเลย
>> Retain - เก็บไว้ แต่ต้องใช้สร้างใหม่ต้องเอา Volume ใหม่มาจับ
>> Recycle - เอาออกในอนาคต แทนด้วย Dynamic Provisioner
accessModes
>> ReadWriteOnce - อ่าน + เขียน จาก Node เดียว
>> ReadOnlyMany - อ่านจากหลายๆ Node ได้
>> ReadWriteMany - อ่าน + เขียน จากหลายๆ Node ได้
>> ReadWriteOncePod - ensuring that only one pod can write to the volume at a time
Ref: Kubernetes 1.27: Single Pod Access Mode for PersistentVolumes Graduates to Beta | Kubernetes - Sample YAML
apiVersion: v1
kind: PersistentVolume
metadata:
name: ds-pv
spec:
capacity:
storage: 1Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
#hostPath for test
hostPath:
path: /tmp
storageClassName: local-path
persistentVolumeReclaimPolicy: Recycle
- Persistent Volume Claim (PVC)
Persistent Volume Claim (PVC) - เป็นยึดพื้นที่ (Claim) จาก PV เพื่อใช้งาน (a request for storage by a user)
- Key Config
- resources - บอกพื้นที่ที่จะใช้ resources.requests.storage
- selector - เลือก Claims จากการระบุตรงๆ หรือ expression (ตาม Selector)
> volumeName = มาจาก PV / Volume ตามชื่อตรงๆ
> selector จับตามเงื่อนไขจาก matchLabels / matchExpressions
Note: selector ตรงนี้อาจจะเขียนจับตาม Region / Zone จะได้ใน Data ใน Cloud เดียวกัน ใน DC เดียวกัน
- Sample YAML ตอนสร้างเสร็จ ต้องได้สถานะ Bind
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ds-pvc
spec:
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 512Mi
storageClassName: local-path
volumeName: ds-pv
#selector:
# matchLabels:
# release: "customer"
# matchExpressions:
# - {key: environment, operator: In, values: [dev]}- ตอนเอาไปใช้งานเอา PVC ที่เตรียมไว้ใน มาอ้างอิง Volume ของ Pod (persistentVolumeClaim.claimName)
apiVersion: v1
kind: Pod
metadata:
name: task-pv-pod
spec:
containers:
- name: task-pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: ds-pvc-storage
volumes:
- name: ds-pvc-storage
persistentVolumeClaim:
claimName: ds-pvcพวกลำดับของ SC > PV > PVC เหมือนจะข้ามได้จาก SC > PVC เลย
Ref: kubernetes - Can we get Persistent Volume with only PVC (without PV) in k8s? - Stack Overflow
Lifecycle of a volume and claim
เด่น Provisioning / Binding / Using / Reclaiming
Ref: Persistent Volumes | Kubernetes
Inject Data Into App
Command and Arguments - การกำหนดคำสั่งจากตัว declarative file เพื่อมา override Command ของ container image ที่มีให้ ใน dockerfile ก็คือ ตัว EntryPoint
apiVersion: v1
kind: Pod
metadata:
name: command-demo
labels:
purpose: demonstrate-command
spec:
containers:
- name: command-demo-container
image: debian
command: ["printenv"] #Overide container image ด้วยคำสั่ง printenv
args: ["HOSTNAME", "KUBERNETES_PORT"] #ส่ง Param ที่ printenv ต้องการ
restartPolicy: OnFailureEnvironment Variables - Inject ตัวแปร Environment Variables เข้าไปใน Container โดยมี 2 รูปแบบ
- env ประกาศใน Declarative File
apiVersion: v1
kind: Pod
metadata:
name: envar-demo
labels:
purpose: demonstrate-envars
spec:
containers:
- name: envar-demo-container
image: gcr.io/google-samples/node-hello:1.0
env:
- name: DEMO_GREETING
value: "Hello from the environment"
- name: DEMO_FAREWELL
value: "Such a sweet sorrow"
#ตอนตรวจก็
- envFrom ดึงจาก ConfigMap จำได้แม่น 55 คนอื่น Mount เข้ามาเราโยนมาใส่ แล้วเขียนไฟล์ใหม่ 555
use-case
apiVersion: v1
kind: Pod
metadata:
name: dependent-envars-demo
spec:
containers:
- name: dependent-envars-demo
args:
- while true; do echo -en '\n'; printf UNCHANGED_REFERENCE=$UNCHANGED_REFERENCE'\n'; printf SERVICE_ADDRESS=$SERVICE_ADDRESS'\n';printf ESCAPED_REFERENCE=$ESCAPED_REFERENCE'\n'; sleep 30; done;
command:
- sh
- -c
image: busybox:1.28
env:
- name: SERVICE_PORT
value: "80"
- name: SERVICE_IP
value: "172.17.0.1"
- name: UNCHANGED_REFERENCE
value: "$(PROTOCOL)://$(SERVICE_IP):$(SERVICE_PORT)"
- name: PROTOCOL
value: "https"
- name: SERVICE_ADDRESS
value: "$(PROTOCOL)://$(SERVICE_IP):$(SERVICE_PORT)"
- name: ESCAPED_REFERENCE
value: "$$(PROTOCOL)://$(SERVICE_IP):$(SERVICE_PORT)"
UNCHANGED_REFERENCE=$(PROTOCOL)://172.17.0.1:80 SERVICE_ADDRESS=https://172.17.0.1:80 ESCAPED_REFERENCE=$(PROTOCOL)://172.17.0.1:80
- Pod fields as values คล้ายๆกับ downwardapi
apiVersion: v1
kind: Pod
metadata:
name: dapi-envars-fieldref
spec:
containers:
- name: test-container
image: registry.k8s.io/busybox
command: [ "sh", "-c"]
args:
- while true; do
echo -en '\n';
printenv MY_NODE_NAME MY_POD_NAME;
printenv MY_POD_IP;
sleep 10;
done;
env:
- name: MY_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
restartPolicy: NeverConfigMap
เก็บ Config ที่ใช้งานบ่อยๆ หรือใช้กันหลายที่ จะได้จัดการจากจุดเดียว แต่ขนาดต้องไม่เกิน 1MB อยู่ในรูปแบบ key & value
- Command
kubectl create configmap config --from-literal=db_user=admin --from-literal=db_password=test #Get the config map as yaml file and try to understand kubectl get configmap config -o yaml #from the file kubectl create configmap configenv --from-env-file=config.env kubectl create configmap configfile --from-file=config.txt —from-file คือใส่ได้ทั้ง file ที่นามสกุลอื่นๆ ได้เลย แต่ถ้าเป็น -env ใส่ .env เข้าไป มันจะ convert เป้น env ใน configmap ให้เลย
- Declarative File
apiVersion: v1
kind: ConfigMap
metadata:
name: invconfigmap
namespace: default
data:
config_dev.json: |
{
"current_env" : "dev",
"Directory_Type" : "folder",
"report_location": "reports"
}
config_prod.json: |
{
"current_env" : "prod",
"Directory_Type" : "folder",
"report_location": "reports"
}Sample Use-Case
- ENV - Override Env ใช้กรณีที่ Key เปลี่ยน กรณีนี้ Image แก้จาก ASPNETCORE_ENVIRONMENT >> ASPNETCORE_ENV ถ้าแก้จาก ConfigMap Container ที่ Run Image Version ก่อน อาจจะตุยได้
...
env:
- name: ASPNETCORE_ENV
valueFrom:
configMapKeyRef:
name: options #ชิ้่อ Config Map
key: ASPNETCORE_ENVIRONMENT #key ใน Config Map- ENV:KeyRef - Initial Env จาก Config Map ทับทั้งชุด
kubectl create configmap config --from-literal=db_user=admin --from-literal=db_password=test #Get the config map as yaml file and try to understand kubectl get configmap config -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx2
name: nginx2
spec:
containers:
- image: nginx
name: nginx2
resources: {}
envFrom:
- configMapRef:
name: config
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}- Volume - All Key as File > Mount เข้าไป
apiVersion: apps/v1
kind: Deployment
metadata:
name: invapp
spec:
replicas: 1
selector:
matchLabels:
app: invapp
template:
metadata:
labels:
app: invapp
spec:
containers:
- name: invapp
image: invapp:latest
volumeMounts:
- mountPath: /etc/config
name: json-configmap-volume
resources:
requests:
cpu: 100m
memory: 1Gi
#Mount configmap เข้าไป
volumes:
- name: json-configmap-volume
configMap:
name: invconfigmap #<-- ต้องประกาศไว้ก่อน ถ้าใน Blog ตัวอย่างอยู่ใน Declarative FileNote ถ้ามัน pending นานให้ describe ดูได้ อาจจะติดเช่น หา ConfigMap ไม่เจอ
Secret
มัน คือ ConfigMap ที่ใช้เก็บข้อมูลสำคัญ โดยจะ Encrypt อยู่ในรูปแบบ Base64 แต่บน K8S มี Guideline ใช้ Secret ดังนี้
- Encrypt At Rest (ปล ข้อมูลใน ETCD ไม่ Encrypt)
- RBAC + least privilege
- Restrict Container Access เช่น การป้องกัน Shell / Distroless Image
- ใช้ External Service Provide เช่น Azure KeyVault / HashiCorp Vault
ตัว Secret พอเข้า env ของ app ไปเดี๋ยวมัน decrypt ออกให้
- Command
kubectl create secret generic pingsecret --from-literal=db_user=admin --from-literal=db_password=test -n group-1 #Get the secret as yaml file and try to understand kubectl get secrets pingsecret -o yaml -n group-1
- Declarative File
apiVersion: v1 kind: Secret metadata: name: my-secret type: Opaque data: username: YWRtaW4= password: MWYyZDFlMmU2N2Rm
Sample Use-Case - เปลี่ยนจาก configMapRef > secretKeyRef
- ENV - Override Env
apiVersion: v1
kind: Pod
metadata:
name: env-single-secret
spec:
containers:
- name: envars-test-container
image: nginx
env:
- name: SECRET_USERNAME #APP ใช้
valueFrom:
secretKeyRef:
name: backend-user #ชื่อ Config
key: backend-username #ชื่อ key- ENV:KeyRef - Initial Env จาก Secret Map ทับทั้งชุด
apiVersion: apps/v1
kind: Deployment
metadata:
name: invapp
labels:
app: invapp
spec:
replicas: 1
selector:
matchLabels:
app: invapp
template:
metadata:
labels:
app: invapp
spec:
containers:
- name: app
image: invapp:lastest
imagePullPolicy: Always
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: env-configmap
- secretRef:
name: my-secret- Volume - All Key as File > Mount เข้าไป
apiVersion: v1
kind: Pod
metadata:
name: secret-test-pod
spec:
containers:
- name: test-container
image: nginx
volumeMounts:
- name: secret-volume
mountPath: /etc/secret-volume
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: my-secretNote: ใน yaml - คือ List มีลำดับ
Q: ถ้า configmap / secret เปลี่ยน ให้ restart เพื่อ load ค่าใหม่
A: witness controller ปรับ App Live reload configuration
สรุปก่อน
การเก็บข้อมูลใน K8S จะแบ่งกลุ่มได้ ดังนี้
Storage Data ที่เรียนมา - Volume -> hostPath - แปะลง node path ระวังเรื่อง pod ตายวิญญาณอาจจะเข้าผิดร่าง -> emptyDir - ตายไปของหลาย ทำงานชั่วคราว -> downwardAPI - ดึงข้อมูลของ Pod -> Persistent Volume - เก็บถาวร PVC / PV / Storage Class -> อื่่นๆ - Inject Data to App: Env + Command & Args - ConfigMap - Secret
วันเสาร์มีมาทำ LAB ตอนเช้าพอทำได้ ตอนบ่ายทำได้แหละ แต่ท่าไม่เหมือนชาวบ้าน คนอื่น mount secret / configmap เป็น volume ผม inject เข้า app แล้ว print ออกมา 555
Reference
Discover more from naiwaen@DebuggingSoft
Subscribe to get the latest posts sent to your email.



