Hướng dẫn sử dụng cloud-init trên Ubuntu Server: Tự động hóa cấu hình hệ thống ngay lần đầu khởi động

Ubuntu tutorial - IT technology blog
Ubuntu tutorial - IT technology blog

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 ]; then khắ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_filesruncmd 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: packages chạy trước runcmd. Không cần apt-get install trong runcmd nếu đã khai báo trong packages.
  • 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.

Share: