通過 AWS S3 在多個服務器之間共享數據

已發表: 2022-03-10
快速總結 ↬創建上傳和操作文件的多步驟表單時,如果應用程序在負載均衡器後面的多台服務器上運行,那麼我們需要確保文件在整個執行過程中都可用, 對於在每個步驟中處理該過程的任何服務器。 在本文中,我們將通過基於 AWS S3 創建可供上傳文件的所有服務器訪問的存儲庫來解決此問題。

在為處理用戶上傳的文件提供某些功能時,該文件必須在整個執行過程中對進程可用。 簡單的上傳和保存操作沒有問題。 但是,如果在保存之前必須對文件進行操作,並且應用程序在負載平衡器後面的多個服務器上運行,那麼我們需要確保每次運行該進程的任何服務器都可以使用該文件。

例如,多步驟“上傳您的用戶頭像”功能可能需要用戶在第 1 步上傳頭像,在第 2 步裁剪,最後在第 3 步保存。在第 3 步將文件上傳到服務器後1,該文件必須可用於處理步驟 2 和 3 的請求的任何服務器,這可能與步驟 1 相同,也可能不同。

一種天真的方法是將步驟 1 中上傳的文件複製到所有其他服務器,這樣文件就可以在所有服務器上使用。 然而,這種方法不僅極其複雜而且不可行:例如,如果站點運行在來自多個區域的數百台服務器上,則無法實現。

一種可能的解決方案是在負載均衡器上啟用“粘性會話”,它總是為給定會話分配相同的服務器。 然後,步驟 1、2 和 3 將由同一服務器處理,並且在步驟 1 上傳到此服務器的文件對於步驟 2 和 3 仍然存在。但是,粘性會話並不完全可靠:如果在步驟 1 之間2 該服務器崩潰,然後負載均衡器將不得不分配不同的服務器,從而破壞功能和用戶體驗。 同樣,在特殊情況下,總是為會話分配相同的服務器可能會導致負載過重的服務器的響應時間變慢。

更合適的解決方案是將文件的副本保存在所有服務器都可以訪問的存儲庫中。 然後,在步驟 1 將文件上傳到服務器後,該服務器會將其上傳到存儲庫(或者,文件可以直接從客戶端上傳到存儲庫,繞過服務器); 服務器處理步驟 2 將從存儲庫下載文件,對其進行操作,然後再次上傳; 最後,服務器處理步驟 3 將從存儲庫中下載並保存。

跳躍後更多! 繼續往下看↓

在本文中,我將描述後一種解決方案,它基於在 Amazon Web Services (AWS) Simple Storage Service (S3)(一種用於存儲和檢索數據的雲對象存儲解決方案)上存儲文件的 WordPress 應用程序,通過 AWS 開發工具包進行操作。

注1:對於裁剪頭像等簡單功能,另一種解決方案是完全繞過服務器,通過Lambda函數直接在雲端實現。 但由於本文是關於將服務器上運行的應用程序與 AWS S3 連接起來,因此我們不考慮這種解決方案。

注意 2:為了使用 AWS S3(或任何其他 AWS 服務),我們需要有一個用戶帳戶。 亞馬遜在這裡提供為期 1 年的免費套餐,這足以試驗他們的服務。

注 3:有用於將文件從 WordPress 上傳到 S3 的 3rd 方插件。 一個這樣的插件是 WP Media Offload(精簡版可在此處獲得),它提供了一個很棒的功能:它將上傳到媒體庫的文件無縫傳輸到 S3 存儲桶,這允許解耦站點的內容(例如在/wp-content/uploads) 來自應用程序代碼。 通過解耦內容和代碼,我們能夠使用 Git 部署我們的 WordPress 應用程序(否則我們不能,因為用戶上傳的內容沒有託管在 Git 存儲庫中),並將應用程序託管在多個服務器上(否則,每個服務器都需要保留所有用戶上傳內容的副本。)

創建存儲桶

創建存儲桶時,我們需要考慮存儲桶名稱:每個存儲桶名稱在 AWS 網絡上必須是全局唯一的,因此即使我們想將存儲桶稱為“頭像”之類的簡單名稱,該名稱可能已經被使用,那麼我們可以選擇更獨特的東西,比如“avatars-name-of-my-company”。

我們還需要選擇存儲桶所在的區域(該區域是數據中心所在的物理位置,位置遍布全球。)

該區域必須與我們的應用程序部署的區域相同,以便在流程執行期間訪問 S3 快速。 否則,用戶可能不得不等待額外的幾秒鐘來上傳/下載圖像到/從遙遠的位置。

注意:只有當我們還使用 Amazon 的雲上虛擬服務器服務 EC2 來運行應用程序時,才有意義使用 S3 作為雲對象存儲解決方案。 相反,如果我們依賴其他公司來託管應用程序,例如 Microsoft Azure 或 DigitalOcean,那麼我們也應該使用他們的雲對象存儲服務。 否則,我們的站點將因數據在不同公司的網絡之間傳輸而產生開銷。

在下面的屏幕截圖中,我們將看到如何創建用於上傳用戶頭像以進行裁剪的存儲桶。 我們首先前往 S3 儀表板並單擊“創建存儲桶”:

S3 儀表板
S3 儀表板,顯示我們所有現有的存儲桶。 (大預覽)

然後我們輸入存儲桶名稱(在本例中為“avatars-smashing”)並選擇區域(“EU (Frankfurt)”):

創建存儲桶屏幕
通過在 S3 中創建存儲桶。 (大預覽)

只有存儲桶名稱和區域是必需的。 對於以下步驟,我們可以保留默認選項,因此我們單擊“下一步”,直到最後單擊“創建存儲桶”,這樣,我們將創建存儲桶。

設置用戶權限

通過 SDK 連接到 AWS 時,我們將需要輸入我們的用戶憑證(一對訪問密鑰 ID 和秘密訪問密鑰),以驗證我們是否可以訪問請求的服務和對象。 用戶權限可以是非常一般的(“管理員”角色可以做任何事情)或非常細化,只授予所需的特定操作的權限,沒有別的。

作為一般規則,我們授予的權限越具體越好,以避免安全問題。 創建新用戶時,我們需要創建一個策略,它是一個簡單的 JSON 文檔,列出了要授予用戶的權限。 在我們的例子中,我們的用戶權限將授予對 S3 的訪問權限,用於存儲桶“avatars-smashing”、“Put”(用於上傳對象)、“Get”(用於下載對象)和“List”操作(用於列出存儲桶中的所有對象),從而產生以下策略:

 { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:Put*", "s3:Get*", "s3:List*" ], "Resource": [ "arn:aws:s3:::avatars-smashing", "arn:aws:s3:::avatars-smashing/*" ] } ] }

在下面的屏幕截圖中,我們可以看到如何添加用戶權限。 我們必須轉到身份和訪問管理 (IAM) 儀表板:

IAM 儀表板
IAM 儀表板,列出我們創建的所有用戶。 (大預覽)

在儀表板中,我們單擊“用戶”,然後立即單擊“添加用戶”。 在添加用戶頁面中,我們選擇一個用戶名(“crop-avatars”),並勾選“程序訪問”作為訪問類型,它將提供訪問密鑰 ID 和秘密訪問密鑰,用於通過 SDK 進行連接:

添加用戶頁面
添加新用戶。 (大預覽)

然後我們單擊“下一步:權限”按鈕,單擊“直接附加現有策略”,然後單擊“創建策略”。 這將在瀏覽器中打開一個新選項卡,其中包含“創建策略”頁面。 我們單擊 JSON 選項卡,然後輸入上面定義的策略的 JSON 代碼:

創建策略頁面
在“avatars-smashing”存儲桶上創建一個授予“Get”、“Post”和“List”操作的策略。 (大預覽)

然後我們單擊 Review policy,為其命名(“CropAvatars”),最後單擊 Create policy。 創建策略後,我們切換回上一個選項卡,選擇 CropAvatars 策略(我們可能需要刷新策略列表才能看到它),單擊 Next: Review,最後單擊 Create user。 完成此操作後,我們終於可以下載訪問密鑰 ID 和秘密訪問密鑰(請注意,這些憑據在這個獨特的時刻可用;如果我們現在不復製或下載它們,我們將不得不創建一個新對):

用戶創建成功頁面
創建用戶後,我們將獲得一個唯一的時間來下載憑據。 (大預覽)

通過 SDK 連接 AWS

SDK 可通過多種語言獲得。 對於 WordPress 應用程序,我們需要 PHP 的 SDK,可以從這裡下載,關於如何安裝它的說明在這裡。

一旦我們創建了存儲桶、準備好用戶憑證並安裝了 SDK,我們就可以開始將文件上傳到 S3。

上傳和下載文件

為方便起見,我們將用戶憑據和區域定義為 wp-config.php 文件中的常量:

 define ('AWS_ACCESS_KEY_ID', '...'); // Your access key id define ('AWS_SECRET_ACCESS_KEY', '...'); // Your secret access key define ('AWS_REGION', 'eu-central-1'); // Region where the bucket is located. This is the region id for "EU (Frankfurt)"

在我們的例子中,我們正在實現裁剪頭像功能,為此頭像將存儲在“頭像粉碎”桶中。 但是,在我們的應用程序中,我們可能有幾個其他功能的桶,需要執行相同的上傳、下載和列出文件的操作。 因此,我們在抽像類AWS_S3上實現常用方法,並在實現子類中獲取輸入,例如通過函數get_bucket定義的存儲桶名稱。

 // Load the SDK and import the AWS objects require 'vendor/autoload.php'; use Aws\S3\S3Client; use Aws\Exception\AwsException; // Definition of an abstract class abstract class AWS_S3 { protected function get_bucket() { // The bucket name will be implemented by the child class return ''; } }

S3Client類公開了與 S3 交互的 API。 我們僅在需要時實例化它(通過延遲初始化),並在$this->s3Client下保存對它的引用以繼續使用相同的實例:

 abstract class AWS_S3 { // Continued from above... protected $s3Client; protected function get_s3_client() { // Lazy initialization if (!$this->s3Client) { // Create an S3Client. Provide the credentials and region as defined through constants in wp-config.php $this->s3Client = new S3Client([ 'version' => '2006-03-01', 'region' => AWS_REGION, 'credentials' => [ 'key' => AWS_ACCESS_KEY_ID, 'secret' => AWS_SECRET_ACCESS_KEY, ], ]); } return $this->s3Client; } }

當我們在應用程序中處理$file時,此變量包含磁盤中文件的絕對路徑(例如/var/app/current/wp-content/uploads/users/654/leo.jpg ),但在上傳時文件到 S3 我們不應該將對象存儲在同一路徑下。 特別是,出於安全原因,我們必須刪除有關係統信息的初始位( /var/app/current ),並且我們可以選擇刪除/wp-content位(因為所有文件都存儲在此文件夾下,這是冗餘信息),只保留文件的相對路徑 ( /uploads/users/654/leo.jpg )。 方便的是,這可以通過從絕對路徑中刪除WP_CONTENT_DIR之後的所有內容來實現。 下面的函數get_fileget_file_relative_path在絕對和相對文件路徑之間切換:

 abstract class AWS_S3 { // Continued from above... function get_file_relative_path($file) { return substr($file, strlen(WP_CONTENT_DIR)); } function get_file($file_relative_path) { return WP_CONTENT_DIR.$file_relative_path; } }

將對像上傳到 S3 時,我們可以通過訪問控制列表 (ACL) 權限來確定授予誰訪問該對像以及訪問類型。 最常見的選項是保持文件私有(ACL => “private”)並使其可在 Internet 上閱讀(ACL => “public-read”)。 因為我們需要直接從 S3 請求文件以將其顯示給用戶,所以我們需要 ACL => “public-read”:

 abstract class AWS_S3 { // Continued from above... protected function get_acl() { return 'public-read'; } }

最後,我們實現了將對像上傳到 S3 存儲桶以及從 S3 存儲桶下載對象的方法:

 abstract class AWS_S3 { // Continued from above... function upload($file) { $s3Client = $this->get_s3_client(); // Upload a file object to S3 $s3Client->putObject([ 'ACL' => $this->get_acl(), 'Bucket' => $this->get_bucket(), 'Key' => $this->get_file_relative_path($file), 'SourceFile' => $file, ]); } function download($file) { $s3Client = $this->get_s3_client(); // Download a file object from S3 $s3Client->getObject([ 'Bucket' => $this->get_bucket(), 'Key' => $this->get_file_relative_path($file), 'SaveAs' => $file, ]); } }

然後,在實現子類中,我們定義存儲桶的名稱:

 class AvatarCropper_AWS_S3 extends AWS_S3 { protected function get_bucket() { return 'avatars-smashing'; } }

最後,我們簡單地實例化類以將頭像上傳到 S3 或從 S3 下載。 此外,當從第 1 步過渡到第 2 步和從第 2 步到第 3 步時,我們需要傳達$file的值。 我們可以通過 POST 操作提交帶有$file相對路徑值的字段“file_relative_path”來做到這一點(出於安全原因,我們不傳遞絕對路徑:不需要包含“/var/www/current ” 供外人查看的信息):

 // Step 1: after the file was uploaded to the server, upload it to S3. Here, $file is known $avatarcropper = new AvatarCropper_AWS_S3(); $avatarcropper->upload($file); // Get the file path, and send it to the next step in the POST $file_relative_path = $avatarcropper->get_file_relative_path($file); // ... // -------------------------------------------------- // Step 2: get the $file from the request and download it, manipulate it, and upload it again $avatarcropper = new AvatarCropper_AWS_S3(); $file_relative_path = $_POST['file_relative_path']; $file = $avatarcropper->get_file($file_relative_path); $avatarcropper->download($file); // Do manipulation of the file // ... // Upload the file again to S3 $avatarcropper->upload($file); // -------------------------------------------------- // Step 3: get the $file from the request and download it, and then save it $avatarcropper = new AvatarCropper_AWS_S3(); $file_relative_path = $_REQUEST['file_relative_path']; $file = $avatarcropper->get_file($file_relative_path); $avatarcropper->download($file); // Save it, whatever that means // ...

直接從 S3 顯示文件

如果我們想在第2步操作後顯示文件的中間狀態(例如裁剪後的用戶頭像),那麼我們必須直接從S3引用文件; URL 無法指向服務器上的文件,因為我們不知道哪個服務器將處理該請求。

下面,我們添加函數get_file_url($file) ,它在 S3 中獲取該文件的 URL。 如果使用此功能,請確保上傳文件的 ACL 為“public-read”,否則用戶無法訪問。

 abstract class AWS_S3 { // Continue from above... protected function get_bucket_url() { $region = $this->get_region(); // North Virginia region is simply "s3", the others require the region explicitly $prefix = $region == 'us-east-1' ? 's3' : 's3-'.$region; // Use the same scheme as the current request $scheme = is_ssl() ? 'https' : 'http'; // Using the bucket name in path scheme return $scheme.'://'.$prefix.'.amazonaws.com/'.$this->get_bucket(); } function get_file_url($file) { return $this->get_bucket_url().$this->get_file_relative_path($file); } }

然後,我們可以簡單地獲取 S3 上文件的 URL 並打印圖像:

 printf( "<img src='%s'>", $avatarcropper->get_file_url($file) );

列出文件

如果在我們的應用程序中我們希望允許用戶查看所有之前上傳的頭像,我們可以這樣做。 為此,我們引入了get_file_urls函數,它列出了存儲在某個路徑下的所有文件的 URL(在 S3 術語中,它稱為前綴):

 abstract class AWS_S3 { // Continue from above... function get_file_urls($prefix) { $s3Client = $this->get_s3_client(); $result = $s3Client->listObjects(array( 'Bucket' => $this->get_bucket(), 'Prefix' => $prefix )); $file_urls = array(); if(isset($result['Contents']) && count($result['Contents']) > 0 ) { foreach ($result['Contents'] as $obj) { // Check that Key is a full file path and not just a "directory" if ($obj['Key'] != $prefix) { $file_urls[] = $this->get_bucket_url().$obj['Key']; } } } return $file_urls; } }

然後,如果我們將每個頭像存儲在路徑“/users/${user_id}/”下,通過傳遞此前綴,我們將獲得所有文件的列表:

 $user_id = get_current_user_id(); $prefix = "/users/${user_id}/"; foreach ($avatarcropper->get_file_urls($prefix) as $file_url) { printf( "<img src='%s'>", $file_url ); }

結論

在本文中,我們探討瞭如何使用雲對象存儲解決方案充當通用存儲庫,為部署在多個服務器上的應用程序存儲文件。 對於解決方案,我們專注於 AWS S3,並繼續展示需要集成到應用程序中的步驟:創建存儲桶、設置用戶權限以及下載和安裝 SDK。 最後,我們解釋瞭如何避免應用程序中的安全隱患,並通過代碼示例演示瞭如何在 S3 上執行最基本的操作:上傳、下載和列出文件,每個操作幾乎不需要幾行代碼。 該解決方案的簡單性表明,將雲服務集成到應用程序中並不難,也可以由對雲沒有太多經驗的開發人員來完成。