This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Bài cuối cùng trong series Bash sẽ đưa bạn vào thế giới DevOps — nơi Bash thực sự tỏa sáng. Từ Git hooks tự động hóa workflow, CI/CD scripting, Docker entrypoint, cron jobs, SSH automation cho đến linting với ShellCheck — đây là những kỹ năng mà mọi developer và DevOps engineer đều cần.
1. Git Hooks — Tự động hóa Git Workflow
Git hooks là các script Bash chạy tự động tại các thời điểm khác nhau trong Git workflow. Chúng nằm
trong thư mục .git/hooks/.
1.1 Các hook phổ biến
# Cấu trúc thư mục hooks
.git/hooks/
├── pre-commit # Chạy TRƯỚC khi commit (lint, format, test)
├── commit-msg # Kiểm tra commit message
├── pre-push # Chạy TRƯỚC khi push (test, build check)
├── post-merge # Chạy SAU khi merge (install deps)
└── prepare-commit-msg # Tự động sửa commit message
# Mọi hook phải có quyền thực thi:
chmod +x .git/hooks/pre-commit
1.2 pre-commit hook — Lint & Format
#!/usr/bin/env bash
set -euo pipefail
echo "🔍 Running pre-commit checks..."
# ── Check 1: Không commit file lớn ──
MAX_SIZE=5242880 # 5MB
large_files=$(git diff --cached --name-only | while read -r file; do
if [[ -f "$file" ]]; then
size=$(wc -c < "$file")
if (( size > MAX_SIZE )); then
echo "$file ($(( size / 1024 / 1024 ))MB)"
fi
fi
done)
if [[ -n "$large_files" ]]; then
echo "❌ File quá lớn (>5MB):"
echo "$large_files"
exit 1
fi
# ── Check 2: Không commit secrets ──
PATTERNS=(
'password\s*='
'api_key\s*='
'secret\s*='
'AWS_SECRET'
'PRIVATE_KEY'
)
for pattern in "${PATTERNS[@]}"; do
if git diff --cached -i -G "$pattern" --name-only | grep -q .; then
echo "❌ Có thể chứa secrets (pattern: $pattern):"
git diff --cached -i -G "$pattern" --name-only
echo "Dùng 'git commit --no-verify' nếu chắc chắn an toàn."
exit 1
fi
done
# ── Check 3: Lint staged files ──
STAGED_JS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$' || true)
if [[ -n "$STAGED_JS" ]]; then
echo "📝 Linting JavaScript/TypeScript..."
echo "$STAGED_JS" | xargs npx eslint --quiet || {
echo "❌ ESLint failed. Fix errors trước khi commit."
exit 1
}
fi
# ── Check 4: Lint shell scripts ──
STAGED_SH=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.sh$' || true)
if [[ -n "$STAGED_SH" ]]; then
echo "🐚 Linting shell scripts..."
echo "$STAGED_SH" | xargs shellcheck || {
echo "❌ ShellCheck failed."
exit 1
}
fi
echo "✅ All pre-commit checks passed!"
1.3 commit-msg hook — Validate Conventional Commits
#!/usr/bin/env bash
set -euo pipefail
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Conventional Commits: type(scope): description
PATTERN='^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\([a-z-]+\))?: .{1,72}$'
if ! echo "$COMMIT_MSG" | head -1 | grep -qE "$PATTERN"; then
echo "❌ Commit message không đúng format!"
echo ""
echo "Format: type(scope): description"
echo "Types: feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert"
echo "Ví dụ: feat(auth): add Google OAuth login"
echo ""
echo "Message hiện tại: $COMMIT_MSG"
exit 1
fi
echo "✅ Commit message hợp lệ"
1.4 Chia sẻ hooks qua .githooks/
Vì .git/hooks/ không được track bởi Git, team thường dùng thư mục riêng:
# Cách 1: Cấu hình Git core.hooksPath
git config core.hooksPath .githooks
# Cách 2: Symlink (tương thích Git cũ)
#!/usr/bin/env bash
HOOKS_DIR=".githooks"
GIT_HOOKS_DIR=".git/hooks"
for hook in "$HOOKS_DIR"/*; do
hook_name=$(basename "$hook")
ln -sf "../../$HOOKS_DIR/$hook_name" "$GIT_HOOKS_DIR/$hook_name"
echo "✅ Linked $hook_name"
done
# Cách 3: Thêm vào package.json (Node.js projects)
# "scripts": { "prepare": "git config core.hooksPath .githooks" }
2. CI/CD Scripting
Hầu hết CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins) đều chạy Bash script. Hiểu cách viết script CI tốt sẽ giúp pipeline của bạn ổn định và dễ debug.
2.1 GitHub Actions — CI Script
#!/usr/bin/env bash
set -euo pipefail
# ══════════════════════════════════════════
# CI Build & Test Script
# Dùng trong GitHub Actions / GitLab CI
# ══════════════════════════════════════════
echo "::group::📦 Install Dependencies"
npm ci
echo "::endgroup::"
echo "::group::🔍 Lint"
npm run lint || {
echo "::error::Lint failed"
exit 1
}
echo "::endgroup::"
echo "::group::🧪 Test"
npm run test -- --coverage || {
echo "::error::Tests failed"
exit 1
}
echo "::endgroup::"
echo "::group::🏗️ Build"
npm run build || {
echo "::error::Build failed"
exit 1
}
echo "::endgroup::"
# ── Kiểm tra build output ──
BUILD_DIR="dist"
if [[ ! -d "$BUILD_DIR" ]] || [[ -z "$(ls -A "$BUILD_DIR")" ]]; then
echo "::error::Build directory rỗng hoặc không tồn tại"
exit 1
fi
BUILD_SIZE=$(du -sh "$BUILD_DIR" | cut -f1)
echo "✅ Build thành công! Size: $BUILD_SIZE"
# ── Export cho step tiếp theo (GitHub Actions) ──
echo "build_size=$BUILD_SIZE" >> "${GITHUB_OUTPUT:-/dev/null}"
2.2 GitLab CI — Deploy Script
#!/usr/bin/env bash
set -euo pipefail
# ── Kiểm tra env vars bắt buộc ──
: "${DEPLOY_HOST:?'DEPLOY_HOST chưa set'}"
: "${DEPLOY_USER:?'DEPLOY_USER chưa set'}"
: "${DEPLOY_PATH:?'DEPLOY_PATH chưa set'}"
: "${SSH_PRIVATE_KEY:?'SSH_PRIVATE_KEY chưa set'}"
# ── Setup SSH ──
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null
SSH_CMD="ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no"
SCP_CMD="scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no"
# ── Deploy ──
echo "🚀 Deploying to $DEPLOY_HOST..."
# Upload build artifacts
$SCP_CMD -r dist/* "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
# Restart service trên remote
$SSH_CMD "${DEPLOY_USER}@${DEPLOY_HOST}" <<'REMOTE'
cd /var/www/myapp
npm ci --production
pm2 restart myapp
echo "Service restarted"
REMOTE
echo "✅ Deploy hoàn thành!"
# Cleanup
rm -f ~/.ssh/deploy_key
Lưu ý quan trọng:
-
: "${VAR:?'message'}"— Cú pháp ngắn gọn kiểm tra biến bắt buộc, thoát ngay nếu thiếu -
::group::/::error::— GitHub Actions workflow commands để format output - Exit code khác 0 sẽ đánh dấu CI job là FAILED
3. Docker & Bash
Docker sử dụng Bash ở nhiều nơi: Dockerfile CMD/ENTRYPOINT, entrypoint scripts, và docker-compose commands.
3.1 Entrypoint Script
Entrypoint script là "cầu nối" giữa Docker container và ứng dụng. Nó thiết lập môi trường trước khi chạy app:
#!/usr/bin/env bash
set -euo pipefail
echo "🐳 Container starting..."
echo "Environment: ${NODE_ENV:-development}"
# ── Wait for dependencies ──
wait_for_service() {
local host="$1" port="$2" timeout="${3:-30}"
local elapsed=0
echo "⏳ Waiting for $host:$port..."
until nc -z "$host" "$port" 2>/dev/null; do
if (( elapsed >= timeout )); then
echo "❌ Timeout waiting for $host:$port after ${timeout}s"
exit 1
fi
sleep 1
elapsed=$((elapsed + 1))
done
echo "✅ $host:$port is ready (${elapsed}s)"
}
# Chờ database và Redis sẵn sàng
wait_for_service "${DB_HOST:-postgres}" "${DB_PORT:-5432}" 60
wait_for_service "${REDIS_HOST:-redis}" "${REDIS_PORT:-6379}" 30
# ── Run migrations ──
if [[ "${RUN_MIGRATIONS:-false}" == "true" ]]; then
echo "📦 Running database migrations..."
npm run migrate
fi
# ── Inject runtime env ──
if [[ -f /run/secrets/app_secrets ]]; then
echo "🔐 Loading secrets..."
set -a
source /run/secrets/app_secrets
set +a
fi
# ── Exec: thay thế shell process bằng app process ──
# Điều này quan trọng để app nhận được signals (SIGTERM, SIGINT)
echo "🚀 Starting application..."
exec "$@"
3.2 Docker utility commands
#!/usr/bin/env bash
set -euo pipefail
# ── Dockerfile sử dụng entrypoint ──
# COPY docker-entrypoint.sh /usr/local/bin/
# RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# ENTRYPOINT ["docker-entrypoint.sh"]
# CMD ["node", "server.js"]
# ── Docker Compose helper ──
dc_up() {
docker-compose up -d --build
echo "✅ Services started"
docker-compose ps
}
dc_logs() {
docker-compose logs -f --tail=100 "${1:-}"
}
dc_restart() {
local service="$1"
docker-compose restart "$service"
docker-compose logs -f --tail=20 "$service"
}
# ── Exec vào container ──
dc_shell() {
local service="${1:-app}"
docker-compose exec "$service" /bin/bash
}
# ── Cleanup Docker resources ──
docker_cleanup() {
echo "🧹 Cleaning up Docker..."
docker system prune -f
docker volume prune -f
echo "Disk recovered: $(docker system df | tail -1)"
}
4. Cron Jobs & SSH Automation
4.1 Crontab — Lập lịch tác vụ
Crontab là công cụ lập lịch mặc định trên Linux/macOS. Cú pháp gồm 5 trường:
# Cú pháp crontab:
# ┌───────────── phút (0-59)
# │ ┌─────────── giờ (0-23)
# │ │ ┌───────── ngày trong tháng (1-31)
# │ │ │ ┌─────── tháng (1-12)
# │ │ │ │ ┌───── ngày trong tuần (0-7, 0 và 7 = Chủ nhật)
# │ │ │ │ │
# * * * * * command
# Ví dụ:
# Mỗi ngày lúc 2:00 AM
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Mỗi 5 phút
*/5 * * * * /opt/scripts/healthcheck.sh
# Thứ 2-6, lúc 9:00 AM
0 9 * * 1-5 /opt/scripts/report.sh
# Ngày 1 hàng tháng lúc 00:00
0 0 1 * * /opt/scripts/monthly_cleanup.sh
# ── Quản lý crontab ──
crontab -l # Xem crontab hiện tại
crontab -e # Sửa crontab
crontab -r # Xóa toàn bộ crontab (CẨN THẬN!)
# ── Tip: Log output ──
# Luôn redirect output để debug:
* * * * * /path/script.sh >> /var/log/cron.log 2>&1
# ── Tip: Set PATH trong crontab ──
# Cron không có PATH giống shell bình thường
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * backup.sh
4.2 SSH Automation
#!/usr/bin/env bash
set -euo pipefail
# ══════════════════════════════════════════
# SSH Automation
# ══════════════════════════════════════════
REMOTE_USER="deploy"
REMOTE_HOST="server.example.com"
REMOTE_DIR="/var/www/myapp"
# ── SSH Key Authentication (setup) ──
# ssh-keygen -t ed25519 -C "deploy@myapp" -f ~/.ssh/deploy_key
# ssh-copy-id -i ~/.ssh/deploy_key.pub [email protected]
# ── Chạy lệnh trên remote server ──
ssh "$REMOTE_USER@$REMOTE_HOST" "df -h && free -m && uptime"
# ── Chạy nhiều lệnh (heredoc) ──
ssh "$REMOTE_USER@$REMOTE_HOST" <<'EOF'
cd /var/www/myapp
git pull origin main
npm ci --production
pm2 restart all
echo "Deploy done at $(date)"
EOF
# ── SCP: Copy file ──
scp ./dist.tar.gz "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
# ── Rsync: Sync thư mục (nhanh hơn scp) ──
rsync -avz --delete \
--exclude='node_modules' \
--exclude='.git' \
./dist/ "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
# -a: archive mode (giữ permissions, timestamps)
# -v: verbose
# -z: nén khi transfer
# --delete: xóa file ở remote không có ở local
# ── SSH Tunnel (port forwarding) ──
# Forward port 5432 từ remote về local:
# ssh -L 5432:localhost:5432 [email protected] -N
# Giờ có thể kết nối PostgreSQL local: psql -h localhost -p 5432
# ── SSH Config (~/.ssh/config) ──
# Host myserver
# HostName server.example.com
# User deploy
# IdentityFile ~/.ssh/deploy_key
# Port 22
#
# Sau đó chỉ cần: ssh myserver
5. Makefile & ShellCheck
5.1 Makefile — Task Runner cho mọi dự án
Makefile không chỉ dùng cho C/C++ — nó là task runner tuyệt vời cho bất kỳ dự án nào:
# Makefile cho dự án web
.PHONY: help install dev build test deploy clean lint
# Target mặc định khi chạy `make` không có argument
.DEFAULT_GOAL := help
# Hiển thị help (parse comments ##)
help: ## Hiển thị danh sách commands
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
install: ## Cài đặt dependencies
npm ci
dev: ## Chạy development server
npm run dev
build: ## Build production
npm run build
test: ## Chạy tests
npm run test
lint: ## Lint code
npm run lint
shellcheck scripts/*.sh
deploy: build ## Deploy (chạy build trước)
./scripts/deploy.sh
clean: ## Dọn dẹp build artifacts
rm -rf dist node_modules .cache
# ── Ví dụ target với biến ──
ENV ?= staging
deploy-env: build ## Deploy tới môi trường cụ thể (ENV=prod make deploy-env)
./scripts/deploy.sh $(ENV)
5.2 ShellCheck — Linting cho Shell Scripts
ShellCheck là công cụ static analysis phát hiện bugs, security issues và bad practices trong shell scripts:
# Cài đặt ShellCheck
# macOS: brew install shellcheck
# Ubuntu: apt install shellcheck
# npm: npm install -g shellcheck
# ── Chạy ShellCheck ──
shellcheck myscript.sh
shellcheck -x myscript.sh # -x: Follow source'd files
shellcheck -s bash myscript.sh # Chỉ định shell type
# ════════════════════════════════════════
# Các lỗi phổ biến mà ShellCheck bắt:
# ════════════════════════════════════════
# SC2086: Double quote to prevent globbing and word splitting
# ❌ Sai:
rm $filename
# ✅ Đúng:
rm "$filename"
# SC2046: Quote this to prevent word splitting
# ❌ Sai:
files=$(find . -name "*.txt")
rm $files
# ✅ Đúng:
while IFS= read -r file; do
rm "$file"
done < <(find . -name "*.txt")
# SC2034: Variable appears unused
# ❌ ShellCheck cảnh báo biến khai báo nhưng không dùng
UNUSED_VAR="hello"
# SC2155: Declare and assign separately
# ❌ Sai:
local output=$(command)
# ✅ Đúng:
local output
output=$(command)
# ── Disable cảnh báo cụ thể ──
# shellcheck disable=SC2086
echo $intentionally_unquoted
# ── Tích hợp CI ──
# GitHub Actions:
# - name: ShellCheck
# uses: ludeeus/action-shellcheck@master
# with:
# scandir: './scripts'
5.3 Tổng kết Series — Best Practices
Sau 8 bài học, đây là checklist cho mọi shell script bạn viết:
-
Luôn bắt đầu với
#!/usr/bin/env bashvàset -euo pipefail - Quote mọi biến:
"$var"thay vì$var -
Dùng
[[ ]]thay vì[ ]cho conditional -
Dùng
$(command)thay vì backtick`command` - Trap signals để cleanup tài nguyên (temp files, lock files)
- Log mọi thứ với timestamp và log levels
- Chạy ShellCheck trước khi commit
- Dùng functions để tổ chức code, tránh script dài 1 khối
- Dry-run mode cho script nguy hiểm (delete, deploy)
- Kiểm tra dependencies với
command -vở đầu script
6. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm: Crontab
Dòng crontab */15 9-17 * * 1-5 /opt/scripts/check.sh có nghĩa là gì?
Tải file code thực hành minh họa bài học
File chứa đầy đủ các ví dụ: Git hooks, CI/CD scripts, Docker entrypoint, cron, SSH automation, Makefile và ShellCheck patterns.
Tải về devops_automation.sh
Comments
Bình luận