Ansible
Step 1: Understanding Ansible Basics
What is Ansible?
- Agentless: No software needed on managed nodes (just Python + SSH or Docker)
- Declarative: You describe the desired state, Ansible makes it happen
- Idempotent: Running the same task multiple times produces the same result
Key Concepts:
- Control Node: Where you run Ansible (your
ansiblecontainer) - Managed Nodes: Servers you manage (debian, ubuntu, rocky, suse)
- Inventory: List of your managed nodes
- Modules: Tools that do specific tasks (copy files, install packages, etc.)
- Playbooks: YAML files containing automation instructions
- Tasks: Individual actions in a playbook
Step 2: Inventory Deep Dive
Your inventory tells Ansible what hosts exist and how to group them.
Let’s create a better inventory:
cd ~/ansible-lab
cat > inventory.ini << 'EOF'
# Individual hosts
[webservers]
[databases]
[monitoring]
[production:children]
[all:vars]
[webservers:vars]
Test your inventory:
# List all hosts
ansible all -i inventory.ini --list-hosts
# List hosts in a specific group
ansible webservers -i inventory.ini --list-hosts
# See all inventory details
ansible-inventory -i inventory.ini --list
Step 3: Ad-Hoc Commands (Quick One-Liners)
Ad-hoc commands are for quick tasks without writing a playbook.
Syntax: ansible <hosts> -i <inventory> -m <module> -a "<arguments>"
Let’s practice:
# Ping all hosts
ansible all -i inventory.ini -m ping
# Check disk space
ansible all -i inventory.ini -m shell -a "df -h /"
# Create a file on webservers only
ansible webservers -i inventory.ini -m file -a "path=/tmp/test.txt state=touch mode=0644"
# Check if file exists
ansible webservers -i inventory.ini -m stat -a "path=/tmp/test.txt"
# Get facts about rocky
ansible rocky -i inventory.ini -m setup
# Install a package (on debian-based systems)
ansible debian -i inventory.ini -m apt -a "name=curl state=present update_cache=yes"
Try these yourself! Experiment with different hosts and modules.
Step 4: Your First Playbook
Playbooks are more powerful than ad-hoc commands. Let’s build one together:
cat > step4-playbook.yml << 'EOF'
---
- name: Basic System Setup
hosts: all
tasks:
- name: Display operating system
ansible.builtin.debug:
msg: "This is {{ inventory_hostname }} running {{ ansible_distribution }}"
- name: Create application directory
ansible.builtin.file:
path: /opt/myapp
state: directory
mode: '0755'
- name: Create a configuration file
ansible.builtin.copy:
content: |
# My App Configuration
hostname={{ inventory_hostname }}
environment=lab
dest: /opt/myapp/config.txt
mode: '0644'
- name: Verify file was created
ansible.builtin.stat:
path: /opt/myapp/config.txt
register: config_file
- name: Show file status
ansible.builtin.debug:
msg: "Config file exists: {{ config_file.stat.exists }}"
EOF
Run it:
ansible-playbook -i inventory.ini step4-playbook.yml
Verify:
docker exec debian cat /opt/myapp/config.txt
docker exec ubuntu cat /opt/myapp/config.txt
Notice how the hostname is different in each file? That’s variables at work!
Step 5: Variables
Variables make playbooks flexible and reusable.
cat > step5-variables.yml << 'EOF'
---
- name: Working with Variables
hosts: webservers
vars:
app_name: "my-web-app"
app_version: "1.0.0"
app_port: 8080
tasks:
- name: Show variables
ansible.builtin.debug:
msg: "Installing {{ app_name }} version {{ app_version }} on port {{ app_port }}"
- name: Create app directory
ansible.builtin.file:
path: "/opt/{{ app_name }}"
state: directory
- name: Create version file
ansible.builtin.copy:
content: "{{ app_version }}"
dest: "/opt/{{ app_name }}/VERSION"
- name: Use facts (automatically gathered info)
ansible.builtin.debug:
msg: "This server has {{ ansible_processor_vcpus }} CPU cores and {{ ansible_memtotal_mb }} MB RAM"
EOF
Run it:
ansible-playbook -i inventory.ini step5-variables.yml
Variable sources (in order of precedence):
- Command line:
-e "app_name=different-app" - Play vars
- Inventory vars
- Facts (gathered automatically)
Step 6: Conditionals (When to Run Tasks)
Sometimes you only want tasks to run under certain conditions:
cat > step6-conditionals.yml << 'EOF'
---
- name: Conditional Tasks
hosts: all
tasks:
- name: Task only for Debian-based systems
ansible.builtin.debug:
msg: "This is a Debian-based system!"
when: ansible_os_family == "Debian"
- name: Task only for RedHat-based systems
ansible.builtin.debug:
msg: "This is a RedHat-based system!"
when: ansible_os_family == "RedHat"
- name: Task only for SUSE
ansible.builtin.debug:
msg: "This is SUSE!"
when: ansible_os_family == "Suse"
- name: Install package on Debian systems
ansible.builtin.apt:
name: htop
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install package on RedHat systems
ansible.builtin.dnf:
name: htop
state: present
when: ansible_os_family == "RedHat"
EOF
Run it:
ansible-playbook -i inventory.ini step6-conditionals.yml
Notice how different tasks run on different systems!
Step 7: Loops
Need to do the same thing multiple times? Use loops:
cat > step7-loops.yml << 'EOF'
---
- name: Working with Loops
hosts: debian
tasks:
- name: Create multiple directories
ansible.builtin.file:
path: "/tmp/{{ item }}"
state: directory
loop:
- app1
- app2
- app3
- name: Create multiple users
ansible.builtin.file:
path: "/tmp/users/{{ item.name }}"
state: directory
mode: "{{ item.mode }}"
loop:
- { name: 'alice', mode: '0755' }
- { name: 'bob', mode: '0750' }
- { name: 'charlie', mode: '0700' }
- name: Install multiple packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
update_cache: yes
loop:
- curl
- wget
- vim
EOF
Run it:
ansible-playbook -i inventory.ini step7-loops.yml
Verify:
docker exec debian ls -la /tmp/users/
Step 8: Handlers (Do Something When Changed)
Handlers run only when notified and only if a change occurred:
cat > step8-handlers.yml << 'EOF'
---
- name: Understanding Handlers
hosts: debian
tasks:
- name: Create a config file
ansible.builtin.copy:
content: |
# Application Config
setting1=value1
setting2=value2
dest: /tmp/app.conf
notify: Restart application
- name: Create another file
ansible.builtin.copy:
content: "test"
dest: /tmp/test.txt
notify: Restart application
handlers:
- name: Restart application
ansible.builtin.debug:
msg: "Application would be restarted here!"
EOF
Run it twice:
# First run - files are created, handler runs
ansible-playbook -i inventory.ini step8-handlers.yml
# Second run - no changes, handler doesn't run
ansible-playbook -i inventory.ini step8-handlers.yml
See how the handler only runs when something changes?
Step 9: Templates (Dynamic Files)
Templates use Jinja2 to create dynamic configuration files:
# First, create a template
cat > nginx.conf.j2 << 'EOF'
# Nginx Configuration for {{ inventory_hostname }}
server {
listen {{ http_port }};
server_name {{ inventory_hostname }};
root /var/www/{{ app_name }};
# Maximum clients: {{ max_clients }}
location / {
try_files $uri $uri/ =404;
}
}
EOF
# Now create a playbook that uses it
cat > step9-templates.yml << 'EOF'
---
- name: Using Templates
hosts: webservers
vars:
app_name: "mywebsite"
tasks:
- name: Deploy nginx configuration from template
ansible.builtin.template:
src: nginx.conf.j2
dest: /tmp/nginx.conf
mode: '0644'
- name: Show the generated file
ansible.builtin.shell: cat /tmp/nginx.conf
register: nginx_conf
- name: Display configuration
ansible.builtin.debug:
var: nginx_conf.stdout_lines
EOF
Run it:
ansible-playbook -i inventory.ini step9-templates.yml
Notice how the template uses variables from your inventory! Each host gets a customized file.
Step 10: Putting It All Together – Real World Example
Let’s create a complete playbook that sets up a web application:
cat > complete-example.yml << 'EOF'
---
- name: Deploy Web Application
hosts: webservers
vars:
app_name: "company-website"
app_version: "2.1.0"
deploy_user: "www-data"
tasks:
- name: Install required packages
ansible.builtin.apt:
name:
- curl
- wget
- git
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Create application directory
ansible.builtin.file:
path: "/opt/{{ app_name }}"
state: directory
owner: root
group: root
mode: '0755'
- name: Create subdirectories
ansible.builtin.file:
path: "/opt/{{ app_name }}/{{ item }}"
state: directory
mode: '0755'
loop:
- logs
- config
- data
- name: Deploy application config
ansible.builtin.copy:
content: |
# {{ app_name }} Configuration
# Deployed by Ansible on {{ ansible_date_time.date }}
APP_NAME={{ app_name }}
APP_VERSION={{ app_version }}
HOSTNAME={{ inventory_hostname }}
ENVIRONMENT=production
LOG_PATH=/opt/{{ app_name }}/logs
dest: "/opt/{{ app_name }}/config/app.conf"
mode: '0644'
notify: App config changed
- name: Create version file
ansible.builtin.copy:
content: "{{ app_version }}"
dest: "/opt/{{ app_name }}/VERSION"
- name: Generate deployment report
ansible.builtin.copy:
content: |
Deployment Report
=================
Server: {{ inventory_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Application: {{ app_name }} v{{ app_version }}
Deployed: {{ ansible_date_time.iso8601 }}
CPU Cores: {{ ansible_processor_vcpus }}
Memory: {{ ansible_memtotal_mb }} MB
dest: "/opt/{{ app_name }}/DEPLOYMENT_REPORT.txt"
- name: Verify deployment
ansible.builtin.stat:
path: "/opt/{{ app_name }}/config/app.conf"
register: app_config
- name: Display deployment status
ansible.builtin.debug:
msg: "โ Application {{ app_name }} v{{ app_version }} deployed successfully on {{ inventory_hostname }}"
when: app_config.stat.exists
handlers:
- name: App config changed
ansible.builtin.debug:
msg: "Configuration changed! In production, this would restart the application."
EOF
Run it:
ansible-playbook -i inventory.ini complete-example.yml
Check the results:
docker exec debian cat /opt/company-website/DEPLOYMENT_REPORT.txt
docker exec ubuntu cat /opt/company-website/config/app.conf
docker exec debian ls -la /opt/company-website/
Your Practice Challenge! ๐ฏ
Create a playbook that:
- Runs on the
databasesgroup (rocky) - Installs
postgresql(usednfmodule) - Creates a directory
/var/lib/mydb - Creates a config file at
/etc/mydb.confwith some settings - Only runs the installation if the OS is RedHat family
Try it yourself! I’ll help if you get stuck.
What’s Next?
You’ve learned:
- โ Inventory and groups
- โ Ad-hoc commands
- โ Playbooks and tasks
- โ Variables
- โ Conditionals
- โ Loops
- โ Handlers
- โ Templates
Next topics to explore:
- Roles (organizing complex playbooks)
- Ansible Vault (encrypting secrets)
- Error handling (block/rescue)
- Tags (running specific parts of playbooks)
- Ansible Galaxy (using community roles)
Great question! Let’s break down ansible_connection=docker.
What is ansible_connection?
ansible_connection tells Ansible HOW to connect to a managed node. It’s the “transport method.”
Common Connection Types
1. SSH (Default and Most Common)
webserver1 ansible_connection=ssh ansible_host=192.168.1.10
- Uses SSH protocol to connect to remote servers
- This is what you’d use in production
- Requires SSH server running on the target
- Most secure for real servers
2. Docker
debian ansible_connection=docker
- Connects directly to Docker containers using the Docker API
- No SSH needed!
- Ansible runs
docker execunder the hood - Perfect for labs and testing
3. Local
localhost ansible_connection=local
- Runs commands on the control node itself
- No network connection needed
4. WinRM (Windows)
windows_server ansible_connection=winrm
- For managing Windows servers
How ansible_connection=docker Actually Works
When you run:
ansible debian -i inventory.ini -m ping
Behind the scenes, Ansible does:
docker exec debian python3 -c "import json; print(json.dumps({'ping': 'pong'}))"
Let’s prove it! Run this and watch:
# In another terminal on your host, watch docker commands
docker events
# Then in your ansible container, run:
ansible debian -i inventory.ini -m shell -a "echo Hello"
You’ll see Docker exec events!
Why Docker Connection for Your Lab?
Advantages:
- โ No SSH setup needed – saves time in a lab
- โ Fast – direct container access
- โ Simple – fewer moving parts
- โ Perfect for learning – focus on Ansible, not networking
Disadvantages:
- โ Only works with Docker containers
- โ Not used in real production (you’d use SSH there)
- โ Requires Docker socket access
What If You Used SSH Instead?
Let’s compare! Here’s what you’d need for SSH:
# SSH method (more realistic)
[webservers]
debian ansible_connection=ssh ansible_host=debian ansible_user=root ansible_ssh_pass=password # Docker method (easier for labs)
[webservers]
debian ansible_connection=docker
With SSH, you’d need:
- SSH server running in each container
- Authentication (password or SSH keys)
- Network connectivity between containers
Experiment: Try Both Methods
Let’s set up one container with SSH so you can see the difference:
# Exit ansible container first
exit
On your host:
# Set up SSH in debian container
docker exec -it debian bash -c "
apt update &&
apt install -y openssh-server &&
mkdir -p /run/sshd &&
echo 'root:password' | chpasswd &&
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config &&
/usr/sbin/sshd
"
# Get debian's IP address
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' debian
Now back in your ansible container:
docker exec -it ansible bash
cd ~/ansible-lab
# Create a new inventory with SSH connection
cat > inventory-ssh.ini << 'EOF'
[webservers]
debian ansible_connection=ssh ansible_host=172.17.0.X ansible_user=root ansible_ssh_pass=password
[all:vars]
ansible_ssh_common_args=’-o StrictHostKeyChecking=no’ EOF # Replace 172.17.0.X with the actual IP you got above!
Then test both:
# Docker connection (fast, no setup)
ansible debian -i inventory.ini -m ping
# SSH connection (more realistic)
ansible debian -i inventory-ssh.ini -m ping
Other Connection Parameters
Here are related parameters you might see:
[webservers]
server1 ansible_connection=ssh # Connection type
ansible_host=192.168.1.10 # Actual IP/hostname
ansible_port=2222 # SSH port (default: 22)
ansible_user=admin # Login user
ansible_password=secret # Password (bad practice!)
ansible_ssh_private_key_file=~/.ssh/id_rsa # SSH key (better!)
ansible_python_interpreter=/usr/bin/python3 # Python path
Quick Reference
| Connection Type | When to Use | Example |
|---|---|---|
ssh | Production servers | Real cloud servers, VMs |
docker | Local containers | Your current lab setup |
local | Control node itself | Running tasks on Ansible host |
winrm | Windows servers | Windows infrastructure |
kubectl | Kubernetes pods | K8s container management |
Try This Exercise
Create an inventory with different connection types:
cat > mixed-inventory.ini << 'EOF'
[local_tasks]
localhost ansible_connection=local
[containers]
debian ansible_connection=docker ubuntu ansible_connection=docker
[all:vars]
ansible_python_interpreter=/usr/bin/python3 EOF
Then run:
ansible localhost -i mixed-inventory.ini -m setup -a "filter=ansible_hostname"
ansible containers -i mixed-inventory.ini -m setup -a "filter=ansible_hostname"
See how Ansible connects differently to each?
TL;DR: ansible_connection=docker tells Ansible to use docker exec instead of SSH to run commands. It’s perfect for your lab but in real life you’d use ansible_connection=ssh for actual servers.
Does that make sense? Want to explore any other inventory parameters?
