Skip to main content

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 address
  • ansible_user=admin: SSH user with sudo privileges
  • [hosts]: Group name (referenced in playbook as hosts: 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

  1. Install ZSH: Uses generic package module (works on Debian, Ubuntu, RHEL, Rocky, etc.)
  2. Install utilities: On RHEL-based systems, installs util-linux-user (provides chsh), git, and tar
  3. Modify PAM: Allows changing shell without restriction
  4. Change shell: Sets ZSH as default shell for each user
  5. Install Oh-My-Zsh: Downloads and installs the framework
  6. Clone theme/plugins: Installs Powerlevel10k theme and useful plugins
  7. Copy configs: Deploys your custom .zshrc and .p10k.zsh files

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

Quick Setup

If you don't have a p10k.zsh file yet:

  1. Install ZSH and Powerlevel10k locally
  2. Run p10k configure and follow the wizard
  3. Copy the generated ~/.p10k.zsh to your files/ 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
note

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:

  1. Check theme is set correctly in .zshrc:
ZSH_THEME="powerlevel10k/powerlevel10k"
  1. Verify theme directory exists:
ssh [email protected] 'ls ~/.oh-my-zsh/custom/themes/'
  1. 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

  1. Use SSH keys instead of passwords
  2. Test in development before production
  3. Use version control for playbooks (git)
  4. Document changes in commit messages
  5. Keep backups of original configs
  6. Use --check mode before executing
  7. Limit users to only those who need ZSH
  8. Monitor disk space (Oh-My-Zsh + plugins = ~50MB per user)
  9. Regular updates to plugins and theme
  10. User training on ZSH features and shortcuts

Additional Resources

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