- Memindahkan infrastruktur produksi senilai $1.432 per bulan ke server dedicated $233 per bulan, bahkan sambil mengganti sistem operasi, dan tetap menjaga kontinuitas layanan tanpa downtime
- Menyiapkan ulang 30 database MySQL dan 34 virtual host Nginx, GitLab EE, Neo4J, Supervisor, dan Gearman secara identik di server baru, lalu menyelesaikan migrasi dengan replikasi real-time dan sinkronisasi incremental terakhir
- Kunci migrasi database adalah kombinasi pemrosesan paralel mydumper·myloader dan MySQL replication, sekaligus memperbaiki masalah skema sys dan hak akses yang muncul saat upgrade dari MySQL 5.7 ke 8.0
- Cutover dilakukan dengan urutan menurunkan DNS TTL, mengubah Nginx di server lama menjadi reverse proxy, lalu mengganti semua A record, sehingga selama propagasi DNS permintaan ke IP lama tetap diteruskan ke server baru
- Hasil akhirnya adalah penghematan $1.199 per bulan, $14.388 per tahun, peningkatan CPU·memori·storage, dan 0 menit downtime
Latar belakang migrasi
- Dalam konteks menjalankan perusahaan software di Turki, inflasi yang sangat tinggi dan melemahnya lira Turki membuat beban biaya infrastruktur berbasis dolar meningkat tajam
- Biaya server DigitalOcean sebelumnya mencapai $1.432 per bulan, dengan konfigurasi 192GB RAM, 32 vCPU, 600GB SSD, 2 volume blok 1TB, plus backup
- Target baru adalah server dedicated Hetzner AX162-R, dengan AMD EPYC 9454P 48-core 96-thread, 256GB DDR5, dan 1.92TB NVMe Gen4 RAID1
- Biaya bulanan turun menjadi $233, dengan penghematan $1.199 per bulan dan $14.388 per tahun
- Tidak ada keluhan terhadap keandalan server lama atau developer experience, tetapi untuk workload steady-state rasio harga terhadap performanya sudah tidak lagi masuk akal
Lingkungan operasional lama
- Stack yang dijalankan bukan lingkungan uji sederhana, melainkan lingkungan produksi nyata
- 30 database MySQL, total 248GB data
- 34 virtual host Nginx yang melayani banyak domain
- GitLab EE termasuk backup 42GB
- Neo4J Graph DB berukuran 30GB
- Supervisor mengelola puluhan background worker
- Menggunakan antrean tugas Gearman
- Menjalankan aplikasi mobile live untuk ratusan ribu pengguna
- Sistem operasi server lama adalah CentOS 7 dan sudah berstatus end-of-life
- Sistem operasi server baru adalah AlmaLinux 9.7, distro kompatibel RHEL 9 dan pilihan penerus yang natural untuk CentOS
- Migrasi ini bukan hanya soal penghematan biaya, tetapi juga kesempatan keluar dari sistem operasi yang sudah bertahun-tahun tidak menerima update keamanan
Strategi tanpa downtime
- Tidak menggunakan pendekatan sekadar mengganti DNS dan me-restart layanan, melainkan migrasi tanpa downtime dengan prosedur 6 tahap
-
Tahap 1: memasang seluruh stack di server baru
- Memasang Nginx dengan kompilasi source memakai flag yang sama seperti sebelumnya
- PHP dipasang melalui Remi repo dan memakai file konfigurasi
.ini yang sama dari server lama
- Memasang MySQL 8.0, Neo4J Graph DB, GitLab EE, Node.js, Supervisor, dan Gearman, lalu mengonfigurasinya agar perilakunya sama seperti sebelumnya
- Sebelum menyentuh record DNS, semua layanan sudah disiapkan agar berjalan identik dengan server lama
- Sertifikat SSL ditangani dengan menyalin seluruh direktori
/etc/letsencrypt/ dari server lama via rsync
- Setelah seluruh traffic berpindah ke server baru, dilakukan force renew massal sertifikat dengan
certbot renew --force-renewal
-
Tahap 2: replikasi file web dengan rsync
- Seluruh direktori
/var/www/html sekitar 65GB dan 1,5 juta file direplikasi lewat rsync berbasis SSH
- Verifikasi integritas dilakukan dengan opsi
--checksum
- Tepat sebelum cutover dilakukan sinkronisasi incremental terakhir untuk memasukkan file yang berubah
-
Tahap 3: replikasi master-slave MySQL
- Alih-alih men-dump lalu me-restore sambil mematikan database, dipakai konfigurasi replikasi real-time
- Server lama dijadikan master, server baru dijadikan slave read-only
- Muatan awal berukuran besar dimasukkan dengan
mydumper, lalu replikasi dimulai dari posisi binlog yang tepat sebagaimana tercatat di metadata dump
- Hingga saat cutover, kedua sisi dipertahankan dalam kondisi sinkron real-time
-
Tahap 4: menurunkan DNS TTL
- Memanggil DigitalOcean DNS API lewat script untuk menurunkan TTL record A/AAAA dari 3600 detik menjadi 300 detik
- Record MX dan TXT tidak diubah
- Perubahan TTL record email dikecualikan karena bisa memicu masalah deliverability
- Setelah menunggu 1 jam agar TTL lama kedaluwarsa secara global, cutover siap dilakukan dalam waktu 5 menit
-
Tahap 5: mengubah Nginx server lama menjadi reverse proxy
- Script Python mem-parse blok
server {} di 34 konfigurasi situs Nginx
- Konfigurasi lama dibackup, lalu diganti dengan konfigurasi proxy yang mengarah ke server baru
- Selama propagasi DNS, request yang masuk ke IP lama tetap diam-diam diteruskan ke server baru
- Dari sudut pandang pengguna, tidak ada gangguan yang terlihat
-
Tahap 6: cutover DNS dan mematikan server lama
- Script Python memanggil DigitalOcean API untuk mengganti semua A record ke IP server baru dalam hitungan detik
- Server lama dipertahankan sebagai cold standby selama 1 minggu lalu dimatikan
- Selama seluruh proses, layanan tetap merespons baik secara langsung maupun lewat proxy, sehingga tidak ada celah availability
Migrasi MySQL
- Bagian paling kompleks dari seluruh pekerjaan adalah proses migrasi MySQL
-
Dump data
- Menggunakan mydumper alih-alih
mysqldump standar
- Dengan memanfaatkan 48 core CPU di server baru untuk export/import paralel, pekerjaan yang butuh berhari-hari dengan
mysqldump single-thread dipangkas menjadi beberapa jam
- Opsi utama yang dipakai mencakup
--threads 32, --compress, --trx-consistency-only, --skip-definer, --chunk-filesize 256
- File
metadata dari dump utama mencatat posisi binlog saat snapshot diambil
File: mysql-bin.000004
Position: 21834307
- Nilai tersebut kemudian dipakai sebagai titik awal replikasi
-
Transfer dump
- Setelah dump selesai, hasilnya ditransfer ke server baru lewat rsync berbasis SSH
- Total data yang ditransfer mencapai 248GB chunk terkompresi
- Opsi
--compress dari mydumper membantu meningkatkan kecepatan transfer jaringan
-
Load data
- Menggunakan
myloader
- Opsi utama yang dipakai adalah
--threads 32, --overwrite-tables, --ignore-errors 1062, --skip-definer
-
Masalah transisi dari MySQL 5.7 ke 8.0
- Karena lingkungan CentOS 7, server lama masih tertahan di MySQL 5.7
- Sebelum migrasi, dilakukan pengecekan kompatibilitas data dengan MySQL 8.0 memakai
mysqlcheck --check-upgrade, dan hasilnya tidak menunjukkan masalah
- Server baru dipasangi MySQL 8.0 Community terbaru
- Di seluruh proyek, waktu eksekusi query menurun secara signifikan; teks asli menyebut optimizer yang lebih baik dan peningkatan InnoDB di MySQL 8.0 sebagai alasan
- Namun lompatan versi ini juga menimbulkan masalah
- Setelah import, struktur kolom tabel
mysql.user ternyata berjumlah 45, bukan 51 seperti yang diharapkan
- Akibatnya
mysql.infoschema hilang dan autentikasi user terganggu
- Percobaan perbaikan pertama memakai perintah berikut
systemctl stop mysqld
mysqld --upgrade=FORCE --user=mysql &
- Percobaan pertama gagal dengan error
ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEW
- Penyebabnya adalah skema sys ter-import sebagai tabel biasa, bukan view
- Solusinya adalah menjalankan
DROP DATABASE sys; lalu mengulangi upgrade
- Setelah itu proses selesai dengan normal
Konfigurasi replikasi MySQL
- Setelah dump selesai dimuat di kedua server, server baru dikonfigurasikan sebagai replica dari server lama
- Pada perintah
CHANGE MASTER TO, ditentukan IP server lama, user replikasi, port 3306, MASTER_LOG_FILE='mysql-bin.000004', dan MASTER_LOG_POS=21834307
- Lalu dijalankan
START SLAVE;
- Hampir seketika replikasi berhenti karena error 1062 Duplicate Key
- Penyebabnya adalah dump dilakukan dalam dua bagian, dan di sela-selanya ada write ke beberapa tabel, sehingga replay binlog mencoba memasukkan ulang baris yang sudah ada di dump
- Untuk mengatasinya diterapkan pengaturan berikut
SET GLOBAL slave_exec_mode = 'IDEMPOTENT';
START SLAVE;
- Mode IDEMPOTENT akan melewati error duplicate key dan missing row secara diam-diam
- Semua database inti akhirnya sinkron tanpa error, dan dalam beberapa menit nilai
Seconds_Behind_Master turun menjadi 0
Verifikasi sebelum cutover
- Sebelum menyentuh record DNS, perlu dipastikan semua layanan di server baru bekerja dengan benar
- Metode verifikasinya adalah memodifikasi file
/etc/hosts di mesin lokal secara sementara agar domain diarahkan ke IP server baru
- Browser dan Postman lalu mengirim request ke server baru, sementara pengguna eksternal tetap mengakses server lama
- Endpoint API, panel admin, dan status respons tiap layanan diperiksa
- Setelah semua lolos, barulah cutover aktual dilakukan
Masalah hak akses SUPER
- Setelah replikasi master-slave sepenuhnya sinkron, ditemukan bahwa di server baru
read_only = 1 tetapi statement INSERT tetap berhasil
- Penyebabnya adalah semua user aplikasi PHP ternyata memiliki hak akses SUPER
- Di MySQL, hak akses SUPER dapat melewati
read_only
- Dari hasil
SHOW GRANTS FOR 'some_db_user'@'localhost'; terlihat bahwa hak akses SUPER memang ada
- Dilakukan
REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost'; berulang pada total 24 user aplikasi
- Setelah itu dijalankan
FLUSH PRIVILEGES;
- Sejak saat itu
read_only = 1 benar-benar memblokir write dari user aplikasi sambil tetap mengizinkan replikasi
Persiapan DNS
- Semua domain dikelola lewat DigitalOcean DNS, sementara nameserver terhubung dari GoDaddy
- Penurunan TTL diotomatisasi dengan script yang menargetkan DigitalOcean API
- Yang diubah hanya record A dan AAAA
- Record MX dan TXT tidak disentuh
- TTL record terkait email dikecualikan karena potensi isu deliverability Google Workspace
- Setelah menunggu 1 jam agar TTL lama habis, cutover siap dilakukan
Mengubah Nginx server lama menjadi reverse proxy
- Alih-alih mengedit 34 file konfigurasi secara manual, konversi otomatis dilakukan dengan script Python
- Script tersebut mem-parse blok
server {} di semua file konfigurasi dan mengidentifikasi content block utama, lalu menggantinya dengan konfigurasi proxy
- Konfigurasi asli dibackup sebagai file
.backup
- Pada contoh konfigurasi dipakai
proxy_pass https://NEW_SERVER_IP;, proxy_set_header Host $host;, proxy_set_header X-Real-IP $remote_addr;, proxy_read_timeout 150;
- Opsi pentingnya adalah
proxy_ssl_verify off
- Karena sertifikat SSL di server baru valid untuk domain, tetapi tidak valid untuk alamat IP
- Karena kedua ujung koneksi dikelola sendiri, penonaktifan verifikasi dianggap dapat diterima di sini
Prosedur cutover
- Tepat sebelum cutover, syaratnya adalah lag replikasi berada di
Seconds_Behind_Master: 0 dan reverse proxy sudah siap
- Urutan eksekusinya sebagai berikut
- Di server baru jalankan
STOP SLAVE;
- Di server baru jalankan
SET GLOBAL read_only = 0;
- Di server baru jalankan
RESET SLAVE ALL;
- Di server baru jalankan
supervisorctl start all
- Di server lama jalankan
nginx -t && systemctl reload nginx untuk mengaktifkan proxy
- Di server lama jalankan
supervisorctl stop all
- Dari Mac lokal jalankan
python3 do_cutover.py untuk mengganti semua A record DNS ke IP server baru
- Tunggu propagasi sekitar 5 menit
- Di server lama beri komentar pada semua entri crontab
- Script cutover DNS memanggil DigitalOcean API untuk mengganti semua A record dalam sekitar 10 detik
Pekerjaan tambahan setelah cutover
- Setelah migrasi selesai, ditemukan banyak webhook proyek GitLab masih mengarah ke IP server lama
- Dibuat dan diterapkan script untuk memindai semua proyek melalui GitLab API dan memperbarui webhook secara massal
Hasil akhir
- Biaya bulanan turun dari $1.432 menjadi $233
- Penghematan tahunan mencapai $14.388
- Dari sisi performa juga diperoleh server yang lebih kuat
- CPU naik dari 32 vCPU menjadi 96 logical CPU
- RAM naik dari 192GB menjadi 256GB DDR5
- Storage berubah dari konfigurasi campuran sekitar 2,6TB menjadi 2TB NVMe RAID1
- Downtime adalah 0 menit
- Total waktu migrasi sekitar 24 jam
- Tidak ada dampak ke pengguna
Pelajaran utama
- MySQL replication adalah sarana kunci untuk migrasi tanpa downtime
- Disiapkan sejak awal, dibiarkan mengejar ketertinggalan, lalu dilakukan cutover
- Hak akses user MySQL wajib diperiksa sebelum migrasi
- Jika ada hak akses SUPER, maka
read_only bisa dilewati sehingga lingkungan slave sebenarnya tidak benar-benar read-only
- Update DNS, perubahan konfigurasi Nginx, dan perbaikan webhook penting untuk diotomatisasi dengan script
- Menangani lebih dari 34 situs secara manual akan memakan waktu lama dan meningkatkan risiko error
- Kombinasi mydumper + myloader jauh lebih cepat daripada
mysqldump untuk dataset besar
- Dump dan restore paralel 32 thread memangkas pekerjaan berhari-hari menjadi beberapa jam
- Untuk workload steady-state, cloud provider bisa menjadi mahal, dan server dedicated dapat memberi performa lebih tinggi dengan biaya lebih rendah
Script GitHub
- Semua script Python yang dipakai untuk migrasi dipublikasikan di GitHub
- Daftar script yang disertakan
do_list_domains_ttl.py
- Menampilkan semua A record, IP, dan TTL untuk domain DigitalOcean
do_ttl_update.py
- Menurunkan TTL semua A/AAAA record menjadi 300 detik secara massal
do_to_hetzner_bulk_dns_records_import.py
- Memindahkan semua DNS zone dari DigitalOcean ke Hetzner DNS
do_cutover_to_new_ip.py
- Mengalihkan semua A record dari IP server lama ke IP server baru
nginx_reverse_proxy_update.py
- Mengubah semua konfigurasi situs nginx menjadi konfigurasi reverse proxy
mysql_compare.py
- Membandingkan row count semua tabel di dua server MySQL
final_gitlab_webhook_update.py
- Memperbarui semua webhook proyek GitLab ke IP server baru
mydumper
- Semua script mendukung mode
DRY_RUN = True untuk preview aman sebelum penerapan nyata
1 komentar
Komentar Hacker News
Beberapa bulan lalu aku memindahkan dua server dari Linode dan DO ke Hetzner, dan berhasil memangkas biaya cukup besar. Yang lebih mengesankan, stack-nya benar-benar berantakan, dengan puluhan situs, bahasa yang berbeda-beda, library lama, plus MySQL dan Redis yang kusut jadi satu. Tapi Claude Code memindahkan semuanya, dan untuk library yang tidak ada, ia bahkan menulis ulang sebagian kode untuk menanganinya. Migrasi serumit ini sekarang jadi jauh lebih mudah, jadi ke depannya sepertinya portabilitas antar penyedia akan makin besar
Aku sedang menyusun rencana untuk pindah dari AWS ke Hetzner. Amazon kadang memasang harga 20 kali lebih mahal daripada pesaing, memaksa komitmen jangka panjang kalau ingin harga yang lumayan, dan juga membuat perpindahan data sangat mahal, jadi terasa sangat anti-pelanggan. Mereka mungkin mengira orang akan terkunci oleh biaya egress, tapi nyatanya begitu satu bagian dipindahkan ke pesaing, itu justru mendorong untuk memindahkan semuanya. Untungnya aku tidak membangun platform di atas layanan khusus Amazon, jadi migrasinya agak lebih mudah
Setiap kali melihat tulisan seperti ini, aku heran karena orang jarang membahas hal-hal seperti redundansi atau load balancer. Kalau satu server mati, beberapa layanan bisa ikut turun sekaligus, jadi aku penasaran apakah orang benar-benar menganggap ini baik-baik saja. Mungkin memang hemat uang, tapi bisa jadi malah membayar lebih mahal dalam waktu maintenance dan masalah masa depan
Di lithus.eu kami sudah sering memindahkan pelanggan dari berbagai cloud ke Hetzner. Biasanya kami menyusun multi-server, kadang multi-AZ, lalu menyebarkan workload dengan Kubernetes untuk memberi HA. Untuk single node, Kubernetes mungkin berlebihan, tapi kalau nodenya beberapa, itu jauh lebih masuk akal. Backup kami gabungkan antara Velero dan backup level aplikasi, misalnya untuk Postgres kami sampai memakai backup WAL untuk PITR. Data stateful ditempatkan minimal di dua node agar HA terjamin. Dari sisi performa, bare metal umumnya juga lebih baik, dan dibanding AWS, waktu respons sering turun hingga setengahnya. Menurutku penyebabnya bukan virtualisasi itu sendiri, melainkan faktor sekitar seperti NVMe, latensi jaringan yang rendah, dan cache contention yang lebih kecil. Ada juga lebih banyak detail di posting HN yang pernah kutulis dulu
Tulisan ini cukup sulit dibaca. Rasanya seperti Claude yang melakukan migrasi, lalu aku membaca laporan yang juga ditulis Claude. Kalau memang penghematan ini terjadi berkat LLM, itu keren, tapi kalau mau dipublikasikan setidaknya seharusnya diedit agar repetisi dan gaya narasi ala LLM dibersihkan
Menurutku Hetzner perlu diwaspadai. Dulu aku sangat suka, tapi belakangan aku pindah keluar. Mereka mematikan sekitar 30 VM yang kami pakai untuk pipeline CI/CD hanya karena satu sengketa tagihan 36 dolar. Padahal kami sudah mengirim bukti pembayaran penuh sampai catatan bank, tapi mereka bahkan tidak mau melihatnya, dan saat kami berusaha menghubungi secara darurat, akses tetap diputus semuanya. Sekarang kami pindah ke Scaleway
Beberapa bulan lalu aku mencari alternatif AWS untuk proyek sampingan SaaS kecil, dan awalnya cukup serius mempertimbangkan Hetzner demi penghematan biaya sekaligus mendukung cloud EU. Aku rela melakukan lebih banyak sendiri, tapi akhirnya reputasi IP jadi penghalang utama. Salah satu aturan managed AWS firewall di perusahaan tempatku bekerja memblokir banyak, mungkin bahkan semua, IP Hetzner, dan di laptop kerja pun situs yang di-host di IP Hetzner tidak bisa dibuka karena kebijakan IT. Mungkin dengan Cloudflare dampaknya berkurang, tapi aku juga pernah melihat komentar bahwa perlindungan DDoS mereka lemah. Akhirnya aku memilih DO App Platform di region EU, dan opsi managed DB juga jadi nilai tambah besar
Cukup bermanfaat dan patut diapresiasi bahwa pengalaman migrasi seperti ini dibagikan. Buatku, perbandingan DO dan Hetzner itu seperti trade-off antara membuka DoorDash atau UberEats versus memasak makan malam sendiri. Rasio biayanya juga terasa mirip. Aku bekerja dengan tiga cloud besar dan on-prem, tapi untuk tugas kecil atau tes PoC, aku masih tetap kembali ke konsol DigitalOcean. Kenyamanan berupa server atau bucket yang siap dengan beberapa klik, sane default, dan backup yang tinggal centang satu kotak jelas punya nilai kalau menghitung harga waktu
Aku penasaran bagaimana mereka melakukan backup DB. Apakah ada replica atau standby, atau cuma backup per jam saja. Dalam setup server tunggal seperti ini, kalau ada kerusakan hardware seperti SSD, aplikasi bisa langsung berhenti, dan khususnya kalau SSD mati total, downtime bisa berlangsung berjam-jam atau berhari-hari selama semuanya diset ulang
Gambar meme di header itu buatanku. Aku pernah memakainya di tulisan ini, jadi rasanya menyenangkan melihatnya dipakai dua kali