Data lake dari chapter sebelumnya menumpu pada satu jenis fondasi: object storage. Layanan ini menyediakan kapasitas nyaris tanpa batas dengan biaya per gigabyte rendah, itulah sebabnya hampir semua data lake modern — baik di cloud maupun on-premise — dibangun di atas object storage. Sebelum merancang arsitektur lake, kita perlu memahami cara kerjanya, konvensi penamaannya, dan pola mengaksesnya dari aplikasi.
Object Storage vs File System #
File system mengatur data dalam direktori bertingkat yang bisa diubah isinya: kita bisa membuka berkas, mengganti sebagian isinya, lalu menutupnya lagi. Object storage bekerja dengan model berbeda. Setiap unit data disebut object dan dikenal lewat sebuah key — string datar seperti transaksi/2026/01/15/pesanan.jsonl. Penulisan selalu mengganti object secara utuh; tidak ada operasi mengubah sebagian isi. Kompensasinya adalah skala: namespace yang datar bisa menampung miliaran object tanpa perlu mengelola struktur direktori.
| Aspek | File System | Object Storage |
|---|---|---|
| Identitas data | path bertingkat dalam direktori | key datar dalam bucket |
| Mengubah isi | sebagian berkas bisa ditimpa | object selalu ditulis utuh |
| Skala | terbatas kapasitas satu mesin | nyaris tanpa batas |
| Semantik | blok, inode, kunci berkas | HTTP API: put, get, delete, list |
Bucket dan Path Convention #
Bucket adalah wadah paling atas yang mengelompokkan object, semacam direktori root milik satu tim atau satu lingkungan. Di dalam bucket, path biasanya ditata mengikuti pola s3://nama-bucket/prefix/nama-berkas, di mana prefix adalah bagian key yang berfungsi seperti folder. Tiga layanan utama di cloud memakai pola yang serupa:
| Layanan | Penyedia | Contoh path |
|---|---|---|
| Amazon S3 | AWS | s3://data-toko/transaksi/2026/01/pesanan.parquet |
| Google Cloud Storage | Google Cloud | gs://data-toko/transaksi/2026/01/pesanan.parquet |
| Azure Data Lake Storage | Microsoft Azure | abfs://[email protected]/transaksi/2026/01/ |
Konvensi penamaan prefix biasanya ditetapkan sejak awal proyek: sumber data, lalu granularitas, lalu tanggal. Penamaan yang konsisten membuat operasi listing berdasarkan prefix menjadi murah karena layanan cukup memfilter key yang berawalan sama.
Pola Akses: Presigned URL #
Sebagian besar akses terjadi lewat API dengan kredensial layanan. Namun ada kalanya pihak luar perlu membaca satu object tanpa boleh menerima kredensial permanen — misalnya aplikasi mobile mengunduh laporan pelanggan. Untuk kasus ini layanan object storage menyediakan presigned URL: tautan bermasa berlaku singkat (menit sampai beberapa hari) yang dihitung dari kredensial pemilik object. Setelah lewat masa berlaku, tautan ditolak. Mekanisme ini terdokumentasi resmi di panduan presigned URL Amazon S3 .
MinIO untuk Development #
Menyiapkan bucket di cloud sejak tahap development memakan biaya dan memperlambat iterasi. Praktik yang lazim adalah menjalankan layanan object storage open source yang kompatibel dengan S3 API di mesin lokal — pilihan populer adalah MinIO, yang bisa dipasang sebagai satu binary atau container. Aplikasi cukup mengarah endpoint-nya ke http://localhost:9000, lalu saat berproduksi endpoint ditukar ke S3 sungguhan tanpa mengubah kode. Untuk tim yang juga memakai layanan AWS lain, LocalStack menyediakan alternatif berupa emulator AWS lokal; untuk kebutuhan object storage saja, MinIO lebih ringan. Dokumentasi resmi MinIO tersedia di min.io/docs
.
Praktik Lokal: Client Object Storage Mini #
Untuk memahami semantik put, get, delete, dan list, kita bisa meniru object storage dengan pathlib: setiap key dipetakan menjadi satu berkas di bawah folder bucket. Kode berikut membuat class MiniObjectStore lalu menggunakannya layaknya client S3:
import hashlib
import json
import pathlib
import shutil
# Root meniru layanan object storage: satu direktori = satu bucket.
ROOT = pathlib.Path(__file__).resolve().parent / "objectstore" / "data-toko"
shutil.rmtree(ROOT, ignore_errors=True)
class MiniObjectStore:
"""Client object storage mini: setiap key dipetakan ke satu berkas."""
def __init__(self, root: pathlib.Path):
self.root = root
self.root.mkdir(parents=True, exist_ok=True)
def _jalur(self, key: str) -> pathlib.Path:
# Namespace datar: key apa pun hanyalah string, divalidasi lalu
# dipetakan ke path di bawah root bucket.
if not key or key.startswith("/") or ".." in key.split("/"):
raise ValueError(f"key tidak valid: {key!r}")
return self.root / key
def put(self, key: str, data: bytes) -> dict:
jalur = self._jalur(key)
jalur.parent.mkdir(parents=True, exist_ok=True)
jalur.write_bytes(data)
return {"key": key, "ukuran": len(data),
"etag": hashlib.md5(data).hexdigest()[:8]}
def get(self, key: str):
jalur = self._jalur(key)
if not jalur.is_file():
return None
return jalur.read_bytes()
def delete(self, key: str) -> bool:
jalur = self._jalur(key)
if jalur.is_file():
jalur.unlink()
return True
return False
def list(self, prefix: str = "") -> list[str]:
kunci = []
for p in self.root.rglob("*"):
rel = p.relative_to(self.root).as_posix()
if p.is_file() and rel.startswith(prefix):
kunci.append(rel)
return sorted(kunci)
store = MiniObjectStore(ROOT)
# Ingestion harian: dua berkas transaksi dan satu berkas katalog.
baris = {"id": 9001, "pelanggan": "Budi", "email": "[email protected]",
"produk": "Kopi Gayo 250g", "jumlah": 2, "total": 90000}
payload = (json.dumps(baris) + "\n").encode()
print(store.put("transaksi/2026/01/15/pesanan.jsonl", payload))
store.put("transaksi/2026/01/16/pesanan.jsonl", payload)
store.put("katalog/produk.json", b'{"produk": "Kopi Gayo 250g", "harga": 45000}')
print("semua key:", store.list())
print("prefix transaksi:", store.list("transaksi/2026/01/"))
print("key s3:", "s3://data-toko/" + store.list("transaksi/2026/01/15/")[0])
isi = store.get("transaksi/2026/01/15/pesanan.jsonl")
print("get:", json.loads(isi)["pelanggan"], "-", json.loads(isi)["produk"])
print("delete:", store.delete("transaksi/2026/01/15/pesanan.jsonl"))
print("delete ulang:", store.delete("transaksi/2026/01/15/pesanan.jsonl"))
print("setelah delete:", store.list("transaksi/2026/01/"))
try:
store.put("../rahasia/aturan.txt", b"tidak boleh")
except ValueError as galat:
print("key berbahaya ditolak:", galat)Output yang Dihasilkan:
{'key': 'transaksi/2026/01/15/pesanan.jsonl', 'ukuran': 120, 'etag': '3a1cfac5'}
semua key: ['katalog/produk.json', 'transaksi/2026/01/15/pesanan.jsonl', 'transaksi/2026/01/16/pesanan.jsonl']
prefix transaksi: ['transaksi/2026/01/15/pesanan.jsonl', 'transaksi/2026/01/16/pesanan.jsonl']
key s3: s3://data-toko/transaksi/2026/01/15/pesanan.jsonl
get: Budi - Kopi Gayo 250g
delete: True
delete ulang: False
setelah delete: ['transaksi/2026/01/16/pesanan.jsonl']
key berbahaya ditolak: key tidak valid: '../rahasia/aturan.txt'Mari kita telaah baris-baris kode di atas:
- Baris 7-8 (ROOT dan rmtree): folder
objectstore/data-tokoberperan sebagai bucketdata-toko;rmtreemembersihkan sisa percobaan sebelumnya agar contoh bisa diulang dengan hasil sama. - Baris 20-23 (
_jalur): seluruh method melewati fungsi ini. Key kosong, key berawalan/, dan key yang memuat..ditolak supaya pemanggil tidak bisa keluar dari root bucket. - Baris 25-29 (
put): menulis object selalu utuh —write_bytesmengganti seluruh isi berkas. Nilai kembalian meniru respons layanan sungguhan: key, ukuran, danetagyang dihitung dari konten. - Baris 44-50 (
list): prefix hanyalah filterstartswithpada string; hasil diurutkan supaya keluaran stabil, meniruListObjectsV2di S3. - Baris 72-74 (delete dua kali):
deletemengembalikanTruelaluFalse— object storage idempotent terhadap penghapusan object yang sudah tidak ada.
Dengan semantik put, get, delete, dan list yang kini kita pahami, langkah berikutnya adalah menyusun data di dalam storage menjadi zona-zona dengan tingkat kematangan berbeda. Penyusunan itu dibahas pada chapter Data Lake Architecture.