|
| 1 | +--- |
| 2 | +author: dmac |
| 3 | +title: "Ansible roles to deploy Ubuntu servers" |
| 4 | +date: 2025-12-04 00:00:00 +100 |
| 5 | +categories: [tools] |
| 6 | +tags: [automation, ansible] |
| 7 | +image: |
| 8 | + path: "/assets/img/posts/2025-12-04/img-preview.webp" |
| 9 | +--- |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | +If you've ever had to manually configure servers one by one, you know how painful it is to maintain consistency across users, groups, SSH keys, Docker, ZSH, editors, and all the other tools you rely on. |
| 14 | + |
| 15 | +In this article, we'll use Ansible to automate the deployment of 6 Ubuntu servers with identical configurations and tooling. |
| 16 | + |
| 17 | + |
| 18 | +## Use cases |
| 19 | + |
| 20 | +This week I needed to build 10 Ubuntu VMs to deploy new services in my lab. |
| 21 | + |
| 22 | +My motivations to automate this deployment were: |
| 23 | +- Keep a consistent environment between all my servers that will match the tools I use on my local machine. |
| 24 | +- Save time by not having to manually configure all these VMs myself |
| 25 | +- Have my code in version control to add more features in the future with minimal risk. |
| 26 | +- Reduce the time it takes me to rebuild one of those VMs in the future. |
| 27 | + |
| 28 | +Beyond my specific scenario, this approach is valuable for: |
| 29 | + |
| 30 | +- **Homelab enthusiasts** who want to quickly rebuild or add new servers with preferred tooling already configured |
| 31 | +- **Network engineers** testing automation tools like containerlab, infrahub, netbox, or nautobot who need fresh environments regularly |
| 32 | +- **DevOps teams** managing development servers where everyone needs the same shell, editor, and CLI tools |
| 33 | +- **Certification prep** (CCNA, CCNP, CKA, etc.) where you need consistent lab environments you can tear down and rebuild quickly |
| 34 | +- **Training and workshops** where all participants need identical setups |
| 35 | + |
| 36 | + |
| 37 | +## Project overview |
| 38 | + |
| 39 | +All the code shown in this example lives in the [dmac-ansible](https://github.com/danielmacuare/dmac-ansible) repo, where you'll find detailed documentation and additional guidance. |
| 40 | + |
| 41 | +This repo currently provides 5 main roles: |
| 42 | + |
| 43 | +{: w="900" h="600" } |
| 44 | +_Figure 1 - Ansible Project Roles_ |
| 45 | + |
| 46 | +- **[ubuntu](https://github.com/danielmacuare/dmac-ansible/tree/main/roles/ubuntu)** - The base role that creates users, groups, SSH keys, and installs APT and Snap packages. |
| 47 | +- **[zerotier](https://github.com/danielmacuare/dmac-ansible/tree/main/roles/zerotier)** - Installs ZeroTier and joins your specified network. |
| 48 | +- **[zsh](https://github.com/danielmacuare/dmac-ansible/tree/main/roles/zsh)** - Installs ZSH with Oh-My-Zsh and the Powerlevel10k theme, plus useful plugins like fzf-tab and zsh-autosuggestions. |
| 49 | +- **[docker](https://github.com/geerlingguy/ansible-role-docker)** - Installs Docker CE, Docker Compose, and manages Docker users. Huge thanks to [Jeff Geerling](https://www.jeffgeerling.com/) for maintaining this excellent role. |
| 50 | +- **[neovim](https://github.com/danielmacuare/dmac-ansible/tree/main/roles/neovim)** - Installs Neovim with Lazy as the plugin manager and Mason for LSP management, along with additional plugins for a great editing experience. |
| 51 | + |
| 52 | +> Note: Only use the neovim role if you want to deploy a custom configuration. If you just need neovim installed, add it to the `apt_packages` list in the ubuntu role instead. |
| 53 | +{: .prompt-tip } |
| 54 | + |
| 55 | +## Getting started |
| 56 | + |
| 57 | +We'll use Ansible to configure the servers once they're created and reachable via SSH. |
| 58 | + |
| 59 | +We will divide this goal in 3 main tasks: |
| 60 | + |
| 61 | +1. Create 6 VMs to test our playbook. |
| 62 | +2. Clone the repository and update the inventory, vars, playbook, and other necessary files. |
| 63 | +3. Configure all 6 servers in a single Ansible run. |
| 64 | + |
| 65 | +### 1 - Creating the VMs |
| 66 | + |
| 67 | +For this step, I'm using Orbstack since I'm on MacOS. Check out the [Orbstack Installation Docs](https://docs.orbstack.dev/quick-start#installation) if you need to install it. |
| 68 | + |
| 69 | +Alternative options: |
| 70 | +- **Windows**: Use WSL2 or VirtualBox |
| 71 | +- **Linux**: KVM or LXC work great |
| 72 | + |
| 73 | +Once Orbstack is installed, create the 6 VMs: |
| 74 | + |
| 75 | +{: w="900" h="600" } |
| 76 | +_Figure 2 - 6 x Ubuntu VMs_ |
| 77 | + |
| 78 | +### 2 - Cloning the repo and updating files |
| 79 | + |
| 80 | +#### 2.1 - Clone and install dependencies |
| 81 | +```bash |
| 82 | +git clone https://github.com/danielmacuare/dmac-ansible.git |
| 83 | +cd dmac-ansible |
| 84 | +uv sync |
| 85 | +uv run ansible-galaxy install -r requirements.yml -p ./roles |
| 86 | +``` |
| 87 | + |
| 88 | + |
| 89 | +#### 2.2 - Customize your deployment |
| 90 | + |
| 91 | +> For a detailed step-by-step guide on updating all necessary files before running the playbook, check out the [Configuration Guide](https://github.com/danielmacuare/dmac-ansible/blob/main/docs/configuration.md#initial-setup) |
| 92 | +{: .prompt-tip } |
| 93 | + |
| 94 | +You'll need to modify these files: |
| 95 | + |
| 96 | +- **[vars.yaml](https://github.com/danielmacuare/dmac-ansible/blob/main/inventories/group_vars/all/vars.yaml)** - Stores all variables used by each role. This controls how your servers are configured. |
| 97 | +- **[vault.yaml](https://github.com/danielmacuare/dmac-ansible/blob/main/inventories/group_vars/all/example.vault.yaml)** - Stores the password for encrypting/decrypting sensitive Ansible files containing secrets. |
| 98 | +- **[inventory.ini](https://github.com/danielmacuare/dmac-ansible/blob/main/inventories/inventory.ini)** - Defines which targets are available in each Ansible group. |
| 99 | +- **[ubuntu.yml](https://github.com/danielmacuare/dmac-ansible/blob/main/playbooks/ubuntu.yml)** - Controls which roles get applied to target servers. Mix and match as needed. The ubuntu role (base) is recommended; all others are optional. |
| 100 | +- **[dmac.pub](https://github.com/danielmacuare/dmac-ansible/blob/main/roles/ubuntu/files/dmac.pub)** - Your SSH public key. The filename should match the username configured in `vars.yaml`. |
| 101 | + |
| 102 | +#### 2.2.1 - Example configuration |
| 103 | + |
| 104 | +Here's what my configuration looks like: |
| 105 | + |
| 106 | +```bash |
| 107 | +--- |
| 108 | +# UBUNTU ROLE |
| 109 | +auth_key_dir: "{{ vault_auth_dir_key }}" |
| 110 | + |
| 111 | +users: |
| 112 | + - username: dmac |
| 113 | + sudo_access: true |
| 114 | + ssh_access: true |
| 115 | + ssh_pub_key: "{{ lookup('file', 'dmac.pub') }}" |
| 116 | + ssh_pass: "{{ vault_dmac_ssh_pass }}" |
| 117 | + custom_alias_file: "aliases_dmac.j2" |
| 118 | + custom_functions_file: "functions_dmac.j2" |
| 119 | + - username: svmt |
| 120 | + sudo_access: true |
| 121 | + ssh_access: true |
| 122 | + ssh_pub_key: "{{ lookup('file', 'svmt.pub') }}" |
| 123 | + ssh_pass: "{{ vault_svmt_ssh_pass }}" |
| 124 | + custom_alias_file: "aliases_svmt.j2" |
| 125 | + custom_functions_file: "functions_svmt.j2" |
| 126 | + |
| 127 | +apt_packages: |
| 128 | + - whois |
| 129 | + - sshpass |
| 130 | + - nmap |
| 131 | + - bat |
| 132 | + - ripgrep |
| 133 | + - zoxide |
| 134 | + - jq |
| 135 | + - fzf |
| 136 | + - tldr |
| 137 | + - duf |
| 138 | + - btop |
| 139 | + - tree |
| 140 | + - tcpdump |
| 141 | + - openssh-server # ORB VMs won't install it by default |
| 142 | + |
| 143 | +snap_packages: |
| 144 | + - name: rustscan # NMAP Faster Alternative |
| 145 | + classic: false |
| 146 | + version: "latest/stable" |
| 147 | + - name: termshark # Wireshark-like TUI |
| 148 | + classic: false |
| 149 | + version: "latest/stable" |
| 150 | + |
| 151 | + |
| 152 | +# ZEROTIER ROLE |
| 153 | +zerotier_api_accesstoken: "{{ vault_zerotier_api_accesstoken }}" |
| 154 | +zerotier_api_url: "https://api.zerotier.com/api/v1" |
| 155 | +zerotier_network_id: "83048a0632608eee" |
| 156 | + |
| 157 | +# ZSH ROLE |
| 158 | +zsh_users: |
| 159 | + - username: dmac |
| 160 | + oh_my_zsh: |
| 161 | + theme: "powerlevel10k/powerlevel10k" |
| 162 | + plugins: |
| 163 | + - git |
| 164 | + update_mode: auto |
| 165 | + update_frequency: 5 |
| 166 | + write_zshrc: true |
| 167 | + |
| 168 | +zsh_p10k_users: |
| 169 | + - dmac |
| 170 | + |
| 171 | +zsh_plugins: |
| 172 | + - "zsh-autosuggestions" |
| 173 | + - "zsh-fast-syntax-highlighting" |
| 174 | + - "fzf-tab" |
| 175 | + - "zsh-completions" |
| 176 | + |
| 177 | +zsh_plugins_path: "{{ item.username }}/.oh-my-zsh/custom/plugins" |
| 178 | + |
| 179 | +# DOCKER ROLE |
| 180 | +docker_edition: 'ce' |
| 181 | +docker_packages: |
| 182 | + - "docker-{{ docker_edition }}" |
| 183 | + - "docker-{{ docker_edition }}-cli" |
| 184 | + - "docker-{{ docker_edition }}-rootless-extras" |
| 185 | + - "containerd.io" |
| 186 | + - docker-buildx-plugin |
| 187 | +docker_packages_state: present |
| 188 | +docker_obsolete_packages: |
| 189 | + - docker |
| 190 | + - docker.io |
| 191 | + - docker-engine |
| 192 | + - docker-doc |
| 193 | + - docker-compose |
| 194 | + - docker-compose-v2 |
| 195 | + - podman-docker |
| 196 | + - containerd |
| 197 | + - runc |
| 198 | + |
| 199 | +# Docker Compose Plugin options. |
| 200 | +docker_install_compose_plugin: true |
| 201 | +docker_compose_package: docker-compose-plugin |
| 202 | +docker_compose_package_state: present |
| 203 | + |
| 204 | +# Docker Compose options. |
| 205 | +docker_install_compose: false # Prevents legacy binary |
| 206 | +docker_compose_version: "v2.40.3" # Latest as of 11/2025 |
| 207 | +docker_compose_path: /usr/local/bin/docker-compose |
| 208 | + |
| 209 | +# A list of users who will be added to the docker group. |
| 210 | +docker_users: |
| 211 | + - dmac |
| 212 | + - svmt |
| 213 | + |
| 214 | + |
| 215 | +# NEOVIM ROLE |
| 216 | +neovim_set_default_editor: true |
| 217 | +neovim_deploy_config: true |
| 218 | +neovim_users: |
| 219 | + - username: dmac |
| 220 | + |
| 221 | +``` |
| 222 | +{: file="group_vars/all/vars.yaml" } |
| 223 | + |
| 224 | +```bash |
| 225 | +[all:vars] |
| 226 | +ansible_user=dmac |
| 227 | +ansible_python_interpreter=/usr/bin/python3 |
| 228 | + |
| 229 | +[ubuntu_hosts] |
| 230 | +ub01-2204 ansible_host=ub01-2204@orb ansible_user=ub01-2204 zerotier_hosted_on=orb-ub01-2204 |
| 231 | +ub02-2204 ansible_host=ub02-2204@orb ansible_user=ub02-2204 zerotier_hosted_on=orb-ub02-2204 |
| 232 | +ub03-2404 ansible_host=ub03-2404@orb ansible_user=ub03-2404 zerotier_hosted_on=orb-ub03-2404 |
| 233 | +ub04-2404 ansible_host=ub04-2404@orb ansible_user=ub04-2404 zerotier_hosted_on=orb-ub04-2404 |
| 234 | +ub05-2504 ansible_host=ub05-2504@orb ansible_user=ub05-2504 zerotier_hosted_on=orb-ub05-2504 |
| 235 | +ub06-2504 ansible_host=ub06-2504@orb ansible_user=ub06-2504 zerotier_hosted_on=orb-ub06-2504 |
| 236 | + |
| 237 | +[proxmox] |
| 238 | +max ansible_host=max-01 |
| 239 | +``` |
| 240 | +{: file="inventories/inventory.ini" } |
| 241 | + |
| 242 | +```bash |
| 243 | +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILLLdt13LmYyZmOn4bwbgVctSuejlc7iPAE46s9KlePs |
| 244 | + |
| 245 | +``` |
| 246 | +{: file="roles/ubuntu/files/dmac.pub" } |
| 247 | + |
| 248 | +```bash |
| 249 | +# Example File - FAKE CREDS |
| 250 | +vault_auth_dir_key: "/etc/ssh/authorized_keys" |
| 251 | +vault_zerotier_api_accesstoken: "Put your Zero Tier API Token HERE" |
| 252 | +# See docs/password-generation.md for instructions on how to generate the password hashes below |
| 253 | +vault_dmac_ssh_pass: "$6$Vqo2MXAAQt6z0KxG$2/XYsLfVbnLRhMveU9YV2lwzxnTRD8gk3jnjKWc894lApaFlJHhAo/m.FV/pqbD3EXV26Iia9otiiKBKTmXDCS" |
| 254 | +vault_svmt_ssh_pass: "$6$U4CbBz7gOuK11hjd$YOtOM6s5ftzqRDr408qsXDnyRBJ4a/lQLFufb1LJM1Q7R9ATcG/CvkybfJhRpaSDb5Z1GdEwsJIvZhyCnnADEX" |
| 255 | +``` |
| 256 | +{: file="group_vars/all/vault.yaml" } |
| 257 | + |
| 258 | +**- Playbook** |
| 259 | +```bash |
| 260 | +--- |
| 261 | +- name: Configure Ubuntu Development Servers |
| 262 | + hosts: ubuntu_hosts |
| 263 | + gather_facts: true |
| 264 | + roles: |
| 265 | + - { role: ubuntu, tags: ["ubuntu"] } |
| 266 | + - { role: zsh, tags: ["zsh"] } |
| 267 | + - { role: geerlingguy.docker, tags: ["docker"], become: true } |
| 268 | + - { role: zerotier, tags: ["zerotier"] } |
| 269 | + - { role: neovim, tags: ["neovim"] } |
| 270 | +``` |
| 271 | +{: file="playbooks/ubuntu.yaml" } |
| 272 | +
|
| 273 | +
|
| 274 | +### 3 - Running the Playbook |
| 275 | +
|
| 276 | +Once you've updated all the variables and files, it's time to execute the playbook: |
| 277 | +
|
| 278 | +```bash |
| 279 | +# Run all roles |
| 280 | +uv run ansible-playbook playbooks/ubuntu.yml -K |
| 281 | + |
| 282 | +# Or run specific roles only |
| 283 | +uv run ansible-playbook playbooks/ubuntu.yml -K --tags ubuntu |
| 284 | +uv run ansible-playbook playbooks/ubuntu.yml -K --tags zerotier |
| 285 | +uv run ansible-playbook playbooks/ubuntu.yml -K --tags zsh |
| 286 | +uv run ansible-playbook playbooks/ubuntu.yml -K --tags docker |
| 287 | +``` |
| 288 | +
|
| 289 | +> I had to run the playbook twice since it froze halfway through on the first attempt. This was likely due to my machine (hosting Orbstack and all 6 VMs) only having 16GB of RAM. |
| 290 | +{: .prompt-warning } |
| 291 | +
|
| 292 | +#### 3.1 - Results |
| 293 | +
|
| 294 | +After running the playbook, all 6 servers were successfully configured. Here's what the deployment looks like: |
| 295 | +
|
| 296 | +{: w="900" h="600" } |
| 297 | +_Figure 3 - Ansible playbook completed successfully on the second attempt_ |
| 298 | +
|
| 299 | +{: w="900" h="600" } |
| 300 | +_Figure 4 - Neovim installed and configured with all plugins_ |
| 301 | +
|
| 302 | +{: w="900" h="600" } |
| 303 | +_Figure 5 - ZSH installed with useful plugins like fzf-tab_ |
| 304 | +
|
| 305 | +{: w="900" h="600" } |
| 306 | +_Figure 6 - ZeroTier installed and connected_ |
| 307 | +
|
| 308 | +{: w="900" h="600" } |
| 309 | +_Figure 7 - Docker and Docker Compose installed_ |
| 310 | +
|
| 311 | +
|
| 312 | +## Closing thoughts |
| 313 | +
|
| 314 | +The modularity of these roles allows us to create different playbooks to target different sets of servers. For example: |
| 315 | +
|
| 316 | +1 - Ubuntu base playbook |
| 317 | + - Ubuntu role: To only apply the base ubuntu config (Users, groups, SSH Keys, etc) |
| 318 | +
|
| 319 | +2 - Ubuntu + Docker Playbook |
| 320 | +
|
| 321 | +3 - Ubuntu + Docker + Tools + Remote Access |
| 322 | + - Ubuntu role |
| 323 | + - Docker role |
| 324 | + - Tools |
| 325 | + - ZSH |
| 326 | + - Neovim |
| 327 | + - Zerotier role |
| 328 | +
|
| 329 | +
|
| 330 | +After completing this automation we can now: |
| 331 | +- Replace VMs effortlessly: If one VM dies or gets too annoying we can quickly create a new one and configure it with the playbook. |
| 332 | +- Get a consistent set of tools and the same experience across all our servers. |
| 333 | +- Save time when creating new servers. |
| 334 | +
|
| 335 | +If you're managing more than a couple of servers or frequently spinning up new VMs for testing, I highly recommend giving this approach a try. The initial setup takes some time, but once you have your vars and inventory configured, deploying new servers becomes almost effortless. |
| 336 | +
|
| 337 | +Feel free to fork the [dmac-ansible repo](https://github.com/danielmacuare/dmac-ansible) and adapt it to your needs. I'd love to hear if there are any improvements you'd suggest! |
0 commit comments