แปลงไฟล์ด้วย K8s Job + ให้ API สั่งงานเอง (RBAC)
สร้างอิมเมจ transcoder, ทดสอบ Job ด้วยมือ แล้วให้ API สร้าง Job อัตโนมัติผ่าน RBAC
หัวใจของ workshop อยู่บทนี้ — งานแปลงวิดีโอคืองานที่ "ทำแล้วจบ" และกิน CPU หนัก ไม่ควรไปเบียด Pod ของ API เราจึงแยกเป็น K8s Job: อัปโหลด 1 ครั้ง = Job 1 ตัวเกิดขึ้นมาแปลงไฟล์แล้วตายไป — นี่คือ pattern เดียวกับที่ระบบวิดีโอจริง (YouTube, Mux) ใช้
Job เหมือนจ้างพนักงานชั่วคราวรายชิ้นงาน — มีของเข้ามาค่อยเรียกมาทำ เสร็จแล้วจ่ายเงินแยกย้าย (Pod ถูกลบ) ไม่ต้องจ้างประจำนั่งเฝ้าโต๊ะเปลือง CPU · ถ้าทำพลาด K8s เรียกคนใหม่มาทำซ้ำให้ (backoffLimit)
สร้างอิมเมจ transcoder
#!/bin/sh
set -e # พลาดขั้นไหนให้ตายทันที (Job จะได้รู้ว่า fail)
echo "=== เริ่มแปลงวิดีโอ videoId=$VIDEO_ID ==="
mc alias set store "$MINIO_URL" "$MINIO_USER" "$MINIO_PASS"
# 1) โหลดต้นฉบับจาก bucket uploads
mc cp "store/uploads/$VIDEO_ID/source.mp4" /work/source.mp4
# 2) แปลงเป็น HLS 720p (คำสั่งเดียวกับที่ลองมือในบท 8.2)
mkdir -p /work/out
ffmpeg -i /work/source.mp4 \
-c:v libx264 -preset veryfast -b:v 2500k -vf "scale=-2:720" \
-c:a aac -b:a 128k \
-f hls -hls_time 4 -hls_playlist_type vod \
-hls_segment_filename "/work/out/seg_%03d.ts" \
/work/out/index.m3u8
# 3) อัปโหลดผลลัพธ์ทั้งโฟลเดอร์ขึ้น bucket vod
mc cp --recursive /work/out/ "store/vod/$VIDEO_ID/"
echo "=== เสร็จแล้ว: vod/$VIDEO_ID/index.m3u8 ==="FROM alpine:3.20
RUN apk add --no-cache ffmpeg
# TARGETARCH ถูกเซ็ตให้อัตโนมัติโดย BuildKit (arm64 บน Apple Silicon, amd64 บน Intel)
ARG TARGETARCH
ADD https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc /usr/local/bin/mc
RUN chmod +x /usr/local/bin/mc
WORKDIR /work
COPY transcode.sh /transcode.sh
RUN chmod +x /transcode.sh
ENTRYPOINT ["/transcode.sh"]docker build -t video-transcoder:v1 ./transcoder
kind load docker-image video-transcoder:v1 --name videoทดสอบ Job ด้วยมือก่อน (สำคัญ!)
ก่อนให้ API สร้าง Job อัตโนมัติ เราเขียน manifest แล้ว apply เองก่อน — ถ้าขั้นนี้ผ่าน แปลว่าอิมเมจ+สคริปต์ถูกต้อง เหลือแค่เรื่องสิทธิ์ของ API · เอา videoId จริงจากบทที่แล้วมาแทนที่ `a1b2c3d4` ให้ตรงกับที่มีใน bucket
apiVersion: batch/v1
kind: Job
metadata:
name: transcode-test
namespace: video
spec:
backoffLimit: 2 # fail ได้ไม่เกิน 2 ครั้งก่อนยอมแพ้
ttlSecondsAfterFinished: 600 # เสร็จแล้ว 10 นาทีลบตัวเองทิ้ง
template:
spec:
restartPolicy: Never
containers:
- name: ffmpeg
image: video-transcoder:v1
imagePullPolicy: IfNotPresent
env:
- name: VIDEO_ID
value: "a1b2c3d4" # ← แก้เป็น videoId จริงของคุณ
- name: MINIO_URL
value: "http://minio:9000"
- name: MINIO_USER
valueFrom:
secretKeyRef: { name: minio-creds, key: user }
- name: MINIO_PASS
valueFrom:
secretKeyRef: { name: minio-creds, key: pass }
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { cpu: "2", memory: 1Gi }kubectl apply -f k8s/job-test.yaml
kubectl get jobs,pods # เห็น Pod transcode-test-xxxxx กำลังรัน
kubectl logs -f job/transcode-test # ดู ffmpeg ทำงานสด ๆ
# จบแล้วต้องเห็น "=== เสร็จแล้ว ===" และ
kubectl get job transcode-test # COMPLETIONS 1/1
# เช็คใน MinIO console: bucket vod ต้องมีโฟลเดอร์ videoId + index.m3u8 + seg ครบให้ API สร้าง Job เอง — ต้องขอสิทธิ์ก่อน (RBAC)
โดย default Pod ไม่มีสิทธิ์สร้าง resource ใด ๆ ใน cluster — เราต้องสร้าง Role (ชุดสิทธิ์จัดการ Job) แล้ว RoleBinding ผูกให้ ServiceAccount video-api ที่ Pod ของ API ใช้อยู่ (หลัก least privilege: ให้แค่ Job ใน namespace video เท่านั้น)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: job-manager
namespace: video
rules:
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: video-api-job-manager
namespace: video
subjects:
- kind: ServiceAccount
name: video-api
namespace: video
roleRef:
kind: Role
name: job-manager
apiGroup: rbac.authorization.k8s.iokubectl apply -f k8s/rbac.yaml
kubectl auth can-i create jobs \
--as=system:serviceaccount:video:video-api -n video # → yescd ~/projects/k8s-video-pipeline/api
npm install @kubernetes/client-nodeconst k8s = require('@kubernetes/client-node')
const kc = new k8s.KubeConfig()
kc.loadFromDefault() // ใน Pod = ใช้ ServiceAccount / บนเครื่องเรา = ใช้ kubeconfig
const batchApi = kc.makeApiClient(k8s.BatchV1Api)
const NAMESPACE = process.env.JOB_NAMESPACE || 'video'
function transcodeJobManifest(videoId) {
return {
apiVersion: 'batch/v1',
kind: 'Job',
metadata: {
name: `transcode-${videoId}`,
labels: { app: 'transcoder', videoId },
},
spec: {
backoffLimit: 2,
ttlSecondsAfterFinished: 600,
template: {
spec: {
restartPolicy: 'Never',
containers: [
{
name: 'ffmpeg',
image: 'video-transcoder:v1',
imagePullPolicy: 'IfNotPresent',
env: [
{ name: 'VIDEO_ID', value: videoId },
{ name: 'MINIO_URL', value: `http://${process.env.MINIO_HOST}:9000` },
{ name: 'MINIO_USER', valueFrom: { secretKeyRef: { name: 'minio-creds', key: 'user' } } },
{ name: 'MINIO_PASS', valueFrom: { secretKeyRef: { name: 'minio-creds', key: 'pass' } } },
],
resources: {
requests: { cpu: '500m', memory: '512Mi' },
limits: { cpu: '2', memory: '1Gi' },
},
},
],
},
},
},
}
}
// ใน handler /api/upload — ต่อจาก minio.fPutObject(...)
if (process.env.ENABLE_JOBS === 'true') {
await batchApi.createNamespacedJob({ namespace: NAMESPACE, body: transcodeJobManifest(videoId) })
console.log(`สร้าง Job transcode-${videoId} แล้ว`)
}docker build -t video-api:v1 ./api
kind load docker-image video-api:v1 --name video
kubectl rollout restart deployment/video-api
kubectl rollout status deployment/video-api
# terminal 1 — Pod เพิ่ง restart ถ้ามี forward เก่าค้างอยู่ให้ Ctrl+C แล้วรันใหม่เสมอ
kubectl port-forward svc/video-api 3000:80
# terminal 2
curl -F "video=@sample.mp4" http://localhost:3000/api/upload
kubectl get jobs -w # ต้องเห็น transcode-<videoId> โผล่มาแล้ววิ่งจน 1/1 เอง!สรุป Key Takeaways
- งานทำแล้วจบ + กิน CPU หนัก = Job แยกจาก Pod เสิร์ฟ · 1 อัปโหลด = 1 Job
- Pod จะสร้าง resource ใน cluster ได้ต้องมี RBAC: ServiceAccount + Role + RoleBinding
- ทดสอบ Job ด้วย manifest มือก่อน แล้วค่อยให้ API สร้างผ่าน client library
- ttlSecondsAfterFinished ให้ Job เก็บกวาดตัวเอง · backoffLimit คุมจำนวน retry

