Progress belajar
Modul 65 dari 73
0% 0/73 modul selesai
Setelah selesai, tandai modul ini agar progres kursus tetap rapi.
Progress disimpan lokal di browser ini.
PostgreSQL di AWS RDS
Database Production yang Aman
Kita pindahkan PostgreSQL skincare dari Docker lokal ke RDS yang private, dibackup, dan hanya bisa disentuh task ECS lewat security group.
Kenapa Database Production Beda Mainnya
API boleh diganti kapan saja, tapi database adalah state bisnis yang tidak boleh hilang.
Di local development, PostgreSQL terasa seperti dependency biasa di docker compose. Di production, ia adalah satu-satunya tempat order, stok, dan pembayaran skincare hidup.
Container API di ECS itu stateless: kamu boleh membunuh dan menggantinya berkali-kali tanpa kehilangan apa pun, karena seluruh state ada di database. Justru karena itu, database production diperlakukan jauh lebih hati-hati. Order yang sudah dibayar, stok serum yang baru direservasi, event webhook pembayaran, dan data customer tidak boleh hilang hanya karena deploy gagal atau satu task crash.
Relational Database Service, layanan database terkelola AWS yang mengotomasi provisioning, backup, patching, dan monitoring engine relasional (PostgreSQL, MySQL, dan lainnya) tanpa kamu mengelola OS server database secara langsung.
Modul ini tidak mengganti SQL dan pgx dari Roadmap 3. SELECT, transaksi, index, dan pgxpool tetap sama persis. Yang kita tambahkan adalah lapisan production di sekeliling database itu: jaringan private, kontrol akses, backup, batas koneksi, migration yang disiplin, dan ketahanan saat satu Availability Zone tumbang.
Di docker compose, API konek ke service postgres lewat network bridge lokal, tanpa firewall. Di AWS, koneksi yang sama dikontrol berlapis oleh VPC, subnet, route table, dan security group. Bukan PostgreSQL-nya yang berubah, melainkan jaringan di sekelilingnya.
State bisnis
Order, payment, stok, dan customer ada di PostgreSQL. Kehilangan data jauh lebih mahal daripada downtime API beberapa menit.
Permukaan serangan
Database production tidak boleh terbuka ke internet. Hanya workload internal (task ECS) yang perlu port 5432.
Kapasitas koneksi
Tiap task ECS membuka pool sendiri. Total pool lintas task yang tak dihitung bisa menabrak max_connections RDS.
Target arsitektur kita sederhana tapi tegas: API di ECS boleh bicara ke RDS, tetapi internet tidak pernah bisa menyentuh RDS.
flowchart LR
U["User / Browser"] -->|HTTPS 443| ALB["Application Load Balancer<br/>public subnet"]
ALB -->|HTTP app port| API["ECS Fargate API<br/>private subnet"]
API -->|TCP 5432| RDS[("RDS PostgreSQL<br/>private subnet")]
Internet["Internet 0.0.0.0/0"] -. blocked .-> RDSGambar 1. RDS hidup di private subnet tanpa IP publik. Hanya traffic dari security group ECS yang boleh masuk ke port 5432.
RDS PostgreSQL sebagai Managed Database
PostgreSQL yang sama, tapi operasi infrastrukturnya dipegang AWS.
RDS bukan ORM, bukan dialek SQL baru, dan bukan database lain. Ia adalah PostgreSQL biasa yang servernya dikelola AWS.
DB instance RDS adalah satu server PostgreSQL terkelola. Bedanya dengan VPS biasa: kamu tidak punya akses SSH ke host, tidak menyentuh OS, dan tidak memasang paket. Akses hanya lewat SQL client atau driver pgx. Sebagai gantinya, AWS menangani patch engine, perawatan disk, snapshot, dan failover. Kamu memilih major version (misalnya PostgreSQL 17, yang sudah GA di RDS sejak akhir 2024) dan RDS mengisi minor terbaru bila tidak kamu tentukan.
Daftar major version yang aktif bergerak. Jangan menebak. Konfirmasi default dan versi tersedia di region target dengan satu perintah AWS CLI sebelum membuat instance.
Terminalaws rds describe-db-engine-versions \ --engine postgres \ --default-only \ --query "DBEngineVersions[].EngineVersion" \ --output text
- PostgreSQL sering dipasang manual di droplet, satu server dengan app atau VM terpisah.
- Backup, patch OS, autovacuum tuning, dan monitoring sering jadi skrip cron buatan sendiri.
- PostgreSQL berjalan sebagai DB instance RDS di dalam VPC.
- API Go cukup konek lewat
DATABASE_URL, lalu query denganpgxpoolseperti biasa.
Untuk skincare shop, beban utama kita adalah OLTP: customer checkout, stok dikurangi dalam transaksi, payment event dicatat, status order berpindah, dan admin menarik laporan. Pola beban ini cocok untuk satu RDS PostgreSQL yang ditata rapi, jauh sebelum kita butuh sharding atau database eksotis.
AWS mengurus server, bukan keputusanmu. Skema, index, query, batas transaksi, credential, akses jaringan, sizing pool, dan migration tetap 100 persen tanggung jawab tim backend. RDS hanya menghapus pekerjaan ops OS, bukan pekerjaan engineering data.
Private Subnet dan Security Group
Aturan emas: RDS tidak punya IP publik, dan inbound 5432 hanya dari security group aplikasi.
Database production tidak menerima koneksi langsung dari laptop developer. Ia hidup di private subnet dan hanya melayani workload di dalam VPC.
RDS ditempatkan lewat DB subnet group, kumpulan subnet di minimal dua Availability Zone. Untuk production, semua subnet itu harus private: route table-nya tidak punya rute langsung ke Internet Gateway, sehingga instance tidak bisa dijangkau dari internet. Setel publicly accessible ke off, dan RDS tidak akan pernah mendapat IP publik.
Daftar subnet (minimal di dua AZ) tempat RDS boleh menaruh endpoint database-nya. Untuk production, isi dengan private subnet saja agar tidak ada jalur dari internet.
Lapisan kedua adalah security group, firewall stateful per-resource. Trik produksi yang penting: inbound rule RDS jangan menunjuk CIDR 0.0.0.0/0, melainkan menunjuk security group milik task ECS sebagai source (SG-to-SG reference). Artinya, hanya resource yang memakai SG itu yang boleh connect, dan aturannya tetap valid walau IP task berubah-ubah saat scaling.
flowchart TD
NET["Internet :443/:80"] --> ALBSG["sg-alb<br/>inbound 80/443 dari internet"]
ALBSG --> APISG["sg-ecs-api<br/>inbound app port HANYA dari sg-alb"]
APISG --> RDSSG["sg-rds-postgres<br/>inbound 5432 HANYA dari sg-ecs-api"]
RDSSG --> DB[("RDS PostgreSQL")]Gambar 2. Pola berlapis. Tiap security group hanya menerima source dari layer di atasnya, dan RDS tidak pernah membuka 5432 ke internet maupun ke ALB.
sg-ecs-api
Dipasang ke ECS service API. Boleh outbound ke RDS pada port 5432.
sg-rds-postgres
Dipasang ke RDS. Inbound TCP 5432 hanya dari sg-ecs-api (dan opsional sg-ecs-worker).
Terminalaws ec2 authorize-security-group-ingress \ --group-id sg-rds-postgres \ --protocol tcp \ --port 5432 \ --source-group sg-ecs-api
Kalau worker ECS juga butuh database untuk memproses payment event, tambahkan SG worker sebagai source kedua. Yang dilarang adalah membuka CIDR internet hanya supaya migration atau debugging dari laptop terasa gampang.
Terminalaws ec2 authorize-security-group-ingress \ --group-id sg-rds-postgres \ --protocol tcp \ --port 5432 \ --source-group sg-ecs-worker
Di VPS, kamu sering atur ufw allow from 10.0.0.5. Security Group lebih kuat: ia stateful (reply otomatis diizinkan, tak perlu rule outbound balasan) dan bisa menunjuk security group lain sebagai source, bukan IP. Jadi sumbernya adalah identitas (siapa task itu), bukan alamat yang bisa berubah.
Godaan terbesar adalah menjadikan RDS publicly accessible supaya migrate up jalan dari laptop. Jangan. Jalankan migration dari runner yang berada di dalam VPC, ECS one-off task, atau bastion yang dikontrol ketat. Public DB untuk data customer adalah audit finding yang menunggu terjadi.
Credential, DATABASE_URL, dan TLS
Password DB tidak masuk image, tidak masuk git, dan koneksi production pakai TLS.
Database yang private masih bocor kalau password-nya tertanam di kode atau di environment plain. Credential harus diperlakukan setara akses langsung ke data customer.
API Go konek ke PostgreSQL lewat satu connection string. pgx menerima format DSN PostgreSQL standar, jadi DATABASE_URL cukup untuk membawa host endpoint RDS, port, user, password, nama database, dan opsi TLS sekaligus.
format DATABASE_URLpostgres://skincare_app:SECRET@skincare-prod.abc123.ap-southeast-1.rds.amazonaws.com:5432/skincare?sslmode=verify-full
RDS mendukung koneksi TLS. Pakai minimal sslmode=require, dan idealnya verify-full dengan CA bundle RDS agar terlindung dari man-in-the-middle di dalam VPC. sslmode=disable di production berarti password dan data lewat plaintext.
Daripada mengarang password sendiri lalu menyimpannya di tiga tempat, biarkan RDS membuat dan mengelola password master di AWS Secrets Manager lewat flag --manage-master-user-password. Untuk user aplikasi sehari-hari, buat role terpisah dengan privilege secukupnya, lalu simpan DATABASE_URL-nya sebagai secret tersendiri.
.envproduction fisik ada di server, dibaca framework saat boot.- Rotasi password berarti mengedit file dan men-deploy ulang.
- Image Docker tidak membawa
.env. Secret di-inject saat task start. - ECS task definition memakai
secretsdenganvalueFrommenunjuk ARN Secrets Manager.
Di task definition, DATABASE_URL masuk lewat field secrets, bukan environment. Field environment terlihat di docker inspect dan console; field secrets di-resolve oleh execution role saat startup dan tidak terpapar mentah.
infra/ecs/api-secrets.json{ "secrets": [ { "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:ap-southeast-1:111122223333:secret:skincare/prod/database-url-AbCdEf" } ] }
Di sisi Go, kode tidak peduli dari mana nilai itu datang. Ia membaca satu env var dan fail-fast bila kosong, supaya aplikasi mati di startup dengan pesan jelas, bukan crash setengah jalan saat query pertama.
internal/config/database.gopackage config import ( "fmt" "os" ) // DatabaseURL membaca DSN PostgreSQL dari environment dan gagal cepat bila kosong. func DatabaseURL() (string, error) { url, ok := os.LookupEnv("DATABASE_URL") if !ok || url == "" { return "", fmt.Errorf("DATABASE_URL wajib diset (lewat ECS secrets di production)") } return url, nil }
Backup Otomatis, Snapshot, dan PITR
Backup bukan checkbox compliance, tapi fitur bisnis saat migration salah atau admin keliru update.
Pertanyaan yang menentukan desain backup bukan “apakah kita punya backup”, tapi “seberapa lama lalu kita bisa kembalikan data, dan secepat apa”.
RDS menyediakan dua mekanisme yang saling melengkapi. Automated backups berjalan harian di window yang kamu pilih, ditambah perekaman transaction log terus-menerus, sehingga kamu bisa restore ke detik mana pun di dalam window retensi. Inilah Point-in-Time Recovery (PITR). Snapshot manual adalah backup permanen yang kamu buat sendiri dan bertahan sampai dihapus, tidak terikat retensi.
Berapa hari automated backup disimpan, antara 1 sampai 35 hari. Nilai 0 mematikan automated backup (jangan di production). Window inilah rentang PITR yang tersedia.
Memulihkan database ke timestamp persis di dalam window retensi, bukan hanya ke titik backup harian. Berguna saat sebuah UPDATE atau migration merusak data pada jam tertentu.
stateDiagram-v2 [*] --> Sehat Sehat --> Insiden: migration salah / admin keliru UPDATE Insiden --> PITR: restore ke timestamp sebelum insiden Insiden --> Snapshot: restore dari snapshot manual PITR --> InstanceBaru: RDS buat DB instance baru Snapshot --> InstanceBaru InstanceBaru --> Verifikasi: cek data, lalu alihkan endpoint Verifikasi --> Sehat
Gambar 3. Restore selalu menghasilkan DB instance baru. Kamu verifikasi dulu, baru mengalihkan aplikasi ke endpoint baru.
Mulai 7 hari untuk staging, 14 sampai 30 hari untuk production, seimbangkan dengan biaya storage backup.
Sebelum migration yang menyentuh banyak data, buat snapshot bernama jelas sebagai checkpoint eksplisit.
Backup yang belum pernah di-restore hanyalah asumsi. Jadwalkan latihan restore ke environment isolasi dan catat RPO/RTO nyata.
Terminalaws rds modify-db-instance \ --db-instance-identifier skincare-prod-postgres \ --backup-retention-period 14 \ --preferred-backup-window "18:00-19:00" \ --apply-immediately
Terminalaws rds create-db-snapshot \ --db-instance-identifier skincare-prod-postgres \ --db-snapshot-identifier skincare-prod-before-voucher-migration-2026-06-09
Di VPS, backup biasanya pg_dump lewat cron lalu rsync ke storage lain, dan kamu sendiri yang menjaga rotasinya. RDS menangani jadwal, retensi, dan transaction log untuk PITR. Kamu pindah dari mengelola skrip backup ke mengelola kebijakan retensi dan latihan restore.
Retention 0 mematikan PITR sepenuhnya. Dan jangan taruh backup window di jam checkout tersibuk, karena snapshot bisa menambah I/O. Pilih window di jam traffic rendah sesuai zona waktu pasar skincare-mu.
Parameter Group dan Batas Koneksi
postgresql.conf versi RDS, tempat max_connections diatur, plus jebakannya.
Di server biasa, kamu mengedit postgresql.conf. Di RDS, semua parameter engine diatur lewat DB parameter group.
RDS memakai default parameter group bila kamu tidak membuat sendiri. Default cukup untuk mulai, tapi production sebaiknya pakai custom parameter group agar setiap perubahan eksplisit, bisa direview, dan bisa dilacak sebagai infrastruktur. Dua parameter yang paling sering jadi perhatian backend engineer: max_connections dan shared_buffers.
Jumlah maksimum koneksi simultan yang diterima PostgreSQL. Di RDS, default-nya mengikuti formula berbasis memori instance class, tapi bisa di-override lewat parameter group.
Memori yang dialokasikan PostgreSQL untuk cache data. Terlalu kecil bikin sering baca disk, terlalu besar menyisakan sedikit memori untuk koneksi dan operasi lain.
Sebagian parameter bersifat dynamic (langsung berlaku) dan sebagian static (butuh reboot DB instance). max_connections termasuk yang butuh reboot, jadi rencanakan perubahannya di window maintenance, bukan di jam ramai.
Terminalaws rds create-db-parameter-group \ --db-parameter-group-name skincare-postgres-prod \ --db-parameter-group-family postgres17 \ --description "Custom PostgreSQL parameters for skincare production"
Terminalaws rds modify-db-parameter-group \ --db-parameter-group-name skincare-postgres-prod \ --parameters "ParameterName=max_connections,ParameterValue=200,ApplyMethod=pending-reboot"
Nilai di atas hanya contoh, bukan resep universal. Sebelum menaikkan, ukur dulu beban nyata lewat metrik DatabaseConnections dan FreeableMemory. Dari dalam database, kamu bisa cek setting efektif:
ops/check-rds-settings.sqlSELECT name, setting, unit, boot_val, reset_val FROM pg_settings WHERE name IN ('max_connections', 'shared_buffers', 'work_mem', 'effective_cache_size') ORDER BY name;
max_connections besar bukan solusi performa otomatis. Ribuan koneksi malah memperlambat database karena context switching, memori per-koneksi, dan lock contention. Akar masalah too many connections hampir selalu ada di sizing pool aplikasi, bukan di kekurangan slot RDS. Itu yang kita beresi di section berikutnya.
Connection Pool Sizing untuk ECS
pgxpool itu pool per proses, dan tiap task ECS adalah satu proses.
Inti masalahnya satu kalimat: setiap ECS task menjalankan satu binary Go dengan satu pgxpool sendiri, jadi total koneksi = jumlah task dikali pool per task.
Bayangkan kamu set MaxConns=50 lalu scale API ke 6 task. API saja sudah berpotensi membuka 300 koneksi. Tambahkan worker, satu sesi psql darurat, monitoring, dan autovacuum, lalu RDS dengan max_connections=200 langsung menolak koneksi baru dengan too many connections. Yang patah bukan satu task, melainkan seluruh layanan.
Di local, kamu sering hanya melihat satu proses app, jadi satu pool. Di Node atau Laravel pun pool disetel per proses (Prisma, Knex, PDO). Bedanya bukan bahasanya, melainkan bahwa ECS menjalankan banyak proses identik secara horizontal, dan tiap proses membawa pool penuhnya. Yang aman di satu proses lokal bisa berbahaya saat dikali jumlah task.
Rumus awal yang aman:
Aturan sizing pool(MaxConns per task x jumlah task) + reserved < max_connections RDS reserved = slot cadangan untuk migration, psql darurat, monitoring, replikasi, dan autovacuum.
Contoh sizing untuk skincare production kecil dengan max_connections=200:
| Komponen | Jumlah task | Pool per task | Total koneksi |
|---|---|---|---|
| API ECS | 4 | 18 | 72 |
| Worker ECS | 2 | 10 | 20 |
| Migrator (one-off) | 1 | 4 | 4 |
| Cadangan ops | - | - | ~40 |
| Total terpakai | ~136 dari 200 |
Pool dikonfigurasi sekali saat boot. pgxpool.ParseConfig membaca DATABASE_URL, lalu kita timpa batas-batasnya dari env (yang nilainya datang dari task definition). MaxConnLifetime penting di belakang load balancer dan di lingkungan yang sering scale: koneksi di-recycle berkala agar tidak menumpuk koneksi basi.
internal/platform/postgres/pool.gopackage postgres import ( "context" "fmt" "time" "github.com/jackc/pgx/v5/pgxpool" ) type PoolConfig struct { DatabaseURL string MaxConns int32 MinConns int32 MaxConnLifetime time.Duration MaxConnIdleTime time.Duration } // NewPool membangun pgxpool production dan memastikan koneksi awal sehat lewat Ping. func NewPool(ctx context.Context, cfg PoolConfig) (*pgxpool.Pool, error) { pgxCfg, err := pgxpool.ParseConfig(cfg.DatabaseURL) if err != nil { return nil, fmt.Errorf("parse database url: %w", err) } pgxCfg.MaxConns = cfg.MaxConns pgxCfg.MinConns = cfg.MinConns pgxCfg.MaxConnLifetime = cfg.MaxConnLifetime pgxCfg.MaxConnIdleTime = cfg.MaxConnIdleTime pool, err := pgxpool.NewWithConfig(ctx, pgxCfg) if err != nil { return nil, fmt.Errorf("create pgx pool: %w", err) } pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() if err := pool.Ping(pingCtx); err != nil { pool.Close() return nil, fmt.Errorf("ping postgres: %w", err) } return pool, nil }
Nilai MaxConns datang dari environment, jadi kamu bisa menyetel per environment tanpa rebuild image. Default kecil yang sehat lebih baik daripada angka besar tanpa alasan.
internal/config/pool.gopackage config import ( "fmt" "os" "strconv" ) // Int32Env membaca integer 32-bit dari env dengan fallback aman. func Int32Env(key string, fallback int32) (int32, error) { raw := os.Getenv(key) if raw == "" { return fallback, nil } parsed, err := strconv.ParseInt(raw, 10, 32) if err != nil { return 0, fmt.Errorf("parse %s: %w", key, err) } return int32(parsed), nil }
Mulai kecil, ukur, lalu naikkan. Untuk API CRUD skincare yang query-nya cepat, pool 15 sampai 20 per task biasanya lebih sehat daripada 50 yang menganggur. Pertimbangkan RDS Proxy hanya bila kamu punya banyak koneksi singkat (misalnya Lambda atau scale-out ekstrem), bukan untuk ECS long-lived dengan pgxpool yang sudah rapi.
Migration Strategy di Production
Migration jalan sekali, sebelum versi API yang butuh skema baru menerima traffic.
Di Laravel, php artisan migrate sering jadi bagian deploy yang dijalankan otomatis. Di ECS dengan banyak task identik, otomatisasi naif itu justru berbahaya.
Kalau setiap task API menjalankan migration saat boot, rolling deploy yang menyalakan beberapa task sekaligus akan memicu beberapa migration paralel. Hasilnya race condition: dua task mencoba membuat tabel atau index yang sama, salah satu error, deploy gagal. Pola yang benar adalah memisahkan migration sebagai langkah deploy tersendiri, dijalankan tepat satu kali.
sequenceDiagram participant CI as CI/CD Pipeline participant ECR as ECR participant MIG as Migrator (one-off task) participant RDS as RDS PostgreSQL participant SVC as ECS Service (API) CI->>ECR: build & push image:GIT_SHA CI->>MIG: RunTask migrate up MIG->>RDS: terapkan migration baru RDS-->>MIG: skema versi N+1 MIG-->>CI: exit 0 (sukses) CI->>SVC: update-service (rolling deploy) SVC->>RDS: API baru jalan di skema N+1
Gambar 4. Urutan aman: push image, jalankan migration sekali, baru rolling deploy. Bila migration gagal, service lama tetap melayani di skema lama.
Untuk jalur Go Artisan kita pakai golang-migrate karena sederhana, punya CLI, dan mendukung PostgreSQL. Setiap perubahan punya pasangan file up dan down.
- db/
- migrations/
- 202606090001_create_products.up.sql buat tabel product
- 202606090001_create_products.down.sql rollback tabel product
- 202606090002_create_orders.up.sql buat tabel order
- 202606090002_create_orders.down.sql rollback tabel order
- cmd/
- api/
- main.go entry point API
- internal/
- platform/
- postgres/
- pool.go pgxpool production
Migration dijalankan dari environment yang punya akses VPC, bukan dari laptop publik (karena RDS private). Pilihan umum: ECS one-off task lewat aws ecs run-task, CodeBuild di dalam VPC, atau self-hosted runner di VPC. Skema deploy ringkasnya:
scripts/deploy-api.shset -euo pipefail IMAGE_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/skincare-api:${GIT_SHA}" # 1. build & push image docker build -t "${IMAGE_URI}" . docker push "${IMAGE_URI}" # 2. migration SEKALI dari dalam VPC (bukan dari laptop) migrate -path db/migrations -database "${DATABASE_URL}" up # 3. baru rolling deploy service API aws ecs update-service \ --cluster skincare-prod \ --service skincare-api \ --force-new-deployment
Migration production yang aman juga harus backward-compatible: skema baru tidak boleh mematahkan versi API lama yang masih berjalan selama rolling deploy. Karena itu perubahan destruktif dipecah jadi beberapa rilis.
Tambah kolom nullable, deploy API yang menulis ke kolom baru, baru di rilis berikutnya pasang constraint ketat atau hapus kolom lama.
CREATE INDEX CONCURRENTLY tidak mengunci tabel untuk write, penting agar checkout tidak ikut terkunci. Catatan: ia tidak boleh berjalan di dalam transaksi.
Jangan UPDATE jutaan row dalam satu transaksi. Pecah per batch agar lock pendek dan WAL tidak meledak.
db/migrations/202606090003_add_products_slug.up.sqlALTER TABLE products ADD COLUMN slug text; CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS products_slug_unique_idx ON products (slug) WHERE slug IS NOT NULL;
db/migrations/202606090003_add_products_slug.down.sqlDROP INDEX CONCURRENTLY IF EXISTS products_slug_unique_idx; ALTER TABLE products DROP COLUMN IF EXISTS slug;
Jangan langsung DROP COLUMN, rename kolom besar, atau backfill jutaan row di jam checkout ramai tanpa rencana lock, batch, dan rollback. Satu ALTER TABLE yang mengambil lock kuat bisa menahan seluruh write order sampai selesai.
Multi-AZ, Read Replica, dan Ketahanan
Dua mekanisme berbeda: Multi-AZ untuk availability, read replica untuk skala baca.
Orang sering mencampur dua konsep ini. Multi-AZ menjaga database tetap hidup saat satu AZ tumbang. Read replica memindahkan beban baca berat. Keduanya bukan saling pengganti.
RDS menjalankan standby sinkron di Availability Zone berbeda. Bila primary gagal, RDS otomatis failover ke standby dan endpoint DNS yang sama menunjuk ke instance baru. Ini soal availability dan durability, bukan menambah kapasitas baca.
Salinan read-only dari primary, direplikasi secara asynchronous. Aplikasi mengarahkan query baca berat ke replica agar primary fokus pada transaksi tulis. Karena asynchronous, data replica bisa tertinggal beberapa detik (replication lag).
flowchart LR
API["ECS API"] -->|checkout, payment, update stok| Primary[("RDS Primary<br/>read/write")]
API -->|laporan admin, analitik katalog| Replica[("Read Replica<br/>read-only")]
Primary -. failover otomatis .-> Standby[("Standby Multi-AZ<br/>AZ berbeda")]
Primary -. async replication .-> ReplicaGambar 5. Standby Multi-AZ untuk failover (tidak melayani query), read replica untuk query baca yang toleran lag.
Untuk skincare shop, kandidat query yang aman diarahkan ke read replica adalah yang toleran terhadap lag beberapa detik:
- Dashboard admin yang menghitung penjualan harian.
- Laporan produk terlaris per brand atau kategori.
- Export order untuk tim finance.
- Analitik pencarian dan rekomendasi sederhana.
Yang TIDAK boleh ke replica: checkout, update stok, payment webhook, dan validasi voucher. Semua operasi yang butuh konsistensi langsung membaca primary. Pola kode yang rapi adalah memisahkan reader dan writer secara eksplisit, supaya pilihan ini terlihat di call site.
internal/platform/postgres/cluster.gopackage postgres import "github.com/jackc/pgx/v5/pgxpool" // Cluster memisahkan jalur tulis (Primary) dari jalur baca yang boleh ke Replica. type Cluster struct { Primary *pgxpool.Pool Replica *pgxpool.Pool } // Writer selalu mengembalikan primary untuk transaksi yang harus konsisten. func (c Cluster) Writer() *pgxpool.Pool { return c.Primary } // Reader memakai replica hanya bila ada dan pemanggil eksplisit memintanya. func (c Cluster) Reader(preferReplica bool) *pgxpool.Pool { if preferReplica && c.Replica != nil { return c.Replica } return c.Primary }
Setelah customer checkout, jangan baca order terbaru dari replica untuk halaman sukses. Replica bisa lag, sehingga order yang baru dibuat seolah belum ada. Gunakan primary untuk read-after-write yang harus langsung konsisten.
Untuk production awal: aktifkan Multi-AZ lebih dulu (failover otomatis sering lebih penting daripada skala baca). Tambah read replica nanti, saat metrik menunjukkan primary memang jenuh oleh query baca berat, bukan oleh query lambat yang sebenarnya kurang index.
Monitoring, Alarm, dan Operasional
Database aman bukan hanya yang private dan dibackup, tapi yang kamu tahu kapan mulai sakit.
Tanpa alarm, masalah RDS baru ketahuan saat customer gagal checkout. Dengan alarm yang benar, kamu tahu lebih dulu dan punya runbook untuk bertindak.
RDS mengirim metrik ke CloudWatch otomatis. Minimal yang perlu dipantau untuk skincare production: DatabaseConnections, CPUUtilization, FreeableMemory, FreeStorageSpace, ReadLatency, dan WriteLatency. Pasang alarm pada metrik yang paling cepat menjelaskan insiden, dan sambungkan ke channel incident lewat SNS.
Terminalaws cloudwatch put-metric-alarm \ --alarm-name "skincare-prod-rds-high-connections" \ --namespace AWS/RDS \ --metric-name DatabaseConnections \ --dimensions Name=DBInstanceIdentifier,Value=skincare-prod-postgres \ --statistic Average \ --period 60 \ --evaluation-periods 5 \ --threshold 160 \ --comparison-operator GreaterThanThreshold \ --alarm-actions "${SNS_TOPIC_ARN}"
Terminalaws cloudwatch put-metric-alarm \ --alarm-name "skincare-prod-rds-low-storage" \ --namespace AWS/RDS \ --metric-name FreeStorageSpace \ --dimensions Name=DBInstanceIdentifier,Value=skincare-prod-postgres \ --statistic Average \ --period 300 \ --evaluation-periods 2 \ --threshold 10737418240 \ --comparison-operator LessThanThreshold \ --alarm-actions "${SNS_TOPIC_ARN}"
Di sisi aplikasi Go, log error database harus kontekstual tetapi tidak membocorkan credential. Jangan pernah mencatat DATABASE_URL mentah, karena ia berisi username dan password.
internal/catalog/repository.gopackage catalog import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type Repository struct { pool *pgxpool.Pool } func NewRepository(pool *pgxpool.Pool) *Repository { return &Repository{pool: pool} } // FindByID mengembalikan satu produk skincare; harga disimpan sebagai int64 rupiah. func (r *Repository) FindByID(ctx context.Context, id int64) (Product, error) { const query = ` SELECT id, name, brand, price_rupiah FROM products WHERE id = $1 AND deleted_at IS NULL ` var p Product err := r.pool.QueryRow(ctx, query, id). Scan(&p.ID, &p.Name, &p.Brand, &p.PriceRupiah) if errors.Is(err, pgx.ErrNoRows) { return Product{}, ErrProductNotFound } if err != nil { // Log konteks (id), bukan DATABASE_URL atau isi row sensitif. return Product{}, fmt.Errorf("find product %d: %w", id, err) } return p, nil }
Sediakan juga endpoint health yang menyentuh database, supaya ALB dan kamu sama-sama tahu pool masih bisa menjangkau RDS. Health check cukup Ping, jangan query berat.
/healthz Liveness ringan, tidak menyentuh database /readyz Readiness, melakukan pool.Ping ke RDS dengan timeout pendek Alarm koneksi tinggi butuh runbook: cek jumlah task ECS, cek MaxConns per task, cek slow query, cek worker yang macet, lalu scale atau rollback. Alarm tanpa runbook hanya menambah kebisingan, bukan keamanan.
Hands-on: Rakit RDS untuk Skincare API
Merangkai semua keputusan jadi satu konfigurasi minimal yang bisa diadaptasi.
Kita asumsikan VPC, private subnet, ECS service, dan Secrets Manager sudah ada dari modul sebelumnya. Fokus di sini: subnet group, security group, DB instance, secret, dan migration.
Pilih private subnet di minimal dua AZ tempat RDS boleh menaruh endpoint.
Inbound 5432 hanya dari sg-ecs-api (dan worker bila perlu), tanpa CIDR internet.
PostgreSQL 17, public access off, backup retention aktif, password master dikelola Secrets Manager, storage autoscaling.
Simpan DSN aplikasi di Secrets Manager, lalu inject ke task definition lewat secrets/valueFrom.
RunTask migrator dari dalam VPC sebelum rolling deploy API versi baru.
Terminalaws rds create-db-subnet-group \ --db-subnet-group-name skincare-private-db-subnets \ --db-subnet-group-description "Private subnets for skincare RDS" \ --subnet-ids subnet-private-a subnet-private-b
Terminalaws rds create-db-instance \ --db-instance-identifier skincare-prod-postgres \ --engine postgres \ --engine-version 17 \ --db-instance-class db.t4g.medium \ --allocated-storage 50 \ --max-allocated-storage 200 \ --db-name skincare \ --master-username skincare_admin \ --manage-master-user-password \ --db-subnet-group-name skincare-private-db-subnets \ --vpc-security-group-ids sg-rds-postgres \ --db-parameter-group-name skincare-postgres-prod \ --backup-retention-period 14 \ --multi-az \ --no-publicly-accessible
Task definition API hanya menerima secret, bukan menyimpan password di image. Perhatikan pemisahan environment (non-sensitif, boleh terlihat) dan secrets (di-resolve execution role saat startup).
infra/ecs/task-definition-rds.json{ "containerDefinitions": [ { "name": "api", "image": "111122223333.dkr.ecr.ap-southeast-1.amazonaws.com/skincare-api:GIT_SHA", "environment": [ { "name": "DATABASE_MAX_CONNS", "value": "18" }, { "name": "DATABASE_MIN_CONNS", "value": "2" } ], "secrets": [ { "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:ap-southeast-1:111122223333:secret:skincare/prod/database-url-AbCdEf" } ] } ] }
Setelah RDS aktif, tes koneksi dan jalankan migration dari dalam VPC, bukan dari laptop. ECS one-off task adalah cara bersih untuk itu.
Terminalaws ecs run-task \ --cluster skincare-prod \ --launch-type FARGATE \ --task-definition skincare-migrator \ --network-configuration "awsvpcConfiguration={subnets=[subnet-private-a],securityGroups=[sg-ecs-worker],assignPublicIp=DISABLED}"
Public access off, Multi-AZ aktif, automated backup aktif dengan retention realistis, credential dari Secrets Manager, inbound RDS hanya dari SG aplikasi, sslmode minimal require, pool dihitung lintas task, migration sudah diuji di staging, dan alarm koneksi serta storage menyala.
Ringkasan & Poin Penting
Memetakan RDS ke proyek skincare dan ke langkah berikutnya di Roadmap 8.
RDS membuat PostgreSQL production lebih mudah dioperasikan, tetapi keamanan jaringan, sizing koneksi, dan disiplin migration tetap pekerjaan tim backend.
Yang Wajib Menempel
- RDS PostgreSQL adalah PostgreSQL biasa yang servernya dikelola AWS. SQL, transaksi, index, dan pgx tidak berubah; yang berubah hanyalah jaringan dan operasi di sekelilingnya.
- RDS production untuk skincare harus di private subnet, tanpa IP publik, inbound 5432 hanya dari security group ECS (SG-to-SG, bukan CIDR internet).
- Credential tidak masuk image atau git. Simpan
DATABASE_URLdi Secrets Manager, inject lewatsecrets/valueFrom, dan pakai TLS (sslmodeminimalrequire). - Automated backup memberi PITR di dalam window retensi; snapshot manual jadi checkpoint sebelum perubahan besar. Backup baru terbukti setelah pernah di-restore.
- Parameter group adalah
postgresql.confversi RDS. Ubahmax_connections(butuh reboot) dengan hati-hati, ukur dampaknya, dan jangan jadikan obat segala masalah koneksi. - Pool dihitung lintas task: (MaxConns x jumlah task) + cadangan harus di bawah
max_connections. Mulai kecil (15 sampai 20 per task), ukur, lalu naikkan. - Migration jalan sekali dari dalam VPC sebelum rolling deploy, backward-compatible, dan index besar pakai
CONCURRENTLY. Bukan side effect boot setiap task. - Multi-AZ untuk availability (failover otomatis), read replica untuk skala baca yang toleran lag. Checkout, payment, dan stok tetap membaca primary.
Setelah modul ini, backend skincare punya database production yang private, dibackup, tahan failover, dan terkoneksi aman dari ECS. Langkah berikutnya di Roadmap 8 adalah memindahkan gambar produk ke S3 plus CloudFront, supaya file fisik tidak lagi membebani database maupun API, sambil metadata-nya tetap rapi di PostgreSQL.
Progress disimpan lokal di browser ini.