在 AWS 上使用 Terraform 實現零停機 Jenkins 持續部署

已發表: 2022-03-11

在當今的互聯網世界中,實際上一切都需要 24/7 全天候運行,可靠性是關鍵。 這意味著您的網站的停機時間接近於零,在您推出最新版本時避開可怕的“未找到:404”錯誤頁面或其他服務中斷。

假設您已經為您的客戶或您自己構建了一個新應用程序,並且已經設法獲得了一個喜歡您的應用程序的良好用戶群。 您已經從用戶那裡收集了反饋,然後您去找您的開發人員並要求他們構建新功能並使應用程序為部署做好準備。 準備就緒後,您可以停止整個應用程序並部署新版本,或者構建一個零停機時間 CI/CD 部署管道,這將完成向用戶推送新版本的所有繁瑣工作,而無需人工干預。

在本文中,我們將準確討論後者,我們如何使用 Terraform 作為基礎架構協調器,在 AWS 雲上使用 Node.js 構建的三層 Web 應用程序的持續部署管道。 我們將使用 Jenkins 進行持續部署,並使用 Bitbucket 來託管我們的代碼庫。

代碼庫

我們將使用一個演示三層 Web 應用程序,您可以在此處找到代碼。

該存儲庫包含 Web 和 API 層的代碼。 這是一個簡單的應用程序,其中 Web 模塊調用 API 層中的一個端點,該端點在內部從數據庫中獲取有關當前時間的信息並返回到 Web 層。

回購的結構如下:

  • API: API 層的代碼
  • Web: Web 層的代碼
  • Terraform:使用 Terraform 進行基礎架構編排的代碼
  • Jenkins:用於 CI/CD 管道的 Jenkins 服務器的基礎架構編排器代碼。

現在我們了解了我們需要部署什麼,讓我們討論在 AWS 上部署這個應用程序必須做的事情,然後我們將討論如何使 CI/CD 管道的這一部分。

烘焙圖像

由於我們將 Terraform 用於基礎架構編排器,因此為您要部署的每個層或應用程序預烘焙圖像是最有意義的。 為此,我們將使用 Hashicorp 的另一種產品,即 Packer。

Packer 是一個開源工具,可幫助構建 Amazon 系統映像或 AMI,用於在 AWS 上進行部署。 它可用於為 EC2、VirtualBox、VMware 等不同平台構建映像。

下面是如何使用 Packer 配置文件 ( terraform/packer-ami-api.json ) 為 API 層創建 AMI 的片段。

 { "builders": [{ "type": "amazon-ebs", "region": "eu-west-1", "source_ami": "ami-844e0bf7", "instance_type": "t2.micro", "ssh_username": "ubuntu", "ami_name": "api-instance {{timestamp}}" }], "provisioners": [ { "type": "shell", "inline": ["mkdir api", "sudo apt-get update", "sudo apt-get -y install npm nodejs-legacy"], "pause_before": "10s" }, { "type": "file", "source" : "../api/", "destination" : "api" }, { "type": "shell", "inline": ["cd api", "npm install"], "pause_before": "10s" } ] }

您需要運行以下命令來創建 AMI:

 packer build -machine-readable packer-ami-api.json

我們將在本文後面的 Jenkins 構建中運行此命令。 以類似的方式,我們還將為 Web 層使用 Packer 配置文件 ( terraform/packer-ami-web.json )。

讓我們瀏覽一下上面的 Packer 配置文件,了解它想要做什麼。

  1. 如前所述,Packer 可用於為許多平台構建映像,並且由於我們將應用程序部署到 AWS,我們將使用構建器“amazon-ebs”,因為這是最容易上手的構建器。
  2. 配置的第二部分包含一個配置器列表,這些配置器更像是腳本或代碼塊,您可以使用它們來配置您的圖像。
    • 第 1 步運行 shell 配置程序以創建 API 文件夾並使用inline屬性在映像上安裝 Node.js,這是您要運行的一組命令。
    • 第 2 步運行文件配置器,將我們的源代碼從 API 文件夾複製到實例上。
    • 第 3 步再次運行 shell 配置程序,但這次使用腳本屬性來指定包含需要運行的命令的文件 (terraform/scripts/install_api_software.sh)。
    • 第 4 步將配置文件複製到 Cloudwatch 所需的實例,該實例將在下一步中安裝。
    • 第 5 步運行 shell 預置程序以安裝 AWS Cloudwatch 代理。 此命令的輸入將是在上一步中復制的配置文件。 我們將在本文後面詳細討論 Cloudwatch。

因此,本質上,Packer 配置包含有關您想要哪個構建器的信息,然後是一組配置器,您可以根據您想要配置圖像的方式以任何順序定義這些配置器。

設置 Jenkins 持續部署

接下來,我們將研究設置將用於我們的 CI/CD 管道的 Jenkins 服務器。 我們也將使用 Terraform 和 AWS 進行設置。

用於設置 Jenkins 的 Terraform 代碼位於文件夾jenkins/setup中。 讓我們來看看關於這個設置的一些有趣的事情。

  1. AWS 憑證:您可以向 Terraform AWS 提供商 ( instance.tf ) 提供 AWS 訪問密鑰 ID 和秘密訪問密鑰,也可以將憑證文件的位置提供給 AWS 提供商中的屬性shared_credentials_file
  2. IAM 角色:由於我們將從 Jenkins 服務器運行 Packer 和 Terraform,它們將訪問 AWS 上的 S3、EC2、RDS、IAM、負載平衡和自動縮放服務。 因此,要么我們在 Jenkins 上提供我們的憑據以供 Packer & Terraform 訪問這些服務,要么我們可以創建一個 IAM 配置文件 ( iam.tf ),我們將使用它來創建一個 Jenkins 實例。
  3. Terraform 狀態: Terraform 必須在文件中的某處維護基礎設施的狀態,並且使用 S3 ( backend.tf ),您可以在此處維護它,因此您可以與其他同事協作,並且任何人都可以更改和部署該狀態維護在遠程位置。
  4. 公鑰/私鑰對:您需要將密鑰對的公鑰與實例一起上傳,以便您可以在 Jenkins 實例啟動後通過 ssh 登錄。 我們定義了一個aws_key_pair資源 ( key.tf ),您可以在其中使用 Terraform 變量指定公鑰的位置。

設置 Jenkins 的步驟:

第 1 步:為了保持 Terraform 的遠程狀態,您需要在 S3 中手動創建一個可供 Terraform 使用的存儲桶。 這將是在 Terraform 之外完成的唯一步驟。 確保在運行以下命令之前運行AWS configure以指定您的 AWS 憑證。

 aws s3api create-bucket --bucket node-aws-jenkins-terraform --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1

第 2 步:運行terraform init 。 這將初始化狀態並將其配置為存儲在 S3 上並下載 AWS 提供程序插件。

第 3 步:運行terraform apply 。 這將檢查所有 Terraform 代碼並創建計劃並顯示在此步驟完成後將創建多少資源。

第 4 步:輸入yes ,然後上一步將開始創建所有資源。 命令完成後,您將獲得 Jenkins 服務器的公共 IP 地址。

第 5 步:使用您的私鑰 SSH 進入 Jenkins 服務器。 ubuntu是 AWS EBS 支持的實例的默認用戶名。 使用terraform apply命令返回的 IP 地址。

 ssh -i mykey [email protected]

第 6 步:通過訪問http://34.245.4.73:8080啟動 Jenkins Web UI。 密碼可以在/var/lib/jenkins/secrets/initialAdminPassword

第 7 步:選擇“安裝建議的插件”並為 Jenkins 創建一個管理員用戶。

在 Jenkins 和 Bitbucket 之間設置 CI 管道

  1. 為此,我們需要在 Jenkins 中安裝 Bitbucket 插件。 轉到Manage Jenkins → Manage Plugins並從Available plugins安裝 Bitbucket 插件。
  2. 在 Bitbucket repo 端,轉到Settings → Webhooks ,添加一個新的 webhook。 這個鉤子會將存儲庫中的所有更改發送到 Jenkins,這將觸發管道。
    通過 Bitbucker 為 Jenkins 持續部署添加 webhook

Jenkins Pipeline 烘焙/構建圖像

  1. 下一步將是在 Jenkins 中創建管道。
  2. 第一個管道將是一個 Freestyle 項目,用於使用 Packer 構建應用程序的 AMI。
  3. 您需要為您的 Bitbucket 存儲庫指定憑據和 URL。
    將憑據添加到 bitbucket
  4. 指定構建觸發器。
    配置構建觸發器
  5. 添加兩個構建步驟,一個用於構建應用程序模塊的 AMI,另一個用於構建 Web 模塊的 AMI。
    添加 AMI 構建步驟
  6. 完成此操作後,您可以保存 Jenkins 項目,現在,當您將任何內容推送到您的 Bitbucket 存儲庫時,它將觸發 Jenkins 中的新構建,該構建將創建 AMI 並將包含該圖像的 AMI 編號的 Terraform 文件推送到您可以從構建步驟的最後兩行中看到 S3 存儲桶。
 echo 'variable "WEB_INSTANCE_AMI" { default = "'${AMI_ID_WEB}'" }' > amivar_web.tf aws s3 cp amivar_web.tf s3://node-aws-jenkins-terraform/amivar_web.tf

Jenkins Pipeline 觸發 Terraform 腳本

現在我們有了 API 和 Web 模塊的 AMI,我們將觸發構建以運行 Terraform 代碼以設置整個應用程序,然後通過 Terraform 代碼中的組件,這使得該管道以零停機時間部署更改。

  1. 我們創建了另一個自由式 Jenkins 項目nodejs-terraform ,它將運行 Terraform 代碼來部署應用程序。
  2. 我們將首先在全局憑證域中創建一個“秘密文本”類型的憑證,它將用作 Terraform 腳本的輸入。 由於我們不想在 Terraform 和 Git 中硬編碼 RDS 服務的密碼,我們使用 Jenkins 憑據傳遞該屬性。
    創建用於 Terraform ci cd 的密鑰
  3. 您需要定義與其他項目類似的憑據和 URL。
  4. 在構建觸發器部分,我們將把這個項目與另一個項目聯繫起來,這樣這個項目就會在前一個項目完成時啟動。
    將項目鏈接在一起
  5. 然後我們可以配置我們之前使用綁定添加到項目中的憑據,因此它可以在構建步驟中使用。
    配置綁定
  6. 現在我們準備添加一個構建步驟,它將下載之前項目上傳到 S3 的 Terraform 腳本文件( amivar_api.tfamivar_web.tf ),然後運行 Terraform 代碼以在 AWS 上構建整個應用程序。
    添加構建腳本

如果一切配置正確,現在如果您將任何代碼推送到您的 Bitbucket 存儲庫,它應該會觸發第一個 Jenkins 項目,然後是第二個項目,您應該將您的應用程序部署到 AWS。

適用於 AWS 的 Terraform 零停機時間配置

現在讓我們討論一下是什麼在 Terraform 代碼中使該管道以零停機時間部署代碼。

首先,Terraform 為資源提供了這些生命週期配置塊,您可以在其中選擇create_before_destroy作為標誌,字面意思是 Terraform 應該在銷毀當前資源之前創建相同類型的新資源。

現在我們在aws_autoscaling_groupaws_launch_configuration資源中利用此功能。 因此aws_launch_configuration配置應預置哪種類型的 EC2 實例以及我們如何在該實例上安裝軟件,並且aws_autoscaling_group資源提供 AWS 自動縮放組。

一個有趣的問題是 Terraform 中的所有資源都應該具有唯一的名稱和類型組合。 因此,除非您為新的aws_autoscaling_groupaws_launch_configuration使用不同的名稱,否則無法銷毀當前名稱。

Terraform 通過向aws_launch_configuration資源提供name_prefix屬性來處理此約束。 定義此屬性後,Terraform 將為所有aws_launch_configuration資源添加唯一後綴,然後您可以使用該唯一名稱創建aws_autoscaling_group資源。

您可以在terraform/autoscaling-api.tf中檢查上述所有代碼

resource "aws_launch_configuration" "api-launchconfig" { name_prefix = "api-launchconfig-" image_ instance_type = "t2.micro" security_groups = ["${aws_security_group.api-instance.id}"] user_data = "${data.template_file.api-shell-script.rendered}" iam_instance_profile = "${aws_iam_instance_profile.CloudWatchAgentServerRole-instanceprofile.name}" connection { user = "${var.INSTANCE_USERNAME}" private_key = "${file("${var.PATH_TO_PRIVATE_KEY}")}" } lifecycle { create_before_destroy = true } } resource "aws_autoscaling_group" "api-autoscaling" { name = "${aws_launch_configuration.api-launchconfig.name}-asg" vpc_zone_identifier = ["${aws_subnet.main-public-1.id}"] launch_configuration = "${aws_launch_configuration.api-launchconfig.name}" min_size = 2 max_size = 2 health_check_grace_period = 300 health_check_type = "ELB" load_balancers = ["${aws_elb.api-elb.name}"] force_delete = true lifecycle { create_before_destroy = true } tag { key = "Name" value = "api ec2 instance" propagate_at_launch = true } }

零停機部署的第二個挑戰是確保您的新部署已準備好開始接收請求。 在某些情況下,僅僅部署和啟動一個新的 EC2 實例是不夠的。

為了解決這個問題, aws_launch_configuration有一個屬性user_data ,它支持原生 AWS 自動縮放user_data屬性,您可以使用它作為自動縮放組的一部分傳遞您希望在新實例啟動時運行的任何腳本。 在我們的示例中,我們跟踪應用服務器的日誌並等待啟動消息出現。 您還可以檢查 HTTP 服務器並查看它們何時啟動。

 until tail /var/log/syslog | grep 'node ./bin/www' > /dev/null; do sleep 5; done

除此之外,您還可以在aws_autoscaling_group資源級別啟用 ELB 檢查,這將確保在 Terraform 銷毀舊實例之前添加新實例以通過 ELB 檢查。 這就是 ELB 檢查 API 層的樣子; 它檢查/api/status端點以返回成功。

 resource "aws_elb" "api-elb" { name = "api-elb" subnets = ["${aws_subnet.main-public-1.id}"] security_groups = ["${aws_security_group.elb-securitygroup.id}"] listener { instance_port = "${var.API_PORT}" instance_protocol = "http" lb_port = 80 lb_protocol = "http" } health_check { healthy_threshold = 2 unhealthy_threshold = 2 timeout = 3 target = "HTTP:${var.API_PORT}/api/status" interval = 30 } cross_zone_load_balancing = true connection_draining = true connection_draining_timeout = 400 tags { Name = "my-elb" } }

總結和後續步驟

因此,這將我們帶到了本文的結尾; 希望到現在為止,您已經使用 Jenkins 部署和 Terraform 最佳實踐部署並運行了零停機 CI/CD 管道,或者您更願意探索這個領域並使您的部署需要盡可能少的手動干預可能的。

在本文中,使用的部署策略稱為藍綠部署,其中我們有一個當前安裝(藍色),它在我們部署和測試新版本(綠色)時接收實時流量,然後在新版本發布後替換它們一切準備就緒。 除了這個策略之外,還有其他方法可以部署您的應用程序,這在本文中很好地解釋了部署策略簡介。 現在,調整另一種策略就像配置 Jenkins 管道一樣簡單。

此外,在本文中,我假設 API、Web 和數據層中的所有新更改都是兼容的,因此您不必擔心新版本會與舊版本通信。 但是,實際上,情況可能並非總是如此。 為了解決這個問題,在設計新版本/功能時,請始終考慮向後兼容層,否則您還需要調整部署以處理這種情況。

此部署管道中也缺少集成測試。 由於您不希望在未經測試的情況下向最終用戶發布任何內容,因此在將這些策略應用於您自己的項目時,絕對要牢記這一點。

如果您有興趣了解有關 Terraform 的工作原理以及如何使用該技術部署到 AWS 的更多信息,我推薦Terraform AWS Cloud:Sane Infrastructure Management ,其中 Toptaler Radoslaw Szalski 同事解釋了 Terraform,然後向您展示了配置多- 團隊的環境和生產就緒的 Terraform 設置

相關: Terraform 與 CloudFormation:權威指南