# Ansible Roles and "The Wizard" Ansible's role features are really helpful for creating reusable chunks of configuration. I wanted to create a fun context for exploring roles, so I built this little project that debugs Black Sabbath's "The Wizard". ## Try it out ### Clone the repository `git clone https://github.com/ashemath/ansible-thewizard` ### Install ansible You can install ansible in a Python venv with the provided script: `./setup_ansible` Activate your the installed venv by running `source bin/activate` This will add the venv's packages to your PATH variable until you run `deactivate`. Alternatively, you could use system-level ansible. This project uses only core ansible functionality. ### Run the playbook `ansible-playbook apply_roles.yml` ## The design ### "The Wizard" The song has three verses and a chorus that repeats after each verse. The chorus changes on the 2nd iteration, and stays that way for the 3rd. We'll be using task, variable, and meta main.yml files. Here's the `tree` of the folder structure: ``` roles/ ├── role_a │   ├── tasks │   │   ├── chorus.yml │   │   └── main.yml │   └── vars │   └── main.yml ├── role_b │   ├── meta │   │   └── main.yml │   ├── tasks │   │   └── main.yml │   └── vars │   └── main.yml └── role_c ├── meta │   └── main.yml ├── tasks │   └── main.yml └── vars └── main.yml ``` ### The playbook file We're going to run all three roles by calling for only the last role in the dependency chain. `apply_roles.yml` ```yaml --- - name: apply roles hosts: localhost roles: - role_c ``` ### The dependency chain When we call role_c, it is dependent on role_b, and role_b is dependent on role_a `$ cat roles/role_b/meta/main.yml` ```yaml dependencies: - role: role_a ``` .. `$ cat roles/role_c/meta/main.yml` .. ```yaml dependencies: - role: role_b ``` ### The task files The task file for role_a does most of the heavy lifting. It executes debug message tasks that "sing" the lyrics. I include a idempotency exercise that pulls on the `role_name` special variable to craft a file in a generated `proof/` folder. `roles/role_a/tasks/main.yml` ```yaml - name: What role are we executing? debug: msg: "We are running this task for {{ role_name }}" - name: Print verse specified for this role. debug: msg: "{{ item.msg }}" loop: "{{ verse }}" - name: include the chorus import_tasks: file: "{{ playbook_dir }}/roles/role_a/tasks/chorus.yml" - name: Ensure the proof folder exists file: path: "{{ playbook_dir }}/proof/" state: directory run_once: true - name: idempotency/proof task lineinfile: path: "{{ playbook_dir }}/proof/{{ role_name }}" line: "{{ role_name }} was here" create: yes - name: import role_a's tasks import_tasks: file: "{{ playbook_dir }}/roles/role_a/tasks/main.yml" - name: import role_a's tasks import_tasks: file: "{{ playbook_dir }}/roles/role_a/tasks/main.yml" ``` The task file for role_b just imports the tasks from role_a, and we do the same thing with role_c: `roles/role_b/tasks/main.yml` ```yaml - name: import role_a's tasks import_tasks: file: "{{ playbook_dir }}/roles/role_a/tasks/main.yml" ``` Finally, we have a chorus.yml file that defines how we produce the chorus and post-chorus. This file lives under role_a: `roles/role_a/tasks/chorus.yml` ```yaml - name: chorus debug: msg: "{{ item }}" loop: "{{ chorus }}" - name: post_chorus debug: msg: "{{ post_chorus }}" ``` ### The vars files Each role contains unique lyrics from "The Wizard." The roles divide the song into a dictionary `verse`, list `chorus`, and string `post-chorus`. `roles/role_a/vars/main.yml` ```yaml verse: - 'msg': "Misty morning, clouds in the sky" - 'msg': "Without warning, the wizard walks by" - 'msg': "Casting his shadow, weaving his spell" - 'msg': "Long grey cloak, tinkling bell" chorus: - "Never Talking" - "Justkeeps walking" post_chorus: "Cursing his magic" ``` `roles/role_b/vars/main.yml` ```yaml verse: - 'msg': "Evil power disappears" - 'msg': "Demons worry when the wizard is near" - 'msg': "He turns tears into joy" - 'msg': "Everyone's happy when the wizard walks by" post_chorus: "Spreading his magic" ``` `roles/role_c/vars/main.yml` ```yaml verse: - 'msg': "Sun is shining, clouds have gone by" - 'msg': "All the people give a happy sigh" - 'msg': "He has passed by, giving his sign" - 'msg': "Left all the people feeling so fine" ``` ### The results When we run the playbooks the first time: `results.txt` ``` PLAY [apply roles] ************************************************************* TASK [Gathering Facts] ********************************************************* ok: [localhost] TASK [role_a : What role are we executing?] ************************************ ok: [localhost] => { "msg": "We are running this task for role_a" } TASK [role_a : Print verse specified for this role.] *************************** ok: [localhost] => (item={'msg': 'Misty morning, clouds in the sky'}) => { "msg": "Misty morning, clouds in the sky" } ok: [localhost] => (item={'msg': 'Without warning, the wizard walks by'}) => { "msg": "Without warning, the wizard walks by" } ok: [localhost] => (item={'msg': 'Casting his shadow, weaving his spell'}) => { "msg": "Casting his shadow, weaving his spell" } ok: [localhost] => (item={'msg': 'Long grey cloak, tinkling bell'}) => { "msg": "Long grey cloak, tinkling bell" } TASK [role_a : chorus] ********************************************************* ok: [localhost] => (item=Never Talking) => { "msg": "Never Talking" } ok: [localhost] => (item=Just keeps walking) => { "msg": "Just keeps walking" } TASK [role_a : post_chorus] **************************************************** ok: [localhost] => { "msg": "Cursing his magic" } TASK [role_a : Ensure the proof folder exists] ********************************* changed: [localhost] TASK [role_a : idempotency/proof task] ***************************************** changed: [localhost] TASK [role_b : What role are we executing?] ************************************ ok: [localhost] => { "msg": "We are running this task for role_b" } TASK [role_b : Print verse specified for this role.] *************************** ok: [localhost] => (item={'msg': 'Evil power disappears'}) => { "msg": "Evil power disappears" } ok: [localhost] => (item={'msg': 'Demons worry when the wizard is near'}) => { "msg": "Demons worry when the wizard is near" } ok: [localhost] => (item={'msg': 'He turns tears into joy'}) => { "msg": "He turns tears into joy" } ok: [localhost] => (item={'msg': "Everyone's happy when the wizard walks by"}) => { "msg": "Everyone's happy when the wizard walks by" } TASK [role_b : chorus] ********************************************************* ok: [localhost] => (item=Never Talking) => { "msg": "Never Talking" } ok: [localhost] => (item=Just keeps walking) => { "msg": "Just keeps walking" } TASK [role_b : post_chorus] **************************************************** ok: [localhost] => { "msg": "Spreading his magic" } TASK [role_b : Ensure the proof folder exists] ********************************* ok: [localhost] TASK [role_b : idempotency/proof task] ***************************************** changed: [localhost] TASK [role_c : What role are we executing?] ************************************ ok: [localhost] => { "msg": "We are running this task for role_c" } TASK [role_c : Print verse specified for this role.] *************************** ok: [localhost] => (item={'msg': 'Sun is shining, clouds have gone by'}) => { "msg": "Sun is shining, clouds have gone by" } ok: [localhost] => (item={'msg': 'All the people give a happy sigh'}) => { "msg": "All the people give a happy sigh" } ok: [localhost] => (item={'msg': 'He has passed by, giving his sign'}) => { "msg": "He has passed by, giving his sign" } ok: [localhost] => (item={'msg': 'Left all the people feeling so fine'}) => { "msg": "Left all the people feeling so fine" } TASK [role_c : chorus] ********************************************************* ok: [localhost] => (item=Never Talking) => { "msg": "Never Talking" } ok: [localhost] => (item=Just keeps walking) => { "msg": "Just keeps walking" } TASK [role_c : post_chorus] **************************************************** ok: [localhost] => { "msg": "Spreading his magic" } TASK [role_c : Ensure the proof folder exists] ********************************* ok: [localhost] TASK [role_c : idempotency/proof task] ***************************************** changed: [localhost] PLAY RECAP ********************************************************************* localhost : ok=19 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ``` Let's grep the results for `"msg"`: `cat results.txt | grep \"msg\"` ```yaml "msg": "We are running this task for role_a" "msg": "Misty morning, clouds in the sky" "msg": "Without warning, the wizard walks by" "msg": "Casting his shadow, weaving his spell" "msg": "Long grey cloak, tinkling bell" "msg": "Never Talking" "msg": "Just keeps walking" "msg": "Cursing his magic" "msg": "We are running this task for role_b" "msg": "Evil power disappears" "msg": "Demons worry when the wizard is near" "msg": "He turns tears into joy" "msg": "Everyone's happy when the wizard walks by" "msg": "Never Talking" "msg": "Just keeps walking" "msg": "Spreading his magic" "msg": "We are running this task for role_c" "msg": "Sun is shining, clouds have gone by" "msg": "All the people give a happy sigh" "msg": "He has passed by, giving his sign" "msg": "Left all the people feeling so fine" "msg": "Never Talking" "msg": "Just keeps walking" "msg": "Spreading his magic" ``` this gives us just the debug messages. ## The Advantages of Roles Roles give us a mechanism for creating complex interactions between objects (tasks, variables, handlers, etc.) and artifacts (generated files, compiled software, processed data sets, etc.) We divide our work into reusable chunks, so we can adapt and extend configuration: as needed, or on-demand. By reusing configuration code when possible, we ensure that we can adapt our code efficiently because we can make sweeping changes that will stay consistent across groups of systems. ### Configuration management Configuration management is when you can push out configuration changes from a centralized platform to client systems. When configuration is modified, the configuration will be put back into compliance the next time it's state is check in on. Ansible's role features allow us to build things that work at a wider scale. Design flexible roles that install services that routinely go together. We assemble configuration that works together in modular roles. Using ansible, we can assess and correct whether the systems meet expactions for all our roles, and we can use variables and tags to specialize things further. ### Ideas for roles: We could develop a ton of roles, and have the ability to adapt to a broad range of computer use cases: - common: generic Debian configuration - xfce_desk: Configured xfce4 desktop environment - gnome_desk: Configured gnome desktop environment - nvidia: Configure nvidia GPU support. - python: Configure generic Python execution environment - pythondev: Configure python development environment - web: configure apache and/or nginx. - db_mysql: Configure MySQL database - db_postgre: Configure PostgreSQL - apps_vscode: Configure vscode - apps_eclipse: Configure eclipse - apps_math: Configure mathematics applications - apps_twod: Configure 2D design software - apps_threed: Configure 3D design software - libs_ai: Configure AI development libraries ## Tips for Building roles: ### Design generic tasks and flexible variable files Design standarized tasks that apply configuration as specified in a roles' variables. For example, build a loop that installs a list of services and restarts the system daemons for you if your configuration files change. ### --list-tasks The ansible-playbook option `--list-tasks` is indespesable for ensuring your roles are triggering the tasks that you want. It's also good for debugging tags! ### Honor the D.R.Y. principle Repeating a task all over makes updating your approach arduous. If each task is generic as possible and only defined once, you can quickly iterate and implement features. ### Idempotency is your friend. Idempotency is when your tasks are designed to only trigger when configuration changes. Tasks that lack idempotency will run every time, even if they have been previously applied. If you have a bunch of tasks that lack idempotency in a dependency role, you'll multiple the number of unnecessary tasks being executed every play. Roles are great, but layering configuration on top of configuration can lead to really slow plays if you are waiting on a ton of unnecesary tasks to return exit codes. Efficient roles will maximize task idempotency.