Introduction

ansible.builtin.blockinfile inserts, updates, or removes a block of text in a file. Unlike lineinfile (single lines), blockinfile manages multi-line content — configuration blocks, script sections, and structured entries. It uses marker comments to track its blocks.

Basic Usage

- name: Add SSH config block
  ansible.builtin.blockinfile:
    path: /etc/ssh/sshd_config
    block: |
      Match Group developers
        AllowTcpForwarding yes
        X11Forwarding no
        PermitTunnel no
  notify: Restart sshd

This produces:

# BEGIN ANSIBLE MANAGED BLOCK
Match Group developers
  AllowTcpForwarding yes
  X11Forwarding no
  PermitTunnel no
# END ANSIBLE MANAGED BLOCK

Parameters

ParameterDefaultDescription
path(required)File to modify
block""Text block to insert (empty = remove block)
marker"# {mark} ANSIBLE MANAGED BLOCK"Marker template ({mark} → BEGIN/END)
marker_beginBEGINText for {mark} in opening marker
marker_endENDText for {mark} in closing marker
insertafterEOFInsert after this regex
insertbefore—Insert before this regex
createfalseCreate file if it doesn't exist
statepresentpresent or absent
backupfalseCreate backup before modifying
owner—File owner
group—File group
mode—File permissions

Custom Markers

Use custom markers to manage multiple blocks in the same file:

# Block 1: Database config
- name: Add database config
  ansible.builtin.blockinfile:
    path: /etc/myapp/config.conf
    marker: "# {mark} DATABASE CONFIG"
    block: |
      db_host=db.internal
      db_port=5432
      db_name=production

# Block 2: Cache config (same file, different marker)
- name: Add cache config
  ansible.builtin.blockinfile:
    path: /etc/myapp/config.conf
    marker: "# {mark} CACHE CONFIG"
    block: |
      cache_host=redis.internal
      cache_port=6379
      cache_ttl=3600

Result:

# BEGIN DATABASE CONFIG
db_host=db.internal
db_port=5432
db_name=production
# END DATABASE CONFIG
# BEGIN CACHE CONFIG
cache_host=redis.internal
cache_port=6379
cache_ttl=3600
# END CACHE CONFIG

Insert Position

# After a specific line
- name: Add after [mysqld]
  ansible.builtin.blockinfile:
    path: /etc/mysql/my.cnf
    insertafter: '^\[mysqld\]'
    block: |
      innodb_buffer_pool_size = 1G
      max_connections = 200
      slow_query_log = 1

# Before a specific line
- name: Add before [client]
  ansible.builtin.blockinfile:
    path: /etc/mysql/my.cnf
    insertbefore: '^\[client\]'
    block: |
      [mysqldump]
      quick
      max_allowed_packet = 64M

# At beginning of file (BOF)
- name: Add at top
  ansible.builtin.blockinfile:
    path: /etc/hosts
    insertbefore: BOF
    block: |
      # Internal services
      10.0.0.10 db.internal
      10.0.0.11 cache.internal

# At end of file (EOF) — default
- name: Add at bottom
  ansible.builtin.blockinfile:
    path: /etc/hosts
    block: |
      10.0.0.20 app1.internal
      10.0.0.21 app2.internal

Remove a Block

# Remove by setting state: absent
- name: Remove database config
  ansible.builtin.blockinfile:
    path: /etc/myapp/config.conf
    marker: "# {mark} DATABASE CONFIG"
    state: absent

# Or set empty block
- name: Remove default block
  ansible.builtin.blockinfile:
    path: /etc/myapp/config.conf
    block: ""

Practical Patterns

/etc/hosts Management

- name: Add application hosts
  ansible.builtin.blockinfile:
    path: /etc/hosts
    marker: "# {mark} APP HOSTS - {{ env }}"
    block: |
      {% for host in app_hosts %}
      {{ host.ip }} {{ host.name }}.{{ domain }}
      {% endfor %}

Nginx Server Block

- name: Add nginx upstream
  ansible.builtin.blockinfile:
    path: /etc/nginx/conf.d/upstream.conf
    create: true
    marker: "# {mark} UPSTREAM {{ app_name }}"
    block: |
      upstream {{ app_name }} {
      {% for server in app_servers %}
          server {{ server }}:{{ app_port }};
      {% endfor %}
      }
  notify: Reload nginx

SSH Authorized Keys

- name: Add team SSH keys
  ansible.builtin.blockinfile:
    path: "/home/{{ user }}/.ssh/authorized_keys"
    create: true
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: "0600"
    marker: "# {mark} TEAM KEYS"
    block: |
      {% for key in team_ssh_keys %}
      {{ key }}
      {% endfor %}

Sudoers Configuration

- name: Add deploy sudoers
  ansible.builtin.blockinfile:
    path: /etc/sudoers.d/deploy
    create: true
    mode: "0440"
    validate: "visudo -cf %s"
    marker: "# {mark} DEPLOY USER RULES"
    block: |
      deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myapp
      deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload nginx

Crontab Entries

- name: Add maintenance cron jobs
  ansible.builtin.blockinfile:
    path: /etc/cron.d/myapp-maintenance
    create: true
    mode: "0644"
    marker: "# {mark} MYAPP MAINTENANCE"
    block: |
      # Database backup - daily at 2 AM
      0 2 * * * postgres pg_dump myapp > /backups/myapp-$(date +\%F).sql
      # Log rotation - daily at 3 AM
      0 3 * * * root find /var/log/myapp -name '*.log' -mtime +30 -delete
      # Health check - every 5 minutes
      */5 * * * * deploy curl -sf http://localhost:8080/health || systemctl restart myapp

Environment File

- name: Set environment variables
  ansible.builtin.blockinfile:
    path: /opt/myapp/.env
    create: true
    owner: deploy
    mode: "0600"
    marker: "# {mark} ANSIBLE MANAGED"
    block: |
      APP_ENV={{ env }}
      DATABASE_URL=postgresql://{{ db_user }}:{{ db_pass }}@{{ db_host }}:5432/{{ db_name }}
      REDIS_URL=redis://{{ redis_host }}:6379
      SECRET_KEY={{ secret_key }}

blockinfile vs lineinfile

blockinfilelineinfile
ScopeMulti-line blocksSingle lines
TrackingMarker commentsRegex matching
UpdatesReplace entire blockReplace matched line
Best forConfig sections, multi-line entriesSingle settings, one-liners
# lineinfile — single setting
- lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'

# blockinfile — config section
- blockinfile:
    path: /etc/ssh/sshd_config
    marker: "# {mark} SFTP CONFIG"
    block: |
      Match Group sftp-users
        ChrootDirectory /home/%u
        ForceCommand internal-sftp
        AllowTcpForwarding no

Troubleshooting

Block Inserted Multiple Times

This happens without proper markers. Always use unique marker values for different blocks in the same file:

# WRONG — both use default marker, second overwrites first
- blockinfile: { path: /etc/hosts, block: "10.0.0.1 app1" }
- blockinfile: { path: /etc/hosts, block: "10.0.0.2 app2" }

# RIGHT — unique markers
- blockinfile: { path: /etc/hosts, marker: "# {mark} APP1", block: "10.0.0.1 app1" }
- blockinfile: { path: /etc/hosts, marker: "# {mark} APP2", block: "10.0.0.2 app2" }

Marker Syntax Matters

{mark} is replaced with BEGIN/END. Don't forget it:

# WRONG — no {mark}
marker: "# ANSIBLE BLOCK"

# RIGHT
marker: "# {mark} ANSIBLE BLOCK"
# Produces: # BEGIN ANSIBLE BLOCK / # END ANSIBLE BLOCK

Conclusion

blockinfile manages multi-line content blocks in files using marker comments. Use unique marker values when managing multiple blocks in the same file. Control placement with insertafter/insertbefore. Always validate critical files like sudoers. For single-line changes, use lineinfile; for multi-line sections, blockinfile; for entire files, template. The markers make blocks fully idempotent — Ansible finds and updates its block on every run.