Featured image of post Quản lý biến và môi trường trong Bash cho DevOps

Quản lý biến và môi trường trong Bash cho DevOps

Hướng dẫn quản lý biến trong Bash: biến cục bộ, export, env, set/unset, file .env, getopts, IFS và các biến đặc biệt thường dùng khi viết script DevOps.

Vì sao biến môi trường quan trọng trong Bash

bài trước chúng ta đã tách logic thành function để script dễ tái sử dụng hơn. Khi script bắt đầu chạy trong nhiều môi trường — local, staging, production, CI/CD runner — vấn đề tiếp theo là: cấu hình lấy từ đâu và truyền vào như thế nào?

Biến trong Bash giúp lưu giá trị tạm thời trong script. Biến môi trường giúp truyền cấu hình cho process con như docker, kubectl, aws, curl hoặc ứng dụng mà script khởi chạy. Nếu quản lý biến không rõ ràng, script rất dễ deploy nhầm môi trường, lộ secret, hoặc chạy sai vì thiếu config.


Biến cục bộ và cách gán giá trị

Cú pháp gán biến trong Bash không có khoảng trắng quanh dấu =:

1
2
3
APP_NAME="blog-api"
ENVIRONMENT="staging"
RETRY_COUNT=3

Khi đọc biến, dùng $VAR hoặc ${VAR}. Trong script thực tế, ${VAR} thường rõ ràng hơn khi nối chuỗi:

1
2
echo "Deploying ${APP_NAME} to ${ENVIRONMENT}"
LOG_FILE="/var/log/${APP_NAME}.log"

Một lỗi rất phổ biến là thêm khoảng trắng:

1
APP_NAME = "blog-api"   # sai: Bash hiểu APP_NAME là command

Khi giá trị có khoảng trắng hoặc ký tự đặc biệt, luôn quote biến:

1
2
BACKUP_DIR="/opt/backups/blog api"
mkdir -p "${BACKUP_DIR}"

Không quote biến có thể làm path bị tách thành nhiều argument, gây lỗi khó đoán trong script vận hành.


export: khi nào biến trở thành environment variable

Biến Bash bình thường chỉ tồn tại trong shell hiện tại. Process con không tự thấy biến đó:

1
2
APP_ENV="staging"
bash -c 'echo "APP_ENV=$APP_ENV"'   # rỗng

Muốn process con đọc được, dùng export:

1
2
export APP_ENV="staging"
bash -c 'echo "APP_ENV=$APP_ENV"'   # APP_ENV=staging

Trong DevOps, export thường dùng khi gọi CLI hoặc chạy ứng dụng:

1
2
3
4
export AWS_PROFILE="staging"
export KUBECONFIG="${HOME}/.kube/staging-config"

kubectl get pods

Bạn cũng có thể truyền biến chỉ cho một command:

1
APP_ENV="staging" ./run-migration.sh

Cách này gọn và an toàn hơn nếu biến chỉ cần tồn tại cho đúng một lệnh.


Xem, đặt và xóa biến với env, set, unset

Một vài command hữu ích khi debug môi trường chạy script:

1
2
3
4
env                  # in environment variables
printenv APP_ENV     # in một biến môi trường cụ thể
set                  # in cả shell variables, functions và environment variables
unset APP_ENV        # xóa biến

env phù hợp để kiểm tra process con sẽ thấy gì. set chi tiết hơn, nhưng output dài và có thể chứa dữ liệu nhạy cảm, nên hạn chế paste nguyên output vào ticket hoặc log.

Ví dụ kiểm tra biến bắt buộc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
require_env() {
  local name="$1"

  if [[ -z "${!name:-}" ]]; then
    echo "ERROR: missing required env ${name}" >&2
    return 1
  fi
}

require_env "APP_ENV"
require_env "DATABASE_URL"

Cú pháp ${!name} là indirect expansion: nếu name="APP_ENV", Bash sẽ đọc giá trị của biến APP_ENV.


Đọc cấu hình từ file .env

File .env giúp tách config khỏi code:

1
2
3
APP_ENV=staging
APP_PORT=8080
BACKUP_DIR=/opt/backups/blog

Cách đơn giản để load file:

1
2
3
set -a
source .env
set +a

set -a làm các biến được gán sau đó tự động export, nên process con có thể đọc được.

Tuy nhiên, source .env sẽ thực thi nội dung file như Bash code. Vì vậy chỉ dùng với file bạn kiểm soát, không source file upload từ user hoặc nguồn không tin cậy.

Một pattern an toàn hơn cho script deploy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ENV_FILE="${1:-.env}"

if [[ ! -f "${ENV_FILE}" ]]; then
  echo "ERROR: env file not found: ${ENV_FILE}" >&2
  exit 1
fi

set -a
source "${ENV_FILE}"
set +a

: "${APP_ENV:?APP_ENV is required}"
: "${APP_PORT:?APP_PORT is required}"

Dòng : "${VAR:?message}" khiến script dừng ngay nếu biến chưa được set hoặc rỗng. Đây là cách fail-fast rất hữu ích trước khi chạy deploy.


Nhận tham số CLI bằng getopts

Không phải cấu hình nào cũng nên nằm trong .env. Những giá trị thay đổi theo lần chạy, như môi trường đích hoặc chế độ dry-run, nên nhận qua flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash
set -euo pipefail

ENVIRONMENT="staging"
DRY_RUN="false"

while getopts ":e:n" opt; do
  case "${opt}" in
    e)
      ENVIRONMENT="${OPTARG}"
      ;;
    n)
      DRY_RUN="true"
      ;;
    ?)
      echo "Usage: $0 [-e environment] [-n]" >&2
      exit 2
      ;;
  esac
done

echo "environment=${ENVIRONMENT} dry_run=${DRY_RUN}"

Chạy thử:

1
./deploy.sh -e production -n

getopts phù hợp cho option ngắn như -e production, -n. Nếu cần long option như --environment, bạn có thể tự parse bằng case, nhưng nên giữ format đơn giản để script dễ bảo trì.


$IFS: kiểm soát cách Bash tách chuỗi

IFS là Internal Field Separator — tập ký tự Bash dùng để tách word khi xử lý một số expansion và lệnh read. Mặc định gồm space, tab và newline.

Khi đọc file từng dòng, dùng pattern này để giữ nguyên khoảng trắng đầu/cuối và không xử lý backslash đặc biệt:

1
2
3
4
5
while IFS= read -r server; do
  [[ -z "${server}" || "${server}" == \#* ]] && continue
  echo "Checking ${server}"
  ssh "deploy@${server}" "hostname && uptime"
done < servers.txt

Khi tách chuỗi CSV đơn giản:

1
2
3
4
5
6
TARGETS="web-01,web-02,web-03"
IFS=',' read -r -a servers <<< "${TARGETS}"

for server in "${servers[@]}"; do
  echo "Deploy to ${server}"
done

Tránh đổi IFS global nếu không cần. Nếu phải đổi, hãy giới hạn trong một dòng hoặc một scope nhỏ để không làm hỏng phần khác của script.


Các biến đặc biệt thường gặp

Bash có nhiều biến đặc biệt rất hữu ích khi viết script vận hành:

BiếnÝ nghĩa
$0Tên script đang chạy
$1, $2Tham số vị trí
$@Toàn bộ tham số, nên dùng "$@"
$#Số lượng tham số
$?Exit code của command vừa chạy
$$PID của shell hiện tại
$!PID của background process gần nhất
${BASH_SOURCE[0]}Đường dẫn file script hiện tại trong Bash

Ví dụ dùng $!wait để theo dõi background job:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
./long-healthcheck.sh &
healthcheck_pid=$!

echo "Healthcheck PID: ${healthcheck_pid}"

if wait "${healthcheck_pid}"; then
  echo "Healthcheck passed"
else
  echo "Healthcheck failed" >&2
  exit 1
fi

Ví dụ lấy thư mục chứa script, không phụ thuộc bạn chạy script từ đâu:

1
2
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/log.sh"

Pattern này rất hữu ích khi script cần load file config hoặc thư viện nằm cạnh nó.


Ví dụ DevOps: script deploy đọc .env, nhận flag và validate biến

Ví dụ dưới đây mô phỏng một script deploy nhỏ. Script nhận môi trường qua -e, hỗ trợ dry-run bằng -n, load file .env.<environment>, validate biến bắt buộc rồi chạy lệnh deploy.

 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
#!/usr/bin/env bash
set -euo pipefail

ENVIRONMENT="staging"
DRY_RUN="false"

usage() {
  echo "Usage: $0 [-e staging|production] [-n]" >&2
}

while getopts ":e:n" opt; do
  case "${opt}" in
    e) ENVIRONMENT="${OPTARG}" ;;
    n) DRY_RUN="true" ;;
    ?) usage; exit 2 ;;
  esac
done

case "${ENVIRONMENT}" in
  staging|production) ;;
  *)
    echo "ERROR: unsupported environment: ${ENVIRONMENT}" >&2
    exit 2
    ;;
esac

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${SCRIPT_DIR}/.env.${ENVIRONMENT}"

if [[ ! -f "${ENV_FILE}" ]]; then
  echo "ERROR: env file not found: ${ENV_FILE}" >&2
  exit 1
fi

set -a
source "${ENV_FILE}"
set +a

: "${APP_NAME:?APP_NAME is required}"
: "${IMAGE_TAG:?IMAGE_TAG is required}"
: "${DEPLOY_HOST:?DEPLOY_HOST is required}"

cmd=(ssh "deploy@${DEPLOY_HOST}" "docker service update --image ${APP_NAME}:${IMAGE_TAG} ${APP_NAME}")

printf 'Environment: %s\n' "${ENVIRONMENT}"
printf 'Command: %q ' "${cmd[@]}"
printf '\n'

if [[ "${DRY_RUN}" == "true" ]]; then
  echo "Dry-run mode: command was not executed"
  exit 0
fi

"${cmd[@]}"

File .env.staging có thể như sau:

1
2
3
APP_NAME=blog-api
IMAGE_TAG=2026.06.24
DEPLOY_HOST=webserver-01

Điểm quan trọng trong ví dụ:

  • Không hardcode host/image tag trong script.
  • Validate môi trường hợp lệ trước khi source file.
  • Dùng set -a để export config cho process con nếu cần.
  • Dùng dry-run để kiểm tra lệnh trước khi chạy thật.
  • Quote biến khi dựng path và argument.

Sai sót thường gặp

  • Gán biến có khoảng trắng quanh =: NAME = value là sai trong Bash.
  • Không quote biến: Path có khoảng trắng hoặc ký tự glob như * có thể làm script chạy sai.
  • Export quá nhiều: Chỉ export biến mà process con cần đọc.
  • Source .env không tin cậy: .env là Bash code khi dùng source, có thể thực thi lệnh.
  • Đưa secret vào log: Tránh set -x quanh đoạn xử lý token/password.
  • Dùng $1 trực tiếp với set -u: Hãy dùng ${1:-} hoặc validate trước.
  • Đổi IFS global: Có thể làm hỏng loop hoặc parse argument ở đoạn sau.

Ghi chú triển khai

  • Khi áp dụng vào dự án của bạn, hãy phân loại cấu hình:
    • Giá trị cố định theo môi trường → .env.staging, .env.production hoặc config file riêng.
    • Giá trị thay đổi theo lần chạy → flag CLI qua getopts.
    • Secret → biến môi trường từ CI/CD secret store hoặc vault, không commit vào git.
  • Best practices:
    • Bật set -u để phát hiện biến chưa khai báo, nhưng dùng ${VAR:-} khi biến optional.
    • Validate biến bắt buộc sớm bằng : "${VAR:?message}".
    • Dùng printenv VAR để debug một biến thay vì in toàn bộ environment.
    • Prefix biến theo app, ví dụ BLOG_APP_ENV, để tránh trùng tên.
    • Không commit file .env chứa secret; chỉ commit .env.example.
  • Troubleshooting:
    • Process con không thấy biến? → Kiểm tra bạn đã export chưa.
    • Script chạy đúng local nhưng sai cron/CI? → Kiểm tra PATH, working directory và biến môi trường runner cung cấp.
    • .env không load đúng? → Kiểm tra file có syntax Bash hợp lệ, không có khoảng trắng quanh =.

🎯 Lời kết

Quản lý biến tốt giúp Bash script chạy ổn định hơn giữa nhiều môi trường. Hãy giữ code và config tách biệt, validate biến bắt buộc trước khi thao tác thật, quote biến khi dùng trong command, và chỉ export những gì process con cần.

Ở bài tiếp theo, chúng ta sẽ đi vào cron job với Bash: cú pháp lịch chạy, crontab, @daily, @reboot, log cron, vấn đề PATH và ví dụ backup/healthcheck tự động. 🚀


Tài liệu tham khảo