Ansible at Home

Using Ansible to automate and manage home environments, including server setups and configurations.

Wayne Lau

  ·  6 min read

Introduction #

While some concepts in Engineering should not be brought back to home, I find that Ansible was one of the few tools that is actually useful in a home environment.

Why Ansible? #

Ansible is a powerful automation tool that can help manage and configure systems efficiently. Another key important aspect commonly missed out is documentation. In the past, I would usually SSH into my home server and make changes directly. If I remember to document it, I would save code snippets into a README.md or obsidian note. However, this approach is prone to human error and can lead to inconsistencies over time. Most forms of IaC (Infrastructure as Code) tools are self documenting, as the code itself serves as documentation.

Setup and Configuration #

Before diving into use cases, it’s important to set up Ansible properly for a home environment. The configuration is straightforward and makes running playbooks much more convenient.

ansible.cfg #

Create an ansible.cfg file in your project directory:

[defaults]
inventory = inventory.ini
become_ask_pass = True

The inventory setting tells Ansible where to find your hosts, while become_ask_pass will prompt for your sudo password when needed, which is useful for home setups where you might not want to store credentials. For my home setup, I do not allow passwordless sudo for security reasons.

inventory.ini #

For managing your local machine, create an inventory.ini file:

[local]
localhost ansible_connection=local

This tells Ansible to run commands on your local machine without SSH overhead. You can expand this file later to include other devices on your network.

With these files in place, you can run playbooks with a simple ansible-playbook playbook.yml command, and Ansible will automatically use your configuration.

Use Cases #

System configuration #

I am using a consumer intel CPU with a stock cooler for my homelab. As I don’t expect it to run heavy workloads, I don’t need it to run at full power. Apart from using CPU governors, I can also set the PL1 and PL2 power limits to reduce power consumption and heat generation. I chose not to use the intel-undervolt tool, instead I chose to use Ansible to manage the configuration. This way, if I ever need to re-install the OS or set up a new server, I can easily apply the same configuration without having to remember the exact commands or settings.

---
- name: Configure CPU power limits with turbo boost enabled
  hosts: localhost
  become: true
  vars:
    pl1_watts: 65 # Sustained power limit
    pl2_watts: 90 # Burst power limit
    pl_hard_lower_limit: 51 # Minimum allowed PL
    pl_hard_upper_limit: 149 # Maximum allowed PL
  tasks:
    - name: Validate PL1 power limit range
      ansible.builtin.fail:
        msg: "pl1_watts must be between {{ pl_hard_lower_limit }} and {{ pl_hard_upper_limit }} watts, got {{ pl1_watts }}"
      when: (pl1_watts | int) < (pl_hard_lower_limit | int) or (pl1_watts | int) > (pl_hard_upper_limit | int)

    - name: Validate PL2 power limit range
      ansible.builtin.fail:
        msg: "pl2_watts must be between {{ pl_hard_lower_limit }} and {{ pl_hard_upper_limit }} watts, got {{ pl2_watts }}"
      when: (pl2_watts | int) < (pl_hard_lower_limit | int) or (pl2_watts | int) > (pl_hard_upper_limit | int)

    - name: Validate PL2 is greater than or equal to PL1
      ansible.builtin.fail:
        msg: "pl2_watts ({{ pl2_watts }}) must be greater than or equal to pl1_watts ({{ pl1_watts }})"
      when: (pl2_watts | int) < (pl1_watts | int)

    - name: Check if intel_pstate directory exists
      ansible.builtin.stat:
        path: /sys/devices/system/cpu/intel_pstate
      register: intel_pstate_dir

    - name: Check if intel-rapl powercap exists
      ansible.builtin.stat:
        path: /sys/class/powercap/intel-rapl/intel-rapl:0
      register: intel_rapl_dir

    - name: Check current turbo boost status
      ansible.builtin.slurp:
        src: /sys/devices/system/cpu/intel_pstate/no_turbo
      register: turbo_status
      when: intel_pstate_dir.stat.exists

    - name: Enable CPU turbo boost (intel_pstate)
      ansible.builtin.shell: echo "0" > /sys/devices/system/cpu/intel_pstate/no_turbo
      when:
        - intel_pstate_dir.stat.exists
        - (turbo_status.content | b64decode | trim) != "0"
      changed_when: false

    - name: Set PL1 (sustained power limit)
      ansible.builtin.shell: |
        echo "{{ pl1_watts * 1000000 }}" > /sys/class/powercap/intel-rapl/intel-rapl:0/constraint_0_power_limit_uw
      when: intel_rapl_dir.stat.exists
      changed_when: false

    - name: Set PL2 (burst power limit)
      ansible.builtin.shell: |
        echo "{{ pl2_watts * 1000000 }}" > /sys/class/powercap/intel-rapl/intel-rapl:0/constraint_1_power_limit_uw
      when: intel_rapl_dir.stat.exists
      changed_when: false

    # For persistence across reboots
    - name: Create systemd service for CPU power management
      ansible.builtin.copy:
        dest: /etc/systemd/system/cpu-power-limits.service
        mode: "0644"
        owner: root
        group: root
        content: |
          [Unit]
          Description=Set CPU Power Limits and Enable Turbo Boost
          After=multi-user.target

          [Service]
          Type=oneshot
          ExecStart=/bin/bash -c 'echo "0" > /sys/devices/system/cpu/intel_pstate/no_turbo'
          ExecStart=/bin/bash -c 'echo "{{ pl1_watts * 1000000 }}" > /sys/class/powercap/intel-rapl/intel-rapl:0/constraint_0_power_limit_uw'
          ExecStart=/bin/bash -c 'echo "{{ pl2_watts * 1000000 }}" > /sys/class/powercap/intel-rapl/intel-rapl:0/constraint_1_power_limit_uw'

          [Install]
          WantedBy=multi-user.target
      when: intel_pstate_dir.stat.exists and intel_rapl_dir.stat.exists
      notify:
        - Enable and start service

  handlers:
    - name: Enable and start service
      ansible.builtin.systemd:
        name: cpu-power-limits
        enabled: true
        state: started
        daemon_reload: true
      when: intel_pstate_dir.stat.exists and intel_rapl_dir.stat.exists

Its very easy to make a mistake when setting the power limits. Adding one less zero is not too bad, but adding an extra zero is disastrous. With Ansible, I can validate the input values before applying them, reducing the risk of human error. It’s also much easier to say 65W and 90W, rather than 65000000 and 90000000. I can also set validations to ensure that the values are within acceptable ranges.

Restic setup #

Restic is a great backup tool that can be used to back up data to various locations. The backup scripts are manually written, but the cron jobs and log rotation are managed by Ansible. If the below process were to be done manually, it would be prone to human error and inconsistencies, as multiple files are involved, such as cron jobs, log rotation configuration, and the backup scripts themselves.

---

- name: Setup restic
  hosts: localhost
  tasks:
    - name: Ensure restic exists
      ansible.builtin.apt:
        name: restic
        state: present
        cache_valid_time: 3600
      become: true

    - name: Ensure restic backup script exists
      ansible.builtin.stat:
        path: "{{ restic_scripts_dir }}"
      register: restic_scripts_dir_stat

    - name: Check if the folder exists
      ansible.builtin.fail:
        msg: "The folder {{ restic_scripts_dir }} does not exist. Please create it first."
      when: not restic_scripts_dir_stat.stat.exists

    - name: Ensure the restic backup script is executable
      ansible.builtin.file:
        path: "{{ restic_scripts_dir }}/{{ item }}"
        mode: '0755'
      loop:
        - backup_immich.sh
        - backup_documents.sh

    - name: Create cron job for restic backup immich
      ansible.builtin.cron:
        name: "Restic Backup Immich"
        minute: "0"
        hour: "2"
        job: "{{ restic_scripts_dir }}/backup_immich.sh >> {{ restic_scripts_dir }}/logs/backup_immich.log 2>&1"
        state: present

    - name: Create cron job for restic backup documents
      ansible.builtin.cron:
        name: "Restic Backup Documents"
        minute: "0"
        hour: "3"
        job: "{{ restic_scripts_dir }}/backup_documents.sh >> {{ restic_scripts_dir }}/logs/backup_documents.log 2>&1"
        state: present

    - name: Setup log rotate for the logs
      ansible.builtin.copy:
        dest: /etc/logrotate.d/restic-backup
        content: |
          {{ restic_scripts_dir }}/logs/*.log {
              daily
              rotate 21
              compress
              delaycompress
              missingok
              notifempty
              create 644 {{ ansible_user }} {{ ansible_user }}
          }
        owner: root
        group: root
        mode: '0644'
        validate: "logrotate -d %s"
      become: true
      

Conclusion #

As you add more services and configurations to your home environment, the benefits of using Ansible become even more apparent. It helps maintain consistency, reduces the risk of human error, and serves as documentation for your setup. Whether you’re managing a single server or multiple devices, Ansible can streamline your home automation tasks effectively.