Install ZSH System-Wide with Ansible
Automated installation and configuration of ZSH with Oh-My-Zsh, Powerlevel10k theme, and useful plugins for multiple users across multiple servers.
Overview
This Ansible playbook installs and configures:
- ZSH shell: Modern shell with powerful features
- Oh-My-Zsh: Framework for managing ZSH configuration
- Powerlevel10k: Beautiful and fast ZSH theme
- Plugins: zsh-autosuggestions and zsh-syntax-highlighting
- System-wide configuration: Apply to multiple users simultaneously
Project Structure
ansible-zsh/
├── zsh-playbook.yml # Main Ansible playbook
├── hosts.ini # Inventory file with target hosts
└── files/
├── zshrc # ZSH configuration file
└── p10k.zsh # Powerlevel10k theme configuration
Prerequisites
On Control Node (Your Machine)
- Ansible installed (version 2.9+)
- SSH access to target servers
- SSH key-based authentication configured
Install Ansible:
# Ubuntu/Debian
sudo apt update
sudo apt install ansible
# RHEL/Rocky/AlmaLinux
sudo dnf install ansible
# macOS
brew install ansible
On Target Servers
- SSH server running
- Sudo privileges for the Ansible user
- Internet connection for downloading packages
Part 1: Inventory Configuration
Create hosts.ini to define your target servers:
[hosts]
192.168.1.10 ansible_user=admin
192.168.1.11 ansible_user=admin
192.168.1.12 ansible_user=admin
192.168.1.13 ansible_user=sysadmin
# Optional: Use localhost
# localhost ansible_connection=local
Inventory Explanation
192.168.1.10: Target server IP addressansible_user=admin: SSH user with sudo privileges[hosts]: Group name (referenced in playbook ashosts: all)
Multiple Groups
You can organize servers into groups:
[webservers]
192.168.1.10 ansible_user=admin
192.168.1.11 ansible_user=admin
[databases]
192.168.1.20 ansible_user=dbadmin
192.168.1.21 ansible_user=dbadmin
[development]
192.168.1.30 ansible_user=developer
Then target specific groups in your playbook:
hosts: webservers # Only web servers
# or
hosts: all # All servers
Part 2: The Playbook
Create zsh-playbook.yml:
---
- name: Install and configure zsh
hosts: all
become: yes
vars:
users:
- name: admin
home: /home/admin
- name: developer
home: /home/developer
- name: root
home: /root
tasks:
- name: Install zsh using package manager
package:
name: zsh
state: present
- name: Install util-linux-user on RHEL-based systems
dnf:
name: "util-linux-user,git,tar"
state: present
when: "'RedHat' in ansible_distribution or 'Rocky' in ansible_distribution or 'AlmaLinux' in ansible_distribution"
- name: Modify /etc/pam.d/chsh for unrestricted chsh
lineinfile:
path: /etc/pam.d/chsh
regexp: '^auth\s+required\s+pam_shells.so'
line: 'auth sufficient pam_shells.so'
state: present
backup: yes
- name: Change shell for users
command: chsh -s /usr/bin/zsh {{ item.name }}
loop: "{{ users }}"
- name: Check if .oh-my-zsh exists
stat:
path: ~/.oh-my-zsh
register: oh_my_zsh_stat
- name: Install .oh-my-zsh
become_user: "{{ item.name }}"
shell: "curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | /bin/sh"
loop: "{{ users }}"
when: not oh_my_zsh_stat.stat.exists
- name: Clone powerlevel10k theme for users
git:
repo: "https://github.com/romkatv/powerlevel10k.git"
dest: "{{ item.home }}/.oh-my-zsh/custom/themes/powerlevel10k"
depth: 1
loop: "{{ users }}"
- name: Clone zsh-autosuggestions plugin
git:
repo: "https://github.com/zsh-users/zsh-autosuggestions.git"
dest: "{{ item.home }}/.oh-my-zsh/custom/plugins/zsh-autosuggestions"
loop: "{{ users }}"
- name: Clone zsh-syntax-highlighting plugin
git:
repo: "https://github.com/zsh-users/zsh-syntax-highlighting.git"
dest: "{{ item.home }}/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
loop: "{{ users }}"
- name: Copy zshrc file
copy:
src: "files/zshrc"
dest: "{{ item.home }}/.zshrc"
loop: "{{ users }}"
- name: Copy p10k.zsh file
copy:
src: "files/p10k.zsh"
dest: "{{ item.home }}/.p10k.zsh"
loop: "{{ users }}"
Playbook Breakdown
Variables Section
vars:
users:
- name: admin
home: /home/admin
- name: developer
home: /home/developer
- name: root
home: /root
Define all users who should get ZSH. Add or remove users as needed.
Key Tasks
- Install ZSH: Uses generic
packagemodule (works on Debian, Ubuntu, RHEL, Rocky, etc.) - Install utilities: On RHEL-based systems, installs
util-linux-user(provideschsh), git, and tar - Modify PAM: Allows changing shell without restriction
- Change shell: Sets ZSH as default shell for each user
- Install Oh-My-Zsh: Downloads and installs the framework
- Clone theme/plugins: Installs Powerlevel10k theme and useful plugins
- Copy configs: Deploys your custom
.zshrcand.p10k.zshfiles
Part 3: Configuration Files
Create files Directory
mkdir -p files
files/zshrc
Create files/zshrc with your ZSH configuration:
# Enable Powerlevel10k instant prompt
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# Path to oh-my-zsh installation
export ZSH="$HOME/.oh-my-zsh"
# Set theme
ZSH_THEME="powerlevel10k/powerlevel10k"
# Plugins
plugins=(
git
docker
kubectl
zsh-autosuggestions
zsh-syntax-highlighting
)
source $ZSH/oh-my-zsh.sh
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
# Custom aliases
alias ll='ls -lah'
alias update='sudo dnf update -y'
alias reload='source ~/.zshrc'
# Environment variables
export EDITOR='vim'
export VISUAL='vim'
files/p10k.zsh
The p10k.zsh file contains Powerlevel10k theme configuration. You can:
Option 1: Generate it by running p10k configure on your machine, then copy the generated ~/.p10k.zsh to files/p10k.zsh
Option 2: Use a basic configuration (the file is ~4000 lines, so it's best to copy from an existing setup)
Option 3: Let users configure it themselves after deployment by running p10k configure
If you don't have a p10k.zsh file yet:
- Install ZSH and Powerlevel10k locally
- Run
p10k configureand follow the wizard - Copy the generated
~/.p10k.zshto yourfiles/directory
Part 4: Running the Playbook
Test Connection
First, verify Ansible can reach your servers:
ansible -i hosts.ini all -m ping
Expected output:
192.168.1.10 | SUCCESS => {
"changed": false,
"ping": "pong"
}
192.168.1.11 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Dry Run (Check Mode)
Test the playbook without making changes:
ansible-playbook -i hosts.ini zsh-playbook.yml --check
Execute Playbook
Run the playbook:
ansible-playbook -i hosts.ini zsh-playbook.yml
Execute with Verbose Output
For debugging:
ansible-playbook -i hosts.ini zsh-playbook.yml -v
# or more verbose
ansible-playbook -i hosts.ini zsh-playbook.yml -vvv
Target Specific Hosts
Run only on specific servers:
# Single host
ansible-playbook -i hosts.ini zsh-playbook.yml --limit 192.168.1.10
# Multiple hosts
ansible-playbook -i hosts.ini zsh-playbook.yml --limit 192.168.1.10,192.168.1.11
# Specific group
ansible-playbook -i hosts.ini zsh-playbook.yml --limit webservers
Part 5: Verification
SSH into Server
You should automatically be in ZSH with the Powerlevel10k prompt.
Verify Shell
echo $SHELL
# Output: /usr/bin/zsh
which zsh
# Output: /usr/bin/zsh
Check Oh-My-Zsh
ls -la ~/.oh-my-zsh
Check Plugins
ls ~/.oh-my-zsh/custom/plugins/
# Output: zsh-autosuggestions zsh-syntax-highlighting
Check Theme
ls ~/.oh-my-zsh/custom/themes/
# Output: powerlevel10k
Test Autosuggestions
Start typing a command you've used before, and you should see gray suggestions.
Test Syntax Highlighting
Commands should be colored (green for valid, red for invalid).
Part 6: Customization
Add More Users
Edit the vars section in the playbook:
vars:
users:
- name: admin
home: /home/admin
- name: developer
home: /home/developer
- name: johndoe
home: /home/johndoe
- name: janedoe
home: /home/janedoe
- name: root
home: /root
Add More Plugins
Edit files/zshrc and add plugins to the array:
plugins=(
git
docker
kubectl
docker-compose
npm
node
python
pip
ansible
terraform
aws
zsh-autosuggestions
zsh-syntax-highlighting
)
Available plugins: Oh-My-Zsh Plugins
Custom Aliases
Add to files/zshrc:
# System shortcuts
alias update='sudo dnf update -y'
alias install='sudo dnf install'
alias remove='sudo dnf remove'
# Navigation
alias ..='cd ..'
alias ...='cd ../..'
alias ....='cd ../../..'
# Git shortcuts
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gl='git log --oneline'
# Docker shortcuts
alias dps='docker ps'
alias di='docker images'
alias dex='docker exec -it'
alias dlogs='docker logs -f'
# Custom
alias c='clear'
alias h='history'
alias reload='source ~/.zshrc'
Configure for Specific Distributions
Add conditional tasks based on OS:
- name: Install additional packages on Ubuntu
apt:
name: "{{ item }}"
state: present
loop:
- fonts-powerline
- zsh-doc
when: ansible_distribution == "Ubuntu"
- name: Install additional packages on Rocky Linux
dnf:
name: "{{ item }}"
state: present
loop:
- powerline-fonts
- zsh-html
when: ansible_distribution == "Rocky"
Part 7: Advanced Configuration
Use Ansible Vault for Sensitive Data
If you need to store sensitive information:
# Create encrypted file
ansible-vault create secrets.yml
Add variables:
admin_password: "SecurePassword123"
Reference in playbook:
vars_files:
- secrets.yml
Run with vault password:
ansible-playbook -i hosts.ini zsh-playbook.yml --ask-vault-pass
Use SSH Key Authentication
Generate SSH key on control node:
ssh-keygen -t ed25519 -C "ansible@controlnode"
Copy to target servers:
ssh-copy-id [email protected]
ssh-copy-id [email protected]
ssh-copy-id [email protected]
Update hosts.ini to use specific SSH key:
[hosts]
192.168.1.10 ansible_user=admin ansible_ssh_private_key_file=~/.ssh/ansible_key
192.168.1.11 ansible_user=admin ansible_ssh_private_key_file=~/.ssh/ansible_key
Install Fonts (Optional)
For proper Powerlevel10k appearance, install Nerd Fonts:
Add to playbook:
- name: Download and install MesloLGS NF font
block:
- name: Create fonts directory
file:
path: /usr/share/fonts/truetype/meslo
state: directory
mode: '0755'
- name: Download MesloLGS NF fonts
get_url:
url: "https://github.com/romkatv/powerlevel10k-media/raw/master/{{ item }}"
dest: "/usr/share/fonts/truetype/meslo/{{ item }}"
loop:
- MesloLGS%20NF%20Regular.ttf
- MesloLGS%20NF%20Bold.ttf
- MesloLGS%20NF%20Italic.ttf
- MesloLGS%20NF%20Bold%20Italic.ttf
- name: Update font cache
command: fc-cache -f -v
Fonts are installed system-wide. Users need to configure their terminal emulator to use "MesloLGS NF" font.
Part 8: Troubleshooting
Playbook Fails with Permission Denied
Issue: SSH connection fails or sudo doesn't work
Solution:
# Test SSH connection
ssh [email protected]
# Test sudo access
ssh [email protected] 'sudo whoami'
# Should output: root
If sudo requires password, add --ask-become-pass:
ansible-playbook -i hosts.ini zsh-playbook.yml --ask-become-pass
Oh-My-Zsh Installation Fails
Issue: curl command fails or times out
Solution: Check internet connectivity on target servers
ansible -i hosts.ini all -m shell -a "curl -I https://raw.githubusercontent.com"
Shell Doesn't Change
Issue: Users still have bash after running playbook
Solution: Check if /usr/bin/zsh exists in /etc/shells
ansible -i hosts.ini all -m shell -a "cat /etc/shells"
If missing, add it:
- name: Add zsh to /etc/shells
lineinfile:
path: /etc/shells
line: /usr/bin/zsh
state: present
Plugins Don't Work
Issue: Autosuggestions or syntax highlighting not working
Solution: Verify plugins are loaded in .zshrc:
ssh [email protected] 'grep "plugins=" ~/.zshrc'
Ensure plugins are in the array:
plugins=(
git
zsh-autosuggestions
zsh-syntax-highlighting
)
Powerlevel10k Not Showing
Issue: Prompt shows [oh-my-zsh] instead of Powerlevel10k
Solution:
- Check theme is set correctly in
.zshrc:
ZSH_THEME="powerlevel10k/powerlevel10k"
- Verify theme directory exists:
ssh [email protected] 'ls ~/.oh-my-zsh/custom/themes/'
- Run configuration wizard:
ssh [email protected]
p10k configure
Part 9: Idempotency
The playbook is idempotent - safe to run multiple times:
# First run: Installs everything
ansible-playbook -i hosts.ini zsh-playbook.yml
# Second run: Skips already completed tasks (changed=0 for most tasks)
ansible-playbook -i hosts.ini zsh-playbook.yml
Output on second run:
PLAY RECAP *************************************************
192.168.1.10 : ok=12 changed=0 unreachable=0 failed=0
192.168.1.11 : ok=12 changed=0 unreachable=0 failed=0
Part 10: Uninstall (Optional)
To revert changes:
---
- name: Uninstall ZSH configuration
hosts: all
become: yes
vars:
users:
- name: admin
home: /home/admin
- name: developer
home: /home/developer
tasks:
- name: Change shell back to bash
command: chsh -s /bin/bash {{ item.name }}
loop: "{{ users }}"
- name: Remove .oh-my-zsh directory
file:
path: "{{ item.home }}/.oh-my-zsh"
state: absent
loop: "{{ users }}"
- name: Remove .zshrc
file:
path: "{{ item.home }}/.zshrc"
state: absent
loop: "{{ users }}"
- name: Remove .p10k.zsh
file:
path: "{{ item.home }}/.p10k.zsh"
state: absent
loop: "{{ users }}"
- name: Uninstall zsh package
package:
name: zsh
state: absent
Best Practices
- ✅ Use SSH keys instead of passwords
- ✅ Test in development before production
- ✅ Use version control for playbooks (git)
- ✅ Document changes in commit messages
- ✅ Keep backups of original configs
- ✅ Use --check mode before executing
- ✅ Limit users to only those who need ZSH
- ✅ Monitor disk space (Oh-My-Zsh + plugins = ~50MB per user)
- ✅ Regular updates to plugins and theme
- ✅ User training on ZSH features and shortcuts
Additional Resources
- Ansible Documentation
- Oh-My-Zsh GitHub
- Powerlevel10k GitHub
- ZSH Documentation
- Ansible Best Practices
Example Output
Successful playbook execution:
PLAY [Install and configure zsh] ***************************
TASK [Gathering Facts] *************************************
ok: [192.168.1.10]
ok: [192.168.1.11]
TASK [Install zsh using package manager] *******************
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Install util-linux-user on RHEL-based systems] ******
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Modify /etc/pam.d/chsh for unrestricted chsh] *******
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Change shell for users] ******************************
changed: [192.168.1.10] => (item={'name': 'admin', 'home': '/home/admin'})
changed: [192.168.1.11] => (item={'name': 'admin', 'home': '/home/admin'})
TASK [Check if .oh-my-zsh exists] **************************
ok: [192.168.1.10]
ok: [192.168.1.11]
TASK [Install .oh-my-zsh] **********************************
changed: [192.168.1.10] => (item={'name': 'admin', 'home': '/home/admin'})
changed: [192.168.1.11] => (item={'name': 'admin', 'home': '/home/admin'})
TASK [Clone powerlevel10k theme for users] ****************
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Clone zsh-autosuggestions plugin] ********************
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Clone zsh-syntax-highlighting plugin] ****************
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Copy zshrc file] *************************************
changed: [192.168.1.10]
changed: [192.168.1.11]
TASK [Copy p10k.zsh file] **********************************
changed: [192.168.1.10]
changed: [192.168.1.11]
PLAY RECAP *************************************************
192.168.1.10 : ok=12 changed=10 unreachable=0 failed=0
192.168.1.11 : ok=12 changed=10 unreachable=0 failed=0