Ansible lineinfile Module: แก้ไข Single Line ในไฟล์

Ansible lineinfile module ใช้แก้ไขไฟล์ text โดยเพิ่ม, แก้ไข, หรือลบบรรทัดเดียวตาม pattern ที่กำหนด เหมาะสำหรับกรณีที่ต้องการแก้ค่า config เดียวในไฟล์ขนาดใหญ่ เช่น เปลี่ยน parameter ใน /etc/ssh/sshd_config หรือเพิ่ม entry ใน /etc/hosts โดยไม่ต้อง overwrite ไฟล์ทั้งหมด

บทความนี้อธิบายการใช้ lineinfile module ตั้งแต่การเพิ่ม/แก้ไข/ลบบรรทัด, regex matching, insertbefore/insertafter, และ blockinfile สำหรับการแทรกหลายบรรทัด

เพิ่มและแก้ไขบรรทัด

Parameter หลักคือ path (ไฟล์ที่แก้ไข), regexp (pattern ค้นหา), และ line (บรรทัดที่ต้องการให้มีในไฟล์) ถ้า regexp พบ match จะแทนด้วย line ถ้าไม่พบจะ append ท้ายไฟล์

---
- name: Configure SSH with lineinfile
  hosts: all
  become: true
  tasks:
    - name: Disable root login via SSH
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin no'
      notify: restart sshd

    - name: Set SSH port
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?Port '
        line: 'Port 2222'
      notify: restart sshd

    - name: Disable password authentication
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
      notify: restart sshd

  handlers:
    - name: restart sshd
      service:
        name: sshd
        state: restarted

regexp: '^#?PermitRootLogin' จะ match ทั้งบรรทัดที่ comment ไว้ (#PermitRootLogin) และที่ไม่ได้ comment (PermitRootLogin) ทำให้ idempotent — รันซ้ำก็ได้ผลเดิม

ลบบรรทัดด้วย state: absent

state: absent ลบบรรทัดที่ match regexp ออกจากไฟล์ ถ้าไม่มี match จะไม่ทำอะไร (idempotent)

---
- name: Clean up config file
  hosts: all
  become: true
  tasks:
    - name: Remove obsolete config line
      lineinfile:
        path: /etc/app/app.conf
        regexp: '^old_setting='
        state: absent

    - name: Remove specific host entry
      lineinfile:
        path: /etc/hosts
        regexp: '192\.168\.1\.100'
        state: absent

    - name: Remove commented debug lines
      lineinfile:
        path: /etc/nginx/nginx.conf
        regexp: '^#.*debug.*$'
        state: absent

insertafter และ insertbefore

insertafter แทรกบรรทัดใหม่หลัง pattern ที่กำหนด และ insertbefore แทรกก่อน pattern เหมาะสำหรับการเพิ่ม config ในตำแหน่งที่ถูกต้องในไฟล์

---
- name: Insert lines at specific positions
  hosts: all
  become: true
  tasks:
    - name: Add config after [database] section
      lineinfile:
        path: /etc/app/app.conf
        insertafter: '^\[database\]'
        line: 'connection_pool = 10'

    - name: Add include before end of http block
      lineinfile:
        path: /etc/nginx/nginx.conf
        insertbefore: '^}'
        line: '    include /etc/nginx/conf.d/*.conf;'

    - name: Add entry at beginning of file
      lineinfile:
        path: /etc/environment
        insertbefore: BOF    # Beginning Of File
        line: '# Managed by Ansible'

    - name: Add entry at end of file
      lineinfile:
        path: /etc/environment
        insertafter: EOF    # End Of File
        line: 'APP_ENV=production'

สร้างไฟล์ถ้ายังไม่มี

create: yes สร้างไฟล์ใหม่ถ้ายังไม่มีอยู่ก่อนที่จะ insert บรรทัด ป้องกัน error เมื่อไฟล์ปลายทางยังไม่ถูกสร้าง

---
- name: Manage environment variables
  hosts: all
  become: true
  tasks:
    - name: Set environment variable
      lineinfile:
        path: /etc/environment
        regexp: '^APP_VERSION='
        line: 'APP_VERSION=2.5.0'
        create: yes    # สร้างไฟล์ถ้าไม่มี
        owner: root
        mode: '0644'

    - name: Add PATH to profile
      lineinfile:
        path: /etc/profile.d/custom.sh
        regexp: '^export PATH=.*myapp'
        line: 'export PATH=$PATH:/opt/myapp/bin'
        create: yes
        mode: '0755'

backrefs — ใช้ Captured Group จาก Regex

backrefs: yes ทำให้ line ใช้ \1, \2 อ้างอิง captured groups จาก regexp เหมาะสำหรับแก้ค่าใน pattern เดิมโดยเก็บ prefix/suffix ไว้

---
- name: Use backrefs for precise editing
  hosts: all
  become: true
  tasks:
    # เปลี่ยนค่า max_connections แต่เก็บ format เดิม
    - name: Update max_connections value
      lineinfile:
        path: /etc/mysql/mysql.conf.d/mysqld.cnf
        regexp: '^(max_connections\s*=\s*)\d+'
        line: '\g<1>200'
        backrefs: yes

    # uncomment บรรทัดที่ถูก comment ไว้
    - name: Uncomment listen directive
      lineinfile:
        path: /etc/nginx/nginx.conf
        regexp: '^#(listen 80;)'
        line: '\1'
        backrefs: yes

blockinfile สำหรับหลายบรรทัด

เมื่อต้องแทรกหลายบรรทัดพร้อมกัน ใช้ blockinfile แทน lineinfile module นี้เพิ่ม marker comments รอบ block เพื่อให้ Ansible รู้ว่าส่วนไหนที่จัดการอยู่ ทำให้ idempotent แม้รันซ้ำ

---
- name: Add configuration blocks
  hosts: all
  become: true
  tasks:
    - name: Add SSH authorized keys block
      blockinfile:
        path: /etc/ssh/sshd_config
        block: |
          # Custom security settings
          AllowUsers deploy admin
          MaxAuthTries 3
          LoginGraceTime 30
        marker: "# {mark} ANSIBLE MANAGED BLOCK"

    - name: Add hosts entries
      blockinfile:
        path: /etc/hosts
        block: |
          192.168.10.10 app-server-01
          192.168.10.11 app-server-02
          192.168.10.20 db-primary
        marker: "# {mark} APPLICATION SERVERS"

    - name: Remove managed block
      blockinfile:
        path: /etc/ssh/sshd_config
        marker: "# {mark} ANSIBLE MANAGED BLOCK"
        state: absent

marker ใช้ {mark} เป็น placeholder ที่จะถูกแทนด้วย BEGIN และ END ทำให้ block มีขอบเขตชัดเจน เปลี่ยน marker ถ้าต้องการ block หลายอันในไฟล์เดียวกัน

Pattern: Harden SSH Configuration

ตัวอย่าง Playbook สำหรับ harden SSH configuration โดยใช้ lineinfile แก้ค่า parameter หลักๆ และ blockinfile เพิ่ม settings กลุ่ม

---
- name: Harden SSH Configuration
  hosts: all
  become: true
  tasks:
    - name: Set SSH hardening parameters
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
        - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
        - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
        - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
        - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
        - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 2' }
      notify: restart sshd

    - name: Add ciphers block
      blockinfile:
        path: /etc/ssh/sshd_config
        block: |
          Ciphers [email protected],[email protected]
          MACs hmac-sha2-512,hmac-sha2-256
          KexAlgorithms curve25519-sha256,diffie-hellman-group14-sha256
        marker: "# {mark} ANSIBLE SSH CRYPTO SETTINGS"
      notify: restart sshd

    - name: Validate SSH config before restart
      command: sshd -t
      changed_when: false

  handlers:
    - name: restart sshd
      service:
        name: sshd
        state: restarted

สรุป

lineinfile module เหมาะกับการแก้ไข config file ทีละบรรทัดโดยไม่ต้อง overwrite ทั้งไฟล์ Pattern ที่ควรจำ: ใช้ regexp: '^#?PARAM' เพื่อ match ทั้ง commented และ uncommented lines, ใช้ backrefs: yes เมื่อต้องการแก้เฉพาะค่าแต่เก็บ format เดิม, และใช้ blockinfile เมื่อต้องแทรกหลายบรรทัดพร้อม marker สำหรับ idempotency

ข้อแตกต่างสำคัญ: lineinfile จัดการหนึ่งบรรทัด, blockinfile จัดการหลายบรรทัดเป็น block, ส่วน template หรือ copy เหมาะกว่าเมื่อต้องการ manage ทั้งไฟล์