Bagaimana Saya Menyiapkan Stack Gateway dan Login untuk Project Baru dengan Satu Perintah
On this page
- Pendahuluan
- A. Apa itu stack gateway dan login
- B. Kenapa boot pertama menentukan
- C. Kenapa menyalin stack sebelumnya adalah awal yang keliru
- D. Lima hambatan yang menghalangi
- Metode
- A. Tiap nilai dalam bentuk yang dibutuhkan file-nya
- B. Nilai yang ditolak gateway, atau diterima tapi tak pernah cocok
- C. Menghapus user yang ditanam migrasi
- D. Merutekan ke sesuatu sebelum backend ada
- E. Memulai run kedua dari nol
- Hasil dan Pembahasan
- A. Apa yang dihasilkan satu perintah
- B. Menjalankannya tanpa orang di depan keyboard
- C. Yang sengaja diserahkan kepada saya
- Simpulan
- Istilah
Pendahuluan
Setiap project baru yang saya mulai butuh kebutuhan yang sama sebelum butuh apa pun lainnya: pintu masuk. Request harus lewat gateway yang tahu siapa pemanggilnya, user harus bisa login lewat identity provider, dan harus ada orang yang menjadi administrator pertama. Saya ingin seluruh lapisan itu hadir untuk project baru cukup dengan satu perintah, langsung jalan sejak boot pertama, dan bisa diuji sebelum satu baris pun dari backend project itu ditulis.
A. Apa itu stack gateway dan login
Gateway yang dimaksud adalah apiga, API gateway dengan login OpenID Connect (OIDC) yang saya tulis untuk project-project saya sendiri. OIDC adalah standar yang membuat aplikasi menyerahkan urusan login ke identity provider terpisah, lalu menerima kembali pernyataan bertanda tangan tentang siapa user itu. apiga berdiri di depan setiap backend: ia mengurus login, menyimpan session, memeriksa permission, dan baru sesudah itu meneruskan request ke service yang dituju. Akibatnya, backend di belakangnya tak perlu tahu cara login bekerja; ia cukup memercayai bahwa setiap request yang sampai kepadanya sudah diperiksa lebih dulu oleh gateway.
Menjalankannya butuh lebih dari gateway itu sendiri. User, role, organization, dan rute disimpannya di PostgreSQL, sedangkan session dan cache berumur pendek disimpan di Redis. Rute dialamatkan dengan triplet — organization, service, dan versi service itu — jadi request ke /my-org/online-shop/v1/api/… diteruskan ke alamat upstream mana pun yang terdaftar untuk kombinasi tersebut. Stack yang berfungsi berarti empat hal sekaligus: database, cache, gateway, dan isi database yang membuat gateway itu benar-benar berguna.
B. Kenapa boot pertama menentukan
Gateway yang menyala tapi tak punya seorang pun yang boleh memakainya belum selesai; ia gateway yang tak bisa dimasuki siapa-siapa. Setiap stack butuh super administrator pertama yang tertaut ke akun sungguhan di identity provider, supaya login pertama benar-benar mendarat di akun yang punya wewenang. Ia butuh setidaknya satu rute yang bisa di-resolve, sebab tanpa itu tak ada yang bisa diuji. Dan semua itu harus sudah benar sejak start pertama, karena pilihan lainnya — menyalakan, menemukan apa yang kurang, lalu menambal database dengan tangan — persis jenis pekerjaan yang hasilnya berbeda setiap kali dikerjakan.
Ada alasan kedua. Backend project baru biasanya belum ada ketika gateway-nya disiapkan. Kalau gateway baru bisa diuji setelah backend siap, setiap persoalan di lapisan login akan muncul bersamaan dengan setiap persoalan di backend baru, dan keduanya sulit dipisahkan. Gateway yang bisa dibuktikan sendiri lebih dulu menyingkirkan salah satu dari dua tersangka itu sebelum backend datang. Begitu backend tersambung dan ada yang gagal, saya sudah tahu kegagalan itu bukan di login, bukan di permission, dan bukan di routing, karena ketiganya sudah terbukti jalan lebih dulu.
C. Kenapa menyalin stack sebelumnya adalah awal yang keliru
Jalan pintas yang paling jelas adalah menyalin folder project terakhir yang berhasil, lalu menyuntingnya. Jalan itu saya tolak sejak awal. Stack hasil salinan membawa identitas project sebelumnya: nama container, nama network, kunci enkripsi untuk token-nya, administratornya, kadang juga datanya. Semua itu harus ditemukan lalu diganti, dan yang terlewat tidak gagal dengan suara keras. Kunci enkripsi yang dipakai bersama oleh dua project adalah persoalan keamanan yang tak akan dilaporkan oleh tes mana pun.
Menulis konfigurasi dengan tangan dari template hanya sedikit lebih baik. Nilai yang sama muncul di beberapa file dalam bentuk berbeda-beda, dan tangan yang menuliskannya adalah tangan yang sama yang lupa bentuk mana yang dibutuhkan tiap file. Jadi keputusannya generator: satu script yang menanyakan nilai yang berbeda antar-project, membuat sendiri nilai yang punya syarat keras, menyebut file yang harus disepakati satu nilai, lalu menulis setiap file dari nilai-nilai itu. Dengan begitu, satu-satunya yang perlu saya ingat untuk project baru hanyalah jawaban atas pertanyaannya, bukan daftar file mana saja yang harus disentuh dan dalam bentuk apa.
D. Lima hambatan yang menghalangi
Gampang dirumuskan, canggung dibangun. Ada lima rintangan yang berdiri di antara "satu perintah" dan stack yang jalan sejak boot pertama.
Satu password harus ditulis dalam tiga bentuk berbeda, dan seed butuh bentuk keempat untuk nilainya sendiri. Password database muncul mentah di file environment, percent-encoded di dalam URL koneksi, dan di-escape lagi di dalam JSON; nilai yang masuk ke SQL seed, seperti email administrator, di-escape untuk SQL. Menuliskannya dengan cara yang sama di mana-mana adalah alternatif yang ditolak, karena password yang mengandung @, :, atau % diam-diam merusak URL koneksi — dan proses encoding-nya sendiri punya urutan yang, bila keliru, membuat sebagian karakter ter-encode dua kali.
Sebagian nilai ditolak, sebagian lagi diterima tapi tak pernah cocok. Kunci yang mengenkripsi ID token wajib tepat 32 byte; kalau tidak, gateway menolak boot. Tautan administrator ke identity provider wajib memakai identifier provider yang sama dengan konfigurasi OIDC gateway; kalau tidak, administrator hasil seed ada di database tapi tak pernah dikenali saat login. Membuat nilai-nilai ini secara longgar lalu membetulkannya sesudah kegagalan pertama adalah alternatif yang ditolak, apalagi yang kedua bahkan tak pernah gagal — ia sekadar tak pernah berhasil.
Migrasi menanam user yang harus dihapus, dengan urutan yang benar. Migrasi database milik gateway sendiri membuat user bootstrap, yang menjadi pemilik organization yang ikut dibuat bersamanya. Menghapus user itu diblokir database, karena organization tak boleh kehilangan pemiliknya. Menaruh seed di init script database juga tak bisa: script itu berjalan sebelum migrasi gateway membuat tabel apa pun.
Rute butuh tujuan sebelum backend-nya ada. Mengarahkan rute pertama ke backend sungguhan project itu adalah alternatif yang ditolak, sebab backend itulah yang belum siap. Harus ada yang menjawab sebagai penggantinya, terjangkau dari container gateway, dan rutenya sendiri harus diizinkan sekaligus diaktifkan; kalau tidak, gateway tak akan melayaninya.
Run kedua untuk project yang sama tak boleh mewarisi database run pertama. Data stack tinggal di folder bind-mount di luar folder output, jadi menimpa output tak menyentuhnya, dan image PostgreSQL melewati inisialisasinya setiap kali data directory-nya tak kosong. Stack baru akan diam-diam mempertahankan user lama, password lama, dan baris-baris lama. Memakai ulang direktori itu adalah alternatif yang ditolak, karena tak ada satu pun darinya yang gagal saat start.
Urutannya penting. Hambatan pertama menentukan apakah ada service yang bisa terhubung sama sekali. Yang kedua menentukan apakah gateway mau start, dan apakah administrator pertamanya bisa login. Yang ketiga menentukan apakah datanya bersih begitu gateway jalan. Yang keempat menentukan apakah ada yang bisa diuji begitu ketiga hambatan sebelumnya beres. Yang kelima menentukan apakah menjalankan perintah itu lagi dimulai dari nol, seperti run pertama.
Metode
A. Tiap nilai dalam bentuk yang dibutuhkan file-nya
Inilah jawaban atas hambatan pertama: password disimpan mentah tepat satu kali, dan setiap file lain menerimanya lewat encoder untuk format file itu. File environment menyimpannya mentah. URL koneksi menerimanya dalam bentuk percent-encoded, karena karakter seperti @ dan : punya arti di dalam URL. Encoder mengganti % paling dulu; kalau urutannya belakangan, % yang baru saja ia tulis untuk @ akan ikut di-encode lagi.
URL yang sudah di-encode lalu ditaruh di dalam konfigurasi JSON gateway, jadi ia melewati encoder kedua yang meng-escape backslash dan tanda kutip ganda. Nilai yang berakhir di SQL seed melewati encoder ketiga, yang menggandakan tanda kutip tunggal. Tiap nilai di-encode untuk tempat tujuannya, pada saat ia dikirim ke sana, dan tak pernah dipakai ulang lintas format.
pg_pw_enc=""
j_dburl=""
s_email=""B. Nilai yang ditolak gateway, atau diterima tapi tak pernah cocok
Inilah jawaban atas hambatan kedua: nilai yang punya syarat keras dibuat oleh script, tak pernah diketik, dan nilai yang harus sepakat dengan file lain ditanyakan dengan menyebut file itu di dalam pertanyaannya. Kunci ID token dibuat dari tepat 32 byte acak lalu ditulis sebagai base64, jadi tak ada jalan untuk memasukkan kunci yang panjangnya salah.
Identifier provider tak bisa dibuat otomatis, karena ia harus sama dengan issuer yang sudah disebut konfigurasi OIDC gateway. Maka prompt-nya sendiri menyebut nilai itu harus sama dengan apa, dan peringatan yang sama ditulis ke seed hasil generate, tepat di samping statement yang memakainya. Bila tautannya salah, tak ada yang melaporkan error: baris administratornya ada, identity provider meloloskan login user itu, dan gateway sekadar tak menemukan user untuk provider dan subject tersebut.
-- provider_id MUST equal issuers[].id in config/apiga/oauth2_config.json.
INSERT INTO oauth2_users (user_id, provider_id, sub, email, email_verified)
SELECT u.id, :'provider', :'sub', :'email', TRUE
FROM users u WHERE u.email = :'email'
ON CONFLICT (provider_id, sub) DO NOTHING;SQL di artikel ini memakai placeholder bernama supaya mudah dibaca; generator menulis nilai yang sudah di-escape langsung ke file yang dihasilkannya.
C. Menghapus user yang ditanam migrasi
Inilah jawaban atas hambatan ketiga: seed berjalan sesudah gateway memigrasikan database, dan ia memindahkan kepemilikan sebelum menghapus. Init script PostgreSQL hasil generate sengaja tak berisi apa pun selain komentar yang menjelaskan alasannya, karena script itu berjalan selagi database masih kosong. Seed yang sesungguhnya adalah file SQL terpisah, dijalankan oleh pembungkus kecil begitu gateway melapor healthy.
Di dalam seed, setiap organization milik user bootstrap lebih dulu diserahkan ke super administrator baru. Baru sesudah itu user bootstrap dihapus, dan baris-baris yang hanya miliknya — tautan identitas dan role-nya — ikut terhapus lewat cascading delete.
UPDATE organizations o
SET owner_user_id = (SELECT id FROM users WHERE email = :'admin_email')
WHERE o.owner_user_id IN (SELECT id FROM users WHERE email = :'bootstrap_email');
DELETE FROM users WHERE email = :'bootstrap_email';Setiap statement di seed bersifat idempoten: insert melewati baris yang sudah ada, sedangkan update dan delete tak punya pekerjaan lagi di jalan kedua. Menjalankan ulang seed sesudah gagal di tengah jalan karenanya selalu aman, dan ini lebih penting daripada kelihatannya, sebab "jalankan lagi" adalah hal pertama yang dicoba siapa pun.
D. Merutekan ke sesuatu sebelum backend ada
Inilah jawaban atas hambatan keempat: generator menambahkan echo server mungil ke stack dan mengarahkan rute pertama ke sana. Echo server itu berasal dari repository gateway sendiri, tempat ia hanya mendengarkan di alamat loopback, yang tak terjangkau dari container lain. Generator menyalinnya dengan satu baris diubah, supaya ia mendengarkan di semua interface di dalam network Docker.
Rutenya sendiri butuh dua flag terpisah. Pemakaian service version oleh satu organization harus diizinkan, yang merupakan keputusan soal hak, dan aktif, yang merupakan keputusan soal lalu lintas. Seed mengisi keduanya dalam satu statement, karena rute yang diizinkan tapi tak aktif, atau aktif tapi tak pernah diizinkan, tampak sudah terkonfigurasi padahal tak melayani apa pun. Di run kedua, tanggal izinnya dipertahankan, bukan ditimpa. Seed juga menambahkan baris upstream berbobot yang diseimbangkan gateway dan memberi super administrator akses ke service version itu, karena gateway dikonfigurasi untuk memeriksa keanggotaan sebelum meneruskan request.
INSERT INTO org_service_versions
(organization_id, service_version_id, upstream_addr, allowed_at, is_active,
type_id, auth_mode_id)
SELECT o.id, sv.id, :'upstream', now, TRUE, 1, 1
FROM organizations o, service_versions sv, services s
WHERE o.slug = :'org' AND s.slug = :'service'
AND sv.service_id = s.id AND sv.slug = :'version'
ON CONFLICT (organization_id, service_version_id) DO UPDATE
SET upstream_addr = EXCLUDED.upstream_addr, is_active = TRUE,
allowed_at = COALESCE(org_service_versions.allowed_at, EXCLUDED.allowed_at);Mendaftarkan backend sungguhan project itu belakangan sengaja tidak dimasukkan ke seed. Langkah itu lewat management API gateway, bukan SQL, dan API itu hanya menerima super administrator yang sudah login dan permission-nya sudah di-warm-up lebih dulu. Generator bisa menuliskan script untuk itu bila diminta, tapi tak pernah menjalankannya, karena langkah tersebut butuh orang yang login.
E. Memulai run kedua dari nol
Generator menghapus direktori database milik run sebelumnya sebelum menulis stack baru, lalu membuatnya lagi sendiri, supaya Docker tak membuatnya sebagai root. Direktori itu berada di bawah path ber-prefix untuk disk data, bukan di dalam folder output, dan itulah sebabnya menimpa output tak pernah cukup.
Saya memilih menghapus, bukan mempertahankan, karena stack yang diam-diam memakai ulang kredensial lama lolos dari setiap pemeriksaan yang diberikan kepada stack baru: database-nya healthy, gateway tersambung, dan satu-satunya gejala adalah administrator yang baru saja saya masukkan bukan administrator yang ada di database.
Hasil dan Pembahasan
A. Apa yang dihasilkan satu perintah
Satu kali jalan, generator menanyakan nama project, network Docker, kredensial, alamat service tetangga, administrator pertama, dan rute pertama, memperlihatkan ringkasan, lalu menulis satu folder yang berdiri sendiri: file compose berisi database, cache, gateway, dan echo server; file environment; konfigurasi gateway; seed beserta pembungkusnya. Bila diminta, ia juga menyalakan stack, menunggu database dan gateway melapor healthy, menjalankan seed, dan memeriksa endpoint readiness gateway, yang memastikan database maupun cache sama-sama terjangkau. Ia ditutup dengan mencetak location reverse proxy yang perlu dipasang di depan gateway. Request pertama ke rute pertama lalu melewati login, permission, dan routing, dan kembali dari echo server.
B. Menjalankannya tanpa orang di depan keyboard
Setiap pertanyaan bisa dijawab di muka lewat environment variable bernama sama, dan variable yang sudah di-set tak ditanyakan lagi. Satu variable melewati semua konfirmasi. Di terminal, pertanyaan yang variable-nya belum di-set tetap diajukan; run berjalan tanpa interaksi sama sekali bila setiap variable sudah di-set, atau bila input-nya bukan terminal sehingga nilai default yang dipakai. Dengan begitu script yang sama bisa dipakai secara interaktif maupun dari otomasi, dan begitulah saya menguji generator itu sendiri.
PROJECT=example.com NETWORK=example.net SERVICE_SLUG=online-shop \
Ada satu nilai yang dibaca, bukan ditanyakan. Alamat untuk binding port secara default memakai gateway network Docker, dan script menanyakannya ke Docker, bukan menghitungnya dari subnet. Docker yang menentukan gateway; anggapan umum bahwa gateway adalah alamat pertama subnet cukup sering benar sampai-sampai berbahaya.
GATEWAY=""C. Yang sengaja diserahkan kepada saya
Dua hal tetap manual, dan itu pilihan. Mendaftarkan backend sungguhan butuh orang yang login, seperti dijelaskan di atas, dan mengimpor permission backend adalah langkah yang ingin saya tinjau sebelum dijalankan, karena tanpanya setiap user biasa ditolak sebelum request-nya sampai ke backend. Konfigurasi client OIDC juga disalin dari template apa adanya: ia milik identity provider, bukan milik project, dan alamat redirect-nya adalah satu-satunya yang saya periksa dengan tangan bila project memakai path callback yang berbeda.
Simpulan
Generator ini mengubah penyiapan lapisan gateway dan login menjadi satu perintah yang boot pertamanya menghasilkan stack yang bisa dipakai login dan diuji, sebelum backend yang dilindunginya ada. Sebagian besar kerjanya bukan menyalakan container, melainkan menjaga lima titik tempat satu nilai bisa salah tanpa memberi tahu: password yang ditulis dalam bentuk keliru, kunci yang panjangnya salah, tautan identitas yang tak pernah cocok, rute yang tampak terkonfigurasi tapi tak melayani apa pun, dan run kedua yang diam-diam mempertahankan database run pertama. Masing-masing kini dibuat otomatis, diperiksa, atau dituliskan tepat di tempat kesalahan berikutnya akan terjadi.
Istilah
- API gateway
- Service yang berdiri di depan service lain, memeriksa setiap request, lalu meneruskannya ke service yang tepat.
- OpenID Connect (OIDC)
- Standar yang membuat aplikasi menyerahkan login ke identity provider terpisah dan menerima pernyataan bertanda tangan tentang siapa user-nya.
- Identity provider
- Service yang menyimpan akun user dan menjalankan proses login, misalnya Keycloak.
- Migrasi
- Perubahan skema database yang berversi, dijalankan berurutan dan dicatat supaya hanya berjalan sekali.
- Seed
- Data yang dimasukkan ke database baru supaya aplikasi punya bahan untuk bekerja sejak start pertama.
- Idempoten
- Aman dijalankan lebih dari sekali: menjalankannya lagi menghasilkan keadaan yang sama dengan menjalankannya sekali.
- Percent-encoding
- Cara menulis karakter yang punya arti di dalam URL sebagai tanda persen diikuti kodenya, misalnya
%40untuk@. - Upstream
- Service tujuan yang menerima request yang diteruskan gateway.
Technologies covered
- Bash
- PostgreSQL
- Redis
- Docker Compose
- OpenID Connect