Introduction

GRUB (GRand Unified Bootloader) controls which kernel boots, with what parameters, and how the boot menu behaves. Ansible automates GRUB configuration — set kernel command-line parameters, change default boot entries, configure timeouts, add password protection, and manage multi-kernel environments. Every change requires regenerating grub.cfg, which Ansible handles automatically.

How GRUB Config Works

/etc/default/grub          ← Your settings (Ansible edits this)
        │
        ▼
update-grub / grub2-mkconfig  ← Regenerates grub.cfg
        │
        ▼
/boot/grub/grub.cfg        ← Actual boot config (never edit directly)

Set Kernel Parameters

---
- name: Configure GRUB kernel parameters
  hosts: all
  become: true
  vars:
    grub_cmdline_defaults:
      - quiet
      - splash
    grub_cmdline_extras:
      - transparent_hugepage=never
      - elevator=noop
      - intel_iommu=on
  tasks:
    - name: Set GRUB_CMDLINE_LINUX_DEFAULT
      ansible.builtin.lineinfile:
        path: /etc/default/grub
        regexp: '^GRUB_CMDLINE_LINUX_DEFAULT='
        line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ (grub_cmdline_defaults + grub_cmdline_extras) | join(" ") }}"'
      notify: update grub

  handlers:
    - name: update grub
      ansible.builtin.command: >
        {{ 'update-grub' if ansible_os_family == 'Debian' else 'grub2-mkconfig -o /boot/grub2/grub.cfg' }}
      changed_when: true

Add/Remove Individual Parameters

- name: Add kernel parameter
  ansible.builtin.replace:
    path: /etc/default/grub
    regexp: '(GRUB_CMDLINE_LINUX_DEFAULT="[^"]*)'
    replace: '\1 transparent_hugepage=never'
  when: >
    'transparent_hugepage=never' not in
    lookup('file', '/etc/default/grub')
  notify: update grub

- name: Remove kernel parameter
  ansible.builtin.replace:
    path: /etc/default/grub
    regexp: '\s*transparent_hugepage=\S*'
    replace: ''
  notify: update grub

Configure Boot Timeout

- name: Set GRUB timeout
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_TIMEOUT='
    line: 'GRUB_TIMEOUT=5'
  notify: update grub

- name: Set timeout style (hidden for fast boot)
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_TIMEOUT_STYLE='
    line: 'GRUB_TIMEOUT_STYLE=hidden'
  notify: update grub

Default Boot Entry

- name: Set default kernel
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_DEFAULT='
    line: 'GRUB_DEFAULT=0'
  notify: update grub

# Boot previous selection
- name: Save last booted entry
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_DEFAULT='
    line: 'GRUB_DEFAULT=saved'
  notify: update grub

- name: Enable savedefault
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_SAVEDEFAULT='
    line: 'GRUB_SAVEDEFAULT=true'
  notify: update grub

Security Parameters

- name: Configure security-focused kernel parameters
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_CMDLINE_LINUX='
    line: 'GRUB_CMDLINE_LINUX="audit=1 apparmor=1 security=apparmor page_poison=1 slab_nomerge vsyscall=none"'
  notify: update grub

GRUB Password Protection

- name: Generate GRUB password hash
  ansible.builtin.command: >
    grub-mkpasswd-pbkdf2 --iteration-count 10000
  args:
    stdin: "{{ vault_grub_password }}\n{{ vault_grub_password }}"
  register: grub_password_hash
  changed_when: false
  no_log: true

- name: Deploy GRUB password config
  ansible.builtin.copy:
    dest: /etc/grub.d/01_password
    content: |
      #!/bin/sh
      cat <<EOF
      set superusers="admin"
      password_pbkdf2 admin {{ grub_password_hash.stdout | regex_search('grub\.pbkdf2\.sha512\..*') }}
      EOF
    mode: '0755'
  notify: update grub

Disable Console Output (Headless Servers)

- name: Configure serial console for headless servers
  ansible.builtin.blockinfile:
    path: /etc/default/grub
    block: |
      GRUB_TERMINAL="serial console"
      GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
  notify: update grub

- name: Add console to kernel cmdline
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_CMDLINE_LINUX='
    line: 'GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8"'
  notify: update grub

Multi-Kernel Management

- name: List installed kernels
  ansible.builtin.shell: >
    grep -oP 'menuentry .+?(?=\x27)' /boot/grub/grub.cfg || true
  register: kernel_list
  changed_when: false

- name: Display available kernels
  ansible.builtin.debug:
    var: kernel_list.stdout_lines

- name: Pin specific kernel version
  ansible.builtin.lineinfile:
    path: /etc/default/grub
    regexp: '^GRUB_DEFAULT='
    line: 'GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 6.8.0-45-generic"'
  notify: update grub

Complete GRUB Configuration Role

# roles/grub/defaults/main.yml
grub_timeout: 5
grub_timeout_style: hidden
grub_default: 0
grub_cmdline_linux_default:
  - quiet
  - splash
grub_cmdline_linux: []
grub_disable_recovery: true
grub_terminal: console
# roles/grub/tasks/main.yml
- name: Configure GRUB settings
  ansible.builtin.template:
    src: grub.j2
    dest: /etc/default/grub
    mode: '0644'
    backup: true
  notify: update grub

- name: Update GRUB
  ansible.builtin.meta: flush_handlers
# roles/grub/templates/grub.j2
# Managed by Ansible — do not edit manually
GRUB_DEFAULT={{ grub_default }}
GRUB_TIMEOUT={{ grub_timeout }}
GRUB_TIMEOUT_STYLE={{ grub_timeout_style }}
GRUB_DISTRIBUTOR=`lsb_release -i -s 2>/dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="{{ grub_cmdline_linux_default | join(' ') }}"
GRUB_CMDLINE_LINUX="{{ grub_cmdline_linux | join(' ') }}"
GRUB_DISABLE_RECOVERY={{ grub_disable_recovery | lower }}
GRUB_TERMINAL={{ grub_terminal }}

Troubleshooting

System Won't Boot After Change

GRUB changes can make systems unbootable. Always:

- name: Backup current grub.cfg
  ansible.builtin.copy:
    src: /boot/grub/grub.cfg
    dest: /boot/grub/grub.cfg.bak
    remote_src: true
    mode: '0600'

Verify Parameters Applied

- name: Check current kernel cmdline
  ansible.builtin.command: cat /proc/cmdline
  register: current_cmdline
  changed_when: false

- name: Show active parameters
  ansible.builtin.debug:
    var: current_cmdline.stdout

Conclusion

GRUB configuration with Ansible follows a simple pattern: edit /etc/default/grub with lineinfile, replace, or template, then regenerate grub.cfg with a handler. Always backup before changes — GRUB errors can make systems unbootable. Use lineinfile for individual parameter tweaks and template for full configuration management across your fleet.