- 1. เข้าใจหน้าที่ของแต่ละส่วน
- 2. ภาพรวมการทำงาน
- 3. DOKS จัดการอะไร และเรายังต้องจัดการอะไร
- 4. เครื่องมือและสิ่งที่ต้องเตรียม
- 5. สร้างแอปตัวอย่าง
- 6. Build และทดสอบบนเครื่อง
- 7. สร้าง DOCR และ Push Image
- 8. สร้างคลัสเตอร์ DOKS
- 9. ให้ DOKS ดึง Private Image จาก DOCR
- 10. สร้าง Deployment และ Service
- 11. Deploy และตรวจสอบ
- 12. เปิดบริการผ่าน DigitalOcean Load Balancer
- 13. อัปเดตแอปด้วย Rolling Update
- 14. ขยายจำนวน Pod
- 15. ต่อเติมเป็น CI/CD
- 16. ปัญหาที่พบบ่อย
- 17. ค่าใช้จ่ายและการเก็บ Image
- 18. ล้างทรัพยากรหลังเวิร์กช็อป
- สิ่งที่ได้จากเวิร์กช็อป
- เอกสารอ้างอิง
อัปเดต: 1 ตุลาคม 2026 · ตัวอย่างคำสั่งใช้ Bash บน macOS, Linux หรือ Windows ผ่าน WSL2
บทความนี้อธิบายแนวคิดและให้ตัวอย่างสำหรับทดลองทำตาม คำสั่งสร้างคลัสเตอร์และ Load Balancer จะสร้างทรัพยากรที่มีค่าใช้จ่าย ตรวจไวยากรณ์ JavaScript/Bash, โครงสร้าง YAML และการตอบ HTTP ของแอปบนเครื่องแล้ว ส่วนการ Build ด้วย Docker และ Deploy บน DigitalOcean ยังไม่ได้รันจริง

เมื่อพัฒนาแอปพลิเคชันด้วย Container เราต้องจัดการทั้งการสร้างแพ็กเกจ การเก็บแพ็กเกจ และการนำแพ็กเกจไปรันบนระบบจริง บน DigitalOcean สามารถเชื่อมงานเหล่านี้ด้วย Docker, DigitalOcean Container Registry (DOCR) และ DigitalOcean Kubernetes (DOKS)
Docker ช่วยสร้าง Container Image, DOCR เก็บและแจกจ่าย Image ส่วนตัว ส่วน DOKS เป็นบริการ Kubernetes ที่นำ Image มารันและจัดการแอปพลิเคชันตามการตั้งค่าของเรา ทั้งสามส่วนทำงานร่วมกันได้ แต่ยังต้องกำหนดสิทธิ์ดึง Image, ทรัพยากร, เครือข่าย และวิธีเผยแพร่บริการให้ถูกต้อง [1][2]
#1. เข้าใจหน้าที่ของแต่ละส่วน
| ส่วนประกอบ | หน้าที่หลัก | สิ่งที่นักพัฒนากำหนด | ตัวอย่าง |
|---|---|---|---|
| Docker | สร้าง Image และรัน Container เพื่อพัฒนา/ทดสอบ | Dockerfile, Base Image, ไฟล์แอป, สถาปัตยกรรม CPU | สร้าง Image ของ REST API |
| DOCR | เก็บและแจกจ่าย Image ใน Private Registry | ชื่อ Registry, Repository, Tag, สิทธิ์เข้าถึง | registry.digitalocean.com/my-registry/demo-api:v1 |
| DOKS | ให้บริการ Kubernetes ที่ DigitalOcean จัดการ Control Plane | Node Pool, Deployment, Service, จำนวน Pod, Resource Limits | รัน API สอง Pod และทำ Rolling Update |
Image คือแพ็กเกจที่ใช้สร้าง Container ส่วน Container คืออินสแตนซ์ที่กำลังรันจาก Image เดียวกัน Image หนึ่งตัวจึงใช้สร้าง Container ได้หลายตัว
Registry เป็นบริการเก็บ Image ขณะที่ Repository เป็นกลุ่ม Image ของแอปหนึ่ง เช่น demo-api ภายใน Registry เดียวกัน และ Tag เป็นชื่ออ้างอิงเวอร์ชัน เช่น v1, v2 หรือ Git commit SHA
ชื่อ Image แบบเต็มมีรูปแบบดังนี้:
registry.digitalocean.com/<registry-name>/<repository>:<tag>
ตัวอย่าง:
registry.digitalocean.com/se-container-lab/demo-api:v1
Docker เป็นเครื่องมือที่ใช้ในเวิร์กช็อปนี้ เราสามารถสร้าง Image ที่เข้ากันได้ด้วยเครื่องมืออื่นเช่นกัน และ Kubernetes ไม่จำเป็นต้องใช้ Docker Engine เป็น Container Runtime เพราะรองรับ Runtime ผ่าน CRI เช่น containerd หรือ CRI-O [3]
#2. ภาพรวมการทำงาน
ลำดับที่นักพัฒนามักใช้งานคือ เขียนโค้ด → Build Image → Push ไป DOCR → Deploy บน DOKS → ตรวจสอบและปรับขนาด
flowchart TD
Source["Source Code และ Dockerfile"] --> Build["Docker Build"]
Build -->|"Push Image"| Registry["DOCR: Private Image Registry"]
Manifest["Kubernetes Manifest"] --> Deploy
Registry -->|"Worker Node ดึง Image"| Pods
Users["ผู้ใช้งาน"] --> LB["DigitalOcean Load Balancer"]
LB --> Service
subgraph Cluster["DOKS"]
Deploy["Deployment"] -->|"กำหนดเวอร์ชันและจำนวน"| Pods["Application Pods"]
Service["Kubernetes Service"] -->|"ส่งต่อ Request"| Pods
end
DOCR อยู่ในเส้นทางแจกจ่าย Image ส่วน Request ของผู้ใช้เข้าผ่าน Load Balancer และ Service ไปยัง Pod การ Push Image ใหม่เข้า Registry โดยลำพังไม่ได้เปลี่ยน Deployment ในเวิร์กช็อปนี้ เราจะอัปเดตค่า image ของ Deployment อย่างชัดเจน
#3. DOKS จัดการอะไร และเรายังต้องจัดการอะไร
DigitalOcean ดูแล Control Plane ของบริการ DOKS และให้การเชื่อมต่อกับผลิตภัณฑ์ เช่น Load Balancer และ Storage ส่วนผู้ใช้ยังต้องดูแลการตั้งค่าแอปและ Worker Node Pool [2]
| DigitalOcean จัดการในบริการ | ผู้ใช้กำหนดและดูแล |
|---|---|
| โครงสร้างพื้นฐานของ Managed Control Plane | ขนาดและจำนวน Worker Node |
| กลไกบริการสำหรับสร้างและอัปเกรดคลัสเตอร์ | แผนอัปเกรดและตรวจสอบความเข้ากันได้ของแอป |
| การเชื่อมต่อกับบริการ Cloud ที่รองรับ | Manifest, สิทธิ์, Network Policy และข้อมูลลับ |
| บริการ Registry และการยืนยันตัวตนที่รองรับ | การออกเวอร์ชัน Image, การสแกน และการเก็บเวอร์ชันย้อนหลัง |
การใช้ Managed Kubernetes ช่วยลดงานดูแล Control Plane แต่คุณภาพของแอปยังขึ้นกับ Health Checks, Resource Requests/Limits, การกระจาย Pod, การสำรองข้อมูล และการเฝ้าระวังระบบ
#4. เครื่องมือและสิ่งที่ต้องเตรียม
- บัญชี DigitalOcean ที่สามารถสร้าง Container Registry และ Kubernetes Cluster ได้
- Docker Desktop หรือ Docker Engine พร้อม Buildx
doctlรุ่นปัจจุบันที่รองรับคำสั่งregistrieskubectlรุ่นที่เข้ากันได้กับ Kubernetes ของคลัสเตอร์- Terminal แบบ Bash และ
curl - API Token ที่มีสิทธิ์เพียงพอสำหรับทรัพยากรที่ต้องสร้าง
บทความนี้ใช้คำสั่ง doctl registries แบบพหูพจน์ เพื่อระบุ Registry ที่ต้องการอย่างชัดเจน เอกสารเก่าบางชุดอาจใช้ doctl registry โดยเอกสาร CLI ปัจจุบันระบุให้ registries login รับชื่อ Registry [4][5]
ตรวจสอบเครื่องมือ:
docker version
docker buildx version
doctl version
doctl registries --help
kubectl version --client
บน macOS ที่ติดตั้ง Homebrew แล้ว สามารถติดตั้ง doctl ด้วย:
brew install doctl
บน Ubuntu ที่ใช้ Snap:
sudo snap install doctl
sudo snap connect doctl:kube-config
sudo snap connect doctl:dot-docker
การเชื่อมต่อ Snap สองรายการนี้ช่วยให้ doctl เข้าถึงไฟล์ตั้งค่า Kubernetes และ Docker ตามที่เอกสารระบุ สำหรับระบบอื่นใช้วิธีติดตั้งจากเอกสารทางการ [6]
ยืนยันตัวตนผ่าน Prompt เพื่อไม่ต้องใส่ Token ไว้ในคำสั่ง:
doctl auth init --context container-lab
doctl auth switch --context container-lab
doctl account get
หากเปิด Terminal ใหม่ ให้กำหนดตัวแปรในเวิร์กช็อปอีกครั้ง ตรวจสอบทั้งบัญชี DigitalOcean และ Kubernetes Context ก่อนสร้างหรือแก้ไขทรัพยากร
#5. สร้างแอปตัวอย่าง
เราจะสร้าง HTTP API ด้วย Node.js โดยใช้โมดูลมาตรฐาน จึงไม่มี Dependency ที่ต้องติดตั้งเพิ่มเติม แอปรองรับ:
| Endpoint | ผลลัพธ์ |
|---|---|
GET / |
ข้อความ, เวอร์ชันแอป และชื่อ Pod/Container |
GET /healthz |
สถานะสำหรับ Health Check |
| เส้นทางอื่น | HTTP 404 |
| Method อื่น | HTTP 405 |
สร้างโฟลเดอร์:
mkdir digitalocean-container-lab
cd digitalocean-container-lab
mkdir k8s
สร้างไฟล์ server.js:
const http = require('node:http');
const os = require('node:os');
const port = Number(process.env.PORT || 8080);
const version = process.env.APP_VERSION || 'development';
function json(res, status, payload) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(payload));
}
const server = http.createServer((req, res) => {
if (req.method !== 'GET') {
res.setHeader('Allow', 'GET');
return json(res, 405, { error: 'Method not allowed' });
}
const pathname = new URL(req.url, 'http://localhost').pathname;
if (pathname === '/healthz') {
return json(res, 200, { status: 'ok' });
}
if (pathname === '/') {
return json(res, 200, {
message: 'Hello from Docker, DOCR and DOKS',
version,
hostname: os.hostname()
});
}
return json(res, 404, { error: 'Not found' });
});
server.listen(port, '0.0.0.0', () => {
console.log(`API listening on port ${port}, version=${version}`);
});
process.once('SIGTERM', () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10000).unref();
});
การ Listen ที่ 0.0.0.0 ช่วยให้ระบบภายนอก Container ติดต่อแอปได้ ส่วนการจัดการ SIGTERM ช่วยให้แอปปิดตัวหลังรับสัญญาณหยุดจาก Kubernetes
สร้าง Dockerfile:
FROM node:24-alpine
WORKDIR /app
ARG APP_VERSION=v1
ENV NODE_ENV=production
ENV PORT=8080
ENV APP_VERSION=${APP_VERSION}
COPY --chown=node:node server.js ./server.js
USER node
EXPOSE 8080
CMD ["node", "server.js"]
ตัวอย่างใช้ Official Node Image และรันด้วยผู้ใช้ node แทน root สำหรับระบบจริงควรเลือก Patch Version หรือ Digest ของ Base Image ที่ผ่านการตรวจสอบแล้ว [7]
สร้าง .dockerignore:
.git
.env
.env.*
node_modules
k8s
*.log
#6. Build และทดสอบบนเครื่อง
docker build --build-arg APP_VERSION=v1 -t demo-api:v1 .
docker run -d --name demo-api-local -p 127.0.0.1:8080:8080 demo-api:v1
curl -fsS http://localhost:8080/
curl -fsS http://localhost:8080/healthz
ผลลัพธ์จาก / ควรมีลักษณะดังนี้ โดย hostname จะแตกต่างกัน:
{
"message": "Hello from Docker, DOCR and DOKS",
"version": "v1",
"hostname": "example-container"
}
ตรวจ Log และหยุด Container หลังทดสอบ:
docker logs demo-api-local
docker rm -f demo-api-local
#7. สร้าง DOCR และ Push Image
กำหนดชื่อ Registry ให้ไม่ซ้ำกับที่มีอยู่ เปลี่ยนค่า your-unique-registry ก่อนรัน:
export REGISTRY_NAME="your-unique-registry"
export APP_NAME="demo-api"
export IMAGE_TAG="v1"
export IMAGE="registry.digitalocean.com/${REGISTRY_NAME}/${APP_NAME}:${IMAGE_TAG}"
doctl registries options available-regions
doctl registries options subscription-tiers
read -r -p "Registry region slug: " DOCR_REGION
export DOCR_REGION
เลือก Region ที่มีในผลลัพธ์ หากมี Region ร่วมกับ DOKS ที่ต้องการใช้งานให้พิจารณาใช้ Region เดียวกัน
สร้าง Registry สำหรับทดลองด้วย Starter Plan:
doctl registries create "$REGISTRY_NAME" \
--region "$DOCR_REGION" \
--subscription-tier starter
doctl registries get "$REGISTRY_NAME"
doctl registries login "$REGISTRY_NAME" --expiry-seconds 3600
Starter มีข้อจำกัดจำนวน Repository และพื้นที่เก็บ Image หากบัญชีใช้ Registry หรือแผนอยู่แล้ว ให้ใช้ Registry เดิมตามสิทธิ์และแผนของบัญชี แล้วข้ามคำสั่งสร้างใหม่ การสร้าง Registry ใช้ชื่อที่ประกอบด้วยตัวพิมพ์เล็ก ตัวเลข และขีดกลาง [4][8]
#ตรวจสถาปัตยกรรมก่อนส่ง Image ขึ้น Cloud
Image ที่ Build บน Mac Apple Silicon อาจเป็น linux/arm64 ขณะที่ Worker Node ที่เราเลือกอาจเป็น linux/amd64 ต้องสร้าง Image ให้ตรงกับปลายทาง [9]
ตัวอย่างต่อไปนี้ตั้งใจเลือก Worker Node แบบ x86-64 ในขั้นตอนสร้างคลัสเตอร์ จึง Build เป็น linux/amd64:
docker buildx build \
--platform linux/amd64 \
--build-arg APP_VERSION="$IMAGE_TAG" \
--tag "$IMAGE" \
--push .
คำสั่งนี้ Build และ Push โดยตรง จึงไม่จำเป็นต้องใช้ docker push ซ้ำ หากใช้ Builder ที่รองรับ สามารถสร้างหลายสถาปัตยกรรมได้ด้วย --platform linux/amd64,linux/arm64 แต่ควรทำเมื่อมีความต้องการใช้งานทั้งสองแบบ
ตรวจข้อมูล Image ที่เผยแพร่แล้ว:
docker buildx imagetools inspect "$IMAGE"
การ Push สำเร็จเป็นเพียงการจัดเก็บ Image ขั้นตอนถัดไปจึงเป็นการสร้างคลัสเตอร์และสั่ง Deployment ใช้ Image นี้
#8. สร้างคลัสเตอร์ DOKS
ตรวจตัวเลือกปัจจุบันก่อนเลือก Region, Machine Size และ Kubernetes Version:
doctl kubernetes options regions
doctl kubernetes options sizes
doctl kubernetes options versions
กำหนดค่าตามผลลัพธ์ เลือก Machine Size แบบ x86-64 เพื่อให้ตรงกับ Image linux/amd64 ในขั้นตอนก่อนหน้า:
export CLUSTER_NAME="container-lab"
export APP_NAMESPACE="container-lab"
read -r -p "DOKS region slug: " DOKS_REGION
read -r -p "Worker size slug (x86-64): " NODE_SIZE
read -r -p "Kubernetes version slug: " K8S_VERSION
export DOKS_REGION NODE_SIZE K8S_VERSION
สร้างคลัสเตอร์สำหรับทดลองหนึ่ง Worker Node:
doctl kubernetes cluster create "$CLUSTER_NAME" \
--region "$DOKS_REGION" \
--version "$K8S_VERSION" \
--size "$NODE_SIZE" \
--count 1 \
--ha=false \
--wait
คำสั่งนี้มีค่าใช้จ่ายสำหรับ Worker Node ใช้หนึ่ง Node เพื่อศึกษาลำดับงาน และปิด HA Control Plane อย่างชัดเจนตามขอบเขตการทดลอง คลัสเตอร์นี้ยังไม่มีความทนทานต่อการเสียของ Worker Node แม้จะรันหลาย Pod [10]
เมื่อสร้างเสร็จ เชื่อมต่อและตรวจสถานะ:
doctl kubernetes cluster kubeconfig save "$CLUSTER_NAME"
kubectl config current-context
kubectl get nodes -o wide
kubectl get nodes -L kubernetes.io/arch
kubectl create namespace "$APP_NAMESPACE"
หาก Node แสดง arm64 ต้อง Build Image สำหรับ linux/arm64 หรือใช้ Image ที่รองรับทั้งสองสถาปัตยกรรมก่อน Deploy
#9. ให้ DOKS ดึง Private Image จาก DOCR
การ Login Docker บนเครื่องของเราไม่ได้ทำให้ Worker Node มีสิทธิ์ดึง Image โดยอัตโนมัติ ต้องให้คลัสเตอร์มี Credentials ของ Registry ด้วย
DigitalOcean มีการเชื่อมต่อผ่าน Control Panel ที่ช่วยกระจาย Secret และตั้งค่า Default Service Account ส่วนเวิร์กช็อปนี้ใช้ Secret เฉพาะ Namespace และอ้างชื่อใน Deployment เพื่อให้เห็นกลไกชัดเจน [1][11]
doctl registries kubernetes-manifest "$REGISTRY_NAME" \
--namespace "$APP_NAMESPACE" \
--name docr-pull-secret \
| kubectl apply -f -
kubectl -n "$APP_NAMESPACE" get secret docr-pull-secret
คำสั่งสร้าง Manifest สำหรับให้ Kubernetes ดึง Image แล้วส่งเข้า kubectl โดยตรง หลีกเลี่ยงการบันทึกไฟล์ที่มี Credentials ลง Git และจัดการอายุ/การหมุนเวียน Credentials ตามนโยบายของระบบ
Secret ที่อ้างผ่าน imagePullSecrets ต้องอยู่ใน Namespace เดียวกับ Pod เสมอ การมี Secret ใน default ไม่ได้ทำให้ Pod ใน container-lab ใช้งานได้ [12]
#10. สร้าง Deployment และ Service
สร้างไฟล์ k8s/app.yaml ด้วยคำสั่งต่อไปนี้ ตัวแปร $IMAGE และ $APP_NAMESPACE จะถูก Bash แทนค่าก่อนเขียนไฟล์ Kubernetes ไม่แทนตัวแปรเหล่านี้เอง:
cat > k8s/app.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
namespace: ${APP_NAMESPACE}
spec:
replicas: 2
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: demo-api
template:
metadata:
labels:
app: demo-api
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 20
imagePullSecrets:
- name: docr-pull-secret
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: ${IMAGE}
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: demo-api
namespace: ${APP_NAMESPACE}
spec:
type: ClusterIP
selector:
app: demo-api
ports:
- name: http
port: 80
targetPort: http
EOF
เวิร์กช็อปเริ่มจาก Service แบบ ClusterIP เพื่อทดสอบแอปก่อนสร้าง Public Load Balancer
| การตั้งค่า | ผลที่ต้องการ |
|---|---|
replicas: 2 |
รันสอง Pod ของแอป |
imagePullSecrets |
ใช้ Credentials ของ DOCR |
requests |
ระบุทรัพยากรที่ใช้ในการจัดตาราง Pod |
limits |
กำหนดเพดาน CPU และ Memory ของ Container |
readinessProbe |
หยุดส่ง Traffic ผ่าน Service ไปยัง Pod ที่ยังไม่พร้อม |
livenessProbe |
ให้ Kubernetes เริ่ม Container ใหม่เมื่อ Probe ล้มเหลวถึงเกณฑ์ |
maxUnavailable: 0 |
ระหว่าง Rolling Update รักษาจำนวน Pod พร้อมใช้งานตามเป้าหมาย |
maxSurge: 1 |
เพิ่ม Pod ชั่วคราวได้หนึ่งตัวระหว่าง Update |
Probe ของตัวอย่างตรวจว่า HTTP Server ตอบได้ แอปจริงอาจต้องแยก Readiness กับ Liveness โดยไม่ทำให้ Liveness ล้มเหลวเพียงเพราะบริการภายนอกขัดข้อง [13][14]
#11. Deploy และตรวจสอบ
kubectl apply -f k8s/app.yaml
kubectl -n "$APP_NAMESPACE" rollout status deployment/demo-api --timeout=180s
kubectl -n "$APP_NAMESPACE" get pods -o wide
kubectl -n "$APP_NAMESPACE" get services
kubectl -n "$APP_NAMESPACE" logs deployment/demo-api --all-pods=true
Pod ควรแสดง Running และ READY เป็น 1/1 หาก Pod เป็น Pending หรือดึง Image ไม่สำเร็จ ให้ใช้ส่วนแก้ปัญหาด้านท้าย
ทดสอบผ่าน Port Forward:
kubectl -n "$APP_NAMESPACE" port-forward service/demo-api 8080:80
เปิด Terminal อีกหน้าต่างแล้วทดสอบ:
curl -fsS http://localhost:8080/
curl -fsS http://localhost:8080/healthz
Port Forward ผูกกับ Pod ที่เลือกหนึ่งตัว จึงใช้ตรวจการเชื่อมต่อได้ แต่ไม่ใช่วิธีพิสูจน์การกระจาย Request ระหว่างทุก Pod กด Ctrl+C เมื่อทดสอบเสร็จ
#12. เปิดบริการผ่าน DigitalOcean Load Balancer
ขั้นตอนนี้เป็นทางเลือกสำหรับทดลองเข้าถึงผ่าน Public IP และมีค่าใช้จ่ายของ Load Balancer เพิ่มจาก Worker Node [2][15]
แก้ type: ClusterIP ในไฟล์ k8s/app.yaml ให้เป็น:
type: LoadBalancer
จากนั้น Apply และติดตามสถานะ:
kubectl apply -f k8s/app.yaml
kubectl -n "$APP_NAMESPACE" get service demo-api --watch
รอ EXTERNAL-IP เปลี่ยนจาก <pending> เป็น IP แล้วกด Ctrl+C กำหนด IP ตามค่าจริงเพื่อทดสอบ:
read -r -p "Load Balancer external IP: " LB_IP
curl -fsS "http://${LB_IP}/"
curl -fsS "http://${LB_IP}/healthz"
เส้นทางการเชื่อมต่อคือ ผู้ใช้ → DigitalOcean Load Balancer → Kubernetes Service → Ready Pod ตัวอย่างเปิด HTTP สำหรับแอปสาธิต เมื่อใช้งานจริงให้กำหนด Domain, TLS และรูปแบบ Ingress/Gateway หรือการตั้งค่า Load Balancer ให้ตรงกับความต้องการ
#13. อัปเดตแอปด้วย Rolling Update
ออก Image ใหม่เป็น v2 โดยเปลี่ยนค่าที่ฝังใน Image ผ่าน Build Argument:
export IMAGE_TAG="v2"
export IMAGE="registry.digitalocean.com/${REGISTRY_NAME}/${APP_NAME}:${IMAGE_TAG}"
doctl registries login "$REGISTRY_NAME" --expiry-seconds 3600
docker buildx build \
--platform linux/amd64 \
--build-arg APP_VERSION="$IMAGE_TAG" \
--tag "$IMAGE" \
--push .
แก้ค่า image ใน k8s/app.yaml ให้ตรงกับค่าใหม่ ก่อน Apply:
image: registry.digitalocean.com/your-unique-registry/demo-api:v2
ใช้ชื่อ Registry จริง จากนั้น:
kubectl apply -f k8s/app.yaml
kubectl -n "$APP_NAMESPACE" rollout status deployment/demo-api --timeout=180s
kubectl -n "$APP_NAMESPACE" rollout history deployment/demo-api
เมื่อ Rollout เสร็จ / ควรแสดง "version":"v2" หากใช้ Port Forward ให้เริ่มคำสั่งใหม่หลัง Update เพื่อเชื่อมต่อกับ Pod ใหม่
Deployment จะค่อย ๆ เพิ่ม Pod รุ่นใหม่และลด Pod รุ่นเดิมตาม Strategy ที่กำหนด โดยต้องมีทรัพยากรสำหรับ Pod ชั่วคราวและมี Readiness ที่ตรวจสถานะได้ถูกต้อง การตั้ง Rolling Update อย่างเดียวไม่ได้รับประกันว่า Request ทุกตัวจะสำเร็จ [13]
#Rollback
หากรุ่นใหม่มีปัญหา:
kubectl -n "$APP_NAMESPACE" rollout undo deployment/demo-api
kubectl -n "$APP_NAMESPACE" rollout status deployment/demo-api --timeout=180s
แก้ image ใน k8s/app.yaml กลับเป็น v1 ด้วย เพื่อให้ไฟล์ตรงกับสถานะที่ต้องการ การ Apply ไฟล์ที่ยังอ้าง v2 จะนำรุ่นที่มีปัญหากลับมาอีก และควรเก็บ Image รุ่นก่อนใน DOCR ให้พร้อมสำหรับ Rollback
#14. ขยายจำนวน Pod
แก้ replicas: 2 ใน k8s/app.yaml เป็น replicas: 3 แล้ว Apply:
kubectl apply -f k8s/app.yaml
kubectl -n "$APP_NAMESPACE" rollout status deployment/demo-api --timeout=180s
kubectl -n "$APP_NAMESPACE" get pods -o wide
การเพิ่ม Pod ไม่ได้เพิ่ม Worker Node โดยอัตโนมัติ หากทรัพยากรของ Node ไม่พอ Pod ใหม่อาจเป็น Pending สำหรับระบบจริงสามารถพิจารณา:
- Horizontal Pod Autoscaler (HPA): ปรับจำนวน Pod ตาม Metric และเงื่อนไขที่กำหนด
- Cluster Autoscaler: ปรับจำนวน Worker Node ของ Node Pool ตามการจัดตาราง Pod
- การกระจาย Pod ข้าม Node เช่น Topology Spread Constraints หรือ Pod Anti-Affinity
เวิร์กช็อปนี้ใช้ Manual Scaling และไม่ได้เปิด Autoscaler ทั้งสองชนิด หลาย Pod บน Node เดียวช่วยศึกษาการจัดการ Replica แต่ยังสูญเสียบริการได้เมื่อ Node นั้นหยุดทำงาน
#15. ต่อเติมเป็น CI/CD
เมื่อทำเวิร์กช็อปได้แล้ว สามารถนำลำดับเดียวกันไปใช้ใน GitHub Actions, GitLab CI หรือ Jenkins:
- Checkout โค้ดและตรวจสอบคุณภาพ
- รัน Unit Test และ API Test
- Build Image ตามสถาปัตยกรรมปลายทาง
- สแกน Image และเผยแพร่เข้า DOCR ด้วย Tag ที่ผูกกับ Commit
- อัปเดต Manifest หรือค่าที่ระบบ GitOps ใช้
- รอ Rollout และรัน Smoke Test
เลือกวิธี Deploy ให้ชัดเจน หาก Pipeline ใช้ kubectl ต้องมีสิทธิ์เข้าคลัสเตอร์ที่เหมาะสม หากใช้ GitOps เช่น Argo CD ให้ Pipeline อัปเดต Manifest ใน Git แล้วให้ Controller ทำการ Reconcile
แยก Credentials สำหรับ Push Image ออกจาก Credentials สำหรับ Pull Image และสิทธิ์ Deploy เท่าที่ระบบรองรับ เก็บข้อมูลลับใน Secret Store ของระบบ CI/CD
#16. ปัญหาที่พบบ่อย
| อาการ | สิ่งที่ควรตรวจ | แนวทางแก้ |
|---|---|---|
unknown command "registries" |
รุ่นของ doctl |
อัปเดต doctl และตรวจ Help ของคำสั่งใหม่ |
| สร้าง Registry ไม่ได้ | ชื่อซ้ำ, สิทธิ์, จำนวน Registry ตามแผน | เปลี่ยนชื่อหรือใช้ Registry ที่มีอยู่ |
Push ได้ unauthorized |
สิทธิ์และอายุ Credentials บนเครื่อง | Login ใหม่โดยระบุชื่อ Registry และอย่าใช้ Read-only Credentials สำหรับ Push |
ImagePullBackOff |
Image Path, Tag, Secret, สิทธิ์, การเชื่อมต่อ | ตรวจว่า Image มีจริงและ Secret อยู่ใน Namespace ของ Pod |
exec format error |
สถาปัตยกรรมของ Image และ Node | Build สำหรับ linux/amd64 หรือ linux/arm64 ให้ตรงกัน |
CrashLoopBackOff |
Log, คำสั่งเริ่มแอป, Probe | อ่าน Log ของ Container ปัจจุบันและก่อนหน้า |
Pod Pending |
CPU/Memory ที่ร้องขอ, Taint, Node Capacity | อ่าน Events และเพิ่มทรัพยากรตามสาเหตุ |
| Service ติดต่อ Pod ไม่ได้ | Selector, Labels, Readiness, Port | ให้ Selector ตรงกับ Pod และตรวจ EndpointSlice |
EXTERNAL-IP เป็น <pending> นาน |
การสร้าง Load Balancer, Quota, Region, Events | อ่าน Events ของ Service และสถานะใน Control Panel |
| Push Tag เดิมแล้วแอปยังเป็นรุ่นเก่า | Deployment และ Image Cache | ออก Tag ใหม่แล้วอัปเดต Deployment หรืออ้าง Digest |
คำสั่งวิเคราะห์:
kubectl -n "$APP_NAMESPACE" get pods -o wide
kubectl -n "$APP_NAMESPACE" get events --sort-by=.metadata.creationTimestamp
kubectl -n "$APP_NAMESPACE" describe deployment demo-api
kubectl -n "$APP_NAMESPACE" describe service demo-api
kubectl -n "$APP_NAMESPACE" get endpointslices \
-l kubernetes.io/service-name=demo-api
สำหรับ Pod ที่มีปัญหา ใส่ชื่อจริงแทน <pod-name>:
kubectl -n "$APP_NAMESPACE" describe pod <pod-name>
kubectl -n "$APP_NAMESPACE" logs <pod-name> -c api
kubectl -n "$APP_NAMESPACE" logs <pod-name> -c api --previous
ใช้ --previous เมื่อ Container เคย Restart แล้วเท่านั้น สำหรับแอปจริง Log อาจมีข้อมูลลับ ควรตรวจและปกปิดข้อมูลก่อนเผยแพร่
#17. ค่าใช้จ่ายและการเก็บ Image
DOCR มีแผน Starter, Basic และ Professional โดยต่างกันด้านจำนวน Registry, Repository และพื้นที่เก็บ Image ตรวจราคาและข้อจำกัดปัจจุบันก่อนเลือกแผน [8]
| ทรัพยากร | สิ่งที่ทำให้เกิดค่าใช้จ่าย |
|---|---|
| DOCR | แผนที่เลือกและพื้นที่เก็บที่ใช้ |
| DOKS Worker Node | ขนาด, จำนวนและระยะเวลาของ Node ที่รัน |
| HA Control Plane | การเปิด HA ตามเงื่อนไขราคาปัจจุบัน |
| Load Balancer | การเปิด Service ที่สร้าง Cloud Load Balancer |
| Storage | Volumes, Snapshot หรือบริการเก็บข้อมูลที่สร้างเพิ่ม |
| เครือข่าย | การรับส่งข้อมูลตามโควตาและเงื่อนไขของบริการ |
การลบ Deployment ไม่ได้หยุดค่าใช้จ่ายของ Worker Node และการลบ Tag อาจยังเหลือ Image Layer ที่ไม่ถูกอ้างอิง ซึ่งยังใช้พื้นที่ Registry ควรวางแผนเก็บ Image ที่ใช้ Production และ Rollback แล้วทำ Cleanup/Garbage Collection ตามวิธีที่บริการรองรับ [2][8]
#18. ล้างทรัพยากรหลังเวิร์กช็อป
ทำส่วนนี้เมื่อจบการทดลองและแน่ใจว่าทรัพยากรเป็นของเวิร์กช็อปเท่านั้น ตรวจ Context และชื่อก่อนลบ:
kubectl config current-context
doctl kubernetes cluster list
doctl registries list
ลบ Service ก่อนเพื่อให้ Kubernetes ส่งคำขอลบ Load Balancer ที่เกี่ยวข้อง จากนั้นตรวจใน Control Panel ว่า Load Balancer ถูกลบแล้วก่อนลบคลัสเตอร์:
kubectl -n "$APP_NAMESPACE" delete service demo-api
kubectl delete namespace "$APP_NAMESPACE"
ลบคลัสเตอร์ที่สร้างเพื่อเวิร์กช็อป:
doctl kubernetes cluster delete "$CLUSTER_NAME"
คำสั่งลบคลัสเตอร์ตามปกติลบ Worker Node แต่ไม่รับประกันการลบทรัพยากรอื่นทั้งหมด เช่น Load Balancer, Volume หรือ Snapshot จึงต้องตรวจทรัพยากรที่เหลือด้วย [16]
หาก Registry นี้สร้างสำหรับเวิร์กช็อปโดยเฉพาะและไม่ต้องเก็บ Image แล้ว:
doctl registries delete "$REGISTRY_NAME"
คำสั่งนี้ลบ Registry อย่างถาวร อย่าใช้กับ Registry ที่แชร์กับโครงการอื่น [17]
#สิ่งที่ได้จากเวิร์กช็อป
เราสร้าง API และ Container Image ด้วย Docker, เผยแพร่ Image ส่วนตัวเข้า DOCR, ตั้งสิทธิ์ให้ DOKS ดึง Image, Deploy แอปพร้อม Health Checks และ Service รวมถึงทดลอง Rolling Update, Rollback และขยายจำนวน Pod
เมื่อนำไปใช้จริง ให้ต่อเติมการออกเวอร์ชันด้วย Commit SHA หรือ Digest, การทดสอบและสแกน Image, TLS, การกระจาย Pod ข้าม Node, การติดตาม Metric/Log และการสำรองข้อมูลตามความต้องการของระบบ
#เอกสารอ้างอิง
ตรวจสอบจากเอกสารทางการ ณ วันที่จัดทำ ตัวเลือก Region, Version, แผนและราคาสามารถเปลี่ยนแปลงได้
- DigitalOcean: Use Your Private Container Registry with Docker and Kubernetes
- DigitalOcean: Kubernetes Pricing และขอบเขตบริการ
- Kubernetes: Dockershim Removal FAQ
- DigitalOcean CLI: doctl registries create
- DigitalOcean CLI: doctl registries login
- DigitalOcean: Install and Configure doctl
- Docker Hub: Official Node Image
- DigitalOcean: Container Registry Pricing
- Docker: Multi-platform Builds
- DigitalOcean CLI: doctl kubernetes cluster create
- DigitalOcean CLI: doctl registries kubernetes-manifest
- Kubernetes: Images และ imagePullSecrets
- Kubernetes: Deployments
- Kubernetes: Liveness, Readiness and Startup Probes
- DigitalOcean: Add Load Balancers to Kubernetes Clusters
- DigitalOcean CLI: doctl kubernetes cluster delete
- DigitalOcean CLI: doctl registries delete