Skip to content
Blog DevOps

Bagaimana Saya Membuktikan Server S3 Siap Sebelum Aplikasi Bersandar Padanya

By Romi Fadli, S.Kom. 23 min read Storage & Data
On this page
Kotak health check bergaris putus-putus di kiri terhubung ke dua silinder: private berwarna biru dengan catatan anonim 403, dan public berwarna hijau dengan catatan anonim 200; di kanan lima kartu kemampuan presigned, integritas, policy, multipart, dan cleanup masing-masing bertanda OK.

Object store yang menjawab percobaan koneksi baru memberi tahu satu hal: ada proses yang mendengarkan. Ia belum memberi tahu bahwa browser sanggup meng-upload langsung ke sana, bahwa file yang semestinya tak boleh dilihat siapa pun memang benar-benar tak terbaca, atau bahwa upload seratus megabyte akan selamat melewati proxy di depannya. Ketiganya klaim yang terpisah, dan masing-masing bisa gugur sendirian sementara semua yang di sekitarnya tampak sehat.

Saya belajar memperlakukannya terpisah karena kegagalannya datang terlambat. Jalur upload patah di fitur yang bahkan belum dibangun; policy akses keliru dengan cara yang baru kelihatan ketika orang yang salah membuka link-nya. Maka sebelum satu service bergantung pada storage server, saya menjalankan health check buatan sendiri terhadapnya — yang melatih tiap kemampuan yang nanti diandalkan aplikasi, lalu melaporkan mana saja yang sungguh-sungguh terbukti.

Pendahuluan

Tulisan ini tentang health check tersebut dan penalaran di dalamnya, bukan panduan menjalankannya; cuplikan kodenya ada untuk memperlihatkan kenapa sebuah keputusan diambil. Bagian menariknya bukan kodenya, melainkan jarak antara "server-nya menjawab" dan "server-nya siap" — sebab jarak itu ternyata memuat enam perkara berbeda yang layak diuji.

A. Apa itu object storage, dan kenapa "S3" nama protokol, bukan nama produk

Object store menyimpan file utuh — object — yang dialamati lewat nama di dalam sebuah bucket, lalu menyerahkannya kembali lewat HTTP. Bentuknya bukan filesystem: tak ada direktori, tak ada penulisan sebagian di tempat, tak ada penggantian nama. Object dilahirkan, dibaca, atau dihapus sebagai satu kesatuan, dan kedataran itulah yang membuatnya sanggup membesar.

Amazon menerbitkan antarmuka HTTP untuk object store mereka, dan seiring waktu antarmuka itu menjelma jadi standar de facto. Kini S3-compatible menyebut server mana pun yang memakainya — MinIO, Ceph, dan daftar panjang lainnya. Akibat praktisnya: aplikasi yang ditulis mengikuti API S3, pada dasarnya, bisa berjalan di atas mana pun dari semuanya. Frasa yang layak dipegang adalah pada dasarnya, sebab kompatibilitas itu rentang, dan tepian rentang itulah tempat tulisan ini tinggal.

Dua mekanisme menentukan segalanya di bawah. Presigned URL adalah link yang ditandatangani server atas nama sebuah client: ia membawa signature dan masa berlaku sendiri, sehingga browser bisa upload atau download langsung tanpa sekali pun memegang kredensial storage-nya. Dan bucket policy memutuskan apakah sebuah object boleh dibaca tanpa kredensial — beda antara avatar publik dan tagihan pribadi.

B. Apa yang sesungguhnya diandalkan aplikasi

Ketika satu service menyatakan ia memakai object storage, diam-diam ia sedang bergantung pada beberapa kemampuan yang sama sekali tak berhubungan satu sama lain.

Ia bergantung pada browser yang sanggup upload langsung lewat presigned URL, supaya file besar tak pernah melewati server aplikasinya. Ia bergantung pada object privat yang sungguh-sungguh privat, dan object publik yang sungguh-sungguh terjangkau. Ia bergantung pada upload besar yang terpecah jadi beberapa part lalu tersusun kembali dengan benar. Dan ia bergantung pada semua itu tetap bekerja lewat proxy mana pun yang duduk di antara internet dan storage server — dan di proxy itulah batas ukuran biasanya sungguh-sungguh tinggal.

Tiap satunya janji yang terpisah. Sebuah server bisa menepati tiga lalu melanggar yang keempat, dan tak ada apa pun pada uji koneksi yang akan menyadarinya.

C. Kenapa kegagalan storage menyembul jauh dari sebabnya

Persoalannya waktu. Jalur upload yang salah konfigurasi tak gugur saat storage-nya disiapkan; ia gugur pertama kali ada orang melampirkan file, yang bisa jadi berminggu-minggu sesudah fiturnya mulai dibangun. Batas ukuran body di proxy tak gugur pada file uji kecil yang dipakai selama pengembangan; ia gugur pada dokumen sungguhan pertama yang di-upload seorang user.

Ada satu golongan yang lebih buruk ketimbang terlambat — yaitu senyap. Kalau bucket yang dimaksudkan privat ternyata terbaca tanpa kredensial, sama sekali tak ada yang gugur. Upload jalan, download jalan, aplikasinya berperilaku benar, dan satu-satunya gejalanya adalah link yang bocor ternyata link yang berfungsi. Kegagalan macam itu tak punya pesan error dan tak punya baris log, dan ia takkan mengumumkan dirinya sampai ada orang di luar sana menemukannya.

D. Kenapa uji koneksi tak menjawab ini

Tiap storage server menyertakan cara memastikan ia hidup, dan client administratifnya dengan senang hati mendaftar bucket lalu mencetak keterangan server. Saya memakainya, dan semuanya menjawab pertanyaan yang lebih sempit ketimbang pertanyaan saya.

Mendaftar bucket membuktikan kredensialnya sah dan service-nya berjalan. Ia tak melatih presigned URL, sebab pendaftaran itu menempuh SDK — library yang dipakai aplikasi untuk berbicara dengan S3 — dengan kredensial menempel — jalur yang sama sekali berbeda dari yang ditempuh browser. Ia tak menguji akses tanpa kredensial, sebab client administratif tak pernah anonim. Dan ia jelas tak mendorong data sebanyak yang diperlukan untuk menemukan bahwa reverse proxy akan menolak apa pun di atas beberapa megabyte.

Yang saya inginkan justru pernyataan tentang kemampuan: bukan "server-nya hidup", melainkan "upload lewat presigned URL bekerja, bucket privat menolak pembaca anonim, bucket publik mengizinkannya, dan file enam belas megabyte selamat pulang-pergi tanpa cacat". Itu sederet klaim, dan tiap klaim menuntut ujinya sendiri.

E. Enam hambatan yang menghalangi

Dinyatakan sebagai tujuan: membuktikan sebuah storage server siap dijadikan sandaran aplikasi, dengan melatih tiap kemampuan yang nanti dipakai lalu melaporkan mana saja yang sungguh-sungguh diperagakan. Dengan satu batasan yang membentuk seluruh rancangannya — pemeriksaannya wajib meninggalkan server persis seperti saat ia ditemukan, sebab saya menjalankannya terhadap environment yang memuat data sungguhan.

Gampang dirumuskan, canggung dibangun. Ada enam hambatan yang menghalangi, dan yang pertama sebenarnya keputusan tentang apa yang boleh diartikan kata "siap".

Hambatan pertama: "ia terhubung" dan "ia siap" dua klaim yang berbeda, dan cuma satu di antaranya gampang diuji. Uji koneksi itu satu pertanyaan dengan satu jawaban, dan justru itulah kenapa ia jadi bawaan sekaligus kenapa ia tak memadai. Kesiapan itu sederet: upload, download, keutuhan isi, policy akses, file besar, pembersihan — dan tiap butirnya bisa gugur sementara yang lain lolos. Alternatif yang ditolak justru yang paling nyaman: memeriksa konektivitas, mendaftar bucket, lalu menyebut hasilnya sehat. Penggantinya satu pemeriksaan per kemampuan, masing-masing dilaporkan terpisah, sehingga ringkasannya sanggup berkata empat belas dari empat belas, bukan sekadar "OK".

Hambatan kedua: menguji upload lewat SDK tidak menguji jalur yang nanti ditempuh browser. SDK menandatangani tiap request dengan kredensial yang ia pegang; browser tak punya kredensial dan memakai presigned URL sebagai gantinya. Keduanya jalur kode yang berbeda di sisi server, keduanya berinteraksi berbeda dengan proxy di depannya, dan yang satu bisa bekerja sempurna sementara yang lain patah. Upload lewat SDK lalu menganggap presign pasti mengikutinya adalah alternatif yang ditolak — dan ia menggoda justru sebab jalur SDK-lah yang paling gampang ditulis. Penggantinya: bangkitkan presigned URL-nya, lalu tinggalkan SDK sama sekali, dan kirim request HTTP biasa persis seperti yang dikirim browser.

Hambatan ketiga: policy akses sebuah bucket wajib dibuktikan, bukan dipercaya. Konfigurasinya menyatakan bucket mana yang privat dan mana yang publik, dan konfigurasi itu pernyataan niat, bukan pernyataan kenyataan. Bucket privat yang policy-nya keliru akan lolos tiap uji fungsional, sebab terbaca oleh orang asing tak merusak apa pun yang bakal disadari uji memakai kredensial saya sendiri. Memercayai konfigurasinya adalah alternatif yang ditolak, dan ongkos kekeliruannya bukan layanan mati melainkan kebocoran. Penggantinya pernyataan eksplisit dari luar: ambil object-nya tanpa kredensial sama sekali, lalu tuntut 403 di tempat konfigurasinya berkata privat dan 200 di tempat ia berkata publik. Selain itu dilaporkan sebagai policy mismatch.

Hambatan keempat: uji file besar tak boleh menahan file-nya di memory, sementara library upload-nya justru berharap bisa memutarnya ulang. Membangkitkan enam belas megabyte — atau beberapa gigabyte, ketika ukurannya dinaikkan — lalu menahannya di buffer itu boros, dan menuliskannya ke file sementara membuat ujinya bergantung pada disk lokal. Stream yang dibangkitkan menyelesaikan keduanya sekaligus melahirkan persoalan yang lebih halus: transfer manager memanggil seek() untuk memutar ulang sebuah part saat request diulang, dan kalau byte yang diputar ulang berbeda dari aslinya, checksum yang dihitung saat penyelesaian takkan cocok. Kedua alternatif yang kentara sama-sama gugur di sini — file sementara, dan pembangkit acak biasa yang tak pernah mengulang dirinya. Penggantinya stream yang isinya fungsi deterministik dari offset-nya, sehingga membaca posisi n dua kali selalu melahirkan byte yang sama.

Hambatan kelima: multipart upload yang melaporkan keberhasilan belum membuktikan ia sungguh-sungguh terpecah. Transfer manager akan diam-diam jatuh ke satu request tunggal ketika ia memutuskan chunking tak diperlukan, dan proxy atau gateway bisa menyatukan part dengan cara yang tak pernah dilihat client-nya. Memercayai bahwa panggilan upload-nya kembali tanpa error adalah alternatif yang ditolak, dan itu meninggalkan sifat yang paling menarik justru tak teruji. Penggantinya bertanya kepada server sesudahnya: entity tag sebuah object multipart membawa suffix yang menyebut dari berapa part ia disusun, dan jumlah part-nya baru dikembalikan ketika request-nya secara eksplisit menanyakan sebuah part. Keduanya murah, dan bersama-sama keduanya mengubah anggapan jadi bukti.

Hambatan keenam: host yang ditandatangani satu request wajib sama persis dengan host tujuan pengirimannya. Signature version 4 ikut mencakup host header, dan itu membuat beberapa normalisasi yang tampak tak berbahaya jadi merusak. Menempelkan :443 secara eksplisit ke endpoint HTTPS mengubah host yang ditandatangani lalu membatalkan tiap signature yang diturunkan darinya. Memakai endpoint internal untuk membangun URL yang nanti dibuka browser melahirkan link yang tak sanggup di-resolve browser itu. Alternatif yang ditolak persis kedua kepraktisan tadi; penggantinya menormalkan endpoint supaya port bawaan tak pernah ditempelkan, lalu menukar scheme dan host saja pada presigned URL sambil membiarkan path dan query yang ditandatangani tetap utuh.

Urutannya mengikuti apa yang disandari tiap hambatan. Yang pertama menetapkan bentuknya — begitu kesiapan jadi sederet butir, bukan lagi satu pertanyaan, semua sisanya jadi butir di dalam deret itu. Yang kedua dan ketiga sepasang soal siapa yang mengajukan request: yang satu soal jalur yang ditempuh client tanpa kredensial, yang lain soal apa yang boleh dilihat orang asing. Yang keempat dan kelima pasangan kedua, dua-duanya soal file besar, dan yang kelima baru bisa dijawab begitu yang keempat membuat upload-nya mungkin sama sekali. Yang keenam ditaruh terakhir sebab dialah yang merusak segalanya sekaligus ketika ia keliru — dan karena itu justru paling gampang didiagnosis lewat semua yang lain gagal bersamaan.

Metode

A. Konfigurasi yang menggambarkan kemampuan, bukan sekadar alamat

Inilah jawaban atas hambatan pertama, dan ia menentukan apa saja yang sanggup dinyatakan tool ini.

Konfigurasinya memuat rincian koneksi, lalu — bagian yang menentukan — sederet bucket, masing-masing menyatakan apa yang semestinya ia izinkan. Dua flag boolean itulah yang mengubah uji fungsional jadi uji policy, sebab keduanya memberi tool sebuah harapan untuk dibandingkan dengan kenyataan.

json
{
  "s3": {
    "endpoint": "https://s3.example.com",
    "region": "us-east-1",
    "addressing_style": "path",
    "access_key": "example-access-key",
    "secret_key": "<secret>"
  },
  "public_origin": "https://s3.example.com",
  "presign_expiry_seconds": 900,
  "buckets": [
    { "name": "private", "upload_auth": true,  "download_auth": true  },
    { "name": "public",  "upload_auth": true,  "download_auth": false }
  ]
}

public_origin sengaja dipisahkan dari endpoint. Tool-nya bisa saja menjangkau server lewat alamat internal sementara URL yang ia bangkitkan wajib bisa dibuka dari luar; memisahkan keduanya itulah yang membuat pemeriksaannya jujur soal alamat yang nanti sungguh-sungguh dipakai browser.

Semua yang di hilir membaca dari sini. Bucket bertanda download_auth: true akan diuji penolakannya; yang bertanda false diuji keberhasilannya. Konfigurasinya bukan sekadar setelan — ia spesifikasi yang dipakai mengukur server-nya.

B. Meninggalkan SDK untuk menguji jalur yang ditempuh browser

Inilah jawaban atas hambatan kedua. Presigned URL-nya dibangkitkan dengan kredensial lalu dipakai tanpa kredensial, dan pemisahan itulah seluruh inti ujinya.

Paruh pertama menempuh SDK, sebab penandatanganan menuntut secret-nya. Paruh kedua sengaja tidak: request-nya dikirim HTTP client biasa, tanpa kredensial di mana pun, persis seperti yang dikirim browser.

python
# Signed with credentials...
presigned = client.generate_presigned_url(
    "put_object",
    Params={"Bucket": bucket.name, "Key": key},
    ExpiresIn=bucket.presign_expiry_seconds,
)

# ...then used without them, over the origin a browser would reach.
public_url = to_public_url(presigned, cfg.public_origin)
resp = requests.put(public_url, data=payload, timeout=timeout)

Dua detail disengaja. URL-nya ditulis ulang ke public origin sebelum dipakai, supaya ujinya melatih alamat yang nanti didatangi client sungguhan, bukan alamat internalnya. Dan status response-nya diperiksa terang-terangan, sebab proxy yang menolak body mengembalikan error HTTP yang bentuknya sempurna dan takkan diangkat sendiri oleh library client.

Ada detail ketiga yang takkan saya lewatkan: URL-nya disamarkan sebelum dicetak. Presigned URL memuat signature yang masih berfungsi, dan tool diagnostik yang menuliskannya ke log sudah melahirkan kebocoran kredensial justru sambil berusaha mencegahnya.

C. Menyatakan policy akses dari luar

Inilah jawaban atas hambatan ketiga, sekaligus satu-satunya uji di sini yang kegagalannya berupa temuan keamanan, bukan layanan mati.

Pemeriksaannya sengaja lugu: ambil object-nya tanpa kredensial, lalu bandingkan hasilnya dengan yang diklaim konfigurasinya. Yang membuatnya berharga bukan kecanggihannya melainkan arahnya — ia mengajukan pertanyaannya dari tempat orang luar berdiri.

python
resp = requests.get(object_url(cfg.public_origin, bucket.name, key))

if bucket.download_auth:                 # config says: private
    ok = resp.status_code in (401, 403)  # a stranger must be refused
else:                                    # config says: public
    ok = resp.status_code == 200         # a stranger must succeed

Kedua arahnya sama-sama kegagalan yang layak dilaporkan, dan keduanya gugur dengan cara berbeda. Bucket privat yang menjawab 200 itu kebocoran: object-nya terbaca siapa pun yang memegang link-nya. Bucket publik yang menjawab 403 itu layanan mati yang sedang menunggu giliran, dan ia nanti menampakkan diri sebagai gambar yang patah, bukan sebagai persoalan storage.

Melaporkan keduanya sebagai policy mismatch, bukan sebagai kegagalan umum, itu disengaja. Server-nya bekerja; yang rusak justru kesepakatan antara server dan konfigurasinya — dan menamai pembedaan itulah yang membuat keluarannya bisa ditindaklanjuti.

D. Stream yang membangkitkan dirinya sendiri, sama persis dua kali

Inilah jawaban atas hambatan keempat, sekaligus tempat kedua cara pelaksanaan yang paling kentara sama-sama gugur karena alasan yang justru tak kentara.

Uji file besar butuh byte tanpa menahannya. Stream yang dibangkitkan menyediakan itu — tapi transfer manager mengulang part yang gagal, dan mengulang berarti mundur lalu membaca rentang yang sama sekali lagi. Kalau pembacaan kedua mengembalikan byte yang berbeda, checksum yang dihitung saat upload-nya selesai takkan cocok dengan yang diterima server, dan kegagalannya menampakkan diri sebagai data yang rusak, bukan sebagai alat uji yang cacat.

Maka isinya diturunkan dari offset-nya, bukan dari pembangkit yang berjalan maju. Blok ke-n adalah hash dari seed dan n, artinya ia bisa dihitung ulang kapan pun, dalam urutan apa pun, sebanyak yang dikehendaki transfer manager.

python
def _block(self, index: int) -> bytes:
    # Content is a function of position, not of call order — a retry seeks
    # backwards and must read exactly the same bytes it read the first time.
    out = bytearray()
    counter = 0
    while len(out) < self._BLOCK:
        out.extend(hashlib.sha256(
            self._seed + index.to_bytes(8, "big") + counter.to_bytes(4, "big")
        ).digest())
        counter += 1
    return bytes(out[:self._BLOCK])

Sifat yang sama pula yang memungkinkan uji keutuhan isinya. Karena stream-nya bisa diputar ulang dari awal tanpa menyimpan apa pun, checksum-nya bisa dihitung sebelum upload lalu dibandingkan dengan object yang di-download sesudahnya — dari ujung ke ujung, tanpa file sementara di mana pun sepanjang prosesnya.

E. Membuktikan upload-nya sungguh-sungguh terpecah

Inilah jawaban atas hambatan kelima. Upload yang melaporkan keberhasilan itu awal pertanyaannya, bukan akhirnya.

Object multipart membawa bukti penyusunannya sendiri. Entity tag-nya berakhir dengan suffix yang menyebut jumlah part, yang tak dimiliki upload satu request — jadi hadirnya suffix itulah bukti chunking benar-benar terjadi, bukan anggapan bahwa ia terjadi.

python
# PartsCount is only returned when the request asks about a specific part.
head = client.head_object(Bucket=bucket.name, Key=key, PartNumber=1)

etag = str(head.get("ETag", ""))          # e.g. "…-4" for a 4-part object
match = _MULTIPART_ETAG_RE.match(etag)    # no suffix ⇒ server did a single PUT

PartNumber=1 itu bukan hiasan. Tanpanya response-nya sama sekali tak menyertakan jumlah part, dan pemeriksaannya akan diam-diam merosot jadi melaporkan "tak diketahui" untuk tiap object — uji yang mustahil gagal, dan itu sama saja dengan tak ada uji sama sekali.

Ada mode kedua yang bersembunyi di uji yang sama, dan ia ada justru gara-gara proxy. Menyetel ukuran part sama dengan atau lebih besar dari file-nya memaksa satu request tunggal, lalu pernyataannya berbalik: ketiadaan suffix multipart justru jadi hasil yang diharapkan, dan yang sesungguhnya sedang diukur adalah apakah proxy di depan server menerima body sebesar itu. Kode yang sama menjawab dua pertanyaan berbeda, tergantung bagaimana ukurannya dibandingkan.

F. Menandatangani untuk host yang nanti sungguh-sungguh didatangi

Inilah jawaban atas hambatan keenam, dan kegagalan yang ia cegah adalah yang paling membingungkan di seluruh tool ini.

Signature version 4 memasukkan host ke dalam apa yang ia tanda tangani. Dua kepraktisan yang tampak biasa saja karenanya jadi merusak. Menempelkan port eksplisit ke endpoint yang sudah memakai port bawaan untuk scheme-nya mengubah host sebagaimana dilihat penanda tangan, dan tiap signature yang diturunkan dari client itu ditolak — dengan pesan error tentang signature-nya, bukan tentang port-nya.

python
# Attach the port only when it is not the default for the scheme: SigV4
# signs the host, and ":443" on an https endpoint changes what is signed.
default_port = 443 if parts.scheme == "https" else 80
netloc = parts.hostname if port == default_port else f"{parts.hostname}:{port}"

Kepraktisan kedua bayangan cerminnya. Ketika presigned URL yang dibangkitkan terhadap endpoint internal harus dibuka dari luar, cuma scheme dan host yang boleh diganti — path dan query string membawa signature-nya dan wajib selamat tanpa tersentuh. Membangun ulang URL-nya dari bagian-bagiannya, bukan menyunting string-nya, itulah yang menjaga jaminan tersebut.

G. Meninggalkan server persis seperti saat ditemukan

Tiap object yang dilahirkan pemeriksaan ini dihapus sebelum ia berakhir, dan pembersihannya berjalan entah ujinya lolos atau tidak. Itu menentukan sebab pemeriksaannya paling berguna justru terhadap environment yang memuat data sungguhan, dan alat diagnostik yang mengotori apa yang ia periksa takkan cukup sering dijalankan untuk jadi berguna.

Object ujinya dinamai dengan prefix yang mudah dikenali dan suffix acak, supaya apa pun yang tertinggal dari jalan yang terputus bisa dikenali dan jelas layak dibuang. Ada flag untuk mempertahankannya dengan sengaja — untuk kasus langka ketika object-nya sendiri harus diperiksa — dan bawaannya menghapus.

python
try:
    ...                      # download, integrity, access policy
finally:
    # Runs even when a check above failed: a diagnostic that litters the
    # thing it inspects is one that stops being run.
    if uploaded and not keep_artifacts:
        client.delete_object(Bucket=bucket.name, Key=key)

Menaruh penghapusannya di dalam finally, bukan sesudah rangkaian pernyataannya, itulah seluruh isinya. Jalan yang paling mungkin meninggalkan object justru jalan yang ada kegagalannya — dan itu pula jalan yang paling mungkin diulang beberapa kali berturut-turut selagi sebabnya dicari.

Hasil dan Pembahasan

A. Apa yang sebenarnya ditegakkan satu jalan yang lolos

Keluarannya sederet kemampuan berikut putusan atas masing-masing, per bucket, lalu ringkasan yang menghitungnya. Bucket privat yang sehat melaporkan empat belas pemeriksaan lolos; yang publik melaporkan tiga belas, bedanya sebab bucket publik tak punya presigned download untuk diuji.

bash
  Hasil per bucket:
    - private              14/14 check lolos   [OK]
    - public               13/13 check lolos   [OK]

  STATUS: HEALTHY  (2/2 bucket sehat)

Putusannya bertiga keadaan, bukan berdua, dan yang di tengah memang layak ada. Healthy berarti tiap bucket lolos; unhealthy berarti tak satu pun; degraded berarti sebagian lolos dan sebagian tidak — dan itulah kasus yang lazim ketika policy satu bucket keliru sementara semua yang lain baik-baik saja. Melumatkannya jadi kegagalan biasa akan menyembunyikan justru keterangan yang membuat laporannya berguna, dan exit code-nya mengikuti pembedaan yang sama supaya hasilnya bisa ditindaklanjuti tanpa membaca teksnya.

Apa yang diperagakan rangkaian itu bersifat menumpuk. Upload presigned-nya berhasil, jadi penandatanganan, proxy, dan izin bucket-nya bekerja bersama-sama. Download-nya mengembalikan checksum yang sama, jadi object-nya tersimpan utuh. Pengambilan anonimnya ditolak, jadi policy-nya cocok dengan niatnya. File besarnya tiba dengan entity tag berempat part, jadi chunking-nya sungguh terjadi dan part-nya tersusun kembali dengan benar.

Itulah klaim yang saya inginkan: bukan bahwa server-nya hidup, melainkan bahwa tiap kemampuan yang sebentar lagi disandari aplikasi sudah dilatih dan terlihat bekerja. Ketika sesuatu patah sesudahnya, storage tak lagi ada di daftar tersangka.

B. Pemeriksaan yang melaporkan temuan keamanan, bukan kegagalan

Dari semua uji di sini, pernyataan policy akses itulah yang akan saya pertahankan seandainya cuma boleh menyisakan satu — sebab cuma dialah yang kegagalannya tak melahirkan gejala di tempat lain mana pun.

Semua sisanya gugur dengan ribut dan pada akhirnya ketahuan. Upload yang tak bekerja akan disadari pertama kali ada orang meng-upload. Batas ukuran di proxy akan ditemukan file besar yang pertama. Tapi bucket privat yang terbaca orang asing berperilaku persis seperti bucket yang benar konfigurasinya, dari segala sudut kecuali satu sudut yang tak seorang pun sedang berdiri di sana. Tak ada error, tak ada request yang lambat, tak ada baris log. Satu-satunya cara menemukannya adalah dengan sengaja berdiri di luar lalu mencobanya.

Ketaksetangkupan itulah alasan membangun pemeriksaan semacam ini sama sekali. Uji yang memastikan sesuatu bekerja punya alternatif — tunggu saja, nanti juga ketahuan. Uji yang memastikan sesuatu tidak mungkin, tidak punya.

C. Di mana ini berhenti jadi tool yang tepat

Ia mengukur konfigurasi dan kemampuan, bukan performa. Throughput yang ia cetak cuma efek samping perpindahan datanya, berguna untuk menyadari ada yang lambatnya keterlaluan, dan tak berguna sebagai tolok ukur.

Ia menguji bucket yang diberitahukan kepadanya, artinya bucket yang tak tercantum di konfigurasi adalah bucket yang tak diperiksa siapa pun. Ia membuktikan keadaan server pada satu saat, dan policy yang berubah minggu depan menuntut pemeriksaannya dijalankan lagi — yang murah, sebab config file-nya sudah terlanjur ada.

Dan ia alat diagnostik sekali jalan, bukan pemantauan. Ia menjawab "apakah ini siap dijadikan sandaran" dengan baik, dan "apakah ini tetap sehat selama sebulan" sama sekali tidak. Keduanya pertanyaan berbeda dan keduanya layak punya instrumen yang berbeda.

Simpulan

Polanya sama dengan yang saya terapkan pada identity provider, dan pengulangan itulah intinya: dependency eksternal semestinya dibuktikan sebelum apa pun dibangun di atasnya, sebab alternatifnya adalah menemukan batas-batasnya lewat perilaku kode yang masih setengah jadi. Dua jam yang dipakai menyatakan kemampuan di muka membuang satu golongan kesamaran yang utuh di belakang.

Pelajaran yang lebih sempit soal apa yang boleh dianggap benar begitu saja oleh sebuah pemeriksaan. Tiap hambatan di atas lahir dari kekeliruan yang sama dalam samaran yang berbeda — memercayai laporan ketimbang menuntut bukti. Upload-nya berkata ia berhasil; konfigurasinya berkata bucket-nya privat; transfer-nya berkata ia memakai multipart. Ketiganya masuk akal, dan ketiganya layak ditanyai satu pertanyaan murah yang mengubahnya dari klaim jadi kenyataan.

Istilah

Object storage
Store yang menyimpan file utuh yang dialamati lewat nama di dalam sebuah bucket, dilayani lewat HTTP. Datar secara rancangan: tak ada direktori, tak ada penulisan sebagian di tempat.
S3-compatible
Server mana pun yang menerapkan antarmuka HTTP yang menjelma jadi standar de facto object storage, sehingga satu client bisa bekerja terhadap beberapa penerapan.
Bucket
Wadah bernama untuk object, sekaligus tingkat tempat policy akses lazimnya diputuskan.
Presigned URL
Link yang ditandatangani atas nama sebuah client, membawa signature dan masa berlakunya sendiri, supaya browser bisa upload atau download tanpa sekali pun memegang kredensial.
Bucket policy
Aturan yang memutuskan apakah object bisa dibaca tanpa kredensial — beda antara aset publik dan dokumen pribadi.
Multipart upload
Object besar yang dikirim sebagai beberapa part lalu disusun kembali server, memakai tiga operasi terpisah, bukan satu request.
ETag
Entity tag sebuah object. Untuk object multipart ia berakhir dengan suffix yang menyebut dari berapa part ia disusun, dan itulah yang menjadikannya bukti chunking terjadi.
SigV4
Skema penandatanganan yang antara lain mencakup host, dan itulah sebabnya host sebuah endpoint wajib sama persis dengan tujuan pengiriman request.
Addressing style
Apakah bucket muncul di path atau di hostname. Server yang di-hosting sendiri lazimnya menuntut bentuk path.
Public origin
Alamat yang nanti dipakai browser, dipisahkan dari endpoint tempat tool-nya menyambung, supaya link yang dibangkitkan diperiksa terhadap alamat yang didatangi client sungguhan.

Technologies covered

  • Python
  • boto3
  • MinIO
  • S3

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

Three stages in a row — development, staging and production — with an image passing from development to staging, configuration promoted from staging to production, staging fed with real data restored from a production backup, and production marked done once its core function is proven

Release Workflow: Development to Production adalah prosedur yang ditempuh tiap perubahan saya dari laptop sampai production — langkah mana yang saya otomatiskan, mana yang sengaja saya serahkan ke manusia, kenapa tiap langkah berada di tempatnya, cara saya mendiagnosis rilis yang gagal, dan cara pulihnya.

9 min read Read more →
A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

Service Rust kecil di antara aplikasi dan storage kompatibel S3: ia membagikan presigned link supaya browser berbicara langsung dengan storage, memberi tiap file siklus hidup, menyalin antar-bucket di sisi server, dan menjauhkan kredensial storage dari setiap aplikasi.

12 min read Read more →
A terminal window listing passing checks, connected to a blue storage bucket with a padlock and a green storage bucket without one.

Satu script Python yang membuktikan storage kompatibel S3 siap sebelum aplikasi bergantung padanya, dengan memakai tiap bucket persis seperti client sungguhan dan menangkap kegagalan yang tetap menjawab HTTP 200.

12 min read Read more →

Related Articles

Three sources on the left — configuration, container images, and a backup — flowing into one staging environment with an orange banner, its services, and a healthy check mark

Tiga perintah yang membangun salinan penuh production untuk satu rilis — konfigurasi dari repository, image dari registry, data dari backup terbaru — me-restore-nya berurutan, menjalankan migration rilis terhadap data itu, dan membuangnya tanpa jejak.

13 min read Read more →
A database, an object storage bucket, and a telemetry store on the left, each flowing into a stack of restore points on the right, the front one showing a timeline and a check mark

Tiga perintah yang mem-backup stack Docker yang berjalan — PostgreSQL, MinIO, dan Elasticsearch — tanpa downtime, me-restore-nya ke titik mana pun, dan menjaga retensi tetap aman, dengan exit code yang menyatakan bobot kegagalannya.

12 min read Read more →
Three stages in a row — development, staging and production — with an image passing from development to staging, configuration promoted from staging to production, staging fed with real data restored from a production backup, and production marked done once its core function is proven

Release Workflow: Development to Production adalah prosedur yang ditempuh tiap perubahan saya dari laptop sampai production — langkah mana yang saya otomatiskan, mana yang sengaja saya serahkan ke manusia, kenapa tiap langkah berada di tempatnya, cara saya mendiagnosis rilis yang gagal, dan cara pulihnya.

9 min read Read more →