在 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:权威指南