Laravel 零停機部署

已發表: 2022-03-11

在更新實時應用程序時,有兩種根本不同的方法。

在第一種方法中,我們對系統狀態進行增量更改。 例如,我們更新文件、修改環境屬性、安裝額外的必需品等等。 在第二種方法中,我們拆除整台機器並使用新圖像和聲明性配置(例如,使用 Kubernetes)重建系統。

Laravel 部署變得簡單

本文主要介紹相對較小的應用程序,這些應用程序可能不會託管在雲中,儘管我會提到 Kubernetes 如何在“無雲”場景之外的部署中極大地幫助我們。 我們還將討論一些常見問題和提示,以執行可能適用於各種不同情況的成功更新,而不僅僅是 Laravel 部署。

出於演示的目的,我將使用 Laravel 示例,但請記住,任何 PHP 應用程序都可以使用類似的方法。

版本控制

對於初學者來說,了解當前部署在生產環境中的代碼版本至關重要。 它可以包含在某個文件中,或者至少包含在文件夾或文件的名稱中。 至於命名,如果我們遵循語義版本控制的標準做法,我們可以在其中包含更多信息,而不僅僅是單個數字。

查看兩個不同的版本,這些添加的信息可以幫助我們輕鬆了解它們之間引入的更改的性質。

顯示語義版本控制解釋的圖像。

版本控制從版本控制系統開始,例如 Git。 假設我們已經準備了一個用於部署的版本,例如 1.0.3 版。 在組織這些版本和代碼流時,有不同的開發風格,例如基於主幹的開發和 Git 流,您可以根據團隊的偏好和項目的具體情況進行選擇或混合。 最後,我們很可能會在我們的主分支上相應地標記我們的版本。

提交後,我們可以創建一個簡單的標籤,如下所示:

git tag v1.0.3

然後我們在執行 push 命令時包含標籤:

git push <origin> <branch> --tags

我們還可以使用它們的哈希將標籤添加到舊提交。

將發布文件傳送到目的地

Laravel 部署需要時間,即使它只是複製文件。 但是,即使不需要太長時間,我們的目標是實現零停機時間

因此,我們應該避免就地安裝更新,並且不應該更改實時提供的文件。 相反,我們應該部署到另一個目錄,並且只有在安裝完全準備好後才進行切換。

實際上,有各種工具和服務可以幫助我們進行部署,例如 Envoyer.io(由 Laravel.com 設計師 Jack McDade 設計)、Capistrano、Deployer 等。我還沒有在生產中使用它們,所以我不能提出建議或寫一個全面的比較,但讓我展示這些產品背後的想法。 如果其中一些(或全部)無法滿足您的要求,您始終可以創建自定義腳本,以您認為合適的最佳方式自動化流程。

出於本演示的目的,假設我們的 Laravel 應用程序由 Nginx 服務器從以下路徑提供服務:

/var/www/demo/public

首先,每次進行部署時,我們都需要一個目錄來放置發布文件。 此外,我們需要一個指向當前工作版本的符號鏈接。 在這種情況下, /var/www/demo將作為我們的符號鏈接。 重新分配指針將使我們能夠快速更改版本。

Laravel 部署文件處理

如果我們正在處理 Apache 服務器,我們可能需要在配置中允許以下符號鏈接:

Options +FollowSymLinks

我們的結構可以是這樣的:

 /opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2

可能有一些文件我們需要通過不同的部署來持久化,例如日誌文件(如果我們不使用 Logstash,顯然)。 在 Laravel 部署的情況下,我們可能希望保留存儲目錄和 .env 配置文件。 我們可以將它們與其他文件分開,並使用它們的符號鏈接。

為了從 Git 存儲庫中獲取我們的發布文件,我們可以使用克隆或存檔命令。 有些人使用 git clone,但您無法克隆特定的提交或標記。 這意味著獲取整個存儲庫,然後選擇特定標籤。 當存儲庫包含許多分支或大量歷史記錄時,它的大小會比發布存檔大得多。 因此,如果您在生產中並不特別需要 git repo,您​​可以使用git archive 。 這使我們可以通過特定標籤僅獲取文件存檔。 使用後者的另一個優點是我們可以忽略一些不應該出現在生產環境中的文件和文件夾,例如測試。 為此,我們只需要在.gitattributes file中設置 export-ignore 屬性。 在 OWASP 安全編碼實踐檢查表中,您可以找到以下建議: “在部署之前刪除測試代碼或任何不適合生產的功能。”

如果我們從源版本控制系統獲取版本,git archive 和 export-ignore 可以幫助我們滿足這個要求。

讓我們看一個簡化的腳本(它在生產中需要更好的錯誤處理):

部署.sh

 #!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo

為了部署我們的版本,我們可以執行以下操作:

deploy.sh v1.0.3

注意:在這個例子中,v1.0.3 是我們發布的 git 標籤。

生產方面的作曲家?

您可能已經註意到該腳本正在調用 Composer 來安裝依賴項。 儘管您在許多文章中都看到了這一點,但這種方法可能存在一些問題。 通常,最佳實踐是創建應用程序的完整構建並通過基礎架構的各種測試環境推進此構建。 最後,您將擁有一個經過全面測試的構建,可以安全地部署到生產環境中。 儘管每個構建都應該可以從頭開始重現,但這並不意味著我們應該在不同的階段重新構建應用程序。 當我們在生產環境中安裝 composer 時,這與測試版本並不完全相同,這可能會出錯:

  • 網絡錯誤可能會中斷下載依賴項。
  • 庫供應商可能並不總是遵循 SemVer。

很容易發現網絡錯誤。 我們的腳本甚至會因錯誤而停止執行。 但是如果不運行測試,庫中的重大更改可能很難確定,而這在生產中是無法做到的。 在安裝依賴項時,Composer、npm 和其他類似工具依賴於語義版本控制——major.minor.patch。 如果你在composer.json中看到~1.0.2,表示安裝的是1.0.2版本或者最新的補丁版本,比如1.0.4。 如果您看到 ^1.0.2,則表示安裝版本 1.0.2 或最新的次要或補丁版本,例如 1.1.0。 我們相信庫供應商在引入任何重大更改時會增加主要編號,但有時會忽略或不遵循此要求。 過去有過這樣的案例。 即使您將固定版本放在 composer.json 中,您的依賴項也可能在它們的 composer.json 中有 ~ 和 ^。

如果可以訪問,我認為更好的方法是使用工件存儲庫(​​Nexus、JFrog 等)。 最初會創建一次包含所有必要依賴項的發布版本。 該工件將存儲在存儲庫中,並從那裡獲取用於各種測試階段。 此外,這將是部署到生產環境的構建,而不是從 Git 重建應用程序。

保持代碼和數據庫兼容

我第一眼就喜歡上 Laravel 的原因是它的作者非常注重細節,考慮到開發者的便利性,並且在框架中融入了很多最佳實踐,比如數據庫遷移。

數據庫遷移使我們能夠使我們的數據庫和代碼同步。 他們的兩個更改都可以包含在單個提交中,因此可以包含單個發布。 但是,這並不意味著可以在不停機的情況下部署任何更改。 在部署期間的某個時間點,將運行不同版本的應用程序和數據庫。 萬一出現問題,這一點甚至可能變成一個週期。 我們應該始終嘗試使它們都與之前版本的同伴兼容:舊數據庫-新應用程序,新數據庫-舊應用程序。

例如,假設我們有一個address列,需要將其拆分為address1address2 。 為了保持一切兼容,我們可能需要多個版本。

  1. 在數據庫中添加兩個新列。
  2. 盡可能修改應用程序以使用新字段。
  3. address數據遷移到新列並將其刪除。

這個案例也是一個很好的例子,說明小改動如何更好地部署。 他們的回滾也更容易。 如果我們在幾週或幾個月內更改代碼庫和數據庫,則可能無法在不停機的情況下更新生產系統。

Kubernetes的一些令人敬畏的地方

儘管我們的應用程序的規模可能不需要雲、節點和 Kubernetes,但我仍然想提一下 K8s 中的部署是什麼樣的。 在這種情況下,我們不會對系統進行更改,而是聲明我們想要實現什麼以及應該在多少個副本上運行什麼。 然後,Kubernetes 確保實際狀態與所需狀態匹配。

每當我們準備好新版本時,我們都會構建一個包含新文件的鏡像,用新版本標記該鏡像,並將其傳遞給 K8s。 後者將在集群中快速啟動我們的圖像。 根據我們提供的就緒檢查,它將在應用程序準備就緒之前等待,然後將流量重定向到新應用程序並終止舊應用程序。 我們可以很容易地運行我們的應用程序的多個版本,這將使我們只需幾個命令即可執行藍/綠或金絲雀部署。

如果您有興趣,可以在 Burr Sutter 的演講“9 Steps to Awesome with Kubernetes”中進行一些令人印象深刻的演示。

相關:完整的用戶身份驗證和訪問控制 - Laravel Passport 教程,Pt。 1