Khi cần dựng mới hoặc dựng lại hàng loạt Cloud VPS (balancer, minion, database, máy vận hành), cài hệ điều hành bằng tay cho từng máy vừa chậm vừa khó đồng nhất. Cách làm hiện đại là tạo mọi máy từ cùng một cloud image, và để cloud-init lo phần việc của lần boot đầu: đặt hostname, nới phân vùng theo dung lượng đĩa thật, nạp SSH key. Tài liệu này giải thích cloud image khác gì cài tay, cách kiểm tra cloud-init đã làm gì trên máy, phần nào vẫn phải chuẩn hoá bằng script, mẫu cloud-config cho minion và mẹo xác định đúng ngày một máy được dựng.

1. Cloud image khác gì cài hệ điều hành bằng tay

Cài tay là boot từ file ISO, chọn phân vùng, chọn gói, đặt user rồi chờ cài xong. Mỗi máy mất vài chục phút, và hai máy cài bởi hai người hiếm khi giống hệt nhau. Cloud image là một ổ đĩa đã cài sẵn hệ điều hành ở trạng thái tối thiểu; nền tảng cloud chỉ việc copy ổ đĩa đó cho VPS mới rồi boot lên. Debian phát hành sẵn các bản này tại kho cloud image chính thức của Debian, nhiều nhà cung cấp dùng chúng làm template.

Cài tay từ ISOCloud image + cloud-init
Thời gian có máy dùng đượcVài chục phút, cần người ngồi chọnVài giây sau khi boot đã SSH được
Độ đồng nhấtPhụ thuộc người càiMọi máy giống nhau tới từng gói
Hostname, IP, SSH keyNhập trong lúc càicloud-init đọc từ metadata của nền tảng
Kích thước phân vùngChọn khi càiImage nhỏ, cloud-init tự nới ra bằng dung lượng đĩa

Vấn đề duy nhất của cloud image: một ổ đĩa giống hệt nhau không thể chứa sẵn hostname hay SSH key riêng của từng máy. Phần "cá nhân hoá" đó là việc của cloud-init.

2. cloud-init là gì và chạy lúc nào

cloud-init là bộ công cụ có sẵn trong hầu hết cloud image Linux. Ở lần boot đầu, nó tìm datasource (nguồn metadata do nền tảng cung cấp), đọc thông tin của máy rồi chạy một chuỗi module theo bốn giai đoạn:

  1. init-local: trước khi có mạng, xác định datasource và cấu hình mạng.
  2. init: có mạng rồi, chạy các module nền như đặt hostname, growpart, resizefs, tạo user, nạp SSH key.
  3. config: các module cấu hình như locale, timezone, apt, ntp, runcmd.
  4. final: cài gói, chạy script của người dùng, ghi file đánh dấu boot-finished.

Mỗi module có tần suất riêng: per-instance (chỉ chạy một lần cho mỗi máy), per-boot hoặc always. Người dùng điều khiển cloud-init qua user-data, thường là một file YAML #cloud-config truyền vào lúc tạo máy. Danh sách module và cú pháp đầy đủ có trong tài liệu chính thức của cloud-init.

3. Kiểm tra cloud-init đã làm gì trên máy

Thay vì đoán, hãy đọc thẳng trên máy vừa tạo:

$ cloud-init --version
$ cloud-init status --long           # mong đợi: status: done

# các mốc chính trong log
$ grep -E "running 'init-local'|Setting the hostname|resized" /var/log/cloud-init.log
... Cloud-init v. 22.4.2 running 'init-local' at ... Up 5.33 seconds.
... cc_set_hostname.py[DEBUG]: Setting the hostname to app-php82-1 (app-php82-1)
... cc_growpart.py[INFO]: '/' resized: changed (/dev/vda, 1) ...

# dữ liệu cloud-init đã nhận
$ ls -l /var/lib/cloud/instance/
$ cat /var/lib/cloud/instance/user-data.txt

Thông thường chỉ vài giây sau khi máy bật, cloud-init đã đặt xong hostname và nới phân vùng gốc ra hết dung lượng đĩa (growpart rồi resize2fs). Nếu user-data.txt rỗng, nghĩa là không có cloud-config nào được truyền vào: mọi việc cloud-init làm đều đến từ cấu hình mặc định trong /etc/cloud/cloud.cfg của image cùng metadata của nền tảng. Hãy đọc kỹ file này; ví dụ dòng disable_root: false nghĩa là image cho phép SSH bằng root ngay từ đầu, thuận tiện nhưng cần siết lại sau khi máy lên.

4. Phần cloud-init không làm: chuẩn hoá máy mới

Một máy vừa boot từ cloud image mới chỉ là Debian trống. Để thành minion hay balancer, nó cần thêm một danh sách việc. Nhật ký dpkg cho biết thứ tự những gì đã được cài sau lần boot đầu:

$ zgrep -h " install " /var/log/dpkg.log* | sort | awk '{print $1, $2, $4}'

Danh sách chuẩn hoá tối thiểu cho một Cloud VPS mới:

ViệcÁp dụng choVì sao
Đặt hostname đúng quy ước, khai báo vào DNS nội bộMọi máyScript và cấu hình gọi máy bằng tên (xem bài DNS nội bộ)
Repo PHP sury, PHP-FPM đúng phiên bản của nhóm minionMinionDebian 12 chỉ có sẵn PHP 8.2 (xem bài chạy nhiều phiên bản PHP song song)
User chạy web, thư mục site thống nhất (ví dụ /var/www)MinionMọi site, mọi script dùng chung một quy ước sở hữu và đường dẫn
nginx, supervisor (include file cấu hình theo site)MinionWeb server và queue worker theo site
nginx, certbot, snippet bảo mậtBalancerReverse proxy và SSL
Swapfile nhỏ, vm.swappiness thấpMáy RAM nhỏCó vùng đệm trước khi OOM killer ra tay
node_exporter hoặc agent giám sátMọi máyĐưa CPU, RAM, đĩa của máy vào giám sát
ufw, SSH key của hệ thống vận hành, tắt đăng nhập root bằng mật khẩuMọi máyChỉ mở cổng cần thiết, script tự động SSH được

Phần cố định của danh sách nên đưa vào cloud-config để máy mới tự hoàn chỉnh ngay lần boot đầu, thay vì chạy script tay sau khi máy lên. Một mẫu minh hoạ cho minion:

#cloud-config
hostname: app-php82-1
package_update: true
packages: [nginx, supervisor, git, zip, unzip, ufw, ca-certificates, curl]
swap:
  filename: /swapfile
  size: 1G
write_files:
  - path: /etc/sysctl.d/90-swap.conf
    content: "vm.swappiness=10\n"
runcmd:
  - curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/keyrings/php.gpg
  - echo "deb [signed-by=/etc/apt/keyrings/php.gpg] https://packages.sury.org/php/ bookworm main" > /etc/apt/sources.list.d/php.list
  - apt-get update && apt-get install -y php8.2-fpm php8.2-cli php8.2-mysql php8.2-mbstring php8.2-xml
  - mkdir -p /var/www && chown www-data:www-data /var/www
  - sysctl --system

Trước khi tin một file user-data, kiểm tra cú pháp bằng cloud-init schema --config-file user-data.yaml, và sau lần boot đầu, đọc /var/log/cloud-init-output.log để thấy output của từng lệnh trong runcmd.

5. Mẹo điều tra: máy này thật sự được dựng ngày nào?

Khi dựng lại hàng loạt máy, câu hỏi "máy này dựng lúc nào, trước hay sau đợt thay đổi kia" xuất hiện liên tục. Cách đầu tiên ai cũng nghĩ tới lại là cách sai:

$ tune2fs -l /dev/vda1 | grep created
Filesystem created:       <ngày build image>

Trên mọi máy tạo từ cùng một cloud image, giá trị này giống hệt nhau, dù máy dựng vào ngày nào. Đó là ngày filesystem được tạo khi build cloud image, không phải ngày tạo VPS; thư mục /lost+found cũng mang đúng ngày đó. Ngày dựng thật phải tìm ở những file mà cloud-init hoặc systemd ghi ra trong lần boot đầu:

# 1. Lần boot đầu tiên trong log cloud-init (nếu log chưa bị xoay mất)
zcat -f /var/log/cloud-init.log* | grep -m1 "running 'init-local'"

# 2. machine-id được sinh ra ở lần boot đầu
stat -c '%y' /etc/machine-id

# 3. Thư mục instance: mỗi instance-id mới là một thư mục
ls -l --time-style=+%F /var/lib/cloud/instances/

# 4. Lần đặt hostname gần nhất
stat -c '%y' /etc/hostname
zcat -f /var/log/cloud-init.log* | grep "Setting the hostname"

# 5. Gói đầu tiên được cài thêm
zgrep -h " install " /var/log/dpkg.log* | sort | head -1

Đối chiếu các nguồn này với nhau mới ra câu trả lời đúng. Lý do cần đối chiếu: hostname do cloud-init đặt theo tên trong metadata của nền tảng ở mỗi lần boot. Một máy có thể boot lần đầu với tên tạm, vài ngày sau mới được đổi sang tên đúng quy ước, thậm chí đổi nhiều lần nếu gõ nhầm; log cloud-init ghi lại đủ từng lần. Vì vậy:

  • mtime của /etc/hostname là ngày đặt tên cuối cùng, gần với ngày máy được đưa vào sử dụng theo quy ước;
  • /etc/machine-id, thư mục instance và dòng init-local đầu tiên mới là ngày máy ra đời;
  • ngày tạo filesystem chỉ cho biết image được build khi nào.

6. Lỗi thường gặp với cloud image và cloud-init

  • Đổi tên máy nhưng hostname quay về tên cũ sau reboot: sửa /etc/hostname bằng tay trong khi metadata của nền tảng vẫn giữ tên cũ. Đổi tên ở cả nền tảng, hoặc đặt preserve_hostname: true nếu muốn tự quản lý.
  • Đĩa mua lớn mà df chỉ thấy vài GB: growpart không chạy được (thiếu gói cloud-guest-utils hoặc phân vùng không nằm cuối đĩa). Kiểm tra dòng cc_growpart trong log.
  • runcmd lỗi nhưng máy vẫn báo "done": lệnh trong runcmd lỗi không chặn boot. Luôn đọc cloud-init-output.log và cloud-init status --long.
  • Chạy lại cloud-init trên máy đang sống: cloud-init clean rồi reboot sẽ chạy lại mọi module per-instance, kể cả tạo user và ghi đè file. Chỉ làm trên máy thử.
  • Template image cũ: image có thể được build từ lâu, nên máy mới boot lên đã thiếu nhiều bản vá. Bước đầu tiên luôn là apt-get update && apt-get upgrade, hoặc bật package_upgrade: true trong cloud-config.

7. Checklist dựng Cloud VPS bằng cloud-init

  • Chọn image chính thức, ghi lại phiên bản image đang dùng làm template.
  • Đặt tên VPS đúng quy ước ngay khi tạo, vì cloud-init lấy hostname từ đó.
  • Phần cố định (repo, gói, user, swap, sysctl) nằm trong user-data, đã qua cloud-init schema.
  • Sau lần boot đầu: cloud-init status --long, đọc cloud-init-output.log, kiểm tra df -h.
  • Siết SSH (disable_root, chỉ đăng nhập bằng key), bật ufw, cập nhật bản vá.
  • Khai báo máy vào DNS nội bộ và giám sát trước khi đưa site lên.

Cloud image và cloud-init giải quyết phần "máy trống": vài giây có máy SSH được, hostname đúng, đĩa đủ. Phần "máy đúng vai trò" vẫn là trách nhiệm của mình: cái gì cố định thì đưa vào user-data, cái gì phụ thuộc site thì để cho pipeline như trong bài deploy tự động đầu tiên.

Bài tiếp theo

Jenkins làm "control plane" cho hàng trăm website. Khi đã có hàng chục Cloud VPS, mọi thao tác tạo site, deploy, backup, đổi domain được gom về một chỗ.