BÀI VIẾT // [DEVOPS] Best Practice trong Bash: Tổng hợp kỹ thuật làm chủ script
Best Practice trong Bash: Tổng hợp kỹ thuật làm chủ script

Best Practice trong Bash: Tổng hợp kỹ thuật làm chủ script

Tổng hợp best practice trong Bash DevOps: shebang, set -euo pipefail, modular functions, logging, config management, exit codes, shellcheck, và test với bats.

DEVOPS BASH

Best Practice tổng hợp trong Bash

Sau 19 bài trong series, chúng ta đã đi qua từ cơ bản đến nâng cao — từ variables, loops, functions, đến security, cloud CLI. Bài viết này sẽ tổng hợp tất cả best practice để bạn viết Bash scripts chuyên nghiệp, dễ bảo trì, và an toàn trong DevOps.

Tại sao cần best practice? Script Bash chạy tự động trên server, trong pipeline, và đôi khi do nhiều người maintain. Nếu không có quy tắc, scripts sẽ trở thành “spaghetti code” — khó debug, dễ lỗi, và tiềm ẩn lỗ hổng bảo mật.


Script template chuẩn

Mọi script mới nên bắt đầu với template này:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/env bash
# ==============================================================================
# Script : <tên script>
# Mục đích: <mô tả ngắn gọn>
# Usage: <script.sh> [options] <args>
# ==============================================================================

set -euo pipefail

# ===== CONSTANTS =====
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"

# ===== CONFIGURATION =====
readonly LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

# ===== FUNCTIONS =====
log() {
    local level="$1"
    local message="$2"
    echo "[$TIMESTAMP] [$level] $message" | tee -a "$LOG_FILE"
}

usage() {
    cat << EOF
Usage: $SCRIPT_NAME <option> <argument>

Options:
    -h, --help      Show this help message
    -v, --verbose   Enable verbose output

Examples:
    $SCRIPT_NAME --verbose server01
EOF
    exit "${1:-0}"
}

cleanup() {
    local exit_code=$?
    if [[ $exit_code -ne 0 ]]; then
        log "ERROR" "Script failed with exit code $exit_code"
    fi
    # Cleanup code here
    return $exit_code
}

# ===== MAIN =====
main() {
    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help) usage 0 ;;
            -v|--verbose) VERBOSE=true; shift ;;
            *) break ;;
        esac
    done

    # Validate required arguments
    if [[ $# -lt 1 ]]; then
        log "ERROR" "Missing required argument"
        usage 1
    fi

    log "INFO" "Script started"
    # Main logic here
    log "INFO" "Script completed"
}

# ===== ENTRY POINT =====
trap cleanup EXIT
main "$@"

Tại sao template này?

ComponentLý do
#!/usr/bin/env bashPortable, tìm bash trong PATH
set -euo pipefailExit on error, undefined vars, pipe failures
readonlyTránh thay đổi constants
function log()Logging chuẩn, dễ debug
usage()Document script usage
cleanup()Trap EXIT để cleanup
main()Entry point rõ ràng

Shebang đúng cách

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# KHÔNG dùng
#!/bin/bash           # Có thể không đúng path

# NÊN dùng
#!/usr/bin/env bash   # Portable, hoạt động trên mọi hệ thống

# Nếu script cần root
#!/usr/bin/env bash
if [[ $EUID -ne 0 ]]; then
    echo "This script must be run as root" >&2
    exit 1
fi

Error handling

set -euo pipefail

1
2
3
4
5
6
# Luôn đặt ở đầu script
set -euo pipefail

# -e: Thoát ngay khi có lỗi
# -u: Báo lỗi khi dùng biến chưa định nghĩa
# -o pipefail: Thoát nếu bất kỳ command trong pipe fail

Trap errors

1
2
3
4
5
6
7
8
# Trap ERR
trap 'echo "Error at line $LINENO" >&2' ERR

# Trap EXIT (luôn chạy, dù thành công hay thất bại)
trap cleanup EXIT

# Trap specific signals
trap 'echo "Interrupted"; exit 130' INT TERM

Exit codes có ý nghĩa

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Trả về exit code có nghĩa
exit 0    # Success
exit 1    # General error
exit 2    # Misuse of shell command
exit 126  # Permission problem
exit 127  # Command not found
exit 128+n  # Fatal error signal "n"

# Trong script
validate_input() {
    if [[ -z "$1" ]]; then
        echo "ERROR: Missing argument" >&2
        return 1
    fi
    return 0
}

Logging

Log với levels

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Constants
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color

log_info() {
    echo -e "${GREEN}[$(date '+%H:%M:%S')] [INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[$(date '+%H:%M:%S')] [WARN]${NC} $1" >&2
}

log_error() {
    echo -e "${RED}[$(date '+%H:%M:%S')] [ERROR]${NC} $1" >&2
}

# Usage
log_info "Starting deployment"
log_warn "Disk usage high"
log_error "Connection failed"

Log to file

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
log() {
    local level="$1"
    local message="$2"
    local log_file="${LOG_FILE:-/tmp/${SCRIPT_NAME}.log}"

    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a "$log_file"
}

# Usage
log "INFO" "Deployment started"
log "ERROR" "Failed to connect to database"

Configuration management

Tách config khỏi code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# BAD — hardcode trong script
DATABASE_URL="postgres://user:pass@localhost:5432/db"
API_KEY="sk-1234567890"

# GOOD — load từ config file
readonly CONFIG_FILE="${CONFIG_FILE:-${SCRIPT_DIR}/config.env}"

if [[ -f "$CONFIG_FILE" ]]; then
    set -a
    source "$CONFIG_FILE"
    set +a
else
    echo "ERROR: Config file not found: $CONFIG_FILE" >&2
    exit 1
fi

# Validate required config
: "${DATABASE_URL:?DATABASE_URL is required}"
: "${API_KEY:?API_KEY is required}"

Config file template

1
2
3
4
5
6
# config.env (chmod 600)
APP_NAME=myapp
DATABASE_URL=postgres://user:pass@localhost:5432/db
REDIS_URL=redis://localhost:6379
LOG_LEVEL=info
SLACK_WEBHOOK=https://hooks.slack.com/services/xxx

Functions

Function structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Function với local variables
deploy_app() {
    local environment="$1"
    local version="$2"

    # Validate inputs
    if [[ -z "$environment" || -z "$version" ]]; then
        echo "Usage: deploy_app <environment> <version>" >&2
        return 1
    fi

    # Local scope — không conflict với global vars
    local deploy_dir="/opt/apps/${environment}"

    # Logic
    echo "Deploying $version to $environment..."
    # Deploy logic here

    return 0
}

# Function với return value qua stdout
get_latest_version() {
    local app_name="$1"
    # Trả value qua stdout, KHÔNG dùng return
    curl -s "https://api.example.com/apps/${app_name}/version"
}

# Usage
VERSION=$(get_latest_version "myapp")
deploy_app "production" "$VERSION"

Source external functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# lib/logger.sh
log_info() { echo "[INFO] $1"; }
log_error() { echo "[ERROR] $1" >&2; }

# lib/utils.sh
validate_email() { [[ "$1" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; }

# Script chính
source "${SCRIPT_DIR}/lib/logger.sh"
source "${SCRIPT_DIR}/lib/utils.sh"

log_info "Starting script"
validate_email "$USER_EMAIL" || { log_error "Invalid email"; exit 1; }

Input validation

Validate arguments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Check required args
if [[ $# -lt 2 ]]; then
    echo "Usage: $0 <hostname> <port>" >&2
    exit 1
fi

HOSTNAME="$1"
PORT="$2"

# Validate hostname
if [[ ! "$HOSTNAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then
    echo "ERROR: Invalid hostname: $HOSTNAME" >&2
    exit 1
fi

# Validate port
if [[ ! "$PORT" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then
    echo "ERROR: Invalid port: $PORT (must be 1-65535)" >&2
    exit 1
fi

Validate files

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require_file() {
    local file="$1"
    local description="${2:-file}"

    if [[ ! -f "$file" ]]; then
        echo "ERROR: $description not found: $file" >&2
        return 1
    fi
    return 0
}

require_executable() {
    local cmd="$1"
    if ! command -v "$cmd" &>/dev/null; then
        echo "ERROR: Required command not found: $cmd" >&2
        return 1
    fi
    return 0
}

# Usage
require_file ".env" "Config file"
require_executable "docker"
require_executable "kubectl"

Variable quoting

Luôn quote khi expand

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# BAD — word splitting, globbing
rm $FILE
cp $SOURCE $DEST
echo $VARIABLE

# GOOD — always quote
rm "$FILE"
cp "$SOURCE" "$DEST"
echo "$VARIABLE"

# Exception: arrays và [[ ]]
for file in "${FILES[@]}"; do
    echo "$file"
done

if [[ $var == "pattern" ]]; then
    echo "matched"
fi

Command substitution

1
2
3
4
5
6
7
# BAD
cd $(dirname $0)
files=$(ls)

# GOOD
cd "$(dirname "$0")"
files=$(ls)  # ls output thường safe, nhưng vẫn nên cẩn thận

Testing với bats

Cài đặt

1
2
3
4
5
6
7
8
# Clone và install
git clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/local

# Hoặc dùng package manager
brew install bats  # macOS
apt-get install bats  # Debian/Ubuntu

Viết tests

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env bats
# tests/test_deploy.sh

# Setup — chạy trước mỗi test
setup() {
    export SCRIPT_DIR="$(dirname "$BATS_TEST_FILENAME")/.."
    export TEST_ENV="staging"
}

# Teardown — chạy sau mỗi test
teardown() {
    # Cleanup
    rm -rf /tmp/test_deploy_* 2>/dev/null || true
}

# Test case
@test "deploy_app returns 0 on success" {
    run bash "$SCRIPT_DIR/deploy.sh" "$TEST_ENV" "1.0.0"
    [ "$status" -eq 0 ]
    [[ "$output" == *"Deploying"* ]]
}

@test "deploy_app fails with invalid environment" {
    run bash "$SCRIPT_DIR/deploy.sh" "invalid-env" "1.0.0"
    [ "$status" -eq 1 ]
    [[ "$output" == *"Invalid environment"* ]]
}

@test "validate_port rejects non-numeric" {
    source "$SCRIPT_DIR/lib/utils.sh"
    run validate_port "abc"
    [ "$status" -eq 1 ]
}

@test "validate_port rejects out of range" {
    source "$SCRIPT_DIR/lib/utils.sh"
    run validate_port "99999"
    [ "$status" -eq 1 ]
}

Chạy tests

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Chạy tất cả tests
bats tests/

# Chạy một file
bats tests/test_deploy.sh

# Output
 ✓ deploy_app returns 0 on success
 ✓ deploy_app fails with invalid environment
 ✓ validate_port rejects non-numeric
 ✓ validate_port rejects out of range

4 tests, 0 failures

Shellcheck integration

Setup

1
2
3
4
5
6
7
8
9
# Install
brew install shellcheck  # macOS
apt-get install shellcheck  # Debian/Ubuntu

# Check script
shellcheck script.sh

# Fix all issues
shellcheck -f diff script.sh | patch -p1

Common fixes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# SC2086: Double quote variables
# BAD
echo $1
# GOOD
echo "$1"

# SC2046: Quote command substitution
# BAD
cd $(dirname $0)
# GOOD
cd "$(dirname "$0")"

# SC2034: Remove unused variables
# SC2155: Declare and assign separately
# BAD
local output=$(command)
# GOOD
local output
output=$(command)

# SC2164: Use cd ... || exit in case cd fails
# BAD
cd /some/dir
# GOOD
cd /some/dir || exit 1

CI/CD integration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# GitHub Actions
lint:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Install shellcheck
      run: sudo apt-get install -y shellcheck
    - name: Run shellcheck
      run: shellcheck -s bash scripts/*.sh lib/*.sh

# pre-commit hook
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.9.0
    hooks:
      - id: shellcheck

Ví dụ thực hành: Production-ready script

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env bash
# ==============================================================================
# Script : health-checker.sh
# Purpose: System health check with alerting
# Usage: health-checker.sh [--webhook URL] [--threshold PERCENT]
# ==============================================================================

set -euo pipefail

# ===== CONSTANTS =====
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
readonly TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"

# ===== DEFAULTS =====
DISK_THRESHOLD=80
MEMORY_THRESHOLD=90
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
LOG_FILE="${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}"

# ===== FUNCTIONS =====
log() {
    local level="$1"
    local message="$2"
    echo "[$TIMESTAMP] [$level] $message" | tee -a "$LOG_FILE"
}

usage() {
    cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]

Options:
    -w, --webhook URL       Slack webhook URL
    -d, --disk PERCENT      Disk threshold (default: 80)
    -m, --memory PERCENT    Memory threshold (default: 90)
    -h, --help              Show this help

Examples:
    $SCRIPT_NAME --webhook https://hooks.slack.com/xxx --disk 90
EOF
    exit "${1:-0}"
}

send_alert() {
    local message="$1"
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -s -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"[$SCRIPT_NAME] $message\"}" "$SLACK_WEBHOOK" >/dev/null 2>&1
    fi
}

check_disk() {
    local usage
    usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')

    if (( usage > DISK_THRESHOLD )); then
        log "WARN" "Disk usage ${usage}% exceeds threshold ${DISK_THRESHOLD}%"
        send_alert "Disk usage ${usage}% on $(hostname)"
        return 1
    fi
    log "INFO" "Disk usage: ${usage}%"
    return 0
}

check_memory() {
    local usage
    usage=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')

    if (( usage > MEMORY_THRESHOLD )); then
        log "WARN" "Memory usage ${usage}% exceeds threshold ${MEMORY_THRESHOLD}%"
        send_alert "Memory usage ${usage}% on $(hostname)"
        return 1
    fi
    log "INFO" "Memory usage: ${usage}%"
    return 0
}

cleanup() {
    local exit_code=$?
    if [[ $exit_code -ne 0 ]]; then
        log "ERROR" "Script failed with exit code $exit_code"
    fi
    return $exit_code
}

# ===== MAIN =====
main() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -w|--webhook) SLACK_WEBHOOK="$2"; shift 2 ;;
            -d|--disk) DISK_THRESHOLD="$2"; shift 2 ;;
            -m|--memory) MEMORY_THRESHOLD="$2"; shift 2 ;;
            -h|--help) usage 0 ;;
            *) log "ERROR" "Unknown option: $1"; usage 1 ;;
        esac
    done

    log "INFO" "=== Health Check Started ==="

    local failed=0
    check_disk || ((failed++))
    check_memory || ((failed++))

    if (( failed > 0 )); then
        log "WARN" "$failed check(s) failed"
        exit 1
    fi

    log "INFO" "=== All checks passed ==="
}

# ===== ENTRY POINT =====
trap cleanup EXIT
main "$@"

Test script

1
2
3
4
5
# Chạy test
$ bash -n health-checker.sh  # Check syntax
$ shellcheck health-checker.sh  # Lint
$ bats tests/test_health.sh  # Run tests
$ ./health-checker.sh --help  # Test help output

Ghi chú triển khai

  • Bắt đầu với template: Luôn dùng template chuẩn khi viết script mới. Tiết kiệm thời gian và đảm bảo consistency.
  • Shellcheck trong CI: Bắt buộc shellcheck pass trước khi merge. Catch bugs sớm.
  • Test trước khi deploy: Viết bats tests cho critical scripts. Đặc biệt cho automation chạy trên production.
  • Document --help: Mọi script nên có --help output rõ ràng. Team members sẽ không cần hỏi.
  • Logging nhất quán: Dùng cùng format log trên tất cả scripts. Dễ aggregate và search.
  • Config tách riêng: Không hardcode config trong script. Dùng .env hoặc config files.
  • Review process: Khi review Bash scripts, check cho: hardcoded secrets, missing validation, unused variables, loose permissions.

Lời kết

Series 20 bài đã tổng hợp từ cơ bản đến nâng cao — từ variables, loops, functions, đến security, cloud automation, và testing.

Best practice không phải là rules cứng nhắc mà là guidelines để viết scripts:

  • Dễ đọc: Comment rõ, naming convention, modular functions
  • Dễ bảo trì: Error handling, logging, config management
  • An toàn: Input validation, secrets management, shellcheck
  • Dễ test: Bats tests, CI/CD integration

Áp dụng những nguyên tắc này, bạn sẽ viết Bash scripts chuyên nghiệp và tự tin trong DevOps工作流.

THẢO LUẬN & BÌNH LUẬN