Structuring Ansible playbooks for low volume, purpose-built images I hope this question is not too vague / opinion-based:

I need to create and maintain a handful of virtual machines that get packaged up and deployed out to thousands of users, depending on specific requirements. Everyone is doing roughly the same job, but different people need different tools. This is important because I don't think I can structure things around roles (though I'm happy to be told I'm wrong).

They way I have things structured now is:

vagrant-directory
|-Vagrantfile # Multi-host setup, with one machine per "use-case"
|-scripts/ # Shell scripts run by Vagrant (mostly used to install Ansible prereqs)
|-ansible/ # All Ansible-related files
|----Playbook1.yml # One playbook per use case
|----tasks/ # Contains task files for everything I want to do
|------install/ # All task files related to installing software
|------apt-update.yml # For example, a script to run apt update
|------iptables-flush.yml # Disable iptables


With this approach, it basically means that when I get a requirement for a new machine, I have to create a new Playbook42.yml, use import_tasks for the tasks that I need, create any machine-specific tasks, and then provision and export.

Is there a more "Ansible official" or industry standard approach to doing this?
Unless I totally missed your question, roles are exactly what you need in this case. I'll use examples from my real world. My code below is just to illustrate my point (so it is not fully runnable, might contain errors and is not bullet proof rocket science: you will probably have to adapt/overcome tools limitations). But at least you'll be able to tell me if I got you wrong.

Let's say your servicing a large community of developers on different techs: java, php, nodejs, scala, python, ruby... needing some tools as well like apache, nginx, postgres, mysql, redis...

You have a role for each of these techs/tools, and possibly use existing ones from galaxy that you don't even have to write and that you can download at provisioning time. These roles can possibly be controlled with vars to choose e.g. versions. (see vagrant provisionner config below)

Now, one way you can activate/deactivate roles in a play is with the use of tags

Your unique playbook could look something like this

- name: Vagrant provisionning playbook
  hosts: all

  pre_tasks:
    # whatever you need to do before roles

  roles:
    - role: php
      tags: [php]
    - role: java
      tags: [java]
    - role: nginx
      tags: [nginx,php] # if you always deploy php with nginx
    - role: redis
      tags: [redis]
    # more roles here

    tasks:
      # Whatever tasks are needed to be played after roles


With such a structure, you can now configure your ansible provisioinner in vagrant defining the tags and specifying the vars needed to provision with your single playbook.

Vagrant.configure(2) do |config|

  config.vm define "php_application" do |php_application|

    # Some config for vm
    php_application.vm.provision "ansible" do |ansible| 
      ansible.playbook = "playbook.yml"
      ansible.tags = "php,nginx,untagged"
    end

  end

  # Host vars declared globally. You might be able to do
  # it on host by host basis but the doc does not specify it
  config.vm.provision "ansible" do |ansible|
    ansible.host_vars = {
      "php_application" =&gt; {"php_version" =&gt; "7.2"}
    }
  end
end 


Does this fit your needs ?

Note: if a single playbook is too much of a hassle, you can still use the roles and assemble in each playbook the techs/tools you need and reuse things that you will need in several places.