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)
Ansible provides four primary modules for modifying configuration files:
| Module | Best Use Case | Operational Limitation |
|---|---|---|
ansible.builtin.lineinfile | Modifying 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.blockinfile | Inserting a multiline block of text surrounded by comment markers | Leaves block markers # BEGIN ANSIBLE MANAGED BLOCK in the target file |
ansible.builtin.replace | Regex pattern replacement across an entire file | Hard to maintain; requires complex regular expressions |
ansible.builtin.template | PRODUCTION STANDARD for complete configs: Renders a dynamic Jinja2 .j2 template | Replaces 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:
- Variables (
{{ ... }}): Evaluates and outputs a variable value.- Example:
listen {{ http_port }};
- Example:
- Control Logic (
{% ... %}): Executes loops, conditionals, and macro calls.- Example:
{% if enable_ssl %} ssl_certificate /etc/ssl/cert.pem; {% endif %}
- Example:
- Comments (
{# ... #}): Internal template comments (completely stripped from the final rendered file).- Example:
{# Internal note: updated for PCI-DSS compliance #}
- Example:
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) }}— Uses80ifhttp_portis 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 theansible_managedstring 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:
- Ansible renders the template to a temporary file on the managed host.
- Ansible executes
nginx -t -c /tmp/tempfile. - If the validation command returns exit code
0, Ansible atomically overwrites/etc/nginx/nginx.conf. - 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.