Bagaimana Saya Memulai Setiap Stack Baru Tanpa Mewarisi Data Project Sebelumnya
On this page
- Pendahuluan
- A. Tiga stack pendukung
- B. Apa artinya "mewarisi", dan kenapa ia senyap
- C. Jalan pintas yang saya tolak
- D. Lima hambatan yang menghalangi
- Metode
- A. Meninggalkan data lama
- B. Dinding di antara database
- C. Membuat lebih dulu sebelum Docker
- D. Loop yang tahu kapan tak ada orang
- E. Membaca aplikasi, bukan template
- Hasil dan Pembahasan
- A. Apa yang dihasilkan tiap perintah
- B. Kesamaan ketiganya
- Simpulan
- Istilah
Pendahuluan
Di luar gateway, broker, dan stack log, kebanyakan project saya butuh tiga kebutuhan yang lebih kecil: database beserta cache, tempat menyimpan file, dan aplikasinya sendiri. Saya ingin masing-masing juga lahir dari satu perintah, dan saya ingin syarat yang lebih ketat daripada "bisa menyala": stack baru tak boleh membawa apa pun dari stack yang menjadi asalnya — tak ada data lama, kredensial lama, nama lama, dan tak ada file yang dimiliki user yang salah.
A. Tiga stack pendukung
Stack pertama adalah storage dalam arti sempit: PostgreSQL, Redis, atau keduanya, untuk aplikasi yang butuh database relasional atau cache cepat. Satu server PostgreSQL sering menampung lebih dari satu database, karena satu project bisa punya beberapa service yang masing-masing memiliki datanya sendiri, dan tiap database itu punya user-nya sendiri.
Stack kedua adalah object storage. MinIO adalah server yang kompatibel dengan S3 dan menyimpan file di dalam bucket, dan di depannya berjalan OSW, object storage wrapper yang saya tulis supaya aplikasi tak pernah berbicara langsung dengan MinIO atau memegang kredensialnya. Stack ini butuh bucket privat untuk file yang hanya boleh dibaca aplikasi, dan bucket publik yang file-nya boleh diunduh siapa saja tapi tak bisa diunggahi oleh pihak luar.
Stack ketiga adalah aplikasinya sendiri, dikemas sebagai image container: aplikasi Laravel, service Rust, atau service Python. Stack-nya butuh port yang benar, file konfigurasi di tempat yang memang diharapkan aplikasi, dan bila aplikasi itu memakai database, database yang sudah healthy sebelum aplikasi menyala.
B. Apa artinya "mewarisi", dan kenapa ia senyap
Setiap generator berangkat dari template, dan setiap template lahir sebagai stack sungguhan yang pernah berjalan. Itulah yang membuat template berguna, sekaligus yang membuatnya berbahaya. Template yang disalin dari stack yang berjalan membawa nama container stack itu, kredensialnya, kadang juga datanya, dan stack yang dibangun darinya mewarisi semua itu kecuali masing-masing dibuang dengan sengaja.
Warisan jarang gagal dengan suara keras. Storage server yang masih menyimpan bucket dan access key project sebelumnya menyala dengan sempurna; ia hanya juga menerima key lama. Server database tempat setiap user bisa connect ke setiap database berjalan dengan sempurna; ia hanya tak punya dinding. Direktori yang dibuat user yang salah tampak baik-baik saja sampai service di dalam container mencoba menulis ke sana. Masing-masing lolos tes sekilas lalu gagal dengan cara yang baru ketahuan belakangan, oleh orang yang bukan pembuatnya. Karena itu pula stack baru harus diperiksa untuk apa yang semestinya tidak ada, bukan hanya untuk apa yang semestinya ada: bucket lama, user lama, database yang terbuka bagi siapa saja. Pemeriksaan semacam itu tak muncul dengan sendirinya dalam tes biasa, yang hanya bertanya apakah sesuatu berhasil, bukan apakah ada yang tersisa.
Hal serupa berlaku untuk stack aplikasi, dalam bentuk lain. Port dan path konfigurasinya adalah fakta tentang aplikasi itu, tertulis di Dockerfile dan folder sumbernya sendiri. Template yang menebak fakta-fakta itu membawa fakta milik aplikasi sebelumnya ke aplikasi baru, dan container-nya lalu mendengarkan di satu port sementara stack mempublikasikan port lain.
C. Jalan pintas yang saya tolak
Cara tercepat mendapatkan stack baru adalah menyalin folder stack terakhir lalu menyuntingnya. Cara itu saya tolak untuk ketiga stack ini, dengan alasan di atas: yang terbawa oleh salinan justru yang paling sulit disadari. Jalan pintas kedua adalah satu server database yang dipakai bersama semua service tanpa isolasi di antara database-nya, yang nyaman sampai kredensial satu service bisa membaca tabel milik service lain.
Jalan pintas ketiga, untuk aplikasi, adalah menulis file compose dan konfigurasi setiap stack dengan tangan. Cara itu berhasil, tapi fakta-fakta yang dibutuhkannya — port tempat container mendengarkan, path tempat aplikasi membaca konfigurasinya, file konfigurasinya sendiri — sudah ada di folder aplikasi itu. Mengetiknya ulang adalah jalan bagi stack dan aplikasinya untuk pelan-pelan saling menjauh.
Karena itu ketiga generator mengikuti satu aturan yang sama: buang dari template apa pun yang milik pihak lain, buat apa pun yang harus dimiliki orang yang menjalankannya, dan baca apa pun yang merupakan fakta tentang aplikasi langsung dari aplikasi itu sendiri.
D. Lima hambatan yang menghalangi
Gampang dirumuskan, canggung dibangun. Ada lima rintangan di antara template dan stack yang tak berutang apa pun kepada stack sebelumnya.
Template membawa data stack sebelumnya. Template object storage berisi direktori data, dan direktori itu menyimpan bucket lama beserta access key lama. Menyalin template apa adanya lalu mengganti kredensial di konfigurasi belakangan adalah alternatif yang ditolak: MinIO mempertahankan apa yang sudah ada di direktori datanya.
Database di satu server secara bawaan bisa saling melihat. Di PostgreSQL, setiap database memberi hak connect kepada role bawaan yang diikuti semua user. Membuat satu user per database saja adalah alternatif yang ditolak, karena tiap user itu tetap bisa connect ke setiap database lain, termasuk database utama.
Docker membuat yang belum ada sebagai root. Direktori bind mount yang belum ada akan dibuat Docker, sebagai root, dan database di dalam container tak bisa menulis ke sana. Template storage punya versi yang lebih halus dari persoalan yang sama: init script yang di-mount sebagai file ternyata pernah ada di template sebagai folder kosong milik root.
Mode otomatis bisa menggantung selamanya. Setiap generator bisa berjalan tanpa pertanyaan, dengan satu variable yang menjawab ya untuk setiap konfirmasi. Generator storage bertanya dalam loop apakah ingin menambah database lagi, dan di mode otomatis pertanyaan itu selalu dijawab ya, sementara prompt sesudahnya membaca dari input yang sudah habis.
Stack aplikasi tak boleh menebak apa yang sudah diketahui aplikasinya. Port, file konfigurasi, dan tempat file itu di-mount berbeda-beda menurut aplikasi dan bahasanya. Menanyakannya dengan tangan, atau membawanya dari template, adalah alternatif yang ditolak.
Urutannya mengikuti seberapa jauh tiap jenis warisan menjangkau. Yang pertama warisan data, yang kedua warisan akses, yang ketiga warisan kepemilikan, yang keempat perilaku generator itu sendiri, dan yang kelima warisan anggapan tentang aplikasinya.
Metode
A. Meninggalkan data lama
Inilah jawaban atas hambatan pertama: generator object storage membuang data template dari folder stack baru, lalu membuatkan direktori data kosong untuk stack itu sebelum apa pun menyala. MinIO pun menginisialisasi diri dari nol, dengan kredensial yang diberikan ke generator. File environment template dan konfigurasi JSON milik wrapper juga menyimpan kredensial dan endpoint stack sebelumnya, jadi keduanya ditulis ulang dari jawaban, dengan nilai JSON di-escape. Bucket juga tidak disalin: semuanya dibuat di server yang sedang berjalan memakai MinIO client, yang sekaligus mengatur bucket publik agar hanya mengizinkan unduhan anonim.
&& \
&& \
Generator yang sama juga mengganti nama setiap container. Template masih memuat nama container dari project asalnya, dan tiap generator menggantinya dengan nama yang dibangun dari project baru, supaya dua stack di host yang sama tak pernah bertabrakan dan tak pernah tampak kembar di daftar container yang berjalan. Generator aplikasi juga mengganti nama service storage milik template — database dan cache menjadi service yang dinamai menurut aplikasinya — dan memberi tiap aplikasi variable versi dan port-nya sendiri, jadi dua aplikasi dalam satu project tak pernah berebut nama service yang sama.
B. Dinding di antara database
Inilah jawaban atas hambatan kedua: setiap database tambahan mendapat owner sendiri, dan hak connect dicabut dari role bawaan yang diikuti semua user, lalu dikembalikan khusus kepada owner-nya. Database utama dikunci dengan cara yang sama, jadi user database tambahan juga tak bisa connect ke sana. Superuser melewati aturan ini, jadi administrasi tetap jalan.
CONNECT ON DATABASE main_db FROM PUBLIC;
CONNECT ON DATABASE main_db TO main_user;
WITH PASSWORD '…';
OWNER crm_user;
CONNECT ON DATABASE crm FROM PUBLIC;
CONNECT ON DATABASE crm TO crm_user;Karena nama database dan user ditulis ke SQL tanpa tanda kutip, generator hanya menerima nama yang aman dengan cara itu: huruf atau garis bawah di depan, lalu huruf, angka, atau garis bawah. Ia juga menolak nama yang mengulang database utama atau user utama, atau database yang ditambahkan dua kali. Password ditulis sebagai string SQL, jadi tanda kutip tunggalnya digandakan.
C. Membuat lebih dulu sebelum Docker
Inilah jawaban atas hambatan ketiga: setiap direktori yang akan di-mount container dibuat oleh generator, sebagai user yang menjalankannya, sebelum container menyala, dan hanya untuk service yang dipilih. Docker menemukan direktori itu sudah ada dan membiarkan kepemilikannya. Init script ditangani dengan semangat yang sama: folder kosong milik root yang tertinggal di tempat script itu seharusnya berada dibuang lalu diganti file sungguhan, karena mount file yang sumbernya direktori tidak sah. Generator aplikasi selalu menulis file itu, walau stack-nya tanpa PostgreSQL, jadi mount yang dideklarasikannya selalu sah.
Script itu kemudian menjadi tempat dinding-dinding database di atas dituliskan, jadi file yang membereskan persoalan kepemilikan sekaligus membawa isolasi database ke start pertama server.
D. Loop yang tahu kapan tak ada orang
Inilah jawaban atas hambatan keempat: di mode otomatis, loop yang menanyakan database tambahan tak pernah dimasuki. Database tambahan diberikan lewat satu variable, berupa daftar nama, user, dan password yang dipisah koma, dan divalidasi dengan aturan yang sama seperti jawaban interaktif.
&&
while ; do
||
Kemacetan semacam ini mudah disalahbaca karena tak ada yang crash: konfirmasi menjawab ya, read mengembalikan jawaban kosong, validasi menolak entri kosong itu dengan keluhan singkat, dan loop bertanya lagi — selamanya, memenuhi log otomasi dengan pesan yang sama tanpa pernah berhenti. Pengamannya cuma satu baris, ditaruh sebelum loop, bukan di dalamnya.
E. Membaca aplikasi, bukan template
Inilah jawaban atas hambatan kelima: generator aplikasi menanyakan folder yang memuat Dockerfile aplikasi itu, lalu membaca fakta-faktanya dari sana. Port internal diambil dari baris EXPOSE di Dockerfile, dan baru ditanyakan bila baris itu tak ada. Untuk service Rust, konfigurasi di-mount di path yang dinamai menurut nama package-nya, jadi generator membaca nama package dari Cargo.toml.
EXPOSE_PORT=""
CARGO_NAME=""File konfigurasinya disalin dari folder yang sama: konfigurasi JSON untuk Rust, di-mount read-only di path tersebut, dan file environment untuk Laravel dan Python, diberikan ke container sebagai environment-nya. Bila stack menyertakan PostgreSQL, aplikasi menunggu database itu healthy; bila tidak, ketergantungan itu tak ditulis sama sekali.
Hasil dan Pembahasan
A. Apa yang dihasilkan tiap perintah
Generator storage menulis stack berisi PostgreSQL, Redis, atau keduanya, dengan berapa pun database terisolasi yang dibuat saat server pertama kali start. Generator object storage menulis MinIO dan OSW dengan kredensial baru, dan bila diminta, menyalakannya lalu membuat bucket privat dan publik. Generator aplikasi menulis stack untuk satu aplikasi, dibangun dari Dockerfile-nya sendiri, dengan konfigurasinya sendiri, dan bila perlu database serta cache-nya sendiri. Setiap folder dinamai menurut project dan service-nya, dan tak satu pun membawa data, key, atau akses terbuka milik stack sebelumnya.
B. Kesamaan ketiganya
Ketiganya mengikuti konvensi yang sama dengan generator saya yang lain. Setiap pertanyaan bisa dijawab lewat environment variable, satu variable melewati semua konfirmasi, alamat binding port dibaca dari deskripsi network milik Docker sendiri, dan data tinggal di bawah satu direktori berprefix yang disusun mengikuti folder stack. Itulah yang memungkinkan satu project disiapkan seluruhnya dengan segelintir perintah, dalam urutan apa pun yang diizinkan ketergantungannya.
Simpulan
Tiga generator yang lebih kecil ini berbagi satu disiplin: apa pun milik stack asal template dibuang, apa pun yang harus dimiliki orang yang menjalankan generator dibuat olehnya sebelum Docker sempat membuatnya sebagai root, dan apa pun yang merupakan fakta tentang aplikasi dibaca dari aplikasi itu. Lima hambatan berdiri di jalan — data lama, database yang terbuka, path milik root, loop tanpa orang yang menjawabnya, dan anggapan tentang aplikasi — dan kelimanya kini ditangani sekali, di generator, bukan ditemukan ulang di setiap project baru.
Istilah
- Template
- Folder berisi file yang menjadi titik awal stack baru, dengan nilai khas project yang menunggu diganti.
- Bind mount
- Direktori atau file di host yang dibuat tersedia di dalam container pada path tertentu.
- Bucket
- Wadah bernama untuk file di object storage yang kompatibel dengan S3.
- Unduhan anonim
- Policy bucket yang membolehkan siapa saja membaca file-nya tanpa kredensial, sementara menulis tetap membutuhkannya.
- Identifier
- Nama objek database, seperti database atau user, yang ditulis ke dalam SQL.
- Role PUBLIC
- Role bawaan PostgreSQL yang diikuti setiap user, dan secara bawaan boleh connect ke setiap database.
- EOF
- End of file: titik ketika input tak punya isi lagi, sesudahnya setiap pembacaan mengembalikan nilai kosong.
- EXPOSE
- Instruksi Dockerfile yang mencatat port tempat aplikasi di dalam image mendengarkan.
- env_file
- Pengaturan compose yang memberikan variable di satu file ke container sebagai environment-nya.
Technologies covered
- Bash
- PostgreSQL
- Redis
- Docker Compose
- MinIO