Installing Ansible
Installing
Ansible is a configuration management (CM) tool to repicate and update configurations across servers.
- Install WSL in Windows to use Linux:
- List available versions:
wsl --list --online - Install specific distro:
wsl --install Ubuntu-24.04 - Restart if prompted and complete first time setup to add user
- Check version: wsl -l -v
- Set default version:
wsl --set-default-version 2 - Install Ansible through Linux shell
- Install Ansible PPA:
sudo apt install -y software-properties-commonsudo add-apt-repository --yes --update ppa:ansible/ansible- Install Ansible:
sudo apt install -y ansible - Check version:
ansible --version
Useful
- Ansible uses indentation instead of brackets to determine the code level/block
- To access files in Windows partition while running WSL Linux shell, use
mtn/to access any file drives C:\Users\Tran\Desktoptranslate to/mnt/c/Users/Tran/Desktopin Linux shell- VS Code has extensions that are useful for ansible files
- WSL: (
ms-vscode-remote.remote-wsl) - Allow VS Code to connect to WSL OS and run command through the Linux shell, mainly python commands
- After opening ansible project in VS Code, press
Ctrl + Shift + Pand typeWSL: Reopen Folder in WSL - Allow cmd like
ansible-galaxy role init [role-name] - Allow cmd like
ansible-playbook -i tests/inventory.ini tests/test.yml - Ansible: (
redhat.ansible) - Need to install in VS Code while it's in WSL mode as well
- Has autocomplete for modules, syntax validation, and hover a module to see its documentation
- YAML: (
redhat.vscode-yaml) - Need to install in VS Code while it's in WSL mode as well
- Has YAML validation, autocomplete, and built-in Kubernetes syntax support
Setup and Running Ansible
Setting Up Ansible
Ansible, at the bare minimum, requires a inventory file and a playbook.yml file
inventory: holds the group of servers IP and SSH info
[solr-nodes]
{host1} ansible_user={username} ansible_ssh_private_key_file={private key}
{host2} ansible_user={username} ansible_ssh_private_key_file={private key}
[solr-zk]
{host1} ansible_user={username} ansible_ssh_private_key_file={private key}
=================================================
// Alternative
[solr-nodes]
{host1}
{host2}
[solr-nodes:vars]
webhhook="xxx"
[solr-zk]
zk1 ansible_host=10.0.0.5
zk2 ansible_host=10.0.0.6
[solr-zk:vars]
webhhook="xxx"
[all:vars]
ansible_user={username}
ansible_ssh_private_key_file={private key}
ssh_port=22[solr-nodes]: the name of the groupansible_user: SSH usernameansible_ssh_private_key_file: SSH key
playbook.yml: holds the tasks that can be run on any server in inventory--- # Indicate start of YAML file
- hosts: solr-nodes # Group name the tasks are for
become: true # Use sudo
tasks: # list of actions
- name: Install nginx # description
apt: # ansible module
name: nginx
state: present
update_cache: yesRunning Ansible
Prep:
- Export the env variable:
export ANSIBLE_HOST_KEY_CHECKING=Falseso ansible would accept password protected ssh key - If ssh key is on Windows, then copy it over to WSL system at "~/.ssh/" and change permission to 600
- Ansible does not allow ssh login as root, so a user need to be created on the server beforehand
sudo adduser [user]sudo usermod -aG sudo [user]sudo apt update && sudo apt upgrade -y: apt should also be updated and upgradedsudo apt install python3-passlib -y: to hash password in vault
To run the playbook:
ansible-playbook -i inventory playbook.ymlansible-playbook -i inventory playbook.yml --check --diff: dry run that show the exact file differencesansible-playbook playbook.yml --ask-vault-pass: run playbook with value secrets insertedansible-playbook -i inventory playbook.yml -u [username] --private-key [private key] --ask-pass -K: ask for sudo passwordansible-playbook -i inventory playbook.yml --limit zk1,zk2: limit tasks to zk1 and zk2 servers label in inventoryansible-playbook -i inventory playbook.yml --limit 10.0.0.5: limit task to specific IPansible-playbook -i inventory playbook.yml --ask-vault-pass -K --check --diff --limit zk2: main checkansible-playbook -i inventory playbook.yml --ask-vault-pass -K --limit zk2: main overall
Directory Structure
In an ansible folder, the roles/ directory is used to organize reusable server configuration
defaults/: contain the default variables as backup if they were not specifiedtasks/: the main playbookfiles/: static files copied to serverstemplates/: Jinja2 templates (dynamic configs)handlers/: Actions triggered when something changes (restart nginx etc.)vars/: Variables specific to the role
ansible/
├── group_vars/all/vault.yml
├── inventory
├── playbook.yml
└── roles/
├── nginx/
│ ├── tasks/
│ │ └── main.yml
│ ├── files/
│ │ └── nginx.conf
│ ├── templates/
│ │ └── nginx.conf.j2
│ ├── handlers/
│ │ └── main.yml
│ └── vars/
│ └── main.yml
│
└── solr/
├── tasks/
│ └── main.yml
├── files/
│ └── solr.xml
└── templates/Using roles in playbook
---
- hosts: solr-nodes
become: true
roles:
- nginx
- solr
============================================================================
Ansible automatically loads:
roles/nginx/tasks/main.yml
roles/solr/tasks/main.ymlDefaults Directory
The defaults/ folder has a main.yml file, containing a list of default variables for the role; they are backup if these parameters were not specified
# defaults/main.yml
ssh_port: 22
solr_port: 8983Tasks Directory
The tasks/ folder has a main.yml file, containing a list of tasks that get inserted into the playbook
# tasks/main.yml
---
- name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Enable nginx
service:
name: nginx
state: started
enabled: trueFiles Directory
The files/ is for static files you want to copy exactly as-is to the remote server
- The
copymodule replace the file at the destination if it exists - Numeric(Octal) Mode for File Permission:
7: rwx6: rw-5: r-x4: r--3: -wx2: -w-1: --x0: ---
roles/
└── nginx/
├── tasks/
│ └── main.yml
├── files/
│ ├── mysite.conf
│ └── ssl/
│ └── example.crt
============================================================================
# Copy single file
- name: Copy SSL key
copy:
src: ssl/example.key
dest: /etc/ssl/private/example.key
owner: root
group: root
mode: '0600'
============================================================================
# Copy multiple files
- name: Copy multiple static files
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- { src: "mysite.conf", dest: "/etc/nginx/sites-available/mysite.conf", mode: "0644" }
- { src: "ssl/example.crt", dest: "/etc/ssl/certs/example.crt", mode: "0644" }
- { src: "ssl/example.key", dest: "/etc/ssl/private/example.key", mode: "0600" }Templates Directory
The templates/ is used for dynamic configuration files that need variable substitution or logic. It uses Jinja2, .j2, syntax.
- Jinja2 also supports conditionals in the templates
templatemodule is the same ascopymodule except used for.j2file- Facts: host-specific variables that Ansible automatically knows and replace
ansible_host: IP/hostname Ansible connects to (10.0.0.5)inventory_hostname: The name used in the inventory (zk1)ansible_user: SSH user connectingansible_port: SSH portansible_fqdn: Fully qualified domain nameansible_hostname: Hostnameansible_default_ipv4.address: Primary IPv4 address
roles/
└── nginx/
├── tasks/
│ └── main.yml
├── templates/
│ └── nginx.conf.j2
└── handlers/
└── main.yml
============================================================================
# nginx.conf.j2
server {
listen 80;
server_name {{ server_name }}; # {{server_name}} will be replaced
{% if enable_ssl %} # start if
listen 443 ssl; |
ssl_certificate {{ ssl_cert }}; |
ssl_certificate_key {{ ssl_key }}; |
{% endif %} # end if
root {{ document_root }};
}
============================================================================
# playbook.yml
---
- hosts: webservers
become: true
vars:
server_name: www.example.com # variable replacement
document_root: /var/www/html
enable_ssl: true # conditional variable
ssl_cert: /etc/ssl/certs/example.crt
ssl_key: /etc/ssl/private/example.key
roles:
- nginx
============================================================================
# nginx/tasks/main.yml
---
- name: Render nginx config from template
template: # How to copy templates to dest
src: nginx.conf.j2
dest: /etc/nginx/sites-available/mysite.conf
owner: root
group: root
mode: '0644'
notify: restart nginxVars Directory
The main.yml file whole the variables that can be applied to the whole role
- Good for variables that rarely change
# vars/main.yml
---
nginx_package: nginx
nginx_port: 80
document_root: /var/www/html
ssl_cert: /etc/ssl/certs/example.crt
ssl_key: /etc/ssl/private/example.keyHandlers Directory
The handlers/ directory in Ansible roles is used for actions that should run only when something changes
- Commonly used to
restart services | reload service | restart daemons after config changes - Handlers are run AFTER all tasks in the playbook finish so even if a handler is triggered multiple times, it'll only run once
# This task will force any pending handlers to run immediately - name: Flush handlers to restart Solr before proceeding meta: flush_handlers
# handlers/main.yml
---
- name: restart nginx
service:
name: nginx
state: restarted
============================================================================
# nginx/tasks/main.yml
---
- name: Install nginx # handler alias must match value of notify parameter
apt:
name: nginx
state: present
- name: Upload nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx # if nginx.conf was changed then handler "restart nginx" will run
- name: Upload site config
template:
src: site.conf.j2
dest: /etc/nginx/sites-enabled/site.conf
notify: restart nginx # if site.conf was changed then handler "restart nginx" will runModules
Ansible modules are standalone scripts that perform the tasks
- Index of Modules: link
stateparameter: tells Ansible the desired end condtion of a resourceloop: keyword used to repeat a task multiple times while replacing"{{ item }}"each time- Can also be used to loop over a range
- name: Upload config files
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: "nginx.conf", dest: "/etc/nginx/nginx.conf" }
- { src: "site.conf", dest: "/etc/nginx/sites-enabled/site.conf" }become: true: run task/playbook as sudovars: set variable block for task/playbook levelregister: [variable]: store the result of the task into the variable[variable].stdout: output what was stored[variable].stdout_lines: output what was stored into an array/ as multi-lines
when: [variable].changed: run the task only if the registered variable's task was changedignore_errors: "{{ ansible_check_mode | default(false) }}": ignore the task if it gave error during --checkapt: install/remove/update packages
state: present: ensure package installed or file is therestate: absent: ensure package or file is removedstate: lastest: upgrade package or file to newest versionstate: reinstalled: force reinstall
- name: Install packages
apt:
name:
- nginx
- openjdk-17-jdk
- curl
state: presentservice: manage services
state: started: service is runningstate: stopped: service is stoppedstate: restarted: restart servicestate: reloaded: reload configenabled: true: enable service at bootdisabled: true: disable service at boot
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: truecopy: upload static files
state: file: creates or updates the filestate: absent: remove the filestate: directory: creates a directory instead
- name: Upload config file
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"template: upload dynamic config
state: file: creates or updates the filestate: absent: remove the file
- name: Upload nginx config template
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conffile: manage files and directories
state: directory: creates directorystate: file: creates empty filestate: touch: Updates file timestamp or creates file if missingstate: absent: remove file or directorystate: link: create a symbolic linkstate: hard: create a hard link
- name: Create Solr directory
file:
path: /opt/solr
state: directory
owner: solr
group: solr
mode: "0755"get_url: download files
state: file: Ensure file exists; download if missing or changedstate: absent: Remove the file
- name: Download Solr archive
get_url:
url: https://archive.apache.org/dist/solr/9.5.0/solr-9.5.0.tgz
dest: /tmp/solr.tgzunarchive: extract .tar.gz or /zip
state: present: Ensure archive is extractedstate: absent: Remove extracted filesremote-src: yes: src is from remote server
- name: Extract Solr
unarchive:
src: /tmp/solr.tgz
dest: /opt/
remote_src: yesuser: manage users
state: present: Create user if missingstate: absent: Remove userstate: locked: Lock user loginstate: unlocked: Unlock user loginsystem: yes: creates system usercreate_home: yes: creates normal userpassword: "{{ 'vaultPassword' | password_hash('sha512') }}"- Use ansible-value instead of hardcoding password into the password field
- Setting up ansible-vault
ansible-vault create vault.yml: create the value file and open a text editorvault.ymlshould go inansible/group_vars/all/vault.ymlto be accessible by all roles- Inside
vault.yml, adduserPassword: "MyVerySecurePassword123!" vault.ymlis encrypted on disk after- Run playbook as
ansible-playbook playbook.yml --ask-vault-pass ansible-vault view vault.yml: check the vaultansible-vault edit vault.yml: edit the vault
- name: Create user securely
user:
name: username
shell: /bin/bash
state: present
create_home: yes
password: "{{ userPassword | password_hash('sha512') }}"group: manage groups
state: present: Create user if missingstate: absent: Remove user
- name: Create solr group
group:
name: solrlineinfile: modify single line in a file
state: present: Ensure line exists (insert if missing)state: absent: Ensure line is removedinsertafter: EOF: ensures line is appended at the end
- name: Disable root SSH login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'ufw: firewall rules
state: enabled: Enable firewallstate: disabled: Disable firewallstate: reloaded: Reload rulesstate: reset: Reset firewall to default
- name: Allow SSH
ufw:
rule: allow
port: 22command: run commands
command: >: allow the command to be written on multiple lines and ansible will treat it as a single-line commandargs: extra arguement for the taskchdir: /path/to/run/task: run the task in the specified directorycreates: /path: if /path already exists, then this task will not be run
- name: Check solr version
command: solr --versionshell: run shell commands
- name: Run install script
shell: |
cd /tmp
bash install_solr.shdebug: for debug purposes: like outputting messages
msg: >-: write multi-line strings- Ansible will print each [.....] in its own line
- name: Show multiple outputs
debug:
msg: >-
{{
['=== Zookeeper Status ==='] + zk_status.stdout_lines +
['"==================================================================",'] +
['=== Zookeeper myid: ' + (myid | string) + ' ===']
}}// Sample Output
"=== Zookeeper Status ===",
"● zookeeper.service - Apache ZooKeeper",
" Loaded: loaded ..."
"========================================================================",
"=== Zookeeper myid: 1 ==="