# Configure a VM with Ansible

Source: https://docs.quake.ai/resources/iac-templates/ansible-configure-vm
Markdown: https://docs.quake.ai/resources/iac-templates/ansible-configure-vm.md

---

# Configure a VM with Ansible

This pattern composes Compute and Network.


A ready-to-use Ansible playbook that takes a freshly provisioned Quake AI VM and configures it for production use: package baseline, non-root sudo user, SSH hardening, host firewall, and a placeholder web service. The playbook is idempotent; rerun it to converge a host that has drifted.

This template handles the Day 1 configure step. Provision the VM with [OpenTofu](/resources/iac-templates/simple-vm) first, or use the [combined provision-and-configure template](/resources/iac-templates/ansible-provision-and-configure) when you want both steps in one workflow.

<PricingCompanion templateSlug="ansible-configure-vm" />

## How to use this template

### Prerequisites

- Ansible 6+ installed: see [Get started with Ansible on Quake AI](/docs/automation/how-to/getting-started-ansible).
- The `openstack.cloud`, `ansible.posix`, and `community.general` collections installed (see Required collections below).
- A Quake AI VM running `Ubuntu-24.04` with a public floating IP and an SSH keypair you can use to log in as `ubuntu`.
- Your operator SSH public key on disk at `~/.ssh/id_ed25519.pub`.

### Required collections

The playbook uses `ansible.posix.authorized_key` for the operator key and `community.general.ufw` for the host firewall. Declare both alongside `openstack.cloud` in a `requirements.yml` at the project root:

```yaml
collections:
  - name: openstack.cloud
  - name: ansible.posix
    version: ">=1.5.4"
  - name: community.general
    version: ">=8.0.0"
```

Install them once before the first run:

```bash
ansible-galaxy collection install -r requirements.yml
```

### File layout

```text
configure-vm/
  inventory.ini
  playbooks/
    site.yml
  group_vars/
    all.yml
```

### Run command

```bash
cd configure-vm
ansible-playbook -i inventory.ini playbooks/site.yml
```

### What this template produces

- A non-root user `deploy` with sudo and your SSH public key
- SSH daemon hardened: root login disabled, password authentication disabled, `MaxAuthTries 3`
- Baseline packages installed: `curl`, `git`, `vim`, `fail2ban`, `ufw`, `nginx`
- `ufw` enabled with default-deny inbound, allow rules for SSH, HTTP, and HTTPS
- `nginx` serving a placeholder index page on port 80, verified with `ansible.builtin.uri`

### How to extend

- Replace the placeholder index page by templating your own `index.html` from `templates/index.html.j2`.
- Add application deployment tasks under a new `roles/app/` and import the role from `site.yml`.
- Move secrets such as the operator email or API tokens into Ansible Vault and reference them with `vars_files`.
- Swap `nginx` for `caddy` if you want automatic TLS; install `caddy` from its apt repo and replace the firewall rule for port 80 with port 443.

## Inventory

`inventory.ini` points Ansible at the host you provisioned. Replace `203.0.113.10` with your VM's floating IP.

```ini
[web]
web-01 ansible_host=203.0.113.10 ansible_user=ubuntu

[web:vars]
ansible_ssh_private_key_file=~/.ssh/id_ed25519
ansible_python_interpreter=/usr/bin/python3
```

## Group variables

`group_vars/all.yml` centralizes the values the playbook references. Edit `operator_email` and `operator_ssh_key` to match your operator account.

```yaml
operator_email: ops@example.com
operator_user: deploy
operator_ssh_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
baseline_packages:
  - curl
  - git
  - vim
  - fail2ban
  - ufw
  - nginx
firewall_allowed_ports:
  - 22
  - 80
  - 443
```

## Playbook

`playbooks/site.yml` is the entry point. Run it with `ansible-playbook -i inventory.ini playbooks/site.yml`.

```yaml
---
- name: Configure Quake AI VM
  hosts: web
  become: true
  gather_facts: true

  tasks:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600

    - name: Upgrade installed packages
      ansible.builtin.apt:
        upgrade: dist
        autoremove: true

    - name: Install baseline packages
      ansible.builtin.apt:
        name: "{{ baseline_packages }}"
        state: present

    - name: Create non-root sudo user
      ansible.builtin.user:
        name: "{{ operator_user }}"
        groups: sudo
        shell: /bin/bash
        create_home: true
        append: true

    - name: Authorize operator SSH key
      ansible.posix.authorized_key:
        user: "{{ operator_user }}"
        key: "{{ operator_ssh_key }}"
        state: present

    - name: Allow operator passwordless sudo
      ansible.builtin.copy:
        dest: "/etc/sudoers.d/90-{{ operator_user }}"
        content: "{{ operator_user }} ALL=(ALL) NOPASSWD:ALL\n"
        mode: "0440"
        owner: root
        group: root
        validate: visudo -cf %s

    - name: Ensure /run/sshd exists for sshd validate
      ansible.builtin.file:
        path: /run/sshd
        state: directory
        mode: "0755"

    - name: Harden sshd_config
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
        state: present
        validate: sshd -t -f %s
      loop:
        - { regexp: "^#?PermitRootLogin",       line: "PermitRootLogin no" }
        - { regexp: "^#?PasswordAuthentication", line: "PasswordAuthentication no" }
        - { regexp: "^#?MaxAuthTries",          line: "MaxAuthTries 3" }
      notify: Restart sshd

    - name: Set ufw default-deny inbound
      community.general.ufw:
        direction: incoming
        policy: deny

    - name: Allow firewall ports
      community.general.ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop: "{{ firewall_allowed_ports }}"

    - name: Enable ufw
      community.general.ufw:
        state: enabled

    - name: Render placeholder index page
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        mode: "0644"
        content: |
          <!doctype html>
          <html>
            <head><title>Configured by Ansible</title></head>
            <body>
              <h1>Hello from Quake AI</h1>
              <p>This host was configured by Ansible.</p>
            </body>
          </html>

    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: Verify nginx responds
      ansible.builtin.uri:
        url: http://127.0.0.1/
        status_code: 200
        return_content: false

  handlers:
    - name: Restart sshd
      ansible.builtin.service:
        name: ssh
        state: restarted
```

## Verify

After the playbook converges, confirm from your workstation:

```bash
curl http://203.0.113.10/
# expect: <h1>Hello from Quake AI</h1> ...

ssh -i ~/.ssh/id_ed25519 deploy@203.0.113.10 -- sudo systemctl status nginx
# expect: active (running)
```

A second run should report `ok=N changed=0` against every task: idempotency is the contract.

## When to use this pattern

Configure an existing VM with Ansible after OpenTofu provisioning. Provision the host with [Simple VM with Floating IP](/resources/iac-templates/simple-vm) or use [Provision and configure with OpenTofu plus Ansible](/resources/iac-templates/ansible-provision-and-configure) for both steps in one workflow.

## Customize this pattern

Provision and size the underlying VM with the OpenTofu customize how-tos on [Simple VM with Floating IP](/resources/iac-templates/simple-vm):

- [Customize a template's image and flavor](/docs/automation/how-to/customize-template-image-flavor)
- [Add a block volume to a template](/docs/automation/how-to/add-volume-to-template)
- [Parameterize a template with a tfvars file](/docs/automation/how-to/parameterize-template-tfvars)

<TemplateResourceMap template="ansible-configure-vm" format="ansible" />

## See also

- [Get started with Ansible on Quake AI](/docs/automation/how-to/getting-started-ansible): installation and first playbook
- [Provision and configure with OpenTofu plus Ansible](/resources/iac-templates/ansible-provision-and-configure): combined-tool workflow that runs this same configuration after provisioning
- [Ansible openstack.cloud module reference](/reference/automation/ansible-modules): catalog of provisioning modules used by the combined template
- [Ansible CI/CD on Quake AI](/docs/automation/how-to/ansible-cicd): run this playbook from GitHub Actions or GitLab CI
