Przejdź do treści
DevOps

Ansible - Server Configuration Automation

Published on:
·6 min read·Author: MDS Software Solutions Group

Ansible Server Configuration

devops

Ansible - 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:

  1. 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
  1. Structure your project according to official recommendations:
project/
  inventory/
    production/
    staging/
  group_vars/
  host_vars/
  roles/
  playbooks/
  ansible.cfg
  requirements.yml
  1. Test playbooks with Molecule and ansible-lint:
pip install molecule ansible-lint
ansible-lint site.yml
molecule test
  1. Use tags for selective task execution:
- name: Install Nginx
  apt:
    name: nginx
    state: present
  tags: [nginx, install]
  1. Use environment variables instead of hardcoded values.

  2. 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!

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Ansible - Server Configuration Automation | MDS Software Solutions Group | MDS Software Solutions Group