The lesson learned from using ansible_user variable
Tags: ansible, vagrant, docker
Published: 2022-08-16 17:53:23

---
title: The lesson learned from using ansible_user variable
published: true
description: 
tags: ansible, vagrant, docker
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
---

**TL;DR**

Use `ansible_user` **ONLY** in `inventory` / `group_vars` / `host_vars` files

---

**Problem: `ansible_user` in my playbooks**

I wanted Ansible to setup Docker with the [geerlingguy/ansible-role-docker](https://github.com/geerlingguy/ansible-role-docker):

`playbook.yml`👇

```yaml
...

roles:
  - role: geerlingguy.docker
    docker_users: ["{{ ansible_user }}"]

...
```

Ran `vagrant provision`: no errors 🎉

Connected to a VM with `vagrant ssh`:
- `docker ps`: no *permission denied* errors 🎉
- `docker run hello-world`: no errors 🎉 

`git commit`, `git push`, done... ✅

---

I have wanted to try out [dev containers](https://code.visualstudio.com/docs/remote/containers) for a long time, and my repository with Ansible stuff was a great choice.

While dev containers have a lot of benefits, there are also some limitations. The one I faced was broken Vagrant integration.

*Before migration to dev containers:* Ansible and Vagrant live on a host and integrate perfectly.

*After the migration:* Ansible lives in a dev container and Vagrant lives on the host. If you run `vagrant provision`, you will see the error about missing `ansible-playbook` executable on your PATH.

While installing Vagrant in a Docker container is easy, it's not with VirtualBox. I found some threads on StackOverflow about getting VirtualBox up and running, *but*...

- It requires sticking to an image with `systemd`. On Docker Hub you can find some `systemd-*` images provided by *jrei*, *but*...

  - I have no idea who *jrei* is and how long they will maintain the images (e. g. rebasing onto the latest  Debian/Ubuntu/etc. to grab all the security patches). I wouldn't like it resting on my shoulders one day.

  - Just using one of these images in `.devcontainer/devcontainer.json` is not enough; I would also need to create a `Dockerfile` and copy everything from the [Microsoft's Python 3 one](https://github.com/microsoft/vscode-dev-containers/tree/main/containers/python-3) to make it suitable for development. We need a dev container, remember? This way it starts resting on my shoulders immediately: periodically I need to check the changes commited to the Microsoft's Python 3 `Dockerfile` and apply them to my `Dockerfile`.

- The fact that there is almost no info on the web about the setup like this says it's uncommon practice, and if I have any problems, there is nobody to help me.

OK, but except the [Ansible provisioner](https://www.vagrantup.com/docs/provisioning/ansible) Vagrant  has the [Ansible Local one](https://www.vagrantup.com/docs/provisioning/ansible_local)!

In a `Vagrantfile` I changed one line:

```diff
-  config.vm.provision 'ansible' do |ansible|
+  config.vm.provision 'ansible_local' do |ansible|
```

... and thought it was a quick win 🏆.

I ran `vagrant provision` and got saddened by the error:

```
... {{ ansible_user }}: 'ansible_user' is undefined ...
```
Hmm, where is `ansible_user`? 🤔

Let's take a step back and look at how Ansible ~~Local~~ provisioning works under the hood. Vagrant generates an inventory and passes it to `ansible-playbook`:

```
<your-project>/.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory
```

```
# Generated by Vagrant

default ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222 ansible_ssh_user='vagrant' ansible_ssh_private_key_file='<your-project>/.vagrant/machines/default/virtualbox/private_key'
                                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^
```

The `ansible_ssh_user` you can see here is the `ansible_user` at the time of Ansible <2. You can find the deprecation notice [here](https://docs.ansible.com/ansible/2.3/intro_inventory.html).

I wish this deprecation didn't happen. So now I would not write these lines 😂

Let's get back to the Ansible Local provisioning. Ansible doesn't connect to any machine with SSH ➡️ there is **no need** for an inventory ➡️ there is **no** `ansible_ssh_user`/`ansible_user` 🙃

OK, googling for *undefined ansible_user* gave me the solution:

```diff
...

roles:
  - role: geerlingguy.docker
-   docker_users: ["{{ ansible_user }}"]
+   docker_users: ["{{ ansible_env.USER }}"]

...
```

that makes my `vagrant provision` successful again 🎉

`git commit`, `git push`, done... ✅

---

It's time for production. I run `vagrant provision` to provision my Raspberry Pi 4 (where my pet projects are hosted) and see how `docker_users` variable evaluates to `[root]` instead of `[pi]` 🤦‍♂️

The easy solution that can be found in some open-sourced Ansible roles:

```diff
...

roles:
  - role: geerlingguy.docker
-   docker_users: ["{{ ansible_env.USER }}"]
+   docker_users: ["{{ ansible_user | default(ansible_env.USER) }}"]

...
```

---

**The root cause**

I specified `ansible_user` only in the inventory and used `{{ ansible_user }}` to refer to it in my playbooks, in my roles, in variables to 3rd party roles (like in the one described above)... everywhere.

Let's say the user is `ubuntu`. Use cases:
1. Ansible used `ubuntu` to SSH for provisioning.
2. Ansible was configuring `ubuntu` user according to my instructions, e.g. changing its shell preference from `bash` to `zsh` and configuring `.zshrc` so when you SSH, you get connected to the running `tmux` session or a new one created for you 👍 Did you get it? Ansible is configuring the user it's using to SSH... What if it gets broken as a result of provisioning? 🙃
3. I used `ubuntu` user to SSH...

At that time I thought: wow, how flexible it is 💪
Now I think: how stupid and fragile it is 🤦‍♂️😂

*The root cause is that I forgot about the single-responsibility principle, overloaded the user and thought it was cool. `ansible_user` is just the possible implementation.* The way I will re-write this Ansible setup and will use for all my future ones:
- `vagrant`/`ubuntu`/`pi`/whatever is used only by Ansible for SSHing, installing packages, blacklisting ports, etc., **but** it stays pristine because it creates and configures 👇
- `dev` user to have `zsh` by default, `tmux` hook described above, etc.

`vars/main.yml` 👇

```yaml
---
dev_user: dev
```

I am using the variable for the case I find a better name one day... before the very first provisioning on a new project of course... changing this in-between would be too risky 💣 Also it simplifies searching across the project.

`playbook.yml` 👇

```diff
...

roles:
  - role: geerlingguy.docker
-   docker_users: ["{{ ansible_user | default(ansible_env.USER) }}"]
+   docker_users: ["{{ dev_user }}"]

...
```

This way you are in a safe place. No matter you are running Ansible Remote or Ansible Local provisioner. Also migration from one OS to another is an easy task:

`inventory`👇

```diff
-some-host ansible_user=ubuntu
+some-host ansible_user=ec2-user
```

... with everything else being familiar: the same `ssh dev@some-host` command, the same `dev` user, etc.

The less complex interpolations, conditions, loops you have - the better. These guys must be tested. Is it easy to test Ansible code? Would you like to test your Ansible code? I guess, no 🙂