Tools: Ansible for DevOps: Automate Server Configuration in 30 Minutes (Not 30 Days)

Tools: Ansible for DevOps: Automate Server Configuration in 30 Minutes (Not 30 Days)

Why Ansible Over Shell Scripts

Getting Started: 5 Minutes

Install Ansible (on your control machine — not the targets)

Create an inventory file

Test connectivity

Run an ad-hoc command

Playbooks: Your Configuration as Code

Full server setup playbook:

Run it:

Roles: Reusable Modules

Example role: nginx

Use roles in a playbook:

Ansible Vault: Managing Secrets

Dynamic Inventory (Cloud Environments)

Key Takeaways You have 15 servers. Each one needs the same packages, the same users, the same firewall rules, the same monitoring agent, and the same application configuration. You can SSH into each one and run the same commands 15 times. Or you can write an Ansible playbook once and apply it to all 15 in parallel. That's Ansible in one sentence: define what your servers should look like, and Ansible makes them look like that. Shell scripts work. Until they don't. Ansible solves all four: A playbook is a YAML file describing the desired state of your servers. When your playbook grows beyond 100 lines, break it into roles. A role is a self-contained unit of configuration. Never put passwords or API keys in plain text YAML: Hard-coded IPs don't work in cloud environments where VMs come and go. Use dynamic inventory to query your cloud provider: 1. Start with ad-hoc commands, then graduate to playbooks, then roles. Don't over-engineer from day one. 2. Always use --check --diff first. See what would change before applying. This builds confidence and catches mistakes. 3. Keep playbooks idempotent. Every task should be safe to run multiple times. Use state: present instead of install commands. 4. Group variables by environment. group_vars/production/, group_vars/staging/ — same playbook, different configs per environment. 5. Version control everything. Playbooks, roles, inventory, vault files — all in Git. Your server configuration is code; treat it like code. Ansible won't replace your cloud-native tools (Terraform for provisioning, Kubernetes for orchestration). But for the servers, VMs, and bare-metal machines that still exist in every organization, Ansible is the fastest path from "manually configured" to "fully automated." What's your go-to configuration management tool? Ansible, Chef, Puppet, or something else? Share your preference in the comments. Follow me for more DevOps automation content. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# This shell script installs nginx... maybe -weight: 500;">apt-get -weight: 500;">update -weight: 500;">apt-get -weight: 500;">install -y nginx -weight: 500;">systemctl -weight: 500;">start nginx -weight: 500;">systemctl -weight: 500;">enable nginx # This shell script installs nginx... maybe -weight: 500;">apt-get -weight: 500;">update -weight: 500;">apt-get -weight: 500;">install -y nginx -weight: 500;">systemctl -weight: 500;">start nginx -weight: 500;">systemctl -weight: 500;">enable nginx # This shell script installs nginx... maybe -weight: 500;">apt-get -weight: 500;">update -weight: 500;">apt-get -weight: 500;">install -y nginx -weight: 500;">systemctl -weight: 500;">start nginx -weight: 500;">systemctl -weight: 500;">enable nginx # This Ansible task installs nginx — correctly, every time - name: Install and -weight: 500;">start nginx hosts: webservers become: true tasks: - name: Install nginx ansible.builtin.package: # Works on -weight: 500;">apt, -weight: 500;">yum, -weight: 500;">apk, etc. name: nginx state: present - name: Start and -weight: 500;">enable nginx ansible.builtin.-weight: 500;">service: name: nginx state: started enabled: true # This Ansible task installs nginx — correctly, every time - name: Install and -weight: 500;">start nginx hosts: webservers become: true tasks: - name: Install nginx ansible.builtin.package: # Works on -weight: 500;">apt, -weight: 500;">yum, -weight: 500;">apk, etc. name: nginx state: present - name: Start and -weight: 500;">enable nginx ansible.builtin.-weight: 500;">service: name: nginx state: started enabled: true # This Ansible task installs nginx — correctly, every time - name: Install and -weight: 500;">start nginx hosts: webservers become: true tasks: - name: Install nginx ansible.builtin.package: # Works on -weight: 500;">apt, -weight: 500;">yum, -weight: 500;">apk, etc. name: nginx state: present - name: Start and -weight: 500;">enable nginx ansible.builtin.-weight: 500;">service: name: nginx state: started enabled: true # macOS -weight: 500;">brew -weight: 500;">install ansible # Ubuntu/Debian -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install ansible # -weight: 500;">pip (any OS) -weight: 500;">pip -weight: 500;">install ansible # macOS -weight: 500;">brew -weight: 500;">install ansible # Ubuntu/Debian -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install ansible # -weight: 500;">pip (any OS) -weight: 500;">pip -weight: 500;">install ansible # macOS -weight: 500;">brew -weight: 500;">install ansible # Ubuntu/Debian -weight: 600;">sudo -weight: 500;">apt-get -weight: 500;">install ansible # -weight: 500;">pip (any OS) -weight: 500;">pip -weight: 500;">install ansible # inventory.ini [webservers] web-1 ansible_host=10.0.1.10 web-2 ansible_host=10.0.1.11 web-3 ansible_host=10.0.1.12 [databases] db-1 ansible_host=10.0.2.10 db-2 ansible_host=10.0.2.11 [all:vars] ansible_user=deploy ansible_ssh_private_key_file=~/.ssh/deploy_key # inventory.ini [webservers] web-1 ansible_host=10.0.1.10 web-2 ansible_host=10.0.1.11 web-3 ansible_host=10.0.1.12 [databases] db-1 ansible_host=10.0.2.10 db-2 ansible_host=10.0.2.11 [all:vars] ansible_user=deploy ansible_ssh_private_key_file=~/.ssh/deploy_key # inventory.ini [webservers] web-1 ansible_host=10.0.1.10 web-2 ansible_host=10.0.1.11 web-3 ansible_host=10.0.1.12 [databases] db-1 ansible_host=10.0.2.10 db-2 ansible_host=10.0.2.11 [all:vars] ansible_user=deploy ansible_ssh_private_key_file=~/.ssh/deploy_key # Ping all hosts ansible all -i inventory.ini -m ping # Output: # web-1 | SUCCESS => {"ping": "pong"} # web-2 | SUCCESS => {"ping": "pong"} # ... # Ping all hosts ansible all -i inventory.ini -m ping # Output: # web-1 | SUCCESS => {"ping": "pong"} # web-2 | SUCCESS => {"ping": "pong"} # ... # Ping all hosts ansible all -i inventory.ini -m ping # Output: # web-1 | SUCCESS => {"ping": "pong"} # web-2 | SUCCESS => {"ping": "pong"} # ... # Check uptime on all webservers ansible webservers -i inventory.ini -m command -a "uptime" # Check disk space on databases ansible databases -i inventory.ini -m command -a "df -h /" # Install a package across all servers ansible all -i inventory.ini -m package -a "name=htop state=present" --become # Check uptime on all webservers ansible webservers -i inventory.ini -m command -a "uptime" # Check disk space on databases ansible databases -i inventory.ini -m command -a "df -h /" # Install a package across all servers ansible all -i inventory.ini -m package -a "name=htop state=present" --become # Check uptime on all webservers ansible webservers -i inventory.ini -m command -a "uptime" # Check disk space on databases ansible databases -i inventory.ini -m command -a "df -h /" # Install a package across all servers ansible all -i inventory.ini -m package -a "name=htop state=present" --become # playbooks/setup-server.yml --- - name: Base Server Configuration hosts: all become: true vars: admin_users: - name: deploy ssh_key: "ssh-rsa AAAA..." - name: sanjay ssh_key: "ssh-rsa BBBB..." required_packages: - -weight: 500;">curl - -weight: 500;">wget - -weight: 500;">git - htop - jq - unzip - net-tools - vim tasks: # System updates - name: Update -weight: 500;">apt cache ansible.builtin.-weight: 500;">apt: update_cache: true cache_valid_time: 3600 # Don't -weight: 500;">update if cached within 1 hour when: ansible_os_family == "Debian" - name: Install required packages ansible.builtin.package: name: "{{ required_packages }}" state: present # User management - name: Create admin users ansible.builtin.user: name: "{{ item.name }}" groups: -weight: 600;">sudo shell: /bin/bash create_home: true loop: "{{ admin_users }}" - name: Add SSH keys for admin users ansible.posix.authorized_key: user: "{{ item.name }}" key: "{{ item.ssh_key }}" state: present loop: "{{ admin_users }}" # Security hardening - name: Disable root SSH login ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PermitRootLogin' line: 'PermitRootLogin no' notify: Restart SSH - name: Disable password authentication ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PasswordAuthentication' line: 'PasswordAuthentication no' notify: Restart SSH # Firewall - name: Install UFW ansible.builtin.-weight: 500;">apt: name: ufw state: present when: ansible_os_family == "Debian" - name: Allow SSH community.general.ufw: rule: allow port: "22" proto: tcp - name: Allow HTTP/HTTPS community.general.ufw: rule: allow port: "{{ item }}" proto: tcp loop: ["80", "443"] when: "'webservers' in group_names" - name: Enable UFW with default deny community.general.ufw: state: enabled default: deny direction: incoming # Time synchronization - name: Install chrony for NTP ansible.builtin.package: name: chrony state: present - name: Enable chrony ansible.builtin.-weight: 500;">service: name: chronyd state: started enabled: true handlers: - name: Restart SSH ansible.builtin.-weight: 500;">service: name: sshd state: restarted # playbooks/setup-server.yml --- - name: Base Server Configuration hosts: all become: true vars: admin_users: - name: deploy ssh_key: "ssh-rsa AAAA..." - name: sanjay ssh_key: "ssh-rsa BBBB..." required_packages: - -weight: 500;">curl - -weight: 500;">wget - -weight: 500;">git - htop - jq - unzip - net-tools - vim tasks: # System updates - name: Update -weight: 500;">apt cache ansible.builtin.-weight: 500;">apt: update_cache: true cache_valid_time: 3600 # Don't -weight: 500;">update if cached within 1 hour when: ansible_os_family == "Debian" - name: Install required packages ansible.builtin.package: name: "{{ required_packages }}" state: present # User management - name: Create admin users ansible.builtin.user: name: "{{ item.name }}" groups: -weight: 600;">sudo shell: /bin/bash create_home: true loop: "{{ admin_users }}" - name: Add SSH keys for admin users ansible.posix.authorized_key: user: "{{ item.name }}" key: "{{ item.ssh_key }}" state: present loop: "{{ admin_users }}" # Security hardening - name: Disable root SSH login ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PermitRootLogin' line: 'PermitRootLogin no' notify: Restart SSH - name: Disable password authentication ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PasswordAuthentication' line: 'PasswordAuthentication no' notify: Restart SSH # Firewall - name: Install UFW ansible.builtin.-weight: 500;">apt: name: ufw state: present when: ansible_os_family == "Debian" - name: Allow SSH community.general.ufw: rule: allow port: "22" proto: tcp - name: Allow HTTP/HTTPS community.general.ufw: rule: allow port: "{{ item }}" proto: tcp loop: ["80", "443"] when: "'webservers' in group_names" - name: Enable UFW with default deny community.general.ufw: state: enabled default: deny direction: incoming # Time synchronization - name: Install chrony for NTP ansible.builtin.package: name: chrony state: present - name: Enable chrony ansible.builtin.-weight: 500;">service: name: chronyd state: started enabled: true handlers: - name: Restart SSH ansible.builtin.-weight: 500;">service: name: sshd state: restarted # playbooks/setup-server.yml --- - name: Base Server Configuration hosts: all become: true vars: admin_users: - name: deploy ssh_key: "ssh-rsa AAAA..." - name: sanjay ssh_key: "ssh-rsa BBBB..." required_packages: - -weight: 500;">curl - -weight: 500;">wget - -weight: 500;">git - htop - jq - unzip - net-tools - vim tasks: # System updates - name: Update -weight: 500;">apt cache ansible.builtin.-weight: 500;">apt: update_cache: true cache_valid_time: 3600 # Don't -weight: 500;">update if cached within 1 hour when: ansible_os_family == "Debian" - name: Install required packages ansible.builtin.package: name: "{{ required_packages }}" state: present # User management - name: Create admin users ansible.builtin.user: name: "{{ item.name }}" groups: -weight: 600;">sudo shell: /bin/bash create_home: true loop: "{{ admin_users }}" - name: Add SSH keys for admin users ansible.posix.authorized_key: user: "{{ item.name }}" key: "{{ item.ssh_key }}" state: present loop: "{{ admin_users }}" # Security hardening - name: Disable root SSH login ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PermitRootLogin' line: 'PermitRootLogin no' notify: Restart SSH - name: Disable password authentication ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^PasswordAuthentication' line: 'PasswordAuthentication no' notify: Restart SSH # Firewall - name: Install UFW ansible.builtin.-weight: 500;">apt: name: ufw state: present when: ansible_os_family == "Debian" - name: Allow SSH community.general.ufw: rule: allow port: "22" proto: tcp - name: Allow HTTP/HTTPS community.general.ufw: rule: allow port: "{{ item }}" proto: tcp loop: ["80", "443"] when: "'webservers' in group_names" - name: Enable UFW with default deny community.general.ufw: state: enabled default: deny direction: incoming # Time synchronization - name: Install chrony for NTP ansible.builtin.package: name: chrony state: present - name: Enable chrony ansible.builtin.-weight: 500;">service: name: chronyd state: started enabled: true handlers: - name: Restart SSH ansible.builtin.-weight: 500;">service: name: sshd state: restarted # Dry run (check mode) — shows what WOULD change ansible-playbook -i inventory.ini playbooks/setup-server.yml --check --diff # Apply ansible-playbook -i inventory.ini playbooks/setup-server.yml # Apply to specific hosts only ansible-playbook -i inventory.ini playbooks/setup-server.yml --limit web-1,web-2 # Dry run (check mode) — shows what WOULD change ansible-playbook -i inventory.ini playbooks/setup-server.yml --check --diff # Apply ansible-playbook -i inventory.ini playbooks/setup-server.yml # Apply to specific hosts only ansible-playbook -i inventory.ini playbooks/setup-server.yml --limit web-1,web-2 # Dry run (check mode) — shows what WOULD change ansible-playbook -i inventory.ini playbooks/setup-server.yml --check --diff # Apply ansible-playbook -i inventory.ini playbooks/setup-server.yml # Apply to specific hosts only ansible-playbook -i inventory.ini playbooks/setup-server.yml --limit web-1,web-2 roles/ ├── common/ # Base server config (every server) │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ ├── files/ │ └── defaults/main.yml # Default variables (overridable) ├── nginx/ # Web server config │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ │ └── nginx.conf.j2 │ └── defaults/main.yml ├── postgresql/ # Database config │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ │ └── postgresql.conf.j2 │ └── defaults/main.yml └── monitoring/ # Node exporter + Promtail ├── tasks/main.yml └── defaults/main.yml roles/ ├── common/ # Base server config (every server) │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ ├── files/ │ └── defaults/main.yml # Default variables (overridable) ├── nginx/ # Web server config │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ │ └── nginx.conf.j2 │ └── defaults/main.yml ├── postgresql/ # Database config │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ │ └── postgresql.conf.j2 │ └── defaults/main.yml └── monitoring/ # Node exporter + Promtail ├── tasks/main.yml └── defaults/main.yml roles/ ├── common/ # Base server config (every server) │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ ├── files/ │ └── defaults/main.yml # Default variables (overridable) ├── nginx/ # Web server config │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ │ └── nginx.conf.j2 │ └── defaults/main.yml ├── postgresql/ # Database config │ ├── tasks/main.yml │ ├── handlers/main.yml │ ├── templates/ │ │ └── postgresql.conf.j2 │ └── defaults/main.yml └── monitoring/ # Node exporter + Promtail ├── tasks/main.yml └── defaults/main.yml # roles/nginx/defaults/main.yml nginx_worker_processes: auto nginx_worker_connections: 1024 nginx_server_name: "_" nginx_root: /var/www/html nginx_ssl_enabled: false # roles/nginx/defaults/main.yml nginx_worker_processes: auto nginx_worker_connections: 1024 nginx_server_name: "_" nginx_root: /var/www/html nginx_ssl_enabled: false # roles/nginx/defaults/main.yml nginx_worker_processes: auto nginx_worker_connections: 1024 nginx_server_name: "_" nginx_root: /var/www/html nginx_ssl_enabled: false # roles/nginx/tasks/main.yml --- - name: Install nginx ansible.builtin.package: name: nginx state: present - name: Deploy nginx configuration ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644' validate: nginx -t -c %s # Validate before applying notify: Reload nginx - name: Deploy site configuration ansible.builtin.template: src: site.conf.j2 dest: /etc/nginx/sites-available/default owner: root group: root mode: '0644' notify: Reload nginx - name: Start and -weight: 500;">enable nginx ansible.builtin.-weight: 500;">service: name: nginx state: started enabled: true # roles/nginx/tasks/main.yml --- - name: Install nginx ansible.builtin.package: name: nginx state: present - name: Deploy nginx configuration ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644' validate: nginx -t -c %s # Validate before applying notify: Reload nginx - name: Deploy site configuration ansible.builtin.template: src: site.conf.j2 dest: /etc/nginx/sites-available/default owner: root group: root mode: '0644' notify: Reload nginx - name: Start and -weight: 500;">enable nginx ansible.builtin.-weight: 500;">service: name: nginx state: started enabled: true # roles/nginx/tasks/main.yml --- - name: Install nginx ansible.builtin.package: name: nginx state: present - name: Deploy nginx configuration ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644' validate: nginx -t -c %s # Validate before applying notify: Reload nginx - name: Deploy site configuration ansible.builtin.template: src: site.conf.j2 dest: /etc/nginx/sites-available/default owner: root group: root mode: '0644' notify: Reload nginx - name: Start and -weight: 500;">enable nginx ansible.builtin.-weight: 500;">service: name: nginx state: started enabled: true # roles/nginx/templates/nginx.conf.j2 worker_processes {{ nginx_worker_processes }}; events { worker_connections {{ nginx_worker_connections }}; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $-weight: 500;">status $body_bytes_sent ' '"$http_referer" "$http_user_agent"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/sites-available/*; } # roles/nginx/templates/nginx.conf.j2 worker_processes {{ nginx_worker_processes }}; events { worker_connections {{ nginx_worker_connections }}; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $-weight: 500;">status $body_bytes_sent ' '"$http_referer" "$http_user_agent"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/sites-available/*; } # roles/nginx/templates/nginx.conf.j2 worker_processes {{ nginx_worker_processes }}; events { worker_connections {{ nginx_worker_connections }}; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $-weight: 500;">status $body_bytes_sent ' '"$http_referer" "$http_user_agent"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/sites-available/*; } # roles/nginx/handlers/main.yml --- - name: Reload nginx ansible.builtin.-weight: 500;">service: name: nginx state: reloaded # roles/nginx/handlers/main.yml --- - name: Reload nginx ansible.builtin.-weight: 500;">service: name: nginx state: reloaded # roles/nginx/handlers/main.yml --- - name: Reload nginx ansible.builtin.-weight: 500;">service: name: nginx state: reloaded # playbooks/webservers.yml --- - name: Configure Web Servers hosts: webservers become: true roles: - common - role: nginx vars: nginx_worker_connections: 4096 nginx_ssl_enabled: true - monitoring # playbooks/webservers.yml --- - name: Configure Web Servers hosts: webservers become: true roles: - common - role: nginx vars: nginx_worker_connections: 4096 nginx_ssl_enabled: true - monitoring # playbooks/webservers.yml --- - name: Configure Web Servers hosts: webservers become: true roles: - common - role: nginx vars: nginx_worker_connections: 4096 nginx_ssl_enabled: true - monitoring # Create an encrypted variables file ansible-vault create group_vars/all/vault.yml # Edit an existing encrypted file ansible-vault edit group_vars/all/vault.yml # Run a playbook with vault (prompts for password) ansible-playbook -i inventory.ini playbooks/deploy.yml --ask-vault-pass # Or use a password file (for CI/CD) ansible-playbook -i inventory.ini playbooks/deploy.yml --vault-password-file ~/.vault_pass # Create an encrypted variables file ansible-vault create group_vars/all/vault.yml # Edit an existing encrypted file ansible-vault edit group_vars/all/vault.yml # Run a playbook with vault (prompts for password) ansible-playbook -i inventory.ini playbooks/deploy.yml --ask-vault-pass # Or use a password file (for CI/CD) ansible-playbook -i inventory.ini playbooks/deploy.yml --vault-password-file ~/.vault_pass # Create an encrypted variables file ansible-vault create group_vars/all/vault.yml # Edit an existing encrypted file ansible-vault edit group_vars/all/vault.yml # Run a playbook with vault (prompts for password) ansible-playbook -i inventory.ini playbooks/deploy.yml --ask-vault-pass # Or use a password file (for CI/CD) ansible-playbook -i inventory.ini playbooks/deploy.yml --vault-password-file ~/.vault_pass # group_vars/all/vault.yml (encrypted) vault_db_password: "super-secret-password" vault_api_key: "sk-1234567890" vault_ssl_cert: | -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- # group_vars/all/vault.yml (encrypted) vault_db_password: "super-secret-password" vault_api_key: "sk-1234567890" vault_ssl_cert: | -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- # group_vars/all/vault.yml (encrypted) vault_db_password: "super-secret-password" vault_api_key: "sk-1234567890" vault_ssl_cert: | -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- # Reference in playbooks (Ansible decrypts automatically) - name: Configure database connection ansible.builtin.template: src: db-config.j2 dest: /etc/app/database.yml vars: db_password: "{{ vault_db_password }}" # Reference in playbooks (Ansible decrypts automatically) - name: Configure database connection ansible.builtin.template: src: db-config.j2 dest: /etc/app/database.yml vars: db_password: "{{ vault_db_password }}" # Reference in playbooks (Ansible decrypts automatically) - name: Configure database connection ansible.builtin.template: src: db-config.j2 dest: /etc/app/database.yml vars: db_password: "{{ vault_db_password }}" # Azure dynamic inventory -weight: 500;">pip -weight: 500;">install azure-mgmt-compute azure-identity # inventory_azure.yml plugin: azure.azcollection.azure_rm auth_source: auto include_vm_resource_groups: - rg-production - rg-staging keyed_groups: - prefix: tag key: tags.role # Group VMs by the 'role' tag # Azure dynamic inventory -weight: 500;">pip -weight: 500;">install azure-mgmt-compute azure-identity # inventory_azure.yml plugin: azure.azcollection.azure_rm auth_source: auto include_vm_resource_groups: - rg-production - rg-staging keyed_groups: - prefix: tag key: tags.role # Group VMs by the 'role' tag # Azure dynamic inventory -weight: 500;">pip -weight: 500;">install azure-mgmt-compute azure-identity # inventory_azure.yml plugin: azure.azcollection.azure_rm auth_source: auto include_vm_resource_groups: - rg-production - rg-staging keyed_groups: - prefix: tag key: tags.role # Group VMs by the 'role' tag # Now Ansible groups VMs by their Azure tags ansible tag_webserver -i inventory_azure.yml -m ping ansible tag_database -i inventory_azure.yml -m ping # Now Ansible groups VMs by their Azure tags ansible tag_webserver -i inventory_azure.yml -m ping ansible tag_database -i inventory_azure.yml -m ping # Now Ansible groups VMs by their Azure tags ansible tag_webserver -i inventory_azure.yml -m ping ansible tag_database -i inventory_azure.yml -m ping - Not idempotent. Run it twice and -weight: 500;">apt-get -weight: 500;">install shows warnings. Run it after a partial failure and you might be in an unknown state. - No error handling. If -weight: 500;">apt-get -weight: 500;">update fails, the script continues and tries to -weight: 500;">install from stale package lists. - OS-specific. This script only works on Debian/Ubuntu. CentOS uses -weight: 500;">yum. Alpine uses -weight: 500;">apk. - No inventory. Which servers to run this on? Hard-coded IPs? SSH in a loop? - Idempotent: Run it 100 times — if nginx is already installed and running, Ansible reports "OK" and changes nothing. - Cross-platform: ansible.builtin.package detects the OS and uses the right package manager. - Inventory-driven: hosts: webservers pulls from your inventory file — no hard-coded IPs.