ข้ามไปเนื้อหาหลัก
คอนเทนเนอร์· ~40 นาที

แปลงไฟล์ด้วย 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 ==="
📄 transcoder/transcode.sh — งานทั้งหมดของ Job: โหลด → แปลง → อัปโหลดกลับ
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"]
📄 transcoder/Dockerfile — ffmpeg + mc (MinIO client) ในอิมเมจเดียว
docker build -t video-transcoder:v1 ./transcoder
kind load docker-image video-transcoder:v1 --name video
💻 build แล้ว load เข้า kind

ทดสอบ 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 }
📄 k8s/job-test.yaml
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.io
📄 k8s/rbac.yaml
kubectl apply -f k8s/rbac.yaml
kubectl auth can-i create jobs \
  --as=system:serviceaccount:video:video-api -n video   # → yes
💻 apply แล้วเช็คสิทธิ์ด้วย can-i
cd ~/projects/k8s-video-pipeline/api
npm install @kubernetes/client-node
💻 ติดตั้ง Kubernetes client ใน API
const 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} แล้ว`)
}
📄 api/server.js — เวอร์ชัน 3: สร้าง Job หลังอัปโหลดเสร็จ
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 เอง!
💻 build ใหม่ → load → restart → ทดสอบ end-to-end ครั้งแรก!

สรุป Key Takeaways

  • งานทำแล้วจบ + กิน CPU หนัก = Job แยกจาก Pod เสิร์ฟ · 1 อัปโหลด = 1 Job
  • Pod จะสร้าง resource ใน cluster ได้ต้องมี RBAC: ServiceAccount + Role + RoleBinding
  • ทดสอบ Job ด้วย manifest มือก่อน แล้วค่อยให้ API สร้างผ่าน client library
  • ttlSecondsAfterFinished ให้ Job เก็บกวาดตัวเอง · backoffLimit คุมจำนวน retry
อ่านจบแล้วอย่าลืมทำเครื่องหมาย