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 ansible container)
  • 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):

  1. Command line: -e "app_name=different-app"
  2. Play vars
  3. Inventory vars
  4. 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:

  1. Runs on the databases group (rocky)
  2. Installs postgresql (use dnf module)
  3. Creates a directory /var/lib/mydb
  4. Creates a config file at /etc/mydb.conf with some settings
  5. 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 exec under 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:

  1. SSH server running in each container
  2. Authentication (password or SSH keys)
  3. 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 TypeWhen to UseExample
sshProduction serversReal cloud servers, VMs
dockerLocal containersYour current lab setup
localControl node itselfRunning tasks on Ansible host
winrmWindows serversWindows infrastructure
kubectlKubernetes podsK8s 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?

Rust Programming

Previous article

The Facade pattern