Database automation with Ansible
Symptom
Goal: Make database installation, configuration, user and backup tasks repeatable and idempotent (re-running the same playbook does not break the system) with Ansible.
1. Define the inventory and groups
Group servers in the inventory by database role; put primary, replica and backup servers in separate groups. Use a static INI/YAML inventory or a dynamic inventory for the cloud (aws_ec2, gcp_compute). Put group variables under group_vars/ and host-specific ones under host_vars/.
PostgreSQL
[pg_primary]
db1.example.local
[pg_replica]
db2.example.local
[postgres:children]
pg_primary
pg_replica2. Install the right collection (community.postgresql / community.mysql / community.mongodb)
Ansible core now ships database modules in separate collections; install the collection for your engine via requirements.yml. Use community.postgresql (postgresql_db, postgresql_user, postgresql_privs) for PostgreSQL, community.mysql (mysql_db, mysql_user) for MySQL/MariaDB and community.mongodb for MongoDB. The corresponding Python client (psycopg2, PyMySQL, pymongo) must be installed on the target host.
PostgreSQL
# requirements.yml
collections:
- name: community.postgresql
# install:
ansible-galaxy collection install -r requirements.ymlMySQL
collections:
- name: community.mysqlMongoDB
collections:
- name: community.mongodb3. Encrypt secrets with Ansible Vault
Do not leave database passwords, replication secrets and superuser credentials in plain text; keep them in an encrypted variable file with ansible-vault. Decrypt at run time with --ask-vault-pass or --vault-password-file. Secrets can then be stored safely in Git and the playbook stays idempotent.
PostgreSQL
ansible-vault create group_vars/postgres/vault.yml
# contents:
vault_pg_admin_password: "S3cret!"
# run:
ansible-playbook site.yml --ask-vault-pass4. Set up the role structure (roles/postgres, roles/mysql)
Create one role per engine and split the logic into tasks/, defaults/, templates/ and handlers/ subfolders. This structure improves reusability: defaults/main.yml for default values, templates/ for jinja2 configuration templates, handlers/ for 'restart only if something changed' logic. You can scaffold the role with ansible-galaxy init roles/postgres.
PostgreSQL
ansible-galaxy init roles/postgres
# roles/postgres/
# defaults/main.yml
# tasks/main.yml
# templates/postgresql.conf.j2
# handlers/main.yml5. Write installation and configuration tasks idempotently
Install the package, initialize the data directory and render postgresql.conf/my.cnf from a template; restart the service via a handler when the configuration changes. Modules are idempotent: an already installed package is not reinstalled, and the handler is not triggered if the file did not change. Render the configuration template with the template module and link it to the handler with 'notify'.
PostgreSQL
- name: postgresql.conf template
ansible.builtin.template:
src: postgresql.conf.j2
dest: /etc/postgresql/16/main/postgresql.conf
notify: restart postgresql
# handlers/main.yml
- name: restart postgresql
ansible.builtin.service:
name: postgresql
state: restartedMySQL
- name: my.cnf template
ansible.builtin.template:
src: my.cnf.j2
dest: /etc/mysql/my.cnf
notify: restart mysql6. Define database and user tasks
Create the database, users and privileges with modules; take the password from a Vault variable. The community.postgresql modules are idempotent: if the user/DB exists they do not recreate it, they apply only the difference. Grant privileges separately with postgresql_privs; in MySQL the priv field of mysql_user is used.
PostgreSQL
- name: application database
community.postgresql.postgresql_db:
name: appdb
state: present
- name: application user
community.postgresql.postgresql_user:
name: app_user
password: "{{ vault_app_password }}"
db: appdb
state: presentMySQL
- name: application user
community.mysql.mysql_user:
name: app_user
password: "{{ vault_app_password }}"
priv: 'appdb.*:ALL'
state: present7. Add the backup task and schedule
Deploy the backup script from a template and schedule it with ansible.builtin.cron; the backup job is then also managed as code. Make the backup target directory, retention period and schedule variables. The cron module is idempotent and identifies the entry by 'name', so re-running does not duplicate it.
PostgreSQL
- name: nightly backup (cron)
ansible.builtin.cron:
name: "pg dump nightly"
minute: "0"
hour: "2"
job: "pg_dump -Fc appdb > /backup/appdb_$(date +\\%F).dump"