🌱 ระดับ: Beginner

Part 1: GitLab CI/CD Fundamentals

เรียนรู้พื้นฐาน CI/CD กับ GitLab ตั้งแต่แนวคิดไปจนถึงการสร้าง Pipeline แรกของคุณ

📖 25 นาที 📅 อัปเดต: กุมภาพันธ์ 2026 🏷️ DevOps, GitLab, CI/CD

1. บทนำ - CI/CD คืออะไร?

🔄 CI/CD คืออะไร?

CI/CD ย่อมาจาก Continuous Integration และ Continuous Deployment (หรือ Continuous Delivery) เป็นแนวปฏิบัติในการพัฒนาซอฟต์แวร์ที่ช่วยให้ทีมสามารถส่งมอบโค้ดได้อย่างรวดเร็ว แม่นยำ และมีคุณภาพ

📌 นิยามแบบเข้าใจง่าย

Continuous Integration (CI) = การรวมโค้ดจากนักพัฒนาหลายคนเข้าด้วยกันอัตโนมัติ พร้อมตรวจสอบความถูกต้องทุกครั้งที่มีการ push

Continuous Deployment (CD) = การปล่อยซอฟต์แวร์สู่ผู้ใช้งานอัตโนมัติ โดยไม่ต้องมีการแทรกแซงจากมนุษย์

❓ ทำไมต้องใช้ CI/CD?

ก่อนจะมี CI/CD การพัฒนาซอฟต์แวร์เผชิญปัญหาหลายอย่าง:

CI/CD ช่วยแก้ปัญหาเหล่านี้ได้อย่างไร?

⚡ รวดเร็ว

Automate ทุกขั้นตอน ลดเวลาจากวันเป็นชั่วโมง จากชั่วโมงเป็นนาที

🎯 แม่นยำ

ระบบทดสอบอัตโนมัติ ลด Human Error ตรวจจับบั๊กได้เร็ว

🔄 Consistent

สภาพแวดล้อมเหมือนกันทุกครั้ง ไม่มีเรื่อง "ในเครื่องฉันรันได้"

📊 มองเห็น

ติดตามสถานะได้ตลอด รู้ปัญหาทันที มี Log ครบถ้วน

⚔️ GitLab CI/CD vs Jenkins vs GitHub Actions

เมื่อพูดถึง CI/CD Tools มีตัวเลือกหลายตัว มาดูการเปรียบเทียบกัน:

คุณสมบัติ GitLab CI/CD Jenkins GitHub Actions
การตั้งค่า ง่าย (YAML) ซับซ้อน ง่าย (YAML)
Built-in Container Registry มี ต้องติดตั้งเพิ่ม ใช้ของ third-party
Free Tier 400 นาที/เดือน ฟรีทั้งหมด 2,000 นาที/เดือน
Self-hosted รองรับ รองรับ จำกัด
Kubernetes Integration ดีเยี่ยม ดี ดี
Security Scanning Built-in ต้องติดตั้ง Plugin มีบางส่วน
All-in-One Platform ใช่ เฉพาะ CI/CD เฉพาะ CI/CD

💡 ทำไมเราเลือก GitLab CI/CD?

GitLab เป็นแพลตฟอร์ม All-in-One ที่มีทุกอย่างในที่เดียว: Git Repository, Issue Tracking, CI/CD, Container Registry, Security Scanning และอีกมากมาย ทำให้ไม่ต้องจัดการหลาย Tools และการ Integration ซับซ้อน

2. Architecture Overview

🔄 CI/CD Pipeline Flow

มาดูภาพรวมของการทำงาน CI/CD Pipeline ตั้งแต่เขียนโค้ดจนถึงการ Deploy:

📝 Code เขียนโค้ด 💾 Commit ส่งโค้ด 🔨 Build สร้าง Artifact 🧪 Test ทดสอบ 🚀 Deploy ปล่อยใช้งาน 🌐 Production ผู้ใช้งานจริง CI (Continuous Integration) CD (Deploy)

รูปที่ 1: ขั้นตอนการทำงานของ CI/CD Pipeline

🏗️ GitLab Runner Architecture

GitLab CI/CD ใช้สถาปัตยกรรมแบบ GitLab Server + GitLab Runner โดย Runner จะทำหน้าที่ execute jobs:

🦊 GitLab Server 📁 Git Repository ⚙️ Pipeline Scheduler 👨‍💻 Developer git push push code 🏃 GitLab Runner Executors: 🐳 Docker ☸️ Kubernetes 🐚 Shell 🖥️ VirtualBox Job Execution build → test → deploy assign job report status 🖥️ Staging Server 🌐 Production Server

รูปที่ 2: สถาปัตยกรรม GitLab CI/CD และ GitLab Runner

🔔 จุดสำคัญที่ต้องจำ

  • GitLab Server เก็บโค้ดและจัดการ Pipeline แต่ ไม่ได้รัน jobs เอง
  • GitLab Runner เป็นตัวที่รัน jobs จริง สามารถอยู่คนละเครื่องกับ Server
  • Runner รองรับหลาย Executor (Docker, Kubernetes, Shell, ฯลฯ)

3. พื้นฐาน .gitlab-ci.yml

ไฟล์ .gitlab-ci.yml เป็นหัวใจของ GitLab CI/CD ทุกอย่างเริ่มต้นที่ไฟล์นี้ โดยจะต้องวางไว้ที่ root ของ repository

📁 โครงสร้างไฟล์

โครงสร้างพื้นฐานของไฟล์ .gitlab-ci.yml ประกอบด้วย:

# 1. กำหนด Stages (ลำดับการทำงาน)
stages:
  - build
  - test
  - deploy

# 2. กำหนด Variables ทั่วไป
variables:
  APP_NAME: "my-app"
  DOCKER_IMAGE: "node:18"

# 3. กำหนด Jobs (งานที่ต้องทำ)
build_job:
  stage: build
  image: $DOCKER_IMAGE
  script:
    - echo "Building $APP_NAME..."
    - npm install
    - npm run build

test_job:
  stage: test
  image: $DOCKER_IMAGE
  script:
    - npm test

deploy_job:
  stage: deploy
  script:
    - echo "Deploying to production..."
  only:
    - main

🏷️ Keywords สำคัญที่ต้องรู้

stages

กำหนดลำดับการทำงานของ Pipeline (build → test → deploy)

stage

ระบุว่า Job นี้อยู่ใน Stage ไหน

script

คำสั่งที่จะรัน (จำเป็นต้องมีในทุก Job)

image

Docker Image ที่จะใช้รัน Job

variables

ตัวแปรที่ใช้ใน Pipeline

only / except

กำหนดเงื่อนไขการรัน (branch, tags, etc.)

before_script

คำสั่งที่รันก่อน script หลัก

after_script

คำสั่งที่รันหลัง script หลัก (แม้ job จะ fail)

📋 Keywords เพิ่มเติม

Keyword คำอธิบาย ตัวอย่าง
tags ระบุ Runner ที่จะรัน Job tags: [docker, linux]
allow_failure อนุญาตให้ Job fail ได้โดยไม่หยุด Pipeline allow_failure: true
when กำหนดเงื่อนไขการรัน when: manual
artifacts ไฟล์ที่เก็บไว้ให้ Job ถัดไปใช้ artifacts: paths: [dist/]
dependencies ระบุ Job ที่ต้องรอให้เสร็จก่อน dependencies: [build]
cache เก็บ cache ระหว่าง Pipeline cache: paths: [node_modules/]
extends สืบทอด configuration จาก Job อื่น extends: .base_job
rules กำหนดเงื่อนไขการรันแบบซับซ้อน (แทน only/except) rules: - if: $CI_COMMIT_BRANCH == "main"

⚠️ ข้อควรระวัง: only/except vs rules

only และ except เป็น syntax เก่าที่ GitLab แนะนำให้เปลี่ยนมาใช้ rules แทน เนื่องจากมีความยืดหยุ่นมากกว่าและจะได้รับการพัฒนาต่อไป

4. Jobs และ Stages

📦 Stage คืออะไร?

Stage เป็นการจัดกลุ่ม Jobs เข้าด้วยกันตามลำดับการทำงาน Jobs ใน Stage เดียวกันจะรัน พร้อมกัน (parallel) ส่วน Stage ถัดไปจะรันหลังจาก Stage ก่อนหน้าเสร็จสมบูรณ์

Stage 1: Build build_web build_api Stage 2: Test unit_test e2e_test lint Stage 3: Deploy staging production Jobs ใน Stage เดียวกันรันพร้อมกัน | Stage ถัดไปรอ Stage ก่อนหน้าเสร็จ

รูปที่ 3: การทำงานของ Jobs และ Stages - Jobs ใน Stage เดียวกันรันแบบ Parallel

🔨 การสร้าง Job

Job คือหน่วยย่อยที่สุดของงาน แต่ละ Job จะ:

# ตัวอย่าง Job ง่ายๆ
hello_world:
  stage: build
  script:
    - echo "Hello, GitLab CI/CD!"
    - echo "This job runs in the build stage"

# Job ที่ซับซ้อนขึ้น
unit_tests:
  stage: test
  image: node:18-alpine
  before_script:
    - npm ci                    # ติดตั้ง dependencies ก่อน
  script:
    - npm run test:unit          # รัน unit tests
    - npm run test:coverage      # สร้าง coverage report
  after_script:
    - echo "Test completed with status: $CI_JOB_STATUS"
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

🔗 Job Dependencies

บางครั้ง Job หนึ่งต้องรอผลจาก Job อื่น เราสามารถกำหนด dependencies ได้:

stages:
  - build
  - test
  - deploy

# Job สร้าง artifact
build_app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

# Job ทดสอบต้องใช้ artifact จาก build_app
test_app:
  stage: test
  dependencies:
    - build_app        # ดาวน์โหลด artifact จาก build_app
  script:
    - ls dist/           # จะเห็นไฟล์จาก build_app
    - npm run test

# Deploy ต้องรอทั้ง build และ test
deploy_staging:
  stage: deploy
  dependencies:
    - build_app
    - test_app
  script:
    - rsync -avz dist/ user@staging:/var/www/
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

💡 Tips: needs vs dependencies

needs ให้ Job รันทันทีที่ Job ที่กำหนดเสร็จ โดยไม่ต้องรอ Stage อื่น ทำให้ Pipeline เร็วขึ้น:

deploy_preview:
  stage: deploy
  needs: [build_app]    # ไม่ต้องรอ test stage
  script:
    - echo "Deploy preview environment"

5. GitLab Runner

🏃 Runner คืออะไร?

GitLab Runner เป็นแอปพลิเคชันที่ทำหน้าที่รัน jobs ที่ส่งมาจาก GitLab CI/CD โดย Runner สามารถติดตั้งบนเซิร์ฟเวอร์, VM, container, หรือแม้แต่เครื่องคอมพิวเตอร์ส่วนตัว

🏢 Shared Runner vs Specific Runner

ประเภท คำอธิบาย ข้อดี ข้อเสีย
Shared Runners Runner ที่ GitLab จัดให้ (สำหรับ GitLab.com) หรือ Admin ตั้งค่าไว้ (Self-managed) ✓ ไม่ต้องดูแล
✓ พร้อมใช้ทันที
✓ Auto-scaling
✗ มีค่าใช้จ่าย (minutes)
✗ แบ่งกับโปรเจคอื่น
✗ จำกัดการปรับแต่ง
Specific Runners Runner ที่ติดตั้งเองและผูกกับโปรเจคเฉพาะ ✓ ควบคุมเต็มที่
✓ ปรับแต่งได้
✓ ไม่จำกัดนาที
✓ ความปลอดภัยสูง
✗ ต้องดูแลเอง
✗ ต้องติดตั้งเอง
✗ ต้องจัดการทรัพยากร
Group Runners Runner ที่ผูกกับ Group และใช้ได้กับทุกโปรเจคใน Group ✓ แชร์ระหว่างโปรเจค
✓ จัดการจากที่เดียว
✗ ทุกโปรเจคใช้ทรัพยากรร่วมกัน

📥 การติดตั้ง GitLab Runner

ติดตั้งบน Linux (Ubuntu/Debian)

# เพิ่ม GitLab repository
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash

# ติดตั้ง GitLab Runner
sudo apt-get install gitlab-runner

# ตรวจสอบการติดตั้ง
sudo gitlab-runner --version

ติดตั้งบน macOS

# ใช้ Homebrew
brew install gitlab-runner

# ติดตั้งเป็น service
brew services start gitlab-runner

ติดตั้งด้วย Docker

# รัน GitLab Runner container
docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

# ลงทะเบียน Runner
docker exec -it gitlab-runner gitlab-runner register

📝 การลงทะเบียน Runner

หลังติดตั้ง ต้องลงทะเบียน Runner กับ GitLab:

ดึง Registration Token

ไปที่ Settings → CI/CD → Runners ในโปรเจคของคุณ แล้ว copy token

รันคำสั่ง register

sudo gitlab-runner register

# จะมีคำถามดังนี้:
# Enter the GitLab instance URL:
https://gitlab.com

# Enter the registration token:
[paste your token here]

# Enter a description for the runner:
My Docker Runner

# Enter tags for the runner (comma-separated):
docker,linux,production

# Enter optional maintenance note:
Runner สำหรับ production deployment

# Enter an executor:
docker

# Enter the default Docker image:
node:18-alpine

ตรวจสอบการลงทะเบียน

# ดูรายการ Runners
sudo gitlab-runner list

# ตรวจสอบสถานะ
sudo gitlab-runner verify

# ดู config
cat /etc/gitlab-runner/config.toml

📌 Executors ที่ควรรู้จัก

  • docker - รันใน Docker container (แนะนำ)
  • kubernetes - รันใน Kubernetes pod
  • shell - รัน command ตรงๆ บน host
  • ssh - รันบนเครื่อง remote ผ่าน SSH
  • virtualbox/parallels - รันใน VM

6. ตัวอย่าง Pipeline แรก

👋 Hello World Pipeline

มาเริ่มต้นด้วย Pipeline ง่ายที่สุด:

# .gitlab-ci.yml - Hello World
stages:
  - hello

hello_job:
  stage: hello
  script:
    - echo "🎉 Hello, GitLab CI/CD!"
    - echo "📝 This is my first pipeline"
    - echo "📅 Date: $(date)"
    - echo "🌿 Branch: $CI_COMMIT_REF_NAME"

🔄 Build → Test → Deploy (Basic)

Pipeline ที่สมบูรณ์สำหรับ Node.js application:

# .gitlab-ci.yml - Basic Node.js Pipeline

# กำหนด stages
stages:
  - build
  - test
  - deploy

# Global variables
variables:
  NODE_VERSION: "18"
  NPM_CONFIG_CACHE: ".npm"

# Cache node_modules ระหว่าง pipelines
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .npm/
    - node_modules/

# ============================================
# BUILD STAGE
# ============================================
install_dependencies:
  stage: build
  image: node:${NODE_VERSION}-alpine
  script:
    - echo "📦 Installing dependencies..."
    - npm ci --cache .npm
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

build_app:
  stage: build
  image: node:${NODE_VERSION}-alpine
  needs: [install_dependencies]
  script:
    - echo "🔨 Building application..."
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

# ============================================
# TEST STAGE
# ============================================
lint:
  stage: test
  image: node:${NODE_VERSION}-alpine
  needs: [install_dependencies]
  script:
    - echo "🔍 Running linter..."
    - npm run lint
  allow_failure: true

unit_test:
  stage: test
  image: node:${NODE_VERSION}-alpine
  needs: [install_dependencies]
  script:
    - echo "🧪 Running unit tests..."
    - npm run test:unit -- --coverage
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

integration_test:
  stage: test
  image: node:${NODE_VERSION}-alpine
  needs: [install_dependencies, build_app]
  script:
    - echo "🔗 Running integration tests..."
    - npm run test:integration
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_MERGE_REQUEST_IID

# ============================================
# DEPLOY STAGE
# ============================================
deploy_staging:
  stage: deploy
  image: alpine:latest
  needs: [build_app, unit_test]
  script:
    - echo "🚀 Deploying to staging..."
    - echo "Staging URL: https://staging.example.com"
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy_production:
  stage: deploy
  image: alpine:latest
  needs: [build_app, unit_test, integration_test]
  script:
    - echo "🌟 Deploying to production..."
    - echo "Production URL: https://example.com"
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  when: manual        # ต้องกดปุ่ม manual เพื่อ deploy

🖥️ การดูผลลัพธ์ใน GitLab UI

หลังจาก push โค้ด สามารถดู Pipeline ได้ที่:

เข้าหน้า CI/CD → Pipelines

ไปที่โปรเจค → CI/CD → Pipelines จะเห็นรายการ Pipeline ทั้งหมด

คลิกดู Pipeline Details

จะเห็นกราฟแสดง Stages และ Jobs พร้อมสถานะ (running, success, failed)

คลิก Job เพื่อดู Log

จะเห็น output แบบ real-time ขณะรัน และสามารถดู log ย้อนหลังได้

7. การ Debug Pipeline

📋 การดู Logs

Logs เป็นสิ่งสำคัญที่สุดในการ debug:

💡 Debug Tips

เพิ่ม CI_DEBUG_TRACE: "true" ใน variables เพื่อแสดง debug info ละเอียด:

variables:
  CI_DEBUG_TRACE: "true"

❌ Common Errors

Error: Job failed (exit code: 1)

สาเหตุ: คำสั่งใน script return exit code ไม่ใช่ 0

แก้ไข: ตรวจสอบ log ว่าคำสั่งไหน fail และเพิ่ม error handling

script:
  - npm run build || echo "Build failed but continuing..."
  - npm run test 2>&1 | tee test-output.log

Error: No runner available

สาเหตุ: ไม่มี Runner ที่พร้อมรับงาน หรือ tags ไม่ตรง

แก้ไข: ตรวจสอบ Runner status และ tags

# ตรวจสอบว่า Job ต้องการ tags อะไร
job_name:
  tags:
    - docker
    - linux

Error: Image not found

สาเหตุ: Docker image ไม่มีหรือไม่สามารถดาวน์โหลดได้

แก้ไข: ใช้ image ที่มีอยู่จริง หรือตรวจสอบ private registry credentials

job_name:
  image: node:18-alpine    # ใช้ image ที่มีอยู่ใน Docker Hub
  # หรือ
  image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA  # ใช้ image จาก GitLab Registry

Error: YAML syntax error

สาเหตุ: ไฟล์ .gitlab-ci.yml มี syntax ผิดพลาด

แก้ไข: ใช้ GitLab CI Lint tool หรือตรวจสอบ indentation

# ❌ ผิด: ใช้ tab แทน space
job:
script:
    - echo "hello"

# ✅ ถูก: ใช้ space 2 ตัว
job:
  script:
    - echo "hello"

Error: Out of memory / Disk space

สาเหตุ: Runner มีทรัพยากรไม่เพียงพอ

แก้ไข: เพิ่มทรัพยากรหรือ optimize job

job_name:
  tags:
    - high-memory    # ใช้ runner ที่มี memory มาก
  variables:
    NODE_OPTIONS: "--max-old-space-size=4096"
  script:
    - npm run build
  after_script:
    - rm -rf node_modules/  # cleanup หลังรัน

🔄 การ Retry Job

เมื่อ Job fail สามารถ retry ได้หลายวิธี:

job_name:
  script:
    - npm run flaky-test
  retry:
    max: 2              # retry สูงสุด 2 ครั้ง
    when:
      - script_failure   # retry เฉพาะเมื่อ script fail
      - runner_system_failure

⏹️ การ Cancel Pipeline

หากต้องการหยุด Pipeline ที่กำลังรัน:

# กำหนดให้ cancel job ใหม่ถ้ามี job เดิมยังรันอยู่
job_name:
  script:
    - npm run deploy
  interruptible: true    # อนุญาตให้ interrupt

8. Tips for Beginners

💡 เคล็ดลับการเขียน .gitlab-ci.yml

🎯 ใช้ Lint

ตรวจสอบ syntax ด้วย GitLab CI Lint ก่อน push: CI/CD → Editor → "Validate"

📦 ใช้ Cache

Cache node_modules หรือ dependencies อื่นๆ เพื่อลดเวลา build

🎨 ใช้ extends

สร้าง hidden job (.template) และ extends ซ้ำเพื่อลดโค้ดซ้ำ

⚡ ใช้ needs

ใช้ needs เพื่อให้ job รันได้เร็วขึ้น ไม่ต้องรอ stage ก่อนหน้า

📝 Best Practices

# 1. ใช้ variables เก็บค่าที่ใช้ซ้ำ
variables:
  DOCKER_IMAGE: "node:18-alpine"
  BUILD_DIR: "dist"

# 2. สร้าง reusable templates
.node_template:
  image: $DOCKER_IMAGE
  before_script:
    - npm ci --cache .npm
  cache:
    paths:
      - .npm/
      - node_modules/

# 3. extends จาก template
test_job:
  extends: .node_template
  stage: test
  script:
    - npm test

# 4. กำหนด rules ให้ชัดเจน
deploy_production:
  stage: deploy
  script:
    - ./deploy.sh production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
    - if: when: manual

# 5. ใช้ artifacts เก็บผลลัพธ์
build_job:
  script:
    - npm run build
  artifacts:
    paths:
      - $BUILD_DIR/
    expire_in: 1 week
    when: always       # เก็บแม้ job จะ fail

# 6. กำหนด failure handling
integration_test:
  script:
    - npm run test:integration
  allow_failure: true    # ไม่หยุด pipeline ถ้า fail
  retry:
    max: 2

⚠️ ข้อผิดพลาดที่พบบ่อย

ปัญหา สาเหตุ วิธีแก้
Pipeline รันทุก commit แม้ไม่จำเป็น ไม่ได้กำหนด rules หรือ only/except ใช้ rules เพื่อกำหนดเงื่อนไขการรัน
Build ช้ามาก ติดตั้ง dependencies ใหม่ทุกครั้ง ใช้ cache และ npm ci แทน npm install
Secrets รั่วไหล Hardcode secrets ในไฟล์ ใช้ CI/CD Variables แบบ Masked
Pipeline fail บน main แต่ผ่านบน branch Environment ต่างกัน ใช้ Docker image เดียวกัน และกำหนด env เหมือนกัน
Deploy fail แต่ไม่รู้จะ rollback ยังไง ไม่มี rollback plan ใช้ GitLab Environments และ Rollback button

✅ Checklist ก่อน Push

  • ✓ ตรวจสอบ syntax ด้วย CI Lint
  • ✓ ทดสอบ script ในเครื่องก่อน
  • ✓ ไม่ hardcode secrets
  • ✓ มี proper error handling
  • ✓ กำหนด artifacts สำหรับ debug
  • ✓ ใช้ cache เพื่อ speed up
  • ✓ เขียน comment อธิบายสิ่งที่ทำ

📚 Resources สำหรับเรียนรู้เพิ่มเติม

🎯 สรุป

ในบทความนี้เราได้เรียนรู้:

📖 Next: Part 2 - Advanced GitLab CI/CD

ใน Part 2 เราจะเจาะลึกหัวข้อขั้นสูง: Multi-project Pipelines, Parent-Child Pipelines, Security Scanning, และ GitLab Pages

📚 Series Navigation

ซีรีส์ GitLab CI/CD ทั้ง 5 Parts คลิกเพื่ออ่านบทความอื่นๆ

🔗 Navigate