原文:
annas-archive.org/md5/3c34d287e2879a0f121f1884a118ac03译者:飞龙
第十章:使用 Packer 实现不可变基础设施
在上一章中,我们探讨了使用 Ansible 进行配置管理及其核心概念。我们还在 第八章 使用 Terraform 实现基础设施即代码 (IaC) 中讨论了 Terraform 和 IaC。本章中,我们将介绍使用这两种工具以及另一个工具——Packer——来配置基础设施。借助这三种工具,我们将在 Azure 上启动一个可扩展的Linux、Apache、MySQL、PHP(LAMP)堆栈。
在本章中,我们将涵盖以下主要内容:
-
使用 HashiCorp 的 Packer 实现不可变基础设施
-
创建 Apache 和 MySQL playbook
-
使用 Packer 和 Ansible 提供程序构建 Apache 和 MySQL 镜像
-
使用 Terraform 创建所需的基础设施
技术要求
你需要一个有效的 Azure 订阅才能完成本章的练习。目前,Azure 正在提供为期 30 天的免费试用,并赠送 200 美元的免费额度;请在 azure.microsoft.com/en-in/free 注册。
你还需要克隆以下 GitHub 仓库以完成部分练习:
github.com/PacktPublishing/Modern-DevOps-Practices-2e
运行以下命令,将仓库克隆到你的主目录中,并使用 cd 进入 ch10 目录以访问所需的资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch10
你还需要在系统上安装Terraform和Ansible。有关安装和设置 Terraform 和 Ansible 的更多详细信息,请参阅 第八章 使用 Terraform 实现基础设施即代码 (IaC) 和 第九章 使用 Ansible 进行配置管理。
使用 HashiCorp 的 Packer 实现不可变基础设施
假设你是一本书的作者,需要对现有版本进行更改。当你需要进行修改,比如改进内容、修正问题并确保书籍内容是最新时,你不会直接编辑现有的书籍。相反,你会创建一个新的版本,加入所需的更新,同时保持现有版本不变,就像这本书的新版一样。这一概念与不可变基础设施相契合。
在 IT 和系统管理中,不可变基础设施是一种策略,其中,你不会对现有的服务器或虚拟机(VMs)进行修改,而是生成具有所需配置的全新实例。这些新实例会替代旧实例,而不是修改它们,类似于当你想要进行更改时,创建一本新版本的书籍。
其工作原理如下:
-
从零开始构建:当你需要更新基础设施的一部分时,避免直接对现有的服务器或机器进行修改。相反,你会从一个预先建立的模板(镜像)中创建新的实例,并包含更新的配置。
-
不可就地修改:就像不编辑现有的书籍一样,你应避免对当前服务器进行就地修改。这种做法减少了不可预见的变化或配置不一致的风险。
-
一致性:不可变基础设施确保每个服务器或实例都是相同的,因为它们都源自相同的模板。这种统一性对于确保可靠性和可预测性非常重要。
-
滚动更新:当需要实施更新时,你会以受控的方式系统地用新实例替换旧实例。这将最小化停机时间和潜在风险。
-
可扩展性:通过按需生成新实例,扩展基础设施变得轻松自如。这类似于在需求激增或事物过时时发布新版本的书籍。
-
回滚和恢复:如果更新过程中出现问题,你可以通过从已知的良好模板重新创建实例,迅速恢复到之前的版本。
因此,将不可变基础设施视为通过创建新的、改进的实例来维护基础设施的一种方式,而不是试图修订或修改现有的实例。这种方法提升了 IT 环境中的一致性、可靠性和可预测性。
为了进一步理解这一点,让我们考虑通过 Terraform 和 Ansible 设置应用程序的传统方法。我们会使用 Terraform 启动基础设施,然后使用 Ansible 在其上应用相关的配置。这就是我们在上一章所做的。虽然这是可行的方法,许多企业都在使用它,但有一种更好的方法,可以通过现代 DevOps 方法和不可变基础设施来实现。
不可变基础设施是一个突破性的概念,它的出现是因为可变基础设施所带来的问题。在可变基础设施的方法中,我们通常会在原地更新服务器。因此,当我们使用 Ansible 安装 Apache 并进一步自定义时,我们遵循的是可变过程。我们可能需要定期更新服务器、打补丁、将 Apache 升级到新版本,或更新应用程序代码。
这种方法的问题在于,虽然我们可以使用 Ansible(或类似工具,如Puppet、Chef和SaltStack)很好地管理它,但问题始终存在,即我们在生产环境中进行实时更改,这可能因各种原因出现问题。更糟糕的是,这可能会更新我们最初没有预见到或测试过的内容。我们也可能最终处于部分升级状态,且此状态可能难以回滚。
借助云提供的可扩展基础设施,你可以拥有一个动态的横向扩展模型,虚拟机根据流量进行扩展。因此,你可以实现最佳的基础设施利用率——用最少的投入获得最大的回报!传统方法的问题在于,即使我们使用 Ansible 将配置应用到新机器上,准备好镜像的速度仍然较慢。因此,扩展并不理想,特别是对于流量突增的情况。
不可变基础设施通过采用与我们在容器中使用的相同方法来帮助你解决这些问题——通过现代的 DevOps 工具和实践将配置直接烘焙到操作系统镜像中。不可变基础设施通过替换现有虚拟机而不是在原地进行更新来帮助你将经过测试的配置部署到生产环境。它启动更快,回滚也更容易。你还可以通过这种方法对基础设施变更进行版本管理。
HashiCorp 提供了一套出色的与基础设施和配置管理相关的 DevOps 产品。HashiCorp 提供 Packer 来帮助你通过直接将配置烘焙到虚拟机镜像中,从而创建不可变基础设施,而不是先使用通用的操作系统镜像创建虚拟机,再后续进行自定义的慢速过程。它的工作原理与 Docker 用于烘焙容器镜像的原理类似;也就是说,你定义一个模板(配置文件),指定源镜像、所需配置以及设置镜像上软件所需的任何提供步骤。然后,Packer 会通过创建一个临时实例来构建镜像,应用已定义的配置,并捕获机器镜像以供重复使用。
Packer 提供以下一些关键功能:
-
多平台支持:Packer 基于插件架构,因此可以用于为许多不同的云平台和本地平台创建虚拟机镜像,如 VMware、Oracle VirtualBox、Amazon EC2、Azure 的 ARM、Google Cloud Compute 以及 Docker 或其他容器运行时的容器镜像。
-
自动化:Packer 自动化镜像创建,消除了手动构建镜像的工作。它还帮助你实现多云战略,因为你可以使用单一配置为各种平台构建镜像。
-
促进 GitOps:Packer 配置是机器可读的,并且以 HCL 或 JSON 格式编写,因此可以轻松与代码一起存放。因此,这促进了 GitOps。
-
与其他工具的集成:Packer 与其他 HashiCorp 工具(如 Terraform 和 Vagrant)集成良好。
Packer 使用一个临时虚拟机来定制镜像。以下是 Packer 在构建自定义镜像时遵循的过程:
-
你从 Packer 配置的 HCL 文件开始,定义你想要启动的基础镜像以及构建镜像的地方。你还需要定义用于构建自定义镜像的提供者,如 Ansible,并指定要使用的剧本。
-
当你运行 Packer 构建时,Packer 使用配置文件中的细节,从基础镜像创建构建虚拟机,运行配置工具进行定制,关闭构建虚拟机,拍摄快照,并将其保存为磁盘镜像。最后,它将镜像保存在镜像仓库中。
-
你可以使用 Terraform 或其他工具,从自定义镜像构建虚拟机。
下图详细解释了该过程:
图 10.1 – Packer 构建过程
结果是你的应用程序启动迅速,扩展性非常好。对于配置中的任何更改,使用 Packer 和 Ansible 创建一个新的磁盘镜像,然后通过 Terraform 将更改应用到你的资源上。Terraform 会停止旧的虚拟机并启动新的虚拟机,应用新的配置。如果你能将其与容器部署工作流联系起来,你会更好地理解这一点。这就像在虚拟机世界中使用容器工作流一样!但不可变基础设施适合所有人吗?让我们来理解它最适合的场景。
何时使用不可变基础设施
决定切换到不可变基础设施是很困难的,特别是当你的运维团队将服务器视为宠物时。大多数人对于删除现有服务器并为每次更新创建新服务器的想法感到疑虑重重。嗯,当你第一次提出这个想法时,你需要做很多说服工作。然而,这并不意味着你必须使用不可变基础设施才能做好 DevOps。最终取决于你的使用场景。
让我们通过分析每种方法的优缺点来更好地理解它们。
可变基础设施的优点
我们先从可变基础设施的优点开始:
-
如果管理得当,可变基础设施的升级和变更速度较快。安全补丁的应用也更为迅速。
-
它更易于管理,因为我们不必担心为每次更新构建整个虚拟机镜像并重新部署它。
可变基础设施的缺点
接下来,让我们看看可变基础设施的缺点:
-
它最终会导致配置漂移。当人们开始在服务器上手动进行更改并且不使用配置管理工具时,之后你很难知道服务器在某个特定时间点上的状态。然后,你将不得不开始依赖快照。
-
在可变基础设施中,无法进行版本控制,回滚更改也很麻烦。
-
由于技术问题,例如网络不稳定、apt 仓库无响应等,可能会出现部分更新的情况。
-
由于更改直接应用到生产环境中,因此存在一定风险。你也有可能陷入一个难以排查的意外状态。
-
由于配置漂移,无法保证当前的配置与版本控制中记录的配置一致。因此,从零开始构建新服务器可能需要手动干预和全面测试。
同样,让我们看看不可变基础设施的优缺点。
不可变基础设施的优点
不可变基础设施的优点如下:
-
它消除了配置漂移,因为一旦部署了基础设施,基础设施就不能改变,任何更改都应通过 CI/CD 流程进行。
-
它对 DevOps 友好,因为每个构建和部署过程本质上遵循现代 DevOps 实践。
-
它使得离散版本控制成为可能,因为从镜像构建生成的每个镜像都可以进行版本控制并保存在镜像仓库中。这使得推出和回滚变得更加简单,并促进现代 DevOps 实践,如金丝雀和蓝绿部署以及 A/B 测试。
-
镜像是预构建和经过测试的,因此我们总是从不可变基础设施中获得可预测的状态。因此,我们从生产实施中减少了很多风险。
-
它有助于云上的水平扩展,因为您现在可以从预构建的镜像创建服务器,使得新的虚拟机启动更快且准备就绪。
不可变基础设施的缺点
不可变基础设施的缺点如下:
-
构建和部署不可变基础设施有些复杂,并且增加更新和管理紧急修复的速度比较慢。
-
生成和管理 VM 镜像存在存储和网络开销
因此,当我们看了两种方法的优缺点之后,最终取决于您当前如何进行基础设施管理以及您的最终目标。不可变基础设施有巨大的好处,因此,每个现代 DevOps 工程师都应该理解并尽可能实现它。然而,技术和流程约束阻止了人们的尝试 - 虽然一些约束与技术堆栈有关,但大多数仅与流程和官僚主义有关。不可变基础设施在需要一致可重现和异常可靠的部署时尤为有利。这种方法通过重建整个环境而不是调整现有元素,最小化了配置漂移的风险,并简化了更新过程。在微服务架构、容器编排以及需要快速扩展和能够回滚更改的场景中,特别有优势。
我们都知道 DevOps 不仅仅关乎工具,而是一种应该从高层发源的文化变革。如果不可能使用不可变基础设施,您总是可以在活跃服务器上使用像 Ansible 这样的配置管理工具。这在一定程度上使事物变得可管理。
现在,继续讲解 Packer,让我们看看如何安装它。
安装 Packer
您可以在各种平台上以多种方式安装 Packer。请参考developer.hashicorp.com/packer/downloads。由于 Packer 作为apt包可用,请使用以下命令在 Ubuntu Linux 上安装 Packer:
$ wget -O- https://apt.releases.hashicorp.com/gpg | sudo \
gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
$ sudo apt update && sudo apt install -y packer
为了验证安装情况,请运行以下命令:
$ packer --version
1.9.2
如我们所见,Packer 安装成功。我们可以继续进行我们的下一个目标活动——创建 playbook。
创建 Apache 和 MySQL playbook
由于我们本章的目标是启动一个可扩展的 LAMP 堆栈,因此我们必须首先定义将在构建 VM 上运行的 Ansible playbook。我们已经在第九章,“使用 Ansible 的配置管理”中为 Apache 和 MySQL 创建了一些角色。我们将在此设置中使用相同的角色。
因此,我们将在 ch10 目录中拥有以下目录结构:
├── ansible
│ ├── dbserver-playbook.yaml
│ ├── roles
│ │ ├── apache
│ │ ├── common
│ │ └── mysql
│ └── webserver-playbook.yaml
├── packer
│ ├── dbserver.pkr.hcl
│ ├── plugins.pkr.hcl
│ ├── variables.pkr.hcl
│ ├── variables.pkrvars.hcl
│ └── webserver.pkr.hcl
└── terraform
├── main.tf
├── outputs.tf
├── terraform.tfvars
└── vars.tf
我们在 ansible 目录中有两个 playbook——webserver-playbook.yaml 和 dbserver-playbook.yaml。让我们分别看看它们,了解如何为 Ansible 编写 playbook。
webserver-playbook.yaml 文件内容如下:
---
- hosts: default
become: true
roles:
- common
- apache
dbserver-playbook.yaml 文件内容如下:
---
- hosts: default
become: true
roles:
- common
- mysql
如我们所见,两个 playbook 的 hosts 都设置为 default。这是因为我们不会为此 playbook 定义清单。相反,Packer 将使用构建 VM 来构建镜像并动态生成清单。
注意
Packer 还会忽略任务中的任何 remote_user 属性,并使用 Ansible provisioner 配置中的用户。
如我们在上一章中已经测试过此配置,现在我们需要做的就是定义 Packer 配置,接下来让我们在下一章节中进行操作。
使用 Packer 和 Ansible provisioner 构建 Apache 和 MySQL 镜像
现在,我们将使用 Packer 创建 Apache 和 MySQL 镜像。在定义 Packer 配置之前,我们有几个前提条件,以允许 Packer 构建自定义镜像。
前提条件
我们必须为 Packer 创建一个 Azure 服务主体,以便它与 Azure 进行交互并构建镜像。
首先,使用以下命令通过 Azure CLI 登录到你的 Azure 帐户:
$ az login
现在,使用以下命令将订阅设置为我们从 az login 命令响应中获取的订阅 ID,并将其存储为环境变量:
$ export SUBSCRIPTION_ID=<SUBSCRIPTION_ID>
接下来,让我们使用以下命令设置订阅 ID:
$ az account set --subscription="${SUBSCRIPTION_ID}"
然后,使用以下命令创建具有贡献者访问权限的服务主体:
$ az ad sp create-for-rbac --role="Contributor" \
--scopes="/subscriptions/${SUBSCRIPTION_ID}"
{"appId": "00000000-0000-0000-0000-00000", "name": "http://azure-
cli-2021-01-07-05-59-24", "password": "xxxxxxxxxxxxxxxxxxxxxxxx", "tenant": "00000000-
0000-0000-0000-0000000000000"}
我们已经成功创建了服务主体。响应的 JSON 包含了 appId、password 和 tenant 值,我们将在接下来的章节中使用这些值。
注意
你还可以重用我们在第八章,“使用 Terraform 进行基础设施即代码 (IaC)”中创建的服务主体。
现在,让我们继续在 packer/variables.pkrvars.hcl 文件中设置这些变量的值,具体内容如下:
client_id = "<VALUE_OF_APP_ID>"
client_secret = "<VALUE_OF_PASSWORD>"
tenant_id = "<VALUE_OF_TENANT>"
subscription_id = "<SUBSCRIPTION_ID>"
我们将在 Packer 构建中使用变量文件。我们还需要一个资源组来存储构建的镜像。
要创建资源组,请运行以下命令:
$ az group create -n packer-rg -l eastus
现在,让我们继续定义 Packer 配置。
定义 Packer 配置
Packer 允许我们在 JSON 和 HCL 文件中定义配置。由于 JSON 已被弃用且 HCL 是推荐格式,因此我们将使用 HCL 来定义 Packer 配置。
要访问本节的资源,请切换到以下目录:
$ cd ~/modern-devops/ch10/packer
我们将在 packer 目录中创建以下文件:
-
variables.pkr.hcl:包含我们在应用配置时使用的变量列表 -
plugins.pkr.hcl:包含 Packer 插件配置 -
webserver.pkr.hcl:包含构建 web 服务器镜像的 Packer 配置 -
dbserver.pkr.hcl:包含构建dbserver镜像的 Packer 配置 -
variables.pkrvars.hcl:包含variables.pkr.hcl文件中定义的 Packer 变量的值
variables.pkr.hcl 文件包含以下内容:
variable "client_id" {
type = string
}
variable "client_secret" {
type = string
}
variable "subscription_id" {
type = string
}
variable "tenant_id" {
type = string
}
variables.pkr.hcl 文件定义了一个用户变量列表,我们可以在 Packer 配置的 source 和 build 块中使用。我们定义了四个字符串变量——client_id、client_secret、tenant_id 和 subscription_id。我们可以通过使用在上一节中定义的 variables.pkrvars.hcl 变量文件来传递这些变量的值。
提示
始终通过外部变量提供敏感数据,如变量文件、环境变量或秘密管理工具,如 HashiCorp 的 Vault。绝不应将敏感信息与代码一起提交。
plugins.pkr.hcl 文件包含以下块:
packer:此部分定义了 Packer 的通用配置。在此案例中,我们定义了构建镜像所需的插件。这里定义了两个插件——ansible 和 azure。插件包含 source 和 version 属性,包含与技术组件交互所需的一切:
packer {
required_plugins {
ansible = {
source = "github.com/hashicorp/ansible"
version = "=1.1.0"
}
azure = {
source = "github.com/hashicorp/azure"
version = "=1.4.5"
}
}
}
webserver.pkr.hcl 文件包含以下几个部分:
source:source块包含我们用于构建虚拟机的配置。由于我们正在构建一个azure-arm镜像,我们将源定义如下:
source "azure-arm" "webserver" {
client_id = var.client_id
client_secret = var.client_secret
image_offer = "UbuntuServer"
image_publisher = "Canonical"
image_sku = "18.04-LTS"
location = "East US"
managed_image_name = "apache-webserver"
managed_image_resource_group_name = "packer-rg"
os_type = "Linux"
subscription_id = var.subscription_id
tenant_id = var.tenant_id
vm_size = "Standard_DS2_v2"
azure-arm and consists of client_id, client_secret, tenant_id, and subscription_id, which helps Packer authenticate with the Azure API server. These attributes’ values are sourced from the variables.pkr.hcl file.
Tip
The managed image name can also contain a version. That will help you build a new image for every new version you want to deploy.
* `build`: The `build` block consists of `sources` and `provisioner` attributes. It contains all the sources we want to use, and the `provisioner` attribute allows us to configure the build VM to achieve the desired configuration. We’ve defined the following `build` block:
build {
sources = [“source.azure-arm.webserver”]
provisioner “ansible” {
playbook_file = “…/ansible/webserver-playbook.yaml”
}
…/ansible/webserver-playbook.yaml。
提示
你可以在 `build` 块中指定多个源,每个源可以是相同或不同类型。类似地,我们可以拥有多个提供者,它们会并行执行。因此,如果你想为多个云提供商构建相同的配置,可以为每个云提供商指定多个源。
类似地,我们定义了以下 `dbserver.pkr.hcl` 文件:
source "azure-arm" "dbserver" {
...
managed_image_name = "mysql-dbserver"
...
}
build {
sources = ["source.azure-arm.dbserver"]
provisioner "ansible" {
playbook_file = "../ansible/dbserver-playbook.yaml"
}
}
`source` 块的配置与 web 服务器相同,除了 `managed_image_name`。`build` 块也类似于 web 服务器,但它使用的是 `../ansible/dbserver-playbook.yaml` playbook。
现在,让我们看看 Packer 的工作流以及如何使用它来构建镜像。
构建镜像的 Packer 工作流
Packer 工作流包括两个步骤——`init` 和 `build`。
正如我们所知,Packer 使用插件与云服务商进行交互;因此,我们需要安装这些插件。为此,Packer 提供了`init`命令。
让我们使用以下命令初始化并安装所需的插件:
$ packer init .
Installed plugin github.com/hashicorp/ansible v1.1.0 in "~/.config/packer/plugins/github.
com/hashicorp/ansible/packer-plugin-ansible_v1.1.0_x5.0_linux_amd64"
Installed plugin github.com/hashicorp/azure v1.4.5 in "~/.config/packer/plugins/github.
com/hashicorp/azure/packer-plugin-azure_v1.4.5_x5.0_linux_amd64"
如我们所见,插件现在已安装。让我们继续构建镜像。
我们使用`build`命令通过 Packer 创建镜像。由于我们需要传递值给变量,我们将通过命令行参数指定变量值,如以下命令所示:
$ packer build -var-file="variables.pkrvars.hcl" .
Packer 将使用`webserver`和`dbserver`配置构建并行堆栈。
Packer 首先创建临时资源组来启动暂存的 VM:
==> azure-arm.webserver: Creating resource group ...
==> azure-arm.webserver: -> ResourceGroupName : 'pkr-Resource-Group-7dfj1c2iej'
==> azure-arm.webserver: -> Location : 'East US'
==> azure-arm.dbserver: Creating resource group ...
==> azure-arm.dbserver: -> ResourceGroupName : 'pkr-Resource-Group-11xqpuxsm3'
==> azure-arm.dbserver: -> Location : 'East US'
Packer 接着验证并部署部署模板,并获取暂存 VM 的 IP 地址:
==> azure-arm.webserver: Validating deployment template ...
==> azure-arm.webserver: Deploying deployment template ...
==> azure-arm.webserver: -> DeploymentName : 'pkrdp7dfj1c2iej'
==> azure-arm.webserver: Getting the VM's IP address ...
==> azure-arm.webserver: -> IP Address : '104.41.158.85'
==> azure-arm.dbserver: Validating deployment template ...
==> azure-arm.dbserver: Deploying deployment template ...
==> azure-arm.dbserver: -> DeploymentName : 'pkrdp11xqpuxsm3'
==> azure-arm.dbserver: Getting the VM's IP address ...
==> azure-arm.dbserver: -> IP Address : '40.114.7.11'
然后,Packer 使用 SSH 连接到暂存的 VM,并使用 Ansible 为其配置:
==> azure-arm.webserver: Waiting for SSH to become available...
==> azure-arm.dbserver: Waiting for SSH to become available...
==> azure-arm.webserver: Connected to SSH!
==> azure-arm.dbserver: Connected to SSH!
==> azure-arm.webserver: Provisioning with Ansible...
==> azure-arm.dbserver: Provisioning with Ansible...
==> azure-arm.webserver: Executing Ansible: ansible-playbook -e packer_build_
name="webserver" -e packer_builder_type=azure-arm --ssh-extra-args '-o IdentitiesOnly=yes'
-e ansible_ssh_private_key_file=/tmp/ansible-key328774773 -i /tmp/packer-provisioner-
ansible747322992 ~/ansible/webserver-playbook.yaml
==> azure-arm.dbserver: Executing Ansible: ansible-playbook -e packer_build_
name="dbserver" -e packer_builder_type=azure-arm --ssh-extra-args '-o IdentitiesOnly=yes'
-e ansible_ssh_private_key_file=/tmp/ansible-key906086565 -i /tmp/packer-provisioner-
ansible3847259155 ~/ansible/dbserver-playbook.yaml
azure-arm.webserver: PLAY RECAP *********************************************************
**
azure-arm.webserver: default: ok=7 changed=5 unreachable=0 failed=0 skipped=0 rescued=0
ignored=0
azure-arm.dbserver: PLAY RECAP ***********************************************************
azure-arm.dbserver: default: ok=11 changed=7 unreachable=0 failed=0 skipped=0 rescued=0
ignored=0
一旦 Ansible 运行完成,Packer 会获取磁盘详情,捕获镜像,并在我们在 Packer 配置中指定的资源组中创建机器镜像:
==> azure-arm.webserver: Querying the machine's properties
==> azure-arm.dbserver: Querying the machine's properties
==> azure-arm.webserver: Querying the machine's additional disks properties ...
==> azure-arm.dbserver: Querying the machine's additional disks properties ...
==> azure-arm.webserver: Powering off machine ...
==> azure-arm.dbserver: Powering off machine ...
==> azure-arm.webserver: Generalizing machine ...
==> azure-arm.dbserver: Generalizing machine ...
==> azure-arm.webserver: Capturing image ...
==> azure-arm.dbserver: Capturing image ...
==> azure-arm.webserver: -> Image ResourceGroupName: 'packer-rg'
==> azure-arm.dbserver: -> Image ResourceGroupName: 'packer-rg'
==> azure-arm.webserver: -> Image Name: 'apache-webserver'
==> azure-arm.webserver: -> Image Location: 'East US'
==> azure-arm.dbserver: -> Image Name: 'mysql-dbserver'
==> azure-arm.dbserver: -> Image Location: 'East US'
最后,它移除部署对象和它所创建的临时资源组:
==> azure-arm.webserver: Deleting Virtual Machine deployment and its attached resources...
==> azure-arm.dbserver: Deleting Virtual Machine deployment and its attached resources...
==> azure-arm.webserver: Cleanup requested, deleting resource group ...
==> azure-arm.dbserver: Cleanup requested, deleting resource group ...
==> azure-arm.webserver: Resource group has been deleted.
==> azure-arm.dbserver: Resource group has been deleted.
然后,它会提供它所生成的工件列表:
==> Builds finished. The artifacts of successful builds are:
--> azure-arm: Azure.ResourceManagement.VMImage:
OSType: Linux
ManagedImageResourceGroupName: packer-rg
ManagedImageName: apache-webserver
ManagedImageId: /subscriptions/Id/resourceGroups/packer-rg/providers/Microsoft.Compute/
images/apache-webserver
ManagedImageLocation: West Europe
OSType: Linux
ManagedImageResourceGroupName: packer-rg
ManagedImageName: mysql-dbserver
ManagedImageId: /subscriptions/Id/resourceGroups/packer-rg/providers/Microsoft.Compute/
images/mysql-dbserver
如果我们查看`packer-rg`资源组,我们会发现其中有两个 VM 镜像:
<https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_10_2.jpg>
图 10.2 – Packer 自定义镜像
我们已经成功地用 Packer 构建了自定义镜像!
小贴士
一旦镜像在资源组中创建,就无法使用相同的托管镜像名称重新运行 Packer。这是因为我们不希望意外覆盖现有镜像。虽然你可以通过使用`-force`标志与`packer build`来覆盖它,但应该在镜像名称中包含版本号,以便在资源组中允许多个版本的镜像存在。例如,使用`apache-webserver-0.0.1`而不是`apache-webserver`。
现在是使用这些镜像并用它们创建我们基础设施的时候了。
使用 Terraform 创建所需的基础设施
我们的目标是构建一个可扩展的 LAMP 堆栈,因此我们将定义一个我们创建的`apache-webserver`镜像和一个使用`mysql-dbserver`镜像的虚拟机。VM 规模集是一个 VM 的自动扩展组,它将根据流量横向扩展和收缩,就像我们在 Kubernetes 中使用容器时做的一样。
我们将创建以下资源:
+ 一个名为`lamp-rg`的新资源组
+ 一个名为`lampvnet`的虚拟网络位于资源组内
+ 一个名为`lampsub`的子网,位于`lampvnet`内
+ 在子网内,我们创建了一个包含以下内容的`db-nic`:
+ 一个名为`db-nsg`的网络安全组
+ 一个名为`db`的虚拟机,使用自定义的`mysql-dbserver`镜像
+ 然后,我们创建一个包括以下内容的 VM 规模集:
+ 一个名为`webnp`的网络配置文件
+ 一个后端地址池
+ 一个名为`web-lb`的负载均衡器
+ 附加到`web-lb`的公共 IP 地址
+ 一个检查`80`端口健康状况的 HTTP 探针
以下图形说明了拓扑结构:
<https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_10_3.jpg>
图 10.3 – 可扩展 LAMP 堆栈拓扑图
要访问本节的资源,请切换到以下目录:
$ cd ~/modern-devops/ch10/terraform
我们使用以下 Terraform 模板,`main.tf`,来定义配置。
我们首先定义 Terraform 提供程序:
terraform {
required_providers {
azurerm = {
source = "azurerm"
}
}
}
provider "azurerm" {
subscription_id = var.subscription_id
client_id = var.client_id
client_secret = var.client_secret
tenant_id = var.tenant_id
}
然后我们定义自定义镜像数据源,以便在我们的配置中使用它们:
data "azurerm_image" "websig" {
name = "apache-webserver"
resource_group_name = "packer-rg"
}
data "azurerm_image" "dbsig" {
name = "mysql-dbserver"
resource_group_name = "packer-rg"
}
然后我们定义资源组、虚拟网络和子网:
resource "azurerm_resource_group" "main" {
name = var.rg_name
location = var.location
}
resource "azurerm_virtual_network" "main" {
name = "lampvnet"
address_space = ["10.0.0.0/16"]
location = var.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "main" {
name = "lampsub"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.2.0/24"]
}
由于 Apache Web 服务器将位于网络负载均衡器后面,我们将定义负载均衡器和我们将附加到其上的公共 IP 地址:
resource "azurerm_public_ip" "main" {
name = "webip"
location = var.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Static"
domain_name_label = azurerm_resource_group.main.name
}
resource "azurerm_lb" "main" {
name = "web-lb"
location = var.location
resource_group_name = azurerm_resource_group.main.name
frontend_ip_configuration {
name = "PublicIPAddress"
public_ip_address_id = azurerm_public_ip.main.id
}
tags = {}
}
然后我们将定义一个后端地址池,附加到负载均衡器,以便我们可以在 Apache 虚拟机规模集中使用它:
resource "azurerm_lb_backend_address_pool" "bpepool" {
loadbalancer_id = azurerm_lb.main.id
name = "BackEndAddressPool"
}
我们将在端口`80`上定义一个 HTTP 探测器,用于健康检查,并将其附加到负载均衡器:
resource "azurerm_lb_probe" "main" {
loadbalancer_id = azurerm_lb.main.id
name = "http-running-probe"
port = 80
}
我们需要在负载均衡器上设置`80`端口,并将其与后端池虚拟机的`80`端口关联。我们还将在此配置中附加 HTTP 健康检查探测器:
resource "azurerm_lb_rule" "lbnatrule" {
resource_group_name = azurerm_resource_group.main.name
loadbalancer_id = azurerm_lb.main.id
name = "http"
protocol = "Tcp"
frontend_port = 80
backend_port = 80
backend_address_pool_ids = [ azurerm_lb_backend_address_pool.bpepool.id ]
frontend_ip_configuration_name = "PublicIPAddress"
probe_id = azurerm_lb_probe.main.id
}
现在,我们将在资源组内使用自定义镜像和之前定义的负载均衡器来定义虚拟机规模集:
resource "azurerm_virtual_machine_scale_set" "main" {
name = "webscaleset"
location = var.location
resource_group_name = azurerm_resource_group.main.name
upgrade_policy_mode = "Manual"
sku {
name = "Standard_DS1_v2"
tier = "Standard"
capacity = 2
}
storage_profile_image_reference {
id=data.azurerm_image.websig.id
}
然后我们继续定义操作系统磁盘和数据磁盘:
storage_profile_os_disk {
name = ""
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
storage_profile_data_disk {
lun = 0
caching = "ReadWrite"
create_option = "Empty"
disk_size_gb = 10
}
操作系统配置文件定义了我们如何登录到虚拟机:
os_profile {
computer_name_prefix = "web"
admin_username = var.admin_username
admin_password = var.admin_password
}
os_profile_linux_config {
disable_password_authentication = false
}
然后我们定义一个网络配置文件,将规模集与之前定义的负载均衡器关联:
network_profile {
name = "webnp"
primary = true
ip_configuration {
name = "IPConfiguration"
subnet_id = azurerm_subnet.main.id
load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.bpepool.id]
primary = true
}
}
tags = {}
}
现在,进入数据库配置,我们将首先为数据库服务器定义一个网络安全组,以允许从虚拟网络内的内部服务器访问端口`22`和`3306`:
resource "azurerm_network_security_group" "db_nsg" {
name = "db-nsg"
location = var.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "SSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "SQL"
priority = 1002
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3306"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = {}
}
然后我们定义一个网络接口卡(NIC)为虚拟机提供内部 IP 地址:
resource "azurerm_network_interface" "db" {
name = "db-nic"
location = var.location
resource_group_name = azurerm_resource_group.main.name
ip_configuration {
name = "db-ipconfiguration"
subnet_id = azurerm_subnet.main.id
private_ip_address_allocation = "Dynamic"
}
}
然后我们将网络安全组与网络接口关联:
resource "azurerm_network_interface_security_group_association" "db" {
network_interface_id = azurerm_network_interface.db.id
network_security_group_id = azurerm_network_security_group.db_nsg.id
}
最后,我们将使用自定义镜像定义数据库虚拟机:
resource "azurerm_virtual_machine" "db" {
name = "db"
location = var.location
resource_group_name = azurerm_resource_group.main.name
network_interface_ids = [azurerm_network_interface.db.id]
vm_size = var.vm_size
delete_os_disk_on_termination = true
storage_image_reference {
id = data.azurerm_image.dbsig.id
}
storage_os_disk {
name = "db-osdisk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = "db"
admin_username = var.admin_username
admin_password = var.admin_password
}
os_profile_linux_config {
disable_password_authentication = false
}
tags = {}
}
现在,既然我们已经定义了所需的一切,请填写`terraform.tfvars`文件中的必要信息,然后使用以下命令初始化我们的 Terraform 工作区:
$ terraform init
由于 Terraform 已成功初始化,请使用以下命令应用 Terraform 配置:
$ terraform apply
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.
Outputs:
web_ip_addr = "40.115.61.69"
由于 Terraform 已应用配置并提供了负载均衡器 IP 地址作为输出,我们使用该地址访问 Web 服务器:
<https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_10_4.jpg>
图 10.4 – LAMP 堆栈正常工作
当我们收到`数据库成功连接`消息时,我们看到配置成功!我们已成功使用 Packer、Ansible 和 Terraform 创建了一个可扩展的 LAMP 堆栈。它结合了*基础设施即代码(IaC)*、*配置即代码*、*不可变基础设施* 和现代 DevOps 实践,创建了一个无须人工干预的无缝环境。
总结
在本章中,我们介绍了使用 Packer 构建不可变基础设施。我们使用 Packer 配合 Ansible 提供程序构建了用于 Apache 和 MySQL 的自定义镜像。然后使用这些自定义镜像通过 Terraform 创建了一个可扩展的 LAMP 堆栈。本章向你介绍了现代 DevOps 的时代,在这个时代中,一切都实现了自动化。我们遵循相同的原则来构建和部署各种类型的基础设施,无论是容器还是虚拟机。在下一章中,我们将讨论 DevOps 中最重要的主题之一——**持续集成**。
问题
1. 不可变基础设施有助于避免配置漂移。(正确/错误)
1. 最佳实践是从外部变量(如环境变量)或像 HashiCorp 的 Vault 这样的秘密管理工具中获取敏感数据。(正确/错误)
1. 我们需要对现有的 playbook 做哪些修改,以便 Packer 能够使用它们?
A. 从当前工作目录中删除任何现有的 `ansible.cfg` 文件。
B. 从当前工作目录中删除任何主机文件。
C. 在 playbook 中将 `hosts` 属性更新为默认值。
D. 以上都不是。
1. 以下哪些是使用 Ansible 提供程序与 Packer 配合使用时的限制?(选择两个)
A. 你不能将 Jinja2 宏原样传递到 Ansible playbook 中。
B. 你不能在 Ansible playbook 中定义 `remote_user`。
C. 你不能在 Ansible playbook 中使用 Jinja2 模板。
D. 你不能在 Ansible playbook 中使用角色和变量。
1. 在命名托管镜像时,我们应考虑哪些因素?(选择两个)
A. 尽可能具体地命名镜像。
B. 将版本作为镜像的一部分。
C. 不要将版本作为镜像名称的一部分。相反,总是使用 `-force` 标志来构建 Packer。
1. 使用多个提供程序时,如何将配置应用于构建虚拟机?
A. 按照 HCL 文件中的顺序逐一执行
B. 并行
1. 我们可以使用一组 Packer 文件,在多个云环境中构建具有相同配置的镜像。(正确/错误)
1. 虚拟机规模集提供了哪些功能?(选择两个)
A. 它帮助你根据流量水平扩展虚拟机实例。
B. 它帮助你自动修复故障虚拟机。
C. 它帮助你进行金丝雀发布。
D. 以上都不是。
答案
1. 正确
1. 正确
1. C
1. A、B
1. A、B
1. B
1. 正确
1. A、B、C
第四部分:使用 GitOps 交付应用程序
本节是本书的核心内容,阐明了在云中有效实施现代 DevOps 的各种工具和技术。以 GitOps 作为核心指导原则,我们将探讨各种工具和技术,帮助我们不断地构建、测试、保护并将应用程序部署到开发、测试和生产环境中。
本部分包含以下章节:
-
第十一章,使用 GitHub Actions 和 Jenkins 的持续集成
-
第十二章,使用 Argo CD 的持续部署/交付
-
第十三章,确保和测试你的 CI/CD 流水线
第十一章:使用 GitHub Actions 和 Jenkins 进行持续集成
在前面的章节中,我们讨论了几个单独的工具,这些工具将帮助我们实现现代 DevOps 的多个方面。现在,是时候看看如何将我们学到的所有工具和概念结合起来,创建一个持续集成(CI)流水线了。首先,我们将介绍一个基于微服务的示例博客应用程序——Blog App,然后看看一些流行的开源和基于 SaaS 的工具,它们可以帮助我们快速启动 CI。我们将从GitHub Actions开始,然后转到Jenkins与Kaniko。对于每个工具,我们都会为 Blog App 实现 CI。我们会尽量保持实现与云平台无关。由于我们从一开始就使用了GitOps方法,因此这里也将使用相同的方法。最后,我们将讨论一些与构建性能相关的最佳实践。
在本章中,我们将涵盖以下主要主题:
-
自动化的重要性
-
示例基于微服务的博客应用程序介绍——Blog App
-
使用 GitHub Actions 构建 CI 流水线
-
在Kubernetes上可扩展的 Jenkins 与 Kaniko
-
使用触发器自动化构建
-
构建性能的最佳实践
技术要求
对于本章,你需要克隆以下 GitHub 仓库,以便进行一些练习:github.com/PacktPublishing/Modern-DevOps-Practices-2e。
运行以下命令,将仓库克隆到你的主目录,并cd进入ch11目录以访问所需的资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch11
那么,让我们开始吧!
自动化的重要性
自动化就像是有一支高效的机器人团队在为你工作,毫不疲倦地处理重复性、耗时且容易出错的任务。让我们简化一下自动化的意义:
-
效率:把它想象成拥有一个神奇的助手,可以在你完成任务的时间里迅速完成。自动化加速了重复性任务的执行,处理数据和运行命令比人类快得多。
-
一致性:人类可能会感到疲倦或分心,导致任务执行的不一致性。自动化保证任务每次都能按照预定规则一致地完成。
-
准确性:自动化操作没有人类可能经历的疲劳或失误。它精确地遵循指令,最小化错误发生的可能性,从而避免可能带来高昂代价的后果。
-
规模:无论是管理一个系统还是一千个系统,自动化都能轻松扩展操作,而无需额外的人力资源。
-
节省成本:通过减少对人工的依赖,自动化在时间和人力资源上带来了显著的成本节省。
-
风险降低:某些任务,例如数据备份和安全检查,虽然至关重要,但可能被人类忽视或跳过。自动化确保这些任务得到持续执行,从而减少风险。
-
更快的响应:自动化能够实时检测并响应问题。例如,它可以自动重启崩溃的服务器,或在高流量期间调整资源分配,确保用户体验不间断。
-
资源分配:自动化日常任务解放了人力资源,使其可以集中精力处理更具战略性和创造性的工作,这些工作需要批判性思维和决策能力。
-
合规性:自动化执行并监控政策和法规的合规性,减少法律和监管方面的潜在问题。
-
数据分析:自动化迅速处理和分析大量数据,促进数据驱动的决策和洞察。
-
24/7 运营:自动化不知疲倦地全天候工作,确保持续运营和可用性。
-
适应性:自动化可以重新编程以适应不断变化的需求和环境,使其具有灵活性和面向未来的能力。
在技术领域,自动化是现代 IT 运营的基石,涵盖了从自动化软件部署到管理云资源和配置网络设备。它使组织能够简化流程、提高可靠性,并在快速变化的数字化环境中保持竞争力。
本质上,自动化类似于一个极其高效、无错误、全天候运作的劳动力,使个人和组织能够以更少的努力完成更多的工作。
为了从自动化中获益,项目管理职能正在迅速被稀释,软件开发团队正在转型为敏捷团队,以迭代的方式交付 Sprint。因此,如果有新的需求,我们不会等到整个需求签署完毕后才开始设计、开发、QA 等工作。而是将软件拆解为可操作的功能模块,并以较小的部分交付,以便迅速获得价值和客户反馈。这意味着快速的软件开发,减少失败的风险。
好吧,团队已经变得更加敏捷,开发软件的速度更快了。但在软件开发生命周期(SDLC)的过程中,许多任务仍然是手动进行的,比如一些团队只有在完成整个开发周期后才生成代码构建,并在之后发现大量的错误。追踪最初是什么原因导致问题变得非常困难。
如果你在将代码提交到源代码控制系统时,就能知道构建失败的原因怎么办?如果你能在构建执行时立即理解软件未通过某些测试怎么办?嗯,这就是 CI 的精髓。
持续集成(CI)是一个过程,开发人员频繁地将代码提交到源代码仓库,可能一天多次。后台的自动化工具可以检测这些提交,然后构建、运行一些测试,并提前告知你提交是否引发了问题。这意味着开发人员、测试人员、产品负责人、运维团队以及所有相关人员都会知道是什么引发了问题,开发人员可以迅速修复。这在软件开发中形成了一个反馈循环。过去我们在软件开发中有一个手动的反馈循环,但它非常缓慢。所以,要么你得等很久才能开始下一任务,要么你做错了事情直到发现时已经为时太晚,无法撤销之前所做的所有工作。这样就增加了之前所有工作的返工量。
众所周知,在 SDLC(软件开发生命周期)中,越早修复漏洞成本越低。因此,持续集成(CI)的目标是尽早在 SDLC 中提供代码质量的持续反馈。这可以为开发人员和组织节省大量时间和金钱,避免在大部分代码已经经过测试时还需要修复发现的漏洞。因此,CI 帮助软件开发团队更快地开发出更好的软件。
既然我们提到了敏捷开发,接下来简要讨论一下它与 DevOps 的比较。敏捷是一种工作方式,对于实现敏捷所需的工具、技术和自动化并未明确说明。DevOps 是敏捷思维的延伸,帮助你有效地实施敏捷。DevOps 高度关注自动化,力求在可能的情况下避免手动操作。它还鼓励软件交付的自动化,旨在加强或替代传统工具和框架。随着现代 DevOps 的出现,特定的工具、技术和最佳实践简化了开发人员、质量保证人员和运维人员的工作。现代公共云平台和 DevOps 为团队提供了即用型的动态基础设施,帮助企业减少上市时间,并构建可扩展、弹性强、性能优越的基础设施,确保企业的系统在最小的停机时间内持续运行。
在第一章介绍现代 DevOps 时,我们提到它通常应用于现代云原生应用程序。为了演示这一点,我构建了一个基于微服务的博客应用示例。我们将在本书的这一章和未来的章节中使用该应用,以确保使用现代 DevOps 工具和实践无缝开发和交付该应用。接下来我们将查看这个示例应用。
微服务架构博客应用介绍 – 博客应用
博客应用是一个基于现代微服务架构的博客 Web 应用,允许用户创建、管理和互动博客帖子。它既适用于作者,也适用于读者。用户可以使用他们的电子邮件地址注册该平台,并开始写博客帖子。读者可以公开查看由多个作者创建的所有博客帖子,登录用户还可以提供评论和评分。
该应用是用一个流行的基于 Python 的 Web 框架Flask编写的,并使用MongoDB作为数据库。该应用被拆分成多个微服务,用于用户、帖子、评论和评分管理。还有一个独立的前端微服务,供用户进行交互。让我们来看看每个微服务:
-
用户管理:用户管理微服务提供创建用户账户、更新个人资料(姓名和密码)和删除用户账户的接口。
-
帖子管理:帖子管理微服务提供创建、列出、获取、更新和删除帖子的接口。
-
评论管理:评论管理微服务允许用户在帖子上添加评论,并对其进行更新和删除。它在内部与评分管理微服务交互,以管理与评论一起提供的评分。
-
评分管理:评分管理微服务管理与特定评论相关联的帖子评分。该微服务在内部由评论管理微服务调用,并不会暴露给前端微服务。
-
前端:前端微服务是一个基于Bootstrap构建的 Python Flask 用户界面应用,为用户提供丰富的交互式界面。它允许用户注册、登录、查看帖子并在帖子之间导航、编辑帖子、添加和更新评论以及管理个人资料。该微服务通过 HTTP 请求与后端微服务无缝交互。
用户、帖子、评论和评分微服务与MongoDB数据库进行交互。
下图展示了各服务之间的交互关系:
图 11.1 – 博客应用服务与交互
如我们所见,单个微服务之间相互解耦,因此可以独立扩展。它也很健壮,因为如果某个特定微服务出现故障,应用的其他部分仍然可以继续工作。各个微服务可以作为独立的组件进行开发和部署,从而增强了应用的灵活性和可维护性。这个应用是利用微服务构建现代功能丰富的 Web 应用的一个优秀示例。
现在,让我们为这个应用实现持续集成(CI)。为了实现 CI,我们需要一个 CI 工具。在接下来的部分中,我们将介绍一些流行的工具以及你可以选择的选项。
使用 GitHub Actions 构建 CI 管道
GitHub Actions 是一款基于 SaaS 的工具,随GitHub提供。因此,当你创建 GitHub 仓库时,开箱即用即可访问此服务。因此,GitHub Actions 是适合 CI/CD 新手的最佳工具之一,特别适合那些想要快速入门的人。GitHub Actions 可以帮助你自动化任务、构建、测试和部署代码,甚至简化工作流程,极大地简化开发者的工作。
以下是 GitHub Actions 能为你做的事情:
-
CI:GitHub Actions 可以在你推送更改到仓库时自动构建和测试代码。这确保了你的代码始终无误,并准备好进行部署。
-
CD:你可以使用 GitHub Actions 将应用程序部署到各种托管平台,如 AWS、Azure 和 GCP。这使你能够快速高效地向用户交付更新。
-
工作流自动化:你可以使用 GitHub Actions 创建自定义工作流,自动化开发过程中的重复任务。例如,你可以自动标记和分配问题,在特定事件触发时启动构建,或向团队发送通知。
-
自定义脚本:GitHub Actions 允许你运行自定义脚本和命令,完全控制自动化任务。无论是需要编译代码、运行测试还是执行部署脚本,GitHub Actions 都能处理。
-
npm用于部署到流行的云提供商。你可以轻松地将这些操作集成到你的工作流中。 -
定时任务:你可以安排在特定时间或间隔运行某些操作。这对于生成报告、发送提醒或在非高峰时段进行维护等任务非常有用。
-
多平台支持:GitHub Actions 支持多种编程语言、操作系统和云环境,这意味着你可以轻松构建和部署面向不同平台的应用程序。
-
集成:GitHub Actions 与 GitHub 仓库无缝集成,使其成为开发环境的自然扩展。你可以直接在仓库中使用 YAML 文件定义工作流。
GitHub Actions 通过自动化日常任务、确保代码质量并简化软件开发生命周期(SDLC),彻底改变了开发人员的工作方式。它是提升生产力和保持高质量代码的团队和个人开发者的宝贵工具。
现在,让我们为我们的示例博客应用创建一个 CI 管道。博客应用由多个微服务组成,每个微服务都运行在单独的Docker容器中。我们还为每个微服务编写了单元测试,可以运行这些测试来验证代码更改。如果测试通过,构建就会通过;否则,它将失败。
要访问本节的资源,请cd进入以下目录:
$ cd ~/modern-devops/blog-app
该目录包含多个微服务,其结构如下:
.
├── frontend
│ ├── Dockerfile
│ ├── app.py
│ ├── app.test.py
│ ├── requirements.txt
│ ├── static
│ └── templates
├── posts
│ ├── Dockerfile
│ ├── app.py
│ ├── app.test.py
│ └── requirements.txt
├── ratings ...
├── reviews ...
└── users ...
frontend目录包含app.py(Flask 应用程序代码)、app.test.py(Flask 应用程序的单元测试)、requirements.txt(包含应用所需的所有 Python 模块)和Dockerfile。它还包括一些其他目录,供此应用的用户界面元素使用。
app.py、app.test.py、requirements.txt和Dockerfile文件。
所以,让我们从切换到posts目录开始:
$ cd posts
由于我们知道 Docker 本身符合 CI 标准,我们可以使用Dockerfile本身来运行测试。让我们来研究一下 posts 服务的 Dockerfile:
FROM python:3.7-alpine
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
EXPOSE 5000
COPY . .
RUN python app.test.py
CMD ["flask", "run"]
这个Dockerfile以python:3.7-alpine基础镜像开始,安装依赖,并将代码复制到工作目录。它运行app.test.py单元测试,检查如果我们部署代码是否能正常工作。最后,CMD命令定义了一个flask run命令,当我们启动容器时执行。
让我们构建我们的Dockerfile,看看会得到什么:
$ docker build --progress=plain -t posts .
#4 [1/6] FROM docker.io/library/python:3.7-alpine
#5 [internal] load build context
#6 [2/6] RUN apk add --no-cache gcc musl-dev linux-headers
#7 [3/6] COPY requirements.txt requirements.txt
#8 [4/6] RUN pip install -r requirements.txt
#9 [5/6] COPY . .
#10 [6/6] RUN python app.test.py
#10 0.676 -------------------------------------------------
#10 0.676 Ran 8 tests in 0.026s
#11 exporting to image
#11 naming to docker.io/library/posts done
如我们所见,它构建了容器,执行了测试,并返回了Ran 8 tests in 0.026s和OK消息。因此,我们可以使用Dockerfile来构建和测试这个应用程序。我们在docker build命令中使用了--progress=plain参数。这是因为我们希望看到逐步的日志输出,而不是 Docker 将进度合并为一条消息(这现在是默认行为)。
现在,让我们来看看 GitHub Actions,以及我们如何自动化这一步骤。
创建一个 GitHub 仓库
在我们能够使用 GitHub Actions 之前,我们需要创建一个 GitHub 仓库。因为我们知道每个微服务可以独立开发,所以我们将它们放在单独的 Git 仓库中。对于本次练习,我们将只关注posts微服务,其余部分留给你作为练习。
为此,请访问github.com/new并创建一个新的仓库。为其起一个合适的名字。对于本次练习,我将使用mdo-posts。
创建完成后,使用以下命令克隆仓库:
$ git clone https://github.com/<GitHub_Username>/mdo-posts.git
然后,使用以下命令将目录切换到仓库目录,并将app.py、app.test.py、requirements.txt和Dockerfile文件复制到仓库目录中:
$ cd mdo-posts
$ cp ~/modern-devops/blog-app/posts/* .
现在,我们需要创建一个 GitHub Actions 工作流文件。我们将在下一部分进行操作。
创建 GitHub Actions 工作流
GitHub Actions 工作流是一个简单的 YAML 文件,包含了构建步骤。我们必须在仓库的.github/workflows目录下创建此工作流。我们可以使用以下命令执行此操作:
$ mkdir -p .github/workflows
我们将使用以下 GitHub Actions 工作流文件build.yaml进行本次练习:
name: Build and Test App
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Login to Docker Hub
id: login
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
- name: Build the Docker image
id: build
run: docker build . --file Dockerfile --tag ${{ secrets.DOCKER_USER }}/
mdo-posts:$(git rev-parse --short "$GITHUB_SHA")
- name: Push the Docker image
id: push
run: docker push ${{ secrets.DOCKER_USER }}/mdo-posts:$(git rev-parse --short
"$GITHUB_SHA")
该文件包含以下内容:
-
name:工作流的名称——在这种情况下是Build and Test App。 -
on:这描述了此工作流何时运行。在这种情况下,如果在main分支上发送了push或pull请求,它将会运行。 -
jobs:GitHub Actions 工作流包含一个或多个作业,默认情况下,它们会并行运行。此属性包括所有作业。 -
jobs.build:这是一个执行容器构建的作业。 -
jobs.build.runs-on:这描述了构建作业将在哪个环境中运行。我们在这里指定了ubuntu-latest。这意味着该作业将在 Ubuntu 虚拟机上运行。 -
jobs.build.steps:这包含在作业中按顺序运行的步骤。构建作业由四个构建步骤组成:checkout,它将从您的仓库检出代码;login,它将登录 Docker Hub;build,它将在您的代码上运行 Docker 构建;以及push,它将您的 Docker 镜像推送到 Docker Hub。请注意,我们使用 Git 提交的 SHA 来标记镜像。这将构建与提交关联,使 Git 成为唯一的真实来源。 -
jobs.build.steps.uses:这是第一步,描述了您将在作业中运行的一个 action。Actions 是可以在管道中执行的可重用代码块。在此情况下,它运行checkoutaction。它将从当前触发 action 的分支中检出代码。
提示
始终使用带版本的 actions。这将防止您的构建因后续版本与管道不兼容而中断。
-
jobs.build.steps.name:这是您构建步骤的名称。 -
jobs.build.steps.id:这是您构建步骤的唯一标识符。 -
jobs.build.steps.run:这是它在构建步骤中执行的命令。
工作流中还包含 ${{ }} 内的变量。我们可以在工作流中定义多个变量,并在后续步骤中使用它们。在此案例中,我们使用了两个变量 – ${{ secrets.DOCKER_USER }} 和 ${{ secrets.DOCKER_PASSWORD }}。这些变量来自 GitHub secrets。
提示
最佳实践是使用 GitHub secrets 来存储敏感信息。切勿将这些详细信息直接存储在包含代码的仓库中。
您必须使用以下 URL 在仓库中定义两个 secrets:https://github.com/<your_user>/mdo-posts/settings/secrets/actions。
在仓库中定义两个 secrets:
DOCKER_USER=<Your Docker Hub username>
DOCKER_PASSWORD=<Your Docker Hub password>
现在,让我们通过以下命令将 build.yml 文件移到 workflows 目录:
$ mv build.yml .github/workflows/
现在,我们准备将代码推送到 GitHub。运行以下命令将更改提交并推送到您的 GitHub 仓库:
$ git add --all
$ git commit -m 'Initial commit'
$ git push
现在,前往 https://github.com/<your_user>/mdo-posts/actions。您应该会看到类似以下内容:
图 11.2 – GitHub Actions
如我们所见,GitHub 使用我们的工作流文件运行了一个构建,并且已经构建了代码并将镜像推送到Docker Hub。访问您的 Docker Hub 账户时,您应该能在账户中看到您的镜像:
图 11.3 – Docker Hub 镜像
现在,让我们尝试以某种方式破坏我们的代码。假设您的团队中的某个人更改了 app.py 代码,并且在 create_post 响应中不再返回 post,而是返回 pos。我们来看看在这种情况下会发生什么。
对 app.py 文件中的 create_post 函数进行以下更改:
@app.route('/posts', methods=['POST'])
def create_post():
...
return jsonify({'pos': str(inserted_post.inserted_id)}), 201
现在,使用以下命令将代码提交并推送到 GitHub:
$ git add --all
$ git commit -m 'Updated create_post'
$ git push
现在,前往 GitHub Actions,查找最新的构建。你将看到该构建会出错,并给出以下输出:
图 11.4 – GitHub Actions – 构建失败
正如我们所看到的,app.test.py 执行失败。这是由于测试用例失败,错误信息为 AssertionError: 'post' not found in {'pos': '60458fb603c395f9a81c9f4a'}。由于预期的 post 键未在输出 {'pos': '60458fb603c395f9a81c9f4a'} 中找到,测试用例失败,正如下面的截图所示:
图 11.5 – GitHub Actions – 测试失败
我们发现错误是在有人将有问题的代码推送到 Git 仓库时发生的。你现在能看到 CI 的好处了吗?
现在,让我们修复代码并再次提交代码。
修改 app.py 中的 create_post 函数,使其如下所示:
@app.route('/posts', methods=['POST'])
def create_post():
...
return jsonify({'post': str(inserted_post.inserted_id)}), 201
然后,使用以下命令将代码 commit 并 push 到 GitHub:
$ git add --all
$ git commit -m 'Updated create_post'
$ git push
这一次,构建将成功:
图 11.6 – GitHub Actions – 构建成功
你看到这有多简单吗?我们很快就开始了 CI,并在幕后实现了 GitOps,因为构建和测试代码所需的配置文件也与应用程序代码一起存放。
作为练习,针对 reviews、users、ratings 和 frontend 微服务重复相同的过程。你可以通过操作它们来理解其工作原理。
并不是每个人都使用 GitHub,因此对于他们来说,SaaS 提供的服务可能不是一个选择。因此,在下一节中,我们将看看最流行的开源 CI 工具:Jenkins。
在 Kubernetes 上可扩展的 Jenkins 与 Kaniko
想象一下,你正在运行一个车间,在这里你构建各种各样的机器。在这个车间里,你有一个神奇的传送带,叫做 Jenkins,用来组装这些机器。但是,为了让你的车间更加高效和适应性更强,你还有一支叫做 Kaniko 的小型机器人团队,帮助构建每台机器的各个部件。让我们把这个车间类比与技术世界进行对比:
-
可扩展的 Jenkins:Jenkins 是一个广泛使用的自动化服务器,有助于自动化各种任务,特别是与构建、测试和部署软件相关的任务。“可扩展的 Jenkins”意味着以一种配置 Jenkins 的方式,使其能够高效处理日益增长的工作负载,就像一个宽敞的车间,能够生产大量的机器。
-
Kubernetes:将 Kubernetes 想象成车间经理。它是一个编排平台,自动化部署、扩展和管理容器化应用程序的过程。Kubernetes 确保 Jenkins 和一队小型机器人(Kaniko)无缝协作,并能适应变化的需求。
-
Kaniko:Kaniko 相当于你的微型机器人团队。在容器化的背景下,Kaniko 是一个帮助构建容器镜像的工具,就像机器的各个部件一样。Kaniko 的特别之处在于,它无需对 Docker 守护进程有高级访问权限就能完成这一任务。与传统的容器构建工具不同,Kaniko 不需要特权,使得它成为构建容器时,尤其是在 Kubernetes 环境中,更安全的选择。
现在,让我们将这三种工具结合起来,看看我们能取得什么成果:
-
大规模构建容器:你的车间可以同时制造多个机器,这要归功于 Jenkins 和小型机器人。同样地,借助基于 Kubernetes 的 Jenkins 与 Kaniko,你可以高效并行地构建容器镜像。这种可扩展性在现代应用程序开发中至关重要,因为容器化在其中扮演了重要角色。
-
隔离性和安全性:就像 Kaniko 的小型机器人在受控环境中运行一样,Kaniko 确保容器镜像的构建在 Kubernetes 集群中以隔离且安全的方式进行。这意味着不同的团队或项目可以使用 Jenkins 和 Kaniko 而不会相互干扰各自的容器构建过程。
-
一致性和自动化:就像传送带(Jenkins)保证了机器组装的一致性一样,Kubernetes 上的 Jenkins 与 Kaniko 结合确保了容器镜像构建的一致性。自动化是这一配置的核心,简化了为应用程序构建和管理容器镜像的过程。
总结来说,基于 Kubernetes 的可扩展 Jenkins 与 Kaniko 的结合,指的是在 Kubernetes 环境中设置 Jenkins,通过 Kaniko 高效构建和管理容器镜像的实践。它能够实现容器镜像的持续、一致且安全的构建,完美契合现代软件开发的工作流。
因此,将 Jenkins、Kubernetes 和 Kaniko 类比为一个工作车间,生动地展示了这一配置如何简化容器镜像的构建,使其在当代软件开发实践中具备可扩展性、高效性和安全性。现在,让我们更深入地了解 Jenkins。
Jenkins是市场上最受欢迎的 CI 工具。它是开源的,安装简单,运行顺畅。Jenkins 是一个基于 Java 的工具,采用插件化架构,设计用于支持多种集成,例如与源代码管理工具如Git、SVN和Mercurial的集成,或与流行的构件库如Nexus和Artifactory的集成。它还与知名的构建工具如Ant、Maven和Gradle兼容,此外,还支持标准的 Shell 脚本和 Windows 批处理文件执行。
Jenkins 遵循控制器-代理模型。尽管从技术上讲,你可以在控制器机器本身上运行所有构建,但将 CI 构建任务分配给你网络中的其他服务器,形成分布式架构,显然更有意义。这样做可以避免控制器机器的过载。你可以用它来存储构建配置和其他管理数据,并管理整个 CI 构建集群,类似于以下图示的方式:
图 11.7 – 可扩展的 Jenkins
在上面的图示中,多个静态的 Jenkins 代理连接到 Jenkins 控制器。现在,这种架构运行良好,但它的可扩展性不足。现代 DevOps 强调资源的利用率,所以我们只希望在需要构建时才部署代理机器。因此,自动化构建流程,在需要时自动部署代理机器,是更好的做法。当部署新虚拟机时,这可能显得有些过头,因为即便是使用 Packer 制作的预构建镜像,配置新虚拟机也需要几分钟时间。更好的替代方案是使用容器。
Jenkins 与 Kubernetes 集成得相当好,允许你在 Kubernetes 集群上运行构建。这样,每当你在 Jenkins 上触发构建时,Jenkins 会指示 Kubernetes 创建一个新的代理容器,容器将连接到控制器机器并在其中运行构建。这就是最好的按需构建。以下图示详细展示了这一过程:
图 11.8 – 可扩展的 Jenkins CI 工作流
这听起来很棒,我们可以继续运行这个构建,但这种方法也存在问题。我们必须理解,Jenkins 控制器和代理作为容器运行,而不是完整的虚拟机。因此,如果我们想在容器内运行 Docker 构建,我们必须以特权模式运行该容器。这不是安全的最佳实践,而且你的管理员应该已经关闭了这个功能。这是因为以特权模式运行容器会将主机文件系统暴露给容器。一个能够访问你容器的黑客将拥有完全的访问权限,从而可以在系统中做任何事情。
为了解决这个问题,你可以使用如Kaniko这样的容器构建工具。Kaniko 是由 Google 提供的构建工具,帮助你在没有 Docker 守护进程的情况下构建容器,甚至不需要在容器中安装 Docker。这是运行构建的一个很好的方法,特别是在Kubernetes 集群中,能够创建一个可扩展的 CI 环境。它简单易用,不需要黑客手段,并且提供了一种安全的构建容器方式,正如我们将在后续章节中看到的那样。
本节将使用Google Kubernetes Engine(GKE)。如前所述,Google Cloud 提供价值 $300 的 90 天免费试用。如果您尚未注册,可以在cloud.google.com/free注册。
启动 Google Kubernetes Engine
一旦您注册并进入控制台,打开Google Cloud Shell CLI 来运行以下命令。
您需要首先使用以下命令启用 Kubernetes Engine API:
$ gcloud services enable container.googleapis.com
要创建一个从一个节点扩展到五个节点的两节点自动扩展 GKE 集群,请运行以下命令:
$ gcloud container clusters create cluster-1 --num-nodes 2 \
--enable-autoscaling --min-nodes 1 --max-nodes 5 --zone us-central1-a
就这样!集群将启动并运行。
您还必须克隆以下 GitHub 仓库,以获取提供的一些练习:github.com/PacktPublishing/Modern-DevOps-Practices-2e。
运行以下命令将仓库克隆到您的主目录,然后cd进入以下目录以访问所需的资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch11/jenkins/jenkins-controller
我们将使用Jenkins 配置即代码(JCasC)功能来配置 Jenkins,因为它是一种声明性方式来管理您的配置,同时也支持 GitOps。您需要创建一个简单的 YAML 文件,包含所有必需的配置,然后将该文件复制到 Jenkins 控制器,在设置一个指向文件的环境变量后,Jenkins 将在启动时自动配置 YAML 文件中定义的所有方面。
让我们首先创建casc.yaml文件来定义我们的配置。
创建 Jenkins CaC(JCasC)文件
用于此目的的casc.yaml文件,我将解释其中的部分内容。让我们首先定义Jenkins 全局安全性。
配置 Jenkins 全局安全性
默认情况下,Jenkins 是不安全的——也就是说,如果您从官方 Docker 镜像启动一个基础版 Jenkins 并暴露它,任何人都可以对该 Jenkins 实例进行任何操作。为了确保我们保护它,我们需要以下配置:
jenkins:
remotingSecurity:
enabled: true
securityRealm:
local:
allowsSignup: false
users:
- id: ${JENKINS_ADMIN_ID}
password: ${JENKINS_ADMIN_PASSWORD}
authorizationStrategy:
globalMatrix:
permissions:
- "Overall/Administer:admin"
- "Overall/Read:authenticated"
在前面的配置中,我们定义了以下内容:
-
remotingSecurity:我们启用了此功能,它将确保 Jenkins 控制器与我们将动态创建的 Kubernetes 代理之间的通信安全。 -
securityRealm:我们已将安全领域设置为local,这意味着 Jenkins 控制器本身将负责所有认证和用户管理。我们也可以将其卸载到外部实体,如 LDAP:-
allowsSignup:此设置为false。这意味着您在 Jenkins 首页上看不到注册链接,Jenkins 管理员应手动创建用户。 -
users:我们将创建一个用户,其id和password分别来自两个环境变量,分别是JENKINS_ADMIN_ID和JENKINS_ADMIN_PASSWORD。
-
-
authorizationStrategy:我们定义了基于矩阵的授权策略,在该策略中,我们为admin提供管理员权限,为authenticated的非管理员用户提供读取权限。
同样,由于我们希望 Jenkins 在代理中执行所有构建而不是在控制器机器上执行,因此我们需要指定以下设置:
jenkins:
systemMessage: "Welcome to Jenkins!"
numExecutors: 0
我们将numExecutors设置为0,以允许在控制器上不执行任何构建,并在 Jenkins 欢迎界面上设置了systemMessage。
现在,我们已经设置了 Jenkins 控制器的安全方面,我们将配置 Jenkins 与 Kubernetes 集群连接。
将 Jenkins 与集群连接
我们将安装 Kubernetes 插件以将 Jenkins 控制器与集群连接起来。我们这样做是因为我们希望 Jenkins 动态为构建启动代理,作为 Kubernetes pod。
我们将首先在jenkins.clouds下创建一个kubernetes配置,如下所示:
jenkins
clouds:
- kubernetes:
serverUrl: "https://<kubernetes_control_plane_ip>"
jenkinsUrl: "http://jenkins-service:8080"
jenkinsTunnel: "jenkins-service:50000"
skipTlsVerify: false
useJenkinsProxy: false
maxRequestsPerHost: 32
name: "kubernetes"
readTimeout: 15
podLabels:
- key: jenkins
value: agent
...
由于配置中有一个名为<kubernetes_control_plane_ip>的占位符,我们必须将其替换为 Kubernetes 控制平面的 IP 地址。运行以下命令获取控制平面的 IP 地址:
$ kubectl cluster-info | grep "control plane"
Kubernetes control plane is running at https://35.224.6.58
现在,请使用以下命令将<kubernetes_control_plane_ip>占位符替换为您从前面命令中获取的实际 IP 地址:
$ sed -i 's/<kubernetes_control_plane_ip>/actual_ip/g' casc.yaml
让我们查看配置文件中的每个属性:
-
serverUrl: 这表示 Kubernetes 控制平面的服务器 URL,允许 Jenkins 控制器与 Kubernetes API 服务器通信。 -
jenkinsUrl: 这表示 Jenkins 控制器的 URL。我们将其设置为 http://jenkins-service:8080。 -
jenkinsTunnel: 这描述了代理 Pod 如何与 Jenkins 控制器连接。由于 JNLP 端口是50000,我们将其设置为jenkins-service:50000。 -
podLabels: 我们还设置了一些 Pod 标签,key=jenkins和value=agent。这些将设置在代理 Pod 上。
其他属性也设置为它们的默认值。
每个 Kubernetes 云配置都包括多个 Pod templates,描述了代理 Pod 的配置方式。配置如下:
- kubernetes:
...
templates:
- name: "jenkins-agent"
label: "jenkins-agent"
hostNetwork: false
nodeUsageMode: "NORMAL"
serviceAccount: "jenkins"
imagePullSecrets:
- name: regcred
yamlMergeStrategy: "override"
containers:
...
在这里,我们定义了以下内容:
-
模板的
name和label。我们将它们都设置为jenkins-agent。 -
hostNetwork: 这被设置为false,因为我们不希望容器与主机网络交互。 -
seviceAccount: 我们将其设置为jenkins,因为我们希望使用此服务账号与 Kubernetes 交互。 -
imagePullSecrets: 我们还提供了一个名为regcred的镜像拉取秘钥,用于与容器注册表进行身份验证以拉取jnlp镜像。
每个 Pod 模板还包含一个容器模板。我们可以使用以下配置来定义它:
...
containers:
- name: jnlp
image: "<your_dockerhub_user>/jenkins-jnlp-kaniko"
workingDir: "/home/jenkins/agent"
command: ""
args: ""
livenessProbe:
failureThreshold: 1
initialDelaySeconds: 2
periodSeconds: 3
successThreshold: 4
timeoutSeconds: 5
volumes:
- secretVolume:
mountPath: /kaniko/.docker
secretName: regcred
在这里,我们已经指定了以下内容:
-
name: 设置为jnlp。 -
image: 在这里,我们指定了我们将在下一节构建的Docker 代理镜像。请确保您使用以下命令将<your_dockerhub_user>占位符替换为您的 Docker Hub 用户名:
$ sed -i 's/<your_dockerhub_user>/actual_dockerhub_user/g' casc.yaml
-
workingDir: 设置为/home/jenkins/agent。 -
我们将
command和args字段都设置为空,因为我们不需要传递它们。 -
livenessProbe: 我们为代理 Pod 定义了一个活动探针。 -
volumes:我们已经将regcred密钥挂载到kaniko/.docker文件作为卷。由于regcred包含 Docker 仓库凭证,Kaniko 将使用此凭证连接到容器注册表。
现在我们的配置文件已经准备好,我们将在下一节安装 Jenkins。
安装 Jenkins
由于我们是在 Kubernetes 集群上运行,我们只需要 Docker Hub 上最新的官方 Jenkins 镜像。我们将根据需求定制镜像。
以下 Dockerfile 文件将帮助我们创建包含所需插件和初始配置的镜像:
FROM jenkins/jenkins
ENV CASC_JENKINS_CONFIG /usr/local/casc.yaml
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
COPY casc.yaml /usr/local/casc.yaml
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
Dockerfile 从 Jenkins 基础镜像开始。接着,我们声明了两个环境变量——CASC_JENKINS_CONFIG,它指向我们在上一节中定义的 casc.yaml 文件,以及 JAVA_OPTS,它告诉 Jenkins 不运行设置向导。然后,我们将 casc.yaml 和 plugins.txt 文件复制到 Jenkins 容器内的相应目录。最后,我们在 plugins.txt 文件上运行 jenkins-plugins-cli,以安装所需的插件。
plugins.txt 文件包含了我们在此设置中所需的所有 Jenkins 插件的列表。
小贴士
你可以根据需要通过更新 plugins.txt 文件来定制并安装更多插件,以满足控制器镜像的需求。
让我们使用以下命令从 Dockerfile 文件构建镜像:
$ docker build -t <your_dockerhub_user>/jenkins-controller-kaniko .
现在我们已经构建了镜像,使用以下命令登录并将镜像推送到 Docker Hub:
$ docker login
$ docker push <your_dockerhub_user>/jenkins-controller-kaniko
我们还需要构建 Jenkins 代理镜像以运行我们的构建。请记住,Jenkins 代理需要所有支持工具才能运行构建。你可以在以下目录找到代理所需的资源:
$ cd ~/modern-devops/ch11/jenkins/jenkins-agent
我们将使用以下 Dockerfile 来实现:
FROM gcr.io/kaniko-project/executor:v1.13.0 as kaniko
FROM jenkins/inbound-agent
COPY --from=kaniko /kaniko /kaniko
WORKDIR /kaniko
USER root
这个 Dockerfile 使用多阶段构建,从 kaniko 基础镜像开始,并将 kaniko 二进制文件从 kaniko 基础镜像复制到 inbound-agent 基础镜像中。让我们使用以下命令构建并推送容器:
$ docker build -t <your_dockerhub_user>/jenkins-jnlp-kaniko .
$ docker push <your_dockerhub_user>/jenkins-jnlp-kaniko
为了在 Kubernetes 集群上部署 Jenkins,我们将首先创建一个 jenkins 服务账号。通过集群角色绑定,一个 Kubernetes cluster-admin。一个 Kubernetes jenkins-sa-crb.yaml 清单描述了这一点。要访问这些资源,请运行以下命令:
$ cd ~/modern-devops/ch11/jenkins/jenkins-controller
要应用清单,运行以下命令:
$ kubectl apply -f jenkins-sa-crb.yaml
下一步是创建一个 PersistentVolumeClaim 资源来存储 Jenkins 数据,以确保 Jenkins 数据在 pod 生命周期之外仍然存在,并且即使删除 pod 后数据也会存在。
要应用清单,运行以下命令:
$ kubectl apply -f jenkins-pvc.yaml
然后,我们将创建一个 Kubernetes regcred 来帮助 Jenkins pod 与 Docker 仓库进行身份验证。使用以下命令来实现:
$ kubectl create secret docker-registry regcred --docker-username=<username> \
--docker-password=<password> --docker-server=https://index.docker.io/v1/
现在,我们将定义一个jenkins-deployment.yaml,它将运行 Jenkins 容器。Pod 使用jenkins服务账户,并通过名为jenkins-pv-claim的PersistentVolumeClaim资源定义一个jenkins-pv-storage,这是我们之前定义的。我们定义了一个 Jenkins 容器,使用我们创建的 Jenkins 控制器镜像。它暴露了 HTTP 端口8080供Web UI使用,暴露了端口50000供JNLP使用,代理将使用该端口与 Jenkins 控制器进行交互。我们还将把jenkins-pv-storage卷挂载到/var/jenkins_home,以便在 Pod 生命周期之外持久化 Jenkins 数据。我们在 Pod 镜像中指定regcred作为imagePullSecret属性。我们还使用initContainer将/var/jenkins_home的所有权分配给jenkins。
由于文件包含占位符,请使用以下命令将<your_dockerhub_user>替换为你的 Docker Hub 用户名,将<jenkins_admin_pass>替换为你选择的 Jenkins 管理员密码:
$ sed -i 's/<your_dockerhub_user>/actual_dockerhub_user/g' jenkins-deployment.yaml
使用以下命令应用清单:
$ kubectl apply -f jenkins-deployment.yaml
既然我们已经创建了部署,我们可以通过jenkins-svc.yaml清单暴露该部署。此服务在负载均衡器上暴露端口8080和50000。使用以下命令应用该清单:
$ kubectl apply -f jenkins-svc.yaml
让我们让服务找到外部 IP 并使用它来访问 Jenkins:
$ kubectl get svc jenkins-service
NAME EXTERNAL-IP PORT(S)
jenkins-service LOAD_BALANCER_EXTERNAL_IP 8080,50000
现在,要访问服务,请在浏览器窗口中输入http://<LOAD_BALANCER_EXTERNAL_IP>:8080:
图 11.9 – Jenkins 登录页面
如我们所见,我们被迎接到了一个登录页面。这意味着全球安全功能正常。让我们使用我们设置的管理员用户名和密码登录:
图 11.10 – Jenkins 首页
如我们所见,我们已成功登录到 Jenkins。现在,让我们继续创建我们的第一个 Jenkins 作业。
运行我们的第一个 Jenkins 作业
在创建我们的第一个作业之前,我们需要准备好仓库以运行该作业。我们将重用mdo-posts仓库。我们将把一个build.sh文件复制到仓库,该文件将为posts微服务构建容器镜像,并将其推送到 Docker Hub。
build.sh脚本接受IMAGE_ID和IMAGE_TAG作为参数。它将这些参数传递给Dockerfile,并使用以下代码将其推送到 Docker Hub:
IMAGE_ID=$1 && \
IMAGE_TAG=$2 && \
export DOCKER_CONFIG=/kaniko/.dockerconfig && \
/kaniko/executor \
--context $(pwd) \
--dockerfile $(pwd)/Dockerfile \
--destination $IMAGE_ID:$IMAGE_TAG \
--force
我们需要使用以下命令将此文件复制到我们的本地仓库:
$ cp ~/modern-devops/ch11/jenkins/jenkins-agent/build.sh ~/mdo-posts/
完成此操作后,进入本地仓库,即~/mdo-posts,然后提交并推送更改到 GitHub。完成此操作后,你就可以准备在 Jenkins 中创建一个作业了。
要在 Jenkins 中创建一个新作业,进入 Jenkins 首页并选择New Item | Freestyle Job。提供一个作业名称(最好与 Git 仓库名称相同),然后点击Next。
点击Source Code Management,选择Git,并添加你的 Git 仓库 URL,如下所示。指定你希望构建的分支:
图 11.11 – Jenkins 源代码管理配置
进入构建触发器,选择轮询 SCM,并添加以下详细信息:
图 11.12 – Jenkins – 构建触发器配置
然后,点击build.sh脚本,使用<your_dockerhub_user>/<image>参数和镜像标签。根据你的要求更改详细信息。完成后,点击保存:
图 11.13 – Jenkins – 执行 Shell 配置
现在,我们准备好构建这个任务了。为此,你可以进入任务配置并点击立即构建,或者推送一个更改到 GitHub。你应该能看到类似以下的内容:
图 11.14 – Jenkins 任务页面
Jenkins 会成功创建一个 Kubernetes 中的代理 Pod,并在其中运行此任务,很快,任务就会开始构建。点击构建 | 控制台输出。如果一切正常,你会看到构建成功,并且 Jenkins 已经构建了posts服务并在推送 Docker 镜像到注册表之前执行了单元测试:
图 11.15 – Jenkins 控制台输出
这样,我们就能够使用可扩展的 Jenkins 服务器运行 Docker 构建。如我们所见,我们已经在 SCM 设置中设置了轮询,每分钟检查一次是否有更改,如果有则构建任务。然而,这种方法消耗资源,并且从长远来看并不理想。试想一下,如果你有数百个任务与多个 GitHub 仓库交互,而 Jenkins 控制器每分钟都在轮询它们。更好的方法是,GitHub 可以在 Jenkins 上触发一个提交后 webhook。在这种情况下,Jenkins 会在仓库发生更改时构建任务。我们将在下一节中查看这种场景。
使用触发器自动化构建
触发 CI 构建的最佳方法是使用提交后 webhook。当我们查看 GitHub Actions 工作流时,已经看到过类似的例子。现在,让我们尝试通过 Jenkins 的触发器来自动化构建。为此,我们需要在 Jenkins 和 GitHub 两端进行一些更改。我们首先处理 Jenkins,然后配置 GitHub。
进入任务配置 | 构建触发器,并进行以下更改:
图 11.16 – Jenkins GitHub 钩子触发器
通过点击保存保存配置。现在,进入你的 GitHub 仓库,点击设置 | Webhooks | 添加 Webhook,并添加以下详细信息。然后,点击添加 Webhook:
图 11.17 – GitHub webhook
现在,推送更改到代码库。Jenkins 上的任务将开始构建:
图 11.18 – Jenkins GitHub webhook 触发器
这就是自动化构建触发器的实际操作。Jenkins 是市场上最流行的开源 CI 工具之一。它的最大优点是几乎可以在任何地方运行。然而,它也有一定的管理开销。你可能已经注意到,使用 GitHub Actions 启动构建是多么简单,但 Jenkins 稍微复杂一些。
其他一些 SaaS 平台也提供 CI 和 CD 服务。例如,如果你在 AWS 上运行,你可以使用其内置的 CI 服务,AWS Code Commit 和 Code Build;Azure 提供了完整的 CI 和 CD 服务套件,在其 Azure DevOps 中;GCP 提供了 Cloud Build 来完成这项工作。
无论你选择使用哪种工具,CI 都遵循相同的原则。它更多的是一个过程和你组织内的文化变革。现在,让我们来看看有关 CI 的一些最佳实践。
构建性能最佳实践
CI 是一个持续的过程,因此在任何给定时间,你的环境中会有许多并行的构建在运行。在这种情况下,我们可以通过一些最佳实践来优化这些构建。
目标是更快速的构建
你完成构建的速度越快,得到反馈的速度就越快,下一次迭代也能更迅速地进行。构建缓慢会拖慢你的开发团队的速度。采取措施确保构建更快速。例如,在 Docker 的情况下,使用较小的基础镜像是合理的,因为每次构建时都会从镜像注册表中下载代码。大多数构建使用相同的基础镜像也能加速构建时间。使用测试有帮助,但要确保它们不是长时间运行的。我们希望避免 CI 构建持续几个小时。因此,将长时间运行的测试卸载到另一个作业中,或者使用管道将是一个不错的选择。如果可能的话,可以并行运行活动。
始终使用提交后的触发器
提交后的触发器对你的团队帮助巨大。团队成员不需要登录 CI 服务器手动触发构建。这完全解耦了你的开发团队与 CI 管理。
配置构建报告
你不希望开发团队登录到 CI 工具并检查构建的运行情况。他们只想知道构建的结果和构建日志。因此,你可以配置构建报告,通过电子邮件或更好的方式(如使用 Slack 频道)发送构建状态。
自定义构建服务器的大小
并不是所有构建在相似类型的构建机器上都能以相同的方式工作。你可能需要根据构建环境的需求来选择机器。如果你的构建倾向于消耗更多的 CPU 而不是内存,那么选择这种机器来运行你的构建,而不是标准机器,将更为合理。
确保你的构建只包含你需要的内容
构建在网络间传输。你下载基础镜像,构建应用镜像,然后将其推送到容器注册中心。臃肿的镜像不仅占用大量网络带宽和传输时间,还可能使你的构建面临安全问题。因此,最佳实践始终是仅在构建中包含所需的内容,避免臃肿。你可以使用 Docker 的多阶段构建来处理这种情况。
并行化构建
同时运行测试和构建过程,以减少整体执行时间。利用分布式系统或基于云的 CI/CD 平台进行可扩展并行化,帮助高效处理更大的工作负载。
利用缓存
缓存依赖项和构建工件,以防止冗余的下载和构建,节省宝贵的时间。实现缓存机制,如 Docker 层缓存,或使用包管理器的内置缓存来最小化数据传输和构建步骤。
使用增量构建
配置 CI/CD 流水线以执行增量构建,仅重建自上次构建以来发生变化的部分。保持强大的版本控制实践,以准确追踪和识别更改。
优化测试
优先运行较快的单元测试,然后再运行较慢的集成或端到端测试。使用 TestNG、JUnit 或 PyTest 等测试框架来有效分类和并行化测试。
使用工件管理
高效地存储和管理构建工件,最好将其存储在专用的工件仓库中,如 Artifactory 或 Nexus。实施工件版本控制和保留策略,以保持工件仓库的整洁。
管理应用程序依赖关系
保持干净且简洁的依赖关系,以减少构建和测试时间。定期更新依赖项,以受益于性能提升和安全更新。
利用基础设施即代码
利用基础设施即代码(IaC)来一致性地配置和提供构建及测试环境。优化 IaC 模板,以最小化资源利用,确保高效的资源分配。
使用容器化管理构建和测试环境
容器化应用程序,并利用容器编排工具如 Kubernetes 高效管理测试环境。利用容器缓存加速镜像构建,提升资源利用率。
利用基于云的 CI/CD
考虑采用基于云的 CI/CD 服务,如 AWS CodePipeline、Google Cloud Build、Azure DevOps 或 Travis CI,以提升可扩展性和性能。利用按需云资源扩展并行化能力,适应不同的工作负载。
监控和分析 CI/CD 流水线
实施性能监控和分析工具,以识别 CI/CD 流水线中的瓶颈和改进领域。定期分析构建和测试日志,收集优化性能的洞察。
流水线优化
持续审查并优化 CI/CD 管道配置,提高效率和相关性。删除不再有显著贡献的多余步骤或阶段。
实施自动化清理
实施自动化清理程序,删除过时的构建产物、容器和虚拟机,避免资源堆积。定期清理旧的构建产物和未使用的资源,以保持环境整洁。
文档和培训
为你的 CI/CD 流程编写最佳实践和性能指南,确保整个团队始终如一地遵循这些标准。为团队成员提供培训和指导,使他们能够有效地实施和维护这些优化策略。
通过实施这些策略,你可以显著提升 CI/CD 管道的速度、效率和可靠性,从而促进更顺畅的软件开发和交付过程。这些是高层次的最佳实践,虽然不完全,但足够让你开始优化 CI 环境。
总结
本章介绍了持续集成(CI),你理解了持续集成的必要性以及容器应用程序的基本 CI 工作流。接着,我们了解了 GitHub Actions,使用它可以构建一个有效的 CI 管道。然后,我们探讨了 Jenkins 开源工具,并在 Kubernetes 上部署了一个可扩展的 Jenkins,结合 Kaniko 设置了 Jenkins 控制器-代理模式。接着,我们了解了如何在基于 GitHub Actions 和基于 Jenkins 的工作流中使用钩子来自动化构建。最后,我们学习了构建性能的最佳实践以及需要避免的事项。
到现在为止,你应该已经熟悉 CI 及其细节,并了解可以用来实现 CI 的各种工具。
在下一章,我们将深入探讨容器世界中的持续部署/交付。
问题
回答以下问题,测试你对本章内容的理解:
-
以下哪些是 CI 工具?(选择三个)
A. Jenkins
B. GitHub Actions
C. Kubernetes
D. AWS Code Build
-
配置提交后触发器是一种最佳实践。 (正确/错误)
-
Jenkins 是基于 SaaS 的 CI 工具。 (正确/错误)
-
Kaniko 需要 Docker 来构建容器。 (正确/错误)
-
Jenkins 代理节点需要哪些原因?(选择三个)
A. 它们使构建更具可扩展性
B. 它们帮助将管理功能从 Jenkins 控制器上卸载
C. 它们允许并行构建
D. 它们让 Jenkins 控制器更少忙碌
-
以下哪些是构建可扩展 Jenkins 服务器所需的?(选择三个)
A. Kubernetes 集群
B. Jenkins 控制器节点
C. Jenkins 代理节点
D. 与容器注册表交互的凭证
答案
以下是本章问题的答案:
-
A, B, D
-
正确
-
错误
-
错误
-
A, C, D
-
A, B, D
第十二章:使用 Argo CD 进行持续部署/交付
在上一章中,我们探讨了现代 DevOps 的一个关键方面——持续集成(CI)。CI 是大多数组织在采用 DevOps 时首先实施的内容,但事情并不止于 CI,CI 只会将经过测试的构建交付到工件仓库。而我们还希望将工件部署到我们的环境中。在本章中,我们将实现 DevOps 工具链的下一部分——持续 部署/交付(CD)。
本章我们将涵盖以下主要内容:
-
CD 和自动化的重要性
-
CD 模型和工具
-
博客应用及其部署配置
-
使用环境仓库进行持续声明式 IaC
-
Argo CD 简介
-
安装和设置 Argo CD
-
管理敏感配置和密钥
-
部署示例博客应用
技术要求
在本章中,我们将启动一个基于云的 Kubernetes 集群,Google Kubernetes Engine(GKE),用于练习。写作时,Google Cloud Platform(GCP)提供免费的 300 美元试用,时效为 90 天,因此你可以在console.cloud.google.com/注册一个账号。
你还需要克隆以下 GitHub 仓库来进行一些练习:github.com/PacktPublishing/Modern-DevOps-Practices。
运行以下命令将仓库克隆到你的主目录,并进入ch12目录以访问所需资源:
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch12
所以,让我们开始吧!
CD 和自动化的重要性
CD 构成了你 DevOps 工具链中的 Ops 部分。因此,在你的开发人员不断构建和推送代码,而 CI 管道负责构建、测试并将构建发布到工件仓库时,Ops 团队将把构建部署到测试和暂存环境。QA 团队是把关人,确保代码符合一定质量标准,只有通过后,Ops 团队才会将代码部署到生产环境。
现在,对于只实施 CI 部分的组织,其余活动仍然是手动的。例如,操作员需要拉取工件并手动运行命令来进行部署。因此,部署的速度将取决于 Ops 团队的可用性。由于部署是手动的,流程容易出错,人类在重复性工作中容易犯错。
现代 DevOps 的一个基本原则是避免繁琐工作。繁琐工作就是开发人员和运维人员日复一日进行的重复性工作,而这些工作可以通过自动化消除。这将帮助你的团队专注于更重要的事情。
通过持续交付,标准工具可以基于某些门控条件将代码部署到更高环境。CD 流水线将在测试构建到达工件库时触发,或者在 GitOps 的情况下,当检测到环境库中有任何变更时。然后,流水线会根据预设的配置决定在哪里以及如何部署代码。它还会确定是否需要手动检查,例如提出变更请求并检查是否已批准。
虽然持续部署和持续交付经常被混淆为相同的概念,但它们之间是有细微区别的。持续交付允许你的团队基于人工触发将经过测试的代码交付到你的环境中。因此,尽管你只需点击按钮就能将代码部署到生产环境中,但仍然需要某人在合适的时机(如维护窗口)发起部署。持续部署则更进一步,当它与 CI 过程集成时,一旦新的经过测试的构建可供使用,它会自动启动部署过程。不需要人工干预,持续部署只会在测试失败时停止。
监控工具构成了 DevOps 工具链的下一部分。运维团队可以通过管理他们的生产环境来获得反馈,并向开发人员提供改进的建议。这些反馈最终会进入开发的待办事项列表,开发人员可以在未来的发布中将其作为新特性交付。这样就完成了一个周期,现在你的团队可以持续不断地推出技术产品。
CD 提供了几个优势,其中一些如下:
-
更快的市场响应时间:CD 和 CI 减少了将新特性、增强功能和修复程序交付给最终用户的时间。这种敏捷性可以使你的组织在市场需求面前具有竞争优势,快速做出反应。
-
降低风险:通过自动化部署过程并频繁推送小规模的代码变更,你可以最小化大型、易出错部署的风险。漏洞和问题更容易被早期发现,并且回滚操作也可以更简单。
-
提高代码质量:频繁的自动化测试和质量检查是 CD 和 CI 的重要组成部分。这能提高代码质量,因为开发人员会被鼓励编写更简洁、更易维护的代码。任何问题都能更早地被发现和解决。
-
增强协作:CD 和 CI 促进了开发和运维团队之间的协作。它打破了传统的壁垒,鼓励跨职能团队合作,从而提升了沟通和理解。
-
提高效率和生产力:自动化重复性的任务,例如测试、构建和部署,使开发人员能够腾出时间专注于更有价值的任务,例如创建新特性和改进。
-
客户反馈:CD 允许您更快速地从真实用户那里收集反馈。通过频繁部署小的更改,您可以收集用户反馈并相应地调整开发工作,确保您的产品更好地满足用户需求。
-
持续改进:CD 促进了持续改进的文化。通过分析部署和监控的数据,团队可以识别出需要改进的领域,并对其流程进行迭代。
-
更好的安全性:频繁的更新意味着可以及时解决安全漏洞,减少攻击者的机会窗口。安全检查可以自动化并集成到 CI/CD 流水线中。
-
减少人工干预:CD 减少了在部署过程中对人工干预的需求。这降低了人为错误的可能性,并简化了发布流程。
-
可扩展性:随着产品的增长,开发人员数量的增加以及代码库复杂性的提高,CD 可以帮助保持可管理的开发流程。通过自动化许多发布和测试过程,它能够有效地扩展。
-
节省成本:虽然实现 CI/CD 需要在工具和流程上进行初期投资,但从长远来看,它可以通过减少大量的手动测试需求、降低与部署相关的错误并提高资源利用率,从而实现成本节省。
-
合规性和审计:对于有监管要求的组织,CD 可以通过提供详细的更改和部署历史记录来改善合规性,使得跟踪和审计代码变更变得更加容易。
需要注意的是,虽然 CD 和 CI 提供了许多优势,但它们也需要精心的规划、基础设施和文化变革才能发挥作用。
有多种模型和工具可以实现 CD。我们将在下一节中详细介绍其中的一些。
CD 模型和工具
一个典型的 CI/CD 工作流如以下图所示,以及随后的步骤:
图 12.1 – CI/CD 工作流
-
开发人员编写代码并将其推送到代码仓库(通常是 Git 仓库)。
-
您的 CI 工具构建代码,运行一系列测试,并将测试通过的构建推送到工件仓库。然后,您的 CD 工具获取该工件,并将其部署到测试和暂存环境中。根据您是否希望进行持续部署或交付,它会自动将工件部署到生产环境中。
好吧,您选择什么样的交付工具呢?让我们回顾一下我们在第十一章中讨论的例子,持续集成。我们选择了posts微服务应用,并使用如 GitHub Actions/Jenkins 这样的 CI 工具,利用Docker将其打包成容器并推送到我们的Docker Hub容器注册表。嗯,我们本可以使用相同的工具来部署到我们的环境中。
例如,如果我们想要部署到kubectl apply。我们可以使用任何这些工具来轻松完成,但我们选择不这么做。为什么?答案很简单——CI 工具是为了 CI 而设计的,如果你想用它们做其他事情,最终会遇到瓶颈。这并不意味着你不能用这些工具来做 CD,它仅适用于基于你所遵循的部署模型的某些用例。
根据你的应用、技术栈、客户需求、风险承受度和成本意识,存在多种部署模型。让我们来看一看一些业界常用的部署模型。
简单部署模型
简单部署模型是所有模型中最直接的一种:你在移除旧版本后,部署所需版本的应用程序。它完全替换了之前的版本,回滚则涉及在移除已部署版本后重新部署旧版本:
图 12.2 – 简单部署模型
由于这是一种简单的部署方式,你可以使用像Jenkins或GitHub Actions这样的 CI 工具来管理它。然而,简单部署模型并不是最理想的部署方法,因为它有一些固有的风险。这种变更具有破坏性,通常需要停机时间。这意味着在升级期间,你的服务会暂时无法提供给客户。对于没有 24/7 用户的组织来说,这可能是可以接受的,但中断会影响服务水平目标(SLOs)和服务水平协议(SLAs)的履行,特别是对于全球化的组织。即便没有相关协议,它们也会影响客户体验并损害组织声誉。
因此,为了应对这种情况,我们有一些复杂的部署模型。
复杂部署模型
复杂部署模型与简单部署模型不同,试图最小化应用中的中断和停机时间,使得发布过程更加顺畅,以至于大多数用户甚至没有注意到升级正在进行。业界流行的两种复杂部署方式是:让我们来看一看。
蓝绿部署
蓝绿部署(也称为红黑部署)是将新版本(绿)与现有版本(蓝)一起推出。然后,你可以进行完整性检查和其他活动,以确保一切正常。接着,你可以将流量从旧版本切换到新版本,并监控是否出现问题。如果遇到问题,你可以将流量切换回旧版本。否则,你可以继续运行最新版本并移除旧版本:
图 12.3 – 蓝绿部署
你可以通过金丝雀部署将蓝绿部署提升到下一个层次。
金丝雀部署和 A/B 测试
金丝雀部署与蓝绿部署类似,但通常用于高风险的升级。因此,像蓝绿部署一样,我们将新版本与现有版本一起部署。不同的是,我们不会立即将所有流量切换到最新版本,而是仅将流量切换给一小部分用户。在切换的过程中,我们可以通过日志和用户行为了解切换是否引起了问题。这就是所谓的 A/B 测试。在进行 A/B 测试时,我们可以根据位置、语言、年龄段或选择测试产品 Beta 版本的用户来针对特定的用户群体进行测试。这有助于组织收集反馈,而不会打扰到普通用户,并在对新版本满意后做出调整。你可以通过将所有流量切换到新版本并删除旧版本来使发布普遍可用:
图 12.4 – 金丝雀部署
虽然复杂的部署对用户的干扰最小,但通常使用传统的 CI 工具(如 Jenkins)来管理时非常复杂。因此,我们需要在这一点上正确配置工具。市场上有几种 CD 工具可供选择,包括 Argo CD、Spinnaker、Circle CI 和 AWS Code Deploy。由于本书的重点是 GitOps,而 Argo CD 是一个 GitOps 原生工具,因此在本章中,我们将重点讨论 Argo CD。在深入应用部署之前,让我们回顾一下我们要部署的内容。
博客应用及其部署配置
由于我们在上一章中讨论了博客应用,让我们再次看看这些服务及其交互:
图 12.5 – 博客应用及其服务和交互
到目前为止,我们已经创建了用于构建、测试和推送博客应用微服务容器的 CI 流水线。这些微服务需要在某个地方运行。因此,我们需要一个环境来运行它们。我们将在上一章中的 posts 微服务中部署该应用程序,我也将其余服务的构建作为一个练习留给你。假设你已经构建了它们,我们将需要以下资源以确保应用程序顺利运行:
-
MongoDB:我们将部署一个启用了认证的 MongoDB 数据库,且拥有 root 凭据。凭据将通过环境变量注入,这些环境变量来自 Kubernetes Secret 资源。我们还需要持久化我们的数据库数据,因此我们需要一个挂载到容器的 PersistentVolume,该卷将通过 PersistentVolumeClaim 动态提供。由于容器是有状态的,我们将使用 StatefulSet 来管理它,并因此使用一个无头 Service 来暴露数据库。
-
posts、reviews、ratings、和users微服务将通过环境变量注入的根凭证与 MongoDB 进行交互,这些凭证来自与 MongoDB 相同的Secret。我们将使用各自的Deployment资源来部署它们,并通过单独的ClusterIP Services来暴露它们。 -
前端:前端微服务不需要与 MongoDB 交互,因此不会与 Secret 资源交互。我们还将使用Deployment资源来部署该服务。由于我们希望将此服务暴露到互联网上,我们将为其创建一个LoadBalancer Service。
我们可以用以下图示总结这些方面:
图 12.6 – 博客应用 – Kubernetes 资源与交互
现在,由于我们遵循 GitOps 模型,我们需要将所有资源的清单存储在 Git 上。然而,由于 Kubernetes Secrets 本身并不安全,我们不能将它们的清单直接存储在 Git 上。相反,我们将使用另一个名为SealedSecrets的资源来安全地管理这些信息。
在第二章,使用 Git 和 GitOps 的源代码管理中,我们讨论了应用程序和环境仓库作为 GitOps 驱动的 CI 和 CD 的基础构建块。在前一章中,我们在 GitHub 上创建了一个应用程序仓库,并使用 GitHub Actions(以及 Jenkins)来构建、测试并将我们的应用程序容器推送到 Docker Hub。由于 CD 专注于 DevOps 中的 Ops 部分,我们需要一个环境仓库来实现这一点,所以接下来我们将创建我们的环境仓库。
使用环境仓库进行持续声明式 IaC
如今,我们已经知道,必须创建一个 GKE 集群来托管我们的微服务。到目前为止,我们一直在使用gcloud命令来完成此任务;然而,由于gcloud命令并不是声明式的,因此在实施 GitOps 时使用它们并不是理想的做法。相反,我们将使用Terraform来为我们创建 GKE 集群。这样可以确保我们能够使用 Git 环境仓库声明性地部署和管理集群。接下来,我们就来创建一个集群。
创建和设置我们的环境仓库
访问github.com,并使用您选择的名称创建一个仓库。对于本练习,我们将使用mdo-environments。完成后,访问 Google Cloud Shell,使用ssh-keygen命令生成一个ssh-key对,将公钥复制到 GitHub(参见第二章,使用 Git 和 GitOps 的源代码管理,获取逐步说明),然后使用以下命令克隆该仓库:
$ cd ~
$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ git clone git@github.com:<your_account>/mdo-environments.git
$ cd mdo-environments
让我们复制一个 Terraform 的.gitignore文件,确保我们不会意外地提交 Terraform 状态、后端文件或.tfvars文件,使用以下命令:
$ cp -r ~/modern-devops/ch12/.gitignore .
现在,让我们使用以下命令将代码推送到 GitHub:
$ git add --all
$ git commit -m 'Added gitignore'
$ git push
现在我们已经推送了第一个文件并初始化了仓库,让我们根据环境来构建仓库结构。在环境仓库中我们将有两个分支——dev 和 prod。dev 分支中的所有配置将应用于 开发环境,而 prod 中的配置将应用于 生产环境。下图详细说明了这种方法:
图 12.7 – CD 流程
现有的仓库只有一个名为 master 的分支。然而,由于我们将在此仓库中管理多个环境,因此最好将 master 分支重命名为 prod。
访问 https://github.com/<your_user>/mdo-environments/branches,点击 prod 旁边的铅笔图标,然后点击 重命名分支。
现在我们已经重命名了分支,让我们移除现有的本地仓库,并使用以下命令重新克隆仓库:
$ cd ~ && rm -rf mdo-environments
$ git clone git@github.com:<your_account>/mdo-environments.git
$ cd mdo-environments
我们想从开发环境开始,因此最好从 prod 分支创建一个名为 dev 的分支。运行以下命令来实现:
$ git branch dev && git checkout dev
现在,我们可以开始在此目录中编写 Terraform 配置。配置文件位于 ~/modern-devops/ch12/mdo-environments/environments。使用以下命令将该目录中的所有内容复制到当前目录:
$ cp -r ~/modern-devops/ch12/environments/terraform .
$ cp -r ~/modern-devops/ch12/environments/.github .
在 terraform 目录中,有几个 Terraform 配置文件。
cluster.tf 文件包含了创建 Kubernetes 集群的配置。它大致如下:
resource "google_service_account" "main" {
account_id = "gke-${var.cluster_name}-${var.branch}-sa"
display_name = "GKE Cluster ${var.cluster_name}-${var.branch} Service Account"
}
resource "google_container_cluster" "main" {
name = "${var.cluster_name}-${var.branch}"
location = var.location
initial_node_count = 3
node_config {
service_account = google_service_account.main.email
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
timeouts {
create = "30m"
update = "40m"
}
}
它创建了两个资源——一个 cloud platform OAuth 范围。
我们将服务账户命名为 cluster_name 和 branch 变量的组合。这是必要的,因为我们需要区分不同环境中的集群。所以,如果集群名称是 mdo-cluster,而 Git 分支是 dev,我们将有一个名为 gke-mdo-cluster-dev-sa 的服务账户。我们将在 GKE 集群上使用相同的命名约定。因此,集群名称将为 mdo-cluster-dev。
我们有一个 provider.tf 文件,包含了 provider 和 backend 配置。我们在这里使用的是远程后端,因为我们希望将 Terraform 状态存储在远程。此场景下,provider.tf 文件大致如下:
provider "google" {
project = var.project_id
region = "us-central1"
zone = "us-central1-c"
}
terraform {
backend "gcs" {
prefix = "mdo-terraform"
}
}
在这里,我们在 provider 配置中指定了默认的 region 和 zone。此外,我们声明了 gcs 后端,只包含了 prefix 属性,值为 mdo-terraform。我们可以使用前缀来分离配置,以便在一个存储桶中存储多个 Terraform 状态。我们故意没有提供 bucket 名称,那个我们会在运行时通过 -backend-config 在 terraform init 时提供。存储桶名称将是 tf-state-mdo-terraform-<PROJECT_ID>。
提示
由于 GCS 存储桶应该具有全球唯一的名称,因此建议使用类似tf-state-mdo-terraform-<PROJECT_ID>这样的名称,因为项目 ID 是全球唯一的。
我们还有一个 variables.tf 文件,声明了 project_id、branch、cluster_name 和 location 变量,如下所示:
variable project_id {}
variable branch {...
default = "dev"
}
variable cluster_name {...
default = "mdo-cluster"
}
variable "location" {...
default = "us-central1-a"
}
现在我们已经准备好 Terraform 配置文件,接下来我们需要一个工作流文件,可以应用到我们的 GCP 项目。为此,我们创建了以下 GitHub Actions 工作流文件,即 .github/workflows/create-cluster.yml:
name: Create Kubernetes Cluster
on: push
jobs:
deploy-terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./terraform
steps:
- uses: actions/checkout@v2
- name: Install Terraform
id: install-terraform
run: wget -O terraform.zip https://releases.hashicorp.com/terraform/1.5.5/
terraform_1.5.5_linux_amd64.zip && unzip terraform.zip && chmod +x terraform && sudo mv
terraform /usr/local/bin
- name: Apply Terraform
id: apply-terraform
run: terraform init -backend-config="bucket=tf-state-mdo-terraform-${{ secrets.
PROJECT_ID }}" && terraform workspace select ${GITHUB_REF##*/} || terraform workspace new
${GITHUB_REF##*/} && terraform apply -auto-approve -var="project_id=${{ secrets.PROJECT_
ID }}" -var="branch=${GITHUB_REF##*/}"
env:
GOOGLE_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }}
这是一个两步的构建文件。第一步安装 Terraform,第二步应用 Terraform 配置。除此之外,我们在全局层面指定了./terraform作为工作目录。此外,我们在此文件中使用了一些秘密变量,即 GCP_CREDENTIALS,它是 Terraform 用来进行身份验证和授权 GCP API 的服务帐户密钥文件,以及 Google Cloud 的 PROJECT_ID。
我们还将存储桶名称提供为 tf-state-mdo-terraform-${{ secrets.PROJECT_ID }},以确保我们有一个唯一的存储桶名称。
由于我们使用 Terraform 工作区来管理多个环境,上述代码选择一个现有的 Terraform 工作区,工作区的名称由 ${GITHUB_REF##*/} 表示的分支名称决定,或者创建一个新的工作区。工作区在这里很重要,因为我们希望使用相同的配置,但为不同的环境使用不同的变量值。Terraform 工作区对应于环境,而环境对应于 Git 分支。所以,既然我们有 dev 和 prod 环境,我们也有对应的 Terraform 工作区和 Git 分支。
从 Terraform 和工作流配置中,我们可以推断出我们将需要以下内容:
-
用于 Terraform 进行身份验证和授权 GCP API 的 服务账户,以及我们需要添加为 GitHub 秘密的 JSON 密钥文件
-
我们将配置为 GitHub 秘密的 项目 ID
-
用作 Terraform 后端的 GCS 存储桶
所以,让我们继续在 GCP 中创建一个服务账户,这样 Terraform 就可以使用它来进行身份验证并授权访问 Google API。使用以下命令创建服务账户,提供相关的 身份与访问管理(IAM)权限,并下载凭证文件:
$ PROJECT_ID=<project_id>
$ gcloud iam service-accounts create terraform \
--description="Service Account for terraform" \
--display-name="Terraform"
$ gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:terraform@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/editor"
$ gcloud iam service-accounts keys create key-file \
--iam-account=terraform@$PROJECT_ID.iam.gserviceaccount.com
在你的工作目录中,你会看到一个名为key-file的文件。现在,前往 https://github.com/<your_github_user>/mdo-environments/settings/secrets/actions/new,创建一个名为GCP_CREDENTIALS的秘密。在“值”字段中,打印出key-file文件,复制其内容并粘贴到 GitHub 秘密的 values 字段中。
接下来,创建另一个秘密,PROJECT_ID,并在 values 字段中指定你的 GCP 项目 ID。
接下来我们需要做的是为 Terraform 创建一个 GCS 存储桶,作为远程后端使用。为此,请运行以下命令:
$ gsutil mb gs://tf-state-mdo-terraform-${PROJECT_ID}
此外,我们需要启用 Terraform 用于创建资源的 GCP API。为此,运行以下命令:
$ gcloud services enable iam.googleapis.com container.googleapis.com
所以,现在所有先决条件都已经满足,我们可以将代码推送到仓库。运行以下命令来执行此操作:
$ git add --all
$ git commit -m 'Initial commit'
$ git push --set-upstream origin dev
一旦我们推送代码,GitHub Actions 工作流就会被触发。很快,工作流将应用配置并创建 Kubernetes 集群。结果应该如下所示:
图 12.8 – 使用 GitHub Actions 和 Terraform 的 GitOps
要验证集群是否已成功创建,运行以下命令:
$ gcloud container clusters list
NAME: mdo-cluster-dev
LOCATION: us-central1-a
MASTER_VERSION: 1.27.3-gke.100
MASTER_IP: x.x.x.x
MACHINE_TYPE: e2-medium
NODE_VERSION: 1.27.3-gke.100
NUM_NODES: 3
STATUS: RUNNING
如您所见,mdo-cluster-dev 集群已经在环境中成功运行。如果我们对 Terraform 配置进行任何更改,这些更改将会自动应用。我们已经成功使用环境仓库创建了环境。这就是 推送模型 GitOps 的应用。现在,我们需要在环境中运行应用程序;为了管理和部署应用程序,我们将需要一个专用的 CD 工具。如前所述,我们将使用 Argo CD,因此让我们来了解一下它。
Argo CD 简介
Argo CD 是一个开源的声明式、基于 GitOps 的持续交付(CD)工具,旨在自动化在 Kubernetes 集群上部署和管理应用程序及基础设施。Argo CD 作为一个强大的应用程序控制器,高效地管理并确保您的应用程序顺利、安全地运行。Argo CD 采用 基于拉取的 GitOps 模型,因此会定期轮询环境仓库,检测任何配置漂移。如果它发现 Git 中的状态与实际运行在环境中的应用程序状态之间有任何漂移,它将进行修正,更改以反映 Git 仓库中声明的期望配置。
Argo CD 明确针对 Kubernetes 环境进行定制,使其成为管理 Kubernetes 集群上应用程序的流行选择。
除了传统的 Kubernetes 清单 YAML 文件外,Argo CD 还支持多种替代方法来定义 Kubernetes 配置:
-
Helm 图表
-
Kustomize
-
Ksonnet
-
Jsonnet 文件
-
普通的 YAML/JSON 清单文件
-
通过插件与其他定制化的配置管理工具进行集成
在 Argo CD 中,您可以定义包含 源 和 目标 的应用程序。源指定与之关联的 Git 仓库的详细信息、清单、helm 图表或 kustomize 文件的位置,然后将这些配置应用到指定的目标环境。这使您能够监控 Git 仓库中特定分支、标签的变化,或跟踪特定版本。您还可以使用多种跟踪策略。
您可以访问一个用户友好的基于 Web 的 UI 和 命令行接口(CLI)与 Argo CD 进行交互。此外,Argo CD 通过同步钩子和应用操作提供应用程序状态报告。如果集群内直接进行的任何修改偏离了 GitOps 方法,Argo CD 会及时通知您的团队,可能通过 Slack 渠道。
以下图表概述了 Argo CD 的架构:
图 12.9 – Argo CD 架构
那么,废话不多说,我们开始启动 Argo CD。
安装和设置 Argo CD
安装 Argo CD 非常简单——我们只需将在线提供的 install.yaml 清单包应用到我们希望安装它的 Kubernetes 集群中,清单包地址为 github.com/argoproj/argo-cd/blob/master/manifests/install.yaml。如需更个性化的安装,请参考 argo-cd.readthedocs.io/en/stable/operator-manual/installation/。
由于我们本章使用的是 GitOps,因此不会手动部署 Argo CD。相反,我们将使用 Terraform 通过环境仓库来设置它。
本节的资源位于 ~/modern-devops/ch12/environments-argocd-app。我们将使用与之前相同的环境仓库来管理这个环境。
因此,让我们 cd 进入 mdo-environments 本地仓库,并运行以下命令:
$ cd ~/mdo-environments
$ cp -r ~/modern-devops/ch12/environments-argocd-app/terraform .
$ cp -r ~/modern-devops/ch12/environments-argocd-app/manifests .
$ cp -r ~/modern-devops/ch12/environments-argocd-app/.github .
现在,让我们看看目录结构,理解我们正在做什么:
.
├── .github
│ └── workflows
│ └── create-cluster.yml
├── manifests
│ └── argocd
│ ├── apps.yaml
│ ├── install.yaml
│ └── namespace.yaml
└── terraform
├── app.tf
├── argocd.tf
├── cluster.tf
├── provider.tf
└── variables.tf
如我们所见,结构与之前类似,只是做了一些改动。首先让我们看一下 Terraform 配置。
Terraform 修改
现在,terraform 目录下新增了两个文件:
-
argocd.tf:这包含了部署 Argo CD 的 Terraform 配置。 -
app.tf:这包含了配置 Argo CD 应用程序的 Terraform 配置。
让我们详细探讨这两个文件。
argocd.tf
该文件以 time_sleep 资源开始,并显式依赖 google_container_cluster 资源。集群创建后,它会休眠 30 秒,以便做好准备响应请求:
resource "time_sleep" "wait_30_seconds" {
depends_on = [google_container_cluster.main]
create_duration = "30s"
}
要连接 GKE,我们将使用由 terraform-google-modules/kubernetes-engine/google//modules/auth 提供的 gke_auth 模块。我们将显式添加对 time_sleep 模块的依赖,以确保身份验证在集群创建后 30 秒发生:
module "gke_auth" {
depends_on = [time_sleep.wait_30_seconds]
source = "terraform-google-modules/kubernetes-engine/google//modules/auth"
project_id = var.project_id
cluster_name = google_container_cluster.main.name
location = var.location
use_private_endpoint = false
}
现在我们已经通过 GKE 集群进行了身份验证,接下来需要应用清单将 Argo CD 部署到集群中。为此,我们将使用 gavinbunney/kubectl 插件 (registry.terraform.io/providers/gavinbunney/kubectl/latest/docs)。
我们首先定义一些数据源,帮助生成 Kubernetes 清单,然后应用它们来安装 Argo CD。我们将为命名空间和 Argo CD 应用创建两个指向 manifests/argocd 目录下 namespace.yaml 和 install.yaml 文件的 kubectl_file_documents 数据源:
data "kubectl_file_documents" "namespace" {
content = file("../manifests/argocd/namespace.yaml")
}
data "kubectl_file_documents" "argocd" {
content = file("../manifests/argocd/install.yaml")
}
使用这些数据源,我们可以为命名空间和 Argo CD 应用创建两个 kubectl_manifest 资源。这些资源将应用 GKE 集群中的清单:
resource "kubectl_manifest" "namespace" {
for_each = data.kubectl_file_documents.namespace.manifests
yaml_body = each.value
override_namespace = "argocd"
}
resource "kubectl_manifest" "argocd" {
depends_on = [
kubectl_manifest.namespace,
]
for_each = data.kubectl_file_documents.argocd.manifests
yaml_body = each.value
override_namespace = "argocd"
}
现在我们已将 Argo CD 安装配置添加完毕,还需要配置 Argo CD 应用程序。为此,我们有 app.tf 文件。
app.tf
类似于 Argo CD 配置,我们有一个从 manifests/argocd/apps.yaml 文件读取的 kubectl_file_documents 数据源;kubectl_manifest 资源将把清单应用到 Kubernetes 集群:
data "kubectl_file_documents" "apps" {
content = file("../manifests/argocd/apps.yaml")
}
resource "kubectl_manifest" "apps" {
depends_on = [
kubectl_manifest.argocd,
]
for_each = data.kubectl_file_documents.apps.manifests
yaml_body = each.value
override_namespace = "argocd"
}
我们还修改了 provider.tf 文件,因此接下来我们将探讨它。
provider.tf
在这个文件中,我们包括了 kubectl 提供者,如下所示:
...
provider "kubectl" {
host = module.gke_auth.host
cluster_ca_certificate = module.gke_auth.cluster_ca_certificate
token = module.gke_auth.token
load_config_file = false
}
terraform {
required_providers {
kubectl = {
source = "gavinbunney/kubectl"
version = ">= 1.7.0"
}
}...
}
现在,让我们检查一下清单目录。
Kubernetes 清单
清单目录包含我们将应用到 Kubernetes 集群的 Kubernetes 清单。由于我们正在首先设置 Argo CD,因此当前它只包含 argocd 目录;然而,我们将在本章后续扩展并添加更多目录。
manifests/argocd 目录包含以下文件:
-
namespace.yaml:创建argocd命名空间的清单,Argo CD 将在该命名空间中运行。 -
install.yaml:创建 Argo CD 应用的清单。该清单从官方 Argo CD 发布 URL 下载。 -
apps.yaml:此文件包含一个 Argo CD ApplicationSet 配置。
虽然 namespace.yaml 和 install.yaml 文件显而易见,但我们来详细讨论一下 apps.yaml 文件以及 Argo CD ApplicationSet 资源。
Argo CD Application 和 ApplicationSet
为了声明性地管理应用程序,Argo CD 使用 source 属性来指定它需要应用的内容,并使用 target 属性指定应用目标。一个 Application 资源只适用于一个应用程序。例如,要部署我们的 Blog 应用,我们需要像下面这样创建一个 Application 资源:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: blog-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/<your_github_repo>/mdo-environments.git
targetRevision: HEAD
path: manifests/nginx
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
selfHeal: true
此清单定义了一个 Argo CD Application 资源,包含以下部分:
-
project:我们可以将应用程序组织到不同的项目中。在这种情况下,我们将使用default项目。 -
source:此部分定义了 Argo CD 所需的配置,用于跟踪并从 Git 仓库拉取应用程序配置。通常包含repoURL、targetRevision和应用程序清单所在路径的path值。 -
destination:此部分定义了我们希望应用清单的target值,通常包含server部分,并包含 Kubernetes 集群的 URL。 -
syncPolicy:此部分定义了 Argo CD 在从 Git 仓库同步博客应用程序时应应用的策略,以及在检测到偏差时应该采取的措施。在之前的配置中,它会尝试自动纠正来自 Git 仓库的任何偏差,因为selfHeal已设置为true。
我们完全可以为每个应用程序定义多个应用程序清单。但是,对于较大的项目来说,这可能会成为一种负担。为了管理这一点,Argo CD 提供了一种通过 ApplicationSet 资源创建和管理应用程序的通用方法。
ApplicationSet 资源为我们提供了一种通过定义模式动态生成应用程序资源的方法。在我们的案例中,我们具有以下结构:
manifests
└── argocd
│ ├── apps.yaml
│ ├── install.yaml
│ └── namespace.yaml
└── blog-app
│ └── manifest.yaml
└── <other-app>
└── manifest.yaml
因此,逻辑上来说,对于 manifests 目录中的每个子目录,我们都需要创建一个新的应用程序,并以目录名称命名。相应的应用程序配置应从子目录中获取所有清单。
我们在apps.yaml文件中定义了以下ApplicationSet:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: argo-apps
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/<your_github_repo>/mdo-environments.git
revision: HEAD
directories:
- path: manifests/*
- path: manifests/argocd
exclude: true
template:
metadata:
name: '{{path.basename}}'
spec:
project: default
source:
repoURL: https://github.com/<your_github_repo>/mdo-environments.git
targetRevision: HEAD
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
selfHeal: true
ApplicationSet 具有以下部分:
-
generators:此部分定义了 Argo CD 应如何生成应用程序资源。我们使用了git生成器,它包含repoURL、revision和directories部分。directories部分定义了我们希望从中获取应用程序的目录。我们已将其设置为manifests/*。因此,它将查找manifests目录中的每个子目录。我们还定义了一个排除目录manifests/argocd,因为我们不希望 Argo CD 管理部署自身的配置。 -
templates:此部分定义了创建应用程序的模板。如我们所见,内容与应用程序资源定义非常相似。对于metadata.name,我们指定了{{path.basename}},这意味着它将根据我们预期的子目录名称创建应用程序资源。template.spec.source.path属性包含相应应用程序清单的源路径,因此我们将其设置为{{path}}—— 即子目录。所以,我们将基于之前的目录结构生成blog-app和<other-app>应用程序。其余属性与我们之前讨论的应用程序资源相同。
现在我们已经配置好了安装和设置 Argo CD 所需的一切,让我们通过以下命令提交并推送此配置到远程仓库:
$ git add --all
$ git commit -m "Added argocd configuration"
$ git push
我们会看到 GitHub 在更新时运行 Actions 工作流并部署 Argo CD。一旦工作流成功完成,我们就可以访问 Argo CD Web UI。
访问 Argo CD Web UI
在我们可以访问 Argo CD Web UI 之前,我们必须通过 GKE 集群进行身份验证。为此,请运行以下命令:
$ gcloud container clusters get-credentials \
mdo-cluster-dev --zone us-central1-a --project $PROJECT_ID
要使用 Argo CD Web UI,您需要获取 argo-server 服务的外部 IP 地址。要获得该地址,请运行以下命令:
$ kubectl get svc argocd-server -n argocd
NAME TYPE EXTERNAL-IP PORTS AGE
argocd-server LoadBalaner 34.122.51.25 80/TCP,443/TCP 6m15s
我们现在知道 Argo CD 可以通过 34.122.51.25/ 访问。访问此链接后,您会注意到需要用户名和密码进行身份验证:
图 12.10 – Argo CD Web UI – 登录页面
Argo CD 默认提供一个初始的admin用户,该用户的密码以明文形式存储在argocd-initial-admin-secret Secret 资源中。虽然您可以使用此默认设置,但值得注意的是,它是从公开的 YAML 清单生成的。因此,建议您进行更新。要更新,请执行以下命令:
$ kubectl patch secret argocd-secret -n argocd \
-p '{"data": {"admin.password": null, "admin.passwordMtime": null}}'
$ kubectl scale deployment argocd-server --replicas 0 -n argocd
$ kubectl scale deployment argocd-server --replicas 1 -n argocd
现在,请等待两分钟,以便生成新的凭据。之后,执行以下命令以获取密码:
$ kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d && echo
现在,您已经拥有必要的凭据,请登录,您将看到以下页面:
图 12.11 – Argo CD Web UI – 首页
我们已经成功设置了 Argo CD。接下来的步骤是部署我们的应用程序;然而,正如我们所知,我们的应用程序使用 Kubernetes Secrets,而我们不能将其存储在 Git 中,因此我们需要找到一种机制来安全地存储它。为了解决这个问题,我们有 Bitnami 的 SealedSecret 资源。我们将在下一节中讨论这一点。
管理敏感配置和机密
Sealed Secrets 解决了 我可以在 Git 中管理所有 Kubernetes 配置,除了 Secrets 的问题。Sealed Secrets 作为存储敏感信息的安全容器。当您需要存储机密信息,如密码或密钥时,您将它们放入这些专门的封装中。只有 Kubernetes 中的 Sealed Secrets 控制器才能解锁并访问其中的内容。这确保了您的宝贵机密的最高安全性和保护。由 Bitnami Labs 创建并开源,它们帮助您使用非对称加密将 Kubernetes Secrets 加密为 Sealed Secrets,只有在集群中运行的 Sealed Secrets 控制器才能解密。这意味着您可以将 Sealed Secrets 存储在 Git 中,并使用 GitOps 设置一切,包括 Secrets。
Sealed Secrets 包含两个组件:
-
一个名为
kubeseal的客户端工具帮助我们从标准 Kubernetes Secret YAML 生成 Sealed Secrets -
集群端 Kubernetes 控制器/操作员解锁您的机密,并将密钥证书提供给客户端工具。
使用 Sealed Secrets 时的典型工作流如下图所示:
图 12.12 – Sealed Secrets 工作流
现在,让我们继续安装 Sealed Secrets 操作员。
安装 Sealed Secrets 操作员
要安装Sealed Secrets 操作符,你只需从最新版本的github.com/bitnami-labs/sealed-secrets/releases下载控制器清单。在编写本书时,最新的控制器清单为github.com/bitnami-labs/sealed-secrets/releases/download/v0.23.1/controller.yaml。
在manifest目录下创建一个名为sealed-secrets的新目录,并使用以下命令下载controller.yaml:
$ cd ~/mdo-environments/manifests & mkdir sealed-secrets
$ cd sealed-secrets
$ wget https://github.com/bitnami-labs/sealed-secrets\
/releases/download/v0.23.1/controller.yaml
然后,将更改提交并推送到远程仓库。大约五分钟后,Argo CD 会创建一个名为sealed-secrets的新应用程序并部署。你可以在 Argo CD Web UI 中查看:
图 12.13 – Argo CD Web UI – Sealed Secrets
在 Kubernetes 集群中,sealed-secrets-controller将在kube-system命名空间中可见。运行以下命令来检查:
$ kubectl get deployment -n kube-system sealed-secrets-controller
NAME READY UP-TO-DATE AVAILABLE AGE
sealed-secrets-controller 1/1 1 1 6m4s
如我们所见,控制器正在运行并已准备好。现在我们可以安装客户端工具kubeseal。
安装 kubeseal
要安装客户端工具,你可以访问github.com/bitnami-labs/sealed-secrets/releases,从页面中获取kubeseal安装二进制文件的链接。以下命令将会在你的系统中安装kubeseal 0.23.1:
$ KUBESEAL_VERSION='0.23.1'
$ wget "https://github.com/bitnami-labs/sealed-secrets/releases/download\
/v${KUBESEAL_VERSION:?}/kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz"
$ tar -xvzf kubeseal-${KUBESEAL_VERSION:?}-linux-amd64.tar.gz kubeseal
$ sudo install -m 755 kubeseal /usr/local/bin/kubeseal
$ rm -rf ./kubeseal*
要检查kubeseal是否已成功安装,请运行以下命令:
$ kubeseal --version
kubeseal version: 0.23.1
既然kubeseal已经安装完成,我们接下来就创建一个blog-app的 Sealed Secret。
创建 Sealed Secrets
要创建 Sealed Secret,我们必须定义 Kubernetes Secret 资源。mongodb-creds Secret 应包含一些键值对,键MONGO_INITDB_ROOT_USERNAME的值为root,键MONGO_INITDB_ROOT_PASSWORD的值为你希望设置的密码。
由于我们不希望将明文的 Secret 作为文件存储,首先我们将使用--dry-run和-o yaml标志创建一个名为mongodb-creds的 Kubernetes Secret 清单,然后将输出直接通过管道传送到kubeseal,以生成SealedSecret资源,命令如下:
$ kubectl create secret generic mongodb-creds \
--dry-run=client -o yaml --namespace=blog-app \
--from-literal=MONGO_INITDB_ROOT_USERNAME=root \
--from-literal=MONGO_INITDB_ROOT_PASSWORD=<your_pwd> \
| kubeseal -o yaml > mongodb-creds-sealed.yaml
这将生成mongodb-creds-sealed.yaml Sealed Secret,其内容如下:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: mongodb-creds
namespace: blog-app
spec:
encryptedData:
MONGO_INITDB_ROOT_PASSWORD: AgB+tyskf72M/…
MONGO_INITDB_ROOT_USERNAME: AgA95xKJg8veOy8v/…
template:
metadata:
name: mongodb-creds
namespace: blog-app
如你所见,Sealed Secret 与 Secret 清单非常相似。然而,它并没有包含 Base64 编码的秘钥值,而是对其进行了加密,以便只有 Sealed Secrets 控制器才能解密。你可以轻松地将这个文件提交到版本控制中。接下来,我们就这么做。使用以下命令将 Sealed Secret YAML 文件移动到manifests/blog-app目录:
$ mkdir -p ~/mdo-environments/manifests/blog-app/
$ mv mongodb-creds-sealed.yaml ~/mdo-environments/manifests/blog-app/
现在我们已经成功生成了 Sealed Secret 并将其移动到manifests/blog-app目录,接下来我们将在下一节设置应用程序的其他部分。
部署示例博客应用
要部署示例博客应用,我们需要定义应用资源。我们已经讨论过应用的组成。我们将应用包定义为一个 Kubernetes 清单文件,名为blog-app.yaml。我们需要使用以下命令将此 YAML 文件复制到manifests/blog-app目录:
$ cp ~/modern-devops/ch12/blog-app/blog-app.yaml \
~/mdo-environments/manifests/blog-app/
我已经预先构建了微服务,并使用了所需的git-sha作为标签,就像我们在上一章中做的那样。你可以编辑 YAML 文件,并将每个应用的镜像替换为你的镜像。
完成后,提交并推送更改到mdo-environments仓库。
一旦你推送更改,你应该注意到blog-app应用在五分钟内会开始出现在 Argo CD UI 中:
图 12.14 – Argo CD Web UI – 应用
等待应用进度更新。一旦显示为绿色,你应该能在应用中看到以下内容:
图 12.15 – Argo CD Web UI – blog-app
现在应用已经完全同步,我们可以检查在blog-app命名空间中创建的资源。首先,使用以下命令列出服务:
$ kubectl get svc -n blog-app
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
frontend LoadBalancer 10.71.244.154 34.68.221.0 80:3203/TCP
mongodb ClusterIP None <none> 27017/TCP
posts ClusterIP 10.71.242.211 <none> 5000/TCP
ratings ClusterIP 10.71.244.78 <none> 5000/TCP
reviews ClusterIP 10.71.247.128 <none> 5000/TCP
users ClusterIP 10.71.241.25 <none> 5000/TCP
如我们所见,它列出了我们定义的所有服务。请注意,frontend 服务是LoadBalancer类型,并且有一个外部 IP。记下这个外部 IP,因为我们将用它来访问应用。
现在,让我们列出pods,看看所有微服务是否都正常运行:
$ kubectl get pod -n blog-app
NAME READY STATUS RESTARTS
frontend-7cbdc4c6cd-4jzdw 1/1 Running 0
mongodb-0 1/1 Running 0
posts-588d8bcd99-sphpm 1/1 Running 0
ratings-7dc45697b-wwfqd 1/1 Running 0
reviews-68b7f9cb8f-2jgvv 1/1 Running 0
users-7cdd4cd94b-g67zw 1/1 Running 0
如我们所见,所有的 Pods 都运行正常。请注意,mongodb-0 Pod 包含数字前缀,但其他 Pods 具有随机 UUID。你可能还记得,当我们创建mongodb-creds秘密时,它也已被创建:
$ kubectl get secret -n blog-app
NAME TYPE DATA AGE
mongodb-creds Opaque 2 80s
在这里,我们可以看到mongodb-creds秘密已经创建。这表明 SealedSecret 工作正常。
现在,让我们通过打开http://<frontend-svc-external-ip>来访问我们的应用。如果看到以下页面,说明应用已正确部署:
图 12.16 – 博客应用首页
作为练习,点击登录 > 还不是用户?创建账户,然后填写信息进行注册。你可以创建一个新帖子,添加评论,并提供评分。你还可以更新评论,删除评论,更新评分等。尝试使用应用,查看是否所有功能都正常工作。你应该能够看到类似以下内容:
图 12.17 – 博客应用文章
由于我们对应用程序很满意,我们可以从 dev 分支向 prod 分支发起拉取请求。一旦你合并了拉取请求,你会看到相似的服务出现在生产环境中。你也可以使用基于拉取请求的门控进行 CD。这确保了你的环境保持独立,尽管它们来自同一个仓库,只是来自不同的分支。
总结
本章涵盖了持续部署和交付,并理解了 CD 的必要性以及容器应用程序的基本 CD 工作流。我们讨论了几种现代部署策略,并了解了 CI 工具无法满足这些责任。利用 GitOps 原则,我们创建了一个环境仓库,并通过 GitHub Actions 使用基于推送的模型部署了基于 GKE 的环境。然后,我们看到了如何使用 Argo CD 作为我们的 CD 工具并安装它。为了避免将敏感信息(如密钥)提交到 Git 中,我们讨论了 Bitnami 的 Sealed Secrets。接着,我们使用 Argo CD 和 GitOps 部署了示例博客应用程序。
在下一章中,我们将探讨现代 DevOps 中的另一个重要方面——确保部署管道的安全。
问题
回答以下问题以测试你对本章内容的掌握:
-
以下哪些是 CD 工具?(选择三个)
A. Spinnaker
B. GitHub
C. Argo CD
D. AWS Code Deploy
-
CD 需要人工干预才能部署到生产环境。(正确/错误)
-
Argo CD 开箱即用支持蓝绿部署。(正确/错误)
-
你会使用什么来启动 Argo CD 的部署?
A. 手动触发管道
B. 提交更改到你的 Git 仓库
C. 使用 CI 触发 Argo CD 管道
D. Argo CD 管道不会响应外部刺激
-
Argo CD 的 ApplicationSet 帮助基于模板生成应用程序。(正确/错误)
-
你应该优先选择哪些分支名称用于你的环境仓库?
A.
dev、staging和prodB.
feature、develop和masterC.
release和main -
以下哪些部署模型是 Argo CD 使用的?
A. 推送模型
B. 拉取模型
C. 阶段性模型
-
你应该使用 Terraform 来安装 Argo CD,因为你可以将所有配置存储在 Git 中。(正确/错误)
-
Argo CD 可以从以下哪些来源同步资源?(选择两个)
A. Git 仓库
B. 容器注册表
C. JFrog Artifactory 的原始仓库
-
如果你在 Git 之外手动更改了一个资源,Argo CD 会怎么办?
A. Argo CD 会更改资源,使其与 Git 配置匹配
B. Argo CD 会通知你资源在 Git 之外发生了变化
C. Argo CD 什么也不做
-
你可以将 Sealed Secrets 提交到 Git 仓库中。(正确/错误)
答案
以下是本章问题的答案:
-
A、C 和 D
-
正确
-
正确
-
B
-
正确
-
A
-
B
-
正确
-
A, B
-
A
-
正确
1174

被折叠的 条评论
为什么被折叠?



