สร้างเว็บฝากรูปสุดประหยัดของคุณเองบน Alibaba Cloud FC + OSS

image

บอกก่อนสามเรื่อง:

  1. เว็บฝากรูปตัวนี้ฟีเจอร์เรียบง่าย ตั้งค่าง่าย และปลอดภัยพอสมควร ถ้ามันไม่พอสำหรับคุณ ก็ส่งซอร์สโค้ดให้ AI ไปเลย — จะเพิ่มหรือลดฟีเจอร์อะไรก็เรื่องง่าย
  2. AccessKey, รหัสผ่าน, โดเมน และชื่อ bucket ทุกตัวในบทความนี้เป็น ตัวอย่าง placeholder ทั้งหมด — เปลี่ยนเป็นของคุณเองก่อนจะก๊อปอะไรไปใช้
  3. โค้ดเป็นแอป Node.js ไฟล์เดียว (ประมาณ 380 บรรทัด) โดย inject dependency ผ่าน Layer ของ FC ไม่มี hardcode ในโค้ดธุรกิจเลย และอ่านค่าคอนฟิกทั้งหมดจาก environment variable

ซอร์สโค้ด: https://github.com/ezzty/oss-upload


1. ทำไมต้องสร้างเว็บฝากรูปเอง

บล็อกสแตติกทุกอันโดยพื้นฐานแล้วต้องมีเว็บฝากรูป หลายคนประหยัดด้วยการใช้เว็บฝากรูปฟรีตลอดชีพของคนอื่น — แต่คนใจดีคนไหนล่ะที่ไม่เคยปิดตัว? แถมข้อมูลอยู่ในมือคนอื่น เราไม่มีอำนาจควบคุมอะไรเลย ก็ต้องคอยระแวงตลอดเวลา

หลังจากเจอหลุมมาเยอะพอแล้ว ผมเลยตัดสินใจสร้างเว็บฝากรูปของตัวเองในราคาถูกสุด ๆ เป้าหมายหลักคือ: ประหยัดเงิน ไม่จำกัดความจุ และปลอดภัย!

เว็บฝากรูปที่โฮสต์เองส่วนใหญ่ต้องมีเซิร์ฟเวอร์ของตัวเอง — เดือนละหลายสิบบาท แถมต้องแพตช์เอง — หรือไม่ก็รันได้แค่ในเครื่อง เลยอัปโหลดจากคอมพิวเตอร์เครื่องอื่นไม่ได้ สุดท้ายผมเลยตัดสินใจสร้างเว็บฝากรูปบนคลาวด์แบบเซิร์ฟเวอร์เลส: อัปโหลดได้ทุกที่ทุกเวลา

Alibaba Cloud Function Compute FC + OSS + CDN

FC คิดตามใช้จริง (แทบเป็น ¥0 ตอนทราฟฟิกน้อย) + ไม่มีฐานข้อมูล + ไม่ต้องดูแลระบบเลย แพ็กเกจ deploy เล็กแค่ 6KB อัปโหลดไม่บ่อยแทบไม่มีค่าใช้จ่าย

ที่เก็บรูปบน OSS ก็คิดตามใช้จริง — รูปสักหลายสิบคุญาต้องจ่ายแค่ไม่กี่หยวนต่อเดือน

เอา CDN มาวางข้างหน้าก็โหลดรูปได้ไวจนตาพร่า!

เว็บฝากรูปส่วนตัวไม่ใช่เป้าที่แฮกเกอร์อยากโจมตีเท่าไหร่ แต่ผมก็ยังเสริมความปลอดภัยไว้หลายชั้น: บังคับ PoW กัน brute force, ตรวจ magic number กันไฟล์ปลอม, เช็กขนาดสามชั้นกันยิงเพย์โหลดถล่ม, จำกัดโควตารายวันกันการใช้งานผิดวัตถุประสงค์ และ token อายุ 2 ชั่วโมงกันการขโมย

แต่ก็ใช่ว่าจะไม่มีข้อเสีย: ไม่รองรับหลายผู้ใช้ ไม่มีระบบจัดการอัลบั้ม ไม่มีสถิติการใช้งาน — จะลบรูปสักรูปยังต้องเข้า OSS console เองเลย

แต่มันเล็ก มันถูก และเหลือเฟือสำหรับใช้ส่วนตัว


2. สถาปัตยกรรมและลำดับการทำงานของ request

Browser upload.html
   │  ① POST /login   {user, pass}
   │  ← 200 {token} (valid 24h)
   │  ② POST /upload  Authorization: Bearer *** + image binary
   ▼
Alibaba Cloud Function Compute FC (Node.js 18, HTTP trigger)
   │  auth / rate limit / magic-number check / size check
   │  ③ ossClient.put(key, stream)
   ▼
OSS Bucket (private read/write, public read disabled)
   │  ④ CDN origin fetch (internal network, free)
   ▼
https://img.example.com/m/26j13/ab12cd.jpg   ← public access domain

จุดตัดสินใจสำคัญสามข้อ:

  • ตั้ง bucket เป็น private: ไม่มีใครเข้าถึงรูปด้วย direct link ได้ รูปจะเข้าถึงได้ผ่าน URL ที่ผูกกับโดเมน CDN เท่านั้น พอเปิด public read ปุ๊บ bucket ของคุณก็กลายเป็นทรัพยากรสาธารณะทันที — คนอื่นสามารถไล่ดูได้และดูดทราฟฟิกคุณไปใช้
  • ต้องล็อกอินก่อนอัปโหลด: ไม่งั้นคนจะเอาโดเมน API ของคุณไปใช้เป็นเว็บฝากรูปฟรี แล้วบิลกับพื้นที่เก็บข้อมูลก็จะพุ่งแบบคุมไม่อยู่
  • โค้ดธุรกิจไม่แตะ secret เลย: AK/SK อยู่ใน environment variable เท่านั้น — ไม่โผล่ในโค้ดหรือใน repo

3. เตรียมตัว

3.1 เปิดใช้บริการ

เปิดใช้ Function Compute FC และ Object Storage OSS (ทั้งคู่คิดตามใช้จริง ไม่ต้องซื้อล่วงหน้า)

3.2 สร้าง bucket

  • ชื่อ: ต้องไม่ซ้ำกับใครในโลก เช่น my-img
  • Region: เลือกที่ใกล้คุณ/ผู้ใช้ เช่น East China 1 (Hangzhou) → oss-cn-hangzhou
  • การควบคุมการเข้าถึง: Private (สำคัญมาก)
  • ประเภทที่เก็บข้อมูล: Standard — รูปที่เข้าถึงบ่อยควรใช้แบบนี้ อย่าเลือก Infrequent Access หรือ Archive เพราะมีระยะเวลาจัดเก็บขั้นต่ำ ถ้าลบก่อนกำหนดจะเสียเงินเพิ่มด้วย

3.3 สร้าง RAM user ที่จำกัดสิทธิ์แค่ bucket เดียว (แนะนำอย่างยิ่ง)

บทสอนหลายที่บอกให้ใช้ AccessKey ของ root account — อย่าทำเด็ดขาด ถ้า root AK รั่ว เท่ากับบัญชี Alibaba Cloud ทั้งบัญชีของคุณถูกยึด

RAM console → Policies → Create Policy → แท็บ JSON:

{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject",
        "oss:DeleteObject",
        "oss:ListObjects",
        "oss:HeadObject"
      ],
      "Resource": [
        "acs:oss:*:*:my-img",
        "acs:oss:*:*:my-img/*"
      ]
    }
  ]
}

ตั้งชื่อ policy ว่า ImageHostSingleBucket จากนั้น:

  1. RAM → Users → Create User ติ๊ก “Use permanent AccessKey for API call”
  2. ผูก เฉพาะ policy ข้างบนให้ user นี้
  3. สร้าง AK/SK แล้ว เซฟลง password manager ในเครื่องทันที

จุดตัดสินใจที่ตั้งใจไว้หลายข้อ:

  • ไม่ให้ oss:ListBuckets — ถ้ารั่ว แฮกเกอร์ยังมองไม่เห็นด้วยซ้ำว่าคุณมี bucket อะไรบ้าง
  • ไม่ให้ oss:CopyObject — เว็บฝากรูปไม่ต้องใช้สิทธิ์คัดลอก ให้เท่าที่จำเป็นที่สุด
  • อย่าผูก AliyunOSSFullAccess — นั่นจะทำให้ AK ของเว็บฝากรูปอ่าน data bucket ทุกอันที่คุณมีได้

4. เตรียม Layer ของ dependency

โค้ดธุรกิจพึ่งพาแค่ ali-oss ตัวเดียว เอามันไปไว้ใน Layer แล้วแพ็กเกจโค้ดก็จะเหลือแค่ไม่กี่ KB

ทางเลือก A: ดาวน์โหลด asset จาก release ตรง ๆ (แนะนำ)

โปรเจกต์มี Release เผยแพร่ไว้แล้ว — ทั้ง layer.zip และแพ็กเกจโค้ดดาวน์โหลดได้ตรง ๆ:

https://github.com/ezzty/oss-upload/releases/latest

Asset ขนาด เนื้อหา
layer.zip 3.25 MB ali-oss@6.23.0 และ dependency ของมัน — รวมทั้งหมด 2338 ไฟล์
oss-upload-code.zip ~6 KB index.js + package.json แพ็กเกจ deploy ของฟังก์ชัน

ข้อดี: ไม่ต้องติดตั้ง Node.js ไม่ต้อง zip เอง และไม่มีปัญหาว่า “ไม่ตรงกับที่คนเขียนเทสต์ไว้” เพราะ npm install ในเครื่องคุณดึง SDK เวอร์ชันใหม่กว่า ใน layer มี แค่ซอร์สโค้ดของ npm package ไม่มี secret

ทางเลือก B: สร้างเอง

เมื่ออยากใช้ dependency เวอร์ชันอื่น หรือไม่ไว้ใจแพ็กเกจที่คนอื่นทำไว้:

mkdir -p layer-build/nodejs
cd layer-build
npm init -y
npm install ali-oss
# The directory MUST have this structure:
#   nodejs/
#     node_modules/
#       ali-oss/
#       ...its transitive dependencies
cd nodejs && zip -r ../layer.zip node_modules && cd ..

อัปโหลด

FC console → Layer Management → Create Layer:

  • ชื่อ: oss-upload-deps
  • Runtime: Node.js 18
  • อัปโหลด layer.zip

สร้าง layer ครั้งเดียวก็พอ ถ้าจะอัปเกรด ali-oss ทีหลัง ก็แพ็กเวอร์ชันใหม่ เผยแพร่ แล้วชี้ฟังก์ชันไปที่ layer เวอร์ชันใหม่ — ไม่ต้องแก้โค้ดธุรกิจเลย


5. Deploy ฟังก์ชัน

5.1 เอาโค้ดธุรกิจมา

แนะนำ: ใช้ asset จาก release ตรง ๆ — ดาวน์โหลด oss-upload-code.zip (~6 KB) จาก https://github.com/ezzty/oss-upload/releases/latest แตกไฟล์ก็ได้แพ็กเกจ deploy มาตรฐานเลย

ถ้าจะแพ็กเอง ใน root ของ zip ต้องมี แค่สองไฟล์นี้เท่านั้น (ห้ามมีโฟลเดอร์ห่อ ไม่งั้น Handler จะหา index.handler ไม่เจอ):

code.zip
├── index.js        ← business source
└── package.json    ← declares dependencies (for reference; actually loaded from the layer at runtime)
zip code.zip index.js package.json

5.2 สร้างฟังก์ชัน

  • Runtime: Node.js 18
  • Handler: index.handler
  • Spec: 0.05 vCPU / 128 MB memory / 512 MB disk (สเปกเล็กสุดที่ใช้งานได้จริง)
  • Layer configuration: ผูก layer oss-upload-deps ที่เพิ่งสร้าง

ระวังเรื่องหน่วยความจำ: โค้ดอ่าน body ทั้งก้อนเข้า memory (bodyBuf) และการถอด base64 ก็สำเนาอีกชุด 128 MB พอสำหรับรูปทั่วไป แต่ถ้าคุณเพิ่มเพดานอัปโหลดเป็น 20 MB และอัปโหลดรูปใกล้เพดานบ่อย ๆ ก็เพิ่มหน่วยความจำตามไปด้วย

5.3 Environment variable (ต้องมี 8 ตัว — ขาดตัวใดตัวหนึ่งก็ไม่สตาร์ท)

environment variable ของฟังก์ชันมี 8 ตัวที่ต้องใส่ — ตั้งค่าตามในรูป

  • AUTH_USER admin — ชื่อผู้ใช้ล็อกอิน
  • AUTH_PASS <ตั้งรหัสผ่านที่แข็งแรง> — รหัสผ่านล็อกอิน ใช้สตริงสุ่มยาว ๆ
  • JWT_SECRET <สตริงสุ่ม 32+ ตัวอักษร> — secret สำหรับเซ็น token
  • OSS_ACCESS_KEY_ID LTAIxxxxxxxxxxxxxxxx — AK ของ RAM user
  • OSS_ACCESS_KEY_SECRET <AccessKey Secret ของคุณ> — SK ของ RAM user
  • OSS_BUCKET my-img — ชื่อ bucket
  • OSS_REGION oss-cn-hangzhou — region ID ของ bucket
  • CDN_DOMAIN https://img.example.com — ต้องมี protocol scheme ด้วย ไม่งั้นจะสตาร์ทไม่ขึ้นทันที

ไม่บังคับ 1 ตัว:

  • ALLOWED_ORIGINS example.com,myblog.net — origin ที่อนุญาตให้ cross-origin คั่นด้วยจุลภาค ถ้าไม่ใส่ฟังก์ชันก็ยังสตาร์ทได้; ค่านี้จะถูกใช้ เฉพาะตอนเปิด CORS ระดับโค้ด เท่านั้น (โดยดีฟอลต์แพลตฟอร์มแค่ echo Origin กลับไป เลยไม่เคยเรียก isValidOrigin เลย) ถ้าเปิด CORS ระดับโค้ดจริง ๆ ต้องตั้งค่านี้ ไม่งั้นค่า fallback example.com จะบล็อกการอัปโหลดจากเบราว์เซอร์ทั้งหมด

image

5.4 ผูกโดเมนของตัวเอง

FC console → Domain Management → Add Domain แล้วทำตามขั้นตอนเพื่อตั้งค่า CNAME กับ certificate นั่นคือวิธีที่คุณจะได้ที่อยู่ API แบบ https://api.example.com


6. หน้า frontend

upload.html เป็น frontend ไฟล์เดียว — เอาไปวางในโฟลเดอร์เว็บของคุณได้เลย

ต้องแก้อันนึง — หา _FC_PARTS:

// Change this to your Function Compute custom domain
const _FC_PARTS = ['https://', 'api.example.com'];

เพดานขนาดไฟล์ต้องตรงกับฝั่ง backend (ทั้งคู่ดีฟอลต์ 20MB):

if (file.size > 20 * 1024 * 1024) { /* block */ }

แต่ละฝั่งมีเพดานของตัวเอง — อันที่เล็กกว่าชนะ แก้แค่ frontend ไฟล์ใหญ่จะโดน 413 จากฝั่งเซิร์ฟเวอร์; แก้แค่ backend ผู้ใช้จะโดนบล็อกในเบราว์เซอร์ก่อนที่ request จะออกไปด้วยซ้ำ


7. อยากลงลึกกว่านี้? รายละเอียดหลักอยู่ใน README ของซอร์ส

นี่คือขั้นตอนครบถ้วนในการทำให้เว็บฝากรูปที่โฮสต์เองรันได้ ทำไมโค้ดถึงเขียนแบบนี้ รายละเอียดการทำงานของแต่ละชั้นป้องกัน และเช็กลิสต์ตรวจรับก่อนเปิดใช้ อยู่ใน README ของ repo ซอร์สทั้งหมด — เปิดดูตอนต้องใช้ได้เลย:

https://github.com/ezzty/oss-upload → หัวข้อ “Core implementation (code level)” และ “Pre-launch acceptance” ใน README.md

บทความบล็อกนี้แค่อยากอธิบายว่า “ทำไมต้องโฮสต์เอง ทำยังไงให้รันได้ และต้องระวังอะไร” ตอน deploy จริง README คือเอกสารเทคนิคฉบับสมบูรณ์


8. ค่าใช้จ่าย

คิดตามใช้จริง แบ่งเป็นสามส่วน:

รายการ การคิดเงิน ขนาด
FC compute Compute CU (vCPU + memory × duration) + จำนวนครั้งที่เรียก มีโควตาฟรี; เว็บฝากรูปส่วนตัวมักอยู่ใกล้ ๆ โควตานี้
OSS storage ¥/GB/เดือน Standard storage ประมาณ ¥0.09–0.12/GB/เดือน
OSS requests ต่อ request น้อยมากจนไม่ต้องคิด
CDN traffic ¥/GB ดูด้านล่าง

ทราฟฟิกคือค่าใช้จ่ายก้อนใหญ่ที่สุด และปรับได้ง่ายที่สุด:

  • อัปโหลดเข้า OSS: ฟรี
  • OSS internal network (FC ↔ OSS ใน region เดียวกัน): ฟรี
  • OSS ขาออกทาง public internet: คิดเงิน (ประมาณ ¥0.5/GB ขึ้นกับช่วงเวลาและ region)
  • ผ่าน CDN: CDN origin fetch ใช้ internal network ทราฟฟิกต้นทางเลยฟรี — จ่ายแค่ค่า CDN traffic

สรุป: เอา CDN มาวางหน้าเว็บฝากรูปเสมอ — เร็วขึ้นและเลี่ยงค่า egress ของ OSS ได้ด้วย และจำไว้ว่า FC กับ OSS ต้องอยู่ region เดียวกัน ถ้าคนละ region จะวิ่งผ่าน public internet ซึ่งช้าและอาจมีค่าใช้จ่ายทราฟฟิกเพิ่ม

จากการทดสอบของผมเอง อัปโหลดรูปสักหลายสิบรูปต่อเดือนจ่ายประมาณ ¥1 ต่อเดือน


9. การดูแลและอัปเดต

แก้โค้ดธุรกิจ (เช่น รูปแบบ URL ของรูป):

  1. แก้ index.js ในเครื่อง
  2. zip code.zip index.js package.json
  3. Console → Code Deployment → Upload → Deploy
  4. รัน acceptance probe ใน README เพื่อยืนยันว่าเวอร์ชันใหม่ขึ้นจริง

แก้ frontend:

  1. แก้ upload.html
  2. อัปโหลดขึ้นเว็บ; ลองตั้ง Cache-Control: no-cache ไว้ด้วย ไม่งั้นผู้ใช้อาจได้เวอร์ชันเก่า

อัปเกรด dependency:

  1. รัน npm install ali-oss ใหม่แล้วแพ็ก layer.zip
  2. เผยแพร่ layer เวอร์ชันใหม่ใน console
  3. สลับฟังก์ชันไปใช้ layer เวอร์ชันใหม่

หมุนเวียน key (ทำเป็นระยะ ๆ):

  1. สร้าง AK/SK ใหม่ใน RAM
  2. อัปเดต environment variable ของ FC → deploy
  3. พอยืนยันว่าบริการยังปกติแล้ว ปิดใช้งาน AK ตัวเก่าใน RAM console

10. อ้างอิง