This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Sau 6 bài học nền tảng, đã đến lúc áp dụng mọi kiến thức vào các script thực tế. Bài này sẽ hướng dẫn bạn viết những script mà DevOps engineer và SysAdmin sử dụng hàng ngày: backup tự động, health check monitor, batch renamer, .env parser và deploy script — tất cả đều có error handling và logging chuyên nghiệp.

1. Error Handling chuyên nghiệp trong Bash

Quy tắc số 1 khi viết production script: luôn bắt đầu với error handling. Nếu script chạy mà bạn không biết nó lỗi ở đâu, bạn sẽ mất hàng giờ debug.

1.1 set -euo pipefail — Bộ ba an toàn

Đây là "boilerplate" mà mọi script chuyên nghiệp đều có ở dòng đầu tiên:

safe_script.sh
#!/usr/bin/env bash
set -euo pipefail

# -e  : Dừng ngay khi bất kỳ lệnh nào exit code != 0
# -u  : Báo lỗi khi dùng biến chưa được khai báo
# -o pipefail : Pipe trả về exit code của lệnh THẤT BẠI đầu tiên
#               (mặc định chỉ trả exit code của lệnh cuối)

# Ví dụ không có pipefail:
# curl http://fail | grep "ok"  → exit 0 (grep thành công)

# Với pipefail:
# curl http://fail | grep "ok"  → exit 1 (curl thất bại)

echo "Script bắt đầu..."
MYVAR="hello"
echo "$MYVAR"

# Nếu bỏ set -u, dòng sau sẽ in rỗng thay vì báo lỗi:
# echo "$UNDEFINED_VAR"

1.2 Cho phép lệnh thất bại với || true

Khi bạn biết một lệnh có thể fail và muốn tiếp tục:

or_true.sh
#!/usr/bin/env bash
set -euo pipefail

# Cách 1: || true — bỏ qua lỗi
rm /tmp/maybe-not-exist.log 2>/dev/null || true

# Cách 2: if để kiểm tra trước
if [[ -f /tmp/maybe-not-exist.log ]]; then
  rm /tmp/maybe-not-exist.log
fi

# Cách 3: Gán giá trị mặc định cho biến (tránh -u error)
DB_PORT="${DB_PORT:-5432}"
echo "Sử dụng port: $DB_PORT"

1.3 trap — Bẫy lỗi và cleanup

trap cho phép bạn chạy một function khi script bị lỗi hoặc kết thúc. Đây là cách dọn dẹp tài nguyên (temp files, lock files) chuyên nghiệp:

trap_demo.sh
#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp)
LOCKFILE="/tmp/myscript.lock"

cleanup() {
  echo "🧹 Dọn dẹp tài nguyên..."
  rm -f "$TMPFILE" "$LOCKFILE"
}

on_error() {
  local exit_code=$?
  local line_no=$1
  echo "❌ Lỗi tại dòng $line_no (exit code: $exit_code)"
  cleanup
  exit "$exit_code"
}

# Bẫy tín hiệu
trap 'on_error $LINENO' ERR      # Khi có lỗi
trap cleanup EXIT                  # Khi script kết thúc (bình thường hoặc lỗi)
trap 'echo "Bị interrupt!"; exit 1' INT TERM  # Ctrl+C hoặc kill

# Tạo lock file (tránh chạy song song)
if [[ -f "$LOCKFILE" ]]; then
  echo "Script đang chạy (lock file tồn tại). Thoát."
  exit 1
fi
touch "$LOCKFILE"

echo "Đang xử lý..." > "$TMPFILE"
# ... logic chính ở đây ...
echo "Hoàn thành!"

2. Logging Function chuyên nghiệp

Script production cần logging có timestamp, log level và khả năng ghi ra cả terminal lẫn file log.

logger.sh
#!/usr/bin/env bash
set -euo pipefail

# ── Cấu hình ──
LOG_FILE="/var/log/myapp/app.log"
LOG_LEVEL="INFO"  # DEBUG, INFO, WARN, ERROR

# ── ANSI Colors ──
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'  # No Color

# ── Log function ──
log() {
  local level="$1"
  shift
  local message="$*"
  local timestamp
  timestamp=$(date '+%Y-%m-%d %H:%M:%S')

  # Chọn màu theo level
  local color="$NC"
  case "$level" in
    DEBUG) color="$BLUE" ;;
    INFO)  color="$GREEN" ;;
    WARN)  color="$YELLOW" ;;
    ERROR) color="$RED" ;;
  esac

  # In ra terminal (có màu)
  echo -e "${color}[${timestamp}] [${level}] ${message}${NC}"

  # Ghi ra file log (không màu) — tee -a append vào file
  echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE" > /dev/null
}

# ── Shortcut functions ──
log_info()  { log "INFO" "$@"; }
log_warn()  { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
log_debug() { log "DEBUG" "$@"; }

# ── Sử dụng ──
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true

log_info "Ứng dụng bắt đầu"
log_debug "Đang kết nối database..."
log_warn "Disk usage > 80%"
log_error "Không thể kết nối tới API server"

Output trên terminal sẽ có màu, trong khi file log chỉ chứa text thuần — rất dễ grep và phân tích sau này.

3. Script 1: Backup tự động với Rotation

Script backup chuyên nghiệp cần: nén dữ liệu, đặt tên theo ngày, xoay vòng (rotation) file cũ và thông báo kết quả.

backup.sh
#!/usr/bin/env bash
set -euo pipefail

# ══════════════════════════════════════════
# Backup Script với Rotation & Logging
# ══════════════════════════════════════════

# ── Cấu hình ──
SOURCE_DIR="/var/www/myapp"
BACKUP_DIR="/backup/myapp"
LOG_FILE="/var/log/backup.log"
MAX_BACKUPS=7          # Giữ tối đa 7 bản backup
DATE=$(date '+%Y-%m-%d_%H%M%S')
BACKUP_NAME="myapp_backup_${DATE}.tar.gz"

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

# ── Cleanup khi lỗi ──
cleanup() {
  if [[ -f "${BACKUP_DIR}/${BACKUP_NAME}" ]]; then
    rm -f "${BACKUP_DIR}/${BACKUP_NAME}"
    log "ERROR: Backup thất bại, đã xóa file không hoàn chỉnh"
  fi
}
trap cleanup ERR

# ── Kiểm tra thư mục ──
if [[ ! -d "$SOURCE_DIR" ]]; then
  log "ERROR: Source directory không tồn tại: $SOURCE_DIR"
  exit 1
fi
mkdir -p "$BACKUP_DIR"

# ── Tạo backup ──
log "INFO: Bắt đầu backup $SOURCE_DIR..."
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}" \
  --exclude='node_modules' \
  --exclude='.git' \
  --exclude='*.log' \
  -C "$(dirname "$SOURCE_DIR")" \
  "$(basename "$SOURCE_DIR")"

# ── Kiểm tra kích thước ──
BACKUP_SIZE=$(du -h "${BACKUP_DIR}/${BACKUP_NAME}" | cut -f1)
log "INFO: Backup hoàn thành: $BACKUP_NAME ($BACKUP_SIZE)"

# ── Rotation: Xóa bản cũ ──
BACKUP_COUNT=$(find "$BACKUP_DIR" -name "myapp_backup_*.tar.gz" -type f | wc -l)
if (( BACKUP_COUNT > MAX_BACKUPS )); then
  EXCESS=$(( BACKUP_COUNT - MAX_BACKUPS ))
  log "INFO: Xóa $EXCESS bản backup cũ nhất..."
  find "$BACKUP_DIR" -name "myapp_backup_*.tar.gz" -type f \
    | sort \
    | head -n "$EXCESS" \
    | xargs rm -f
fi

# ── Thống kê ──
TOTAL=$(find "$BACKUP_DIR" -name "myapp_backup_*.tar.gz" -type f | wc -l)
DISK_USAGE=$(du -sh "$BACKUP_DIR" | cut -f1)
log "INFO: Tổng cộng $TOTAL bản backup, tổng dung lượng: $DISK_USAGE"

# ── Thông báo (ví dụ gửi qua webhook) ──
notify() {
  local message="$1"
  # Slack webhook
  if [[ -n "${SLACK_WEBHOOK:-}" ]]; then
    curl -s -X POST "$SLACK_WEBHOOK" \
      -H 'Content-Type: application/json' \
      -d "{\"text\": \"$message\"}" > /dev/null || true
  fi
  # Hoặc gửi email
  # echo "$message" | mail -s "Backup Report" [email protected]
}

notify "✅ Backup thành công: $BACKUP_NAME ($BACKUP_SIZE)"

Giải thích các kỹ thuật:

  • tar -czf: Tạo file nén gzip (c=create, z=gzip, f=file)
  • --exclude: Loại bỏ thư mục không cần backup (node_modules, .git)
  • find | sort | head | xargs rm: Pipeline xóa N file cũ nhất
  • ${SLACK_WEBHOOK:-}: Giá trị mặc định rỗng, tránh lỗi set -u

4. Script 2: Health Check Monitor

Một script giám sát service bằng cách kiểm tra HTTP status và response time theo chu kỳ:

healthcheck.sh
#!/usr/bin/env bash
set -euo pipefail

# ══════════════════════════════════════════
# Health Check Monitor
# ══════════════════════════════════════════

# ── Cấu hình ──
ENDPOINTS=(
  "https://api.example.com/health"
  "https://web.example.com"
  "https://admin.example.com/ping"
)
CHECK_INTERVAL=30      # Giây giữa các lần check
TIMEOUT=10             # Timeout cho mỗi request (giây)
MAX_FAILURES=3         # Số lần thất bại trước khi cảnh báo
LOG_FILE="/var/log/healthcheck.log"

declare -A FAILURE_COUNT  # Associative array đếm lỗi

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

# ── Check một endpoint ──
check_endpoint() {
  local url="$1"
  local http_code
  local response_time

  # curl trả về HTTP status code và thời gian phản hồi
  http_code=$(curl -s -o /dev/null -w "%{http_code}" \
    --max-time "$TIMEOUT" "$url" 2>/dev/null) || http_code="000"

  response_time=$(curl -s -o /dev/null -w "%{time_total}" \
    --max-time "$TIMEOUT" "$url" 2>/dev/null) || response_time="N/A"

  if [[ "$http_code" -ge 200 && "$http_code" -lt 400 ]]; then
    log "✅ OK  | $url | HTTP $http_code | ${response_time}s"
    FAILURE_COUNT["$url"]=0
  else
    local count=${FAILURE_COUNT["$url"]:-0}
    count=$((count + 1))
    FAILURE_COUNT["$url"]=$count
    log "❌ FAIL | $url | HTTP $http_code | Lần thứ $count"

    if (( count >= MAX_FAILURES )); then
      alert "$url" "$http_code" "$count"
    fi
  fi
}

# ── Cảnh báo ──
alert() {
  local url="$1" code="$2" count="$3"
  log "🚨 ALERT: $url đã fail $count lần liên tiếp (HTTP $code)"

  # Gửi notification qua webhook/email
  if [[ -n "${SLACK_WEBHOOK:-}" ]]; then
    curl -s -X POST "$SLACK_WEBHOOK" \
      -H 'Content-Type: application/json' \
      -d "{\"text\": \"🚨 Service DOWN: $url (HTTP $code, $count failures)\"}" \
      > /dev/null || true
  fi
}

# ── Kiểm tra ping ──
check_ping() {
  local host="$1"
  if ping -c 1 -W 2 "$host" > /dev/null 2>&1; then
    log "📡 PING OK  | $host"
  else
    log "📡 PING FAIL | $host"
  fi
}

# ── Main loop ──
log "═══ Health Check Monitor bắt đầu ═══"
log "Endpoints: ${#ENDPOINTS[@]} | Interval: ${CHECK_INTERVAL}s"

while true; do
  for endpoint in "${ENDPOINTS[@]}"; do
    check_endpoint "$endpoint"
  done
  echo "---"
  sleep "$CHECK_INTERVAL"
done

Kỹ thuật nổi bật:

  • curl -w "%{http_code}": Lấy HTTP status code mà không cần parse output
  • curl -w "%{time_total}": Đo response time chính xác (giây)
  • declare -A: Associative array để đếm failure theo từng URL
  • while true; do ... sleep N; done: Vòng lặp giám sát liên tục

5. Script 3: Batch File Renamer, .env Parser & Deploy

Ba script nhỏ nhưng cực kỳ hữu dụng trong công việc hàng ngày.

5.1 Batch File Renamer

Đổi tên hàng loạt file theo pattern — ví dụ thêm prefix ngày, chuyển đuôi file, hoặc thay thế ký tự:

batch_rename.sh
#!/usr/bin/env bash
set -euo pipefail

# ══════════════════════════════════════════
# Batch File Renamer
# ══════════════════════════════════════════

TARGET_DIR="${1:-.}"   # Thư mục mục tiêu (mặc định: thư mục hiện tại)
PATTERN="${2:-*}"      # Pattern file (mặc định: tất cả)
DRY_RUN=true           # Chạy thử trước

echo "📁 Thư mục: $TARGET_DIR"
echo "📋 Pattern: $PATTERN"
echo "═══════════════════════════════"

# Ví dụ 1: Thêm prefix ngày
add_date_prefix() {
  local dir="$1"
  local date_prefix
  date_prefix=$(date '+%Y%m%d')

  for file in "$dir"/*; do
    [[ -f "$file" ]] || continue
    local basename
    basename=$(basename "$file")

    # Bỏ qua nếu đã có prefix
    if [[ "$basename" == ${date_prefix}* ]]; then
      continue
    fi

    local new_name="${dir}/${date_prefix}_${basename}"
    if $DRY_RUN; then
      echo "[DRY RUN] $basename → ${date_prefix}_${basename}"
    else
      mv "$file" "$new_name"
      echo "✅ $basename → ${date_prefix}_${basename}"
    fi
  done
}

# Ví dụ 2: Thay thế khoảng trắng bằng dấu gạch dưới
sanitize_filenames() {
  local dir="$1"
  for file in "$dir"/*; do
    [[ -f "$file" ]] || continue
    local basename dirname new_basename
    basename=$(basename "$file")
    dirname=$(dirname "$file")

    # Thay space → underscore, xóa ký tự đặc biệt
    new_basename=$(echo "$basename" | tr ' ' '_' | tr -d '()[]{}')

    if [[ "$basename" != "$new_basename" ]]; then
      if $DRY_RUN; then
        echo "[DRY RUN] '$basename' → '$new_basename'"
      else
        mv "$file" "${dirname}/${new_basename}"
        echo "✅ '$basename' → '$new_basename'"
      fi
    fi
  done
}

# Ví dụ 3: Đổi đuôi file hàng loạt
change_extension() {
  local dir="$1" old_ext="$2" new_ext="$3"
  for file in "$dir"/*."$old_ext"; do
    [[ -f "$file" ]] || continue
    local new_file="${file%.$old_ext}.$new_ext"
    if $DRY_RUN; then
      echo "[DRY RUN] $(basename "$file") → $(basename "$new_file")"
    else
      mv "$file" "$new_file"
      echo "✅ $(basename "$file") → $(basename "$new_file")"
    fi
  done
}

# Chạy
add_date_prefix "$TARGET_DIR"

5.2 .env Parser

Đọc file .env và export biến môi trường — pattern cực phổ biến trong mọi dự án:

env_parser.sh
#!/usr/bin/env bash
set -euo pipefail

# ══════════════════════════════════════════
# .env File Parser
# ══════════════════════════════════════════

load_env() {
  local env_file="${1:-.env}"

  if [[ ! -f "$env_file" ]]; then
    echo "❌ File $env_file không tồn tại"
    return 1
  fi

  echo "📄 Loading $env_file ..."
  local line_num=0
  local loaded=0

  while IFS= read -r line || [[ -n "$line" ]]; do
    line_num=$((line_num + 1))

    # Bỏ qua comment và dòng trống
    [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue

    # Bỏ khoảng trắng đầu/cuối
    line=$(echo "$line" | xargs)

    # Kiểm tra format KEY=VALUE
    if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
      local key="${BASH_REMATCH[1]}"
      local value="${BASH_REMATCH[2]}"

      # Bỏ quotes bao quanh value
      value="${value%\"}"
      value="${value#\"}"
      value="${value%\'}"
      value="${value#\'}"

      export "$key=$value"
      loaded=$((loaded + 1))
      echo "  ✅ $key=****"
    else
      echo "  ⚠️  Dòng $line_num: format không hợp lệ: $line"
    fi
  done < "$env_file"

  echo "📊 Đã load $loaded biến từ $env_file"
}

# Sử dụng
load_env ".env"
load_env ".env.local"  2>/dev/null || true

# Kiểm tra biến bắt buộc
require_env() {
  local var_name="$1"
  if [[ -z "${!var_name:-}" ]]; then
    echo "❌ Biến bắt buộc $var_name chưa được set!"
    exit 1
  fi
}

require_env "DB_HOST"
require_env "DB_PASSWORD"
require_env "API_KEY"

5.3 Deploy Script đơn giản

Kết hợp tất cả kỹ thuật trên vào một deploy script hoàn chỉnh:

deploy.sh
#!/usr/bin/env bash
set -euo pipefail

# ══════════════════════════════════════════
# Simple Deploy Script
# ══════════════════════════════════════════

APP_NAME="myapp"
DEPLOY_DIR="/var/www/$APP_NAME"
REPO_URL="[email protected]:company/$APP_NAME.git"
BRANCH="${1:-main}"
LOG_FILE="/var/log/deploy.log"

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

# Cleanup khi lỗi
rollback() {
  log "❌ Deploy thất bại! Đang rollback..."
  if [[ -d "${DEPLOY_DIR}.backup" ]]; then
    rm -rf "$DEPLOY_DIR"
    mv "${DEPLOY_DIR}.backup" "$DEPLOY_DIR"
    log "✅ Rollback thành công"
  fi
  exit 1
}
trap rollback ERR

# ── Step 1: Pre-checks ──
log "═══ Bắt đầu deploy $APP_NAME (branch: $BRANCH) ═══"

command -v git >/dev/null || { log "ERROR: git chưa cài"; exit 1; }
command -v node >/dev/null || { log "ERROR: node chưa cài"; exit 1; }

# ── Step 2: Backup ──
if [[ -d "$DEPLOY_DIR" ]]; then
  log "Đang backup bản hiện tại..."
  cp -a "$DEPLOY_DIR" "${DEPLOY_DIR}.backup"
fi

# ── Step 3: Pull code ──
if [[ -d "$DEPLOY_DIR/.git" ]]; then
  log "Đang pull code mới..."
  cd "$DEPLOY_DIR"
  git fetch origin
  git checkout "$BRANCH"
  git pull origin "$BRANCH"
else
  log "Đang clone repository..."
  git clone -b "$BRANCH" "$REPO_URL" "$DEPLOY_DIR"
  cd "$DEPLOY_DIR"
fi

# ── Step 4: Install dependencies ──
log "Đang install dependencies..."
npm ci --production 2>&1 | tail -1

# ── Step 5: Build ──
log "Đang build..."
npm run build 2>&1 | tail -5

# ── Step 6: Restart service ──
log "Đang restart service..."
if command -v systemctl >/dev/null; then
  sudo systemctl restart "$APP_NAME"
elif command -v pm2 >/dev/null; then
  pm2 restart "$APP_NAME"
fi

# ── Step 7: Health check ──
log "Đang kiểm tra health..."
sleep 3
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000/health" || echo "000")
if [[ "$HTTP_CODE" == "200" ]]; then
  log "✅ Deploy thành công! Health check: HTTP $HTTP_CODE"
  rm -rf "${DEPLOY_DIR}.backup"
else
  log "❌ Health check thất bại (HTTP $HTTP_CODE)"
  rollback
fi

6. Câu hỏi trắc nghiệm ôn tập

Trắc nghiệm: set -euo pipefail

Khi sử dụng set -euo pipefail, điều gì xảy ra nếu một lệnh trong pipe thất bại (ví dụ: curl http://fail | grep "ok")?

Tải file code thực hành minh họa bài học

File chứa đầy đủ các ví dụ: error handling, logging, backup, health check, batch renamer, .env parser và deploy script.

Tải về real_world_scripts.sh

Related Articles

Bài viết liên quan trong series

Lesson 8: DevOps & Automation with Bash: Git Hooks, CI/CD & Docker Bài 8: DevOps & Automation với Bash: Git Hooks, CI/CD & Docker Lesson 6: Process & Signals in Bash: Process Management & trap Bài 6: Process & Signals trong Bash: Quản Lý Tiến Trình & trap Back to Bash Series Overview Quay lại Lộ trình Bash Series