Giao diện
Bash Scripting — Tự Động Hóa Cho Kỹ Sư 🤖
Mỗi ngày, một DevOps engineer lặp đi lặp lại hàng chục thao tác: deploy code, kiểm tra service, rotate log, backup database. Bash scripting biến những thao tác thủ công đó thành quy trình tự động, nhất quán, có thể lặp lại — loại bỏ hoàn toàn yếu tố "tay run lúc 3 giờ sáng" khỏi production.
1. Bash — Keo Dán Tự Động Hóa 🧠
🎯 Mục tiêu
Sau bài học này, bạn sẽ:
- Viết được bash script an toàn với
set -euo pipefailvà quoting đúng cách - Xây dựng deploy script có backup, rollback, và health check
- Sử dụng
shellcheckđể bắt bug trước khi script chạy trên production - Hiểu tại sao một biến không được quote có thể xóa sạch production data
Bash không phải ngôn ngữ lập trình — Bash là keo dán tự động hóa. Nó kết nối các công cụ Unix lại với nhau thành workflow hoàn chỉnh.
Dùng Bash cho:
| Use Case | Ví dụ | Tại sao Bash? |
|---|---|---|
| Deploy scripts | Pull code, restart service | Gọi trực tiếp system commands |
| Health checks | Curl endpoint, kiểm tra response | Kết hợp curl + grep + exit code |
| Log rotation | Nén log cũ, xóa log > 30 ngày | find + gzip + rm |
| System monitoring | Disk usage, memory, connections | One-liner kết hợp nhiều tool |
| CI/CD pipeline steps | Build, test, push image | Chạy tuần tự các lệnh |
Quy tắc vàng: Script dưới 100 dòng, gọi các tool có sẵn, logic đơn giản → Bash là lựa chọn đúng.
⚠️ Khi KHÔNG nên dùng Bash
| Tình huống | Tại sao không? | Dùng gì thay? |
|---|---|---|
| Script > 100 dòng | Khó đọc, khó debug, khó maintain | Python |
| Xử lý dữ liệu phức tạp | Bash không có data structure | Python / Go |
| Error handling quan trọng | set -e không đủ cho logic phức tạp | Python / Go |
| Cần unit test | Bash không có testing framework tốt | Python |
| Parse JSON/YAML | sed/awk cho JSON = thảm họa | Python + jq |
Nguyên tắc: Nếu bạn đang viết if/else lồng nhau 3 cấp trong Bash — dừng lại và chuyển sang Python.
2. Nền Tảng — Variables, Quoting, Exit Codes 🏗️
2.1 Template bắt buộc cho mọi script
bash
#!/bin/bash
set -euo pipefail # ← LUÔN bắt đầu với dòng này
# Variables
APP_NAME="myapp"
DEPLOY_DIR="/opt/${APP_NAME}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Quoting rules (CRITICAL)
echo "Deploy dir: ${DEPLOY_DIR}" # ✅ Double quotes: biến được expand
echo 'Literal: ${DEPLOY_DIR}' # ✅ Single quotes: không expand
echo "Files: $(ls ${DEPLOY_DIR})" # ✅ Command substitution
# Exit codes
# 0 = success, non-zero = failure
if command -v docker &>/dev/null; then
echo "Docker is installed"
else
echo "Docker is NOT installed" >&2
exit 1
fi2.2 set -euo pipefail — Dòng quan trọng nhất
Đây là dòng code quan trọng nhất trong bất kỳ bash script nào. Không có nó, script của bạn sẽ âm thầm tiếp tục chạy ngay cả khi có lỗi.
| Flag | Tác dụng | Không có nó thì sao? |
|---|---|---|
-e | Dừng ngay khi bất kỳ lệnh nào fail | Script tiếp tục chạy với state hỏng |
-u | Báo lỗi khi dùng biến chưa khai báo | Biến rỗng → hành vi không đoán trước |
-o pipefail | Bắt lỗi trong pipe chain | `cmd1 |
Ví dụ không có set -e:
bash
#!/bin/bash
# THIẾU set -euo pipefail
cd /opt/myapp # ← cd fail vì thư mục không tồn tại
rm -rf ./old_builds/* # ← BÂY GIỜ ĐANG Ở THƯ MỤC KHÁC!
# Xóa nhầm file ở thư mục hiện tại 💀Ví dụ có set -e:
bash
#!/bin/bash
set -euo pipefail
cd /opt/myapp # ← cd fail → script DỪNG NGAY
rm -rf ./old_builds/* # ← Không bao giờ chạy đến đây ✅2.3 Quoting — Luật sống còn
| Cú pháp | Hành vi | Khi nào dùng |
|---|---|---|
"${VAR}" | Expand biến, giữ nguyên khoảng trắng | Mặc định — luôn dùng cái này |
'${VAR}' | Không expand gì cả | Khi cần chuỗi literal |
$(command) | Chạy lệnh, trả về output | Command substitution |
${VAR:-default} | Dùng default nếu VAR rỗng | Giá trị mặc định an toàn |
${VAR:?msg} | Báo lỗi nếu VAR rỗng | Validate bắt buộc |
💡 Quy tắc đơn giản
Luôn dùng double quotes quanh biến: "${MY_VAR}". Nếu bạn viết $MY_VAR không có quotes — bạn đang viết bug chờ phát nổ. Phần Production Incident ở cuối bài sẽ cho bạn thấy hậu quả.
3. Conditionals & Loops — Thực Chiến 🔄
3.1 Health check với retry logic
Đây là pattern bạn sẽ dùng hàng ngày trong production:
bash
check_health() {
local url="$1"
local max_retries="${2:-5}"
local wait_seconds="${3:-3}"
for ((i=1; i<=max_retries; i++)); do
if curl -sf "${url}/health" > /dev/null 2>&1; then
echo "✅ Health check passed (attempt ${i})"
return 0
fi
echo "⏳ Attempt ${i}/${max_retries} failed, waiting ${wait_seconds}s..."
sleep "${wait_seconds}"
done
echo "❌ Health check failed after ${max_retries} attempts" >&2
return 1
}
# Sử dụng
check_health "http://localhost:8000"Phân tích pattern:
| Element | Giải thích |
|---|---|
local | Biến local trong function — tránh ô nhiễm global scope |
${2:-5} | Tham số thứ 2, mặc định 5 nếu không truyền |
curl -sf | -s silent, -f fail on HTTP error |
> /dev/null 2>&1 | Bỏ cả stdout và stderr — chỉ quan tâm exit code |
return 0 / return 1 | 0 = success, 1 = failure (Unix convention) |
3.2 Conditional patterns thường gặp
bash
# Kiểm tra file/directory tồn tại
[[ -f "/etc/myapp/config.yml" ]] || { echo "Config not found!" >&2; exit 1; }
[[ -d "${DEPLOY_DIR}" ]] || mkdir -p "${DEPLOY_DIR}"
# Kiểm tra command có sẵn
command -v docker &>/dev/null || { echo "Docker required!" >&2; exit 1; }
# So sánh chuỗi
if [[ "${ENV}" == "production" ]]; then
echo "⚠️ Running in PRODUCTION"
fi
# So sánh số
if (( DISK_USAGE > 90 )); then
echo "🚨 Disk usage critical: ${DISK_USAGE}%"
fi⚠️ [[ ]] vs [ ]
Luôn dùng [[ ]] (double bracket) thay vì [ ] (single bracket):
[[ ]]hỗ trợ pattern matching, regex, và an toàn hơn với chuỗi rỗng[ ]là POSIX nhưng dễ gây lỗi với khoảng trắng và chuỗi rỗng
bash
# ❌ Sai — lỗi nếu $NAME rỗng
[ $NAME == "admin" ]
# ✅ Đúng — an toàn với mọi giá trị
[[ "${NAME}" == "admin" ]]4. Deploy Script Thực Tế 🚀
Đây là deploy script production-ready — pattern mà các team DevOps thực sự sử dụng:
bash
#!/bin/bash
set -euo pipefail
# === CONFIG ===
APP_NAME="myapp"
DEPLOY_DIR="/opt/${APP_NAME}"
BACKUP_DIR="/opt/backups/${APP_NAME}"
GIT_BRANCH="${1:-main}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# === FUNCTIONS ===
log() { echo "[$(date '+%H:%M:%S')] $*"; }
err() { echo "[$(date '+%H:%M:%S')] ERROR: $*" >&2; }
rollback() {
err "Deploy failed! Rolling back..."
if [[ -d "${BACKUP_DIR}/latest" ]]; then
cp -r "${BACKUP_DIR}/latest/." "${DEPLOY_DIR}/"
sudo systemctl restart "${APP_NAME}"
log "Rollback complete"
else
err "No backup found! Manual intervention required."
fi
exit 1
}
check_health() {
local url="$1"
local max_retries="${2:-5}"
local wait_seconds="${3:-3}"
for ((i=1; i<=max_retries; i++)); do
if curl -sf "${url}/health" > /dev/null 2>&1; then
log "✅ Health check passed (attempt ${i})"
return 0
fi
log "⏳ Attempt ${i}/${max_retries} failed, waiting ${wait_seconds}s..."
sleep "${wait_seconds}"
done
err "Health check failed after ${max_retries} attempts"
return 1
}
# Trap errors for automatic rollback
trap rollback ERR
# === MAIN ===
log "Starting deploy of ${APP_NAME} (branch: ${GIT_BRANCH})"
# 1. Backup current version
log "Backing up current version..."
mkdir -p "${BACKUP_DIR}"
cp -r "${DEPLOY_DIR}" "${BACKUP_DIR}/${TIMESTAMP}"
ln -sfn "${BACKUP_DIR}/${TIMESTAMP}" "${BACKUP_DIR}/latest"
# 2. Pull latest code
log "Pulling latest code..."
cd "${DEPLOY_DIR}"
git fetch origin
git checkout "${GIT_BRANCH}"
git pull origin "${GIT_BRANCH}"
# 3. Install dependencies
log "Installing dependencies..."
source .venv/bin/activate
pip install -r requirements.txt --quiet
# 4. Restart service
log "Restarting service..."
sudo systemctl restart "${APP_NAME}"
# 5. Health check
log "Running health check..."
sleep 3
check_health "http://localhost:8000"
log "✅ Deploy complete!"Phân tích các pattern quan trọng
| Pattern | Dòng | Tại sao quan trọng? |
|---|---|---|
| Backup trước khi thay đổi | cp -r + ln -sfn | Luôn có bản backup để rollback |
trap rollback ERR | Tự động rollback | Bất kỳ lệnh nào fail → rollback ngay |
ln -sfn ... latest | Symlink tới backup mới nhất | Rollback chỉ cần copy từ latest/ |
| Health check sau restart | check_health | Xác nhận service thực sự hoạt động |
| Log có timestamp | log() function | Debug khi deploy fail lúc nửa đêm |
💡 Nguyên tắc deploy an toàn
- Backup trước mọi thay đổi
- Trap để tự động rollback khi lỗi
- Health check sau khi restart — đừng bao giờ tin rằng "restart xong là chạy được"
- Log mọi bước — bạn sẽ cần nó khi debug lúc 3 giờ sáng
5. shellcheck — Linter Cho Bash 🔍
shellcheck là static analysis tool cho bash — nó đọc script của bạn và chỉ ra bug trước khi bạn chạy script đó trên production.
5.1 Cài đặt và sử dụng
bash
# Cài đặt
sudo apt install shellcheck
# Chạy kiểm tra
shellcheck deploy.sh
# Chạy với severity level
shellcheck --severity=warning deploy.sh
# Tích hợp vào CI/CD
shellcheck scripts/*.sh || exit 15.2 Các lỗi phổ biến shellcheck bắt được
bash
# SC2086: Double quote to prevent globbing and word splitting
echo $DEPLOY_DIR # ⚠️ shellcheck cảnh báo
echo "${DEPLOY_DIR}" # ✅ Đã fix
# SC2034: Variable appears unused
UNUSED_VAR="hello" # ⚠️ Khai báo nhưng không dùng
# SC2046: Quote this to prevent word splitting
docker rm $(docker ps -aq) # ⚠️ Cảnh báo
docker rm "$(docker ps -aq)" # ✅ Đã fix
# SC2164: Use cd ... || exit in case cd fails
cd /opt/myapp # ⚠️ Nếu fail thì sao?
cd /opt/myapp || { echo "cd failed"; exit 1; } # ✅ Đã fix💡 Quy tắc bắt buộc
Luôn chạy shellcheck trước khi commit bash script. Nó bắt được 90% bug phổ biến — từ unquoted variables đến logic errors. Nhiều team cấu hình CI/CD reject PR nếu shellcheck báo lỗi.
6. 🔥 Production Incident — Unquoted Variable Xóa Production Data
🔥 INCIDENT THẬT — Mất toàn bộ dữ liệu Production
Tình huống
Một engineer viết cleanup script để dọn file tạm:
bash
#!/bin/bash
CLEANUP_DIR="/opt/myapp/tmp"
rm -rf $CLEANUP_DIR/*Script chạy hàng ngày qua cron job. Hoạt động bình thường 3 tháng liên tục.
Ngày xảy ra sự cố
Team thay đổi cách load environment variables. File .env không được source đúng cách. Biến CLEANUP_DIR bây giờ là chuỗi rỗng.
bash
# CLEANUP_DIR="" (rỗng vì .env không được load)
# Script thực thi:
rm -rf /*
# Kết quả: 💀💀💀 XÓA TOÀN BỘ HỆ THỐNG TỪ ROOTRoot Cause Analysis
| Nguyên nhân | Giải thích |
|---|---|
| Unquoted variable | $CLEANUP_DIR không có quotes → expand thành rỗng |
Thiếu set -u | Không báo lỗi khi biến rỗng |
| Thiếu validation | Không kiểm tra biến có giá trị hợp lệ trước khi rm -rf |
| Thiếu safety check | Không kiểm tra path có phải root / không |
Fix — Script an toàn
bash
#!/bin/bash
set -euo pipefail
# Fail NGAY nếu biến rỗng (${VAR:?} = crash if empty)
CLEANUP_DIR="${CLEANUP_DIR:?ERROR: CLEANUP_DIR not set}"
# Safety check: không cho xóa root hoặc path quá ngắn
if [[ "${CLEANUP_DIR}" == "/" ]] || [[ "${#CLEANUP_DIR}" -lt 5 ]]; then
echo "FATAL: CLEANUP_DIR looks dangerous: '${CLEANUP_DIR}'" >&2
exit 1
fi
# Kiểm tra thư mục tồn tại
[[ -d "${CLEANUP_DIR}" ]] || { echo "Dir not found: ${CLEANUP_DIR}" >&2; exit 1; }
# An toàn: quote biến + dùng :? để fail nếu rỗng
rm -rf "${CLEANUP_DIR:?}"/*
echo "Cleaned up ${CLEANUP_DIR}"Bài học — Ba lớp phòng thủ
set -euo pipefail— Luôn có ở đầu mọi script- Quote mọi biến —
"${VAR}"không bao giờ$VAR - Validate trước khi hành động — Đặc biệt với
rm -rf, kiểm tra path hợp lệ
7. One-Liners Hữu Ích ⚡
Những one-liner này bạn sẽ dùng hàng ngày khi quản trị hệ thống:
7.1 Quản lý Disk & Files
bash
# Tìm file lớn > 100MB
find /var/log -type f -size +100M -exec ls -lh {} \;
# Top 10 thư mục chiếm nhiều disk nhất
du -sh /var/* | sort -rh | head -10
# Xóa file log cũ hơn 30 ngày
find /var/log/myapp -name "*.log" -mtime +30 -delete7.2 Monitoring & Debugging
bash
# Theo dõi log real-time, chỉ hiện ERROR
tail -f /var/log/myapp/app.log | grep --line-buffered ERROR
# Đếm connections theo IP
ss -tn | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
# Quick HTTP health check (chỉ trả về status code)
curl -sf -o /dev/null -w "%{http_code}" http://localhost:8000/health
# Kiểm tra disk usage, cảnh báo nếu > 80%
df -h | awk '$5+0 > 80 {print "⚠️ " $1 " is " $5 " full"}'7.3 Process Management
bash
# Tìm process ngốn CPU nhất
ps aux --sort=-%cpu | head -5
# Tìm process ngốn RAM nhất
ps aux --sort=-%mem | head -5
# Kill tất cả process của một user
pkill -u deploybot
# Kiểm tra port đang được dùng
ss -tlnp | grep ":8080"8. Bài Tập Luyện Tập 📝
Bài 1 — Spot the Bug 🐛
Script sau có 3 lỗi nghiêm trọng. Tìm và sửa tất cả:
bash
#!/bin/bash
BACKUP_DIR=/opt/backups
APP_DIR=/opt/myapp
cd $APP_DIR
tar czf $BACKUP_DIR/backup_$(date +%Y%m%d).tar.gz .
echo "Backup complete: $BACKUP_FILE"💡 Gợi ý
- Thiếu gì ở đầu script?
- Biến nào không được quote?
- Biến nào chưa khai báo mà đã dùng?
✅ Lời giải
Lỗi 1: Thiếu set -euo pipefail — nếu cd fail, tar sẽ chạy ở thư mục sai
Lỗi 2: $APP_DIR và $BACKUP_DIR không được quote — nguy hiểm nếu path có khoảng trắng
Lỗi 3: $BACKUP_FILE chưa bao giờ khai báo — với set -u sẽ crash, không có thì in chuỗi rỗng
Script đã fix:
bash
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/opt/backups"
APP_DIR="/opt/myapp"
BACKUP_FILE="${BACKUP_DIR}/backup_$(date +%Y%m%d).tar.gz"
cd "${APP_DIR}" || { echo "Cannot cd to ${APP_DIR}" >&2; exit 1; }
tar czf "${BACKUP_FILE}" .
echo "Backup complete: ${BACKUP_FILE}"Bài 2 — Viết Health Check Function
Viết một function wait_for_service nhận 2 tham số:
- URL của service
- Timeout tính bằng giây (mặc định: 30)
Function phải:
- Thử curl mỗi 2 giây
- In trạng thái mỗi lần thử
- Return 0 nếu service lên, return 1 nếu timeout
✅ Lời giải
bash
wait_for_service() {
local url="${1:?Usage: wait_for_service URL [TIMEOUT]}"
local timeout="${2:-30}"
local interval=2
local elapsed=0
echo "Waiting for ${url} (timeout: ${timeout}s)..."
while (( elapsed < timeout )); do
if curl -sf "${url}" > /dev/null 2>&1; then
echo "✅ Service is up after ${elapsed}s"
return 0
fi
echo "⏳ ${elapsed}s / ${timeout}s — not ready yet..."
sleep "${interval}"
(( elapsed += interval ))
done
echo "❌ Timeout after ${timeout}s — service not responding" >&2
return 1
}
# Sử dụng
wait_for_service "http://localhost:8000/health" 60Điểm quan trọng:
${1:?...}— crash với message rõ ràng nếu thiếu tham sốlocal— tránh ô nhiễm global scope- Exit code chuẩn Unix: 0 = success, 1 = failure
9. Production Anti-Pattern — "Script Bash 500 Dòng" 🚫
⚠️ Cạm bẫy
Tình huống
Team maintain một deploy script Bash 500 dòng với:
- Nested
if/elsesâu 4 cấp - "Parse" JSON bằng
sedvàawk - Array dùng làm "database" tạm
- Không có test, không có error handling nhất quán
- Mỗi lần API response format thay đổi → script vỡ
Tại sao đây là thảm họa
bash
# 💀 Parse JSON bằng sed — VỠ ngay khi format thay đổi
STATUS=$(curl -s http://api/status | sed 's/.*"status":"\([^"]*\)".*/\1/')
# 💀 Nested if/else — không ai hiểu logic
if [[ "$STATUS" == "running" ]]; then
if [[ "$MEMORY" -gt 80 ]]; then
if [[ "$CONNECTIONS" -gt 1000 ]]; then
# 3 cấp sâu — bạn đã lạc rồi
fi
fi
fiGiải pháp: Chuyển sang Python khi > 100 dòng
python
#!/usr/bin/env python3
"""Deploy script — readable, testable, maintainable."""
import subprocess
import requests
import sys
def check_health(url: str, retries: int = 5) -> bool:
"""Health check with retry — clear, testable."""
for i in range(retries):
try:
resp = requests.get(f"{url}/health", timeout=5)
if resp.status_code == 200:
print(f"✅ Health check passed (attempt {i+1})")
return True
except requests.RequestException:
pass
print(f"⏳ Attempt {i+1}/{retries} failed...")
return False
def run_command(cmd: str) -> None:
"""Run shell command with proper error handling."""
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ Command failed: {cmd}", file=sys.stderr)
print(result.stderr, file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
run_command("git pull origin main")
run_command("pip install -r requirements.txt --quiet")
run_command("sudo systemctl restart myapp")
if not check_health("http://localhost:8000"):
print("❌ Deploy failed — rolling back", file=sys.stderr)
run_command("sudo systemctl restart myapp")
sys.exit(1)
print("✅ Deploy complete!")Python thắng Bash khi:
- Cần parse JSON/YAML →
json.loads()thay vìsed - Cần error handling phức tạp →
try/exceptthay vì trap - Cần unit test →
pytestcho deploy logic - Script dài hơn 100 dòng → đọc được, maintain được
10. Checklist — Bash Script An Toàn ✅
✅ Checklist triển khai
Bắt buộc cho mọi script
- [ ] Có
#!/bin/bashở dòng đầu - [ ] Có
set -euo pipefailngay sau shebang - [ ] Mọi biến được quote:
"${VAR}"không phải$VAR - [ ] Chạy
shellchecktrước khi commit
Destructive operations (rm, mv, overwrite)
- [ ] Validate biến không rỗng trước
rm -rf - [ ] Dùng
${VAR:?}cho biến bắt buộc - [ ] Backup trước khi thay đổi
- [ ] Có
trapđể cleanup/rollback khi lỗi
Functions
- [ ] Dùng
localcho biến trong function - [ ] Validate parameters với
${1:?message} - [ ] Return 0 cho success, non-zero cho failure
Khi nào chuyển sang Python?
- [ ] Script > 100 dòng → Python
- [ ] Parse JSON/YAML → Python + jq
- [ ] Cần unit test → Python + pytest
- [ ] Logic phức tạp (nested if > 2 cấp) → Python
🧠 Quiz
Câu 1: Tại sao set -euo pipefail quan trọng?
- [ ] A) Tăng tốc độ chạy script
- [x] B) Dừng script khi có lỗi, biến undefined, hoặc lỗi trong pipe
- [ ] C) Tự động backup trước khi chạy
- [ ] D) Cho phép chạy script không cần quyền root
💡 Giải thích:
-edừng khi lỗi,-ubáo biến undefined,-o pipefailbắt lỗi trong pipe. Không có nó, script tiếp tục chạy với state hỏng — dẫn tới data loss hoặc hành vi không đoán trước.
Câu 2: Điều gì xảy ra khi CLEANUP_DIR rỗng và chạy rm -rf $CLEANUP_DIR/*?
- [ ] A) Lệnh
rmbáo lỗi và dừng - [ ] B) Không xóa gì cả
- [x] C) Chạy
rm -rf /*— xóa toàn bộ hệ thống từ root - [ ] D) Xóa thư mục hiện tại
💡 Giải thích: Khi
CLEANUP_DIRrỗng và không có quotes,$CLEANUP_DIR/*expand thành/*— tức là toàn bộ filesystem từ root. Đây là lý do phải quote mọi biến và dùngset -u+${VAR:?}.
Câu 3: Khi nào nên chuyển từ Bash sang Python?
- [ ] A) Khi script dài hơn 10 dòng
- [ ] B) Khi cần chạy trên nhiều server
- [x] C) Khi script > 100 dòng, cần parse JSON, hoặc cần unit test
- [ ] D) Khi script cần quyền root
💡 Giải thích: Bash tốt cho automation ngắn gọn (< 100 dòng). Khi logic phức tạp hơn, Python mang lại error handling tốt hơn, data structure phong phú, testing framework, và khả năng maintain lâu dài.