Object Storage (S3/R2/GCS) — DevOps

Object Storage untuk Web Developer Object storage menyimpan file (gambar, video, backup, static assets) sebagai objek di cloud. Berbeda dari filesystem…

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.jpg jam 10:00:00 WIB dengan expiresIn: 300. Temanmu baru membukanya jam 10:06:00. Apa jawaban S3? Dan kalau dia mengubah X-Amz-Expires=300 di URL itu jadi 3600? 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:

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",
}));

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.

Upload langsung dari browser

Untuk upload, server membuat presigned URL dengan PutObjectCommand, dan browser mengirim file-nya langsung ke bucket:

  1. Browser: POST /lampiran/presign dengan nama dan ukuran file.
  2. catatan-api memeriksa login, ukuran, dan tipe file, memilih key acak (lampiran/9b1e4c77.mp4), lalu menjawab presigned URL PUT dengan expiresIn: 300.
  3. Browser: fetch(url, { method: "PUT", body: file }) langsung ke bucket.
  4. 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 } }
  ]
}

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari