File Manipulation, Text Editing & Jinja2 Template Engine

Compare lineinfile, blockinfile, replace, and template modules, Jinja2 expression syntax, filters, template validation, and ansible_managed headers.

intermediate 25 min lesson hands-on task included

Configuring servers almost always involves editing configuration files (such as /etc/nginx/nginx.conf, /etc/ssh/sshd_config, /etc/sysctl.conf, or database configs).

Choosing the right module for file modification determines whether your automation is clean, idempotent, and maintainable — or fragile, buggy, and prone to breaking during updates.


Topic 1: Text Editing Module Matrix (lineinfile vs. blockinfile vs. replace vs. template)

JINJA2 TEMPLATING ENGINE & VARIABLE PRECEDENCE HIERARCHY TEMPLATE MODULE (templates/nginx.conf.j2 → /etc/nginx/nginx.conf) Jinja2 Source Template (.j2) {{ ansible_managed | comment }} worker_processes {{ nginx_workers }}; listen {{ http_port | default(80) }}; {% for server in upstream_servers %} Rendered File on Target (/etc/nginx/...) # Ansible managed: Do NOT edit manually worker_processes 4; listen 80; server 10.0.1.10:8080; VARIABLE PRECEDENCE LADDER (22 LEVELS — HIGHEST OVERRIDES LOWEST) 1. Extra Vars ansible-playbook -e "v=1" Highest Precedence 2. Task Vars vars: inside task Mid Precedence 3. Host Facts ansible_facts Mid Precedence 4. group_vars group_vars/all.yml Mid Precedence 5. Role Defaults roles/x/defaults/main.yml Lowest Precedence RED HAT BEST PRACTICE: PREFIX ROLE VARIABLES Always prefix role variables with the role name (e.g., `nginx_worker_processes` instead of `workers`) to prevent namespace collisions!
Jinja2 Template Engine: Rendering variables and loops from Jinja2 templates into target managed files with ansible_managed headers.

Ansible provides four primary modules for modifying configuration files:

ModuleBest Use CaseOperational Limitation
ansible.builtin.lineinfileModifying a single specific line (e.g., setting PermitRootLogin no in sshd_config)Fragile if attempting to edit multiple interdependent lines across a large file
ansible.builtin.blockinfileInserting a multiline block of text surrounded by comment markersLeaves block markers # BEGIN ANSIBLE MANAGED BLOCK in the target file
ansible.builtin.replaceRegex pattern replacement across an entire fileHard to maintain; requires complex regular expressions
ansible.builtin.templatePRODUCTION STANDARD for complete configs: Renders a dynamic Jinja2 .j2 templateReplaces the entire target file content

1. lineinfile Module Example:

Modifies or inserts a single line based on regex matching:

- name: Disable Password Authentication in SSH Daemon Config
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PasswordAuthentication'
    line: 'PasswordAuthentication no'
    state: present
    backup: yes
  notify: Restart SSH Daemon

2. blockinfile Module Example:

Inserts or updates a multiline block of text:

- name: Configure custom environment variables in /etc/environment
  ansible.builtin.blockinfile:
    path: /etc/environment
    marker: "# {mark} ANSIBLE MANAGED BLOCK — ENV VARS"
    block: |
      JAVA_HOME=/usr/lib/jvm/java-17-openjdk
      APP_ENV=production
      LOG_LEVEL=WARN

Topic 2: Jinja2 Template Engine Syntax & Expression Mechanics

For complex, multi-line configuration files (Nginx, HAProxy, PostgreSQL, Kubernetes manifests), using lineinfile becomes unmaintainable.

The Production Standard is to use the ansible.builtin.template module with Jinja2 .j2 template files.

Jinja2 Syntax Rules:

  1. Variables ({{ ... }}): Evaluates and outputs a variable value.
    • Example: listen {{ http_port }};
  2. Control Logic ({% ... %}): Executes loops, conditionals, and macro calls.
    • Example: {% if enable_ssl %} ssl_certificate /etc/ssl/cert.pem; {% endif %}
  3. Comments ({# ... #}): Internal template comments (completely stripped from the final rendered file).
    • Example: {# Internal note: updated for PCI-DSS compliance #}

Production Jinja2 Template Example (templates/nginx.conf.j2):

{# templates/nginx.conf.j2 — Production Nginx Template #}
{{ ansible_managed | comment }}

user www-data;
worker_processes {{ ansible_processor_vcpus | default(2) }};
pid /run/nginx.pid;

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Upstream Backend Server Pool (Looping over inventory group)
    upstream backend_pool {
{% for host in groups['appservers'] %}
        server {{ hostvars[host]['ansible_host'] }}:8080 max_fails=3 fail_timeout=10s;
{% endfor %}
    }

    server {
        listen {{ http_port | default(80) }};
        server_name {{ server_name | default(ansible_fqdn) }};

{% if enable_gzip | default(true) %}
        gzip on;
        gzip_types text/plain text/css application/json;
{% endif %}

        location / {
            proxy_pass http://backend_pool;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

Topic 3: Essential Jinja2 Filters

Filters modify or format variable values inside Jinja2 expressions using the pipe operator (|):

  • Default Fallback: {{ http_port | default(80) }} — Uses 80 if http_port is undefined.
  • List Join: {{ dns_servers | join(' ') }} — Converts ['8.8.8.8', '1.1.1.1'] to "8.8.8.8 1.1.1.1".
  • JSON / YAML Formatting: {{ my_dict | to_json }} or {{ my_dict | to_nice_yaml }}.
  • Base64 Encoding: {{ secret_token | b64encode }} / {{ encoded_val | b64decode }}.
  • Comment Header Filter: {{ ansible_managed | comment }} — Converts the ansible_managed string into proper comments based on the target file type (# for shell/nginx, /* */ for C/CSS).

Topic 4: Marking Managed Files ({{ ansible_managed }})

A common operational hazard in sysadmin teams is an engineer manually editing /etc/nginx/nginx.conf on a server, unaware that Ansible manages the file. The next time Ansible runs, the engineer’s manual edits are silently overwritten!

To prevent this, Red Hat best practice mandates adding {{ ansible_managed | comment }} at the top of every Jinja2 template:

{{ ansible_managed | comment }}

When rendered, Ansible prepends a clear warning header to the file:

#
# Ansible managed: Do NOT edit this file manually!
# Any local changes will be overwritten on the next playbook execution run.
#

Topic 5: Template Validation (validate:)

If a template task writes a syntax error to a core configuration file (e.g., missing a semicolon in nginx.conf) and then triggers a service restart handler, the service will crash and cause a production outage!

Ansible solves this with Template Validation (validate:):

- name: Deploy Production Nginx configuration with validation
  ansible.builtin.template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    validate: "nginx -t -c %s"    # %s is automatically replaced by the temp file path!
  notify: Reload Nginx

How validate: works:

  1. Ansible renders the template to a temporary file on the managed host.
  2. Ansible executes nginx -t -c /tmp/tempfile.
  3. If the validation command returns exit code 0, Ansible atomically overwrites /etc/nginx/nginx.conf.
  4. If validation returns non-zero (syntax error), Ansible ABORTS the task, leaves the live config file untouched, and reports a task failure!

Common mistake: Managing a whole config file with a series of lineinfile tasks. Each one is a regex against a file somebody else may have edited, the order matters, and the result is unreviewable. Once you need more than two lines changed, template with the full file is simpler, diffable in --check, and cannot half-apply.