Infrastructure

Infrastructure as Code with Ansible

Automate server provisioning and configuration management using Ansible playbooks and roles

James Thompson

DevOps Lead • Nov 10, 2025 • 11 min read

Infrastructure Automation

Infrastructure as Code (IaC) transforms infrastructure management from manual processes to automated, version-controlled, and reproducible workflows. Ansible, with its agentless architecture and simple YAML syntax, has become a leading choice for configuration management and orchestration. This guide explores advanced Ansible techniques for modern infrastructure automation.

Why Ansible for IaC?

Ansible Advantages

  • Agentless: Uses SSH and WinRM, no agents required on managed nodes
  • Simple Syntax: YAML-based playbooks are easy to read and write
  • Idempotent: Safe to run multiple times, only makes necessary changes
  • Extensible: Vast ecosystem of modules and collections
  • Multi-Platform: Manages Linux, Windows, network devices, and cloud services
  • No Programming Required: Declarative approach suitable for sysadmins

1. Ansible Project Structure

Best Practice Directory Layout

Organize your Ansible project for scalability and maintainability:

ansible-infrastructure/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ │ ├── all.yml
│ │ ├── webservers.yml
│ │ └── databases.yml
│ └── staging/
│ └── hosts.yml
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ ├── databases.yml
│ └── deploy-app.yml
├── roles/
│ ├── common/
│ ├── nginx/
│ ├── postgresql/
│ └── docker/
├── group_vars/
├── host_vars/
├── files/
├── templates/
└── vault/
└── secrets.yml

Configuration File

Optimize ansible.cfg for production use:

# ansible.cfg
[defaults]
inventory = inventory/production/hosts.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
forks = 20
callback_whitelist = profile_tasks, timer

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

2. Dynamic Inventory

Cloud-Based Dynamic Inventory

Automatically discover infrastructure from cloud providers:

# inventory/aws_ec2.yml
plugin: aws_ec2
regions:
- us-east-1
- us-west-2
filters:
tag:Environment: production
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
- key: tags.Environment
prefix: env
- key: placement.availability_zone
prefix: az
hostnames:
- tag:Name
compose:
ansible_host: public_ip_address

For Azure:

# inventory/azure_rm.yml
plugin: azure_rm
include_vm_resource_groups:
- production-rg
- staging-rg
auth_source: auto
keyed_groups:
- prefix: tag
key: tags
- prefix: location
key: location
conditional_groups:
webservers: "'web' in tags.role"
databases: "'db' in tags.role"

3. Creating Reusable Roles

NGINX Role Structure

Build a comprehensive NGINX role:

# roles/nginx/tasks/main.yml
---
- name: Install NGINX
apt:
name: nginx
state: present
update_cache: yes
when: ansible_os_family == "Debian"

- name: Install NGINX (RedHat)
yum:
name: nginx
state: present
when: ansible_os_family == "RedHat"

- name: Create NGINX directories
file:
path: "{{ item }}"
state: directory
owner: nginx
group: nginx
mode: '0755'
loop:
- /etc/nginx/sites-available
- /etc/nginx/sites-enabled
- /var/www/html

- name: Deploy NGINX configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: restart nginx

- name: Deploy virtual host configs
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item.name }}.conf"
owner: root
group: root
mode: '0644'
loop: "{{ nginx_vhosts }}"
notify: reload nginx

- name: Enable virtual hosts
file:
src: "/etc/nginx/sites-available/{{ item.name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ nginx_vhosts }}"
notify: reload nginx

- name: Ensure NGINX is running
service:
name: nginx
state: started
enabled: yes

Role Variables and Defaults

# roles/nginx/defaults/main.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 20M

nginx_vhosts:
- name: default
listen: 80
server_name: localhost
root: /var/www/html
index: index.html index.htm
ssl: false

nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "HIGH:!aNULL:!MD5"
nginx_gzip: yes
nginx_gzip_types:
- text/plain
- text/css
- application/json
- application/javascript
- text/xml
- application/xml

4. Advanced Playbook Patterns

Multi-Tier Application Deployment

# playbooks/deploy-app.yml
---
- name: Deploy Load Balancers
hosts: loadbalancers
become: yes
roles:
- role: nginx
vars:
nginx_vhosts:
- name: app
listen: 80
proxy_pass: http://app_backend

- name: Deploy Application Servers
hosts: appservers
become: yes
serial: "50%" # Rolling deployment
max_fail_percentage: 25
tasks:
- name: Pull latest code from Git
git:
repo: "{{ app_git_repo }}"
dest: "{{ app_directory }}"
version: "{{ app_version }}"
notify: restart app

- name: Install Python dependencies
pip:
requirements: "{{ app_directory }}/requirements.txt"
virtualenv: "{{ app_directory }}/venv"

- name: Run database migrations
command: "{{ app_directory }}/venv/bin/python manage.py migrate"
run_once: yes
delegate_to: "{{ groups['appservers'][0] }}"

- name: Collect static files
command: "{{ app_directory }}/venv/bin/python manage.py collectstatic --noinput"

handlers:
- name: restart app
systemd:
name: myapp
state: restarted

- name: Configure Database
hosts: databases
become: yes
roles:
- postgresql
tasks:
- name: Create application database
postgresql_db:
name: "{{ db_name }}"
state: present

- name: Create database user
postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
db: "{{ db_name }}"
priv: ALL

Blue-Green Deployment

# playbooks/blue-green-deploy.yml
---
- name: Determine inactive environment
hosts: localhost
gather_facts: no
tasks:
- name: Get current active environment
uri:
url: "http://{{ lb_ip }}/health"
register: health_check

- name: Set deployment target
set_fact:
inactive_env: "{{ 'green' if health_check.json.active == 'blue' else 'blue' }}"
active_env: "{{ health_check.json.active }}"

- name: Deploy to inactive environment
hosts: "{{ hostvars['localhost']['inactive_env'] }}_servers"
become: yes
tasks:
- name: Deploy application
include_role:
name: deploy_app
vars:
environment: "{{ hostvars['localhost']['inactive_env'] }}"

- name: Run health checks
uri:
url: "http://{{ inventory_hostname }}:8080/health"
status_code: 200
register: health
until: health.status == 200
retries: 5
delay: 10

- name: Switch load balancer
hosts: loadbalancers
become: yes
tasks:
- name: Update load balancer configuration
template:
src: lb-config.j2
dest: /etc/nginx/conf.d/app.conf
vars:
active_env: "{{ hostvars['localhost']['inactive_env'] }}"
notify: reload nginx

handlers:
- name: reload nginx
service:
name: nginx
state: reloaded

5. Ansible Vault for Secrets

Encrypting Sensitive Data

# Create encrypted vault file
ansible-vault create vault/secrets.yml

# Encrypt existing file
ansible-vault encrypt group_vars/production/vault.yml

# Edit encrypted file
ansible-vault edit vault/secrets.yml

# Decrypt for viewing
ansible-vault view vault/secrets.yml

# Run playbook with vault
ansible-playbook site.yml --ask-vault-pass

# Using password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass

Vault file structure:

# vault/secrets.yml (encrypted)
---
vault_db_password: "supersecret123"
vault_api_key: "abc123def456"
vault_ssl_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASC...
-----END PRIVATE KEY-----

# group_vars/all/vars.yml (unencrypted)
---
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
ssl_private_key: "{{ vault_ssl_private_key }}"

6. Testing and Validation

Ansible Molecule for Role Testing

# Install Molecule
pip install molecule molecule-docker

# Initialize new role with tests
molecule init role my_role --driver-name docker

# Test sequence
molecule create # Create test instance
molecule converge # Run playbook
molecule verify # Run tests
molecule destroy # Clean up

# Full test cycle
molecule test

Test scenario example:

# molecule/default/verify.yml
---
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: Check if NGINX is installed
package:
name: nginx
state: present
check_mode: yes
register: nginx_installed
failed_when: nginx_installed.changed

- name: Check if NGINX is running
service:
name: nginx
state: started
check_mode: yes
register: nginx_running
failed_when: nginx_running.changed

- name: Test NGINX responds
uri:
url: http://localhost
status_code: 200
register: nginx_response

Ansible-lint for Code Quality

# Install ansible-lint
pip install ansible-lint

# Lint playbooks
ansible-lint playbooks/site.yml

# Lint entire project
ansible-lint

# Configuration file .ansible-lint
---
skip_list:
- '306' # Skip specific rule
warn_list:
- experimental
exclude_paths:
- .cache/
- test/
- molecule/
parseable: true
quiet: false
verbosity: 1

7. Performance Optimization

Optimization Techniques

# Enable SSH pipelining
[ssh_connection]
pipelining = True

# Increase forks for parallel execution
[defaults]
forks = 50

# Use fact caching
gathering = smart
fact_caching = redis
fact_caching_connection = localhost:6379:0
fact_caching_timeout = 86400

# Disable cowsay
nocows = 1

# Use mitogen for faster execution
strategy_plugins = /usr/share/ansible/plugins/mitogen/plugins/strategy
strategy = mitogen_linear

Async Tasks for Long-Running Operations

- name: Long running database backup
command: /usr/bin/backup-database.sh
async: 3600 # Max runtime
poll: 0 # Fire and forget
register: backup_job

- name: Check backup status later
async_status:
jid: "{{ backup_job.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 30
delay: 60

8. CI/CD Integration

GitLab CI Pipeline

# .gitlab-ci.yml
stages:
- lint
- test
- deploy

lint:playbooks:
stage: lint
image: cytopia/ansible-lint
script:
- ansible-lint playbooks/

test:roles:
stage: test
image: quay.io/ansible/molecule
script:
- cd roles/nginx
- molecule test

deploy:staging:
stage: deploy
image: ansible/ansible:latest
script:
- ansible-playbook -i inventory/staging site.yml
only:
- develop
environment:
name: staging

deploy:production:
stage: deploy
image: ansible/ansible:latest
script:
- ansible-playbook -i inventory/production site.yml
only:
- main
when: manual
environment:
name: production

Best Practices Checklist

  • ✓ Use version control for all Ansible code
  • ✓ Organize projects with standard directory structure
  • ✓ Create reusable roles with proper defaults
  • ✓ Use Ansible Vault for sensitive data
  • ✓ Implement idempotency in all tasks
  • ✓ Tag tasks for selective execution
  • ✓ Document variables and role requirements
  • ✓ Test roles with Molecule before deployment
  • ✓ Use dynamic inventory for cloud environments
  • ✓ Implement proper error handling
  • ✓ Enable fact caching for performance
  • ✓ Use handlers for service restarts
  • ✓ Implement CI/CD pipelines
  • ✓ Regular security audits of playbooks

Conclusion

Ansible provides a powerful, flexible platform for infrastructure automation that scales from small projects to enterprise deployments. By following infrastructure as code principles, organizing code properly, testing thoroughly, and integrating with CI/CD pipelines, teams can achieve reliable, repeatable infrastructure management. The key is starting with well-structured roles, building comprehensive playbooks, and continuously refining automation as infrastructure evolves.

Additional Resources

  • → Ansible Galaxy for community roles
  • → Ansible Automation Platform for enterprise features
  • → AWX for open-source web UI
  • → Ansible collections for extended functionality

Related Articles

Cloud

Multi-Cloud Strategy

AWS, Azure & GCP architecture

Docker

Docker Security Hardening

Secure containerized applications

Linux

Linux Kernel Optimization

Performance tuning strategies