Docker-AWS-教程(全)

Docker AWS 教程(全)

原文:zh.annas-archive.org/md5/13D3113D4BA58CEA008B572AB087A5F5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎阅读《在亚马逊网络服务上使用 Docker》!我非常兴奋能够写这本书,并分享如何利用 Docker 和亚马逊网络服务(AWS)生态系统提供的精彩技术,构建真正世界一流的解决方案,用于部署和运营您的应用程序。

Docker 已成为构建、打包、发布和运营应用程序的现代标准,利用容器的力量来提高应用程序交付速度,增加安全性并降低成本。本书将向您展示如何通过使用持续交付的最佳实践,来加速构建 Docker 应用程序的过程,提供一个完全自动化、一致、可靠和可移植的工作流程,用于测试、构建和发布您的 Docker 应用程序。在我看来,这是在考虑将应用程序部署到云端之前的基本先决条件,本书的前几章将重点介绍建立本地 Docker 环境,并为我们在整本书中将使用的示例应用程序创建一个本地持续交付工作流程。

AWS 是全球领先的公共云服务提供商,提供丰富的解决方案来管理和运营您的 Docker 应用程序。本书将涵盖 AWS 提供的所有主要服务,以支持 Docker 和容器,包括弹性容器服务(ECS)、Fargate、弹性 Beanstalk 和弹性 Kubernetes 服务(EKS),还将讨论您如何利用 Docker Inc 提供的 Docker for AWS 解决方案来部署 Docker Swarm 集群。

在 AWS 中运行完整的应用程序环境远不止您的容器平台,这本书还将描述管理访问 AWS 账户的最佳实践,并利用其他 AWS 服务来支持应用程序的要求。例如,您将学习如何设置 AWS 应用程序负载均衡器,为您的应用程序发布高可用、负载均衡的端点,创建 AWS 关系数据库服务(RDS)实例,提供托管的应用程序数据库,将您的应用程序集成到 AWS Secrets Manager 中,提供安全的秘密管理解决方案,并使用 AWS CodePipeline、CodeBuild 和 CloudFormation 服务创建完整的持续交付管道,该管道将自动测试、构建和发布 Docker 镜像,以适应您应用程序的任何新更改,并自动将其部署到开发和生产环境。

您将使用 AWS CloudFormation 服务构建所有这些支持基础设施,该服务提供了强大的基础设施即代码模板,允许您定义我提到的所有 AWS 服务和资源,并将其部署到 AWS,只需点击一个按钮。

我相信现在你一定和我一样对学习所有这些美妙的技术充满了期待,我相信在读完这本书之后,你将拥有部署和管理 Docker 应用程序所需的专业知识和技能,使用最新的前沿技术和最佳实践。

这本书适合谁

《在亚马逊网络服务上使用 Docker》适用于任何希望利用容器、Docker 和 AWS 的强大功能来构建、部署和操作应用程序的人。

读者最好具备对 Docker 和容器的基本理解,并且已经使用过 AWS 或其他云服务提供商,尽管不需要有容器或 AWS 的先前经验,因为这本书采用了一步一步的方法,并在您进展时解释关键概念。了解如何使用 Linux 命令行、Git 和基本的 Python 脚本知识将是有用的,但不是必需的。

请参阅“充分利用本书”部分,了解推荐的先决条件技能的完整列表。

本书涵盖了什么

第一章,“容器和 Docker 基础”,将简要介绍 Docker 和容器,并概述 AWS 中可用于运行 Docker 应用程序的各种服务和选项。您将设置您的本地环境,安装 Docker、Docker Compose 和其他各种工具,以完成每章中的示例。最后,您将下载示例应用程序,并学习如何在本地测试、构建和运行应用程序,以便您对应用程序的工作原理和您需要执行的特定任务有一个良好的理解,以使应用程序正常运行。

第二章,“使用 Docker 构建应用程序”,将描述如何构建一个完全自动化的基于 Docker 的工作流程,用于测试、构建、打包和发布您的应用程序作为生产就绪的 Docker 发布映像,使用 Docker、Docker Compose 和其他工具。这将建立一个便携式的持续交付工作流的基础,您可以在多台机器上一致地执行,而无需在每个本地环境中安装特定于应用程序的依赖项。

第三章,“开始使用 AWS”,将描述如何创建一个免费的 AWS 账户,并开始使用各种免费的服务,让您熟悉 AWS 提供的广泛的服务。您将学习如何为您的账户建立最佳实践的管理和用户访问模式,配置增强安全性的多因素身份验证(MFA)并安装 AWS 命令行界面,该界面可用于各种操作和自动化用例。您还将介绍 CloudFormation,这是 AWS 免费提供的管理工具和服务,您将在本书中使用它,它允许您使用强大而富有表现力的基础设施即代码模板格式,通过单击按钮部署复杂的环境。

第四章,ECS 简介,将帮助您快速上手弹性容器服务(ECS),这是在 AWS 中运行 Docker 应用程序的旗舰服务。您将了解 ECS 的架构,创建您的第一个 ECS 集群,使用 ECS 任务定义定义您的容器配置,然后将 Docker 应用程序部署为 ECS 服务。最后,您将简要介绍 ECS 命令行界面(CLI),它允许您与本地 Docker Compose 文件进行交互,并使用 ECS 自动部署 Docker Compose 资源到 AWS。

第五章,使用 ECR 发布 Docker 镜像,将教您如何使用弹性容器注册表(ECR)建立一个私有的 Docker 注册表,使用 IAM 凭证对您的注册表进行身份验证,然后将 Docker 镜像发布到注册表中的私有存储库。您还将学习如何与其他账户和 AWS 服务共享您的 Docker 镜像,以及如何配置生命周期策略以自动清理孤立的镜像,确保您只支付活动和当前的镜像。

第六章,构建自定义 ECS 容器实例,将向您展示如何使用一种流行的开源工具 Packer 来构建和发布自定义的 Amazon Machine Images(AMIs)用于在 ECS 集群中运行您的容器工作负载的 EC2 实例(ECS 容器实例)。您将安装一组辅助脚本,使您的实例能够与 CloudFormation 集成,并在实例创建时下载自定义的配置操作,从而使您能够动态配置 ECS 集群,配置实例应发布日志信息的 CloudWatch 日志组,并最终向 CloudFormation 发出信号,表明配置已成功或失败。

第七章,创建 ECS 集群,将教您如何基于利用上一章中创建的自定义 AMI 的特性来构建基于 EC2 自动扩展组的 ECS 集群。您将使用 CloudFormation 定义您的 EC2 自动扩展组、ECS 集群和其他支持资源,并配置 CloudFormation Init 元数据来执行自定义运行时配置和 ECS 容器实例的配置。

第八章,“使用 ECS 部署应用程序”,将扩展上一章创建的环境,添加支持资源,如关系数据库服务(RDS)实例和 AWS 应用程序负载均衡器(ALB)到您的 CloudFormation 模板中。然后,您将为示例应用程序定义一个 ECS 任务定义和 ECS 服务,并学习 ECS 如何执行应用程序的自动滚动部署和更新。为了编排所需的部署任务,如运行数据库迁移,您将扩展 CloudFormation 并编写自己的 Lambda 函数,创建一个 ECS 任务运行器自定义资源,提供运行任何可以作为 ECS 任务执行的配置操作的强大能力。

第九章,“管理机密”,将介绍 AWS Secrets Manager,这是一个完全托管的服务,可以以加密格式存储机密数据,被授权方(如您的用户、AWS 资源和应用程序)可以轻松安全地访问。您将使用 AWS CLI 与 Secrets Manager 进行交互,为敏感凭据(如数据库密码)创建机密,然后学习如何为容器创建入口脚本,在容器启动时将机密值作为内部环境变量注入,然后交给主应用程序。最后,您将创建一个 CloudFormation 自定义资源,将机密暴露给不支持 AWS Secrets Manager 的其他 AWS 服务,例如为关系数据库服务(RDS)实例提供管理密码。

第十章,“隔离网络访问”,描述了如何在 ECS 任务定义中使用 awsvpc 网络模式,以隔离网络访问,并将 ECS 控制平面通信与容器和应用程序通信分开。这将使您能够采用最佳安全实践模式,例如在私有网络上部署您的容器,并实现提供互联网访问的解决方案,包括 AWS VPC NAT 网关服务。

第十一章,“管理 ECS 基础设施生命周期”,将为您提供在运行 ECS 集群时的操作挑战的理解,其中包括将 ECS 容器实例移出服务,无论是为了缩减自动扩展组还是用新的 Amazon 机器映像替换 ECS 容器实例。您将学习如何利用 EC2 自动扩展生命周期挂钩,在 ECS 容器实例即将被终止时调用 AWS Lambda 函数,这允许您执行优雅的关闭操作,例如将活动容器转移到集群中的其他实例,然后发出 EC2 自动扩展以继续实例终止。

第十二章,“ECS 自动扩展”,将描述 ECS 集群如何管理 CPU、内存和网络端口等资源,以及这如何影响您的集群容量。如果您希望能够动态自动扩展您的集群,您需要动态监视 ECS 集群容量,并在容量阈值处扩展或缩减集群,以确保您将满足组织或用例的服务水平期望。您将实施一个解决方案,该解决方案在通过 AWS CloudWatch Events 服务生成 ECS 容器实例状态更改事件时计算 ECS 集群容量,将容量指标发布到 CloudWatch,并使用 CloudWatch 警报动态地扩展或缩减您的集群。有了动态集群容量解决方案,您将能够配置 AWS 应用程序自动扩展服务,根据适当的指标(如 CPU 利用率或活动连接)动态调整服务实例的数量,而无需担心对底层集群容量的影响。

第十三章,“持续交付 ECS 应用程序”,将使用 AWS CodePipeline 服务建立一个持续交付流水线,该流水线与 GitHub 集成,以侦测应用程序源代码和基础设施部署脚本的更改,使用 AWS CodeBuild 服务运行单元测试,构建应用程序构件并使用示例应用程序 Docker 工作流发布 Docker 镜像,并使用本书中迄今为止使用的 CloudFormation 模板持续部署您的应用程序更改到 AWS。

这将自动部署到一个您测试的 AWS 开发环境中,然后创建一个变更集和手动批准操作,以便将其部署到生产环境,为您的所有应用程序新功能和错误修复提供了一个快速且可重复的生产路径。

第十四章《Fargate 和 ECS 服务发现》将介绍 AWS Fargate,它提供了一个完全管理传统上需要使用常规 ECS 服务来管理的 ECS 服务控制平面和 ECS 集群的解决方案。您将使用 Fargate 部署 AWS X-Ray 守护程序作为 ECS 服务,并配置 ECS 服务发现,动态发布您的服务端点使用 DNS 和 Route 53。这将允许您为您的示例应用程序添加对 X-Ray 跟踪的支持,该跟踪可用于跟踪传入的 HTTP 请求到您的应用程序,并监视 AWS 服务调用、数据库调用和其他类型的调用,这些调用是为了服务每个传入请求。

第十五章《弹性 Beanstalk》将介绍流行的平台即服务PaaS)提供的概述,其中包括对 Docker 应用程序的支持。您将学习如何创建一个弹性 Beanstalk 多容器 Docker 应用程序,建立一个由托管的 EC2 实例、一个 RDS 数据库实例和一个应用负载均衡器ALB)组成的环境,然后使用各种技术扩展环境,以支持 Docker 应用程序的要求,例如卷挂载和在每个应用程序部署中运行单次任务。

第十六章,AWS 中的 Docker Swarm,将重点介绍如何在 AWS 中运行 Docker Swarm 集群,使用为 Docker Swarm 社区版提供的 Docker for AWS 蓝图。该蓝图为您提供了一个 CloudFormation 模板,在几分钟内在 AWS 中建立一个预配置的 Docker Swarm 集群,并与关键的 AWS 服务(如弹性负载均衡(ELB)、弹性文件系统(EFS)和弹性块存储(EBS)服务)进行集成。您将使用 Docker Compose 定义一个堆栈,该堆栈配置了以熟悉的 Docker Compose 规范格式表示的多服务环境,并学习如何配置关键的 Docker Swarm 资源,如服务、卷和 Docker 秘密。您将学习如何创建由 EFS 支持的共享 Docker 卷,由 EBS 支持的可重定位 Docker 卷,Docker Swarm 将在节点故障后自动重新连接到重新部署的新容器,并使用由 Docker Swarm 自动创建和管理的 ELB 为您的应用程序发布外部服务端点。

第十七章,弹性 Kubernetes 服务,介绍了 AWS 最新的容器管理平台,该平台基于流行的开源 Kubernetes 平台。您将首先在本地 Docker Desktop 环境中设置 Kubernetes,该环境包括 Docker 18.06 CE 版本对 Kubernetes 的本机支持,并学习如何使用多个 Kubernetes 资源(包括 pod、部署、服务、秘密、持久卷和作业)为您的 Docker 应用创建完整的环境。然后,您将在 AWS 中建立一个 EKS 集群,创建一个 EC2 自动扩展组,将工作节点连接到您的集群,并确保您的本地 Kubernetes 客户端可以对 EKS 控制平面进行身份验证和连接,之后您将部署 Kubernetes 仪表板,为您的集群提供全面的管理界面。最后,您将定义一个使用弹性块存储(EBS)服务的默认存储类,然后将您的 Docker 应用部署到 AWS,利用您之前为本地环境创建的相同 Kubernetes 定义,为您提供一个强大的解决方案,可以快速部署用于开发目的的 Docker 应用程序,然后使用 EKS 直接部署到生产环境。

为了充分利用本书

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保使用最新版本的解压软件解压文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Docker-on-Amazon-Web-Services。如果代码有更新,将在现有的 GitHub 存储库中进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/DockeronAmazonWebServices_ColorImages.pdf

代码演示

访问以下链接查看代码运行的视频:

bit.ly/2Noqdpn

使用的约定

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

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:“请注意,要点中包含了一个名为PASTE_ACCOUNT_NUMBER的占位符,位于策略文档中,因此您需要将其替换为您的实际 AWS 账户 ID。”

代码块设置如下:

AWSTemplateFormatVersion: "2010-09-09"

Description: Cloud9 Management Station

Parameters:
  EC2InstanceType:
    Type: String
    Description: EC2 instance type
    Default: t2.micro
  SubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: Target subnet for instance

任何命令行输入或输出都以以下方式编写:

> aws configure
AWS Access Key ID [None]:

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单中的单词或对话框中的单词会以这种方式出现在文本中。例如:“要创建管理员角色,请从 AWS 控制台中选择服务|IAM,从左侧菜单中选择角色,然后单击创建角色按钮。”

警告或重要说明会出现在这样的样式中。提示和技巧会出现在这样的样式中。

第一章:容器和 Docker 基础知识

Docker 和 Amazon Web Services 是目前最炙手可热和最受欢迎的两种技术。Docker 目前是全球最流行的容器平台,而 Amazon Web Services 是排名第一的公共云提供商。无论是大型还是小型组织都在大规模地采用容器技术,公共云已不再是初创企业的游乐场,大型企业和组织也纷纷迁移到云端。好消息是,本书将为您提供有关如何同时使用 Docker 和 AWS 来帮助您比以往更快更高效地测试、构建、发布和部署应用程序的实用、现实世界的见解和知识。

在本章中,我们将简要讨论 Docker 的历史,为什么 Docker 如此革命性,以及 Docker 的高级架构。我们将描述支持在 AWS 中运行 Docker 的各种服务,并根据组织的需求讨论为什么您可能会选择一个服务而不是另一个服务。

然后,我们将专注于使用 Docker 在本地环境中运行起来,并安装运行本书示例应用程序所需的各种软件前提条件。示例应用程序是一个简单的用 Python 编写的 Web 应用程序,它将数据存储在 MySQL 数据库中,本书将使用示例应用程序来帮助您解决诸如测试、构建和发布 Docker 镜像,以及在 AWS 上部署和运行 Docker 应用程序等真实世界挑战。在将示例应用程序打包为 Docker 镜像之前,您需要了解应用程序的外部依赖项以及测试、构建、部署和运行应用程序所需的关键任务,并学习如何安装应用程序依赖项、运行单元测试、在本地启动应用程序,并编排诸如建立示例应用程序所需的初始数据库架构和表等关键操作任务。

本章将涵盖以下主题:

  • 容器和 Docker 简介

  • 为什么容器是革命性的

  • Docker 架构

  • AWS 中的 Docker

  • 设置本地 Docker 环境

  • 安装示例应用程序

技术要求

以下列出了完成本章所需的技术要求:

  • 满足软件和硬件清单中定义的最低规格的计算机环境

以下 GitHub 网址包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch1.

查看以下视频,了解代码的实际应用:

bit.ly/2PEKlVQ

容器和 Docker 简介

近年来,容器已成为技术世界中的共同语言,很难想象仅仅几年前,只有技术界的一小部分人甚至听说过容器。

要追溯容器的起源,您需要倒回到 1979 年,当时 Unix V7 引入了 chroot 系统调用。chroot 系统调用提供了将运行中进程的根目录更改为文件系统中的不同位置的能力,并且是提供某种形式的进程隔离的第一个机制。chroot 于 1982 年添加到伯克利软件发行版(BSD)中(这是现代 macOS 操作系统的祖先),在容器化和隔离方面没有太多其他进展,直到 2000 年发布了一个名为 FreeBSD Jails 的功能,它提供了称为“jails”的单独环境,每个环境都可以分配自己的 IP 地址,并且可以在网络上独立通信。

2004 年,Solaris 推出了 Solaris 容器的第一个公共测试版(最终成为 Solaris Zones),通过创建区域提供系统资源分离。这是我记得在 2007 年使用的技术,帮助克服了昂贵的物理 Sun SPARC 基础设施的缺乏,并在单个 SPARC 服务器上运行应用程序的多个版本。

在 2000 年代中期,容器的进展更加显著,Open Virtuozzo(Open VZ)于 2005 年发布,它对 Linux 内核进行了补丁,提供了操作系统级的虚拟化和隔离。2006 年,谷歌推出了一个名为进程容器(最终更名为控制组或 cgroups)的功能,提供了限制一组进程的 CPU、内存、网络和磁盘使用的能力。2008 年,一个名为 Linux 命名空间的功能,提供了将不同类型的资源相互隔离的能力,与 cgroups 结合起来创建了 Linux 容器(LXC),形成了今天我们所知的现代容器的初始基础。

2010 年,随着云计算开始流行起来,一些平台即服务(PaaS)初创公司出现了,它们为特定的应用程序框架(如 Java Tomcat 或 Ruby on Rails)提供了完全托管的运行时环境。一家名为 dotCloud 的初创公司非常不同,因为它是第一家“多语言”PaaS 提供商,意味着您可以使用他们的服务运行几乎任何应用程序环境。支撑这一技术的是 Linux 容器,dotCloud 添加了一些专有功能,为他们的客户提供了一个完全托管的容器平台。到了 2013 年,PaaS 市场已经真正进入了 Gartner 炒作周期的失望低谷,dotCloud 濒临财务崩溃。该公司的联合创始人之一 Solomon Hykes 向董事会提出了一个开源他们的容器管理技术的想法,他感觉到有巨大的潜力。然而,董事会不同意,但 Solomon 和他的技术团队仍然继续前进,剩下的就是历史。

在 2013 年将 Docker 作为一个新的开源容器管理平台向世界宣布后,Docker 迅速崛起,成为开源世界和供应商社区的宠儿,很可能是历史上增长最快的技术之一。到 2014 年底,Docker 1.0 发布时,已经下载了超过 1 亿个 Docker 容器 - 快进到 2018 年 3 月,这个数字已经达到了370 亿次下载。到 2017 年底,财富 100 强公司中使用容器的比例达到了 71%,表明 Docker 和容器已经成为创业公司和企业普遍接受的技术。如今,如果您正在构建基于微服务架构的现代分布式应用程序,那么您的技术栈很可能是以 Docker 和容器为基础。

容器为何是革命性的

容器的简短而成功的历史证明了它的价值,这引出了一个问题,为什么容器如此受欢迎?以下提供了这个问题的一些更重要的答案:

  • 轻量级:容器经常与虚拟机进行比较,在这种情况下,容器比虚拟机要轻量得多。与典型虚拟机需要几分钟启动时间相比,容器可以在几秒钟内为您的应用程序启动一个隔离和安全的运行时环境。容器镜像也比虚拟机镜像要小得多。

  • 速度:容器很快 - 它们可以在几秒内下载和启动,并且在几分钟内您就可以测试、构建和发布您的 Docker 镜像以供立即下载。这使得组织能够更快地创新,这在当今竞争日益激烈的环境中至关重要。

  • 便携:Docker 使您能够更轻松地在本地机器、数据中心和公共云上运行应用程序。因为 Docker 包含了应用程序的完整运行时环境,包括操作系统依赖和第三方软件包,您的容器主机不需要任何特殊的预先设置或针对每个应用程序的特定配置 - 所有这些特定的依赖和要求都包含在 Docker 镜像中,使得“但在我的机器上可以运行!”这样的评论成为过去的遗迹。

  • 安全性:关于容器安全性的讨论很多,但在我看来,如果实施正确,容器实际上比非容器替代方法提供了更高的安全性。主要原因是容器非常好地表达了安全上下文 - 在容器级别应用安全控制通常代表了这些控制的正确上下文级别。很多这些安全控制都是“默认”提供的 - 例如,命名空间本质上是一种安全机制,因为它们提供了隔离。一个更明确的例子是,它们可以在每个容器基础上应用 SELinux 或 AppArmor 配置文件,这样很容易根据每个容器的特定安全要求定义不同的配置文件。

  • 自动化:组织正在采用诸如持续交付之类的软件交付实践,其中自动化是基本要求。Docker 本身支持自动化 - 在其核心,Dockerfile 是一种自动化规范,允许 Docker 客户端自动构建您的容器,而其他 Docker 工具如 Docker Compose 允许您表达连接的多容器环境,您可以在几秒钟内自动创建和拆除。

Docker 架构

正如本书前言中所讨论的,我假设您至少具有基本的 Docker 工作知识。如果您是 Docker 的新手,那么我建议您通过阅读docs.docker.com/engine/docker-overview/上的 Docker 概述,并通过运行一些 Docker 教程来补充学习本章内容。

Docker 架构包括几个核心组件,如下所示:

  • Docker 引擎:它提供了用于运行容器工作负载的几个服务器代码组件,包括用于与 Docker 客户端通信的 API 服务器,以及提供 Docker 核心运行时的 Docker 守护程序。守护程序负责完整的容器和其他资源的生命周期,并且还内置了集群支持,允许您构建 Docker 引擎的集群或群集。

  • Docker 客户端:这提供了一个用于构建 Docker 镜像、运行 Docker 容器以及管理其他资源(如 Docker 卷和 Docker 网络)的客户端。Docker 客户端是您在使用 Docker 时将要使用的主要工具,它与 Docker 引擎和 Docker 注册表组件进行交互。

  • Docker 注册表:这负责存储和分发您应用程序的 Docker 镜像。Docker 支持公共和私有注册表,并且通过 Docker 注册表打包和分发您的应用程序是 Docker 成功的主要原因之一。在本书中,您将从 Docker Hub 下载第三方镜像,并将自己的应用程序镜像存储在名为弹性容器注册表(ECR)的私有 AWS 注册表服务中。

  • Docker Swarm:Swarm 是一组 Docker 引擎,形成一个自管理和自愈的集群,允许您水平扩展容器工作负载,并在 Docker 引擎故障时提供弹性。Docker Swarm 集群包括一些形成集群控制平面的主节点,以及一些实际运行您的容器工作负载的工作节点。

当您使用上述组件时,您将与 Docker 架构中的许多不同类型的对象进行交互:

  • 镜像:镜像是使用 Dockerfile 构建的,其中包括一些关于如何为您的容器构建运行时环境的指令。执行每个构建指令的结果被存储为一组层,并作为可下载和可安装的镜像进行分发,Docker 引擎读取每个层中的指令,以构建基于给定镜像的所有容器的运行时环境。

  • 容器:容器是 Docker 镜像的运行时表现形式。在幕后,容器由一组 Linux 命名空间、控制组和存储组成,共同创建了一个隔离的运行时环境,您可以在其中运行给定的应用程序进程。

  • :默认情况下,容器的基础存储机制基于联合文件系统,允许从 Docker 镜像中的各个层构建虚拟文件系统。这种方法非常高效,因为您可以共享层并从这些共享层构建多个容器,但是这会带来性能损失,并且不支持持久性。 Docker 卷提供对专用可插拔存储介质的访问,您的容器可以使用该介质进行 IO 密集型应用程序和持久化数据。

  • 网络:默认情况下,Docker 容器各自在其自己的网络命名空间中运行,这提供了容器之间的隔离。但是,它们仍然必须提供与其他容器和外部世界的网络连接。 Docker 支持各种网络插件,支持容器之间的连接,甚至可以跨 Docker Swarm 集群进行扩展。

  • 服务:服务提供了一个抽象,允许您通过在 Docker Swarm 集群中的多个 Docker 引擎上启动多个容器或服务副本来扩展您的应用程序,并且可以在这些 Docker 引擎上进行负载平衡。

在 AWS 中运行 Docker

除了 Docker 之外,本书将针对的另一个主要技术平台是 AWS。

AWS 是世界领先的公共云提供商,因此提供了多种运行 Docker 应用程序的方式:

  • 弹性容器服务(ECS):2014 年,AWS 推出了 ECS,这是第一个支持 Docker 的专用公共云服务。 ECS 提供了一种混合托管服务,ECS 负责编排和部署您的容器应用程序(例如容器管理平台的控制平面),而您负责提供 Docker 引擎(称为 ECS 容器实例),您的容器实际上将在这些实例上运行。 ECS 是免费使用的(您只需支付运行您的容器的 ECS 容器实例),并且消除了管理容器编排和确保应用程序始终运行的许多复杂性。但是,这需要您管理运行 ECS 容器实例的 EC2 基础设施。 ECS 被认为是亚马逊的旗舰 Docker 服务,因此将是本书重点关注的主要服务。

  • Fargate:Fargate 于 2017 年底推出,提供了一个完全托管的容器平台,可以为您管理 ECS 控制平面和 ECS 容器实例。使用 Fargate,您的容器应用程序部署到共享的 ECS 容器实例基础设施上,您无法看到这些基础设施,而 AWS 进行管理,这样您就可以专注于构建、测试和部署容器应用程序,而不必担心任何基础设施。Fargate 是一个相对较新的服务,在撰写本书时,其区域可用性有限,并且有一些限制,这意味着它并不适用于所有用例。我们将在第十四章《Fargate 和 ECS 服务发现》中介绍 Fargate 服务。

  • 弹性 Kubernetes 服务(EKS):EKS 于 2018 年 6 月推出,支持流行的开源 Kubernetes 容器管理平台。EKS 类似于 ECS,它是一个混合托管服务,亚马逊提供完全托管的 Kubernetes 主节点(Kubernetes 控制平面),您提供 Kubernetes 工作节点,以 EC2 自动扩展组的形式运行您的容器工作负载。与 ECS 不同,EKS 并不免费,在撰写本书时,其费用为每小时 0.20 美元,加上与工作节点相关的任何 EC2 基础设施成本。鉴于 Kubernetes 作为一个云/基础设施不可知的容器管理平台以及其开源社区的不断增长的受欢迎程度,EKS 肯定会变得非常受欢迎,我们将在第十七章《弹性 Kubernetes 服务》中介绍 Kubernetes 和 EKS。

  • 弹性 Beanstalk(EBS):Elastic Beanstalk 是 AWS 提供的一种流行的平台即服务(PaaS)产品,提供了一个完整和完全托管的环境,针对不同类型的流行编程语言和应用框架,如 Java、Python、Ruby 和 Node.js。Elastic Beanstalk 还支持 Docker 应用程序,允许您支持各种使用您选择的编程语言编写的应用程序。您将在第十五章《弹性 Beanstalk》中学习如何部署多容器 Docker 应用程序。

  • 在 AWS 中的 Docker Swarm:Docker Swarm 是内置在 Docker 中的本地容器管理和集群平台,利用本地 Docker 和 Docker Compose 工具链来管理和部署容器应用程序。在撰写本书时,AWS 并未提供 Docker Swarm 的托管服务,但 Docker 提供了一个 CloudFormation 模板(CloudFormation 是 AWS 提供的免费基础设施即代码自动化和管理服务),允许您快速在 AWS 中部署与本地 AWS 提供的 Elastic Load Balancing(ELB)和 Elastic Block Store(EBS)服务集成的 Docker Swarm 集群。我们将在章节《在 AWS 中的 Docker Swarm》中涵盖所有这些内容以及更多内容。

  • CodeBuild:AWS CodeBuild 是一个完全托管的构建服务,支持持续交付用例,提供基于容器的构建代理,您可以使用它来测试、构建和部署应用程序,而无需管理与持续交付系统传统相关的任何基础设施。CodeBuild 使用 Docker 作为其容器平台,以按需启动构建代理,您将在章节《持续交付 ECS 应用程序》中介绍 CodeBuild 以及其他持续交付工具,如 CodePipeline。

  • 批处理:AWS Batch 是基于 ECS 的完全托管服务,允许您运行基于容器的批处理工作负载,无需担心管理或维护任何支持基础设施。我们在本书中不会涵盖 AWS Batch,但您可以在aws.amazon.com/batch/了解更多关于此服务的信息。

在 AWS 上运行 Docker 应用程序有各种各样的选择,因此根据组织或特定用例的要求选择合适的解决方案非常重要。

如果您是一家希望快速在 AWS 上使用 Docker 并且不想管理任何支持基础设施的中小型组织,那么 Fargate 或 Elastic Beanstalk 可能是您更喜欢的选项。Fargate 支持与关键的 AWS 服务原生集成,并且是一个构建组件,不会规定您构建、部署或运行应用程序的方式。在撰写本书时,Fargate 并不是所有地区都可用,与其他解决方案相比价格昂贵,并且有一些限制,比如不能支持持久存储。Elastic Beanstalk 为管理您的 Docker 应用程序提供了全面的端到端解决方案,提供了各种开箱即用的集成,并包括操作工具来管理应用程序的完整生命周期。Elastic Beanstalk 确实要求您接受一个非常有主见的框架和方法论,来构建、部署和运行您的应用程序,并且可能难以定制以满足您的需求。

如果您是一个有特定安全和合规要求的大型组织,或者只是希望对运行容器工作负载的基础架构拥有更大的灵活性和控制权,那么您应该考虑 ECS、EKS 和 Docker Swarm。ECS 是 AWS 的本地旗舰容器管理平台,因此拥有大量客户群体多年来一直在大规模运行容器。正如您将在本书中了解到的,ECS 与 CloudFormation 集成,可以让您使用基础设施即代码的方法定义所有集群、应用服务和容器定义,这可以与其他 AWS 资源结合使用,让您能够通过点击按钮部署完整、复杂的环境。尽管如此,ECS 的主要批评是它是 AWS 特有的专有解决方案,这意味着您无法在其他云环境中使用它,也无法在自己的基础设施上运行它。越来越多的大型组织正在寻找基础设施和云无关的云管理平台,如果这是您的目标,那么您应该考虑 EKS 或 Docker Swarm。Kubernetes 已经席卷了容器编排世界,现在是最大和最受欢迎的开源项目之一。AWS 现在提供了 EKS 这样的托管 Kubernetes 服务,这使得在 AWS 中轻松启动和运行 Kubernetes 变得非常容易,并且可以利用与 CloudFormation、弹性负载均衡(ELB)和弹性块存储(EBS)服务的核心集成。Docker Swarm 是 Kubernetes 的竞争对手,尽管它似乎已经输掉了容器编排的霸主地位争夺战,但它有一个优势,那就是作为 Docker 的本地开箱即用功能与 Docker 集成,使用熟悉的 Docker 工具非常容易启动和运行。Docker 目前确实发布了 CloudFormation 模板,并支持与 AWS 服务的关键集成,这使得在 AWS 中轻松启动和运行变得非常容易。然而,人们对这类解决方案的持久性存在担忧,因为 Docker Inc.是一个商业实体,而 Kubernetes 的日益增长的流行度和主导地位可能会迫使 Docker Inc.将未来的重点放在其付费的 Docker 企业版和其他商业产品上。

如您所见,选择适合您的解决方案时有许多考虑因素,而本书的好处在于您将学习如何使用这些方法中的每一种来在 AWS 中部署和运行 Docker 应用程序。无论您现在认为哪种解决方案更适合您,我鼓励您阅读并完成本书中的所有章节,因为您将学到的大部分内容都可以应用于其他解决方案,并且您将更有能力根据您的期望结果定制和构建全面的容器管理解决方案。

设置本地 Docker 环境

完成介绍后,是时候开始设置本地 Docker 环境了,您将使用该环境来测试、构建和部署本书中使用的示例应用程序的 Docker 镜像。现在,我们将专注于启动和运行 Docker,但请注意,稍后我们还将使用您的本地环境与本书中讨论的各种容器管理平台进行交互,并使用 AWS 控制台、AWS 命令行界面和 AWS CloudFormation 服务来管理所有 AWS 资源。

尽管本书的标题是 Docker on Amazon Web Services,但重要的是要注意 Docker 容器有两种类型:

  • Linux 容器

  • Windows 容器

本书专注于 Linux 容器,这些容器旨在在安装了 Docker Engine 的基于 Linux 的内核上运行。当您想要使用本地环境来构建、测试和本地运行 Linux 容器时,这意味着您必须能够访问本地基于 Linux 的 Docker Engine。如果您正在使用基于 Linux 的系统,如 Ubuntu,您可以在操作系统中本地安装 Docker Engine。但是,如果您使用 Windows 或 macOS,则需要设置一个运行 Docker Engine 的本地虚拟机,并为您的操作系统安装 Docker 客户端。

幸运的是,Docker 在 Windows 和 macOS 环境中有很好的打包和工具,使得这个过程非常简单,我们现在将讨论如何在 macOS、Windows 10 和 Linux 上设置本地 Docker 环境,以及本书中将使用的其他工具,如 Docker Compose 和 GNU Make。对于 Windows 10 环境,我还将介绍如何设置 Windows 10 Linux 子系统与本地 Docker 安装进行交互,这将为您提供一个环境,您可以在其中运行本书中使用的其他基于 Linux 的工具。

在我们继续之前,还需要注意的是,从许可的角度来看,Docker 目前有两个不同的版本,您可以在docs.docker.com/install/overview/了解更多信息。

  • 社区版(CE)

  • 企业版(EE)

我们将专门使用免费的社区版(Docker CE),其中包括核心 Docker 引擎。Docker CE 适用于本书中将涵盖的所有技术和服务,包括弹性容器服务(ECS)、Fargate、Docker Swarm、弹性 Kubernetes 服务(EKS)和弹性 Beanstalk。

除了 Docker,我们还需要其他一些工具来帮助自动化一些构建、测试和部署任务,这些任务将贯穿本书的整个过程:

  • Docker Compose:这允许您在本地和 Docker Swarm 集群上编排和运行多容器环境

  • Git:这是从 GitHub 分叉和克隆示例应用程序以及为本书中创建的各种应用程序和环境创建您自己的 Git 存储库所需的

  • GNU Make 3.82 或更高版本:这提供了任务自动化,允许您运行简单命令(例如make test)来执行给定的任务

  • jq:用于解析 JSON 的命令行实用程序

  • curl:命令行 HTTP 客户端

  • tree:用于在 shell 中显示文件夹结构的命令行客户端

  • Python 解释器:这是 Docker Compose 和我们将在后面的章节中安装的 AWS 命令行界面(CLI)工具所需的

  • pip:用于安装 Python 应用程序的 Python 包管理器,如 AWS CLI

本书中使用的一些工具仅代表性,这意味着如果您愿意,可以用替代工具替换它们。例如,您可以用另一个工具替换 GNU Make 来提供任务自动化。

您还需要的另一个重要工具是一个体面的文本编辑器 - Visual Studio Code(code.visualstudio.com/)和 Sublime Text(www.sublimetext.com/)是在 Windows、macOS 和 Linux 上都可用的绝佳选择。

现在,让我们讨论如何为以下操作系统安装和配置本地 Docker 环境:

  • macOS

  • Windows 10

  • Linux

在 macOS 环境中设置

如果您正在运行 macOS,最快的方法是安装 Docker for Mac,您可以在docs.docker.com/docker-for-mac/install/了解更多信息,并从store.docker.com/editions/community/docker-ce-desktop-mac下载。在幕后,Docker for Mac 利用本机 macOS 虚拟机框架,创建一个 Linux 虚拟机来运行 Docker Engine,并在本地 macOS 环境中安装 Docker 客户端。

首先,您需要创建一个免费的 Docker Hub 账户,然后完成注册并登录,点击获取 Docker按钮下载最新版本的 Docker:

下载 Docker for Mac

完成下载后,打开下载文件,将 Docker 图标拖到应用程序文件夹中,然后运行 Docker:

安装 Docker

按照 Docker 安装向导进行操作,完成后,您应该在 macOS 工具栏上看到一个 Docker 图标:

macOS 工具栏上的 Docker 图标

如果单击此图标并选择首选项,将显示 Docker 首选项对话框,允许您配置各种 Docker 设置。您可能希望立即更改的一个设置是分配给 Docker Engine 的内存,我已将其从默认的 2GB 增加到 8GB:

增加内存

此时,您应该能够启动终端并运行docker info命令:

> docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.06.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
...
...

您还可以使用docker run命令启动新的容器:

> docker run -it alpine echo "Hello World"
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
ff3a5c916c92: Pull complete
Digest: sha256:e1871801d30885a610511c867de0d6baca7ed4e6a2573d506bbec7fd3b03873f
Status: Downloaded newer image for alpine:latest
Hello World
> docker ps -a
CONTAINER ID      IMAGE   COMMAND              CREATED       STATUS                 
a251bd2c53dd      alpine  "echo 'Hello World'" 3 seconds ago Exited (0) 2 seconds ago 
> docker rm a251bd2c53dd
a251bd2c53dd

在上面的示例中,您必须运行基于轻量级 Alpine Linux 发行版的alpine镜像,并运行echo "Hello World"命令。-it标志指定您需要在交互式终端环境中运行容器,这允许您查看标准输出并通过控制台与容器进行交互。

一旦容器退出,您可以使用docker ps命令显示正在运行的容器,并附加-a标志以显示正在运行和已停止的容器。最后,您可以使用docker rm命令删除已停止的容器。

安装其他工具

正如本节前面讨论的那样,我们还需要一些其他工具来帮助自动化一些构建、测试和部署任务。在 macOS 上,其中一些工具已经包含在内,如下所述:

  • Docker Compose:在安装 Docker for Mac 时已经包含在内。

  • Git:当您安装 Homebrew 软件包管理器(我们将很快讨论 Homebrew)时,会安装 XCode 命令行实用程序,其中包括 Git。如果您使用另一个软件包管理器,可能需要使用该软件包管理器安装 Git。

  • GNU Make 3.82 或更高版本:macOS 包括 Make 3.81,不完全满足 3.82 版本的要求,因此您需要使用 Homebrew 等第三方软件包管理器安装 GNU Make。

  • curl:这在 macOS 中默认包含,因此无需安装。

  • jq 和 tree:这些在 macOS 中默认情况下不包括在内,因此需要通过 Homebrew 等第三方软件包管理器安装。

  • Python 解释器:macOS 包括系统安装的 Python,您可以使用它来运行 Python 应用程序,但我建议保持系统 Python 安装不变,而是使用 Homebrew 软件包管理器安装 Python(docs.brew.sh/Homebrew-and-Python)。

  • pip:系统安装的 Python 不包括流行的 PIP Python 软件包管理器,因此如果使用系统 Python 解释器,必须单独安装此软件。如果选择使用 Homebrew 安装 Python,这将包括 PIP。

在 macOS 上安装上述工具的最简单方法是首先安装一个名为 Homebrew 的第三方软件包管理器。您可以通过简单地浏览到 Homebrew 主页brew.sh/来安装 Homebrew:

安装 Homebrew

只需将突出显示的命令复制并粘贴到终端提示符中,即可自动安装 Homebrew 软件包管理器。完成后,您将能够使用brew命令安装先前列出的每个实用程序:

> brew install make --with-default-names
==> Downloading https://ftp.gnu.org/gnu/make/make-4.2.1.tar.bz2
Already downloaded: /Users/jmenga/Library/Caches/Homebrew/make-4.2.1.tar.bz2
==> ./configure --prefix=/usr/local/Cellar/make/4.2.1_1
==> make install
/usr/local/Cellar/make/4.2.1_1: 13 files, 959.5KB, built in 29 seconds
> brew install jq tree ==> Downloading https://homebrew.bintray.com/bottles/jq-1.5_3.high_sierra.bottle.tar.gz
Already downloaded: /Users/jmenga/Library/Caches/Homebrew/jq-1.5_3.high_sierra.bottle.tar.gz
==> Downloading https://homebrew.bintray.com/bottles/tree-1.7.0.high_sierra.bottle.1.tar.gz
Already downloaded: /Users/jmenga/Library/Caches/Homebrew/tree-1.7.0.high_sierra.bottle.1.tar.gz
==> Pouring jq-1.5_3.high_sierra.bottle.tar.gz
/usr/local/Cellar/jq/1.5_3: 19 files, 946.6KB
==> Pouring tree-1.7.0.high_sierra.bottle.1.tar.gz
/usr/local/Cellar/tree/1.7.0: 8 files, 114.3KB

您必须首先使用--with-default-names标志安装 GNU Make,这将替换在 macOS 上安装的系统版本的 Make。如果您喜欢省略此标志,则 GNU 版本的 make 将通过gmake命令可用,并且现有的系统版本的 make 不会受到影响。

最后,要使用 Homebrew 安装 Python,您可以运行brew install python命令,这将安装 Python 3 并安装 PIP 软件包管理器。请注意,当您使用brew安装 Python 3 时,Python 解释器通过python3命令访问,而 PIP 软件包管理器通过pip3命令访问,而不是pip命令:

> brew install python
==> Installing dependencies for python: gdbm, openssl, readline, sqlite, xz
...
...
==> Caveats
Python has been installed as
  /usr/local/bin/python3

Unversioned symlinks `python`, `python-config`, `pip` etc. pointing to
`python3`, `python3-config`, `pip3` etc., respectively, have been installed into
  /usr/local/opt/python/libexec/bin

If you need Homebrew's Python 2.7 run
  brew install python@2

Pip, setuptools, and wheel have been installed. To update them run
  pip3 install --upgrade pip setuptools wheel

You can install Python packages with
  pip3 install <package>
They will install into the site-package directory
  /usr/local/lib/python3.7/site-packages

See: https://docs.brew.sh/Homebrew-and-Python
==> Summary
/usr/local/Cellar/python/3.7.0: 4,788 files, 102.2MB

在 macOS 上,如果您使用通过 brew 或其他软件包管理器安装的 Python,还应将站点模块USER_BASE/bin文件夹添加到本地路径,因为这是 PIP 将安装任何使用--user标志安装的应用程序或库的位置(AWS CLI 是您将在本书后面以这种方式安装的应用程序的一个示例):

> python3 -m site --user-base
/Users/jmenga/Library/Python/3.7
> echo 'export PATH=/Users/jmenga/Library/Python/3.7/bin:$PATH' >> ~/.bash_profile > source ~/.bash_profile 

确保在前面的示例中使用单引号,这样可以确保在您的 shell 会话中不会展开对$PATH的引用,而是将其作为文字值写入.bash_profile文件。

在前面的示例中,您使用--user-base标志调用站点模块,该标志告诉您用户二进制文件将安装在何处。然后,您可以将此路径的bin子文件夹添加到您的PATH变量中,并将其附加到您的主目录中的.bash_profile文件中,每当您生成新的 shell 时都会执行该文件,确保您始终能够执行已使用--user标志安装的 Python 应用程序。请注意,您可以使用source命令立即处理.bash_profile文件,而无需注销并重新登录。

设置 Windows 10 环境

就像对于 macOS 一样,如果您正在运行 Windows 10,最快的方法是安装 Docker for Windows,您可以在docs.docker.com/docker-for-windows/上了解更多信息,并从store.docker.com/editions/community/docker-ce-desktop-windows下载。在幕后,Docker for Windows 利用了称为 Hyper-V 的本机 Windows hypervisor,创建了一个虚拟机来运行 Docker 引擎,并为 Windows 安装了一个 Docker 客户端。

首先,您需要创建一个免费的 Docker Hub 帐户,以便继续进行,一旦完成注册并登录,点击获取 Docker按钮下载最新版本的 Docker for Windows。

完成下载后,开始安装并确保未选择使用 Windows 容器选项:

使用 Linux 容器

安装将继续,并要求您注销 Windows 以完成安装。重新登录 Windows 后,您将被提示启用 Windows Hyper-V 和容器功能:

启用 Hyper-V

您的计算机现在将启用所需的 Windows 功能并重新启动。一旦您重新登录,打开 Windows 的 Docker 应用程序,并确保选择在不使用 TLS 的情况下在 tcp://localhost:2375 上公开守护程序选项:

启用对 Docker 的传统客户端访问

必须启用此设置,以便允许 Windows 子系统访问 Docker 引擎。

安装 Windows 子系统

现在您已经安装了 Docker for Windows,接下来需要安装 Windows 子系统,该子系统提供了一个 Linux 环境,您可以在其中安装 Docker 客户端、Docker Compose 和本书中将使用的其他工具。

如果您使用的是 Windows,那么在本书中我假设您正在使用 Windows 子系统作为您的 shell 环境。

要启用 Windows 子系统,您需要以管理员身份运行 PowerShell(右键单击 PowerShell 程序,然后选择以管理员身份运行),然后运行以下命令:

PS > Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 

启用此功能后,您将被提示重新启动您的机器。一旦您的机器重新启动,您就需要安装一个 Linux 发行版。您可以在文章docs.microsoft.com/en-us/windows/wsl/install-win10中找到各种发行版的链接 - 参见安装您选择的 Linux 发行版中的第 1 步。

例如,Ubuntu 的链接是www.microsoft.com/p/ubuntu/9nblggh4msv6,如果您点击获取应用程序,您将被引导到本地机器上的 Microsoft Store 应用程序,并且您可以免费下载该应用程序:

为 Windows 安装 Ubuntu 发行版

下载完成后,点击启动按钮,这将运行 Ubuntu 安装程序并在 Windows 子系统中安装 Ubuntu。您将被提示输入用户名和密码,假设您正在使用 Ubuntu 发行版,您可以运行lsb_release -a命令来显示安装的 Ubuntu 的具体版本:

为 Windows 安装 Ubuntu 发行版所提供的信息适用于 Windows 10 的最新版本。对于较旧的 Windows 10 版本,您可能需要按照docs.microsoft.com/en-us/windows/wsl/install-win10#for-anniversary-update-and-creators-update-install-using-lxrun中的说明进行操作。

请注意,Windows 文件系统被挂载到 Linux 子系统下的/mnt/c目录(其中c对应于 Windows C:驱动器),因此为了使用安装在 Windows 上的文本编辑器来修改您可以在 Linux 子系统中访问的文件,您可能需要将您的主目录更改为您的 Windows 主目录,即/mnt/c/Users/<用户名>,如下所示:

> exec sudo usermod -d /mnt/c/Users/jmenga jmenga
[sudo] password for jmenga:

请注意,在输入上述命令后,Linux 子系统将立即退出。如果您再次打开 Linux 子系统(点击开始按钮并输入Ubuntu),您的主目录现在应该是您的 Windows 主目录:

> pwd
/mnt/c/Users/jmenga
> echo $HOME
/mnt/c/Users/jmenga

在 Windows 子系统中安装 Docker for Linux

现在您已经安装了 Windows 子系统,您需要在您的发行版中安装 Docker 客户端、Docker Compose 和其他支持工具。在本节中,我将假设您正在使用 Ubuntu Xenial(16.04)发行版。

安装 Docker,请按照docs.docker.com/install/linux/docker-ce/ubuntu/#install-docker-ce上的说明安装 Docker:

> sudo apt-get update Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [107 kB]
Hit:2 http://archive.ubuntu.com/ubuntu xenial InRelease
Get:3 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [109 kB]
...
...
> sudo apt-get install \
 apt-transport-https \
 ca-certificates \
 curl \
 software-properties-common
...
...
> curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - OK> sudo add-apt-repository \
 "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
 $(lsb_release -cs) stable" > sudo apt-get update
...
...
> sudo apt-get install docker-ce
...
...
> docker --version
Docker version 18.06.0-ce, build 0ffa825
> docker info
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

在上面的示例中,您必须按照各种说明将 Docker CE 存储库添加到 Ubuntu 中。安装完成后,您必须执行docker --version命令来检查安装的版本,然后执行docker info命令来连接到 Docker 引擎。请注意,这会失败,因为 Windows 子系统是一个用户空间组件,不包括运行 Docker 引擎所需的必要内核组件。

Windows 子系统不是一种虚拟机技术,而是依赖于 Windows 内核提供的内核仿真功能,使底层的 Windows 内核看起来像 Linux 内核。这种内核仿真模式不支持支持容器的各种系统调用,因此无法运行 Docker 引擎。

要使 Windows 子系统能够连接到由 Docker for Windows 安装的 Docker 引擎,您需要将DOCKER_HOST环境变量设置为localhost:2375,这将配置 Docker 客户端连接到 TCP 端口2375,而不是尝试连接到默认的/var/run/docker.sock套接字文件:

> export DOCKER_HOST=localhost:2375
> docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.06.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
...
...
> echo "export DOCKER_HOST=localhost:2375" >> ~/.bash_profile

因为您在安装 Docker 和 Windows 时之前启用了在 tcp://localhost:2375 上无需 TLS 暴露守护程序选项,以将本地端口暴露给 Windows 子系统,Docker 客户端现在可以与在由 Docker for Windows 安装的单独的 Hyper-V 虚拟机中运行的 Docker 引擎进行通信。您还将export DOCKER_HOST命令添加到用户的主目录中的.bash_profile文件中,每次生成新的 shell 时都会执行该命令。这确保您的 Docker 客户端将始终尝试连接到正确的 Docker 引擎。

在 Windows 子系统中安装其他工具

在这一点上,您需要在 Windows 子系统中安装以下支持工具,我们将在本书中一直使用:

  • Python

  • pip 软件包管理器

  • Docker Compose

  • Git

  • GNU Make

  • jq

  • 构建基本工具和 Python 开发库(用于构建示例应用程序的依赖项)

只需按照正常的 Linux 发行版安装程序来安装上述每个组件。Ubuntu 16.04 的 Linux 子系统发行版已经包含了 Python 3,因此您可以运行以下命令来安装 pip 软件包管理器,并设置您的环境以便能够定位可以使用--user标志安装的 Python 软件包:

> curl -O https://bootstrap.pypa.io/get-pip.py > python3 get-pip.py --user
Collecting pip
...
...
Installing collected packages: pip, setuptools, wheel
Successfully installed pip-10.0.1 setuptools-39.2.0 wheel-0.31.1
> rm get-pip.py
> python3 -m site --user-base /mnt/c/Users/jmenga/.local > echo 'export PATH=/mnt/c/Users/jmenga/.local/bin:$PATH' >> ~/.bash_profile
> source ~/.bash_profile 

现在,您可以使用pip install docker-compose --user命令来安装 Docker Compose:

> pip install docker-compose --user
Collecting docker-compose
...
...
Successfully installed cached-property-1.4.3 docker-3.4.1 docker-compose-1.22.0 docker-pycreds-0.3.0 dockerpty-0.4.1 docopt-0.6.2 jsonschema-2.6.0 texttable-0.9.1 websocket-client-0.48.0
> docker-compose --version
docker-compose version 1.22.0, build f46880f

最后,您可以使用apt-get install命令安装 Git、GNU Make、jq、tree、构建基本工具和 Python3 开发库:

> sudo apt-get install git make jq tree build-essential python3-dev
Reading package lists... Done
Building dependency tree
...
...
Setting up jq (1.5+dfsg-1) ...
Setting up make (4.1-6) ...
Processing triggers for libc-bin (2.23-0ubuntu10) ...
> git --version
git version 2.7.4
> make --version
GNU Make 4.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
> jq --version
jq-1.5-1-a5b5cbe

设置 Linux 环境

Docker 在 Linux 上有原生支持,这意味着您可以在本地操作系统中安装和运行 Docker 引擎,而无需设置虚拟机。Docker 官方支持以下 Linux 发行版(docs.docker.com/install/)来安装和运行 Docker CE:

安装完 Docker 后,您可以按照以下步骤安装完成本书所需的各种工具:

  • Docker Compose:请参阅docs.docker.com/compose/install/上的 Linux 选项卡。另外,由于您需要 Python 来安装 AWS CLI 工具,您可以使用pip Python 软件包管理器来安装 Docker Compose,就像之前在 Mac 和 Windows 上演示的那样,运行pip install docker-compose

  • PythonpipGitGNU Makejqtree构建基本工具Python3 开发库:使用您的 Linux 发行版的软件包管理器(例如yumapt)来安装这些工具。在使用 Ubuntu Xenial 时,可以参考上面的示例演示。

安装示例应用程序

现在,您已经设置好了本地环境,支持 Docker 和完成本书所需的各种工具,是时候为本课程安装示例应用程序了。

示例应用程序是一个名为todobackend的简单的待办事项 Web 服务,提供了一个 REST API,允许您创建、读取、更新和删除待办事项(例如洗车遛狗)。这个应用程序是一个基于 Django 的 Python 应用程序,Django 是一个用于创建 Web 应用程序的流行框架。您可以在www.djangoproject.com/上了解更多信息。如果您对 Python 不熟悉,不用担心 - 示例应用程序已经为您创建,您在阅读本书时需要做的就是构建和测试应用程序,将应用程序打包和发布为 Docker 镜像,然后使用本书中讨论的各种容器管理平台部署您的应用程序。

Forking the sample application

要安装示例应用程序,您需要从 GitHub 上fork该应用程序(我们将很快讨论这意味着什么),这需要您拥有一个活跃的 GitHub 账户。如果您已经有 GitHub 账户,可以跳过这一步,但是如果您没有账户,可以在github.com免费注册一个账户:

Signing up for GitHub

一旦您拥有一个活跃的 GitHub 账户,您就可以访问示例应用程序存储库github.com/docker-in-aws/todobackend。与其克隆存储库,一个更好的方法是fork存储库,这意味着将在您自己的 GitHub 账户中创建一个新的存储库,该存储库与原始的todobackend存储库链接在一起(因此称为fork)。Fork是开源社区中的一种流行模式,允许您对fork存储库进行自己独立的更改。对于本书来说,这是特别有用的,因为您将对todobackend存储库进行自己的更改,添加一个本地 Docker 工作流来构建、测试和发布示例应用程序作为 Docker 镜像,以及在本书的进程中进行其他更改。

fork存储库,请点击右上角的fork按钮:

Forking the todobackend repository

点击分叉按钮几秒钟后,将创建一个名为<your-github-username>/todobackend的新存储库。此时,您可以通过单击克隆或下载按钮来克隆存储库的分支。如果您刚刚设置了一个新帐户,请选择使用 HTTPS 克隆选项并复制所呈现的 URL:

获取 todobackend 存储库的 Git URL

打开一个新的终端并运行git clone <repository-url>命令,其中<repository-url>是您在前面示例中复制的 URL,然后进入新创建的todobackend文件夹:

> git clone https://github.com/<your-username>/todobackend.git
Cloning into 'todobackend'...
remote: Counting objects: 231, done.
remote: Total 231 (delta 0), reused 0 (delta 0), pack-reused 231
Receiving objects: 100% (231/231), 31.75 KiB | 184.00 KiB/s, done.
Resolving deltas: 100% (89/89), done.
> cd todobackend todobackend> 

在阅读本章时,我鼓励您经常提交您所做的任何更改,以及清晰标识所做更改的描述性消息。

示例存储库包括一个名为final的分支,该分支代表完成本书中所有章节后存储库的最终状态。如果遇到任何问题,您可以使用git checkout final命令将其作为参考点。您可以通过运行git checkout master命令切换回主分支。

如果您对 Git 不熟悉,可以参考在线的众多教程(例如,www.atlassian.com/git/tutorials),但通常在提交更改时,您需要执行以下命令:

> git pull
Already up to date.
> git diff
diff --git a/Dockerfile b/Dockerfile
index e56b47f..4a73ce3 100644
--- a/Dockerfile
+++ b/Dockerfile
-COPY --from=build /build /build
-COPY --from=build /app /app
-WORKDIR /app
+# Create app user
+RUN addgroup -g 1000 app && \
+ adduser -u 1000 -G app -D app

+# Copy and install application source and pre-built dependencies
> git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

  modified: src/todobackend/settings.py
  modified: src/todobackend/wsgi.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)

  docker-compose.yml
  src/acceptance.bats
> git add -A > git commit -a -m "Some commit message"
> git push -u origin master
> git push

您应该经常检查您是否拥有存储库的最新版本,方法是运行git pull命令,这样可以避免混乱的自动合并和推送失败,特别是当您与其他人一起合作时。接下来,您可以使用git diff命令显示您对现有文件所做的任何更改,而git status命令则显示对现有文件的文件级更改,并标识您可能已添加到存储库的任何新文件。git add -A命令将所有新文件添加到存储库,而git commit -a -m "<message>"命令将提交所有更改(包括您使用git add -A添加的任何文件)并附带指定的消息。最后,您可以使用git push命令推送您的更改-第一次推送时,您必须使用git push -u origin <branch>命令指定远程分支的原点-之后您可以只使用更短的git push变体来推送您的更改。

一个常见的错误是忘记将新文件添加到您的 Git 存储库中,这可能直到您将存储库克隆到另一台机器上才会显现出来。在提交更改之前,始终确保运行git status命令以识别任何尚未被跟踪的新文件。

在本地运行示例应用程序

现在您已经在本地下载了示例应用程序的源代码,您现在可以构建和在本地运行该应用程序。当您将应用程序打包成 Docker 镜像时,您需要详细了解如何构建和运行您的应用程序,因此在本地运行应用程序是能够为您的应用程序构建容器的旅程的第一步。

安装应用程序依赖项

要运行该应用程序,您需要首先安装应用程序所需的任何依赖项。示例应用程序包括一个名为requirements.txt的文件,位于src文件夹中,其中列出了必须安装的所有必需的 Python 软件包,以便应用程序运行:

Django==2.0
django-cors-headers==2.1.0
djangorestframework==3.7.3
mysql-connector-python==8.0.11
pytz==2017.3
uwsgi==2.0.17

要安装这些要求,请确保您已更改到src文件夹,并配置 PIP 软件包管理器以使用-r标志读取要求文件。请注意,日常开发的最佳实践是在虚拟环境中安装应用程序依赖项(请参阅packaging.python.org/guides/installing-using-pip-and-virtualenv/),但是考虑到我们主要是为了演示目的安装应用程序,我不会采取这种方法:

todobackend> cd src
src> pip3 install -r requirements.txt --user
Collecting Django==2.0 (from -r requirements.txt (line 1))
...
...
Successfully installed Django-2.0 django-cors-headers-2.1.0 djangorestframework-3.7.3 mysql-connector-python-8.0.11 pytz-2017.3 uwsgi-2.0.17

随着时间的推移,每个依赖项的特定版本可能会更改,以确保示例应用程序继续按预期工作。

运行数据库迁移

安装了应用程序依赖项后,您可以运行python3 manage.py命令来执行各种 Django 管理功能,例如运行测试、生成静态网页内容、运行数据库迁移以及运行您的 Web 应用程序的本地实例。

在本地开发环境中,您首先需要运行数据库迁移,这意味着您的本地数据库将根据应用程序配置的适当数据库模式进行初始化。默认情况下,Django 使用 Python 附带的轻量级SQLite数据库,适用于开发目的,并且无需设置即可运行。因此,您只需运行python3 manage.py migrate命令,它将自动为您运行应用程序中配置的所有数据库迁移:

src> python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK

当您运行 Django 迁移时,Django 将自动检测是否存在现有模式,并在不存在模式时创建新模式(在前面的示例中是这种情况)。如果再次运行迁移,请注意 Django 检测到已经存在最新模式,因此不会应用任何内容:

src> python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  No migrations to apply.

运行本地开发 Web 服务器

现在本地 SQLite 数据库已经就位,您可以通过执行python3 manage.py runserver命令来运行应用程序,该命令将在 8000 端口上启动本地开发 Web 服务器:

src> python3 manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
July 02, 2018 - 07:23:49
Django version 2.0, using settings 'todobackend.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

如果您在浏览器中打开http://localhost:8000/,您应该会看到一个名为Django REST framework的网页:

todobackend 应用程序

此页面是应用程序的根,您可以看到 Django REST 框架为使用浏览器时导航 API 提供了图形界面。如果您使用curl命令而不是浏览器,请注意 Django 检测到一个简单的 HTTP 客户端,并且只返回 JSON 响应:

src> curl localhost:8000
{"todos":"http://localhost:8000/todos"}

如果您单击 todos 项目的超媒体链接(http://localhost:8000/todos),您将看到一个当前为空的待办事项列表:

待办事项列表

请注意,您可以使用 Web 界面创建具有标题和顺序的新待办事项,一旦单击 POST 按钮,它将填充待办事项列表:

创建待办事项

当然,您也可以使用命令行和curl命令来创建新的待办事项,列出所有待办事项并更新待办事项:

> curl -X POST -H "Content-Type: application/json" localhost:8000/todos \
 -d '{"title": "Wash the car", "order": 2}'
{"url":"http://localhost:8000/todos/2","title":"Wash the car","completed":false,"order":2}

> curl -s localhost:8000/todos | jq
[
 {
 "url": "http://localhost:8000/todos/1",
 "title": "Walk the dog",
 "completed": false,
 "order": 1
 },
 {
 "url": "http://localhost:8000/todos/2",
 "title": "Wash the car",
 "completed": false,
 "order": 2
 }
]

> curl -X PATCH -H "Content-Type: application/json" localhost:8000/todos/2 \
 -d '{"completed": true}' {"url":"http://localhost:8000/todos/2","title":"Wash the car","completed":true,"order":1}

在前面的示例中,您首先使用HTTP POST方法创建一个新的待办事项,然后验证 Todos 列表现在包含两个待办事项,将curl命令的输出传输到之前安装的jq实用程序中以格式化返回的项目。最后,您使用HTTP PATCH方法对待办事项进行部分更新,将该项目标记为已完成。

您创建和修改的所有待办事项都将保存在应用程序数据库中,在这种情况下,这是一个运行在您的开发机器上的 SQLite 数据库。

在本地测试示例应用程序

现在您已经浏览了示例应用程序,让我们看看如何在本地运行测试以验证应用程序是否按预期运行。todobackend 应用程序包括一小组待办事项的测试,这些测试位于src/todo/tests.py文件中。了解这些测试的编写方式超出了本书的范围,但是知道如何运行这些测试对于能够测试、构建和最终将应用程序打包成 Docker 镜像至关重要。

在测试应用程序时,很常见的是有额外的依赖项,这些依赖项是特定于应用程序测试的,如果你正在构建应用程序以在生产环境中运行,则不需要这些依赖项。这个示例应用程序在一个名为src/requirements_test.txt的文件中定义了测试依赖项,该文件导入了src/requirements.txt中的所有核心应用程序依赖项,并添加了额外的特定于测试的依赖项:

-r requirements.txt
colorama==0.3.9
coverage==4.4.2
django-nose==1.4.5
nose==1.3.7
pinocchio==0.4.2

要安装这些依赖项,您需要运行 PIP 软件包管理器,引用requirements_test.txt文件:

src> pip3 install -r requirements_test.txt --user
Requirement already satisfied: Django==2.0 in /usr/local/lib/python3.7/site-packages (from -r requirements.txt (line 1)) (2.0)
Requirement already satisfied: django-cors-headers==2.1.0 in /usr/local/lib/python3.7/site-packages (from -r requirements.txt (line 2)) (2.1.0)
...
...
Installing collected packages: django-coverage, nose, django-nose, pinocchio
Successfully installed django-nose-1.4.5 pinocchio-0.4.2

现在,您可以通过运行python3 manage.py test命令来运行示例应用程序的测试,传入--settings标志,这允许您指定自定义设置配置。在示例应用程序中,有额外的测试设置,这些设置在src/todobackend/settings_test.py文件中定义,扩展了src/todobackend/settings.py中包含的默认设置,增加了测试增强功能,如规范样式格式和代码覆盖统计:

src> python3 manage.py test --settings todobackend.settings_test
Creating test database for alias 'default'...

Ensure we can create a new todo item
- item has correct title
- item was created
- received 201 created status code
- received location header hyperlink

Ensure we can delete all todo items
- all items were deleted
- received 204 no content status code

Ensure we can delete a todo item
- received 204 no content status code
- the item was deleted

Ensure we can update an existing todo item using PATCH
- item was updated
- received 200 ok status code

Ensure we can update an existing todo item using PUT
- item was updated
- received 200 created status code

----------------------------------------------------------------------
XML: /Users/jmenga/todobackend/src/unittests.xml
Name                              Stmts   Miss  Cover
-----------------------------------------------------
todo/__init__.py                      0      0   100%
todo/admin.py                         1      1     0%
todo/migrations/0001_initial.py       5      0   100%
todo/migrations/__init__.py           0      0   100%
todo/models.py                        6      6     0%
todo/serializers.py                   7      0   100%
todo/urls.py                          6      0   100%
todo/views.py                        17      0   100%
-----------------------------------------------------
TOTAL                                42      7    83%
----------------------------------------------------------------------
Ran 12 tests in 0.281s

OK

Destroying test database for alias 'default'...

请注意,Django 测试运行器会扫描存储库中的各个文件夹以寻找测试,创建一个测试数据库,然后运行每个测试。在所有测试完成后,测试运行器会自动销毁测试数据库,因此您无需执行任何手动设置或清理任务。

摘要

在本章中,您了解了 Docker 和容器,并了解了容器的历史以及 Docker 如何成为最受欢迎的解决方案之一,用于测试、构建、部署和运行容器工作负载。您了解了 Docker 的基本架构,其中包括 Docker 客户端、Docker 引擎和 Docker 注册表,并介绍了在使用 Docker 时将使用的各种类型的对象和资源,包括 Docker 镜像、卷、网络、服务,当然还有 Docker 容器。

我们还讨论了在 AWS 中运行 Docker 应用程序的各种选项,包括弹性容器服务、Fargate、弹性 Kubernetes 服务、弹性 Beanstalk,以及运行自己的 Docker 平台,如 Docker Swarm。

然后,您在本地环境中安装了 Docker,它在 Linux 上得到原生支持,并且在 macOS 和 Windows 平台上需要虚拟机。Docker for Mac 和 Docker for Windows 会自动为您安装和配置虚拟机,使得在这些平台上更容易地开始并运行 Docker。您还学习了如何将 Windows 子系统与 Docker for Windows 集成,这将允许您支持本书中将使用的基于*nix 的工具。

最后,您设置了 GitHub 账户,将示例应用程序存储库 fork 到您的账户,并将存储库克隆到您的本地环境。然后,您学习了如何安装示例应用程序的依赖项,如何运行本地开发服务器,如何运行数据库迁移以确保应用程序数据库架构和表位于正确位置,以及如何运行单元测试以确保应用程序按预期运行。在您能够测试、构建和发布应用程序作为 Docker 镜像之前,理解所有这些任务是很重要的。这将是下一章的重点,您将在其中创建一个完整的本地 Docker 工作流程,自动化创建适用于生产的 Docker 镜像的过程。

问题

  1. 正确/错误:Docker 客户端使用命名管道与 Docker 引擎通信。

  2. 正确/错误:Docker 引擎在 macOS 上原生运行。

  3. 正确/错误:Docker 镜像会发布到 Docker 商店供下载。

  4. 你安装了 Windows 子系统用于 Linux,并安装了 Docker 客户端。你的 Docker 客户端无法与 Windows 上的 Docker 通信。你该如何解决这个问题?

  5. 真/假:卷、网络、容器、镜像和服务都是您可以使用 Docker 处理的实体。

  6. 你通过运行pip install docker-compose --user命令标志来安装 Docker Compose,但是当尝试运行程序时收到了docker-compose: not found的消息。你该如何解决这个问题?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第二章:使用 Docker 构建应用程序

在上一章中,您已经介绍了示例应用程序,并且能够下载并在本地运行该应用程序。目前,您的开发环境已经设置好用于本地开发;但是,在将应用程序部署到生产环境之前,您需要能够打包应用程序及其所有依赖项,确保目标生产环境具有正确的操作系统支持库和配置,选择适当的 Web 服务器来托管您的应用程序,并且有一种机制能够将所有这些内容打包在一起,最好是一个自包含的构件,需要最少的外部配置。传统上,要可靠和一致地实现所有这些内容非常困难,但是 Docker 已经极大地改变了这一局面。通过 Docker 和支持工具,您现在有能力以比以往更快、更可靠、更一致和更可移植的方式实现所有这些内容以及更多。

在本章中,您将学习如何使用 Docker 创建一个全面的工作流程,使您能够以可移植、可重复和一致的方式测试、构建和发布应用程序。您将学习的方法有许多好处,例如,您将能够通过运行几个简单、易于记忆的命令来执行所有任务,并且无需在本地开发或构建环境中安装任何特定于应用程序或操作系统的依赖项。这使得在另一台机器上移动或配置连续交付服务来执行相同的工作流程非常容易——只要您在上一章中设置的核心基于 Docker 的环境,您就能够在任何机器上运行工作流程,而不受应用程序或编程语言的具体细节的影响。

您将学习如何使用 Dockerfile 为应用程序定义测试和运行时环境,配置支持多阶段构建,允许您在具有所有开发工具和库的镜像中构建应用程序构件,然后将这些构件复制到 Dockerfile 的其他阶段。您将利用 Docker Compose 作为一个工具来编排具有多个容器的复杂 Docker 环境,这使您能够测试集成场景,例如您的应用程序与数据库的交互,并模拟您在生产环境中运行应用程序的方式。一个重要的概念是引入构建发布镜像的概念,这是一个可以被部署到生产环境的生产就绪镜像,假设任何新的应用程序特性和功能都能正常工作。您将在本地 Docker 环境中构建和运行此发布镜像,将您的应用程序连接到数据库,然后创建验收测试,验证应用程序从外部客户端连接到您的应用程序的角度来看是否正常工作。

最后,您将使用 GNU Make 将学到的所有知识整合起来,自动化您的工作流程。完成后,您只需运行make test即可运行单元测试和构建应用程序构件,然后构建您的发布镜像,启动类似生产环境的环境,并通过运行make release运行验收测试。这将使测试和发布新的应用程序更改变得非常简单,并且可以放心地使用便携和一致的工作流在本地开发环境和任何支持 Docker 和 Docker Compose 的持续交付环境中运行。

将涵盖以下主题:

  • 使用 Docker 测试和构建应用程序

  • 创建多阶段构建

  • 创建一个测试阶段来构建和测试应用程序构件

  • 创建一个发布阶段来构建和测试发布镜像

  • 使用 Docker Compose 测试和构建应用程序

  • 创建验收测试

  • 自动化工作流程

技术要求

以下列出了完成本章所需的技术要求:

  • 根据第一章的说明安装先决软件

  • 根据第一章的说明创建 GitHub 帐户

以下 GitHub 网址包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch2.

查看以下视频,了解代码的运行情况:

bit.ly/2PJG2Zm

使用 Docker 测试和构建应用程序

在上一章中,您对示例应用程序是什么以及如何在本地开发环境中测试和运行应用程序有了很好的理解。现在,您已经准备好开始创建一个 Docker 工作流程,用于测试、构建和打包应用程序成为一个 Docker 镜像。

重要的是要理解,每当您将一个应用程序打包成一个 Docker 镜像时,最佳实践是减少或消除所有开发和测试依赖项,使其成为最终打包的应用程序。按照我的约定,我将这个打包的应用程序——不包含测试和开发依赖项——称为发布镜像,支持持续交付的范式,即每次成功构建都应该是一个发布候选,可以在需要时发布到生产环境。

为了实现创建发布镜像的目标,一个行之有效的方法是将 Docker 构建过程分为两个阶段:

  • 测试阶段:该阶段具有所有测试和开发依赖项,可用于编译和构建应用程序源代码成应用程序构件,并运行单元测试和集成测试。

  • 发布阶段:该阶段将经过测试和构建的应用程序构件从测试阶段复制到一个最小化的运行时环境中,该环境已适当配置以在生产环境中运行应用程序。

Docker 原生支持这种方法,使用一个名为多阶段构建的功能,这是我们将在本书中采用的方法。现在,我们将专注于测试阶段,并在下一节转移到发布阶段。

创建一个测试阶段

我们将从在todobackend存储库的根目录创建一个Dockerfile开始,这意味着您的存储库结构应该看起来像这样:

todobackend> tree -L 2
.
├── Dockerfile
├── README.md
└── src
    ├── coverage.xml
    ├── db.sqlite3
    ├── manage.py
    ├── requirements.txt
    ├── requirements_test.txt
    ├── todo
    ├── todobackend
    └── unittests.xml

3 directories, 8 files

现在让我们在新创建的 Dockerfile 中定义一些指令:

# Test stage
FROM alpine AS test
LABEL application=todobackend

FROM指令是您在 Dockerfile 中定义的第一个指令,注意我们使用 Alpine Linux 发行版作为基础镜像。Alpine Linux 是一个极简的发行版,比传统的 Linux 发行版(如 Ubuntu 和 CentOS)的占用空间要小得多,并且自从 Docker 采用 Alpine 作为官方 Docker 镜像的首选发行版以来,在容器世界中变得非常流行。

一个你可能不熟悉的关键字是AS关键字,它附加到FROM指令,将 Dockerfile 配置为多阶段构建,并将当前阶段命名为test。当你有一个多阶段构建时,你可以包含多个FROM指令,每个阶段都包括当前的FROM指令和后续的指令,直到下一个FROM指令。

接下来,我们使用LABEL指令附加一个名为application的标签,其值为todobackend,这对于能够识别支持 todobackend 应用程序的 Docker 镜像非常有用。

安装系统和构建依赖

现在我们需要安装各种系统和构建操作系统依赖项,以支持测试和构建应用程序:

# Test stage
FROM alpine AS test
LABEL application=todobackend

# Install basic utilities
RUN apk add --no-cache bash git
# Install build dependencies RUN apk add --no-cache gcc python3-dev libffi-dev musl-dev linux-headers mariadb-dev
RUN pip3 install wheel

在上面的示例中,我们安装了以下依赖项:

  • 基本实用程序:在 Alpine Linux 中,软件包管理器称为apk,在 Docker 镜像中常用的模式是apk add --no-cache,它安装了引用的软件包,并确保下载的软件包不被缓存。我们安装了bash,这对故障排除很有用,还有git,因为我们将在以后使用 Git 元数据来为 Docker 发布镜像生成应用程序版本标签。

  • 构建依赖:在这里,我们安装了构建应用程序所需的各种开发库。这包括gccpython3-devlibffi-devmusl-devlinux-headers,用于编译任何 Python C 扩展及其支持的标准库,以及mariadb-dev软件包,这是构建 todobackend 应用程序中 MySQL 客户端所需的。您还安装了一个名为wheel的 Python 软件包,它允许您构建 Python“wheels”,这是一种预编译和预构建的打包格式,我们以后会用到。

安装应用程序依赖

下一步是安装应用程序的依赖项,就像你在上一章中学到的那样,这意味着安装在src/requirements.txtsrc/requirements_test.txt文件中定义的软件包:

# Test stage
FROM alpine AS test
LABEL application=todobackend

# Install basic utilities
RUN apk add --no-cache bash git

# Install build dependencies
RUN apk add --no-cache gcc python3-dev libffi-dev musl-dev linux-headers mariadb-dev
RUN pip3 install wheel

# Copy requirements
COPY /src/requirements* /build/
WORKDIR /build

# Build and install requirements
RUN pip3 wheel -r requirements_test.txt --no-cache-dir --no-input
RUN pip3 install -r requirements_test.txt -f /build --no-index --no-cache-dir

首先使用COPY指令将src/requirements.txtsrc/requirements_test.txt文件复制到/build容器中的一个文件夹中,然后通过WORKDIR指令将其指定为工作目录。请注意,/src/requirements.txt不是您的 Docker 客户端上的物理路径 - 它是 Docker 构建上下文中的路径,这是您在执行构建时指定的 Docker 客户端文件系统上的可配置位置。为了确保 Docker 构建过程中所有相关的应用程序源代码文件都可用,一个常见的做法是将应用程序存储库的根目录设置为构建上下文,因此在上面的示例中,/src/requirements.txt指的是您的 Docker 客户端上的<path-to-repository>/src/requirements.txt

接下来,您使用pip3 wheel 命令将 Python wheels 构建到/build工作目录中,用于所有基本应用程序和测试依赖项,使用--no-cache-dir标志来避免膨胀我们的镜像,使用--no-input标志来禁用提示用户确认。最后,您使用pip3 install命令将先前构建的 wheels 安装到容器中,使用--no-index标志指示 pip 不要尝试从互联网下载任何软件包,而是从/build文件夹中安装所有软件包,如-f标志所指定的那样。

这种方法可能看起来有点奇怪,但它基于一个原则,即您应该只构建一次您的应用程序依赖项作为可安装的软件包,然后根据需要安装构建的依赖项。稍后,我们将在发布镜像中安装相同的依赖项,确保您的发布镜像准确反映了您的应用程序经过测试和构建的确切依赖项集。

复制应用程序源代码并运行测试

测试阶段的最后步骤是将应用程序源代码复制到容器中,并添加支持运行测试的功能:

# Test stage
FROM alpine AS test
LABEL application=todobackend

# Install basic utilities
RUN apk add --no-cache bash git

# Install build dependencies
RUN apk add --no-cache gcc python3-dev libffi-dev musl-dev linux-headers mariadb-dev
RUN pip3 install wheel

# Copy requirements
COPY /src/requirements* /build/
WORKDIR /build

# Build and install requirements
RUN pip3 wheel -r requirements_test.txt --no-cache-dir --no-input
RUN pip3 install -r requirements_test.txt -f /build --no-index --no-cache-dir

# Copy source code COPY /src /app
WORKDIR /app # Test entrypoint CMD ["python3", "manage.py", "test", "--noinput", "--settings=todobackend.settings_test"]

在前面的例子中,您首先将整个/src文件夹复制到一个名为/app的文件夹中,然后将工作目录更改为/app。您可能会想为什么我们在复制需求文件时没有直接复制所有应用程序源代码。答案是,我们正在实施缓存优化,因为您的需求文件需要构建应用程序依赖项,并且通过在一个单独的较早的层中构建它们,如果需求文件保持不变(它们往往会这样做),Docker 可以利用最近构建的层的缓存版本,而不必每次构建图像时都构建和安装应用程序依赖项。

最后,我们添加了CMD指令,它定义了应该在基于此镜像创建和执行的容器中执行的默认命令。请注意,我们指定了与上一章中用于在本地运行应用程序测试的python3 manage.py test命令相同的命令。

您可能会想为什么我们不直接使用RUN指令在图像中运行测试。答案是,您可能希望在构建过程中收集构件,例如测试报告,这些构件更容易从您从 Docker 镜像生成的容器中复制,而不是在实际的图像构建过程中。

到目前为止,我们已经定义了 Docker 构建过程的第一个阶段,它将创建一个准备好进行测试的自包含环境,其中包括所需的操作系统依赖项、应用程序依赖项和应用程序源代码。要构建图像,您可以运行docker build命令,并使用名称todobackend-test对图像进行标记。

> docker build --target test -t todobackend-test . Sending build context to Docker daemon 311.8kB
Step 1/12 : FROM alpine AS test
 ---> 3fd9065eaf02
Step 2/12 : LABEL application=todobackend
 ---> Using cache
 ---> afdd1dee07d7
Step 3/12 : RUN apk add --no-cache bash git
 ---> Using cache
 ---> d9cd912ffa68
Step 4/12 : RUN apk add --no-cache gcc python3-dev libffi-dev musl-dev linux-headers mariadb-dev
 ---> Using cache
 ---> 89113207b0b8
Step 5/12 : RUN pip3 install wheel
 ---> Using cache
 ---> a866d3b1f3e0
Step 6/12 : COPY /src/requirements* /build/
 ---> Using cache
 ---> efc869447227
Step 7/12 : WORKDIR /build
 ---> Using cache
 ---> 53ced29de259
Step 8/12 : RUN pip3 wheel -r requirements_test.txt --no-cache-dir --no-input
 ---> Using cache
 ---> ba6d114360b9
Step 9/12 : RUN pip3 install -r requirements_test.txt -f /build --no-index --no-cache-dir
 ---> Using cache
 ---> ba0ebdace940
Step 10/12 : COPY /src /app
 ---> Using cache
 ---> 9ae5c85bc7cb
Step 11/12 : WORKDIR /app
 ---> Using cache
 ---> aedd8073c9e6
Step 12/12 : CMD ["python3", "manage.py", "test", "--noinput", "--settings=todobackend.settings_test"]
 ---> Using cache
 ---> 3ed637e47056
Successfully built 3ed637e47056
Successfully tagged todobackend-test:latest

在前面的例子中,--target标志允许您针对多阶段 Dockerfile 中的特定阶段进行构建。尽管我们目前只有一个阶段,但该标志允许我们仅在 Dockerfile 中有多个阶段的情况下构建测试阶段。按照惯例,docker build命令会在运行命令的目录中查找Dockerfile文件,并且命令末尾的句点指定了当前目录(例如,在本例中是应用程序存储库根目录)作为构建上下文,在构建图像时应将其复制到 Docker 引擎。

使用构建并在本地 Docker Engine 中标记为todobackend的映像名称构建的映像,您现在可以从映像启动一个容器,默认情况下将运行python3 manage.py test命令,如CMD指令所指定的那样:

todobackend>  docker run -it --rm todobackend-test
Creating test database for alias 'default'...

Ensure we can create a new todo item
- item has correct title
- item was created
- received 201 created status code
- received location header hyperlink

Ensure we can delete all todo items
- all items were deleted
- received 204 no content status code

Ensure we can delete a todo item
- received 204 no content status code
- the item was deleted

Ensure we can update an existing todo item using PATCH
- item was updated
- received 200 ok status code

Ensure we can update an existing todo item using PUT
- item was updated
- received 200 created status code
----------------------------------------------------------------------
XML: /app/unittests.xml
Name                              Stmts   Miss  Cover
-----------------------------------------------------
todo/__init__.py                      0      0   100%
todo/admin.py                         1      1     0%
todo/migrations/0001_initial.py       5      0   100%
todo/migrations/__init__.py           0      0   100%
todo/models.py                        6      6     0%
todo/serializers.py                   7      0   100%
todo/urls.py                          6      0   100%
todo/views.py                        17      0   100%
-----------------------------------------------------
TOTAL                                42      7    83%
----------------------------------------------------------------------
Ran 12 tests in 0.433s

OK

Destroying test database for alias 'default'...

-it标志指定以交互式终端运行容器,--rm标志将在容器退出时自动删除容器。请注意,所有测试都成功通过,因此我们知道映像中构建的应用程序在至少在当前为应用程序定义的测试方面是良好的状态。

配置发布阶段

有了测试阶段,我们现在有了一个映像,其中包含了所有应用程序依赖项,以一种可以在不需要编译或开发依赖项的情况下安装的格式打包,以及我们的应用程序源代码处于一个我们可以轻松验证通过所有测试的状态。

我们需要配置的下一个阶段是发布阶段,它将应用程序源代码和在测试阶段构建的各种应用程序依赖项复制到一个新的生产就绪的发布映像中。由于应用程序依赖项现在以预编译格式可用,因此发布映像不需要开发依赖项或源代码编译工具,这使我们能够创建一个更小、更精简的发布映像,减少了攻击面。

安装系统依赖项

要开始创建发布阶段,我们可以在 Dockerfile 的底部添加一个新的FROM指令,Docker 将把它视为新阶段的开始:

# Test stage
FROM alpine AS test
LABEL application=todobackend
.........
...# Test entrypointCMD ["python3", "manage.py", "test", "--noinput", "--settings=todobackend.settings_test"]

# Release stage
FROM alpine
LABEL application=todobackend

# Install operating system dependencies
RUN apk add --no-cache python3 mariadb-client bash

在上面的示例中,您可以看到发布映像再次基于 Alpine Linux 映像,这是一个非常好的选择,因为它的占用空间非常小。您可以看到我们安装了更少的操作系统依赖项,其中包括以下内容:

  • python3:由于示例应用程序是一个 Python 应用程序,因此需要 Python 3 解释器和运行时

  • mariadb-client:包括与 MySQL 应用程序数据库通信所需的系统库

  • bash:用于故障排除和执行入口脚本,我们将在后面的章节中讨论。

请注意,我们只需要安装这些软件包的非开发版本,而不是安装python3-devmariadb-dev软件包,因为我们在测试阶段编译和构建了所有应用程序依赖项的预编译轮。

创建应用程序用户

下一步是创建一个应用程序用户,我们的应用程序将作为该用户运行。默认情况下,Docker 容器以 root 用户身份运行,这对于测试和开发目的来说是可以的,但是在生产环境中,即使容器提供了隔离机制,作为非 root 用户运行容器仍被认为是最佳实践:

# Test stage
...
...
# Release stage
FROM alpine
LABEL application=todobackend

# Install operating system dependencies
RUN apk add --no-cache python3 mariadb-client bash

# Create app user
RUN addgroup -g 1000 app && \
 adduser -u 1000 -G app -D app

在上面的示例中,我们首先创建了一个名为app的组,组 ID 为1000,然后创建了一个名为app的用户,用户 ID 为1000,属于app组。

复制和安装应用程序源代码和依赖项

最后一步是复制先前在测试阶段构建的应用程序源代码和依赖项,将依赖项安装到发布镜像中,然后删除在此过程中使用的任何临时文件。我们还需要将工作目录设置为/app,并配置容器以作为前一节中创建的app用户运行:

# Test stage
...
...
# Release stage
FROM alpine
LABEL application=todobackend

# Install operating system dependencies
RUN apk add --no-cache python3 mariadb-client bash

# Create app user
RUN addgroup -g 1000 app && \
    adduser -u 1000 -G app -D app

# Copy and install application source and pre-built dependencies
COPY --from=test --chown=app:app /build /build
COPY --from=test --chown=app:app /app /app
RUN pip3 install -r /build/requirements.txt -f /build --no-index --no-cache-dir
RUN rm -rf /build

# Set working directory and application user
WORKDIR /app
USER app

您首先使用COPY指令和--from标志,告诉 Docker 在--from标志指定的阶段查找要复制的文件。在这里,我们将测试阶段镜像中的/build/app文件夹复制到发布阶段中同名的文件夹,并配置--chown标志以将这些复制的文件夹的所有权更改为应用程序用户。然后我们使用pip3命令仅安装requirements.txt文件中指定的核心要求(您不需要requirements_test.txt中指定的依赖项来运行应用程序),使用--no-index标志禁用 PIP 连接到互联网下载软件包,而是使用-f标志引用的/build文件夹来查找先前在测试阶段构建并复制到此文件夹的依赖项。我们还指定--no-cache-dir标志以避免在本地文件系统中不必要地缓存软件包,并在安装完成后删除/build文件夹。

最后,您将工作目录设置为/app,并通过指定USER指令配置容器以app用户身份运行。

构建和运行发布镜像

现在我们已经完成了 Dockerfile 发布阶段的配置,是时候构建我们的新发布镜像,并验证我们是否能成功运行我们的应用程序。

要构建镜像,我们可以使用docker build命令,因为发布阶段是 Dockerfile 的最后阶段,所以你不需要针对特定阶段进行目标设置,就像我们之前为测试阶段所做的那样:

> docker build -t todobackend-release . Sending build context to Docker daemon 312.8kB
Step 1/22 : FROM alpine AS test
 ---> 3fd9065eaf02
...
...
Step 13/22 : FROM alpine
 ---> 3fd9065eaf02
Step 14/22 : LABEL application=todobackend
 ---> Using cache
 ---> afdd1dee07d7
Step 15/22 : RUN apk add --no-cache python3 mariadb-client bash
 ---> Using cache
 ---> dfe0b6487459
Step 16/22 : RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app
 ---> Running in d75df9cadb1c
Removing intermediate container d75df9cadb1c
 ---> ac26efcbfea0
Step 17/22 : COPY --from=test --chown=app:app /build /build
 ---> 1f177a92e2c9
Step 18/22 : COPY --from=test --chown=app:app /app /app
 ---> ba8998a31f1d
Step 19/22 : RUN pip3 install -r /build/requirements.txt -f /build --no-index --no-cache-dir
 ---> Running in afc44357fae2
Looking in links: /build
Collecting Django==2.0 (from -r /build/requirements.txt (line 1))
Collecting django-cors-headers==2.1.0 (from -r /build/requirements.txt (line 2))
Collecting djangorestframework==3.7.3 (from -r /build/requirements.txt (line 3))
Collecting mysql-connector-python==8.0.11 (from -r /build/requirements.txt (line 4))
Collecting pytz==2017.3 (from -r /build/requirements.txt (line 5))
Collecting uwsgi (from -r /build/requirements.txt (line 6))
Collecting protobuf>=3.0.0 (from mysql-connector-python==8.0.11->-r /build/requirements.txt (line 4))
Requirement already satisfied: setuptools in /usr/lib/python3.6/site-packages (from protobuf>=3.0.0->mysql-connector-python==8.0.11->-r /build/requirements.txt (line 4)) (28.8.0)
Collecting six>=1.9 (from protobuf>=3.0.0->mysql-connector-python==8.0.11->-r /build/requirements.txt (line 4))
Installing collected packages: pytz, Django, django-cors-headers, djangorestframework, six, protobuf, mysql-connector-python, uwsgi
Successfully installed Django-2.0 django-cors-headers-2.1.0 djangorestframework-3.7.3 mysql-connector-python-8.0.11 protobuf-3.6.0 pytz-2017.3 six-1.11.0 uwsgi-2.0.17
Removing intermediate container afc44357fae2
 ---> ab2bcf89fe13
Step 20/22 : RUN rm -rf /build
 ---> Running in 8b8006ea8636
Removing intermediate container 8b8006ea8636
 ---> ae7f157d29d1
Step 21/22 : WORKDIR /app
Removing intermediate container fbd49835ca49
 ---> 55856af393f0
Step 22/22 : USER app
 ---> Running in d57b2cb9bb69
Removing intermediate container d57b2cb9bb69
 ---> 8170e923b09a
Successfully built 8170e923b09a
Successfully tagged todobackend-release:latest

在这一点上,我们可以运行位于发布镜像中的 Django 应用程序,但是你可能想知道它是如何工作的。当我们之前运行python3 manage.py runserver命令时,它启动了一个本地开发 Web 服务器,这在生产用户案例中是不推荐的,所以我们需要一个替代的 Web 服务器来在生产环境中运行我们的应用程序。

你可能已经在requirements.txt文件中注意到了一个名为uwsgi的包——这是一个非常流行的 Web 服务器,可以在生产中使用,并且对于我们的用例非常方便,可以通过 PIP 安装。这意味着uwsgi已经作为 Web 服务器在我们的发布容器中可用,并且可以用来提供示例应用程序。

> docker run -it --rm -p 8000:8000 todobackend-release uwsgi \
    --http=0.0.0.0:8000 --module=todobackend.wsgi --master *** Starting uWSGI 2.0.17 (64bit) on [Tue Jul 3 11:44:44 2018] *
compiled with version: 6.4.0 on 02 July 2018 14:34:31
os: Linux-4.9.93-linuxkit-aufs #1 SMP Wed Jun 6 16:55:56 UTC 2018
nodename: 5be4dd1ddab0
machine: x86_64
clock source: unix
detected number of CPU cores: 1
current working directory: /app
detected binary path: /usr/bin/uwsgi
!!! no internal routing support, rebuild with pcre support !!!
your memory page size is 4096 bytes
detected max file descriptor number: 1048576
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uWSGI http bound on 0.0.0.0:8000 fd 4
uwsgi socket 0 bound to TCP address 127.0.0.1:35765 (port auto-assigned) fd 3
Python version: 3.6.3 (default, Nov 21 2017, 14:55:19) [GCC 6.4.0]
* Python threads support is disabled. You can enable it with --enable-threads *
Python main interpreter initialized at 0x55e9f66ebc80
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 145840 bytes (142 KB) for 1 cores
* Operational MODE: single process *
WSGI app 0 (mountpoint='') ready in 0 seconds on interpreter 0x55e9f66ebc80 pid: 1 (default app)
* uWSGI is running in multiple interpreter mode *
spawned uWSGI master process (pid: 1)
spawned uWSGI worker 1 (pid: 7, cores: 1)
spawned uWSGI http 1 (pid: 8)

我们使用-p标志将容器上的端口8000映射到主机上的端口8000,并执行uwsgi命令,传入各种配置标志,以在端口8000上运行应用程序,并指定todobackend.wsgi模块作为uwsgi提供的应用程序。

Web 服务器网关接口(WSGI)是 Python 应用程序用来与 Web 服务器交互的标准接口。每个 Django 应用程序都包括一个用于与 Web 服务器通信的 WSGI 模块,可以通过<application-name>.wsgi访问。

在这一点上,你可以浏览http://localhost:8000,虽然应用程序确实返回了一个响应,但你会发现 Web 服务器和应用程序缺少一堆静态内容:

问题在于,当你运行 Django 开发 Web 服务器时,Django 会自动生成静态内容,但是当你在生产环境中与外部 Web 服务器一起运行应用程序时,你需要自己生成静态内容。我们将在本章后面学习如何做到这一点,但是现在,你可以使用curl来验证 API 是否可用:

> curl -s localhost:8000/todos | jq
[
 {
 "url": "http://localhost:8000/todos/1",
 "title": "Walk the dog",
 "completed": false,
 "order": 1
 },
 {
 "url": "http://localhost:8000/todos/2",
 "title": "Wash the car",
 "completed": true,
 "order": 2
 }
]

这里需要注意的一点是,尽管我们是从头开始构建 Docker 镜像,但是 todobackend 数据与我们在第一章加载的数据相同。问题在于,第一章中创建的 SQLite 数据库位于src文件夹中,名为db.sqlite3。显然,在构建过程中我们不希望将此文件复制到我们的 Docker 镜像中,而要实现这一点的一种方法是在存储库的根目录创建一个.dockerignore文件:

# Ignore SQLite database files
/***.sqlite3

# Ignore test output and private code coverage files
/*.xml
/.coverage

# Ignore compiled Python source files
/*.pyc
/pycache# Ignore macOS directory metadata files
/.DS_Store

.dockerignore文件的工作方式类似于 Git 存储库中的.gitignore,用于从 Docker 构建上下文中排除文件。因为db.sqlite3文件位于子文件夹中,我们使用通配符 globing 模式**(请注意,这与.gitignore的行为不同,默认情况下进行 globing),这意味着我们递归地排除与通配符模式匹配的任何文件。我们还排除任何具有.xml扩展名的测试输出文件,代码覆盖文件,__pycache__文件夹以及任何具有.pyc扩展名的编译 Python 文件,这些文件是打算在运行时动态生成的。

如果您现在重新构建 Docker 镜像,并在本地端口8000上启动uwsgiWeb 服务器,当您浏览应用程序(http://localhost:8000)时,您将会得到一个不同的错误:

现在的问题是 todobackend 应用程序没有数据库存在,因此应用程序失败,因为它无法找到存储待办事项的表。为了解决这个问题,我们现在需要集成一个外部数据库引擎,这意味着我们需要一个解决方案来在本地使用多个容器。

使用 Docker Compose 测试和构建应用程序

在上一节中,您使用 Docker 命令执行了以下任务:

  • 构建一个测试镜像

  • 运行测试

  • 构建一个发布镜像

  • 运行应用程序

每次我们运行 Docker 命令时,都需要提供相当多的配置,并且试图记住需要运行的各种命令已经开始变得困难。除此之外,我们还发现,要启动应用程序的发布镜像,我们需要有一个操作的外部数据库。对于本地测试用例,运行另一个容器中的外部数据库是一个很好的方法,但是通过运行一系列带有许多不同输入参数的 Docker 命令来协调这一点很快变得难以管理。

Docker Compose是一个工具,允许您使用声明性方法编排多容器环境,使得编排可能需要多个容器的复杂工作流程变得更加容易。按照惯例,Docker Compose 会在当前目录中寻找一个名为docker-compose.yml的文件,所以让我们在todobackend存储库的根目录下创建这个文件,与我们的Dockerfile放在一起。

version: '2.4'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
  release:
    build:
      context: .
      dockerfile: Dockerfile

Docker Compose 文件是用 YAML 格式定义的,需要正确的缩进来推断父对象、同级对象和子对象或属性之间的正确关系。如果您以前没有使用过 YAML,可以查看Ansible YAML Syntax guide,这是一个对 YAML 格式的简要介绍。您也可以使用在线的 YAML linting 工具,比如 http://www.yamllint.com/来检查您的 YAML,或者在您喜欢的文本编辑器中安装 YAML 支持。

首先,我们指定了version属性,这是必需的,引用了我们正在使用的 Compose 文件格式语法的版本。如果您正在使用 Docker 进行本地开发和构建任务,我建议使用 Compose 文件格式的 2.x 版本,因为它包括一些有用的功能,比如对依赖服务进行健康检查,我们很快将学习如何使用。如果您正在使用 Docker Swarm 来运行您的容器,那么您应该使用 Compose 文件格式的 3.x 版本,因为这个版本支持一些与管理和编排 Docker Swarm 相关的功能。

如果您选择使用 3.x 版本,您的应用程序需要更加健壮,以处理诸如数据库在应用程序启动时不可用的情况(参见docs.docker.com/compose/startup-order/),这是我们在本章后面将遇到的一个问题。

接下来,我们指定services属性,它定义了在我们的 Docker Compose 环境中运行的一个或多个服务。在前面的示例中,我们创建了两个服务,对应于工作流程的测试和发布阶段,然后为每个服务添加了一个build属性,它定义了我们希望如何为每个服务构建 Docker 镜像。请注意,build属性基于我们传递给docker build命令的各种标志,例如,当我们构建测试阶段镜像时,我们将构建上下文设置为本地文件夹,使用本地 Dockerfile 作为构建规范的图像,并仅针对测试阶段构建图像。我们不是在每次运行 Docker 命令时命令式地指定这些设置,而是声明性地定义了构建过程的期望配置,这是一个重要的区别。

当然,我们需要运行一个命令来实际构建这些服务,您可以在todobackend存储库的根目录运行docker-compose build命令。

> docker-compose build test
Building test
Step 1/12 : FROM alpine AS test
 ---> 3fd9065eaf02
Step 2/12 : LABEL application=todobackend
 ---> Using cache
 ---> 23e0c2657711
...
...
Step 12/12 : CMD ["python3", "manage.py", "test", "--noinput", "--settings=todobackend.settings_test"]
 ---> Running in 1ac9bded79bf
Removing intermediate container 1ac9bded79bf
 ---> f42d0d774c23

Successfully built f42d0d774c23
Successfully tagged todobackend_test:latest

你可以看到运行docker-compose build test命令实现了我们之前运行的docker build命令的等效效果,然而,我们不需要向docker-compose命令传递任何构建选项或配置,因为我们所有的特定设置都包含在docker-compose.yml文件中。

如果现在要从新构建的镜像运行测试,可以执行docker-compose run命令:

> docker-compose run test
Creating network "todobackend_default" with the default driver
nosetests --verbosity=2 --nologcapture --with-coverage --cover-package=todo --with-spec --spec-color --with-xunit --xunit-file=./unittests.xml --cover-xml --cover-xml-file=./coverage.xml
Creating test database for alias 'default'...

Ensure we can create a new todo item
- item has correct title
- item was created
- received 201 created status code
- received location header hyperlink
...
...
...
...
Ran 12 tests in 0.316s

OK

Destroying test database for alias 'default'...

您还可以扩展 Docker Compose 文件,以向服务添加端口映射和命令配置,如下例所示:

version: '2.4'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
  release:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
 - 8000:8000
 command:
 - uwsgi
 - --http=0.0.0.0:8000
 - --module=todobackend.wsgi
 - --master

在这里,我们指定当运行发布服务时,它应该在主机的 8000 端口和容器的 8000 端口之间创建静态端口映射,并将我们之前使用的uwsgi命令传递给发布容器。如果现在使用docker-compose up命令运行发布阶段,请注意 Docker Compose 将自动为服务构建镜像(如果尚不存在),然后启动服务:

> docker-compose up release
Building release
Step 1/22 : FROM alpine AS test
 ---> 3fd9065eaf02
Step 2/22 : LABEL application=todobackend
 ---> Using cache
 ---> 23e0c2657711
...
...

Successfully built 5b20207e3e9c
Successfully tagged todobackend_release:latest
WARNING: Image for service release was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating todobackend_release_1 ... done
Attaching to todobackend_release_1
...
...
release_1 | *** uWSGI is running in multiple interpreter mode *
release_1 | spawned uWSGI master process (pid: 1)
release_1 | spawned uWSGI worker 1 (pid: 6, cores: 1)
release_1 | spawned uWSGI http 1 (pid: 7)

通常,您使用docker-compose up命令来运行长时间运行的服务,使用docker-compose run命令来运行短暂的任务。您还不能覆盖传递给docker-compose up的命令参数,而可以将命令覆盖传递给docker-compose run命令。

使用 Docker Compose 添加数据库服务

为了解决运行发布图像时出现的应用程序错误,我们需要运行一个应用程序可以连接到的数据库,并确保应用程序配置为使用该数据库。

我们可以通过使用 Docker Compose 添加一个名为db的新服务来实现这一点,该服务基于官方的 MySQL 服务器容器:

version: '2.4'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
  release:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8000:8000
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
  db:
 image: mysql:5.7
 environment:
 MYSQL_DATABASE: todobackend
 MYSQL_USER: todo
 MYSQL_PASSWORD: password
 MYSQL_ROOT_PASSWORD: password

请注意,您可以使用image属性指定外部图像,并且环境设置将使用数据库名为 todobackend、用户名、密码和根密码配置 MySQL 容器。

现在,您可能想知道如何配置我们的应用程序以使用 MySQL 和新的db服务。todobackend 应用程序包括一个名为src/todobackend/settings_release.py的设置文件,该文件配置了 MySQL 作为数据库后端的支持:

# Import base settings
from .settings import *
import os

# Disable debug
DEBUG = True

# Set secret key
SECRET_KEY = os.environ.get('SECRET_KEY', SECRET_KEY)

# Must be explicitly specified when Debug is disabled
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')

# Database settings
DATABASES = {
    'default': {
        'ENGINE': 'mysql.connector.django',
        'NAME': os.environ.get('MYSQL_DATABASE','todobackend'),
        'USER': os.environ.get('MYSQL_USER','todo'),
        'PASSWORD': os.environ.get('MYSQL_PASSWORD','password'),
        'HOST': os.environ.get('MYSQL_HOST','localhost'),
        'PORT': os.environ.get('MYSQL_PORT','3306'),
    },
    'OPTIONS': {
      'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
    }
}

STATIC_ROOT = os.environ.get('STATIC_ROOT', '/public/static')
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', '/public/media')

DATABASES设置包括一个配置,指定了mysql.connector.django引擎,该引擎提供了对 MySQL 的支持,覆盖了默认的 SQLite 驱动程序,并且您可以看到数据库名称、用户名和密码可以通过os.environ.get调用从环境中获取。还要注意STATIC_ROOT设置-这是 Django 查找静态内容(如 HTML、CSS、JavaScript 和图像)的位置-默认情况下,如果未定义此环境变量,Django 将在/public/static中查找。正如我们之前看到的,目前我们的 Web 应用程序缺少这些内容,因此在以后修复缺少内容问题时,请记住这个设置。

现在您了解了如何配置 todobackend 应用程序以支持 MySQL 数据库,让我们修改 Docker Compose 文件以使用db服务:

version: '2.4'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
  release:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8000:8000
 depends_on:
 db:
 condition: service_healthy
    environment:
 DJANGO_SETTINGS_MODULE: todobackend.settings_release
 MYSQL_HOST: db
 MYSQL_USER: todo
 MYSQL_PASSWORD: password
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
  db:
    image: mysql:5.7
 healthcheck:
 test: mysqlshow -u $$MYSQL_USER -p$$MYSQL_PASSWORD
      interval: 3s
      retries: 10
    environment:
      MYSQL_DATABASE: todobackend
      MYSQL_USER: todo
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password

我们首先配置release服务上的environment属性,该属性配置了将传递给容器的环境变量。请注意,对于 Django 应用程序,您可以配置DJANGO_SETTINGS_MODULE环境变量以指定应该使用哪些设置,这使您可以使用添加了 MySQL 支持的settings_release配置。此配置还允许您使用环境变量来指定 MySQL 数据库设置,这些设置必须与db服务的配置相匹配。

接下来,我们为release服务配置depends_on属性,该属性描述了服务可能具有的任何依赖关系。因为应用程序在启动之前必须与数据库建立有效连接,所以我们指定了service_healthy的条件,这意味着在 Docker Compose 尝试启动release服务之前,db服务必须通过 Docker 健康检查。为了配置db服务上的 Docker 健康检查,我们配置了healthcheck属性,它将配置 Docker 运行db服务容器内由test参数指定的命令来验证服务健康,并重试此命令,每 3 秒一次,最多重试 10 次,直到db服务健康为止。对于这种情况,我们使用mysqlshow命令,它只有在 MySQL 进程接受连接时才会返回成功的零退出代码。由于 Docker Compose 将单个美元符号解释为应该在 Docker Compose 文件中评估和替换的环境变量,我们使用双美元符号转义test命令中引用的环境变量,以确保该命令会直接执行mysqlshow -u $MYSQL_USER -p$MYSQL_PASSWORD

在这一点上,我们可以通过在运行release服务的终端中按下Ctrl + C并输入docker-compose down -v命令(-v标志还将删除 Docker Compose 创建的任何卷)来拆除当前环境,然后执行docker-compose up release命令来测试更改:

> docker-compose down -v
Removing todobackend_release_1 ... done
Removing todobackend_test_run_1 ... done
Removing network todobackend_default
> docker-compose up release Creating network "todobackend_default" with the default driver
Pulling db (mysql:5.7)...
5.7: Pulling from library/mysql
683abbb4ea60: Pull complete
0550d17aeefa: Pull complete
7e26605ddd77: Pull complete
9882737bd15f: Pull complete
999c06ab75f6: Pull complete
c71d695f9937: Pull complete
c38f847c1491: Pull complete
74f9c61f40bf: Pull complete
30b252a90a12: Pull complete
9f92ebb7da55: Pull complete
90303981d276: Pull complete
Digest: sha256:1203dfba2600f140b74e375a354b1b801fa1b32d6f80fdee5f155d1e9f38c841
Status: Downloaded newer image for mysql:5.7
Creating todobackend_db_1 ... done
Creating todobackend_release_1 ... done
Attaching to todobackend_release_1
release_1 | *** Starting uWSGI 2.0.17 (64bit) on [Thu Jul 5 07:45:38 2018] *
release_1 | compiled with version: 6.4.0 on 04 July 2018 11:33:09
release_1 | os: Linux-4.9.93-linuxkit-aufs #1 SMP Wed Jun 6 16:55:56 UTC 2018
...
... *** uWSGI is running in multiple interpreter mode *
release_1 | spawned uWSGI master process (pid: 1)
release_1 | spawned uWSGI worker 1 (pid: 7, cores: 1)
release_1 | spawned uWSGI http 1 (pid: 8)

在上面的示例中,请注意,Docker Compose 会根据image属性自动拉取 MySQL 5.7 镜像,然后启动db服务。这将需要 15-30 秒,在此期间,Docker Compose 正在等待 Docker 报告db服务的健康状况。每 3 秒,Docker 运行在健康检查中配置的mysqlshow命令,不断重复此过程,直到命令返回成功的退出代码(即退出代码为0),此时 Docker 将标记容器为健康。只有在这一点上,Docker Compose 才会启动release服务,假设db服务完全可操作,release服务应该会成功启动。

如果您再次浏览http://localhost:8000/todos,您会发现即使我们添加了一个db服务并配置了发布服务以使用这个数据库,您仍然会收到之前在上一个截图中看到的no such table错误。

运行数据库迁移

我们仍然收到有关缺少表的错误,原因是因为我们尚未运行数据库迁移以建立应用程序期望存在的所需数据库架构。请记住,我们在本地使用python3 manage.py migrate命令来运行这些迁移,因此我们需要在我们的 Docker 环境中执行相同的操作。

如果您再次拆除环境,按下Ctrl + C并运行docker-compose down -v,一个方法是使用docker-compose run命令:

> docker-compose down -v ...
...
> docker-compose run release python3 manage.py migrate
Creating network "todobackend_default" with the default driver
Creating todobackend_db_1 ... done
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/mysql/connector/network.py", line 515, in open_connection
    self.sock.connect(sockaddr)
ConnectionRefusedError: [Errno 111] Connection refused
...
...

在上面的示例中,请注意,当您使用docker-compose run命令时,Docker Compose 不支持我们之前在运行docker-compose up时观察到的健康检查行为。这意味着您可以采取以下两种方法:

  • 确保您首先运行docker-compose up release,然后运行docker-compose run python3 manage.py migrate - 这将使您的应用程序处于一种状态,直到迁移完成之前都会引发错误。

  • 将迁移定义为一个单独的服务,称为migrate,依赖于db服务,启动migrate服务,该服务将执行迁移并退出,然后启动应用程序。

尽管很快您会看到,选项 1 更简单,但选项 2 更健壮,因为它确保在启动应用程序之前数据库处于正确的状态。选项 2 也符合我们稍后在本书中在 AWS 中编排运行数据库迁移时将采取的方法,因此我们现在将实施选项 2。

以下示例演示了我们需要进行的更改,以将迁移作为一个单独的服务运行:

version: '2.4'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
  release:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      DJANGO_SETTINGS_MODULE: todobackend.settings_release
      MYSQL_HOST: db
      MYSQL_USER: todo
      MYSQL_PASSWORD: password
  app:
 extends:
 service: release
 depends_on:
 db:
 condition: service_healthy
 ports:
 - 8000:8000
 command:
 - uwsgi
 - --http=0.0.0.0:8000
 - --module=todobackend.wsgi
 - --master
  migrate:
 extends:
 service: release
 depends_on:
 db:
 condition: service_healthy
 command:
 - python3
 - manage.py
 - migrate
 - --no-input
  db:
    image: mysql:5.7
    healthcheck:
      test: mysqlshow -u $$MYSQL_USER -p$$MYSQL_PASSWORD
      interval: 3s
      retries: 10
    environment:
      MYSQL_DATABASE: todobackend
      MYSQL_USER: todo
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password

在上面的示例中,请注意,除了migrate服务,我们还添加了一个名为app的新服务。原因是我们希望从release服务扩展migrate(如extends参数所定义),以便它将继承发布映像和发布服务设置,但是扩展另一个服务的一个限制是您不能扩展具有depends_on语句的服务。这要求我们将release服务更多地用作其他服务继承的基本配置,并将depends_onportscommand参数从发布服务转移到新的app服务。

有了这个配置,我们可以拆除环境并建立我们的新环境,就像以下示例中演示的那样:

> docker-compose down -v ...
...
> docker-compose up migrate
Creating network "todobackend_default" with the default driver
Building migrate
Step 1/24 : FROM alpine AS test
 ---> 3fd9065eaf02
...
...
Successfully built 5b20207e3e9c
Successfully tagged todobackend_migrate:latest
WARNING: Image for service migrate was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating todobackend_db_1 ... done
Creating todobackend_migrate_1 ... done
Attaching to todobackend_migrate_1
migrate_1 | Operations to perform:
migrate_1 | Apply all migrations: admin, auth, contenttypes, sessions, todo
migrate_1 | Running migrations:
migrate_1 | Applying contenttypes.0001_initial... OK
migrate_1 | Applying auth.0001_initial... OK
migrate_1 | Applying admin.0001_initial... OK
migrate_1 | Applying admin.0002_logentry_remove_auto_add... OK
migrate_1 | Applying contenttypes.0002_remove_content_type_name... OK
migrate_1 | Applying auth.0002_alter_permission_name_max_length... OK
migrate_1 | Applying auth.0003_alter_user_email_max_length... OK
migrate_1 | Applying auth.0004_alter_user_username_opts... OK
migrate_1 | Applying auth.0005_alter_user_last_login_null... OK
migrate_1 | Applying auth.0006_require_contenttypes_0002... OK
migrate_1 | Applying auth.0007_alter_validators_add_error_messages... OK
migrate_1 | Applying auth.0008_alter_user_username_max_length... OK
migrate_1 | Applying auth.0009_alter_user_last_name_max_length... OK
migrate_1 | Applying sessions.0001_initial... OK
migrate_1 | Applying todo.0001_initial... OK
todobackend_migrate_1 exited with code 0
> docker-compose up app
Building app
Step 1/24 : FROM alpine AS test
 ---> 3fd9065eaf02
...
...
Successfully built 5b20207e3e9c
Successfully tagged todobackend_app:latest
WARNING: Image for service app was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
todobackend_db_1 is up-to-date
Creating todobackend_app_1 ... done
Attaching to todobackend_app_1
app_1 | *** Starting uWSGI 2.0.17 (64bit) on [Thu Jul 5 11:21:00 2018] *
app_1 | compiled with version: 6.4.0 on 04 July 2018 11:33:09
app_1 | os: Linux-4.9.93-linuxkit-aufs #1 SMP Wed Jun 6 16:55:56 UTC 2018
...
...

在上面的示例中,请注意 Docker Compose 为每个服务构建新的映像,但是由于每个服务都扩展了release服务,因此这些构建非常快速。当您启动migrate服务等待db服务的健康检查通过时,您将观察到 15-30 秒的延迟,之后将运行迁移,创建 todobackend 应用程序期望的适当模式和表。启动app服务后,您应该能够与 todobackend API 交互而不会收到任何错误:

> curl -s localhost:8000/todos | jq
[]

生成静态网页内容

如果您浏览http://localhost:8000/todos,尽管应用程序不再返回错误,但网页的格式仍然是错误的。问题在于 Django 要求您运行一个名为collectstatic的单独的manage.py管理任务,它会生成静态内容并将其放置在STATIC_ROOT设置定义的位置。我们应用程序的发布设置将文件位置定义为/public/static,因此我们需要在应用程序启动之前运行collectstatic任务。请注意,Django 从/static URL 路径提供所有静态内容,例如http://localhost:8000/static

有几种方法可以解决这个问题:

  • 创建一个在启动时运行并在启动应用程序之前执行collectstatic任务的入口脚本。

  • 创建一个外部卷并运行一个容器,执行collectstatic任务,在卷中生成静态文件。然后启动应用程序,挂载外部卷,确保它可以访问静态内容。

这两种方法都是有效的,但是为了介绍 Docker 卷的概念以及你如何在 Docker Compose 中使用它们,我们将采用第二种方法。

要在 Docker Compose 中定义卷,你可以使用顶层的volumes参数,它允许你定义一个或多个命名卷。

version: '2.4'

volumes:
 public:
 driver: local

services:
  test:
    ...
    ...
  release:
    ...
    ...
  app:
    extends:
      service: release
    depends_on:
      db:
        condition: service_healthy
    volumes:
 - public:/public
    ports:
      - 8000:8000
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
 - --check-static=/public
  migrate:
    ...
    ...
  db:
    ...
    ...

在上面的例子中,你添加了一个名为public的卷,并将驱动程序指定为本地,这意味着它是一个标准的 Docker 卷。然后你在 app 服务中使用volumes参数将 public 卷挂载到容器中的/public路径,最后你配置uwsgi来从/public路径为静态内容提供服务,这避免了昂贵的应用程序调用 Python 解释器来提供静态内容。

在销毁当前的 Docker Compose 环境后,生成静态内容只需要使用docker-compose run命令。

> docker-compose down -v ...
...
> docker-compose up migrate
...
...
> docker-compose run app python3 manage.py collectstatic --no-input
Starting todobackend_db_1 ... done
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/prepopulate.js'
Traceback (most recent call last):
  File "manage.py", line 15, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/lib/python3.6/site-packages/django/core/management/__init__.py", line 371, in execute_from_command_line
    utility.execute()
...
...
PermissionError: [Errno 13] Permission denied: '/public/static'

在上面的例子中,collectstatic任务失败,因为默认情况下卷是以 root 创建的,而容器是以 app 用户运行的。为了解决这个问题,我们需要在Dockerfile中预先创建/public文件夹,并将 app 用户设置为该文件夹的所有者。

# Test stage
...
...
# Release stage
FROM alpine
LABEL application=todobackend
...
...
# Copy and install application source and pre-built dependencies
COPY --from=test --chown=app:app /build /build
COPY --from=test --chown=app:app /app /app
RUN pip3 install -r /build/requirements.txt -f /build --no-index --no-cache-dir
RUN rm -rf /build

# Create public volume
RUN mkdir /public
RUN chown app:app /public
VOLUME /public

# Set working directory and application user
WORKDIR /app
USER app

请注意,上面显示的方法仅适用于使用 Docker 卷挂载创建的卷,这是 Docker Compose 在你没有在 Docker Engine 上指定主机路径时使用的方法。如果你指定了主机路径,卷将被绑定挂载,这会导致卷默认具有 root 所有权,除非你在主机上预先创建具有正确权限的路径。当我们使用弹性容器服务时,我们将在以后遇到这个问题,所以请记住这一点。

因为你修改了 Dockerfile,你需要告诉 Docker Compose 重新构建所有镜像,你可以使用docker-compose build命令来实现。

> docker-compose down -v
...
...
> docker-compose build Building test
Step 1/13 : FROM alpine AS test
...
...
Building release
...
...
Building app
...
...
Building migrate
...
...
> docker-compose up migrate
...
...
> docker-compose run app python3 manage.py collectstatic --no-input
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/prepopulate.js'
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/SelectFilter2.js'
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/change_form.js'
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/js/inlines.min.js'
...
...
> docker-compose up app

如果你现在浏览http://localhost:8000,正确的静态内容应该被显示出来。

在 Docker Compose 中定义本地卷时,当你运行docker-compose down -v命令时,卷将自动销毁。如果你希望独立于 Docker Compose 持久存储,你可以定义一个外部卷,然后你需要负责创建和销毁它。更多详情请参阅docs.docker.com/compose/compose-file/compose-file-v2/#external

创建验收测试

现在应用程序已正确配置,为发布阶段配置的最后一个任务是定义验收测试,以验证应用程序是否按预期工作。验收测试的目的是确保您构建的发布镜像在尽可能接近生产环境的环境中工作,在本地 Docker 环境的约束条件下。至少,如果您的应用程序是 Web 应用程序或 API 服务,比如 todobackend 应用程序,您可能只需验证应用程序返回有效的 HTTP 响应,或者您可能运行关键功能,比如创建项目、更新项目和删除项目。

对于 todobackend 应用程序,我们将创建一些基本测试来演示这种方法,使用一个名为 BATS(Bash 自动化测试系统)的工具。BATS 非常适合更喜欢使用 bash 的系统管理员,并利用开箱即用的工具来执行测试。

要开始使用 BATS,我们需要在todobackend存储库的src文件夹中使用 BATS 语法创建一个名为acceptance.bats的测试脚本,您可以在github.com/sstephenson/bats上了解更多信息:

setup() {
  url=${APP_URL:-localhost:8000}
  item='{"title": "Wash the car", "order": 1}'
  location='Location: ([^[:space:]]*)'
  curl -X DELETE $url/todos
}

@test "todobackend root" {
  run curl -oI -s -w "%{http_code}" $APP_URL
  [ $status = 0 ]
  [ $output = 200 ]
}

@test "todo items returns empty list" {
  run jq '. | length' <(curl -s $url/todos)
  [ $output = 0 ]
}

@test "create todo item" {
  run curl -i -X POST -H "Content-Type: application/json" $url/todos -d "$item"
  [ $status = 0 ]
  [[ $output =~ "201 Created" ]] || false
  [[ $output =~ $location ]] || false
  [ $(curl ${BASH_REMATCH[1]} | jq '.title') = $(echo "$item" | jq '.title') ]
}

@test "delete todo item" {
  run curl -i -X POST -H "Content-Type: application/json" $url/todos -d "$item"
  [ $status = 0 ]
  [[ $output =~ $location ]] || false
  run curl -i -X DELETE ${BASH_REMATCH[1]}
  [ $status = 0 ]
  [[ $output =~ "204 No Content" ]] || false
  run jq '. | length' <(curl -s $APP_URL/todos)
  [ $output = 0 ]
}

BATS 文件包括一个setup()函数和一些测试用例,每个测试用例都以@test标记为前缀。setup()函数是一个特殊的函数,在每个测试用例运行之前都会运行,用于定义公共变量并确保应用程序状态在每个测试之前保持一致。您可以看到我们设置了一些在各种测试用例中使用的变量:

  • url:定义了要测试的应用程序的 URL。这由APP_URL环境变量定义,默认为localhost:8000,如果未定义APP_URL

  • item:以 JSON 格式定义了一个测试 Todo 项,该项在测试期间通过 Todos API 创建。

  • location:定义了一个正则表达式,用于定位和捕获在创建 Todo 项时返回的 HTTP 响应中的 Location 标头的值。正则表达式的([^[:space:]]*)部分捕获零个或多个字符,直到遇到空格(由[:space:]指示)为止。例如,如果位置标头是Location: http://localhost:8000/todos/53,正则表达式将捕获http://localhost:8000/todos/53

  • curl命令:最后的设置任务是删除数据库中的所有待办事项,您可以通过向/todosURL 发送 DELETE 请求来实现。这确保了每次测试运行时 todobackend 数据库都是干净的,减少了不同测试引入破坏其他测试的副作用的可能性。

BATS 文件接下来定义了几个测试用例:

  • todobackend root:这包括run函数,该函数运行指定的命令并将命令的退出代码捕获在一个名为 status 的变量中,将命令的输出捕获在一个名为output的变量中。对于这种情况,测试运行curl命令的特殊配置,该配置仅捕获返回的 HTTP 状态代码,然后通过调用[ $status = 0 ]来验证curl命令成功完成,并通过调用[ $output = 200 ]来验证返回的 HTTP 状态代码是 200 代码。这些测试是常规的 shell 测试表达式,相当于许多编程语言中找到的规范assert语句。

  • todo items returns empty list:这个测试用例使用jq命令传递调用/todos路径的输出。请注意,由于不能在特殊的run函数中使用管道,我已经使用了 bash 进程替换语法<(...),使curl命令的输出看起来像是被jq命令读取的文件。

  • 创建待办事项:首先创建一个待办事项,检查返回的退出代码是否为零,然后使用* bash 条件表达式*(如[[...]]语法所示)来验证curl命令的输出是否包含 HTTP 响应中的201 Created,这是创建事项时的标准响应。在使用 bash 条件表达式时,重要的是要注意,如果条件表达式失败,BATS 不会检测到错误,因此我们使用|| false特殊语法,该语法仅在条件表达式失败并返回非零响应false时才会被评估,如果测试表达式失败,测试用例将失败。条件表达式使用=~正则表达式运算符(此运算符在条件表达式中不可用,因此我们使用 bash 测试表达式),第二个条件表达式评估了设置函数中定义的location正则表达式。最后一个命令使用特殊的BASH_REMATCH变量,其中包含最近一次条件表达式评估的结果,本例中是在 Location 标头中匹配的 URL。这允许我们在创建待办事项时捕获返回的位置,并验证创建的事项是否与我们发布的事项匹配。

  • 删除待办事项:这将创建一个待办事项,捕获返回的位置,删除该事项,然后验证事项是否被删除,验证数据库中的待办事项数量在删除后是否为零。请记住,设置函数在每个测试用例运行之前运行,它会清除所有待办事项,因此在这个测试用例开始时,待办事项数量始终为零,创建和删除事项的操作应该总是将数量返回为零。此测试用例中使用的各种命令基于“创建待办事项”测试用例中介绍的概念,因此我不会详细描述每个命令。

现在我们已经定义了一套验收测试,是时候修改 Docker 环境,以支持在应用程序成功启动后执行这些测试。

我们首先需要将curlbatsjq软件包添加到 todobackend 存储库根目录下的Dockerfile中。

# Test stage
FROM alpine AS test
LABEL application=todobackend
...
...
# Release stage
FROM alpine
LABEL application=todobackend

# Install dependencies
RUN apk add --no-cache python3 mariadb-client bash curl bats jq
...
...

接下来,我们需要向docker-compose.yml文件添加一个名为acceptance的新服务,该服务将等待app服务健康,然后运行验收测试。

version: '2.4'

volumes:
  public:
    driver: local

services:
  test:
    ...
    ...
  release:
    ...
    ...
  app:
    extends:
      service: release
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - public:/public
 healthcheck:
 test: curl -fs localhost:8000
      interval: 3s
 retries: 10
    ports:
      - 8000:8000
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
      - --check-static=/public
  acceptance:
 extends:
 service: release
 depends_on:
 app:
 condition: service_healthy
 environment:
 APP_URL: http://app:8000
 command:
 - bats 
 - acceptance.bats
  migrate:
    ...
    ...
  db:
    ...
    ...

首先,我们为app服务添加了一个healthcheck属性,它使用curl实用程序来检查与本地 Web 服务器端点的连接。然后,我们定义了接受服务,我们从release镜像扩展,并配置了APP_URL环境变量,该变量配置了应该针对执行接受测试的正确 URL,而commanddepends_on属性用于在app服务健康时运行接受测试。

有了这个配置,现在您需要拆除当前的环境,重建所有镜像,并执行各种步骤来启动应用程序,除非您到了即将运行docker-compose up app命令的时候,您现在应该运行docker-compose up acceptance命令,因为这将自动在后台启动app服务:

> docker-compose down -v
...
...
> docker-compose build
...
...
> docker-compose up migrate
...
...
> docker-compose run app python3 manage.py collectstatic --no-input
...
...
> docker-compose up acceptance todobackend_db_1 is up-to-date
Creating todobackend_app_1 ... done
Creating todobackend_acceptance_1 ... done
Attaching to todobackend_acceptance_1
acceptance_1 | Processing secrets []...
acceptance_1 | 1..4
acceptance_1 | ok 1 todobackend root
acceptance_1 | ok 2 todo items returns empty list
acceptance_1 | ok 3 create todo item
acceptance_1 | ok 4 delete todo item
todobackend_acceptance_1 exited with code 0

正如您所看到的,所有测试都通过了,每个测试都显示了ok状态。

自动化工作流程

到目前为止,您已成功配置了 Docker Compose 来构建、测试和创建样本应用程序的工作本地环境,包括 MySQL 数据库集成和接受测试。现在,您可以用少数命令来启动这个环境,但即使使用 Docker Compose 大大简化了您需要运行的命令,仍然很难记住要使用哪些命令以及以什么顺序。理想情况下,我们希望有一个单一的命令来运行完整的工作流程,这就是 GNU Make 这样的工具非常有用的地方。

Make 已经存在很长时间了,仍然被认为是许多 C 和 C++应用程序的首选构建工具。任务自动化是 Make 的一个关键特性,能够以简单的格式定义任务或目标,并且可以通过单个命令调用,这使得 Make 成为一个流行的自动化工具,特别是在处理 Docker 容器时。

按照惯例,make 会在当前工作目录中寻找一个名为 Makefile 的文件,您可以创建一个非常简单的 Makefile,就像这里演示的那样:

hello:
    @ echo "Hello World"
    echo "How are you?"

在前面的示例中,您创建了一个名为hello目标,其中包含两个 shell 命令,您可以通过运行make <target>或在这个例子中运行make hello来执行这些命令。每个目标可以包括一个或多个命令,这些命令按照提供的顺序执行。

需要注意的一点是,make 期望在为给定目标定义各种命令时使用制表符(而不是空格),因此如果你收到缺少分隔符的错误,比如Makefile:2: *** missing separator. Stop.,请检查你是否使用了制表符来缩进每个命令。

> make hello
Hello World
echo "How are you?"
How are you?

在上面的例子中,你可以看到每个命令的输出都显示在屏幕上。请注意,第一个命令上的特殊字符@会抑制每个命令的回显。

任何像 Sublime Text 或 Visual Studio Code 这样的体面的现代文本编辑器都应该自动处理 Makefiles 中的制表符。

在使用 Makefiles 进行任务自动化时,你应该执行一个重要的清理工作,即配置一个名为.PHONY的特殊目标,并列出你将要执行的每个目标的名称:

.PHONY: hello

hello:
    @ echo "Hello World"
    echo "How are you?"

因为make实际上是一个用于编译源代码文件的构建工具,所以.PHONY目标告诉 make,如果它看到一个名为hello的文件,它仍然应该运行该目标。如果你没有指定.PHONY,并且本地目录中有一个名为hello的文件,make 将退出并声明hello文件已经构建完成。当你使用 make 来自动化任务时,这显然没有多大意义,所以你应该始终使用.PHONY目标来避免任何奇怪的意外。

自动化测试阶段

既然你已经了解了如何制作,让我们修改我们的 Makefile,以执行实际有用的操作,并执行测试阶段执行的各种操作。回想一下,测试阶段涉及构建 Dockerfile 的第一个阶段作为一个名为test的服务,然后运行test服务,默认情况下将运行python3 manage.py test命令,执行应用程序单元测试:

.PHONY: test

test:
    docker-compose build --pull release
    docker-compose build
    docker-compose run test

请注意,我们实际上并没有在 Docker Compose 文件中构建test服务,而是构建了发布服务并指定了--pull标志,这确保 Docker 始终检查 Docker 镜像中的任何更新版本。我们以这种方式构建release服务,因为我们只想构建整个Dockerfile一次,而不是在每个阶段执行时重新构建Dockerfile

这可以防止一个不太可能但仍然可能发生的情况,即在发布阶段重新构建时,您可能会拉取一个更新的基础镜像,这可能导致与您在测试阶段测试的不同的运行时环境。我们还立即运行 docker-compose build 命令,这可以确保在运行测试之前构建所有服务。因为我们在前一个命令中构建了整个Dockerfile,这将确保其他服务的缓存镜像都更新为最新的镜像构建。

自动化发布阶段

完成测试阶段后,我们接下来运行发布阶段,这需要我们执行以下操作:

  • 运行数据库迁移

  • 收集静态文件

  • 启动应用程序

  • 运行验收测试

以下演示了在 Makefile 中创建一个名为release的目标:

.PHONY: test release

test:
    docker-compose build --pull release
    docker-compose build
    docker-compose run test

release:
 docker-compose up --abort-on-container-exit migrate
 docker-compose run app python3 manage.py collectstatic --no-input
 docker-compose up --abort-on-container-exit acceptance

请注意,我们执行所需命令的每一个时,都会有一个小变化,即在每个docker-compose up命令中添加--abort-on-container-exit命令。默认情况下,docker-compose up命令不会返回非零退出代码,如果命令启动的任何容器失败。这个标志允许您覆盖这一点,并指定任何由docker-compose up命令启动的服务失败,那么 Docker Compose 应该以错误退出。如果您希望在出现错误时使您的 make 命令失败,设置此标志是很重要的。

完善工作流程

有一些更小的增强可以应用到工作流程中,这将确保我们有一个强大、一致和可移植的测试和构建应用程序的机制。

清理 Docker 环境

在本章中,我们一直通过运行docker-compose down命令来清理我们的环境,该命令停止并销毁与 todobackend Docker Compose 环境相关的任何容器。

在构建 Docker 镜像时,您需要注意的另一个方面是孤立或悬空的镜像的概念,这些镜像已经被新版本取代。您可以通过运行docker images命令来了解这一点,我已经用粗体标出了哪些镜像是悬空的:

> docker images REPOSITORY            TAG        IMAGE ID        CREATED            SIZEtodobackend_app       latest     ca3e62e168f2    13 minutes ago     137MBtodobackend_migrate   latest     ca3e62e168f2    13 minutes ago     137MB
todobackend_release   latest     ca3e62e168f2    13 minutes ago     137MB
<none>                <none>     03cc5d44bd7d    14 minutes ago     253MB
<none>                <none>     e88666a35577    22 minutes ago     137MB
<none>                <none>     8909f9001297    23 minutes ago     253MB
<none>                <none>     3d6f9a5c9322    2 hours ago        137MB todobackend_test      latest     60b3a71946cc    2 hours ago        253MB
<none>                <none>     53d19a2de60d    9 hours ago        136MB
<none>                <none>     54f0fb70b9d0    15 hours ago       135MB alpine                latest     11cd0b38bc3c    23 hours ago       4.41MB

请注意,每个突出显示的图像都没有存储库和标签,因此它们被称为孤立或悬空。这些悬空图像没有用处,占用资源和存储空间,因此最好定期清理这些图像,以确保 Docker 环境的性能。回到我们的 Dockerfile,我们在每个阶段添加了LABEL指令,这允许轻松识别与我们的 todobackend 应用相关的图像。

我们可以利用这些标签来定位为 todobackend 应用构建的悬空图像,因此让我们在 Makefile 中添加一个名为clean的新目标,该目标关闭 Docker Compose 环境并删除悬空图像。

.PHONY: test release clean

test:
    docker-compose build --pull release
    docker-compose build
    docker-compose run test

release:
    docker-compose up --abort-on-container-exit migrate
    docker-compose run app python3 manage.py collectstatic --no-input
    docker-compose up --abort-on-container-exit acceptance

clean:
 docker-compose down -v
 docker images -q -f dangling=true -f label=application=todobackend | xargs -I ARGS docker rmi -f --no-prune ARGS

使用-q标志仅打印出图像 ID,然后使用-f标志添加过滤器,指定仅显示具有application=todobackend标签的悬空图像。然后将此命令的输出导入到xargs命令中,xargs捕获过滤图像列表并将其传递给docker rmi -f --no-prune命令,根据-f标志强制删除图像,并使用--no-prune标志确保不删除包含当前标记图像层的未标记图像。我们在这里使用xargs是因为它能智能地处理图像列表-例如,如果没有要删除的图像,那么xargs会在没有错误的情况下静默退出。

以下演示了运行make clean命令的输出:

> make test
...
...
> make release
...
...
> make clean
docker-compose down -v
Stopping todobackend_app_1 ... done
Stopping todobackend_db_1 ... done
Removing todobackend_app_run_2 ... done
Removing todobackend_app_1 ... done
Removing todobackend_app_run_1 ... done
Removing todobackend_migrate_1 ... done
Removing todobackend_db_1 ... done
Removing todobackend_test_run_1 ... done
Removing network todobackend_default
Removing volume todobackend_public
docker images -q -f dangling=true -f label=application=todobackend | xargs -I ARGS docker rmi -f --no-prune ARGS
Deleted: sha256:03cc5d44bd7dec8d535c083dd5a8e4c177f113bc49f6a97d09f7a1deb64b7728
Deleted: sha256:6448ea330f415f773fc4cd5fe35862678ac0e35a1bf24f3780393eb73637f765
Deleted: sha256:baefcaca3929d6fc419eab06237abfb6d9ba9a1ba8d5623040ea4f49b2cc22d4
Deleted: sha256:b1dca5a87173bfa6a2c0c339cdeea6287e4207f34869a2da080dcef28cabcf6f
...
...

当运行make clean命令时,您可能会注意到一件事,即停止 todobackend 应用服务需要一些时间,实际上,需要大约 10 秒才能停止。这是因为在停止容器时,Docker 首先向容器发送 SIGTERM 信号,这会向容器发出即将被终止的信号。默认情况下,如果容器在 10 秒内没有退出,Docker 会发送 SIGKILL 信号,强制终止容器。

问题在于我们应用容器中运行的uwsgi进程默认情况下会忽略 SIGTERM 信号,因此我们需要在配置uwsgi的 Docker Compose 文件中添加--die-on-term标志,以确保它能够优雅地和及时地关闭,如果收到 SIGTERM 信号。

version: '2.4'

volumes:
  public:
    driver: local

services:
  test:
    ...
    ...
  release:
    ...
    ...
  app:
    extends:
      service: release
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - public:/public
    healthcheck:
      test: curl -fs localhost:8000
      interval: 3s
      retries: 10
    ports:
      - 8000:8000
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
      - --check-static=/public
 - --die-on-term
 - --processes=4
 - --threads=2
  acceptance:
    ...
    ...
  migrate:
    ...
    ...
  db:
    ...
    ...

在上面的例子中,我还添加了--processes--threads标志,这些标志启用并发处理。您可以在uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html#adding-concurrency-and-monitoring中了解更多配置选项。

使用动态端口映射

当前,发布阶段工作流程使用静态端口映射运行应用程序,其中 app 服务容器上的端口 8000 映射到 Docker Engine 上的端口8000。尽管在本地运行时通常可以正常工作(除非有其他使用端口 8000 的应用程序),但是在远程持续交付构建服务上运行发布阶段工作流程时可能会导致问题,该服务可能正在为许多不同的应用程序运行多个构建。

更好的方法是使用动态端口映射,将app服务容器端口映射到 Docker Engine 上当前未使用的动态端口。端口是从所谓的临时端口范围中选择的,这是一个为应用程序动态使用保留的端口范围。

要配置动态端口映射,您需要在docker-compose.yml文件中的app服务中更改端口映射:

version: '2.4'

volumes:
  public:
    driver: local

services:
  test:
    ...
    ...
  release:
    ...
    ...
  app:
    extends:
      service: release
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - public:/public
    healthcheck:
      test: curl -fs localhost:8000
      interval: 3s
      retries: 10
    ports:
 - 8000
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
      - --check-static=/public
      - --die-on-term
      - --processes=4
      - --threads=2
  acceptance:
    ...
    ...
  migrate:
    ...
    ...
  db:
    ...
    ...

在上面的例子中,我们只是将端口映射从8000:8000的静态映射更改为8000,这样就可以启用动态端口映射。有了这个配置,一个问题是您事先不知道将分配什么端口,但是您可以使用docker-compose port <service> <container-port>命令来确定给定服务在给定容器端口上的当前动态端口映射:

> docker-compose port app 8000
0.0.0.0:32768

当然,与其每次手动输入此命令,我们可以将其纳入自动化工作流程中:

.PHONY: test release clean

test:
    docker-compose build --pull release
    docker-compose build
    docker-compose run test

release:
    docker-compose up --exit-code-from migrate migrate
    docker-compose run app python3 manage.py collectstatic --no-input
    docker-compose up --exit-code-from acceptance acceptance
 @ echo App running at http://$$(docker-compose port app 8000 | sed s/0.0.0.0/localhost/g) clean:
    docker-compose down -v
    docker images -q -f dangling=true -f label=application=todobackend | xargs -I ARGS docker rmi -f --no-prune ARGS

在上面的例子中,我们使用命令替换来获取当前的端口映射,并将输出传输到一个sed表达式,将0.0.0.0替换为localhost。请注意,因为 GNU Make 将美元符号解释为 Make 变量引用,如果您希望 shell 命令执行时评估单个美元符号,则需要双重转义美元符号($$)。

有了这个配置,make release命令的输出现在将完成如下:

> make release
...
...
docker-compose run app bats acceptance.bats
Starting todobackend_db_1 ... done
1..4
ok 1 todobackend root
ok 2 todo items returns empty list
ok 3 create todo item
ok 4 delete todo item
App running at http://localhost:32771

添加版本目标

对应用程序进行版本控制非常重要,特别是在构建 Docker 镜像时,您希望区分各种镜像。稍后,当我们发布我们的 Docker 镜像时,我们将需要在每个发布的镜像上包含一个版本标签,版本控制的一个简单约定是在应用程序存储库中使用当前提交的 Git 提交哈希。

以下演示了如何在一个 Make 变量中捕获这个,并显示当前版本:

.PHONY: test release clean version

export APP_VERSION ?= $(shell git rev-parse --short HEAD)

version:
 @ echo '{"Version": "$(APP_VERSION)"}'

test:
    docker-compose build --pull release
    docker-compose build
    docker-compose run test

release:
    docker-compose up --abort-on-container-exit migrate
    docker-compose run app python3 manage.py collectstatic --no-input
    docker-compose up --abort-on-container-exit acceptance
    @ echo App running at http://$$(docker-compose port app 8000 | sed s/0.0.0.0/localhost/g)clean:
    docker-compose down -v
    docker images -q -f dangling=true -f label=application=todobackend | xargs -I ARGS docker rmi -f --no-prune ARGS

我们首先声明一个名为APP_VERSION的变量,并在前面加上export关键字,这意味着该变量将在每个目标的环境中可用。然后,我们使用一个名为shell的 Make 函数来执行git rev-parse --short HEAD命令,该命令返回当前提交的七个字符的短哈希。最后,我们添加一个名为version的新目标,它简单地以 JSON 格式打印版本到终端,这在本书后面当我们自动化应用程序的持续交付时将会很有用。请注意,make使用美元符号来引用变量,也用来执行 Make 函数,您可以在www.gnu.org/software/make/manual/html_node/Functions.html了解更多信息。

如果只运行make命令而没有指定目标,make 将执行 Makefile 中的第一个目标。这意味着,对于我们的情况,只运行make将输出当前版本。

以下演示了运行make version命令:

> make version
{"Version": "5cd83c0"}

测试端到端工作流

此时,我们本地 Docker 工作流的所有部分都已就位,现在是审查工作流并验证一切是否正常运行的好时机。

核心工作流现在包括以下任务:

  • 运行测试阶段 - make test

  • 运行发布阶段 - make release

  • 清理 - make clean

我会把这个测试留给你,但我鼓励你熟悉这个工作流程,并确保一切都能顺利完成。运行make release后,验证您是否可以导航到应用程序,应用程序是否正确显示 HTML 内容,以及您是否可以执行创建、读取、更新和删除操作。

一旦您确信一切都按预期工作,请确保已提交并推送您在上一章中分叉的 GitHub 存储库的更改。

总结

在本章中,您实现了一个 Docker 工作流,用于测试、构建和打包应用程序成一个 Docker 镜像,准备发布和部署到生产环境。您学会了如何使用 Docker 多阶段构建来构建应用程序的两个阶段——测试阶段使用开发环境,包括开发库和源代码编译工具,允许您构建和测试应用程序及其依赖关系的预编译包;而发布阶段则将这些构建好的包安装到一个生产就绪的操作环境中,不包含开发库和其他工具,显著减少了应用程序的攻击面。

您学会了如何使用 Docker Compose 来简化测试和发布阶段需要执行的各种命令和操作,创建了一个docker-compose.yml文件,其中包含了一些服务,每个服务都以一种声明性、易于理解的格式进行定义。您学会了如何复制一些部署任务,例如运行数据库迁移、收集静态文件,并确保应用程序数据库在尝试运行应用程序之前是健康的。在本地环境中执行每个任务使您能够对这些任务在实际生产环境中的工作方式有信心和了解,并在本地出现任何应用程序或配置更改破坏这些过程时提前警告。在将应用程序处于正确状态并连接到应用程序数据库后,您学会了如何从外部客户端的角度运行验收测试,这让您对镜像是否按预期工作有了很大的信心,并在这些验收测试在应用程序持续开发过程中失败时提前警告。

最后,你学会了如何使用 GNU Make 将所有这些内容整合到一个完全自动化的工作流程中,它为你提供了简单的高级命令,可以用来执行工作流程。现在你可以通过简单地运行make test来执行测试阶段,通过运行make release来运行发布阶段,并使用make clean清理你的环境。这使得运行工作流程变得非常容易,并且在本书的后面,将简化我们将使用的自动测试、构建和发布 Docker 应用程序的持续交付构建系统的配置。

在接下来的章节中,你将学习如何实际发布你在本章中创建的 Docker 发布镜像,但在你这样做之前,你需要建立一个 AWS 账户,配置对你的账户的访问,并安装支持与 AWS 交互的工具,这将是下一章的重点。

问题

  1. 真/假:你使用FROMTO指令来定义多阶段 Dockerfile。

  2. 真/假:docker命令的--rm标志在容器退出后自动删除容器。

  3. 真/假:当运行你的工作流程时,你应该只构建应用程序构件一次。

  4. 真/假:当运行docker-compose run命令时,如果目标服务启动失败并出现错误,docker-compose 将以非零代码退出。

  5. 真/假:当运行docker-compose up命令时,如果其中一个服务启动失败并出现错误,docker-compose 将以非零代码退出。

  6. 真/假:如果你想使用 Docker Swarm,你应该配置一个 Docker Compose 版本为 3.x。

  7. 你在 Docker 文件中为一个服务的依赖项配置了 service_healthy 条件。然后你使用docker-compose run命令运行服务;依赖项已启动,然而 Docker Compose 并不等待依赖项健康,而是立即启动服务,导致失败。你如何解决这个问题?

  8. 你在 Docker Compose 中创建了一个服务,端口映射为8000:8000。当你尝试启动这个服务时,会出现一个错误,指示端口已被使用。你如何解决这个问题,并确保它不会再次发生呢?

  9. 创建一个 Makefile 后,当尝试运行一个目标时,收到一个关于缺少分隔符的错误。这个错误最有可能的原因是什么?

  10. 哪个 GNU Make 函数允许你捕获 shell 命令的输出?

  11. 在 Makefile 中定义了一个名为 test 的目标,但是当你运行make test时,你会得到一个回应说没有什么可做的。你该如何解决这个问题呢?

  12. 在 Docker Compose 服务定义中必须配置哪些属性才能使用docker-compose push命令?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第三章:开始使用 AWS

在上一章中,我们讨论了部署容器应用程序到 AWS 的各种选项,现在是时候开始使用弹性容器服务(ECS)、Fargate、弹性 Kubernetes 服务(EKS)、弹性 Beanstalk 和 Docker Swarm 来实施实际解决方案了。在我们能够涵盖所有这些令人兴奋的材料之前,您需要建立一个 AWS 账户,了解如何为您的账户设置访问权限,并确保您对我们将在本书中使用的各种工具有牢固的掌握,以与 AWS 进行交互。

开始使用 AWS 非常容易——AWS 提供了一套免费的服务套件,使您能够在 12 个月内免费测试和尝试许多 AWS 服务,或者在某些情况下,无限期地免费使用。当然,会有一些限制,以确保您不能免费设置自己的比特币挖矿服务,但在大多数情况下,您可以利用这些免费套餐服务来测试大量的场景,包括我们将在本书中进行的几乎所有材料。因此,本章将从建立一个新的 AWS 账户开始,这将需要您拥有一张有效的信用卡,以防您真的跟进了那个伟大的新比特币挖矿企业。

一旦您建立了一个账户,下一步是为您的账户设置管理访问权限。默认情况下,所有 AWS 账户都是使用具有最高级别账户特权的根用户创建的,但 AWS 不建议将根账户用于日常管理。因此,我们将配置 AWS 身份访问和管理(IAM)服务,创建 IAM 用户和组,并学习如何使用多因素身份验证(MFA)实施增强安全性。

建立了对 AWS 账户的访问权限后,我们将专注于您可以用来与 AWS 进行交互的各种工具,包括提供基于 Web 的管理界面的 AWS 控制台,以及用于通过命令行与 AWS 进行交互的 AWS CLI 工具。

最后,我们将介绍一种名为 AWS CloudFormation 的管理服务和工具集,它提供了一种基础设施即代码的方法来定义您的 AWS 基础设施和服务。CloudFormation 允许您定义模板,使您能够通过单击按钮构建完整的环境,并且以可重复和一致的方式进行操作。在本书中,我们将广泛使用 CloudFormation,因为在实践中,大多数部署基于 Docker 的应用程序的组织都采用基础设施即代码工具,如 CloudFormation、Ansible 或 Terraform 来自动化其 Docker 应用程序和支持基础设施的部署。您将学习如何创建一个简单的 CloudFormation 模板,然后使用 AWS 控制台和 AWS CLI 部署该模板。

本章将涵盖以下主题:

  • 设置 AWS 账户

  • 以根账户登录

  • 创建 IAM 用户、组和角色

  • 创建一个 EC2 密钥对

  • 安装 AWS CLI

  • 在 AWS CLI 中配置凭据和配置文件

  • 使用 AWS CLI 与 AWS 进行交互

  • 介绍 AWS CloudFormation

  • 定义一个简单的 AWS CloudFormation 模板

  • 部署 AWS CloudFormation 堆栈

  • 删除 AWS CloudFormation 堆栈

技术要求

本章的技术要求如下:

  • 根据第一章《容器和 Docker 基础知识》中的说明安装先决条件软件

  • 在本章中,需要一个有效的信用卡来创建免费的 AWS 账户

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch3.

查看以下视频,了解代码的实际运行情况:

bit.ly/2N1nzJc

设置 AWS 账户

您 AWS 之旅的第一步是建立一个 AWS 账户,这是 AWS 的基础构建块,为您管理 AWS 服务和资源提供了安全和管理上下文。为了鼓励采用 AWS,并确保首次用户有机会免费尝试 AWS,AWS 提供了一个免费套餐,允许您免费访问一些 AWS 服务(在使用方面有一些限制)。您可以在aws.amazon.com/free/了解更多关于免费套餐和提供的服务。确保您对可以免费使用和不能免费使用有很好的理解,以避免不必要的账单冲击。

在本书中,我们将使用一些免费套餐服务,以下是每月使用限制:

服务 限制
EC2 750 小时的 Linux t2.micro(单个 vCPU,1 GB 内存)实例
弹性块存储 30 GB 的块级存储(SSD 或传统旋转磁盘)
RDS 750 小时的 db.t2.micro(单个 vCPU,1 GB 内存)MySQL 实例
弹性容器注册表 500 MB 的存储空间
弹性负载均衡 750 小时的经典或应用负载均衡器
S3 5 GB 的 S3 存储空间
Lambda 1,000,000 次请求
CloudWatch 10 个自定义指标
SNS 1,000,000 次发布
CodeBuild 100 分钟的构建时间
CodePipeline 1 个活动管道
X-Ray 100,000 个跟踪
密钥管理服务 20,000 个请求
Secrets Manager 30 天免费试用期,然后每个秘密/月$0.40

正如您所看到的,我们将在本书中涵盖许多 AWS 服务,几乎所有这些服务都是免费的,假设您遵守前表中描述的使用限制。实际上,在本书中我们将使用的唯一一个不免费的服务是 AWS Fargate 服务,所以当您阅读 Fargate 章节时请记住这一点,并尽量减少使用,如果您担心成本。

要注册免费套餐访问,请点击aws.amazon.com/free/上的创建免费账户按钮:

创建免费账户

您将被提示输入电子邮件地址、密码和 AWS 账户名称。重要的是要理解,您在这里输入的电子邮件地址和密码被称为您的 AWS 账户的根账户,这是对您的账户具有最高访问级别的账户。对于 AWS 账户名称,您可以输入任何您喜欢的名称,但它必须在所有其他 AWS 账户中是唯一的,所以至少您将无法使用我选择的账户名称,即docker-in-aws。这个账户名称在您登录时使用,比您的 AWS 账户号码更容易记住,后者是一个 12 位数字。

注册过程的其余部分是不言自明的,所以我不会在这里详细说明,但请理解,您将需要提供信用卡详细信息,并将对超出免费使用限制的任何费用负责。您还需要验证注册期间指定的电话号码,这涉及自动电话呼叫到您的号码,因此请确保您在注册期间输入一个有效的电话号码。

安装谷歌身份验证器

本节描述的步骤是完全可选的,但是作为安全最佳实践,您应该始终在根账户上启用多因素身份验证(MFA)。事实上,无论所需访问级别如何,您都应该为所有基于用户的 AWS 账户访问启用 MFA。在许多使用 AWS 的组织中,启用 MFA 越来越成为强制性要求,因此在涉及 MFA 时习惯于使用 AWS 是很重要的。因此,我们实际上将在本书中始终使用 MFA。

在您使用 MFA 之前,您需要有一个 MFA 设备,可以是硬件或虚拟 MFA 设备。虚拟 MFA 设备通常安装在您的智能手机上,作为应用程序的形式,完成了您所知道的东西(密码)和您所拥有的东西(您的手机)的多因素范式。

一个流行的 MFA 应用程序可用于 Android 和 iOS 的是谷歌身份验证器应用程序,您可以从谷歌 Play 或苹果应用商店下载。安装应用程序后,您可以继续登录到根账户并设置 MFA 访问。

以根账户登录

设置和激活您的账户后,您应该能够登录到 AWS 控制台,您可以在console.aws.amazon.com/console/home访问。

使用根凭据登录后,您应立即启用 MFA 访问。这提供了额外的安全级别,确保如果您的用户名和密码被泄露,攻击者不能在没有您的 MFA 设备(在我们的示例中,这意味着您智能手机上的 Google Authenticator 应用程序)的情况下访问您的帐户。

要为您的根帐户启用 MFA,请选择指定您帐户名称的下拉菜单(在我的情况下,这是“docker-in-aws”),然后选择“我的安全凭据”:

访问我的安全凭据

在下一个提示中,点击“继续到安全凭据”按钮,在“您的安全凭据”页面上展开“多因素身份验证(MFA)”选项,然后点击“激活 MFA”按钮:

您的安全凭据屏幕

在“管理 MFA 设备”屏幕上,点击“虚拟 MFA 设备”选项,然后连续点击两次“下一步”,此时您将看到一个 QR 码:

获取 QR 码

您可以使用智能手机上的 Google Authenticator 应用程序扫描此代码,方法是点击添加按钮,选择“扫描条形码”,然后在 AWS 控制台中扫描 QR 码:

  !注册 MFA 设备

一旦扫描完成,您需要在“管理 MFA 设备”屏幕上的“身份验证代码 1”输入中输入显示的六位代码。

代码旋转后,将代码的下一个值输入到“身份验证代码 2”输入中,然后点击“激活虚拟 MFA”按钮,以完成 MFA 设备的注册:

带有 MFA 设备的您的安全凭据

创建 IAM 用户、组和角色

在使用 MFA 保护根帐户后,您应立即在您的帐户中创建身份访问和管理(IAM)用户、组和角色以进行日常访问。 IAM 是日常管理和访问 AWS 帐户的推荐方法,您应仅限制根帐户访问计费或紧急情况。在继续之前,您需要知道您的 AWS 帐户 ID,您可以在上一个屏幕截图中看到,在您的 MFA 设备的序列号中(请注意,这将与显示的序列号不同)。记下这个帐户号,因为在配置各种 IAM 资源时将需要它。

创建 IAM 角色

创建 IAM 资源的标准做法是创建用户可以承担的角色,这将为用户在有限的时间内(通常最多 1 小时)授予提升的特权。最低限度,您需要默认创建一个 IAM 角色:

  • 管理员:此角色授予对帐户的完全管理控制,但不包括计费信息

要创建管理员角色,请从 AWS 控制台中选择“服务”|“IAM”,从左侧菜单中选择“角色”,然后单击“创建角色”按钮。在“选择受信任的实体类型”屏幕中,选择“另一个 AWS 帐户”选项,并在“帐户 ID”字段中配置您的帐户 ID:

选择受信任的实体作为管理员角色

单击“下一步:权限”按钮后,选择“AdministratorAccess”策略,该策略授予角色管理访问权限:

将策略附加到 IAM 角色

最后,指定一个名为“admin”的角色名称,然后单击“创建角色”以完成管理员角色的创建:

创建 IAM 角色

这将创建管理员 IAM 角色。如果单击新创建的角色,请注意角色的角色 ARN(Amazon 资源名称),因为您以后会需要这个值:

管理员角色

创建管理员组

有了管理角色之后,下一步是将您的角色分配给用户或组。与其直接为用户分配权限,强烈建议改为将其分配给组,因为这提供了一种更可扩展的权限管理方式。鉴于我们已经创建了具有管理权限的角色,现在创建一个名为管理员的组是有意义的,该组将被授予假定您刚刚创建的 admin 角色的权限。请注意,我指的是假定一个角色,这类似于 Linux 和 Unix 系统,在那里您以普通用户身份登录,然后使用sudo命令临时假定根权限。

您将在本章后面学习如何假定一个角色,但现在您需要通过在 IAM 控制台的左侧菜单中选择并单击创建新组按钮来创建管理员组。

创建 IAM 组

您首先需要指定一个名为管理员的组名称,然后单击下一步两次以跳过附加策略屏幕,最后单击创建组以完成组的创建:

管理员组

这创建了一个没有附加权限的组,但是如果您单击该组并选择权限,现在您有创建内联策略的选项:

创建内联策略

在上述截图中选择点击此处链接后,选择自定义策略选项并单击选择,这将允许您配置一个 IAM 策略文档,以授予假定您之前创建的admin角色的能力:

管理员组内联策略

该策略包括一个允许执行sts:AssumeRole操作的声明 - 这里的sts指的是安全令牌服务,这是您在假定角色时与之交互的服务(假定角色的操作会授予您与所假定角色相关联的临时会话凭证)。请注意,资源是您创建的 IAM 角色的 ARN,因此该策略允许任何属于管理员组的成员假定admin角色。单击应用策略按钮后,您将成功创建和配置管理员组。

创建一个用户组

我通常建议创建的另一个组是用户组,每个访问您的 AWS 账户的人类用户都应该属于该组,包括您的管理员(他们也将成为管理员组的成员)。用户组的核心功能是确保除了一小部分权限外,用户组的任何成员执行的所有操作都必须经过 MFA 身份验证,而不管通过其他组可能授予该用户的权限。这本质上是一个强制 MFA 策略,您可以在www.trek10.com/blog/improving-the-aws-force-mfa-policy-for-IAM-users/上阅读更多相关信息,并且实施这种方法可以增加您为访问 AWS 账户设置的整体安全保护。请注意,该策略允许用户执行一小部分操作而无需 MFA,包括登录、更改用户密码,以及最重要的是允许用户注册 MFA 设备。这允许新用户使用临时密码登录,更改密码,并自行注册 MFA 设备,一旦用户注销并使用 MFA 重新登录,策略允许用户创建用于 API 和 CLI 访问的 AWS 访问密钥。

要实施用户组,我们首先需要创建一个托管 IAM 策略,与我们在前面的截图中采用的内联方法相比,这是一种更可扩展和可重用的机制,用于将策略分配给组和角色。要创建新的托管策略,请从右侧菜单中选择策略,然后单击创建策略按钮,这将打开创建策略屏幕。您需要创建的策略非常广泛,并且在 GitHub 的要点中发布,网址为bit.ly/2KfNfAz,该策略基于先前引用的博客文章中讨论的策略,添加了一些额外的安全增强功能。

请注意,要点包括在策略文件中包含一个名为PASTE_ACCOUNT_NUMBER的占位符,因此您需要将其替换为您的实际 AWS 账户 ID:

创建一个 IAM 托管策略

点击Review policy按钮后,您需要为策略配置一个名称,我们将其称为RequireMFAPolicy,然后点击Create policy创建策略,您需要按照本章前面创建 Administrators 组时的相同说明创建一个 Users 组。

当您在创建 Users 组时到达Attach Policy屏幕时,您可以输入刚刚创建的 RequireMFAPolicy 托管策略的前几个字母,然后将其附加到组中。

将 RequireMFAPolicy 附加到 Users 组

完成创建Users组的向导后,您现在应该在 IAM 控制台中拥有一个Administrators组和Users组。

创建 IAM 用户

您需要执行的最后一个 IAM 设置任务是创建一个 IAM 用户来管理您的帐户。正如本章前面讨论的那样,您不应该使用根凭证进行日常管理任务,而是创建一个管理 IAM 用户。

要创建用户,请从 IAM 控制台的右侧菜单中选择Users,然后点击Add user按钮。在Add user屏幕上,指定一个User name,并且只选择AWS Management Console access作为Access type,确保Console password设置为Autogenerated password,并且Require password reset选项已设置:

创建新用户

点击Next: Permissions按钮后,将用户添加到您之前创建的AdministratorsUsers组中:

将用户添加到组中

现在,您可以点击Next: reviewCreate user按钮来创建用户。用户将被创建,因为您选择创建了自动生成的密码,您可以点击Password字段中的Show链接来显示用户的初始密码。请注意这个值,因为您将需要它来测试作为刚刚创建的 IAM 用户登录:

新创建的用户临时密码

作为 IAM 用户登录

现在您已经创建了 IAM 用户,您可以通过单击菜单中的帐户别名/ID 并选择注销来测试用户的首次登录体验。如果您现在单击登录到控制台按钮或浏览到console.aws.amazon.com/console/home,选择登录到其他帐户选项,输入您的帐户别名或帐户 ID,然后单击下一步,然后输入刚刚创建的 IAM 用户的用户名和临时密码:

首次以 IAM 用户身份登录

然后会提示您输入新密码:

输入新密码

确认密码更改后,您将成功以新用户身份登录。

为 IAM 用户启用 MFA

在这一点上,您已经首次使用 IAM 用户登录,接下来需要执行的步骤是为新用户注册 MFA 设备。要做到这一点,选择服务 | IAM 打开 IAM 控制台,从左侧菜单中选择用户,然后点击您的 IAM 用户。

安全凭证选项卡中,单击分配的 MFA 设备字段旁边的铅笔图标:

IAM 用户安全凭证

管理 MFA 设备对话框将弹出,允许您注册新的 MFA 设备。这个过程与本章前面为根帐户设置 MFA 的过程相同,因此我不会重复说明这个过程,但是一旦您注册了 MFA 设备,重要的是您登出并重新登录到控制台以强制进行 MFA 身份验证。

如果您已经正确配置了一切,当您再次登录到控制台时,应该会提示您输入 MFA 代码:

MFA 提示

假设 IAM 角色

一旦您完成了注册 MFA 设备并使用 MFA 登出并重新登录到 AWS 控制台,您现在满足了导致您之前创建的RequireMFAPolicy中的以下语句不被应用的要求:

{
    "Sid": "DenyEverythingExceptForBelowUnlessMFAd",
    "Effect": "Deny",
    "NotAction": [
        "iam:ListVirtualMFADevices",
        "iam:ListMFADevices",
        "iam:ListUsers",
        "iam:ListAccountAliases",
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:ResyncMFADevice",
        "iam:ChangePassword",
        "iam:CreateLoginProfile",
        "iam:DeleteLoginProfile",
        "iam:GetAccountPasswordPolicy",
        "iam:GetAccountSummary",
        "iam:GetLoginProfile",
        "iam:UpdateLoginProfile"
    ],
    "Resource": "*",
    "Condition": {
        "Null": {
            "aws:MultiFactorAuthAge": "true"
        }
    }
}

在上述代码中,重要的是要注意Deny的 IAM 效果是绝对的——一旦 IAM 遇到给定权限或一组权限的Deny,那么该权限就无法被允许。然而,Condition属性使这个广泛的Deny有条件——只有在特殊条件aws:MultiFactorAuthAge为 false 的情况下才会应用,这种情况发生在您没有使用 MFA 登录时。

假设 IAM 用户已通过 MFA 登录,并附加到具有承担管理员角色权限的Administrators组,那么RequireMFAPolicy中没有任何内容会拒绝此操作,因此您现在应该能够承担管理员角色。

要使用 AWS 控制台承担管理员角色,请点击下拉菜单,选择切换角色

切换角色

点击切换角色按钮后,您将被提示输入帐户 ID 或名称,以及您想要在配置帐户中承担的角色:

切换角色

您现在应该注意到 AWS 控制台的标题指示您必须承担管理员角色,现在您已经完全具有对 AWS 帐户的管理访问权限。

承担管理员角色在本书的其余部分中,每当您需要在您的帐户中执行管理任务时,我将假定您已经承担了管理员角色,就像在之前的屏幕截图中演示的那样。

创建 EC2 密钥对

如果您打算在 AWS 帐户中运行任何 EC2 实例,那么需要完成的一个关键设置任务就是建立一个或多个 EC2 密钥对,对于 Linux EC2 实例,可以用来定义一个 SSH 密钥对,以授予对 EC2 实例的 SSH 访问。

当您创建 EC2 密钥对时,将自动生成一个 SSH 公钥/私钥对,其中 SSH 公钥将作为命名的 EC2 密钥对存储在 AWS 中,并相应的 SSH 私钥下载到您的本地客户端。如果随后创建任何 EC2 实例并在实例创建时引用命名的 EC2 密钥对,您将能够自动使用相关的 SSH 私钥访问您的 EC2 实例。

访问 Linux EC2 实例的 SSH 需要您使用与配置的 EC2 密钥对关联的 SSH 私钥,并且还需要适当的网络配置和安全组,以允许从您的 SSH 客户端所在的任何位置访问 EC2 实例的 SSH 端口。

要创建 EC2 密钥对,首先在 AWS 控制台中导航到服务| EC2,从左侧菜单中的网络和安全部分中选择密钥对,然后单击创建密钥对按钮:

在这里,您已配置了一个名为 admin 的 EC2 密钥对名称,并在单击“创建”按钮后,将创建一个新的 EC2 密钥对,并将 SSH 私钥下载到您的计算机:

此时,您需要将 SSH 私钥移动到计算机上的适当位置,并按下面的示例修改私钥文件的默认权限:

> mv ~/Downloads/admin.pem ~/.ssh/admin.pem
> chmod 600 ~/.ssh/admin.pem

请注意,如果您不使用 chmod 命令修改权限,当您尝试使用 SSH 密钥时,将会出现以下错误:

> ssh -i ~/.ssh/admin.pem 192.0.2.1
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for '/Users/jmenga/.ssh/admin.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/Users/jmenga/.ssh/admin.pem": bad permissions

使用 AWS CLI

到目前为止,在本章中,您只与 AWS 控制台进行了交互,该控制台可以从您的 Web 浏览器访问。虽然拥有 AWS 控制台访问权限非常有用,但在许多情况下,您可能更喜欢使用命令行工具,特别是在需要自动化关键操作和部署任务的情况下。

安装 AWS CLI

AWS CLI 是用 Python 编写的,因此您必须安装 Python 2 或 Python 3,以及 PIP Python 软件包管理器。

本书中使用的说明和示例假定您使用的是 MacOS 或 Linux 环境。

有关如何在 Windows 上设置 AWS CLI 的说明,请参阅docs.aws.amazon.com/cli/latest/userguide/awscli-install-windows.html

假设您已满足这些先决条件,您可以在终端中使用pip命令安装 AWS CLI,并使用--upgrade标志升级到最新的 AWS CLI 版本(如果已安装),并使用--user标志避免修改系统库:

> pip install awscli --upgrade --user
Collecting awscli
  Downloading https://files.pythonhosted.org/packages/69/18/d0c904221d14c45098da04de5e5b74a6effffb90c2b002bc2051fd59222e/awscli-1.15.45-py2.py3-none-any.whl (1.3MB)
    100% |████████████████████████████████| 1.3MB 1.2MB/s
...
...
Successfully installed awscli-1.15.45 botocore-1.10.45 colorama-0.3.9 pyasn1-0.4.3 python-dateutil-2.7.3

根据您的环境,如果您使用的是 Python 3,您可能需要用pip3 install命令替换pip install

如果您现在尝试运行 AWS CLI 命令,该命令将失败,并指示您必须配置您的环境:

> aws ec2 describe-vpcs
You must specify a region. You can also configure your region by running "aws configure".

创建 AWS 访问密钥

如果您按照前面的代码建议运行aws configure命令,将提示您输入 AWS 访问密钥 ID:

> aws configure
AWS Access Key ID [None]:

要使用 AWS CLI 和 AWS SDK,您必须创建 AWS 访问密钥,这是由访问密钥 ID 和秘密访问密钥值组成的凭据。要创建访问密钥,请在 AWS 控制台中打开 IAM 仪表板,从左侧菜单中选择用户,然后单击您的用户名。在安全凭据选项卡下的访问密钥部分,单击创建访问密钥按钮,这将打开一个对话框,允许您查看访问密钥 ID 和秘密访问密钥值:

访问密钥凭证

记下访问密钥 ID 和秘密访问密钥值,因为您将需要这些值来配置您的本地环境。

配置 AWS CLI

回到您的终端,现在您可以完成aws configure设置过程:

> aws configure
AWS Access Key ID [None]: AKIAJXNI5XLCSBRQAZCA
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-1
Default output format [None]:

如果您现在尝试运行之前尝试过的aws ec2 describe-vpcs命令,该命令仍然失败;但是,错误是不同的:

> aws ec2 describe-vpcs

An error occurred (UnauthorizedOperation) when calling the DescribeVpcs operation: You are not authorized to perform this operation.

现在的问题是,您未被授权执行此命令,因为您刚刚创建的访问密钥与您的用户帐户相关联,您必须假定管理员角色以获得管理特权。

配置 AWS CLI 以假定角色

此时,AWS CLI 正在以您的用户帐户的上下文中运行,您需要配置 CLI 以假定管理员角色以能够执行任何有用的操作。

当您运行aws configure命令时,AWS CLI 在名为.aws的文件夹中创建了两个重要文件,该文件夹位于您的主目录中:

> ls -l ~/.aws

total 16
-rw------- 1 jmenga staff 29  23 Jun 19:31 config
-rw------- 1 jmenga staff 116 23 Jun 19:31 credentials

credentials文件保存了一个或多个命名配置文件中的 AWS 凭据:

> cat ~/.aws/credentials
[default]
aws_access_key_id = AKIAJXNI5XLCSBRQAZCA
aws_secret_access_key = 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

在上述代码中,请注意aws configure命令创建了一个名为default的配置文件,并将访问密钥 ID 和秘密访问密钥值存储在该文件中。作为最佳实践,特别是如果您正在使用多个 AWS 账户,我建议避免使用默认配置文件,因为如果输入 AWS CLI 命令,AWS CLI 将默认使用此配置文件。您很快将学会如何使用命名配置文件来处理多个 AWS 账户,如果您有一个默认配置文件,很容易忘记指定要使用的配置文件,并在默认配置文件引用的账户中意外执行操作。我更喜欢根据您正在使用的账户的名称命名每个配置文件,例如,在这里,我已将凭据文件中的默认配置文件重命名为docker-in-aws,因为我将我的 AWS 账户命名为docker-in-aws

[docker-in-aws]
aws_access_key_id = AKIAJXNI5XLCSBRQAZCA
aws_secret_access_key = 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

AWS CLI 创建的另一个文件是~/.aws/config文件,如下所示:

[default]
region = us-east-1

该文件包括命名的配置文件,并且因为您在运行aws configure命令时指定了默认区域,所以default配置文件中已经添加了region变量。配置文件支持许多变量,允许您执行更高级的任务,比如自动假定角色,因此这就是我们需要配置 CLI 以假定我们在本章前面创建的admin角色的地方。鉴于我们已经在credentials文件中重命名了default配置文件,以下代码演示了将default配置文件重命名为docker-in-aws并添加支持假定admin角色的操作:

[profile docker-in-aws]
source_profile = docker-in-aws
role_arn = arn:aws:iam::385605022855:role/admin
role_session_name=justin.menga
mfa_serial = arn:aws:iam::385605022855:mfa/justin.menga
region = us-east-1

请注意,在配置命名配置文件时,我们在配置文件名前面添加了profile关键字,这是必需的。我们还在配置文件中配置了许多变量:

  • source_profile:这是应该用于获取凭据的凭据配置文件。我们指定docker-in-aws,因为我们之前已将凭据文件中的配置文件重命名为docker-in-aws

  • role_arn:这是要假定的 IAM 角色的 ARN。在这里,您指定了您在上一个截图中创建的admin角色的 ARN。

  • role_session_name:这是在承担配置的角色时创建的临时会话的名称。作为最佳实践,您应该指定您的 IAM 用户名,因为这有助于审计您使用角色执行的任何操作。当您使用承担的角色在 AWS 中执行操作时,您的身份实际上是arn:aws:sts::<account-id>:assumed-role/<role-name>/<role-session-name>,因此将用户名设置为角色会话名称可以确保可以轻松确定执行操作的用户。

  • mfa_serial:这是应该用于承担角色的 MFA 设备的 ARN。鉴于您的 IAM 用户属于用户组,对于所有操作,包括通过 AWS CLI 或 SDK 进行的任何 API 调用,都需要 MFA。通过配置此变量,AWS CLI 将在尝试承担配置的角色之前自动提示您输入 MFA 代码。您可以在 IAM 用户帐户的安全凭据选项卡中获取 MFA 设备的 ARN(请参阅分配的 MFA 设备字段,但它将始终遵循arn:aws:iam::<account-id>:mfa/<user-id>的命名约定)。

有关支持凭据和配置文件中的所有变量的完整描述,请参阅docs.aws.amazon.com/cli/latest/topic/config-vars.html

配置 AWS CLI 以使用命名配置文件

有了配置,您就不再有默认配置文件,因此运行 AWS CLI 将返回相同的输出。要使用命名配置文件,您有两个选项:

  • 在 AWS CLI 命令中使用--profile标志指定配置文件名称。

  • 在名为AWS_PROFILE的环境变量中指定配置文件名称。这是我首选的机制,我将假设您在本书中一直采用这种方法。

上面的代码演示了使用这两种方法:

> aws ec2 describe-vpcs --profile docker-in-aws
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga: ****
{
    "Vpcs": [
        {
            "VpcId": "vpc-f8233a80",
            "InstanceTenancy": "default",
            "CidrBlockAssociationSet": [
                {
                    "AssociationId": "vpc-cidr-assoc-32524958",
                    "CidrBlock": "172.31.0.0/16",
                    "CidrBlockState": {
                        "State": "associated"
                    }
                }
            ],
            "State": "available",
            "DhcpOptionsId": "dopt-a037f9d8",
            "CidrBlock": "172.31.0.0/16",
            "IsDefault": true
        }
    ]
}
> export AWS_PROFILE=docker-in-aws
> aws ec2 describe-vpcs --query Vpcs[].VpcId
[
    "vpc-f8233a80"
]

在上面的示例中,请注意当您首次运行aws命令时,会提示您输入 MFA 令牌,但是当您下次运行该命令时,将不会提示您。这是因为默认情况下,从承担角色获取的临时会话凭据在一个小时内有效,并且 AWS CLI 会缓存凭据,以便您在不必在每次执行命令时刷新凭据的情况下重用它们。当然,在一个小时后,由于临时会话凭据将会过期,您将再次被提示输入 MFA 令牌。

在前面的代码中,还有一个有趣的地方需要注意,就是在最后一个命令示例中使用了--query标志。这允许您指定一个 JMESPath 查询,这是一种用于查询 JSON 数据结构的查询语言。AWS CLI 默认输出 JSON,因此您可以使用查询从 AWS CLI 输出中提取特定信息。在本书中,我将经常使用这些查询的示例,您可以在jmespath.org/tutorial.html上阅读更多关于 JMESPath 查询语言的信息。

AWS CloudFormation 简介

AWS CloudFormation是一项托管的 AWS 服务,允许您使用基础架构即代码来定义 AWS 服务和资源,并且是使用 AWS 控制台、CLI 或各种 SDK 部署 AWS 基础架构的替代方案。虽然需要一些学习曲线来掌握 CloudFormation,但一旦掌握了使用 CloudFormation 的基础知识,它就代表了一种非常强大的部署 AWS 基础架构的方法,特别是一旦开始部署复杂的环境。

在使用 CloudFormation 时,您可以在 CloudFormation 模板中定义一个或多个资源,这是一种将相关资源组合在一个地方的便捷机制。当您部署模板时,CloudFormation 将创建一个包含在模板中定义的物理资源的堆栈。CloudFormation 将部署每个资源,自动确定每个资源之间的任何依赖关系,并优化部署,以便在适用的情况下可以并行部署资源,或者在资源之间存在依赖关系时按正确的顺序部署资源。最好的消息是,所有这些强大的功能都是免费的 - 您只需要在通过 CloudFormation 部署堆栈时支付您消耗的资源。

需要注意的是,有许多第三方替代方案可以替代 CloudFormation - 例如,Terraform 非常受欢迎,传统的配置管理工具如 Ansible 和 Puppet 也包括部署 AWS 资源的支持。我个人最喜欢的是 CloudFormation,因为它得到了 AWS 的原生支持,对各种 AWS 服务和资源有很好的支持,并且与 AWS CLI 和 CodePipeline 等服务进行了原生集成(我们将在本书的第十三章“持续交付 ECS 应用程序”中利用这种集成)。

定义 CloudFormation 模板

使用 CloudFormation 的最简单方法是创建一个 CloudFormation 模板。该模板以 JSON 或 YAML 格式定义,我建议使用 YAML 格式,因为相比 JSON,YAML 更容易让人类操作。

CloudFormation 用户指南详细描述了模板结构,但是出于本书的目的,我们只需要关注一个基本的模板结构,最好通过一个真实的例子来演示,您可以将其保存在计算机上一个方便的位置的名为stack.yml的文件中。

AWSTemplateFormatVersion: "2010-09-09"

Description: Cloud9 Management Station

Parameters:
 EC2InstanceType:
   Type: String
   Description: EC2 instance type
   Default: t2.micro
 SubnetId:
   Type: AWS::EC2::Subnet::Id
   Description: Target subnet for instance

Resources:
  ManagementStation:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: !Sub ${AWS::StackName}-station
      Description:
        Fn::Sub: ${AWS::StackName} Station
      AutomaticStopTimeMinutes: 15
      InstanceType: !Ref EC2InstanceType
      SubnetId:
        Ref: SubnetId

在上述代码中,CloudFormation 定义了一个 Cloud9 管理站 - Cloud9 提供基于云的 IDE 和终端,在 EC2 实例上运行。让我们通过这个例子来讨论模板的结构和特性。

AWSTemplateFormatVersion属性是必需的,它指定了 CloudFormation 模板的格式版本,通常以日期形式表示。Parameters属性定义了一组输入参数,您可以将这些参数提供给您的模板,这是处理多个环境的好方法,因为您可能在每个环境之间有不同的输入值。例如,EC2InstanceType参数指定了管理站的 EC2 实例类型,而SubnetId参数指定了 EC2 实例应连接到的子网。这两个值在非生产环境和生产环境之间可能不同,因此将它们作为输入参数使得根据目标环境更容易更改。请注意,SubnetId参数指定了AWS::EC2::Subnet::Id类型,这意味着 CloudFormation 可以使用它来查找或验证输入值。有关支持的参数类型列表,请参见docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html。您还可以看到EC2InstanceType参数为参数定义了默认值,如果没有为此参数提供输入,则将使用该默认值。

Resources属性定义了堆栈中的所有资源 - 这实际上是模板的主体部分,可能包含多达两百个资源。在上面的代码中,我们只定义了一个名为ManagementStation的资源,这将创建 Cloud9 EC2 环境,其Type值为AWS::Cloud9::EnvironmentEC2。所有资源必须指定一个Type属性,该属性定义了资源的类型,并确定了每种类型可用的各种配置属性。CloudFormation 用户指南包括一个定义了所有支持的资源类型的部分,截至最后一次统计,有 300 种不同类型的资源。

每个资源还包括一个 Properties 属性,其中包含资源可用的所有各种配置属性。在上面的代码中,您可以看到我们定义了五个不同的属性 - 可用的属性将根据资源类型而变化,并在 CloudFormation 用户指南中得到充分的文档记录。

  • 名称:这指定了 Cloud9 EC2 环境的名称。属性的值可以是简单的标量值,比如字符串或数字,但是值也可以引用模板中的其他参数或资源。请注意,Name属性的值包括所谓的内置函数Sub,可以通过前面的感叹号(!Sub)来识别。!Sub语法实际上是Fn::Sub的简写,你可以在Description属性中看到一个例子。Fn::Sub内置函数允许您定义一个表达式,其中包括对堆栈中其他资源或参数的插值引用。例如,Name属性的值是${AWS::StackName}-station,其中${AWS::StackName}是一个称为伪参数的插值引用,它将被模板部署时的 CloudFormation 堆栈的名称替换。如果您的堆栈名称是cloud9-management,那么${AWS::StackName}-station的值在部署堆栈时将扩展为cloud9-management-station

  • Description: 这为 Cloud9 EC2 环境提供了描述。这包括Fn::Sub内部函数的长格式示例,该函数要求您缩进一个新行,而简写的!Sub格式允许您在同一行上指定值。

  • AutomaticStopTime: 这定义了在停止 Cloud9 EC2 实例之前等待的空闲时间,单位为分钟。这可以节省成本,但只有在您使用 EC2 实例时才会运行(Cloud9 将自动启动您的实例,并从您之前停止的地方恢复会话)。在上面的代码中,该值是一个简单的标量值为 15。

  • InstanceType: 这是 EC2 实例的类型。这引用了EC2InstanceType参数,使用了 Ref 内部函数(!Ref是简写形式),允许您引用堆栈中的其他参数或资源。这意味着在部署堆栈时为此参数提供的任何值都将应用于InstanceType属性。

  • SubnetId: 这是 EC2 实例将部署的目标子网 ID。此属性引用了 SubnetID 参数,使用了Ref内部函数的长格式,这要求您在缩进的新行上表达此引用。

部署 CloudFormation 堆栈

现在您已经定义了一个 CloudFormation 模板,可以以 CloudFormation 堆栈的形式部署模板中的资源。

您可以通过选择服务 | CloudFormation在 AWS 控制台上部署堆栈,这将打开 CloudFormation 仪表板。在继续之前,请确保您已经在您的帐户中扮演了管理员角色,并且还选择了美国东部北弗吉尼亚(us-east-1)作为地区:

在本书的所有示例中,我们将使用美国东部北弗吉尼亚(us-east-1)地区。CloudFormation 仪表板

如果单击创建新堆栈按钮,将提示您选择模板,您可以选择示例模板、上传模板或指定 S3 模板 URL。因为我们在名为stack.yml的文件中定义了我们的堆栈,所以选择上传模板的选项,并单击选择文件按钮选择计算机上的文件:

选择 CloudFormation 模板

上传模板后,CloudFormation 服务将解析模板并要求您为堆栈指定名称,并为堆栈中的任何参数提供值:

指定模板详细信息

在上述截图中,默认情况下为EC2InstanceType参数设置了值t2.micro,因为您在模板中将其设置为默认值。由于您将AWS::EC2::Subnet::Id指定为SubnetId参数的类型,创建堆栈向导会自动查找您帐户和区域中的所有子网,并在下拉菜单中呈现它们。在这里,我选择了位于us-east-1a可用区的每个新 AWS 帐户中创建的默认 VPC 中的子网。

您可以通过在 AWS 控制台中选择服务|VPC|子网,或者通过运行带有 JMESPath 查询的aws ec2 describe-subnets AWS CLI 命令来确定每个子网属于哪个可用区:

> aws ec2 describe-subnets --query 'Subnets[].[SubnetId,AvailabilityZone,CidrBlock]' \
    --output table
-----------------------------------------------------
| DescribeSubnets                                   |
+-----------------+--------------+------------------+
| subnet-a5d3ecee | us-east-1a   | 172.31.16.0/20   |
| subnet-c2abdded | us-east-1d   | 172.31.80.0/20   |
| subnet-aae11aa5 | us-east-1f   | 172.31.48.0/20   |
| subnet-fd3a43c2 | us-east-1e   | 172.31.64.0/20   |
| subnet-324e246f | us-east-1b   | 172.31.32.0/20   |
| subnet-d281a2b6 | us-east-1c   | 172.31.0.0/20    |
+-----------------+--------------+------------------+

此时,您可以单击下一步,然后在创建堆栈向导中单击创建,以开始部署新堆栈。在 CloudFormation 仪表板中,您将看到创建了一个名为cloud9-management的新堆栈,最初状态为CREATE_IN_PROGRESS。通过 CloudFormation 部署 Cloud9 环境的一个有趣行为是,通过AWS::Cloud9::Environment资源会自动创建一个单独的子 CloudFormation 堆栈,这在部署其他类型的 CloudFormation 资源时是不太常见的。部署完成后,堆栈的状态将变为CREATE_COMPLETE

部署 CloudFormation 堆栈

在上述截图中,您可以单击事件选项卡以显示与堆栈部署相关的事件。这将显示每个资源部署的进度,并指示是否存在任何失败。

现在您已成功部署了第一个 CloudFormation 堆栈,您应该可以使用全新的 Cloud9 IDE 环境。如果您在 AWS 控制台菜单栏中选择服务|Cloud9,您应该会看到一个名为cloud9-management-station的单个环境:

Cloud9 环境

如果单击打开 IDE按钮,这将打开一个包含安装了 AWS CLI 的集成终端的新 IDE 会话。请注意,会话具有创建 Cloud9 环境的用户关联的所有权限 - 在本例中,这是假定的admin角色,因此您可以从终端执行任何管理任务。Cloud9 环境也在您的 VPC 中运行,因此,如果您部署其他资源(如 EC2 实例),即使您的其他资源部署在没有互联网连接的私有子网中,您也可以从此环境本地管理它们。

确保您了解创建具有完全管理特权的 Cloud9 环境的影响。尽管这非常方便,但它确实代表了一个潜在的安全后门,可能被用来破坏您的环境和帐户。Cloud9 还允许您与其他用户共享您的 IDE,这可能允许其他用户冒充您并执行您被允许执行的任何操作。 Cloud9 IDE

更新 CloudFormation 堆栈

创建 CloudFormation 堆栈后,您可能希望对堆栈进行更改,例如添加其他资源或更改现有资源的配置。 CloudFormation 定义了与堆栈相关的三个关键生命周期事件 - CREATE,UPDATE 和 DELETE - 这些事件可以应用于堆栈中的单个资源,也可以应用于整个堆栈。

要更新堆栈,只需对 CloudFormation 模板进行任何必要的更改,并提交修改后的模板 - CloudFormation 服务将计算每个资源所需的更改,这可能导致创建新资源,更新或替换现有资源,或删除现有资源。 CloudFormation 还将首先进行任何新更改,仅当这些更改成功时,它才会清理应该被移除的任何资源。这提供了在 CloudFormation 堆栈更新失败的情况下恢复的更高机会,在这种情况下,CloudFormation 将尝试回滚更改以将堆栈恢复到其原始状态。

要测试更新您的 CloudFormation 堆栈,让我们对stack.yml模板进行小的更改:

AWSTemplateFormatVersion: "2010-09-09"

Description: Cloud9 Management Station

Parameters:
  EC2InstanceType:
    Type: String
    Description: EC2 instance type
    Default: t2.micro
  SubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: Target subnet for instance

Resources:
  ManagementStation:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Name: !Sub ${AWS::StackName}-station
      Description:
        Fn::Sub: ${AWS::StackName} Station
 AutomaticStopTimeMinutes: 20
      InstanceType: !Ref EC2InstanceType
      SubnetId:
        Ref: SubnetId

应用此更改,我们将使用 AWS CLI 而不是使用 AWS 控制台,AWS CLI 支持通过aws cloudformation deploy命令部署 CloudFormation 模板。我们将在本书的其余部分大量使用此命令,现在是介绍该命令的好时机:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --stack-name cloud9-management --template-file stack.yml \
--parameter-overrides SubnetId=subnet-a5d3ecee
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga: ****

Waiting for changeset to be created..
Waiting for stack create/update to complete

Failed to create/update the stack. Run the following command
to fetch the list of events leading up to the failure
aws cloudformation describe-stack-events --stack-name cloud9-management

在上述代码中,我们首先确保配置了正确的配置文件,然后运行aws cloudformation deploy命令,使用--stack-name标志指定堆栈名称和--template-file标志指定模板文件。--parameter-overrides标志允许您以<parameter>=<value>格式提供输入参数值-请注意,在像这样的更新场景中,如果您不指定任何参数覆盖,将使用先前提供的参数值(在本例中创建堆栈时)。

请注意,更新实际上失败了,如果您通过 CloudFormation 控制台查看堆栈事件,您可以找出堆栈更新失败的原因。

CloudFormation 堆栈更新失败

在上述屏幕截图中,您可以看到堆栈更新失败,因为更改需要 CloudFormation 创建并替换现有资源(在本例中为 Cloud9 环境)为新资源。由于 CloudFormation 始终在销毁任何已被替换的旧资源之前尝试创建新资源,因为资源配置了名称,CloudFormation 无法使用相同名称创建新资源,导致失败。这突显了 CloudFormation 的一个重要注意事项-在定义资源的静态名称时要非常小心-如果 CloudFormation 需要在像这样的更新场景中替换资源,更新将失败,因为通常资源名称必须是唯一的。

有关 CloudFormation 何时选择替换资源(如果正在更新资源),请参考Amazon Web Services 资源类型参考文档中为每种资源类型定义的资源属性。

您可以看到,CloudFormation 在失败后会自动回滚更改,撤消导致失败的任何更改。堆栈的状态最终会更改为UPDATE_ROLLBACK_COMPLETE,表示发生了失败和回滚。

解决堆栈失败的一个修复方法是在堆栈中的ManagementStation资源上删除Name属性 - 在这种情况下,CloudFormation 将确保生成一个唯一的名称(通常基于 CloudFormation 堆栈名称并附加一些随机的字母数字字符),这意味着每次更新资源以便需要替换时,CloudFormation 将简单地生成一个新的唯一名称并避免我们遇到的失败场景。

删除 CloudFormation 堆栈

现在您了解了如何创建和更新堆栈,让我们讨论如何删除堆栈。您可以通过 CloudFormation 仪表板非常轻松地删除堆栈,只需选择堆栈,选择操作,然后点击删除堆栈

删除 CloudFormation 堆栈

点击是,删除以确认删除堆栈后,CloudFormation 将继续删除堆栈中定义的每个资源。完成后,堆栈将从 CloudFormation 仪表板中消失,尽管您可以更改位于创建堆栈按钮下方的筛选器下拉菜单,以点击已删除以查看以前删除的堆栈。

有人可能会认为删除堆栈太容易了。如果您担心意外删除堆栈,您可以在前面的截图中选择更改终止保护选项以启用终止保护,这将防止堆栈被意外删除。

摘要

在本章中,您学习了如何通过创建免费账户和建立账户的根用户来开始使用 AWS。您学会了如何使用多因素身份验证来保护根访问权限,然后创建了一些 IAM 资源,这些资源是管理您的账户所必需的。您首先创建了一个名为admin的管理 IAM 角色,然后创建了一个管理员组,将其分配为允许假定您的管理 IAM 角色的单一权限。假定角色的这种方法是管理 AWS 的推荐和最佳实践方法,并支持更复杂的多账户拓扑结构,在这种结构中,您可以将所有 IAM 用户托管在一个账户中,并在其他账户中假定管理角色。

然后,您创建了一个用户组,并分配了一个托管策略,该策略强制要求属于该组的任何用户进行多因素身份验证(MFA)。MFA 现在应被视为任何使用 AWS 的组织的强制性安全要求,简单地将用户分配到强制执行 MFA 要求的用户组是一种非常简单和可扩展的机制来实现这一点。创建用户并将其分配到管理员和用户组后,您学会了首次用户设置其访问所需的步骤,其中包括使用一次性密码登录,建立新密码,然后设置 MFA 设备。一旦用户使用 MFA 登录,用户就能执行分配给他们的任何权限 - 例如,您在本章中创建的用户被分配到了管理员组,因此能够承担管理员 IAM 角色,您可以在 AWS 控制台中使用内置的 Switch Role 功能执行此操作。

随着您的 IAM 设置完成并能够通过控制台承担管理员角色,我们接下来将注意力转向命令行,安装 AWS CLI,通过控制台生成访问密钥,然后在本地~/.aws文件夹中配置您的访问密钥凭据,该文件夹由 AWS CLI 用于存储凭据和配置文件。您学会了如何在~/.aws/configuration文件中配置命名配置文件,该文件会自动承担管理员角色,并在 CLI 检测到需要新的临时会话凭据时提示输入 MFA 代码。您还创建了一个 EC2 密钥对,以便您可以使用 SSH 访问 EC2 实例。

最后,您了解了 AWS CloudFormation,并学会了如何定义 CloudFormation 模板并部署 CloudFormation 堆栈,这是基于您的 CloudFormation 模板定义的资源集合。您学会了 CloudFormation 模板的基本结构,如何使用 AWS 控制台创建堆栈,以及如何使用 AWS CLI 部署堆栈。

在下一章中,您将介绍弹性容器服务,您将充分利用您的新 AWS 账户,并学习如何创建 ECS 集群并将 Docker 应用程序部署到 ECS。

问题

  1. 真/假:建立免费的 AWS 账户需要一个有效的信用卡。

  2. 正确/错误:您应该始终使用根帐户执行管理操作。

  3. 正确/错误:您应该直接为 IAM 用户和/或组分配 IAM 权限。

  4. 您将使用哪个 IAM 托管策略来分配管理权限?

  5. 您运行什么命令来安装 AWS CLI?

  6. 正确/错误:当您配置 AWS CLI 时,您必须在本地存储您的 IAM 用户名和密码。

  7. 您在哪里存储 AWS CLI 的凭据?

  8. 您设置了一个需要 MFA 才能执行管理操作的 IAM 用户。IAM 用户设置了他们的 AWS CLI,但在尝试运行 AWS CLI 命令时抱怨未经授权的错误。命名配置文件包括source_profilerole_arnrole_session_name参数,并且您确认这些已正确配置。您将如何解决这个问题?

  9. 正确/错误:CloudFormation 模板可以使用 JSON 或 YAML 编写。

  10. 正确/错误:您可以使用!Ref关键字来引用 CloudFormation 模板中的另一个资源或参数。

  11. 您在 CloudFormation 模板中定义了一个资源,其中包括一个可选的Name属性,您将其配置为my-resource。您成功从模板创建了一个新堆栈,然后对文档中规定将需要替换整个资源的资源进行了更改。您能成功部署这个更改吗?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第四章:ECS 简介

弹性容器服务(ECS)是一项流行的 AWS 托管服务,为您的应用程序提供容器编排,并与各种 AWS 服务和工具集成。

在本章中,您将学习 ECS 的关键概念;ECS 的架构方式,并了解 ECS 的各个组件,包括弹性容器注册表(ECR),ECS 集群,ECS 容器实例,ECS 任务定义,ECS 任务和 ECS 服务。本章的重点将是使用 AWS 控制台创建您的第一个 ECS 集群,定义 ECS 任务定义,并配置 ECS 服务以部署您的第一个容器应用程序到 ECS。您将更仔细地了解 ECS 集群是如何由 ECS 容器实例形成的,并检查 ECS 容器实例的内部,以进一步了解 ECS 如何与您的基础架构连接以及如何部署和管理容器。最后,您将介绍 ECS 命令行界面(CLI),这是一个有用的工具,可以快速搭建 ECS 集群,任务定义和服务,它使用流行的 Docker Compose 格式来定义您的容器和服务。

将涵盖以下主题:

  • ECS 架构

  • 创建 ECS 集群

  • 理解 ECS 容器实例

  • 创建 ECS 任务定义

  • 创建 ECS 服务

  • 部署 ECS 服务

  • 运行 ECS 任务

  • 使用 ECS CLI

技术要求

以下是完成本章所需的技术要求:

  • Docker Engine 18.06 或更高版本

  • Docker Compose 1.22 或更高版本

  • jq

  • 对 AWS 账户的管理员访问权限

  • 根据第三章的说明配置本地 AWS 配置文件

  • 在第二章中配置的示例应用程序的工作 Docker 工作流程(请参阅github.com/docker-in-aws/docker-in-aws/tree/master/ch2)。

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch4

查看以下视频以查看代码实际操作:

bit.ly/2MTG1n3

ECS 架构

ECS 是 AWS 托管服务,为您提供了构建在 AWS 中部署和操作容器应用程序的核心构建块。

在 2017 年 12 月之前,弹性容器服务被称为 EC2 容器服务。

ECS 允许您:

  • 构建和发布您的 Docker 镜像到私有仓库

  • 创建描述容器镜像、配置和运行应用程序所需资源的定义。

  • 使用您自己的 EC2 基础设施或使用 AWS 托管基础设施启动和运行您的容器

  • 管理和监视您的容器

  • 编排滚动部署新版本或修订您的容器应用程序

为了提供这些功能,ECS 包括以下图表中说明的一些组件,并在下表中描述:

组件 描述
弹性容器注册表(ECR) 提供安全的私有 Docker 镜像仓库,您可以在其中发布和拉取您的 Docker 镜像。我们将在第五章中深入研究 ECR,使用 ECR 发布 Docker 镜像
ECS 集群 运行您的容器应用程序的 ECS 容器实例的集合。
ECS 容器实例 运行 Docker 引擎和 ECS 代理的 EC2 实例,该代理与 AWS ECS 服务通信,并允许 ECS 管理容器应用程序的生命周期。每个 ECS 容器实例都加入到单个 ECS 集群中。
ECS 代理 以 Docker 容器的形式运行的软件组件,与 AWS ECS 服务通信。代理负责代表 ECS 管理 Docker 引擎,从注册表中拉取 Docker 镜像,启动和停止 ECS 任务,并向 ECS 发布指标。
ECS 任务定义 定义组成您的应用程序的一个或多个容器和相关资源。每个容器定义包括指定容器镜像、应分配给容器的 CPU 和内存量、运行时环境变量等许多配置选项的信息。
ECS 任务 ECS 任务是 ECS 任务定义的运行时表现,代表在给定 ECS 集群上运行的任务定义中定义的容器。ECS 任务可以作为短暂的临时任务运行,也可以作为长期任务运行,这些任务是 ECS 服务的构建块。
ECS 服务 ECS 服务定义了在给定的 ECS 集群上运行的一个或多个长期运行的 ECS 任务实例,并代表您通常会考虑为您的应用程序或微服务实例。ECS 服务定义了一个 ECS 任务定义,针对一个 ECS 集群,并且还包括一个期望的计数,它定义了与服务关联的基于 ECS 任务定义的实例或 ECS 任务的数量。您的 ECS 服务可以与 AWS 弹性负载均衡服务集成,这允许您为您的 ECS 服务提供高可用的、负载均衡的服务端点,并且还支持新版本应用程序的滚动部署。
AWS ECS 管理 ECS 架构中的所有组件。提供管理 ECS 代理的服务端点,与其他 AWS 服务集成,并允许客户管理其 ECR 存储库、ECS 任务定义和 ECS 集群。

随着我们在本章中的进展,参考以下图表以获得各种 ECS 组件之间关系的视觉概述。

ECS 架构

创建 ECS 集群

为了帮助您了解 ECS 的基础知识,我们将通过 AWS 控制台逐步进行一系列配置任务。

我们首先将创建一个 ECS 集群,这是一组将运行您的容器应用程序的 ECS 容器实例,并且通常与 EC2 自动扩展组密切相关,如下图所示。

可以通过以下步骤执行创建 ECS 集群的操作:

本章中的所有 AWS 控制台配置示例都基于您已登录到 AWS 控制台并假定了适当的管理角色,如在第三章“开始使用 AWS”中所述。撰写本章时,本节中描述的任务特定于 us-east-1(北弗吉尼亚)地区,因此在继续之前,请确保您已在 AWS 控制台中选择了该地区。

  1. 从主 AWS 控制台中,在计算部分中选择“服务”|“弹性容器服务”。

  2. 如果您以前没有在您的 AWS 账户和地区中使用或配置过 ECS,您将看到一个欢迎屏幕,并且可以通过单击“开始”按钮来调用一个入门配置向导。

  3. 在撰写本文时,入门向导只允许您使用 Fargate 部署类型开始。我们将在后面的章节中了解有关 Fargate 的信息,因此请滚动到屏幕底部,然后单击取消

  4. 您将返回到 ECS 控制台,现在可以通过单击创建集群按钮开始创建 ECS 集群。

  5. 选择集群模板屏幕上,选择EC2 Linux + Networking模板,该模板将通过启动基于特殊的 ECS 优化 Amazon 机器映像(AMI)的 EC2 实例来设置网络资源和支持 Linux 的 Docker 的 EC2 自动扩展组,我们稍后将了解更多信息。完成后,单击下一步继续。

  6. 配置集群屏幕上,配置一个名为test-cluster的集群名称,确保EC2 实例类型设置为t2.micro以符合免费使用条件,并将密钥对设置为您在早期章节中创建的 EC2 密钥对。请注意,将创建新的 VPC 和子网,以及允许来自互联网(0.0.0.0/0)的入站 Web 访问(TCP 端口80)的安全组。完成后,单击创建开始创建集群:

配置 ECS 集群

  1. 此时,将显示启动状态屏幕,并将创建一些必要的资源来支持您的 ECS 集群。完成集群创建后,单击查看集群按钮继续。

现在,您将转到刚刚创建的test-cluster的详细信息屏幕。恭喜 - 您已成功部署了第一个 ECS 集群!

集群详细信息屏幕为您提供有关 ECS 集群的配置和操作数据 - 例如,如果您单击ECS 实例选项卡,则会显示集群中每个 ECS 容器实例的列表:

ECS 集群详细信息

您可以看到向导创建了一个单个容器实例,该实例正在从部署到显示的可用区的 EC2 实例上运行。请注意,您还可以查看有关 ECS 容器实例的其他信息,例如 ECS 代理版本和状态、运行任务、CPU/内存使用情况,以及 Docker Engine 的版本。

ECS 集群没有比这更多的东西——它本质上是一组 ECS 容器实例,这些实例又是运行 Docker 引擎以及提供 CPU、内存和网络资源来运行容器的 EC2 实例。

理解 ECS 容器实例

使用 AWS 控制台提供的向导创建 ECS 集群非常容易,但是显然,在幕后进行了很多工作来使您的 ECS 集群正常运行。本入门章节范围之外的所有创建资源的讨论都是不在讨论范围内的,但是在这个阶段,集中关注 ECS 容器实例并对其进行进一步详细检查是有用的,因为它们共同构成了 ECS 集群的核心。

加入 ECS 集群

当 ECS 创建集群向导启动实例并创建我们的 ECS 集群时,您可能会想知道 ECS 容器实例如何加入 ECS 集群。这个问题的答案非常简单,可以通过单击新创建集群中 ECS 容器实例的 EC2 实例 ID 链接来轻松理解。

此链接将带您转到 EC2 仪表板,其中选择了与容器实例关联的 EC2 实例,如下一个屏幕截图所示。请注意,我已经突出显示了一些元素,我们在讨论 ECS 容器实例时将会回顾到它们:

EC2 实例详情

如果您右键单击实例并选择实例设置 | 查看/更改用户数据(参见上一个屏幕截图),您将看到实例的用户数据,这是在实例创建时运行的脚本,可用于帮助初始化您的 EC2 实例:

加入 ECS 集群的 EC2 实例用户数据脚本

通过入门向导配置的用户数据脚本显示在上一个屏幕截图中,正如您所看到的,这是一个非常简单的 bash 脚本,它将ECS_CLUSTER=test-cluster文本写入名为/etc/ecs/ecs.config的文件中。在这个例子中,回想一下,test-cluster是您为 ECS 集群配置的名称,因此在引用的 ECS 代理配置文件中的这一行配置告诉运行在 ECS 容器实例上的代理尝试注册到名为test-cluster的 ECS 集群。

/etc/ecs/ecs.config文件包含许多其他配置选项,我们将在第六章中进一步详细讨论构建自定义 ECS 容器实例

授予加入 ECS 集群的访问权限

在上一个屏幕截图中,请注意连接到 ECS 集群不需要凭据—您可能会原谅认为 ECS 只允许任何 EC2 实例加入 ECS 集群,但当然这并不安全。

EC2 实例包括一个名为 IAM 实例配置文件的功能,它将 IAM 角色附加到定义实例可以执行的各种 AWS 服务操作的 EC2 实例上。在您的 EC2 实例的 EC2 仪表板中,您可以看到一个名为ecsInstanceRole的角色已分配给您的实例,如果您点击此角色,您将被带到 IAM 仪表板,显示该角色的摘要页面。

权限选项卡中,您可以看到一个名为AmazonEC2ContainerServiceforEC2Role的 AWS 托管策略附加到该角色,如果您展开该策略,您可以看到与该策略相关的各种 IAM 权限,如下面的屏幕截图所示:

EC2 实例角色 IAM 策略

请注意,该策略允许ecs:RegisterContainerInstance操作,这是 ECS 容器实例加入 ECS 集群所需的 ECS 权限,并且该策略还授予了ecs:CreateCluster权限,这意味着尝试注册到当前不存在的 ECS 集群的 ECS 容器实例将自动创建一个新的集群。

还有一件需要注意的事情是,该策略适用于所有资源,由"Resource": "*"属性指定,这意味着分配了具有此策略的角色的任何 EC2 实例都能够加入您帐户和区域中的任何 ECS 集群。再次强调,这可能看起来不太安全,但请记住,这是一个旨在简化授予 ECS 容器实例所需权限的策略,在后面的章节中,我们将讨论如何创建自定义 IAM 角色和策略,以限制特定 ECS 容器实例可以加入哪些 ECS 集群。

管理 ECS 容器实例

通常,ECS 容器实例应该是自管理的,需要很少的直接管理,但是总会有一些时候你需要排查你的 ECS 容器实例,因此学习如何连接到你的 ECS 容器实例并了解 ECS 容器实例内部发生了什么是很有用的。

连接到 ECS 容器实例

ECS 容器实例是常规的 Linux 主机,因此您可能期望,连接到您的实例只是意味着能够与实例建立安全外壳(SSH)会话:

  1. 如果您在 EC2 仪表板中导航回到您的实例,我们首先需要配置附加到您的实例的安全组,以允许入站 SSH 访问。您可以通过点击安全组,选择入站选项卡,然后点击编辑按钮来修改安全组的入站规则来实现这一点。

  2. 编辑入站规则对话框中,点击添加规则按钮,并使用以下设置添加新规则:

  • 协议:TCP

  • 端口范围:22

  • 来源:我的 IP

为 SSH 访问添加安全组规则

  1. 点击保存后,您将允许来自您的公共 IP 地址的入站 SSH 访问到 ECS 容器实例。如果您在浏览器中返回到您的 EC2 实例,现在您可以复制公共 IP 地址并 SSH 到您的实例。

以下示例演示了如何建立与实例的 SSH 连接,使用-i标志引用与实例关联的 EC2 密钥对的私钥。您还需要使用用户名ec2-user登录,这是 Amazon Linux 中包含的默认非 root 用户:

> ssh -i ~/.ssh/admin.pem ec2-user@34.201.120.79
The authenticity of host '34.201.120.79 (34.201.120.79)' can't be established.
ECDSA key fingerprint is SHA256:c/MniTAq931tJj8bCVtRUP9gixM/ZXZSqDuMENqpod0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '34.201.120.79' (ECDSA) to the list of known hosts.

   __| __| __|
   _| ( \__ \ Amazon ECS-Optimized Amazon Linux AMI 2017.09.g
 ____|\___|____/

For documentation visit, http://aws.amazon.com/documentation/ecs
5 package(s) needed for security, out of 7 available
Run "sudo yum update" to apply all updates.

首先要注意的是登录横幅指示此实例基于 Amazon ECS-Optimized Amazon Linux AMI,这是创建 ECS 容器实例时默认和推荐的 Amazon Machine Image(AMI)。AWS 定期维护此 AMI,并使用与 ECS 推荐使用的 Docker 和 ECS 代理版本定期更新,因此这是迄今为止最简单的用于 ECS 容器实例的平台,我强烈建议使用此 AMI 作为 ECS 容器实例的基础。

您可以在此处了解有关此 AMI 的更多信息:docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html。它包括每个 ECS 支持的区域的当前 AMI 映像 ID 列表。

在第六章中,构建自定义 ECS 容器实例,您将学习如何自定义和增强 Amazon ECS 优化的 Amazon Linux AMI。

检查本地 Docker 环境

正如您可能期望的那样,您的 ECS 容器实例将运行一个活动的 Docker 引擎,您可以通过运行docker info命令来收集有关其信息:

> docker info
Containers: 1
 Running: 1
 Paused: 0
 Stopped: 0
Images: 2
Server Version: 17.09.1-ce
Storage Driver: devicemapper
 Pool Name: docker-docker--pool
 Pool Blocksize: 524.3kB
 Base Device Size: 10.74GB
 Backing Filesystem: ext4
...
...

在这里,您可以看到实例正在运行 Docker 版本 17.09.1-ce,使用设备映射器存储驱动程序,并且当前只有一个容器正在运行。

现在让我们通过执行docker container ps命令来查看运行的容器:

> docker ps
CONTAINER ID   IMAGE                            COMMAND    CREATED          STATUS          NAMES
a1b1a89b5e9e   amazon/amazon-ecs-agent:latest   "/agent"   36 minutes ago   Up 36 minutes   ecs-agent

您可以看到 ECS 代理实际上作为一个名为ecs-agent的容器运行,这应该始终在您的 ECS 容器实例上运行,以便您的 ECS 容器实例由 ECS 管理。

检查 ECS 代理

如前所示,ECS 代理作为 Docker 容器运行,我们可以使用docker container inspect命令来收集有关此容器如何工作的一些见解。在先前的示例中,我们引用了 ECS 代理容器的名称,然后使用 Go 模板表达式以及--format标志来过滤命令输出,显示 ECS 代理容器到 ECS 容器实例主机的各种绑定挂载或卷映射。

在许多命令示例中,我正在将输出传输到jq实用程序,这是一个用于在命令行解析 JSON 输出的实用程序。 jq不是 Amazon Linux AMI 默认包含的,因此您需要通过运行sudo yum install jq命令来安装jq

> docker container inspect ecs-agent --format '{{json .HostConfig.Binds}}' | jq
[
  "/var/run:/var/run",
  "/var/log/ecs:/log",
  "/var/lib/ecs/data:/data",
  "/etc/ecs:/etc/ecs",
  "/var/cache/ecs:/var/cache/ecs",
  "/cgroup:/sys/fs/cgroup",
  "/proc:/host/proc:ro",
  "/var/lib/ecs/dhclient:/var/lib/dhclient",
  "/lib64:/lib64:ro",
  "/sbin:/sbin:ro"
]

运行 docker container inspect 命令

请注意,将/var/run文件夹从主机映射到代理,这将允许 ECS 代理访问位于/var/run/docker.sock的 Docker 引擎套接字,从而允许 ECS 代理管理 Docker 引擎。您还可以看到 ECS 代理日志将写入 Docker 引擎主机文件系统上的/var/log/ecs

验证 ECS 代理

ECS 代理包括一个本地 Web 服务器,可用于内省当前的 ECS 代理状态。

以下示例演示了使用curl命令内省 ECS 代理:

> curl -s localhost:51678 | jq
{
  "AvailableCommands": [
    "/v1/metadata",
    "/v1/tasks",
    "/license"
  ]
}
> curl -s localhost:51678/v1/metadata | jq
{
  "Cluster": "test-cluster",
  "ContainerInstanceArn": "arn:aws:ecs:us-east-1:385605022855:container-instance/f67cbfbd-1497-47c0-b56c-a910c923ba70",
  "Version": "Amazon ECS Agent - v1.16.2 (998c9b5)"
}

审查 ECS 代理

请注意,ECS 代理监听端口 51678,并提供三个可以查询的端点:

  • /v1/metadata:描述容器实例加入的集群、容器实例的 Amazon 资源名称(ARN)和 ECS 代理版本

  • /v1/tasks:返回当前正在运行的任务列表。目前我们还没有将任何 ECS 服务或任务部署到我们的集群,因此此列表为空

  • /license:提供适用于 ECS 代理软件的各种软件许可证

/v1/metadata端点特别有用,因为您可以使用此端点来确定 ECS 代理是否成功加入了给定的 ECS 集群。我们将在第六章中使用这一点,构建自定义 ECS 容器实例来执行实例创建的健康检查,以确保我们的实例已成功加入了正确的 ECS 集群。

ECS 容器实例日志

每个 ECS 容器实例都包括可帮助排除故障的日志文件。

您将处理的主要日志包括以下内容:

  • Docker 引擎日志:位于/var/log/docker

  • ECS 代理日志:位于/var/log/ecs

请注意,有两种类型的 ECS 代理日志:

  • 初始化日志:位于/var/log/ecs/ecs-init.log,这些日志提供与ecs-init服务相关的输出,这是一个 Upstart 服务,确保 ECS 代理在容器实例启动时运行。

  • 代理日志:位于/var/log/ecs/ecs-agent.log.*,这些日志提供与 ECS 代理操作相关的输出。这些日志是您检查任何与 ECS 代理相关问题的最常见日志。

创建 ECS 任务定义

现在您已经设置了 ECS 集群并了解了 ECS 容器实例如何注册到集群,现在是时候配置 ECS 任务定义了,该定义定义了您要为应用程序部署的容器的配置。ECS 任务定义可以定义一个或多个容器,以及其他元素,例如容器可能需要读取或写入的卷。

为了简化问题,我们将创建一个非常基本的任务定义,该任务将运行官方的 Nginx Docker 镜像,该镜像发布在hub.docker.com/_/nginx/。Nginx 是一个流行的 Web 服务器,默认情况下将提供欢迎页面,现在这足以代表一个简单的 Web 应用程序。

现在,让我们通过执行以下步骤为我们的简单 Web 应用程序创建一个 ECS 任务定义:

  1. 在 ECS 控制台上导航到服务 | 弹性容器服务。您可以通过从左侧菜单中选择任务定义并单击创建新任务定义按钮来创建一个新的任务定义。

  2. 选择启动类型兼容性屏幕上,选择EC2 启动类型,这将配置任务定义在基于您拥有和管理的基础设施上启动 ECS 集群。

  3. 配置任务和容器定义屏幕上,配置任务定义名称simple-web,然后向下滚动并单击添加容器以添加新的容器定义。

  4. 添加容器屏幕上,配置以下设置,完成后单击添加按钮以创建容器定义。此容器定义将在 ECS 容器主机上将端口 80 映射到容器中的端口80,允许从外部世界访问 Nginx Web 服务器:

  • 容器名称:nginx

  • 镜像:nginx

  • 内存限制250 MB 硬限制

  • 端口映射:主机端口80,容器端口80,协议 tcp:

创建容器定义

  1. 通过单击配置任务和容器定义页面底部的创建按钮完成任务定义的创建。

创建 ECS 服务

我们已经创建了一个 ECS 集群,并配置了一个 ECS 任务定义,其中包括一个运行 Nginx 的单个容器,具有适当的端口映射配置,以将 Nginx Web 服务器暴露给外部世界。

现在我们需要定义一个 ECS 服务,这将配置 ECS 以部署一个或多个实例到我们的 ECS 集群。ECS 服务将给定的 ECS 任务定义部署到给定的 ECS 集群,允许您配置要运行多少个实例(ECS 任务)的引用 ECS 任务定义,并控制更高级的功能,如负载均衡器集成和应用程序的滚动更新。

要创建一个新的 ECS 服务,请完成以下步骤:

  1. 在 ECS 控制台上,从左侧选择集群,然后单击您在本章前面创建的test-cluster

选择要创建 ECS 服务的 ECS 集群

  1. 在集群详细信息页面,选择服务选项卡,然后点击创建来创建一个新服务。

  2. 在配置服务屏幕上,配置以下设置,完成后点击下一步按钮。请注意,我们在本章前面创建的任务定义和 ECS 集群都有所提及:

  • 启动类型:EC2

  • 任务定义:simple-web:1

  • 集群:test-cluster

  • 服务名称:simple-web

  • 任务数量:1

  1. ECS 服务配置设置的其余部分是可选的。继续点击下一步,直到到达审阅屏幕,在那里您可以审阅您的设置并点击创建服务来完成 ECS 服务的创建。

  2. 启动状态屏幕现在将出现,一旦您的服务已创建,点击查看服务按钮。

  3. 服务详细信息屏幕现在将出现在您的新 ECS 服务中,您应该看到一个处于运行状态的单个 ECS 任务,这意味着与 simple-web ECS 任务定义相关联的 Nginx 容器已成功启动:

完成新 ECS 服务的创建

此时,您现在应该能够浏览到您新部署的 Nginx Web 服务器,您可以通过浏览到您之前作为 ECS 集群的一部分创建的 ECS 容器实例的公共 IP 地址来验证。如果一切正常,您应该会看到默认的欢迎使用 nginx页面,如下截图所示:

浏览到 Nginx Web 服务器

部署 ECS 服务

现在您已成功创建了一个 ECS 服务,让我们来看看 ECS 如何管理容器应用的新部署。重要的是要理解,ECS 任务定义是不可变的—也就是说,一旦创建了任务定义,就不能修改任务定义,而是需要创建一个全新的任务定义或创建当前任务定义的修订版,您可以将其视为给定任务定义的新版本。

ECS 将 ECS 任务定义的逻辑名称定义为family,ECS 任务定义的给定修订版以family:revision的形式表示—例如,my-task-definition:3指的是my-task-definition家族的第 3 个修订版。

这意味着为了部署一个容器应用的新版本,您需要执行一些步骤:

  1. 创建一个新的 ECS 任务定义修订,其中包含已更改为应用程序新版本的配置设置。这通常只是您为应用程序构建的 Docker 镜像关联的图像标签,但是任何配置更改,比如分配的内存或 CPU 资源的更改,都将导致创建 ECS 任务定义的新修订。

  2. 更新您的 ECS 服务以使用 ECS 任务定义的新修订版。每当以这种方式更新 ECS 服务时,ECS 将自动执行应用程序的滚动更新,试图优雅地用基于新 ECS 任务定义修订的新容器替换组成 ECS 服务的每个运行容器。

为了演示这种行为,现在让我们修改本章前面创建的 ECS 任务定义,并通过以下步骤更新 ECS 服务:

  1. 在 ECS 控制台中,从左侧选择任务定义,然后点击您之前创建的 simple-web 任务定义。

  2. 注意,当前任务定义修订只有一个存在——在任务定义名称后的冒号后面标明修订号。例如,simple-web:1 指的是 simple-web 任务定义的修订 1。选择当前任务定义修订,然后点击创建新修订以基于现有任务定义修订创建新的修订。

  3. 创建新的任务定义修订屏幕显示出来,这与您之前配置的创建新任务定义屏幕非常相似。滚动到容器定义部分,点击 Nginx 容器以修改 Nginx 容器定义。

  4. 我们将对任务定义进行的更改是修改端口映射,从当前端口 80 的静态主机映射到主机上的动态端口映射。这可以通过简单地将主机端口设置为空来实现,在这种情况下,Docker 引擎将从基础 ECS 容器实例上的临时端口范围中分配动态端口。对于我们使用的 Amazon Linux AMI,此端口范围介于3276860999之间。动态端口映射的好处是我们可以在同一主机上运行多个容器实例 - 如果静态端口映射已经存在,只能启动一个容器实例,因为随后的容器实例将尝试绑定到已使用的端口80。完成配置更改后,点击更新按钮继续。

  5. 创建新的任务定义版本屏幕的底部点击创建按钮,完成新版本的创建。

要获取 Docker 使用的临时端口范围,您可以检查/proc/sys/net/ipv4/ip_local_port_range文件的内容。如果您的操作系统上没有此文件,Docker 将使用4915365535的端口范围。

此时,已从您的 ECS 任务定义创建了一个新版本(版本 2)。现在,您需要通过完成以下步骤来更新您的 ECS 服务以使用新的任务定义版本。

  1. 在 ECS 控制台中,从左侧选择集群,选择您的测试集群。在服务选项卡上,选择您的 ECS 服务旁边的复选框,然后点击更新按钮。

  2. 在配置服务屏幕上的任务定义下拉菜单中,您应该能够选择您刚刚创建的任务定义的新版本(simple-web:2)。完成后,继续点击下一步按钮,直到到达审阅屏幕,在这时您可以点击更新服务按钮完成配置更改:

修改 ECS 服务任务定义

  1. 与您创建 ECS 服务时之前看到的类似,启动状态屏幕将显示。如果您点击查看服务按钮,您将进入 ECS 服务详细信息屏幕,如果选择部署选项卡,您应该看到正在部署的任务定义的新版本:

ECS 服务部署

请注意,有两个部署——活动部署显示现有的 ECS 服务部署,并指示当前有一个正在运行的容器。主要部署显示基于新修订的新 ECS 服务部署,并指示期望计数为 1,但请注意运行计数尚未为 1。

如果您定期刷新部署状态,您将能够观察到新任务定义修订版部署时的各种状态变化:

部署更改将会相当快速地进行,所以如果您没有看到任何这些更改,您可以随时更新 ECS 服务,以使用 ECS 任务定义的第一个修订版本来强制进行新的部署。

  1. 主要部署应该指示挂起计数为 1,这意味着新版本的容器即将启动。

新部署待转换

  1. 主要部署接下来将转换为运行计数为 1,这意味着新版本的容器正在与现有容器一起运行:

新部署运行转换

  1. 在这一点上,现有容器现在可以停止,所以您应该看到活动部署的运行计数下降到零:

旧部署停止转换

  1. 活动部署从部署选项卡中消失,滚动部署已完成:

滚动部署完成

在这一点上,我们已成功执行了 ECS 服务的滚动更新,值得指出的是,新的动态端口映射配置意味着您的 Nginx Web 服务器不再在端口 80 上对外界进行监听,而是在 ECS 容器实例动态选择的端口上进行监听。

您可以通过尝试浏览到您的 Nginx Web 服务器的公共 IP 地址来验证这一点——这应该会导致连接失败,因为 Web 服务器不再在端口 80 上运行。如果您选择Tasks选项卡,找到simple-web ECS 服务,您可以点击任务,找出我们的 Web 服务器现在正在监听的端口。

在扩展 Nginx 容器后,您可以看到在这种情况下,ECS 容器实例主机上的端口32775映射到 Nginx 容器上的端口80,但由于 ECS 容器实例分配的安全组仅允许在端口80上进行入站访问,因此您无法从互联网访问该端口。

为了使动态端口映射有用,您需要将您的 ECS 服务与应用程序负载均衡器相关联,负载均衡器将自动检测每个 ECS 服务实例的动态端口映射,并将传入的请求负载均衡到负载均衡器上定义的静态端口到每个 ECS 服务实例。您将在后面的章节中了解更多相关内容。ECS 服务动态端口映射

运行 ECS 任务

我们已经看到了如何将长时间运行的应用程序部署为 ECS 服务,但是如何使用 ECS 运行临时任务或短暂的容器呢?答案当然是创建一个 ECS 任务,通常用于运行临时任务,例如运行部署脚本,执行数据库迁移,或者执行定期批处理。

尽管 ECS 服务本质上是长时间运行的 ECS 任务,但 ECS 确实会对您自己创建的 ECS 任务与 ECS 服务进行不同的处理,如下表所述:

场景/特性 ECS 服务行为 ECS 任务行为
容器停止或失败 ECS 将始终尝试维护给定 ECS 服务的期望计数,并将尝试重新启动容器,如果活动计数由于容器停止或失败而低于期望计数。 ECS 任务是一次性执行,要么成功,要么失败。ECS 永远不会尝试重新运行失败的 ECS 任务。
任务定义配置 您无法覆盖给定 ECS 服务的任何 ECS 任务定义配置。 ECS 任务允许您覆盖环境变量和命令行设置,允许您利用单个 ECS 任务定义来运行各种不同类型的 ECS 任务。
负载均衡器集成 ECS 服务具有与 AWS 弹性负载均衡服务的完全集成。 ECS 任务不与任何负载均衡服务集成。

ECS 服务与 ECS 任务

让我们现在看看如何使用 AWS 控制台运行 ECS 任务。您将创建一个非常简单的 ECS 任务,该任务将在 ECS 任务定义中定义的 Nginx 镜像中运行sleep 300命令。

这将导致任务在执行之前休眠五分钟,模拟短暂的临时任务:

  1. 在 ECS 控制台上,选择左侧的集群,然后单击名为test-cluster的集群。

  2. 选择任务选项卡,单击运行新任务按钮以创建新的 ECS 任务:

运行 ECS 任务

  1. 运行任务屏幕上,首先选择EC2作为启动类型,并确保任务定义集群设置正确配置。如果您展开高级选项部分,注意您可以为nginx容器指定容器覆盖。请注意,要配置命令覆盖,您必须以逗号分隔的格式提供要运行的命令及其任何参数,例如,要执行sleep 300命令,您必须配置sleep,300的命令覆盖。配置完成后,单击运行任务以执行新的 ECS 任务:

配置 ECS 任务

此时,您将返回到 ECS 集群的任务选项卡,您应该看到一个状态为挂起的新任务:

ECS 任务处于挂起状态

新任务应该很快转换为运行状态,如果我们让任务运行,它最终会在五分钟后退出。

现在让我们利用这个机会观察 ECS 任务在停止时的行为。如果您选择所有任务并单击停止按钮,系统将提示您确认是否要停止每个任务。确认要停止每个任务后,任务窗格应立即显示没有活动任务,然后单击刷新按钮几次后,您应该看到一个任务重新启动。这个任务是由 ECS 自动启动的,以保持 simple-web 服务的期望计数为 1。

使用 ECS CLI

在本章中,我们专注于使用 AWS 控制台来开始使用 ECS。AWS 编写和维护的另一个工具称为 ECS CLI,它允许您从命令行创建 ECS 集群并部署 ECS 任务和服务。

ECS CLI 与 AWS CLI 在许多方面不同,但主要区别包括:

  • ECS CLI 专注于与 ECS 交互,并且仅支持与为 ECS 提供支持资源的其他 AWS 服务进行交互,例如 AWS CloudFormation 和 EC2 服务。

  • ECS CLI 操作比 AWS CLI 操作更粗粒度。例如,ECS CLI 将编排创建 ECS 集群及其所有支持资源,就像您在本章前面使用的 ECS 集群向导的行为一样,而 AWS CLI 则专注于执行单个特定任务的更细粒度操作。

  • ECS CLI 是用 Golang 编写的,而 AWS CLI 是用 Python 编写的。这确实引入了一些行为差异——例如,ECS CLI 不支持启用了 MFA(多因素认证)的 AWS 配置文件的使用,这意味着您需要使用不需要 MFA 的 AWS 凭据和角色。

ECS CLI 的一个特别有用的功能是它支持 Docker Compose 文件的第 1 版和第 2 版,这意味着您可以使用 Docker Compose 来提供对多容器环境的通用描述。ECS CLI 还允许您使用基于 YAML 的配置文件来定义您的基础设施,因此可以被视为一个简单而功能强大的基础设施即代码工具。

一般来说,ECS CLI 对于快速搭建沙盒/开发环境以进行快速原型设计或测试非常有用。对于部署正式的非生产和生产环境,您应该使用诸如 Ansible、AWS CloudFormation 或 Terraform 等工具和服务,这些工具和服务提供了对您运行生产级环境所需的所有 AWS 资源的更广泛支持。

ECS CLI 包括完整的文档,您可以在 docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_CLI.html 找到。您还可以查看 ECS CLI 源代码并在 github.com/aws/amazon-ecs-cli 上提出问题。

删除测试集群

此时,您应该按照 ECS 仪表板中的以下步骤删除本章中创建的测试集群:

  1. 从集群中选择测试集群

  2. 选择并更新 simple-web ECS 服务,使其期望计数为 0

  3. 等待直到 simple-web ECS 任务计数下降到 0

  4. 选择测试集群,然后单击删除集群按钮

摘要

在本章中,您了解了 ECS 架构,并了解了构成 ECS 的核心组件。您了解到 ECS 集群是一组 ECS 容器实例,这些实例在 EC2 自动扩展组实例上运行 Docker 引擎。AWS 为您提供了预构建的 ECS 优化 AMI,使得使用 ECS 能够快速启动和运行。每个 ECS 容器实例包括一个作为系统容器运行并与 ECS 通信的 ECS 代理,提供启动、停止和部署容器所需的管理和控制平面。

接下来,您创建了一个 ECS 任务定义,该定义定义了一个或多个容器和卷定义的集合,包括容器映像、环境变量和 CPU/内存资源分配等信息。有了您的 ECS 集群和 ECS 任务定义,您随后能够创建和配置一个 ECS 服务,引用 ECS 任务定义来定义 ECS 服务的容器配置,并将一个或多个实例的 ECS 服务定位到您的 ECS 集群。

ECS 支持滚动部署以更新容器应用程序,您可以通过简单地创建 ECS 任务定义的新版本,然后将定义与 ECS 服务关联来成功部署新的应用程序更改。

最后,您学会了如何使用 ECS CLI 简化创建 ECS 集群和服务,使用 Docker Compose 作为通用机制来定义任务定义和 ECS 服务。

在下一章中,您将更仔细地了解弹性容器注册表(ECR)服务,您将学习如何创建自己的私有 ECR 存储库,并将您的 Docker 映像发布到这些存储库。

问题

  1. 列出三个在使用 ECS 运行长时间运行的 Docker 容器所需的 ECS 组件

  2. 真/假:ECS 代理作为 upstart 服务运行

  3. 在使用 ECS CLI 时,您使用什么配置文件格式来定义基础架构?

  4. 真/假:您可以将两个实例的 ECS 任务部署到单个实例 ECS 集群并进行静态端口映射

  5. 真/假:ECS CLI 被认为是将 Docker 环境部署到生产环境的最佳工具

  6. 使用 ECS 运行每晚运行 15 分钟的批处理作业时,您将配置什么?

  7. 真/假:ECS 任务定义是可变的,可以修改

  8. 真/假:您可以通过运行curl localhost:51678命令来检查给定 Docker Engine 上代理的当前状态

更多信息

您可以查看以下链接以获取有关本章涵盖的主题的更多信息:

第五章:使用 ECR 发布 Docker 镜像

Docker 注册表是 Docker 和容器生态系统的关键组件,提供了一种通用机制来公开和分发您的容器应用程序,无论是公开还是私有。

ECR 提供了一个完全托管的私有 Docker 注册表,具有与上一章介绍的 ECS 组件和其他 AWS 服务紧密集成的特性。ECR 具有高度可扩展性,安全性,并提供工具来与用于构建和发布 Docker 镜像的本机 Docker 客户端集成。

在本章中,您将学习如何创建 ECR 存储库来存储您的 Docker 镜像,使用各种机制,包括 AWS 控制台,AWS CLI 和 CloudFormation。一旦您建立了第一个 ECR 存储库,您将学习如何使用 ECR 进行身份验证,拉取存储在您的存储库中的 Docker 镜像,并使用 Docker 客户端构建和发布 Docker 镜像到 ECR。最后,您将学习如何处理更高级的 ECR 使用和管理场景,包括配置跨帐户访问以允许在其他 AWS 帐户中运行的 Docker 客户端访问您的 ECR 存储库,并配置生命周期策略,以确保孤立的 Docker 镜像定期清理,减少管理工作量和成本。

将涵盖以下主题:

  • 了解 ECR

  • 创建 ECR 存储库

  • 登录到 ECR

  • 将 Docker 镜像发布到 ECR

  • 从 ECR 拉取 Docker 镜像

  • 配置生命周期策略

技术要求

以下列出了完成本章所需的技术要求:

  • Docker 18.06 或更高版本

  • Docker Compose 1.22 或更高版本

  • GNU Make 3.82 或更高版本

  • jq

  • AWS CLI 1.15.71 或更高版本

  • 对 AWS 帐户的管理员访问权限

  • 本地 AWS 配置文件按第三章中的说明配置

  • 在第二章中配置的示例应用程序的工作 Docker 工作流程(请参阅github.com/docker-in-aws/docker-in-aws/tree/master/ch2)。

此 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch5

查看以下视频以查看代码的实际操作:

bit.ly/2PKMLSP

了解 ECR

在开始创建和配置 ECR 存储库之前,重要的是要简要介绍 ECR 的核心概念。

ECR 是由 AWS 提供的完全托管的私有 Docker 注册表,并与 ECS 和其他 AWS 服务紧密集成。ECR 包括许多组件,如下图所示:

ECR 架构

ECR 的核心组件包括:

  • 仓库: 仓库存储给定 Docker 镜像的所有版本。每个仓库都配置有名称和 URI,该 URI 对于您的 AWS 帐户和区域是唯一的。

  • 权限: 每个仓库都包括权限,允许您授予各种 ECR 操作的访问权限,例如推送或拉取 Docker 镜像。

  • 生命周期策略: 每个仓库都可以配置一个可选的生命周期策略,用于清理已被新版本取代的孤立的 Docker 镜像,或者删除您可能不再使用的旧 Docker 镜像。

  • 认证服务: ECR 包括一个认证服务,其中包括一个令牌服务,可用于以临时认证令牌交换您的 IAM 凭据以进行身份验证,与 Docker 客户端身份验证过程兼容。

考虑 ECR 的消费者也很重要。如前图所示,这些包括:

  • 与您的仓库在同一本地 AWS 帐户中的 Docker 客户端: 这通常包括在 ECS 集群中运行的 ECS 容器实例。

  • 不同 AWS 帐户中的 Docker 客户端: 这是较大组织的常见情况,通常包括在远程帐户中运行的 ECS 集群中的 ECS 容器实例。

  • AWS 服务使用的 Docker 客户端: 一些 AWS 服务可以利用您在 ECR 中发布的自己的 Docker 镜像,例如 AWS CodeBuild 服务。

在撰写本书时,ECR 仅作为私有注册表提供 - 这意味着如果您想公开发布您的 Docker 镜像,那么至少在发布您的公共 Docker 镜像方面,ECR 不是正确的解决方案。

创建 ECR 仓库

现在您已经对 ECR 有了基本概述,让我们开始创建您的第一个 ECR 存储库。回想一下,在早期的章节中,您已经介绍了本书的示例todobackend应用程序,并在本地环境中构建了一个 Docker 镜像。为了能够在基于此镜像的 ECS 集群上运行容器,您需要将此镜像发布到 ECS 容器实例可以访问的 Docker 注册表中,而 ECR 正是这个问题的完美解决方案。

todobackend应用程序创建 ECR 存储库,我们将专注于三种流行的方法来创建和配置您的存储库:

  • 使用 AWS 控制台创建 ECR 存储库

  • 使用 AWS CLI 创建 ECR 存储库

  • 使用 AWS CloudFormation 创建 ECR 存储库

使用 AWS 控制台创建 ECR 存储库

通过执行以下步骤,可以在 AWS 控制台上创建 ECR 存储库:

  1. 从主 AWS 控制台中,选择服务 | 弹性容器服务,在计算部分中选择存储库,然后单击“开始”按钮。

  2. 您将被提示配置存储库的名称。一个标准的约定是以<organization>/<application>格式命名您的存储库,这将导致一个完全合格的存储库 URI 为<registry>/<organization>/<application>。在下面的示例中,我将存储库命名为docker-in-aws/todobackend,但您可以根据自己的喜好命名您的镜像。完成后,点击“下一步”继续:

配置存储库名称

  1. 您的 ECR 存储库现在将被创建,并提供如何登录到 ECR 并发布您的 Docker 镜像的说明。

使用 AWS CLI 创建 ECR 存储库

通过运行aws ecr create-repository命令可以创建 ECR 存储库,但是考虑到您已经通过 AWS 控制台创建了存储库,让我们看看如何检查 ECR 存储库是否已经存在以及如何使用 AWS CLI 删除存储库。

查看您的 AWS 帐户和本地区域中的 ECR 存储库列表,您可以使用aws ecr list-repositories命令,而要删除 ECR 存储库,您可以使用aws ecr delete-repository命令,如下所示:

> aws ecr list-repositories
{
    "repositories": [
        {
            "repositoryArn": "arn:aws:ecr:us-east-1:385605022855:repository/docker-in-aws/todobackend",
            "registryId": "385605022855",
            "repositoryName": "docker-in-aws/todobackend",
            "repositoryUri": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend",
            "createdAt": 1517692382.0
        }
    ]
}
> aws ecr delete-repository --repository-name docker-in-aws/todobackend
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-east-1:385605022855:repository/docker-in-aws/todobackend",
        "registryId": "385605022855",
        "repositoryName": "docker-in-aws/todobackend",
        "repositoryUri": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend",
        "createdAt": 1517692382.0
    }
}

使用 AWS CLI 描述和删除 ECR 存储库

现在,您已经使用 AWS 控制台删除了之前创建的仓库,您可以按照这里演示的方法重新创建它:

> aws ecr create-repository --repository-name docker-in-aws/todobackend
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-east-1:385605022855:repository/docker-in-aws/todobackend",
        "registryId": "385605022855",
        "repositoryName": "docker-in-aws/todobackend",
        "repositoryUri": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend",
        "createdAt": 1517693074.0
    }
}

使用 AWS CLI 创建 ECR 仓库

使用 AWS CloudFormation 创建 ECR 仓库

AWS CloudFormation 支持通过AWS::ECR::Repository资源类型创建 ECR 仓库,在撰写本文时,这允许您管理 ECR 资源策略和生命周期策略,我们将在本章后面介绍。

作为一个经验法则,鉴于 ECR 仓库作为 Docker 镜像分发机制的关键性质,我通常建议将您的帐户和区域中的各种 ECR 仓库定义在一个单独的共享 CloudFormation 堆栈中,专门用于创建和管理 ECR 仓库。

遵循这个建议,并为将来的章节,让我们创建一个名为todobackend-aws的仓库,您可以用来存储您将在本书中创建和管理的各种基础架构配置。我会让您在 GitHub 上创建相应的仓库,之后您可以将您的 GitHub 仓库配置为远程仓库:

> mkdir todobackend-aws
> touch todobackend-aws/ecr.yml > cd todobackend-aws
> git init Initialized empty Git repository in /Users/jmenga/Source/docker-in-aws/todobackend-aws/.git/
> git remote add origin https://github.com/jmenga/todobackend-aws.git
> tree .
.
└── ecr.yml

现在,您可以配置一个名为ecr.yml的 CloudFormation 模板文件,该文件定义了一个名为todobackend的单个 ECR 仓库:

AWSTemplateFormatVersion: "2010-09-09"

Description: ECR Repositories

Resources:
  TodobackendRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: docker-in-aws/todobackend

使用 AWS CloudFormation 定义 ECR 仓库

正如您在前面的示例中所看到的,使用 CloudFormation 定义 ECR 仓库非常简单,只需要定义RepositoryName属性,这个属性定义了仓库的名称,正如您所期望的那样。

假设您已经删除了之前的 todobackend ECR 仓库,就像之前演示的那样,现在您可以使用aws cloudformation deploy命令使用 CloudFormation 创建 todobackend 仓库:

> aws cloudformation deploy --template-file ecr.yml --stack-name ecr-repositories
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - ecr-repositories

使用 AWS CloudFormation 创建 ECR 仓库

一旦堆栈成功部署,您可以在 CloudFormation 控制台中查看堆栈,如下截图所示:

ECR 仓库 CloudFormation 堆栈

如果您现在返回 ECS 控制台,并从左侧菜单中选择资源,您应该会看到一个名为docker-in-aws/todobackend的单个 ECR 仓库,就像在您的 CloudFormation 堆栈中定义的那样。如果您点击该仓库,您将进入仓库详细页面,该页面为您提供了仓库 URI、仓库中发布的镜像列表、ECR 权限和生命周期策略设置。

登录到 ECR

创建 Docker 镜像的存储库后,下一步是构建并将您的镜像发布到 ECR。在此之前,您必须对 ECR 进行身份验证,因为在撰写本文时,ECR 是一个不支持公共访问的私有服务。

登录到 ECR 的说明和命令显示在 ECR 存储库向导的一部分中,但是您可以随时通过选择适当的存储库并单击查看推送命令按钮来查看这些说明,该按钮将显示登录、构建和发布 Docker 镜像到存储库所需的各种命令。

显示的第一个命令是aws ecr get-login命令,它将生成一个包含临时身份验证令牌的docker login表达式,有效期为 12 小时(请注意,出于节省空间的考虑,命令输出已被截断):

> aws ecr get-login --no-include-email
docker login -u AWS -p eyJwYXl2ovSUVQUkJkbGJ5cjQ1YXJkcnNLV29ubVV6TTIxNTk3N1RYNklKdllvanZ1SFJaeUNBYk84NTJ2V2RaVzJUYlk9Iiw
idmVyc2lvbiI6IjIiLCJ0eXBlIjoiREFUQV9LRVkiLCJleHBpcmF0aW9uIjoxNTE4MTIyNTI5fQ== https://385605022855.dkr.ecr.us-east-1.amazonaws.com

为 ECR 生成登录命令

对于 Docker 版本 17.06 及更高版本,--no-include-email标志是必需的,因为从此版本开始,-e Docker CLI 电子邮件标志已被弃用。

尽管您可以复制并粘贴前面示例中生成的命令输出,但更快的方法是使用 bash 命令替换自动执行aws ecr get-login命令的输出,方法是用$(...)将命令括起来:

> $(aws ecr get-login --no-include-email)
Login Succeeded

登录到 ECR

将 Docker 镜像发布到 ECR

在早期的章节中,您学习了如何使用 todobackend 示例应用程序在本地构建和标记 Docker 镜像。

现在,您可以将此工作流程扩展到将 Docker 镜像发布到 ECR,这需要您执行以下任务:

  • 确保您已登录到 ECR

  • 使用您的 ECR 存储库的 URI 构建和标记您的 Docker 镜像

  • 将您的 Docker 镜像推送到 ECR

使用 Docker CLI 发布 Docker 镜像

您已经看到如何登录 ECR,并且构建和标记您的 Docker 镜像与本地使用情况大致相同,只是在标记图像时需要指定 ECR 存储库的 URI。

以下示例演示了构建todobackend镜像,使用您的新 ECR 存储库的 URI 标记图像(用于您的存储库的实际 URI),并使用docker images命令验证图像名称:

> cd ../todobackend
> docker build -t 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend .
Sending build context to Docker daemon 129.5kB
Step 1/25 : FROM alpine AS build
 ---> 3fd9065eaf02
Step 2/25 : LABEL application=todobackend
 ---> Using cache
 ---> f955808a07fd
...
...
...
Step 25/25 : USER app
 ---> Running in 4cf3fcab97c9
Removing intermediate container 4cf3fcab97c9
---> 2b2d8d17367c
Successfully built 2b2d8d17367c
Successfully tagged 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest
> docker images
REPOSITORY                                                             TAG    IMAGE ID     SIZE 
385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend latest 2b2d8d17367c 99.4MB

为 ECR 标记图像

构建并标记了您的镜像后,您可以将您的镜像推送到 ECR。

请注意,要将图像发布到 ECR,您需要各种 ECR 权限。因为您在您的帐户中使用管理员角色,所以您自动拥有所有所需的权限。我们将在本章后面更详细地讨论 ECR 权限。

因为您已经登录到 ECR,所以只需使用docker push命令并引用您的 Docker 图像的名称即可:

> docker push 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend]
1cdf73b07ed7: Pushed
0dfffc4aa16e: Pushed
baaced0ec8f8: Pushed
e3b27097ac3f: Pushed
3a29354c4bcc: Pushed
a031167f960b: Pushed
cd7100a72410: Pushed
latest: digest: sha256:322c8b378dd90b3a1a6dc8553baf03b4eb13ebafcc926d9d87c010f08e0339fa size: 1787

将图像推送到 ECR

如果您现在在 ECS 控制台中导航到 todobackend 存储库,您应该会看到您新发布的图像以默认的latest标签出现,如下图所示。请注意,当您比较图像的构建大小(在我的示例中为 99 MB)与存储在 ECR 中的图像大小(在我的示例中为 34 MB)时,您会发现 ECR 以压缩格式存储图像,从而降低了存储成本。

在使用 ECR 时,AWS 会对数据存储和数据传输(即拉取 Docker 图像)收费。有关更多详细信息,请参见aws.amazon.com/ecr/pricing/查看 ECR 图像

使用 Docker Compose 发布 Docker 图像

在之前的章节中,您已经学会了如何使用 Docker Compose 来帮助简化测试和构建 Docker 图像所需的 CLI 命令数量。目前,Docker Compose 只能在本地构建 Docker 图像,但当然您现在希望能够发布您的 Docker 图像并利用您的 Docker Compose 工作流程。

Docker Compose 包括一个名为image的服务配置属性,通常用于指定要运行的容器的图像:

version: '2.4'

services:
  web:
    image: nginx

示例 Docker Compose 文件

尽管这是 Docker Compose 的一个非常常见的使用模式,但如果您结合buildimage属性,还存在另一种配置和行为集,如在 todobackend 存储库的docker-compose.yml文件中所示:

version: '2.4'

volumes:
  public:
    driver: local

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile
      target: test
  release:
 image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      DJANGO_SETTINGS_MODULE: todobackend.settings_release
      MYSQL_HOST: db
      MYSQL_USER: todo
      MYSQL_PASSWORD: password
  app:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:${APP_VERSION}
    extends:
  ...
  ...

Todobackend Docker Compose 文件

在上面的示例中,为releaseapp服务同时指定了imagebuild属性。当这两个属性一起使用时,Docker 仍将从引用的 Dockerfile 构建图像,但会使用image属性指定的值对图像进行标记。

您可以通过创建新服务并定义包含附加标签的图像属性来应用多个标签。

请注意,对于app服务,我们引用环境变量APP_VERSION,这意味着要使用在 todobackend 存储库根目录的 Makefile 中定义的当前应用程序版本标记图像:

.PHONY: test release clean version

export APP_VERSION ?= $(shell git rev-parse --short HEAD)

version:
  @ echo '{"Version": "$(APP_VERSION)"}'

在上面的示例中,用您自己 AWS 账户生成的适当 URI 替换存储库 URI。

为了演示当您结合imagebuild属性时的标记行为,首先删除本章前面创建的 Docker 图像,如下所示:

> docker rmi 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
Untagged: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest
Untagged: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend@sha256:322c8b378dd90b3a1a6dc8553baf03b4eb13ebafcc926d9d87c010f08e0339fa
Deleted: sha256:2b2d8d17367c32993b0aa68f407e89bf4a3496a1da9aeb7c00a8e49f89bf5134
Deleted: sha256:523126379df325e1bcdccdf633aa10bc45e43bdb5ce4412aec282e98dbe076fb
Deleted: sha256:54521ab8917e466fbf9e12a5e15ac5e8715da5332f3655e8cc51f5ad3987a034
Deleted: sha256:03d95618180182e7ae08c16b4687a7d191f3f56d909b868db9e889f0653add46
Deleted: sha256:eb56d3747a17d5b7d738c879412e39ac2739403bbf992267385f86fce2f5ed0d
Deleted: sha256:9908bfa1f773905e0540d70e65d6a0991fa1f89a5729fa83e92c2a8b45f7bd29
Deleted: sha256:d9268f192cb01d0e05a1f78ad6c41bc702b11559d547c0865b4293908d99a311
Deleted: sha256:c6e4f60120cdf713253b24bba97a0c2a80d41a0126eb18f4ea5269034dbdc7e1
Deleted: sha256:0b780adf8501c8a0dbf33f49425385506885f9e8d4295f9bc63c3f895faed6d1

删除 Docker 图像

如果您现在运行docker-compose build release命令,一旦命令完成,Docker Compose 将构建一个新的图像,并标记为您的 ECR 存储库 URI:

> docker-compose build release WARNING: The APP_VERSION variable is not set. Defaulting to a blank string.
Building release
Step 1/25 : FROM alpine AS build
 ---> 3fd9065eaf02
Step 2/25 : LABEL application=todobackend
 ---> Using cache
 ---> f955808a07fd
...
...
Step 25/25 : USER app
 ---> Using cache
 ---> f507b981227f

Successfully built f507b981227f
Successfully tagged 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest
> docker images
REPOSITORY                                                               TAG                 IMAGE ID            CREATED             SIZE
385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend   latest              f507b981227f        4 days ago          99.4MB

使用 Docker Compose 构建带标签的图像

当您的图像构建并正确标记后,您现在可以执行docker-compose push命令,该命令可用于推送在 Docker Compose 文件中定义了buildimage属性的服务:

> docker-compose push release
Pushing release (385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest)...
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend]
9ae8d6169643: Layer already exists
cdbc5d8be7d1: Pushed
08a1fb32c580: Layer already exists
2e3946df4029: Pushed
3a29354c4bcc: Layer already exists
a031167f960b: Layer already exists
cd7100a72410: Layer already exists
latest: digest: sha256:a1b029d347a2fabd3f58d177dcbbcd88066dc54ccdc15adad46c12ceac450378 size: 1787

使用 Docker Compose 发布图像

在上面的示例中,与名为release的服务关联的图像被推送,因为这是您使用 Docker 图像 URI 配置的服务。

自动化发布工作流程

在之前的章节中,您学习了如何使用 Docker、Docker Compose 和 Make 自动化测试和构建 todobackend 应用程序的 Docker 图像。

现在,您可以增强此工作流程以执行以下附加操作:

  • 登录和注销 ECR

  • 发布到 ECR

为了实现这一点,您将在 todobackend 存储库的 Makefile 中创建新的任务。

自动化登录和注销

以下示例演示了添加名为loginlogout的两个新任务,这些任务将使用 Docker 客户端执行这些操作:

.PHONY: test release clean version login logout

export APP_VERSION ?= $(shell git rev-parse --short HEAD)

version:
  @ echo '{"Version": "$(APP_VERSION)"}'

login:
 $$(aws ecr get-login --no-include-email)

logout:
 docker logout https://385605022855.dkr.ecr.us-east-1.amazonaws.com test:
    docker-compose build --pull release
    docker-compose build
    docker-compose run test

release:
    docker-compose up --abort-on-container-exit migrate
    docker-compose run app python3 manage.py collectstatic --no-input
    docker-compose up --abort-on-container-exit acceptance
    @ echo App running at http://$$(docker-compose port app 8000 | sed s/0.0.0.0/localhost/g)

clean:
    docker-compose down -v
    docker images -q -f dangling=true -f label=application=todobackend | xargs -I ARGS docker rmi -f ARGS

登录和注销 ECR

请注意,login任务使用双美元符号($$),这是必需的,因为 Make 使用单美元符号来定义 Make 变量。当您指定双美元符号时,Make 将向 shell 传递单美元符号,这将确保执行 bash 命令替换。

在使用logout任务注销时,请注意您需要指定 Docker 注册表,否则 Docker 客户端会假定默认的公共 Docker Hub 注册表。

有了这些任务,您现在可以轻松地使用make logoutmake login命令注销和登录 ECR:

> make logout docker logout https://385605022855.dkr.ecr.us-east-1.amazonaws.com
Removing login credentials for 385605022855.dkr.ecr.us-east-1.amazonaws.com
 > make login
$(aws ecr get-login --no-include-email)
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded

运行 make logout 和 make login

自动化发布 Docker 图像

要自动化发布工作流,您可以在 Makefile 中添加一个名为publish的新任务,该任务简单地调用标记为releaseapp服务的docker-compose push命令:

.PHONY: test release clean login logout publish

export APP_VERSION ?= $(shell git rev-parse --short HEAD)

version:
  @ echo '{"Version": "$(APP_VERSION)"}'

...
...

release:
    docker-compose up --abort-on-container-exit migrate
    docker-compose run app python3 manage.py collectstatic --no-input
    docker-compose up --abort-on-container-exit acceptance
    @ echo App running at http://$$(docker-compose port app 8000 | sed s/0.0.0.0/localhost/g)

publish:
 docker-compose push release app
clean:
    docker-compose down -v
    docker images -q -f dangling=true -f label=application=todobackend | xargs -I ARGS docker rmi -f ARGS

自动发布到 ECR

有了这个配置,您的 Docker 镜像现在将被标记为提交哈希和最新标记,然后您只需运行make publish命令即可将其发布到 ECR。

现在让我们提交您的更改并运行完整的 Make 工作流来测试、构建和发布您的 Docker 镜像,如下例所示。请注意,一个带有提交哈希97e4abf标记的镜像被发布到了 ECR:

> git commit -a -m "Add publish tasks"
[master 97e4abf] Add publish tasks
 2 files changed, 12 insertions(+), 1 deletion(-)

> make login
$(aws ecr get-login --no-include-email)
Login Succeeded

> make test && make release
docker-compose build --pull release
Building release
...
...
todobackend_db_1 is up-to-date
Creating todobackend_app_1 ... done
App running at http://localhost:32774
$ make publish
docker-compose push release app
Pushing release (385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest)...
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend]
53ca7006d9e4: Layer already exists
ca208f4ebc53: Layer already exists
1702a4329d94: Layer already exists
e2aca0d7f367: Layer already exists
c3e0af9081a5: Layer already exists
20ae2e176794: Layer already exists
cd7100a72410: Layer already exists
latest: digest: sha256:d64e1771440208bde0cabe454f213d682a6ad31e38f14f9ad792fabc51008888 size: 1787
Pushing app (385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:97e4abf)...
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend]
53ca7006d9e4: Layer already exists
ca208f4ebc53: Layer already exists
1702a4329d94: Layer already exists
e2aca0d7f367: Layer already exists
c3e0af9081a5: Layer already exists
20ae2e176794: Layer already exists
cd7100a72410: Layer already exists
97e4abf: digest: sha256:d64e1771440208bde0cabe454f213d682a6ad31e38f14f9ad792fabc51008888 size: 1787

> make clean
docker-compose down -v
Stopping todobackend_app_1 ... done
Stopping todobackend_db_1 ... done
...
...

> make logout
docker logout https://385605022855.dkr.ecr.us-east-1.amazonaws.com
Removing login credentials for 385605022855.dkr.ecr.us-east-1.amazonaws.com

运行更新后的 Make 工作流

从 ECR 中拉取 Docker 镜像

现在您已经学会了如何将 Docker 镜像发布到 ECR,让我们专注于在各种场景下运行的 Docker 客户端如何从 ECR 拉取您的 Docker 镜像。回想一下本章开头对 ECR 的介绍,客户端访问 ECR 存在各种场景,我们现在将重点关注这些场景,以 ECS 容器实例作为您的 Docker 客户端:

  • 在与您的 ECR 存储库相同的账户中运行的 ECS 容器实例

  • 运行在不同账户中的 ECS 容器实例访问您的 ECR 存储库

  • 需要访问您的 ECR 存储库的 AWS 服务

来自相同账户的 ECS 容器实例对 ECR 的访问

当您的 ECS 容器实例在与您的 ECR 存储库相同的账户中运行时,推荐的方法是使用与运行为 ECS 容器实例的 EC2 实例应用的 IAM 实例角色相关联的 IAM 策略,以使在 ECS 容器实例内运行的 ECS 代理能够从 ECR 中拉取 Docker 镜像。您已经在上一章中看到了这种方法的实际操作,AWS 提供的 ECS 集群向导附加了一个名为AmazonEC2ContainerServiceforEC2Role的托管策略到集群中 ECS 容器实例的 IAM 实例角色,并注意到此策略中包含的以下 ECR 权限:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeregisterContainerInstance",
        "ecs:DiscoverPollEndpoint",
        "ecs:Poll",
        "ecs:RegisterContainerInstance",
        "ecs:StartTelemetrySession",
        "ecs:Submit*",
        "ecr:GetAuthorizationToken",
 "ecr:BatchCheckLayerAvailability",
 "ecr:GetDownloadUrlForLayer",
 "ecr:BatchGetImage",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

AmazonEC2ContainerServiceforEC2Role 策略

在上面的例子中,您可以看到授予了四个 ECR 权限,这些权限共同允许 ECS 代理登录到 ECR 并拉取 Docker 镜像:

  • ecr:GetAuthorizationToken:允许检索有效期为 12 小时的身份验证令牌,可用于使用 Docker CLI 登录到 ECR。

  • ecr:BatchCheckLayerAvailability: 检查给定存储库中多个镜像层的可用性。

  • ecr:GetDownloadUrlForLayer: 为 Docker 镜像中的给定层检索预签名的 S3 下载 URL。

  • ecr:BatchGetImage: 重新获取给定存储库中 Docker 镜像的详细信息。

这些权限足以登录到 ECR 并拉取镜像,但请注意前面示例中的Resource属性允许访问您帐户中的所有存储库。

根据您组织的安全要求,对所有存储库的广泛访问可能是可以接受的,也可能不可以接受 - 如果不可以接受,则需要创建自定义 IAM 策略,限制对特定存储库的访问,就像这里演示的那样:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage"
      ],
      "Resource": [
        "arn:aws:ecr:us-east-1:385605022855:repository/docker-in-aws/todobackend"
      ]
    }
  ]
}

授予特定存储库的 ECR 登录和拉取权限

在前面的示例中,请注意ecr:GetAuthorizationToken权限仍然适用于所有资源,因为您没有登录到特定的 ECR 存储库,而是登录到给定区域中您帐户的 ECR 注册表。然而,用于拉取 Docker 镜像的其他权限可以应用于单个存储库,您可以看到这些权限仅允许对您的 ECR 存储库的 ARN 进行操作。

请注意,如果您还想要在前面的示例中授予对 ECR 存储库的推送访问权限,则需要额外的 ECR 权限:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",         
        "ecr:InitiateLayerUpload",         
        "ecr:UploadLayerPart",         
        "ecr:CompleteLayerUpload"
      ],
      "Resource": [
        "arn:aws:ecr:us-east-1:385605022855:repository/docker-in-aws/todobackend"
      ]
    }
  ]
}

授予特定存储库的 ECR 推送权限

来自不同帐户的 ECS 容器实例访问 ECR

在较大的组织中,资源和用户通常分布在多个帐户中,一个常见的模式是拥有一个中央构建帐户,应用程序构件(如 Docker 镜像)在其中进行集中存储。

下图说明了这种情况,您可能有几个帐户运行 ECS 容器实例,这些实例需要拉取存储在您中央存储库中的 Docker 镜像:

需要访问中央 ECR 存储库的多个帐户

当您需要授予其他帐户对您的 ECR 存储库的访问权限时,需要执行两项配置任务:

  1. 在托管存储库的帐户中配置 ECR 资源策略,允许您定义适用于单个 ECR 存储库(这是资源)的策略,并定义可以访问存储库(例如,AWS 帐户)以及他们可以执行的操作(例如,登录,推送和/或拉取映像)。定义可以访问给定存储库的能力是允许通过资源策略启用和控制跨帐户访问的关键。例如,在前面的图中,存储库配置为允许来自帐户333333444444555555666666的访问。

  2. 远程帐户中的管理员需要以 IAM 策略的形式分配权限,以从您的 ECR 存储库中提取映像。这是一种委托访问的形式,即托管 ECR 存储库的帐户信任远程帐户的访问,只要通过 IAM 策略明确授予了访问权限。例如,在前面的图中,ECS 容器实例分配了一个 IAM 策略,允许它们访问帐户111111222222中的 myorg/app-a 存储库。

使用 AWS 控制台配置 ECR 资源策略

您可以通过打开适当的 ECR 存储库,在权限选项卡中选择添加来配置 ECS 控制台中的 ECR 资源策略,并单击添加以添加新的权限集:

配置 ECR 资源策略

在上图中,请注意您可以通过主体设置将 AWS 帐户 ID 配置为主体,然后通过选择仅拉取操作选项轻松允许拉取访问。通过此配置,您允许与远程帐户关联的任何实体从此存储库中拉取 Docker 映像。

配置 ECR 资源策略

请注意,如果您尝试保存前图和上图中显示的配置,您将收到错误,因为我使用了无效的帐户。假设您使用了有效的帐户 ID 并保存了策略,则将为配置生成以下策略文档:

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "RemoteAccountAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::*<remote-account-id>*:root"
            },
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability"
            ]
        }
    ]
}

示例 ECR 存储库策略文档

使用 AWS CLI 配置 ECR 资源策略

您可以使用aws ecr set-repository-policy命令通过 AWS CLI 配置 ECR 资源策略,如下所示:

> aws ecr set-repository-policy --repository-name docker-in-aws/todobackend --policy-text '{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "RemoteAccountAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::*<remote-account-id>*:root"
            },
            "Action": [
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability"
            ]
        }
    ]
}'

通过 AWS CLI 配置 ECR 资源策略

如前面的示例所示,您必须使用--repository-name标志指定存储库名称,并使用--policy-text标志配置存储库策略为 JSON 格式的文档。

使用 AWS CloudFormation 配置 ECR 资源策略

在使用 AWS CloudFormation 定义 ECR 存储库时,您可以配置AWS::ECR::Repository资源的RepositoryPolicyText属性,以定义 ECR 资源策略:

AWSTemplateFormatVersion: "2010-09-09"

Description: ECR Repositories

Resources:
  TodobackendRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: docker-in-aws/todobackend
      RepositoryPolicyText:
 Version: "2008-10-17"
 Statement:
 - Sid: RemoteAccountAccess
 Effect: Allow
 Principal:
 AWS: arn:aws:iam::*<remote-account-id>*:root
 Action:
 - ecr:GetDownloadUrlForLayer
 - ecr:BatchGetImage
 - ecr:BatchCheckLayerAvailability

使用 AWS CloudFormation 配置 ECR 资源策略

在前面的示例中,策略文本以 YAML 格式表达了您在之前示例中配置的 JSON 策略,并且您可以通过运行aws cloudformation deploy命令将更改部署到您的堆栈。

配置远程帐户中的 IAM 策略

通过控制台、CLI 或 CloudFormation 配置好 ECR 资源策略后,您可以继续在您的 ECR 资源策略中指定的远程帐户中创建 IAM 策略。这些策略的配置方式与您在本地帐户中配置 IAM 策略的方式完全相同,如果需要,您可以引用远程 ECR 存储库的 ARN,以便仅授予对该存储库的访问权限。

AWS 服务访问 ECR

我们将讨论的最后一个场景是 AWS 服务访问您的 ECR 镜像的能力。一个例子是 AWS CodeBuild 服务,它使用基于容器的构建代理执行自动化持续集成任务。CodeBuild 允许您定义自己的自定义构建代理,一个常见的做法是将这些构建代理的镜像发布到 ECR 中。这意味着 AWS CodeBuild 服务现在需要访问 ECR,您可以使用 ECR 资源策略来实现这一点。

以下示例扩展了前面的示例,将 AWS CodeBuild 服务添加到资源策略中:

AWSTemplateFormatVersion: "2010-09-09"

Description: ECR Repositories

Resources:
  TodobackendRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: docker-in-aws/todobackend
      RepositoryPolicyText:
        Version: "2008-10-17"
        Statement:
          - Sid: RemoteAccountAccess
            Effect: Allow
            Principal:
              AWS: arn:aws:iam::*<remote-account-id>*:root              Service: codebuild.amazonaws.com
            Action:
              - ecr:GetDownloadUrlForLayer
              - ecr:BatchGetImage
              - ecr:BatchCheckLayerAvailability

配置 AWS 服务访问 ECR 存储库

在前面的示例中,请注意您可以在Principal属性中使用Service属性来标识将应用该策略语句的 AWS 服务。在后面的章节中,当您创建自己的自定义 CodeBuild 镜像并发布到 ECR 时,您将看到这一示例的实际操作。

配置生命周期策略

如果您在本章中跟随操作,您将已经多次将 todobackend 图像发布到您的 ECR 存储库,并且很可能已经在您的 ECR 存储库中创建了所谓的孤立图像。在早期的章节中,我们讨论了在本地 Docker 引擎中创建的孤立图像,并将其定义为其标记已被新图像取代的图像,从而使旧图像无名,并因此“孤立”。

如果您浏览到您的 ECR 存储库并在 ECS 控制台中选择图像选项卡,您可能会注意到您有一些不再具有标记的图像,这是因为您推送了几个带有latest标记的图像,这些图像已经取代了现在孤立的图像:

孤立的 ECR 图像

在前面的图中,请注意您的 ECR 中的存储使用量现在已经增加了三倍,即使您只有一个当前的latest图像,这意味着您可能也要支付三倍的存储成本。当然,您可以手动删除这些图像,但这很容易出错,而且通常会成为一个被遗忘和忽视的任务。

幸运的是,ECR 支持一种称为生命周期策略的功能,允许您定义包含在策略中的一组规则,管理您的 Docker 图像的生命周期。您应该始终应用于您创建的每个存储库的生命周期策略的标准用例是定期删除孤立的图像,因此现在让我们看看如何创建和应用这样的策略。

使用 AWS 控制台配置生命周期策略

在配置生命周期策略时,因为这些策略可能实际删除您的 Docker 图像,最好始终使用 AWS 控制台来测试您的策略,因为 ECS 控制台包括一个功能,允许您模拟如果应用生命周期策略会发生什么。

使用 AWS 控制台配置生命周期策略,选择 ECR 存储库中的生命周期规则的干运行选项卡,然后单击添加按钮以创建新的干运行规则。这允许您在不实际删除 ECR 存储库中的任何图像的情况下测试生命周期策略规则。一旦您满意您的规则安全地行为并符合预期,您可以将它们转换为实际的生命周期策略,这些策略将应用于您的存储库:

ECR 干运行规则

您现在可以在“添加规则”屏幕中使用以下参数定义规则:

  • 规则优先级:确定在策略中定义多个规则时的规则评估顺序。

  • 规则描述:规则的可读描述。

  • 图像状态:定义规则适用于哪种类型的图像。请注意,您只能有一个指定未标记图像的规则。

  • 匹配条件:定义规则应何时应用的条件。例如,您可以配置条件以匹配自上次推送到 ECR 存储库以来超过七天的未标记图像。

  • 规则操作:定义应对匹配规则的图像执行的操作。目前,仅支持过期操作,将删除匹配的图像。

单击保存按钮后,新规则将添加到生命周期规则的模拟运行选项卡。如果您现在单击保存并执行模拟运行按钮,将显示符合规则条件的任何图像,其中应包括先前显示的孤立图像。

现在,取决于您是否有未标记的图像以及它们与您最后推送到存储库的时间相比有多久,您可能会或可能不会看到与您的模拟运行规则匹配的图像。无论实际结果如何,关键在于确保与规则匹配的任何图像都是您期望的,并且您确信模拟运行规则不会意外删除您期望发布和可用的有效图像。

如果您对模拟运行规则满意,接下来可以单击应用为生命周期策略按钮,首先会显示对新规则的确认对话框,一旦应用,如果您导航到生命周期策略选项卡,您应该会看到您的生命周期策略:

ECR 生命周期策略

要确认您的生命周期策略是否起作用,您可以单击任何策略规则,然后从“操作”下拉菜单中选择查看历史记录,这将显示 ECR 执行的与策略规则相关的任何操作。

使用 AWS CLI 配置生命周期策略

AWS CLI 支持与通过 AWS 控制台配置 ECR 生命周期策略类似的工作流程,概述如下:

  • aws ecr start-lifecycle-policy-preview --repository-name <*name*> --lifecycle-policy-text <*json*>:对存储库启动生命周期策略的模拟运行

  • aws ecr get-lifecycle-policy-preview --repository-name <*name*>:获取试运行的状态

  • aws ecr put-lifecycle-policy --repository-name <*name*> --lifecycle-policy-text <*json*>:将生命周期策略应用于存储库

  • aws ecr get-lifecycle-policy --repository-name <*name*>:显示应用于存储库的当前生命周期策略

  • aws ecr delete-lifecycle-policy --repository-name <*name*>:删除应用于存储库的当前生命周期策略

在使用 CLI 时,您需要以 JSON 格式指定生命周期策略,您可以通过单击前面截图中的“查看 JSON”操作来查看示例。

使用 AWS CloudFormation 配置生命周期策略

在使用 AWS CloudFormation 定义 ECR 存储库时,您可以配置之前创建的AWS::ECR::Repository资源的LifecyclePolicy属性,以定义 ECR 生命周期策略:

AWSTemplateFormatVersion: "2010-09-09"

Description: ECR Repositories

Resources:
  TodobackendRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: docker-in-aws/todobackend
      LifecyclePolicy:
 LifecyclePolicyText: |
 {
 "rules": [
 {
 "rulePriority": 10,
 "description": "Untagged images",
 "selection": {
 "tagStatus": "untagged",
 "countType": "sinceImagePushed",
 "countUnit": "days",
 "countNumber": 7
 },
 "action": {
 "type": "expire"
 }
 }
 ]
 }

使用 AWS CloudFormation 配置 ECR 生命周期策略

前面示例中的策略文本表示您在之前示例中配置的 JSON 策略作为 JSON 字符串 - 请注意使用管道(|)YAML 运算符,它允许您输入多行文本以提高可读性。

有了这个配置,您可以通过运行aws cloudformation deploy命令将更改应用到您的堆栈。

总结

在本章中,您学习了如何创建和管理 ECR 存储库,您可以使用它来安全和私密地存储您的 Docker 镜像。创建了第一个 ECR 存储库后,您学会了如何使用 AWS CLI 和 Docker 客户端进行 ECR 身份验证,然后成功地给 ECR 打上标签并发布了您的 Docker 镜像。

发布了您的 Docker 镜像后,您还了解了 Docker 客户端可能需要访问存储库的各种情况,包括来自与您的 ECR 存储库相同账户的 ECS 容器实例访问、来自与您的 ECR 存储库不同账户的 ECS 容器实例访问(即跨账户访问),以及最后授予对 AWS 服务(如 CodeBuild)的访问权限。您创建了 ECR 资源策略,这在配置跨账户访问和授予对 AWS 服务的访问权限时是必需的,并且您了解到,尽管在定义远程账户为受信任的中央账户中创建了 ECR 资源策略,但您仍然需要在每个远程账户中创建明确授予对中央账户存储库访问权限的 IAM 策略。

最后,您创建了 ECR 生命周期策略规则,允许您自动定期删除未标记(孤立)的 Docker 镜像,从而有助于减少存储成本。在下一章中,您将学习如何使用一种流行的开源工具 Packer 构建和发布自己的自定义 ECS 容器实例 Amazon Machine Images(AMIs)。

问题

  1. 您执行哪个命令以获取 ECR 的身份验证令牌?

  2. 真/假:ECR 允许您公开发布和分发 Docker 镜像

  3. 如果您注意到存储库中有很多未标记的图像,您应该配置哪个 ECR 功能?

  4. 真/假:ECR 以压缩格式存储 Docker 镜像

  5. 真/假:配置从相同帐户的 ECS 容器实例访问 ECR 需要 ECR 资源策略

  6. 真/假:配置从远程帐户的 ECS 容器实例访问 ECR 需要 ECR 资源策略

  7. 真/假:配置从 AWS CodeBuild 访问 ECR 需要 ECR 资源策略

  8. 真/假:配置从相同帐户的 ECS 容器实例访问 ECR 需要 IAM 策略

  9. 真/假:配置从远程帐户的 ECS 容器实例访问 ECR 需要 IAM 策略

进一步阅读

您可以查看以下链接以获取有关本章涵盖的主题的更多信息:

第六章:构建自定义 ECS 容器实例

在早期的章节中,您学习了如何使用 Amazon ECS-Optimized Amazon Machine Image (AMI)来在几个简单的步骤中创建 ECS 容器实例并将它们加入到 ECS 集群中。尽管 ECS-Optimized AMI 非常适合快速启动和运行,但您可能希望为生产环境的 ECS 容器实例添加其他功能,例如添加日志代理或包括对 HTTP 代理的支持,以便将 ECS 集群放置在私有子网中。

在本章中,您将学习如何使用 ECS-Optimized AMI 作为基础机器映像来构建自定义 ECS 容器实例,并使用一种名为 Packer 的流行开源工具应用自定义。您将扩展基础映像以包括 AWS CloudWatch 日志代理,该代理可使用 CloudWatch 日志服务从您的 ECS 容器实例进行集中日志记录,并安装一组有用的 CloudFormation 辅助脚本,称为 cfn-bootstrap,它将允许您在实例创建时运行强大的初始化脚本,并提供与 CloudFormation 的强大集成功能。

最后,您将创建一个首次运行脚本,该脚本将允许您使您的实例适应目标环境的特定要求,而无需为每个应用程序和环境构建新的 AMI。该脚本将使您有条件地启用 HTTP 代理支持,从而可以在更安全的私有子网中安装您的 ECS 容器实例,并且还将包括一个健康检查,该检查将等待您的 ECS 容器实例已注册到其配置的 ECS 集群,然后向 CloudFormation 发出信号,表明您的实例已成功初始化。

将涵盖以下主题:

  • 设计自定义 AMI

  • 使用 Packer 构建自定义 AMI

  • 创建自定义存储配置

  • 安装 CloudFormation 辅助脚本

  • 安装 CloudWatch 日志代理

  • 创建首次运行脚本

  • 测试您的自定义 ECS 容器实例

技术要求

以下列出了完成本章所需的技术要求:

  • Packer 1.0 或更高(将提供安装 Packer 的说明)

  • 对 AWS 账户的管理员访问权限

  • 根据第三章的说明配置本地 AWS 配置文件

  • GNU Make 版本 3.82 或更高(请注意,macOS 默认不包含此版本)

  • AWS CLI 1.15.71 或更高

此 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch6.

查看以下视频以查看代码的实际操作:

bit.ly/2LzoxaO

设计自定义 Amazon Machine Image

在学习如何构建自定义 Amazon Machine Image 之前,了解为什么您想要或需要构建自己的自定义镜像是很重要的。

这取决于您的用例或组织要求,但通常有许多原因您可能想要构建自定义镜像:

  • 自定义存储配置:默认的 ECS 优化 AMI 附带一个 30GB 的卷,其中包括 8GB 用于操作系统分区,以及一个 22GB 的卷用于存储 Docker 镜像和容器文件系统。我通常建议更改配置的一个方面是,默认情况下,不使用分层文件系统的 Docker 卷存储在 8GB 的操作系统分区上。这种方法通常不适合生产用例,而应该为存储 Docker 卷挂载一个专用卷。

  • 安装额外的软件包和工具:与 Docker 的极简主义理念一致,ECS 优化 AMI 附带了一个最小安装的 Amazon Linux,只包括运行 Docker Engine 和支持的 ECS 代理所需的核心组件。对于实际用例,您通常至少需要添加 CloudWatch 日志代理,它支持在系统级别(例如操作系统、Docker Engine 和 ECS 代理日志)记录到 AWS CloudWatch 日志服务。另一个重要的工具集是 cfn-bootstrap 工具,它提供一组 CloudFormation 辅助脚本,您可以在 CloudFormation 模板中定义自定义的配置操作,并允许您的 EC2 实例在配置和实例初始化完成后向 CloudFormation 发出信号。

  • 添加首次运行脚本:在部署 ECS 容器实例到 AWS 时,您可能会在各种用例中使用它们,这些用例根据应用程序的性质需要不同的配置。例如,一个常见的安全最佳实践是将 ECS 容器实例部署到没有默认路由附加的私有子网中。这意味着您的 ECS 容器实例必须配置 HTTP 代理,以便与 AWS 服务(如 ECS 和 CloudWatch 日志)或 ECS 容器实例可能依赖的任何其他互联网服务进行通信。然而,在某些情况下,使用 HTTP 代理可能不可行(例如,考虑运行为您的环境提供 HTTP 代理服务的 ECS 容器实例),而不是构建单独的机器映像(一个启用了 HTTP 代理和一个未启用 HTTP 代理),您可以创建一次性运行的配置脚本,根据目标用例有条件地启用/禁用所需的配置,例如 HTTP 代理设置。

当然,还有许多其他用例可能会驱使您构建自己的自定义映像,但在本章中,我们将专注于这里定义的用例示例,这将为您提供坚实的基础和理解如何应用您可能想要使用的任何其他自定义的额外定制。

使用 Packer 构建自定义 AMI

现在您了解了构建自定义 ECS 容器实例映像的原因,让我们介绍一个名为 Packer 的工具,它允许您为各种平台构建机器映像,包括 AWS。

Packer是 HashiCorp 创建的开源工具,您可以在www.packer.io/了解更多信息。Packer 可以为各种目标平台构建机器映像,但在本章中,我们将只关注构建 Amazon Machine Images。

安装 Packer

在您开始使用 Packer 之前,您需要在本地环境中安装它。Packer 支持 Linux、macOS 和 Windows 平台,要安装 Packer 到您的目标平台,请按照位于www.packer.io/intro/getting-started/install.html的说明进行操作。

请注意,Packer 在操作系统和第三方软件包管理工具中得到了广泛支持,例如,在 mac OS 上,您可以通过运行brew install packer来使用 Brew 软件包管理器安装 Packer。

创建 Packer 模板

安装了 Packer 后,您现在可以开始创建一个 Packer 模板,该模板将定义如何构建您的自定义机器镜像。不过,在这之前,我建议为您的 Packer 模板创建一个单独的存储库,该存储库应该始终放置在版本控制下,就像应用程序源代码和其他基础设施一样。

在本章中,我假设您已经创建了一个名为packer-ecs的存储库,并且您可以参考github.com/docker-in-aws/docker-in-awsch6文件夹,该文件夹提供了一个基于本章内容的示例存储库。

Packer 模板结构

Packer 模板是提供了一个声明性描述的 JSON 文档,告诉 Packer 如何构建机器镜像。

Packer 模板围绕着四个常见的顶级参数进行组织,如下例所示,并在这里描述:

  • variables:提供构建的输入变量的对象。

  • builders:Packer 构建器的列表,定义了目标机器镜像平台。在本章中,您将针对一个名为EBS-backed AMI builder的构建器进行定位,这是用于创建自定义 Amazon Machine Images 的最简单和最流行的构建器。构建器负责确保正确的图像格式,并以适合部署到目标机器平台的格式发布最终图像。

  • provisioners:Packer 配置器的列表或数组,作为图像构建过程的一部分执行各种配置任务。最简单的配置器包括文件和 shell 配置器,它们将文件复制到图像中并执行 shell 任务,例如安装软件包。

  • post-processors:Packer 后处理器的列表或数组,一旦机器镜像构建和发布完成,将执行后处理任务。

{
    "variables": {},
    "builders": [],
    "provisioners": [],
    "post-processors": []
}

Packer 模板结构

配置构建器

让我们开始配置我们的 Packer 模板,首先在 packer-ecs 存储库的根目录下创建一个名为packer.json的文件,然后定义构建器部分,如下例所示:

{
  "variables": {},
  "builders": [
 {
 "type": "amazon-ebs",
 "access_key": "{{user `aws_access_key_id`}}",
 "secret_key": "{{user `aws_secret_access_key`}}",
 "token": "{{user `aws_session_token`}}",
 "region": "us-east-1",
 "source_ami": "ami-5e414e24",
 "instance_type": "t2.micro",
 "ssh_username": "ec2-user",
 "associate_public_ip_address": "true",
 "ami_name": "docker-in-aws-ecs {{timestamp}}",
 "tags": {
 "Name": "Docker in AWS ECS Base Image 2017.09.h",
 "SourceAMI": "{{ .SourceAMI }}",
 "DockerVersion": "17.09.1-ce",
 "ECSAgentVersion": "1.17.0-2"
 }
 }
 ],
  "provisioners": [],
  "post-processors": []
}

定义一个 EBS-backed AMI 构建器

在上述示例中,将表示我们构建器的单个对象添加到构建器数组中。type参数将构建器定义为基于 EBS 的 AMI 构建器,并且随后的设置特定于此类型的构建器:

  • access_key:定义用于验证对 AWS 的访问权限的 AWS 访问密钥 ID,用于构建和发布 AMI。

  • secret_key:定义用于验证对 AWS 的访问权限的 AWS 秘密访问密钥,用于构建和发布 AMI。

  • token:可选地定义用于验证临时会话凭据的 AWS 会话令牌。

  • region:目标 AWS 地区。

  • source_ami:要构建的源 AMI。在此示例中,指定了撰写时 us-east-1 地区最新的 ECS-Optimized AMI 的源 AMI,您可以从docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html获取最新列表。

  • instance_type:用于构建 AMI 的实例类型。

  • ssh_username:Packer 在尝试连接到 Packer 构建过程中创建的临时 EC2 实例时应使用的 SSH 用户名。对于基于 Amazon Linux 的 AMI(例如 ECS-Optimized AMI),必须将其指定为ec2-user用户。

  • associate_public_ip_address:当设置为 true 时,将公共 IP 地址与实例关联。如果您在互联网上使用 Packer 并且没有对 Packer 构建过程中创建的临时 EC2 实例的私有网络访问权限,则需要此选项。

  • ami_name:将要创建的 AMI 的名称。此名称必须是唯一的,确保唯一性的常见方法是使用{{timestamp}} Go 模板函数,Packer 将自动将其替换为当前时间戳。

  • tags:要添加到创建的 AMI 中的标记列表。这允许您附加元数据,例如图像的源 AMI、ECS 代理版本、Docker 版本或任何其他您可能发现有用的信息。请注意,您可以引用一个名为SourceAMI的特殊模板变量,该变量由 Amazon EBS 构建器添加,并基于source_ami变量的值。

需要注意的一点是,与其在模板中硬编码您的 AWS 凭据,不如引用一个名为{{user }}的 Go 模板函数,这将注入在我们即将配置的顶级变量参数中定义的用户变量。

Packer 模板使用 Go 的模板语言进行处理,您可以在golang.org/pkg/text/template/上了解更多信息。Go 模板允许您定义自己的模板函数,Packer 包含了一些有用的函数,这些函数在www.packer.io/docs/templates/engine.html中定义。模板函数通过模板表达式调用,表达式采用句柄样式格式:{{<function> <parameters>}}

配置变量

变量用于在构建时将用户特定或环境特定的设置注入到模板中,这对于使您的机器映像模板更通用并避免在模板中硬编码凭据非常有用。

在前面的示例中,您在定义 AWS 凭据设置时引用了用户变量,这些变量必须在 Packer 模板的变量部分中定义,就像在前面的示例中演示的那样:

{
  "variables": {
 "aws_access_key_id": "{{env `AWS_ACCESS_KEY_ID`}}",
 "aws_secret_access_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
 "aws_session_token": "{{env `AWS_SESSION_TOKEN`}}",
 "timezone": "US/Eastern"
 },
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access_key_id`}}",
      "secret_key": "{{user `aws_secret_access_key`}}",
      "token": "{{user `aws_session_token`}}",
      "region": "us-east-1",
      "source_ami": "ami-5e414e24",
      "instance_type": "t2.micro",
      "ssh_username": "ec2-user",
      "associate_public_ip_address": "true",
      "ami_name": "docker-in-aws-ecs {{timestamp}}",
      "tags": {
        "Name": "Docker in AWS ECS Base Image 2017.09.h",
        "SourceAMI": "{{ .SourceAMI }}",
        "DockerVersion": "17.09.1-ce",
        "ECSAgentVersion": "1.17.0-2"
      }
    }
  ],
  "provisioners": [],
  "post-processors": []
}

定义变量

在上面的示例中,请注意您在构建器部分的用户函数中定义了 AWS 凭据设置的每个变量。例如,构建器部分将access_key设置定义为{{user aws_access_key_id}},它依次引用了变量部分中定义的aws_access_key_id变量。

每个变量依次引用env模板函数,该函数查找传递给此函数的环境变量的值。这意味着您可以控制每个变量的值如下:

  • aws_access_key_id:使用AWS_ACCESS_KEY_ID环境变量进行配置

  • aws_secret_access_key:使用AWS_SECRET_ACCESS_KEY环境变量进行配置

  • aws_session_token:使用AWS_SESSION_TOKEN环境变量进行配置

  • timezone:使用默认值US/Eastern进行配置。在运行packer build命令时,您可以通过设置-var '<variable>=<value>'标志(例如,-var 'timezone=US/Pacific')来覆盖默认变量。

请注意,我们还没有在 Packer 模板中定义timezone变量,因为您将在本章后面使用这个变量。

配置配置程序

配置程序是 Packer 模板的核心,形成在自定义和构建机器镜像时执行的各种内部配置操作。

Packer 支持许多不同类型的配置程序,包括流行的配置管理工具,如 Ansible 和 Puppet,您可以在www.packer.io/docs/provisioners/index.html上阅读更多关于不同类型的配置程序的信息。

对于我们的机器镜像,我们只会使用两种最基本和基本的配置程序:

作为配置程序的介绍,让我们定义一个简单的 shell 配置程序,更新已安装的操作系统软件包,如下例所示:

{
  "variables": {
    "aws_access_key_id": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_access_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "aws_session_token": "{{env `AWS_SESSION_TOKEN`}}",
    "timezone": "US/Eastern"
  },
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access_key_id`}}",
      "secret_key": "{{user `aws_secret_access_key`}}",
      "token": "{{user `aws_session_token`}}",
      "region": "us-east-1",
      "source_ami": "ami-5e414e24",
      "instance_type": "t2.micro",
      "ssh_username": "ec2-user",
      "associate_public_ip_address": "true",
      "ami_name": "docker-in-aws-ecs {{timestamp}}",
      "tags": {
        "Name": "Docker in AWS ECS Base Image 2017.09.h",
        "SourceAMI": "ami-5e414e24",
        "DockerVersion": "17.09.1-ce",
        "ECSAgentVersion": "1.17.0-2"
      }
    }
  ],
  "provisioners": [
 {
 "type": "shell",
 "inline": [
 "sudo yum -y -x docker\\* -x ecs\\* update"
 ] 
 }
 ],
  "post-processors": []
}

定义内联 shell 配置程序

在上面的例子中定义的配置程序使用inline参数来定义在配置阶段将执行的命令列表。在这种情况下,您正在运行yum update命令,这是 Amazon Linux 系统上的默认软件包管理器,并更新所有安装的操作系统软件包。为了确保您使用基本 ECS-Optimized AMI 中包含的 Docker 和 ECS 代理软件包的推荐和经过测试的版本,您使用-x标志来排除以dockerecs开头的软件包。

在上面的例子中,yum 命令将被执行为sudo yum -y -x docker\* -x ecs\* update。因为反斜杠字符(\)在 JSON 中被用作转义字符,在上面的例子中,双反斜杠(例如,\\*)用于生成一个字面上的反斜杠。

最后,请注意,您必须使用sudo命令运行所有 shell 配置命令,因为 Packer 正在以构建器部分中定义的ec2_user用户身份配置 EC2 实例。

配置后处理器

我们将介绍 Packer 模板的最终结构组件是后处理器,它允许您在机器镜像被配置和构建后执行操作。

后处理器可以用于各种不同的用例,超出了本书的范围,但我喜欢使用的一个简单的后处理器示例是清单后处理器,它输出一个列出 Packer 生成的所有构件的 JSON 文件。当您创建首先构建 Packer 镜像,然后需要测试和部署镜像的持续交付流水线时,此输出非常有用。

在这种情况下,清单文件可以作为 Packer 构建的输出构件,描述与新机器映像相关的区域和 AMI 标识符,并且可以作为一个示例用作 CloudFormation 模板的输入,该模板将您的新机器映像部署到测试环境中。

以下示例演示了如何向您的 Packer 模板添加清单后处理器:

{
  "variables": {
    "aws_access_key_id": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_access_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "aws_session_token": "{{env `AWS_SESSION_TOKEN`}}",
    "timezone": "US/Eastern"
  },
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access_key_id`}}",
      "secret_key": "{{user `aws_secret_access_key`}}",
      "token": "{{user `aws_session_token`}}",
      "region": "us-east-1",
      "source_ami": "ami-5e414e24",
      "instance_type": "t2.micro",
      "ssh_username": "ec2-user",
      "associate_public_ip_address": "true",
      "ami_name": "docker-in-aws-ecs {{timestamp}}",
      "tags": {
        "Name": "Docker in AWS ECS Base Image 2017.09.h",
        "SourceAMI": "ami-5e414e24",
        "DockerVersion": "17.09.1-ce",
        "ECSAgentVersion": "1.17.0-2"
      }
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update"
      ] 
    }
  ],
  "post-processors": [
 {
 "type": "manifest",
 "output": "manifest.json",
 "strip_path": true
 }
 ]
}

定义清单后处理器

正如您在前面的示例中所看到的,清单后处理器非常简单 - output参数指定清单将被写入本地的文件名,而strip_path参数会剥离任何构建构件的本地文件系统路径信息。

构建机器映像

在这一点上,您已经创建了一个简单的 Packer 镜像,它在定制方面并不太多,但仍然是一个完整的模板,可以立即构建。

在实际运行构建之前,您需要确保本地环境已正确配置以成功完成构建。回想一下,在上一个示例中,您为模板定义了引用环境变量的变量,这些环境变量配置了您的 AWS 凭据,这里的一个常见方法是将本地 AWS 访问密钥 ID 和秘密访问密钥设置为环境变量。

然而,在我们的用例中,我假设您正在使用早期章节介绍的最佳实践方法,因此您的模板配置为使用临时会话凭据,这可以通过aws_session_token输入变量来证明,需要在运行 Packer 构建之前动态生成并注入到您的本地环境中。

生成动态会话凭据

要生成临时会话凭据,假设您已经使用AWS_PROFILE环境变量配置了适当的配置文件,您可以运行aws sts assume-role命令来生成凭据:

> export AWS_PROFILE=docker-in-aws
> aws sts assume-role --role-arn=$(aws configure get role_arn) --role-session-name=$(aws configure get role_session_name)
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga: ****
{
    "Credentials": {
        "AccessKeyId": "ASIAIIEUKCAR3NMIYM5Q",
        "SecretAccessKey": "JY7HmPMf/tPDXsgQXHt5zFZObgrQJRvNz7kb4KDM",
        "SessionToken": "FQoDYXdzEM7//////////wEaDP0PBiSeZvJ9GjTP5yLwAVjkJ9ZCMbSY5w1EClNDK2lS3nkhRg34/9xVgf9RmKiZnYVywrI9/tpMP8LaU/xH6nQvCsZaVTxGXNFyPz1BcsEGM6Z2ebIFX5rArT9FWu3v7WVs3QQvXeDTasgdvq71eFs2+qX7zbjK0YHXaWuu7GA/LGtNj4i+yi6EZ3OIq3hnz3+QY2dXL7O1pieMLjfZRf98KHucUhiokaq61cXSo+RJa3yuixaJMSxJVD1myx/XNritkawUfI8Xwp6g6KWYQAzDYz3MIWbA5LyX9Q0jk3yXTRAQOjLwvL8ZK/InJCDoPBFWFJwrz+Wxgep+I8iYoijOhqTUBQ==",
        "Expiration": "2018-02-18T05:38:38Z"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAJASB32NFHLLQHZ54S:justin.menga",
        "Arn": "arn:aws:sts::385605022855:assumed-role/admin/justin.menga"
    }
}
> export AWS_ACCESS_KEY_ID="ASIAIIEUKCAR3NMIYM5Q"
> export AWS_SECRET_ACCESS_KEY="JY7HmPMf/tPDXsgQXHt5zFZObgrQJRvNz7kb4KDM"
> export AWS_SESSION_TOKEN="FQoDYXdzEM7//////////wEaDP0PBiSeZvJ9GjTP5yLwAVjkJ9ZCMbSY5w1EClNDK2lS3nkhRg34/9xVgf9RmKiZnYVywrI9/tpMP8LaU/xH6nQvCsZaVTxGXNFyPz1BcsEGM6Z2ebIFX5rArT9FWu3v7WVs3QQvXeDTasgdvq71eFs2+qX7zbjK0YHXaWuu7GA/LGtNj4i+yi6EZ3OIq3hnz3+QY2dXL7O1pieMLjfZRf98KHucUhiokaq61cXSo+RJa3yuixaJMSxJVD1myx/XNritkawUfI8Xwp6g6KWYQAzDYz3MIWbA5LyX9Q0jk3yXTRAQOjLwvL8ZK/InJCDoPBFWFJwrz+Wxgep+I8iYoijOhqTUBQ=="

生成临时会话凭据

在上面的示例中,请注意您可以使用 bash 替换动态获取role_arnrole_session_name参数,使用aws configure get <parameter>命令从 AWS CLI 配置文件中获取,这些参数在生成临时会话凭据时是必需的输入。

上面示例的输出包括一个包含以下值的凭据对象,这些值与 Packer 模板中引用的环境变量相对应:

  • AccessKeyId:此值作为AWS_ACCESS_KEY_ID环境变量导出

  • SecretAccessKey:此值作为AWS_SECRET_ACCESS_KEY环境变量导出

  • SessionToken:此值作为AWS_SESSION_TOKEN环境变量导出

自动生成动态会话凭据

虽然您可以使用上面示例中演示的方法根据需要生成临时会话凭据,但这种方法会很快变得繁琐。有许多方法可以自动将生成的临时会话凭据注入到您的环境中,但考虑到本书使用 Make 作为自动化工具,以下示例演示了如何使用一个相当简单的 Makefile 来实现这一点:

.PHONY: build
.ONESHELL:

build:
  @ $(if $(AWS_PROFILE),$(call assume_role))
  packer build packer.json

# Dynamically assumes role and injects credentials into environment
define assume_role
  export AWS_DEFAULT_REGION=$$(aws configure get region)
  eval $$(aws sts assume-role --role-arn=$$(aws configure get role_arn) \
    --role-session-name=$$(aws configure get role_session_name) \
    --query "Credentials.[ \
        [join('=',['export AWS_ACCESS_KEY_ID',AccessKeyId])], \
        [join('=',['export AWS_SECRET_ACCESS_KEY',SecretAccessKey])], \
        [join('=',['export AWS_SESSION_TOKEN',SessionToken])] \
      ]" \
    --output text)
endef

使用 Make 自动生成临时会话凭据确保您的 Makefile 中的所有缩进都是使用制表符而不是空格。

在上面的示例中,请注意引入了一个名为.ONESHELL的指令。此指令配置 Make 在给定的 Make 配方中为所有定义的命令生成单个 shell,这意味着 bash 变量赋值和环境设置可以在多行中重复使用。

如果当前环境配置了AWS_PROFILEbuild任务有条件地调用名为assume_role的函数,这种方法很有用,因为这意味着如果您在配置为以不同方式获取 AWS 凭据的构建代理上运行此 Makefile,临时会话凭据的动态生成将不会发生。

在 Makefile 中,如果命令以@符号为前缀,则执行的命令将不会输出到 stdout,而只会显示命令的输出。

assume_role函数使用高级的 JMESPath 查询表达式(如--query标志所指定的)来生成一组export语句,这些语句引用了在前面示例中运行的命令的Credentials字典输出上的各种属性,并使用 JMESPath join 函数将值分配给相关的环境变量。这些语句被包裹在一个命令替换中,使用eval命令来执行每个输出的export语句。如果你不太理解这个查询,不要担心,但要认识到 AWS CLI 确实包含一个强大的查询语法,可以创建一些相当复杂的一行命令。

在上面的示例中,注意你可以使用反引号(`) 作为bash命令替换的替代语法。换句话说,$(command)``command` ``都表示将执行命令并返回输出的命令替换。

构建镜像

现在我们有了自动生成临时会话凭据的机制,假设您的packer.json文件和 Makefile 位于您的 packer-ecs 存储库的根目录中,让我们通过运行make build来测试构建您的 Packer 镜像:

> export AWS_PROFILE=docker-in-aws
> make build
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga: ******
packer build packer.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: docker-in-aws-ecs 1518934269
    amazon-ebs: Found Image ID: ami-5e414e24
==> amazon-ebs: Creating temporary keypair: packer_5a8918fd-018d-964f-4ab3-58bff320ead5
==> amazon-ebs: Creating temporary security group for this instance: packer_5a891904-2c84-aca1-d368-8309f215597d
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-04c150456ac0748aa
==> amazon-ebs: Waiting for instance (i-04c150456ac0748aa) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Provisioning with shell script: /var/folders/s4/1mblw7cd29s8xc74vr3jdmfr0000gn/T/packer-shell190211980
    amazon-ebs: Loaded plugins: priorities, update-motd, upgrade-helper
    amazon-ebs: Resolving Dependencies
    amazon-ebs: --> Running transaction check
    amazon-ebs: ---> Package elfutils-libelf.x86_64 0:0.163-3.18.amzn1 will be updated
    amazon-ebs: ---> Package elfutils-libelf.x86_64 0:0.168-8.19.amzn1 will be an update
    amazon-ebs: ---> Package python27.x86_64 0:2.7.12-2.121.amzn1 will be updated
    amazon-ebs: ---> Package python27.x86_64 0:2.7.13-2.122.amzn1 will be an update
    amazon-ebs: ---> Package python27-libs.x86_64 0:2.7.12-2.121.amzn1 will be updated
    amazon-ebs: ---> Package python27-libs.x86_64 0:2.7.13-2.122.amzn1 will be an update
    amazon-ebs: --> Finished Dependency Resolution
    amazon-ebs:
    amazon-ebs: Dependencies Resolved
    amazon-ebs:
    amazon-ebs: ================================================================================
    amazon-ebs: Package Arch Version Repository Size
    amazon-ebs: ================================================================================
    amazon-ebs: Updating:
    amazon-ebs: elfutils-libelf x86_64 0.168-8.19.amzn1 amzn-updates 313 k
    amazon-ebs: python27 x86_64 2.7.13-2.122.amzn1 amzn-updates 103 k
    amazon-ebs: python27-libs x86_64 2.7.13-2.122.amzn1 amzn-updates 6.8 M
    amazon-ebs:
    amazon-ebs: Transaction Summary
    amazon-ebs: ================================================================================
    amazon-ebs: Upgrade 3 Packages
    amazon-ebs:
    amazon-ebs: Total download size: 7.2 M
    amazon-ebs: Downloading packages:
    amazon-ebs: --------------------------------------------------------------------------------
    amazon-ebs: Total 5.3 MB/s | 7.2 MB 00:01
    amazon-ebs: Running transaction check
    amazon-ebs: Running transaction test
    amazon-ebs: Transaction test succeeded
    amazon-ebs: Running transaction
    amazon-ebs: Updating : python27-2.7.13-2.122.amzn1.x86_64 1/6
    amazon-ebs: Updating : python27-libs-2.7.13-2.122.amzn1.x86_64 2/6
    amazon-ebs: Updating : elfutils-libelf-0.168-8.19.amzn1.x86_64 3/6
    amazon-ebs: Cleanup : python27-2.7.12-2.121.amzn1.x86_64 4/6
    amazon-ebs: Cleanup : python27-libs-2.7.12-2.121.amzn1.x86_64 5/6
    amazon-ebs: Cleanup : elfutils-libelf-0.163-3.18.amzn1.x86_64 6/6
    amazon-ebs: Verifying : python27-libs-2.7.13-2.122.amzn1.x86_64 1/6
    amazon-ebs: Verifying : elfutils-libelf-0.168-8.19.amzn1.x86_64 2/6
    amazon-ebs: Verifying : python27-2.7.13-2.122.amzn1.x86_64 3/6
    amazon-ebs: Verifying : python27-libs-2.7.12-2.121.amzn1.x86_64 4/6
    amazon-ebs: Verifying : elfutils-libelf-0.163-3.18.amzn1.x86_64 5/6
    amazon-ebs: Verifying : python27-2.7.12-2.121.amzn1.x86_64 6/6
    amazon-ebs:
    amazon-ebs: Updated:
    amazon-ebs: elfutils-libelf.x86_64 0:0.168-8.19.amzn1
    amazon-ebs: python27.x86_64 0:2.7.13-2.122.amzn1
    amazon-ebs: python27-libs.x86_64 0:2.7.13-2.122.amzn1
    amazon-ebs:
    amazon-ebs: Complete!
==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: docker-in-aws-ecs 1518934269
    amazon-ebs: AMI: ami-57415b2d
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Adding tags to AMI (ami-57415b2d)...
==> amazon-ebs: Tagging snapshot: snap-0bc767fd982333bf8
==> amazon-ebs: Tagging snapshot: snap-0104c1a352695c1e9
==> amazon-ebs: Creating AMI tags
    amazon-ebs: Adding tag: "SourceAMI": "ami-5e414e24"
    amazon-ebs: Adding tag: "DockerVersion": "17.09.1-ce"
    amazon-ebs: Adding tag: "ECSAgentVersion": "1.17.0-2"
    amazon-ebs: Adding tag: "Name": "Docker in AWS ECS Base Image 2017.09.h"
==> amazon-ebs: Creating snapshot tags
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
==> amazon-ebs: Running post-processor: manifest
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-57415b2d

运行 Packer 构建

回顾前面的示例和上一个示例的输出,在build任务中注意到构建 Packer 镜像的命令只是packer build <template-file>,在这种情况下是packer build packer.json

如果您回顾上一个示例的输出,您会看到以下步骤由 Packer 执行:

  • Packer 首先验证源 AMI,然后生成临时 SSH 密钥对和安全组,以便能够访问临时 EC2 实例。

  • Packer 从源 AMI 启动临时 EC2 实例,然后等待能够建立 SSH 访问。

  • Packer 根据模板的 provisioners 部分中定义的配置执行配置操作。在这种情况下,您可以看到 yum update 命令的输出,这是我们当前的单个配置操作。

  • 完成后,Packer 停止实例并创建 EBS 卷实例的快照,从而生成具有适当名称和 ID 的 AMI。

  • 创建完成后,Packer 终止实例,删除临时 SSH 密钥对和安全组,并输出新的 AMI ID。

回顾前面的示例,您向模板添加了一个 manifest 后处理器,并且您应该在存储库的根目录中找到一个名为manifest.json的文件,通常您不会想要提交到您的 packer-ecs 存储库中:


> cat manifest.json
{
  "builds": [
    {
      "name": "amazon-ebs",
      "builder_type": "amazon-ebs",
      "build_time": 1518934504,
      "files": null,
 "artifact_id": "us-east-1:ami-57415b2d",
      "packer_run_uuid": "db07ccb3-4100-1cc8-f0be-354b9f9b021d"
    }
  ],
  "last_run_uuid": "db07ccb3-4100-1cc8-f0be-354b9f9b021d"
}
> echo manifest.json >> .gitignore

查看 Packer 构建清单

使用 Packer 构建自定义 ECS 容器实例镜像

在前一节中,您已经建立了一个用于使用 Packer 构建自定义 AMI 的基本模板,并且继续构建和发布了您的第一个自定义 AMI。在这一点上,您尚未执行任何特定于 ECS 容器实例配置的自定义操作,因此本节将重点介绍如何改进您的 Packer 模板以包括这些自定义操作。

您现在将了解以下自定义内容:

  • 定义自定义存储配置

  • 安装额外的软件包并配置操作系统设置

  • 配置清理脚本

  • 创建第一次运行脚本

有了这些自定义设置,我们将通过构建最终的自定义 ECS 容器实例 AMI 并启动实例来完成本章,并验证各种自定义设置。

定义自定义存储配置

AWS ECS 优化的 AMI 包括一个使用 30GB EBS 卷的默认存储配置,分区如下:

  • /dev/xvda:作为根文件系统挂载的 8GB 卷,用作操作系统分区。

  • dev/xvdcz:一个 22GB 卷,配置为逻辑卷管理(LVM)设备,用于 Docker 镜像和元数据存储。

ECS 优化的 AMI 使用 devicemapper 存储驱动程序进行 Docker 镜像和元数据存储,您可以在docs.docker.com/v17.09/engine/userguide/storagedriver/device-mapper-driver/上了解更多信息。

对于大多数用例,这种存储配置应该足够了,但是有一些情况下您可能希望修改默认配置:

  • 你需要更多的 Docker 镜像和元数据存储:这可以通过简单地配置您的 ECS 容器实例以使用更大的卷大小来轻松解决。默认存储配置将始终保留 8GB 用于操作系统和根文件系统,其余存储用于 Docker 镜像和元数据存储。

  • 你需要支持具有大容量存储需求的 Docker 卷:默认情况下,ECS 优化的 AMI 将 Docker 卷存储在 /var/lib/docker/volumes,这是根文件系统中 8GB /dev/xvda 分区的一部分。如果您有更大的卷需求,这可能会导致您的操作系统分区很快变满,所以在这种情况下,您应该将卷存储分离到单独的 EBS 卷中。

现在让我们看看您如何修改您的 Packer 模板,以为 Docker 卷存储添加一个新的专用卷,并确保在实例创建时正确挂载此卷。

添加 EBS 卷

要向您的自定义 AMI 添加 EBS 卷,您可以在 Amazon EBS 构建器中配置 launch_block_device_mappings 参数:


{
  "variables": {...},
  "builders": [
    {
      "type": "amazon-ebs",
      "access_key": "{{user `aws_access_key_id`}}",
      "secret_key": "{{user `aws_secret_access_key`}}",
      "token": "{{user `aws_session_token`}}",
      "region": "us-east-1",
      "source_ami": "ami-5e414e24",
      "instance_type": "t2.micro",
      "ssh_username": "ec2-user",
      "associate_public_ip_address": "true",
      "ami_name": "docker-in-aws-ecs {{timestamp}}",
      "launch_block_device_mappings": [
 {
 "device_name": "/dev/xvdcy",
 "volume_size": 20,
 "volume_type": "gp2",
 "delete_on_termination": true
 }
 ],
      "tags": {
        "Name": "Docker in AWS ECS Base Image 2017.09.h",
        "SourceAMI": "ami-5e414e24",
        "DockerVersion": "17.09.1-ce",
        "ECSAgentVersion": "1.17.0-2"
      }
    }
  ],
  "provisioners": [...],
  "post-processors": [...]
}

添加一个启动块设备映射

在上述示例中,为了简洁起见,我已经截断了 Packer 模板的其他部分,您可以看到我们添加了一个名为 /dev/xvdcy 的 20GB 单一卷,该卷配置为在实例终止时销毁。请注意,volume_type 参数设置为 gp2,这是通常在 AWS 中提供最佳整体价格/性能的通用 SSD 存储类型。

格式化和挂载卷

有了上述示例的配置,我们接下来需要格式化和挂载新卷。因为我们使用了 launch_block_device_mappings 参数(而不是 ami_block_device_mappings 参数),所以块设备实际上是在构建镜像时附加的(后者仅在创建镜像时附加),我们可以在构建时执行所有格式化和挂载配置设置。

要执行此配置,我们将向您的 Packer 模板添加一个 shell provisioner,该 provisioner 引用名为 scripts/storage.sh 的文件:


{
  "variables": {...},
  "builders": [...],
  "provisioners": [
    {
 "type": "shell",
 "script": "scripts/storage.sh"
 },
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update"
      ] 
    }
  ],
  "post-processors": [...]
}

添加一个用于配置存储的 shell provisioner

引用的脚本表示为相对于 Packer 模板的路径,因此您现在需要创建此脚本:


> mkdir -p scripts
> touch scripts/storage.sh
> tree
.
├── Makefile
├── manifest.json
├── packer.json
└── scripts
 └── storage.sh

1 directory, 4 files

创建一个 scripts 文件夹

通过使用以下示例中所示的脚本文件,你可以定义各种 shell 配置操作:


#!/usr/bin/env bash
set -e

echo "### Configuring Docker Volume Storage ###"
sudo mkdir -p /data
sudo mkfs.ext4 -L docker /dev/xvdcy
echo -e "LABEL=docker\t/data\t\text4\tdefaults,noatime\t0\t0" | sudo tee -a /etc/fstab
sudo mount -a

存储配置脚本

如你在前面的示例中所见,这个脚本是一个普通的 bash 脚本,重要的是要为所有的 Packer shell 脚本设置错误标志 (set -e),这样可以确保脚本在任何命令失败时都会以错误代码退出。

首先创建一个名为 /data 的文件夹,用于存储 Docker 卷,然后使用 .ext4 文件系统格式化之前示例中附加的 /dev/xvdcy 设备,并附加一个名为 docker 的标签,这使得挂载操作更加简单。下一个 echo 命令将添加一个条目到 /etc/fstab 文件中,该文件定义了在启动时将应用的所有文件系统挂载,注意你必须通过 sudo tee -a /etc/fstabecho 命令传递给 sudo,以使用正确的 sudo 权限将 echo 的输出追加到 /etc/fstab 文件中。

最后,通过运行 mount -a 命令,你可以自动挂载 /etc/fstab 文件中的新条目,尽管在构建镜像时不是必需的,但这是一种简单的方法来验证该挂载是否正确配置(如果不正确,此命令将失败并导致构建失败)。

安装额外的软件包和配置系统设置

接下来,你将执行其他自定义操作,例如安装额外的软件包和配置系统设置。

安装额外的软件包

我们需要安装一些额外的软件包到我们的自定义 ECS 容器实例中,包括以下几个:

  • CloudFormation 帮助脚本:当你使用 CloudFormation 部署基础设施时,AWS 提供了一组称为 cfn-bootstrap 的 CloudFormation 帮助脚本,它们与 CloudFormation 一起工作,以获取初始化元数据,允许你在实例创建时执行自定义初始化任务,并在实例成功完成初始化后向 CloudFormation 发出信号。我们将在后面的章节中探讨这种方法的好处,但现在你需要确保这些帮助脚本存在于你的自定义 ECS 容器实例镜像中。

  • CloudWatch 日志代理:AWS CloudWatch 日志服务提供了从各种来源(包括 EC2 实例、ECS 容器和其他 AWS 服务)集中存储日志的功能。要将 ECS 容器实例(EC2 实例)的日志发送到 CloudWatch 日志,你必须在本地安装 CloudWatch 日志代理,并将其用于转发各种系统日志,包括操作系统、Docker 和 ECS 代理的日志。

  • jq 实用程序jq 实用程序(stedolan.github.io/jq/manual/)对于解析 JSON 输出很方便,在本章后面当您定义一个简单的健康检查来验证 ECS 容器实例是否已加入到配置的 ECS 集群时,您将需要此实用程序。

安装这些额外的软件包非常简单,可以通过修改您之前创建的内联 shell provisioner 来实现:


{
  "variables": {...},
  "builders": [...],
  "provisioners": [
    {
      "type": "shell",
      "script": "scripts/storage.sh"
    },
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update",
 "sudo yum -y install aws-cfn-bootstrap awslogs jq"
      ] 
    }
  ],
  "post-processors": [...]
}

安装其他操作系统软件包

如您在上述示例中所见,每个所需的软件包都可以通过 yum 软件包管理器轻松安装。

配置系统设置

您需要对自定义 ECS 容器实例进行一些小的系统设置:

  • 配置时区设置

  • 修改默认的 cloud-init 行为

配置时区设置

之前,您定义了一个名为 timezone 的变量,到目前为止您还没有在模板中引用过。您可以使用此变量来配置自定义 ECS 容器实例镜像的时区。

要做到这一点,您首先需要在您的 Packer 模板中添加一个新的 shell provisioner:


{
  "variables": {...},
  "builders": [...],
  "provisioners": [
    {
      "type": "shell",
      "script": "scripts/storage.sh"
    },
    {
 "type": "shell",
 "script": "scripts/time.sh",
 "environment_vars": [
 "TIMEZONE={{user `timezone`}}"
 ]
 },
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update",
        "sudo yum -y install aws-cfn-bootstrap awslogs jq"
      ] 
    }
  ],
  "post-processors": [...]
}

添加一个 provisioner 来配置时间设置

在上述示例中,我们引用了一个名为 scripts/time.sh 的脚本,您将很快创建它,但请注意,我们还包含了一个名为 environment_vars 的参数,它允许您将您的 Packer 变量(例如此示例中的 timezone)作为环境变量注入到您的 shell provisioning 脚本中。

下面的示例展示了新的 Packer 模板配置任务中引用的必需 scripts/time.sh 脚本:


#!/usr/bin/env bash
set -e

# Configure host to use timezone
# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/set-time.html
echo "### Setting timezone to end-inline-katex-->TIMEZONE ###"
sudo tee /etc/sysconfig/clock << EOF > /dev/null
ZONE="c194a9eg<!-- begin-inline-katexTIMEZONE"
UTC=true
EOF

sudo ln -sf /usr/share/zoneinfo/"end-inline-katex-->TIMEZONE" /etc/localtime

# Use AWS NTP Sync service
echo "server 169.254.169.123 prefer iburst" | sudo tee -a /etc/ntp.conf

# Enable NTP
sudo chkconfig ntpd on

时间设置配置脚本

在上面的示例中,首先配置了配置时间的 AWS 推荐设置,通过配置 /etc/sysconfig/clock 文件使用配置的 TIMEZONE 环境变量,创建了符号链接 /etc/localtime,最后确保 ntpd 服务配置为使用AWS NTP 同步服务并在实例启动时启动。

AWS NTP 同步服务是一个免费的 AWS 服务,提供了一个位于本地地址 169.254.169.123 的 NTP 服务器端点,确保您的 EC2 实例可以获得准确的时间,而无需穿越网络或互联网。

修改默认的 cloud-init 行为

cloud-init 是一组标准的工具,用于执行云映像和相关实例的初始化。cloud-init 最流行的功能是 user-data 机制,它是在实例创建时运行您自己的自定义初始化命令的一种简单方法。

云初始化还用于 ECS 优化的 AMI,以在实例创建时执行自动安全补丁,尽管这听起来像一个有用的功能,但它可能会导致问题,特别是在您的实例位于私有子网并且需要使用 HTTP 代理与互联网通信的环境中。

云初始化安全机制的问题在于,虽然可以通过设置代理环境变量来配置它与 HTTP 代理一起工作,但它在执行 userdata 之前被调用,导致鸡和蛋的情况,即如果你使用代理,你别无选择,只能禁用自动安全补丁。

要禁用此机制,您首先需要在 Packer 模板中配置一个新的外壳供应商:


{
  "variables": {...},
  "builders": [...],
  "provisioners": [
    {
      "type": "shell",
      "script": "scripts/storage.sh"
    },
    {
      "type": "shell",
      "script": "scripts/time.sh",
      "environment_vars": [
        "TIMEZONE={{user `timezone`}}"
      ]
    },
    {
 "type": "shell",
 "script": "scripts/cloudinit.sh"
 },
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update",
        "sudo yum -y install aws-cfn-bootstrap awslogs jq"
      ] 
    }
  ],
  "post-processors": [...]
}

添加一个供应商以配置云初始化设置引用的scripts/cloudinit.sh脚本现在可以按以下方式创建:


#!/usr/bin/env bash
set -e

# Disable cloud-init repo updates or upgrades
sudo sed -i -e '/^repo_update: /{h;s/: .*/: false/};c194a9eg<!-- begin-inline-katex{x;/^end-inline-katex-->/{s//repo_update: false/;H};x}' /etc/cloud/cloud.cfg
sudo sed -i -e '/^repo_upgrade: /{h;s/: .*/: none/};c194a9eg<!-- begin-inline-katex{x;/^end-inline-katex-->/{s//repo_upgrade: none/;H};x}' /etc/cloud/cloud.cfg
复制 ErrorOK!

禁用云初始化的安全更新

在下面的示例中,看起来相当可怕的sed表达式将在/etc/cloud/cloud.cfg云初始化配置文件中添加或替换以repo_updaterepo_upgrade开头的行,并确保它们分别设置为falsenone

配置清理脚本

到目前为止,我们已经执行了所有必需的安装和配置外壳供应任务。我们将创建一个最终的外壳供应商,一个清理脚本,它将删除构建自定义镜像的实例运行时创建的任何日志文件,并确保机器镜像处于准备启动的状态。

您首先需要向 Packer 模板添加一个引用scripts/cleanup.sh脚本的外壳供应商:


{
  "variables": {...},
  "builders": [...],
  "provisioners": [
    {
      "type": "shell",
      "script": "scripts/storage.sh"
    },
    {
      "type": "shell",
      "script": "scripts/time.sh",
      "environment_vars": [
        "TIMEZONE={{user `timezone`}}"
      ]
    },
    {
      "type": "shell",
      "script": "scripts/cloudinit.sh"
    },
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update",
        "sudo yum -y install aws-cfn-bootstrap awslogs jq"
      ] 
    },
 { "type": "shell",
 "script": "scripts/cleanup.sh"
 }
  ],
  "post-processors": [...]
}

添加一个清理镜像的供应商

定义了 Packer 模板中的供应商之后,您需要创建清理脚本,如下所示:


#!/usr/bin/env bash
echo "### Performing final clean-up tasks ###"
sudo stop ecs
sudo docker system prune -f -a
sudo service docker stop
sudo chkconfig docker off
sudo rm -rf /var/log/docker /var/log/ecs/*

清理脚本

在下面的示例中,请注意您不执行set -e命令,因为这是一个清理脚本,如果出现错误,您不太担心,也不希望如果服务已经停止,构建失败。首先停止 ECS 代理,使用docker system prune命令清除可能存在的任何 ECS 容器状态,然后停止 Docker 服务,并使用chkconfig命令停用它。原因是在实例创建时,我们总是会调用一个首次运行脚本,该脚本将执行实例的初始配置,并要求停止 Docker 服务。当然,这意味着一旦首次运行脚本完成其初始配置,它将负责确保 Docker 服务已启动并且已启用以在启动时启动。

最后,清理脚本将删除在自定义机器镜像构建过程中实例运行的短时间内可能创建的任何 Docker 和 ECS 代理日志文件。

创建首次运行脚本

我们将对自定义 ECS 容器实例镜像应用的最终一组自定义是创建一个首次运行脚本,该脚本将负责在实例创建时执行 ECS 容器实例的运行时配置,执行以下任务:

  • 配置 ECS 集群成员身份

  • 配置 HTTP 代理支持

  • 配置 CloudWatch 日志代理

  • 启动所需服务

  • 执行健康检查

要提供首次运行脚本,您需要在您的 Packer 模板中定义一个文件提供者任务,如下所示:


{
  "variables": {...},
  "builders": [...],
  "provisioners": [
    {
      "type": "shell",
      "script": "scripts/storage.sh"
    },
    {
      "type": "shell",
      "script": "scripts/time.sh",
      "environment_vars": [
        "TIMEZONE={{user `timezone`}}"
      ]
    },
    {
      "type": "shell",
      "script": "scripts/cloudinit.sh"
    },
    {
      "type": "shell",
      "inline": [
        "sudo yum -y -x docker\\* -x ecs\\* update",
        "sudo yum -y install aws-cfn-bootstrap awslogs jq"
      ] 
    },
    {
      "type": "shell",
      "script": "scripts/cleanup.sh"
    },
    {
 "type": "file",
 "source": "files/firstrun.sh",
 "destination": "/home/ec2-user/firstrun.sh"
 }
  ],
  "post-processors": [...]
}

添加文件提供者

注意,配置了提供者类型为 file,并指定了需要位于 files/firstrun.sh 中的本地源文件。 destination 参数定义了首次运行脚本将位于 AMI 中的位置。请注意,文件提供者任务将文件复制为 ec2-user 用户,因此它对可以复制该脚本的位置具有有限的权限。

配置 ECS 集群成员身份

您现在可以在您的 Packer 模板引用的 files/firstrun.sh 位置创建首次运行脚本。在开始配置此文件之前,重要的是要记住,首次运行脚本被设计为在从您的自定义机器映像创建的实例的初始引导时运行,因此在配置将执行的各种命令时,必须考虑到这一点。

我们首先将配置 ECS 代理加入 ECS 集群,以加入 ECS 容器实例打算加入的 ECS 集群,如下面的示例所示:


#!/usr/bin/env bash
set -e

# Configure ECS Agent
echo "ECS_CLUSTER=c194a9eg<!-- begin-inline-katex{ECS_CLUSTER}" > /etc/ecs/ecs.config

配置 ECS 集群成员身份

回到第五章,使用 ECR 发布 Docker 镜像,您看到了 ECS 集群向导使用了这种相同的方法配置 ECS 容器实例,尽管有一个区别是脚本期望在环境中配置了一个名为 ECS_CLUSTER 的环境变量,如 ${ECS_CLUSTER} 表达式所指定的那样。与硬编码 ECS 集群名称不同,这会使首次运行脚本非常不灵活,这里的想法是,应用于给定实例的配置定义了具有正确集群名称的 ECS_CLUSTER 环境变量,这意味着该脚本是可重用的,并且可以配置为任何给定的 ECS 集群。

配置 HTTP 代理支持

一个常见的安全最佳实践是将您的 ECS 容器实例放置在私有子网中,这意味着它们位于没有默认路由到互联网的子网中。这种方法使得攻击者更难以破坏您的系统,即使他们这样做了,也提供了一种限制他们可以向互联网传输的信息的方法。

根据您的应用程序性质,您通常需要您的 ECS 容器实例能够连接到互联网,使用 HTTP 代理提供了一种有效的机制,以控制方式提供具有第 7 层应用层检查功能的访问。

无论您的应用程序的性质如何,都重要的是要了解,ECS 容器实例需要互联网连接,用于以下目的:

  • ECS 代理控制平面和管理平面与 ECS 的通信

  • Docker 引擎与 ECR 和其他存储库的通信,以下载 Docker 镜像

  • CloudWatch 日志代理与 CloudWatch 日志服务的通信

  • CloudFormation 辅助脚本与 CloudFormation 服务的通信

尽管配置完整的端到端代理解决方案超出了本书的范围,但了解如何自定义 ECS 容器实例以使用 HTTP 代理是有用的,如下例所示:


#!/usr/bin/env bash
set -e

# Configure ECS Agent
echo "ECS_CLUSTER=c194a9eg<!-- begin-inline-katex{ECS_CLUSTER}" > /etc/ecs/ecs.config

# Set HTTP Proxy URL if provided
if [ -n end-inline-katex-->PROXY_URL ]
then
 echo export HTTPS_PROXY=c194a9eg<!-- begin-inline-katexPROXY_URL >> /etc/sysconfig/docker
 echo HTTPS_PROXY=end-inline-katex-->PROXY_URL >> /etc/ecs/ecs.config
 echo NO_PROXY=169.254.169.254,169.254.170.2,/var/run/docker.sock >> /etc/ecs/ecs.config
 echo HTTP_PROXY=c194a9eg<!-- begin-inline-katexPROXY_URL >> /etc/awslogs/proxy.conf
 echo HTTPS_PROXY=end-inline-katex-->PROXY_URL >> /etc/awslogs/proxy.conf
 echo NO_PROXY=169.254.169.254 >> /etc/awslogs/proxy.conf
fi

配置 HTTP 代理支持

在上面的示例中,脚本检查名为PROXY_URL的非空环境变量是否存在,如果存在,则继续为 ECS 容器实例的各个组件配置代理设置:

  • Docker 引擎:通过/etc/sysconfig/docker配置

  • ECS 代理:通过/etc/ecs/ecs.config配置

  • CloudWatch 日志代理:通过/etc/awslogs/proxy.conf配置

请注意,在某些情况下,您需要配置NO_PROXY设置,该设置禁用以下 IP 地址的代理通信:

  • 169.254.169.254:这是一个特殊的本地地址,用于与 EC2 元数据服务通信,以获取实例元数据,如 EC2 实例角色凭证。

  • 169.254.170.2:这是一个特殊的本地地址,用于获取 ECS 任务凭证。

配置 CloudWatch 日志代理

您将在首次运行脚本中执行的下一个配置任务是配置 CloudWatch 日志代理。在 ECS 容器实例上,CloudWatch 日志代理负责收集系统日志,例如操作系统、Docker 和 ECS 代理日志。

请注意,此代理不需要为您的 Docker 容器实现 CloudWatch 日志支持 - 这已经在 Docker 引擎中通过awslogs日志驱动程序实现了。

配置 CloudWatch 日志代理需要执行以下配置任务:

下面的示例演示了所需的配置:


#!/usr/bin/env bash
set -e

# Configure ECS Agent
echo "ECS_CLUSTER=c194a9eg<!-- begin-inline-katex{ECS_CLUSTER}" > /etc/ecs/ecs.config

# Set HTTP Proxy URL if provided
if [ -n end-inline-katex-->PROXY_URL ]
then
  echo export HTTPS_PROXY=c194a9eg<!-- begin-inline-katexPROXY_URL >> /etc/sysconfig/docker
  echo HTTPS_PROXY=end-inline-katex-->PROXY_URL >> /etc/ecs/ecs.config
  echo NO_PROXY=169.254.169.254,169.254.170.2,/var/run/docker.sock >> /etc/ecs/ecs.config
  echo HTTP_PROXY=c194a9eg<!-- begin-inline-katexPROXY_URL >> /etc/awslogs/proxy.conf
  echo HTTPS_PROXY=end-inline-katex-->PROXY_URL >> /etc/awslogs/proxy.conf
  echo NO_PROXY=169.254.169.254 >> /etc/awslogs/proxy.conf
fi

# Write AWS Logs region
sudo tee /etc/awslogs/awscli.conf << EOF > /dev/null
[plugins]
cwlogs = cwlogs
[default]
region = c194a9eg<!-- begin-inline-katex{AWS_DEFAULT_REGION}
EOF

# Write AWS Logs config
sudo tee /etc/awslogs/awslogs.conf << EOF > /dev/null
[general]
state_file = /var/lib/awslogs/agent-state 

[/var/log/dmesg]
file = /var/log/dmesg
log_group_name = /end-inline-katex-->{STACK_NAME}/ec2/c194a9eg<!-- begin-inline-katex{AUTOSCALING_GROUP}/var/log/dmesg
log_stream_name = {instance_id} 
[/var/log/messages]
file = /var/log/messages
log_group_name = /end-inline-katex-->{STACK_NAME}/ec2/c194a9eg<!-- begin-inline-katex{AUTOSCALING_GROUP}/var/log/messages
log_stream_name = {instance_id}
datetime_format = %b %d %H:%M:%S 
[/var/log/docker]
file = /var/log/docker
log_group_name = /end-inline-katex-->{STACK_NAME}/ec2/c194a9eg<!-- begin-inline-katex{AUTOSCALING_GROUP}/var/log/docker
log_stream_name = {instance_id}
datetime_format = %Y-%m-%dT%H:%M:%S.%f 
[/var/log/ecs/ecs-init.log]
file = /var/log/ecs/ecs-init.log*
log_group_name = /end-inline-katex-->{STACK_NAME}/ec2/c194a9eg<!-- begin-inline-katex{AUTOSCALING_GROUP}/var/log/ecs/ecs-init
log_stream_name = {instance_id}
datetime_format = %Y-%m-%dT%H:%M:%SZ
time_zone = UTC 
[/var/log/ecs/ecs-agent.log]
file = /var/log/ecs/ecs-agent.log*
log_group_name = /end-inline-katex-->{STACK_NAME}/ec2/c194a9eg<!-- begin-inline-katex{AUTOSCALING_GROUP}/var/log/ecs/ecs-agent
log_stream_name = {instance_id}
datetime_format = %Y-%m-%dT%H:%M:%SZ
time_zone = UTC

[/var/log/ecs/audit.log]
file = /var/log/ecs/audit.log*
log_group_name = /end-inline-katex-->{STACK_NAME}/ec2/c194a9eg<!-- begin-inline-katex{AUTOSCALING_GROUP}/var/log/ecs/audit.log
log_stream_name = {instance_id}
datetime_format = %Y-%m-%dT%H:%M:%SZ
time_zone = UTC
EOF

配置 CloudWatch 日志代理

您可以看到第一次运行脚本中包含对每个定义的日志组的log_group_name参数中环境变量的引用,这有助于确保在您的 AWS 账户中具有唯一的日志组命名:

  • STACK_NAME:CloudFormation 堆栈的名称

  • AUTOSCALING_GROUP:自动缩放组的名称

再次强调,这些环境变量必须在实例创建时注入到第一次运行的脚本中,请记住这一点,因为在未来的章节中,我们将学习如何执行此操作。

在前面的示例中需要注意的另一点是每个log_stream_name参数的值 - 这设置为一个称为{instance_id}的特殊变量,CloudWatch 日志代理将自动配置为实例的 EC2 实例 ID。

结果是,对于每种类型的日志,您将获得几个日志组,这些日志组的范围限定为特定的 CloudFormation 堆栈和 EC2 自动缩放组的上下文,并且在每个日志组中,将为每个 ECS 容器实例创建一个日志流,如下图所示:

图ECS 容器实例的 CloudWatch 日志组配置

启动所需服务

在前面的示例中,您添加了一个清理脚本作为镜像构建过程的一部分,该脚本禁用了 Docker 引擎服务在启动时的启动。这种方法允许您在启动 Docker 引擎之前执行所需的初始化任务,在第一次运行脚本的这一点上,我们准备好启动 Docker 引擎和其他重要的系统服务:


#!/usr/bin/env bash
set -e

# Configure ECS Agent
echo "ECS_CLUSTER=end-inline-katex-->{ECS_CLUSTER}" > /etc/ecs/ecs.config

# Set HTTP Proxy URL if provided
...
...

# Write AWS Logs region
...
...

# Write AWS Logs config
...
...

# Start services
sudo service awslogs start
sudo chkconfig docker on
sudo service docker start
sudo start ecs

启动服务

在前面的示例中,请注意,出于简洁起见,我省略了第一次运行脚本的早期部分。请注意,您首先启动了 awslogs 服务,这确保了 CloudWatch 日志代理将捕获所有 Docker 引擎日志,然后继续启用 Docker 以在启动时启动,启动 Docker,最后启动 ECS 代理。

执行所需的健康检查

在第一次运行脚本中我们需要执行的最终任务是健康检查,以确保 ECS 容器实例已初始化并成功注册到配置的 ECS 集群。鉴于 ECS 代理只能在 Docker 引擎可用时运行,并且必须将 ECS 代理注册到 ECS 集群中以部署您的应用程序,因此这是对您的 ECS 容器实例的合理健康检查。

在上一章中回顾,当您检查 ECS 容器实例的内部时,ECS 代理公开了一个本地 HTTP 端点,可以查询当前 ECS 代理状态。您可以使用此端点创建一个非常简单的健康检查,如下所示:


#!/usr/bin/env bash
set -e

# Configure ECS Agent
echo "ECS_CLUSTER=c194a9eg<!-- begin-inline-katex{ECS_CLUSTER}" > /etc/ecs/ecs.config

# Set HTTP Proxy URL if provided
...
...

# Write AWS Logs region
...
...

# Write AWS Logs config
...
...

# Start services
...
...

# Health check
# Loop until ECS agent has registered to ECS cluster
echo "Checking ECS agent is joined to end-inline-katex-->{ECS_CLUSTER}"
until [[ "c194a9eg<!-- begin-inline-katex(curl --fail --silent http://localhost:51678/v1/metadata | jq '.Cluster // empty' -r -e)" == end-inline-katex-->{ECS_CLUSTER} ]]
 do printf '.'
 sleep 5
done
echo "ECS agent successfully joined to ${ECS_CLUSTER}"

执行健康检查

在上面的示例中,配置了一个 bash until 循环,该循环使用 curl 查询http://localhost:51678/v1/metadata端点,每五秒钟一次。此命令的输出通过管道传输到jq,它将返回 Cluster 属性或如果不存在此属性,则返回空值。一旦 ECS 代理注册到正确的 ECS 集群并在 JSON 响应中返回此属性,循环将完成,并且第一次运行脚本将完成。

测试你的自定义 ECS 容器实例映像

你现在已经完成了所有的定制工作,现在是使用packer build命令重建你的映像的时候了。在此之前,现在是验证你已经放置了正确的 Packer 模板,并且也创建了相关的支持文件的好时机。下面的示例显示了你的 packer-ecs 仓库现在应该具有的文件夹和文件结构:


> tree
.
├── Makefile
├── files
│   └── firstrun.sh
├── manifest.json
├── packer.json
└── scripts
    ├── cleanup.sh
    ├── cloudinit.sh
    ├── storage.sh
    └── time.sh

2 directories, 8 files

验证 Packer 仓库

假设一切就绪,你现在可以通过运行make build命令再次运行你的 Packer 构建。

一旦一切都完成并且你的 AMI 已成功创建,你现在可以通过导航到服务 | EC2并从左侧菜单中选择 AMIs 来在 AWS 控制台中查看你的 AMI:

EC2 仪表板 AMIs

在上面的截图中,你可以看到你在本章和刚刚构建的两个 AMI。请注意,最近的 AMI 现在包括三个块设备,其中/dev/xvdcy代表你在本章前面添加的额外 20 GB gp2 卷。

此时,你可以通过点击启动按钮来测试你的 AMI,这将启动 EC2 实例向导。点击审核并启动按钮后,点击编辑安全组链接以通过 SSH 将你的 IP 地址授权给实例,如下截图所示:

启动新的 EC2 实例

完成后,点击审核并启动,然后点击启动按钮,最后配置你有权限访问的适当的 SSH 密钥对。

在启动实例屏幕上,你现在可以点击链接到你的新 EC2 实例,并复制公共 IP 地址,以便你可以通过 SSH 连接到实例,如下截图所示:

连接到新的 EC2 实例

连接到实例后,你可以验证你为 Docker 卷存储配置的额外 20 GB 卷已成功挂载:


> sudo mount
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
/dev/xvda1 on / type ext4 (rw,noatime,data=ordered)
devtmpfs on /dev type devtmpfs (rw,relatime,size=500292k,nr_inodes=125073,mode=755)
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /dev/shm type tmpfs (rw,relatime)
/dev/xvdcy on /data type ext4 (rw,noatime,data=ordered)
none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)

验证存储挂载

你可以通过运行date命令来检查时区是否正确配置,该命令应显示正确的时区(美国/东部),并验证ntpd服务是否正在运行:


> date
Wed Feb 21 06:45:40 EST 2018
> sudo service ntpd status
ntpd is runningntpd 正在运行

验证时间设置

接下来,你可以通过查看/etc/cloud/cloud.cfg文件来验证 cloud-init 配置已经配置为禁用安全更新:


> cat /etc/cloud/cloud.cfg
# WARNING: Modifications to this file may be overridden by files in
# /etc/cloud/cloud.cfg.d

# If this is set, 'root' will not be able to ssh in and they
# will get a message to login instead as the default user (ec2-user)
disable_root: true

# This will cause the set+update hostname module to not operate (if true)
preserve_hostname: true

datasource_list: [ Ec2, None ]

repo_upgrade: none
repo_upgrade_exclude:
 - kernel
 - nvidia*
 - cudatoolkit

mounts:
 - [ ephemeral0, /media/ephemeral0 ]
 - [ swap, none, swap, sw, "0", "0" ]
# vim:syntax=yaml
repo_update: false

验证 cloud-init 设置

你还应该验证 Docker 服务是否已停止,并根据你配置的清理脚本在启动时被禁用:


> sudo service docker status
docker is stopped
> sudo chkconfig --list docker
docker 0:off 1:off 2:off 3:off 4:off 5:off 6:off

验证已禁用的服务

最后,你可以验证 ec2-user 用户的家目录中是否存在首次运行脚本:


> pwd
/home/ec2-user
> ls 
firstrun.sh

验证首次运行脚本

此时,您已成功验证了您的 ECS 容器实例已根据您的定制构建,并且现在应该从 EC2 控制台终止该实例。您会注意到它处于未配置状态,实际上您的 ECS 容器实例无法做太多事情,因为 Docker 服务已被禁用,在下一章中,您将学习如何使用 CloudFormation 利用您安装到自定义机器镜像中的 CloudFormation 辅助脚本来配置您的 ECS 容器实例在实例创建时,并利用您创建的定制。

摘要

在本章中,您学习了如何使用流行的开源工具 Packer 构建自定义的 ECS 容器实例机器镜像。您学习了如何创建 Packer 模板,并了解了组成模板的各个部分,包括变量、构建器、供应商和后处理器。您能够在图像构建过程中注入临时会话凭据,以便验证访问 AWS,使用 Packer 变量、环境变量和一些 Make 自动化的组合。

您已成功将一些构建时定制引入到您的 ECS 容器实例镜像中,包括安装 CloudFormation 辅助脚本和 CloudWatch 日志代理,并确保系统配置为在启动时以正确的时区运行 NTP 服务。您在 cloud-init 配置中禁用了自动安全更新,这可能会在使用 HTTP 代理时造成问题。

最后,您创建了一个首次运行脚本,旨在在实例创建和首次启动时配置您的 ECS 容器实例。该脚本配置 ECS 集群成员资格,启用可选的 HTTP 代理支持,为 Docker 和 ECS 代理系统日志配置 CloudWatch 日志代理,并执行健康检查,以确保您的实例已成功初始化。

在下一章中,您将学习如何使用自定义 AMI 来构建 ECS 集群和相关的底层 EC2 自动扩展组,这将帮助您理解对自定义机器映像执行的各种自定义的原因。

问题

  1. Packer 模板的哪个部分定义了 Packer 构建过程中使用的临时实例的 EC2 实例类型?

  2. True/False: Packer 在构建过程中需要对临时实例进行 SSH 访问。

  3. 您使用什么配置文件格式来定义 Packer 模板?

  4. True/False: 您必须将 AWS 凭据硬编码到 Packer 模板中。

  5. True/False: 要捕获 Packer 创建的 AMI ID,您必须解析 Packer 构建过程的日志输出。

  6. ECS-Optimized AMI 的默认存储配置是什么?

  7. 您会使用什么类型的 Packer provisioner 来将文件写入/etc 目录?

  8. 您从一个需要很长时间才能启动的自定义 AMI 创建了一个 EC2 实例。该 AMI 安装在一个没有额外基础架构配置的私有子网中。导致启动时间缓慢的可能原因是什么?

进一步阅读

您可以查看以下链接以获取本章涵盖的主题的更多信息:

第七章:创建 ECS 集群

在上一章中,您学习了如何构建自定义 ECS 容器实例 Amazon Machine Image(AMI),介绍了您在生产实际用例中通常需要的功能,包括自定义存储配置、CloudWatch 日志支持以及与 CloudFormation 的集成。

在本章中,您将使用自定义机器映像构建 ECS 集群,该集群由基于您的自定义机器映像的 ECS 容器实例组成。与之前章节的方法不同,讨论配置 AWS 资源的各种方法,本章将专注于使用基础设施即代码的方法,并使用 CloudFormation 定义您的 ECS 集群和支持资源。

部署 ECS 集群的标准模型基于 EC2 自动扩展组,它由一组 EC2 实例组成,可以根据各种因素自动扩展或缩小。在 ECS 集群的用例中,EC2 自动扩展组是一组 ECS 容器实例,共同形成一个 ECS 集群,您可以将您的 ECS 服务和 ECS 任务部署到其中。您将学习如何定义 EC2 自动扩展组,定义控制您的 EC2 实例部署方式的启动配置,并配置 CloudFormation Init 元数据,该元数据允许您在实例创建时触发自定义初始化逻辑,并等待每个实例发出初始化成功的信号。最后,您将配置支持资源,如 IAM 实例配置文件和 EC2 安全组,然后创建您的 CloudFormation 堆栈,部署您的 ECS 集群和底层 EC2 自动扩展组。

将涵盖以下主题:

  • 部署概述

  • 定义 ECS 集群

  • 配置 EC2 自动扩展组

  • 定义 EC2 自动扩展启动配置

  • 配置 CloudFormation Init Metadata

  • 配置自动扩展组创建策略

  • 配置 EC2 实例配置文件

  • 配置 EC2 安全组

  • 部署和测试 ECS 集群

技术要求

本章列出了完成本章所需的技术要求:

  • AWS 账户的管理员访问权限

  • 根据第三章的说明配置本地 AWS 配置文件

  • AWS CLI

此 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch7.

查看以下视频以查看代码实际运行情况:

bit.ly/2PaK6AM

部署概述

接下来两章的目标是建立支持基础设施和资源,以便使用 AWS 部署 Docker 应用程序。根据将基础设施定义为代码的最佳实践精神,您将定义一个 CloudFormation 模板,其中包括支持 Docker 应用程序在 ECS 中运行所需的所有 AWS 资源。随着您在每个章节中的进展,您将逐渐添加更多的资源,直到您拥有一个完整的解决方案,可以在 AWS 中使用 ECS 部署您的 Docker 应用程序。

考虑到这一点,本章的重点是学习如何使用 CloudFormation 构建 ECS 集群,正如您在之前的章节中已经学到的,ECS 集群是一组 ECS 容器实例,您可以在运行 ECS 服务或 ECS 任务时对其进行定位。

ECS 集群本身是非常简单的构造 - 它们只是定义了一组 ECS 容器实例和一个集群名称。然而,这些集群是如何形成的,涉及到更多的工作,并需要几个支持资源,包括以下内容:

  • EC2 自动扩展组:定义具有相同配置的 EC2 实例集合。

  • EC2 自动扩展启动配置:定义自动扩展组中新创建实例的启动配置。启动配置通常包括用户数据脚本,这些脚本在实例首次运行时执行,并可用于触发您在上一章中安装的 CloudFormation 助手脚本与 CloudFormation Init Metadata 交互的自定义机器映像。

  • CloudFormation Init Metadata:定义每个自动扩展组中的 EC2 实例在初始创建时应运行的初始化逻辑,例如运行配置命令、启用服务以及创建用户和组。CloudFormation Init Metadata 比用户数据提供的配置能力更强大,最重要的是,它为每个实例提供了一种向 CloudFormation 发出信号的机制,表明实例已成功配置自身。

  • CloudFormation Creation Policy:定义了确定 CloudFormation 何时可以将 EC2 自动扩展组视为已成功创建并继续在 CloudFormation 堆栈中提供其他依赖项的标准。这基于 CloudFormation 从 EC2 自动扩展组中的每个 EC2 实例接收到可配置数量的成功消息。

有其他方法可以形成 ECS 集群,但是对于大规模生产环境,通常希望使用 EC2 自动扩展组,并使用 CloudFormation 以及相关的 CloudFormation Init Metadata 和 Creation Policies 来以稳健、可重复、基础设施即代码的方式部署您的集群。

这些组件如何一起工作可能最好通过图表来描述,然后简要描述 ECS 集群是如何从这些组件中形成的,之后您将学习如何执行每个相关的配置任务,以创建自己的 ECS 集群。

以下图表说明了使用 EC2 自动扩展组和 CloudFormation 创建 ECS 集群的部署过程:

使用 EC2 自动扩展组和 CloudFormation 部署 ECS 集群的概述

在前面的图表中,一般的方法如下:

  1. 作为 CloudFormation 部署的一部分,CloudFormation 确定已准备好开始创建配置的 ECS 集群资源。ECS 集群资源将被引用在 EC2 自动扩展启动配置资源中的 CloudFormation Init Metadata 中,因此必须首先创建此 ECS 集群资源。请注意,此时 ECS 集群为空,正在等待 ECS 容器实例加入集群。

  2. CloudFormation 创建了一个 EC2 自动扩展启动配置资源,该资源定义了 EC2 自动扩展组中每个 EC2 实例在实例创建时将应用的启动配置。启动配置包括一个用户数据脚本,该脚本调用安装在 EC2 实例上的 CloudFormation 辅助脚本,后者又下载定义了每个实例在创建时应执行的一系列命令和其他初始化操作的 CloudFormation Init Metadata。

  3. 一旦启动配置资源被创建,CloudFormation 将创建 EC2 自动扩展组资源。自动扩展组的创建将触发 EC2 自动扩展服务在组中创建可配置的期望数量的 EC2 实例。

  4. 每当 EC2 实例启动时,它会应用启动配置,执行用户数据脚本,并下载并执行 CloudFormation Init Metadata 中定义的配置任务。这将包括各种初始化任务,在我们的特定用例中,实例将执行您在上一章中添加到自定义机器映像中的第一次运行脚本,以加入配置的 ECS 集群,确保 CloudWatch 日志代理配置为记录到正确的 CloudWatch 日志组,启动和启用 Docker 和 ECS 代理,最后,验证 EC2 实例成功加入 ECS 集群,并向 CloudFormation 发出信号,表明 EC2 实例已成功启动。

  5. 自动扩展组配置了创建策略,这是 CloudFormation 的一个特殊功能,它会导致 CloudFormation 等待直到从自动扩展组中的 EC2 实例接收到可配置数量的成功信号。通常,您将配置为 EC2 自动扩展组中的所有实例,确保所有实例成功加入 ECS 集群并且健康,然后才能继续其他的配置任务。

  6. 在 ECS 集群中有正确数量的从 EC2 自动扩展组派生的 ECS 容器实例的情况下,CloudFormation 可以安全地配置其他需要健康的 ECS 集群的 ECS 资源。例如,您可以创建一个 ECS 服务,该服务将将您的容器应用程序部署到 ECS 集群中。

定义 ECS 集群

现在您已经了解了 ECS 集群配置过程的概述,让我们逐步进行所需的配置,以使 ECS 集群正常运行。

如部署概述所示,您将使用 CloudFormation 以基础设施即代码的方式创建资源,因为您刚刚开始这个旅程,您首先需要创建这个 CloudFormation 模板,我假设您正在根据第五章“使用 ECR 发布 Docker 镜像”中在todobackend-aws存储库中创建的文件stack.yml中定义,如下例所示:

> touch stack.yml
> tree .
.
├── ecr.yml
└── stack.yml

0 directories, 2 files

在 todobackend-aws 存储库中建立

您现在可以在stack.yml文件中建立一个基本的 CloudFormation 模板,并创建您的 ECS 集群资源:

AWSTemplateFormatVersion: "2010-09-09"

Description: Todobackend Application

Resources:
  ApplicationCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: todobackend-cluster

定义一个 CloudFormation 模板

如前面的示例所示,定义 ECS 集群非常简单,AWS::ECS::Cluster资源类型只有一个可选属性叫做ClusterName。确保您的环境配置了正确的 AWS 配置文件后,您现在可以使用aws cloudformation deploy命令创建和部署堆栈,并使用aws ecs list-clusters命令验证您的集群是否已创建,就像下面的示例中演示的那样:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file stack.yml --stack-name todobackend
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend
> aws ecs list-clusters
{
    "clusterArns": [
        "arn:aws:ecs:us-east-1:385605022855:cluster/todobackend-cluster"
    ]
}

使用 CloudFormation 创建 ECS 集群

配置 EC2 自动扩展组

您已经建立了一个 ECS 集群,但如果没有 ECS 容器实例来提供容器运行时和计算资源,该集群就没有太多用处。此时,您可以创建单独的 ECS 容器实例并加入到集群中,但是,如果您需要运行需要支持数十甚至数百个容器的生产工作负载,根据集群当前资源需求动态添加和移除 ECS 容器实例,这种方法就不可行了。

AWS 提供的用于为 ECS 容器实例提供这种行为的机制是 EC2 自动扩展组,它作为一组具有相同配置的 EC2 实例的集合,被称为启动配置。EC2 自动扩展服务是 AWS 提供的托管服务,负责管理您的 EC2 自动扩展组和组成组的 EC2 实例的生命周期。这种机制提供了云的核心原则之一-弹性-并允许您根据应用程序的需求动态扩展或缩减服务您应用程序的 EC2 实例数量。

在 ECS 的背景下,您可以将 ECS 集群通常视为与 EC2 自动扩展组有密切关联,ECS 容器实例则是 EC2 自动扩展组中的 EC2 实例,其中 ECS 代理和 Docker 引擎是每个 EC2 实例上运行的应用程序。这并不完全正确,因为您可以拥有跨多个 EC2 自动扩展组的 ECS 集群,但通常情况下,您的 ECS 集群和 EC2 自动扩展组之间会有一对一的关系,ECS 容器实例与 EC2 实例直接关联。

现在您了解了 EC2 自动缩放组的基本背景以及它们与 ECS 的特定关系,重要的是要概述在创建 EC2 自动缩放组时需要与之交互的各种配置构造:

  • 自动缩放组:定义了一组 EC2 实例,并为该组指定了最小、最大和期望的容量。

  • 启动配置:启动配置定义了应用于每个 EC2 实例在实例创建时的通用配置。

  • CloudFormation Init 元数据:定义可以应用于实例创建的自定义初始化逻辑。

  • IAM 实例配置文件和角色:授予每个 EC2 实例与 ECS 服务交互和发布到 CloudWatch 日志的权限。

  • EC2 安全组:定义入站和出站网络策略规则。至少,这些规则必须允许每个 EC2 实例上运行的 ECS 代理与 ECS API 进行通信。

请注意,我正在提出一种自上而下的方法来定义 EC2 自动缩放组的要求,这在使用声明性基础设施即代码方法(例如 CloudFormation)时是可能的。在实际实现这些资源时,它们将以自下而上的方式应用,首先创建依赖项(例如安全组和 IAM 角色),然后创建启动配置,最后创建自动缩放组。当然,这是由 CloudFormation 处理的,因此我们可以专注于所需的状态配置,让 CloudFormation 处理满足所需状态的具体执行要求。

创建 EC2 自动缩放组

在创建 EC2 自动缩放组时,您需要定义的第一个资源是 EC2 自动缩放组本身,在 CloudFormation 术语中,它被定义为AWS::AutoScaling::AutoScalingGroup类型的资源。

AWSTemplateFormatVersion: "2010-09-09"

Description: Todobackend Application

Parameters:
  ApplicationDesiredCount:
 Type: Number
 Description: Desired EC2 instance count
  ApplicationSubnets:
 Type: List<AWS::EC2::Subnet::Id>
 Description: Target subnets for EC2 instances

Resources:
  ApplicationCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: todobackend-cluster
  ApplicationAutoscaling:
 Type: AWS::AutoScaling::AutoScalingGroup
 Properties:
 LaunchConfigurationName: !Ref ApplicationAutoscalingLaunchConfiguration
 MinSize: 0
 MaxSize: 4
 DesiredCapacity: !Ref ApplicationDesiredCount
 VPCZoneIdentifier: !Ref ApplicationSubnets
 Tags:
 - Key: Name
 Value: !Sub ${AWS::StackName}-ApplicationAutoscaling-instance
 PropagateAtLaunch: "true"

定义 EC2 自动缩放组

前面示例中的配置是满足定义 EC2 自动缩放组的最低要求的基本配置,如下所示:

  • LaunchConfigurationName:应该应用于组中每个实例的启动配置的名称。在前面的示例中,我们使用Ref内部函数的简写语法,结合一个名为ApplicationAutoscalingLaunchConfiguration的资源的名称,这是我们将很快定义的资源。

  • MinSizeMaxSizeDesiredCapacity:自动扩展组中实例的绝对最小值,绝对最大值和期望数量。EC2 自动扩展组将始终尝试保持期望数量的实例,尽管它可能根据您在MinSizeMaxSize属性的范围内的自己的标准暂时扩展或缩减实例的数量。在前面的示例中,您引用了一个名为ApplicationDesiredCount的参数,以定义期望的实例数量,具有缩减为零实例或扩展为最多四个实例的能力。

  • VPCZoneIdentifier:EC2 实例应部署到的目标子网列表。在前面的示例中,您引用了一个名为ApplicationSubnets的输入参数,它被定义为List<AWS::EC2::Subnet::Id>类型的参数。这可以简单地提供为逗号分隔的列表,您很快将看到定义这样一个列表的示例。

  • Tags:定义要附加到自动扩展组的一个或多个标记。至少,定义Name标记是有用的,以便您可以清楚地识别您的 EC2 实例,在前面的示例中,您使用Fn::Sub内在函数的简写形式来动态注入由AWS::StackName伪参数定义的堆栈名称。PropagateAtLaunch标记配置标记在每次 EC2 实例启动时附加,确保配置的名称对于每个实例都可见。

有关如何配置自动扩展组资源的更多信息,请参阅 AWS CloudFormation 文档(docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html)。

配置 CloudFormation 输入参数

在前面的示例中,您向 CloudFormation 模板添加了名为ApplicationDesiredCountApplicationSubnets的参数,您需要在部署模板时为其提供值。

ApplicationDesiredCount参数只需要是配置的 MinSize 和 MaxSize 属性之间的数字(即 0 和 4 之间),但是,要确定您帐户中子网 ID 的值,您可以使用aws ec2 describe-subnets命令,如下所示:

> aws ec2 describe-subnets --query "Subnets[].[SubnetId,AvailabilityZone]" --output table
-----------------------------------
| DescribeSubnets                 |
+------------------+--------------+
| subnet-a5d3ecee  | us-east-1a   |
| subnet-c2abdded  | us-east-1d   |
| subnet-aae11aa5  | us-east-1f   |
| subnet-fd3a43c2  | us-east-1e   |
| subnet-324e246f  | us-east-1b   |
| subnet-d281a2b6  | us-east-1c   |
+------------------+--------------+

使用 AWS CLI 查询子网

在前面的示例中,您使用了一个 JMESPath 查询表达式来选择每个子网的SubnetIdAvailabilityZone属性,并以表格格式显示输出。在这里,我们只是利用了为您的账户在默认 VPC 中创建的默认子网,但是根据您网络拓扑的性质,您可以使用在您的账户中定义的任何子网。

在这个例子中,我们将使用us-east-1aus-east-1b可用区中的两个子网,你接下来的问题可能是,我们如何将这些值传递给 CloudFormation 堆栈?AWS CLI 目前只能通过命令行标志与aws cloudformation deploy命令一起提供输入参数的能力,然而,当您有大量堆栈输入并且想要持久化它们时,这种方法很快变得乏味和笨拙。

我们将采用的一个非常简单的方法是在todobackend-aws存储库的根目录下定义一个名为dev.cfg的配置文件中的各种输入参数:

ApplicationDesiredCount=1
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f

为堆栈参数定义一个配置文件 dev.cfg

配置文件的方法是在新的一行上以<key>=<value>格式添加每个参数,稍后在本章中,您将看到我们如何可以将此文件与aws cloudformation deploy命令一起使用。在前面的示例中,请注意我们将ApplicationSubnets参数值配置为逗号分隔的列表,这是在配置 CloudFormation 参数时配置任何列表类型的标准格式。

堆栈参数通常是特定于环境的,因此根据您的环境命名您的配置文件是有意义的。例如,如果您有开发和生产环境,您可能会分别称呼您的配置文件为dev.cfgprod.cfg

定义 EC2 自动扩展启动配置

尽管您已经定义了一个 EC2 自动扩展组资源,但是您还不能部署您的 CloudFormation 模板,因为自动扩展组引用了一个名为ApplicationAutoscalingLaunchConfiguration的资源,该资源尚未定义。

EC2 自动扩展启动配置定义了在启动时应用于每个实例的配置,并提供了一种确保自动扩展组中的每个实例保持一致的常见方法。

以下示例演示了在 CloudFormation 模板中配置自动扩展启动配置:

...
...
Parameters:
  ApplicationDesiredCount:
    Type: Number
    Description: Desired EC2 instance count
  ApplicationImageId:
 Type: String
 Description: ECS Amazon Machine Image (AMI) ID
  ApplicationSubnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Target subnets for EC2 instances

Resources:
  ApplicationAutoscalingLaunchConfiguration:
    Type: AWS::AutoScaling::LaunchConfiguration
 Properties:
 ImageId: !Ref ApplicationImageId
 InstanceType: t2.micro
 KeyName: admin
 IamInstanceProfile: !Ref ApplicationAutoscalingInstanceProfile
 SecurityGroups:
 - !Ref ApplicationAutoscalingSecurityGroup
 UserData:
 Fn::Base64:
 Fn::Sub: |
 #!/bin/bash
 /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} \
 --resource ApplicationAutoscalingLaunchConfiguration \
 --region ${AWS::Region}
 /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} \
 --resource ApplicationAutoscaling \
 --region ${AWS::Region}
  ApplicationCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: todobackend-cluster
  ApplicationAutoscaling:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      LaunchConfigurationName: !Ref ApplicationAutoscalingLaunchConfiguration
      MinSize: 0
      MaxSize: 4
      DesiredCapacity: !Ref ApplicationDesiredCount
      VPCZoneIdentifier: !Ref ApplicationSubnets
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-ApplicationAutoscaling-instance
          PropagateAtLaunch: "true"

定义 EC2 自动扩展启动配置

请注意,您指定了AWS::AutoScaling::LaunchConfiguration资源类型,并为您的启动配置配置了以下属性:

  • ImageId: EC2 实例将从中启动的镜像的 AMI。对于我们的用例,您将使用在上一章中创建的 AMI。此属性引用了一个名为ApplicationImageId的新参数,因此您需要将此参数与自定义机器映像的 AMI ID 添加到dev.cfg文件中。

  • InstanceType: EC2 实例的实例系列和类型。

  • KeyName: 将被允许对每个 EC2 实例进行 SSH 访问的 EC2 密钥对。

  • IamInstanceProfile: 要附加到 EC2 实例的 IAM 实例配置文件。正如您在前几章中学到的,为了支持作为 ECS 容器实例的操作,IAM 实例配置文件必须授予 EC2 实例与 ECS 服务交互的权限。在前面的示例中,您引用了一个名为ApplicationAutoscalingInstanceProfile的资源,您将在本章后面创建。

  • SecurityGroups: 要附加到每个实例的 EC2 安全组。这些定义了应用于网络流量的入站和出站安全规则,至少必须允许与 ECS 服务、CloudWatch 日志服务和其他相关的 AWS 服务进行通信。同样,您引用了一个名为ApplicationAutoscalingSecurityGroup的资源,您将在本章后面创建。

  • UserData: 定义在实例创建时运行的用户数据脚本。这必须作为 Base64 编码的字符串提供,您可以使用Fn::Base64内在函数让 CloudFormation 自动执行此转换。您定义一个 bash 脚本,首先运行cfn-init命令,该命令将下载并执行与ApplicationAutoscalingLaunchConfiguration引用资源相关的 CloudFormation Init 元数据,然后运行cfn-signal命令来向 CloudFormation 发出信号,指示cfn-init是否成功运行(请注意,cfn-signal引用AutoscalingGroup资源,而不是ApplicationAutoscalingLaunchConfiguration资源)。

注意使用Fn::Sub函数后跟管道运算符(|),这使您可以输入自由格式文本,该文本将遵守所有换行符,并允许您使用AWS::StackNameAWS::Region伪参数引用正确的堆栈名称和 AWS 区域。

您可能会注意到在 UserData bash 脚本中未设置set -e标志,这是有意为之的,因为我们希望cfn-signal脚本将cfn-init脚本的退出代码报告给 CloudFormation(由-e $?选项定义,其中$?输出最后一个进程的退出代码)。如果包括set -e,则如果cfn-init返回错误,脚本将立即退出,cfn-signal将无法向 CloudFormation 发出失败信号。

ApplicationDesiredCount=1 ApplicationImageId=ami-ec957491
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f

将 ApplicationImageId 参数添加到 dev.cfg 文件

配置 CloudFormation Init 元数据

到目前为止,在我们的堆栈中,您执行的最复杂的配置部分是UserData属性,作为自动扩展启动配置的一部分。

回想一下,在上一章中,当您创建了一个自定义机器映像时,您安装了cfn-bootstrap CloudFormation 助手脚本,其中包括在前面的示例中引用的cfn-initcfn-signal脚本。这些脚本旨在与称为 CloudFormation Init 元数据的功能一起使用,我们将在下面的示例中进行配置,如下例所示:

...
...
Resources:
  ...
  ...
  ApplicationAutoscalingLaunchConfiguration:
    Type: AWS::AutoScaling::LaunchConfiguration
    Metadata:
 AWS::CloudFormation::Init:
 config:
 commands:            05_public_volume:
 command: mkdir -p /data/public
 06_public_volume_permissions:
 command: chown -R 1000:1000 /data/public
 10_first_run:
 command: sh firstrun.sh
 cwd: /home/ec2-user
 env:
                ECS_CLUSTER: !Ref ApplicationCluster
 STACK_NAME: !Ref AWS::StackName
 AUTOSCALING_GROUP: ApplicationAutoscaling
 AWS_DEFAULT_REGION: !Ref AWS::Region
    Properties:
      ImageId: !Ref ApplicationImageId
      InstanceType: t2.micro
      KeyName: admin
      IamInstanceProfile: !Ref ApplicationAutoscalingInstanceProfile
      SecurityGroups:
        - !Ref ApplicationAutoscalingSecurityGroup
      UserData:
        Fn::Base64:
          Fn::Sub: |
            #!/bin/bash
            /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} \
              --resource ApplicationAutoscalingLaunchConfiguration \
              --region ${AWS::Region}
            /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} \
              --resource ApplicationAutoscaling \
              --region ${AWS::Region}
  ...
  ...

配置 CloudFormation Init 元数据

在上面的示例中,您可以看到 CloudFormation Init 元数据定义了一个包含commands指令的配置集,该指令定义了几个命令对象:

  • 05_public_volume - 在我们定制的 ECS AMI 中创建一个名为public的文件夹,该文件夹位于/data挂载下。我们需要这个路径,因为我们的应用程序需要一个公共卷,静态文件将位于其中,而我们的应用程序以非 root 用户身份运行。稍后我们将创建一个 Docker 卷,该卷引用此路径,并注意因为 ECS 目前仅支持绑定挂载,所以需要预先在底层 Docker 主机上创建一个文件夹(有关更多详细信息,请参见github.com/aws/amazon-ecs-agent/issues/1123#issuecomment-405063273)。

  • 06_public_volume_permissions - 这将更改前一个命令中创建的/data/public文件夹的所有权,使其由 ID 为 1000 的用户和组拥有。这是 todobackend 应用程序运行的相同用户 ID/组 ID,因此将允许应用程序读取和写入/data/public文件夹。

  • 10_first_run - 在工作目录/home/ec2-user中运行sh firstrun.sh命令,回顾前一章提到的自定义机器镜像中包含的第一次运行脚本,用于在实例创建时执行自定义初始化任务。这个第一次运行脚本包括对许多环境变量的引用,这些环境变量在 CloudFormation Init 元数据的env属性下定义,并为第一次运行脚本提供适当的值。

为了进一步说明10_first_run脚本的工作原理,以下代码片段配置了 ECS 容器实例加入 ECS 集群,由ECS_CLUSTER环境变量定义:

#!/usr/bin/env bash
set -e

# Configure ECS Agent
echo "ECS_CLUSTER=${ECS_CLUSTER}" > /etc/ecs/ecs.config
...
...

第一次运行脚本片段

类似地,STACK_NAMEAUTOSCALING_GROUPAWS_DEFAULT_REGION变量都用于配置 CloudWatch 日志代理:

...
...
# Write AWS Logs region
sudo tee /etc/awslogs/awscli.conf << EOF > /dev/null
[plugins]
cwlogs = cwlogs
[default]
region = ${AWS_DEFAULT_REGION}
EOF

# Write AWS Logs config
sudo tee /etc/awslogs/awslogs.conf << EOF > /dev/null
[general]
state_file = /var/lib/awslogs/agent-state 

[/var/log/dmesg]
file = /var/log/dmesg
log_group_name = /${STACK_NAME}/ec2/${AUTOSCALING_GROUP}/var/log/dmesg
log_stream_name = {instance_id}
...
...

第一次运行脚本片段

配置自动扩展组创建策略

在前一节中,您配置了用户数据脚本和 CloudFormation Init 元数据,以便您的 ECS 容器实例可以执行适合于给定目标环境的首次初始化和配置。虽然每个实例都会向 CloudFormation 发出 CloudFormation Init 过程的成功或失败信号,但您需要显式地配置 CloudFormation 等待每个自动扩展组中的实例发出成功信号,这一点非常重要,如果您希望确保在 ECS 集群注册或由于某种原因失败之前,不会尝试将 ECS 服务部署到 ECS 集群。

CloudFormation 包括一个称为创建策略的功能,允许您在创建 EC2 自动扩展组和 EC2 实例时指定可选的创建成功标准。当创建策略附加到 EC2 自动扩展组时,CloudFormation 将等待自动扩展组中的可配置数量的实例发出成功信号,然后再继续进行,这为我们提供了强大的能力,以确保您的 ECS 自动扩展组和相应的 ECS 集群处于健康状态,然后再继续创建 CloudFormation 堆栈中的其他资源。回想一下在上一章中,您自定义机器映像中第一次运行脚本的最后一步是查询本地 ECS 代理元数据,以验证实例是否已加入配置的 ECS 集群,因此,如果第一次运行脚本成功完成并且 cfn-signal 向 CloudFormation 发出成功信号,我们知道该实例已成功注册到 ECS 集群。

以下示例演示了如何在现有的 EC2 自动扩展组资源上配置创建策略:

Resources:
  ...
  ...
  ApplicationAutoscaling:
    Type: AWS::AutoScaling::AutoScalingGroup
    CreationPolicy:
 ResourceSignal:
 Count: !Ref ApplicationDesiredCount
 Timeout: PT15M
    Properties:
      LaunchConfigurationName: !Ref ApplicationAutoscalingLaunchConfiguration
      MinSize: 0
      MaxSize: 4
      DesiredCapacity: !Ref ApplicationDesiredCount
      VPCZoneIdentifier: !Split [",", !Ref ApplicationSubnets]
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-ApplicationAutoscaling-instance
          PropagateAtLaunch: "true"

在 CloudFormation 中配置创建策略

正如您在前面的示例中所看到的,使用CreationPolicy属性配置创建策略,目前,这些策略只能为 EC2 自动扩展组资源、EC2 实例资源和另一种特殊类型的 CloudFormation 资源调用等待条件进行配置。

ResourceSignal对象包括一个Count属性,该属性定义了确定自动扩展组是否已成功创建所需的最小成功信号数量,并引用ApplicationDesiredCount参数,这意味着您期望自动扩展组中的所有实例都能成功创建。Timeout属性定义了等待所有成功信号的最长时间 - 如果在此时间范围内未满足配置的计数,则将认为自动扩展组未成功创建,并且堆栈部署将失败并回滚。此属性使用一种称为ISO8601 持续时间格式的特殊格式进行配置,PT15M的值表示 CloudFormation 将等待最多 15 分钟的所有成功信号。

配置 EC2 实例配置文件

在前面示例中定义的 EC2 自动扩展启动配置中,您引用了一个 IAM 实例配置文件,我们需要在堆栈中创建为一个单独的资源。EC2 实例配置文件允许您附加一个 IAM 角色,您的 EC2 实例可以使用该角色来访问 AWS 资源和服务,在 ECS 容器实例使用情况下。回想一下第四章,当您创建第一个 ECS 集群时,自动附加了一个 IAM 实例配置文件和相关的 IAM 角色,授予了各种 ECS 权限。

因为我们正在从头开始配置 ECS 集群和自动扩展组,我们需要明确定义适当的 IAM 实例配置文件和关联的 IAM 角色,就像以下示例中所示的那样:


Resources:
  ...
  ...
  ApplicationAutoscalingInstanceProfile:
 Type: AWS::IAM::InstanceProfile
 Properties:
 Roles:
 - Ref: ApplicationAutoscalingInstanceRole
 ApplicationAutoscalingInstanceRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Principal:
 Service:
 - ec2.amazonaws.com
 Action:
 - sts:AssumeRole
 Policies:
 - PolicyName: ECSContainerInstancePermissions
 PolicyDocument: 
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Action:
 - ecs:RegisterContainerInstance
 - ecs:DeregisterContainerInstance
                  - ecs:UpdateContainerInstancesState
 Resource: !Sub ${ApplicationCluster.Arn}
 - Effect: Allow
 Action:
 - ecs:DiscoverPollEndpoint
 - ecs:Submit*
 - ecs:Poll
 - ecs:StartTelemetrySession
 Resource: "*"
 - Effect: Allow
 Action: 
 - ecr:BatchCheckLayerAvailability
 - ecr:BatchGetImage
 - ecr:GetDownloadUrlForLayer
 - ecr:GetAuthorizationToken
 Resource: "*"
 - Effect: Allow
 Action:
 - logs:CreateLogGroup
 - logs:CreateLogStream
 - logs:PutLogEvents
 - logs:DescribeLogStreams
 Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/${AWS::StackName}*
...
...

定义 IAM 实例配置文件和 IAM 角色

在前面的示例中,您不是附加AmazonEC2ContainerServiceforEC2Role托管策略,而是附加了一个定义了类似权限集的自定义策略,注意以下区别:

  • 未授予创建集群的权限,因为您已经在堆栈中自己创建了 ECS 集群。

  • 注册、注销和更新容器实例状态的权限仅限于您堆栈中定义的 ECS 集群。相比之下,AmazonEC2ContainerServiceforEC2Role角色授予您账户中所有集群的权限,因此您的自定义配置被认为更安全。

  • 自定义策略授予logs:CreateLogGroup权限 - 即使日志组已经创建,CloudWatch 日志代理也需要此权限。在前面的示例中,我们将此权限限制为以当前堆栈名称为前缀的日志组,限制了这些权限的范围。

配置 EC2 安全组

您几乎已经完成了部署 ECS 集群和 EC2 自动扩展组所需的配置,但是我们还需要创建一个最终资源,即您之前在ApplicationAutoscalingLaunchConfiguration资源配置中引用的ApplicationAutoscalingSecurityGroup资源:

Parameters:
  ApplicationDesiredCount:
    Type: Number
    Description: Desired EC2 instance count
  ApplicationImageId:
    Type: String
    Description: ECS Amazon Machine Image (AMI) ID
  ApplicationSubnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Target subnets for EC2 instances
  VpcId:
 Type: AWS::EC2::VPC::Id
 Description: Target VPC

Resources:
  ApplicationAutoscalingSecurityGroup:
 Type: AWS::EC2::SecurityGroup
 Properties:
 GroupDescription: !Sub ${AWS::StackName} Application Autoscaling Security Group
 VpcId: !Ref VpcId
 SecurityGroupIngress:
 - IpProtocol: tcp
 FromPort: 22
 ToPort: 22
 CidrIp: 0.0.0.0/0
 SecurityGroupEgress:
 - IpProtocol: udp
 FromPort: 53
 ToPort: 53
 CidrIp: 0.0.0.0/0
 - IpProtocol: tcp
 FromPort: 80
 ToPort: 80
 CidrIp: 0.0.0.0/0
 - IpProtocol: tcp
 FromPort: 443
 ToPort: 443
 CidrIp: 0.0.0.0/0
...
...

定义 EC2 安全组

在上面的示例中,您允许入站 SSH 访问您的实例,并允许您的实例访问互联网上的 DNS、HTTP 和 HTTPS 资源。这不是最安全的安全组配置,在生产用例中,您至少会将 SSH 访问限制在内部管理地址,但为了简化和演示目的,您配置了一组相当宽松的安全规则。

查找堆栈依赖的外部资源的物理标识符的更可扩展的方法是使用一个称为 CloudFormation exports 的功能,它允许您将有关资源的数据导出到其他堆栈。例如,您可以在一个名为 network-resources 的堆栈中定义所有网络资源,然后配置一个 CloudFormation 导出,将该堆栈创建的 VPC 资源的 VPC ID 导出。然后,可以通过使用Fn::ImportValue内部函数在其他 CloudFormation 堆栈中引用这些导出。有关此方法的更多详细信息,请参见docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html

> aws ec2 describe-vpcs
{
    "Vpcs": [
        {
            "CidrBlock": "172.31.0.0/16",
            "DhcpOptionsId": "dopt-a037f9d8",
            "State": "available",
            "VpcId": "vpc-f8233a80",
            "InstanceTenancy": "default",
            "CidrBlockAssociationSet": [
                {
                    "AssociationId": "vpc-cidr-assoc-32524958",
                    "CidrBlock": "172.31.0.0/16",
                    "CidrBlockState": {
                        "State": "associated"
                    }
                }
            ],
            "IsDefault": true
        }
    ]
}

请注意,您还定义了一个新参数,称为 VPC ID,它指定将在其中创建安全组的 VPC 的 ID,您可以使用aws ec2 describe-vpcs命令获取默认 VPC 的 ID,该 VPC 默认在您的 AWS 账户中创建:确定您的 VPC ID

一旦您有了正确的 VPC ID 值,您需要更新您的dev.cfg文件,以包括VpcId参数和值:

ApplicationDesiredCount=1ApplicationImageId=ami-ec957491
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

在 dev.cfg 中配置 VpcId 参数

部署和测试 ECS 集群

您现在已经完成了 CloudFormation 模板的配置,是时候部署您在上一节中所做的更改了。请记住,您创建了一个单独的配置文件,名为dev.cfg,用于存储每个堆栈参数的值。以下示例演示了如何使用aws cloudformation deploy命令来部署您更新的堆栈并引用您的输入参数值:

> aws cloudformation deploy --template-file stack.yml \
 --stack-name todobackend --parameter-overrides $(cat dev.cfg) \
 --capabilities CAPABILITY_NAMED_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend

使用参数覆盖部署 CloudFormation 堆栈

在前面的例子中,您使用--parameter-overrides标志为模板期望的每个参数指定值。而不是每次手动输入这些值,您只需使用 bash 替换并列出本地dev.cfg文件的内容,该文件以正确的格式表示每个参数名称和值。

还要注意,因为您的 CloudFormation 堆栈现在包括 IAM 资源,您必须使用--capabilities标志,并将其值指定为CAPABILITY_IAMCAPABILITY_NAMED_IAM。当您这样做时,您正在承认 CloudFormation 将代表您创建 IAM 资源,并且您授予权限。虽然只有在创建命名 IAM 资源时才需要指定CAPABILITY_NAMED_IAM值(我们没有),但我发现这样更通用,更不容易出错,总是引用这个值。

假设您的模板没有配置错误,您的堆栈应该可以成功部署,如果您浏览到 AWS 控制台中的 CloudFormation,选择 todobackend 堆栈,您可以查看堆栈部署过程中发生的各种事件:

查看 CloudFormation 部署状态

在前面的截图中,您可以看到 CloudFormation 在20:18:56开始创建一个自动扩展组,然后一分半钟后,在20:20:39,从自动扩展组中的单个 EC2 实例接收到一个成功的信号。这满足了接收所需数量的实例的创建策略标准,堆栈更新成功完成。

此时,您的 ECS 集群应该有一个注册和活动的 ECS 容器实例,您可以使用aws ecs describe-cluster命令来验证。

> aws ecs describe-clusters --cluster todobackend-cluster
{
    "clusters": [
        {
            "clusterArn": "arn:aws:ecs:us-east-1:385605022855:cluster/todobackend-cluster",
            "clusterName": "todobackend-cluster",
 "status": "ACTIVE",
 "registeredContainerInstancesCount": 1,
            "runningTasksCount": 0,
            "pendingTasksCount": 0,
            "activeServicesCount": 0,
            "statistics": []
        }
    ],
    "failures": []
}

验证 ECS 集群

在上一个例子中,您可以看到 ECS 集群有一个注册的 ECS 容器实例,集群的状态是活动的,这意味着您的 ECS 集群已准备好运行 ECS 任务和服务。

您还可以通过导航到 EC2 控制台,并从左侧菜单中选择自动扩展组来验证您的 EC2 自动扩展组是否正确创建:

验证 EC2 自动扩展组

在上一个截图中,请注意您的自动缩放组的名称包括堆栈名称(todobackend)、逻辑资源名称(ApplicationAutoscaling)和一个随机字符串值(XFSR1DDVFG9J)。这说明了 CloudFormation 的一个重要概念 - 如果您没有显式地为资源命名(假设资源具有Name或等效属性),那么 CloudFormation 将附加一个随机字符串以确保资源具有唯一的名称。

如果您按照并且配置您的堆栈没有任何错误,那么您的 CloudFormation 堆栈应该能够成功部署,就像之前的截图演示的那样。有可能,使用大约 150 行配置的 CloudFormation 模板,您会出现错误,您的 CloudFormation 部署将失败。如果您遇到问题并且无法解决部署问题,请参考此 GitHub URL 作为参考:github.com/docker-in-aws/docker-in-aws/blob/master/ch7/todobackend-aws

摘要

在本章中,您学习了如何创建一个 ECS 集群,包括一个 EC2 自动缩放组和基于自定义 Amazon 机器映像的 ECS 容器实例,使用基础设施即代码的方法使用 CloudFormation 定义所有资源。

您了解了 ECS 集群如何简单地是 ECS 容器实例的逻辑分组,并由管理一组 EC2 实例的 EC2 自动缩放组组成。EC2 自动缩放组可以动态地进行缩放,您将 EC2 自动缩放启动配置附加到了您的自动缩放组,该配置为每个添加到组中的新 EC2 实例提供了一组通用的设置。

CloudFormation 为确保自动扩展组中的实例正确初始化提供了强大的功能,您学会了如何配置用户数据以调用您在自定义机器映像中安装的 CloudFormation 辅助脚本,然后下载附加到启动配置资源的 CloudFormation Init 元数据中定义的可配置初始化逻辑。一旦 CloudFormation Init 过程完成,辅助脚本会向 CloudFormation 发出初始化过程的成功或失败信号,并为自动扩展组配置了一个创建策略,该策略定义了必须报告成功的实例数量,以便将整个自动扩展组资源视为健康。

接下来,您需要将 IAM 实例配置文件和安全组附加到启动配置中,确保您的 ECS 容器实例具有与 ECS 服务交互,从 ECR 下载图像,将日志发布到 CloudWatch 日志以及与相关的 AWS API 端点通信所需的权限。

通过核心自动扩展组、启动配置和其他支持资源的部署,您成功地使用 CloudFormation 部署了您的集群,建立了运行 ECS 任务和服务所需的基础设施基础。在下一章中,您将在此基础上构建,扩展您的 CloudFormation 模板以定义 ECS 任务定义、ECS 服务和部署完整端到端应用环境所需的其他支持资源。

问题

  1. 真/假:EC2 自动扩展组允许您为每个实例定义固定的 IP 地址。

  2. EC2 用户数据需要应用什么类型的编码?

  3. 您如何在 CloudFormation 模板中引用当前的 AWS 区域?

  4. 真/假:Ref内在函数只能引用 CloudFormation 模板中的资源。

  5. 在使用 CloudFormation Init 元数据时,您需要在 EC2 实例上运行哪两个辅助脚本?

  6. 您正在尝试使用亚马逊发布的标准 ECS 优化 AMI 创建 EC2 自动扩展组和 ECS 集群,但是您收到错误消息,指示没有实例注册到目标 ECS 集群,即使 CloudFormation 报告自动扩展组已创建。您如何解决这个问题?

  7. 真/假:aws cloudformation create命令用于部署和更新 CloudFormation 堆栈。

  8. 您正在尝试在没有默认互联网路由的私有子网中部署 ECS 集群,但是集群中的 ECS 容器实例未能注册到 ECS。这最有可能的解释是什么?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第八章:使用 ECS 部署应用程序

在上一章中,您学习了如何使用 EC2 自动扩展组在 AWS 中配置和部署 ECS 集群,本章的目标是使用 CloudFormation 将 ECS 应用程序部署到您新建的 ECS 集群。

您将首先开始学习如何定义和部署通常在生产环境中 ECS 应用程序中所需的各种支持资源。这些资源包括创建应用程序数据库以存储应用程序的数据,部署应用程序负载均衡器以服务和负载均衡对应用程序的请求,以及配置其他资源,例如 IAM 角色和安全组,以控制对应用程序的访问和从应用程序的访问。

有了这些支持资源,您将继续创建 ECS 任务定义,定义容器的运行时配置,然后配置 ECS 服务,将 ECS 任务定义部署到 ECS 集群,并与应用程序负载均衡器集成,以管理滚动部署等功能。最后,您将学习如何创建 CloudFormation 自定义资源,执行自定义的配置任务,例如运行数据库迁移,为您提供基于 AWS CloudFormation 的完整应用程序部署框架。

将涵盖以下主题:

  • 使用 RDS 创建应用程序数据库

  • 配置应用程序负载均衡器

  • 创建 ECS 任务定义

  • 部署 ECS 服务

  • ECS 滚动部署

  • 创建 CloudFormation 自定义资源

技术要求

以下列出了完成本章所需的技术要求:

  • AWS 账户的管理员访问权限

  • 本地 AWS 配置文件按第三章的说明配置

  • AWS CLI

  • 本章将继续自第七章开始,因此需要您成功完成那里定义的所有配置任务

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch8.

查看以下视频以查看代码的实际操作:

bit.ly/2Mx8wHX

使用 RDS 创建应用程序数据库

示例 todobackend 应用程序包括一个 MySQL 数据库,用于持久化通过应用程序 API 创建的待办事项。当您在第一章首次设置和运行示例应用程序时,您使用 Docker 容器提供应用程序数据库,但是在生产级环境中,通常认为最佳做法是在专门为数据库和数据访问操作进行了优化的专用机器上运行数据库和其他提供持久性存储的服务。AWS 中的一个这样的服务是关系数据库服务(RDS),它提供了专用的托管实例,针对提供流行的关系数据库引擎进行了优化,包括 MySQL、Postgres、SQL Server 和 Oracle。RDS 是一个非常成熟和强大的服务,非常常用于支持在 AWS 中运行的 ECS 和其他应用程序的数据库需求。

可以使用 CloudFormation 配置 RDS 实例。要开始,让我们在您的 todobackend CloudFormation 模板中定义一个名为ApplicationDatabase的新资源,其资源类型为AWS::RDS::DBInstance,如下例所示:

AWSTemplateFormatVersion: "2010-09-09"

Description: Todobackend Application

Parameters:
  ApplicationDesiredCount:
    Type: Number
    Description: Desired EC2 instance count
  ApplicationImageId:
    Type: String
    Description: ECS Amazon Machine Image (AMI) ID
  ApplicationSubnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Target subnets for EC2 instances
  DatabasePassword:
 Type: String
 Description: Database password
 NoEcho: "true"
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Target VPC

Resources:
  ApplicationDatabase:
 Type: AWS::RDS::DBInstance
 Properties:
 Engine: MySQL
 EngineVersion: 5.7
 DBInstanceClass: db.t2.micro
 AllocatedStorage: 10
 StorageType: gp2
 MasterUsername: todobackend
 MasterUserPassword: !Ref DatabasePassword
 DBName: todobackend
 VPCSecurityGroups:
 - !Ref ApplicationDatabaseSecurityGroup
 DBSubnetGroupName: !Ref ApplicationDatabaseSubnetGroup
 MultiAZ: "false"
 AvailabilityZone: !Sub ${AWS::Region}a
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-db  ApplicationAutoscalingSecurityGroup:
    Type: AWS::EC2::SecurityGroup
...
...

创建 RDS 资源

前面示例中的配置被认为是定义 RDS 实例的最小配置,如下所述:

  • EngineEngineVersion:数据库引擎,在本例中是 MySQL,以及要部署的主要或次要版本。

  • DBInstanceClass:用于运行数据库的 RDS 实例类型。为了确保您有资格获得免费使用,您可以将其硬编码为db.t2.micro,尽管在生产环境中,您通常会将此属性参数化为更大的实例大小。

  • AllocatedStorageStorageType:定义以 GB 为单位的存储量和存储类型。在第一个示例中,存储类型设置为 10GB 的基于 SSD 的 gp2(通用用途 2)存储。

  • MasterUsernameMasterUserPassword:指定为 RDS 实例配置的主用户名和密码。MasterUserPassword属性引用了一个名为DatabasePassword的输入参数,其中包括一个名为NoEcho的属性,确保 CloudFormation 不会在任何日志中打印出此参数的值。

  • DBName:指定数据库的名称。

  • VPCSecurityGroups:要应用于 RDS 实例的网络通信入口和出口的安全组列表。

  • DBSubnetGroupName:引用AWS::RDS::DBSubnetGroup类型的资源,该资源定义 RDS 实例可以部署到的子网。请注意,即使您只配置了单可用区 RDS 实例,您仍然需要引用您创建的数据库子网组资源中的至少两个子网。在前面的例子中,您引用了一个名为ApplicationDatabaseSubnetGroup的资源,稍后将创建该资源。

  • MultiAZ:定义是否在高可用的多可用区配置中部署 RDS 实例。对于演示应用程序,可以将此设置配置为false,但在实际应用程序中,您通常会将此设置配置为true,至少对于生产环境是这样。

  • AvailabilityZone:定义 RDS 实例将部署到的可用区。此设置仅适用于单可用区实例(即MultiAZ设置为 false 的实例)。在前面的例子中,您使用AWS::Region伪参数来引用本地区域中可用区a

配置支持的 RDS 资源

回顾前面的例子,很明显您需要配置至少两个额外的支持资源用于 RDS 实例:

  • ApplicationDatabaseSecurityGroup:定义应用于 RDS 实例的入站和出站安全规则的安全组资源。

  • ApplicationDatabaseSubnetGroup:RDS 实例可以部署到的子网列表。

除了这些资源,以下示例还演示了我们还需要添加一些资源:

...

Resources:
  ApplicationDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: MySQL
      EngineVersion: 5.7
      DBInstanceClass: db.t2.micro
      AllocatedStorage: 10
      StorageType: gp2
      MasterUsername: todobackend
      MasterUserPassword:
        Ref: DatabasePassword
      DBName: todobackend
      VPCSecurityGroups:
        - !Ref ApplicationDatabaseSecurityGroup
      DBSubnetGroupName: !Ref ApplicationDatabaseSubnetGroup
      MultiAZ: "false"
      AvailabilityZone: !Sub ${AWS::Region}a
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-db
 ApplicationDatabaseSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Application Database Subnet Group
      SubnetIds: !Ref ApplicationSubnets
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-db-subnet-group
  ApplicationDatabaseSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${AWS::StackName} Application Database Security Group
      VpcId: !Ref VpcId
      SecurityGroupEgress:
        - IpProtocol: icmp
          FromPort: -1
          ToPort: -1
          CidrIp: 192.0.2.0/32
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-db-sg
  ApplicationToApplicationDatabaseIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      GroupId: !Ref ApplicationDatabaseSecurityGroup
      SourceSecurityGroupId: !Ref ApplicationAutoscalingSecurityGroup
  ApplicationToApplicationDatabaseEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      GroupId: !Ref ApplicationAutoscalingSecurityGroup
      DestinationSecurityGroupId: !Ref ApplicationDatabaseSecurityGroup
...
...

创建支持的 RDS 资源

在前面的例子中,您首先创建了数据库子网组资源,其中 SubnetIds 属性引用了您在第七章中创建的相同的ApplicationSubnets列表参数,这意味着您的数据库实例将安装在与应用程序 ECS 集群和 EC2 自动扩展组实例相同的子网中。在生产应用程序中,您通常会在单独的专用子网上运行 RDS 实例,理想情况下,出于安全目的,该子网不会连接到互联网,但出于简化示例的目的,我们将利用与应用程序 ECS 集群相同的子网。

接下来,您创建了一个名为ApplicationDatabaseSecurityGroup的安全组资源,并注意到它只包含一个出站规则,有点奇怪的是允许对 IP 地址192.0.2.0/32进行 ICMP 访问。这个 IP 地址是"TEST-NET" IP 地址范围的一部分,是互联网上的无效 IP 地址,用于示例代码和文档。包含这个作为出站规则的原因是,AWS 默认情况下会自动应用一个允许任何规则的出站规则,除非您明确覆盖这些规则,因此通过添加一个允许访问无法路由的 IP 地址的规则,您实际上阻止了 RDS 实例发起的任何出站通信。

最后,请注意,您创建了两个与安全组相关的资源,ApplicationToApplicationDatabaseIngressApplicationToApplicationDatabaseEgress,它们分别具有AWS::EC2::SecurityGroupIngressAWS::EC2::SecurityGroupEgress的资源类型。这些特殊资源避免了在 CloudFormation 中出现的一个问题,即创建了两个需要相互引用的资源之间的循环依赖。在我们的具体场景中,我们希望允许ApplicationAutoscalingSecurityGroup的成员访问ApplicationDatabaseSecurityGroup的成员,并应用适当的安全规则,从应用程序数据库中进行入站访问,并从应用程序实例中进行出站访问。如果您尝试按照以下图表所示的规则进行配置,CloudFormation 将抛出错误并检测到循环依赖。

CloudFormation 循环依赖

为了解决这个问题,以下图表演示了一种替代方法,使用了您在上一个示例中创建的资源。

ApplicationToApplicationDatabaseIngress资源将动态创建ApplicationDatabaseSecurityGroup中的入口规则(由GroupId属性指定),允许从ApplicationAutoscalingSecurityGroup(由SourceSecurityGroupId属性指定)访问 MySQL 端口(TCP/3306)。同样,ApplicationToApplicationDatabaseEgress资源将动态创建ApplicationAutoscalingSecurityGroup中的出口规则(由GroupId属性指定),允许访问属于ApplicationDatabaseSecurityGroup的实例的 MySQL 端口(TCP/3306)(由DestinationSecurityGroupId属性指定)。这最终实现了前面图表中所示配置的意图,但不会在 CloudFormation 中引起任何循环依赖错误。

解决 CloudFormation 循环依赖

使用 CloudFormation 部署 RDS 资源

在上述示例的配置完成后,您现在可以实际更新 CloudFormation 堆栈,其中将添加 RDS 实例和其他支持资源。在执行此操作之前,您需要更新第七章中创建的dev.cfg文件,该文件为您的 CloudFormation 堆栈提供了环境特定的输入参数值。具体来说,您需要为MasterPassword参数指定一个值,如下例所示:

ApplicationDesiredCount=1
ApplicationImageId=ami-ec957491
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
DatabasePassword=my-super-secret-password
VpcId=vpc-f8233a80

向 dev.cfg 文件添加数据库密码

此时,如果您对于以明文提供最终将提交到源代码中的密码感到担忧,那么恭喜您,您对于这种方法感到非常担忧是完全正确的。在接下来的章节中,我们将专门讨论如何安全地管理凭据,但目前我们不会解决这个问题,因此请记住,上述示例中演示的方法并不被认为是最佳实践,我们只会暂时保留这个方法来使您的应用数据库实例正常运行。

在上述示例的配置完成后,您现在可以使用在第七章中使用过的aws cloudformation deploy命令来部署更新后的堆栈。

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file stack.yml \
 --stack-name todobackend --parameter-overrides $(cat dev.cfg) \
 --capabilities CAPABILITY_NAMED_IAM
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend
> aws cloudformation describe-stack-resource --stack-name todobackend \
    --logical-resource-id ApplicationDatabase
{
    "StackResourceDetail": {
        "StackName": "todobackend",
        "StackId": "arn:aws:cloudformation:us-east-1:385605022855:stack/todobackend/297933f0-37fe-11e8-82e0-503f23fb55fe",
        "LogicalResourceId": "ApplicationDatabase",
 "PhysicalResourceId": "ta10udhxgd7s4gf",
        "ResourceType": "AWS::RDS::DBInstance",
        "LastUpdatedTimestamp": "2018-04-04T12:12:13.265Z",
        "ResourceStatus": "CREATE_COMPLETE",
        "Metadata": "{}"
    }
}
> aws rds describe-db-instances --db-instance-identifier ta10udhxgd7s4gf
{
    "DBInstances": [
        {
            "DBInstanceIdentifier": "ta10udhxgd7s4gf",
            "DBInstanceClass": "db.t2.micro",
            "Engine": "mysql",
            "DBInstanceStatus": "available",
            "MasterUsername": "todobackend",
            "DBName": "todobackend",
            "Endpoint": {
                "Address": "ta10udhxgd7s4gf.cz8cu8hmqtu1.us-east-1.rds.amazonaws.com",
                "Port": 3306,
                "HostedZoneId": "Z2R2ITUGPM61AM"
            }
...
...

使用 RDS 资源更新 CloudFormation 堆栈

部署将需要一些时间(通常为 15-20 分钟)才能完成,一旦部署完成,请注意您可以使用aws cloudformation describe-stack-resource命令获取有关ApplicationDatabase资源的更多信息,包括PhysicalResourceId属性,该属性指定了 RDS 实例标识符。

配置应用负载均衡器

我们已经建立了一个 ECS 集群并创建了一个应用程序数据库来存储应用程序数据,接下来我们需要创建前端基础设施,以服务于外部世界对我们的 Docker 应用程序的连接。

在 AWS 中提供这种基础设施的一种流行方法是利用弹性负载均衡服务,该服务提供了多种不同的选项,用于负载均衡连接到您的应用程序:

  • 经典弹性负载均衡器:原始的 AWS 负载均衡器,支持第 4 层(TCP)负载均衡。一般来说,您应该使用较新的应用负载均衡器或网络负载均衡器,它们共同提供了经典负载均衡器的所有现有功能以及更多功能。

  • 应用负载均衡器:一种特别针对基于 Web 的应用程序和 API 的 HTTP 感知负载均衡器。

  • 网络负载均衡器:高性能的第 4 层(TCP)负载均衡服务,通常用于非 HTTP 基于 TCP 的应用程序,或者需要非常高性能的应用程序。

对于我们的目的,我们将利用应用负载均衡器(ALB),这是一个现代的第 7 层负载均衡器,可以根据 HTTP 协议信息执行高级操作,例如基于主机头和基于路径的路由。例如,ALB 可以将针对特定 HTTP 主机头的请求路由到一组特定的目标,并且还可以将针对 some.domain/foo 路径的请求路由到一组目标,将针对 some.domain/bar 路径的请求路由到另一组目标。

AWS ALB 与弹性容器服务集成,支持许多关键的集成功能:

  • 滚动更新:ECS 服务可以以滚动方式部署,ECS 利用负载均衡器连接排空来优雅地将旧版本的应用程序停止服务,终止并替换每个应用程序容器为新版本,然后将新容器添加到负载均衡器,确保更新在没有最终用户中断或影响的情况下进行。

  • 动态端口映射:此功能允许您将容器端口映射到 ECS 容器实例上的动态端口,ECS 负责确保动态端口映射正确地注册到应用负载均衡器。动态端口映射的主要好处是它允许同一应用程序容器的多个实例在单个 ECS 容器实例上运行,从而在维度和扩展 ECS 集群方面提供了更大的灵活性。

  • 健康检查:ECS 使用应用负载均衡器的健康检查来确定您的 Docker 应用程序的健康状况,自动终止和替换任何可能变得不健康并且无法通过负载均衡器健康检查的容器。

应用负载均衡器架构

如果您熟悉旧版经典弹性负载均衡器,您会发现新版应用负载均衡器的架构更加复杂,因为 ALB 支持高级的第 7 层/HTTP 功能。

以下图显示了组成应用负载均衡器的各种组件:

应用负载均衡器组件

以下描述了上图中所示的每个组件:

  • 应用负载均衡器:应用负载均衡器是定义负载均衡器的物理资源,例如负载均衡器应该运行在哪些子网以及允许或拒绝网络流量到负载均衡器或从负载均衡器流出的安全组。

  • 监听器:监听器定义了终端用户和设备连接的网络端口。您可以将监听器视为负载均衡器的前端组件,为传入连接提供服务,最终将被路由到托管应用程序的目标组。每个应用负载均衡器可以包括多个监听器——一个常见的例子是监听器配置,可以为端口80和端口443的网络流量提供服务。

  • 监听规则:监听规则可选择性地根据接收到的主机标头和/或请求路径的值将由监听器接收的 HTTP 流量路由到不同的目标组。例如,如前图所示,您可以将发送到/foo/*请求路径的所有流量路由到一个目标组,而将发送到/bar/*的所有流量路由到另一个目标组。请注意,每个监听器必须定义一个默认目标组,所有未路由到监听规则的流量将被路由到该目标组。

  • 目标组:目标组定义了应该路由到的一个或多个目标的传入连接。您可以将目标组视为负载均衡器的后端组件,负责将接收到的连接负载均衡到目标组中的成员。在将应用程序负载均衡器与 ECS 集成时,目标组链接到 ECS 服务,每个 ECS 服务实例(即容器)被视为单个目标。

配置应用程序负载均衡器

现在您已经了解了应用程序负载均衡器的基本架构,让我们在您的 CloudFormation 模板中定义各种应用程序负载均衡器组件,并继续将新资源部署到您的 CloudFormation 堆栈中。

创建应用程序负载均衡器

以下示例演示了如何添加一个名为ApplicationLoadBalancer的资源,正如其名称所示,它配置了基本的应用程序负载均衡器资源:

...
...
Resources:
 ApplicationLoadBalancer:
 Type: AWS::ElasticLoadBalancingV2::LoadBalancer
 Properties:
 Scheme: internet-facing
 Subnets: !Ref ApplicationSubnets
 SecurityGroups:
 - !Ref ApplicationLoadBalancerSecurityGroup
 LoadBalancerAttributes:
 - Key: idle_timeout.timeout_seconds
 Value : 30
 Tags:
 - Key: Name
 Value: !Sub ${AWS::StackName}-alb
  ApplicationDatabase:
    Type: AWS::RDS::DBInstance
...
...

创建应用程序负载均衡器

在上述示例中,为应用程序负载均衡器资源配置了以下属性:

  • 方案:定义负载均衡器是否具有公共 IP 地址(由值internet-facing指定)或仅具有私有 IP 地址(由值internal指定)

  • 子网:定义了应用程序负载均衡器端点将部署到的子网。在上述示例中,您引用了ApplicationSubnets输入参数,该参数之前已用于 EC2 自动扩展组和 RDS 数据库实例资源。

  • 安全组:指定要应用于负载均衡器的安全组列表,限制入站和出站网络流量。您引用了一个名为ApplicationLoadBalancerSecurityGroup的安全组,稍后将创建该安全组。

  • LoadBalancerAttributes:以键/值格式配置应用程序负载均衡器的各种属性。您可以在docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#load-balancer-attributes找到支持的属性列表,在前面的示例中,您配置了一个属性,将空闲连接超时从默认值60秒减少到30秒。

CloudFormation 的一个特性是能够定义自己的输出,这些输出可用于提供有关堆栈中资源的信息。您可以为堆栈配置一个有用的输出,即应用程序负载均衡器端点的公共 DNS 名称的值,因为这是负载均衡器提供的任何应用程序发布的地方:

...
...
Resources:
  ...
  ...
Outputs:
 PublicURL:
 Description: Public DNS name of Application Load Balancer
 Value: !Sub ${ApplicationLoadBalancer.DNSName}

配置 CloudFormation 输出

在前面的例子中,请注意ApplicationLoadBalancer资源输出一个名为DNSName的属性,该属性返回ApplicationLoadBalancer资源的公共 DNS 名称。

配置应用程序负载均衡器安全组

在前面的例子中,您引用了一个名为ApplicationLoadBalancerSecurityGroup的资源,该资源定义了对应用程序负载均衡器的入站和出站网络访问。

除了这个资源,您还需要以类似的方式创建AWS::EC2::SecurityGroupIngressAWS::EC2::SecurityGroupEgress资源,这些资源确保应用程序负载均衡器可以与您的 ECS 服务应用程序实例通信:

...
...
Resources:
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      Subnets: !Ref ApplicationSubnets
      SecurityGroups:
        - !Ref ApplicationLoadBalancerSecurityGroup
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value : 30
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-alb
  ApplicationLoadBalancerSecurityGroup:
 Type: AWS::EC2::SecurityGroup
 Properties:
 GroupDescription: Application Load Balancer Security Group
 VpcId: !Ref VpcId
 SecurityGroupIngress:
 - IpProtocol: tcp
 FromPort: 80
 ToPort: 80
 CidrIp: 0.0.0.0/0
 Tags:
 - Key: Name
 Value: 
 Fn::Sub: ${AWS::StackName}-alb-sg  ApplicationLoadBalancerToApplicationIngress:
 Type: AWS::EC2::SecurityGroupIngress
 Properties:
 IpProtocol: tcp
 FromPort: 32768
 ToPort: 60999
 GroupId: !Ref ApplicationAutoscalingSecurityGroup
 SourceSecurityGroupId: !Ref ApplicationLoadBalancerSecurityGroup
 ApplicationLoadBalancerToApplicationEgress:
 Type: AWS::EC2::SecurityGroupEgress
 Properties:
 IpProtocol: tcp
 FromPort: 32768
 ToPort: 60999
 GroupId: !Ref ApplicationLoadBalancerSecurityGroup
 DestinationSecurityGroupId: !Ref ApplicationAutoscalingSecurityGroup
  ApplicationDatabase:
    Type: AWS::RDS::DBInstance
...
...

配置应用程序负载均衡器安全组资源

在前面的例子中,您首先创建了ApplicationLoadBalancerSecurityGroup资源,允许从互联网访问端口 80。ApplicationLoadBalancerToApplicationIngressApplicationLoadBalancerToApplicationEgress资源向ApplicationLoadBalancerSecurityGroupApplicationAutoscalingSecurityGroup资源添加安全规则,而不会创建循环依赖(请参阅前面的图表和相关描述),请注意这些规则引用了应用程序自动缩放组的短暂端口范围3276860999,因为我们将为您的 ECS 服务配置动态端口映射。

创建一个监听器

现在,您已经建立了基本的应用程序负载均衡器和相关的安全组资源,可以为应用程序负载均衡器配置一个监听器。对于本书的目的,您只需要配置一个支持 HTTP 连接的单个监听器,但在任何真实的生产用例中,您通常会为任何面向互联网的服务配置 HTTPS 监听器以及相关证书。

以下示例演示了配置一个支持通过端口80(HTTP)访问应用程序负载均衡器的单个监听器:

...
...
Resources:
  ApplicationLoadBalancerHttpListener:
 Type: AWS::ElasticLoadBalancingV2::Listener
 Properties:
 LoadBalancerArn: !Ref ApplicationLoadBalancer
 Protocol: HTTP
 Port: 80
 DefaultActions:
 - TargetGroupArn: !Ref ApplicationServiceTargetGroup
 Type: forward
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      Subnets: !Ref ApplicationSubnets
      SecurityGroups:
        - !Ref ApplicationLoadBalancerSecurityGroup
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value : 30
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-alb
...
...

创建应用程序负载均衡器监听器

在上面的示例中,通过LoadBalancerArn属性将监听器绑定到ApplicationLoadBalancer资源,ProtocolPort属性配置监听器以期望在端口80上接收传入的 HTTP 连接。请注意,您必须定义DefaultActions属性,该属性定义了传入连接将被转发到的默认目标组。

创建目标组

与配置应用程序负载均衡器相关的最终配置任务是配置目标组,该目标组将用于将监听器资源接收的传入请求转发到应用程序实例。

以下示例演示了配置目标组资源:

...
...
Resources:
  ApplicationServiceTargetGroup:
 Type: AWS::ElasticLoadBalancingV2::TargetGroup
 Properties:
 Protocol: HTTP
 Port: 8000
 VpcId: !Ref VpcId
 TargetGroupAttributes:
 - Key: deregistration_delay.timeout_seconds
 Value: 30
  ApplicationLoadBalancerHttpListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      Protocol: HTTP
      Port: 80
      DefaultActions:
        - TargetGroupArn: !Ref ApplicationServiceTargetGroup
          Type: forward
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
...
...

创建目标组

在上面的示例中,为目标组定义了以下配置:

  • Protocol:定义将转发到目标组的连接的协议。

  • Port:指定应用程序将运行的容器端口。默认情况下,todobackend 示例应用程序在端口8000上运行,因此您可以为端口配置此值。请注意,当配置动态端口映射时,ECS 将动态重新配置此端口。

  • VpcId:配置目标所在的 VPC ID。

  • TargetGroupAttributes:定义了目标组的配置属性(docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-attributes),在上面的示例中,deregistration_delay.timeout_seconds属性配置了在滚动部署应用程序期间排空连接时等待取消注册目标的时间。

使用 CloudFormation 部署应用负载均衡器

现在,您的 CloudFormation 模板中已经定义了所有应用负载均衡器组件,您可以使用aws cloudformation deploy命令将这些组件部署到 AWS。

一旦您的堆栈部署完成,如果您打开 AWS 控制台并导航到 EC2 仪表板,在负载均衡部分,您应该能够看到您的新应用负载均衡器资源。

以下截图演示了查看作为部署的一部分创建的应用负载均衡器资源:

查看应用负载均衡器

在前面的截图中,您可以看到应用负载均衡器资源有一个 DNS 名称,这是您的最终用户和设备在访问负载均衡器后面的应用时需要连接的端点名称。一旦您完全部署了堆栈中的所有资源,您将在稍后使用这个名称,但是现在因为您的目标组是空的,这个 URL 将返回一个 503 错误,如下例所示。请注意,您可以通过单击前面截图中的监听器选项卡来查看您的监听器资源,您可以通过单击左侧菜单上的目标组链接来查看您的关联目标组资源。

您会注意到应用负载均衡器的 DNS 名称并不是您的最终用户能够识别或记住的友好名称。在实际应用中,您通常会创建一个 CNAME 或 ALIAS DNS 记录,配置一个友好的规范名称,比如 example.com,指向您的负载均衡器 DNS 名称。有关如何执行此操作的更多详细信息,请参阅docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-elb-load-balancer.html,并注意您可以并且应该使用 CloudFormation 创建 CNAME 和 ALIAS 记录(docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-route53.html#scenario-recordsetgroup-zoneapex)。

> aws cloudformation describe-stacks --stack-name todobackend --query Stacks[].Outputs[]
[
    {
        "OutputKey": "PublicURL",
        "OutputValue": "todob-Appli-5SV5J3NC6AAI-2078461159.us-east-1.elb.amazonaws.com",
        "Description": "Public DNS name of Application Load Balancer"
    }
]
> curl todob-Appli-5SV5J3NC6AAI-2078461159.us-east-1.elb.amazonaws.com
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body bgcolor="white">
<center><h1>503 Service Temporarily Unavailable</h1></center>
</body>
</html>

测试应用负载均衡器端点

请注意,在上面的示例中,您可以使用 AWS CLI 来查询 CloudFormation 堆栈的输出,并获取应用程序负载均衡器的公共 DNS 名称。您还可以在 CloudFormation 仪表板中选择堆栈后,单击“输出”选项卡来查看堆栈的输出。

创建 ECS 任务定义

您现在已经达到了定义使用 CloudFormation 的 ECS 集群并创建了许多支持资源的阶段,包括用于应用程序数据库的 RDS 实例和用于服务应用程序连接的应用程序负载均衡器。

在这个阶段,您已经准备好创建将代表您的应用程序的 ECS 资源,包括 ECS 任务定义和 ECS 服务。

我们将从在 CloudFormation 模板中定义 ECS 任务定义开始,如下例所示:

Parameters:
  ...
  ...
  ApplicationImageId:
    Type: String
    Description: ECS Amazon Machine Image (AMI) ID
 ApplicationImageTag:
 Type: String
 Description: Application Docker Image Tag
 Default: latest  ApplicationSubnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Target subnets for EC2 instances
 ...
  ... 
Resources:
  ApplicationTaskDefinition:
 Type: AWS::ECS::TaskDefinition
 Properties:
 Family: todobackend      Volumes:
 - Name: public          Host:
 SourcePath: /data/public
 ContainerDefinitions:        - Name: todobackend
 Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/todobackend:${ApplicationImageTag}
 MemoryReservation: 395
 Cpu: 245
 MountPoints:
 - SourceVolume: public
 ContainerPath: /public
 Environment:
            - Name: DJANGO_SETTINGS_MODULE
 Value: todobackend.settings_release
 - Name: MYSQL_HOST
 Value: !Sub ${ApplicationDatabase.Endpoint.Address}
 - Name: MYSQL_USER
 Value: todobackend
 - Name: MYSQL_PASSWORD
 Value: !Ref DatabasePassword
 - Name: MYSQL_DATABASE
 Value: todobackend            - Name: SECRET_KEY
 Value: some-random-secret-should-be-here
 Command: 
 - uwsgi
 - --http=0.0.0.0:8000
 - --module=todobackend.wsgi
 - --master
 - --die-on-term
 - --processes=4
 - --threads=2
 - --check-static=/public
 PortMappings:
 - ContainerPort: 8000
              HostPort: 0
 LogConfiguration:
 LogDriver: awslogs
 Options:
 awslogs-group: !Sub /${AWS::StackName}/ecs/todobackend
 awslogs-region: !Ref AWS::Region
 awslogs-stream-prefix: docker
 - Name: collectstatic
          Essential: false
 Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/todobackend:${ApplicationImageTag}
 MemoryReservation: 5
 Cpu: 5          MountPoints:
 - SourceVolume: public
              ContainerPath: /public
 Environment:
 - Name: DJANGO_SETTINGS_MODULE
              Value: todobackend.settings_release
 Command:
 - python3
            - manage.py
            - collectstatic
            - --no-input
 LogConfiguration:
 LogDriver: awslogs
 Options:
 awslogs-group: !Sub /${AWS::StackName}/ecs/todobackend
 awslogs-region: !Ref AWS::Region
 awslogs-stream-prefix: docker  ApplicationLogGroup:
 Type: AWS::Logs::LogGroup
 Properties:
 LogGroupName: !Sub /${AWS::StackName}/ecs/todobackend
 RetentionInDays: 7
  ApplicationServiceTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
...
...

使用 CloudFormation 定义 ECS 任务定义

正如您在上面的示例中所看到的,配置任务定义需要合理的配置量,并需要对任务定义所代表的容器应用程序的运行时配置有详细的了解。

在第一章中,当您创建了示例应用并在本地运行时,您必须使用 Docker Compose 执行类似的操作。以下示例显示了 todobackend 存储库中 Docker Compose 文件中的相关片段:

version: '2.3'

volumes:
  public:
    driver: local

services:
  ...
  ...
  app:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:${APP_VERSION}
    extends:
      service: release
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - public:/public
    healthcheck:
      test: curl -fs localhost:8000
    ports:
      - 8000
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
      - --die-on-term
      - --processes=4
      - --threads=2
      - --check-static=/public
  acceptance:
    extends:
      service: release
    depends_on:
      app:
        condition: service_healthy
    environment:
      APP_URL: http://app:8000
    command:
      - bats 
      - acceptance.bats
  migrate:
    extends:
      service: release
    depends_on:
      db:
        condition: service_healthy
    command:
      - python3
      - manage.py
      - migrate
      - --no-input
  ...
  ...

Todobackend 应用程序 Docker Compose 配置

如果您比较前面两个示例的配置,您会发现您可以使用本地 Docker Compose 配置来确定 ECS 任务定义所需的配置。

现在让我们更详细地检查各种 ECS 任务定义配置属性。

配置 ECS 任务定义家族

您在任务定义中定义的第一个属性是Family属性,它建立了 ECS 任务定义家族名称,并影响 CloudFormation 在您对任务定义进行更改时创建新实例的方式。

回想一下第四章中,ECS 任务定义支持修订的概念,您可以将其视为 ECS 任务定义的特定版本或配置,每当您需要修改任务定义(例如修改镜像标签)时,您可以创建 ECS 任务定义的新修订版本。

因此,如果您的 ECS 任务定义族名称为todobackend,则任务定义的第一个修订版将为todobackend:1,对任务定义的任何后续更改都将导致创建一个新的修订版,例如todobackend:2todobackend:3等。配置 ECS 任务定义资源中的Family属性可确保 CloudFormation 在修改 ECS 任务定义资源时采用创建新修订版的行为。

请注意,如果您未按照之前的示例配置Family属性,CloudFormation 将为族生成一个随机名称,修订版为 1,对任务定义的任何后续更改都将导致创建一个的族,其名称随机,修订版仍为 1。

配置 ECS 任务定义卷

回顾之前示例中的ApplicationTaskDefinition资源,Volumes属性定义了每当 ECS 任务定义的实例部署到 ECS 容器实例时将创建的本地 Docker 卷。参考之前示例中的本地 Docker Compose 配置,您可以看到配置了一个名为public的卷,然后在app服务定义中引用为挂载点。

该卷用于存储静态网页文件,这些文件是通过在本地 Makefile 工作流中运行python3 manage.py collectstatic --no-input命令生成的,并且必须对主应用程序容器可用,因此需要一个卷来确保通过运行此命令生成的文件对应用程序容器可用:

...
...
release:
  docker-compose up --abort-on-container-exit migrate
 docker-compose run app python3 manage.py collectstatic --no-input
  docker-compose up --abort-on-container-exit acceptance
  @ echo App running at http://$$(docker-compose port app 8000 | sed s/0.0.0.0/localhost/g)
...
...

Todobackend Makefile

请注意,在我们的 ECS 任务定义中,我们还需要指定一个主机源路径/data/public,这是我们在上一章中作为 ECS 集群自动扩展组 CloudFormation init 配置的一部分创建的。该文件夹在底层 ECS 容器实例上具有正确的权限,这确保我们的应用程序能够读取和写入公共卷。

配置 ECS 任务定义容器

之前配置的 ECS 任务定义包括ContainerDefinitions属性,该属性定义了与任务定义关联的一个或多个容器的列表。您可以看到有两个容器被定义:

  • todobackend容器:这是主应用程序容器定义。

  • collectstatic容器:这个容器是一个短暂的容器,运行python3 manage.py collectstatic命令来生成本地静态网页文件。与这个容器相关的一个重要配置参数是Essential属性,它定义了 ECS 是否应该尝试重新启动容器,如果它失败或退出(事实上,ECS 将尝试重新启动任务定义中的所有容器,导致主应用容器不必要地停止和重新启动)。鉴于collectstatic容器只打算作为短暂的任务运行,您必须将此属性设置为 false,以确保 ECS 不会尝试重新启动您的 ECS 任务定义容器。

有许多方法可以满足运行收集静态过程以生成静态网页文件的要求。例如,您可以定义一个启动脚本,首先运行收集静态,然后启动应用程序容器,或者您可能希望将静态文件发布到 S3 存储桶,这意味着您将以完全不同的方式运行收集静态过程。

除了 Essential 属性之外,todobackendcollectstatic容器定义的配置属性非常相似,因此我们将在这里仅讨论主todobackend容器定义的属性,并在适当的地方讨论与collectstatic容器定义的任何差异。

  • Image:此属性定义了容器基于的 Docker 镜像的 URI。请注意,我们发布了您在第五章创建的 ECR 存储库的 URI,用于 todobackend 应用程序,并引用了一个名为ApplicationImageTag的堆栈参数,这允许您在部署堆栈时提供适当版本的 Docker 镜像。

  • CpuMemoryReservation:这些属性为您的容器分配 CPU 和内存资源。我们将在接下来的章节中更详细地讨论这些资源,但现在要明白,这些值保留了配置的 CPU 分配和内存,但允许您的容器在可用时使用更多的 CPU 和内存(即“burst”)。请注意,您为 collectstatic 容器分配了最少量的 CPU 和内存,因为它只需要运行很短的时间,而且很可能 ECS 容器实例将有多余的 CPU 和内存容量可用来满足容器的实际资源需求。这避免了为只在一小部分时间内活动的容器保留大量的 CPU 和内存。

  • MountPoints:定义将挂载到容器的 Docker 卷。每个容器都有一个单独的挂载点,将 public 卷挂载到 /public 容器路径,用于托管静态网页文件。

  • Environment:定义将可用于容器的环境变量。参考前面示例中的本地 Docker Compose 配置,您可以看到 release 服务,这是应用服务继承的基本服务定义,指示容器需要将 DJANGO_SETTINGS_MODULE 变量设置为 todobackend.settings_release,并需要定义一些与数据库相关的环境变量,以定义与应用程序数据库的连接。另一个需要的环境变量是 SECRET_KEY 变量,它用于 Django 框架中的各种加密功能,用于驱动 todobackend 应用程序,应该配置为一个秘密的随机值。正如您所看到的,现在我们设置了一个相当非随机的明文值,下一章中,您将学习如何将此值作为加密的秘密注入。

  • Command:定义启动容器时应执行的命令。您可以看到 todobackend 容器定义使用了与本地 Docker Compose 工作流使用的相同的 uwsgi 命令来启动 uwsgi 应用服务器,而 collectstatic 容器使用 python3 manage.py collectstatic 命令来生成要从主应用程序提供的静态网页文件。

  • PortMappings:指定应从容器公开的端口映射。todobackend 容器定义有一个单一的端口映射,指定了容器端口的默认应用程序端口8000,并指定了主机端口值为0,这意味着将使用动态端口映射(请注意,当使用动态端口映射时,您也可以省略 HostPort 参数)。

  • LogConfiguration:配置容器的日志记录配置。在前面的示例中,您使用 awslogs 驱动程序配置 CloudWatch 日志作为日志驱动程序,然后配置特定于此驱动程序的选项。awslogs-group 选项指定日志将输出到的日志组,这引用了在ApplicationLogGroup资源下方定义的日志组的名称。awslogs-stream-prefix 非常重要,因为它修改了容器 ID 的默认日志流命名约定为<prefix-name>/<container-name>/<ecs-task-id>格式,这里的关键信息是 ECS 任务 ID,这是您在使用 ECS 时处理的主要任务标识,而不是容器 ID。

在第七章中,您授予了 ECS 容器实例发布到任何以您的 CloudFormation 堆栈名称为前缀的日志组的能力。只要您的 ECS 任务定义和相关的日志组遵循这个命名约定,Docker 引擎就能够将您的 ECS 任务和容器的日志发布到 CloudWatch 日志中。

使用 CloudFormation 部署 ECS 任务定义

现在您已经定义了 ECS 任务定义,您可以使用现在熟悉的aws cloudformation deploy命令部署它。一旦您的堆栈已经更新,一个名为 todobackend 的新任务定义应该被创建,您可以使用 AWS CLI 查看,如下例所示:

> aws ecs describe-task-definition --task-definition todobackend
{
    "taskDefinition": {
        "taskDefinitionArn": "arn:aws:ecs:us-east-1:385605022855:task-definition/todobackend:1",
        "family": "todobackend",
        "revision": 1,
        "volumes": [
            {
                "name": "public",
                "host": {
                    "sourcePath": "/data/public"
                }
            }
        ],
        "containerDefinitions": [
            {
                "name": "todobackend",
                "image": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest",
                "cpu": 245,
                "memoryReservation": 395,
...
...

验证 todobackend 任务定义

部署 ECS 服务

有了您的 ECS 集群、ECS 任务定义和各种支持资源,现在您可以定义一个 ECS 服务,将您在 ECS 任务定义中定义的容器应用程序部署到您的 ECS 集群中。

以下示例演示了向您的 CloudFormation 模板添加一个AWS::ECS::Service资源的 ECS 服务资源:

...
...
Resources:
  ApplicationService:
 Type: AWS::ECS::Service
 DependsOn:
      - ApplicationAutoscaling
      - ApplicationLogGroup
      - ApplicationLoadBalancerHttpListener
    Properties:
      TaskDefinition: !Ref ApplicationTaskDefinition
      Cluster: !Ref ApplicationCluster
      DesiredCount: !Ref ApplicationDesiredCount
      LoadBalancers:
        - ContainerName: todobackend
          ContainerPort: 8000
          TargetGroupArn: !Ref ApplicationServiceTargetGroup
      Role: !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS 
 DeploymentConfiguration:
 MaximumPercent: 200
 MinimumHealthyPercent: 100
  ApplicationTaskDefinition:
    Type: AWS::ECS::TaskDefinition
...
...

创建 ECS 服务

在前面的例子中,配置的一个有趣方面是DependsOn参数,它定义了堆栈中必须在创建或更新 ECS 服务资源之前创建或更新的其他资源。虽然 CloudFormation 在资源直接引用另一个资源时会自动创建依赖关系,但是一个资源可能对其他资源有依赖,而这些资源与该资源没有直接关系。ECS 服务资源就是一个很好的例子——服务在没有功能的 ECS 集群和相关的 ECS 容器实例(这由ApplicationAutoscaling资源表示)的情况下无法运行,并且在没有ApplicationLogGroup资源的情况下无法写入日志。一个更微妙的依赖关系是ApplicationLoadBalancerHttpListener资源,在与 ECS 服务关联的目标组注册目标之前必须是功能性的。

这里描述了为 ECS 服务配置的各种属性:

  • TaskDefinitionDesiredCountCluster:定义了 ECS 任务定义、ECS 任务数量和服务将部署到的目标 ECS 集群。

  • LoadBalancers:配置了 ECS 服务应该集成的负载均衡器资源。您必须指定容器名称、容器端口和 ECS 服务将注册的目标组 ARN。请注意,您引用了在本章前面创建的ApplicationServiceTargetGroup资源。

  • Role:如果要将 ECS 服务与负载均衡器集成,则只有在这种情况下才需要此属性,并且指定了授予 ECS 服务管理配置的负载均衡器权限的 IAM 角色。在前面的例子中,您引用了一个特殊的 IAM 角色的 ARN,这个角色被称为服务角色(docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html),它在创建 ECS 资源时由 AWS 自动创建。AWSServiceRoleForECS服务角色授予了通常需要的 ECS 权限,包括管理和集成应用程序负载均衡器。

  • DeploymentConfiguration:配置与 ECS 任务定义的新版本滚动部署相关的设置。在部署过程中,ECS 将停止现有容器,并根据 ECS 任务定义的新版本部署新容器,MinimumHealthyPercent设置定义了在部署过程中必须处于服务状态的容器的最低允许百分比,与DesiredCount属性相关。同样,MaximumPercent设置定义了在部署过程中可以部署的容器的最大允许百分比,与DesiredCount属性相关。

使用 CloudFormation 部署 ECS 服务

在设置好 ECS 服务配置后,现在是时候使用aws cloudformation deploy命令将更改部署到您的堆栈了。部署完成后,您的 ECS 服务应该注册到您在本章前面创建的目标组中,如果您浏览到您的应用程序负载均衡器的 URL,您应该看到示例应用程序的根 URL 正在正确加载:

测试 todobackend 应用程序

然而,如果您点击前面截图中显示的todos链接,您将收到一个错误,如下截图所示:

todobackend 应用程序错误

在前面的截图中的问题是,应用程序数据库中预期的数据库表尚未创建,因为我们尚未对应用程序数据库运行数据库迁移。我们将很快学习如何解决这个问题,但在我们这样做之前,我们还有一个与部署 ECS 服务相关的主题要讨论:滚动部署。

ECS 滚动部署

ECS 的一个关键特性是滚动部署,ECS 将自动以滚动方式部署应用程序的新版本,与您配置的负载均衡器一起协调各种操作,以确保您的应用程序成功部署,没有停机时间,也不会影响最终用户。ECS 如何管理滚动部署的过程实际上是非常详细的,以下图表试图以一个图表高层次地描述这个过程:

ECS 滚动部署

在前面的图表中,滚动部署期间发生了以下事件:

  1. 对与 ECS 服务关联的ApplicationTaskDefinition ECS 任务定义进行配置更改,通常是应用程序新版本的镜像标签的更改,但也可能是对任务定义的任何更改。这将导致任务定义的新修订版被创建(在这个例子中是修订版 2)。

  2. ECS 服务配置为使用新的任务定义修订版,当使用 CloudFormation 来管理 ECS 资源时,这是自动发生的。ECS 服务的部署配置决定了 ECS 如何管理滚动部署-在前面的图表中,ECS 必须确保在部署过程中维持配置的期望任务数量的最低 100%,并且可以在部署过程中暂时增加任务数量达到最高 200%。假设期望的任务数量为 1,这意味着 ECS 可以部署基于新任务定义修订版的新 ECS 任务并满足部署配置。请注意,您的 ECS 集群必须有足够的资源来容纳这些部署,并且您负责管理 ECS 集群的容量(即 ECS 不会暂时增加 ECS 集群的容量来容纳部署)。您将在后面的章节中学习如何动态管理 ECS 集群的容量。

  3. 一旦新的 ECS 任务成功启动,ECS 会将新任务注册到配置的负载均衡器(在应用负载均衡器的情况下,任务将被注册到目标组资源)。负载均衡器将执行健康检查来确定新任务的健康状况,一旦确认健康,新的 ECS 任务将被注册到负载均衡器并能够接受传入连接。

  4. ECS 现在指示负载均衡器排水现有的 ECS 任务。负载均衡器将使现有的 ECS 任务停止服务(即不会将任何新连接转发到任务),但会等待一段可配置的时间来使现有连接“排水”或关闭。在此期间,任何对负载均衡器的新连接将被转发到在第 3 步中向负载均衡器注册的新 ECS 任务。

  5. 一旦排水过程完成,负载均衡器将完全从目标组中删除旧的 ECS 任务,ECS 现在可以终止现有的 ECS 任务。一旦这个过程完成,新应用任务定义的部署就完成了。

从这个描述中可以看出,部署过程非常复杂。好消息是,所有这些都可以通过 ECS 开箱即用——您需要理解的是,对任务定义的任何更改都将触发新的部署,并且您的部署配置,由 DeploymentConfiguration 属性确定,可以在滚动部署中对其进行一些控制。

执行滚动部署

现在您了解了滚动部署的工作原理,让我们通过对 ECS 任务定义进行更改并通过 CloudFormation 部署更改的过程来看看它的实际操作,这将触发 ECS 服务的滚动部署。

目前,您的 CloudFormation 配置未指定 ApplicationImageTag 参数,这意味着您的 ECS 任务定义正在使用 latest 的默认值。回到第五章,当您将 Docker 镜像发布到 ECR 时,实际上推送了两个标签——latest 标签和 todobackend 存储库的提交哈希。这为我们提供了一个很好的机会来进一步改进我们的 CloudFormation 模板——通过引用提交哈希,而不是 latest 标签,我们将始终能够在您有新版本的应用程序要部署时触发对 ECS 任务定义的配置更改。

以下示例演示了在 todobackend-aws 存储库中的 dev.cfg 文件中添加 ApplicationImageTag 参数,引用当前发布的 ECR 镜像的提交哈希:

ApplicationDesiredCount=1
ApplicationImageId=ami-ec957491
ApplicationImageTag=97e4abf
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

将 ApplicationImageTag 添加到 dev.cfg 文件

如果您现在使用 aws cloudformation deploy 命令部署更改,尽管您现在引用的镜像与当前 latest 标记的镜像相同,CloudFormation 将检测到这是一个配置更改,创建 ECS 任务定义的新修订版本,并更新 ApplicationService ECS 服务资源,触发滚动部署。

在部署运行时,如果您浏览 ECS 仪表板中的 ECS 服务并选择部署选项卡,如下截图所示,您将看到两个部署——ACTIVE 部署指的是现有的 ECS 任务,而 PRIMARY 部署指的是正在部署的新的 ECS 任务。

ECS 服务滚动部署

最终,一旦滚动部署过程完成,ACTIVE 部署将消失,如果您点击“事件”选项卡,您将看到部署过程中发生的各种事件,这些事件对应了先前的描述:

ECS 服务滚动部署事件

创建 CloudFormation 自定义资源

尽管我们的应用已经部署并运行,但很明显我们有一个问题,即我们尚未运行数据库迁移,这是一个必需的部署任务。我们已经处理了运行另一个部署任务,即收集静态文件,但是数据库迁移应该只作为单个部署任务运行。例如,如果您正在部署服务的多个实例,您不希望为每个部署的实例运行迁移,您只想在每个部署中运行一次迁移,而不管服务中有多少实例。

一个明显的解决方案是在每次部署后手动运行迁移,但是理想情况下,您希望完全自动化您的部署,并确保您有一种机制可以自动运行迁移。CloudFormation 不提供允许您运行一次性 ECS 任务的资源,但是 CloudFormation 的一个非常强大的功能是能够创建自己的自定义资源,这使您能够执行自定义的配置任务。创建自定义资源的好处是您可以将自定义的配置任务纳入部署各种 AWS 服务和资源的工作流程中,使用 CloudFormation 框架来为您管理这一切。

现在让我们学习如何创建一个简单的 ECS 任务运行器自定义资源,该资源将作为创建和更新应用程序环境的一部分来运行迁移任务。

理解 CloudFormation 自定义资源

在开始配置 CloudFormation 自定义资源之前,值得讨论它们实际上是如何工作的,并描述组成自定义资源的关键组件。

以下图表说明了 CloudFormation 自定义资源的工作原理:

CloudFormation 自定义资源

在上图中,当您在 CloudFormation 模板中使用自定义资源时,将发生以下步骤:

  1. 您需要在 CloudFormation 模板中定义自定义资源。自定义资源具有AWS::CloudFormation::CustomResource资源类型,或者是Custom::<resource-name>。当 CloudFormation 遇到自定义资源时,它会查找一个名为ServiceToken的特定属性,该属性提供应该配置自定义资源的 Lambda 函数的 ARN。

  2. CloudFormation 调用 Lambda 函数,并以 JSON 对象的形式将自定义资源请求传递给函数。事件具有请求类型,该类型定义了请求是创建、更新还是删除资源,并包括请求属性,这些属性是您可以在自定义资源定义中定义的自定义属性,将传递给 Lambda 函数。请求的另一个重要属性是响应 URL,它提供了一个预签名的 S3 URL,Lambda 函数应在配置完成后向其发布响应。

  3. Lambda 函数处理自定义资源请求,并根据请求类型和请求属性执行资源的适当配置。配置完成后,函数向自定义资源请求中收到的响应 URL 发布成功或失败的响应,并在创建或更新资源时包含资源标识符。假设响应信号成功,响应可能包括Data属性,该属性可以包含有关已配置的自定义资源的有用信息,可以在 CloudFormation 堆栈的其他位置使用标准的!Sub ${<resource-name>.<data-property>}语法引用,其中<data-property>是响应的Data属性中包含的属性。

  4. 云形成服务轮询响应 URL 以获取响应。一旦收到响应,CloudFormation 解析响应并继续堆栈配置(或在响应指示失败的情况下回滚堆栈)。

创建自定义资源 Lambda 函数

如前一节所讨论的,自定义资源需要您创建一个 Lambda 函数,该函数处理 CloudFormation 发送的传入事件,执行自定义配置操作,然后使用预签名的 S3 URL 响应 CloudFormation。

这听起来相当复杂,但是有许多可用的工具可以使这在相对简单的用例中成为可能,如以下示例所示。

...
...
Resources:
 EcsTaskRunner:
 Type: AWS::Lambda::Function
    DependsOn:
 - EcsTaskRunnerLogGroup
 Properties:
 FunctionName: !Sub ${AWS::StackName}-ecsTasks
 Description: !Sub ${AWS::StackName} ECS Task Runner
 Handler: index.handler
 MemorySize: 128
 Runtime: python3.6
 Timeout: 300
      Role: !Sub ${EcsTaskRunnerRole.Arn}
 Code:
 ZipFile: |
 import cfnresponse
 import boto3

 client = boto3.client('ecs')

 def handler(event, context):
 try:
              print("Received event %s" % event)
              if event['RequestType'] == 'Delete':
                cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, event['PhysicalResourceId'])
                return
              tasks = client.run_task(
                cluster=event['ResourceProperties']['Cluster'],
                taskDefinition=event['ResourceProperties']['TaskDefinition'],
                overrides=event['ResourceProperties'].get('Overrides',{}),
                count=1,
                startedBy=event['RequestId']
              )
              task = tasks['tasks'][0]['taskArn']
              print("Started ECS task %s" % task)
              waiter = client.get_waiter('tasks_stopped')
              waiter.wait(
                cluster=event['ResourceProperties']['Cluster'],
                tasks=[task],
              )
              result = client.describe_tasks(
                cluster=event['ResourceProperties']['Cluster'],
                tasks=[task]
              )
              exitCode = result['tasks'][0]['containers'][0]['exitCode']
              if exitCode > 0:
                print("ECS task %s failed with exit code %s" % (task, exitCode))
                cfnresponse.send(event, context, cfnresponse.FAILED, {}, task)
              else:
                print("ECS task %s completed successfully" % task)
                cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, task)
            except Exception as e:
              print("A failure occurred with exception %s" % e)
              cfnresponse.send(event, context, cfnresponse.FAILED, {})
 EcsTaskRunnerRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Principal:
 Service: lambda.amazonaws.com
 Action:
 - sts:AssumeRole
 Policies:
 - PolicyName: EcsTaskRunnerPermissions
 PolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Sid: EcsTasks
 Effect: Allow
 Action:
 - ecs:DescribeTasks
 - ecs:ListTasks
 - ecs:RunTask
 Resource: "*"
 Condition:
 ArnEquals:
 ecs:cluster: !Sub ${ApplicationCluster.Arn}
 - Sid: ManageLambdaLogs
 Effect: Allow
 Action:
 - logs:CreateLogStream
 - logs:PutLogEvents
 Resource: !Sub ${EcsTaskRunnerLogGroup.Arn}
 EcsTaskRunnerLogGroup:
 Type: AWS::Logs::LogGroup
 Properties:
 LogGroupName: !Sub /aws/lambda/${AWS::StackName}-ecsTasks
 RetentionInDays: 7
  ApplicationService:
    Type: AWS::ECS::Service
...
...

使用 CloudFormation 创建内联 Lambda 函数

前面示例中最重要的方面是EcsTaskRunner资源中的Code.ZipFile属性,它定义了一个内联 Python 脚本,执行自定义资源的自定义配置操作。请注意,这种内联定义代码的方法通常不推荐用于实际用例,稍后我们将创建一个更复杂的自定义资源,其中包括自己的 Lambda 函数代码的源代码库,但为了保持这个示例简单并介绍自定义资源的核心概念,我现在使用了内联方法。

理解自定义资源函数代码

让我们专注于讨论自定义资源函数代码,我已经在之前的示例中将其隔离,并添加了注释来描述各种语句的作用。

# Generates an appropriate CloudFormation response and posts to the pre-signed S3 URL
import cfnresponse
# Imports the AWS Python SDK (boto3) for interacting with the ECS service
import boto3

# Create a client for interacting with the ECS service
client = boto3.client('ecs')

# Lambda functions require a handler function that is passed an event and context object
# The event object contains the CloudFormation custom resource event
# The context object contains runtime information about the Lambda function
def handler(event, context):
  # Wrap the code in a try/catch block to ensure any exceptions generate a failure
  try:
    print("Received event %s" % event)
    # If the request is to Delete the resource, simply return success
    if event['RequestType'] == 'Delete':
      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, event.get('PhysicalResourceId'))
      return
    # Run the ECS task
    # http://boto3.readthedocs.io/en/latest/reference/services/ecs.html#ECS.Client.run_task
    # Requires 'Cluster', 'TaskDefinition' and optional 'Overrides' custom resource properties
    tasks = client.run_task(
      cluster=event['ResourceProperties']['Cluster'],
      taskDefinition=event['ResourceProperties']['TaskDefinition'],
      overrides=event['ResourceProperties'].get('Overrides',{}),
      count=1,
      startedBy=event['RequestId']
    )
    # Extract the ECS task ARN from the return value from the run_task call
    task = tasks['tasks'][0]['taskArn']
    print("Started ECS task %s" % task)

    # Creates a waiter object that polls and waits for ECS tasks to reached a stopped state
    # http://boto3.readthedocs.io/en/latest/reference/services/ecs.html#waiters
    waiter = client.get_waiter('tasks_stopped')
    # Wait for the task ARN that was run earlier to stop
    waiter.wait(
      cluster=event['ResourceProperties']['Cluster'],
      tasks=[task],
    )
    # After the task has stopped, get the status of the task
    # http://boto3.readthedocs.io/en/latest/reference/services/ecs.html#ECS.Client.describe_tasks
    result = client.describe_tasks(
      cluster=event['ResourceProperties']['Cluster'],
      tasks=[task]
    )
    # Get the exit code of the container that ran
    exitCode = result['tasks'][0]['containers'][0]['exitCode']
    # Return failure for non-zero exit code, otherwise return success
    # See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html for more details on cfnresponse module
    if exitCode > 0:
      print("ECS task %s failed with exit code %s" % (task, exitCode))
      cfnresponse.send(event, context, cfnresponse.FAILED, {}, task)
else:
      print("ECS task %s completed successfully" % task)
      cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, task)
  except Exception as e:
    print("A failure occurred with exception %s" % e)
    cfnresponse.send(event, context, cfnresponse.FAILED, {})

使用 CloudFormation 创建内联 Lambda 函数

在高层次上,自定义资源函数接收 CloudFormation 自定义资源事件,并调用 AWS Python SDK 中 ECS 服务的run_task方法,传入 ECS 集群、ECS 任务定义和可选的覆盖以执行。然后函数等待任务完成,检查 ECS 任务的结果,以确定相关容器是否成功完成,然后向 CloudFormation 响应成功或失败。

注意,函数导入了一个名为cfnresponse的模块,这是 AWS Lambda Python 运行时环境中包含的一个模块,提供了一个简单的高级机制来响应 CloudFormation 自定义资源请求。函数还导入了一个名为boto3的模块,它提供了 AWS Python SDK,并用于创建一个与 ECS 服务专门交互的client对象。然后 Lambda 函数定义了一个名为handler的函数,这是传递给 Lambda 函数的新事件的入口点,并注意handler函数必须接受包含 CloudFormation 自定义资源事件的event对象和提供有关 Lambda 环境的运行时信息的context对象。请注意,函数应该只尝试运行 CloudFormation 创建和更新请求的任务,并且当接收到删除自定义资源的请求时,可以简单地返回成功,因为任务是短暂的资源。

前面示例中的代码绝不是生产级代码,并且已经简化为仅处理与成功和失败相关的两个主要场景以进行演示。

了解自定义资源 Lambda 函数资源

现在您了解了 Lambda 函数代码的实际工作原理,让我们专注于您在之前示例中添加的配置的其余部分。

EcsTaskRunner资源定义了 Lambda 函数,其中描述了关键配置属性:

  • FunctionName:函数的名称。要理解的一个重要方面是,用于存储函数日志的关联 CloudWatch 日志组必须遵循/aws/lambda/<function-name>的命名约定,您会看到FunctionName属性与EcsTaskRunnerLogGroup资源的LogGroupName属性匹配。请注意,EcsTaskRunner还必须声明对EcsTaskRunnerLogGroup资源的依赖性,根据DependsOn设置的配置。

  • 处理程序:指定 Lambda 函数的入口点,格式为<module>.<function>。请注意,当使用模块创建的内联代码机制时,用于 Lambda 函数的模块始终被称为index

  • 超时:重要的是要理解,目前 Lambda 的最长超时时间为五分钟(300 秒),这意味着您的函数必须在五分钟内完成,否则它们将被终止。Lambda 函数的默认超时时间为 3 秒,因为部署新的 ECS 任务,运行 ECS 任务并等待任务完成需要时间,因此将此超时时间增加到最大超时时间为 300 秒。

  • 角色:定义要分配给 Lambda 函数的 IAM 角色。请注意,引用的EcsTaskRunnerRole资源必须信任 lambda.amazonaws.com,而且至少每个 Lambda 函数必须具有权限写入关联的 CloudWatch 日志组,如果您想要捕获任何日志。ECS 任务运行器函数需要权限来运行和描述 ECS 任务,并且使用条件配置为仅向堆栈中定义的 ECS 集群授予这些权限。

创建自定义资源

现在你的自定义资源 Lambda 函数和相关的支持资源都已经就位,你可以定义实际的自定义资源对象。对于我们的用例,我们需要定义一个自定义资源,它将在我们的应用容器中运行python3 manage.py migrate命令,并且由于迁移任务与应用数据库交互,任务必须配置各种数据库环境变量,以定义与应用数据库资源的连接。

一种方法是利用之前创建的ApplicationTaskDefinition资源,并指定一个命令覆盖,但一个问题是ApplicationTaskDefinition包括collectstatic容器,我们并不真的想在运行迁移时运行它。为了克服这个问题,你需要创建一个名为MigrateTaskDefinition的单独任务定义,它只包括一个特定运行数据库迁移的容器定义:

...
...
Resources:
 MigrateTaskDefinition:
    Type: AWS::ECS::TaskDefinition
 Properties:
 Family: todobackend-migrate
 ContainerDefinitions:
 - Name: migrate
 Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/todobackend:${ApplicationImageTag}
 MemoryReservation: 5
 Cpu: 5
 Environment:
 - Name: DJANGO_SETTINGS_MODULE
 Value: todobackend.settings_release
 - Name: MYSQL_HOST
 Value: !Sub ${ApplicationDatabase.Endpoint.Address}
 - Name: MYSQL_USER
 Value: todobackend
 - Name: MYSQL_PASSWORD
 Value: !Ref DatabasePassword
 - Name: MYSQL_DATABASE
 Value: todobackend
Command: 
 - python3
 - manage.py
 - migrate
 - --no-input
 LogConfiguration:
 LogDriver: awslogs
 Options:
 awslogs-group: !Sub /${AWS::StackName}/ecs/todobackend
 awslogs-region: !Ref AWS::Region
 awslogs-stream-prefix: docker
  EcsTaskRunner:
    Type: AWS::Lambda::Function
...
...

创建迁移任务定义

在上面的例子中,注意到MigrateTaskDefinition资源需要配置与数据库相关的环境变量,但不需要你之前在ApplicationTaskDefinition资源中配置的卷映射或端口映射。

有了这个任务定义,你现在可以创建你的自定义资源,就像下面的例子所示:

...
...
Resources:
 MigrateTask:
 Type: AWS::CloudFormation::CustomResource
 DependsOn:
 - ApplicationAutoscaling
 - ApplicationDatabase
 Properties:
 ServiceToken: !Sub ${EcsTaskRunner.Arn}
 Cluster: !Ref ApplicationCluster
 TaskDefinition: !Ref MigrateTaskDefinition MigrateTaskDefinition:
     Type: AWS::ECS::TaskDefinition
   ...
   ...
   ApplicationService:
    Type: AWS::ECS::Service
    DependsOn:
      - ApplicationAutoscaling
      - ApplicationLogGroup
      - ApplicationLoadBalancerHttpListener
 - MigrateTask
Properties:
...
...

创建迁移任务自定义资源

在上面的例子中,注意到你的自定义资源是用AWS::CloudFormation::CustomResource类型创建的,你创建的每个自定义资源都必须包括ServiceToken属性,它引用了相关自定义资源 Lambda 函数的 ARN。其余的属性是特定于你的自定义资源函数的,对于我们的情况,至少必须指定要执行的任务的目标 ECS 集群和 ECS 任务定义。注意,自定义资源包括依赖关系,以确保它只在ApplicationAutoscalingApplicationDatabase资源创建后运行,你还需要在本章前面创建的ApplicationService资源上添加一个依赖关系,以便在MigrateTask自定义资源成功完成之前不会创建或更新此资源。

部署自定义资源

现在,您可以使用aws cloudformation deploy命令部署您的更改。在 CloudFormation 堆栈更改部署时,一旦 CloudFormation 启动创建自定义资源并调用您的 Lambda 函数,您可以导航到 AWS Lambda 控制台查看您的 Lambda 函数,并检查函数日志。

CloudFormation 自定义资源在最初工作时可能会耗费大量时间,特别是如果您的代码抛出异常并且没有适当的代码来捕获这些异常并发送失败响应。您可能需要等待几个小时才能超时,因为您的自定义资源抛出了异常并且没有返回适当的失败响应给 CloudFormation。

以下屏幕截图演示了在 AWS Lambda 控制台中查看从 CloudFormation 堆栈创建的todobackend-ecsTasks Lambda 函数:

在 AWS 控制台中查看 Lambda 函数

在上面的屏幕截图中,配置选项卡提供了有关函数的配置详细信息,甚至包括内联代码编辑器,您可以在其中查看、测试和调试您的代码。监控选项卡提供了对函数的各种指标的访问权限,并包括一个有用的跳转到日志链接,该链接可以直接带您到 CloudWatch 日志中函数的日志:

在 AWS 控制台中查看 Lambda 函数日志

在上面的屏幕截图中,START 消息指示函数何时被调用,并且您可以看到生成了一个状态为 SUCCESS 的响应体,该响应体被发布到 CloudFormation 自定义资源响应 URL。

现在是审查 ECS 任务的 CloudWatch 日志的好时机——显示了/todobackend/ecs/todobackend日志组,这是在您的 CloudFormation 堆栈中配置的日志组,用于收集应用程序的所有 ECS 任务日志。请注意,有几个日志流 - 一个用于生成静态任务的collectstatic容器,一个用于运行迁移的migrate容器,以及一个用于主要 todobackend 应用程序的日志流。请注意,每个日志流的末尾都包括 ECS 任务 ID - 这些直接对应于您使用 ECS 控制台或 AWS CLI 与之交互的 ECS 任务 ID:

ECS CloudWatch 日志组

验证应用程序

作为最后的检查,示例应用程序现在应该是完全功能的 - 例如,之前失败的待办事项链接现在应该可以工作,如下面的截图所示。

您可以与 API 交互以添加或删除待办事项,并且所有待办事项现在将持久保存在应用程序数据库中,该数据库在您的堆栈中定义:

Working todobackend application

总结

在本章中,您成功地将示例 Docker 应用程序部署到 AWS 使用 ECS。您学会了如何定义关键的支持应用程序和基础设施资源,包括如何使用 AWS RDS 服务创建应用程序数据库,以及如何将您的 ECS 应用程序与 AWS 弹性负载均衡服务提供的应用程序负载均衡器集成。

有了这些支持资源,您学会了如何创建控制容器运行时配置的 ECS 任务定义,然后通过为示例应用程序创建 ECS 服务来部署您的 ECS 任务定义的实例。您学会了 ECS 任务定义如何定义卷和多个容器定义,并且您使用了这个功能来创建一个单独的非必要容器定义,每当部署您的 ECS 任务定义时,它总是运行并为示例应用程序生成静态网页文件。您还将示例应用程序的 ECS 服务与堆栈中的各种应用程序负载均衡器资源集成,确保可以跨多个 ECS 服务实例进行负载均衡连接到您的应用程序。

尽管您能够成功将应用程序部署为 ECS 服务,但您发现您的应用程序并不完全功能,因为尚未运行为应用程序数据库建立架构和表的数据库迁移。您通过创建 ECS 任务运行器 CloudFormation 自定义资源来解决了这个问题,这使您能够在每次应用程序部署时运行迁移作为单次任务。自定义资源被定义为一个简单的用 Python 编写的 Lambda 函数,它首先在给定的 ECS 集群上为给定的 ECS 任务定义运行任务,等待任务完成,然后根据与任务相关联的容器的退出代码报告任务的成功或失败。

有了这个自定义资源,您的示例应用现在已经完全可用,尽管它仍然存在一些不足之处。在下一章中,我们将解决其中一个不足之处——保密管理和确保密码保持机密——这在安全的、生产级别的 Docker 应用中至关重要。

问题

  1. 真/假:RDS 实例需要您创建至少两个子网的 DB 子网组。

  2. 在配置应用负载均衡器时,哪个组件服务于来自最终用户的前端连接?

  3. 真/假:在创建应用负载均衡器监听器之前,目标组可以接受来自目标的注册。

  4. 在配置允许应用数据库和 ECS 容器实例之间访问的安全组规则时,您收到了关于循环依赖的 CloudFormation 错误。您可以使用哪种类型的资源来克服这个问题?

  5. 您配置了一个包括两个容器定义的 ECS 任务定义。其中一个容器定义执行一个短暂的配置任务然后退出。您发现 ECS 不断地基于这个任务定义重新启动 ECS 服务。您如何解决这个问题?

  6. 您可以配置哪个 CloudFormation 参数来定义对其他资源的显式依赖关系?

  7. 真/假:CloudFormation 自定义资源使用 AWS Lambda 函数执行自定义的配置任务。

  8. 在接收 CloudFormation 自定义资源事件时,您需要处理哪三种类型的事件?

  9. 您创建了一个带有内联 Python 函数的 Lambda 函数,用于执行自定义的配置任务,但是当尝试查看该函数的日志时,没有任何内容被写入 CloudWatch 日志。您确认日志组名称已正确配置给该函数。出现这个问题最可能的原因是什么?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第九章:管理秘密

秘密管理是现代应用程序和系统的关键安全和运营要求。诸如用户名和密码之类的凭据通常用于验证对可能包含私人和敏感数据的资源的访问,因此非常重要的是,您能够实现一个能够以安全方式向您的应用程序提供这些凭据的秘密管理解决方案,而不会将它们暴露给未经授权的方。

基于容器的应用程序的秘密管理具有挑战性,部分原因是容器的短暂性质以及在一次性和可重复基础设施上运行容器的基本要求。长期存在的服务器已经过去了,您可以在本地文件中存储秘密 - 现在您的服务器是可以来来去去的 ECS 容器实例,并且您需要一些机制能够在运行时动态地将秘密注入到您的应用程序中。我们迄今为止在本书中使用的一个天真的解决方案是使用环境变量直接将您的秘密注入到您的应用程序中;然而,这种方法被认为是不安全的,因为它经常会通过各种运营数据源以纯文本形式暴露您的秘密。一个更健壮的解决方案是实现一个安全的凭据存储,您的应用程序可以以安全的方式动态检索其秘密 - 然而,设置您自己的凭据存储可能会很昂贵、耗时,并引入重大的运营开销。

在本章中,您将实现一个简单而有效的秘密管理解决方案,由两个关键的 AWS 服务提供支持 - AWS Secrets Manager 和密钥管理服务或 KMS。这些服务将为您提供一个基于云的安全凭据存储,易于管理、成本效益,并且完全集成了标准的 AWS 安全控制,如 IAM 策略和角色。您将学习如何将支持通过环境变量进行配置的任何应用程序与您的秘密管理解决方案集成,方法是在您的 Docker 映像中创建一个入口脚本,该脚本使用 AWS CLI 动态地检索和安全地注入秘密到您的内部容器环境中,并且还将学习如何在使用 CloudFormation 部署您的环境时,将秘密暴露给 CloudFormation 堆栈中的其他资源。

以下主题将被涵盖:

  • 创建 KMS 密钥

  • 使用 AWS Secrets Manager 创建秘密

  • 在容器启动时注入秘密

  • 使用 CloudFormation 提供秘密

  • 将秘密部署到 AWS

技术要求

以下列出了完成本章所需的技术要求:

  • 对 AWS 帐户具有管理员访问权限

  • 根据第三章的说明配置本地 AWS 配置文件

  • AWS CLI 版本 1.15.71 或更高版本

  • 第八章需要完成,并成功部署示例应用程序到 AWS

以下 GitHub URL 包含本章中使用的代码示例 - github.com/docker-in-aws/docker-in-aws/tree/master/ch9

查看以下视频以查看代码的实际操作:

bit.ly/2LzpEY2

创建 KMS 密钥

任何秘密管理解决方案的关键构建块是使用加密密钥加密您的凭据,这确保了您的凭据的隐私和保密性。AWS 密钥管理服务(KMS)是一项托管服务,允许您创建和控制加密密钥,并提供了一个简单、低成本的解决方案,消除了许多管理加密密钥的操作挑战。KMS 的关键功能包括集中式密钥管理、符合许多行业标准、内置审计和与其他 AWS 服务的集成。

在构建使用 AWS Secrets Manager 的秘密管理解决方案时,您应该在本地 AWS 帐户和区域中创建至少一个 KMS 密钥,用于加密您的秘密。AWS 确实提供了一个默认的 KMS 密钥,您可以在 AWS Secrets Manager 中使用,因此这不是一个严格的要求,但是一般来说,根据您的安全要求,您应该能够创建自己的 KMS 密钥。

您可以使用 AWS 控制台和 CLI 轻松创建 KMS 密钥,但是为了符合采用基础设施即代码的一般主题,我们将使用 CloudFormation 创建一个新的 KMS 密钥。

以下示例演示了在新的 CloudFormation 模板文件中创建 KMS 密钥和 KMS 别名,您可以将其放在 todobackend-aws 存储库的根目录下,我们将其称为kms.yml

AWSTemplateFormatVersion: "2010-09-09"

Description: KMS Keys

Resources:
  KmsKey:
    Type: AWS::KMS::Key
    Properties:
      Description: Custom key for Secrets
      Enabled: true
      KeyPolicy:
        Version: "2012-10-17"
        Id: key-policy
        Statement: 
          - Sid: Allow root account access to key
            Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action:
              - kms:*
            Resource: "*"
  KmsKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/secrets-key
      TargetKeyId: !Ref KmsKey

Outputs:
  KmsKey:
    Description: Secrets Key KMS Key ARN
    Value: !Sub ${KmsKey.Arn}
    Export:
      Name: secrets-key

使用 CloudFormation 创建 KMS 资源

在前面的例子中,您创建了两个资源——一个名为KmsKeyAWS::KMS::Key资源,用于创建新的 KMS 密钥,以及一个名为KmsKeyAliasAWS::KMS::Alias资源,用于为密钥创建别名或友好名称。

KmsKey资源包括一个KeyPolicy属性,该属性定义了授予根帐户对密钥访问权限的资源策略。这是您创建的任何 KMS 密钥的要求,以确保您始终至少有一些方法访问密钥,您可能已经使用该密钥加密了有价值的数据,如果密钥不可访问,这将给业务带来相当大的成本。

如果您通过 AWS 控制台或 CLI 创建 KMS 密钥,根帐户访问策略将自动为您创建。

在前面的示例中,CloudFormation 模板的一个有趣特性是创建了一个 CloudFormation 导出,每当您将Export属性添加到 CloudFormation 输出时就会创建。在前面的示例中,KmsKey输出将Value属性指定的KmsKey资源的 ARN 导出,而Export属性创建了一个 CloudFormation 导出,您可以在其他 CloudFormation 堆栈中引用它,以注入导出的值,而不必明确指定导出的值。稍后在本章中,您将看到如何利用这个 CloudFormation 导出,所以如果现在还不太明白,不用担心。

有了前面示例中的配置,假设您已经将此模板放在名为kms.yml的文件中,现在可以部署新的堆栈,这将导致创建新的 KMS 密钥和 KMS 资源:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file kms.yml --stack-name kms
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - kms
> aws cloudformation list-exports
{
    "Exports": [
        {
            "ExportingStackId": "arn:aws:cloudformation:us-east-1:385605022855:stack/kms/be0a6d20-3bd4-11e8-bf63-50faeaabf0d1",
            "Name": "secrets-key",
            "Value": "arn:aws:kms:us-east-1:385605022855:key/ee08c380-153c-4f31-bf72-9133b41472ad"
        }
    ]
}

使用 CloudFormation 部署 KMS 密钥

在前面的例子中,在创建 CloudFormation 堆栈之后,请注意aws cloudformation list-exports命令现在列出了一个名为secrets-key的单个导出。此导出的值是您堆栈中 KMS 密钥资源的 ARN,您现在可以在其他 CloudFormation 堆栈中使用Fn::ImportValue内部函数来导入此值,只需简单地引用secrets-key的导出名称(例如,Fn::ImportValue: secrets-key)。

在使用 CloudFormation 导出时要小心。这些导出是用于引用静态资源的,您导出的值在未来永远不会改变。一旦另一个堆栈引用了 CloudFormation 导出,您就无法更改该导出的值,也无法删除导出所属的资源或堆栈。CloudFormation 导出对于诸如 IAM 角色、KMS 密钥和网络基础设施(例如 VPC 和子网)等静态资源非常有用,一旦实施后就不会改变。

使用 KMS 加密和解密数据

现在您已经创建了一个 KMS 密钥,您可以使用这个密钥来加密和解密数据。

以下示例演示了使用 AWS CLI 加密简单纯文本值:

> aws kms encrypt --key-id alias/secrets-key --plaintext "Hello World"
{
    "CiphertextBlob": "AQICAHifCoHWAYb859mOk+pmJ7WgRbhk58UL9mhuMIcVAKJ18gHN1/SRRhwQVoVJvDS6i7MoAAAAaTBnBgkqhkiG9w0BBwagWjBYAgEAMFMGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMYm4au5zNZG9wa5ceAgEQgCZdADZyWKTcwDfTpw60kUI8aIAtrECRyW+/tu58bYrMaZFlwVYmdA==",
    "KeyId": "arn:aws:kms:us-east-1:385605022855:key/ee08c380-153c-4f31-bf72-9133b41472ad"
}

使用 KMS 密钥加密数据

在上面的示例中,请注意您必须使用--key-id标志指定 KMS 密钥 ID 或别名,并且每当使用 KMS 密钥别名时,您总是要使用alias/<alias-name>作为前缀。加密数据以 Base64 编码的二进制块形式返回到CiphertextBlob属性中,这也方便地将加密的 KMS 密钥 ID 编码到加密数据中,这意味着 KMS 服务可以解密密文块,而无需您明确指定加密的 KMS 密钥 ID:

> ciphertext=$(aws kms encrypt --key-id alias/secrets-key --plaintext "Hello World" --query CiphertextBlob --output text)
> aws kms decrypt --ciphertext-blob fileb://<(echo $ciphertext | base64 --decode)
{
    "KeyId": "arn:aws:kms:us-east-1:385605022855:key/ee08c380-153c-4f31-bf72-9133b41472ad",
    "Plaintext": "SGVsbG8gV29ybGQ="
}

使用 KMS 密钥解密数据

在上面的示例中,您加密了一些数据,这次使用 AWS CLI 查询和文本输出选项来捕获CiphertextBlob属性值,并将其存储在名为ciphertext的 bash 变量中。然后,您使用aws kms decrypt命令将密文作为二进制文件传递,使用 bash 进程替换将密文的 Base64 解码值传递到二进制文件 URI 指示器(fileb://)中。请注意,返回的Plaintext值不是您最初加密的Hello World值,这是因为Plaintext值是以 Base64 编码格式,下面的示例进一步使用aws kms decrypt命令返回原始明文值:

> aws kms decrypt --ciphertext-blob fileb://<(echo $ciphertext | base64 --decode) \
    --query Plaintext --output text | base64 --decode
Hello World

使用 KMS 密钥解密数据并返回明文值在前两个示例中,base64 --decode命令用于解码 MacOS 和大多数 Linux 平台上的 Base64 值。在一些 Linux 平台(如 Alpine Linux)上,--decode标志不被识别,您必须使用base64 -d命令。

使用 AWS Secrets Manager 创建秘密

您已经建立了一个可以用于加密和解密数据的 KMS 密钥,现在您可以将此密钥与 AWS Secrets Manager 服务集成,这是一个在 2018 年 3 月推出的托管服务,可以让您轻松且具有成本效益地将秘密管理集成到您的应用程序中。

使用 AWS 控制台创建秘密

尽管在过去的几章中我们专注于通过 CloudFormation 创建 AWS 资源,但不幸的是,在撰写本文时,CloudFormation 不支持 AWS Secrets Manager 资源,因此如果您使用 AWS 工具,您需要通过 AWS 控制台或 AWS CLI 来配置您的秘密。

要通过 AWS 控制台创建新秘密,请从服务列表中选择 AWS Secrets Manager,然后单击存储新秘密按钮。选择其他类型的秘密作为秘密类型,指定秘密键和值,并选择您在本章前面创建的secrets-key KMS 密钥,如下面的屏幕截图所示:

使用 AWS Secrets Manager 创建新秘密

在前面的示例中,请注意 AWS Secrets Manager 允许您在单个秘密中存储多个键/值对。这很重要,因为您经常希望将秘密注入为环境变量,因此以键/值格式存储秘密允许您将环境变量名称指定为键,将秘密指定为值。

单击下一步后,您可以配置秘密名称和可选描述:

配置秘密名称和描述

在前面的屏幕截图中,您配置了要称为todobackend/credentials的秘密,我们将在本章后面用于 todobackend 应用程序。一旦您配置了秘密名称和描述,您可以单击下一步,跳过配置自动轮换部分,最后单击存储按钮以完成秘密的创建。

使用 AWS CLI 创建秘密

您还可以使用aws secretsmanager create-secret命令通过 AWS CLI 创建秘密:

> aws secretsmanager create-secret --name test/credentials --kms-key-id alias/secrets-key \
 --secret-string '{"MYSQL_PASSWORD":"some-super-secret-password"}'
{
    "ARN": "arn:aws:secretsmanager:us-east-1:385605022855:secret:test/credentials-l3JdTI",
    "Name": "test/credentials",
    "VersionId": "beab75bd-e9bc-4ac8-913e-aca26f6e3940"
}

使用 AWS CLI 创建秘密

在前面的示例中,请注意您将秘密字符串指定为 JSON 对象,这提供了您之前看到的键/值格式。

使用 AWS CLI 检索秘密

您可以使用aws secretsmanager get-secret-value命令通过 AWS CLI 检索秘密:

> aws secretsmanager get-secret-value --secret-id test/credentials
{
    "ARN": "arn:aws:secretsmanager:us-east-1:385605022855:secret:test/credentials-l3JdTI",
    "Name": "test/credentials",
    "VersionId": "beab75bd-e9bc-4ac8-913e-aca26f6e3940",
    "SecretString": "{\"MYSQL_PASSWORD\":\"some-super-password\"}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": 1523605423.133
}

使用 AWS CLI 获取秘密值

在本章后面,您将为示例应用程序容器创建一个自定义入口脚本,该脚本将使用上面示例中的命令在启动时将秘密注入到应用程序容器环境中。

使用 AWS CLI 更新秘密

回想一下第八章,驱动 todobackend 应用程序的 Django 框架需要配置一个名为SECRET_KEY的环境变量,用于各种加密操作。在本章早些时候,当您创建todobackend/credentials秘密时,您只为用于数据库密码的MYSQL_PASSWORD变量创建了一个键/值对。

让我们看看如何现在更新todobackend/credentials秘密以添加SECRET_KEY变量的值。您可以通过运行aws secretsmanager update-secret命令来更新秘密,引用秘密的 ID 并指定新的秘密值:

> aws secretsmanager get-random-password --password-length 50 --exclude-characters "'\""
{
    "RandomPassword": "E2]eTfO~8Z5)&amp;0SlR-&amp;XQf=yA:B(`,p.B#R6d]a~X-vf?%%/wY"
}
> aws secretsmanager update-secret --secret-id todobackend/credentials \
    --kms-key-id alias/secrets-key \
    --secret-string '{
 "MYSQL_PASSWORD":"some-super-secret-password",
 "SECRET_KEY": "E2]eTfO~8Z5)&amp;0SlR-&amp;XQf=yA:B(`,p.B#R6d]a~X-vf?%%/wY"
 }'
{
    "ARN": "arn:aws:secretsmanager:us-east-1:385605022855:secret:todobackend/credentials-f7AQlO",
    "Name": "todobackend/credentials",
    "VersionId": "cd258b90-d108-4a06-b0f2-849be15f9c33"
}

使用 AWS CLI 更新秘密值

在上面的例子中,请注意您可以使用aws secretsmanager get-random-password命令为您生成一个随机密码,这对于SECRET_KEY变量非常理想。重要的是,您要使用--exclude-characters排除引号和引号字符,因为这些字符通常会导致处理这些值的 bash 脚本出现问题。

然后运行aws secretsmanager update-secret命令,指定正确的 KMS 密钥 ID,并提供一个更新的 JSON 对象,其中包括MYSQL_PASSWORDSECRET_KEY键/值对。

使用 AWS CLI 删除和恢复秘密

可以通过运行aws secretsmanager delete-secret命令来删除秘密,如下例所示:

> aws secretsmanager delete-secret --secret-id test/credentials
{
    "ARN": "arn:aws:secretsmanager:us-east-1:385605022855:secret:test/credentials-l3JdTI",
    "Name": "test/credentials",
    "DeletionDate": 1526198116.323
}

使用 AWS CLI 删除秘密值

请注意,AWS Secrets Manager 不会立即删除您的秘密,而是在 30 天内安排删除该秘密。在此期间,该秘密是不可访问的,但可以在安排的删除日期之前恢复,如下例所示:

> aws secretsmanager delete-secret --secret-id todobackend/credentials
{
    "ARN": "arn:aws:secretsmanager:us-east-1:385605022855:secret:todobackend/credentials-f7AQlO",
    "Name": "todobackend/credentials",
    "DeletionDate": 1526285256.951
}
> aws secretsmanager get-secret-value --secret-id todobackend/credentials
An error occurred (InvalidRequestException) when calling the GetSecretValue operation: You can’t perform this operation on the secret because it was deleted.

> aws secretsmanager restore-secret --secret-id todobackend/credentials
{
    "ARN": "arn:aws:secretsmanager:us-east-1:385605022855:secret:todobackend/credentials-f7AQlO",
    "Name": "todobackend/credentials"
}

> aws secretsmanager get-secret-value --secret-id todobackend/credentials \
 --query SecretString --output text
{
  "MYSQL_PASSWORD":"some-super-secret-password",
  "SECRET_KEY": "E2]eTfO~8Z5)&amp;0SlR-&amp;XQf=yA:B(`,p.B#R6d]a~X-vf?%%/wY"
}

使用 AWS CLI 恢复秘密值

您可以看到,在删除秘密后,您无法访问该秘密,但是一旦使用aws secretsmanager restore-secret命令恢复秘密,您就可以再次访问您的秘密。

在容器启动时注入秘密

在 Docker 中管理秘密的一个挑战是以安全的方式将秘密传递给容器。

下图说明了一种有些天真但可以理解的方法,即使用环境变量直接注入你的秘密作为明文值,这是我们在第八章中采取的方法:

通过环境变量注入密码

这种方法简单易配置和理解,但从安全角度来看并不被认为是最佳实践。当你采用这种方法时,你可以通过检查 ECS 任务定义来以明文查看你的凭据,如果你在 ECS 容器实例上运行docker inspect命令,你也可以以明文查看你的凭据。你也可能无意中使用这种方法记录你的秘密,这可能会无意中与未经授权的第三方共享,因此显然这种方法并不被认为是良好的实践。

另一种被认为更安全的替代方法是将你的秘密存储在安全的凭据存储中,并在应用程序启动时或在需要秘密时检索秘密。AWS Secrets Manager 就是一个提供这种能力的安全凭据存储的示例,显然这是我们在本章将重点关注的解决方案。

当你将你的秘密存储在安全的凭据存储中,比如 AWS Secrets Manager 时,你有两种一般的方法来获取你的秘密,如下图所示:

  • 应用程序注入秘密: 采用这种方法,你的应用程序包括直接与凭据存储进行接口的支持。在这里,你的应用程序可能会寻找一个静态名称的秘密,或者可能会通过环境变量注入秘密名称。在 AWS Secrets Manager 的示例中,这意味着你的应用代码将使用 AWS SDK 来进行适当的 API 调用,以从 AWS Secrets Manager 检索秘密值。

  • Entrypoint 脚本注入秘密:使用这种方法,您可以将应用程序需要的秘密的名称配置为标准环境变量,然后在应用程序之前运行 entrypoint 脚本,从 AWS Secrets Manager 中检索秘密,并将它们作为环境变量注入到内部容器环境中。尽管这听起来与在 ECS 任务定义级别配置环境变量的方法类似,但不同之处在于这发生在容器内部,而外部配置的环境变量应用后,这意味着它们不会暴露给 ECS 控制台或docker inspect命令:

使用凭据存储存储和检索密码

应用程序注入秘密的方法通常从安全角度被认为是最佳方法,但这需要应用程序明确支持与您使用的凭据存储进行交互,这意味着需要额外的开发和成本来支持这种方法。

entrypoint 脚本方法被认为不太安全,因为您在应用程序外部暴露了一个秘密,但秘密的可见性仅限于容器本身,不会在外部可见。使用 entrypoint 脚本确实提供了一个好处,即不需要应用程序专门支持与凭据存储进行交互,使其成为为大多数组织提供运行时秘密的更通用解决方案,而且足够安全,这是我们现在将要关注的方法。

创建一个 entrypoint 脚本

Docker 的ENTRYPOINT指令配置了容器执行的第一个命令或脚本。当与CMD指令一起配置时,ENTRYPOINT命令或脚本被执行,CMD命令作为参数传递给entrypoint脚本。这建立了一个非常常见的模式,即 entrypoint 执行初始化任务,例如将秘密注入到环境中,然后根据传递给脚本的命令参数调用应用程序。

以下示例演示了为 todobackend 示例应用程序创建 entrypoint 脚本,您应该将其放在 todobackend 存储库的根目录中:

> pwd
/Users/jmenga/Source/docker-in-aws/todobackend
> touch entrypoint.sh > tree -L 1 .
├── Dockerfile
├── Makefile
├── docker-compose.yml
├── entrypoint.sh
└── src

1 directory, 4 files

在 Todobackend 存储库中创建一个 entrypoint 脚本

以下示例显示了入口脚本的内容,该脚本将从 AWS Secrets Manager 中注入秘密到环境中:

#!/bin/bash
set -e -o pipefail

# Inject AWS Secrets Manager Secrets
# Read space delimited list of secret names from SECRETS environment variable
echo "Processing secrets [${SECRETS}]..."
read -r -a secrets <<< "$SECRETS"
for secret in "${secrets[@]}"
do
  vars=$(aws secretsmanager get-secret-value --secret-id $secret \
    --query SecretString --output text \
    | jq -r 'to_entries[] | "export \(.key)='\''\(.value)'\''"')
  eval "$vars"
done

# Run application
exec "$@"

定义一个将秘密注入到环境中的入口脚本

在前面的例子中,从SECRETS环境变量创建了一个名为secrets的数组,该数组预计以空格分隔的格式包含一个或多个秘密的名称,这些秘密应该被处理。例如,您可以通过在示例中演示的方式设置SECRETS环境变量来处理名为db/credentialsapp/credentials的两个秘密:

> export SECRETS="db/credentials app/credentials"

定义多个秘密

回顾前面的例子,然后脚本通过循环遍历数组中的每个秘密,使用aws secretsmanager get-secret-value命令获取每个秘密的SecretString值,然后将每个值传递给jq实用程序,将SecretString值解析为 JSON 对象,并生成一个 shell 表达式,将每个秘密键和值导出为环境变量。请注意,jq表达式涉及大量的转义,以确保特殊字符被解释为文字,但这个表达式的本质是为凭据中的每个键值对输出export *key*='*value*'

为了进一步理解这一点,您可以在命令行上使用您之前创建的todobackend/credentials秘钥运行相同的命令:

> aws secretsmanager get-secret-value --secret-id todobackend/credentials \
 --query SecretString --output text \
 | jq -r 'to_entries[] | "export \(.key)='\''\(.value)'\''"'
export MYSQL_PASSWORD='some-super-secret-password'
export SECRET_KEY='E2]eTfO~8Z5)&amp;0SlR-&amp;XQf=yA:B(`,p.B#R6d]a~X-vf?%%/wY'

生成一个将秘钥导出到环境中的 Shell 表达式

在前面的例子中,请注意输出是您将执行的单独的export命令,以将秘密键值对注入到环境中。每个环境变量值也被单引号引起来,以确保 bash 将所有特殊字符视为文字值。

回顾前面的例子,在 for 循环中的eval $vars语句简单地将生成的导出语句作为 shell 命令进行评估,这导致每个键值对被注入到本地环境中。

在单独的变量中捕获aws secretsmanager ...命令替换的输出,可以确保任何在此命令替换中发生的错误将被传递回您的入口脚本。您可能会尝试在 for 循环中只运行一个eval $(aws secretsmanager ..)语句,但采用这种方法意味着如果aws secretsmanager ...命令替换退出并出现错误,您的入口脚本将不会意识到这个错误,并且将继续执行,这可能会导致应用程序出现奇怪的行为。

循环完成后,最终的exec "$@"语句将控制权交给传递给入口脚本的参数,这些参数由特殊的$@ shell 变量表示。例如,如果您的入口脚本被调用为entrypoint python3 manage.py migrate --noinput,那么$@ shell 变量将保存参数python3 manage.py migrate --noinput,最终的exec命令将启动并将控制权交给python3 manage.py migrate --noinput命令。

在容器入口脚本中使用exec "$@"方法非常重要,因为exec确保容器的父进程成为传递给入口点的命令参数。如果您没有使用exec,只是运行命令,那么运行脚本的父 bash 进程将保持为容器的父进程,并且在停止容器时,bash 进程(而不是您的应用程序)将接收到后续的信号以终止容器。通常希望您的应用程序接收这些信号,以便在终止之前优雅地清理。

向 Dockerfile 添加入口脚本

现在,您已经在 todobackend 存储库中建立了一个入口脚本,您需要将此脚本添加到现有的 Dockerfile,并确保使用ENTRYPOINT指令指定脚本作为入口点:

...
...
# Release stage
FROM alpine
LABEL=todobackend

# Install operating system dependencies
RUN apk add --no-cache python3 mariadb-client bash curl bats jq && \
 pip3 --no-cache-dir install awscli

# Create app user
RUN addgroup -g 1000 app && \
    adduser -u 1000 -G app -D app

# Copy and install application source and pre-built dependencies
COPY --from=test --chown=app:app /build /build
COPY --from=test --chown=app:app /app /app
RUN pip3 install -r /build/requirements.txt -f /build --no-index --no-cache-dir
RUN rm -rf /build

# Create public volume
RUN mkdir /public
RUN chown app:app /public
VOLUME /public

# Entrypoint script
COPY entrypoint.sh /usr/bin/entrypoint
RUN chmod +x /usr/bin/entrypoint
ENTRYPOINT ["/usr/bin/entrypoint"]

# Set working directory and application user
WORKDIR /app
USER app

向 Dockerfile 添加入口脚本

在前面的例子中,请注意您修改第一个RUN指令以确保安装了 AWS CLI,方法是添加pip3 --no-cache install awscli命令。

最后,您将入口脚本复制到/usr/bin/entrypoint,确保脚本具有可执行标志,并将脚本指定为镜像的入口点。请注意,您必须以 exec 样式格式配置ENTRYPOINT指令,以确保您在容器中运行的命令作为参数传递给入口脚本(请参阅docs.docker.com/engine/reference/builder/#cmd中的第一个注释)。

现在您的 Dockerfile 已更新,您需要提交更改,重新构建并发布 Docker 镜像更改,如下例所示:

> git add -A
> git commit -a -m "Add entrypoint script"
[master 5fdbe62] Add entrypoint script
 4 files changed, 31 insertions(+), 7 deletions(-)
 create mode 100644 entrypoint.sh
> export AWS_PROFILE=docker-in-aws
> make login
$(aws ecr get-login --no-include-email)
Login Succeeded
> make test && make release docker-compose build --pull release
Building release
Step 1/28 : FROM alpine AS test
latest: Pulling from library/alpine...
...
docker-compose run app bats acceptance.bats
Starting todobackend_db_1 ... done
Processing secrets []...
1..4
ok 1 todobackend root
ok 2 todo items returns empty list
ok 3 create todo item
ok 4 delete todo item
App running at http://localhost:32784
> make publish docker-compose push release
Pushing release (385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest)...
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend]
fdc98d6948f6: Pushed
9f33f154b3fa: Pushed
d8aedb2407c9: Pushed
f778da37eed6: Pushed
05e5971d2995: Pushed
4932bb9f39a5: Pushed
fa63544c9f7e: Pushed
fd3b38ee8bd6: Pushed
cd7100a72410: Layer already exists
latest: digest: sha256:5d456c61dd23728ec79c281fe5a3c700370382812e75931b45f0f5dd1a8fc150 size: 2201
Pushing app (385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:5fdbe62)...
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend]
fdc98d6948f6: Layer already exists
9f33f154b3fa: Layer already exists
d8aedb2407c9: Layer already exists
f778da37eed6: Layer already exists
05e5971d2995: Layer already exists
4932bb9f39a5: Layer already exists
fa63544c9f7e: Layer already exists
fd3b38ee8bd6: Layer already exists
cd7100a72410: Layer already exists
34d86eb: digest: sha256:5d456c61dd23728ec79c281fe5a3c700370382812e75931b45f0f5dd1a8fc150 size: 2201

发布更新的 Docker 镜像

在上面的示例中,当 Docker 镜像发布时,请注意应用程序服务的 Docker 标签(在我的示例中为5fdbe62,实际哈希值会因人而异),您可以从第一章中回忆起,它指定了源代码库的 Git 提交哈希。您将在本章后面需要此标签,以确保您可以部署您的更改到在 AWS 中运行的 todobackend 应用程序。

使用 CloudFormation 提供秘密

您已在 AWS Secrets Manager 中创建了一个秘密,并已添加了支持使用入口脚本将秘密安全地注入到容器中的功能。请记住,入口脚本会查找一个名为SECRETS的环境变量,而您 CloudFormation 模板中的ApplicationTaskDefinitionMigrateTaskDefinition资源目前正在直接注入应用程序数据库。为了支持在您的堆栈中使用秘密,您需要配置 ECS 任务定义,以包括SECRETS环境变量,并配置其名称为您的秘密名称,并且您还需要确保您的容器具有适当的 IAM 权限来检索和解密您的秘密。

另一个考虑因素是您的ApplicationDatabase资源的密码是如何配置的——目前配置为使用堆栈参数输入的密码;但是,您的数据库现在需要能够以某种方式从您新创建的秘密中获取其密码。

配置 ECS 任务定义以使用秘密

首先要处理重新配置 ECS 任务定义以使用您新创建的秘密。您的容器现在包括一个入口脚本,该脚本将从 AWS Secrets Manager 中检索秘密,并且在更新各种 ECS 任务定义以将您的秘密名称导入为环境变量之前,您需要确保您的容器具有执行此操作的正确权限。虽然您可以将此类权限添加到应用于 EC2 实例级别的 ECS 容器实例角色,但更安全的方法是创建特定的 IAM 角色,您可以将其分配给您的容器,因为您可能会与多个应用程序共享 ECS 集群,并且不希望从在集群上运行的任何容器中授予对您秘密的访问权限。

ECS 包括一个名为 IAM 任务角色的功能(docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html),它允许您在 ECS 任务定义级别授予 IAM 权限,并且在我们只想要将对 todobackend 秘密的访问权限授予 todobackend 应用程序的情况下非常有用。以下示例演示了创建授予这些特权的 IAM 角色:

...
...
Resources:
  ...
  ...
  ApplicationTaskRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Principal:
 Service: ecs-tasks.amazonaws.com
 Action:
 - sts:AssumeRole
 Policies:
 - PolicyName: SecretsManagerPermissions
 PolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Sid: GetSecrets
 Effect: Allow
 Action:
 - secretsmanager:GetSecretValue
 Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:todobackend/*
 - Sid: DecryptSecrets
 Effect: Allow
 Action:
 - kms:Decrypt
 Resource: !ImportValue secrets-key
  ApplicationTaskDefinition:
    Type: AWS::ECS::TaskDefinition
...
...

创建 IAM 任务角色

在前面的示例中,您创建了一个名为ApplicationTaskRole的新资源,其中包括一个AssumeRolePolicyDocument属性,该属性定义了可以承担该角色的受信任实体。请注意,这里的主体是ecs-tasks.amazonaws.com服务,这是您的容器在尝试使用 IAM 角色授予的权限访问 AWS 资源时所假定的服务上下文。该角色包括一个授予secretsmanager:GetSecretValue权限的策略,这允许您检索秘密值,这个权限被限制为所有以todobackend/为前缀命名的秘密的 ARN。如果您回顾一下之前的示例,当您通过 AWS CLI 创建了一个测试秘密时,您会发现秘密的 ARN 包括 ARN 末尾的随机值,因此您需要在 ARN 中使用通配符,以确保您具有权限,而不考虑这个随机后缀。请注意,该角色还包括对secrets-key KMS 密钥的Decrypt权限,并且您使用!ImportValueFn::ImportValue内部函数来导入您在第一个示例中导出的 KMS 密钥的 ARN。

有了ApplicationTaskRole资源,以下示例演示了如何重新配置stack.yml文件中的todobackend-aws存储库中的ApplicationTaskDefinitionMigrateTaskDefinition资源:

Parameters:
  ...
  ...
  ApplicationSubnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Target subnets for EC2 instances
 # The DatabasePassword parameter has been removed
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Target VPC
 ...
  ... 
Resources:
  ...
  ...
  MigrateTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: todobackend-migrate
 TaskRoleArn: !Sub ${ApplicationTaskRole.Arn}
      ContainerDefinitions:
        - Name: migrate
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/todobackend:${ApplicationImageTag}
          MemoryReservation: 5
          Cpu: 5
          Environment:
            - Name: DJANGO_SETTINGS_MODULE
              Value: todobackend.settings_release
            - Name: MYSQL_HOST
              Value: !Sub ${ApplicationDatabase.Endpoint.Address}
            - Name: MYSQL_USER
              Value: todobackend
            - Name: MYSQL_DATABASE
              Value: todobackend
            # The MYSQL_PASSWORD variable has been removed
 - Name: SECRETS
 Value: todobackend/credentials
            - Name: AWS_DEFAULT_REGION
              Value: !Ref AWS::Region  ...
  ...
  ApplicationTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: todobackend
 TaskRoleArn: !Sub ${ApplicationTaskRole.Arn}
      Volumes:
        - Name: public
      ContainerDefinitions:
        - Name: todobackend
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/todobackend:${ApplicationImageTag}
          MemoryReservation: 395
          Cpu: 245
          MountPoints:
            - SourceVolume: public
              ContainerPath: /public
          Environment:- Name: DJANGO_SETTINGS_MODULE
              Value: todobackend.settings_release
            - Name: MYSQL_HOST
              Value: !Sub ${ApplicationDatabase.Endpoint.Address}
            - Name: MYSQL_USER
              Value: todobackend
            - Name: MYSQL_DATABASE
              Value: todobackend
 # The MYSQL_PASSWORD and SECRET_KEY variables have been removed            - Name: SECRETS
 Value: todobackend/credentials
            - Name: AWS_DEFAULT_REGION
              Value: !Ref AWS::Region
...
...

配置 ECS 任务定义以使用秘密

在上面的示例中,您配置每个任务定义使用 IAM 任务角色通过TaskRoleArn属性,该属性引用了您在上一个示例中创建的ApplicationTaskRole资源。接下来,您添加新入口脚本在您的 Docker 镜像中期望的SECRETS环境变量,并删除先前从 AWS Secrets Manager 服务中检索的MYSQL_PASSWORDSECRET_KEY变量。请注意,您需要包括一个名为AWS_DEFAULT_REGION的环境变量,因为这是 AWS CLI 所需的,以确定您所在的区域。

因为您不再将数据库密码作为参数注入到堆栈中,您还需要更新 todobackend-aws 存储库中的dev.cfg文件,并且还要指定您在之前示例中发布的更新的 Docker 镜像标记:

ApplicationDesiredCount=1
ApplicationImageId=ami-ec957491
ApplicationImageTag=5fdbe62
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

更新输入参数

在上面的示例中,DatabasePassword=my-super-secret-password行已被删除,并且ApplicationImageTag参数的值已被更新,引用了您新更新的 Docker 镜像上标记的提交哈希。

向其他资源公开秘密

您已更新了 ECS 任务定义,使您的应用容器现在将从 AWS Secrets Manager 中提取秘密并将它们注入为环境变量。这对于您的 Docker 镜像效果很好,因为您可以完全控制您的镜像的行为,并且可以添加诸如入口脚本之类的功能来适当地注入秘密。对于依赖这些秘密的其他资源,您没有这样的能力,例如,您堆栈中的ApplicationDatabase资源定义了一个 RDS 实例,截至撰写本文时,它不包括对 AWS Secrets Manager 的本地支持。

解决这个问题的一个方法是创建一个 CloudFormation 自定义资源,其工作是查询 AWS Secrets Manager 服务并返回与给定秘密相关的秘密值。因为自定义资源可以附加数据属性,所以您可以在其他资源中引用这些属性,提供一个简单的机制将您的秘密注入到任何不原生支持 AWS Secrets Manager 的 CloudFormation 资源中。如果您对这种方法的安全性有疑问,CloudFormation 自定义资源响应规范(docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html)包括一个名为NoEcho的属性,该属性指示 CloudFormation 不通过控制台或日志信息公开数据属性。通过设置此属性,您可以确保您的秘密不会因查询 CloudFormation API 或审查 CloudFormation 日志而无意中暴露。

创建一个 Secrets Manager Lambda 函数

以下示例演示了向您的 CloudFormation 堆栈添加一个 Lambda 函数资源,该函数查询 AWS Secrets Manager 服务,并返回给定秘密名称和秘密值内键/值对中的目标键的秘密值:

...
...
Resources:
  SecretsManager:
 Type: AWS::Lambda::Function
 DependsOn:
 - SecretsManagerLogGroup
 Properties:
 FunctionName: !Sub ${AWS::StackName}-secretsManager
 Description: !Sub ${AWS::StackName} Secrets Manager
 Handler: index.handler
 MemorySize: 128
 Runtime: python3.6
 Timeout: 300
 Role: !Sub ${SecretsManagerRole.Arn}
 Code:
 ZipFile: |
 import cfnresponse, json, sys, os
 import boto3

 client = boto3.client('secretsmanager')

 def handler(event, context):
            sys.stdout = sys.__stdout__
 try:
 print("Received event %s" % event)
 if event['RequestType'] == 'Delete':
 cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, event['PhysicalResourceId'])
 return
 secret = client.get_secret_value(
 SecretId=event['ResourceProperties']['SecretId'],
 )
 credentials = json.loads(secret['SecretString'])
              # Suppress logging output to ensure credential values are kept secure
              with open(os.devnull, "w") as devnull:
                sys.stdout = devnull
                cfnresponse.send(
                  event, 
                  context, 
                  cfnresponse.SUCCESS,
                  credentials, # This dictionary will be exposed to CloudFormation resources
                  secret['VersionId'], # Physical ID of the custom resource
                  noEcho=True
                )
 except Exception as e:
 print("A failure occurred with exception %s" % e)
 cfnresponse.send(event, context, cfnresponse.FAILED, {})
 SecretsManagerRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Principal:
 Service: lambda.amazonaws.com
 Action:
 - sts:AssumeRole
 Policies:
 - PolicyName: SecretsManagerPermissions
 PolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Sid: GetSecrets
 Effect: Allow
 Action:
 - secretsmanager:GetSecretValue
 Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:todobackend/*
            - Sid: DecryptSecrets
              Effect: Allow
              Action:
 - kms:Decrypt
 Resource: !ImportValue secrets-key
- Sid: ManageLambdaLogs
 Effect: Allow
 Action:
 - logs:CreateLogStream
 - logs:PutLogEvents
 Resource: !Sub ${SecretsManagerLogGroup.Arn}
SecretsManagerLogGroup:
 Type: AWS::Logs::LogGroup
 Properties:
 LogGroupName: !Sub /aws/lambda/${AWS::StackName}-secretsManager
 RetentionInDays: 7...
  ...

添加一个 Secrets Manager CloudFormation 自定义资源函数

前面示例的配置与您在第八章中执行的配置非常相似,当时您创建了EcsTaskRunner自定义资源函数。在这里,您创建了一个SecretsManager Lambda 函数,配有一个关联的SecretsManagerRole IAM 角色,该角色授予了从 AWS Secrets Manager 检索和解密密钥的能力,类似于之前创建的ApplicationTaskRole,以及一个SecretsManagerLogGroup资源,用于收集来自 Lambda 函数的日志。

函数代码比 ECS 任务运行器代码更简单,期望传递一个名为 SecretId 的属性给自定义资源,该属性指定秘密的 ID 或名称。函数从 AWS Secrets Manager 获取秘密,然后使用 json.loads 方法将秘密键值对加载为名为 credentials 的 JSON 对象变量。然后,函数将 credentials 变量返回给 CloudFormation,这意味着每个凭据都可以被堆栈中的其他资源访问。请注意,您使用 with 语句来确保由 cfnresponse.send 方法打印的响应数据被抑制,通过将 sys.stdout 属性设置为 /dev/null,因为响应数据包含您不希望以明文形式暴露的秘密值。这种方法需要一些小心,您需要在 handler 方法的开头将 sys.stdout 属性恢复到其默认状态(由 sys.__stdout__ 属性表示),因为您的 Lambda 函数运行时可能会在多次调用之间被缓存。

自定义资源函数代码可以扩展到将秘密部署到 AWS Secrets Manager。例如,您可以将预期的秘密值的 KMS 加密值作为输入,甚至生成一个随机的秘密值,然后部署和公开此凭据给其他资源。

创建一个秘密自定义资源

现在您已经为自定义资源准备了一个 Lambda 函数,您可以创建实际的自定义资源,该资源将提供对存储在 AWS Secrets Manager 中的秘密的访问。以下示例演示了在本章前面创建的 todobackend/credentials 密钥的自定义资源,然后从您的 ApplicationDatabase 资源中访问该密钥:

...
...
Resources:
  Secrets:
 Type: AWS::CloudFormation::CustomResource
 Properties:
 ServiceToken: !Sub ${SecretsManager.Arn}
 SecretId: todobackend/credentials
  SecretsManager:
    Type: AWS::Lambda::FunctionResources:
  ...
  ...
  ApplicationDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: MySQL
      EngineVersion: 5.7
      DBInstanceClass: db.t2.micro
      AllocatedStorage: 10
      StorageType: gp2
      MasterUsername: todobackend
 MasterUserPassword: !Sub ${Secrets.MYSQL_PASSWORD} ...
  ...

添加一个 Secrets Manager 自定义资源

在前面的示例中,您创建了一个名为 Secrets 的自定义资源,它通过 ServiceToken 属性引用 SecretsManager 函数,然后通过 SecretId 属性传递要检索的凭据的名称。然后,现有的 ApplicationDatabase 资源上的 MasterUserPassword 属性被更新为引用通过 Secrets 资源可访问的 MYSQL_PASSWORD 键,该键返回存储在 todobackend/credentials 密钥中的正确密码值。

将秘密部署到 AWS

此时,您已准备好部署对 CloudFormation 堆栈的更改,您可以使用我们在过去几章中使用的aws cloudformation deploy命令来执行:

> aws cloudformation deploy --template-file stack.yml \
 --stack-name todobackend --parameter-overrides $(cat dev.cfg) \
 --capabilities CAPABILITY_NAMED_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend

部署 CloudFormation 堆栈更改

部署将影响以下资源:

  • 支持自定义资源的资源将首先被创建,同时将应用于 ECS 任务定义的更改。

  • 名为Secrets的自定义资源将被创建,一旦创建,将公开todobackend/credentials密钥的键/值对给其他 CloudFormation 资源。

  • ApplicationDatabase资源将被更新,MasterPassword属性将根据todobackend/credentials密钥中MYSQL_PASSWORD变量的值进行更新。

  • MigrateTask自定义资源将根据与关联的MigrateTaskDefinition的更改进行更新,并运行一个新任务,该任务使用更新后的 todobackend 镜像中的入口脚本将todobackend/credentials密钥中的每个键/值对导出到环境中,其中包括访问应用程序数据库所需的MYSQL_PASSWORD变量。

  • ApplicationService资源将根据与关联的ApplicationTaskDefinition的更改进行更新,并且类似于MigrateTask,每个应用程序实例现在在启动时将注入与todobackend/credentials密钥相关的环境变量。更新将触发ApplicationService的滚动部署,这将使新版本的应用程序投入使用,然后排空和移除旧版本的应用程序,而不会造成任何中断。

假设部署成功,您应该能够验证应用程序仍然成功运行,并且可以列出、添加和删除待办事项。

您还应该验证您的SecretsManagerFunction资源未记录秘密的明文值—以下屏幕截图显示了此功能的日志输出,并且您可以看到它抑制了发送回 CloudFormation 的成功响应的日志记录:

查看 Secrets Manager 功能的日志输出

摘要

秘密管理对于短暂的 Docker 应用程序来说是一个挑战,其中预先配置的长时间运行的服务器并不再是一个选项,因为凭据存储在配置文件中,直接将密码作为外部配置的环境变量注入被认为是一种糟糕的安全实践。这需要一个秘密管理解决方案,使您的应用程序可以动态地从安全凭据存储中获取秘密,在本章中,您成功地使用 AWS Secrets Manager 和 KMS 服务实现了这样的解决方案。

您学会了如何创建 KMS 密钥,用于加密和解密机密信息,并由 AWS Secrets Manager 使用,以确保其存储的秘密的隐私和保密性。接下来,您将介绍 AWS Secrets Manager,并学习如何使用 AWS 控制台和 AWS CLI 创建秘密。您学会了如何在秘密中存储多个键/值对,并介绍了诸如删除保护之类的功能,其中 AWS Secrets Manager 允许您在 30 天内恢复先前删除的秘密。

有了样本应用程序的凭据存储位置,您学会了如何在容器中使用入口点脚本,在容器启动时动态获取和注入秘密值,使用简单的 bash 脚本与 AWS CLI 结合,将一个或多个秘密值作为变量注入到内部容器环境中。尽管这种方法被认为比应用程序直接获取秘密不太安全,但它的优势在于可以应用于支持环境变量配置的任何应用程序,使其成为一个更加通用的解决方案。

在为您的应用程序发布更新的 Docker 镜像后,您更新了 ECS 任务定义,以注入每个容器应检索的秘密的名称,然后创建了一个简单的自定义资源,能够将您的秘密暴露给不支持 AWS Secrets Manager 的其他类型的 AWS 资源,并且没有机制(如容器入口点脚本)来检索秘密。您确保配置了此自定义资源,以便它不会通过日志或其他形式的操作事件透露您的凭据,并更新了应用程序数据库资源,以通过此自定义资源检索应用程序的数据库密码。

有了一个安全管理解决方案,您已经解决了前几章的核心安全问题,在下一章中,您将学习如何解决应用程序的另一个安全问题,即能够独立隔离网络访问并在每个容器或 ECS 任务定义基础上应用网络访问规则。

问题

  1. 真/假:KMS 服务要求您提供自己的私钥信息。

  2. KMS 的哪个特性允许您为密钥指定逻辑名称,而不是基于 UUID 的标识符?

  3. 您想避免手动配置在多个 CloudFormation 堆栈中使用的 KMS 密钥的 ARN。假设您在单独的 CloudFormation 堆栈中定义了 KMS 密钥,您可以使用哪个 CloudFormation 功能来解决这个问题?

  4. 真/假:当您从 AWS Secrets Manager 中删除一个秘密时,您永远无法恢复该秘密。

  5. 在入口脚本中,您通常会使用哪些工具来从 AWS Secrets Manager 检索秘密并将秘密中的键/值对转换为适合导出到容器环境的形式?

  6. 在容器入口脚本中收到一个错误,指示您没有足够的权限访问一个秘密。您检查了 IAM 角色,并确认它对该秘密允许了一个单一的secretsmanager:GetSecretValue权限。您需要授予哪些其他权限来解决这个问题?

  7. 在处理不应公开为明文值的敏感数据时,应设置哪个 CloudFormation 自定义资源属性?

  8. 在访问 AWS 资源的容器入口脚本中收到错误消息“您必须配置区域”。您应该向容器添加哪个环境变量?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十章:隔离网络访问

应用安全的基本组件是控制网络访问的能力,无论是应用内部还是应用外部。AWS 提供了 EC2 安全组,可以在每个网络接口上应用到您的 EC2 实例。这种机制对于部署到 EC2 实例的传统应用程序非常有效,但对于容器应用程序来说效果不佳,因为它们通常在共享的 EC2 实例上运行,并通过 EC2 实例上的共享主机接口进行通信。对于 ECS 来说,直到最近的方法是为您需要支持在给定 ECS 容器实例上运行的所有容器的网络安全需求应用两个安全组,这降低了安全规则的有效性,对于具有高安全要求的应用程序来说是不可接受的。直到最近,这种方法的唯一替代方案是为每个应用程序构建专用的 ECS 集群,以确保满足应用程序的安全要求,但这会增加额外的基础设施和运营开销。

AWS 在 2017 年底宣布了一项名为 ECS 任务网络的功能,引入了动态分配弹性网络接口(ENI)给您的 ECS 容器实例的能力,这个 ENI 专门用于给定的 ECS 任务。这使您能够为每个容器应用程序创建特定的安全组,并在同一 ECS 容器实例上同时运行这些应用程序,而不会影响安全性。

在本章中,您将学习如何配置 ECS 任务网络,这需要您了解 ECS 任务网络的工作原理,为任务网络配置 ECS 任务定义,并创建部署与您的任务网络启用的 ECS 任务定义相关联的 ECS 服务。与您在上一章中配置的 ECS 任务角色功能相结合,这将使您能够构建高度安全的容器应用程序环境,以在 IAM 权限和网络安全级别上执行隔离和分离。

将涵盖以下主题:

  • 理解 ECS 任务网络

  • 配置 NAT 网关

  • 配置 ECS 任务网络

  • 部署和测试 ECS 任务网络

技术要求

以下列出了完成本章所需的技术要求:

  • 对 AWS 账户的管理员访问

  • 根据第三章的说明配置本地 AWS 配置文件

  • AWS CLI 1.15.71 或更高版本

  • 完成第九章,并成功将示例应用程序部署到 AWS

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch10

观看以下视频以查看代码的实际操作:

bit.ly/2MUBJfs

理解 ECS 任务网络

在幕后,ECS 任务网络实际上是一个相当复杂的功能,它依赖于许多 Docker 网络功能,并需要对 Docker 网络有详细的了解。作为在 AWS 中使用 ECS 设计、构建和部署容器环境的人,好消息是你不必理解这个细节层次,你只需要对 ECS 任务网络如何工作有一个高层次的理解。因此,在本节中,我将提供 ECS 任务网络如何工作的高层次概述,但是,如果你对 ECS 任务网络如何工作感兴趣,这篇来自 AWS 的博客文章提供了更多信息(aws.amazon.com/blogs/compute/under-the-hood-task-networking-for-amazon-ecs/)。

Docker 桥接网络

要理解 ECS 任务网络,有助于了解 Docker 网络和 ECS 容器的标准配置是如何默认工作的。默认情况下,ECS 任务定义配置为 Docker 桥接网络模式,如下图所示:

Docker 桥接网络

在上图中,您可以看到每个 ECS 任务都有自己的专用网络接口,这是由 Docker 引擎在创建 ECS 任务容器时动态创建的。Docker 桥接接口是一个类似于以太网交换机的第 2 层网络组件,它在 Docker 引擎主机内部连接每个 Docker 容器网络接口。

请注意,每个容器都有一个 IP 地址,位于172.16.0.x子网内,而 ECS 容器实例的外部 AWS 公共网络和弹性网络接口的 IP 地址位于172.31.0.x子网内,您可以看到所有容器流量都通过单个主机网络接口路由,在 AWS EC2 实例的情况下,这是分配给实例的默认弹性网络接口。弹性网络接口(ENI)是一种 EC2 资源,为您的 VPC 子网提供网络连接,并且是您认为每个 EC2 实例使用的标准网络接口。

ECS 代理也作为一个 Docker 容器运行,与其他容器不同的是它以主机网络模式运行,这意味着它使用主机操作系统的网络接口(即 ENI)进行网络通信。因为容器位于内部对 Docker 引擎主机的不同 IP 网络上,为了与外部世界建立网络连接,Docker 在 ENI 上配置了 iptables 规则,将所有出站网络流量转换为弹性网络接口的 IP 地址,并为入站网络流量设置动态端口映射规则。例如,前面图表中一个容器的动态端口映射规则会将172.31.0.99:32768的传入流量转换为172.16.0.101:8000

iptables 是标准的 Linux 内核功能,为您的 Linux 主机提供网络访问控制和网络地址转换功能。

尽管许多应用程序使用网络地址转换(NAT)运行良好,但有些应用程序对 NAT 的支持不佳,甚至根本无法支持,并且对于网络流量较大的应用程序,使用 NAT 可能会影响性能。还要注意,应用于 ENI 的安全组是所有容器、ECS 代理和操作系统本身共享的,这意味着安全组必须允许所有这些组件的组合网络连接要求,这可能会危及您的容器和 ECS 容器实例的安全。

可以配置 ECS 任务定义以在主机网络模式下运行,这意味着它们的网络配置类似于 ECS 代理配置,不需要网络地址转换(NAT)。主机网络模式具有自己的安全性影响,通常不建议用于希望避免 NAT 或需要网络隔离的应用程序,而应该使用 ECS 任务网络来满足这些要求。主机网络应谨慎使用,仅用于执行系统功能的 ECS 任务,例如日志记录或监视辅助容器。

ECS 任务网络

现在您对 ECS 容器实例及其关联容器的默认网络配置有了基本了解,让我们来看看当您配置 ECS 任务网络时,这个情况会如何改变。以下图表概述了 ECS 任务网络的工作原理:

ECS 任务网络

在上图中,每个 ECS 任务都被分配和配置为使用自己专用的弹性网络接口。这与第一个图表有很大不同,其中容器使用由 Docker 动态创建的内部网络接口,而 ECS 负责动态创建每个 ECS 任务的弹性网络接口。这对 ECS 来说更加复杂,但优势在于您的容器可以直接附加到 VPC 子网,并且可以拥有自己独立的安全组。这意味着您的容器网络端口不再需要复杂的功能,如动态端口映射,这会影响安全性和性能,您的容器端口直接暴露给 AWS 网络环境,并可以直接被负载均衡器访问。

在前面的图表中需要注意的一点是外部网络配置,引入了私有子网和公共子网的概念。我以这种方式表示网络连接,因为在撰写本文时,ECS 任务网络不支持为每个动态创建的 ENI 分配公共 IP 地址,因此如果您的容器需要互联网连接,则确实需要额外的 VPC 网络设置。此设置涉及在公共网络上创建 NAT 网关或 HTTP 代理,然后您的 ECS 任务可以将互联网流量路由到该网关。在当前 todobackend 应用程序的情况下,第九章介绍的入口脚本与位于互联网上的 AWS Secrets Manager API 通信,因此需要类似于第一个图表中显示的网络设置。

ECS 代理没有无法分配公共 IP 地址的限制,因为它使用在创建时分配给实例的默认 EC2 实例 ENI。例如,在前面的图表中,您可以将 ECS 代理使用的默认 ENI 连接到公共网络或具有互联网连接的其他网络。

通过比较前面的两个图表,您可以看到 ECS 任务网络简化了 ECS 容器实例的内部网络配置,使其看起来更像是传统的虚拟机网络模型,如果您想象 ECS 容器实例是一台裸金属服务器,您的容器是虚拟机。这带来了更高的性能和安全性,但需要更复杂的外部网络设置,需要为出站互联网连接配置 NAT 网关或 HTTP 代理,并且 ECS 负责动态附加 ENI 到您的实例,这也带来了自己的限制。

例如,可以附加到给定 EC2 实例的 ENI 的最大数量取决于 EC2 实例类型,如果您查看docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI,您会发现免费套餐 t2.micro 实例类型仅支持最多两个 ENI,这限制了您可以在 ECS 任务网络模式下运行的 ECS 任务的最大数量为每个实例只能运行一个(因为一个 ENI 始终保留给主机)。

配置 NAT 网关

正如您在前一节中了解到的,在撰写本文时,ECS 任务网络不支持分配公共 IP 地址,这意味着您必须配置额外的基础设施来支持应用程序可能需要的任何互联网连接。尽管应用程序可以通过堆栈中的应用程序负载均衡器进行无出站互联网访问,但应用程序容器入口脚本确实需要在启动时与 AWS Secrets Manager 服务通信,这需要与 Secrets Manager API 通信的互联网连接。

为了提供这种连接性,您可以采用两种典型的方法:

  • 配置 NAT 网关:这是 AWS 管理的服务,为出站通信提供网络地址转换,使位于私有子网上的主机和容器能够访问互联网。

  • 配置 HTTP 代理:这提供了一个前向代理,其中配置了代理支持的应用程序并将 HTTP、HTTPS 和 FTP 请求转发到您的代理。

我通常推荐后一种方法,因为它可以根据 DNS 命名限制对 HTTP 和 HTTPS 流量的访问(后者取决于所使用的 HTTP 代理的能力),而 NAT 网关只能根据 IP 地址限制访问。然而,设置代理确实需要更多的努力,并且需要管理额外的服务的运营开销,因此为了专注于 ECS 任务网络并保持简单,我们将在本章中实施 NAT 网关方法。

配置私有子网和路由表

为了支持具有典型路由配置的 NAT 网关,我们需要首先添加一个私有子网以及一个私有路由表,这些将作为 CloudFormation 资源添加到您的 todobackend 堆栈中。以下示例演示了在 todobackend-aws 存储库的根目录中的stack.yml文件中执行此配置:

为了保持本示例简单,我们正在创建 todobackend 应用程序堆栈中的网络资源,但通常您会在单独的网络重点 CloudFormation 堆栈中创建网络子网和相关资源,如 NAT 网关。

...
...
Resources:
  PrivateSubnet:
 Type: AWS::EC2::Subnet
 Properties:
 AvailabilityZone: !Sub ${AWS::Region}a
 CidrBlock: 172.31.96.0/20
 VpcId: !Ref VpcId
 PrivateRouteTable:
 Type: AWS::EC2::RouteTable
 Properties:
 VpcId: !Ref VpcId
 PrivateSubnetRouteTableAssociation:
 Type: AWS::EC2::SubnetRouteTableAssociation
 Properties:
 RouteTableId: !Ref PrivateRouteTable
 SubnetId: !Ref PrivateSubnet
...
...

创建私有子网和路由表

在前面的例子中,您创建了私有子网和路由表资源,然后通过PrivateSubnetRouteTableAssociation资源将它们关联起来。这个配置意味着从私有子网发送的所有网络流量将根据私有路由表中发布的路由进行路由。请注意,您只在本地 AWS 区域的可用区 A 中指定了一个子网—在实际情况下,您通常会为高可用性配置至少两个可用区中的两个子网。还有一点需要注意的是,您必须确保为您的子网配置的CidrBlock落在为您的 VPC 配置的 IP 范围内,并且没有分配给任何其他子网。

以下示例演示了使用 AWS CLI 来确定 VPC IP 范围并查看现有子网 CIDR 块:

> export AWS_PROFILE=docker-in-aws
> aws ec2 describe-vpcs --query Vpcs[].CidrBlock
[
    "172.31.0.0/16"
]
> aws ec2 describe-subnets --query Subnets[].CidrBlock
[
    "172.31.16.0/20",
    "172.31.80.0/20",
    "172.31.48.0/20",
    "172.31.64.0/20",
    "172.31.32.0/20",
    "172.31.0.0/20"
]

查询 VPC 和子网 CIDR 块

在前面的例子中,您可以看到默认的 VPC 已经配置了一个 CIDR 块172.31.0.0/16,您还可以看到已经分配给默认 VPC 中创建的默认子网的现有 CIDR 块。如果您回到第一个例子,您会看到我们选择了这个块中的下一个/20子网(172.31.96.0/20)用于新定义的私有子网。

配置 NAT 网关

在私有路由配置就绪后,您现在可以配置 NAT 网关和其他支持资源。

NAT 网关需要一个弹性 IP 地址,这是出站流量经过 NAT 网关时将显示为源自的固定公共 IP 地址,并且必须安装在具有互联网连接的公共子网上。

以下示例演示了配置 NAT 网关以及关联的弹性 IP 地址:

...
...
Resources:
 NatGateway:
 Type: AWS::EC2::NatGateway
 Properties:
 AllocationId: !Sub ${ElasticIP.AllocationId}
 SubnetId:
 Fn::Select:
 - 0
 - !Ref ApplicationSubnets
 ElasticIP:
 Type: AWS::EC2::EIP
 Properties:
 Domain: vpc
...
...

配置 NAT 网关

在前面的例子中,您创建了一个为 VPC 分配的弹性 IP 地址,然后通过AllocationId属性将分配的 IP 地址链接到 NAT 网关。

弹性 IP 地址在计费方面有些有趣,因为 AWS 只要您在积极使用它们,就不会向您收费。如果您创建弹性 IP 地址但没有将它们与 EC2 实例或 NAT 网关关联,那么 AWS 将向您收费。有关弹性 IP 地址计费方式的更多详细信息,请参见aws.amazon.com/premiumsupport/knowledge-center/elastic-ip-charges/

注意在指定SubnetId时使用了Fn::Select内在函数,重要的是要理解子网必须与将链接到 NAT 网关的子网和路由表资源位于相同的可用区。在我们的用例中,这是可用区 A,ApplicationSubnets输入包括两个子网 ID,分别位于可用区 A 和 B,因此您选择第一个从零开始的子网 ID。请注意,您可以使用以下示例中演示的aws ec2 describe-subnets命令来验证子网的可用区:

> cat dev.cfg
ApplicationDesiredCount=1
ApplicationImageId=ami-ec957491
ApplicationImageTag=5fdbe62
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f VpcId=vpc-f8233a80
> aws ec2 describe-subnets --query Subnets[].[AvailabilityZone,SubnetId] --output table
-----------------------------------
|         DescribeSubnets         |
+-------------+-------------------+
|  us-east-1a |  subnet-a5d3ecee  |
|  us-east-1d |  subnet-c2abdded  |
|  us-east-1f |  subnet-aae11aa5  |
|  us-east-1e |  subnet-fd3a43c2  |
|  us-east-1b |  subnet-324e246f  |
|  us-east-1c |  subnet-d281a2b6  |
+-------------+-------------------+

按可用区查询子网 ID

在前面的示例中,您可以看到dev.cfg文件中ApplicationSubnets输入中的第一项是us-east-1a的子网 ID,确保 NAT 网关将安装到正确的可用区。

为您的私有子网配置路由

配置 NAT 网关的最后一步是为您的私有子网配置默认路由,指向您的 NAT 网关资源。此配置将确保所有出站互联网流量将被路由到您的 NAT 网关,然后执行地址转换,使您的私有主机和容器能够与互联网通信。

以下示例演示了为您之前创建的私有路由表添加默认路由:

...
...
Resources:
 PrivateRouteTableDefaultRoute:
 Type: AWS::EC2::Route
 Properties:
 DestinationCidrBlock: 0.0.0.0/0
 RouteTableId: !Ref PrivateRouteTable
      NatGatewayId: !Ref NatGateway
...
...

配置默认路由

在前面的示例中,您可以看到您配置了RouteTableIdNatGatewayId属性,以确保您在第一个示例中创建的私有路由表的默认路由设置为您在后面示例中创建的 NAT 网关。

现在您已经准备好部署您的更改,但在这之前,让我们在 todobackend-aws 存储库中创建一个名为ecs-task-networking的单独分支,这样您就可以在本章末尾轻松恢复您的更改:

> git checkout -b ecs-task-networking
M stack.yml
Switched to a new branch 'ecs-task-networking'
> git commit -a -m "Add NAT gateway resources"
[ecs-task-networking af06d37] Add NAT gateway resources
 1 file changed, 33 insertions(+)

创建 ECS 任务网络分支

现在,您可以使用您一直在本书中用于堆栈部署的熟悉的aws cloudformation deploy命令部署您的更改:

> export AWS_PROFILE=docker-in-aws > aws cloudformation deploy --template-file stack.yml \
 --stack-name todobackend --parameter-overrides $(cat dev.cfg) \ --capabilities CAPABILITY_NAMED_IAM Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend
> aws ec2 describe-subnets --query "Subnets[?CidrBlock=='172.31.96.0/20'].SubnetId" ["subnet-3acd6370"]
> aws ec2 describe-nat-gateways
{
    "NatGateways": [
        {
            "CreateTime": "2018-04-22T10:30:07.000Z",
            "NatGatewayAddresses": [
                {
                    "AllocationId": "eipalloc-838abd8a",
                    "NetworkInterfaceId": "eni-90d8f10c",
                    "PrivateIp": "172.31.21.144",
 "PublicIp": "18.204.39.34"
                }
            ],
            "NatGatewayId": "nat-084089330e75d23b3",
            "State": "available",
            "SubnetId": "subnet-a5d3ecee",
            "VpcId": "vpc-f8233a80",
...
...

部署更改到 todobackend 应用程序

在前面的示例中,成功部署 CloudFormation 更改后,您使用aws ec2 describe-subnets命令查询您创建的新子网的子网 ID,因为您稍后在本章中将需要这个值。您还运行aws ec2 describe-nat-gateways命令来验证 NAT 网关是否成功创建,并查看网关的弹性 IP 地址,该地址由突出显示的PublicIP属性表示。请注意,您还应检查默认路由是否正确创建,如以下示例所示:

> aws ec2 describe-route-tables \
 --query "RouteTables[].Routes[?DestinationCidrBlock=='0.0.0.0/0']"
[
    [
        {
            "DestinationCidrBlock": "0.0.0.0/0",
            "NatGatewayId": "nat-084089330e75d23b3",
            "Origin": "CreateRoute",
            "State": "active"
        }
    ],
    [
        {
            "DestinationCidrBlock": "0.0.0.0/0",
            "GatewayId": "igw-1668666f",
            "Origin": "CreateRoute",
            "State": "active"
        }
    ]
]
...
...

检查默认路由

在前面的示例中,您可以看到存在两个默认路由,一个默认路由与 NAT 网关关联,另一个与互联网网关关联,证实您帐户中的一个路由表正在将互联网流量路由到您新创建的 NAT 网关。

配置 ECS 任务网络

现在,您已经建立了支持 ECS 任务网络私有 IP 寻址要求的网络基础设施,您可以继续在 ECS 资源上配置 ECS 任务网络。这需要以下配置和考虑:

  • 您必须配置 ECS 任务定义和 ECS 服务以支持 ECS 任务网络。

  • 任务定义的网络模式必须设置为awsvpc

  • 用于 ECS 任务网络的弹性网络接口只能与一个 ECS 任务关联。根据您的 ECS 实例类型,这将限制您在任何给定的 ECS 容器实例中可以运行的 ECS 任务的最大数量。

  • 使用配置了 ECS 任务网络的 ECS 任务部署比传统的 ECS 部署时间更长,因为需要创建一个弹性网络接口并将其绑定到您的 ECS 容器实例。

  • 由于您的容器应用程序有一个专用的网络接口,动态端口映射不再可用,您的容器端口直接暴露在网络接口上。

  • 当使用awsvpc网络模式的 ECS 服务与应用程序负载均衡器目标组一起使用时,目标类型必须设置为ip(默认值为instance)。

动态端口映射的移除意味着,例如,todobackend 应用程序(运行在端口 8000 上)将在启用任务网络的情况下在外部使用端口8000访问,而不是通过动态映射的端口。这将提高生成大量网络流量的应用程序的性能,并且意味着您的安全规则可以针对应用程序运行的特定端口,而不是允许访问动态端口映射使用的临时网络端口范围。

为任务网络配置 ECS 任务定义

配置 ECS 任务定义以使用任务网络的第一步是配置您的 ECS 任务定义。以下示例演示了修改ApplicationTaskDefinition资源以支持 ECS 任务网络:

...
...
  ApplicationTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: todobackend
 NetworkMode: awsvpc
      TaskRoleArn: !Sub ${ApplicationTaskRole.Arn}
      Volumes:
        - Name: public
      ContainerDefinitions:
        - Name: todobackend
          ...
          ...
 PortMappings:
 - ContainerPort: 8000 
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Sub /${AWS::StackName}/ecs/todobackend
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: docker
        - Name: collectstatic
          Essential: false
...
...

配置 ECS 任务定义以使用任务网络

在上面的示例中,NetworkMode属性已添加并配置为awsvpc的值。默认情况下,此属性设置为bridge,实现了默认的 Docker 行为,如第一个图中所示,包括一个 Docker 桥接口,并配置了网络地址转换以启用动态端口映射。通过将网络模式设置为awsvpc,ECS 将确保从此任务定义部署的任何 ECS 任务都分配了专用的弹性网络接口(ENI),并配置任务定义中的容器以使用 ENI 的网络堆栈。此示例中的另一个配置更改是从PortMappings部分中删除了HostPort: 0配置,因为 ECS 任务网络不使用或支持动态端口映射。

为任务网络配置 ECS 服务

将 ECS 任务定义配置为使用正确的任务网络模式后,接下来需要配置 ECS 服务。您的 ECS 服务配置定义了 ECS 应该创建 ENI 的目标子网,并且还定义了应该应用于 ENI 的安全组。以下示例演示了在 todobackend 堆栈中更新ApplicationService资源:

...
...
Resources:
  ...
  ...
  ApplicationService:
    Type: AWS::ECS::Service
    DependsOn:
      - ApplicationAutoscaling
      - ApplicationLogGroup
      - ApplicationLoadBalancerHttpListener
      - MigrateTask
    Properties:
      TaskDefinition: !Ref ApplicationTaskDefinition
      Cluster: !Ref ApplicationCluster
      DesiredCount: !Ref ApplicationDesiredCount
      NetworkConfiguration:
 AwsvpcConfiguration:
 SecurityGroups:
 - !Ref ApplicationSecurityGroup
 Subnets:
            - !Ref PrivateSubnet
      LoadBalancers:
        - ContainerName: todobackend
          ContainerPort: 8000
          TargetGroupArn: !Ref ApplicationServiceTargetGroup
 # The Role property has been removed
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 100
...
...

配置 ECS 服务以使用任务网络

在前面的例子中,向 ECS 服务定义添加了一个名为NetworkConfiguration的新属性。每当您启用任务网络时,都需要此属性,并且您可以看到需要配置与 ECS 将创建的 ENI 相关联的子网和安全组。请注意,您引用了本章前面创建的PrivateSubnet资源,这确保您的容器网络接口不会直接从互联网访问。一个不太明显的变化是Role属性已被移除 - 每当您使用使用 ECS 任务网络的 ECS 服务时,AWS 会自动配置 ECS 角色,并且如果您尝试设置此角色,将会引发错误。

为任务网络配置支持资源

如果您回顾一下前面的例子,您会注意到您引用了一个名为ApplicationSecurityGroup的新安全组,需要将其添加到您的模板中,如下例所示:

...
...
 ApplicationSecurityGroup:
Type: AWS::EC2::SecurityGroup
 Properties:
 GroupDescription: !Sub ${AWS::StackName} Application Security Group
 VpcId: !Ref VpcId
 SecurityGroupEgress:
 - IpProtocol: udp
 FromPort: 53
 ToPort: 53
 CidrIp: 0.0.0.0/0
 - IpProtocol: tcp
 FromPort: 443
 ToPort: 443
 CidrIp: 0.0.0.0/0
  ...
  ...
  ApplicationLoadBalancerToApplicationIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
 FromPort: 8000
 ToPort: 8000
 GroupId: !Ref ApplicationSecurityGroup
      SourceSecurityGroupId: !Ref ApplicationLoadBalancerSecurityGroup
  ApplicationLoadBalancerToApplicationEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      IpProtocol: tcp
 FromPort: 8000
 ToPort: 8000
      GroupId: !Ref ApplicationLoadBalancerSecurityGroup
 DestinationSecurityGroupId: !Ref ApplicationSecurityGroup
  ...
  ...
  ApplicationToApplicationDatabaseIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      GroupId: !Ref ApplicationDatabaseSecurityGroup
 SourceSecurityGroupId: !Ref ApplicationSecurityGroup
  ApplicationToApplicationDatabaseEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
GroupId: !Ref ApplicationSecurityGroup
      DestinationSecurityGroupId: !Ref ApplicationDatabaseSecurityGroup
...
...

为任务网络配置安全组

在前面的例子中,您首先创建了一个安全组,其中包括一个出站规则集,允许出站 DNS 和 HTTPS 流量,这是必需的,以允许您容器中的入口脚本与 AWS Secrets Manager API 进行通信。请注意,您需要修改现有的AWS::EC2::SecurityGroupIngressAWS::EC2::SecurityGroupEgress资源,这些资源之前允许应用负载均衡器/应用数据库与应用自动扩展组实例之间的访问。您可以看到,对于ApplicationLoadBalancerToApplicationEgressApplicationLoadBalancerToApplicationEgress资源,端口范围已从32768的临时端口范围减少到60999,仅为端口8000,这导致了更安全的配置。此外,ECS 容器实例控制平面(与ApplicationAutoscalingSecurityGroup资源相关联)现在无法访问您的应用数据库(现在只有您的应用可以这样做),这再次更安全。

当前对 todobackend 堆栈的修改存在一个问题,即您尚未更新MigrateTaskDefinition以使用任务网络。我之所以不这样做的主要原因是因为这将需要您的 ECS 容器实例支持比免费套餐 t2.micros 支持的更多弹性网络接口,并且还需要更新 ECS 任务运行器自定义资源以支持运行临时 ECS 任务。当然,如果您想在生产环境中使用 ECS 任务网络,您需要解决这些问题,但是出于提供对 ECS 任务网络的基本理解的目的,我选择不这样做。这意味着如果您进行任何需要运行迁移任务的更改,它将失败,并且一旦本章完成,您将恢复 todobackend 堆栈配置,以确保不使用 ECS 任务网络来完成剩余的章节。

最后,您需要对模板进行最后一次更改,即修改与 ECS 服务关联的应用程序负载均衡器目标组。当您的 ECS 服务运行在awsvpc网络模式下的任务时,您必须将目标组类型从默认值instance更改为ip的值,如下例所示,因为您的 ECS 任务现在具有自己独特的 IP 地址:

Resources:
 ...
 ...
 ApplicationServiceTargetGroup:
     Type: AWS::ElasticLoadBalancingV2::TargetGroup
     Properties:
       Protocol: HTTP
       Port: 8000
       VpcId: !Ref VpcId
       TargetType: ip
       TargetGroupAttributes:
         - Key: deregistration_delay.timeout_seconds
           Value: 30
 ...
 ...

更新应用程序负载均衡器目标组目标类型

部署和测试 ECS 任务网络

您现在可以部署更改并验证 ECS 任务网络是否正常工作。如果运行aws cloudformation deploy命令,应该会发生以下情况:

  • 将创建应用程序任务定义的新修订版本,该版本配置为 ECS 任务网络。

  • ECS 服务配置将检测更改并尝试部署新的修订版本,以及 ECS 服务配置更改。ECS 将动态地将新的 ENI 附加到私有子网,并将此 ENI 分配给ApplicationService资源的新 ECS 任务。

部署完成后,您应该验证应用程序仍在正常工作,一旦完成此操作,您可以浏览到 ECS 控制台,单击您的 ECS 服务,并选择服务的当前运行任务。

以下屏幕截图显示了 ECS 任务屏幕:

ECS 任务处于任务网络模式

如您所见,任务的网络模式现在是awsvpc,并且已经从本章前面创建的私有子网中动态分配了一个 ENI。如果您点击 ENI ID 链接,您将能够验证附加到 ENI 的安全组,并且还可以检查 ENI 是否已附加到您的某个 ECS 容器实例中。

在这一点上,您应该将在本章中进行的最终一组更改提交到 ECS 任务网络分支,检出主分支,并重新部署您的 CloudFormation 堆栈。这将撤消本章中所做的所有更改,将您的堆栈恢复到上一章末尾时的相同状态。这是必需的,因为我们不希望不得不升级到更大的实例类型来适应MigrateTaskDefinition资源和我们将在后续章节中测试的未来自动扩展方案:

> git commit -a -m "Add ECS task networking resources"
 [ecs-task-networking 7e995cb] Add ECS task networking resources
 2 files changed, 37 insertions(+), 10 deletions(-)
> git checkout master
Switched to branch 'master'
> aws cloudformation deploy --template-file stack.yml --stack-name todobackend \
 --parameter-overrides $(cat dev.cfg) --capabilities CAPABILITY_NAMED_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend

还原 todobackend-aws 存储库

摘要

在本章中,您学会了如何使用 ECS 任务网络增加 Docker 应用程序的网络隔离和安全性。ECS 任务网络将默认的 Docker 桥接和 NAT 网络配置更改为每个 ECS 任务接收自己的专用弹性网络接口或 ENI 的模型。这意味着您的 Docker 应用程序被分配了自己的专用安全组,并且可以通过其发布的端口直接访问,这避免了实现动态端口映射等功能的需要,这些功能可能会影响性能并需要更宽松的安全规则才能工作。然而,ECS 任务网络也带来了一系列挑战和限制,包括更复杂的网络拓扑来适应当前仅支持私有 IP 地址的限制,以及每个 ENI 只能运行单个 ECS 任务的能力。

ECS 任务网络目前不支持公共 IP 地址,这意味着如果您的任务需要出站互联网连接,您必须提供 NAT 网关或 HTTP 代理。NAT 网关是 AWS 提供的托管服务,您学会了如何配置用于 ECS 任务的私有子网,以及如何配置私有路由表将互联网流量路由到您在现有公共子网中创建的 NAT 网关。

您已经了解到,配置 ECS 任务网络需要在 ECS 任务定义中指定 awsvpc 网络模式,并且需要向 ECS 服务添加网络配置,指定 ECS 任务将连接到的子网和将应用的安全组。如果您的应用由应用负载均衡器提供服务,您还需要确保与 ECS 服务关联的目标组的目标类型配置为ip,而不是默认的instance目标类型。如果您要将这些更改应用到现有环境中,您可能还需要更新附加到资源的安全组,例如负载均衡器和数据库,因为您的 ECS 任务不再与应用于 ECS 容器实例级别的安全组相关联,并且具有自己的专用安全组。

在接下来的两章中,您将学习如何处理 ECS 的一些更具挑战性的运营方面,包括管理 ECS 容器实例的生命周期和对 ECS 集群进行自动扩展。

问题

  1. 真/假:默认的 Docker 网络配置使用 iptables 执行网络地址转换。

  2. 您有一个应用程序,形成应用程序级别的集群,并使用 EC2 元数据来发现运行您的应用程序的其他主机的 IP 地址。当您使用 ECS 运行应用程序时,您会注意到您的应用程序正在使用172.16.x.x/16地址,但您的 EC2 实例配置为172.31.x.x/16地址。哪些 Docker 网络模式可以帮助解决这个问题?

  3. 真/假:在 ECS 任务定义的NetworkMode中,host值启用了 ECS 任务网络。

  4. 您为 ECS 任务定义启用了 ECS 任务网络,但是您的应用负载均衡器无法再访问您的应用程序。您检查了附加到 ECS 容器实例的安全组的规则,并确认您的负载均衡器被允许访问您的应用程序。您如何解决这个问题?

  5. 您为 ECS 任务定义启用了 ECS 任务网络,但是您的容器在启动时失败,并显示无法访问位于互联网上的位置的错误。您如何解决这个问题?

  6. 在 t2.micro 实例上最大可以运行多少个 ENI?

  7. 在 t2.micro 实例上以任务网络模式运行的 ECS 任务的最大数量是多少?

  8. 在 t2.micro 实例上以任务网络模式运行的最大容器数量是多少?

  9. 启用 ECS 任务网络模式后,您收到一个部署错误,指示目标组具有目标类型实例,与 awsvpc 网络模式不兼容。您如何解决这个问题?

  10. 启用 ECS 任务网络模式后,您收到一个部署错误,指出您不能为需要服务关联角色的服务指定 IAM 角色。您如何解决这个问题?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十一章:管理 ECS 基础设施生命周期

与操作 ECS 基础设施相关的一个基本持续活动是管理 ECS 容器实例的生命周期。在任何生产级别的场景中,您都需要对 ECS 容器实例进行打补丁,并确保 ECS 容器实例的核心组件(如 Docker 引擎和 ECS 代理)经常更新,以确保您可以访问最新功能和安全性和性能增强。在一个不可变基础设施的世界中,您的 ECS 容器实例被视为“牲畜”,标准方法是通过滚动新的 Amazon 机器映像(AMIs)销毁和替换 ECS 容器实例,而不是采取传统的打补丁“宠物”方法,并将 ECS 容器实例保留很长时间。另一个常见的用例是需要管理生命周期的与自动扩展相关,例如,如果您在高需求期后扩展 ECS 集群,您需要能够从集群中移除 ECS 容器实例。

将 ECS 容器实例从服务中移除听起来可能是一个很简单的任务,然而请考虑一下如果您的实例上有正在运行的容器会发生什么。如果立即将实例移出服务,连接到运行在这些容器上的应用程序的用户将会受到干扰,这可能会导致数据丢失,至少会让用户感到不满。所需的是一种机制,使您的 ECS 容器实例能够优雅地退出服务,保持当前用户连接,直到可以在不影响最终用户的情况下关闭它们,然后在确保实例完全退出服务后终止实例。

在本章中,您将学习如何通过利用两个关键的 AWS 功能来实现这样的能力——EC2 自动缩放生命周期钩子和 ECS 容器实例排空。EC2 自动缩放生命周期钩子让您了解与启动或停止 EC2 实例相关的待处理生命周期事件,并为您提供机会在发出生命周期事件之前执行任何适当的初始化或清理操作。这就是您可以利用 ECS 容器实例排空的地方,它将受影响的 ECS 容器实例上的 ECS 任务标记为排空或停用,并开始优雅地将任务从服务中取出,方法是在集群中的其他 ECS 容器实例上启动新的替代 ECS 任务,然后排空到受影响的 ECS 任务的连接,直到任务可以停止并且 ECS 容器实例被排空。

将涵盖以下主题:

  • 理解 ECS 基础设施的生命周期管理

  • 构建新的 ECS 容器实例 AMI

  • 配置 EC2 自动缩放滚动更新

  • 创建 EC2 自动缩放生命周期钩子

  • 创建用于消耗生命周期钩子的 Lambda 函数

  • 部署和测试自动缩放生命周期钩子

技术要求

以下列出了完成本章所需的技术要求:

  • AWS 账户的管理员访问

  • 根据第三章的说明配置本地 AWS 配置文件

  • AWS CLI 版本 1.15.71 或更高版本

  • 本章继续自第九章(而不是第十章),因此需要您成功完成第九章中定义的所有配置任务,并确保您已将todobackend-aws存储库重置为主分支(应基于第九章的完成)

以下 GitHub URL 包含本章中使用的代码示例 - github.com/docker-in-aws/docker-in-aws/tree/master/ch11.

查看以下视频以查看代码的实际操作:

bit.ly/2BT7DVh

理解 ECS 生命周期管理

如本章介绍中所述,ECS 生命周期管理是指将现有的 ECS 容器实例从服务中取出的过程,而不会影响连接到在您受影响的实例上运行的应用程序的最终用户。

这需要您利用 AWS 提供的两个关键功能:

  • EC2 自动扩展生命周期挂钩

  • ECS 容器实例排水

EC2 自动扩展生命周期挂钩

EC2 自动扩展生命周期挂钩允许您在挂起的生命周期事件发生之前收到通知并在事件发生之前执行某些操作。目前,您可以收到以下生命周期挂钩事件的通知:

  • EC2_INSTANCE_LAUNCHING:当 EC2 实例即将启动时引发

  • EC2_INSTANCE_TERMINATING:当 EC2 实例即将终止时引发

一般情况下,您不需要担心EC2_INSTANCE_LAUNCHING事件,但是任何运行生产级 ECS 集群的人都应该对EC2_INSTANCE_TERMINATING事件感兴趣,因为即将终止的实例可能正在运行具有活动最终用户连接的容器。一旦您订阅了生命周期挂钩事件,EC2 自动扩展服务将等待您发出信号,表明生命周期操作可以继续进行。这为您提供了一种机制,允许您在EC2_INSTANCE_TERMINATING事件发生时执行优雅的拆除操作,这就是您可以利用 ECS 容器实例排水的地方。

ECS 容器实例排水

ECS 容器实例排水是一个功能,允许您优雅地排水您的 ECS 容器实例中正在运行的 ECS 任务,最终结果是您的 ECS 容器实例没有正在运行的 ECS 任务或容器,这意味着可以安全地终止实例而不影响您的容器应用程序。ECS 容器实例排水首先将您的 ECS 容器实例标记为 DRAINING 状态,这将导致在实例上运行的所有 ECS 任务被优雅地关闭并在集群中的其他容器实例上启动。这种排水活动使用了您已经在 ECS 服务中看到的标准滚动行为,例如,如果您有一个与具有应用程序负载均衡器集成的 ECS 服务相关联的 ECS 任务,ECS 将首先尝试在另一个 ECS 容器实例上注册一个新的 ECS 任务作为应用程序负载均衡器目标组中的新目标,然后将与正在排水的 ECS 容器实例相关联的目标放置到连接排水状态。

请注意,重要的是您的 ECS 集群具有足够的资源和 ECS 容器实例来迁移每个受影响的 ECS 任务,这可能具有挑战性,因为您还通过一个实例减少了 ECS 集群的容量。这意味着,例如,如果您正在计划替换集群中的 ECS 容器实例(例如,您正在更新到新的 AMI),那么您需要临时向集群添加额外的容量,以便以滚动方式交换实例,而不会减少整体集群容量。如果您正在使用 CloudFormation 部署您的 EC2 自动扩展组,一个非常有用的功能是能够指定更新策略,在滚动更新期间临时向您的自动扩展组添加额外的容量,您将学习如何利用此功能始终确保在执行滚动更新时始终保持 ECS 集群容量。

ECS 生命周期管理解决方案

现在您已经了解了 ECS 生命周期管理的一些背景知识,让我们讨论一下您将在本章中实施的解决方案,该解决方案将利用 EC2 生命周期挂钩来触发 ECS 容器实例的排空,并在安全终止 ECS 容器实例时向 EC2 自动扩展服务发出信号。

以下图表说明了一个简单的 EC2 自动扩展组和一个具有两个 ECS 容器实例的 ECS 集群,支持 ECS Service A和 ECS Service B,它们都有两个 ECS 任务或 ECS 服务的实例正在运行:

在服务中的 EC2 自动扩展组/ECS 集群

假设您现在希望使用新的 Amazon Machine Image 更新 EC2 自动扩展组中的 ECS 容器实例,这需要终止并替换每个实例。以下图表说明了我们的生命周期挂钩解决方案将如何处理这一要求,并确保自动扩展组中的每个实例都可以以不干扰连接到每个 ECS 服务的应用程序的最终用户的方式进行替换:

执行滚动更新的在服务中的 EC2 自动扩展组/ECS 集群

在上图中,发生以下步骤:

  1. CloudFormation 滚动更新已配置为 EC2 自动扩展组,这会导致 CloudFormation 服务临时增加 EC2 自动扩展组的大小。

  2. EC2 自动扩展组根据 CloudFormation 中组大小的增加,向自动扩展组添加一个新的 EC2 实例(ECS 容器实例 C)。

  3. 一旦新的 EC2 实例启动并向 CloudFormation 发出成功信号,CloudFormation 服务将指示 EC2 自动扩展服务终止 ECS 容器实例 A,因为 ECS 容器实例 C 现在已加入 EC2 自动扩展组和 ECS 集群。

  4. 在终止实例之前,EC2 自动扩展服务触发一个生命周期挂钩事件,将此事件发布到配置的简单通知服务(SNS)主题。SNS 是一种发布/订阅样式的通知服务,可用于各种用例,在我们的解决方案中,我们将订阅一个 Lambda 函数到 SNS 主题。

  5. Lambda 函数是由 SNS 主题调用的,以响应生命周期挂钩事件被发布到主题。

  6. Lambda 函数指示 ECS 排空即将被终止的 ECS 容器实例。然后,该函数轮询 ECS 容器实例上正在运行的任务数量,等待任务数量为零后才认为排空过程完成。

  7. ECS 将正在运行在 ECS 容器实例 A 上的当前任务转移到具有空闲容量的其他容器实例。在上图中,由于 ECS 容器实例 C 最近被添加到集群中,因此正在运行在 ECS 容器实例 A 上的 ECS 任务可以被转移到容器实例 C。请注意,如果容器实例 C 尚未添加到集群中,集群中将没有足够的容量来转移容器实例 A,因此确保集群具有足够的容量来处理这些类型的事件非常重要。

  8. 在许多情况下,ECS 容器实例的排空可能会超过 Lambda 的当前五分钟执行超时限制。在这种情况下,您可以简单地重新发布生命周期挂钩事件通知到 SNS 主题,这将自动重新调用 Lambda 函数。

  9. Lambda 函数再次指示 ECS 排空容器实例 A(已在进行中),并继续轮询运行任务数量,等待运行任务数量为零。

  10. 假设容器实例完成排空并且运行任务数量减少为零,Lambda 函数会向 EC2 自动扩展服务发出生命周期挂钩已完成的信号。

  11. EC2 自动缩放服务现在终止 ECS 容器实例,因为生命周期挂钩已经完成。

此时,由 CloudFormation 在步骤 1 中发起的滚动更新已经完成了 50%,因为旧的 ECS 容器实例 A 已被 ECS 容器实例 C 替换。在前面的图表中描述的过程再次重复,引入了一个新的 ECS 容器实例到集群中,并将 ECS 容器实例 B 标记为终止。一旦 ECS 容器实例 B 的排空完成,自动缩放组/集群中的所有实例都已被替换,滚动更新完成。

构建一个新的 ECS 容器实例 AMI

为了测试我们的生命周期管理解决方案,我们需要有一种机制来强制终止您的 ECS 容器实例。虽然您可以简单地调整自动缩放组的期望计数(实际上这是自动缩放组缩减时的常见情况),但另一种常见情况是当您需要通过引入一个新构建的 Amazon Machine Image(AMI)来更新您的 ECS 容器实例,其中包括最新的操作系统和安全补丁,以及最新版本的 Docker Engine 和 ECS 代理。至少,如果您正在使用类似于第六章中学到的方法构建自定义 ECS 容器实例 AMI,那么每当 Amazon 发布基本 ECS 优化 AMI 的新版本时,您都应该重新构建您的 AMI,并且每周或每月更新您的 AMI 是常见做法。

要模拟将新的 AMI 引入 ECS 集群,您可以简单地执行第六章中执行的相同步骤,这将输出一个新的 AMI,然后您可以将其作为输入用于您的堆栈,并强制您的 ECS 集群升级每个 ECS 容器实例。

以下示例演示了从packer-ecs存储库的根目录运行make build命令,这将输出一个新的 AMI ID,用于新创建和发布的镜像。确保您记下这个 AMI ID,因为您稍后在本章中会需要它:

> export AWS_PROFILE=docker-in-aws
> make build
packer build packer.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: docker-in-aws-ecs 1518934269
...
...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-77893508

运行 Packer 构建

配置 EC2 自动缩放滚动更新

当您使用 CloudFormation 创建和管理您的 EC2 自动扩展组时,一个有用的功能是能够管理滚动更新。滚动更新是指以受控的方式将新的 EC2 实例滚入您的自动扩展组,以确保您的更新过程可以在不引起中断的情况下完成。在第八章,当您通过 CloudFormation 创建 EC2 自动扩展组时,您了解了 CloudFormation 支持创建策略,可以帮助您确保 EC2 自动扩展中的所有实例都已成功初始化。CloudFormation 还支持更新策略,正如您在前面的图表中看到的那样,它可以帮助您管理和控制对 EC2 自动扩展组的更新。

如果您打开 todobackend-aws 存储库并浏览到stack.yml文件中的 CloudFormation 模板,您可以向ApplicationAutoscaling资源添加更新策略,如以下示例所示:

...
...
Resources:
  ...
  ...
  ApplicationAutoscaling:
    Type: AWS::AutoScaling::AutoScalingGroup
    CreationPolicy:
      ResourceSignal:
        Count: !Ref ApplicationDesiredCount
        Timeout: PT15M
    UpdatePolicy:
 AutoScalingRollingUpdate:
 MinInstancesInService: !Ref ApplicationDesiredCount
 MinSuccessfulInstancesPercent: 100
 WaitOnResourceSignals: "true"
 PauseTime: PT15M
  ...
  ...

配置 CloudFormation 自动扩展组更新策略

在上面的示例中,UpdatePolicy设置应用于ApplicationAutoscaling资源,该资源配置 CloudFormation 根据以下AutoScalingRollingUpdate配置参数来编排滚动更新,每当自动扩展组中的实例需要被替换(更新)时:

  • MinInstancesInService:在滚动更新期间必须处于服务状态的最小实例数。这里的标准方法是指定自动扩展组的期望计数,这意味着自动扩展将临时增加大小,以便在添加新实例时保持所需实例的最小数量。

  • MinSuccessfulInstancesPercent:必须成功部署的新实例的最低百分比,以便将滚动更新视为成功。如果未达到此百分比,则 CloudFormation 将回滚堆栈更改。

  • WaitOnResourceSignals:当设置为 true 时,指定 CloudFormation 在考虑实例成功部署之前等待每个实例发出的成功信号。这需要您的 EC2 实例在第六章安装并在第七章配置的cfn-bootstrap脚本向 CloudFormation 发出信号,表示实例初始化已完成。

  • PauseTime:当配置了WaitOnResourceSignals时,指定等待每个实例发出 SUCCESS 信号的最长时间。此值以 ISO8601 格式表示,在下面的示例中配置为等待最多 15 分钟。

然后,使用aws cloudformation deploy命令部署您的更改,如下例所示,您的自动扩展组现在将应用更新策略:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file stack.yml \
 --stack-name todobackend --parameter-overrides $(cat dev.cfg) \
 --capabilities CAPABILITY_NAMED_IAM
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend
  ...
  ...

配置 CloudFormation 自动扩展组更新策略

此时,您现在可以更新堆栈以使用您在第一个示例中创建的新 AMI。这需要您首先更新 todobackend-aws 存储库根目录下的dev.cfg文件:

ApplicationDesiredCount=1
ApplicationImageId=ami-77893508
ApplicationImageTag=5fdbe62
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

更新 ECS AMI

然后,使用相同的aws cloudformation deploy命令部署更改。

在部署运行时,如果您打开 AWS 控制台,浏览到 CloudFormation 仪表板,并选择 todobackend 堆栈事件选项卡,您应该能够看到 CloudFormation 如何执行滚动更新:

CloudFormation 滚动更新

在前面的屏幕截图中,您可以看到 CloudFormation 首先临时增加了自动扩展组的大小,因为它需要始终保持至少一个实例在服务中。一旦新实例向 CloudFormation 发出 SUCCESS 信号,自动扩展组中的旧实例将被终止,滚动更新就完成了。

此时,您可能会感到非常高兴——只需对 CloudFormation 配置进行小小的更改,您就能够为堆栈添加滚动更新。不过,有一个问题,就是旧的 EC2 实例被立即终止。这实际上会导致服务中断,如果您导航到 CloudWatch 控制台,选择指标,在所有指标选项卡中选择 ECS | ClusterName,然后选择名为 todobackend-cluster 的集群的 MemoryReservation 指标,您可以看到这种迹象。

在您单击图形化指标选项卡并将统计列更改为最小值,周期更改为 1 分钟后,将显示以下屏幕截图:

ECS 内存预留

如果您回顾之前的屏幕截图中的时间线,您会看到在 21:17:33 旧的 ECS 容器实例被终止,在之前的屏幕截图中,您可以看到集群内存预留在 21:18(09:18)降至 0%。这表明在这个时间点上,没有实际的容器在运行,因为集群内存保留的百分比为 0,这表明在旧实例突然终止后,ECS 尝试将 todobackend 服务恢复到新的 ECS 容器实例时出现了短暂的中断。

因为最小的 CloudWatch 指标分辨率是 1 分钟,如果 ECS 能够在一分钟内恢复 ECS 服务,您可能无法观察到在前一个图表中降至 0%的情况,但请放心,您的应用程序确实会中断。

显然,这并不理想,正如我们之前讨论的那样,我们现在需要引入 EC2 自动扩展生命周期挂钩来解决这种情况。

创建 EC2 自动扩展生命周期挂钩

为了解决 EC2 实例终止影响我们的 ECS 服务的问题,我们现在需要创建一个 EC2 自动扩展生命周期挂钩,它将通知我们 EC2 实例即将被终止。回顾第一个图表,这需要几个资源:

  • 实际的生命周期挂钩

  • 授予 EC2 自动扩展组权限向 SNS 主题发布生命周期挂钩通知的生命周期挂钩角色

  • SNS 主题,生命周期挂钩可以发布和订阅

以下示例演示了创建生命周期挂钩、生命周期挂钩角色和 SNS 主题:

...
...
Resources:
  ...
  ...
 LifecycleHook:
 Type: AWS::AutoScaling::LifecycleHook
 Properties:
 RoleARN: !Sub ${LifecycleHookRole.Arn}
 AutoScalingGroupName: !Ref ApplicationAutoscaling
 DefaultResult: CONTINUE
 HeartbeatTimeout: 900
 LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING
 NotificationTargetARN: !Ref LifecycleHookTopic
 LifecycleHookRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Action:
 - sts:AssumeRole
 Effect: Allow
 Principal:
 Service: autoscaling.amazonaws.com
 Policies:
- PolicyName: LifecycleHookPermissions
 PolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Sid: PublishNotifications
 Action: 
 - sns:Publish
 Effect: Allow
 Resource: !Ref LifecycleHookTopic
 LifecycleHookTopic:
 Type: AWS::SNS::Topic
 Properties: {}
  LifecycleHookSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !Sub ${LifecycleHookFunction.Arn}
      Protocol: lambda
      TopicArn: !Ref LifecycleHookTopic    ...
    ...

在 CloudFormation 中创建生命周期挂钩资源

在前面的示例中,LifecycleHook资源创建了一个新的钩子,该钩子与ApplicationAutoscaling资源相关联,使用AutoScalingGroupName属性,并由 EC2 实例触发,这些实例即将被终止,如LifecycleTransition属性配置的autoscaling:EC2_INSTANCE_TERMINATING值所指定的那样。该钩子配置为向名为LifecycleHookTopic的新 SNS 主题资源发送通知,链接的LifecycleHookRole IAM 角色授予autoscaling.amazonaws.com服务(如角色的AssumeRolePolicyDocument部分中所指定的)权限,以将生命周期钩子事件发布到此主题。DefaultResult属性指定了在HeartbeatTimeout期间到达并且没有收到钩子响应时应创建的默认结果,例如,在本示例中,发送一个CONTINUE消息,指示 Auto Scaling 服务继续处理可能已注册的任何其他生命周期钩子。DefaultResult属性的另一个选项是发送一个ABANDON消息,这仍然指示 Auto Scaling 服务继续进行实例终止,但放弃处理可能配置的任何其他生命周期钩子。

最终的LifecycleHookSubscription资源创建了对LifecycleHookTopic SNS 主题资源的订阅,订阅了一个名为LifecycleHookFunction的 Lambda 函数资源,我们将很快创建,这意味着每当消息发布到 SNS 主题时,将调用此函数。

创建用于消耗生命周期钩子的 Lambda 函数

有了各种生命周期钩子资源,谜题的最后一块是创建一个 Lambda 函数和相关资源,该函数将订阅您在上一节中定义的生命周期钩子 SNS 主题,并最终在发出信号表明生命周期钩子操作可以继续之前执行 ECS 容器实例排空。

让我们首先关注 Lambda 函数本身以及它将需要执行的相关源代码:

...
...
Resources: LifecycleHookFunction:
    Type: AWS::Lambda::Function
    DependsOn:
      - LifecycleHookFunctionLogGroup
    Properties:
      Role: !Sub ${LifecycleFunctionRole.Arn}
      FunctionName: !Sub ${AWS::StackName}-lifecycleHooks
      Description: !Sub ${AWS::StackName} Autoscaling Lifecycle Hook
      Environment:
        Variables:
          ECS_CLUSTER: !Ref ApplicationCluster
      Code:
        ZipFile: |
          import os, time
          import json
          import boto3
          cluster = os.environ['ECS_CLUSTER']
          # AWS clients
          ecs = boto3.client('ecs')
          sns = boto3.client('sns')
          autoscaling = boto3.client('autoscaling')

          def handler(event, context):
            print("Received event %s" % event)
            for r in event.get('Records'):
              # Parse SNS message
              message = json.loads(r['Sns']['Message'])
              transition, hook = message['LifecycleTransition'], message['LifecycleHookName']
              group, ec2_instance = message['AutoScalingGroupName'], message['EC2InstanceId']
              if transition != 'autoscaling:EC2_INSTANCE_TERMINATING':
                print("Ignoring lifecycle transition %s" % transition)
                return
              try:
                # Get ECS container instance ARN
                ecs_instance_arns = ecs.list_container_instances(
                  cluster=cluster
                )['containerInstanceArns']
                ecs_instances = ecs.describe_container_instances(
                  cluster=cluster,
                  containerInstances=ecs_instance_arns
                )['containerInstances']
                # Find ECS container instance with same EC2 instance ID in lifecycle hook message
                ecs_instance_arn = next((
                  instance['containerInstanceArn'] for instance in ecs_instances
                  if instance['ec2InstanceId'] == ec2_instance
                ), None)
                if ecs_instance_arn is None:
                  raise ValueError('Could not locate ECS instance')
                # Drain instance
                ecs.update_container_instances_state(
                  cluster=cluster,
                  containerInstances=[ecs_instance_arn],
                  status='DRAINING'
                )
                # Check task count on instance every 5 seconds
                count = 1
                while count > 0 and context.get_remaining_time_in_millis() > 10000:
                  status = ecs.describe_container_instances(
                    cluster=cluster,
                    containerInstances=[ecs_instance_arn],
                  )['containerInstances'][0]
                  count = status['runningTasksCount']
                  print("Sleeping...")
                  time.sleep(5)
                if count == 0:
                  print("All tasks drained - sending CONTINUE signal")
                  autoscaling.complete_lifecycle_action(
                    LifecycleHookName=hook,
                    AutoScalingGroupName=group,
                    InstanceId=ec2_instance,
                    LifecycleActionResult='CONTINUE'
                  )
                else:
                  print("Function timed out - republishing SNS message")
                  sns.publish(TopicArn=r['Sns']['TopicArn'], Message=r['Sns']['Message'])
              except Exception as e:
                print("A failure occurred with exception %s" % e)
                autoscaling.complete_lifecycle_action(
                  LifecycleHookName=hook,
                  AutoScalingGroupName=group,
                  InstanceId=ec2_instance,
                  LifecycleActionResult='ABANDON'
                )
      Runtime: python3.6
      MemorySize: 128
      Timeout: 300
      Handler: index.handler
  LifecycleHookFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: Delete
    Properties:
      LogGroupName: !Sub /aws/lambda/${AWS::StackName}-lifecycleHooks
      RetentionInDays: 7    ...
    ...

创建用于处理生命周期钩子的 Lambda 函数

Lambda 函数比我们迄今为止处理的要复杂一些,但如果您有 Python 经验,它仍然是一个相对简单的函数,应该相对容易理解。

该函数首先定义所需的库,并查找名为ECS_CLUSTER的环境变量,这是必需的,以便函数知道生命周期挂钩与哪个 ECS 集群相关,并且通过 Lambda 函数资源的Environment属性传递此环境变量值。

接下来,函数声明了三个 AWS 客户端:

  • ecs:与 ECS 通信,以审查 ECS 容器实例信息并根据生命周期挂钩中接收的 EC2 实例 ID 排空正确的实例。

  • autoscaling:在生命周期挂钩可以继续时,向 EC2 自动缩放服务发出信号。

  • sns:如果 Lambda 函数即将达到最长五分钟的执行超时,并且 ECS 容器实例尚未排空,则重新发布生命周期挂钩事件。这将再次调用 Lambda 函数,直到 ECS 容器实例完全排空。

handler方法定义了 Lambda 函数的入口点,并首先提取出许多变量,这些变量从接收到的 SNS 消息中捕获信息,包括生命周期挂钩事件类型(transition变量)、挂钩名称(hook变量)、Auto Scaling 组名称(group变量)和 EC2 实例 ID(ec2_instance变量)。然后立即进行检查,以验证生命周期挂钩事件类型是否与 EC2 实例终止事件相关,如果事件类型(在 transition 变量中捕获)不等于值autoscaling:EC2_INSTANCE_TERMINATING,则函数立即返回,有效地忽略该事件。

假设事件确实与 EC2 实例的终止有关,处理程序接下来通过ecs客户端查询 ECS 服务,首先描述配置集群中的所有实例,然后尝试定位与生命周期挂钩事件捕获的 EC2 实例 ID 匹配的 ECS 容器实例。如果找不到实例,则会引发ValueError异常,该异常将被 catch 语句捕获,导致记录错误并使用ABANDON的结果完成生命周期挂钩。如果找到实例,处理程序将继续通过在ecs客户端上调用update_container_instances_state()方法来排水实例,该方法将实例的状态设置为DRAINING,这意味着 ECS 将不再将任何新任务调度到该实例,并尝试将现有任务迁移到集群中的其他实例。在这一点上,处理程序需要等待在实例上运行的所有当前 ECS 任务被排水,这可以通过每五秒轮询一次 ECS 任务计数的while循环来实现,直到任务计数减少到零。您可以无限期地尝试这样做,但是在撰写本文时,Lambda 具有最长五分钟的执行时间限制,因此while循环使用context.get_remaining_time_in_millis()方法来检查 Lambda 执行超时是否即将到达。

context对象是由 Lambda 运行时环境传递给处理程序方法的对象,其中包括有关 Lambda 环境的信息,包括内存、CPU 和剩余执行时间。

如果任务计数减少到零,您可以安全地终止 ECS 容器实例,自动缩放客户端将使用CONTINUE的结果完成生命周期挂钩,这意味着 EC2 自动缩放服务将继续处理任何其他注册的挂钩并终止实例。如果任务计数在函数即将退出之前没有减少到零,则函数只是重新发布原始的生命周期挂钩通知,这将重新启动函数。由于函数中的所有操作都是幂等的,即更新已经处于排水状态的 ECS 容器实例的状态为 DRAINING 会导致相同的排水状态,因此这种方法是安全的,也是克服 Lambda 执行超时限制的一种非常简单而优雅的方法。

为生命周期挂钩 Lambda 函数配置权限

Lambda 函数现在已经就位,最后的配置任务是为 Lambda 函数执行的各种 API 调用和操作添加所需的权限:

...
...
Resources: LifecycleHookPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref LifecycleHookFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref LifecycleHookTopic
  LifecycleFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: LifecycleHookPermissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: ListContainerInstances
                Effect: Allow
                Action:
                  - ecs:ListContainerInstances
                Resource: !Sub ${ApplicationCluster.Arn}
              - Sid: ManageContainerInstances
                Effect: Allow
                Action:
                  - ecs:DescribeContainerInstances
                  - ecs:UpdateContainerInstancesState
                Resource: "*"
                Condition:
                  ArnEquals:
                    ecs:cluster: !Sub ${ApplicationCluster.Arn}
              - Sid: Publish
                Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref LifecycleHookTopic
              - Sid: CompleteLifecycleAction
                Effect: Allow
                Action:
                  - autoscaling:CompleteLifecycleAction
                Resource: !Sub arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${ApplicationAutoscaling}
              - Sid: ManageLambdaLogs
                Effect: Allow
                Action:
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: !Sub ${LifecycleHookFunctionLogGroup.Arn}    LifecycleHookFunction:
      Type: AWS::Lambda::Function
    ...
    ...

为生命周期挂钩 Lambda 函数配置权限

在前面的示例中,需要一个名为LifecycleHookPermission的资源,类型为AWS::Lambda::Permission,它授予 SNS 服务(由Principal属性引用)调用 Lambda 函数(由LambdaFunction属性引用)的权限,用于 SNS 主题发布的通知(由SourceArn属性引用)。每当您需要授予另一个 AWS 服务代表您调用 Lambda 函数的能力时,通常需要采用这种配置权限的方法,尽管也有例外情况(例如 CloudFormation 自定义资源用例,其中 CloudFormation 隐含具有这样的权限)。

您还需要为 Lambda 函数创建一个名为LambdaFunctionRole的 IAM 角色,该角色授予函数执行各种任务和操作的能力,包括:

  • 列出、描述和更新应用程序集群中的 ECS 容器实例

  • 如果 Lambda 函数即将超时,则重新发布生命周期挂钩事件到 SNS

  • 在 ECS 容器实例排空后完成生命周期操作

  • 将日志写入 CloudWatch 日志

部署和测试自动扩展生命周期挂钩

您现在可以使用aws cloudformation deploy命令部署完整的自动扩展生命周期挂钩解决方案,就像本章前面演示的那样。

部署完成后,为了测试生命周期管理是否按预期工作,您可以执行一个简单的更改,强制替换 ECS 集群中当前的 ECS 容器实例,即恢复您在本章前面所做的 AMI 更改:

ApplicationDesiredCount=1
ApplicationImageId=ami-ec957491
ApplicationImageTag=5fdbe62
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

恢复 ECS AMI

现在,一旦您部署了这个更改,再次使用aws cloudformation deploy命令,就像之前的示例演示的那样,接下来切换到 CloudFormation 控制台,当事件引发终止现有的 EC2 实例时,快速导航到 ECS 仪表板并选择您的 ECS 集群。在容器实例选项卡上,您应该看到您的 ECS 容器实例中的一个状态正在排空,如下面的屏幕截图所示,一旦所有任务从这个实例中排空,生命周期挂钩函数将向 EC2 自动扩展服务发出信号,以继续终止实例:

ECS 容器实例排空

如果您重复执行前面屏幕截图中的步骤,以查看 ECS 容器实例在排空和终止期间的集群内存保留量,您应该会看到一个类似下面示例中的图表:

ECS 容器实例排空期间的集群内存保留

在前面的屏幕截图中,请注意在滚动更新期间,集群内存保留量从未降至 0%。由于在滚动升级期间集群中有两个实例,内存利用率百分比确实会发生变化,但我们排空 ECS 容器实例的能力确保了在集群上运行的应用程序的不间断服务。

作为最后的检查,您还可以导航到生命周期挂钩函数的 CloudWatch 日志组,如下面的屏幕截图所示:

生命周期挂钩函数日志

在前面的屏幕截图中,您可以看到该函数在容器实例排空时定期休眠,大约两分钟后,在这种情况下,所有任务排空并且函数向自动扩展服务发送CONTINUE信号以继续挂钩。

摘要

在本章中,您创建了一个解决方案,用于管理 ECS 容器实例的生命周期,并确保在需要终止和替换 ECS 集群中的 ECS 容器实例时,运行在 ECS 集群上的应用程序和服务不会受到影响。

您学习了如何通过利用 CloudFormation 更新策略来配置 EC2 自动扩展组的滚动更新,从而控制新实例如何以滚动方式添加到您的自动扩展组。您发现这个功能在自动扩展和 EC2 实例级别上运行良好,但是您发现在集群中突然终止现有 ECS 容器实例会导致应用程序中断。

为了解决这个挑战,您创建了一个注册为EC2_INSTANCE_TERMINATING事件的 EC2 生命周期挂钩,并配置此挂钩以将通知发布到 SNS 主题,然后触发一个 Lambda 函数。该函数负责定位与即将终止的 EC2 实例相关联的 ECS 容器实例,排空容器实例,然后等待直到 ECS 任务计数达到 0,表示实例上的所有 ECS 任务都已终止并替换。如果 ECS 容器实例的执行时间超过 Lambda 函数的五分钟最大执行时间,您学会了可以简单地重新发布包含生命周期挂钩信息的 SNS 事件,这将触发函数的新调用,这个过程可以无限期地继续,直到实例上的 ECS 任务计数达到 0。

在下一章中,您将学习如何动态管理 ECS 集群的容量,这对支持应用程序的自动扩展要求至关重要。这涉及不断向您的 ECS 集群添加和删除 ECS 容器实例,因此您可以看到,本章介绍的 ECS 容器实例生命周期机制对确保您的应用程序不受任何自动扩展操作影响至关重要。

问题

  1. 真/假:当您终止 ECS 容器实例时,该实例将自动将运行的 ECS 任务排空到集群中的另一个实例。

  2. 您可以接收哪些类型的 EC2 自动扩展生命周期挂钩?

  3. 一旦完成处理 EC2 自动扩展生命周期挂钩,您可以发送哪些类型的响应?

  4. 真/假:EC2 自动扩展生命周期挂钩可以向 AWS Kinesis 发布事件。

  5. 您创建了一个处理生命周期挂钩并排空 ECS 容器实例的 Lambda 函数。您注意到有时这需要大约 4-5 分钟,但通常需要 15 分钟。您可以采取什么措施来解决这个问题?

  6. 您可以配置哪个 CloudFormation 功能以启用自动扩展组的滚动更新?

  7. 您想要执行滚动更新,并确保在更新期间始终至少有当前所需数量的实例在服务中。您将如何实现这一点?

  8. 在使用 CloudFormation 订阅 Lambda 函数到 SNS 主题时,您需要创建什么类型的资源以确保 SNS 服务具有适当的权限来调用函数?

进一步阅读

您可以查看以下链接以获取有关本章涵盖的主题的更多信息:

第十二章:ECS 自动扩展

弹性是云计算的基本原则之一,描述了根据需求自动扩展应用程序的能力,以确保客户获得最佳体验和响应性,同时通过仅在实际需要时提供额外容量来优化成本。

AWS 支持通过两个关键功能来扩展使用 ECS 部署的 Docker 应用程序:

  • 应用程序自动扩展:这使用 AWS 应用程序自动扩展服务,并支持在 ECS 服务级别进行自动扩展,您的 ECS 服务运行的 ECS 任务或容器的数量可以增加或减少。

  • EC2 自动扩展:这使用 EC2 自动扩展服务,并支持在 EC2 自动扩展组级别进行自动扩展,您的自动扩展组中的 EC2 实例数量可以增加或减少。在 ECS 的上下文中,您的 EC2 自动扩展组通常对应于 ECS 集群,而单独的 EC2 实例对应于 ECS 容器实例,因此 EC2 自动扩展正在管理您的 ECS 集群的整体容量。

由于这里涉及两种范式,为您的 Docker 应用程序实现自动扩展可能是一个具有挑战性的技术概念,更不用说以可预测和可靠的方式成功实现了。更糟糕的是,截至撰写本书的时间,应用程序自动扩展和 EC2 自动扩展是完全独立的功能,彼此之间没有集成,因此,您需要确保这两个功能能够相互配合。

在分析这些功能时,好消息是应用程序自动扩展非常容易理解和实现。使用应用程序自动扩展,您只需定义应用程序的关键性能指标,并增加(增加)或减少(减少)运行应用程序的 ECS 任务的数量。坏消息是,当应用于在 ECS 集群中自动扩展 ECS 容器实例时,EC2 自动扩展绝对是一个更难处理的命题。在这里,您需要确保您的 ECS 集群为在集群中运行的所有 ECS 任务提供足够的计算、内存和网络资源,并确保您的集群能够在应用程序自动扩展时增加或减少容量。

扩展 ECS 集群的另一个挑战是确保您不会在缩减/缩小事件期间从集群中移除的 ECS 容器实例上中断服务并排空正在运行的任务。第十一章中实施的 ECS 生命周期挂钩解决方案会为您处理这一问题,确保在允许 EC2 自动扩展服务将实例移出服务之前,ECS 容器实例会排空所有正在运行的任务。

解决扩展 ECS 集群资源的问题是本章的主要焦点,一旦解决了这个问题,您将能够任意扩展您的 ECS 服务,并确保您的 ECS 集群会动态地添加或移除 ECS 容器实例,以确保您的应用程序始终具有足够和最佳的资源。在本章中,我们将首先专注于解决 ECS 集群容量管理的问题,然后讨论如何配置 AWS 应用程序自动扩展服务以自动扩展您的 ECS 服务和应用程序。

将涵盖以下主题:

  • 了解 ECS 集群资源

  • 计算 ECS 集群容量

  • 实施 ECS 集群容量管理解决方案

  • 配置 CloudWatch 事件以触发容量管理计算

  • 发布与 ECS 集群容量相关的自定义 CloudWatch 指标

  • 配置 CloudWatch 警报和 EC2 自动扩展策略以扩展您的 ECS 集群

  • 配置 ECS 应用程序自动扩展

技术要求

以下列出了完成本章所需的技术要求:

  • AWS 账户的管理员访问权限

  • 根据第三章的说明配置本地 AWS 配置文件

  • AWS CLI

  • 本章是从第十一章继续下去的,因此需要您成功完成那里定义的所有配置任务。

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch12

查看以下视频以查看代码的实际操作:

bit.ly/2PdgtPr

了解 ECS 集群资源

在您开始管理 ECS 集群的容量之前,您需要清楚而牢固地了解影响 ECS 集群容量的各种资源。

一般来说,有三个关键资源需要考虑:

  • CPU

  • 内存

  • 网络

CPU 资源

CPU是 Docker 支持和管理的核心资源。ECS 利用 Docker 的 CPU 资源管理能力,并公开通过 ECS 任务定义管理这些资源的能力。ECS 根据CPU 单位定义 CPU 资源,其中单个 CPU 核心包含 1,024 个 CPU 单位。在配置 ECS 任务定义时,您需要指定 CPU 保留,这定义了每当 CPU 时间存在争用时将分配给应用程序的 CPU 时间。

请注意,CPU 保留并不限制 ECS 任务可以使用多少 CPU-每个 ECS 任务都可以自由地突发并使用所有可用的 CPU 资源-当 CPU 存在争用时才会应用保留,并且 Docker 会根据每个运行的 ECS 任务的配置保留公平地分配 CPU 时间。

重要的是要理解,每个 CPU 保留都会从给定的 ECS 容器实例的可用 CPU 容量中扣除。例如,如果您的 ECS 容器实例有 2 个 CPU 核心,那就相当于总共有 2,048 个 CPU 单位。如果您运行了配置为 500、600 和 700 CPU 单位的 3 个 ECS 任务,这意味着您的 ECS 容器实例有 2,048 - (500 + 600 + 700),或 248 个 CPU 单位可用。请注意,每当 ECS 调度程序需要运行新的 ECS 任务时,它将始终确保目标 ECS 容器实例具有足够的 CPU 容量来运行任务。根据前面的例子,如果需要启动一个保留 400 个 CPU 单位的新 ECS 任务,那么剩余 248 个 CPU 单位的 ECS 容器实例将不被考虑,因为它当前没有足够的 CPU 资源可用:

分配 CPU 资源

在配置 CPU 保留方面,您已经学会了如何通过 CloudFormation 进行此操作-请参阅第八章使用 ECS 部署应用程序中的使用 CloudFormation 定义 ECS 任务定义示例,在该示例中,您通过一个名为Cpu的属性为 todobackend 容器定义分配了 245 的值。

内存资源

内存是另一个通过 Docker 管理的基本资源,其工作方式类似于 CPU,尽管您可以为给定的 ECS 任务保留和限制内存容量,但在管理 CPU 容量时,您只能保留(而不是限制)CPU 资源。当涉及到配置 ECS 任务的内存时,这种额外的限制内存的能力会导致三种情况:

  • 仅内存保留:这种情况的行为与 CPU 保留的工作方式相同。Docker 将从 ECS 容器实例的可用内存中扣除配置的保留,并在内存有争用时尝试分配这些内存。ECS 将允许 ECS 任务使用 ECS 容器实例支持的最大内存量。内存保留是在 ECS 任务容器定义中使用MemoryReservation属性进行配置的。

  • 内存保留+限制:在这种情况下,内存保留的工作方式与前一种情况相同,但 ECS 任务可以使用的最大内存量受到配置内存限制的限制。一般来说,配置内存保留和内存限制被认为是最佳选择。内存限制是在 ECS 任务容器定义中使用Memory属性进行配置的。

  • 仅内存限制:在这种情况下,ECS 将内存保留和内存限制值视为相同,这意味着 ECS 将从可用的 ECS 容器实例内存中扣除配置的内存限制,并且还将限制内存使用到相同的限制。

配置内存保留和限制是直接的-如果您回顾一下第八章使用 CloudFormation 定义 ECS 任务定义部分,您会发现您可以配置MemoryReservation属性来配置 395 MB 的保留。如果您想配置内存限制,您还需要使用适当的最大限制值配置Memory属性。

网络资源

CPU 和内存是您期望您的 ECS 集群控制和管理的典型和明显的资源。另一组不太明显的资源是网络资源,可以分为两类:

  • 主机网络端口:每当您为 ECS 服务配置静态端口映射时,主机网络端口是您需要考虑的资源。原因是静态端口映射使用 ECS 容器实例公开的一个常用端口 - 例如,如果您创建了一个 ECS 任务,其中静态端口映射公开了给定应用程序的端口 80,那么如果端口 80 仍在使用中,您将无法在同一 ECS 容器实例主机上部署 ECS 任务的另一个实例。

  • 主机网络接口:如果您正在使用 ECS 任务网络,重要的是要了解,该功能目前要求您为每个 ECS 任务实现单个弹性网络接口(ENI)。因为 EC2 实例对每种实例类型支持的 ENI 数量有限制,因此使用 ECS 任务网络配置的 ECS 任务数量将受到 ECS 容器实例可以支持的 ENI 最大数量的限制。

计算 ECS 集群容量

在计算 ECS 集群容量之前,您需要清楚地了解哪些资源会影响容量以及如何计算每种资源的当前容量。一旦为每个单独的资源定义了这一点,您就需要在所有资源上应用一个综合计算,这将导致最终计算出当前容量。

计算容量可能看起来是一项相当艰巨的任务,特别是当考虑到不同类型的资源以及它们的行为时:

  • CPU:这是您可以使用的最简单的资源,因为每个 CPU 预留只是从集群的可用 CPU 容量中扣除。

  • 内存:根据内存计算集群的当前容量与 CPU 相同,因为内存预留会从集群的可用内存容量中扣除。根据本章早期讨论,内存预留的配置受到内存限制和内存预留的各种排列组合的影响,但基本上一旦确定了内存预留,计算方式与 CPU 资源相同。

  • 静态网络端口:如果您的 ECS 集群需要支持使用静态端口映射的任何容器,那么您需要将您的 ECS 容器实例网络端口视为一种资源。例如,如果一个容器应用程序始终在 ECS 容器实例上使用端口 80,那么您只能在每个实例上部署一个容器,而不管该实例可能拥有多少 CPU、内存或其他资源。

  • 网络接口:如果您有任何配置为 ECS 任务网络的 ECS 服务或任务,重要的是要了解,您目前只能在一个网络接口上运行一个 ECS 任务。例如,如果您正在运行一个 t2.micro 实例,这意味着您只能在一个实例上运行一个启用了任务网络的 ECS 任务,因为 t2.micro 只能支持一个弹性网络接口用于 ECS 任务网络。

鉴于示例应用程序未使用 ECS 任务网络,并且正在使用动态端口映射进行部署,我们在本章的其余部分只考虑 CPU 和内存资源。如果您对包含静态网络端口的示例解决方案感兴趣,请查看我的《使用亚马逊网络服务进行生产中的 Docker》课程的 Auto Scaling ECS Applications 模块。

挑战在于如何考虑所有 ECS 服务和任务,然后根据所有前述考虑做出决定,决定何时应该扩展或缩减集群中实例的数量。我见过的一种常见且有些天真的方法是独立地处理每个资源,并相应地扩展您的实例。例如,一旦您的集群的内存容量用尽,您就会添加一个新的容器实例,同样,如果您的集群即将耗尽 CPU 容量,也会这样做。如果您纯粹考虑扩展的能力,这种方法是有效的,但是当您想要缩减集群时,它就不起作用了。如果您仅基于当前内存容量来缩减集群,那么在 CPU 容量方面,您可能会过早地缩减,因为如果您从集群中移除一个实例,您的集群可能没有足够的 CPU 容量。

这将使您的集群陷入自动扩展循环中-也就是说,您的集群不断地扩展然后再缩小,这是因为各个资源容量独立地驱动着缩小和扩展的决策,而没有考虑对其他资源的影响。

解决这一挑战的关键在于您需要做出单一的扩展或缩小决策,并考虑您集群中所有适用的资源。这可能会使整体问题看起来更难解决,但实际上它非常简单。解决方案的关键在于您始终考虑最坏情况,并基于此做出决策。例如,如果您的集群中有足够的 CPU 和内存容量,但是所有静态端口映射都在所有集群实例上使用,最坏情况是,如果您缩小集群并删除一个实例,您将无法再支持使用受影响的静态端口映射的当前 ECS 任务。因此,这里的决策是简单的,纯粹基于最坏情况-所有其他情况都被忽略。

计算容器容量

在计算集群容量时的一个关键考虑因素是,您需要对资源容量进行归一化计算,以便每个资源的容量可以以一个通用和等效的格式来表达,独立于每个单独资源的具体计量单位。这在做出考虑所有资源的集体决策时至关重要,而这样做的一种自然方式是以当前可用的未分配资源来支持多少额外的 ECS 任务数量来表达资源容量。此外,与最坏情况的主题保持一致,您不需要考虑所有需要支持的不同 ECS 任务-您只需要考虑当前正在计算容量的资源的最坏情况的 ECS 任务(需要最多资源的任务)。

例如,如果您有两个需要分别需要 200 CPU 单位和 400 CPU 单位的 ECS 任务,那么您只需要根据需要 400 CPU 单位的 ECS 任务来计算 CPU 容量:

公式中带有有点奇怪的倒立的 A 的表达意思是“对于给定的 taskDefinitions 集合中的每个 taskCpu 值”。

一旦确定了需要支持的最坏情况 ECS 任务,就可以开始计算集群目前可以支持的额外 ECS 任务数量。假设最坏情况的 ECS 任务需要 400 个 CPU 单位,如果现在假设您的集群中有两个实例,每个实例都有 600 个 CPU 单位的空闲容量,这意味着您目前可以支持额外的 2 个 ECS 任务:

计算容器容量

这里需要注意的是,您需要按照每个实例的基础进行计算,而不仅仅是在整个集群上进行计算。使用先前的例子,如果您考虑整个集群的空闲 CPU 容量,您有 1,200 个 CPU 单位可用,因此您将计算出三个 ECS 任务的空闲容量,但实际情况是您不能分割ECS 任务跨越 2 个实例,因此如果您按照每个实例的空闲容量进行考虑,显然您只能在每个实例上支持一个额外的 ECS 任务,从而得到集群中总共 2 个额外的 ECS 任务的正确总数。

这可以形式化为一个数学方程,如下所示,其中公式右侧的注释表示取floor或计算的最低最近整数值,并且代表集群中的一个实例:

如果您对内存资源重复之前的方法,将计算一个单独的计算,以内存的形式定义集群的当前备用容量。如果我们假设内存的最坏情况 ECS 任务需要 500MB 内存,并且两个实例都有 400MB 可用,显然就内存而言,集群目前没有备用容量:

如果现在考虑 CPU 的两个先前计算(目前有两个空闲的 ECS 任务)和内存(目前没有空闲的 ECS 任务),显然最坏情况是内存容量计算为零个空闲的 ECS 任务,可以形式化如下:

请注意,虽然我们没有将静态网络端口和网络接口的计算纳入到我们的解决方案中以帮助简化,但一般的方法是相同的 - 计算每个实例的当前容量并求和以获得资源的整体集群容量值,然后将该值纳入整体集群容量的计算中:

决定何时扩展

在这一点上,我们已经确定您需要评估集群中每个当前资源容量,并以当前集群可以支持的空闲或备用 ECS 任务数量来表达,然后使用最坏情况的计算(最小值)来确定您当前集群的整体容量。一旦您完成了这个计算,您需要决定是否应该扩展集群,或者保持当前集群容量不变。当然,您还需要决定何时缩小集群,但我们将很快单独讨论这个话题。

现在,我们将专注于是否应该扩展集群(即增加容量),因为这是更简单的情景来评估。规则是,至少在当前集群容量小于 1 时,您应该扩展您的集群:

换句话说,如果您当前的集群容量不足以支持一个更糟的情况的 ECS 任务,您应该向 ECS 集群添加一个新实例。这是有道理的,因为您正在努力确保您的集群始终具有足够的容量来支持新的 ECS 任务的启动。当然,如果您希望获得更多的空闲容量,您可以将此阈值提高,这可能适用于更动态的环境,其中容器经常启动和关闭。

计算空闲主机容量

如果我们现在考虑缩减规模的情况,这就变得有点难以确定了。我们讨论过的备用 ECS 任务容量计算是相关且必要的,但是你需要从这些角度思考:如果你从集群中移除一个 ECS 容器实例,是否有足够的容量来运行所有当前正在运行的 ECS 任务,以及至少还有一个额外的 ECS 任务的备用容量?另一种表达方式是计算集群的空闲主机容量——如果集群中有多于 1.0 个主机处于空闲状态,那么你可以安全地缩减集群规模,因为减少一个主机会导致剩余的正值非零容量。请注意,我们指的是整个集群中的空闲主机容量——所以把这看作更像是一个虚拟主机计算,因为你可能不会有完全空闲的主机。这个虚拟主机计算是安全的,因为如果我们从集群中移除一个主机,我们在第十一章管理 ECS 基础设施生命周期中介绍的生命周期钩子和 ECS 容器实例排空功能将确保任何运行在要移除的实例上的容器将被迁移到集群中的其他实例上。

还需要了解的是,空闲主机容量必须大于 1.0,而不是等于 1.0,因为你必须有足够的备用容量来运行一个 ECS 任务,否则你将触发一个扩展规模的动作,导致自动扩展的扩展/缩减循环。

要确定当前的空闲主机容量,我们需要了解以下内容:

  • 每个不同类型的 ECS 资源对应的每个 ECS 容器实例可以运行的最大 ECS 任务数量(表示为)。

  • 整个集群中每种类型的 ECS 资源的当前空闲容量(表示为),这是我们在确定是否扩展规模时已经计算过的。

有了这些信息,你可以按照以下方式计算给定资源的空闲主机容量:

空闲主机容量示例

为了更清楚地说明这一点,让我们通过以下示例来进行计算,如下图所示,假设以下情况:

  • 最坏情况下需要 400 个 CPU 单位的 ECS 任务 CPU 要求

  • 最坏情况下需要 200 MB 的 ECS 任务内存

  • 每个 ECS 容器实例支持最多 1,000 个 CPU 单位和 1,000 MB 内存

  • 当前在 ECS 集群中有两个 ECS 容器实例

  • 每个 ECS 容器实例目前有 600 个 CPU 单位的空闲容量。使用之前讨论的空闲容量计算,这相当于集群中的当前空闲容量为 2

  • ECS 任务的 CPU 资源,我们将称之为 

  • 每个 ECS 容器实例目前有 800 MB 的空闲容量。使用之前讨论的空闲容量计算,这相当于集群中的当前空闲容量为 8 个 ECS 任务的内存资源,我们将称之为 

空闲主机容量

我们可以首先计算  值如下:

对于 CPU,它等于 2,对于内存等于5

通过计算这些值并了解集群当前的空闲容量,我们现在可以计算每个资源的空闲主机容量:

以下是如何计算最坏情况下的空闲主机容量:

在这一点上,鉴于空闲主机容量为 1.0,我们应该缩减集群,因为容量目前不大于1。这可能看起来有些反直觉,因为您确实有一个空闲主机,但如果此时删除一个实例,将导致集群的可用 CPU 容量为 0,并且集群将扩展,因为没有空闲的 CPU 容量。

实施 ECS 自动扩展解决方案

现在您已经很好地了解了如何计算 ECS 集群容量,以便进行扩展和缩减决策,我们准备实施一个自动扩展解决方案,如下图所示:

以下提供了在前面的图表中显示的解决方案的步骤:

  1. 在计算 ECS 集群容量之前,您需要一个机制来触发容量的计算,最好是在 ECS 容器实例的容量发生变化时触发。这可以通过利用 CloudWatch Events 服务来实现,该服务为包括 ECS 在内的各种 AWS 服务发布事件,并允许您创建事件规则,订阅特定事件并使用各种机制(包括 Lambda 函数)处理它们。CloudWatch 事件支持接收有关 ECS 容器实例状态更改的信息,这代表了触发集群容量计算的理想机制,因为 ECS 容器实例的可用资源的任何更改都将触发状态更改事件。

  2. 一个负责计算 ECS 集群容量的 Lambda 函数会在每个 ECS 容器实例状态变化事件触发时被触发。

  3. Lambda 函数不会决定自动扩展集群,而是简单地以 CloudWatch 自定义指标的形式发布当前容量,报告当前空闲容器容量和空闲主机容量。

  4. CloudWatch 服务配置了警报,当空闲容器容量或空闲主机容量低于或超过扩展或收缩集群的阈值时,会触发 EC2 自动扩展操作。

  5. EC2 自动扩展服务配置了 EC2 自动扩展策略,这些策略会在 CloudWatch 引发的警报时被调用。

  6. 除了配置用于管理 ECS 集群容量的 CloudWatch 警报外,您还可以为每个 ECS 服务配置适当的 CloudWatch 警报,然后触发 AWS 应用自动扩展服务,以扩展或收缩运行您的 ECS 服务的 ECS 任务数量。例如,在前面的图表中,ECS 服务配置了一个应用自动扩展策略,当 ECS 服务的 CPU 利用率超过 50%时,会增加 ECS 任务的数量。

现在让我们实现解决方案的各个组件。

为 ECS 配置 CloudWatch 事件

我们需要执行的第一个任务是设置一个 CloudWatch 事件规则,订阅 ECS 容器实例状态变化事件,并配置一个 Lambda 函数作为目标,用于计算 ECS 集群容量。

以下示例演示了如何向 todobackend-aws stack.yml CloudFormation 模板添加 CloudWatch 事件规则:

...
...
Resources:
  EcsCapacityPermission:
 Type: AWS::Lambda::Permission
 Properties:
 Action: lambda:InvokeFunction
 FunctionName: !Ref EcsCapacityFunction
 Principal: events.amazonaws.com
 SourceArn: !Sub ${EcsCapacityEvents.Arn}
 EcsCapacityEvents:
 Type: AWS::Events::Rule
 Properties:
 Description: !Sub ${AWS::StackName} ECS Events Rule
 EventPattern:
 source:
 - aws.ecs
 detail-type:
 - ECS Container Instance State Change
 detail:
 clusterArn:
 - !Sub ${ApplicationCluster.Arn}
 Targets:
 - Arn: !Sub ${EcsCapacityFunction.Arn}
 Id: !Sub ${AWS::StackName}-ecs-events
  LifecycleHook:
    Type: AWS::AutoScaling::LifecycleHook
...
...

EcsCapacityEvents 资源定义了事件规则,并包括两个关键属性:

  • EventPattern:定义了与此规则匹配事件的模式。所有 CloudWatch 事件都包括 sourcedetail-typedetail 属性,事件模式确保只有与 ECS 事件相关的 ECS 事件(由 source 模式 aws.ecs 定义)与 ECS 容器实例状态更改(由 detail-type 模式定义)与 ApplicationCluster 资源(由 detail 模式定义)相关的事件将被匹配到规则。

  • Targets:定义了事件应该路由到的目标资源。在前面的例子中,你引用了一个名为 EcsCapacityFunction 的 Lambda 函数的 ARN,你很快将定义它。

EcsCapacityPermission 资源确保 CloudWatch 事件服务有权限调用 EcsCapacityFunction Lambda 函数。这是任何调用 Lambda 函数的服务的常见方法,你可以添加一个 Lambda 权限,授予给定 AWS 服务(由 Principal 属性定义)对于给定资源(由 SourceArn 属性定义)调用 Lambda 函数(FunctionName 属性)的能力。

现在,让我们添加引用的 Lambda 函数,以及一个 IAM 角色和 CloudWatch 日志组:

...
...
Resources:
  EcsCapacityRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Action:
 - sts:AssumeRole
 Effect: Allow
 Principal:
 Service: lambda.amazonaws.com
 Policies:
 - PolicyName: EcsCapacityPermissions
 PolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Sid: ManageLambdaLogs
 Effect: Allow
 Action:
 - logs:CreateLogStream
 - logs:PutLogEvents
 Resource: !Sub ${EcsCapacityLogGroup.Arn}
 EcsCapacityFunction:
 Type: AWS::Lambda::Function
 DependsOn:
 - EcsCapacityLogGroup
 Properties:
 Role: !Sub ${EcsCapacityRole.Arn}
 FunctionName: !Sub ${AWS::StackName}-ecsCapacity
 Description: !Sub ${AWS::StackName} ECS Capacity Manager
 Code:
 ZipFile: |
 import json
 def handler(event, context):
 print("Received event %s" % json.dumps(event))
 Runtime: python3.6
 MemorySize: 128
 Timeout: 300
 Handler: index.handler
  EcsCapacityLogGroup:
 Type: AWS::Logs::LogGroup
 DeletionPolicy: Delete
 Properties:
 LogGroupName: !Sub /aws/lambda/${AWS::StackName}-ecsCapacity
 RetentionInDays: 7
  EcsCapacityPermission:
    Type: AWS::Lambda::Permission
...
...

到目前为止,你应该已经对如何使用 CloudFormation 定义 Lambda 函数有了很好的理解,所以我不会深入描述前面的例子。但是请注意,目前我已经实现了一个基本的函数,它只是简单地打印出接收到的任何事件——我们将使用这个函数来初步了解 ECS 容器实例状态更改事件的结构。

此时,你现在可以使用 aws cloudformation deploy 命令部署你的更改:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file stack.yml \
 --stack-name todobackend --parameter-overrides $(cat dev.cfg) \
 --capabilities CAPABILITY_NAMED_IAM
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - todobackend

部署完成后,你可以通过停止运行在 ECS 集群上的现有 ECS 任务来触发 ECS 容器实例状态更改:

> aws ecs list-tasks --cluster todobackend-cluster
{
    "taskArns": [
        "arn:aws:ecs:us-east-1:385605022855:task/5754a076-6f5c-47f1-8e73-c7b229315e31"
    ]
}
> aws ecs stop-task --cluster todobackend-cluster --task 5754a076-6f5c-47f1-8e73-c7b229315e31
{
    "task": {
        ...
        ...
        "lastStatus": "RUNNING",
        "desiredStatus": "STOPPED",
        ...
        ...
    }
}

由于这个 ECS 任务与 ECS 服务相关联,ECS 将自动启动一个新的 ECS 任务,如果你前往 CloudWatch 控制台,选择日志,然后打开用于处理 ECS 容器实例状态更改事件的 Lambda 函数的日志组的最新日志流(/aws/lambda/todobackend-ecsCapacity),你应该会看到一些事件已被记录:

在前面的屏幕截图中,您可以看到在几秒钟内记录了两个事件,这些事件代表您停止 ECS 任务,然后 ECS 自动启动新的 ECS 任务,以确保链接的 ECS 服务达到其配置的期望计数。

您可以看到sourcedetail-type属性与您之前配置的事件模式匹配,如果您在第二个事件中继续向下滚动,您应该会找到一个名为registeredResourcesremainingResources的属性,如下例所示:

{
  ...
  ...
  "clusterArn":  "arn:aws:ecs:us-east-1:385605022855:cluster/todobackend-cluster",      
  "containerInstanceArn":  "arn:aws:ecs:us-east-1:385605022855:container-instance/d27868d6-79fd-4858-bec6-65720855e0b3",
 "ec2InstanceId":  "i-0d9bd79d19a843216",
  "registeredResources": [             
    { "name":  "CPU", "type":  "INTEGER", "integerValue":  1024 },
    {       "name":  "MEMORY",                 
       "type":  "INTEGER",                 
       "integerValue":  993 },
    { "name":  "PORTS",                 
       "type":  "STRINGSET",                 
       "stringSetValue": ["22","2376","2375","51678","51679"]
    }
  ],
  "remainingResources": [ 
    { 
      "name": "CPU", 
      "type": "INTEGER", 
      "integerValue": 774 
    },
    { 
       "name": "MEMORY", 
       "type": "INTEGER", 
       "integerValue": 593 
    },
    {
       "name": "PORTS", 
       "type": "STRINGSET", 
       "stringSetValue": ["22","2376","2375","51678","51679"]
    }
  ],
  ...
  ...
}

registeredResources属性定义了分配给实例的总资源,而remainingResources指示每个资源的当前剩余数量。因为在前面的示例中,当 ECS 为 todobackend 服务启动新的 ECS 任务时会引发事件,因此从registeredResources中扣除了分配给此任务的总 250 个 CPU 单位和 400 MB 内存,然后反映在remainingResources属性中。还要注意在示例 12-6 的输出顶部,事件包括其他有用的信息,例如 ECS 集群 ARN 和 ECS 容器实例 ARN 值(由clusterArncontainerInstanceArn属性指定)。

编写计算集群容量的 Lambda 函数

现在,您已经设置了一个 CloudWatch 事件和 Lambda 函数,每当检测到 ECS 容器实例状态变化时就会被调用,您现在可以在 Lambda 函数中实现所需的应用程序代码,以执行适当的 ECS 集群容量计算。

...
...
Resources:
  ...
  ...
  EcsCapacityFunction:
    Type: AWS::Lambda::Function
    DependsOn:
      - EcsCapacityLogGroup
    Properties:
      Role: !Sub ${EcsCapacityRole.Arn}
      FunctionName: !Sub ${AWS::StackName}-ecsCapacity
      Description: !Sub ${AWS::StackName} ECS Capacity Manager
      Code:
 ZipFile: |
 import json
          import boto3
          ecs = boto3.client('ecs')
          # Max memory and CPU - you would typically inject these as environment variables
          CONTAINER_MAX_MEMORY = 400
          CONTAINER_MAX_CPU = 250

          # Get current CPU
          def check_cpu(instance):
            return sum(
              resource['integerValue']
              for resource in instance['remainingResources']
              if resource['name'] == 'CPU'
            )
          # Get current memory
          def check_memory(instance):
            return sum(
              resource['integerValue']
              for resource in instance['remainingResources']
              if resource['name'] == 'MEMORY'
            )
          # Lambda entrypoint
          def handler(event, context):
            print("Received event %s" % json.dumps(event))

            # STEP 1 - COLLECT RESOURCE DATA
            cluster = event['detail']['clusterArn']
            # The maximum CPU availble for an idle ECS instance
            instance_max_cpu = next(
              resource['integerValue']
              for resource in event['detail']['registeredResources']
              if resource['name'] == 'CPU')
            # The maximum memory availble for an idle ECS instance
            instance_max_memory = next(
              resource['integerValue']
              for resource in event['detail']['registeredResources']
              if resource['name'] == 'MEMORY')
            # Get current container capacity based upon CPU and memory
            instance_arns = ecs.list_container_instances(
              cluster=cluster
            )['containerInstanceArns']
            instances = [
              instance for instance in ecs.describe_container_instances(
                cluster=cluster,
                containerInstances=instance_arns
              )['containerInstances']
              if instance['status'] == 'ACTIVE'
            ]
            cpu_capacity = 0
            memory_capacity = 0
            for instance in instances:
              cpu_capacity += int(check_cpu(instance)/CONTAINER_MAX_CPU)
              memory_capacity += int(check_memory(instance)/CONTAINER_MAX_MEMORY)
            print("Current container cpu capacity of %s" % cpu_capacity)
            print("Current container memory capacity of %s" % memory_capacity)

            # STEP 2 - CALCULATE OVERALL CONTAINER CAPACITY
            container_capacity = min(cpu_capacity, memory_capacity)
            print("Overall container capacity of %s" % container_capacity)

            # STEP 3 - CALCULATE IDLE HOST COUNT
            idle_hosts = min(
              cpu_capacity / int(instance_max_cpu / CONTAINER_MAX_CPU),
              memory_capacity / int(instance_max_memory / CONTAINER_MAX_MEMORY)
            )
            print("Overall idle host capacity of %s" % idle_hosts)
      Runtime: python3.6
      MemorySize: 128
      Timeout: 300
      Handler: index.handler
...
...

在前面的示例中,您首先定义了 ECS 任务的最大 CPU 和最大内存,这是进行各种集群容量计算所必需的,我们使用当前配置的 CPU 和内存设置来支持 todobackend 服务,因为这是我们集群上唯一支持的应用程序。在handler函数中,第一步是使用接收到的 CloudWatch 事件收集当前的资源容量数据。该事件包括有关 ECS 容器实例在registeredResources属性中的最大容量的详细信息,还包括实例所属的 ECS 集群。该函数首先列出集群中的所有实例,然后使用 ECS 客户端上的describe_container_instances调用加载每个实例的详细信息。

对每个实例收集的信息仅限于活动实例,因为您不希望包括可能处于 DRAINING 状态或其他非活动状态的实例的资源。

前面示例中的代码只能在 Python 3.x 环境中正确运行,因此请确保您的 Lambda 函数配置为使用 Python 3.6。

收集有关每个 ECS 容器实例的必要信息后,然后迭代每个实例并计算 CPU 和内存容量。这调用了查询每个实例的remainingResources属性的辅助函数,该函数返回每个资源的当前可用容量。每个计算都以您之前定义的最大容器大小来表达,并将它们相加以提供整个集群的 CPU 和内存容量,以供信息目的打印。

下一步是计算整体容器容量,这可以通过取先前计算的资源容量的最小值来轻松计算,这将用于确定您的 ECS 集群何时需要扩展,至少当容器容量低于零时。最后,进行空闲主机容量计算 - 此值将用于确定您的 ECS 集群何时应该缩减,只有当空闲主机容量大于 1.0 时才会发生,如前所述。

为计算集群容量添加 IAM 权限

关于前面示例中的代码需要注意的一点是,它需要能够调用 ECS 服务并执行ListContainerInstancesDescribeContainerInstances API 调用的能力。这意味着您需要向 Lambda 函数 IAM 角色添加适当的 IAM 权限,如下例所示:

...
...
Resources:
  ...
  ...
  EcsCapacityRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: EcsCapacityPermissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: ListContainerInstances
 Effect: Allow
 Action:
 - ecs:ListContainerInstances
 Resource: !Sub ${ApplicationCluster.Arn}
 - Sid: DescribeContainerInstances
 Effect: Allow
 Action:
 - ecs:DescribeContainerInstances
 Resource: "*"
 Condition:
 ArnEquals:
 ecs:cluster: !Sub ${ApplicationCluster.Arn}
              - Sid: ManageLambdaLogs
                Effect: Allow
                Action:
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: !Sub ${EcsCapacityLogGroup.Arn}
  ...
  ...

测试集群容量计算

您已经添加了计算集群容量所需的代码,并确保您的 Lambda 函数有适当的权限来查询 ECS 以确定集群中所有 ECS 容器实例的当前容量。您现在可以使用aws cloudformation deploy命令部署您的更改,一旦部署完成,您可以通过停止运行在 todobackend ECS 集群中的任何 ECS 任务来再次测试您的 Lambda 函数。

如果您查看 Lambda 函数的 CloudWatch 日志,您应该会看到类似于这里显示的事件:

请注意,当您停止 ECS 任务(如停止任务事件所表示的),Lambda 函数报告 CPU 容量为 4,内存容量为 2,总体容量为 2,这是计算出的每个资源容量的最小值。

如果您对此进行合理检查,您应该会发现计算是准确和正确的。对于初始事件,因为您停止了 ECS 任务,没有任务在运行,因此可用的 CPU 和内存资源分别为 1,024 个单位和 993 MB(即 t2.micro 实例的容量)。这相当于以下容器容量:

  • CPU 容量 = 1024 / 250 = 4

  • 内存容量 = 993 / 400 = 2

当 ECS 自动替换停止的 ECS 任务时,您会看到集群容量下降,因为新的 ECS 任务(具有 250 个 CPU 单位和 400 MB 内存)现在正在消耗资源:

  • CPU 容量 = 1024 - 250 / 250 = 774 / 250 = 3

  • 内存容量 = 993 - 400 / 400 = 593 / 400 = 1

最后,您可以看到,当您停止 ECS 任务时,总体空闲主机容量正确计算为 1.0,这是正确的,因为此时集群上没有运行任何 ECS 任务。当 ECS 替换停止的任务时,总体空闲主机容量减少为 0.5,因为 ECS 容器实例现在运行的是最多可以在单个实例上运行的两个 ECS 任务中的一个,就内存资源而言。

发布自定义 CloudWatch 指标

此时,我们正在计算确定何时需要扩展或缩小集群的适当指标,并且函数中需要执行的最终任务是发布自定义 CloudWatch 事件指标,我们可以使用这些指标来触发自动扩展策略:

...
...
Resources:
  ...
  ...
  EcsCapacityFunction:
    Type: AWS::Lambda::Function
    DependsOn:
      - EcsCapacityLogGroup
    Properties:
      Role: !Sub ${EcsCapacityRole.Arn}
      FunctionName: !Sub ${AWS::StackName}-ecsCapacity
      Description: !Sub ${AWS::StackName} ECS Capacity Manager
      Code:
        ZipFile: |
          import json
          import boto3
          import datetime
          ecs = boto3.client('ecs') cloudwatch = boto3.client('cloudwatch') # Max memory and CPU - you would typically inject these as environment variables
          CONTAINER_MAX_MEMORY = 400
          CONTAINER_MAX_CPU = 250          ...
          ...
          # Lambda entrypoint
          def handler(event, context):
            print("Received event %s" % json.dumps(event))            ...
            ...# STEP 3 - CALCULATE IDLE HOST COUNT            idle_hosts = min(
              cpu_capacity / int(instance_max_cpu / CONTAINER_MAX_CPU),
              memory_capacity / int(instance_max_memory / CONTAINER_MAX_MEMORY)
            )
            print("Overall idle host capacity of %s" % idle_hosts)

 # STEP 4 - PUBLISH CLOUDWATCH METRICS
 cloudwatch.put_metric_data(
 Namespace='AWS/ECS',
 MetricData=[
              {
                'MetricName': 'ContainerCapacity',
                'Dimensions': [{
                  'Name': 'ClusterName',
                  'Value': cluster.split('/')[-1]
                }],
                'Timestamp': datetime.datetime.utcnow(),
                'Value': container_capacity
              }, 
              {
 'MetricName': 'IdleHostCapacity',
 'Dimensions': [{
 'Name': 'ClusterName',
 'Value': cluster.split('/')[-1]
 }],
 'Timestamp': datetime.datetime.utcnow(),
 'Value': idle_hosts
 }
            ])
      Runtime: python3.6
      MemorySize: 128
      Timeout: 300
      Handler: index.handler
...
...

在前面的示例中,您使用 CloudWatch 客户端的put_metric_data函数来发布 AWS/ECS 命名空间中的ContainerCapacityIdleHostCapacity自定义指标。这些指标基于 ECS 集群进行维度化,由 ClusterName 维度名称指定,并且仅限于 todobackend ECS 集群。

确保 Lambda 函数正确运行的最后一个配置任务是授予函数权限以发布 CloudWatch 指标。这可以通过在先前示例中创建的EcsCapacityRole中添加适当的 IAM 权限来实现:

...
...
Resources:
  ...
  ...
  EcsCapacityRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: EcsCapacityPermissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: PublishCloudwatchMetrics
 Effect: Allow
 Action:
 - cloudwatch:putMetricData
 Resource: "*"
              - Sid: ListContainerInstances
                Effect: Allow
                Action:
                  - ecs:ListContainerInstances
                Resource: !Sub ${ApplicationCluster.Arn}
              - Sid: DescribeContainerInstances
                Effect: Allow
                Action:
                  - ecs:DescribeContainerInstances
                Resource: "*"
                Condition:
                  ArnEquals:
                    ecs:cluster: !Sub ${ApplicationCluster.Arn}
              - Sid: ManageLambdaLogs
                Effect: Allow
                Action:
                - logs:CreateLogStream
                - logs:PutLogEvents
                Resource: !Sub ${EcsCapacityLogGroup.Arn}
  ...
  ...

如果您现在使用aws cloudformation deploy命令部署更改,然后停止运行的 ECS 任务,在切换到 CloudWatch 控制台后,您应该能够看到与您的 ECS 集群相关的新指标被发布。如果您从左侧菜单中选择指标,然后在所有指标下选择ECS > ClusterName,您应该能够看到您的自定义指标(ContainerCapacityIdleHostCapacity)。以下截图显示了这些指标基于一分钟内收集的最大值进行绘制。在图表的 12:49 处,您可以看到当您停止 ECS 任务时,ContainerCapacityIdleHostCapacity指标都增加了,然后一旦 ECS 启动了新的 ECS 任务,这两个指标的值都减少了,因为新的 ECS 任务从您的集群中分配了资源:

为集群容量管理创建 CloudWatch 警报。

现在,您可以在 ECS 集群中计算和发布 ECS 集群容量指标,每当 ECS 集群中的 ECS 容器实例状态发生变化时。整体解决方案的下一步是实施 CloudWatch 警报,这将在指标超过或低于与集群容量相关的指定阈值时触发自动扩展操作。

以下代码演示了向 todobackend 堆栈添加两个 CloudWatch 警报:

...
...
Resources:
  ...
  ...
 ContainerCapacityAlarm:
 Type: AWS::CloudWatch::Alarm
 Properties:
 AlarmDescription: ECS Cluster Container Free Capacity
 AlarmActions:
        - !Ref ApplicationAutoscalingScaleOutPolicy
 Namespace: AWS/ECS
 Dimensions:
 - Name: ClusterName
 Value: !Ref ApplicationCluster
 MetricName: ContainerCapacity
 Statistic: Minimum
 Period: 60
 EvaluationPeriods: 1
 Threshold: 1
 ComparisonOperator: LessThanThreshold
 TreatMissingData: ignore
 IdleHostCapacityAlarm:
 Type: AWS::CloudWatch::Alarm
 Properties:
 AlarmDescription: ECS Cluster Container Free Capacity
 AlarmActions:
        - !Ref ApplicationAutoscalingScaleInPolicy
 Namespace: AWS/ECS
 Dimensions:
 - Name: ClusterName
 Value: !Ref ApplicationCluster
 MetricName: IdleHostCapacity
 Statistic: Maximum
 Period: 60
 EvaluationPeriods: 1
 Threshold: 1
 ComparisonOperator: GreaterThanThreshold
 TreatMissingData: ignore
  ...
  ...

在前面的示例中,您添加了两个 CloudWatch 警报-一个ContainerCapacityAlarm,每当容器容量低于 1 时将用于触发扩展操作,以及一个IdleHostCapacityAlarm,每当空闲主机容量大于 1 时将用于触发缩减操作。每个警报的各种属性在此处有进一步的描述:

  • AlarmActions:定义应该采取的操作,如果警报违反其配置的条件。在这里,我们引用了我们即将定义的 EC2 自动扩展策略资源,这些资源在引发警报时会触发适当的自动扩展扩展或缩减操作。

  • Namespace:定义警报所关联的指标的命名空间。

  • Dimensions:定义指标与给定命名空间内的资源的关系的上下文。在前面的示例中,上下文配置为我们堆栈内的 ECS 集群。

  • MetricName:定义指标的名称。在这里,我们指定了在上一节中发布的每个自定义指标的名称。

  • 统计:定义应该评估的指标的统计数据。这实际上是一个非常重要的参数,在容器容量警报的情况下,设置最大值确保短暂指标不会不必要地触发警报,假设在每个评估周期内至少有 1 个值超过配置的阈值。对于空闲主机容量警报也是如此,但方向相反。

  • PeriodEvaluationPeriodsThresholdComparisonOperator:这些定义了指标必须在配置的阈值和比较运算符的范围之外的时间范围。如果超出了这些范围,将会触发警报。

  • TreatMissingData:此设置定义了如何处理缺少的指标数据。在我们的用例中,由于我们仅在 ECS 容器实例状态更改时发布指标数据,因此将值设置为ignore可以确保我们不会将缺失的数据视为有问题的指示。

创建 EC2 自动扩展策略

现在,您需要创建您在每个 CloudWatch 警报资源中引用的 EC2 自动扩展策略资源。

以下示例演示了向 todobackend 堆栈添加扩展和缩减策略:

...
...
Resources:
  ...
  ...
 ApplicationAutoscalingScaleOutPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      PolicyType: SimpleScaling
      AdjustmentType: ChangeInCapacity
      ScalingAdjustment: 1
      AutoScalingGroupName: !Ref ApplicationAutoscaling
      Cooldown: 600
  ApplicationAutoscalingScaleInPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      PolicyType: SimpleScaling
      AdjustmentType: ChangeInCapacity
      ScalingAdjustment: -1
      AutoScalingGroupName: !Ref ApplicationAutoscaling
      Cooldown: 600
  ...
  ...
  ApplicationAutoscaling:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn:
      - DmesgLogGroup
      - MessagesLogGroup
      - DockerLogGroup
      - EcsInitLogGroup
      - EcsAgentLogGroup
    CreationPolicy:
      ResourceSignal:
 Count: 1
        Timeout: PT15M
    UpdatePolicy:
      AutoScalingRollingUpdate:
        SuspendProcesses:
 - HealthCheck
 - ReplaceUnhealthy
 - AZRebalance
 - AlarmNotification
 - ScheduledActions        MinInstancesInService: 1
        MinSuccessfulInstancesPercent: 100
        WaitOnResourceSignals: "true"
        PauseTime: PT15M
    Properties:
      LaunchConfigurationName: !Ref ApplicationAutoscalingLaunchConfiguration
      MinSize: 0
      MaxSize: 4
 DesiredCapacity: 1        ...
        ...

在上面的示例中,您定义了两种SimpleScaling类型的自动扩展策略,它代表了您可以实现的最简单的自动扩展形式。各种自动扩展类型的讨论超出了本书的范围,但如果您对了解更多可用选项感兴趣,可以参考docs.aws.amazon.com/autoscaling/ec2/userguide/as-scale-based-on-demand.htmlAdjustmentTypeScalingAdjustment属性配置为增加或减少自动扩展组的一个实例的大小,而Cooldown属性提供了一种机制,以确保在指定的持续时间内禁用进一步的自动扩展操作,这可以帮助避免集群频繁地扩展和缩减。

请注意,ApplicationAutoscalingUpdatePolicy设置已更新以包括SuspendProcesses参数,该参数配置 CloudFormation 在进行自动扩展滚动更新时禁用某些操作过程。这特别是在滚动更新期间禁用自动扩展操作很重要,因为您不希望自动扩展操作干扰由 CloudFormation 编排的滚动更新。最后,我们还将ApplicationAutoscaling资源上的各种计数设置为固定值 1,因为自动扩展现在将管理我们的 ECS 集群的大小。

测试 ECS 集群容量管理

现在,我们已经拥有了计算 ECS 集群容量、发布指标和触发警报的所有组件,这将调用自动扩展操作,让我们部署我们的更改并测试解决方案是否按预期工作。

测试扩展

人为触发扩展操作,我们需要在dev.cfg配置文件中将ApplicationDesiredCount输入参数设置为 2,这将增加我们的 ECS 服务的 ECS 任务计数为 2,并导致 ECS 集群中的单个 ECS 容器实例不再具有足够的资源来支持任何进一步的附加容器:

ApplicationDesiredCount=2
ApplicationImageId=ami-ec957491
ApplicationImageTag=5fdbe62
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

此配置更改应导致ContainerCapacity指标下降到配置的警报阈值1以下,我们可以通过运行aws cloudformation deploy命令将更改部署到 CloudFormation 来进行测试。

部署完成后,如果您浏览到 CloudWatch 控制台并从左侧菜单中选择警报,您应该会看到您的容器容量警报进入警报状态(可能需要几分钟),如前所示:

您可以在操作详细信息中看到 CloudWatch 警报已触发应用程序自动扩展的扩展策略,并且在左侧的图表中注意到,这是因为容器容量由于单个 ECS 容器实例上运行的 ECS 任务增加而下降到 0。

如果您现在导航到 EC2 控制台,从左侧菜单中选择自动扩展组,然后选择 todobackend 自动扩展组的活动历史选项卡,您会看到自动扩展组中当前实例计数为2,并且由于容器容量警报转换为警报状态而启动了一个新的 EC2 实例:

一旦新的 ECS 容器实例被添加到 ECS 集群中,新的容量计算将会发生,如果您切换回 CloudWatch 控制台,您应该看到 ContainerCapacity 警报最终转换为 OK 状态,如下面的截图所示:

在右下角的图表中,您可以看到添加一个新的 ECS 容器实例的效果,这将把容器容量从0增加到2,将容器容量警报置为 OK 状态。

测试缩减规模

现在您已经成功测试了 ECS 集群容量管理解决方案的扩展行为,让我们现在通过在dev.cfg文件中将ApplicationDesiredCount减少到 1,并运行aws cloudformation deploy命令来部署修改后的计数,人为地触发缩减行为:

ApplicationDesiredCount=1
ApplicationImageId=ami-ec957491
ApplicationImageTag=5fdbe62
ApplicationSubnets=subnet-a5d3ecee,subnet-324e246f
VpcId=vpc-f8233a80

一旦这个改变被部署,您应该在 CloudWatch 控制台上看到空闲主机容量警报在几分钟后变为 ALARM 状态:

在前面的截图中,空闲主机容量从 1.0 增加到 1.5,因为现在我们只有一个正在运行的 ECS 任务和两个 ECS 容器实例在集群中。这触发了配置的应用程序自动缩放缩减策略,它将减少 ECS 集群容量到一个 ECS 容器实例,并最终空闲主机容量警报将转换为 OK 状态。

配置 AWS 应用自动扩展服务

我们现在已经有了一个 ECS 集群容量管理解决方案,它将自动扩展和缩减您的 ECS 集群,当新的 ECS 任务在您的 ECS 集群中出现和消失时。到目前为止,我们通过手动增加 todobackend ECS 服务的任务数量来人为测试这一点,然而在您的真实应用中,您通常会使用 AWS 应用自动扩展服务,根据应用程序最合适的指标动态地扩展和缩减您的 ECS 服务。

ECS 集群容量的另一个影响因素是部署新应用程序,以 ECS 任务定义更改的形式应用到 ECS 服务。ECS 的滚动更新机制通常会暂时增加 ECS 任务数量,这可能会导致 ECS 集群在短时间内扩展,然后再缩小。您可以通过调整容器容量在降低到配置的最小阈值之前可以持续的时间来调整此行为,并且还可以增加必须始终可用的最小容器容量阈值。这种方法可以在集群中建立更多的备用容量,从而使您能够对容量变化做出较少激进的响应,并吸收滚动部署引起的瞬时容量波动。

AWS 应用自动扩展比 EC2 自动扩展更复杂,至少需要几个组件:

  • CloudWatch 警报:这定义了您感兴趣的指标,并在应该扩展或缩小时触发。

  • 自动扩展目标:这定义了应用程序自动扩展将应用于的目标组件。对于我们的场景,这将被配置为 todobackend ECS 服务。

  • 自动扩展 IAM 角色:您必须创建一个 IAM 角色,授予 AWS 应用自动扩展服务权限来管理您的 CloudWatch 警报,读取您的应用自动扩展策略,并修改您的 ECS 服务以增加或减少 ECS 服务任务数量。

  • 扩展和缩小策略:这些定义了与扩展 ECS 服务和缩小 ECS 服务相关的行为。

配置 CloudWatch 警报

让我们首先通过在stack.yml模板中添加一个 CloudWatch 警报来触发应用程序自动扩展:

...
...
Resources:
  ApplicationServiceLowCpuAlarm:
 Type: AWS::CloudWatch::Alarm
 Properties:
 AlarmActions:
 - !Ref ApplicationServiceAutoscalingScaleInPolicy
 AlarmDescription: Todobackend Service Low CPU 
 Namespace: AWS/ECS
 Dimensions:
 - Name: ClusterName
 Value: !Ref ApplicationCluster
 - Name: ServiceName
 Value: !Sub ${ApplicationService.Name}
 MetricName: CPUUtilization
 Statistic: Average
 Period: 60
 EvaluationPeriods: 3
 Threshold: 20
 ComparisonOperator: LessThanThreshold
 ApplicationServiceHighCpuAlarm:
 Type: AWS::CloudWatch::Alarm
 Properties:
 AlarmActions:
 - !Ref ApplicationServiceAutoscalingScaleOutPolicy
 AlarmDescription: Todobackend Service High CPU 
 Namespace: AWS/ECS
 Dimensions:
 - Name: ClusterName
 Value: !Ref ApplicationCluster
 - Name: ServiceName
 Value: !Sub ${ApplicationService.Name}
 MetricName: CPUUtilization
 Statistic: Average
 Period: 60
 EvaluationPeriods: 3
 Threshold: 40
 ComparisonOperator: GreaterThanThreshold
  ...
  ...

在前面的示例中,为低 CPU 和高 CPU 条件创建了警报,并将其维度设置为运行在 todobackend ECS 集群上的 todobackend ECS 服务。当 ECS 服务的平均 CPU 利用率在 3 分钟(3 x 60 秒)的时间内大于 40%时,将触发高 CPU 警报,当平均 CPU 利用率在 3 分钟内低于 20%时,将触发低 CPU 警报。在每种情况下,都配置了警报操作,引用了我们即将创建的扩展和缩小策略资源。

定义自动扩展目标

AWS 应用自动缩放要求您定义自动缩放目标,这是您需要扩展或缩小的资源。对于 ECS 的用例,这被定义为 ECS 服务,如前面的示例所示:

...
...
Resources:
 ApplicationServiceAutoscalingTarget:
 Type: AWS::ApplicationAutoScaling::ScalableTarget
 Properties:
 ServiceNamespace: ecs
 ResourceId: !Sub service/${ApplicationCluster}/${ApplicationService.Name}
 ScalableDimension: ecs:service:DesiredCount
 MinCapacity: 1
 MaxCapacity: 4
 RoleARN: !Sub ${ApplicationServiceAutoscalingRole.Arn}
  ...
  ...

在前面的示例中,您为自动缩放目标定义了以下属性:

  • ServiceNamespace:定义目标 AWS 服务的命名空间。当针对 ECS 服务时,将其设置为 ecs

  • ResourceId:与目标关联的资源的标识符。对于 ECS,这是以 service/<ecs-cluster-name>/<ecs-service-name> 格式定义的。

  • ScalableDimension:指定可以扩展的目标资源类型的属性。在 ECS 服务的情况下,这是 DesiredCount 属性,其定义为 ecs:service:DesiredCount

  • MinCapacityMaxCapacity:期望的 ECS 服务计数可以扩展的最小和最大边界。

  • RoleARN:应用自动缩放服务将用于扩展和缩小目标的 IAM 角色的 ARN。在前面的示例中,您引用了下一节中将创建的 IAM 资源。

有关上述每个属性的更多详细信息,您可以参考 应用自动缩放 API 参考

创建自动缩放 IAM 角色

在应用自动缩放目标的资源定义中,您引用了应用自动缩放服务将扮演的 IAM 角色。以下示例定义了此 IAM 角色以及应用自动缩放服务所需的权限:

...
...
Resources:
  ApplicationServiceAutoscalingRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Action:
 - sts:AssumeRole
 Effect: Allow
 Principal:
 Service: application-autoscaling.amazonaws.com
 Policies:
 - PolicyName: AutoscalingPermissions
 PolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Action:
 - application-autoscaling:DescribeScalableTargets
 - application-autoscaling:DescribeScalingActivities
 - application-autoscaling:DescribeScalingPolicies
 - cloudwatch:DescribeAlarms
 - cloudwatch:PutMetricAlarm
 - ecs:DescribeServices
 - ecs:UpdateService
 Resource: "*"
  ApplicationServiceAutoscalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
  ...
  ...

您可以看到应用自动缩放服务需要与应用自动缩放服务本身关联的一些读取权限,以及管理 CloudWatch 警报的能力,并且必须能够更新 ECS 服务以管理 ECS 服务的期望计数。请注意,您必须在 AssumeRolePolicyDocument 部分中将主体指定为 application-autoscaling.amazonaws.com,这允许应用自动缩放服务扮演该角色。

配置扩展和缩小策略

配置应用自动缩放时的最后一个任务是添加扩展和缩小策略:

...
...
Resources:
  ApplicationServiceAutoscalingScaleInPolicy:
 Type: AWS::ApplicationAutoScaling::ScalingPolicy
 Properties:
 PolicyName: ScaleIn
 PolicyType: StepScaling
 ScalingTargetId: !Ref ApplicationServiceAutoscalingTarget
 StepScalingPolicyConfiguration:
 AdjustmentType: ChangeInCapacity
 Cooldown: 360
 MetricAggregationType: Average
 StepAdjustments:
 - ScalingAdjustment: -1
 MetricIntervalUpperBound: 0
 ApplicationServiceAutoscalingScaleOutPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
 Properties:
 PolicyName: ScaleOut
 PolicyType: StepScaling
 ScalingTargetId: !Ref ApplicationServiceAutoscalingTarget
 StepScalingPolicyConfiguration:
 AdjustmentType: ChangeInCapacity
 Cooldown: 360
 MetricAggregationType: Average
 StepAdjustments:
 - ScalingAdjustment: 1
 MetricIntervalLowerBound: 0
ApplicationServiceAutoscalingRole:
    Type: AWS::IAM::Role
  ...
  ...

在这里,您定义了扩展和缩小策略,确保资源名称与您之前引用的那些匹配,当您配置用于触发策略的 CloudWatch 警报时。PolicyType参数指定您正在配置 Step-Scaling 策略,它们的工作方式类似于您之前定义的 EC2 自动缩放策略,并允许您以增量步骤进行缩放。其余属性都相当容易理解,尽管StepAdjustments属性确实需要进一步描述。

ScalingAdjustment指示每次缩放时您将增加或减少 ECS 服务计数的数量,而MetricIntervalLowerBoundMetricIntervalUpperBound属性允许您在超出警报阈值时定义额外的边界,以便您的自动缩放操作应用。

在上面的示例中显示的配置是,每当 CPU 利用率超过或低于配置的 CloudWatch 警报阈值时,应用程序自动缩放将始终被调用。这是因为未配置的上限和下限默认为无穷大或负无穷大,因此在警报阈值和无穷大/负无穷大之间的任何指标值都将触发警报。为了进一步澄清指标间隔边界的上下文,如果您改为配置MetricIntervalLowerBound值为 10 和MetricIntervalUpperBound为 30,当超过 CloudWatch 警报阈值(当前配置为 40%的 CPU 利用率)时,自动缩放操作将仅在 50%利用率(阈值+MetricIntervalLowerBound或 40+10=50)和 70%利用率(阈值+MetricIntervalUpperBound或 40+30=70%)之间应用。

部署应用程序自动缩放

在这一点上,您现在已经准备部署您的 ECS 应用程序自动缩放解决方案。运行aws cloudformation deploy命令后,如果您浏览到 ECS 控制台,选择 todobackend 集群和 todobackend ECS 服务,在自动缩放选项卡上,您应该看到您的新应用程序自动缩放配置已经就位:

现在,每当您的 ECS 服务的 CPU 利用率超过 40%(在所有 ECS 任务中平均),您的 ECS 服务的期望计数将增加一个。只要 CPU 利用率超过 40%,这将持续下去,最多增加到 4 个任务,根据前面示例的配置,每个自动扩展操作之间将应用 360 秒的冷却期。

在 ECS 服务级别上,您无需担心底层 ECS 集群资源,因为您的 ECS 集群容量管理解决方案确保集群中始终有足够的空闲容量来容纳额外的 ECS 任务。这意味着您现在可以根据每个 ECS 服务的特定性能特征独立扩展每个 ECS 服务,并强调了了解每个应用程序的最佳 ECS 任务资源分配的重要性。

总结

在本章中,您创建了一个全面的自动扩展解决方案,可以让您根据应用程序负载和客户需求自动扩展您的 ECS 服务和应用程序,同时确保底层 ECS 集群有足够的资源来部署新的 ECS 任务。

首先,您了解了关键的 ECS 资源,包括 CPU、内存、网络端口和网络接口,以及 ECS 如何分配这些资源。在管理 ECS 集群容量时,这些资源决定了 ECS 容器实例是否能够运行特定的 ECS 任务,因此您必须了解每种资源的消耗情况至关重要。

接下来,您实现了一个 ECS 集群容量管理解决方案,该解决方案在 ECS 容器实例状态发生变化时计算 ECS 集群容量。ECS 通过 CloudWatch 事件发布这些状态更改,您创建了一个 CloudWatch 事件规则,触发一个 Lambda 函数来计算当前的集群容量。该函数计算了两个关键指标——容器容量,表示集群当前可以支持的额外容器或 ECS 任务的数量,以及空闲主机容量,定义了整个集群中当前有多少“虚拟”主机处于空闲状态。容器容量用于扩展您的 ECS 集群,在容器容量低于 1 时添加额外的 ECS 容器实例,这意味着集群不再具有足够的资源来部署额外的 ECS 任务。空闲主机容量用于缩小您的 ECS 集群,在空闲主机容量大于 1.0 时移除 ECS 容器实例,这意味着您可以安全地移除一个 ECS 容器实例,并仍然有能力部署新的 ECS 任务。

我们讨论的一个关键概念是始终要为所有资源的最坏情况共同进行这些计算的要求,这确保了当您拥有某种类型资源的充足空闲容量时,您永远不会进行缩小,但可能对另一种类型资源的容量较低。

最后,您学会了如何配置 AWS 应用程序自动扩展服务来扩展和缩小您的 ECS 服务。在这里,您根据应用程序特定的适当指标来扩展单个 ECS 服务,因为您是在单个 ECS 服务的上下文中进行扩展,所以在这个级别进行自动扩展是简单定义和理解的。扩展您的 ECS 服务最终是驱动您整体 ECS 集群容量变化的原因,而您实现的 ECS 集群容量管理解决方案负责处理这一点,使您能够自动扩展您的 ECS 服务,而无需担心对底层 ECS 集群的影响。

在下一章中,您将学习如何将您的 ECS 应用程序持续交付到 AWS,将我们在前几章中讨论过的所有功能都纳入其中。这将使您能够以完全自动化的方式部署最新的应用程序更改,减少运营开销,并为开发团队提供快速反馈。

问题

  1. 真/假:当您使用 ECS 并部署自己的 ECS 容器实例时,ECS 会自动为您扩展集群。

  2. 您使用哪个 AWS 服务来扩展您的 ECS 集群?

  3. 您使用哪个 AWS 服务来扩展您的 ECS 服务?

  4. 您的应用程序需要最少 300MB,最多 1GB 的内存才能运行。您会在 ECS 任务定义中配置哪些参数来支持这个配置?

  5. 您将 3 个不同的 ECS 任务部署到单个实例 ECS 集群中,每个任务运行不同的应用程序,并配置每个 ECS 任务保留 10 个 CPU 单位。在繁忙时期,其中一个 ECS 任务占用了 CPU,减慢了其他 ECS 任务的速度。假设 ECS 容器实例的容量为 1,000 个 CPU 单位,您可以采取什么措施来避免一个 ECS 任务占用 CPU?

  6. 真/假:如果您只为 ECS 任务使用动态端口映射,您就不需要担心网络端口资源。

  7. 您在 AWS 部署了一个支持总共四个网络接口的实例。假设所有 ECS 任务都使用 ECS 任务网络,那么实例的容量是多少?

  8. 在 EC2 自动缩放组中,何时应该禁用自动缩放?你会如何做?

  9. 您的 ECS 集群目前有 2 个 ECS 容器实例,每个实例有 500 个 CPU 单位和 500MB 的内存剩余容量。您只向集群部署了一种应用程序,目前有两个 ECS 任务正在运行。假设 ECS 任务需要 500 个 CPU 单位、500MB 的内存,并且静态端口映射到 TCP 端口 80,那么集群当前的整体剩余容量是多少个 ECS 任务?

  10. 您的 ECS 集群需要支持 3 个不同的 ECS 任务,分别需要 300MB、400MB 和 500MB 的内存。如果您的每个 ECS 容器实例都有 2GB 的内存,那么在进行 ECS 集群容量计算时,您会将每个 ECS 容器实例的最大容器数量计算为多少?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十三章:持续交付 ECS 应用程序

持续交付是创建一个可重复和可靠的软件发布过程的实践,以便您可以频繁且按需地将新功能部署到生产环境,成本和风险更低。采用持续交付有许多好处,如今越来越多的组织正在采用它,以更快地将功能推向市场,提高客户满意度,并降低软件交付成本。

实施持续交付需要在软件交付的端到端生命周期中实现高度自动化。到目前为止,在这门课程中,您已经使用了许多支持自动化和持续交付的技术。例如,Docker 本身带来了高度自动化,并促进了可重复和一致的构建过程,这些都是持续交付的关键组成部分。todobackend存储库中的 make 工作流进一步实现了这一点,自动化了 Docker 镜像的完整测试、构建和发布工作流程。在整个课程中,我们还广泛使用了 CloudFormation,它使我们能够以完全自动化的方式创建、更新和销毁完整的 AWS 环境,并且可以轻松地以可靠和一致的方式部署新功能(以新的 Docker 镜像形式)。持续交付将所有这些功能和能力整合在一起,创建了一个端到端的软件变更交付过程,从开发和提交源代码的时间到回归测试和部署到生产的时间。为了实现这种端到端的协调和自动化,我们需要采用专为此目的设计的新工具,AWS 提供了一系列服务来实现这一点,包括 AWS CodePipeline、CodeBuild 和 CloudFormation。

在本章中,您将学习如何实现一个端到端的持续交付流水线(使用 CodePipeline、CodeBuild 和 CloudFormation),该流水线将持续测试、构建和发布 Docker 镜像,然后持续将新构建的 Docker 镜像部署到非生产环境。该流水线还将支持对生产环境进行受控发布,自动创建必须经过审查和批准的变更集,然后才能将新变更部署到生产环境。

本章将涵盖以下主题:

  • 介绍 CodePipeline 和 CodeBuild

  • 创建自定义 CodeBuild 容器

  • 为您的应用程序存储库添加 CodeBuild 支持

  • 使用 CodePipeline 创建持续集成流水线

  • 使用 CodePipeline 创建持续部署流水线

  • 持续将您的应用程序交付到生产环境

技术要求

以下列出了完成本章所需的技术要求:

  • AWS 账户的管理员访问权限。

  • 本地 AWS 配置文件,根据第三章的说明进行配置。

  • AWS CLI 版本 1.15.71 或更高

  • 本章继续自第十二章,因此需要您成功完成第十二章中定义的所有配置任务。

  • 本章要求您将todobackendtodobackend-aws存储库发布到您具有管理访问权限的 GitHub 账户。

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch13

查看以下视频以查看代码的实际操作:

bit.ly/2BVGMYI

介绍 CodePipeline 和 CodeBuild

CodePipeline 和 CodeBuild 是 AWS 开发工具组合中的两项服务,与我们在本书中广泛使用的 CloudFormation 服务一起,为创建完整和全面的持续交付解决方案提供了构建块,为您的应用程序从开发到生产铺平道路。

CodePipeline 允许您创建复杂的流水线,将应用程序的源代码、构建、测试和发布应用程序工件,然后将应用程序部署到非生产和生产环境中。这些流水线的顶层构建模块是阶段,它们必须始终以包含一个或多个流水线的源材料的源阶段开始,例如应用程序的源代码仓库。然后,每个阶段可以由一个或多个操作组成,这些操作会产生一个工件,可以在流水线的后续阶段中使用,或者实现期望的结果,例如部署到一个环境。您可以按顺序或并行定义操作,这使您能够编排几乎任何您想要的场景;例如,我已经使用 CodePipeline 以高度受控的方式编排了完整、复杂的多应用程序环境的部署,这样可以轻松地进行可视化和管理。

每个 CodePipeline 流水线必须定义至少两个阶段,我们将在最初看到一个示例,当我们创建一个包括源阶段(从源代码仓库收集应用程序源代码)和构建阶段(从源阶段收集的应用程序源代码测试、构建和发布应用程序工件)的持续集成流水线。

理解这里的一个重要概念是“工件”的概念。CodePipeline 中的许多操作都会消耗输入工件并产生输出工件,一个操作消耗早期操作的输出的能力是 CodePipeline 工作原理的本质。

例如,以下图表说明了我们将创建的初始持续集成流水线:

持续集成流水线

在上图中,源阶段包括一个与您的 todobackend GitHub 存储库相关联的源操作。每当对 GitHub 存储库进行提交更改时,此操作将下载最新的源代码,并生成一个输出工件,将您的源代码压缩并使其可用于紧随其后的构建阶段。构建阶段有一个构建操作,它将您的源操作输出工件作为输入,然后测试、构建和发布 Docker 镜像。上图中的构建操作由 AWS CodeBuild 服务执行,该服务是一个完全托管的构建服务,为按需运行构建作业提供基于容器的构建代理。CodePipeline 确保 CodeBuild 构建作业提供了一个包含应用程序源代码的输入工件,这样 CodeBuild 就可以运行本地测试、构建和发布工作流程。

到目前为止,我们已经讨论了 CodePipeline 中源和构建阶段的概念;您在流水线中将使用的另一个常见阶段是部署阶段,在该阶段中,您将应用程序工件部署到目标环境。以下图示了如何扩展上图中显示的流水线,以持续部署您的应用程序:

持续部署流水线

在上图中,添加了一个新阶段(称为Dev 阶段);它利用 CodePipeline 与 CloudFormation 的集成将应用程序部署到非生产环境中,我们称之为 dev(开发)。因为我们使用 CloudFormation 进行部署,所以需要提供一个 CloudFormation 堆栈进行部署,这是通过在源阶段添加 todobackend-aws 存储库作为另一个源操作来实现的。部署操作还需要另一个输入工件,用于定义要部署的 Docker 镜像的标签,这是通过构建阶段中的 CodeBuild 构建操作的输出工件(称为ApplicationVersion)提供的。如果现在这些都不太明白,不要担心;我们将在本章中涵盖所有细节并设置这些流水线,但至少了解阶段、操作以及如何在它们之间传递工件以实现所需的结果是很重要的。

最后,CodePipeline 可以支持部署到多个环境,本章的最后一部分将扩展我们的流水线,以便在生产环境中执行受控发布,如下图所示:

持续交付流水线

在前面的图表中,流水线添加了一个新阶段(称为生产阶段),只有在您的应用程序成功部署在开发环境中才能执行。与开发阶段的持续部署方法不同,后者立即部署到开发环境中,生产阶段首先创建一个 CloudFormation 变更集,该变更集标识了部署的所有更改,然后触发一个手动批准操作,需要某人审查变更集并批准或拒绝更改。假设更改得到批准,生产阶段将部署更改到生产环境中,这些操作集合将共同提供对生产(或其他受控)环境的受控发布的支持。

现在您已经对 CodePipeline 有了一个高层次的概述,让我们开始创建我们在第一个图表中讨论的持续集成流水线。在构建这个流水线之前,我们需要构建一个自定义的构建容器,以满足 todobackend 存储库中定义的 Docker 工作流的要求,并且我们还需要添加对 CodeBuild 的支持,之后我们可以在 CodePipeline 中创建我们的流水线。

创建自定义 CodeBuild 容器

AWS CodeBuild 提供了一个构建服务,使用容器构建代理来执行您的构建。CodeBuild 提供了许多 AWS 策划的镜像,针对特定的应用程序语言和/或平台,比如Python,Java,PHP 等等。CodeBuild 确实提供了一个专为构建 Docker 镜像而设计的镜像;然而,这个镜像有一定的限制,它不包括 AWS CLI、GNU make 和 Docker Compose 等工具,而我们构建 todobackend 应用程序需要这些工具。

虽然您可以在 CodeBuild 中运行预构建步骤来安装额外的工具,但这种方法会减慢构建速度,因为安装额外工具将在每次构建时都会发生。CodeBuild 确实支持使用自定义镜像,这允许您预打包所有应用程序构建所需的工具。

对于我们的用例,CodeBuild 构建环境必须包括以下内容:

  • 访问 Docker 守护程序,鉴于构建建立了一个多容器环境来运行集成和验收测试

  • Docker Compose

  • GNU Make

  • AWS CLI

您可能想知道如何满足第一个要求,因为您的 CodeBuild 运行时环境位于一个隔离的容器中,无法直接访问其正在运行的基础架构。Docker 确实支持Docker 中的 DockerDinD)的概念,其中 Docker 守护程序在您的 Docker 容器内运行,允许您安装一个可以构建 Docker 镜像并使用工具如 Docker Compose 编排多容器环境的 Docker 客户端。

Docker 中的 Docker 实践有些有争议,并且是使用 Docker 更像虚拟机而不是容器的一个例子。然而,为了运行构建,这种方法是完全可以接受的。

定义自定义 CodeBuild 容器

首先,我们需要构建我们的自定义 CodeBuild 镜像,我们将在名为Dockerfile.codebuild的 Dockerfile 中定义,该文件位于 todobackend-aws 存储库中。

以下示例显示了 Dockerfile:

FROM docker:dind

RUN apk add --no-cache bash make python3 && \
    pip3 install --no-cache-dir docker-compose awscli

因为 Docker 发布了一个 Docker 中的 Docker 镜像,我们可以简单地基于这个镜像进行定制;我们免费获得了 Docker 中的 Docker 功能。DinD 镜像基于 Alpine Linux,并已经包含所需的 Docker 守护程序和 Docker 客户端。接下来,我们将添加我们构建所需的特定工具。这包括 bash shell,GNU make 和 Python 3 运行时,这是安装 Docker Compose 和 AWS CLI 所需的。

您现在可以使用docker build命令在本地构建此镜像,如下所示:

> docker build -t codebuild -f Dockerfile.codebuild .
Sending build context to Docker daemon 405.5kB
Step 1/2 : FROM docker:dind
dind: Pulling from library/docker
ff3a5c916c92: Already exists
1a649ea86bca: Pull complete
ce35f4d5f86a: Pull complete
d0600fe571bc: Pull complete
e16e21051182: Pull complete
a3ea1dbce899: Pull complete
133d8f8629ec: Pull complete
71a0f0a757e5: Pull complete
0e081d1eb121: Pull complete
5a14be8d6d21: Pull complete
Digest: sha256:2ca0d4ee63d8911cd72aa84ff2694d68882778a1c1f34b5a36b3f761290ee751
Status: Downloaded newer image for docker:dind
 ---> 1f44348b3ad5
Step 2/2 : RUN apk add --no-cache bash make python3 && pip3 install --no-cache-dir docker-compose awscli
 ---> Running in d69027d58057
...
...
Successfully built 25079965c64c
Successfully tagged codebuild:latest

在上面的示例中,使用名称为codebuild创建新构建的 Docker 镜像。现在这样做是可以的,但是我们需要将此 CodeBuild 发布到弹性容器注册表ECR),以便 CodeBuild 可以使用。

为自定义 CodeBuild 容器创建存储库

现在,您已经构建了一个自定义的 CodeBuild 图像,您需要将图像发布到 CodeBuild 可以从中拉取图像的位置。如果您使用 ECR,通常会将此图像发布到 ECR 中的存储库,这就是我们将采取的方法。

首先,您需要在todobackend-aws文件夹的根目录中的ecr.yml文件中添加一个新的存储库,该文件夹是您在本章中创建的:

AWSTemplateFormatVersion: "2010-09-09"

Description: ECR Resources

Resources:
  CodebuildRepository:
 Type: AWS::ECR::Repository
 Properties:
RepositoryName: docker-in-aws/codebuild
 RepositoryPolicyText:
 Version: '2008-10-17'
 Statement:
 - Sid: CodeBuildAccess
 Effect: Allow
 Principal:
 Service: codebuild.amazonaws.com
 Action:
 - ecr:GetDownloadUrlForLayer
 - ecr:BatchGetImage
 - ecr:BatchCheckLayerAvailability
  TodobackendRepository:
    Type: AWS::ECR::Repository
  ...
  ...

在前面的示例中,您创建了一个名为docker-in-aws/codebuild的新存储库,这将导致一个名为<account-id>.dkr.ecr.<region>.amazonaws.com/docker-in-aws/codebuild的完全限定存储库(例如385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/codebuild)。请注意,您必须授予 CodeBuild 服务拉取访问权限,因为 CodeBuild 需要拉取图像以运行作为其构建容器。

您现在可以使用aws cloudformation deploy命令将更改部署到 ECR 堆栈,您可能还记得来自章节《使用 ECR 发布 Docker 镜像》的命令,部署到名为 ecr-repositories 的堆栈:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file ecr.yml --stack-name ecr-repositories
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - ecr-repositories

部署完成后,您需要使用您之前创建的图像的完全限定名称重新标记图像,然后您可以登录到 ECR 并发布图像:

> docker tag codebuild 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/codebuild
> eval $(aws ecr get-login --no-include-email)
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
> docker push 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/codebuild
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/codebuild]
770fb042ae3b: Pushed
0cdc6e0d843b: Pushed
395fced17f47: Pushed
3abf4e550e49: Pushed
0a6dfdbcc220: Pushed
27760475e1ac: Pushed
5270ef39cae0: Pushed
2c88066e123c: Pushed
b09386d6aa0f: Pushed
1ed7a5e2d1b3: Pushed
cd7100a72410: Pushed
latest: digest:
sha256:858becbf8c64b24e778e6997868f587b9056c1d1617e8d7aa495a3170761cf8b size: 2618

向您的应用程序存储库添加 CodeBuild 支持

每当您创建 CodeBuild 项目时,必须定义 CodeBuild 应如何测试和构建应用程序源代码,然后发布应用程序工件和/或 Docker 镜像。 CodeBuild 在构建规范中定义这些任务,构建规范提供了 CodeBuild 代理在运行构建时应执行的构建说明。

CodeBuild 允许您以多种方式提供构建规范:

  • 自定义:CodeBuild 查找项目的源存储库中定义的文件。默认情况下,这是一个名为buildspec.yml的文件;但是,您还可以配置一个自定义文件,其中包含您的构建规范。

  • 预配置:当您创建 CodeBuild 项目时,可以在项目设置的一部分中定义构建规范。

  • 按需:如果您使用 AWS CLI 或 SDK 启动 CodeBuild 构建作业,您可以覆盖预配置或自定义的构建规范

一般来说,我建议使用自定义方法,因为它允许存储库所有者(通常是您的开发人员)独立配置和维护规范;这是我们将采取的方法。

以下示例演示了在名为buildspec.yml的文件中向 todobackend 存储库添加构建规范:

version: 0.2

phases:
  pre_build:
    commands:
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --storage-driver=overlay&
      - timeout -t 15 sh -c "until docker info; do echo .; sleep 1; done"
      - export BUILD_ID=$(echo $CODEBUILD_BUILD_ID | sed 's/^[^:]*://g')
      - export APP_VERSION=$CODEBUILD_RESOLVED_SOURCE_VERSION.$BUILD_ID
      - make login
  build:
    commands:
      - make test
      - make release
      - make publish
  post_build:
    commands:
      - make clean
      - make logout

构建规范首先指定了必须包含在每个构建规范中的版本,本书编写时最新版本为0.2。接下来,您定义了阶段序列,这是必需的,定义了 CodeBuild 将在构建的各个阶段运行的命令。在前面的示例中,您定义了三个阶段:

  • pre_build:CodeBuild 在构建之前运行的命令。在这里,您可以运行诸如登录到 ECR 或构建成功运行所需的任何其他命令。

  • build:这些命令运行您的构建步骤。

  • post_build:CodeBuild 在构建后运行的命令。这些通常涉及清理任务,例如退出 ECR 并删除临时文件。

您可以在docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html找到有关 CodeBuild 构建规范的更多信息。

pre_build阶段,您执行以下操作:

  • 前两个命令用于在自定义 CodeBuild 镜像中启动 Docker 守护程序;nohup命令将 Docker 守护程序作为后台任务启动,而timeout命令用于确保 Docker 守护程序已成功启动,然后再继续尝试。

  • 导出一个BUILD_ID环境变量,用于将构建信息添加到将为您的构建生成的应用程序版本中。此BUILD_ID值将被添加到构建阶段期间构建的 Docker 镜像附加的应用程序版本标记中,因此,它只能包含与 Docker 标记格式兼容的字符。CodeBuild 作业 ID 通过CODEBUILD_BUILD_ID环境变量暴露给您的构建代理,并且格式为<project-name>:<job-id>,其中<job-id>是 UUID 值。CodeBuild 作业 ID 中的冒号在 Docker 标记中不受支持;因此,您可以使用sed表达式剥离作业 ID 的<project-name>部分,只留下将包含在 Docker 标记中的作业 ID 值。

  • 导出APP_VERSION环境变量,在 Makefile 中用于定义构建的 Docker 镜像上标记的应用程序版本。当您在 CodeBuild 与 CodePipeline 一起使用时,重要的是要了解,呈现给 CodeBuild 的源构件实际上是位于 S3 存储桶中的一个压缩版本,CodePipeline 在从源代码库克隆源代码后创建。CodePipeline 不包括任何 Git 元数据;因此,在 todobackend Makefile 中的APP_VERSION指令 - export APP_VERSION ?= $(shell git rev-parse --short HEAD - 将失败,因为 Git 客户端将没有任何可用的 Git 元数据。幸运的是,在 GNU Make 中的?=语法意味着如果环境中已经定义了前述环境变量的值,那么就使用该值。因此,我们可以在 CodeBuild 环境中导出APP_VERSION,并且 Make 将只使用配置的值,而不是运行 Git 命令。在前面的示例中,您从一个名为CODEBUILD_RESOLVED_SOURCE_VERSION的变量构造了APP_VERSION,它是源代码库的完整提交哈希,并由 CodePipeline 设置。您还附加了在前一个命令中计算的BUILD_ID变量,这允许您将特定的 Docker 镜像构建跟踪到一个 CodeBuild 构建作业。

  • 使用源代码库中包含的make login命令登录到 ECR。

一旦pre_build阶段完成,构建阶段就很简单了,只需执行我们在本书中迄今为止手动执行的各种构建步骤。最终的post_build阶段运行make clean任务来拆除 Docker Compose 环境,然后通过运行make logout命令删除任何本地 ECR 凭据。

一个重要的要点是post_build阶段始终运行,即使构建阶段失败也是如此。这意味着您应该仅将post_build任务保留为无论构建是否通过都会运行的操作。例如,您可能会尝试将make publish任务作为post_build步骤运行;但是,如果您这样做,且前一个构建阶段失败,CodeBuild 仍将尝试运行 make publish 任务,因为它被定义为post_build步骤。将 make publish 任务放置在构建阶段的最后一个操作确保如果 make test 或 make release 失败,构建阶段将立即以错误退出,绕过 make publish 操作并继续执行post_build步骤中的清理任务。

您可以在docs.aws.amazon.com/codebuild/latest/userguide/view-build-details.html#view-build-details-phases找到有关所有 CodeBuild 阶段以及它们在成功/失败时是否执行的更多信息。

您需要执行的最后一步是将更改提交并推送到您的 Git 存储库,以便在配置 CodePipeline 和 CodeBuild 时新创建的buildspec.yml文件可用:

> git add -A
> git commit -a -m "Add build specification"
[master ab7ac16] Add build specification
 1 file changed, 19 insertions(+)
 create mode 100644 buildspec.yml
> git push
Counting objects: 3, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 584 bytes | 584.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:docker-in-aws/todobackend.git
   5fdbe62..ab7ac16 master -> master

使用 CodePipeline 创建持续集成管道

现在,您已经建立了支持 CodeBuild 的先决条件,您可以创建一个持续集成的 CodePipeline 管道,该管道将使用 CodeBuild 来测试、构建和发布您的 Docker 镜像。持续集成侧重于不断将应用程序源代码更改合并到主分支,并通过创建构建并针对其运行自动化测试来验证更改。

根据本章第一个图表,当您为持续集成配置 CodePipeline 管道时,通常涉及两个阶段:

  • 源阶段:下载源应用程序存储库,并使其可用于后续阶段。对于我们的用例,您将把 CodePipeline 连接到 GitHub 存储库的主分支,对该存储库的后续提交将自动触发新的管道执行。

  • 构建阶段:运行在源应用程序存储库中定义的构建、测试和发布工作流程。对于我们的用例,我们将使用 CodeBuild 来运行此阶段,它将执行源存储库中定义的构建任务buildspec.yml文件,这是在本章前面创建的。

使用 AWS 控制台创建 CodePipeline 管道

要开始,请首先从 AWS 控制台中选择服务,然后选择CodePipeline。如果这是您第一次使用 CodePipeline,您将看到一个介绍页面,您可以单击“开始”按钮开始 CodePipeline 向导。

首先要求您为管道输入名称,然后单击“下一步”,您将被提示设置源提供程序,该提供程序定义将在您的管道中使用的源存储库或文件的提供程序:

在从下拉菜单中选择 GitHub 后,单击“连接到 GitHub”按钮,这将重定向您到 GitHub,在那里您将被提示登录并授予 CodePipeline 对您的 GitHub 帐户的访问权限:

点击授权 aws-codesuite 按钮后,您将被重定向回 CodePipeline 向导,您可以选择 todobackend 存储库和主分支:

如果单击“下一步”,您将被要求选择构建提供程序,该提供程序定义将在您的管道中执行构建操作的构建服务的提供程序:

在选择 AWS CodeBuild 并选择“创建新的构建项目”选项后,您需要配置构建项目,如下所示:

  • 环境镜像:对于环境镜像,请选择“指定 Docker 镜像”选项,然后将环境类型设置为 Linux,自定义镜像类型设置为 Amazon ECR;然后选择您在本章前面发布的docker-in-aws/codebuild repository/latest镜像。

  • 高级:确保设置特权标志,如下面的屏幕截图所示。每当您在 Docker 中运行 Docker 镜像时,这是必需的:

完成构建项目配置后,请确保在单击“下一步”继续之前,单击“保存构建项目”。

在下一阶段,您将被要求定义一个部署阶段。在这一点上,我们只想执行测试、构建和发布我们的 Docker 应用程序的持续集成任务,因此选择“无部署”选项,然后单击“下一步”继续:

最后一步是配置 CodePipeline 可以假定的 IAM 角色,以执行管道中的各种构建和部署任务。单击“创建角色”按钮,这将打开一个新窗口,要求您创建一个新的 IAM 角色,具有适当的权限,供 CodePipeline 使用:

在审阅政策文件后,单击“允许”,这将在 CodePipeline 向导中选择新角色。最后,单击“下一步”,审查管道配置,然后单击“创建管道”以创建新管道。

在这一点上,您的管道将被创建,并且您将被带到您的管道的管道配置视图。每当您第一次为管道创建管道时,CodePipeline 将自动触发管道的第一次执行,几分钟后,您应该注意到管道的构建阶段失败了:

要了解有关构建失败的更多信息,请单击“详细信息”链接,这将弹出有关失败的更多详细信息,并且还将包括到构建失败的 CodeBuild 作业的链接。如果单击此链接并向下滚动,您会看到失败发生在pre_build阶段,并且在构建日志中,问题与 IAM 权限问题有关:

问题在于 CodePipeline 向导期间自动创建的 IAM 角色不包括登录到 ECR 的权限。

为了解决这个问题,打开 IAM 控制台,从左侧菜单中选择角色,找到由向导创建的code-build-todobackend-service-role。在权限选项卡中,点击附加策略,找到AmazonEC2ContainerRegistryPowerUser托管策略,并点击附加策略按钮。power user 角色授予登录、拉取和推送权限,因为我们将作为构建工作流的一部分发布到 ECR,所以需要这个级别的访问权限。完成配置后,角色的权限选项卡应该与下面的截图一样:

现在您已经解决了权限问题,请导航回到您的流水线的 CodePipeline 详细信息视图,点击构建阶段的重试按钮,并确认重试失败的构建。这一次,几分钟后,构建应该成功完成,您可以使用aws ecr list-images命令来验证已经发布了新的镜像到 ECR:

> aws ecr list-images --repository-name docker-in-aws/todobackend \
 --query imageIds[].imageTag --output table
-----------------------------------------------------------------------------------
| ListImages                                                                      |
+---------------------------------------------------------------------------------+
| 5fdbe62                                                                         |
| latest                                                                          |
| ab7ac1649e8ef4d30178c7f68899628086155f1d.10f5ef52-e3ff-455b-8ffb-8b760b7b9c55   |
+---------------------------------------------------------------------------------+

请注意,最后发布的镜像的格式为<long commit hash>.<uuid>,其中<uuid>是 CodeBuild 作业 ID,证实 CodeBuild 已成功将新镜像发布到 ECR。

使用 CodePipeline 创建持续交付流水线

此时,您已经拥有了一个持续集成流水线,每当在主分支上推送提交到您的源代码库时,它将自动发布新的 Docker 镜像。在某个时候,您将希望将 Docker 镜像部署到一个环境(也许是一个分段环境,在那里您可以运行一些端到端测试来验证您的应用程序是否按预期工作),然后再部署到为最终用户提供服务的生产环境。虽然您可以通过手动更新ApplicationImageTag输入来手动部署这些更改到 todobackend 堆栈,但理想情况下,您希望能够自动将这些更改持续部署到至少一个环境中,这样可以立即让开发人员、测试人员和产品经理访问,并允许从参与应用程序开发的关键利益相关者那里获得快速反馈。

这个概念被称为持续部署。换句话说,每当您持续集成和构建经过测试的软件构件时,您就会持续部署这些构件。持续部署在当今非常普遍,特别是如果您部署到非生产环境。远不那么普遍的是一直持续部署到生产环境。要实现这一点,您必须具有高度自动化的部署后测试,并且至少根据我的经验,这对大多数组织来说仍然很难实现。更常见的方法是持续交付,您可以将其视为一旦确定您的发布准备好投入生产,就能自动部署到生产的能力。

持续交付允许常见的情况,即您需要对生产环境进行受控发布,而不是一旦发布可用就持续部署到生产环境。这比一直持续部署到生产环境更可行,因为它允许在选择部署到生产环境之前对非生产环境进行手动测试。

现在您已经了解了持续交付的背景,让我们扩展我们的管道以支持持续交付。

CodePipeline 包括对 ECS 作为部署目标的支持,您可以将持续集成管道发布的新镜像部署到目标 ECS 集群和 ECS 服务。在本章中,我将使用 CloudFormation 来部署应用程序更改;但是,您可以在docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-cd-pipeline.html了解更多关于 ECS 部署机制的信息。

这一阶段的第一步是配置您的代码更改的持续部署到非生产环境,这需要您执行以下配置操作,这些操作将在后续详细讨论:

  • 在您的源代码存储库中发布版本信息

  • 为您的部署存储库添加 CodePipeline 支持

  • 将您的部署存储库添加到 CodePipeline

  • 为您的构建操作添加一个输出构件

  • 为 CloudFormation 部署创建一个 IAM 角色

  • 在管道中添加一个部署阶段

在您的源代码存储库中发布版本信息

我们流水线的一个关键要求是能够将新构建的 Docker 镜像部署到我们的 AWS 环境中。目前,CodePipeline 并不真正了解发布的 Docker 镜像标记。我们知道该标记在 CodeBuild 环境中配置,但 CodePipeline 并不了解这一点。

为了使用在 CodeBuild 构建阶段生成的 Docker 镜像标记,您需要生成一个输出构件,首先由 CodeBuild 收集,然后在 CodePipeline 中的未来部署阶段中提供。

为了做到这一点,您必须首先定义 CodeBuild 应该收集的构件,您可以通过在 todobackend 存储库中的buildspec.yml构建规范中添加artifacts参数来实现这一点:

version: 0.2

phases:
  pre_build:
    commands:
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --storage-driver=overlay&
      - timeout -t 15 sh -c "until docker info; do echo .; sleep 1; done"
      - export BUILD_ID=$(echo $CODEBUILD_BUILD_ID | sed 's/^[^:]*://g')
      - export APP_VERSION=$CODEBUILD_RESOLVED_SOURCE_VERSION.$BUILD_ID
      - make login
  build:
    commands:
      - make test
      - make release
      - make publish
      - make version > version.json
  post_build:
    commands:
      - make clean
      - make logout

artifacts:
 files:
 - version.json

在上面的示例中,artifacts参数配置 CodeBuild 在位置version.json查找构件。请注意,您还需要向构建阶段添加一个额外的命令,该命令将make version命令的输出写入version.json文件,CodeBuild 期望在那里找到构件。

在这一点上,请确保您提交并推送更改到 todobackend 存储库,以便将来的构建可以使用这些更改。

向部署存储库添加 CodePipeline 支持

当您使用 CodePipeline 使用 CloudFormation 部署您的环境时,您需要确保您可以提供一个包含输入堆栈参数、堆栈标记和堆栈策略配置的配置文件。该文件必须以 JSON 格式实现,如docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/continuous-delivery-codepipeline-cfn-artifacts.html#w2ab2c13c15c15中定义的那样,因此我们需要修改todobackend-aws存储库中输入参数文件的格式,该文件目前以<parameter>=<value>格式位于名为dev.cfg的文件中。根据所引用的文档,您所有的输入参数都需要位于一个名为Parameters的键下的 JSON 文件中,您可以在todobackend-aws存储库的根目录下定义一个名为dev.json的新文件。

{ 
  "Parameters": {
    "ApplicationDesiredCount": "1",
    "ApplicationImageId": "ami-ec957491",
    "ApplicationImageTag": "latest",
    "ApplicationSubnets": "subnet-a5d3ecee,subnet-324e246f",
    "VpcId": "vpc-f8233a80"
  }
}

在前面的例子中,请注意我已将ApplicationImageTag的值更新为latest。这是因为我们的流水线实际上会动态地从流水线的构建阶段获取ApplicationImageTag输入的值,而latest值是一个更安全的默认值,以防您希望从命令行手动部署堆栈。

此时,dev.cfg文件是多余的,可以从您的存储库中删除;但是,请注意,鉴于aws cloudformation deploy命令期望以<parameter>=<value>格式提供输入参数,您需要修改手动从命令行运行部署的方式。

您可以解决这个问题的一种方法是使用jq实用程序将新的dev.json配置文件转换为所需的<parameter>=<value>格式:

> aws cloudformation deploy --template-file stack.yml --stack-name todobackend \
    --parameter-overrides $(cat dev.json | jq -r '.Parameters|to_entries[]|.key+"="+.value') \
    --capabilities CAPABILITY_NAMED_IAM

这个命令现在相当冗长,为了简化运行这个命令,您可以向todobackend-aws存储库添加一个简单的 Makefile:

.PHONY: deploy

deploy/%:
  aws cloudformation deploy --template-file stack.yml --stack-name todobackend-$* \
    --parameter-overrides $$(cat $*.json | jq -r '.Parameters|to_entries[]|.key+"="+.value') \
    --capabilities CAPABILITY_NAMED_IAM

在前面的例子中,任务名称中的%字符捕获了一个通配文本值,无论何时执行make deploy命令。例如,如果您运行make deploy/dev,那么%字符将捕获dev,如果您运行make deploy/prod,那么捕获的值将是prod。然后,您可以使用$*变量引用捕获的值,您可以看到我们已经在堆栈名称(todobackend-$*,在前面的例子中会扩展为todobackend-devtodobackend-prod)和用于 catdev.jsonprod.json文件的命令中替换了这个变量。请注意,因为在本书中我们一直将堆栈命名为todobackend,所以这个命令对我们来说不太适用,但是如果您将堆栈重命名为todobackend-dev,这个命令将使手动部署到特定环境变得更加容易。

在继续之前,您需要添加新的dev.json文件,提交并推送更改到源 Git 存储库,因为我们将很快将todobackend-aws存储库添加为 CodePipeline 流水线中的另一个源。

为 CloudFormation 部署创建 IAM 角色

当您使用 CodePipeline 部署 CloudFormation 堆栈时,CodePipeline 要求您指定一个 IAM 角色,该角色将由 CloudFormation 服务来部署您的堆栈。CloudFormation 支持指定 CloudFormation 服务将承担的 IAM 角色,这是一个强大的功能,允许更高级的配置场景,例如从中央构建账户进行跨账户部署。此角色必须指定 CloudFormation 服务作为可信实体,可以承担该角色;因此,通常不能使用为人员访问创建的管理角色,例如您在本书中一直在使用的管理员角色。

要创建所需的角色,请转到 IAM 控制台,从左侧菜单中选择“角色”,然后点击“创建角色”按钮。在“选择服务”部分,选择“CloudFormation”,然后点击“下一步:权限”继续。在“附加权限策略”屏幕上,您可以创建或选择一个适当的策略,其中包含创建堆栈所需的各种权限。为了保持简单,我将只选择“AdministratorAccess”策略。但是,在实际情况下,您应该创建或选择一个仅授予创建 CloudFormation 堆栈所需的特定权限的策略。点击“下一步:审阅”按钮后,指定角色名称为cloudformation-deploy,然后点击“创建角色”按钮创建新角色:

向 CodePipeline 添加部署存储库

现在,您已经准备好了适当的堆栈配置文件和 IAM 部署角色,可以开始修改管道,以支持将应用程序更改持续交付到目标 AWS 环境。您需要执行的第一个修改是将 todobackend-aws 存储库作为另一个源操作添加到管道的源阶段。要执行此操作,请转到管道的详细信息视图,并点击“编辑”按钮。

在编辑屏幕中,您可以点击源阶段右上角的铅笔图标,这将改变视图并允许您添加一个新的源操作,可以在当前操作之前、之后或与当前操作在同一级别:

编辑管道

对于我们的场景,我们可以并行下载部署存储库源;因此,在与其他源存储库相同级别添加一个新操作,这将打开一个添加操作对话框。选择“动作类别”为“源”,配置一个名称为DeploymentRepository或类似的操作名称,然后选择 GitHub 作为源提供者,并单击“连接到 GitHub”按钮,在docker-in-aws/todobackend-aws存储库上选择主分支:

添加部署存储库

接下来,滚动到页面底部,并为此源操作的输出工件配置一个名称。CodePipeline 将使部署存储库中的基础架构模板和配置可用于管道中的其他阶段,您可以通过配置的输出工件名称引用它们:

配置输出工件名称

在前面的截图中,您还将输出工件名称配置为DeploymentRepository(与源操作名称相同),这有助于,因为管道详细信息视图仅显示阶段和操作名称,不显示工件名称。

在构建阶段添加输出工件

添加部署存储库操作后,编辑管道屏幕应如下截图所示:

编辑管道屏幕

您需要执行的下一个管道配置任务是修改构建阶段中的 CodeBuild 构建操作,该操作是由 CodePipeline 向导为您创建的,当您创建管道时。

您可以通过点击前面截图中 CodeBuild 操作框右上角的铅笔图标来执行此操作,这将打开编辑操作对话框:

编辑构建操作

在前面的截图中,请注意 CodePipeline 向导已经配置了输入和输出工件:

  • 输入工件:CodePipeline 向导将其命名为MyApp,这指的是与您创建管道时引用的源存储库相关联的输出工件(在本例中,这是 GitHub todobackend 存储库)。如果要重命名此工件,必须确保在拥有操作(在本例中是源阶段中的源操作)上重命名输出工件名称,然后更新任何使用该工件作为输入的操作。

  • 输出工件:CodePipeline 向导默认将其命名为MyAppBuild,然后可以在流水线的后续阶段中引用。输出工件由buildspec.yml文件中的 artifacts 属性确定,对于我们的用例,这个工件不是应用程序构建,而是捕获版本元数据(version.json)的版本工件,因此我们将这个工件重命名为ApplicationVersion

向流水线添加部署阶段

在上述截图中单击“更新”按钮后,您可以通过单击构建阶段下方的“添加阶段”框来添加一个新阶段。对于阶段名称,请输入名称Dev,这将代表部署到名为 Dev 的环境,然后单击“添加操作”框以添加新操作:

添加部署操作

因为这是一个部署阶段,所以从操作类别下拉菜单中选择“部署”,配置一个操作名称为“部署”,并选择 AWS CloudFormation 作为部署提供程序:

配置 CloudFormation 部署操作

这将公开与 CloudFormation 部署相关的一些配置参数,如前面的截图所示:

  • 操作模式:选择“创建或更新堆栈”选项,如果堆栈不存在,则将创建一个新堆栈,或者更新现有堆栈。

  • 堆栈名称:引用您在之前章节中已部署的现有 todobackend 堆栈。

  • 模板:指的是应该部署的 CloudFormation 模板文件。这是以InputArtifactName::TemplateFileName的格式表示的,在我们的情况下是DeploymentRepository::stack.yml,因为我们为DeploymentRepository源操作配置了一个输出工件名称,并且我们的堆栈位于存储库根目录的stack.yml文件中。

  • 模板配置:指的是用于提供堆栈参数、标记和堆栈策略的配置文件。这需要引用您之前创建的新dev.json文件,在todobackend-aws部署存储库中;它与模板参数的格式相同,值为DeploymentRepository::dev.json

一旦您配置了前面截图中显示的属性,请继续向下滚动并展开高级部分,如下面的截图所示:

配置额外的 CloudFormation 部署操作属性

以下描述了您需要配置的每个额外参数:

  • 功能:这授予了 CloudFormation 部署操作的权限,以代表您创建 IAM 资源,并且与您传递给aws cloudformation deploy命令的--capabilities标志具有相同的含义。

  • 角色名称:这指定了 CloudFormation 部署操作使用的 IAM 角色,用于部署您的 CloudFormation 堆栈。引用您之前创建的cloudformation-deploy角色。

  • 参数覆盖:此参数允许您覆盖通常由模板配置文件(dev.json)或 CloudFormation 模板中的默认值提供的输入参数值。对于我们的用例,我们需要覆盖ApplicationImageTag参数,因为这需要反映作为构建阶段的一部分创建的图像标记。CodePipeline 支持两种类型的参数覆盖(请参阅使用参数覆盖函数),对于我们的用例,我们使用Fn::GetParam覆盖,它可以用于从由您的流水线输出的工件中提取属性值的 JSON 文件中。回想一下,在本章的前面,我们向 todobackend 存储库添加了一个make version任务,该任务输出了作为 CodeBuild 构建规范的一部分收集的version.json文件。我们更新了构建操作以引用此工件为ApplicationVersion。在前面的屏幕截图中,提供给Fn::GetParam调用的输入列表首先引用了工件(ApplicationVersion),然后是工件中 JSON 文件的路径(version.json),最后是 JSON 文件中保存参数覆盖值的键(Version)。

  • 输入工件:这必须指定您在部署配置中引用的任何输入工件。在这里,我们添加了DeploymentRepository(用于模板和模板配置参数)和ApplicationVersion(用于参数覆盖配置)。

完成后,单击“添加操作”按钮,然后您可以单击“保存管道更改”以完成管道的配置。在这一点上,您可以通过单击“发布更改”按钮来测试您的新部署操作是否正常工作,这将手动触发管道的新执行;几分钟内,您的管道应该成功构建、测试和发布一个新的镜像作为构建阶段的一部分,然后成功通过 dev 阶段将更改部署到您的 todobackend 堆栈:

通过 CodePipeline 成功部署 CloudFormation

在上面的屏幕截图中,您可以在部署期间或之后单击“详细信息”链接,这将带您到 CloudFormation 控制台,并向您显示有关正在进行中或已完成的部署的详细信息。如果您展开“参数”选项卡,您应该会看到 ApplicationImageTag 引用的标签格式为<长提交哈希>.<codebuild 作业 ID>,这证实我们的流水线实际上已部署了在构建阶段构建的 Docker 镜像:

确认覆盖的输入参数

使用 CodePipeline 持续交付到生产环境

现在我们正在持续部署到非生产环境,我们持续交付旅程的最后一步是启用能够以受控方式将应用程序发布到生产环境的能力。CodePipeline 通过利用 CloudFormation 的一个有用特性来支持这一能力,称为变更集。变更集描述了将应用于给定 CloudFormation 堆栈的各种配置更改,这些更改基于可能已应用于堆栈模板文件和/或输入参数的任何更改。对于新的应用程序发布,通常只会更改定义新应用程序构件版本的输入参数。例如,我们的流水线的 dev 阶段覆盖了ApplicationImageTag输入参数。在某些情况下,您可能会对 CloudFormation 堆栈和输入参数进行更广泛的更改。例如,您可能需要为容器添加新的环境变量,或者向堆栈添加新的基础设施组件或支持服务。这些更改通常会提交到您的部署存储库中,并且鉴于我们的部署存储库是我们流水线中的一个源,对部署存储库的任何更改都将被捕获为一个变更。

CloudFormation 变更集为您提供了一个机会,可以审查即将应用于目标环境的任何更改,如果变更集被认为是安全的,那么您可以从该变更集发起部署。CodePipeline 支持生成 CloudFormation 变更集作为部署操作,然后可以与单独的手动批准操作结合使用,允许适当的人员审查变更集,随后批准或拒绝变更。如果变更得到批准,那么您可以从变更集触发部署,从而提供一种有效的方式来对生产环境或任何需要某种形式的变更控制的环境进行受控发布。

现在让我们扩展我们的流水线,以支持将应用程序发布受控地部署到新的生产环境,这需要您执行以下配置更改:

  • 向部署存储库添加新的环境配置文件

  • 向流水线添加创建变更集操作

  • 向流水线添加手动批准操作

  • 向流水线添加部署变更集操作

  • 部署到生产环境

向部署存储库添加新的环境配置文件

因为我们正在创建一个新的生产环境,我们需要向部署存储库添加一个环境配置文件,其中将包括特定于您的生产环境的输入参数。如前面的示例所示,演示了在todobackend-aws存储库的根目录下添加一个名为prod.json的新文件:

{ 
  "Parameters": {
    "ApplicationDesiredCount": "1",
    "ApplicationImageId": "ami-ec957491",
    "ApplicationImageTag": "latest",
    "ApplicationSubnets": "subnet-a5d3ecee,subnet-324e246f",
    "VpcId": "vpc-f8233a80"
  }
}

您可以看到配置文件的格式与我们之前修改的dev.json文件相同。在现实世界的情况下,您当然会期望配置文件中有所不同。例如,我们正在使用相同的应用子网和 VPC ID;您通常会为生产环境拥有一个单独的 VPC,甚至一个单独的账户,但为了保持简单,我们将生产环境部署到与开发环境相同的 VPC 和子网中。

您还需要对我们的 CloudFormation 堆栈文件进行一些微小的更改,因为其中有一些硬编码的名称,如果您尝试在同一 AWS 账户中创建一个新堆栈,将会导致冲突。

...
...
Resources:
  ...
  ...
  ApplicationCluster:
    Type: AWS::ECS::Cluster
    Properties:
      # ClusterName: todobackend-cluster
      ClusterName: !Sub: ${AWS::StackName}-cluster
  ...
  ...
  MigrateTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      # Family: todobackend-migrate
      Family: !Sub ${AWS::StackName}-migrate
      ...
      ...
  ApplicationTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      # Family: todobackend
      Family: !Ref AWS::StackName
  ...
  ...

在前面的示例中,我已经注释了以前的配置,然后突出显示了所需的新配置。在所有情况下,我们将硬编码的对 todobackend 的引用替换为对堆栈名称的引用。鉴于 CloudFormation 堆栈名称在给定的 AWS 账户和区域内必须是唯一的,这确保了修改后的资源将具有唯一名称,不会与同一账户和区域内的其他堆栈发生冲突。

为了保持简单,生产环境的 CloudFormation 堆栈将使用我们在管理秘密章节中创建的相同秘密。在现实世界的情况下,您会为每个环境维护单独的秘密。

在放置新的配置文件和模板更改后,确保在继续下一部分之前已经将更改提交并推送到 GitHub:

> git add -A
> git commit -a -m "Add prod environment support"
[master a42af8d] Add prod environment support
 2 files changed, 12 insertions(+), 3 deletions(-)
 create mode 100644 prod.json
> git push
...
...

向管道添加创建变更集操作

我们现在准备向我们的管道中添加一个新阶段,用于将我们的应用部署到生产环境。我们将在这个阶段创建第一个操作,即创建一个 CloudFormation 变更集。

在管道的详细信息视图中,单击“编辑”按钮,然后在 dev 阶段之后添加一个名为 Production 的新阶段,然后向新阶段添加一个操作:

向管道添加一个生产阶段

在“添加操作”对话框中,您需要创建一个类似于为 dev 阶段创建的部署操作的操作,但有一些变化:

向管道添加创建更改集操作

如果您将 dev 阶段的部署操作配置与前面截图中显示的新创建更改集操作配置进行比较,您会发现配置非常相似,除了以下关键差异:

  • 操作模式:您可以将其配置为createreplace更改集,而不是部署堆栈,只会创建一个新的更改集。

  • 堆栈名称:由于此操作涉及我们的生产环境,您需要配置一个新的堆栈名称,我们将其称为todobackend-prod

  • 更改集名称:这定义了更改集的名称。我通常将其命名为与堆栈名称相同,因为该操作将在每次执行时创建或替换更改集。

  • 模板配置:在这里,您需要引用之前示例中添加到todobackend-aws存储库的新prod.json文件,因为这个文件包含特定于生产环境的输入参数。该文件通过从todobackend-aws存储库创建的DeploymentRepository工件提供。

接下来,您需要向下滚动,展开高级部分,使用Fn::GetParam语法配置参数覆盖属性,并最终将ApplicationVersionDeploymentRepository工件配置为输入工件。这与您之前为dev/deploy操作执行的配置相同。

向管道添加手动批准操作

完成更改集操作的配置后,您需要添加一个在更改集操作之后执行的新操作:

向管道添加批准操作

在“添加操作”对话框中,选择“批准”作为操作类别,然后配置一个操作名称为 ApproveChangeSet。选择手动批准类型后,注意您可以添加 SNS 主题 ARN 和其他信息到手动批准请求。然后可以用于向批准者发送电子邮件,或触发执行一些自定义操作的 lambda 函数,例如将消息发布到 Slack 等消息工具中。

向管道添加部署更改集操作

您需要创建的最后一个操作是,在批准 ApproveChangeSet 操作后,部署先前在 ChangeSet 操作中创建的更改集:

向流水线添加执行更改集操作

在上述截图中,我们选择了“执行更改集”操作模式,然后配置了堆栈名称和更改集名称,这些名称必须与您在 ChangeSet 操作中之前配置的相同值匹配。

部署到生产环境

在上述截图中单击“添加操作”后,您的新生产阶段的管道配置应如下所示:

向流水线添加创建更改集操作

在这一点上,您可以通过单击“保存管道更改”按钮保存管道更改,并通过单击“发布更改”按钮测试新的管道阶段,这将强制执行新的管道执行。在管道成功执行构建和开发阶段后,生产阶段将首次被调用,由 ChangeSet 操作创建一个 CloudFormation 更改集,之后将触发批准操作。

向流水线添加创建更改集操作

现在管道将等待批准,这是批准者通常会通过单击 ChangeSet 操作的“详细信息”链接来审查先前创建的更改集:

CloudFormation 更改集

正如您在上述截图中所看到的,更改集指示将创建堆栈中的所有资源,因为生产环境目前不存在。随后的部署应该有非常少的更改,因为堆栈将就位,典型的更改是部署新的 Docker 镜像。

审查更改集并返回到 CodePipeline 详细视图后,您现在可以通过单击“审查”按钮来批准(或拒绝)更改集。这将呈现一个批准或拒绝修订对话框,在这里您可以添加评论并批准或拒绝更改:

批准或拒绝手动批准操作

如果您点击“批准”,流水线将继续执行下一个操作,即部署与之前 ChangeSet 操作相关联的变更集。对于这次执行,将部署一个名为todobackend-prod的新堆栈,一旦完成,您就成功地使用 CodePipeline 部署了一个全新的生产环境!

在这一点上,您应该测试并验证您的新堆栈和应用程序是否按预期工作,按照“使用 ECS 部署应用程序”章节中“部署应用程序负载均衡器”部分的步骤获取应用程序负载均衡器端点的 DNS 名称,您的生产应用程序端点将从中提供服务。我还鼓励您触发流水线,无论是手动触发还是通过对任一存储库进行测试提交,然后审查生成的后续变更集,以进行对现有环境的应用程序部署。请注意,您可以选择何时部署到生产环境。例如,您的开发人员可能多次提交应用程序更改,每次更改都会自动部署到非生产环境,然后再选择部署下一个版本到生产环境。当您选择部署到生产环境时,您的生产阶段将采用最近成功部署到非生产环境的最新版本。

一旦您完成了对生产部署的测试,如果您使用的是免费套餐账户,请记住您现在有多个 EC2 实例和 RDS 实例在运行,因此您应该考虑拆除您的生产环境,以避免产生费用。

摘要

在本章中,您创建了一个端到端的持续交付流水线,该流水线自动测试、构建和发布您的应用程序的 Docker 镜像,持续将新的应用程序更改部署到非生产环境,并允许您在生成变更集并在部署到生产环境之前需要手动批准的情况下执行受控发布。

您学会了如何将您的 GitHub 存储库与 CodePipeline 集成,方法是将它们定义为源阶段中的源操作,然后创建一个构建阶段,该阶段使用 CodeBuild 来测试、构建和发布应用程序的 Docker 镜像。您向 todobackend 存储库添加了构建规范,CodeBuild 使用它来执行您的构建,并创建了一个自定义的 CodeBuild 容器,该容器能够在 Docker 中运行 Docker,以允许您构建 Docker 镜像并在 Docker Compose 环境中执行集成和验收测试。

接下来,您在 CodePipeline 中创建了一个部署阶段,该阶段会自动将应用程序更改部署到我们在本书中一直使用的 todobackend 堆栈。这要求您在源阶段为todobackend-aws存储库添加一个新的源操作,这使得 CloudFormation 堆栈文件和环境配置文件可用作以后的 CloudFormation 部署操作的工件。您还需要为 todobackend 存储库创建一个输出工件,这种情况下,它只是捕获了在构建阶段构建和发布的 Docker 镜像标记,并使其可用于后续阶段。然后,您将此工件作为参数覆盖引用到您的 dev 阶段部署操作中,使用构建操作版本工件中输出的 Docker 镜像标记覆盖ApplicationImageTag参数。

最后,您扩展了管道以支持在生产环境中进行受控发布,这需要创建一个创建变更集操作,该操作创建一个 CloudFormation 变更集,一个手动批准操作,允许某人审查变更集并批准/拒绝它,以及一个部署操作,执行先前生成的变更集。

在下一章中,我们将改变方向,介绍 AWS Fargate 服务,它允许您部署 Docker 应用程序,而无需部署和管理自己的 ECS 集群和 ECS 容器实例。我们将利用这个机会通过使用 Fargate 部署 X-Ray 守护程序来为 AWS X-Ray 服务添加支持,并将通过使用 ECS 服务发现发布守护程序端点。

问题

  1. 您通常在应用程序存储库的根目录中包含哪个文件以支持 AWS CodeBuild?

  2. 真/假:AWS CodeBuild 是一个构建服务,它会启动虚拟机并使用 AWS CodeDeploy 运行构建脚本。

  3. 您需要运行哪些 Docker 配置来支持 Docker 镜像和多容器构建环境的构建?

  4. 您希望在部署 CloudFormation 模板之前审查所做的更改。您将使用 CloudFormation 的哪个功能来实现这一点?

  5. 在使用 CodePipeline CloudFormation 部署操作部署 CloudFormation 堆栈时,必须信任哪个服务以用于指定这些操作的服务角色?

  6. 您设置了一个新的 CodeBuild 项目,其中包括一个发布到弹性容器注册表的构建任务。当您尝试发布图像时,第一次构建失败。您确认目标 ECR 存储库存在,并且您可以手动发布图像到存储库。这个问题的可能原因是什么?

  7. 您为 CodeBuild 创建了一个自定义构建容器,并将其发布到 ECR,并创建了一个允许您的 AWS 账户从 ECR 拉取访问的存储库策略。在执行构建时,您会收到失败的消息,指示 CodeBuild 无法重试自定义镜像。您将如何解决这个问题?

  8. 您创建了一个自定义构建容器,该容器使用 Docker in Docker 来支持 Docker 镜像构建。当构建容器启动并尝试启动 Docker 守护程序时,会出现权限错误。您将如何解决这个问题?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十四章:Fargate 和 ECS 服务发现

到目前为止,在本书中,我们已经花了大量时间专注于构建支持您的 ECS 集群的基础架构,详细介绍了如何为 ECS 容器实例构建自定义的 Amazon 机器映像,以及如何创建可以动态添加或删除 ECS 容器实例到 ECS 集群的 EC2 自动扩展组,还有专门用于管理集群的生命周期和容量的章节。

想象一下不必担心 ECS 集群和 ECS 容器实例。想象一下,有人为您管理它们,以至于您甚至真的不知道它们的存在。对于某些用例,对硬件选择、存储配置、安全姿态和其他基础设施相关问题具有强大的控制能力非常重要;到目前为止,您应该对 ECS 如何提供这些功能有相当深入的了解。然而,在许多情况下,不需要那种控制水平,并且能够利用一个管理您的 ECS 集群补丁、安全配置、容量和其他一切的服务将会带来显著的好处,降低您的运营开销,并使您能够专注于实现您的组织正在努力实现的目标。

好消息是,这是完全可能的,这要归功于一个名为AWS Fargate的服务,该服务于 2017 年 12 月推出。Fargate 是一个完全托管的服务,您只需定义 ECS 任务定义和 ECS 服务,然后让 Fargate 来处理本书中您已经习惯的 ECS 集群和容器实例管理的其余部分。在本章中,您将学习如何使用 AWS Fargate 部署容器应用程序,使用我们在本书中一直在采用的 CloudFormation 的基础设施即代码IaC)方法。为了使本章更加有趣,我们将为名为 X-Ray 的 AWS 服务添加支持,该服务为在 AWS 中运行的应用程序提供分布式跟踪。

当您想要在容器应用程序中使用 X-Ray 时,您需要实现所谓的 X-Ray 守护程序,这是一个从容器应用程序收集跟踪信息并将其发布到 X-Ray 服务的应用程序。我们将扩展 todobackend 应用程序以捕获传入请求的跟踪信息,并通过利用 AWS Fargate 服务向您的 AWS 环境添加 X-Ray 守护程序,该服务将收集跟踪信息并将其转发到 X-Ray 服务。

作为额外的奖励,我们还将实现一个名为 ECS 服务发现的功能,它允许您的容器应用程序自动发布和发现,使用 DNS。这个功能对于 X-Ray 守护程序非常有用,它是一个基于 UDP 的应用程序,不能由各种可用于前端 TCP 和基于 HTTP 的应用程序的负载平衡服务提供服务。ECS 包含对服务发现的内置支持,负责在您的 ECS 任务启动和停止时进行服务注册和注销,使您能够创建其他应用程序可以轻松发现的高可用服务。

本章将涵盖以下主题:

  • 何时使用 Fargate

  • 为应用程序添加对 AWS X-Ray 的支持

  • 创建 X-Ray 守护程序 Docker 镜像

  • 配置 ECS 服务发现资源

  • 为 Fargate 配置 ECS 任务定义

  • 为 Fargate 配置 ECS 服务

  • 部署和测试 X-Ray 守护程序

技术要求

本章的技术要求如下:

  • 对 AWS 帐户的管理员访问权限

  • 本地 AWS 配置文件,根据第三章的说明进行配置

  • AWS CLI 版本 1.15.71 或更高版本

  • Docker 18.06 CE 或更高版本

  • Docker Compose 1.22 或更高版本

  • GNU Make 3.82 或更高版本

  • 本章继续自第十三章,因此需要您成功完成第十三章中定义的所有配置任务

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch14

查看以下视频以查看代码的实际操作:

bit.ly/2Lyd9ft

何时使用 Fargate?

正如本章介绍中所讨论的,AWS Fargate 是一项服务,允许您部署基于容器的应用程序,而无需部署任何 ECS 容器实例、自动扩展组,或者与管理 ECS 集群基础设施相关的任何操作要求。这使得 AWS Fargate 成为一个介于使用 AWS Lambda 运行函数作为服务和使用传统 ECS 集群和 ECS 容器实例运行自己基础设施之间的无服务器技术。

尽管 Fargate 是一项很棒的技术,但重要的是要了解,Fargate 目前还很年轻(至少在撰写本书时是这样),它确实存在一些限制,这些限制可能使其不适用于某些用例,如下所述:

  • 无持久存储:Fargate 目前不支持持久存储,因此如果您的应用程序需要使用持久的 Docker 卷,您应该使用其他服务,例如传统的 ECS 服务。

  • 定价:定价始终可能会有变化;然而,与 ECS 一起获得的常规 EC2 实例定价相比,Fargate 的初始定价被许多人认为是昂贵的。例如,您可以购买的最小 Fargate 配置为 0.25v CPU 和 512 MB 内存,价格为每月 14.25 美元。相比之下,具有 0.5v CPU 和 512 MB 内存的 t2.nano 的价格要低得多,为 4.75 美元(所有价格均基于“us-east-1”地区)。

  • 部署时间:就我个人经验而言,在 Fargate 上运行的 ECS 任务通常需要更长的时间来进行配置和部署,这可能会影响您的应用程序部署所需的时间(这也会影响自动扩展操作)。

  • 安全和控制:使用 Fargate,您无法控制运行容器的底层硬件或实例的任何内容。如果您有严格的安全和/或合规性要求,那么 Fargate 可能无法为您提供满足特定要求的保证或必要的控制。然而,重要的是要注意,AWS 将 Fargate 列为符合 HIPAA 和 PCI Level 1 DSS 标准。

  • 网络隔离:在撰写本书时,Fargate 不支持 ECS 代理和 CloudWatch 日志通信使用 HTTP 代理。这要求您将 Fargate 任务放置在具有互联网连接性的公共子网中,或者放置在具有 NAT 网关的私有子网中,类似于您在“隔离网络访问”章节中学到的方法。为了允许访问公共 AWS API 端点,这确实要求您打开出站网络访问,这可能违反您组织的安全要求。

  • 服务可用性:在撰写本书时,Fargate 仅在美国东部(弗吉尼亚州)、美国东部(俄亥俄州)、美国西部(俄勒冈州)和欧盟(爱尔兰)地区可用;但是,我希望 Fargate 能够在大多数地区迅速广泛地可用。

如果您可以接受 Fargate 当前的限制,那么 Fargate 将显著减少您的运营开销,并使您的生活更加简单。例如,在自动扩展方面,您可以简单地使用我们在“ECS 自动扩展”章节末尾讨论的应用自动扩展方法来自动扩展您的 ECS 服务,Fargate 将负责确保有足够的集群容量。同样,您无需担心 ECS 集群的打补丁和生命周期管理 - Fargate 会为您处理上述所有事项。

在本章中,我们将部署一个 AWS X-Ray 守护程序服务,以支持 todobackend 应用程序的应用程序跟踪。鉴于这种类型的服务是 Fargate 非常适合的,因为它是一个不需要持久存储、不会影响 todobackend 应用程序的可用性(如果它宕机),也不会处理最终用户数据的后台服务。

为应用程序添加对 AWS X-Ray 的支持

在我们可以使用 AWS X-Ray 服务之前,您的应用程序需要支持收集和发布跟踪信息到 X-Ray 服务。X-Ray 软件开发工具包(SDK)包括对各种编程语言和流行的应用程序框架的支持,包括 Python 和 Django,它们都是 todobackend 应用程序的动力源。

您可以在aws.amazon.com/documentation/xray/找到适合您选择的语言的适当 SDK 文档,但对于我们的用例,docs.aws.amazon.com/xray-sdk-for-python/latest/reference/frameworks.html提供了有关如何配置 Django 以自动为应用程序的每个传入请求创建跟踪的相关信息。

在 todobackend 存储库中,您首先需要将 X-Ray SDK 包添加到src/requirements.txt文件中,这将确保 SDK 与 todobackend 应用程序的其他依赖项一起安装:

Django==2.0
django-cors-headers==2.1.0
djangorestframework==3.7.3
mysql-connector-python==8.0.11
pytz==2017.3
uwsgi==2.0.17
aws-xray-sdk

接下来,您需要将 Django X-Ray 中间件组件(包含在 SDK 中)添加到位于src/todobackend/settings_release.py中的 Django 项目的MIDDLEWARE配置元素中:

from .settings import *
...
...
STATIC_ROOT = os.environ.get('STATIC_ROOT', '/public/static')
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', '/public/media')

MIDDLEWARE.insert(0,'aws_xray_sdk.ext.django.middleware.XRayMiddleware')

这种配置与Django 的 X 射线文档有所不同,但通常情况下,您只想在 AWS 环境中运行 X-Ray,并且使用标准方法可能会导致本地开发环境中的 X-Ray 配置问题。因为我们有一个单独的发布设置文件,导入基本设置文件,我们可以简单地使用insert()函数将 X-Ray 中间件组件插入到基本的MIDDLEWARE列表的开头,如所示。这种方法确保我们将在使用发布设置的 AWS 环境中运行 X-Ray,但不会在本地开发环境中使用 X-Ray。

重要的是要在MIDDLEWARE列表中首先指定 X-Ray 中间件组件,因为这样可以确保 X-Ray 可以在任何其他中间件组件之前开始跟踪传入请求。

最后,Python X-Ray SDK 包括对许多流行软件包的跟踪支持,包括mysql-connector-python软件包,该软件包被 todobackend 应用程序用于连接其 MySQL 数据库。在 Python 中,X-Ray 使用一种称为 patching 的技术来包装受支持软件包的调用,这允许 X-Ray 拦截软件包发出的调用并捕获跟踪信息。对于我们的用例,对mysql-connector-python软件包进行 patching 将使我们能够跟踪应用程序发出的数据库调用,这对于解决性能问题非常有用。要对此软件包进行 patching,您需要向应用程序入口点添加几行代码,对于 Django 来说,该入口点位于文件src/todobackend.wsgi.py中:

"""
WSGI config for todobackend project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todobackend.settings")

from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all

# Required to avoid SegmentNameMissingException errors
xray_recorder.configure(service="todobackend")

patch_all()

application = get_wsgi_application()

xray_recorder配置将向每个跟踪段添加服务名称,否则您将观察到 SegmentNameMissingException 错误。在这一点上,您已经在应用程序级别上添加了支持以开始跟踪传入请求,并且在提交并将更改推送到 GitHub 之前,您应该能够成功运行“make workflow”(运行make testmake release)。因为您现在已经建立了一个持续交付管道,这将触发该管道,该管道确保一旦管道构建阶段完成,您的应用程序更改将被发布到 ECR。如果您尚未完成上一章,或者已删除管道,则需要在运行make testmake release后使用make loginmake publish命令手动发布新镜像。

创建 X-Ray 守护程序 Docker 镜像

在我们的应用程序可以发布 X-Ray 跟踪信息之前,您必须部署一个 X-Ray 守护程序,以便您的应用程序可以将此信息发送到它。我们的目标是使用 AWS Fargate 运行 X-Ray 守护程序,但在此之前,我们需要创建一个将运行守护程序的 Docker 镜像。AWS 提供了如何构建 X-Ray 守护程序镜像的示例,我们将按照 AWS 文档中记录的类似方法创建一个名为Dockerfile.xray的文件,该文件位于todobackend-aws存储库的根目录中:

FROM amazonlinux
RUN yum install -y unzip
RUN curl -o daemon.zip https://s3.dualstack.us-east-2.amazonaws.com/aws-xray-assets.us-east-2/xray-daemon/aws-xray-daemon-linux-2.x.zip
RUN unzip daemon.zip && cp xray /usr/bin/xray

ENTRYPOINT ["/usr/bin/xray", "-b", "0.0.0.0:2000"]
EXPOSE 2000/udp

您现在可以使用docker build命令在本地构建此镜像,如下所示:

> docker build -t xray -f Dockerfile.xray .
Sending build context to Docker daemon 474.1kB
Step 1/6 : FROM amazonlinux
 ---> 81bb3e78db3d
Step 2/6 : RUN yum install -y unzip
 ---> Running in 35aca63a625e
Loaded plugins: ovl, priorities
Resolving Dependencies
...
...
Step 6/6 : EXPOSE 2000/udp
 ---> Running in 042542d22644
Removing intermediate container 042542d22644
 ---> 63b422e40099
Successfully built 63b422e40099
Successfully tagged xray:latest

现在我们的镜像已构建,我们需要将其发布到 ECR。在此之前,您需要为 X-Ray 镜像创建一个新的存储库,然后将其添加到todobackend-aws存储库的根目录中的现有ecr.yml文件中:

AWSTemplateFormatVersion: "2010-09-09"

Description: ECR Resources

Resources:
  XrayRepository:
 Type: AWS::ECR::Repository
 Properties:
 RepositoryName: docker-in-aws/xray
  CodebuildRepository:
    Type: AWS::ECR::Repository
  ...
  ...

在前面的示例中,您使用名称docker-in-aws/xray创建了一个新的存储库,这将导致一个完全合格的存储库名称为<account-id>.dkr.ecr.<region>.amazonaws.com/docker-in-aws/xray(例如,385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/xray)。

您现在可以通过运行aws cloudformation deploy命令来创建新的存储库:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file ecr.yml --stack-name ecr-repositories
Enter MFA code for arn:aws:iam::385605022855:mfa/justin.menga:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - ecr-repositories
  ...
  ...

部署完成后,您可以登录到 ECR,然后使用新的 ECR 存储库的完全合格名称对之前创建的图像进行标记和发布。

> eval $(aws ecr get-login --no-include-email)
Login Succeeded
> docker tag xray 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/xray
> docker push 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/xray
The push refers to repository [385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/xray]
c44926e8470e: Pushed
1c9da599a308: Pushed
9d486dac1b0b: Pushed
0c1715974ca1: Pushed
latest: digest: sha256:01d9b6982ce3443009c7f07babb89b134c9d32ea6f1fc380cb89ce5639c33938 size: 1163

配置 ECS 服务发现资源

ECS 服务发现是一个功能,允许您的客户端应用程序在动态环境中发现 ECS 服务,其中基于容器的端点来来去去。到目前为止,我们已经使用 AWS 应用程序负载均衡器来执行此功能,您可以配置一个稳定的服务端点,您的应用程序可以连接到该端点,然后在 ECS 管理的目标组中进行负载平衡,该目标组包括与 ECS 服务相关联的每个 ECS 任务。尽管这通常是我推荐的最佳实践方法,但对于不支持负载均衡器的应用程序(例如,基于 UDP 的应用程序),或者对于非常庞大的微服务架构,在这种架构中,与给定的 ECS 任务直接通信更有效,ECS 服务发现可能比使用负载均衡器更好。

ECS 服务发现还支持 AWS 负载均衡器,如果负载均衡器与给定的 ECS 服务相关联,ECS 将发布负载均衡器侦听器的 IP 地址。

ECS 服务发现使用 DNS 作为其发现机制,这是有用的,因为在其最基本的形式中,DNS 被任何应用客户端普遍支持。您的 ECS 服务注册的 DNS 命名空间被称为服务发现命名空间,它简单地对应于 Route 53 DNS 域或区域,您在命名空间中注册的每个服务被称为服务发现。例如,您可以将services.dockerinaws.org配置为服务发现命名空间,如果您有一个名为todobackend的 ECS 服务,那么您将使用 DNS 名称todobackend.services.dockerinaws.org连接到该服务。ECS 将自动管理针对您的服务的 DNS 记录注册的地址(A)记录,动态添加与您的 ECS 服务的每个活动和健康的 ECS 任务关联的 IP 地址,并删除任何退出或变得不健康的 ECS 任务。ECS 服务发现支持公共和私有命名空间,对于我们运行 X-Ray 守护程序的示例,私有命名空间是合适的,因为此服务只需要支持来自 todobackend 应用程序的内部应用程序跟踪通信。

ECS 服务发现支持 DNS 服务(SRV)记录的配置,其中包括有关给定服务端点的 IP 地址和 TCP/UDP 端口信息。当使用静态端口映射或awsvpc网络模式(例如 Fargate)时,通常使用地址(A)记录,当使用动态端口映射时使用 SRV 记录,因为 SRV 记录可以包括为创建的端口映射提供动态端口信息。请注意,应用程序对 SRV 记录的支持有些有限,因此我通常建议在 ECS 服务发现中使用经过验证的A记录的方法。

配置服务发现命名空间

与大多数 AWS 资源一样,您可以使用 AWS 控制台、AWS CLI、各种 AWS SDK 之一或 CloudFormation 来配置服务发现资源。鉴于本书始终采用基础设施即代码的方法,我们自然会在本章中采用 CloudFormation;因为 X-Ray 守护程序是一个新服务(通常被视为每个应用程序发布跟踪信息的共享服务),我们将在名为xray.yml的文件中创建一个新的堆栈,放在todobackend-aws存储库的根目录。

以下示例演示了创建初始模板和创建服务发现命名空间资源:

AWSTemplateFormatVersion: "2010-09-09"

Description: X-Ray Daemon

Resources:
  ApplicationServiceDiscoveryNamespace:
    Type: AWS::ServiceDiscovery::PrivateDnsNamespace
    Properties:
      Name: services.dockerinaws.org.
      Description: services.dockerinaws.org namespace
      Vpc: vpc-f8233a80

在前面的示例中,我们创建了一个私有服务发现命名空间,它只需要命名空间的 DNS 名称、可选描述和关联的私有 Route 53 区域的 VPC ID。为了保持简单,我还硬编码了与我的 AWS 账户相关的 VPC ID 的适当值,通常您会通过堆栈参数注入这个值。

鉴于服务发现命名空间的意图是支持多个服务,您通常会在单独的 CloudFormation 堆栈中创建命名空间,比如创建共享网络资源的专用网络堆栈。然而,为了保持简单,我们将在 X-Ray 堆栈中创建命名空间。

现在,您可以使用aws cloudformation deploy命令将初始堆栈部署到 CloudFormation,这应该会创建一个服务发现命名空间和相关的 Route 53 私有区域。

> aws cloudformation deploy --template-file xray.yml --stack-name xray-daemon
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - xray-daemon
> aws servicediscovery list-namespaces
{
    "Namespaces": [
        {
            "Id": "ns-lgd774j6s2cmxwq3",
            "Arn": "arn:aws:servicediscovery:us-east-1:385605022855:namespace/ns-lgd774j6s2cmxwq3",
            "Name": "services.dockerinaws.org",
            "Type": "DNS_PRIVATE"
        }
    ]
}
> aws route53 list-hosted-zones --query HostedZones[].Name --output table
-------------------------------
| ListHostedZones             |
+-----------------------------+
| services.dockerinaws.org.   |
+-----------------------------+

在前面的示例中,一旦您的堆栈成功部署,您将使用aws servicediscovery list-namespaces命令来验证是否创建了一个私有命名空间,而aws route53 list-hosted-zones命令将显示已创建一个 Route 53 区域,其区域名称为services.dockerinaws.org

配置服务发现服务

现在您已经有了一个服务发现命名空间,下一步是创建一个服务发现服务,它与每个 ECS 服务都有一对一的关系,这意味着您需要创建一个代表稍后在本章中创建的 X-Ray ECS 服务的服务发现服务。

AWSTemplateFormatVersion: "2010-09-09"

Description: X-Ray Daemon

Resources:
  ApplicationServiceDiscoveryService:
 Type: AWS::ServiceDiscovery::Service
 Properties:
 Name: xray
 Description: xray service 
 DnsConfig: 
 NamespaceId: !Ref ApplicationServiceDiscoveryNamespace
 DnsRecords:
 - Type: A
 TTL: 60
 HealthCheckCustomConfig:
 FailureThreshold: 1
  ApplicationServiceDiscoveryNamespace:
    Type: AWS::ServiceDiscovery::PrivateDnsNamespace
    Properties:
      Name: services.dockerinaws.org.
      Description: services.dockerinaws.org namespace
      Vpc: vpc-f8233a80

在前面的示例中,您添加了一个名为ApplicationServiceDiscoveryService的新资源,并配置了以下属性:

  • Name:定义服务的名称。此名称将用于在关联的命名空间中注册服务。

  • DnsConfig:指定服务关联的命名空间(由NamespaceId属性定义),并定义应创建的 DNS 记录类型和生存时间(TTL)。在这里,您指定了一个地址记录(类型为A)和一个 60 秒的 TTL,这意味着客户端只会缓存该记录最多 60 秒。通常情况下,您应该将 TTL 设置为较低的值,以确保您的客户端在新的 ECS 任务注册到服务或现有的 ECS 任务从服务中移除时能够获取 DNS 更改。

  • HealthCheckCustomConfig:这配置 ECS 来管理确定是否可以注册 ECS 任务的健康检查。您还可以配置 Route 53 健康检查(参见docs.aws.amazon.com/AmazonECS/latest/developerguide/service-discovery.html#service-discovery-concepts);然而,对于我们的用例来说,鉴于 X-Ray 是基于 UDP 的应用程序,而 Route 53 健康检查仅支持基于 TCP 的服务,您必须使用前面示例中显示的HealthCheckCustomConfig配置。FailureThreshold指定服务发现在接收到自定义健康检查更新后等待更改给定服务实例的健康状态的30秒间隔数。

您现在可以使用aws cloudformation deploy命令将更新后的堆栈部署到 CloudFormation,这应该会创建一个服务发现服务。

> aws cloudformation deploy --template-file xray.yml --stack-name xray-daemon
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - xray-daemon
> aws servicediscovery list-services
{
    "Services": [
        {
            "Id": "srv-wkdxwh4pzo7ea7w3",
            "Arn": "arn:aws:servicediscovery:us-east-1:385605022855:service/srv-wkdxwh4pzo7ea7w3",
            "Name": "xray",
            "Description": "xray service"
        }
    ]
}

这将为xray.services.dockerinaws.org创建一个 DNS 记录集,直到我们在本章后面将要创建的 X-Ray ECS 服务的 ECS 服务发现支持配置之前,它将不会有任何地址(A)记录与之关联。

为 Fargate 配置 ECS 任务定义

您现在可以开始定义您的 ECS 资源,您将配置为使用 AWS Fargate 服务,并利用您在上一节中创建的服务发现资源。

在配置 ECS 任务定义以支持 Fargate 时,有一些关键考虑因素需要您了解:

  • 启动类型:ECS 任务定义包括一个名为RequiresCompatibilities的参数,该参数定义了定义的兼容启动类型。当前的启动类型包括 EC2,指的是在传统 ECS 集群上启动的 ECS 任务,以及 FARGATE,指的是在 Fargate 上启动的 ECS 任务。默认情况下,RequiresCompatibilities参数配置为 EC2,这意味着如果要使用 Fargate,必须显式配置此参数。

  • 网络模式:Fargate 仅支持awsvpc网络模式,我们在第十章“隔离网络访问”中讨论过。

  • 执行角色:Fargate 要求您配置一个执行角色,这是分配给管理 ECS 任务生命周期的 ECS 代理和 Fargate 运行时的 IAM 角色。这是一个独立的角色,不同于您在第九章“管理机密”中配置的任务 IAM 角色功能,该功能用于向在 ECS 任务中运行的应用程序授予 IAM 权限。执行角色通常配置为具有类似权限的权限,这些权限您将为与传统 ECS 容器实例关联的 EC2 IAM 实例角色配置,至少授予 ECS 代理和 Fargate 运行时从 ECR 拉取图像和将日志写入 CloudWatch 日志的权限。

  • CPU 和内存:Fargate 要求您在任务定义级别定义 CPU 和内存要求,因为这决定了基于您的任务定义运行的 ECS 任务的基础目标实例。请注意,这与您在第八章“使用 ECS 部署应用程序”中为 todobackend 应用程序的 ECS 任务定义配置的每个容器定义 CPU 和内存设置是分开的;您仍然可以配置每个容器定义的 CPU 和内存设置,但需要确保分配给容器定义的总 CPU/内存不超过分配给 ECS 任务定义的总 CPU/内存。Fargate 目前仅支持有限的 CPU/内存分配,您可以在docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html的“任务 CPU 和内存”部分了解更多信息。

  • 日志记录:截至撰写本文时,Fargate 仅支持awslogs日志记录驱动程序,该驱动程序将您的容器日志转发到 CloudWatch 日志。

考虑到上述情况,现在让我们为我们的 X-Ray 守护程序服务定义一个任务定义:

...
...
Resources:
  ApplicationTaskDefinition:
 Type: AWS::ECS::TaskDefinition
 Properties:
 Family: !Sub ${AWS::StackName}-task-definition
 NetworkMode: awsvpc
 ExecutionRoleArn: !Sub ${ApplicationTaskExecutionRole.Arn}
 TaskRoleArn: !Sub ${ApplicationTaskRole.Arn}
 Cpu: 256
 Memory: 512
 RequiresCompatibilities:
 - FARGATE
 ContainerDefinitions:
 - Name: xray
 Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/xray
 Command:
 - -o
 LogConfiguration:
 LogDriver: awslogs
 Options:
 awslogs-group: !Sub /${AWS::StackName}/ecs/xray
 awslogs-region: !Ref AWS::Region
 awslogs-stream-prefix: docker
 PortMappings:
 - ContainerPort: 2000
 Protocol: udp
 Environment:
 - Name: AWS_REGION
 Value: !Ref AWS::Region
  ApplicationLogGroup:
 Type: AWS::Logs::LogGroup
 DeletionPolicy: Delete
 Properties:
 LogGroupName: !Sub /${AWS::StackName}/ecs/xray
 RetentionInDays: 7
  ApplicationServiceDiscoveryService:
    Type: AWS::ServiceDiscovery::Service
  ...
  ...

在上面的示例中,请注意RequiresCompatibilities参数指定FARGATE作为支持的启动类型,并且NetworkMode参数配置为所需的awsvpc模式。CpuMemory设置分别配置为 256 CPU 单位(0.25 vCPU)和 512 MB,这代表了最小可用的 Fargate CPU/内存配置。对于ExecutionRoleArn参数,您引用了一个名为ApplicationTaskExecutionRole的 IAM 角色,我们将很快单独配置,与为TaskRoleArn参数配置的角色分开。

接下来,您定义一个名为xray的单个容器定义,该容器定义引用了您在本章前面创建的 ECR 存储库;请注意,您为Command参数指定了-o标志。这将在您在上一个示例中配置的 X-Ray 守护程序镜像的ENTRYPOINT指令中附加-o,从而阻止 X-Ray 守护程序尝试查询 EC2 实例元数据,因为在使用 Fargate 时不支持这一操作。

容器定义的日志配置配置为使用awslogs驱动程序,这是 Fargate 所需的,它引用了任务定义下配置的ApplicationLogGroup CloudWatch 日志组资源。最后,您指定了 X-Ray 守护程序端口(UDP 端口 2000)作为容器端口映射,并配置了一个名为AWS_REGION的环境变量,该变量引用您部署堆栈的区域,这对于 X-Ray 守护程序确定守护程序应将跟踪数据发布到的区域性 X-Ray 服务端点是必需的。

为 Fargate 配置 IAM 角色

在上一个示例中,您的 ECS 任务定义引用了一个任务执行角色(由ExecutionRoleArn参数定义)和一个任务角色(由TaskRoleArn参数定义)。

如前所述,任务执行角色定义了将分配给 ECS 代理和 Fargate 运行时的 IAM 权限,通常包括拉取任务定义中定义的容器所需的 ECR 镜像的权限,以及写入容器日志配置中引用的 CloudWatch 日志组的权限:

...
...
Resources:
  ApplicationTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: EcsTaskExecutionRole
          PolicyDocument:
            Statement:
              - Sid: EcrPermissions
                Effect: Allow
                Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:BatchGetImage
                  - ecr:GetDownloadUrlForLayer
                  - ecr:GetAuthorizationToken
                Resource: "*"
              - Sid: CloudwatchLogsPermissions
                Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub ${ApplicationLogGroup.Arn}
  ApplicationTaskDefinition:
    Type: AWS::ECS::TaskDefinition
  ...
  ...

任务角色定义了从您的 ECS 任务定义中运行的应用程序可能需要的任何 IAM 权限。对于我们的用例,X-Ray 守护程序需要权限将跟踪发布到 X-Ray 服务,如下例所示:

Resources:
 ApplicationTaskRole:
 Type: AWS::IAM::Role
 Properties:
 AssumeRolePolicyDocument:
 Version: "2012-10-17"
 Statement:
 - Effect: Allow
 Principal:
 Service:
 - ecs-tasks.amazonaws.com
 Action:
 - sts:AssumeRole
 Policies:
 - PolicyName: EcsTaskRole
 PolicyDocument:
 Statement:
 - Effect: Allow
 Action:
 - xray:PutTraceSegments
 - xray:PutTelemetryRecords
 Resource: "*"    ApplicationTaskExecutionRole:
    Type: AWS::IAM::Role
  ...
  ...

在前面的例子中,您授予xray:PutTraceSegmentsxray:PutTelemetryRecords权限给 X-Ray 守护程序,这允许守护程序将从您的应用程序捕获的应用程序跟踪发布到 X-Ray 服务。请注意,对于ApplicationTaskExecutionRoleApplicationTaskRole资源,AssumeRolePolicyDocument部分中的受信任实体必须配置为ecs-tasks.amazonaws.com服务。

为 Fargate 配置 ECS 服务

现在您已经为 Fargate 定义了一个 ECS 任务定义,您可以创建一个 ECS 服务,该服务将引用您的 ECS 任务定义,并为您的服务部署一个或多个实例(ECS 任务)。

正如您可能期望的那样,在配置 ECS 服务以支持 Fargate 时,有一些关键考虑因素需要您注意:

  • 启动类型:您必须指定 Fargate 作为任何要使用 Fargate 运行的 ECS 服务的启动类型。

  • 平台版本:AWS 维护不同版本的 Fargate 运行时或平台,这些版本会随着时间的推移而发展,并且可能在某个时候为您的 ECS 服务引入破坏性更改。您可以选择为您的 ECS 服务针对特定的平台版本,或者简单地省略配置此属性,以使用最新可用的平台版本。

  • 网络配置:因为 Fargate 需要使用awsvpc网络模式,您的 ECS 服务必须定义一个网络配置,定义您的 ECS 服务将在其中运行的子网,分配给您的 ECS 服务的安全组,以及您的服务是否分配了公共 IP 地址。在撰写本书时,当使用 Fargate 时,您必须分配公共 IP 地址或使用 NAT 网关,如章节隔离网络访问中所讨论的,以确保管理您的 ECS 服务的 ECS 代理能够与 ECS 通信,从 ECR 拉取镜像,并将日志发布到 CloudWatch 日志服务。

尽管您无法与 ECS 代理进行交互,但重要的是要理解所有 ECS 代理通信都使用与在 Fargate 中运行的容器应用程序相同的网络接口。这意味着您必须考虑 ECS 代理和 Fargate 运行时的通信需求,当附加安全组并确定您的 ECS 服务的网络放置时。

以下示例演示了为 Fargate 和 ECS 服务发现配置 ECS 服务:

...
...
Resources:
 ApplicationCluster:
 Type: AWS::ECS::Cluster
 Properties:
 ClusterName: !Sub ${AWS::StackName}-cluster
 ApplicationService:
 Type: AWS::ECS::Service
 DependsOn:
 - ApplicationLogGroup
 Properties:
 ServiceName: !Sub ${AWS::StackName}-application-service
 Cluster: !Ref ApplicationCluster
 TaskDefinition: !Ref ApplicationTaskDefinition
 DesiredCount: 2
 LaunchType: FARGATE
 NetworkConfiguration:
 AwsvpcConfiguration:
 AssignPublicIp: ENABLED
 SecurityGroups:
 - !Ref ApplicationSecurityGroup
 Subnets:
 - subnet-a5d3ecee
 - subnet-324e246f
 DeploymentConfiguration:
 MinimumHealthyPercent: 100
 MaximumPercent: 200
 ServiceRegistries:
 - RegistryArn: !Sub ${ApplicationServiceDiscoveryService.Arn}
  ApplicationTaskRole:
    Type: AWS::IAM::Role
  ...
  ...

在前面的示例中,首先要注意的是,尽管在使用 Fargate 时您不运行任何 ECS 容器实例或其他基础设施,但在为 Fargate 配置 ECS 服务时仍需要定义一个 ECS 集群,然后在您的 ECS 服务中引用它。

ECS 服务配置类似于在隔离网络访问章节中使用 ECS 任务网络运行 todobackend 应用程序时定义的配置,尽管有一些关键的配置属性需要讨论:

  • LaunchType:必须指定为FARGATE。确保将您的 ECS 服务放置在公共子网中,并在网络配置中将AssignPublicIp属性配置为ENABLED,或者将您的服务放置在带有 NAT 网关的私有子网中非常重要。在前面的示例中,请注意我已经将Subnets属性硬编码为我的 VPC 中的公共子网;您需要将这些值更改为您的环境的适当值,并且通常会通过堆栈参数注入这些值。

  • ServiceRegistries:此属性配置您的 ECS 服务以使用我们在本章前面配置的 ECS 服务发现功能,在这里,您引用了您在上一个示例中配置的服务发现服务的 ARN。有了这个配置,ECS 将自动在为链接的服务发现服务创建的 DNS 记录集中注册/注销每个 ECS 服务实例(ECS 任务)的 IP 地址。

在这一点上,还有一个最终需要配置的资源——您需要定义被您的 ECS 服务引用的ApplicationSecurityGroup资源:

...
...
Resources:
  ApplicationSecurityGroup:
 Type: AWS::EC2::SecurityGroup
 Properties:
 VpcId: vpc-f8233a80
 GroupDescription: !Sub ${AWS::StackName} Application Security Group
 SecurityGroupIngress:
 - IpProtocol: udp
 FromPort: 2000
 ToPort: 2000
 CidrIp: 172.31.0.0/16
 SecurityGroupEgress:
 - IpProtocol: tcp
 FromPort: 80
 ToPort: 80
 CidrIp: 0.0.0.0/0
 - IpProtocol: tcp
 FromPort: 443
 ToPort: 443
 CidrIp: 0.0.0.0/0
 - IpProtocol: udp
 FromPort: 53
 ToPort: 53
 CidrIp: 0.0.0.0/0
 Tags:
 - Key: Name
 Value: !Sub ${AWS::StackName}-ApplicationSecurityGroup
  ApplicationCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${AWS::StackName}-cluster
  ApplicationService:
    Type: AWS::ECS::Service
  ...
  ...

在上面的示例中,再次注意,我在这里使用了硬编码的值,而我通常会使用堆栈参数,以保持简单和简洁。安全组允许从 VPC 内的任何主机对 UDP 端口 2000 进行入口访问,而出口安全规则允许访问 DNS、HTTP 和 HTTPS,这是为了确保 ECS 代理可以与 ECS、ECR 和 CloudWatch 日志进行通信,以及 X-Ray 守护程序可以与 X-Ray 服务进行通信。

部署和测试 X-Ray 守护程序

此时,我们已经完成了配置 CloudFormation 模板的工作,该模板将使用启用了 ECS 服务发现的 Fargate 服务将 X-Ray 守护程序部署到 AWS;您可以使用aws cloudformation deploy命令将更改部署到您的堆栈中,包括--capabilities参数,因为我们的堆栈现在正在创建 IAM 资源:

> aws cloudformation deploy --template-file xray.yml --stack-name xray-daemon \
 --capabilities CAPABILITY_NAMED_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - xray-daemon

一旦部署完成,如果您在 AWS 控制台中打开 ECS 仪表板并选择集群,您应该会看到一个名为 xray-daemon-cluster 的新集群,其中包含一个单一服务和两个正在运行的任务,在 FARGATE 部分:

X-Ray 守护程序集群

如果您选择集群并单击xray-daemon-application-service,您应该在“详细信息”选项卡中看到 ECS 服务发现配置已经就位:

X-Ray 守护程序服务详细信息

在服务发现命名空间中,您现在应该找到附加到xray.services.dockerinaws.org记录集的两个地址记录,您可以通过导航到 Route 53 仪表板,从左侧菜单中选择托管区域,并选择services.dockerinaws.org区域来查看:

服务发现 DNS 记录

请注意,这里有两个A记录,每个支持我们的 ECS 服务的 ECS 任务一个。如果您停止其中一个 ECS 任务,ECS 将自动从 DNS 中删除该记录,然后在 ECS 将 ECS 服务计数恢复到所需计数并启动替换的 ECS 任务后,添加一个新的A记录。这确保了您的服务具有高可用性,并且依赖于您的服务的应用程序可以动态解析适当的服务实例。

为 X-Ray 支持配置 todobackend 堆栈

有了我们的 X 射线守护程序服务,我们现在可以为todobackend-aws堆栈添加对 X 射线的支持。在本章的开头,您配置了 todobackend 应用程序对 X 射线的支持,如果您提交并推送了更改,您在上一章中创建的持续交付流水线应该已经将更新的 Docker 镜像发布到了 ECR(如果不是这种情况,请在 todobackend 存储库中运行make publish命令)。您需要执行的唯一其他配置是更新附加到 todobackend 集群实例的安全规则,以允许 X 射线通信,并确保 Docker 环境配置了适当的环境变量,以启用正确的 X 射线操作。

以下示例演示了在todobackend-aws堆栈中的ApplicationAutoscalingSecurityGroup资源中添加安全规则,该规则允许与 X 射线守护程序进行通信:

...
...
Resources:
  ...
  ...
  ApplicationAutoscalingSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub ${AWS::StackName} Application Autoscaling Security Group
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
 - IpProtocol: udp
 FromPort: 2000
 ToPort: 2000
 CidrIp: 172.31.0.0/16
        - IpProtocol: udp
          FromPort: 53
          ToPort: 53
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
...
...

以下示例演示了为ApplicationTaskDefinition资源中的 todobackend 容器定义配置环境设置:

...
...
Resources:
  ...
  ...
  ApplicationAutoscalingSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
    ...
    ...
      ContainerDefinitions:
        - Name: todobackend
          Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/docker-in-aws/todobackend:${ApplicationImageTag}
          MemoryReservation: 395
          Cpu: 245
          MountPoints:
            - SourceVolume: public
              ContainerPath: /public
          Environment:
            - Name: DJANGO_SETTINGS_MODULE
              Value: todobackend.settings_release
            - Name: MYSQL_HOST
              Value: !Sub ${ApplicationDatabase.Endpoint.Address}
            - Name: MYSQL_USER
              Value: todobackend
            - Name: MYSQL_DATABASE
              Value: todobackend
            - Name: SECRETS
              Value: todobackend/credentials
            - Name: AWS_DEFAULT_REGION
              Value: !Ref AWS::Region
            - Name: AWS_XRAY_DAEMON_ADDRESS
 Value: xray.services.dockerinaws.org:2000
...
...

在前面的示例中,您添加了一个名为AWS_XRAY_DAEMON_ADDRESS的变量,该变量引用了我们的 X 射线守护程序服务的xray.services.dockerinaws.org服务端点,并且必须以<hostname>:<port>的格式表示。

您可以通过设置AWS_XRAY_TRACE_NAME环境变量来覆盖 X 射线跟踪中使用的服务名称。在我们的场景中,我们在同一帐户中有 todobackend 应用程序的开发和生产实例,并希望确保每个应用程序环境都有自己的跟踪集。

如果您现在提交并推送所有更改到todobackend-aws存储库,则上一章的持续交付流水线应该会检测到更改并自动部署您的更新堆栈,或者您可以通过命令行运行make deploy/dev命令来部署更改。

测试 X 射线服务

在成功部署更改后,浏览到您环境的 todobackend URL,并与应用程序进行一些交互,例如添加一个todo项目。

如果您接下来从 AWS 控制台打开 X 射线仪表板(服务|开发人员工具|X 射线)并从左侧菜单中选择服务地图,您应该会看到一个非常简单的地图,其中包括 todobackend 应用程序。

X-Ray 服务地图

在上述截图中,我点击了 todobackend 服务,显示了右侧的服务详情窗格,显示了响应时间分布和响应状态响应等信息。另外,请注意,服务地图包括 todobackend RDS 实例,因为我们在本章的前一个示例中配置了应用程序以修补mysql-connector-python库。

如果您点击“查看跟踪”按钮,将显示该服务的跟踪;请注意,Django 的 X-Ray 中间件包括 URL 信息,允许根据 URL 对跟踪进行分组:

X-Ray 跟踪

在上述截图中,请注意 85%的跟踪都命中了一个 IP 地址 URL,这对应于正在进行的应用程序负载均衡器健康检查。如果您点击跟踪列表中的“年龄”列,以从最新到最旧对跟踪进行排序,您应该能够看到您对 todobackend 应用程序所做的请求,对我来说,是一个创建新的todo项目的POST请求。

您可以通过点击 ID 链接查看以下截图中POST跟踪的更多细节:

X-Ray 跟踪详情

在上述截图中,您可以看到响应总共花费了 218 毫秒,并且进行了两次数据库调用,每次调用都少于 2 毫秒。如果您正在使用 X-Ray SDK 支持的其他库,您还可以看到这些库所做调用的跟踪信息;例如,通过 boto3 库进行的任何 AWS 服务调用,比如将文件复制到 S3 或将消息发布到 Kinesis 流,也会被捕获。显然,这种信息在排除应用程序性能问题时非常有用。

摘要

在本章中,您学习了如何使用 AWS Fargate 服务部署 Docker 应用程序。为了使事情更有趣,您还学习了如何利用 ECS 服务发现自动发布应用程序端点的服务可达性信息,这是传统方法的替代方案,传统方法是将应用程序端点发布在负载均衡器后面。最后,为了结束这一定会让您觉得有趣和有趣的章节,您为 todobackend 应用程序添加了对 AWS X-Ray 服务的支持,并部署了一个 X-Ray 守护程序服务,使用 Fargate 来捕获应用程序跟踪。

首先,您学习了如何为 Python Django 应用程序添加对 X-Ray 的支持,这只需要您添加一个拦截传入请求的 X-Ray 中间件组件,并且还需要修补支持包,例如 mysql-connector-python 和 boto3 库,这允许您捕获 MySQL 数据库调用和应用程序可能进行的任何 AWS 服务调用。然后,您为 X-Ray 守护程序创建了一个 Docker 镜像,并将其发布到弹性容器注册表,以便在您的 AWS 环境中部署。

然后,您学习了如何配置 ECS 服务发现所需的支持元素,添加了一个服务发现命名空间,创建了一个公共或私有 DNS 区域,其中维护了服务发现服务端点,然后为 X-Ray 守护程序创建了一个服务发现服务,允许您的 todobackend 应用程序(以及其他应用程序)通过逻辑 DNS 名称发现所有活动和健康的 X-Ray 守护程序实例。

有了这些组件,您继续创建了一个使用 Fargate 的 X-Ray 守护程序服务,创建了一个 ECS 任务定义和一个 ECS 服务。ECS 任务定义对支持 Fargate 有一些特定要求,包括定义一个单独的任务执行角色,该角色授予基础 ECS 代理和 Fargate 运行时的特权,指定 Fargate 作为支持的启动类型,并确保配置了 awsvpc 网络模式。您创建的 ECS 服务要求您配置网络配置以支持 ECS 任务定义的 awsvpc 网络模式。您还通过引用本章早些时候创建的服务发现服务,为 ECS 服务添加了对 ECS 服务发现的支持。

最后,您在 todobackend 堆栈中配置了现有的 ECS 任务定义,以将服务发现服务名称指定为AWS_XRAY_DAEMON_ADDRESS变量;在部署更改后,您学会了如何使用 X-Ray 跟踪来分析传入请求到您的应用程序的性能,并能够对 todobackend 应用程序数据库的个别调用进行分析。

在下一章中,您将了解另一个支持 Docker 应用程序的 AWS 服务,称为 Elastic Beanstalk。它提供了一种平台即服务(PaaS)的方法,用于在 AWS 中部署和运行基于容器的应用程序。

问题

  1. Fargate 是否需要您创建 ECS 集群?

  2. 在配置 Fargate 时,支持哪些网络模式?

  3. 真/假:Fargate 将 ECS 代理的控制平面网络通信与 ECS 任务的数据平面网络通信分开。

  4. 您使用 Fargate 部署一个新的 ECS 服务,但失败了,出现错误指示无法拉取任务定义中指定的 ECR 镜像。您验证了镜像名称和标签是正确的,并且任务定义的TaskRoleArn属性引用的 IAM 角色允许访问 ECR 存储库。这个错误最有可能的原因是什么?

  5. 根据这些要求,您正在确定在 AWS 中部署基于容器的应用程序的最佳技术。您的组织部署 Splunk 来收集所有应用程序的日志,并使用 New Relic 来收集性能指标。基于这些要求,Fargate 是否是一种合适的技术?

  6. 真/假:ECS 服务发现使用 Consul 发布服务注册信息。

  7. 哪种服务发现资源创建了 Route 53 区域?

  8. 您配置了一个 ECS 任务定义来使用 Fargate,并指定任务应分配 400 个 CPU 单位和 600 MB 的内存。当您部署使用任务定义的 ECS 服务时,部署失败了。您如何解决这个问题?

  9. 默认情况下,AWS X-Ray 通信使用哪种网络协议和端口?

  10. 真/假:当您为基于容器的应用程序添加 X-Ray 支持时,它们将发布跟踪到 AWS X-Ray 服务。

进一步阅读

您可以查看本章涵盖的主题的更多信息的以下链接:

第十五章:弹性 Beanstalk

到目前为止,在本书中,我们已经专注于使用弹性容器服务(ECS)及其变体 AWS Fargate 来管理和部署 Docker 应用程序。本书的其余部分将专注于您可以使用的替代技术,以在 AWS 中运行 Docker 应用程序,我们将首先介绍的是弹性 Beanstalk。

弹性 Beanstalk 属于行业通常称为平台即服务PaaS)的类别,旨在为您的应用程序提供受管的运行时环境,让您专注于开发、部署和操作应用程序,而不必担心周围的基础设施。为了强调这一范式,弹性 Beanstalk 专注于支持各种流行的编程语言,如 Node.js、PHP、Python、Ruby、Java、.NET 和 Go 应用程序。创建弹性 Beanstalk 应用程序时,您会指定目标编程语言,弹性 Beanstalk 将部署一个支持您的编程语言和相关运行时和应用程序框架的环境。弹性 Beanstalk 还将部署支持基础设施,如负载均衡器和数据库,更重要的是,它将配置您的环境,以便您可以轻松访问日志、监控信息和警报,确保您不仅可以部署应用程序,还可以监视它们,并确保它们处于最佳状态下运行。

除了前面提到的编程语言外,弹性 Beanstalk 还支持 Docker 环境,这意味着它可以支持在 Docker 容器中运行的任何应用程序,无论编程语言或应用程序运行时如何。在本章中,您将学习如何使用弹性 Beanstalk 来管理和部署 Docker 应用程序。您将学习如何使用 AWS 控制台创建弹性 Beanstalk 应用程序并创建一个环境,其中包括应用程序负载均衡器和我们应用程序所需的 RDS 数据库实例。您将遇到一些初始设置问题,并学习如何使用 AWS 控制台和弹性 Beanstalk 命令行工具来诊断和解决这些问题。

为了解决这些问题,您将配置一个名为ebextensions的功能,这是 Elastic Beanstalk 的高级配置功能,可用于将许多自定义配置方案应用于您的应用程序。您将利用 ebextensions 来解决 Docker 卷的权限问题,将 Elastic Beanstalk 生成的默认环境变量转换为应用程序期望的格式,并最终确保诸如执行数据库迁移之类的一次性部署任务仅在每个应用程序部署的单个实例上运行。

本章不旨在详尽介绍 Elastic Beanstalk,并且只关注与部署和管理 Docker 应用程序相关的核心场景。有关对其他编程语言的支持和更高级场景的覆盖,请参考AWS Elastic Beanstalk 开发人员指南

本章将涵盖以下主题:

  • Elastic Beanstalk 简介

  • 使用 AWS 控制台创建 Elastic Beanstalk 应用程序

  • 使用 Elastic Beanstalk CLI 管理 Elastic Beanstalk 应用程序

  • 自定义 Elastic Beanstalk 应用程序

  • 部署和测试 Elastic Beanstalk 应用程序

技术要求

本章的技术要求如下:

  • AWS 帐户的管理员访问权限

  • 本地环境按照第一章的说明进行配置

  • 本地 AWS 配置文件,按照第三章的说明进行配置

  • Python 2.7 或 3.x

  • PIP 软件包管理器

  • AWS CLI 版本 1.15.71 或更高版本

  • Docker 18.06 CE 或更高版本

  • Docker Compose 1.22 或更高版本

  • GNU Make 3.82 或更高版本

本章假设您已成功完成本书迄今为止涵盖的所有配置任务

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch14

查看以下视频以查看代码的实际操作:

bit.ly/2MDhtj2

Elastic Beanstalk 简介

正如本章介绍中所讨论的,Elastic Beanstalk 是 AWS 提供的 PaaS 服务,允许您专注于应用程序代码和功能,而不必担心支持应用程序所需的周围基础设施。为此,Elastic Beanstalk 在其方法上有一定的偏见,并且通常以特定的方式工作。Elastic Beanstalk 尽可能地利用其他 AWS 服务,并试图消除与这些服务集成的工作量和复杂性,如果您按照 Elastic Beanstalk 期望您使用这些服务的方式,这将非常有效。如果您在一个中小型组织中运行一个小团队,Elastic Beanstalk 可以为您提供很多价值,提供了大量的开箱即用功能。然而,一旦您的组织发展壮大,并且希望优化和标准化部署、监控和操作应用程序的方式,您可能会发现您已经超出了 Elastic Beanstalk 的个体应用程序重点的范围。

例如,重要的是要了解 Elastic Beanstalk 基于每个 EC2 实例的单个 ECS 任务定义的概念运行,因此,如果您希望在共享基础设施上运行多个容器工作负载,Elastic Beanstalk 不是您的正确选择。相同的情况也适用于日志记录和操作工具 - 一般来说,Elastic Beanstalk 提供了其专注于个体应用程序的工具链,而您的组织可能希望采用跨多个应用程序运行的标准工具集。就个人而言,我更喜欢使用 ECS 提供的更灵活和可扩展的方法,但我必须承认,Elastic Beanstalk 免费提供的一些开箱即用的操作和监控工具对于快速启动应用程序并与其他 AWS 服务完全集成非常有吸引力。

Elastic Beanstalk 概念

本章主要关注使用 Elastic Beanstalk 运行 Docker 应用程序,因此不要期望对 Elastic Beanstalk 及其支持的所有编程语言进行详尽的覆盖。然而,在我们开始创建 Elastic Beanstalk 应用程序之前,了解基本概念是很重要的,我将在这里简要介绍一下。

在使用 Elastic Beanstalk 时,您创建应用程序,可以定义一个或多个环境。以 todobackend 应用程序为例,您将把 todobackend 应用程序定义为 Elastic Beanstalk 应用程序,并创建一个名为 Dev 的环境和一个名为 Prod 的环境,以反映我们迄今部署的开发和生产环境。每个环境引用应用程序的特定版本,其中包含应用程序的可部署代码。对于 Docker 应用程序,源代码包括一个名为Dockerrun.aws.json的规范,该规范定义了应用程序的容器环境,可以引用外部 Docker 镜像或引用用于构建应用程序的本地 Dockerfile。

另一个重要的概念要了解的是,在幕后,Elastic Beanstalk 在常规 EC2 实例上运行您的应用程序,并遵循一个非常严格的范例,即每个 EC2 实例运行一个应用程序实例。每个 Elastic Beanstalk EC2 实例都运行一个根据目标应用程序特别策划的环境,例如,在多容器 Docker 应用程序的情况下,EC2 实例包括 Docker 引擎和 ECS 代理。Elastic Beanstalk 还允许您通过 SSH 访问和管理这些 EC2 实例(在本章中我们将使用 Linux 服务器),尽管您通常应该将此访问保留用于故障排除目的,并且永远不要尝试直接修改这些实例的配置。

创建一个 Elastic Beanstalk 应用程序

现在您已经了解了 Elastic Beanstalk 的基本概念,让我们把注意力转向创建一个新的 Elastic Beanstalk 应用程序。您可以使用各种方法创建和配置 Elastic Beanstalk 应用程序:

  • AWS 控制台

  • AWS CLI 和 SDK

  • AWS CloudFormation

  • Elastic Beanstalk CLI

在本章中,我们将首先在 AWS 控制台中创建一个 Elastic Beanstalk 应用程序,然后使用 Elastic Beanstalk CLI 来管理、更新和完善应用程序。

创建 Docker 应用程序时,重要的是要了解 Elastic Beanstalk 支持两种类型的 Docker 应用程序:

对于我们的用例,我们将采用与之前章节中为 ECS 配置 todobackend 应用程序的非常相似的方法,因此我们将需要一个多容器应用程序,因为我们之前在 ECS 任务定义中定义了一个名为todobackend的主应用程序容器定义和一个collectstatic容器定义(请参阅章节使用 CloudFormation 定义 ECS 任务定义中的部署使用 ECS 的应用程序)。总的来说,我建议采用多容器方法,无论您的应用程序是否是单容器应用程序,因为原始的单容器应用程序模型违反了 Docker 最佳实践,并且在应用程序要求发生变化或增长时,强制您从单个容器中运行所有内容。

创建 Dockerrun.aws.json 文件

无论您创建的是什么类型的 Docker 应用程序,您都必须首先创建一个名为Dockerrun.aws.json的文件,该文件定义了组成您的应用程序的各种容器。该文件以 JSON 格式定义,并基于您在之前章节中配置的 ECS 任务定义格式,我们将以此为Dockerrun.aws.json文件中的设置基础。

让我们在todobackend-aws存储库中创建一个名为eb的文件夹,并定义一个名为Dockerrun.aws.json的新文件,如下所示:

{
  "AWSEBDockerrunVersion": 2,
  "volumes": [
    {
      "name": "public",
      "host": {"sourcePath": "/tmp/public"}
    }
  ],
  "containerDefinitions": [
    {
      "name": "todobackend",
      "image": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend",
      "essential": true,
      "memoryReservation": 395,
      "mountPoints": [
        {
          "sourceVolume": "public",
          "containerPath": "/public"
        }
      ],
      "environment": [
        {"name":"DJANGO_SETTINGS_MODULE","value":"todobackend.settings_release"}
      ],
      "command": [
        "uwsgi",
        "--http=0.0.0.0:8000",
        "--module=todobackend.wsgi",
        "--master",
        "--die-on-term",
        "--processes=4",
        "--threads=2",
        "--check-static=/public"
      ],
      "portMappings": [
        {
          "hostPort": 80,
          "containerPort": 8000
        }
      ]
    },
    {
      "name": "collectstatic",
      "image": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend",
      "essential": false,
      "memoryReservation": 5,
      "mountPoints": [
        {
          "sourceVolume": "public",
          "containerPath": "/public"
        }
      ],
      "environment": [
        {"name":"DJANGO_SETTINGS_MODULE","value":"todobackend.settings_release"}
      ],
      "command": [
        "python3",
        "manage.py",
        "collectstatic",
        "--no-input"
      ]
    }
  ]
}

在定义多容器 Docker 应用程序时,您必须指定并使用规范格式的第 2 版本,该版本通过AWSEBDockerrunVersion属性进行配置。如果您回顾一下章节使用 ECS 部署应用程序中的使用 CloudFormation 定义 ECS 任务定义,您会发现Dockerrun.aws.json文件的第 2 版本规范非常相似,尽管格式是 JSON,而不是我们在 CloudFormation 模板中使用的 YAML 格式。我们使用驼峰命名来定义每个参数。

文件包括两个容器定义——一个用于主要的 todobackend 应用程序,另一个用于生成静态内容——我们定义了一个名为public的卷,用于存储静态内容。我们还配置了一个静态端口映射,将容器端口 8000 映射到主机的端口 80,因为 Elastic Beanstalk 默认期望您的 Web 应用程序在端口 80 上监听。

请注意,与我们用于 ECS 的方法相比,有一些重要的区别。

  • 镜像:我们引用相同的 ECR 镜像,但是我们没有指定镜像标签,这意味着最新版本的 Docker 镜像将始终被部署。Dockerrun.aws.json文件不支持参数或变量引用,因此如果您想引用一个明确的镜像,您需要一个自动生成此文件的持续交付工作流作为构建过程的一部分。

  • 环境:请注意,我们没有指定与数据库配置相关的任何环境变量,比如MYSQL_HOSTMYSQL_USER。我们将在本章后面讨论这样做的原因,但现在要明白的是,当您在 Elastic Beanstalk 中使用 RDS 的集成支持时,自动可用于应用程序的环境变量遵循不同的格式,我们需要转换以满足我们应用程序的期望。

  • 日志:我已经删除了 CloudWatch 日志配置,以简化本章,但您完全可以在您的容器中包含 CloudWatch 日志配置。请注意,如果您使用了 CloudWatch 日志,您需要修改 Elastic Beanstalk EC2 服务角色,以包括将您的日志写入 CloudWatch 日志的权限。我们将在本章后面看到一个例子。

我还删除了XRAY_DAEMON_ADDRESS环境变量,以保持简单,因为您可能不再在您的环境中运行 X-Ray 守护程序。请注意,如果您确实想支持 X-Ray,您需要确保附加到 Elastic Beanstalk 实例的实例安全组包含允许与 X-Ray 守护程序进行网络通信的安全规则。

现在我们已经定义了一个Dockerrun.aws.json文件,我们需要创建一个 ZIP 存档,其中包括这个文件。Elastic Beanstalk 要求您的应用程序源代码以 ZIP 或 WAR 存档格式上传,因此有这个要求。您可以通过使用zip实用程序从命令行执行此操作:

todobackend-aws/eb> zip -9 -r app.zip . -x .DS_Store
adding: Dockerrun.aws.json (deflated 69%)

这将在todobackend-aws/eb文件夹中创建一个名为app.zip的存档,使用-r标志指定 zip 应该递归添加所有可能存在的文件夹中的所有文件(这将在本章后面的情况下发生)。在指定app.zip的存档名称后,我们通过指定.而不是*来引用当前工作目录,因为使用.语法将包括任何隐藏的目录或文件(同样,这将在本章后面的情况下发生)。

还要注意,在 macOS 环境中,您可以使用-x标志来排除.DS_Store目录元数据文件,以防止其被包含在存档中。

使用 AWS 控制台创建一个弹性 Beanstalk 应用程序

现在我们准备使用 AWS 控制台创建一个弹性 Beanstalk 应用程序。要开始,请选择服务 | 弹性 Beanstalk,然后单击开始按钮创建一个新应用程序。在创建 Web 应用程序屏幕上,指定一个名为 todobackend 的应用程序名称,配置一个多容器 Docker的平台,最后使用上传您的代码选项为应用程序代码设置上传之前创建的app.zip文件:

创建一个弹性 Beanstalk Web 应用程序

接下来,点击配置更多选项按钮,这将呈现一个名为配置 Todobackend-Env的屏幕,允许您自定义应用程序。请注意,默认情况下,弹性 Beanstalk 将您的第一个应用程序环境命名为<application-name>-Env,因此名称为Todobackend-Env

在配置预设部分,选择高可用性选项,这将向您的配置添加一个负载均衡器:

配置弹性 Beanstalk Web 应用程序

如果您查看当前设置,您会注意到EC2 实例类型实例部分是t1.micro负载均衡器类型负载均衡器部分是经典,而数据库部分目前未配置。让我们首先通过单击实例部分的修改链接,更改实例类型,然后单击保存来修改EC2 实例类型为免费层t2.micro实例类型:

修改 EC2 实例类型

接下来,通过单击负载均衡器部分中的修改链接,然后单击保存,将负载均衡器类型更改为应用程序负载均衡器。请注意,默认设置期望在应用程序负载均衡器规则部分中将您的应用程序暴露在端口80上,以及您的容器在 EC2 实例上的端口 80 上,如进程部分中定义的那样:

修改负载均衡器类型

最后,我们需要通过单击数据库部分中的修改链接为应用程序定义数据库配置。选择mysql作为引擎,指定适当的用户名密码,最后将保留设置为删除,因为我们只是为了测试目的而使用这个环境。其他设置的默认值足够,因此在完成配置后,可以单击保存按钮:

配置数据库设置

在这一点上,您已经完成了应用程序的配置,并且可以单击配置 Todobackend-env屏幕底部的创建应用程序按钮。弹性 Beanstalk 现在将开始创建您的应用程序,并在控制台中显示此过程的进度。

弹性 Beanstalk 应用程序向导在幕后创建了一个包括您指定的所有资源和配置的 CloudFormation 堆栈。也可以使用 CloudFormation 创建自己的弹性 Beanstalk 环境,而不使用向导。

一段时间后,应用程序的创建将完成,尽管您可以看到应用程序存在问题:

初始应用程序状态

配置 EC2 实例配置文件

我们已经创建了一个新的弹性 Beanstalk 应用程序,但由于几个错误,当前应用程序的健康状态记录为严重。

如果您在左侧菜单中选择日志选项,然后选择请求日志 | 最后 100 行,您应该会看到一个下载链接,可以让您查看最近的日志活动:

初始应用程序状态

在您的浏览器中应该打开一个新的标签页,显示各种 Elastic Beanstalk 日志。在顶部,您应该看到 ECS 代理日志,最近的错误应该指示 ECS 代理无法从 ECR 将图像拉入您的Dockerrun.aws.json规范中:

Elastic Beanstalk ECS 代理错误

为了解决这个问题,我们需要配置与附加到我们的 Elastic Beanstalk 实例的 EC2 实例配置文件相关联的 IAM 角色,以包括从 ECR 拉取图像的权限。我们可以通过从左侧菜单中选择配置并在安全部分中查看虚拟机实例配置文件设置来查看 Elastic Beanstalk 正在使用的角色:

查看安全配置

您可以看到正在使用名为aws-elasticbeanstalk-ec2-role的 IAM 角色,因此,如果您从导航栏中选择服务 | IAM,选择角色,然后找到 IAM 角色,您需要按照以下方式将AmazonEC2ContainerRegistryReadOnly策略附加到角色:

将 AmazonEC2ContainerRegistryReadOnly 策略附加到 Elastic Beanstack EC2 实例角色

在这一点上,我们应该已经解决了之前导致应用程序启动失败的权限问题。您现在需要配置 Elastic Beanstalk 尝试重新启动应用程序,可以使用以下任一技术来执行:

  • 上传新的应用程序源文件-这将触发新的应用程序部署。

  • 重新启动应用程序服务器

  • 重建环境

鉴于我们的应用程序源(在 Docker 应用程序的情况下是Dockerrun.aws.json文件)没有更改,最不破坏性和最快的选项是重新启动应用程序服务器,您可以通过在所有应用程序 | todobackend | Todobackend-env配置屏幕的右上角选择操作 | 重新启动应用程序服务器(s)来执行。

几分钟后,您会注意到您的应用程序仍然存在问题,如果您重复获取最新日志的过程,并扫描这些日志,您会发现collectstatic容器由于权限错误而失败:

collectstatic 权限错误

回想一下,在本书的早些时候,我们如何在我们的 ECS 容器实例上配置了一个具有正确权限的文件夹,以托管collectstatic容器写入的公共卷?对于 Elastic Beanstalk,为 Docker 应用程序创建的默认 EC2 实例显然没有以这种方式进行配置。

我们将很快解决这个问题,但现在重要的是要了解还有其他问题。要了解这些问题,您需要尝试访问应用程序,您可以通过单击 All Applications | todobackend | Todobackend-env 配置屏幕顶部的 URL 链接来实现:

获取 Elastic Beanstalk 应用程序 URL

浏览到此链接应立即显示静态内容文件未生成:

缺少静态内容

如果您单击todos链接以查看当前的 Todo 项目列表,您将收到一个错误,指示应用程序无法连接到 MySQL 数据库:

数据库连接错误

问题在于我们尚未向Dockerrun.aws.json文件添加任何数据库配置,因此我们的应用程序默认使用本地主机来定位数据库。

使用 CLI 配置 Elastic Beanstalk 应用程序

我们将很快解决我们应用程序中仍然存在的问题,但为了解决这些问题,我们将继续使用 Elastic Beanstalk CLI 来继续配置我们的应用程序并解决这些问题。

在我们开始使用 Elastic Beanstalk CLI 之前,重要的是要了解,当前版本的该应用程序在与我们在早期章节中引入的多因素身份验证(MFA)要求进行交互时存在一些挑战。如果您继续使用 MFA,您会注意到每次执行 Elastic Beanstalk CLI 命令时都会提示您。

为了解决这个问题,我们可以通过首先将用户从Users组中删除来临时删除 MFA 要求:

> aws iam remove-user-from-group --user-name justin.menga --group-name Users

接下来,在本地的~/.aws/config文件中的docker-in-aws配置文件中注释掉mfa_serial行:

[profile docker-in-aws]
source_profile = docker-in-aws
role_arn = arn:aws:iam::385605022855:role/admin
role_session_name=justin.menga
region = us-east-1
# mfa_serial = arn:aws:iam::385605022855:mfa/justin.menga

请注意,这并不理想,在实际情况下,您可能无法或不想临时禁用特定用户的 MFA。在考虑 Elastic Beanstalk 时,请记住这一点,因为您通常会依赖 Elastic Beanstalk CLI 执行许多操作。

现在 MFA 已被临时禁用,您可以安装 Elastic Beanstalk CLI,您可以使用 Python 的pip软件包管理器来安装它。安装完成后,可以通过eb命令访问它:

> pip3 install awsebcli --user
Collecting awsebcli
...
...
Installing collected packages: awsebcli
Successfully installed awsebcli-3.14.2
> eb --version
EB CLI 3.14.2 (Python 3.6.5)

下一步是在您之前创建的todobackend/eb文件夹中初始化 CLI:

todobackend/eb> eb init --profile docker-in-aws

Select a default region
1) us-east-1 : US East (N. Virginia)
2) us-west-1 : US West (N. California)
3) us-west-2 : US West (Oregon)
4) eu-west-1 : EU (Ireland)
5) eu-central-1 : EU (Frankfurt)
6) ap-south-1 : Asia Pacific (Mumbai)
7) ap-southeast-1 : Asia Pacific (Singapore)
8) ap-southeast-2 : Asia Pacific (Sydney)
9) ap-northeast-1 : Asia Pacific (Tokyo)
10) ap-northeast-2 : Asia Pacific (Seoul)
11) sa-east-1 : South America (Sao Paulo)
12) cn-north-1 : China (Beijing)
13) cn-northwest-1 : China (Ningxia)
14) us-east-2 : US East (Ohio)
15) ca-central-1 : Canada (Central)
16) eu-west-2 : EU (London)
17) eu-west-3 : EU (Paris)
(default is 3): 1

Select an application to use
1) todobackend
2) [ Create new Application ]
(default is 2): 1
Cannot setup CodeCommit because there is no Source Control setup, continuing with initialization

eb init命令使用--profile标志来指定本地 AWS 配置文件,然后提示您将要交互的区域。然后 CLI 会检查是否存在任何现有的 Elastic Beanstalk 应用程序,并询问您是否要管理现有应用程序或创建新应用程序。一旦您做出选择,CLI 将在名为.elasticbeanstalk的文件夹下将项目信息添加到当前文件夹中,并创建或追加到.gitignore文件。鉴于我们的eb文件夹是todobackend存储库的子目录,将.gitignore文件的内容追加到todobackend存储库的根目录是一个好主意:

todobackend-aws/eb> cat .gitignore >> ../.gitignore todobackend-aws/eb> rm .gitignore 

您现在可以使用 CLI 查看应用程序的当前状态,列出应用程序环境,并执行许多其他管理任务:

> eb status
Environment details for: Todobackend-env
  Application name: todobackend
  Region: us-east-1
  Deployed Version: todobackend-source
  Environment ID: e-amv5i5upx4
  Platform: arn:aws:elasticbeanstalk:us-east-1::platform/multicontainer Docker running on 64bit Amazon Linux/2.11.0
  Tier: WebServer-Standard-1.0
  CNAME: Todobackend-env.p6z6jvd24y.us-east-1.elasticbeanstalk.com
  Updated: 2018-07-14 23:23:28.931000+00:00
  Status: Ready
  Health: Red
> eb list
* Todobackend-env
> eb open
> eb logs 
Retrieving logs...
============= i-0f636f261736facea ==============
-------------------------------------
/var/log/ecs/ecs-init.log
-------------------------------------
2018-07-14T22:41:24Z [INFO] pre-start
2018-07-14T22:41:25Z [INFO] start
2018-07-14T22:41:25Z [INFO] No existing agent container to remove.
2018-07-14T22:41:25Z [INFO] Starting Amazon Elastic Container Service Agent

-------------------------------------
/var/log/eb-ecs-mgr.log
-------------------------------------
2018-07-14T23:20:37Z "cpu": "0",
2018-07-14T23:20:37Z "containers": [
...
...

请注意,eb status命令会列出应用程序的 URL 在CNAME属性中,请记下这个 URL,因为您需要在本章中测试您的应用程序。您还可以使用eb open命令访问您的应用程序,这将在您的默认浏览器中打开应用程序的 URL。

管理 Elastic Beanstalk EC2 实例

在使用 Elastic Beanstalk 时,能够访问 Elastic Beanstalk EC2 实例是很有用的,特别是如果您需要进行一些故障排除。

CLI 包括建立与 Elastic Beanstalk EC2 实例的 SSH 连接的功能,您可以通过运行eb ssh --setup命令来设置它:

> eb ssh --setup
WARNING: You are about to setup SSH for environment "Todobackend-env". If you continue, your existing instances will have to be **terminated** and new instances will be created. The environment will be temporarily unavailable.
To confirm, type the environment name: Todobackend-env

Select a keypair.
1) admin
2) [ Create new KeyPair ]
(default is 1): 1
Printing Status:
Printing Status:
INFO: Environment update is starting.
INFO: Updating environment Todobackend-env's configuration settings.
INFO: Created Auto Scaling launch configuration named: awseb-e-amv5i5upx4-stack-AWSEBAutoScalingLaunchConfiguration-8QN6BJJX43H
INFO: Deleted Auto Scaling launch configuration named: awseb-e-amv5i5upx4-stack-AWSEBAutoScalingLaunchConfiguration-JR6N80L37H2G
INFO: Successfully deployed new configuration to environment.

请注意,设置 SSH 访问需要您终止现有实例并创建新实例,因为您只能在创建 EC2 实例时将 SSH 密钥对与实例关联。在选择您在本书中早期创建的现有 admin 密钥对后,CLI 终止现有实例,创建一个新的自动缩放启动配置以启用 SSH 访问,然后启动新实例。

在创建弹性 Beanstalk 应用程序时,您可以通过在配置向导的安全部分中配置 EC2 密钥对来避免此步骤。

现在,您可以按照以下步骤 SSH 进入您的弹性 Beanstalk EC2 实例:

> eb ssh -e "ssh -i ~/.ssh/admin.pem"
INFO: Attempting to open port 22.
INFO: SSH port 22 open.
INFO: Running ssh -i ~/.ssh/admin.pem ec2-user@34.239.245.78
The authenticity of host '34.239.245.78 (34.239.245.78)' can't be established.
ECDSA key fingerprint is SHA256:93m8hag/EtCPb5i7YrYHUXFPloaN0yUHMVFFnbMlcLE.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '34.239.245.78' (ECDSA) to the list of known hosts.
 _____ _ _ _ ____ _ _ _
| ____| | __ _ ___| |_(_) ___| __ ) ___ __ _ _ __ ___| |_ __ _| | | __
| _| | |/ _` / __| __| |/ __| _ \ / _ \/ _` | '_ \/ __| __/ _` | | |/ /
| |___| | (_| \__ \ |_| | (__| |_) | __/ (_| | | | \__ \ || (_| | | <
|_____|_|\__,_|___/\__|_|\___|____/ \___|\__,_|_| |_|___/\__\__,_|_|_|\_\
 Amazon Linux AMI

This EC2 instance is managed by AWS Elastic Beanstalk. Changes made via SSH
WILL BE LOST if the instance is replaced by auto-scaling. For more information
on customizing your Elastic Beanstalk environment, see our documentation here:
http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-ec2.html

默认情况下,eb ssh 命令将尝试使用名为 ~/.ssh/<ec2-keypair-name>.pem 的 SSH 私钥,本例中为 ~/.ssh/admin.pem。如果您的 SSH 私钥位于不同位置,您可以使用 -e 标志来覆盖使用的文件,就像上面的示例中演示的那样。

现在,您可以查看一下您的弹性 Beanstalk EC2 实例。鉴于我们正在运行一个 Docker 应用程序,您可能首先倾向于运行 docker ps 命令以查看当前正在运行的容器:

[ec2-user@ip-172-31-20-192 ~]$ docker ps
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.37/containers/json: dial unix /var/run/docker.sock: connect: permission denied

令人惊讶的是,标准的 ec2-user 没有访问 Docker 的权限 - 为了解决这个问题,我们需要添加更高级的配置,称为 ebextensions

自定义弹性 Beanstalk 应用程序

如前一节所讨论的,我们需要添加一个 ebextension,它只是一个配置文件,可用于自定义您的弹性 Beanstalk 环境以适应我们现有的弹性 Beanstalk 应用程序。这是一个重要的概念需要理解,因为我们最终将使用相同的方法来解决我们应用程序当前存在的所有问题。

要配置 ebextensions,首先需要在存储 Dockerrun.aws.json 文件的 eb 文件夹中创建一个名为 .ebextensions 的文件夹(请注意,您需要断开 SSH 会话,转到您的弹性 Beanstalk EC2 实例,并在本地环境中执行此操作):

todobackend/eb> mkdir .ebextensions todobackend/eb> touch .ebextensions/init.config

.ebextensions 文件夹中具有 .config 扩展名的每个文件都将被视为 ebextension,并在应用程序部署期间由弹性 Beanstalk 处理。在上面的示例中,我们创建了一个名为 init.config 的文件,现在我们可以配置它以允许 ec2-user 访问 Docker 引擎:

commands:
  01_add_ec2_user_to_docker_group:
    command: usermod -aG docker ec2-user
    ignoreErrors: true

我们在commands键中添加了一个名为01_add_ec2_user_to_docker_group的命令指令,这是一个顶级属性,定义了在设置和部署最新版本应用程序到实例之前应该运行的命令。该命令运行usermod命令,以确保ec2-userdocker组的成员,这将授予ec2-user访问 Docker 引擎的权限。请注意,您可以使用ignoreErrors属性来确保忽略任何命令失败。

有了这个配置,我们可以通过在eb文件夹中运行eb deploy命令来部署我们应用程序的新版本,这将自动创建我们现有的Dockerrun.aws.json和新的.ebextensions/init.config文件的 ZIP 存档。

todobackend-aws/eb> rm app.zip
todobackend-aws/eb> eb deploy
Uploading todobackend/app-180715_195517.zip to S3\. This may take a while.
Upload Complete.
INFO: Environment update is starting.
INFO: Deploying new version to instance(s).
INFO: Stopping ECS task arn:aws:ecs:us-east-1:385605022855:task/dd2a2379-1b2c-4398-9f44-b7c25d338c67.
INFO: ECS task: arn:aws:ecs:us-east-1:385605022855:task/dd2a2379-1b2c-4398-9f44-b7c25d338c67 is STOPPED.
INFO: Starting new ECS task with awseb-Todobackend-env-amv5i5upx4:3.
INFO: ECS task: arn:aws:ecs:us-east-1:385605022855:task/d9fa5a87-1329-401a-ba26-eb18957f5070 is RUNNING.
INFO: New application version was deployed to running EC2 instances.
INFO: Environment update completed successfully.

我们首先删除您第一次创建 Elastic Beanstalk 应用程序时创建的初始app.zip存档,因为eb deploy命令会自动处理这个问题。您可以看到一旦新配置上传,部署过程涉及停止和启动运行我们应用程序的 ECS 任务。

部署完成后,如果您建立一个新的 SSH 会话到 Elastic Beanstalk EC2 实例,您应该能够运行docker命令:

[ec2-user@ip-172-31-20-192 ~]$ docker ps --format "{{.ID}}: {{.Image}}"
63183a7d3e67: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
45bf3329a686: amazon/amazon-ecs-agent:latest

您可以看到实例当前正在运行 todobackend 容器,并且还运行 ECS 代理。这表明 Elastic Beanstalk 中的 Docker 支持在后台使用 ECS 来管理和部署基于容器的应用程序。

解决 Docker 卷权限问题

在本章的前面,我们遇到了一个问题,即 collectstatic 容器无法写入公共卷。问题在于 Elastic Beanstalk EC2 实例上运行的 ECS 代理创建了一个绑定挂载,这些挂载始终以 root 权限创建。这会阻止我们的 collectstatic 容器以 app 用户的身份写入公共卷,因此我们需要一些方法来解决这个问题。

正如我们已经看到的,ebextensions功能可以在 Elastic Beanstalk EC2 实例上运行命令,我们将再次利用这个功能来确保公共卷被配置为允许我们容器中的app用户读写.ebextensions/init.config文件:

commands:
  01_add_ec2_user_to_docker_group:
    command: usermod -aG docker ec2-user
    ignoreErrors: true
 02_docker_volumes:
 command: |
 mkdir -p /tmp/public
 chown -R 1000:1000 /tmp/public

我们添加了一个名为02_docker_volumes的新命令指令,它将在01_add_ec2_user_to_docker_group命令之后执行。请注意,您可以使用 YAML 管道运算符(|)来指定多行命令字符串,从而允许您指定要运行的多个命令。我们首先创建/tmp/public文件夹,该文件夹是Dockerrun.aws.json文件中公共卷主机sourcePath属性所指的位置,然后确保用户 ID/组 ID 值为1000:1000拥有此文件夹。因为应用程序用户的用户 ID 为 1000,组 ID 为 1000,这将使任何以该用户身份运行的进程能够写入和读取公共卷。

在这一点上,您可以使用eb deploy命令将新的应用程序配置上传到 Elastic Beanstalk(请参阅前面的示例)。部署完成后,您可以通过运行eb open命令浏览到应用程序的 URL,并且现在应该看到 todobackend 应用程序的静态内容和格式正确。

配置数据库设置

我们已解决了访问公共卷的问题,但是应用程序仍然无法工作,因为我们没有传递任何环境变量来配置数据库设置。造成这种情况的原因是,当您在 Elastic Beanstalk 中配置数据库时,所有数据库设置都可以通过以下环境变量获得:

  • RDS_HOSTNAME

  • RDS_USERNAME

  • RDS_PASSWORD

  • RDS_DB_NAME

  • RDS_PORT

todobackend 应用程序的问题在于它期望以 MYSQL 为前缀的与数据库相关的设置,例如,MYSQL_HOST用于配置数据库主机名。虽然我们可以更新我们的应用程序以使用 RDS 前缀的环境变量,但我们可能希望将我们的应用程序部署到其他云提供商,而 RDS 是 AWS 特定的技术。

另一种选择,尽管更复杂的方法是将环境变量映射写入 Elastic Beanstalk 实例上的文件,将其配置为 todobackend 应用程序容器可以访问的卷,然后修改我们的 Docker 镜像以在容器启动时注入这些映射。这要求我们修改位于todobackend存储库根目录中的entrypoint.sh文件中的 todobackend 应用程序的入口脚本:

#!/bin/bash
set -e -o pipefail

# Inject AWS Secrets Manager Secrets
# Read space delimited list of secret names from SECRETS environment variable
echo "Processing secrets [${SECRETS}]..."
read -r -a secrets <<< "$SECRETS"
for secret in "${secrets[@]}"
do
  vars=$(aws secretsmanager get-secret-value --secret-id $secret \
    --query SecretString --output text \
    | jq -r 'to_entries[] | "export \(.key)='\''\(.value)'\''"')
  eval $vars
done

# Inject runtime environment variables
if [ -f /init/environment ]
then
 echo "Processing environment variables from /init/environment..."
 export $(cat /init/environment | xargs)
fi

# Run application
exec "$@"

在上面的例子中,我们添加了一个新的测试表达式,用于检查是否存在一个名为/init/environment的文件,使用语法[ -f /init/environment ]。如果找到了这个文件,我们假设该文件包含一个或多个环境变量设置,格式为<环境变量>=<值> - 例如:

MYSQL_HOST=abc.xyz.com
MYSQL_USERNAME=todobackend
...
...

有了前面的格式,我们接着使用export $(cat /init/environment | xargs)命令,该命令会扩展为export MYSQL_HOST=abc.xyz.com MYSQL_USERNAME=todobackend ... ...,使用前面的例子,确保在/init/environment文件中定义的每个环境变量都被导出到环境中。

如果您现在提交您对todobackend存储库的更改,并运行make loginmake testmake releasemake publish命令,最新的todobackend Docker 镜像现在将包括更新后的入口脚本。现在,我们需要修改todobackend-aws/eb文件夹中的Dockerrun.aws.json文件,以定义一个名为init的新卷和挂载:

{
  "AWSEBDockerrunVersion": 2,
  "volumes": [
    {
      "name": "public",
      "host": {"sourcePath": "/tmp/public"}
    },
 {
 "name": "init",
 "host": {"sourcePath": "/tmp/init"}
 }
  ],
  "containerDefinitions": [
    {
      "name": "todobackend",
      "image": "385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend",
      "essential": true,
      "memoryReservation": 395,
      "mountPoints": [
        {
          "sourceVolume": "public",
          "containerPath": "/public"
        },
{
 "sourceVolume": "init",
 "containerPath": "/init"
 }
      ],
      "environment": [
{"name":"DJANGO_SETTINGS_MODULE","value":"todobackend.settings_release"}
      ],
   ...
   ...

有了这个卷映射到弹性 Beanstalk EC2 实例上的/tmp/inittodobackend容器中的/init,现在我们所需要做的就是将环境变量设置写入到 EC2 实例上的/tmp/init/environment,这将显示为todobackend容器中的/init/environment,并使用我们对入口脚本所做的修改来触发文件的处理。这里的想法是,我们将弹性 Beanstalk RDS 实例设置写入到 todobackend 应用程序所期望的适当环境变量设置中。

在我们能够做到这一点之前,我们需要一个机制来获取 RDS 设置 - 幸运的是,每个弹性 Beanstalk 实例上都有一个名为/opt/elasticbeanstalk/deploy/configuration/containerconfiguration的文件,其中包含整个环境和应用程序配置的 JSON 文件格式。

如果您 SSH 到一个实例,您可以使用jq实用程序(它已经预先安装在弹性 Beanstalk 实例上)来提取您的弹性 Beanstalk 应用程序的 RDS 实例设置:

> sudo jq '.plugins.rds.env' -r \ 
 /opt/elasticbeanstalk/deploy/configuration/containerconfiguration
{
  "RDS_PORT": "3306",
  "RDS_HOSTNAME": "aa2axvguqnh17c.cz8cu8hmqtu1.us-east-1.rds.amazonaws.com",
  "RDS_USERNAME": "todobackend",
  "RDS_DB_NAME": "ebdb",
  "RDS_PASSWORD": "some-super-secret"
}

有了这个提取 RDS 设置的机制,我们现在可以修改.ebextensions/init.config文件,将这些设置中的每一个写入到/tmp/init/environment文件中,该文件将通过init卷暴露给todobackend容器,位于/init/environment

commands:
  01_add_ec2_user_to_docker_group:
    command: usermod -aG docker ec2-user
    ignoreErrors: true
  02_docker_volumes:
    command: |
      mkdir -p /tmp/public
 mkdir -p /tmp/init
      chown -R 1000:1000 /tmp/public
 chown -R 1000:1000 /tmp/init

container_commands:
 01_rds_settings:
 command: |
 config=/opt/elasticbeanstalk/deploy/configuration/containerconfiguration
 environment=/tmp/init/environment
 echo "MYSQL_HOST=$(jq '.plugins.rds.env.RDS_HOSTNAME' -r $config)" >> $environment
 echo "MYSQL_USER=$(jq '.plugins.rds.env.RDS_USERNAME' -r $config)" >> $environment
 echo "MYSQL_PASSWORD=$(jq '.plugins.rds.env.RDS_PASSWORD' -r $config)" >> $environment
 echo "MYSQL_DATABASE=$(jq '.plugins.rds.env.RDS_DB_NAME' -r $config)" >> $environment
 chown -R 1000:1000 $environment

我们首先修改02_docker_volumes指令,创建 init 卷映射到的/tmp/init路径,并确保在 todobackend 应用程序中运行的 app 用户对此文件夹具有读/写访问权限。接下来,我们添加container_commands键,该键指定应在应用程序配置应用后但在应用程序启动之前执行的命令。请注意,这与commands键不同,后者在应用程序配置应用之前执行命令。

container_commands键的命名有些令人困惑,因为它暗示命令将在 Docker 容器内运行。实际上并非如此,container_commands键与 Docker 中的容器完全无关。

01_rds_settings命令编写了应用程序所需的各种 MYSQL 前缀环境变量设置,通过执行jq命令获取每个变量的适当值,就像我们之前演示的那样。因为这个文件是由 root 用户创建的,所以我们最终确保app用户对/tmp/init/environment文件具有读/写访问权限,该文件将通过 init 卷作为/init/environment存在于容器中。

如果您现在使用eb deploy命令部署更改,一旦部署完成并导航到 todobackend 应用程序 URL,如果尝试列出 Todos 项目(通过访问/todos),请注意现在显示了一个新错误:

访问 todobackend Todos 项目错误

回想一下,当您之前访问相同的 URL 时,todobackend 应用程序尝试使用 localhost 访问 MySQL,但现在我们收到一个错误,指示在ebdb数据库中找不到todo_todoitem表。这证实了应用程序现在正在与 RDS 实例通信,但由于我们尚未运行数据库迁移,因此不支持应用程序的架构和表。

运行数据库迁移

要解决我们应用程序的当前问题,我们需要一个机制,允许我们运行数据库迁移以创建所需的数据库架构和表。这也必须发生在每次应用程序更新时,但这应该只发生一次每次应用程序更新。例如,如果您有多个 Elastic Beanstalk 实例,您不希望在每个实例上运行迁移。相反,您希望迁移仅在每次部署时运行一次。

在上一节中介绍的container_commands键中包含一个有用的属性叫做leader_only,它配置 Elastic Beanstalk 只在领导者实例上运行指定的命令。这是第一个可用于部署的实例。因此,我们可以在todobackend-aws/eb文件夹中的.ebextensions/init.config文件中添加一个新的指令,每次应用程序部署时只运行一次迁移:

commands:
  01_add_ec2_user_to_docker_group:
    command: usermod -aG docker ec2-user
    ignoreErrors: true
  02_docker_volumes:
    command: |
      mkdir -p /tmp/public
      mkdir -p /tmp/init
      chown -R 1000:1000 /tmp/public
      chown -R 1000:1000 /tmp/init

container_commands:
  01_rds_settings:
    command: |
      config=/opt/elasticbeanstalk/deploy/configuration/containerconfiguration
      environment=/tmp/init/environment
      echo "MYSQL_HOST=$(jq '.plugins.rds.env.RDS_HOSTNAME' -r $config)" >> $environment
      echo "MYSQL_USER=$(jq '.plugins.rds.env.RDS_USERNAME' -r $config)" >> $environment
      echo "MYSQL_PASSWORD=$(jq '.plugins.rds.env.RDS_PASSWORD' -r $config)" >> $environment
      echo "MYSQL_DATABASE=$(jq '.plugins.rds.env.RDS_DB_NAME' -r $config)" >> $environment
      chown -R 1000:1000 $environment
  02_migrate:
 command: |
 echo "python3 manage.py migrate --no-input" >> /tmp/init/commands
 chown -R 1000:1000 /tmp/init/commands
 leader_only: true

在这里,我们将python3 manage.py migrate --no-input命令写入/tmp/init/commands文件,该文件将暴露给应用程序容器,位置在/init/commands。当然,这要求我们现在修改todobackend存储库中的入口脚本,以查找这样一个文件并执行其中包含的命令,如下所示:

#!/bin/bash
set -e -o pipefail

# Inject AWS Secrets Manager Secrets
# Read space delimited list of secret names from SECRETS environment variable
echo "Processing secrets [${SECRETS}]..."
read -r -a secrets <<< "$SECRETS"
for secret in "${secrets[@]}"
do
  vars=$(aws secretsmanager get-secret-value --secret-id $secret \
    --query SecretString --output text \
    | jq -r 'to_entries[] | "export \(.key)='\''\(.value)'\''"')
  eval $vars
done

# Inject runtime environment variables
if [ -f /init/environment ]
then
  echo "Processing environment variables from /init/environment..."
  export $(cat /init/environment | xargs)
fi # Inject runtime init commands
if [ -f /init/commands ]
then
  echo "Processing commands from /init/commands..."
  source /init/commands
fi

# Run application
exec "$@"

在这里,我们添加了一个新的测试表达式,检查/init/commands文件是否存在,如果存在,我们使用source命令来执行文件中包含的每个命令。因为这个文件只会在领导者弹性 Beanstalk 实例上写入,入口脚本将在每次部署时只调用这些命令一次。

在这一点上,您需要通过运行make loginmake testmake releasemake publish命令来重新构建 todobackend Docker 镜像,之后您可以通过从todobackend-aws/eb目录运行eb deploy命令来部署 Elastic Beanstalk 更改。一旦这个成功完成,如果您 SSH 到您的 Elastic Beanstalk 实例并审查当前活动的 todobackend 应用程序容器的日志,您应该会看到数据库迁移是在容器启动时执行的:

> docker ps --format "{{.ID}}: {{.Image}}"
45b8cdac0c92: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
45bf3329a686: amazon/amazon-ecs-agent:latest
> docker logs 45b8cdac0c92
Processing secrets []...
Processing environment variables from /init/environment...
Processing commands from /init/commands...
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK
[uwsgi-static] added check for /public
* Starting uWSGI 2.0.17 (64bit) on [Sun Jul 15 11:18:06 2018] *

如果您现在浏览应用程序 URL,您应该会发现应用程序是完全可用的,并且您已成功将 Docker 应用程序部署到 Elastic Beanstalk。

在结束本章之前,您应该通过将您的用户帐户重新添加到Users组来恢复您在本章前面暂时禁用的 MFA 配置:

> aws iam add-user-to-group --user-name justin.menga --group-name Users

然后在本地的~/.aws/config文件中重新启用docker-in-aws配置文件中的mfa_serial行:

[profile docker-in-aws]
source_profile = docker-in-aws
role_arn = arn:aws:iam::385605022855:role/admin
role_session_name=justin.menga
region = us-east-1
mfa_serial = arn:aws:iam::385605022855:mfa/justin.menga 

您还可以通过浏览主 Elastic Beanstalk 仪表板并单击操作|删除按钮旁边的todobackend应用程序来删除 Elastic Beanstalk 环境。这将删除 Elastic Beanstalk 环境创建的 CloudFormation 堆栈,其中包括应用程序负载均衡器、RDS 数据库实例和 EC2 实例。

总结

在本章中,您学会了如何使用 Elastic Beanstalk 部署多容器 Docker 应用程序。您了解了为什么以及何时会选择 Elastic Beanstalk 而不是其他替代容器管理服务,如 ECS,总的结论是 Elastic Beanstalk 非常适合规模较小的组织和少量应用程序,但随着组织的增长,需要开始专注于提供共享容器平台以降低成本、复杂性和管理开销时,它变得不那么有用。

您使用 AWS 控制台创建了一个 Elastic Beanstalk 应用程序,这需要您定义一个名为Dockerrun.aws.json的单个文件,其中包括运行应用程序所需的容器定义和卷,然后自动部署应用程序负载均衡器和最小配置的 RDS 数据库实例。将应用程序快速运行到完全功能状态是有些具有挑战性的,需要您定义名为ebextensions的高级配置文件,这些文件允许您调整 Elastic Beanstalk 以满足应用程序的特定需求。您学会了如何安装和设置 Elastic Beanstalk CLI,使用 SSH 连接到 Elastic Beanstalk 实例,并部署对Dockerrun.aws.json文件和ebextensions文件的配置更改。这使您能够为以非根用户身份运行的容器应用程序在 Elastic Beanstalk 实例上设置正确权限的卷,并引入了一个特殊的 init 卷,您可以在其中注入环境变量设置和应作为容器启动时执行的命令。

在下一章中,我们将看一下 Docker Swarm 以及如何在 AWS 上部署和运行 Docker Swarm 集群来部署和运行 Docker 应用程序。

问题

  1. 真/假:Elastic Beanstalk 只支持单容器 Docker 应用程序。

  2. 使用 Elastic Beanstalk 创建 Docker 应用程序所需的最低要求是什么?

  3. 真/假:.ebextensions 文件夹存储允许您自定义 Elastic Beanstalk 实例的 YAML 文件。

  4. 您创建了一个部署存储在 ECR 中的 Docker 应用程序的新 Elastic Beanstalk 服务。在初始创建时,应用程序失败,Elastic Beanstalk 日志显示错误,包括“CannotPullECRContainerError”一词。您将如何解决此问题?

  5. 真/假:在不进行任何额外配置的情况下,以非根用户身份运行的 Docker 容器在 Elastic Beanstalk 环境中可以读取和写入 Docker 卷。

  6. 真/假:您可以将 leader_only 属性设置为 true,在 commands 键中仅在一个 Elastic Beanstalk 实例上运行命令。

  7. 真/假:eb connect 命令用于建立对 Elastic Beanstalk 实例的 SSH 访问。

  8. 真/假:Elastic Beanstalk 支持将应用程序负载均衡器集成到您的应用程序中。

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十六章:AWS 中的 Docker Swarm

Docker Swarm 代表了 Docker 的本机容器管理平台,直接内置到 Docker Engine 中,对于许多第一次使用 Docker 的人来说,Docker Swarm 是他们首次了解和学习的容器管理平台,因为它是 Docker Engine 的集成功能。Docker Swarm 自然是 AWS 支持的 ECS、Fargate、弹性 Beanstalk 和最近的弹性 Kubernetes 服务(EKS)的竞争对手,因此您可能会想知道为什么一本关于 AWS 中的 Docker 的书会有一个专门介绍 Docker Swarm 的章节。许多组织更喜欢使用与云提供商无关的容器管理平台,可以在 AWS、谷歌云和 Azure 等其他云提供商以及本地运行,如果这对您和您的组织是这种情况,那么 Docker Swarm 肯定是值得考虑的选项。

在本章中,您将学习如何使用 Docker for AWS 解决方案将 Docker Swarm 部署到 AWS,该解决方案使得在 AWS 上快速启动和运行 Docker Swarm 集群变得非常容易。您将学习如何管理和访问 Swarm 集群的基础知识,如何创建和部署服务到 Docker Swarm,以及如何利用与 Docker for AWS 解决方案集成的许多 AWS 服务。这将包括将 Docker Swarm 与弹性容器注册表(ECR)集成,通过与 AWS 弹性负载均衡(ELB)集成将应用程序发布到外部世界,使用 AWS 弹性文件系统(EFS)创建共享卷,以及使用 AWS 弹性块存储(EBS)创建持久卷。

最后,您将学习如何解决关键的运营挑战,包括运行一次性部署任务,使用 Docker secrets 进行秘密管理,以及使用滚动更新部署应用程序。通过本章的学习,您将了解如何将 Docker Swarm 集群部署到 AWS,如何将 Docker Swarm 与 AWS 服务集成,以及如何将生产应用程序部署到 Docker Swarm。

本章将涵盖以下主题:

  • Docker Swarm 简介

  • 安装 Docker for AWS

  • 访问 Docker Swarm

  • 将 Docker 服务部署到 Docker Swarm

  • 将 Docker 堆栈部署到 Docker Swarm

  • 将 Docker Swarm 与 ECR 集成

  • 使用 EFS 创建共享 Docker 卷

  • 使用 EBS 创建持久 Docker 卷

  • 支持一次性部署任务

  • 执行滚动更新

技术要求

以下是本章的技术要求:

  • 对 AWS 账户的管理访问权限

  • 本地环境按照第一章的说明进行配置

  • 本地 AWS 配置文件,按照第三章的说明进行配置

  • AWS CLI 版本 1.15.71 或更高版本

  • Docker 18.06 CE 或更高版本

  • Docker Compose 1.22 或更高版本

  • GNU Make 3.82 或更高版本

本章假定您已经完成了本书中的所有前一章节

以下 GitHub URL 包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch16

查看以下视频以查看代码的实际操作:

bit.ly/2ogdBpp

Docker Swarm 介绍

Docker Swarm是 Docker Engine 的一个本地集成功能,提供集群管理和容器编排功能,允许您在生产环境中规模化运行 Docker 容器。每个运行版本 1.13 或更高版本的 Docker Engine 都包括在 swarm 模式下运行的能力,提供以下功能:

  • 集群管理:所有在 swarm 模式下运行的节点都包括本地集群功能,允许您快速建立集群,以便部署您的应用程序。

  • 多主机网络:Docker 支持覆盖网络,允许您创建虚拟网络,所有连接到网络的容器可以私下通信。这个网络层完全独立于连接 Docker Engines 的物理网络拓扑,这意味着您通常不必担心传统的网络约束,比如 IP 地址和网络分割——Docker 会为您处理所有这些。

  • 服务发现和负载均衡:Docker Swarm 支持基于 DNS 的简单服务发现模型,允许您的应用程序发现彼此,而无需复杂的服务发现协议或基础设施。Docker Swarm 还支持使用 DNS 轮询自动负载均衡流量到您的应用程序,并可以集成外部负载均衡器,如 AWS Elastic Load Balancer 服务。

  • 服务扩展和滚动更新:您可以轻松地扩展和缩小您的服务,当需要更新您的服务时,Docker 支持智能的滚动更新功能,并在部署失败时支持回滚。

  • 声明式服务模型:Docker Swarm 使用流行的 Docker Compose 规范来声明性地定义应用程序服务、网络、卷等,以易于理解和维护的格式。

  • 期望状态:Docker Swarm 持续监视应用程序和运行时状态,确保您配置的服务按照期望的状态运行。例如,如果您配置一个具有 2 个实例或副本计数的服务,Docker Swarm 将始终尝试维持这个计数,并在现有节点失败时自动部署新的副本到新节点。

  • 生产级运维功能,如秘密和配置管理:一些功能,如 Docker 秘密和 Docker 配置,是 Docker Swarm 独有的,并为实际的生产问题提供解决方案,例如安全地将秘密和配置数据分发给您的应用程序。

在 AWS 上运行 Docker Swarm 时,Docker 提供了一个名为 Docker for AWS CE 的社区版产品,您可以在store.docker.com/editions/community/docker-ce-aws找到更多信息。目前,Docker for AWS CE 是通过预定义的 CloudFormation 模板部署的,该模板将 Docker Swarm 与许多 AWS 服务集成在一起,包括 EC2 自动扩展、弹性负载均衡、弹性文件系统和弹性块存储。很快您将会看到,这使得在 AWS 上快速搭建一个新的 Docker Swarm 集群变得非常容易。

Docker Swarm 与 Kubernetes 的比较

首先,正如本书的大部分内容所证明的那样,我是一个 ECS 专家,如果您的容器工作负载完全在 AWS 上运行,那么我的建议,至少在撰写本书时,几乎总是会选择 ECS。然而,许多组织不想被锁定在 AWS 上,他们希望采用云无关的方法,这就是 Docker Swarm 目前是其中一种领先的解决方案的原因。

目前,Docker Swarm 与 Kubernetes 直接竞争,我们将在下一章讨论。可以说,Kubernetes 似乎已经确立了自己作为首选的云无关容器管理平台,但这并不意味着您一定要忽视 Docker Swarm。

总的来说,我个人认为 Docker Swarm 更容易设置和使用,至少对我来说,一个关键的好处是它使用熟悉的工具,比如 Docker Compose,这意味着你可以非常快速地启动和运行,特别是如果你之前使用过这些工具。对于只想快速启动并确保事情顺利进行的较小组织来说,Docker Swarm 是一个有吸引力的选择。Docker for AWS 解决方案使在 AWS 中建立 Docker Swarm 集群变得非常容易,尽管 AWS 最近通过推出弹性 Kubernetes 服务(EKS)大大简化了在 AWS 上使用 Kubernetes 的过程——关于这一点,我们将在下一章中详细介绍。

最终,我鼓励你以开放的心态尝试两者,并根据你和你的组织目标的最佳容器管理平台做出自己的决定。

安装 Docker for AWS

在 AWS 中快速启动 Docker Swarm 的推荐方法是使用 Docker for AWS,你可以在docs.docker.com/docker-for-aws/上了解更多。如果你浏览到这个页面,在设置和先决条件部分,你将看到允许你安装 Docker 企业版(EE)和 Docker 社区版(CE)for AWS 的链接。

我们将使用免费的 Docker CE for AWS(稳定版),请注意你可以选择部署到全新的 VPC 或现有的 VPC:

选择 Docker CE for AWS 选项

鉴于我们已经有一个现有的 VPC,如果你点击部署 Docker CE for AWS(稳定版)用户现有的 VPC 选项,你将被重定向到 AWS CloudFormation 控制台,在那里你将被提示使用 Docker 发布的模板创建一个新的堆栈:

创建 Docker for AWS 堆栈

点击下一步后,你将被提示指定一些参数,这些参数控制了你的 Docker Swarm Docker 安装的配置。我不会描述所有可用的选项,所以假设对于我没有提到的任何参数,你应该保留默认配置。

  • 堆栈名称:为你的堆栈指定一个合适的名称,例如 docker-swarm。

  • Swarm Size: 在这里,您可以指定 Swarm 管理器和工作节点的数量。最少可以指定一个管理器,但我建议还配置一个工作节点,以便您可以测试将应用程序部署到多节点 Swarm 集群。

  • Swarm Properties: 在这里,您应该配置 Swarm EC2 实例以使用您现有的管理员 SSH 密钥(EC2 密钥对),并启用创建 EFS 存储属性的先决条件,因为我们将在本章后面使用 EFS 提供共享卷。

  • Swarm Manager Properties: 将 Manager 临时存储卷类型更改为 gp2(SSD)。

  • Swarm Worker Properties: 将工作节点临时存储卷类型更改为 gp2(SSD)。

  • VPC/网络: 选择现有的默认 VPC,然后确保您指定选择 VPC 时显示的 VPC CIDR 范围(例如172.31.0.0/16),然后从默认 VPC 中选择适当的子网作为公共子网 1 至 3。

完成上述配置后,点击两次“下一步”按钮,最后在“审阅”屏幕上,选择“我承认 AWS CloudFormation 可能创建 IAM 资源”选项,然后点击“创建”按钮。

此时,您的新 CloudFormation 堆栈将被创建,并且应在 10-15 分钟内完成。请注意,如果您想要增加集群中的管理器和/或工作节点数量,建议的方法是执行 CloudFormation 堆栈更新,修改定义管理器和工作节点计数的适当输入参数。另外,要升级 Docker for AWS Swarm Cluster,您应该应用包含 Docker Swarm 和其他各种资源更新的最新 CloudFormation 模板。

由 Docker for AWS CloudFormation 堆栈创建的资源

如果您在 CloudFormation 控制台的新堆栈中查看资源选项卡,您将注意到创建了各种资源,其中最重要的资源列在下面:

  • CloudWatch 日志组: 这存储了通过您的 Swarm 集群安排的所有容器日志。只有在堆栈创建期间启用了使用 Cloudwatch 进行容器日志记录参数时(默认情况下,此参数已启用),才会创建此资源。

  • 外部负载均衡器: 创建了一个经典的弹性负载均衡器,用于发布对您的 Docker 应用程序的公共访问。

  • 弹性容器注册表 IAM 策略:创建了一个 IAM 策略,并附加到所有 Swarm 管理器和工作节点 EC2 实例角色,允许对 ECR 进行读取/拉取访问。如果您将 Docker 镜像存储在 ECR 中,这是必需的,适用于我们的场景。

  • 其他资源:还创建了各种资源,例如用于集群管理操作的 DynamoDB 表,以及用于 EC2 自动扩展生命周期挂钩的简单队列服务(SQS)队列,用于 Swarm 管理器升级场景。

如果单击“输出”选项卡,您会注意到一个名为 DefaultDNSTarget 的输出属性,它引用了外部负载均衡器的公共 URL。请注意这个 URL,因为稍后在本章中,示例应用将可以从这里访问:

Docker for AWS 堆栈输出

访问 Swarm 集群

在 CloudFormation 堆栈输出中,还有一个名为 Managers 的属性,它提供了指向每个 Swarm 管理器的 EC2 实例的链接:

Swarm Manager 自动扩展组

您可以使用这些信息来获取您的 Swarm 管理器之一的公共 IP 地址或 DNS 名称。一旦您有了这个 IP 地址,您就可以建立到管理器的 SSH 连接。

> ssh -i ~/.ssh/admin.pem docker@54.145.175.148
The authenticity of host '54.145.175.148 (54.145.175.148)' can't be established.
ECDSA key fingerprint is SHA256:Br/8IMAuEzPOV29B8zdbT6H+DjK9sSEEPSbXdn+v0YM.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '54.145.175.148' (ECDSA) to the list of known hosts.
Welcome to Docker!
~ $ docker ps --format "{{ .ID }}: {{ .Names }}"
a5a2dfe609e4: l4controller-aws
0d7f5d2ae4a0: meta-aws
d54308064314: guide-aws
58cb47dad3e1: shell-aws

请注意,当访问管理器时,您必须指定一个用户名为docker,如果运行docker ps命令,您会看到默认情况下管理器上运行着四个系统容器:

  • shell-aws:这提供了对管理器的 SSH 访问,这意味着您建立到 Swarm 管理器的 SSH 会话实际上是在这个容器内运行的。

  • meta-aws:提供通用的元数据服务,包括提供允许新成员加入集群的令牌。

  • guide-aws:执行集群状态管理操作,例如将每个管理器添加到 DynamoDB,以及其他诸如清理未使用的镜像和卷以及停止的容器等日常任务。

  • l4controller-aws:管理与 Swarm 集群的外部负载均衡器的集成。该组件负责发布新端口,并确保它们可以在弹性负载均衡器上访问。请注意,您不应直接修改集群的 ELB,而应依赖l4controller-aws组件来管理 ELB。

要查看和访问集群中的其他节点,您可以使用docker node ls命令:

> docker node ls
ID                         HOSTNAME                      STATUS   MANAGER STATUS   ENGINE VERSION
qna4v46afttl007jq0ec712dk  ip-172-31-27-91.ec2.internal  Ready                     18.03.0-ce
ym3jdy1ol17pfw7emwfen0b4e* ip-172-31-40-246.ec2.internal Ready    Leader           18.03.0-ce
> ssh docker@ip-172-31-27-91.ec2.internal Permission denied (publickey,keyboard-interactive).

请注意,工作节点不允许公共 SSH 访问,因此您只能通过管理器从 SSH 访问工作节点。然而,有一个问题:鉴于管理节点没有本地存储管理员 EC2 密钥对的私钥,您无法建立与工作节点的 SSH 会话。

设置本地访问 Docker Swarm

虽然您可以通过 SSH 会话远程运行 Docker 命令到 Swarm 管理器,但是能够使用本地 Docker 客户端与远程 Swarm 管理器守护程序进行交互要容易得多,在那里您可以访问本地 Docker 服务定义和配置。我们还有一个问题,即无法通过 SSH 访问工作节点,我们可以通过使用 SSH 代理转发和 SSH 隧道这两种技术来解决这两个问题。

配置 SSH 代理转发

设置 SSH 代理转发,首先使用ssh-add命令将您的管理员 SSH 密钥添加到本地 SSH 代理中:

> ssh-add -K ~/.ssh/admin.pem
Identity added: /Users/jmenga/.ssh/admin.pem (/Users/jmenga/.ssh/admin.pem)
> ssh-add -L
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF7aAzIRayGHiiR81wcz/k9b+ZdmAEkdIBU0pOvAaFYjrDPf4JL4I0rJjdpFBjFZIqKXM9dLWg0skENYSUl9pfLT+CzValQat/XpBw/HfwzbzMy8wqcKehN0pB4V1bpzfOYe7lTLmTYIQ/21wW63QVlZnNyV1VZiVgN5DcLqgiG5CHHAooMIbiExAYvRrgo8XEXoqFRODLwIn4HZ7OAtojWzxElBx+EC4lmDekykgxnfGd30QgATIEF8/+UzM17j91JJohfxU7tA3GhXkScMBXnxBhdOftVvtB8/bGc+DHjJlkYSxL20792eBEv/ZsooMhNFxGLGhidrznmSeC8qL /Users/jmenga/.ssh/admin.pem

-K标志是特定于 macOS 的,并将您的 SSH 密钥的密码添加到您的 OS X 钥匙串中,这意味着此配置将在重新启动后持续存在。如果您不使用 macOS,可以省略-K标志。

现在您可以使用-A标志访问您的 Swarm 管理器,该标志配置 SSH 客户端使用您的 SSH 代理身份。使用 SSH 代理还可以启用 SSH 代理转发,这意味着用于与 Swarm 管理器建立 SSH 会话的 SSH 密钥可以自动用于或转发到您可能在 SSH 会话中建立的其他 SSH 连接:

> ssh -A docker@54.145.175.148
Welcome to Docker!
~ $ ssh docker@ip-172-31-27-91.ec2.internal
Welcome to Docker!

如您所见,使用 SSH 代理转发解决了访问工作节点的问题。

配置 SSH 隧道

SSH 隧道是一种强大的技术,允许您通过加密的 SSH 会话安全地隧道网络通信到远程主机。 SSH 隧道通过暴露一个本地套接字或端口,该套接字或端口连接到远程主机上的远程套接字或端口。这可以产生您正在与本地服务通信的错觉,当与 Docker 一起工作时特别有用。

以下命令演示了如何使运行在 Swarm 管理器上的 Docker 套接字显示为运行在本地主机上的端口:

> ssh -i ~/.ssh/admin.pem -NL localhost:2374:/var/run/docker.sock docker@54.145.175.148 &
[1] 7482
> docker -H localhost:2374 ps --format "{{ .ID }}: {{ .Names }}"
a5a2dfe609e4: l4controller-aws
0d7f5d2ae4a0: meta-aws
d54308064314: guide-aws
58cb47dad3e1: shell-aws
> export DOCKER_HOST=localhost:2374
> docker node ls --format "{{ .ID }}: {{ .Hostname }}" qna4v46afttl007jq0ec712dk: ip-172-31-27-91.ec2.internal
ym3jdy1ol17pfw7emwfen0b4e: ip-172-31-40-246.ec2.internal

传递给第一个 SSH 命令的-N标志指示客户端不发送远程命令,而-L或本地转发标志配置了将本地主机上的 TCP 端口2374映射到远程 Swarm 管理器上的/var/run/docker.sock Docker Engine 套接字。命令末尾的和符号(&)使命令在后台运行,并将进程 ID 作为此命令的输出发布。

有了这个配置,现在您可以运行 Docker 客户端,本地引用localhost:2374作为连接到远程 Swarm 管理器的本地端点。请注意,您可以使用-H标志指定主机,也可以通过导出环境变量DOCKER_HOST来指定主机。这将允许您在引用本地文件的同时执行远程 Docker 操作,从而更轻松地管理和部署到 Swarm 集群。

尽管 Docker 确实包括了一个客户端/服务器模型,可以在 Docker 客户端和远程 Docker Engine 之间进行通信,但要安全地进行这样的通信需要相互传输层安全性(TLS)和公钥基础设施(PKI)技术,这些技术设置和维护起来很复杂。使用 SSH 隧道来暴露远程 Docker 套接字要容易得多,而且被认为与任何形式的远程 SSH 访问一样安全。

将应用程序部署到 Docker Swarm

现在您已经使用 Docker for AWS 安装了 Docker Swarm,并建立了与 Swarm 集群的管理连接,我们准备开始部署应用程序。将应用程序部署到 Docker Swarm 需要使用docker servicedocker stack命令,这些命令在本书中尚未涉及,因此在处理 todobackend 应用程序的部署之前,我们将通过部署一些示例应用程序来熟悉这些命令。

Docker 服务

尽管您在 Swarm 集群中可以技术上部署单个容器,但应避免这样做,并始终使用 Docker 服务作为部署到 Swarm 集群的标准单位。实际上,我们已经使用 Docker Compose 来使用 Docker 服务,但是与 Docker Swarm 一起使用时,它们被提升到了一个新的水平。

要创建一个 Docker 服务,您可以使用docker service create命令,下面的示例演示了如何使用流行的 Nginx Web 服务器搭建一个非常简单的 Web 应用程序:

> docker service create --name nginx --publish published=80,target=80 --replicas 2 nginx ez24df69qb2yq1zhyxma38dzo
overall progress: 2 out of 2 tasks
1/2: running [==================================================>]
2/2: running [==================================================>]
verify: Service converged
> docker service ps --format "{{ .ID }} ({{ .Name }}): {{ .Node }} {{ .CurrentState }}" nginx 
wcq6jfazrums (nginx.1): ip-172-31-27-91.ec2.internal  Running 2 minutes ago
i0vj5jftf6cb (nginx.2): ip-172-31-40-246.ec2.internal Running 2 minutes ago

--name标志为服务提供了友好的名称,而--publish标志允许您发布服务将从中访问的外部端口(在本例中为端口80)。--replicas标志定义了服务应部署多少个容器,最后您指定了要运行的服务的图像的名称(在本例中为 nginx)。请注意,您可以使用docker service ps命令来列出运行服务的各个容器和节点。

如果现在尝试浏览外部负载均衡器的 URL,您应该收到默认的Welcome to nginx!网页:

Nginx 欢迎页面要删除一个服务,您可以简单地使用docker service rm命令:

> docker service rm nginx
nginx

Docker 堆栈

Docker 堆栈被定义为一个复杂的、自包含的环境,由多个服务、网络和/或卷组成,并在 Docker Compose 文件中定义。

一个很好的 Docker 堆栈的例子,将立即为我们的 Swarm 集群增加一些价值,是一个名为swarmpit的开源 Swarm 管理工具,您可以在swarmpit.io/上了解更多。要开始使用 swarmpit,请克隆github.com/swarmpit/swarmpit存储库到本地文件夹,然后打开存储库根目录中的docker-compose.yml文件。

version: '3.6'

services:

  app:
    image: swarmpit/swarmpit:latest
    environment:
      - SWARMPIT_DB=http://db:5984
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
 - target: 8080
 published: 8888
 mode: ingress
    networks:
      - net
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 1024M
        reservations:
          cpus: '0.25'
          memory: 512M
      placement:
        constraints:
          - node.role == manager

  db:
    image: klaemo/couchdb:2.0.0
    volumes:
      - db-data:/opt/couchdb/data
    networks:
      - net
    deploy:
      resources:
        limits:
          cpus: '0.30'
          memory: 512M
        reservations:
          cpus: '0.15'
          memory: 256M
 placement:
 constraints:
 - node.role == manager

  agent:
    ...
    ...

networks:
  net:
    driver: overlay

volumes:
  db-data:
    driver: local

我已经突出显示了对文件的修改,即将 Docker Compose 文件规范版本更新为 3.6,修改 app 服务的端口属性,以便在端口 8888 上外部发布管理 UI,并确保数据库仅部署到集群中的 Swarm 管理器。固定数据库的原因是确保在任何情况下,如果数据库容器失败,Docker Swarm 将尝试将数据库容器重新部署到存储本地数据库卷的同一节点。

如果您意外地擦除了 swarmpit 数据库,请注意管理员密码将被重置为默认值 admin,如果您已将 swarmpit 管理界面发布到公共互联网上,这将构成重大安全风险。

有了这些更改,现在可以运行docker stack deploy命令来部署 swarmpit 管理应用程序:

> docker stack deploy -c docker-compose.yml swarmpit
Creating network swarmpit_net
Creating service swarmpit_agent
Creating service swarmpit_app
Creating service swarmpit_db
> docker stack services swarmpit
ID            NAME            MODE        REPLICAS  IMAGE                     PORTS
8g5smxmqfc6a  swarmpit_app    replicated  1/1       swarmpit/swarmpit:latest  *:8888->8080/tcp
omc7ewvqjecj  swarmpit_db     replicated  1/1
klaemo/couchdb:2.0.0
u88gzgeg8rym  swarmpit_agent  global      2/2       swarmpit/agent:latest

您可以看到docker stack deploy命令比docker service create命令简单得多,因为 Docker Compose 文件包含了所有的服务配置细节。在端口 8888 上浏览您的外部 URL,并使用默认用户名和密码admin/admin登录,然后立即通过选择右上角的管理员下拉菜单并选择更改密码来更改管理员密码。更改管理员密码后,您可以查看 swarmpit 管理 UI,该界面提供了有关您的 Swarm 集群的大量信息。以下截图展示了基础设施 | 节点页面,其中列出了集群中的节点,并显示了每个节点的详细信息:

swarmkit 管理界面

将示例应用部署到 Docker Swarm

我们现在进入了本章的业务端,即将我们的示例 todobackend 应用部署到新创建的 Docker swarm 集群。正如你所期望的那样,我们将遇到一些挑战,需要执行以下配置任务:

  • 将 Docker Swarm 集成到弹性容器注册表

  • 定义堆栈

  • 创建用于托管静态内容的共享存储

  • 创建 collectstatic 服务

  • 创建用于存储 todobackend 数据库的持久性存储

  • 使用 Docker Swarm 进行秘密管理

  • 运行数据库迁移

将 Docker Swarm 集成到弹性容器注册表

todobackend 应用已经发布在现有的弹性容器注册表(ECR)存储库中,理想情况下,我们希望能够集成我们的 Docker swarm 集群,以便我们可以从 ECR 拉取私有镜像。截至撰写本书时,ECR 集成在某种程度上得到支持,即您可以在部署时将注册表凭据传递给 Docker swarm 管理器,这些凭据将在集群中的所有节点之间共享。但是,这些凭据在 12 小时后会过期,目前没有本机机制来自动刷新这些凭据。

为了定期刷新 ECR 凭据,以便您的 Swarm 集群始终可以从 ECR 拉取镜像,您需要执行以下操作:

  • 确保您的管理器和工作节点具有从 ECR 拉取的权限。Docker for AWS CloudFormation 模板默认配置了此访问权限,因此您不必担心配置此项。

  • docker-swarm-aws-ecr-auth自动登录系统容器部署为服务,发布在github.com/mRoca/docker-swarm-aws-ecr-auth。安装后,此服务会自动刷新集群中所有节点上的 ECR 凭据。

要部署docker-swarm-aws-ecr-auth服务,您可以使用以下docker service create命令:

> docker service create \
    --name aws_ecr_auth \
    --mount type=bind,source=/var/run/docker.sock,destination=/var/run/docker.sock \
    --constraint 'node.role == manager' \
    --restart-condition 'none' \
    --detach=false \
    mroca/swarm-aws-ecr-auth
lmf37a9pbzc3nzhe88s1nzqto
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged

请注意,一旦此服务启动运行,您必须为使用 ECR 镜像部署的任何服务包括--with-registry-auth标志。

以下代码演示了使用docker service create命令部署 todobackend 应用程序,以及--with-registry-auth标志:

> export AWS_PROFILE=docker-in-aws
> $(aws ecr get-login --no-include-email)
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
> docker service create --name todobackend --with-registry-auth \
 --publish published=80,target=8000 --env DJANGO_SETTINGS_MODULE=todobackend.settings_release\
 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend \
 uwsgi --http=0.0.0.0:8000 --module=todobackend.wsgi p71rje93a6pqvipqf2a14v6cc
overall progress: 1 out of 1 tasks
1/1: running [==================================================>]
verify: Service converged

您可以通过浏览到外部负载均衡器 URL 来验证 todobackend 服务确实已部署:

部署 todobackend 服务

请注意,因为我们还没有生成任何静态文件,todobackend 服务缺少静态内容。稍后当我们创建 Docker Compose 文件并为 todobackend 应用程序部署堆栈时,我们将解决这个问题。

定义一个堆栈

虽然您可以使用docker service create等命令部署服务,但是您可以使用docker stack deploy命令非常快速地部署完整的多服务环境,引用捕获各种服务、网络和卷配置的 Docker Compose 文件,构成您的堆栈。将堆栈部署到 Docker Swarm 需要 Docker Compose 文件规范的版本 3,因此我们不能使用todobackend存储库根目录下的现有docker-compose.yml文件来定义我们的 Docker Swarm 环境,并且我建议保持开发和测试工作流分开,因为 Docker Compose 版本 2 规范专门支持适用于持续交付工作流的功能。

现在,让我们开始为 todobackend 应用程序定义一个堆栈,我们可以通过在todobackend存储库的根目录创建一个名为stack.yml的文件来部署到 AWS 的 Docker Swarm 集群中:

version: '3.6'

networks:
  net:
    driver: overlay

services:
  app:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
    ports:
      - target: 8000
        published: 80
    networks:
      - net
    environment:
      DJANGO_SETTINGS_MODULE: todobackend.settings_release
    command:
      - uwsgi
      - --http=0.0.0.0:8000
      - --module=todobackend.wsgi
      - --master
      - --die-on-term
      - --processes=4
      - --threads=2
      - --check-static=/public

    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 30s

我们指定的第一个属性是强制性的version属性,我们将其定义为 3.6 版本,这是在撰写本书时支持的最新版本。接下来,我们配置顶级网络属性,该属性指定了堆栈将使用的 Docker 网络。您将创建一个名为net的网络,该网络实现了overlay驱动程序,该驱动程序在 Swarm 集群中的所有节点之间创建了一个虚拟网络段,堆栈中定义的各种服务可以在其中相互通信。通常,您部署的每个堆栈都应该指定自己的覆盖网络,这样可以在每个堆栈之间提供分割,并且无需担心集群的 IP 寻址或物理网络拓扑。

接下来,您必须定义一个名为app的单个服务,该服务代表了主要的 todobackend web 应用程序,并通过image属性指定了您在之前章节中发布的 todobackend 应用程序的完全限定名称的 ECR 镜像。请注意,Docker 堆栈不支持build属性,必须引用已发布的 Docker 镜像,这是为什么您应该始终为开发、测试和构建工作流程分别拥有单独的 Docker Compose 规范的一个很好的理由。

ports属性使用了长格式配置语法(在之前的章节中,您使用了短格式语法),这提供了更多的配置选项,允许您指定容器端口 8000(由target属性指定)将在端口 80 上对外发布(由published属性指定),而networks属性配置app服务附加到您之前定义的net网络。请注意,environment属性没有指定任何数据库配置设置,现在的重点只是让应用程序运行起来,尽管状态可能有些混乱,但我们将在本章后面配置数据库访问。

最后,deploy属性允许您控制服务的部署方式,replica属性指定部署两个服务实例,update_config属性配置滚动更新,以便一次更新一个实例(由parallelism属性指定),每个更新实例之间延迟 30 秒。

有了这个配置,您现在可以使用docker stack deploy命令部署您的堆栈了:

> $(aws ecr get-login --no-include-email)
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
> docker stack deploy --with-registry-auth -c stack.yml todobackend Creating network todobackend_net
Creating service todobackend_app
> docker service ps todobackend_app --format "{{ .Name }} -> {{ .Node }} ({{ .CurrentState }})"
todobackend_app.1 -> ip-172-31-27-91.ec2.internal (Running 6 seconds ago)
todobackend_app.2 -> ip-172-31-40-246.ec2.internal (Running 6 seconds ago)

请注意,我首先登录到 ECR——这一步并非绝对必需,但如果未登录到 ECR,Docker 客户端将无法确定与最新标签关联的当前图像哈希,并且会出现以下警告:

> docker stack deploy --with-registry-auth -c stack.yml todobackend image 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest could not be accessed on a registry to record
its digest. Each node will access 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:latest independently,
possibly leading to different nodes running different
versions of the image.
...
...

如果您现在浏览外部负载均衡器 URL,todobackend 应用程序应该加载,但您会注意到应用程序缺少静态内容,如果您尝试访问 /todos,将会出现数据库配置错误,这是可以预料的,因为我们尚未配置任何数据库设置或考虑如何在 Docker Swarm 中运行 collectstatic 过程。

为托管静态内容创建共享存储

Docker for AWS 解决方案包括 Cloudstor 卷插件,这是由 Docker 构建的存储插件,旨在支持流行的云存储机制以实现持久存储。

在 AWS 的情况下,此插件提供了与以下类型的持久存储的开箱即用集成:

  • 弹性块存储EBS):提供面向专用(非共享)访问的块级存储。这提供了高性能,能够将卷分离和附加到不同的实例,并支持快照和恢复操作。EBS 存储适用于数据库存储或任何需要高吞吐量和最小延迟来读写本地数据的应用程序。

  • 弹性文件系统EFS):使用 网络文件系统NFS)版本 4 协议提供共享文件系统访问。NFS 允许在多个主机之间同时共享存储,但这比 EBS 存储要低得多。NFS 存储适用于共享常见文件并且不需要高性能的应用程序。在之前部署 Docker for AWS 解决方案时,您选择了为 EFS 创建先决条件,这为 Cloudstor 卷插件集成了一个用于 Swarm 集群的 EFS 文件系统。

正如您在之前的章节中所了解的,todobackend 应用程序对存储静态内容有特定要求,尽管我通常不建议将 EFS 用于这种用例,但静态内容的要求代表了一个很好的机会,可以演示如何在 Docker Swarm 环境中配置和使用 EFS 作为共享卷。

version: '3.6'

networks:
  net:
    driver: overlay

volumes:
 public:
 driver: cloudstor:aws
 driver_opts:
 backing: shared

services:
  app:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
    ports:
      - target: 8000
        published: 80
    networks:

      - net
 volumes:
 - public:/public
    ...
    ...

首先,您必须创建一个名为public的卷,并指定驱动程序为cloudstor:aws,这可以确保 Cloudstor 驱动程序加载了 AWS 支持。要创建一个 EFS 卷,您只需配置一个名为backing的驱动选项,值为shared,然后在app服务中挂载到/public

如果您现在使用docker stack deploy命令部署您的更改,卷将被创建,并且app服务实例将被更新:

> docker stack deploy --with-registry-auth -c stack.yml todobackend
Updating service todobackend_app (id: 59gpr2x9n7buikeorpf0llfmc)
> docker volume ls
DRIVER          VOLUME NAME
local           bd3d2804c796064d6e7c4040040fd474d9adbe7aaf68b6e30b1d195b50cdefde
local           sshkey
cloudstor:aws   todobackend_public
>  docker service ps todobackend_app \
 --format "{{ .Name }} -> {{ .DesiredState }} ({{ .CurrentState }})"
todobackend_app.1 -> Running (Running 44 seconds ago)
todobackend_app.1 -> Shutdown (Shutdown 45 seconds ago)
todobackend_app.2 -> Running (Running 9 seconds ago)
todobackend_app.2 -> Shutdown (Shutdown 9 seconds ago)

您可以使用docker volume ls命令查看当前卷,您会看到一个新的卷,根据约定命名为<stack name>_<volume name>(例如,todobackend_public),并且驱动程序为cloudstor:aws。请注意,docker service ps命令输出显示todobackend.app.1首先被更新,然后 30 秒后todobackend.app.2被更新,这是基于您在app服务的deploy设置中应用的早期滚动更新配置。

要验证卷是否成功挂载,您可以使用docker ps命令查询 Swarm 管理器上运行的任何 app 服务容器,然后使用docker exec来验证/public挂载是否存在,并且app用户可以读写 todobackend 容器运行的。

> docker ps -f name=todobackend -q
60b33d8b0bb1
> docker exec -it 60b33d8b0bb1 touch /public/test
> docker exec -it 60b33d8b0bb1 ls -l /public
total 4
-rw-r--r-- 1 app app 0 Jul 19 13:45 test

一个重要的要点是,在前面的示例中显示的docker volume和其他docker命令只在您连接的当前 Swarm 节点的上下文中执行,并且不会显示卷或允许您访问集群中其他节点上运行的容器。要验证卷确实是共享的,并且可以被我们集群中其他 Swarm 节点上运行的 app 服务容器访问,您需要首先 SSH 到 Swarm 管理器,然后 SSH 到集群中的单个工作节点:

> ssh -A docker@54.145.175.148
Welcome to Docker!
~ $ docker node ls
ID                          HOSTNAME                        STATUS  MANAGER  STATUS
qna4v46afttl007jq0ec712dk   ip-172-31-27-91.ec2.internal    Ready   Active 
ym3jdy1ol17pfw7emwfen0b4e * ip-172-31-40-246.ec2.internal   Ready   Active   Leader
> ssh docker@ip-172-31-27-91.ec2.internal
Welcome to Docker!
> docker ps -f name=todobackend -q
71df5495080f
~ $ docker exec -it 71df5495080f ls -l /public
total 4
-rw-r--r-- 1 app app 0 Jul 19 13:58 test
~ $ docker exec -it 71df5495080f rm /public/test

正如您所看到的,该卷在工作节点上是可用的,可以看到我们在另一个实例上创建的/public/test文件,证明该卷确实是共享的,并且可以被所有app服务实例访问,而不管底层节点如何。

创建一个 collectstatic 服务

现在您已经有了一个共享卷,我们需要考虑如何定义和执行 collectstatic 过程来生成静态内容。迄今为止,在本书中,您已经将 collectstatic 过程作为一个需要在定义的部署序列中的特定时间发生的命令式任务执行,然而 Docker Swarm 提倡最终一致性的概念,因此您应该能够部署您的堆栈,并且有一个可能失败但最终会成功的 collectstatic 过程运行,此时达到了应用程序的期望状态。这种方法与我们之前采取的命令式方法非常不同,但被认为是良好架构的现代云原生应用程序的最佳实践。

为了演示这是如何工作的,我们首先需要拆除 todobackend 堆栈,这样您就可以观察在 Docker 存储引擎创建和挂载 EFS 支持的卷时 collectstatic 过程中将发生的失败:

> docker stack rm todobackend
Removing service todobackend_app
Removing network todobackend_net
> docker volume ls
DRIVER         VOLUME NAME
local          sshkey
cloudstor:aws  todobackend_public
> docker volume rm todobackend_public

需要注意的一点是,Docker Swarm 在销毁堆栈时不会删除卷,因此您需要手动删除卷以完全清理环境。

现在我们可以向堆栈添加一个 collectstatic 服务:

version: '3.6'

networks:
  net:
    driver: overlay

volumes:
  public:
    driver: cloudstor:aws
    driver_opts:
      backing: shared

services:
  app:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
    ports:
      - target: 8000
        published: 80
    networks:
      - net
    volumes:
      - public:/public
    ...
    ...
  collectstatic:
 image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend volumes:
 - public:/public    networks:
 - net
 environment:
 DJANGO_SETTINGS_MODULE: todobackend.settings_release
 command:
 - python3
 - manage.py
 - collectstatic
 - --no-input
 deploy:
 replicas: 1
 restart_policy:
 condition: on-failure
 delay: 30s
 max_attempts: 6

collectstatic 服务挂载 public 共享卷,并运行适当的 manage.py 任务来生成静态内容。在 deploy 部分,我们配置了一个副本数量为 1,因为 collectstatic 服务只需要在部署时运行一次,然后配置了一个 restart_policy,指定 Docker Swarm 在失败时应尝试重新启动服务,每次重新启动尝试之间间隔 30 秒,最多尝试 6 次。这提供了最终一致的行为,因为它允许 collectstatic 在 EFS 卷挂载操作正在进行时最初失败,然后在卷挂载和准备就绪后最终成功。

如果您现在部署堆栈并监视 collectstatic 服务,您可能会注意到一些最初的失败:

> docker stack deploy --with-registry-auth -c stack.yml todobackend
Creating network todobackend_default
Creating network todobackend_net
Creating service todobackend_collectstatic
Creating service todobackend_app
> docker service ps todobackend_collectstatic NAME                        NODE                          DESIRED STATE CURRENT STATE
todobackend_collectstatic.1 ip-172-31-40-246.ec2.internal Running       Running 2 seconds ago
\_ todobackend_collectstatic.1 ip-172-31-40-246.ec2.internal Shutdown     Rejected 32 seconds ago

docker service ps命令不仅显示当前服务状态,还显示服务历史(例如任何先前尝试运行服务),您可以看到 32 秒前第一次尝试运行collectstatic失败,之后 Docker Swarm 尝试重新启动服务。这次尝试成功了,尽管collectstatic服务最终会完成并退出,但由于重启策略设置为失败,Docker Swarm 不会尝试重新启动服务,因为服务没有错误退出。这支持了在失败时具有重试功能的“一次性”服务的概念,Swarm 尝试再次运行服务的唯一时机是在为服务部署新配置到集群时。

如果您现在浏览外部负载均衡器的 URL,您应该会发现 todobackend 应用程序的静态内容现在被正确呈现,但是数据库配置错误仍然存在。

创建用于存储应用程序数据库的持久存储

现在我们可以将注意力转向应用程序数据库,这是 todobackend 应用程序的一个基本支持组件。如果您在 AWS 上运行,我的典型建议是,无论容器编排平台如何,都要像我们在本书中一样使用关系数据库服务(RDS),但是 todobackend 应用程序对应用程序数据库的要求提供了一个机会,可以演示如何使用 Docker for AWS 解决方案支持持久存储。

除了 EFS 支持的卷之外,Cloudstor 卷插件还支持可重定位的弹性块存储(EBS)卷。可重定位意味着插件将自动将容器当前分配的 EBS 卷重新分配到另一个节点,以防 Docker Swarm 确定必须将容器从一个节点重新分配到另一个节点。在重新分配 EBS 卷时实际发生的情况取决于情况:

  • 新节点位于相同的可用区:插件只是从现有节点的 EC2 实例中分离卷,并在新节点上重新附加卷。

  • 新节点位于不同的可用区:在这里,插件对现有卷进行快照,然后从快照在新的可用区创建一个新卷。完成后,之前的卷将被销毁。

重要的是要注意,Docker 仅支持对可移动的 EBS 支持卷的单一访问,也就是说,在任何给定时间,应该只有一个容器读取/写入该卷。如果您需要对卷进行共享访问,那么必须创建一个 EFS 支持的共享卷。

现在,让我们定义一个名为data的卷来存储 todobackend 数据库,并创建一个db服务,该服务将运行 MySQL 并附加到data卷:

version: '3.6'

networks:
  net:
    driver: overlay

volumes:
  public:
    driver: cloudstor:aws
    driver_opts:
      backing: shared
 data:
 driver: cloudstor:aws
 driver_opts: 
 backing: relocatable
 size: 10
 ebstype: gp2

services:
  app:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
    ports:
      - target: 8000
        published: 80
    networks:
      - net
    volumes:
      - public:/public
    ...
    ...
  collectstatic:
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
    volumes:
      - public:/public
    ...
    ...
  db:
 image: mysql:5.7
 environment:
 MYSQL_DATABASE: todobackend
 MYSQL_USER: todo
 MYSQL_PASSWORD: password
 MYSQL_ROOT_PASSWORD: password
 networks:
 - net
 volumes:
 - data:/var/lib/mysql
 command:
 - --ignore-db-dir=lost+found
 deploy:
      replicas: 1
 placement:
 constraints:
 - node.role == manager

首先,我们创建一个名为data的卷,并将驱动程序配置为cloudstor:aws。在驱动程序选项中,我们指定了一个可移动的后端来创建一个 EBS 卷,指定了 10GB 的大小和gp2(SSD)存储的 EBS 类型。然后,我们定义了一个名为db的新服务,该服务运行官方的 MySQL 5.7 镜像,将db服务附加到先前定义的 net 网络,并将数据卷挂载到/var/lib/mysql,这是 MySQL 存储其数据库的位置。请注意,由于 Cloudstor 插件将挂载的卷格式化为ext4,在格式化过程中会自动创建一个名为lost+found的文件夹,这会导致MySQL 容器中止,因为它认为存在一个名为lost+found的现有数据库。

为了克服这一点,我们传入一个称为--ignore-db-dir的单个标志,该标志引用lost+found文件夹,该文件夹传递给 MySQL 镜像入口点,并配置 MySQL 守护进程忽略此文件夹。

最后,我们定义了一个放置约束,将强制db服务部署到 Swarm 管理器,这将允许我们通过将此放置约束更改为工作程序来测试数据卷的可移动特性。

如果您现在部署堆栈并监视db服务,您应该观察到服务需要一些时间才能启动,同时数据卷正在初始化:

> docker stack deploy --with-registry-auth -c stack.yml todobackend
docker stack deploy --with-registry-auth -c stack.yml todobackend
Updating service todobackend_app (id: 28vrdqcsekdvoqcmxtum1eaoj)
Updating service todobackend_collectstatic (id: sowciy4i0zuikf93lmhi624iw)
Creating service todobackend_db
> docker service ps todobackend_db --format "{{ .Name }} ({{ .ID }}): {{ .CurrentState }}" todobackend_db.1 (u4upsnirpucs): Preparing 35 seconds ago
> docker service ps todobackend_db --format "{{ .Name }} ({{ .ID }}): {{ .CurrentState }}"
todobackend_db.1 (u4upsnirpucs): Running 2 seconds ago

要验证 EBS 卷是否已创建,可以使用 AWS CLI 如下:

> aws ec2 describe-volumes --filters Name=tag:CloudstorVolumeName,Values=* \
    --query "Volumes[*].{ID:VolumeId,Zone:AvailabilityZone,Attachment:Attachments,Tag:Tags}"
[
    {
        "ID": "vol-0db01995ba87433b3",
        "Zone": "us-east-1b",
        "Attachment": [
            {
                "AttachTime": "2018-07-20T09:58:16.000Z",
                "Device": "/dev/xvdf",
                "InstanceId": "i-0dc762f73f8ce4abf",
                "State": "attached",
                "VolumeId": "vol-0db01995ba87433b3",
                "DeleteOnTermination": false
            }
        ],
        "Tag": [
            {
                "Key": "CloudstorVolumeName",
                "Value": "todobackend_data"
            },
            {
                "Key": "StackID",
                "Value": "0825319e9d91a2fc0bf06d2139708b1a"
            }
        ]
    }
]

请注意,由 Cloudstor 插件创建的 EBS 卷标记为CloudstorVolumeName的键和 Docker Swarm 卷名称的值。在上面的示例中,您还可以看到该卷已在 us-east-1b 可用区创建。

迁移 EBS 卷

现在,您已成功创建并附加了一个 EBS 支持的数据卷,让我们通过更改其放置约束来测试将db服务从管理节点迁移到工作节点:

version: '3.6'
...
...
services:
  ...
  ...
  db:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: todobackend
      MYSQL_USER: todo
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: password
    networks:
      - net
    volumes:
      - data:/var/lib/mysql
    command:
      - --ignore-db-dir=lost+found
    deploy:
      replicas: 1
      placement:
        constraints:
 - node.role == worker

如果你现在部署你的更改,你应该能够观察到 EBS 迁移过程:

> volumes='aws ec2 describe-volumes --filters Name=tag:CloudstorVolumeName,Values=*
 --query "Volumes[*].{ID:VolumeId,State:Attachments[0].State,Zone:AvailabilityZone}"
 --output text' > snapshots='aws ec2 describe-snapshots --filters Name=status,Values=pending
    --query "Snapshots[].{Id:VolumeId,Progress:Progress}" --output text' > docker stack deploy --with-registry-auth -c stack.yml todobackend
Updating service todobackend_app (id: 28vrdqcsekdvoqcmxtum1eaoj)
Updating service todobackend_collectstatic (id: sowciy4i0zuikf93lmhi624iw)
Updating service todobackend_db (id: 4e3sc0dlot9lxlmt5kwfw3sis)
> eval $volumes vol-0db01995ba87433b3 detaching us-east-1b
> eval $volumes vol-0db01995ba87433b3 None us-east-1b
> eval $snapshots vol-0db01995ba87433b3 76%
> eval $snapshots
vol-0db01995ba87433b3 99%
> eval $volumes vol-0db01995ba87433b3 None us-east-1b
vol-07e328572e6223396 None us-east-1a
> eval $volume
vol-07e328572e6223396 None us-east-1a
> eval $volume
vol-07e328572e6223396 attached us-east-1a
> docker service ps todobackend_db --format "{{ .Name }} ({{ .ID }}): {{ .CurrentState }}"
todobackend_db.1 (a3i84kwz45w9): Running 1 minute ago
todobackend_db.1 (u4upsnirpucs): Shutdown 2 minutes ago

我们首先定义一个volumes查询,显示当前 Cloudstor 卷的状态,以及一个snapshots查询,显示任何正在进行中的 EBS 快照。在部署放置约束更改后,我们运行卷查询多次,并观察当前位于us-east-1b的卷,过渡到分离状态,然后到状态(分离)。

然后我们运行快照查询,在那里你可以看到一个快照正在为刚刚分离的卷创建,一旦这个快照完成,我们运行卷查询多次来观察旧卷被移除并且在us-east-1a创建了一个新卷,然后被附加。在这一点上,todobackend_data卷已经从us-east-1b的管理者迁移到了us-east-1a,你可以通过执行docker service ps命令来验证db服务现在已经重新启动并运行。

由于 Docker for AWS CloudFormation 模板为管理者和工作者创建了单独的自动扩展组,有可能管理者和工作者正在相同的子网和可用区中运行,这将改变上面示例的行为。

在我们继续下一节之前,实际上我们需要拆除我们的堆栈,因为在我们的堆栈文件中使用明文密码的当前密码管理策略并不理想,而且我们的数据库已经使用这些密码进行了初始化。

> docker stack rm todobackend
Removing service todobackend_app
Removing service todobackend_collectstatic
Removing service todobackend_db
Removing network todobackend_net
> docker volume ls
DRIVER          VOLUME NAME
local           sshkey
cloudstor:aws   todobackend_data
cloudstor:aws   todobackend_public
> docker volume rm todobackend_public
todobackend_public
> docker volume rm todobackend_data
todobackend_data

请记住,每当你拆除一个堆栈时,你必须手动删除在该堆栈中使用过的任何卷。

使用 Docker secrets 进行秘密管理

在前面的例子中,当我们创建db服务时,我们实际上并没有配置应用程序与db服务集成,因为虽然我们专注于如何创建持久存储,但我没有将app服务与db服务集成的另一个原因是因为我们目前正在以明文配置db服务的密码,这并不理想。

Docker Swarm 包括一个名为 Docker secrets 的功能,为在 Docker Swarm 集群上运行的应用程序提供安全的密钥管理解决方案。密钥存储在内部加密的存储机制中,称为raft log,该机制被复制到集群中的所有节点,确保被授予对密钥访问权限的任何服务和相关容器可以安全地访问密钥。

要创建 Docker 密钥,您可以使用docker secret create命令:

> openssl rand -base64 32 | docker secret create todobackend_mysql_password -
wk5fpokcz8wbwmuw587izl1in
> openssl rand -base64 32 | docker secret create todobackend_mysql_root_password -
584ojwg31c0oidjydxkglv4qz
> openssl rand -base64 50 | docker secret create todobackend_secret_key -
t5rb04xcqyrqiglmfwrfs122y
> docker secret ls
ID                          NAME                              CREATED          UPDATED
wk5fpokcz8wbwmuw587izl1in   todobackend_mysql_password        57 seconds ago   57 seconds ago
584ojwg31c0oidjydxkglv4qz   todobackend_mysql_root_password   50 seconds ago   50 seconds ago
t5rb04xcqyrqiglmfwrfs122y   todobackend_secret_key            33 seconds ago   33 seconds ago

在前面的例子中,我们使用openssl rand命令以 Base64 格式生成随机密钥,然后将其作为标准输入传递给docker secret create命令。我们为 todobackend 用户的 MySQL 密码和 MySQL 根密码创建了 32 个字符的密钥,最后创建了一个 50 个字符的密钥,用于 todobackend 应用程序执行的加密操作所需的 Django SECRET_KEY设置。

现在我们已经创建了几个密钥,我们可以配置我们的堆栈来使用这些密钥:

version: '3.6'

networks:
  ...

volumes:
  ...

secrets:
 todobackend_mysql_password:
 external: true
 todobackend_mysql_root_password:
 external: true
 todobackend_secret_key:
 external: true

services:
  app:
    ...
    ...
    environment:
      DJANGO_SETTINGS_MODULE: todobackend.settings_release
 MYSQL_HOST: db
 MYSQL_USER: todo
    secrets:
 - source: todobackend_mysql_password
 target: MYSQL_PASSWORD
 - source: todobackend_secret_key
 target: SECRET_KEY
    command:
    ...
    ...
  db:
    image: mysql:5.7
    environment:
      MYSQL_DATABASE: todobackend
      MYSQL_USER: todo
      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
    secrets:
 - source: todobackend_mysql_password
 target: mysql_password
 - source: todobackend_mysql_root_password
 target: mysql_root_password
  ...
  ...

我们首先声明顶级secrets参数,指定我们之前创建的每个密钥的名称,并将每个密钥配置为external,因为我们在堆栈之外创建了这些密钥。如果您不使用外部密钥,必须在文件中定义您的密钥,这并不能解决安全地存储密码在堆栈定义和配置之外的问题,因此将您的密钥作为独立于堆栈的单独实体创建会更安全。

然后,我们重新配置app服务以通过secrets属性消耗每个密钥。请注意,我们指定了MYSQL_PASSWORDSECRET_KEY的目标。每当您将密钥附加到服务时,将在/run/secrets创建一个基于内存的 tmpfs 挂载点,每个密钥存储在位置/run/secrets/<target-name>,因此对于app服务,将挂载以下密钥:

  • /run/secrets/MYSQL_PASSWORD

  • /run/secrets/SECRET_KEY

我们将在以后学习如何配置我们的应用程序来使用这些密钥,但也请注意,我们配置了MYSQL_HOSTMYSQL_USER环境变量,以便我们的应用程序知道如何连接到db服务以及要进行身份验证的用户。

接下来,我们配置db服务以使用 MySQL 密码和根密码密钥,并在这里配置每个密钥的目标,以便以下密钥在db服务容器中挂载:

  • /run/secrets/mysql_password

  • /run/secrets/mysql_root_password

最后,我们从db服务中删除了MYSQL_PASSWORDMYSQL_ROOT_PASSWORD环境变量,并用它们的基于文件的等效项替换,引用了每个配置的秘密的路径。

在这一点上,如果您部署了新更新的堆栈(如果您之前没有删除堆栈,您需要在此之前执行此操作,以确保您可以使用新凭据重新创建数据库),一旦您的 todobackend 服务成功启动,您可以通过运行docker ps命令来确定在 Swarm 管理器上运行的app服务实例的容器 ID,之后您可以检查/run/secrets目录的内容:

> docker stack deploy --with-registry-auth -c stack.yml todobackend
Creating network todobackend_net
Creating service todobackend_db
Creating service todobackend_app
Creating service todobackend_collectstatic
> docker ps -f name=todobackend -q
7804a7496fa2
> docker exec -it 7804a7496fa2 ls -l /run/secrets
total 8
-r--r--r-- 1 root root 45 Jul 20 23:49 MYSQL_PASSWORD
-r--r--r-- 1 root root 70 Jul 20 23:49 SECRET_KEY
> docker exec -it 7804a7496fa2 cat /run/secrets/MYSQL_PASSWORD
qvImrAEBDz9OWJS779uvs/EWuf/YlepTlwPkx4cLSHE=

正如您所看到的,您之前创建的秘密现在可以在/run/secrets文件夹中使用,如果您现在浏览发布应用程序的外部负载均衡器 URL 上的/todos路径,不幸的是,您将收到访问被拒绝的错误:

数据库认证错误

问题在于,尽管我们已经在app服务中挂载了数据库秘密,但我们的 todobackend 应用程序不知道如何使用这些秘密,因此我们需要对 todobackend 应用程序进行一些修改,以便能够使用这些秘密。

配置应用程序以使用秘密

在之前的章节中,我们使用了一个入口脚本来支持诸如在容器启动时注入秘密等功能,然而同样有效(实际上更好更安全)的方法是配置您的应用程序以原生方式支持您的秘密管理策略。

对于 Docker 秘密,这非常简单,因为秘密被挂载在容器的本地文件系统中的一个众所周知的位置(/run/secrets)。以下演示了修改todobackend存储库中的src/todobackend/settings_release.py文件以支持 Docker 秘密,正如您应该记得的那样,这些是我们传递给app服务的设置,由环境变量配置DJANGO_SETTINGS_MODULE=todobackend.settings_release指定。

from .settings import *
import os

# Disable debug
DEBUG = True

# Looks up secret in following order:
# 1\. /run/secret/<key>
# 2\. Environment variable named <key>
# 3\. Value of default or None if no default supplied
def secret(key, default=None):
 root = os.environ.get('SECRETS_ROOT','/run/secrets')
 path = os.path.join(root,key)
 if os.path.isfile(path):
 with open(path) as f:
 return f.read().rstrip()
 else:
 return os.environ.get(key,default)

# Set secret key
SECRET_KEY = secret('SECRET_KEY', SECRET_KEY)

# Must be explicitly specified when Debug is disabled
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')

# Database settings
DATABASES = {
    'default': {
        'ENGINE': 'mysql.connector.django',
        'NAME': os.environ.get('MYSQL_DATABASE','todobackend'),
        'USER': os.environ.get('MYSQL_USER','todo'),
 'PASSWORD': secret('MYSQL_PASSWORD','password'),
        'HOST': os.environ.get('MYSQL_HOST','localhost'),
        'PORT': os.environ.get('MYSQL_PORT','3306'),
    },
    'OPTIONS': {
      'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
    }
}

STATIC_ROOT = os.environ.get('STATIC_ROOT', '/public/static')
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', '/public/media')

MIDDLEWARE.insert(0,'aws_xray_sdk.ext.django.middleware.XRayMiddleware')

我们首先创建一个名为secret()的简单函数,该函数以设置或key的名称作为输入,并在无法找到秘密时提供一个可选的默认值。然后,该函数尝试查找路径/run/secrets(可以通过设置环境变量SECRETS_ROOT来覆盖此路径),并查找与请求的键相同名称的文件。如果找到该文件,则使用f.read().rstrip()调用读取文件的内容,rstrip()函数会去除read()函数返回的换行符。否则,该函数将查找与键相同名称的环境变量,如果所有这些查找都失败,则返回传递给secret()函数的default值(该值本身具有默认值None)。

有了这个函数,我们可以简单地调用秘密函数,如对SECRET_KEYDATABASES['PASSWORD']设置进行演示,并以SECRET_KEY设置为例,该函数将按以下优先顺序返回:

  1. /run/secrets/SECRET_KEY的内容值

  2. 环境变量SECRET_KEY的值

  3. 传递给secrets()函数的默认值的值(在本例中,从基本设置文件导入的SECRET_KEY设置)

现在我们已经更新了 todobackend 应用程序以支持 Docker secrets,您需要提交您的更改,然后测试、构建和发布您的更改。请注意,您需要在连接到本地 Docker 引擎的单独 shell 中执行此操作(而不是连接到 Docker Swarm 集群):

> git commit -a -m "Add support for Docker secrets"
[master 3db46c4] Add support for Docker secrets
> make login
...
...
> make test
...
...
> make release
...
...
> make publish
...
...

一旦您的镜像成功发布,切换回连接到 Swarm 集群的终端会话,并使用docker stack deploy命令重新部署您的堆栈:

> docker stack deploy --with-registry-auth -c stack.yml todobackend
Updating service todobackend_app (id: xz0tl79iv75qvq3tw6yqzracm)
Updating service todobackend_collectstatic (id: tkal4xxuejmf1jipsg24eq1bm)
Updating service todobackend_db (id: 9vj845j54nsz360q70lk1nrkr)
> docker service ps todobackend_app --format "{{ .Name }}: {{ .CurrentState }}"
todobackend_app.1: Running 20 minutes ago
todobackend_app.2: Running 20 minutes ago

如果您运行docker service ps命令,如前面的示例所示,您可能会注意到您的 todobackend 服务没有重新部署(在某些情况下,服务可能会重新部署)。原因是我们在堆栈文件中默认使用最新的镜像。为了确保我们能够持续交付和部署我们的应用程序,我们需要引用特定版本或构建标签,这是您应该始终采取的最佳实践方法,因为它将强制在每次服务更新时部署显式版本的镜像。

通过我们的本地工作流程,我们可以利用 todobackend 应用程序存储库中已经存在的Makefile,并包含一个APP_VERSION环境变量,返回当前的 Git 提交哈希,随后我们可以在我们的堆栈文件中引用它:

version: '3.6'

services:
  app:
 image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:${APP_VERSION}
    ...
    ...
  collectstatic:
 image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:${APP_VERSION}
    ...
    ...

有了这个配置,我们现在需要在todobackend存储库的根目录中添加一个Makefile的部署配方,当 Docker 客户端解析堆栈文件时,它将自动使APP_VERSION环境变量可用:

.PHONY: test release clean version login logout publish deploy

export APP_VERSION ?= $(shell git rev-parse --short HEAD)

version:
  @ echo '{"Version": "$(APP_VERSION)"}'

deploy: login
  @ echo "Deploying version ${APP_VERSION}..."
 docker stack deploy --with-registry-auth -c stack.yml todobackend 
login:
  $$(aws ecr get-login --no-include-email)
...
...

deploy配方引用login配方,确保我们始终首先运行等效的make login,然后再运行deploy配方中的任务。这个配方只是运行docker stack deploy命令,这样我们现在可以通过运行make deploy来部署对我们堆栈的更新:

> make deploy
Deploying version 3db46c4,,,
docker stack deploy --with-registry-auth -c stack.yml todobackend
Updating service todobackend_app (id: xz0tl79iv75qvq3tw6yqzracm)
Updating service todobackend_collectstatic (id: tkal4xxuejmf1jipsg24eq1bm)
Updating service todobackend_db (id: 9vj845j54nsz360q70lk1nrkr)
> docker service ps todobackend_app --format "{{ .Name }}: {{ .CurrentState }}"
todobackend_app.1: Running 5 seconds ago
todobackend_app.1: Shutdown 6 seconds ago
todobackend_app.2: Running 25 minutes ago
> docker service ps todobackend_app --format "{{ .Name }}: {{ .CurrentState }}"
todobackend_app.1: Running 45 seconds ago
todobackend_app.1: Shutdown 46 seconds ago
todobackend_app.2: Running 14 seconds ago
todobackend_app.2: Shutdown 15 seconds ago

因为我们的堆栈现在配置了一个特定的图像标记,由APP_VERSION变量(在前面的示例中为3db46c4)定义,所以一旦检测到更改,app服务就会被更新。您可以使用docker service ps命令来确认这一点,就像之前演示的那样,并且我们已经配置这个服务以每次更新一个实例,并且每次更新之间有 30 秒的延迟。

如果您现在浏览外部负载均衡器 URL 上的/todos路径,认证错误现在应该被替换为表不存在错误,这证明我们现在至少能够连接到数据库,但还没有处理数据库迁移作为我们的 Docker Swarm 解决方案的一部分:

数据库错误

运行数据库迁移

现在我们已经建立了一个安全访问堆栈中的 db 服务的机制,我们需要执行的最后一个配置任务是添加一个将运行数据库迁移的服务。这类似于我们之前创建的 collectstatic 服务,它需要是一个“一次性”任务,只有在我们创建堆栈或部署新版本的应用程序时才执行:

version: '3.6'

networks:
  ...

volumes:
  ...

secrets:
  ...

services:
  app:
    ...
  migrate:
 image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:${APP_VERSION}
 networks:
 - net
 environment:
 DJANGO_SETTINGS_MODULE: todobackend.settings_release
 MYSQL_HOST: db
 MYSQL_USER: todo
 secrets:
 - source: todobackend_mysql_password
 target: MYSQL_PASSWORD
command:
 - python3
 - manage.py
 - migrate
 - --no-input
 deploy:
 replicas: 1
 restart_policy:
 condition: on-failure
 delay: 30s
 max_attempts: 6
  collectstatic:
    ...
  db:
    ...

新的migrate服务的所有设置应该是不言自明的,因为我们之前已经为其他服务配置过它们。deploy配置尤其重要,并且与其他一次性 collectstatic 服务配置相同,Docker Swarm 将尝试确保migrate服务的单个副本能够成功启动最多六次,每次尝试之间延迟 30 秒。

如果您现在运行make deploy来部署您的更改,migrate服务应该能够成功完成:

> make deploy
Deploying version 3db46c4...
docker stack deploy --with-registry-auth -c stack.yml todobackend
Updating service todobackend_collectstatic (id: tkal4xxuejmf1jipsg24eq1bm)
Updating service todobackend_db (id: 9vj845j54nsz360q70lk1nrkr)
Updating service todobackend_app (id: xz0tl79iv75qvq3tw6yqzracm)
Creating service todobackend_migrate
> docker service ps todobackend_migrate --format "{{ .Name }}: {{ .CurrentState }}"
todobackend_migrate.1: Complete 18 seconds ago

为了验证迁移实际上已经运行,因为我们在创建 Docker Swarm 集群时启用了 CloudWatch 日志,您可以在 CloudWatch 日志控制台中查看migrate服务的日志。当使用 Docker for AWS 解决方案模板部署集群时,会创建一个名为<cloudformation-stack-name>-lg的日志组,我们的情况下是docker-swarm-lg。如果您在 CloudWatch 日志控制台中打开此日志组,您将看到为在 Swarm 集群中运行或已运行的每个容器存在日志流:

部署 migrate 服务

您可以看到最近的日志流与migrate服务相关,如果您打开此日志流,您可以确认数据库迁移已成功运行:

migrate 服务日志流

此时,您的应用程序应该已成功运行,并且您应该能够与应用程序交互以创建、更新、查看和删除待办事项。验证这一点的一个好方法是运行您在早期章节中创建的验收测试,这些测试包含在 todobackend 发布图像中,并确保通过APP_URL环境变量传递外部负载均衡器 URL,这可以作为自动部署后测试的策略。

> docker run -it --rm \ 
 -e APP_URL=http://docker-sw-external-1a5qzeykya672-1599369435.us-east-1.elb.amazonaws.com \ 
 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend:3db46c4 \
 bats /app/src/acceptance.bats
Processing secrets []...
1..4
ok 1 todobackend root
ok 2 todo items returns empty list
ok 3 create todo item
ok 4 delete todo item

您现在已成功将 todobackend 应用程序部署到在 AWS 上运行的 Docker Swarm 集群中,我鼓励您进一步测试您的应用程序是否已经准备好投入生产,方法是拆除/重新创建堆栈,并通过进行测试提交和创建新的应用程序版本来运行一些示例部署。

完成后,您应该提交您所做的更改,并不要忘记通过在 CloudFormation 控制台中删除docker-swarm堆栈来销毁您的 Docker Swarm 集群。

总结

在本章中,您学会了如何使用 Docker Swarm 和 Docker for AWS 解决方案部署 Docker 应用程序。Docker for AWS 提供了一个 CloudFormation 模板,允许您在几分钟内设置一个 Docker Swarm 集群,并提供与 AWS 服务的集成,包括弹性负载均衡器服务、弹性文件系统和弹性块存储。

在创建了一个 Docker Swarm 集群之后,您学会了如何通过配置 SSH 隧道来为本地 Docker 客户端建立与 Swarm 管理器的远程访问,该隧道链接到 Swarm 管理器上的/var/run/docker.sock套接字文件,并将其呈现为本地端点,以便您的 Docker 客户端可以与之交互。这使得管理 Swarm 集群的体验类似于管理本地 Docker Engine。

您学会了如何创建和部署 Docker 服务,这些服务通常代表长时间运行的应用程序,但也可以代表一次性任务,比如运行数据库迁移或生成静态内容文件。Docker 堆栈代表复杂的多服务环境,并使用 Docker Compose 版本 3 规范进行定义,并使用docker stack deploy命令进行部署。使用 Docker Swarm 的一个优势是可以访问 Docker secrets 功能,该功能允许您将秘密安全地存储在加密的 raft 日志中,该日志会自动复制并在集群中的所有节点之间共享。然后,Docker secrets 可以作为内存 tmpfs 挂载暴露给服务,位于/run/secrets。您已经学会了如何轻松地配置您的应用程序以集成 Docker secrets 功能。

最后,您学会了如何解决在生产环境中运行容器时遇到的常见操作挑战,例如如何提供持久的、持久的存储访问,以 EBS 卷的形式,这些卷可以自动与您的容器重新定位,如何使用 EFS 提供对共享卷的访问,以及如何编排部署新的应用程序功能,支持运行一次性任务和滚动升级您的应用程序服务。

在本书的下一章和最后一章中,您将了解到 AWS 弹性 Kubernetes 服务(EKS),该服务于 2018 年中期推出,支持 Kubernetes,这是一种与 Docker Swarm 竞争的领先开源容器管理平台。

问题

  1. 真/假:Docker Swarm 是 Docker Engine 的本机功能。

  2. 您使用哪个 Docker 客户端命令来创建服务?

  3. 正确/错误:Docker Swarm 包括三种节点类型——管理器、工作节点和代理。

  4. 正确/错误:Docker for AWS 提供与 AWS 应用负载均衡器的集成。

  5. 正确/错误:当后备设置为可重定位时,Cloudstor AWS 卷插件会创建一个 EFS 支持的卷。

  6. 正确/错误:您创建了一个使用 Cloudstor AWS 卷插件提供位于可用性区域 us-west-1a 的 EBS 支持卷的数据库服务。发生故障,并且在可用性区域 us-west-1b 中创建了一个新的数据库服务容器。在这种情况下,原始的 EBS 卷将重新附加到新的数据库服务容器上。

  7. 您需要在 Docker Stack deploy 和 Docker service create 命令中附加哪个标志以与私有 Docker 注册表集成?

  8. 您部署了一个从 ECR 下载图像的堆栈。第一次部署成功,但是当您尝试在第二天执行新的部署时,您注意到您的 Docker swarm 节点无法拉取 ECR 图像。您该如何解决这个问题?

  9. 您应该使用哪个版本的 Docker Compose 规范来定义 Docker Swarm 堆栈?

  10. 正确/错误:在配置单次服务时,您应该将重启策略配置为 always。

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十七章:弹性 Kubernetes 服务

Kubernetes 是一种流行的开源容器管理平台,最初由谷歌开发,基于谷歌自己内部的 Borg 容器平台。Kubernetes 借鉴了谷歌在大规模运行容器方面的丰富经验,现在得到了所有主要云平台提供商的支持,包括 AWS Elastic Kubernetes Service(EKS)的发布。EKS 提供了一个托管的 Kubernetes 集群,您可以在其中部署容器应用程序,而无需担心日常运营开销和集群管理的复杂性。AWS 已经完成了建立一个强大和可扩展平台的大部分工作,使得使用 Kubernetes 变得比以往更容易。

在本章中,您将被介绍到 Kubernetes 的世界,我们将通过如何配置 Kubernetes 来确保我们能够成功部署和操作本书中使用的示例应用程序,并在 AWS 中建立一个 EKS 集群,您将使用本地开发的配置部署应用程序。这将为您提供实际的、现实世界的见解,作为应用程序所有者,您可以将您的容器工作负载部署到 Kubernetes,并且您可以快速地开始使用 EKS。

我们将首先学习如何在本地使用 Docker for Mac 和 Docker for Windows 对 Kubernetes 进行本地支持。您可以直接启动一个本地单节点集群,减少了通常需要进行的大量手动配置,以便快速启动本地环境。您将学习如何创建运行 Kubernetes 中示例应用程序所需的各种资源,解决关键的运营挑战,如为应用程序数据库提供持久存储、管理密钥和运行一次性任务,如数据库迁移。

一旦您建立了一个工作配置,可以在 Kubernetes 中本地运行示例应用程序,我们将把注意力转向开始使用 EKS,创建 EKS 集群,并建立一个 EC2 自动扩展组,管理运行容器工作负载的工作节点。您将学习如何从本地环境设置对集群的访问,并继续部署 Kubernetes 仪表板,该仪表板提供了丰富的管理用户界面,您可以从中部署和管理应用程序。最后,您将设置与其他 AWS 服务的集成,包括弹性块存储(EBS)和弹性负载均衡(ELB),并将示例应用程序部署到您的 EKS 集群。

本章将涵盖以下主题:

  • Kubernetes 简介

  • Kubernetes 架构

  • 开始使用 Kubernetes

  • 使用 Docker Desktop 安装 Kubernetes

  • 创建核心 Kubernetes 资源,包括 pod、部署和服务

  • 创建持久卷

  • 创建 Kubernetes secrets

  • 运行 Kubernetes 作业

  • 创建 EKS 集群

  • 建立对 EKS 集群的访问

  • 将应用程序部署到 EKS

技术要求

以下是本章的技术要求:

  • AWS 账户的管理员访问权限

  • 本地 AWS 配置文件,按照第三章的说明进行配置

  • AWS CLI 版本 1.15.71 或更高版本

  • Docker 18.06 或更高版本

  • Docker Compose 1.22 或更高版本

  • GNU Make 3.82 或更高版本

  • 本章假设您已经完成了本书中的所有前几章。

以下 GitHub 网址包含本章中使用的代码示例:github.com/docker-in-aws/docker-in-aws/tree/master/ch17

观看以下视频,了解代码的实际操作:

bit.ly/2LyGtSY

Kubernetes 简介

Kubernetes是一个开源的容器管理平台,由 Google 在 2014 年开源,并在 2015 年通过 1.0 版本实现了生产就绪。在短短三年的时间里,它已经成为最受欢迎的容器管理平台,并且非常受大型组织的欢迎,这些组织希望将他们的应用程序作为容器工作负载来运行。Kubernetes 是 GitHub 上最受欢迎的开源项目之一(github.com/cncf/velocity/blob/master/docs/top30_chart_creation.md),根据Redmonk的说法,截至 2017 年底,Kubernetes 在财富 100 强公司中被使用率达到了 54%。

Kubernetes 的关键特性包括以下内容:

  • 平台无关:Kubernetes 可以在任何地方运行,从您的本地机器到数据中心,以及在 AWS、Azure 和 Google Cloud 等云提供商中,它们现在都提供集成的托管 Kubernetes 服务。

  • 开源:Kubernetes 最大的优势在于其社区和开源性质,这使得 Kubernetes 成为了全球领先的开源项目之一。主要组织和供应商正在投入大量时间和资源来为平台做出贡献,确保整个社区都能从这些持续的增强中受益。

  • 血统:Kubernetes 的根源来自 Google 内部的 Borg 平台,自从 2000 年代初以来一直在大规模运行容器。Google 是容器技术的先驱之一,毫无疑问是容器的最大采用者之一,如果不是最大的采用者。在 2014 年,Google 表示他们每周运行 20 亿个容器,而当时大多数企业刚刚通过一个名为 Docker 的新项目听说了容器技术。这种血统和传统确保了 Google 在多年大规模运行容器中所学到的许多经验教训都被包含在 Kubernetes 平台中。

  • 生产级容器管理功能:Kubernetes 提供了您在其他竞争平台上期望看到并会遇到的所有容器管理功能。这包括集群管理、多主机网络、可插拔存储、健康检查、服务发现和负载均衡、服务扩展和滚动更新、期望阶段配置、基于角色的访问控制以及秘密管理等。所有这些功能都以模块化的构建块方式实现,使您可以调整系统以满足组织的特定要求,这也是 Kubernetes 现在被认为是企业级容器管理的黄金标准的原因之一。

Kubernetes 与 Docker Swarm

在上一章中,我提出了关于 Docker Swarm 与 Kubernetes 的个人看法,这一次我将继续,这次更加关注为什么选择 Kubernetes 而不是 Docker Swarm。当您阅读本章时,应该会发现 Kubernetes 具有更为复杂的架构,这意味着学习曲线更高,而我在本章中涵盖的内容只是 Kubernetes 可能实现的一小部分。尽管如此,一旦您理解了这些概念,至少从我的角度来看,您应该会发现最终 Kubernetes 更加强大、更加灵活,可以说 Kubernetes 肯定比 Docker Swarm 更具“企业级”感觉,您可以调整更多的参数来定制 Kubernetes 以满足您的特定需求。

Kubernetes 相对于 Docker Swarm 和其他竞争对手最大的优势可能是其庞大的社区,这意味着几乎可以在更广泛的 Kubernetes 社区和生态系统中找到关于几乎任何配置方案的信息。Kubernetes 运动背后有很多动力,随着 AWS 等领先供应商和提供商采用 Kubernetes 推出自己的产品和解决方案,这一趋势似乎正在不断增长。

Kubernetes 架构

在架构上,Kubernetes 以集群的形式组织自己,其中主节点形成集群控制平面,工作节点运行实际的容器工作负载:

Kubernetes 架构

在每个主节点中,存在许多组件:

  • kube-apiserver:这个组件公开 Kubernetes API,是您用来与 Kubernetes 控制平面交互的前端组件。

  • etcd:这提供了一个跨集群的分布式和高可用的键/值存储,用于存储 Kubernetes 配置和操作数据。

  • kube-scheduler:这将 pod 调度到工作节点上,考虑资源需求、约束、数据位置和其他因素。稍后您将了解更多关于 pod 的信息,但现在您可以将它们视为一组相关的容器和卷,需要一起创建、更新和部署。

  • kube-controller-manager:这负责管理控制器,包括一些组件,用于检测节点何时宕机,确保 pod 的正确数量的实例或副本正在运行,为在 pod 中运行的应用程序发布服务端点,并管理集群的服务帐户和 API 访问令牌。

  • cloud-controller-manager:这提供与底层云提供商交互的控制器,使云提供商能够支持特定于其平台的功能。云控制器的示例包括服务控制器,用于创建、更新和删除云提供商负载均衡器,以及卷控制器,用于创建、附加、分离和删除云提供商支持的各种存储卷技术。

  • 插件:有许多可用的插件可以扩展集群的功能。这些以 pod 和服务的形式运行,提供集群功能。在大多数安装中通常部署的一个插件是集群 DNS 插件,它为在集群上运行的服务和 pod 提供自动 DNS 命名和解析。

在所有节点上,存在以下组件:

  • kubelet:这是在集群中每个节点上运行的代理,确保 pod 中的所有容器健康运行。kubelet 还可以收集容器指标,可以发布到监控系统。

  • kube-proxy:这管理每个节点上所需的网络通信、端口映射和路由规则,以支持 Kubernetes 支持的各种服务抽象。

  • 容器运行时:提供运行容器的容器引擎。最受欢迎的容器运行时是 Docker,但是也支持 rkt(Rocket)或任何 OCI 运行时规范实现。

  • Pods:Pod 是部署容器应用程序的核心工作单元。每个 Pod 由一个或多个容器和相关资源组成,并且一个单一的网络接口,这意味着给定 Pod 中的每个容器共享相同的网络堆栈。

请注意,工作节点只直接运行先前列出的组件,而主节点运行到目前为止我们讨论的所有组件,允许主节点也运行容器工作负载,例如单节点集群的情况。

Kubernetes 还提供了一个名为kubectl的客户端组件,它提供了通过 Kubernetes API 管理集群的能力。kubectl支持 Windows、macOS 和 Linux,并允许您轻松管理和在本地和远程之间切换多个集群。

开始使用 Kubernetes

现在您已经简要介绍了 Kubernetes,让我们专注于在本地环境中启动和运行 Kubernetes。

在本书中的早期,当您设置本地开发环境时,如果您使用的是 macOS 或 Windows,您安装了 Docker Desktop 的社区版(CE)版本(Docker for Mac 或 Docker for Windows,在本章中我可能统称为 Docker Desktop),其中包括对 Kubernetes 的本地支持。

如果您使用的是不支持 Kubernetes 的 Docker for Mac/Windows 的变体,或者使用 Linux,您可以按照以下说明安装 minikube:github.com/kubernetes/minikube。本节中包含的大多数示例应该可以在 minikube 上运行,尽管诸如负载平衡和动态主机路径配置等功能可能不会直接支持,需要一些额外的配置。

要启用 Kubernetes,请在本地 Docker Desktop 设置中选择Kubernetes,并勾选启用 Kubernetes选项。一旦您点击应用,Kubernetes 将被安装,并需要几分钟来启动和运行:

使用 Docker for Mac 启用 Kubernetes

Docker Desktop 还会自动为您安装和配置 Kubernetes 命令行实用程序kubectl,该实用程序可用于验证您的安装:

> kubectl get nodes
NAME                STATUS  ROLES   AGE  VERSION
docker-for-desktop  Ready   master  1m   v1.10.3

如果您正在使用 Windows 的 Docker 与 Linux 子系统配合使用,您需要通过运行以下命令将kubectl安装到子系统中(有关更多详细信息,请参见kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-via-native-package-management):

sudo apt-get update && sudo apt-get install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo touch /etc/apt/sources.list.d/kubernetes.list 
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee -a /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubectl

安装kubectl后,如果您之前将 Linux 子系统的主文件夹更改为 Windows 主文件夹,则现在应该能够与本地 Kubernetes 集群进行交互,无需进一步配置。

如果您的主文件夹与 Windows 主文件夹不同(默认情况下是这种情况),那么您将需要设置一个符号链接,指向 Windows 主文件夹中的kubectl配置文件,之后您应该能够使用kubectl与本地 Kubernetes 安装进行交互:

# Only required if Linux Subsystem home folder is different from Windows home folder
$ mkdir -p ~/.kube
$ ln -s /mnt/c/Users/<username>/.kube/config ~/.kube/config
$ kubectl get nodes
NAME                STATUS  ROLES   AGE  VERSION
docker-for-desktop  Ready   master  1m   v1.10.3

Windows 的 Linux 子系统还允许您运行 Windows 命令行程序,因此您也可以运行kubectl.exe来调用 Windows kubectl 组件。

创建一个 pod

在 Kubernetes 中,您将应用程序部署为pods,这些 pods 指的是一个或多个容器和其他与之密切相关的资源,共同代表您的应用程序。pod是 Kubernetes 中的核心工作单元,概念上类似于 ECS 任务定义,尽管在底层它们以完全不同的方式工作。

Kubernetes 的常用简写代码是 k8s,其中名称 Kubernetes 中的“ubernete”部分被数字 8 替换,表示“ubernete”中的字符数。

在创建我们的第一个 pod 之前,让我们在 todobackend 存储库中建立一个名为k8s的文件夹,该文件夹将保存 todobackend 应用程序的所有 Kubernetes 配置,然后创建一个名为app的文件夹,该文件夹将存储与核心 todobackend 应用程序相关的所有资源定义:

todobackend> mkdir -p k8s/app todobackend> touch k8s/app/deployment.yaml

以下代码演示了 todobackend 应用程序的基本 pod 定义,我们将其保存到k8s/app/deployment.yaml文件中:

apiVersion: v1
kind: Pod
metadata:
  name: todobackend
  labels:
    app: todobackend
spec:
  containers:
  - name: todobackend
    image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
    imagePullPolicy: IfNotPresent
    command:
    - uwsgi
    - --http=0.0.0.0:8000
    - --module=todobackend.wsgi
    - --master
    - --die-on-term
    - --processes=4
    - --threads=2
    - --check-static=/public
    env:
    - name: DJANGO_SETTINGS_MODULE
      value: todobackend.settings_release

pod 配置文件的格式很容易遵循,通常情况下,您看到的大多数参数都与使用 Docker Compose 定义容器时的同名参数相对应。一个经常引起混淆的重要区别是command参数-在 Kubernetes 中,此参数相当于ENTRYPOINT Dockerfile 指令和 Docker Compose 服务规范中的entrypoint参数,而在 Kubernetes 中,args参数相当于 CMD 指令(Dockerfile)和 Docker Compose 中的command服务参数。这意味着在前面的配置中,我们的容器中的默认入口脚本被绕过,而是直接运行 uwsgi web 服务器。

imagePullPolicy属性值为IfNotPresent配置了 Kubernetes 只有在本地 Docker Engine 注册表中没有可用的镜像时才拉取镜像,这意味着在尝试创建 pod 之前,您必须确保已运行现有的 todobackend Docker Compose 工作流以在本地构建和标记 todobackend 镜像。这是必需的,因为当您在 AWS EC2 实例上运行 Kubernetes 时,Kubernetes 只包括对 ECR 的本机支持,并且在您在 AWS 之外运行 Kubernetes 时,不会本地支持 ECR。

有许多第三方插件可用,允许您管理 AWS 凭据并拉取 ECR 镜像。一个常见的例子可以在github.com/upmc-enterprises/registry-creds找到。

要创建我们的 pod 并验证它是否正在运行,您可以运行kubectl apply命令,使用-f标志引用您刚刚创建的部署文件,然后运行kubectl get pods命令:

> kubectl apply -f k8s/app/deployment.yaml
pod "todobackend" created
> kubectl get pods
NAME          READY   STATUS    RESTARTS   AGE
todobackend   1/1     Running   0          7s
> docker ps --format "{{ .Names }}"
k8s_todobackend_todobackend_default_1b436412-9001-11e8-b7af-025000000001_0
> docker ps --format "{{ .ID }}: {{ .Command }} ({{ .Status }})"
fc0c8acdd438: "uwsgi --http=0.0.0.…" (Up 16 seconds)
> docker ps --format "{{ .ID }} Ports: {{ .Ports }}"
fc0c8acdd438 Ports:

您可以看到 pod 的状态为Running,并且已经部署了一个容器到在您的本地 Docker Desktop 环境中运行的单节点 Kubernetes 集群。一个重要的要注意的是,已部署的 todobackend 容器无法与外部世界通信,因为从 pod 及其关联的容器中没有发布任何网络端口。

Kubernetes 的一个有趣之处是您可以使用 Kubernetes API 与您的 pod 进行交互。为了演示这一点,首先运行kubectl proxy命令,它会设置一个本地 HTTP 代理,通过普通的 HTTP 接口公开 API:

> kubectl proxy
Starting to serve on 127.0.0.1:8001

您现在可以通过 URL http://localhost:8001/api/v1/namespaces/default/pods/todobackend:8000/proxy/ 访问 pod 上的容器端口 8000:

运行 kubectl 代理

如您所见,todobackend 应用正在运行,尽管它缺少静态内容,因为我们还没有生成它。还要注意页面底部的 todos 链接(http://localhost:8001/todos)是无效的,因为 todobackend 应用程序不知道通过代理访问应用程序的 API 路径。

Kubernetes 的另一个有趣特性是通过运行 kubectl port-forward 命令,将 Kubernetes 客户端的端口暴露给应用程序,从而连接到指定的 pod,这样可以实现从 Kubernetes 客户端到应用程序的端口转发:

> kubectl proxy
Starting to serve on 127.0.0.1:8001
^C
> kubectl port-forward todobackend 8000:8000
Forwarding from 127.0.0.1:8000 -> 8000
Forwarding from [::1]:8000 -> 8000
Handling connection for 8000

如果您现在尝试访问 http://localhost:8000,您应该能看到 todobackend 的主页,并且页面底部的 todos 链接现在应该是可访问的:

访问一个端口转发的 pod

您可以看到,再次,我们的应用程序并不处于完全功能状态,因为我们还没有配置任何数据库设置。

创建一个部署

尽管我们已经能够发布我们的 todobackend 应用程序,但我们用来做这件事的机制并不适合实际的生产使用,而且只对有限的本地开发场景真正有用。

在现实世界中运行我们的应用程序的一个关键要求是能够扩展或缩减应用程序容器的实例或副本数量。为了实现这一点,Kubernetes 支持一类资源,称为控制器,它负责协调、编排和管理给定 pod 的多个副本。一种流行的控制器类型是部署资源,正如其名称所示,它包括支持创建和更新 pod 的新版本,以及滚动升级和在部署失败时支持回滚等功能。

以下示例演示了如何更新 todobackend 仓库中的 k8s/app/deployment.yaml 文件来定义一个部署资源:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend
  labels:
    app: todobackend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: todobackend
  template:
    metadata:
      labels:
        app: todobackend
    spec:
      containers:
      - name: todobackend
        image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
        imagePullPolicy: IfNotPresent
        readinessProbe:
          httpGet:
            port: 8000
        livenessProbe:
          httpGet:
            port: 8000
        command:
        - uwsgi
        - --http=0.0.0.0:8000
        - --module=todobackend.wsgi
        - --master
        - --die-on-term
        - --processes=4
        - --threads=2
        - --check-static=/public
        env:
        - name: DJANGO_SETTINGS_MODULE
          value: todobackend.settings_release

我们将之前的 pod 资源更新为现在的 deployment 资源,使用顶级 spec 属性(即 spec.template)的 template 属性内联定义应该部署的 pod。部署和 Kubernetes 的一个关键概念是使用基于集合的标签选择器匹配来确定部署适用于哪些资源或 pod。在前面的示例中,部署资源的 spec 指定了两个副本,并使用 selectors.matchLabels 来将部署与包含标签 app 值为 todobackend 的 pod 匹配。这是一个简单但强大的范例,可以以灵活和松散耦合的方式创建自己的结构和资源之间的关系。请注意,我们还向容器定义添加了 readinessProbe 和 livenessProbe 属性,分别创建了 readiness probe 和 liveness probe。readiness probe 定义了 Kubernetes 应执行的操作,以确定容器是否准备就绪,而 liveness probe 用于确定容器是否仍然健康。在前面的示例中,readiness probe 使用 HTTP GET 请求到端口 8000 来确定部署控制器何时应允许连接转发到容器,而 liveness probe 用于在容器不再响应 liveness probe 时重新启动容器。有关不同类型的探针及其用法的更多信息,请参阅 https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/。

要创建新的部署资源,我们可以首先删除现有的 pod,然后使用 kubectl 应用 todobackend 仓库中的 k8s/app/deployment.yaml 文件:

> kubectl delete pods/todobackend
pod "todobackend" deleted
> kubectl apply -f k8s/app/deployment.yaml deployment.apps "todobackend" created> kubectl get deployments NAME                    DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
todobackend             2        2        2           2          12s> kubectl get pods NAME                                     READY  STATUS   RESTARTS  AGE
todobackend-7869d9965f-lh944             1/1    Running  0         17s
todobackend-7869d9965f-v986s             1/1    Running  0         17s

创建部署后,您可以看到配置的副本数量以两个 pod 的形式部署,每个都有一个唯一的名称。只要您配置的 readiness probe 成功,每个 pod 的状态就会立即转换为 ready。

创建服务

在这一点上,我们已经为我们的应用程序定义了一个 pod,并使用部署资源部署了多个应用程序副本,现在我们需要确保外部客户端可以连接到我们的应用程序。鉴于我们有多个应用程序副本正在运行,我们需要一个能够提供稳定服务端点、跟踪每个副本位置并在所有副本之间负载平衡传入连接的组件。

服务是提供此类功能的 Kubernetes 资源,每个服务都被分配一个虚拟 IP 地址,可以用来访问一组 pod,并且对虚拟 IP 地址的传入连接进行负载平衡到每个 pod 副本,基于通过一个名为 kube-proxy 的标准 Kubernetes 系统资源管理和更新的 iptables 规则:

Kubernetes 中的服务和端点

在上图中,一个客户端 pod 正试图使用虚拟 IP 地址10.1.1.1的端口80(10.1.1.1:80)与应用程序 pod 进行通信。请注意,服务虚拟 IP 地址在集群中的每个节点上都是公开的,kube-proxy组件负责更新 iptables 规则,以循环方式选择适当的端点,将客户端连接路由到。由于虚拟 IP 地址在集群中的每个节点上都是公开的,因此任何节点上的任何客户端都可以与服务通信,并且流量会均匀地分布在整个集群中。

现在您已经对服务的工作原理有了高层次的理解,让我们实际在k8s/app/deployment.yaml文件中定义一个新的服务,该文件位于todobackend存储库中:

apiVersion: v1
kind: Service
metadata:
 name: todobackend
spec:
 selector:
 app: todobackend
 ports:
 - protocol: TCP
 port: 80
    targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend
  labels:
    app: todobackend
...
...

请注意,您可以使用---分隔符在单个 YAML 文件中定义多个资源,并且我们可以创建一个名为 todobackend 的服务,该服务使用标签匹配将服务绑定到具有app=todobackend标签的任何 pod。在spec.ports部分,我们将端口 80 配置为服务的传入或监听端口,该端口将连接负载平衡到每个 pod 上的 8000 端口。

我们的服务定义已经就位,现在您可以使用kubectl apply命令部署服务:

> kubectl apply -f k8s/app/deployment.yaml
service "todobackend" created
deployment.apps "todobackend" unchanged
> kubectl get svc
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes           ClusterIP   10.96.0.1       <none>        443/TCP   8h
todobackend          ClusterIP   10.103.210.17   <none>        80/TCP    10s
> kubectl get endpoints
NAME          ENDPOINTS                       AGE
kubernetes    192.168.65.3:6443               1d
todobackend   10.1.0.27:8000,10.1.0.30:8000   16h

您可以使用kubectl get svc命令查看当前服务,并注意到每个服务都包括一个唯一的集群 IP 地址,这是集群中其他资源可以用来与与服务关联的 pod 进行通信的虚拟 IP 地址。kubectl get endpoints命令显示与每个服务关联的实际端点,您可以看到对todobackend服务虚拟 IP 地址10.103.210.17:80的连接将负载均衡到10.1.0.27:800010.1.0.30:8000

每个服务还分配了一个唯一的 DNS 名称,格式为<service-name>.<namespace>.svc.cluster.local。Kubernetes 中的默认命名空间称为default,因此对于我们的 todobackend 应用程序,它将被分配一个名为todobackend.default.svc.cluster.local的名称,您可以使用kubectl run命令验证在集群内是否可访问:

> kubectl run dig --image=googlecontainer/dnsutils --restart=Never --rm=true --tty --stdin \
 --command -- dig todobackend a +search +noall +answer
; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> todobackend a +search +noall +answer
;; global options: +cmd
todobackend.default.svc.cluster.local. 30 IN A   10.103.210.17

在上面的示例中,您可以简单地查询 todobackend,因为 Kubernetes 将 DNS 搜索域发送到<namespace>.svc.cluster.local(在我们的用例中为default.svc.cluster.local),您可以看到这将解析为 todobackend 服务的集群 IP 地址。

重要的是要注意,集群 IP 地址只能在 Kubernetes 集群内访问 - 如果没有进一步的配置,我们无法从外部访问此服务。

暴露服务

为了允许外部客户端和系统与 Kubernetes 服务通信,您必须将服务暴露给外部世界。按照 Kubernetes 的风格,有多种选项可用于实现这一点,这些选项由 Kubernetes 的ServiceTypes控制:

  • 节点端口:此服务类型将 Kubernetes 每个节点上的外部端口映射到为服务配置的内部集群 IP 和端口。这为您的服务创建了几个外部连接点,随着节点的进出可能会发生变化,这使得创建稳定的外部服务端点变得困难。

  • 负载均衡器:表示专用的外部第 4 层(TCP 或 UDP)负载均衡器,专门映射到您的服务。部署的实际负载均衡器取决于您的目标平台 - 例如,对于 AWS,将创建一个经典的弹性负载均衡器。这是一个非常受欢迎的选项,但一个重要的限制是每个服务都会创建一个负载均衡器,这意味着如果您有很多服务,这个选项可能会变得非常昂贵。

  • Ingress:这是一个共享的第 7 层(HTTP)负载均衡器资源,其工作方式类似于 AWS 应用程序负载均衡器,其中对单个 HTTP/HTTPS 端点的连接可以根据主机标头或 URL 路径模式路由到多个服务。鉴于您可以跨多个服务共享一个负载均衡器,因此这被认为是基于 HTTP 的服务的最佳选择。

发布您的服务的最流行的方法是使用负载均衡器方法,其工作方式如下图所示:

Kubernetes 中的负载均衡

外部负载均衡器发布客户端将连接到的外部服务端点,在前面的示例中是192.0.2.43:80。负载均衡器服务端点将与具有与服务关联的活动 pod 的集群中的节点相关联,每个节点都通过kube-proxy组件设置了节点端口映射。然后,节点端口映射将映射到节点上的每个本地端点,从而实现在整个集群中高效均匀地进行负载平衡。

对于集群内部客户端的通信,通信仍然使用服务集群 IP 地址,就像本章前面描述的那样。

在本章后面,我们将看到如何将 AWS 负载均衡器与 EKS 集成,但是目前您的本地 Docker 桌面环境包括对其自己的负载均衡器资源的支持,该资源会在您的主机上发布一个外部端点供您的服务使用。向服务添加外部负载均衡器非常简单,就像在以下示例中演示的那样,我们修改了k8s/app/deployments.yaml文件中的配置,该文件位于 todobackend 存储库中:

apiVersion: v1
kind: Service
metadata:
  name: todobackend
spec:
  selector:
    app: todobackend
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8000 type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend
  labels:
    app: todobackend
...
...

为了在您的环境中部署适当的负载均衡器,所需的全部就是将spec.type属性设置为LoadBalancer,Kubernetes 将自动创建一个外部负载均衡器。您可以通过应用更新后的配置并运行kubectl get svc命令来测试这一点:

> kubectl apply -f k8s/app/deployment.yaml
service "todobackend" configured
deployment.apps "todobackend" unchanged
> kubectl get svc
NAME                 TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes           ClusterIP      10.96.0.1       <none>        443/TCP        8h
todobackend          LoadBalancer   10.103.210.17   localhost     80:31417/TCP   10s
> curl localhost
{"todos":"http://localhost/todos"}

请注意,kubectl get svc输出现在显示 todobackend 服务的外部 IP 地址为 localhost(当使用 Docker Desktop 时,localhost 始终是 Docker 客户端可访问的外部接口),并且它在端口 80 上外部发布,您可以通过运行curl localhost命令来验证这一点。外部端口映射到单节点集群上的端口 31417,这是kube-proxy组件监听的端口,以支持我们之前描述的负载均衡器架构。

向您的 pods 添加卷

现在我们已经了解了如何在 Kubernetes 集群内部和外部发布我们的应用程序,我们可以专注于通过添加对 todobackend 应用程序的各种部署活动和依赖项的支持,使 todobackend 应用程序完全功能。

首先,我们将解决为 todobackend 应用程序提供静态内容的问题 - 正如您从之前的章节中了解的那样,我们需要运行collectstatic任务,以确保 todobackend 应用程序的静态内容可用,并且应该在部署 todobackend 应用程序时运行。collectstatic任务需要将静态内容写入一个卷,然后由主应用程序容器挂载,因此让我们讨论如何向 Kubernetes pods 添加卷。

Kubernetes 具有强大的存储子系统,支持各种卷类型,您可以在kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes上阅读更多信息。对于collectstatic用例,emptyDir卷类型是合适的,这是一个遵循每个 pod 生命周期的卷 - 它会随着 pod 的创建和销毁而动态创建和销毁 - 因此它适用于诸如缓存和提供静态内容之类的用例,这些内容在 pod 创建时可以轻松重新生成。

以下示例演示了向k8s/app/deployment.yaml文件添加公共emptyDir卷:

...
...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend
  labels:
    app: todobackend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: todobackend
  template:
    metadata:
      labels:
        app: todobackend
    spec:
      securityContext:
 fsGroup: 1000
 volumes:
 - name: public
 emptyDir: {}
      containers:
      - name: todobackend
        image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
        imagePullPolicy: IfNotPresent
        readinessProbe:
          httpGet:
            port: 8000
        livenessProbe:
          httpGet:
            port: 8000
        volumeMounts:
 - name: public
 mountPath: /public
        command:
        - uwsgi
        - --http=0.0.0.0:8000
        - --module=todobackend.wsgi
        - --master
        - --die-on-term
        - --processes=4
        - --threads=2
        - --check-static=/public
        env:
        - name: DJANGO_SETTINGS_MODULE
          value: todobackend.settings_release

我们在 pod 模板的 spec.Volumes 属性中定义了一个名为 public 的卷,然后在 todobackend 容器定义中使用 volumeMounts 属性将 public 卷挂载到 /public。我们的用例的一个重要配置要求是设置 spec.securityContext.fsGroup 属性,该属性定义了将配置为文件系统挂载点的组所有者的组 ID。我们将此值设置为 1000;回想一下前几章中提到的,todobackend 映像以 app 用户运行,其用户/组 ID 为 1000。此配置确保 todobackend 容器能够读取和写入 public 卷的静态内容。

如果您现在部署配置更改,您应该能够使用 kubectl exec 命令来检查 todobackend 容器文件系统,并验证我们能够读取和写入 /public 挂载点:

> kubectl apply -f k8s/app/deployment.yaml
service "todobackend" unchanged
deployment.apps "todobackend" configured
> kubectl exec $(kubectl get pods -l app=todobackend -o=jsonpath='{.items[0].metadata.name}') \
    -it bash
bash-4.4$ touch /public/foo
bash-4.4$ ls -l /public/foo
-rw-r--r-- 1 app app 0 Jul 26 11:28 /public/foo
bash-4.4$ rm /public/foo

kubectl exec 命令类似于 docker exec 命令,允许您在当前运行的 pod 容器中执行命令。此命令必须引用 pod 的名称,我们使用 kubectl get pods 命令以及 JSON 路径查询来提取此名称。正如您所看到的,todobackend 容器中的 app 用户能够读取和写入 /public 挂载点。

向您的 pod 添加初始化容器

在为静态内容准备了临时卷后,我们现在可以专注于安排 collectstatic 任务来为我们的应用程序生成静态内容。Kubernetes 支持 初始化容器,这是一种特殊类型的容器,在 pod 中启动主应用程序容器之前执行。Kubernetes 将确保您的初始化容器运行完成并成功完成,然后再启动您的应用程序,如果您指定了多个初始化容器,Kubernetes 将按顺序执行它们,直到所有初始化容器都完成。

以下代码演示了向 k8s/app/deployment.yaml 文件添加初始化容器:

...
...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend
  labels:
    app: todobackend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: todobackend
  template:
    metadata:
      labels:
        app: todobackend
    spec:
      securityContext:
        fsGroup: 1000
      volumes:
      - name: public
        emptyDir: {}
 initContainers:
      - name: collectstatic
 image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
 imagePullPolicy: IfNotPresent
 volumeMounts:
 - name: public
 mountPath: /public
 command: ["python3","manage.py","collectstatic","--no-input"]
 env:
 - name: DJANGO_SETTINGS_MODULE
 value: todobackend.settings_release
      containers:
      ...
      ...

您现在可以部署您的更改,并使用 kubectl logs 命令来验证 collectstatic 初始化容器是否成功执行:

> kubectl apply -f k8s/app/deployment.yaml
service "todobackend" unchanged
deployment.apps "todobackend" configured
> kubectl logs $(kubectl get pods -l app=todobackend -o=jsonpath='{.items[0].metadata.name}') \
    -c collectstatic
Copying '/usr/lib/python3.6/site-packages/django/contrib/admin/static/admin/fonts/README.txt'
...
...
159 static files copied to '/public/static'.

如果您现在在浏览器中浏览 http://localhost,您应该能够验证静态内容现在正确呈现:

todobackend 应用程序具有正确的静态内容

添加数据库服务

使 todobackend 应用程序完全功能的下一步是添加一个数据库服务,该服务将托管 todobackend 应用程序数据库。我们将在我们的 Kubernetes 集群中运行此服务,但是在 AWS 中的真实生产用例中,我通常建议使用关系数据库服务(RDS)。

定义数据库服务需要两个主要的配置任务:

  • 创建持久存储

  • 创建数据库服务

创建持久存储

我们的数据库服务的一个关键要求是持久存储,在我们的单节点本地 Kubernetes 开发环境中,hostPath卷类型代表提供简单持久存储需求的标准选项。

虽然您可以通过在卷定义中直接指定路径来轻松创建 hostPath 卷(请参阅kubernetes.io/docs/concepts/storage/volumes/#hostpath中的示例 pod 定义),但这种方法的一个问题是它对底层卷类型创建了硬依赖,并且如果您想要删除 pod 和与卷关联的数据,则需要手动清理。

Docker Desktop Kubernetes 支持的一个非常有用的功能是包含一个名为docker.io/hostpath的动态卷提供程序,它会自动为您创建 hostPath 类型的卷,该卷可通过运行kubectl get sc命令查看的默认storage class来使用:

> kubectl get sc
NAME                 PROVISIONER          AGE
hostpath (default)   docker.io/hostpath   2d

存储类提供了对底层卷类型的抽象,这意味着您的 pod 可以从特定类中请求存储。这包括通用要求,如卷大小,而无需担心底层卷类型。在 Docker Desktop 的情况下,开箱即用包含了一个默认的存储类,它使用 hostPath 卷类型来提供存储请求。

然而,当我们稍后在 AWS 中使用 EKS 设置 Kubernetes 集群时,我们将配置一个使用 AWS Elastic Block Store(EBS)作为底层卷类型的默认存储类。采用这种方法意味着我们不需要更改我们的 pod 定义,因为我们将在每个环境中引用相同的存储类。

如果您正在使用 minikube,名为k8s.io/minikube-hostpath的动态 provisioner 提供了类似于 Docker hostpath provisioner 的功能,但是将卷挂载在/tmp/hostpath-provisioner下。

要使用存储类而不是直接在 pod 定义中指定卷类型,您需要创建持久卷索赔,它提供了存储需求的逻辑定义,如卷大小和访问模式。让我们定义一个持久卷索赔,但在此之前,我们需要在 todobackend 存储库中建立一个名为k8s/db的新文件夹,用于存储我们的数据库服务配置:

todobackend> mkdir -p k8s/db todobackend> touch k8s/db/storage.yaml

在这个文件夹中,我们将创建一个名为k8s/db/storage.yaml的文件,在其中我们将定义一个持久卷索赔。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: todobackend-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi         

我们在一个专用文件中创建索赔(称为todobackend-data),因为这样可以让我们独立管理索赔的生命周期。在前面的示例中未包括的一个属性是spec.storageClassName属性 - 如果省略此属性,将使用默认的存储类,但请记住您可以创建和引用自己的存储类。spec.accessModes属性指定存储应该如何挂载 - 在本地存储和 AWS 中的 EBS 存储的情况下,我们只希望一次只有一个容器能够读写卷,这由ReadWriteOnce访问模式包含。

spec.resources.requests.storage属性指定持久卷的大小,在这种情况下,我们配置为 8GB。

如果您正在使用 Windows 版的 Docker,第一次尝试使用 Docker hostPath provisioner 时,将提示您与 Docker 共享 C:\。

如果您现在使用kubectl部署持久卷索赔,可以使用kubectl get pvc命令查看您新创建的索赔:

> kubectl apply -f k8s/db/storage.yaml
persistentvolumeclaim "todobackend-data" created
> kubectl get pvc
NAME               STATUS  VOLUME                                    CAPACITY  ACCESS MODES STORAGECLASS  AGE
todobackend-data   Bound   pvc-afba5984-9223-11e8-bc1c-025000000001  8Gi       RWO              hostpath      5s

您可以看到,当您创建持久卷索赔时,会动态创建一个持久卷。在使用 Docker Desktop 时,实际上是在路径~/.docker/Volumes/<persistent-volume-claim>/<volume>中创建的。

> ls -l ~/.docker/Volumes/todobackend-data
total 0
drwxr-xr-x 2 jmenga staff 64 28 Jul 17:04 pvc-afba5984-9223-11e8-bc1c-025000000001

如果您正在使用 Windows 版的 Docker 并且正在使用 Windows 子系统用于 Linux,您可以在 Windows 主机上创建一个符号链接到.docker文件夹:

> ln -s /mnt/c/Users/<user-name>/.docker ~/.docker
> ls -l ~/.docker/Volumes/todobackend-data
total 0
drwxrwxrwx 1 jmenga jmenga 4096 Jul 29 17:04 pvc-c02a8614-932d-11e8-b8aa-00155d010401

请注意,如果您按照第一章中的说明进行了设置,容器和 Docker 基础知识,为了设置 Windows Subsystem for Linux,您已经将 /mnt/c/Users/<user-name>/ 配置为您的主目录,因此您不需要执行上述配置。

创建数据库服务

现在我们已经创建了一个持久卷索赔,我们可以定义数据库服务。我们将在 todobackend 仓库中的一个新文件 k8s/db/deployment.yaml 中定义数据库服务,其中我们创建了一个服务和部署定义:

apiVersion: v1
kind: Service
metadata:
  name: todobackend-db
spec:
  selector:
    app: todobackend-db
  clusterIP: None 
  ports:
  - protocol: TCP
    port: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend-db
  labels:
    app: todobackend-db
spec:
  selector:
    matchLabels:
      app: todobackend-db
  template:
    metadata:
      labels:
        app: todobackend-db
    spec:
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: todobackend-data
      containers:
      - name: db
        image: mysql:5.7
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "mysqlshow -h 127.0.0.1 -u $(MYSQL_USER) -p$(cat /tmp/secrets/MYSQL_PASSWORD)"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        args:
        - --ignore-db-dir=lost+found
        env:
        - name: MYSQL_DATABASE
          value: todobackend
        - name: MYSQL_USER
          value: todo
        - name: MYSQL_ROOT_PASSWORD
          value: super-secret-password
        - name: MYSQL_PASSWORD
          value: super-secret-password

我们首先定义一个名为 todobackend-db 的服务,它发布默认的 MySQL TCP 端口 3306。请注意,我们指定了 spec.clusterIP 值为 None,这将创建一个无头服务。无头服务对于单实例服务非常有用,并允许使用 pod 的 IP 地址作为服务端点,而不是使用 kube-proxy 组件与虚拟 IP 地址进行负载均衡到单个端点。定义无头服务仍将发布服务的 DNS 记录,但将该记录与 pod IP 地址关联,确保 todobackend 应用可以通过名称连接到 todobackend-db 服务。然后,我们为 todobackend-db 服务创建一个部署,并定义一个名为 data 的卷,该卷映射到我们之前创建的持久卷索赔,并挂载到 MySQL 容器中的数据库数据目录 (/var/lib/mysql)。请注意,我们指定了 args 属性(在 Docker/Docker Compose 中相当于 CMD/command 指令),它配置 MySQL 忽略 lost+found 目录(如果存在的话)。虽然在使用 Docker Desktop 时这不会成为问题,但在 AWS 中会成为问题,原因与前面的 Docker Swarm 章节中讨论的原因相同。最后,我们创建了一个类型为 exec 的活动探针,执行 mysqlshow 命令来检查在 MySQL 容器内部可以本地进行与 MySQL 数据库的连接。由于 MySQL 密钥位于文件中,我们将 MySQL 命令包装在一个 shell 进程 (/bin/sh) 中,这允许我们使用 $(cat /tmp/secrets/MYSQL_PASSWORD) 命令替换。

Kubernetes 允许您在执行时使用语法$(<environment variable>)来解析环境变量。例如,前面存活探针中包含的$(MYSQL_USER)值将在执行探针时解析为环境变量MYSQL_USER。有关更多详细信息,请参阅kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#use-environment-variables-to-define-arguments

如果您现在部署数据库服务和部署资源,可以使用kubectl get svckubectl get endpoints命令来验证无头服务配置:

> kubectl apply -f k8s/db/deployment.yaml
service "todobackend-db" created
deployment.apps "todobackend-db" created
> kubectl get svc NAME                 TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes           ClusterIP      10.96.0.1       <none>        443/TCP        8h
todobackend          LoadBalancer   10.103.210.17   localhost     80:31417/TCP   1d
todobackend-db       ClusterIP      None            <none>        3306/TCP       6s
> kubectl get endpoints
NAME             ENDPOINTS                       AGE
kubernetes       192.168.65.3:6443               2d
todobackend      10.1.0.44:8000,10.1.0.46:8000   1d
todobackend-db   10.1.0.55:3306                  14s

请注意,todobackend-db服务部署时的集群 IP 为 none,这意味着服务的发布端点是todobackend-db pod 的 IP 地址。

您还可以通过列出本地主机上~/.docker/Volumes/todobackend-data目录中物理卷的内容来验证数据卷是否正确创建:

> ls -l ~/.docker/Volumes/todobackend-data/pvc-afba5984-9223-11e8-bc1c-025000000001
total 387152
-rw-r----- 1 jmenga wheel 56 27 Jul 21:49 auto.cnf
-rw------- 1 jmenga wheel 1675 27 Jul 21:49 ca-key.pem
...
...
drwxr-x--- 3 jmenga wheel 96 27 Jul 21:49 todobackend

如果您现在只删除数据库服务和部署,您应该能够验证持久卷未被删除并持续存在,这意味着您随后可以重新创建数据库服务并重新附加到data卷而不会丢失数据。

> kubectl delete -f k8s/db/deployment.yaml
service "todobackend-db" deleted
deployment.apps "todobackend-db" deleted
> ls -l ~/.docker/Volumes/todobackend-data/pvc-afba5984-9223-11e8-bc1c-025000000001
total 387152
-rw-r----- 1 jmenga wheel 56 27 Jul 21:49 auto.cnf
-rw------- 1 jmenga wheel 1675 27 Jul 21:49 ca-key.pem
...
...
drwxr-x--- 3 jmenga wheel 96 27 Jul 21:49 todobackend
> kubectl apply -f k8s/db/deployment.yaml
service "todobackend-db" created
deployment.apps "todobackend-db" created

前面的代码很好地说明了为什么我们将持久卷索赔分离成自己的文件的原因 - 这样做意味着我们可以轻松地管理数据库服务的生命周期,而不会丢失任何数据。如果您确实想要销毁数据库服务及其数据,您可以选择删除持久卷索赔,这样 Docker Desktop hostPath提供程序将自动删除持久卷和任何存储的数据。

Kubernetes 还支持一种称为 StatefulSet 的控制器,专门用于有状态的应用程序,如数据库。您可以在kubernetes.io/docs/concepts/workloads/controllers/statefulset/上阅读更多关于 StatefulSets 的信息。

创建和使用秘密

Kubernetes 支持secret对象,允许将诸如密码或令牌之类的敏感数据以加密格式安全存储,然后根据需要私密地暴露给您的容器。Kubernetes 秘密以键/值映射或字典格式存储,这与 Docker 秘密不同,正如您在上一章中看到的,Docker 秘密通常只存储秘密值。

您可以使用文字值手动创建秘密,也可以将秘密值包含在文件中并应用该文件。我建议使用文字值创建您的秘密,以避免将您的秘密存储在配置文件中,这可能会意外地提交并推送到您的源代码存储库中。

> kubectl create secret generic todobackend-secret \
 --from-literal=MYSQL_PASSWORD="$(openssl rand -base64 32)" \
 --from-literal=MYSQL_ROOT_PASSWORD="$(openssl rand -base64 32)" \
 --from-literal=SECRET_KEY="$(openssl rand -base64 50)"
secret "todobackend-secret" created
> kubectl describe secrets/todobackend-secret
Name: todobackend-secret
Namespace: default
Labels: <none>
Annotations: <none>

Type: Opaque

Data
====
MYSQL_PASSWORD: 44 bytes
MYSQL_ROOT_PASSWORD: 44 bytes
SECRET_KEY: 69 bytes

在上面的示例中,您使用kubectl create secret generic命令创建了一个名为todobackend-secret的秘密,其中存储了三个秘密值。请注意,每个值都使用与预期环境变量相同的键存储,这将使这些值的配置易于消耗。

现在创建了秘密,您可以配置todobackenddb部署以使用该秘密。Kubernetes 包括一种特殊的卷类型,称为秘密,允许您在容器中的可配置位置挂载您的秘密,然后您的应用程序可以安全和私密地读取。

为数据库服务使用秘密

让我们首先更新k8s/db/deployment.yaml文件中定义的数据库部署资源,以使用todobackend-secret

apiVersion: v1
kind: Service
metadata:
  name: todobackend-db
spec:
  selector:
    app: todobackend-db
  clusterIP: None 
  ports:
  - protocol: TCP
    port: 3306
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend-db
  labels:
    app: todobackend-db
spec:
  selector:
    matchLabels:
      app: todobackend-db
  template:
    metadata:
      labels:
        app: todobackend-db
    spec:
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: todobackend-data
 - name: secrets
 secret:
 secretName: todobackend-secret          items:
 - key: MYSQL_PASSWORD
 path: MYSQL_PASSWORD
 - key: MYSQL_ROOT_PASSWORD
 path: MYSQL_ROOT_PASSWORD
      containers:
      - name: db
        image: mysql:5.7
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "mysqlshow -h 127.0.0.1 -u $(MYSQL_USER) -p$(cat /tmp/secrets/MYSQL_PASSWORD)"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
 - name: secrets
 mountPath: /tmp/secrets
 readOnly: true
        env:
        - name: MYSQL_DATABASE
          value: todobackend
        - name: MYSQL_USER
          value: todo
 - name: MYSQL_ROOT_PASSWORD_FILE
 value: /tmp/secrets/MYSQL_ROOT_PASSWORD
 - name: MYSQL_PASSWORD_FILE
 value: /tmp/secrets/MYSQL_PASSWORD

首先创建一个名为secrets的卷,类型为secret,引用我们之前创建的todobackend-secret。默认情况下,所有秘密项目都将可用,但是您可以通过可选的items属性控制发布到卷的项目。因为todobackend-secret包含特定于 todobackend 应用程序的SECRET_KEY秘密,我们配置items列表以排除此项目,并仅呈现MYSQL_PASSWORDMYSQL_ROOT_PASSWORD键。请注意,指定的path是必需的,并且表示为相对路径,基于秘密卷在每个容器中挂载的位置。

然后,您将secrets卷作为只读挂载到/tmp/secrets中的db容器,并更新与密码相关的环境变量,以引用秘密文件,而不是直接使用环境中的值。请注意,每个秘密值将被创建在基于秘密卷挂载到的文件夹中的键命名的文件中。

要部署我们的新配置,您首先需要删除数据库服务及其关联的持久卷,因为这包括了先前的凭据,然后重新部署数据库服务。您可以通过在执行删除和应用操作时引用整个k8s/db目录来轻松完成此操作,而不是逐个指定每个文件:

> kubectl delete -f k8s/db
service "todobackend-db" deleted
deployment.apps "todobackend-db" deleted
persistentvolumeclaim "todobackend-data" deleted
> kubectl apply -f k8s/db
service "todobackend-db" created
deployment.apps "todobackend-db" created
persistentvolumeclaim "todobackend-data" created

一旦您重新创建了db服务,您可以使用kubectl exec命令来验证MYSQL_PASSWORDMYSQL_ROOT_PASSWORD秘密项目是否已写入/tmp/secrets

> kubectl exec $(kubectl get pods -l app=todobackend-db -o=jsonpath='{.items[0].metadata.name}')\
 ls /tmp/secrets
MYSQL_PASSWORD
MYSQL_ROOT_PASSWORD

为应用程序使用秘密

现在,我们需要通过修改k8s/app/deployment.yaml文件来更新 todobackend 服务以使用我们的秘密:

...
...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todobackend
  labels:
    app: todobackend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: todobackend
  template:
    metadata:
      labels:
        app: todobackend
    spec:
      securityContext:
        fsGroup: 1000
      volumes:
      - name: public
        emptyDir: {}
 - name: secrets
 secret:
 secretName: todobackend-secret
          items:
 - key: MYSQL_PASSWORD
            path: MYSQL_PASSWORD
 - key: SECRET_KEY
            path: SECRET_KEY
      initContainers:
      - name: collectstatic
        image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: public
          mountPath: /public
        command: ["python3","manage.py","collectstatic","--no-input"]
        env:
        - name: DJANGO_SETTINGS_MODULE
          value: todobackend.settings_release
      containers:
      - name: todobackend
        image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
        imagePullPolicy: IfNotPresent
        readinessProbe:
          httpGet:
            port: 8000
        livenessProbe:
          httpGet:
            port: 8000
        volumeMounts:
        - name: public
          mountPath: /public
 - name: secrets
 mountPath: /tmp/secrets
 readOnly: true
        command:
        - uwsgi
        - --http=0.0.0.0:8000
        - --module=todobackend.wsgi
        - --master
        - --die-on-term
        - --processes=4
        - --threads=2
        - --check-static=/public
        env:
        - name: DJANGO_SETTINGS_MODULE
          value: todobackend.settings_release
 - name: SECRETS_ROOT
 value: /tmp/secrets
 - name: MYSQL_HOST
 value: todobackend-db
 - name: MYSQL_USER
 value: todo

您必须定义secrets卷,并确保只有MYSQL_PASSWORDSECRET_KEY项目暴露给todobackend容器。在todobackend应用程序容器中只读挂载卷后,您必须使用SECRETS_ROOT环境变量配置到secrets挂载的路径。回想一下,在上一章中,我们为todobackend应用程序添加了对 Docker 秘密的支持,默认情况下,它期望您的秘密位于/run/secrets。但是,因为/run是一个特殊的 tmpfs 文件系统,您不能在此位置使用常规文件系统挂载您的秘密,因此我们需要配置SECRETS_ROOT环境变量,重新配置应用程序将查找的秘密位置。我们还必须配置MYSQL_HOSTMYSQL_USER环境变量,以便与MYSQL_PASSWORD秘密一起,todobackend应用程序具有连接到数据库服务所需的信息。

如果您现在部署更改,您应该能够验证todobackend容器中挂载了正确的秘密项目:

> kubectl apply -f k8s/app/
service "todobackend" unchanged
deployment.apps "todobackend" configured
> kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
todobackend-74d47dd994-cpvl7     1/1     Running   0          35s
todobackend-74d47dd994-s2pp8     1/1     Running   0          35s
todobackend-db-574fb5746c-xcg9t  1/1     Running   0          12m
> kubectl exec todobackend-74d47dd994-cpvl7 ls /tmp/secrets
MYSQL_PASSWORD
SECRET_KEY

如果您浏览http://localhost/todos,您应该会收到一个错误,指示数据库表不存在,这意味着应用程序现在成功连接和验证到数据库,但缺少应用程序所需的模式和表。

运行作业

我们的todobackend应用程序几乎完全功能,但是有一个关键的部署任务,我们需要执行,那就是运行数据库迁移,以确保todobackend数据库中存在正确的模式和表。正如您在本书中所看到的,数据库迁移应该在每次部署时只执行一次,而不管我们的应用程序运行的实例数量。Kubernetes 通过一种特殊类型的控制器job支持这种性质的任务,正如其名称所示,运行一个任务或进程(以 pod 的形式)直到作业成功完成。

为了创建所需的数据库迁移任务作业,我们将创建一个名为k8s/app/migrations.yaml的新文件,该文件位于todobackend存储库中,这样可以独立于在同一位置定义的deployment.yaml文件中的其他应用程序资源来运行作业。

apiVersion: batch/v1
kind: Job
metadata:
  name: todobackend-migrate
spec:
  backoffLimit: 4
  template:
    spec:
      restartPolicy: Never
      volumes:
      - name: secrets
        secret:
          secretName: todobackend-secret
          items:
          - key: MYSQL_PASSWORD
            path: MYSQL_PASSWORD
      containers:
      - name: migrate
        image: 385605022855.dkr.ecr.us-east-1.amazonaws.com/docker-in-aws/todobackend
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: secrets
          mountPath: /tmp/secrets
          readOnly: true
        command: ["python3","manage.py","migrate","--no-input"]
        env:
        - name: DJANGO_SETTINGS_MODULE
          value: todobackend.settings_release
        - name: SECRETS_ROOT
          value: /tmp/secrets
        - name: MYSQL_HOST
          value: todobackend-db
        - name: MYSQL_USER
          value: todo

您必须指定一种Job的类型来配置此资源作为作业,大部分情况下,配置与我们之前创建的 pod/deployment 模板非常相似,除了spec.backoffLimit属性,它定义了 Kubernetes 在失败时应尝试重新运行作业的次数,以及模板spec.restartPolicy属性,它应始终设置为Never以用于作业。

如果您现在运行作业,您应该能够验证数据库迁移是否成功运行:

> kubectl apply -f k8s/app
service "todobackend" unchanged
deployment.apps "todobackend" unchanged
job.batch "todobackend-migrate" created
> kubectl get jobs
NAME                  DESIRED   SUCCESSFUL   AGE
todobackend-migrate   1         1            6s
> kubectl logs jobs/todobackend-migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK

在这一点上,您已经成功地部署了 todobackend 应用程序,处于完全功能状态,您应该能够连接到 todobackend 应用程序,并创建、更新和删除待办事项。

创建 EKS 集群

现在您已经对 Kubernetes 有了扎实的了解,并且已经定义了部署和本地运行 todobackend 应用程序所需的核心资源,是时候将我们的注意力转向弹性 Kubernetes 服务(EKS)了。

EKS 支持的核心资源是 EKS 集群,它代表了一个完全托管、高可用的 Kubernetes 管理器集群,为您处理 Kubernetes 控制平面。在本节中,我们将重点关注在 AWS 中创建 EKS 集群,建立对集群的认证和访问,并部署 Kubernetes 仪表板。

创建 EKS 集群包括以下主要任务:

  • 安装客户端组件:为了管理您的 EKS 集群,您需要安装各种客户端组件,包括kubectl(您已经安装了)和 AWS IAM 认证器用于 Kubernetes 工具。

  • 创建集群资源:这建立了 Kubernetes 的控制平面组件,包括 Kubernetes 主节点。在使用 EKS 时,主节点作为一个完全托管的服务提供。

  • 为 EKS 配置 kubectl:这允许您使用本地 kubectl 客户端管理 EKS。

  • 创建工作节点:这包括用于运行容器工作负载的 Kubernetes 节点。在使用 EKS 时,您需要负责创建自己的工作节点,通常会以 EC2 自动扩展组的形式部署。就像对于 ECS 服务一样,AWS 提供了一个 EKS 优化的 AMI(docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html),其中包括所有必要的软件组件,使工作节点能够加入您的 EKS 集群。

  • 部署 Kubernetes 仪表板:Kubernetes 仪表板为您提供了一个基于 Web 的管理界面,用于管理和监视您的集群和容器应用程序。

在撰写本文时,EKS 集群不属于 AWS 免费套餐,并且每分钟收费 0.20 美元,因此在继续之前请记住这一点(请参阅aws.amazon.com/eks/pricing/获取最新定价信息)。我们将使用 CloudFormation 模板来部署 EKS 集群和 EKS 工作节点,因此您可以根据需要轻松拆除和重新创建 EKS 集群和工作节点,以减少成本。

安装客户端组件

要管理您的 EKS 集群,您必须安装kubectl,以及 AWS IAM 认证器用于 Kubernetes 组件,它允许kubectl使用您的 IAM 凭据对您的 EKS 集群进行身份验证。

您已经安装了kubectl,因此要安装用于 Kubernetes 的 AWS IAM 认证器,您需要安装一个名为aws-iam-authenticator的二进制文件,该文件由 AWS 发布如下:

> curl -fs -o /usr/local/bin/aws-iam-authenticator https://amazon-eks.s3-us-west-2.amazonaws.com/1.10.3/2018-07-26/bin/darwin/amd64/aws-iam-authenticator
> chmod +x /usr/local/bin/aws-iam-authenticator

创建集群资源

在创建您的 EKS 集群之前,您需要确保您的 AWS 账户满足以下先决条件:

  • VPC 资源:EKS 资源必须部署到具有至少三个子网的 VPC 中。AWS 建议您为每个 EKS 集群创建自己的专用 VPC 和子网,但是在本章中,我们将使用在您的 AWS 账户中自动创建的默认 VPC 和子网。请注意,当您创建 VPC 并定义集群将使用的子网时,您必须指定所有子网,您期望您的工作节点负载均衡器将被部署在其中。一个推荐的模式是在私有子网中部署您的工作节点,并确保您还包括了公共子网,以便 EKS 根据需要创建面向公众的负载均衡器。

  • EKS 服务角色:在创建 EKS 集群时,您必须指定一个 IAM 角色,该角色授予 EKS 服务管理您的集群的访问权限。

  • 控制平面安全组:您必须提供一个用于 EKS 集群管理器和工作节点之间的控制平面通信的安全组。安全组规则将由 EKS 服务修改,因此您应为此要求创建一个新的空安全组。

AWS 文档包括一个入门(docs.aws.amazon.com/eks/latest/userguide/getting-started.html)部分,其中提供了如何使用 AWS 控制台创建 EKS 集群的详细信息。鉴于 EKS 受 CloudFormation 支持,并且我们在本书中一直使用的基础设施即代码方法,我们需要在todobackend-aws存储库中创建一个名为eks的文件夹,并在一个名为todobackend-aws/eks/stack.yml的新 CloudFormation 模板文件中定义我们的 EKS 集群和相关的 EKS 服务角色:

AWSTemplateFormatVersion: "2010-09-09"

Description: EKS Cluster

Parameters:
  Subnets:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Target subnets for EKS cluster
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Target VPC

Resources:
  EksServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: eks-service-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - eks.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
        - arn:aws:iam::aws:policy/AmazonEKSServicePolicy
  EksClusterSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: eks-cluster-control-plane-sg
      GroupDescription: EKS Cluster Control Plane Security Group
      VpcId: !Ref VpcId
      Tags:
        - Key: Name
          Value: eks-cluster-sg
  EksCluster:
    Type: AWS::EKS::Cluster
    Properties:
      Name: eks-cluster
      RoleArn: !Sub ${EksServiceRole.Arn}
      ResourcesVpcConfig:
        SubnetIds: !Ref Subnets
        SecurityGroupIds: 
          - !Ref EksClusterSecurityGroup

模板需要两个输入参数 - 目标 VPC ID 和目标子网 ID。EksServiceRole资源创建了一个 IAM 角色,授予eks.awsamazon.com服务代表您管理 EKS 集群的能力,如ManagedPolicyArns属性中引用的托管策略所指定的。然后,您必须为控制平面通信定义一个空安全组,并最后定义 EKS 集群资源,引用EksServiceRole资源的RoleArn属性,并定义一个针对输入ApplicationSubnets的 VPC 配置,并使用EksClusterSecurityGroup资源。

现在,您可以使用aws cloudformation deploy命令部署此模板,如下所示:

> export AWS_PROFILE=docker-in-aws
> aws cloudformation deploy --template-file stack.yml --stack-name eks-cluster \
--parameter-overrides VpcId=vpc-f8233a80 Subnets=subnet-a5d3ecee,subnet-324e246f,subnet-d281a2b6\
--capabilities CAPABILITY_NAMED_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - eks-cluster

集群将大约需要 10 分钟来创建,一旦创建完成,您可以使用 AWS CLI 获取有关集群的更多信息:

> aws eks describe-cluster --name eks-cluster --query cluster.status "ACTIVE"
> aws eks describe-cluster --name eks-cluster --query cluster.endpoint
"https://E7B5C85713AD5B11625D7A689F99383F.sk1.us-east-1.eks.amazonaws.com"
> aws eks describe-cluster --name eks-cluster --query cluster.certificateAuthority.data
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNE1EY3lNakV3TURRME9Gb1hEVEk0TURjeE9URXdNRFEwT0Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBUEh5CkVsajhLMUQ4M1V3RDFmdlhqYi9TdGZBK0tvWEtZNkVtZEhudnNXeWh1Snd2aGhkZDU2M0tVdGJnYW15Z0pxMVIKQkNCTWptWXVocG8rWm0ySEJrckZGakFFZDVIN1lWUXVOSm15TXdrQVV5MnpFTUU5SjJid3hkVEpqZ3pZdmlwVgpJc05zd3pIL1lSa1NVSElDK0VSaCtURmZJODhsTTBiZlM1R1pueUx0VkZCS3RjNGxBREVxRE1BTkFoaEc5OVZ3Cm5hL2w5THU2aW1jT1VOVGVCRFB0L1hxNGF3TFNUOEgwQlVvWGFwbEt0cFkvOFdqR055RUhzUHZHdXNXU3lkTHMKK3lKNXBlUm8yR3Nxc0VqMGhsbHpuV0RXWnlqQVU5Ni82QXVKRGZVSTBING1WNkpCZWxVU0tTRTZBOU1GSjRjYgpHeVpkYmh0akg1d3Zzdit1akNjQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFIRkRIODZnNkNoR2FMejBQb21EK2tyc040SUMKRzhOb0xSc2xkTkJjQmlRczFYK0hKenNxTS9TN0svL1RhUndqVjRZTE1hbnBqWGp4TzRKUWh4Q0ZHR1F2SHptUApST1FhQXRjdWRJUHYySlg5eUlOQW1rT0hDaloyNm1Yazk1b2pjekxQRE1NTlFVR2VmbXUxK282T1ZRUldTKzBMClpta211KzVyQVVFMWtTK00yMDFPeFNGcUNnL0VDd0F4ZXd5YnFMNGw4elpPWCs3VzlyM1duMWh6a3NhSnIrRHkKUVRyQ1p2MWJ0ZENpSnhmbFVxWXN5UEs1UDh4NmhKOGN2RmRFUklFdmtYQm1VbjRkWFBWWU9IdUkwdElnU2h1RAp3K0IxVkVOeUF3ZXpMWWxLaGRQQTV4R1BMN2I0ZmN4UXhCS0VlVHpaUnUxQUhMM1R4THIxcVdWbURUbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="

集群端点和证书颁发机构数据在本章后面都是必需的,因此请注意这些值。

为 EKS 配置 kubectl

使用您创建的 EKS 集群,现在需要将新集群添加到本地的kubectl配置中。kubectl知道的所有集群默认都在一个名为~/.kube/config的文件中定义,目前如果您使用 Docker for Mac 或 Docker for Windows,则该文件将包括一个名为docker-for-desktop-cluster的单个集群。

以下代码演示了将您的 EKS 集群和相关配置添加到~/.kube/config文件中:

apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://localhost:6443
  name: docker-for-desktop-cluster
- cluster:
 certificate-authority-data: <Paste your EKS cluster certificate data here>
 server: https://E7B5C85713AD5B11625D7A689F99383F.sk1.us-east-1.eks.amazonaws.com
 name: eks-cluster
contexts:
- context:
    cluster: docker-for-desktop-cluster
    user: docker-for-desktop
  name: docker-for-desktop
- context:
 cluster: eks-cluster
 user: aws
 name: eks
current-context: docker-for-desktop-cluster
kind: Config
preferences: {}
users:
- name: aws
 user:
 exec:
 apiVersion: client.authentication.k8s.io/v1alpha1
 args:
 - token
 - -i
 - eks-cluster
 command: aws-iam-authenticator
 env:
 - name: AWS_PROFILE
 value: docker-in-aws
- name: docker-for-desktop
  user:
    client-certificate-data: ...
    client-key-data: ...

clusters属性中首先添加一个名为eks-cluster的新集群,指定您在创建 EKS 集群后捕获的证书颁发机构数据和服务器端点。然后添加一个名为eks的上下文,这将允许您在本地 Kubernetes 服务器和 EKS 集群之间切换,并最后在用户部分添加一个名为aws的新用户,该用户由eks上下文用于对 EKS 集群进行身份验证。aws用户配置配置 kubectl 执行您之前安装的aws-iam-authenticator组件,传递参数token -i eks-cluster,并使用您本地的docker-in-aws配置文件进行身份验证访问。执行此命令将自动返回一个身份验证令牌给kubectl,然后可以用于对 EKS 集群进行身份验证。

在上述配置就位后,您现在应该能够访问一个名为eks的新上下文,并验证连接到您的 EKS 集群,如下所示:

> kubectl config get-contexts
CURRENT   NAME                 CLUSTER                      AUTHINFO            NAMESPACE
*         docker-for-desktop   docker-for-desktop-cluster   docker-for-desktop
          eks                  eks-cluster                  aws
> kubectl config use-context eks
Switched to context "eks".
> kubectl get all Assume Role MFA token code: ****
NAME                TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes  ClusterIP   10.100.0.1   <none>        443/TCP   1h

请注意,如果您在前几章中设置了多因素身份验证MFA)配置,每次对您的 EKS 集群运行kubectl命令时,都会提示您输入 MFA 令牌,这将很快变得烦人。

要暂时禁用 MFA,您可以使用aws iam remove-user-from-group命令将用户帐户从用户组中移除:

# Removes user from Users group, removing MFA requirement
# To restore MFA run: aws iam add-user-to-group --user-name justin.menga --group-name Users
> aws iam remove-user-from-group --user-name justin.menga --group-name Users

然后在~/.aws/config文件中为您的本地 AWS 配置文件注释掉mfa_serial行:

[profile docker-in-aws]
source_profile = docker-in-aws
role_arn = arn:aws:iam::385605022855:role/admin
role_session_name=justin.menga
region = us-east-1
# mfa_serial = arn:aws:iam::385605022855:mfa/justin.menga

创建工作节点

设置 EKS 的下一步是创建将加入您的 EKS 集群的工作节点。与由 AWS 完全管理的 Kubernetes 主节点不同,您负责创建和管理您的工作节点。AWS 提供了一个 EKS 优化的 AMI,其中包含加入 EKS 集群并作为 EKS 工作节点运行所需的所有软件。您可以浏览docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html来获取您所在地区的最新 AMI ID:

Amazon EKS-Optimized AMI

在编写本书时,EKS-Optimized AMI 需要使用我们在前几章中学到的cfn-init框架进行广泛配置。创建工作节点的推荐方法是使用由 AWS 发布的预定义 CloudFormation 模板,该模板已经包含了在docs.aws.amazon.com/eks/latest/userguide/launch-workers.html中指定的所需配置:

工作节点 CloudFormation 模板 URL

您现在可以通过在 AWS 控制台中选择服务 | CloudFormation,单击创建堆栈按钮,并粘贴您之前在选择模板部分获取的工作模板 URL 来为您的工作节点创建新的 CloudFormation 堆栈:

创建工作节点 CloudFormation 堆栈

点击下一步后,您将被提示输入堆栈名称(您可以指定一个类似eks-cluster-workers的名称)并提供以下参数:

  • ClusterName:指定您的 EKS 集群的名称(在我们的示例中为eks-cluster)。

  • ClusterControlPlaneSecurityGroup:控制平面安全组的名称。在我们的示例中,我们在创建 EKS 集群时先前创建了一个名为eks-cluster-control-plane-sg的安全组。

  • NodeGroupName:这定义了将为您的工作节点创建的 EC2 自动扩展组名称的一部分。对于我们的情况,您可以指定一个名为eks-cluster-workers或类似的名称。

  • NodeAutoScalingGroupMinSizeNodeAutoScalingGroupMaxSize:默认情况下,分别设置为 1 和 3。请注意,CloudFormation 模板将自动缩放组的期望大小设置为NodeAutoScalingGroupMaxSize参数的值,因此您可能希望降低此值。

  • NodeInstanceType:您可以使用预定义的工作节点 CloudFormation 模板指定的最小实例类型是t2.small。对于 EKS,节点实例类型不仅在 CPU 和内存资源方面很重要,而且还对网络要求的 Pod 容量有影响。EKS 网络模型(docs.aws.amazon.com/eks/latest/userguide/pod-networking.html)将 EKS 集群中的每个 Pod 公开为可在您的 VPC 内访问的 IP 地址,使用弹性网络接口(ENI)和运行在每个 ENI 上的次要 IP 地址的组合。您可以参考docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI,其中描述了各种 EC2 实例类型的每个接口的最大 ENI 和次要 IP 地址的数量,并最终确定了每个节点可以运行的最大 Pod 数量。

  • NodeImageId:指定您所在地区的 EKS 优化 AMI 的 ID(请参阅上面的截图)。

  • KeyName:指定您帐户中现有的 EC2 密钥对(例如,您在本书中之前创建的管理员密钥对)。

  • VpcId:指定您的 EKS 集群所在的 VPC ID。

  • Subnets:指定您希望放置工作节点的子网。

一旦您配置了所需的各种参数,点击下一步按钮两次,最后确认 CloudFormation 可能会在点击创建按钮之前创建 IAM 资源,以部署您的工作节点。当您的堆栈成功创建后,打开堆栈的输出选项卡,并记录NodeInstanceRole输出,这是下一个配置步骤所需的:

获取 NodeInstanceRole 输出

将工作节点加入您的 EKS 集群

CloudFormation 堆栈成功部署后,您的工作节点将尝试加入您的集群,但是在此之前,您需要通过将名为aws-auth的 AWS 认证器ConfigMap资源应用到您的集群来授予对工作节点的 EC2 实例角色的访问权限。

ConfigMap 只是一个键/值数据结构,用于存储配置数据,可以被集群中的不同资源使用。 aws-auth ConfigMap 被 EKS 用于授予 AWS 用户与您的集群进行交互的能力,您可以在docs.aws.amazon.com/eks/latest/userguide/add-user-role.html上了解更多信息。您还可以从amazon-eks.s3-us-west-2.amazonaws.com/1.10.3/2018-06-05/aws-auth-cm.yaml下载一个示例aws-auth ConfigMap。

创建aws-auth ConfigMap, 在todobackend-aws/eks文件夹中创建一个名为aws-auth-cm.yaml的文件:

apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: arn:aws:iam::847222289464:role/eks-cluster-workers-NodeInstanceRole-RYP3UYR8QBYA
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes

在上面的示例中,您需要粘贴在创建工作节点 CloudFormation 堆栈时获得的NodeInstanceRole输出的值。创建此文件后,您现在可以使用kubectl apply命令将其应用到您的 EKS 集群,然后通过运行kubectl get nodes --watch等待您的工作节点加入集群:

> kubectl apply -f aws-auth-cm.yaml
configmap "aws-auth" created
> **kubectl get nodes --watch
NAME                                          STATUS     ROLES    AGE   VERSION
ip-172-31-15-111.us-west-2.compute.internal   NotReady   <none>   20s   v1.10.3
ip-172-31-28-179.us-west-2.compute.internal   NotReady   <none>   16s   v1.10.3
ip-172-31-38-41.us-west-2.compute.internal    NotReady   <none>   13s   v1.10.3
ip-172-31-15-111.us-west-2.compute.internal   NotReady   <none>   23s   v1.10.3
ip-172-31-28-179.us-west-2.compute.internal   NotReady   <none>   22s   v1.10.3
ip-172-31-38-41.us-west-2.compute.internal    NotReady   <none>   22s   v1.10.3
ip-172-31-15-111.us-west-2.compute.internal   Ready      <none>   33s   v1.10.3
ip-172-31-28-179.us-west-2.compute.internal   Ready      <none>   32s   v1.10.3
ip-172-31-38-41.us-west-2.compute.internal    Ready      <none>   32s   v1.10.3

一旦您的所有工作节点的状态都为Ready,您已成功将工作节点加入您的 EKS 集群。

部署 Kubernetes 仪表板

设置 EKS 集群的最后一步是将 Kubernetes 仪表板部署到您的集群。Kubernetes 仪表板是一个功能强大且全面的基于 Web 的管理界面,用于管理和监视集群和容器应用程序,并部署为 Kubernetes 集群的 kube-system 命名空间中的基于容器的应用程序。仪表板由许多组件组成,我在这里不会详细介绍,但您可以在 github.com/kubernetes/dashboard 上阅读更多关于仪表板的信息。

要部署仪表板,我们将首先创建一个名为 todobackend-aws/eks/dashboard 的文件夹,并继续下载和应用组成该仪表板的各种组件到此文件夹:

> **curl -fs -O https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml
> **curl -fs -O https://raw.githubusercontent.com/kubernetes/heapster/master/deploy/kube-config/influxdb/heapster.yaml
> **curl -fs -O https://raw.githubusercontent.com/kubernetes/heapster/master/deploy/kube-config/influxdb/influxdb.yaml
> **curl -fs -O https://raw.githubusercontent.com/kubernetes/heapster/master/deploy/kube-config/rbac/heapster-rbac.yaml** > **kubectl apply -f kubernetes-dashboard.yaml
secret "kubernetes-dashboard-certs" created
serviceaccount "kubernetes-dashboard" created
role.rbac.authorization.k8s.io "kubernetes-dashboard-minimal" created
rolebinding.rbac.authorization.k8s.io "kubernetes-dashboard-minimal" created
deployment.apps "kubernetes-dashboard" created
service "kubernetes-dashboard" created
> **kubectl apply -f heapster.yaml** serviceaccount "heapster" createddeployment.extensions "heapster" createdservice "heapster" created
> **kubectl apply -f influxdb.yaml
deployment.extensions "monitoring-influxdb" created
service "monitoring-influxdb" created
> **kubectl apply -f heapster-rbac.yaml** clusterrolebinding.rbac.authorization.k8s.io "heapster" created

然后,您需要创建一个名为 eks-admin.yaml 的文件,该文件创建一个具有完整集群管理员特权的服务帐户和集群角色绑定:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: eks-admin
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: eks-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: eks-admin
  namespace: kube-system

创建此文件后,您需要将其应用于您的 EKS 集群:

> **kubectl apply -f eks-admin.yaml
serviceaccount "eks-admin" created
clusterrolebinding.rbac.authorization.k8s.io "eks-admin" created

有了 eks-admin 服务帐户,您可以通过运行以下命令检索此帐户的身份验证令牌:

> **kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep eks-admin | awk '{print $1}')
Name: eks-admin-token-24kh4
Namespace: kube-system
Labels: <none>
Annotations: kubernetes.io/service-account.name=eks-admin
              kubernetes.io/service-account.uid=6d8ba3f6-8dba-11e8-b132-02b2aa7ab028

Type: kubernetes.io/service-account-token

Data
====
namespace: 11 bytes
token: **eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJla3MtYWRtaW4tdG9rZW4tMjRraDQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZWtzLWFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNmQ4YmEzZjYtOGRiYS0xMWU4LWIxMzItMDJiMmFhN2FiMDI4Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmVrcy1hZG1pbiJ9.h7hchmhGUZKjdnZRk4U1RZVS7P1tvp3TAyo10TnYI_3AOhA75gC6BlQz4yZSC72fq2rqvKzUvBqosqKmJcEKI_d6Wb8UTfFKZPFiC_USlDpnEp2e8Q9jJYHPKPYEIl9dkyd1Po6er5k6hAzY1O1Dx0RFdfTaxUhfb3zfvEN-X56M34B_Gn3FPWHIVYEwHCGcSXVhplVMMXvjfpQ-0b_1La8fb31JcnD48UolkJ1Z_DH3zsVjIR9BfcuPRoooHYQb4blgAJ4XtQYQans07bKD9lmfnQvNpaCdXV_lGOx_I5vEbc8CQKTBdJkCXaWEiwahsfwQrYtfoBlIdO5IvzZ5mg
ca.crt: 1025 bytes

在前面的例子中,关键信息是令牌值,连接到仪表板时需要复制和粘贴。要连接到仪表板,您需要启动 kubectl 代理,该代理提供对 Kubernetes API 的 HTTP 访问:

> **kubectl proxy** Starting to serve on 127.0.0.1:8001

如果您现在浏览到 http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/,您将被提示登录到仪表板,您需要粘贴之前为 eks-admin 服务帐户检索的令牌:

登录 Kubernetes 仪表板

一旦您登录,如果将 Namespace 更改为 kube-system 并选择 Workloads | Deployments,可能会显示一个错误,指示找不到 monitoring-influxdb 部署的图像:

Kubernetes 仪表板部署失败

如果是这种情况,您需要更新之前下载的 todobackend-aws/eks/dashboard/influxdb.yml 文件,以引用 k8s.gcr.io/heapster-influxdb-amd64:v1.3.3(这是一个已知问题(https://github.com/kubernetes/heapster/issues/2059)可能在您阅读本章时存在或不存在):

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
 name: monitoring-influxdb
 namespace: kube-system
spec:
 replicas: 1
 template:
 metadata:
 labels:
 task: monitoring
 k8s-app: influxdb
 spec:
 containers:
 - name: influxdb
 image: k8s.gcr.io/heapster-influxdb-amd64:v1.3.3
...
...

如果您现在通过运行kubectl apply -f influxdb.yml重新应用文件,则仪表板应该显示所有服务都按预期运行。

将示例应用程序部署到 EKS

现在我们的 EKS 集群和工作节点已经就位,并且我们已经确认可以向集群部署,是时候将 todobackend 应用程序部署到 EKS 了。在本地定义了在 Kubernetes 中运行应用程序所需的各种资源时,您已经在之前完成了大部分艰苦的工作,现在所需的只是调整一些外部资源,例如负载均衡器和数据库服务的持久卷,以使用 AWS 原生服务。

现在您需要执行以下配置任务:

  • 使用 AWS Elastic Block Store(EBS)配置持久卷支持

  • 配置支持 AWS Elastic Load Balancers

  • 部署示例应用程序

使用 AWS EBS 配置持久卷支持

在本章的前面,我们讨论了持久卷索赔和存储类的概念,这使您可以将存储基础设施的细节与应用程序分离。我们了解到,在使用 Docker Desktop 时,提供了一个默认的存储类,它将自动创建类型为 hostPath 的持久卷,这些持久卷可以从本地操作系统的~/.docker/Volumes访问,这样在使用 Docker Desktop 与 Kubernetes 时就可以轻松地提供、管理和维护持久卷。

在使用 EKS 时,重要的是要了解,默认情况下,不会为您创建任何存储类。这要求您至少创建一个存储类,如果要支持持久卷索赔,并且在大多数用例中,您通常会定义一个提供标准默认存储介质和卷类型的默认存储类,以支持您的集群。在使用 EKS 时,这些存储类的一个很好的候选者是弹性块存储(EBS),它为在集群中作为工作节点运行的 EC2 实例提供了一种标准的集成机制来支持基于块的卷存储。Kubernetes 支持一种名为AWSElasticBlockStore的卷类型,它允许您从工作节点访问和挂载 EBS 卷,并且还包括对名为aws-ebs的存储供应商的支持,该供应商提供 EBS 卷的动态提供和管理。

在这个原生支持 AWS EBS 的基础上,非常容易创建一个默认的存储类,它将自动提供 EBS 存储,我们将在名为todobackend-aws/eks/gp2-storage-class.yaml的文件中定义它。

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: gp2
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Delete
mountOptions:
  - debug

我们将创建一个名为gp2的存储类,顾名思义,它将使用kubernetes.io/aws-ebs存储供应程序从 AWS 提供gp2类型或 SSD 的 EBS 存储。parameters部分控制此存储选择,根据存储类型,可能有其他配置选项可用,您可以在kubernetes.io/docs/concepts/storage/storage-classes/#aws了解更多信息。reclaimPolicy的值可以是RetainDelete,它控制存储供应程序在从 Kubernetes 中删除与存储类关联的持久卷索赔时是否保留或删除关联的 EBS 卷。对于生产用例,您通常会将其设置为Retain,但对于非生产环境,您可能希望将其设置为默认的回收策略Delete,以免手动清理不再被集群使用的 EBS 卷。

现在,让我们在我们的 EKS 集群中创建这个存储类,之后我们可以配置新的存储类为集群的默认存储类。

> kubectl get sc
No resources found.
> kubectl apply -f eks/gp2-storage-class.yaml
storageclass.storage.k8s.io "gp2" created
> kubectl patch storageclass gp2 \
 -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' storageclass.storage.k8s.io "gp2" patched
> kubectl describe sc/gp2 Name: gp2
IsDefaultClass: Yes
Annotations: ...
Provisioner: kubernetes.io/aws-ebs
Parameters: type=gp2
AllowVolumeExpansion: <unset>
MountOptions:
  debug
ReclaimPolicy: Delete
VolumeBindingMode: Immediate
Events: <none>

创建存储类后,您可以使用kubectl patch命令向存储类添加注释,将该类配置为默认类。当您运行kubectl describe sc/gp2命令查看存储类的详细信息时,您会看到IsDefaultClass属性设置为Yes,确认新创建的类是集群的默认存储类。

有了这个,todobackend应用程序的 Kubernetes 配置现在有了一个默认的存储类,可以应用于todobackend-data持久卷索赔,它将根据存储类参数提供一个gp2类型的 EBS 卷。

在本章的前面创建的eksServiceRole IAM 角色包括AmazonEKSClusterPolicy托管策略,该策略授予您的 EKS 集群管理 EBS 卷的能力。如果您选择为 EKS 服务角色实现自定义 IAM 策略,您必须确保包括用于管理卷的各种 EC2 IAM 权限,例如ec2:AttachVolumeec2:DetachVolumeec2:CreateVolumesec2:DeleteVolumesec2:DescribeVolumesec2:ModifyVolumes(这不是详尽的清单)。有关由 AWS 定义的 EKS 服务角色和托管策略授予的 IAM 权限的完整清单,请参阅docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html

配置对 AWS 弹性负载均衡器的支持

在本章的前面,当您为 todobackend 应用程序定义 Kubernetes 配置时,您创建了一个类型为LoadBalancer的 todobackend 应用程序的服务。我们讨论了负载均衡器的实现细节是特定于部署到的 Kubernetes 集群的平台的,并且在 Docker Desktop 的情况下,Docker 提供了自己的负载均衡器组件,允许服务暴露给开发机器上的本地网络接口。

在使用 EKS 时,好消息是您不需要做任何事情来支持LoadBalancer类型的服务 - 您的 EKS 集群将自动为每个服务端点创建并关联一个 AWS 弹性负载均衡器,AmazonEKSClusterPolicy托管策略授予了所需的 IAM 权限。

Kubernetes 确实允许您通过配置注释来配置LoadBalancer类型的供应商特定功能,这是一种元数据属性,将被给定供应商在其目标平台上理解,并且如果在不同平台上部署,比如您的本地 Docker Desktop 环境,将被忽略。您可以在kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types了解更多关于这些注释的信息,以下示例演示了向todobackend/k8s/app/deployment.yaml文件中的服务定义添加了几个特定于 AWS 弹性负载均衡器的注释:

apiVersion: v1
kind: Service
metadata:
  name: todobackend
  annotations:
 service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
 service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
 service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "60"
spec:
  selector:
    app: todobackend
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8000
  type: LoadBalancer
---
...
...

在前面的示例中,我们添加了以下注释:

  • service.beta.kubernetes.io/aws-load-balancer-backend-protocol: 这将配置后端协议。值为http可确保在传入请求上设置X-Forward-For标头,以便您的 Web 应用程序可以跟踪客户端 IP 地址。

  • service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: 这将启用连接排空。

  • service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: 这指定了连接排空超时。

一个重要的要点是,注释期望每个值都是字符串值,因此请确保引用布尔值,如"true""false",以及任何数值,如"60",如前面的代码所示。

部署示例应用程序

您现在可以准备将示例应用程序部署到 AWS,首先切换到 todobackend 存储库,并确保您正在使用本章前面创建的eks上下文:

todobackend> kubectl config use-context eks
Switched to context "eks".
todobackend> kubectl config get-contexts
CURRENT   NAME                 CLUSTER                      AUTHINFO             NAMESPACE
          docker-for-desktop   docker-for-desktop-cluster   docker-for-desktop
*         eks                  eks-cluster                  aws

创建秘密

请注意,应用程序和数据库服务都依赖于我们在本地 Docker Desktop 上手动创建的秘密,因此您首先需要在 EKS 上下文中创建这些秘密:

> kubectl create secret generic todobackend-secret \
 --from-literal=MYSQL_PASSWORD="$(openssl rand -base64 32)" \
 --from-literal=MYSQL_ROOT_PASSWORD="$(openssl rand -base64 32)" \
 --from-literal=SECRET_KEY="$(openssl rand -base64 50)"
secret "todobackend-secret" created

部署数据库服务

现在可以部署数据库服务,这应该根据您之前创建的默认存储类的配置创建一个新的由 EBS 支持的持久卷:

> kubectl apply -f k8s/db
service "todobackend-db" created
deployment.apps "todobackend-db" created
persistentvolumeclaim "todobackend-data" created
> kubectl get pv
NAME                                      CAPACITY STATUS  CLAIM                     STORAGECLASS
pvc-18ac5d3f-925c-11e8-89e1-06186d140068  8Gi      Bound   default/todobackend-data  gp2 

您可以看到已创建了持久卷,如果您在 AWS 控制台中浏览服务 | EC2并从左侧 ELASTIC BLOCK STORAGE 菜单中选择,您应该能够看到持久值的相应 EBS 卷:

查看 EBS 卷

请注意,Kubernetes 使用多个标签标记 EBS 卷,以便轻松识别与给定 EBS 卷关联的哪个持久卷和持久卷索赔。

在 Kubernetes 仪表板中,您可以通过选择工作负载 | 部署来验证todobackend-db部署是否正在运行:

查看 EBS 卷

部署应用程序服务

有了数据库服务,现在可以继续部署应用程序:

> kubectl apply -f k8s/app
service "todobackend" created
deployment.apps "todobackend" created
job.batch "todobackend-migrate" created

部署应用程序将执行以下任务:

  • 创建todobackend-migrate作业,运行数据库迁移

  • 创建 todobackend 部署,其中运行一个 collectstatic initContainer,然后运行主要的 todobackend 应用程序容器

  • 创建 todobackend 服务,将部署一个带有 AWS ELB 前端的新服务

在 Kubernetes 仪表板中,如果选择 发现和负载均衡 | 服务 并选择 todobackend 服务,您可以查看服务的每个内部端点,以及外部负载均衡器端点:

在 Kubernetes 仪表板中查看 todobackend 服务您还可以通过运行 kubectl describe svc/todobackend 命令来获取外部端点 URL。

如果您单击外部端点 URL,您应该能够验证 todobackend 应用程序是完全功能的,所有静态内容都正确显示,并且能够在应用程序数据库中添加、删除和更新待办事项项目:

验证 todobackend 应用程序

拆除示例应用程序

拆除示例应用程序非常简单,如下所示:

> kubectl delete -f k8s/app
service "todobackend" deleted
deployment.apps "todobackend" deleted
job.batch "todobackend-migrate" deleted
> kubectl delete -f k8s/db
service "todobackend-db" deleted
deployment.apps "todobackend-db" deleted
persistentvolumeclaim "todobackend-data" deleted

完成后,您应该能够验证与 todobackend 服务关联的弹性负载均衡器资源已被删除,以及 todobackend 数据库的 EBS 卷已被删除,因为您将默认存储类的回收策略配置为删除。当然,您还应该删除本章前面创建的工作节点堆栈和 EKS 集群堆栈,以避免不必要的费用。

摘要

在本章中,您学习了如何使用 Kubernetes 和 AWS 弹性 Kubernetes 服务 (EKS) 部署 Docker 应用程序。Kubernetes 已经成为了领先的容器管理平台之一,拥有强大的开源社区,而且现在 AWS 支持 Kubernetes 客户使用 EKS 服务,Kubernetes 肯定会更受欢迎。

您首先学会了如何在 Docker Desktop 中利用 Kubernetes 的本机支持,这使得在本地快速启动和运行 Kubernetes 变得非常容易。您学会了如何创建各种核心 Kubernetes 资源,包括 pod、部署、服务、秘密和作业,这些为在 Kubernetes 中运行应用程序提供了基本的构建块。您还学会了如何配置对持久存储的支持,利用持久卷索赔来将应用程序的存储需求与底层存储引擎分离。

然后,您了解了 EKS,并学会了如何创建 EKS 集群以及相关的支持资源,包括运行工作节点的 EC2 自动扩展组。您建立了对 EKS 集群的访问,并通过部署 Kubernetes 仪表板来测试集群是否正常工作,该仪表板为您的集群提供了丰富而强大的管理用户界面。

最后,您开始部署 todobackend 应用程序到 EKS,其中包括与 AWS Elastic Load Balancer(ELB)服务集成以进行外部连接,以及使用 Elastic Block Store(EBS)提供持久存储。这里的一个重要考虑因素是,当在 Docker Desktop 环境中部署时,我们不需要修改我们之前创建的 Kubernetes 配置,除了添加一些注释以控制 todobackend 服务负载均衡器的配置(在使用 Docker Desktop 时会忽略这些注释,因此被视为“安全”的特定于供应商的配置元素)。您应该始终努力实现这个目标,因为这确保了您的应用程序在不同的 Kubernetes 环境中具有最大的可移植性,并且可以轻松地独立部署,而不受基础 Kubernetes 平台的影响,无论是本地开发环境、AWS EKS 还是 Google Kubernetes Engine(GKE)。

好吧,所有美好的事情都必须结束了,现在是时候恭喜并感谢您完成了这本书!写这本书是一件非常愉快的事情,我希望您已经学会了如何利用 Docker 和 AWS 的力量来测试、构建、部署和操作自己的容器应用程序。

问题

  1. True/false: Kubernetes is a native feature of Docker Desktop CE.

  2. 您可以使用 commands 属性在 pod 定义中定义自定义命令字符串,并注意到 entrypoint 脚本容器不再被执行。您如何解决这个问题?

  3. 正确/错误:Kubernetes 包括三种节点类型-管理节点、工作节点和代理节点。

  4. 正确/错误:Kubernetes 提供与 AWS 应用负载均衡器的集成。

  5. 正确/错误:Kubernetes 支持将 EBS 卷重新定位到集群中的其他节点。

  6. 您可以使用哪个组件将 Kubernetes API 暴露给 Web 应用程序?

  7. 正确/错误:Kubernetes 支持与弹性容器注册表的集成。

  8. 什么 Kubernetes 资源提供可用于连接到给定应用程序的多个实例的虚拟 IP 地址?

  9. 什么 Kubernetes 资源适合运行数据库迁移?

  10. 正确/错误:EKS 管理 Kubernetes 管理节点和工作节点。

  11. 在使用 EKS 时,默认存储类提供什么类型的 EBS 存储?

  12. 您想在每次部署需要在启动 Pod 中的主应用程序之前运行的任务。您将如何实现这一点?

进一步阅读

您可以查看以下链接,了解本章涵盖的主题的更多信息:

第十八章:评估

第一章,容器和 Docker 基础知识

  1. 错误 - Docker 客户端通过 Docker API 进行通信。

  2. 错误 - Docker Engine 在 Linux 上本地运行。

  3. 错误 - Docker 镜像会发布到 Docker 注册表以供下载。

  4. 您需要在常规设置下启用在 tcp://localhost:2375 上公开守护程序而不使用 TLS设置,并确保在运行 Docker 客户端的任何位置将 DOCKER_HOST 环境变量设置为localhost:2375

  5. 正确。

  6. 您需要将USER_BASE/bin路径添加到您的PATH环境变量中。您可以通过运行python -m site --user-base命令来确定USER_BASE部分。

第二章,使用 Docker 构建应用程序

  1. 错误 - 您可以使用FROMAS指令来定义多阶段 Dockerfiles - 例如,FROM nginx AS build

  2. 正确。

  3. 正确。

  4. 正确。

  5. 错误 - 默认情况下,docker-compose up命令不会因命令启动的任何服务失败而失败。您可以使用--exit-code-from标志指示特定服务失败是否应导致docker-compose up命令失败。

  6. 正确。

  7. 如果要 Docker Compose 等待直到满足 service_healthy 条件,则必须使用docker-compose up命令。

  8. 您应该使用端口映射只是8000。这将创建一个动态端口映射,其中 Docker Engine 将从 Docker Engine 操作系统的临时端口范围中选择一个可用端口。

  9. Makefile 需要使用单个制表符缩进的配方命令。

  10. $(shell <command>)函数。

  11. 您应该将测试配方添加到.PHONY目标,例如.PHONY: test

  12. buildimage属性。

第三章,开始使用 AWS

  1. 正确。

  2. 错误 - 您应该设置一个管理 IAM 用户来执行账户上的管理操作。根帐户只应用于计费或紧急访问。

  3. 错误 - AWS 最佳实践是创建定义一组 IAM 权限并适用于一个或多个资源的 IAM 角色。然后,根据您的用例,应授予 IAM 用户/组承担特定角色或一组角色的能力。

  4. AdministratorAccess。

  5. pip install awscli --user

  6. 错误 - 您必须存储访问密钥 ID 和秘密访问密钥。

  7. ~/.aws/credentials文件中。

  8. 您需要向配置文件添加mfa_serial参数,并为用户指定 MFA 设备的 ARN。

  9. 正确。

  10. 正确。

  11. 不 - CloudFormation 始终尝试成功创建任何新资源,然后删除旧资源。在这种情况下,因为您定义了一个固定的名称值,CloudFormation 将无法创建具有相同名称的新资源。

第四章,ECS 简介

  1. ECS 集群,ECS 任务定义和 ECS 服务。

  2. 正确。

  3. YAML。

  4. 错误 - 当使用静态端口映射时,每个 ECS 容器实例只能有一个给定静态端口映射的实例(假设单个网络接口)。

  5. 错误 - ECS CLI 仅建议用于沙箱/测试环境。

  6. 您将创建一个 ECS 任务。

  7. 错误 - ECS 任务定义是不可变的,给定任务定义的修订版本不能被修改。但是,您可以创建一个给定 ECS 任务定义的新修订版本,该版本基于先前的修订版本但包括更改。

  8. 错误 - 您需要运行 curl localhost:51678/v1/metadata

第五章,使用 ECR 发布 Docker 镜像

  1. aws ecr get-login

  2. 错误 - 在撰写本文时,ECR 仅支持私有注册表

  3. ECR 生命周期策略 - 请参阅docs.aws.amazon.com/AmazonECR/latest/userguide/LifecyclePolicies.html

  4. 正确

  5. 错误 - 您可以同时使用 ECR 资源策略和/或 IAM 策略来配置对来自同一帐户的 ECR 的访问

  6. 正确

  7. 正确

  8. 错误 - 可能(虽然不是最佳实践)使用 ECR 资源策略来授予 IAM 主体访问权限,例如同一帐户中的 IAM 角色

  9. 正确 - 您必须在源帐户中配置 ECR 资源策略,并在远程帐户中配置 IAM 策略

第六章,构建自定义 ECS 容器实例

  1. variables部分。

  2. 正确。

  3. JSON。

  4. 错误 - 您可以(而且应该)引用环境变量值作为您的 AWS 凭据。

  5. 错误 - 您可以使用清单后处理器(www.packer.io/docs/post-processors/manifest.html)来捕获 AMI ID。

  6. 默认情况下,将创建一个 8 GB 的操作系统分区和一个 22 GB 的设备映射逻辑卷。

  7. 文件提供程序。

  8. 云初始化启动脚本可能正在尝试在 EC2 实例上运行软件包更新。如果没有公共互联网连接,这将在长时间超时后失败。

第七章,创建 ECS 集群

  1. 错误 - EC2 自动缩放组仅支持动态 IP 寻址。

  2. Base64 编码。

  3. 使用AWS::Region伪参数。

  4. 错误 - Ref内部函数可以引用 CloudFormation 模板中的资源和参数。

  5. 您需要首先运行cfn-init来下载 CloudFormation Init 元数据,然后运行cfn-signal来通知 CloudFormation 运行cfn-init的结果。

  6. 您需要确保在 UserData 脚本中将每个实例应该加入的 ECS 集群的名称写入/etc/ecs/ecs.config - 例如,echo "ECS_CLUSTER=<cluster-name>" > /etc/ecs/ecs.config

  7. 错误 - 此命令仅用于创建堆栈。您应该使用aws cloudformation deploy命令根据需要创建和更新堆栈。

  8. 每个实例上的 ECS 代理无法与 ECS 服务 API 通信,目前只能作为公共端点使用。

第八章,使用 ECS 部署应用程序

  1. 正确。

  2. 一个监听器。

  3. 错误 - 一个目标组只有在关联的应用程序负载均衡监听器创建后才能接受注册。

  4. AWS::EC2::SecurityGroupIngressAWS::EC2::SecurityGroupEgress资源。

  5. 您应该将短暂容器定义上的essential属性标记为false

  6. DependsOn参数。

  7. 正确。

  8. CREATEUPDATEDELETE

  9. 与 Lambda 函数关联的 IAM 角色没有权限为 Lambda 函数日志组创建日志流。

第九章,管理秘密

  1. 错误 - KMS 服务允许您使用 AWS 创建的密钥以及您自己的私有密钥。

  2. 一个 KMS 别名

  3. CloudFormation 导出

  4. 错误 - 您可以在可配置的时间内恢复秘密,最长可达 30 天。

  5. AWS CLI 和jq实用程序

  6. 您必须为用于加密秘密值的 KMS 密钥授予kms:Decrypt权限。

  7. NoEcho属性

  8. AWS_DEFAULT_REGION环境变量

第十章,隔离网络访问

  1. 正确。

  2. 您可以使用awsvpc(推荐)或host网络模式,确保您的容器将从附加的 EC2 实例弹性网络接口(ENI)获取 IP 地址。

  3. 错误 - awsvpc网络模式是 ECS 任务网络所必需的。

  4. 您需要确保为 ECS 服务配置的安全组允许从负载均衡器访问。

  5. 您为 ECS 任务定义启用了 ECS 任务网络,但是您的容器在启动时失败,并显示无法访问位于互联网上的位置的错误。您如何解决这个问题?

  6. 两 - 请参阅docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI

  7. 一 - t2.micro 支持最多两个 ENI,但是一个 ENI 必须保留用于操作系统和 ECS 代理通信。任务网络只允许每个 ENI 一个任务定义。

  8. 10 - 鉴于您最多可以在任务网络模式下运行 1 个 ECS 任务定义(请参阅上一个答案),并且您可以在单个 ECS 任务定义中运行多达 10 个容器(请参阅docs.aws.amazon.com/AmazonECS/latest/developerguide/service_limits.html)。

  9. 在使用 awsvpc 网络模式时,必须使用 IP 目标类型。

  10. 您应该从 ECS 服务定义中删除 loadBalancers 属性。

第十一章,管理 ECS 基础设施生命周期

  1. 错误 - 您负责调用和管理 ECS 容器实例的排水。

  2. EC2_INSTANCE_LAUNCHINGEC2_INSTANCE_TERMINATING

  3. ABANDONCONTINUE

  4. 错误 - 您可以将生命周期挂钩发布到 SNS、SQS 或 CloudWatch Events。

  5. 很可能是您的 Lambda 函数由于达到最大函数执行超时时间(5 分钟)而失败,这意味着生命周期挂钩永远不会完成,并最终超时。您应该确保您的 Lambda 函数在即将达到函数执行超时时间时重新发布生命周期挂钩,这将自动重新调用您的函数。

  6. 您应该配置UpdatePolicy属性。

  7. MinSuccessfulInstancesPercent属性设置为 100。

  8. 一个 Lambda 权限。

第十二章,ECS 自动缩放

  1. 错误 - 您负责自动扩展您的 ECS 容器实例。

  2. EC2 自动缩放。

  3. 应用自动缩放。

  4. 使用值 300 配置memoryReservation参数,并使用值 1,024 配置memory参数。

  5. 将 ECS 容器实例的 CPU 单位分配均匀分配到每个 ECS 任务中(即,将每个任务配置为分配 333 个单位的 CPU)。

  6. 正确。

  7. 三。

  8. 在滚动更新期间,您应该禁用自动缩放。您可以通过配置 CloudFormation UpdatePolicy属性的AutoScalingRollingUpdate.SuspendProcesses属性来实现这一点。

  9. 零任务 - 根据集群的当前状态,每个实例上都运行一个 ECS 任务。鉴于每个任务都有一个静态端口映射到 TCP 端口80,您无法安排另一个任务,因为所有端口都在使用中。

  10. 四 - 您应该使用每个容器 500 MB 内存的最坏情况。

第十三章,持续交付 ECS 应用程序

  1. buildspec.yml

  2. 错误 - CodeBuild 使用容器,并包含自己的代理来运行构建脚本

  3. Docker 中的 Docker

  4. CloudFormation 变更集

  5. cloudformation.amazonaws.com

  6. 在尝试推送映像之前,请确保您的构建脚本登录 ECR

  7. 允许codebuild.amazonaws.com服务主体对存储库进行拉取访问

  8. 确保容器正在以特权标志运行

第十四章,Fargate 和 ECS 服务发现

  1. 正确。

  2. 仅支持awsvpc网络模式。

  3. 错误 - 您必须确保 ECS 代理可以通过分配给 Fargate ECS 任务的 ENI 进行通信。

  4. 您需要确保任务定义的 ExecutionRoleArn 属性引用的 IAM 角色允许访问 ECR 存储库。

  5. 不 - Fargate 仅支持 CloudWatch 日志。

  6. 错误 - ECS 服务发现使用 Route53 区域发布服务注册信息。

  7. 服务发现命名空间。

  8. 在配置 Fargate ECS 任务定义时,必须配置受支持的 CPU/内存配置。有关受支持的配置,请参阅docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html

  9. UDP 端口2000

  10. 错误 - 跟踪必须发布到在您的环境中运行的 X-Ray 守护程序。

第十五章,弹性 Beanstalk

  1. 错误 - Elastic Beanstalk 支持单容器和多容器 Docker 应用程序

  2. Dockerrun.aws.json文件。

  3. 正确。

  4. 向用于 Elastic Beanstalk EC2 实例的虚拟机实例角色添加 IAM 权限以拉取 ECR 映像。

  5. 错误 - Elastic Beanstalk 使用绑定挂载卷,这会分配 root:root 权限,导致非 root 容器在写入卷时失败。

  6. 错误 - 您可以将leader_only属性设置为true,以便在container_commands键中仅在一个 Elastic Beanstalk 实例上运行命令。

  7. 错误 - eb ssh命令用于建立对 Elastic Beanstalk EC2 实例的 SSH 访问。

  8. 正确。

第十六章,Docker Swarm 在 AWS 中

  1. 正确。

  2. docker service create

  3. 错误 - Docker Swarm 包括两种节点类型:主节点和从节点。

  4. 错误 - Docker for AWS 与经典的 AWS 弹性负载均衡器集成。

  5. 错误 - 当后端设置为可重定位时,Cloudstore AWS 卷插件会创建一个基于 EBS 的卷。

  6. 错误 - 因为 EBS 卷位于不同的可用区,将首先创建原始卷的快照,然后从快照创建新卷,然后将其附加到新的数据库服务容器。

  7. --with-registry-auth

  8. 您需要安装一个系统组件,该组件将定期自动刷新 Docker 凭据,例如github.com/mRoca/docker-swarm-aws-ecr-auth

  9. 版本 3。

  10. 错误 - 您应该将重启策略配置为neveron-failure

第十七章,弹性 Kubernetes 服务

  1. 正确 - 适用于 Docker CE 18.06 及更高版本

  2. args属性中定义自定义命令字符串(这相当于 Dockerfile 中的 CMD 指令)

  3. 错误 - Kubernetes 包括两种节点类型:管理器和工作节点

  4. 错误 - 在撰写时,Kubernetes 支持与经典弹性负载均衡器的集成

  5. 错误

  6. kube-proxy

  7. 正确

  8. 一个服务

  9. 一个作业

  10. 错误 - EKS 管理 Kubernetes 管理节点

  11. 无 - EKS 中没有默认存储类,您必须创建自己的存储类

  12. 在 pod 中像 initContainer 一样定义任务

posted @ 2024-05-06 18:32  绝不原创的飞龙  阅读(52)  评论(0编辑  收藏  举报