Linux-DevOps-手册-全-

Linux DevOps 手册(全)

原文:annas-archive.org/md5/55f0ee1b5d0f6f58bdd7da1ffd9f7954

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

DevOps 已经成为现代软件开发和交付的关键组成部分。它彻底改变了我们构建、测试、部署和运营软件系统的方式。DevOps 不仅是一套工具和实践,它更是一种文化和心态,强调协作、沟通和自动化。

本书旨在成为一本全面的 DevOps 指南,涵盖从选择合适的 Linux 发行版到避免 DevOps 中的陷阱的所有内容。本书的每一章都提供了详细的信息和实际的示例,帮助你理解这些概念并将其应用于实际场景中。

本书适合对象

本书面向那些已经在软件开发和 IT 运维领域积累了一定知识和经验的人,旨在进一步扩展他们对 DevOps 和 Linux 系统的理解。

如果你对 Linux 系统不太熟悉,本书将为你提供必要的指导和工具,帮助你快速学习并掌握 Linux 基础设施的管理。你将了解 Linux 操作系统、其架构及基本概念。

此外,本书还强调学习公共云技术,重点介绍 AWS。如果你有兴趣了解如何使用 AWS 构建和管理可扩展、可靠的系统,本书将为你提供必要的知识和工具,帮助你入门。

无论你是 DevOps 新手还是已经有一定经验,本书都为学习更复杂的概念提供了坚实的基础。它涵盖了从 Linux 系统基础到更高级的 DevOps 实践(如配置与基础设施即代码、CI/CD 等)的一系列主题。

本书涵盖的内容

第一章选择合适的 Linux 发行版,讨论了 GNU/Linux 的历史以及流行发行版之间的差异。

第二章命令行基础,引导你了解命令行的使用以及我们在全书中将使用的常用工具。

第三章进阶 Linux,描述了 GNU/Linux 中的一些高级功能,这些功能将对你有用。

第四章使用 Shell 脚本自动化,解释了如何使用 Bash shell 编写自己的脚本。

第五章Linux 中的服务管理,讨论了管理 Linux 中服务的不同方式,并向你展示如何使用 systemd 定义自己的服务。

第六章Linux 中的网络,描述了网络是如何工作的,如何控制网络配置的不同方面,以及如何使用命令行工具。

第七章Git,通往 DevOps 的门户,讨论了 Git 是什么,以及如何使用 Git 的版本控制系统,包括一些不太为人知的 Git 特性。

第八章Docker 基础,探讨了如何将你的服务或工具容器化,以及如何运行和管理容器。

第九章深入探索 Docker,讨论了 Docker 的更多高级功能,包括 Docker Compose 和 Docker Swarm。

第十章监控、追踪和分布式日志记录,讨论了如何监控你的服务、可以在云中使用的工具以及如何进行基础设置。

第十一章使用 Ansible 进行配置即代码(Configuration as Code),介绍了如何使用 Ansible 实现配置即代码;它将引导你完成 Ansible 的基本设置及更多高级功能。

第十二章利用基础设施即代码(Infrastructure as Code),讨论了基础设施即代码IaC)的概念、流行工具以及如何使用 Terraform 管理基础设施。

第十三章使用 Terraform、GitHub 和 Atlantis 实现 CI/CD,通过使用 Terraform 和 Atlantis 对基础设施进行持续集成CI)和持续部署(**CD),将 IaC 推向更高水平。

第十四章避免 DevOps 中的陷阱,讨论了你在 DevOps 工作中可能遇到的挑战。

为了最大限度地从本书中受益

你需要在虚拟机或计算机的主操作系统中安装 Debian Linux 或 Ubuntu Linux。我们使用的其他软件要么已作为默认工具集预装,要么我们会告诉你如何获取并安装它。

假设你具备一些基本的 Linux 知识及其命令行界面(CLI)操作经验。熟悉 shell 脚本和基本的编程概念也将对你有帮助。此外,建议你具备一定的 IT 基础设施管理知识,并对软件开发实践有一定的了解。

本书面向 DevOps 新手,假设你渴望学习这个领域中常用的工具和概念。在阅读完本书后,你将能深入理解如何使用 IaC 工具(如 Terraform 和 Atlantis)来管理基础设施,并掌握如何使用 Ansible 和 Bash 脚本自动化重复任务。你还将学习如何设置日志记录和监控解决方案,帮助你维护和排查基础设施问题。

本书涉及的软件/硬件 操作系统要求
Bash Linux 操作系统预装
Ansible Python 3 或更新版本
Terraform Linux 操作系统
AWS CLI Python 3 或更新版本
Docker Linux 操作系统

如果您正在使用本书的数字版,我们建议您自己输入代码或通过本书的 GitHub 仓库获取代码(下节会提供链接)。这样可以帮助您避免与复制粘贴代码相关的潜在错误。

下载示例代码文件

您可以从 GitHub 上下载本书的示例代码文件,链接为 github.com/PacktPublishing/The-Linux-DevOps-Handbook。如果代码有更新,将会在 GitHub 仓库中进行更新。

我们还提供了丰富的书籍和视频代码包,您可以访问 github.com/PacktPublishing/ 查看。

使用的约定

本书中使用了多种文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。示例如下:“当以 root 用户登录时,您的提示符号会以 # 结尾。当以普通用户登录时,提示符号会显示 $。”

代码块如下所示:

docker build [OPTIONS] PATH | URL | -

当我们希望引起您注意某一代码块的特定部分时,相关行或项目将以粗体显示:

docker build [OPTIONS] PATH | URL | -

任何命令行输入或输出将按如下方式书写:

chmod ug=rx testfile

粗体:表示新术语、重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会以粗体显示。示例如下:“Ansible Galaxy 是一个由社区驱动的平台,提供了大量的 Ansible 角色和剧本。”

提示或重要说明

这样显示。

联系我们

我们始终欢迎读者的反馈。

常规反馈:如果您对本书的任何方面有疑问,请通过电子邮件联系 customercare@packtpub.com,并在邮件主题中注明书名。

勘误:尽管我们已尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将非常感激您能将其报告给我们。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果您在互联网上遇到我们作品的任何非法副本,请向我们提供该位置地址或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个主题上有专长并且有兴趣写作或为书籍贡献内容,请访问 authors.packtpub.com

分享您的想法

一旦您阅读完 《Linux DevOps 手册》,我们很想听听您的想法!请点击这里直接进入本书的亚马逊评论页面并分享您的反馈

您的评论对我们以及技术社区非常重要,能够帮助我们确保提供优质内容。

下载本书的免费 PDF 版本

感谢您购买本书!

您喜欢随时阅读,但无法随身携带纸质书籍吗?

您购买的电子书与您选择的设备不兼容吗?

不用担心,现在每本 Packt 书籍都可以免费获得该书的无 DRM 版本 PDF。

随时随地,在任何设备上阅读。您可以直接从您最喜欢的技术书籍中搜索、复制并粘贴代码到您的应用程序中。

好处不仅仅是这些,您还可以获得独家的折扣、新闻通讯,并且每天会在您的邮箱中收到精彩的免费内容

按照以下简单步骤获得福利:

  1. 扫描二维码或访问下面的链接

packt.link/free-ebook/9781803245669

  1. 提交您的购买证明

  2. 就是这样!我们会直接将您的免费 PDF 和其他福利发送到您的邮箱

第一部分:Linux 基础

在本书的开篇部分,我们将重点介绍 Linux 发行版及您需要的基本技能,以便高效使用 Linux 操作系统。您还将学习如何编写基本的 Shell 脚本来自动化日常任务。

本部分包括以下章节:

  • 第一章选择合适的 Linux 发行版

  • 第二章命令行基础

  • 第三章Linux 进阶

  • 第四章使用 Shell 脚本自动化

第一章:选择合适的 Linux 发行版

在本章中,我们将从 Linux 的最初开始深入探讨 Linux 世界。我们将简要介绍 Linux 的历史,解释什么是发行版,并解释在选择发行版用于生产环境时需要考虑的因素。你不需要了解 Linux、其管理或云计算。如果你不理解我们使用的某些词汇,不用担心。本章中不会有太多让人困惑的术语,如果有,我们将在后续章节中解释它们。当你读完本章后,你应该能够理解为什么有这么多不同的 Linux 版本,它的费用大概是多少,以及如何为自己选择合适的 Linux 版本。

本章将涵盖以下主要内容:

  • 什么是 Linux,什么是 Linux 发行版?

  • 你可以使用什么来帮助你做出正确的决策?

  • 目前有几个非常流行的主要 Linux 发行版。

技术要求

本章没有任何技术要求。我们暂时不会运行任何命令或安装任何软件。这些内容会在后续章节中讲解。

我们在书中展示的代码可以在公开的 GitHub 仓库中找到,您可以通过以下地址进行查看:github.com/PacktPublishing/The-Linux-DevOps-Handbook

什么是 Linux 发行版?

Linux 是云工作负载的标准操作系统。然而,并没有一个统一的 Linux 操作系统叫这个名字。它的生态系统相当复杂,这源于其最初的形成过程。

在 Linux 的创始人 Linus Torvalds 设想出 Linux 之前,Unix 就已经存在。由于法律原因,Unix 的源代码是可以授权给任何购买它的人使用的,这使得它在许多机构中非常流行,包括大学。然而,这些代码并非完全免费。这让许多人不满,他们认为软件应该是免费的——就像言论自由或啤酒自由一样——包括源代码。到了 1980 年代,一个完全免费和开放的 Unix 实现诞生了,这个项目被称为 GNU 项目。其目标是开发一个可以让用户完全控制计算机的操作系统。该项目成功地开发出了运行操作系统所需的所有软件,除了一个——内核。

操作系统的内核简而言之,就是操作硬件并为用户管理硬件和程序的核心。

1991 年,芬兰学生 Linus Torvalds 著名地宣布了他的爱好内核——Linux。当时他称其为“只是一个爱好——不会像 GNU 那样变得庞大和专业。”它本不打算变得庞大和流行。接下来发生的就是历史。Linux 内核在开发者和管理员中变得流行,并成为了 GNU 项目的缺失部分。Linux 是内核,GNU 工具是所谓的用户空间,它们一起组成了 GNU/Linux 操作系统。

前面的短故事对我们有两个重要的意义:

  • 虽然 GNU 用户空间和 Linux 内核是最流行的组合,但你会看到它并不是唯一的。

  • Linux 提供内核,GNU 项目提供用户空间工具,但它们必须以某种方式准备好以供安装。许多人和团队对如何最好地做这件事有不同的想法。我将继续扩展这个思路。

一个团队或公司将 GNU/Linux 操作系统交付给最终用户的方式被称为 发行版。它便于操作系统的安装,后续管理软件的手段,以及如何管理操作系统和正在运行的进程的一般概念。

是什么使得各个发行版不同?

Linux 和 GNU 项目的开放性质使几乎任何人都可以创建自己的发行版。让新用户感到困惑的其中一个原因是他们可以使用的 操作系统OS)版本之多。让 Linux 用户之间爆发圣战的可靠方法就是问哪个发行版最好。

我们可以通过发行软件的格式(软件包)和用于安装和移除这些软件的附加软件(软件包管理器)来对 Linux 发行版进行分组。有很多种方式,但最常见的两种是 RPMRPM 软件包管理器)和 DEB 软件包。软件包不仅仅是包含二进制文件的档案。它们还包含设置软件以便使用的脚本——创建目录、用户、权限、日志规则,以及其他一些我们将在后续章节中解释的内容。

RPM 发行版家族从 Red Hat Enterprise LinuxRHEL)开始,由 Red Hat 公司创建和维护。与之紧密相关的是 Fedora(由 Red Hat 赞助的自由社区发行版)。它还包括 CentOS Linux(RHEL 的免费版本)和 Rocky Linux(RHEL 的另一个免费版本)。

DEB 发行版包括 Debian(DEB 软件包的来源)——一个技术官僚社区项目。从 Debian 发行版中衍生出了一些基于它的发行版,使用了大部分核心组件。最著名的是 Ubuntu,由 Canonical 赞助的服务器和桌面发行版。

也有一些发行版使用最少自动化的软件包,最著名的是 Slackware,这是现存最古老的 Linux 发行版之一。

有些发行版为用户提供了一套脚本,用于在实际使用的硬件上编译软件——最著名的就是 Gentoo

最后,有一种发行版实际上是一本包含一套指令的书籍,用户可以按照这些指令手动构建整个操作系统——Linux From Scratch 项目。

另一种对发行版进行分组的方式是根据它们对封闭软件的接受程度——封闭软件限制了源代码、二进制文件或两者的分发。这可以是硬件驱动程序,如 NVIDIA 显卡驱动,也可以是用户软件,如允许播放流媒体和 DVD、蓝光光盘的电影解码器。一些发行版让安装和使用这些软件变得简单,而另一些则让它更困难,主张我们应该努力实现所有软件都为开源且免费的(无论是自由言论还是免费饮料的含义)。

另一个区分它们的方式是某个发行版使用的安全框架。两个最著名的框架是 AppArmor,主要由 Ubuntu 使用,以及 SELinux(来自美国国家安全局),用于包括 Red Hat Enterprise Linux(及其衍生版)和 Fedora Linux 在内的发行版。

还值得注意的是,虽然大多数 Linux 发行版使用 GNU 项目作为用户空间,流行的云端 Alpine Linux 发行版使用的是自己的一套软件,特别是以最小化大小为目标编写的。

看看发行版的开发方式,它可以是由社区驱动的(没有任何商业实体拥有过程或软件的任何部分——以 Debian 为例),也可以是商业性质的(完全由公司拥有——以 RHEL 为例,SuSE 另一个例子),以及介于两者之间的各种混合形式(Ubuntu 和 Fedora 是典型的商业拥有的发行版,拥有大量独立贡献者)。

最后,我们可以通过发行版如何促进云工作负载来对它们进行分组。在这里,我们可以从不同的方面来看:

  • 服务器端:某个发行版作为我们基础设施、虚拟机和容器的底层操作系统的表现如何。

  • 服务端:某个发行版在作为容器或虚拟机运行我们的软件时的表现如何。

为了让新用户更加困惑和有趣,每个发行版可以有许多变体(根据不同发行版的术语,叫做 flavorsvariantsspins),它们提供不同的软件集合或默认配置。

最后,亲爱的读者,为了让你更加困惑,对于桌面或笔记本电脑的使用,Linux 提供了它能给你的最佳选择——选择权。Linux 操作系统的图形界面种类繁多,甚至最有经验的用户也会头晕——KDE Plasma、GNOME、Cinnamon Desktop、MATE、Unity Desktop(与 Unity 3D 游戏引擎无关)和 Xfce。这个列表并不详尽,带有主观性,且非常有限。它们在易用性、可配置性、内存和 CPU 使用量以及许多其他方面都有所不同。

发行版的数量令人震惊——跟踪 Linux 发行版的主流网站 (distrowatch.com/dwres.php?resource=popularity) 在撰写本文时列出了 265 个不同的 Linux 发行版。如此庞大的数量使得本书只能选择其中的三个进行讲解。在大多数情况下,选择哪一个发行版对你自己并无太大区别,除非你选择的是商业版本,可能在许可和订阅上有所不同。每当选择的发行版产生影响,特别是技术方面的影响时,我们都会指出来。

选择发行版不仅仅是一个实用的选择。Linux 社区深受理想驱动。对于一些人来说,这些理想是他们构建生活的基石。关于哪款文本编辑器更好,基于其用户界面、发布的许可证或源代码质量,已经发生过无数次激烈的争论。对于选择运行 WWW 服务器的软件,或者如何接受新贡献,同样也会表现出同样的情绪。这不可避免地会影响 Linux 发行版的安装方式、配置和维护工具的选择,以及开箱即用的安装软件的种类。

尽管如此,我们必须提到,尽管他们有着强烈的信念,开源社区,包括 Linux 社区,实际上是非常友好的。在大多数情况下,你能够在在线论坛上找到帮助或建议,并且你很有可能会亲自遇到他们。

在选择发行版时,你需要注意以下几个因素:

  • 你希望运行的软件是否支持该发行版?一些商业软件限制了发布软件包的发行版数量。尽管有可能在不受支持的 Linux 版本上运行这些软件,但这可能会比较棘手,并容易出现中断。

  • 你打算运行的软件版本是否可用?有时,所选的发行版更新你所需的软件包的频率可能不够高。在云计算的世界里,几个月前的软件可能已经过时,缺乏重要的功能或安全更新。

  • 这个发行版的许可是怎样的?它是免费使用的,还是需要订阅计划?

  • 你的支持选项是什么?对于由社区驱动的免费发行版,你的选择仅限于在线和本地的友好 Linux 专家。对于商业发行版,你可以支付费用获得各种支持服务。根据你的需求和预算,你可以找到一种支持选项组合,既能满足你的需求,也能符合你的财务状况。

  • 你对编辑配置文件和运行长而复杂的命令的舒适度如何?一些发行版提供工具(包括命令行和图形界面工具),使配置任务更容易且减少出错。然而,这些工具大多是特定于发行版的,你在其他地方是找不到的。

  • 云相关工具在某个发行版上的支持情况如何?这可能包括安装的简便性、软件本身的更新程度,或者配置使用所需的步骤数。

  • 这个发行版在你选择的云服务上支持得怎么样?这意味着有多少云服务提供商提供带有这个发行版的虚拟机。要获得这个发行版的容器镜像来运行你的软件有多容易?我们怀疑,构建这个发行版并在其上部署的难易程度如何?

  • 它在互联网上的文档情况如何?这不仅包括发行版维护者编写的文档,还包括用户编写的各种博客文章和文档(主要是教程和所谓的操作指南)。

到目前为止,你已经了解了什么是 Linux 发行版,它们如何区分,以及你可以使用哪些标准来选择作为你将管理的系统核心的发行版。

在接下来的章节中,我们将深入探讨每个发行版,以便更好地了解最流行的几个,给你一个初步的了解,展示每个发行版是如何运作的,以及你可以期待什么。

介绍各个发行版

在这一段略显冗长但精炼的 Linux 操作系统历史之后,终于可以开始探索我们在本书中选定的几个发行版了。在这一节中,我们将涵盖我们刚刚列出的因素,因为我们认为它们在做决定时非常重要。不过,请记住,尽管我们力求为你呈现客观的事实和评估,但我们无法避免自己的主观看法。在你做出选择之前,务必自己进行评估,因为很有可能你将长期使用这个发行版。

需要注意的是,我们不会全面覆盖所有发行版。我们将尽力为你提供一个基础,接下来你需要通过研究来进行构建。

此外,在学习过程中,不要害怕从一个发行版跳到另一个发行版。只有通过实际经验,你才能真正了解哪个发行版最适合你的需求。

Debian

Debian (www.debian.org/) 是最古老的活跃 Linux 发行版之一。它的开发由社区支持的 Debian 项目主导。它以两件事著称——该发行版提供的包数量庞大以及稳定版发布的缓慢。后者在近年来有所改善,稳定版现在每两年发布一次。软件通过名为包的归档进行交付。Debian 包的文件名扩展名为 .deb,通常被称为deb。这些包保存在在线仓库中,仓库又被细分为多个池。仓库提供了大约 60,000 个包,包含最新稳定版中的软件。

Debian 始终提供三个版本(所谓的分支)——稳定、测试和不稳定。每个发布版本的名称都来源于《玩具总动员》电影系列中的角色。最新的稳定版——版本 11,名为 Bullseye。

不稳定分支是面向开发者、喜欢挑战极限的人或那些对最新软件的需求高于稳定性需求的用户的滚动分支。软件会在经过最小测试后进入不稳定分支。

测试分支是进行测试的地方,顾名思义,这里进行大量的测试,感谢最终用户的贡献。软件包从不稳定分支流入这里。这里的软件比稳定分支中的软件更新,但不如不稳定分支中的软件新。在新稳定版发布前的几个月,测试分支会被冻结。冻结意味着不再接受新的软件,仅允许已经接受的包的新版本,前提是它们修复了错误。

几个月后,测试分支会变成稳定分支。此时,软件仅会更新安全修复。

这个发行版适用于许多硬件平台——Intel、ARM、PowerPC 等等。除了非官方的移植版本,还有许多硬件平台可以安装它。

Debian 被视为最稳定的发行版,通常作为各种计算集群的基础平台使用,因此它通常被安装在数据中心机架中的裸金属服务器上,旨在长期持续使用。

根据 W3Techs (w3techs.com/technologies/details/os-linux) 的数据,Debian 占据了互联网上所有服务器的 16%。它的衍生版 Ubuntu 运行了其中的 33%。两者合计占所有服务器的 49%。这使得与 Debian 相关的管理技能非常具有市场需求。

Ubuntu Linux

Ubuntu Linux 发行版(ubuntu.com/)因使 Linux 在个人计算机上变得流行而广受赞誉,的确如此。由 Canonical 资助,其使命是使 Linux 对大多数人来说更易使用。它是第一个(如果不是第一个的话)通过分发非自由和非开源的二进制驱动程序和库,简化桌面使用并使其更加舒适的 Linux 版本之一。

著名的是,Mark Shuttleworth(Canonical 和 Ubuntu 的创始人)为 Ubuntu 发行版开设的第一个错误报告是:“微软拥有市场份额”。

该发行版本身基于 Debian Linux,最初的主要目标之一是实现完全的二进制兼容性。随着开发的进展,这一目标的相对重要性有所下降。

该发行版由社区和 Canonical 开发。公司的主要收入来源是与 Ubuntu Linux 相关的高级服务——支持、培训和咨询。

由于 Debian Linux 和 Ubuntu Linux 之间非常紧密的关系,许多开发人员和维护人员在一个发行版中的角色也在另一个发行版中担任相同职务。这导致了大量的软件同时为两个发行版打包。

Ubuntu 有三种主要版本——桌面版、服务器版和核心版(用于物联网)。桌面版和服务器版在默认配置的服务设置上可能略有不同,而核心版则差异较大。

软件以 .deb 包的形式分发,与 Debian 一样,源代码实际上是从 Debian 不稳定分支导入的。然而,这并不意味着你可以在 Ubuntu 上安装 Debian 包,反之亦然,因为它们不一定是二进制兼容的。应该可以重新构建并安装你自己的版本。

每个版本有四个软件包仓库——由 Canonical 官方支持的自由和非自由软件分别称为 mainrestricted。由社区提供和维护的自由和非自由软件分别称为 universemultiverse

重要提示

一条建议——在主要版本之间进行系统升级的普遍做法是等待第一个子版本发布。因此,如果当前安装的版本是 2.5,而新版本 3.0 已发布,建议等到 3.1 或甚至 3.2 发布后再进行升级。这适用于我们在这里列出的所有发行版。

长期支持LTS)版本的支持期限为五年。每两年发布一个新的 LTS 版本。也可以协商延长支持期。这为计划重大升级提供了一个非常好的时间表。每六个月发布一个新的 Ubuntu 版本。

Ubuntu Linux 在教育和政府项目中得到了广泛采用。著名的是慕尼黑市,在 2004 到 2013 年间,将超过 14,000 台市政桌面电脑迁移到了一个带有 KDE 桌面环境的 Ubuntu 版本。虽然这次迁移在政治上遭遇了干扰——其他操作系统供应商强烈反对这一迁移——但从技术角度来看,它被视为一次成功。

Ubuntu 是个人计算机的首选 Linux。Canonical 与硬件厂商,特别是联想和戴尔,紧密合作,最近也与 HP 合作,确保发行版与计算机之间的完全兼容性。戴尔销售的旗舰笔记本电脑预装了 Ubuntu。

有多个来源将 Ubuntu Linux 列为安装在服务器和个人计算机上的最多的 Linux 发行版。实际数字只能估算,因为 Ubuntu 不要求任何订阅或注册。

作为 Ubuntu Linux 流行的副产品,软件供应商通常会提供其软件的.deb包版本,尤其是对于桌面软件而言,这一点尤为明显。

基于 Ubuntu 的非官方版本、克隆或修改版的发行版数量惊人。

Ubuntu 拥有一个非常活跃的社区,既有组织化的也有非组织化的。你很容易就能找到你所在城市的用户群体。这也直接影响了互联网上教程和文档的数量。

Ubuntu Linux,尤其是在支持计划下,作为许多云计算基础设施部署的基础。许多电信、银行和保险公司已选择 Ubuntu Server 作为他们的基础平台。

Red Hat 企业 Linux(RHEL)

RHEL (www.redhat.com/en/technologies/linux-platforms/enterprise-linux) 是 Red Hat Linux 的精神继承者,由 Red Hat Inc. (www.redhat.com/)开发和维护。其主要目标是商业实体市场。对于开发或最多支持 16 台服务器的生产环境(截至写作时),可以免费使用 RHEL。然而,这个发行版的主要优势在于大量的文章资源,有助于解决问题,并且可以获得支持工程师的帮助,尽管后者只能通过付费支持计划获得。

RHEL 被认为是一个非常稳定和可靠的发行版。它是银行、保险公司和金融市场的主要选择之一。虽然缺乏许多流行的桌面软件包,但在服务器方面,尤其是在运行其他商业应用程序的操作系统方面,它是一等公民。

该软件以在线仓库包的形式分发,文件以.rpm结尾,因此得名RPMs。管理这些包的主要工具是 RPM,还有更复杂的工具——yum,最近的继任者dnf也可用。

作为一家基于开源的公司,Red Hat 提供了其发行版的源代码。这促成了一个著名的免费的开源 RHEL 克隆版本——CentOS的诞生。直到不久前,它一直是那些想要使用 RHEL,但又不愿意或无法支付订阅费用的人的热门选择。在 2014 年,CentOS 加入了 Red Hat 公司,而在 2020 年,Red Hat 宣布 CentOS 的版本发布将不再提供;将只会有所谓的滚动更新版本,它会不断更新软件包,并且不再与 RHEL 的发布版本一致。这一决定引发了 CentOS 用户的强烈反应。CentOS 的原始创始人 Gregory Kurtzer 创建了另一个 RHEL 克隆版本,名为Rocky Linux。它的主要目标与原始 CentOS 相同——提供一个免费的、开源的、由社区驱动的发行版,并且完全与 RHEL 二进制兼容。

RHEL 发行版每隔几年发布一个稳定版本,并且为这些版本提供 10 年的支持,从第 5 版开始。然而,完整的支持只会提供几年。其余时间,Red Hat 仅为系统提供安全修复和关键更新,不会引入新的软件包版本。然而,这种生命周期是大规模安装或关键任务系统用户所青睐的。

与 Ubuntu 类似,可以协商延长支持时间。

Red Hat 公司与开源社区的关系较为复杂。虽然该公司大部分时间是公平竞争,但也曾做出一些社区不满的决定。最近,Red Hat 做出了将 CentOS 发布模型更改为滚动发布的决定,这一决定引发了广泛争议 (lists.centos.org/pipermail/centos-announce/2020-December/048208.xhtml)。

与 Ubuntu 一样,RHEL 是商业支持云基础设施部署的首选基础。

Fedora Linux

Fedora (fedoraproject.org/wiki/Fedora_Project_Wiki)是与 Red Hat 公司相关的一个发行版。虽然超过 50%的开发者和维护者是与 Red Hat 无关的社区成员,但该公司在开发过程中拥有完全的管理权。它是 RHEL 的上游,这意味着它是实际 RHEL 的真正开发前端。这并不意味着 Fedora 中的所有内容都会被包含在 RHEL 的发布版本中。然而,紧跟 Fedora 的步伐将能够洞察 RHEL 发行版的当前方向。

与 RHEL 不同,Fedora 是 RHEL 的基础,Fedora 每六个月发布一次新版本。它使用与 RHEL 相同的软件包类型,RPM。

Fedora 被认为是一个节奏较快的发行版。它迅速采用最新和最前沿的包版本。

CentOS

CentOS (centos.org) 曾是 RHEL 的首选免费版本。该名称是 社区企业操作系统(Community Enterprise Operating System)的缩写。其主要目标是与 RHEL 完全二进制兼容,并遵循相同的版本和编号方案。2014 年,CentOS 加入了 Red Hat,但承诺该发行版将保持独立于该公司,同时享受开发和测试资源的支持。不幸的是,2020 年,Red Hat 宣布 CentOS 8 将是最后一个有编号的版本,从那时起,CentOS Stream 将成为唯一的变种。CentOS Stream 是一个中间版本,这意味着它位于前沿且快速发展的 Fedora 和稳定且生产就绪的 RHEL 之间。CentOS Stream 与 CentOS 的区别在于,Stream 是一个开发版,而 CentOS 只是实际最终产品 RHEL 的重建和打包镜像。

在使用 RHEL 时获得的所有知识、技能和经验,100% 可以应用于 CentOS。根据 W3Techs 的数据(w3techs.com/technologies/details/os-linux),由于 CentOS 是服务器上第三大最常部署的 Linux 发行版,这些技能在市场上非常有价值。

Rocky Linux

为了应对 CentOS 发行版的现状,其创始人宣布创建了 Rocky Linux (rockylinux.org/)。其目标与原始 CentOS 相同。发行方案和编号遵循 RHEL。宣布之后不久,Rocky Linux 的 GitHub 仓库成为了热门趋势(web.archive.org/web/20201212220049/https://github.com/trending)。Rocky Linux 与 CentOS 100% 二进制兼容。该项目已发布一套工具,可以轻松地将系统从 CentOS 迁移到 Rocky Linux,而无需重新安装系统。

该发行版相当年轻,成立于 2020 年,其受欢迎程度尚待观察。它在社区内引起了许多关注,似乎有一批 CentOS 用户将 Rocky Linux 作为他们的首选。

Rocky Linux 项目对开源世界的一个非常重要的贡献是其构建系统。它确保即使 Rocky Linux 停止运营,社区也能够轻松启动一个新的 RHEL 克隆版本。

所有适用于 RHEL 和 CentOS 的技能、知识和文章,100% 可以应用于 Rocky Linux。在 Rocky Linux 上,所有在 RHEL 和 CentOS 上运行的软件也应无需任何修改地运行。

Alpine

Alpine Linux (alpinelinux.org/) 是一个有趣的发行版。其主要的编程库和大部分基本命令行工具并非来自 GNU 项目。另外,目前在大多数发行版中使用的 systemd 服务管理系统在 Alpine 中并不常见。这使得其他主流发行版的一些管理技能在 Alpine 中不适用。Alpine 的优势在于其体积(相对较小)、注重安全的理念,以及在现有 Linux 发行版中最快的启动时间。正是这些特点,尤其是启动时间(毫无疑问是最重要的),使得它成为容器的最受欢迎选择。如果你运行容器化的软件或构建自己的容器镜像,很可能它就是基于 Alpine Linux。

Alpine 起源于 LEAF (Linux 嵌入式设备框架;参见:bering-uclibc.zetam.org/wiki/Main_Page) 项目——一个可以装入单张软盘的 Linux 发行版。LEAF 目前是嵌入式市场、路由器和防火墙的热门选择。Alpine 是一个更大的发行版,但必须做出这样的牺牲,因为开发者希望包含一些有用但相对较大的软件包。

包管理器叫做 apk。构建系统借鉴了另一个名为 Gentoo Linux 的发行版。由于 Gentoo 在安装软件时会进行构建,portage 显然包含了大量关于构建软件的逻辑,这些逻辑作为操作系统的一部分使用。

Alpine 可以完全从 RAM 中运行。它甚至有一个特殊的机制,可以让你最初只从启动设备加载一些必要的包,并且这可以通过 Alpine 的 本地备份 工具 (LBU) 实现。

如前所述,这是容器镜像首选的发行版。你不太可能看到它在大型服务器安装上运行,或者根本没有。当我们进入云计算世界时,你很可能会看到很多 Alpine Linux。

话虽如此,这些发行版中的每一个都有一个适用于云计算的变体,作为容器基础镜像——这是一种以真正的云计算方式运行你的软件的方法。

在这一章中,你了解了流行的 Linux 发行版的基础知识,以及它们之间的区别。你现在应该对你可以选择的发行版以及你将需要面对的后果(好与坏)有了一些了解。为了更好地让你了解如何与一些精选的 Linux 发行版互动,我们将研究如何在第二章中通过键盘与系统进行交互。

概述

本章中的简短清单只是现有 Linux 发行版的一小部分。这个清单主要基于技能的流行度和市场性,以及我们多年来积累的经验和知识。它们绝不是你唯一的选择,也不是最好的选择。

我们尝试指出主要优点所在,并阐明用户与各个发行版之间的关系。

我们可能无法回答你所有的问题。我们列表中的每个 Linux 发行版都有自己的书籍,而且在博客、维基和 YouTube 教程中还有更多的知识。

在下一章,我们将深入探讨命令行的神奇世界。

第二章:命令行基础知识

在本章中,我们将直接深入学习 Linux 命令行。我们将解释它的强大之处,以及如何对每个系统管理员和 DevOps 人员都至关重要。更重要的是,我们将开始教授你最有用的命令及其有效使用方式。在此过程中,我们还将添加其他核心 Linux 概念,因为这些概念对理解本章是必需的。

在本章中,我们将涵盖以下主要内容:

  • 什么是命令行及其工作原理

  • 为什么熟练掌握命令行如此重要

  • Linux 系统管理的基本命令

不可能在单一章节中介绍所有命令和工具。接下来我们选择了您需要了解的最基本工具。管理 Linux 系统是一个独立的书籍主题。恰好 Packt 出版社确实有几本相关出版物。

技术要求

强烈建议准备好并安装 Linux 系统以供使用。我们建议使用虚拟机或可以安全地从头开始重新安装的笔记本电脑。这将使您能够按照书中的示例并执行我们给出的任何练习。

我们不会涵盖安装过程。每个发行版可能都有自己的安装程序,可以是图形化的或文本的(取决于发行版及其选择的变体)。您需要记下或记住您的用户名(方便地称为用户名或登录名)和密码。如果您有物理访问权限但不知道登录名或密码,或两者都不知道,有方法可以进入系统,但这些超出了本书的范围。

我们书中的主要发行版是 Debian。然而,只要不是 Alpine,你在上一章中涵盖的任何主要发行版都应该可以使用。

Linux 命令行 - shell

Linux 系统管理员的自然环境是命令行。但是,你永远不会听到有人这样称呼它。正确的名称是shell,从现在开始我们在书中就这样称呼它。

Shell 是一个接受用户输入(主要是键盘输入,但也有其他方式,甚至可以使用鼠标指针)的程序,解释这些输入,如果是有效的命令则执行它,并向用户提供结果或错误信息,如果命令不能正确执行。

有几种访问 shell 的方法:

  • 登录到终端(*图 2.1** 中的截图)

注意

你还会看到术语 console。终端和控制台是有区别的。控制台 是一个物理设备,让用户与计算机交互。它是物理输入(现在通常是键盘)和输出(现在通常是显示器,但最初输出是打印出来的)。终端 是一个控制台模拟器,一个程序,让用户执行相同的任务。

  • 在图形界面中打开一个终端窗口(如果你有的话)

  • 通过安全连接远程登录到另一台设备(手机、平板或你的电脑)

Shell 是一个非常强大的环境。一开始通过输入命令做所有事情可能看起来有些繁琐,但很快你会发现,任务越复杂,通过 shell 完成起来比图形界面更容易。每个有经验的 Linux 系统管理员都知道如何在 shell 中完成任务,以及如何通过它来管理系统,我敢打赌他们更倾向于使用 shell 而非任何 GUI。

了解你的 shell

Shell 是一个程序,因此并不是只有一个 shell。相反,有许多流行程度不同的 shell,它们各自呈现出不同的处理方式。

到目前为止,大多数 Linux 发行版中最流行且默认的 shell 是 BashBourne Again Shell)。还有其他一些你可能需要了解的 shell:

  • sh:原始的 Steve Bourne shell。它是最初的 shell,也是我们所知道的第一个编写出来的 shell。虽然它缺少许多用户从其他更现代的 shell 中获得的交互功能,sh 以其快速的脚本执行速度和小巧的体积而闻名。

  • ksh:作为 Bourne shell 的演变版本开发而来,它是其前身的超集。因此,所有为 sh 编写的脚本都能在 ksh 中运行。

  • csh:C shell。这个名字来自于它的语法,它与 C 编程语言的语法非常相似。

  • zsh:Z shell。对于 macOS 用户来说,它应该非常熟悉,因为它是该操作系统的默认 shell。它是一个完全现代化的 shell,提供了许多你所期待的功能:命令历史、算术运算、命令补全等。

我们不会用太多的 shell 变种和历史来困扰你。如果你对 Bash 的起源感兴趣,可以参考这篇维基百科文章:en.wikipedia.org/wiki/Bash_(Unix_shell)

在本书中,我们使用的是 Bash。如前所述,它是大多数 Linux 发行版的默认 shell,提供 sh 兼容模式,拥有现代 shell 所有的特性,而且关于它的书籍、文章、教程等资料极其丰富。

我们将要执行的第一个任务是登录到 Shell 中。根据你选择的安装方式,你可能需要启动本地虚拟机、物理机器或基于云的虚拟专用服务器VPS)。如果你选择了没有图形界面的服务器安装,你应该看到类似以下的屏幕截图:

图 2.1 – 登录界面

图 2.1 – 登录界面

你会看到一个登录界面,你可以在此输入用户名和密码。成功登录后,你会看到一个命令提示符,表示你已经成功启动了 Shell。

提示符的外观是可配置的,并且可能根据你选择的发行版而有所不同。不过,有一件事会保持不变,我们建议你不要更改它。在 Linux 世界中,有两种类型的用户:普通用户超级用户。普通用户的登录名可以是任何符合 Linux 用户命名规范的名称,而超级用户的登录名是root,下面是其密码。

超级用户账户之所以这么命名,是有原因的。在大多数 Linux 发行版中,它是无所不能的用户。登录为root后,你可以做任何事情,甚至删除所有文件,包括操作系统本身。

为了帮助你区分普通用户和root用户,提示符会给你提供一个线索。当以root用户登录时,提示符会以#符号结尾。当以普通用户登录时,提示符则会以$符号结尾。

顺便提一下,#(井号)符号也是所谓的注释符号。如果你从互联网复制命令或输入命令时,若它以#$开头,这就是提示你该命令应该由哪种类型的用户来执行。你应该省略这个符号,尤其是#符号会阻止命令的执行。

除了结束提示符的符号,许多发行版会在其前面加上用户名,确保你知道自己是哪位用户。如果在一个名为myhome的 Linux 系统上有一个管理员用户,Debian 11 的默认提示符看起来像这样:

$admin@myhome:~$

对于root用户,它看起来像这样:

root@myhome:~#

需要说明的是,检查身份的方式有很多种,但我们将留待在第四章中讨论。

我呼唤你

到目前为止,一切都很好。你已经登录,知道自己是谁,可以输入命令并进行阅读。但实际操作程序是如何进行的呢?

在 Linux 术语中,运行一个程序就是执行它或调用它。实际上,调用这个词通常用于指代系统命令或 Shell 内建命令,而执行则用于谈论那些不属于发行版的一部分的程序——即所谓的第三方程序或二进制文件。

在我告诉你如何执行程序或调用命令之前,我得先解释一下文件系统结构和一个非常重要的系统变量,叫做 PATH

文件系统结构

由于这可能是你第一次接触,我们将稍微退后一步,解释一下文件系统的结构(换句话说,就是典型 Linux 系统中文件夹是如何组织的)。

Linux 遵循 Unix 的哲学思想,即一切皆文件。(虽然有例外,但不多。)其结果是操作系统几乎每个方面都以文件或目录的形式体现。内存状态、进程(正在运行的程序)的状态、日志、二进制文件和设备驱动程序都在这个结构中。也就是说,几乎你 Linux 系统的每个方面都可以通过普通的文本编辑工具进行编辑或检查。

在目录树中,它的结构总是从一个 / 文件夹开始,这个文件夹被称为 根目录。每个驱动器、网络共享和系统目录都在一个从根目录开始的层次结构中。

将网络共享或本地驱动器提供给系统或用户的过程被称为 /home/ 目录。你在浏览文件系统结构时是无法看出它的。唯一能看出来的方法是通过以下命令检查已挂载的驱动器和分区:

$ mount

或者

$ df

我们将在第三章中详细讲解它们,所以现在你只需要知道它们的存在即可。

最上层目录的名称是 /。我们已经讲过了。嵌套在另一个文件夹中的文件夹之间的分隔符也是 /。所以 /usr/bin 表示在 usr 目录中存在一个 bin 目录,而 usr 目录位于 / 目录中。很简单。

有一个非常方便的命令,可以让我们查看目录结构,叫做 tree。这个命令可能在你的系统中没有安装。如果没有,别担心;它并不是那么重要,你可以通过我们的讲解来了解。在第三章中,当我们介绍如何安装软件包时,你可以回过头来再试试这个命令。默认情况下,tree 命令会把整个目录结构显示出来,可能会让屏幕信息满得让人难以阅读和跟随。不过,有一个选项可以让我们限制查看的深度:

admin@myhome:~$ tree -L 1 /
/
|-- bin -> usr/bin
|-- boot
|-- dev
|-- etc
|-- home
|-- lib -> usr/lib
|-- lib32 -> usr/lib32
|-- lib64 -> usr/lib64
|-- libx32 -> usr/libx32
|-- lost+found
|-- media
|-- mnt
|-- opt
|-- proc
|-- root
|-- run
|-- sbin -> usr/sbin
|-- srv
|-- sys
|-- tmp
|-- usr
`-- var
22 directories, 0 files

这里有几个重要的概念需要讲解;不过我们暂时不会讲解所有目录。每当某个文件夹变得重要时,从第三章开始,我们会简要地提及它。

首先,调用 tree 命令。你看到了我的提示符,它告诉我当前以 admin 用户身份在名为 myhome 的系统上运行,并且我不是 root 用户(提示符末尾的美元符号)。如果你想运行 tree 命令,你可以跳过提示符。接下来是实际的命令调用:tree 加上 -L 选项和数字 1;这会指示程序只打印一个深度级别。换句话说,它不会进一步进入目录。最后,/ 符号告诉程序从文件系统的最开始——root 文件夹开始打印。

接下来,你会注意到某些行中有一个神秘的箭头,指向一个名字。这个箭头表示一个快捷方式。有两种类型的快捷方式,硬链接和符号链接。目录只能拥有符号链接。在前面的输出中,/bin 目录是指向 /usr/bin 目录的链接。从实际使用的角度来看,它们可以当作一个目录来对待。存在这个链接有技术上的原因和历史背景。过去,位于 /bin/sbin 目录中的工具用于挂载 /usr 分区,然后允许访问 /usr/bin/usr/sbin。如今,这一任务由启动过程中的其他工具更早处理,因此不再需要此链接。该结构的保留是为了与可能需要同时存在 /bin/sbin 目录的工具的向后兼容性。更多详情请参考 refspecs.linuxfoundation.org/FHS_3.0/fhs/index.xhtmlwww.linuxfoundation.org/blog/blog/classic-sysadmin-the-linux-filesystem-explained

既然我们已经提到了 /bin/sbin 目录,那么我们来解释一下它们之间的区别。/usr/bin 目录包含 /usr/sbin 目录,后者包含所谓的 root 用户。它还包含系统进程的二进制文件(称为 守护进程)——这些程序在后台运行并为正在运行的系统执行重要工作。

/root 目录是超级用户的主目录。所有与超级用户相关的配置文件都存放在这里。

有趣的是 /home 目录。这里存放着所有用户的主目录。当我为我的家用机器创建 admin 用户时,它被放置在 /home/admin 文件夹中。

对我们来说,目前重要的是 /etc/ 目录。它包含整个系统的所有配置文件:在线软件包仓库的源,默认的 shell 配置,系统名称,启动时会启动的进程,系统用户及其密码,以及基于时间的命令。在一个全新安装的 Debian 11 系统中,/etc/ 目录包含大约 150 个子目录和文件,每个子目录可能还包含更多的文件夹。

/tmp文件夹包含临时文件。它们只在系统启动时存在,一旦关闭或重启系统,它们会被删除。这些文件的性质通常非常不稳定;它们可能会迅速生成并消失,或者频繁被修改。这个目录有时仅存在于计算机的内存中。它是你系统中最快的存储设备,并且在重启或关闭电源时会自动清除。

如前所述,附加驱动器会在该结构下挂载。我们可能会为主目录分配一个单独的硬盘。整个/home文件夹可能会存放在该硬盘上,甚至存放在一个网络硬盘阵列中。如前所述,/tmp目录通常挂载在内存中。有时,/var目录(包含系统中经常变化的内容但不应被清除的文件,如日志)会挂载在单独的硬盘上。这样做的原因之一是,/var的内容,尤其是/var/log(系统日志所在的目录),可能会增长得非常快,占用所有可用空间,导致无法访问系统,或者变得非常困难。

最后,有两个重要且特殊的目录,遍布你访问的每个地方:

  • .:一个点表示文件夹,你将停留在当前位置。不过,它很有用,正如你将在第三章中看到的那样。

  • ..:两个点表示上级目录——..文件夹,你将进入比起始位置高一级的目录。请注意,对于/目录来说,...表示相同的目录:/。你无法再向上移动超出根目录。

运行程序。

现在我们对文件夹层次结构有了一些了解,接下来简要讲一下执行程序的过程。

在 Shell 中执行程序有三种基本方法:

  • PATH变量(在第三章中解释)。

  • /。当使用绝对路径时,必须列出所有指向程序的目录,包括前导的/。执行示例可能如下所示:

/opt/somecompany/importantdir/bin/program
  • ./。这个快捷方式表示./myprogram./bin/myprogram。后者意味着:在当前目录中的bin目录里启动一个名为myprogram的程序。

  • 要在系统中的其他目录调用程序时,我们需要使用两个点,表示父文件夹。假设你已经登录到你的主目录/home/admin,并想执行位于/opt/some/program/bin中的程序;你需要输入../../opt/some/program/bin/myprogram。这两个点和斜杠表示向上移动。

如果这看起来很神秘,那是因为它确实有点复杂。幸运的是,随着本书的推进,一切都会逐渐变得清晰。

用来教你所有命令的命令。

你应该养成遇到问题或有疑问时上网查找的习惯。大多数问题已经在网上得到了解决或解释。然而,有一个命令能拯救你的生命——或者至少能节省很多时间。你应该养成经常使用它的习惯——即使你确信自己知道正确的语法,你可能还是会发现一种更好的完成任务的方法。这个命令如下:

$ man

man 命令是 manual(手册)的简写,它正是它所说的:它是你想了解的任何命令的手册。要了解更多关于 man 命令的内容,只需调用以下命令:

$ man man

你看到的输出应该类似如下:

MAN(1)                                                                 
                                   Manual pager utils                                     
                                                               MAN(1)
NAME
       man - an interface to the system reference manuals
SYNOPSIS
       man [man options] [[section] page ...] ...
       man -k [apropos options] regexp ...
       man -K [man options] [section] term ...
       man -f [whatis options] page ...
       man -l [man options] file ...
       man -w|-W [man options] page ...
DESCRIPTION
       man is the system's manual pager.  Each page argument given to man is normally the name of a program, utility or function.  The manual page associated with each of these arguments is then found and displayed.  A section, if
       provided, will direct man to look only in that section of the manual.  The default action is to search in all of the available sections following a pre-defined order (see DEFAULTS), and to show only the  first  page  found,
       even if page exists in several sections.

我已简化内容以便简洁。编写良好的 man 页面将包含多个部分:

  • name:这里列出了命令的名称。如果命令有多个名称,它们都会列出。

  • synopsis:这将列出调用命令的可能方式。

  • description:这是命令的目的。

  • examples:这将展示几个命令调用的示例,以使语法更加清晰并提供一些思路。

  • options:这将显示所有可用选项及其含义。

  • getting help:这是获取简化版命令摘要的方式。

  • files:如果命令有配置文件或使用文件,并且这些文件在文件系统中已知存在,它们将被列在此处(对于 man,我列出了 /etc/manpath.config/usr/share/man)。

  • bugs:这是查找 bug 和报告新 bug 的地方。

  • history:这将显示程序的当前及所有历史作者。

  • see also:这些是与命令有某种关联的程序(对于 manapropos(1)groff(1)less(1)manpath(1)nroff(1)troff(1)whatis(1)zsoelim(1)manpath(5)man(7)catman(8)mandb(8))。

许多命令会包含大量额外的部分,这些部分是特定于该程序的。

man 页面包含大量知识,有时名称可能会重复。这时,括号中的神秘数字就派上用场了。man 页面分为多个部分。引用 man 页面中的 man

  1. 可执行程序或 shell 命令

  2. 系统调用(内核提供的函数)

  3. 库调用(程序库中的函数)

  4. 特殊文件(通常位于 /dev

  5. 文件格式和约定,例如,/etc/passwd

  6. 游戏

  7. 其他(包括宏包和约定),例如,man(7)groff(7)

  8. 系统管理命令(通常仅限root使用)

  9. 内核例程 [非标准]

举个例子,printf。有几种东西被称为 printf,其中之一是 C 语言的库函数。它的 man 页面位于第 3 节。

要阅读这个库函数,你必须告诉 man 去查找第 3 节:

admin@myhome:~$ man 3 printf
PRINTF(3)                                                              
                                Linux Programmer's Manual                                
                                                             PRINTF(3)
NAME
       printf, fprintf, dprintf, sprintf, snprintf, vprintf, vfprintf, vdprintf, vsprintf, vsnprintf - formatted output conversion
If you don't, what you'll get is a shell function for printing—totally useless in C programming:
admin@myhome:~$ man printf
PRINTF(1)                                                              
                                      User Commands                                        
                                                           PRINTF(1)
NAME
       printf - format and print data
SYNOPSIS
       printf FORMAT [ARGUMENT]...
       printf OPTION

大多数命令和 shell 程序都有一个简短的概要,叫做help。通常,可以通过运行带有-h或--help选项的二进制文件来调用它:

admin@myhome:/$ man --help
Usage: man [OPTION...] [SECTION] PAGE...
  -C, --config-file=FILE     use this user configuration file
  -d, --debug                emit debugging messages
  -D, --default              reset all options to their default values
      --warnings[=WARNINGS]  enable warnings from groff

我已经简化了输出,但你应该明白我的意思。

注意

短选项前面加一个短横线,而长选项前面加两个。--help不是一个长横线,而是两个标准的短横线。

man–help命令应该成为你的朋友,即使在你搜索在线资料之前。很多问题可以通过查看help输出快速得到答案。即使你是一个经验丰富的管理员,也可以忘记命令语法。网络上有一个无尽的 Linux 指南来源,叫做The Linux Documentation Projecttldp.org。将它加入书签吧。

了解你的环境

系统的行为由几个因素控制。其中之一就是一组被称为环境变量的变量。它们设置了系统与用户交互时使用的语言,列出文件时的排序方式,shell 寻找可执行文件的路径等许多其他内容。具体的环境变量集取决于所使用的发行版。

可以使用env命令打印出 shell 设置的所有环境变量的完整列表:

admin@myhome:/$ env
SHELL=/bin/Bash
PWD=/
LOGNAME=admin
XDG_SESSION_TYPE=tty
MOTD_SHOWN=pam
HOME=/home/admin
LANG=C.UTF-8
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

如果你知道要检查的变量,可以使用echo命令:

admin@myhome:/$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

请注意,当你使用一个变量时,你必须在变量名前加上美元符号,因此是$PATH

我当前在 PATH 中的哪个位置?

既然我们提到过PATH,那么我们简要地谈一下它。Path可以有两种含义:

  • 系统中的一个指向某个对象的地方:一个二进制文件,一个文件,或者一个设备

  • 一个列出 shell 在执行程序时会查找的地方的环境变量

你已经对第一种路径有了些了解。我们解释了绝对路径和相对路径。有一个命令可以让你在目录间移动,它叫做cd(如果不带参数,cd会将你带到家目录。如果你带上参数,它会将你移动到指定的文件夹,前提是文件夹存在,你指定的路径正确,并且你有权限访问该文件夹。我们来看看几个例子:

  • 检查当前所在的目录:
admin@myhome:~# pwd
/home/admin
  • 切换目录到/home/admin/documents/current
admin@myhome:~# cd documents/current
admin@myhome:~/documents/current#
  • 从当前目录上级跳转:
admin@myhome:~/documents/current# cd ..
admin@myhome:~/documents#
  • 切换到用户的家目录:
admin@myhome:~/documents# cd
admin@myhome:~# pwd
/home/admin

了解你的权限

Linux 中最基本的安全机制是基于为一组实体定义权限组合。权限如下:

  • read

  • write

  • execute(在谈到目录时,表示读取内容)

实体如下:

  • 文件或目录的拥有者

  • 拥有文件或目录的组

  • 所有其他用户和组

这是一个粗略的安全系统。它足以用于小型服务器和桌面使用,但对于更复杂的设置,有时它的限制太大。还有其他附加的系统,例如访问控制列表ACLs)、AppArmor、SELinux 等。本书不打算涵盖这些内容。

使用之前提到的系统,我们仍然可以在系统安全性方面取得相当大的进展。

这些权限和所有权是如何工作的?我们使用ls命令(列出文件和目录):

admin@myhome:/$ ls -ahl
total 36K
drwxr-xr-x 3 admin admin 4.0K Aug 20 20:21 .
drwxr-xr-x 3 root  root  4.0K Aug 20 14:35 ..
-rw------- 1 admin admin  650 Aug 20 20:21 .Bash_history
-rw-r--r-- 1 admin admin  220 Aug  4  2021 .Bash_logout
-rw-r--r-- 1 admin admin 3.5K Aug 20 14:47 .Bashrc
-rw-r--r-- 1 admin admin    0 Aug 20 14:40 .cloud-locale-test.skip
-rw------- 1 admin admin   28 Aug 20 16:39 .lesshst
-rw-r--r-- 1 admin admin  807 Aug  4  2021 .profile
drwx------ 2 admin admin 4.0K Aug 20 14:35 .ssh
-rw------- 1 admin admin 1.5K Aug 20 14:47 .viminfo

输出呈现为九列。

第一列简洁地呈现条目的类型和权限,但让我们跳到第三列和第四列。这些列告诉我们谁是文件的所有者,以及该文件属于哪个用户组。所有文件和目录必须属于一个用户和一个组。在前面的输出中,大多数文件属于用户 admin 和组 admin。例外是..目录,它属于root用户和root组。通常用user:group的形式表示这对。

接下来的列描述了文件的大小(对于目录,它描述的是条目的大小,而不是目录内容占用的空间),最后修改的日期、最后修改的时间以及条目的名称。

现在,让我们回到第一列。它告诉我们,文件或目录的所有者、组和系统中所有其他用户被允许对该条目执行什么操作:

  • 字母d表示我们正在处理的是一个目录。一个破折号()表示它是一个文件。

  • 接下来是一组九个单字符符号,表示谁可以对给定的条目执行什么操作:

前三个字母表示文件或目录的所有者可以对其执行的操作。r表示他们可以读取,w表示他们可以写入,x表示他们可以将其作为程序执行。如果一个文本文件设置了x,shell 将尝试将其作为脚本运行。需要注意的是,当我们处理目录时,x表示我们可以将当前工作目录切换到该目录。有可能进入一个目录(x设置)但无法查看其中的内容(r未设置)。

相同的三个字母在第二组中解释了组的权限。

相同的一组符号在第三组中解释了所有其他用户的权限。

在前面的输出中,.Bash_history是一个文件(它在第一列有一个破折号);文件的所有者(用户 admin)可以读取并写入该文件。可能会有这样的情况:可以写入一个文件(例如日志文件),但无法读取它。该文件不能作为脚本执行(破折号)。接下来的六个破折号告诉我们,分配给 admin 组的用户以及系统中任何其他用户或组都不能对该文件执行任何操作。

有一个例外,就是root用户。除非通过 ACL 和诸如 SELinux 等工具提升权限,否则无法限制root在系统中的全能性。即使是没有权限分配的文件或目录(全是破折号),root也能完全访问。

所有权和权限的管理是通过两条命令来完成的:

  • chown:此命令允许你更改文件或目录的所有权。该命令的名称是更改所有者的缩写。语法非常简单。让我们借此机会练习一下 Linux 帮助文档中的表示法:
chown [OPTION]... [OWNER][:[GROUP]] FILE...

有一个不成文的约定,几乎所有命令的帮助文档都会遵循:

  • 没有括号的文本必须按显示方式输入。

  • 任何位于[ ]括号中的内容是可选的。在chown命令中,用户和组是可选的,但你必须至少提供一个。

  • 尖括号< >中的文本是强制性的,但它是一个占位符,代表你需要提供的内容。

  • 花括号{ }表示一组选项,你需要选择其中一个。它们可以通过竖线|分隔。

  • 元素后面跟三个点表示该元素可以多次提供。在chown的例子中,它是文件或目录的名称。

以下是我对文件应用的一组所有权变更:

  • 我将所有权从admin更改为testuser,但不改变组。请注意,进行更改实际上需要使用root账户(通过sudo命令,详见第三章)。

  • 我将所有权更改回admin,但将组更改为testuser

admin@myhome:~$ ls -ahl testfile
-rw-r--r-- 1 admin admin 0 Aug 22 18:32 testfile
admin@myhome:~$ sudo chown testuser testfile
admin@myhome:~$ ls -ahl testfile
-rw-r--r-- 1 testuser admin 0 Aug 22 18:32 testfile
admin@myhome:~$ sudo chown admin:testuser testfile
admin@myhome:~$ ls -ahl testfile
-rw-r--r-- 1 admin testuser 0 Aug 22 18:32 testfile

在前面的输出中,我们可以看到,成功调用一个命令时不会产生任何输出(chown),除非输出本身就是命令的目的(如ls)。这是 Linux 遵循的基本规则之一。在接下来的输出中,我们可以看到当命令因错误而终止时的情况——没有足够的权限来更改组:

admin@myhome:~$ chown :testuser testfile
chown: changing group of 'testfile': Operation not permitted

运行chown命令的另一种方式是指定一个参考文件,如下面的示例所示:

admin@myhome:~$ sudo chown —reference=.Bash_history testfile
admin@myhome:~$ ls -ahl testfile
-rw-r—r—1 admin admin 0 Aug 22 18:32 testfile
admin@myhome:~$ ls -ahl .Bash_history
-rw------- 1 admin admin 1.1K Aug 22 18:33 .Bash_history

使用–reference选项,我们可以指定一个文件作为我们更改的基准。这在我们进入下一章节时会变得更加有趣。

  • chmod:与chown命令类似,chmod更改模式的缩写)是你用来更改分配给用户和组的权限的命令:
admin@myhome:~$ chmod --help
Usage: chmod [OPTION]... MODE[,MODE]... FILE...
  or:  chmod [OPTION]... OCTAL-MODE FILE...
  or:  chmod [OPTION]... --reference=RFILE FILE...

chmod命令将接受选项、强制模式、可选的更多模式以及需要应用更改的文件列表。与chown命令一样,我们可以指定一个参考文件,其模式将被复制过来。

在第一种形式中,你将指定一个用户、一个组、其他人或所有这些的权限模式,语法如下:

chmod [ugoa…] [-+=] [perms…] FILE...

这里适用以下含义:

  • u:用户,也就是文件的拥有者

  • g:拥有文件的组

  • o:其他人——所有其他人

  • a:所有,意味着每个人

  • -:移除指定的权限

  • +:添加指定的权限

  • =:将权限设置为完全符合指定的内容

让我们来看一些例子。

这为文件所有者添加了testfile文件的读写权限:

chmod u+rw testfile

这会移除对testfile文件的执行权限,适用于所有不是文件所有者且不在文件所有组中的用户:

chmod o-x testfile

这为用户和组授予了testfile文件的读取和执行权限:

chmod ug=rx testfile

在我们粘贴的语法总结中,中间一行很有趣。八进制模式意味着我们可以通过数字指定模式。这在脚本中尤其有用,因为处理数字更简单。一旦你记住了模式数字,你可能会发现使用八进制chmod更加方便。

设置文件模式的数字公式很简单:

  • 0:无权限 (---)

  • 1:执行模式 (--x)

  • 2:写入模式 (-w-)

  • 4:读取模式 (r–)

要设置文件或目录的模式,你将使用你想要应用的权限的总和,前面加上一个0,这将告诉chmod你正在设置八进制模式。语法如下:

chmod [OPTION]... OCTAL-MODE FILE...

这种形式和字母形式之间有一个非常重要的区别——你必须为所有三种实体(用户、组和其他)指定模式。你不能省略其中任何一个。这意味着你也不会使用-+=符号。使用八进制语法时,权限将始终严格按指定方式设置。

要组合多个模式,你需要将它们的数字相加,并使用这个总和作为最终的指定。你会发现这是一个非常优雅的解决方案。没有两个相同的数字组合(权限组合)。试试吧:

  • 执行和读取是 1 和 2,合起来是 3。没有其他组合可以得到 3。

  • 读和写是 2 和 4,合起来是 6。再次强调,没有其他组合能得到 6。

现在,让我们尝试一些例子。

文件的所有者将拥有文件的读写权限,而组用户和其他用户将只有读取权限:

chmod 0644 testfile

文件的所有者拥有所有权限(读、写和执行),而组用户和其他用户只能读取和执行:

chmod 0755 testfile

模式前面的0并非必需。

我们已经讲解了文件系统、目录结构以及与用户和组相关的基本文件权限。在下一部分,我们将介绍基本的 Linux 命令。

与系统交互

存储在硬盘上的程序和脚本只是文件。当它们被映射到内存并开始执行时,它们就变成了进程。在这一阶段,你可以安全地假设系统中运行的任何东西都是某种进程。

处理这个

Linux 中的进程有几个特性是你需要了解的:

  • 进程 ID (PID):一个系统范围内唯一的数字标识符。

  • 父进程 IDPPID):Linux 系统中的每个进程(除进程号为 1 的进程外)都有一个父进程。进程号为 1 的进程是 init 进程。它是负责启动所有系统服务的程序。启动另一个程序的程序被称为 父进程。由另一个程序启动的程序被称为 子进程。当你登录到系统时,你的 shell 也是一个进程,并且它有自己的 PID。当你在该 shell 中启动一个程序时,你的命令行 PID 将成为该程序的父进程 ID。如果一个进程失去了它的父进程(即父进程在未终止其子进程的情况下终止),那么该子进程将被分配一个新的父进程:进程号 1。

  • S 状态,进程进入休眠状态。然而,它不会接受任何中断和信号。只有当请求的资源可用时,它才会醒来。

  • T:我们可以指示程序停止执行并等待。这被称为 停止状态。这样的进程可以通过使用特殊信号重新启动执行。

  • Z:僵尸进程。当一个进程结束其执行时,它会通知其父进程。进程终止时也会发生同样的事情。父进程负责将其从进程表中移除。在此之前,进程会保持在僵尸状态,也叫做 defunc

  • 用户:进程的所有者,或者更准确地说,是使用该用户权限执行进程的用户。如果该用户无法做某事,进程也无法做到。CPU:进程所使用的 CPU 时间的百分比,以 0.0 到 1.0 之间的浮动数字表示。MEM:内存使用量,同样在 0.0 到 1.0 之间,其中 1.0 表示系统内存的 100%。

每个进程都有比我们刚才提到的更多特征,但这些是绝对必要的。管理进程是一个独立章节的主题,如果不是整本书的内容。

检查进程的工具叫做 ps。乍一看,它似乎是一个非常简单的命令,但实际上,man 页面展示了大量的选项。需要注意的是,ps 本身只会打印出系统的快照。它不会监控并更新其输出。你可以将 pswatchrun tophtop 命令结合使用,以持续获取关于进程和系统负载的信息。

在最简单的形式下,ps 将打印出与调用它的用户 ID 相同的所有进程:

admin@myhome:~$ ps
    PID TTY          TIME CMD
  24133 pts/1    00:00:00 Bash
  25616 pts/1    00:00:00 ps

在我的 Linux 机器上,只有两个进程在为我的用户运行:Bash shell 和 ps 程序本身。

ps 命令有几个有趣的选项,可以帮助查询系统中的运行进程。现在,为 ps 指定选项是有点棘手的,因为它接受两种语法,一种带有连字符,另一种不带,有些选项根据连字符的不同有不同的含义。让我引用一下 man 页面:

       1   UNIX options, which may be grouped and must be preceded by a dash.
       2   BSD options, which may be grouped and must not be used with a dash.
       3   GNU long options, which are preceded by two dashes.
Note that ps -aux is distinct from ps aux.  The POSIX and UNIX standards require that ps -aux print all processes owned by a user named x, as well as printing all processes that would be selected by the -a option.  If the
       user named x does not exist, this ps may interpret the command as ps aux instead and print a warning.  This behavior is intended to aid in transitioning old scripts and habits.  It is fragile, subject to change, and thus
       should not be relied upon.

为了简洁起见,我已经省略了一些输出内容。一旦你开始使用 shell 脚本,这个区别可能变得很重要,因为它们可能会采用三种语法中的任何一种。每当你有疑问时,请使用破折号表示法:

  • -f:所谓的 PIDPPIDCSTIMETTYTIME 以及启动进程的命令:
admin@myhome:~$ ps -f
UID          PID    PPID  C STIME TTY          TIME CMD
admin      24133   24132  0 16:05 pts/1    00:00:00 -Bash
admin      25628   24133  0 17:35 pts/1    00:00:00 ps -f
  • -e:所有用户的所有进程:
admin@myhome:~$ ps -e
    PID TTY          TIME CMD
      1 ?        00:00:04 systemd
      2 ?        00:00:00 kthreadd
[...]
  25633 ?        00:00:00 kworker/u30:0-events_unbound
  25656 pts/1    00:00:00 ps
  • -ef:以长格式查看所有进程:
admin@myhome:~$ ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 Aug20 ?        00:00:04 /sbin/init
root           2       0  0 Aug20 ?        00:00:00 [kthreadd]
  • -ejH:一个漂亮的进程树。输出中缩进更多的 CMD(最后一列)是较少缩进的 CMD 的子进程:
admin@myhome:~$ ps -ejH
    PID    PGID     SID TTY          TIME CMD
      2       0       0 ?        00:00:00 kthreadd
      3       0       0 ?        00:00:00   rcu_gp
      4       0       0 ?        00:00:00   rcu_par_gp
      6       0       0 ?        00:00:00   kworker/0:0H-events_highpri
      9       0       0 ?        00:00:00   mm_percpu_wq

还有很多其他选项可用,特别是用于控制哪些字段是感兴趣的。我们将在后续章节中继续讨论这些。

有一个名字可能会让人误解的命令,叫做 kill 命令。它用于向正在运行的进程发送所谓的信号。信号是一种通知进程执行某种操作的方式。其中一个信号确实会终止程序,立即将其结束,但这只是其中的一种。

要列出现有信号,请使用 kill -l 命令:

admin@myhome:~$ admin@myhome:~$ kill -l
 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

好吧,这个列表很可爱,但它什么都没告诉我们。我们怎么知道这些信号分别有什么作用呢?下面来做点侦探工作。首先,由于我们在 kill -l 的输出中看到了这些内容,我们可以运行 man kill 命令,看看是否有什么能解释这些信号:

EXAMPLES
       kill -9 -1
              Kill all processes you can kill.
       kill -l 11
              Translate number 11 into a signal name.
       kill -L
              List the available signal choices in a nice table.
       kill 123 543 2341 3453
              Send the default signal, SIGTERM, to all those processes.
SEE ALSO
       kill(2), killall(1), nice(1), pkill(1), renice(1), signal(7), sigqueue(3), skill(1)

例子部分显示并描述了一个信号,但在 SEE ALSO 部分,我们看到有一个指向 man 页面中第七部分的信号的引用。我们来检查一下:

admin@myhome:~$ man 7 signal
SIGNAL(7)                                                              
                                 Linux Programmer's Manual                                     
                                                         SIGNAL(7)
NAME
       signal - overview of signals

现在,有一个漂亮的页面,其中有一个表格列出了你在 Linux 中可以使用的所有信号。

那么,如何使用这个 kill 命令呢?你需要学习所有这些信号吗?答案是否定的。你会使用一些常见的信号。如果你忘记了某个信号,别犹豫,直接查阅 man 或在网上搜索:

  • kill -9 PID:臭名昭著的 SIGKILL。这会强制终止我们指定 PID 的进程,且会突然中断,忽略任何它可能进行的清理工作。如果该进程已打开文件句柄,它不会释放;如果需要写入文件或与其他程序同步,它也不会执行。这个命令应该谨慎使用,仅在我们确定必须停止程序时才使用。

  • kill PID:如果没有指定信号,默认会发送 SIGTERM。这会告诉程序停止运行,但要优雅地停止——执行所有必要的退出例程并清理资源。如果你不得不使用 kill 命令,这是首选的使用方式。

  • kill -1:所谓的 SIGHUP 信号。它最初用于检测用户连接丢失——电话线路挂断。现在,它常常用于通知进程重新读取其配置。

这里是一个调用 kill 来终止进程的例子。我启动了一个 shell 脚本,做的事情只是等待键盘输入。我将它命名为 sleep.sh

admin@myhome:~$ ps aux | grep sleep
admin      24184  0.0  0.2   5648  2768 pts/1    S+   16:09   0:00 /bin/Bash ./sleep.sh
admin@myhome:~$ pgrep sleep.sh
24184
admin@myhome:~$ kill -9 24184
admin@myhome:~$ pgrep sleep.sh
admin@myhome:~$ ps aux | grep sleep
admin      24189  0.0  0.0   4732   732 pts/0    S+   16:09   0:00 grep sleep

首先,我使用ps aux命令并在输出中查找sleep.sh进程,目的是向你展示它确实存在。接着,我使用pgrep命令快速找到正在运行的脚本的 PID,并将该 PID 传递给kill -9命令。实际上,sleep.sh进程已经被终止。你可以在另一个终端中确认这一点,那时我正运行着sleep.sh

admin@myhome:~$ ./sleep.sh
Killed

如果我只是简单地使用kill命令,输出会有所不同:

admin@myhome:~$ ./sleep.sh
Terminated

还有一种方式可以将所有信号传递给正在运行的程序,但前提是当前程序正在我们登录的 shell 中运行,并且处于前台状态;这意味着它控制了屏幕和键盘:

  • 按下Ctrl + C键会向程序发送SIGINT信号。SIGINT告诉程序用户已经按下了该组合键,程序应该停止。程序如何终止还取决于它自身的实现。

  • 按下Ctrl + D键会发送SIGQUIT信号——它类似于SIGINT,但还会生成所谓的核心转储(core dump),也就是一个可以在调试过程中使用的文件。

在文本中表示这些组合键的常见方式是^c^d^代表Ctrl键),ctrl+cctrl+dctrlCtrl键的快捷方式),以及C-cC-dC代表Ctrl键)。

在找东西吗?

有时你需要在文件系统中查找一个目录或文件。Linux 提供了一些命令,可以让你执行这一操作。在这些命令中,find是最强大的。要详细介绍它的所有功能需要更多的篇幅,而我们目前没有足够的空间。你可以查找与指定名称完全匹配的文件或目录,查找名称中包含你指定的部分字符的文件或目录,查找在特定时间修改过的文件,查找属于某个用户或组的文件,以及更多其他情况。此外,对于每一个找到的文件,还可以执行一些操作,比如重命名、压缩或搜索某个单词。

在下面的例子中,我们正在查找一个文件signals.h,它位于/``usr目录下:

admin@myhome:/$ find / -name os-release
find: '/lost+found': Permission denied
find: '/etc/sudoers.d': Permission denied
/etc/os-release
find: '/etc/ssl/private': Permission denied
/usr/lib/os-release
[...]

首先,我们调用find命令,然后告诉它从文件系统的根目录(/)开始查找,接着告诉它查找一个名为os-release的文件(-``name os-release)。

你会注意到,在输出中(为了简洁起见我省略了一部分),有一些错误信息,表示find没有权限访问某些文件。

如果你不确定名称的大小写,即它是否包含小写字母或大写字母(记住,Linux 是区分大小写的,OS-Releaseos-release不是同一个文件),你可以使用-iname选项。

如果你确定你要找的是一个文件,那么可以使用-type f选项:

admin@myhome:/$ find / -type f -name os-release

对于目录选项,使用-``type d

如果要查找与某个模式匹配的文件,比如以.sh结尾的文件,可以使用以下模式:

admin@myhome:/$ find / -type f -name "*.sh"

星号表示任意数量的任意字符。我们将它放在引号中,以避免在 find 有机会执行之前,shell 先解释星号。我们将在下一章 中级 Linux 中解释所有特殊符号(称为 globs)和正则表达式。

要删除所有找到的文件,你可以使用 -delete 选项:

admin@myhome:~$ find . -type f -name test -delete

要对找到的文件执行操作,你可以使用 -exec 选项:

admin@myhome:/$ find / -type f -name  os-release -exec grep -i debian {} \;
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

在前面的示例中,我们使用了名为 grep 的命令来查找包含 debian 这个单词的所有行,无论是大写还是小写。

这引出了 grep 命令。

grep 命令用于在文件中查找某个模式的出现。找到匹配的行后,它会打印该行。grep 命令与 find 命令类似,不同之处在于它的目的是在文件内部进行搜索,而 find 只关心文件和目录本身的特性。

假设有一个名为 red_fox.txt 的文本文件,内容如下:

admin@myhome:~$ cat red_fox.txt
The red fox
jumps over
the lazy
brown dog.

顺便说一下,cat 是一个将指定文件内容打印到终端的命令。

假设我们想查找所有包含单词 the 的行:

admin@myhome:~$ grep -i The red_fox.txt
The red fox
the lazy

你可能已经猜到,-i 选项表示我们不在乎大小写。

等等,我们可以搜索替代项。假设我们关心的行包含 foxdog。你可以使用 -e 选项为每个要搜索的单词添加一次,或者使用 -E 选项将所有单词放在单引号内,并用 | 字符分隔:

admin@myhome:~$ grep -e fox -e dog red_fox.txt
The red fox
brown dog.
admin@myhome:~$ grep -E 'fox|dog' red_fox.txt
The red fox
brown dog.

通过添加 -n 选项,你将获得匹配项所在行的行号:

admin@myhome:~$ grep -n -E 'fox|dog' red_fox.txt
1:The red fox
4:brown dog.

你可以查找以指定单词开头的行或在行尾具有特定模式的行。

你甚至可以在一个目录层次结构中的所有文件中执行 grep 搜索。语法稍有不同:模式放在前面,目录放在后面。同时,你还会得到匹配所在的文件名:

admin@myhome:~$ grep -r "fox" .
./red_fox.txt:The red fox

findgrep 的最大威力来自于一个叫做 正则表达式regexregexp 的简称)的概念,它们有一本专门的书,且对于新用户可能会感到困惑。我们将在 第三章第四章 中详细解释它们。不过,我们会仅介绍最常用的用法。

如果你正在寻找一个程序并且想知道它的完整路径,有一个命令可以做到,那就是 whereis。这里有一个示例:

admin@myhome:~$ whereis ping
ping: /usr/bin/ping /usr/share/man/man8/ping.8.gz

whereis 命令不仅会打印二进制文件的完整路径,如果安装了相应的 man 手册页,它还会打印该手册页。

让我们进行一些操作

对文件和目录可以执行四种基本操作:

  • 创建

  • 重命名或移动

  • 删除

  • 复制

每一个操作都有一个特殊的工具:

  • mkdir:这个命令的语法非常简单且有限。基本上,你只是告诉它创建一个指定名称的目录。如果你要创建嵌套目录,也就是一个文件夹包含另一个文件夹,路径中的所有目录必须存在。如果它们不存在,你可以使用特殊的-p选项来创建它们:
admin@myhome:~$ mkdir test
admin@myhome:~$ ls -l
total 4
drwxr-xr-x 2 admin admin 4096 Aug 24 15:17 test
admin@myhome:~$ mkdir something/new
mkdir: cannot create directory 'something/new': No such file or directory
admin@myhome:~$ mkdir -p something/new
admin@myhome:~$ ls -l
total 8
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something
drwxr-xr-x 2 admin admin 4096 Aug 24 15:17 test

在前面的示例中,你可以看到我直接在我的主文件夹中创建了一个名为 test 的目录。接下来,我尝试在something文件夹中创建一个new文件夹。然而,后者并不存在,mkdir告诉我这一点并拒绝创建新的目录。我使用了特殊的-p选项来创建一个完整的路径以便创建新的目录。

  • mv:这是一个用于移动和重命名文件及目录的命令。同样,语法相当简单,尽管这个命令提供了一些额外的功能,比如创建移动文件的备份。

    要重命名一个文件或目录,我们将其从当前名称移动到新名称:

admin@myhome:~$ mv test no-test
admin@myhome:~$ ls -l
total 8
drwxr-xr-x 2 admin admin 4096 Aug 24 15:17 no-test

查看其man页面或help信息以了解更多。

  • rm:这个命令比较有趣,主要因为它提供了安全功能。通过特殊的-i选项,你可以指示它在删除文件或目录之前始终询问你。通常,rm会对目录进行退出,如下例所示:
admin@myhome:~$ admin@myhome:~$ ls -l no-test/
total 0
-rw-r--r-- 1 admin admin 0 Aug 24 15:26 test
admin@myhome:~$ rm no-test/
rm: cannot remove 'no-test/': Is a directory
admin@myhome:~$ rm -d no-test/
rm: cannot remove 'no-test/': Directory not empty
admin@myhome:~$ rm no-test/test
admin@myhome:~$ rm -d no-test/

我在no-test目录中创建了一个名为 test 的文件。rm拒绝删除该文件夹。我使用了-d选项,指示命令删除空目录。然而,该目录仍然包含一个文件。接着,我删除了文件,然后使用rm -d成功删除了no-test文件夹。我本可以使用-r选项,这将使命令删除所有目录,即使它们不是空的。

  • cp:这个命令用于复制文件和目录。请注意,与rm类似,cp会拒绝复制目录,除非使用-r选项。cp可以说是所有命令中最复杂且功能最丰富的命令之一,包括备份文件、创建链接(快捷方式)代替真正的复制等功能。查看它的man页面。在以下示例中,我将something目录复制到new目录。显然,我必须使用-r选项。接着,我创建了一个名为file的空文件并将其复制到newfile。对于这些,我不需要任何选项:
admin@myhome:~$ ls -l
total 4
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something
admin@myhome:~$ cp something/ new
cp: -r not specified; omitting directory 'something/'
admin@myhome:~$ cp -r something new
admin@myhome:~$ ls -l
total 8
drwxr-xr-x 3 admin admin 4096 Aug 24 15:33 new
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something
admin@myhome:~$ touch file
admin@myhome:~$ cp file newfile
admin@myhome:~$ ls -l
total 8
-rw-r--r-- 1 admin admin    0 Aug 24 15:33 file
drwxr-xr-x 3 admin admin 4096 Aug 24 15:33 new
-rw-r--r-- 1 admin admin    0 Aug 24 15:34 newfile
drwxr-xr-x 3 admin admin 4096 Aug 24 15:18 something

你现在应该理解并能够使用 Linux 或类似系统中的基本命令行命令,比如创建、复制和删除文件;你还可以查找文本文件中的内容,或者按名称查找文件或目录。你对你工作系统中的进程也有所了解。在下一章中,我们将深化这些知识。

总结

我们展示的只是 Linux 管理员在工作中可能使用的几百个命令中的一小部分。正如本章开头所提到的,完整的参考超出了本书的范围。然而,我们学到的内容足以进行基本的系统操作,并为下一章的内容打下基础:更高级的 Linux 管理话题。

练习

  • 了解如何递归地应用chown——这意味着我们的chown调用应该进入目录并将更改应用于其中的所有项目。

  • 了解watch命令的功能。将其与ps命令一起使用。

  • 了解如何删除一个目录。

  • 对于你在这里学到的所有命令,阅读它们的–help输出。打开man页面并浏览其中的内容,特别是示例部分。

资源

第三章:中级 Linux

在本章中,我们将继续介绍 Linux shell。本话题非常广泛,足以写成一本独立的书。我们将回顾上一章的内容并介绍新的主题。

在本章中,我们将涵盖以下内容:

  • 通配符

  • 自动化重复任务

  • 软件安装

  • 用户管理

  • 安全外壳SSH)协议

技术要求

强烈建议你安装并准备好使用 Linux 系统。我们建议使用虚拟机或笔记本电脑,这样如果出了什么大问题,可以安全地重新安装。这将让你能够跟随本书的示例并完成我们给出的任何练习。

我们不会讲解安装过程。每个发行版可能会使用不同的安装程序,无论是图形界面还是文本界面(具体取决于发行版及所选变种)。你需要记下或记住你的用户名称(方便地称为用户名登录名)和密码。如果你有物理访问权限,但不知道登录名或密码,甚至两者都不知道,还是有方法可以进入系统,但这些方法远超本书的范围。

本书的主要发行版是Debian。不过,你应该可以使用上一章中介绍的任何主要发行版,只要它不是 Alpine。

通配符

Shell 能为你做很多事情,帮助你简化工作。其中之一就是允许在输入命令参数时存在一定的不确定性。为此,shell 定义了几个特殊字符,将它们作为符号使用,而不是字面输入。这些符号称为全局模式,或称为通配符。在通配符中使用的字符有时被称为通配符(wildcards)。

不要将通配符与正则表达式regexps)混淆。虽然通配符本身非常强大,但它们无法与正则表达式相比。另一方面,bash 在执行模式匹配时并不会对正则表达式进行求值。

下表描述了 Shell 通配符及其含义。我们将通过几个示例来解释它们的确切含义:

通配符 含义
* 匹配任意数量的字符(包括零个字符)
? 精确匹配一个字符
[...] 匹配括号内集合中的任意一个字符

表 3.1 – Shell 通配符

上表可能对你来说不够清晰,下面我们将通过一些示例来进一步说明:

示例 含义
* 这将匹配任意长度的任何字符串。
*``test* 这将匹配任何包含test的字符串:test.txtgood_test.txttest_run,甚至是简单的test(记住,它也可以匹配空字符串)。
test*txt 这将匹配任何名称以test开头,txt结尾的文件,例如 test.txttesttxttest_file.txt 等。
test? 这将匹配任何一个包含test并加上一个字符的情况:test1test2testatest,等等。
test.[ch] 这将匹配两种情况中的一种:test.ctest.h,其他情况不匹配。
*.[``ab] 这将匹配任何以点号结束并且后面跟着 ab 的字符串。
?[``tf] 这将匹配任何一种字符,后跟 tf

表 3.2 – Shell 通配符 – 示例

通配符的真正威力在于你开始编写一些更复杂的命令字符串(所谓的单行命令)或脚本时才能显现。

一些简单命令在与通配符结合使用时会达到全新的层次,像是 findgreprm。在以下示例中,我使用通配符删除所有以任意字符开始、接着是 test 和一个点,然后是 log 和任意字符结尾的文件。因此,weirdtest.loganothertest.log1test.log.3 文件会被匹配,但 testlogimportant_test.out 则不会。首先,让我们列出所有包含 test 字样的文件:

admin@myhome:~$ ls -ahl *test*
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 importat_test.out
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 runner_test.lo
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 runner_test.log
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 test.log
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.1
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.2
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.3
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.4
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.5
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.6
-rw-r--r-- 1 admin admin 0 Sep 17 20:35 test.log.7

你会注意到,我使用了通配符(*)来实现我的目标。现在,是时候进行实际的删除操作了:

admin@myhome:~$ rm *test.log*
admin@myhome:~$ ls -ahl *test*
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 importat_test.out
-rw-r--r-- 1 admin admin 0 Sep 17 20:36 runner_test.lo

正如这里演示的那样,它已经成功执行了。你还会注意到,一个正确执行的命令不会打印任何消息。

在本节中,我们解释了如何使用通配符(globs)——这些特殊字符允许我们以一定的不确定性匹配系统中的名称。在下一节中,我们将介绍自动化重复任务的机制。

自动化重复任务

有时你可能希望使某些任务变得重复。你可能会编写一个脚本来备份数据库、检查用户的主目录权限,或者将当前操作系统的性能指标转储到文件中。现代的 Linux 发行版提供了两种设置这些任务的方法。还有第三种方法允许你在延迟的时间一次性运行任务(at 命令),但在这里我们关注的是重复性任务。

Cron 作业

Cron 是一种传统的方式,用来定期执行需要在特定时间间隔内运行的任务。通常,它们应该被 systemd 定时器 取代,但许多软件仍通过 cron 作业提供可重复性,而 Alpine Linux 在最小化发行版的情况下不会包含这一特性。

Cron 作业本质上是定期执行的命令。这些命令及其触发定时器被定义在位于 /etc/ 目录中的配置文件中。不同的发行版会有不同数量的文件和目录。所有的发行版都会包含一个 /etc/crontab 文件。这个文件通常包含对其中字段的解释以及几个实际命令,可以作为模板使用。在以下的代码块中,我粘贴了来自默认 /etc/crontab 文件的解释:

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *    * * *   root    cd / && run-parts --report /etc/cron.hourly

通常,有两种方法来设置 cron 任务。其一是将一个运行命令的脚本放置在四个目录中的一个:/etc/cron.hourly/etc/cron.daily/etc/cron.weekly/etc/cron.monthly。这些目录应该足以满足日常操作。/etc/crontab 文件指定了这些任务何时运行。

第二个选项是使用 crontab 命令。crontab 命令允许给定的用户在其 crontab 文件中创建一个条目。然而,系统范围的 crontab 文件(位于 /etc/ 目录中)和每个用户的 crontab 文件之间是有区别的。用户的 cron 文件不指定用户字段。所有条目都以用户的权限运行。我们来看看这些区别:

  • crontab -l 列出用户在其 crontab 文件中定义的所有 cron 任务。

  • crontab -e 让用户编辑 crontab 文件,添加、删除或修改任务。

Systemd 定时器

我们在这里不会详细介绍 systemd 定时器,只会简要提到它们为何可能是更好的选择。

Systemd 定时器 单元是 cron 守护进程 任务的更新版本。它们可以完成 cron 的所有功能。然而,它们提供了一些额外的能力:

  • 您可以指定任务在系统启动后某个时间运行。

  • 您可以指定任务必须在其他任务运行后的某个间隔内执行。

  • 对定时器单元的依赖甚至可以是服务单元——即普通的系统服务任务。

  • 精度更高。Cron 任务的精度通常只能到每分钟一次,而 Systemd 定时器的触发精度可以达到秒级。

在本节中,我们介绍了 cron 任务,并提到 systemd 定时器是自动化定期任务的两个最重要的方式。下一节中,我们将介绍软件管理。

软件安装

根据您选择的发行版和安装方式,您的系统可能缺少一些日常工作所需的软件。也可能有一天,您需要安装一款默认未安装的软件。

Linux 发行版在一些方面领先于其他操作系统,后来其他操作系统才开始效仿。Linux 操作系统上安装软件的常见方式是运行一个合适的命令,这个命令会下载一个二进制文件,正确地将其放置到系统中,必要时添加一些配置,并使其对用户可用。今天,这可能听起来并不算革命性。毕竟,我们生活在一个拥有 Apple App Store、Google Play 和 Microsoft Apps 的世界中。但是,在过去,当 Windows 和 macOS 用户需要在互联网上找到合适的软件安装程序时,Linux 用户只需一个命令就能安装大多数软件。

这一点在 DevOps 力图建立的自动化环境中至关重要,原因有多个:

  • 可安装的软件(以软件包形式分发)存储在发行版团队维护的仓库中。这意味着你不需要知道软件在互联网上的位置;你只需要知道它的包名,并确保它在仓库中。

  • 我们将在这里介绍的包管理标准(rpmdeb了解依赖关系。这意味着,如果你尝试安装的软件依赖于另一个尚未安装的软件,它会自动拉取并安装。

  • 我们将在这里介绍的发行版都有安全团队。它们与包维护者合作,确保修复已知的漏洞。然而,这并不意味着他们会主动研究这些包中的漏洞。

  • 仓库会在互联网上进行镜像。这意味着,即使其中一个仓库出现故障(下线、被 DDoS 攻击或其他原因),你仍然可以从全球各地访问它的镜像副本。这对于商业仓库未必适用。

  • 如果你愿意,可以在本地局域网中创建一个本地仓库镜像。这将为你提供最快的下载速度,但代价是大量的硬盘空间。软件包仓库可能非常庞大。

软件的数量和版本在许多方面取决于发行版:

  • 政策分发必须分发具有不同许可证类型的软件:有些发行版会禁止任何不完全开放和免费的软件(根据开源倡议定义)。其他发行版则会让用户选择是否添加包含更严格许可的软件的仓库。

  • 维护者数量和维护模式:显而易见,发行版能做的工作量取决于他们拥有多少人力资源。团队越小,能够打包和维护的软件就越少。部分工作是自动化的,但很多工作始终需要人工完成。由于 Debian 是一个非商业发行版,它完全依赖志愿者的工作。而 Ubuntu 和 Fedora 则有商业支持,部分团队成员甚至是由其中的公司雇佣的:Canonical 和 Red Hat。红帽企业 LinuxRHEL)完全由红帽员工构建和维护。

  • 你决定使用的仓库类型:一些软件厂商将它们的软件包分发在独立的仓库中,你可以将其添加到你的配置中,像使用普通的发行版仓库一样使用它们。不过,如果你这样做了,有些事情需要注意:

    • 第三方仓库中的软件不属于发行版的质量管理工作:这完全取决于仓库维护者——在这种情况下,就是软件供应商。这也包括安全修复。

    • 第三方仓库中的软件可能不会与核心发行版仓库同时更新:这意味着有时软件所需的包版本与发行版提供的包版本之间会存在冲突。而且,随着你向服务器添加更多第三方仓库,发生冲突的概率也会增加。

Debian 和 Ubuntu

Debian 发行版及其衍生版 Ubuntu 使用 DEB 包格式。它是专为 Debian 创建的。我们这里不会深入探讨其历史,只会根据需要触及一些技术细节。

直接操作包文件的命令是 dpkg。它用于安装、移除、配置,并且重要的是,构建 .deb 包。它只能安装存在于文件系统上的包,并且不理解远程仓库。让我们看看 dpkg 的一些可能操作:

  • dpkg -i package_file.deb:安装包文件。该命令会经过多个阶段,之后安装该软件。

  • dpkg –unpack package_file.deb:解包意味着它会将所有重要文件放入相应的位置,但不会配置包。

  • dpkg –configure package:请注意,这里需要的是包名,而不是包文件名。如果因为某些原因包已解包但未配置,你可以使用 -a–pending 标志来处理它们。

  • dpkg -r package:此操作会移除软件,但不会删除配置文件及其可能包含的数据。如果你打算将来重新安装该软件,这可能会很有用。

  • dpkg -p package:此操作会清除该包并移除一切:软件、数据、配置文件和缓存。一切。

在以下示例中,我们正在从一个物理下载到系统的包中安装 nano 编辑器,这个包可能是通过点击网页上的下载按钮获得的。请注意,这并不是一种常见的做法:

root@myhome:~# dpkg -i nano_5.4-2+deb11u1_amd64.deb
(Reading database ... 35904 files and directories currently installed.)
Preparing to unpack nano_5.4-2+deb11u1_amd64.deb ...
Unpacking nano (5.4-2+deb11u1) over (5.4-2+deb11u1) ...
Setting up nano (5.4-2+deb11u1) ...
Processing triggers for man-db (2.9.4-2) ...

通常,你会需要使用 apt 工具集来安装和移除软件:

  • apt-cache search NAME 将搜索包含给定字符串的软件包。在以下示例中,我正在寻找一个包含 vim 字符串的软件包(vim 是几个流行的命令行文本编辑器之一)。为了简洁,我已将输出进行了缩短:
root@myhome:~# apt-cache search vim
acr - autoconf like tool
alot - Text mode MUA using notmuch mail
[...]
vim - Vi IMproved - enhanced vi editor
[...]
  • apt-get install NAME 将安装你指定名称的包。你可以在一行中安装多个软件包。在以下示例中,我正在安装一个 C 编译器、一个 C++编译器和一个 Go 语言套件。请注意,输出中还包含了一些为了确保我的软件正常工作而需要的依赖包,并且它们将会被安装以提供该功能:
root@myhome:~# apt-get install gcc g++ golang
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
gcc is already the newest version (4:10.2.1-1).
The following additional packages will be installed:
  bzip2 g++-10 golang-1.15 golang-1.15-doc golang-1.15-go golang-1.15-src golang-doc golang-go golang-src libdpkg-perl libfile-fcntllock-perl libgdbm-compat4 liblocale-gettext-perl libperl5.32 libstdc++-10-dev perl perl-modules-5.32
  pkg-config
Suggested packages:
  bzip2-doc g++-multilib g++-10-multilib gcc-10-doc bzr | brz git mercurial subversion debian-keyring gnupg patch bzr libstdc++-10-doc perl-doc libterm-readline-gnu-perl | libterm-readline-perl-perl libtap-harness-archive-perl dpkg-dev
The following NEW packages will be installed:
  bzip2 g++ g++-10 golang golang-1.15 golang-1.15-doc golang-1.15-go golang-1.15-src golang-doc golang-go golang-src libdpkg-perl libfile-fcntllock-perl libgdbm-compat4 liblocale-gettext-perl libperl5.32 libstdc++-10-dev perl
  perl-modules-5.32 pkg-config
0 upgraded, 20 newly installed, 0 to remove and 13 not upgraded.
Need to get 83.9 MB of archives.
After this operation, 460 MB of additional disk space will be used.
Do you want to continue? [Y/n]

安装程序在这里停止并等待我们的输入。默认操作是接受所有附加包并继续安装。通过输入 nN 并按下 Enter,我们可以停止这个过程。安装操作的 -y 开关将跳过这个问题,并自动继续到下一步:

  • apt-get update 将刷新包数据库,获取新的可用包和新版本。

  • apt-get upgrade 将所有已安装的包升级到数据库中列出的最新版本。

  • apt-get remove NAME 将删除给定名称的包。在以下示例中,我们正在卸载 C++ 编译器:

root@myhome:~# apt-get remove g++
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages were automatically installed and are no longer required:
  g++-10 libstdc++-10-dev
Use 'apt autoremove' to remove them.
The following packages will be REMOVED:
  g++
0 upgraded, 0 newly installed, 1 to remove and 13 not upgraded.
After this operation, 15.4 kB disk space will be freed.
Do you want to continue? [Y/n]
(Reading database ... 50861 files and directories currently installed.)
Removing g++ (4:10.2.1-1) ...

CentOS、RHEL 和 Fedora

另一类流行的发行版使用 RPM 包格式。与包交互的基本工具是 rpm。使用这种格式的主要发行版是由 Red Hat 公司制作的 RHEL。包的文件扩展名始终是 .rpm

它们使用 dnf 命令来管理包。还有 yum 命令,它是 RHEL 发行版的原始包管理器(并且,扩展到 Fedora 和 CentOS 发行版),但已被移除。dnfyum 的下一代重写,具有许多改进,使其更强大、更现代:

  • dnf install package_name 将安装给定名称的包及其依赖项。

  • dnf remove package_name 将删除包。

  • dnf update 将所有包更新到包数据库中的最新版本。你可以指定包名,之后 yum 将更新该包。

  • dnf search NAME 将搜索包含 NAME 字符串的包名。

  • dnf check-update 将刷新包数据库。

让我们来看看另一款广泛使用的 Linux 发行版,特别是作为 Docker 镜像的基础——Alpine Linux。

Alpine Linux

Alpine Linux 尤其受到主要使用 Docker 和 Kubernetes 的工程师喜爱。正如该发行版的主页所宣称,小巧、简单、安全。它没有像基于 Debian 或 Red Hat 的发行版中那样的花里胡哨,但输出的 Docker 镜像非常小,而且由于它专注于安全,你可以假设如果你已经将所有包更新到最新版本,那么没有重大安全漏洞。

Alpine Linux 的主要缺点(也可以说是优点,视角不同而定)是它使用 musl 库而非广泛使用的 libc 库进行编译,尽管它确实使用了 libc,因此在安装任何 Python 依赖之前,你需要执行额外的步骤,确保已安装编译时依赖。

与包交互的命令是 3.16)和 edge,这是一个滚动更新版本(它始终拥有最新版本的包)。

此外,还有三个仓库可以用来安装包:maincommunitytesting

你将会在主仓库中找到官方支持的包;所有经过测试的包都放在社区仓库中,而测试版则用于测试,这意味着可能存在一些损坏、过时或带有安全漏洞的包。

搜索包

在搜索或安装任何包之前,建议先下载最新的包缓存。你可以通过调用apkupdate命令来实现:

root@myhome:~# apk update
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
v3.16.2-376-g3ff8974e73 [https://dl-cdn.alpinelinux.org/alpine/v3.16/main]
v3.16.2-379-g3c25b38306 [https://dl-cdn.alpinelinux.org/alpine/v3.16/community]
OK: 17037 distinct packages available

如果你在构建一个用于以后使用的 Docker 镜像,建议在构建过程的最后一步使用apk cacheclean命令来删除此缓存。

有时,在我们创建新的 Docker 镜像时,不知道包的确切名称。此时,最简单的方式是使用 Web 界面:pkgs.alpinelinux.org/packages

使用命令行界面(CLI),你将能够搜索部分库名和二进制文件名,尽管你可以使用so:前缀来指定你正在搜索的是库。其他有用的前缀包括cmd:用于命令,pc:用于pkg-config文件:

root@myhome:~# apk search libproc
libproc-3.3.17-r1
libksysguard-5.24.5-r0
process-cpp-3.0.1-r3
samba-dc-libs-4.15.7-r0
procps-dev-3.3.17-r1
root@myhome:~# apk search vim
neovim-doc-0.7.0-r0
gvim-8.2.5000-r0
vim-tutor-8.2.5000-r0
faenza-icon-theme-vim-1.3.1-r6
notmuch-vim-0.36-r0
kmymoney-5.1.2-r3
faenza-icon-theme-gvim-1.3.1-r6
meson-vim-0.62.1-r0
runvimtests-1.30-r1
graphviz-3.0.0-r0
neovim-0.7.0-r0
py3-pynvim-0.4.3-r3
nftables-vim-0_git20200629-r1
vim-doc-8.2.5000-r0
vim-editorconfig-0.8.0-r0
apparmor-vim-3.0.4-r0
geany-plugins-vimode-1.38-r1
vimdiff-8.2.5000-r0
vimb-3.6.0-r0
neovim-lang-0.7.0-r0
u-boot-tools-2022.04-r1
fzf-neovim-0.30.0-r7
nginx-vim-1.22.1-r0
msmtp-vim-1.8.20-r0
protobuf-vim-3.18.1-r3
vimb-doc-3.6.0-r0
icinga2-vim-2.13.3-r1
fzf-vim-0.30.0-r7
vim-sleuth-1.2-r0
gst-plugins-base-1.20.3-r0
mercurial-vim-6.1.1-r0
skim-vim-plugin-0.9.4-r5
root@myhome:~# apk search -e vim
gvim-8.2.5000-r0
root@myhome:~# apk search -e so:libproc*
libproc-3.3.17-r1
libksysguard-5.24.5-r0
process-cpp-3.0.1-r3
samba-dc-libs-4.15.7-r0

安装、升级和卸载包

你可以使用add(安装)、del(卸载)和upgrade命令对包执行基本操作。在安装过程中,你也可以使用在搜索操作中可用的特殊前缀,但建议使用包的准确名称。请注意,当向系统添加新包时,apk将选择该包的最新版本:

root@myhome:~# apk search -e postgresql14
postgresql14-14.5-r0
root@myhome:~# apk add postgresql14
(1/17) Installing postgresql-common (1.1-r2)
Executing postgresql-common-1.1-r2.pre-install
(2/17) Installing libpq (14.5-r0)
(3/17) Installing ncurses-terminfo-base (6.3_p20220521-r0)
(4/17) Installing ncurses-libs (6.3_p20220521-r0)
(5/17) Installing readline (8.1.2-r0)
(6/17) Installing postgresql14-client (14.5-r0)
(7/17) Installing tzdata (2022c-r0)
(8/17) Installing icu-data-en (71.1-r2)
Executing icu-data-en-71.1-r2.post-install
*
* If you need ICU with non-English locales and legacy charset support, install
* package icu-data-full.
*
(9/17) Installing libgcc (11.2.1_git20220219-r2)
(10/17) Installing libstdc++ (11.2.1_git20220219-r2)
(11/17) Installing icu-libs (71.1-r2)
(12/17) Installing gdbm (1.23-r0)
(13/17) Installing libsasl (2.1.28-r0)
(14/17) Installing libldap (2.6.3-r1)
(15/17) Installing xz-libs (5.2.5-r1)
(16/17) Installing libxml2 (2.9.14-r2)
(17/17) Installing postgresql14 (14.5-r0)
Executing postgresql14-14.5-r0.post-install
*
* If you want to use JIT in PostgreSQL, install postgresql14-jit or
* postgresql-jit (if you didn't install specific major version of postgresql).
*
Executing busybox-1.35.0-r17.trigger
Executing postgresql-common-1.1-r2.trigger
* Setting postgresql14 as the default version
OK: 38 MiB in 31 packages

你还可以选择安装特定版本的包,而不是最新版本。不幸的是,无法从同一仓库安装旧版本的包,因为每当部署新版本时,旧版本会被移除。然而,你可以从其他仓库安装包的旧版本:

root@myhome:~# apk add bash=5.1.16-r0 --repository=http://dl-cdn.alpinelinux.org/alpine/v3.15/main
(1/4) Installing ncurses-terminfo-base (6.3_p20220521-r0)
(2/4) Installing ncurses-libs (6.3_p20220521-r0)
(3/4) Installing readline (8.1.2-r0)
(4/4) Installing bash (5.1.16-r0)
Executing bash-5.1.16-r0.post-install
Executing busybox-1.35.0-r17.trigger
OK: 8 MiB in 18 packages

你还可以使用以下命令安装你事先准备好的自定义包:

root@myhome:~# apk add --allow-untrusted your-package.apk

要升级系统中所有可用的包,你可以简单地调用apk upgrade命令。但是,如果你只想升级某个特定的包,你需要在升级选项后添加该包的名称。记得在此之前刷新包缓存:

root@myhome:~# apk update
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
v3.16.2-376-g3ff8974e73 [https://dl-cdn.alpinelinux.org/alpine/v3.16/main]
v3.16.2-383-gcca4d0a396 [https://dl-cdn.alpinelinux.org/alpine/v3.16/community]
OK: 17037 distinct packages available
root@myhome:~# apk upgrade
(1/2) Upgrading alpine-baselayout-data (3.2.0-r22 -> 3.2.0-r23)
(2/2) Upgrading alpine-baselayout (3.2.0-r22 -> 3.2.0-r23)
Executing alpine-baselayout-3.2.0-r23.pre-upgrade
Executing alpine-baselayout-3.2.0-r23.post-upgrade
Executing busybox-1.35.0-r17.trigger
OK: 8 MiB in 18 packages

你可以通过不带任何其他选项地调用apk来查找所有可能的操作。最有用的操作之一是apk info命令。它会打印出关于包或仓库的信息(以下输出已被缩略):

root@myhome:~# apk info --all bash
bash-5.1.16-r2 description:
The GNU Bourne Again shell
bash-5.1.16-r2 webpage:
https://www.gnu.org/software/bash/bash.xhtml
bash-5.1.16-r2 installed size:
1308 KiB
bash-5.1.16-r2 depends on:
/bin/sh
so:libc.musl-x86_64.so.1
so:libreadline.so.8
bash-5.1.16-r2 provides:
cmd:bash=5.1.16-r2.

在这一部分,我们介绍了包管理工具——在 Linux 发行版中管理软件的标准方式。在接下来的部分中,我们将介绍用户账户管理。

用户管理

在 Linux 系统中,用户由三组文件定义:

  • /etc/passwd:此文件包含关于用户的信息 —— 即用户的名称、唯一的数字 IDUID)、用户所属的主要组的 GID、主目录的路径以及用户登录时加载的 shell。一个典型的条目如下所示:
   admin:x:1000:1000:Debian:/home/admin:/bin/bash

每一行描述一个用户。字段通过冒号分隔。第二个字段在非常特殊的情况下才会包含除了 x 以外的其他内容。在这里,x 表示密码存储在 /etc/shadow 文件中。原因是,/etc/passwd 文件的权限必须稍微宽松一些,以便登录过程能够正常工作。/etc/shadow 只能被 root 用户和 root 组读取,并且只能由 root 用户写入:

    root@myhome:~# ls -ahl /etc/passwd
    -rw-r--r-- 1 root root 1.6K Aug 22 18:38 /etc/passwd
    root@myhome:~# ls -ahl /etc/shadow
    -rw-r----- 1 root shadow 839 Aug 22 18:38 /etc/shadow
  • /etc/shadow -:此文件包含加密的密码。正如前面提到的,为了安全起见,只有 root 用户可以读取和写入此文件。

  • /etc/group -:此文件包含有关用户所属组的信息。组就是那些已被归类在一起的账户,以便可以管理它们的权限。

你永远不应该手动修改这些文件,特别是 /etc/shadow 文件。正确的方法是使用 passwd 命令来更改它的内容。我们建议你阅读手册页获取更多信息。

有三个命令参与用户修改,另外有三个命令用于管理组:useradduserdelusermodgroupaddgroupdelgroupmod

添加用户

useradd 向系统添加一个用户账户。不同的选项修改该命令的行为。调用 useradd 命令的最常见版本会添加一个用户,创建其主目录,并指定默认的 shell:

root@myhome:~# useradd -md /home/auser -s /bin/bash auser

-m 告诉命令创建主目录,-d(这里它和 -m 一起传递了一个减号)告诉命令主目录应该是什么(注意是绝对路径),-s 指定默认的 shell。可以指定更多的参数,再次建议你查看手册页获取更多详细信息。

修改用户

usermod 修改现有的用户账户。它可以用来更改用户的组成员、主目录、锁定账户等。这里有一个有趣的选项是 -p 标志,它允许你非交互式地应用新密码。这在自动化中非常有用,当我们可能希望通过脚本或工具更新用户密码,而不是通过命令行。然而,这也存在安全风险:在命令执行期间,系统中的任何人都可以列出正在运行的进程及其参数,并查看密码条目。虽然这不会自动危及密码,因为密码必须通过 crypt (3) 函数加密后提供,但如果攻击者获得了加密的密码版本,他们可以运行密码破解程序进行攻击,最终通过暴力破解得到明文密码:

root@myhome:~# usermod -a -G admins auser

上述命令将把 auser 用户添加到名为 admins 的组中。-a 选项表示该用户将被添加到附加组(不会从其他已有的组中删除)。

删除用户

userdel 命令用于从系统中删除用户。它只能从系统文件中删除用户条目,并保持主目录不变,或者删除带有主目录的用户:

root@myhome:~# userdel -r auser

上述命令将删除用户及其主目录和所有文件。注意,如果用户仍然登录,它将不会被删除。

管理组

与管理用户类似,你可以在 Linux 系统中添加、删除和修改组。为此有等效的命令:

  • groupadd:在系统中创建一个组。组可以用于将用户归类,并指定其执行、目录或文件访问权限。

  • groupdel:从系统中删除该组。

  • groupmod:更改组定义。

你还可以使用 who 命令查看当前登录系统的用户:

admin@myhome:~$ who
ubuntu   tty1         2023-09-21 11:58
ubuntu   pts/0        2023-09-22 07:54 (1.2.3.4)
ubuntu   pts/1        2023-09-22 09:25 (5.6.7.8)
trochej  pts/2        2023-09-22 11:21 (10.11.12.13)

你还可以使用 id 命令查看当前登录用户的 UID 和 GID:

admin@myhome:~$ id
uid=1000(ubuntu) gid=1000(ubuntu) 
groups=1000(ubuntu),4(adm),20(dialout), 24(cdrom),25(floppy),27(sudo),29(audio), 30(dip),44(video),46(plugdev),119(netdev),120(lxd),123(docker)

执行此命令时不带任何选项,它将显示你的用户 ID 和用户所在的所有组。或者,你可以提供一个名称,查看该用户的 UID 或 GID:

admin@myhome:~$ id dnsmasq
uid=116(dnsmasq) gid=65534(nogroup) groups=65534(nogroup)

要查看主组的 ID 和 UID,你可以分别使用 -u-g 选项:

admin@myhome:~$ id -u
1000
admin@myhome:~$ id -g
1000

在本节中,我们介绍了用于管理 Linux 系统中用户帐户和组的命令。下一节将解释如何使用 SSH 安全地连接到远程系统。

安全外壳(SSH)协议

在 DevOps 世界中,几乎没有什么东西在你的笔记本电脑或 PC 上本地运行。要连接到远程系统,SSH 协议是公认的黄金标准。SSH 于 1995 年开发,作为一种安全的加密远程 shell 访问工具,用来取代像 telnetrsh 这样的明文工具。其主要原因是,在分布式网络中,监听通信过于容易,任何明文传输的内容都容易被截获。这包括诸如登录信息等重要数据。

Linux 世界中最常用的 SSH 服务器(和客户端)是 OpenSSH (www.openssh.com/)。截至撰写时,其他仍在维护的开源服务器包括 lsh (www.lysator.liu.se/~nisse/lsh/)、wolfSSH (www.wolfssl.com/products/wolfssh/) 和 Dropbear (matt.ucc.asn.au/dropbear/dropbear.xhtml)。

SSH 主要用于登录远程机器执行命令。但它也能够传输文件(22)。

配置 OpenSSH

安装 OpenSSH 服务器后,你的发行版将在 /etc/ssh/sshd_config 文件中放置一个基本配置。最基本的配置如下所示:

AuthorizedKeysFile .ssh/authorized_keys
AllowTcpForwarding no
GatewayPorts no
X11Forwarding no
Subsystem sftp /usr/lib/ssh/sftp-server

在继续选项之前,让我们先研究一下它们:

  • AuthorizedKeysFIle 告诉我们的服务器在用户目录中查找存储所有可以用来作为指定用户连接到该机器的公钥的文件。因此,如果你将公钥放在 AlphaOne 的主目录中,即 /home/AlphaOne/.ssh/authorized_keys,你将能够使用对应的私钥作为该用户进行连接(关于密钥的更多内容将在 创建和管理 SSH 密钥 小节中讨论)。

  • AllowTCPForwarding 将启用或禁用所有用户转发 TCP 端口的能力。端口转发用于访问那些无法直接在互联网上访问的远程机器,但你可以访问另一个可以连接的机器。这意味着你正在使用 SSH 主机作为所谓的跳跃主机来连接到私有网络,类似于使用 VPN。

  • GatewayPorts 是另一个直接与端口转发功能相关的选项。通过允许 GatewayPorts,你不仅可以将转发的端口暴露给你的机器,还可以暴露给你连接的网络中的其他主机。由于安全原因,不建议将此选项设置为 yes;你可能会不小心将私有网络暴露给你所在的网络,例如在咖啡馆中。

  • X11Forwarding 有一个非常特定的使用场景。通常情况下,你不希望在服务器上运行完整的图形界面,但如果你有这个需求,启用此选项后,你将能够登录到远程机器并启动远程图形应用程序,这些应用程序将像在本地主机上运行一样显示。

  • Subsystem 使你能够通过附加功能扩展 OpenSSH 服务器,例如在此案例中使用 SFTP。

在前面的命令块中没有指定的一个非常重要的选项是 PermitRootLogin,默认情况下设置为 prohibit-password。这意味着你将能够以 root 用户身份登录,但只有在需要使用公钥和私钥对进行身份验证时才可以登录。我们建议将此选项设置为 no,并且仅通过 sudo 命令允许 root 用户访问。

就是这些。当然,你可以添加更多高级配置,例如使用 man sshd_config 命令。

以同样的方式,你可以了解如何配置 SSH 客户端——也就是说,运行 man ssh_config

以下是一些非常有用的客户端选项:

# Show keys ascii graphics
VisualHostKey yes
# Offer only one Identity at a time
Host *
  ForwardAgent yes
  IdentitiesOnly yes
  IdentityFile ~/.ssh/mydefaultkey
# Automatically add all used keys to agent
AddKeysToAgent yes

VisualHostKey 设置为 yes 时,将显示服务器公钥的 ASCII 图形。你将连接的每个服务器都有一个唯一的密钥,因此会有独特的图形。它很有用,因为作为人类,我们非常擅长识别模式,因此如果你连接到 1.2.35.2 服务器,但打算进入不同的系统,你很可能通过看到与预期不同的图形来察觉到不对劲。

这里有一个示例:

root@myhome:~# ssh user@hosts
Host key fingerprint is SHA256:EppY0d4YBvXQhCk0f98n1tyM7fBoyRMQl2o3ife1pY
+--[ED25519 256]--+
|    .oB++o       |
|     +o*o ..     |
|    +..o*.+ .    |
|    .o +.= . +   |
|    ..o.S.= = .  |
|      .ooooE. .  |
|        .o.o     |
|                 |
|                 |
+----[SHA256]-----+

主机选项允许你为一个或多个服务器设置特定的选项。在这个示例中,我们启用了 SSH 代理转发,并禁用了所有服务器的密码登录。此外,我们还设置了一个默认的私钥,用于连接任何服务器。这将引出我们对 SSH 密钥和加密算法的讨论。

最后一个选项,AddKeysToAgent,意味着每当你使用(并解锁)一个密钥时,它也会被添加到 SSH 代理中,以供将来使用。这样,你就不需要在连接时指定密钥,也不必在每次连接尝试时解锁密钥。

创建和管理 SSH 密钥

SSH 由三个组件组成:传输层(传输控制协议 (TCP)/互联网协议 (IP)),用户身份验证层,以及一个连接层,可以有效地是多个独立传输数据的连接。

就不同的身份验证选项而言,你有一种基本的密码身份验证方式,证明它不够安全。还有公钥身份验证,我们将在这里讨论。剩下的两个是 keyboard-interactive通用安全服务应用程序接口 (GSSAPI)。

公钥身份验证要求我们生成一个密钥,它将有两个对应的部分:私钥和公钥。你将把公钥放在服务器的 authorized_keys 文件中;私钥则用于身份验证。

在编写本书时,RSA 密钥是与 SSH 一起使用的标准。它是安全的,但建议使用更大的密钥,长度为 4,096 位,但 3,072 位(默认)也被认为足够。更大的密钥意味着更慢的连接速度。

当前,更好的选择是使用 ed25519 类型的密钥,它具有固定长度。

此外,所有密钥都可以用密码进行保护。

以下代码展示了如何生成这两种密钥类型:

root@myhome:~# ssh-keygen -b 4096 -o -a 500 -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:eMu9AMUjpbQQ7DJIl4MB2UnpfiUqwZJi+/e9dsKWXZg root@614bbd02e559
The key's randomart image is:
+---[RSA 4096]----+
|o=+++.. .        |
|.o++.o =         |
|o+... + +        |
|*o+ o .+ .       |
|+o.+ oo S   o    |
|..o .  + o E .   |
| ...    = + .    |
|   . .  .B +     |
|    . ..oo=      |
+----[SHA256]-----+
root@myhome:~# ssh-keygen -o -a 100 -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/root/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_ed25519
Your public key has been saved in /root/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:EOCjeyRRIz+hZy6Pbtcnjan2lucJ2Mdih/rFc/ZnthI
root@614bbd02e559
The key's randomart image is:
+--[ED25519 256]--+
|  . =..          |
|   * o .         |
|  o B .          |
|   * o .         |
|  + o   S        |
|   B o +    E    |
|  o +.*=B o  .   |
| ...ooB*+= .. +  |
| ..oo=o=o   .=.. |
+----[SHA256]-----+

现在,要将这个新创建的密钥放到服务器上,你需要手动将其复制到 authorized_keys 文件中,或者使用 ssh-copy-id 命令,如果你已经有其他访问手段(如密码身份验证),它会为你完成此操作:

root@myhome:~# ssh-copy-id -i ~/.ssh/id_ed25519 user@remote-host

你只能通过使用密钥执行下次登录到此服务器的操作。

到目前为止,你应该已经很好地理解了 SSH 的工作原理以及如何使用它的最常用功能。你现在知道如何创建密钥以及如何在远程系统上保存它们。

总结

本章结束了我们对日常工作中所需的基本 Linux 操作的介绍。虽然这并没有全面解释你管理 Linux 系统所需了解的所有内容,但足以帮助你入门,并帮助你管理系统。在下一章中,我们将从头开始讲解如何编写 shell 脚本,并指导你学习基本以及更高级的主题。

练习

通过以下练习来测试你对本章内容的掌握:

  1. 在 Debian/Ubuntu 中,安装vim软件包。

  2. 创建一个定时任务,每周六上午 10:00 生成一个名为/tmp/cronfile的文件。

  3. 创建一个名为admins的组,并将一个现有用户添加到该组中。

第四章:使用 Shell 脚本自动化

在本章中,我们将通过 shell 脚本演示系统管理任务的自动化。我们将展示使用 Bash shell 处理脚本的几种方法。计划是创建一个脚本来自动化数据库转储的创建。这个任务虽然简单,但将演示如何处理可能出现的问题,并且如何应对这些情况。

在本章中,我们将涵盖以下主题:

  • 备份数据库

  • 理解脚本编写

  • 理解 Bash 内建命令和语法

  • 理解备份脚本——第一步

  • 处理错误和调试

技术要求

对于本章内容,你将需要一个可以安装软件包的 Linux 系统,并且不害怕在过程中出错。为此,虚拟机是最理想的,通常是在一台可以从头开始重新安装的旧计算机上运行。我们不指望会出错,但在学习过程中,可能会发生这种情况。

备份数据库

对于最常见的数据库,如 MySQL 和 PostgreSQL,至少有两种不同的方式来备份数据库:

  • 通过提取当前的所有数据和数据库架构来进行数据库转储

  • 复制复制日志

在云环境中,你也可以对存储备份的磁盘数据库进行快照。

数据库转储也可以作为完整备份使用。复制日志并不是独立的数据库转储,因此你需要将它们与完整备份结合使用。这叫做增量备份。

进行完整备份可能会花费很长时间,尤其是对于大型数据库。在备份过程中,数据库会锁定其数据文件,因此不会将新数据保存到磁盘上;相反,所有数据都会存储在复制日志中,直到数据库锁被释放。对于大型数据库,这个过程可能需要数小时。鉴于此,我们将每周进行一次完整备份,并每小时复制一次所有复制日志。此外,我们还会每天创建一次 AWS 数据库实例的磁盘快照。

有了这些知识,我们可以开始创建最基本版本的脚本了。

理解脚本编写

Shell 脚本是一个简单的文本文件,里面包含了命令。与编译程序不同,shell 脚本在执行之前并不会被解析,而是在执行时解析。这使得开发过程非常快速——完全没有编译过程。但同时,执行速度稍慢。而且,编译器能够捕获的错误会在执行时显现,通常会导致脚本退出。

优点是,编写脚本时需要学习的东西并不多——远远少于用 C 或 Python 编写程序时。与系统命令的交互就像是直接输入命令的名称一样简单。

Bash 编程语言缺乏许多复杂性:它没有很多数据类型和结构,作用域控制非常基础,内存实现也不是为了在大规模下高效。

没有一个固定的经验法则来选择何时编写脚本,何时开发程序。然而,有一些要点需要考虑。适合用 Shell 脚本的一个好候选条件如下:

  • 它不会很长。我们不能给你一个具体的规则,但当你的代码行数达到数百行时,考虑使用 Python 或将其拆分成多个脚本可能是个好主意。

  • 它与系统命令进行交互,有时会进行很多交互。你可以将其视为一种自动化运行这些命令的方式。

  • 它不做大量的数据处理。只有少数的数组、字符串和数字,仅此而已。

  • 它不执行任何系统调用。Shell 脚本不是用来做系统调用的,也没有直接的方法来做到这一点。

Shell 脚本的基本结构如下:

#!/bin/bash
echo "Hello world"

第一行以所谓的 she-bang 开头。它用于告诉系统使用哪个解释器(在本例中是 Bash)来运行这个脚本。在很多网上的脚本中,she-bang 看起来像这样:

#!/usr/bin/env bash

使用 env 命令既有一个很大的优势,也有一个劣势。使用它的优点是它会使用当前用户 PATH 环境变量中排在最前面的 Bash 可执行文件。具体取决于目的。env 命令还不允许你传递任何参数给你选择的解释器。

上面脚本的第二行简单地显示了Hello world。它使用了一个内置命令 echo,它的作用就是显示你输入的任何文本。

现在,要执行这个脚本,我们需要将其保存到一个文件中。最好以 .sh.bash 后缀来结束这个文件。执行这个新脚本有两种方式——通过调用解释器并传入脚本名,或者直接通过脚本名执行:

admin@myhome:~$ bash ./myscript.sh
Hello world
admin@myhome:~$

要直接执行脚本,我们需要更改其权限,使其可以被执行;否则,系统将无法识别它为可执行文件:

admin@myhome:~$ chmod +x myscript.sh
admin@myhome:~$ ./myscript.sh
Hello world
admin@myhome:~$

类似地,我们可以轻松地将 Python 或任何其他 Shell 设置为我们脚本的解释器。

现在,让我们聚焦于 Bash,并看一些我们将要使用的 Bash 内置命令。

理解 Bash 内置命令和语法

在我们开始创建脚本之前,让我们回归基础。首先,我们将探讨 Bash 脚本语言的语法及其局限性。

内置命令是与 Bash 紧密相关的命令,是我们将要使用的主要脚本语法。Bash 会尝试执行它运行所在系统中的任何其他命令。

就像其他任何解释型语言(例如 Python 或 Ruby)一样,Bash 有其独特的语法和语法规则。让我们来看看。

Bash,类似于其他编程语言,从上到下、从左到右解释文件。每行通常包含一个或多个命令。你可以使用管道符号(|)或双管道符号(||)、分号(;)或双和符号(&&)将多个命令连接在同一行中。记住,双管道的作用与逻辑OR相同,而双和符号的作用与逻辑AND相同。这种方式可以让你按顺序运行命令,并根据前一个命令的结果执行下一个命令,而不需要使用更复杂的条件语句。这被称为命令列表或命令链:

commandA && commandB

在前面的命令中,你可以看到使用双和符号的示例。在这里,commandB 只有在 commandA 成功执行后才会执行。我们可以通过在命令链的末尾添加||来继续连接更多的命令。

commandA || commandB

另一方面,这个示例展示了如何使用双管道。在这里,commandB 只有在 commandA 执行失败时才会执行。

Bash(或 Linux 中的任何其他 shell)通过使用返回码来判断一个命令是否执行失败或成功退出。每个命令需要以一个正整数退出——零(0)是成功的代码,任何其他代码都是失败的。如果你使用AND(&&)OR(||)将多个命令连接在一起,则整行的返回状态将由先前执行的命令的返回状态决定。

那么命令后面的单个和符号(&)呢?它有一个完全不同的功能——它会在后台执行命令,并且脚本会继续运行,而无需等待命令完成。这对于不需要其他部分程序完成的任务,或者同时运行多个相同命令的实例(例如同时进行多个数据库的完整备份以缩短执行时间)非常有用。

现在我们知道如何连接命令,接下来我们可以了解任何编程语言的另一个核心特性——变量。

变量

在 Bash 中,有两种类型的变量:全局变量和局部变量。全局变量在脚本运行期间可以访问,除非该变量被取消设置。局部变量仅在脚本的某个块中可访问(例如,定义的函数)。

每当脚本执行时,它会从当前运行的 shell 中获取一组现有的变量;这被称为环境。你可以使用export命令将新变量添加到环境中,使用use unset命令移除变量。你也可以使用declare -x命令将函数添加到环境中。

所有的参数,无论是局部的还是全局的,都以美元符号($)为前缀。所以,如果你有一个名为 CARS(区分大小写)的变量,引用它时,需要在脚本中写成$CARS

对于变量,单引号或双引号(或没有引号)是重要的。如果你将变量放在单引号中,它将不会被展开,而引号中的变量名将被当作字面字符串处理。双引号中的变量会被展开,并且这被认为是一种安全的引用变量的方式(以及连接它们,或者将它们拼接在一起),因为如果字符串中有空格或其他特殊字符,它们对脚本不会有实际意义 —— 也就是说,它们不会被执行。

有时,你需要将多个变量连接起来。你可以使用大括号({})来完成。例如,"${VAR1}${VAR2}" 将扩展为你设置的 VAR1VAR2 的值。在这种情况下,你也可以使用大括号来截取或替换字符串的部分。这里有一个例子:

name="John"
welcome_str="Hello ${name}"
echo "${welcome_str%John}Jack"

上述代码将显示 % 操作符只会移除字符串末尾的字符。如果你想从字符串开头截取变量,你可以使用 # 操作符,方法相同。

如果你在没有引号的情况下引用变量,变量值中的空格可能会破坏脚本的流程并妨碍调试,因此我们强烈建议使用单引号或双引号。

参数

我们可以使用两种类型的参数,但它们都是特殊类型的变量,因此每个变量前都有一个美元符号($)。

你需要注意的第一类参数是 位置参数 —— 这些是传递给脚本或脚本内函数的参数。所有参数从 1 开始索引,一直到 n,其中 n 是最后一个参数。你可以用 $1$n 引用每一个参数。你可能会好奇,如果使用 $0 会发生什么。

$0 包含脚本的名称,因此它对于在脚本中生成文档等操作非常有用。

要引用从 1 开始的所有可用参数,你可以使用 $@(美元符号,at 符号)。

以下是一些其他常用的特殊参数:

  • #:位置参数的数量

  • ?:最近执行的前台命令的退出代码

  • $:Shell 的进程 ID

循环

你可能熟悉其他编程语言中的不同类型的循环。在 Bash 中也有这些循环,但语法稍有不同。

最基本的 for 循环 看起来像这样:

for variable_name in other_variable; do some_commands; done

这个变种的 for 循环将 other_variable 中的每个元素设置为 variable_name 的值,并为每个找到的元素执行 some_commands。执行完成后,它将以最后执行的命令的状态退出循环。in other_variable 部分是可以省略的 —— 在这种情况下,for 循环会为每个位置参数执行一次 some_commands。使用该参数的示例如下:

for variable_name; do some_commands; done

上述 for 循环将根据你为函数(或此情况下的脚本)添加的输入变量执行多次。你可以使用 $@ 引用所有位置参数。

以下是一个 C/C++风格的for循环:

for (( var1 ; var2 ; var3 )); do some_commands; done

下面是此语法的一个示例:

for ((i=1; i<=5; i++)); do echo $i; done

第一个表达式将i变量设置为1,第二个表达式是循环继续运行的条件,最后一个表达式将i变量的值增加 1。每次循环运行时,会显示分配给i变量的下一个值。

此命令的输出将如下所示:

1
2
3
4
5

另一个有用的循环是while循环,它会根据需要运行多次,直到满足条件(传递给它的命令成功退出——返回零)。与之对应的是until循环,它会一直运行,直到传递给它的命令返回非零状态:

while some_command; do some_command; done
until some_command; do some_command; done

你可以通过使用始终满足条件的命令来创建一个无限循环,对于while循环,条件可以简单地设为true

最常用的命令块是条件语句,它们与if语句一起使用。让我们仔细看看。

条件执行——if语句

if语句的语法如下:

if test_command
then
  executed_if_test_was_true
fi

test_command 可以是你能想到的任何命令,但通常,测试是用双方括号或单方括号包围的。这两者之间的区别是前者是一个系统命令,叫做test(你可以通过执行man test来查看它的语法),而后者是 Bash 的内建命令,功能更强大。

放置变量在方括号中的经验法则是使用双引号,这样即使变量包含空格,也不会改变我们的测试意图:

if [[ -z "$SOME_VAR" ]]; then
    echo "Variable SOME_VAR is empty"
fi

-z 测试检查$SOME_VAR变量是否为空。如果变量为空,评估为true,否则为false

以下是其他常用的测试:

  • -a:逻辑“与”

  • -o:逻辑“或”

  • -eq:等于

  • -ne:不等于

  • -gt 或 >:大于

  • -ge 或 >=:大于或等于

  • -lt 或 <:小于

  • -le 或 <=:小于或等于

  • = 或 ==:等于

  • !=:不等于

  • -z:字符串为空(其长度为零字符)

  • -n:字符串不为空

  • -e:文件存在(目录、符号链接、设备文件或文件系统中的任何其他文件)

  • -f:文件是常规文件(不是目录或设备文件)

  • -d:文件是目录

  • -h-L:文件是符号链接

  • -r:文件具有读取权限(对于执行测试的用户)

  • -w:文件具有写权限(对于执行测试的用户)

  • -x:文件可以被执行脚本的用户执行

注意,使用系统测试(单方括号,[...])时,测试可能与内建测试(双方括号,[[...]])行为不同。

双等号比较运算符,在使用通配符匹配字符串时,将根据你是否对模式加引号来匹配模式或字面字符串。

以下是一个模式匹配的例子,假设字符串以w开头:

if [[ $variable == w* ]];
    echo "Starts with w"
fi

当使用系统测试(单个方括号)而不是内置测试时,测试将尝试查找$变量是否与本地目录中的任何文件名匹配(包括带有空格的文件)。这可能会导致一些不可预测的结果。

以下是模式匹配的示例,如果字符串为w*

if [[ $variable == "w*" ]];
    echo "String is literally 'w*'"
fi

拥有这些知识后,我们已经准备好开始创建和运行脚本了。让我们直接开始吧!

理解备份脚本 – 第一步

既然我们知道脚本的样子了,就可以开始编写脚本了。你可以使用你喜欢的控制台编辑器或 IDE 来做这件事。让我们创建一个名为run_backups.sh的空文件,并更改其权限,使其可执行:

admin@myhome:~$ touch run_backups.sh && chmod +x run_backups.sh
admin@myhome:~$ ls -l run_backups.sh
-rwxr-xr-x  1 admin  admin  0 Dec  1 15:56 run_backups.sh

这是一个空文件,因此我们需要添加一个基本的数据库备份命令,并从那里继续。我们不会讨论如何授予该脚本访问数据库的权限。我们将备份一个 PostgreSQL 数据库,并使用pg_dump工具来实现。

让我们在基本脚本中输入一个 shebang 行并调用pg_dump命令:

#!/usr/bin/env bash
pg_dump mydatabase > mydatabase.sql

要执行此脚本,我们需要启动以下命令:

admin@myhome:~$ ./run_backups.sh

点号和斜杠表示我们要执行当前目录中的某个文件,文件名为run_backups.sh。如果没有最初的点斜杠对,当前运行的 shell(这里是bash)将查找PATH环境变量,并尝试在其中列出的目录中查找我们的脚本:

admin@myhome:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

如你所见,这是一个由冒号分隔的目录列表。

现在,让我们看看执行时我们的 Bash 脚本做了什么:

admin@myhome:~$ ./run_backups.sh
./run_backups.sh: line 3: pg_dump: command not found

除非你系统中已安装pg_dump,否则你将看到此错误。这意味着 Bash 没有找到我们打算运行的命令。它还会显示错误发生的行。此外,一个空的mydatabase.sql文件被创建了。

通常,我们会创建一个包含所有所需工具的 Docker 镜像,另一个镜像运行 PostgreSQL 数据库。但由于这一部分将在第八章中讲解,我们就直接继续安装所有所需的软件包到本地机器上吧。假设你使用的是 Ubuntu 或 Debian Linux 系统,你可以运行以下命令:

admin@myhome:~$ sudo apt-get update
Get:1 http://archive.ubuntu.com/ubuntu jammy InRelease [270 kB]
[Condensed for brevity]
Get:18 http://archive.ubuntu.com/ubuntu jammy-backports/main amd64 Packages [3520 B]
Fetched 24.9 MB in 6s (4016 kB/s)
Reading package lists... Done
admin@myhome:~$ sudo apt-get install postgresql
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  cron libbsd0 libcommon-sense-perl libedit2 libgdbm-compat4 libgdbm6 libicu70 libjson-perl libjson-xs-perl libldap-2.5-0 libldap-common libllvm14 libmd0 libperl5.34 libpopt0 libpq5 libreadline8
[Condensed for brevity]
Suggested packages:
  anacron checksecurity default-mta | mail-transport-agent gdbm-l10n libsasl2-modules-gssapi-mit | libsasl2-modules-gssapi-heimdal libsasl2-modules-ldap libsasl2-modules-otp libsasl2-modules-sql
[Condensed for brevity]
0 upgraded, 42 newly installed, 0 to remove and 2 not upgraded.
Need to get 68.8 MB of archives.
After this operation, 274 MB of additional disk space will be used.
Do you want to continue? [Y/n] y

经用户确认后,数据库将被安装、配置,并在后台启动。为了可读性,我们已经截断了后续输出。

安装完成后,可能还需要对数据库做一些额外的配置更改,以便你能够使用另一个名为psql的工具连接到数据库。psql是一个用于连接 PostgreSQL 的控制台命令。在/etc/postgresql/14/main/pg_hba.conf文件中,我们定义了信任关系以及谁可以使用多种机制连接到数据库。

查找以下行:

local   all  postgres peer

将其更改为以下内容:

local   all  all  trust

做出此修改后,你可以使用以下命令重启数据库:

admin@myhome:~$ sudo systemctl restart postgresql
* Restarting PostgreSQL 14 database server

现在,你应该能够登录到数据库并列出所有可用的数据库:

admin@myhome:~$ psql -U postgres postgres
psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1))
Type "help" for help.
postgres=# \l
                              List of databases
   Name    |  Owner   | Encoding | Collate |  Ctype  |   Access privileges
-----------+----------+----------+---------+---------+-----------------------
 postgres  | postgres | UTF8     | C.UTF-8 | C.UTF-8 |
 template0 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres          +
           |          |          |         |         | postgres=CTc/postgres
 template1 | postgres | UTF8     | C.UTF-8 | C.UTF-8 | =c/postgres          +
           |          |          |         |         |
postgres=CTc/postgres
(3 rows)
postgres=# \q
admin@myhome:~$

登录后,我们使用 \l(反斜杠,小写 L)列出所有可用的数据库,并使用 \q(反斜杠,小写 Q)退出 psql shell。一旦设置完成,我们可以回到脚本并再次尝试运行它:

admin@myhome:~$ ./run_backups.sh
pg_dump: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL:  role "root" does not exist

PostgreSQL 中没有 root 角色,这是此时预期的错误。我们需要使用不同的角色连接到数据库。默认角色是 postgres,传递给 pg_dump 的选项是 -U,这与我们在 psql 中使用的相同。更新后,我们的脚本将如下所示:

#!/usr/bin/env bash
pg_dump -U postgres mydatabase > mydatabase.sql

最后一步是创建一个数据库并生成一些实际数据,以确保输出的 sql 文件不会为空。以下脚本将创建一个名为 mydatabase 的数据库,并创建两个包含随机数据的表:

CREATE DATABASE mydatabase;
\c mydatabase
CREATE TABLE random_data AS SELECT data_series, md5(random()::text) from generate_series(1,100000) data_series;
CREATE TABLE another_random AS SELECT data_series, md5(random()::text) from generate_series(1,100000) data_series;

CREATE DATABASE 这一行正在创建一个名为 mydatabase 的数据库。第二行表示我们正在连接到这个新数据库。接下来的两行以 CREATE TABLE 开头,分别创建表并使用内置的 PostgreSQL 函数填充数据。让我们将其分解为两个独立的查询——SELECTCREATE

SELECT data_series, md5(random()::text) from generate_series(1,100000) data_series;

这里发生了几件事:

  • generate_series() 函数正在创建一个从 1 到 100,000 的整数序列——这将生成表中的所有记录

  • data_series 关键字位于分号之前,命名了来自 generate_series() 函数的输出,因此它是我们打算创建的表中的实际字段名。

  • random() 函数生成一个介于 0 和 1 之间的值——即大于或等于 0 小于 1

  • random() 函数后的 ::text 将此函数的输出转换为文本格式

  • md5() 函数将 random()::text 的输出进行哈希处理,使用 md5 算法,确保我们获得一个唯一的字符串,并运行的次数与 generate_series() 函数的输出数量一致(这里是从 1 到 100,000)

  • 最后,SELECT data_series, md5() 正在生成一个包含两列(data_seriesmd5)的表,这两列的数据由两个函数生成

现在,回到 CREATE TABLE,有一部分叫做 another_random AS —— 这将从 SELECT 获取输出,并为我们创建一个表。

有了这些知识,我们可以创建一个 sql 脚本并使用 psql 执行它:

admin@myhome:~$ psql -U postgres < create_db.sql
CREATE DATABASE
You are now connected to database "mydatabase" as user "postgres".

要检查我们是否创建了某些内容并查看我们创建的数据,我们再次需要使用 psql 和对新数据库的 SELECT 查询:

admin@myhome:~$ psql (14.1)
Type "help" for help.
postgres=# \c mydatabase
You are now connected to database "mydatabase" as user "postgres".
mydatabase=# \dt
             List of relations
 Schema |      Name      | Type  |  Owner
--------+----------------+-------+----------
 public | another_random | table | postgres
 public | random_data    | table | postgres
(2 rows)
mydatabase=# select * from random_data ;
 data_series |               md5
-------------+----------------------------------
           1 | 4c250205e8f6d5396167ec69e3436d21
           2 | a5d562ccd600b3c4c70149361d3ab307
           3 | 7d363fac3c83d35733566672c765317f
           4 | 2fd7d594e6d972698038f88d790e9a35
--More--

前面的输出末尾的 --More-- 表示还有更多记录要显示。你可以按空格键查看更多数据,或按 Q 键退出。

一旦你创建了一个数据库并填充了一些数据,你可以尝试再次运行我们的备份脚本:

admin@myhome:~$ ./run_backup.sh
admin@myhome:~$

没有错误,所以我们可能已经成功创建了完整的数据库转储:

admin@myhome:~$ ls -l mydatabase.sql
-rw-r--r--    1 root     root      39978060 Dec 15 10:30 mydatabase.sql

输出文件不是空的;让我们看看里面有什么:

admin@myhome:~$ head mydatabase.sql
--
-- PostgreSQL database dump
--
-- Dumped from database version 14.1
-- Dumped by pg_dump version 14.1
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;

在一些 SET 语句之后,你还应该找到 CREATE TABLEINSERT 语句。由于输出内容较多,我没有提供完整的输出。

在本节中,我们学习了如何为脚本设置测试环境,并使得脚本能够创建数据库转储。在下一节中,我们将更多地关注错误处理,检查备份是否成功。

错误处理和调试

在运行备份脚本时,我们可能会遇到几种错误:数据库访问可能被阻止,pg_dump 进程可能被杀死,磁盘空间可能不足,或者任何其他错误导致我们无法完成完整的数据库转储。

在任何这些情况下,我们需要捕获错误并优雅地处理它。

此外,我们可能需要重构脚本,使其具有配置功能,使用函数,并进行调试。调试将非常有用,特别是在处理较大的脚本时。

让我们深入了解并开始添加一个函数:

#!/usr/bin/env bash
function run_dump() {
  database_name=$1
  pg_dump -U postgres $database_name > $database_name.sql
}
run_dump mydatabase

我们添加了一个 run_dump 函数,它接受一个参数,并将该参数的内容设置为一个名为 database_name 的局部变量。然后它使用这个局部变量将选项传递给 pg_dump 命令。

这将立即允许我们通过使用 for 循环来备份多个数据库,代码如下:

for dbname in mydatabase mydatabase2 mydatabase3; do
    run_dump $dbname
done

这个循环将创建数据库 mydatabasemydatabase2mydatabase3 的完整转储。备份将逐个完成,通过此函数。我们现在可以将数据库列表放入变量中,以使其更具配置性。当前脚本将如下所示:

#!/usr/bin/env bash
databases="mydatabase"
function run_dump() {
  database_name=$1
  pg_dump -U postgres $database_name > $database_name.sql
}
for database in $databases; do
  run_dump "$database"
done

现在,这个备份脚本变得更加复杂。我们需要注意接下来会发生的一些事情:

  • 如果任何备份失败,脚本将继续运行

  • 如果备份由于 pg_dump 无法访问数据库而失败,我们将覆盖之前的数据库转储

  • 我们将在每次运行时覆盖转储文件

在脚本中,几项默认设置被认为是良好的实践需要覆盖。我们提到的第一个问题可以通过在任何命令返回一个与零(或 true)不同的值时终止运行来减轻。这意味着该命令执行时出现了错误。此选项名为 errexit,我们可以通过 set 命令覆盖它,set 是 Bash 内建命令。我们可以通过两种方式来实现这一点:

set -o errexit
set -e

这里是我们推荐使用的一些其他选项:

  • set -u:这将把我们在脚本中尝试使用的任何未设置的变量视为错误

  • set -o pipefail:当使用管道链式命令时,这个管道的退出状态将是最后一个命令的状态,如果该命令以非零状态结束,或者如果所有命令都成功执行(即退出状态为零),则为零(成功)。

  • set -Cset -o noclobber:如果启用,Bash 将不会使用重定向命令(例如我们在脚本中使用的 >)覆盖任何现有文件

一个非常有用的附加选项是 set -xset -o xtrace,它使得 Bash 在执行每个命令之前打印该命令。

让我们看看一个简单 Bash 脚本的工作原理:

#!/usr/bin/env bash
set -x
echo "Hello!"

这是执行该脚本后的输出:

admin@myhome:~$ ./simple_script.sh
+ echo 'Hello!'
Hello!

让我们用推荐的 Bash 设置更新我们的备份脚本:

#!/usr/bin/env bash
set -u
set -o pipefail
set -C
databases="mydatabase"
function run_dump() {
  database_name=$1
  pg_dump -U postgres $database_name > $database_name.sql
}
for database in $databases; do
  run_dump "$database"
done

现在,让我们回到控制台测试它是否仍然按预期工作:

admin@myhome:~$ ./run_backups.sh
./run_backups.sh: line 12: mydatabase.sql: cannot overwrite existing file

我们已启用 noclobber 选项,它已防止我们覆盖先前创建的备份。我们需要重命名或删除旧文件才能继续。现在,我们还将启用 xtrace 选项,以查看正在执行的命令脚本:

admin@myhome:~$ rm mydatabase.sql
admin@myhome:~$ bash -x ./run_backups.sh
+ set -u
+ set -o pipefail
+ set -C
+ databases=mydatabase
+ for database in $databases
+ run_dump mydatabase
+ database_name=mydatabase
+ pg_dump -U postgres mydatabase

为了避免覆盖现有文件错误,我们可以采取以下三种方法之一:

  • 在尝试运行备份之前删除旧文件,这将销毁以前的备份。

  • 重命名上一个备份文件并添加当前日期后缀。

  • 确保每次运行脚本时,转储文件都有一个不同的名称,例如当前日期。这将确保我们保留以前的备份,以防需要恢复到比上次完整备份更早的版本。

在这种情况下,最常见的解决方案是我们提出的最后一个——每次备份运行时生成不同的备份文件名。首先,让我们尝试获取一个带有本地日期和时间的时间戳,格式为 YYYYMMDD_HHMM,我们有以下选项:

  • YYYY:当前年份的四位数字格式

  • MM:当前月份的两位数字格式

  • DD:两位数格式的日期

  • HH:当前小时

  • MM:当前分钟

我们可以通过使用date命令来实现这一点。默认情况下,它将返回当前的日期、星期几和时区:

admin@myhome:~$ date
Fri Dec 16 14:51:34 UTC 2022

为了更改该命令的默认输出,我们需要使用格式字符传递一个日期格式字符串。

date 命令的最常见格式字符如下:

  • %Y:年份(例如,2022)

  • %m:月份(01-12)

  • %B:长月份名称(例如,January)

  • %b:短月份名称(例如,Jan)

  • %d:日期(例如,01-31,取决于某个月的天数)

  • %j:年份中的天数(例如,001-366)

  • %u:星期几(1-7)

  • %A:完整的星期几名称(例如,Friday)

  • %a:短星期几名称(例如,Fri)

  • %H:小时(00-23)

  • %I:小时(01-12)

  • %M:分钟(00-59)

  • %S:秒(00-59)

  • %D:以 mm/dd/yy 格式显示日期

要按照我们希望的格式格式化日期,我们需要使用格式字符 %Y%m%d_%H%M,并将其传递给 date 命令进行解释:

admin@myhome:~$ date +"%Y%m%d_%H%M"
20221216_1504

为了将输出字符串传递给脚本中的变量,我们需要在子 shell 中运行 date(由我们的 Bash 脚本执行的 Bash 进程):

timestamp=$(date +"%Y%m%d_%H%M")

让我们将其放入脚本中,并使用timestamp变量来生成输出文件名:

#!/usr/bin/env bash
set -u
set -o pipefail
set -C
timestamp=$(date +"%Yum'd_%H%M")
databases="mydatabase"
function run_dump() {
  database_name="$1"
  pg_dump -U postgres "$database_name" > "${database_name}_${timestamp}".sql
}
for database in $databases; do
  run_dump "$database"
done

如果你在pg_dump命令行中看到变量之间有大括号,可能会想知道为什么我们需要它们。我们使用大括号确保变量名称在扩展为字符串时是正确的。在我们的案例中,我们防止了 Bash 尝试搜索一个不存在的变量名$database_name_

现在,每次运行备份脚本时,它都会尝试创建一个带有当前日期和备份开始时间的文件。如果我们每天运行这个脚本,文件数量会随时间增加,最终填满我们的磁盘空间。所以,我们还需要让脚本删除旧备份——比如,删除 14 天及以上的备份。

我们可以通过使用find命令来实现这一点。让我们找到所有以数据库名开头,后跟下划线,且以.sql结尾的文件,这些文件的修改时间超过 14 天:

admin@myhome:~$ find . -name "mydatabase_*.sql" -type f -mtime +14
./mydatabase_20221107.sql

find命令有一种独特的语法,与其他命令行工具略有不同,所以我们来描述一下每个选项的含义:

  • .(一个点):这是我们希望搜索文件的目录。点代表当前目录

  • -name:此选项可以接受完整字符串或通配符,如*?,用于查找文件名。它是区分大小写的。如果我们不确定正在查找的文件或目录是大写还是小写,可以使用-iname选项代替。

  • -type f:这表示我们在寻找一个常规文件。其他选项如下:

    • d:目录

    • l:符号链接

    • s:套接字文件

    • p:FIFO 文件

    • b:块设备

    • c:字符设备

  • -mtime +14:此文件的修改时间应早于 14 天。此选项还可以接受其他单位(秒,s;周,w;小时,h;天,d——如果未提供单位,则默认为天)。

要删除找到的文件,我们至少有两种选择:使用-delete选项或通过-exec find选项使用rm命令。让我们看看在这两种情况下的表现:

admin@myhome:~$ find . -name "mydatabase_*.sql" -type f -mtime +14 -delete
admin@myhome:~$ find . -name "mydatabase_*.sql" -type f -mtime +14 -exec rm -- {} \;
  • 在这种情况下,更安全的选择是使用-execdir而不是-exec。它们的区别微妙但重要:-exec不会在找到的文件所在的同一目录中执行,而-execdir会,这使得它在边缘情况下更安全。

让我们解析一下-exec选项之后的内容:

  • rm:这是一个 CLI 工具,用于删除文件或目录。

  • --(双破折号):这表示将从stdin获取参数,或find命令的输出。

  • {}:这将替代我们找到的文件名。

  • \;(反斜杠,分号):这将允许多个命令由-exec执行。反斜杠是一个转义字符,可以防止该分号被解释为下一个命令的分隔符。find工具使用;+来终止 Shell 命令,因此我们可以将其标记为";"\++(不带引号)。

  • -delete选项用于删除文件,但它总是返回true,因此如果例如我们的脚本没有删除文件的权限,它会悄无声息地失败。在我们的脚本中使用它相对安全,所以我们会继续使用它。

现在,让我们将这个嵌入到我们的脚本中,看看它的最终版本:

#!/usr/bin/env bash
set -u
set -o pipefail
set -C
timestamp=$(date +"%Y%m%d_%H%M")
databases="mydatabase"
function cleanup_old_backups() {
  database_name="$1"
  find . -type f -name "${database_name}_*.sql" -mtime +14 -delete
}
function run_dump() {
  database_name="$1"
  pg_dump -U postgres "$database_name" > "${database_name}_${timestamp}".sql
}
for database in $databases; do
  cleanup_old_backups "$database"
  run_dump "$database"
done

在这里,我们添加了一个名为cleanup_old_backups的函数,它会在创建新转储之前运行,以释放一些空间来存放新的文件。我们在run_dump之前的 for 循环中调用了这个函数。这个脚本可以通过cron 守护进程systemd cron服务自动运行;我们将在第五章在 Linux 中管理服务中更详细地讨论这一点。

在本节中,我们了解了在 Shell 脚本中建议启用的选项以及如何启用调试选项。我们现在知道如何创建函数和循环。此外,我们也部分了解了 PostgreSQL 以及如何创建测试数据库。

下一章将带领我们深入了解各种 Linux 服务,以及如何使用initsystemd来管理它们。

总结

Shell 脚本是一种在 Linux 系统中自动化定期执行任务的常见方式。有时,它会演变成一个更大的系统,通过多个 Bash 脚本和 Python 程序连接起来,完成复杂任务,同时利用多个小任务以非常可靠的方式同时做一件事。

在现代系统中,你可能会看到和 Bash 一样多的 Python 脚本。

在这一章中,我们学习了如何创建可执行脚本,并且如何创建一个简单的备份脚本,处理错误并在每次运行时生成一个新的文件名。我们还添加了一个函数来删除旧的备份,以避免填满磁盘空间。此外,作为附带效果,我们学习了如何创建一个新的 PostgreSQL 数据库,并允许本地系统访问它。

在下一章,我们将学习如何创建 Linux 服务以及如何管理它们。

练习

尝试以下练习,以测试你对本章内容的理解:

  1. 编写一个函数,列出所有数据库,并将该列表传递给我们创建的 for 循环。

  2. 将日期时间戳转换为您选择的另一种格式。

  3. 捕捉find函数可能返回的任何错误(例如,它无法删除文件)。

第二部分:日常 DevOps 工具

在第二部分中,我们将学习 Linux 内部机制。从管理服务和网络开始,我们将继续了解最常见的工具,如 Git 和 Docker。

本部分包含以下章节:

  • 第五章在 Linux 中管理服务

  • 第六章Linux 中的网络

  • 第七章Git,通向 DevOps 的大门

  • 第八章Docker 基础

  • 第九章深入探讨 Docker

第五章:管理 Linux 中的服务

在本章中,我们将深入解释服务(作为守护进程在后台运行的程序)。我们将解释 init 脚本和 systemd 单元。我们还将介绍管理服务的 Alpine Linux rc 命令。

本章内容包括以下主题:

  • 详细了解 Linux 服务

  • 关于 Upstart 的简要介绍,作为一种替代方案

技术要求

本章内容,你需要一台可以执行特权命令的 Linux 系统,可以使用 sudo 或直接跳到 root 账户(尽管我们特别推荐使用前者)。你还需要一款 Linux 文本编辑器,能够生成纯文本文件。如果你打算在 Windows 系统上编辑,请使用支持保存 Unix 文件的文本编辑器。我们推荐在命令行中使用你喜欢的命令行文本编辑器进行编辑:vimemacsjoenano,或者任何你习惯使用的编辑器。

详细了解 Linux 服务

除非你在桌面上运行某种低级别的嵌入式设备——我们强烈怀疑这一点——否则你的操作系统会管理大量任务,为你创建一个舒适且高效的环境。无论是 Mac OS X、Linux、Windows 还是 FreeBSD,它们都运行许多后台程序,这些程序共同提供一个有用的系统。服务器版本的操作系统也是如此。后台程序或后台进程(在 Unix 和 Linux 中称为“守护进程”)是指不与任何输入(键盘、鼠标等)或输出(显示器、终端等)连接的程序。这样,它们即使在没有人登录系统时也能开始工作,并且在用户注销时继续工作。它们还可以在一个永远无法登录系统的用户的权限下运行,从而使它们的执行更加安全。

你的 Linux 系统上运行的服务数量,很大程度上取决于发行版,甚至更大程度上取决于系统的用途。

Linux 服务管理的历史

正如你可以想象的那样,管理系统服务——即那些让你的计算机能够使用的程序——是一项复杂的任务。运行这些服务的软件必须稳定且强大。这一点,以及系统启动不频繁,尤其是在服务器上,促使了 Linux 采用的解决方案得以持续数十年。随着许多线程 CPU 的出现并广泛使用,启动服务的需求日益增加,且更需要智能化管理,促成了新 init 系统的多个实现,我们将在接下来的部分中介绍。

systemd

systemd是一个用于 Linux 的服务管理器,能够管理与操作系统一起启动的服务。它取代了传统的init脚本。它负责启动和停止系统服务,管理系统状态,以及记录系统事件。它已经成为许多流行 Linux 发行版的默认init系统,包括 CentOS、Fedora Linux、红帽企业 LinuxRHEL)和 Ubuntu Linux。

这个服务管理器负责控制系统本身的初始化(Linux 操作系统所需的服务)、启动和停止系统服务,以及管理系统资源。它提供了另一种管理服务和其他系统组件的方式,并且允许系统管理员以比init更标准化的方式配置和自定义系统行为。

systemd的一个关键特性是能够并行启动服务,这可以显著减少系统的启动时间。它还包括一些用于管理和监控系统服务的工具。

另外,systemd受到赞扬的另一点是它最终为 Linux 世界带来的服务配置统一性。在每个 Linux 发行版中,systemd配置文件都被送到相同的路径,并且看起来相同。然而,根据二进制文件的安装路径,仍然存在一些差异。systemd还更擅长判断进程是否正在运行,这使得我们更不容易遇到因为过时的文件无法启动进程的情况。

systemd的一个主要优势是它对依赖关系的意识。服务(在systemd控制下的运行程序)配置包含了关于它依赖的所有其他服务的信息,还可以指向依赖于它的服务。而且,服务可以向systemd告知它需要运行的目标:如果您的服务需要网络运行,您可以将此信息写入配置中,systemd会确保只有在网络正确配置后才会启动您的守护进程。

以下是作为systemd一部分提供的一些工具和实用程序的列表:

  • systemd:这是主要的系统和服务管理器。它是控制服务和其他系统组件初始化和管理的主要程序。

  • systemctl:这是一个命令行工具,用于管理系统服务和其他系统组件。它可以用于启动、停止、重启、启用和禁用服务,还可以查看服务和其他系统组件的状态。

  • journalctl:用于查看和操作系统日志,日志由systemd管理。它可以用来查看日志消息,根据各种标准过滤日志消息,并将日志数据导出到文件。

  • coredumpctl:这是一个实用工具,顾名思义,它有助于从systemd的日志中检索核心转储。

  • systemd-analyze:这个工具可以用来分析系统的启动性能。它衡量系统启动所需的时间,以及识别潜在的瓶颈和性能问题的时间。

  • systemd-cgls:这是一个用于查看系统上控制组层级的命令行工具。systemd 用于管理系统资源并将进程相互隔离。

  • systemd-delta:这是一个用于分析 systemd 提供的默认配置文件与对这些文件所做的本地修改之间差异的命令行工具。

  • systemd-detect-virt:这是一个用于检测系统运行的虚拟化环境的命令行工具。它可以用来判断系统是运行在虚拟机VM)、容器中,还是裸机上。

  • systemd-inhibit:这是一个命令行工具,用于防止执行某些系统操作,例如暂停或关闭系统。

  • systemd-nspawn:这是一个用于在轻量级容器中运行进程的命令行工具。它可以用来创建和管理容器,以及在容器内执行进程。

这只是 systemd 提供的一些常见工具和实用程序的列表。还有很多其他工具,但我们在这里不再讨论它们。

目标

systemd 中,目标 是系统可以处于的特定状态,并通过一个符号名称表示。目标用于定义系统的高级行为,通常用于将一组相关的服务和其他系统组件组合在一起。

例如,multi-user.target 是一个表示已准备好提供多用户访问的系统,启用了网络和其他服务;graphical.target 是一个表示已准备好显示图形登录界面的系统,启用了图形桌面环境和相关服务。

目标通常在单元文件中定义,这些文件是描述系统组件属性和行为的配置文件。当目标被激活时,systemd 将启动所有与该目标相关的服务和其他系统组件。

systemd 包括许多预定义的目标,涵盖了广泛的常见系统状态,管理员还可以定义自定义目标以满足其系统的特定需求。目标可以通过 systemctl 命令激活,或者通过修改系统启动时设置的默认目标来激活。

下面是一些预定义的 systemd 目标的示例:

  • poweroff.target:表示一个正在关机或已关闭的系统。

  • rescue.target:表示一个处于救援模式的系统,启用了最小化的服务。

  • multi-user.target:表示一个已经准备好提供多用户访问、启用网络和其他服务的系统。

  • graphical.target:表示一个已经准备好显示图形登录屏幕的系统,启用了图形桌面环境和相关服务。

  • reboot.target:表示一个正在重启的系统。

  • emergency.target:表示一个运行在紧急模式下的系统,仅启用最基本的服务。

这是一个定义自定义目标的systemd单元文件示例:

[Unit]
Description=Unit File With Custom Target
[Install]
WantedBy=multi-user.target

这个单元文件定义了一个名为Custom Target的目标,旨在作为multi-user.target的一部分被激活。WantedBy指令指定,当multi-user.target被激活时,目标应该被激活。

这是另一个名为custom.targetsystemd单元文件示例,它定义了一个自定义目标:

[Unit]
Description=My simple service
[Install]
WantedBy=multi-user.target

以下是一个使用我们custom.target目标的单元文件:

[Service]
ExecStart=/usr/local/bin/my-simple-service
Type=simple
[Install]
WantedBy=custom.target

这个单元文件定义了一个名为Unit File With Custom Target的目标和一个名为My simple service的服务。ExecStart指令指定了启动服务时应使用的命令,而Type指令指定了服务的类型。服务单元中[Install]部分的WantedBy指令指定,当custom.target被激活时,服务应该被激活。

现在,既然我们已经简单介绍了单元文件,让我们深入了解它们,看看能用它们做些什么。

单元文件

单元文件通常存储在 Linux 操作系统文件系统中的/lib/systemd/系统目录中。此目录中的文件不应以任何方式修改,因为当通过软件包管理器升级服务时,这些文件将会被包中的文件替换。

相反,要修改特定服务的单元文件,请在/etc/systemd/system目录中创建自定义单元文件。此etc目录中的文件将优先于默认位置的文件。

systemd能够使用以下方式提供单元激活:

  • systemd。最简单的情况是,如果您的服务使用了网络,您需要添加一个与multi-user.targetnetwork.target的依赖关系。

  • /etc/systemd/system目录。

  • 模板:您还可以定义模板单元文件。这些特殊单元可用于创建同一通用单元的多个实例。

  • private/tmp或网络访问,并限制内核功能。

  • 路径:您可以基于文件系统中某个文件或目录的活动或可用性来启动一个单元。

  • 套接字:这是 Linux 操作系统中的一种特殊类型的文件,它允许两个进程之间进行通信。通过使用此功能,您可以延迟启动服务,直到相关的套接字被访问。您还可以创建一个单元文件,在启动过程的早期仅创建一个套接字,并创建一个单独的单元文件来使用此套接字。

  • 总线:您可以使用 D-Bus 提供的总线接口来激活单元。D-Bus 只是一个用于进程间通信IPC)的消息总线,最常用于 GNOME 或 KDE 等图形用户界面中。

  • dev 文件,位于 /dev 目录下)。这将利用一种被称为 udev 的机制,udev 是一个 Linux 子系统,用于提供设备事件。

一旦你启动了一个服务,你可能希望通过查看日志文件来检查它是否正常运行。这项工作由 journald 完成。

日志记录

每个由 systemd 管理的服务都会将其日志发送到 journald——systemd 的一个特殊部分。管理这些日志有一个专用的命令行工具:journalctl。在最简单的形式下,运行 journalctl 命令会输出所有系统日志,最新的日志会显示在最上面。虽然日志的格式类似于 syslog——这是 Linux 上用于收集日志的传统工具——但 journald 捕获了更多数据。它收集了启动过程、内核日志等信息。

启动日志默认是临时的。这意味着它们不会在系统重启之间保存。然而,也可以将它们永久记录下来。下面是两种方法:

  • 创建一个特殊的目录。当 journald 在系统启动时检测到该目录时,它会将日志保存在该目录中:sudo mkdir -p /var/log/journal

  • 编辑 journald 配置文件并启用持久化启动日志。使用你喜欢的编辑器打开 /etc/systemd/journald.conf 文件,在 [Journal] 部分,将 Storage 选项修改为如下所示:

    [Journal]
    
    Storage=persistent
    

journald 可以通过使用 -u service.name 选项按服务过滤日志——也就是说,journalctl -u httpd.service 只会打印来自 httpd 守护进程的日志。你也可以通过 man journalctl 命令来查看如何在指定的时间范围内打印日志,或从多个服务中打印日志。

在本节中,我们介绍了 Linux 世界中最常用的服务软件——systemd。在下一节中,我们将探讨 OpenRC——一种用于 Alpine Linux 的系统,Alpine Linux 是云端容器首选的 Linux 发行版。

OpenRC

Alpine Linux 使用另一种用于管理系统服务的系统——init 系统,最初为 Gentoo Linux 开发。它旨在轻量、简单且易于维护。OpenRC 使用纯文本配置文件,使其易于定制和配置。它也很容易通过自定义脚本和程序进行扩展。OpenRC 灵活,可以在各种系统上使用,从嵌入式设备到服务器。

以下是 OpenRC 在 Alpine Linux 中的使用示例:

  • sshcron。你可以使用 rc-service 命令来启动、停止或检查服务的状态。例如,要启动 ssh 服务,可以运行 rc-service ssh start

  • 自定义系统初始化和关机:OpenRC 允许你编写自定义脚本来定制系统在启动或关机过程中的行为。这些脚本会在启动过程中的特定点执行,用于设置自定义配置或执行其他任务。

  • rc-update命令用于将服务添加或移除到不同的运行级别。例如,要使服务在启动时启动,可以运行rc-update add <service> boot

要启动服务,请使用以下rc-service命令:

admin@myhome:~$ rc-service <service> start

要停止服务,请使用以下rc-service命令:

admin@myhome:~$ rc-service <service> stop

要检查服务的状态,请使用以下rc-service命令:

admin@myhome:~$ rc-service <service> status

要启用服务在启动时启动,请使用以下rc-update命令:

admin@myhome:~$ rc-update add <service> default

要禁止服务在启动时启动,请使用以下rc-update命令:

admin@myhome:~$ rc-update del <service> default

在此上下文中,默认指的是 Alpine Linux 系统的默认运行级别。运行级别通常用于定义系统的行为。大多数 Linux 发行版都有几个预定义的运行级别,每个运行级别对应一组特定的服务,这些服务会被启动或停止。

在 Alpine Linux 中,以下是默认的运行级别:

  • default:这是默认的运行级别,用于系统正常启动时。此运行级别中启动的服务包括网络、SSH 和系统日志。

  • boot:此运行级别在系统启动时使用。此运行级别中启动的服务包括系统控制台、系统时钟和内核。

  • single:此运行级别在系统启动到单用户模式时使用。此运行级别中启动的服务仅包含一组最小服务,包括系统控制台和系统时钟。

  • shutdown:此运行级别在系统关闭时使用。此运行级别中停止的服务包括网络、SSH 和系统日志。

OpenRC 使用与我们在本章前面提到的 SysV init非常相似的方式来定义服务操作。像startstoprestartstatus这样的命令是在 Bash 脚本中定义的。以下是一个基本的服务示例:

# Name of the service
name="exampleservice"
# Description of the service
description="This is my example service"
# Start command
start() {
  # Add your start commands
}
# Stop command
stop() {
  # Add your stop command here
}
# Restart command
restart() {
  stop
  start
}

要创建一个新的服务,可以将此文件复制到新文件并根据需要修改namedescriptionstartstoprestart函数。start函数应包含启动服务的命令,stop函数应包含停止服务的命令,restart函数应停止并重新启动服务。它们可以与 SysV init中的相同。

在 OpenRC 中,init脚本通常存储在/etc/init.d目录中。这些脚本用于启动和停止服务以及管理系统的运行级别。

要为 OpenRC 创建一个新的init脚本,可以在/etc/init.d目录中创建一个新文件并使其可执行。

创建init脚本后,可以使用rc-update命令将其添加到默认运行级别,这将导致服务在启动时启动。例如,要将exampleservice服务添加到默认运行级别,可以运行以下命令:

admin@myhome:~$ rc-update add exampleservice default

在大多数情况下,我们将在 Docker 环境中使用 Alpine Linux,在这种环境下 OpenRC 的使用不多,但它仍然对某些边缘案例的使用有所帮助。我们将在第八章第九章中更详细地讨论 Docker。

在本节中,我们已经介绍了 OpenRC,它是管理 Alpine Linux 中系统服务的软件。在下一节中,我们将简要介绍一种过时的 SysV init形式,这种形式可能出现在较老或最小化的 Linux 发行版中。

SysV init

如前所述,init进程是系统中最重要的、持续运行的进程。它负责在系统启动时或管理员请求时启动系统服务,在系统关机时或请求时停止系统服务,且按正确的顺序执行。它还负责在请求时重新启动服务。由于init将代表 root 用户执行代码,因此必须确保其经过充分的稳定性和安全性测试。

旧的init系统的一个迷人特点是它的简单性。服务的启动、停止和重启是通过脚本来管理的——这些脚本必须由应用程序作者、该应用程序的发行版包所有者或系统管理员编写——如果该服务不是通过包安装的。脚本简单且易于理解、编写和调试。

然而,随着软件复杂性的增加,旧的init系统的局限性变得越来越明显。启动一个简单的服务还可以,但启动由多个程序组成的应用变得更加困难,尤其是当它们之间的依赖关系变得更加重要时。init缺乏对它所管理的服务启动依赖关系的观察。

旧的init系统越来越不适合现代系统的另一个原因是串行启动:它无法并行启动服务,从而抵消了现代多核 CPU 的优势。是时候寻找一个更适合新时代的系统了。

一个典型的init系统由以下几个组件组成:

  • 一个包含启动/停止脚本的/etc/init.d/etc/rc.d/init.d目录。

  • 一个/etc/inittab文件,用于定义运行级别并设置默认的运行级别。

  • 一个/etc/rcX.d目录,包含所有应在运行级别X中启动或停止的服务的脚本,其中X是从0(零)到6的数字。我们将在下一段中详细介绍。

/etc/init.d/ 目录包含用于启动、停止和重启服务的 Shell 脚本。该脚本接受一个参数,可以是 startstoprestart。传递给脚本的每个参数都会执行相应的函数(通常函数名称与参数相同:startstoprestart),该函数会执行一系列步骤来正确启动、停止或重启指定的服务。系统最终进入的启动过程的最终状态被称为运行级别。运行级别决定了服务是否正在启动或停止,并且如果是启动服务,哪些服务会被启动。

为了确定要调用的操作类型,会在与相关运行级别相对应的目录中创建一个指向脚本的链接。

假设我们希望系统最终进入运行级别 3。如果我们希望在该运行级别启动我们的服务,我们会创建一个指向 /etc/rc.d/my_service 脚本的链接,该链接指向 /etc/rc3.d/ 目录。链接的名称决定了操作的类型和顺序。因此,如果我们希望服务在 0149 之间的数字之后启动,我们会将其命名为 /etc/rc.3/S50my_service。字母 S 告诉 init 系统启动该服务,数字 50 告诉它在所有较低数字的服务启动之后再启动该服务。请注意,编号更多的是一种框架,并不能保证在 50 之前有所有其他数字的脚本。停止服务也是如此。在确定停止系统的默认运行级别后(通常是 0),会为服务脚本创建一个适当的 symlink

前述框架的主要问题在于它完全没有考虑依赖关系。确保守护进程所依赖的服务正在运行的唯一方法是将其脚本化,放入 start 函数中。在包含多个服务的复杂应用程序中,这可能会导致更大的启动/停止脚本。

管理员和开发人员提出的另一个与 init 脚本相关的问题是,关于如何编写这些脚本有多个标准,并且有多种不同的工具与 init 脚本相关。基本上,每个主要的 Linux 发行版都有自己编写这些脚本的方式和自己的辅助函数库。我们来看看在 Slackware Linux 和 Debian/GNU Linux 上启动相同的 My Service 服务的 init 脚本。这也是编写 Shell 脚本的入门章节派上用场的地方。

在 Slackware 和 Debian 两种情况下,为了简洁起见,我们将删去一些原始内容,只保留最重要的部分。请不用担心,因为这两种发行版都提供了完全注释的示例脚本。

以下的 init 脚本将在 Slackware Linux 环境中工作。脚本以头部开始,我们在头部声明服务名称和一些重要的路径:

#!/bin/bash
#
# my_service       Startup script for the My Service daemon
#
# chkconfig: 2345 99 01
# description: My Service is a custom daemon that performs some important functions.
# Source function library.
. /etc/rc.d/init.d/functions
# Set the service name.
SERVICE_NAME=my_service
# Set the path to the service binary.
SERVICE_BINARY=/usr/local/bin/my_service
# Set the path to the service configuration file.
CONFIG_FILE=/etc/my_service/my_service.conf
# Set the user that the service should run as.
SERVICE_USER=my_service
# Set the process ID file.
PIDFILE=/var/run/my_service.pid
# Set the log file.
LOGFILE=/var/log/my_service.log

好的一点是,Slackware 提供的示例脚本有很好的注释。我们需要声明守护进程的二进制文件路径。我们还需要声明服务将以哪个用户和组身份运行,这实际上决定了它的文件系统权限。

接下来的部分定义了服务的所有重要操作:startstoprestart。通常会有其他操作,但为了简洁我们将其省略:

start() {
  echo -n "Starting $SERVICE_NAME: "
  # Check if the service is already running.
  if [ -f $PIDFILE ]; then
    echo "already running"
    return 1
  fi
  # Start the service as the specified user and group.
  daemon --user $SERVICE_USER --group $SERVICE_GROUP $SERVICE_BINARY -c $CONFIG_FILE -l $LOGFILE -p $PIDFILE
  # Write a lock file to indicate that the service is running.
  touch $LOCKFILE
  echo "done"
}

start 函数利用 pid 文件检查服务是否正在运行。pid 文件是一个包含服务 PID 的文本文件。它提供关于服务主进程及其状态的信息。然而有一个问题,服务可能已经停止运行,但 pid 文件仍然存在。这会导致 start 函数无法实际启动服务。

在检查到 pid 文件不存在后,这意味着服务没有运行,一个名为 daemon 的特殊工具被用来以用户和组权限启动进程,并指向配置文件、日志文件和 pid 文件的位置。

该函数通过 bash echo 命令来传达它的操作,echo 命令会打印出给定的文本。如果是自动执行,echo 命令的输出将根据日志 daemon 配置记录到系统日志中:

stop() {
  echo -n "Stopping $SERVICE_NAME: "
  # Check if the service is running.
  if [ ! -f $PIDFILE ]; then
    echo "not running"
    return 1
  fi
  # Stop the service.
  killproc -p $PIDFILE $SERVICE_BINARY
  # Remove the lock file.
  rm -f $LOCKFILE
  # Remove the PID file.
  rm -f $PIDFILE
  echo "done"
}

同样,stop 函数使用 pid 文件检查服务是否正在运行。没有 pid 文件而服务仍在运行的可能性几乎为零。检查完成后,一个名为 killproc 的特殊命令被用来终止该进程。在函数的最后部分,脚本执行一些清理任务,清除 pid 文件和 lock 文件:

restart() {
  stop
  start
}

restart 函数非常简单。它重用已经定义的 startstop 函数,准确执行它所说的:重启服务。如果服务配置需要重新加载二进制文件,这通常很有用:

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  restart)
    restart
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|uninstall}"
esac

脚本的最后部分评估我们想要执行的操作——启动、停止或重启服务——并调用相应的函数。如果我们要求它执行一个它不识别的操作,脚本会打印出使用说明。

然而,以下脚本是为 Debian Linux 环境设计的:

#!/bin/bash
#
# my_service       Startup script for the My Service daemon
#
# chkconfig: 2345 99 01
# description: My Service is a custom daemon that performs some important functions.
# Source function library.
. /lib/lsb/init-functions
# Set the service name.
SERVICE_NAME=my_service
# Set the path to the service binary.
SERVICE_BINARY=/usr/local/bin/my_service
# Set the path to the service configuration file.
CONFIG_FILE=/etc/my_service/my_service.conf
# Set the user that the service should run as.
SERVICE_USER=my_service
# Set the group that the service should run as.
SERVICE_GROUP=my_service

同样,脚本以一个头部部分开始,定义了稍后将在 startstop 部分使用的路径。通常会有更多的行,但为了简洁,我们将它们省略了:

start() {
  log_daemon_msg "Starting $SERVICE_NAME"
  # Check if the service is already running.
  if [ -f $PIDFILE ]; then
    log_failure_msg "$SERVICE_NAME is already running"
    log_end_msg 1
    return 1
  fi
  # Start the service as the specified user and group.
  start-stop-daemon --start --background --user $SERVICE_USER --group $SERVICE_GROUP --make-pidfile --pidfile $PIDFILE --startas $SERVICE_BINARY -- -c $CONFIG_FILE -l $LOGFILE
  # Write a lock file to indicate that the service is running.
  touch $LOCKFILE
  log_end_msg 0
}

start 函数与 Slackware 版本中的类似。但你会注意到一些微妙的差异,如下所示:

  • 这里使用了start-stop-daemon辅助函数来管理运行中的服务,而不是使用daemonkillproc

  • 这里使用了专门的日志记录函数,而不是简单的 echo:log_daemon_msglog_failure_msglog_end_msg

  • start-stop-daemon函数接受特殊标志来确定操作(startstop),并将程序从终端中分离,使其有效地成为系统服务(--background),如下面所示:

stop() {
  log_daemon_msg "Stopping $SERVICE_NAME"
  # Check if the service is running.
  if [ ! -f $PIDFILE ]; then
    log_failure_msg "$SERVICE_NAME is not running"
    log_end_msg 1
    return 1
  fi
  # Stop the service.
  start-stop-daemon --stop --pidfile $PIDFILE
  # Remove the lock file.
  rm -f $LOCKFILE
  # Remove the PID file.
  rm -f $PIDFILE
  log_end_msg 0
}

stop函数与 Slackware 的stop函数非常相似,具有与start函数相似的差异。

剩下的脚本包含restart函数和任务评估部分并不太有趣,因此我们已将其省略。

正如你可能还记得在关于systemd的章节中提到的,它解决了其中的一些问题。SysV 在现代系统中不常见,因此你通常需要处理的是systemd

然而,还有另一个替代 SysV init的工具,叫做 Upstart。

关于 Upstart,一种替代方案,简要说明

Upstart 是一个基于事件的替代方案,替代传统的 SysV init系统,用于管理和控制系统上的服务和守护进程。Upstart 在 Ubuntu 6.10及其后续版本中被引入,旨在提高启动时间,简化系统配置,并提供更灵活的系统服务管理方式。现在,它已在大多数 Linux 发行版中被systemd取代。

Upstart 用于管理系统的初始化过程,并启动、停止和监督任务与服务。它设计上比传统的init守护进程更灵活高效,并提供关于任务和服务状态的更多信息。

所有可以由systemd和/或cron管理的内容已成为行业标准,所以如果没有充分的理由使用它们,或者你已经有一个使用 Upstart 的系统,我们不建议你将其作为默认选择。

概述

在本章中,我们讨论了系统服务——即 Unix 和 Linux 世界中的守护进程——以及最常用的管理它们的软件。我们解释了这些是什么,如何检查它们的状态,以及如何控制它们。在下一章中,我们将深入探讨 Linux 网络。

第六章:Linux 中的网络

网络是一个复杂的话题,无论在哪个操作系统上都是如此。就灵活性而言,Linux 在配置、内核功能和命令行工具的众多可能性上可能让人感到非常不知所措,这些工具可以帮助我们配置这些选项。在本章中,我们将为这个话题奠定基础,以便您可以在其他出版物中查找有关特定主题的更多信息。在本章中,我们将涵盖以下主题:

  • Linux 中的网络

  • ISO/OSI 作为网络标准

  • 防火墙

  • 高级话题

Linux 中的网络

在 Linux 中,网络是通过内核实现的,这意味着它是操作系统的一部分。内核包括几个组件,这些组件协同工作以实现网络功能,包括设备驱动程序、协议实现和系统调用。

当用户想要通过网络发送或接收数据时,他们可以使用 Linux 中任何可用的网络应用程序,如pingtraceroutetelnetssh。这些应用程序使用系统调用与内核通信,并请求通过网络发送或接收数据。

内核通过设备驱动程序与网络硬件进行通信,设备驱动程序是软件程序,使得内核能够访问和控制硬件。不同类型的网络硬件需要不同的驱动程序,例如以太网或 Wi-Fi。

内核还实现了几个网络协议,这些协议是定义如何在网络上传输和格式化数据的规则和标准。Linux 中常用的协议包括 TCP、UDP 和 IP(版本4和版本6)。

ISO/OSI 作为网络标准

关于网络的任何讨论总是从国际标准化组织/开放系统互联ISO/OSI)定义的参考模型开始。ISO/OSI 参考模型是一个概念模型,定义了一个网络框架,用于在七层中实现协议。它是一个框架,让我们可以将系统(计算机或其他)之间的通信与其实际的物理和软件结构区分开来看。

在 Linux 中,OSI 模型是通过一系列软件组件来实现的,这些组件负责执行每一层的功能。这些组件共同作用,实现 Linux 中的网络功能。

在 Linux 中实现的 OSI 模型的七层如下:

  • 物理层

  • 数据链路

  • 网络层

  • 传输层

  • 会话层

  • 表示层

  • 应用层

在运行在云端的系统中,您将访问到 Linux 内核中实现的所有层。这些层包括网络层、传输层、会话层、表示层和应用层。为了调试网络连接、检查统计信息并找出任何其他可能的问题,Linux 提供了一个控制台工具。让我们逐一查看每一层,研究我们可以在 Linux 中使用哪些命令行工具来检查它们。

要了解更多关于 OSI 模型的信息,可以参考 osi-model.com/。我们将在接下来的子节中深入探讨这些层并对其进行解释。

物理层

这一层负责通过通信通道传输原始比特,并通过设备驱动程序来实现,这些驱动程序控制网络硬件,例如以太网标准,如 10BASE-T10BASE210BASE5100BASE-TX100BASE-FX100BASE-T1000BASE-T 等。由于我们将专注于软件实现以及如何在 Linux 控制台上与其交互,因此在这里不会进一步讨论这一层。你可以在网上找到大量关于电缆、硬件设备和网络的信息。

数据链路层 – MAC,VLAN

数据链路层负责在网络上的设备之间提供可靠的链路。它分为逻辑链路控制LLC)子层和媒体访问控制MAC)子层。

数据链路层将来自网络层(第 3 层)的原始数据转换为可以通过物理链路传输的格式。它还提供错误检测和校正、流量控制以及 MAC 功能。

LLC 子层为网络层提供一致的接口,不论使用何种类型的物理网络。它还提供流量控制和错误校正服务。

MAC 子层控制对物理网络的访问并提供寻址服务。它使用 MAC 地址,这是分配给网络上每个设备的唯一标识符,确保数据能传送到正确的目的地。

数据链路层还包括使用协议,如以太网、PPP 和帧中继,以在网络上提供设备间的通信。它还提供流量和错误控制机制—例如,它使用循环冗余校验CRC)进行错误检测,并使用滑动窗口进行流量控制。

有多个 Linux 命令行工具可用于调试数据链路层问题。以下是一些示例:

  • ifconfig:此命令可用于查看网络接口的状态及其相关的 IP 地址、子网掩码和 MAC 地址。它还可以用来配置网络接口,例如设置 IP 地址或启用或禁用接口。

  • ping:此命令可用于测试网络中主机的可达性。它向指定主机发送一个互联网控制消息协议ICMP)回显请求数据包,并等待回显响应。如果主机响应,则表明主机可达,并且数据链路层工作正常。

  • traceroutetracepathmtr:这些命令可以用来追踪数据包从源到目的地的路径。它们还可以用来识别任何可能导致问题的网络跳数或设备。此外,tracepath可以测量mtr,而mtr则提供更多关于网络健康的信息。

  • arp:此命令可用于查看和操作地址解析协议ARP)缓存。ARP 用于将 IP 地址映射到本地网络上的 MAC 地址。此命令可以用来验证 ARP 缓存中是否有正确的 IP-MAC 地址映射。

  • ethtool:此命令可用于查看和配置以太网接口的高级设置,如链路速度、双工模式和自动协商设置。

  • tcpdump:此命令可用于实时捕获和分析网络数据包。它可以用来排查诸如数据包丢失、延迟包和网络拥堵等问题。

我们将在本章的接下来的部分中深入探讨前述工具,因为大多数工具可以用于同时查看多个 OSI 层。

网络层 – IPv4 和 IPv6

每个公有(互联网)或私有(你的办公室或家里)网络上的设备都有一个唯一地址,用于识别它并与之连接。当你向一个网站发送请求时,你的设备会使用其地址向目标服务器发送消息。服务器随后会使用其地址向你的设备发送消息以响应。这一过程就是设备通过互联网互相通信的方式。我们使用的地址有两种类型:IPv6 和 IPv4。v4(版本4)和v6(版本6)是你可以使用的地址数量。

IPv4 是最广泛使用的 IP 版本,但只有大约 43 亿个唯一地址可用。然而,这些地址不足以支持不断增加的连接到互联网的设备数量。为了解决这个问题,开发了一种新的 IP 版本——IPv6,提供了更大的地址空间。

IPv4 地址是 32 位数字,通常以点分十进制表示,包含四个范围从0255的八位字节。例如,192.168.0.11.2.3.4都是有效的 IPv4 地址。

IPv6 地址是 128 位数字(在十六进制中介于zeroFFFF之间,等于十进制值65535),以十六进制表示,采用八组四个十六进制数字,并以冒号分隔。IPv6 地址的示例为2001:0db8:bad:f00d:0000:dead:beef:7331

我们在这里主要关注 IPv4,因为它通常更容易理解,但类似的原则也适用于 IPv6,因此稍后我们将在本章中学到的内容更容易重新应用到 IPv6 环境中。

子网、类别和网络掩码

子网 是通过将一个较大的网络划分成更小的网络而创建的网络片段。这么做的原因有很多,包括安全性、组织结构以及高效利用 IP 地址。在公共互联网网络中,每个组织都有自己的一部分网络——一个子网。

当一个网络被划分成子网时,IP 地址中的主机部分(标识网络中具体设备的部分)会被分成两部分:一部分标识子网,另一部分标识子网内的主机 IP 地址。子网掩码是应用于 IP 地址的二进制表示,用于确定 IP 地址中哪一部分标识子网,哪一部分标识主机。

假设一个使用 IP 地址范围 192.168.11.0/24(或 192.168.11.0/255.255.255.0 的十进制形式)的网络。/24 部分表示 24 位网络部分和 8 位主机部分。

这意味着该网络的 IP 地址有 24 位(或地址中的前三个数字)用于 网络 部分,8 位(或最后一个数字)用于 主机 部分。所以,对于这个网络,你将有一个可用的地址范围 192.168.11.0 - 192.168.11.255,其中 192.168.11.0 是网络地址,192.168.11.255 是广播地址。

广播网络地址 是一种特殊类型的 IP 地址,用于向特定网络或子网中的所有主机发送信息。广播地址是网络或子网 IP 地址范围中的最高地址,并与子网掩码一起使用,以标识广播域。当主机将数据包发送到广播地址时,该数据包将被送到同一网络或子网中的所有主机。需要注意的是,广播数据包不会离开当前的子网网络——它们仅在本地网络或子网内起作用。

在标准化 CIDR 之前,IP 地址根据网络需要支持的主机数量被分为不同的类别(ABC)。这些类别是根据 IP 地址的前导位来定义的,每个类别有不同数量的位用于网络部分和主机部分。目前我们主要使用 CIDR,但一些网络地址仍然存在。例如,10.0.0.012.0.0.015.0.0.0 通常具有 255.0.0.0/8 网络掩码。以下是你可能会遇到的一些其他网络:

  • 10.0.0.0/812.0.0.0/815.0.0.0/8

  • 172.16.0.0/16172.17.0.0/16172.18.0.0/16

  • 192.168.0.0/24192.168.1.0/24192.168.2.0/24

这些例子并不是实际的网络,而仅仅是通过查看 IP 地址的前导位和子网掩码来识别网络类别的表示——这并不是一种规则,你可以在你的基础设施中创建更小(或更大)的网络。

你可以在网上找到许多计算器,帮助你更好地理解网络地址是如何工作的。这里有一些例子:

既然我们已经学习了子网、类别和网络掩码,那么接下来让我们进入下一个小节。

网络配置和控制台工具

通过你目前所掌握的知识,你可以轻松地使用一些在现代 Linux 环境中都可用的命令行工具来检查你的 Linux 网络配置。

这里是一些用于网络配置的基本控制台工具:

  • iproute2 包,取代了 ifconfigroute 命令

  • ifconfig

  • route

  • ip

  • netplan

让我们一起了解这些工具的语法以及使用它们时可能实现的功能。

ifconfig

在 Linux 中,通常可用的命令之一是ifconfig。这个工具用于配置网络接口。它可以用来显示接口的状态、为接口分配 IP 地址、设置网络掩码以及设置默认网关。ifconfig(来自 net-tools 包)在近年来被 iproute2 工具集所取代;这个包中最著名的命令是 ip

以下是 ifconfig 命令的示例输出:

admin@myhome:~$ ifconfig
eth0: flags=4163<UP,BRODCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.2  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:02  txqueuelen 0  (Ethernet)
        RX packets 17433  bytes 26244858 (26.2 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8968  bytes 488744 (488.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

当没有任何额外选项时,执行 ifconfig 会列出系统中所有可用的接口,并显示一些基本信息,如接口名称(这里是 eth0lo)、网卡的 MAC 地址(设备的物理地址)、网络配置(IP 地址、网络掩码和广播地址)、接收和发送的包,以及其他信息。这将让你一眼查看网络状态。

回环设备(在上面的示例中命名为 lo)是一个虚拟网络接口,用于将网络数据包发送回同一个发送源的主机。它也被称为回环接口,并由 lolo0 表示。

回环设备的主要作用是为主机提供一种稳定且一致的方式,通过网络栈与自身进行通信,而不需要依赖任何物理网络接口。

回环接口通常用于测试、故障排除和一些系统与应用程序功能,以及进程间通信IPC),尤其是在同一主机上运行的进程之间。

使用 ifconfig 还可以让你启用或禁用某些接口并配置它们的设置。要使配置保持有效,你需要将其保存到/etc/network/interfaces 文件中:

auto lo
iface lo inet loopback
# eth0 network device
auto eth0
iface eth0 inet static
    address 172.17.0.2
    netmask 255.255.0.0
    gateway 172.17.0.1
    dns-nameservers 8.8.8.8 4.4.4.4
auto eth1
allow-hotplug eth1
iface eth1 inet dhcp

在前面的例子中,我们设置了一个自动回环设备,分别为eth0eth1eth0接口具有静态网络配置,并将在系统启动时像lo一样进行配置。eth1接口具有动态网络配置,它是通过allow-hotplug配置获取的,这意味着该设备将在 Linux 内核检测到后启动。

需要知道的是,在编辑/etc/network/interfaces文件后,您需要在 Debian Linux 或 Ubuntu Linux 中使用ifupifdown工具,或者在 Alpine Linux 中使用ifupdown工具。或者,您可以通过在 Debian Linux、Ubuntu Linux 或 RHEL/CentOS 中使用systemctl restart网络来重启网络。必须在 Alpine Linux 中使用rc-service网络restart命令。

要使用ifconfig手动配置设备,您需要运行以下命令:

admin@myhome:~$ ifconfig eth0 192.168.1.2 netmask 255.255.255.0
admin@myhome:~$ ifconfig eth0 up

这将为eth0接口配置一个192.168.1.2的 IP 地址,网络的子网掩码为255.255.255.0(或 CIDR 表示法中的/24)。

在 Debian Linux 和 Ubuntu Linux 系统中,您可以使用ifupifdown代替ifconfig upifconfig down命令,或者在 Arch Linux 系统中使用ifupdown

route

route命令用于查看和操作 IP 路由表。它用于确定网络数据包在给定目标 IP 地址下的发送位置。

在不带任何选项的情况下调用route命令将显示系统中的当前路由表:

admin@myhome:~$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default       172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      0.0.0.0       255.255.0.0     U     0      0       0 eth0

要向路由表添加新条目,请使用以下命令:

admin@myhome:~$ sudo route add default gw 172.17.0.1 eth0

只能有一个默认路由。要添加自定义路由,请执行以下操作:

admin@myhome:~$ sudo route add -net 192.168.1.0 netmask 255.255.255.0 gw 192.168.1.1 dev eth0

del命令用于从路由表中删除条目,类似于前面的示例:

admin@myhome:~$ route del -net 192.168.1.0 gw 192.168.1.1 netmask 255.255.255.0 dev eth0

最后,flush命令会删除路由表中的所有条目,这意味着您将失去所有网络连接,如果您连接到远程机器,您将无法再对其进行操作。

使用ifconfigroute命令还有更多的可能性,但正如我们之前所说,两个命令都被iproute2包(iproute的继任者)取代,该包包括ip命令。

iproute2

更高级的命令用于操作路由和网络设备的配置是ip,它可以用来执行更广泛的任务,如创建和删除接口、添加和删除路由以及显示网络统计信息。

让我们来看看使用iproute2时可以执行的最常见命令:

  • ip addrip a:此命令显示有关网络接口及其 IP 地址的信息。它还支持子命令;例如,ip addr add命令可用于向接口添加 IP 地址,而ip route add可用于向路由表中添加路由。

  • ip linkip l:此命令显示有关网络接口及其链路层设置的信息。

  • ip routeip r:此命令显示 IP 路由表。

  • ip -s link(或ip -s l):这将显示关于网络接口的统计信息。

以下是运行ip linkip addr命令的输出:

admin@myhome:~$ sudo ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/tunnel6 :: brd :: permaddr 4ec4:2248:2903::
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
admin@myhome:~$ sudo ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1000
    link/tunnel6 :: brd :: permaddr 4ec4:2248:2903::
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

两个命令打印出非常相似的信息,但ip addr除了提供物理接口的信息外,还添加了有关网络配置的信息。ip link命令用于控制接口状态。与ifconfig up eth0启用接口类似,ip link set dev eth0 up也会做同样的事情。

要使用iproute2配置网络接口,你需要执行以下命令:

admin@myhome:~$ sudo ip addr add 172.17.0.2/255.255.0.0 dev eth0
admin@myhome:~$ sudo ip link set dev eth0 up

要将接口设置为默认路由,请使用以下命令:

admin@myhome:~$ sudo ip route add default via 172.17.0.1 dev eth0

要仅检查eth0接口的状态,你可以执行以下命令:

admin@myhome:~$ sudo ip addr show dev eth0
47: eth0@if48: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid
0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

netplan

这个工具是 Ubuntu 中引入的新网络配置工具,并且在 Debian 中也得到了支持。它是一个 YAML 配置文件,可用于管理网络接口、IP 地址和其他网络设置。它首次在 Ubuntu 17.10 中作为传统/etc/network/interfaces文件的替代品被引入,随后也被其他发行版(如 Debian 和 Fedora)采纳。Ubuntu 18.04 及更高版本默认安装了netplan,其他发行版如 Debian 10 和 Fedora 29 及更高版本也默认包含了netplan

要使用 Netplan,你首先需要在/etc/netplan/目录中创建一个配置文件。该文件应具有.yaml扩展名,并且应该命名为具有描述性的名称,如01-eth0.yamlhomenetwork.yaml

样本配置如下所示:

network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: true

该配置定义了一个单一的网络接口eth0,它使用 DHCP 获取 IP 地址。renderer键指定了 netplan 使用的网络管理器(在本例中为networkd)。version键用于表示正在使用的netplan版本。networkdsystemd系统和服务管理器的一部分,用于网络管理的守护进程。

eth0接口配置静态 IP 地址的配置文件将如下所示:

network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      addresses: [192.168.0.2/24]
      gateway4: 192.168.0.1
      nameservers:
        addresses: [8.8.8.8, 4.4.4.4]

该配置文件定义了一个以太网接口(eth0)及其静态 IP 地址。此接口还定义了网关和 DNS 服务器。注意,我们使用了 CIDR 表示法,而不是十进制表示法。

一旦你保存了配置文件,为了应用更改,可以运行以下命令:

admin@myhome:~$ sudo netplan apply

你还可以使用以下命令检查配置文件是否有语法错误:

admin@myhome:~$ sudo netplan --debug generate

你可以使用以下命令检查网络接口的当前状态:

admin@myhome:~$ sudo netplan --debug try

最后,如果你想检查网络接口的状态而不应用配置,可以使用以下命令:

admin@myhome:~$ sudo netplan --debug networkd try

要查看与网络接口启动时的错误相关的日志,可以使用 dmesg 命令查看内核消息,包括与网络接口相关的消息。您可以使用 dmesg | grep eth0 来过滤与 eth0 接口相关的日志。其他位置包括 /var/log/messages 文件,journalctl(例如,journalctl -u systemd-networkd.service 命令),以及包含 netplan 生成的日志的 /var/log/netplan/

在日常操作中,编辑 /etc/network/interfaces 文件或 netplan 配置比手动配置接口更常见,但是知道如何临时更改网络配置以进行测试或调试问题非常有用。

接下来,我们将介绍传输层。

传输层 – TCP 和 UDP

在 OSI 模型的传输层中,我们更多地关注传输控制协议TCP)和 IP,它们是现代互联网的基础。此外,我们还将深入了解用户数据报协议UDP)。我们在本章的网络层 – IPv4 和 IPv6 小节中讨论了 IP,因此这里我们只会稍微加深对该协议的了解。

TCP 用于通信,需要服务之间可靠的双向通信。UDP 是一种无状态协议,不需要持续连接。

使用 三次握手 建立了 TCP 连接:

  1. 客户端向服务器发送一个 SYN(同步)包,以初始化连接。

  2. 服务器接收到 SYN 包,并向客户端发送一个 SYN-ACK(同步-确认)包,以确认连接已建立。

  3. 客户端接收到 SYN-ACK 包,并向服务器发送一个 ACK(确认)包,以完成三次握手。

一旦完成三次握手,设备就可以通过已建立的 TCP 连接开始互相发送数据。

TCP 和 UDP 协议的连接是通过向连接的服务器端口发送数据包来初始化的。端口号包含在 IP 数据包头中,与 IP 地址一起,以便目标设备知道将数据发送到哪个进程。

端口是一个整数,范围从065535。它用于标识设备上运行的特定进程,并与其他服务区分开来。端口号会附加在 IP 地址后面。因此,如果多个程序在同一 IP 地址上监听连接,另一端可以准确知道它想要与哪个程序通信。假设有两个进程正在运行并监听:一个 WWW 服务器和一个 SSH 服务器。WWW 服务器通常会监听80443端口,SSH 服务器会监听22端口。假设系统的 IP 地址是192.168.1.1。要连接到 SSH 服务器,我们会将 IP 地址与端口地址配对(通常写作192.168.1.1:22)。服务器会知道该传入的连接需要由 SSH 进程处理。与 TCP 不同,UDP 连接并不会建立与服务器机器的连接。相反,设备可以在已知端口上相互发送 UDP 数据报(数据包)。

当设备发送 UDP 数据报时,它会在数据包头中包含目标 IP 地址和端口号。接收设备检查数据包头中的目标 IP 地址和端口号,以确定将数据发送到哪个进程。

由于 UDP 是无连接的,因此不能保证数据会被目标设备接收,或者接收的顺序与发送的顺序相同。也没有错误检查或丢失数据包的重传。UDP 通常用于需要低开销和快速通信的服务。最著名的使用 UDP 进行通信的服务是域名系统DNS)。

端口号有三种类型:

  • /etc/services 文件。这些是为特定服务保留的端口号,例如HTTP流量的80端口或 DNS 流量的53端口。

  • 11024

  • 短暂端口是用于临时连接的端口号,并由操作系统动态分配,例如用于 TCP 连接。

有各种工具可供查看机器上 TCP 和 UDP 流量的详细信息。你还可以设置防火墙,以便控制从网络访问你机器的权限。

netstat

要查看从你的机器发起的所有 TCP 连接,可以使用netstat命令。它可以用于查看打开的连接列表,并显示系统网络流量的统计信息。

要查看系统上所有打开的连接列表,可以使用netstat -an命令。这将显示所有当前连接的列表,包括本地和远程 IP 地址及端口,以及连接状态(例如,监听、已建立等)。

使用netstat -s,你可以查看系统上每个连接的统计信息。这将显示关于系统网络流量的各种统计数据,包括发送和接收的包数量、错误数量等。

要查看 netstat 中的所有 UDP 连接,你可以使用 netstat -an -u 命令。它会显示所有当前的 UDP 连接,包括本地和远程的 IP 地址与端口,以及连接的状态。

或者,你也可以使用 netstat -an -u | grep "udp" | grep "0.0.0.0:*" 命令,只显示处于监听状态的 UDP 连接。这个命令过滤 netstat -an -u 的输出,只显示包含 "UDP""0.0.0.0:*" 的行,这表示一个监听中的 UDP 连接。

你还可以使用其他选项,例如 -p 显示每个连接所属进程的进程 ID 和名称,-r 显示路由表,-i 显示特定接口的统计信息。

tcpdump

tcpdump 是一个命令行数据包分析器,允许你通过显示传输或接收的网络数据包来捕获和分析网络流量。tcpdump 可以用来排查网络问题、分析网络性能以及监控网络安全。你可以在特定接口上捕获数据包,基于各种标准过滤数据包,并将捕获的包保存到文件中以供后续分析。

要捕获并显示 eth0 接口上的所有网络流量,你可以使用 -i 选项,后跟接口名称。要捕获并显示 eth0 接口上的所有网络流量,你需要运行 sudo tcpdump -i eth0

你还可以通过使用 -w 选项将捕获的数据包保存到文件中,以便以后分析——例如,tcpdump -i eth0 -w all_traffic.pcap。这将把 eth0 接口上捕获的所有数据包保存到文件中。

默认情况下,tcpdump 会无限期地捕获数据包。

要捕获特定时间内的数据包,你可以使用 -c 选项,后跟要捕获的数据包数量——例如,sudo tcpdump -i eth0 -c 100。该命令会捕获并显示 100 个数据包,然后退出。

你还可以通过使用像 portiphost 等过滤器来筛选流量——例如,sudo tcpdump -i eth0 'src host 192.168.1.2 and (tcp or udp)'。这个过滤器捕获所有源 IP 地址为 192.168.1.2 且为 TCPUDP 的数据包。

为了展示 tcpdump 更高级的用法,让我们只捕获 SYN 数据包(查看所有正在建立的连接)。你可以通过使用 tcp[tcpflags] & (tcp-syn) != 0 过滤器来实现。这个过滤器检查数据包的 TCP 头部中是否设置了 SYN 标志。

这是一个命令示例,它捕获并显示 eth0 接口上的所有 SYN 数据包:

sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn) != 0'

你还可以通过使用 -w 选项将捕获的数据包保存到文件中,以便后续分析,如下所示:

sudo tcpdump -i eth0 -w syn_packets.pcap 'tcp[tcpflags] & (tcp-syn) != 0'

这将把所有捕获的 SYN 数据包保存到 syn_packets.pcap 文件中。

你也可以指定一个更复杂的过滤器,如下所示:

sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn) != 0 and src host 192.168.1.2'

这个过滤器仅捕获源 IP 地址为 192.168.1.2SYN 数据包。

Wireshark

另一个类似于tcpdump的流行工具是 Wireshark。它既可以在无头模式下使用(仅在命令行中),也可以通过图形界面使用:

  • 要使用 Wireshark 显示eth0接口上的所有流量,你可以使用sudo wireshark -i eth0命令。这将启动 Wireshark 并监听eth0接口上的流量。你还可以使用-k标志立即开始捕获,并使用-w标志将捕获的流量写入文件:sudo wireshark -k -i eth0 -w output_file.pcap

  • 如果你想仅显示SYN数据包,如同我们在tcpdump示例中所展示的那样,你可以运行sudo wireshark -i eth0 -f "tcp.flags.syn == 1"命令。前面的命令使用了一个过滤器,"tcp.flags.syn == 1",这表示我们只想看到标记为SYNTCP协议标志。你也可以在 Wireshark 的图形界面版本中使用此过滤器,方法是进入捕获菜单,选择选项,然后在捕获过滤器字段中输入过滤器,再开始捕获。

或者,在捕获流量后,你也可以通过点击过滤字段中的"tcp.flags.syn==1"并按Enter来应用该过滤器。

ngrep

下一个非常有用的工具是ngrep,它类似于tcpdump和 Wireshark。与我们之前提到的其他工具不同,它的使用更加简单,而且允许你(类似于grep)在网络数据包中搜索字符串。

例如,要监视GET HTTP请求,你可以使用以下命令:

admin@myhome:~$ ngrep -d eth0 -q -W byline "GET" "tcp and port 80"
interface: en0 (192.168.1.0/255.255.255.0)
filter: ( tcp and port 80 ) and ((ip || ip6) || (vlan && (ip || ip6)))
match: GET
# Later output will contain actual GET requests, removed for readability

该命令将在eth0接口上监听,并仅匹配80端口上的TCP数据包(HTTP的默认端口)。-q选项告诉ngrep保持安静,不显示数据包摘要信息,而-W则告诉ngrep将每个数据包的数据打印在单独的行上。此时,我们可以进入会话层。

会话层

会话层,正如我们之前提到的,它负责在设备之间建立、维护和终止连接。这意味着它设置、协调和终止应用程序之间的对话、交换或连接。会话层通过使用令牌管理和检查点等技术,确保数据可靠地传输并按正确顺序传送。它还负责解决会话中可能出现的任何冲突,例如当两个应用程序试图同时发起会话时。简而言之,会话层在网络中建立、维护和终止设备之间的连接。

换句话说,会话层是我们已经讨论过的下层与接下来几节将要介绍的上层之间的粘合剂。

解决会话问题的最佳工具是与你正在使用的服务相关的日志。如果你遇到 FTP 连接问题,可能需要查看你管理的机器上运行的客户端和/或服务器的日志。如果日志不足以理解你要解决的问题,之前介绍的工具也可以提供帮助。

表示层 – SSL 和 TLS

ASCII)或 8 位(EBCDIC)整数。我们还在这一层处理加密和压缩。表示层还确保数据格式正确,便于应用程序处理。它充当应用程序与数据之间的中介,使得应用程序不受接收数据特定格式的影响。

对于这一层,最常见的加密标准是安全套接字层SSL)和传输层安全性TLS),你可能需要调试和修复这两个协议的问题。

TLS是一个广泛使用的协议,用于确保网络通信的安全性。它是SSL的继任者,用于加密和验证通过网络(如互联网)传输的数据。

TLS 通过在两个设备之间建立一个安全的tunnel(隧道)来工作,例如 Web 服务器和 Web 浏览器。该隧道用于在两个设备之间以加密格式传输数据,使得攻击者难以拦截和读取数据。

建立 TLS 连接的过程包括几个步骤:

  1. 握手:客户端和服务器交换信息,以建立共享的加密方法和密钥,确保连接的安全。

  2. 认证:服务器通过提供包含服务器身份和公钥信息的数字证书向客户端进行身份验证。客户端可以使用这些信息验证服务器的身份。

  3. 密钥交换:客户端和服务器交换公钥,以建立一个共享的秘密密钥,该密钥将用于加密和解密数据。

  4. 数据加密:一旦共享的秘密密钥建立,客户端和服务器就可以使用对称加密算法开始加密数据。

  5. 数据传输:然后,数据通过安全连接进行传输,并通过握手过程中建立的加密保护。

TLS 有多个版本,其中最新的版本被认为更为安全。然而,旧系统可能不支持最新版本。可用的 TLS 版本如下:

  • TLS 1.0:这是该协议的第一个版本,于 1999 年发布。

  • TLS 1.1:该版本于 2006 年发布。

  • TLS 1.2:该版本于 2008 年发布,增加了对新加密算法的支持,并做了其他多个安全性增强。

  • TLS 1.3:该版本于 2018 年发布,采用了前向保密性(forward secrecy),使得攻击者更难解密捕获的数据。

SSL 是一种广泛用于保障网络通信安全的协议,如互联网通信。它由 Netscape 在 1990 年代开发,后来被 TLS 协议所取代。

和 TLS 一样,SSL 通过在两个设备之间建立一个安全的tunnel(隧道)来工作,比如网页服务器和网页浏览器。这个隧道用于在两个设备之间传输加密格式的数据,使得攻击者很难拦截和读取数据。目前不建议再使用 SSL,最好使用最新版本的 TLS,但你可能仍然会遇到使用 SSL 的系统。

调试 SSL 和 TLS 问题的最佳工具是openssl命令。你可以用它测试与服务器的 SSL 连接。例如,你可以使用以下命令测试与服务器在端口(通常是443,这是 HTTPS 的常用端口)的连接:

admin@myhome:~$ openssl s_client -connect myhome:443

你可以使用openssl命令检查 SSL 证书的详细信息,包括过期日期、发行机构和公钥。例如,你可以使用以下命令来检查证书的详细信息:

admin@myhome:~$ openssl s_client -connect myhome:443 -showcerts

这将允许你检查服务器提供的证书是否有效,并且是否符合你的预期。这比使用网页浏览器要快得多。

使用openssl命令,你还可以检查服务器支持哪些加密算法。例如,你可以使用以下命令检查服务器支持的加密算法:

admin@myhome:~$ openssl s_client -connect myhome:443 -cipher 'ALL:eNULL'
# Cut most of the output for readability
SSL handshake has read 5368 bytes and written 415 bytes
---
New, TLSv1/SSLv3, Cipher is AEAD-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : AEAD-AES256-GCM-SHA384
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Start Time: 1674075742
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---
closed

此外,openssl还内置了诊断工具,可以检测系统中已知的漏洞:

admin@myhome:~$ openssl s_client -connect myhome:443 -tlsextdebug -status
CONNECTED(00000006)
140704621852864:error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version:/AppleInternal/Library/BuildRoots/aaefcfd1-5c95-11ed-8734-2e32217d8374/Library/Caches/com.apple.xbs/Sources/libressl/libressl-3.3/ssl/tls13_lib.c:151:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 5 bytes and written 303 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Start Time: 1674075828
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

还值得注意的是,SSL 和 TLS 都使用公钥加密。在这种加密方法中,我们会创建两个文件:一个私钥和一个公钥。它们共同构成一对。公钥用于加密数据,而私钥用于解密数据。私钥应始终保密,正如它的名字所示。这种加密方法基于大素数的数学特性,被认为是非常安全的。

在 TLS 的情况下,公钥加密在握手阶段用于建立客户端与服务器之间的安全连接。这个过程如下:

  1. 服务器生成公钥和私钥。公钥作为服务器数字证书的一部分发送给客户端。

  2. 客户端生成一个会话密钥,用于加密发送到服务器的数据。服务器的公钥用于加密会话密钥。一旦服务器收到加密的数据,它使用私钥解密数据并恢复会话密钥。

  3. 一旦会话密钥被解密,它就会用于加密客户端与服务器之间发送的数据。

接下来的最后一层是 OSI 模型的顶层。在接下来的章节中,我们将介绍应用层,涵盖诸如 HTTP 和 FTP 等协议,这些协议通常用于浏览网页和共享文件。

应用层 – HTTP 和 FTP

应用层是 OSI 网络模型的第七层,也是最高层。它提供软件应用程序与网络之间的接口,使得应用程序能够访问网络的通信服务。应用层定义了特定于应用程序的协议和服务,例如文件传输(文件传输协议FTP))、电子邮件(简单邮件传输协议SMTP)和互联网消息访问协议IMAP))以及广泛使用的网络服务(超文本传输协议HTTP))。

让我们更仔细地看看 HTTP。这个通信协议用于在万维网上传输数据。它基于客户端-服务器模型,其中 Web 浏览器(客户端)向 Web 服务器发送请求,服务器则返回响应。

当用户在浏览器中输入统一资源定位符URL)时,浏览器会向与该 URL 关联的网络服务器发送 HTTP 请求。URL 是你在浏览器地址输入框中看到的内容,通常位于浏览器窗口的顶部,例如,google.com

请求包含方法(如GETPOSTPUTPATCHDELETE),指示浏览器希望服务器执行的操作类型,并可能包含其他信息,如POSTPUT请求的数据。

然后,网络服务器处理请求,之后服务器会发送回一个 HTTP 响应,包含状态码(如200表示成功或404表示未找到)以及浏览器请求的任何数据,例如构成网站的 HTML 和 CSS。

一旦浏览器收到响应,它会解析 HTML、CSS,通常还包括 JavaScript,以便将网站显示给用户。

HTTP 是一种无状态协议,这意味着每个请求都是独立的,服务器不会保留任何关于先前请求的信息。然而,许多 Web 应用程序使用 cookies 或其他技术来维持多个请求之间的状态。

HTTP 1.1 版本引入了新特性,如持久连接、主机头字段和字节服务,这些改进提高了协议的整体性能,并使其更适合重负载使用场景。该协议的最新版本是 2.0,详细描述在 RFC 7540 中(https://www.rfc-editor.org/rfc/rfc7540.xhtml),该版本于 2015 年发布,并在 2020 年由 RFC 8740(www.rfc-editor.org/rfc/rfc8740.xhtml)更新。

要解决 HTTP 问题,您可以使用任何调试网络问题的工具,例如 tcpdumpngrep。有多种控制台和图形界面工具可供调试 HTTP。最常见的控制台工具是 wgetcurl,图形界面工具包括 PostmanFiddler。浏览器中也会内置调试工具,例如 Firefox 或 Chrome 开发者工具。

我们现在将重点关注控制台工具,因此首先来看看 wget。这个工具本用于下载文件,但我们仍然可以用它来调试 HTTP。首先,我们将展示请求和响应的详细信息:

admin@myhome:~$ wget -d https://google.com
DEBUG output created by Wget 1.21.3 on darwin22.1.0.
Reading HSTS entries from /home/admin/.wget-hsts
URI encoding = 'UTF-8'
Converted file name 'index.xhtml' (UTF-8) -> 'index.xhtml' (UTF-8)
--2023-01-20 14:11:02--  https://google.com/
Resolving google.com (google.com)... 216.58.215.78
Caching google.com => 216.58.215.78
Connecting to google.com (google.com)|216.58.215.78|:443... connected.
Created socket 5.
Releasing 0x0000600000d2dac0 (new refcount 1).
Initiating SSL handshake.
Handshake successful; connected socket 5 to SSL handle 0x00007fca4a008c00
certificate:
  subject: CN=*.google.com
  issuer:  CN=GTS CA 1C3,O=Google Trust Services LLC,C=US
X509 certificate successfully verified and matches host google.com
---request begin---
GET / HTTP/1.1
Host: google.com
User-Agent: Wget/1.21.3
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
---request end---
HTTP request sent, awaiting response...
# Rest of the output omitted

您可能需要发送带有特定头部的 HTTP 请求。为此,您可以使用 --header 选项:

admin@myhome:~$ wget --header='<header-name>: <header-value>' <url>

POST 是一种特殊的 HTTP 请求——它使用 URL,但也需要一些请求数据,因为它的目的是向您的系统发送数据,无论是用户名和密码还是文件。要发送带有准备数据的 HTTP POST 请求,可以使用 --post-data 选项,如下所示:

admin@myhome:~$ wget --post-data=<data> <url>

一个更强大的调试 HTTP 问题的工具是 curl。要发送 HTTP GET 请求并显示响应,您可以使用以下命令:

admin@myhome:~$ curl linux.com
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>

要发送 HTTP POST 请求并显示响应,您可以使用 -X POST 选项和 -d 选项:

admin@myhome:~$ curl -X POST -d <data> <url>

使用 -X 选项,您可以发送其他类型的请求,例如 PATCHDELETE

要发送带有特定头部的 HTTP 请求,您可以使用 -H 选项:

admin@myhome:~$ curl -H '<header-name>: <header-value>' <url>

若要仅显示响应头部,请使用以下代码:

admin@myhome:~$ curl -I linux.com
HTTP/1.1 301 Moved Permanently
Connection: keep-alive
Content-Length: 162
Content-Type: text/html
Location: https://linux.com/
Server: nginx
X-Pantheon-Styx-Hostname: styx-fe3-b-7f84d5c76-q4hpv
X-Styx-Req-Id: f1409a84-9832-11ed-abcb-7611ac88195f
Cache-Control: public, max-age=86400
Date: Fri, 20 Jan 2023 13:14:42 GMT
X-Served-By: cache-chi-kigq8000084-CHI, cache-fra-eddf8230050-FRA
X-Cache: HIT, HIT
X-Cache-Hits: 37, 1
X-Timer: S1674220483.602573,VS0,VE1
Vary: Cookie, Cookie
Age: 62475
Accept-Ranges: bytes
Via: 1.1 varnish, 1.1 varnish

您还可以通过使用 -v 选项显示请求和响应的详细信息,如下所示:

admin@myhome:~$ curl -v <url>

通过发送不同类型的请求并分析响应,您可以使用 wgetcurl 来调试各种 HTTP 问题,如连接问题、响应错误和性能问题。像往常一样,您可以参考这两个工具的完整文档,以加深对它们使用方法的理解。

在本节中,我们介绍了 ISO/OSI 标准定义的几层网络层。每一层都标准化了网络通信不同元素的功能。接下来,我们将讨论防火墙。

防火墙

防火墙 是一种安全措施,根据预定义的规则和策略控制进出网络流量。它通常放置在受保护的网络和互联网之间,其主要目的是阻止未经授权的访问,同时允许授权的通信。防火墙可以是硬件基础的,也可以是软件基础的,且可以使用多种技术,如数据包过滤、状态检测和应用层过滤等来控制网络流量。在这一部分,我们将介绍一种适用于 Linux 系统的防火墙。

要控制 Linux 防火墙,您需要使用 iptablesufwnftablesfirewalld。数据包过滤已内置在 Linux 内核中,因此这些命令行工具将与其交互。

iptables

iptables 是控制防火墙的最冗长工具,这意味着它没有太多的抽象,但理解基本概念很重要,这样我们才能继续使用更用户友好的工具。

如前所述,iptables 允许你创建规则来过滤和操作网络数据包,并可以根据各种标准(如 IP 地址、MAC 地址、端口和协议)控制进出网络的流量。

iptables 使用多个概念来组织规则并将其划分为功能部分:表、链、规则和目标。最一般的概念是用表来组织规则。

我们可以使用三种表:filternatmanglefilter 表用于过滤进出数据包,nat 表用于网络地址转换,mangle 表用于高级数据包修改。

每个表包含一组链,用于组织规则。例如,filter 表包含三条预定义链:INPUTOUTPUTFORWARDINPUT 链用于接收的数据包,OUTPUT 链用于发送的数据包,FORWARD 链用于转发的数据包。

每个链包含一组规则,这些规则用于匹配数据包并决定如何处理它们。每条规则都有一个匹配条件和一个动作。例如,某条规则可能会匹配来自特定 IP 地址的数据包并将其丢弃,或者它可能会匹配发送到特定端口的数据包并允许通过。

每条规则都有一个目标,它是当规则的匹配条件满足时应执行的操作。最常见的目标是 ACCEPTDROPREJECTACCEPT 表示允许数据包通过防火墙,DROP 表示丢弃数据包而不向对端反馈任何信息,REJECT 表示主动拒绝数据包,使得远端知道该端口的访问已被拒绝。

iptables 的默认表会将规则添加到 filter 表中,默认情况下,每个链(INPUTOUTPUTFORWARD)的默认策略设置为 ACCEPT。你还可以创建额外的表,并将数据包定向到该表进行后续处理。通常的做法是将至少 FORWARDINPUT 策略设置为 DROP

admin@myhome:~$ sudo iptables -P INPUT DROP
admin@myhome:~$ sudo iptables -P FORWARD DROP

同时,我们可以允许所有回环接口访问并设置为 ACCEPT

admin@myhome:~$ sudo iptables -A INPUT -i lo -j ACCEPT

此外,所有处于 ESTABLISHEDRELATED 状态的数据包应当被接受,否则我们将失去所有已建立的连接或正在建立中的连接:

admin@myhome:~$ sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

为了允许 HTTP 和 HTTPS 流量,我们可以执行以下操作:

admin@myhome:~$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
admin@myhome:~$ sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

允许 SSH 流量是一个好主意,这样我们可以远程登录到这台机器:

admin@myhome:~$ sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT

这里有一些 iptables 中常用的其他选项:

  • -A--append:将规则追加到链的末尾

  • -I--insert:在链中的特定位置插入一条规则

  • -D--delete:从链中删除一条规则

  • -P--policy:设置链的默认策略

  • -j--jump:指定规则的目标

  • -s--source:根据源 IP 地址或网络匹配数据包

  • -d--destination:根据目标 IP 地址或网络匹配数据包

  • -p--protocol:根据协议匹配数据包(例如,TCP、UDP 或 ICMP)

  • -i--in-interface:根据入站接口匹配数据包

  • -o--out-interface:根据出站接口匹配数据包

  • --sport--source-port:根据源端口匹配数据包

  • --dport--destination-port:根据目标端口匹配数据包

  • -m--match:添加匹配扩展,允许你根据其他标准(如连接状态、数据包长度等)匹配数据包

在处理 iptables 时有许多其他功能可用,例如设置 NAT、接口绑定、TCP 多路径等。我们将在本章的 高级主题 部分讨论其中一些内容。

nftables

iptables 以其冗长和缺乏内置抽象而闻名。

nftables 使用逻辑结构来组织规则,该结构包括表、链、规则和判决。表作为规则的顶级容器,并且在对规则进行分类时发挥重要作用。nftables 提供几种表类型:iparpip6bridgeinetnetdev

在每个表中,有不同的链,帮助进一步组织规则,按类别分为:filterroutenat

每个链包含单独的规则,这些规则作为匹配数据包和确定后续操作的标准。规则由匹配条件和判决组成。例如,规则可以匹配来自特定 IP 地址的数据包,并指示防火墙丢弃它们,或者它可以匹配前往特定端口的数据包,并决定接受它们。

让我们为传入和转发的数据包设置默认策略为“丢弃”(停止处理数据包并不作响应):

sudo nft add rule ip filter input drop
sudo nft add rule ip filter forward drop

此外,通常做法是允许所有环回接口访问:

sudo nft add rule ip filter input iifname "lo" accept

为确保已建立的和相关的连接被允许,你可以运行以下命令:

sudo nft add rule ip filter input ct state established,related accept

你可以运行以下命令以允许 HTTP 和 HTTPS 流量:

sudo nft add rule ip filter input tcp dport {80, 443} accept

最后,为了启用远程访问的 SSH 流量,你可以使用以下命令:

sudo nft add rule ip filter input tcp dport 22 accept

以下是一些在 nftables 中常用的选项:

  • add:将规则追加到链的末尾

  • insert:将规则插入到链中的特定位置

  • delete:从链中删除规则

  • chain:指定规则的目标

  • ip saddr:根据源 IP 地址或网络匹配数据包

  • ip daddr:根据目标 IP 地址或网络匹配数据包

  • ip protocol:根据协议匹配数据包(例如,TCP、UDP 或 ICMP)

  • iifname:根据入站接口匹配数据包

  • oifname:根据出站接口匹配数据包

  • tcp sport:根据源端口匹配数据包

  • tcp dport:根据目标端口匹配数据包

  • ct state:添加一个匹配扩展,允许根据连接状态、数据包长度等其他标准匹配数据包

nftables被视为iptables的替代品,但在现代系统中,二者都常常被使用。接下来,我们将介绍一些更抽象、更易于使用的工具。

ufw

ufw是 Linuxiptables防火墙的前端,为其提供了一个简单易用的管理界面。ufw设计上非常易于使用,它会根据你指定的配置选项自动为你设置iptables规则。它比直接使用iptables更加用户友好,适用于常见任务。

在开始使用ufw之前,你需要启用它,这样你添加或移除的所有规则在系统重启后都会持久保存。只需运行以下命令:

admin@myhome:~$ sudo ufw enable

要使用ufw打开 TCP 端口80443,你可以使用以下命令:

admin@myhome:~$ sudo ufw allow 80/tcp
admin@myhome:~$ sudo ufw allow 443/tcp

或者,你也可以使用一个命令同时打开这两个端口:

admin@myhome:~$ sudo ufw allow 80,443/tcp

一旦你打开了端口,你可以通过检查ufw的状态来验证更改:

admin@myhome:~$ sudo ufw status

ufw可在所有主要 Linux 发行版上使用,包括 Debian Linux、Ubuntu Linux、Arch Linux 和 Fedora Linux。但在某些情况下,你需要安装它,因为它并非系统的默认组成部分。

firewalld

你可以使用的另一个管理 Linux 防火墙的工具是firewalld。这是一个旨在简化防火墙动态配置的程序。firewalld的一个重要特点是区域(zones),它允许你声明不同级别的信任关系,针对不同的接口和网络。它默认包含在许多流行的 Linux 发行版中,如 Red Hat Enterprise Linux、Fedora、CentOS 和 Debian。其他一些 Linux 发行版,如 Ubuntu,默认并不包含firewalld,但你可以在这些系统上安装并使用它。

要使用firewalld打开 TCP 端口80443,你可以使用firewall-cmd命令行工具。以下是打开这些端口的命令:

admin@myhome:~$ sudo firewall-cmd --add-port=80/tcp --permanent
admin@myhome:~$ sudo firewall-cmd --add-port=443/tcp --permanent

你可以使用一个命令同时打开这两个端口:

admin@myhome:~$ sudo firewall-cmd --add-port=80/tcp --add-port=443/tcp --permanent

添加端口后,你需要重新加载防火墙以使更改生效:

admin@myhome:~$ sudo firewall-cmd --reload

你还可以使用以下命令检查端口的状态:

admin@myhome:~$ sudo firewall-cmd --list-ports

无论你使用什么工具来配置防火墙,设置默认规则策略为DROP,并只允许你希望系统处理的流量,始终是个好主意。本章篇幅有限,无法涵盖很多相关话题,但了解网络配置时的一些可能性总是有帮助的。

高级话题

在本节中,我们将介绍网络功能的更高级用法。某些功能非常常见(如端口转发或 NAT),而有些则不太为人所知。我们将从最常见的功能开始,它们是你最有可能经常遇到的,然后再介绍一些更高级和不太为人知的功能。

NAT

网络地址转换NAT)是一种将一个网络映射到另一个网络的技术。其最初的目的在于简化整个网络段的路由,而无需更改数据包中每个主机的地址。

源地址转换SNAT)是一种改变数据包源 IP 地址的 NAT 类型。它用于允许私有网络上的主机通过单个公共 IP 地址访问互联网。

目标地址转换DNAT)是一种改变数据包目标 IP 地址的 NAT 类型。它用于根据目标 IP 地址将传入的流量转发到特定的内部主机。通常用于允许外部客户端通过公共 IP 地址访问内部主机上运行的服务。

要使用iptables设置 NAT,你可以使用以下基本命令:

admin@myhome:~$ echo 1 > /proc/sys/net/ipv4/ip_forward

这将启用转发,使得你的机器能够将数据包从一个接口转发到另一个接口。这个特定的命令通过使用proc文件系统动态更新 Linux 内核配置。你也可以使用sysctl命令实现相同的功能:

admin@myhome:~$ sudo sysctl -w net.ipv4.ip_forward=1

要为本地网络如192.168.10.0/24配置 NAT,你需要以root用户身份运行以下命令:

admin@myhome:~$ sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
admin@myhome:~$ sudo iptables -A FORWARD -i eth0 -o eth1 -m state --state RELATED,ESTABLISHED -j ACCEPT
admin@myhome:~$ sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT

请注意,eth0是连接到互联网的接口,eth1是连接到内部网络的接口。

MASQUERADE目标用于在 Linux 路由器上实现 NAT。当数据包通过路由器并发送到互联网时,MASQUERADE目标会将数据包的源地址更改为路由器的公共 IP 地址。这使得内部网络中的设备能够使用路由器的公共 IP 地址作为源地址与互联网中的设备通信,有效地将内部网络隐藏于互联网之外。

MASQUERADE目标通常用于nat表的POSTROUTING链,通常应用于连接到互联网的接口。它仅适用于动态分配的 IP 地址(DHCP),通常用于家庭路由器的情况。

端口转发

端口转发是一种将网络流量从一个网络地址和端口定向到另一个地址和端口的技术。这对于将传入流量定向到计算机或网络设备上运行的特定服务或应用程序非常有用。这有助于从远程位置访问私有网络上的服务或应用程序,或者使私有网络上运行的服务或应用程序对外界可访问。

本质上,它是 NAT 的另一种用途,因为你将更改到达你机器的数据包的目标 IP(以及端口)。

要将进入eth0接口的 TCP 端口80的数据包转发到内部 IP192.168.10.101的端口8080,可以使用以下命令:

admin@myhome:~$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.10.101:8080
admin@myhome:~$ sudo iptables -t nat -A POSTROUTING -j MASQUERADE

这里我们需要使用MASQUERADE,因为我们希望隐藏内部 IP 地址。

接口绑定

active-backupbalance-rr802.3adactive-backup模式意味着当主设备发生故障时,两个绑定接口中的一个作为备用。balance-rr将同时使用两个接口,并采用轮询策略。802.3ad绑定创建共享相同技术规格(速度和双工设置)的聚合组。您可以在官方 Linux 内核网站上阅读更多关于模式和绑定设置的信息:www.kernel.org/doc/Documentation/networking/bonding.txt

TCP 多路径

TCP 多路径指的是使用多个路径在网络中两个点之间发送和接收数据,而不是仅使用一个路径。这可以通过允许当一个路径不可用时进行故障切换,并通过在多个路径之间进行负载均衡,从而提高网络的可靠性和性能。可以通过多种技术实现这一点,例如使用设备上的多个接口或通过网络使用多个路由路径。

使用iproute2软件包配置多路径非常简单。要在 Linux 上使用eth0eth1接口配置多路径,您需要运行以下命令:

admin@myhome:~$ ip route add default scope global nexthop via 192.168.1.1 dev eth0 nexthop via 192.168.2.1 dev eth1 weight 1

此命令创建了一个新的默认路由,使用eth0eth1接口,并设置权重为1。在此示例中使用的 IP 地址(192.168.1.1192.168.2.1)应替换为eth0eth1接口上下一跳路由器的实际 IP 地址。

当运行ip route show时,您将看到一个新的多路径路由。要开始使用它,您需要更改默认路由:

admin@myhome:~$ ip route change default scope global nexthop via 192.168.1.1 dev eth0 nexthop via 192.168.2.1 dev eth1 weight 1

您可以在www.multipath-tcp.org/上阅读更多关于多路径及其使用方法的信息。

BGP

边界网关协议 (BGP) 是一种路由协议,用于在单个自治系统 (AS) 内部或多个自治系统之间分发路由信息。BGP 用于在互联网骨干路由器以及企业网络中构建路由表。

BGP 路由器与其邻居交换路由信息,这些邻居可以是同一自治系统或不同自治系统中的其他 BGP 路由器。当 BGP 路由器从其邻居接收路由信息时,它使用一套规则和策略来决定将哪些路由添加到其路由表中并广告给邻居。这使得 BGP 能够支持多个通向目标的路径,并根据距离、成本或优先级等各种因素选择最佳路径。

BGP 被认为是一种路径向量协议,因为它交换关于通往目标的完整路径的信息,而不仅仅是下一个跳点。这使得 BGP 能够支持高级功能,如路由策略和流量工程。

在 Linux 机器上使用 BGP 有多种方式,具体取决于您的使用场景和网络环境。

BIRD 是一款支持 BGP 和其他路由协议的 Linux 路由守护进程。你可以配置 BIRD,使其充当 BGP 发言人并与其他 BGP 路由器交换路由信息。BIRD 可以安装在大多数 Linux 发行版上,并可以通过简单的配置文件进行配置。

Quagga 是另一款支持 BGP、OSPF 和其他路由协议的 Linux 开源路由软件套件。你可以配置 Quagga 使其充当 BGP 发言人并与其他 BGP 路由器交换路由信息。Quagga 可以安装在大多数 Linux 发行版上,并可以通过命令行接口或配置文件进行配置。

Free Range RoutingFRR)是一款支持 BGP、OSPF 和其他路由协议的 Linux 路由软件套件;它是 Quagga 的一个分支。

你可以在以下的 Linux Journal 文章中阅读更多关于 BGP 和使用 BIRD 的内容:www.linuxjournal.com/content/linux-advanced-routing-tutorial

总结

本章介绍了你在 DevOps 团队工作中可能会遇到的一些基本网络主题。这是一个起点和基础,帮助你理解处理容器内部运行的服务时与网络相关的主题。你可能还会想通过阅读关于 IPv6 协议的资料来扩展你的知识,IPv6 尚未完全取代 IPv4。

在下一章中,我们将重点介绍现代组织中主要使用的版本控制系统VCS):Git

第七章:Git,您的 DevOps 之门

Git 是一个免费的开源版本控制系统VCS),被软件开发者和团队广泛使用,用于跟踪代码库的更改并进行协作。它允许多人在同一个代码库上工作而不会覆盖彼此的更改,并记录每次对代码所做的更改,使得在必要时可以轻松回滚到以前的版本。

Git 是由 Linus Torvalds 于 2005 年为 Linux 内核的开发而创建的,后来它成为了软件行业版本控制的事实标准。全球数百万开发者使用 Git,且它得到了一个庞大且活跃的开源社区的支持。

在本章中,我们将介绍最常用的 Git 命令以及如何使用它们。我们将从设置 Git 仓库并进行首次提交的基础开始,然后继续讲解更高级的话题,如分支和合并。

本章将涵盖以下内容:

  • 基本的 Git 命令

  • 本地与远程 Git 仓库

  • GitFlow 和 GitHub Flow

技术要求

在本章中,您需要一台具有 Bash shell 的系统。您需要确保该系统已安装 Git 命令或能够安装它。我们推荐使用 Linux 或 macOS 系统,但也可以在 Windows 上设置功能齐全的 Bash 和 Git。本书并不涉及该环境的安装。

基本的 Git 命令

您可以使用很多 Git 命令,但最常用的一些命令包括:

  • git config:此命令用于配置本地 Git 环境。配置可以是全局性的,这时配置值会保存在 .gitconfig 文件中,位于您的家目录下。值也可以仅在某个仓库中设置,并保存在该仓库内。

  • git init:此命令初始化一个新的 Git 仓库。当您在某个目录中运行此命令时,它会在项目根目录下创建一个新的 .git 目录,用于跟踪项目文件所做的更改。

  • git clone:此命令创建一个远程 Git 仓库的本地副本。当您运行此命令时,它会创建一个与仓库同名的新目录,并将所有文件及其历史记录克隆到该目录中。

  • git add:此命令用于暂存文件以供提交。当您在 Git 仓库中修改文件时,这些更改不会被自动跟踪。您必须使用 git add 命令告诉 Git 跟踪您所做的更改。

  • git commit:此命令将您的更改保存到 Git 仓库中。当您运行此命令时,它会打开一个文本编辑器,让您编写提交信息,即您所做更改的简短描述。在编写并保存提交信息后,您所做的更改将被保存到仓库中,并创建一个新的提交。

  • git push:此命令将本地提交推送到远程仓库。当你运行此命令时,它会将所有本地提交推送到远程仓库,更新项目历史,并使你的更改对其他开发者可见。

  • git pull:此命令从远程仓库获取更新并将其合并到本地仓库。当你运行此命令时,它会从远程仓库获取最新的更改,并将它们合并到本地仓库中,使你的项目副本保持最新。

  • git branch:此命令用于在 Git 仓库中创建、列出或删除分支。分支允许你同时处理项目的多个版本,通常用于功能开发或修复 bug。

  • git checkoutgit switch:此命令用于在分支之间切换或恢复工作目录中的文件。当你运行此命令时,它会将工作目录切换到你指定的分支,或将指定的文件恢复到先前提交的状态。

  • git merge:此命令将一个分支合并到另一个分支。当你运行此命令时,它会将指定分支中的更改合并到当前分支,并创建一个新的提交,表示合并后的更改。

  • git stash:此命令用于暂时存储你还未准备好提交的更改。当你运行此命令时,它会将你的更改保存到一个临时区域,并将工作目录恢复到上一次提交的状态。稍后,你可以使用git stash apply命令将存储的更改恢复到工作目录。

配置本地 Git 环境

在使用 Git 命令之前,至少有两个选项需要设置。它们是你的姓名和电子邮件地址。可以通过git config命令来完成此操作。接下来,我将演示如何设置 Git 用户的姓名和电子邮件地址。我们将设置全局变量。除非专门为某个仓库单独设置,否则这些全局变量将在每个本地克隆的仓库中默认使用:

admin@myhome:~$ git config –global user.name "Damian Wojsław"
admin@myhome:~$ git config –global user.email damian@example.com

现在,当我们查看~/.gitconfig文件时,会看到以下部分:

# This is Git's per-user configuration file
[user]
    name = Damian Wojsław
    email = damian@example.com

还有更多配置选项,如默认编辑器等,但它们超出了本节内容的范围。

设置本地 Git 仓库

在你开始使用 Git 之前,需要创建一个仓库(也称为repo)。仓库是 Git 存储项目所有文件和元数据的目录。

要创建一个新的仓库,你可以使用git init命令。此命令会创建一个新的目录,并在其中生成一个.git子目录,包含仓库所需的所有文件。

例如,要在当前目录中创建一个新的仓库,你可以运行以下命令:

admin@myhome:~$ mkdir git-repository && cd git-repository
admin@myhome:~/git-repository$ git init

这会在当前目录中创建一个新的仓库,并设置必要的文件和元数据。一旦你设置了 Git 仓库,就可以开始向其中添加和提交文件。

要将一个文件添加到代码库中,你可以使用git add命令。这个命令会将文件添加到暂存区,暂存区是一个包含下次提交将包括的更改的列表。

要将一个名为main.c的文件添加到暂存区,你可以运行以下命令:

admin@myhome:~/git-repository$ git add main.py

你也可以通过用空格分隔文件或目录的名称来一次性添加多个文件:

admin@myhome:~/git-repository$ git add main.py utils.py directory/

要提交暂存区中的更改,你可以使用git commit命令。这会创建一个新的提交,其中包括暂存区中的所有更改。

每个提交都需要有提交信息。这是对你所做更改的简短描述。要指定提交信息,你可以使用-m选项,后跟消息内容:

admin@myhome:~/git-repository$ git commit -m "Added main.py and additional utils"

你也可以使用git commit命令而不加-m选项,这样会打开一个文本编辑器,你可以在其中编写更详细的提交信息。

在你进行 Git 操作的每个步骤中,你都可以使用git status命令。这用于查看 Git 代码库的当前状态。它显示哪些文件已被修改、添加或删除,以及它们是否已准备好提交。

有许多种格式化提交信息的方法,几乎每个项目都有自己的约定。一般来说,一个好的做法是添加一个来自问题跟踪系统的issue ID,然后是一个简短的描述,描述字符数不超过 72 个。第二行应留空,第三行跟随更详细的描述。以下是这样的提交信息示例:

[TICKET-123] Adding main.py and utils.py
main.py contains the root module of the application and is a default entry point for a Docker image.
utils.py contains helper functions for setting up the environment and reading configuration from the file.
Co-authors: other-commiter@myproject.example

当你运行git status时,它将显示已修改的文件列表,以及代码库中任何未跟踪的文件。它还会显示当前分支和暂存区的状态。

以下是git status命令输出的示例:

admin@myhome:~/git-repository$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   main.py
        deleted:    root/__init__.py
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        ui/main.py
no changes added to commit (use "git add" and/or "git commit -a")

在这个例子中,git status显示了两个已修改的文件(main.py__init__.py)以及一个未跟踪的文件(main.py)。它还指出当前分支是main,并且该分支与origin/main分支保持同步。

git status是一个有用的工具,可以帮助你了解当前代码库的状态,并识别哪些文件已经被修改并需要提交。你可以使用它快速查看已做的更改,并看到哪些文件已经准备好提交。它还可以显示你当前所在的分支,以及你是否与远程代码库保持同步。

git revert命令用于撤销已经对代码库所做的更改。它通过创建一个新的提交来撤销之前提交所引入的更改。

例如,假设你已经向你的代码库提交了几次更改,现在你想撤销上次提交所引入的更改。你可以使用git revert命令来实现:

admin@myhome:~/git-repository$ git revert HEAD

这将创建一个新的提交,撤销上次提交所引入的更改。你的代码库的提交历史现在看起来就像是上次提交从未发生过一样。

你也可以使用 git revert 命令撤销特定提交所引入的更改。为此,你需要指定你想撤销的提交的哈希值:

admin@myhome:~/git-repository$ git revert abc123

这将创建一个新的提交,撤销由 abc123 哈希值的提交所引入的更改。

需要注意的是,git revert 并不会删除提交或销毁历史记录。相反,它会创建一个新的提交,撤销之前提交所引入的更改。这意味着更改仍然存在于仓库中,但在当前分支的状态下不可见。

如果你需要永久删除提交,可以使用 git reset 命令或 git filter-branch 命令。然而,这些命令可能会永久销毁历史,因此使用时需要非常小心。

现在我们已经了解如何在本地仓库上工作,接下来我们可以讨论远程和本地仓库的副本。

本地与远程 Git 仓库

在 Git 中,仓库是文件及其历史的集合,以及用于管理仓库的配置文件。仓库可以是本地的,也可以是远程的。

执行 git init 命令或使用 git clone 命令克隆一个现有仓库时,你正在创建一个本地仓库。本地仓库对于在没有互联网连接时或当你希望将项目的副本保存在自己机器上时非常有用。

当你执行 git push 命令时,你正在用本地更改更新远程仓库。远程仓库对于与其他开发者合作非常有用,因为它们允许多人共同工作并互相共享更改。

Git 使用分布式版本控制系统(VCS),这意味着每个开发者的本地机器上都有仓库的完整副本。这允许开发者在本地工作,完成后将更改推送到远程仓库,与他人共享。此外,即使开发者没有连接到互联网,也可以在项目上进行协作。

git clone 命令用于创建一个远程 Git 仓库的本地副本。当你运行此命令时,它会创建一个与仓库同名的新目录,并将所有文件及其历史克隆到该目录中。

这是你可能如何使用 git clone 命令的示例:

admin@myhome:~/git-repository$ git clone https://github.com/user/repo.git

这将创建一个名为 repo 的新目录,并将位于 github.com/user/repo.git 的仓库克隆到该目录中。

你还可以通过将目录名作为参数添加,来指定本地目录的不同名称:

admin@myhome:~/git-repository$ git clone https://github.com/user/repo.git my-local-repo

这将创建一个名为 my-local-repo 的新目录,并将仓库克隆到其中。

如果你使用像 GitHub 这样的 Git 托管服务,你也可以使用仓库 URL 的简写版本:

admin@myhome:~/git-repository$ git clone git@github.com:user/repo.git

这将使用 SSH 协议克隆该仓库,这要求你为你的账户配置了一个 SSH 密钥。

git clone命令是创建远程仓库本地副本的有用方法,无论是开始一个新项目还是为现有项目做贡献。它允许你在本地工作,准备好后将更改推送回远程仓库。

与远程仓库交互

git pull命令用于从远程仓库获取更新,并将其合并到你的本地仓库中。它是git fetch命令(用于从远程仓库下载更新)和git merge命令(用于将更新合并到本地仓库)的一种组合。

git fetch命令从远程仓库下载更新,但不会将其合并到本地仓库中。相反,它将更新存储在一个名为remote-tracking branches的临时区域中。你可以使用git fetch命令更新你的远程跟踪分支,查看有哪些更改可用,但你需要使用git merge命令将这些更改实际合并到本地仓库中。

下面是一些你在使用git fetch时可能会用到的常见关键字:

  • origin:这是你从中克隆的远程仓库的默认名称。你可以使用origin来指定你想要从中获取更新的远程仓库。也可以更改默认名称并添加多个远程仓库。

  • mainmastermaster是 Git 仓库中默认分支的名称。main是 GitHub 平台引入的新默认名称。

  • REMOTE_HEAD:这是一个特殊的引用,指向远程仓库中分支的head提交。你可以使用REMOTE_HEAD来获取远程仓库当前检出的分支的更新。

  • HEAD:这是一个特殊的引用,指向当前分支在本地仓库中的head提交。

这是使用git fetch更新origin仓库中main分支的一个示例:

admin@myhome:~/git-repository$ git fetch origin main
From gitlab.com:gstlt/git-repository
 * branch            main       -> FETCH_HEAD

这将为origin仓库中的master分支下载更新,并将其存储在origin/master远程跟踪分支中。

然后,你可以使用git merge命令将更新合并到本地仓库中:

admin@myhome:~/git-repository$ git merge origin/main
Already up to date.

这将把origin/master远程跟踪分支的更新合并到你的本地master分支中。在这种情况下,我们没有需要合并的内容。

或者,你可以使用git pull命令将这两个步骤合并为一个命令来执行:

admin@myhome:~/git-repository$ git pull origin main
Already up to date.

这将从origin仓库获取更新,并将它们合并到本地仓库中,除非你已经按照本章开头的建议配置了 Git 客户端——在这种情况下,Git 将尝试在远程仓库的main分支上进行变基。

git rebasegit merge是用于将一个分支的更改合并到另一个分支中的命令。然而,它们的工作方式略有不同,对你的仓库也有不同的影响。

git rebase是一个用于将一个分支的一系列提交应用到另一个分支的命令。当你运行git rebase时,它会查看目标分支中有而源分支中没有的提交,并将它们逐个应用到源分支。这种操作的效果是重新播放源分支上的提交,并将其应用到目标分支之上。

例如,假设你在代码库中有两个分支:maindevelopmain代表主开发分支,develop代表你正在处理的某个功能。如果你对develop分支进行几次提交,然后运行git rebase main,Git 会将这些提交一个接一个地应用到main分支,就好像你直接在main分支上做了这些提交一样。

然而,git merge是一个用于将一个分支的更改合并到另一个分支的命令。当你运行git merge时,它会查看源分支中的更改,并将它们以单个提交的形式应用到目标分支。这种操作的效果是,在目标分支上创建一个新的提交,将源分支中的所有更改合并进来。

例如,你有与 rebase 示例中相同的两个分支:maindevelop。如果你对develop分支进行几次提交,然后运行git merge develop,Git 会在main分支上创建一个新的提交,将develop分支中的所有更改合并进来。

git rebasegit merge都是将一个分支的更改合并到另一个分支的有用工具,但它们对代码库有不同的影响。git rebase保持线性历史,并避免不必要的额外(合并)提交,但如果目标分支自源分支创建以来已有修改,它也可能会引发冲突。

git merge是合并更改的更直接方式,但它可能会创建许多合并提交,这会使得代码库的历史记录变得更加难以阅读,即使使用图形化工具也一样。

在合并或将本地代码库与远程版本进行 rebase 之前,检查两者之间的差异是很有用的。为此,我们可以使用git diff命令。

什么是 git diff 命令?

git diff命令用于比较对代码库所做的更改。它显示文件的两个版本或代码库中两个分支(本地或远程)之间的差异。

以下是git diff命令的一些常见用法。

比较文件当前状态和最新提交之间的差异:

admin@myhome:~/git-repository$ git diff path/to/file

这将显示文件当前状态与最后一次提交的版本之间的差异。

比较两个提交之间的差异:

admin@myhome:~/git-repository$ git diff hash1 hash2

这将显示你提供的两个提交之间的差异。

比较一个分支和其上游分支之间的差异:

admin@myhome:~/git-repository$ git diff branch-name @{u}

这将显示指定分支与其上游对应分支之间的差异。

比较暂存区与最新提交之间的差异:

admin@myhome:~/git-repository$ git diff --staged

这将显示已添加到暂存区与最新提交之间的差异。

如果你已经添加了一些文件准备提交,但在创建提交之前想再次确认,可以使用以下命令:

admin@myhome:~/git-repository$ git diff --cached

这将显示你执行了git add命令的所有文件的更改。

要比较同一仓库的本地和远程分支,你需要引用远程分支,可以通过执行git diff来实现:

admin@myhome:~/git-repository$ git diff main origin/main

origin/main指的是一个远程分支,其中远程仓库被命名为“origin”。

git diff对于理解 Git 仓库中的变化以及这些变化如何在提交时被合并非常有用。你可以使用git diff命令的各种选项和参数来指定要比较的文件版本或分支。

既然我们已经讨论了如何比较仓库中的更改,这里有一个特别的场景,这项技能非常有用:在重新基准(rebase)或合并(merge)更改时解决仓库中的冲突。

查看提交历史

Git 会记录每一次对仓库进行的提交,你可以使用git log命令查看提交历史。这将显示仓库中所有提交的列表,包括提交信息和提交日期。

git log命令的常见用法包括以下几种:

  • 使用git log查看仓库的完整提交历史,包括已做的更改及这些更改的原因。当你在一个项目中工作并且需要理解它是如何随着时间演变时,这非常有帮助。

  • 使用git log查看受影响文件的提交历史,并查看哪个提交引入了这个 bug。

  • 使用git log查看受影响文件的历史,并查看哪些更改可能导致了问题。

你可以使用多种选项配合git log来定制其输出并筛选显示的提交。例如,你可以使用--author选项仅显示由特定人员提交的内容,或者使用--since选项仅显示过去一个月内的提交。

下面是一些使用git log命令的示例。

要显示当前仓库的提交历史,请调用git log命令:

admin@myhome:~/git-repository$ git log
commit 9cc1536fb04be3422ce18a6271ab83f419840ae3 (HEAD -> main, origin/main, origin/HEAD)
Author: Grzegorz Adamowicz <grzegorz@devopsfury.com>
Date:   Wed Jan 4 10:13:26 2023 +0100
    Add first version of the table schema
commit fb8a64f0fd7d21c5df7360cac427668c70545c83
Author: Grzegorz Adamowicz <grzegorz@devopsfury.com>
Date:   Tue Jan 3 22:02:25 2023 +0100
    Add testing data, mock Azure class, remove not needed comments
commit ae0ac170f01142dd80bafcf40fd2616cd1b1bc0b
Author: Grzegorz Adamowicz <grzegorz@devopsfury.com>
Date:   Tue Dec 27 14:50:47 2022 +0100
    Initial commit

你可以通过运行以下命令查看特定文件的提交历史:

admin@myhome:~/git-repository$ git log path/to/file

使用此命令,你可以显示特定分支的提交历史:

admin@myhome:~/git-repository$ git log branch-name

这将显示指定分支的提交历史,仅显示该分支上的提交。

你可以使用此命令显示一段提交范围内的提交历史:

admin@myhome:~/git-repository$ git log hash1..hash2

这将显示hash1提交 ID 和hash2提交 ID 之间的提交历史,仅显示发生在该范围内的提交。

git log --oneline将以紧凑的格式显示提交历史,仅显示每个提交的提交哈希值和消息:

admin@myhome:~/git-repository$ git log --oneline

使用此命令,你可以显示包含差异的提交历史:

admin@myhome:~/git-repository$ git log -p

这将显示提交历史,并展示每个提交的差异,显示每个提交中所做的更改。

这些只是你可以使用git log命令的一些示例。你可以在 Git 文档中找到更多关于可用选项的信息。

在下一部分,我们将探讨在将更改合并回main分支之前如何缩短我们的 Git 历史记录。如果我们在本地开发分支中有很长的提交历史,这会非常有用。

分支管理

git branchgit switch是两个用于管理 Git 仓库中分支的命令。

git branch是一个用于创建、列出或删除仓库中分支的命令。你可以通过指定分支名称作为参数来创建一个新的分支,像这样:

admin@myhome:~/git-repository$ git branch new-branch

这将创建一个名为new-branch的新分支,该分支基于当前分支。

你可以使用git branch命令配合-a选项列出仓库中的所有分支,包括本地分支和远程跟踪分支:

admin@myhome:~/git-repository$ git branch -a

你可以使用git branch命令配合-d选项删除一个分支,示例如下:

admin@myhome:~/git-repository$ git branch -d old-branch

这将删除old branch,但仅在它已经完全合并到上游分支的情况下。如果你希望删除此分支,可以添加--force选项,或使用-D选项,它是--delete --force Git 分支的别名。

最后,要删除一个远程分支,我们需要使用git push命令:

admin@myhome:~/git-repository$ git push origin --delete old-branch

这是一个具有破坏性的命令,无法撤销,因此在使用时应该谨慎操作。

git switch是一个用于在仓库中切换分支的命令。你可以通过指定分支名称作为参数来切换到不同的分支,像这样:

admin@myhome:~/git-repository$ git switch new-branch

这将切换到new-branch分支,并将其设置为当前分支。

git branchgit switch命令都允许你创建新分支、列出可用分支,并根据需要在分支之间切换。在接下来的部分,我们将介绍两种与 Git 工作相关的方法,称为工作流:git workflowgithub workflow

使用交互式变基合并提交

要使用git rebase压缩提交,你首先需要确定你想要压缩的提交范围。通常通过指定范围内第一个提交的哈希值和最后一个提交的哈希值来完成此操作。例如,如果你想压缩仓库中的最后三个提交,可以使用以下命令:

admin@myhome:~/git-repository$ git rebase -i HEAD~3

这将打开一个编辑器窗口,显示最近三次提交的列表,并附带一些说明。每个提交在文件中占据一行,行首是pick。要压缩某个提交,你需要将pick改为squashs

以下是文件可能的示例:

pick abc123 Add feature X
pick def456 Fix bug in feature X
pick ghi789 Add test for feature X

若要将第二个和第三个提交压缩到第一个提交中,你需要将文件更改为如下所示:

pick abc123 Add feature X
squash def456 Fix bug in feature X
squash ghi789 Add test for feature X

在做出更改后,你可以保存并关闭文件。Git 将应用这些更改,并向你展示另一个编辑器窗口,你可以在此输入合并提交的新提交消息。

需要注意的是,使用git rebase压缩提交可能是一个具有破坏性的操作,因为它会永久改变仓库的提交历史。一般建议在使用git rebase时小心,并确保在使用前有仓库的备份。

如果你想撤销git rebase操作所做的更改,可以使用git rebase --abort命令来丢弃这些更改,并将仓库恢复到先前的状态。

在成功压缩提交后,你可以将其推送到远程仓库,但必须使用git push --force命令,这将忽略你刚才重写的提交历史。这是一个破坏性操作,无法撤销,因此在使用--force选项推送更改前,请务必进行多次确认。

解决 Git 冲突

当你尝试合并或变基(rebase)存在冲突更改的分支时,可能会发生冲突。这通常是因为两个分支中相同的代码行都有修改,而 Git 无法自动解决这些冲突。

当在合并或变基过程中发生冲突时,Git 会在受影响的文件中标记冲突的行,你需要手动解决这些冲突才能继续操作。

下面是一个在合并过程中解决冲突的示例。

运行git merge来合并两个分支:

admin@myhome:~/git-repository$ git merge branch-to-merge

Git 将检测到任何冲突,并标记受影响文件中的冲突行。冲突的行将被<<<<<<<=======>>>>>>>标记包围:

<<<<<<< HEAD
something = 'this is in our HEAD';
=======
something = 'this is in our branch-to-merge';
>>>>>>> branch-to-merge

打开受影响的文件并解决冲突,选择你想保留的代码版本。你可以保留当前分支(HEAD)的版本,或是你正在合并的分支(branch-to-merge)的版本:

something = 'this is in our branch-to-merge';

使用git add暂存已解决的文件:

admin@myhome:~/git-repository$ git add path/to/file

继续合并或变基操作,运行git rebase --continuegit merge --continue

admin@myhome:~/git-repository$ git merge --continue

在合并或变基过程中解决冲突可能是一个繁琐的过程,但它是使用 Git 时非常重要的一部分。通过仔细审查冲突的更改并选择正确的代码版本,你可以确保仓库的一致性并避免错误。

另外,定期将本地仓库与源分支同步,在合并或变基之前是一个好主意,以防您遇到无法解决的冲突或其他问题。这有助于您在过程中恢复任何可能发生的错误或意外。

还有一些非常棒的图形化工具,可以以更互动的方式解决冲突。如果您使用的是KDiff3WinMergeMeld或其他类似工具。

在本节中,我们已解释了分支、仓库、本地和远程仓库、合并和变基的概念。接下来,我们将研究浏览仓库的历史更改并进行修改。

GitFlow 和 GitHub Flow

developmasterdevelop分支用于开发新特性,而master分支表示当前的生产就绪状态。还有几个支持分支,如featurereleasehotfix,用于管理软件的开发、发布和维护过程。

使用拉取请求(pull requests)向master分支进行合并。没有单独的develop分支,master分支始终被认为是当前的生产就绪版本。

另一种非常相似的分支模型是GitLab Flow。它用于管理软件项目的开发和维护,专门设计用于与 GitLab 一起使用,GitLab 是一个基于 Web 的 Git 仓库管理工具,提供源代码管理SCM)、持续集成CI)等功能。

在 GitLab Flow 模型中,所有开发都在分支中进行,新特性通过合并请求合并到master分支。没有单独的develop分支,master分支始终被认为是当前的生产就绪版本。然而,GitLab Flow 确实包含一些附加功能和工具,如使用受保护分支和合并请求审批的功能,这可以帮助团队执行开发最佳实践并保持高水平的代码质量。

在下一节中,我们将深入研究如何使用配置文件根据我们的需求来配置 Git。

全局 Git 配置 - .gitconfig

.gitconfig文件是一个用于设置 Git 全局选项的配置文件。它通常位于用户的home目录中,可以使用任何文本编辑器进行编辑。

您可能想在.gitconfig中包含的一些常见选项如下:

  • user.nameuser.email:这些选项指定将与您的 Git 提交关联的用户名和电子邮件地址。正确设置这些选项非常重要,因为它们将用于识别您作为提交的作者。

  • color.ui:此选项启用或禁用 Git 中的彩色输出。您可以将此选项设置为auto,以便在 Git 在支持彩色输出的终端中运行时启用彩色输出,或者将其设置为truefalse,分别表示始终启用或禁用彩色输出。

  • core.editor:此选项指定 Git 在需要你输入提交信息或其他输入时使用的文本编辑器。你可以将此选项设置为你喜欢的文本编辑器命令,如nanoviemacs

  • merge.tool:此选项指定 Git 在合并分支时解决冲突的工具。你可以将此选项设置为可视化合并工具的命令,如kdiff3meldtkdiff

  • push.default:此选项指定当你没有指定分支时,git push命令的行为。你可以将此选项设置为simple,它会将当前分支推送到远程的同名分支,或者设置为upstream,它会将当前分支推送到它正在跟踪的远程分支。

  • alias.*:这些选项允许你为 Git 命令创建别名。例如,你可以将alias.st设置为status,这将允许你使用git st命令代替git status

以下是一个示例.gitconfig文件,使用了前面提到的选项,并在每个部分后面添加了注释:

[user]
name = Jane Doe
email = jane@doe.example
[color]
    ui = always

user部分定义了每个提交的作者用户名和电子邮件。color部分启用颜色,以便提高可读性。ui = always将始终启用颜色,适用于所有输出类型(无论是机器消费还是其他)。其他可能的选项有trueautofalsenever

alias部分允许你在使用 Git 时简化一些长命令。它将在左侧定义一个别名,用于 Git 命令,你可以将它添加到等号后面。

这是一个示例:

[alias]
  ci = commit

我们将ci命令定义为commit。在将此内容添加到你的.gitconfig文件中后,你将获得另一个 Git 命令:git ci,它实际上会执行git commit命令。你可以为日常使用的常见 Git 命令添加别名。以下命令告诉 Git 去哪里查找默认的.gitignore文件:

[core]
    excludesfile = ~/.gitignore

以下的push设置更改了默认的推送行为,这将要求你指定要将本地分支推送到哪个远程分支:

[push]
  default = current

通过指定current选项,我们指示 Git 尝试将本地分支推送到一个与本地分支名称完全相同的远程分支。其他选项在这里列出:

  • nothing:不尝试推送任何内容。

  • matching:将所有本地和远程名称相同的分支视为匹配并推送所有匹配的分支。

  • upstream:将当前分支推送到上游分支。

  • simple:这是默认选项。如果本地分支的名称与远程上游分支的名称不同,它将拒绝推送到上游。

当运行git pull命令时,Git 会尝试将远程提交合并到你的本地分支。默认情况下,它会尝试进行上游合并,这可能会导致不必要的合并提交。这将改变默认行为为rebase

[pull]
rebase = false

更多信息请参见 本地与远程 Git 仓库 部分。

使用 .gitignore 配置文件忽略某些文件

.gitignore 文件是一个配置文件,用来告诉 Git 在跟踪仓库中的更改时,哪些文件或目录需要被忽略。如果你有一些由构建过程生成的文件,或者是特定于本地环境的文件,或者是与项目无关且不需要被 Git 跟踪的文件,这个功能会非常有用。

下面是一些你可能会在 .gitignore 文件中包含的文件和目录类型的示例:

  • *.tmp*.bak*.swp 文件。

  • *.exe*.jar*.war 文件,以及 bin/obj/dist/ 目录。

  • 如果你使用 npm,则为 node_modules/ 目录;如果你使用 Composer,则为 vendor/ 目录。

  • 如果你使用的是 JetBrains IDEs,则为 idea/ 目录;如果你使用的是 Visual Studio Code,则为 .vscode/ 目录。

  • 敏感信息:包含密码或 API 密钥的文件。我们强烈建议特别小心忽略这些文件,以防它们被提交到仓库中。这将为你避免许多麻烦和不必要的风险。现在,像 GitHub 的 Dependabot 这样的机器人会在这些敏感文件进入你的仓库时发出警报(甚至阻止提交),但最好还是在开发过程早期就避免这些问题。

这是一个示例 .gitignore 文件,它会忽略一些常见类型的文件:

# Temporary files or logs
*.tmp
*.bak
*.swp
*.log
# Build artifacts
bin/
obj/
dist/
venv/
*.jar
*.war
*.exe
# Dependencies
node_modules/
vendor/
requirements.txt
# IDE-specific files
.idea/
.vscode/
# Sensitive information
secrets.txt
api_keys.json

也可以使用通配符和 .gitignore 文件来处理某些目录中需要特殊规则的情况,而这些规则不希望在全局范围内生效。

最后,你可以将你全站范围的 .gitignore 文件放入你的主目录,以确保不会提交任何本地开发所需的文件。

总结

Git 是一个非常强大的工具,很难将其所有功能浓缩到一本书中。在这一章中,我们学习了作为 DevOps 工程师在日常工作中最基本的任务,掌握了处理大多数问题所需的技能。

在下一章,我们将重点介绍 Docker 和容器化,并将在那里将你迄今为止获得的所有技能付诸实践。

第八章:Docker 基础

本章我们介绍 DevOps 工具包的一个基础构件——容器。我们将解释虚拟化和容器之间的区别,并展示这两种解决方案的优缺点。此外,我们还将展示如何根据工作负载选择适合的解决方案。

本章覆盖的主要主题如下:

  • 虚拟化与容器化

  • Docker 架构

  • Docker 命令

  • Dockerfile

  • Docker 镜像注册表

  • Docker 网络

技术要求

本章你需要一台安装了 Docker 引擎的 Linux 系统。我们这里不会涉及安装步骤。不同的 Linux 发行版提供 Docker 的方式不同。我们将使用 Docker 引擎版本 20.10.23。由于本章的所有示例都非常基础,较旧版本的 Docker 很可能也能正常工作。不过,如果你在跟随我们的示例时遇到问题,更新 Docker 到我们这个版本应当是排查问题的第一步。

虚拟化与容器化

本节我们将解释虚拟化和容器化是什么,它们之间的主要区别是什么。

虚拟化

虚拟化是一种在另一台计算机内运行完整模拟计算机的技术。完整意味着它模拟了物理计算机所具有的所有硬件:主板、BIOS、处理器、硬盘、USB 端口等。模拟意味着它完全是软件的产物。这台计算机在物理上并不存在,因此被称为虚拟计算机。为了存在,虚拟机(VM)需要一台真实的物理计算机来模拟它。物理计算机被称为宿主机或虚拟化管理程序(Hypervisor)。

所以,我有一台物理计算机。它非常强大。我为什么要在它上运行虚拟机呢?显而易见,虚拟机的性能会比主机差:毕竟,主机需要为自己分配 RAM、CPU 和硬盘空间。与物理机相比,虚拟机的性能也会有一些小幅下降(因为我们实际上是在运行一个模拟完整硬件的程序)。

理由因使用场景而异,但有很多。

你可能想要运行一个与你自己的操作系统不同的完整操作系统,用来测试一些软件,运行你当前操作系统中没有的软件,或者是为了忠实地重建你的应用程序开发环境。你可能想尽可能精确地重建一个生产环境来测试你的应用程序。这些都是使用虚拟机的有效且非常流行的理由。

让我们来看看虚拟化的优势:

  • 隔离:正如前面所提到的,虚拟机(VMs)表现为完全功能的计算机。对运行中的操作系统来说,它们创造了与物理机器分离的假象。我们在虚拟机中运行的任何程序都不应能访问主机计算机(除非明确允许),实际上,除了几个由于编程错误导致的事故,虚拟机一直提供安全的环境。这种隔离在恶意软件分析、运行需要独立服务器的工作负载等场景中是一个非常好的解决方案。举例来说,如果一个虚拟机运行一个单独的 WWW 服务器,那么服务器的安全漏洞可能会使攻击者获取操作系统的访问权限,从而让他们可以自由操作。但由于其他基础设施组件(例如数据库)是运行在独立的虚拟机中的,这个问题只能局限于 WWW 服务器。

  • 调优:借助足够强大的主机,可以对其资源进行分区,以确保每个运行中的虚拟机都有保证的内存、硬盘空间和 CPU。

  • 操作系统简化:当运行各种工作负载时,比如数据库、WWW 服务器和邮件服务器,维持单一服务器同时运行这些服务的复杂性会迅速增加。每安装一个软件就需要安装额外的软件(例如库文件和辅助程序)。不同程序所需的库可能会引发不兼容的问题(尤其是当我们安装的不是操作系统开发者发布的软件,即所谓的第三方程序时)。在少数情况下,甚至操作系统自带的软件之间也可能存在不兼容的问题,使得在一个操作系统上安装它们变得不可能或非常困难。维护这样的系统可能会变得麻烦,并需要大量的排查工作。现代的虚拟机管理软件通过克隆、快照、黄金镜像等方式缓解了许多系统管理的难题。

  • 自动化:现代虚拟化软件提供了许多功能,促进了多层次的系统管理自动化。快照——即系统的某一时刻快照——允许在任何时刻回滚到之前的系统状态。这使得轻松回退到最后一个已知的良好状态成为可能,避免了不想要的变化。克隆可以让我们基于另一个已经运行并配置好的虚拟机来配置新的虚拟机。黄金镜像是虚拟机的存档镜像,我们可以轻松快速地导入并启动,完全省略了安装过程,并将配置限制到绝对最小化。这也使得环境的可靠重建成为可能。

  • 加速:正确设置虚拟机工作流可以让我们在几分钟内启动一个新的操作系统,配备自己的服务器或桌面硬件,而不是几小时。这为测试环境、远程桌面解决方案等开辟了新的可能性。

上述列表并非详尽无遗,但应该能清楚地展示为什么虚拟化成为数据中心和托管公司宠爱的技术。我们可以廉价租用的各种服务器,正是虚拟化使公司能够对硬件进行分区的直接结果。

尽管这一解决方案非常出色,但它并不是万灵药,并不是所有东西都应该虚拟化。而且,为每个软件单独运行一个虚拟机,很容易导致资源利用率的开销。100 个虚拟服务器不仅会使用分配给操作系统的 CPU 和 RAM,还会有一部分被用来做主机机器的管理工作。每个虚拟服务器都会占用操作系统所需的磁盘空间,即使它可能与同一服务器上其他 99 个虚拟机是完全一样的——这是一种空间、RAM 和 CPU 的浪费。而且,启动一个新的虚拟机也需要一些时间。诚然,如果一切配置和自动化得当,启动时间会比设置一台新硬件机器要短,但仍然需要时间。

在虚拟化广泛普及之前,操作系统开发人员试图提供一些技术,使系统管理员能够隔离各种工作负载。主要目标是数据中心,在那里一个物理服务器对于一个工作负载(如数据库或 WWW 服务器)来说已经太多,但运行多个工作负载又存在风险(如安全性、稳定性等)。随着虚拟化的普及,人们很快意识到有时使用虚拟化就像用大炮打麻雀。当你只想运行一个小程序时,采购一个全新的服务器(即便是虚拟的)和整个操作系统是没有意义的。因此,通过不断创新出更好的进程隔离特性,容器应运而生。

容器化

容器是轻量级的虚拟环境,允许我们以隔离的方式运行单个进程。一个理想配置的容器只包含运行应用程序所需的软件和库。主机操作系统负责操作硬件、管理内存和其他外围任务。容器的主要假设是它并不模拟一个独立的操作系统或独立的服务器。容器中的进程或用户可以轻松发现自己被隔离在其中。缺点是容器不会模拟硬件。例如,你不能用它们来测试新驱动程序。优点是单个容器的硬盘空间占用可能仅为几个兆字节,只需要运行进程所需的内存。

因此,启动一个容器只需要应用程序启动所需的时间。启动时间——BIOS、硬件测试和操作系统启动时间——都被缩短了。所有应用程序不需要的软件可以并且应该被省略。由于容器镜像的体积较小,它们的重新分发时间几乎可以忽略不计,启动时间几乎是瞬时的,构建时间和过程也大大简化。这使得环境的重建变得更加容易。反过来,这使得测试环境的设置变得更简单,而且通常会频繁地部署软件的新版本——有时一天可以进行几千次部署。应用程序的扩展变得更加容易和快速。

上述变化引入了另一种运行应用程序的方法。上述变化的逻辑结果是,容器的维护方式与操作系统不同。你不会在容器内升级软件——你会部署一个包含新版本软件的容器,替换掉过时的版本。这就导致了一个假设:你不应该在容器内保存数据,而是将数据保存在运行时挂载到容器的文件系统中。

Linux 容器化的代表性技术是Docker。Docker 所做的一件事可能帮助推动了这场革命,那就是创建了一个容器镜像共享的生态系统。

在 Docker 中,容器镜像是一个简单的存档,包含了应用程序所需的所有二进制文件和库以及一些配置文件。由于镜像体积通常较小,且镜像内部从不包含数据,因此允许人们共享他们构建的镜像是合乎逻辑的。Docker 有一个镜像中心(称为 Docker Hub),提供了一个漂亮的 WWW 界面,以及用于搜索、下载和上传镜像的命令行工具。这个中心允许对镜像进行评分,并给作者提供评论和反馈。

既然我们已经知道容器化是什么,我们可以更深入地了解 Docker 如何在内部工作以及它的运作原理。让我们来看看 Docker 的构成。

Docker 的构成

Docker 包含多个组件:

  • 命令行工具 – Docker

  • 主机

  • 对象

  • 注册表

Docker CLI 工具——docker——是管理容器和镜像的主要工具。它用于构建镜像、从注册表中拉取镜像、上传镜像到注册表、运行容器、与容器互动、设置运行时选项,并最终销毁容器。它是一个命令行工具,通过 API 与 Docker 主机进行通信。默认情况下,假设在主机上调用 docker 命令,但这并非严格要求。一个 docker CLI 工具可以管理多个主机。

主机更有趣。主机运行dockerd——一个负责实际执行通过docker工具下达的操作的守护进程。正是在这里存储容器镜像。主机还提供诸如网络、存储以及容器本身等资源。

dockerd守护进程是容器的心脏。它是一个在主机上运行的后台进程,负责管理容器。dockerd管理容器的创建与管理,提供与守护进程交互的 API,管理卷、网络和镜像分发,提供管理镜像和容器的接口,并存储和管理容器与镜像的元数据。它还负责在 Docker Swarm 模式下管理其他进程之间的通信。

OverlayFS

OverlayFS首次作为 Linux 内核版本 3.18 的一部分于 2014 年 8 月发布。最初它是为了提供一种比之前的存储驱动程序Another UnionFSAUFS)更高效和灵活的方式来处理容器存储。OverlayFS 被认为是 UnionFS 的下一代,而 UnionFS 是当时 Docker 使用的存储驱动程序。

从 Docker 版本 1.9.0 开始,这个文件系统被作为内置存储驱动程序包含在 Docker 中。从那时起,OverlayFS 成为大多数 Linux 发行版中 Docker 的默认存储驱动程序,并且在各种容器编排平台(如 Kubernetes 和 OpenShift)中得到广泛使用。

OverlayFS 是 Linux 的一个文件系统,允许一个目录覆盖另一个目录。它允许创建一个由两个不同目录组成的虚拟文件系统:一个下层目录和一个上层目录。上层目录包含用户可见的文件,而下层目录包含隐藏的基础文件。

当访问上层目录中的文件或目录时,OverlayFS 首先在上层目录中查找,如果没有找到,再查找下层目录。如果在上层目录中找到该文件或目录,则使用该版本;如果在下层目录中找到,则使用该版本。

该机制允许创建覆盖文件系统,在这种文件系统中,上层目录可以用于添加、修改或删除下层目录中的文件和目录,而无需修改下层目录本身。这在容器化场景中非常有用,其中上层可以用于存储在容器中所做的更改,而下层则包含容器的基础镜像。

什么是镜像?

Docker 镜像是一个预构建的包,包含了在容器中运行软件所需的所有文件和设置。它包括你的应用程序代码或二进制文件、运行时、系统工具、库和所有需要的配置文件。一旦镜像构建完成,它可以用于启动一个或多个容器,这些容器是提供一致方式运行软件的隔离环境。

启动容器时,你必须选择一个程序作为容器的主进程来运行。如果这个进程退出,整个容器也会被终止。

构建一个 Docker 镜像通常涉及创建一个 Dockerfile,这是一个包含构建镜像指令的脚本。Dockerfile 指定了使用的基础镜像、需要安装的任何附加软件、需要添加到镜像中的文件以及需要应用的任何配置设置。

在构建镜像时,Docker 会读取 Dockerfile 中的指令并执行我们在 Dockerfile 中准备的步骤。

一旦镜像构建完成,它可以被保存并用于启动一个或多个容器。构建镜像的过程也可以通过使用 Jenkins、GitHub 或 GitLab 等工具来自动化,这些工具可以在每次对代码库进行更改时自动构建和测试新的镜像。

结果镜像由一个唯一的 ID(SHA-256 哈希)组成,这是镜像内容和元数据的哈希值,它还可以有一个标签,这是一个可读的字符串,可以用来引用镜像的特定版本。UnionFS 负责在运行容器时合并所有内容。

要检查镜像的元数据和内容部分,可以运行以下命令:

admin@myhome:~$ docker pull ubuntu
admin@myhome:~$ docker inspect ubuntu
[
    {
        "Id": "sha256:6b7dfa7e8fdbe18ad425dd965a1049d984f31cf0ad57fa6d5377cca355e65f03",
        "RepoTags": [
            "ubuntu:latest"
        ],
        "RepoDigests": [
            "ubuntu@sha256:27cb6e6ccef575a4698b66f5de06c7ecd61589132d5a91d098f7f3f9285415a9"
        ],
        "Created": "2022-12-09T01:20:31.321639501Z",
        "Container": "8bf713004e88c9bc4d60fe0527a509636598e73e3ad1e71a9c9123c863c17c31",
            "Image": sha256:070606cf58d59117ddc1c48c0af233d6761addbcd4bf9e8e39fd10eef13c1bb7",
        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/f2c75e37be7af790f0823f6e576ec511396582ba71defe5a3ad0f661a632f11e/merged",
                "UpperDir": "/var/lib/docker/overlay2/f2c75e37be7af790f0823f6e576ec511396582ba71defe5a3ad0f661a632f11e/diff",
                "WorkDir": "/var/lib/docker/overlay2/f2c75e37be7af790f0823f6e576ec511396582ba71defe5a3ad0f661a632f11e/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": ["sha256:6515074984c6f8bb1b8a9962c8fb5f310fc85e70b04c88442a3939c026dbfad3"
            ]
        },
    }
]

这里有很多信息,所以我们已经去除了不必要的输出,只保留了我们要关注的信息。你可以看到一个镜像 ID,GraphDriver部分下合并的所有目录,以及RootFS sha256层。RootFS包含了当我们在容器内启动一个进程时,由 UnionFS 创建的整个文件系统。

什么是容器运行时?

容器运行时(或容器引擎)是一个在你的系统上运行容器的软件组件。容器运行时从 Docker 注册表加载容器镜像,监控系统资源,为容器分配系统资源,并管理其生命周期。

有许多正在使用的运行时容器。最著名并且在笔记本电脑上使用得最多的容器运行时是containerd——你可能已经在你的系统上安装了它。它是一个高性能的容器运行时,旨在嵌入到更大的系统中。许多云服务提供商使用它,它也是 Kubernetes 的默认运行时。

LXC 是一个使用 Linux 命名空间和 cgroups 提供容器隔离的运行时。它被认为比 Docker(containerd)更加轻量级和高效,但也更难使用。

另一个有趣的运行时是 Open Container 接口容器运行时CRI-O)。CRI-O 完全符合 Open Container Initiative (OCI) 规范,这意味着它可以运行任何符合 OCI 规范的容器镜像。此外,它设计为与 Kubernetes Pods 原生兼容,这使得它在与 Kubernetes 的集成上优于其他运行时。

Rocket (rkt) 是一种替代的容器运行时,旨在比 Docker 更加安全和高效。它使用 App Container (appc) 镜像格式,并且架构比 Docker 更加简洁。它也不是很常用。

其他值得注意的容器引擎包括 run Open Container (runC),这是一个低级别的容器引擎,提供了创建和管理容器的基本功能,以及由 AWS 开发的 Firecracker。

cgroups

Linux cgroups(即 控制组)是 Linux 内核的一项功能,允许管理和隔离一组进程的系统资源。Cgroups 允许系统管理员为特定的进程组分配资源,如 CPU、内存和网络带宽,并监控和控制它们的使用。

这可以用于限制特定应用程序或用户使用的资源,或者在共享系统上隔离不同类型的工作负载。

默认情况下,Docker 不会限制容器内应用程序的 CPU 或内存使用。启用此功能非常简单,不需要直接与 cgroups 或内核设置交互——Docker 守护进程会为我们处理这些。

你可以通过在 docker run 命令中使用 --memory-m 选项来限制 Docker 容器可以使用的内存量。

例如,使用以下命令以 500 MB 的内存限制运行 alpine 镜像:

admin@myhome:~$ docker run --memory 500m alpine /bin/sh

你可以通过使用适当的后缀(bkmg)来指定内存限制,单位可以是字节、千字节、兆字节或吉字节。

当你限制容器的内存时,Docker 还会限制容器可以使用的交换内存量。默认情况下,交换内存的限制是内存限制的两倍。也可以通过使用 --memory-swap--memory-swappiness 选项来限制交换内存。

通过使用 CPU 配额限制(--cpus-c 选项),可以限制 Docker 容器内应用程序可以使用的 CPU 时间。CPU 配额是容器可以使用的 CPU 时间的相对度量。默认情况下,容器会被分配一定数量的 CPU 配额,容器可以根据其分配的份额来消耗 CPU 时间。例如,如果一个容器有 0.5 个 CPU 配额,那么如果没有其他容器消耗 CPU,它最多可以使用 50% 的 CPU 时间。

其他可用的选项如下:

  • --cpuset-cpus:此选项允许你指定容器可以使用的 CPU 核心范围,例如 0-1 表示使用前两个核心,或者 0,2 表示使用第一个和第三个核心。

  • --cpu-shares:允许你为 Docker 容器设置 CPU 时间限制。它指定了容器在给定时间内可以使用的 CPU 时间(以微秒为单位)。时间段由 --cpu-period 选项指定。

  • --cpu-quota--cpu-period--cpu-quota 是 CPU 时间限制(以微秒为单位),--cpu-period 是 CPU 时间周期的长度(以微秒为单位)。

--cpu-quota--cpu-period 选项允许你为容器指定比 --cpus--cpuset-cpus 选项更精确的 CPU 时间限制。如果你需要更精确地限制容器的 CPU 时间,以防止性能问题或确保应用程序可靠运行,这些选项非常有用。

在这一节中,我们讲解了容器运行时及其工作原理。接下来,我们将探讨 containerd 守护进程的命令行界面,以便更轻松、强大地与所有 Docker 组件交互。

Docker 命令

containerd 守护进程使用套接字文件或网络。

你可以使用的最常见命令如下:

  • build:允许你使用 Dockerfile 构建新的 Docker 镜像

  • run:启动一个新的容器

  • start:重新启动一个或多个停止的容器

  • stop:停止一个或多个正在运行的容器

  • login:用于访问私有注册表

  • pull:从注册表下载镜像或仓库

  • push:将镜像或仓库上传到注册表

  • build:帮助从提供的 Dockerfile 创建镜像

  • images:列出你机器上的所有镜像

  • ps:列出所有正在运行的容器

  • exec:在正在运行的容器中执行命令

  • logs:显示容器的日志

  • rm:删除一个或多个容器

  • rmi:删除一个或多个镜像

  • network:用于管理 Docker 网络

  • volume:用于管理卷

docker build

docker build 命令用于从 Dockerfile 构建 Docker 镜像。基本语法如下:

docker build [OPTIONS] PATH | URL | -

PATH 是包含 Dockerfile 的目录路径。

URL 是指向包含 Dockerfile 的 Git 仓库的 URL。

-(破折号)用于从 stdin 的内容构建镜像,因此你可以将 Dockerfile 内容从先前命令的输出管道传输给它,例如,从模板生成它。

要从当前目录中的 Dockerfile 构建镜像,你需要运行以下命令:

admin@myhome:~$ docker build .

你也可以使用一个特定标签来构建镜像,如以下示例所示:

admin@myhome:~$ docker build -t my-image:1.0 .

你也可以将 --build-arg 参数传递给构建命令,以将构建时的变量传递给 Dockerfile:

admin@myhome:~$ docker build --build-arg VAR1=value1 -t my-image:1.0 .

docker run

当你运行一个容器时,本质上是将一个 Docker 镜像拿来并在该环境中执行一个进程。镜像是一个容器的蓝图或快照;它是一个只读模板,包含创建容器的指令。正在运行的容器是该镜像的一个实例,但有自己的状态。

docker run命令用于从 Docker 镜像启动一个容器。例如,要从myimage镜像启动一个容器并运行/bin/bash,你可以运行以下命令:

admin@myhome:~$ docker run myimage /bin/bash

你也可以向run命令传递选项,如下例所示:

admin@myhome:~$ docker run -d -p 8080:80 --name containername myimage

此命令以分离模式启动容器(-d选项;它将容器置于后台),将容器中的端口80映射到主机的端口8080-p 8080:80),并将容器命名为containername--``name containername)。

你还可以向容器传递环境变量:

admin@myhome:~$ docker run -e VAR1=value1 -e VAR2=value2 myimage:latest

Docker 容器在被终止后不会存储数据。为了使数据持久化,你需要使用容器本身以外的存储。在最简单的设置中,这可以是容器外部文件系统中的一个目录或文件。

有两种方法可以实现:创建一个 Docker 卷:

admin@myhome:~$ docker volume create myvolume

要使用此卷进行数据持久化,你需要在启动容器时挂载它:

admin@myhome:~$ docker run –v myvolume:/mnt/volume myimage:latest

你还可以绑定挂载本地文件夹(-v选项。在这种情况下,你不需要运行 Docker 卷创建命令:

admin@myhome:~$ docker run -v /host/path:/mnt/volume myimage:latest

你还可以使用-``w选项来指定容器内的工作目录:

admin@myhome:~$ docker run -w /opt/srv my-image

其他有用的选项如下:

  • --rm:此选项将在容器停止后删除该容器

  • -P--publish-all:此选项将所有暴露的端口(Dockerfile中的EXPOSE选项)发布到一个随机的本地端口

  • --network:此选项将容器连接到指定的 Docker 网络

你可以通过执行docker run --``help命令来查看更多可用选项。

docker start

docker start命令用于启动一个或多个已停止的 Docker 容器。例如,要启动一个容器,你可以运行以下命令:

admin@myhome:~$ docker start mycontainer

mycontainer是你想要启动的容器的名称或 ID。你可以使用docker ps命令查看所有正在运行和已停止的容器;稍后我们会详细介绍。你也可以一次启动多个容器。为此,你可以列出它们的名称或 ID,用空格隔开。

要启动多个容器,你可以运行以下命令:

admin@myhome:~$ docker start mycontainer othercontainer lastcontainer

要附加到容器的进程,以便查看其输出,在启动容器时使用-a选项:

admin@myhome:~$ docker start -a mycontainer

docker stop

此命令用于停止后台运行的容器。该命令的语法与启动容器时相同,区别在于你可以使用的可用选项。

要一次停止多个容器,你可以列出它们的名称或 ID,用空格隔开。

admin@myhome:~$ docker stop mycontainer

要停止多个容器,你可以运行以下命令:

admin@myhome:~$ docker stop mycontainer othercontainer lastcontainer

您还可以使用-t选项来指定等待容器停止的时间(以秒为单位),然后发送SIGKILL信号。例如,要在停止容器之前等待10秒,请运行以下命令:

admin@myhome:~$ docker stop -t 10 mycontainer

您也可以使用--time-t来指定在发送SIGKILL信号之前等待容器停止的时间。

默认情况下,docker stop命令会向容器发送SIGTERM信号,这样容器中的进程就有机会进行正常关闭。如果容器在默认的 10 秒超时后仍未停止,将会发送SIGKILL信号强制停止。

docker ps

该命令用于列出正在运行或已停止的容器。当您没有任何选项地运行docker ps命令时,它将显示正在运行的容器列表,并附带它们的容器 ID、名称、镜像、命令、创建时间和状态:

admin@myhome:~$ docker ps

可以使用-a选项查看所有容器:

admin@myhome:~$ docker ps -a
CONTAINER ID           IMAGE                          COMMAND         
STATUS                          PORTS                    NAMES
a1e83e89948e   ubuntu:latest                                "/bin/bash -c 'while…"   29 seconds ago       Up 29 seconds                                            pedantic_mayer
0e17e9729e9c   ubuntu:latest                                "bash"                   About a minute ago   Exited (0) About a minute ago                            angry_stonebraker
aa3665f022a5   ecr.aws.com/pgsql/server:latest   "/opt/pgsql/bin/nonr…"   5 weeks ago          Exited (255) 8 days ago         0.0.0.0:1433->1433/tcp   db-1

您可以使用--quiet-q选项只显示容器 ID,这在脚本编写中可能会很有用:

admin@myhome:~$
docker ps --quiet

您可以使用--filter-f选项根据某些标准过滤输出:

admin@myhome:~$
docker ps --filter "name=ubuntu"

要检查容器使用的磁盘空间,您需要使用-s选项:

admin@myhome:~$ docker ps -s
CONTAINER ID      IMAGE      COMMAND   CREATED          STATUS          PORTS     NAMES            SIZE
a1e83e89948e   ubuntu:latest   "/bin/bash -c 'while…"   14 seconds ago   Up 13 seconds             pedantic_mayer   0B (virtual 77.8MB)

这个容器不会占用额外的磁盘空间,因为它没有保存任何数据。

docker login

docker login命令用于登录 Docker 注册表。注册表是您存储和分发 Docker 镜像的地方。最常用的注册表是 Docker Hub,但您也可以使用其他注册表,如 AWS Elastic Container RegistryECR)、Project Quay 或 Google Container Registry。

默认情况下,docker login会连接到 Docker Hub 注册表。如果您想登录到其他注册表,您可以将服务器 URL 作为参数指定:

admin@myhome:~$ docker login quay.io

当您运行docker login命令时,它会提示您输入用户名和密码。如果您在注册表中没有帐户,可以通过访问注册表的网站来创建一个帐户。

登录后,您将能够从注册表推送和拉取镜像。

您还可以使用--username-u选项来指定您的用户名,使用--password-p来指定密码,但由于安全原因,不推荐在命令行中这样做。

您也可以使用--password-stdin-P选项通过stdin传递密码:

admin@myhome:~$ echo "mypassword" | docker login --username myusername --password-stdin

它可以是任何命令的输出。例如,要登录到 AWS ECR,您可以使用以下命令:

admin@myhome:~$ aws ecr get-login-password | docker login --username AWS --password-stdin 1234567890.dkr.ecr.region.amazonaws.com

您还可以使用--token-t选项来指定您的令牌:

admin@myhome:~$ docker login --token usertokenwithrandomcharacters

登录后,您将能够从注册表推送和拉取镜像。

docker pull

要拉取 Docker 镜像,您可以使用docker pull命令,后跟镜像名称和标签。默认情况下,pull会拉取标签latest(latest 是标签的名称,或镜像的版本)。

例如,使用以下命令从 Docker Hub 拉取alpine镜像的最新版本:

admin@myhome:~$ docker pull alpine

使用以下命令拉取特定版本的alpine镜像,例如版本 3.12:

admin@myhome:~$ docker pull alpine:3.12

你还可以通过在镜像名称中指定注册表 URL 来从不同的注册表拉取镜像。

例如,使用以下命令从私有注册表拉取镜像:

admin@myhome:~$ docker pull myregistry.com/myimage:latest

docker push

在构建新镜像后,你可以将该镜像推送到镜像注册表。默认情况下,push将尝试将其上传到 Docker Hub 注册表:

admin@myhome:~$ docker push myimage

要推送特定版本的镜像,例如版本 1.0,你需要在本地将镜像标记为版本 1.0,然后推送到注册表:

admin@myhome:~$ docker tag myimage:latest myimage:1.0
admin@myhome:~$ docker push myimage:1.0

你还可以通过在镜像名称中指定注册表 URL,将镜像推送到不同的注册表:

admin@myhome:~$ docker push myregistry.com/myimage:latest

在推送镜像之前,你需要使用docker login命令登录到你要推送镜像的注册表。

docker image

这是一个用于管理镜像的命令。docker image的常见用例如下所示:

要列出你机器上可用的镜像,可以使用docker image ls命令:

admin@myhome:~$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE

ubuntu           latest        6b7dfa7e8fdb   7 weeks ago    77.8MB
mcr.microsoft.com/mssql/server     2017-latest               a03c94c3147d   4 months ago   1.33GB
mcr.microsoft.com/azure-functions/python   3.0.15066-python3.9-buildenv   b4f18abb38f7   2 years ago    940MB

你也可以使用docker images命令执行相同的操作:

admin@myhome:~$ docker images
REPOSITORY     TAG           IMAGE ID       CREATED        SIZE
ubuntu        latest        6b7dfa7e8fdb   7 weeks ago    77.8MB
mcr.microsoft.com/mssql/server             2017-latest                    a03c94c3147d   4 months ago   1.33GB
mcr.microsoft.com/azure-functions/python   3.0.15066-python3.9-buildenv   b4f18abb38f7   2 years ago    940MB

要从 Docker 注册表拉取镜像,请使用以下命令:

admin@myhome:~$ docker image pull ubuntu

当你指定镜像名称时,可以使用完整的仓库名称(例如,docker.io/library/alpine)或仅使用镜像名称(例如,alpine),如果镜像位于默认仓库(Docker Hub)中。另请参阅之前部分中讨论的docker pull命令。

也可以构建镜像:

admin@myhome:~$ docker image build -t <image_name> .

另请参阅有关docker build命令的部分,了解更多关于构建镜像的详细信息。

要为镜像创建标签,应运行以下命令:

admin@myhome:~$ docker image tag <image> <new_image_name>

最后,你可以删除一个镜像:

admin@myhome:~$ docker image rm <image>

请参阅有关docker rmi命令的部分,它是此命令的别名。

另一种删除镜像的选项是docker image prune –命令。此命令将删除所有未使用的镜像(悬空镜像)。

docker exec

docker exec允许你在运行中的 Docker 容器中执行命令。基本语法如下:

docker exec CONTAINER COMMAND ARGUMENTS

在前面的示例中,术语的含义如下:

  • CONTAINER是要在其中运行命令的容器的名称或 ID

  • COMMAND是要在容器中运行的命令

  • ARGUMENTS表示命令的任何附加参数(这是可选的)

例如,要在名为my_container的容器中运行ls命令,可以使用以下命令:

admin@myhome:~$ docker exec mycontainer ls

docker logs

docker logs用于获取 Docker 容器生成的日志:

docker logs CONTAINER_NAME_OR_ID

你可以传递给命令的其他选项如下:

  • --details, -a:显示日志提供的额外详细信息

  • --follow, -f:跟踪日志输出

  • --since, -t:仅显示自某个日期以来的日志(例如,2013-01-02T13:23:37)

  • --tail, -t:从日志的末尾显示的行数(默认all

它的使用示例如下:

admin@myhome:~$ docker logs CONTAINER_ID

docker rm

docker rm用于删除一个或多个 Docker 容器:

docker rm CONTAINER_NAME_OR_ID

它的使用示例如下:

admin@myhome:~$ docker rm CONTAINER_ID

要列出所有容器,请使用docker ps -a命令。

docker rmi

从本地拉取或构建的镜像可能会占用大量磁盘空间,因此检查和删除未使用的镜像是很有用的。docker rmi用于删除一个或多个 Docker 镜像。

以下是它的用法:

docker rmi IMAGE

它的使用示例如下:

admin@myhome:~$ docker rmi IMAGE_ID

docker network

docker network命令用于管理 Docker 网络。除了常见的操作(创建、删除和列出),还可以将运行中的 Docker 容器连接(和断开连接)到不同的网络。

也可以通过选择专用的网络插件来扩展 Docker。这里有多个选择,我们只列出一些网络插件及其简短描述。插件也可以用于更高级的设置,如 Kubernetes 集群:

  • Contiv-VPP (contivpp.io/) 使用向量数据包处理VPP)技术,提供一个高效、可扩展和可编程的容器网络解决方案,适用于企业和服务提供商环境,在这些环境中,高性能和可扩展的网络是必需的。

  • Weave Net (www.weave.works/docs/net/latest/overview/) 允许容器之间进行通信,无论它们在哪个主机上运行。Weave Net 创建一个跨多个主机的虚拟网络,使得容器能够以高可用性、冗余和负载均衡的方式进行部署。

  • Calico (www.tigera.io/tigera-products/calico/) 是最知名的插件之一。它采用纯 IP 驱动的网络方式,提供了简洁性和可扩展性。Calico 允许管理员定义和执行网络策略,例如根据源、目的地和端口允许或拒绝特定的流量。Calico 设计用于大规模部署,并支持虚拟和物理网络。

使用docker network命令时的常见操作如下:

  • 创建新网络:
    admin@myhome:~$ docker network create mynetwork
  • 检查网络:
    admin@myhome:~$ docker network inspect mynetwork
  • 删除网络:
    admin@myhome:~$ docker network rm mynetwork
  • 列出网络:
    admin@myhome:~$ docker network ls
  • 连接容器到网络:
    admin@myhome:~$ docker network connect mynetwork        CONTAINER_NAME_OR_ID
  • 从网络中断开容器:
    admin@myhome:~$ docker network disconnect mynetwork         CONTAINER_NAME_OR_ID

docker volume

docker volume命令用于管理 Docker 中的卷。通过这个命令,你可以列出可用的卷,清理未使用的卷,或者创建一个以便稍后使用。

Docker 支持多种卷驱动程序,包括以下几种:

  • local:默认驱动程序;使用 UnionFS 将数据存储在本地文件系统上

  • awslogs:使应用程序生成的日志能够存储在 Amazon CloudWatch Logs 中

  • cifs:允许你将 SMB/CIFS(Windows)共享挂载为 Docker 卷

  • GlusterFS:将 GlusterFS 分布式文件系统挂载为 Docker 卷

  • NFS:将网络文件系统NFS)挂载为 Docker 卷

还有许多其他驱动程序可供选择。可用驱动程序的列表可以在官方 Docker 文档中找到:docs.docker.com/engine/extend/legacy_plugins/

以下是其使用示例:

  • docker volume ls

  • docker volume create <volume-name>

  • docker volume inspect <volume-name>

  • docker volume rm <volume-name>

  • docker volume create myvolume

    docker run -v myvolume:/opt/data alpine

在本节中,我们已经学习了如何使用命令行界面与所有 Docker 组件进行交互。到目前为止,我们一直在使用公开可用的 Docker 镜像,但现在是时候学习如何构建自己的镜像了。

Dockerfile

Dockerfile 本质上是一个具有预定结构的文本文件,包含一组构建 Docker 镜像的指令。Dockerfile 中的指令指定了从哪个基础镜像开始(例如,Ubuntu 20.04),安装哪些软件以及如何配置镜像。Dockerfile 的目的是自动化构建 Docker 镜像的过程,以便镜像能够轻松重现和分发。

Dockerfile 的结构是一个命令列表(每行一个命令),Docker(准确来说是containerd)使用这些命令来构建镜像。每个命令在镜像的 UnionFS 中创建一个新层,最终生成的镜像是所有层的联合体。我们管理的层越少,最终生成的镜像就越小。

Dockerfile 中最常用的命令如下:

  • FROM

  • COPY

  • ADD

  • EXPOSE

  • CMD

  • ENTRYPOINT

  • RUN

  • LABEL

  • ENV

  • ARG

  • VOLUME

  • USER

  • WORKDIR

你可以在官方 Docker 文档网站上找到完整的命令列表:docs.docker.com/engine/reference/builder/

让我们逐一了解上述列表,理解每个命令的作用以及何时使用它。

FROM

Dockerfile 从FROM命令开始,该命令指定要从哪个基础镜像开始:

FROM ubuntu:20.04

你还可以使用as关键字为此构建命名,并跟上自定义名称:

FROM ubuntu:20.04 as builder1

docker build 将尝试从公共 Docker Hub 注册表下载 Docker 镜像,但也可以使用其他注册表,或使用私有注册表。

COPY 和 ADD

COPY命令用于将文件或目录从主机复制到容器文件系统。以下是一个示例:

COPY . /var/www/html

你还可以使用ADD命令将文件或目录添加到 Docker 镜像中。ADD具有比COPY更多的功能。它可以自动解压 TAR 压缩文件,并检查源字段中是否有 URL,如果找到 URL,它将从该 URL 下载文件。最后,ADD命令还具有--chown选项,用于设置目标中文件的所有权。通常情况下,建议在大多数情况下使用COPY,只有在需要ADD提供的附加功能时才使用它。

EXPOSE

Dockerfile 中的 EXPOSE 命令告知 Docker 容器在运行时监听指定的网络端口。它并不会实际发布端口,而是用来向用户提供容器计划发布哪些端口的信息。

例如,如果一个容器在端口 80 上运行 Web 服务器,你应该在 Dockerfile 中包含以下行:

EXPOSE 80

你可以指定端口是监听 TCP 还是 UDP – 在指定端口号后,添加一个斜杠和 TCP 或 UDP 关键字(例如,EXPOSE 80/udp)。如果只指定端口号,则默认使用 TCP。

EXPOSE 命令并不实际发布端口。要使端口可用,你需要在运行 docker run 命令时使用 -p--publish 选项来发布端口:

admin@myhome:~$ docker run -p 8080:80 thedockerimagename:tag

这将把主机上的端口 8080 映射到容器中的端口 80,这样所有到达端口 8080 的流量都会被转发到容器中运行的 Web 服务器的端口 80

不管 EXPOSE 命令如何,你都可以在运行容器时发布不同的端口。EXPOSE 用于告知用户容器计划发布哪些端口。

ENTRYPOINT 和 CMD

接下来是 ENTRYPOINT 命令,它在 Dockerfile 中指定容器启动时应始终执行的命令。它不能通过传递给 docker run 命令的任何命令行选项来覆盖。

ENTRYPOINT 命令用于将容器配置为可执行文件。它类似于 CMD 命令,但用于配置容器作为可执行文件运行。通常用于指定容器启动时应运行的命令,例如命令行工具或脚本。

例如,如果你有一个运行 Web 服务器的容器,你可能会使用 ENTRYPOINT 命令来指定启动 Web 服务器的命令:

ENTRYPOINT ["nginx", "-g", "daemon off;"]

如果你想使用不同的参数运行容器,可以使用 CMD 命令设置默认参数,这些参数可以在容器启动时被覆盖:

ENTRYPOINT ["/usr/bin/python"]
CMD ["main.py","arg1","arg2"]

CMD 用于指定从镜像启动容器时应执行的命令。以下是一个例子:

CMD ["nginx", "-g", "daemon off;"]

这里的经验法则是,如果你希望应用程序接受自定义参数,你可以使用 ENTRYPOINT 启动一个进程,使用 CMD 向其传递参数。这样,你可以通过命令行传递不同的选项,使进程更灵活地执行不同的操作。

RUN

Dockerfile 中的 RUN 命令用于在容器内执行命令。每次执行时,它都会在镜像中创建一个新层。

RUN 命令用于安装软件包、创建目录、设置环境变量,以及执行设置容器内环境所需的任何其他操作。

例如,你可以使用 RUN 命令来安装一个软件包:

RUN apt-get update && apt-get install -y python3 python3-dev

你可以使用 RUN 命令来创建一个目录:

RUN mkdir /var/www

你可以使用 RUN 命令设置环境变量:

RUN echo "export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >> ~/.bashrc

值得注意的是,Dockerfile 中 RUN 命令的顺序很重要,因为每个命令都会在镜像中创建一个新层,最终的镜像是所有层的联合体。因此,如果你期望某些软件包在后续过程中安装,必须在使用它们之前进行安装。

LABEL

LABEL 命令用于向镜像添加元数据。它基本上是将数据的键值对添加到镜像中。这些数据可以用于存储信息,如镜像的版本、维护者以及你在组织中可能需要的其他相关信息。以下是该命令的示例:

LABEL maintainer="Chris Carter <chcarter@your.comain.tld>"

你也可以在一行中添加多个标签:

LABEL maintainer="Chris Carter <chcarter@your.comain.tld>" version="0.2"

使用 docker inspect 命令可以查看添加到镜像上的标签:

admin@myhome:~$ docker inspect --format='{{json .Config.Labels}}' <image_name_or_ID>

使用 LABEL 命令向镜像添加元数据可以帮助用户理解镜像的目的或他们应该向谁询问细节,并有助于管理镜像。

ENV 和 ARG

ENV 命令用于以以下格式设置环境变量:

ENV <key>=<value>

另一方面,ARG 命令用于定义构建时变量。这些变量可以通过 --build-arg 标志传递给 docker build 命令,并且它们的值可以在 Dockerfile 中使用。

ARG 命令用于定义类似于 ENV 格式的构建时变量:

ARG <name>[=<default value>]

ARG 命令创建的变量仅在构建过程中可访问,而 ENV 命令创建的环境变量则可以被容器内运行的所有进程访问。

在下一章节,我们将详细介绍 Docker 镜像的构建过程,其中 ARGENV 被一起使用,以便在构建阶段之间持久化 ENV 变量。

VOLUME

另一个命令是 VOLUME。通过它,你可以配置容器在特定位置为卷创建挂载点。卷是一种将数据存储在容器文件系统外部的方式,这意味着即使容器被删除或重新创建,数据仍然可以保持。以下是该命令:

VOLUME /opt/postgresql_data

使用以下方式指定多个目录:

VOLUME /opt/postgresql_data /opt/postgresql_xferlog

或者,以下方式也有效:

VOLUME ["/opt/postgresql_data", "/opt/postgresql_xferlog"]

如果目录中有标记为 volume 的数据,当使用 docker run 命令运行 Docker 时,将使用该目录的内容创建一个新的卷。这样,即使容器被终止或以其他方式停止,确保在该 Docker 容器运行期间创建的数据不会丢失。这对于数据库尤为重要,正如我们在前面的示例中所建议的那样。

USER

Dockerfile 中的 USER 命令用于设置容器运行时的默认用户。默认情况下,容器以 root 用户身份运行;建议以没有 root 权限的自定义用户身份运行容器。

USER 命令用于设置容器运行时的用户,且可以选择设置用户所属的组。例如,你可以使用 USER 命令以 webserver 用户身份运行容器:

USER webserver

你还可以指定用户和组:

USER webserver:webserver

也可以设置用户 ID 和组 ID,而不是使用名称:

USER 1001:1001

USER 命令仅设置容器的默认用户,但你在运行容器时可以覆盖该设置:

admin@myhome:~$ docker run --user=root webserver-image

出于安全原因,以非 root 用户身份运行应用程序是最佳实践。如果攻击者获得容器访问权限,这样可以限制潜在的损害,因为运行具有完全权限的进程在宿主机上也会以相同的 UID(此处为 root)运行。

WORKDIR

Dockerfile 中的 WORKDIR 命令用于设置容器的当前工作目录。工作目录是容器文件系统中所有后续 RUNCMDENTRYPOINTCOPYADD 命令执行的位置。

你可以使用 WORKDIR 命令将工作目录设置为 /usr/local/app

WORKDIR /usr/local/app

使用 WORKDIR 时,你在使用其他命令时无需设置文件的完整路径,还可以将应用程序位置参数化(使用 ARGENV)。

现在我们已经熟悉了 Dockerfile 以及如何构建 Docker 镜像,接下来了解如何以某种方式存储这个新镜像是很有用的。Docker 镜像注册表正是用于这个目的。我们将在下一节讨论注册表。

Docker 镜像注册表

Docker 镜像注册表用于托管 Docker 镜像。Docker 镜像通过标签组织,用户可以访问并下载这些标签。使用这些镜像可以在宿主机上创建并运行容器。镜像仓库可以托管在本地或远程服务器上,例如 Docker Hub,这是 Docker 提供的公共仓库。你还可以创建自己的私有镜像仓库,在组织内共享和分发镜像。

当你从 Docker 镜像仓库拉取镜像时,镜像由多个层组成。每一层代表 Dockerfile 中用于构建镜像的一条指令。这些层按顺序叠加在一起,创建最终的镜像。每一层是只读的,并具有唯一的 ID。

得益于 UnionFS,Docker 注册表在多个镜像和容器之间共享公共层,从而减少所需的磁盘空间。当容器修改文件时,它会在基础镜像上方创建一个新层,而不是修改基础镜像中的文件。这使得回滚到容器的先前状态变得容易,并且使镜像具有很高的可移植性。

根据你使用的云解决方案(例如 AWS 的 ECR 或 GCP 的 Google Container Registry)或 SaaS 解决方案(Docker Hub 是最受欢迎的 - hub.docker.com),你可以使用多个镜像仓库。也有一些开放源代码的授权解决方案可用:

在本节中,我们已经了解了 Docker 镜像注册表,便于将 Docker 镜像存储在远程位置。在下一节中,我们将深入探讨 Docker 网络及其扩展。

Docker 网络

Docker 网络有四种类型:none、bridge、host 和 overlay。

Bridge是 Docker 中的默认网络模式。处于同一桥接网络中的容器可以相互通信。简而言之,它创建了一个虚拟网络,在该网络中,容器被分配了 IP 地址,并可以通过这些地址进行通信,而网络之外的任何事物都无法访问这些地址。在Host网络中,容器使用主机的网络栈。这意味着容器共享主机的 IP 地址和网络接口。

Overlay模式允许你创建一个跨越多个 Docker 主机的虚拟网络。不同主机上的容器可以相互通信,就像它们在同一主机上一样。它在运行 Docker Swarm 时非常有用。

使用 Docker 命令行,你可以创建任何这些类型的自定义网络。

None 网络

None 网络是 Docker 中的一种特殊网络模式,它禁用容器的所有网络功能。当容器在none网络模式下运行时,它无法访问任何网络资源,也不能与其他容器或主机进行通信。

要在none网络模式下运行容器,你可以在运行docker run命令时使用--network none选项。

例如,要在none网络模式下启动一个运行nginx镜像的容器,你可以运行以下命令:

admin@myhome:~$ docker run --network none -d ubuntu:20

None网络适用于运行不需要任何网络连接的工作负载,例如在连接的卷中处理数据。

桥接模式

在创建容器时使用bridge 模式时,还会创建一个虚拟接口,并将其附加到虚拟网络。每个容器将被分配一个唯一的 IP 地址,允许它与其他容器和主机进行通信。

主机机器充当容器的网关,在容器与外部网络之间路由流量。当一个容器想要与另一个容器或主机机器通信时,它将数据包发送到虚拟网络接口。虚拟网络接口然后将数据包路由到正确的目的地。

默认情况下,它是一个172.17.0.0/16网络,并且与您机器中的桥接设备docker0连接。在这个网络中,容器与主机机器之间的所有流量都是允许的。

如果在执行 docker run 命令时未使用 --network 选项选择网络,则所有容器都将附加到默认的桥接网络。

您可以使用以下命令列出所有可用的网络:

admin@myhome:~$ docker network ls
NETWORK ID     NAME                   DRIVER    SCOPE
6c898bde2c0c   bridge                 bridge    local
926b731b94c9   host                   host      local
b9f266305e10   none                   null      local

要获取更多关于网络的信息,您可以使用以下命令:

admin@myhome:~$  docker inspect bridge
[
    {
        "Name": "bridge",
        "Id": "6c898bde2c0c660cd96c3017286635c943adcb152c415543373469afa0aff13a",
        "Created": "2023-01-26T16:51:30.720499274Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

让我们进入 HOST 模式。

主机模式

主机网络模式 下,容器共享主机的网络栈和网络接口。这意味着容器使用您的机器的 IP 地址和网络设置,并且可以直接访问与其运行的机器相同的网络资源,包括其他容器。

运行在主机网络模式下的容器也可以直接监听主机机器的端口(绑定到它)。

主机网络模式的主要优点之一是提供更好的性能,因为容器不需要经过额外的网络栈。

与其他网络模式相比,这种模式的安全性较低,因为容器可以直接访问主机的网络资源,并能够监听主机接口上的连接。

覆盖网络

覆盖网络是由管理节点创建的,管理节点负责维护网络配置并管理工作节点的成员资格。管理节点创建一个虚拟网络交换机,并为网络中的每个容器分配 IP 地址。

每个工作节点运行 Docker 引擎和容器网络驱动程序,负责将该主机上的容器连接到虚拟网络交换机。容器网络驱动程序还确保数据包被正确封装并路由到正确的目的地。

当一个主机上的容器想要与另一个主机上的容器通信时,它将数据包发送到虚拟网络交换机。交换机然后将数据包路由到正确的主机,容器网络驱动程序在目标主机上解封装数据包并将其传送到目标容器。

覆盖网络使用 虚拟扩展局域网VXLAN)协议来封装 IP 数据包,并使得在多个主机之间创建二层网络成为可能。

总结

在本章中,我们介绍了现代 DevOps 引领的基础设施的主要组成部分之一,那就是容器。我们描述了最突出的容器技术——Docker。我们还介绍了运行 Docker 容器和构建自定义容器的基础知识。在下一章中,我们将基于这些知识,介绍更高级的 Docker 主题。

第九章:深入了解 Docker

Docker 的出现彻底改变了我们运行、部署和维护应用程序的方式。随着容器化的兴起,我们能够抽象出应用程序所依赖的许多底层基础设施和依赖项,使得跨不同环境部署和管理应用程序变得比以往更容易。然而,强大的能力也伴随着巨大的责任,我们必须理解 Docker 的内部机制,并建立良好的实践,以确保我们的应用程序是安全、可靠和高效的。

在本章中,我们将深入探讨 Docker 的细节,探索它的架构、组件和关键特性。我们还将研究一些在 Docker 之上出现的辅助项目,如 Docker Compose 和 Kubernetes,并学习如何使用它们来构建更复杂和可扩展的应用程序。在整个过程中,我们将强调与 Docker 一起工作的最佳实践,如创建高效的 Docker 镜像、管理容器和优化性能。到本章结束时,你将能够自信地在 Docker 中运行应用程序,并利用其全部功能构建强大和可扩展的系统。

本章包含以下主题:

  • Docker 的高级用例

  • Docker Compose

  • 高级 Dockerfile 技术

  • Docker 编排

Docker 的高级用例

在使用 Docker 及其 CLI 时,我们需要处理许多关于容器生命周期、构建过程、数据卷和网络的事项。你可以通过使用其他工具来自动化其中的一些任务,但了解底层的工作原理仍然是很有用的。

运行公共镜像

你可以在 Docker Hub 上找到的许多公共镜像(hub.docker.com)都提供了初始化脚本,这些脚本从环境变量或挂载的文件中获取配置,写入到预定义的目录中。

最常用的镜像是使用了两种技术的数据库镜像。让我们来看看官方的Docker PostgreSQL镜像。我们将使用的镜像可以在这里找到:hub.docker.com/_/postgres

要运行官方的 PostgreSQL Docker 镜像,你可以使用以下命令:

admin@myhome:~$ docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres

在这个命令中,我们有以下内容:

  • --name some-postgres给容器指定了一个名称some-postgres

  • -e POSTGRES_PASSWORD=mysecretpassword为默认的 PostgreSQL 用户(postgres)设置密码

  • -d在后台运行容器;postgres指定使用的镜像

还可以通过添加POSTGRES_USER环境变量来覆盖默认用户(postgres)。其他配置环境变量可以在文档中找到。

当你使用官方 PostgreSQL 镜像时,一个非常有用的功能是通过 SQL 脚本进行数据库预填充。为此,你需要将一个本地目录与脚本绑定挂载到容器内的 /docker-entrypoint-initdb.d。有两件事需要注意:空数据目录和确保所有脚本成功完成。空数据目录是必要的,因为它将充当你可以加载 SQL 或 shell 脚本的入口点;它还可以防止数据丢失。如果任何脚本出现错误,数据库将无法启动。

对于在 Docker Hub 中运行的其他数据库,类似的功能也可以使用。

你可以使用的另一个有用的官方镜像是nginx:它可能更简单,因为你已经有一个配置好的 web 服务器,并且你需要提供内容(HTML 文件、JavaScript 或 CSS)供其提供,或者覆盖默认配置。

下面是将一个静态 HTML 网站挂载到容器的示例:

admin@myhome:~$ docker run -p 8080:80 -v /your/webpage:/usr/share/nginx/html:ro -d nginx

在这个命令中,我们有以下内容:

  • -p 8080:80:这个选项将主机上的 8080 端口映射到容器内的 80 端口。这意味着当有人访问主机上的 8080 端口时,它将被重定向到容器内的 80 端口。

  • -v /your/webpage:/usr/share/nginx/html:ro:这个选项将主机上的 /your/webpage 目录挂载到容器内的 /usr/share/nginx/html 目录。ro 选项意味着挂载是只读的,这意味着容器不能修改 /your/webpage 目录中的文件。

  • -d:这个选项告诉 Docker 以分离模式运行容器,这意味着它将在后台运行。

  • nginx:这是将用于运行容器的 Docker 镜像的名称。在本例中,它是来自 Docker Hub 的官方 nginx 镜像。

我们可以像这样覆盖默认的 nginx 配置:

admin@myhome:~$ docker run -p 8080:80 -v ./config/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx

在这个命令中,大部分之前的选项重复出现,只有一个例外:-v ./config/nginx.conf:/etc/nginx/nginx.conf:ro。这个选项将主机上的 ./config/nginx.conf 文件挂载到容器内的 /etc/nginx/nginx.conf 文件。ro 选项表示挂载是只读的,意味着容器不能修改 nginx.conf 文件。

运行调试容器

在生产环境中运行的容器通常只有很少的工具对故障排除有帮助。更重要的是,这些容器不是以 root 用户身份运行,并且有多个安全机制来防止篡改。考虑到这一点,如果出现问题,我们该如何进入 Docker 网络进行调试呢?

这个问题的答案就是运行另一个我们可以进入的容器。这个容器会预安装一些工具,或者允许我们在运行时安装所需的工具。我们可以使用多种技术来实现这一点。

首先,我们需要一个会无限运行的进程,直到我们手动停止它。这个进程运行时,我们可以进入并使用 docker exec 命令进入正在运行的 Docker 容器中。

了解 Bash 脚本后,执行这个过程最简单的方式是创建一个 while 循环:

admin@myhome:~$ docker run -d ubuntu while true; do sleep 1; done

另一种方法是使用 sleep 程序:

admin@myhome:~$ docker run -d ubuntu sleep infinity

或者,你可以尝试读取一个特殊设备,/dev/null,它什么都不输出,以及使用tail命令:

admin@myhome:~$ docker run -d ubuntu tail -f /dev/null

最后,当这些命令之一在你正在尝试排查故障的网络中运行时,你可以在其中运行一个命令,并有效地能够从你需要调查的环境中运行命令:

admin@myhome:~$ docker exec -it container_name /bin/bash

现在让我们来看看如何清理未使用的容器。

清理未使用的容器

随着时间的推移,Docker 镜像可能会积累,尤其是当你频繁构建和实验容器时。这些镜像中的一些可能不再需要,并且它们可能占用宝贵的磁盘空间。要清理这些未使用的镜像,你可以使用 docker image prune 命令。该命令会删除所有未与容器关联的镜像,也就是所谓的悬挂镜像

除了未使用的镜像外,可能还有一些未正确移除的容器。这些容器可以通过 docker ps -a 命令来识别。要删除特定的容器,你可以使用 docker rm <container_id> 命令,其中 <container_id> 是你想要删除的容器的标识符。如果你想删除所有已停止的容器,你可以使用 docker container prune 命令。

定期执行镜像和容器清理是一个良好的实践,有助于保持健康的 Docker 环境。这不仅可以节省磁盘空间,还可以防止与未使用资源相关的潜在安全漏洞。最好还要从容器和镜像中移除所有敏感信息,如密码和密钥。

这是一个使用 docker image prune 命令来删除悬挂镜像的示例:

admin@myhome:~$ docker image prune
Deleted Images:
deleted:
sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef
deleted:sha256:c937c4dd0c2eaf57972d4f80f55058b3685f87420a9a9fb9ef0dfe3c7c3e60bc
Total reclaimed space: 65.03MB

这是一个使用 docker container prune 命令删除所有已停止容器的示例:

admin@myhome:~$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
8c922b2d9708fcee6959af04c8f29d2d6850b3a3b3f3b966b0c360f6f30ed6c8
6d90b6cbc47dd99b2f78a086e959a1d18b7e0cf4778b823d6b0c6b0f6b64b6c64
Total reclaimed space: 0B

为了自动化这些任务,你可以使用 crontab 来安排定期清理。要编辑你的 crontab 文件,你可以使用 crontab -e 命令。以下是一个例子,展示如何在每天凌晨 3 点安排定期清理悬挂镜像:

0 3 * * * docker image prune -f

这一行由五个字段组成,字段之间由空格分隔。这些字段表示命令执行时的分钟、小时、月日、月份和星期几。在这个例子中,命令将在每天的凌晨 3 点执行。让我们详细看看每个元素:

  • 第一个字段,0,表示分钟。在这种情况下,我们希望命令在整点的 0 分钟执行。

  • 第二个字段,3,表示小时。在这个例子中,我们希望命令在凌晨 3 点执行。

  • 第三个字段,*,表示每月的天数。星号表示“任何”一天。

  • 第四个字段,*,表示月份。星号意味着“任何”一个月份。

  • 第五个字段,*,表示星期几。星号意味着“任何”一天。1表示星期一,2表示星期二,以此类推,直到7表示星期天。

下面是一个示例,演示如何安排在每周日凌晨 4 点清理停止的容器:

0 4 * * 7 docker container prune -f

-f标志用于强制删除镜像或容器,而不需要用户确认。

要列出当前用户的所有 cron 作业,你可以使用crontab -l命令。关于crontab的更多信息可以在线查找或使用man crontab命令。关于如何使用它的一篇优秀教程文章可以在 Ubuntu Linux 知识库中找到:help.ubuntu.com/community/CronHowto

Docker 卷和绑定挂载

如前一章所述,Docker 卷和绑定挂载是两种不同的持久化 Docker 容器生成的数据的方法。卷由 Docker 管理,并存在于容器的文件系统之外。它们可以在容器之间共享和重用,即使原始容器被删除,它们也会持久存在。而绑定挂载则将主机系统上的文件或目录链接到容器中的文件或目录。绑定挂载中的数据可以从主机和容器直接访问,并且只要主机文件或目录存在,它就会持续存在。

要使用 Docker 卷,你可以在运行docker run命令时使用-v--mount标志,并指定主机源和容器目标。例如,要创建一个卷并将其挂载到容器的/app/data,你可以运行以下命令:

admin@myhome:~$ docker run -v my_data:/app/data <image_name>

要使用绑定挂载,你可以使用相同的标志并指定主机源和容器目标,就像使用卷一样。然而,与你使用卷名称不同的是,你需要使用主机文件或目录的路径。例如,要将主机目录/host/data绑定挂载到容器的/app/data,你可以运行以下命令:

admin@myhome:~$ docker run -v /host/data:/app/data <image_name>

在使用 Docker 的绑定挂载时,你可能会遇到与挂载中文件和目录的权限问题。这是因为主机和容器的用户 IDUIDs)和组 IDGIDs)可能不匹配,从而导致无法访问或修改绑定挂载中的数据。

例如,如果主机文件或目录的所有者是 UID 为 1000 的用户,而容器中的 UID 不同,那么容器可能无法访问或修改绑定挂载中的数据。同样,如果组 ID 不匹配,由于组权限,容器也可能无法访问或修改数据。

为了避免这些权限问题,你可以在运行docker run命令时指定主机文件或目录的 UID 和 GID。例如,要使用 UID 和 GID 为 1000 的绑定挂载运行容器,你可以运行以下命令:

admin@myhome:~$ docker run -v /local/data:/app/data:ro,Z --user 1000:1000 <image_name>

在此示例中,:ro 标志指定绑定挂载应为只读,,Z 标志告诉 Docker 使用私有标签标记绑定挂载,以防它与其他容器交互。--user 标志将容器内运行的进程的 UID 和 GID 设置为 1000

通过在容器中指定主机文件或目录的 UID 和 GID,可以避免 Docker 中绑定挂载的权限问题,并确保容器可以按预期访问和修改数据。

Docker 网络高级用法

Docker 提供了一种便捷的方式来管理用户定义网络中容器的网络。通过使用 Docker 网络,您可以轻松控制容器之间的通信,并将它们与主机网络隔离开来。

Docker 桥接网络是一种默认的网络配置,可以使运行在同一主机上的容器之间进行通信。它通过在主机系统上创建一个虚拟网络接口来实现容器与主机网络之间的桥接。桥接网络上的每个容器都被分配一个唯一的 IP 地址,允许它与其他容器和主机进行通信。

桥接网络彼此隔离,这意味着连接到不同桥接网络的容器无法直接通信。要在不同网络的容器之间实现通信,可以使用 Docker 服务发现机制,例如连接到特定容器 IP 地址或使用负载均衡器。

在实际中使用桥接网络,您可以使用 Docker CLI 创建一个新的桥接网络。例如,您可以使用命令 docker network create --driver bridge production-network 来创建一个名为 production-network 的新桥接网络。网络创建后,您可以使用 docker run 命令中的 --network 选项将容器连接到网络。您可以使用命令 docker run --network production-network my-imageproduction-network 网络上运行来自 my-image 镜像的容器。

除了创建新的桥接网络外,还可以将容器连接到安装 Docker 时自动创建的默认 bridge 网络。要将容器连接到默认网络,您无需在 docker run 命令中指定 --network 选项。容器将自动连接到默认 bridge 网络,并从桥接网络子网中分配 IP 地址。

现在,如果创建多个网络,默认情况下它们将被隔离,不允许它们之间进行通信。要允许两个桥接网络之间的通信,例如 production-networkshared-network,您需要通过连接您选择的容器到这两个网络或允许这两个网络之间的所有通信来创建网络连接。如果可能的话,后者选项不受支持。

最后一个选项是使用Docker Swarm模式和覆盖网络模式,我们将在本章的Docker 编排部分稍后讨论。

以下是将容器同时连接到两个网络的示例。首先,让我们创建一个生产网络和一个共享网络:

admin@myhome:~$ docker network create production-network
fd1144b9a9fb8cc2d9f65c913cef343ebd20a6b30c4b3ba94fdb1fb50aa1333c
admin@myhome:~$ docker network create shared-network
a544f0d39196b95d772e591b9071be38bafbfe49c0fdf54282c55e0a6ebe05ad

现在,我们可以启动一个连接到production-network的容器:

admin@myhome:~$ docker run -itd --name prod-container --network production-network alpine sh
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
8921db27df28: Pull complete
Digest:sha256:f271e74b17ced29b915d351685fd4644785c6d1559dd1f2d4189a5e851ef753a
Status: Downloaded newer image for alpine:latest
287be9c1c76bd8aa058aded284124f666d7ee76c73204f9c73136aa0329d6bb8

我们也对shared-network做同样的操作:

admin@myhome:~$ docker run -itd --name shared-container --network shared-network alpine sh
38974225686ebe9c0049147801e5bc777e552541a9f7b2eb2e681d5da9b8060b

让我们检查一下两个容器是否都在运行:

admin@myhome:~$ docker ps
CONTAINER ID    IMAGE    COMMAND   CREATED     STATUS    PORTS   NAMES
38974225686e   alpine    "sh"      4 seconds ago    Up 3 seconds              shared-container
287be9c1c76b   alpine    "sh"      16 seconds ago   Up 14 seconds             prod-container

最后,我们还需要将prod-container连接到共享网络:

admin@myhome:~$ docker network connect shared-network prod-container

然后,我们可以进入prod-container并 ping shared-container

admin@myhome:~$ docker exec -it prod-container /bin/sh
/ # ping shared-container
PING shared-container (172.24.0.2): 56 data bytes
64 bytes from 172.24.0.2: seq=0 ttl=64 time=0.267 ms
64 bytes from 172.24.0.2: seq=1 ttl=64 time=0.171 ms
^C
--- shared-container ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.171/0.219/0.267 ms
/ # ping prod-container
PING prod-container (172.23.0.2): 56 data bytes
64 bytes from 172.23.0.2: seq=0 ttl=64 time=0.167 ms
64 bytes from 172.23.0.2: seq=1 ttl=64 time=0.410 ms
64 bytes from 172.23.0.2: seq=2 ttl=64 time=0.108 ms
^C
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.108/0.188/0.410 ms

你可以在 Docker 官方网页上了解更多关于网络的内容:docs.docker.com/network/

Docker 的安全特性

Docker 的核心并非旨在作为安全工具。这是在后期通过 Linux 内核特性的支持逐步构建的,且仍在开发中,更多的安全特性也在不断加入。

这个话题涉及很多内容,但我们将重点介绍四个最常用的安全特性:

  • 命名空间

  • 安全计算 模式seccomp

  • 无根模式

  • Docker 签名镜像

Linux 内核命名空间

内核命名空间是 Docker 安全的重要组成部分,因为它们为容器和主机系统之间提供隔离。它们允许每个容器查看系统资源,如文件系统、网络和进程表,而不会影响主机或其他容器。这意味着运行在容器内的进程不能访问或修改主机文件系统、网络配置或进程,从而帮助保护主机系统免受恶意或不受控制的容器的威胁。

Docker 使用多个 Linux 内核命名空间为容器提供隔离环境。这些命名空间用于为进程、网络、挂载点等创建隔离的环境。

Docker 守护进程的USER命名空间将确保容器内的 root 用户与主机环境中的 root 用户处于不同的上下文。这是为了确保容器内的 root 用户与主机上的 root 用户不相同。

PID命名空间隔离容器之间的进程 ID。每个容器只能看到自己的进程集,且与其他容器和主机隔离。

NET命名空间的功能是隔离每个容器的网络堆栈,使得每个容器都有自己的虚拟网络堆栈,包括网络设备、IP 地址和路由。

IPC命名空间处理容器之间的进程间通信IPC)资源。每个容器都有自己独立的 IPC 资源,如 System V IPC 对象、信号量和消息队列。

UTS命名空间用于容器的主机名和域名隔离。在这里,每个容器都有自己的主机名和域名,这些不会影响其他容器或主机。

最后,MNT命名空间隔离了每个容器的挂载点。这意味着每个容器都有一个私有的文件系统层级,拥有自己的根文件系统、挂载的文件系统和绑定挂载。

通过使用这些命名空间,Docker 容器彼此之间以及与主机之间都被隔离,这有助于确保容器和主机系统的安全性。

USER命名空间是最难使用的,因为它需要特别的 UID 和 GID 映射配置。默认情况下并未启用它,因为与主机共享PIDNET命名空间(–pid=host–network=host)是不可能的。此外,使用docker run时如果没有指定–userns=host(从而禁用USER命名空间的隔离),将无法使用–privileged mode标志。之前列出的其他命名空间大多无需任何特别配置即可生效。

Seccomp

Seccomp,即安全计算模式,是一个 Linux 内核功能,允许进程指定其允许进行的系统调用。这使得可以限制容器能够执行的系统调用类型,从而通过减少容器逃逸或特权提升的风险来提高主机系统的安全性。

当进程指定其 seccomp 配置文件时,Linux 内核会过滤传入的系统调用,并仅允许在配置文件中指定的那些调用。这意味着,即使攻击者获得了容器的访问权限,他们也会受到能执行的操作类型的限制,从而减少攻击的影响。

要为容器创建 seccomp 配置文件,可以在docker run命令中使用seccomp 配置选项。这允许你在启动容器时指定要使用的 seccomp 配置文件。

创建 seccomp 配置文件有两种主要方式:使用预定义的配置文件或创建自定义配置文件。预定义配置文件适用于常见的使用场景,可以在docker run命令中轻松指定。例如,默认配置文件允许所有系统调用,而限制配置文件仅允许一组被认为对大多数使用场景安全的系统调用。

要创建自定义的 seccomp 配置文件,可以使用Podmanpodman.io/blogs/2019/10/15/generate-seccomp-profiles.xhtml)或seccomp-gengithub.com/blacktop/seccomp-gen)工具。这两个工具自动识别容器在生产环境中使用时会进行的系统调用,并生成一个 JSON 文件,可用作 seccomp 配置文件。

Seccomp 并不能保证安全。了解你的应用所需的系统调用并确保它们在 seccomp 配置文件中被允许,是非常重要的。

以下是一个 seccomp 配置文件示例,它允许运行 Web 服务器应用的容器使用有限的系统调用:

{
    "defaultAction": "SCMP_ACT_ALLOW",
    "syscalls": [
        {
            "name": "accept",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "bind",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "connect",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "listen",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "sendto",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "recvfrom",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "read",
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "name": "write",
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

在这个示例中,defaultAction被设置为SCMP_ACT_ALLOW,意味着所有未在syscalls数组中特别列出的系统调用都会被允许。为了阻止所有未定义的调用,你可以将默认操作设置为SCMP_ACT_ERRNO。所有可用的操作都在seccomp_rule_add过滤器规格的在线手册中有所描述:man7.org/linux/man-pages/man3/seccomp_rule_add.3.xhtml

syscalls数组列出了应该允许容器使用的系统调用,并为每个调用指定了要采取的行动(在这种情况下,所有调用都被允许)。该配置文件仅允许 Web 服务器运行所需的系统调用,阻止所有其他系统调用,从而提高容器的安全性。

有关系统调用的更多信息,请参阅:docs.docker.com/engine/security/seccomp/

无根模式

Docker 无根模式是一个功能,允许用户在不以 root 用户身份运行 Docker 守护进程的情况下运行 Docker 容器。该模式通过减少主机系统的攻击面并最小化特权升级的风险,提供了额外的安全层。

让我们在 Ubuntu Linux 或 Debian Linux 上设置一个无根 Docker 守护进程。首先,确保你已从官方 Docker 软件包仓库安装了 Docker,而不是从 Ubuntu/Debian 软件包中安装:

admin@myhome:~$ sudo apt-get install -y -qq apt-transport-https ca-certificates curl
admin@myhome:~$ sudo mkdir -p /etc/apt/keyrings && sudo chmod -R 0755 /etc/apt/keyrings
admin@myhome:~$ curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" | sudo gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
admin@myhome:~$ sudo chmod a+r /etc/apt/keyrings/docker.gpg
admin@myhome:~$ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" | sudo tee /etc/apt/sources.list.d/docker.list
admin@myhome:~$ sudo apt-get update
admin@myhome:~$ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-scan-plugin docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin

docker-ce-rootless-extras将在你的/usr/bin目录中安装一个名为dockerd-rootless-setuptool.sh的脚本,它将自动化整个过程:

admin@myhome~$ dockerd-rootless-setuptool.sh  --help
Usage: /usr/bin/dockerd-rootless-setuptool.sh [OPTIONS] COMMAND
A setup tool for Rootless Docker (dockerd-rootless.sh).
Documentation: https://docs.docker.com/go/rootless/
Options:
  -f, --force                Ignore rootful Docker (/var/run/docker.sock)
      --skip-iptables        Ignore missing iptables
Commands:
  check        Check prerequisites
  install      Install systemd unit (if systemd is available) and show how to manage the service
  uninstall    Uninstall systemd unit

要运行此脚本,我们需要一个具有配置环境的非 root 用户,以便能够运行 Docker 守护进程。让我们首先创建一个dockeruser用户:

admin@myhome~$ sudo adduser dockeruser
Adding user `dockeruser' ...
Adding new group `dockeruser' (1001) ...
Adding new user `dockeruser' (1001) with group `dockeruser' ...
Creating home directory `/home/dockeruser' ...
Copying files from `/etc/skel' ...
New password:
Retype new password:
passwd: password updated successfully
Changing the user information for dockeruser
Enter the new value, or press ENTER for the default
     Full Name []:
     Room Number []:
     Work Phone []:
     Home Phone []:
     Other []:
Is the information correct? [Y/n] y

在继续之前,我们还需要创建一个 UID 映射配置。为此,我们需要安装uidmap软件包,并创建/etc/subuid/etc/subgid配置文件:

admin@myhome~$ sudo apt install -y uidmap
admin@myhome~$ echo "dockeruser:100000:65536" | sudo tee /etc/subuid
admin@myhome~$ echo "dockeruser:100000:65536" | sudo tee /etc/subgid

dockeruser身份登录并运行dockerd-rootless-setuptool.sh脚本:

admin@myhome~$ sudo -i -u dockeruser

确保设置了environment XDG_RUNTIME_DIR,并且 systemd 可以从dockeruser读取环境变量:

$ export XDG_RUNTIME_DIR=/run/user/$UID
$ echo 'export XDG_RUNTIME_DIR=/run/user/$UID' >> ~/.bashrc
$ systemctl --user show-environment
HOME=/home/dockeruser
LANG=en_US.UTF-8
LOGNAME=dockeruser
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin
SHELL=/bin/bash
SYSTEMD_EXEC_PID=720
USER=dockeruser
XDG_RUNTIME_DIR=/run/user/1001
XDG_DATA_DIRS=/usr/local/share/:/usr/share/:/var/lib/snapd/desktop
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1001/bus

现在,你可以使用dockerd-rootless-setuptool.sh脚本安装无根(rootless)Docker(部分输出已被截断以提高可读性):

$ dockerd-rootless-setuptool.sh install
[INFO] Creating  [condensed for brevity]
     Active: active (running) since Fri 2023-02-17 14:19:04 UTC; 3s ago
+ DOCKER_HOST=unix:///run/user/1001/docker.sock /usr/bin/docker version
Client: Docker Engine - Community
 Version:           23.0.1
[condensed for brevity]
Server: Docker Engine - Community
 Engine:
  Version:          23.0.1
 [condensed for brevity]
 rootlesskit:
  Version:          1.1.0
 [condensed for brevity]
+ systemctl --user enable docker.service
Created symlink /home/dockeruser/.config/systemd/user/default.target.wants/docker.service → /home/dockeruser/.config/systemd/user/docker.service.
[INFO] Installed docker.service successfully.

现在,让我们验证一下能否使用 Docker 无根守护进程:

dockeruser@vagrant:~$ export DOCKER_HOST=unix:///run/user/1001/docker.sock
dockeruser@vagrant:~$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

此时,我们已经有一个以dockeruser系统用户身份运行的 Docker 守护进程,而不是 root 用户。我们将能够像在标准配置中一样运行所有需要的服务。某些情况,如 Docker 中的 Docker 配置,需要进一步的配置。

有关无根模式的更多详细信息,请访问docs.docker.com/engine/security/rootless/

Docker 签名镜像

Docker 签名镜像是一种安全措施,它确保用户知道 Docker 镜像来自受信任的源,并且没有被篡改。Docker 使用数字签名对镜像进行签名,Docker 引擎可以验证签名,以确保镜像与发布者签名时完全一致。

Docker 签名镜像可以通过检查签名者的公钥来验证,公钥可以从受信任的注册中心(如 Docker Hub)获取。如果镜像有效,Docker 会允许你在本地拉取并运行该镜像。

签名 Docker 镜像的第一步是生成一个签名密钥。可以使用 docker trust key generate 命令:

admin@myhome:~/$ docker trust key generate devops
Generating key for devops...
Enter passphrase for new devops key with ID 6b6b768:
Repeat passphrase for new devops key with ID 6b6b768:
Successfully generated and loaded private key. Corresponding public key available: /home/admin/devops.pub

记得使用强密码来保护密钥免受访问。私钥将保存在你的主目录中——例如,~/.docker/trust/private。文件的名称会被哈希化。

一旦你生成了签名密钥,下一步就是初始化镜像的信任元数据。信任元数据包含关于镜像的信息,包括授权签名镜像的密钥列表。要初始化信任元数据,可以使用 docker trust signer add 命令。请注意,你需要登录到所使用的 Docker 注册中心(通过 docker login 命令):

admin@myhome:~/$ docker trust signer add --key devops.pub private-registry.mycompany.tld/registries/pythonapps
Adding signer "devops" to private-registry.mycompany.tld/registries/pythonapps/my-python-app...
Initializing signed repository for private-registry.mycompany.tld/registries/pythonapps/my-python-app...
You are about to create a new root signing key passphrase. This passphrase
will be used to protect the most sensitive key in your signing system. Please
choose a long, complex passphrase and be careful to keep the password and the
key file itself secure and backed up. It is highly recommended that you use a
password manager to generate the passphrase and keep it safe. There will be no
way to recover this key. You can find the key in your config directory.
Enter passphrase for new root key with ID a23d653:
Repeat passphrase for new root key with ID a23d653:
Enter passphrase for new repository key with ID de78215:
Repeat passphrase for new repository key with ID de78215:
Successfully initialized "private-registry.mycompany.tld/registries/pythonapps/my-python-app"
Successfully added signer: devops to private-registry.mycompany.tld/registries/pythonapps/my-python-app

你可以在成功构建 Docker 镜像并用注册中心名称进行标签标记后,使用 docker trust sign 命令对镜像进行签名。此命令会使用信任元数据中的授权密钥对镜像进行签名,并将该信息连同你的 Docker 镜像一起推送到注册中心:

admin@myhome:~/$ docker trust sign private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA
Signing and pushing trust data for local image private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA, may overwrite remote trust data
The push refers to repository [private-registry.mycompany.tld/registries/pythonapps]
c5ff2d88f679: Mounted from library/ubuntu
latest: digest:sha256:41c1003bfccce22a81a49062ddb088ea6478eabea1457430e6235828298593e6 size: 529
Signing and pushing trust metadata
Enter passphrase for devops key with ID 6b6b768:
Successfully signed private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA

要验证你的 Docker 镜像是否已经签名,并且使用了哪个密钥,你可以使用 docker trust inspect 命令:

admin@myhome:~/$ docker trust inspect --pretty private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA
Signatures for private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA
SIGNED TAG    DIGEST                  SIGNERS
latest       41c1003bfccce22a81a49062ddb088ea6478eabea1457430e6235828298593e6   devops
List of signers and their keys for private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA
SIGNER    KEYS
devops    6b6b7688a444
Administrative keys for private-registry.mycompany.tld/registries/pythonapps/my-python-app:2.9BETA
  Repository Key: de782153295086a2f13e432db342c879d8d8d9fdd55a77f685b79075a44a5c37
  Root Key: c6d5d339c75b77121439a97e893bc68a804368a48d4fd167d4d9ba0114a7336b

DOCKER_CONTENT_TRUST 环境变量设置为 1。这样将防止 Docker 下载未签名和未验证的镜像到本地存储。

关于 DCT 的更详细信息可以在官方站点找到:docs.docker.com/engine/security/trust/

Docker 用于 CI/CD 管道集成

持续集成CI)和 持续部署CD)是流行的软件开发实践,旨在确保软件开发过程高效流畅,并保持代码质量。

CI 指的是在共享代码库中自动构建和测试代码更改的实践。CD 是 CI 之后的下一步,其中代码更改会自动部署到生产或暂存环境中。

Docker 是在 CI/CD 管道中广泛使用的工具,它提供了一种高效的方式来打包和分发应用程序。在这一小节中,我们将展示如何使用 GitHub Actions 构建并将 Docker 镜像推送到 AWS Elastic Container RegistryECR)。

让我们来看一个示例,展示如何设置 GitHub Action 来构建并将 Docker 镜像推送到 AWS ECR。

通过在你的仓库的 .github/workflows 目录中创建一个名为 main.yml 的新文件来创建一个新的 GitHub Actions 工作流。添加并推送到主分支后,它将在每次该分支有新的推送时可用并被触发。

main.yml文件中,定义工作流的步骤,如下所示:

name: Build and Push Docker Image
on:
  push:
    branches:
      - main
env:
  AWS_REGION: eu-central-1
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
    - name: Build Docker image
      uses: docker/build-push-action@v2
      with:
        push: true
        tags: ${{ env.AWS_REGION }}/my-image:${{ env.GITHUB_SHA }}
    - name: Push Docker image to AWS ECR
      uses: aws-actions/amazon-ecr-push-action@v1
      with:
        region: ${{ env.AWS_REGION }}
        registry-url: ${{ env.AWS_REGISTRY_URL }}
        tags: ${{ env.AWS_REGION }}/my-image:${{ env.GITHUB_SHA }}

用你的特定值替换 AWS_REGIONAWS_REGISTRY_URL 环境变量。你还应该将 my-image 替换为你 Docker 镜像的名称。

在你的 GitHub 仓库设置中,创建两个密钥,命名为 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,并使用具有推送权限的 AWS 凭证。或者,你可以使用你自己的运行器和附加到运行器的 AWS IAM 角色,或 GitHub OIDC,这将通过 AWS 账户进行身份验证。你可以在这里找到相关文档:docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services

完成这些步骤后,每当你将代码更改推送到主分支时,你的 GitHub Action 将自动构建并推送 Docker 镜像到 AWS ECR。推送之后,你可以触发服务器端的另一个过程,评估并将新 Docker 镜像部署到你的某个环境中,而无需进一步的手动操作。这有助于简化你的 CI/CD 管道,并确保你的代码更改可以放心地部署到生产环境。

也可以以类似的方式将相同的管道集成到 GitLab 或其他 CI/CD 工具中。

在本节中,我们学习了一些容器的非常见使用场景,如无根模式、受保护计算模式、网络高级用例以及如何启动调试容器。在下一节中,我们将进一步集中讨论如何自动化设置 Docker 容器的过程,并探讨如何比手动逐个启动容器更好地进行编排。

Docker Compose

Docker Compose 是一个控制台工具,用于通过一个命令运行多个容器。它提供了一种简便的方式来管理和协调多个容器,使构建、测试和部署复杂应用变得更加容易。使用 Docker Compose,你可以在一个 YAML 文件中定义应用程序的服务、网络和卷,然后通过命令行启动和停止所有服务。

要使用 Docker Compose,首先需要在 docker-compose.yml 文件中定义应用程序的服务。该文件应包括有关你希望运行的服务、它们的配置以及它们如何连接的信息。文件还应指定每个服务使用的 Docker 镜像。

docker-compose.yaml 文件是一个核心配置文件,用于 Docker Compose 管理应用程序的部署和运行。它采用 YAML 语法编写。

docker-compose.yaml 文件的结构分为几个部分,每个部分定义部署的不同方面。第一部分 version 指定了使用的 Docker Compose 文件格式的版本。第二部分 services 定义了组成应用程序的服务,包括它们的镜像名称、环境变量、端口和其他配置选项。

services 部分是 docker-compose.yaml 文件中最重要的部分,因为它定义了应用程序的构建、运行和连接方式。每个服务通过其自己的键值对集合来定义其配置选项。例如,image 键用于指定要用于服务的 Docker 镜像的名称,而 ports 键用于指定服务的端口映射。

docker-compose.yaml 文件还可以包含其他部分,比如 volumesnetworks,这些部分允许您为应用程序定义共享数据存储和网络配置。总体来说,docker-compose.yaml 文件提供了一种集中式、声明式的方式来定义、配置和运行使用 Docker Compose 的多容器应用程序。借助其简单的语法和强大的功能,它是简化复杂应用程序开发和部署的关键工具。

环境变量是键值对,允许您在运行服务时传递配置信息。在 docker-compose.yaml 文件中,可以使用服务定义中的环境键来指定环境变量。

docker-compose.yaml 文件中指定环境变量的一种方法是在环境部分中简单地列出它们作为键值对。以下是一个例子:

version: '3'
services:
  db:
    image: mariadb
    environment:
      MYSQL_ROOT_PASSWORD: example
      MYSQL_DATABASE: example_db

除了直接在 docker-compose.yaml 文件中指定环境变量之外,还可以将它们存储在外部文件中,并在 docker-compose.yaml 文件中使用 env_file 键引用该文件。以下是一个例子:

version: '3'
services:
  db:
    image: mariadb
    env_file:
      - db.env

db.env 文件的内容可能如下所示:

MYSQL_ROOT_PASSWORD=example
MYSQL_DATABASE=example_db

通过使用外部的 env_file 键,您可以将敏感信息与 docker-compose.yaml 文件分开,并在不同的环境中轻松管理环境变量。

举例来说,考虑一个 MariaDB Docker 镜像。MariaDB 镜像需要设置几个环境变量来配置数据库,例如 MYSQL_ROOT_PASSWORD 用于 root 密码,MYSQL_DATABASE 用于默认数据库的名称,以及其他变量。这些环境变量可以在 docker-compose.yaml 文件中定义以配置 MariaDB 服务。

让我们看一个使用 Docker Compose 设置 nginx 容器、PHP-FPM 容器、WordPress 容器和 MySQL 容器的例子。我们将在 docker-compose.yml 文件中定义我们的服务,并通过注释将其分解成较小的块:

version: '3'

前一行定义了 Docker Compose 文件语法的版本。接下来,我们将定义所有要运行并与彼此交互的 Docker 镜像:

services:
  web:
    image: nginx:latest
    depends_on:
      - wordpress
    ports:
      - 80:80
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - wordpress:/var/www/html
    networks:
      - wordpress

这定义了我们应用程序栈中名为web的组件。它将使用来自 Docker Hub 的nginx镜像,标签为latest。以下是一些其他重要设置:

  • depends_on: 这告诉 Docker Compose 在wordpress服务之后启动此组件。

  • ports: 这将您的主机端口转发到 Docker 端口;在这种情况下,它将在计算机上打开端口80并将所有传入流量转发到 Docker 镜像中的相同端口,就像在命令行中启动单个 Docker 容器时所做的-p设置一样。

  • volumes: 这个设置等同于 Docker 命令行工具中的-v选项,因此它将从本地目录将一个nginx.conf文件挂载到 Docker 镜像内的/etc/nginx/conf.d/default.conf文件。

  • wordpress:/var/www/html: 这一行将一个名为wordpress的 Docker 卷挂载到 Docker 镜像内部的目录。卷将在前面定义。

  • networks: 在这里,我们将此服务连接到名为wordpress的 Docker 网络,其定义如下:

  wordpress:
    image: wordpress:php8.2-fpm-alpine
    depends_on:
      - db
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: example_user
      WORDPRESS_DB_NAME: example_database
      WORDPRESS_DB_PASSWORD: example_password
    restart: unless-stopped
    volumes:
      - wordpress:/var/www/html
    networks:
      - wordpress

前面的服务与web服务非常相似,具有以下附加项:

  • environment: 这定义了 Docker 镜像内部存在的环境变量。

  • restart: 这配置了服务,使其在某些原因导致进程停止工作时自动重启。如果我们手动停止了该服务,Docker Compose 将不会尝试重新启动它。

  • depends_on: 该服务器只会在db服务启动后才会启动。

让我们看看db服务:

  db:
    image: mariadb:10.4
    environment:
      MYSQL_ROOT_PASSWORD: example_password
      MYSQL_DATABASE: example_database
      MYSQL_USER: example_user
      MYSQL_PASSWORD: example_password
    restart: unless-stopped
    volumes:
      - dbdata:/var/lib/mysql
    networks:
      - wordpress

此服务正在设置 MariaDB 数据库,以便存储 WordPress 数据。请注意,我们可以为 MariaDB 或 WordPress 镜像使用的所有环境变量都记录在它们各自的 Docker Hub 页面上:

volumes:
  wordpress:
  dbdata:

在这里,我们正在定义用于 WordPress 和 MariaDB 的 Docker 卷。这些是存储在本地的常规 Docker 卷,但通过安装 Docker 引擎插件,可以将它们作为分布式文件系统(如 GlusterFS 或 MooseFS)分发:

networks:
  wordpress:
    name: wordpress
    driver: bridge

最后,我们定义了一个wordpress网络,使用bridge驱动程序,允许所有前述服务之间的通信,并与在其上运行的 Docker 镜像隔离开来。

在前面的示例中,除了本节已经涵盖的选项之外,还有一个服务依赖项(depends_on),这将允许我们强制容器启动的顺序。

我们定义的两个卷(wordpressdbdata)用于数据持久化。wordpress卷用于托管所有 WordPress 文件,并且它也被挂载到运行 nginx Web 服务器的 web 容器上。这样,Web 服务器将能够提供静态文件,如 CSS、图像和 JavaScript,同时将请求转发到 PHP-FPM 服务器。

这是 nginx 配置,它使用 fastcgi 连接到运行 PHP-FPM 守护进程的 WordPress 容器:

server {
    listen 80;
    listen [::]:80;
    index index.php index.htm index.xhtml;
    server_name _;
    error_log  /dev/stderr;
    access_log /dev/stdout;
    root /var/www/html;
    location / {
        try_files $uri $uri/ /index.php;
    }
    location ~ \.php$ {
      include fastcgi_params;
      fastcgi_intercept_errors on;
      fastcgi_pass wordpress:9000;
      fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
      expires max;
      log_not_found off;
    }
}

使用这个 docker-compose.yml 文件,你可以通过 docker-compose updocker-compose down 命令分别启动和停止文件中定义的所有服务。当你运行 docker-compose up 时,Docker 将下载必要的镜像并启动容器,你就可以在 localhost 访问你的 WordPress 网站。

docker-compose 是一个非常有用的工具,用于以简单且可重复的方式运行需要多个服务的应用程序。它通常在本地开发时运行应用程序时使用,但一些组织决定在生产系统中使用 docker-compose,在这种情况下它也能发挥作用。

如果你能使用现成的 Docker 镜像进行本地开发或生产环境,实际上是极为罕见的。使用公共镜像作为自定义的基础是所有使用 Docker 的组织普遍采用的做法。考虑到这一点,在接下来的章节中,我们将学习如何使用多阶段构建构建 Docker 镜像,以及如何正确使用每个 Dockerfile 命令。

高级 Dockerfile 技巧

Dockerfile 用于定义应用程序在 Docker 容器内应如何构建。我们在 第八章 中介绍了大部分可用命令。在这里,我们将介绍一些更高级的技术,例如多阶段构建或不太常见的 ADD 命令用法。

多阶段构建

多阶段构建是 Docker 的一个功能,允许你使用多个 Docker 镜像创建一个最终的单一镜像。通过创建多个阶段,你可以将构建过程分为不同的步骤,从而减少最终镜像的大小。多阶段构建在构建需要多个依赖的复杂应用程序时特别有用,因为它允许开发人员将必要的依赖项放在一个阶段,而将应用程序本身放在另一个阶段。

使用多阶段构建来构建 Golang 应用程序的一个例子涉及创建两个阶段:一个用于构建应用程序,另一个用于运行它。在第一个阶段,Dockerfile 拉取必要的依赖项并编译应用程序代码。在第二个阶段,仅从第一个阶段复制已编译的二进制文件,从而减少最终镜像的大小。以下是一个 Golang 应用程序的 Dockerfile 示例:

# Build stage
FROM golang:alpine AS build
RUN apk add --no-cache git
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o /go/bin/app
# Final stage
FROM alpine
COPY --from=build /go/bin/app /go/bin/app
EXPOSE 8080
ENTRYPOINT ["/go/bin/app"]

在上述示例中,Dockerfile 创建了两个阶段。第一个阶段使用 golang:alpine 镜像并安装必要的依赖项。然后,它编译应用程序并将二进制文件放置在 /go/bin/app 目录下。第二个阶段使用更小的 Alpine 镜像,并将第一个阶段中的二进制文件复制到 /go/bin/app 目录下。最后,它将入口点设置为 /go/bin/app

ADD 命令的用法示例

Dockerfile 中的 ADD 命令用于将文件或目录添加到 Docker 镜像中。它的工作方式与 COPY 相同,但具有一些额外的功能。我们之前已经讨论过基本的用法,但还有其他用例。

第二种用法允许你即时解压 ZIP 或 TAR 和 gzip 工具压缩的文件。在将压缩文件添加到镜像时,文件将被解压,所有文件将被提取到目标文件夹中。以下是一个示例:

ADD my-tar-file.tar.gz /app

使用 ADD 命令的第三种方式是从 URL 将远程文件复制到 Docker 镜像中。例如,要从 URL yourdomain.tld/configurations/nginx.conf 下载名为 file.txt 的文件并将其复制到 Docker 镜像中的 nginx 配置目录 /etc/nginx,可以使用以下 ADD 命令:

ADD https://yourdomain.tld/configurations/nginx.conf /etc/nginx/nginx.conf

你也可以使用 Git 仓库来添加代码:

ADD --keep-git-dir=true https://github.com/your-user-or-organization/some-repo.git#main /app/code

要通过 SSH 克隆 Git 仓库,你需要允许 Docker 内的 ssh 命令访问拥有访问你要克隆仓库的私钥。你可以通过在多阶段构建中添加私钥,并在克隆仓库的阶段结束时将其移除来实现这一点。如果可能的话,一般不推荐这样做。你可以通过使用 Docker secrets 并在构建时挂载该 secret 来更安全地实现。

下面是使用 ARG 配合私钥的示例:

ARG SSH_PRIVATE_KEY
RUN mkdir /root/.ssh/
RUN echo "${SSH_PRIVATE_KEY}" > /root/.ssh/id_rsa
# make sure your domain is accepted
RUN touch /root/.ssh/known_hosts
RUN ssh-keyscan gitlab.com >> /root/.ssh/known_hosts
RUN git clone git@gitlab.com:your-user/your-repo.git

下面是使用 Docker secret 和挂载的示例:

FROM python:3.9-alpine
WORKDIR /app
RUN --mount=type=secret,id=ssh_id_rsa,dst=~/id_rsa chmod 400 ~/id_rsa \
  && ssh-agent bash -c 'ssh-add ~/id_rsa; git clone git@gitlab.com:your-user/your-repo.git' && rm -f ~/id_rsa
  # Rest of the build process follows…

在前面的示例中,我们假设你的私钥没有密码保护,并且你的密钥保存在 ssh_id_rsa 文件中。

使用 ADD 命令的最后一种方式是从主机机器中提取 TAR 压缩包并将其内容复制到 Docker 镜像中。例如,要从主机机器中提取一个名为 data.tar.gz 的 TAR 压缩包并将其内容复制到 Docker 镜像中的 /data 目录,可以使用以下 ADD 命令:

ADD data.tar.gz /data/

Secrets 管理

Docker secrets 管理是构建安全可靠的容器化应用程序的一个重要方面。

Secrets 是应用程序需要运行的敏感信息,但不应暴露给未授权的用户或进程。Secrets 的例子包括密码、API 密钥、SSL 证书以及其他身份验证或授权令牌。这些 secrets 通常在应用程序运行时需要,但如果将其以明文形式存储在代码或配置文件中,则存在安全风险。

保护 secrets 对于确保应用程序的安全性和可靠性至关重要。泄露 secrets 可能导致数据泄露、服务中断和其他安全事件。

在基本的 Docker 设置中,您只能通过环境变量将密钥提供给 Docker 镜像,正如我们在第八章中介绍的那样。Docker 还提供了一个内置的密钥管理机制,可以安全地存储和管理密钥。但是,它仅在启用 Swarm 模式时可用(我们将在本章的Docker 编排部分进一步探讨 Swarm)。

要使密钥可用于运行在 Docker 中的应用程序,您可以使用docker secret create命令。例如,要为 MySQL 数据库密码创建一个密钥,您可以使用以下命令:

admin@myhome:~/$ echo "mysecretpassword" | docker secret create mysql_password -

此命令创建了一个名为mysql_password的密钥,值为mysecretpassword

要在服务中使用密钥,您需要在服务配置文件中定义该密钥。例如,要在服务中使用mysql_password密钥,您可以在docker-compose.yml文件中定义它,如下所示:

version: '3'
services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_password
    secrets:
      - mysql_password
    volumes:
      - db_data:/var/lib/mysql
secrets:
  mysql_password:
    external: true
volumes:
  db_data:

在此配置文件中,mysql_password密钥定义在secrets部分,且MYSQL_ROOT_PASSWORD_FILE环境变量设置为密钥文件的路径,即/run/secrets/mysql_password

要部署服务,您可以使用docker stack deploy命令。例如,要部署在docker-compose.yml文件中定义的服务,您可以使用以下命令:

admin@myhome:~/$ docker stack deploy -c docker-compose.yml myapp

从安全角度来看,小心处理密钥至关重要。最常见的错误是将密钥直接放入 Docker 镜像、环境文件或提交到 Git 仓库的应用程序配置文件中。虽然已有现成的应急措施防止用户这么做(如 GitHub 中的 Dependabot),但如果它们失败,之后从 Git 历史中删除这些密钥将非常困难。

在这一部分中,我们介绍了如何处理构建容器的不同方面以及高级构建技巧。掌握这些知识并使用 Docker Compose,您将能够以相当程度的自动化构建和运行您的应用程序。如果您有 10 个这样的应用程序呢?100 个?甚至更多呢?

在接下来的部分,我们将深入探讨集群,它将进一步自动化操作,并将您的应用程序同时部署到多个主机。

Docker 编排

在容器化的世界中,编排是自动化部署、管理和扩展应用程序到多个主机的过程。编排解决方案通过提供一个抽象层,帮助简化容器化应用程序的管理,提升可用性,并改善可扩展性,使您可以在更高层次上管理容器,而不是手动管理每个容器。

Docker Swarm 是一个原生的 Docker 集群和编排工具,允许你创建并管理 Docker 节点的集群,使用户能够在多个主机上部署和管理 Docker 容器。Docker Swarm 是一个易于使用的解决方案,并且与 Docker 一起内置,成为了那些已经熟悉 Docker 的用户的热门选择。

Kubernetes 是一个开源容器编排平台,最初由 Google 开发。Kubernetes 允许你在多个主机上部署、扩展和管理容器化应用,同时提供了自愈、自动发布和回滚等高级功能。Kubernetes 是目前最流行的编排解决方案之一,并广泛应用于生产环境。

OpenShift 是一个建立在 Kubernetes 基础上的容器应用平台,由 Red Hat 开发。这个平台提供了一个完整的解决方案,用于部署、管理和扩展容器化应用,附加功能包括内建的 CI/CD 流水线、集成监控和自动扩展。OpenShift 设计为企业级平台,具备多租户支持和基于角色的访问控制RBAC)等功能,是需要管理复杂容器化环境的大型组织的热门选择。

市场上有各种各样的编排解决方案,每种方案都有其优缺点。选择使用哪种方案最终取决于你的具体需求,但 Docker Swarm、Kubernetes 和 OpenShift 都是提供强大且可靠的容器化应用编排功能的热门选择。

Docker Swarm

Docker Swarm 是一个为 Docker 容器提供的原生集群和编排解决方案。它提供了一种简单而强大的方式来管理和扩展跨主机的 Docker 化应用。通过 Docker Swarm,用户可以创建并管理一个由 Docker 节点组成的集群,这些节点作为一个虚拟系统共同工作。

Docker Swarm 的基本组件如下:

  • 节点:这些是构成 Swarm 的 Docker 主机。节点可以是运行 Docker 守护进程的物理或虚拟机,并可以根据需要加入或退出 Swarm。

  • 服务:这些是在 Swarm 上运行的应用程序。服务是一个可扩展的工作单元,定义了应用程序应该运行多少个副本,以及如何在 Swarm 上部署和管理它们。

  • 管理节点:这些节点负责管理 Swarm 状态并协调服务的部署。管理节点负责维持服务的期望状态,确保它们按预期运行。

  • 工作节点:这些节点运行实际的容器。工作节点从管理节点接收指令,并运行所需的服务副本。

  • 覆盖网络:这些是允许服务相互通信的网络,无论它们运行在哪个节点上。覆盖网络提供了一个跨越整个 Swarm 的透明网络。

Docker Swarm 提供了一种简单且易于使用的方式来管理容器化应用程序。它与 Docker 生态系统紧密集成,为 Docker 用户提供了熟悉的界面。借助其内置的服务发现、负载均衡、滚动更新和扩展功能,Docker Swarm 是刚开始使用容器编排的组织的热门选择。

要初始化 Docker Swarm 模式并将两个工作节点添加到集群中,你需要初始化 Swarm 模式:

admin@myhome:~/$ docker swarm init
Swarm initialized: current node (i050z7b0tjoew7hxlz419cd8l) is now a manager.
To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-0hu2dmht259tb4skyetrpzl2qhxgeddij3bc1wof3jxh7febmd-6pzkhrh4ak345m8022hauviil 10.0.2.15:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

这将创建一个新的 Swarm,并将当前节点设置为 Swarm 管理器。

一旦 Swarm 初始化完成,你可以将工作节点添加到集群中。为此,你需要在每个工作节点上运行以下命令:

admin@myhome:~/$ docker swarm join --token <token> <manager-ip>

在这里,<token>docker swarm init 命令输出中生成的令牌,你可以在前面的代码块中找到它,而 <manager-ip> 是 Swarm 管理器的 IP 地址。

例如,如果令牌是 SWMTKN-1-0hu2dmht259tb4skyetrpzl2qhxgeddij3bc1wof3jxh7febmd-6pzkhrh4ak345m8022hauviil,而管理节点 IP 地址是 10.0.2.15,命令将如下所示:

admin@myhome:~/$ docker swarm join --token SWMTKN-1-0hu2dmht259tb4skyetrpzl2qhxgeddij3bc1wof3jxh7febmd-6pzkhrh4ak345m8022hauviil 10.0.2.15

在每个工作节点上运行 docker swarm join 命令后,你可以通过在 Swarm 管理节点上运行以下命令来验证它们是否已加入 Swarm:

admin@myhome:~/$ docker node ls
ID                      HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
i050z7b0tjoew7hxlz419cd8l *   myhome    Ready     Active         Leader           23.0.1

这将显示 Swarm 中所有节点的列表,包括管理节点和你已添加的任何工作节点。

在那之后,你可以添加更多节点并开始将应用程序部署到 Docker Swarm。你可以重用任何正在使用的 Docker Compose 文件或 Kubernetes 清单。

要部署一个示例应用程序,我们可以通过部署 wordpress 服务来重用 Docker Compose 模板,但我们需要稍微更新它,通过在环境变量中使用 MySQL 用户和密码文件:

  wordpress:
    image: wordpress:php8.2-fpm-alpine
    depends_on:
      - db
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER_FILE: /run/secrets/mysql_user
      WORDPRESS_DB_NAME: example_database
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_password

以下是向 wordpressdb 服务添加机密的示例:

    secrets:
      - mysql_user
      - mysql_password
  db:
    image: mariadb:10.4
    environment:
      MYSQL_ROOT_PASSWORD: example_password
      MYSQL_DATABASE: example_database
      MYSQL_USER_FILE: /run/secrets/mysql_user
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password

以下是一个在 docker-compose.yml:secrets 文件底部添加机密定义的示例:

  mysql_user:
    external: true
  mysql_password:
    external: true

external: true 设置告诉 docker-compose 机密已经存在,它不应该尝试更新或重新创建这些机密。

在这个版本的 Compose 文件中,我们使用机密来存储 wordpressdb 服务的 MySQL 用户和密码。

要将此文件部署到 Docker Swarm,我们可以使用以下命令:

admin@myhome:~/$ echo "root" | docker secret create mysql_user -
vhjhswo2qg3bug9w7id08y34f
echo "mysqlpwd" | docker secret create mysql_password -
oy9hsbzmzrh0jrgjo6bgsydol

然后,我们可以部署堆栈:

admin@myhome:~/$ docker stack deploy -c docker-compose.yml wordpress
Ignoring unsupported options: restart
Creating network wordpress_wordpress
Creating service wordpress_web
Creating service wordpress_wordpress
Creating service wordpress_db

这里,docker-compose.yaml 是 Compose 文件的名称,my-stack-name 是 Docker 堆栈的名称。

一旦堆栈部署完成,wordpresswebdb 服务将使用在机密中指定的 MySQL 用户和密码运行。你可以通过列出堆栈并检查容器是否正在运行来验证这一点:

admin@myhome:~/$ root@vagrant:~# docker stack ls
NAME        SERVICES
wordpress   3
root@vagrant:~# docker ps
CONTAINER ID     IMAGE     CREATED      STATUS       PORTS      NAMES
7ea803c289b0   mariadb:10.4                  "docker-entrypoint.s…"   28 seconds ago   Up 27 seconds   3306/tcp   wordpress_db.1.dogyh5rf52zzsiq0t95nrhuhc
ed25de3273a2   wordpress:php8.2-fpm-alpine   "docker-entrypoint.s…"   33 seconds ago   Up 31 seconds   9000/tcp   wordpress_wordpress.1.xmmljnd640ff9xs1249jpym45

Docker Swarm 是一个很好的项目,可以让你开始学习 Docker 编排方法。通过使用各种插件扩展其默认功能,它也可以用于生产级系统。

Kubernetes 和 OpenShift

两个最受欢迎的 Docker 容器编排工具是 Kubernetes 和 OpenShift。尽管它们有一些相似之处,但也存在一些显著的区别。以下是 Kubernetes 和 OpenShift 之间的主要区别:

  • 架构:Kubernetes 是一个独立的编排平台,旨在与多个容器运行时(包括 Docker)一起工作。而 OpenShift 是建立在 Kubernetes 之上的平台。它提供了额外的功能和工具,如源代码管理、持续集成和部署。这些附加功能使 OpenShift 成为一个更全面的解决方案,适合需要端到端 DevOps 功能的企业。

  • 易用性:Kubernetes 是一个强大的编排工具,需要较高的技术水平才能设置和操作。而 OpenShift 则旨在更加用户友好,适合具有不同技术背景的开发人员。OpenShift 提供了一个基于 Web 的界面来管理应用,并且可以与各种开发工具集成,方便开发人员使用。

  • 成本:Kubernetes 是一个开源项目,可以免费使用,但企业可能需要投入额外的工具和资源来搭建和操作它。OpenShift 是一个企业级平台,完全访问其功能和支持需要订阅。OpenShift 的成本可能高于 Kubernetes,但它提供了额外的功能和支持,对于需要高级 DevOps 功能的企业来说,可能值得投资。

这两种解决方案都是强大的 Docker 编排工具,提供不同的优势和权衡。Kubernetes 高度可定制,适合更具技术背景的用户。另一方面,OpenShift 提供了更全面的解决方案,具有额外的功能和更易于使用的界面,但成本较高。在选择这两种工具时,你应该考虑你组织的具体需求,同时记住 Docker Swarm 也是一个可选项。云服务提供商也开发了自己的解决方案,其中 Elastic Container Service 就是其中之一。

总结

本章中,我们讨论了关于 Docker 的更高级话题,仅涉及了容器编排相关的内容。Kubernetes、OpenShift 和云服务提供商提供的 SaaS 解决方案正在推动新工具的创造,这些工具将进一步简化 Docker 在现代应用中的使用。

Docker 对软件开发和部署的世界产生了深远影响,使我们能够比以往更加高效和可靠地构建、发布和运行应用程序。通过了解 Docker 的内部机制并遵循最佳实践,我们可以确保我们的应用程序在各种环境中都是安全、高效和可扩展的。

在下一章,我们将探讨在基于 Docker 容器的分布式环境中,如何监控和收集日志的挑战。

第三部分:DevOps 云工具包

本书的最后部分将更多地关注通过配置即代码CaC)和基础设施即代码IaC)实现自动化。我们还将讨论监控和追踪作为现代应用程序开发和维护的关键部分。在最后一章,我们将讨论我们在许多项目中经历过的 DevOps 陷阱。

本部分包含以下章节:

  • 第十章监控、追踪与分布式日志记录

  • 第十一章使用 Ansible 进行配置即代码

  • 第十二章利用基础设施即代码

  • 第十三章使用 Terraform、GitHub 和 Atlantis 进行 CI/CD

  • 第十四章避免 DevOps 中的陷阱

第十章:监控、追踪和分布式日志

现代开发的应用程序往往运行在 Docker 容器中或作为无服务器应用栈。传统上,应用程序是作为一个单体实体构建的——一个进程在服务器上运行。所有的日志都存储在磁盘上,便于快速获取相关信息。若要诊断应用程序的问题,您需要登录到服务器,查找日志或堆栈跟踪以找出问题所在。但当您将应用程序运行在多个容器中的 Kubernetes 集群里,并且这些容器在不同的服务器上执行时,事情就变得复杂了。

这也使得存储日志变得非常困难,更不用说查看它们了。实际上,在容器中运行应用程序时,不建议在其中保存任何文件。我们通常在只读文件系统中运行这些容器。这是可以理解的,因为您应该将正在运行的容器视为一个短暂的身份,随时可能被销毁。

运行无服务器应用程序时,我们也面临类似的情况;在 Amazon Web Services (AWS) Lambda 上,进程在收到请求时开始,处理完请求中的数据后,应用程序会在完成任务后终止。如果您保存了任何数据到磁盘,它将在处理完成后被删除。

最合逻辑的解决方案当然是将所有日志发送到某个外部系统,该系统将保存、编目并使您的日志可以被搜索。有多种解决方案,包括 软件即服务 (SaaS) 和特定于云的应用程序。

顺便提一下,将日志发送到外部系统对于裸金属服务器也是有益的——无论是用于分析、警报,还是诊断服务器无法访问或停止响应的情况。

除了系统和应用程序日志外,我们还可以发送应用程序追踪指标。追踪是比指标更深入的一种形式,它可以提供更多关于系统性能及应用程序在特定情况下表现的洞察。追踪数据的示例包括:某个请求被应用程序处理的时间、耗费的 CPU 周期数,以及应用程序等待数据库响应的时间。

在本章中,您将学习以下内容:

  • 监控、追踪和日志是什么?

  • 如何选择并配置一种云原生的日志解决方案

  • 自托管解决方案及如何选择它们

  • SaaS 解决方案及如何评估哪些对您的组织最有用

此外,我们将涵盖以下主题:

  • 监控、追踪和日志之间的区别

  • 云解决方案

  • 自托管的开源解决方案

  • SaaS 解决方案

  • 日志和指标的保留

那么,让我们直接开始吧!

监控、追踪和日志之间的区别

根据上下文和交流对象的不同,您会听到这些术语交替使用,但它们之间存在微妙且非常重要的差异。

监控是指对你的服务器和应用程序进行仪表化,并收集关于它们的数据以进行处理、识别问题,最终将结果呈现给相关方。这也包括警报功能。

追踪,另一方面,更为具体,正如我们已经提到的。追踪数据可以告诉你很多关于系统性能的信息。通过追踪,你可以观察对开发者非常有用的统计数据(例如一个函数的运行时间,以及 SQL 查询是否快速或存在瓶颈),对 DevOps 工程师有用的数据(例如我们等待数据库或网络的时间),甚至对业务部门有用的数据(例如用户在我们的应用程序中的体验)。所以,你可以看到,当它被正确使用时,它可以成为你手中的一个非常强大的工具。

日志记录的目的是以集中化的方式提供可操作的信息,通常就是将所有消息保存到文件中(称为日志文件)。这些消息通常包括给定操作的成功或失败,并可配置详细程度。日志记录主要由系统管理员或 DevOps 工程师使用,以便更好地了解操作系统或任何给定应用程序中的情况。

澄清了这些后,我们可以进入云中分布式监控解决方案的具体实现,无论是自定义解决方案,还是作为 SaaS 应用程序使用。

云解决方案

每个云服务提供商都完全意识到进行适当监控和分布式日志记录的必要性,因此他们都会开发自己的本地解决方案。有时候,使用本地解决方案是值得的,但并非总是如此。让我们来看一看主要的云服务提供商及其提供的服务。

AWS 最早提供的服务之一就是CloudWatch。最初,它只是收集各种度量数据,并允许你创建仪表板以更好地理解系统性能,轻松发现问题,或者简单地识别是否发生了拒绝服务攻击,从而使你能够快速响应。

CloudWatch 的另一个功能是警报,但它仅限于使用另一个 Amazon 服务——简单邮件服务Simple Email Service)发送电子邮件。警报和指标也可以触发 AWS 账户中的其他操作,比如扩展或缩减运行实例的数量。

截至本书撰写时,CloudWatch 的功能已经远不止监控。该服务的开发者新增了收集和搜索日志的功能(CloudWatch Logs Insights),监控 AWS 资源本身的变化,以及触发操作的能力。我们还能够利用CloudWatch 异常检测在应用程序中检测异常。

至于追踪,AWS 准备了一个名为AWS X-Ray的服务,这是一个先进的分布式追踪系统,几乎可以实时提供有关应用程序在生产环境中如何运行的信息。不幸的是,它的能力仅限于几个语言:Node.js、Java 和.NET。如果你的应用程序是用 Python 编写的,那么就没那么幸运了。

看看其他流行的云解决方案,谷歌提供了Google Cloud PlatformGCP),这是一个智能的日志收集、查询和错误报告解决方案,称为… Cloud Logging。如果在 GCP 中使用此服务,与 CloudWatch Logs 类似,你将能够发送应用程序日志,存储它们,搜索所需数据(IP 地址、查询字符串、调试数据等),并使用类似 SQL 的查询分析日志。

然而,相似之处到此为止,因为谷歌在一些额外功能上走得更远,比如能够创建带有可视化错误报告的日志仪表板,或者创建基于日志的指标。

在 GCP 中,监控是由另一个完全不同的服务来完成的——Google Cloud Monitoring。它专注于收集应用程序数据,创建服务级目标SLOs),从 Kubernetes(Google Kubernetes Engine,或GKE)收集大量指标,并进行第三方集成,例如与知名服务Prometheus的集成。

看看微软的云平台——Azure,你会找到Azure Monitor Service,它由多个部分组成,涵盖了完整应用监控和追踪的需求。显然,Azure Monitor Logs用于收集日志。还有Azure Monitor Metrics,用于监控和可视化你推送到平台的所有指标。你还可以像在 GCP 或 AWS 中一样分析、查询并设置警报。追踪是由Azure Application Insights完成的。微软将其推广为应用性能管理APM)解决方案,并且它是Azure Monitor的一部分。它提供了应用程序的可视化地图、实时指标、代码分析、使用数据和许多其他功能。显然,不同云服务商及其解决方案的实现方式有所不同。你需要参考文档来了解如何对这些服务进行工具化和配置。

我们将重点介绍 AWS 服务。我们将为我们的应用程序创建一个日志组,并从 EC2 实例收集指标。我们还将讨论如何在 Python 中使用 AWS X-Ray 进行追踪,无论底层服务如何,这都可以用于在 AWS 基础设施中运行的应用程序。

CloudWatch 日志和指标

CloudWatch Logs 是 AWS 提供的日志管理服务,使您能够集中、搜索并监控来自多个来源的日志数据。它允许您排查操作问题和安全事件,以及监控资源利用率和性能。

CloudWatch 指标是 AWS 提供的一项监控服务,允许您收集、跟踪并监控 AWS 资源和应用程序的各种指标。

CloudWatch 指标通过收集并显示关键指标(如 CPU 利用率、网络流量、磁盘 I/O 以及与 AWS 资源相关的其他指标,如 EC2 实例、RDS 实例、S3 存储桶和 Lambda 函数)为用户提供详细的 AWS 资源性能视图。

用户可以使用 CloudWatch 指标设置警报,当某些阈值被超越时会通知他们,并且创建自定义仪表板,近实时显示重要指标。CloudWatch 指标还允许用户检索和分析历史数据,这些数据可用于识别趋势并优化资源使用。

为了能够将日志和指标发送到 CloudWatch,我们需要以下内容:

  • 一项 IAM 策略,授予将日志发送到 CloudWatch Logs 的权限。此外,我们还将允许将指标数据与日志一起推送。

  • 创建一个 IAM 角色,并将之前创建的策略附加到其中。此角色然后可以被 EC2 实例、Lambda 函数或任何其他需要将日志发送到 CloudWatch Logs 的 AWS 服务假设。

  • 将角色附加到我们希望将日志发送到 CloudWatch Logs 的资源。为了我们的目的,我们将角色附加到 EC2 实例。

一个 IAM 策略的示例如下:

{
    “Version”: “2012-10-17”,
    “Statement”: [
        {
            “Sid”: “CloudWatchLogsPermissions”,
            “Effect”: “Allow”,
            “Action”: [
                “logs:CreateLogStream”,
                “logs:CreateLogGroup”,
                “logs:PutLogEvents”
            ],
            “Resource”: “arn:aws:logs:*:*:*”
        },
        {
            “Sid”: “CloudWatchMetricsPermissions”,
            “Effect”: “Allow”,
            “Action”: [
                “cloudwatch:PutMetricData”
            ],
            “Resource”: “*”
        }
    ]
}

在此策略中,logs:CreateLogStreamlogs:PutLogEvents 操作对于所有 CloudWatch Logs 资源(arn:aws:logs:*:*:*)都是允许的,cloudwatch:PutMetricData 操作对于所有 CloudWatch 指标资源(*)也是允许的。

我们还需要一项信任策略,允许 EC2 假设我们为其创建的角色,以便能够发送指标和日志。信任策略如下所示:

{
  “Version”: “2012-10-17”,
  “Statement”: [
    {
      “Effect”: “Allow”,
      “Principal”: { “Service”: “ec2.amazonaws.com”},
      “Action”: “sts:AssumeRole”
    }
  ]
}

将其保存为 trust-policy.json 文件,我们稍后将使用它。

使用 AWS CLI 工具,创建实例配置文件并将之前的策略附加到其中,您需要运行以下命令:

admin@myhome:~$ aws iam create-instance-profile --instance-profile-name DefaultInstanceProfile
{
    “InstanceProfile”: {
        “Path”: “/”,
        “InstanceProfileName”: “DefaultInstanceProfile”,
        “InstanceProfileId”: “AIPAZZUIKRXR3HEDBS72R”,
        “Arn”: “arn:aws:iam::673522028003:instance-profile/DefaultInstanceProfile”,
        “CreateDate”: “2023-03-07T10:59:01+00:00”,
        “Roles”: []
    }
}

我们还需要一个附加了信任策略的角色:

admin@myhome:~$ aws iam create-role --role-name DefaultInstanceProfile --assume-role-policy-document file://trust-policy.json
{
    “Role”: {
        “Path”: “/”,
        “RoleName”: “DefaultInstanceProfile”,
        “RoleId”: “AROAZZUIKRXRYB6HO35BL”,
        “Arn”: “arn:aws:iam::673522028003:role/DefaultInstanceProfile”,
        “CreateDate”: “2023-03-07T11:13:54+00:00”,
        “AssumeRolePolicyDocument”: {
            “Version”: “2012-10-17”,
            “Statement”: [
                {
                    “Effect”: “Allow”,
                    “Principal”: {
                        “Service”: “ec2.amazonaws.com”
                    },
                    “Action”: “sts:AssumeRole”
                }
            ]
        }
    }
}

现在,我们可以将刚刚创建的角色附加到实例配置文件上,以便在 EC2 实例中使用它:

admin@myhome:~$ aws iam add-role-to-instance-profile --role-name DefaultInstanceProfile --instance-profile-name DefaultInstanceProfile

现在,让我们附加一个用于 EC2 服务的策略:

admin@myhome:~$ aws iam put-role-policy --policy-name DefaultInstanceProfilePolicy --role-name DefaultInstanceProfile --policy-document file://policy.json

policy.json 文件是您保存策略的文件。

实例配置文件,顾名思义,仅适用于 EC2 实例。要在 Lambda 函数中使用相同的策略,我们需要创建一个 IAM 角色并将新创建的角色附加到函数上。

让我们也使用 AWS CLI 创建一个新实例,并附加我们刚刚创建的实例配置文件。这个特定的实例将被放置在默认 VPC 和公共子网中。这将导致实例获得一个公网 IP 地址,并且可以从公共互联网访问。

要在默认 VPC 的公共子网上创建一个 EC2 实例,并使用DefaultInstanceProfile,你可以按照以下步骤操作:

  1. 获取默认 VPC 的 ID:
    admin@myhome:~$ aws ec2 describe-vpcs --filters     “Name=isDefault,Values=true” --query “Vpcs[0].VpcId” --output text
    vpc-0030a3a495df38a0e

该命令将返回默认 VPC 的 ID。我们将在后续步骤中使用它。

  1. 获取默认 VPC 中一个公共子网的 ID,并保存以备后用:
    admin@myhome:~$ aws ec2 describe-subnets --filters     “Name=vpc-id,Values=vpc-0030a3a495df38a0e”     “Name=map-public-ip-on-launch,Values=true” --query     “Subnets[0].SubnetId” --output text     subnet-0704b611fe8a6a169

要启动 EC2 实例,我们需要一个叫做Amazon 机器镜像AMI)的实例模板和一个我们将用于访问该实例的 SSH 密钥。为了获得 Ubuntu 镜像的 ID,我们也可以使用 AWS CLI 工具。

  1. 我们将通过以下命令筛选出最新的 Ubuntu 20.04 的 AMI ID:
    admin@myhome:~$ aws ec2 describe-images --owners     099720109477 --filters “Name=name,Values=*ubuntu/images/    hvm-ssd/ubuntu-focal-20.04*” “Name=state,Values=available”    “Name=architecture,Values=x86_64” --query “reverse(sort_by(Images,    &CreationDate))[:1].ImageId” --output text
    ami-0a3823a4502bba678

该命令将列出 Canonical(099720109477)拥有的所有可用的 Ubuntu 20.04 镜像,并通过名称(ubuntu-focal-20.04-*)、架构(我们需要x86_64,而非 ARM)以及是否可用(状态为可用)进行过滤。它还将按照创建日期降序排序,并返回最新的(列表中的第一个)镜像 ID。

  1. 现在,要创建一个 SSH 密钥,你需要为自己生成一个密钥,或者使用你已经在机器上拥有的密钥。我们需要将密钥的公钥部分上传到 AWS。你可以简单地运行另一个 CLI 命令来实现这一点:
admin@myhome:~$ aws ec2 import-key-pair --key-name admin-key --public-key-material fileb://home/admin/.ssh/admin-key.pub
{
    “KeyFingerprint”: “12:97:23:0f:d6:2f:2b:28:4d:a0:ad:62:a7:20:e3:f8”,
    “KeyName”: “admin-key”,
    “KeyPairId”: “key-0831b2bc5c2a08d82”
}

完成所有这些步骤后,最后,我们准备好在公共子网中使用DefaultInstanceProfile启动一个新的实例:

admin@myhome:~$ aws ec2 run-instances --image-id ami-0abbe417ed83c0b29 --count 1 --instance-type t2.micro --key-name admin-key --subnet-id subnet-0704b611fe8a6a169 --associate-public-ip-address --iam-instance-profile Name=DefaultInstanceProfile
{
    “Groups”: [],
    “Instances”: [
        {
            “AmiLaunchIndex”: 0,
            “ImageId”: “ami-0abbe417ed83c0b29”,
            “InstanceId”: “i-06f35cbb39f6e5cdb”,
            “InstanceType”: “t2.micro”,
            “KeyName”: “admin-key”,
            “LaunchTime”: “2023-03-08T14:12:00+00:00”,
            “Monitoring”: {
                “State”: “disabled”
            },
            “Placement”: {
                “AvailabilityZone”: “eu-central-1a”,
                “GroupName”: “”,
                “Tenancy”: “default”
            },
            “PrivateDnsName”: “ip-172-31-17-127.eu-central-1.compute.internal”,
            “PrivateIpAddress”: “172.31.17.127”,
            “ProductCodes”: [],
            “PublicDnsName”: “”,
            “State”: {
                “Code”: 0,
                “Name”: “pending”
            },
            “StateTransitionReason”: “”,
            “SubnetId”: “subnet-0704b611fe8a6a169”,
            “VpcId”: “vpc-0030a3a495df38a0e”,
            “Architecture”: “x86_64”,
            “BlockDeviceMappings”: [],
            “ClientToken”: “5e4a0dd0-665b-4878-b852-0a6ff21c09d3”,
            “EbsOptimized”: false,
            “EnaSupport”: true,
            “Hypervisor”: “xen”,
            “IamInstanceProfile”: {
                “Arn”: “arn:aws:iam::673522028003:instance-profile/DefaultInstanceProfile”,
                “Id”: “AIPAZZUIKRXR3HEDBS72R”
            },
# output cut for readability

上述命令的输出是有关新启动实例的信息,你可以将其用于脚本化操作,或者仅仅保存实例的 IP 地址以备后用。

目前,你还无法连接到机器,因为默认情况下,所有端口都被关闭。为了打开 SSH 端口(22),我们需要创建一个新的安全组。

  1. 使用以下命令来实现:
admin@myhome:~$ aws ec2 create-security-group --group-name ssh-access-sg --description “Security group for SSH access” --vpc-id vpc-0030a3a495df38a0e
{
    “GroupId”: “sg-076f8fad4e60192d8”
}

我们使用的 VPC ID 是我们在之前步骤中保存的,而输出是我们新安全组的 ID。我们需要为其添加一个入站规则,并将其连接到我们的 EC2 实例。在机器创建后,查看长输出中的InstanceID值(例如:i-06f35cbb39f6e5cdb)。

  1. 使用以下命令为安全组添加一个入站规则,允许来自0.0.0.0/0的 SSH 访问:
admin@myhome:~$ aws ec2 authorize-security-group-ingress --group-id sg-076f8fad4e60192d8 --protocol tcp --port 22 --cidr 0.0.0.0/0
{
    “Return”: true,
    “SecurityGroupRules”: [
        {
            “SecurityGroupRuleId”: “sgr-0f3b4be7d2b01a7f6”,
            “GroupId”: “sg-076f8fad4e60192d8”,
            “GroupOwnerId”: “673522028003”,
            “IsEgress”: false,
            “IpProtocol”: “tcp”,
            “FromPort”: 22,
            “ToPort”: 22,
            “CidrIpv4”: “0.0.0.0/0”
        }
    ]
}

我们使用了在之前步骤中创建的安全组的 ID。

这个命令向安全组添加了一个新的入站规则,允许来自任何 IP 地址(0.0.0.0/0)的 TCP 流量通过端口22(SSH)。你也可以选择使用自己的公网 IP 地址,而不是允许新 EC2 实例完全访问互联网。

  1. 现在,我们可以将这个安全组附加到一个实例上:
admin@myhome:~$ aws ec2 modify-instance-attribute --instance-id i-06f35cbb39f6e5cdb --groups  sg-076f8fad4e60192d8

目前,端口22应该已经打开,并准备接收连接。

让我们暂时停下来。你可能会想,是否有更好的方法来完成此操作,而不是使用 AWS CLI。是的,确实有;有许多工具可以自动化创建基础设施。这些工具通常被称为基础设施即代码IaC),我们将在第十二章中讨论它们。在这个示例中,我们本可以使用多种选择,从 AWS 的首选 IaC 工具 CloudFormation,到 HashiCorp 的 Terraform,再到逐渐受到关注的 Pulumi 项目。

现在我们有了一个 EC2 实例,我们可以连接到它并安装CloudWatch 代理。之所以需要它,是因为 AWS 默认只监控两个指标:CPU 和内存使用率。如果您想要监控磁盘空间并将附加数据(如日志或自定义指标)发送到 CloudWatch,则必须使用该代理。

  1. 进入 SSH 控制台后,我们需要下载 CloudWatch 代理的deb包并使用dpkg工具安装:
admin@myhome:~$ ssh -i ~/.ssh/admin-key ubuntu@3.121.74.46
ubuntu@ip-172-31-17-127:~$ wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
ubuntu@ip-172-31-17-127:~$ sudo dpkg -i -E ./amazon-cloudwatch-agent.deb

让我们切换到root用户,以便可以在每个命令中省略sudo

ubuntu@ip-172-31-17-127:~$ sudo -i
root@ip-172-31-17-127:~# /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard
================================================================
= Welcome to the Amazon CloudWatch Agent Configuration Manager =
=                                                              =
= CloudWatch Agent allows you to collect metrics and logs from =
= your host and send them to CloudWatch. Additional CloudWatch =
= charges may apply.                                           =
================================================================
On which OS are you planning to use the agent?
1\. linux
2\. windows
3\. darwin
default choice: [1]:

它会询问很多问题,但大多数问题可以保持默认设置,直接按 Enter 即可。然而,有一些问题需要我们特别注意:

Do you want to monitor metrics from CollectD? WARNING: CollectD must be installed or the Agent will fail to start
1\. yes
2\. no
default choice: [1]:
  1. 如果您回答了yes1)这个问题,您将需要通过以下命令安装 collectd:
root@ip-172-31-17-127:~# apt install -y collectd
  1. 对于以下问题,除非您希望上传某些特定的日志文件到 CloudWatch Logs,否则请回答no2):
Do you want to monitor any log files?
1\. yes
2\. no
default choice: [1]:
2
  1. 最后的问题是是否将代理配置保存在 AWS SSM 中,您可以安全地回答no2):
Do you want to store the config in the SSM parameter store?
1\. yes
2\. no
default choice: [1]:
2
Program exits now.

向导会将配置保存在/opt/aws/amazon-cloudwatch-agent/bin/config.json中。如果需要,您可以稍后修改它或再次启动向导。

  1. 在启动代理之前,我们需要将输出的 JSON 文件转换为新的Tom 的显而易见、最小化语言TOML)格式,这是代理使用的格式。幸运的是,执行此任务的命令也有。我们将使用代理控制脚本加载现有的架构,保存 TOML 文件,并在一切就绪时可选择启动代理:
root@ip-172-31-17-127:~# /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json
root@ip-172-31-17-127:~# systemctl status amazon-cloudwatch-agent
amazon-cloudwatch-agent.service - Amazon CloudWatch Agent
     Loaded: loaded (/etc/systemd/system/amazon-cloudwatch-agent.service; enabled; vendor preset: enabled)
     Active: active (running) since Wed 2023-03-08 15:00:30 UTC; 4min 54s ago
   Main PID: 20130 (amazon-cloudwat)
      Tasks: 6 (limit: 1141)
     Memory: 14.3M
     CGroup: /system.slice/amazon-cloudwatch-agent.service
             └─20130 /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent -config /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml -envconfig /opt/aws/amazon-cloudwatch-agent/e>
Mar 08 15:00:30 ip-172-31-17-127 systemd[1]: Started Amazon CloudWatch Agent.
Mar 08 15:00:30 ip-172-31-17-127 start-amazon-cloudwatch-agent[20130]: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json does not exist or cannot read. Skipping it.
Mar 08 15:00:30 ip-172-31-17-127 start-amazon-cloudwatch-agent[20130]: I! Detecting run_as_user...

现在,我们可以前往 AWS Web 控制台并导航到 CloudWatch,查看是否能看到传入的指标。可能需要几分钟才能显示。

在启动 CloudWatch 代理之前,我们将为 EC2 实例获取大约 17 个不同的指标,如下图所示:

图 10.1 – 在未安装 CloudWatch 代理的情况下,CloudWatch 中的基本 EC2 和 EBS 指标

图 10.1 – 在未安装 CloudWatch 代理的情况下,CloudWatch 中的基本 EC2 和 EBS 指标

启动 CloudWatch 代理后,我们将开始接收更多的指标,并且在 CloudWatch Metrics 面板中会看到一个额外的命名空间。请参见以下屏幕截图:

图 10.2 – 成功启用 CloudWatch 代理后 EC2 实例中的 CloudWatch 指标

图 10.2 – 在 EC2 实例上成功启用 CloudWatch 代理后的 CloudWatch 指标

我们接收到的所有指标都可以用于在 CloudWatch 服务中创建仪表板和警报(包括异常检测)。

AWS X-Ray

AWS X-Ray 是一项服务,允许您跟踪分布式系统和微服务应用程序中的请求。它提供了请求在应用程序中流动的端到端视图,使开发人员能够识别性能瓶颈、诊断错误并提高整体应用程序效率。

使用 X-Ray,可以可视化应用程序的不同组件,并查看请求如何在各个组件间流动时被处理。这包括诸如完成每个组件所需时间、发生的任何错误以及错误的原因等细节。

X-Ray 还提供了一系列分析工具,包括统计分析和热图,帮助开发人员识别请求处理中的趋势和模式。这些见解可以用于优化性能,确保应用程序尽可能高效地运行。

AWS X-Ray 支持多种编程语言,包括以下几种:

  • Node.js

  • Java

  • .NET

  • Go

  • Python

  • Ruby

  • PHP

要使用 AWS X-Ray 提供的诊断工具来为应用程序进行监控,您可以使用 AWS SDK。考虑以下代码(可以在 GitHub 仓库中找到:github.com/Sysnove/flask-hello-world):

from flask import Flask
app = Flask(__name__)
@app.route(‘/’)
def hello_world():
    return ‘Hello World!’
if __name__ == ‘__main__’:
    app.run()

要收集此服务的追踪数据,您需要使用 pip 包管理器安装 aws_xray_sdk 包。然后,将 xray_recorder 子包导入到我们的代码中。在这种情况下,我们还将使用此 SDK 与 Flask 框架的集成。修改后的代码如下所示:

from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware
xray_recorder.configure(service=’FlaskHelloWorldApp’)
app = Flask(__name__)
XRayMiddleware(app, xray_recorder)

其余代码可以保持不变。在这里,我们将配置 X-Ray 记录器使用服务名称 FlaskHelloWorldApp,该名称将在 X-Ray 控制台中显示为我们的服务名称。当服务开始运行时,您可以进入 X-Ray 控制台,查看服务名称 FlaskHelloWorldApp 以及相关的追踪列表。

AWS X-Ray SDK 的完整文档可以在以下网站找到:docs.aws.amazon.com/xray-sdk-for-python/latest/reference/index.xhtml

当在我们之前创建的 EC2 实例上运行上述应用程序时,您将看到应用程序运行环境的完整图像,包括 Flask 进程的内部情况。

有多个项目涉及应用程序监控、追踪和日志收集。除了云环境中可用的云托管解决方案外,还有一些值得了解的商业和开源解决方案。这些认知在处理越来越常见的混合解决方案时可能会非常有用。

自托管的开源解决方案

最受欢迎的监控相关项目之一,也是商业解决方案采用的项目是OpenTelemetry。它是一个开源的应用程序监控与可观察性项目,提供了一整套 API、库、代理和集成,用于收集、处理和导出来自分布式系统中不同来源的遥测数据,如跟踪、指标和日志。OpenTelemetry 设计上是供应商无关的且云原生的,这意味着它可以与各种云服务提供商、编程语言、框架和架构兼容。

OpenTelemetry 的主要目标是为开发者和运维人员提供一种统一和标准化的方式,在其应用程序和服务的整个堆栈中对遥测数据进行注入、收集和分析,而不管底层基础设施是什么。OpenTelemetry 支持不同的数据格式、协议和导出目标,包括流行的可观察性平台,如PrometheusJaegerZipkinGrafanaSigNoz。这使得用户可以根据需要灵活组合他们喜欢的工具和服务,构建一个全面的可观察性管道。

一些采用 OpenTelemetry 的商业软件示例包括Datadog、AWS 和New Relic。AWS 提供 OpenTelemetry Collector 作为托管服务,用于收集和导出遥测数据到 AWS 服务,如 Amazon CloudWatch、AWS X-Ray 和 AWS App Runner。

Prometheus

Prometheus 是一个开源的监控解决方案,广泛用于收集和查询分布式系统中的指标。它由 SoundCloud 的开发人员创建,现在由云原生计算基金会CNCF)维护。Prometheus 设计上具有高度的可扩展性和适应性,支持多种数据源和集成选项。它允许用户定义和收集自定义指标,通过内置仪表板可视化数据,并基于预定义的阈值或异常设置警报。Prometheus 常与 Kubernetes 及其他云原生技术一起使用,但也可以用于监控传统的基础设施和应用程序。

一个常见的使用场景是跟踪请求延迟和错误率,这有助于识别应用程序中的性能瓶颈和潜在问题。要开始使用 Prometheus 监控 Flask 应用程序,您可以使用 Python 的 Prometheus 客户端库。该库提供了可以添加到 Flask 路由中的装饰器,自动生成请求计数、请求持续时间和 HTTP 响应码等指标。然后,这些指标可以由 Prometheus 服务器收集,并显示在 Grafana 仪表板上进行可视化和分析。

这是一个示例,展示如何将“Hello World” Flask 应用程序与 Prometheus 配合使用以发送指标。我们在前一节中使用了相同的应用程序和 AWS X-Ray。

首先,你需要使用 pip 安装 prometheus_client 库:

$ pip install prometheus_client

接下来,你可以修改 flask-hello-world 仓库中的 app.py 文件,添加 Prometheus 客户端库,并用指标对路由进行仪器化。以下是一个示例:

from flask import Flask
from prometheus_client import Counter, Histogram, start_http_server
app = Flask(__name__)
# Define Prometheus metrics
REQUEST_COUNT = Counter(‘hello_world_request_count’, ‘Hello World Request Count’)
REQUEST_LATENCY = Histogram(‘hello_world_request_latency_seconds’, ‘Hello World Request Latency’,
                            bins=[0.1, 0.2, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0])
# Instrument Flask routes with Prometheus metrics
@app.route(‘/’)
@REQUEST_LATENCY.time()
def hello():
    REQUEST_COUNT.inc()
    return “Hello World!”
# Start the Prometheus server on port 8000
if __name__ == ‘__main__’:
    start_http_server(8000)
    app.run(debug=True)

在这个示例中,我们定义了两个 Prometheus 指标:hello_world_request_counthello_world_request_latency_secondshello() 路由使用装饰器对这些指标进行了仪器化。REQUEST_LATENCY 直方图度量每个请求的延迟,而 REQUEST_COUNT 计数器在每个请求时递增。

我们已经通过 start_http_server() 在端口 8000 启动了 Prometheus 服务器。这样可以使这些指标可供 Prometheus 服务器收集。

要查看这些指标,你可以在浏览器中访问 localhost:8000/metrics。这将以 Prometheus 格式显示原始的指标数据。你也可以使用像 Grafana 这样的工具在仪表盘上可视化这些指标。

Grafana

Grafana 是一个流行的开源仪表盘和数据可视化平台,允许用户创建交互式和可定制的仪表盘,用于监控和分析来自各种数据源的指标。它通常与 Prometheus 一起使用。

使用 Grafana,用户可以创建可视化图表、警报规则和仪表盘,以便深入了解他们应用程序和基础设施的性能和行为。Grafana 支持多种数据源,包括流行的时序数据库,如 Prometheus、InfluxDB 和 Graphite,使其成为一个多功能的监控和可视化工具。一旦你连接了数据源,你就可以通过添加面板来开始创建仪表盘,以便可视化数据。这些面板可以包含各种类型的可视化图表,包括折线图、条形图和仪表盘。你还可以自定义仪表盘布局、添加注释,并设置警报以在指标出现异常或问题时通知你。凭借其强大的功能和灵活性,Grafana 成为可视化和分析应用程序及基础设施指标的首选工具。

Grafana Labs 还创建了 Grafana Loki 项目,可以用来扩展你的监控,提供日志可视化。Grafana Loki 是一个水平可扩展的日志聚合系统,提供了一种集中化来自不同源的日志并快速搜索和分析它们的方法。它被视为 Prometheus 的替代品,但这两种工具有不同的使用场景,并且可以相互补充。

与传统的日志管理解决方案不同,Loki 不会预先对日志进行索引或解析。相反,它使用一个流式管道,提取日志标签并以紧凑高效的格式存储,这使得它非常适合实时摄取和查询大量日志。Grafana Loki 与 Grafana 无缝集成,允许用户将日志与指标关联,并创建强大的仪表盘,深入了解他们的应用程序和基础设施的行为。

要使用 Grafana Loki,你需要设置一个 Loki 服务器并配置它接收来自你的应用程序和基础设施的日志数据。一旦 Loki 设置完成,你可以使用 Grafana 的 Explore 功能实时搜索和可视化日志。Explore 提供了一个用户友好的界面,使你能够使用各种过滤器(如标签、时间范围和查询表达式)来搜索日志。

SigNoz

SigNoz 是一个可观察性平台,使用户能够收集、存储和分析应用程序的指标遥测数据,并提供统一的日志管理界面。它基于 OpenTelemetry 规范构建,这是一个分布式追踪和指标收集的行业标准框架。SigNoz 提供了一个简单、直观的界面,供用户查看其应用程序性能和健康状况的实时和历史数据。

SigNoz 有自己的代理程序,可以安装在你的服务器上,但它也支持 Prometheus 作为数据源。因此,如果你已经在使用 Prometheus,你可以在不对监控基础设施进行重大更改的情况下使用 SigNoz。

要在服务器上安装 SigNoz,你可以参考官方项目网站上的详细安装指南:signoz.io/docs/install/

New Relic Pixie

New Relic 是一个知名的监控 SaaS 解决方案;我们将在本章的SaaS 解决方案部分稍后详细介绍它。Pixie 是一个由 New Relic 启动的开源项目,并且已贡献给 CNCF。

CNCF 是一个开源软件基金会,成立于 2015 年,旨在推动云原生技术的发展和采用。CNCF 是许多流行项目的家园,例如 Kubernetes、Prometheus 和 Envoy,这些项目在现代云原生应用中得到了广泛应用。该基金会的目标是创建一个云原生计算的供应商中立生态系统,促进不同云平台和技术之间的互操作性和标准化。CNCF 还主持多个认证项目,帮助开发者和组织验证他们在云原生技术方面的能力。CNCF 在推动云原生领域快速发展的创新和标准化方面起着至关重要的作用。

New Relic Pixie 是一个开源的、Kubernetes 原生的可观测性解决方案,提供现代应用程序的实时监控和追踪功能。它可以帮助开发人员和运维团队快速识别和排查在 Kubernetes 集群上运行的微服务应用程序中的性能问题。Pixie 可以轻松部署在任何 Kubernetes 集群上,并提供对流行的开源工具(如 Prometheus、Jaeger 和 OpenTelemetry)的开箱即用支持。

使用 New Relic Pixie 的一个主要好处是,它提供了从应用代码到底层 Kubernetes 资源的端到端性能可见性。通过收集和分析来自不同来源的数据,包括日志、度量和追踪,Pixie 可以帮助准确找出性能瓶颈和问题的根本原因。这可以显著减少 平均修复时间 (MTTR),并提高应用程序的可靠性和正常运行时间。

New Relic Pixie 的另一个优势是,它采用了一种独特的监控方式,无需任何代码更改或配置。Pixie 使用 扩展伯克利数据包过滤器 (eBPF) 技术在内核级别收集性能数据,从而实现低开销监控,且不会给应用程序或基础设施

Graylog

Graylog 是一个开源的日志管理平台,允许用户从各种来源收集、索引和分析日志数据。该平台提供了一个集中位置,用于监控和排查应用程序、系统和网络基础设施的问题。它构建在 Elasticsearch、MongoDB 和 Apache Kafka 之上,确保了高可扩展性和可用性。

Graylog 具有横向扩展能力,这意味着您可以添加额外的 Graylog 节点来处理增加的日志数据量和查询负载。该系统还可以将工作负载分配到多个节点,从而实现资源的高效利用和数据的快速处理。这种可扩展性使 Graylog 适用于任何规模的组织,从小型初创公司到大型企业。

Graylog 使用 Elasticsearch 作为索引和搜索日志数据的主要数据存储。Elasticsearch 是一个强大的搜索和分析引擎,可以快速高效地查询大型数据集。Graylog 中的 MongoDB 用于存储有关日志数据的元数据,并管理系统的配置和状态。

Graylog 还提供了一个基于 Web 的用户界面,允许用户搜索和可视化日志数据,以及管理系统配置和设置:

图 10.3 – Graylog 日志系统架构

图 10.3 – Graylog 日志系统架构

该解决方案的架构非常简单,正如你在前面的图示中所看到的。

Sentry

Sentry 是一个开源的错误追踪工具,帮助开发者监控和修复他们应用中的错误。它允许开发者实时追踪错误和异常,使他们能够在问题变得严重之前迅速诊断和修复问题。Sentry 支持多种编程语言,包括 Python、Java、JavaScript 和 Ruby 等。

使用 Sentry 的一个关键优势是其易于设置和集成。Sentry 可以轻松与流行的框架和平台集成,如 Django、Flask 和 Rails 等。它还提供了一系列插件和与第三方工具的集成,如 Slack 和 GitHub,以帮助开发者简化工作流程并更有效地协作。

Sentry 为开发者提供详细的错误报告,其中包含关于错误的信息,如堆栈跟踪、环境变量和请求参数。这使得开发者能够快速识别错误的根本原因并采取纠正措施。Sentry 还在错误发生时提供实时通知,帮助开发者及时响应。

使用 Sentry 的另一个好处是它能够分析错误随时间的变化。Sentry 允许开发者追踪错误率并识别错误发生的模式,这使得发现和解决应用中的系统性问题变得更容易。这些数据还可以用于改善应用的整体性能和可靠性。

Sentry 提供与 Jira 的集成,Jira 是一个流行的工单和问题追踪系统。该集成使得开发者可以直接从 Sentry 内创建 Jira 问题,从而更方便地管理和追踪通过 Sentry 发现的问题。

要设置集成,首先需要创建 Jira API 令牌并在 Sentry 中配置集成设置。集成设置完成后,你可以通过点击 创建 JIRA 问题 按钮,在 错误详情 页面上直接从 Sentry 创建 Jira 问题。这将自动填充有关错误的相关信息,如错误消息、堆栈跟踪和请求参数。你可以在官方文档页面上找到详细的操作说明:docs.sentry.io/product/integrations/issue-tracking/jira/

Sentry 提供与其他多个流行的工单和问题追踪系统的集成,如 GitHub、Trello、Asana、Clubhouse 和 PagerDuty,允许你直接从 Sentry 触发 PagerDuty 事件。

在本节中,我们向您展示了几种领先的解决方案,这些解决方案既是开源的,又适合自托管。然而,如果您希望降低部署和维护的复杂性,自托管可能并不是您需要的。下一节将介绍由第三方公司为您托管的监控和日志软件。

SaaS 解决方案

SaaS 监控解决方案是最容易(也是最昂贵)使用的。在大多数情况下,您需要做的就是在服务器或集群内安装并配置一个小型守护进程(代理)。这样,所有的监控数据就会在几分钟内显示出来。如果您的团队没有能力实施其他解决方案,但预算允许您使用 SaaS,那它就是一个不错的选择。以下是一些更受欢迎的应用程序,用于处理您的监控、追踪和日志需求。

Datadog

Datadog 是一个监控和分析平台,提供对应用程序、基础设施和网络性能与健康状况的可视化。它由 Olivier Pomel 和 Alexis Lê-Quôc 于 2010 年创立,总部位于纽约市,并在全球设有办公室。根据 Datadog 2021 财年(截至 2021 年 12 月 31 日)的财务报告,公司的总收入为 20.65 亿美元,同比增长了 60%(相比 2020 财年)。

Datadog 的平台与超过 450 种技术集成,包括云服务提供商、数据库和容器,使用户能够收集和关联来自整个技术栈的数据。它提供实时监控、警报和协作工具,使团队能够排查问题、优化性能并改善用户体验。

Datadog 允许用户监控其服务器、容器和云服务的健康状况和性能,提供关于 CPU 使用率、内存利用率、网络流量等方面的见解。

Datadog 的 APM 工具提供关于 Web 应用程序、微服务和其他分布式系统性能的详细见解,使用户能够识别并诊断瓶颈和问题。

Datadog 的日志管理工具使用户能够收集、处理和分析来自整个基础设施的日志,帮助排查问题并识别趋势。

最后,Datadog 的安全监控通过分析网络流量、识别异常并与安全解决方案集成,帮助检测和响应威胁。

Datadog 中的仪表板功能使用户能够在集中位置可视化和分析来自应用、基础设施和网络的数据。用户可以通过点击创建仪表板按钮并选择他们希望创建的仪表板类型(例如,基础设施APM日志自定义)来创建 Datadog 中的仪表板。然后,他们可以向仪表板添加小部件并配置其设置。有多个自动化仪表板可用;例如,如果您开始从 Kubernetes 集群发送数据,Datadog 将显示一个相关的仪表板。您可以在 Datadog 文档网站上找到更多关于如何使用仪表板的详细信息:docs.datadoghq.com/getting_started/dashboards/

小部件是 Datadog 仪表板的构建模块。它们可以显示指标、日志、追踪、事件或自定义数据。要添加小部件,用户可以点击+按钮并选择他们希望添加的小部件类型。然后,他们可以配置小部件的设置,如选择数据源、应用过滤器和设置时间范围。例如,您可以在 Datadog 网页上查看 nginx Web 服务器的示例仪表板:www.datadoghq.com/dashboards/nginx-dashboard/

除了在仪表板上显示数据外,Datadog 还提供了各种工具来探索和分析数据,例如查询构建器、Live Tail 和追踪。用户可以使用这些工具深入挖掘数据并排查问题。

New Relic

New Relic是一家基于云的软件分析公司,提供有关 Web 和移动应用性能的实时洞察。New Relic 由 Lew Cirne(拥有在苹果公司和 Wily Technology 等公司工作经验的软件工程师和企业家)于 2008 年创立,现已成为应用性能管理APM)市场的领先者。公司总部位于旧金山,并在全球多个城市设有办事处。New Relic 于 2014 年上市,并在纽约证券交易所交易,股票代码为NEWR

New Relic 于 2021 年 5 月公布了 2021 财年的财务报告。根据报告,New Relic 在 2021 财年的总收入为 6.008 亿美元,较上一财年增长了 3%。

值得注意的是,New Relic 在 2021 财年面临了一些挑战,包括 COVID-19 大流行的影响以及向新定价模式的战略转变。

New Relic 的主要目的是帮助公司优化其应用性能,并在问题变成重大问题之前识别它们。该平台提供了对整个应用堆栈的实时可见性,从前端用户界面到后端基础设施,使开发人员和运维团队能够快速识别瓶颈并优化性能。

New Relic 的 APM 解决方案提供多种功能,包括代码级可见性、事务追踪、实时监控和告警。该平台还提供有关应用依赖、数据库性能和用户行为的洞察。

除了 APM,New Relic 还提供一系列其他产品和服务,包括基础设施监控、移动 APM 和浏览器监控。

Ruxit

Ruxit 是一款全面的 APM 解决方案,帮助企业在复杂的分布式应用、微服务和云原生环境中识别和排查性能问题。它最初于 2012 年作为独立公司成立,并在 2015 年被 Dynatrace 收购,扩展了 Dynatrace 的 APM 能力。

Ruxit 的一个关键特性是其能够提供应用性能的端到端可视化,包括代码级诊断、用户体验监控和基础设施监控。这意味着它可以帮助企业快速找出性能问题的根本原因,并发现优化的机会。

Ruxit 还具有一系列其他功能,旨在使监控和故障排除变得更加简单和高效。例如,它使用人工智能和机器学习自动检测异常和性能下降,并实时警报。它还提供一系列分析和可视化工具,帮助用户了解应用性能趋势,并识别随着时间推移的模式。

除了监控功能外,Ruxit 还提供与现代应用环境中常用的其他工具和服务的集成。这包括与容器编排平台(如 Kubernetes)的集成,以及与流行的应用开发框架和工具的集成。

Splunk

Splunk 成立于 2003 年,由 Erik Swan、Rob Das 和 Michael Baum 在美国加利福尼亚州旧金山创办。从那时起,公司的规模迅速扩大,并成为一家全球化的上市公司。Splunk 的软件解决方案被各行各业的组织广泛使用,包括金融服务、医疗保健、政府和零售等行业。

正如你所猜到的,Splunk 是一款数据分析和监控软件解决方案,用于实时监控、搜索、分析和可视化机器生成的数据。该软件可以从多个来源收集和分析数据,包括服务器、应用、网络和移动设备,并提供关于组织 IT 基础设施性能和行为的洞察。

Splunk 的主要用途包括安全监控、应用监控、日志管理和业务分析。使用 Splunk,用户可以识别安全威胁、排查应用性能问题、监控网络活动,并深入了解业务运营。

Splunk 的一个关键特点是其能够从多种来源收集和分析数据,包括结构化数据和非结构化数据。该软件还可以扩展以处理大量数据,使其成为一个强大的工具。

在本节中,我们向您展示了一些由第三方公司托管的领先解决方案,这些解决方案已经可以使用;它们只需要与您的系统进行集成。在下一节中,我们将描述并解释日志和指标的保留策略。

日志和指标保留

数据保留是指保留数据或将数据存储一定时间的做法。这可以包括将数据存储在服务器、硬盘或其他存储设备上。数据保留的目的是确保数据在未来可以用于使用或分析。

数据保留策略通常由组织制定,以确定特定类型的数据应保留多久。这些策略可能受到法规要求、法律义务或业务需求的推动。例如,一些法规可能要求金融机构保留交易数据若干年,而企业可能会选择保留客户数据用于营销或分析目的。

数据保留策略通常包括有关如何存储数据、数据应保留多长时间以及何时应删除数据的指南。有效的数据保留策略可以帮助组织更高效地管理数据、减少存储成本,并确保遵守适用的法规和法律。

在数据保留策略方面,组织有多种选择可供考虑。根据组织的具体需求,不同的策略可能更适合或不适合。

完全保留

在这种策略中,所有数据将被无限期保留。通常用于合规目的,例如满足法规要求,要求数据在特定时间段内进行保留。虽然这种策略可能会很昂贵,因为它需要大量的存储,但它也可以在历史分析和趋势分析方面提供显著的好处。

基于时间的保留

基于时间的保留是一种策略,其中数据在被删除之前会被保留特定的时间段。这种策略通常用于平衡数据需求与存储成本。保留期限可以根据法规要求、业务需求或其他因素来设定。

基于事件的保留

基于事件的保留是一种策略,其中数据根据特定事件或触发条件进行保留。例如,数据可以根据特定的客户或交易进行保留,或者根据事件的严重性进行保留。这种策略可以帮助减少存储成本,同时仍保持对重要数据的访问。

有选择的保留

选择性保留是一种仅保留特定类型数据的策略。这种策略可以用来优先保留最重要的数据,同时减少存储成本。例如,一个组织可能选择仅保留与财务交易或客户互动相关的数据。

分层保留

分层保留是一种根据数据的年龄或重要性将数据存储在不同层级中的策略。例如,最近的数据可能会存储在快速且昂贵的存储上,而较旧的数据则移至速度较慢且成本较低的存储。这种策略可以帮助平衡对近期数据快速访问的需求与逐步降低存储成本的需求。

每种数据保留策略都有其自身的优点和缺点,适合组织的最佳策略将取决于其特定的需求和目标。在选择数据保留策略时,仔细考虑成本、存储容量和所保留数据的价值之间的权衡是非常重要的。

组织中最常见的错误是采用全量保留策略以防万一,这通常会导致磁盘空间耗尽和云成本增加。有时这种策略是合理的,但在大多数情况下并不适用。

总结

在本章中,我们介绍了监控、追踪和日志记录之间的差异。监控是观察和收集系统数据的过程,以确保系统正常运行。追踪是跟踪请求在系统中流动的过程,以识别性能问题。日志记录是记录系统事件和错误的过程,以便后续分析。

我们还讨论了在 Azure、GCP 和 AWS 中的云解决方案,用于监控、日志记录和追踪。对于 Azure,我们提到了 Azure Monitor 用于监控,Azure Application Insights 用于追踪。对于 AWS,我们提到了 CloudWatch 用于监控和日志记录,X-Ray 用于追踪。

接下来,我们解释了如何配置 AWS CloudWatch 代理在 EC2 实例上运行,并通过一个代码示例介绍了如何使用 AWS X-Ray 在分布式系统中追踪请求。

最后,我们列举了一些用于监控、日志记录和追踪的开源和 SaaS 解决方案,包括 Grafana、Prometheus、Datadog、New Relic 和 Splunk。这些解决方案根据用户的需求和偏好,提供了不同的监控和故障排除功能。

在下一章中,我们将通过使用配置即代码解决方案:Ansible,亲自实践自动化服务器配置。

第十一章:使用 Ansible 实现配置即代码

本章我们将介绍 配置管理CM)、配置即代码CaC)以及我们选择的工具:Ansible。

我们将覆盖以下主题:

  • CM 系统与 CaC

  • Ansible

  • Ansible Galaxy

  • 处理机密信息

  • Ansible Tower 和替代方案

  • 高级话题

技术要求

对于本章内容,你需要一个可以通过 ssh 访问的 Linux 系统。如果你的主要操作系统是 Windows,你将需要另外一台 Linux 系统来充当控制节点。到目前为止,Ansible 项目尚不支持 Windows 作为控制节点。

CM 系统与 CaC

设置和维护一个非业余服务器的系统(甚至是业余服务器,可能也需要这样做)是一个严峻的挑战:如何确保系统按照预期正确安装和配置?当你需要安装一台与现有配置完全相同的服务器时,如何确保这一点?过去,一种做法是在安装过程完成后记录当前的配置。这将是一份描述硬件、操作系统、已安装软件、创建的用户以及应用的配置的文档。任何想要重建该配置的人,都需要按照文档中的步骤操作以达到描述的配置。

接下来的合乎逻辑的步骤是编写 shell 脚本,达到与手动过程相同的目标,但有一个额外的改进:这些脚本——只要编写得当、经过测试并且维护良好——不需要人工操作,除非,可能,在初始系统安装时需要人工干预。但一个正确设置的环境将自动处理这一点。

然而,这些脚本也存在一些缺陷或不足。其一是你需要在脚本中考虑未完成执行的情况。这可能由于各种原因发生,导致系统处于部分配置状态。再次执行脚本时,所有配置操作将从头开始,有时会导致意外结果。应对未完成的执行的一种方式是将每个配置操作都包装在检查中,查看该操作是否之前已执行过。这将导致配置脚本变得更大,最终演变成一个配置函数和检查函数的库。

开发和维护这样一个工具的任务可能非常艰巨,并且可能需要一个完整的团队。但最终的结果可能值得付出这些努力。

编写和维护描述系统期望状态的文档,乍一看可能比前面提到的自动化方法更简单且更具吸引力。脚本无法从未完成的执行中恢复过来。它所能做的最好的事情就是通知系统管理员失败,记录错误并优雅地停止。手动配置允许系统管理员绕过程序中的任何障碍和不足,并实时编辑文档以反映当前状态。

尽管如此,一个经过良好开发和测试的脚本最终还是更好。让我们列举出一些原因:

  • 如果脚本执行没有出错,保证会执行正确的操作。一次又一次地证明,在 IT 领域,最容易出错的元素是人类。

  • 如果脚本提前退出,更新它以满足新需求的行为与更新文档的过程完全相同。

  • 众所周知,人类在维护文档方面很差。编程的圣杯是自文档化代码,使注释变得不再必要,从而消除了注释与代码不同步的风险。

  • 脚本可以在多个系统上同时执行,扩展性非常好,几乎是无限的。而人类只能一次配置一个系统,且犯错的风险较小。

  • 以脚本或程序形式保存的配置得益于典型的编程技巧,例如自动化测试、演练和静态分析。更重要的是,将代码保存在版本库中让我们能够轻松追踪更改历史,并与问题跟踪工具集成。

  • 代码是明确的,而书面语言却不能如此。文档可能留有解释的空间,但脚本不会。

  • 自动化配置让你可以转向其他更有趣的任务,留给计算机去做它们最擅长的事情——执行重复性且枯燥的任务。

编程和系统管理的世界倾向于将小项目转变为更大的项目,并且有一个充满活力的开发者和用户社区。CM(配置管理)系统的诞生只是时间问题。它们将开发和管理负责配置操作的代码部分的负担从你肩上移走。CM 系统的开发者编写代码、测试并认为它稳定。剩下的工作就是编写配置文件或指令,告诉系统应该做什么。这些系统大多数能够覆盖最流行的平台,使你能够只描述一次配置,并在商业 Unix 系统(如 AIX 或 Solaris)与 Linux 或 Windows 上获得相同的预期结果。

这些系统的配置文件可以轻松地存储在 Git 等版本控制系统中。它们易于理解,这使得同事可以轻松进行审查。它们可以通过自动化工具检查语法错误,并使你能够专注于整个工作中的最重要部分:配置。

这种将配置保留为一组脚本或其他数据,而不是手动遵循的过程的方法,被称为 CaC。

随着需要管理的系统数量不断增加,以及对快速高效配置的需求不断扩大,CaC(配置即代码)方法变得越来越重要。在 DevOps 的世界里,通常需要每天设置数十个或数百个系统:包括开发人员、测试人员和生产系统,以应对服务需求的新水平。手动管理将是一项不可能完成的任务。良好实现的 CaC 允许通过点击按钮来完成这项任务。因此,开发人员和测试人员可以自行部署系统,而不需要打扰系统运维人员。你的任务将是开发、维护和测试配置数据。

如果编程世界里有一件事是可以确定的,那就是永远不会只有一个解决方案。CM 工具也不例外。

Ansible 的替代方案包括SaltStackChefPuppetCFEngine,后者是最古老的,它的首次发布是在 1993 年,因此截至本书撰写时已存在 30 年。通常,这些解决方案通过强制配置的方法(拉取或推送)和描述系统状态的方法(命令式或声明式)有所不同。

命令式(Imperative)方法意味着我们通过命令描述服务器的状态,让工具执行。命令式编程侧重于一步步描述给定工具的操作方式。

声明式(Declarative)方法意味着我们关注 CaC 工具应该完成什么,而不是指定它应该如何实现结果的所有细节。

SaltStack

SaltStack 是一个开源的 CM 工具,允许在大规模的 IT 基础设施中进行管理。它使得日常任务的自动化成为可能,如软件包安装、用户管理和软件配置,并设计用于跨多种操作系统和平台工作。SaltStack 由 Thomas Hatch 于 2011 年创立。SaltStack 的第一个版本 0.8.0 也发布于 2011 年。

SaltStack 通过利用主从架构工作,其中一个中央的 salt-master 与运行在远程机器上的 salt-minion 进行通信,执行命令并管理配置。它采用拉取方式来强制执行配置:minion 从主服务器拉取最新的清单。

一旦 minion 被安装并配置好,我们可以使用 SaltStack 来管理服务器的配置。以下是一个安装并配置 nginx 的示例 nginx.sls 文件:

nginx:
  pkg.installed
/etc/nginx/sites-available/yourdomain.tld.conf:
  file.managed:
    - source: salt://nginx/yourdomain.tld.conf
    - user: root
    - group: root
    - mode: 644

在这个示例中,第一行指定应该在目标服务器上安装nginx包。接下来的两行定义了一个假设网站example.com的配置文件,该配置文件将被复制到/etc/nginx/sites-available/yourdomain.tld.conf

要将此状态文件应用到服务器,我们需要在 SaltStack 命令行界面中使用state.apply命令,并指定状态文件的名称作为参数:

admin@myhome:~$ salt 'webserver' state.apply nginx

这将把nginx.sls文件中的指令发送到运行在 Web 服务器上的 salt-minion,salt-minion 将执行必要的步骤以确保nginx正确安装和配置。

Chef

Chef是一个强大的开源配置管理(CM)工具,允许用户自动化基础设施、应用程序和服务的部署与管理。它最初由 Opscode 于 2009 年发布,后被 Chef Software Inc.收购。从那时起,Chef 被 IT 专业人员和 DevOps 团队广泛采用,以简化工作流程并减少管理复杂系统所需的时间和精力。

Chef 通过在一组代码文件中定义基础设施的期望状态来工作,这些代码文件称为食谱(cookbooks)。食谱(cookbook)是描述如何安装、配置和管理特定软件或服务的一组指令。每个食谱包含一系列资源,资源是预构建的模块,可以执行特定任务,例如安装软件包或配置文件。Chef 使用声明式的配置管理(CM)方法,意味着用户定义系统的期望状态,而 Chef 负责处理如何实现该状态的细节。

要使用 Chef 安装nginx,你首先需要创建一个包含安装nginx食谱的 cookbook。这个食谱将使用package资源来安装nginx包,并使用service资源确保nginx服务正在运行。根据需求,你还可以使用其他资源,例如filedirectorytemplate,来配置nginx的设置。

一旦创建了食谱(cookbook),你需要将其上传到 Chef 服务器,Chef 服务器作为食谱及其相关元数据的中央仓库。然后,你可以使用 Chef 的命令行工具knife来配置目标系统使用该食谱。这涉及将系统与 Chef 环境关联,Chef 环境定义了应应用于系统的食谱及其版本。接下来,你可以使用chef-client命令在目标系统上运行 Chef 客户端,该客户端将下载并应用必要的食谱和配置,以使系统达到所需状态。

这是安装和配置nginx的示例:

# Install Nginx package
package 'nginx'
# Configure Nginx service
service 'nginx' do
  action [:enable, :start]
end
# Configure Nginx site
template '/etc/nginx/sites-available/yourdomain.tld.conf' do
  source 'nginx-site.erb'
  owner 'root'
  group 'root'
  mode '0644'
  notifies :restart, 'service[nginx]'
end

这个食谱使用了三个资源,如下所示:

  • package:使用系统默认的软件包管理器安装nginx包。

  • service:这将启动并启用nginx服务,使其在启动时自动启动并保持运行。

  • template:通过从模板文件生成配置文件,为nginx创建一个配置文件。模板文件(nginx-site.erb)位于食谱的templates目录中。notifies属性告诉 Chef 在配置文件更改时重新启动nginx服务。

一旦你在食谱中创建了这个食谱,你可以使用knife命令将食谱上传到 Chef 服务器。然后,你可以使用chef-client命令将食谱应用于目标系统,按照食谱安装和配置nginx

Puppet

2.0

Puppet 通过在声明性语言中定义基础设施资源的期望状态来工作,这种语言被称为 Puppet 语言。管理员可以在 Puppet 代码中定义服务器、应用程序和其他基础设施组件的配置,然后将其一致地应用于多个系统。

Puppet 由一个主服务器和多个代理节点组成。主服务器充当 Puppet 代码和配置数据的中央存储库,而代理节点则执行 Puppet 代码并将期望的状态应用到系统中。

Puppet 有一个强大的模块生态系统,这些模块是预先编写的 Puppet 代码,可以用于配置常见的基础设施资源。这些模块可在Puppet Forge中找到,Puppet Forge 是一个公开的 Puppet 代码存储库。

下面是一个示例 Puppet 清单,它安装nginx并创建一个类似我们在 SaltStack 和 Chef 中所做的配置文件:

# Install Nginx
package { 'nginx':
  ensure => installed,
}
# Define the configuration template for the domain
file { '/etc/nginx/sites-available/yourdomain.tld.conf':
  content => template('nginx/yourdomain.tld.conf.erb'),
  owner   => 'root',
  group   => 'root',
  mode    => '0644',
  notify  => Service['nginx'],
}
# Enable the site by creating a symbolic link from sites-available to sites-enabled
file { '/etc/nginx/sites-enabled/yourdomain.tld.conf':
  ensure  => 'link',
  target  => '/etc/nginx/sites-available/yourdomain.tld.conf',
  require => File['/etc/nginx/sites-available/yourdomain.tld.conf'],
}
# Restart Nginx when the configuration changes
service { 'nginx':
  ensure     => running,
  enable     => true,
  subscribe  => File['/etc/nginx/sites-enabled/yourdomain.tld.conf'],
}

一旦你创建了清单并将其放置在 Puppet 服务器上,它将被安装在服务器上的 Puppet 代理拾取并执行。通信方式与 SaltStack 相同,通过 TLS 协议确保安全,使用与互联网上的 HTTPS 服务器相同的机制。

代理节点运行 Puppet 代理进程,通过 TCP 端口8140连接到主服务器。代理向服务器发送证书签名请求CSR),管理员必须批准此请求。一旦 CSR 被批准,代理将获得对服务器 Puppet 配置的访问权限。

当代理运行时,它向主服务器发送请求以获取其配置。服务器响应并提供一个资源目录,说明应应用于节点的内容。该目录是根据存储在服务器上的 Puppet 代码和清单,以及配置的任何外部数据源或层次结构生成的。

然后,代理将目录应用到节点,这包括对节点配置进行必要的更改,以确保其与目录中定义的期望状态匹配。这可能包括安装软件包、更新配置文件或启动或停止服务。

代理在应用完目录后将报告发送回服务器,这些报告可用于监控和审计。服务器还可以使用这些信息检测通过 Puppet 未做的节点配置更改,并在必要时采取纠正措施。

CFEngine

CFEngine 是一个开源的配置管理系统,允许用户自动化部署、配置和维护 IT 系统。它由 Mark Burgess 于 1993 年创立,之后成为管理大规模 IT 基础设施的流行工具。CFEngine 以其强大而灵活的语言来描述系统配置和执行策略而闻名,是处理复杂 IT 环境的理想选择。

CFEngine 的第一次发布是在 1994 年,使其成为现存最古老的配置管理工具之一。此后,CFEngine 经历了许多更新和改进,以适应不断变化的 IT 环境和新兴技术。CFEngine 最新版本 3.18 包括改进的加密功能、增强的监控能力和对云基础设施的更好支持。

多年来,CFEngine 因其强大的功能、易用性和强大的社区支持而广受欢迎。今天,许多组织仍在使用它,并且它仍在积极开发,因此它是一个安全的选择,可以用来管理你的服务器配置。

这里将展示一个 CFEngine 配置示例。由于需要,它只是一个片段,而非完整配置:

##############################################################
# cf.main - for master infrastructure server
##################################################################
###
# BEGIN cf.main
###
control:
   access    = ( root )        # Only root should run this
   site      = ( main )
   domain    = ( example.com )
   sysadm    = ( admin@example.com )
   repository = ( /var/spool/cfengine )
   netmask   = ( 255.255.255.0 )
   timezone  = ( CET )
#################################################################
files:
  Prepare::
      /etc/motd              m=0644 r=0 o=root act=touch

在本节中,我们已经解释了什么是 CaC,以及为什么它是系统管理员工具箱中的一个重要工具。我们简要描述了你可以使用的最流行的工具。在下一节中,我们将介绍我们首选的工具——Ansible。

Ansible

在本节中,我们将向你介绍 Ansible,这是我们在 CaC 方面的首选工具。

Ansible 是一款用于管理系统和设备配置的工具。它是用 Python 编写的,源代码可以自由下载和修改(在 Apache License 2.0 许可范围内)。名字“Ansible”来源于 Ursula K. Le Guin 的小说 Rocannon’s World,指的是一种无论距离多远都能实现瞬时通信的设备。

这里列出了 Ansible 一些有趣的特点:

  • 模块化:Ansible 不是一个单体工具。它是一个核心程序,每个它知道如何执行的任务都被写作一个独立的模块——如果你愿意的话,可以把它看作是一个库。由于从一开始就是这种设计,它产生了一个干净的 API,任何人都可以使用这个 API 来编写自己的模块。

  • 幂等性:无论执行多少次配置,结果始终保持不变。这是 Ansible 最重要和最基本的特点之一。你不必知道哪些操作已经执行过。当你扩展配置并再次运行工具时,它的工作是找出系统的状态并仅应用新的操作。

  • 无代理:Ansible 不会在配置系统上安装代理。这并不意味着它完全不需要任何东西。为了执行 Ansible 脚本,目标系统需要能够连接到它的一些手段(通常是运行的 SSH 服务器)以及安装 Python 语言。这个范式带来了几个优势,包括以下几点:

    • Ansible 与通信协议无关。它使用 SSH,但并不实现该协议,而是将细节留给操作系统、SSH 服务器和客户端。一个优势是,你可以根据需要自由地将一个 SSH 解决方案替换为另一个,而你的 Ansible 剧本应该照常工作。此外,Ansible 也不关心 SSH 配置的安全性,这让开发人员可以集中精力处理系统的真正问题:配置你的系统。

    • Ansible 项目无需为被管理的节点开发和维护独立的程序。这不仅减轻了开发人员的不必要负担,还限制了安全漏洞被发现并用于攻击目标机器的可能性。

    • 在使用代理的解决方案中,如果由于任何原因,代理程序停止工作,就无法将新的配置传递到系统中。SSH 服务器通常使用广泛,故障的概率几乎可以忽略不计。

    • 使用 SSH 作为通信协议可以降低防火墙阻止 CM 系统通信端口的风险。

  • nginx 已安装,正确的配置项应该如下所示:

    - name: install nginx
    
      package:
    
        name: nginx
    
        state: present
    

与 Ansible 交互的主要方式是通过编写使用一种叫做 YAML 的特殊语法的配置文件。YAML 是一种专为配置文件设计的语法,基于 Python 的格式化方式。缩进在 YAML 文件中起着重要作用。YAML 的主页提供了完整的备忘单卡片(yaml.org/refcard.xhtml),其中涵盖了该语法。然而,这里将呈现最重要的部分,因为我们在本章中将主要与这些文件打交道:

  • 它们是明文文件。这意味着可以使用最简单的编辑器(如 Notepad、Vim、Emacs,或任何你喜欢的文本编辑工具)查看和编辑它们。

  • 缩进用于表示范围。不允许使用制表符进行缩进,通常使用空格来进行缩进。

  • 新文档以三个短横线(-)开头。一个文件可以包含多个文档。

  • 注释与 Python 中相同,以井号(#)开始,直到行尾。注释必须被空白字符包围,否则它将被视为文本中的字面井号(#)。

  • 文本(字符串)可以不加引号、单引号(')或双引号(")括起来。

  • 当指定一个列表时,每个成员由一个连字符(-)表示。每个项目将占一行。如果需要单行表示,可以将列表项用方括号([])括起来,并用逗号(,)分隔。

  • 关联数组通过键值对表示,每个键和值之间用冒号和空格分隔。如果必须在一行中呈现,数组将用大括号({})括起来,键值对之间用逗号(,)分隔。

如果前面的规则现在还不太清楚,别担心。我们将编写正确的 Ansible 配置文件,随着过程的推进,所有问题都会明朗。

Ansible 将机器分为两组:控制节点是存储配置指令的计算机,它将连接到目标机器并进行配置。控制节点可以有多个。目标机器称为库存。Ansible 将在列出的库存中的计算机上运行操作。库存通常以初始化INI)格式编写,这种格式简单易懂,如下所示:

  • 注释以分号(;)开头。

  • 各个部分有名称,并且名称用方括号([])括起来。

  • 配置指令以键值对的形式存储,每一对独占一行,键和值之间用等号(=)分隔。

我们稍后会看到一个库存文件的例子。然而,也有可能存在所谓的动态库存,它是通过脚本或每次运行 Ansible 时由系统自动生成的。

我们将要交互的主要配置文件称为剧本。剧本是 Ansible 的入口点,工具将从这里开始执行。剧本可以包括其他文件(这通常会这么做)。

目标主机可以根据自定义标准进行分组:操作系统、组织中的角色、物理位置——任何需要的内容。这样的分组称为角色

一项要执行的单一操作称为任务

使用 Ansible 的基础知识

首先要做的是安装 Ansible。这在所有主要的 Linux 发行版上都非常简单,macOS 也同样如此。我们建议您根据所选择的操作系统找到相应的解决方案。

对于基于 Debian 的发行版,以下命令应该足够:

$ sudo apt-get install ansible

对于 Fedora Linux 发行版,您可以运行以下命令:

$ sudo dnf install ansible

然而,要安装最新版本的 Ansible,我们建议使用 Python 虚拟环境及其pip工具,如下所示:

$ python3 -m venv venv

在前面的代码中,我们使用venv python3模块激活了虚拟环境。它将创建一个特殊的venv目录,其中包含所有重要的文件和库,允许我们设置 Python 虚拟环境。接下来,我们有以下内容:

$ source venv/bin/activate

在前面的代码行中,我们读取了一个特殊的文件,该文件通过配置 shell 来设置环境。接下来,我们有以下内容:

$ pip install -U pip
Requirement already satisfied: pip in ./venv/lib/python3.11/site-packages (22.3.1)
Collecting pip
  Using cached pip-23.0.1-py3-none-any.whl (2.1 MB)
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 22.3.1
    Uninstalling pip-22.3.1:
      Successfully uninstalled pip-22.3.1
Successfully installed pip-23.0.1

在上面的代码中,我们已经升级了 pip,一个 Python 包管理器。在下一步中,我们将实际安装 Ansible,也使用 pip。为了简洁起见,输出将被缩短:

$ pip install ansible
Collecting ansible
  Using cached ansible-7.3.0-py3-none-any.whl (43.1 MB)
Collecting ansible-core~=2.14.3
Installing collected packages: resolvelib, PyYAML, pycparser, packaging, MarkupSafe, jinja2, cffi, cryptography, ansible-core, ansible
Successfully installed MarkupSafe-2.1.2 PyYAML-6.0 ansible-7.3.0 ansible-core-2.14.3 cffi-1.15.1 cryptography-39.0.2 jinja2-3.1.2 packaging-23.0 pycparser-2.21 resolvelib-0.8.1

正如你在上面的代码中看到的,pip 告诉我们 Ansible 及其依赖项已成功安装。

这两种安装 Ansible 的方式都将下载并安装运行 Ansible 所需的所有软件包。

你可以运行的最简单命令是所谓的临时 ping。它如此基础,以至于它成为了 Ansible 在教程和书籍中最常见的入门用法之一。我们也不打算偏离它。以下命令尝试连接到指定主机,并打印连接结果:

$ ansible all -m ping -i inventory
hostone | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

在上面的命令中,我们告诉 Ansible 运行针对清单中的所有主机,使用 ping 模块(-m ping),并使用名为 inventory 的清单文件(-i inventory)。如果你没有指定清单文件,Ansible 将尝试使用 /etc/ansible/hosts,但通常这样做不是个好主意。

我们将在稍后的段落中深入探讨清单文件。

一个 ping 模块(我们也可以理解为一个 ping 命令)。然而,它与操作系统中的 ping 命令不同,后者发送一个特别制作的网络数据包来判断主机是否在线。而 Ansible 的 ping 命令则会尝试登录,以确定凭证是否正确。

默认情况下,Ansible 使用 SSH 协议和一对公私密钥。在正常操作中,你通常不想使用基于密码的身份验证,而是会选择基于密钥的身份验证。

Ansible 在执行结果中非常擅长提供自解释的信息。上面的输出告诉我们,Ansible 成功连接到清单中的所有节点(这里只有一个),python3 已安装,并且没有做任何更改。

清单文件是一个简单但强大的工具,用于列出、分组和为被管理的节点提供变量。我们的示例清单文件如下所示:

[www]
hostone ansible_host=192.168.1.2  ansible_ssh_private_key_file=~/.ssh/hostone.pem  ansible_user=admin

上面的代码有两行。第一行声明了一个名为 www 的节点组。第二行声明了一个名为 hostone 的节点。由于这是一个无法通过 DNS 解析的名称,我们通过 ansible_host 变量声明了它的 IP 地址。然后,我们指定了正确的 ssh 密钥文件,并声明了登录时应该使用的用户名(admin)。在这个文件中,我们还可以定义更多内容。有关编写清单文件的详细信息,可以参考 Ansible 项目的文档 (docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.xhtml)。

我们无法涵盖 Ansible 的所有方面,也没有深入探讨我们在这里将使用的大部分功能。然而,Packt 出版社有很多很好的 Ansible 书籍,如果你想深化你的知识,可能会有兴趣选择其中的一些书籍——例如 James Freeman 和 Jesse Keating 的《Mastering Ansible,第四版》

任务(Tasks)

任务是 Ansible 配置的核心。它们正是那些要在托管节点上执行的任务。任务包含在剧本中。它们可以直接放入剧本中(写入剧本文件),也可以间接地通过角色包含。

有一种特殊类型的任务叫做处理(handles)。这是一个只有在另一个任务通知时才会执行的任务。

角色(Roles)

Ansible 的文档将角色(roles)定义为“可重用 Ansible 内容(任务、处理程序、变量、插件、模板和文件)的一种有限分发,用于剧本内部使用。”就我们而言,我们可以将它们视为一种将剧本部分进行分组的机制。一个角色可以是我们要执行剧本的主机类型:Web 服务器、数据库服务器、Kubernetes 节点等等。通过将剧本分解为角色,我们可以更容易、更高效地管理单独的任务。更不用说,这些包含文件的内容也变得更加可读,因为我们限制了其中的任务数量,并按其功能进行分组。

剧本和剧本集

剧本(Plays)是在任务执行的上下文中。虽然剧本是一个有些短暂的概念,剧本集(playbooks)是其物理表现:定义剧本的 YAML 文件。

让我们看一个剧本示例。以下剧本将在托管节点上安装nginxphp软件包:

---
- name: Install nginx  and php
  hosts: www
  become: yes
  tasks:
  - name: Install nginx
    package:
      name: nginx
      state: present
  - name: Install php
    package:
      name: php8
      state: present
  - name: Start nginx
    service:
      name: nginx
      state: started

第一行(三个破折号)标志着一个新的 YAML 文档的开始。下一行是整个剧本的名称。剧本的名称应简短但具有描述性。它们最终会出现在日志和调试信息中。

接下来,我们通知 Ansible 该剧本应该在www组中的节点上执行。我们还告诉 Ansible 在执行命令时使用sudo。这是必需的,因为我们在本指南中覆盖的所有发行版都需要 root 权限来安装和删除软件包。

然后,我们开始tasks部分。每个任务都会被命名,并指定我们将要使用的模块(命令),同时命令会附带选项和参数。如你所见,缩进声明了范围。如果你熟悉 Python 编程语言,这对你来说应该是直观的。

在运行这个剧本之前,让我们使用一个非常有用的工具,ansible-lint

$ ansible-lint install.yaml
WARNING: PATH altered to expand ~ in it. Read https://stackoverflow.com/a/44704799/99834 and correct your system configuration.
WARNING  Listing 9 violation(s) that are fatal
yaml[trailing-spaces]: Trailing spaces
install.yaml:1
yaml[truthy]: Truthy value should be one of [false, true]
install.yaml:4
fqcn[action-core]: Use FQCN for builtin module actions (package).
install.yaml:6 Use `ansible.builtin.package` or `ansible.legacy.package` instead.
[...]
yaml[empty-lines]: Too many blank lines (1 > 0)
install.yaml:18
Read documentation for instructions on how to ignore specific rule violations.
                   Rule Violation Summary
 count tag                   profile    rule associated tags
     1 yaml[empty-lines]     basic      formatting, yaml
     1 yaml[indentation]     basic      formatting, yaml
     3 yaml[trailing-spaces] basic      formatting, yaml
     1 yaml[truthy]          basic      formatting, yaml
     3 fqcn[action-core]     production formatting
Failed after min profile: 9 failure(s), 0 warning(s) on 1 files.

为了简洁,我已经删除了一部分输出,但你可以看到工具打印了关于 YAML 语法和 Ansible 最佳实践违规的信息。失败是会停止剧本执行的错误类型。警告仅仅是警告:剧本将继续执行,但存在一些违反最佳实践的错误。让我们按如下方式修正我们的剧本:

---
- name: Install nginx  and php
  hosts: www
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present
    - name: Install php
      ansible.builtin.package:
        name: php8
        state: present
    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started

现在我们可以使用ansible-playbook命令运行 playbook:

$ ansible-playbook -i inventory install.yaml
PLAY [Install nginx  and php] ********************************************************************
TASK [Gathering Facts] ********************************************************************
ok: [hostone]
TASK [Install nginx] ********************************************************************
changed: [hostone]
TASK [Install php] ********************************************************************
fatal: [hostone]: FAILED! => {"changed": false, "msg": "No package matching 'php8' is available"}
PLAY RECAP ********************************************************************
hostone     : ok=2    changed=1   reachable=0    failed=1    skipped=0    rescued=0    ignored=0

在前面的输出中,我们指示ansible-playbook命令使用名为inventory的清单文件,并运行名为install.yaml的 playbook。输出应该是自解释的:我们会看到我们运行的 play 的名称。接下来,我们会看到 Ansible 将尝试执行操作的受管节点列表。然后,我们看到任务和任务成功或失败的节点列表。nginx任务在hostone上成功执行。然而,安装php失败了。Ansible 给出了确切的原因:我们的受管节点上没有php8包。有时,解决方案很明显,但有时则需要一些挖掘。经过与我们的发行版检查后,我们发现实际可用的php包是php7.4。在快速更正有问题的行后,我们再次运行 playbook,如下所示:

$ ansible-playbook -i inventory install.yaml
PLAY [Install nginx  and php] ********************************************************************
TASK [Gathering Facts] ********************************************************************
ok: [hostone]
TASK [Install nginx] ********************************************************************
ok: [hostone]
TASK [Install php] ********************************************************************
changed: [hostone]
TASK [Start nginx] ********************************************************************
ok: [hostone]
PLAY RECAP ********************************************************************
hostone                    : ok=4    changed=1    unreachable=0   failed=0    skipped=0    rescued=0    ignored=0

注意输出中的变化。首先,Ansible 告诉我们hostone上的nginx是正常的。这意味着 Ansible 能够确认包已安装,因此没有采取任何行动。接着,它告诉我们hostone服务器上的php7.4安装成功(changed: [hostone])。

前面的 playbook 很短,但我们希望它能够展示这个工具的有用性。Playbook 是以线性方式执行的,从上到下。

我们的 playbook 存在一个问题。虽然它只安装了两个包,但如果需要安装数十个甚至上百个包,你可能会担心可维护性和可读性。为每个包单独创建任务是麻烦的。幸好,有解决方案。你可以为给定任务创建一个项目列表,任务将对该列表中的每个项目执行——类似于一个循环。让我们如下编辑 playbook 的tasks部分:

  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: '{{ item }}'
        state: present
      with_items:
        - nginx
        - php7.4
       - gcc
       - g++
    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started

我们添加了两个包,以展示一个更完整的运行过程。注意没有为php7.4单独创建任务。

在运行这个 play 之前,最好先用ansible-lint检查一下。下面是如何操作:

$ ansible-lint install.yaml
Passed with production profile: 0 failure(s), 0 warning(s) on 1 files.

现在,在ansible-lint给出绿色信号后,让我们运行这个 playbook:

$ ansible-playbook -i inventory install.yaml
PLAY [Install nginx  and php] ********************************************************************
TASK [Gathering Facts] ********************************************************************
ok: [hostone]
TASK [Install nginx] ********************************************************************
ok: [hostone] => (item=nginx)
ok: [hostone] => (item=php7.4)
changed: [hostone] => (item=gcc)
changed: [hostone] => (item=g++)
TASK [Start nginx] ********************************************************************
ok: [hostone]
PLAY RECAP ********************************************************************
hostone            : ok=3    changed=1    unreachable=0   failed=0    skipped=0    rescued=0    ignored=0

假设我们想在nginx中安装页面配置。Ansible 可以复制文件。我们可以设置它来复制nginx虚拟服务器配置,但我们只希望在服务设置时重启nginx一次。

我们可以通过notifyhandlers来实现这一点。我会先粘贴整个 playbook:

---
- name: Install nginx  and php
  hosts: www
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: '{{ item }}'
        state: present
      with_items:
        - nginx
        - php7.4
        - gcc
        - g++
      notify:
        - Start nginx
    - name: Copy service configuration
      ansible.builtin.copy:
        src: "files/service.cfg"
        dest: "/etc/nginx/sites-available/service.cfg"
        owner: root
        group: root
        mode: '0640'
    - name: Enable site
      ansible.builtin.file:
        src: "/etc/nginx/sites-available/service.cfg"
        dest: "/etc/nginx/sites-enabled/default"
        state: link
      notify:
        - Restart nginx
  handlers:
    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

注意到与复制文件相关的整个新部分。我们还创建了一个nginx。为了简洁起见,我们简化了它,只是为了展示一个原理。

执行此 play 后,得到以下输出:

$ ansible-playbook -i inventory install.yaml
PLAY [Install nginx  and php] ********************************************************************
TASK [Gathering Facts] ********************************************************************
ok: [hostone]
TASK [Install nginx] ********************************************************************
ok: [hostone] => (item=nginx)
ok: [hostone] => (item=php7.4)
ok: [hostone] => (item=gcc)
ok: [hostone] => (item=g++)
TASK [Copy service configuration] ********************************************************************
ok: [hostone]
TASK [Enable site] ********************************************************************
ok: [hostone]
PLAY RECAP ********************************************************************
hostone                     ok=4    changed=0    unreachable=0   failed=0    skipped=0    rescued=0    ignored=0

一旦你开始在实际生产环境中使用 Ansible,就会明显发现,即使使用我们之前展示的循环技巧,playbook 也会迅速增长并变得难以管理。幸运的是,有一种方法可以将 playbook 分解成更小的文件。

其中一种方法是将任务划分为不同的角色。如前所述,角色是一个抽象的概念,因此最简单的方式是将其视为一个组。你根据对你来说相关的标准将任务分组。虽然标准完全由你决定,但通常情况下,角色会根据给定的受管节点执行的功能类型来命名。一个受管节点可以执行多个功能,但仍然最好将这些功能分开到不同的角色中。因此,HTTP 服务器将是一个角色,数据库服务器将是另一个角色,文件服务器将是另一个角色。

Ansible 中的角色有一个预定的目录结构。定义了八个标准目录,尽管你只需要创建其中一个。

以下代码摘自 Ansible 文档(docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.xhtml):

roles/
    common/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        templates/        #  <-- files for use with the template resource
            ntp.conf.j2   #  <------- templates end in .j2
        files/            #
            bar.txt       #  <-- files for use with the copy resource
            foo.sh        #  <-- script files for use with the script resource
        vars/             #
            main.yml      #  <-- variables associated with this role
        defaults/         #
            main.yml      #  <-- default lower priority variables for this role
        meta/             #
            main.yml      #  <-- role dependencies
        library/          # roles can also include custom modules
        module_utils/     # roles can also include custom module_utils
        lookup_plugins/   # or other types of plugins, like lookup in this case

roles 目录与 playbook 位于同一级别。你可能会看到的最常见子目录是 taskstemplatesfilesvarshandlers。在每个子目录中,Ansible 会查找一个 main.ymlmain.yamlmain 文件(所有这些文件必须是有效的 YAML 文件)。它们的内容将自动提供给 playbook。那么,这在实践中是如何工作的呢?在与我们当前 install.yaml playbook 同一目录下,我们将创建一个 roles 目录。在该目录下,我们将再创建一个目录:www。在这个目录中,我们将创建:tasksfileshandlers 目录。我们将为 development 角色创建一个类似的结构:

roles/
    www/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        files/            #
            service.cfg       #  <-- files for use with the copy resource
    development/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted

development 角色只有任务子目录,因为我们不需要任何额外的附加功能。那么,为什么是开发角色呢?当你回顾我们的当前 playbook 时,你会发现我们将 www 服务器和开发软件包(即编译器)的安装混在一起了。这是一个不好的做法,即使它们最终会安装在同一台物理服务器上。

因此,我们将编辑我们的清单文件,以便我们有两个独立的角色:

[www]
hostone ansible_host=192.168.1.2  ansible_ssh_private_key_file=~/.ssh/hostone.pem  ansible_user=admin
[development]
hostone ansible_host=192.168.1.3  ansible_ssh_private_key_file=~/.ssh/hostone.pem  ansible_user=admin

两个组只包含一个节点,这种做法整体上是不好的。我们这样做仅仅是为了本指南的目的。不要在 HTTP 服务器上安装编译器和开发软件,尤其是在生产环境中。

现在,我们需要在 playbook 中移动一些内容。install.yml 文件将变得更短,正如我们在这里看到的:

---
- name: Install nginx and php
  hosts: www
  roles:
    - www
  become: true
- name: Install development packages
  hosts: development
  roles:
    - development
  become: true

其实我们在这个 playbook 中有两个 play。一个是针对 www 主机的,另一个是针对 development 主机的。我们为每个 play 起个名字,并列出希望它运行的主机组,然后使用关键字 roles 列出实际使用的角色。如你所见,只要你遵循之前解释的目录结构,Ansible 会自动定位到正确的角色。当然,也可以通过指定角色的完整路径直接包含角色,但我们这里不做详细讲解。

现在,对于 www 角色,我们将执行以下代码:

---
- name: Install nginx
  ansible.builtin.package:
    name: '{{ item }}'
    state: present
  with_items:
    - nginx
    - php7.4
  notify:
    - Start nginx
- name: Copy service configuration
  ansible.builtin.copy:
    src: "files/service.cfg"
    dest: "/etc/nginx/sites-available/service.cfg"
    owner: root
    group: root
    mode: '0640'
- name: Enable site
  ansible.builtin.file:
    src: "/etc/nginx/sites-available/service.cfg"
    dest: "/etc/nginx/sites-enabled/default"
    state: link
  notify:
    - Restart nginx

从一开始你就应该注意到几个变化:

  • 这份文档中没有 tasks 关键字。这是因为该文件是 tasks 子目录中的 main.yaml 文件,默认包含任务。

  • 缩进已经向左移动。

  • 这里没有 handlers。

  • 我们已经移除了 gccg++ 包的安装。

现在,让我们来看一下 handlers:

---
- name: Start nginx
  ansible.builtin.service:
    name: nginx
    state: started
  listen: "Start nginx"
- name: Restart nginx
  ansible.builtin.service:
    name: nginx
    state: restarted
  listen: "Restart nginx"

总体变化如这里所示:

  • 我们已经移除了 handlers 关键字

  • 我们已将缩进移到左侧

现在,让我们来看一下 roles/development/tasks/main.yaml

---
- name: Install compilers
  ansible.builtin.package:
    name: '{{ item }}'
    state: present
  with_items:
    - gcc
    - g++

这非常简单。我们可能增加了目录结构的复杂性,但我们也简化了任务和 play 的编写。当你的 playbook 越来越大,越来越复杂时,这些好处完全值得这个权衡。

使用 CaC 工具有许多优势,具体如下:

  • 配置可以进行 lint 检查——也就是说,可以通过自动化工具检查语法错误

  • 它可以无限次应用,且结果相同

  • 它可以在多个系统上并行运行

  • 它可以存放在版本控制系统中,比如 Git,其中保存了变更的历史记录和评论,可以随时查看

  • 它可以由自动化工具运行,从而不再需要人工干预,只需编写 playbook 即可

在这一小节中,我们展示了如何编写简单的 Ansible playbook。我们解释了什么是 Ansible,以及配置脚本的基本组成部分。我们介绍了 playbook、角色和清单。Ansible 还可以做很多其他事情。你可以管理设备、文件系统、用户、组、权限、网络等等。Ansible 默认自带的所有模块列表令人印象深刻。你可以随时查看这个列表,访问 docs.ansible.com/ansible/latest/module_plugin_guide/index.xhtml。记得查看你所使用的 Ansible 版本的列表。

所以,现在我们已经介绍了如何安装、配置以及使用 Ansible 来管理你的服务器(安装软件、创建配置文件和管理服务),接下来我们将探讨 Ansible Galaxy:一个社区开发的模块,能够提升 Ansible 的实用性。

Ansible Galaxy

Ansible 是一个强大的自动化工具,使用户能够轻松配置、部署和管理复杂的 IT 基础设施。然而,创建和维护 Ansible playbook 可能会耗费时间,特别是在处理大规模环境时。幸运的是,Ansible Galaxy 通过提供一个预构建角色和 playbook 的中心化存储库,来帮助简化这个过程。

Ansible Galaxy是一个社区驱动的平台,托管着大量的 Ansible 角色和 playbook。这些角色和 playbook 由全球用户提交,并由 Ansible 的维护人员进行审查和策划。Ansible Galaxy 提供了一种简单高效的方法来查找和使用预构建的自动化内容,可以节省用户的时间和精力,同时确保质量和一致性。

使用 Ansible Galaxy,用户可以快速找到、下载和使用流行应用程序、服务和基础设施组件的预构建角色和 playbook。这些预构建组件可以帮助加快部署时间,确保遵循最佳实践,并减少错误或不一致性的可能性。Ansible Galaxy 还可以帮助用户从他人的经验中学习,并获得同行最佳实践的见解。

让我们使用一个 Galaxy 角色来在我们的webserver角色上安装nginx web 服务器。为了做到这一点,我们需要从 Ansible Galaxy 安装角色。首先,请确保在系统上安装了 Ansible,运行以下命令:

admin@myhome:~$ ansible-galaxy install nginxinc.nginx

此命令将从 Ansible Galaxy 下载并安装nginx角色。默认情况下,所有安装的角色都放置在~/.ansible/roles目录中。您可以通过在您的家目录中创建全局 Ansible 配置文件~/.ansible.cfg来更改这一设置。

更改roles_path目录的配置文件示例如下:

[defaults]
roles_path = /home/admin/myansibleroles

一个良好的做法是固定角色版本号,并将该版本保存在与 Ansible playbook 同一 Git 存储库中的 YAML 文件中。为了实现这一点,让我们创建一个ansible_requirements.yml文件:

---
- src: nginxinc.nginx
  version: 0.24.0

使用该文件从 Ansible Galaxy 安装角色,您可以运行以下命令:

admin@myhome:~$ ansible-galaxy install -r ansible_requirements.yml

安装角色后,可以通过将以下行添加到 playbook 中,在 Ansible playbook 中使用该角色:

roles:
    - nginxinc.nginx

这是一个示例 playbook,使用 Ansible Galaxy 中的nginx角色在远程服务器上安装和配置nginx

---
- name: Install and configure Nginx
  hosts: webservers
  become: true
  roles:
    - nginxinc.nginx
  vars:
    nginx_sites:
      myapp:
        template: "{{ playbook_dir }}/templates/myapp.conf.j2"

在这个 playbook 中,我们将webservers组指定为目标主机,并使用nginxinc.nginx角色来安装和配置nginx。我们还定义了一个名为nginx_sites的变量,该变量指定了在 playbook 的templates目录中使用的 Jinja2 模板来创建一个nginx服务器块的配置。

通过使用 Ansible Galaxy 和预构建角色,如 nginxinc.nginx,用户可以快速而可靠地自动化复杂任务,确保一致性并减少错误的风险。

处理机密

保护诸如密码、令牌和证书等机密在任何 IT 基础设施中都至关重要。这些机密是访问敏感信息和服务的钥匙,它们的泄露可能导致严重的安全漏洞。因此,保持它们的安全至关重要。Ansible 提供了多种管理机密的方法,例如 Ansible Vault,它允许用户使用密码或密钥文件加密和解密敏感数据。此功能有助于保护机密,并确保只有授权用户才能访问它们。

将机密保存在 Git 仓库或任何其他公共地方是一个重大的安全风险。这类仓库通常对多个用户开放,其中一些用户可能没有访问敏感数据的必要权限。此外,像 Git 这样的版本控制系统会保留文件修改的历史记录,这可能导致机密不小心泄露。如果用户不小心将机密提交到仓库,或者黑客获取了仓库的提交历史,机密就可能会被暴露。因此,必须避免将机密保存到公共地方,以降低未经授权访问的风险。相反,Ansible 提供了安全的方式来管理机密,确保它们被加密且仅授权用户可访问。这样,用户可以确信他们的机密是安全的。

Ansible Vault

Ansible Vault 是 Ansible 提供的一个功能,允许用户加密和解密敏感数据,如密码、密钥和证书。该保险库创建一个加密文件,只有授权用户才能解密,从而确保敏感数据的安全。保险库可用于存储文件、变量或其他 Ansible 使用的源中的机密。

Ansible Vault 使用多种加密方法来保护其中存储的机密。默认情况下,Ansible Vault 使用 AES 256 加密,这是一个广泛接受且安全的加密算法。此外,Ansible Vault 还支持其他加密算法,如 AES 192 和 AES 128,为加密强度提供了灵活性。当使用 Ansible Vault 加密数据时,用户可以选择使用密码或密钥文件进行加密。这确保了只有持有密码或密钥文件的授权用户才能解密存储在保险库中的机密。

要使用 Ansible Vault 创建一个新的保险库,可以使用以下命令:

admin@myhome:~$ ansible-vault create secrets.yml
New Vault password:
Confirm New Vault password:

这将创建一个名为 secrets.yml 的新加密保险库文件。系统会提示你输入一个密码来加密文件。一旦输入密码,保险库文件将被创建并在默认编辑器中打开。以下是一个示例 secrets.yml 文件:

somesecret: pleaseEncryptMe
secret_pgsql_password: veryPasswordyPassword

要编辑这个秘密文件,你需要使用ansible-vault edit secrets.yml命令,并在之后输入加密密码。

要编写一个 Ansible 任务,从保险库中读取pgsql_password秘密,你可以使用ansible.builtin.include_vars模块,并指定vault_password_file参数。以下是一个示例任务:

- name: Read pgsql_password from Ansible Vault
  include_vars:
    file: secrets.yml
    vault_password_file: /path/to/vault/password/file
  vars:
    pgsql_password: "{{ secret_pgsql_password }}"

在这个任务中,我们使用include_vars模块从secrets.yml保险库文件中读取变量。vault_password_file参数指定包含解密保险库密码的文件位置。然后,我们将secret_pgsql_password的值赋给pgsql_password变量,可以在剧本的其他地方使用该变量。

请注意,secret_pgsql_password变量应该在保险库中定义。secret_前缀表示该密码是从保险库中检索的。Ansible 不区分常规变量和秘密变量。

当你以提高的 Ansible 调试级别运行这个剧本时,你会注意到 PostgreSQL 密码暴露在调试输出中。为了防止这种情况发生,任何处理敏感信息的任务都可以启用no_log: True选项来执行。

SOPS

Secrets OPerationSSOPS)是 Mozilla 开发的一个开源工具,允许用户在各种配置文件中安全地存储和管理他们的秘密信息,包括 Ansible 剧本。SOPS 采用混合加密方法,即结合了对称加密和非对称加密,以确保最大安全性。

SOPS 使用主密钥加密秘密信息,主密钥可以是对称的或非对称的。对称加密使用密码或密码短语来加密和解密秘密,而非对称加密使用一对密钥,一个公钥和一个私钥,来加密和解密秘密。SOPS 通过密钥包装加密主密钥,密钥包装是一种使用另一个密钥加密密钥的技术。在 SOPS 中,通常用于密钥包装的密钥是 AWS 密钥管理服务KMS)密钥,但也可以是Pretty Good PrivacyPGP)密钥或 Google Cloud KMS 密钥。

SOPS 与 Ansible 无缝集成,支持多种文件格式,包括 YAML、JSON 和 INI。它还支持多种云提供商,包括 AWS、Google Cloud 和 Azure,使其成为跨不同环境管理秘密的多功能工具。

这是如何创建一个 SOPS 加密的 YAML 文件并使用community.sops.load_vars将其内容加载到 Ansible 剧本中的示例:

首先,创建一个名为secrets.yaml的 YAML 文件,内容如下:

postgresql_password: So.VerySecret

然后,使用以下命令通过 SOPS 加密文件:

admin@myhome:~$ sops secrets.yaml > secrets.sops.yaml

这将创建一个名为secrets.sops.yaml的加密版本的secrets.yaml文件。现在可以安全地删除secrets.yaml的明文文件。最常见的错误是忘记删除这些文件并将它们提交到 Git 仓库中。

接下来,创建一个新的 Ansible 剧本,命名为database.yml,并包含以下内容:

---
- hosts: dbserver
  become: true
  vars:
    postgresql_password: "{{ lookup('community.sops.load_vars', 'secrets.sops.yaml')['postgresql_password'] }}"
  tasks:
    - name: Install PostgreSQL
      apt:
        name: postgresql
        state: present
    - name: Create PostgreSQL user and database
      postgresql_user:
        db: mydatabase
        login_user: postgres
        login_password: "{{ postgresql_password }}"
        name: myuser
        password: "{{ postgresql_password }}"
        state: present

在这个示例中,我们使用 community.sops.load_vars 从加密的 secrets.sops.yaml 文件中加载 postgresql_password 变量。然后,使用 {{ postgresql_password }} Jinja2 语法将 postgresql_password 变量传递给 postgresql_user 任务。

当您使用 Ansible 运行这个 playbook 时,它会使用 SOPS 解密 secrets.sops.yaml 文件,并将 postgresql_password 变量加载到 playbook 中的 postgresql_password 变量中。这确保密码不会以明文形式存储在 playbook 中,为您的敏感信息提供额外的安全层。

有关 SOPS 的更多信息,请参见其官方 GitHub 仓库:github.com/mozilla/sops

其他解决方案

有几种替代 Ansible Vault 和 SOPS 的解决方案,可以用于安全地管理敏感数据,具体如下:

  • HashiCorp Vault:一个开源工具,用于安全地存储和访问秘密。它提供了一种服务,用于安全和集中存储秘密、访问控制和审计。

  • Blackbox:一个命令行工具,使用 GNU Privacy GuardGPG)加密和解密文件。它通过为每个需要访问加密数据的用户或团队创建一个单独的 GPG 密钥来工作。

  • Keywhiz:另一个开源的秘密管理系统,提供一种简单的方法来安全地存储和分发秘密。它包括一个用于管理秘密的 Web 界面和一个用于访问秘密的命令行工具。

  • Azure Key Vault, AWS Secrets Manager 或 Google Cloud Secret Manager:在处理云环境时,您可能想要考虑使用的解决方案来存储秘密。

当然,这不是所有可用选项的完整列表。

在本节中,我们介绍了 Ansible Vault 和 SOPS 作为处理秘密(如密码)的方法。在下一节中,我们将介绍 Ansible 的图形前端(GUI)。

Ansible Tower 和替代方案

Ansible Tower 提供了一个集中平台,用于管理 Ansible 自动化工作流,使 IT 团队更容易合作、共享知识和维护基础设施。其一些关键功能包括用于管理 Ansible playbook、库存和作业运行的基于 Web 的界面、用于管理用户权限的 基于角色的访问控制RBAC)、内置仪表板用于监控作业状态和结果,以及与其他工具和平台集成的 API。

它最早由 Ansible, Inc.(现在是 Red Hat 的一部分)于 2013 年发布,此后成为自动化 IT 工作流的最受欢迎工具之一。

自首次发布以来,Ansible Tower 经历了多次更新和增强,包括支持更复杂的自动化工作流、与 AWS 和 Azure 等云平台的集成以及改进的可扩展性和性能。Ansible Tower 是由 Red Hat 公司发布的商业产品。与 Ansible Tower 最相似的替代品是Ansible WorXAWX)。

Ansible AWX是 Ansible Tower 的开源替代品,提供与 Tower 类似的许多功能,但具有更大的定制性和灵活性。AWX 于 2017 年首次发布,并迅速成为寻求在不需要商业许可证的情况下大规模实施 Ansible 自动化的组织的热门选择。

Ansible Tower 和 Ansible AWX 之间的主要区别之一是它们的许可模型。Ansible Tower 需要商业许可证,并作为 Red Hat Ansible Automation Platform 的一部分进行销售,而 Ansible AWX 是开源的,可以从 Ansible 官网免费下载。这意味着组织可以在自己的基础设施上部署 AWX,并根据特定需求进行定制,而无需依赖预构建的商业解决方案。

从功能上来看,Ansible Tower 和 Ansible AWX 非常相似,两者都提供基于 Web 的界面用于管理 Ansible 剧本、清单和任务运行,RBAC 用于管理用户权限,并且具有内置的仪表板来监控任务状态和结果。然而,Ansible Tower 提供了一些 Ansible AWX 没有的附加功能,比如与 Red Hat Ansible Automation Platform 的本地集成、先进的分析和报告功能,以及认证模块和集合。

另一个 Ansible Tower 的开源替代品是Ansible Semaphore。与 Tower 类似,它是一个基于 Web 的应用程序,旨在简化 Ansible 剧本和项目的管理。它是一个开源、免费的、易于使用的 Ansible Tower 替代品,允许用户轻松自动化其基础设施任务,而无需大量的编码知识。Ansible Semaphore 的首次发布是在 2016 年,从那时起,它已成为那些希望获得简单而强大的 Web 界面的用户的热门选择,用于管理他们的 Ansible 自动化工作流。

你可以在各自的官方网站上了解更多关于这些替代品的信息:

高级主题

在本节中,我们将展示如何处理高级 Ansible 功能和调试技术,以及如何自动检查剧本中可能存在的错误。

调试

为了调试 Ansible 剧本运行中的问题,通常将详细程度提高是有用的,以便获得关于 Ansible 正在执行的操作的更详细输出。Ansible 有四个详细程度:-v-vv-vvv-vvvv。你添加的v越多,输出越详细。

默认情况下,Ansible 以-v运行,提供有关执行任务的基本信息。然而,如果你在剧本中遇到问题,增加详细程度可能会有帮助,以便获得更详细的输出。例如,使用-vv将提供有关正在执行的剧本、角色和任务的额外信息,而使用-vvv还会显示 Ansible 跳过的任务。

要提高 Ansible 剧本运行的详细程度,只需在ansible-playbook命令中添加一个或多个-v选项。以下是一个示例:

admin@myhome:~$ ansible-playbook playbook.yml -vv

这将以-vv详细程度运行剧本。如果你需要更详细的输出,可以添加额外的-v选项。你可以在这里看到一个示例:

admin@myhome:~$ ansible-playbook playbook.yml -vvv

除了使用-v选项外,你还可以通过在ansible.cfg文件中添加以下行来设置详细程度:

[defaults]
verbosity = 2

这将为所有ansible-playbook命令设置-vv的详细程度。你可以将值更改为34,以进一步增加详细程度。

如果你需要在详细模式中添加一些自定义通信(例如打印变量),你可以通过使用debug任务来实现。

这是一个示例剧本,演示了如何在-vv debug模式下打印一个变量:

---
- hosts: all
  gather_facts: true
  vars:
    my_variable: "Hello, World!"
  tasks:
    - name: Print variable in debug mode
      debug:
        msg: "{{ my_variable }}"
      verbosity: 2

在这个剧本中,我们定义了一个my_variable变量,存储了字符串"Hello, World!"。然后,我们使用debug模块通过msg参数打印出这个变量的值。

verbosity: 2这一行启用了debug模式。它告诉 Ansible 将详细程度设置为-vv,这样我们就可以看到debug模块的输出。

Ansible 剧本代码检查

Ansible 代码检查是分析和验证 Ansible 代码语法和风格的过程,确保其符合最佳实践和标准。检查的目的是在代码执行之前捕捉潜在错误或问题,从而节省时间和精力。

最常用的 Ansible 代码检查工具是ansible-lint。这是一个开源命令行工具,用于分析 Ansible 剧本和角色,找出潜在问题并提供改进建议。

要运行ansible-lint检查示例剧本,你可以在终端中执行以下命令:

admin@myhome:~$ ansible-lint sample-playbook.yml

假设示例剧本已保存为sample-playbook.yml,并位于当前工作目录中,其内容如下:

---
- name: (Debian/Ubuntu) {{ (nginx_setup == 'uninstall') | ternary('Remove', 'Configure') }} NGINX repository
  ansible.builtin.apt_repository:
    filename: nginx
    repo: "{{ item }}"
    update_cache: true
    mode: "0644"
    state: "{{ (nginx_state == 'uninstall') | ternary('absent', 'present') }}"
  loop: "{{ nginx_repository | default(nginx_default_repository_debian) }}"
  when: nginx_manage_repo | bool
- name: (Debian/Ubuntu) {{ (nginx_setup == 'uninstall') | ternary('Unpin', 'Pin') }} NGINX repository
  ansible.builtin.blockinfile:
    path: /etc/apt/preferences.d/99nginx
    create: true
    block: |
      Package: *
      Pin: origin nginx.org
      Pin: release o=nginx
      Pin-Priority: 900
    mode: "0644"
    state: "{{ (nginx_state == 'uninstall') | ternary('absent', 'present') }}"
  when: nginx_repository is not defined
- name: (Debian/Ubuntu) {{ nginx_setup | capitalize }} NGINX
  ansible.builtin.apt:
    name: nginx{{ nginx_version | default('') }}
    state: "{{ nginx_state }}"
    update_cache: true
    allow_downgrade: "{{ omit if ansible_version['full'] is version('2.12', '<') else true }}"
  ignore_errors: "{{ ansible_check_mode }}"
  notify: (Handler) Run NGINX

请注意,ansible-lint的输出可能会根据所使用的 Ansible 版本和ansible-lint版本以及启用的特定规则而有所不同。

作为示例,以下是运行ansible-lint检查提供的示例剧本后的输出:

[WARNING]: empty path for ansible.builtin.blockinfile, path set to ''
[WARNING]: error loading version info from /usr/lib/python3.10/site-packages/ansible/modules/system/setup.py: __version__ = '2.10.7'
[WARNING]: 3.0.0 includes an experimental document syntax parser that could result in parsing errors for documents that used the previous parser. Use `--syntax-check` to verify new documents before use or consider setting `document_start_marker` to avoid using the experimental parser.
sample-playbook.yml:1:1: ELL0011: Trailing whitespace
sample-playbook.yml:7:1: ELL0011: Trailing whitespace
sample-playbook.yml:9:1: ELL0011: Trailing whitespace
sample-playbook.yml:17:1: ELL0011: Trailing whitespace
sample-playbook.yml:22:1: ELL0011: Trailing whitespace
sample-playbook.yml:26:1: ELL0011: Trailing whitespace
sample-playbook.yml:29:1: ELL0011: Trailing whitespace
sample-playbook.yml:35:1: ELL0011: Trailing whitespace
sample-playbook.yml:38:1: ELL0011: Trailing whitespace
sample-playbook.yml:41:1: ELL0011: Trailing whitespace
sample-playbook.yml:44:1: ELL0011: Trailing whitespace
sample-playbook.yml:47:1: ELL0011: Trailing whitespace
sample-playbook.yml:50:1: ELL0011: Trailing whitespace
sample-playbook.yml:53:1: ELL0011: Trailing whitespace

输出指示 Playbook 的多个行存在行尾空白,违反了 ansible-lintELL0011 规则。关于空路径和版本信息加载的警告消息并不是关键问题,可以安全地忽略。

要修复行尾空白问题,只需删除每个受影响行末尾的额外空格。问题修复后,可以重新运行 ansible-lint 来确保 Playbook 没有进一步的问题。

加速 SSH 连接

Ansible 是一款开源自动化工具,广泛用于部署和管理 IT 基础设施。Ansible 的一个关键特性是能够使用 SSH 与远程服务器进行安全通信。然而,为每个单独任务使用 SSH 可能会耗费时间并影响性能,特别是在处理大量服务器时。为了解决这个问题,Ansible 支持SSH 多路复用,允许多个 SSH 连接共享单个 TCP 连接。

SSH 多路复用通过重用现有的 SSH 连接而不是为每个任务创建新连接来工作。当 Ansible 建立与远程服务器的 SSH 连接时,它打开一个 TCP 套接字并为该连接创建一个控制套接字。控制套接字是用于管理 SSH 连接的特殊套接字。当请求到同一主机的另一个 SSH 连接时,Ansible 会检查是否已存在该连接的控制套接字。如果存在,则 Ansible 会重用现有的控制套接字,并在同一 SSH 连接内为新任务创建一个新通道。

SSH 多路复用的好处在于通过减少 Ansible 需要建立的 SSH 连接数量来节省时间和资源。此外,它可以通过减少为每个任务创建和拆除 SSH 连接的开销来提高 Ansible 的性能。

要在 Ansible 中启用 SSH 多路复用,需要在 SSH 客户端配置文件中配置 ControlMasterControlPath 选项。ControlMaster 选项启用 SSH 多路复用的使用,而 ControlPath 选项指定控制套接字的位置。默认情况下,Ansible 使用 ~/.ansible/cp 目录存储控制套接字。您还可以通过设置 ControlPersist 选项来配置可以同时复用的最大 SSH 连接数。

要自定义 SSH 多路复用配置,可以通过在 ~/.ansible.cfg 中添加 [ssh_connection] 部分来将 SSH 选项放入默认的 Ansible 配置文件中,如下所示:

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=3600
control_path = ~/.ssh/multiplexing/ansible-ssh-%%r@%%h:%%p

在添加此配置后,确保还创建了 ~/.ssh/multiplexing 目录。

ControlMaster=auto 选项会自动创建一个主会话,如果已经存在主会话,则后续会话将自动进行复用。设置 ControlPersist=3600 将会在后台保持主连接打开,以接受 3600 秒(1 小时)内的新连接。

动态清单

在 AWS 等云环境中,服务器可以根据需求动态创建和终止。手动管理这些服务器可能是一个艰巨的任务,这就是为什么自动化工具,如 Ansible,非常重要。Ansible 的一个关键特性是动态清单,它使其非常适合云环境。

动态清单 是 Ansible 的一个功能,它支持在云环境中自动发现主机(服务器)和组(标签)。在 AWS 中,Ansible 可以使用 弹性计算云EC2)清单插件来查询 AWS API,获取有关 EC2 实例和组的信息。

要在 AWS 中使用动态清单,你需要在 Ansible 配置中配置 EC2 清单插件。

amazon.aws.aws_ec2 库插件是一个官方的 Ansible 插件,它支持 Amazon EC2 实例的动态清单。要使用此插件,你需要按照接下来的步骤操作。

根据你使用的 Ansible 版本,以及是否安装了完整的 Ansible(而不仅仅是 Ansible Core),你可能需要使用 Ansible Galaxy 安装 AWS 集合插件,方法如下:

admin@myhome:~$ ansible-galaxy collection install amazon.aws

在你的 Ansible 控制节点上安装 boto3botocore 库。你可以使用 pip 包管理器安装它们,方法如下:

admin@myhome:~$ pip install boto3 botocore

创建一个具有必要权限以访问 EC2 实例和组的身份和访问管理IAM)用户。你可以使用 AWS 管理控制台或 AWS CLI 创建 IAM 用户。确保保存 IAM 用户的访问密钥和秘密访问密钥。

在你的 Ansible 控制节点上创建一个 AWS 凭证文件(~/.aws/credentials),并添加 IAM 用户的访问密钥和秘密访问密钥,如下所示:

[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY

在你的项目目录中创建一个 Ansible 清单文件(inventory.yml),并配置它使用 amazon.aws.aws_ec2 插件,方法如下:

plugin: amazon.aws.aws_ec2
regions:
  - eu-central-1
filters:
  tag:Environment:
    - webserver
    - frontend

下面是配置选项的简要说明:

  • plugin:指定要使用的清单插件

  • regions:指定要搜索实例的 AWS 区域

  • filters:允许你按标签筛选 EC2 实例

通过运行以下命令测试清单:

admin@myhome:~$ ansible-inventory -i inventory.yml --list

该命令应输出一个 JSON 对象,列出指定区域内所有 EC2 实例,并按 Ansible 标签分组。

这样,你就不需要在每次运行 Ansible 剧本时都更新清单文件,以确保拥有最新的服务器列表。

总结

在本章中,我们介绍了 Ansible CaC 工具。我们已经解释并演示了如何将配置从部落知识和文档(以及描述使系统达到目标状态所需的步骤)转移到能够基于定义良好的语法实现该配置的工具,这为你的组织带来了诸如可重复性、能够并行运行多个配置、自动化测试和执行等好处。

在下一章中,我们将向你介绍基础设施即代码IaC)。

进一步阅读

  • 掌握 Ansible(第四版) 由 James Freeman 和 Jesse Keating 编写

  • Ansible Playbook 基础 由 Gourav Shah 编写

  • Ansible 实际应用自动化 由 Gineesh Madapparambath 编写

第十二章:利用基础设施即代码

在当今的数字化环境中,管理和部署基础设施是一个复杂且耗时的过程。传统上,基础设施部署涉及手动配置每个服务器、网络和存储设备。这个过程不仅耗时,而且容易出错和产生不一致性。基础设施即代码IaC)解决方案提供了一种自动化的方式来管理和部署基础设施。IaC 解决方案使开发人员能够将基础设施视为代码,从而以与代码相同的方式定义、管理和配置基础设施。

在本章中,我们将探索 IaC 解决方案,重点关注 Terraform。从 2.01.1。BSL 允许你自由使用 Terraform,并可以访问其源代码,因此对最终用户没有变化。

使用 Terraform,开发人员可以编写代码来定义他们的基础设施需求,而 Terraform 将负责所需资源的配置和部署。近年来,由于其简便性、灵活性以及对多个云服务提供商的支持,Terraform 变得越来越流行。在接下来的章节中,我们将讨论 Terraform 的主要特性和优势,以及如何使用它在流行的云服务提供商上配置基础设施。

本章我们将学习以下内容:

  • 什么是 IaC?

  • IaC 与配置即代码的区别

  • 值得了解的 IaC 项目

  • Terraform

  • 深入了解 HCL

  • 使用 AWS 的 Terraform 示例

技术要求

本章中,您需要一台能够运行 Terraform 的系统。Terraform 是一个用 Go 编程语言编写的单一二进制程序。其安装过程简单易懂,并在 HashiCorp Terraform 项目页面上有详细说明(developer.hashicorp.com/terraform/downloads)。HashiCorp 是 Terraform 以及其他云管理工具的背后公司,这些工具已成为 DevOps 领域的事实标准。您还需要一个 AWS 账户。AWS 提供有限时间的免费服务。我们使用的服务在写本书时具有免费层。在运行示例之前,请查阅 AWS 免费层清单,以避免不必要的费用。

什么是 IaC?

基础设施即代码(IaC)是一种软件开发实践,它通过代码定义和管理基础设施。实质上,这意味着将基础设施视为软件,并通过相同的流程和工具进行管理。IaC 解决方案使开发人员能够通过代码定义、配置和管理基础设施,而无需手动配置服务器、网络和存储设备。这种基础设施管理方法高度自动化、可扩展且高效,能够帮助组织减少部署时间,提高一致性和可靠性。

IaC 解决方案有不同的形式,包括配置管理工具、配置工具和云编排工具。配置管理工具,如AnsibleChef,用于管理单个服务器或服务器组的配置。配置工具,如 Terraform 和CloudFormation,用于配置和管理基础设施资源。云编排工具,如KubernetesOpenShift,用于管理容器化的应用程序及其相关基础设施。无论使用何种具体工具,IaC 解决方案都能提供多个好处,包括可重复性和一致性。

基础设施即代码与配置即代码

你可能会想,难道我们在第十一章中已经讲过这个内容了吗?我们谈到的是 Ansible?答案是否定的,我们并没有讲过。基础设施即代码(IaC)和配置即代码CaC)之间有着非常明显的区别。IaC 工具关注的正是这一点:基础设施。这意味着网络、DNS 名称、路由以及服务器(虚拟机或物理机),一直到操作系统的安装。而 CaC 关注的是操作系统内部的内容。人们常常试图用一个工具做所有事情,因此你会看到 Ansible 有一些模块可以配置交换机和路由器,但该工具最擅长的还是它原本设计的用途。如果你把这两者混在一起,虽然不会出什么大问题,但你的工作会变得更加困难。

值得了解的 IaC 项目

自从公共云的崛起,尤其是 AWS 之后,对于一种可重复和可靠的方式来设置基础设施并配置云服务的需求也开始增长。从那时起,许多工具应运而生,且越来越多的工具正在开发中。在本节中,我们将回顾一些最流行和最具创新性的工具。

AWS CloudFormation

AWS CloudFormation 是由Amazon Web ServicesAWS)提供的一个流行的 IaC 工具,用于自动化 AWS 资源的配置。它首次发布于 2011 年,并迅速成为云端管理基础设施的广泛使用工具。

CloudFormation 允许你使用声明性语言(如 YAML 或 JSON)来定义基础设施,然后根据这些定义创建、更新或删除资源堆栈。这不仅能实现一致且可重复的基础设施部署,还能方便地进行回滚和版本控制。不过,并非所有情况都是一帆风顺的——有时,你可能会在进行未经测试的更改后卡在回滚循环中。例如,假设你正在更改 AWS Lambda 的环境版本。不幸的是,由于你当前使用的版本不再受支持,因此更改失败。现在它被卡在回滚状态,显示为UPDATE_ROLLBACK_FAILED。你需要手动解决这个问题,因为没有自动化的方式来处理这一问题。

CloudFormation 与其他 AWS 服务(如 AWS 身份与访问管理IAM)、AWS 弹性负载均衡ELB)和 AWS 自动扩展)集成,轻松实现复杂架构的创建。

下面是一个用 YAML 编写的 CloudFormation 堆栈示例,它在默认 VPC 的公共子网中创建一个名为t4g.small的 EC2 实例:

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Default: admin-key
  InstanceType:
    Type: String
    Default: t4g.small
  SSHCIDR:
    Type: String
    MinLength: 9
    MaxLength: 18
    Default: 0.0.0.0/0
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
  LatestAmiId:
    Type:  'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/canonical/ubuntu/server/jammy/stable/current/amd6/hvm/ebs-gp2/ami-id'
Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref 'InstanceType'
      SecurityGroups: [!Ref 'InstanceSecurityGroup']
      KeyName: !Ref 'KeyName'
      ImageId: !Ref 'LatestAmiId'
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable SSH access
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: !Ref 'SSHCIDR'
Outputs:
  InstanceId:
    Description: InstanceId of the newly created EC2 instance
    Value: !Ref 'EC2Instance'
  PublicDNS:
    Description: Public DNSName of the newly created EC2 instance
    Value: !GetAtt [EC2Instance, PublicDnsName]
  PublicIP:
    Description: Public IP address of the newly created EC2 instance
    Value: !GetAtt [EC2Instance, PublicIp]

在这个堆栈中,我们创建了两个资源:一个 EC2 实例和一个附加到该实例的安全组。CloudFormation 堆栈可以获取四个参数:

  • KeyName:已经在 AWS EC2 服务中创建的 SSH 密钥名称。默认值为admin-key

  • InstanceType:我们要启动的实例类型。默认值为t4g.small

  • SSHCIDR22。默认值为0.0.0.0/0。在这里,我们验证提供的输入是否符合正则表达式,并检查变量的长度。

  • LatestAmiId:用于启动 EC2 实例的基础系统 AMI ID。默认值为 Ubuntu Linux 22.04的最新 AMI。

接下来是Resources部分。在这里,EC2 实例是使用AWS::EC2::Instance资源类型创建的,安全组是使用AWS::EC2::SecurityGroup资源创建的。

最后一部分称为Outputs;在这里,我们可以显示已创建资源的 ID 和其他属性。在这里,我们公开实例 ID、其公共 DNS 名称和公共 IP 地址。

可以将这些输出值作为另一个 CloudFormation 堆栈的输入,这将使 CloudFormation 代码的 YAML 文件大大减小,并更易于维护。

AWS 云开发工具包

AWS 云开发工具包CDK)是一个开源软件开发框架,用于在代码中定义云基础设施。通过 CDK,开发人员可以使用熟悉的编程语言,如 TypeScript、Python、Java、C#和 JavaScript,来创建和管理 AWS 上的云资源。

AWS CDK 于 2018 年 7 月首次作为开源项目发布。它旨在简化构建和部署云基础设施的过程,让开发人员能够使用现有的编程语言技能和工具。通过 CDK,开发人员可以定义基础设施即代码(IaC),并利用版本控制、自动化测试和持续集成/持续部署CI/CD)管道的好处。自发布以来,CDK 已成为在 AWS 上构建基础设施的流行选择,并持续更新和新增功能。

下面是一些 AWS CDK Python 代码的示例,用于创建 EC2 实例:

from aws_cdk import core
import aws_cdk.aws_ec2 as ec2
class MyStack(core.Stack):
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
        # VPC
        vpc = ec2.Vpc(self, "VPC",
            nat_gateways=0,
            subnet_configuration=[ec2.SubnetConfiguration(name="public",subnet_type=ec2.SubnetType.PUBLIC)]
            )
        # Get AMI
        amzn_linux = ec2.MachineImage.latest_amazon_linux(
            generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
            edition=ec2.AmazonLinuxEdition.STANDARD,
            virtualization=ec2.AmazonLinuxVirt.HVM,
            storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE
            )
        # Create an EC2 instance
        instance = ec2.Instance(self, "Instance",
            instance_type=ec2.InstanceType("t4g.small"),
            machine_image=amzn_linux,
            vpc = vpc
        )

这段代码创建了一个新的 VPC 和一个 EC2 实例,实例类型为t4g.small,操作系统为安装了 Amazon Linux 的 EC2 实例。请注意,运行此代码之前需要先安装并配置 AWS CDK。

Terraform

Terraform 是一个流行的开源工具,用于基础设施自动化,特别是用于创建、管理和配置云资源。它使开发者能够定义基础设施即代码(IaC),并自动化在多个云平台上部署基础设施的过程。通过 Terraform,用户可以编写声明性配置文件,这些文件使用简单直观的语言,可以进行版本管理、共享和重用。这种基础设施管理方式确保了系统的一致性和可扩展性,并减少了手动错误的风险。Terraform 支持多种云服务提供商,包括 AWS、Azure、Google Cloud 等,这使得它成为拥有复杂云基础设施需求的组织的热门选择。

Terraform 由 HashiCorp 创建,HashiCorp 是由 Mitchell Hashimoto 和 Armon Dadgar 于 2012 年创立的公司。该公司以开发流行的基础设施自动化开源工具而闻名,包括 Vagrant、Consul、Nomad 和 Vault。Terraform 于 2014 年 7 月首次发布,随后成为业内最广泛采用的 IaC 工具之一。HashiCorp 继续维护和开发 Terraform,并定期发布更新,解决新的云服务提供商特性、安全漏洞和社区反馈。该工具拥有一个庞大且活跃的贡献者社区,进一步增强了其功能并支持新的使用场景。

Terraform 也是本章的一个主要话题,稍后我们将在 Terraform 部分深入研究其代码和内部实现。

Terraform 云开发工具包

Terraform 云开发工具包CDKTF)是一个开源软件开发框架,用于以代码形式定义云基础设施。它允许用户使用熟悉的编程语言(如 TypeScript、JavaScript、Python 和 C#)来定义基础设施。这为开发者提供了更大的灵活性和控制力,因为他们可以利用现有的编程技能和工具来定义复杂的基础设施。CDKTF 于 2019 年首次发布,是 AWS 和 HashiCorp 的合作成果。从那时起,它作为一个强大的工具,在使用 Terraform 定义和部署基础设施方面获得了广泛的关注。

CDKTF 支持多种编程语言,使开发者可以轻松使用自己熟悉的语言。它使用构造函数,这些构造函数是可重用的构建块,代表 AWS 资源,用来创建基础设施。用户可以为每个要创建的资源定义构造函数,并将其组合形成更复杂的基础设施。这使得用户能够以模块化和可重用的方式定义基础设施,从而简化了创建和维护基础设施的过程。

以下是使用 Python 在 AWS 中创建 EC2 实例的 CDKTF 示例代码:

from constructs import Construct
from cdktf import App, TerraformStack
from imports.aws import AwsProvider, Instance, SecurityGroup
class MyStack(TerraformStack):
    def __init__(self, scope: Construct, ns: str):
        super().__init__(scope, ns)
        # Configure AWS provider
        aws_provider = AwsProvider(self, 'aws', region='us-east-1')
        # Create a security group
        security_group = SecurityGroup(self, 'web-server-sg',
            name='web-server-sg',
            ingress=[
                {
                    'from_port': 22,
                    'to_port': 22,
                    'protocol': 'tcp',
                    'cidr_blocks': ['0.0.0.0/0'],
                },
                {
                    'from_port': 80,
                    'to_port': 80,
                    'protocol': 'tcp',
                    'cidr_blocks': ['0.0.0.0/0'],
                },
            ],
        )
        # Create an EC2 instance
        Instance(self, 'web-server',
            ami='ami-0c55b159cbfafe1f0',
            instance_type='t4g.small',
            security_groups=[security_group.id],
            user_data="""
                #!/bin/bash
                echo "Hello, DevOps People!" > index.xhtml
                nohup python -m SimpleHTTPServer 80 &
            """
        )
app = App()
MyStack(app, "my-stack")
app.synth()

AppTerraformStack 类从 cdktf 包导入,而 AWS 资源则从 imports.aws 模块导入。上面的代码创建了一个带有安全组的 EC2 实例,并带有一个基本的用户数据脚本,用于启动一个简单的 HTTP 服务器。生成的基础设施可以使用 cdktf deploy 命令进行部署,该命令会生成 Terraform 配置文件并执行 Terraform CLI。

你可以在 developer.hashicorp.com/terraform/cdktf 阅读更多关于 CDKTF 的信息。

Pulumi

Pulumi 是一个开源的 IaC 工具,允许开发人员使用熟悉的编程语言构建、部署和管理云基础设施。与依赖声明性语言如 YAML 或 JSON 的传统 IaC 工具不同,Pulumi 使用真实的编程语言,如 Python、TypeScript、Go 和 .NET,来定义和管理基础设施。这使得开发人员能够利用他们现有的技能和经验,使用构建应用程序时所用的相同工具和流程来创建基础设施。使用 Pulumi,开发人员可以像进行代码更改一样创建、测试和部署基础设施更改——即通过使用版本控制和 CI/CD 工具。

Pulumi 的首次发布是在 2018 年 5 月,旨在简化管理云基础设施的过程。Pulumi 由 Joe Duffy 创立,他是前微软工程师,曾参与 .NET 运行时和编译器的开发。Duffy 看到了一个机会,利用编程语言来管理基础设施,提供了一种比传统的 IaC 工具更灵活、更强大的方法。自发布以来,Pulumi 在开发者中获得了广泛的关注,尤其是在云原生环境中工作或使用多个云服务提供商的开发者。

Pulumi 支持多种编程语言,包括 Python、TypeScript、Go、.NET 和 Node.js。Pulumi 还提供了一套丰富的库和工具,用于处理云资源,包括对 AWS、Azure、Google Cloud 和 Kubernetes 等流行云服务提供商的支持。此外,Pulumi 还与流行的 CI/CD 工具集成,如 Jenkins、CircleCI 和 GitLab,使开发人员能够轻松地将基础设施更改融入现有的工作流程中。

以下是一个 Pulumi 使用 Python 创建 AWS EC2 实例的示例代码:

import pulumi
from pulumi_aws import ec2
# Create a new security group for the EC2 instance
web_server_sg = ec2.SecurityGroup('web-server-sg',
    ingress=[
        ec2.SecurityGroupIngressArgs(
            protocol='tcp',
            from_port=22,
            to_port=22,
            cidr_blocks=['0.0.0.0/0'],
        ),
    ],
)
# Create the EC2 instance
web_server = ec2.Instance('web-server',
    instance_type='t4g.small',
    ami='ami-06dd92ecc74fdfb36', # Ubuntu 22.04 LTS
    security_groups=[web_server_sg.name],
    tags={
        'Name': 'web-server',
        'Environment': 'production',
    },
)
# Export the instance public IP address
pulumi.export('public_ip', web_server.public_ip)

这段代码定义了一个 AWS 安全组,允许通过端口 22(SSH)进行入站流量,然后创建一个 t4g.small 类型的 EC2 实例,使用 Ubuntu 22.04 LTS AMI。该实例与我们之前创建的安全组关联,并带有名称和环境标签。最后,实例的公共 IP 地址作为 Pulumi 堆栈输出被导出,可以供堆栈中的其他资源使用或由用户访问。

在这一部分中,我们介绍了几种 IaC 解决方案:CDK、CDKTF、Terraform 和 Pulumi。它们中的一些针对特定的云提供商,而另一些则允许我们配置不同的云环境。

在接下来的部分中,我们将回到 Terraform,深入探讨它的工作原理,并学习如何在实践中使用基础设施即代码(IaC)。这将为我们快速理解其他解决方案奠定基础,包括我们之前提到的 CDK。

Terraform

在这一部分中,我们将介绍 Terraform,这是目前最广泛使用的 IaC 解决方案之一。

Terraform 是一个由 HashiCorp 开发的 IaC 工具。使用它的原理类似于使用 Ansible 配置系统:基础设施配置保存在文本文件中。它们不像 Ansible 那样是 YAML 格式的,而是采用 HashiCorp 开发的特殊配置语言:HashiCorp 配置语言HCL)。文本文件容易版本化,这意味着基础设施变更可以存储在如 Git 之类的版本控制系统中。

Terraform 执行的操作比你在 Ansible 中看到的更复杂。一个简单的 HCL 语句可能意味着设置一堆虚拟服务器以及它们之间的路由。因此,尽管 Terraform 和 Ansible 一样是声明式的,但它比其他工具更高层次。此外,与 Ansible 相反,Terraform 是状态感知的。Ansible 有一个待执行操作的列表,每次运行时,它会检查哪些操作已经执行。而 Terraform 则记录系统最后一次的状态,并确保每次执行时系统都会与代码中的状态一致。

为了实现这一点,Terraform 创建并维护一个状态文件。它是一个扩展名为.tfstate的文本文件,记录了工具所知道的基础设施的最后已知状态。状态文件在内部是有版本控制的;Terraform 维护一个特殊的计数器,允许它知道文件是否是最新的。状态文件对于 Terraform 正常工作至关重要。你绝不能损坏或丢失状态文件。如果丢失了该文件,Terraform 会尝试创建已经存在的资源,并可能删除不该删除的内容。

有几种方法可以确保状态文件的安全。其中一种方法是将其存储在经过适当配置的对象存储中(例如 S3),这样状态文件就无法被删除。为了增强安全性,您可以确保该文件是有版本控制的,这意味着存储将保留文件的旧副本以供以后使用。

有一件关于.tfstate的重要事情需要注意:它将包含与您的基础设施相关的所有信息,以及明文密码、登录凭证、访问密钥等。保护该文件的隐私至关重要,并且应将其排除在版本控制系统的提交之外(在 Git 中,可以将其添加到.gitignore文件中)。

代码在以.tf扩展名的文本文件中开发。与 Ansible 不同,您在文件中放置指令的顺序并不重要。在执行之前,Terraform 会分析当前目录下的所有.tf文件,创建配置元素之间的依赖关系图,并正确地排列它们。通常情况下,代码会被分解成更小的.tf文件,这些文件将相关的配置指令分组。然而,您也可以将所有代码保存在一个巨大的文件中,尽管它很快会变得庞大,不利于使用。

尽管您可以自由命名文件,只要它们的扩展名是.tf,但还是有一些最佳实践需要遵守:

  • main.tf:这是您开发配置代码的主要文件。它将包含资源、模块和其他重要信息。

  • variables.tf:此文件将包含您希望在main.tf文件中使用的所有变量的声明。

  • outputs.tf:如果您的main.tf文件中的资源产生任何输出,它们将在此处声明。

  • versions.tf:此文件声明 Terraform 二进制文件本身和提供者所需的版本。最好声明已知能够正常工作的最低版本。

  • providers.tf:如果任何提供者需要额外的配置,您应该将它们放在这个文件中。

  • backend.tf:此文件包含 Terraform 应将状态文件存储的位置的配置。状态文件是 Terraform 中基础设施即代码(IaC)的一个重要组成部分。我们将在Terraform 状态小节中更深入地讨论这一点。

在 Ansible 中,重活是由名为模块的 Python 程序完成的。而在 Terraform 中,这项工作是由提供者(Providers)完成的。提供者是小型的 Golang 程序,它们消耗由 Terraform 准备的配置计划,通过这些服务的 API 连接云端、设备等服务,并执行配置。你可以把它们看作是插件。提供者提供一组资源类型,并最终提供所需的数据源,以便为提供者所连接的 API 编写配置。官方解释是,提供者“是上游 API 的逻辑抽象”。提供者通常发布在 Terraform Registry 上,这是由 HashiCorp 维护的公共插件库。你可以使用其他注册表,但发布在 Terraform Registry 上的提供者通常已通过测试并且被信任能够正常工作。每个发布在该注册表上的提供者都有详细的文档和良好注释的示例。每当你使用一个新提供者时,应该访问该注册表(registry.terraform.io/)。一个例子是 AWS 提供者。这个提供者公开了大量资源,你可以用来与 AWS 服务交互,以配置和部署它们。记住:配置仅限于基础设施。你可以将 Terraform(例如,用来配置虚拟机)与 Ansible(用于在虚拟机中安装软件并进行配置)结合,体验完整的工作流。

让我们看一个来自 Terraform Registry AWS 提供者文档的示例(registry.terraform.io/providers/hashicorp/aws/latest/docs):

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws" version = "~> 4.0"
    }
  }
}
# Configure the AWS Provider
provider "aws" {
  region = "us-east-1"
}
# Create a VPC
resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
}

在上述代码片段中,我们声明需要从 Terraform Registry 下载 AWS 提供者。它的版本应该不低于 4.0。然后,我们配置要使用的区域(us-east-1)。最后,我们创建一个虚拟私有网络(虚拟私有云VPC))并为其声明一个 IP 地址块。

单个目录中的 .tf 文件集合称为 模块。如果你在包含模块文件的目录中运行 Terraform 命令,那么这个目录就被称为 根模块

Terraform 模块

Terraform 的一个关键概念是模块。Terraform 模块是一个资源集合及其依赖关系,用于构建基础设施的特定组件。模块提供了一种组织代码的方式,并使其能够在多个项目之间复用。模块可以与其他用户和团队共享,甚至发布到像 Terraform Registry 这样的公共注册表中。

在使用 Terraform 时,确保使用兼容的基础设施提供者版本非常重要。提供者负责管理您云环境中的资源,不同版本可能具有不同的功能或行为。为了避免基础设施发生意外变化,您可以在 Terraform 配置中固定您使用的提供者版本。通过在提供者块中指定版本约束,使用 Terraform 版本约束语法,您可以实现这一点。当您运行 Terraform 时,它将下载并使用指定版本的提供者,确保您的基础设施保持一致和可预测。

这是一个示例versions.tf文件,它将 AWS 提供者固定到最新版本,并要求至少版本 1.0.0 的 Terraform:

terraform {
  required_providers {
    aws = ">= 3.0.0"
  }
  required_version = ">= 1.0.0"
}

在这个示例中,我们使用required_providers块来指定我们至少需要版本 3.0.0 的 AWS 提供者。通过使用>=操作符,我们允许 Terraform 使用任何版本的提供者,只要它等于或大于 3.0.0,包括最新版本。

当我们运行terraform init时,Terraform 将自动下载并使用提供者的最新版本。此命令还会更新或下载您可能在主模块(或根模块)中使用的其他模块。然而,使用大量依赖其他模块的模块是不推荐的,因为这可能会导致依赖冲突(例如,一些旧模块可能需要 AWS 提供者版本 1.23,而根模块需要版本 3.0 或更高)。我们将在本章的Terraform CLI小节中再次回到命令行界面(CLI)。

要引用另一个模块,您可以使用module代码块。假设我们在根模块相对路径./module/aws_ec2目录中有一个简单的模块。aws_ec2模块需要传入amisubnetvpcsecurity_group变量:

module "aws_ec2_instance" {
  source = "./modules/aws_ec2"
  ami            = "ami-06dd92ecc74fdfb36"
  subnet_id      = "subnet-12345678"
  vpc_id         = "vpc-12345678"
  security_group = "sg-12345678"
}

如果一个模块公开了某些输出(您可以将其用作资源或其他模块的输入),您可以通过module.NAME.OUTPUT_NAME来引用它们。在这种情况下,我们可以公开 EC2 实例的 ID,您可以通过名称module.aws_ec2_instance.instance_id来引用它。

除了使用本地路径外,还有几种其他方法可以指定 Terraform 中引用模块时的源参数:

  • 该模块可以存储在 Git 仓库中并检索:

    module "example" {
    
      source = "git::https://github.com/example-org/example-module.git"
    
    }
    

    使用 Git 仓库时,您还可以引用提交 ID、分支或标签:

    module "example" {
    
      source = "git::https://github.com/example-org/example-module.git?ref=branch_name"
    
    }
    

    对于私有仓库,您需要使用 SSH 而不是 HTTPS 来将其克隆到本地:

    module "example" {
    
      source = "git::ssh://github.com/example-org/example-module.git?ref=branch_name"
    
    }
    
  • 该模块可以发布并从 Terraform 注册表中检索:

    module "example" {
    
      source = "hashicorp/example-module/aws"
    
    }
    

    在这种情况下,您可以使用version属性指定模块版本,如下所示:

    module "example" {
    
      source = "hashicorp/example-module/aws"
    
      version = "1.0.0"
    
    }
    
  • 该模块可以存储在 S3 桶中并检索:

    module "example" {
    
      source = "s3::https://s3-eu-cental-1.amazonaws.com/example-bucket/example-module.zip"
    
    }
    

您可以在官方文档中找到其他可能的来源:developer.hashicorp.com/terraform/language/modules/sources

Terraform 状态

Terraform 的一个基本概念是状态文件。它是一个 JSON 文件,描述了你基础设施的当前状态。这个文件用于跟踪 Terraform 已创建、更新或删除的资源,并且还存储每个资源的配置。

状态文件的目的是使 Terraform 能够一致可靠地管理你的基础设施。通过跟踪 Terraform 已创建或修改的资源,状态文件确保 Terraform 的后续运行能够了解基础设施的当前状态,并根据需要进行更改。如果没有状态文件,Terraform 将无法知道当前部署了哪些资源,并且无法做出关于如何进行后续更改的明智决策。

状态文件还被用作 Terraform 的planapply操作的事实来源。当你运行terraform planterraform apply时,Terraform 将当前基础设施的状态与 Terraform 代码中定义的目标状态进行比较。状态文件用于确定需要进行哪些更改,以使你的基础设施达到所需状态。总体而言,状态文件是 Terraform 基础设施管理功能的关键组成部分,确保 Terraform 能够保证基础设施的一致性和可靠性。

虽然 Terraform 状态文件是该工具的关键组成部分,但使用它也存在一些缺点和挑战。

状态文件是一个集中式文件,用于存储有关基础设施的信息。虽然这很方便,但在团队协作时也可能会带来问题,特别是当多个用户同时对同一基础设施进行更改时。这可能会导致冲突,使得保持状态文件的最新状态变得具有挑战性。通过使用分布式锁机制可以缓解这一问题。在 AWS 环境中,它实际上只是一个 DynamoDB 表,包含一个状态为01的锁条目。

Terraform 状态的另一个缺点是,状态文件包含有关基础设施的敏感信息,如密码、密钥和 IP 地址。因此,必须保护状态文件以防止未经授权的访问。如果状态文件被泄露,攻击者可能会获得对基础设施或敏感数据的访问权限。在 AWS 内部,状态文件通常保存在一个 S3 桶中,并且需要启用加密并阻止公共访问。

随着时间的推移,状态文件可能会变得庞大且难以管理,尤其是在你管理着大量资源的基础设施时。这可能会使管理和维护状态文件变得具有挑战性,进而导致错误和不一致性。

我们可能遇到的下一个关于状态文件的挑战是,Terraform 状态文件是版本特定的。这意味着你必须使用与创建状态文件时相同版本的 Terraform 来管理该文件。这可能会在升级到新版 Terraform 时带来问题,因为你可能需要将状态文件迁移到新的格式。

最后,Terraform 的状态文件有一些局限性,例如无法管理外部资源或处理资源之间复杂依赖关系的困难。这在处理某些类型的基础设施或应对复杂部署时可能会带来挑战。

状态文件的另一个功能是强制执行 Terraform 管理的资源的配置。如果有人手动进行了更改,你将在下次执行 terraform planterraform apply 时看到这些更改,并且这些更改将被回滚。

考虑到所有这些,Terraform 仍然是最好的解决方案之一,而且大多数这些挑战在规划基础设施时都可以轻松解决。

这是一个示例 backend.tf 文件,配置 Terraform 使用名为 state-files 的 S3 存储桶来存储状态文件,并使用名为 terraform 的 DynamoDB 表进行状态锁定:

terraform {
  backend "s3" {
    bucket         = "state-files"
    key            = "terraform.tfstate"
    region         = "eu-central-1"
    dynamodb_table = "terraform"
  }
}

在此配置中,后端块指定了我们希望使用 s3 后端类型,该类型旨在将状态文件存储在 S3 存储桶中。bucket 参数指定了状态文件应存储的存储桶名称,而 key 参数指定了存储桶中状态文件的名称。

region 参数指定了存储桶所在的 AWS 区域。你应该将其设置为最符合你使用场景的区域。

最后,dynamodb_table 参数指定了将用于状态锁定的 DynamoDB 表的名称。这是 S3 后端的一个重要特性,因为它确保一次只有一个用户可以对基础设施进行更改。

这是 Terraform 状态文件的一个示例:

{
    "version": 3,
    "serial": 1,
    "lineage": "f763e45d-ba6f-9951-3498-cf5927bc35c7",
    "backend": {
        "type": "s3",
        "config": {
            "access_key": null,
            "acl": null,
            "assume_role_policy": null,
            "bucket": "terraform-states",
            "dynamodb_endpoint": null,
            "dynamodb_table": "terraform-state-lock",
            "encrypt": true,
            "endpoint": null,
            "external_id": null,
            "force_path_style": null,
            "iam_endpoint": null,
            "key": "staging/terraform.tfstate",
            "kms_key_id": null,
            "lock_table": null,
            "max_retries": null,
            "profile": null,
            "region": "eu-central-1",
            "role_arn": null,
            "secret_key": null,
            "session_name": null,
            "shared_credentials_file": null,
            "skip_credentials_validation": null,
            "skip_get_ec2_platforms": null,
            "skip_metadata_api_check": null,
            "skip_region_validation": null,
            "skip_requesting_account_id": null,
            "sse_customer_key": null,
            "sts_endpoint": null,
            "token": null,
            "workspace_key_prefix": null
        },
        "hash": 1619020936
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
}

通过使用 S3 后端和 DynamoDB 状态锁定,你可以确保 Terraform 部署在团队环境中是安全且一致的,即使多个用户可能同时对相同基础设施进行更改。

在下一个小节中,我们将讨论如何使用 Terraform CLI 与我们的基础设施和状态文件进行交互。

Terraform CLI

Terraform 的核心是其命令行工具,恰如其分地被称为 terraform。我们在介绍 Terraform 时已链接了安装指南。虽然也有工具可以自动化工作流程,省去了使用 CLI 的必要,但该工具的使用非常简单,并且从与它一起工作中可以获得许多有用的知识。在这一节中,我们将介绍 terraform 命令的最常见选项和工作流程。

初始化工作环境

您将使用的第一个 terraform 子命令是 terraform init。在编写完 main.tf 文件的第一部分(如果您遵循建议的模块结构)后,您将运行 terraform init 来下载所需的插件并创建一些重要的目录和帮助文件。

让我们来看一下之前使用的第一段代码的一部分:

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws" version = "~> 4.0"
    }
  }
}

这段代码告诉 Terraform 需要下载的插件及其最低版本。现在,让我们运行 terraform init 命令:

admin@myhome:~$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v4.58.0...
- Installed hashicorp/aws v4.58.0 (signed by HashiCorp)
Terraform has been successfully initialized!

我们已简化了输出以提高简洁性,但最重要的部分仍然存在。您将看到 Terraform 告诉您已执行了哪些操作。后端是用于存储 .tfstate 文件的存储区域。如果您没有指定存储区域,.tfstate 文件将保存在本地目录中的 terraform.tfstate 文件中。还有一个新的子目录 .terraform,其中安装了所需的插件。最后,存在一个 .terraform.lock.hcl 文件,Terraform 会记录已使用的提供程序版本,以便您可以出于兼容性原因保留这些版本。

terraform init 命令是一个安全命令。您可以根据需要多次运行它;它不会破坏任何内容。

规划更改

接下来要运行的命令是 terraform fmt。此命令将根据现有的最佳实践格式化您的 .tf 文件。使用它可以提高代码的可读性和可维护性,使所有源文件在您将看到的所有 Terraform 项目中遵循相同的格式化策略。在我们的示例上运行 terraform fmt 将产生以下输出:

admin@myhome:~$ terraform fmt
│ Error: Missing attribute separator
│
│   on main.tf line 4, in terraform:
│    3:     aws = {
│    4:       source = "hashicorp/aws" version = "~> 4.0"
│
│ Expected a newline or comma to mark the beginning of the next attribute.

您会注意到 fmt 在我的 main.tf 文件中发现了一个明显的错误。这不仅是一个可读性问题;它还可能在某些提供程序中引入代码解析错误。我将两个属性写在了同一行。将其编辑成如下所示就可以解决问题:

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

通过这个更改,fmt 满意了,我们可以继续进行下一步。

通过使用 terraform plan 命令来构建操作计划。它将您基础设施的最后已知记录状态(terraform.tfstate)与目录中的代码进行比较,并准备出步骤以使其匹配。在我们之前的示例代码上运行 terraform plan 会产生以下输出:

admin@myhome:~$ terraform plan
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

由于我们没有引入任何可以创建资源的代码,Terraform 告知我们没有计划进行任何更改。

然而,这并不太有趣。因此,我们将展示一些将在 AWS 中创建资源的内容。

注意

在遵循此示例之前,请先了解您在 AWS 免费套餐服务中的责任。运行这些示例可能会产生费用,如果发生费用,本文的作者和出版商不对其承担任何责任。

如果你想跟随这些示例,你需要拥有一个 AWS 账户(在写本书时是免费的)。然后,你需要创建一个角色并生成 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY。这样做超出了本章的范围。

我们稍微修改了前面的示例。required_providers 块已被移动到 providers.tf 文件中。我们还在其中添加了另一个提供程序块。文件如下所示:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}
provider "aws" {
  region = "us-west-2"
}

请注意,新的块正在配置一个名为 aws 的新提供程序资源。名称(aws)实际上由我们决定,可以是任何名称。记得给它们起有意义的名字,能帮助你以后理解代码。我们为这个提供程序提供了最低限度的配置,指定了我们的资源将启动的区域。

我们在新创建的空 main.tf 文件中进行实际操作:

resource "aws_instance" "vm_example" {
  ami           = "ami-830c94e3"
  instance_type = "t2.micro"
  tags = {
    Name = "DevOpsGuideTerraformExample"
}

在这里,我们告诉 Terraform 我们想要创建一个新的 aws_instance 类型的资源。我们将其命名为 vm_example。接下来,我们告诉工具使用名为 ami-830c94e3 的虚拟机镜像(AMI)。该实例的类型(它将拥有多少 RAM、多少 CPU 核心、系统驱动器的大小等等)是 t2.micro。最后,我们添加了一个标签,帮助我们识别和查找这个实例。

让我们调用 terraform plan 并应用它:

admin@myhome:~$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # aws_instance.vm_example will be created
  + resource "aws_instance" "vm_example" {
      + ami                                  = "ami-830c94e3"
[...]
      + tags                                 = {
          + "Name" = "DevOpsGuideTerraformExample"
        }
      + tags_all                             = {
          + "Name" = "DevOpsGuideTerraformExample"
        }
      + tenancy                              = (known after apply)
[...]
      + vpc_security_group_ids               = (known after apply)
    }
Plan: 1 to add, 0 to change, 0 to destroy.
────────────────────────────────────────────────────────────────────

注意

你没有使用 -out 选项来保存这个计划,因此 Terraform 无法保证如果你现在运行 terraform apply,它会准确执行这些操作。

我们已经省略了计划中的大量输出。然而,你可以看到它与之前的示例有所不同。Terraform 注意到我们没有具有指定参数的虚拟机(记住,它是与 .tfstate 文件进行比较的)。因此,它将创建一个。我们始终可以在以 Plan 开头的行中看到摘要。在计划中,所有以 +(加号)开头的属性将被创建。所有以 -(减号)开头的属性将被销毁,而所有以 ~(波浪号)开头的属性将被修改。

在更改已创建资源的属性时要小心使用 Terraform。通常情况下,它会将其视为新资源,特别是当你更改名称时。这将导致销毁旧名称的虚拟机,并创建一个新名称的虚拟机。这可能不是你想要的结果。

应用更改

计划通过调用 terraform apply 来实施。如果你的 .tf 文件与 .tfstate 文件不同,这个命令将会对你的环境进行更改。此外,如果你的实际运行基础设施与 .tfstate 文件不同,terraform apply 将尽最大努力使实时基础设施与 Terraform 状态文件重新对齐:

admin@myhome:~$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # aws_instance.vm_example will be created
  + resource "aws_instance" "vm_example" {
      + ami                                  = "ami-830c94e3"
[...]
      + subnet_id                            = (known after apply)
      + tags                                 = {
          + "Name" = "DevOpsGuideTerraformExample"
        }
      + tags_all                             = {
          + "Name" = "DevOpsGuideTerraformExample"
        }
      + tenancy                              = (known after apply)
[...]
      + vpc_security_group_ids               = (known after apply)
    }
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
  Enter a value: yes
aws_instance.vm_example: Creating...
aws_instance.vm_example: Still creating... [10s elapsed]
aws_instance.vm_example: Still creating... [20s elapsed]
aws_instance.vm_example: Still creating... [30s elapsed]
aws_instance.vm_example: Still creating... [40s elapsed]
[...]
aws_instance.vm_example: Still creating... [1m20s elapsed]
aws_instance.vm_example: Creation complete after 1m29s [id=i-0a8bee7070b7129e5]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

再次,出于简洁性,很多输出被省略了。

terraform apply 命令再次创建了一个计划。我们可以通过将 terraform plan 记录到文件中,然后将文件输入到 apply 步骤中来避免这种情况。

有趣的部分是确认步骤,在这个步骤中,Terraform 会要求你输入yes才能继续。然后,它将每 10 秒钟打印一次已执行操作的摘要。经过一段时间与 Terraform 的工作,你通常可以根据操作完成所花费的时间来猜测该操作是否成功。

在 AWS 控制台的实例菜单中,我们可以看到虚拟机已经创建完成:

图 12.1 – 通过 Terraform 创建的新虚拟机实例

图 12.1 – 通过 Terraform 创建的新虚拟机实例

我们可以通过运行terraform destroy来删除我们刚刚创建的所有基础设施。

有趣的一点是,在我们的工作流程中,我们没有告诉 Terraform 它应该解释哪些文件。这是因为,正如之前提到的,Terraform 会读取当前目录下的所有.tf文件,并创建一个正确的执行计划。

如果你有兴趣查看步骤的层次结构,Terraform 提供了terraform graph命令,它会为你打印出该层次结构:

admin@myhome:~$ terraform graph
digraph {
     compound = "true"
     newrank = "true"
     subgraph "root" {
          "[root] aws_instance.vm_example (expand)" [label = "aws_instance.vm_example", shape = "box"]
          "[root] provider[\"registry.terraform.io/hashicorp/aws\"]" [label = "provider[\"registry.terraform.io/hashicorp/aws\"]", shape = "diamond"]
          "[root] aws_instance.vm_example (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]"
          "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.vm_example (expand)"
          "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)"
     }
}

有一些工具可以创建 Terraform 生成的图形的良好可视化表示。

修改 Terraform 状态

有时,需要修改状态文件中的资源。在 Terraform 的旧版本中,这必须手动完成,并且容易出错。幸运的是,Terraform 开发者添加了一些命令行工具,可以帮助我们完成这项工作。

最有用的命令如下:

  • Terraform state rm:该命令从状态中移除资源。当我们手动删除了资源并从 Terraform 代码中移除它,但它仍然存在于状态中时,这个命令非常有用。

  • terraform state mv:该命令用于更改资源的名称。这在我们更改资源名称时很有用,以防止删除并创建一个新的资源,这通常不是我们想要的行为。

  • terraform taint:该命令强制重新创建资源。

导入现有资源

将现有资源导入到 Terraform 中,可以将这些资源纳入到 Terraform 状态中,Terraform 状态是一个由 Terraform 管理的资源快照。

terraform import命令用于将现有资源添加到你的 Terraform 状态中。该命令将现有资源映射到 Terraform 代码中的配置块,从而允许你使用 Terraform 管理该资源。

terraform import命令的语法如下:

terraform import [options] resource_in_code resource_identifier

terraform import命令的两个重要参数如下:

  • resource_in_code:在 Terraform 代码中资源的地址。

  • resource_identifier:你想导入的资源的唯一标识符。

举个例子,假设你有一个现有的 AWS S3 存储桶,ARN 为arn:aws:s3:::devopsy-bucket。要将这个资源导入到 Terraform 状态中,可以运行以下命令:

terraform import aws_s3_bucket.devopsy_bucket arn:aws:s3:::devopsy-bucket

导入资源对于你有现有基础设施并希望使用 Terraform 进行管理时非常有用。当你开始在现有项目中使用 Terraform 或者有一些在 Terraform 之外创建的资源时,通常会遇到这种情况。导入资源可以让你将这些资源纳入 Terraform 管理中,从而将来可以使用 Terraform 对其进行修改。

并非所有资源都可以导入到 Terraform 中。你打算导入的资源必须有一个唯一标识符,Terraform 可以利用它在远程服务中找到该资源。此外,资源还必须得到你在 Terraform 中使用的提供程序的支持。

工作区

Terraform 有一个工作区的概念。工作区类似于状态的版本。工作区允许你为相同的代码存储不同的状态。为了能够使用工作区,必须将状态文件存储在支持工作区的后端中。支持工作区的后端列表非常长,涵盖了大多数流行的云服务提供商。

这些工作区可以通过 .tf 文件中的 ${terraform.workspace} 序列来访问。结合条件表达式,这使得你能够创建不同的环境。例如,您可以根据工作区使用不同的 IP 地址,从而区分测试环境和生产环境。

总是存在一个工作区:default。它无法被删除。工作区操作可以通过 terraform workspace 命令来完成。

我们可以使用 terraform list 命令轻松查看当前有哪些工作区以及哪个是活动工作区:

admin@myhome:~$ terraform workspace list
* default

前面带星号的是当前工作区。如果我们只关心查看当前工作区而不是整个列表,可以运行 terraform show 命令:

admin@myhome:~$ terraform workspace show
default

每个工作区都会有一个状态文件。让我们做个实验:我们将创建一个名为 testing 的新工作区,并对测试工作区应用 Terraform。

首先,我们必须调用 terraform workspace new 来创建工作区:

admin@myhome:~$ terraform workspace new testing
Created and switched to workspace "testing"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

现在,我们必须确认自己确实处于新的工作区中,并使用我们之前的示例在其中运行 terraform apply

admin@myhome:~$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # aws_instance.vm_example will be created
  + resource "aws_instance" "vm_example" {
      + ami                                  = "ami-830c94e3"
[...]
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
  Enter a value: yes
aws_instance.vm_example: Creating...
aws_instance.vm_example: Still creating... [10s elapsed]
aws_instance.vm_example: Still creating... [20s elapsed]
aws_instance.vm_example: Still creating... [30s elapsed]
aws_instance.vm_example: Still creating... [40s elapsed]
aws_instance.vm_example: Still creating... [50s elapsed]
aws_instance.vm_example: Creation complete after 57s [id=i-06cf29fde369218e2]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

如你所见,我们成功创建了虚拟机。然而,当我们将工作区切换回默认工作区时,Terraform 又会要求重新创建它:

admin@myhome:~$ terraform workspace switch default
Switched to workspace "default".
admin@myhome:~$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # aws_instance.vm_example will be created
  + resource "aws_instance" "vm_example" {
[...]

即使资源的创建已经成功,Terraform 仍然会希望重新创建它。当我们检查存放 .tf 文件的目录时,我们会发现与默认工作区关联的 .tfstate 文件,以及一个名为 terraform.tfstate.d/ 的新目录,里面存放着 .tfstate 文件,每个文件都会存放在一个以工作区名称命名的子目录中。因此,对于测试工作区,状态文件将存储在 terraform.tfstate.d/testing 中:

admin@myhome:~$ ll
total 40
-rw-r--r--  1 trochej  staff   159B Mar 21 13:11 main.tf
-rw-r--r--  1 trochej  staff   158B Mar 21 12:27 providers.tf
-rw-r--r--  1 trochej  staff   4.4K Mar 21 21:17 terraform.tfstate
-rw-r--r--  1 trochej  staff   180B Mar 21 21:15 terraform.tfstate.backup
drwxr-xr-x  3 trochej  staff    96B Mar 21 21:07 terraform.tfstate.d
admin@myhome:~$ ll terraform.tfstate.d
total 0
drwxr-xr-x  3 trochej  staff    96B Mar 21 21:20 testing
admin@myhome:~$ ll terraform.tfstate.d/testing
total 8
-rw-r--r--  1 trochej  staff   180B Mar 21 21:18 terraform.tfstate

我们如何在 Terraform 代码中利用这一点呢?正如我们提到的,有一个特殊的序列(我们称之为变量),它会扩展为当前工作区的名称:

resource "aws_instance" "vm_example" {
  ami           = "ami-830c94e3"
  instance_type = terraform.workspace == "default" ? "t2.micro" : "t2.nano"
  tags = {
    Name = "DevOpsGuideTerraformExample"
}

通过这个小的修改(如果 terraform.workspace 是默认值,则实例将是 t2.micro;否则,它将是 t2.nano),我们引入了与启动虚拟机的工作空间相关的条件变化。

让我们通过 terraform plan 快速确认一下:

admin@myhome:~$ terraform workspace show
default
admin@myhome:~$ terraform plan | grep instance_type
      + instance_type                        = "t2.micro"
admin@myhome:~$ terraform workspace select testing
Switched to workspace "testing".
admin@myhome:~$ terraform plan | grep instance_type
      + instance_type                        = "t2.nano"

如前面的输出所示,取决于我们选择的工作空间,将创建不同类型的实例。

在本节中,我们深入探讨了 Terraform IaC 工具。我们解释了提供者和模块的概念,以及状态文件的作用。我们还演示了简单的 Terraform 配置及其与 AWS 云的互动。

在下一节中,我们将更详细地介绍 HashiCorp 配置语言(HCL),它是专门用于编写这些配置的。

HCL 深入解析

HCL 是一种配置语言,由多个 HashiCorp 工具使用,包括 Terraform,用于定义和管理基础设施即代码(IaC)。

HCL 旨在让人类和机器都能轻松阅读和编写。它使用的语法简单,类似于 JSON,但结构更为宽松,并且支持注释。HCL 文件通常具有 .hcl.tf 文件扩展名。

HCL 使用花括号来定义代码块,每个代码块都有一个标签,用于标识其类型。在每个代码块内,我们使用 key-value 语法定义属性,其中键是属性名,值是属性值。我们还可以使用花括号定义对象,如示例中所示的 tags 对象。

变量

在 HCL 中,变量使用 variable 块来定义。以下是如何在 HCL 中定义变量的示例:

variable "region" {
  type = string
  default = "eu-central-1"
}

在这个示例中,我们定义了一个名为 region 的变量,类型为 string,并指定了默认值 us-west-2。我们可以在代码中使用 ${var.region} 语法引用该变量。

HCL 支持多种数据类型的变量,包括 stringnumberbooleanlistmapobject。我们还可以使用 variable 块中的 description 参数为变量指定描述。

变量可以通过多种方式赋值,包括默认值、命令行参数或环境变量。在使用 Terraform 时,我们还可以在单独的文件中定义变量,并通过 .tfvars 文件扩展名(例如 variables.tfvars)或命令行参数在执行过程中传入。

一旦定义了变量,它们就不能更改,但 HCL 还允许在 locals 块内定义局部变量。局部变量对于简化模块或资源块中的复杂表达式或计算非常有用,因为它们可以将逻辑分解为更小、更易管理的部分。它们还可以使我们更容易维护代码,因为我们可以为那些可能频繁变化或需要跨多个资源或模块更新的值定义局部变量。

以下是一个locals块示例,它定义了eu-central-1区域并在每个 AZ 中生成子网:

locals {
  azs         = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
  cidr_block  = "10.0.0.0/16"
  subnet_bits = 8
  subnets     = {
    for idx, az in local.azs : az => {
      name       = "${var.environment}-subnet-${idx}"
      cidr_block = cidrsubnet(local.cidr_block, local.subnet_bits, idx)
      availability_zone = az
    }
  }
}

在此示例中,我们定义了一个locals块,其中包括以下变量:

  • azseu-central-1区域中的可用区列表

  • cidr_block:VPC 的 CIDR 块

  • subnet_bits:在 CIDR 块内为子网分配的位数

  • subnets:一个映射,使用for表达式为azs列表中的每个可用区生成子网

subnets映射中的for表达式会为azs列表中的每个可用区生成一个子网。子网的名称包括环境变量(可以作为变量传递)和可用区在列表中的索引。cidrsubnet函数用于根据cidr_block变量和subnet_bits变量计算每个子网的 CIDR 块。

生成的subnets映射将包含azs列表中每个可用区的键值对,其中键是可用区名称,值是一个映射,包含子网名称、CIDR 块和可用区。

注释

HCL 中的注释可以通过两种方式编写:单行注释和多行注释。单行注释以#符号开头,直到行末。多行注释则以/*开头,以*/结尾。多行注释可以跨越多行,通常用于提供更长的解释或暂时禁用代码段。

以下是一个单行注释的示例:

# This is a single-line comment in HCL

以下是一个多行注释的示例:

/*
This is a multi-line comment in HCL
It can span multiple lines and is often used
to provide longer explanations or to temporarily disable sections of code.
*/

Terraform 元参数

在 Terraform 中,元参数是可以用来修改资源块行为的特殊参数。之所以称其为元参数,是因为它们作用于整个资源块,而不是资源块中的特定属性。

元参数用于配置诸如资源实例数量(count)、资源名称(name)、资源之间的依赖关系(depends_on)等内容。

count

count允许你基于数字值创建多个资源实例。这在不重复整个代码块的情况下创建多个资源实例(例如 AWS 中的 EC2 实例)时非常有用。

例如,假设你想在 AWS 账户中创建三个 EC2 实例。你可以使用count元参数来创建多个相同的aws_ec2_instance资源块,而不是创建三个单独的资源块。以下是一个示例:

resource "aws_ec2_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  count         = 3
}

在此示例中,我们使用相同的amiinstance_type创建三个 EC2 实例。count元参数设置为3,意味着 Terraform 将创建三个aws_ec2_instance资源块的实例。每个实例将被赋予唯一的标识符,例如aws_ec2_instance.example[0]aws_ec2_instance.example[1]aws_ec2_instance.example[2]

for_each

for_each元参数类似于count元参数,它允许你创建多个资源实例。然而,for_eachcount更灵活,因为它允许你根据一个映射或值集合来创建实例,而不仅仅是基于一个数值。

例如,假设你有一个 AWS 安全组的映射,并希望在 Terraform 代码中创建它们。你可以使用for_each在一个块中创建所有安全组,而不是创建多个aws_security_group资源块。下面是一个示例:

variable "security_groups" {
  type = map(object({
    name        = string
    description = string
    ingress     = list(object({
      from_port   = number
      to_port     = number
      protocol    = string
      cidr_blocks = list(string)
    }))
  }))
}
resource "aws_security_group" "example" {
  for_each = var.security_groups
  name_prefix = each.value.name
  description = each.value.description
  ingress {
    from_port   = each.value.ingress[0].from_port
    to_port     = each.value.ingress[0].to_port
    protocol    = each.value.ingress[0].protocol
    cidr_blocks = each.value.ingress[0].cidr_blocks
  }
}

在这个示例中,我们使用for_each元参数根据security_groups变量(这是一个对象的映射)创建多个aws_security_group资源块的实例。每个实例将根据映射的键生成一个唯一的标识符。我们还使用name_prefix属性来设置每个安全组的名称,使用description属性来设置描述。最后,我们使用ingress块定义每个安全组的入站流量规则。

使用for_each可以简化你的 Terraform 代码,并使其更具可重用性,特别是在处理映射或集合值时。然而,需要注意实例之间可能存在的依赖关系,并确保代码结构能够正确处理多个实例。

lifecycle

lifecycle元参数用于定义创建、更新和删除资源的自定义行为。它允许你比默认行为更精细地控制资源及其依赖关系的生命周期。

lifecycle元参数可用于定义以下属性:

  • create_before_destroy:如果设置为true,Terraform 将在销毁旧资源之前创建新资源,这在某些情况下可以防止停机。

  • prevent_destroy:如果设置为true,Terraform 将防止资源被销毁。这对于保护关键资源免受意外删除非常有用。

  • ignore_changes:Terraform 在判断是否需要更新资源时,应该忽略的一些属性名称列表。

  • replace_triggered_by:一个依赖项列表,当这些依赖项发生变化时,资源将被重新创建。

下面是使用lifecycle元参数来防止销毁 S3 存储桶的示例:

resource "aws_s3_bucket" "example" {
  bucket = "example-bucket"
  acl    = "private"
  lifecycle {
    prevent_destroy = true
  }
}

在这个示例中,lifecycle块用于将prevent_destroy属性设置为true,这意味着 Terraform 将防止aws_s3_bucket资源被销毁。这对于保护关键资源免于被意外删除非常有用。

depends_on

depends_on元参数用于定义资源之间的依赖关系。它允许你指定一个资源依赖于另一个资源,这意味着 Terraform 将在依赖的资源创建之后创建依赖资源。

然而,重要的是要注意,在大多数情况下,Terraform 可以通过分析你的资源配置自动创建依赖树。这意味着除非绝对必要,否则应避免使用 depends_on,因为它可能导致依赖循环,从而引发错误并使你的 Terraform 代码更难以管理。

如果你确实需要使用 depends_on,那么重要的是要意识到可能出现的依赖循环,并以避免它们的方式来组织代码。这可能涉及将资源拆分成更小的模块,或者使用其他技术来减少复杂性并避免循环依赖。

下面是一个使用 depends_on 来指定 EC2 实例与安全组之间依赖关系的示例:

resource "aws_security_group" "example" {
  name_prefix = "example"
  ingress {
    from_port = 22
    to_port   = 22
    protocol  = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  depends_on = [aws_security_group.example]
}

在这个示例中,我们使用 depends_on 来指定 aws_instance 资源依赖于 aws_security_group 资源。这意味着 Terraform 会在创建实例之前先创建安全组。

你可以通过阅读官方文档来了解更多关于 HCL 语言的信息:developer.hashicorp.com/terraform/language

Terraform 与 AWS 示例

在本节中,我们将创建两个示例模块,以演示如何创建一个模块以及在选择创建资源的方式时需要考虑的事项。我们将要创建的模块将能够创建一个或多个 EC2 实例,一个附加的安全组以及其他所需的资源,如实例配置文件。它将做几乎所有我们在第十章中讨论的内容,但会使用 AWS CLI。

EC2 实例模块

让我们创建一个能够创建 EC2 实例的模块。考虑以下目录结构:

├── aws
│   └── eu-central-1
└── modules

modules 目录是我们放置所有模块的地方,aws 是我们存放 AWS 基础设施的地方,eu-central-1 是法兰克福 AWS 区域的基础设施代码。因此,让我们开始创建 EC2 模块。我们先创建一个目录来存放它和我们将需要的基本文件,如前所述:

admin@myhome:~$ cd modules
admin@myhome:~/modules$ mkdir aws_ec2
admin@myhome:~/modules$ cd aws_ec2
admin@myhome:~/modules/aws_ec2$ touch versions.tf main.tf variables.tf outputs.tf providers.tf
admin@myhome:~/modules/aws_ec2$ ls -l
total 0
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 main.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 outputs.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 providers.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 variables.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 versions.tf
admin@myhome:~/modules/aws_ec2$

注意,我们没有创建后端配置文件。这是因为后端将在根模块中配置。模块没有状态文件,因为由模块创建的资源将使用根(或主)模块的状态文件。让我们开始配置提供程序。在这种情况下,我们此时只需要 AWS 提供程序。在我们的示例中,我们将使用 eu-central-1 区域:

provider "aws" {
  region = "eu-central-1"
}

接下来,让我们在 versions.tf 文件中配置我们将使用的 Terraform 和 AWS 提供程序的版本:

terraform {
  required_version = ">= 1.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 3.0.0"
    }
  }
}

在这个示例中,required_version 属性设置为 >= 1.0.0,要求使用 Terraform 1.0.0 或更高版本。required_providers 属性用于指定 AWS 提供者,source 属性设置为 hashicorp/aws,并且 version 属性设置为 >= 3.0.0,要求使用最新版本的 AWS 提供者。

现在,我们可以做一些更有趣的事情,比如添加一个实际的 aws_instance 资源。为此,我们将开始填写这个资源所需的变量:

resource "aws_instance" "test_instance" {
  ami           = "ami-1234567890"
  instance_type = "t3.micro"
  tags = {
    Name = "TestInstance"
  }
}

保存所有模块文件的更改后,我们可以回到 aws/eu-central-1 目录,并创建一个与模块中类似的文件集:

admin@myhome:~/modules/aws_ec2$ cd ../../aws/eu-central-1
admin@myhome:~/aws/eu-central-1$ touch versions.tf main.tf variables.tf providers.tf
admin@myhome:~/aws/eu-central-1$ ls -l
total 0
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 main.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 providers.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 variables.tf
-rw-r--r--  1 admin  admin  0 Mar 16 13:02 versions.tf
admin@myhome:~/aws/eu-central-1$

这次,我们只需要 main.ftproviders.tfvariables.tfversions.tf。为了简化,我们可以直接复制 providers 和 versions 文件的内容:

admin@myhome:~/aws/eu-central-1$ cp ../../modules/aws_ec2/providers.tf .
admin@myhome:~/aws/eu-central-1$ cp ../../modules/aws_ec2/versions.tf .

现在,我们可以集中精力在 main.tf 文件中,在这里我们将尝试使用模块的第一个版本。main.tf 文件将如下所示:

module "test_instance" {
  source = "../../modules/aws_ec2"
}

我们创建的模块不需要任何变量,所以在这个文件中这就是我们所需要的全部内容。

由于这是我们的根模块,我们还需要配置 Terraform 状态文件的位置。为了简化,我们将使用本地状态文件,但在实际环境中,我们建议使用配置了分布式锁的 S3 桶。如果没有后端块,Terraform 将创建一个本地文件。我们已经准备好测试我们的模块(输出已缩短以便简洁):

admin@myhome:~/aws/eu-central-1$ terraform init
Initializing modules...
- test_instance in ../../modules/aws_ec2
Initializing the backend...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

一旦你运行了 terraform init(只有在更新模块或后端配置时才需要重新运行),你可以执行 terraform plan 来查看需要应用的更改:

admin@myhome:~/aws/eu-central-1$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # module.test_instance.aws_instance.test_instance will be created
  + resource "aws_instance" "test_instance" {
      + ami                                  = "ami-1234567890"
# Some of the output removed for readability
Plan: 1 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

注意

你没有使用 -out 选项保存这个计划,因此如果你现在运行 terraform apply,Terraform 无法保证会执行完全相同的操作。

在这个计划中,Terraform 确认我们的模块将为我们创建一个 EC2 实例。不幸的是,这个计划并不理想,因为它没有检查 AMI 是否实际存在,或者子网是否存在。这些错误将在我们运行 terraform apply 时出现。例如,我们提供的 AMI 是假的,因此 Terraform 在创建实例时会失败。让我们回到模块并改进它,通过自动获取正确的 Ubuntu Linux AMI。为此,Terraform AWS 提供者提供了一个数据资源。这个特殊资源使我们能够通过其 API 请求 AWS 提供各种资源。让我们在 modules 目录的 main.tf 文件中添加一个 AMI 数据资源:

data "aws_ami" "ubuntu" {
  most_recent = true
  owners = ["099720109477"] # Canonical
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}
resource "aws_instance" "test_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  tags = {
    Name = "TestInstance"
  }
}

(aws_ami) 代码块使用 aws_ami 数据源从 AWS 市场获取 Canonical 所拥有的最新 Ubuntu AMI。它通过将 most_recent 参数设置为 true,并使用 AMI 的 name 属性来过滤结果。它寻找一个具有特定名称模式的 AMI:ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*

第二段代码使用在第一段代码中获取的 AMI 创建了一个 AWS EC2 实例。它将实例类型设置为t3.micro,这是一种适合测试目的的小型实例类型。

它还为 EC2 实例添加了一个标签,键名为Name,值为TestInstance,这样它就能在 AWS 管理控制台中轻松识别。

您可以在文档中阅读更多关于aws_ami数据资源的内容:registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami

做了这个修改后,我们可以运行terraform plan并查看是否有所变化:

admin@myhome:~/aws/eu-central-1$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # module.test_instance.aws_instance.test_instance will be created
  + resource "aws_instance" "test_instance" {
      + ami                                  = "ami-050096f31d010b533"
# Rest of the output removed for readability

计划成功执行,看起来它找到了一个最近的适用于 Ubuntu Linux 22.04 的 AMI。我们需要考虑一些其他问题,尤其是如果我们想确保能够连接到这个新的 EC2 实例。 当然,如果我们应用更改,它会被创建,但我们目前没有办法连接到它。首先,让我们将 EC2 实例连接到正确的网络:我们将使用一个默认的 VPC 和一个公有子网,这样我们就能直接连接到这个实例。

为了找出默认 VPC 和公有子网的 ID,我们将再次使用数据资源:

问题是我们是否希望将所有创建的实例自动放入默认的 VPC(和公有子网)。通常,这个问题的答案是。在这种情况下,我们需要添加一些变量并将其传递给这个模块。

让我们向根模块中添加另一个文件,在这个文件中放置所有的数据资源,命名为data.tf

data "aws_vpc" "default" {
  filter {
    name   = "isDefault"
    values = ["true"]
  }
}
data "aws_subnets" "public" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
  filter {
    name   = "map-public-ip-on-launch"
    values = ["true"]
  }
}

现在,我们可以在模块中创建一个输入变量。返回到modules/aws_ec2目录并编辑variables.tf文件:

variable "public_subnet_id" {
  description = "Subnet ID we will run our EC2 instance"
  type        = string
}

现在,当你运行terraform plan时,你会看到以下错误:

│ Error: Missing required argument
│
│   on main.tf line 1, in module "test_instance":
│    1: module "test_instance" {
│
│ The argument "public_subnet_id" is required, but no definition was found.

在这里,我们创建了一个必需的变量,我们需要将其提供给模块。让我们通过编辑根模块中的main.tf文件(aws/eu-central-1目录)来完成:

module "test_instance" {
  source = "../../modules/aws_ec2"
  public_subnet_id = data.aws_subnets.public.ids[0]
}

注意data.aws_subnets.public.ids[0]。我们使用了列表表示法,在这里我们选择了列表中的第一个元素(它是一个字符串,因为模块期望它是字符串)。这是因为有多个子网,aws_subnets为我们返回了这些子网的列表。

再次运行计划应该会给我们添加一个资源。太好了!现在,我们的实例将获得一个可以连接的公网 IP 地址。但我们仍然缺少一个防火墙规则,这个规则允许我们连接到端口22(SSH)。让我们创建一个安全 SG)。

再次提醒,我们可以选择在根模块中创建一个安全组,这样我们就可以在不更改 EC2 模块的情况下修改它。或者,我们可以将安全组添加到 EC2 模块中,这意味着该模块将完全控制它,但会缺少一些灵活性。还可以创建一个同时执行两者的模块:从根模块注入一个安全组并使用模块中预定义的安全组,但这超出了本章的范围。在本例中,为了简化起见,我们将在模块内部创建一个安全组。

要创建一个安全组(SG),我们将使用aws_security_group资源,它需要一个 VPC ID。有两种可能性:我们需要向 EC2 模块引入另一个变量,或者使用另一个数据资源从提供的子网自动获取 VPC ID。这次,采用更优雅的解决方案是使用数据资源。让我们将其添加到模块中的main.tf

data "aws_subnet" "current" {
  id = var.public_subnet_id
}

有了这个配置,我们现在可以添加一个安全组了:

resource "aws_security_group" "allow_ssh" {
  name        = "TestInstanceSG"
  description = "Allow SSH traffic"
  vpc_id      = data.aws_subnet.current.vpc_id
  ingress {
    description      = "SSH from the Internet"
    from_port        = 22
    to_port          = 22
    protocol         = "tcp"
    cidr_blocks      = ["0.0.0.0/0"]
  }
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
  tags = {
    Name = "TestInstanceSG"
  }
}

上述代码创建了一个允许 SSH 流量通过的 AWS 安全组(SG),以连接我们之前创建的 EC2 实例。

aws_security_group资源用于为 EC2 实例创建一个安全组。它将安全组的名称设置为TestInstanceSG,并提供简短的描述。

vpc_id属性被设置为当前子网的 VPC ID。它使用名为currentaws_subnet数据源来获取当前子网的 VPC ID。

ingress块定义了安全组的入站规则。在本例中,它通过指定from_portto_portprotocolcidr_blocks,允许来自任何 IP 地址(0.0.0.0/0)的 SSH 流量。

egress块定义了安全组的出站规则。在本例中,它通过指定from_portto_portprotocolcidr_blocks允许所有出站流量。它还通过指定ipv6_cidr_blocks来允许所有 IPv6 流量。

tags属性为安全组设置一个标签,键为Name,值为TestInstanceSG,这样可以方便地在 AWS 管理控制台中进行识别。

现在,我们准备将此安全组附加到我们的实例。我们需要在aws_instance资源中使用security_groups选项:

resource "aws_instance" "test_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  security_groups = [aws_security_group.allow_ssh.id]
  tags = {
    Name = "TestInstance"
  }
}

现在,运行terraform plan后,您将看到两个需要添加的资源:

Plan: 2 to add, 0 to change, 0 to destroy.

在此时,我们需要将我们的公有 SSH 密钥添加到 AWS 并配置 EC2 使用它作为默认的 Ubuntu Linux 用户(ubuntu)。假设您已经生成了 SSH 密钥,我们将创建一个包含该密钥的变量,创建一个资源使该密钥可用于 EC2 实例,最后将其添加到实例配置中。

重要说明

在某些 AWS 区域中,要求使用旧版 RSA 密钥格式。只要可用,我们建议根据最新的推荐使用最新的格式。在撰写本书时,推荐使用ED25519密钥。

让我们在根模块中添加一个变量:

variable "ssh_key" {
  description = "SSH key attached to the instance"
  type = string
  default = "ssh-rsa AAASomeRSAKEY""
}

让我们为 EC2 模块添加一个类似的安全组:

variable "ssh_key" {
  description = "SSH key attached to the instance"
  type = string
}

这是没有默认值的,以使得这个变量对每个模块使用时都是必需的。现在,让我们在 EC2 模块(main.tf)中添加密钥到 AWS 中:

resource "aws_key_pair" "deployer" {
  key_name   = "ssh_deployer_key"
  public_key = var.ssh_key
}

然后,我们可以在aws_instance资源中使用它:

resource "aws_instance" "test_instance" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  security_groups = [aws_security_group.allow_ssh.id]
  key_name = aws_key_pair.deployer.key_name
  tags = {
    Name = "TestInstance"
  }
}

我们需要在根模块中的main.tf里使用这个新变量:

module "test_instance" {
  source = "../../modules/aws_ec2"
  public_subnet_id = data.aws_subnets.public.ids[0]
  ssh_key          = var.ssh_key
}

最后,运行terraform plan将给我们显示三个资源:

Plan: 3 to add, 0 to change, 0 to destroy.

太棒了!运行terraform apply并接受任何更改将会在公共子网中部署 EC2 实例,并使用我们的密钥。但是,除非我们去 AWS 控制台手动检查,否则我们仍然无法知道实例的 IP 地址。

为了获取这些信息,我们需要从 EC2 模块中导出这些变量,然后再次在根模块中使用。为此,我们有另一个代码块叫output。它的语法与variable语法非常相似,但你还可以将output变量标记为敏感信息,这样在运行terraform planterraform apply命令时,默认情况下不会显示它。

让我们定义输出,显示 EC2 实例的公共 IP 地址。在 EC2 模块的outputs.tf文件中,放入以下代码:

output "instance_public_ip" {
  value       = aws_instance.test_instance.public_ip
  description = "Public IP address of the EC2 instance"
}

在根模块中创建outputs.tf文件,并在那里放入以下代码:

output "instance_public_ip" {
  value       = module.test_instance.instance_public_ip
  description = "Public IP address of the instance"
}

现在,当你运行terraform plan时,你会看到输出结果有所变化:

Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
  + instance_public_ip = (known after apply)

这样,我们通过创建一个单独的 EC2 实例创建了一个简单的模块。如果我们运行terraform apply,实例将被创建,并且输出结果会显示我们该实例的 IP 地址。

从这里开始,接下来的步骤将涉及为模块添加更多功能,通过使用count元参数来创建多个实例,或者通过使用for_each元参数来创建一组不同的 EC2 实例。

总结

在本章中,我们介绍了 IaC 的概念。我们解释了为什么它是管理和开发基础设施的重要方法。我们还介绍了一些在这种工作方式中相当流行的工具。作为首选工具,我们讲解了 Terraform——可能是最广泛使用的工具。

在下一章,我们将展示如何利用一些在线工具和自动化来构建CICD的管道。

练习

尝试以下练习,测试你在本章中学到的内容:

  1. 创建一个模块,用于创建启用服务器端加密的 S3 存储桶。

  2. 向我们创建的模块添加一个实例配置文件,并使用我们在第十章中使用的相同 IAM 策略。

  3. 使用count元参数创建两个实例。

第十三章:使用 Terraform、GitHub 和 Atlantis 实现 CI/CD

本章我们将在前几章的基础上,介绍持续集成CI)和持续部署CD)的管道。市场上有许多可用的 CI 和 CD 工具,包括开源和闭源工具、自托管和 软件即服务SaaS)工具。我们将演示一个示例管道,从将源代码提交到存储 Terraform 代码的仓库,到在基础设施中应用更改。我们将自动完成这一过程,但会经过团队的审查。

在本章中,我们将覆盖以下主题:

  • 什么是 CI/CD?

  • 持续集成和部署你的基础设施

  • 使用 Atlantis 实现 CI/CD

技术要求

本章需要以下内容:

  • 一个 Linux 服务器

  • GitHub 或类似平台(GitLab 或 Bitbucket)上的免费账户

  • 最新版本的 Terraform

  • AWS CLI

  • Git

什么是 CI/CD?

CI/CD 是一套实践、工具和流程,帮助软件开发团队自动化构建、测试和部署应用程序,从而更频繁地发布软件,并且更有信心其质量。

持续集成CI)是一种实践,开发人员定期将代码更改集成到代码仓库中,每次集成都触发自动构建和测试过程。这有助于尽早发现错误,并确保应用程序可以可靠地构建和测试。

例如,使用 Docker,开发人员可以设置一个 CI 管道,该管道在每次将更改推送到代码仓库时自动构建和测试应用程序。管道可以包括构建 Docker 镜像、运行自动化测试以及将镜像发布到 Docker 仓库的步骤。

持续交付是指在成功集成过程后,使软件可以随时进行部署的实践。例如,使用 Docker 镜像,交付过程是将镜像推送到 Docker 仓库,之后可以由部署管道提取。

最后,持续部署CD)是将经过测试和验证的持续交付过程代码工件(如 Docker 镜像、Java JAR 文件、ZIP 压缩包等)自动部署到生产环境中的实践。这样消除了部署过程中的人工干预需求。

让我们来看一下几种常见的部署策略:

  • 滚动部署:这种策略涉及一次只将更改部署到一部分服务器,逐步将更改推广到整个基础设施。这使得团队能够监控更改,并在出现问题时迅速回滚。

  • 蓝绿部署:在这种策略中,设置两个相同的生产环境,一个处于激活状态(蓝色),另一个处于非激活状态(绿色)。代码变更被部署到非激活环境,并在切换流量到新环境之前进行测试。这种方式可以实现零停机部署。

  • 金丝雀部署:这种策略涉及将变更部署到一小部分用户,并保持大多数用户使用当前版本。这样,团队可以监控这些变更并收集反馈,在将这些变更推广到所有用户之前,进行充分测试。

  • 功能开关/特性开关:通过这种策略,变更被部署到生产环境,但会被隐藏在功能开关后面。这个开关随后会逐步对特定用户或环境启用,允许团队在将新功能提供给所有人之前,控制功能的发布并收集反馈。

所有这些策略都可以通过 CD 工具 来实现自动化,如 JenkinsCircleCIGitHubGitLab ActionsTravis CI 等众多工具。

在讨论应用程序的部署时,我们至少需要提到 GitOps。GitOps 是一种新的基础设施和应用程序部署方法,它使用 Git 作为声明性基础设施和应用程序规格的唯一真理来源。其核心思想是在 Git 仓库中定义基础设施和应用程序的期望状态,并使用 GitOps 工具自动将这些变更应用到目标环境。

在 GitOps 中,对基础设施或应用程序的每一次更改都是通过 Git 提交来完成的,这会触发一个流水线,将这些更改应用到目标环境。这提供了完整的变更审计跟踪,并确保基础设施始终处于期望的状态。

一些有助于启用 GitOps 的工具包括以下几种:

  • FluxCD:这是一个流行的 GitOps 工具,可以通过 Git 作为唯一的真理来源,自动化应用程序和基础设施的部署和扩展。它与 KubernetesHelm 和其他工具集成,提供完整的 GitOps 工作流。

  • ArgoCD:这是另一款流行的 GitOps 工具,使用 Git 作为真理来源来部署和管理应用程序和基础设施。它提供基于网页的 UI 和 CLI 来管理 GitOps 流水线,并与 Kubernetes、Helm 以及其他工具集成。

  • Jenkins X:这是一个包含 GitOps 工作流的 CI/CD 平台,用于构建、测试和将应用程序部署到 Kubernetes 集群。它利用 GitOps 来管理整个流水线,从源代码到生产环境的部署。

既然我们已经了解了 CI/CD 的概念,接下来我们可以探索一些工具,这些工具可以用来构建这样的流水线。在下一节中,我们将为您提供一些流水线示例,包含克隆最新版本的代码库、构建 Docker 镜像以及运行一些测试。

CI/CD 流水线示例

让我们看一些自动化流水线的示例,这些流水线将我们的 Terraform 更改应用到不同的 CD 工具中。

Jenkins

Jenkins 是最流行的开源 CI/CD 工具之一。它从点击式配置转变为在用户批准更改后运行apply

pipeline {
  agent any
  environment {
    AWS_ACCESS_KEY_ID = credentials('aws-key-id')
    AWS_SECRET_ACCESS_KEY = credentials('aws-secret-key')
  }

上述代码开启了一个新的流水线定义。它被设置为在任何可用的 Jenkins 代理上运行。接下来,我们设置了将在该流水线中使用的环境变量。这些环境变量的文本是从aws-key-idaws-secret-key Jenkins 凭证中提取的。在运行该流水线之前,这些凭证需要被定义。

接下来,我们将在stages块中定义每个步骤的流水线:

  stages {
    stage('Checkout') {
        steps {
            checkout scm
        }
    }

首先,我们将克隆我们的 Git 仓库;执行此操作的步骤是checkout scm。URL 将在 Jenkins UI 中直接配置:

    stage('TF Plan') {
        steps {
            dir('terraform') {
                sh 'terraform init'
                sh 'terraform plan -out terraform.tfplan'
            }
       }
     }

接下来,我们将运行terraform init来初始化 Terraform 环境。在这里,我们运行计划并将输出保存到terraform.tfplan文件中,最后一步将使用此文件运行apply

    stage('Approval') {
        steps {
            script {
                def userInput = input(id: 'confirm', message: 'Run apply?', parameters: [ [$class: 'BooleanParameterDefinition', defaultValue: false, description: 'Running apply', name: 'confirm'] ])
            }
        }
    }

这个步骤定义了用户输入。我们需要在审查计划的输出后通过运行apply来确认这一点。我们将默认值定义为false。流水线将在此步骤等待您的输入:

    stage('TF Apply') {
        steps {
            dir('terraform') {
                sh 'terraform apply -auto-approve -input=false terraform.tfplan'
                sh 'rm -f terraform.tfplan'
            }
        }
    }
  }
}

最后,如果您已确认流水线将运行apply而无需进一步的用户输入(-input=false选项),并且apply运行没有任何错误,它将删除在计划步骤中创建的terraform.tfplan文件。

GitHub Actions 基础

有可能创建一个类似的流水线,使用workflow_dispatch选项,但它会在 Action 运行之前要求用户输入(请参阅官方文档作为参考:github.blog/changelog/2021-11-10-github-actions-input-types-for-manual-workflows/)。因此,我们改为创建一个将运行planapply的 Action:

name: Terraform Apply
on:
  push:
    branches: [ main ]

上述代码定义了 GitHub Action 将仅在更改主分支时触发:

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

在这里,我们以类似于 Jenkins 流水线的方式定义了环境变量。AWS 的访问密钥和密钥从存储在 GitHub 中的秘密中提取,这些秘密需要我们事先添加。如果我们的 GitHub 运行器在 AWS 环境中运行或我们使用 GitHub OpenID Connect,则无需此操作。您可以通过查看 GitHub 文档了解后者:docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services

接下来,我们可以在jobs块中定义 GitHub Action 的步骤:

jobs:
  terraform_apply:
    runs-on: ubuntu-latest

在这里,我们定义了一个名为terraform_apply的作业,它将在 GitHub Actions 中可用的最新版本的 Ubuntu 运行器上运行:

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

该步骤检出代码。我们使用 GitHub 中可用的预定义 Action,而不是创建一个运行 Git 命令行的脚本。它将执行的确切代码可在github.com/actions/checkout找到:

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v1

Setup Terraform步骤将为我们下载 Terraform。默认情况下,它将下载最新的可用版本,但如果需要,我们可以固定某个特定版本。该步骤的代码可在github.com/hashicorp/setup-terraform找到:

    - name: Terraform Plan
      working-directory: terraform/
      run: |
        terraform init
        terraform plan -out terraform.tfplan

Terraform Plan步骤中,我们初始化 Terraform 并以与 Jenkins 管道相同的方式运行计划:

    - name: Terraform Apply
      working-directory: terraform/
      run: |
        terraform apply -auto-approve –input=false terraform.tfplan
        rm -f terraform.tfplan

最后,Terraform Apply步骤将从之前保存的 Terraform 计划文件terraform.tfplan中应用基础设施变更,并删除计划文件。

如果您希望创建一段可以在任何 CI/CD 工具中工作的更强大的代码,可以创建一个Bash 脚本来完成繁重的工作。使用 Bash 脚本,您还可以在运行计划之前嵌入一些测试。以下是一个示例 Bash 脚本,它将为您运行 Terraform 计划并应用它:

#!/usr/bin/env bash
set -u
set -e

在这里,我们将 Bash 设置为运行此脚本的默认 shell。在接下来的几行中,我们将修改脚本的默认设置,使其在遇到任何未绑定的变量或错误时停止执行:

# Check if Terraform binary is in PATH
if command -v terraform &> /dev/null; then
  TERRAFORM_BIN="$(command -v terraform)"
else
  echo "Terraform not installed?"
  exit 1
fi

这个代码块检查系统中是否可用 Terraform,并将其完整路径保存到TERRAFORM_BIN变量中,我们稍后将使用它:

# Init terraform backend
$TERRAFORM_BIN init -input=false

在运行计划之前,初始化 Terraform 环境:

# Plan changes
echo "Running plan..."
$TERRAFORM_BIN plan -input=false -out=./terraform.tfplan

运行plan并将其保存到文件中以供后续使用:

echo "Running Terraform now"
if $TERRAFORM_BIN apply -input=false ./terraform.tfplan; then
    echo "Terraform finished successfully"
    RETCODE=0
else
    echo "Failed!"    fi
fi

上面的代码块执行Terraform apply并检查命令的返回代码。它还会显示相应的信息。

其他主要的 CI/CD 解决方案使用类似的方法。最大的区别在于 Jenkins、企业工具和开源解决方案之间,其中YAML配置最为常见。在下一节中,我们将更深入地探讨管道的每个阶段,重点关注 Terraform 的集成测试以及基础设施变更的部署。

持续集成和部署您的基础设施

测试应用程序代码现在已经成为事实上的标准,特别是在测试驱动开发TDD)被采纳之后。TDD 是一种软件开发过程,在这个过程中,开发人员在编写代码之前先编写自动化测试。

这些测试最初是故意失败的,开发人员随后编写代码使其通过。代码会不断重构,以确保高效、可维护,并且能够通过所有测试。这种方法有助于减少漏洞并提高软件的可靠性。

测试基础设施并不像看起来那么简单,因为很难在不实际启动实例的情况下检查 Amazon 弹性计算云EC2)是否会成功启动。虽然可以模拟对 AWS 的 API 调用,但这并不能保证实际的 API 会返回与测试代码相同的结果。使用 AWS 时,这也意味着测试会很慢(我们需要等待该 EC2 实例启动),并且可能会产生额外的云端费用。

有多个基础设施测试工具,既有与 Terraform 集成的,也有第三方软件(这也是开源软件)。

集成测试

我们在 CI 流水线中可以运行多种基本测试。我们可以检测实际代码与云端运行代码之间的偏差,我们可以根据推荐的格式对代码进行 lint 检查,还可以测试代码是否符合我们的合规政策。我们还可以从 Terraform 代码中估算 AWS 成本。更复杂且耗时的过程包括单元测试和代码的端到端测试。让我们来看一下在流水线中可以使用的可用工具。

我们可以从一开始运行的大多数基本测试仅涉及运行 terraform validateterraform fmt。前者将检查 Terraform 代码的语法是否有效(意味着资源和/或变量没有拼写错误、所有必需的变量都存在等)。fmt 检查将根据 Terraform 标准更新代码的格式,这意味着所有多余的空格将被删除,或者某些空格可能会被添加以对齐 = 符号以增强可读性。这对于更简单的基础设施来说可能已经足够,我们建议从一开始就添加这些测试,因为这样做非常简单。你可以重用我们之前提供的代码片段来启动你现有代码的过程。

基础设施成本

基础设施成本不是你可以在测试流水线中运行的功能性、静态或单元测试。虽然监控这一方面对你的基础设施非常有用,但你的经理也会很高兴知道 AWS 预算是否符合预测。

Infracost.io 是一个云成本估算工具,允许你通过提供实时的云资源成本分析来监控和管理基础设施成本。使用 Infracost.io,你可以估算基础设施变更的成本,并在开发周期的每个阶段提供成本反馈,避免任何意外的费用。

将 Infracost.io 集成到 GitHub Actions 中是一个简单的过程,包括创建一个 Infracost.io 账户并生成一个 API 密钥,允许你访问成本估算数据。

接下来,你需要在本地机器上安装 Infracost.io 的命令行工具(CLI)。CLI 是一个命令行工具,允许你计算和比较基础设施成本。

安装 CLI 后,您可以通过创建一个新的操作文件(例如 .github/workflows/infracost.yml)来将 Infracost.io 操作添加到您的 GitHub 工作流中。

在 Infracost.io 的操作文件中,您需要指定 Infracost.io API 密钥以及 Terraform 配置文件的路径:

name: Infracost
on:
  push:
    branches:
      - main
jobs:
  infracost:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Run Infracost
        uses: infracost/infracost-action@v1
        with:
          api_key: ${{ secrets.INFRACOST_API_KEY }}
          terraform_dir: ./terraform

最后,将更改提交并推送到您的 GitHub 仓库。每当新的 Terraform 配置文件被推送到仓库时,Infracost.io 操作会自动计算成本估算,并在 GitHub Actions 页面上提供反馈。

对于开源项目,Infracost 是免费的,但您也可以创建自己的服务来监控云成本。Infracost 的 GitHub 仓库可以在github.com/infracost/infracost找到。

通过将其集成到您的 CI 流水线中,您可以主动监控和管理基础设施成本,并在将其部署到云账户之前,基于这些信息做出有关基础设施更改的明智决策。

漂移测试

使用 Terraform 管理基础设施的一个挑战是确保基础设施的实际状态与 Terraform 配置文件中定义的期望状态相匹配。这就是 漂移 概念的由来。

漂移发生在基础设施的期望状态与实际状态之间存在差异时。例如,如果使用 Terraform 创建的资源被手动修改而不通过 Terraform,基础设施的实际状态将与 Terraform 配置文件中定义的期望状态不同。这可能导致基础设施的不一致,并可能引发操作问题。

为了检测基础设施中的漂移,Terraform 提供了一个名为 terraform plan 的命令。当运行此命令时,Terraform 会将配置文件中定义的期望状态与基础设施的实际状态进行比较,并生成一份计划,列出将基础设施恢复到期望状态所需的更改。如果期望状态和实际状态之间存在任何差异,Terraform 会在计划输出中显示它们。

还可以使用第三方工具扩展 Terraform 的这一功能。其中一个工具就是 Driftctl

Driftctl 是一个开源工具,帮助检测由 Terraform 管理的云基础设施中的漂移。它扫描基础设施资源的实际状态,并将其与 Terraform 配置文件中定义的期望状态进行比较,以识别任何差异或不一致。Driftctl 支持广泛的云服务提供商,包括 AWS、Google CloudMicrosoft Azure 和 Kubernetes。

Driftctl 可以以多种方式使用来检测基础设施中的漂移。它可以与 CI/CD 流水线集成,自动检测漂移并触发修正措施。也可以手动运行以按需检查漂移。

这是一个使用Driftctl工具来检测基础设施漂移的 GitHub pipeline 示例:

name: Detect Infrastructure Drift
on:
  push:
    branches:
      - main

上述代码表明,此 pipeline 将仅在main分支上运行。

在这里,我们定义了一个名为detect-drift的作业,它将在最新的 Ubuntu Linux runner 上运行:

jobs:
  detect-drift:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2

接下来,我们开始定义 pipeline 中每个步骤要做的事情——首先,我们将使用一个预定义的动作,在 runner 上运行git clone

    - name: Install Terraform
      run: |
        sudo apt-get update
        sudo apt-get install -y unzip
        curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
        sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
        sudo apt-get update && sudo apt-get install terraform

接下来的步骤是定义一个 shell 脚本,用于从 HashiCorp 发布的公共仓库安装、解压并下载 Terraform:

    - name: Install Driftctl
      run: |
        curl https://github.com/cloudskiff/driftctl/releases/download/v0.8.2/driftctl_0.8.2_linux_amd64.tar.gz -sSLo driftctl.tar.gz
        tar -xzf driftctl.tar.gz
        sudo mv driftctl /usr/local/bin/

在这一步中,我们将通过从 GitHub 上的公共发布下载归档来安装Driftctl工具。我们将提取文件并将二进制文件移动到/usr/local/bin目录中:

    - name: Initialize Terraform
      run: terraform init
    - name: Check Terraform Configuration
      run: terraform validate

上述步骤仅涉及运行terraform initterraform validate,以验证我们是否可以访问 Terraform 后端,以及我们打算在接下来的几步中检查的代码是否在语法上有效:

    - name: Detect Drift with Driftctl
      run: |
        driftctl scan –from tfstate://./terraform.tfstate –output json > drift.json
    - name: Upload Drift Report to GitHub
      uses: actions/upload-artifact@v2
      with:
        name: drift-report
        path: drift.json

最后两步是运行Driftctl工具,并将其发现结果保存在driftctl.json文件中,该文件将作为drift-report上传到 GitHub 工件中。

总结一下,这个 pipeline 在main分支上运行,并执行以下步骤:

  • 从 GitHub 仓库检出代码。

  • 安装terraformdriftctl

  • 初始化 Terraform 并验证 Terraform 配置文件。

  • 使用driftctl扫描基础设施资源的实际状态,并将其与 Terraform 配置文件中定义的期望状态进行比较,以检测任何漂移。此扫描的输出将保存到名为drift.json的 JSON 文件中。

  • drift.json文件作为工件上传到 GitHub,供后续分析使用。

此外,这个 pipeline 还可以根据特定需求进行自定义,例如集成到 CI/CD pipeline 中或按计划定期检查基础设施中的漂移。

每次 pipeline 运行时都需要安装driftctlterraform并不是我们期望的做法,因此我们建议您准备一个包含这些工具预安装的 Docker 镜像,并使用该镜像。这还将提升您的安全性。

您可以在网站driftctl.com/上找到有关该项目的更多信息。

安全测试

测试基础设施的安全性是维护安全稳定系统的一个重要方面。由于现代基础设施通常作为代码进行定义和管理,因此有必要像测试其他代码一样对其进行测试。基础设施即代码IaC)测试有助于发现安全漏洞、配置问题以及可能导致系统被攻破的其他缺陷。

有几种自动化工具可以帮助进行基础设施安全测试。这些工具可以帮助识别潜在的安全问题,例如配置错误的安全组、未使用的安全规则和不安全的敏感数据。

我们可以使用几个工具来作为 CI 管道外的单独过程进行安全性测试:

  • Prowler:这是一个开源工具,用于扫描 AWS 基础设施中的安全漏洞。它可以检查诸如 AWS 身份与访问管理IAM)配置错误、开放的安全组和S3 存储桶权限问题等。

  • CloudFormation Guard:这是一个工具,用于根据一组预定义的安全规则验证AWS CloudFormation模板。它可以帮助识别诸如开放安全组、未使用的 IAM 策略和未加密的 S3 存储桶等问题。

  • OpenSCAP:这是一个为基于 Linux 的基础设施提供自动化安全合规性测试的工具。它可以扫描系统是否符合各种安全标准,如支付卡行业数据安全标准PCI DSS)或国家标准与技术研究院特别出版物 800-53NIST SP 800-53)。

  • InSpec:这是另一个开源测试框架,可用于测试基础设施的合规性和安全性。它内置支持多种平台,并可以用于针对不同的安全标准进行测试,例如健康保险流通与责任法案HIPAA)和互联网安全中心CIS)。

在这里,我们重点关注在持续集成(CI)中集成一些安全测试。我们可以在此处集成的工具如下:

  • tfsec 是一个开源的 Terraform 代码静态分析工具。它扫描 Terraform 配置,以检测潜在的安全问题,并提供修复建议。它内置支持多种云服务提供商,可以帮助识别诸如弱身份验证、不安全的网络配置和未加密的数据存储等问题。其 GitHub 仓库可以在 github.com/aquasecurity/tfsec 找到。

  • Terrascan 是一个用于基础设施即代码(IaC)文件的静态代码分析开源工具。它支持多种 IaC 文件格式,包括 Terraform、Kubernetes YAML 和 Helm 图表,并扫描这些文件中的安全漏洞、合规性违规和其他问题。Terrascan 可以集成到 CI/CD 管道中,并帮助确保基础设施部署是安全的,并符合行业标准。其 GitHub 仓库可以在 github.com/tenable/terrascan 找到。

  • CloudQuery 是一个开源工具,使用户能够在不同的云平台上测试安全策略和合规性,包括 AWS、Google Cloud PlatformGCP)和 Microsoft Azure。它提供了统一的查询语言和接口来访问云资源,允许用户分析配置并检测潜在的安全漏洞。CloudQuery 集成了各种 CI/CD 流水线,使自动化安全策略和合规性测试变得更加容易。用户还可以根据他们的特定需求和标准自定义查询和规则。你可以在他们的博客文章中阅读更多相关内容:www.cloudquery.io/how-to-guides/open-source-cspm

让我们来看一下这个示例 GitHub 流水线,它集成了 terrascan 工具:

name: Terrascan Scan
on: [push]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Install Terrascan
      run: |
        wget https://github.com/accurics/terrascan/releases/latest/download/terrascan_linux_amd64.zip
        unzip terrascan_linux_amd64.zip
        rm -f terrascan_linux_amd64.zip
        sudo mv terrascan /usr/local/bin/
    - name: Run Terrascan
      run: |
        terrascan scan -f ./path/to/infrastructure/code

在这个工作流中,on: [push] 行指定每当有更改推送到仓库时,工作流应该被触发。

jobs 部分包含一个名为 scan 的单个作业。runs-on 键指定该作业应在 Ubuntu 机器上运行。

steps 部分包含三个步骤:

  1. 它使用 actions/checkout 操作从仓库中检出代码。

  2. 它使用 wgetunzip 命令在机器上下载并安装 Terrascan。请注意,这一步假设你在 Linux 机器上运行工作流。

  3. 它运行 Terrascan 来扫描基础设施代码。你需要将 ./path/to/infrastructure/code 替换为你实际的基础设施代码路径。

一旦你创建了这个工作流并将其推送到你的 GitHub 仓库,GitHub Actions 会在每次有更改推送到仓库时自动运行该工作流。你可以在工作流日志中查看 Terrascan 扫描的结果。

让我们继续讨论 基础设施单元测试。目前通常有两种选择:直接测试 HCL 代码时使用 Terratest,或者如果你想使用更先进的编程语言来维护你的基础设施即代码(IaC),可以选择 CDKTF/Pulumi

使用 Terratest 进行测试

Terratest 是一个开源测试框架,用于基础设施代码的测试,包括 Terraform 的 HCL 代码。它于 2017 年由专注于基础设施自动化的公司 Gruntwork 发布,该公司还提供一套预构建的基础设施模块,称为 Gruntwork IaC 库。

Terratest 旨在通过提供一套辅助函数和库来简化基础设施代码的测试,使用户能够编写使用 Go(一种流行的基础设施自动化编程语言)编写的自动化测试。Terratest 不仅可以用于测试 Terraform 代码,还可以用于测试使用其他工具构建的基础设施,如 AnsiblePackerDocker

Terratest 的一个关键优势是,它允许开发人员在类似生产环境的环境中测试他们的基础设施代码,而无需专门的测试环境。这可以通过使用 Docker 和 Terraform 等工具来创建临时基础设施资源进行测试。

Terratest 还提供了一系列测试类型,包括 单元测试集成测试端到端测试,允许用户在不同的抽象层次测试他们的基础设施代码。这有助于确保在将代码部署到生产环境之前,进行充分的测试,减少停机或其他问题的风险。

测试我们在上一章中创建的 aws_instance 资源的示例如下所示:

package test
import (
  "testing"
  "github.com/gruntwork-io/terratest/modules/aws"
  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/stretchr/testify/assert"
)
func TestAwsInstance(t *testing.T) {
  terraformOptions := &terraform.Options{
    TerraformDir: "../path/to/terraform/module",
  }
  defer terraform.Destroy(t, terraformOptions)
  terraform.InitAndApply(t, terraformOptions)
  instanceID := terraform.Output(t, terraformOptions, "aws_instance_id")
  instance := aws.GetEc2Instance(t, "us-west-2", instanceID)
  assert.Equal(t, "t3.micro", instance.InstanceType)
  assert.Equal(t, "TestInstance", instance.Tags["Name"])
}

在此示例中,我们首先定义一个名为 TestAwsInstance 的测试函数,使用标准的 terraformOptions 对象,该对象指定我们的 Terraform 模块的目录。

然后,我们使用 terraform.InitAndApply 函数来初始化和应用 Terraform 配置,创建 AWS EC2 实例资源。

接下来,我们使用 Terratest AWS 模块 中的 aws.GetEc2Instance 函数,通过实例 ID 获取有关已创建实例的信息。

最后,我们使用 testify 包中的 assert 库编写断言,验证实例的属性,例如其实例类型和标签。如果任何断言失败,测试将失败。

要运行此示例,您需要确保已安装 Terratest 和 AWS Go 模块,并在您的环境中设置了有效的 AWS 凭据。

使用 CDKTF 进行单元测试

AWS Cloud Development Kit for Terraform (CDKTF) 是一个开源框架,允许开发人员使用编程语言(如TypeScriptPythonC#)在 Terraform 支持的任何云解决方案中定义基础设施和服务。它通过使用高级面向对象抽象来实现基础设施即代码(IaC)的创建,减少了编写和维护基础设施代码的复杂性。

CDKTF 最初于 2020 年 3 月发布,是 AWS 和 Terraform 背后的公司 HashiCorp 之间的合作。CDKTF 融合了两者的优势:现代编程语言的熟悉性和表现力,以及 Terraform 的声明式、多云能力。

TypeScript 是与 CDKTF 一起使用的最流行的语言,它提供了一个类型安全的开发体验,具有静态类型检查、代码补全和重构等功能。

作为示例,让我们重用 第十二章 中的 Terraform 代码:

resource "aws_instance" "vm_example" {
  ami           = "ami-830c94e3"
  instance_type = "t2.micro"
  tags = {
    Name = "DevOpsGuideTerraformExample"
}

在 Python 中,CDKTF 的等效代码如下所示:

#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.instance import Instance
from cdktf_cdktf_provider_aws.data_aws_ami import DataAwsAmi, DataAwsAmiFilter

这里,我们导入了所有必需的模块。脚本的第一行表示该脚本的默认解释器应该是系统中可用的 Python 解释器:

class MyStack(TerraformStack):
    def __init__(self, scope: Construct, id: str):
        super().__init__(scope, id)
        AwsProvider(self, "AWS", region="eu-central-1")

在前述代码行中,我们正在配置一个默认提供程序来使用 eu-central-1 区域。接下来让我们看看:

        ec2_instance = Instance(
            self,
            id_="ec2instanceName",
            instance_type="t2.micro"
            ami="ami-830c94e3",
        )
app = App()
MyStack(app, "ec2instance")
app.synth()

以下是一个使用 unittest Python 模块并遵循 Python 中 TDD 通常语法的单元测试示例:

 import unittest
from your_cdk_module import MyStack  # Replace 'your_cdk_module' with the actual module name containing the MyStack class
class TestMyStack(unittest.TestCase):
    def test_ec2_instance(self):
        app = App()
        stack = MyStack(app, "test-stack")
        # Get the EC2 instance resource created in the stack
        ec2_instance = stack.node.try_find_child("ec2instance")
        # Assert EC2 instance properties
        self.assertEqual(ec2_instance.ami, "ubuntuAMI")
        self.assertEqual(ec2_instance.instance_type, "t3.micro")
        self.assertEqual(ec2_instance.key_name, "admin_key")
        self.assertEqual(ec2_instance.subnet_id, "subnet-1234567890")
if __name__ == '__main__':
    unittest.main()

如前所述,不幸的是,实例必须在 AWS 中运行,才能测试我们是否拥有通过 CDKTF 获取的期望标签和其他属性。该实例由 setUp() 函数创建,并通过 tearDown() 函数终止。在这里,我们使用的是一个 免费套餐适用 的小型实例,但对于更大的实例,会产生一定的费用。

实验性 Terraform 测试模块

最后但非常有趣的选择是使用测试 Terraform 模块。这个模块允许你编写 Terraform(HCL)代码测试,也是在相同的语言中进行测试。这将可能大大简化测试编写,因为我们之前介绍的现有选项需要使用 Golang 或 CTKTF 来编写测试。

在撰写本文时,该模块被认为是高度实验性的,但值得关注它未来的发展。

该模块的网站可以在 developer.hashicorp.com/terraform/language/modules/testing-experiment 找到。

其他值得一提的集成工具

市面上有许多其他测试工具,随着你阅读本文,更多工具还在开发中。以下是一些值得一提的工具清单,由于篇幅限制,无法对其进行详细描述:

总结来说,我们有很多可用的测试选项。对于一些人来说,这可能会感到不知所措。Terraform 代码的测试仍在开发中,类似于我们提到的其他 IaC 解决方案。在实现 CI 流水线时,最好一开始专注于简单的任务(格式检查、运行terraform plan等),然后在开发过程中逐步增加更多测试。我们意识到这是一个艰巨的任务,但我们相信,投资这些工作将帮助我们以更高的信心进行基础设施更改,并避免无意的停机。

在下一节中,我们将重点介绍各种 CD 解决方案,包括 SaaS 和自托管版本。

部署

terraform apply。这可以通过一个简单的 Bash 脚本自动完成,但难点在于如何将其集成到 CD 工具中,以确保我们不会无意中删除数据,并且可以有信心地执行。假设我们已经完成了出色的集成测试,并且有足够的信心在没有进一步用户交互的情况下执行它,我们可以启用自动运行。

之前,我们展示了使用 Jenkins、GitHub Actions,甚至一个可以嵌入到流程中的 Bash 脚本来自动化这一过程的示例。我们可以成功地使用这些解决方案将更改部署到基础设施中;然而,已经有专门的解决方案来完成这项工作。让我们从 Terraform 背后的公司——HashiCorp 的 SaaS 产品开始,来看看最流行的几种。

HashiCorp Cloud 与 Terraform Cloud

HashiCorp 是一家提供各种基础设施自动化和管理工具的软件公司。HashiCorp 提供的两个最受欢迎的产品是HashiCorp CloudTerraform Cloud

HashiCorp Cloud 是一个基于云的服务,提供一套用于基础设施自动化和管理的工具。它包括 HashiCorp 的流行工具,如 Terraform、VaultConsulNomad。使用 HashiCorp Cloud,用户可以使用与本地相同的工具来创建和管理他们的基础设施。

另一方面,Terraform Cloud 是 HashiCorp 专门推出的一款产品,专注于 IaC 工具 Terraform。Terraform Cloud 为团队提供了一个集中的地方,协作管理基础设施代码、存储配置状态以及自动化基础设施工作流。它提供了多个功能,如工作区管理、版本控制和协作工具,使团队更容易在大规模基础设施项目中协作。

HashiCorp Cloud 与 Terraform Cloud 之间的主要区别在于,前者提供一整套用于基础设施自动化和管理的工具,而后者则专注于 Terraform。

Scalr

Scalr 是一个云管理平台,为云基础架构的自动化和管理提供企业级解决方案。它由 Sebastian Stadil 于 2007 年创立,旨在简化管理多个云环境的流程。

Scalr 是一个多云管理平台。它有两种版本:商业版作为 SaaS 解决方案由母公司托管,开源版则可由您自行部署。它可以运行您的 Terraform 代码,但它还具有更多功能,例如成本分析,用于展示您正在部署的基础设施的云提供商的预估账单。它配备了 Web UI,抽象了在处理 IaC 时需要完成的大部分工作。正如我们之前提到的,它是一个多云解决方案,并且配备了集中式的单点登录SSO),可让您从一个地方查看和管理所有的云环境。它提供角色、模块注册表等功能。如果您需要的不仅仅是一个集中化的 IaC 工具,那么它是一个很好的选择。

Spacelift

Spacelift 是一个云原生的 IaC 平台,帮助开发团队通过 Terraform、Pulumi 或CloudFormation自动化和管理其基础架构。它还支持使用kubectlAnsible CaaC自动化 Kubernetes。

该平台提供多种功能,如版本控制、自动化测试和持续交付,使团队能加快基础设施部署周期并降低错误风险。Spacelift 还提供实时监控和警报,帮助轻松识别和解决可能导致停机或影响用户体验的问题。

Spacelift 由一支经验丰富的 DevOps 和基础架构工程师团队于 2020 年创立,他们意识到需要更好的方式来管理 IaC。公司自那时以来迅速发展,吸引了来自医疗保健、金融和电子商务等各行业的客户。

官方网站位于 https://spacelift.com

Env0

Env0 是一个 SaaS 平台,使团队能够通过 Terraform 自动化其基础架构和应用程序交付工作流程。它由一支经验丰富的 DevOps 工程师团队于 2018 年创立,他们意识到需要一个简化且易于使用的解决方案来管理 IaC。

Env0 提供各种功能和集成,帮助团队管理其 Terraform 环境,包括自动环境配置、与流行的 CI/CD 工具(如 Jenkins 和CircleCI)集成,以及支持 AWS、Azure 和 Google Cloud 等多个云提供商。

作为一家私人公司,Env0 不公开其财务信息。然而,他们从风险投资公司获得了重要资金支持,包括 2020 年由 Boldstart Ventures 和 Grove Ventures 领投的 350 万美元种子轮融资。

Env0 迅速确立了自己作为管理 Terraform 环境和简化 DevOps 工作流的领先 SaaS 提供商,看起来在你的环境中是一个非常有趣的选择。

官方网站可以在www.env0.com/找到。

Atlantis

Atlantis是一个开源项目,旨在通过提供简化的工作流来管理 Terraform 基础设施代码,帮助创建、审查和合并拉取请求。Atlantis 的首次发布是在 2018 年,随后在使用 Terraform 作为 IaC 工具的开发者和 DevOps 团队中获得了广泛的关注。

Atlantis 通过与现有的版本控制系统(如 GitHub 或 GitLab)集成来工作,持续监控包含 Terraform 代码更改的拉取请求。当新的拉取请求被打开时,Atlantis 会自动为这些更改创建一个新的环境,并在拉取请求中发布一个链接,指向该环境。这样,审阅者可以快速且轻松地在实时环境中查看更改并提供反馈。一旦更改被审阅并批准,Atlantis 可以自动合并拉取请求并将更改应用到目标基础设施中。

这个开源工具是我们将深入研究的对象。由于其源代码是免费的,你可以自己下载并将其部署到本地环境或公有云中。让我们在 AWS 中部署 Atlantis,并配置一个简单的基础设施来进行管理。

使用 Atlantis 进行 CI/CD

掌握 CI/CD(包括交付和部署)工具和原则的相关知识后,我们将使用 Git 和开源工具 Atlantis 创建一个 CI/CD 管道。我们将利用它自动测试并部署对 AWS 基础设施的更改,并在此过程中进行基本测试。

将 Atlantis 部署到 AWS

我们将使用 Anton Bobenko 在 GitHub 上的terraform-aws-modules项目中创建的 Terraform 模块。以下是该模块的 Terraform 注册表链接:registry.terraform.io/modules/terraform-aws-modules/atlantis/aws/latest

你可以通过两种方式使用这个模块。第一种方式是最常见的,将其用于你现有的 Terraform 代码中,将其部署到 AWS。第二种方式是我们在这个演示中将要使用的,即将这个模块作为独立项目来使用。该模块还会在eu-west AWS 区域为你创建一个新的虚拟私有云VPC),Atlantis 将在 AWS ECS 服务中运行。这将产生一些基础设施费用。

为了做到这一点,我们需要克隆 GitHub 仓库:

git clone git@github.com:terraform-aws-modules/terraform-aws-atlantis.git
Cloning into 'terraform-aws-atlantis'...
Host key fingerprint is SHA256:+Aze234876JhhddE
remote: Enumerating objects: 1401, done.
remote: Counting objects: 100% (110/110), done.
remote: Compressing objects: 100% (101/101), done.
remote: Total 1400 (delta 71), reused 81 (delta 52), pack-reused 1282
Receiving objects: 100% (1401/1401), 433.19 KiB | 1.12 MiB/s, done.
Resolving deltas: 100% (899/899), done.

接下来,我们需要创建一个 Terraform 变量文件。我们在terraform.tfvars.sample文件中有一些模板代码。让我们复制它:

cp terraform.tfvars.sample terraform.tfvars

在继续之前,确保你已经创建了一个 GitHub 仓库来存放所有的 Terraform 代码。我们将在部署 Atlantis 时为这个仓库创建一个 Webhook,但你需要在应用之前将它添加到terraform.tfvars文件中。

让我们来看一下在terraform.tfvars文件中可以更改的变量:

cidr = "10.10.0.0/16"
azs = ["eu-west-1a", "eu-west-1b"]
private_subnets = ["10.10.1.0/24", "10.10.2.0/24"]
public_subnets = ["10.10.11.0/24", "10.10.12.0/24"]
route53_zone_name = "example.com"
ecs_service_assign_public_ip = true
atlantis_repo_allowlist = ["github.com/terraform-aws-modules/*"]
atlantis_github_user = ""
atlantis_github_user_token = ""
tags = {
  Name = "atlantis"
}

atlantis_repo_allowlist是你需要更新的第一个变量,它指定了 Atlantis 能够使用的仓库。确保它指向你的仓库。route53_zone_name也应更改为类似的内容,如automation.yourorganisation.tld。请注意,它需要是一个公共域名—GitHub 将使用它来发送 Webhook 到 Atlantis 以触发构建。你需要在 Terraform 代码中创建Route53托管 DNS 区域,或者使用 Web 控制台。

你还需要更新另外两个变量,分别是atlantis_github_useratlantis_github_user_token。第一个变量不言而喻,第二个变量,你需要访问 GitHub 网站并生成你的个人访问令牌PAT)。这将允许 Atlantis 访问你想使用的仓库。为此,你需要按照 GitHub 文档页面上的指南操作:docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token

更新terraform.tfvars文件后,我们准备运行terraform initterraform plan

admin@myhome~/aws$ terraform init
Initializing modules...
# output truncated for readability- Installed hashicorp/random v3.4.3 (signed by HashiCorp)
Terraform has been successfully initialized!

Terraform 已经创建了一个名为.terraform.lock.hcl的锁文件,用于记录它所做的提供者选择。将此文件包含在你的版本控制仓库中,这样下次运行terraform init时,Terraform 会默认做出相同的选择。

现在,我们可以运行以下的terraform plan命令:

admin@myhome~/aws$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)
Terraform will perform the following actions:
# Output truncated for readability
  # aws_cloudwatch_log_group.atlantis will be created
  + resource "aws_cloudwatch_log_group" "atlantis" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + name              = "atlantis"
      + name_prefix       = (known after apply)
      + retention_in_days = 7
      + skip_destroy      = false
      + tags              = {
          + "Name" = "atlantis"
        }
      + tags_all          = {
          + "Name" = "atlantis"
        }
    }
# Removed some output for readability
Plan: 49 to add, 0 to change, 0 to destroy.

如果你看到类似的输出,你可以应用它。该模块还会返回关于已创建资源的大量信息,值得关注。

在运行terraform apply之后(这会花费几分钟),你将看到类似下面的输出:

Apply complete! Resources: 49 added, 0 changed, 0 destroyed.
Outputs:
alb_arn = "arn:aws:elasticloadbalancing:eu-central-1:673522028003:loadbalancer/app/atlantis/8e6a5c314c2936bb"
# Output truncated for readabilityatlantis_url = "https://atlantis.atlantis.devopsfury.com"
atlantis_url_events = "https://atlantis.atlantis.devopsfury.com/events"
public_subnet_ids = [
  "subnet-08a96bf6a15a65a20",
  "subnet-0bb98459f42567bdb",
]
webhook_secret = <sensitive>

如果你一切操作正确,你应该能够访问我们之前创建的atlantis.automation.yourorganisation.tld域名下的 Atlantis 网站。该模块已将所有必要的记录添加到 Route53 区域。

如果到目前为止一切顺利,当你访问atlantis.automation.yourorganisation.tld时,你将看到以下 Atlantis 面板:

图 13.1 – 使用 Terraform 模块成功部署后的 Atlantis 网站

图 13.1 – 使用 Terraform 模块成功部署后的 Atlantis 网站

在前述输出中标记为敏感的webhook_secret输出将用于在 GitHub 仓库端设置 Webhook。要查看它,你需要运行以下命令:

admin@myhome:~/aws$ terraform output webhook_secret
"bf3b20b285c91c741eeff34621215ce241cb62594298a4cec44a19ac3c70ad3333cc97d9e8b24c06909003e5a879683e4d07d29efa750c47cdbeef3779b3eaef"

我们也可以通过 Terraform 自动化这一过程,使用与 Atlantis 同一仓库中提供的模块。

这是模块的完整 URL:github.com/terraform-aws-modules/terraform-aws-atlantis/tree/master/examples/github-repository-webhook

或者,你可以通过访问 GitHub 网站并按照文档创建 Webhook 来手动进行测试:docs.github.com/en/webhooks-and-events/webhooks/creating-webhooks

记得使用 Terraform 自动生成的机密,该机密位于输出变量中——即webhook_secret

创建 Webhook 的文档在 Atlantis 文档中也有很好的描述:www.runatlantis.io/docs/configuring-webhooks.xhtml#github-github-enterprise

你可能会遇到 Atlantis 没有按预期启动,并且在访问网页面板时看到HTTP 500 error错误。为了排查与该服务相关的问题,例如 Atlantis 仍然不可用或对 GitHub webhook 响应错误,你可以进入 AWS 控制台,找到 ECS 服务。在这里,你应该能看到一个名为atlantis的集群。如果点击它,你将看到集群的配置和状态,如下图所示:

图 13.2 – Amazon Elastic Container Service (ECS) Atlantis 集群信息

图 13.2 – Amazon Elastic Container Service (ECS) Atlantis 集群信息

如果你进入任务标签页(如前述截图所示),并点击任务 ID(例如,8ecf5f9ced3246e5b2bf16d7485e981c),你将看到以下信息:

图 13.3 – Atlantis ECS 集群内任务的详细信息

图 13.3 – Atlantis ECS 集群内任务的详细信息

日志标签页将显示所有最近的事件。

你可以在CloudWatch服务中查看更详细的日志信息,进入日志 | 日志组部分并找到atlantis日志组。在这里,你可以看到包含任务所有日志的日志流。如果已经有多个日志流,你可以通过任务 ID快速跟踪正确的日志流:

图 13.4 – 包含运行 Atlantis 的 ECS 任务日志流的 CloudWatch 日志

图 13.4 – 包含运行 Atlantis 的 ECS 任务日志流的 CloudWatch 日志

如果到目前为止一切正常,我们准备测试 Atlantis 是否能运行terraform planterraform apply。让我们回到代码中。

使用 Atlantis 运行 Terraform

要执行 terraform plan,我们需要创建一个新的 main.tf 文件,内容如下:

# Configure the AWS Provider
provider "aws" {
  region = var.region
}

上述代码配置了 AWS 提供程序,以使用 region 变量中指定的区域。

这个 Terraform 代码块配置了所需的 Terraform 版本以及 Terraform 状态文件的位置。在这个示例中,我们使用的是本地存储,但在生产环境中,我们应该使用远程状态位置。

terraform {
  required_version = ">=1.0"
  backend "local" {
    path = "tfstate/terraform.local-tfstate"
  }
}
resource "aws_s3_bucket" "terraform_state" {
  bucket        = "terraform-states"
  acl           = "private"
  force_destroy = false
  versioning {
    enabled = true
  }

上述代码块定义了一个 S3 桶,用于存储 Terraform 状态。它是一个启用了版本控制的私有 S3 桶。这是存储状态文件的推荐设置。

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }
}

上述代码还为 S3 桶配置了服务器端加密SSE)。

resource "aws_dynamodb_table" "dynamodb-terraform-state-lock" {
  name           = "terraform-state-lock"
  hash_key       = "LockID"
  read_capacity  = 1
  write_capacity = 1
  attribute {
    name = "LockID"
    type = "S"
  }
  tags = {
    Name = "DynamoDB Terraform State Lock Table"
  }
}

上述代码块定义了一个 DynamoDB 表,用于 Terraform 状态锁定。variables.tf 文件将只包含一个变量:

variable "region" {
  type    = string
  default = "eu-central-1"
}

在将这些文件添加到 Git 并提交更改后,创建一个新的 GitHub 拉取请求,你将能够请求 Atlantis 为你运行计划并应用更改。如果你运行计划,Atlantis 将锁定你修改的模块,其他人将无法对其应用任何更改,除非你解锁或应用自己的更改。

要为你的新拉取请求触发计划,只需在拉取请求中添加评论,内容为 atlantis plan。稍后,根据模块的大小,你将得到类似下面屏幕截图中的计划输出:

图 13.5 – 在 GitHub 上与 Atlantis 的交互

图 13.5 – 在 GitHub 上与 Atlantis 的交互

在撰写本文时,Atlantis 不支持自动应用更改。然而,可以在 CI 级别实现自动化。例如,使用 GitHub 时,可以创建一个 GitHub Action,在测试成功后,添加 atlantis apply 评论,触发 Atlantis 应用更改并返回状态。

到这时,我们可以赋予开发人员修改基础设施的权限,而不需要直接允许他们在本地机器上运行 Terraform。同时,我们也消除了多个用户同时应用更改的可能性,未使用分布式锁机制时,这种做法可能会非常破坏性。此外,使用 Terraform 将变得更加简单,因为不需要在本地机器上安装它,也不需要直接访问我们的 AWS 账户,我们将获得对基础设施更改的更多可视性。

使用 Terraform 构建 CI/CD 仍然任重道远。基础设施即代码(IaC)在测试功能方面仍然落后于其他编程语言,但许多开发人员正在为此努力。我们期待着让每个人都能更轻松地测试基础设施。

总结

本章中,我们探讨了使用 Terraform 进行基础设施即代码(IaC)的好处,并讨论了在 Terraform 工作流中纳入 CI/CD 流程的重要性。我们涵盖了基础设施测试以及各种自动化部署的工具。

在最后一节中,我们解释了如何将 Atlantis(一个用于自动化 Terraform 拉取请求预览的开源工具)部署到 AWS,并配置 GitHub 以触发terraform planterraform apply。通过 Atlantis,Terraform 用户可以通过 GitHub 拉取请求协作进行基础设施变更,使得在将变更应用到生产环境之前可以进行审核和批准。通过将 Atlantis 融入 Terraform 工作流,你可以改善协作,减少错误,并实现更快速、更安全的基础设施变更。

在最后一章中,我们将放慢节奏,讨论 DevOps 的误解和反模式,以及如何避免它们。

练习

尝试以下练习来测试你对本章内容的理解:

  1. 尝试通过遵循www.runatlantis.io/guide/testing-locally.xhtml上的文档来本地部署 Atlantis。

  2. 创建一个仓库并为自己配置 Webhook 和 PAT。为你的新仓库运行计划(提示:你可以使用null 资源进行测试,而不是 AWS 资源)。

  3. 在其中一个 CD 解决方案网站上创建一个帐户,尝试使用该 SaaS 运行计划。通常,公共仓库有免费的计划。

第十四章:避免 DevOps 中的陷阱

本章的重点是 DevOps 中的陷阱和反模式,这些问题可能会妨碍 DevOps 实践的成功实施。我们将强调采用协作文化的重要性,优先考虑持续改进,并讨论各种常见的陷阱,如忽视测试和质量保证QA)、过度依赖自动化、监控和反馈环节不足、安全和合规措施不充分、以及缺乏可扩展性和灵活性。

我们还将强调适当的文档和知识共享的重要性,并讨论克服变革抵抗的策略。通过突出这些常见的陷阱和反模式,本章旨在为组织提供指导,帮助它们成功实施 DevOps 实践并避免常见的错误。这些也是组织在技术部分之外最常遇到的挑战。

本章将涵盖以下主题:

  • 自动化过多或过少

  • 不理解技术

  • 没有采用协作文化

  • 忽视测试和 QA

  • 监控和反馈环节不足

  • 安全和合规措施不充分

  • 缺乏可扩展性和灵活性

  • 缺乏适当的文档和知识共享

  • 克服对变革的抵触

技术要求

本章没有技术要求。它更像是一种讨论,并没有提供需要在系统上执行的具体指令。

自动化过多或过少

自动化是 DevOps 的核心原则。说实话,自动化是使我们的工作更轻松、更高效和更有趣的最佳方式。

但是,组织有时可能会过度依赖自动化,从而导致缺乏人类的监督和责任。如果你自动化了太多的内容,你就会错过一些可以通过人工审核发现的错误,尤其是在流程中没有嵌入这些检查时。这也是为什么我们有同行评审流程来确保我们不会错过任何测试或任何集成测试工具没能发现的问题。这也是为什么许多组织更愿意在terraform apply过程真正部署之前手动签署它。

另一方面,如果你什么都不自动化,你就会暴露于意外错误中,因为我们人类在处理枯燥且可重复的任务时并不擅长。而这正是重点:识别可重复的任务 以进行自动化

为了识别可以自动化的任务,我们建议先关注那些易于实现的低挂果任务。考虑到这一点,让我们识别出最容易自动化的任务,以确保成功的自动化策略。

请考虑以下清单,它按从最简单到最困难的策略排列,帮助你绕过自动化问题:

  • 重复性任务

  • 耗时的任务

  • 手动且容易出错的任务

  • 集成版本控制的任务

  • 具有可重复模式的任务

  • 拥有明确定义 API 或接口的任务

  • 拥有明确且定义清晰需求的任务

现在让我们来看看这些策略。

重复性任务

寻找那些重复性强并且可以通过一组一致步骤执行的任务。这些任务是自动化的理想候选,因为它们可以节省时间并减少人为错误的风险。这些任务包括使用 Terraform 配置和管理 AWS 资源、备份作业、创建和管理 GitHub 仓库,或者通过GitHub Actions设置构建和部署流水线,这些任务都可以自动化,从而简化 DevOps 工作流程。

耗时的任务

寻找那些耗时且能从自动化中受益的任务。这些任务包括长时间运行的任务,如数据同步、编译、构建 Docker 镜像以及安全审计,这些都可以通过许多工具完成,包括商业的 SaaS 模式工具或开源工具(例如,Prowler就是一个例子)。

手动且容易出错的任务

确定在手动执行时容易出错的任务。这些任务通常涉及多个步骤或配置,可能既繁琐又容易出错。使用 Python 脚本或基础设施即代码IaC)工具,如 Terraform,来自动化这些任务可以帮助减少人为错误,并确保环境之间的一致性。用清晰的代码自动化这些任务还具有文档化的好处。你常常听说代码即文档,尤其在这种情况下,这句话是真的。

与版本控制系统集成的任务

确定那些可以与版本控制系统(如 GitHub)集成的任务。例如,GitHub Actions 提供了一个强大的自动化框架,可以根据 GitHub 仓库中的事件(如代码推送、拉取请求或问题更新)自动触发工作流。这使得你能够自动化诸如构建和部署应用程序、运行测试或创建文档等任务,作为 DevOps 工作流程的一部分。

具有重复模式的任务

确定那些遵循可重复模式或可以模板化的任务。这些任务可以通过 Python 脚本、Terraform 模块或 GitHub Actions 模板来自动化。例如,创建多个环境(如开发、测试、生产)中的相似 AWS 资源、管理具有相似设置的多个 GitHub 仓库,或者将相同的应用部署到多个 AWS 账户等任务,可以通过模板或脚本进行自动化,从而减少重复性工作并提高效率。

拥有明确 API 或接口的任务

确定那些有良好文档的 API 或接口的任务。这些任务可以通过 Python 库、Terraform 提供程序、AWS SDK 或 GitHub API 轻松自动化。例如,AWS 为多种编程语言提供了全面的 SDK,包括 Python,这使得自动化诸如管理 AWS 资源、配置 AWS 服务或监控 AWS 资源等任务变得容易。

具有明确且明确定义要求的任务

寻找那些有明确且定义良好的需求、输入和预期输出的任务。这些任务更容易自动化,因为它们可以在脚本、模板或配置中被精确定义。例如,使用 Terraform 配置 AWS 资源、设置 AWS CloudFormation 堆栈或配置 GitHub 仓库设置等任务,可以通过声明性 IaC 模板或配置文件来实现自动化。

通过考虑这些标准,你可以识别出在组织中哪些任务最容易实现自动化。请记住,这通常是一个过程,可能需要几个月甚至几年时间。作为经验法则,尽量创建涉及许多小的(原子化)步骤的解决方案,以便构建一个更复杂的系统,而不是一开始就构建复杂的解决方案。例如,部署过程可以拆分为更小的步骤,如设置环境、构建、测试、上传工件以及创建更新应用程序版本的清单,最终实现服务器端的部署。

自动化常见任务将使你的工作更轻松、更令人满意,但如果不了解背后的技术以及你正在使用的技术如何运作,可能会导致一些无法预见的问题。

不理解技术

你不需要知道电视如何工作就能使用它。但了解其背后的原理、输入接口、输出是什么等内容是必要的。同样,面对任何其他技术,你不需要 100% 理解它是如何运作的,但你需要了解其核心用例是什么,它的目的是什么。

作为一名 DevOps 专业人士,深入理解支持现代软件开发和运维的底层技术至关重要。从数据库到消息队列,从通知到块存储和对象存储,每一项技术都在构建和维护可靠、可扩展的软件系统中发挥着至关重要的作用。然而,DevOps 中最常见的陷阱之一就是没有完全掌握这些常见任务背后的技术。

为什么理解技术如此重要? 答案很简单——DevOps 不仅仅是通过工具和自动化来简化软件开发和部署过程。它是关于理解这些工具如何工作,它们做了什么,以及它们如何相互作用。没有这种理解,DevOps 实践可能变得肤浅和低效,导致次优结果和更高的故障和停机风险。记住:DevOps 是一种工作方式。工具只是工具——帮助你日常工作的工具。理解你的工具将使你能够高效、有效地使用它们,并根据你的需求进行调整。

举个例子,假设你被指派为一个新应用程序设置一个高可用和高性能的数据库。如果没有对数据库技术的深入理解,团队可能仅依赖默认配置或沿用过时的做法,导致性能差、数据丢失,甚至系统崩溃。另一方面,一个对数据库原理有深入理解的团队则能做出明智的决策,关于数据建模、索引、缓存、复制等关键方面,进而实现一个强大且可扩展的数据库架构。

同样,理解消息队列、通知、块存储和对象存储等技术,对于设计和实现可靠高效的通信模式、数据处理管道和存储策略至关重要。这让你能够优化系统性能、确保数据完整性,并为未来的增长做好规划。

另一方面,你不一定需要了解实现的细节。对于数据库,你不必担心其源代码。

那么,如何能快速学习新的和未知的概念呢?以下是一些你可以遵循的建议:

  • 保持好奇心和积极性

  • 从基础开始

  • 动手学习

  • 协作与分享知识

  • 保持更新

让我们详细看看这些建议。

保持好奇心和积极性

拥抱成长型思维,并主动寻找学习的机会。不要等到问题出现才去深入了解新技术。保持好奇心,探索文档、教程和在线资源,主动在安全可控的环境中尝试不同的工具和技术。我们无法强调这一点——实验是非常重要的。作为 DevOps,说“总是这么做”的说法是最糟糕的。没有沙箱环境和测试新配置、新工作流、新工具,你无法改进基础设施和管道。

从基础开始

不要被复杂的概念吓倒。从基础开始,并逐步建立你的理解。熟悉你想学习的技术的基本原理、术语和概念。一旦你有了扎实的基础,你就可以逐渐深入了解更高级的话题。

动手学习

理论很重要,但动手实践是无价的。建立一个沙箱环境,尝试不同的配置,构建小项目或原型,将所学应用到实践中。通过实践学习将帮助你获得实际技能,并加深对技术的理解。

协作与分享知识

DevOps 是一个协作领域,从同事那里学习是非常宝贵的。与团队互动,参与在线社区,参加聚会或会议,并与他人分享你的知识。教授他人是加深自己理解和从不同角度学习的有效方法。

保持更新

技术不断发展,保持对最新趋势、最佳实践和领域更新的了解至关重要。你可以关注行业博客,订阅时事通讯,参与相关论坛或社区,及时掌握最新动态。核心技术通常保持不变,但使用案例、使用方式以及与技术的互动始终在变化。你知道吗,你可以使用 RESTful API (postgrest.org/en/stable/) 使 PostgreSQL 数据库可用?或者,使用 Multicorn 扩展(multicorn.org/),你可以通过一个端点查询(甚至连接多个数据源,如 Twitter)?

我们喜欢把技术看作是创新和提高团队生产力的最终游乐场。如果它不有趣,为什么要做呢?说到这一点,想想与一个只处理自己任务碎片的孤岛团队合作的情景,这肯定不会有趣。在 DevOps 中,协作是基本原则之一,协作也是你组织文化的一部分。

未能采纳协作文化

DevOps 完全是关于协作并打破团队之间的孤岛。然而,许多组织在采纳协作文化方面存在困难,导致沟通不畅、延误,最终项目失败。讨论 DevOps 中协作的重要性,以及缺乏协作如何导致项目偏离轨道,显然是另一个章节的好主题。

孤岛效应指的是各个团队或部门之间缺乏有效沟通与合作,导致协调不畅,从而影响整体的生产力和效率。组织未能在 DevOps 中建立协作文化的几个可能原因包括:缺乏领导力、从一开始就存在的孤岛结构、缺乏信任以及沟通不足。我们来逐一看看这些原因,并尝试找到一个好的解决方案。

缺乏领导力

一个常见的挑战是领导层没有优先考虑或积极推动协作文化。这可能导致团队只专注于各自的任务,而忽略更广泛的组织目标。为了解决这一问题,必须确保领导层支持 DevOps 实践,包括促进协作文化的建立。领导者应该通过积极推动协作、确立共同目标,并为跨团队合作提供必要的资源和支持来定下基调。

优秀的领导者稀缺,因此在组织内识别潜在领导者并支持他们的成长与发展作为个人及领导者是一个好主意。仅仅提拔一个之前只是贡献者的人,会导致他们失败,因为他们还没有获得执行这项工作的必要工具。

孤岛式组织结构

拥有层级化和信息孤岛结构的组织可能会阻碍协作。团队可能会在孤立的信息孤岛中运作,拥有自己的目标、流程和工具,导致跨团队的可见性和协调性不足。为了克服这一点,组织应重新结构,以促进拥有端到端应用或服务的跨功能团队。创建由不同部门(如开发、运营和质量保证)代表组成的多学科团队可以促进协作,实现更好的沟通和协调。

另一个非常有效的策略是建立协作文化。这涉及促进重视开放沟通、透明度和团队合作的思维方式。鼓励跨功能团队共同工作、分享信息并在项目上进行协作。认可和奖励协作,并为跨不同团队和部门的知识分享和学习创建论坛。

您还可以创建共享的目标和度量标准。让团队定义与整体业务目标一致,并要求跨团队协作的共享目标和度量标准。这将鼓励他们共同努力实现共同的结果,并帮助他们看到超越个人信息孤岛的更大图景。定期在跨功能会议上审查向这些共享目标和度量标准的进展,以促进责任和对齐。

此外,领导在打破信息孤岛中扮演着至关重要的角色。领导者应该以身作则,积极促进团队之间和部门之间的协作、沟通和对齐。这包括为协作设定明确的期望,认可和奖励协作行为,以及提供支持和资源以促进跨功能的协作。

缺乏信任和沟通

缺乏信任和有效沟通,协作可能会受到影响。团队可能因为害怕批评或竞争而不愿分享信息或想法,从而导致信息孤岛的产生。建立信任和开放沟通文化至关重要。这可以通过定期团队会议、跨团队研讨会以及促进团队成员在不受评判恐惧的环境中分享其观点和想法来实现。鼓励开放和透明的沟通渠道,如聊天平台或协作文档工具,也可以促进沟通和协作。

在团队和团队成员之间建立信任对于有效的协作和沟通至关重要。信任是建立健康关系和成功团队工作的基础。在其他书籍中学习的不同策略中,最有效的是:建立明确的期望、促进开放式沟通、提升透明度、分享知识和建立个人联系。

让我们来详细分析一下:

  • 明确定义每个团队和团队成员的角色、责任和期望:这有助于避免误解,促进责任感。确保期望是现实的、可实现的,并与组织的整体目标一致。根据需要定期审查和更新期望。

  • 开放和包容的沟通:创造一个安全和包容的环境,让团队成员能够舒适地表达他们的想法、意见和担忧,而不必担心被评判或报复。鼓励积极倾听并尊重多样化的观点。避免在问题出现时归咎或指责,专注于协作解决问题。

  • 培养透明文化:在团队和团队成员之间公开、一致地共享信息,包括与项目、流程和目标相关的更新、进展和挑战。透明的沟通通过确保每个人都能访问相同的信息并保持一致,从而建立信任。

  • 协作思维:为了鼓励协作和知识共享,你需要培养一种协作思维,促使团队及其成员积极协作并相互分享知识。鼓励跨职能协作、结对编程和跨培训机会。创造工作之外的空间,赞助活动,并鼓励团队成员分享他们的知识和经验。认可并奖励协作行为,以强化其重要性。

  • 主动解决冲突:冲突在任何团队或组织中都是不可避免的,但如果不及时处理,冲突会削弱信任。鼓励团队成员以建设性和及时的方式解决冲突。提供冲突解决工具和资源,例如调解或主持讨论,帮助团队解决冲突并重建信任。

  • 领导者的角色:领导者在建立信任方面发挥着至关重要的作用。领导者应以身作则,展示开放和公正的沟通方式,积极倾听团队成员的意见,并通过自己的行动和决策展现可信度。领导者还应鼓励建立信任的行为,并对在团队内部及跨团队之间建立信任负责。

  • 定期提供反馈和认可:公开表扬他人的努力和成功,并在私下给予建设性反馈以帮助他们改进。这有助于建立积极的反馈循环,促进信任,并鼓励开放的沟通。

还有许多策略可以用来促进团队成员之间的信任,从而改善沟通。

还有一个常见的陷阱,最终会在你的组织中形成壁垒。它发生在你忽视组织的文化方面,单纯专注于工具时。

以工具为中心的方式

组织可能过于侧重于实施 DevOps 工具,而忽视了其背后的文化因素。虽然工具很重要,但它们不能替代协作文化。采用以工具为中心的方法可能导致团队孤立工作,仅依赖自动化流程,这会妨碍有效的协作。为了克服这一点,组织应首先优先构建协作文化,然后选择与其文化契合并促进协作的工具。提供培训和支持,确保团队能够熟练使用所选工具来促进协作,是至关重要的。

为了纠正这一点,你可以使用已经讨论过的策略,并额外提供共享的沟通渠道,鼓励并且自己组织跨团队的会议和活动,提供培训和资源(特别是时间)。

一个最终的优秀纠正方法是促进跨团队的角色和责任。定义并鼓励那些促进协作的跨团队角色和责任。这可以包括像联络员或大使这样的角色,帮助促进团队之间的沟通和协调。这些角色可以帮助弥合团队之间的差距,通过作为信息共享的渠道来推动协作。

软件质量保证(QA)也常常被忽视。与文化一样,质量需要在组织层面和团队层面有意识地发展和鼓励。

最好通过一个例子来解释前述方法,而在 Linux 世界中有一个完美的例子:Linux 内核项目

它在 1991 年 8 月 25 日以一篇帖子在新闻组中开始,帖子内容如下:

“我正在为 386(486) AT 克隆机做一个(免费的)操作系统(只是一个爱好,不会像 GNU 那样大规模和专业)。这个项目自四月以来一直在酝酿,现在开始准备好了。我希望能收到关于人们在 minix 中喜欢/不喜欢的反馈,因为我的操作系统在某些方面与它相似(例如文件系统的物理布局(出于实际原因)等)。目前我已经移植了 bash(1.08)和 gcc(1.40),目前看起来一切正常。这意味着我将在几个月内做出一些实际的成果[...] 是的——它完全没有使用任何 minix 代码,并且拥有一个多线程文件系统。它并不是可移植的[原文如此](使用 386 任务切换等),而且它可能永远只支持 AT 硬盘,因为我只有这一种硬盘 😦。”

正如你所见,从一开始,Linux 的创始人林纳斯·托瓦兹就邀请其他爱好者加入他的这个小项目,并帮助开发它。这种合作精神从第一天就展现出来。每个人都可以加入该项目,他们的贡献会在技术层面上进行评估。沟通的方式是一个开放的公共邮件列表,名字非常贴切——Linux 内核邮件列表LKML),在这里讨论着项目相关的路线图、补丁、新点子以及所有内容。每个人都可以阅读列表的档案,加入邮件列表和讨论。

虽然讨论几乎对所有人开放,补丁(或拉取请求)可以由任何人提交(尽管是否接受是另外一回事,因为项目必须遵循代码质量、法律和许可要求,我们将在此不做讨论),但仍然存在一种领导层等级,尽管它相当扁平。内核子系统有维护者,这些人负责决定新代码是否会被接受或拒绝。最终的代码合并到内核是由林纳斯·托瓦兹完成的,他在代码接受过程中拥有最终发言权,尽管他通常依赖子系统的维护者来做出决定。

上述结构本质上使 Linux 内核项目免于孤立的管理结构,因为管理层级不多。所有的知识都是公开和自由获取的;项目管理链中的每个人都可以轻松联系。

Linux 内核的源代码公开存放在一个 Git 仓库中。任何人都可以克隆和修改内核,只要他们不违反 Linux 内核发布所使用的许可协议。

沟通和信任是项目采用开放沟通模型和开源代码库的直接结果。没有“幕后交易”的沟通;决策是基于技术的,因此可以信任开发人员和领导。

忽视测试和质量保证

测试和质量保证是任何 DevOps 工作流程中的关键组成部分,然而许多组织未能优先考虑这些因素,导致了软件的缺陷、用户的不满以及收入的损失。在 DevOps 的世界里,软件开发与运维紧密结合,测试和质量保证是开发过程中的关键组成部分。忽视这些方面可能会导致各种问题,这些问题会对软件开发项目产生严重后果。让我们探索忽视测试和质量保证可能带来的一些陷阱,并提出解决方案:

  • 增加了软件缺陷

  • 部署失败

  • 安全漏洞

  • 缺乏文档

  • 测试覆盖不足

  • 缺乏持续改进

让我们详细检查这些陷阱。

增加了软件缺陷

没有适当的测试和质量保证,软件缺陷可能——而且通常会——被忽视,导致质量较差的软件进入生产环境。这可能导致客户投诉增多、用户满意度下降以及收入损失。

实施全面的测试过程至关重要,包括单元测试、集成测试和端到端E2E)测试,以在开发生命周期的不同阶段识别和修复缺陷。从基本的代码格式检查linting)、静态代码分析开始,逐步在工作流中添加更多测试。除非你愿意为你的应用程序编写所有测试,否则与开发人员合作是非常必要的。

部署失败

如果没有彻底的测试和质量保证,软件部署可能会失败,导致系统停机并干扰业务运营。这可能导致财务损失、声誉损害和客户流失。为了避免部署失败,至关重要的是建立自动化测试和部署管道,在将代码发布到生产环境之前进行严格的测试和质量保证检查。这有助于及早发现问题,并确保只有稳定可靠的软件被部署。

安全漏洞

忽视质量保证和测试可能使软件面临安全威胁,例如代码注入、跨站脚本攻击XSS)以及其他类型的攻击。这可能导致数据泄露、合规性违规和法律责任。为了解决这个问题,安全测试应作为测试和质量保证过程的一个核心部分。包括漏洞评估、渗透测试和其他安全测试技术,以识别和修复软件中的安全缺陷。

缺乏文档

适当的文档对于维护软件质量、促进故障排除、维护以及未来开发至关重要。忽视质量保证(QA)和测试可能导致文档不完整或过时,从而使理解和维护软件变得困难。为了缓解这一问题,文档应该被视为测试和质量保证过程中的一个重要交付物。

文档应定期更新,以反映开发和测试过程中所做的更改,并且应易于开发和运维团队访问。为了实现这一点,文档应尽可能靠近代码,以便在更新代码时能够轻松更新文档。技术文档(例如类、代码接口等)应当实现自动化,并且对所有相关人员开放。

测试覆盖不充分

如果没有适当的测试和 QA,可能会出现测试覆盖范围的空白,导致未经过测试或测试不充分的代码。这可能导致预料之外的问题和缺陷进入生产环境。为了解决这个问题,必须建立明确的测试目标,定义测试覆盖标准,并使用代码覆盖率分析工具,确保所有关键代码路径都得到彻底测试。

缺乏持续改进

忽视测试和质量保证(QA)可能导致一种自满的文化,其中质量未被优先考虑。这可能导致软件开发过程中缺乏持续改进,从而随着时间的推移,软件质量下降。为了解决这个问题,必须建立一种持续改进的文化,其中测试和 QA 的反馈被用来识别和解决过程中的空白,改进测试实践,并提升整体软件质量。

为了避免这些陷阱,必须实施全面和自动化的测试流程,优先进行安全测试,保持文档的最新性,确保充足的测试覆盖率,并培养持续改进的文化。通过解决这些挑战,组织可以确保交付符合客户期望的高质量软件,推动业务成功,并带来愉快的团队——我们可不能忘了这一点。

虽然质量保证(QA)会尽力捕捉进入生产系统的任何错误和缺陷,但没有什么能比得上良好的监控和告警。

差的监控和反馈回路

监控和反馈回路对于识别问题并改进 DevOps 工作流程至关重要,但许多组织未能实施有效的监控和反馈机制。

在 DevOps 环境中,反馈回路是指软件开发和运维生命周期中不同阶段之间信息的持续交换。它涉及数据收集、分析,并提供推动开发和运维过程改进的见解。

反馈回路在帮助团队早期识别和纠正软件交付生命周期中的问题方面发挥着至关重要的作用,这将导致更快的开发周期、提高的质量以及增加的值班人员满意度,因为他们不会在夜间被叫醒。

良好监控的特点是能够提供及时、准确且相关的信息,关于系统的健康状况、性能和行为。良好监控的关键特点包括多个方面,如下所示:

  • 实时

  • 全面

  • 可扩展

  • 可操作

  • 持续改进

让我们来拆解一下。

实时

良好的监控提供了对系统状态的实时可见性,使团队能够在问题升级为关键问题之前快速检测和解决问题。实时监控有助于识别异常、趋势和模式,这些可能表明潜在的问题或瓶颈,从而使得主动排查和解决问题成为可能。

综合性

良好的监控涵盖系统的所有关键组件,包括基础设施、应用程序、服务和依赖关系。它提供了整个系统的全面视图,帮助团队理解不同组件之间的关系和依赖性,以及它们对系统性能和可用性的影响。

此外,监控需要能够仅提供相关信息,而不仅仅是枯燥的警报数据。例如,如果服务器宕机,良好的监控不会发送关于 CPU 或 RAM 使用率、服务宕机等的警报。核心问题是服务器无法响应——将不相关的信息发送给值班团队会导致响应时间变慢。

可扩展的

良好的监控能够处理大量数据,并且能够横向扩展以适应系统不断增长的需求。它可以从多个来源收集和处理数据,并将其与不同的工具和技术进行集成,提供系统健康状况的统一和综合视图。

可操作的

良好的监控提供可操作的见解,使团队能够做出明智的决策并采取及时的行动。它包括丰富的可视化、报告和分析功能,帮助团队识别趋势、关联和异常,并采取适当的措施来优化系统性能和可用性。

持续改进

除了我们在本节中讨论的内容,还需要补充的是,从良好监控到卓越监控的转变需要持续审查当前状态并实施改进。

如果您的组织变化迅速,应每月进行此审查;如果您已经建立了良好的监控系统并且它正常运作,则每季度进行一次。此审查包括以下内容:

  • 审查上一个周期内最常触发的警报,并将警报与之前的周期进行比较

  • 审查新增的监控指标,以检查其相关性

  • 审查长时间没有触发的警报(3-4 个审查周期)

有了这些数据,你可以决定哪些警报增加了噪音,确保新的指标确实是你期望被监控的内容,并审查那些没有触发的警报,这将让你意识到可能根本不需要监控的指标。

尽管在讨论监控时,我们通常关注响应时间或内存消耗等方面,但它不仅仅是这些。你可以追踪软件代码中类之间的交互、系统和数据库之间的延迟,或者可以衡量安全性。如果你不监控你的安全态势,会发生什么?系统中会出现漏洞,而你甚至不知道它们的存在。让我们来看看安全和合规性措施。

安全性和合规性措施不足

安全性和合规性是任何团队的重要关注点,但许多组织未能充分解决这些问题。在 DevOps 的世界中,安全性是一个至关重要的方面,必须将其集成到软件开发生命周期的每一个环节。然而,许多组织仍在安全性和合规性措施上存在不足,这可能导致严重后果,如数据泄露、监管罚款和声誉损害。本章将探讨 DevOps 中关于安全措施的常见误解和陷阱,并讨论组织应努力实现的良好安全措施的特征。

我们将讨论以下方面:

  • 什么是安全措施?

  • 良好安全措施的特征

什么是安全措施?

安全措施指的是用于保护软件系统、应用程序和数据免受未经授权访问、泄露或其他安全威胁的实践、流程和工具。在 DevOps 的背景下,安全措施贯穿整个软件开发流程,从代码创建和测试到部署和操作。

DevOps 中的常见安全措施包括以下内容:

  • 身份验证和授权确保只有授权用户可以访问系统,并且他们拥有执行任务所需的适当权限。

  • 加密敏感数据,防止未经授权的用户拦截或访问

  • 定期扫描软件组件中的漏洞,并应用补丁或更新以修复它们

  • 监控和记录系统内的活动,以便发现和调查安全事件

  • 网络安全,包括实施防火墙、入侵检测系统IDS)和/或入侵防御系统IPS),以及虚拟私人网络VPN)以保护网络免受外部威胁

  • 审查代码中的安全漏洞,并使用静态分析工具识别潜在的弱点,防止泄露密码和访问令牌等敏感信息

在实施了一些安全措施后,我们需要确保这些措施的质量尽可能高,同时满足组织需要遵守的法律合规性要求。让我们来看一下安全措施的特征。

良好安全措施的特征

DevOps 中有效的安全措施应具备某些特征,以确保它们能为软件系统和数据提供足够的保护。

良好的安全措施是主动的而非被动的,这意味着它们的设计目的是防止安全漏洞,而不仅仅是在事后发现并减轻这些漏洞。主动的安全措施可能包括定期的漏洞评估、代码审查和自动化测试,以在漏洞成为关键问题之前发现并修复安全问题。

安全措施还应涵盖软件开发生命周期的各个方面,从代码创建和测试到部署和操作。这包括保护开发环境、代码库、构建和部署过程,以及部署和操作软件的生产环境。

可扩展性是良好安全措施的另一个特征,意味着它们可以应用于不同类型的软件应用程序、环境和技术。它们应该足够灵活,以适应组织不断变化的需求和不断发展的威胁格局。

利用自动化实现快速且一致的安全检查和响应是谈论 DevOps 实践时始终重复的主题。自动化可以帮助及时高效地识别安全漏洞、应用补丁并强制执行安全策略,从而减少人为错误的风险。

许多组织需要遵守行业法规、标准和最佳实践。遵守相关法规,如通用数据保护条例GDPR)、健康保险流通与责任法案HIPAA)或支付卡行业数据安全标准PCI DSS),对于避免与不合规相关的法律和财务风险至关重要。即使你的组织未受这些标准的监管,选择最合适的标准并在合适时遵循它,仍然是一个好主意。这将使你的安全级别高于不遵循任何标准的情况。

最后,所有安全措施都需要不断更新和改进,以应对新出现的安全威胁和技术。威胁格局在不断变化,安全措施必须具有敏捷性和适应性,才能有效防范新的漏洞和攻击。

一如既往,选择最简单的任务开始,建立团队中的良好安全措施。要经常回顾并逐步改进。

缺乏可扩展性和灵活性

许多组织未能设计出可扩展和灵活的 DevOps 工作流,导致项目在规模或复杂性增长时出现问题,尽管这两者是 DevOps 至关重要的方面,因为它们使组织能够响应不断变化的业务需求并高效处理日益增加的工作负载。然而,你可能会忽视这些因素,从而导致严重的误解和陷阱。让我们深入探讨 DevOps 中可扩展性和灵活性的重要性,并探讨一些常见的误解和陷阱,例如以下几点:

  • DevOps 只适用于小型团队或项目

  • 无法扩展基础设施

  • 灵活性妥协了稳定性

  • 发布管理中缺乏灵活性

让我们详细看看这些误解和陷阱。

DevOps 只适用于小型团队或项目

一个常见的误解是 DevOps 只适用于小团队或小项目。一些组织认为,大团队或大项目不需要 DevOps 实践,因为他们认为传统的开发和运维方式可以处理规模问题。

实际上,DevOps 不仅限于团队或项目的规模。它是一套可以应用于各种规模组织的原则和实践。事实上,随着团队和项目的发展,DevOps 的需求变得更加关键,以确保顺畅的协作、更快速的交付和高效的运营。

无法扩展基础设施

很容易忽视基础设施的可扩展性,这可能导致系统故障、性能问题和未计划的停机时间。

低估 DevOps 过程对基础设施的需求将导致未来出现许多问题,从糟糕的用户体验到错失组织盈利机会。例如,在资源有限或容量不足的环境中部署应用程序,当工作负载增加时,可能会导致性能问题和系统故障。同样,未规划未来的增长或业务需求变化可能会导致需要进行昂贵且耗时的基础设施升级或迁移。这对本地部署和云基础设施都适用。此外,由于全球大流行导致的电子设备短缺,AWS 和其他云服务商有时可能缺少硬件资源。当你尝试为基础设施添加更多资源时,这可能会影响你的组织。对于本地部署,你可以更紧密地控制硬件资源。

为了避免这个陷阱,团队应该仔细评估其应用程序和基础设施的可扩展性需求,规划未来的增长,并确保基础设施被设计和配置为有效应对增加的工作负载。这可能涉及采用 IaC、自动化配置和水平扩展等实践,这些可以使团队迅速且轻松地扩展其基础设施,以应对变化的需求。对于云部署,你可能需要预先支付一些容量来保留资源。

灵活性妥协稳定性

另一个关于 DevOps 的误解是灵活性妥协稳定性。一些组织担心,在开发和运维过程中引入灵活性可能导致不稳定的环境,从而增加风险和漏洞。因此,他们可能会采取僵化的 DevOps 方法,强调稳定性而非灵活性。

然而,这种误解可能会妨碍 DevOps 旨在实现的敏捷性和创新性。灵活性在 DevOps 中至关重要,因为它使团队能够快速响应不断变化的业务需求,尝试新想法,并对应用程序和基础设施进行迭代。事实上,DevOps 实践如持续集成持续部署CI/CD)以及自动化测试,旨在确保在部署到生产环境之前,所有的变更都经过充分的测试和验证,从而在确保稳定性的同时,能够实现灵活性。

发布管理缺乏灵活性

一个常见的陷阱是发布管理中的灵活性不足。发布管理涉及将变更部署到生产环境的过程,如果你采用一种僵化的方式来处理发布,可能会导致延迟、复杂性增加以及风险增加。

例如,遵循固定的发布计划或僵化的变更管理流程可能会妨碍快速响应业务需求或客户反馈的能力,导致错失机会或客户满意度下降。同样,不允许进行实验或快速回滚选项会限制对变更进行迭代的能力,并且无法迅速解决生产环境中可能出现的问题。

为了避免这种情况,你应该专注于建立灵活且敏捷的发布管理流程。这可能涉及实施一些实践,如功能开关、暗启动、金丝雀部署和蓝绿部署,它们可以实现渐进式和可控的变更推出,并在出现问题时提供回滚选项。此外,采用自动化发布管道、版本控制和监控可以帮助团队获得发布过程的可见性和控制。

构建灵活且可扩展的系统并非易事。更重要的是,你还需要考虑来自业务角度的变更,这些变更会影响你当前的流程。如果你的流程难以变更,或者你无法根据高流量对系统进行扩展,你将分别遇到延迟和系统不稳定的问题。

为了理解并识别当前流程中的薄弱环节,你需要合适的文档和可视化工具,如网络图或工作流图。在下一节中,我们将讨论你所建立的这些流程部分。

缺乏适当的文档和知识共享

文档和知识共享对于维护一致性并避免 DevOps 工作流中的错误至关重要,然而许多组织未能优先考虑这些活动。

在任何软件开发项目中,文档都起着至关重要的作用,确保项目的成功。它作为参考指南,提供关于项目架构、设计和实现细节的见解,并帮助维护和故障排除软件。DevOps 中的一个常见问题是缺乏适当且最新的文档,这可能导致混乱、延误和错误。为了解决这个问题,必须理解软件项目中不同类型的文档以及它们的目标受众。这里列出了这些文档:

  • 技术文档

  • API 文档

  • 用户文档

  • 流程文档

  • 运维文档

  • 发布说明和变更日志

让我们详细探讨一下它们。

技术文档

技术文档是面向开发人员、运维团队和其他参与软件开发与部署过程的技术相关人员的。它包括与系统架构、代码库、API、数据库模式、部署脚本、配置文件以及其他技术细节相关的文档。技术文档有助于理解软件的内部运作,从而更容易进行维护、故障排除和系统增强。

一些文档内容,如代码库和 API 的文档,可以通过专业软件实现自动化。你可以确保你的开发团队能够编写自文档代码,并且还可以使用Doxygen (www.doxygen.nl/)、Swimm (swimm.io/) 或 Redoc (github.com/Redocly/redoc)等软件自动化生成代码文档。要记录你的 API 文档,你可以使用基于 OpenAPI 的项目,比如Swagger (github.com/swagger-api/swagger-ui)。

API 文档

API 文档专注于记录软件暴露的 API,这些 API 用于与其他系统集成或构建扩展或插件。它包括与 API 端点、请求和响应格式、身份验证和授权机制、错误处理及其他与 API 相关的细节的文档。API 文档帮助开发者理解如何通过编程方式与软件交互,从而实现与其他系统的无缝集成。

用户文档

用户文档面向软件的最终用户,包括客户、客户以及其他与软件互动的相关人员。它包括用户手册、指南、教程以及其他解释如何有效安装、配置和使用软件的资源。用户文档应使用简明清晰的语言编写,避免技术性语言,涵盖软件的所有必要功能和特点。

确保最终用户能够轻松地通过文档联系到你的支持团队。

流程文档

流程文档侧重于记录软件开发和部署生命周期中遵循的工作流程、过程和程序。它包括与编码标准、版本控制、构建和部署过程、测试方法、发布管理以及其他开发实践相关的文档。流程文档有助于保持一致性、可重复性和效率,确保团队始终如一地遵循最佳实践。

运维文档

运维文档是为负责在生产环境中部署、配置和管理软件的运维团队准备的。它包括与安装说明、配置指南、监控和故障排除程序、灾难恢复DR)计划以及其他运维任务相关的文档。运维文档帮助运维团队有效地管理和维护生产环境中的软件,确保其可用性、性能和可靠性。

发布说明和更新日志

发布说明和更新日志记录了每个版本发布中对软件所做的更改和更新。它们提供了新功能、修复的漏洞以及其他更改的摘要,并提供如何升级或迁移到最新版本的说明。发布说明和更新日志有助于让利益相关者了解软件的进展,并作为对软件随时间变化的历史记录。

如你所见,文档的组织可能会很棘手,因为你首先需要了解你的目标受众和目的。结合我们在这一部分讨论的信息,你应该能很快识别出这些内容。此外,值得补充的是,文档永远不是一成不变的,需要定期更新,以反映你应用程序当前的状况。

在下一部分,我们将讨论抵制变化的问题。组织越大,惯性越强,变化就越困难。让我们从 DevOps 的角度来探讨这个问题。

克服对变化的抵制

DevOps 要求许多组织进行重大文化转型,抵制变化可能是成功实施的一个重要障碍。抵制变化是实施新流程、新工具和其他文化变革时的一个常见因素,在任何组织中都起着重要作用。这就是为什么我们在几页前说过 “一直都是这么做的” 是最糟糕的说法之一。改进需要改变,而改变需要开放的心态和准备好摧毁现状的勇气。

改变的抵制有多个来源。其中之一是对变化的恐惧。改变组织会带来困难:一个新流程增加了失败的可能性。它还需要学习新事物,放弃已经熟悉并经过验证的解决方案。对大多数人来说,这是超出舒适区的领域。

另一个变革抗拒因素是组织惯性。通常,任何变革的引入都需要大量的文书工作和高层管理的认可。阅读前一段关于变革恐惧的内容。在公司中,绩效的一个指标是完成的工作量。任何接受会导致延误的变革的人都会处于压力之下。

有一些策略可以克服这种抗拒情绪。所有策略的基础是双向透明的沟通。任何引入变革的人都必须以清晰的方式进行沟通,并提前提供通知。那些将受到变革影响的人必须有时间考虑将要发生的事情以及他们在其中的角色。他们必须能够表达自己的意见,并感受到自己被听见。

变革实施失败的最大原因是如果变革看起来被强加,且让人们在整个过程中觉得自己毫无意义。

摘要

本书的最后一章探讨了可能妨碍 DevOps 实践成功实施的潜在陷阱和误解。我们强调了培养协作文化和优先考虑持续改进以实现预期结果的重要性。

我们已经讨论了各种常见的陷阱,包括忽视测试和质量保证、过度依赖自动化、忽略适当的监控和反馈循环、未能妥善处理安全和合规措施、未能实现可扩展性和灵活性,以及未与业务目标对齐等问题。

本章的一个重点是文档和知识共享的重要性,以及如何克服变革抗拒的策略。许多组织在实施 DevOps 时,在这些非技术性方面面临困难,本章提供了如何有效应对这些问题的实用指导。

本章中强调的另一个关键方面是需要建立强大的监控和反馈循环,以便及时了解 DevOps 流水线的性能和稳定性。如果没有适当的监控,及时识别和修复问题将变得十分困难,可能导致长时间的停机和生产力下降。

我们希望你能够在 DevOps 旅程中,对你可能遇到的一些问题产生影响,并在你之后成功地为其他人开辟加入组织的道路。通过本出版物中所包含的知识,你将具备充足的准备,迎接挑战,并拥有扎实的知识基础。

祝你好运!

posted @ 2025-06-26 15:33  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报