Skip to content
Blog DevOps

Bagaimana Saya Menyiapkan Stack Gateway dan Login untuk Project Baru dengan Satu Perintah

By Romi Fadli, S.Kom. 16 min read Environments & Deployment
On this page
A single generate command on the left produces a new project folder holding a gateway with login, a PostgreSQL database and a Redis cache behind it, and an echo upstream that answers the first route

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.

bash
urlenc_pw() {
  printf '%s' "$1" | sed \
    -e 's/%/%25/g'  -e 's/@/%40/g'  -e 's/:/%3A/g' \
    -e 's|/|%2F|g'  -e 's/#/%23/g'  -e 's/?/%3F/g' \
    -e 's/&/%26/g'  -e 's/=/%3D/g'  -e 's/ /%20/g'
}

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.

bash
pg_pw_enc="$(urlenc_pw "$POSTGRES_PASSWORD")"
j_dburl="$(json_esc "postgres://${POSTGRES_USER}:${pg_pw_enc}@postgres.apiga:5432/${POSTGRES_DB}")"
s_email="$(sql_esc "$SA_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.

bash
rand_key32() {
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -base64 32
  else
    head -c 32 /dev/urandom | base64 | tr -d '\n'
  fi
}

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.

sql
-- 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.

sql
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.

bash
sed 's|ThreadingHTTPServer(("127.0.0.1", port)|ThreadingHTTPServer(("0.0.0.0", port)|' \
  "$DUMMY_SRC" > "$OUT/scripts/dummy_upstream.py"

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.

sql
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.

bash
host_data_rm  "data/postgres"   # the previous run's database, outside $OUT
host_data_dir "data/postgres"   # created by the caller, not by Docker as root

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.

bash
PROJECT=example.com NETWORK=example.net SERVICE_SLUG=online-shop \
POSTGRES_PASSWORD='…' RUN_SETUP=yes ASSUME_YES=1 ./generate-apiga-stack

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.

bash
GATEWAY="$(docker network inspect "$NETWORK" \
  --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}')"

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 %40 untuk @.
Upstream
Service tujuan yang menerima request yang diteruskan gateway.

Technologies covered

  • Bash
  • PostgreSQL
  • Redis
  • Docker Compose
  • OpenID Connect

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A checklist card on the left, a three-part token in the middle with a check mark, and an identity provider marked with a key on the right

Script Python interaktif yang membuktikan identity provider siap untuk satu client: ia menggladi login dan logout PKCE utuh seperti aplikasi, lalu memeriksa token yang diterimanya terhadap key milik provider sebagaimana yang wajib dilakukan aplikasi.

11 min read Read more →

Related Articles

A four-level tree of Indonesian regions, with one chosen path coloured level by level, feeding a form of four cascading dropdowns in the same colours

Service Rust yang menyajikan wilayah administratif Indonesia — 38 provinsi sampai 83.768 desa, berkunci kode resmi Kementerian Dalam Negeri — sebagai empat lookup bertingkat dengan pencarian, supaya setiap form alamat di setiap project mendapat jawaban yang sama.

10 min read Read more →