Otomatisasi pengelolaan database produksi: pembuatan database dan user di PostgreSQL, MySQL/MariaDB, dan MongoDB, manajemen privilege, backup otomatis, hingga konfigurasi replication dan connection pooling dengan Ansible Vault.

Setelah di episode 23 sebelumnya kita membahas Kubernetes & Container Orchestration — mengelola container dan cluster dengan collection kubernetes.core dan community.docker — pada episode kali ini kita akan turun ke lapisan yang paling sensitif dari sebuah sistem: database.
Coba kita analogikan begini. Container itu seperti dapur modern di restoran: kalau satu kompor atau panci rusak, kita tinggal ganti dengan yang baru dalam hitungan menit. Tapi database adalah gudang arsip tempat seluruh catatan bisnis disimpan — pelanggan, transaksi, stok, laporan. Kalau gudang ini dikelola sembarangan, rusak, atau datanya hilang, restoran itu bisa gulung tikar. Container yang rusak bisa di-deploy ulang; data yang hilang belum tentu bisa dikembalikan.
Masalahnya, di banyak tim, pengelolaan database masih dilakukan dengan cara yang sangat manual dan rawan: seorang engineer SSH ke server, mengetik psql atau mysql, lalu menjalankan perintah SQL satu per satu tanpa dokumentasi. Repetitif, lambat, dan error-prone. Lebih parah lagi, SQL bersifat imperative dan sering tidak idempotent — menjalankan CREATE DATABASE dua kali justru melempar error, bukan menyelesaikan dengan rapi.
Di sinilah Ansible berperan. Lewat collection database resmi komunitas, kita bisa membuat database dan user, mengelola privilege, menjalankan backup otomatis, hingga mengonfigurasi replication secara deklaratif dan idempotent — semuanya dalam YAML yang sama dengan yang kalian kenal sejak episode 1. Pada episode ini kita akan membahas collection community.postgresql, community.mysql, dan community.mongodb, pola deployment seperti master-slave replication dan connection pooling, serta best practice menyimpan kredensial database menggunakan Ansible Vault (episode 14).
Tiga collection berikut adalah tulang punggung automasi database di ekosistem Ansible:
| Collection | Database | Modul Utama |
|---|---|---|
community.postgresql | PostgreSQL | postgresql_db, postgresql_user, postgresql_privs, postgresql_pg_dump, postgresql_primary, postgresql_standby |
community.mysql | MySQL / MariaDB | mysql_db, mysql_user, mysql_query, mysql_info |
community.mongodb | MongoDB | mongodb_user, mongodb_replicaset, mongodb_shell, mongodb_info |
Yang menarik dari modul-modul ini: mereka berbicara langsung ke server database (TCP) dari control node, bukan lewat SSH. Artinya, kalian tidak perlu agent di server database, cukup pastikan port database bisa diakses dari control node — persis filosofi agentless Ansible yang kita bahas di episode 1.
Install ketiga collection tersebut beserta dependensinya:
ansible-galaxy collection install community.postgresql community.mysql community.mongodb --with-depsNote
Modul-modul database membutuhkan library Python di sisi control node, misalnya psycopg2 untuk PostgreSQL, pymysql/PyMySQL untuk MySQL, dan pymongo untuk MongoDB. Gunakan virtual environment atau pip install sesuai dokumentasi masing-masing collection jika menemui error seperti ModuleNotFoundError: No module named 'psycopg2'.
Sebelum menulis playbook, satu hal yang wajib kalian lakukan adalah jangan pernah menulis password database di dalam playbook atau di Git. Kita sudah membahas konsep Ansible Vault secara detail di episode 14, jadi di sini kita langsung mempraktikkannya. Simpan semua kredensial dalam satu file variabel terenkripsi:
ansible-vault create group_vars/databases/vault.ymlIsi file tersebut dengan kredensial setiap database yang akan kita kelola:
---
vault_postgres_password: "S3cure-R0ot#Pass"
vault_db_user_password: "App-Us3r#Pass"
vault_mysql_root_password: "My5ql-R00t#Pass"
vault_mongo_admin_password: "M0ng0-Adm1n#Pass"
vault_mongo_user_password: "M0ng0-App#Pass"Saat menjalankan playbook, tambahkan flag --ask-vault-pass (atau gunakan --vault-password-file untuk CI/CD). Jangan lupa task yang menyentuh kredensial diberi no_log: true agar password tidak bocor ke output playbook.
Mari mulai dari skenario paling umum: membuat database ecommerce beserta user aplikasi yang punya akses ke database tersebut. Karena seluruh operasi berjalan lewat TCP, kita perlu memberikan login_user dan login_password (admin) kepada setiap modul:
- name: Kelola database dan user PostgreSQL
hosts: postgresql_servers
become: true
vars:
pg_login_user: postgres
db_name: ecommerce
db_user: app_ecommerce
db_owner: app_ecommerce
tasks:
- name: Buat database ecommerce
community.postgresql.postgresql_db:
name: "{{ db_name }}"
owner: "{{ db_owner }}"
encoding: UTF8
login_user: "{{ pg_login_user }}"
login_password: "{{ vault_postgres_password }}"
no_log: true
- name: Buat user aplikasi ecommerce
community.postgresql.postgresql_user:
name: "{{ db_user }}"
password: "{{ vault_db_user_password }}"
login_user: "{{ pg_login_user }}"
login_password: "{{ vault_postgres_password }}"
no_log: truePerhatikan beberapa hal penting:
postgresql_db dengan state: present (default) akan membuat database jika belum ada, dan tidak melakukan apa pun jika sudah ada — tidak seperti CREATE DATABASE manual yang error ketika database sudah terlanjur ada.no_log: true: memastikan password tidak tercetak di log, sekaligus meredam secret dari mode verbose.owner: mendeklarasikan pemilik database. Menetapkan pemilik yang tepat sejak awal menghindari operasi ALTER DATABASE di kemudian hari.Tip
Jika playbook dijalankan langsung dari server PostgreSQL itu sendiri (bukan dari control node lain), kalian bisa memakai login_unix_socket atau cukup menghilangkan login_password sehingga modul memakai autentikasi peer sebagai user lokal (misalnya user postgres). Ini lebih aman daripada membuka password admin di konfigurasi yang tidak perlu.
Membuat user saja tidak cukup — user aplikasi harus diberikan hak akses yang paling kecil yang memungkinkan agar aplikasi tetap berjalan. Untuk PostgreSQL, modul yang tepat adalah postgresql_privs:
- name: Kelola privilege PostgreSQL
hosts: postgresql_servers
become: true
tasks:
- name: Berikan akses schema kepada user aplikasi
community.postgresql.postgresql_privs:
database: ecommerce
roles: app_ecommerce
type: schema
objs: public
privs: USAGE
login_user: postgres
login_password: "{{ vault_postgres_password }}"
- name: Berikan akses tabel di schema public
community.postgresql.postgresql_privs:
database: ecommerce
roles: app_ecommerce
type: table
objs: "public.*"
privs: SELECT,INSERT,UPDATE,DELETE
login_user: postgres
login_password: "{{ vault_postgres_password }}"Mengapa kita repot-repot memisahkan privilege? Karena di dunia nyata, breach paling sering dimulai dari kredensial yang memiliki akses terlalu luas. Jika user aplikasi adalah superuser dan kredensialnya bocor, penyerang bisa menghapus seluruh database. Dengan privilege minimal, dampak kebocoran bisa dibatasi. Pola yang umum dipakai:
postgres) — hanya dipakai saat provisioning dan migrasi, tidak untuk aplikasi.Untuk MySQL/MariaDB, pola-nya hampir sama dengan community.mysql. Perhatikan bahwa user di MySQL diidentifikasi oleh kombinasi name dan host, dan privilege diberikan langsung melalui parameter priv:
- name: Kelola database dan user MySQL
hosts: mysql_servers
become: true
vars:
mysql_login_user: root
db_name: ecommerce
db_user: app_ecommerce
tasks:
- name: Buat database ecommerce
community.mysql.mysql_db:
name: "{{ db_name }}"
encoding: utf8mb4
login_user: "{{ mysql_login_user }}"
login_password: "{{ vault_mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: Buat user aplikasi dengan privilege spesifik
community.mysql.mysql_user:
name: "{{ db_user }}"
host: "10.0.0.%"
password: "{{ vault_db_user_password }}"
priv: "ecommerce.*:SELECT,INSERT,UPDATE,DELETE"
state: present
login_user: "{{ mysql_login_user }}"
login_password: "{{ vault_mysql_root_password }}"Sintaks priv di MySQL berbentuk "database.table:PENGATURAN". Beberapa contoh:
Nilai priv | Arti |
|---|---|
ecommerce.*:ALL | Semua hak atas semua tabel di database ecommerce |
ecommerce.*:SELECT,INSERT,UPDATE,DELETE | Akses baca-tulis standar aplikasi |
*.*:ALL | Superuser (sangat berbahaya untuk user aplikasi!) |
Warning
Jika kalian menuliskan host: "%", user tersebut bisa login dari mana saja di jaringan — termasuk internet jika port MySQL terbuka. Batasi host ke subnet internal aplikasi (misalnya 10.0.0.%) dan pastikan port MySQL tidak diekspos ke publik. Di episode 27 nanti kita akan membahas hardening lebih lanjut.
MongoDB sedikit berbeda: user dikelola per authentication database (database: admin untuk user administratif), dan roles adalah daftar objek yang terdiri dari db dan role:
- name: Kelola user MongoDB
hosts: mongodb_servers
become: true
vars:
mongo_host: 127.0.0.1
db_name: ecommerce
db_user: app_ecommerce
tasks:
- name: Buat user aplikasi di MongoDB
community.mongodb.mongodb_user:
login_host: "{{ mongo_host }}"
login_user: "{{ mongo_admin_user }}"
login_password: "{{ vault_mongo_admin_password }}"
database: admin
name: "{{ db_user }}"
password: "{{ vault_mongo_user_password }}"
roles:
- db: "{{ db_name }}"
role: readWrite
state: presentPerhatikan bahwa modul mongodb_user memerlukan login_user/login_password dari user administratif (di authentication database admin) untuk membuat user lain. MongoDB adalah document store, jadi "schema migration" di sini lebih sering berupa validasi schema di level aplikasi atau koleksi dengan validator — perbedaan konseptual yang perlu kalian ingat.
Backup adalah salah satu tugas yang paling sering "dilupakan" sampai terjadi bencana. Dengan Ansible, backup bisa dijadwalkan dan dijalankan secara konsisten — dan hasilnya bisa diverifikasi.
Pendekatan paling idiomatis untuk PostgreSQL adalah memanfaatkan modul postgresql_db dengan state: dump, yang mengeluarkan file SQL dump ke target:
- name: Backup otomatis database PostgreSQL
hosts: postgresql_servers
become: true
vars:
backup_dir: /var/backups/postgres
db_name: ecommerce
tasks:
- name: Siapkan direktori backup
ansible.builtin.file:
path: "{{ backup_dir }}"
state: directory
owner: postgres
group: postgres
mode: "0750"
- name: Dump database ecommerce
community.postgresql.postgresql_db:
name: "{{ db_name }}"
state: dump
target: "{{ backup_dir }}/ecommerce-{{ ansible_date_time.date }}.sql"
login_user: postgres
login_password: "{{ vault_postgres_password }}"Perhatikan penggunaan {{ ansible_date_time.date }} — fact yang dikumpulkan Ansible dari server — sehingga setiap hari menghasilkan nama file baru. Jangan lupa menambahkan task retensi menggunakan modul ansible.builtin.find + ansible.builtin.file untuk menghapus backup yang lebih tua dari, misalnya, 7 hari, agar disk tidak penuh.
Banyak tim juga menggabungkan pendekatan modul dengan perintah pg_dump/mysqldump langsung via shell + register, terutama untuk kebutuhan yang belum didukung modul (misal backup dengan kompresi atau opsi tertentu). Contohnya:
- name: Dump via pg_dump dengan kompresi
ansible.builtin.shell: |
pg_dump -h 127.0.0.1 -U postgres -Fc {{ db_name }} > "{{ backup_dir }}/{{ db_name }}-{{ ansible_date_time.date }}.dump"
become_user: postgres
become: true
args:
executable: /bin/bash
changed_when: false
register: backup_resultImportant
Backup tanpa proses restore yang teruji bukanlah backup. Jadwalkan juga proses verifikasi — misalnya restore ke database staging sementara lalu cek jumlah baris tabel — sebelum menyatakan backup tersebut valid. Banyak insiden "data hilang" justru terungkap ketika tim mencoba restore dan menemukan dump-nya korup atau tidak lengkap.
Ketika database mulai dipakai banyak server atau kebutuhannya read-heavy, kalian membutuhkan replikasi. Alih-alih mengetik konfigurasi pg_hba.conf, postgresql.auto.conf, dan pg_basebackup secara manual di setiap server, collection community.postgresql menyediakan modul postgresql_primary dan postgresql_standby:
- name: Konfigurasi streaming replication
hosts: db_servers
become: true
vars:
pg_primary_host: db-01.internal
tasks:
- name: Konfigurasi server primary
community.postgresql.postgresql_primary:
login_host: "{{ pg_primary_host }}"
login_user: postgres
login_password: "{{ vault_postgres_password }}"
host: "{{ pg_primary_host }}"
replication_user: repl
replication_password: "{{ vault_pg_replication_password }}"
when: inventory_hostname == groups['db_primary'][0]
- name: Konfigurasi server standby
community.postgresql.postgresql_standby:
login_host: "{{ pg_primary_host }}"
login_user: postgres
login_password: "{{ vault_postgres_password }}"
host: "{{ pg_primary_host }}"
primary_conninfo: "host={{ pg_primary_host }} port=5432 user=repl password={{ vault_pg_replication_password }} application_name=standby1"
when: inventory_hostname in groups['db_standby']Note
Replication yang diatur manual seperti di atas cukup untuk failover manual dan distribusi beban baca. Untuk high availability yang sungguh-sungguh — dengan automatic failover dan healthcheck — produksi biasanya memakai Patroni atau Replication Manager (repmgr) yang dikelola di atas Ansible. Konsep yang kalian pelajari di sini tetap menjadi fondasinya.
Koneksi database itu mahal. Setiap koneksi TCP ke PostgreSQL memakan memori dan waktu. Ketika aplikasi meledak trafiknya, jumlah koneksi bisa melampaui max_connections, dan database mulai menolak koneksi — aplikasi ikut kolaps. Solusi standarnya adalah connection pooler: aplikasi cukup memegang sedikit koneksi ke pooler, dan pooler mem-reuse koneksi ke database.
transaction adalah pilihan paling populer.Deploy PgBouncer dengan template + handler seperti biasa:
[databases]
ecommerce = host=127.0.0.1 port=5432 dbname=ecommerce
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 100
default_pool_size = 20
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid- name: Deploy PgBouncer
hosts: pgbouncer_servers
become: true
tasks:
- name: Install PgBouncer
ansible.builtin.apt:
name: pgbouncer
state: present
- name: Render konfigurasi PgBouncer
ansible.builtin.template:
src: templates/pgbouncer.ini.j2
dest: /etc/pgbouncer/pgbouncer.ini
owner: postgres
group: postgres
mode: "0644"
notify: restart pgbouncerAplikasi kemudian cukup mengarahkan koneksinya ke port 6432 — perubahan di sisi aplikasi minimal, sementara ketahanan database meningkat drastis. Pola yang sama berlaku untuk ProxySQL di MySQL/MariaDB.
Berikut praktik yang membedakan engineer database yang amatir dengan yang profesional:
--ask-vault-pass di lokal dan --vault-password-file di CI/CD. (Detail lengkap ada di episode 14.)postgresql_db, mysql_user, dll) bukan shell + SQL mentah. Modul tahu cara membandingkan state; shell tidak. Jika terpaksa pakai shell, bungkus dengan kondisi changed_when dan register.host.ansible-playbook --check untuk dry-run dan Molecule (episode 18) untuk pengujian role.schema_migrations) agar changed_when akurat dan migrasi tidak ganda.1. Password database plaintext di playbook atau Git
Ini kesalahan paling fatal. Password yang pernah masuk ke history Git tidak bisa benar-benar dihapus (episode 14). Selalu simpan di Ansible Vault dan gunakan no_log: true pada task yang menyentuhnya.
2. Memakai shell + SQL ketika modul tersedia
mysql dan psql via shell tidak idempotent. Contoh nyata: task yang menjalankan CREATE DATABASE akan fail di run kedua. Modul seperti mysql_db dan postgresql_db menangani hal ini dengan state: present/absent.
3. Salah menargetkan host database
Modul database terhubung via TCP ke server database. Jika port tidak bisa diakses dari control node (firewall, bind address 127.0.0.1), task akan gagal dengan Connection refused. Gunakan login_host, login_port, dan pastikan network path-nya benar. Sebaliknya, jangan mengubah bind address database ke 0.0.0.0 tanpa alasan keamanan yang kuat.
4. Privilege superuser untuk user aplikasi
Mudah tergoda memberikan ALL PRIVILEGES atau superuser agar "tidak ribet". Efeknya: satu kebocoran kredensial = database hilang total. Terapkan least privilege sejak awal.
5. Migrasi skema yang tidak idempotent
Menjalankan ALTER TABLE ... ADD COLUMN dua kali akan error. Solusinya: guard dengan IF NOT EXISTS, atau catat versi migrasi di tabel ledger. Jangan andalkan "jalan sekali saja" di production.
6. Backup tanpa retensi dan tanpa verifikasi restore
Disk penuh karena backup menumpuk, atau dump korup yang baru disadari saat bencana — keduanya sama-sama menyakitkan. Otomasikan keduanya: retensi (hapus backup > N hari) dan verifikasi (restore ke staging).
Pada episode ini kita telah membahas automasi database dengan Ansible: collection community.postgresql, community.mysql, dan community.mongodb untuk membuat database, user, dan mengelola privilege; backup otomatis yang idempotent dan teruji; deployment pattern seperti master-slave replication dengan postgresql_primary/postgresql_standby; connection pooling dengan PgBouncer dan ProxySQL; serta best practice menyimpan kredensial di Ansible Vault dengan no_log: true.
Inti dari semua ini sederhana: database adalah aset yang paling mahal untuk diganti, jadi setiap perubahan padanya harus bisa diaudit, direproduksi, dan diuji. Dengan Ansible, pekerjaan DBA yang repetitif dan rawan menjadi konfigurasi yang deklaratif, terdokumentasi, dan idempotent.
Di episode 25 selanjutnya, kita akan membahas Monitoring & Observability Stack Automation — deployment Prometheus dan Alertmanager, Grafana dengan dashboard as code, Node Exporter, hingga logging stack seperti ELK dan Loki/Promtail — semuanya dikelola dengan Ansible agar seluruh infrastruktur yang kita bangun di series ini bisa dipantau dengan baik. Pastikan tetap semangat!