สอนใช้ Ansible เขียน Playbook บน Ubuntu 24.04
วิธีติดตั้ง Ansible ด้วย pipx บน Ubuntu 24.04 พร้อมวิธีเขียน Playbook เพื่อ Hardening VPS และวิธีแก้ไขปัญหา Permission denied หรือ sudo error อย่างละเอียด
สิ่งที่คุณกำลังสร้าง
คุณต้องมีเครื่องควบคุม (control machine) ที่ติดตั้ง Ansible ไว้หนึ่งเครื่อง และ VPS ที่ติดตั้ง Ubuntu 24.04 ใหม่ตั้งแต่เริ่มต้นอย่างน้อยหนึ่งเครื่อง เมื่อเสร็จสิ้นกระบวนการ คุณจะมีไฟล์ inventory ที่ระบุชื่อเซิร์ฟเวอร์ของคุณ มีการทดสอบ ad-hoc ping เพื่อยืนยันว่าการยืนยันตัวตนทำงานได้อย่างถูกต้อง และมี playbook ที่ใช้รันรายการตรวจสอบสำหรับ VPS ใหม่ในรูปแบบ code ได้แก่: การสร้าง user สำหรับ deploy พร้อมกับ SSH key ของคุณ, การตั้งค่า sshd ให้มีความปลอดภัย (hardened), การติดตั้ง fail2ban, การตั้งค่า unattended upgrades และการตั้งค่า firewall ที่อนุญาต OpenSSH ก่อนที่จะสั่งปฏิเสธการเชื่อมต่ออื่นทั้งหมด คุณสามารถเลือกใช้กับเซิร์ฟเวอร์หนึ่งเครื่องหรือยี่สิบเครื่องก็ได้ หากรันซ้ำเป็นครั้งที่สองจะไม่มีการเปลี่ยนแปลงใดๆ เกิดขึ้น ซึ่งนี่คือวัตถุประสงค์หลัก
จากประสบการณ์การติดตั้ง VPS มา 15 ปี ผมพบรูปแบบปัญหาที่เกิดขึ้นจริงคือ: ทุกคนมักตั้งค่าเซิร์ฟเวอร์ 5 เครื่องแรกด้วยตนเอง จากนั้นจะเสียเวลาช่วงวันหยุดไปกับเครื่องที่ 6 เนื่องจากไม่มีใครจำได้ว่าได้ตั้งค่าอะไรไว้ใน 5 เครื่องแรกบ้าง คู่มือนี้เป็นการต่อยอดเนื้อหาจาก การจัดการ Linux servers หลายเครื่อง — เริ่มศึกษาได้ทันทีที่คุณเริ่มพบว่าตนเองกำลังพิมพ์คำสั่ง apt install เดิมซ้ำกันใน 3 terminal
ความหมายของ Ansible ในหนึ่งย่อหน้า
Ansible เป็นระบบแบบ agentless โดยไม่จำเป็นต้องติดตั้ง daemon บนเครื่อง server ที่ต้องการจัดการ เครื่องควบคุมจะเชื่อมต่อผ่าน SSH ปกติ จากนั้นจะคัดลอก Python module ขนาดเล็กไปยังเครื่องเป้าหมาย เพื่อสั่งรันและอ่านค่า JSON ที่ส่งกลับมา แล้วจึงลบไฟล์นั้นทิ้ง สิ่งเดียวที่เครื่องเป้าหมายต้องมีคือ python3 ซึ่งมีอยู่ใน Ubuntu ทุก image มาตรฐาน คำสำคัญที่สำคัญคือ idempotent ซึ่งมีความหมายตรงตัวคือ task จะอธิบายถึง state ไม่ใช่การกระทำ ตัวอย่างเช่น state: present สำหรับ package หมายถึง "ตรวจสอบให้แน่ใจว่ามีการติดตั้งแล้ว" ไม่ใช่ "สั่งรันตัวติดตั้ง" หากสถานะเป็นไปตามที่กำหนดอยู่แล้ว Ansible จะไม่ดำเนินการใดๆ และจะรายงานผลเป็น ok แทนที่จะเป็น changed คุณสมบัตินี้คือหัวใจสำคัญของผลิตภัณฑ์ ซึ่งช่วยให้การรัน playbook ซ้ำมีความปลอดภัย และการรันซ้ำได้อย่างปลอดภัยนี้เองที่เปลี่ยน shell script ให้กลายเป็น infrastructure
สิ่งที่ต้องเตรียม และข้อควรระวังเบื้องต้น
- เครื่องควบคุม (Control machine): แล็ปท็อปของคุณ หรือ VPS ขนาดเล็ก สมมติว่าเป็น Ubuntu 24.04 สำหรับ macOS สามารถใช้งานได้เช่นเดียวกันหลังจากติดตั้ง pipx ผ่าน Homebrew แล้ว
- VPS เป้าหมายหนึ่งเครื่องหรือมากกว่าที่รัน Ubuntu 24.04 บน KVM และสามารถเข้าถึงด้วยสิทธิ์ root ได้ จะไม่มีการติดตั้งซอฟต์แวร์ใดๆ ลงบนเครื่องเหล่านี้
- การยืนยันตัวตนด้วย SSH key สำหรับทุกเครื่องเป้าหมาย Ansible จะใช้ระดับการยืนยันตัวตนเดียวกับคำสั่ง
sshของคุณ หากssh root@hostถามรหัสผ่าน Ansible จะทำงานล้มเหลว - บน Ubuntu 24.04
pip install ansibleจะใช้งานไม่ได้เนื่องจากerror: externally-managed-environmentซึ่งเป็นนโยบายของ distro ไม่ใช่ข้อผิดพลาดของระบบ ให้ใช้ pipx แทน - ช่องว่าง (Whitespace) ใน YAML คือไวยากรณ์ การย่อหน้าผิดจะทำให้เกิด
mapping values are not allowed in this contextและการใช้ตัวอักษร tab จะทำให้การทำงานล้มเหลวทันที - เปิดเซสชัน SSH ค้างไว้ในแต่ละเครื่องเป้าหมายในขณะที่ playbook กำลังปรับปรุงความปลอดภัยของ sshd ทุกกรณีที่ลูกค้าต้องกู้คืนระบบเนื่องจากถูกล็อกออก เกิดจากการปิดเซสชันสุดท้ายเพื่อ "ทดสอบระบบใหม่"
Step 1: ติดตั้ง Ansible บน control machine ด้วย pipx แทนการใช้ pip
โดยปกติคนมักจะใช้ pip3 install ansible แต่หากใช้ Ubuntu 24.04 ที่เพิ่งติดตั้งใหม่และพบข้อผิดพลาดในขั้นตอนแรกคือ Command 'pip3' not found, but can be installed with: sudo apt install python3-pip การติดตั้ง pip จะทำให้เกิดปัญหาตามมาดังนี้:
pip3 install ansibleerror: 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 กำหนดให้ system Python เป็นแบบ externally managed (PEP 668) เพื่อป้องกันไม่ให้ pip ทำงานทับซ้อนกับ apt ในไฟล์ชุดเดียวกัน ห้ามใช้ --break-system-packages เนื่องจากชื่อ flag ระบุไว้อย่างชัดเจน วิธีที่ถูกต้องคือการใช้ pipx ซึ่งจะสร้าง virtualenv แยกเฉพาะให้ Ansible และเพิ่ม binaries ลงใน PATH ของคุณ:
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansibleหลังจากรัน pipx ensurepath ให้เปิด shell ใหม่เพื่อให้การเปลี่ยนแปลง PATH มีผล หากเจอ --include-deps แสดงว่าไม่ใช่เรื่องบังเอิญ เนื่องจาก package ansible ไม่มี console scripts เป็นของตัวเอง โดยที่ ansible, ansible-playbook และตัวอื่น ๆ เป็นเพียง entry points ของ dependency ansible-core ดังนั้นหากไม่ระบุ flag pipx จะปฏิเสธการติดตั้งด้วยข้อความ No apps associated with package ansible or its dependencies และควรติดตั้ง package ansible แทนการใช้ ansible-core แบบปกติ เนื่องจาก package ตัวเต็มได้รวม community collections ไว้แล้ว ซึ่ง playbook นี้มีการใช้ module จากสอง collection นั้น (ansible.posix และ community.general)
ansible --versionผลลัพธ์ที่ถูกต้องควรเริ่มต้นด้วยบรรทัดลักษณะ ansible [core 2.19.x] และระบุชื่อ Python ที่ใช้งานอยู่ โดยสามารถใช้ Python core release เวอร์ชันใดก็ได้ในปัจจุบัน หากเจอ ansible: command not found หมายความว่า ~/.local/bin ยังไม่ได้อยู่ใน PATH ให้เปิด shell ใหม่ หรือใช้ source ~/.bashrc
ขั้นตอนการติดตั้งสิ้นสุดเพียงเท่านี้ ส่วนเครื่อง target จะยังไม่มีการติดตั้งใด ๆ
Step 2: การเข้าถึงผ่าน SSH key ไปยังทุก target
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20จากนั้นให้ทดสอบกับ host แต่ละเครื่องหนึ่งครั้ง:
ssh root@10.0.0.10 true && echo okคำสั่งบรรทัดเดียวนี้ทำหน้าที่สองอย่าง: คือการยืนยันว่าการยืนยันตัวตนด้วย key ทำงานได้โดยไม่ต้องใช้ password และเป็นการบันทึก host key ลงใน known_hosts โปรดดำเนินการทันที เนื่องจาก Ansible จะแสดง host key ที่ยังไม่ได้บันทึกเป็น prompt แบบ interactive ระหว่างการทำงาน ซึ่งอาจทำให้เข้าใจผิดว่าระบบค้างได้
Step 3: the inventory — ใช้ INI ในช่วงแรก และใช้ YAML เมื่อระบบขยายตัว
Inventory คือไฟล์ข้อความที่ระบุรายชื่อเครื่องที่ Ansible สามารถเข้าถึงได้ ให้สร้าง inventory.ini ใน directory ของโปรเจกต์ใหม่:
[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20
[vps:vars]
ansible_user=rootweb1 คือ alias ที่คุณกำหนดขึ้น ซึ่งจะปรากฏในผลลัพธ์และใช้เป็นเป้าหมายสำหรับ --limit web1 ส่วน ansible_host คือ address จริง [vps] คือ group และ [vps:vars] ใช้สำหรับกำหนด variables ให้กับทุก host ใน group นั้น ส่วน ansible_user คือ user ที่ Ansible ใช้ในการ login ถัดจากนั้นคือ ansible.cfg เพื่อให้คุณไม่ต้องพิมพ์ -i อีกต่อไป:
[defaults]
inventory = inventory.iniAnsible จะอ่าน ansible.cfg จาก directory ปัจจุบัน สำหรับ inventory รูปแบบ YAML — ให้บันทึกเป็น inventory.yml และชี้ ansible.cfg ไปที่ชื่อดังกล่าวแทน — จะเป็นรูปแบบที่คุณต้องการเมื่อแต่ละ host มี variables จำนวนมาก:
vps:
hosts:
web1:
ansible_host: 10.0.0.10
web2:
ansible_host: 10.0.0.20
vars:
ansible_user: rootทั้งสองรูปแบบมีคุณสมบัติเท่ากัน การใช้ INI จะตรวจสอบได้ง่ายกว่าเมื่อมีเพียง 2 server แต่ YAML จะรองรับการขยายตัวได้ดีกว่าเมื่อมีถึง 20 server ให้เลือกรูปแบบใดรูปแบบหนึ่งแล้วใช้งานได้เลย
Step 4: ad-hoc commands — the green pong that proves everything
ansible all -m pingนี่ไม่ใช่ ICMP แต่เป็นกระบวนการทดสอบระบบเต็มรูปแบบของ module ping ซึ่งประกอบด้วยการ login ผ่าน SSH, การคัดลอก module, การรัน Python บนเครื่องเป้าหมาย และการล้างข้อมูลหลังเสร็จสิ้น ผลลัพธ์ที่ถูกต้องคือแถบสีเขียว โดยจะปรากฏหนึ่งบล็อกต่อหนึ่ง host:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}แถบสีเขียว SUCCESS หมายถึงการยืนยันตัวตน (authentication), Python interpreter และการรับส่งข้อมูล (transport) ทำงานได้ปกติ ซึ่งหมายความว่า playbook จะทำงานได้เช่นกัน หากเป็นแถบสีแดง UNREACHABLE! หมายถึงการรับส่งข้อมูลล้มเหลวก่อนที่ module จะเริ่มทำงาน โดยสามารถดูรายละเอียดข้อความแสดงข้อผิดพลาดและวิธีแก้ไขได้ในส่วน failure modes ด้านล่าง นอกจากนี้ยังมี ad-hoc commands อีก 2 คำสั่งที่ควรทราบ:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomeAd-hoc ใช้สำหรับการทำงานครั้งเดียวหรือการตรวจสอบเท่านั้น หากคำสั่งใดที่คุณต้องรันซ้ำมากกว่าหนึ่งครั้ง ควรนำไปเขียนไว้ใน playbook
Step 5: the first playbook — the new-VPS checklist as code
นี่คือขั้นตอนทั้งหมดที่คุณต้องทำด้วยตนเองในช่วง 10 นาทีแรกบนเซิร์ฟเวอร์ใหม่ ให้บันทึกไฟล์นี้ในชื่อ 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ส่วนสำคัญที่ควรทำความเข้าใจแทนที่จะคัดลอกเพียงอย่างเดียว:
Variables จะอยู่ภายใต้ vars: และอ้างอิงด้วย "{{ deploy_user }}" — ให้ใส่เครื่องหมายคำพูดครอบทั้งนิพจน์หากค่าเริ่มต้นด้วยปีกกา เพื่อป้องกันไม่ให้ YAML parser อ่านค่าผิดพลาด ส่วน lookup('file', ...) จะอ่าน public key จากเครื่อง control ในขณะรันงาน ทำให้ playbook ไม่ต้องเก็บข้อมูลคีย์ไว้ในตัว
The loop. loop: "{{ baseline_services }}" จะรัน task ของ service หนึ่งครั้งต่อหนึ่งรายการ และแสดงผลลัพธ์แยกกันทีละบรรทัด โปรดทราบว่า task ของ apt จะรับรายการ package ทั้งหมดในครั้งเดียว — การทำ apt transaction เพียงครั้งเดียวจะเร็วกว่าและเป็นรูปแบบที่แนะนำสำหรับ package; การใช้ loop เหมาะสำหรับ module ที่ต้องจัดการทีละรายการเท่านั้น
The handler คือแนวคิดที่ต้องทำความเข้าใจ notify: Restart ssh ไม่ได้หมายถึง "restart ssh ทันที" แต่เป็นการจัดคิว handler เพื่อให้ทำงานหนึ่งครั้งเมื่อจบ play และจะทำงาน เฉพาะ เมื่อ task ที่ส่งการแจ้งเตือน (notifying task) รายงานค่า changed เท่านั้น หากรัน playbook ซ้ำในวันพรุ่งนี้: ไฟล์ drop-in จะถูกต้องอยู่แล้ว, task การคัดลอกจะรายงานค่า ok และ sshd จะไม่ถูก restart ส่วนบรรทัด validate: คือระบบความปลอดภัย — sshd จะตรวจสอบไฟล์ก่อนที่จะแทนที่ไฟล์เดิม ดังนั้นหากพิมพ์ผิด task จะล้มเหลวแทนที่จะทำให้ daemon เสียหาย
PermitRootLogin prohibit-password ไม่ใช่ no — อย่างตั้งใจ Playbook นี้จะล็อกอินด้วยสิทธิ์ root โดยใช้ key ส่วน prohibit-password จะปิดการล็อกอิน root ด้วย password ในขณะที่ยังคงรักษาการเข้าถึงของคุณไว้ เมื่อยืนยันผู้ใช้สำหรับ deploy แล้ว (ssh deploy@10.0.0.10 sudo true — ใช้ที่อยู่แบบปกติ เนื่องจาก web1 เป็นเพียง alias ที่ Ansible เท่านั้นที่รู้จัก) ให้เปลี่ยน ansible_user=deploy ใน inventory และปรับให้เข้มงวดขึ้นเป็น no ในการรันครั้งถัดไป การทำ hardening ควรทำตามลำดับที่ป้องกันไม่ให้คุณถูกตัดขาดจากการเข้าถึงระบบ
ความสำคัญของ prefix 00- สำหรับ keyword ส่วนใหญ่ sshd จะยึดตามค่า แรก ที่อ่านเจอ และ sshd_config ของ Ubuntu จะรวม sshd_config.d/*.conf ไว้ตามลำดับอักษรก่อนเนื้อหาหลัก ส่วน Ubuntu 24.04 cloud images จะมีไฟล์ 60-cloudimg-settings.conf อยู่ใน directory นั้นอยู่แล้ว และผู้ให้บริการที่เปิดใช้งาน password logins ผ่าน cloud-init จะเพิ่ม 50-cloud-init.conf พร้อมกับ PasswordAuthentication yes; การตั้งชื่อของเราเป็น 00-hardening.conf จะทำให้ไฟล์ถูกจัดลำดับไว้ก่อนและมีผลเหนือกว่าทั้งสองแบบ
ลำดับของ task คือความปลอดภัยของ firewall Allow OpenSSH จะทำงานก่อน Enable ufw ที่มีนโยบาย deny — Ansible จะรัน task ตามลำดับที่ระบุไว้เท่านั้น ดังนั้นช่องโหว่จะถูกสร้างขึ้นก่อนที่กำแพงจะถูกสร้างขึ้น fail2ban ไม่จำเป็นต้องตั้งค่าเพิ่มเติมเพื่อให้ใช้งานได้ในกรณีนี้; ค่าเริ่มต้นของ Ubuntu จะเฝ้าติดตาม sshd ให้ทันที ส่วนรายละเอียดการทำงานของ jails และการปรับแต่งค่าต่างๆ สามารถอ่านได้ใน คู่มือ fail2ban บน Ubuntu 24.04
Step 6: ทดสอบด้วย --check จากนั้นจึงเริ่มทำงานจริง
ansible-playbook site.yml --checkCheck mode จะทำการเชื่อมต่อและคำนวณสิ่งที่ระบบ จะ ทำ โดยไม่มีการเปลี่ยนแปลงใดๆ ให้ตรวจสอบจำนวน changed= ใน PLAY RECAP ที่ด้านล่าง ซึ่งคือจำนวน task ที่จะทำการแก้ไขแต่ละ host ข้อควรระวังอย่างหนึ่งคือ check mode มีข้อจำกัดเชิงโครงสร้างในกรณีที่ task ถัดไปต้องพึ่งพาการเปลี่ยนแปลงจาก task ก่อนหน้า เนื่องจาก Ubuntu standard server image มี ufw ติดตั้งมาให้ล่วงหน้า การทำ dry-run ด้วย playbook นี้จึงผ่านไปได้ด้วยดี แต่หากใช้ minimal image ที่ไม่มี ufw task ของ ufw จะ ล้มเหลว ใน check mode เนื่องจาก check mode ไม่ได้ทำการติดตั้ง package จริง ทำให้ module ไม่มีคำสั่งให้เรียกใช้งาน นี่คือข้อจำกัดของการทำ dry run ไม่ใช่ข้อผิดพลาดใน playbook เมื่อแผนงานถูกต้องแล้ว ให้ดำเนินการดังนี้:
ansible-playbook site.ymlแต่ละ task จะแสดงผลหนึ่งบรรทัดต่อหนึ่ง host โดยใช้สีเหลือง changed และสีเขียว ok และสรุปผลควรเป็นดังนี้:
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จำนวน ok ประกอบด้วยการรวบรวมข้อมูล (fact-gathering) บวกกับ 8 tasks และ handler ค่า changed ของคุณอาจแตกต่างจากของผม 1 หรือ 2 ค่า เนื่องจาก Ubuntu standard image มี ufw และ unattended-upgrades ติดตั้งมาให้ และ fail2ban จะเริ่มทำงานทันทีที่ apt ติดตั้งเสร็จ ดังนั้น task อาจรายงานผลเป็น ok ในการรันครั้งแรก เนื่องจากระบบอยู่ในสถานะที่กำหนดไว้แล้ว ตัวเลขที่ต้องเป็นศูนย์คือ unreachable และ failed ข้อควรระวังเกี่ยวกับ become: true คือเป็นเพียงขั้นตอนทางพิธีการขณะเชื่อมต่อด้วย root แต่เมื่อคุณเปลี่ยน ansible_user เป็น deploy ระบบจะใช้ sudo จริง และไฟล์ sudoers แบบ NOPASSWD ที่ playbook นี้ติดตั้งให้ คือสิ่งที่ช่วยป้องกันไม่ให้เกิด -K บน command line หากไม่มีสิ่งนี้ คุณจะพบกับ Missing sudo password ซึ่งมีรายละเอียดระบุไว้ด้านล่าง
Step 7: run it twice — what idempotence looks like
รันคำสั่งเดิมซ้ำอีกครั้งทันที:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 และ ok ลดลงหนึ่งหน่วย เนื่องจาก handler ที่ไม่ได้รับแจ้งเหตุไม่ได้ทำงาน ไม่มีการติดตั้งซอฟต์แวร์ใหม่ ไม่มีการ restart sshd และไม่มีการแก้ไข ufw สิ่งนี้ทำให้ playbook ทำหน้าที่เป็นทั้งเครื่องมือตรวจสอบ (audit) และเครื่องมือติดตั้ง (provisioner): หากเพิ่ม web3 ลงใน inventory ในเดือนหน้าแล้วรันซ้ำ เครื่องใหม่จะถูกติดตั้ง ส่วนเครื่องเก่าจะถูกตรวจสอบความถูกต้อง ค่า changed ที่ไม่เป็นศูนย์บนเครื่องที่คุณไม่ได้แก้ไขคือค่า drift ซึ่งบ่งบอกว่ามีคนเข้าไปแก้ไขด้วยตนเองในส่วนที่ควรจะแก้ไขผ่าน playbook
รูปแบบนี้จะขยายผลต่อไป playbook ถัดไปที่ควรเขียนคือการติดตั้ง WireGuard VPN บน VPS เครื่องเดิม และจำกัดกฎ ufw ให้ SSH ตอบสนองผ่าน tunnel เท่านั้น หลังจากนั้นคือการติดตั้ง Docker and Compose บน app server ทุกเครื่อง เมื่อ site.yml มีความยาวเกินสามหน้าจอ ให้แยกออกเป็น roles — แต่ห้ามทำก่อนหน้านั้น
Failure modes, with the strings you will see
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
}การเชื่อมต่อ SSH ล้มเหลวก่อนที่ module จะเริ่มทำงาน: ansible_user ไม่ถูกต้อง, ไม่มีการคัดลอก key ไปยัง host นั้น หรือมีการใช้ key ที่ไม่ถูกต้อง สามารถตรวจสอบซ้ำได้ด้วยคำสั่ง ssh root@10.0.0.10 จากนั้นใช้ ssh -v เพื่อดูว่ามีการเสนอ key ใดบ้าง หาก SSH แบบใช้ password ใช้งานได้แต่ Ansible ใช้งานไม่ได้ แสดงว่าคุณข้ามขั้นตอน ssh-copy-id
Missing sudo password.
web1 | FAILED! => {
"msg": "Missing sudo password"
}คุณตั้งค่า become: true และเชื่อมต่อด้วย user ที่ไม่ใช่ root ซึ่ง user นั้นจำเป็นต้องใช้ password สำหรับ sudo ให้แก้ไขโดยการเพิ่ม -K (--ask-become-pass) ใน command line หรือกำหนด sudoers entry เป็น NOPASSWD ให้แก่ user นั้น ซึ่งเป็นเหตุผลที่ playbook ทำการติดตั้งค่าดังกล่าวให้แก่ deploy ก่อนที่คุณจะสลับไปใช้งาน
error: externally-managed-environment. คุณใช้ pip กับ system Python บน Ubuntu 24.04 ซึ่งอธิบายไว้ในขั้นตอนที่ 1: ต้องใช้ pipx แทน pip และไม่ใช่ --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สาเหตุเกือบทั้งหมดเกิดจากการย่อหน้า (indentation): การวาง key ในระดับความลึกที่ไม่ถูกต้อง หรือลืมเว้นวรรคหลังเครื่องหมาย colon หมายเลขบรรทัดที่แจ้งเตือนจะอยู่ ใกล้กับ จุดที่ผิดพลาด ไม่ใช่จุดที่ผิดโดยตรง ให้ตรวจสอบบรรทัดด้านบนประกอบด้วย ส่วนข้อผิดพลาด found character '\t' that cannot start any token หมายถึงมีการใช้ tab ซึ่ง YAML ไม่อนุญาต ให้ฝึกใช้ ansible-playbook site.yml --syntax-check จนเป็นนิสัยก่อนเริ่มรันทุกครั้ง และตั้งค่า editor ให้ใช้การย่อหน้าแบบ two-space สำหรับไฟล์ YAML
/usr/bin/python3: not found. พบได้น้อยใน Ubuntu 24.04 image มาตรฐาน แต่พบได้บ่อยในแบบ minimal หรือ netboot: การรัน module ล้มเหลวเนื่องจากเครื่องปลายทางไม่มี Python ให้ติดตั้ง Python ด้วย module raw ซึ่งเป็น module เดียวที่ไม่ต้องการสิ่งใดบนเครื่องปลายทาง: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become จากนั้นจึงรัน playbook ใหม่อีกครั้ง
FAQ
Do I need to install Ansible on the servers it manages?
No. Ansible is agentless: the control machine pushes small Python modules over SSH, runs them, and removes them. A target needs only python3 and SSH access, both of which stock Ubuntu images already have. The only install in this entire guide happens on your control machine.
Why does Ansible say "Permission denied (publickey)"?
The UNREACHABLE! block with Permission denied (publickey) means SSH authentication failed before Ansible ran anything. Check that ansible_user in the inventory matches the account you actually set up, that you ran ssh-copy-id to that host, and that plain ssh user@host logs in without a password. Whatever fixes the plain ssh command fixes Ansible, because they are the same transport.
What does idempotent mean in Ansible?
A task declares a desired state — "this package is present", "this line is in this file" — rather than an action to perform. If the state already holds, Ansible does nothing and reports ok instead of changed. That is why running a playbook twice shows changed=0 the second time, and why a rerun is a safe audit rather than a risky re-install.
Should I use pip or pipx to install Ansible on Ubuntu 24.04?
pipx. Ubuntu 24.04 marks the system Python as externally managed, so pip install ansible fails with error: externally-managed-environment by design. pipx install --include-deps ansible puts Ansible in an isolated virtualenv and exposes ansible, ansible-playbook, and the rest on your PATH cleanly.
What is the difference between the ansible and ansible-core packages?
ansible-core is the engine plus only the ansible.builtin modules. The ansible package bundles core with the curated community collections — including ansible.posix (the authorized_key module) and community.general (the ufw module), both used in this guide. Start with the full package; slim down to core plus hand-picked collections only when you have a reason to.