Bootstrapping a fresh Linux install with Ansible


Ansible is an IT tool that enables Infrastructure as Code, letting you automate provisioning, configuration, management and deployment of services and applications. I like using it at a fraction of it's full power to bootstrap fresh installs of Linux for my homelab.

Sections

  1. Install Ansible
  2. The playbook
  3. The inventory and config files
  4. Running the playbook
  5. References
Information

This is not a comprehensive tutorial for Ansible, but simple a terse quick guide of my personal use case for Ansible in my home lab. For a deeper dive, I strongly suggest Learn Linux TV series of Ansible tutorials which is how I first learned to use it myself.

Install Ansible

First install Ansible, which requires adding the repository to download the package via APT.

sudo apt-add-repository ppa:ansible/ansible
sudo apt install ansible -y

The playbook

Contents of the playbook, which I named bootstrap.yml:

- name: Bootstrap Linux Server
  hosts: hostname-or-IP-address
  become: true
  vars:
    user: admin

  tasks:
    - name: Update all installed packages
      apt:
        update_cache: yes
        name: "*"
        state: latest

    - name: Install various apt packages
      apt:
        update_cache: yes
        name:
          - zsh
          - git
          - sudo
          - vim
          - htop
          - inxi
          - net-tools
          - speedtest-cli
          - samba
          - cifs-utils
          - docker
          - docker-compose
          - smartmontools
          - neofetch
        state: present
        install_recommends: yes
      when: ansible_distribution == 'Debian' or ansible_distribution == 'Kali' or ansible_distribution == 'Linuxmint' or ansible_distribution == 'Ubuntu'

    - name: Clean cache & remove unnecessary dependencies
      apt:
        autoclean: yes
        autoremove: yes
      when: ansible_distribution == 'Debian' or ansible_distribution == 'Kali' or ansible_distribution == 'Linuxmint' or ansible_distribution == 'Ubuntu'

    # Samba
    - name: Copy the Samba config file
      copy:
        src: files/smb
        dest: /etc/samba/smb.conf

    - name: Start the Samba daemon
      service:
        name: smbd
        state: started

    - name: Start the NetBIOs daemon
      service:
        name: nmbd
        state: started

    - name: Enable the Samba daemon
      ansible.builtin.systemd:
        name: smbd
        enabled: yes

    - name: Enable the NetBIOS daemon
      ansible.builtin.systemd:
        name: nmbd
        enabled: yes

    - name: Copy the zsh config file
      copy:
        src: files/zshrc
        dest: /home/user/zshrc # rename file to .zshrc after setting up oh-my-zsh
        owner: user
        group: group
        mode: 0644

    - name: Copy the aliases file
      copy:
        src: files/aliases
        dest: /home/user/.aliases
        owner: user
        group: group
        mode: 0644

    - name: Copy the Git config file
      copy:
        src: files/gitconfig
        dest: /home/user/.gitconfig
        owner: user
        group: group
        mode: 0644

    - name: Copy the Nano config file
      copy:
        src: files/nanorc
        dest: /etc/nanorc
        owner: user
        group: group
        mode: 0644

    # Final steps
    - name: Set the default shell
      user:
        name: "user"
        shell: "zsh"

    - name: Check if reboot is required
      stat:
        path: /var/run/reboot-required
      register: reboot_required_file

    - name: Reboot if required
      reboot:
        msg: Rebooting due to a kernel update
      when: reboot_required_file.stat.exists

This playbook will installs a selection of packages I commonly use, changes the default shell, and copies over dotfiles and other configs.

The inventory and config files

Next we need the inventory file, which I named hosts (no file extension) and I’m using the INI file format (you can also use YAML if you prefer):

[server]
192.168.1.200
192.168.1.210

[test]
192.168.1.220

[all:vars]
ansible_user=username
ansible_sudo_pass=password

This inventory is very simple, it gives Ansible two groups of hosts as targets [server] and [test], and provides a superuser and their sudo password as variables for all targets. This works for me because I use the same username and password for all the linux machines on my network, but you can make different sets of variables, for example:

[targets]
192.168.1.200   ansible_user=user1   ansible_password=password2
192.168.1.210   ansible_user=user2   ansible_password=password1
192.168.1.220   ansible_user=user3   ansible_password=password3

Next is the configuration file, ansible.cfg:

[defaults]
inventory = hosts
private_key_file = ~/.ssh/id_ed25519
retry_files_enabled = False
ansible_python_interpreter=/usr/bin/python3
timeout=30

[ssh_connections]
pipelining = true

This tells Ansible the name of the inventory file (hosts), the location of the SSH private key to use, and pipelining = true vastly improves the speed at which Ansible executes tasks.

Running the playbook

All that’s left now is to run the playbook and bootstrap the server, but first it’s highly recommended to do a dry-run that runs the playbook in check mode, to verify it works without making any changes. Make sure you’re in the same directory as bootstrap.yml (the playbook), ansible.cfg (the config) and hosts (inventory file). Then, use the following command:

ansible-playbook bootstrap.yml --check

You’ll see some output as the playbook carries out its tasks. Below as an example:

PLAY [Bootstrap home server] *********************************************************************
TASK [Gathering Facts] ***************************************************************************
ok: [apollo]

TASK [Update all installed packages] *************************************************************
ok: [apollo]

TASK [Install various apt packages] **************************************************************
ok: [apollo]

TASK [Clean cache & remove unnecessary dependencies] *********************************************
ok: [apollo]

TASK [Start the Samba daemon] ********************************************************************
ok: [apollo]

TASK [Start the NetBIOs daemon] ******************************************************************
ok: [apollo]

TASK [Enable the Samba daemon] *******************************************************************
ok: [apollo]

TASK [Enable the NetBIOS daemon] *****************************************************************
ok: [apollo]

TASK [Copy the zsh config file] ******************************************************************
changed: [apollo]

TASK [Copy the aliases file] *********************************************************************
changed: [apollo]

TASK [Copy the Git config file] ******************************************************************
ok: [apollo]

TASK [Copy the Nano config file] *****************************************************************
ok: [apollo]

TASK [Set the default shell] *********************************************************************
changed: [apollo]


TASK [Check if reboot is required] ***************************************************************
ok: [apollo]

TASK [Reboot if required] ************************************************************************
skipping: [apollo]

PLAY RECAP ***************************************************************************************
apollo     : ok=15   changed=3    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

If all looks good and there’s no errors, you can run the playbook for real:

ansible-playbook bootstrap.yml

Using MergerFS to combine multiple hard drives into one unified media storage

Mounting (either internal or external) hard drives in Linux