Object storage menyimpan file sebagai objek di dalam bucket: tiap objek punya nama unik (key), isi file-nya, dan metadata, dan semuanya diakses lewat HTTP API, bukan lewat folder di disk server. AWS S3 yang memulainya, dan sekarang banyak penyedia memakai API yang sama (S3-compatible): Cloudflare R2, DigitalOcean Spaces, Google Cloud Storage, atau MinIO yang bisa kamu jalankan sendiri. Bayangin tempat penitipan barang di mal. Kamu menyerahkan tas, petugas memberi tiket bernomor, dan tas itu cuma bisa diambil dengan nomor itu. Ruang penitipannya gak punya lorong atau lemari bernama, cuma rak dengan nomor. Kalau temanmu yang mau mengambil, petugas bisa memberi tiket yang dicap jam dan diparaf: berlaku sampai jam sekian, dan kalau nomornya dicoret lalu diganti, parafnya gak cocok lagi. Tiket bercap itu adalah presigned URL.
🧪 Tebakan cepat: catatan-api membuat presigned URL untuk
lampiran/a4c2e9f1.jpgjam 10:00:00 WIB denganexpiresIn: 300. Temanmu baru membukanya jam 10:06:00. Apa jawaban S3? Dan kalau dia mengubahX-Amz-Expires=300di URL itu jadi3600? Tebak dulu, jawabannya ada di bagian "Presigned URL: tiket yang kedaluwarsa".
Kenapa lampiran keluar dari disk web-1
Sejak pelajaran pertama, lampiran catatan-api disimpan di /srv/catatan-api/storage/lampiran di web-1. Itu bekerja, sampai:
- Disk-nya terbatas. Disk
web-125 GB, dan tiap lampiran foto 200 KB sampai beberapa MB. Disk penuh membuat database ikut gagal menulis. - Cuma satu server yang punya file-nya. Container kedua, server kedua, atau pod Kubernetes lain gak melihat folder itu.
- Semua byte lewat Node. Upload video 500 MB berarti 500 MB mengalir lewat proses Express dan memorinya.
- Backup-nya terpisah lagi. Folder itu harus ikut di-backup dan di-drill, seperti database.
Bucket menyelesaikan keempatnya: kapasitasnya praktis gak terbatas (bayar per GB), semua server membaca bucket yang sama, file bisa diunggah dan diunduh langsung tanpa lewat Node, dan penyedia menyimpan beberapa salinan tiap objek.
Bucket, key, dan objek
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "ap-southeast-3" }); // Jakarta; kredensial dari env
await s3.send(new PutObjectCommand({
Bucket: "catatan-lampiran",
Key: `avatars/${userId}.jpg`,
Body: fileBuffer,
ContentType: "image/jpeg",
}));
Bucketadalah wadahnya, namanya unik di seluruh penyedia.Keyadalah nama unik objek di dalam bucket, ditulis mirip path:avatars/7.jpg. Bukan kunci rahasia, bukan folder fisik:avatars/cuma bagian dari namanya, dan gak ada folder yang perlu dibuat dulu. Upload ke key yang sama menimpa objek lama.- Objek = isi file (
Body) + metadata (ContentType, ukuran, waktu). Kredensial (AWS_ACCESS_KEY_IDdanAWS_SECRET_ACCESS_KEY) dibaca SDK dari environment variable, seperti di pelajaran Environment Variables & Secrets.
Isi bucket di Lab:
| Key | Ukuran | Storage class |
|---|---|---|
avatars/7.jpg |
38.204 byte | STANDARD |
lampiran/a4c2e9f1.jpg |
248.913 byte | STANDARD |
lampiran/7f3a9c1e.jpg |
1.204.877 byte | STANDARD |
backup/catatan-2026-10-08.sql.gz |
410.112 byte | STANDARD |
Public atau private
Bucket baru di S3 bersifat private, dan fitur Block Public Access-nya menyala bawaan. Request tanpa tanda tangan dijawab 403 Forbidden dengan <Code>AccessDenied</Code>. Cuma server yang memegang kredensial yang bisa membaca dan menulis.
Membuat sebagian bucket public (siapa pun boleh GET) butuh dua langkah sengaja: matikan Block Public Access, lalu tulis bucket policy yang mengizinkan s3:GetObject. Itu cocok untuk file yang memang untuk semua orang, seperti avatar di profil publik atau aset frontend, biasanya disajikan lewat CDN. Lampiran di catatan pribadi tetap private.
Presigned URL: tiket yang kedaluwarsa
File private tetap bisa dibagikan sebentar lewat presigned URL: URL biasa ke objek itu, ditambah waktu dibuat, masa berlaku, dan tanda tangan yang dihitung server dari kredensialnya. Siapa pun yang memegang URL-nya boleh GET objek itu sampai masa berlakunya habis.
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const url = await getSignedUrl(s3, new GetObjectCommand({
Bucket: "catatan-lampiran",
Key: "lampiran/a4c2e9f1.jpg",
}), { expiresIn: 300 }); // 5 menit
URL yang dihasilkan, dipecah per bagian:
| Bagian | Isi | Artinya |
|---|---|---|
| host + path | catatan-lampiran.s3.ap-southeast-3.amazonaws.com/lampiran/a4c2e9f1.jpg |
bucket dan key |
X-Amz-Credential |
AKIA.../20261009/ap-southeast-3/s3/aws4_request |
kunci siapa yang menandatangani |
X-Amz-Date |
20261009T030000Z |
dibuat 03:00:00 UTC = 10:00:00 WIB |
X-Amz-Expires |
300 |
berlaku 300 detik |
X-Amz-Signature |
64 karakter hex | HMAC-SHA256 dari method, key, dan semua parameter di atas |
S3 menghitung X-Amz-Date + X-Amz-Expires = 10:05:00 WIB. Dibuka jam 10:06:00, jawabannya 403 Forbidden dengan <Code>AccessDenied</Code><Message>Request has expired</Message>. Mengubah X-Amz-Expires jadi 3600 gak menolong: tanda tangannya dihitung dari angka 300, jadi S3 menjawab <Code>SignatureDoesNotMatch</Code>. Itu jawaban tebakannya. Mengganti key di URL ke lampiran orang lain juga berakhir di SignatureDoesNotMatch. Di Lab, tanda tangannya dihitung dengan algoritma SigV4 yang sama, dan kamu bisa mencoba ketiganya.
- Masa berlaku maksimal presigned URL SigV4 adalah 7 hari. Pakai yang sependek mungkin: halaman yang menampilkan lampiran cukup 5 menit.
X-Amz-Dateselalu UTC. Jam server yang meleset membuat URL baru sudah kedaluwarsa atau ditolak.
Upload langsung dari browser
Untuk upload, server membuat presigned URL dengan PutObjectCommand, dan browser mengirim file-nya langsung ke bucket:
- Browser:
POST /lampiran/presigndengan nama dan ukuran file. - catatan-api memeriksa login, ukuran, dan tipe file, memilih key acak (
lampiran/9b1e4c77.mp4), lalu menjawab presigned URLPUTdenganexpiresIn: 300. - Browser:
fetch(url, { method: "PUT", body: file })langsung ke bucket. - Browser memberi tahu catatan-api key-nya, dan catatan-api menyimpannya di tabel
lampiran.
Video 500 MB gak pernah lewat server API, jadi server gak kewalahan. Bucket-nya butuh aturan CORS yang mengizinkan PUT dari https://catatan.example.com, karena browser memanggil domain lain.
Lifecycle rules dan storage class
Storage class menentukan harga dan kecepatan akses objek:
| Storage class | Untuk | Catatan |
|---|---|---|
| Standard | file yang sering dibuka: lampiran, avatar | latency rendah, harga simpan paling mahal |
| Infrequent Access | jarang dibuka tapi harus cepat saat dibutuhkan | simpan lebih murah, ada biaya per pengambilan |
| Glacier / Archive | backup jangka panjang yang hampir gak pernah dibuka | paling murah, pengambilannya lambat (menit sampai jam) |
Lifecycle rule memindahkan atau menghapus objek otomatis berdasarkan umurnya. Backup harian dari pelajaran Database DevOps bisa disalin ke backup/, lalu:
{
"Rules": [
{ "ID": "hapus-tmp", "Filter": { "Prefix": "tmp/" }, "Status": "Enabled",
"Expiration": { "Days": 1 } },
{ "ID": "arsip-backup", "Filter": { "Prefix": "backup/" }, "Status": "Enabled",
"Transitions": [{ "Days": 30, "StorageClass": "GLACIER" }],
"Expiration": { "Days": 365 } }
]
}
tmp/(upload yang gak pernah selesai) dihapus setelah 1 hari.backup/pindah ke Glacier setelah 30 hari, dan dihapus setelah 365 hari.- S3 menjalankan lifecycle sekitar sekali sehari, bukan tepat di detiknya. Cloudflare R2 punya Standard dan Infrequent Access, tanpa kelas arsip.
⚠️ Jebakan yang sering ketemu
- Bucket publik tanpa sengaja. Lampiran pribadi bisa dibuka siapa pun yang menebak key-nya. Biarkan Block Public Access menyala; buka publik cuma untuk prefix yang memang publik.
- Presigned URL berumur panjang. URL yang berlaku 7 hari dan bocor di chat bisa dibuka siapa saja selama 7 hari. Buat baru tiap kali dibutuhkan, dengan umur menit.
- Kredensial di frontend. Access key yang ikut ter-bundle ke JavaScript browser memberi semua orang akses penuh ke bucket. Kredensial cuma di server; browser cukup memegang presigned URL.
- Key dari nama file pengguna. Dua pengguna upload
foto.jpgke key yang sama, dan yang kedua menimpa yang pertama. Pakai key acak.
TL;DR: Object storage (S3, R2, GCS, Spaces, MinIO) menyimpan objek di bucket lewat HTTP API. Key adalah nama unik objek di dalam bucket, ditulis mirip path (avatars/${userId}.jpg), bukan folder fisik dan bukan kunci rahasia. Bucket private bawaannya (Block Public Access); yang publik cuma aset untuk semua orang, biasanya lewat CDN. Presigned URL memberi akses sementara ke objek private: X-Amz-Date (UTC) + X-Amz-Expires menentukan batasnya, dan tanda tangan SigV4 membuat key, method, dan masa berlakunya gak bisa diubah (Request has expired, SignatureDoesNotMatch). Upload besar memakai presigned PUT dengan expiresIn: 300, jadi file langsung ke bucket tanpa lewat server. Storage class: Standard untuk yang sering dibuka, Infrequent Access untuk yang jarang, Glacier/Archive untuk backup jangka panjang; lifecycle rule memindahkan dan menghapusnya otomatis.