Skip to content
IaC Templates

Configure a VM with Ansible

Template · Updated May 2026
Validated May 2026

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 first, or use the combined provision-and-configure template when you want both steps in one workflow.

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on shared vCPU.

Starting template$13.10/mo

Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.

What each resource is for

Vm

s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50/mo

Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.

Included in baseline

s1a.small

2 shared vCPU, 2 GiB RAM, 0.5 Gbps

$16.50

Compute + RAM rate basis

2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (20 GiB)

20 GiB at $0.08/GiB/mo

$1.60

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

Included at no charge

These line items are zero on Quake AI. Many other providers meter them separately.

Data transfer (inbound and outbound)

Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.

AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.

Learn more
$0.00

Private networking

Private networks, subnets, Neutron routers, and security groups are included with the plan.

VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.

$0.00

Control-plane API requests

OpenStack API calls for provisioning and management are included.

Some managed services on other clouds meter API calls or charge for premium control-plane features.

$0.00

Pricing data last validated: . For current rates, check quake.ai/pricing.

How to use this template#

Prerequisites#

  • Ansible 6+ installed: see Get started with Ansible on Quake AI.
  • 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#

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: [email protected]
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 [email protected] -- 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 or use Provision and configure with OpenTofu plus Ansible 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, parameters, and variables

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 23.05.2026

Quick answers

Was this page helpful?