Tháng đầu tiên mình bắt đầu triển khai hạ tầng trên cloud, mỗi lần spin up VPS mới là mình lại mở checklist ra làm từng bước: cập nhật package, tạo user, cấu hình SSH, cài công cụ cơ bản… Mất 15-20 phút mỗi lần, và vẫn hay bỏ sót bước nào đó. Sau 6 tháng dùng cloud-init trên production, mình không còn muốn quay lại cách cũ nữa.
Ba cách cấu hình server mới: So sánh thực tế
Ba hướng mình đã thử — và cái nào cũng có lý do tồn tại:
Cách 1: Cấu hình thủ công từng bước
SSH vào server mới, chạy từng lệnh theo checklist. Cách này ai cũng từng làm. Linh hoạt tuyệt đối, nhưng không scalable — thêm 10 server là mất cả buổi sáng. Và checklist thì hay lỗi thời.
Cách 2: Bash script chạy sau khi login
Viết một file setup.sh, SSH vào rồi chạy. Tốt hơn cách thủ công nhiều — reproducible, có thể version control. Nhưng vẫn cần người chạy. Nếu script fail giữa chừng mà không có idempotency check, chạy lại dễ gây lỗi.
Cách 3: cloud-init user-data
Truyền cấu hình vào lúc tạo VM/VPS. Server tự cấu hình khi boot lần đầu, không cần ai SSH vào. Hầu hết cloud provider lớn — AWS, GCP, Azure, DigitalOcean, Vultr, Hetzner — đều hỗ trợ sẵn.
Phân tích ưu nhược điểm
Bash script: Tốt nhưng còn thiếu
Mình vẫn dùng bash script song song với cloud-init cho các tác vụ phức tạp. Nhưng bash script thuần có vài điểm yếu:
- Chạy sau khi boot xong: Cần có network, cần SSH — có thêm bước thủ công hoặc pipeline phức tạp hơn.
- Không có module system: Muốn tạo user, cấu hình SSH key, viết file — phải tự xử lý từng thứ.
- Idempotency phải tự implement: Kiểm tra
if [ ! -f /etc/myapp/configured ]; thenkhắp nơi làm script rối.
cloud-init: Chạy trước, nhất quán hơn
cloud-init chạy trong giai đoạn early boot, trước khi bạn SSH được vào. Tức là khi bạn connect lần đầu, server đã config xong — thường mất 3-8 phút tùy số package cần cài và upgrade. Các module built-in xử lý user, SSH key, package, file, command — đủ cho 90% nhu cầu provisioning ban đầu.
Nhược điểm thực sự: debugging khó hơn bash script. Khi có lỗi, phải biết log ở đâu mà tìm. YAML indent sai một chút là cả config không chạy — không có error message rõ ràng.
Khi nào nên chọn cloud-init thay vì bash script
Mình thường chọn cloud-init khi:
- Tạo VPS mới từ cloud provider — hầu như luôn có sẵn trường user-data khi tạo instance.
- Cần rebuild server nhiều lần (test environment, staging).
- Muốn cấu hình nhất quán giữa nhiều server trong cùng một lúc.
- Infrastructure as Code — lưu user-data vào git cùng Terraform/Ansible.
Bash script thuần phù hợp hơn khi cần cài đặt phức tạp có điều kiện nhiều tầng, hoặc khi provider không hỗ trợ user-data.
Hướng dẫn triển khai cloud-init thực tế
Cú pháp cơ bản của user-data
File user-data là YAML, bắt đầu bắt buộc bằng #cloud-config. Thiếu dòng này, cloud-init bỏ qua hoàn toàn — không báo lỗi, không làm gì cả.
#cloud-config
# Cập nhật package ngay lúc boot
package_update: true
package_upgrade: true
# Cài đặt package
packages:
- curl
- git
- htop
- ufw
- fail2ban
- unattended-upgrades
Tạo user và cấu hình SSH key
Phần này tiết kiệm nhiều nhất: không phải SSH vào root rồi tạo user thủ công, copy SSH key từng bước nữa.
#cloud-config
users:
- name: deploy
groups: sudo
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-public-key
# Tắt SSH login bằng password, chỉ dùng key
ssh_pwauth: false
# Disable root login
disable_root: true
Viết file và chạy lệnh
Kết hợp write_files và runcmd lại, bạn có thể thay thế phần lớn những gì bash script setup thường làm — mà gọn hơn nhiều:
#cloud-config
write_files:
- path: /etc/sysctl.d/99-custom.conf
content: |
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv6.conf.all.disable_ipv6 = 0
permissions: '0644'
- path: /etc/motd
content: |
==========================================
Production Server — Authorized access only
==========================================
permissions: '0644'
runcmd:
# Cấu hình firewall
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow 22/tcp
- ufw allow 80/tcp
- ufw allow 443/tcp
- ufw --force enable
# Áp dụng sysctl
- sysctl --system
# Timezone
- timedatectl set-timezone Asia/Tokyo
Ví dụ user-data hoàn chỉnh cho web server
Config mình đang dùng cho các VPS Nginx mới — kết hợp tất cả phần trên, paste vào là chạy được ngay:
#cloud-config
package_update: true
package_upgrade: true
packages:
- curl
- git
- nginx
- certbot
- python3-certbot-nginx
- ufw
- fail2ban
- htop
- unattended-upgrades
users:
- name: deploy
groups: sudo, www-data
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-key-here
ssh_pwauth: false
disable_root: true
write_files:
- path: /etc/fail2ban/jail.local
content: |
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
permissions: '0644'
runcmd:
- timedatectl set-timezone Asia/Tokyo
- ufw default deny incoming
- ufw default allow outgoing
- ufw allow 22/tcp
- ufw allow 80/tcp
- ufw allow 443/tcp
- ufw --force enable
- systemctl enable nginx
- systemctl start nginx
- systemctl enable fail2ban
- systemctl start fail2ban
# Reboot sau khi upgrade xong để áp dụng kernel mới (nếu có)
power_state:
mode: reboot
condition: true
Cách truyền user-data khi tạo VPS
Tùy provider, cách khác nhau một chút:
Hetzner Cloud (hcloud CLI):
hcloud server create \
--name my-webserver \
--type cx21 \
--image ubuntu-24.04 \
--user-data-from-file cloud-init.yaml
DigitalOcean (doctl CLI):
doctl compute droplet create my-webserver \
--image ubuntu-24-04-x64 \
--size s-1vcpu-1gb \
--region sgp1 \
--user-data-file cloud-init.yaml
Không có CLI thì paste thẳng vào trường “User Data” trong web UI — Hetzner, DigitalOcean, Vultr đều có ô này khi tạo instance mới.
Kiểm tra log khi có lỗi
cloud-init không chạy đúng? Ba chỗ cần nhìn theo thứ tự:
# Log tổng quan
sudo cat /var/log/cloud-init.log
# Output của các lệnh runcmd
sudo cat /var/log/cloud-init-output.log
# Xem status tổng thể
sudo cloud-init status --long
# Kiểm tra config đã parse đúng chưa
sudo cloud-init schema --config-file /path/to/cloud-init.yaml
Lệnh schema mình dùng trước khi tạo server thật. Bị sai YAML một lần, phải xóa VPS đi tạo lại — sau đó thành thói quen validate trước cho lẹ.
Một số lưu ý từ kinh nghiệm thực tế
Hồi mới chuyển từ CentOS sang Ubuntu, mình mất khoảng 1 tuần để làm quen. CentOS cũng có cloud-init, nhưng tên module và behavior khác ở một số chỗ nhỏ. Trên Ubuntu thì apt module là mặc định và chạy rất mượt.
Mấy thứ hay bị trip up:
- Thứ tự thực thi:
packageschạy trướcruncmd. Không cầnapt-get installtrongruncmdnếu đã khai báo trongpackages. - YAML indent: Dùng 2 space, không dùng tab. Một lần mình dùng tab trong
write_files, cả block không chạy mà không có error rõ ràng — mất gần tiếng debug. - runcmd chạy một lần duy nhất: cloud-init không chạy lại khi reboot (trừ khi bạn xóa state files). Đây là behavior đúng cho provisioning.
- Test local trước: Multipass (miễn phí, có cho macOS và Linux) cho phép tạo Ubuntu VM với user-data — vài phút là có môi trường để test, không tốn tiền VPS.
# Test nhanh với Multipass
multipass launch --name test-vm --cloud-init cloud-init.yaml ubuntu:24.04
# SSH vào kiểm tra
multipass shell test-vm
# Xóa khi xong
multipass delete test-vm && multipass purge
cloud-init không phải silver bullet — với setup phức tạp có nhiều điều kiện, Ansible vẫn là lựa chọn tốt hơn. Nhưng cho nhu cầu provisioning ban đầu của vài chục server, user-data YAML đủ gọn và đủ mạnh. Mình đang lưu các file cloud-init vào git cùng Terraform config — mỗi lần rebuild environment, một lệnh là xong.

