Ansible2-容器化指南-全-

Ansible2 容器化指南(全)

原文:annas-archive.org/md5/c27fae1f3e1e6fdbb9d0f411fc795a32

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在过去的几年中,IT 领域在软件应用开发和部署的方式上发生了剧变。自动化、云计算和虚拟化的兴起,深刻改变了系统管理员、软件开发人员以及整个组织如何看待和管理基础设施。几年前,很多 IT 行业内的人认为,允许关键任务应用程序在企业数据中心外运行几乎是不可能的。然而,现在比以往更多的组织将基础设施迁移到云服务,如 AWS、Azure 和 Google Compute,以节省时间并削减与运行物理基础设施相关的开销成本。通过抽象化硬件,企业可以将注意力集中在真正重要的事物上——为用户提供服务的软件应用。

IT 领域的下一波巨大浪潮正式始于 2013 年,Docker 容器引擎的首次发布。Docker 让用户能够轻松地将软件打包成小型、可重用的执行环境,称为容器,并利用 Linux 内核中的特性与 LXC(Linux 容器)一起使用。使用 Docker,开发人员可以创建微服务应用程序,这些应用程序可以快速构建、在任何环境中都能运行,并利用可版本控制的可重用服务工件(容器镜像)。随着越来越多的用户采用容器化工作流,执行过程中开始出现一些问题。尽管 Docker 在构建和运行容器方面表现出色,但它在整个容器生命周期中难以成为一个真正的端到端解决方案。

Ansible Container 项目旨在将 Ansible 配置管理和自动化平台的强大功能引入容器世界。Ansible Container 弥补了容器生命周期管理的空白,它允许容器的构建和部署流水线使用 Ansible 语言。通过使用 Ansible Container,你不仅可以构建容器,还能利用强大的 Ansible 配置管理语言,在远程服务器和云平台上部署全规模应用。

本书将作为使用 Ansible Container 项目的指南。我的目标是通过本书的学习,你将牢固地理解 Ansible Container 的工作原理,并能够利用其众多功能,从开发到生产环境,构建稳健的容器化软件堆栈。

本书涵盖内容

第一章,使用 Docker 构建容器,向读者介绍了 Docker 是什么,它是如何工作的,以及如何使用 Dockerfile 和 Docker Compose 的基础知识。本章奠定了学习如何使用 Ansible Container 所需的基础概念。

第二章,使用 Ansible Container,探讨了 Ansible Container 工作流。本章使读者熟悉 Ansible Container 的核心概念,如构建、运行和销毁。

第三章,你的第一个 Ansible 容器项目,通过利用 Ansible Galaxy 上的社区角色,带给用户构建一个简单 Ansible 容器项目的体验。在本章结束时,读者将熟悉如何构建项目并将容器制品推送到像 Docker Hub 这样的容器镜像仓库。

第四章,角色中有什么?,向用户概述了如何编写自定义容器启用的角色以供 Ansible Container 使用。本章的总体目标是编写一个角色,从头开始构建一个功能完备的 MariaDB 容器镜像。在本章结束时,用户应对使用正确风格和语法编写 Ansible playbook 有基本了解。

第五章,使用 Kubernetes 大规模管理容器,向读者概述了 Kubernetes 平台及其核心功能。在本章中,读者将有机会在 Google Cloud 中创建一个多节点的 Kubernetes 集群并在其中运行容器。

第六章,使用 OpenShift 管理容器,向读者介绍了 Redhat 的 OpenShift 平台。本章向读者展示了使用 Minishift 部署本地 OpenShift 集群并在其上运行容器化工作负载所需的步骤。本章还探讨了 Kubernetes 和 OpenShift 之间的关键区别,尽管它们的架构本质上是相似的。

第七章,部署你的第一个项目,深入探讨了 Ansible Container 工作流中的最终命令——部署。通过使用部署,读者将获得将先前构建的项目部署到 Kubernetes 和 OpenShift 上的第一手经验,利用 Ansible Container 作为端到端工作流工具。

第八章,构建和部署多容器项目,探讨了如何使用 Ansible Container 构建一个涉及多个应用容器的项目。要全面理解这一主题,必须先了解容器网络并配置容器以访问网络资源。本章将为读者提供构建和部署一个多容器项目的机会,该项目使用 Django、Gulp、NGINX 和 PostgreSQL 容器。

第九章,深入了解 Ansible Container,为读者提供了掌握整个 Ansible Container 工作流后应采取的下一步措施。此部分讨论的主题包括将 Ansible Container 与 CICD 工具集成,以及在 Ansible Galaxy 上共享项目。

本书所需的内容

本书假设读者具有初级到中级的 Linux 操作系统使用经验、应用部署经验和服务器管理经验。本书将引导你完成所需步骤,在本地笔记本电脑上建立一个完全功能的实验环境,并快速使用 VirtualBox 和 Vagrant 环境进行操作。在开始之前,建议读者已经安装并运行 VirtualBox、Vagrant 和 Git 命令行客户端。为了完全符合本环境的要求,必须满足或超过以下系统规格:

  • CPU:2 核(Intel Core i5 或同等处理器)

  • 内存:8 GB RAM

  • 硬盘空间:80 GB

在本书中,你将需要以下软件列表:

  • VirtualBox 5.1 或更高版本

  • Vagrant 1.9.1 或更高版本

  • 一个用于编辑 YAML 文件的文本编辑器(推荐使用 GitHub Atom 或 VIM)

安装必要的软件包需要互联网连接。

本书适合谁阅读

本书旨在帮助目前担任系统管理员、DevOps 工程师、技术架构师(或类似角色)的人,快速掌握 Ansible 容器工作流。如果读者在阅读本书之前已经具备 Docker、Ansible 或其他相关自动化平台的基本知识,那么本书将更加有帮助,尽管这不是必需的。我希望读者在阅读本书时能够深入理解这些基础知识。最终目标是帮助读者在了解如何使用 Ansible Container 加速构建、运行、测试和部署应用容器的过程中,建立从开发到生产环境的坚实基础。

约定

在本书中,你将看到许多文本样式,它们用于区分不同类型的信息。以下是这些样式的一些示例及其含义解释:文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入以及 Twitter 用户名显示如下:“我们可以通过使用 include 指令来包含其他上下文。”

代码块如下所示:

---
- name: Create User Account
  user:
    name: MyUser
    state: present

- name: Install Vim text editor
  apt:
    name: vim
    state: present

所有命令行输入或输出将如下所示:

ubuntu@node01:/tmp$ ansible-galaxy init MyRole --container-enabled
- MyRole was created successfully

新术语重要词汇以粗体显示。屏幕上出现的词汇,例如菜单或对话框中的内容,会像这样出现在文本中:“点击 Next 按钮将带你进入下一个屏幕。”

警告或重要说明会以如下方式出现。

提示和技巧会以如下方式出现。

读者反馈

我们始终欢迎读者的反馈。让我们知道您对本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能够从中受益的书籍。若要向我们提供一般反馈,只需发送电子邮件至feedback@packtpub.com,并在邮件主题中注明书名。如果您在某个领域有专业知识并且有兴趣编写或参与编写书籍,请参阅我们的作者指南,网址为www.packtpub.com/authors

客户支持

现在,您已经成为一本 Packt 书籍的骄傲拥有者,我们提供了一些内容,帮助您最大限度地从您的购买中获益。

下载本书的彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/ContainerizationwithAnsible2_ColorImages.pdf下载此文件。

勘误表

尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——我们将非常感激您能向我们报告。通过这样做,您可以帮助其他读者避免困扰,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata报告勘误,选择您的书籍,点击“勘误提交表格”链接,并输入勘误的详细信息。一旦您的勘误被验证,您的提交将被接受,并且勘误将上传到我们的网站或添加到该书籍的勘误列表中。要查看已提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将显示在“勘误”部分。

盗版

互联网版权材料盗版是一个跨媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的非法副本,请立即提供该内容的链接或网站名称,以便我们采取相应措施。请通过copyright@packtpub.com联系我们,并附上涉嫌盗版内容的链接。感谢您在保护我们的作者及我们为您提供有价值内容方面的帮助。

问题

如果您在本书的任何方面遇到问题,可以通过questions@packtpub.com联系我们,我们会尽力解决问题。

第一章:使用 Docker 构建容器

近年来,IT 行业的格局发生了巨大变化。高度互动的移动应用、云计算和流媒体的兴起,推动了现有 IT 基础设施的极限。曾经满足于网页浏览和电子邮件的用户,现在正在利用高度互动的服务,并且不断要求更高的带宽、可靠性和更多的功能。随着这一变化的发生,IT 部门和应用程序开发人员不断尝试找到应对日益增加的需求的方法,以保持与依赖其服务的消费者的相关性。

作为一名应用程序开发人员、基础设施支持专家或 DevOps 工程师,你无疑已经看到了基础设施支持和维护方式的根本变化。曾经,开发人员可以独立编写应用程序,将其部署到企业内部,并将工作交给那些可能仅对应用程序功能有基本了解的运维人员。而如今,开发和运维的范式在大多数企业中已经紧密交织在一起,这就是许多企业所称的 DevOps。在 DevOps 思维模式中,运维和支持人员直接与应用开发人员合作,共同编写应用程序,以及基础设施代码。利用这种新的思维方式,服务能够上线,且可扩展多个层次,分布在数百台服务器、数据中心和云服务提供商之间。一旦组织采纳了 DevOps 思维模式,这将促使各个部门之间发生文化上的转变。通常会出现一种新的团队意识,开发人员和运维人员会感受到一种新的伙伴关系。开发人员乐于贡献代码,使应用程序部署变得更轻松,而运维人员则对新 DevOps 支持的应用程序带来的易用性、扩展性和可重复性感到满意。

即便在 DevOps 的世界中,容器化作为一种更新且更好的应用部署和维护方式,正在组织中积极增长和扩展。像信息技术领域的其他任何事物一样,我们需要对容器的构建、部署和在组织内的扩展过程进行控制。Ansible Container 提供了一种抽象且易于实现的方法论,适用于大规模构建和运行容器。在我们开始学习 Ansible 和容器化平台之前,必须首先审视应用程序和服务的历史部署方式。

在我们开始之前,让我们看看本章将涉及的主题:

  • DevOps 和 IT 基础设施的历史概述

    • 手动部署

    • 自动化简介

    • 应用虚拟化

    • 应用容器化

    • 容器化应用程序的编排

  • 构建你的第一个 Docker 容器

    • 设置实验环境

    • 开始您的第一个 Docker 容器

    • 构建您的第一个 Docker 容器

    • 容器生命周期管理

DevOps 和 IT 景观的变化

让我们快速看看许多 IT 部门的演变以及整个行业对这一根本性转变的响应。在我们深入学习容器之前,了解应用程序和服务部署的历史非常重要,以了解容器化解决了哪些问题,以及基础设施在几十年间如何变化和演进。

大型单体应用的手动部署

大型单体应用的手动部署通常是大多数应用部署的起点,并且在上世纪 90 年代末和 21 世纪初期到中期的大多数基础设施状态下。这种方法通常是这样的:

  1. 组织决定创建新的服务或应用程序。

  2. 组织委托开发团队编写新的服务。

  3. 安装新服务器和网络设备以支持新服务。

  4. 新服务由运维和工程团队部署,这些团队可能对新服务的实际功能一无所知。

通常,这种部署应用程序的方法很少或根本不使用自动化工具,只使用基本的 Shell 或批处理脚本,并且需要大量复杂的工作来维护应用程序或部署升级。在文化上,这种方法在团队之间创建信息孤岛,个人负责复杂整体中的小部分。如果团队成员在部门之间调动或离开组织,那些随后负责服务的人必须逆向工程最初开发应用程序的思维过程,可能会造成混乱。如果有文档存在,其可能模糊不清。

自动化介绍

迈向更灵活、面向 DevOps 架构演进的下一步是引入自动化平台,使操作和支持工程师能够简化组织内部部署和维护任务的许多方面。自动化工具种类繁多,具体取决于您希望自动化应用程序的程度。某些自动化工具仅在操作系统级别工作,以确保操作系统和应用程序按预期运行。其他自动化工具可以使用诸如 IPMI 之类的接口远程启动裸机服务器,以部署从操作系统到上层应用的一切。

自动化工具基于当前状态期望状态的配置管理概念。自动化平台的目标是根据一个程序化模板评估服务器的当前状态,并且仅对服务器执行将其带入期望状态所需的操作。例如,一个自动化平台检查 NGINX 是否处于运行状态时,可能会查看一台 Ubuntu 16.04 服务器,发现 NGINX 当前未安装。

为了将此服务器带入期望状态,它可能会在后台运行命令apt-get install nginx,使该服务器符合要求。当相同的自动化工具评估一台 CentOS 服务器时,它可能会发现 NGINX 已安装但未运行。为了使该服务器符合要求,它会运行systemctl start nginx来使服务器符合期望状态。注意,它并没有尝试重新安装 NGINX。进一步扩展我们的例子,如果自动化工具检查一台既已安装又已运行 NGINX 的服务器,它将不会对该服务器采取任何操作,因为它已经处于期望状态。一个好的自动化平台的关键在于,该工具仅执行将服务器带入期望状态所需的步骤。这个概念被称为幂等性,是大多数自动化平台的一个标志。

现在我们将查看一些开源自动化工具,并分析它们是如何工作的以及它们有什么独特之处。深入理解自动化工具及其工作原理将帮助你理解 Ansible Container 是如何工作的,以及为什么它是容器编排中不可或缺的工具:

  • Chef:Chef 是由 Adam Jacobs 于 2008 年编写的配置管理工具,旨在解决当时他所面临的具体用例。Chef 代码使用 Ruby 基础的领域特定语言编写,称为recipes。为了特定目的而将多个 recipes 集合在一起的文件被称为cookbook。Cookbook 存储在服务器上,客户端可以定期下载更新的 recipes,通过作为守护进程运行的客户端软件来获取。Chef Client 负责评估当前状态与 cookbook 中描述的期望状态之间的差异。

  • Puppet:Puppet 是由 Luke Kaines 在 2005 年编写的,类似于 Chef,采用客户端-服务器模型。Puppet 清单使用 Ruby DSL 编写,并存储在一个专用服务器上,该服务器被称为Puppet Master。客户端运行一个名为Puppet Agent的守护进程,负责下载 Puppet 清单并在客户端上本地执行。

  • Salt:Salt 是 Thomas Hatch 于 2011 年编写的配置管理工具。与 Puppet 和 Chef 类似,Salt 主要基于客户端-服务器模型,其中存储在Salt Master上的states会在 minions 上执行,以实现所需的状态。Salt 的特点是它是最迅速且高效的配置管理平台之一,因为它在 master 和节点之间采用了消息总线架构(ZeroMQ)。通过这个消息总线,它能够迅速评估这些消息并采取相应的行动。

  • Ansible:Ansible 可能是我们迄今为止所研究的自动化平台中最具独特性的之一。Ansible 由 Michael DeHaan 于 2012 年编写,旨在提供一个简洁而强大的配置管理工具。Ansible 的playbooks是简单的 YAML 文件,详细列出了将在目标主机上执行的操作和参数,格式非常易读。默认情况下,Ansible 是无代理的,并采用推送模型,其中 playbooks 从一个集中位置(例如你的笔记本或网络上的专用主机)执行,并通过 SSH 在目标主机上进行评估。部署 Ansible 的唯一要求是,你运行 playbooks 的主机必须能够通过 SSH 访问,并且必须安装正确版本的 Python(撰写时为 2.7 版)。如果满足这些要求,Ansible 将是一个非常强大的工具,入门几乎不需要什么知识和资源。最近,Ansible 推出了 Ansible Container 项目,旨在将配置管理范式引入到基于容器的平台构建和部署中。Ansible 是一个极为灵活且可靠的配置管理平台,拥有一个庞大且健康的开源生态系统。

到目前为止,我们已经看到将自动化引入基础设施如何帮助我们更接近实现 DevOps 的目标。通过一个可靠的自动化平台,并配备正确的工作流程来引入变化,我们可以利用这些工具真正掌控我们的基础设施。虽然自动化的好处确实很大,但也存在重大缺点。错误实现的自动化会为我们的基础设施引入一个故障点。在选择自动化平台之前,必须考虑在我们的主服务器故障时会发生什么(适用于 Salt、Chef 和 Puppet 等工具)。或者,如果一个状态、食谱、playbook 或清单在某个裸机基础设施服务器上执行失败时会发生什么。使用配置管理和自动化工具在今天的环境中已成为一种必需,且出现了一些简化甚至消除这些潜在问题的应用部署方式。

应用程序和基础设施的虚拟化

随着近年来云计算的崛起,许多组织已经用应用程序和基础设施的虚拟化替代了传统的内部部署应用程序和服务。目前,租用像亚马逊、微软和谷歌等公司的硬件资源,创建具有精确硬件配置的虚拟服务器实例,已被证明对个人和公司而言更具成本效益。

如今,许多配置管理和自动化工具都在向这些云服务提供商添加直接的 API 访问,以扩展基础设施的灵活性。例如,使用 Ansible,你可以在剧本中精确描述所需的服务器配置,以及云服务提供商的凭证。执行该剧本不仅会启动所需的实例,还会将它们配置为运行你的应用程序。如果一个虚拟实例出现故障怎么办?销毁它并创建一个新的。随着云计算的兴起,基础设施的全新视角也随之而来。单一的服务器或服务器组不再被视为特殊的并以特定方式维护。云计算使 DevOps 从业者认识到一个非常现实的概念,那就是基础设施可以是一次性的。

然而,虚拟化不仅仅局限于云服务提供商。许多组织目前正在使用如 ESXi、Xen 和 KVM 等平台在内部实施虚拟化。这些平台允许具有大量存储、内存和 CPU 资源的大型服务器托管多个虚拟机,每个虚拟机使用宿主操作系统资源的一部分。

尽管虚拟化和自动化带来了许多好处,但采用这种架构仍然存在许多缺点。首先,各种形式的虚拟化可能相当昂贵。在云服务提供商中创建更多的虚拟服务器,你的月度开销费用将变得更高,还不算大型硬件配置虚拟机所带来的额外成本。此外,这样的部署可能需要大量的资源。即使是低配置,启动大量虚拟机也会消耗虚拟化主机硬件的巨大存储、内存和 CPU 资源。

最后,还必须考虑虚拟机操作系统以及虚拟化主机操作系统的维护和补丁更新。尽管自动化平台和现代虚拟化主机允许虚拟机快速启动和销毁,但对于那些可能需要维持数周或数月的实例,补丁和更新依然必须考虑。记住,即使操作系统已经虚拟化,它仍然容易受到安全漏洞、补丁和维护的影响。

应用程序和基础设施的容器化

容器化技术在 2013 年 3 月随着 Docker 的发布进入了 DevOps 领域。尽管容器化的概念早于 Docker,但对于许多在这一领域工作的人来说,这还是他们第一次接触到在容器内运行应用程序的概念。在继续之前,我们首先需要明确什么是容器,什么不是容器。

容器是一个在 Linux 系统中具有控制组和内核命名空间的隔离进程。在容器内部,有一个非常薄的操作系统层,只有足够的资源来启动和运行其他进程。基础操作系统层可以基于任何操作系统,甚至可以是与主机上运行的操作系统不同的操作系统。当运行容器时,容器引擎会分配访问主机操作系统内核的权限,以便将容器与主机上其他进程隔离开来。从容器内应用程序的角度看,它似乎是主机上唯一的进程,尽管同一主机可能同时运行多个该容器的版本。

以下插图展示了主机操作系统、容器引擎与主机上运行的容器之间的关系:

图 1:运行多个容器并使用不同基础操作系统的 Ubuntu 16.04 主机

许多容器化初学者将容器误认为是轻量级的虚拟机,并试图像处理虚拟机或未正确运行的裸机服务器一样修复或修改正在运行的容器。容器的设计理念是完全可丢弃的。如果容器没有正确运行,它们足够轻量,可以在几秒钟内终止现有容器并从头开始重建一个新的容器。如果虚拟机和裸机服务器应被视为宠物(需要照顾、喂养和维护),那么容器应该被视为牲畜(在这里一分钟,下一分钟就被删除和替换)。我想你明白这个意思了。

这种实现与传统虚拟化的显著区别在于,容器可以通过容器源文件快速构建,并在主机操作系统上运行,类似于 Linux 内核中的其他进程或守护进程。由于容器是隔离的且非常精简,因此不必担心容器内部运行任何不必要的进程,例如 SSH、安全工具或监控工具。容器的存在是为了特定的目的,即运行一个单一的应用程序。容器运行环境(例如 Docker)提供了必要的资源,使得容器能够成功运行,并提供与主机软件和硬件资源(如存储和网络)的接口。

容器天生就是为了便于移植设计的。一个使用 CentOS 基础镜像并运行 Apache Web 服务器的容器可以在 CentOS 主机、Ubuntu 主机,甚至是 Windows 主机上加载;它们都有相同的容器运行时环境,并且以完全相同的方式运行。拥有这种模块化的好处是巨大的。例如,开发人员可以在自己的笔记本电脑上为 MyAwesomeApplication 1.0 构建容器镜像,仅使用几个兆字节的存储和内存,并且可以确保该容器在生产环境中与在他们笔记本上的运行方式完全相同。当需要将 MyAwesomeApplication 升级到 2.0 版本时,升级路径只是简单地用更新的容器镜像版本替换正在运行的容器镜像,从而显著简化了升级过程。

将在 Docker 等运行时环境中运行容器的可移植性与 Ansible 等自动化工具相结合,可以为软件开发人员和运维团队提供一个强大的组合。新软件可以更快地部署,运行更加可靠,并且维护开销更低。这正是我们将在本书中进一步探讨的理念。

容器化应用程序的编排

朝着更灵活、面向 DevOps 的基础设施迈进,并不仅仅是将应用程序和工具运行在容器中。容器天生具有可移植性和灵活性。与 IT 行业中的其他技术一样,容器所带来的可移植性和灵活性可以被进一步构建,以使其变得更有用。Kubernetes 和 Docker Swarm 是两个容器调度平台,它们使得容器的维护和部署变得更加容易。

Kubernetes 和 Docker Swarm 可以主动维护在集群中各主机上运行的容器,使得容器的扩展和升级变得非常简单。如果你想增加集群中运行的容器数量,你只需告诉调度 API 增加副本数,容器将自动在集群中的节点上实时扩展。

如果你想升级应用程序版本,你也可以类似地指示这些工具使用新的容器版本,并且你几乎可以立刻看到滚动升级过程的发生。这些工具甚至可以提供容器之间的网络和 DNS 服务,从而将容器的网络流量与主机网络完全抽象开。这只是 Docker Swarm 和 Kubernetes 等容器编排和调度工具可以为你的容器化基础设施做的一小部分。但这些内容将在本书后续章节中进行更详细的讨论。

构建你的第一个 Docker 容器

现在我们已经涵盖了一些入门信息,帮助读者了解 DevOps、配置管理和容器化的基本概念,接下来是时候动手实践,实际从零开始构建我们的第一个 Docker 容器了。本章这一部分将带你手动构建容器,并通过编写 Dockerfile 脚本来实现。这样可以为你提供关于 Ansible Container 平台如何在后台工作、自动化构建和部署容器镜像的基础知识。

在使用容器镜像时,理解容器镜像和运行的实例之间的区别非常重要。当你使用 Ansible Container 或通过 Dockerfile 手动构建容器时,运行容器的过程需要两步:构建容器镜像和运行容器实例:

  • 构建容器:构建过程包括下载基础容器操作系统镜像,并执行 Dockerfile 或 Ansible Container 剧本中列出的步骤,将容器构建到所需状态。构建过程的结果是一个已缓存的容器镜像,准备启动容器实例。docker pull 命令也可以用来从互联网下载容器镜像,并将其缓存到本地 Docker 主机上。

  • 运行容器:启动一个已缓存的容器镜像并运行它的过程被称为运行容器。你可以从单个容器镜像启动任意数量的容器。如果你尝试运行一个本地 Docker 主机上尚未缓存的容器镜像,Docker 会尝试从互联网下载该镜像。

实例化实验环境

我鼓励你在我们进行这些实验操作时一同实践。为了简化获得本书中所覆盖工具的环境设置过程,我创建了一个 Git 仓库,里面包含了本书中所覆盖的多个实验场景。我们将从一个快速教程开始,介绍如何在你的本地工作站或笔记本电脑上设置实验环境。为了安装这些实验组件,我建议使用至少配备 8 GB RAM、支持虚拟化的 CPU(Intel Core i5 或同等配置)以及 128 GB 或更大硬盘的计算机。Linux 或 macOS 是安装实验环境的首选操作系统,因为这些工具在类 Unix 操作系统上运行通常更加稳定。但这些工具同样支持 Windows,只是效果可能因系统而异。

实验环境将启动一个一次性的 Ubuntu 16.04 Vagrant 虚拟机,预装了 Docker、Ansible Container 以及你成功熟悉 Ansible Container 工作原理所需的各种工具。还需要一个面向开发的文本编辑器,用于在本书中创建和编辑示例及实验任务。我建议使用 GitHub Atom 或 Vim,因为这两个编辑器都支持 YAML 文档和 Dockerfile 的语法高亮。GitHub Atom 和 Vim 都是免费的开源软件,并且支持跨平台使用。

请注意,你不必安装这个实验环境也能学习和理解 Ansible Container。虽然跟随本书操作并亲身体验这项技术是有帮助的,但并不是必须的。

本书应该足够简单,即使你没有可用的资源,也能理解内容而无需实例化实验室环境。同时需要注意,你也可以通过在工作站上安装 Ansible、Ansible Container 和 Docker 来实例化自己的实验室环境。稍后在本书中,我们将涉及 Kubernetes 和 OpenShift,因此后面的章节也需要这些工具。这些参考资料可以在书本的最后找到。

安装实验环境:

以下是在你的本地工作站上设置实验环境所需的步骤。关于如何为你的平台安装 Vagrant 和 VirtualBox 的详细信息,可以在各自的官方网站找到。尽量下载与列出的版本号相似的版本,以确保最大兼容性:

  1. 下载并安装 Git:git-scm.com/downloads

  2. 下载并安装 VirtualBox(版本 5.1):www.virtualbox.org/wiki/Downloads

  3. 下载并安装 Vagrant(版本 1.9.1):www.vagrantup.com/docs/installation/

  4. 克隆 Ansible Container Lab Git 仓库:

git clone https://github.com/aric49/ansible_container_lab.git

在终端中,导航到 ansible_container_lab Git 仓库,并运行:vagrant up 来启动虚拟机:

cd Ansible_Container_Lab
vagrant up

如果 Vagrant 和 VirtualBox 已正确安装和配置,你应该开始看到虚拟机在工作站上启动,类似于以下显示:

user@host:$ vagrant up
Bringing machine 'node01' up with 'virtualbox' provider...
==> node01: Importing base box 'ubuntu/xenial64'...
==> node01: Matching MAC address for NAT networking...
==> node01: Checking if box 'ubuntu/xenial64' is up to date...
==> node01: Setting the name of the VM: AnsibleBook_node01_1496327441174_45550
==> node01: Clearing any previously set network interfaces...
==> node01: Preparing network interfaces based on configuration...
node01: Adapter 1: nat
==> node01: Forwarding ports...
node01: 22 (guest) => 2022 (host) (adapter 1)
==> node01: Running 'pre-boot' VM customizations...
==> node01: Booting VM...
==> node01: Waiting for machine to boot. This may take a few minutes...
node01: SSH address: 127.0.0.1:2022

一旦 Vagrant box 成功启动,你可以执行命令:vagrant ssh node01 来访问虚拟机。

当你在 Vagrant 虚拟机中完成工作后,可以使用命令:vagrant destroy -f 来终止虚拟机。销毁虚拟机应在你完成当天的工作后进行,或者当你希望删除并重新创建虚拟机时进行,以便将其重置为原始设置。

请注意:在实验虚拟机中,任何未保存在/vagrant目录中的工作都会被删除且无法恢复。/vagrant目录是你本地机器上lab目录根目录和 Vagrant 虚拟机之间的共享文件夹。如果你想要以后使用这些文件,请保存到这个目录中。

启动你的第一个 Docker 容器

默认情况下,实验环境会在 Docker 引擎已经启动并作为服务运行的情况下开始。如果你需要手动安装 Docker 引擎,可以在基于 Ubuntu 或 Debian 的 Linux 发行版上使用:sudo apt-get install docker.io。Docker 安装并运行后,你可以通过执行docker ps -a来检查正在运行的容器状态:

ubuntu@node01:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ubuntu@node01:~$

我们可以从前面的输出中看到,我们有列标题,但没有实际的信息。这是因为我们没有运行任何容器实例。让我们使用docker images命令来检查 Docker 知道多少个容器镜像:

ubuntu@node01:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE

这里也没有发生什么。那是因为我们还没有容器镜像可以使用。让我们通过docker run命令来运行我们的第一个容器——Docker 的hello-world容器:

ubuntu@node01:~$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
78445dd45222: Pull complete
Digest: sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7
Status: Downloaded newer image for hello-world:latest
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 the Docker Hub.
3\. The Docker daemon created a new container from that image which 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://cloud.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/engine/userguide/

我们执行的命令是:docker run hello-world。当我们运行这个命令时,发生了很多事情。命令docker run是 Docker 中启动并运行容器所必需的命令。我们正在运行的容器是hello-world。如果你查看输出,你会看到 Docker 报告说无法在本地找到镜像 'hello-world:latest'。Docker 运行的第一步是测试它是否已经在本地缓存了容器镜像,这样它就不需要重新下载已经在主机上运行的容器。我们之前验证了目前 Docker 中没有任何容器镜像,所以 Docker 会从其默认的注册中心(Docker Hub)下载镜像。当 Docker 下载容器镜像时,它会一次下载一层,并计算一个哈希值,以确保镜像正确并且完整。你可以从前面的输出中看到,Docker 提供了sha256摘要,这样我们就可以确信下载的是正确的镜像。由于我们没有指定容器版本,Docker 会在 Docker Hub 注册中心搜索名为hello-world的镜像,并下载最新版本。当容器执行时,它输出了Hello From Docker,这是该容器设计的功能。

你也可以使用docker ps命令,省略-a标志,这样只会显示当前正在运行的容器,而不是已退出或已停止的容器。

Docker 容器是基于层构建的。每次构建 Docker 镜像时,你运行的每个命令都会在 Docker 镜像中创建一个层。当 Docker 构建或拉取镜像时,Docker 会逐个处理每一层,确保整个容器镜像被完整地拉取或构建。当你开始构建自己的 Docker 镜像时,记住以下几点很重要:层数越少,文件大小越小,镜像效率越高。下载层数很多的镜像对于使用你服务的用户并不理想,而且如果你的 Docker 镜像下载时间过长,快速升级服务也不方便。

现在我们已经下载并运行了第一个容器镜像,让我们再次查看本地的 Docker 镜像列表:

ubuntu@node01:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest 48b5124b2768 4 months ago 1.84 kB
ubuntu@node01:~$

如你所见,我们在本地缓存了 hello-world 镜像。如果我们重新运行这个容器,它将不再需要重新拉取镜像,除非我们指定一个比本地缓存中存储的镜像版本更高的版本号。现在我们可以再次查看 docker ps -a 的输出:

ubuntu@node01:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b0c4093ab38f hello-world "/hello" 28 minutes ago Exited (0) 28 minutes ago romantic_easley

从上面的输出中,你可以看到 Docker 创建了一个新的正在运行的容器,容器 ID 为 b0c4093ab38f。它还列出了用于启动此容器的源镜像的名称、执行的命令(在本例中是:/hello)、创建时间、当前状态以及容器名称。你可以看到这个特定的容器已经不再运行,因为它的状态是 Exited (0)。这个容器的设计方式是执行一个单一任务,任务完成后就退出。Exited (0) 状态让用户知道执行已成功完成。这与 Unix 系统中的二进制可执行文件非常相似,比如 catecho 命令。这些命令执行一个单一任务,完成后就停止。如果你的目的是为用户提供一个执行输出(比如解析文本、进行计算,甚至执行 Docker 主机上的任务)的容器,那么构建这种类型的容器是非常有用的。正如你稍后将看到的,你甚至可以向 docker run 命令传递参数,从而修改容器内应用程序的运行方式。

构建你的第一个容器

现在我们已经了解了 Docker 容器如何运行,以及 Docker 引擎如何下载和缓存容器镜像,我们可以开始构建运行服务的容器,例如 Web 服务器和数据库。在本节课中,我们将从一个 Dockerfile 构建一个容器,该容器将运行 Apache Web 服务器。然后我们将在 Docker 引擎中暴露端口,这样我们就可以访问我们刚刚实例化的运行中的 Web 服务。让我们开始吧。

Dockerfile

正如我们之前所学,Docker 容器由多个层组成,这些层本质上是叠加在一起,形成一个 Docker 容器镜像。这些层由命令组成,这些命令保存在纯文本文件中,Docker 引擎会按顺序执行这些命令来构建最终的镜像。Dockerfile 中的每一行代表镜像中的一层。构建 Dockerfile 的目标是让它们尽可能小巧简洁,这样我们的容器镜像就不会比必要的要大。在你的虚拟机的/vagrant目录中,创建一个名为Dockerfile的纯文本文件,并使用你喜欢的文本编辑器打开它。我们将从以下几行开始,逐一探讨:

FROM ubuntu:16.04
RUN apt-get update; apt-get install -y apache2
EXPOSE 80
ENTRYPOINT ["apache2ctl"]
CMD ["-DFOREGROUND"]

让我们逐行查看这个 Dockerfile:

  • FROM:指定我们希望基于哪个镜像来构建容器。在这个例子中,我们使用的是 Ubuntu 基础镜像,版本 16.04。Docker Hub 上有多个基础镜像和预构建的应用镜像,可以免费使用。

  • RUN:任何你希望容器在构建过程中执行的命令都会通过RUN参数传入。我们正在同时执行apt-get updateapt-get install。我们将这两个命令放在同一行RUN中执行,以便尽量减小容器层的大小。将包管理命令放在同一行RUN中也是一个好习惯,这样可以确保在执行apt-get install之前,源列表已经被更新。需要注意的是,当 Docker 镜像重新构建时,它只会执行那些被更改或添加过的行。

  • EXPOSEEXPOSE行指示 Docker 哪些端口应该在容器中开放,以接受外部连接。如果一个服务需要多个端口,可以用空格分开列出它们。

  • ENTRYPOINTENTRYPOINT定义了当容器启动时,默认运行的命令。在这个例子中,我们使用apache2ctl启动apache2 web 服务器。如果你希望容器保持持久运行,重要的是要以守护进程模式或后台模式运行应用,这样容器不会立即发送EXIT信号。稍后我们将介绍一个名为dumb-init的开源项目,它是一个用于在容器中运行服务的初始化系统。

  • CMD:在这个示例中,CMD定义了在运行时传递给ENTRYPOINT命令的参数。这些参数可以通过在启动容器时在docker run命令末尾提供额外的参数来覆盖。你在CMD中提供的所有命令或参数都以/bin/sh -c为前缀,这使得在运行时传递环境变量成为可能。还需要注意的是,根据你希望默认 shell 如何解释在容器内启动的应用程序,你可以在某些情况下交替使用ENTRYPOINTCMD。在线 Docker 文档中对如何使用CMDENTRYPOINT有更深入的最佳实践说明。

Dockerfile 中的每一行都会在最终的 Docker 容器镜像中形成一个独立的层,如下图所示。通常,开发人员希望尽量将容器镜像做得尽可能小,以最小化磁盘使用、下载和构建时间。通常通过在 Dockerfile 中的同一行上运行多个命令来实现这一点。

在这个示例中,我们运行了apt-get udpate; apt-get install apache2,以尝试将最终容器镜像的大小最小化。

图 2:Apache2 容器镜像中的层

这并不是 Dockerfile 中所有可用命令的详尽列表。你可以使用ENV导出环境变量,在构建时将配置文件和脚本复制到容器中,甚至使用VOLUME命令在容器中创建挂载点。更多这样的命令可以在官方的 Dockerfile 参考指南中找到,地址是docs.docker.com/engine/reference/builder/

既然我们已经了解了 Dockerfile 的内容,接下来让我们使用docker build命令来构建一个功能性的容器。默认情况下,docker build会在当前目录中查找名为Dockerfile的文件,并尝试逐层创建容器。请在你的虚拟机上执行以下命令:

docker build -t webservercontainer:1.0 .

重要的是使用-t标志传递一个镜像构建标签。在这种情况下,我们给镜像打上了webservercontainer的标签,并指定了版本1.0。这确保你能够通过docker image list输出识别你构建的版本。

如果你再次执行docker images命令,你会看到新构建的镜像现在已存储在本地镜像缓存中:

ubuntu@node01:$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
webservercontainer 1.0 3f055adaab20 7 seconds ago 255.1 MB

我们现在可以使用docker run启动新的容器实例:

docker run -d --name "ApacheServer1" -p 80:80 webservercontainer:1.0

这次,我们在docker run中传递了新的参数:

  • -d:表示我们将以分离模式或后台模式运行该容器。在这种模式下运行的容器启动时不会立即将用户登录到容器 shell 中。相反,容器将直接在后台启动。

  • --name:为我们的容器指定一个易于理解的名称,以便我们可以清楚地了解容器的用途。如果你没有传递名称标志,Docker 会为你的容器分配一个随机名称。

  • -p:允许我们打开主机上的端口,并将其转发到容器中的暴露端口。在这个示例中,我们将主机上的80端口转发到容器中的80端口。-p标志的语法始终是<HostPort>:<ContainerPort>

你可以通过在虚拟机上针对本地主机的80端口执行curl命令来测试该容器是否正在运行。如果一切顺利,你应该能看到默认的 Ubuntu Apache 欢迎页面:

ubuntu@node01:~$ curl localhost:80
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
...

这意味着 Docker 正在监听本地主机的80端口,并将该连接转发到容器,也监听80端口。容器的一个大优点是,你可以启动多个相同容器的实例,只要它们监听不同的端口号。只需几秒钟,你就可以创建一个提供各种服务的容器集群,并同样快速地清除它们。

让我们再创建两个 Apache 网络服务器容器,监听主机网络接口的100200端口。请注意,在以下示例中,我提供了不同的名称参数以及不同的主机端口:

docker run -d --name "ApacheServer2" -p 100:80 webservercontainer:1.0

docker run -d --name "ApacheServer3" -p 200:80 webservercontainer:1.0

如果你再次运行相同的curl命令,这次是针对100200端口,你将看到相同的 Ubuntu 默认网页。这有点无聊。让我们给容器增加一些个性。我们可以使用docker exec命令登录到正在运行的容器,并稍作自定义:

ubuntu@node01:~$ docker exec -it ApacheServer1 /bin/bash
root@bc951d6ec658:/#

docker exec命令需要以下标志才能访问正在运行的容器:

  • -i:以交互方式运行docker exec,因为我们将启动一个 Bash shell

  • -t:分配一个伪终端或终端会话

  • ApacheServer1:我们想要登录的容器的名称(或容器 ID)

  • /bin/bash:我们希望使用docker exec命令启动的终端或命令

运行docker exec命令后,你应该直接进入第一个 Apache 容器的 Bash shell。运行以下命令来更改 Docker 容器中的index.html文件。完成后,你可以通过输入exit退出容器的 shell 会话。

root@bc951d6ec658:/# echo "Web Server 1" > /var/www/html/index.html

从 Docker 主机上,再次在端口80上运行curl命令。你应该能看到你的 Apache 网络服务器使用的页面已发生变化:

ubuntu@node01:~$ curl localhost:80
Web Server 1

使用docker exec登录到另外两个容器,使用echo命令将默认的index.html页面更改为所有三个网络服务器容器独特的内容。你的curl结果应该能反映你所做的更改:

ubuntu@node01:~$ curl localhost:80
Web Server 1
ubuntu@node01:~$ curl localhost:100
Web Server 2
ubuntu@node01:~$ curl localhost:200
Web Server 3

注意:本练习的目的是演示docker exec命令。docker exec不是更新、修复或维护正在运行的容器的推荐方式。从最佳实践角度来看,当需要进行更改时,你应始终重新构建 Docker 容器,并在版本标签上进行递增。这确保了更改始终记录在 Dockerfile 中,因此容器可以尽可能快速地启动和销毁。

你可能还注意到,许多 Linux 操作系统工具、文本编辑器和其他实用程序在 Docker 容器中是缺失的。容器的目标是提供运行应用程序所需的最小化足迹。在构建自己的 Dockerfile 时,或者稍后当我们探索 Ansible 容器环境时,请思考一下容器内部的内容,考虑你的容器是否符合设计微服务的最佳实践。

容器生命周期管理

Docker 利用 Linux 控制组和命名空间为你提供了进程隔离的优势。与类 Unix 操作系统中的进程类似,这些进程可以在容器生命周期内启动、停止和重启,以实现更改。Docker 通过提供启动、停止、重新加载甚至查看容器日志的选项,使你能够直接控制容器的状态,必要时可以查看可能出现问题的容器日志。Docker 让你可以使用容器的内部 ID 或在我们启动容器时使用 docker run 指定的容器名称。以下是可以用来管理容器生命周期的 Docker 原生命令列表,在你构建和迭代不同版本时可以使用这些命令:

  • docker stop <容器 ID 或名称>:停止运行中的容器及容器内的进程。

  • docker start <容器 ID 或名称>:启动一个已停止或已退出的容器。

  • docker reload <容器 ID 或名称>:如果容器正在运行,reload 将优雅地停止容器并重新启动容器,以将其恢复到运行状态。如果容器已停止,reload 将启动已停止的容器。

  • docker logs <容器 ID 或名称>:显示由容器或在容器内运行的应用程序生成的任何日志,利用STDOUTSTDERR。日志对于调试行为异常的容器非常有用,而无需exec进入容器内部。

docker logs 有一个 --follow 标志,非常适合流式输出实时日志。这可以通过 docker logs --follow <容器 ID 或名称> 来访问。

从前面的示例中,我们可以像下面这样开始、停止、重新加载或查看我们之前构建的任何 Apache Web 服务器容器的日志:

docker stop ApacheServer2
docker start ApacheServer2
docker reload ApacheServer2
docker logs ApacheServer2

类似于这个示例,你可以通过查看 docker ps -a 的输出验证任何容器的状态。

对于所有 Docker 命令,包括docker runexecbuild,你可以通过添加--help标志来查看给定命令的所有可用选项。例如:docker run --help

参考资料

总结

在本章中,我们回顾了应用部署在 IT 基础设施中的历史,以及容器的历史和它们为什么会彻底改变软件开发。我们还通过手动运行容器和通过 Dockerfile 从头构建容器,迈出了构建 Docker 容器的第一步。

我希望,如果你是容器化和 Docker 的新手,本章能够为你提供一个好的起点,让你能亲身体验容器化的世界。Dockerfile 是构建容器的优秀工具,因为它们轻量、易于版本控制,并且具有很好的可移植性。然而,它们也有局限性,因为它们相当于 DevOps 世界中的 Bash 脚本。如果你需要调整配置文件、根据服务的状态动态配置服务,或根据容器将要部署的环境条件配置容器怎么办?如果你曾经在配置管理方面工作过,你会知道,虽然 Shell 脚本可以完成这项工作,但实际上有更好、更简单的工具可供使用。Ansible Container 就是我们所需要的工具,它将配置管理的强大功能与容器所带来的可移植性和灵活性结合在一起。在第二章,使用 Ansible Container,你将了解 Ansible Container,并亲眼看到我们如何快速构建和部署容器。

第二章:使用 Ansible Container

正如我们在第一章《使用 Docker 构建容器》中所见,容器化正在改变关键 IT 基础设施的维护和部署方式。随着 DevOps 方法论和思维方式在各组织中不断发展,开发与运维角色之间的界限越来越模糊。尽管 Docker 等工具持续发展和进化,但仍然需要开发新的工具,以应对不断增长的容器化应用部署和扩展需求。

Ansible 是一个独特的自动化框架,正如我们在第一章《使用 Docker 构建容器》中所见,它依赖于无代理架构,通过 SSH 协议从集中位置将服务器和虚拟化应用程序带入所需状态。与其他核心自动化工具相比,Ansible 提供了一种不同的方法,它与其他配置管理工具(如 Chef 和 Puppet)有所不同,后者依赖于代理和集中式服务器来存储和维护配置状态。

Ansible Container 项目旨在解决将关键的配置管理技术引入当前手动构建和部署 Docker 容器镜像的过程中的需求。当前,Docker 和 Docker 工具更侧重于使用 Swarm 和 Docker Compose 部署容器到 Docker 原生环境中。Ansible Container 是许多标准 Docker 工具的封装器,提供了将项目部署到各种云提供商、Kubernetes 和 OpenShift 的功能。撰写本文时,其他容器编排工具,如 Docker Swarm 和 Apache Mesos,尚不被支持。如果说 Dockerfile 在单体应用部署时代类似于 Shell 脚本,那么 Ansible Container 就是将自动化和可重复性带入容器生态系统的解决方案。由于 Ansible Core 使用 playbook 和 SSH 作为接口来实现期望的状态,Ansible Container 可以使用相同的 playbook 和原生容器 API 来构建和部署容器。

如果你或你的组织已经在使用 Ansible 角色来定制化部署应用程序和服务,那么这些相同的角色可以被用来将这些应用程序和服务转化为容器,帮助简化你的容器构建流程。在从裸机和虚拟化部署转向容器化时,你可以放心,定制的配置和设置将在构建容器时得到保留。

本章我们将学习:

  • Ansible Container 和微服务架构简介

  • Docker Compose 快速入门

  • Ansible Container 工作流

  • Ansible Container 快速入门

Ansible Container 和微服务架构简介

虽然使用 Ansible Container 在重用现有 Ansible 工件、模块和剧本方面具有大量的优势,但必须仔细考虑将现有服务移植过来所需的任何更改。Ansible 给予您在编写剧本和角色时极大的自由度,以适应您组织架构和资源限制的独特性。例如,一个典型的网页应用程序可能具有三层不同的功能:一个网页服务器,为您的最终用户提供网站;一个数据库用于存储数据;以及一个缓存,提供从数据库中常访问的数据给网页服务器。根据架构和资源限制,这些服务可能以多种方式实现。您可能将网页服务器、缓存层和数据库部署在三组独立的服务器集群上。您也可以选择将网页服务器和缓存层部署在同一集群上,而数据库则部署在第二集群上。或者,所有三层可能部署在同一裸机或虚拟化的服务器集群上,并由负载均衡器在必要时提供冗余。您的基础设施是一个独特的“雪花”,Ansible 让您可以自由地编写并以几乎任何适合您需求的配置部署剧本角色。

微服务架构 是用来描述将应用服务独立且模块化地拆分为独立且可部署单元的术语。在容器的世界里,您希望每个容器都符合微服务架构,将每个服务作为一个独立的容器进行创建,这样可以独立于其他服务进行部署和扩展。虽然在同一个容器中部署多个服务是可能的,但通常这是一个不好的做法,因为每增加一个服务,容器就会增加额外的层,导致构建和部署新容器时产生不必要的开销。

在前面的示例中,每个核心服务(网页服务器、缓存和数据库)都将是一个独立的微服务,您希望将其隔离并封装到容器中。如果您的网页应用程序进入生产环境后,您发现预计的流量比原先预期的要高得多,且数据库查询成为瓶颈,那么根据需求动态部署更多的缓存或数据库容器将带来巨大的优势。采用以微服务为导向的容器设计将使您的基础设施更加简化、易于部署,并且能够更快地扩展,以满足需求较大的用户。

在考虑将现有的 Ansible 角色迁移到 Ansible Container 项目时,关键是要考虑你的角色目前有多紧密集成。理想情况下,Ansible 角色应该能够独立运行,几乎不依赖于其他环境特性。是不是感觉这听起来像我们之前描述的容器化微服务?这正是 Ansible Container 在其他配置管理工具中独树一帜的原因。Ansible 的原语已经设计得很好地适应容器化生态系统。即使你当前没有使用 Ansible 作为你的配置管理工具,Ansible Container 仍然是一个非常出色的工具,适用于从开发到生产的整个容器构建、维护和部署过程。

Docker Compose 简介

Docker Compose 是 Docker 工作流工具之一,允许你轻松地同时构建和运行多个容器。在开始使用 Ansible Container 之前,了解 Docker Compose 的基本原理非常重要,因为 Ansible Container 的许多核心功能都围绕 Docker Compose 构建。

在上一章中,我举了一个例子,展示了如何创建三个 Apache Web 服务器容器,以演示如何利用相同的容器基础镜像同时运行多个容器。使用 Docker Compose,你可以通过提供一个 YAML 定义文件来描述你想要运行的容器、容器运行时所需的任何 docker run 参数(如端口、卷等)以及运行容器之前要创建的任何链接或依赖,而无需提供三个单独的 docker run 命令。当执行 Docker Compose 时,它将自动尝试启动 YAML 文件中描述的容器。如果镜像尚未在本地缓存,它将尝试从互联网上下载,或者如果提供了 Dockerfile,它将构建容器镜像。让我们做一个快速练习,了解 Docker Compose 的工作原理。

如果你没有使用提供的 Vagrant 实验环境,如在第一章 构建 Docker 容器 中讨论的那样,你需要首先使用以下命令下载 Docker Compose。所提供的步骤假设你已经在 Linux 或 macOS 机器上安装并运行了 Docker Engine。确保安装与现有 Docker Engine 版本相同的 Docker Compose,以确保最大兼容性。执行以下命令下载 Docker Compose 可执行文件,并将其复制到 /usr/local/bin 目录,并赋予 execute 权限。

sudo curl -L https://github.com/docker/compose/releases/download/1.17.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

最新的安装文档可以在 docs.docker.com/compose/install 找到。

默认情况下,Docker Compose 会在您当前工作目录中查找名为 docker-compose.yml 的文件。我提供了一个示例 docker-compose.yml 文件作为参考。在您的工作站上,创建一个名为 docker-compose 的目录,并在该目录下创建一个空白的 docker-compose.yml 文件。然后粘贴以下内容:

version: '2'
services:
  Cache_Server:
    image: memcached:1.4.36
    ports:
      - 11211:11211
    volumes:
      - .:/var/lib/MyVolume

让我们逐行分析这个文件:

  • version:这一行表示要使用哪个版本的 Docker Compose API。在本例中,我们使用的是版本 2。写作时,API 还有版本 3,提供了一些新特性。然而,对于我们的目的,使用版本 2 就足够了。version 参数通常是 Docker Compose 文件的开头,并且没有缩进。

  • servicesservices 行启动了您的 docker-compose.yml 文件中列出要创建的每个服务容器的部分。在这个特定的 Docker Compose 文件中,我们将创建一个名为 Cache_Server 的服务,它启动一个 memcached 容器。您指定的每个服务都应该在 services 声明下缩进两个空格。还应注意,服务名称是用户定义的,并用于生成容器名称。在创建多容器的 Docker Compose 文件时,Docker 提供了基于服务名称的简单 DNS 解析。更多内容请参考第八章,构建和部署多容器项目

  • imageimage 用于指定您希望容器基于的容器镜像。在本例中,我们使用的是来自 Docker Hub 的官方 memcached 镜像,指定版本为 1.4.36。如果我们希望始终使用最新版本的镜像,也可以使用 latest 关键字来代替版本号。

  • portsports 参数指示要转发到容器的主机端口。在本例中,我们将端口 11211 转发到暴露的容器端口 11211。类似于 docker run,端口必须按 host:container 的格式指定。这是一个 YAML 列表,因此每个端口必须缩进并以连字符(-)作为前缀。

  • volumes:该参数指定您希望使 Docker 主机上的任何目录或存储卷可供容器访问。如果容器中有您可能想要备份、导出或以其他方式与容器共享的数据,这将非常有用。这个卷挂载仅作为语法示例。与 ports 参数类似,volumeshostDirectory:containerDirectory 的格式接收一个列表。

要使用 Docker Compose 启动我们的容器,只需执行命令docker-compose up。默认情况下,这将按顺序启动 Docker Compose 文件中的所有容器,除非指定了容器依赖。使用docker-compose启动的容器将以attached模式启动,这意味着容器进程会运行,并接管您使用的终端。与docker run类似,我们可以提供-d标志,以detached模式运行容器,这样我们就可以在同一个终端中进行一些验证:

docker-compose up -d 

您将看到,类似于docker run,Docker Compose 会自动判断 Docker 主机上没有容器镜像,并成功从互联网下载镜像及相应的层。

ubuntu@node01:/vagrant/Docker_Compose/test$ docker-compose up -d
Creating network "test_default" with the default driver
Pulling Cache_Server (memcached:1.4.36)...
1.4.36: Pulling from library/memcached
56c7afbcb0f1: Pull complete
49acdc7c75c9: Pull complete
152590a2a704: Pull complete
4dc7b8165378: Pull complete
4cb74c11bcdd: Pull complete
Digest: sha256:a2dfef5700944ec8bb2d2c0d6f5b2819324b1b91647dc09847ce81e7a91e3fe4n
Status: Downloaded newer image for memcached:1.4.36
Creating test_Cache_Server_1 ...
Creating test_Cache_Server_1 ... done

运行docker ps -a将显示 Docker Compose 成功创建了具有正确暴露端口和挂载卷的运行容器,这些信息列在我们的docker-compose.yml文件中:

ubuntu@node01:/vagrant/Docker_Compose/test$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
cacf58b455f3        memcached:1.4.36    "docker-entrypoint.sh"   7 minutes ago       Up 7 minutes        0.0.0.0:11211->11211/tcp   test_Cache_Server_1

我们可以使用telnet来确保memcached应用程序正常运行,并通过主机网络进行转发。使用telnet,我们可以直接从Memcached存储和检索数据:

ubuntu@node01:/vagrant/Docker_Compose/test$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
STAT active_slabs 0
STAT total_malloced 0
END 

运行stats slabs命令可以让我们知道memcached已经部署并按预期运行。

现在我们对 Docker 和 Docker Compose 有了简要的介绍,已经掌握了开始使用 Ansible Container 所需的基本技能。

Ansible Container 工作流

与其他编排和自动化工具类似,Ansible Container 包含一组构成容器化工作流的实用工具。使用 Ansible Container,您可以创建、构建、运行和部署容器,从开发到生产,利用 Ansible Container 自带的工具套件。Ansible Core 的开箱即用方法论也被应用到 Ansible Container 中,为开发人员和系统管理员提供完整的容器化工作流解决方案。以下是 Ansible Container 的主要功能概述,以及它们如何与容器化应用程序的典型生命周期相对应:

  • ansible-container init:用于初始化启动一个 Ansible Container 项目。init会构建并创建启动 Ansible Container 项目所需的目录框架和基础文件。

  • ansible-container build:顾名思义,build将解析项目中的主要文件,并尝试构建描述的容器。Ansible Container 通过首先创建一个被称为conductor容器来实现这一点。conductor容器是在项目的构建阶段创建的主容器,里面运行着 Ansible 的副本。一旦其他容器启动,conductor容器负责对它们运行 Ansible 角色和剧本,以使容器进入预期状态。

  • ansible-container runrun的工作方式与docker run非常相似,执行时,run会将构建好的容器尝试在主机的容器引擎中运行。默认情况下,run命令会考虑在container.yml文件中列出的任何开发选项,除非在运行时传入-- production标志。

  • ansible-container destroy:停止所有运行中的容器,并移除已构建的镜像文件。此命令在从头测试端到端部署时非常有用。

  • ansible-container push:此命令将你用 Ansible Container 构建的容器镜像推送到你选择的容器注册表中,如 Docker Hub、Quay 或 GCR。此命令类似于docker push

  • ansible-container deploydeploy(以前叫做ShipIt)会生成一个自定义的 Ansible playbook 和角色,将你的容器部署到云服务提供商。写这篇文章时,deploy仅支持 OpenShift 和 Kubernetes。使用ansible-playbook命令运行这个 playbook,将把容器部署到指定的提供商。

如你所见,Ansible Container 自带一个端到端生命周期管理系统,允许你从开发到生产管理容器。Ansible Container 利用强大且可定制的 Ansible 配置管理系统,使容器的创建和部署类似于裸金属或虚拟节点。

所有 Ansible Container 的子命令可以通过运行ansible-container --help来查看。

Ansible Container 快速入门

本章的这一部分将重点介绍如何开始使用 Ansible Container,初始化一个基础项目,并重新创建之前的memcached示例。如果你没有跟随 GitHub 上提供的 Vagrant 实验,第一步是使用python-pip包管理器安装 Ansible Container。以下步骤将介绍如何在基于 Debian 的 Linux 发行版上安装支持 Docker 的 Ansible Container:

sudo apt-get update
sudo apt-get install python-pip
sudo pip install ansible-container docker

Ansible Container 初始化

现在,你应该已经在你的环境中安装并准备好运行 Ansible Container。启动一个新的 Ansible Container 项目所需的第一个命令是ansible-container init。登录到你的 Vagrant 虚拟机后,在/vagrant目录下创建一个空目录并输入:

ubuntu@node01:~$ mkdir /vagrant/demo
ubuntu@node01:~$ cd /vagrant/demo
ubuntu@node01:/vagrant/demo$ ansible-container init 
Ansible Container initialized.

需要注意的是,最终的实验练习可以在官方书籍的 GitHub 仓库中找到,目录路径为:AnsibleContainer/demo

当 Ansible Container 成功创建新项目时,它将返回响应Ansible Container initialized

如前所述,init命令创建了构建 Ansible Container 项目所需的基本目录结构和布局。导航到该目录并查看目录列表,可以让你了解 Ansible Container 项目的样子:

demo
├── ansible.cfg
├── ansible-requirements.txt
├── container.yml
├── meta.yml
└── requirements.yml

让我们逐一查看这些文件,以便了解它们在 Ansible Container 项目中的作用:

  • ansible.cfg:Ansible 引擎的主要配置文件。任何希望 Ansible conductor容器利用的设置都应该放在此文件中。如果你已经熟悉使用 Ansible 进行配置管理任务,那么你应该对ansible.cfg文件有所了解。大多数情况下,除非在容器构建过程中需要以特定方式运行 Ansible,否则可以安全地忽略此文件。有关 Ansible 配置选项的更多信息,可以参考 Ansible 文档:docs.ansible.com.

  • ansible-requirements.txtansible-requirements.txt文件用于指定你的剧本成功运行所需的任何 Python pip 依赖项。Ansible 引擎是由一系列模块构建的,这些模块执行剧本中描述的任务。任何运行 Ansible 角色所需的额外 Python 包都列在此文件中。

  • container.yml:描述容器的状态,包括基础镜像、暴露的端口和卷挂载。container.yml的语法类似于 Docker Compose 格式,但有一些差异,我们将在本书中逐步介绍。

  • meta.ymlmeta.yml文件包含有关容器项目的元数据,包括作者名称、版本信息、软件许可详情和标签等。这些信息使得其他用户在你选择将项目分享到 Ansible Galaxy 时,能够轻松找到你的项目。

  • requirements.yml:定义容器项目将使用的任何 Ansible Galaxy 角色和版本信息。在此文件中,你可以描述项目所需的具体角色和角色版本。Ansible Container 将在构建容器项目之前从 Ansible Galaxy 下载这些角色。通过在requirements.yml文件中指定角色,可以确保项目始终使用相同的角色来构建基础容器镜像。需要注意的是,ansible-requirements.ymlrequirements.yml之间的区别。requirements.yml用于管理项目所依赖的 Ansible 角色,而ansible-requirements.yml用于管理这些角色可能需要的 Python pip 包。

现在我们已经了解了 Ansible Container 项目的基本结构,接下来可以深入探索并开始创建一个简单的 Ansible Container 项目。还记得我们之前创建的 Docker Compose 项目吗?我们可以以此为起点,通过编辑container.yml文件将该项目迁移到 Ansible Container。在文本编辑器中打开container.yml文件。默认情况下,container.yml文件带有预填充的结构,这在很多方面类似于 Docker Compose 文件。你的container.yml文件应该类似于以下内容。为了节省空间,我已经删除了许多注释和示例数据:

version: "2"
settings:
  conductor_base: centos:7

services: {}

registries: {}

每个部分都有其特定目的,用于构建你的 Ansible Container 项目。理解每个 YAML 定义的作用非常重要。文件中默认的注释提供了各个部分所使用的设置示例。以下是 container.yml 文件的关键部分及如何在 Ansible Container 项目中使用这些部分的列表:

  • versionversion 部分标明了使用哪个版本的 Docker Compose API。如前所述,Ansible Container 是许多 Docker Compose 服务的封装器。在这里,我们可以指定要使用哪个版本的 Docker Compose API 来运行我们的容器。

  • settingssettings 部分用于指定附加集成或修改我们 Ansible Container 项目的任何默认行为。默认情况下,有一个设置已启用。

  • conductor_base:这表示我们希望项目使用哪个基础镜像。conductor 容器负责创建用于运行 Ansible playbooks 和 roles 的 Python 环境。conductor 镜像将连接到它创建的其他容器,在构建过程中提供访问其自身 Python 环境的权限。因此,使用与计划构建的容器镜像相同的基础容器操作系统非常重要,这样可以确保 Python 和 Ansible 方面的完全兼容性。可以把 conductor 镜像看作是一个类似于标准 Ansible 实现中 Ansible 控制节点的容器。这个容器将通过 Docker API 直接与其他节点(容器)进行交互,以将其他容器带入期望的状态。构建完容器后,conductor 容器会默认自我删除,除非你指示 Ansible Container 保留该镜像以便进行调试。除了指定我们的 conductor 镜像外,我们还可以在设置部分指定其他集成项,如 Kubernetes 凭证或 OpenShift 端点。我们将在后续章节中深入探讨这些内容。

  • servicesservices 部分几乎与我们 Docker Compose 文件中的 services 部分相同。在该部分中,我们将提供 YAML 定义,描述容器的运行状态:我们将使用的基础镜像、容器名称、暴露的端口、卷等。services 部分中描述的每个容器都是一个由我们的 conductor 镜像通过 Ansible 配置的 节点。默认情况下,services 部分被禁用,YAML 定义旁边有两个大括号:{}。在添加容器定义之前,删除大括号,以便 Ansible Container 可以访问子数据。

  • registries:我们 container.yml 文件的最后一部分是 registries 部分。在这里,您可以指定容器注册表,Ansible Container 将从中拉取镜像。默认情况下,Ansible Container 使用 Docker Hub,但您也可以指定其他注册表,例如 Quay、gcr.io 或本地托管的容器注册表。此部分还与 ansible-container push 命令一起使用,将您构建的容器推送到您选择的注册表服务。

Ansible Container 构建

我们的 Ansible Container 工作流的第二部分是构建过程。现在我们已经初始化了第一个项目,即使没有定义任何服务或角色,我们仍然可以探索 ansible-container build 功能的工作原理。从 demo 目录中运行 ansible-container build 命令。您应该会看到类似于以下的输出:

ubuntu@node01:/vagrant/AnsibleContainer/demo$ ansible-container build
Building Docker Engine context...
Starting Docker build of Ansible Container Conductor image (please be patient)...
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting.       project=demo
All images successfully built.
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=c4f7806f8afb0910e4f7d25e5c37be32800ed8b41618d246f70da0508322c479 save_container=False

在本地工作站上第一次运行 Ansible Container 构建可能需要几分钟时间才能完成,因为在开始之前需要先构建 conductor 容器。需要记住的是,conductor 容器负责通过 Docker API 连接到服务容器,并在其上执行 Ansible 剧本和角色。由于这是一个基本的 ansible-container build 命令示例,所以我们正在创建的容器上没有要运行的 Ansible 剧本。稍后在书中,我们将编写自己的角色,真正探讨 conductor 容器的功能。下图演示了 conductor 容器如何连接到服务容器:

图 1:Conductor 容器将服务容器带入所需状态

然而,在此示例中,Ansible Container 将首先连接到本地主机上的 Docker API 以确定构建上下文,下载所需的镜像依赖项,并执行 conductor 容器的构建。您可以从前面的输出中看到,我们的 conductor 容器已成功为我们的项目 demo 构建。它还列出了返回码,确认我们的镜像已成功构建,以及一个内部的 conductor ID,这是 Ansible Container 生成的。

如果我们执行命令 docker ps -a,我们会看到当前没有正在运行或退出的容器。这是预期的,因为我们还没有在 container.yml 文件的 services 部分定义任何容器。您还会看到,由于我们没有传递任何参数或配置来指示 Ansible Container 保存我们的 conductor 容器,Ansible Container 在运行完成后删除了该容器。

ubuntu@node01:demo$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES

然而,如果我们查看 docker images 输出,你会发现我们构建的 conductor 镜像被缓存了,以及用于创建它的基础镜像。请注意,指挥官镜像的前缀是 demo-*。Ansible Container 会根据 project-service 命名规则自动命名容器镜像。这确保了,如果你同时构建和运行多个容器项目,能够轻松辨别哪个容器属于哪个项目。

在这种情况下,我们的项目名为 demo,而我们正在构建的服务是 conductor

ubuntu@node01:demo$ docker images
REPOSITORY  TAG  IMAGE ID  CREATED  SIZE
demo-conductor  latest  a24fbeee16e2  38 seconds ago  574.5 MB
centos  7  3bee3060bfc8  3 weeks ago  192.6 MB

我们还可以通过传递 --save-conductor-container 标志来构建我们的项目,以在 ansible-container build 过程结束后保留我们的 conductor 容器。这对于调试失败的构建非常有用,因为它让我们能够从 Ansible 运行的上下文中查看容器。让我们尝试重新构建 demo 项目,这次保存 conductor 容器:

ubuntu@node01:demo$ ansible-container build --save-conductor-container
Building Docker Engine context...
Starting Docker build of Ansible Container Conductor image (please be patient)...
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting.       project=demo
All images successfully built.
Conductor terminated. Preserving as requested.  command_rc=0 conductor_id=ff84fa95d5908b076ce432d1076533679d945104e506ad5599e417cece7c3a5d save_container=True

这一次,你会看到输出显示出一个细微的差异:Conductor terminated. Preserving as requested,除此之外还有我们之前观察到的输出。这表示,尽管指挥官由于完成任务而停止了,但容器 demo_conductor 仍然存在,可以通过 docker ps -a 查看:

ubuntu@node01:/vagrant/AnsibleContainer/demo$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
c3c7dc04d251  a24fbeee16e2  "conductor build –pr"  3 minutes ago Exited (0) 3 minutes ago  demo_conductor

通过深入理解 Ansible Container 构建过程的工作原理,以及 Ansible Container 如何构建指挥官镜像,我们可以利用这些知识重新创建本章开始时介绍的 Docker Compose 项目。我们可以使用 Ansible Container 启动之前创建的 memcached 服务器容器。

在文本编辑器中,打开我们之前查看过的 conductor.yml 文档。删除 services: {} 声明后的花括号,并按照 YAML 语法在其下方添加以下内容,缩进两个空格:

services:
  AC_Cache_Server:
    from: memcached:1.4.36
    ports:
      - "11211:11211"
    volumes:
      - ".:/var/lib/MyVolume"

你可以看到,我们用于指定服务的语法与之前创建的 Docker Compose 语法非常相似。为了演示的目的,我们将使用与之前 Docker Compose 相同的 portsvolume 参数,这样读者可以轻松看到语法中的细微差别。你会注意到,container.yml 语法和 Docker Compose 语法有许多相似之处,但主要的区别在于,Ansible Container 允许在构建和部署容器服务时更具灵活性。

保存并关闭文件。如果你再次执行 ansible-container build 命令,你应该会看到以下输出:

ubuntu@node01:demo$ ansible-container build
Building Docker Engine context...
Starting Docker build of Ansible Container Conductor image (please be patient)...
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting.       project=demo
Building service...     project=demo service=AC_Cache_Server
Service had no roles specified. Nothing to do.  service=AC_Cache_Server
All images successfully built.
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=22126436967e7810aff44c83fb75d2276bb9a66352ddbd44a68d44219fe97344 save_container=False

在 Ansible Container 构建了我们的 conductor 镜像后,我们可以从输出中观察到,Ansible Container 现在识别出我们启用了名为AC_Cache_Server的服务,并且它正在尝试构建该服务。然而,我们并没有为此服务定义任何 Ansible 角色,因此它返回了消息Nothing to do。通常情况下,这将是执行我们的 playbook 以构建我们创建的服务的步骤。由于我们没有定义任何角色,Ansible Container 将跳过此步骤,并像往常一样终止conductor容器。

Ansible Container 运行

现在我们已经定义了一个服务,可以使用ansible-container run命令来启动该服务。该命令快速生成一个小型的 Ansible playbook,负责启动container.yml文件中指定的容器。这个 playbook 利用了 docker_service Ansible 模块来启动、停止、重启和销毁容器。docker_service 模块还可用于与主机操作系统上安装的 Docker 守护进程进行交互,拉取和删除 Docker 镜像缓存中的镜像。虽然目前理解该模块的实现细节并不是特别重要,但了解 Ansible Container 如何在幕后运行容器是很有帮助的。执行 ansible-container run 命令将显示 playbook 执行的各个阶段,以及 play recap,其输出类似于以下内容:

ubuntu@node01:demo$ ansible-container run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
WARNING Image memcached:1.4.36 for service AC_Cache_Server not found. An attempt will be made to pull it.

PLAY [localhost] ***************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=17aaa7aac99ff12427a7f4fb2671b24cc1ec33b774c701723dabb27eb6d75b07 save_container=False

正如您通过阅读 playbook 运行的输出可以看到的,您可以轻松地跟踪我们项目的关键亮点,随着我们将其带入运行状态:

  • 我们的项目无法找到我们指定的memcached镜像,因此 Ansible Container 从默认仓库(Docker Hub)中拉取它。

  • 我们的主机上做出了一个更改,以使容器进入运行状态。

  • 我们的任何操作都没有失败;有一个任务成功了(启动我们的容器),而这个成功的任务在我们的主机上进行了更改,以便启动容器。

  • conductor 服务已终止。

理解 Ansible Container 剧本中的亮点对于理解 Ansible 编排如何部署和维护我们的应用程序至关重要。正如我们之前讨论的那样,Ansible 团队非常努力地确保 Ansible 剧本的执行非常简单易懂,并且容易调试。通过展示启动容器项目所需的所有步骤,调试失败问题非常容易,也能看到潜在的改进点,帮助我们在开发更复杂项目时不断前进。刚刚执行的剧本是在执行 ansible-container run 时动态生成的,位于 ansible-deployment 目录中。利用 Ansible Container 运行项目,消除了部署和维护项目的许多复杂性,因为所有部署的复杂性都被抽象化。从用户的角度来看,你关注的是确保容器能够正常运行并正确构建。Ansible Container 成为一个端到端的生命周期管理工具,使得容器能够始终如一地构建,并每次都在预期的状态下运行。正如我们将在本书后面看到的那样,借助 Ansible Container 简化部署的复杂性,在使用 Kubernetes 或 OpenShift 的环境中尤为有用。

现在我们的容器运行已经完成,让我们使用 docker ps -a 命令查看一下主机上正在运行哪些容器:

ubuntu@node01:/vagrant/AnsibleContainer/demo$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c4a7792fb1fb memcached:1.4.36 "docker-entrypoint.sh" 14 seconds ago Up 13 seconds 0.0.0.0:11211->11211/tcp demo_AC_Cache_Server_1

正如预期的那样,很容易看到我们的 memcached 容器(版本 1.4.36)处于运行状态。同时,注意到 conductor 容器没有在我们的 docker ps 输出中显示出来。Ansible Container 仅运行在 container.yml 文件中定义的容器作为期望状态,除非你选择保留 conductor 容器用于调试目的。正如我们在 container.yml 文件中指定的那样,容器的名称是 demo_AC_Cache_Server_1。你可能会问,为什么会这样?因为我们在创建 container.yml 文件时明确地将容器命名为 AC_Cache_Server。Ansible Container 的一个重要特点是它能够理解,作为开发者,我们可能会在同一台主机或一组主机上同时运行和测试多个版本的项目。默认情况下,当 Ansible Container 启动容器时,它会自动将我们的项目名称(在本例中是 demo)附加到正在运行的容器名称的前面,并加上一个表示实例 ID 的数字。

在这种情况下,由于我们已经有一个容器实例在运行,Ansible Container 自动将 demo__1 分别附加到容器名称的开头和结尾,这样可以避免如果我们在同一主机上测试多个版本的容器时发生冲突。

由于我们正在重新创建章节开始时在此主机上的练习,让我们使用之前执行过的telnet命令来测试stats slabs,以查看我们的memcached容器是否正常运行并按预期响应:

ubuntu@node01:/vagrant/AnsibleContainer/demo$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
STAT active_slabs 0
STAT total_malloced 0
END

看起来我们的容器化服务正在运行,并且正确地接受请求,监听 Docker 主机的网络接口。请记住,我们在container.yml文件中指定了本地主机端口(11211)应该转发到容器的监听端口(也是11211)。

让我们快速查看 Docker 主机上的镜像缓存。我们可以通过执行docker images命令来完成:

ubuntu@node01:/vagrant/AnsibleContainer/demo$ docker images
REPOSITORY  TAG  IMAGE ID  CREATED  SIZE
demo-conductor  latest  a24fbeee16e2  48 minutes ago  574.5 MB
centos  7  3bee3060bfc8  3 weeks ago  192.6 MB
memcached  1.4.36  6c32c12d9101  6 weeks ago  83.88 MB

根据这个输出,我们可以更清楚地了解 Ansible Container 在后台是如何工作的。为了启动我们的demo项目,Ansible Container 不得不利用三个镜像:CentOS 7memcacheddemo-conductor。名为demo-conductor的容器镜像是构建过程中创建的指挥镜像。为了构建指挥镜像,Ansible Container 不得不下载并缓存这个输出中看到的CentOS 7基础镜像。最后,memcached是 Ansible 需要从镜像库拉取的容器,因为它在我们的container.yml文件的services部分中被指定。读者还可能注意到,指挥镜像的名称以我们的项目名demo为前缀,这与前面输出中的服务容器运行状态类似。这是为了避免名称冲突,并且能够在同一主机上同时运行多个容器项目。

Ansible Container 销毁

完成demo项目的实验后,我们可以使用ansible-container destroy命令停止所有正在运行的容器实例,并从系统中移除它的所有痕迹。destroy对于清理现有部署以及通过从头重建容器来测试我们的容器非常有用。要销毁容器,只需在项目目录中运行ansible-container destroy命令。

ubuntu@node01:/vagrant/AnsibleContainer/demo$ ansible-container destroy
Parsing conductor CLI args.
Engine integration loaded. Preparing to stop+delete all containers and built images.    engine=Docker™ daemon

PLAY [localhost] ***************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

TASK [docker_image] ************************************************************
changed: [localhost]

TASK [docker_image] ************************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=3    unreachable=0    failed=0

All services destroyed. playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=1dc36baefde06235a8c7c18733479501bfd48f7c8da0915f4bde1b196e3eff65 save_container=False

类似于之前看到的run命令,destroy执行的是由run过程自动生成的相同的剧本。然而,这次,它会停止并删除container.yml文件中指定的容器。你可能会发现,运行docker ps -a命令后,我们主机上不再显示任何正在运行的容器:

同样,destroy功能已经清除了conductor容器镜像以及 Docker 主机上的服务容器镜像。我们可以通过docker images命令来验证这一点:

ubuntu@node01:/vagrant/AnsibleContainer/demo$ docker images
REPOSITORY  TAG  IMAGE ID  CREATED  SIZE
<none>  <none>  e23c420b896a  43 minutes ago  576.3 MB
centos  3bee3060bfc8  4 weeks ago  192.6 MB

请注意,系统上唯一剩下的容器是基础CentOS容器。它可以手动删除,但默认情况下,Ansible Container 会将其保留在系统中,以加快销毁和重建项目的过程。

摘要

在本章中,我们学习了一些关于 Ansible Container 如何工作的基本概念,了解了它如何利用 Docker Compose API,以及 Ansible Container 内置的一些基本生命周期管理工具,包括 initbuildrundestroy。深入掌握这些功能的作用和工作原理,对于今后深入探讨我们将在 Ansible Container 中创建的更复杂项目具有基础性意义。虽然这个示例包含在本书的官方 Git 仓库中,但你可以随意重新创建并调整这些示例,以进一步实验 Ansible Container 的工作方式。在下一章,我们将学习如何将 Ansible Container 与现有角色结合使用,利用这些角色创建可复用的容器工件。

第三章:你的第一个 Ansible Container 项目

正如我们在第二章《与 Ansible Container 一起工作》中了解到的,Ansible Container 是一个强大的工具,用于在生产环境中编排、部署和管理容器。通过一套独特的多功能工具,Ansible Container 可以启动、构建、运行和部署容器,使开发人员能够构建容器化应用程序,并将其部署到本地环境或云托管服务提供商。使用 Ansible Container,我们可以确保容器能够准确构建、可靠运行,并为用户提供一致的体验,无论容器部署到哪个应用程序或平台。

在本章中,我们将专注于构建我们的第一个 Ansible Container 项目,方法是构建一个应用程序容器,在本地环境中测试它,并将我们的容器工件推送到容器镜像仓库。这将为用户提供 Ansible Container 的实际使用案例,并提供利用容器启用角色的经验。在本章中,你将学到:

  • 什么是 Ansible 角色和容器启用角色?

  • Ansible Galaxy 中的角色

  • Ansible Container NGINX 角色

什么是 Ansible 角色和容器启用角色?

Ansible 中的角色是一种将 playbook 组织成可重用、可共享且独立单元的方式,这些单元通常按应用程序进行划分。一个角色内部通常包含一系列 playbook、配置文件模板、静态文件和其他元数据,这些都是将目标主机(或容器)带入所需状态所必需的。在典型的三层应用堆栈中,包括 Web 服务器、数据库服务器和负载均衡器,这些组件可能被包含在三个独立的 Ansible 角色中。这提供了跨基础架构重用的好处,并且为共享 playbook 提供了一种简单的方式,无论是在互联网上还是与同事共享。例如,如果你为一个项目编写了负载均衡器角色,并且需要为另一个完全不同的项目配置另一个负载均衡器,你只需下载该角色并将其分配给另一个主机清单集。在 Ansible Core 中,角色通过父 playbook 分配给服务器或虚拟机,父 playbook 描述了基础架构的样子以及 Ansible 如何将该基础架构带入所需的状态。角色的主要好处是,它们为用户提供了一个简单的界面,用于访问常用的 playbook 任务和资源,从而确保用户的基础架构按预期精确配置并正常运行。

在 Ansible Container 中,角色的工作方式与 Ansible Core 非常相似。在 Ansible Container 中,角色不是基于基础设施组件分配的,而是分配给单个容器,然后通过 conductor 容器使用 Ansible playbooks 中描述的配置来构建这些容器。Ansible Container 的一个主要优点是,它大大简化了在基础设施中启用容器化资源的过程。如果你当前正在使用 Ansible Core 进行配置管理,许多 Ansible Core 角色可以重用,以构建与基础设施运行方式非常相似的容器。不幸的是,由于容器和完整的基础设施服务器在本质上是不同的,并不是所有任务都可以直接移植到 Ansible Container 角色中,仍需要进行一些修改。例如,由于容器比完整的操作系统更轻量,因此容器通常缺少大多数操作系统发行版中自带的工具和组件,如初始化系统和资源管理器。

为了解决这种差异,Ansible Container 项目创建了一种不同子集的角色,称为 容器启用角色。这些角色是专门为容器设计的,通常比常规的 Ansible 角色更简约。这些角色用于创建具有尽可能小的占用空间,同时最大化功能和灵活性的最终容器镜像。容器启用角色包含与常规 Ansible 角色相同的许多构造,如模板、任务、处理程序和元数据。这使得如果你熟悉 Ansible 语法和语言构造,开始为 Ansible Container 编写角色变得非常容易。

Ansible Galaxy 中的角色

Ansible Galaxy,位于 galaxy.ansible.com,是由 Ansible 社区创建的网站,用于共享、下载和鼓励重用 Ansible 角色。在 Ansible Galaxy 中,你可以搜索并下载几乎任何你希望自动化的应用程序或平台的角色。如果你有 Ansible Core 的经验,你无疑已经使用过 Ansible Galaxy 来下载、共享和探索由其他 Ansible 用户编写和维护的角色。如果你是 Ansible 新手,Galaxy 让你可以轻松地通过 web 浏览器或 Ansible 命令行查找并利用新的角色。随着 Ansible Container 的发布,你可以浏览 Ansible Galaxy 查找核心角色以及容器启用角色。从主网站 (galaxy.ansible.com) 上,你可以选择 BROWSE ROLES | Role Type | Container Enabled,来搜索适合你特定需求的角色:

图 1:在 Ansible Galaxy 网站上浏览容器启用角色

最近,Ansible Container 社区创建了容器应用程序的概念,这些应用程序(有时)用于部署构成应用堆栈的多个容器。我们将在本书后面讨论容器应用程序

Ansible Container NGINX 角色

在本章中,我们将学习如何利用在 Ansible Galaxy 上预写的 Ansible Container 角色,快速通过角色来部署基于容器的服务。Ansible Galaxy 的一个主要优点是,它使用户能够利用其他选择共享项目并将其以角色形式发布的用户的集体知识库。像许多 DevOps 工程师一样,你可能并不熟悉如何为每个可能的应用程序、框架或服务配置最佳的性能。像 Ansible Galaxy 这样的在线仓库有助于简化部署许多新应用程序的学习曲线,因为这些应用程序基本上可以开箱即用,用户几乎不需要任何输入。使用 Ansible Galaxy 角色的用户也可以选择自定义已有的角色,以适应他们的特定需求。在本章中,我们将使用官方的 Ansible Container NGINX 角色来构建并部署一个功能完整的 NGINX web 服务器容器。我们使用的角色链接如下:galaxy.ansible.com/ansible/nginx-container/

在开始安装和使用 NGINX 角色之前,让我们回顾一下 Ansible Container 的工作流程,以及它如何应用于预写角色:

  • ansible-container init:用于初始化一个新项目,以便使用我们的角色。

  • ansible-container build:生成我们将用于安装 NGINX 角色的指挥容器。build也用于在安装角色后构建容器镜像。

  • ansible-container install:利用指挥容器下载并在项目中安装我们的角色。

  • ansible-container run:在本地运行项目,以测试并验证 NGINX 服务器是否按预期运行。

  • ansible-container push:将构建的容器镜像推送到你的 Docker Hub 仓库。

在本章的任何时候,你都可以在 GitHub 仓库中查看完成的实验:github.com/aric49/ansible_container_lab/tree/master/AnsibleContainer/nginx_demo

在开始进行本实验之前,最好创建一个免费的 Docker Hub 账户,这样你就可以上传和分享你创建的容器。前往hub.docker.com创建一个免费账户。

启动新项目

到现在为止,你可能已经相当熟悉如何初始化一个新的 Ansible Container 项目,并使用 ansible-container init 命令自动生成文件和目录结构。从 Vagrant 主机上的一个新目录,运行 ansible-container init 来开始你的新项目,并确保所需的文件被自动生成:

ubuntu@node01:$ ansible-container init 
Ansible Container initialized.

一旦验证了新项目文件和目录框架已创建,我们需要运行一个初始的空构建以创建一个指挥容器。在 Ansible Container 安装角色或构建更复杂的项目之前,必须先在工作站上存在指挥容器,以便 Ansible Container 可以本地修改文件并下载允许容器角色正常工作的所需依赖项。现在我们已经初始化了项目,接下来让我们进行一次空构建,以便创建一个指挥容器:

ubuntu@node01:/vagrant/AnsibleContainer/nginx_webserver$ ansible-container build
Building Docker Engine context...
Starting Docker build of Ansible Container Conductor image (please be patient)...
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting.       project=nginx_webserver
All images successfully built.
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=1e8a3e0164cf617ad121c27b41dfcc782c0a2990eab54b70b687555726874e27 save_container=False

最佳实践是,始终使用与你构建项目容器时相同的基础镜像来构建指挥容器,以确保兼容性。如果你选择使用与默认的centos:7不同的基础镜像,你可能需要在构建项目之前修改container.yml文件。更多内容将在后续章节中介绍。

一旦项目构建完成,你应该看到All Images Successfully Builtcommand_rc=0的返回信息,表示 Ansible Container 指挥容器已成功构建。你可以使用docker images命令检查以确保指挥容器镜像已经构建并且存在于本地主机上。

更新版的 Ansible Container(1.0+)带有预构建的指挥容器镜像,使用这些镜像时无需在安装角色之前构建项目。然而,为了充分发挥 Ansible Container 工作流的优势,最好为你的项目构建专属的指挥容器镜像。

安装 NGINX 角色

现在我们已经初始化了一个新项目并构建了一个指挥容器,我们可以使用 ansible-container install 命令从 Ansible Galaxy 安装 NGINX 角色。这个命令的语法很简单:执行 ansible-container install,然后是项目所有者的用户名,在本例中为 ansible,接着是一个点号 . 和项目名称 nginx-container。你应该看到类似以下的输出:

ubuntu@node01:$ ansible-container install ansible.nginx-container
Parsing conductor CLI args.
- downloading role 'nginx-container', owned by ansible
- downloading role from https://github.com/ansible/nginx-container/archive/master.tar.gz
- extracting ansible.nginx-container to /tmp/tmpip0YiN/ansible.nginx-container
- ansible.nginx-container (master) was installed successfully
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=a9e6723de6f3a236dd7823dbd999b97a5e1917bcb6794f3b0e9cd4b6bb54433b save_container=False

完成后,你应该看到以下消息:

- ansible.nginx-container (master) was installed successfully

这表示角色已经成功从 Ansible Galaxy 和父 GitHub 仓库下载并安装。install 命令还对你项目目录中已经存在的 container.ymlrequirements.yml 文件进行了修改。如果你在文本编辑器中打开这些文件,你会发现角色已经被添加到了这些文件中:

requirements.yml

- src: ansible.nginx-container

container.yml

services:
  ansible.nginx-container:
    roles:
    - ansible.nginx-container

需要注意的是,容器角色已经将其自身添加到了 container.yml 中,并包含了角色作者希望我们使用的任何预填充信息。默认情况下,Ansible Container 会查看角色内部,使用 meta/main.ymlmeta/container.yml 文件中提供的默认信息,并将这些信息传递到构建过程中,除非在 container.yml 文件中进行了覆盖。稍后在本章中,我们将看到当我们稍微自定义 NGINX 角色的工作方式时,这一过程是如何运作的。

安装过程还将角色名 ansible.nginx-container 添加到了 requirements.yml 文件中。此文件用于跟踪项目中使用的 Ansible Galaxy 角色和其他依赖项。如果你将项目与其他开发者共享,而他们希望在本地构建项目,Ansible Container 会利用 requirements.yml 文件一次性安装所有依赖的角色。如果你在项目中使用了多个支持容器的角色,这将大大加速开发过程。

现在我们已经安装了支持容器的角色,让我们重新运行构建过程并构建我们的新容器镜像:

ubuntu@node01:$ ansible-container build
Building Docker Engine context...                                              
Starting Docker build of Ansible Container Conductor image (please be patient)...    
Parsing conductor CLI args.                                      
Docker™ daemon integration engine loaded. Build starting.       project=nginx_webserver   
Building service… project=nginx_webserver service=ansible.nginx-container                                                

PLAY [ansible.nginx-container] *************************************************                                  
TASK [Gathering Facts] *********************************************************                       ok:[ansible.nginx-container]                                                                                                                                     
TASK [ansible.nginx-container : Install epel-release] **************************                    
changed:[ansible.nginx-container]                                               

TASK [ansible.nginx-container : Install nginx] *********************************
changed: [ansible.nginx-container] => (item=[u'nginx', u'rsync'])                                                                      

TASK [ansible.nginx-container : Install dumb init]
*****************************
changed:[ansible.nginx-container]                                                       
TASK [ansible.nginx-container : Update nginx user]
*****************************                            
changed:[ansible.nginx-container]

TASK [ansible.nginx-container : Put nginx config]
******************************
changed: [ansible.nginx-container]

TASK [ansible.nginx-container : Create directories, if they don't exist]
******************************
changed: [ansible.nginx-container] => (item=/static)   
changed: [ansible.nginx-container] => (item=/run/nginx)
changed: [ansible.nginx-container] => (item=/var/log/nginx)  
changed: [ansible.nginx-container] => (item=/var/lib/nginx)  

TASK [ansible.nginx-container : Clear log files]
*******************************
ok: [ansible.nginx-container] => (item=access.log)
ok: [ansible.nginx-container] => (item=error.log)
................

PLAY RECAP *********************************************************************
ansible.nginx-container    : ok=18   changed=14   unreachable=0    failed=0

Applied role to service role=ansible.nginx-container service=ansible.nginx-container
Committed layer as image        image=sha256:e4416fbb0ba74f4d39a5b6522466f8c0087582de64298ac63bc43a73f577d85a service=ansible.nginx-container
Build complete. service=ansible.nginx-container
All images successfully built.
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=86b437e5e4ebca2d29ef89193be1bd7184b5bc9e8566305dbf470a8cd188ac7e save_container=False

看起来,我们的构建输出比之前的示例更有趣了。你可以看到,Ansible Container 已经识别出我们的项目现在有一个名为 ansible.nginx-container 的服务,并且在 container.yml 文件中执行了与之关联的 ansible.nginx-container 角色。在构建过程中,指挥者镜像会运行 Ansible Core,并传入位于角色中的 playbook 任务,以便将容器镜像带入所需的状态。从角色中执行的每个任务都会显示在构建输出中,这使得开发者能够准确看到容器内正在执行哪些操作。在检查 Ansible Container 构建输出时,以下是一些需要记住的关键点:

  • 已执行的任务:在 Ansible 中,每个任务都有一个唯一的名称,帮助使构建输出对于几乎任何人来说都易于阅读和理解。有时,逻辑条件未能正确触发,这可能会导致某些任务被跳过。阅读任务列表,确保你期待执行的任务确实被执行。

  • 已更改的任务与 OK 任务:由于 Ansible 本质上是一个配置管理工具,它紧密遵循幂等性原则。换句话说,如果 Ansible 发现某个任务不需要执行(因为容器已经处于期望的状态),它将把该任务标记为OK。当 Ansible 进行更改时,它会将任务标记为CHANGED,表示 Ansible 在基础容器镜像中修改了某些内容。需要注意的是,所有任务,无论是SKIPPEDCHANGED还是OK,都会在构建过程结束时被计为OK,表示在任务执行过程中没有发生失败。

  • PLAY RECAP:在每次 Ansible 容器构建结束时,你将看到一个PLAY RECAP部分,突出显示 Ansible 容器构建的状态。这提供了一个便捷的参考,快速显示 Ansible 容器执行的每个任务及其状态:OKChangedUnreachableFailed。失败的任务会导致构建过程在失败的任务处立即停止,除非在角色中另有覆盖。

一旦构建过程完成,Ansible 容器将这些更改作为一个单独的层提交到基础镜像中,创建一个全新的容器镜像供你的项目使用。记住,在第一章中,使用 Docker 构建容器时,我们是如何利用 Dockerfile 来构建容器镜像的?如果你还记得,Dockerfile 中的每一行都代表着容器镜像中的一层。使用 Dockerfile 构建复杂的容器镜像会很快产生大且难以管理的容器,这些容器的文件大小也可能很大。

使用 Ansible 容器时,我们可以通过在角色中添加任务进行任意更改,并且我们的最终容器镜像依然保持精简,因为它只创建了一个容器层:

图 2:通过 Ansible 容器构建的容器镜像中的层

然而,请记住,你仍然应该尽量通过仅添加最必要的文件、包和服务,保持 Ansible 容器构建的容器镜像尽可能小。如果容器镜像的单一层大小达到了 2GB,那么 Ansible 容器创建的这一层的优势很快就会被抵消!

运行 NGINX 角色

现在,既然我们的项目已经构建完成且角色已成功应用,我们可以使用ansible-container run命令来运行我们的容器。run将利用在构建过程中创建的本地 Ansible 部署剧本来启动容器,以便我们测试它并确保它按预期运行:

ubuntu@node01:$ ansible-container run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
Verifying service image service=ansible.nginx-container

PLAY [localhost] ***************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=e62dc0e401d3d76bf771c6e8db74fb0970e9d5e57be9ad6642cff92592248215 save_container=False

根据提供的PLAY RECAP,我们可以轻松地识别出,在我们的本地虚拟机上执行的任务使得容器的状态发生了变化,从而使容器进入了运行状态。docker ps -a的输出也显示我们的容器正在运行:

ubuntu@node01:~$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
f213412bd485  nginx_webserver.. "/usr/bin/dumb-ini..." 2 minutes ago Up 2 minutes  0.0.0.0:8000->8000/tcp  nginxwebserver_ansible..

默认情况下,此容器使用角色默认配置的主机和容器 TCP 端口:8000。让我们使用 curl 工具看看是否可以在 8000 端口访问 NGINX 默认网站:

ubuntu@node01:$ curl localhost:8000
.....(output truncated)
<title>Test Page for the Nginx HTTP Server on Fedora</title>

根据 curl 的输出,看来我们已经成功地在工作站上部署了 NGINX 角色,并且有一个功能正常的 NGINX 服务器容器在运行。如果你希望一个 Web 服务器在 8000 端口运行,并且只使用绝对默认配置,那么这非常棒。不幸的是,这对于任何人来说可能都不太理想。让我们通过覆盖一些默认值来修改我们的角色,看看能否获得一个运行环境中实际功能更接近的容器。

修改 NGINX 角色

Ansible 的功能和行为与许多配置管理平台(如 Chef、Puppet 或 Salt)有很大不同。角色被视为服务抽象,用户可以根据需要进行调整和修改,以便以几乎任何方式运行。Ansible 提供了变量和变量优先级的概念,这些变量可以来自多个来源,并且根据优先级的顺序,修改角色,使其根据角色本身的设计以不同方式运行。需要注意的是,角色变量优先级在 Ansible Core 中更为常见,用户可能会有需要在开发、预发布、QA 和生产环境中运行的 playbook,并且这些 playbook 根据部署的环境需要不同的配置。

理解如何在 Ansible Container 中重写角色变量和参数,以构建具有韧性和定制化的基础设施组件,仍然是非常重要的。Ansible 角色的设计方式使得可以在不修改角色本身的情况下覆盖角色变量。通过变量优先级的概念,Ansible Container 会自动识别 container.yml 文件中的角色变量值,并将这些值传递给角色,供 playbook 访问。这允许用户编写便于移植和可重复使用的代码,只需从 Ansible Galaxy 下载正确的角色,并使用包含所有自定义项的正确 container.yml 文件构建项目。当然,并非角色的每一部分都可以在 container.yml 文件中被覆盖,但我们将在本节中学习如何进行基本修改,并将我们的自定义容器镜像推送到 Docker Hub。

在使用 Ansible Galaxy 上由其他用户编写的角色时,一位优秀的 Ansible Container 工程师应该首先阅读 README 文件,通常该文件位于角色的根目录中。README 文件通常会提供有关如何以最基本的方式运行该角色的指南,并列出可以被重写的常用变量。深入理解 README 对于了解角色在更复杂项目中的整体功能至关重要。你可以在这里查看 NGINX 角色的 README:github.com/ansible/nginx-container/blob/master/README.md

随着你编写自己的 Ansible Container 角色和容器启用应用程序,拥有一个更新且准确的 README 文件将对其他用户尝试使用你的项目非常有帮助。务必更新你的 README!

在本次练习中,我们将自定义 container.yml 文件,使其暴露在主机端口 80 上,而不是默认的 8000,并且还传递一个新的路径作为文档根目录,用于提供网站服务。还应该注意,我们将服务名称从角色的名称更改为更常见的名称:webserver。最终的 container.yml 文件可以在书籍的 GitHub 仓库中的 AnsibleContainer/nginx_demo 目录中找到。

首先,修改 container.yml 文件,使其类似于以下内容,记住,我们传递的是重写变量 STATIC_ROOT,并将其作为我们为服务指定的角色的子参数。我们根据开发人员提供的角色 README 文件中的信息,确定 STATIC_ROOT 是一个可以重写的有效变量。本质上,这告诉 Ansible Container 使用用户提供的值,而不是角色中硬编码的默认值:

version: '2'
settings:
  conductor_base: centos:7

services:
  webserver:
    roles:
      - role: ansible.nginx-container
        STATIC_ROOT: /MySite
    ports:
      - "80:8000"

registries: {}

在重新构建我们的项目后,Ansible Container 会识别 container.yml 文件中的变化。这将提示 Ansible Container 重新运行该角色,使用更新后的 STATIC_ROOT 值。你会注意到,这一次,构建过程所需的时间会更短,并且与第一次执行构建时相比,变更的任务也会更少。你应该看到类似以下的输出,记住这个示例是经过截断的:

ubuntu@node01:$ ansible-container build
Building Docker Engine context...
Starting Docker build of Ansible Container Conductor image (please be patient)...
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting.       project=nginx_webserver
Building service...     project=nginx_webserver service=WebServer                                                               

PLAY [WebServer] ***************************************************************

运行修改后的角色

一旦构建完成,你可以执行 ansible-container run 命令,以确保我们的 NGINX 容器仍按预期运行:

buntu@node01:$ ansible-container run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
Verifying service image service=WebServer

PLAY [localhost] ***************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=b97bbb161e1a735891cacbbf1eae263c8947cf16d55480568aee8debe7763e17 save_container=False

从前面的示例中可以看出,运行过程如预期完成,显示消息All services running. Conductor Terminated. Cleaning Up,并且返回的相关零值代码。这表示我们的容器正在按预期运行。我们可以在本地 Docker 环境中验证这一点,再次使用docker ps -a命令。在此示例中,我们可以看到容器的端口8000映射到主机的端口80,这表明我们在container.yml文件中的更改已正确构建到项目的新迭代中:

ubuntu@node01:$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
5d979fc13cad  nginx_webserver-webserver:20170802144124 "/usr/bin/dumb-ini…"  3 minutes ago  Up 3 minutes 0.0.0.0:80 -> 8000/tcp  nginxwebserver_WebServer_1

为了确保我们的 NGINX 服务器按预期工作,我们可以使用可靠的curl命令,确保我们在虚拟机本地主机端口80上收到预期的响应:

ubuntu@node01:/vagrant/AnsibleContainer/nginx_webserver$ curl localhost:80
.....
<title>Test Page for the Nginx HTTP Server on Fedora</title>

恭喜!通过利用 Ansible Galaxy 中的社区角色,你已经成功构建了一个功能正常的 NGINX 服务器容器!我们甚至通过向角色中传递自己的参数来稍微定制了该角色,从而微调了角色的功能和生成的容器。不幸的是,我们在容器中所做的工作对在本地工作站上运行的容器没有太大帮助。构建容器的一个主要好处是,我们可以将构建的容器上传到镜像仓库,供其他用户部署和使用。为此,我们将学习使用ansible-container push命令,将我们的 NGINX 镜像推送到我们在本章开头创建的免费 Docker Hub 仓库,以供他人使用和下载。

将项目推送到 Docker Hub

为了启用此功能,我们将通过移除registries部分后的花括号来激活container.yml文件的最后部分。在registries部分下,我们将创建一个名为 docker 的子部分,该子部分有两个主要参数:URL 和 namespace。对于此示例,由于我们使用的是 Docker Hub 注册表,我们将提供 Docker Hub 的公共 API URL(截至写作时)和我们在本章开头创建的用户名作为 namespace 参数。你container.yml文件中的 registries 部分应该如下所示:

registries:
  docker:
    url: https://index.docker.io/v1/
    namespace: username

还需要注意的是,你可以在container.yml文件中为你的注册表命名任何你想要的名字。在这个示例中,由于我们使用的是 Docker Hub,我使用的名字是:docker。如果你使用的是内部或私有注册表,你可以提供任何合适的名称。例如,My_Corporate_Registry可能是一个合适的名称,用于你公司托管的内部镜像注册表。你甚至可以列出多个注册表,只要它们的名字不重复即可。

还需要注意的是,registries部分是container.yml文件中的完全可选部分。默认情况下,如果在container.ymlregistries部分没有写入任何条目,ansible-container push命令将推送到 Docker Hub。所需的唯一操作是用户在ansible-container push命令中提供--username标志。

以下示例演示了我将项目上传到个人镜像仓库,并提供了我的用户名:aric49。接着,Ansible Container 会提示你输入 Docker Hub 的密码,并将容器镜像推送到你的免费仓库,如图所示。Ansible Container 将自动根据container.yml文件中的服务名称为你的容器命名。

ubuntu@node01:$ ansible-container push --username aric49 --tag 1.0
Enter password for aric49 at Docker Hub:
Parsing conductor CLI args.
Engine integration loaded. Preparing push.      engine=Docker™ daemon
Tagging aric49/nginx_webserver-webserver
Pushing aric49/nginx_webserver-webserver:1.0...
The push refers to a repository [docker.io/aric49/nginx_webserver-webserver]
Preparing
Layer already exists
1.0: digest: sha256:e0d93e16fd1ec9432ab0024653e3781ded3b1ac32ed6386677447637fcd2d3ea size: 741
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=6685c1596e1da31b90b2553a84c23e112c84eeb27573e33a6c5ef7389df58f56 save_container=False

push命令中始终提供--tag标志非常重要。这可以确保你在未来对不同版本的容器镜像进行版本控制。在这个示例中,我们正在上传版本 1.0 的容器镜像。如果你将来对项目做出更改,可以上传版本 2.0 标签,镜像仓库将自动保持旧版本 1.0,以防你需要回滚或升级到项目的另一个版本。

在本次演示中,我们不会使用默认的推送行为将镜像上传到 Docker Hub,而是将容器镜像上传到我们在container.yml文件中指定的镜像仓库,而这个镜像仓库恰好也是 Docker Hub。我们可以使用--push-to标志来指定我们在项目中配置的镜像仓库名称,提供用户名和镜像标签信息,正如之前的示例所示:

ansible-container push --username username --push-to docker --tag 1.0

一旦容器上传到我们选择的镜像仓库,我们可以手动执行docker pull命令从镜像仓库下载容器。默认情况下,docker pull要求用户提供容器镜像仓库的名称、镜像的名称以及你想要拉取的标签版本。在使用 Docker Hub 时,我们将使用你的用户名作为镜像仓库,因为我们使用的是个人的 Docker Hub 账户。例如,你可以使用docker pull命令拉取我的 NGINX 网页服务器镜像:

ubuntu@node01:~$ docker -D pull aric49/nginx_demo-webserver:1.0
1.0: Pulling from aric49/nginx_demo-webserver
e6e5bfbc38e5: Pull complete
51c9be88e17b: Pull complete
Digest: sha256:e0d93e16fd1ec9432ab0024653e3781ded3b1ac32ed6386677447637fcd2d3ea
Status: Downloaded newer image for aric49/nginx_demo-webserver:1.0

使用-D标志来启用调试模式。这样可以让你看到更多关于 Docker 镜像拉取过程的详细信息。

从前面的输出中可以看到,我们拉取的镜像只有两层。这是因为 Ansible Container 将所有的 playbook 运行作为容器镜像中的单层进行提交。这使得开发者能够构建一个相当复杂的容器,同时最大限度地减少生成镜像的大小。只要记住保持 playbook 小巧高效,否则你将失去容器化微服务架构的优势。

现在,由于我们的镜像已被缓存到本地,我们可以使用 Docker 手动运行容器。当然,我们也可以直接使用 Ansible Container 运行项目,但本例的目的是演示如何直接在 Docker 中运行容器,这可能模拟一些你不能或不愿意安装 Ansible Container 的环境。这个方法的唯一注意事项是,你需要手动指定端口转发,因为这个配置是我们container.yml文件的一部分,并没有内嵌到镜像本身。在本例中,我们将以Ansible_Nginx为容器名称,并按照以下格式指定容器镜像:username/containername:tag

docker run -d -p 80:8000 --name Ansible_Nginx aric49/nginx_demo-webserver:1.0

docker ps -a的输出应该显示容器正在运行并且功能正常:

ubuntu@node01:$ sudo docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
6061f0249930 aric49/nginx_demo-webserver:1.0 "/usr/bin/dumb-init n" 8 seconds ago Up 7 seconds 0.0.0.0:80->8000/tcp Ansible_Nginx

在通过 Docker 手动运行容器之前,你可能需要先运行 Ansible Container 的destroy命令,因为端口80可能已被你正在运行的项目容器占用。

总结

在本章节中,你已经学习了 Ansible Container 核心概念之一:使用角色构建容器镜像。通过利用 Ansible 角色来创建容器镜像,你可以确保构建出的容器镜像具备生产级、可靠的容器服务所需的精确配置。此外,这也确保容器镜像是通过与你的基础设施已经在使用的几乎相同的 playbook 角色构建的,这使得容器服务的构建可以确保当前在生产环境中运行的服务能够轻松复制,并且通常几乎不需要重新工作。Ansible Container 为裸机或虚拟化的应用部署与容器化服务之间提供了一个出色的适配层。通过利用 Ansible Galaxy,你甚至可以下载并分享你自己或其他 Ansible Container 社区成员构建的自定义容器启用角色。

然而,正如本章节前面提到的那样,现有的 Ansible 角色不能直接 1:1 地移植到容器启用的角色,因为容器与传统基础设施的工作方式非常不同。在下一章中,我们将学习如何编写自定义的 Ansible 容器启用角色,以及将现有角色迁移到 Ansible Container 的一些最佳实践。准备好你的文本编辑器,我们即将开始写代码!

第四章:一个 role 里有什么?

在第三章,Your First Ansible Container Project,我们学习了关于 Ansible Container roles 的基础知识,它们的功能及如何从 Ansible Galaxy 下载、安装和调整它们。在本章中,我们将学习如何编写我们自己的 Ansible Container roles,用于从头开始构建自定义的容器镜像。您将了解到,Ansible 提供了一种易于学习、表达力强的语言,用于定义所需的状态和服务配置。为了说明 Ansible Container 如何快速构建服务和运行容器,本章的过程中我们将编写一个 role,用于构建一个可以在您的本地工作站上运行的 MariaDB MySQL 容器。在本章中,我们将涵盖以下内容:

  • 使用 Ansible Container 的自定义 roles

  • MariaDB 的简要概述

  • 初始化一个 Ansible Container role

  • 一个容器化 role 里有什么?

  • 创建 MariaDB 项目和 role

  • 编写一个容器化 role

  • 自定义一个容器化 role

使用 Ansible Container 的自定义 roles

本书内容逐渐深入时的一个主题是 Ansible Container 给予您多大的自由,可以快速、高效、安全和可靠地构建和部署自定义的容器镜像。到目前为止,我们已经学习了使用 Ansible Container 来定义和运行来自预构建社区容器的服务,以及利用社区编写的 roles 来实例化、构建和定制我们的容器。这是开始使用 Ansible Container 并熟悉 Ansible Container 工作流程的绝佳方式。然而,当您开始编写构建自定义容器镜像的 roles 时,Ansible Container 的真正力量开始显现出来。

如果您有使用 Ansible 作为配置管理工具的经验,您可能已经熟悉了编写 Ansible playbook 和 roles。这肯定会让您在编写容器化 roles 方面有一定的优势,但这并不是本章示例工作的先决条件。为了让每个人都处于同一起跑线上,我假设您没有编写 Ansible playbook 或 roles 的经验,我们将从头开始。对于那些 Ansible 老手来说,这些内容很大程度上会是回顾,但希望您也能学到一些新东西。对于 Ansible 初学者,我希望本章能激发您的好奇心,不仅仅是进一步构建更高级的 Ansible Container roles,还要深入探索 Ansible Core 配置管理概念。

Ansible 的最初动机是创建一个配置管理和编排系统,使得几乎任何人都能轻松上手并开始使用。Ansible 很快在软件开发人员、系统管理员和 DevOps 工程师中变得极为流行,因为它不仅易于采用,而且易于定制,甚至能够集成到现有的平台和配置管理工具中。我第一次开始使用 Ansible 是因为当时我在做的项目需要我登录到大量的裸机服务器和虚拟机上,反复执行同一组命令。当时,我试图通过编写不稳定的 shell 脚本来简化这一过程,这些脚本会使用 SSH 将远程命令推送到服务器。通过研究如何使这些脚本更加可靠,我发现了 Ansible,并立即采用了它,它使我的工作变得比我想象的更加简单和可靠。我相信,Ansible 在 IT 行业如此受欢迎的原因有两个主要方面:

  • 易于理解的 YAML 语法,适用于 playbook 和角色。YAML 易于学习和编写,非常适合 Ansible。

  • 数百个,如果不是成千上万的,Ansible Core 内置模块。这些模块使我们能够开箱即用地做几乎任何你能想象的事情。

让我们来看看 Ansible 的这两个独特方面,理解我们如何在自己的项目中利用这种易用性。

YAML 语法

YAML 是一种数据序列化格式,其递归地代表了 YAML Ain't Markup Language。你可能以前使用过其他序列化格式,例如 XML 或 JSON。YAML 的独特之处在于它易于编写,而且可能是当前最人类可读的数据格式。Ansible 选择使用 YAML 作为定义其 playbook 语法和语言的基础,原因是即使你没有编程背景,YAML 也非常容易入门,编写、使用和理解都很简单。YAML 的独特之处在于它通过使用一系列冒号(:)、破折号(-)和缩进(空格,不是制表符)来定义键值对。这些键值对可以用来定义几乎所有类型的计算机科学数据类型,例如整数、布尔值、字符串、数组和哈希表。以下是一个 YAML 文档的示例,展示了这些构造的一些用法:

---
#This is a Comment in a YAML document. Notice the YAML document starts with a series of three dashes: ---

MyString: "This is a string"

MyArray:
  - "Item1"
  - "Item2"

MyBoolean: true

MyInteger: 10

MyHashTable:
  KeyOne: "ValueOne"
  KeyTwo: "ValueTwo"
  KeyThree: "ValueThree"

上面的示例展示了一个简单的 YAML 文件,包含最基本的构造:一个字符串变量,一个数组(项目列表),一个布尔值(真/假)变量,一个整数变量,以及一个包含一系列键值对的哈希表。这可能看起来与我们在本书中修改 container.yml 文件以及 Docker Compose 文件时做的工作非常相似。这些格式也是定义在 YAML 中,并且包含许多相同的构造。

在前面的例子中,我想要提醒您一些事情(当您开始编写 Ansible playbook 和角色时,也应该注意到这些):

  • 所有 YAML 文档都以三个破折号开头:---。这很重要,因为您可以在同一个文件中定义多个 YAML 文档。文档之间用三个破折号分隔。

  • 注释使用井号符号#定义。

  • 字符串被引号包围。这将字符串与字面值(例如没有引号的布尔值(true 或 false)或整数(没有引号的数字))分开。如果您将单词true/false或数值用引号包围起来,它们将被解释为字符串。

  • 冒号(:)用于分隔键值对,几乎定义了所有内容。

  • 缩进用两个空格表示。在 YAML 格式中不识别制表符。开始编写 YAML 文档时,请确保您的文本编辑器配置为在按下Tab键时将两个空格插入到文档中。这样可以在输入时快速自然地缩进文本。

我意识到 YAML 语法比我在这个示例中提供的要多得多。我在这里的目标是比早期章节更深入地挖掘,以帮助读者更深入地理解 YAML 格式。这绝不是整个 YAML 格式的完整描述。如果您想了解更多关于 YAML 的信息,我建议您访问官方 YAML 规范网站:yaml.org

Ansible 模块

使得 Ansible 如此受欢迎且易于使用的第二部分是 Ansible 可以利用的大量模块,这些模块可以做用户能想到的几乎任何事情。将模块视为 Ansible 的构建块,定义您的 playbook 的操作。Ansible 模块可以编辑远程系统上文件的内容,添加或删除用户,安装服务包,甚至与远程应用程序的 API 进行交互。模块本身是用 Python 编写的,并以脚本格式从 YAML playbook 中调用。Playbook 本身只是对 Ansible 模块的一系列调用,执行特定的任务序列。让我们看一个非常简单的 playbook,以了解这在实践中是如何工作的:

---
- name: Create User Account
  user:
    name: MyUser
    state: present

- name: Install Vim text editor
  apt:
    name: vim
    state: present

这个简单的 playbook 由两个独立的任务组成:创建一个用户账户和安装 Vim 文本编辑器。Ansible 中的每个任务都调用恰好一个模块来执行操作。Ansible 中的任务使用 YAML 中的短横线来定义,接着是任务的名称、模块的名称以及所有要传递给该模块的参数,缩进格式如下所示。在我们的第一个任务中,我们通过调用 user 模块来创建一个用户账户。我们为 user 模块提供了两个参数:namestatename表示我们要创建的用户的名称,而 state表示我们希望远程系统或容器中的用户状态。此时,我们希望存在一个名为 MyUser 的用户,且该用户的状态为 present。如果这个 Ansible playbook 执行时,名为 MyUser 的用户已经存在,Ansible 将不会采取任何操作,因为系统已经处于期望状态。

本手册中的第二个任务是在我们的远程系统或容器上安装文本编辑器 Vim。为此,我们将使用apt模块来安装一个 Debian APT 包。如果这是一个 Red Hat 或 CentOS 系统,我们也会使用yumdnf模块。name表示我们要安装的包的名称,state表示服务器或容器的期望状态。因此,我们希望 Vim Debian 包能够被安装。如前所述,Ansible 有成百上千的模块可以在 playbook 和角色中使用。你可以在 Ansible 文档中找到按类别组织的完整模块列表,以及这些模块所需参数的优秀示例,网址为docs.ansible.com/ansible/latest/modules_by_category.html

state 参数还可以取 absent 值,用于删除用户、包或几乎任何可以定义的东西。

Ansible Container 的一个主要优势是,在编写容器配置时使用 Ansible 角色,你可以使用所有可用的 Ansible 模块。不幸的是,并不是所有的 Ansible 模块都能在容器环境中工作。一个典型的例子是管理运行中服务状态的模块,比如 service 模块。service 模块在容器中无法运行,因为应用容器通常缺少传统的初始化系统(你会在完整操作系统中找到它们),这些系统负责启动、停止和重启运行中的服务。在容器化的环境中,这个过程通过使用 CMDentrypoint 语句直接执行服务二进制文件来处理。

此外,几乎所有管理云服务编排或调用外部 API 的模块都无法在容器化环境中运行。这一点非常直接,因为在构建一个独立的容器化微服务时,通常不会想要编排外部服务的状态。当然,如果你正在编写一个 Ansible 剧本来部署一个之前使用 Ansible Container 构建的容器化应用,你可以使用这些编排模块来在容器上线时作出响应。不过,在本章中,我们将仅限于讨论编写构建容器化服务的角色。

MariaDB 简要概述

在本章中,我们将编写一个 Ansible 角色来构建 MariaDB 数据库容器。MariaDB 是 MySQL 关系数据库服务器的一个分支,提供了许多 MySQL 中没有的自定义功能和优化。开箱即用,MariaDB 支持多种优化,如复制、查询优化、加密、性能和速度上的提升,相比标准的 MySQL 更具优势,但仍然完全兼容 MySQL,采用免费开源的 GPL 许可证。选择 MariaDB 作为示例,是因为它相对容易部署,而且该应用本身是免费的。在本章中,我们将构建一个相对基础的单节点 MariaDB 安装,这个安装不包含很多你在生产环境安装中会找到的功能和性能优化。本文的目的不是教你如何构建一个生产就绪的 MariaDB 容器,而是通过 Ansible Container 构建容器化服务的概念。如果你想进一步拓展这个示例,可以根据自己的需求调整这段代码。对那些能够构建生产就绪容器的朋友,将会额外加分!

初始化一个 Ansible Container 角色

如前所述,Ansible 角色是一个自包含的、可重用的一组剧本、模板、变量和其他元数据,用于定义一个应用或服务。由于 Ansible 角色是为与 Ansible Galaxy 配合使用而设计的,Ansible Galaxy 命令行工具具有内置功能,可以初始化包含所有正确目录、默认文件和结构的角色,从而创建一个功能齐全的 Ansible 角色,且无需过多的麻烦。这与 ansible container init 命令创建 Ansible Container 项目时的工作方式非常相似。

容器启用角色中包含什么?

为了在 Ansible Container 中创建一个新的容器启用角色,我们将使用带有 container-enabled 标志的 ansible-galaxy init 命令来为我们创建新的角色目录结构。为了了解当我们使用这个命令时发生了什么,我们将在我们的 Vagrant 虚拟机的 /tmp 目录中初始化一个角色,看看 Ansible 为我们创建了什么:

ubuntu@node01:/tmp$ ansible-galaxy init MyRole --container-enabled
- MyRole was created successfully

在成功执行 init 命令后,Ansible 应返回一条消息,指示你的新角色已 创建成功。如果你运行 ls 命令,你会看到一个新目录,目录名就是我们刚刚初始化的角色的名称。根据默认的目录结构,角色的所有组成部分都位于此目录中。当你从 Ansible 调用一个角色时,Ansible 会查找你指定的角色所在的所有位置,并查找一个与角色同名的目录。稍后在本章中,我们将更详细地了解这一点。如果你进入该目录,你将看到类似于以下的文件夹结构:

MyRole/
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── meta
│   ├── container.yml
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── ansible.cfg
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml

让我们看一下这些目录和文件的作用:

  • defaults/defaults 是一个包含特定于你角色的变量的目录,并且在覆盖值时具有最低优先级。如果你希望将一些变量放入角色中,并且希望用户一定要重写这些变量,则应将它们放在该目录的 main.yml 文件中。这与 vars/ 目录不同,不要混淆。

  • handlers/handlers 是 Ansible 中的一个概念,定义了在角色执行过程中,响应其他任务发送的通知事件时应执行的任务。例如,你可能有一个任务用于更新角色中的配置文件。如果服务需要因该配置文件更新而重启,你可以在任务中指定一个 notify: 步骤,并指定处理程序的名称。如果父任务执行并且状态为 CHANGED,Ansible 会在 handlers/ 目录中查找由通知语句指定的任务,并执行该任务。请注意,除非另一个 playbook 任务通过 notify: 语句明确调用该任务并且导致状态改变,否则 handlers 不会执行。由于容器通常不依赖于外部事件和情况,handlers 在启用容器的角色中并不常见。

  • metaMeta 是一个包含 Ansible 角色元数据的目录。在启用容器的角色中,它包含两个主要文件:main.yml,该文件包含关于角色的一般元数据,例如依赖关系、Ansible Galaxy 数据以及角色所依赖的条件。为了这个示例的目的,我们不会对这个文件做太多处理。第二个文件,container.yml,对我们来说更为重要。这个 container.yml 文件是专门针对启用容器的角色的,它对于指定当我们调用角色时将被注入到项目级 container.yml 文件中的默认值至关重要。在这里,我们可以指定容器镜像、卷信息,以及我们希望容器默认运行的命令和 entrypoint 数据。如果我们愿意,所有这些数据都可以在父级 container.yml 文件中覆盖。

  • taskstasks 目录是我们指定实际在容器中执行的任务,并构建服务的地方。默认情况下,Ansible 会执行 main.yml 文件,并按照指定的顺序执行所有任务。任何其他的任务文件也可以放入此目录,并可以通过在 main.yml 文件中使用 include: 语句来执行。

  • templatestemplates 目录存储我们希望在角色中使用的配置文件模板。由于 Ansible 是基于 Python 的,它使用 Jinja2 模板引擎将配置模板放入容器,并根据 defaults/vars/ 目录中识别的变量更新值。此目录中的所有文件应该具有 .j2 文件扩展名,尽管这并不是强制要求的。

  • tests:任何您希望 CICD 工具执行的自动化测试都应该放在这里。通常,开发者会将任何自定义的 Ansible 配置、参数或清单文件放入这个目录中,以供 CI/CD 工具作为输入使用,这些文件通常是自动生成的。

  • varsvars/ 目录是开发者可以在此指定其他可用于角色的变量的位置。需要注意的是,vars/ 目录的优先级低于 defaults/ 目录,因此在这里定义的变量比在 defaults 中指定的变量更难覆盖。通常,当我编写角色时,我会将所有变量都放在 defaults 目录中,因为我希望用户能够完全覆盖他们想要的任何内容。但在某些情况下,您可能不希望让变量太容易被访问,这时它们可以被指定在 vars/ 目录中。

任何在您的角色中名为 main.yml 的文件都表示该文件是默认文件,并会被自动执行。

现在我们知道了容器启用角色的组成部分,我们可以利用这些知识创建一个新的 Ansible 容器项目,来构建我们的 MariaDB MySQL 角色。为此,我们将初始化一个新项目,并创建一个名为roles/的子目录,里面将包含我们要创建的角色。当我们构建项目时,Ansible 会知道去我们的roles/目录中查找我们指定并创建的所有角色。请注意,本章节的以下部分会包含大量代码。为了让跟随过程更容易,完整的示例可以在官方书籍的 GitHub 仓库中找到,位于AnsibleContainer/mariadb_demo目录下。然而,学习如何编写 Ansible 代码的最佳方式是通过重复和实践,而这一点只有通过自己编写代码才能实现。强烈建议,尽管逐字复制本章编写的代码可能不太实际,但应该通过使用这些示例创建自己的项目或修改 Git 仓库中的示例来练习编写 Ansible 代码。你写的代码越多,成为更好、更流利的 Ansible 开发者的机会就越大。

初始化 MariaDB 项目和角色

现在我们已经了解了容器启用角色的结构,我们可以通过初始化一个新的 Ansible 容器项目来启动我们的 MariaDB 容器。在 Vagrant 主机上的新目录中,像往常一样使用ansible-container init命令启动一个新项目:

ubuntu@node01:$ ansible-container init 
Ansible Container initialized.

在我们的project目录中,我们可以创建一个目录来存储我们的角色。在 Ansible 核心中,角色的默认位置是在/etc/ansible/roles或相对于你正在执行的 playbook 的roles/目录中。然而,需要注意的是,角色可以存储在任何位置,只要 Ansible 安装能够读取该路径。为了本示范,我们将把角色路径创建为项目的目录。在我们的project目录中,创建一个名为roles的新目录,并在该目录中初始化我们的 Ansible 容器角色。我们将把我们的角色命名为mariadb_role

ubuntu@node01:$ mkdir roles/
ubuntu@node01:$ cd roles/
ubuntu@node01:roles$ ansible-galaxy init mariadb_role --container-enabled
- mariadb_role was created successfully

现在我们的角色已经在项目中创建,我们需要修改项目中的container.yml文件,让它知道我们从哪里加载角色的路径,并创建一个我们将使用角色构建的服务。角色的位置可以通过在container.yml文件的settings:下设置roles_path选项来指定。这里,我们可以使用短横线表示法(-)将 Ansible 需要查找角色的路径列出为roles_path的列表项。我们将指定刚才创建的roles目录。在services:子部分下,我们可以创建一个名为MySQL_database_container的新服务。该服务将利用我们刚刚创建的mariadb_role角色。我们还需要确保指定我们希望用于服务的基准镜像。在本示例中,MariaDB 容器将基于 Ubuntu 16.04,因此我们需要确保我们的conductor_base镜像与之相同,以确保兼容性。

container.yml

以下是提供这些设置的container.yml文件示例:

version: "2"
settings:
  conductor_base: ubuntu:16.04
  roles_path:
    - ./roles/

services:
services:
  MySQL_database_container:
    roles:
      - role: mariadb_role
registries: {}

到这时,我们可以构建我们的项目,但这将导致一个空容器,因为我们的角色没有任何任务可以用来构建容器镜像。让我们通过添加任务并更新角色特定的container.yml文件来使事情变得有趣。

始终记得使用与服务容器相同的主机基准镜像。这将确保在构建项目时最大程度的兼容性。

编写一个支持容器的角色

正如我们之前讨论过的,由于文件路径很容易变得复杂,导致很容易迷失在其中,因此从零开始编写代码并引导读者是相当困难的。在本节中,我将展示修改后的文件内容,并引导读者关注需要解释的文件部分。由于很容易迷失,我会指导读者前往以下网址的官方书籍 GitHub 仓库进行跟随:github.com/aric49/ansible_container_lab/tree/master/AnsibleContainer/mariadb_demo

作为一个容器启用的角色的开发者,编写角色最重要的部分是角色特定的container.yml文件,该文件指定了容器运行时的默认值,以及用于构建容器并将所有组件放置到位的任务。你用来构建容器的任务通常会决定角色特定的container.yml文件中的参数。在编写角色时,开发者通常会在编写 playbook 任务时调整和修改container.yml文件。当你从项目特定的container.yml文件中调用一个角色时,角色特定的container.yml文件的内容将被用来构建你的容器。在任何时候,开发者都可以通过简单地修改项目container.yml中的参数来覆盖角色特定的container.yml文件。

作为角色开发者,为你的角色编写合理的默认值在container.yml中是很重要的,这样可以帮助其他用户迅速使用你的角色。对于我们的 MariaDB 示例,我们将创建一个简单的角色特定container.yml文件,其内容如下所示:

roles/mariadb_role/meta/container.yml

这个文件位于 MariaDB 角色的meta目录中:

from: ubuntu:16.04
ports:
  - "3306:3306"
entrypoint: ['/usr/bin/dumb-init']
command: ['/usr/sbin/MySQLd']

我们在角色特定的container.yml中定义的参数应该立即引起你的注意,其方式与在项目特定的container.yml服务部分中定义参数是完全一致的。在这里,我们使用的是 Ubuntu 16.04 基础镜像,它与我们的 conductor 容器相同。为了使 MySQL 服务能够被外部用户访问,我们将会把主机上的 MySQL 端口 3306 映射到容器内的 3306 端口。最后,我们将指定一个默认的入口点和命令,当容器启动时应该运行这些命令。最近,容器开发人员中常见的做法是利用轻量级的初始化系统来启动和管理容器内的进程。一个流行的容器初始化系统是 dumb-init,它由 Yelp 在 2013 年编写,目的是提供一个易于安装的轻量级初始化二进制文件,用于管理容器内的进程。dumb-init 本质上作为 PID 1 在容器内启动,并将容器服务可执行文件作为参数传递给它。这样做的好处是,由于 dumb-init 作为 PID 1 运行,所有内核信号将首先被 dumb-init 拦截并转发给容器服务(mysqld)。如果容器被突然停止或不规范地重启,dumb-init 还将为我们的子进程提供回收服务。请记住,使用容器 init 系统并不是构建容器的要求,但在某些情况下,它有助于在进程没有正常退出时,运行、停止和重启容器。在这个示例中,我们将使用 dumb-init 作为容器的 entrypoint,并使用 command: 参数将 /usr/sbin/MySQLd 命令作为参数传递给它。这样,mysqld 进程将在 dumb-init 的监督下启动,dumb-init 将拦截所有 POSIX 信号并转发给 mysqld

容器化角色的第二个最重要的方面是实际编写在我们的基础镜像中执行的任务,以创建项目容器。所有任务都是位于角色的 tasks 目录中的 YAML 文件。每个任务都有一个名称,并调用一个单一的 Ansible 模块,使用参数执行容器中的一项工作。虽然对任务命名或排列顺序没有严格的要求,但你确实需要考虑到 playbook 的流程,特别是与其他任务的依赖关系,这些任务可能会在某些步骤之前或之后执行。你还需要确保任务的命名方式,使得任何查看构建过程的用户,即使他们不是 Ansible 开发人员,也能大致了解发生了什么。命名任务是 Ansible 被誉为 自文档化 的原因之一。这意味着,当你编写代码时,代码基本上会自动进行文档化,因为几乎任何阅读你代码的用户,凭借任务命名,都会立刻明白它的作用。还应注意的是,由于所有服务和应用程序都不同,它们的部署和配置方式也不同。使用 Ansible 和容器化技术,没有一种 一刀切 的方法能够充分捕捉部署和配置应用程序的最佳实践。Ansible 的一个优势是,它提供了所需的工具,能够自动化几乎任何人能想到的配置,以确保应用程序能可靠地构建和部署。以下是我们正在编写的 MariaDB 角色中 tasks/main.yml 文件的内容。花一点时间阅读当前的 playbook;我们将逐个任务进行解析,详细说明 playbook 是如何运行的。在我描述 playbook 的工作原理时,回顾每个任务的内容会对你理解描述有帮助。

tasks/main.yml

此文件位于 roles/mariadb_role/tasks/main.yml

---
- name: Install Base Packages
  apt:
    name: "{{ item }}"
    state: present
    update_cache: true
  with_items:
    - "ca-certificates"
    - "apt-utils"

- name: Install dumb-init for container init system
  get_url:
    url: https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64
    dest: /usr/bin/dumb-init
    owner: root
    group: root
    mode: 0775

- name: Create MySQL Group
  group:
    name: mysql
    state: present

- name: Create MySQL Users
  user:
    name: mysql
    state: present
    groups: mysql
    append: true

- name: Install MySQL Server
  apt:
    name: mariadb-server
    state: present
    update_cache: true

- name: Change Permissions on directories
  file:
    path: "{{ item }}"
    owner: mysql
    group: mysql
    mode: 0777
    state: directory
    recurse: true
  with_items:
    - "/etc/mysql/"
    - "/var/lib/mysql/"
    - "/var/run/mysql/"

- name: Remove my.cnf
  file:
    path: /etc/mysql/my.cnf
    state: absent

- name: Install MySQL Configuration File
  template:
    src: my.cnf.j2
    dest: /etc/mysql/my.cnf
    owner: mysql
    group: mysql

- name: Initialize Database
  include: initialize_database.yml
  when:
    - initialize_database == true

任务细分(main.yml)

  • 安装基础包:由于我们正在构建一个 Ubuntu 16.04 镜像,安装基础包 任务调用 apt 包管理模块来安装两个包:ca-certificatesapt-utils,它们是后续任务所必需的。我们希望这些包的状态在 container 中是存在的,并且 apt 数据库缓存已更新,以便在安装这些包之前使用。我们可以使用 with_items 操作符来安装多个包。with_items 会遍历指定的列表项,并通过 apt 模块运行每个值。使用 with_items 后,我们不需要创建两个或更多独立的任务来重复执行相同的操作。在任务的 name 部分,我们指定了 {{ item }},这是一个 Jinja2 关键字变量,它告诉 Ansible 即将遍历一个列表。

  • 为容器初始化系统安装 dumb-init:此任务利用get_url模块从互联网下载远程文件到容器中。在这个特定的情况下,我们下载dumb-init二进制文件,并将其放置在容器中的目标位置/usr/bin/dumb-init。我们更改文件权限,使其归 root 所有,属于 root 组,并且可以执行。Ansible 允许我们通过一次模块调用执行所有这些操作。

  • 创建 MySQL 用户和组:接下来的两个任务非常相似。通过这些任务,我们开始为安装 MariaDB MySQL 服务铺设基础。我们调用usergroup模块创建一个名为mysql的用户,创建一个同名的组,并将用户添加到该组中。请注意,在组模块中,我们指定了append: true。这意味着我们希望将mysql组附加到mysql用户已经分配的任何组中。这个选项是添加到任何组声明中的安全选择,以确保我们不会意外地将用户从他们可能需要加入的其他组中移除。

  • 安装 MySQL 服务器:此任务的功能与我们在剧本中看到的第一个任务非常相似。然而,我们并不是安装多个软件包,而是调用 apt 模块只安装一个软件包——MariaDB 服务器。像往常一样,我们希望软件包已安装并存在,并且更新软件包缓存。可以说,我们本可以在第一个任务中将该软件包添加到已安装软件包列表中,这也会起作用。然而,从开发风格上讲,我更喜欢在剧本的步骤之间提供逻辑上的区分,以免之后的操作变得混乱。毕竟,安装基础软件包通常与安装核心服务包是两个不同且独立的步骤。

  • 更改目录权限:此任务是剧本中较为复杂的任务之一。在这个任务中,我们需要更改一些目录路径的权限,以便 MySQL 服务能够向其中写入数据。file模块允许我们创建、删除或修改容器中任何文件。类似于第一个任务,我们将调用文件模块对{{ item }}关键字变量进行操作,以便在with_items中指定的每个列表项都应用相同的权限和属性。如果指定的路径不存在,Ansible 将以directory状态创建这些路径,并应用适当的权限。我们还提供了recurse: true选项,这样权限将应用到指定位置的所有子目录。

  • 移除 my.cnfmy.cnf是 MySQL 用来配置数据库服务操作的主要配置文件。当 MariaDB 首次安装时,它会创建一个指向其他配置文件的my.cnf符号链接。我们不希望出现这种行为,因此我们将使用文件模块删除默认的my.cnf文件,并将状态值设置为absent。我们将使用自己的my.cnf文件。

  • 安装 MySQL 配置文件:现在默认的my.cnf符号链接已被移除,我们可以调用模板模块,将新的my.cnf文件放置在其位置。templates模块通过利用本地templates目录,并查找与我们指定的源文件名称my.cnf.j2匹配的文件来工作。模板使用 Jinja2 模板语言来放置新的配置,并替换任何来自角色的变量。新配置文件的位置将是/etc/MySQL/my.cnf,并会应用适当的权限。

  • 初始化数据库:该剧本中的最终任务被称为include任务。include语句,顾名思义,会包含其他剧本的 YAML 文件以供执行。通常,include语句是将剧本分解成逻辑上分组的相似任务块的好方法。在此场景中,我们希望根据逻辑条件(即变量initialize_database设置为 true),包含剧本initialize_database.yml。在其他编程语言中,存在如if, else...ifelse的结构,用于表示逻辑评估。Ansible 通过关键字when来处理此类逻辑,列出执行某个操作的条件。在本例中,当变量initialize_database为 true 时,将执行剧本initialize_database.yml;如果变量为 false,则跳过这些任务。

现在我们已经很好地理解了main.yml剧本中的任务执行内容,让我们来看一下initialize_database.yml剧本中的任务,看看当initialize_database变量评估为 true 时会发生什么:

tasks/initialize_database.yml

该文件位于roles/mariadb_role/tasks/initialize_database.yml

---
- name: Temporarily Start MariaDB Server
  shell: MySQLd --user=MySQL &

- name: Create Initial Accounts
  shell: MySQL -e "CREATE USER '{{ default_user }}'@'%' IDENTIFIED BY '{{ default_password }}';"

- name: Grant Privileges to New Account
  shell: MySQL -e "GRANT ALL ON *.* TO '{{ default_user }}'@'%' WITH GRANT OPTION;"

- name: Create Default Databases
  shell: MySQL -e "CREATE DATABASE {{ item }};"
  with_items:
    - "{{ databases }}"

- name: Flush Privileges
  shell: MySQL -e "FLUSH PRIVILEGES;"

任务细分(initialize_database.yml)

  • 临时启动 MariaDB 服务器:默认情况下,当 MariaDB 首次安装时,未创建任何数据库,且没有用户访问数据库。在某些情况下,我们可能希望启动一个干净的 MariaDB 服务器,并让外部用户或工具创建默认的数据库和访问凭据。然而,也可能有相同数量的情况,我们需要创建带有内置数据库和用户凭据的数据库实例。为了创建这些默认项,我们首先需要启动 MySQL 服务器,以便能够通过命令行访问它。为了临时启动服务器,我们将调用shell模块,该模块以类似于在 Bash 提示符下输入命令的方式执行 shell 命令。我们将运行命令mysqld,指定以mysql用户身份运行,并使用符号&强制服务器在后台运行。此时,MySQL 服务器将继续运行,直到构建完成并且容器被关闭。

  • 创建初始账户:创建初始账户的步骤类似地调用了 shell 模块,以便利用 MySQL 命令行客户端。-e标志允许我们传入可执行的 SQL 命令,这些命令将由服务器进行评估。我们将使用此命令来创建一个默认的用户名和密码,用于登录数据库。默认凭据将从我们的变量中获取,因此使用了双大括号。

  • 授予新账户权限:再次使用shell模块,我们可以调用 MySQL 客户端,授予我们之前创建的新账户权限。在此示例中,我们将授予所有权限,允许从任何网络接口连接并访问该 MySQL 服务器。

  • 创建默认数据库:通过使用我们的迭代或循环操作符with_items,我们可以传入一个我们希望 MySQL 客户端创建的数据库列表。在我们的defaults/main.yml文件中,我们将数据库变量指定为一个数组或项列表。Ansible 将识别我们的数据库变量实际上是一个字符串列表,并对其进行迭代。结果是,任何我们指定为数据库变量列表项的数据库都将被迭代并创建在我们的 MySQL 容器中。

  • 刷新权限:最后一次调用 shell 模块将允许我们执行 SQL 命令FLUSH PRIVILEGES,使新用户账户在数据库中生效。此命令执行完毕后,容器构建将完成,通知 Ansible Container 关闭中间容器,并将最终更改提交到我们刚刚构建的容器中。

现在我们已经查看了tasks目录并了解了角色如何执行任务,接下来我们看看templates/目录,了解我们正在生成并传递到容器中的模板化配置文件。你会发现,在roles的 templates 目录中,有一个文件:my.cnf.j2。这是我们希望 Ansible 在构建过程中编译并传递到容器中的my.cnf文件的模板。最佳实践是始终将你的 Ansible 模板文件命名为目标文件名,并使用.j2扩展名。这表示该文件是一个 Jinja2 模板,包含变量和 Jinja2 逻辑,供 Ansible 进行评估。

Jinja2 是一个功能强大的模板语言,可以在你的项目中做很多很酷的事情。虽然不是严格要求,但对 Jinja2 有一定了解会对你的 Ansible 开发大有帮助。你可以在官方网站上了解更多关于 Jinja2 语言的信息:jinja.pocoo.org/.

以下是templates目录中my.cnf.j2文件的内容:

templates/my.cnf.j2

该文件位于roles/mariadb_role/templates/my.cnf.j2

# Ansible Container Generated MariaDB Config File
[client]
port            = 3306
socket          = /var/lib/MySQL/MySQL.sock

# The MariaDB server
[MySQLd]
user           = MySQL
port            = 3306
socket          = /var/lib/MySQL/MySQL.sock
datadir         = /var/lib/MySQL
bind-address = 0.0.0.0
skip-external-locking
key_buffer_size = {{ key_buffer_size }}
max_allowed_packet = {{ max_allowed_packet }}
table_open_cache = {{ table_open_cache }}
sort_buffer_size = {{ sort_buffer_size }}

请注意,在文件的第一行,我们通过注释块明确告诉用户该文件是Ansible 容器生成的 MariaDB 配置文件。如果你有连接远程服务器并排查问题的经验,你会知道确切了解文件来源、值的来源以及哪个配置管理工具负责将这些文件放到那里的重要性。虽然这不是严格要求的,当然也是个人喜好的问题,但我喜欢在 Ansible 触及的文件上添加这样的标语。这样,稍后有人查看时,会清楚地知道这个容器是如何以这种状态创建的。

接下来你会注意到,配置文件的最后四行的值被设置为双大括号,其中夹着配置文件的键名。如我们之前讨论的,双大括号表示 Jinja2 变量参数。当 Ansible 在将模板安装到容器中的目标位置之前进行评估时,Ansible 会解析文件中的所有 Jinja2 块,并执行读取到的指令,将模板带到所需的状态。这可能意味着填充变量的值、评估逻辑条件,甚至获取模板所需的环境信息。在这种情况下,Ansible 会看到双大括号并用这些变量的定义值替换它们。通过修改或覆盖变量,Ansible 使得更改容器和应用程序的功能变得非常容易。此外,请注意,变量的名称与它们修改的配置选项相匹配。变量名完全由开发人员决定,因此开发人员可以自由选择变量名。然而,通常的最佳实践是使用描述性强的变量名,以便用户清楚地了解他们正在覆盖或修改哪些设置。

阅读这些角色文件时,你可能已经很清楚,变量与 Ansible 的运行方式、模板的填充方式,甚至任务的执行和控制方式都有很大关系。现在让我们看看变量是如何在角色中定义的,以及我们如何利用变量使角色更加灵活,并使其能够复用。如前所述,角色变量可以存储在两个地方:defaults/ 目录或 vars/ 目录。作为开发人员,你可以选择将变量存储在哪个位置(或者两个位置)。唯一的区别在于变量的优先级顺序,这决定了变量的评估顺序。存储在 defaults/ 目录中的变量最容易被覆盖,而存储在 vars/ 目录中的变量优先级稍低,因此更难覆盖。在这个例子中,我选择将所有变量存储在 defaults/ 目录中的 main.yml 文件里。让我们看看这个文件长什么样:

---
# defaults file for MySQL_role
initialize_database: true
default_user: "root"
default_password: "password"
databases:
  - "TestDB1"
  - "TestDB2"
  - "TestDB3"

#MySQL Basic Tuning for my.cnf
key_buffer_size: "16K"
max_allowed_packet: "1M"
table_open_cache: 4
sort_buffer_size: "64K"

在这里,你可以看到这些都是我们之前见过的变量,它们在角色任务以及 my.cnf 模板文件中都有引用。变量 YAML 文件本质上只是使用我们在章节开始时探索的相同 YAML 结构的静态 YAML 文件。例如,默认情况下,该文件通过将 initialize_database 变量设置为布尔值 true 来初始化数据库。我们还可以看到,数据库中将创建的默认凭据设置为字符串 rootpassword,以及在初始化数据库任务期间将创建的测试数据库列表。最后,在文件底部,我们有一组变量,用于定义将被纳入模板的值。如果我们按原样构建角色,而不提供任何变量覆盖,我们将得到一个完全符合这些规格的容器。然而,如果不探索如何定制我们刚刚编写的角色,这本书将不完整!

构建容器启用角色

在我们开始定制角色之前,让我们先构建该角色并演示使用我们指定的默认变量的默认功能。我们继续回到我们的 Ansible 容器工作流程,执行 ansible-container build,然后在项目的 root 目录下执行 ansible-container run 命令:

ubuntu@node01:$ ansible-container build
Building Docker Engine context...                                                                                                                                                                            
Starting Docker build of Ansible Container Conductor image (please be patient)...                                                                                                                                                                                      
Parsing conductor CLI args.                                                                                                                                                                                  
Docker™ daemon integration engine loaded. Build starting.       project=mariadb_demo                                                                                                                         
Building service...     project=mariadb_demo service=MySQL_database_container                                                                                                                                

PLAY [MySQL_database_container] ************************************************                                                                                                                             

TASK [Gathering Facts] *********************************************************                                                                                                                             
ok: [MySQL_database_container]                                                                                                                                                                               

TASK [mariadb_role : Install Base Packages] ************************************                                                                                                                             
changed: [MySQL_database_container] => (item=[u'ca-certificates', u'apt-utils'])                                                                                                                             

TASK [mariadb_role : Install dumb-init for Container Init System] **************                                                                                                                             
changed: [MySQL_database_container]

TASK [mariadb_role : Create MySQL Group] ***************************************
changed: [MySQL_database_container]

TASK [mariadb_role : Create MySQL Users] ***************************************
changed: [MySQL_database_container]

TASK [mariadb_role : Install MySQL Server] *************************************
changed: [MySQL_database_container]

TRUNCATED

你可能会注意到从构建输出中,Ansible 正在使用 with_items 迭代运算符将我们在任务中提供的列表项准确地构建成镜像,并根据我们在角色中提供的变量将其带入所需状态,暂时来说,这些是默认变量。

让我们运行我们的项目并尝试访问 MySQL 服务:

ubuntu@node01:$ ansible-container run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
Verifying service image service=MySQL_database_container

PLAY [localhost] ***************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=e33fb25670f1bc040a6edd19360ec528be28b9f14d4ccb0d9a8ed34a71d1c561 save_container=False

执行 docker ps -a 会显示我们的容器正在运行,并且在主机上暴露了端口 3306

ubuntu@node01:~$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
7cab59c33cfa  mariadb_demo-MySQL_database_container  "/usr/bin/dumb-ini…"  About a minute ago   Up About a minute   0.0.0.0:3306->3306/tcp   mariadb_demo-MySQL_database_container

为了测试确保一切正常,我们可以下载并安装 mariadb-client 包,或者选择任何你喜欢的 MySQL 客户端:

ubuntu@node01:~$ sudo apt-get install -y mariadb-client

安装 MariaDB 客户端后,你可以使用以下命令连接到暴露在 Vagrant 虚拟机 localhost 上的 MariaDB 容器。如果你不熟悉 MySQL 客户端,请记住,传递给客户端的所有标志后面不带空格。这看起来有点奇怪,但它应该会把你带入 MySQL 控制台:

ubuntu@node01:~$ MySQL -h127.0.0.1 -uroot -ppassword
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2
Server version: 10.0.31-MariaDB-0ubuntu0.16.04.2 Ubuntu 16.04

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> 

让我们运行 show databases; 命令,看看我们在默认变量中指定的测试数据库是否已经创建:

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| TestDB1            |
| TestDB2            |
| TestDB3            |
| information_schema |
| MySQL              |
| performance_schema |
+--------------------+
6 rows in set (0.00 sec)

MariaDB [(none)]>

看起来一切都已经正确创建并按预期工作。当你完成此会话工作时,可以使用 exit 命令退出 MySQL CLI。使用 ansible-container destroy 来重置你的环境。让我们通过定制我们的角色并引入外部变量值来让事情变得有趣。

定制容器启用角色

正如我们在上一章看到的那样,通过将变量直接添加到项目的container.yml文件并重新构建项目,抽象掉 Ansible 容器项目中的变化非常简单。这提供了将所有配置更改集中在一个位置的额外便利,并有效地作为单一的真相来源。对于某些用例来说,这可能已经足够,但如果需要为不同的环境或位置(例如开发、测试、QA 和生产环境)提供配置不同的容器呢?你可以简单地更新container.yml文件,并为这些场景构建不同的镜像。然而,Ansible Container 提供了更好的处理方式,允许我们通过外部变量文件来管理这一切。ansible-container父命令的一部分是--var-files标志,它允许为变量定义提供外部 YAML 文件的选项。这为我们提供了一个抽象,允许不同配置选项的独立构建并行运行。这还允许我们使用不同的变量文件来定制我们的角色,几乎可以针对任何能够与项目一起版本控制的情况。

为了启用此功能,让我们在项目根目录(与项目特定的container.yml同级)创建一个名为variable_files的目录。在此目录内,我们将创建三个独立的文件:dev.ymltest.ymlprod.yml,它们具有略有不同的配置选项。以下是这三个文件的示例。希望你喜欢我的《星际迷航》引用!

在我们开始之前,最好先执行ansible-container destroy操作,然后使用不同的变量重新构建容器。这样,你可以清楚地看到在构建过程中究竟发生了什么变化。

在开发环境中,我们数据库的主要用户将是叶曼·兰德。她将主要关注星际舰队数据:

variable_files/dev.yml

该文件位于<project_root>/variable_files/dev.yml

---
# Development Defaults for MySQL role
initialize_database: true
default_user: "yeoman_rand"
default_password: "starfleet"
databases:
  - "starfleet_data"

系统测试中,斯波克先生将是我们数据库的主要用户。他对与火山星、舰船条例、穿梭机以及联邦数据相关的数据更感兴趣。

variable_files/test.yml

该文件位于<project_root>/variable_files/test.yml

---
# System Test Defaults for MySQL role
initialize_database: true
default_user: "MrSpock"
default_password: "theBridge"
databases:
  - "planet_vulcan"
  - "federation_data"
  - "shuttle_crafts"
  - "ship_ordinances"

variable_files/prod.yml

在生产环境中,柯克舰长将需要存储与其他船员完全不同的数据。我们需要稍微增强我们的 MySQL 配置,以支持存储舰长日志、企业数据以及联邦指令所增加的负载。该文件位于:<project_root>/variable_files/prod.yml

---
# defaults file for MySQL_role
initialize_database: true
default_user: "captainkirk"
default_password: "ussenterprise"
databases:
  - "CaptainsLog"
  - "USS_Enterprise"
  - "InterstellarColonies"
  - "FederationMandates"

#MySQL Basic Tuning for my.cnf
key_buffer_size: "128K"
max_allowed_packet: "20M"
table_open_cache: 12
sort_buffer_size: "128K"

你可能还会注意到,并不是所有的变量在这里显示的每个示例中都被覆盖。在变量没有被源文件覆盖的情况下,Ansible 将使用角色中的 defaults/main.yml 中的值。重要的是,您的角色默认值必须为所有变量提供值,因为没有值的变量会导致构建过程失败。

变量文件的名称可以是你想要的任何名称。由于我们在构建过程中引用这些文件,并且它们不是 Ansible Container 会自动发现的文件,所以命名规则完全由你决定。

我们可以通过执行 ansible-container build 命令,并将 --vars-files 标志作为 ansible-container 命令的参数来基于这些变量中的任何一个构建容器。记住,我们始终在与项目特定的 container.yml 文件位于同一目录下运行 build 命令:

ansible-container --vars-files variable_files/dev.yml build

在构建过程中,你应该注意到,根据我们提供的变量,许多任务的执行方式略有不同。例如,当引用开发变量时,你会看到只创建了一个数据库:starfleet_data。这表明新的变量已经被正确引用并在构建过程中正确填充。现在让我们执行一个新的容器版本的 ansible-container run,并尝试使用与之前相同的凭证登录:

ubuntu@node01:$ ansible-container run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
Verifying service image service=MySQL_database_container

PLAY [Deploy mariadb_demo] *****************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=bf5221a710736d238b0995dd4ce42b57bcb9338131225d71c0d7d1b3cef85677 save_container=False

现在,要使用 MariaDB 客户端登录:

ubuntu@node01:$ MySQL -h127.0.0.1 -uroot -ppassword
ERROR 1698 (28000): Access denied for user 'root'@'172.18.0.1'

很明显,我们在角色默认值中设置的默认凭证不再有效。让我们再试一次,使用我们在开发变量文件中为 Yeoman Rand 用户指定的凭证:

ubuntu@node01:$ MySQL -h127.0.0.1 -uyeoman_rand -pstarfleet
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.0.31-MariaDB-0ubuntu0.16.04.2 Ubuntu 16.04

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

看起来我们的新容器使用开发环境的源变量文件工作正常。让我们运行 show databases; 命令,确保数据库已正确创建并存在:

MariaDB [(none)]> show databases;
'+--------------------+
| Database           |
+--------------------+
| information_schema |
| MySQL              |
| performance_schema |
| starfleet_data     |
+--------------------+
4 rows in set (0.04 sec)

MariaDB [(none)]>

如你所见,数据库 starfleet_data 存在,并与 MariaDB 默认的数据库(如 information_schemaMySQLperformance_schema)一起显示。这表明容器已正确构建,并且已准备好在我们的开发环境中部署(以本示例为目的)。现在我们可以将镜像推送到我们选择的容器注册表。在这个例子中,我将在项目特定的 container.yml 文件的 registries 部分添加 Docker Hub,指定命名空间为我的 Docker Hub 用户名(记得在 registries 段落开始时删除大括号)。保存该文件后,让我们将镜像标记为 dev 并将其推送到我们的 Docker Hub 仓库,以便我们有一个可以用来部署应用程序的构建镜像工件:

container.yml

项目特定的 container.yml 文件位于项目的 root 目录中:

registries:
  docker:
    url: https://index.docker.io/v1/
    namespace: username

使用 --push-to 标志推送镜像:

ubuntu@node01:$ ansible-container push --push-to docker --tag dev
Parsing conductor CLI args.
Engine integration loaded. Preparing push.      engine=Docker™ daemon
Tagging aric49/mariadb_demo-MySQL_database_container
Pushing aric49/mariadb_demo-MySQL_database_container:dev...
The push refers to a repository [docker.io/aric49/mariadb_demo-MySQL_database_container]
Preparing
Waiting
Pushing
Pushed
Pushing
Pushed
Pushing
Pushed
dev: digest: sha256:98d288cfa09acc3f06578532cd6ccd78af0eb65b84ba3b0ee011105e59cfb588 size: 1569
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=323116464d8f687238ae6ab64f86fb54746b6c2f5f0b895754da6ef0ce540d76 save_container=False

container.yml文件中配置 Docker Hub 作为镜像仓库并不是完全必要的,因为 Ansible Container 默认会使用 Docker Hub。不过,我喜欢确保不会不小心将镜像推送到错误的仓库,因此最佳实践是始终在container.yml文件中提供镜像仓库,并始终使用-–push-to标志命令来指定正确的仓库进行推送。

我们可以对test.yml配置文件以及prod.yml配置文件执行相同的构建过程,并将它们推送到 Docker Hub 仓库(记得在构建之间执行destroy操作)。注意,在上传不同版本的镜像时,Docker 会自动识别与先前上传版本相同的镜像层。在这种情况下,Docker 会通过不推送相同的镜像层,仅推送已更改的层,帮助你节省带宽和资源,如下所示。请注意Layer already exists行:

ubuntu@node01:$ ansible-container push --push-to docker --tag test
Parsing conductor CLI args.
Engine integration loaded. Preparing push.      engine=Docker™ daemon
Tagging aric49/mariadb_demo-MySQL_database_container
Pushing aric49/mariadb_demo-MySQL_database_container:test...
The push refers to a repository [docker.io/aric49/mariadb_demo-MySQL_database_container]
Preparing
Waiting
Layer already exists
Pushing
Layer already exists
Pushing
Layer already exists
Pushing
Pushed
test: digest: sha256:1f9604585e50efe360a927f0a5d6614bb960b109ad8060fa3173d4ab259ee904 size: 1569
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=ef42185fab1cfe20de65101059957844bbc4a166a8ab10352fe1b796e7ea5c3d save_container=False

现在我们应该有三个不同的容器镜像可供下载。这些镜像可以在我们假想的开发实验室、系统测试实验室以及生产环境中下载和部署。这些容器镜像保证在这些环境中以与我们本地工作站完全相同的方式运行。此时,我们可以进行最终的操作,并在不同的端口上运行这三个容器,以模拟这些容器在不同环境中、使用不同配置的运行方式。为了快速演示,我们将使用本地的docker run命令来指定我们打标签的镜像以及容器服务要使用的端口;我们还指定我们的服务应该使用-d标志在后台运行。请注意,我们正在创建的每个容器实例都使用devtestprod标签以及我们的用户仓库地址。在我的例子中是aric49

docker run -d --name MySQL_Dev -p 3308:3306 aric49/mariadb_demo-MySQL_database_container:dev

docker run -d --name MySQL_Test -p 3309:3306 aric49/mariadb_demo-MySQL_database_container:test

docker run -d --name MySQL_Prod -p 33010:3306 aric49/mariadb_demo-MySQL_database_container:prod

测试容器功能的过程与之前完全相同。我们可以使用 MariaDB 客户端登录到我们容器的一个实例。不过这次,我们需要指定我们的服务监听的端口,因为所有三个实例不能在主机网络端口3306上监听。如果我们想登录到生产容器,我们可以使用33010端口和ussenterprise密码为 Captain Kirk 指定凭据:

ubuntu@node01:$ MySQL -h127.0.0.1 -ucaptainkirk -pussenterprise -P33010
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2
Server version: 10.0.31-MariaDB-0ubuntu0.16.04.2 Ubuntu 16.04

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

利用相同的 Ansible 容器项目和容器启用角色,我们能够使用 Ansible 容器默认的原语构建多种配置的容器,这些容器可以在不同的环境和用例中使用。这种方法使我们能够确保构建过程在未来的所有构建迭代中保持完全相同,但我们也能灵活地为我们的角色提供新的配置值,而无需修改之前编写的代码。通过使用容器标签,可以捕获容器配置的快照并与其他用户共享。现在,我们拥有了一个极其有用且可重复的流水线,确保未来版本的应用程序容器可以追溯到用于生成它们的源角色。即使容器镜像不小心从镜像注册中心被删除,我们也可以随时轻松构建和重建我们的容器,因为所有容器中的配置都是以代码形式声明的,使用的是 Ansible playbook 语言。如果你曾在 IT 相关的 DevOps 或系统管理员岗位工作过很长时间,你会明白,拥有这种级别的基础设施洞察力是多么宝贵。

参考资料

概述

在本章中,我们探讨了角色如何不仅使 Ansible 容器中的重用成为可能,而且实际上是 Ansible 容器成为一个强大工具的核心,帮助构建和管理容器。

我们首先了解了如何使用 Ansible Galaxy 命令行工具创建容器启用角色的基础框架,包括所有必需的目录和默认的 YAML 文件,从中我们可以构建我们的角色。接着,我们编写了一个自定义角色,使用合理的默认配置选项构建 MariaDB 容器。最后,我们在角色上方开发了一个抽象层,通过传递自定义的变量配置选项,使我们能够在不修改任何代码的情况下定制我们的容器项目。

我希望本章能够有效展示使用 Ansible Container 项目所能提供的强大功能。到目前为止,我认为很容易做出假设,认为使用 Dockerfiles 构建容器镜像会更简单,而不必担心 Ansible Container 带来的额外开销。希望你现在能够理解,使用 Ansible Container 的好处远远超过了所需的少许额外复杂性。通过使用 Ansible Container,你可以创建一个强大的管道,用于构建、运行、测试和推送容器镜像。通过利用易于理解的 Ansible playbook 语法语言,我们有了一个基础,可以开始构建现代化、敏捷的容器化基础设施,这使得我们能够快速部署变更,并真正开始拥抱真正模块化基础设施的承诺。

现在我们了解了如何构建和部署真正自定义的容器,我们可以开始了解 Kubernetes,一个开源框架,用于自动化容器的大规模部署、编排和管理。

第五章:使用 Kubernetes 扩展容器规模

Kubernetes 无疑是迄今为止最受欢迎的开源项目之一,它席卷了整个 IT 世界。似乎无论你走到哪里,阅读的每一篇博客或遇到的每一篇新闻文章,都在讲述 Kubernetes 如何彻底改变了 DevOps 和 IT 基础设施的管理方式。 Kubernetes 确实牢牢抓住了 IT 领域的主导地位,并引入了全新的概念和看待基础设施的方式,这些是其他平台所无法比拟的。你可能属于那些听说过 Kubernetes 但对它是什么以及如何真正为你的基础设施带来益处一无所知的 IT 专业人员。或者,你可能正像我们大多数人一样,正在进行应用程序和工作负载的容器化,但暂时不想涉及 Kubernetes 额外的复杂性和模糊的领域。最后,你可能是那些幸运的 DevOps 工程师或 IT 管理员中的一员,已经成功采用了容器和 Kubernetes,并能够真正享受到 Kubernetes 所提供的可靠性和灵活性。

本章的目的是提供一个关于 Kubernetes 的概述,介绍它是什么,它是如何工作的(从高层次看),以及如何使用 Ansible Container 将你的容器化应用部署到 Kubernetes 集群。在我们深入之前,你可能会问,Kubernetes 到底是什么?Kubernetes 是由 Google 开发的平台,用于在小规模和大规模上部署、管理、配置和编排容器。Kubernetes 最初是 Google 的一个内部项目,名为 Borg,用于管理跨 Google 广泛基础设施的容器的自动部署和扩展。基于在 Borg 上获得的一些现实世界的经验,Google 将 Kubernetes 作为开源项目发布,让其他用户和组织能够利用同样的灵活性来大规模部署容器。通过 Kubernetes,用户可以轻松地在多个集群节点之间运行容器化应用程序,自动维护所需数量的副本、服务端点以及在集群中的负载均衡。

在本书中,我们详细探讨了如何使用 Ansible 容器平台,通过 Ansible Playbooks 快速且可靠地构建容器镜像,并在本地工作站上运行这些容器。由于我们现在已经很好地理解了如何在容器内构建版本控制和配置管理,接下来的步骤是使用配置管理声明我们的应用程序应该如何在容器外部运行。这就是 Kubernetes 填补的空白。没错,它确实像听起来那样令人惊叹。准备好了吗?让我们开始吧。

本章中,我们将涵盖:

  • Kubernetes 简介

  • 使用 Google Cloud Engine 入门

  • 使用 kubectl 在 Kubernetes 中部署应用程序

  • 编写 Kubernetes 清单

  • 在 Kubernetes 中部署和更新容器

Kubernetes 简介

诚然,当人们想到 Kubernetes 时,可能立刻会想到与 Kubernetes 相关的复杂性和多层次的概念体系,并迅速认为本章内容可能会让读者感到困惑,难以理解和应用这些概念。大多数曾经尝试进入 Kubernetes 却未成功的用户,可能仍然心有余悸,对继续学习 Kubernetes 持谨慎态度。使用 Kubernetes 进行容器自动化可能会迅速变得相当复杂,但学习和使用 Kubernetes 的回报是巨大的。在继续之前,我必须强调,Kubernetes 是一个相当复杂的平台。尝试详细解释 Kubernetes 的每个特性和功能,可能需要一本完整的书,甚至更长的时间。事实上,已有许多关于使用 Kubernetes 进行容器编排的书籍,我建议读者可以参考这些书籍,以便深入理解这些概念。本章的重点是让读者对 Kubernetes 有一个基本的了解,掌握其主要功能,并能够快速入门,利用 Kubernetes 优化容器的部署。本书的篇幅无法涵盖关于 Kubernetes 的所有内容,因此如果读者想要进一步学习 Kubernetes,我强烈建议访问 Kubernetes 官网的文档:kubernetes.io/docs

到目前为止,本书中我们已经介绍了如何使用 Ansible Container 来构建在本地工作站或安装并运行 Docker 的远程服务器上运行的 Docker 容器。Docker 为我们提供了一个可用的容器运行时环境,具备启动容器、暴露端口、挂载系统卷并通过桥接接口和 IP 网络地址转换(NAT)提供基本网络连接的功能。Docker 在运行容器方面做得非常好,但未能为用户提供太多其他功能。那么当你的容器崩溃时怎么办?当你需要将应用程序扩展到更多节点、机架或数据中心时该怎么办?当一个主机上的容器需要访问另一个主机上运行的容器的资源时又该怎么办?这正是像 Kubernetes 这样的工具所解决的实际问题。可以把 Kubernetes 看作是一个使用调度器和 API 来主动监控在 Docker(或其他容器运行时)中运行的容器当前状态,并持续努力将其引导到操作员指定的期望状态的服务。例如,假设你有一个 4 节点的 Kubernetes 集群,运行着 3 个应用容器实例。如果操作员(你)指示 Kubernetes API 启动应用容器的第四个实例,Kubernetes 会识别到你当前只有三个正在运行的实例,并立即调度创建第四个容器。Kubernetes 本质上通过使用 bin-packing 算法来理解,容器应当调度在不同主机上运行,以提供高可用性并最大限度地利用集群资源。在上面的例子中,调度的第四个容器很可能会被调度到第四个集群主机上,前提是没有任何阻止新容器工作负载在该主机上运行的配置被应用。此外,如果集群中的某个主机出现故障,Kubernetes 足够智能,能够识别这种差异,并重新调度这些工作负载到不同的主机上,直到故障节点被恢复。

除了 Kubernetes 提供的灵活配置管理功能外,它还以其独特的能力而闻名,能够为容器提供弹性的网络资源,如服务发现、DNS 解析和容器间的负载均衡。换句话说,Kubernetes 具有基于集群中运行的服务提供内部 DNS 解析的天生能力。当新 pod 被添加到服务中时,Kubernetes 会自动识别新的容器并更新 DNS 端点,以便新容器可以通过调用集群内的内部服务域名来提供服务。这确保了其他容器可以通过调用 Kubernetes 覆盖网络内的内部域名和集群 IP 地址,直接与其他容器化服务进行通信。

Kubernetes 引入了许多新概念,如果你来自静态容器部署的背景,这些概念可能会有些陌生。在本章中,我们会不断提及这些概念,因此理解这些术语的含义对于我们继续学习 Kubernetes 非常重要:

  • Pod:Pod 表示 Kubernetes 集群中运行的一个或多个应用容器。默认情况下,pod 定义至少会指定一个用户希望在集群中运行的容器,包括用户希望容器运行时的额外环境变量、命令或入口点配置。如果 pod 包含多个容器定义,那么 pod 中运行的所有容器共享 pod 的网络和存储资源。例如,你可以运行一个包含 Web 服务器容器和缓存服务器容器的 pod。从 pod 的角度来看,Web 服务器可能运行在 localhost 的 80 端口,而缓存也运行在 localhost 的 11211 端口。从 Kubernetes 的角度来看,pod 本身会有一个在集群内部的单一 IP 地址,服务会通过这个 IP 地址进行暴露,但实际上,这个 pod 会包含两个完全独立的容器。

  • 部署:部署是 Kubernetes 中定义将运行在集群中的 pod 的对象。部署包含各种参数,例如容器镜像的名称、卷挂载以及要运行的副本数。为了从 Kubernetes 集群中删除 pod,必须删除该部署。如果你只是尝试删除 pod,你会发现 Kubernetes 会尝试重新创建这些 pod。这是因为部署对象会告知集群这些 pod 应该运行,控制器管理器(稍后会介绍)会尝试将集群恢复到期望的状态。

  • 标签:标签是可以分配给 Kubernetes 中几乎任何对象的键值对。标签可以用来组织集群中资源的子集。例如,如果你有一个运行多个相同 pod 的集群,可以通过不同的标签来指示它们的不同用途。标签甚至可以被调度器利用,来确定在哪些时间和位置运行这些 pod。

  • 服务:服务定义了 pod 的逻辑子分组(通常通过标签选择器),以及它们应该如何被集群中其他资源访问。例如,你可以创建一个服务,将一组 pod 暴露给外部世界。可以使用标签选择器来确定哪些 pod 应该被暴露。如果之后有 pod 被添加或移除,Kubernetes 会自动扩展该服务,只要新 pod 使用相同的选择器运行。

要使此功能透明化,Kubernetes 在集群中提供多个服务,这些服务协同工作,确保集群和应用程序持续处于所需状态。总称为 Kubernetes 控制平面。控制平面允许功能操作、管理运行的容器,并在集群中跟踪节点和资源的状态。现在让我们快速看一下这些服务:

  • KubeCTL:kubectl(发音为 kube-control),是与 Kubernetes 交互的命令行工具。kubectl 直接访问 Kubernetes API,用于调度新的部署、与 Pod 交互、暴露部署等。kubectl 工具需要一组凭据来访问 Kubernetes API。

  • Kubernetes API 服务器:Kubernetes API 服务器负责接受来自操作者的输入,可以是来自 kubectl 命令行工具,也可以是直接访问 API 本身。Kubernetes API 负责协调信息到集群的其余部分,以执行期望的状态。需要注意的是,Kubernetes API 服务器依赖 ETCD 服务来存储和检索有关集群节点和运行中服务的信息。

  • Kubernetes 调度器:Kubernetes 调度器负责在集群节点上调度新的工作负载。其核心责任是监控集群,以确保集群中有足够的可用资源来运行 Pod,同时确保服务器可用且可达。

  • Kubernetes 控制器管理器:控制器管理器主要关注集群中期望的状态合规性。控制器管理器服务与 ETCD 服务交互,并监视通过 API 服务器进入的新作业和请求。当接收到新请求并存储在 ETCD 中时,控制器管理器与调度器协作启动新作业,以确保集群处于操作者定义的期望状态。控制器管理器通过使用控制循环持续监视集群状态,并立即纠正当前状态与期望状态之间的任何差异。当您删除 Kubernetes Pod 并自动创建新 Pod 时,感谢控制器管理器。

  • ETCD:ETCD 是由 CoreOS 创建的分布式键值存储,用于跨 Kubernetes 集群存储配置信息。如前所述,ETCD 主要由 Kubernetes API 服务器写入。

  • 容器网络接口:容器网络接口项目(CNI)旨在提供比 Kubernetes 自带的网络功能更多的附加功能。CNI 提供接口和插件支持,允许各种网络插件在 Kubernetes 集群中部署。这使得 Kubernetes 能够为分布在不同主机上的容器提供覆盖网络连接,避免容器依赖 Kubernetes 主机上提供的相对有限的网络空间。常见的第三方插件,遵循 CNI 标准的有 Flannel、Weave 和 Calico。

  • Kubelet:Kubelet 是运行在 Kubernetes 集群中每个主机上的服务。Kubelet 的主要责任是利用底层的容器运行时(Docker 或 rkt)根据 API、调度器和控制器管理器接收到的指令,在集群节点上创建和管理 Pod。Kubelet 服务不会管理在主机上运行的、非 Kubernetes 创建的容器或 Pod。可以将 Kubelet 看作是 Docker 和 Kubernetes 之间的翻译层。

现在我们已经了解了 Kubernetes 平台及其工作原理,我们可以开始使用 Kubernetes 运行本书中早些时候构建的一些容器。

开始使用 Google Cloud 平台

在本书的多个章节中,我们主要在一个单节点的 Vagrant 实验环境中进行操作,该环境预加载了你开始使用 Docker 和 Ansible Container 所需的大部分工具和实用程序,帮助你通过各种示例和实验来初始化、构建、运行和销毁容器。不幸的是,由于 Kubernetes 的复杂性,目前我们使用的 Vagrant 实验环境很难运行 Kubernetes 环境。虽然有一些方法可以实现,但它们需要更多的计算资源,并且涉及的内容超出了本书的范围。为了解决这个问题,我建议读者注册一个免费的 Google Cloud Platform 账户,在几分钟内快速启动一个三节点 Kubernetes 集群,并可以通过kubectl命令行工具从单节点 Vagrant 实验环境中使用。撰写本文时,Google 为注册 Google Cloud 免费账户的用户提供$300.00 的免费信用额度。信用额度用完后,Google 不会在未明确授权的情况下收费。这个额度足以运行我们简单的集群,并涵盖许多 Kubernetes 的核心概念。

如果你无法注册 Google Cloud Platform 账户,你可以使用 Minikube 项目在本地工作站上免费启动一个 Kubernetes 节点。使用 Virtualbox 虚拟化管理程序时,将 Minikube 配置为在你的笔记本上正常运行,并确保 kubectl 命令能正常工作相对简单。如果你有兴趣,可以在 github.com/kubernetes/minikube 上找到更多关于 Minikube 的信息。

在我们继续创建 Google Cloud Kubernetes 集群之前,我们需要先在 cloud.google.com/free/ 注册一个账户。

一旦你创建了一个免费账户,它会提示你创建一个新项目。你可以为你的项目命名任何你喜欢的名字,因为 Google Cloud 会在控制台内为其分配一个唯一的标识符。我把我的命名为 AC-Kubernetes-Demo。如果注册过程中没有提示你创建新项目,你可以从主控制台选择“项目”并点击 + 按钮创建一个新项目:

创建项目后,我们可以使用 Google Container Engine 创建 Kubernetes 集群。在主控制台窗口的左侧菜单中,选择“容器引擎 | 容器集群”:

为了示范的目的,同时也为了最大程度地利用 Google Container Engine 提供的免费配额,我们将使用最小规格创建一个三节点容器集群。为此,在容器集群仪表盘上,点击“创建集群”按钮。这将引导你进入一个表单,允许你选择集群的规格。我按照以下规格创建了我的集群:

  • 名称:Cluster-1

  • 集群版本:1.6.9

  • 每个节点 1 vCPU(总共 3 个 vCPU)

  • 容器优化操作系统

  • 禁用自动升级

一旦集群创建完成,你应该能够看到一个类似下面截图的集群:

自本文编写时,Google Cloud 界面的最新版本可能已经有所变化。你可能需要使用稍有不同的步骤或自定义选项来设置你的 Kubernetes 集群。默认设置应该足以创建一个不会迅速消耗掉你 $300.00 配额的集群。记住,你分配给集群的资源越多,你的信用额度消耗得就越快!

一旦我们的集群完全部署完成,我们就可以从 Vagrant 开发实验室连接到它。为此,我们需要先使用 Gcloud 界面初始化 kubectl 工具。默认情况下,这些包并未安装在 Vagrant 实验室中,以节省创建虚拟机时的时间和复杂性。要启用此功能,我们需要修改位于本书官方 Git 仓库 root 目录中的 Vagrantfile。在 Vagrantfile 的底部,你会看到一个标题为 #Un-Comment this section to Install the Google Cloud SDK 的部分。取消注释此部分应导致 Vagrantfile 中以下更改:

##Un-Comment this to Install the Google Cloud SDK:
export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" |     sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
apt-get update && sudo apt-get install -y google-cloud-sdk
apt-get install kubectl
SHELL
end

进行这些更改后,保存文件,并使用 vagrant up 命令启动实验室虚拟机。如果实验室虚拟机已经在运行,你可以使用 vagrant provision 命令重新配置正在运行的虚拟机,或者按照以下步骤摧毁并重新创建虚拟机:

user@localhost:~/$ vagrant destroy -f
==> node01: Forcing shutdown of VM...
==> node01: Destroying VM and associated drives...

user@localhost:~/$ vagrant up
Bringing machine 'node01' up with 'virtualbox' provider...
==> node01: Checking if box 'ubuntu/xenial64' is up to date..

一旦 Vagrant 实验室虚拟机安装了 Google Cloud SDKkubectl,执行命令 gcloud init,并在提示登录时,输入 Y 以确认并继续登录。

ubuntu@node01:~$ gcloud init
Welcome! This command will take you through the configuration of gcloud.

Your current configuration has been set to: [default]

You can skip diagnostics next time by using the following flag:
  gcloud init --skip-diagnostics

Network diagnostic detects and fixes local network connection issues.
Checking network connection...done.
Reachability Check passed.
Network diagnostic (1/1 checks) passed.

You must log in to continue. Would you like to log in (Y/n)? Y

然后,Gcloud CLI 工具会返回一个超链接,允许你使用 Google Cloud 帐户授权你的 Vagrant 实验室。授权使用 Google Cloud 帐户后,你的 web 浏览器应会返回一个代码,你可以在命令行中输入该代码以完成授权过程。

接下来,CLI 向导会提示你选择一个项目。你刚刚创建的项目应该会显示在一系列选项中。请选择我们刚刚创建的项目:

Pick cloud project to use: 
 [1] ac-kubernetes-demo
 [2] api-project-815655054520
 [3] Create a new project
Please enter numeric choice or text value (must exactly match list 
item): 1

然后,它会提示你是否希望配置 Google Compute Engine。这不是必须的步骤,但如果你选择执行此操作,你将看到一个地理区域列表供你选择。请选择离你最近的区域。最后,你的 Google Cloud 帐户应已成功连接到你的 Vagrant 实验室。

现在,我们可以使用 kubectl 工具设置与 Kubernetes 集群的连接。可以通过在容器引擎仪表盘上选择连接按钮,位于我们集群旁边来完成此操作。屏幕应会弹出,显示如何从初始化的 Vagrant 实验室连接到集群的详细信息:

将该命令复制并粘贴到你的 Vagrant 环境中:

ubuntu@node01:~$ gcloud container clusters get-credentials cluster-1 --zone us-central1-a --project ac-kubernetes-demo

WARNING: Accessing a Container Engine cluster requires the kubernetes commandline
client [kubectl]. To install, run
  $ gcloud components install kubectl

Fetching cluster endpoint and auth data.
kubeconfig entry generated for cluster-1.

这应该会缓存访问我们集群所需的默认 Kubernetes 凭证,以便从 kubectl 命令行工具访问。由于本章早些时候对 Vagrantfile 所做的更改,kubectl 将已经安装在 Vagrant 实验室虚拟机中。

由于 kubectl 已经安装,我们可以通过执行 kubectl cluster-info 来验证与 Kubernetes 集群的连接,并查看正在运行的集群的详细信息。我已经对我的集群环境中的 IP 细节进行了屏蔽。不过,你的输出会显示核心服务的所有相关地址:

ubuntu@node01:~$ kubectl cluster-info
Kubernetes master is running at https://IPADDRESS
GLBCDefaultBackend is running at https://IPADDRESS/api/v1/namespaces/kube-system/services/default-http-backend/proxy
Heapster is running at https://IPADDRESS/api/v1/namespaces/kube-system/services/heapster/proxy
KubeDNS is running at https://IPADDRESS/api/v1/namespaces/kube-system/services/kube-dns/proxy
kubernetes-dashboard is running at https://IPADDRESS/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy

你还可以运行 kubectl get nodes 来查看集群中的节点输出:

ubuntu@node01:~$ kubectl get nodes
NAME                                       STATUS    AGE       VERSION
gke-cluster-1-default-pool-ca63b897-7pwx   Ready     2d       v1.6.9
gke-cluster-1-default-pool-ca63b897-d9cf   Ready     2d       v1.6.9
gke-cluster-1-default-pool-ca63b897-fnnt   Ready     2d       v1.6.9

在 Kubernetes 中使用 kubectl 部署应用程序

KubeCTL 或 Kube Control 是进入 Kubernetes API Server 和其他 Kubernetes 控制平面的官方命令行接口。通过使用 kubectl 工具,你可以查看 pods 的状态、访问集群资源,甚至进入运行中的 pods 进行故障排除。在本章的这一部分,我们将了解如何使用 kubectl 手动描述部署、扩展 pods 并创建服务以访问这些 pods。这对于理解 Kubernetes 的基本概念非常有益,有助于理解 Ansible Container 如何利用可用的本地 Kubernetes 模块自动化这些任务。

让我们来看一些在使用 Kubernetes 时最常见的 kubectl 选项和语法:

  • kubectl getkubectl get 用于返回当前在 Kubernetes 集群中存在的资源。通常,该命令用于获取当前正在运行的 pods 或集群中的节点列表。可以将此命令视为类似于 docker ps 命令。get 命令的示例包括:kubectl get podskubectl get deployments

  • kubectl describedescribe 用于查看特定集群资源的详细信息。如果你想知道某个资源的最新状态或当前如何运行,可以使用 describe 命令。describe 非常有用,因为你可以指定某个集群资源,例如 pod、service、deployment 或 replication controller,来查看直接与该实例相关的详细信息。describe 也非常适用于在 Kubernetes 环境中进行故障排除。describe 的示例包括:kubectl describe podkubectl describe node

  • kubectl runkubectl run 的功能与我们之前在本书中探讨过的 docker run 命令非常相似。Run 主要用于启动新的部署并快速启动 pods。在 Kubernetes 集群中运行。Run 的使用场景比较有限,因为更复杂的部署更适合使用 createapply 命令。然而,对于测试或快速高效地启动容器,run 是一个非常棒的工具。

  • kubectl create:Create 用于创建新的集群资源,如 pods、deployments、namespaces 或 services。Create 的功能与 apply 和 run 非常相似,区别在于它仅用于启动新对象。使用 create 时,可以使用 -f 标志传入清单文件或直接的 URL,以启动比使用 kubectl run 更复杂的资源。

  • kubectl applyapply 常常与 create 混淆,因为它们的语法和功能非常相似。apply 用于更新 Kubernetes 集群中已经存在的资源,而 create 用于创建新的资源。例如,如果你通过 kubectl create 创建了一系列基于 Kubernetes 清单的 pod,你可以使用 kubectl apply 来更新你可能对清单所做的任何更改。Kubernetes 控制平面将分析清单并尝试进行必要的更改,以使集群资源达到所需的状态。

  • kubectl deletedelete 的功能非常直观,主要用于从 Kubernetes 集群中删除对象。与 createapply 类似,你可以使用 -f 标志传入之前创建或更新的 Kubernetes 清单文件,并用它作为标识符来从集群中删除这些资源。

正如你会注意到的,kubectl 使用的动词/名词语法相当容易记住。你使用 kubectl 执行的每个操作都将带有一个动词参数(如 get、describe、create、apply),后面跟着你希望操作的 Kubernetes 对象(如 pod、命名空间、节点和其他特定资源)。使用 kubectl 工具有许多其他命令选项,但这些无疑是你在刚开始使用 Kubernetes 时最常用的一些选项。

要查看 Kubectl 支持的所有参数,可以使用 kubectl --helpkubectl 子命令 --help 来获取特定功能或子命令的帮助。

既然我们现在可以通过 Vagrant 实验室访问 Kubernetes 集群,我们可以使用 kubectl 工具来探索集群中的资源和对象。我们首先要查看的命令是 kubectl get pods 命令。我们将使用这个命令返回集群中所有命名空间下的 pod 列表。仅仅输入 kubectl get pods 将不会返回任何内容,因为 Kubernetes 资源是按命名空间隔离的。命名空间为 Kubernetes 资源提供了基于 DNS 和网络规则的逻辑分隔,使用户能够对同时存在于同一集群中的多个部署进行精细化控制。目前,Kubernetes 集群中存在的所有内容都作为运行中的进程,且对于 Kubernetes 控制平面的功能至关重要,位于 kube-system 命名空间中。要查看所有命名空间中正在运行的所有内容,我们可以使用 kubectl get pods 命令,并传入 --all-namespaces 标志:

ubuntu@node01:~$ kubectl get pods --all-namespaces
NAME                                                  READY     STATUS    RESTARTS   AGE
fluentd-gcp-v2.0-k8nrl                                2/2       Running   0          17m
fluentd-gcp-v2.0-l05dw                                2/2       Running   0          17m
fluentd-gcp-v2.0-svnfw                                2/2       Running   0          17m
heapster-v1.3.0-1288166888-cqpd3                      2/2       Running   0          16m
kube-dns-3664836949-sl69q                             3/3       Running   0          17m
kube-dns-3664836949-tbmvl                             3/3       Running   0          17m
kube-dns-autoscaler-2667913178-vdjc5                  1/1       Running   0          17m
kube-proxy-gke-cluster-1-default-pool-ca63b897-7pwx   1/1       Running   0          17m
kube-proxy-gke-cluster-1-default-pool-ca63b897-d9cf   1/1       Running   0          17m
kube-proxy-gke-cluster-1-default-pool-ca63b897-fnnt   1/1       Running   0          17m
kubernetes-dashboard-2917854236-sctqd                 1/1       Running   0          17m
l7-default-backend-1044750973-68fx0                   1/1 Running   0          17m

你的列表可能与我这里提供的输出略有不同,具体取决于你的集群运行的 Kubernetes 版本,以及 Google 容器引擎平台自写作时以来可能引入的任何变化。然而,你会看到一列正在运行的容器,它们支持 Kubernetes 控制平面,如kube-proxykube-dns和使用fluentd的日志机制。默认输出将显示 Pod 的名称、运行时长(age)、运行副本的数量以及 Pod 重启的次数。

你可以使用-o wide标志来查看更多细节,例如 Pod 的命名空间和分配给 Pod 的覆盖网络 IP 地址。例如,kubectl get pods -o wide --all-namespaces

现在我们已经牢固掌握了列出 Pod 的方法,我们可以使用kubectl run命令来启动我们的第一个部署。在第三章,你的第一个 Ansible 容器项目中,我们学习了如何使用社区开发的容器角色构建一个 NGINX 容器,并将其上传到我们的个人 Docker Hub 仓库。我们可以使用kubectl run命令来下载我们的容器,快速创建一个名为nginx-web的新 Kubernetes 部署,并让该 Pod 在我们的集群中运行。为了从我们的仓库拉取 Pod,我们需要提供完整的容器名称,格式为:image-repository/username/containername。此外,我们需要将端口映射到端口8000,因为社区开发的角色默认使用了该端口。最后,我们将在default命名空间中启动这个部署,因此无需应用额外的命名空间配置:

kubectl run nginx-web --image=docker.io/username/nginx_demo-webserver  --port=8000

现在,如果我们尝试运行kubectl get pods,我们将看到在默认命名空间下运行的单个 NGINX Pod:

ubuntu@node01:~$ kubectl get pods -o wide
NAME                         READY     STATUS    RESTARTS   AGE
nginx-web-1202329523-qjkwp   1/1       Running   0          3m  

类似地,我们可以使用kubectl get deployments功能来查看默认命名空间中当前部署的状态:

ubuntu@node01:~$ kubectl get deployments
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-web   1         1         1            1           12m

如你从 get podsget deployments 的输出中看到的,我们有一个名为nginx-web的单一部署,它由一个单独的 Pod 和该 Pod 内的一个单独容器组成。这与我们通过kubectl run命令提供给 Kubernetes API 服务器的输入完全一致。如果我们尝试删除这个 Pod,部署的期望状态和当前状态之间会出现差异。Kubernetes 将尝试通过重新创建被删除的集群资源来使集群恢复到期望状态。让我们尝试删除我们创建的 NGINX Pod,看看会发生什么。通常,这个过程发生得非常迅速,你需要注意 Pod 的名称以及运行时长(age),才能看到变化是否已经发生:

ubuntu@node01:~$ kubectl delete pod nginx-web-1202329523-qjkwp
pod "nginx-web-498735019-6llvp" deleted
ubuntu@node01:~$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
nginx-web-498735019-xcp21   1/1       Running   0          15s

如果我们想要永久删除集群中的 pod,我们可以在部署本身上使用删除命令,语法为:kubectl delete deployment nginx-web。这将声明一个新的期望状态,即我们不再希望部署nginx-web存在,且该部署中的所有 pod 也应被删除。

描述 Kubernetes 资源

Kubernetes 也可以用来查看有关我们在集群中实例化的 pod 或其他对象的详细信息。我们可以使用 kubectl describe 命令来做到这一点。Describe 可以用来查看我们集群中几乎任何资源的详细视图。

让我们花点时间描述一下我们的 NGINX pod,并确保它按预期运行:

ubuntu@node01:~$ kubectl describe deployment nginx-web
Name:                   nginx-web
Namespace:              default
CreationTimestamp:      Wed, 13 Sep 2017 19:36:48 +0000
Labels:                 run=nginx-web
Annotations:            deployment.kubernetes.io/revision=1
Selector:               run=nginx-web
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
Pod Template:
  Labels:       run=nginx-web
  Containers:
   nginx-web:
    Image:              docker.io/aric49/nginx_demo-webserver
    Port:               8000/TCP
    Environment:        <none>
    Mounts:             <none>
  Volumes:              <none>
Conditions:
  Type          Status  Reason
  ----          ------  ------
  Available     True    MinimumReplicasAvailable
OldReplicaSets: <none>
NewReplicaSet:  nginx-web-498735019 (1/1 replicas created)
Events:
  FirstSeen     LastSeen        Count   From                    SubObjectPath   Type            Reason                  Message
  ---------     --------        -----   ----                    -------------   --------        ------                  -------
  59s           59s             1       deployment-controller Normal          ScalingReplicaSet       Scaled up replica set nginx-web-498735019 to 1

如你所见,describe 命令显示了有关我们集群的许多相关信息,包括 pod 运行的命名空间、pod 配置的标签、正在运行的容器镜像的名称和位置,以及 pod 上发生的最新事件。describe 输出提供了丰富的信息,有助于我们排查故障或优化集群中的部署和容器。

暴露 Kubernetes 服务

既然我们现在在集群中运行着一个功能正常的 NGINX Web 服务器,我们可以将此服务暴露给外部世界,以便他人可以使用我们崭新的服务。为此,我们可以创建一个 Kubernetes 抽象对象,称为服务(service),来控制我们的 pod 如何获得外部访问权限。默认情况下,Kubernetes pod 会通过覆盖网络分配一个集群 IP 地址,该地址仅能在集群内部的其他容器和节点间访问。如果你有一个不应该直接暴露给外部世界的部署,这非常有用。然而,Kubernetes 也支持使用服务抽象来暴露部署。服务可以通过多种方式暴露 pod,从为服务分配可公开路由的 IP 地址、跨集群进行负载均衡,到在主节点上开放一个简单的节点端口,从中监听服务。Google 容器引擎原生支持 LoadBalancer 服务类型,可以为我们的部署分配一个公共虚拟 IP 地址,极大地简化了服务的暴露过程。为了让我们的服务能够连接到外部世界,我们可以使用 kubectl expose deployment 命令,并将服务类型指定为 LoadBalancer。成功执行后,你应该会看到信息 nginx-web 服务已暴露。

ubuntu@node01:~$ kubectl expose deployment nginx-web –type=LoadBalancer
service "nginx-web" exposed

我们可以通过运行 kubectl get services 命令来查看我们新创建的服务。你可能会注意到,EXTERNAL IP 列在 Kubernetes 为集群分配公共 IP 时,可能会处于挂起状态一两分钟。如果你在几分钟后再次执行 kubectl get services 命令,你应该会看到它已经有了外部 IP,并且可以访问了:

ubuntu@node01:~$ kubectl get services
NAME         CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
kubernetes   10.11.240.1     <none>        443/TCP          31m
nginx-web    10.11.255.240   <pending>     8000:32567/TCP   6s

一两分钟后:

ubuntu@node01:~$ kubectl get services
NAME         CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
kubernetes   10.11.240.1     <none>          443/TCP          1h
nginx-web    10.11.241.144   35.202.165.54   8000:32567/TCP   1m

在这个例子中,外部 IP 地址 35.202.165.54 已经分配给我们的部署。你可以在网页浏览器中访问这个 IP 地址,实际看到 NGINX 默认的网页效果。记住,你必须通过 TCP 端口 8000 来访问这个服务,因为容器启用角色的默认配置就是这样。如果你愿意,可以额外加分,返回并重新配置你的 NGINX 容器使其在端口 80 上运行!

Google Cloud Platform 与 Google Cloud 虚拟负载均衡资源有原生集成,允许 Kubernetes 为服务分配外部 IP 地址。在裸金属环境或运行在其他云上的集群中,需额外配置以便让 Kubernetes 无缝分配公有路由的 IP 地址。

扩展 Kubernetes Pod

现在我们已经在集群中运行了 Pod,我们可以利用强大的 Kubernetes 原语来扩展容器和服务的规模,跨节点运行以实现高可用性。如前所述,一旦声明了希望运行多个副本的期望状态,Kubernetes 会应用 bin-packing 算法来确定服务应该在哪些节点上运行。如果你声明的副本数与集群中的节点数相同,Kubernetes 默认会在每个节点上运行一个副本。如果声明的副本数多于节点数,Kubernetes 会在一些节点上运行多个副本,而在其他节点上运行单个副本。这为我们提供了开箱即用的原生高可用性。使用 Kubernetes 的一个好处是,通过利用这些特性和功能,操作员不再需要像以前那样过于担心容器所运行的底层基础设施,而更专注于集群抽象本身。

注意:Kubernetes 也可以使用标签来控制某些部署应该运行的位置。例如,如果你有一个高计算能力的节点,你可以将该节点标记为计算节点。你可以自定义部署,使得这些 Pod 只会在具有该特定标签的节点上运行。

为了展示这一功能的强大,我们使用 kubectl 来扩展我们现有的 web 服务器部署。由于我们目前运行的是一个三节点集群,因此我们将 NGINX 部署扩展到四个副本。这样,我们可以更好地展示 Kubernetes 在决定将容器放置在哪些节点时所做的决策。为了扩展当前的部署,我们可以使用 kubectl scale deployment 命令将副本数从 1 增加到 4:

ubuntu@node01:~$ kubectl scale deployment nginx-web --replicas=4
deployment "nginx-web" scaled

使用kubectl get deployments命令,我们可以看到 Kubernetes 正在积极地重新配置我们的集群以达到期望的状态。根据你为集群选择的配置,Kubernetes 可能需要几秒钟才能运行所有四个 Pod。接下来我们可以看到期望的 Pod 数量、当前在集群中运行的 Pod 数量、正在更新的 Pod 数量,以及准备好并可接收流量的 Pod。看起来我们的集群已经达到了预期的状态:

ubuntu@node01:~$ kubectl get deployments
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-web   4         4         4            4           14m

运行kubectl get pods并加上-o wide标志后,我们可以看到所有四个 NGINX Pod 都在不同的 IP 地址和不同的集群节点上运行。需要注意的是,由于我们指定了四个副本而只有三个节点,Kubernetes 决定将两个 Pod 副本运行在同一主机上。请记住,这两个 Pod 是独立的,拥有不同的 IP 地址,即使它们运行在同一主机上。

ubuntu@node01:~$ kubectl get pods -o wide
NAME   READY     STATUS    RESTARTS   AGE       IP          NODE
nginx  1/1       Running   0          2m        10.8.2.5    7pwx
nginx  1/1       Running   0          2m        10.8.1.10   fnnt
nginx  1/1       Running   0          15m       10.8.1.9    fnnt
nginx  1/1       Running   0          2m        10.8.0.5    d9cf

上述输出略有截断,因为-o wide 输出格式在书籍页面的上下文中较难阅读。你的输出将比我的稍微冗长一些。

再次访问公共 IP 地址将导致服务开始在集群中的 Pod 之间进行负载均衡。由于我们将服务类型指定为LoadBalancer,Kubernetes 将使用轮询算法将流量分配给我们的 Pod,确保高可用性。不幸的是,读者可能看不到明显的效果,因为每个 Pod 都在运行相同的 NGINX 测试网页。Kubernetes 的一个主要优势是,服务通常与部署关联。当你扩展一个部署时,服务将自动扩展并开始将流量分配到新的 Pod!

在我们进行下一个练习之前,让我们删除刚刚创建的部署以及公开的服务。这将使我们的集群恢复到一个干净的状态:


ubuntu@node01:~$ kubectl delete deployment nginx-web
deployment "nginx-web" deleted

ubuntu@node01:~$ kubectl delete services nginx-web
service "nginx-web" deleted

使用 Kubernetes 清单创建部署

除了能够直接从命令行创建服务和其他对象外,Kubernetes 还允许你通过清单文档来描述期望的状态。Kubernetes 清单让你能够在更易于阅读、理解和重复的格式中提供更多自定义选项,而不像命令行那样格式较为有限。由于本章并不是对 Kubernetes 进行深度探讨,我们不会花太多时间介绍 Kubernetes 清单中可以使用的各种配置选项。我的目的是向读者展示清单的样子以及它们如何在基本层面上工作。

由于你已经熟悉使用 kubectl 命令行工具创建部署,接下来我们将展示如何通过 Kubernetes 清单来描述我们的 nginx-web 部署。这些示例可以在本书的官方 Git 仓库中找到,位于 Kubernetes/nginx-demo 目录下。打开你的文本编辑器并创建一个文件:webserver-deployment.yml。这个文件的内容应该类似于以下内容。在这个示例中,我们将继续使用之前创建的 NGINX 容器。然而,如果你想尝试使用其他类型的服务和端口,可以自由选择其他容器 URL。

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: webserver-deployment
spec:
  replicas: 4
  template:
    metadata:
      labels:
        app: http-webserver

    spec:
      containers:
        - name: webserver-container
          image: docker.io/USERNAME/nginx_demo-webserver
          ports:
            - containerPort: 8000

和我们到目前为止看到的所有 YAML 文档一样,Kubernetes 清单文档以三个短横线开始,表示 YAML 文件的开始。Kubernetes 清单总是先指定 API 版本、对象类型和元数据。这通常被称为头部数据,用于告诉 Kubernetes API 文档将要创建哪些类型的对象。由于我们正在创建一个部署,我们将 kind 参数指定为 Deployment,并提供部署的名称作为元数据名称。spec 部分下列出的一切都提供了特定于文档所创建的 pod 对象的配置参数。由于我们基本上是在逆向工程我们之前的部署,所以我们将副本数指定为 4。接下来的几行指定了我们将配置 pod 的元数据,具体是一个键值对标签,叫做 app:http-webserver。请记住这个标签,因为我们接下来在创建服务资源时会使用它。

最后,我们还有另一个 spec 部分,列出了将在 pod 内运行的容器。在本章前面我提到过,pod 可以是一个或多个容器,使用共享的网络和集群资源。pod 内的容器共享 pod IP 地址和本地主机命名空间。Kubernetes 部署允许你在 containers: 部分指定多个容器,并将它们作为键值对项目传入。然而,在这个示例中,我们将创建一个单容器 pod,命名为 webserver-container。在这里,我们将指定容器镜像版本以及容器端口(80)。

这个清单可以使用 kubectl create 命令应用,并传入 -f 参数,指明这是一个清单对象,并提供清单的路径:

kubectl create -f webserver-deployment.yml

完成成功后,你应该能通过 kubectl get pods 查看创建的 pod:

使用 Kubernetes 清单创建服务

类似于我们使用 Kubernetes 清单创建部署的方式,我们还可以使用清单创建其他 Kubernetes 对象。我们之前创建的服务可以通过以下 Kubernetes 清单描述:

---
apiVersion: v1
kind: Service
metadata:
  name: webserver-service
spec:
  type: LoadBalancer
  selector:
    app: http-webserver
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000

注意,在此示例中,我们指定了不同的 kind 参数为 Service,而不是我们之前示例中使用的 Deployment。这告诉 Kubernetes API,接下来的文档内容将包含描述服务而非其他类型 Kubernetes 对象的规格。在元数据部分,我们将服务命名为 webserver-service(有创意吧?)。在规格部分,我们将提供我们暴露的服务类型,LoadBalancer,并提供我们为部署指定的标签:app: http-webserver。当使用 kubectl 暴露部署时,您创建的服务与您暴露的部署紧密关联。当该部署扩展时,服务会自动感知并根据后端 Pod 的数量进行调整。然而,使用 Kubernetes 清单文件创建服务时,我们可以更加灵活地将服务与它们所暴露的服务关联。在这个示例中,我们创建了一个与任何具有标签 app: http-webserver 的 Pod 关联的服务。从理论上讲,这可以是跨不同命名空间和部署的任意数量的 Pod。这为围绕 Kubernetes 架构设计应用程序提供了很大的灵活性。

清单的最后部分描述了我们将执行负载均衡的端口。还记得我们 NGINX 容器使用固定端口 8000 吗?这是因为我们使用社区编写的角色构建了这个容器。通过负载均衡器服务,我们可以在前端暴露任何端口,将流量转发到后端 Pod 的任意端口。我们将使用的协议是 TCP。我们希望在虚拟 IP 地址上暴露的端口是 80,用于标准的 HTTP 请求。最后,我们将列出 NGINX 在 Pod 内部监听的端口,以将流量转发到此端口。在这种情况下是 8000

使用 kubectl create 命令,我们可以像创建初始部署一样创建我们的服务:

ubuntu@node01:/vagrant/Kubernetes/nginx-demo$ kubectl create -f webserver-service.yml
service "webserver-service" created

使用 kubectl get services,我们可以看到哪个外部虚拟 IP 地址分配给了我们的集群:

ubuntu@node01:$ kubectl get services
NAME                CLUSTER-IP      EXTERNAL-IP      PORT(S)
webserver-service   10.11.251.238   104.197.85.153   80:30600/TCP

查看 PORTS 列,我们可以看到 TCP 端口 80 在我们的集群中被暴露。再次使用浏览器,我们可以访问新的公共 IP 地址的端口 80,查看是否正常工作:

使用 Kubernetes 清单文件,我们可以更详细地描述我们希望容器化应用程序的功能。清单文件也可以轻松修改,并通过 kubectl apply -f manifest.yml 命令重新应用。如果我们在任何时候想要将应用程序更新为不同版本的容器镜像,或修改服务上的暴露端口,kubectl apply 只会做出必要的更改,以使应用程序达到所需的状态。您可以自由调整这些清单文件并重新应用,查看如何配置服务运行。

接下来,我们将了解如何使用 Ansible Container 将容器部署到 Kubernetes。在继续之前,让我们使用 kubectl delete 命令删除 Kubernetes 集群中的 pod,并指定我们用于创建或修改部署和服务的 Kubernetes 清单:

ubuntu@node01:$ kubectl delete -f webserver-deployment.yml
deployment "webserver-deployment" deleted

ubuntu@node01:$ kubectl delete -f webserver-service.yml
service "webserver-service" deleted

至此,我们已经完成了 Kubernetes 集群的工作。如果你希望从 Google Cloud 中删除集群,可以现在执行此操作。然而,需要注意的是,第七章,《部署你的第一个项目》涵盖了如何将项目部署到 Kubernetes。我建议你在完成 第七章,《部署你的第一个项目》的内容之前,保持集群处于活动状态。

参考资料

总结

Kubernetes 正迅速成为最强大、灵活且流行的容器部署与编排平台,正在席卷 IT 行业。在本章中,我们深入了解了 Kubernetes,学习了它作为平台的工作原理以及使其如此有用和多功能的关键特性。如果你曾在容器领域工作过,你会很清楚,Kubernetes 正因其部署和管理容器的极其复杂机制,迅速被全球各地的组织所采用。

由于 Ansible Container 对 Kubernetes 的原生支持,我们可以使用相同的工作流程来构建、运行、测试和销毁容器化应用程序,这些应用程序可以部署到如 Kubernetes 这样的强大服务中。Ansible Container 真正提供了合适的工具,帮助我们使用统一且可靠的框架推动复杂的部署。

然而,Google Cloud 和 Kubernetes 框架并不是当前市场上唯一的基于云的容器编排解决方案。OpenShift 是由 Red Hat 构建的快速增长的托管解决方案,基于 Kubernetes 平台之上。接下来,我们将应用本章中学到的 Kubernetes 概念,将应用程序部署到 OpenShift 软件堆栈,利用 Ansible Container 平台为我们提供的强大工具,推动大规模应用工作负载。

第六章:使用 OpenShift 管理容器

使用一套综合工具,涵盖了市面上最优秀、最具韧性的开源工具之一,Kubernetes 正在迅速改变软件应用的构建和部署方式,无论是在组织内部还是在云端。Kubernetes 带来了从部署容器化基础设施中获得的经验教训,而这一基础设施出自拥有全球最大且最强大基础设施之一的公司:Google。正如我们在上一章所看到的,Kubernetes 是一个极其灵活且可靠的平台,可以在非常大规模下部署容器,并带有一系列特性和功能,能够在集群服务器上部署高可用性应用,通过运行在原生容器引擎如 DockerrktRunc 上实现。然而,Kubernetes 所带来的强大功能和灵活性,也伴随着极大的复杂性。可以说,使用 Kubernetes 部署容器化基础设施的最大缺点之一,就是需要在迁移工作负载到 Kubernetes 之前,掌握大量关于 Kubernetes 架构的知识。

针对 Kubernetes 当前面临的高开销和技术复杂性,Red Hat 提供了一个解决方案,使其能够被更多组织所使用。近年来,Red Hat 开发了一个简化 Kubernetes 概念并使其更加易于访问的解决方案,从而使软件开发人员和 DevOps 工程师能够快速部署并快速构建。OpenShift 是 Red Hat 开发的一套工具,它基于 Red Hat 版本的 Kubernetes,提供一个既复杂又易于理解的平台,用于自动化和简化容器化应用程序的部署。OpenShift 的目标是提供一个可靠、安全的 Kubernetes 环境,向用户提供一个流畅的 web 界面和命令行工具,用于部署、扩展和监控运行在 Kubernetes 中的应用程序。此外,OpenShift 是目前由 Ansible 容器项目支持的第二大云平台(Kubernetes 和 OpenShift)。

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

  • 什么是 OpenShift?

  • 在本地安装 Minishift

  • 从 web 界面部署容器

  • OpenShift web 界面提示

  • OpenShift CLI 介绍

  • OpenShift 和 Ansible 容器

什么是 OpenShift?

OpenShift 是由 Red Hat 提供的一套产品,用于构建生产就绪、可靠且安全的 Kubernetes 平台。使用 OpenShift,开发人员在使用 OpenShift API 部署容器化应用程序或访问 Kubernetes API 调整功能和特性时,拥有极大的自由度。由于 OpenShift 使用相同的底层容器运行时环境,因此可以在本地开发 Docker 容器并将其部署到 OpenShift,后者利用 Kubernetes 的所有基本元素,如命名空间、Pod 和部署,将服务暴露给外部世界。撰写本文时,Red Hat 提供了具有以下配置选项的 OpenShift 平台:

  • OpenShift Origin:一个完全免费的开源版本的 OpenShift,由社区支持。OpenShift Origin 可以通过一个名为 Minishift 的项目在本地部署。

  • OpenShift Online:OpenShift Online 是 Red Hat 提供的一个完全托管的公共云服务,允许个人和组织在不投入硬件资源的情况下,利用 OpenShift Origin。用户可以注册 OpenShift Online 免费账户,允许最多 1GB 的内存和两个 vCPU 的应用程序部署。

  • OpenShift Dedicated/Container Platform:OpenShift Dedicated 和 OpenShift Container Platform 提供由 Red Hat 管理并支持的 OpenShift 部署,可以部署在本地或通过 Google Cloud、Azure 或 AWS 等公共云服务提供商进行部署。

在本章以及接下来的章节中,我们将使用完全免费的开源 OpenShift Origin 来部署本地 Minishift 集群。与上一章不同,OpenShift 的免费版由于限制较多,无法涵盖本章将要介绍的广泛示例。为了全面展示 OpenShift 的功能,我选择引导用户完成 Minishift 的本地安装,只有运行在本地工作站的硬件资源限制了其能力。如果你一直跟进到这里,Minishift 在 VirtualBox 上的设置并不比我们之前使用的本地 Vagrant 实验环境复杂。然而,如果你想使用 OpenShift Online 的免费版,大部分示例仍然可以在其中复制,尽管它的功能有限,无法像在本地环境中运行 Minishift 那样灵活。

本地安装 Minishift

Minishift 是一个本地 OpenShift 集群,可以在您的本地 PC 上下载并运行,作为开发环境使用。Minishift 的主要用途是提供一个沙盒环境,为开发人员提供一个功能齐全的开发环境,该环境可以在您的笔记本电脑上启动。Minishift 还与 OpenShift 客户端 (OC) CLI 完全兼容,您可以使用命令行界面与 OpenShift 集群进行交互。在本章的这一部分,我们将学习如何安装 Minishift 和 OC,以便在您的本地环境中运行它。在继续之前,您需要确保您的 PC 上安装了 VirtualBox,它将作为启动 Minishift 虚拟机的虚拟化程序。为了演示目的,我们将使用 Minishift 版本 1.7.0。由于本文撰写时,Minishift 已发布了更新的版本,建议您下载 1.7.0 版本,尽管更新版本很可能也能同样良好地工作。

此外,Minishift 和 OC 可跨平台支持 Windows、Linux 和 macOS。此示例将演示如何在 Linux 上下载并安装 Minishift。如需了解在其他平台上安装 Minishift 的更多信息,您可以访问以下链接:docs.openshift.org/latest/minishift/getting-started/installing.html

安装 Minishift 二进制文件

以下步骤应在您的本地工作站上执行(而非 Vagrant 实验室虚拟机)以在本地安装 Minishift:

  1. 下载 Minishift 二进制文件:Minishift 1.7.0 二进制文件可以从以下 GitHub URL 下载,适用于所有平台(github.com/minishift/minishift/releases/tag/v1.7.0)。您可以使用网页浏览器下载该二进制文件,或者使用 wget,例如在本示例中的做法:
aric@local:~/minishift$ wget https://github.com/minishift/minishift/releases/download/v1.7.0/minishift-1.7.0-linux-amd64.tgz
--2017-10-09 19:41:08--  https://github.com/minishift/minishift/releases/download/v1.7.0/minishift-1.7.0-linux-amd64.tgz
Resolving github.com (github.com)... 192.30.253.113, 192.30.253.112
Connecting to github.com (github.com)|192.30.253.113|:443... connected.
 'minishift-1.7.0-linux-amd64.tgz' saved [3980931/3980931]
  1. 解压 Minishift 压缩包:Minishift 以与您的操作系统兼容的归档格式打包。对于 Linux 和 OSX,这是一个压缩的 tarball 文件。对于 Windows,则是一个压缩归档文件:
aric@local:~/Development/minishift$ tar -xvf minishift-1.7.0-linux-amd64.tgz
minishift-1.7.0-linux-amd64/
minishift-1.7.0-linux-amd64/LICENSE
minishift-1.7.0-linux-amd64/README.adoc
minishift-1.7.0-linux-amd64/minishift 
  1. 将 Minishift 二进制文件复制到可执行路径:将 Minishift 二进制文件复制到您的本地可执行路径将确保可以在任何上下文中从命令行执行 minishift 命令。在 Linux 中,常见的路径位置是 /usr/local/bin
aric@local:~minishift/minishift-1.7.0-linux-amd64$ sudo cp minishift /usr/local/bin

您可能需要检查二进制文件的权限,以确保它们设置为可执行,例如,chmod +x /usr/local/bin/minishift

  1. 验证安装:执行 minishift version 命令应该返回相关的 Minishift 版本信息,在此例中为 1.7.0:
aric@local:~minishift$ minishift version
minishift v1.7.0+1549135
  1. 下载 OC 二进制文件:OC 可以在以下网址下载适用于 Windows、macOS 或 Linux 的版本:mirror.openshift.com/pub/openshift-v3/clients/3.6.173.0.5/。在本示例中,我们将使用版本 3.6 的 OC 客户端。与 MiniShift 类似,可能自编写时以来已经发布了更新版本。为了与示例最大兼容,建议您使用版本 3.6:
aric@local:~/minishift$ wget https://mirror.openshift.com/pub/openshift-v3/clients/3.6.173.0.5/linux/oc.tar.gz
--2017-10-09 20:03:57--  https://mirror.openshift.com/pub/openshift-v3/clients/3.6.173.0.5/linux/oc.tar.gz
Resolving mirror.openshift.com (mirror.openshift.com)... 54.173.18.88, 54.172.163.83, 54.172.173.155
'oc.tar.gz' saved [36147137/36147137]
  1. 解压 OC 客户端归档:解压此归档后,会提取出一个二进制文件oc。这是 OC 的可执行二进制文件:
aric@local:~/minishift$ tar -xvf oc.tar.gz
oc
  1. 将 OC 复制到可执行路径:与 Minishift 安装类似,我们将 OC 二进制文件复制到可执行路径位置:
aric@local~/minishift$ sudo cp oc /usr/local/bin
  1. 验证安装:执行oc version命令以确保 OC 已成功安装并返回相关版本信息:
aric@local:~/minishift$ oc version
oc v3.6.173.0.5
  1. 启动 Minishift 集群:现在 Minishift 和 OC 已经安装完成,我们通过minishift start命令启动 Minishift 集群。默认情况下,Minishift 将期望使用 KVM 虚拟化程序,并分配大约 2 GB 的内存。我们将稍作修改,改为使用 VirtualBox 虚拟化程序、8 GB 的 RAM 和 50 GB 的存储。一旦 Minishift 集群启动,它将返回一个 URL,您可以通过这个 URL 访问 OpenShift 控制台:
aric@local:~$ minishift start --vm-driver=virtualbox --disk-size=50GB --memory=8GB
-- Starting local OpenShift cluster using 'virtualbox' hypervisor ...
-- Minishift VM will be configured with ...
   Memory:    8 GB
   vCPUs :    2
   Disk size: 50 GB

   Downloading ISO 'https://github.com/minishift/minishift-b2d-iso/releases/download/v1.2.0/minishift-b2d.iso'
 40.00 MiB / 40.00 MiB [=========================================================================================================================================================================] 100.00% 0s
-- Starting Minishift VM ................................. OK
-- Checking for IP address ... OK
-- Checking if external host is reachable from the Minishift VM ...
   Pinging 8.8.8.8 ... OK
-- Checking HTTP connectivity from the VM ...
   Retrieving http://minishift.io/index.html ... OK
-- Checking if persistent storage volume is mounted ... OK
-- Checking available disk space ... 0% used OK
-- Downloading OpenShift binary 'oc' version 'v3.6.0'
 34.72 MiB / 34.72 MiB [=========================================================================================================================================================================] 100.00% 0s-- Downloading OpenShift v3.6.0 checksums ... OK
-- OpenShift cluster will be configured with ...
   Version: v3.6.0
-- Checking `oc` support for startup flags ...
   host-config-dir ... OK
   host-data-dir ... OK
   host-pv-dir ... OK
   host-volumes-dir ... OK
   routing-suffix ... OK
Starting OpenShift using openshift/origin:v3.6.0 ...
Pulling image openshift/origin:v3.6.0
Pulled 1/4 layers, 26% complete
Pulled 2/4 layers, 71% complete
Pulled 3/4 layers, 90% complete
Pulled 4/4 layers, 100% complete
Extracting
Image pull complete
OpenShift server started.

The server is accessible via web console at:
    https://192.168.99.100:8443

You are logged in as:
    User:     developer
    Password: <any value>

To login as administrator:
    oc login -u system:admin

如果您的资源不足以为 Minishift 部署分配 8 GB 的 RAM,大多数示例可以使用默认的 2 GB RAM 运行。

  1. 放宽默认安全权限:在后台,OpenShift 是一个高度安全的 Kubernetes 集群,不允许容器运行本地 root 用户。在我们深入了解新的 Minishift 安装之前,我们需要首先放宽默认的安全上下文约束,以便我们可以运行任何 Docker 镜像。由于这是一个开发环境,这将为我们提供更多的自由来探索平台并运行不同的工作负载。为此,我们将使用 OC 以系统管理员身份登录。从那里,我们可以使用oc adm命令将anyuid安全上下文约束添加到所有经过身份验证的 OpenShift 用户:
aric@local:~/minishift$ oc login -u system:admin
Logged into "https://192.168.99.100:8443" as "system:admin" using existing credentials.

You have access to the following projects and can switch between them with 'oc project <projectname>':

    default
    kube-public
    kube-system
  * myproject
    openshift
    openshift-infra

Using project "myproject".

aric@local:~minishift$ oc adm policy add-scc-to-group anyuid system:authenticated

在生产部署中,最好不要修改 OpenShift 安全上下文约束。容器镜像应始终以自己的用户身份在 Docker 容器内运行。遗憾的事实是,许多开发人员使用默认的 root 用户来构建和部署应用程序。我们放宽安全权限仅仅是为了让我们可以更自由地探索平台,而不受仅能运行由专用用户构建和运行的容器的限制。

当您完成在 Minishift 环境中的工作时,请确保使用minishift stop命令停止 Minishift 虚拟机,如下所示。与销毁本地 Vagrant 实验室不同,Minishift 实例将在下次启动虚拟机时保留运行中的部署和服务工件:

aric@local:~/minishift$ minishift stop
Stopping local OpenShift cluster...
Cluster stopped.

使用网页界面部署容器

如你所见,当 minishift start 命令完成后,它提供了一个可以用来访问网页用户界面的 IP 地址。使用 OpenShift 相较于标准 Kubernetes 的最大优势之一是,OpenShift 通过直观的网页界面暴露了 Kubernetes 几乎所有的核心功能。OpenShift 控制台的工作方式类似于你过去使用过的其他云或服务仪表板。你可以一眼看出哪些部署正在运行、由于 pod 失败而触发的警报,甚至是其他用户在项目中触发的新部署。要访问网页界面,只需将 minishift start 命令输出中的 IP 地址复制并粘贴到你本地机器上任何现代网页浏览器中。然后,你可能需要接受 Minishift 默认提供的自签名 SSL 证书,之后你将看到类似以下截图的登录界面:

图 1:OpenShift 登录页面

访问 Minishift 的默认凭据是用户名 developer 和你选择的任何密码。你输入的密码并不重要,因为每次以开发者用户身份进行身份验证时,你都可以随便输入任何密码。成功登录后,你将被要求访问一个项目。Minishift 为你提供的默认项目名为 My Project。为了简单起见,我们将在接下来的实验示例中使用这个项目,你可以跟着一起操作。

网页界面由屏幕左侧和顶部的两个主要导航栏布局,而界面的中央部分则保留用于显示你当前访问的环境的详细信息、修改设置或删除资源:

图 2:初始 OpenShift 项目

现在你已经熟悉了 OpenShift 用户界面,让我们创建一个部署,并查看当 pod 在集群中运行时的样子。创建新 pod 和部署的功能可以在屏幕顶部通过选择“添加到项目”下拉框找到:

图 3:向项目添加服务

我们可以通过多种不同方式创建新的部署。OpenShift 提供的默认选项是浏览预构建的镜像和服务目录、基于容器注册表 URL 部署镜像,或导入描述我们构建的服务的 YAML 或 JSON 清单。我们可以在 Web 界面中部署一个目录中的服务。选择“添加到项目”下拉菜单中的“浏览目录”选项,将打开 OpenShift 目录的界面。目录中找到的任何 OpenShift 示例都能在 OpenShift 中良好运行,但为了演示,我们部署一个简单 Python 应用程序的框架。为此,点击 Python 选项卡,然后选择 Python 3.5 源代码应用程序:

图 4:从 OpenShift 目录创建一个简单的 Python 服务

在下一个屏幕上,OpenShift 会提示你选择部署应用程序的选项,包括一个包含 Python 源代码的源代码仓库和一个应用程序名称。你可以为 Python 应用程序选择任何名称。在这个示例中,我将它命名为 oc-test-deployment。由于我们没有预先开发好的 Python 应用程序,可以点击 Git 仓库 URL 文本框下方的“尝试它”链接,使用 OpenShift 提供的示例应用程序:

图 5:修改 Python 应用程序的属性

如果你是 Python 开发者,且希望部署一个 Django 应用程序,可以随意使用另一个 Git 仓库来替代示例仓库!

点击蓝色的“创建”按钮将启动部署并启动容器。根据你工作站的规格,服务完全部署可能需要一段时间。在部署过程中,你可以通过点击用户界面的各个页面来观察部署的不同阶段。例如,点击侧边栏中的 Pods,将显示 Pods 在创建过程中所经历的各个阶段,直到它们在 Kubernetes 中可用。OpenShift 会显示描述正在运行的资源状态的圆形图。健康、响应请求并按预期运行的 Pods 会以蓝色圆圈显示。而其他可能没有按预期运行、抛出错误或警告的 Kubernetes 资源,则会用黄色或红色圆圈表示。这提供了一种直观的方式,让你一目了然地了解服务的运行状态:

图 6:成功创建的测试 Python 应用程序

一旦服务完全部署,OpenShift 会提供一个链接以访问该服务。在 OpenShift 术语中,这被称为 路由。路由的功能类似于 Kubernetes 中暴露的服务,不同之处在于,它们默认使用 nip.io DNS 转发。你可能会注意到,指向我们刚刚创建的服务的路由,其完全限定的域名是 servicename.ipaddress.nip.io。这为用户提供了可路由的访问路径,以便在不需要配置外部负载均衡器或服务代理的情况下访问 Kubernetes 集群。通过 Web 浏览器访问该 URL 应该会打开一个类似于以下内容的页面:

图 7:Python 应用程序测试页面

这是此简单示例应用程序的默认 Python-Django 索引页面。如果我们点击返回 OpenShift 控制台,我们可以更详细地了解在此部署中运行的 pod。与 kubectl 类似,OpenShift 可以为我们提供有关部署的详细信息,包括运行中的 pod、日志事件,甚至允许我们从 Web 用户界面自定义部署。要查看这些详细信息,选择应用程序 | 部署并点击你希望查看的部署。在此案例中,我们将查看我们唯一运行的部署 oc-test-deployment 的详细信息:

图 8:OpenShift 部署页面

从此页面,我们可以查看容器历史、修改配置、检查或更改环境变量,并查看来自部署的最新事件日志。在屏幕的右上角,操作下拉框为我们提供了更多选项,可以添加存储、自动扩展规则,甚至修改用于部署该服务的 Kubernetes 清单:

图 9:管理 OpenShift 应用程序和部署

OpenShift 提供了一个非常好的界面,用于实时调整清单并实验更改。OpenShift 用户界面会反馈你所做的更改,并在出现潜在问题时提醒你。

有关运行中 pod 的信息也可以通过 Web 用户界面访问。从应用程序菜单中选择 Pods,并点击你希望查看的 pod。在这种情况下,我们有一个正在运行的 pod,oc-test-deployment-1-l18l8

图 10:查看应用程序部署中的 pod

在此页面上,我们可以查看关于运行在任何部署中的 pod 的几乎所有相关细节。从此界面,你可以查看环境配置、实时访问容器日志,甚至通过一个完全功能的终端模拟器登录到容器:

图 11:在 OpenShift 中查看 pod 特定的详细信息

类似于部署菜单,我们也可以从这个页面的操作下拉菜单中选择,修改 YAML 编辑器中的容器设置,或将存储卷挂载到容器内。

最后,通过使用 OpenShift 的 Web 界面,我们可以删除部署和 Pod。由于 OpenShift 本质上是运行在 Kubernetes 之上的一层,我们需要应用许多相同的概念。为了删除 Pod,我们必须首先删除部署,以便在 Kubernetes API 中设置新的期望状态。在 OpenShift 中,这可以通过选择应用程序 | 部署 | 你的部署(oc-test-deployment)来完成。从操作下拉菜单中,选择删除部署。OpenShift 会提示你确认是否真想执行此操作;点击删除以最终确定操作:

图 12:在 OpenShift 中删除部署

以类似的方式,你需要前往应用程序 | 服务,然后前往应用程序 | 路由,以删除 OpenShift 为该服务创建的服务和路由。完成后,点击左侧菜单栏中的概览按钮所产生的屏幕将再次为空,显示当前在 OpenShift 集群中没有任何运行的内容。

OpenShift Web 用户界面 提示

上述示例引导用户完成了一些主要的 OpenShift 和 Kubernetes 工作流步骤,包括创建新部署、管理部署,最终删除部署和其他资源。OpenShift 通过 Web 用户界面暴露了比本书所能深入探讨的更多功能;我建议你花时间自己探索 Web 界面,真正熟悉 OpenShift 提供的功能。为了避免单调,我提供了一些关键功能,供你在 OpenShift Web 界面中留意:

  • 概览仪表板:概览仪表板可以通过屏幕左侧的导航栏访问。概览仪表板显示关于 OpenShift 集群内最近活动的信息。它对于访问最新的部署并能单击访问各种集群资源非常有用。

  • 应用程序菜单:应用程序菜单是查看或修改 OpenShift 集群中运行的任何部署或 Pod 的单一位置。在应用程序中,你可以访问与部署、Pod、有状态集合、服务和路由相关的信息。可以将应用程序菜单视为与集群中运行的容器配置相关的一站式入口。

  • 构建仪表板:构建仪表板展示了一个轻量的 持续集成 持续交付 (CICD) 工作流,用于 Kubernetes。这对于触发镜像构建、建立 Jenkins 启用的工作流管道以及将自动化集成到 OpenShift 项目中非常有用。

  • 资源菜单:资源菜单主要用于定义配额和用户帐户权限,管理 OpenShift 集群内用户和项目的访问权限及限制。此处还定义了一个轻量级的机密存储界面,以及配置映射选项,用于定义 OpenShift 项目内容器的配置。

  • 存储仪表盘:存储仪表盘用于显示有关容器和部署所使用的持久卷声明的信息,卷声明存储在 OpenShift 所运行的底层硬件或虚拟机上。可以通过该网页界面部分创建或删除卷声明,也可以根据新的或变化的需求进行管理或修改。

  • 监控仪表盘:最后,监控仪表盘为用户提供有关运行中 Pods 的详细信息、触发的事件,以及与事件相关的环境变化的历史背景。监控可以轻松与构建管道结合使用,甚至用于报告已配置的服务健康检查。

利用 OpenShift 提供的强大工具套件,有助于抽象和简化我们在第五章《使用 Kubernetes 进行大规模容器管理》中学到的许多 Kubernetes 概念。

OpenShift CLI 简介

用户与 OpenShift 平台交互的第二种主要方式是通过 OpenShift 命令行接口,简称 OC(OpenShift 客户端)。OpenShift 命令行接口使用了我们在第五章《使用 Kubernetes 进行大规模容器管理》中探索的许多相同概念,利用kubectl命令行接口来管理 pods、部署和暴露服务。然而,OpenShift 客户端还支持许多特定于 OpenShift 的功能,例如创建路由以暴露部署和使用集成的安全上下文约束进行访问控制。在本节中,我们将展示如何使用 OC 完成一些基本的工作流概念,这些概念在通过网页用户界面时已经有所探索,例如创建部署、创建路由以及操作运行中的容器。最后,我们将探讨一些 OC 的深入使用技巧以及一些可用的高级功能。在继续之前,请确保已安装 OC(有关安装详情,请参阅本章开头部分):

  1. 登录到 OpenShift:与 Web 用户界面类似,我们可以使用 CLI 通过 oc login 命令登录到本地 OpenShift 集群。此命令的基本语法为 oc login URL:PORT,其中用户需要将 URL 和端口替换为他们登录的 OpenShift 环境的 URL 和端口号。登录成功后,提示符将返回 Login Successful,并授予你访问默认项目的权限。在此情况下,我们将使用 developer 用户名和任意密码登录到 192.168.99.100 本地环境:
aric@local:~$ oc login https://192.168.99.100:8443
Authentication required for https://192.168.99.100:8443 (openshift)
Username: developer
Password:
Login successful.

You have one project on this server: "myproject"

Using project "myproject".
  1. 使用 OC status 检查状态oc status 命令的使用方式与 Web 用户界面中的概览仪表盘相似,用于显示环境中部署的关键服务、正在运行的 pod 或集群中可能触发警报的任何内容。仅输入 oc status 不会返回任何内容,因为我们删除了通过 Web 用户界面创建的部署、路由和服务:
aric@local:~$ oc status
In project My Project (myproject) on server https://192.168.99.100:8443

You have no services, deployment configs, or build configs.
Run 'oc new-app' to create an application.
  1. 创建 OpenShift 部署:可以使用 oc create 命令轻松创建部署和其他集群资源。与 kubectl 类似,你可以通过使用 oc create deployment 命令并引用你希望使用的容器镜像名称来创建部署。需要注意的是,部署名称对于使用下划线和连字符等特殊字符非常敏感。为了简化操作,让我们重新创建第五章中的示例,大规模容器与 Kubernetes,并使用官方 NGINX Docker 镜像通过 oc create 命令创建一个简单的 NGINX pod,并指定对象为 deployment
aric@local:~$ oc create deployment webserver --image=nginx
deployment "webserver" created

另一个与 kubectl 相似之处是,OpenShift 支持使用 -f 选项基于 Kubernetes 清单文件创建部署。

  1. 列出 pod 并查看 OC 状态:现在我们在 OpenShift 集群中有一个正在运行的部署和 pod,我们可以使用 oc get pods 命令查看正在运行的 pod,并检查 oc status 命令的输出,以查看集群中正在运行的 pod 概览:
aric@local:~/Development/minishift$ oc get pods
NAME                         READY     STATUS    RESTARTS   AGE
webserver-1266346274-m2jvd   1/1       Running   0          9m

aric@local:~/Development/minishift$ oc status
In project My Project (myproject) on server https://192.168.99.100:8443

pod/webserver-1266346274-m2jvd runs nginx

You have no services, deployment configs, or build configs.
Run 'oc new-app' to create an application.
  1. 使用 oc describe 查看详细输出:除了简单列出在 OpenShift 集群中创建并可用的对象外,还可以使用 oc describe 命令查看有关特定对象的详细信息。与 kubectl describe 类似,oc describe 允许我们查看集群中几乎所有定义对象的相关详细信息。例如,我们可以使用 oc describe deployment 命令查看刚才创建的 Web 服务器部署的详细信息:
aric@local:~/Development/minishift$ oc describe deployment webserver
Name:                   webserver
Namespace:              myproject
CreationTimestamp:      Sun, 15 Oct 2017 15:17:30 -0400
Labels:                 app=webserver
Annotations:            deployment.kubernetes.io/revision=1
Selector:               app=webserver
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
Pod Template:
  Labels:       app=webserver
  Containers:
   nginx:
    Image:              nginx
    Port:
    Environment:        <none>
    Mounts:             <none>
  Volumes:              <none>
Conditions:
  Type          Status  Reason
  ----          ------  ------
  Available     True    MinimumReplicasAvailable
OldReplicaSets: <none>
NewReplicaSet:  webserver-1266346274 (1/1 replicas created)
Events:
  FirstSeen     LastSeen        Count   From                    SubObjectPath   Type            Reason                  Message
  ---------     --------        -----   ----                    -------------   --------        ------                  -------
  11m           11m             1       deployment-controller Normal ScalingReplicaSet       Scaled up replica set webserver-1266346274 to 1
  1. 创建一个 OpenShift 服务:为了暴露正在 OpenShift 集群中运行的 pods,我们必须首先创建一个服务。你可能还记得在第五章中提到的使用 Kubernetes 扩展容器,Kubernetes 服务是一种抽象,它的工作方式类似于 Kubernetes 集群内的内部负载均衡器。基本上,我们是创建一个单一的内部(或外部)IP 地址,其他 pods 或服务可以通过该 IP 地址访问任何符合给定选择器的 pod。在 OpenShift 中,我们将创建一个名为 webserver 的服务,它将使用内部路由的集群 IP 地址来拦截 Web 服务器流量并将其转发到我们在部署过程中创建的 Web 服务器 pod。通过将服务命名为 webserver,它将默认使用匹配标签 app=webserver 的选择器标准。这个标签是在我们创建 OpenShift 部署时默认创建的。可以创建任意数量的标签或选择器标准,这使得 Kubernetes 和 OpenShift 可以选择 pod 来进行流量负载均衡。为了这个示例,我们将使用内部集群 IP,并基于命名我们的服务与我们命名部署相同的名称来映射选择器标准。最后,我们将选择从外部转发到服务的端口,并映射到容器内服务监听的端口。为了简化操作,我们将把目标端口 80 的流量转发到 pod 的 80 端口:
aric@local:~$ oc create service clusterip webserver --tcp=80:80
service "webserver" created

我们可以使用 oc get services 命令检查服务配置。我们可以看到我们的服务已经创建,内部路由的集群地址是 172.30.136.131。你的地址很可能会不同,因为这些地址是从 Kubernetes 中的 CNI 子网中获取的:

aric@lemur:~$ oc get services
NAME        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
webserver   172.30.136.131   <none>        80/TCP    18m
  1. 创建一个路由以启用访问:最后,我们可以使用 oc expose 命令创建一个路由,以便访问我们的服务,后面跟上我们要暴露的服务名称(webserver)。为了使其能够从我们的工作站进行路由,OpenShift 使用基于虚拟机 IP 地址的 nip.io DNS 转发。我们可以通过指定 --hostname 参数来启用此功能,该参数后跟我们希望服务被访问时的名称,最后加上虚拟机的 IP 地址,并以后缀 nip.io 结尾:
aric@local:~$ oc expose service webserver --hostname="awesomewebapp.192.168.99.100.nip.io"
route "webserver" exposed

执行 oc get routes 命令将显示我们刚刚创建的路由:

aric@local:~$ oc get routes
NAME        HOST/PORT                            PATH      SERVICES    PORT      TERMINATION   WILDCARD
webserver   awesomewebapp.192.168.99.100.nip.io             webserver   80-80 None

为了确保路由正常工作,我们可以使用 Web 浏览器并导航到我们为路由分配的转发 DNS 地址。如果一切正常,我们将能够看到 NGINX 欢迎屏幕:

随时可以继续使用本地 Minishift 集群部署更复杂的容器化应用程序。当你完成后,确保使用 minishift stop 命令停止 Minishift 实例。

OpenShift 和 Ansible 容器

正如我们在本章中所看到的,OpenShift 是一个丰富的平台,提供了 Kubernetes 之上的有价值的抽象。因此,Ansible Container 提供了充足的支持,可以通过 OpenShift 部署和运行容器化应用程序的生命周期。由于 OpenShift 和 Ansible Container 都是同一母公司 Red Hat 的产品,因此显然 OpenShift 和 Ansible Container 具有出色的兼容性。到目前为止,我们主要讨论了如何使用 Ansible Container 构建容器,并在 Docker 主机上本地运行它们。

现在,我们已经拥有了理解 Kubernetes 和 OpenShift 的坚实基础,可以将迄今为止获得的知识与 Ansible Container 相结合,学习如何将 Ansible Container 作为一个真正的端到端生产就绪部署和生命周期管理解决方案来使用。接下来的内容将变得更加有趣!

参考文献

摘要

像 Kubernetes 和 OpenShift 这样的容器编排平台正在被组织迅速采用,以简化扩展应用程序、部署更新并确保最大可靠性的复杂过程。随着这些平台的日益流行,我们更需要理解采用这些技术的影响,以支持这些技术带来的组织和文化思维转变。

OpenShift 是建立在 Red Hat 分发版 Kubernetes 之上的平台,旨在提供最佳的 Kubernetes 使用体验。在本章开始时,我们了解了 OpenShift 是什么,以及 Red Hat 正在通过 OpenShift 平台交付的各种功能。接下来,我们学习了如何安装 Minishift 项目,这是一个面向开发者的解决方案,用于在本地部署 OpenShift。

一旦我们安装并本地运行了 Minishift,我们就学会了如何通过 Minishift 的 Web 用户界面运行 pods、deployments、services 和 routes。最后,我们了解了 OpenShift 命令行界面 OC,并学习了它如何类似于 kubectl,提供对 OpenShift 的 CLI 访问以及 OpenShift 在 Kubernetes 之上构建的创新功能。

在下一章中,我的目标是将我们对 OpenShift 和 Kubernetes 的知识与 Ansible Container 相结合,学习 Ansible Container 工作流中的最后一步——部署。部署功能使 Ansible Container 成为一个真正强大的工具,不仅可以构建和测试容器镜像,还可以将它们部署到运行 Kubernetes 和 OpenShift 的容器化生产环境中。

第七章:部署你的第一个项目

迄今为止,在本书中,我们已经研究了如何使用 Ansible Container 工作流运行构建和容器的各种方式。我们了解了如何在本地 Docker 守护进程中运行容器,将构建的容器推送到远程 Docker 镜像仓库,管理容器镜像,甚至使用 Kubernetes 和 OpenShift 等容器编排工具在大规模上运行容器。我们几乎已经展示了 Ansible Container 的全部功能,并演示了如何将其作为一个完整的工具,用于在应用程序生命周期中构建、运行和测试容器镜像。

然而,Ansible Container 工作流中有一个方面我们还没有深入研究。在之前的章节中,我们提到了 deploy 命令,以及如何利用 deploy 在生产环境或远程系统中运行容器。现在,我们已经涵盖了 Docker、Kubernetes 和 OpenShift 的许多基础知识,是时候将注意力转向 Ansible Container 工作流的最后一个组件:ansible-container deploy。我的目标是,通过阅读本章并跟随示例,读者将清楚地意识到 Ansible Container 不仅仅是一个用来本地构建和运行容器镜像的工具。它是一个强大的工具,可以在各种流行的容器平台上进行复杂的容器化应用部署。

本章将涵盖以下主题:

  • ansible-container deploy 概述

  • 将容器部署到 Kubernetes

  • 将容器部署到 OpenShift

ansible-container deploy 概述

ansible-container deploy 命令是 Ansible Container 工作流的一个组件,负责,如你所猜测的那样,将容器部署到远程容器服务引擎。在写本文时,这些引擎包括 Docker、Kubernetes 和 OpenShift。通过利用 container.yml 文件中的配置,Ansible Container 能够验证这些服务并通过 API 调用启动容器,按照用户指定的配置进行部署。使用 Ansible Container 进行部署是一个两步过程。首先,Ansible Container 将构建好的容器镜像推送到远程镜像仓库,类似于 Docker Hub 或 Quay.io。这使得远程容器运行时服务能够在部署过程中访问容器。第二,Ansible Container 生成可以在本地执行的部署剧本,并使用 ansible-container run 命令执行部署。刚开始时,部署过程可能会有些混乱。下面的流程图展示了在本地构建并运行项目后的部署过程:

图 1:ansible-container deploy 工作流

我们将从使用简单的 NGINX 容器项目的示例开始。稍后,我们将查看使用我们在第四章中构建的 MariaDB 项目将其部署到 Kubernetes 和 OpenShift 的示例,角色中有什么?

ansible-container deploy

在我们开始查看ansible-container deploy之前,首先让我们重新构建之前创建的 NGINX 项目。在您的 Ubuntu Vagrant 实验室虚拟机中,导航到/vagrant/AnsibleContainer/nginx_demo目录;或者,如果您在其他目录中自行构建了这个示例,请导航到该目录并运行ansible-container build命令。这将确保实验室虚拟机有一个项目的全新构建:

ubuntu@node01:/vagrant/AnsibleContainer/nginx_demo$ ansible-container build 
 Building Docker Engine context...                                                                                                              
Starting Docker build of Ansible Container Conductor image (please be patient)...                                                              
Parsing conductor CLI args.                                                                                                                    
Docker™ daemon integration engine loaded. Build starting.       project=nginx_demo                                                             
Building service...     project=nginx_demo service=webserver                                                                                   

PLAY [webserver] ***************************************************************                                                               

TASK [Gathering Facts] *********************************************************                                                               
ok: [webserver]                                                                                                                                

TASK [ansible.nginx-container : Install epel-release] **************************                                                               
changed: [webserver]

TASK [ansible.nginx-container : Install nginx] *********************************
changed: [webserver] => (item=[u'nginx', u'rsync'])

TASK [ansible.nginx-container : Install dumb init] *****************************
changed: [webserver]

TASK [ansible.nginx-container : Update nginx user] *****************************
changed: [webserver]

TASK [ansible.nginx-container : Put nginx config] ******************************
changed: [webserver]

您可以通过运行docker images命令来验证项目是否已成功构建并且容器镜像已被缓存:

ubuntu@node01:/vagrant/AnsibleContainer/nginx_demo$ docker images 
REPOSITORY  TAG   IMAGE ID  CREATED  SIZE
aric49/nginx_demo-webserver 20171022202358 09f7b7cc3e3e 10 minutes ago  268MB

现在,我们已经在本地缓存了容器,我们可以使用ansible-container deploy命令来模拟项目部署。如果不提供关于将容器部署到哪个引擎的任何参数,ansible-container deploy将生成可用于将我们的项目部署到运行 Docker 的本地或远程主机上的剧本。它还会将我们的项目推送到在项目root目录中找到的container.yml文件中配置的注册表。由于deployansible-container push使用了许多相同的功能,我们将向deploy提供与push命令相同的标志,关于容器镜像注册表。在这种情况下,我们将告诉它将容器推送到我们的 Docker Hub 注册表,因为我们将提供帐户的用户名以及任何我们希望用来区分该版本容器与以前版本的标签。为了演示目的,我们将使用deploy标签:

ubuntu@node01:/vagrant/AnsibleContainer/nginx_demo$ ansible-container deploy --push-to docker --username aric49 --tag deploy 
Enter password for aric49 at Docker Hub:                                                                                                       
Parsing conductor CLI args.                                                                                                                    
Engine integration loaded. Preparing push.      engine=Docker™ daemon                                                                          
Tagging aric49/nginx_demo-webserver                                                                                                            
Pushing aric49/nginx_demo-webserver:deploy...                                                                                          
The push refers to a repository [docker.io/aric49/nginx_demo-webserver]                                                                        
Preparing                                                                                                                                      
Pushing                                                                                                                                        
Pushed                                                                                                                                         
Pushing                                                                                                                                        
Pushed                                                                                                                                         
20171022202358: digest: sha256:74948d56b3289009a6329c0c2035e3217d0e83479dfaee3da3d8ae6444b04165 size: 741                                      
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=4c7c43d090654e62869185458434941cb7718e257eeed80f03e846d460eae24f save_contain
er=False                                                                                                                                       
Parsing conductor CLI args.                                                                                                                    
Engine integration loaded. Preparing deploy.    engine=Docker™ daemon                                                                          
Verifying image for webserver                                                                                                                  
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=acf8f1ec2adc821c33b2a341bc1404346b6d41f6ef18de5fb8dce7f98ddaea3f save_container=False                                                                                                                         

部署过程与推送过程类似,会提示您输入 Docker Hub 帐户的密码。在成功认证后,它将把您的容器镜像层推送到容器镜像注册表。到目前为止,这看起来与推送过程完全相同。然而,您可能会注意到,在项目的root目录中,现在存在一个名为ansible-deployment的新目录。在这个目录中,您将找到一个与您的项目同名的 Ansible 剧本,名为nginx_demo。以下是该剧本的示例:

  - name: Deploy nginx_demo
    hosts: localhost
    gather_facts: false
    tasks:
      - docker_service:
            definition:
                services: &id001
                    webserver:
                        image: docker.io/aric49/nginx_demo-webserver:deploy
                        command: [/usr/bin/dumb-init, nginx, -c, /etc/nginx/nginx.conf]
                        ports:
                          - 80:8000
                        user: nginx
                version: '2'
            state: present
            project_name: nginx_demo
        tags:
          - start
      - docker_service:
            definition:
                services: *id001
                version: '2'
            state: present
            project_name: nginx_demo
            restarted: true
        tags:
          - restart
TRUNCATED

您可能需要确保image行反映了以下格式的镜像路径,docker.io/username/containername:tag,因为某些版本的 Ansible Container 会在剧本中提供错误的路径。如果是这种情况,只需在文本编辑器中修改剧本。

部署 playbook 通过调用运行在目标主机上的 docker_service 模块来工作。默认情况下,容器使用 localhost 作为部署的目标主机。然而,你可以轻松地提供一个标准的 Ansible 清单文件,让这个项目在远程主机上运行。部署 playbook 支持完整的 Docker 生命周期应用管理,例如启动容器、重启容器,最终通过提供一系列 playbook 标签有条件地执行所需功能来销毁项目。你会发现 playbook 继承了我们在 container.yml 文件中配置的许多设置。Ansible Container 使用这些设置,使得 playbooks 可以独立于项目本身执行。

由于我们已经在本书中通过使用 ansible-container run 运行本地容器,接下来让我们尝试直接执行 playbook 启动容器。这模拟了如果你希望手动运行部署而不依赖 Ansible Container 工作流时的相同过程。可以通过使用 ansible-playbook 命令并传递 start 标签来在本地主机上部署项目。你会发现这个过程和运行 ansible-container run 命令的过程完全相同。需要注意的是,任何核心的 Ansible Container 功能(如 run、restart、stop 和 destroy)都可以通过直接运行 playbooks,并根据你想要实现的功能传递相应的标签来独立执行,而不依赖 Ansible Container:

ubuntu@node01:$ ansible-playbook nginx_demo.yml --tags start 
[WARNING]: Host file not found: /etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available

PLAY [Deploy nginx_demo] ************************************************

TASK [docker_service] ************************************************
changed: [localhost]

PLAY RECAP *************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

一旦 playbook 执行完成,PLAY RECAP 会显示一个任务已在你的本地主机上执行了更改。你可以执行 docker ps -a 命令确认项目已成功部署:

ubuntu@node01:$ docker ps -a 
CONTAINER ID  IMAGE   COMMAND  CREATED   STATUS  PORTS  NAMES
4b4b3b032c61        aric49/nginx_demo-webserver:deploy   "/usr/bin/dumb-ini..."   5 minutes ago       Up 5 minutes        0.0.0.0:80->8000/tcp   nginxdemo_webserver_1

以类似的方式,我们可以再次运行此 playbook,传递 --restart 标签以重新启动 Docker 主机上的容器。执行 playbook 第二次后,你应该会看到一个任务再次发生了变化,表示容器已被重新启动。这模拟了 ansible-container restart 命令的功能。docker ps -a 中的 status 列将显示容器在执行重启后只运行了几秒钟:

ubuntu@node01:$ docker ps -a 
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
4b4b3b032c61        aric49/nginx_demo-webserver:deploy "/usr/bin/dumb-ini..."   7 minutes ago       Up 2 seconds 0.0.0.0:80->8000/tcp   nginxdemo_webserver_1

stop 标签可以传递给 ansible-playbook 命令,以暂时停止正在运行的容器。与 restart 类似,docker ps -a 输出将显示容器处于 exit 状态:

ubuntu@node01:$ ansible-playbook nginx_demo.yml --tags stop 
[WARNING]: Host file not found: /etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available

PLAY [Deploy nginx_demo] *************************************

TASK [docker_service] **************************************************************
changed: [localhost]

TRUNCATED

PLAY RECAP **************************************************
localhost: ok=4    changed=2    unreachable=0    failed=0 

现在我们可以使用 docker ps -a 命令检查状态:

ubuntu@node01:$ docker ps -a 
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
4b4b3b032c61        aric49/nginx_demo-webserver:deploy   "/usr/bin/dumb-ini..."   11 minutes ago      Exited (0) 2 minutes ago                       nginxdemo_webserver_1

最后,可以通过传递 destroy 标签将项目从 Docker 主机上完全移除。运行这个 playbook 标签会在 playbook 中执行更多的步骤,但最终会将项目的所有痕迹从主机上删除:

ubuntu@node01:$ ansible-playbook nginx_demo.yml --tags destroy 
[WARNING]: Host file not found: /etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available

PLAY [Deploy nginx_demo] *************************************

TASK [docker_service] **************************************************************
changed: [localhost]

TASK [docker_image] **************************************************************
ok: [localhost]

TASK [docker_image] **************************************************************

TASK [docker_image] **************************************************************
ok: [localhost]

TRUNCATED

PLAY RECAP **************************************************
localhost: ok=7    changed=2    unreachable=0    failed=0

在幕后,当执行任何核心 Ansible Container 命令时,它们本质上是围绕生成的项目中的相同 playbook 的包装器。本章这一部分的目的是向读者展示如何使用 Ansible Container 在本地部署项目的整体流程,并在课程中更深入地讲解这些技能。部署真正变得有趣的是当使用 Kubernetes 和 OpenShift 作为目标部署引擎时。通过使用 Ansible Container 工作流命令与相应的容器平台引擎,我们可以直接使用 Ansible Container 工作流命令管理容器化部署,而不是直接执行 playbooks。

部署容器到 Kubernetes

使 Ansible Container 工作流如此灵活且吸引组织和个人采用 Ansible 作为使用 Kubernetes 和 OpenShift 进行远程部署的原生支持的一大原因之一。在本节中,我们将使用ansible-container deploy命令将容器部署到我们在第五章中创建的 Google Cloud Kubernetes 集群,Kubernetes 下的容器规模管理

正如我们在上一节中讨论的那样,单独运行ansible-container deploy将默认将容器推送到你在container.yml文件中配置的任何镜像仓库,并在项目的root目录下生成一个名为ansible-deployment的新目录。在该目录中,将存在一个以项目名称命名的单个 YAML playbook 文件。这个 playbook 用于将容器部署到任何 Docker 主机,类似于ansible-container run命令。为了这个示例,我们将使用 Kubernetes 引擎运行ansible-container deploy,这样我们就可以利用 Ansible Container 作为 Kubernetes 的部署工具,自动创建服务定义和部署抽象。

为了使 Kubernetes 能够在 Ansible Container 中部署功能,我们将在项目的container.yml文件中添加几个新参数。具体来说,我们需要将项目指向 Kubernetes 认证配置文件,并定义容器在 Kubernetes 中的命名空间。作为示例,我将使用我们之前使用的 MariaDB 项目,但对container.yml文件做一些修改以支持 Kubernetes。作为参考,该项目可以在本书的官方 Git 仓库中找到,位于Kubernetes/mariadb_demo_k8s目录下。欢迎跟随示例,也可以修改现有的 MariaDB 项目以支持 Kubernetes。在设置部分,我们将添加k8s_namespacek8s_auth

Kubernetes namespace 将包含我们希望部署项目的集群中的命名空间名称,而 auth 部分将提供 Kubernetes 认证配置文件的路径。Kubernetes 认证配置的默认位置是 /home/user/.kube/config。如果你正在使用 Vagrant 实验室,Google Cloud SDK 会将该配置文件放置在 /home/ubuntu/.kube/config

然而,在开始之前,我们需要设置一个默认的访问令牌,以便 Ansible Container 可以访问 Google Cloud API。我们可以通过执行 gcloud auth application-default login 命令来做到这一点。执行此命令将为你提供一个超链接,允许你使用 Google 登录凭证授予对 Google Cloud API 的权限,类似于我们在第五章中做的,使用 Kubernetes 扩展容器。Google Cloud API 将为你提供一个令牌,你可以在命令行输入该令牌,从而生成一个位于 /home/ubuntu/.config/gcloud/application_default_credentials.json 的应用程序默认凭证文件:

ubuntu@node01:$ gcloud auth application-default login
Go to the following link in your browser:

    https://accounts.google.com/o/oauth2/TRUNCATED

Enter verification code: XXXXXXXXXXXXXXX

Credentials saved to file: [/home/ubuntu/.config/gcloud/application_default_credentials.json]

这些凭证将被任何请求访问 Google Cloud 资源的库使用,包括 Ansible Container。

仅在你使用 Google Cloud Kubernetes 集群时,才需要进行 Google Container Engineer 特定的步骤。如果你使用的是像 Minikube 这样的本地 Kubernetes 环境,可以跳过这些步骤。

现在我们已在 Google Cloud API 中设置了适当的权限,我们可以修改 MariaDB 项目的 container.yml 文件,以支持 Kubernetes 部署引擎:

version: "2"
settings:
  conductor_base: ubuntu:16.04
  project_name: mariadb-k8s
  roles_path:
    - ./roles/
  k8s_namespace:
    name: database
  k8s_auth:
    config_file: /home/ubuntu/.kube/config
services:
  mariadb-database:
    roles:
      - role: mariadb_role

registries:
  docker:
    url: https://index.docker.io/v1/
    namespace: username

你将注意到我们已做出以下更改以支持 Kubernetes 部署:

  • project_name:在这个示例中,我们在 settings 块中添加了一个名为 project_name 的字段。在本书中,我们允许我们的项目使用其构建目录的默认名称。由于 Kubernetes 对于定义服务和 Pod 使用的字符有限制,我们希望通过在 container.yml 文件中覆盖它们来确保项目名称中不包含非法字符。

  • k8s_namespace:这定义了我们将部署 Pod 的 Kubernetes 命名空间。如果将此部分留空,Ansible Container 将使用我们在本章早些时候的 NGINX 部署中使用的默认命名空间。在此示例中,我们将使用一个名为 database 的不同命名空间。

  • k8s_auth:这是我们指定 Kubernetes 认证配置文件位置的地方。在该文件中,Ansible Container 能够提取我们的 API 服务器的 IP 地址、用于在集群中创建资源的访问凭证,以及连接 Kubernetes 所需的 SSL 证书。

一旦这些更改被放入你的 container.yml 文件中,让我们开始构建项目:

ubuntu@node01:$ ansible-container build                                                                                                                                  
Building Docker Engine context...                                                                                                                                                                            
Starting Docker build of Ansible Container Conductor image (please be patient)...                                                                                                                            
Parsing conductor CLI args.                                                                                                                                                                                  
Docker™ daemon integration engine loaded. Build starting.       project=mariadb-k8s                                                                                                                          
Building service...     project=mariadb-k8s service=mariadb-database                                                                                                                                                                                                                                                                        

PLAY [mariadb-database] ********************************************************                                                                                                                             

TASK [Gathering Facts] *********************************************************                                                                                                                             
ok: [mariadb-database]                                                                                                                                                                                       

TASK [mariadb_role : Install Base Packages] ************************************                                                                                                                             
changed: [mariadb-database] => (item=[u'ca-certificates', u'apt-utils'])                                                                                                                                     

TASK [mariadb_role : Install dumb-init for Container Init System] 

一旦项目构建完成,我们可以使用ansible-container deploy命令,指定--engine标志来使用k8s,并提供我们在container.yml文件中配置的 Docker 镜像仓库的详细信息。为了实现版本分离,我们还可以使用kubernetes标签标记镜像版本,这样我们可以在仓库中将此版本区分开来。然后,Ansible Container 会将我们的镜像推送到 Docker Hub 仓库,并生成特定于 Kubernetes 的部署剧本:

K8s 是 Kubernetes 的简写,Kubernetes 由字母 K 和后面 8 个字母组成。在社区中,这通常被发音为凯-艾特斯

ubuntu@node01:$ ansible-container --engine k8s deploy --push-to docker --tag kubernetes
Parsing conductor CLI args.
Engine integration loaded. Preparing push.      engine=K8s
Tagging aric49/mariadb-k8s-mariadb-database
Pushing aric49/mariadb-k8s-mariadb-database:kubernetes...
The push refers to a repository [docker.io/aric49/mariadb-k8s-mariadb-database]
Preparing
Waiting
Layer already exists
Pushing
Pushed
Pushing
Pushed
kubernetes: digest: sha256:563ec4593945b13b481c03ab7813bb64c0dc1b7a1d1ae8c4b61b744574df2926 size: 1569
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=27fd42d3920deb12f8c81802f151e95931315f516e04307a3a682bd9638fdf49 save_container=False
Parsing conductor CLI args.
Engine integration loaded. Preparing deploy.    engine=K8s
Verifying image for mariadb-database
ansible-galaxy 2.5.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python2.7/dist-packages/ansible
  executable location = /usr/local/bin/ansible-galaxy
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
Using /etc/ansible/ansible.cfg as config file
Opened /root/.ansible_galaxy
Processing role ansible.kubernetes-modules
Opened /root/.ansible_galaxy
- downloading role 'kubernetes-modules', owned by ansible
https://galaxy.ansible.com/api/v1/roles/?owner__username=ansible&name=kubernetes-modules
https://galaxy.ansible.com/api/v1/roles/16501/versions/?page_size=50
- downloading role from https://github.com/ansible/ansible-kubernetes-modules/archive/master.tar.gz
- extracting ansible.kubernetes-modules to /vagrant/Kubernetes/mariadb_demo_k8s/ansible-deployment/roles/ansible.kubernetes-modules
- ansible.kubernetes-modules (master) was installed successfully
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=d66eb0a142190022fee50de4e2560c11260bd69ec3cc634be2a389425e6bf477 save_container=False

一旦这个过程完成,您会注意到,类似于上一个示例,项目中出现了一个新的目录,名为ansible-deployment。在这个目录内,您会找到一个剧本和一个roles目录,后者负责将我们的服务部署到 Kubernetes 中。与我们之前的本地主机示例一样,这个剧本同样根据标签进行划分,控制在集群中启动、停止和重启服务。由于我们尚未部署我们的服务,因此我们可以使用带有--engine k8s标志的ansible-container run命令来启动部署,以指示进行 Kubernetes 部署。如果我们已经正确配置了container.yml文件,您应该看到剧本成功运行,表示容器已经部署到 Kubernetes 集群中:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ ansible-container --engine k8s run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=k8s
Verifying service image service=mariadb-database
PLAY [Manage the lifecycle of mariadb-k8s on K8s] **************************************************

TASK [Create namespace database] ***************************************************
ok: [localhost]

TASK [Create service] ******************************************************
ok: [localhost]

TASK [Create deployment, and scale replicas up] *******************************************************
changed: [localhost]

PLAY RECAP ********************************************
localhost: ok=3    changed=1    unreachable=0    failed=0

使用之前的kubectl get pods命令,我们可以验证我们的 Kubernetes pod 已经部署并成功运行。由于我们将这个 pod 部署在自己的命名空间中,因此我们需要使用--namespace标志来查看运行在其他命名空间中的 pods:

ubuntu@node01:$ kubectl get pods --namespace database
NAME                                READY     STATUS    RESTARTS   AGE
mariadb-database-1880651791-979zd   1/1       Running   0          3m

使用带有 Kubernetes 引擎的ansible-container run命令是创建在 Kubernetes 集群上运行的云原生服务的强大工具。通过使用 Ansible Container,您可以灵活地选择部署应用程序的方式,可以直接执行剧本,也可以通过 Ansible Container 工作流自动执行。如果您希望从 Kubernetes 集群中删除当前部署,您可以简单地运行ansible-container --engine k8s destroy命令,以完全从集群中删除 pods 和部署工件。需要注意的是,其他 Ansible Container 工作流命令(start、stop 和 restart)完全适用于 Kubernetes 部署引擎。为了减少冗余,我们来看一下ansible-container deploy和工作流命令在 OpenShift 部署引擎下的工作方式。从功能上讲,Ansible Container 的工作流命令对于 Kubernetes 和 OpenShift 是相同的。

将容器部署到 OpenShift

ansible-container deploy命令还支持使用原生 OpenShift API 直接部署到 OpenShift。由于 OpenShift 是基于 Kubernetes 构建的,您会发现将容器部署到 OpenShift 的过程与 Kubernetes 的部署非常相似,因为 OpenShift 的身份验证与 Kubernetes 在后台的工作方式非常相似。在开始之前,这些示例将使用与我们在第六章中创建的 Minishift 虚拟机同时运行的 Vagrant 实验室虚拟机,使用 OpenShift 管理容器。这可能会消耗大量的 CPU 和内存。如果您尝试在 8 GB 或更高的内存下运行这些示例,您应该能够获得良好的性能。

然而,如果您的资源有限,这些示例仍然可以在 OpenShift 的免费云账户上运行得相当顺利,尽管您可能会遇到提供的配额有限的问题。

在开始之前,我们首先需要确保 Vagrant 实验室环境以及我们的 Minishift 虚拟机正在运行,并且可以从 VirtualBox 网络中访问。由于用于创建我们 Vagrant 实验室环境和 Minishift 集群的虚拟化管理程序都使用 VirtualBox,我们默认情况下应该能够在两个虚拟机之间建立网络连接。我们可以通过尝试从 Vagrant 实验室虚拟机 ping Minishift 虚拟机来验证这一点。首先,我们需要使用适合您本地工作站的规格启动 Minishift 虚拟机。在这个示例中,我将启动 Minishift 虚拟机,分配 8 GB 内存和 50 GB 虚拟硬盘存储。如果您同时运行两个虚拟机,您可能只能为 Minishift 分配最小的 2 GB 内存:

aric@lemur:~/Development/minishift$ minishift start --vm-driver=virtualbox --disk-size=50GB --memory=8GB 
-- Starting local OpenShift cluster using 'virtualbox' hypervisor ...                                                                                           
-- Starting Minishift VM .................... OK                                                                                                                       
-- Checking for IP address ... OK                                                                                                                                          
-- Checking if external host is reachable from the Minishift VM ...                                 
   Pinging 8.8.8.8 ... OK                                                                                      
-- Checking HTTP connectivity from the VM ...                                 
   Retrieving http://minishift.io/index.html ... OK
-- Checking if persistent storage volume is mounted ... OK
-- Checking available disk space ... 7% used OK                                                             
-- OpenShift cluster will be configured with ...                                                            
   Version: v3.6.0                               
-- Checking `oc` support for startup flags ...             
   host-config-dir ... OK                       
   host-data-dir ... OK                                                                                   
   host-pv-dir ... OK                                                                               
   host-volumes-dir ... OK                                                                                  
   routing-suffix ... OK                                                                               
Starting OpenShift using openshift/origin:v3.6.0 ...     
OpenShift server started.                            

The server is accessible via web console at:                                                                                                                                                            
    https://192.168.99.100:8443                          

启动过程结束时,您应该会收到一个 IP 地址,通过这个地址可以访问 OpenShift Web UI。我们需要确保这个 IP 地址可以从我们的 Vagrant 实验室节点访问:

ubuntu@node01:~$ ping 192.168.99.100
PING 192.168.99.100 (192.168.99.100) 56(84) bytes of data.
64 bytes from 192.168.99.100: icmp_seq=1 ttl=63 time=0.642 ms
64 bytes from 192.168.99.100: icmp_seq=2 ttl=63 time=0.602 ms
64 bytes from 192.168.99.100: icmp_seq=3 ttl=63 time=0.929 ms
^C
--- 192.168.99.100 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.602/0.724/0.929/0.147 ms

如果这个 IP 地址无法 ping 通,您可能需要配置您的 VirtualBox 网络,使网络连接可用。了解如何配置和调试 VirtualBox 网络的一个很好的资源是官方的 VirtualBox 文档:www.virtualbox.org/manual/ch06.html

一旦 Minishift 虚拟机和 Vagrant 实验室虚拟机之间的网络连接已经建立并验证,接下来我们需要在 Vagrant 实验室虚拟机上安装 OC,以便我们可以进行 OpenShift 身份验证。这将是我们在第六章中完成的相同过程,使用 OpenShift 管理容器

  1. 使用wget下载 OC 二进制包:
ubuntu@node01:~$ wget https://mirror.openshift.com/pub/openshift-v3/clients/3.6.173.0.5/linux/oc.tar.gz                                                                                                                                                                                 

Resolving mirror.openshift.com (mirror.openshift.com)... 54.173.18.88, 54.172.163.83, 54.172.173.155                                                                                                         
Connecting to mirror.openshift.com (mirror.openshift.com)|54.173.18.88|:443... connected.                                                                                                                    
HTTP request sent, awaiting response... 200 OK                                                                                                                                                               
Length: 36147137 (34M) [application/x-gzip]                                                                                                                                                                  
Saving to: ‘oc.tar.gz' 
2017-10-28 15:21:24 (6.79 MB/s) - ‘oc.tar.gz' saved [36147137/36147137]
  1. 使用tar -xf命令提取 TAR 归档文件:
ubuntu@node01:~$ tar -xf oc.tar.gz
oc    oc.tar.gz
  1. 将二进制文件复制到$PATH位置:
ubuntu@node01:~$ sudo cp oc /usr/local/bin

如果您在/home/ubuntu/.kube/config中有现有的 Kubernetes 凭证,您需要将其备份到其他位置,以防 OC 覆盖它们(或者如果您不再需要它,只需删除配置文件:rm -rf ~/.kube/config)。

接下来,我们需要使用oc login命令对本地 OpenShift 集群进行认证,以便生成 Kubernetes 凭证文件,该文件将由 Ansible Container 使用。oc login命令需要 OpenShift 集群的 URL 作为参数。默认情况下,OC 将把 Kubernetes 配置文件写入/home/ubuntu/.kube/config。此文件将作为我们认证 OpenShift Kubernetes 以执行自动化部署的凭证。记得以 developer 用户身份登录,该用户使用任何用户提供的密码进行登录:

ubuntu@node01:$ oc login https://192.168.99.100:8443 
The server uses a certificate signed by an unknown authority.                                                                                                                                                
You can bypass the certificate check, but any data you send to the server could be intercepted by others.                                                                                                    
Use insecure connections? (y/n): y                                                                                                                                                                           

Authentication required for https://192.168.99.100:8443 (openshift)                                                                                                                                          
Username: developer                                                                                                                                                                                          
Password:                                                                                                                                                                                                    
Login successful.                                                                                                                                                                                            

You have one project on this server: "myproject"                                                                                                                                                             

Using project "myproject".                                                                                                                                                                                   
Welcome! See 'oc help' to get started.

一旦您成功认证,您应该会注意到现在有一个新的 Kubernetes 配置文件被写入到路径/home/ubuntu/.kube/config。这是 Ansible Container 用于访问 OpenShift 的配置文件:

ubuntu@node01:~$ cat .kube/config
apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://192.168.99.100:8443
  name: 192-168-99-100:8443
contexts:
- context:
    cluster: 192-168-99-100:8443
    namespace: myproject
    user: developer/192-168-99-100:8443
  name: myproject/192-168-99-100:8443/developer
current-context: myproject/192-168-99-100:8443/developer
kind: Config
preferences: {}
users:
- name: developer/192-168-99-100:8443
  user:
    token: TOKEN-CENSORED

让我们通过使用oc get all命令测试对本地 OpenShift 实例的认证。如果认证成功,您应该会看到当前在本地 OpenShift 环境中运行的 pod、部署、服务和路由的列表:

ubuntu@node01:~$ oc get all
NAME   DOCKER REPO   TAGS    UPDATED
is/oc-test-deployment   172.30.1.1:5000/myproject/oc-test-deployment   latest    13 days ago
is/test-deployment      172.30.1.1:5000/myproject/test-deployment      latest    2 weeks ago

NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/webserver   1         1         1            1           12d

NAME               HOST/PORT                            PATH      SERVICES    PORT      TERMINATION   WILDCARD
routes/webserver   awesomwebapp.192.168.99.100.nip.io             webserver   80-80                   None

NAME            CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
svc/webserver   172.30.136.131   <none>        80/TCP    12d

NAME                      DESIRED   CURRENT   READY     AGE
rs/webserver-1266346274   1         1         1         12d

NAME                            READY     STATUS    RESTARTS   AGE
po/webserver-1266346274-m2jvd   1/1       Running   3          12d

默认情况下,OpenShift 使用与 Kubernetes 相同的认证机制,在我们的container.yml文件中。我们需要提供的唯一信息是 Kubernetes 配置文件的路径,以及项目将要部署到的 Kubernetes 命名空间。由于我们之前在上一部分的 MariaDB 项目中已经配置过这些内容,所以我们可以重用这些配置,将我们的项目部署到 OpenShift。回顾一下,我们来看一下 Vagrant Lab 虚拟机(/vagrant/Kubernetes/mariadb_demo_k8s)中 MariaDB 项目的内容,并查看container.yml文件的内容:

version: "2"
settings:
  conductor_base: ubuntu:16.04
  project_name: mariadb-k8s
  roles_path:
    - ./roles/
  k8s_namespace:
    name: database
  k8s_auth:
    config_file: /home/ubuntu/.kube/config
services:
  mariadb-database:
    roles:
      - role: mariadb_role

registries:
  docker:
    url: https://index.docker.io/v1/
    namespace: aric49

唯一的区别是,k8s_namespace参数将定义您希望将容器部署到的 OpenShift 项目。在 OpenShift 术语中,projectnamespace本质上是相同的。目前,让我们保持配置不变,来看一下如何使用 OpenShift 引擎部署我们的项目。使用 OpenShift 部署项目与我们使用 Kubernetes 部署的方式非常相似,唯一的区别是我们需要在 Ansible Container 命令前添加--engine openshift标志,以便我们的项目知道要直接与 OpenShift API 进行交互。这里也适用相同的语法规则。我们将为deploy命令提供在container.yml文件中定义的仓库名称,用于推送我们的容器镜像,并为其指定一个唯一的标签,方便后续引用:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ ansible-container --engine openshift deploy --push-to docker --tag openshift

Parsing conductor CLI args.                                                                                                                                                                                  
Engine integration loaded. Preparing push.      engine=OpenShift™
Tagging aric49/mariadb-k8s-mariadb-database
Pushing aric49/mariadb-k8s-mariadb-database:openshift...
The push refers to a repository [docker.io/aric49/mariadb-k8s-mariadb-database]
Preparing
Waiting
Layer already exists
openshift: digest: sha256:99139cdd73b80ed29cedf8df4399b368f22e747f18e66e2529daf2e7fcf82c41 size: 1569
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=9b1bdcabd9399757c54a7218b8db2233503dacc4ecdde95d8a358a965bac954e save_container=False
Parsing conductor CLI args.
Engine integration loaded. Preparing deploy.    engine=OpenShift™
Verifying image for mariadb-database
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=76e86c54e5545328a6806bb3315d604c8e7cdb59d4484a2c32f78ba35787716b save_container=False

一旦我们的容器镜像推送完成,我们可以验证部署的 playbooks 是否已经在ansible-deployment目录下生成(Kubernetes/mariadb_demo_k8s/ansible-deployment/mariadb-k8s.yml):

  - name: Manage the lifecycle of mariadb-k8s on OpenShift™
    hosts: localhost
    gather_facts: no
    connection: local
    # Include Ansible Kubernetes and OpenShift modules
    roles:
      - role: ansible.kubernetes-modules
    vars_files: []
    # Tasks for setting the application state. Valid tags include: start, stop, restart, destroy
    tasks:
      - name: Create project myproject
        openshift_v1_project:
            name: myproject
            state: present
        tags:
          - start
      - name: Destroy the application by removing project myproject
        openshift_v1_project:
            name: myproject
            state: absent
        tags:
          - destroy
TRUNCATED

类似于 Docker 和 Kubernetes 部署引擎,这些 playbooks 可以独立执行,使用ansible-playbook命令,或者使用ansible-container run命令。让我们运行我们的项目,并通过ansible-container run命令将其部署到 OpenShift:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ ansible-container --engine openshift run 
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=OpenShift™
Verifying service image service=mariadb-database

PLAY [Manage the lifecycle of mariadb-k8s on OpenShift?] ***********************

TASK [Create project database] *************************************************
changed: [localhost]

TASK [Create service] **********************************************************
changed: [localhost]

TASK [Create deployment, and scale replicas up] ********************************
changed: [localhost]

TASK [Create route] ************************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=4    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=ac5ead07a0bb145cc507b5cf815290597d1c1a8ee77a894c60916d2c0d543f18 save_container=False

在 playbook 运行成功完成后,我们可以登录到 OpenShift 网页用户界面,查看刚才执行的部署。在网页浏览器中,导航到minishift start命令输出中提供的 URL(在我的例子中是192.168.99.100),接受自签名证书,并以developer用户身份登录:

图 2:登录到 OpenShift 控制台

登录后,您应该看到一个新项目已创建,名为database。在此项目中,您可以看到 Ansible Container 部署默认生成的所有内容:

图 3:数据库项目已在 OpenShift 控制台的“我的项目”下创建

点击项目,database将带你进入一个仪表盘,显示与部署相关的详细信息:

图 4:MariaDB 数据库部署

如您所见,默认情况下,用于部署 OpenShift 的 Ansible Container playbooks 运行时具有一组非常实用的默认配置选项。我们可以立刻看到,Ansible Container 为我们的项目创建了一个名为database的新项目。在此项目中,存在一个默认部署,其中包含我们创建并正在运行的 MariaDB pod。它甚至为我们采取了创建一个默认服务的步骤,预先配置了标签,并使用nip.io DNS 服务创建了一个路由来访问该服务。实际上,我们的新服务已经部署并准备好随时使用。为了使用 OpenShift 部署引擎,我们实际上不需要更改任何container.yml配置;我们使用的配置与部署到 Kubernetes 时完全相同,唯一的区别是使用了不同的 Kubernetes 配置文件,并在run命令中指定了 OpenShift 引擎。正如您所看到的,能够透明地部署到 OpenShift 或 Kubernetes 是极其强大的。这使得 Ansible Container 能够无缝地工作,无论您的服务配置使用的是哪种目标架构。

我们还可以通过使用 OC 命令行界面客户端来验证部署。在 Vagrant 实验室虚拟机中,您可以使用oc project命令切换到database项目:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ oc project database
Now using project "database" on server "https://192.168.99.100:8443".

一旦我们切换到新的项目上下文,就可以使用oc get all命令来显示在该项目中配置的所有内容,包括由 Ansible Container 生成的 pods、服务和路由配置:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ oc get all
NAME                  REVISION   DESIRED   CURRENT   TRIGGERED BY
dc/mariadb-database   1          1         1         config

NAME                    DESIRED   CURRENT   READY     AGE
rc/mariadb-database-1   1         1         1         27s

NAME                           HOST/PORT                                              PATH      SERVICES           PORT            TERMINATION   WILDCARD
routes/mariadb-database-3306   mariadb-database-3306-database.192.168.99.100.nip.io             mariadb-database   port-3306-tcp                 None

NAME                   CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
svc/mariadb-database   172.30.154.77   <none>        3306/TCP   28s

NAME                          READY     STATUS    RESTARTS   AGE
po/mariadb-database-1-bj19h   1/1       Running   0          26s

除了ansible-container run之外,我们还可以使用标准的 Ansible Container 工作流命令来管理我们的部署,比如stoprestartdestroy。正如我们之前讨论的,这些工作流命令在 Kubernetes 引擎中也能以相同的方式工作。让我们首先启动ansible-container stop命令。stop将优雅地停止部署中所有正在运行的 Pods,同时保持其他已部署的资源活跃。让我们尝试停止部署并重新执行get all命令,了解会发生什么:

ubuntu@node01:$ ansible-container --engine openshift stop
Parsing conductor CLI args.
Engine integration loaded. Preparing to stop all containers.    engine=OpenShift™

PLAY [Manage the lifecycle of mariadb-k8s on OpenShift?] ***********************

TASK [Stop running containers by scaling replicas down to 0] *******************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services stopped.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=8374839c00e2d92c02351905bd6dd463ba5592783430bc08c5b18ba068fece77 save_container=False

一旦stop命令成功完成,请重新执行oc get all命令:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ oc get all
NAME                  REVISION   DESIRED   CURRENT   TRIGGERED BY
dc/mariadb-database   2          0         0         config

NAME                    DESIRED   CURRENT   READY     AGE
rc/mariadb-database-1   0         0         0         7m
rc/mariadb-database-2   0         0         0         1m

NAME                           HOST/PORT                                              PATH      SERVICES           PORT            TERMINATION   WILDCARD
routes/mariadb-database-3306   mariadb-database-3306-database.192.168.99.100.nip.io             mariadb-database   port-3306-tcp                 None

NAME                   CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
svc/mariadb-database   172.30.154.77   <none>        3306/TCP   7m

从前面的输出中,我们可以看到 OpenShift 为我们部署的配置更改创建了一个新修订版本(REVISION 2),它描述了该部署没有正在运行的 Pod 副本,表明部署处于停止状态(Current 0Desired 0Ready 0)。然而,路由和服务仍然存在,并且在集群中运行。OpenShift 的一个主要优点是,它能够跟踪在各种修订定义下对项目所做的更改。这使得在变更失败或需要回滚时,能够轻松地回退到先前的部署。stop命令的补充命令是restart,它确保在首先停止服务后,当前修订版本处于运行状态。

stop不同,restart不会创建新的修订版本,因为我们当前的修订版本已经缩减为零副本,但会将当前修订版本扩展,以确保在项目中运行所需数量的 Pods。让我们执行ansible-container restart命令,查看它如何影响我们的部署:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ ansible-container --engine openshift restart
Parsing conductor CLI args.
Engine integration loaded. Preparing to restart containers.     engine=OpenShift™

PLAY [Manage the lifecycle of mariadb-k8s on OpenShift?] ***********************

TASK [Stop running containers by scaling replicas down to 0] *******************
ok: [localhost]

TASK [Create deployment, and scale replicas up] ********************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0

All services restarted. playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=d54f160a64f5f57cfe88e6b824dfe609a9000355bc2e29584509ec3d97eee82f save_container=False

再次执行oc get all命令,我们将看到当前的修订版本(#2)已经按所需的 Pod 数量运行:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ oc get all
NAME                  REVISION   DESIRED   CURRENT   TRIGGERED BY
dc/mariadb-database   2          1         1         config

NAME                    DESIRED   CURRENT   READY     AGE
rc/mariadb-database-1   0         0         0         31m
rc/mariadb-database-2   1         1         1         26m

NAME                           HOST/PORT                                              PATH      SERVICES           PORT            TERMINATION   WILDCARD
routes/mariadb-database-3306   mariadb-database-3306-database.192.168.99.101.nip.io             mariadb-database   port-3306-tcp                 None

NAME                   CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
svc/mariadb-database   172.30.154.77   <none>        3306/TCP   31m

NAME                          READY     STATUS    RESTARTS   AGE
po/mariadb-database-2-g7r7f   1/1       Running   0          6m

最后,我们可以使用ansible-container destroy命令彻底删除 OpenShift(或 Kubernetes)集群中所有与我们服务相关的痕迹。请记住,这还将删除项目以及任何其他在项目中运行的容器,无论这些容器是通过 Ansible Container 手动部署还是通过其他方式部署的。这就是为什么在运行诸如ansible-container destroy等命令时,将应用程序部署按 OpenShift 项目和 Kubernetes 命名空间进行区分非常重要的原因:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ ansible-container --engine openshift destroy
Parsing conductor CLI args.
Engine integration loaded. Preparing to stop+delete all containers and built images.    engine=OpenShift™

PLAY [Manage the lifecycle of mariadb-k8s on OpenShift?] ***********************

TASK [Destroy the application by removing project database] ********************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services destroyed. playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=dde59aed5fb90b1e46df4fa9e4558a46ff043f89189cc101f25a01004878f472 save_container=False

根据任务执行情况,似乎有一个任务执行时删除了整个 OpenShift 项目。如果我们最后一次执行oc get all命令,这一点会显现出来:

ubuntu@node01:/vagrant/Kubernetes/mariadb_demo_k8s$ oc get all 
Error from server (Forbidden): User "developer" cannot list buildconfigs in project "database"
Error from server (Forbidden): User "developer" cannot list builds in project "database"
Error from server (Forbidden): User "developer" cannot list imagestreams in project "database"
Error from server (Forbidden): User "developer" cannot list deploymentconfigs in project "database"
Error from server (Forbidden): User "developer" cannot list deployments.extensions in project "database"
Error from server (Forbidden): User "developer" cannot list horizontalpodautoscalers.autoscaling in project "database"
Error from server (Forbidden): User "developer" cannot list replicationcontrollers in project "database"
Error from server (Forbidden): User "developer" cannot list routes in project "database"
Error from server (Forbidden): User "developer" cannot list services in project "database"
Error from server (Forbidden): User "developer" cannot list statefulsets.apps in project "database"
Error from server (Forbidden): User "developer" cannot list jobs.batch in project "database"
Error from server (Forbidden): User "developer" cannot list replicasets.extensions in project "database"
Error from server (Forbidden): User "developer" cannot list pods in project "database"

这些错误表明我们的用户已经无法列出 database 项目中存在的任何内容,因为该项目不再存在。该项目的所有痕迹,包括部署、服务、Pod 和路由,都已从集群中删除。从 Web 界面上也可以明显看出这一点,因为刷新网页后会显示该项目已经不存在。

参考文献

总结

在本章中,我们研究了最后一个 Ansible Container 工作流命令:ansible-container deploydeploy 是 Ansible Container 中最通用的命令之一,因为它允许我们在生产级的 Kubernetes 和 OpenShift 环境中运行和管理容器。deploy 为我们的旅程开辟了一条新路,能够实现容器赋予我们基础设施的灵活性和敏捷性。现在,我们可以真正地使用一个工具,不仅在本地构建和调试容器化应用,还可以在生产环境中部署和管理这些应用。能够使用相同的表达式 Ansible Playbook 语言来真正构建可靠且可扩展的应用程序,意味着部署可以从第一天起围绕 DevOps 和自动化最佳实践构建,而不是事后重新设计部署以使其自动化的艰难任务。

仅仅因为我们已经完成了关于主要的 Ansible Container 工作流组件的学习,并不意味着我们的旅程已经结束。到目前为止,在本书中,我们已经研究了如何使用 Ansible Container 部署不依赖于其他服务的单功能微服务。Ansible Container 的强大之处在于,它还具有通过表达与其他服务的链接和依赖关系来构建和部署多个容器化应用的内在能力。在下一章,我们将研究如何构建和部署多容器应用。

第八章:构建和部署多容器项目

到目前为止,在本书的过程中,我们已经探索了 Ansible 容器和容器化应用程序部署的多个方面。我们已经学习了如何从基本的 Dockerfile 构建 Docker 容器,使用 Ansible 容器安装角色、构建容器,甚至将应用程序部署到像 Kubernetes 和 OpenShift 这样的云解决方案中。然而,你可能已经注意到,我们到目前为止的讨论主要集中在部署单一微服务应用程序,例如 Apache2、Memcached、NGINX 和 MariaDB。这些应用程序可以独立部署,不依赖于除了基本 Docker 守护进程以外的其他服务或应用程序。虽然通过构建单容器微服务来学习容器化是学习容器化核心概念的好方法,但它并不能准确反映现实世界中的应用程序基础设施。

正如你可能已经知道的,应用程序通常由一堆互相连接的软件组成,它们共同工作,为最终用户提供服务。一个典型的应用程序栈可能涉及一个接收用户输入的 Web 前端。Web 界面可能需要知道如何联系数据库后端来存储用户提供的数据,并且能够检索之前存储的数据。大数据应用程序可能会定期分析数据库中的数据,试图找出数据趋势、分析使用情况或执行其他功能,帮助数据科学家了解用户如何使用应用程序。这些应用程序依赖于网络连接、DNS 解析和服务发现来进行相互通信并执行它们的整体功能。

容器的世界在最开始时并没有太大不同。毕竟,容器化的软件仍然依赖于其他容器化和非容器化的应用程序来存储、检索和处理数据,并执行不同的功能。正如我们在第五章《使用 Kubernetes 扩展容器》和第六章《使用 OpenShift 管理容器》中提到的那样,容器为部署和扩展多层次应用程序问题带来了更多的灵活性,并且显著降低了管理复杂性。

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

  • 使用 Docker 网络定义复杂应用程序

  • 探索 Ansible 容器 django-gulp-nginx 项目

  • 构建 django-gulp-nginx 项目

  • 开发和生产配置

  • 将项目部署到 OpenShift

使用 Docker 网络定义复杂应用程序

容器化环境是动态的,容易快速改变状态。与传统基础设施不同,容器在不断地扩展和缩减,甚至可能在主机之间迁移。容器能够快速高效地发现其他容器、建立网络连接并共享资源至关重要。

如我们在前几章中提到的,Docker、Kubernetes 和 OpenShift 具有原生功能,可以使用各种网络协议和 DNS 解析自动发现并访问其他容器,这与裸机或虚拟化服务器并无太大不同。当在单个 Docker 主机上部署容器时,Docker 会为每个容器分配一个 IP 地址,该地址位于一个虚拟子网中,可以用于与同一子网中的其他容器 IP 地址进行通信。同样,Docker 还会提供简单的 DNS 解析,可以用来解析容器名称。当通过容器编排系统如 Kubernetes、OpenShift 或 Docker Swarm 在多个主机上扩展时,容器使用覆盖网络在主机之间建立网络连接,并像在同一主机上一样运行。正如我们在第五章《使用 Kubernetes 扩展容器》中看到的,Kubernetes 提供了一个复杂的内部 DNS 系统,可以根据更大的 Kubernetes DNS 域中的命名空间解析容器。关于容器网络的内容很多,因此本章将重点讨论 Docker 网络以实现服务发现。在本节中,我们将创建一个专用的 Docker 网络子网,并创建利用 DNS 建立与其他运行中容器的网络连接的容器。

为了演示 Docker 容器之间的基本网络连接性,我们将在 Vagrant 实验室主机的 Docker 环境中使用桥接网络驱动程序创建一个新的虚拟容器网络。桥接网络是最基本的容器网络类型之一,它仅限于单个 Docker 主机。我们可以使用 docker network create 命令创建此网络。在本例中,我们将使用 172.100.0.0/16 CIDR 块和 bridge 网络驱动程序创建一个名为 SkyNet 的网络:

ubuntu@node01:~$ docker network create -d bridge --subnet 172.100.0.0/16 SkyNet

2679e6a7009912fbe5b8203c83011f5b3f3a5fa7c154deebb4a9aac7af80a6aa

我们可以使用 docker network ls 命令验证该网络是否已成功创建:

ubuntu@node01:~$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
2679e6a70099        SkyNet              bridge              local
truncated..

我们可以使用 docker network inspect 命令,以 JSON 格式查看关于此网络的详细信息:

ubuntu@node01:~$ docker network inspect SkyNet
[
    {
        "Name": "SkyNet",
        "Id": "2679e6a7009912fbe5b8203c83011f5b3f3a5fa7c154deebb4a9aac7af80a6aa",
        "Created": "2017-11-05T02:26:22.790958921Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.100.0.0/16"
                }
            ]
        },

现在我们已经在 Docker 主机上建立了一个网络,我们可以创建容器连接到此网络以测试其功能。让我们创建两个 Alpine Linux 容器,连接到该网络,并用它们测试 DNS 解析和可达性。Alpine Linux Docker 镜像是一个极其轻量的容器镜像,可用于快速启动容器进行测试。在此示例中,我们将创建两个名为service1service2的 Alpine Linux 容器,并使用--network标志将它们连接到 SkyNet Docker 网络:

ubuntu@node01:~$ docker run --network=SkyNet -itd --name=service1 alpine
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
b56ae66c2937: Pull complete
Digest: sha256:d6bfc3baf615dc9618209a8d607ba2a8103d9c8a405b3bd8741d88b4bef36478
Status: Downloaded newer image for alpine:latest
5f1fba3964fae85e90cc1b3854fc443de0b479f94af68c14d9d666999962e25a 

以类似的方式,我们可以启动service2容器,使用SkyNet网络:

ubuntu@node01:~$ docker run --network=SkyNet -itd --name=service2 alpine
8f6ad6b88b52e446cee44df44d8eaa65a9fe0d76a2aecb156fac704c71b34e27

尽管这些容器没有运行服务,但它们通过使用-t标志分配一个伪终端(pseudo-tty)实例给它们,从而使容器能够运行。分配伪终端给容器将防止容器立即退出,但如果终端会话终止,容器将退出。在本书中,我们探讨了通过命令和入口点参数来运行容器,这是推荐的方法。通过分配伪终端来运行容器非常适合快速启动容器进行测试,但这并不是运行传统应用容器的推荐方式。应用容器应该始终根据其中运行的进程 IDPID)的状态来运行。

在第一个示例中,我们可以看到我们的本地 Docker 主机拉取了最新的 Alpine 容器镜像,并使用我们传递给docker run命令的参数运行它。同样,第二个docker run命令使用相同的参数创建了该容器镜像的第二个实例。使用docker inspect命令,我们可以查看 Docker 守护进程为我们的容器分配了哪些 IP 地址:

ubuntu@node01:~$ docker inspect service1
TRUNCATED..
"NetworkID": "2679e6a7009912fbe5b8203c83011f5b3f3a5fa7c154deebb4a9aac7af80a6aa",
"EndpointID": "47e16d352111007b9f19caf8c10a388e768cc20e5114a3b346d08c64f1934e1f",
"Gateway": "172.100.0.1",
"IPAddress": "172.100.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:64:00:02",
"DriverOpts": null

我们也可以对service2执行相同的操作:

ubuntu@node01:~$ docker inspect service2
TRUNCATED..
"NetworkID": "2679e6a7009912fbe5b8203c83011f5b3f3a5fa7c154deebb4a9aac7af80a6aa",
"EndpointID": "3ca5485aa27bd1baffa826b539f905b50005c9157d5a4b8ba0907d15a3ae7a21",
"Gateway": "172.100.0.1",
"IPAddress": "172.100.0.3",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:64:00:03",
"DriverOpts": null

如你所见,Docker 将 IP 地址172.100.0.2分配给了我们的service1容器,将 IP 地址172.100.0.3分配给了我们的service2容器。这些 IP 地址提供了正如你所期望的,在同一网络段上连接的两个主机之间的网络连接。如果我们使用docker exec登录到service1容器,我们可以检查service1是否能够通过 Docker 分配的 IP 地址 ping 通service2

ubuntu@node01:~$ docker exec -it service1 /bin/sh
/ # ping 172.100.0.3
PING 172.100.0.3 (172.100.0.3): 56 data bytes
64 bytes from 172.100.0.3: seq=0 ttl=64 time=0.347 ms
64 bytes from 172.100.0.3: seq=1 ttl=64 time=0.160 ms
64 bytes from 172.100.0.3: seq=2 ttl=64 time=0.159 ms

由于这些容器是通过伪终端而不是命令或入口点运行的,只需在容器的 Shell 中键入exit即可终止 TTY 会话并停止容器。要在退出 Shell 时保持容器运行,请使用键盘上的 Docker 逃逸序列:Ctrl + P Ctrl + Q

我们也可以从service2容器执行此测试:

ubuntu@node01:~$ docker exec -it service2 /bin/sh
/ # ping 172.100.0.2
PING 172.100.0.2 (172.100.0.2): 56 data bytes
64 bytes from 172.100.0.2: seq=0 ttl=64 time=0.175 ms
64 bytes from 172.100.0.2: seq=1 ttl=64 time=0.157 ms

很容易看出,基于 IP 的网络通信在建立运行中的容器之间的网络连接时效果良好。该方法的缺点是,我们不能总是提前知道容器运行环境会分配给容器的 IP 地址。例如,某个容器可能需要在配置文件中添加一个条目,以指向它所依赖的服务。虽然你可能会想直接将一个 IP 地址插入容器角色并进行构建,但每次在不同的环境中部署时,这个容器角色都必须重新构建。此外,当容器停止并重新启动时,它们可能会获得不同的 IP 地址,这将导致应用程序崩溃。幸运的是,作为解决这一问题的方案,Docker 提供了基于容器名称的 DNS 解析,这将主动跟踪运行中的容器,并在容器的 IP 地址发生变化时解析出正确的 IP 地址。与 IP 地址不同,容器名称可以提前知道,并且可以用于在配置文件中指向正确的服务,或作为环境变量存储在内存中。我们可以通过重新登录service1容器,并使用ping命令 ping service2的名称来看到这一点的实际应用:

ubuntu@node01:~$ docker exec -it service1 /bin/sh
/ # ping service2
PING service2 (172.100.0.3): 56 data bytes
64 bytes from 172.100.0.3: seq=0 ttl=64 time=0.233 ms
64 bytes from 172.100.0.3: seq=1 ttl=64 time=0.142 ms
64 bytes from 172.100.0.3: seq=2 ttl=64 time=0.184 ms
64 bytes from 172.100.0.3: seq=3 ttl=64 time=0.263 ms

此外,我们可以创建一个第三个服务容器,并检查新容器是否能够分别解析service1service2的名称:

ubuntu@node01:~$ docker run --network=SkyNet -itd --name=service3 alpine
8db62ae30457c351474d909f0600db7f744fb339e06e3c9a29b87760ad6364ff 
ubuntu@node01:~$ docker exec -it service3 /bin/sh
/ # ping service1
PING service1 (172.100.0.2): 56 data bytes
64 bytes from 172.100.0.2: seq=0 ttl=64 time=0.207 ms
64 bytes from 172.100.0.2: seq=1 ttl=64 time=0.165 ms
64 bytes from 172.100.0.2: seq=2 ttl=64 time=0.159 ms
^C
--- service1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.159/0.177/0.207 ms
/ #
/ # ping service2
PING service2 (172.100.0.3): 56 data bytes
64 bytes from 172.100.0.3: seq=0 ttl=64 time=0.224 ms
64 bytes from 172.100.0.3: seq=1 ttl=64 time=0.162 ms
64 bytes from 172.100.0.3: seq=2 ttl=64 time=0.146 ms

最后,如果我们登录到service2容器,我们可以使用nslookup命令解析新创建的service3容器的 IP 地址:

ubuntu@node01:~$ docker exec -it service2 /bin/sh
/ # nslookup service3

Name:      service3
Address 1: 172.100.0.4 service3.SkyNet

Docker 通过将 Docker 网络的名称作为域名来创建 DNS 解析。因此,nslookup 结果显示了 service3 的完全限定域名 service3.SkyNet。然而,正如你所想象的那样,为容器提供 DNS 解析是构建可靠且强大的容器化基础设施的一个非常强大的工具。仅仅通过知道容器的名称,你就可以在容器之间建立链接和依赖关系,这些关系会随着基础设施的扩展而扩展。这个概念远远超出了学习容器的单独 IP 地址。例如,正如我们在第五章《Kubernetes 下的容器规模化》和第六章《使用 OpenShift 管理应用程序》中所见,Kubernetes 和 OpenShift 允许创建与后端 Pod 使用标签或其他标识符进行逻辑连接的服务。当其他 Pod 向服务 DNS 条目传递流量时,Kubernetes 会根据服务条目中配置的标签规则负载均衡流量到与之匹配的正在运行的 Pod。依赖该服务的容器只需要知道如何解析服务的 FQDN,容器编排器会处理其余的部分。后端 Pod 可以扩展或缩减,但只要容器编排器的 DNS 服务能够解析服务条目,调用该服务的其他容器就不会察觉到任何变化。

探索 Ansible Container django-gulp-nginx 项目

现在我们已经对容器网络概念和 Docker DNS 解析有了基本的理解,接下来我们可以构建具有多容器依赖的项目。Ansible Container 提出了创建完全可重用的全栈容器化应用程序的概念,恰当地称为容器应用(Container Apps)。容器应用可以像容器启用角色一样,从 Ansible Galaxy 快速下载并部署。容器应用的好处在于,用户可以迅速开始开发,开发的对象是完全功能的多层应用,这些应用作为独立的微服务容器运行。在本例中,我们将使用一个社区开发的 web 应用项目,该项目启动一个基于 Python 的 Django、Gulp 和 NGINX 环境,我们可以将其部署到本地以及像 OpenShift 或 Kubernetes 这样的容器编排环境。

你可以通过访问 Ansible Galaxy 网站 galaxy.ansible.com,选择“浏览角色”(BROWSE ROLES),点击关键字下拉框中的“角色类型”(Role Type),并从搜索对话框中选择“容器应用”(Container App),来探索各种容器应用:

图 1:在 Ansible Galaxy 中搜索容器应用

在这个例子中,我们将利用预构建的 Ansible django-gulp-nginx 容器应用程序,这是一个官方的 Ansible 容器项目。这个容器应用程序创建了一个容器化的 Django 框架 Web 应用,使用 NGINX 作为 Web 服务器,Django 和 Gulp 作为框架,PostgreSQL 作为数据库服务器。这个项目是一个完全自包含的演示环境,我们可以利用它来探索 Ansible 容器如何与其他服务和依赖项一起工作。

为了开始使用这个项目,我们需要先在 Vagrant 实验室虚拟机上的一个干净目录中安装它。首先,创建一个新目录(我将它命名为 demo),然后运行 ansible-container init 命令,后跟我们想要安装的容器应用程序名称 ansible.django-gulp-nginx。你可以在 Ansible Galaxy 上找到该项目的完整名称,使用之前的步骤来搜索容器应用程序。以下代码演示了如何创建新目录并初始化 Django-Gulp-NGINX 项目:

ubuntu@node01:~$ mkdir demo/
ubuntu@node01:~$ cd demo/
ubuntu@node01:~$ ansible-container init ansible.django-gulp-nginx
Ansible Container initialized from Galaxy container app 'ansible.django-gulp-nginx'

在成功初始化项目后,你应该会看到来自 Galaxy 容器应用程序 ansible.django-gulp-nginx 的 Ansible 容器初始化信息。这表明容器应用程序已成功从 Ansible Galaxy 安装。执行 ls 命令查看 demo/ 目录时,应该会显示类似如下的项目文件:

ubuntu@node01:~/demo$ ls
bower.json     dist         Makefile   meta.yml      package.json       project    requirements.txt  roles    src         test
AUTHORS             container.yml  gulpfile.js  manage.py  node_modules  package-lock.json  README.md  requirements.yml  scripts  temp-space  update-authors.py

列出的许多文件是配置文件,支持我们将要创建的 Gulp/Django 框架应用程序。我们在此次演示中关心的主要文件是所有 Ansible 容器项目中的核心文件:container.yml。如果你用文本编辑器打开 container.yml 文件,它应该类似如下内容:

version: '2'
settings:
  conductor:
    base: 'centos:7'
    volumes:
    - temp-space:/tmp   # Used to copy static content between containers
  k8s_namespace:
    name: demo
    display_name: Ansible Container Demo 
    description: Django framework demo 
defaults:
  POSTGRES_USER: django
  POSTGRES_PASSWORD: sesame
  POSTGRES_DB: django
  DJANGO_ROOT: /django
  DJANGO_USER: django
  DJANGO_PORT: 8080
  DJANGO_VENV: /venv
  NODE_USER: node
  NODE_HOME: /node
  NODE_ROOT: ''
  GULP_DEV_PORT: 8080
services:
  django:
    from: 'centos:7'
    roles:
    - role: django-gunicorn
    environment:
      DATABASE_URL: 'pgsql://{{ POSTGRES_USER }}:{{ POSTGRES_PASSWORD }}@postgresql:5432/{{ POSTGRES_DB }}'
      DJANGO_ROOT: '{{ DJANGO_ROOT }}'
      DJANGO_VENV: '{{ DJANGO_VENV }}'
    expose:
    - '{{ DJANGO_PORT }}'
    working_dir: '{{ DJANGO_ROOT }}'
    links:
    - postgresql
    user: '{{ DJANGO_USER }}'
    command: ['/usr/bin/dumb-init', '{{ DJANGO_VENV }}/bin/gunicorn', -w, '2', -b, '0.0.0.0:{{ DJANGO_PORT }}', 'project.wsgi:application']
    entrypoint: [/usr/bin/entrypoint.sh]
    dev_overrides:
      volumes:
      - '$PWD:{{ DJANGO_ROOT }}'
      command: [/usr/bin/dumb-init, '{{ DJANGO_VENV }}/bin/python', manage.py, runserver, '0.0.0.0:{{ DJANGO_PORT }}']
      depends_on:
      - postgresql

  gulp:
    from: 'centos:7'
    roles:
    - role: gulp-static 
    working_dir: '{{ NODE_HOME }}'
    command: ['/bin/false']
    environment:
      NODE_HOME: '{{ NODE_HOME }}'
    dev_overrides:
      entrypoint: [/entrypoint.sh]
      command: [/usr/bin/dumb-init, /usr/local/bin/gulp]
      ports:
      - '8080:{{ GULP_DEV_PORT }}'
      - 3001:3001
      links:
      - django
      volumes:
      - '$PWD:{{ NODE_HOME }}'
    openshift:
      state: absent

  nginx:
    from: 'centos:7'
    roles:
    - role: ansible.nginx-container
      ASSET_PATHS:
      - /tmp/dist
      PROXY: yes
      PROXY_PASS: 'http://django:8080'
      PROXY_LOCATION: "~* /(admin|api)"
    ports:
    - '{{ DJANGO_PORT }}:8000'
    links:
    - django
    dev_overrides:
      ports: []
      command: /bin/false

  postgresql:
    # Uses a pre-built postgresql image from Docker Hub 
    from: ansible/postgresql:latest
    environment:
    - 'POSTGRES_DB={{ POSTGRES_DB }}'
    - 'POSTGRES_USER={{ POSTGRES_USER }}'
    - 'POSTGRES_PASS={{ POSTGRES_PASSWORD }}'
    - 'PGDATA=/var/lib/pgsql/data/userdata'
    volumes:
    - postgres-data:/var/lib/pgsql/data
    expose:
    - 5432

volumes:
  postgres-data:
    docker: {}
    openshift:
      access_modes:
      - ReadWriteMany
      requested_storage: 3Gi 

  temp-space: 
    docker: {}
    openshift:
      state: absent

registries:
   local_openshift:
     url: https://local.openshift
     namespace: demo
     pull_from_url: 172.30.1.1:5000

这里显示的输出反映了在写作时 container.yml 文件的内容。如果自写作以来该项目有更新,可能会与这里显示的内容略有不同。

如你所见,这个 container.yml 文件包含了我们在本书前几章已经讨论过的许多相同规格。开箱即用,这个项目包含了构建 Gulp、Django、NGINX 和 Postgres 容器的服务声明,完整的角色路径和各种角色变量被定义以确保项目能够以完全自包含的方式运行。此项目还内建了对将项目部署到 OpenShift 的支持。这个项目的一个优点是,它几乎暴露了 Ansible 容器项目中可用的所有配置选项,以及激活这些功能所需的正确语法。就我个人而言,我喜欢将这个项目作为参考指南,以防我忘记在我的项目 container.yml 文件中使用的正确语法。以下是 container.yml 文件中一些对用户理解有帮助的部分,从顶部开始,逐渐往下查看:

  • conductor:正如我们在本书中所看到的,这一部分定义了指挥容器及其基础容器镜像的构建方式。在这种情况下,指挥容器镜像将是一个 CentOS 7 容器,它利用root目录下temp-space目录的卷挂载,将数据挂载到容器内的/tmp目录。值得注意的是,指挥容器镜像可以利用卷挂载来在构建过程中存储数据。

  • defaults:这一部分被称为顶级默认值部分,用于实例化可以在整个项目中使用的变量。在这里,您可以定义变量,这些变量可以用作项目中服务部分的角色变量重写,或者简单地代替在container.yml文件中反复硬编码的相同值。值得注意的是,在 Ansible Container 评估变量优先级的顺序中,顶级默认值部分的优先级最低。

  • services:在services部分,我们可以看到在此堆栈中运行的核心服务条目(djangogulpnginxpostgresql)。大部分情况下,该部分应该根据我们之前章节所讲解的内容进行审核。然而,您会注意到,在django容器的定义中,有一行link,它指定了postgresql容器的名称。您在其他容器定义中也会注意到这一点,这些定义列出了django容器的名称。在 Docker 的早期版本中,link是用来为单独的容器建立网络连接和容器名称解析的方式。然而,Docker 的最新版本已废弃了link语法,转而使用内建的容器名称解析功能,结合 Docker 的网络堆栈。值得注意的是,尽管许多项目仍然使用link来建立网络依赖关系和容器名称解析,但它们很可能在未来的 Docker 版本中被移除。像 Kubernetes 和 OpenShift 这样的容器编排工具也会忽略link语法,因为它们仅使用本地 DNS 服务来解析其他容器和服务。在services部分,我还想特别提一下,nginxgulpdjango容器有一个新子部分,名为dev-overrides。该部分用于指定仅在本地构建测试容器时才会存在的容器配置。通常,开发人员使用dev-overrides来运行容器,开启详细的调试输出,或使用其他类似的日志机制来排查潜在的问题。当使用--production标志执行ansible-container run时,dev-override配置将被忽略。

  • volumes:顶级 volumes 部分用于指定 持久卷声明PVCs),即使容器被停止或销毁,这些声明仍然存在。此部分通常会将已在 container.yml 文件中容器特定服务部分创建的卷映射到该部分,提供更详细的配置,以说明容器编排器应该如何处理持久卷声明。在此情况下,已在 PostgreSQL 容器中映射的 postgres-data 卷被赋予了 OpenShift 特定的 ReadWriteMany 访问模式,并且分配了 3 GB 的存储空间。PVC 通常用于依赖存储和检索数据的应用程序,例如数据库或存储 API。PVC 的整体目标是,如果需要重新部署、升级或将容器迁移到另一个主机时,我们不希望丢失数据。

构建 django-gulp-nginx 项目

现在我们已经对一些在容器应用程序中常见的更高级的 Ansible Container 语法有了深入了解,我们可以将目前学到的 Ansible Container 工作流知识应用于构建和运行容器应用程序。由于容器应用程序是完整的 Ansible Container 项目,包含角色、container.yml 文件和其他支持的项目数据,因此我们之前使用的相同的 Ansible Container 工作流命令也可以在这里无修改地使用。当你准备好时,请在项目的 root 目录下执行 ansible-container build 命令:

ubuntu@node01:~/demo$ ansible-container build
Building Docker Engine context...       
Starting Docker build of Ansible Container Conductor image (please be patient)...       
Parsing conductor CLI args.
Docker™ daemon integration engine loaded. Build starting. project=demo
Building service...     project=demo service=django

PLAY [django] ******************************************************************

TASK [Gathering Facts] *********************************************************
ok: [django]

TASK [django-gunicorn : Install dumb init] *************************************
changed: [django]

TASK [django-gunicorn : Install epel] ******************************************
changed: [django]

TASK [django-gunicorn : Install python deps] ***********************************
changed: [django] => (item=[u'postgresql-devel', u'python-devel', u'gcc', u'python-virtualenv', u'nc', u'rsync'])

TASK [django-gunicorn : Make Django user] **************************************
changed: [django]

TASK [django-gunicorn : Create /django] ****************************************
changed: [django]

TASK [django-gunicorn : Make virtualenv dir] ***********************************
changed: [django]

TASK [django-gunicorn : Setup virtualenv] **************************************
changed: [django]

TASK [django-gunicorn : Copy core source items] ********************************
changed: [django] => (item=manage.py)
changed: [django] => (item=package.json)
changed: [django] => (item=project)
changed: [django] => (item=requirements.txt)
changed: [django] => (item=requirements.yml)
TRUNCATED...

由于容器应用程序正在构建四个服务容器,因此构建过程可能比平常稍长一些。如果你在跟随操作,你会看到 Ansible Container 逐个处理每个 playbook 角色,创建容器并努力使其达到 playbooks 中描述的目标状态。当构建成功完成后,我们可以执行 ansible-container run 命令来启动容器并使我们的新网页服务上线:

ubuntu@node01:~/demo$ ansible-container run
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
Verifying service image service=django
Verifying service image service=gulp
Verifying service image service=nginx

PLAY [Deploy demo] *************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=3109066bdb82a46e0b44fdbbbeaaa02fe8daf7bc18600c0c8466e19346e57b39 save_container=False

当运行的 playbooks 执行完毕后,服务容器应当在开发者模式下运行在 Vagrant 虚拟机上,因为 container.yml 文件为许多服务指定了 dev-overrides。需要注意的是,ansible-container run 默认会根据 container.yml 文件中列出的任何 dev-override 配置来运行服务容器。例如,其中一个开发者重载配置是当处于开发者模式时不运行 NGINX 容器。这是通过为 NGINX 容器设置开发者重载选项实现的,使其执行 /bin/false 作为初始容器命令,立即将其终止。执行 docker ps -a 命令会显示 postgresqldjangogulp 容器正在运行,而 NGINX 容器处于停止状态。通过开发者重载,NGINX 被停止,而 gulp 负责提供 HTML 页面:

ubuntu@node01:~/demo$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
c3e3c2e07427  demo-gulp:20171107030355     "/entrypoint.sh /u..."   56 seconds
0e14b6468ad4  demo-nginx:20171107031508    "/bin/false"             Exited (1)
987345cf6460  demo-django:20171107031311   "/usr/bin/en"            57 seconds ago9660b816e86f  ansible/postgresql:latest "/usr/bin/entrypoi..."   58 seconds 

一旦容器启动,django-gulp-nginx 容器应用程序将会在 Vagrant 实验室虚拟机的本地地址 8080 端口监听。我们可以使用 curl 命令来测试应用程序,确保我们能够得到该服务设计提供的默认 Hello World 简单 HTML 页面响应:

ubuntu@node01:~/demo$ curl http://localhost:8080
<!DOCTYPE html><html lang="en-US"><head><title></title><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="style.css"></head><body><div class="content"><div class="visible"><p>Hello</p><ul><li>world !</li><li>users !</li><li>you!</li><li>everybody !</li></ul></div></div><script src="img/bundle.min.js"></script></body></html>

开发与生产配置

默认情况下,在为某个服务指定了开发者覆盖配置的项目上执行 ansible-container run 命令时,服务会在开发者覆盖配置激活的状态下运行。通常,开发者覆盖配置会暴露应用程序的详细日志或调试选项,而这些选项一般不应让普通终端用户看到,更不用说在日志堆栈追踪始终运行的情况下,运行应用程序会消耗大量资源了。ansible-container run 命令可以使用 --production 标志来指定何时以模拟生产部署的模式运行服务。使用 --production 标志会忽略 container.yml 文件中的 dev_overrides 部分,按文件中明确指定的方式运行服务。现在我们已经验证了网页服务能够在开发模式下运行并正常工作,我们可以尝试在生产模式下运行该服务,以模拟在本地工作站上的完整生产部署。

首先,我们需要运行 ansible-container stop 以停止所有以开发模式运行的容器实例:

ubuntu@node01:~/demo$ ansible-container stop
Parsing conductor CLI args.
Engine integration loaded. Preparing to stop all containers.    engine=Docker™ daemon

PLAY [Deploy demo] *************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services stopped.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=8ab40a594ec72012afdf0abc31ff527925fc5960e4ecbb40eeb16763a12e973a save_container=False

接下来,让我们重新运行 ansible-container run 命令,这次添加 --production 标志,表示我们希望忽略开发者的覆盖配置,并以生产模式运行该服务:

ubuntu@node01:~/demo$ ansible-container run --production
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=Docker™ daemon
Verifying service image service=django
Verifying service image service=gulp
Verifying service image service=nginx

PLAY [Deploy demo] *************************************************************

TASK [docker_service] **********************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0

All services running.   playbook_rc=0
Conductor terminated. Cleaning up.      command_rc=0 conductor_id=1916f63a843d490ec936672528e507332ef408363f65387256fe8a75a1ed7a2f save_container=False

如果现在查看正在运行的服务,你会注意到 NGINX 服务器容器现在正在运行,并作为网页流量的前端服务在 8080 端口上运行,而不再是 Gulp 容器。同时,Gulp 容器已经使用默认命令 /bin/false 启动,该命令会立即终止容器。在这个例子中,我们引入了一种生产配置,终止了开发环境中的 HTTP 网页服务器,转而使用一个适合生产的 NGINX 网页服务器:

ubuntu@node01:~/demo$ docker ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
1aabc9745942  demo-nginx:20171107031508    "/usr/bin/dumb-ini..."   7 seconds ago
16154bbfae54  demo-django:20171107031311   "/usr/bin/entrypoi..."   14 seconds ago      
ea2ec92e9c50  demo-gulp:20171107030355     "/bin/false"             Exited (1) 15
9660b816e86f  ansible/postgresql:latest    "/usr/bin/entrypoi..."   20 minutes ago

我们可以再次测试网页服务,确保该服务可访问并在本地 Vagrant Lab 虚拟机的 8080 端口上运行:

ubuntu@node01:~/demo$ curl http://localhost:8080
<!DOCTYPE html><html lang="en-US"><head><title></title><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="style.css"></head><body><div class="content"><div class="visible"><p>Hello</p><ul><li>world !</li><li>users !</li><li>you!</li><li>everybody !</li></ul></div></div><script src="img/bundle.min.js"></script></body></html>

将项目部署到 OpenShift

到目前为止,我们已经学习了如何使用dev_override语法提供的生产和开发配置在本地运行演示 Web 应用程序。现在,我们已经了解了 Web 应用程序的功能以及如何利用其他服务,我们可以开始了解如何在生产级容器编排环境中部署该应用程序,例如 OpenShift 或 Kubernetes。在本书的这一部分,我们将使用生产配置将该项目部署到我们在第六章中创建的本地 Minishift 集群中,使用 OpenShift 管理应用程序。在开始此示例之前,请确保你有一个有效的 OpenShift 凭证文件,并且它可以与本地集群一起使用,文件位置在/home/ubuntu/.kube/config目录。如果需要创建新的 OpenShift 凭证,请务必返回第七章,部署你的第一个项目,以获取更多详细信息。

为了确保我们的应用程序可以部署到 OpenShift,我们需要修改容器应用程序的container.yml文件,使其指向我们的 Kubernetes 配置文件,并指向 Docker Hub 注册表以便推送我们的容器镜像。

OpenShift 自带一个集成的容器注册表,您可以在ansible-container deploy过程中用它来推送容器镜像。但是,它需要一些额外的配置,超出了本书的范围。现在,使用我们到目前为止在本书中使用的 Docker Hub 注册表就足够了。

container.yml文件的settings部分,我们将添加一个k8s_auth段落,指向 OC 生成的 Kubernetes 配置文件:

 k8s_namespace:
   name: demo
   display_name: Ansible Container Demo
   description: Django framework demo
 k8s_auth:
   config_file: /home/ubuntu/.kube/config

接下来,在registries部分,我们将为 Docker Hub 容器注册表添加一项条目,使用我们的用户凭证:

registries:  
   docker:
     url: https://index.docker.io/v1/
     namespace: aric49

现在,我们已经在项目中配置了 OpenShift 和 Docker Hub,我们可以使用ansible-container deploy命令并加上--engine openshift标志,来生成 OpenShift 部署并将镜像文件推送到 Docker Hub。为了区分这些镜像,我们可以使用containerapp标签将它们推送到 Docker Hub。由于我们需要推送多个镜像到 Docker Hub,具体时间取决于你的网络连接速度,这个过程可能需要几分钟才能完成:

ubuntu@node01:~/demo$ ansible-container --engine openshift deploy --push-to docker --username aric49 --tag containerapp
Enter password for aric49 at Docker Hub: 
Parsing conductor CLI args.
Engine integration loaded. Preparing push.      engine=OpenShift™
Tagging aric49/demo-django
Pushing aric49/demo-django:containerapp...
The push refers to a repository [docker.io/aric49/demo-django]
Preparing
Pushing
Mounted from library/centos
Pushing
Pushed
containerapp: digest: sha256:983afc3cb7c0f393d20047d0a1aa75a94a9ab30a2f3503147c09b55a81e007a9 size: 741
Tagging aric49/demo-gulp
Pushing aric49/demo-gulp:containerapp...
The push refers to a repository [docker.io/aric49/demo-gul
TRUNCATED...

一旦部署过程成功完成,我们可以使用ansible-container run命令并加上--engine openshift标志,来启动我们的应用程序并在模拟的 OpenShift 生产环境中运行它。别忘了指定--production标志,这样我们的服务将使用生产配置而不是开发者覆盖配置进行部署:

ubuntu@node01:~/demo$ ansible-container --engine openshift run --production
Parsing conductor CLI args.
Engine integration loaded. Preparing run.       engine=OpenShift™
Verifying service image service=django
Verifying service image service=gulp
Verifying service image service=nginx

PLAY [Manage the lifecycle of demo on OpenShift?] ******************************

TASK [Create project demo] *****************************************************
changed: [localhost]

TASK [Create service] **********************************************************
changed: [localhost]

TASK [Create service] **********************************************************
changed: [localhost]

TASK [Create service] **********************************************************
changed: [localhost]

TASK [Remove service] **********************************************************
ok: [localhost]

TASK [Create deployment, and scale replicas up] ********************************
changed: [localhost]

TASK [Create deployment, and scale replicas up] ********************************
changed: [localhost]
TRUNCATED..

一旦该过程成功完成,我们可以登录到 OpenShift Web 控制台,验证服务是否按预期运行。除非有其他更改,否则容器应用程序已部署到名为 demo 的新项目中,但在 Web 界面中将显示为 Ansible Container Demo,这与我们的 container.yml 配置一致:

图 2:部署到 OpenShift 的 Ansible Container Demo 项目

点击 Ansible Container Demo 项目的显示名称,将显示标准的 OpenShift 仪表盘,演示根据生产配置运行的 Pods。你应该能看到运行中的 djangongnixpostgresql Pods,并且可以在控制台显示的右上角看到访问 Web 应用程序的路由链接:

图 3:演示项目中的运行中的 Pod

我们可以通过点击在 OpenShift 中创建的 nip.io 路由来测试应用程序是否正在运行,并确保 NGINX Web 服务器容器是可以访问的。点击该链接应该会显示完整的简单 Hello you! Django 应用程序:

图 4:在 OpenShift 中运行的 Hello World 页面

这看起来比我们在本地 Vagrant 实验室中进行的 curl 测试要好看多了,不是吗?恭喜你,你已经成功将一个多容器应用程序部署到模拟的生产环境中!

从 OpenShift 控制台中,我们可以验证部署的各个方面是否已存在并按预期功能运行。例如,你可以点击左侧导航栏中的 Storage 链接,验证在 OpenShift 中是否已创建并功能正常的 PVC Postgres 数据。点击 postgres-data 会显示 PVC 对象的详细信息,包括分配的存储(3 GiB)以及在 container.yml 文件中配置的访问模式,Read-Write-Many

图 5:PostgreSQL PVC

参考文献

总结

随着我们即将结束与 Ansible Container 的旅程,我们已经覆盖了在学习使用 Ansible Container 项目自动化容器的过程中,可能是最后一个难关:处理多容器项目。由于几乎所有容器运行时环境(如 Docker、Kubernetes 和 OpenShift)都具备固有的网络功能,因此构建简化的微服务软件栈变得轻而易举。正如本章所示,微服务容器可以像拼积木一样高效地连接在一起,从而在生产环境中构建和部署强大的应用程序。

在这一部分中,我们探讨了容器运行时环境如何通过容器网络架构建立对其他容器的依赖关系,以及如何创建链接依赖。我们观察了这些概念如何协同工作,通过 Gulp、Django、NGINX 和 Postgres 容器构建一个相当复杂的多容器应用程序。我们在开发模式下使用dev_overrides进行了测试,并根据项目配置在生产模式下进行测试。最后,我们将该应用程序部署到本地 OpenShift 集群中,以模拟真实世界的生产部署,完整地包含容器网络和持久化卷声明。

本书的最后一章将介绍你如何扩展对 Ansible Container 的知识,并提供一些关于如何在 Ansible Container 知识上继续深入的实用技巧,帮助你将迄今为止在本书中获得的知识付诸实践。

第九章:深入探索 Ansible Container

在本书的引言章节中,我们了解了信息技术(IT)行业的趋势如何发生根本性的变化,并塑造了应用程序和服务的设计与部署方式。随着高 CPU 和带宽密集型服务的消费需求上升,消费者经常要求更多功能,容忍不了故障,并且希望有更多选项来消费服务和应用程序。为了应对这种变化,单体应用程序部署和静态服务器不再能作为这一老化基础设施的支柱。即便是配置管理和自动化工具,尽管它们非常动态,也无法满足组织在各种平台上不断扩展现有基础设施的需求。

为应对这一趋势,像 Docker 这样的容器化平台迎接挑战,解决了需要一致且可靠地部署和管理应用程序的问题。Docker 容器使得企业和组织能够采用模块化的基础设施,使得应用程序可以完全自包含地构建,并保证能够在任何使用兼容容器运行时环境的系统上运行。这使得软件开发人员和 DevOps 工程师能够快速构建微服务应用程序,类似于许多方面的乐高积木,可以将它们组合在一起设计出大型且复杂的软件堆栈。

虽然微服务应用程序似乎解决了许多当今行业中的问题,但构建和部署微服务应用程序的传统方法正证明不足以满足需求,并且使这些服务的运营人员在真正构建和配置满足组织需求的容器镜像时面临更少的选择。Ansible Container 项目旨在填补传统配置管理与容器构建和部署流水线之间的空白。正如我们在本书中所看到的,Ansible Container 不仅可以利用 Ansible 的强大功能来构建真正定制的容器镜像,还可以管理容器化软件的生命周期,从开发一直到生产。

在本书的最后一章,我想为读者提供一些资源,帮助他们在使用 Ansible Container 构建和部署容器化项目时,走得更远,超越本书的范围。尽管我们已经涵盖了使用和操作 Ansible Container 的所有功能方面,微服务架构是一个快速发展的领域,它不断随着开源社区的变化和发展而变化。本章旨在为读者指引一些有用的方向,作为扩展容器化软件知识的起点,提供管理大规模容器的有用提示,并运用你新获得的知识,帮助推动围绕这些项目的开源社区的发展。

在本章的最后,我们将涵盖以下主题:

  • 编写角色和容器应用的技巧

  • 使用 Ansible Container 构建强大的部署 playbook

  • 排除应用容器故障的技巧

  • 使用 Jenkins 或 Travis CI 进行 CICD 部署

  • 在 GitHub 和 Ansible Galaxy 上共享角色和应用

  • 容器化一切

编写角色和容器应用的技巧

如果你是第一次接触 Ansible 和 playbook 语法,在编写 playbook 时很容易感到困惑。尽管 Ansible 本身是一种非常易读且对非程序员友好的语言,但在编写角色或容器应用时,仍有一些陷阱需要记住,以便最大限度地提高可用性。

使用完整的 YAML 语法

当我在使用 Ansible 代码时,一个个人的恼怒点是作者使用我所称之为简化方法来编写 playbook。实际上,功能性的 Ansible 代码可以通过将模块调用和属性写在同一行上,使用等号(=)来分隔属性和值,就像下面的代码所示。

简化方法示例代码

- name: Deploy configuration file
  template: src=ConfigFile.j2 dest=/etc/myApp/myConfig.yml mode=0644

- name: Install Package
  apt: name=myApp state=present update_cache=true

正确的 YAML 语法示例代码

- name: Deploy configuration file
  template:
    src: ConfigFile.j2
    dest: /etc/myApp/myConfig.j2
    mode: 0644

- name: Install Package
  apt:
    name: myApp
    state: present
    update_cache: true

如你所见,简化方法和正确的 YAML 语法在功能上是相同的,但它们在视觉上有所不同。使用正确的 YAML 语法会将模块调用分布在多行上,并要求用户将模块属性缩进到模块调用的定义下方。这使得 playbook 更容易一眼看懂,也更容易在查找错误时进行调试。使用正确的 YAML 语法还能确保你的文本编辑器可以对代码进行正确的语法高亮显示,因为它符合标准的 YAML 规范。然而,使用简化方法虽然可以更快速地编写 Ansible 代码,但却牺牲了可读性和可用性,尤其是对于未来可能使用你 playbook 的其他人而言。这不仅在视觉上不吸引人,而且使得阅读代码和理解功能变得困难。最佳实践是养成使用完整的 YAML 语法和缩进来编写 Ansible playbook 和角色的习惯。其他使用你代码并为其贡献的人会感谢你。

使用 Ansible 模块

在刚开始编写 Ansible playbook 和角色时,人们很容易对几乎所有任务都使用 shellcommand 模块。如果你对 BASH 以及大多数 Linux 操作系统中原生提供的 GNU/Linux 工具和实用程序有扎实的理解,那么使用 shell 或 command 模块来构建 playbook 是合乎逻辑的。但这种方法的问题在于,它忽视了 Ansible 自带的超过千个独特模块。

虽然shellcommand在某些情况下确实有其作用,但你应该首先检查是否有 Ansible 模块可以编程地完成你想要实现的目标。使用 Ansible 模块而不是直接在 shell 上运行命令的好处在于,Ansible 模块具有评估幂等性的能力,并且只有在目标不处于所需状态时才会采取行动。虽然使用命令行模块也能实现幂等性,但这要难得多。此外,Ansible 模块具有独特的能力,可以在内存中存储和检索任务的元数据。例如,你可以在任务定义中添加 register 行,将任务元数据存储到名为 task_output 的变量中。在 playbook 中,你可以通过检查 task_output.changed == true 来查看该任务是否对系统进行了更改,并据此采取相应的行动。同样,这一逻辑也可以用于检查任务的返回代码、搜索元数据或在任务失败时采取行动。使用模块使你能够自由地利用 Ansible 按照你想要的方式工作。

使用 Ansible Core 构建强大的部署 playbook

正如我们在本章中所看到的,Ansible Core 本质上是在部署执行过程中后台工作的引擎。我们查看了如何从 ansible-deployment 目录中提取这些 playbook 并手动运行它们,传入相应的标签手动执行运行、停止、重启和销毁功能。然而,这些 playbook 一般都是有限的,形式和功能都很基础。不要以为在部署项目时,你只能局限于运行 ansible-container deploy 命令,或者手动执行部署 playbook。如果你查看自动生成的部署 playbook,你会注意到它们调用了 docker_service 模块,这是 Ansible Core 中的一个模块。使用类似的方法,你可以编写自己的 playbook 来构建完全自定义的部署,超出 Ansible Container 的范围。

一个很好的使用案例可能是你有其他服务,这些服务依赖于你使用 Ansible Container 构建的容器化项目的状态。这些服务可以是监控服务、数据库集群,甚至是你希望在启动过程中让容器拉取数据的外部基础设施 API。通过使用单独的 Ansible Core playbook,你可以完全控制启动容器并与依赖服务交互的过程。这里有一个示例代码片段,帮助你获得一些灵感。请注意,我们将项目名称定义为一个变量,并将其传递到 REST API 调用中,以注册服务:

- name:Deploy New Service
  hosts: localhost
  connection: local
  gather_facts: no
  vars:
    ProjectName: "MyAwesomeApp"

  tasks:

    #Start the Container Service using the variable defined above
    - docker_service:
        project_name:"{{ ProjectName }}"
        definition:
            App:
              image: MyContainer:tag
              command: /usr/bin/dumb-init AwesomeApp
      register: ServiceStarted

#Register the service only when the container is updated (changed)
- name: Register Service in API
  uri:
    url: https://your.service.example.com/api/v2/
    method: POST
    body: "service: {{ ProjectName }}, state: deployed"
    body_format: yaml
  when: ServiceStarted.changed

如你所见,使用 Ansible Core playbooks 部署容器化基础设施是一个非常强大的工具。当使用 Ansible Core playbook 模块来抽象化你的部署时,你的想象力将是唯一的限制。

故障排除应用容器

在构建容器化服务和应用程序的过程中,您不可避免地会遇到需要排查异常容器的情况。有时,容器由于配置错误的 container start 命令或入口点而无法启动。其他时候,容器本身会开始抛出错误,需进行调试或诊断。大多数情况下,可以通过查看容器日志或在 OpenShift、Docker 或 Kubernetes 中查看容器运行时详细信息来排查这些问题。以下是我多年来在不同容器化运行时环境中找到的最有帮助的命令列表:

  • Docker

    • docker logs:使用 docker logs 命令查看任何已停止、正在运行或退出的容器的标准输出日志。通常情况下,当容器停止时,它们最后一条记录的标准输出消息会确认容器停止的原因。Docker 日志也有助于实时调试容器中的错误。此命令的完整语法是 docker logs [容器名称或 ID]

    • docker inspect:此命令可用于查看几乎所有 Docker 资源的所有属性和配置细节,如容器、网络或存储卷,以 JSON 格式显示。inspect 命令对于理解 Docker 如何查看相应的资源以及它是否获取了某些配置参数非常有用。inspect 的完整语法是 docker inspect [docker 资源名称或 ID]

  • Kubernetes

    • kubectl logskubectl logs 用于查看 Kubernetes 中运行的 Pod 的日志。如果 Kubernetes 使用 Docker 作为底层容器运行时环境,则 kubectl logs 输出通常与 docker logs 输出类似。然而,使用 Kubernetes 原生转发日志输出可以让用户通过 Kubernetes 原生抽象检索日志,这可能有助于指示问题所在。结合查看容器运行时日志和 Kubernetes 日志也是非常有帮助的。kubectl logs 的完整语法是 kubectl logs [完整 Pod 名称] --namespace [命名空间名称]

    • kubectl describedescribe 用于查看几乎任何 Kubernetes 资源的详细输出和配置参数。可以在集群节点、Pod、命名空间、副本集和服务等资源上使用。使用 describe,可以查看资源标签、事件消息以及其他配置选项。kubectl describe 的主要好处是能够描述几乎任何集群资源,帮助排查问题,无论是集群节点还是行为异常的 Pod。describe 的完整语法是 kubectl describe [资源类型] [资源名称] --namespace [命名空间名称(如适用)]

  • OpenShift:

    • oc logsoc logskubectl logsdocker logs 非常相似,它允许用户查看特定运行或重启中的 Pod 的日志。与 Kubernetes 类似,比较 oc logs 的输出和 docker logs 的输出通常非常有助于定位问题的根源。oc logs 的完整语法是 oc logs [pod 或资源名称]

    • oc debugdebug 用于创建问题 Pod 或部署的精确副本,以便在不干扰运行服务的情况下进行检查。使用 debug,可以向复制的 Pod 中传入命令,进行调试操作,如打开 shell 或转储环境变量。debug 的完整语法是 oc debug [pod、deployment 或资源名称] -- [要执行的命令]

使用 Jenkins 或 TravisCI 创建构建流水线

近年来,持续集成持续部署CICD)的概念已经在软件开发社区掀起了热潮。像 Jenkins、TeamCity 和 TravisCI 这样的丰富项目为开发人员提供了自动化框架,代码更改可以自动构建、测试,并在通过后部署到基础设施上。通过使用 CICD 工具,采用敏捷等快速软件开发方法的开发人员可以比以往更快、更可靠地部署软件。

大多数 CICD 工具和工作流通过定义具有特定触发条件的任务来运行。这些触发条件可以是 Git 仓库中的代码提交、用户手动启动构建,或是直接调用 CICD API 的自动化过程。这些任务执行非常具体和自动化的功能,如构建代码、对新构建的代码进行测试,甚至处理代码变更的部署。CICD 任务甚至可以在构建、测试或部署步骤失败时向聊天室或电子邮件发送通知。有些甚至足够智能,能够通知提交代码的用户,如果某个特定的变更导致了构建失败。CICD 的主要好处在于它为软件开发人员提供的自动化,确保这些任务能够可靠、一致和定期地执行。

想象一下,CICD 工具为 Ansible 容器项目带来的巨大好处:

  • 在检查代码到某个 Git 仓库分支时自动执行 ansible-container build

  • 在 CICD 主机上执行 ansible-container run,以在本地启动容器并触发烟雾测试,以确保容器按预期运行

  • 如果构建和测试步骤通过,可以在镜像上执行 ansible-container push,它会用特定的构建号标记这些镜像,并将它们推送到容器镜像注册中心,如 Docker Hub

  • 自动触发 ansible-container deployansible-container run,将项目部署到特定环境(甚至是生产环境!)

使用自动化的 CICD 工具,你可以朝着完全自动化的构建和部署流水线迈进,这个过程完全不需要人工干预。如果你对 CICD 工具如何帮助你和你的工作流程感到好奇,我建议你注册一个免费的 Travis CI 账户:travisci.org。如果你对一个完全免费的开源解决方案,用于在自己的基础设施中部署,我推荐 Jenkins:jenkins.io

此外,你可以查看 Travis CI 构建流水线,这些流水线构建了我们在本书中涉及的一些 Ansible 容器项目。例如,你可以在这里找到 django-gulp-nginx 项目:travis-ci.org/ansible/django-gulp-nginx

在 GitHub 和 Ansible Galaxy 上共享角色和应用程序

Ansible Galaxy 是一个绝对棒的资源,可以用来复用社区开发的最佳 playbooks 和角色。如我们在本书中所见,Ansible Galaxy 主持了数百个 Ansible 核心角色、容器角色和容器应用。这些项目是由社区开发并分享的,得益于组成庞大 Ansible 生态系统的那些杰出且聪明的人的友好与无私的精神。然而,分享 Ansible 角色到 Galaxy 并不仅仅是为了一些 Ansible 老手准备的。任何人都可以在 galaxy.ansible.com 注册一个账户,并分享已经托管在 GitHub 仓库中的项目。在我与 Ansible 社区合作的经验中,如果你开发了一个非常酷的角色或应用,能够解决某个问题,几乎可以保证会有其他人也在为解决相同的问题而挣扎。通过将你的代码贡献到 Ansible Galaxy,你不仅是在帮助他人,还在为他人帮助你打开大门。很多时候,别人会直接为你的代码贡献,或通过留下反馈,提出改进或修复的意见和建议。新的、更好的版本也可以由社区成员贡献,并被重新使用,从而让你的生活更加轻松。这正是我如此喜爱开源软件的原因之一:我们作为一个社区,可以实现那些通常很难单独完成的目标。

至少,你应该将代码提交到 GitHub 仓库中,以便分享你的项目,并让其他用户在未来能够访问到你的代码。GitHub 仓库也有助于你对项目进行版本控制和跟踪变更。要使用 GitHub,可以在 github.com 注册一个账户:

图 1:github.com 首页

github.com/ 首页,你可以通过提供用户名、密码和有效的电子邮件地址来注册一个免费账户。创建账户并首次登录后,你可以通过点击屏幕右上角的 +(加号)下拉菜单,选择“新建仓库”来创建一个 GitHub 仓库。这将引导你进入一个表单页面,在这里你可以提供有关新项目的详细信息:

图 2:创建一个新的 GitHub 仓库

一旦新的 GitHub 仓库创建完成,你可以使用 SSH 或 HTTPS 链接克隆该仓库并开始贡献代码。每个 GitHub 仓库都有一个唯一的公共 URL,可以用来分享该仓库的链接。这个链接是将你的代码分享至 Ansible Galaxy 所必需的:

图 3:克隆一个空的 Git 仓库

以下示例演示了一个典型的 Git 工作流程:克隆存储库,创建初始文件,并将它们提交到存储库中。

aric@local:$ git clone https://github.com/aric49/AwesomeApplication.git
Cloning into 'AwesomeApplication'...
warning: You appear to have cloned an empty repository.

aric@local:$ git status
fatal: Not a git repository (or any of the parent directories): .git
aric@local:$ cd AwesomeApplication/
aric@local:AwesomeApplication$ touch file.txt
aric@local:AwesomeApplication$ git status
On branch master

Initial commit

Untracked files:
 (use "git add <file>..." to include in what will be committed)

 file.txt

nothing added to commit but untracked files present (use "git add" to track)

aric@local:AwesomeApplication$ git add file.txt
 aric@local:AwesomeApplication$ git commit -m "Adding code to my git repo"
[master (root-commit) 8bfd103] Adding code to my git repo
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file.txt

有关 Ansible Galaxy 和在线共享角色或应用程序的更多信息,请阅读位于 galaxy.ansible.com/intro 的 Ansible Galaxy 文档。

容器化一切!

在您迈向完全模块化的容器化基础设施的旅程中,这是一个令人兴奋的过程。我相信,当您逐步学习本书中包含的示例时,无疑会感染上容器热情。希望您已经看到了容器的强大之处,特别是在诸如 Kubernetes 和 OpenShift 等容器编排解决方案中运行时。在我们的旅程结束之前,我给您的最后建议是坚持下去!现在您已经完全理解了整个 Ansible 容器工作流程,请继续构建项目并尽可能容器化。在过去几年中,如果 DevOps 和基础设施领域有什么可以证明的,那就是容器是我们所知软件的未来。随着越来越多的企业和组织采用容器化解决方案,对具有构建和部署容器的坚实理解的合格开发人员的需求也在不断增长。

我个人的经验法则是尽可能频繁地和尽可能多地进行容器化。为我正在开发的应用程序创建可重用的 Docker 容器使我能够在需要的时候快速构建、销毁、部署和重新部署整个项目。使用 Ansible Container 允许我作为一名 DevOps 工程师构建能够与我的基础设施说同一种语言的容器:Ansible。使用 Ansible,当我在进行基础设施自动化和构建容器化项目时,我不再需要在脑海中切换模式。我能够像开发任何其他 Ansible 项目一样快速开发容器。这让我非常有动力和动力,以适应以前编写的 Ansible 角色来构建和部署可以在任何容器化环境中运行的 Ansible 容器。随后,这也让我思考我目前正在进行的项目以及如何最好地调整我的自动化策略以适应容器化环境。

观察目前的一些项目,例如 CoreOS 的 Container Linux (coreos.com/why/) 和其他完全容器化的操作系统平台,可以明显看出,所有的东西,甚至整个操作系统,最终都会容器化。现在就开始吧!通过容器化你正在处理的所有内容,你将使工作更加高效、可重复,并且可以在本地进行测试。不仅如此,你还在确保你的平台、应用程序和基础设施是面向未来的。即使你的团队目前还没有考虑容器化,你也应该考虑。云基础设施和容器化的世界发展迅速,如果不拥抱真正的模块化软件开发方法论,你肯定会被抛在后头。

参考文献

总结

当我大约两年前开始使用 Docker 容器时,最初我认为构建和管理容器是一项非常麻烦的工作,因为我可以通过编写 Ansible playbooks 快速高效地完成相同的事情。最终,我掌握了 Dockerfile 语法,并开始真正理解将应用程序部署到容器中有多么强大。最终让我彻底信服容器的强大,是当我使用 Kolla 项目构建了一个完整的 OpenStack 云时 (docs.openstack.org/kolla/latest/)。Kolla 项目旨在通过使用 Ansible Playbooks 在 Docker 容器中部署 OpenStack 服务,从而部署一个完整的 OpenStack 云解决方案。使用 Kolla,我可以在大约 30 分钟内部署一个完整的多节点 OpenStack 集群。作为曾经使用 Chef 和 Ansible 自动化各种 OpenStack 组件的背景下,我完全惊讶于这个速度和简便性。

大约一年前,我开始关注 Ansible Container 项目,作为我当时从事 Docker、OpenStack 和 Kubernetes 工作的补充。当时,我认为 Ansible Container 是我所需的拼图缺失部分,它可以让我将 Ansible 作为一个完整的端到端开发和部署解决方案来处理我的容器工作。到目前为止,这一旅程绝对令人惊叹。在社区的支持下,我在过去一年多的时间里,个人和职业上都使用 Ansible Container 自动化和部署了许多项目。作为一名 DevOps 工程师,Ansible Container 给我的灵活性远远超过了构建标准 Dockerfiles,让我惊叹不已。

我希望你能带着热情完成这本书的阅读,无论你从事哪个行业或领域,都能继续推动你的工作和职业发展。开源软件让我惊叹不已的一点是,来自各个行业、各种背景的人都在积极采用这些技术。我希望,当你在使用 Ansible Container 逐步深入时,能时刻关注未来并保持敏锐的洞察力。随着越来越多的人采纳这些技术,越来越聪明的人会继续为这些平台贡献新功能,使它们变得更加完善。感谢你抽出时间阅读本书。同时,我也要感谢那些撰写 Ansible Container 的了不起的团队成员:Chris Houseknecht(@chouseknecht)和 Joshua Ginsberg(@j00bar),他们在很多示例中给予了我极大的支持,修复了 bug,并为这个显然充满激情的项目提供了出色的支持。如果本书对你有帮助,请在 Twitter 或 Freenode IRC 上给我留言:@aric49

posted @ 2025-07-02 17:45  绝不原创的飞龙  阅读(44)  评论(0)    收藏  举报