Module 05 Scripting & Automation 5 labs

Bash, PowerShell và Python Automation

Thành thạo ba ngôn ngữ scripting cốt lõi của DevOps: viết Bash script cho Linux, PowerShell cho Windows/Cloud, Python cho API automation — cùng nguyên lý idempotency và tích hợp script vào CI/CD pipeline.

Công cụ thực hành Bash, PowerShell 7+, Python 3.11+, VS Code, Git
Nền tảng Linux Terminal, WSL2, PowerShell, Windows Terminal
Thời điểm phát hành 23/05/2026
Ngày biên soạn 23/05/2026
Người biên soạn Trần Văn Hòa — Microsoft Certified Trainer (MCT)

Mục tiêu học tập

1. Lý thuyết cốt lõi

1.1. Vì sao DevOps Engineer cần scripting?

Scripting là bộ keo dán nối các công cụ trong pipeline DevOps. Automation thay thế toàn bộ thao tác thủ công lặp đi lặp lại — cài đặt môi trường, backup, kiểm tra health service, provision user — giúp tăng Deployment Frequency (DORA) và giảm human error. Ba ngôn ngữ phổ biến nhất trong môi trường enterprise:

  • Bash — mặc định trên mọi Linux/macOS; gắn chặt với hệ thống file, process, pipe. Chuẩn cho CI runner, Docker entrypoint, cron job.
  • PowerShell 7+ — cross-platform (Windows/Linux/macOS), object pipeline thay vì text stream, tích hợp sâu với Azure, Active Directory, Windows API. Ưu tiên cho môi trường hybrid.
  • Python — thư viện phong phú (requests, boto3, paramiko), dễ test, phù hợp cho orchestration script phức tạp và API automation.

1.2. Bash — Best Practices

Mỗi script production-grade phải bắt đầu bằng safety flags:

#!/usr/bin/env bash
set -euo pipefail   # -e: exit on error, -u: unset var = error, -o pipefail: pipe error propagation
IFS=$'\n\t'         # Tránh word splitting bất ngờ

Các pattern quan trọng: trap để cleanup khi exit, logging function với timestamp, positional parameters với default value (${1:-default}), kiểm tra dependency (command -v tool), heredoc cho multiline string.

1.3. PowerShell 7 — Object Pipeline

Điểm khác biệt cốt lõi: PowerShell truyền object qua pipeline, không phải text. Get-Process | Where-Object CPU -gt 50 | Select-Object Name, CPU — mỗi bước nhận object .NET, không cần parse text. Export-Csv, ConvertTo-Json, Invoke-RestMethod là các cmdlet thiết yếu cho DevOps. Script nên dùng CmdletBinding()param() để hỗ trợ -WhatIf, -Verbose.

1.4. Python cho DevOps Automation

Python phù hợp khi logic phức tạp hơn shell: xử lý JSON nested, retry với backoff, concurrent API calls. Thư viện cốt lõi: requests (HTTP), subprocess (gọi CLI), pathlib (file system), logging (có level và formatter), argparse (CLI args). Luôn dùng virtual environment (python -m venv .venv) và requirements.txt.

1.5. Nguyên lý Idempotency

Idempotent script: chạy 1 lần hay 100 lần đều cho kết quả như nhau, không gây side-effect bổ sung. Pattern thực hiện: check-then-act — kiểm tra trạng thái trước khi thực hiện thay đổi. Ví dụ: mkdir -p (không lỗi nếu đã tồn tại), id user && echo "exists" || useradd user (tạo user chỉ khi chưa có), apt-get install -y (idempotent nếu đã cài). Đây là nền tảng của Infrastructure as Code.

1.6. Script trong CI/CD Pipeline

Trong GitHub Actions, script được gọi qua run: block. Best practices: exit code khác 0 tự động fail pipeline step; script không nên hardcode credentials — dùng ${{ secrets.MY_SECRET }}; dùng shell: bash để đảm bảo môi trường nhất quán; cache dependencies (actions/cache) để tăng tốc. Tách script ra file .sh/.py thay vì inline dài để dễ test local.

2. Thực hành (Labs)

LAB-021

Viết Bash backup script

CLI · Linux Terminal · WSL2

🎯 Mục tiêu: Viết Bash script backup thư mục, nén thành .tar.gz với timestamp, tự động xóa bản backup cũ hơn 7 ngày (rotation), ghi log mỗi lần chạy.

🧰 Công cụ / nền tảng: Bash 5+, tar, find, cron — Linux/WSL2.

📦 Chuẩn bị: Linux hoặc WSL2; thư mục ~/app-data/ chứa vài file mẫu để backup.

▶️ Các bước:

# 1. Tạo thư mục và file mẫu
mkdir -p ~/app-data ~/backups
echo "config data v1" > ~/app-data/config.json
echo "database dump" > ~/app-data/db.sql

# 2. Tạo script backup
cat > ~/backup.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# ---- Cấu hình ----
SOURCE_DIR="${HOME}/app-data"
BACKUP_DIR="${HOME}/backups"
LOG_FILE="${HOME}/backups/backup.log"
RETAIN_DAYS=7

# ---- Logging function ----
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}

# ---- Kiểm tra nguồn tồn tại ----
if [[ ! -d "${SOURCE_DIR}" ]]; then
    log "ERROR: Source directory ${SOURCE_DIR} not found"
    exit 1
fi

mkdir -p "${BACKUP_DIR}"

# ---- Tạo backup với timestamp ----
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
BACKUP_FILE="${BACKUP_DIR}/backup_${TIMESTAMP}.tar.gz"

log "INFO: Starting backup of ${SOURCE_DIR}"
tar -czf "${BACKUP_FILE}" -C "$(dirname "${SOURCE_DIR}")" "$(basename "${SOURCE_DIR}")"
BACKUP_SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1)
log "INFO: Backup created: ${BACKUP_FILE} (${BACKUP_SIZE})"

# ---- Rotation: xóa backup cũ hơn RETAIN_DAYS ngày ----
DELETED_COUNT=$(find "${BACKUP_DIR}" -name "backup_*.tar.gz" -mtime +${RETAIN_DAYS} | wc -l)
find "${BACKUP_DIR}" -name "backup_*.tar.gz" -mtime +${RETAIN_DAYS} -delete
log "INFO: Cleaned up ${DELETED_COUNT} old backup(s) older than ${RETAIN_DAYS} days"
log "INFO: Backup completed successfully"
SCRIPT

# 3. Cấp quyền thực thi
chmod +x ~/backup.sh

# 4. Chạy thử
~/backup.sh

# 5. Kiểm tra kết quả
ls -lh ~/backups/
cat ~/backups/backup.log

# 6. (Tùy chọn) Lên lịch cron chạy mỗi ngày lúc 2:00 AM
# crontab -e
# Thêm dòng: 0 2 * * * /home/$USER/backup.sh

🖥️ Windows Terminal (chạy trong WSL2):

# Mở Windows Terminal → chọn profile WSL2 (Ubuntu)
# Sau đó chạy các lệnh Bash trên như bình thường
wsl bash ~/backup.sh

✅ Kết quả mong đợi: File ~/backups/backup_YYYYMMDD_HHMMSS.tar.gz được tạo; backup.log ghi 3 dòng INFO; chạy lần 2 vẫn thành công (không lỗi); sau 7 ngày giả lập (touch -d "8 days ago") backup cũ bị xóa tự động.

🧹 Cleanup: rm -rf ~/app-data ~/backups ~/backup.sh

LAB-022

Viết PowerShell inventory script

PowerShell · Windows Terminal · CLI

🎯 Mục tiêu: Thu thập thông tin hệ thống (OS, CPU, RAM, disk, running services), xuất ra CSV và JSON, chạy được trên Windows và Linux (PowerShell 7).

🧰 Công cụ / nền tảng: PowerShell 7+ (pwsh), Windows Terminal hoặc VS Code integrated terminal.

📦 Chuẩn bị: Cài PowerShell 7: winget install Microsoft.PowerShell (Windows) hoặc snap install powershell --classic (Linux).

▶️ Các bước:

# 1. Tạo file script
# Lưu nội dung bên dưới vào: Get-SystemInventory.ps1

# ---- Get-SystemInventory.ps1 ----
[CmdletBinding()]
param(
    [string]$OutputPath = "$HOME/inventory",
    [switch]$SkipServices
)

# Tạo thư mục output nếu chưa tồn tại
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null

$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"

Write-Verbose "Collecting OS information..."
$OS = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue

# Cross-platform: dùng $PSVersionTable nếu không có WMI (Linux)
$OSInfo = if ($OS) {
    [PSCustomObject]@{
        Hostname    = $env:COMPUTERNAME ?? (hostname)
        OS          = $OS.Caption
        Version     = $OS.Version
        Architecture= $OS.OSArchitecture
        Uptime_Hours= [math]::Round(((Get-Date) - $OS.LastBootUpTime).TotalHours, 1)
    }
} else {
    [PSCustomObject]@{
        Hostname    = (hostname)
        OS          = (uname -s)
        Version     = (uname -r)
        Architecture= (uname -m)
        Uptime_Hours= "N/A"
    }
}

Write-Verbose "Collecting CPU/RAM..."
$CPU = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue |
       Select-Object -First 1 -ExpandProperty Name

$RAM_GB = if ($OS) {
    [math]::Round($OS.TotalVisibleMemorySize / 1MB, 1)
} else { "N/A" }

$FreeRAM_GB = if ($OS) {
    [math]::Round($OS.FreePhysicalMemory / 1MB, 1)
} else { "N/A" }

Write-Verbose "Collecting disk information..."
$Disks = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -ne $null } |
    Select-Object Name,
        @{N="Total_GB";  E={[math]::Round(($_.Used + $_.Free)/1GB, 1)}},
        @{N="Used_GB";   E={[math]::Round($_.Used/1GB, 1)}},
        @{N="Free_GB";   E={[math]::Round($_.Free/1GB, 1)}},
        @{N="Used_Pct";  E={[math]::Round($_.Used/($_.Used+$_.Free)*100, 1)}}

Write-Verbose "Collecting top services..."
$Services = if (-not $SkipServices) {
    Get-Service -ErrorAction SilentlyContinue |
        Where-Object Status -eq "Running" |
        Sort-Object DisplayName |
        Select-Object -First 20 Name, DisplayName, Status
} else { @() }

# Tổng hợp inventory object
$Inventory = [PSCustomObject]@{
    Timestamp   = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss")
    System      = $OSInfo
    CPU         = $CPU ?? "N/A"
    RAM_Total_GB= $RAM_GB
    RAM_Free_GB = $FreeRAM_GB
    Disks       = $Disks
    RunningServices = $Services
}

# Xuất CSV (flatten disks)
$CsvFile = Join-Path $OutputPath "inventory_${Timestamp}.csv"
$Disks | Export-Csv -Path $CsvFile -NoTypeInformation -Encoding UTF8
Write-Host "CSV saved: $CsvFile" -ForegroundColor Green

# Xuất JSON đầy đủ
$JsonFile = Join-Path $OutputPath "inventory_${Timestamp}.json"
$Inventory | ConvertTo-Json -Depth 5 | Out-File $JsonFile -Encoding UTF8
Write-Host "JSON saved: $JsonFile" -ForegroundColor Green

# In tóm tắt ra console
Write-Host "`n=== System Inventory Summary ===" -ForegroundColor Cyan
Write-Host "Host: $($OSInfo.Hostname) | OS: $($OSInfo.OS)"
Write-Host "CPU: $CPU"
Write-Host "RAM: ${RAM_Free_GB}GB free / ${RAM_GB}GB total"
$Disks | Format-Table -AutoSize
# ---- End of script ----
# 2. Chạy script
pwsh -File ./Get-SystemInventory.ps1 -Verbose

# 3. Chạy với -WhatIf để xem output mà không lưu file (nếu thêm SupportsShouldProcess)
pwsh -File ./Get-SystemInventory.ps1 -SkipServices

# 4. Xem kết quả
Get-Content ~/inventory/inventory_*.json | ConvertFrom-Json | Select-Object Timestamp, CPU, RAM_Total_GB

🖥️ VS Code:

Mở file Get-SystemInventory.ps1 → Terminal → "Run Active File" (F5) hoặc nhấn nút play. VS Code sẽ dùng PowerShell extension để chạy và hiển thị output trong integrated terminal.

✅ Kết quả mong đợi: Hai file xuất hiện trong ~/inventory/: .csv liệt kê disk drives với dung lượng; .json chứa toàn bộ thông tin hệ thống; console hiển thị bảng disk với màu xanh "CSV saved / JSON saved".

🧹 Cleanup: Remove-Item ~/inventory -Recurse -Force

LAB-023

Python gọi API kiểm tra service

CLI · Python · VS Code

🎯 Mục tiêu: Viết Python script gọi REST API của nhiều service (health endpoint), tổng hợp kết quả, in report màu với trạng thái UP/DOWN và response time.

🧰 Công cụ / nền tảng: Python 3.11+, requests, concurrent.futures, terminal.

📦 Chuẩn bị: Python 3.11+; chạy python -m venv .venv && source .venv/bin/activate && pip install requests.

▶️ Các bước:

# 1. Tạo môi trường
mkdir devops-m05-lab && cd devops-m05-lab
python -m venv .venv
source .venv/bin/activate          # Linux/macOS
# .venv\Scripts\Activate.ps1       # PowerShell Windows
pip install requests
echo "requests==2.32.3" > requirements.txt
# 2. Tạo file check-service-health.py
cat > check-service-health.py << 'PYEOF'
#!/usr/bin/env python3
"""
check-service-health.py — Kiểm tra health của nhiều REST API endpoint đồng thời.
Usage: python check-service-health.py [--timeout 5] [--output report.json]
"""
import argparse
import json
import logging
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime

import requests

# ---- Cấu hình logging ----
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)

# ---- Màu ANSI cho terminal ----
GREEN  = "\033[92m"
RED    = "\033[91m"
YELLOW = "\033[93m"
RESET  = "\033[0m"
BOLD   = "\033[1m"

# ---- Danh sách service cần kiểm tra ----
SERVICES = [
    {"name": "GitHub API",        "url": "https://api.github.com",           "expected_status": 200},
    {"name": "JSONPlaceholder",   "url": "https://jsonplaceholder.typicode.com/posts/1", "expected_status": 200},
    {"name": "HTTPBin Health",    "url": "https://httpbin.org/status/200",   "expected_status": 200},
    {"name": "Fake Down Service", "url": "https://httpbin.org/status/503",   "expected_status": 200},
]


def check_service(service: dict, timeout: int) -> dict:
    """Gọi API và trả về kết quả kiểm tra."""
    name   = service["name"]
    url    = service["url"]
    expected = service.get("expected_status", 200)

    start = time.monotonic()
    try:
        resp = requests.get(url, timeout=timeout, allow_redirects=True)
        elapsed_ms = int((time.monotonic() - start) * 1000)
        is_up = resp.status_code == expected
        return {
            "name":         name,
            "url":          url,
            "status":       "UP" if is_up else "DEGRADED",
            "http_code":    resp.status_code,
            "response_ms":  elapsed_ms,
            "error":        None,
        }
    except requests.exceptions.ConnectionError as e:
        elapsed_ms = int((time.monotonic() - start) * 1000)
        logger.warning("Connection error for %s: %s", name, e)
        return {"name": name, "url": url, "status": "DOWN", "http_code": None, "response_ms": elapsed_ms, "error": str(e)}
    except requests.exceptions.Timeout:
        return {"name": name, "url": url, "status": "DOWN", "http_code": None, "response_ms": timeout * 1000, "error": "Timeout"}


def print_report(results: list[dict]) -> None:
    """In report dạng bảng màu ra stdout."""
    print(f"\n{BOLD}{'='*60}{RESET}")
    print(f"{BOLD}  Service Health Report — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}{RESET}")
    print(f"{BOLD}{'='*60}{RESET}")
    print(f"  {'Service':<25} {'Status':<10} {'HTTP':>5}  {'ms':>6}")
    print(f"  {'-'*25} {'-'*10} {'-'*5}  {'-'*6}")
    for r in results:
        color = GREEN if r["status"] == "UP" else (YELLOW if r["status"] == "DEGRADED" else RED)
        http  = str(r["http_code"]) if r["http_code"] else "N/A"
        print(f"  {r['name']:<25} {color}{r['status']:<10}{RESET} {http:>5}  {r['response_ms']:>5}ms")
    up    = sum(1 for r in results if r["status"] == "UP")
    total = len(results)
    color = GREEN if up == total else (YELLOW if up > 0 else RED)
    print(f"\n  {color}Summary: {up}/{total} services UP{RESET}\n")


def main():
    parser = argparse.ArgumentParser(description="Check REST API health endpoints")
    parser.add_argument("--timeout", type=int, default=5, help="Request timeout in seconds")
    parser.add_argument("--output",  type=str, default=None, help="Save JSON report to file")
    args = parser.parse_args()

    logger.info("Checking %d services (timeout=%ds)...", len(SERVICES), args.timeout)
    results = []

    with ThreadPoolExecutor(max_workers=10) as executor:
        futures = {executor.submit(check_service, svc, args.timeout): svc for svc in SERVICES}
        for future in as_completed(futures):
            results.append(future.result())

    results.sort(key=lambda x: x["name"])
    print_report(results)

    if args.output:
        report = {"checked_at": datetime.now().isoformat(), "results": results}
        with open(args.output, "w", encoding="utf-8") as f:
            json.dump(report, f, indent=2, ensure_ascii=False)
        logger.info("Report saved to %s", args.output)

    # Exit code 1 nếu có service DOWN (hữu ích cho CI pipeline)
    any_down = any(r["status"] == "DOWN" for r in results)
    sys.exit(1 if any_down else 0)


if __name__ == "__main__":
    main()
PYEOF
# 3. Chạy script
python check-service-health.py --timeout 5

# 4. Lưu report ra JSON
python check-service-health.py --output health-report.json

# 5. Xem report JSON
python -m json.tool health-report.json | head -30

# 6. Xem exit code (0 = all up, 1 = có DOWN)
echo "Exit code: $?"

🖥️ VS Code:

Mở check-service-health.py → chọn Python interpreter (.venv) ở góc dưới bên phải → Run (F5) hoặc nhấn play button → xem output màu trong terminal.

✅ Kết quả mong đợi: Bảng report hiển thị 4 service: 3 màu xanh UP, 1 màu đỏ DOWN (httpbin 503); echo $? trả về 1; file health-report.json chứa đầy đủ kết quả với response time tính bằng ms.

🧹 Cleanup: deactivate && cd .. && rm -rf devops-m05-lab

LAB-024

Script idempotent tạo folder/user

CLI · Bash · PowerShell · WSL2

🎯 Mục tiêu: Viết script idempotent tạo cấu trúc thư mục dự án và system user — chạy lần 2, lần 3 vẫn không lỗi, không tạo duplicate.

🧰 Công cụ / nền tảng: Bash (Linux/WSL2) và PowerShell 7 (Windows) — hai phiên bản song song.

📦 Chuẩn bị: Linux/WSL2 với sudo; hoặc PowerShell 7 trên Windows với quyền Admin.

▶️ Phiên bản Bash (Linux / WSL2):

cat > setup-project-env.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

# ---- Tham số ----
APP_NAME="${1:-myapp}"
APP_USER="${APP_NAME}svc"
BASE_DIR="/opt/${APP_NAME}"

log() { echo "[$(date '+%H:%M:%S')] $*"; }

# ---- Tạo thư mục (idempotent: mkdir -p không lỗi nếu đã tồn tại) ----
DIRS=("${BASE_DIR}" "${BASE_DIR}/config" "${BASE_DIR}/logs" "${BASE_DIR}/data")
for dir in "${DIRS[@]}"; do
    if [[ -d "${dir}" ]]; then
        log "SKIP: Directory ${dir} already exists"
    else
        mkdir -p "${dir}"
        log "OK:   Created directory ${dir}"
    fi
done

# ---- Tạo system user (idempotent: kiểm tra trước khi tạo) ----
if id "${APP_USER}" &>/dev/null; then
    log "SKIP: User ${APP_USER} already exists (uid=$(id -u "${APP_USER}"))"
else
    # --system: không có home dir, --no-create-home, --shell /usr/sbin/nologin
    useradd --system --no-create-home --shell /usr/sbin/nologin "${APP_USER}"
    log "OK:   Created system user ${APP_USER}"
fi

# ---- Set permissions ----
chown -R "${APP_USER}:${APP_USER}" "${BASE_DIR}" 2>/dev/null || \
    log "WARN: chown skipped (run as root for full setup)"

# ---- Tạo config file mẫu nếu chưa có ----
CONFIG_FILE="${BASE_DIR}/config/app.env"
if [[ ! -f "${CONFIG_FILE}" ]]; then
    cat > "${CONFIG_FILE}" << EOF
APP_NAME=${APP_NAME}
APP_PORT=8080
LOG_LEVEL=INFO
CREATED_AT=$(date '+%Y-%m-%d')
EOF
    log "OK:   Created ${CONFIG_FILE}"
else
    log "SKIP: Config file ${CONFIG_FILE} already exists"
fi

log "DONE: Environment for ${APP_NAME} is ready."
SCRIPT

chmod +x setup-project-env.sh

# Chạy lần 1
sudo bash setup-project-env.sh myapp

# Chạy lần 2 — phải không lỗi, hiển thị SKIP
sudo bash setup-project-env.sh myapp

# Kiểm tra kết quả
ls -la /opt/myapp/
id myappsvc

▶️ Phiên bản PowerShell 7 (Windows — chạy với quyền Admin):

# setup-project-env.ps1
[CmdletBinding(SupportsShouldProcess)]
param([string]$AppName = "myapp")

$BaseDir = "C:\Apps\$AppName"
$Dirs = @("$BaseDir", "$BaseDir\config", "$BaseDir\logs", "$BaseDir\data")

# Tạo thư mục (idempotent)
foreach ($dir in $Dirs) {
    if (Test-Path $dir) {
        Write-Host "SKIP: $dir already exists" -ForegroundColor Yellow
    } else {
        New-Item -ItemType Directory -Path $dir -Force | Out-Null
        Write-Host "OK:   Created $dir" -ForegroundColor Green
    }
}

# Tạo config file nếu chưa có (idempotent)
$ConfigFile = "$BaseDir\config\app.env"
if (-not (Test-Path $ConfigFile)) {
    @"
APP_NAME=$AppName
APP_PORT=8080
LOG_LEVEL=INFO
CREATED_AT=$(Get-Date -Format 'yyyy-MM-dd')
"@ | Out-File $ConfigFile -Encoding UTF8
    Write-Host "OK:   Created $ConfigFile" -ForegroundColor Green
} else {
    Write-Host "SKIP: $ConfigFile already exists" -ForegroundColor Yellow
}

Write-Host "`nDONE: Environment for $AppName is ready." -ForegroundColor Cyan

# Chạy lần 1
pwsh -File .\setup-project-env.ps1 -AppName myapp

# Chạy lần 2 — không lỗi, hiển thị SKIP
pwsh -File .\setup-project-env.ps1 -AppName myapp

✅ Kết quả mong đợi: Lần 1: tất cả dòng "OK: Created..."; lần 2: tất cả dòng "SKIP: ... already exists"; không có error hay exception; thư mục và config file tồn tại đúng cấu trúc; user myappsvc có shell /usr/sbin/nologin.

🧹 Cleanup: Bash: sudo userdel myappsvc && sudo rm -rf /opt/myapp. PowerShell: Remove-Item C:\Apps\myapp -Recurse -Force.

LAB-025

Dùng script trong CI pipeline

CLI · GitHub Actions · Python · Bash

🎯 Mục tiêu: Tích hợp Python health-check script (LAB-023) và Bash script vào GitHub Actions workflow; pipeline fail khi service DOWN, pass khi tất cả UP.

🧰 Công cụ / nền tảng: GitHub Actions, Git, Python 3.11, Bash, VS Code.

📦 Chuẩn bị: Repo GitHub từ LAB-016; có file check-service-health.pyrequirements.txt từ LAB-023.

▶️ Các bước:

# 1. Cấu trúc repo cần có
mkdir -p .github/workflows scripts
cp check-service-health.py scripts/
cp requirements.txt scripts/
# 2. Tạo Bash lint script để kiểm tra Python syntax
cat > scripts/check-syntax.sh << 'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

log() { echo "[$(date '+%H:%M:%S')] $*"; }

log "Checking Python syntax for all .py files..."
FAIL=0
while IFS= read -r -d '' file; do
    if python3 -m py_compile "$file" 2>&1; then
        log "OK:   $file"
    else
        log "FAIL: $file"
        FAIL=1
    fi
done < <(find . -name "*.py" -not -path "./.venv/*" -print0)

if [[ $FAIL -eq 1 ]]; then
    log "ERROR: Syntax check failed"
    exit 1
fi
log "DONE: All Python files passed syntax check"
SCRIPT
chmod +x scripts/check-syntax.sh
# 3. Tạo GitHub Actions workflow
cat > .github/workflows/ci-scripts.yml << 'WORKFLOW'
name: CI — Script Quality & Health Check

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  # Cho phép chạy thủ công từ GitHub UI
  workflow_dispatch:

jobs:
  lint-and-syntax:
    name: Syntax Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python 3.11
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: pip
          cache-dependency-path: scripts/requirements.txt

      - name: Install dependencies
        run: pip install -r scripts/requirements.txt

      - name: Run Bash syntax check
        shell: bash
        run: bash scripts/check-syntax.sh

  health-check:
    name: Service Health Check
    runs-on: ubuntu-latest
    needs: lint-and-syntax
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python 3.11
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: pip
          cache-dependency-path: scripts/requirements.txt

      - name: Install dependencies
        run: pip install -r scripts/requirements.txt

      - name: Run service health check
        id: health
        # Chỉ check các public service — không kiểm tra httpbin 503 trong CI
        env:
          CI: "true"
        run: |
          # Tạo danh sách service chỉ dùng endpoint trả 200
          python3 - << 'PYEOF'
          import sys, time, requests

          SERVICES = [
              ("GitHub API",      "https://api.github.com"),
              ("JSONPlaceholder", "https://jsonplaceholder.typicode.com/posts/1"),
          ]

          results = []
          for name, url in SERVICES:
              try:
                  t = time.monotonic()
                  r = requests.get(url, timeout=8)
                  ms = int((time.monotonic() - t) * 1000)
                  status = "UP" if r.status_code == 200 else "DOWN"
                  print(f"  {status:<6} {name:<25} HTTP {r.status_code} ({ms}ms)")
                  results.append(status)
              except Exception as e:
                  print(f"  DOWN   {name:<25} ERROR: {e}")
                  results.append("DOWN")

          if "DOWN" in results:
              print("\nFAIL: One or more services are DOWN")
              sys.exit(1)
          print("\nPASS: All services are UP")
          PYEOF

      - name: Upload health report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: health-report
          path: health-report.json
          if-no-files-found: ignore
WORKFLOW
# 4. Commit và push để kích hoạt CI
git add .github/workflows/ci-scripts.yml scripts/
git commit -m "ci: add script quality and health check workflow"
git push origin main

# 5. Theo dõi pipeline
gh run list --limit 5
gh run watch   # xem realtime log

# 6. Xem chi tiết job
gh run view --log

🖥️ GitHub Web:

Repo → tab "Actions" → click run mới nhất → xem từng job (Syntax Check → Health Check) → click step để xem log chi tiết → download artifact "health-report" nếu có.

✅ Kết quả mong đợi: Pipeline xanh (pass): job "Syntax Check" pass → job "Health Check" pass với log "PASS: All services are UP"; gh run list hiển thị status "completed" và conclusion "success"; nếu cố ý thêm service DOWN vào code, pipeline tự động fail với exit code 1 và hiển thị đỏ trong GitHub Actions UI.

🧹 Cleanup: Không cần xóa — workflow sẽ chạy tự động mỗi khi push. Để disable: GitHub → Actions → chọn workflow → "Disable workflow".

3. Tình huống doanh nghiệp thực tế

Bối cảnh

Một công ty logistics vận hành 50 Linux server + 20 Windows server. Đội Ops mỗi sáng mất 1 tiếng thủ công: SSH vào từng server check disk, service, backup. Khi server bị full disk, chỉ phát hiện khi app crash, không có cảnh báo sớm.

Giải pháp scripting

  • Bash backup script (LAB-021) deploy qua Ansible lên 50 Linux server, cron job 2AM, log tập trung về ELK. Rotation 7 ngày tự động.
  • PowerShell inventory script (LAB-022) chạy qua WinRM trên 20 Windows server, xuất JSON, đẩy vào Grafana dashboard. Thay 1 tiếng kiểm tra thủ công bằng dashboard real-time.
  • Python health check (LAB-023) chạy mỗi 5 phút qua GitHub Actions Scheduled workflow. Khi có service DOWN, gửi alert vào Slack qua webhook.
  • Idempotent setup (LAB-024) dùng khi onboard server mới: chạy một lần, môi trường đúng; chạy lại sau incident để verify — không gây destructive side-effect.
  • Kết quả: MTTR giảm từ 45 phút (phát hiện thủ công) xuống 3 phút (alert tự động + runbook link trong message); đội Ops tiết kiệm 5 giờ/tuần.

📚 Nguồn tham khảo

Module 04: Git, GitHub, GitLab Module 06: Virtualization
Zalo