在前幾天,我們已經完成了 EC2 主機部署 以及 RDS 資料庫串接,
一個完整的後端雲端架構也漸漸成形。
但如果今天你的應用要讓使用者能上傳圖片、影片、報表、PDF…
這些「檔案」要放哪裡呢?
這時候,主角就登場了 —— AWS S3 (Simple Storage Service)。
💡 S3 是 AWS 三大基礎服務之一(EC2、RDS、S3),
它就像你在雲端的「硬碟」,負責安全地儲存、管理檔案。
今天我們就要實際操作:
從建立 S3 Bucket → 設定權限 → 實作 Node.js 上傳功能,
一步步完成「檔案雲端化」的實戰練習!
S3 的資料儲存設計達到「99.999999999%(11個9)」的耐久性,
意思是:你的圖片幾乎不可能遺失。
你只需為實際用到的空間與流量付費,
不用像傳統伺服器那樣一直維護硬碟或備份。
S3 的檔案可以透過 URL 在全世界直接被讀取,
搭配 CDN(如 CloudFront)後,速度更是飛快。
與 EC2、Lambda、CloudFront、IAM、Route53 等無縫結合
不論是後端 API、影片平台、或電商後台都能輕鬆擴充。
登入 AWS Console
進入 S3 服務
建立新的 Bucket
點擊「 建立儲存貯體 」按鈕
AWS Region: 選擇離您最近的區域(例如:ap-northeast-1
東京)
Bucket name: 輸入唯一名稱(例如:my-ithome-2025-bucket
)
注意:Bucket 名稱必須是全球唯一的
設定 Object Ownership
選擇「ACLs disabled (recommended)」
Block Public Access settings
取消勾選「Block all public access」
勾選確認框(我了解這會讓檔案變成公開存取)
⚠️ 注意:如果您想讓上傳的圖片可以公開存取,需要取消封鎖
Bucket Versioning
其他設定保持預設,點擊「 建立儲存貯體 」
點進剛建立的 Bucket
前往「Permissions」標籤
找到「Bucket policy」區塊,點擊「Edit」
貼上以下 Policy(記得替換 YOUR-BUCKET-NAME
):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}
左側選單點選「Users」
點擊「Create user」
User name: 輸入 s3-upload-user
點擊「Next」
選擇「Attach policies directly」
搜尋並勾選「AmazonS3FullAccess」(簡單起見;生產環境建議使用更細緻的權限)
點擊「Next」,然後「Create user」
點進剛建立的使用者
選擇「Security credentials」標籤
找到「Access keys」區塊,點擊「Create access key」
選擇「Application running outside AWS」
點擊「Next」
描述標籤:輸入「Node.js Upload App」
點擊「Create access key」
重要! 複製並儲存:(等等需在 .env
設定)
AKIAIOSFODNN7EXAMPLE
)wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
)npm install @aws-sdk/client-s3
npm install --save-dev @types/node
在您的 .env
檔案加入:
# AWS S3 設定
AWS_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_S3_BUCKET_NAME=my-ithome-2025-bucket
建立新檔案:src/utils/s3Utils.ts
import { S3Client } from "@aws-sdk/client-s3";
// 初始化 S3 Client
export const s3Client = new S3Client({
region: process.env.AWS_REGION || "ap-northeast-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "",
},
});
export const bucketName = process.env.AWS_S3_BUCKET_NAME || "";
修改 src/controllers/uploadController.ts
,新增一個使用 S3 的函式:
import { Response, NextFunction } from "express";
import path from "path";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { s3Client, bucketName } from "../utils/s3Utils";
import { AuthRequest } from "../middleware/isAuth";
import { AppDataSource } from "../config/db";
import { User } from "../entities/User";
/**
* 上傳大頭照到 AWS S3
*/
export async function uploadAvatarToS3(req: AuthRequest, res: Response, next: NextFunction) {
try {
// 1. 檢查是否有上傳檔案
if (!req.file) {
res.status(400).json({
status: "failed",
message: "請選擇要上傳的圖片檔案",
});
return;
}
// 2. 檢查使用者是否已登入
if (!req.user) {
res.status(401).json({
status: "failed",
message: "請先登入",
});
return;
}
// 3. 產生檔案路徑與名稱
const timestamp = Date.now();
const ext = path.extname(req.file.originalname).toLowerCase();
const key = `images/avatars/user-${req.user.id}-${timestamp}${ext}`;
// 4. 上傳到 S3
const command = new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: req.file.buffer,
ContentType: req.file.mimetype,
});
await s3Client.send(command);
// 5. 產生公開 URL
const region = process.env.AWS_REGION || "ap-northeast-1";
const publicUrl = `https://${bucketName}.s3.${region}.amazonaws.com/${key}`;
// 6. 更新資料庫
await AppDataSource.getRepository(User).update(
{ id: req.user.id },
{ profileUrl: publicUrl }
);
// 7. 回傳成功訊息
res.status(200).json({
status: "success",
message: "大頭照上傳成功",
data: { avatarUrl: publicUrl },
});
} catch (err) {
next(err);
}
}
修改 src/routes/uploadRoutes.ts
:
import { Router } from "express";
import { uploadAvatar, uploadAvatarToS3 } from "../controllers/uploadController";
import { imageUpload } from "../middleware/imageUpload";
import { isAuth } from "../middleware/isAuth";
const router = Router();
// Firebase 版本(保留原有功能)
router.post(
"/avatar",
isAuth,
imageUpload.single("file"),
uploadAvatar,
);
// AWS S3 版本(新增)
router.post(
"/avatar-s3",
isAuth,
imageUpload.single("file"),
uploadAvatarToS3,
);
export default router;
使用 Postman 測試:
查看資料庫 profile_url
欄位 : (成功填入檔案位址)
到 S3 查看檔案是否已經上傳 :
成功驗證檔案上傳!
.env
在 .gitignore
中這次的實作,我們練習了:
雖然看起來步驟很多,但這正是「真實世界後端」會遇到的流程。
加油!
commit :
feat: 🎸 integrate AWS S3 upload feature
新增 AWS S3 檔案上傳功能(s3Utils + uploadController + route)