Bash Shell Scripting cho Người Mới Bắt Đầu: Tự Động Hóa để Thoát Khỏi Mớ Hỗn Độn Production

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

2 Giờ Sáng và Server Của Bạn Đang Bốc Cháy

Cảnh báo đổ vào điện thoại bạn. Disk trên server production đã lên 94%. Bạn SSH vào, mắt nhắm mắt mở, và bắt đầu kiểm tra thủ công từng thư mục, xóa từng file log cũ một, khởi động lại các service. Một tiếng sau, bạn đã xử lý xong — nhưng biết rằng tuần tới chuyện này sẽ lặp lại.

Đêm đó, tôi tự hứa sẽ viết một script để xử lý tự động. Không phải vì lười biếng, mà vì con người hay mắc lỗi lúc 2 giờ sáng, còn máy tính thì không. Đó là lúc bash scripting chuyển từ “thứ gì đó tôi nên học lúc nào đó” thành “thứ tôi cần ngay bây giờ.”

Đây là tài liệu mà tôi ước mình có lúc đó.

Khái Niệm Cốt Lõi: Bash Scripting Thực Sự Là Gì

Một bash script là file văn bản thuần túy chứa danh sách các lệnh shell, được thực thi từ trên xuống dưới. Vậy thôi. Những lệnh giống hệt bạn gõ thủ công trong terminal — chỉ là được lưu vào file và chạy khi cần.

Script Đầu Tiên của Bạn

Tạo một file tên hello.sh:

#!/bin/bash
# Đây là một comment
echo "Server đang hoạt động lúc: $(date)"
echo "Thời gian hoạt động: $(uptime -p)"

Dòng đầu tiên #!/bin/bash là shebang. Nó nói với OS biết dùng interpreter nào — nếu thiếu, hệ thống có thể thử chạy bash script của bạn bằng sh hoặc shell khác, gây ra các lỗi khó phát hiện. Cấp quyền thực thi và chạy:

chmod +x hello.sh
./hello.sh

Biến

Biến lưu trữ các giá trị bạn sẽ dùng nhiều lần trong script. Một quy tắc sắt: không có khoảng trắng xung quanh dấu =. NAME="prod" hoạt động. NAME = "prod" báo lỗi. Bash hiểu NAME là một lệnh và cố thực thi nó.

#!/bin/bash
SERVER_NAME="prod-web-01"
DISK_THRESHOLD=90

echo "Đang kiểm tra server: $SERVER_NAME"
echo "Cảnh báo nếu disk vượt quá: ${DISK_THRESHOLD}%"

Dùng ${VAR} khi biến đứng ngay trước các ký tự khác. $DISK_THRESHOLD_MB sẽ âm thầm thất bại — bash tìm biến có tên chính xác là DISK_THRESHOLD_MB. ${DISK_THRESHOLD}_MB mới cho bạn kết quả mong muốn.

Thay Thế Lệnh (Command Substitution)

Bắt output của lệnh trực tiếp vào biến bằng cú pháp $():

CURRENT_DISK=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
echo "Mức sử dụng disk hiện tại: $CURRENT_DISK%"

Câu Lệnh Điều Kiện

Câu lệnh điều kiện biến một script từ chỗ chỉ phát lại lệnh đơn giản thành thứ gì đó phản ứng với trạng thái thực tế của hệ thống. So sánh giá trị, kiểm tra sự tồn tại của file, rẽ nhánh logic:

#!/bin/bash
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
THRESHOLD=85

if [ "$DISK_USAGE" -gt "$THRESHOLD" ]; then
    echo "CẢNH BÁO: Disk đang ở ${DISK_USAGE}% — đang dọn log"
    find /var/log -name "*.log" -mtime +7 -delete
else
    echo "Disk ổn: ${DISK_USAGE}%"
fi

Khoảng trắng bên trong [ ] là bắt buộc — không phải tùy chọn. Viết ["$DISK_USAGE" -gt "$THRESHOLD"] và bash sẽ báo lỗi “command not found” khó hiểu. Shell cần những khoảng trắng đó để parse dấu ngoặc là lệnh test, không phải tên file.

Vòng Lặp

Vòng lặp cho phép bạn lặp lại hành động trên một danh sách hoặc khi một điều kiện còn đúng:

# Vòng lặp For — duyệt qua từng phần tử
for SERVICE in nginx mysql redis; do
    systemctl is-active --quiet $SERVICE && echo "$SERVICE: đang chạy" || echo "$SERVICE: ĐÃ DỪNG"
done

# Vòng lặp While — chạy cho đến khi điều kiện sai
COUNT=0
while [ $COUNT -lt 5 ]; do
    echo "Lần thử $COUNT"
    COUNT=$((COUNT + 1))
done

Hàm

Không có hàm, một health check cho 10 service là 60 dòng code copy-paste. Có hàm, chỉ còn 20 dòng. Định nghĩa một lần, gọi ở bất kỳ đâu bạn cần:

#!/bin/bash

check_service() {
    local SERVICE_NAME=$1
    if systemctl is-active --quiet "$SERVICE_NAME"; then
        echo "[OK] $SERVICE_NAME đang chạy"
    else
        echo "[FAIL] $SERVICE_NAME đã dừng — đang thử khởi động lại"
        systemctl restart "$SERVICE_NAME"
    fi
}

check_service nginx
check_service mysql
check_service redis

Từ khóa local giới hạn phạm vi của biến trong hàm. Nếu không có nó, SERVICE_NAME sẽ rò rỉ vào phạm vi toàn cục và có thể âm thầm ghi đè các biến khác — chính xác là loại bug chỉ xuất hiện lúc 2 giờ sáng, không phải khi test.

Thực Hành: Xây Dựng Script Giám Sát Thực Tế

Đây là script tôi chạy hàng đêm qua cron trên server production Ubuntu 22.04 với 4GB RAM. Trước khi có nó, điều tra sự cố disk tốn 20 phút làm thủ công. Giờ nó tự động ghi log và tôi phát hiện vấn đề vào buổi sáng, không phải lúc nửa đêm.

Script Kiểm Tra Sức Khỏe Server

#!/bin/bash
# server-health.sh — kiểm tra sức khỏe server production
# Cách dùng: ./server-health.sh [--notify]

set -euo pipefail

# Cấu hình
HOSTNAME=$(hostname)
ALERT_EMAIL="[email protected]"
DISK_WARN=85
RAM_WARN=90
LOG_FILE="/var/log/health-check.log"

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

# Kiểm tra mức sử dụng disk
check_disk() {
    local USAGE
    USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
    if [ "$USAGE" -gt "$DISK_WARN" ]; then
        log "CẢNH BÁO: Disk đang ở ${USAGE}%"
        return 1
    fi
    log "ỔN: Disk đang ở ${USAGE}%"
    return 0
}

# Kiểm tra mức sử dụng RAM
check_ram() {
    local TOTAL FREE USAGE
    TOTAL=$(free | awk '/Mem:/ {print $2}')
    FREE=$(free | awk '/Mem:/ {print $4}')
    USAGE=$(( (TOTAL - FREE) * 100 / TOTAL ))
    if [ "$USAGE" -gt "$RAM_WARN" ]; then
        log "CẢNH BÁO: RAM đang ở ${USAGE}%"
        return 1
    fi
    log "ỔN: RAM đang ở ${USAGE}%"
    return 0
}

# Kiểm tra các service quan trọng
check_services() {
    local FAILED=0
    for SVC in nginx mysql; do
        if ! systemctl is-active --quiet "$SVC"; then
            log "NGHIÊM TRỌNG: $SVC không chạy"
            FAILED=$((FAILED + 1))
        else
            log "ỔN: $SVC đang chạy"
        fi
    done
    return $FAILED
}

# Chạy tất cả các kiểm tra
log "=== Kiểm tra sức khỏe server $HOSTNAME ==="
ALL_OK=true

check_disk || ALL_OK=false
check_ram || ALL_OK=false
check_services || ALL_OK=false

if [ "$ALL_OK" = false ]; then
    log "Kiểm tra sức khỏe THẤT BẠI — xem lại log"
    exit 1
fi

log "Tất cả các kiểm tra đã qua"

Chạy Tự Động với Cron

Lưu script, cấp quyền thực thi, rồi lên lịch chạy:

chmod +x /opt/scripts/server-health.sh

# Mở trình chỉnh sửa crontab
crontab -e

# Chạy mỗi 30 phút
*/30 * * * * /opt/scripts/server-health.sh >> /var/log/health-check.log 2>&1

Debug Khi Mọi Thứ Hỏng

Script sẽ có lúc lỗi. Đây là cách tìm ra nguyên nhân:

# Chạy với output chi tiết — hiển thị từng lệnh trước khi thực thi
bash -x ./server-health.sh

# Kiểm tra exit code của lệnh vừa chạy
echo $?

# Thêm vào đầu script để xử lý lỗi nghiêm ngặt:
set -euo pipefail
# -e: thoát khi có lỗi
# -u: báo lỗi nếu dùng biến chưa được khai báo
# -o pipefail: bắt lỗi trong pipe

set -euo pipefail nên đặt ở đầu mọi script production. Nếu không có nó, script có thể âm thầm thất bại giữa chừng và để lại hệ thống ở trạng thái nửa hỏng mà không có dòng log nào giải thích lý do.

Nhận Tham Số Đầu Vào

Đường dẫn cứng trong code khiến script dễ vỡ. Tham số vị trí cho phép bạn truyền giá trị khi chạy, giúp cùng một script có thể tái sử dụng ở các thư mục và môi trường khác nhau:

#!/bin/bash
# Cách dùng: ./backup.sh /var/www/html /backups

SOURCE_DIR=$1
DEST_DIR=$2
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

if [ $# -ne 2 ]; then
    echo "Cách dùng: $0 <nguồn> <đích>"
    exit 1
fi

tar -czf "${DEST_DIR}/backup_${TIMESTAMP}.tar.gz" "$SOURCE_DIR"
echo "Backup hoàn tất: ${DEST_DIR}/backup_${TIMESTAMP}.tar.gz"

$1$2 là tham số thứ nhất và thứ hai. $# cho biết tổng số tham số. $0 là tên của chính script — tiện dụng để in thông báo hướng dẫn chính xác nếu ai đó gọi sai.

Ngừng Làm Thủ Công

Bash scripting không phải để thể hiện sự thông minh — mà là để không làm cùng một việc hai lần. Mọi tác vụ lặp đi lặp lại bạn xử lý thủ công đều là một script đang chờ được viết ra.

Bắt đầu nhỏ. Chọn một việc bạn làm thủ công mỗi tuần và tự động hóa nó. Kiểm tra dung lượng disk, xoay vòng log, backup một thư mục — không quan trọng là gì. Viết script, test trên máy không phải production, rồi lên lịch với cron.

Mục tiêu không phải là code hoàn hảo. Mục tiêu là lần tới khi disk đầy lúc 2 giờ sáng, script của bạn đã xử lý xong trước khi điện thoại reo.

Share: