AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible for PostgreSQL and MySQL: Database Automation Complete Guide

By Luca Berton · Published 2024-01-01 · Category: installation

Automate PostgreSQL and MySQL with Ansible. Create databases and users, manage permissions, configure replication, automate backups, tune performance.

Why Ansible for Database Automation?

Database setup and maintenance is repetitive, error-prone, and often done manually. Ansible automates the entire database lifecycle: installation, configuration, user/permission management, replication setup, backups, and performance tuning — all version-controlled and repeatable.

See also: Ansible for Database Automation: PostgreSQL, MySQL, MongoDB, and Redis

Collections

# PostgreSQL
ansible-galaxy collection install community.postgresql

# MySQL / MariaDB
ansible-galaxy collection install community.mysql

# Dependencies on control node
pip install psycopg2-binary  # PostgreSQL
pip install PyMySQL           # MySQL/MariaDB

PostgreSQL

Install PostgreSQL

---
- name: Install and configure PostgreSQL
  hosts: db_servers
  become: true

  vars:
    pg_version: 16
    pg_data_dir: "/var/lib/postgresql/{{ pg_version }}/main"
    pg_conf_dir: "/etc/postgresql/{{ pg_version }}/main"

  tasks:
    - name: Install PostgreSQL
      ansible.builtin.apt:
        name:
          - "postgresql-{{ pg_version }}"
          - "postgresql-client-{{ pg_version }}"
          - "postgresql-contrib-{{ pg_version }}"
          - python3-psycopg2
        state: present
        update_cache: true

    - name: Start PostgreSQL
      ansible.builtin.systemd:
        name: postgresql
        state: started
        enabled: true

Configure postgresql.conf

    - name: Configure PostgreSQL
      ansible.builtin.lineinfile:
        path: "{{ pg_conf_dir }}/postgresql.conf"
        regexp: "^#?{{ item.key }} ="
        line: "{{ item.key }} = {{ item.value }}"
      loop:
        - { key: listen_addresses, value: "'*'" }
        - { key: max_connections, value: "200" }
        - { key: shared_buffers, value: "'4GB'" }
        - { key: effective_cache_size, value: "'12GB'" }
        - { key: work_mem, value: "'64MB'" }
        - { key: maintenance_work_mem, value: "'1GB'" }
        - { key: wal_level, value: "'replica'" }
        - { key: max_wal_senders, value: "5" }
        - { key: max_replication_slots, value: "5" }
        - { key: log_min_duration_statement, value: "1000" }
        - { key: log_checkpoints, value: "on" }
        - { key: log_connections, value: "on" }
        - { key: log_disconnections, value: "on" }
      notify: restart postgresql

    - name: Configure pg_hba.conf
      community.postgresql.postgresql_pg_hba:
        dest: "{{ pg_conf_dir }}/pg_hba.conf"
        contype: host
        users: "{{ item.user }}"
        source: "{{ item.source }}"
        databases: "{{ item.db }}"
        method: scram-sha-256
      loop:
        - { user: app_user, source: "10.0.0.0/24", db: app_db }
        - { user: readonly, source: "10.0.0.0/24", db: app_db }
        - { user: replication, source: "10.0.1.0/24", db: replication }
      notify: reload postgresql

  handlers:
    - name: restart postgresql
      ansible.builtin.systemd:
        name: postgresql
        state: restarted

    - name: reload postgresql
      ansible.builtin.systemd:
        name: postgresql
        state: reloaded

Create Databases and Users

    - name: Create application database
      community.postgresql.postgresql_db:
        name: app_db
        encoding: UTF-8
        lc_collate: en_US.UTF-8
        lc_ctype: en_US.UTF-8
        template: template0
        state: present
      become_user: postgres

    - name: Create application user
      community.postgresql.postgresql_user:
        name: app_user
        password: "{{ vault_pg_app_password }}"
        encrypted: true
        state: present
      become_user: postgres
      no_log: true

    - name: Grant privileges
      community.postgresql.postgresql_privs:
        database: app_db
        privs: ALL
        type: database
        role: app_user
        state: present
      become_user: postgres

    - name: Grant schema privileges
      community.postgresql.postgresql_privs:
        database: app_db
        privs: ALL
        type: schema
        objs: public
        role: app_user
        state: present
      become_user: postgres

    - name: Create read-only user
      community.postgresql.postgresql_user:
        name: readonly
        password: "{{ vault_pg_readonly_password }}"
        state: present
      become_user: postgres
      no_log: true

    - name: Grant read-only access
      community.postgresql.postgresql_privs:
        database: app_db
        privs: SELECT
        type: table
        objs: ALL_IN_SCHEMA
        schema: public
        role: readonly
        state: present
      become_user: postgres

Extensions

    - name: Install extensions
      community.postgresql.postgresql_ext:
        name: "{{ item }}"
        db: app_db
        state: present
      loop:
        - pg_stat_statements
        - pgcrypto
        - uuid-ossp
        - pg_trgm
      become_user: postgres

Backup Automation

---
- name: PostgreSQL backup
  hosts: db_servers
  become: true
  become_user: postgres

  vars:
    backup_dir: /backups/postgresql
    retention_days: 30

  tasks:
    - name: Create backup directory
      ansible.builtin.file:
        path: "{{ backup_dir }}"
        state: directory
        mode: '0700'
        owner: postgres

    - name: Full database dump
      community.postgresql.postgresql_db:
        name: app_db
        state: dump
        target: "{{ backup_dir }}/app_db_{{ ansible_date_time.date }}.sql.gz"

    - name: Clean old backups
      ansible.builtin.find:
        paths: "{{ backup_dir }}"
        patterns: "*.sql.gz"
        age: "{{ retention_days }}d"
      register: old_backups

    - name: Remove old backups
      ansible.builtin.file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ old_backups.files }}"

    - name: Deploy backup cron job
      ansible.builtin.cron:
        name: "PostgreSQL daily backup"
        minute: "0"
        hour: "2"
        job: "pg_dump -Fc app_db | gzip > {{ backup_dir }}/app_db_$(date +\\%Y-\\%m-\\%d).sql.gz"
        user: postgres

Streaming Replication

---
- name: Configure PostgreSQL primary
  hosts: pg_primary
  become: true
  tasks:
    - name: Create replication user
      community.postgresql.postgresql_user:
        name: replication
        password: "{{ vault_replication_password }}"
        role_attr_flags: REPLICATION,LOGIN
      become_user: postgres

    - name: Create replication slot
      community.postgresql.postgresql_slot:
        name: replica_slot_1
        slot_type: physical
        state: present
      become_user: postgres

---
- name: Configure PostgreSQL replica
  hosts: pg_replica
  become: true
  tasks:
    - name: Stop PostgreSQL
      ansible.builtin.systemd:
        name: postgresql
        state: stopped

    - name: Remove existing data
      ansible.builtin.file:
        path: "{{ pg_data_dir }}"
        state: absent

    - name: Base backup from primary
      ansible.builtin.command:
        cmd: >
          pg_basebackup
          -h {{ hostvars['pg_primary']['ansible_host'] }}
          -U replication
          -D {{ pg_data_dir }}
          -Fp -Xs -P -R
          --slot=replica_slot_1
      become_user: postgres
      environment:
        PGPASSWORD: "{{ vault_replication_password }}"

    - name: Start PostgreSQL replica
      ansible.builtin.systemd:
        name: postgresql
        state: started

See also: ansible.mysql 5.0.0 Release: Migrate from community.mysql Collection

MySQL / MariaDB

Install MySQL

---
- name: Install MySQL 8
  hosts: mysql_servers
  become: true

  tasks:
    - name: Install MySQL
      ansible.builtin.apt:
        name:
          - mysql-server-8.0
          - mysql-client-8.0
          - python3-pymysql
        state: present
        update_cache: true

    - name: Start MySQL
      ansible.builtin.systemd:
        name: mysql
        state: started
        enabled: true

    - name: Set root password
      community.mysql.mysql_user:
        name: root
        password: "{{ vault_mysql_root_password }}"
        host: localhost
        login_unix_socket: /var/run/mysqld/mysqld.sock
        state: present
      no_log: true

    - name: Create .my.cnf for root
      ansible.builtin.template:
        src: my.cnf.j2
        dest: /root/.my.cnf
        mode: '0600'

Create Databases and Users

    - name: Create application database
      community.mysql.mysql_db:
        name: app_db
        encoding: utf8mb4
        collation: utf8mb4_unicode_ci
        state: present
        login_unix_socket: /var/run/mysqld/mysqld.sock

    - name: Create application user
      community.mysql.mysql_user:
        name: app_user
        password: "{{ vault_mysql_app_password }}"
        host: "10.0.0.%"
        priv: "app_db.*:SELECT,INSERT,UPDATE,DELETE,CREATE,ALTER,INDEX,DROP"
        state: present
        login_unix_socket: /var/run/mysqld/mysqld.sock
      no_log: true

    - name: Create read-only user
      community.mysql.mysql_user:
        name: readonly
        password: "{{ vault_mysql_readonly_password }}"
        host: "10.0.0.%"
        priv: "app_db.*:SELECT"
        state: present
        login_unix_socket: /var/run/mysqld/mysqld.sock
      no_log: true

    - name: Create backup user
      community.mysql.mysql_user:
        name: backup
        password: "{{ vault_mysql_backup_password }}"
        host: localhost
        priv: "*.*:SELECT,SHOW VIEW,TRIGGER,LOCK TABLES,PROCESS,RELOAD"
        state: present
        login_unix_socket: /var/run/mysqld/mysqld.sock
      no_log: true

Configure MySQL

    - name: Deploy MySQL configuration
      ansible.builtin.template:
        src: mysqld.cnf.j2
        dest: /etc/mysql/mysql.conf.d/mysqld.cnf
        mode: '0644'
      notify: restart mysql

    # templates/mysqld.cnf.j2
    # [mysqld]
    # bind-address = 0.0.0.0
    # max_connections = 200
    # innodb_buffer_pool_size = 4G
    # innodb_log_file_size = 1G
    # innodb_flush_log_at_trx_commit = 1
    # slow_query_log = 1
    # slow_query_log_file = /var/log/mysql/slow.log
    # long_query_time = 2
    # log_error = /var/log/mysql/error.log
    # server-id = {{ mysql_server_id }}
    # log_bin = /var/log/mysql/mysql-bin
    # binlog_format = ROW
    # expire_logs_days = 7

  handlers:
    - name: restart mysql
      ansible.builtin.systemd:
        name: mysql
        state: restarted

Run SQL Queries

    - name: Run schema migration
      community.mysql.mysql_query:
        login_db: app_db
        query:
          - CREATE TABLE IF NOT EXISTS users (
              id BIGINT AUTO_INCREMENT PRIMARY KEY,
              email VARCHAR(255) NOT NULL UNIQUE,
              name VARCHAR(255),
              created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            ) ENGINE=InnoDB;
          - CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
        login_unix_socket: /var/run/mysqld/mysqld.sock

    - name: Import SQL file
      community.mysql.mysql_db:
        name: app_db
        state: import
        target: /tmp/schema.sql
        login_unix_socket: /var/run/mysqld/mysqld.sock

MySQL Backup

    - name: Backup MySQL database
      community.mysql.mysql_db:
        name: app_db
        state: dump
        target: "/backups/mysql/app_db_{{ ansible_date_time.date }}.sql.gz"
        login_unix_socket: /var/run/mysqld/mysqld.sock

    - name: Deploy backup script
      ansible.builtin.copy:
        content: |
          #!/bin/bash
          BACKUP_DIR=/backups/mysql
          DATE=$(date +%Y-%m-%d)
          mysqldump --single-transaction --routines --triggers app_db | gzip > ${BACKUP_DIR}/app_db_${DATE}.sql.gz
          find ${BACKUP_DIR} -name "*.sql.gz" -mtime +30 -delete
        dest: /usr/local/bin/mysql-backup.sh
        mode: '0755'

    - name: Schedule backup
      ansible.builtin.cron:
        name: "MySQL daily backup"
        minute: "0"
        hour: "3"
        job: /usr/local/bin/mysql-backup.sh

Database Security Hardening

---
- name: Harden database security
  hosts: db_servers
  become: true
  tasks:
    # PostgreSQL
    - name: Remove default postgres trust auth
      community.postgresql.postgresql_pg_hba:
        dest: /etc/postgresql/16/main/pg_hba.conf
        contype: local
        users: all
        databases: all
        method: scram-sha-256
      when: "'postgresql' in group_names"

    # MySQL — remove anonymous users and test database
    - name: Remove anonymous MySQL users
      community.mysql.mysql_user:
        name: ""
        host_all: true
        state: absent
        login_unix_socket: /var/run/mysqld/mysqld.sock
      when: "'mysql' in group_names"

    - name: Remove test database
      community.mysql.mysql_db:
        name: test
        state: absent
        login_unix_socket: /var/run/mysqld/mysqld.sock
      when: "'mysql' in group_names"

    - name: Disallow root remote login
      community.mysql.mysql_user:
        name: root
        host: "%"
        state: absent
        login_unix_socket: /var/run/mysqld/mysqld.sock
      when: "'mysql' in group_names"

See also: Ansible for PostgreSQL: PGMeetUp Bari 2022 Highlights

FAQ

Should I use become_user: postgres or login_user?

For PostgreSQL, become_user: postgres uses Unix socket authentication (peer auth), which is the default and most secure for local connections. Use login_user/login_password for remote connections or when peer auth isn't configured.

How do I handle database migrations with Ansible?

For simple schema changes, use community.postgresql.postgresql_query or community.mysql.mysql_query. For complex applications, use a dedicated migration tool (Alembic, Flyway, Liquibase) and call it from Ansible with ansible.builtin.command. Ansible handles infrastructure, the migration tool handles schema versioning.

Can Ansible manage RDS / Cloud SQL?

Yes. Use the same community.postgresql and community.mysql modules with login_host, login_user, and login_password pointed at your cloud database endpoint. For provisioning the database instance itself, use amazon.aws.rds_instance, azure.azcollection.azure_rm_postgresqlserver, or google.cloud.gcp_sql_instance.

How do I encrypt database passwords in Ansible?

Use Ansible Vault: ansible-vault encrypt_string 'my-password' --name vault_pg_password. Store encrypted values in group_vars/db_servers/vault.yml and reference them as {{ vault_pg_password }}. Always use no_log: true on tasks that handle passwords.

Conclusion

Ansible automates the complete database lifecycle — PostgreSQL and MySQL installation, configuration tuning, user and permission management, replication setup, backup automation, and security hardening. Use community.postgresql and community.mysql collections for idempotent database management. Combine with Ansible Vault for secrets, scheduled backups via cron, and streaming replication for high availability.

See also

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home