Ansible - Server Configuration Automation
Ansible Server Configuration
devopsAnsible - Server Configuration Automation
Managing server infrastructure manually is a dead end. Every administrator who has ever configured dozens of servers knows that repetitive tasks consume enormous amounts of time and are prone to errors. Ansible, created by Michael DeHaan and currently developed by Red Hat, solves this problem in an elegant and straightforward way. In this guide, we will cover everything you need to know to effectively automate server configurations with Ansible.
What is Ansible?#
Ansible is an open-source IT automation tool that enables configuration management, application deployment, and infrastructure orchestration. Unlike many other tools, Ansible stands out for its simplicity - it uses YAML to define tasks, making playbooks readable even for people without programming experience.
Key use cases for Ansible include:
- Configuration management - installing packages, configuring services, managing files
- Application deployment - automated deployments across multiple servers simultaneously
- Orchestration - coordinating complex multi-step processes
- Provisioning - preparing new servers from scratch
- Security and compliance - enforcing security policies
Agentless Architecture#
One of the most important differentiators of Ansible is its agentless architecture. This means that no additional software needs to be installed on the managed servers. Ansible communicates with target machines exclusively via SSH (for Linux/Unix systems) or WinRM (for Windows systems).
This architecture brings many benefits:
- No overhead - no agents consuming resources on target servers
- No additional maintenance - no need to update agents on hundreds of machines
- Security - smaller attack surface, no open ports for agents
- Easy to start - only SSH and Python are needed on target machines
# Requirements on target machines:
# - SSH server (OpenSSH)
# - Python 3.x
# - User with sudo privileges (optional)
On the control machine (from which we run Ansible), we only need to install Ansible itself:
# Installation on Ubuntu/Debian
sudo apt update
sudo apt install ansible
# Installation via pip
pip install ansible
# Installation on CentOS/RHEL
sudo dnf install ansible-core
# Verify installation
ansible --version
Inventory Files#
The inventory is a file that defines hosts and host groups on which Ansible will execute tasks. By default, Ansible looks for the file /etc/ansible/hosts, but you can specify any file using the -i flag.
# inventory.ini - INI format
[webservers]
web1.example.com
web2.example.com
web3.example.com ansible_port=2222
[dbservers]
db1.example.com ansible_user=dbadmin
db2.example.com
[loadbalancers]
lb1.example.com
[production:children]
webservers
dbservers
loadbalancers
[webservers:vars]
http_port=80
max_clients=200
The inventory can also be defined in YAML format, which provides more flexibility:
# inventory.yml - YAML format
all:
children:
webservers:
hosts:
web1.example.com:
http_port: 80
web2.example.com:
http_port: 8080
vars:
ansible_user: deploy
nginx_version: "1.24"
dbservers:
hosts:
db1.example.com:
postgresql_version: "16"
db2.example.com:
postgresql_version: "16"
vars:
ansible_user: dbadmin
production:
children:
webservers:
dbservers:
Playbooks and Tasks#
Playbooks are the heart of Ansible. They are YAML files that define a series of tasks to be executed on specified groups of hosts. Each playbook consists of one or more "plays," and each play contains a list of tasks.
# site.yml - example playbook
---
- name: Configure web servers
hosts: webservers
become: yes
vars:
app_name: myapplication
app_port: 8080
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install required packages
apt:
name:
- nginx
- curl
- git
- ufw
state: present
- name: Start and enable Nginx
service:
name: nginx
state: started
enabled: yes
- name: Allow HTTP through firewall
ufw:
rule: allow
port: "80"
proto: tcp
- name: Allow HTTPS through firewall
ufw:
rule: allow
port: "443"
proto: tcp
Running a playbook:
# Basic execution
ansible-playbook -i inventory.ini site.yml
# With sudo password prompt
ansible-playbook -i inventory.ini site.yml --ask-become-pass
# Dry run (check without making changes)
ansible-playbook -i inventory.ini site.yml --check
# Limit to a specific host
ansible-playbook -i inventory.ini site.yml --limit web1.example.com
# With increased verbosity
ansible-playbook -i inventory.ini site.yml -vvv
Ansible Modules#
Modules are the units of work in Ansible. Each module performs a specific task. Ansible ships with hundreds of built-in modules. Here are the most commonly used ones:
apt/yum Module - Package Management#
# For Debian/Ubuntu systems
- name: Install multiple packages
apt:
name:
- nginx
- postgresql
- python3-pip
- certbot
state: present
update_cache: yes
# For CentOS/RHEL systems
- name: Install packages on CentOS
yum:
name:
- httpd
- mariadb-server
- php
state: latest
# Remove a package
- name: Remove Apache package
apt:
name: apache2
state: absent
purge: yes
copy Module - File Copying#
- name: Copy configuration file
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
backup: yes
- name: Create file with inline content
copy:
content: |
# Application configuration
APP_ENV=production
APP_DEBUG=false
APP_PORT=8080
dest: /etc/myapp/.env
owner: www-data
group: www-data
mode: "0600"
template Module - Jinja2 Templates#
- name: Deploy Nginx configuration from template
template:
src: templates/nginx-vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ app_name }}"
owner: root
group: root
mode: "0644"
notify: Restart Nginx
service Module - Service Management#
- name: Restart and enable PostgreSQL
service:
name: postgresql
state: restarted
enabled: yes
- name: Stop Apache (if installed)
service:
name: apache2
state: stopped
enabled: no
ignore_errors: yes
Variables and Jinja2 Templates#
The Jinja2 template system is a fundamental element of Ansible. It allows for dynamic generation of configuration files based on variables.
# group_vars/webservers.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
server_name: "{{ inventory_hostname }}"
app_root: /var/www/{{ app_name }}
php_version: "8.3"
php_memory_limit: "256M"
php_max_execution_time: 30
ssl_enabled: true
ssl_certificate: "/etc/letsencrypt/live/{{ server_name }}/fullchain.pem"
ssl_certificate_key: "/etc/letsencrypt/live/{{ server_name }}/privkey.pem"
Example Jinja2 template for Nginx configuration:
{# templates/nginx-vhost.conf.j2 #}
server {
listen 80;
server_name {{ server_name }};
{% if ssl_enabled %}
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ server_name }};
ssl_certificate {{ ssl_certificate }};
ssl_certificate_key {{ ssl_certificate_key }};
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
{% endif %}
root {{ app_root }}/public;
index index.php index.html;
access_log /var/log/nginx/{{ app_name }}_access.log;
error_log /var/log/nginx/{{ app_name }}_error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php{{ php_version }}-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Jinja2 also offers advanced constructs:
{# Loops and conditionals in templates #}
{% for upstream_server in upstream_servers %}
server {{ upstream_server }}:{{ app_port }} weight=1;
{% endfor %}
{% if environment == 'production' %}
error_page 500 502 503 504 /50x.html;
{% else %}
# In development environment, show detailed errors
{% endif %}
Handlers#
Handlers are special tasks that are only executed when triggered by a notify directive. They are ideal for restarting services after configuration changes:
---
- name: Configure Nginx
hosts: webservers
become: yes
tasks:
- name: Deploy Nginx configuration
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate Nginx configuration
- Restart Nginx
- name: Deploy PHP-FPM configuration
template:
src: templates/php-fpm.conf.j2
dest: "/etc/php/{{ php_version }}/fpm/pool.d/www.conf"
notify: Restart PHP-FPM
handlers:
- name: Validate Nginx configuration
command: nginx -t
changed_when: false
- name: Restart Nginx
service:
name: nginx
state: restarted
- name: Restart PHP-FPM
service:
name: "php{{ php_version }}-fpm"
state: restarted
An important characteristic of handlers is that they execute only once at the end of a play, even if triggered multiple times. This ensures that a service is not restarted unnecessarily multiple times.
Roles and Ansible Galaxy#
Roles are a way to organize and reuse Ansible code. A role groups tasks, handlers, variables, templates, and files in a structured manner.
# Role structure
roles/
webserver/
tasks/
main.yml
handlers/
main.yml
templates/
nginx-vhost.conf.j2
files/
ssl-params.conf
vars/
main.yml
defaults/
main.yml
meta/
main.yml
# roles/webserver/tasks/main.yml
---
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Install PHP and extensions
apt:
name:
- "php{{ php_version }}-fpm"
- "php{{ php_version }}-cli"
- "php{{ php_version }}-pgsql"
- "php{{ php_version }}-mbstring"
- "php{{ php_version }}-xml"
- "php{{ php_version }}-curl"
- "php{{ php_version }}-zip"
state: present
- name: Configure virtual host
template:
src: nginx-vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ app_name }}"
notify: Restart Nginx
- name: Enable virtual host
file:
src: "/etc/nginx/sites-available/{{ app_name }}"
dest: "/etc/nginx/sites-enabled/{{ app_name }}"
state: link
notify: Restart Nginx
- name: Remove default configuration
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Restart Nginx
# roles/webserver/defaults/main.yml
---
php_version: "8.3"
app_name: myapp
app_port: 8080
nginx_worker_processes: auto
ssl_enabled: false
Ansible Galaxy is a community repository of roles and collections:
# Install a role from Galaxy
ansible-galaxy install geerlingguy.nginx
ansible-galaxy install geerlingguy.postgresql
ansible-galaxy install geerlingguy.php
# Install roles from a requirements file
ansible-galaxy install -r requirements.yml
# Create a new role scaffold
ansible-galaxy init my_new_role
# requirements.yml
---
roles:
- name: geerlingguy.nginx
version: "3.2.0"
- name: geerlingguy.postgresql
version: "3.4.0"
- name: geerlingguy.php
version: "6.0.0"
- name: geerlingguy.certbot
version: "4.1.0"
collections:
- name: community.postgresql
version: "3.2.0"
- name: ansible.posix
version: "1.5.0"
Using roles in a playbook:
# site.yml
---
- name: Provision web server
hosts: webservers
become: yes
vars:
app_name: production
php_version: "8.3"
roles:
- role: geerlingguy.nginx
- role: geerlingguy.php
vars:
php_packages:
- "php{{ php_version }}-fpm"
- "php{{ php_version }}-pgsql"
- role: webserver
- name: Configure database
hosts: dbservers
become: yes
roles:
- role: geerlingguy.postgresql
vars:
postgresql_version: "16"
Practical Example - Provisioning an Nginx + PHP + PostgreSQL Server#
Below is a complete playbook for provisioning a production server with a full technology stack:
# provision-webserver.yml
---
- name: Provision production server
hosts: webservers
become: yes
vars:
app_name: myapplication
app_user: deploy
app_root: "/var/www/{{ app_name }}"
php_version: "8.3"
node_version: "20"
postgresql_version: "16"
db_name: "{{ app_name }}_production"
db_user: "{{ app_name }}_user"
db_password: "{{ vault_db_password }}"
server_name: app.example.com
tasks:
# === BASE CONFIGURATION ===
- name: Update system packages
apt:
upgrade: dist
update_cache: yes
cache_valid_time: 3600
- name: Install essential packages
apt:
name:
- curl
- git
- unzip
- ufw
- fail2ban
- htop
- supervisor
state: present
- name: Create application user
user:
name: "{{ app_user }}"
shell: /bin/bash
groups: www-data
append: yes
create_home: yes
# === FIREWALL ===
- name: Configure UFW - default policy
ufw:
direction: incoming
policy: deny
- name: UFW - allow SSH
ufw:
rule: allow
port: "22"
proto: tcp
- name: UFW - allow HTTP/HTTPS
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
- name: Enable UFW
ufw:
state: enabled
# === NGINX ===
- name: Install Nginx
apt:
name: nginx
state: present
- name: Deploy Nginx configuration
template:
src: templates/nginx-vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ app_name }}"
notify: Restart Nginx
- name: Enable site
file:
src: "/etc/nginx/sites-available/{{ app_name }}"
dest: "/etc/nginx/sites-enabled/{{ app_name }}"
state: link
notify: Restart Nginx
# === PHP ===
- name: Add PHP repository
apt_repository:
repo: "ppa:ondrej/php"
state: present
- name: Install PHP and extensions
apt:
name:
- "php{{ php_version }}-fpm"
- "php{{ php_version }}-cli"
- "php{{ php_version }}-pgsql"
- "php{{ php_version }}-mbstring"
- "php{{ php_version }}-xml"
- "php{{ php_version }}-curl"
- "php{{ php_version }}-zip"
- "php{{ php_version }}-gd"
- "php{{ php_version }}-intl"
- "php{{ php_version }}-redis"
state: present
update_cache: yes
- name: Configure PHP-FPM pool
template:
src: templates/php-fpm-pool.conf.j2
dest: "/etc/php/{{ php_version }}/fpm/pool.d/{{ app_name }}.conf"
notify: Restart PHP-FPM
# === POSTGRESQL ===
- name: Install PostgreSQL
apt:
name:
- "postgresql-{{ postgresql_version }}"
- "postgresql-client-{{ postgresql_version }}"
- python3-psycopg2
state: present
- name: Start PostgreSQL
service:
name: postgresql
state: started
enabled: yes
- name: Create database
become_user: postgres
community.postgresql.postgresql_db:
name: "{{ db_name }}"
encoding: UTF-8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
- name: Create database user
become_user: postgres
community.postgresql.postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
db: "{{ db_name }}"
priv: ALL
state: present
# === APPLICATION DIRECTORY ===
- name: Create application directory
file:
path: "{{ app_root }}"
state: directory
owner: "{{ app_user }}"
group: www-data
mode: "0755"
- name: Create storage directories
file:
path: "{{ app_root }}/{{ item }}"
state: directory
owner: "{{ app_user }}"
group: www-data
mode: "0775"
loop:
- storage
- storage/logs
- storage/cache
- storage/uploads
# === SSL (Certbot) ===
- name: Install Certbot
apt:
name:
- certbot
- python3-certbot-nginx
state: present
- name: Obtain SSL certificate
command: >
certbot certonly --nginx
-d {{ server_name }}
--non-interactive
--agree-tos
--email admin@example.com
args:
creates: "/etc/letsencrypt/live/{{ server_name }}/fullchain.pem"
handlers:
- name: Restart Nginx
service:
name: nginx
state: restarted
- name: Restart PHP-FPM
service:
name: "php{{ php_version }}-fpm"
state: restarted
Deploying Applications with Ansible#
Ansible is excellent for automating the application deployment process:
# deploy.yml
---
- name: Deploy application
hosts: webservers
become: yes
become_user: deploy
vars:
app_name: myapplication
app_root: /var/www/myapplication
repo_url: "git@github.com:company/myapplication.git"
branch: main
release_dir: "{{ app_root }}/releases/{{ ansible_date_time.epoch }}"
shared_dir: "{{ app_root }}/shared"
current_link: "{{ app_root }}/current"
tasks:
- name: Create release directory
file:
path: "{{ release_dir }}"
state: directory
- name: Clone repository
git:
repo: "{{ repo_url }}"
dest: "{{ release_dir }}"
version: "{{ branch }}"
depth: 1
accept_hostkey: yes
- name: Install Composer dependencies
composer:
command: install
working_dir: "{{ release_dir }}"
no_dev: yes
optimize_autoloader: yes
- name: Link shared directories
file:
src: "{{ shared_dir }}/{{ item }}"
dest: "{{ release_dir }}/{{ item }}"
state: link
force: yes
loop:
- .env
- storage
- node_modules
- name: Run database migrations
command: php artisan migrate --force
args:
chdir: "{{ release_dir }}"
- name: Build front-end assets
command: npm run build
args:
chdir: "{{ release_dir }}"
- name: Clear and rebuild caches
command: "php artisan {{ item }}"
args:
chdir: "{{ release_dir }}"
loop:
- cache:clear
- config:cache
- route:cache
- view:cache
- name: Switch symlink to new release
file:
src: "{{ release_dir }}"
dest: "{{ current_link }}"
state: link
force: yes
- name: Restart PHP-FPM
become_user: root
service:
name: "php8.3-fpm"
state: restarted
- name: Clean up old releases (keep last 5)
shell: |
ls -dt {{ app_root }}/releases/*/ | tail -n +6 | xargs rm -rf
args:
warn: false
changed_when: false
Idempotency - A Key Feature of Ansible#
Idempotency means that running the same playbook multiple times produces identical results. Ansible checks the current state of the system and only makes changes that are necessary:
# This task is idempotent - if the package is already installed,
# Ansible will take no action
- name: Install Nginx
apt:
name: nginx
state: present
# Result: "ok" if already installed, "changed" if installed now
# This task is NOT idempotent - it will always execute
- name: Restart application (bad example)
command: systemctl restart nginx
# Result: always "changed"
# Better approach - idempotent
- name: Ensure Nginx is running
service:
name: nginx
state: started
# Result: "ok" if already running, "changed" if started now
Thanks to idempotency, you can safely run playbooks multiple times without worrying about unwanted side effects. This is a key difference between Ansible and traditional bash scripts.
Ansible vs Puppet vs Chef#
| Feature | Ansible | Puppet | Chef | |---------|---------|--------|------| | Architecture | Agentless (SSH) | Agent on every host | Agent on every host | | Configuration language | YAML | Puppet DSL | Ruby DSL | | Learning curve | Gentle | Moderate | Steep | | Idempotency | Yes | Yes | Yes | | Push vs Pull | Push (default) | Pull | Pull | | Community | Very large | Large | Medium | | Best for | Deployments & orchestration | Configuration management | Complex environments |
Ansible wins with its simplicity and low barrier to entry. It does not require learning a dedicated programming language, and YAML playbooks are readable by any team member. Puppet and Chef may be a better choice in very large, static environments where the pull model is more appropriate.
Ansible Tower / AWX#
Ansible Tower (commercial) and AWX (open-source) are web-based platforms for managing Ansible at an organizational scale:
# Example job template in Tower/AWX
# Defined via API or web interface:
#
# - Name: "Deploy Production"
# - Playbook: deploy.yml
# - Inventory: Production
# - Credentials: SSH Key + Vault Password
# - Schedule: On-demand
# - Notifications: Slack #deployments
# - RBAC: DevOps team only
Key Tower/AWX features:
- Web interface - visual management of playbooks and inventories
- Access control (RBAC) - granular permissions for teams
- Scheduling - automatic playbook execution on schedules
- Notifications - integration with Slack, email, webhooks
- REST API - full automation through the API
- Audit trail - complete log of who ran what and when
CI/CD Integration#
Ansible integrates seamlessly with popular CI/CD tools:
# .github/workflows/deploy.yml - GitHub Actions
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip install ansible
- name: Configure SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: Run deploy playbook
run: |
ansible-playbook \
-i "${{ secrets.SERVER_IP }}," \
--user deploy \
--extra-vars "branch=${{ github.sha }}" \
deploy.yml
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
# .gitlab-ci.yml - GitLab CI
stages:
- test
- deploy
deploy_production:
stage: deploy
image: python:3.11
only:
- main
before_script:
- pip install ansible
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- ansible-playbook -i inventory/production deploy.yml
environment:
name: production
url: https://app.example.com
Best Practices#
To wrap up, here are some proven best practices for working with Ansible:
- Use Ansible Vault to store passwords and secrets:
ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-playbook site.yml --ask-vault-pass
- Structure your project according to official recommendations:
project/
inventory/
production/
staging/
group_vars/
host_vars/
roles/
playbooks/
ansible.cfg
requirements.yml
- Test playbooks with Molecule and ansible-lint:
pip install molecule ansible-lint
ansible-lint site.yml
molecule test
- Use tags for selective task execution:
- name: Install Nginx
apt:
name: nginx
state: present
tags: [nginx, install]
-
Use environment variables instead of hardcoded values.
-
Write descriptive task names - every task should have a clear, descriptive name.
Summary#
Ansible is an indispensable tool in the arsenal of every DevOps team. Thanks to its simple YAML syntax, agentless architecture, and vast module library, it enables fast and reliable infrastructure automation. Whether you manage a single server or hundreds of machines, Ansible will help you save time and eliminate errors caused by manual configuration.
Key takeaways:
- Simplicity - YAML is readable by everyone
- Agentless - no additional software on servers
- Idempotency - safe to run multiple times
- Scalability - from one server to thousands of machines
- Community - massive library of roles and collections in Galaxy
Need Help with Infrastructure Automation?#
At MDS Software Solutions Group, we specialize in infrastructure automation and DevOps processes. We offer:
- Designing and implementing automation with Ansible
- CI/CD pipeline configuration
- Audit and optimization of existing infrastructure
- Cloud migration (Azure, AWS, GCP)
- Ansible and DevOps best practices training
- 24/7 infrastructure support and maintenance
Contact us to discuss automating your infrastructure!
Team of programming experts specializing in modern web technologies.