现代 DevOps 实践指南第二版(一)

现代 DevOps 实践与容器技术指南

原文:annas-archive.org/md5/3c34d287e2879a0f121f1884a118ac03

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书的新修订版超越了 DevOps 工具及其部署的基础知识,涵盖了实际示例,帮助你快速掌握容器、基础设施自动化、无服务器容器服务、持续集成和交付、自动化部署、部署流水线安全性以及在生产中运行服务的技能,所有这些都围绕容器和 GitOps 展开。

本书适用对象

如果你是软件工程师、系统管理员或运维工程师,想要进入公共云平台中的 DevOps 世界,那么本书适合你。当前的 DevOps 工程师也会发现本书非常有用,因为它涵盖了实施 DevOps 时的最佳实践、技巧和窍门,并以云原生的思维方式为重点。虽然不要求有容器化经验,但对软件开发生命周期和交付的基本理解将帮助你更好地从本书中获益。

本书涵盖内容

第一章现代 DevOps 方法,深入探讨了现代 DevOps 的领域,强调其与传统 DevOps 的区别。我们将探讨推动现代 DevOps 的核心技术,特别强调容器在其中的核心作用。鉴于容器是相对较新的技术发展,我们将深入了解开发、部署和安全管理基于容器的应用程序的最佳实践和技术。

第二章使用 Git 和 GitOps 进行源代码管理,介绍了 Git 这一领先的源代码管理工具及其在通过 GitOps 管理软件开发和交付中的应用。

第三章使用 Docker 进行容器化,开启了我们对 Docker 的探索,包括安装、Docker 存储配置、启动初始容器,并通过 Journald 和 Splunk 进行 Docker 监控。

第四章创建和管理容器镜像,剖析了 Docker 镜像,这是 Docker 使用中的一个关键组件。我们将了解 Docker 镜像、分层模型、Dockerfile 指令、镜像扁平化、镜像构建以及构建镜像的最佳实践。此外,我们还将探讨无发行版镜像及其在 DevSecOps 中的相关性。

第五章使用 Kubernetes 进行容器编排,介绍了 Kubernetes。我们将使用 minikube 和 kinD 安装 Kubernetes,深入研究 Kubernetes 的架构基础,并探索 Kubernetes 的基本构建模块,如 Pods、容器、ConfigMaps、密钥、探针以及多容器 Pods。

第六章管理高级 Kubernetes 资源,深入探讨了复杂的 Kubernetes 概念,包括网络、DNS、服务、部署、水平 Pod 自动缩放器以及有状态集。

第七章容器即服务(CaaS)和容器的无服务器计算,探讨了 Kubernetes 的混合特性,架起了 IaaS 和 PaaS 的桥梁。此外,我们还将研究 AWS ECS 等无服务器容器服务,以及 Google Cloud Run 和 Azure 容器实例等替代方案。最后,我们将讨论 Knative,一种开源、云原生、无服务器技术。

第八章使用 Terraform 的基础设施即代码(IaC),介绍了使用 Terraform 的 IaC,阐明了其核心原则。我们将通过动手实践的例子,使用 Terraform 从头开始在 Azure 上创建资源组和虚拟机,同时掌握 Terraform 的基本概念。

第九章使用 Ansible 的配置管理,使我们熟悉通过 Ansible 进行配置管理及其基本原理。我们将通过在 Azure 虚拟机上配置 MySQL 和 Apache 应用,来探索 Ansible 的关键概念。

第十章使用 Packer 的不可变基础设施,深入探讨了使用 Packer 实现不可变基础设施的内容。我们将结合第八章使用 Terraform 的基础设施即代码(IaC),以及第九章使用 Ansible 的配置管理,在 Azure 上启动基于 IaaS 的 Linux、Apache、MySQL 和 PHP(LAMP)栈。

第十一章使用 GitHub Actions 和 Jenkins 的持续集成,从容器中心的角度解释了持续集成,评估了各种工具和方法,以持续构建基于容器的应用程序。我们将研究 GitHub Actions 和 Jenkins 等工具,分析何时以及如何使用每个工具,并在部署示例微服务架构的分布式应用——博客应用时加以应用。

第十二章使用 Argo CD 的持续部署/交付,深入探讨了持续部署/交付,使用了 Argo CD 这一基于 GitOps 的现代持续交付工具。Argo CD 简化了容器应用的部署和管理。我们将利用它的功能来部署我们的示例博客应用。

第十三章保护和测试部署管道,探讨了多种保护容器部署管道的策略,包括容器镜像分析、漏洞扫描、机密管理、存储、集成测试和二进制授权。我们将整合这些技术,以增强我们现有的 CI/CD 管道的安全性。

第十四章理解生产服务的关键性能指标(KPIs),介绍了站点可靠性工程,并调查了一系列关键性能指标,这些指标对于有效管理生产中的分布式应用至关重要。

第十五章使用 Istio 在生产中操作容器,将带你了解广泛采用的服务网格技术 Istio。我们将探索在生产环境中进行日常操作的各种技术,包括流量管理、安全措施以及增强可观察性的技术,应用于我们示例中的博客应用。

如何最大限度地利用本书

本书所需的资源包括:

  • 一个 Azure 订阅来进行一些练习:目前,Azure 提供一个为期 30 天、价值 $200 的免费试用;请在 azure.microsoft.com/en-in/free 注册。

  • 一个 AWS 订阅:目前,AWS 提供一些产品的免费套餐。你可以在 aws.amazon.com/free 注册。本书使用了一些付费服务,但我们将尽量在练习中最小化使用这些付费服务的数量。

  • 一个 Google Cloud Platform 订阅:目前,Google Cloud Platform 提供一个免费的 $300 试用,试用期为 90 天,你可以在 console.cloud.google.com/ 注册。

  • 对于某些章节,你需要克隆以下 GitHub 仓库以继续进行练习: github.com/PacktPublishing/Modern-DevOps-Practices-2e

    本书中涵盖的软件/硬件操作系统要求
    Google Cloud PlatformWindows、macOS 或 Linux
    AWSWindows、macOS 或 Linux
    AzureWindows、macOS 或 Linux
    Linux 虚拟机Ubuntu 18.04 LTS 或更高版本

如果你使用的是本书的数字版,我们建议你自己输入代码,或者从本书的 GitHub 仓库获取代码(链接将在下一节中提供)。这样做有助于避免与复制粘贴代码相关的潜在错误。

下载示例代码文件

你可以从 GitHub 上下载本书的示例代码文件,链接为 github.com/PacktPublishing/Modern-DevOps-Practices-2e。如果代码有更新,将会在 GitHub 仓库中更新。

我们还提供了其他来自丰富图书和视频目录的代码包,链接为 github.com/PacktPublishing/。快来看看吧!

使用的约定

本书中使用了若干文本约定。

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。示例如下:“让我们尝试发起一个拉取请求,将我们的代码从 feature/feature1 分支合并到 master 分支。”

一段代码块如下所示:

import os
import datetime
from flask import Flask
app = Flask(__name__)
@app.route('/')
def current_time():
  ct = datetime.datetime.now()
  return 'The current time is : {}!\n'.format(ct)
if __name__ == "__main__":
  app.run(debug=True,host='0.0.0.0')

任何命令行输入或输出如下所示:

$ cp ~/modern-devops/ch13/install-external-secrets/app.tf \
terraform/app.tf
$ cp ~/modern-devops/ch13/install-external-secrets/\
external-secrets.yaml manifests/argocd/ 

粗体:表示新术语、重要词汇或屏幕上显示的文字。例如,菜单或对话框中的文字通常会以 粗体 显示。示例如下:“点击 创建拉取请求 按钮来创建拉取请求。”

提示或重要说明

以这种方式出现。

联系我们

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

一般反馈:如果你对本书的任何方面有疑问,请通过 customercare@packtpub.com 发送电子邮件,并在邮件主题中注明书名。

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

盗版:如果你在互联网上遇到任何形式的非法复制品,我们将非常感激你能提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并附上材料链接。

如果你有兴趣成为作者:如果你在某个领域有专长,并且有兴趣写作或为一本书做贡献,请访问 authors.packtpub.com

分享你的想法

一旦你阅读完 现代 DevOps 实践,我们很想听听你的想法!请 点击这里直接进入亚马逊评论页面 并分享你的反馈。

你的评论对我们和技术社区都非常重要,它将帮助我们确保提供优质的内容。

下载本书的免费 PDF 版本

感谢你购买本书!

你喜欢随时阅读,但无法随身携带印刷版书籍吗?

你的电子书购买无法兼容你选择的设备吗?

不用担心,现在每本 Packt 书籍都会附带免费的无 DRM PDF 版本。

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

优惠不仅仅如此,你还可以获得独家折扣、新闻通讯以及每日送到你收件箱的精彩免费内容。

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

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

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_QR_Free_PDF.jpg

packt.link/free-ebook/9781805121824

  1. 提交你的购买凭证

  2. 就这些!我们将直接通过电子邮件发送您的免费 PDF 和其他福利

第一部分:现代 DevOps 基础

本部分将向您介绍现代 DevOps 和容器的世界,并为容器技术打下坚实的知识基础。在本节中,您将了解容器如何帮助组织在云中构建分布式、可扩展且可靠的系统。

本部分包含以下章节:

  • 第一章现代 DevOps 方法

  • 第二章使用 Git 和 GitOps 进行源代码管理

  • 第三章使用 Docker 进行容器化

  • 第四章创建和管理容器镜像

第一章:现代 DevOps 方式

本章将提供一些 DevOps 实践、流程和工具的背景知识。我们将了解现代 DevOps 以及它与传统 DevOps 的区别。我们还将介绍容器,并详细理解容器如何在云环境中改变整个 IT 格局,以便在本书的基础上继续深入学习。尽管本书并不完全聚焦于容器及其编排,但现代 DevOps 实践强调这一点。

本章将涵盖以下主要主题:

  • 什么是 DevOps?

  • 云计算简介

  • 了解现代云原生应用

  • 现代 DevOps 与传统 DevOps

  • 容器的需求

  • 容器架构

  • 容器与现代 DevOps 实践

  • 从虚拟机迁移到容器

到本章结束时,你应该理解以下几个关键方面:

  • 什么是 DevOps,它在现代 IT 环境中扮演的角色

  • 什么是云计算,它如何改变 IT 服务

  • 现代云原生应用的样貌以及它如何改变 DevOps

  • 为什么我们需要容器,以及容器解决了什么问题

  • 容器架构及其工作原理

  • 容器如何促成现代 DevOps 实践

  • 从基于虚拟机的架构迁移到容器的高级步骤

什么是 DevOps?

如你所知,软件开发和运维传统上由不同的团队承担,各自有不同的角色和责任。开发者专注于编写代码和创建新功能,而运维团队则专注于在生产环境中部署和管理软件。这种分离常常导致沟通鸿沟、发布周期缓慢以及低效的工作流程。

DevOps通过推动协作文化、共享责任以及在整个软件开发生命周期中使用自动化来弥合开发和运维之间的差距,确保持续反馈。

这是一套原则和实践,也是一种哲学,鼓励开发和运维团队参与整个软件开发生命周期,包括软件维护和运维。为了实现这一点,组织管理多个流程和工具,帮助自动化软件交付过程,从而提高速度和灵活性,通过持续集成和持续交付CI/CD)管道减少代码发布的周期时间,并监控运行在生产环境中的应用程序。

DevOps 团队应该确保,不是将开发、运维和质量保证分成不同的职能小组,而是组建一个能够涵盖整个软件开发生命周期(SDLC)的单一团队——即,该团队负责构建、部署和监控软件。合并后的团队拥有整个应用程序的所有权,而不是某些职能的所有权。这并不意味着团队成员没有专业技能,而是要确保开发人员了解运维工作,运维工程师也要了解开发工作。QA 团队与开发人员和运维工程师密切合作,了解业务需求以及在实际操作中遇到的各种问题。基于这些经验,他们需要确保自己开发的产品能够满足业务需求,并解决实际操作中遇到的问题。

在传统的开发团队中,积压工作的来源是业务及其架构师。然而,对于 DevOps 团队来说,他们的日常积压工作有两个来源——业务及其架构师,以及客户和他们在生产环境中运行应用时遇到的问题。因此,DevOps 实践通常遵循一个无限循环,而不是线性的交付路径,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.01.jpg

图 1.1 – DevOps 无限循环

为了确保具有不同技能的人员之间能够顺利协作,DevOps 强调自动化和工具的使用。DevOps 的目标是尽可能地自动化重复性的任务,集中精力处理更重要的事务。这确保了产品的质量和快速交付。DevOps 注重 人员流程工具,其中最重要的是人员,最不重要的是工具。我们通常使用工具来自动化流程,帮助人员实现正确的目标。

DevOps 工程师通常会遇到一些基本的概念和术语,下面列出了一些。我们将在本书中重点讨论这些内容:

  • 持续 集成CI

CI 是一种软件开发实践,涉及频繁地将多个开发人员的代码更改合并到共享的代码库中,通常是每天几次。这确保开发人员定期将代码合并到一个中央代码库中,并在此库中运行自动化构建和测试,实时向团队反馈。这大大减少了周期时间,并提高了代码质量。该过程旨在尽早发现代码中的错误,而不是等到测试阶段。它能尽早发现集成问题,并确保软件始终保持可发布状态。

  • 持续 交付CD

CD 主要是将您经过测试的软件在准备好时部署到生产环境中。因此,CD 管道将把您的更改打包并进行集成和系统测试。一旦您彻底测试了代码,就可以自动(或经批准后)将更改部署到测试和生产环境中。所以,CD 的目标是准备好最新的经过测试的工件,以便进行部署。

  • 基础设施即 代码 (IaC)

IaC 是软件开发中的一种实践,涉及使用代码和配置文件而非手动过程来管理和配置基础设施资源,如服务器、网络和存储。IaC 将基础设施视为软件,使团队能够以可编程和版本控制的方式定义和管理基础设施资源。随着虚拟机、容器和云技术的出现,技术基础设施在很大程度上已变得虚拟化。这意味着我们可以通过 API 调用和模板来构建基础设施。借助现代工具,我们还可以以声明式方式在云中构建基础设施。这意味着您现在可以构建 IaC,将构建基础设施所需的代码存储在源代码库中,如 Git,并使用 CI/CD 管道来创建和管理基础设施。

  • 配置即 代码 (CaC)

CaC 是软件开发和系统管理中的一种实践,涉及使用代码和版本控制系统管理和配置设置。它将配置设置视为代码制品,使团队能够以程序化和可重现的方式定义、存储和管理配置。历史上,服务器通常是从头开始手动构建的,并且很少发生变化。然而,随着弹性基础设施的出现和对自动化的重视,配置也可以通过代码进行管理。CaC 与 IaC 密切配合,共同构建可扩展的、容错的基础设施,从而使您的应用程序能够无缝运行。

  • 监控 和日志记录

监控和日志记录是软件开发和运维中的基本实践,涉及捕获和分析关于软件应用和系统行为及性能的数据。它们为软件的健康、可用性和性能提供洞察,使团队能够识别问题、排查故障并做出有依据的改进决策。监控和日志记录属于可观察性范畴,这是任何 DevOps 团队都必须关注的领域——即通过监控知道您的应用程序出现问题和异常,并通过日志记录进行问题追踪。这些实践和工具就像您的眼睛,是 DevOps 堆栈中的关键领域。此外,它们对构建 DevOps 团队的待办事项列表也有重要贡献。

  • 沟通 与协作

沟通与协作是 DevOps 实践中的关键要素。它们促进了开发、运营和其他参与软件交付生命周期的利益相关者之间的高效团队合作、知识共享和简化的工作流程。沟通与协作使得 DevOps 团队能够高效运作。过去,通过电子邮件的沟通方式已经不再适用。现代 DevOps 团队使用票务和敏捷工具来管理待办事项,利用 Wiki 跟踪知识文章和其他文档,并通过聊天和即时消息IM)工具进行即时沟通。

虽然这些只是 DevOps 实践和工具的一些核心方面,但随着容器和云的出现,已经发生了一些变化——即现代云原生应用栈。现在我们已经介绍了几个流行词汇,接下来让我们理解什么是云计算和云计算服务。

云计算简介

传统上,软件应用程序通常运行在内部计算机(服务器)上,这些服务器被称为数据中心。这意味着组织必须购买和管理物理计算机和网络基础设施,这通常需要大量的资本支出,同时还需要支付相当高的运营费用。此外,服务器会出现故障并需要维护。这意味着那些希望尝试新事物的小型公司通常不会开始,因为所需的资本支出CapEx)非常庞大。这表明,项目必须经过良好的规划、预算和架构设计,然后再根据需求购买和配置基础设施。这也意味着,随着时间推移,快速扩展基础设施变得不可行。例如,假设你开始时规模较小,并未预料到网站的流量很大,因此你订购和配置了较少的资源,但网站突然变得很受欢迎。在这种情况下,你的服务器将无法处理如此大量的流量,可能会崩溃。快速扩展将涉及购买新的硬件并将其添加到数据中心,这需要时间,而你的业务可能会失去这一机会。

为了解决这个问题,像亚马逊、微软和谷歌等互联网巨头开始建设公共基础设施来运行他们的互联网系统,最终将其推出供公众使用。这促成了一个新的现象,即云计算

云计算是指通过互联网按需提供计算资源,如服务器、存储、数据库、网络、软件和分析服务。与将这些资源托管在本地物理基础设施上不同,云计算使组织能够访问和利用云服务提供商CSPs)提供的计算服务。一些领先的公共 CSPs 包括亚马逊云服务AWS)、微软 Azure谷歌云平台

在云计算中,云服务提供商(CSP)拥有、维护并管理底层的基础设施和资源,而用户或组织则利用这些资源来支持他们的应用程序和服务。

简单来说,云计算不过是使用他人的数据中心来运行您的应用程序,且应按需提供。它应该通过网络门户、API 等方式提供控制面板,以便让您进行操作。作为交换,您需要按按需付费的方式支付租赁费用,来使用您所配置(或使用)的资源。

因此,云计算提供了多种好处,并为企业打开了前所未有的新机遇。以下是其中的一些好处:

  • 可扩展性:云上的资源是可扩展的。这意味着您可以根据需要向现有服务器添加新的服务器或资源。您还可以通过流量自动扩展应用程序。这意味着,如果您需要一台服务器来运行应用程序,而由于受欢迎程度或高峰时段的原因,您突然需要五台服务器,您的应用程序可以通过云计算 API 和内置的管理资源自动扩展到五台服务器。这为企业提供了强大的能力,因为他们可以从小规模开始,而不必过多担心未来的流量和扩展问题。

  • 成本节省:云计算遵循按需付费模式,用户只需为他们实际使用的资源和服务付费。这消除了对硬件和基础设施的前期资本支出(CapEx)的需求。对于企业来说,租用资源通常比投资计算硬件要便宜。因此,您只需为特定时间段内所需的资源付费,无需预先配置资源来应对未来的负载,这为大多数中小型组织节省了大量成本。

  • 灵活性:云资源不再仅仅是服务器。您还可以获取许多其他服务,如简单的对象存储解决方案、网络和块存储、托管数据库、容器服务等。这些为您在应用程序的使用上提供了极大的灵活性。

  • 可靠性:云计算资源受服务水平协议SLA)的约束,有时可达到 99.999%的可用性。这意味着大多数云资源永远不会停机;如果停机,您也不会注意到,因为云平台有内置的冗余机制。

  • 安全性:由于云计算公司为多个客户运行应用程序,它们通常拥有比您在本地构建的更严格的安全防护网。它们拥有一支安全专家团队,全天候监控云平台,并且默认提供加密、访问控制和威胁检测等服务。因此,在正确架构的情况下,运行在云上的应用程序更加安全。

云计算提供了多种服务,包含以下几种:

  • 基础设施即服务IaaS)类似于在服务器上运行应用程序。它是一种云计算服务模型,通过互联网提供虚拟化的计算资源。使用 IaaS,组织可以访问和管理基本的 IT 基础设施组件,如虚拟机、存储和网络,而无需投资和维护物理硬件。在 IaaS 模式中,CSP 拥有并管理底层的物理基础设施,包括服务器、存储设备、网络设备和数据中心。而用户或组织则可以控制虚拟化基础设施上运行的操作系统OSs)、应用程序和配置。

  • 平台即服务PaaS)为你提供了一个抽象层,在这个层级上,你可以专注于代码编写,将应用程序管理交给云服务。PaaS 是一种云计算服务模型,为开发人员提供一个平台和环境,以便构建、部署和管理应用程序,而无需担心底层的基础设施组件。PaaS 抽象化了基础设施管理的复杂性,使开发人员可以专注于应用程序开发和部署。在 PaaS 模式中,CSP 提供的平台包括操作系统(OS)、开发框架、运行时环境,以及支持应用程序开发生命周期所需的各种工具和服务。用户或组织可以利用这些平台资源来开发、测试、部署和扩展应用程序。

  • 软件即服务SaaS)为你提供了一个预构建的应用程序,例如一个可以轻松与应用程序集成的现成监控服务。在 SaaS 模式中,云服务提供商(CSP)托管和管理软件应用程序,包括基础设施、服务器、数据库和维护。用户或组织可以通过网页浏览器或瘦客户端应用程序访问该应用。通常他们根据使用量支付订阅费用,软件作为按需服务提供。

云的出现带来了一个新的行业流行词——云原生应用。我们将在下一节中讨论它们。

理解现代云原生应用

当我们说云原生时,指的是那些为在云中原生运行而构建的应用程序。云原生应用旨在充分利用云的能力和优势,尽可能多地使用云服务在云中运行。

这些应用程序天生具有可扩展性灵活性弹性(容错能力)。它们在很大程度上依赖于云服务和自动化。

现代云原生应用的一些特征如下:

微服务架构:现代云原生应用通常遵循微服务架构。微服务是将应用程序拆分为多个较小,松散耦合的部分,具有独立的业务功能。独立的微服务可以根据需要或特定功能使用不同的编程语言编写。这些较小的部分可以独立扩展,灵活运行,并且从设计上具有弹性。

容器化:微服务应用通常使用容器来运行。容器为应用程序提供了一种一致便携轻量级的环境,确保它们捆绑了所有必要的依赖关系和配置。容器可以在所有环境和云平台上运行相同的内容。

DevOps 和自动化:云原生应用程序大量使用现代 DevOps 的实践和工具,因此在很大程度上依赖自动化。这简化了应用程序的开发,测试和运营。自动化还带来了可扩展性韧性一致性

动态编排:云原生应用程序构建为可扩展且本质上是容错的。这些应用程序通常是短暂的临时的);因此,服务的副本可以根据需要随时出现和消失。诸如KubernetesDocker Swarm之类的动态编排平台用于管理这些服务。这些工具帮助在变化的需求和流量模式下运行您的应用程序。

云原生数据服务的使用:云原生应用通常使用托管的云数据服务,如存储数据库缓存消息系统,以便多个服务之间进行通信。

云原生系统强调 DevOps,并且现代 DevOps 已经出现来管理它们。因此,现在让我们来看看传统 DevOps 和现代 DevOps 之间的区别。

现代 DevOps 与传统 DevOps 的对比

DevOps 的传统方法涉及建立一个包含开发质量保证运维成员的 DevOps 团队,并努力创建更快,更好的软件。然而,虽然会专注于自动化软件交付,但自动化工具如JenkinsGit等仍需手动安装和维护。这导致了另一个问题,因为现在我们不得不管理另一套 IT 基础设施。最终问题归结为基础设施和配置,而焦点是自动化自动化过程。

随着容器的出现和公共云景观的近期繁荣,DevOps 的现代方法进入了视野,其中涉及到一切的自动化。从基础设施的供应到工具和流程的配置,一切都有相应的代码。因此,现在我们有IaCCaC不可变基础设施容器。我称这种方法为现代 DevOps,并且这将是本书的重点。

以下表格描述了现代 DevOps 和传统 DevOps 之间的一些关键相似性和差异:

方面现代 DevOps传统 DevOps
软件交付重视 CI/CD 流水线、自动化测试和部署自动化。重视 CI/CD 流水线、自动化测试和部署自动化。
基础设施管理常用 IaC 来进行基础设施资源的规划和管理。经常使用云平台和容器化技术。手动进行基础设施的规划和配置,通常依赖传统数据中心和有限的自动化。
应用程序部署广泛采用容器化和容器编排技术,如 Docker 和 Kubernetes,以确保应用程序的可移植性和可扩展性。使用传统的部署方法,如直接在虚拟机或物理服务器上部署应用程序,没有采用容器化。
可扩展性和弹性利用云平台和容器编排的自动扩展能力来处理不同的工作负载。专注于高可用性和容错性。通过纵向扩展(向现有服务器添加资源)或手动容量规划来实现可扩展性。通过手动添加冗余服务器来实现高可用性。弹性不存在,容错性不是重点。
监控与日志记录广泛使用监控工具、日志聚合和实时分析来获取应用程序和基础设施性能的洞察。监控和日志记录做法有限,可用工具和分析较少。
协作与文化强调开发和运维团队之间的协作、沟通和共享责任(DevOps 文化)。强调开发和运维团队之间的协作、沟通和共享责任(DevOps 文化)。
安全性安全性通过使用DevSecOps实践集成到开发过程中。安全测试和漏洞扫描是自动化的。安全措施通常是手动应用并由独立的安全团队管理。在软件开发生命周期中自动化安全测试有限。
部署速度通过自动化流水线快速频繁地部署软件更新,从而缩短上市时间。快速部署应用程序,但缺乏自动化的基础设施部署。

表 1.1 – 现代 DevOps 和传统 DevOps 之间的主要相似性和差异

需要注意的是,现代 DevOps 与传统 DevOps 之间的区别并不是严格的二元对立,因为组织可以在一个范围内采用各种实践和技术。现代 DevOps 方法通常专注于利用云技术、自动化、容器化和 DevSecOps 原则,以提高协作性、敏捷性以及软件开发和部署的效率。

正如我们之前讨论的,容器有助于实现现代 DevOps,并构成了这一实践的核心。我们将在下一节中深入了解容器。

容器的需求

最近,容器非常流行,原因也很充分。它们解决了计算机架构中最关键的问题——在任何计算环境中运行可靠的、分布式的软件,并具备接近无限的可扩展性

它们促成了软件工程领域的一门全新学科——微服务。它们还在技术中引入了一次打包,随时部署的概念。结合云计算和分布式应用程序,容器与容器编排技术共同促成了业界的一个新热词——云原生——彻底改变了 IT 生态系统。

在我们深入探讨更多技术细节之前,让我们用简单明了的方式来理解容器。

容器得名于集装箱。我将通过集装箱类比来解释容器,以便更好地理解。历史上,由于交通运输的改善,大量物品跨越多个地理区域进行运输。由于各种货物通过不同方式运输,货物在每个运输点的装卸问题巨大。而且,随着劳动力成本的上升,航运公司要在规模化运营的同时保持低价格变得不切实际。

此外,这也导致了物品经常遭受损坏,货物容易被错放或与其他货物混淆,因为没有隔离措施。运输货物需要一种标准的方式,以提供必要的货物隔离,并便于装卸。航运业提出了集装箱这一优雅的解决方案来解决这个问题。

现在,集装箱简化了航运业中的许多事情。通过标准化集装箱,我们可以只移动集装箱就将货物从一个地方运输到另一个地方。相同的集装箱可以用于公路、装载到火车上并通过船只运输。这些运输工具的操作员大多数时候不需要担心集装箱内的货物。下图以图形方式描绘了整个工作流程,便于理解:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.02.jpg

图 1.2 – 集装箱工作流程

类似地,软件行业在软件可移植性和计算资源管理方面也存在问题。在标准软件开发生命周期中,软件会在多个环境中移动,有时候,多个应用程序共享同一个操作系统。环境之间的配置可能存在差异,因此在开发环境中可以正常工作的软件在测试环境中可能无法正常工作。测试环境中正常工作的东西在生产环境中也可能不正常。

此外,当您在单台计算机内运行多个应用程序时,它们之间没有隔离。一个应用程序可能会耗尽另一个应用程序的计算资源,这可能导致运行时问题。

在部署的每个步骤中都需要重新打包和重新配置应用程序,因此需要大量的时间和精力,有时会出错。

在软件行业,容器通过提供应用程序之间的隔离和计算资源管理来解决这些问题,为这些问题提供了最佳解决方案。

软件行业面临的最大挑战是提供应用程序隔离和优雅地管理外部依赖项,以便它们可以在任何平台上运行,无论操作系统或基础设施如何。软件用多种编程语言编写,并使用各种依赖项和框架。这导致了一个称为地狱矩阵的场景。

地狱矩阵

假设您正在准备一台服务器,该服务器将为多个团队的多个应用程序运行。现在,假设您没有虚拟化基础设施,并且需要在一个物理机器上运行所有内容,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.03.jpg

图 1.3 – 物理服务器上的应用程序

一个应用程序使用某个依赖项的特定版本,而另一个应用程序使用不同版本,您最终在一个系统中管理两个版本的同一软件。当您扩展系统以适应多个应用程序时,您将管理数百个依赖项和不同应用程序版本,这会逐渐变得在一个物理系统内难以管理。这种情况在流行的计算术语中被称为地狱矩阵

地狱矩阵产生了多种解决方案,但有两种显著的技术贡献 – 虚拟机容器

虚拟机

虚拟机通过一种叫做虚拟机管理程序(hypervisor)的技术来模拟操作系统。虚拟机管理程序可以作为软件运行在物理主机操作系统上,或者作为固件运行在裸机上。虚拟机作为虚拟的客操作系统在虚拟机管理程序上运行。借助这项技术,您可以将一台庞大的物理机器划分为多个较小的虚拟机,每个虚拟机都服务于特定的应用程序。这项技术已经彻底改变了计算基础设施,近二十年来一直在使用,至今仍然活跃在市场上。市场上一些最流行的虚拟机管理程序包括VMwareOracle VirtualBox

下图展示了虚拟机上的同一堆栈。您可以看到,每个应用程序现在都包含一个专用的客操作系统,每个操作系统都有自己的库和依赖关系:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.04.jpg

图 1.4 – 虚拟机上的应用程序

尽管这种方法是可接受的,但它就像使用一整艘船来运输货物,而不是使用简单的货运集装箱来做类比。虚拟机资源消耗较大,因为它需要一个较重的客操作系统层来隔离应用程序,而不是更轻量的解决方案。我们需要为虚拟机分配专用的 CPU 和内存;资源共享不够理想,因为人们往往为了应对高峰负载而过度配置虚拟机。虚拟机的启动速度也较慢,虚拟机的扩展传统上较为繁琐,因为涉及到多个活动组件和技术。因此,使用虚拟机自动化水平扩展(通过向资源池中添加更多机器来处理更多来自用户的流量)并不是特别直接。此外,系统管理员现在不得不处理多个服务器,而不是一个服务器中的大量库和依赖关系。虽然比以前有所改善,但从计算资源的角度来看,它仍然不是最优解。

容器

这就是容器技术的引入背景。容器技术解决了地狱矩阵的问题,并且没有涉及重型的客操作系统层。相反,容器通过封装应用程序运行时和依赖关系,将它们隔离开来,创建了一种叫做容器的抽象。现在,您可以在单一操作系统上运行多个容器。运行在容器中的众多应用程序可以共享相同的基础设施。因此,它们不会浪费您的计算资源。您也无需担心应用程序的库和依赖关系,因为它们被隔离在其他应用程序之外——对每个人来说,都是一种双赢的局面!

容器运行在容器运行时环境上。虽然Docker是最流行且几乎是事实上的容器运行时,但市场上还有其他选择,比如RktContainerd。它们都使用相同的 Linux 内核cgroups特性,这一特性的基础来源于谷歌、IBM、OpenVZ 和 SGI 的共同努力,将OpenVZ嵌入到 Linux 主内核中。OpenVZ 是最早尝试在不使用客操作系统层的情况下,在 Linux 内核中实现虚拟环境特性的项目,我们现在称之为容器。

它在我的机器上能运行

你可能在职业生涯中听过这句话很多次。这是一个典型的情形:开发人员给测试团队带来困扰,他们会说“但在我的机器上能运行”,而测试团队则回应道“我们不会把你的机器交给客户。”容器使用一次构建,到处运行一次打包,随处部署的理念,解决了它在我的机器上能运行的问题。由于容器需要容器运行时,它们可以在任何机器上以相同的方式运行。应用程序的标准化设置也意味着系统管理员的工作仅限于照顾容器运行时和服务器,并将应用程序的责任委托给开发团队。这减少了软件交付的管理负担,软件开发团队现在可以在没有太多外部依赖的情况下引领开发——这确实是强大的能力!现在,让我们看看容器是如何设计来实现这一点的。

容器架构

在大多数情况下,你可以将容器视为迷你虚拟机——至少,它们看起来像虚拟机。但实际上,它们只是运行在操作系统中的计算机程序。那么,让我们来看看容器中应用堆栈的高层次示意图:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.05.jpg

图 1.5 – 容器中的应用程序

正如我们所看到的,计算基础设施位于最底部,形成了基础层,其上是主机操作系统和运行其上的容器运行时(在本例中为 Docker)。接着,我们有多个使用容器运行时的容器化应用程序,它们作为独立进程在主操作系统上运行,利用命名空间cgroups

正如你可能已经注意到的,我们在其中没有像虚拟机那样的客操作系统层。每个容器都是在内核用户空间上运行的软件程序,它与主机操作系统共享相同的操作系统及相关的运行时和其他依赖项,容器中仅包含所需的库和依赖项。容器不会继承操作系统的环境变量。你必须为每个容器单独设置这些变量。

容器复制了文件系统,尽管它们存在于磁盘上,但彼此隔离。这使得容器能够在安全的环境中运行应用程序。单独的容器文件系统意味着容器不需要与操作系统文件系统进行频繁的交互,从而比虚拟机更快地执行。

容器设计使用 Linux 命名空间 来提供隔离,并使用 cgroups 对 CPU、内存和磁盘 I/O 消耗进行限制。

这意味着,如果你列出操作系统进程,你将看到容器进程与其他进程一起运行,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.06.jpg

图 1.6 – 操作系统进程

然而,当你列出容器的进程时,你只会看到容器进程,如下所示:

$ docker exec -it mynginx1 bash
root@4ee264d964f8:/# pstree
nginx---nginx

这就是命名空间如何在容器之间提供一定程度的隔离。

Cgroups 在限制一组进程可以使用的计算资源方面起着重要作用。例如,如果你将进程添加到一个 cgroup,你可以限制这些进程使用的 CPU、内存和磁盘 I/O。此外,你还可以衡量和监控资源使用情况,当应用程序出现问题时,可以停止一组进程。所有这些特性构成了容器化技术的核心,我们将在本书后面看到这些内容。

一旦我们有了独立运行的容器,我们还需要了解它们如何交互。因此,我们将在下一节探讨容器网络。

容器网络

容器是操作系统中的独立网络实体。Docker 运行时使用网络驱动程序来定义容器之间的网络连接,它们是软件定义的网络。容器网络通过使用软件来操作 主机 iptables、连接外部网络接口、创建隧道网络以及执行其他活动,以便容器之间可以进行连接。

虽然你可以在容器中实现多种网络配置,但了解一些常用的配置还是很有帮助的。如果细节看起来有些复杂,不要太担心——你将在本书后续的实操练习中理解它们,跟随文本并不需要完全掌握这些内容。现在,让我们来看一下你可以定义的几种容器网络类型:

  • :这是一个完全隔离的网络,您的容器无法与外界通信。它们被分配了一个回环接口,并且无法连接到外部网络接口。您可以使用此网络来测试容器、为未来使用准备容器,或者运行不需要任何外部连接的容器,比如批处理任务。

  • docker0 接口用于默认容器。桥接网络通过操作 IP 表来提供 网络地址转换NAT),实现容器与主机网络之间的外部网络连接。它还可以避免端口冲突,使得运行在同一主机上的容器之间能够进行网络隔离。因此,你可以在单个主机内运行多个使用相同容器端口的应用程序。桥接网络允许同一主机内的容器通过容器 IP 地址进行通信。然而,它不允许与运行在不同主机上的容器通信。因此,你不应该在集群配置中使用桥接网络(即使用多台服务器联合运行容器)。

  • 主机网络:主机网络使用主机机器的网络命名空间来处理所有容器的网络。这类似于在主机上运行多个应用程序。虽然主机网络实现简单,易于可视化和故障排除,但它容易出现端口冲突问题。尽管容器使用主机网络进行所有通信,但除非在特权模式下运行,否则它无法操作主机网络接口。主机网络不使用 NAT,因此速度较快,并且以裸机速度进行通信。因此,你可以使用主机网络来优化性能。然而,由于容器之间没有网络隔离,从安全和管理的角度来看,在大多数情况下,你应该避免使用主机网络。

  • 底层网络:底层网络直接将主机网络接口暴露给容器。这意味着你可以直接在网络接口上运行容器,而无需使用桥接网络。有几种底层网络,最著名的有 MACvlan 和 IPvlan。MACvlan 允许你为每个容器分配一个 MAC 地址,使得容器看起来像是一个物理设备。这对于将现有堆栈迁移到容器特别有用,尤其是当你的应用程序需要在物理机器上运行时。MACvlan 还提供了对主机网络的完全隔离,因此,如果你有严格的安全要求,可以使用这种模式。MACvlan 有一定的限制,因为它不能与设置了安全策略禁止 MAC 欺骗的网络交换机一起使用。它还受到某些网络接口卡的 MAC 地址数量限制,例如 Broadcom,只允许每个接口最多 512 个 MAC 地址。

  • Overlay:不要将 Overlay 与 Underlay 混淆 - 尽管它们看起来像是反义词,但实际上并非如此。Overlay 网络通过网络隧道允许不同主机上的容器进行通信。因此,从容器的角度来看,它们似乎在单个主机上与容器进行交互,即使它们位于其他位置。它克服了桥接网络的限制,特别适用于集群配置,尤其是在使用容器编排器如 Kubernetes 或 Docker Swarm 时。一些流行的 Overlay 技术包括 flannelcalicoVXLAN

在深入讨论不同类型网络技术之前,让我们了解容器网络的微妙之处。在这次讨论中,我们将特别谈谈 Docker。

每个在主机上运行的 Docker 容器都分配了一个唯一的 IP 地址。如果你 exec(打开一个 shell 会话)进入容器并运行 hostname -I,你应该看到类似以下的内容:

$ docker exec -it mynginx1 bash
root@4ee264d964f8:/# hostname -I
172.17.0.2

这允许不同的容器通过简单的 TCP/IP 链路进行通信。Docker 守护进程充当每个容器的 DHCP 服务器。在这里,你可以为一组容器定义虚拟网络,并将它们结合在一起以提供网络隔离(如果需要)。你还可以将容器连接到多个网络,以便为它们提供两种不同的角色共享。

Docker 为每个容器分配一个唯一的主机名,默认为容器 ID。然而,这可以很容易地被覆盖,只要在特定网络中使用唯一的主机名即可。因此,如果你 exec 进入一个容器并运行 hostname,你应该看到容器 ID 作为主机名,如下所示:

$ docker exec -it mynginx1 bash
root@4ee264d964f8:/# hostname
4ee264d964f8

这使得容器能够作为独立的网络实体而不仅仅是简单的软件程序,你可以轻松地将容器视为小型虚拟机。

容器还继承了主机操作系统的 DNS 设置,因此如果你希望所有容器共享相同的 DNS 设置,你不必太担心。如果你想为你的容器定义单独的 DNS 配置,你可以通过传递一些标志来轻松实现。Docker 容器不继承 /etc/hosts 文件中的条目,因此你必须在使用 docker run 命令创建容器时声明它们。

如果你的容器需要代理服务器,你必须在 Docker 容器的环境变量中设置或者通过向 ~/.docker/config.json 文件添加默认代理来设置。

到目前为止,我们已经讨论了容器及其定义。现在,让我们讨论容器如何正在改变 DevOps 的世界,以及在一开始就明确拼写这一点是必要的。

容器和现代 DevOps 实践

容器和现代 DevOps 实践高度互补,并且已经改变了我们对软件开发和部署的方式。

容器与现代 DevOps 实践具有很好的协同作用,因为它们提供了必要的基础设施封装、可移植性、可扩展性和灵活性,从而实现快速高效的软件交付。通过现代 DevOps 实践,如 CI/CD、IaC 和微服务,容器为组织提供了一个强大的基础,使它们能够实现更快的市场响应时间、提升的软件质量和增强的操作效率。

容器从一开始就遵循 DevOps 实践。如果你查看典型的容器构建和部署工作流,你将会看到以下内容:

  1. 首先,用你想要的任何语言编写应用程序。

  2. 然后,创建一个Dockerfile,其中包含一系列步骤来安装应用程序的依赖项和运行应用所需的环境配置。

  3. 接下来,通过以下操作,使用 Dockerfile 创建容器镜像:

a) 构建容器镜像。

b) 运行容器镜像。

c) 对运行在容器上的应用程序进行单元测试。

  1. 然后,将镜像推送到像DockerHub这样的容器注册中心。

  2. 最后,从容器镜像创建容器,并在集群中运行它们。

你可以将这些步骤优雅地嵌入到下面显示的 CI/CD 管道示例中:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.07.jpg

图 1.7 – 容器 CI/CD 管道示例

这意味着你的应用程序及其运行时依赖项都在代码中定义。你从一开始就遵循配置管理,允许开发人员像处理短期工作负载一样处理容器(短期工作负载是可以临时替换的工作负载,如果某个工作负载消失了,可以随时启动另一个,而不会对功能产生影响)。如果它们表现不佳,你可以替换它们——这在虚拟机中是不太优雅的做法。

容器非常适合现代的 CI/CD 实践,因为现在你有了一种标准的方式来构建和部署应用程序,无论你使用何种编程语言。你不必管理昂贵的构建和部署软件,因为容器提供了一切。

容器很少独立运行,在业界,将它们插入到容器编排器中,如Kubernetes,或者使用容器即服务CaaS)平台,如AWS ECSEKSGoogle Cloud RunKubernetes EngineAzure ACSAKSOracle OCIOKE等,已经成为行业的标准做法。流行的功能即服务FaaS)平台,如AWS LambdaGoogle FunctionsAzure FunctionsOracle Functions,也会在后台运行容器。因此,尽管它们可能已经将底层机制抽象化,但你可能已经在不知不觉中使用了容器。

由于容器轻量化,你可以将应用程序的更小部分构建成容器,以便独立管理它们。结合像 Kubernetes 这样的容器编排工具,你就可以轻松运行一个分布式微服务架构。这些小部分可以独立扩展、自我修复,并独立于其他部分发布,这意味着你可以比以前更快、更可靠地将它们发布到生产环境中。

你还可以在上面接入服务网格(允许你发现、列出、管理并使你的微服务应用程序中多个组件(服务)之间进行通信的基础设施组件),比如Istio,并轻松获得流量管理、安全性和可观察性等高级运维功能。然后,你可以做一些很酷的事情,如蓝绿部署A/B 测试,在生产环境中进行操作测试,使用流量镜像基于地理位置的路由等,功能丰富。

因此,大型和小型企业比以往任何时候都更快地拥抱容器化,且这一领域正在呈指数级增长。根据businesswire.com的数据,应用容器市场年均复合增长率为 31%,预计到 2025 年将达到 69 亿美元。云计算领域每年增长 30.3%,预计到 2025 年将超过 24 亿美元,这也促进了容器化的快速发展。

因此,现代 DevOps 工程师必须理解容器及相关技术,以有效地交付容器化应用程序。这并不意味着虚拟机变得不再必要,我们不能完全忽视基于 IaaS 的解决方案在市场中的作用,因此在后续章节中我们还会涉及一些Ansible配置管理内容。由于云计算的到来,基础设施即代码(IaC)最近获得了很大的发展势头,因此我们还将介绍作为 IaC 工具的Terraform

从虚拟机迁移到容器

随着技术市场朝着容器化发展,DevOps 工程师面临着一个重要任务——将运行在虚拟机上的应用程序迁移到容器中,以便它们能够在容器上运行。嗯,这是大多数 DevOps 工程师职位描述中的一项任务,也是我们做的最重要的事情之一。

理论上,容器化一个应用程序就像写几个简单的步骤,但从实际操作来说,它可能是一个复杂的难题,尤其是在没有使用配置管理来设置虚拟机的情况下。当前企业中运行的虚拟机大多是由系统管理员通过大量手动劳动创建的,他们一块一块地改进服务器,并使得很难追溯他们可能做过的热修复记录。

由于容器从一开始就遵循配置管理原则,因此它不像直接获取虚拟机镜像并使用转换工具将其转换为 Docker 容器那样简单。

将运行在虚拟机上的遗留应用程序迁移需要多个步骤。让我们更详细地了解这些步骤。

发现

首先,我们从发现阶段开始:

  • 了解应用程序的不同部分

  • 评估可以容器化的遗留应用程序部分,并判断是否技术上可行

  • 定义迁移范围,并与相关方就迁移的明确目标、收益以及时间表达成一致

应用程序需求评估

一旦发现阶段完成,我们需要进行应用程序需求评估:

  • 评估是否更好地将应用程序拆分为更小的部分。如果是,那么这些应用程序部分是什么,它们如何相互作用?

  • 评估与应用程序相关的架构、性能和安全性方面,思考在容器化环境中的对应内容。

  • 了解相关风险并决定减轻风险的应对措施。

  • 理解迁移原则,并决定迁移策略,例如应该首先容器化应用程序的哪一部分。总是从外部依赖最少的应用程序开始。

容器基础设施设计

容器基础设施设计涉及创建一个稳健且可扩展的环境,以支持容器化应用程序的部署和管理。

设计容器基础设施涉及考虑可扩展性、网络、安全、自动化和监控等因素。关键是将基础设施设计与容器化应用程序的特定需求和目标对齐,并遵循高效、可靠的容器部署和管理的最佳实践。

一旦我们评估了所有需求、架构及其他方面,我们就可以进入容器基础设施设计阶段:

  • 在做出决策时,了解当前和未来的运营规模。根据应用程序的复杂性,你可以从多种选项中进行选择。关键问题包括:我们需要在平台上运行多少个容器?这些容器之间有哪些依赖关系?我们将多频繁地部署组件更改?应用程序可能会接收到的潜在流量是多少?应用程序的流量模式是什么?

  • 根据前面问题的答案,你需要了解将应用程序运行在哪种基础设施上。是本地部署还是云端,使用托管的 Kubernetes 集群,还是自行托管并管理?你还可以考虑轻量级应用程序的 CaaS 选项。

  • 你将如何监控和操作容器?是否需要安装专业工具?是否需要与现有监控工具堆栈进行集成?了解可行性并做出适当的设计决策。

  • 你将如何保护你的容器?是否有任何关于安全性的法规和合规要求?所选择的解决方案是否满足这些要求?

容器化应用程序

容器化应用程序包括将应用程序及其依赖项打包成容器镜像,这样可以在不同的环境中一致地部署和运行。

容器化应用程序带来了更好的可移植性、可扩展性和可重现性等好处。它简化了部署过程,并确保在不同环境中应用程序行为的一致性。

一旦我们考虑了设计的所有方面,就可以开始容器化应用程序:

  • 这时,我们需要查看应用程序并创建一个 Dockerfile,包含创建容器的步骤,就像当前一样。这需要大量的头脑风暴和评估,特别是当配置管理工具没有通过在虚拟机上运行来构建你的应用程序时,比如 Ansible。如果应用程序是如何安装的需要花费很多时间去弄清楚,那么你就需要编写准确的步骤来实现这一过程。

  • 如果你计划将应用程序拆分为更小的部分,可能需要从头开始构建应用程序。

  • 你必须决定一个适用于基于虚拟机并行应用程序的测试套件,并随着时间推移不断改进。

测试

测试容器化应用程序是确保其功能、性能和兼容性的重要步骤。

通过实施全面的测试策略,你可以确保容器化应用程序的可靠性、性能和安全性。在不同层次进行测试,集成自动化,并密切监控应用程序的行为,将帮助你在开发生命周期的早期识别和解决问题,从而使容器化应用程序更加稳健和可靠。

一旦我们容器化了应用程序,接下来的步骤是进行测试:

  • 为了证明你的容器化应用程序是否与虚拟机中的应用程序完全一样,你需要进行广泛的测试,证明你没有遗漏任何细节或之前应该考虑的部分。运行现有的测试套件或你为容器创建的测试套件。

  • 运行现有的测试套件可能是正确的方法,但你也需要考虑软件的非功能性方面。对原始应用程序进行基准测试是一个好的开始,你需要理解容器化解决方案所带来的开销。你还需要对应用程序进行微调,以使其符合性能指标。

  • 你还需要考虑安全性的重要性以及如何将其引入容器化世界。渗透测试将揭示许多你可能没有意识到的安全漏洞。

部署和发布

部署和发布容器化应用程序涉及将容器镜像部署到目标环境,并使应用程序可供使用。

一旦我们测试了容器并且足够有信心,我们就可以将我们的应用部署到生产环境中:

  • 最后,我们将应用部署到生产环境,并从中学习是否需要进一步的修改。然后,我们返回到发现过程,直到我们完善了应用。

  • 你必须定义并开发一个自动化的运行手册和 CI/CD 管道,以减少周期时间并快速排查问题。

  • 进行 A/B 测试,容器化应用并行运行,可以帮助你在将所有流量切换到新方案之前发现潜在问题。

以下图表总结了这些步骤,如你所见,这一过程是循环的。这意味着你可能需要根据在生产中运行容器时学到的经验,时不时地重新审视这些步骤:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.08.jpg

图 1.8 – 从虚拟机迁移到容器

现在,让我们了解一下为了确保从虚拟机迁移到容器时尽量减少摩擦并获得最佳效果,我们需要做些什么。

哪些应用应该容器化?

在从虚拟机迁移到容器的过程中,首先需要评估哪些应用可以容器化,哪些不能。广义上讲,你的应用工作负载可以分为两种类型——无状态有状态。无状态工作负载不存储状态,是计算能力强大的应用,如 API 和函数;而有状态应用,如数据库,则需要持久化存储才能正常工作。

虽然任何可以在 Linux 虚拟机上运行的应用都可以容器化,但无状态应用通常是你首先要考虑的低悬果。容器化这些工作负载相对容易,因为它们没有存储依赖。你拥有的存储依赖越多,应用在容器中的复杂性就越高。

其次,你还需要评估你想要托管应用的基础设施形式。例如,如果你打算在 Kubernetes 上运行整个技术栈,尽量避免异构环境。在这种情况下,你可能也希望容器化有状态应用。对于 Web 服务和中间件层,大多数应用依赖某种形式的状态才能正确运行。所以,无论如何,你都会管理存储。

虽然这可能会打开潘多拉的盒子,但业内并没有达成共识关于是否将数据库容器化。虽然一些专家反对在生产环境中使用容器化数据库,但也有相当一部分人认为没有问题。主要原因是缺乏足够的数据来支持或反驳在生产环境中使用容器化数据库。

我建议你在处理数据库时要小心。虽然我并不反对将数据库容器化,但你必须考虑各种因素,如分配适当的内存、CPU、磁盘,以及你在虚拟机上的每个依赖关系。此外,你还需要关注团队的行为方面。如果你有一支管理生产环境中数据库的 DBAs 团队,他们可能不太愿意处理额外的复杂性——即容器。

我们可以通过以下流程图总结这些高层次的评估步骤:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_1.09.jpg

图 1.9 – 虚拟机到容器迁移评估

这个流程图考虑了评估过程中最常见的因素。你还需要考虑到属于你组织的独特情况。因此,在做出任何决定之前,最好也考虑这些因素。

让我们看一些适合容器化的用例,以便更好地理解。以下类型的应用程序通常使用容器进行部署:

  • 微服务架构:采用微服务架构的应用程序,其中功能被分割成小的、独立的服务,非常适合容器化。每个微服务可以作为一个独立的容器进行打包,从而简化了个别服务的开发、部署、扩展和管理。

  • Web 应用:Web 应用程序,包括前端应用、后端 API 和 Web 服务,可以容器化。容器提供了一致的运行环境,使得在不同环境(如开发、测试和生产)中打包和部署 Web 应用变得更加容易。

  • 有状态应用:容器也可以用于运行需要持久数据存储的有状态应用。通过利用容器编排平台的功能,如持久卷或有状态集,有状态应用程序(如数据库、内容管理系统或文件服务器)可以被容器化并有效管理。

  • 批处理或定时任务:执行批处理任务或定时任务的应用程序,如数据处理、定期备份或报告生成,可以从容器化中受益。容器为这些任务提供了一个受控和隔离的环境,确保一致的执行和可重现性。

  • CI/CD 工具:将 Jenkins、GitLab CI/CD 或 CircleCI 等 CI/CD 工具容器化,可以实现一致和可重现的构建、测试和部署流水线。容器简化了依赖关系的管理、构建环境的隔离,并能够快速部署 CI/CD 基础设施。

  • 开发和测试环境:容器对于创建隔离的、可重复的开发和测试环境非常有价值。开发人员可以使用容器将他们的应用及所需的依赖、库和开发工具一起打包,这样可以确保在不同机器和团队成员之间拥有一致的开发和测试体验。

  • 物联网IoT应用:容器可以用于在物联网场景中部署和管理应用程序。它们为物联网应用提供轻量级、便捷的运行环境,使得应用可以在边缘设备、网关或云基础设施上轻松部署。

  • 机器学习和数据分析应用:容器化在部署机器学习模型和数据科学应用方面的应用日益增多。容器封装了所需的依赖、库和运行时环境,从而实现数据密集型应用的无缝部署和扩展。

需要注意,并非所有应用都适合容器化。具有复杂图形界面的应用、与底层基础设施紧密耦合的遗留单体架构,或者需要直接访问硬件的应用可能不适合容器化。在这种情况下,虚拟机或其他部署方法可能更为合适。

将应用拆分成更小的部分

如果你将应用的各个部分独立运行,你将最大限度地发挥容器的优势。

这种方法具有许多好处,具体如下:

  • 由于你现在可以在不影响其他部分的情况下更改应用的某一部分,你可以更频繁地发布应用;你的部署也将花费更少的时间来运行。

  • 你的应用部分可以独立扩展。例如,如果你有一个购物应用,而你的订单模块非常繁忙,它可以比评论模块扩展得更多,因为后者的繁忙程度可能远低于前者。如果是单体应用,整个应用会随着流量一起扩展,这从资源消耗的角度来看并不是最优方案。

  • 影响应用某一部分的变化不会破坏整个系统。例如,如果评论模块出现故障,顾客仍然可以将商品添加到购物车并结算订单。

然而,你也不应将你的应用拆分成过小的组件。这将导致相当大的管理开销,因为你将无法区分各个组件的作用。以购物网站为例,拥有一个订单容器,一个评论容器,一个购物车容器和一个目录容器是可以的。然而,拥有创建订单删除订单更新订单容器则不合适,这会显得过于繁琐。将应用拆分成符合你业务逻辑的组件才是正确的方法。

但你应该在第一步就将应用程序拆分成更小的部分吗?嗯,这取决于情况。大多数人都希望从容器化工作中获得投资回报ROI)。假设你从虚拟机迁移到容器,尽管你面临的变量很少,而且可以快速进入容器。那样的话,你不会从中获得任何好处——尤其是当你的应用程序是一个庞大的单体时。相反,由于容器层的存在,你会增加一些应用程序的开销。所以,将应用程序重新架构以适应容器化环境是继续前进的关键。

我们到了吗?

所以,你可能会想,我们到了吗?其实还没有!虚拟机将长期存在。它们有存在的充分理由,虽然容器解决了大部分问题,但并不是所有事情都能容器化。许多遗留系统运行在无法迁移到容器上的虚拟机中。

随着云计算的到来,虚拟化基础设施成为其基础,虚拟机处于核心地位。大多数容器在云中的虚拟机上运行,尽管你可能在一组节点中运行容器,但这些节点仍然是虚拟机。

然而,容器时代最棒的一点是,它将虚拟机视为标准设置的一部分。你只需在虚拟机上安装容器运行时,便无需区分它们。你可以在任何你希望的虚拟机上运行容器内的应用程序。使用像Kubernetes这样的容器编排工具,你还可以受益于编排工具在考虑各种因素的情况下决定容器的运行位置——资源可用性是其中最关键的因素之一。

本书将探讨现代 DevOps 实践的各个方面,包括管理基于云的基础设施、虚拟机和容器。虽然我们主要讨论容器,但我们也会同样重视使用 Ansible 进行配置管理,并学习如何使用 Terraform 快速部署基础设施。

我们还将探讨现代 CI/CD 实践,并学习如何高效、无误地将应用程序交付到生产环境中。为此,我们将介绍JenkinsArgo CD等工具。本书将为你提供在云和容器时代担任现代 DevOps 工程师角色所需的一切知识。

总结

在本章中,我们了解了现代 DevOps、云计算和现代云原生应用程序。接着,我们探讨了软件行业如何迅速转向容器,并讨论了随着云计算的发展,现代 DevOps 工程师必须掌握的技能,以应对容器和云的挑战。然后,我们简要了解了容器架构,并讨论了从基于虚拟机的架构迁移到容器化架构的一些高层步骤。

在下一章中,我们将学习使用Git进行源代码管理,这将构成本书其余部分的基础。

问题

回答以下问题以测试你对本章的知识:

  1. 云计算比本地部署更贵。 (正确/错误)

  2. 云计算比资本支出CapEx)需要更多的运营支出OpEx)。 (正确/错误)

  3. 以下哪项关于云原生应用程序的说法是正确的?(选择三项)

A. 它们通常遵循微服务架构

B. 它们通常是单体的

C. 它们使用容器

D. 它们使用动态编排

E. 它们使用本地数据库

  1. 容器需要虚拟机管理程序才能运行。 (正确/错误)

  2. 以下哪项关于容器的说法是正确的?(选择一项)

A. 容器是虚拟机中的虚拟机

B. 容器是简单的操作系统进程

C. 容器使用 cgroups 提供隔离

D. 容器使用容器运行时

E. 容器是短暂的工作负载

  1. 所有应用程序都可以容器化。 (正确/错误)

  2. 以下哪项是容器运行时?(选择两项)

A. Docker

B. Kubernetes

C. Containerd

D. Docker Swarm

  1. 你应该选择容器化哪种类型的应用程序?

A. APIs

B. 数据库

C. 大型机

  1. 容器原生支持 CI/CD 原则。 (正确/错误)

  2. 以下哪项是将应用程序拆分成多个部分的优点?(选择四项)

A. 故障隔离

B. 更短的发布周期

C. 独立的、细粒度的扩展

D. 应用架构简单性

E. 更简单的基础设施

  1. 在将应用程序拆分为微服务时,应该考虑哪个方面?

A. 将应用程序拆分成尽可能多的小组件

B. 将应用程序拆分成逻辑组件

  1. 你应该首先容器化哪种类型的应用程序?

A. 无状态

B. 有状态

  1. 以下哪项是 CaaS 的示例?(选择三项)

A. Azure Functions

B. Google Cloud Run

C. Amazon ECS

D. Azure ACS

E. Oracle Functions

答案

  1. 错误

  2. 错误

  3. A, C, D

  4. 错误

  5. A

  6. 错误

  7. A, C

  8. A

  9. 正确

  10. A, B, C, E

  11. B

  12. A

  13. B, C, D

第二章:使用 Git 和 GitOps 进行源代码管理

在上一章中,我们了解了现代 DevOps 的核心概念,简要介绍了云计算,并对容器有了基本了解。在本章中,我们将了解源代码管理以及启用 DevOps 的现代方法之一——GitOps

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

  • 什么是源代码管理?

  • Git 快速入门

  • 什么是 GitOps?

  • GitOps 的原则

  • 为什么选择 GitOps?

  • 分支策略和 GitOps 工作流

  • Git 与 GitOps

技术要求

要跟随本章内容,您需要访问基于 Linux 的命令行。如果您使用 macOS,可以使用内建的终端完成所有任务。如果您是 Windows 用户,则必须从git-scm.com/download/win安装GitBash。我们将在接下来的章节中介绍安装说明。

现在,让我们从了解源代码管理开始。

什么是源代码管理?

软件开发涉及编写代码。代码是软件的唯一有形部分,它使软件能够运行。因此,您需要将代码存储在某个地方,以便编写和修改现有软件。代码有两种类型——源代码,它是用高级语言编写的,以及二进制文件,它是从源代码编译而来的。通常,二进制文件只不过是执行软件时运行的功能性应用,而源代码是为了生成二进制文件而编写的人类可读的代码,这也是为什么源代码被称为源代码的原因。

一个软件开发团队有多个成员在编写软件功能,因此他们必须在代码上进行协作。他们不能单独编写代码而不理解应用程序如何工作。有时,多个开发人员会共同开发同一个功能,因此他们需要一个地方来与同伴共享代码。源代码本身是一种资产;因此,我们希望将其安全地存储在中央位置,同时仍然能够便捷地为开发人员提供访问权限,而不妨碍他们的工作。您还需要跟踪更改并对其进行版本控制,因为您可能想知道是什么导致了问题,并能够立即回滚。您还需要保存代码的历史记录,以便了解哪些更改是由谁进行的,并且您还希望有一个源代码同行评审的机制。

如您所见,您可能希望管理源代码的多个方面,因此您将使用源代码管理工具来实现这一目标。

源代码管理工具帮助您管理源代码的各个方面。它提供一个集中位置来存储代码、版本更改,并允许多个开发者在相同的源代码上进行协作。它还通过版本历史记录保存所有的更改,以及我们之前提到的其他内容。有效的源代码管理实践能够提高协作效率;促进开发工作流的高效性;提供版本控制、仓库管理、分支和合并、变更跟踪、审计等功能;并提高软件项目的整体质量和可维护性。一些流行的 SCM 工具有 GitSubversionMercurialCVS。然而,最流行且事实上的 SCM 标准是 Git。所以,接下来我们来学习它。

Git 快速入门

Git 是目前最流行的源代码管理系统,现如今所有开发者都必须学习 Git,至少要掌握基础内容。在本次快速入门中,我们将学习所有基本的 Git 操作,并在后续章节中进行拓展。

Git 是一个分布式版本控制系统。这意味着每个 Git 仓库都是原始仓库的副本,您可以在需要时将其复制到远程位置。在本章中,我们将创建并初始化一个本地 Git 仓库,然后将整个仓库推送到远程位置。

存储在远程中央位置的 Git 仓库也被称为 远程仓库。在这个中央仓库中,所有开发者将本地仓库的更改同步到远程仓库,类似于下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.01.jpg

图 2.1 – Git 分布式仓库模型

首先,让我们在本地安装 Git 并初始化一个本地仓库。稍后我们将查看远程仓库。

安装 Git

根据您的平台和工作站的不同,安装 Git 的方式也不同。在 Ubuntu 上安装 Git,请运行以下命令:

$ sudo apt install -y git-all

对于其他操作系统和平台,您可以按照以下链接中的步骤操作:https://git-scm.com/book/en/v2/Getting-Started-Installing-Git。

要检查 Git 是否安装成功,请运行以下命令:

$ git --version
git version 2.30.2

现在,让我们初始化第一个 Git 仓库。

初始化您的第一个 Git 仓库

要创建一个 Git 仓库,您需要创建一个目录并运行 git init 命令,如下所示:

$ mkdir first-git-repo && cd first-git-repo/
$ git init
Initialized empty Git repository in ~/first-git-repo/.git/

您现在可以使用您的 Git 仓库了。您还可以看到,当您初始化 Git 仓库时,Git 创建了一个隐藏目录 .git,用于跟踪所有的更改和提交。您在仓库中所做的任何更改,Git 都会将它们作为差异(delta)记录,使用 +- 符号表示。我们将在后续部分详细探讨这些内容。现在,让我们在 Git 仓库中创建一个新文件并暂存更改。

暂存代码更改

Git 允许开发者在提交之前暂存他们的更改。这有助于你准备好提交到仓库的内容。暂存区是你更改的临时存放区,你可以通过使用git addgit restore命令将文件添加或移除出暂存区。

让我们在本地 Git 仓库中创建第一个文件并暂存更改:

$ touch file1

或者,你也可以在first-git-repo目录中创建一个空白文件。

现在,我们将检查 Git 是否能检测到我们创建的新增文件。为此,我们需要运行以下命令:

$ git status
On branch master
No commits yet
Untracked files: (use "git add <file>..." to include in what will be committed)
	file1
nothing added to commit but untracked files present (use "git add" to track)

所以,如我们所见,Git 已经检测到file1并告诉我们它目前没有跟踪该文件。为了让 Git 跟踪该文件,让我们运行以下命令:

$ git add file1

现在,让我们再次运行git status来查看有哪些更改:

$ git status
On branch master
No commits yet
Changes to be committed: (use "git rm --cached <file>..." to unstage)
	new file:   file1

如我们所见,Git 现在显示file1作为暂存区中的新文件。你可以继续进行更改,当你完成后,可以使用以下命令提交这些更改:

$ git commit -m "My first commit"
[master (root-commit) cecfb61] My first commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file1

Git 现在已记录了一个包含你更改的提交。现在,让我们再次使用以下命令查看它的状态:

$ git status
On branch master
nothing to commit, working tree clean

Git 现在报告工作树是干净的,没有任何需要提交的内容。它还显示没有未跟踪的文件。现在,让我们修改file1并在其中添加一些文本:

$ echo "This is first line" >> file1
$ cat file1
This is first line

file1现在包含了第一行。让我们继续提交这个更改:

$ git add file1
$ git commit -m "My second commit"
[master 4c55cf5] My second commit
 1 file changed, 1 insertion(+)

正如我们所看到的,Git 现在报告有一个文件已更改,并且有一处插入。记得我们讨论过 Git 只追踪提交之间的增量更改吗?这就是发生的情况。

在介绍中,我们提到 Git 提供了所有提交的历史记录。让我们看看如何显示这个历史记录。

显示提交历史

Git 保留了所有提交的历史记录。要查看你所做的所有提交列表,可以使用以下命令:

$ git log
commit 275d24c62a0e946b8858f562607265c269ec5484 (HEAD -> master)
Author: Gaurav Agarwal <example@gmail.com>
Date:   Wed Apr 19 12:27:13 2023 +0530
    My second commit
commit cecfb61b251f9966f50a4d8bb49742b7af014da4
Author: Gaurav Agarwal < example@gmail.com>
Date:   Wed Apr 19 12:20:02 2023 +0530
    My first commit

如我们所见,Git 已显示了我们两个提交的历史记录。注意,Git 为每个提交标记了一个提交 ID。我们还可以通过使用git diff <first_commit_id> <second_commit_id>命令来深入查看在提交中进行了哪些更改,具体如下:

$ git diff cecfb61b251f9966f50a4d8bb49742b7af014da4 \
275d24c62a0e946b8858f562607265c269ec5484
diff --git a/file1 b/file1
index e69de29..0cbcf32 100644
--- a/file1
+++ b/file1
@@ -0,0 +1 @@
+This is first line

输出清楚地显示,第二次提交已将This is first line添加到file1中。

你突然意识到需要在file1中再添加一行,并希望将其与同一提交一起完成。我们可以通过修改提交来做到这一点。我们将在下一节中讨论这个问题。

修改上一次提交

最佳实践是为某个特定功能的更改创建单独的提交。这有助于你更好地追踪更改,并使审阅者更容易进行审查。反过来,这也使得可视化和管理更清晰。然而,频繁提交也是最佳实践,以确保你的更改不会丢失。幸运的是,Git 允许你向上一次提交中添加更改。

为了演示这一点,让我们修改file1并添加另一行:

$ echo "This is second line" >> file1
$ cat file1
This is first line
This is second line

现在,让我们使用以下命令将更改添加到之前的提交中:

$ git add file1
$ git commit --amend

一旦你运行这个命令,Git 会显示一个提示,允许你修改提交信息(如果需要的话)。它将类似于以下内容:

My second commit
# Please enter the commit message for your changes. Lines
# starting with # will be ignored and an empty message aborts the commit
# Date Wed Apr 19 12:27:13 2023 +0530
# on branch master
# Changes to be commited
#	modified: file1
#

保存此文件(使用 ESC:wq 保存退出 Vim)。这将修改最后一次提交并包含更改。你应该会看到以下输出:

Date: Wed Apr 19 12:27:13 2023 +0530
 1 file changed, 2 insertions(+)

当 Git 修改提交时,你将无法再使用相同的提交 ID 引用之前的提交。相反,Git 会为修改后的提交生成一个单独的SHA-1 id。所以,让我们查看日志,亲自看看这个变化:

$ git log
commit d11c13974b679b1c45c8d718f01c9ef4e96767ab (HEAD -> master)
Author: Gaurav Agarwal <gaurav.agarwal@example.com>
Date:   Wed Apr 19 12:27:13 2023 +0530
    My second commit
commit cecfb61b251f9966f50a4d8bb49742b7af014da4
Author: Gaurav Agarwal < example@gmail.com>
Date:   Wed Apr 19 12:20:02 2023 +0530
    My first commit

现在,让我们再次运行 diff 命令,看看它报告了什么:

$ git diff cecfb61b251f9966f50a4d8bb49742b7af014da4 \
d11c13974b679b1c45c8d718f01c9ef4e96767ab
diff --git a/file1 b/file1
index e69de29..655a706 100644
--- a/file1
+++ b/file1
@@ -0,0 +1,2 @@
+This is first line
+This is second line

输出清楚地显示,第二次提交在 file1 中添加了 This is first lineThis is second line。这样,你就成功修改了提交。

本地仓库的作用就像是在系统上保存文件。然而,既然你需要与他人共享代码,并防止因笔记本操作系统崩溃、盗窃、物理损坏等原因造成的安全隐患,你需要将代码推送到远程仓库。我们将在下一节讨论远程仓库。

理解远程仓库

远程仓库是 Git 仓库在中央位置的副本,供多人访问。这样,开发者可以同时在同一个代码库上工作,并且为你的代码提供了备份。你可以使用各种工具来托管远程仓库。著名的包括GitHubBitbucketGerrit。你可以将它们安装在本地服务器或云服务器上,或者使用软件即服务SaaS)平台在线存储它们。本书将重点讲解 GitHub。

GitHub 是一个基于 web 的平台,帮助开发者进行代码协作。它基于 Git,并允许你托管远程 Git 仓库。GitHub 成立于 2008 年,并在 2018 年被微软收购。它是最受欢迎的开源 SaaS Git 仓库服务之一,包含了几乎所有全球可用的开源代码。

在创建第一个远程仓库之前,我们必须访问 github.com/signup 创建一个账户。

创建完账户后,我们可以继续创建第一个远程 Git 仓库。

创建远程 Git 仓库

在 GitHub 上创建远程 Git 仓库非常简单。访问 github.com/new,设置 first-git-repo,其他字段保持默认,然后点击 创建 仓库 按钮。

完成后,GitHub 会为你提供一些步骤,指导你如何连接到远程仓库。在我们深入这些步骤之前,我们需要为本地 Git 命令行配置一些身份验证,以便与远程仓库交互。让我们来看看。

设置远程 Git 仓库的身份验证

你可以通过以下几种方式与远程 Git 仓库进行身份验证:

  • HTTPS:在这种模式下,Git 使用 HTTPS 协议连接远程 Git 仓库。我们需要在 GitHub 账户中创建一个 HTTPS 令牌,并使用该令牌作为密码来进行身份验证。这个过程要求你每次进行 Git 身份验证时都输入令牌,因此并不是一个方便的选项。

  • SSH:在这种模式下,Git 使用 SSH 协议连接远程 Git 仓库。在使用 SSH 时,我们无需使用密码进行身份验证;相反,我们必须将从 Linux(或如果你使用 Git Bash 的话,可以在 Windows 上)命令行生成的 SSH 密钥对公钥 添加到 GitHub 账户中。这个过程既更安全又更方便。

所以,让我们通过 SSH 认证设置与远程 Git 仓库的连接。

首先,我们必须在本地系统中生成 SSH 密钥对。打开终端,运行以下命令生成 SSH 密钥对:

$ ssh-keygen -t rsa
Generating public/private rsa key pair.

系统会提示你输入其他详细信息。继续按 Enter 键,直到再次出现提示。

一旦生成了密钥对,复制 ~/.ssh/id_rsa.pub 文件中的公钥。

然后,访问 github.com/settings/ssh/new,将公钥粘贴到 Key 字段,并点击 Add SSH Key 按钮。现在,我们已经准备好连接远程 Git 仓库。接下来,让我们看看在本地仓库中需要做哪些配置才能连接到远程仓库。

将本地仓库与远程仓库连接

你需要使用以下命令添加远程条目,以便从本地仓库连接到远程仓库:

$ git remote add origin git@github.com:<your-github-username>/first-git-repo.git

你还可以在 GitHub 仓库的 快速设置 页面找到这些详细信息。

现在我们已经设置好连接,接下来让我们看看如何将更改推送到远程仓库。

将更改从本地仓库推送到远程仓库

要将本地仓库的更改推送到远程仓库,使用以下命令:

$ git push -u origin master
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), 474 bytes | 474.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com: <your-github-username>/first-git-repo.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

现在,刷新远程仓库页面。你应该能看到代码已经同步,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.02.jpg

图 2.2 – 远程仓库中的代码同步

你还可以使用内联编辑器通过 GitHub Web 门户进一步修改文件。虽然不推荐这么做,但我们会这样做来模拟另一位开发者更改了你正在工作的相同文件的情况。

点击 file1,然后点击 铅笔 图标以编辑文件,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.03.jpg

图 2.3 – 编辑远程仓库中的文件

完成此操作后,会打开一个编辑窗口,你可以在文件中进行更改。让我们在文件中添加 This is third line,如图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.04.jpg

图 2.4 – 添加新行

向下滚动–你应该能看到一个提交更改部分,在这里你可以添加提交信息字段并点击提交按钮,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.05.jpg

图 2.5 – 提交更改部分

一旦你点击了那个按钮,你应该能看到第三行,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.06.jpg

图 2.6 – 远程提交的更改

此时,远程仓库已经做出了更改,但你也在本地仓库上进行了一些更改。为了模拟这种情况,让我们也在本地仓库中做一个更改,使用以下命令:

$ echo "This is fourth line" >> file1
$ cat file1
This is first line
This is second line
This is fourth line
$ git add file1
$ git commit -m "Added fourth line"
[master e411e91] Added fourth line
 1 file changed, 1 insertion(+)

现在我们已经在本地仓库提交了更改,让我们尝试使用以下命令将代码推送到远程仓库:

$ git push
To github.com:<your-github-username>/first-git-repo.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'github.com:<your-github-username>/first-git-repo.git'
hint: Updates were rejected because the remote contains work that you do not have locally. 
This is usually caused by another repository pushing to the same ref. You may want to 
first integrate the remote changes.
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

等等,发生了什么?我们尝试推送更改时,远程仓库拒绝了更改,因为在我们推送时,其他人已经在远程仓库做了提交,而我们的更改已经过时。我们需要先将远程仓库的更改拉取到本地仓库,然后再将我们的更改应用到这些现有的更改之上。我们将在下一节中讨论这个问题。

拉取和变基你的代码

拉取代码是指将远程仓库的最新代码下载到本地仓库。变基意味着将你的更改应用到最新的远程提交之上。在远程仓库中,最好的实践是先拉取代码,再将你的更改变基到已经存在的代码上。

让我们使用以下命令执行此操作:

$ git pull --rebase
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0),pack-reused 0
Unpacking objects: 100% (3/3), 652 bytes | 130.00 KiB/s, done.
From github.com:<your-github-username>/first-git-repo
   d11c139..f5b7620  master     -> origin/master
Auto-merging file1
CONFLICT (content): Merge conflict in file1
error: could not apply e411e91... Added fourth line
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply e411e91... Added fourth line

现在,我们遇到了另一个问题:我们无法变基我们的提交,因为我们试图修改一个已经被其他人修改过的文件。Git 要求我们检查文件并做出适当的更改,以确保更改被正确应用。这种情况被称为合并冲突。Git 还会提供包含冲突的文件。让我们用文本编辑器打开该文件并进行适当的更改。

当前文件看起来是这样的:

This is first line
This is second line
<<<<<<< HEAD
This is third line
=======
This is fourth line
>>>>>>> e411e91 (Added fourth line)

HEAD所示的部分是远程仓库中的一行,显示了最近在远程做的更改。e411e91 提交显示了我们在本地所做的更改。让我们将文件更改为以下内容并保存:

This is first line
This is second line
This is third line
This is fourth line

现在,让我们将文件添加到暂存区,并使用以下命令继续执行变基:

$ git add file1
$ git rebase --continue
[detached HEAD 17a0242] Added fourth line
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/master.

现在我们已经变基了更改,让我们通过运行以下命令查看 Git 仓库的状态:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
nothing to commit, working tree clean

如我们所见,我们已经添加了一个需要推送到远程仓库的提交。现在让我们使用以下命令将其推送:

$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 297 bytes | 148.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:<your-github-username>/first-git-repo.git
   f5b7620..17a0242  master -> master

这次,推送成功了。

在大多数情况下,你通常需要复制主代码并进行更改,以测试新特性。你可能还希望在将更改合并到主代码库之前,先让别人审核这些更改。Git 通过使用 Git 分支来管理这一过程。我们将在下一节讨论 Git 分支。

Git 分支

Git 分支是代码库的一个副本(从中创建分支的位置),你可以独立修改和工作,而不会影响主代码库。你在处理新特性时希望创建分支,以确保不影响包含已审核代码的主分支。大多数科技公司通常会有多个环境,其中代码会在不同阶段部署。例如,你可能有一个开发环境来测试特性,一个预生产环境来集成所有特性并测试完整的应用程序,还有一个生产环境,是终端用户访问的应用程序所在的环境。所以,也有可能你会有额外的特定环境分支,用于存放在这些分支上部署的代码。在本章的接下来的章节中,我们将讨论 GitOps,它基于这一基本原则。现在,让我们看看如何创建和管理 Git 分支。

创建和管理 Git 分支

要创建一个 Git 分支,必须先切换到你希望从中分支的分支。例如,在我们的示例仓库中,我们是在 master 分支上工作的。让我们保持在该分支并从中创建一个特性分支。

要创建分支,运行以下命令:

$ git branch feature/feature1

如我们所见,特性分支已经创建。要查看我们现在所在的分支,可以使用git branch命令。让我们现在就这样做:

$ git branch

正如我们通过*符号看到的,我们仍然在master分支上。好的一点是,它还在列表中显示了feature/feature1分支。现在让我们使用以下命令切换到特性分支:

$ git checkout feature/feature1
Switched to branch 'feature/feature1'

现在,我们在feature/feature1分支上。让我们对feature/feature1分支进行一些更改并提交到本地仓库:

$ echo "This is feature 1" >> file1
$ git add file1
$ git commit -m "Feature 1"
[feature/feature1 3fa47e8] Feature 1
 1 file changed, 1 insertion(+)

如我们所见,代码现在已经提交到feature/feature1分支。要查看版本历史记录,让我们运行以下命令:

$ git log
commit 3fa47e8595328eca0bc7d2ae45b3de8d9fd7487c (HEAD -> feature/feature1)
Author: Gaurav Agarwal <gaurav.agarwal@example.com>
Date:   Fri Apr 21 11:13:20 2023 +0530
    Feature 1
commit 17a02424d2b2f945b479ab8ba028f3b535f03575 (origin/master, master)
Author: Gaurav Agarwal <gaurav.agarwal@example.com>
Date:   Wed Apr 19 15:35:56 2023 +0530
    Added fourth line

如我们所见,Feature 1的提交记录出现在 Git 日志中。现在,让我们切换到master分支并再次运行相同的命令:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git log
commit 17a02424d2b2f945b479ab8ba028f3b535f03575 (HEAD -> master, origin/master)
Author: Gaurav Agarwal <gaurav.agarwal@example.com>
Date:   Wed Apr 19 15:35:56 2023 +0530
    Added fourth line
commit f5b7620e522c31821a8659b8857e6fe04c2f2355
Author: Gaurav Agarwal <<your-github-username>@gmail.com>
Date:   Wed Apr 19 15:29:18 2023 +0530
    My third commit

如我们所见,Feature 1的提交更改缺失。这表明两个分支现在是隔离的(并且已经分叉)。现在,改变只存在于本地,还没有推送到远程仓库。为了将更改推送到远程仓库,我们将再次切换到feature/feature1分支。让我们通过以下命令来实现:

$ git checkout feature/feature1
Switched to branch 'feature/feature1'

现在我们已经切换到特性分支,让我们使用以下命令将分支推送到远程仓库:

$ git push -u origin feature/feature1
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 286 bytes | 286.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote:
remote: Create a pull request for 'feature/feature1' on GitHub by visiting:
remote:      https://github.com/<your-github-username>/first-git-repo/pull/new/feature/feature1
remote:
To github.com:<your-github-username>/first-git-repo.git
 * [new branch]      feature/feature1 -> feature/feature1
Branch 'feature/feature1' set up to track remote branch 'feature/feature1' from 'origin'.

这样,我们就成功地将新分支推送到了远程仓库。假设功能已经准备好,我们希望将更改合并到 master 分支。为此,我们需要发起一个拉取请求。我们将在下一部分查看拉取请求。

使用拉取请求

master。拉取请求通常对开发者有用,帮助他们在将代码合并到最终版本之前进行同行评审。评审者通常会检查代码质量、是否遵循最佳实践、以及编码标准是否合适。如果评审者不满意,他们可能会标记出某些更改部分并要求修改。通常会有多轮评审、修改和重新评审。一旦评审者对更改满意,他们可以批准拉取请求,然后请求者就可以合并代码。让我们看一下这个过程:

  1. 让我们尝试发起一个拉取请求,将我们的代码从feature/feature1分支合并到master分支。为此,进入你的 GitHub 仓库,选择拉取请求,然后点击新建拉取请求按钮,如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.07.jpg

图 2.7 – 新拉取请求

  1. 保持master,并在feature/feature1中:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.08.jpg

图 2.8 – 比较更改

  1. 如你所见,它显示了我们在feature/feature1分支上所做的所有更改。点击创建拉取请求按钮来创建拉取请求。在下一页,保持默认设置并点击创建拉取请求按钮:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.09.jpg

图 2.9 – 拉取请求已创建

  1. 如你所见,拉取请求已成功创建。在这里,你可以分配评审者并进行代码评审。一旦评审者批准代码,你就可以将代码合并到 master 分支。现在,让我们点击合并拉取请求按钮,接着点击确认合并按钮,这样就可以合并拉取请求。

  2. 现在,检查master分支是否在 GitHub 上显示了更改。如果显示了,切换到master分支,并将更改拉取到本地仓库。你应该也能在本地仓库中看到这些更改。

这个留给你作为练习。

这是一个关于 Git 的速成课程,帮助你入门。现在,让我们继续了解 GitOps,它使用 Git 作为单一的真理来源,来启动你应用程序和基础设施中的几乎任何内容。

什么是 GitOps?

GitOps 是一种方法,旨在实现 DevOps,使得 Git 成为唯一的真相来源。与其维护一长串脚本和工具来支持这一点,GitOps 专注于为一切编写声明式代码,包括基础设施、配置和应用程序代码。这意味着你可以通过简单地使用 Git 仓库来轻松地创建任何东西。其理念是,你在 Git 仓库中声明所需的内容,背后有工具确保所需的状态始终保持在运行的应用程序和周围的基础设施中。用于启动这些工具的代码也存储在 Git 中,你不需要任何 Git 以外的东西。这意味着包括工具在内的一切都是自动化的。

虽然 GitOps 也使得组织内部的 DevOps 得以实现,但它主要侧重于使用 Git 来管理基础设施的配置和应用程序软件的部署。DevOps 是一个广泛的术语,包含一组原则、过程和工具,旨在使开发人员和运维团队无缝协作,缩短开发生命周期,最终目标是通过 CI/CD 周期更快地交付更好的软件。尽管 GitOps 严重依赖 Git 及其特性,并始终依赖 Git 进行版本控制、查找配置漂移并仅应用增量,DevOps 本身并不依赖任何工具,更加关注概念和过程。因此,你可以在不使用 Git 的情况下实现 DevOps,但无法在没有 Git 的情况下实现 GitOps。简单来说,GitOps 实现了 DevOps,但反过来并不总是成立。

为什么选择 GitOps?

GitOps 为我们提供了以下好处:

  • 它更快地部署更好的软件:GitOps 简化了软件交付。你无需担心为部署类型需要什么工具。相反,你只需在 Git 中提交你的更改,背后的工具会自动进行部署。

  • git revert并恢复你的环境。其理念是,你不需要学习除 Git 之外的其他任何内容来执行发布或回滚。

  • 它提供更好的凭证管理:使用 GitOps,你无需将凭证存储在不同的位置来使部署正常工作。你只需为工具提供对 Git 仓库和二进制仓库的访问权限,GitOps 会处理其余的部分。你可以通过限制开发人员对环境的访问并为他们提供对 Git 的访问,来保持环境的安全。

  • 部署是自我文档化的:因为一切都保存在 Git 中,而 Git 记录了所有提交,所以部署是自动自我文档化的。你可以通过查看提交历史,准确知道是谁在什么时间部署了什么内容。

  • 它促进共享所有权和知识:由于 Git 是组织内所有代码和配置的唯一真理来源,团队可以在一个地方理解事情是如何实现的,而不必依赖其他团队成员或存在歧义。这有助于推动团队内代码和知识的共享所有权。

现在我们已经了解了 GitOps 的好处,让我们来看看它的关键原则。

GitOps 的原则

GitOps 有以下关键原则:

  • 它以声明性方式描述整个系统:拥有声明性代码是 GitOps 的第一原则。这意味着,与其提供构建基础设施的指令,应用相关配置并部署应用程序,不如声明所需的最终状态。这意味着您的 Git 仓库始终保持唯一的真理来源。由于声明性变更是幂等的,您不必担心系统的状态,因为它最终会与 Git 中的代码保持一致。

  • 它使用 Git 来版本化所需的系统状态:由于 Git 是一个优秀的版本控制系统,您不必过于担心如何部署和回滚。一次简单的 Git 提交意味着一个新的部署,而 Git 回滚意味着回滚。这意味着,除了确保 Git 仓库反映您需要的内容外,您不需要担心其他任何问题。

  • 它使用工具自动应用已批准的更改:由于您已经将一切存储在 Git 中,您可以使用工具来查找仓库中的更改,并自动将其应用到环境中。您还可以有多个分支将更改应用到不同的环境中,并使用基于拉取请求的审批和控制过程,确保只有已批准的更改最终进入您的环境。

  • 它使用自愈代理来警报并纠正任何偏离:我们有工具可以自动将 Git 中的任何更改应用到环境中。然而,我们还需要自愈代理来警报任何与仓库的偏离。例如,假设有人手动从环境中删除了一个容器,但没有从 Git 仓库中删除它。在这种情况下,代理应该警报团队,并重新创建该容器以纠正偏离。这意味着没有办法绕过 GitOps,Git 始终是唯一的真理来源。

使用现代 DevOps 工具和技术来实现并遵循这些原则非常简单,我们将在 第十一章第十二章 中实际实施它们。然而,在本章中,我们将通过分支策略和 GitOps 工作流来审视这些设计原则。

分支策略和 GitOps 工作流

GitOps 至少需要两种 Git 仓库才能正常工作:应用仓库,它是触发构建的来源,以及环境仓库,它包含所有基础设施和配置即代码CaC)。所有的部署都来自环境仓库,而对代码仓库的变化驱动部署。GitOps 有两种主要的部署模型:推送模型拉取模型。我们来逐一讨论这两种模型。

推送模型

推送模型将 Git 仓库中的任何变化推送到环境中。下图详细解释了这一过程:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.010.jpg

图 2.10 – 推送模型

推送模型本身并不关注现有配置,只会对 Git 仓库中的变化作出反应。因此,你需要设置某种形式的监控,来了解是否存在任何偏差。此外,推送模型需要在工具中存储所有环境凭证,因为它与环境交互并且需要管理部署。通常,我们使用JenkinsCircleCITravis CI 来实现推送模型。虽然不推荐使用推送模型,但在使用Terraform进行云资源配置或使用Ansible进行配置管理时,它是不可避免的,因为它们本身就是基于推送的模型。现在,让我们更仔细地看看拉取模型。

拉取模型

拉取模型是一种基于代理的部署模型(也叫做基于操作员的部署模型)。在你的环境中,代理(或操作员)监控 Git 仓库的变化,并根据需要应用这些变化。操作员不断地将现有的配置与环境仓库中的配置进行比较,如果需要,则应用变化。下图详细展示了这一过程:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.011.jpg

图 2.11 – 拉取模型

拉取模型的优点是,它在监控和响应环境变化的同时,也能响应仓库中的变化。这确保了任何与 Git 仓库不匹配的变化都会从环境中恢复。它还会通过邮件通知、工单工具或 Slack 通知,提醒运维团队任何无法修复的问题。由于操作员与代码部署所在的环境处于同一环境中,因此我们不需要在工具中存储凭证。相反,这些凭证会安全地存储在环境中。你甚至可以完全不存储任何凭证,使用像 Kubernetes 这样的工具时,你可以利用基于角色的访问控制RBAC)和服务账户来管理操作员对环境的管理。

提示

在选择 GitOps 模型时,最佳实践是检查是否可以实现基于拉取的模型,而不是基于推送的模型。仅当无法使用基于拉取的模型时,才实施基于推送的模型。在基于推送的模型中实施轮询是一个好主意,通过定期运行cron作业等调度,定期运行推送以确保没有配置漂移。

我们不能单靠其中一种模型生存,因此大多数组织采用混合模型来运行 GitOps。这种混合模型结合了推送和拉取模型,并侧重于使用拉取模型。当无法使用拉取模型时,它使用推送模型。现在,让我们了解如何构建我们的 Git 仓库,以便它可以实施 GitOps。

Git 仓库的结构化

要实施 GitOps,我们至少需要两个仓库:应用程序仓库和环境仓库。这并不意味着您不能将两者合并,但是为了简单起见,让我们分别看看它们。

应用程序仓库

应用程序仓库存储应用程序代码。这是一个您的开发人员可以积极开发您业务运行的产品的代码仓库。通常,您的构建源自这个应用程序代码,并且它们最终成为容器(如果我们使用基于容器的方法)。您的应用程序仓库可能有环境特定的分支,也可能没有。大多数组织将应用程序仓库独立于环境,并专注于使用分支策略构建语义化的代码版本。现在,有多种分支策略可用于管理您的代码,例如GitflowGitHub flow和任何适合您需求的其他分支策略。

Gitflow 是组织中使用的最流行的分支策略之一。也可以说,它是最复杂的之一,因为它需要多种类型的分支(例如主分支、热修复分支、发布分支、开发分支和功能分支)并且有一个严格的结构。Gitflow 的结构如下图所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.012.jpg

图 2.12 – Gitflow 结构

使用 GitHub flow 的简化方法是做事情更少的分支,并且更易于维护。通常,它包含一个单一的主分支和许多功能分支,这些分支最终与主分支合并。主分支始终具有准备部署到环境中的软件。您在主分支中标记和版本化代码,选择并部署它,测试它,然后将其推广到更高的环境中。以下图详细展示了 GitHub flow:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.013.jpg

图 2.13 – GitHub flow

请注意,您可以根据自己的需求和适用情况自由创建自己的分支策略。

提示

如果你有一个庞大的团队、大型单体仓库,并且有多个并行发布版本,可以选择 Gitflow。如果你在一个快速发展的组织中工作,每周发布几次更新,并且没有并行发布的概念,可以选择 GitHub flow。GitHub flow 通常适用于微服务,在这种情况下,变更较小且快速。

通常,应用程序仓库不需要过多担心环境问题;它们可以更多地专注于创建可部署的软件版本。

环境仓库

环境仓库存储运行应用程序代码所需的环境特定配置。因此,它们通常会包含基础设施即代码IaC),如 Terraform 脚本、Ansible 剧本形式的 CaC,或 Kubernetes 清单,这些通常有助于部署我们从应用程序仓库构建的代码。

环境仓库应遵循特定环境的分支策略,其中每个分支代表一个特定的环境。对于这些场景,您可以使用基于拉取请求的门控。通常,您会从开发分支构建开发环境,然后提出拉取请求将更改合并到暂存分支。从暂存分支到生产分支,您的代码随着环境的变化而推进。如果有 10 个环境,您可能会在环境仓库中有 10 个不同的分支。以下图示展示了您可能希望遵循的环境仓库分支策略:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_2.014.jpg

图 2.14 – 环境仓库

环境仓库旨在作为您环境的唯一可信来源。您添加到仓库中的配置将直接应用于您的环境。

提示

尽管可以将环境和应用程序仓库合并为一个,但最佳实践是将它们分开。GitOps 通过分别使用应用程序仓库和环境仓库,为 CI 和 CD 过程提供了明确的分离。

现在我们已经详细介绍了 Git 和 GitOps,接下来我们来看看为什么 Git 和 GitOps 是相关但不同的概念。

Git 与 GitOps

下表总结了 Git 和 GitOps 之间的区别:

GitGitOps
定义Git 是一种分布式版本控制系统,用于跟踪源代码和其他文件的更改。它允许多个开发者高效地协作和管理代码修订。GitOps 是一组实践和原则,通过将 Git 作为管理和自动化基础设施和应用程序部署与操作的唯一可信来源。
重点主要集中于源代码的版本控制和协作。专注于通过基于 Git 的 DevOps 工作流自动化和管理基础设施和应用程序的部署与操作。
用法在软件开发项目中广泛用于版本控制和协作。开发者使用 Git 来跟踪变更、管理分支和合并代码。用于声明性定义和管理基础设施和应用程序配置。Git 存储库作为定义所需状态和驱动自动化的中心枢纽。
核心组件存储库、分支、提交和拉取请求。Git 存储库、声明性配置文件(如 YAML)、Kubernetes 清单、CI/CD 流水线和部署工具如 Argo CD 或 Flux。
工作流程开发者克隆、修改、提交和推送更改到远程存储库。他们通过拉取请求和分支合并进行协作。基础设施和应用配置存储在 Git 存储库中。对这些配置的更改会触发自动化流程,如 CI/CD 流水线或协调循环,以将这些更改应用到目标环境中。
好处为软件开发团队提供高效的版本控制、协作和代码管理。促进基础设施和应用程序的代码、配置版本控制和声明式管理。简化基础设施部署,提供一致性,并启用自动化工作流程。
关注领域源代码管理。基础设施和应用程序部署与管理。
示例GitHub、Bitbucket、GitLab。Argo CD、Flux、Jenkins X、Weave Flux。

请记住,虽然 Git 是一个版本控制系统,但 GitOps 通过利用 Git 作为基础设施和应用程序配置的中心真实来源,允许自动化部署和管理 DevOps 工作流程。

总结

本章涵盖了 Git、GitOps、我们为什么需要它、其原则以及各种 GitOps 部署。我们还研究了可以创建来实现 GitOps 的不同类型的存储库,以及每种存储库的分支策略选择。

你现在应该能够做到以下几点:

  • 理解什么是源代码管理及其在现代 DevOps 中的必要性

  • 创建一个 Git 存储库并尝试使用cloneaddcommitpushpullbranchcheckout命令进行操作

  • 理解 GitOps 是什么以及它如何适应现代 DevOps 的背景

  • 理解为什么我们需要 GitOps 以及它如何实现现代 DevOps

  • 理解 GitOps 的显著原则

  • 理解如何使用有效的分支策略来实现基于组织结构和产品类型的 GitOps

在下一章中,我们将深入理解容器并探讨 Docker。

问题

回答以下问题以测试你对本章的理解:

  1. 以下关于 Git 的说法哪些是正确的?(选择三个)

    1. 这是一个分布式 SCM 平台

    2. 这是一个集中式 SCM 平台

    3. 它允许多个开发者协作

    4. 它具有提交和分支

  2. 在 Git 术语中,Git checkout 是什么意思?

    1. 从远程同步代码到本地

    2. 从一个分支切换到另一个分支

    3. 审查并批准拉取请求

  3. 在 GitOps 中,什么构成了唯一的真理来源

    1. Git 仓库

    2. 存储在数据存储中的配置

    3. 秘密管理系统

    4. 人工制品库

  4. 以下哪些选项是 GitOps 的部署模型?(选择两个)

    1. 推送模型

    2. 拉取模型

    3. 错开模型

  5. 是否应该在环境仓库中使用 Gitflow?

  6. 对于具有多个并行开发和多个版本发布的单体应用,最适合的 Git 分支策略是什么?

    1. Gitflow

    2. GitHub 流

    3. 混合 GitHub 流

  7. 哪种是 GitOps 推荐的部署模型?

    1. 推送模型

    2. 拉取模型

    3. 错开模型

答案

  1. A,C,D

  2. B

  3. A

  4. A,B

  5. A

  6. B

第三章:使用 Docker 容器化

在上一章中,我们讨论了 Git 的源代码管理,在那里我们进行了 Git 的速成学习,然后探讨了 GitOps 以及它如何塑造现代 DevOps 实践。

在本章中,我们将亲自操作并探索 Docker —— 当今最常用的容器运行时。到本章结束时,你应该能够安装并配置 Docker,运行你的第一个容器,并进行监控。本章内容也将为后续章节奠定基础,因为我们将在后面的演示中使用相同的设置。

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

  • 安装工具

  • 安装 Docker

  • 介绍 Docker 存储驱动程序和卷

  • 运行你的第一个容器

  • Docker 日志记录与日志驱动

  • 使用 Prometheus 进行 Docker 监控

  • 使用 Docker Compose 进行声明式容器管理

技术要求

本章内容需要一台运行 Ubuntu 18.04 Bionic LTS 或更高版本的 Linux 机器,并且具有 sudo 权限。我们将在本书中使用 Ubuntu 22.04 Jammy Jellyfish,但你也可以自由选择任何操作系统。我将提供其他安装说明的链接。

你还需要克隆以下 GitHub 仓库以进行一些练习:github.com/PacktPublishing/Modern-DevOps-Practices-2e

我们在上一章中详细讨论了 Git;因此,你可以轻松地利用这些知识克隆仓库。现在,让我们继续安装 Docker。

安装 Docker

我们将在 Ubuntu 系统上安装 Docker。对于其他操作系统,请参考 docs.docker.com/engine/install/

要安装 Docker,我们需要安装支持工具,允许 apt 包管理器通过 HTTPS 下载 Docker。我们使用以下命令来完成此操作:

$ sudo apt-get update
$ sudo apt-get install -y ca-certificates curl gnupg

下载 Docker gpg 密钥并将其添加到 apt 包管理器:

$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ sudo chmod a+r /etc/apt/keyrings/docker.gpg

然后,你需要将 Docker 仓库添加到你的 apt 配置中,以便从那里下载包:

$ echo \
  "deb [arch="$(dpkg --print-architecture)" \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" \
stable" | sudo tee /etc/apt/sources.list.d/docker.list \
> /dev/null

最后,通过以下命令安装 Docker 引擎:

$ sudo apt-get update
$ sudo apt-get -y install docker-ce docker-ce-cli \
containerd.io docker-buildx-plugin docker-compose-plugin

要验证 Docker 是否安装成功,请运行以下命令:

$ sudo docker --version

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

Docker version 24.0.2, build cb74dfc

接下来,你需要做的事是允许普通用户使用 Docker。你希望你的用户在构建和运行容器时不是以 root 用户身份进行操作。为此,请运行以下命令:

$ sudo usermod -a -G docker <username>

要使更改应用到你的配置文件中,请从虚拟机退出并重新登录。

现在 Docker 已经在你的机器上完全设置好,让我们运行一个 hello-world 容器来验证这一点:

$ docker run hello-world

你应该看到以下输出:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
719385e32844: Pull complete
Digest: sha256:fc6cf906cbfa013e80938cdf0bb199fbdbb86d6e3e013783e5a766f50f5dbce0
Status: Downloaded newer image for hello-world:latest
Hello from Docker!

你还会看到以下信息,告诉你在后台发生了什么,为什么会在屏幕上显示 Hello from Docker! 消息:

This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
 1\. The Docker client contacted the Docker daemon.
 2\. The Docker daemon pulled the hello-world image from Docker Hub.(amd64).
 3\. The Docker daemon created a new container from that image that runs the executable 
that produces the output you are currently reading.
4\. The Docker daemon streamed that output to the Docker client, which sent it to your 
Terminal:
To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/
For more examples and ideas, visit:
 https://docs.docker.com/get-started/

所有这些有用的信息是不言自明的。简单解释一下 Docker Hub,它是一个公共的 Docker 容器注册表,托管着许多供像你我这样的用户使用的 Docker 镜像。

由于 Docker 使用分层架构,大多数 Docker 镜像都是从一个或多个托管在 Docker Hub 上的基础镜像派生出来的。因此,请为自己创建一个 Docker Hub 账户,以便托管容器并与世界分享。

大多数组织可能希望保持镜像的私密性,因此你可以选择在 Docker Hub 中创建私有仓库。你还可以使用 Google 容器注册表 (GCR) 等 SaaS 服务托管自己的内部 Docker 注册表,或者安装如 Sonatype NexusJFrog Artifactory 这样的工件仓库。不管你选择哪种工具,机制和工作原理始终是相同的。

介绍 Docker 存储驱动和卷

Docker 容器是短暂的工作负载。你在容器文件系统上存储的任何数据,在容器消失后都会被清除。数据在容器生命周期内存储在磁盘上,但不会超出其生命周期。实际上,大多数应用程序都是有状态的。它们需要在容器生命周期之外存储数据,并希望数据能够持久化。

那么,我们该如何进行呢?Docker 提供了几种存储数据的方式。默认情况下,所有数据都存储在可写容器层上,而该层是临时的。可写容器层通过存储驱动与主机文件系统交互。由于存在抽象层,写入容器层的文件比直接写入主机文件系统要慢。

为了解决这个问题并提供持久存储,Docker 提供了卷、绑定挂载和 tmpfs。通过这些,你可以直接与主机文件系统(以及在 tmpfs 的情况下与内存)交互,并节省大量的 每秒 I/O 操作 (IOPS),从而提高性能。尽管本节侧重于容器文件系统的存储驱动,但讨论 Docker 中的多种数据存储选项也很有价值,以便提供背景信息。

Docker 数据存储选项

每个选项都有其使用场景和权衡。让我们来看一下每个选项以及应该在哪些地方使用它们。

Docker 卷将数据直接存储在主机的文件系统中。它们不使用中间的存储驱动层,因此写入卷的速度更快。它们是持久化数据的最佳方式。Docker 将卷存储在 /var/lib/docker/volumes 中,并假设除了 Docker 守护进程外,没有人可以修改它们上的数据。

因此,卷提供以下功能:

  • 提供与主机文件系统的某些隔离。如果你不希望其他进程与数据交互,那么卷应该是你的选择。

  • 你可以与多个容器共享一个卷。

  • 卷可以是命名的或匿名的。Docker 将匿名卷存储在一个具有唯一随机名称的目录中。

  • 数据卷使你能够将数据远程存储或通过云提供商使用数据卷驱动进行存储。如果多个容器共享同一个数据卷以提供多实例的主动-主动配置,这非常有帮助。

  • 即使容器被删除,数据卷中的数据也会持久存在。

现在,让我们来看一下另一种存储选项——绑定挂载。

绑定挂载

绑定挂载与数据卷非常相似,但有一个显著的区别:它允许你将现有的主机目录作为容器的文件系统进行挂载。这使你能够与 Docker 容器共享重要文件,例如/etc/resolv.conf

绑定挂载也允许多个进程与 Docker 一起修改数据。因此,如果你将容器数据与另一个不在 Docker 中运行的应用程序共享,绑定挂载是最佳选择。

tmpfs 挂载

tmpfs 挂载将数据存储在内存中。它们不会在磁盘上存储任何数据——无论是容器还是主机文件系统。你可以使用它们来存储敏感信息和容器生命周期中的非持久状态。

挂载数据卷

如果你将一个已经包含文件的主机目录挂载到容器的空数据卷上,容器就可以看到主机中存储的文件。这是为容器预填充文件的绝佳方式。然而,如果该目录在主机文件系统中不存在,Docker 会自动创建该目录。如果数据卷非空且主机文件系统中已经包含文件,Docker 将遮蔽该挂载。这意味着,在 Docker 卷挂载到该目录时,你不会看到原始文件,但文件并没有被删除,你可以通过卸载 Docker 卷来恢复它们。

我们将在下一部分讨论 Docker 存储驱动。

Docker 存储驱动

存储驱动类型有很多种。以下是一些最受欢迎的存储驱动:

  • overlay2:这是一个适用于生产环境的驱动,是 Docker 推荐的存储选择。它适用于大多数环境。

  • devicemapper:这是 RHEL 和 CentOS 7 及以下版本的设备驱动,这些版本不支持 overlay2。如果你的容器中有写密集型操作,你可以使用此驱动。

  • btrfszfs:这些驱动程序是写密集型的,提供许多功能,例如允许快照,并且仅在主机使用 btrfszfs 文件系统时才可以使用。

  • vfs:此存储驱动仅应在没有写时复制文件系统的情况下使用。它非常慢,不建议在生产环境中使用。

让我们集中讨论两种最受欢迎的驱动——overlay2devicemapper

overlay2

overlay2是大多数操作系统中默认和推荐的存储驱动,除了 RHEL 7、CentOS 7 及更早版本。它们使用基于文件的存储,并在读取操作多于写入操作时表现最佳。

devicemapper

devicemapper 是基于块的存储,当写操作比读操作多时,它的表现最佳。虽然它与 CentOS 7、RHEL 7 及以下版本兼容,并且是默认存储驱动(因为它们不支持 overlay2),但在这些支持 overlay2 的新版本操作系统中,当前不推荐使用它。

提示

尽可能使用 overlay2,但如果你有特定的用例不适合使用它(比如容器写操作过于频繁),那么 devicemapper 是一个更好的选择。

配置存储驱动

在本讨论中,我们将配置 overlay2 作为存储驱动。虽然它是默认配置,如果你跟随本书的内容,你可以跳过这一步,但如果你想将其更改为其他驱动,还是值得了解一下。

首先,让我们列出当前的存储驱动:

$ docker info | grep 'Storage Driver'
Storage Driver: overlay2

我们可以看到当前的存储驱动已经是 overlay2。如果我们需要更改为 devicemapper,下面我们来看如何操作。

使用你选择的编辑器编辑 /etc/docker/daemon.json 文件。如果你使用 vim,可以运行以下命令:

$ sudo vim /etc/docker/daemon.json

storage-driver 条目添加到 daemon.json 配置文件中:

{
  "storage-driver": "devicemapper"
}

然后,重启 Docker 服务:

$ sudo systemctl restart docker

检查 Docker 服务的状态:

$ sudo systemctl status docker

现在,重新运行 docker info 查看我们得到的结果:

$ docker info | grep 'Storage Driver'
Storage Driver: devicemapper
WARNING: The devicemapper storage-driver is deprecated, and will be removed in a future 
release.
         Refer to the documentation for more information: https://docs.docker.com/go/
storage-driver/
WARNING: devicemapper: usage of loopback devices is strongly discouraged for production 
use.
         Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.

这里,我们可以看到 devicemapper 存储驱动。我们还看到几个警告,表示 devicemapper 存储驱动已被弃用,并将在未来的版本中移除。

因此,除非有特定需求,否则我们应该坚持使用默认配置。

所以,让我们撤销之前的更改,并再次将存储驱动设置为 overlay2

$ sudo vim /etc/docker/daemon.json

daemon.json 配置文件中,将 storage-driver 条目修改为 overlay2

{
  "storage-driver": "overlay2"
}

然后,重启 Docker 服务并检查其状态:

$ sudo systemctl restart docker
$ sudo systemctl status docker

如果你重新运行 docker info,你将看到存储驱动变为 overlay2,所有警告也将消失:

$ docker info | grep 'Storage Driver'
Storage Driver: overlay2

提示

更改存储驱动会清除磁盘上现有的容器,因此在进行此操作时请小心,并确保在生产环境中安排适当的停机时间。如果你这样做,你还需要重新拉取镜像,因为本地镜像将不再存在。

现在我们已经在机器上安装了 Docker 并配置了正确的存储驱动,是时候运行我们的第一个容器了。

运行你的第一个容器

你可以通过 Docker 容器镜像来创建 Docker 容器。虽然我们将在后续章节中讨论容器镜像及其架构,但一个很好的形象化方式是将其视为包含所有文件、应用程序库和依赖项的副本,这些内容构成了你的应用环境,类似于虚拟机镜像。

要运行 Docker 容器,我们可以使用 docker run 命令,它的结构如下:

$ docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]

让我们通过一些实际示例来看一下 docker run 命令及其变种。

在最简单的形式下,你只需输入以下命令来使用 docker run

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582
838d92970a09f323
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
...

如你所记得,我们在安装 Docker 时使用了这个命令。在这里,我有意省略了tagoptionscommandarguments。我们将通过多个示例来展示它的实际使用场景。

由于我们没有提供tag,Docker 自动假设taglatest,因此,如果你查看命令输出,你将看到 Docker 正在从 Docker Hub 拉取hello-world:latest镜像。

现在,让我们来看一个带有特定版本标签的示例。

从有版本标记的镜像运行容器

我们可以使用以下命令运行nginx:1.18.0

$ docker run nginx:1.18.0
Unable to find image 'nginx:1.18.0' locally
1.18.0: Pulling from library/nginx
852e50cd189d: Pull complete
48b8657f2521: Pull complete
b4f4d57f1a55: Pull complete
d8fbe49a7d55: Pull complete
04e4a40fabc9: Pull complete
Digest: sha256:2104430ec73de095df553d0c7c2593813e01716a48d66f
85a3dc439e050919b3
Status: Downloaded newer image for nginx:1.18.0
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform
configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-
listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-
envsubst-on-templates.sh
/docker-entrypoint.sh: Configuration complete; ready for
start up

注意,提示符在此后将会卡住。这是有原因的:nginx是一个长期运行的进程,也就是守护进程。由于 NGINX 是一个需要持续监听 HTTP 请求的 Web 服务器,所以它应该永远不会停止。而hello-world应用程序的唯一任务是打印消息并退出。NGINX 的用途完全不同。

现在,没人会为一个 Web 服务器保持打开的 Bash 会话,所以必须有某种方法将其运行在后台。你可以通过分离模式来运行容器。我们将在下一节中详细讨论这一点。

在后台运行 Docker 容器

要将 Docker 容器作为守护进程在后台运行,你可以使用带有-d标志的docker run命令:

$ docker run -d nginx:1.18.0
beb5dfd529c9f001539c555a18e7b76ad5d73b95dc48e8a35aecd7471ea938fc

如你所见,它仅打印一个随机 ID 并将控制权返回到 shell。

容器故障排除

要查看容器内发生的情况,你可以使用docker logs命令。但在使用之前,我们需要知道容器的 ID 或名称,以便查看容器的日志。

要获取主机中正在运行的容器列表,请运行以下命令

$ docker ps
CONTAINER ID  IMAGE         COMMAND        CREATED        STATUS        PORTS   NAMES
beb5dfd529c9  nginx:1.18.0  "/docker-      2 minutes ago  Up 2 minutes  80/tcp  fervent_
                            entrypoint.…"                                       shockley

上面的命令列出了我们刚刚启动的 NGINX 容器。除非你为容器指定了特定的名称,否则 Docker 会分配一个随机名称。在这种情况下,它将容器命名为fervent_shockley。它还会为每个容器分配一个唯一的容器 ID,例如beb5dfd529c9

你可以使用容器 ID 或容器名称与容器进行交互,以列出日志。我们这次使用容器 ID:

$ docker logs beb5dfd529c9
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform 
configuration
...
/docker-entrypoint.sh: Configuration complete; ready for start up

如你所见,它打印出了与我们在前台运行时相似的日志输出。

实际上,除非你需要用 BusyBox 调试某些问题,否则你 90%的时间都会使用docker logs。BusyBox 是一个轻量级的 Shell 容器,可以帮助你排查和调试容器的问题——主要是网络问题。

让我们让 BusyBox 为我们回显Hello World!

$ docker run busybox echo 'Hello World!'
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
325d69979d33: Pull complete
Digest: sha256:560af6915bfc8d7630e50e212e08242d37b63bd5c1ccf9bd4acccf116e262d5b
Status: Downloaded newer image for busybox:latest
Hello World!

如我们所见,Docker 从 Docker Hub 拉取了最新的busybox镜像并运行了echo 'Hello World'命令。

你还可以通过使用-it标志在交互模式下使用 BusyBox,这将帮助你在 BusyBox shell 上运行一系列命令。最好还加上--rm标志,告诉 Docker 在我们退出 shell 后清理容器,像这样:

$ docker run -it --rm busybox /bin/sh
/ # echo 'Hello world!'
Hello world!
/ # wget http://example.com
Connecting to example.com (93.184.216.34:80)
saving to 'index.html'
index.html           100% |***********************************
****|  1256  0:00:00 ETA
'index.html' saved
/ # exit

在列出所有容器时,我们没有看到 busybox 容器:

$ docker ps -a
CONTAINER ID  IMAGE   COMMAND        CREATED     STATUS    PORTS   NAMES
beb5dfd529c9  nginx:  "/docker-      17 minutes  Up 17     80/tcp  fervent_
              1.18.0  entrypoint.…"  ago         minutes           shockley

你可以使用各种其他标志来配置容器,每个标志都有特定的用途。让我们看几个常见的。

综合起来

高可用 NGINX 容器的最佳设置应类似于以下内容:

$ docker run -d --name nginx --restart unless-stopped \
-p 80:80 --memory 1000M --memory-reservation 250M nginx:1.18.0

让我们更详细地看看这个:

  • -d:以守护进程模式在分离模式下运行。

  • --name nginx:为容器命名为 nginx

  • --restart unless-stopped:除非手动停止,否则始终在失败时自动重启,并且在 Docker 守护进程启动时自动启动。其他选项包括 noon_failurealways

  • -p 80:80:将主机端口 80 转发到容器端口 80。这允许你将容器暴露到主机网络。

  • --memory 1000M:将容器的内存消耗限制为 1000M。如果内存超出此限制,容器会停止,并根据 --restart 标志进行处理。

  • --memory-reservation 250M:为容器分配一个软限制 250M 内存,如果服务器内存不足时使用。

在接下来的部分中,我们将深入探讨其他标志,并进行更多实操。

提示

考虑使用 unless-stopped 代替 always,因为它允许在需要进行维护时手动停止容器。

现在,让我们列出容器,看看我们得到什么:

$ docker ps -a
CONTAINER ID  IMAGE   COMMAND         CREATED     STATUS   PORTS       NAMES
06fc749371b7  nginx   "/docker-       17 seconds  Up 16    0.0.0.0:    nginx   
                      entrypoint.…"   ago         seconds  80->80/tcp  
beb5dfd529c9  nginx:  "/docker-       22 minutes  Up 22    80/tcp      fervent_shockley
              1.18.0  entrypoint.…"   ago         minutes

如果仔细观察,你会看到一个名为 nginx 的容器,并且有一个从 0.0.0.0:80 -> 80 的端口转发。

现在,让我们在主机的 localhost:80 上使用 curl 来查看结果:

$ curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

我们得到了 NGINX 欢迎信息。这意味着 NGINX 正常运行,我们可以从机器上访问它。如果你已将机器的端口 80 暴露到外部世界,你也可以通过浏览器访问它,如下所示:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_3.01.jpg

图 3.1 – NGINX 欢迎页面

你可能还希望偶尔重启或删除容器。我们将在下一节中介绍如何做到这一点。

重启和删除容器

要重启容器,你必须先停止它们,然后再启动它们。

要停止容器,请运行以下命令:

$ docker stop nginx

要启动容器,请运行以下命令:

$ docker start nginx

如果你想完全删除容器,首先需要停止容器,然后使用以下命令删除它:

$ docker stop nginx && docker rm nginx

或者,你可以使用以下命令一次性完成:

$ docker rm -f nginx

现在,让我们看看如何使用 journald 和 Splunk 等工具监控容器。

Docker 日志和日志驱动程序

Docker 不仅改变了应用程序的部署方式,还改变了日志管理的工作流。容器不再将日志写入文件,而是将日志写入控制台(stdout/stderr)。然后,Docker 使用日志驱动程序将容器日志导出到指定的目标位置。

容器日志管理

日志管理是 Docker 中的一个重要功能,就像任何应用程序一样。然而,由于 Docker 工作负载的临时性质,它变得更加重要,因为当容器被删除或遇到任何问题时,我们可能会丢失文件系统和日志。因此,我们应该使用日志驱动程序将日志导出到特定的位置并存储和持久化它。如果你有日志分析解决方案,那么将日志存储在其中是最好的地方。Docker 通过日志驱动程序支持多个日志目标。让我们来看看。

日志驱动程序

在撰写时,以下日志驱动程序可用:

  • none: 容器没有日志,因此日志不会存储在任何地方。

  • local: 日志以自定义格式本地存储,从而最小化开销。

  • json-file: 日志文件以 JSON 格式存储。这是默认的 Docker 日志驱动程序。

  • syslog: 该驱动程序也使用 syslog 来存储 Docker 日志。当你将 syslog 作为默认日志机制时,这个选项是有意义的。

  • journald: 使用 journald 存储 Docker 日志。你可以使用 journald 命令行来浏览容器和 Docker 守护进程日志。

  • gelf: 将日志发送到 Graylog 扩展日志格式 (GELF) 端点,如 Graylog 或 Logstash。

  • fluentd: 将日志发送到 Fluentd。

  • awslogs: 将日志发送到 AWS CloudWatch。

  • splunk: 使用 HTTP 事件收集器将日志发送到 Splunk。

  • etwlogs: 将日志发送到 Windows 事件追踪 (ETW) 事件。只能在 Windows 平台上使用。

  • gcplogs: 将日志发送到 Google Cloud Logging。

  • logentries: 将日志发送到 Rapid7 Logentries。

尽管这些选项都可行,我们将重点查看 journald 和 Splunk。journald 是一种本地操作系统服务监控选项,而 Splunk 是最著名的日志分析和监控工具之一。现在,让我们了解如何配置日志驱动程序。

配置日志驱动程序

让我们从查找当前的日志驱动程序开始:

$ docker info | grep "Logging Driver"
Logging Driver: json-file

当前,默认日志驱动程序设置为 json-file。如果我们希望使用 journald 或 Splunk 作为默认日志驱动程序,必须在 daemon.json 文件中配置默认日志驱动程序。

使用你喜欢的编辑器编辑 /etc/docker/daemon.json 文件。如果你使用 vim,运行以下命令:

$ sudo vim /etc/docker/daemon.json

daemon.json 配置文件中添加 log-driver 项:

{
  "log-driver": "journald"
}

然后,重启 Docker 服务:

$ sudo systemctl restart docker

检查 Docker 服务的状态:

$ sudo systemctl status docker

现在,重新运行 docker info 查看我们得到的结果:

$ docker info | grep "Logging Driver"
Logging Driver: journald

现在,journald 是默认的日志驱动程序,让我们启动一个新的 NGINX 容器并查看日志:

$ docker run --name nginx-journald -d nginx
66d50cc11178b0dcdb66b114ccf4aa2186b510eb1fdb1e19d563566d2e96140c

现在,让我们查看 journald 日志,看看我们能得到什么:

$ sudo journalctl CONTAINER_NAME=nginx-journald
...
Jun 01 06:11:13 99374c32101c fb8294aece02[10826]: 10-listen-on-ipv6-by-default.sh: info: 
Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
...
Jun 01 06:11:13 99374c32101c fb8294aece02[10826]: 2023/06/01 06:11:13 [notice] 1#1: start 
worker process 30
...

我们可以在日志中查看到这些内容。

同样,我们可以配置 Splunk 日志驱动程序将数据发送到 Splunk 进行分析和可视化。让我们来看看。

使用你喜欢的编辑器编辑 /etc/docker/daemon.json 文件。如果你使用 vim,运行以下命令:

$ vim /etc/docker/daemon.json

将 log-driver 条目添加到daemon.json配置文件:

{
  "log-driver": "splunk",
  "log-opts": {
    "splunk-token": "<Splunk HTTP Event Collector token>",
    "splunk-url": "<Splunk HTTP(S) url>"
  }
}

然后,重启 Docker 服务:

$ sudo systemctl restart docker

检查 Docker 服务的状态:

$ sudo systemctl status docker

现在,重新运行docker info,看看我们得到什么:

$ docker info | grep "Logging Driver"
Logging Driver: splunk

由于 Splunk 现在是默认的日志驱动程序,让我们启动一个新的 NGINX 容器并可视化日志:

$ docker run --name nginx-splunk -d nginx
dedde062feba33f64efd89ef9102c7c93afa854473cda3033745d35d9065c9e5

登录到你的 Splunk 实例;你将看到 Docker 日志正在流式传输。然后,你可以分析日志并从中创建可视化。

你还可以为不同的容器配置不同的日志驱动程序,方法是通过命令行传递log-driverlog-opts标志来覆盖默认设置。由于我们当前的配置是 Splunk,并且我们希望将数据导出到 JSON 文件,因此可以在运行容器时将log-driver指定为json-file。让我们来看一下:

$ docker run --name nginx-json-file --log-driver json-file -d nginx
379eb8d0162d98614d53ae1c81ea1ad154745f9edbd2f64cffc2279772198bb2

要查看 JSON 日志,我们需要查看 JSON 日志目录——即/var/lib/docker/containers/<container_id>/<container_id>-json.log

对于nginx-json-file容器,我们可以执行以下操作:

$ cat /var/lib/docker/containers\
/379eb8d0162d98614d53ae1c81ea1ad154745f9edbd2f64cffc2279772198bb2\
/379eb8d0162d98614d53ae1c81ea1ad154745f9edbd2f64cffc2279772198bb2-json.log
{"log":"/docker-entrypoint.sh: /docker-entrypoint.d/ is not
empty, will attempt to perform configuration\n","stream":"
stdout","time":"2022-06-01T06:27:05.922950436Z"}
...
{"log":"/docker-entrypoint.sh: Configuration complete; ready
for start up\n","stream":"stdout","time":"2023-06-01T06:27:
05.937629749Z"}

我们可以看到,日志现在正在流式传输到 JSON 文件,而不是 Splunk。这就是我们如何覆盖默认日志驱动程序的方式。

提示

在大多数情况下,最好坚持使用一个默认的日志驱动程序,这样你就可以在一个地方分析和可视化你的日志。

现在,让我们了解一下与 Docker 日志相关的一些挑战和最佳实践。

Docker 日志的典型挑战以及应对这些挑战的最佳实践:

Docker 允许你在单台机器或一组机器上运行多个应用程序。大多数组织同时运行虚拟机和容器,并且已经配置了日志记录和监控堆栈以支持虚拟机。

大多数团队在让 Docker 日志像虚拟机日志一样工作时遇到困难。因此,大多数团队会将日志发送到主机文件系统,然后日志分析解决方案从中获取数据。这并不是理想的做法,你应该避免犯这个错误。如果你的容器是静态的,这种做法可能有效,但如果你有一组运行 Docker 的服务器集群,并且可以将容器调度到任意虚拟机上,那么就会成为一个问题。

从日志的角度来看,将容器视为运行在虚拟机上的应用程序是一个错误。相反,你应该将容器视为一个实体——就像虚拟机一样。你绝不应该将容器与虚拟机关联在一起。

一种解决方案是使用日志驱动程序将日志直接转发到日志分析解决方案中。但是,日志记录就会严重依赖于日志分析解决方案的可用性。所以,这可能不是最好的做法。当 Docker 上的服务因为日志分析解决方案不可用或网络问题而宕机时,很多人都遇到过问题。

好吧,解决这个问题的最佳方法是使用 JSON 文件暂时存储虚拟机中的日志,并使用另一个容器将日志以传统方式推送到你选择的日志分析解决方案。这样,你就将应用程序的运行与外部服务的依赖解耦了。

你可以使用日志驱动程序直接将日志导出到日志转发容器中的日志分析解决方案。有许多日志驱动程序支持多个日志目标。始终以这样的方式标记日志,使容器作为独立实体出现。这将使容器与虚拟机解耦,之后你就可以充分利用分布式容器架构。

到目前为止,我们已经看过容器的日志方面,但 DevOps 工程师角色的一个重要元素是监控。我们将在下一节中详细探讨这一点。

使用 Prometheus 进行 Docker 监控

监控 Docker 节点和容器是管理 Docker 的关键部分。有许多工具可以用于监控 Docker。虽然你可以使用传统工具,如 Nagios,但 Prometheus 在云原生监控中逐渐崭露头角,因为它简单且具有可插拔的架构。

Prometheus 是一个免费的开源监控工具,它提供了维度数据模型、使用 Prometheus 查询语言PromQL)的高效且简单的查询、有效的时间序列数据库和现代的告警功能。

它提供了多个导出器,可从各种来源导出数据,支持虚拟机和容器。在深入细节之前,让我们看看容器监控的一些挑战。

容器监控的挑战

从概念上讲,容器监控与传统方法没有区别。你仍然需要指标、日志、健康检查和服务发现。这些并不是未知的或没有被探索过的东西。容器面临的问题是它们带来的抽象;让我们来看一下其中的一些问题:

  • 容器表现得像迷你虚拟机;然而,实际上,它们是运行在服务器上的进程。但它们仍然有许多需要监控的内容,就像我们在虚拟机中一样。容器进程会有许多指标,类似于虚拟机,需要作为独立实体来处理。处理容器时,大多数人会犯一个错误,那就是把容器映射到特定的虚拟机。

  • 容器是临时的,大多数人没有意识到这一点。当你有一个容器并且它被重建时,它会有一个新的 IP 地址。这可能会混淆传统的监控系统。

  • 运行在集群中的容器可以从一个节点(服务器)移动到另一个节点。这增加了一个复杂性层,因为您的监控工具需要知道容器的位置,以便从中抓取指标。使用现代的、容器优化的工具,这不应该成为问题。

Prometheus 帮助我们解决这些挑战,因为它是从分布式应用程序的角度构建的。为了理解这一点,我们将通过一个实践示例来进行说明。但是,在此之前,让我们在另一台 Ubuntu 22.04 Linux 机器上安装 Prometheus。

安装 Prometheus

安装 Prometheus 包括几个步骤,为了简化,我创建了一个 Bash 脚本,用于在 Ubuntu 机器上安装和设置 Prometheus。

在您想要设置 Prometheus 的另一台机器上使用以下命令:

$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops
$ cd modern-devops/ch3/prometheus/
$ sudo bash prometheus_setup.sh

要检查 Prometheus 是否已安装并正在运行,可以使用以下命令检查 Prometheus 服务的状态:

$ sudo systemctl status prometheus
prometheus.service – Prometheus
   Loaded: loaded (/etc/systemd/system/prometheus.service; enabled; vendor preset: 
enabled)
   Active: active (running) since Tue 2023-06-01 09:26:57 UTC; 1min 22s ago

由于服务处于活动状态,我们可以得出结论,Prometheus 已成功安装并正在运行。下一步是配置 Docker 服务器,以便 Prometheus 能够从中收集日志。

配置 cAdvisor 和节点出口程序以暴露指标

现在,我们将在运行 Docker 的机器上启动一个 cAdvisor 容器,以暴露 Docker 容器的指标。cAdvisor 是一个指标收集器,用于从容器中抓取指标。要启动容器,请使用以下命令:

$ docker run -d --restart always --name cadvisor -p 8080:8080 \
-v "/:/rootfs:ro" -v "/var/run:/var/run:rw" -v "/sys:/sys:ro" \
-v "/var/lib/docker/:/var/lib/docker:ro" google/cadvisor:latest

现在 cAdvisor 正在运行,我们需要配置节点导出器,以便在 Docker 机器上导出节点指标。为此,运行以下命令:

$ cd ~/modern-devops/ch3/prometheus/
$ sudo bash node_exporter_setup.sh

现在节点导出器正在运行,让我们配置 Prometheus 以连接到 cAdvisor 和节点导出器,并从那里抓取指标。

配置 Prometheus 以抓取指标

我们现在将在 Prometheus 机器上配置 Prometheus,以便它可以从 cAdvisor 抓取指标。为此,修改 /etc/prometheus/prometheus.yml 文件,使其在运行 Prometheus 的服务器中包含以下内容:

$ sudo vim /etc/prometheus/prometheus.yml
  ...
  - job_name: 'node_exporter'
    scrape_interval: 5s
    static_configs:
      - targets: ['localhost:9100', '<Docker_IP>:9100']
  - job_name: 'Docker Containers'
    static_configs:
      - targets: ['<Docker_IP>:8080']

更改此配置后,我们需要重新启动 Prometheus 服务。使用以下命令执行此操作:

$ sudo systemctl restart prometheus

现在,让我们启动一个示例 Web 应用程序,我们将使用 Prometheus 来监控它。

启动一个示例容器应用程序

现在,让我们运行一个名为 web 的 NGINX 容器,该容器在 Docker 机器上的 8081 端口上运行。为此,请使用以下命令:

$ docker run -d --name web -p 8081:80 nginx
f9b613d6bdf3d6aee0cb3a08cb55c99a7c4821341b058d8757579b52cabbb0f5

现在我们已经设置好 Docker 容器,让我们通过访问 https://<PROMETHEUS_SERVER_EXTERNAL_IP>:9090 打开 Prometheus UI,然后在文本框中输入以下查询来运行它:

container_memory_usage_bytes{name=~"web"}

它应该显示类似以下内容:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_3.02.jpg

图 3.2 – Prometheus – container_memory_usage_bytes

我们还可以通过点击图表标签页来查看此指标的时间序列。然而,在此之前,让我们使用 Apache Bench 工具加载我们的 NGINX 服务。Apache Bench 是一款负载测试工具,帮助我们通过命令行向 NGINX 端点发送 HTTP 请求。

在你的 Docker 服务器上,运行以下命令来启动负载测试:

$ ab -n 100000 http://localhost:8081/

它将向端点发送 100,000 个请求,这意味着它提供了相当大的负载来进行内存突增。现在,如果你打开图表标签页,你应该会看到类似以下内容:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_3.03.jpg

图 3.3 – Prometheus – container_memory_usage_bytes – 图表

要可视化节点指标,我们可以使用以下 PromQL 语句来获取 Docker 主机的 node_cpu 值:

node_cpu{instance="<Docker_IP>:9100",job="node_exporter"}

如下图所示,它将为我们提供多种模式的 node_cpu 指标:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_3.04.jpg

图 3.4 – Prometheus – node_cpu

Prometheus 还提供了多种其他可视化的指标。让我们来了解一些你可以监控的指标。

需要监控的指标

监控指标是一个复杂的话题,主要取决于你的使用场景。不过,以下是一些关于你需要监控的指标的指南。

主机指标

你需要监控你的主机指标,因为你的容器是在它们上面运行的。你可以监控的一些指标如下:

  • 主机 CPU:了解你的主机是否有足够的 CPU 来运行容器非常重要。如果没有,它可能会终止一些容器来弥补这一点。因此,为了确保可靠性,你需要时刻监控这一点。

  • 主机内存:像监控主机 CPU 一样,你需要监控主机内存,以便发现内存泄漏或内存异常等问题。

  • 主机磁盘空间:由于 Docker 容器使用主机文件系统来存储临时和持久化文件,因此你需要监控它。

Docker 容器指标

Docker 容器指标是下一个需要考虑的事项:

  • 容器 CPU:此指标将提供 Docker 容器使用的 CPU 数量。你应该监控它,以了解容器的使用模式,并决定如何有效地放置容器。

  • 限速 CPU 时间:此指标帮助我们了解容器 CPU 被限速的总时间。这可以让我们知道是否某个特定的容器比其他容器需要更多的 CPU 时间,从而可以相应地调整 CPU 份额限制。

  • 容器内存失败计数器:此指标提供容器请求的内存超过分配内存的次数。这将帮助你了解哪些容器需要更多的内存,从而可以根据需要调整容器运行。

  • 容器内存使用量:此指标将提供 Docker 容器使用的内存量。你可以根据使用情况设置内存限制。

  • 容器交换:这个指标会告诉你哪些容器使用了交换空间而不是内存。它帮助我们识别占用内存较多的容器。

  • 容器磁盘 I/O:这是一个重要的指标,帮助我们了解容器的磁盘使用情况。峰值可能表明磁盘存在瓶颈,或者提示你可能需要重新审视存储驱动程序的配置。

  • 容器网络指标:这个指标将告诉我们容器使用了多少网络带宽,并帮助我们理解流量模式。你可以使用这些指标来检测意外的网络高峰或拒绝服务攻击。

重要提示

在非生产环境的性能测试阶段对你的应用程序进行性能分析,将给你一个系统在生产环境中表现的粗略概念。应用程序的实际调优工作开始于你将其部署到生产环境时。因此,监控至关重要,调优是一个持续的过程。

到目前为止,我们一直在运行命令来完成大部分工作。这是命令式的做法。但如果我告诉你,你不需要输入命令,而是可以声明你想要的内容,接着某个工具就能代表你执行所有需要的命令呢?这就是所谓的声明式应用程序管理方法。Docker Compose 是实现这一目标的流行工具之一。我们将在下一节中对此进行详细了解。

使用 Docker Compose 进行声明式容器管理

Docker Compose 帮助你以声明的方式管理多个容器。你可以创建一个 YAML 文件,指定你想构建的内容、要运行的容器以及容器之间如何交互。你可以在 YAML 文件中定义挂载、网络、端口映射以及许多不同的配置。

之后,你只需运行docker compose up来启动整个容器化应用程序。

声明式管理因其强大和简便正在迅速获得认可。现在,系统管理员不再需要记住他们运行过的命令,也不需要编写冗长的脚本或剧本来管理容器。相反,他们只需在 YAML 文件中声明自己想要的内容,然后docker compose或其他工具可以帮助他们实现这一状态。我们在安装 Docker 时也安装了 Docker Compose,接下来我们将通过示例应用程序来实际操作。

使用 Docker Compose 部署示例应用程序

我们有一个 Python Flask 应用程序,监听端口5000,最终我们将其映射到主机端口80。该应用程序将作为后端服务连接到 Redis 数据库,使用默认端口6379,并获取页面的最后访问时间。我们不会将该端口暴露给主机系统。这意味着数据库对于任何能够访问应用程序的外部方来说完全是不可触及的。

以下图表展示了应用程序架构:

https://github.com/OpenDocCN/freelearn-devops-pt3-zh/raw/master/docs/mdn-dop-prac-2e/img/B19877_Figure_3.05.jpg

图 3.5 – 示例应用程序

必要的文件可以在本书的 GitHub 仓库中找到。运行以下命令来定位文件:

$ git clone https://github.com/PacktPublishing/Modern-DevOps-Practices-2e.git \
modern-devops 
$ cd modern-devops/ch3/docker-compose
$ ls -l
total 16
-rw-r--r-- 1 root root 681 Nov 25 06:11 app.py
-rw-r--r-- 1 root root 389 Nov 25 06:45 docker-compose.yaml
-rw-r--r-- 1 root root 238 Nov 25 05:27 Dockerfile
-rw-r--r-- 1 root root  12 Nov 25 05:26 requirements.txt

app.py 文件如下所示:

import time
import redis
from flask import Flask
from datetime import datetime
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
def get_last_visited():
    try:
        last_visited = cache.getset('last_visited',str(datetime.now().strftime("%Y-%m-%d, 
%H:%M:%S")))
        if last_visited is None:
            return cache.getset('last_visited',str(datetime.now().strftime("%Y-%m-%d, 
%H:%M:%S")))
        return last_visited
    except redis.exceptions.ConnectionError as e:
        raise e
@app.route('/')
def index():
    last_visited = str(get_last_visited().decode('utf-8'))
    return 'Hi there! This page was last visited on {}.\n'.format(last_visited)

requirements.txt 文件如下所示:

flask
redis

我已经为你构建了应用程序,镜像已上传到 Docker Hub。下一章将详细介绍如何构建 Docker 镜像。现在,让我们先看一下 docker-compose 文件。

创建 docker-compose 文件

过程的下一步是创建一个 docker-compose 文件。docker-compose 文件是一个 YAML 文件,其中包含服务、网络、卷和其他相关配置的列表。让我们看一下以下示例 docker-compose.yaml 文件,以便更好地理解:

version: "2.4"
services:
  flask:
    image: "bharamicrosystems/python-flask-redis:latest"
    ports:
      - "80:5000"
    networks:
      - flask-app-net
  redis:
    image: "redis:alpine"
    networks:
      - flask-app-net
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
networks:
  flask-app-net:
    driver: bridge
volumes:
  redis-data:

YAML 文件描述了两个服务——flaskredis

flask 服务使用 python-flask-redis:latest 镜像——这是我们使用前述代码构建的镜像。它还将主机端口 80 映射到容器端口 5000,将此应用程序暴露给主机机器的端口 80,你可以通过 http://localhost 访问它。

redis 服务使用官方的 redis:alpine 镜像,并且不暴露任何端口,因为我们不希望此服务位于容器网络的外部。然而,它声明了一个持久化卷 redis-data,该卷包含 /data 目录。我们可以将此卷挂载到主机文件系统中,以便在容器生命周期之外保持数据持久性。

还有一个使用桥接驱动的 flask-app-net 网络,两个服务共享同一个网络。这意味着这些服务可以通过服务名称相互调用。如果你查看 app.py 代码,你会看到我们使用 redis 主机名建立了与 Redis 服务的连接。

要应用配置,只需运行 docker-compose up -d

$ docker compose up -d
[+] Running 17/17
flask 9 layers []      0B/0B      Pulled 10.3s
redis 6 layers []      0B/0B      Pulled 9.1s
[+] Building 0.0s (0/0)
[+] Running 4/4
Network docker-compose_flask-app-net  Created 0.1s
Volume "docker-compose_redis-data"    Created 0.0s
Container docker-compose-flask-1      Started 3.8s
Container docker-compose-redis-1      Stated

现在,让我们列出 Docker 容器,看看情况如何:

$ docker ps
CONTAINER ID  IMAGE              COMMAND         CREATED    STATUS   PORTS             NAMES
9151e72f5d66  redis:             "docker-        3 minutes  Up 3     6379/tcp          docker-compose
              alphone            entrypoint.s…"  ago        minutes                    -redis-1
9332c2aaf2c4  bharamicrosystems  "flask run"     3 minutes  Up 3     0.0.0.0:80->      docker-compose
              /python-flask-                     ago        minutes  5000/tcp,         -flask-1
              redis:latest                                           :::80->5000/tcp

我们可以看到两个容器正在为这两个服务运行。我们还可以看到主机端口 80 将连接转发到 flask 服务的容器端口 5000

redis 服务是内部的,因此没有端口映射。

让我们执行 curl localhost 看看我们得到什么:

$ curl localhost
Hi there! This page was last visited on 2023-06-01, 06:54:27.

在这里,我们根据示例 Flask 应用程序代码从 Redis 缓存中获取最后访问的页面。

让我们运行几次,看看时间是否会变化:

$ curl localhost
Hi there! This page was last visited on 2023-06-01, 06:54:28.
$ curl localhost
Hi there! This page was last visited on 2023-06-01, 06:54:51.
$ curl localhost
Hi there! This page was last visited on 2023-06-01, 06:54:52.

我们可以看到每次执行 curl 时,最后访问时间都会变化。由于卷是持久的,即使容器重新启动,我们应该能得到类似的最后访问时间。

首先,让我们执行 curl,获取最后访问时间以及当前日期:

$ curl localhost && date
Hi there! This page was last visited on 2023-06-01, 06:54:53.
Thu Jun  1 06:55:50 UTC 2023

现在,下次执行 curl 时,我们应该得到类似 2023-06-01, 06:55:50 的日期时间。但在此之前,让我们重新启动容器,看看数据是否持久:

$ docker compose restart redis
[+] Restarting 1/1
Container docker-compose-redis-1  Started

现在 Redis 已重新启动,让我们再次运行 curl

$ curl localhost
Hi there! This page was last visited on 2023-06-01, 06:55:50.

如我们所见,即使重新启动 redis 服务,我们也能获取到正确的最后访问时间。这意味着数据持久性工作正常,卷已经正确挂载。

你可以在 docker compose 上进行许多其他配置,这些内容可以从官方文档中轻松获取。然而,你现在应该对如何使用 docker compose 及其优点有一个大致的了解。接下来,让我们看看一些与 Docker Compose 相关的最佳实践。

Docker Compose 最佳实践

Docker Compose 提供了一种声明式的方式来管理 Docker 容器配置。这使得 GitOps 成为可能,适用于你的 Docker 工作负载。虽然 Docker Compose 主要用于开发环境,但你可以在生产环境中非常有效地使用它,特别是当 Docker 在生产环境中运行,并且没有使用像 Kubernetes 这样的容器编排工具时。

始终与代码一起使用 docker-compose.yml 文件

YAML 文件定义了如何运行你的容器。因此,它成为一个非常有价值的工具,可以从一个地方声明性地构建和部署你的容器。你可以将所有依赖项添加到你的应用程序中,并在同一个网络中运行相关的应用程序。

使用覆盖文件来分隔多个环境的 YAML 文件

Docker Compose YAML 文件允许我们同时构建和部署 Docker 镜像。Docker 实现了 一次构建,随处运行 的概念。这意味着我们在开发环境中构建一次,然后在随后的环境中使用创建的镜像。因此,问题就来了:我们如何实现这一点呢?Docker Compose 允许我们按顺序应用多个 YAML 文件,其中下一个配置会覆盖上一个配置。这样,我们就可以为不同的环境创建单独的覆盖文件,并使用这些文件来管理多个环境。

例如,假设我们有以下基础的 docker-compose.yaml 文件:

version: "2.4"
services:
  flask:
    image: "bharamicrosystems/python-flask-redis:latest"
    ports:
      - "80:5000"
    networks:
      - flask-app-net
  redis:
    image: "redis:alpine"
    networks:
      - flask-app-net
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis-data:/data
networks:
  flask-app-net:
    driver: bridge
volumes:
  redis-data:

我们只需要在开发环境中构建 Flask 应用程序容器镜像,以便为开发环境创建一个覆盖文件——即 docker-compose.override.yaml

web:
  build: .
  environment:
    DEBUG: 'true'
redis:
  ports:
    - 6379:6379

在这里,我们在 web 服务中添加了一个 build 参数。这意味着 Python Flask 应用程序将被重新构建并部署。我们还在 web 服务中设置了 DEBUG 环境变量,并将 redis 端口暴露到宿主文件系统。这在开发环境中是有意义的,因为我们可能希望直接从开发机器调试 Redis。不过,我们不希望在生产环境中出现这种情况。因此,默认的 docker-compose.yaml 文件将在生产环境中工作,正如我们在上一节中所看到的那样。

使用 .env 文件存储敏感变量

你可能不想将密码和机密等敏感内容存储在版本控制中。相反,你可以使用一个 .env 文件,里面包含变量名和值的列表,并将其保存在像 HashiCorp Vault 这样的机密管理系统中。

在生产环境中要注意依赖关系

当你更改某个容器并希望重新部署时,docker-compose 也会重新部署所有依赖项。不过,这可能并不是你想要的行为,因此你可以通过使用以下命令来覆盖这一行为:

$ docker-compose up --no-deps -d <container_service_name>

将 docker-compose 文件视为代码

永远对你的 docker-compose 文件进行版本控制,并将其与代码一起保存。这将允许你跟踪文件版本,并使用 Git 特性,如拉取请求(pull request)。

总结

本章旨在同时满足初学者和有经验的用户需求。我们从介绍 Docker 的基础概念开始,逐步深入到更高级的主题和实际应用场景。本章首先介绍了 Docker 的安装,运行第一个 Docker 容器,理解不同的容器运行模式,了解 Docker 卷和存储驱动程序。我们还学习了如何选择合适的存储驱动程序、卷选项以及一些最佳实践。所有这些技能将帮助你轻松搭建一个生产就绪的 Docker 服务器。我们还讨论了日志代理,如何将 Docker 日志快速发送到多个目的地,如 journaldSplunk 和 JSON 文件,帮助你监控容器。我们还探讨了如何使用 Docker Compose 声明性地管理 Docker 容器,并部署了一个完整的复合容器应用。请尝试本章中提到的所有命令,进行更多的实践体验——实践是实现有价值的成果和学习新知识的关键。

下一步,在接下来的章节中,我们将研究 Docker 镜像,创建和管理它们,以及一些最佳实践。

问题

回答以下问题,测试你对本章内容的掌握:

  1. 对于 CentOS 和 RHEL 7 及以下版本,应该使用 overlay2。(正确/错误)

  2. 以下哪个陈述是正确的?(选择四个)

    A. 卷增加 IOPS。

    B. 卷减少 IOPS。

    C. tmpfs 挂载使用系统内存。

    D. 你可以使用绑定挂载将主机文件挂载到容器中。

    E. 你可以使用卷挂载进行多实例的活动-活动配置。

  3. 更改存储驱动会删除主机上的现有容器。(正确/错误)

  4. devicemapper 是比 overlay2 更适合写密集型容器的选项。(正确/错误)

  5. Docker 支持以下哪些日志驱动?(选择四个)

    A. journald

    B. Splunk

    C. JSON 文件

    D. Syslog

    E. Logstash

  6. Docker Compose 是一种管理容器的命令式方法。(正确/错误)

  7. 以下哪些 docker run 配置是正确的?(选择三个)

    A. docker run nginx

    B. docker run --name nginx nginx:1.17.3

    C. docker run -d --name nginx nginx

    D. docker run -d --name nginx nginx --``restart never

答案

以下是本章问题的答案:

  1. 错误 – 对于 CentOS 和 RHEL 7 及以下版本,应该使用 devicemapper,因为它们不支持 overlay2

  2. B、C、D、E。

  3. 正确。

  4. 正确。

  5. A、B、C、D。

  6. 错误 – Docker Compose 是一种声明式的容器管理方法。

  7. A, B, C.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值