SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-07

Hướng dẫn Ansible: playbook đầu tiên trên VPS

Cài Ansible bằng pipx trên Ubuntu 24.04, tạo inventory và playbook harden VPS mới. Có cách sửa lỗi “Permission denied” và sudo khi chạy qua SSH.

Bạn sẽ xây dựng gì

Một máy điều khiển đã cài Ansible và một hoặc nhiều VPS Ubuntu 24.04 mới, chỉ có image mặc định. Khi hoàn tất, bạn sẽ có một file inventory chứa tên các server, một lệnh ping ad-hoc xác nhận việc xác thực hoạt động từ đầu đến cuối, và một playbook tự động hóa toàn bộ checklist cho VPS mới: tạo user deploy cùng SSH key của bạn, harden sshd, cài fail2ban, bật unattended upgrades và cấu hình firewall cho phép OpenSSH trước khi từ chối mọi lưu lượng khác. Bạn có thể chạy playbook trên một server hoặc hai mươi server. Chạy lần thứ hai sẽ không thay đổi gì. Đó là mục tiêu.

Sau mười lăm năm provision VPS, tôi nhận thấy một quy luật rõ ràng: mọi người đều cấu hình thủ công năm server đầu tiên, rồi mất cả cuối tuần với server thứ sáu vì không ai nhớ đã làm gì trên năm server trước. Hướng dẫn này mở rộng nội dung quản lý nhiều server Linux. Hãy bắt đầu khi bạn nhận ra mình đang gõ cùng một apt install vào ba terminal.

Ansible thực sự là gì, trong một đoạn

Ansible không cần agent. Không có daemon nào cần cài trên các server mà Ansible quản lý: máy điều khiển kết nối qua SSH thông thường, sao chép một Python module nhỏ lên target, thực thi module đó, đọc JSON mà module in ra, rồi xóa module. Điều duy nhất target cần có là python3, và mọi Ubuntu image mặc định đều đã có sẵn. Từ khóa quan trọng là idempotent, với ý nghĩa rất rõ: một task mô tả một state, không phải một hành động. state: present cho một package có nghĩa là “đảm bảo package này đã được cài”, không phải “chạy trình cài đặt”. Nếu state đã đúng, Ansible không thay đổi gì và báo trạng thái là ok thay vì changed. Đây là toàn bộ giá trị cốt lõi của sản phẩm, giúp chạy lại playbook an toàn; việc chạy lại an toàn biến một shell script thành infrastructure.

Điều kiện tiên quyết và các điểm dễ sai cần biết trước

  • Một máy điều khiển: laptop hoặc VPS nhỏ. Tôi giả định dùng Ubuntu 24.04; macOS cũng hoạt động tương tự sau khi cài pipx bằng Homebrew.
  • Một hoặc nhiều VPS đích chạy Ubuntu 24.04 trên KVM, có thể truy cập bằng root. Không cài thêm gì trên các máy này.
  • Xác thực bằng SSH key trên mọi máy đích. Ansible dùng đúng cơ chế xác thực như lệnh ssh của bạn; nếu ssh root@host yêu cầu nhập mật khẩu thì Ansible sẽ thất bại.
  • Trên Ubuntu 24.04, pip install ansible sẽ dừng với error: externally-managed-environment. Đây là chính sách có chủ ý của distro, không phải lỗi hỏng. Hãy dùng pipx.
  • Khoảng trắng trong YAML là cú pháp. Thụt lề sai sẽ tạo ra mapping values are not allowed in this context, còn ký tự tab ở bất kỳ vị trí nào cũng khiến cấu hình thất bại.
  • Giữ một phiên SSH đang hoạt động trên mỗi máy đích trong khi playbook harden sshd. Mọi trường hợp khóa truy cập mà tôi từng giúp khách hàng khôi phục đều xảy ra sau khi họ đóng phiên cuối cùng để "kiểm tra từ môi trường sạch".

Bước 1: cài Ansible trên máy điều khiển bằng pipx, không dùng pip

Cách làm quen thuộc là pip3 install ansible. Trên một image 24.04 hoàn toàn mới, cách này fail sớm hơn một bước vì Command 'pip3' not found, but can be installed with: sudo apt install python3-pip, còn việc cài pip chỉ đưa bạn đến đúng rào cản thật sự:

pip3 install ansible
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

Ubuntu 24.04 đánh dấu Python hệ thống là do hệ thống bên ngoài quản lý (PEP 668), nên pip không thể tranh chấp cùng các file với apt. Đừng dùng --break-system-packages; tên của flag này đã nói đúng bản chất. Cách sạch là dùng pipx. pipx tạo một virtualenv tách biệt cho Ansible và đưa các binary vào PATH của bạn:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

Mở shell mới sau pipx ensurepath để thay đổi PATH có hiệu lực. --include-deps không phải phần trang trí: package ansible không tự cung cấp console script nào, ansible, ansible-playbook, còn các entry point khác thuộc dependency ansible-core của nó. Vì vậy, nếu thiếu flag này, pipx sẽ từ chối cài đặt với No apps associated with package ansible or its dependencies. Hãy cài package ansible, không phải chỉ ansible-core. Package đầy đủ này kèm các community collection, và playbook này dùng module từ 2 collection trong số đó (ansible.posixcommunity.general).

ansible --version

Kết quả đúng sẽ mở đầu bằng một dòng như ansible [core 2.19.x] và cho biết Python mà Ansible đang chạy bằng; bất kỳ bản core hiện tại nào cũng đủ cho toàn bộ nội dung ở đây. ansible: command not found thay vào đó có nghĩa là ~/.local/bin chưa có trong PATH của bạn; hãy mở shell mới hoặc source ~/.bashrc.

Đó là toàn bộ phần cài đặt. Các máy đích không được cài gì.

Bước 2: Truy cập bằng SSH key đến mọi máy đích

ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20

Sau đó xác nhận trên từng host:

ssh root@10.0.0.10 true && echo ok

Dòng lệnh này thực hiện 2 việc: xác nhận key authentication hoạt động mà không cần password, đồng thời ghi host key vào known_hosts. Hãy làm ngay bước này, vì Ansible sẽ hiển thị host key chưa được ghi nhận dưới dạng prompt tương tác nằm giữa quá trình chạy. Điều này dễ bị hiểu nhầm là tiến trình bị treo.

Bước 3: inventory, dùng INI trước, chuyển sang YAML khi mở rộng

Inventory là một file văn bản liệt kê các máy mà Ansible có thể tác động. Tạo inventory.ini trong một thư mục project mới:

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 là alias do bạn chọn. Tên này xuất hiện trong output và là tên bạn target bằng --limit web1. ansible_host là địa chỉ thật. [vps] là một group, còn [vps:vars] đặt biến cho mọi host trong group đó; ansible_user là tài khoản Ansible dùng để đăng nhập. Đặt thêm ansible.cfg bên cạnh để bạn không phải nhập -i lần nữa:

[defaults]
inventory = inventory.ini

Ansible đọc ansible.cfg từ thư mục hiện tại. Inventory tương đương ở dạng YAML, lưu thành inventory.yml rồi trỏ ansible.cfg đến tên đó, sẽ phù hợp hơn khi mỗi host có nhiều biến:

vps:
  hosts:
    web1:
      ansible_host: 10.0.0.10
    web2:
      ansible_host: 10.0.0.20
  vars:
    ansible_user: root

Hai dạng này tương đương. INI dễ đọc nhanh khi có 2 server; YAML mở rộng tốt hơn khi có 20 server. Chọn một dạng và không cần bận tâm thêm.

Bước 4: lệnh ad-hoc, dòng “pong” màu xanh xác nhận mọi thứ hoạt động

ansible all -m ping

Đây không phải ICMP. Module ping là một lần kiểm tra đầy đủ: đăng nhập SSH, sao chép module, thực thi Python trên máy đích và dọn dẹp. Kết quả đúng là màu xanh, mỗi máy chủ hiển thị một block:

web1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Màu xanh của SUCCESS nghĩa là xác thực, Python interpreter và transport đều hoạt động; playbook cũng sẽ hoạt động. Màu đỏ của UNREACHABLE! nghĩa là transport đã fail trước khi bất kỳ module nào chạy; chuỗi lỗi chính xác và cách khắc phục nằm trong phần các trường hợp lỗi bên dưới. Có thêm 2 lệnh ad-hoc đáng biết:

ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --become

Ad-hoc dùng cho các tác vụ chạy một lần và kiểm tra nhanh. Bất kỳ việc gì bạn sẽ chạy lần thứ 2 đều nên đưa vào playbook.

Bước 5: playbook đầu tiên, checklist VPS mới dưới dạng mã

Đây là toàn bộ những việc bạn sẽ làm thủ công trong 10 phút đầu tiên trên một máy chủ mới. Lưu nội dung này thành site.yml:

---
- name: Baseline a fresh Ubuntu VPS
  hosts: vps
  become: true

  vars:
    deploy_user: deploy
    deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
    baseline_packages:
      - fail2ban
      - unattended-upgrades
      - ufw
    baseline_services:
      - fail2ban
      - unattended-upgrades

  tasks:
    - name: Create the deploy user
      ansible.builtin.user:
        name: "{{ deploy_user }}"
        groups: sudo
        append: true
        shell: /bin/bash

    - name: Install the deploy user's SSH key
      ansible.posix.authorized_key:
        user: "{{ deploy_user }}"
        key: "{{ deploy_pubkey }}"

    - name: Passwordless sudo for the deploy user
      ansible.builtin.copy:
        dest: /etc/sudoers.d/deploy
        content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s

    - name: Install baseline packages
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present
        update_cache: true

    - name: Enable and start baseline services
      ansible.builtin.service:
        name: "{{ item }}"
        state: started
        enabled: true
      loop: "{{ baseline_services }}"

    - name: Harden sshd with a drop-in
      ansible.builtin.copy:
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        content: |
          PasswordAuthentication no
          KbdInteractiveAuthentication no
          PermitRootLogin prohibit-password
          X11Forwarding no
        mode: "0644"
        validate: /usr/sbin/sshd -t -f %s
      notify: Restart ssh

    - name: Allow OpenSSH through ufw
      community.general.ufw:
        rule: allow
        name: OpenSSH

    - name: Enable ufw with default deny
      community.general.ufw:
        state: enabled
        policy: deny

  handlers:
    - name: Restart ssh
      ansible.builtin.service:
        name: ssh
        state: restarted

Các dòng cần hiểu thay vì sao chép:

Biến nằm dưới vars: và được tham chiếu bằng "{{ deploy_user }}". Hãy đặt toàn bộ biểu thức trong dấu ngoặc kép khi giá trị bắt đầu bằng dấu ngoặc nhọn, nếu không YAML parser sẽ phân tích sai. lookup('file', ...) đọc public key của bạn từ máy control tại runtime, nên playbook không chứa key material.

Vòng lặp. loop: "{{ baseline_services }}" chạy task service một lần cho mỗi item và output hiển thị từng item trên một dòng riêng. Lưu ý task apt nhận toàn bộ danh sách package trong một lần gọi. Một apt transaction nhanh hơn và là pattern được ưu tiên khi cài package; vòng lặp dành cho các module thực sự xử lý từng đối tượng một.

Handler là khái niệm cần nắm rõ. notify: Restart ssh không có nghĩa là “restart ssh ngay”. Nó xếp handler vào hàng đợi. Handler chạy một lần ở cuối play và chỉ chạy nếu task gửi notification thực sự báo trạng thái changed. Nếu ngày mai bạn chạy lại playbook, file drop-in đã đúng, task copy báo ok và sshd không bị restart. Dòng validate: là cơ chế an toàn cho trigger. sshd kiểm tra file trước khi thay thế file cũ, nên lỗi đánh máy sẽ làm task fail thay vì làm daemon bị hỏng.

Dùng PermitRootLogin prohibit-password, không dùng no, một cách có chủ đích. Playbook này đăng nhập bằng root với key. prohibit-password tắt đăng nhập root bằng password nhưng vẫn giữ phiên đăng nhập của bạn. Sau khi xác nhận deploy user hoạt động (ssh deploy@10.0.0.10 sudo true, dùng địa chỉ thuần, vì web1 chỉ là alias mà Ansible biết), hãy đổi sang ansible_user=deploy trong inventory và siết thành no ở lần chạy sau. Hãy harden theo thứ tự để không tự khóa quyền truy cập.

Prefix 00- rất quan trọng. Với hầu hết keyword, sshd sử dụng lần xuất hiện đầu tiên mà nó parse được. Ubuntu có sshd_config, file này include sshd_config.d/*.conf theo thứ tự từ điển trước phần cấu hình riêng của nó. Cloud image Ubuntu 24.04 đã có sẵn 60-cloudimg-settings.conf trong thư mục đó. Các provider bật đăng nhập bằng password qua cloud-init sẽ thêm 50-cloud-init.conf với PasswordAuthentication yes. Đặt file của chúng ta tên 00-hardening.conf khiến file này được sắp xếp đầu tiên và có hiệu lực trước cả hai file kia.

Thứ tự task bảo đảm an toàn cho firewall. Allow OpenSSH chạy trước Enable ufw với policy deny. Ansible thực thi task đúng theo thứ tự được liệt kê, nên lỗ truy cập tồn tại trước khi rule chặn được áp dụng. fail2ban không cần cấu hình thêm để hữu ích trong trường hợp này; default của Ubuntu theo dõi sshd ngay từ đầu. Phần giải thích jail thực sự làm gì và cần tinh chỉnh những gì có trong hướng dẫn fail2ban trên Ubuntu 24.04.

Bước 6: chạy thử bằng --check, sau đó chạy thật

ansible-playbook site.yml --check

Chế độ kiểm tra sẽ kết nối, tính toán những gì nó sẽ thực hiện và không thay đổi gì. Đọc số changed= trong PLAY RECAP ở cuối output. Đây là số task sẽ thay đổi từng host. Có một giới hạn cần lưu ý: chế độ kiểm tra có giới hạn về mặt cấu trúc khi một task phía sau phụ thuộc vào thay đổi do task phía trước tạo ra. Image server tiêu chuẩn của Ubuntu có sẵn ufw, nên playbook này chạy thử không lỗi. Nhưng trên image tối giản không có ufw, các task ufw sẽ fail trong chế độ kiểm tra vì chế độ này không thực sự cài package, nên module không có gì để gọi. Đây là giới hạn của dry run, không phải lỗi trong playbook. Khi kế hoạch trông đúng như mong muốn:

ansible-playbook site.yml

Mỗi task in một dòng cho từng host, màu vàng là changed, màu xanh lá là ok, và phần tổng kết sẽ có dạng:

PLAY RECAP *********************************************************************
web1 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0
web2 : ok=10  changed=9  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

Mười ok đó gồm việc thu thập facts, 8 task và handler. changed của bạn có thể lệch mine một hoặc hai đơn vị: image tiêu chuẩn của Ubuntu có sẵn ufwunattended-upgrades, còn fail2ban tự khởi động ngay khi apt cài nó. Vì vậy, một task có thể hợp lệ báo ok ngay trong lần chạy đầu tiên, tức là trạng thái mà task khai báo đã được giữ nguyên. Các số bắt buộc phải bằng 0 là unreachablefailed. Một lưu ý về become: true: nó chỉ là thủ tục khi bạn kết nối bằng root. Nhưng ngay khi đổi ansible_user thành deploy, sudo trở thành cơ chế thực sự, và file sudoers NOPASSWD mà playbook này cài chính là thứ giúp -K không xuất hiện trong command line. Nếu không có file này, bạn sẽ gặp Missing sudo password, được đề cập bên dưới.

Bước 7: chạy lần hai để thấy tính idempotence

Chạy lại ngay cùng lệnh đó:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0ok đều giảm 1 vì handler chưa được thông báo không chạy. Không có gì được cài lại, sshd không được restart và ufw không bị thay đổi. Đây là lý do playbook vừa là công cụ audit, vừa là công cụ provision: tháng sau, thêm web3 vào inventory rồi chạy lại; máy mới sẽ được dựng, còn các máy cũ sẽ được kiểm tra. changed khác 0 trên một máy mà bạn chưa chỉnh sửa là dấu hiệu cấu hình bị lệch, cho biết ai đó đã chỉnh thủ công thứ đáng lẽ phải được chỉnh trong playbook.

Từ đây, mô hình này tiếp tục mở rộng. Playbook tiếp theo đáng viết sẽ triển khai WireGuard VPN trên cùng VPS và siết rule ufw để SSH chỉ nhận kết nối qua tunnel; sau đó viết một playbook cài Docker và Compose trên mọi app server. Khi site.yml dài quá ba màn hình, hãy tách nó thành các role, nhưng đừng làm vậy trước thời điểm đó.

Các dạng lỗi và chuỗi bạn sẽ thấy

UNREACHABLE with Permission denied.

web1 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
    "unreachable": true
}

Kết nối SSH bị lỗi trước khi bất kỳ module nào chạy: ansible_user không đúng, key chưa được chép lên host đó hoặc SSH đang dùng sai key. Hãy kiểm tra lại bằng ssh root@10.0.0.10 thông thường, sau đó dùng ssh -v để xem những key nào đã được cung cấp. Nếu SSH bằng password hoạt động nhưng Ansible không hoạt động, bạn đã bỏ qua ssh-copy-id.

Missing sudo password.

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

Bạn đã đặt become: true, kết nối bằng user không phải root và user đó cần password để chạy sudo. Hãy thêm -K (--ask-become-pass) vào command line hoặc cấp cho user một entry NOPASSWD trong sudoers. Đây chính là lý do playbook cài entry đó cho deploy trước khi bạn chuyển sang user này.

error: externally-managed-environment. Bạn đã chạy pip trên Python hệ thống của Ubuntu 24.04. Nội dung này đã được đề cập ở bước 1: dùng pipx, không dùng pip và không dùng --break-system-packages.

mapping values are not allowed in this context.

ERROR! Syntax Error while loading YAML.
  mapping values are not allowed in this context

Gần như luôn là lỗi indentation: một key nằm sai cấp hoặc thiếu khoảng trắng sau dấu hai chấm. Số dòng được báo thường nằm gần vị trí lỗi, không nhất thiết đúng tại đó; hãy kiểm tra cả dòng phía trên. Lỗi tương tự found character '\t' that cannot start any token có nghĩa là một tab đã bị chèn vào; YAML không cho phép tab. Hãy biến ansible-playbook site.yml --syntax-check thành thao tác mặc định trước mỗi lần chạy và cấu hình editor dùng indentation hai khoảng trắng cho YAML.

/usr/bin/python3: not found. Lỗi này hiếm gặp trên image Ubuntu 24.04 tiêu chuẩn nhưng phổ biến trên image tối giản hoặc netboot: module execution bị lỗi vì target không có Python. Hãy bootstrap bằng module raw, module duy nhất không cần Python ở phía target: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, sau đó chạy lại playbook.

FAQ

Tôi có cần cài Ansible trên các server mà Ansible quản lý không?

Không. Ansible không cần agent: máy control đẩy các Python module nhỏ qua SSH, chạy chúng rồi xóa chúng. Máy đích chỉ cần python3 và quyền truy cập SSH; các image Ubuntu mặc định đã có cả hai. Trong toàn bộ guide này, lần cài đặt duy nhất diễn ra trên máy control.

Vì sao Ansible báo "Permission denied (publickey)"?

Block UNREACHABLE! cùng với Permission denied (publickey) cho biết xác thực SSH đã thất bại trước khi Ansible chạy bất kỳ lệnh nào. Kiểm tra ansible_user trong inventory có khớp với account bạn đã thiết lập không, bạn đã chạy ssh-copy-id đến host đó chưa, và ssh user@host thuần có đăng nhập mà không cần password không. Sửa được lệnh ssh thuần thì cũng sửa được Ansible, vì chúng dùng cùng một transport.

Idempotent trong Ansible nghĩa là gì?

Một task khai báo trạng thái mong muốn, chẳng hạn "package này đã được cài", "dòng này có trong file", thay vì khai báo một hành động cần thực hiện. Nếu trạng thái đó đã đúng, Ansible không làm gì và báo ok thay vì changed. Vì vậy, khi chạy playbook lần thứ hai, changed=0 sẽ xuất hiện, và việc chạy lại trở thành một lần audit an toàn thay vì cài đặt lại có rủi ro.

Tôi nên dùng pip hay pipx để cài Ansible trên Ubuntu 24.04?

pipx. Ubuntu 24.04 đánh dấu Python hệ thống là externally managed, nên pip install ansible cố ý thất bại với error: externally-managed-environment. pipx install --include-deps ansible đặt Ansible trong một virtualenv riêng và đưa ansible, ansible-playbook cùng các lệnh khác vào PATH một cách sạch sẽ.

Sự khác biệt giữa package ansible và ansible-core là gì?

ansible-core là engine chỉ kèm các module ansible.builtin. Package ansible bao gồm core cùng các community collection được tuyển chọn, trong đó có ansible.posix (module authorized_key) và community.general (module ufw), đều được dùng trong guide này. Hãy bắt đầu với package đầy đủ; chỉ chuyển sang core cùng các collection được chọn thủ công khi bạn có lý do cụ thể.