# Viết script preflight để máy khách nói thật trước khi bạn cài

> Mỗi kiểm tra trả về PASS, WARN hoặc FAIL, và chỉ FAIL mới được dừng việc cài.

Bản gốc: https://fdetimes.net/vi/bach-khoa/thuc-hanh-preflight-check-truoc-khi-cai-vao-moi-truong-khach/

Nếu một bootstrap check thất bại khi Elasticsearch đang ở chế độ production, nó sẽ không chịu khởi động. Cũng lỗi đó, ở chế độ development, chỉ hiện thành một dòng cảnh báo trong log. Có thể bạn chưa từng cài Elasticsearch cho khách, nhưng quy tắc này vẫn đáng mang theo đến mọi lần triển khai.

Thử hình dung một buổi cài ở site khách: bộ cài chạy được nửa chừng thì đổ lỗi, rồi mới lộ ra swap đang bật, thông số kernel chưa đặt hoặc firewall chặn cổng. Script preflight đưa toàn bộ những phát hiện này lên trước giờ cài. Nó còn quy định rõ phát hiện nào được phép dừng việc cài, phát hiện nào chỉ cần ghi lại.

Bài này hướng dẫn bạn dựng một script như thế bằng bash. Bạn cần một máy Linux hoặc VM có quyền sudo để thử. Các lệnh đọc RAM, swap, cổng mạng là **bản rút gọn, giả định Linux**: trước khi dùng thật, hãy kiểm tra lại trên đúng bản distro của khách.

## Mức độ phải có luật, không thể tùy hứng

Mỗi phát hiện chỉ được xếp vào một mức theo một câu hỏi: nếu bỏ qua nó, thành phần sắp cài sẽ từ chối chạy hay chỉ chạy kém? Từ chối chạy là FAIL. Chạy kém, hoặc chưa chắc, là WARN.

Tài liệu Kubernetes cho sẵn một ví dụ FAIL rõ ràng: mặc định, kubelet không khởi động nếu phát hiện swap trên node. Ngược lại, một bản OS bạn chưa từng thử với sản phẩm của mình chưa có nghĩa là hỏng, nên nó chỉ đáng là WARN.

Để ý chữ "thành phần sắp cài" trong câu hỏi. Swap là FAIL vì kubelet, chứ không phải vì mọi phần mềm. Mỗi kiểm tra chỉ có giá trị khi đúng thành phần của nó nằm trong danh sách cài, và script bên dưới sẽ bật check theo thành phần.

## Bước 1: một hàm report chịu trách nhiệm cho mọi kết quả

Mọi kiểm tra đều đi qua một hàm duy nhất. Chính hàm này áp chế độ chạy và danh sách miễn trừ.

```bash
#!/usr/bin/env bash
# preflight.sh — chỉ đọc, không sửa gì trên máy khách
MODE="${MODE:-production}"   # production | development (chỉ dùng trên VM thử)
WAIVE="${WAIVE:-}"           # check được hạ FAIL -> WARN, ví dụ "swap,net_443"
FAILS=0; WARNS=0

if [ "$WAIVE" = all ]; then
  echo "Không chấp nhận WAIVE=all: hãy liệt kê từng check"; exit 2
fi

report() {  # report <id> <PASS|WARN|FAIL> <thông báo>
  local id="$1" level="$2" msg="$3"
  if [ "$level" = FAIL ]; then
    if [ "$MODE" = development ] || [[ ",$WAIVE," == *",$id,"* ]]; then
      level=WARN; msg="$msg (đã hạ xuống WARN)"
    fi
  fi
  case "$level" in FAIL) FAILS=$((FAILS+1));; WARN) WARNS=$((WARNS+1));; esac
  printf '%-5s %-14s %s\n' "$level" "$id" "$msg"
}
```

Chế độ `development` mô phỏng cách Elasticsearch hạ lỗi xuống cảnh báo. `WAIVE` thì học từ kubeadm: pha preflight chạy trước khi nâng cấp có cờ `--ignore-preflight-errors`, nhận một danh sách check mà lỗi của chúng chỉ hiện thành cảnh báo. Script này cố ý từ chối giá trị `all`, để mỗi lần miễn trừ đều phải gọi đúng tên.

Có điều `MODE=development` hạ mọi FAIL xuống WARN, nên trên thực tế nó cũng là một kiểu bỏ qua tất cả. Vì thế chế độ này chỉ dành cho VM thử nghiệm của bạn, không bao giờ chạy trên máy production của khách. Ở đó chỉ có `production`, cộng với `WAIVE` liệt kê từng tên check.

**Kiểm tra:** gọi thử `report demo FAIL "test"` với `WAIVE=demo` rồi chạy lại. Lần chạy đó phải in ra WARN kèm ghi chú "đã hạ xuống WARN".

## Bước 2: tài nguyên, có ngưỡng ghi rõ trong thông báo

```bash
MIN_RAM_KB="${MIN_RAM_KB:-2097152}"   # ngưỡng giả định, thay bằng khuyến nghị của sản phẩm bạn
check_ram() {
  local kb; kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
  if [ "$kb" -ge "$MIN_RAM_KB" ]; then report ram PASS "MemTotal=${kb}kB"
  else report ram WARN "MemTotal=${kb}kB, khuyến nghị từ ${MIN_RAM_KB}kB"; fi
}
check_swap() {
  local kb; kb=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo)
  if [ "$kb" -eq 0 ]; then report swap PASS "không có swap"
  else report swap FAIL "swap=${kb}kB, kubelet mặc định sẽ không khởi động"; fi
}
check_max_map_count() {
  local v; v=$(sysctl -n vm.max_map_count 2>/dev/null || echo 0)
  if [ "$v" -ge 262144 ]; then report max_map_count PASS "vm.max_map_count=$v"
  else report max_map_count FAIL "vm.max_map_count=$v, cần >= 262144, sửa bằng sysctl"; fi
}
```

Mỗi ngưỡng thuộc về một sản phẩm. Swap là chuyện của Kubernetes, vì kubelet không chạy khi có swap. Còn `vm.max_map_count` đặt qua `sysctl` ở mức ít nhất 262144 là yêu cầu của Elasticsearch, nên khách chỉ cài Kubernetes thì check này không có lý do để chặn họ.

RAM được xếp vào WARN vì thiếu RAM thường làm hệ thống chạy kém chứ chưa chắc khiến nó từ chối chạy. Ngưỡng `MIN_RAM_KB` trong ví dụ là giả định: hãy lấy con số từ tài liệu sản phẩm bạn đang cài và ghi nguồn vào README.

Có một cái bẫy nhỏ: con số `MemTotal` mà máy báo có thể thấp hơn dung lượng RAM ghi trên giấy, nên một VM "2 GB" vẫn có thể rơi xuống dưới ngưỡng và bị WARN oan. Trước khi chốt, hãy chạy thử trên máy thật của khách xem nó báo bao nhiêu.

**Kiểm tra:** trên một VM có bật swap, với `COMPONENTS=kubernetes`, bạn phải thấy FAIL ở dòng `swap`.

## Bước 3: phiên bản, mạng và quyền

```bash
check_os() {
  . /etc/os-release
  case "$ID-$VERSION_ID" in
    rhel-8*|rhel-9*) report os PASS "$PRETTY_NAME" ;;   # danh sách ví dụ
    *) report os WARN "$PRETTY_NAME chưa nằm trong danh sách đã thử" ;;
  esac
}
check_port() {  # check_port <host> <port> — kết nối TCP đi ra từ máy đang chạy script
  if timeout 3 bash -c "</dev/tcp/$1/$2" 2>/dev/null; then
    report "net_$2" PASS "kết nối được $1:$2"
  else report "net_$2" FAIL "không mở được kết nối tới $1:$2"; fi
}
check_root() {
  if [ "$(id -u)" -eq 0 ]; then report root PASS "đang chạy bằng root"
  else report root WARN "không phải root, các bước sửa sẽ cần sudo"; fi
}
```

Danh sách OS trong ví dụ là giả định. Bạn thay bằng danh sách đã thật sự kiểm thử với sản phẩm của mình, và giữ OS lạ ở mức WARN.

`check_port` chỉ kiểm tra chiều **đi ra**: máy này có mở được kết nối TCP tới một endpoint, chẳng hạn registry, hay không. Muốn soát chiều đi vào, như cổng 6443 của API server Kubernetes, hãy chạy cùng hàm từ một máy khác trong mạng của khách. Tài liệu Kubernetes cũng gợi ý dùng netcat cho việc này.

Cách kết nối qua `/dev/tcp` là bản rút gọn. Nó cho biết có mở được kết nối hay không, nhưng không thay được việc hỏi khách về proxy và firewall. Nếu khách dựng nhiều VM từ cùng một template, hãy soát thêm MAC address và product_uuid giữa các node: tài liệu kubeadm cảnh báo giá trị trùng nhau có thể làm cài đặt thất bại.

## Bước 4: chọn check theo thành phần, rồi trả exit code

```bash
COMPONENTS="${COMPONENTS:-kubernetes}"   # ví dụ "kubernetes,elasticsearch"
has() { [[ ",$COMPONENTS," == *",$1,"* ]]; }

check_os; check_root; check_ram
has kubernetes && check_swap
has elasticsearch && check_max_map_count
check_port registry.example.com 443   # thay bằng endpoint thật
echo "Tổng: $FAILS FAIL, $WARNS WARN"
[ "$FAILS" -eq 0 ]
```

Biến `COMPONENTS` giữ cho mỗi check ở đúng chỗ của nó: swap chỉ được chặn khi có kubelet, `vm.max_map_count` chỉ được chặn khi có Elasticsearch. Lệnh cuối biến kết quả thành exit code, để bộ cài, Ansible hay CI dừng lại mà không cần người ngồi đọc output.

Trên một VM thử đang bật swap, chạy bằng user thường, output sẽ có dạng như dưới đây. Đây là **output minh họa**: tên OS và các con số kB là giả định, máy của bạn sẽ báo số khác.

```text
PASS  os             Red Hat Enterprise Linux 9
WARN  root           không phải root, các bước sửa sẽ cần sudo
WARN  ram            MemTotal=1863000kB, khuyến nghị từ 2097152kB
FAIL  swap           swap=2097148kB, kubelet mặc định sẽ không khởi động
PASS  net_443        kết nối được registry.example.com:443
Tổng: 1 FAIL, 2 WARN
```

**Kiểm tra:** chạy `bash preflight.sh; echo $?`. Còn FAIL thì kết quả khác 0. Trên VM thử, chạy `MODE=development bash preflight.sh; echo $?` thì phải ra 0, kể cả khi máy lỗi.

**Điểm mấu chốt:** Preflight tốt chỉ đọc máy khách, không bao giờ âm thầm sửa nó.

## Những lỗi khiến preflight mất uy tín

Lỗi nặng nhất là để script tự sửa máy, ví dụ tự gọi `sysctl -w`. kubeadm init mở đầu bằng loạt kiểm tra preflight để xác nhận trạng thái hệ thống trước khi thay đổi. Còn pha preflight của `kubeadm upgrade apply` có chế độ `--dry-run`, chỉ in ra những việc sẽ làm mà không đổi trạng thái.

Giá trị của preflight nằm ở chỗ chưa đụng vào gì; phần sửa là việc của đội IT bên khách, theo quy trình của họ.

Lỗi kế tiếp là thông báo mơ hồ. Luật cần nhớ: **mỗi dòng FAIL phải có giá trị đo được, ngưỡng yêu cầu và hướng sửa**, như dòng `max_map_count` ở Bước 2.

Leapp, công cụ nâng cấp RHEL, là mẫu đáng học. Nó gọi những phát hiện chặn là *inhibitor*, báo thẳng nâng cấp bị chặn vì những vấn đề nào, chẳng hạn một driver kernel đã bị gỡ, và ghi báo cáo thành file trong `/var/log/leapp`.

Theo Microsoft Learn, inhibitor phải được xử lý xong mới nâng cấp tiếp được, nên nó chính là FAIL của bạn; hãy lưu output ra file, chẳng hạn qua `tee`, để khách xem lại.

Còn hai lỗi nữa. Gắn FAIL cho mọi thứ, kể cả check của sản phẩm khách không cài, khiến khách quen tay bỏ qua và chẳng bao lâu ai cũng đòi miễn trừ tất cả. Exit code lúc nào cũng 0 thì pipeline chạy tiếp như không có chuyện gì.

## Ở site khách, script này mở đầu cuộc trò chuyện

Cách dùng hiệu quả nhất là gửi script cho đội IT của khách vài ngày trước buổi cài, nhờ họ chạy ở chế độ `production` và dán lại output. Mỗi dòng FAIL thành một ticket có chủ. Mỗi lần miễn trừ qua `WAIVE` thì ghi tên người đồng ý vào biên bản. Đến hôm cài, bạn đã biết trước máy nào chưa sẵn sàng.

Với người đang muốn chuyển sang FDE, đây là một dự án portfolio gọn nhưng cho thấy đúng thứ nhà tuyển dụng cần. Đưa script lên repo, kèm một output mẫu và một bảng giải thích vì sao từng check là WARN hay FAIL.

Trong CV, hãy mô tả nó như một công cụ giảm rủi ro khi triển khai lên hạ tầng khách. Khi phỏng vấn, hãy sẵn sàng giải thích vì sao swap là FAIL còn RAM thấp chỉ là WARN: câu trả lời đó cho thấy bạn biết cân rủi ro chứ không chỉ biết viết lệnh.

Thứ khách nhớ thường không phải là bộ cài chạy nhanh đến đâu. Họ nhớ việc bạn đã chỉ ra máy nào chưa sẵn sàng, ngay từ trước khi có gì kịp hỏng.

**Thử ngay tuần này:**

- Viết preflight.sh theo khung trong bài, chạy trên một VM đang bật swap với COMPONENTS=kubernetes, rồi xác nhận script báo FAIL và trả exit code khác 0
- Thêm một kiểm tra riêng cho sản phẩm bạn đang triển khai, gắn nó vào đúng thành phần trong COMPONENTS, ghi kèm ngưỡng và cách sửa ngay trong thông báo
- Đưa script và một bản output mẫu vào repo portfolio, viết README giải thích vì sao từng kiểm tra là WARN hay FAIL

## Nguồn

- [Bootstrap checks | Elastic Documentation](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/bootstrap-checks)

- [Maximum map count check | Elasticsearch Guide 8.19](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/bootstrap-checks-max-map-count.html)

- [kubeadm upgrade apply phase preflight | Kubernetes v1.34](https://v1-34.docs.kubernetes.io/docs/reference/setup-tools/kubeadm/generated/kubeadm_upgrade/kubeadm_upgrade_apply_phase_preflight)

- [kubeadm init | Kubernetes](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init/)

- [Installing kubeadm | Kubernetes](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)

- [Running Leapp pre-upgrade analysis: take unknowns out of your next RHEL upgrade](https://www.redhat.com/en/blog/running-leapp-pre-upgrade-analysis-take-unknowns-out-your-next-rhel-upgrade)

- [Upgrade RHEL pay-as-you-go virtual machines using Leapp - Virtual Machines | Microsoft Learn](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/leapp-upgrade-process-rhel-7-and-8)
