iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Build on AWS

從一個網站的誕生,看懂 AWS 架構與自動化的全流程!系列 第 19

Day 19 檔案後處理:S3 x Lambda x Step Functions 自動化壓縮與掃毒

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20251003/201727433eApHm5hck.png

一、前言

使用者上傳檔案到雲端後,若沒有進一步的處理,會帶來安全風險與儲存成本壓力。透過自動化流程,我們可以在檔案進入 S3 後即時進行 壓縮(節省儲存空間與傳輸頻寬)以及 掃毒(避免惡意檔案進入系統),確保檔案安全且具備最佳化的儲存效率。

在無伺服器架構中,檔案存取是常見需求,但隨之而來的挑戰是:

(1) 大量檔案累積導致儲存與傳輸成本上升。
(2) 檔案來源多元,若沒有檢測,惡意程式可能透過檔案傳播。
(3) 傳統人工檢查與壓縮流程效率低落,難以應付大規模上傳需求。
本 Lab 透過 S3 事件觸發 Lambda,並藉由 Step Functions 串接壓縮、掃毒、標記等多步驟處理,建立 全自動檔案後處理管線,在整體 Serverless 架構中擔任「檔案安全與最佳化守門員」的角色。

二、需要使用到的服務

  • Amazon S3:存放上傳的原始檔案,並觸發後處理流程。
  • AWS Lambda:負責執行檔案壓縮與掃毒邏輯(可整合第三方病毒掃描引擎,如 ClamAV)。
  • AWS Step Functions:編排多個 Lambda 的執行順序,確保壓縮後再掃毒,最後再將結果寫回 S3 或 DynamoDB。
  • Amazon DynamoDB:紀錄檔案處理結果(壓縮成功、掃毒結果、安全標記、追蹤追溯)。
  • Amazon CloudWatch Logs:紀錄流程執行狀況,協助偵錯與監控。

三、架構/概念圖

https://ithelp.ithome.com.tw/upload/images/20251003/20172743M29ptCT1Et.png

四、技術重點

(1) 使用 多 Bucket 架構(原始/處理後/隔離)避免污染。
(2) Lambda 掃毒建議使用 Layer 部署 ClamAV,定期更新病毒碼。
(3) 壓縮與掃毒建議分離 Lambda,降低單一函數複雜度,便於維護。
(4) 對於大檔案,建議使用 S3 Multipart Upload 搭配 Step Functions 並行處理。
(5) 針對異常狀況(如掃毒失敗)設計 Dead Letter Queue(DLQ),確保不遺漏檔案。(此次沒有做這塊)

五、Lab流程

1️⃣ 前置作業

1. 建立 S3 Bucket:Day6已創建過。

2️⃣ 主要配置

1. 在GitHub上創建一個空資料夾(放置處理完的檔案用)

  1. 到自己的GitHub中,並點選創建檔案。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743t9YcWpkjhP.png

  2. 創建資料夾及佔位檔案「.gitkeep」。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743UKTHpYxevV.png

  3. 上傳並推送。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743ygLO9z5rLh.png

2. 創建DynamoDB(紀錄掃描成功的檔案用)

  1. 進到「DynamoDB」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743CEsMQXAGo7.png

  2. 創建一個新的資料表。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743GrfDWW4KsJ.png

  3. 命名並設定分割索引。(FileKey)
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Yfb8gxqI3I.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743G9rfpjmk3v.png

  4. 完成畫面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743vI2IFuNJzc.png

3. 創建Lambda(Step Functions用:壓縮檔案、掃描標記、寫入結果)× 3

  1. 進入「Lambda」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743HLBWDMHTKb.png

  2. 創建3個新的函數。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743RVSLQWaOkG.png

  3. 輸入函數名稱,並選擇編撰語言。(名稱:Ducky-Scan、Ducky-Compress、Ducky-Update)
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Yg8HKt4zp2.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743lWNXoV1L7o.png
    https://ithelp.ithome.com.tw/upload/images/20251003/201727437Y1YcXb7ev.png

  4. 跳過建議畫面。
    https://ithelp.ithome.com.tw/upload/images/20251003/2017274316t3ZpsvdR.png

  5. 寫入程式碼,並部署。

    • **(1)**範例程式碼(Scan)檔案掃描病毒用

      // index.mjs (ScanLambda)
      export const handler = async (event) => {
        const { bucket, key } = event; // 接收 S3 資訊
      
        console.log(`Starting virus scan for s3://${bucket}/${key}`);
      
        // ⚠️ 模擬掃描邏輯:如果檔名包含 'virus' 或 'eicar',則視為感染
        const fileName = key.split('/').pop();
        const scanResult = (fileName.toLowerCase().includes('virus') || fileName.toLowerCase().includes('eicar')) 
                           ? "INFECTED" 
                           : "CLEAN";
      
        if (scanResult === "INFECTED") {
            console.log(`❌ File ${key} is INFECTED. Throwing error for quarantine.`);
            // 丟出錯誤,讓 Step Functions 轉到錯誤/隔離處理
            throw new Error("FileScanFailed"); 
        }
      
        console.log(`✅ File ${key} is CLEAN.`);
      
        // 將結果傳遞給下一個步驟 (CompressLambda)
        return {
            ...event, // 帶上原始 bucket 和 key
            scanStatus: scanResult,
            scanTimestamp: new Date().toISOString()
        };
      };
      
    • **(2)**範例程式碼(Compress)檔案壓縮,放到S3內的路徑用

      // index.mjs (CompressLambda)
      import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
      import { Buffer } from 'buffer'; // 確保 Buffer 類別可用
      
      const s3Client = new S3Client({ region: process.env.AWS_REGION });
      const PROCESSED_FOLDER = process.env.PROCESSED_FOLDER; 
      
      // 輔助函數:將 Stream 完整讀取到 Buffer 中 (這是解決錯誤的關鍵)
      const streamToBuffer = (stream) =>
        new Promise((resolve, reject) => {
          const chunks = [];
          stream.on('data', (chunk) => chunks.push(chunk));
          stream.on('error', reject);
          stream.on('end', () => resolve(Buffer.concat(chunks)));
        });
      
      export const handler = async (event) => {
          const { bucket, key } = event; // 接收上一步的輸出
      
          // 1. 下載原始檔案,獲取 Stream
          const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
          const fileGetResponse = await s3Client.send(getCommand);
      
          // 2. 將 Stream 完整讀取到 Buffer 中
          // 解決錯誤: fileGetResponse.Body 是 Stream,必須完整讀取
          const originalFileBuffer = await streamToBuffer(fileGetResponse.Body);
      
          // 3. 決定新的檔案路徑和名稱
          const originalFilename = key.split('/').pop(); 
          const compressedKey = PROCESSED_FOLDER + originalFilename + ".zip"; 
      
          // 4. 執行壓縮和上傳 
          // ⚠️ 實際壓縮程式碼會在這裡執行,並生成 compressedBuffer
          // 這裡我們只是模擬,直接使用原始 Buffer 作為壓縮後的內容
          const compressedBuffer = originalFileBuffer; 
      
          const putCommand = new PutObjectCommand({
              Bucket: bucket, // 寫回同一個 Bucket
              Key: compressedKey, // 寫入新的資料夾
              Body: compressedBuffer, // 傳遞完整的 Buffer
              ContentLength: compressedBuffer.length, // 雖然 SDK 會自動計算,但明確傳遞更安全
              ContentType: 'application/zip',
              Metadata: { 
                  'scan-status': event.scanStatus 
              }
          });
      
          await s3Client.send(putCommand);
      
          console.log(`✅ File compressed (simulated) and moved to s3://${bucket}/${compressedKey}`);
      
          // 傳遞給下一個步驟 (UpdateDDBLambda)
          return {
              ...event,
              processedKey: compressedKey
          };
      };
      
    • **(3)**範例程式碼(Update)檔案上傳寫入DynamoDB用

      // index.mjs (UpdateDDBLambda)
      import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
      import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
      
      const ddbClient = new DynamoDBClient({});
      const ddbDocClient = DynamoDBDocumentClient.from(ddbClient);
      
      const DDB_TABLE = process.env.DDB_TABLE; 
      
      // 檢查表格名稱是否設定 
      if (!DDB_TABLE) {
          throw new Error("DDB_TABLE environment variable is not set.");
      }
      
      export const handler = async (event) => {
          const { key, processedKey, scanStatus, scanTimestamp } = event; 
      
          // 檢查 Step Functions 的輸入是否包含主鍵所需的值
          if (!key) {
              throw new Error("Missing key (original file path) from workflow input.");
          }
      
          // 🎯 最終修正:將屬性名稱從 FlienNme 修正為 FileKey
          const item = {
              FileKey: key, // <-- 使用 DynamoDB 實際要求的主鍵名稱
              ScanStatus: scanStatus,
              ProcessedPath: processedKey,
              ProcessedAt: scanTimestamp,
              Status: 'COMPLETED_SUCCESS' 
          };
      
          const putItemParams = {
              TableName: DDB_TABLE,
              Item: item
          };
      
          try {
              await ddbDocClient.send(new PutCommand(putItemParams));
      
              console.log(`✅ Result saved for file: ${key}`);
              return { status: "Success", file: key };
          } catch (e) {
              // 輸出詳細錯誤,以便追蹤
              console.error("❌ DynamoDB PutCommand Failed:", e);
              // 拋出錯誤,讓 Step Functions 知道
              throw new Error(`DynamoDB PutCommand Failed: ${e.message}`);
          }
      };
      

    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Gi9CfdXMxF.png

4. 設定Lambda的環境變數 × 2

  1. 新增Lambda「Ducky-Compress」變數key:

    「PROCESSED_FOLDER」,Value為你在 S3 中要將處理完的檔案丟入的路徑(如:步驟一的「processed-files/」)
    https://ithelp.ithome.com.tw/upload/images/20251003/201727437PJ1LatMQf.png

  2. 新增Lambda「Ducky-Update」變數key:

    「DDB_TABLE」,Value為你在 DynamoDB 中,要儲存處理紀錄的資料庫名稱(如:步驟二的「Ducky-file-processing-Results」)
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743YfemF74UKa.png

5. 建立 Lambda Function - LambdaCompress

  1. 進入「Step Functions」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743SgTKLCFY1X.png

  2. 新增一個狀態機器。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Y0ypu7GrXS.png

  3. 命名,並選「空白範本」及「標準」的模式做創建。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743WhRIRR8Gqf.png

  4. 設定Step Functions的工作流程。

    • 程式碼範例(ARN要替換)

      {
        "Comment": "File Processing Pipeline: Scan -> Compress -> Database Update",
        "StartAt": "VirusScan",
        "States": {
          "VirusScan": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:730335441348:function:Ducky-Scan",
            "Catch": [],
            "Next": "CompressAndSave"
          },
          "CompressAndSave": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:730335441348:function:Ducky-Compress",
            "Next": "UpdateDatabase"
          },
          "UpdateDatabase": {
            "Type": "Task",
            "Resource": "arn:aws:lambda:us-east-1:730335441348:function:Ducky-Update",
            "End": true
          }
        }
      }
      

    https://ithelp.ithome.com.tw/upload/images/20251003/20172743fbfaarmfKO.png

    • Lambda的ARN在哪裡?
      https://ithelp.ithome.com.tw/upload/images/20251003/20172743APZmb91rBQ.png
  5. 確認建立並存檔。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743sLVdQEZkKq.png

  6. 完成畫面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743IjIv4lekin.png

6. 調整Lambda compress的IAM user role權限 × 3

  1. 進入「IAM」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Gw7gWmpKGC.png

  2. 進入IAM role的頁面,點選該Lambda自動創建的IAM role。(有3個Lambda IAM role要做調整)
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743KJyoRwMYPz.png

  3. 新增「許可政策」。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Ug9zQn9Yw4.png

  4. 調整一下對應的IAM Role :

    • **(1)**Ducky-Scan:增加S3的「GetObject」、「PutObject」權限,授權範圍為「指定的S3桶」。
      https://ithelp.ithome.com.tw/upload/images/20251003/20172743cNlAHbtNOd.png

      • S3的ARN在哪?
        https://ithelp.ithome.com.tw/upload/images/20251003/20172743NZUYiOkH6V.png
        .................................._
        https://ithelp.ithome.com.tw/upload/images/20251003/201727434h7JvHhWXq.png
        https://ithelp.ithome.com.tw/upload/images/20251003/20172743dXtm3W19sd.png

      設定「許可政策」名稱。
      https://ithelp.ithome.com.tw/upload/images/20251003/20172743BktnS7XU9G.png

    • **(2)**Ducky-Compress:增加S3的「GetObject」、「PutObject」權限,授權範圍為「指定的S3桶」。
      https://ithelp.ithome.com.tw/upload/images/20251003/2017274370QtROBiRi.png

      https://ithelp.ithome.com.tw/upload/images/20251003/20172743M4Ec6ZNXr8.png
      - Ducky-Update:增加DynamoDB的「PutItem」權限,授權範圍為「指定的DynamoDB」。

      • DynamoDB的ARN在哪?
        https://ithelp.ithome.com.tw/upload/images/20251003/20172743sug7yLvcrQ.png

3. 創建Lambda函數(觸發Step Functions用)

  1. 進入「Lambda」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251003/201727432chaCQnxpj.png

  2. 創建一個新的函數。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743V7NZjQ1A8R.png

  3. 輸入函數名稱,並選擇編撰語言。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743fh4QW25clm.png

  4. 寫入程式碼,並部署。

    • 範例程式碼:

      // index.mjs (S3ToStepFunctionInvoker)
      import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn";
      
      // import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; 
      // import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
      
      const sfnClient = new SFNClient({});
      const STATE_MACHINE_ARN = process.env.STATE_MACHINE_ARN; // 從環境變數讀取
      
      // 確保環境變數已設定,否則在初始化階段就會報錯
      if (!STATE_MACHINE_ARN) {
          throw new Error('Environment variable STATE_MACHINE_ARN must be set.');
      }
      
      export const handler = async (event, context) => {
          // 取得 S3 Event 的核心資料 (Bucket, Key)
          const record = event.Records[0]; 
          const bucket = record.s3.bucket.name;
          const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
      
          // 將檔案資訊包裝成 Step Functions 的輸入
          const inputPayload = JSON.stringify({ bucket: bucket, key: key });
          const command = new StartExecutionCommand({
              stateMachineArn: STATE_MACHINE_ARN,
              input: inputPayload,
              // 為每次執行命名,方便追蹤
              name: `${key.replace(/[^a-zA-Z0-9]/g, '_')}-${Date.now()}`
          });
      
          try {
              await sfnClient.send(command);
              console.log(`✅ Started Step Function for: ${key}`);
      
              return { statusCode: 200, body: 'Execution started.' };
          } catch (error) {
              console.error("❌ Failed to start Step Function:", error);
              return { statusCode: 500, body: 'Failed to start Step Function.' };
          }
      };
      

    https://ithelp.ithome.com.tw/upload/images/20251003/20172743BUUCvbK7nM.png

  5. 設定環境變數「STATE_MACHINE_ARN」為「Step Functions的ARN」
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743qv36m5dezd.png
    - Step Functions的ARN在哪?
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743Yx4tSBivMx.png

4. 授予Lambda的IAM user role觸發Step Functions權限

https://ithelp.ithome.com.tw/upload/images/20251003/20172743YPCkBmbR2v.png

5. 創建S3的事件通知

  1. 進入「S3」頁面。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743TDbJSLkqnz.png

  2. 點選Day6創建的S3。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743imlc7gR3hJ.png

  3. 進入「屬性」頁面,並新增一個「事件通知」。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743YpthWlw6YB.png

  4. 設定「當指定的路徑創建事件時,會通知Lambda函數」。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743vIwh3n4Kv4.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743d4U28DJ0o4.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743XofL40M1yK.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743L2El8CyGoz.png

3️⃣ 測試驗證

1. 呼叫 API Gateway,取得特定的 Presigned URL(包含命名上傳後檔名)

  • 指令範例

    curl "<YOUR_API_GATEWAY_URL>/get-upload-url?filename=<上傳後的檔案名稱>"
    

https://ithelp.ithome.com.tw/upload/images/20251003/20172743GkWLhtYxmF.png

  • YOUR_API_GATEWAY_URL 在哪獲得?
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743XQYu3O1iUo.png

2. 使用取得的 Presigned URL 上傳檔案

  • 指令範例

    curl --upload-file <本地檔案路徑.格式> "<YOUR_PRESIGNED_URL>"
    

    https://ithelp.ithome.com.tw/upload/images/20251003/20172743v8VMfNEyKe.png

3. 確認S3桶子內有檔案

  • 上傳的檔案:
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743n91VH9ZJD6.png

  • 壓縮過的檔案:
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743troglNgZJd.png

4. 確認DynamoDB是否有掃描紀錄寫入

https://ithelp.ithome.com.tw/upload/images/20251003/20172743ziyV5ol9JY.png

5. 檢查 Step Functions 是否成功執行三個 Lambda 步驟。

  1. 確認Step Functions有正常執行。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743CkeYDFyQ16.png

  2. 正確內容範例。
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743oXNhQYpeHC.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743siweZR6df9.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743sP8b2XQ6nP.png

  3. 錯誤內容範例。(可以確認是哪個環節有誤)
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743nbpsuJ898r.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743mL6kgT9nCu.png
    https://ithelp.ithome.com.tw/upload/images/20251003/20172743wdgCQkZyp9.png

六、結語

本 Lab 展示如何利用 S3 x Lambda x Step Functions 建立一個全自動檔案後處理管線,包含壓縮與掃毒功能。
這不僅能降低儲存成本,也能強化系統安全性,避免惡意檔案入侵。在 Serverless 架構中,這樣的流程讓檔案管理更高效、更安全、更自動化。


上一篇
Day 18 檔案存取控制:S3 x API x Presigned URL 實現安全/限時下載與預覽
下一篇
Day 20 檔案分享功能:限時連結 x 權限管理 API 設計
系列文
從一個網站的誕生,看懂 AWS 架構與自動化的全流程!24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言