摘要
本文描述如何使用 AWS CloudFormation 创建和管理虚拟私有云(VPC),包括子网、NATting 等。这是关于在构建和管理云资源时将基础设施视为代码的经验。
本文要点
AWS CloudFormation 允许我们在 AWS 环境中实现“基础设施即代码”。
借助 CloudFormation 很容易就可以构建出复杂的虚拟私有云(VPC),并可以自动升级。
我们可以重用 CloudFormation 模板构建用于各种用途的各种资源堆栈。
CloudFormation 提供了伪参数和内部函数。
-我们可以在用完之后把堆栈删除。
本文描述如何使用 AWS CloudFormation 创建和管理虚拟私有云(VPC),包括子网、NATting 等。本文的重点是使用 CloudFormation 和基础设施即代码构建和管理 AWS 资源,有关 VPC 设计的问题介绍得相对较少。
网络专家:由于本文的重点是 CloudFormation,所以你可能对 CIDR 块、路由表等有不同的看法。没问题,你可以根据需要修改这个模板。
转入正题:本文介绍的 CloudFormation 模板源代码可以在GitHub上找到。你可以随意下载、修改和使用此模板(不过我不会为误用承担责任)。
为什么使用 CloudFormation?
你可能想知道,当我们可以通过管理控制台中的 VPC 向导创建 VPC 时,为什么要使用 CloudFormation 来构建 VPC,原因如下。
基础设施即代码:CloudFormation 使我们只用一个步骤就可以创建一个“资源堆栈”。资源是我们创建的东西(EC2 实例、VPC、子网等等),一组这样的资源称为堆栈。我们可以编写一个模板,使用它可以很容易地按照我们的意愿通过一个步骤创建一个网络堆栈。这比通过管理控制台或 CLI 手动创建网络更快,而且可重复,一致性更好。我们可以将模板签入源代码控制,并在任何时候根据需要把它用于任何目的。
可升级:我们可以通过修改 CloudFormation 模板来修改网络堆栈,然后根据修改后的模板修改堆栈。CloudFormation 足够智能,可以通过修改堆栈来匹配模板。
可重用:我们可以重用这个模板,在不同时期、不同区域创建多个不用用途的网络。
漂移检测:CloudFormation 有一个新特性(截止到 2018 年 11 月),可以让我们知道资源是否已经“漂移”出了最初的配置。这可能发生在管理员手动更改资源时,这通常不是成熟的组织所鼓励的做法。
用完即弃:我们很容易在用完之后把堆栈删除。
你需要做什么准备?
学习本教程之前,你需要做好以下准备。
一个 AWS 账户。作为 IAM 用户,你可能可以继续本教程,但你必须拥有创建 VPC、子网、路由表、EC2 实例等的权限。通常,我会建议你创建一个你可以完全控制的私人 AWS 账户。在这个帐户中,创建一个具有全部权限的 IAM 用户,用于日常工作。与本教程相关的成本非常低,特别是当你在用完后把堆栈删除的话。
一个文本编辑器。几乎任何编辑器都可以:Sublime、Atom、nano,或者 Eclipse、IntelliJ、Visual Studio 等成熟的 IDE。你只要确保不使用文字处理器——它会嵌入一些会导致语法错误的特殊字符。我将使用 Visual Studio Code,这是我最喜欢的编辑器(本周)。
开始
创建一个空的 YAML 文件:首先创建一个空文件。保存时将其命名为“MyNetwork.YML”。你可以使用任何自己喜欢的名称,但稍后我会用到这个文件名。一定要使用 YML 扩展名。CloudFormation 支持 JSON 或 YAML,我们将使用后者。主要原因是:1)句法不那么讲究,2)能够在工作中添加注释,没有注释我就记不起一周前在做什么了。
添加样板内容:创建好空文件之后,复制并粘贴下面这个结构,这是任何 CloudFormation 模板都需要的样板内容:
注释:在“#”字符之后的所有内容都是注释。我鼓励你们在自己有新发现时写下自己的注释。我的建议是:仅把注释用于描述任何你必须做的不寻常的事情上,尤其是那些你花了一段时间才发现的事情。想象一下,你的同事正在阅读你的模板,他的问题无法通过已发布的文档得到解答,那就是你的注释。
添加一个 VPC
首先要添加的资源是 VPC 本身。复制这些行,资源段将变成下面这样:
缩进:第一件事,把间距调整好。“Resources:”前面不应该有空格。“VPC:”应该缩进两个空格,“Type”和“Properties”应该再缩进两个空格,等等。不要使用制表符(Tab),除非编辑器会将制表符转换为两个空格。只要知道你正在创建的文件的类型,上面列出的大多数编辑器都会这样做。缩进在 YAML 中非常重要,它是我们为避免 JSON 中的括号和逗号而付出的代价。
VPC:该资源指示 CloudFormation 创建一个 VPC 资源,以及一些基本属性和名称。第一行只有“VPC”——这是我们为了在堆栈中标识这个资源而指定的任意一个名称。令人困惑的是,许多资源类型都具有各自的“名称”属性,那是不同的东西。前者仅用于引用堆栈中的资源,后者是生成的资源的公共名称。
Type: AWS::EC2::VPC:如果你使用谷歌进行搜索,就会看到一个链接,该链接将直接把你带到有关 VPC 资源的 CloudFormation 文档。在这里,你会看到一个与你在这里看到的非常相似的示例。我个人认为,CloudFormation 参考页面是不可或缺的。我承认,复制和粘贴示例代码片段是我自己创建资源的一个起点。
双引号:你可以看到,官方文档在不必要的地方使用了双引号。值 AWS::EC2::VPC 不需要用双引号,下面的“true”值也不需要。我的推测是,AWS 文档团队最初从 JSON 中复制了 YAML 示例,后者几乎需要把所有内容都放在引号中。
Properties:每个资源都有属性。文档会告诉你哪些是必需的,哪些不是。文档没有告诉你可选设置的默认值,也没有解释大多数设置的含义和允许值。一般来说,你需要完全理解所有可能值的含义、语法和所有可能值的结果,但没有人能做到这一点。大概 90%的开发时间都花在了这种辅助研究上。
CidrBlock:我给我的 VPC 指定的 CIDR 是 10.1.0.0/16。如果你需要有关这个值的含义的详细说明,请参阅此描述。但简单地说,我将为 VPC 提供超过 65000 个可能的私有 IP 地址,所有这些都将以“10.1”开头。这对于大多数用例来说已经足够了。
DNS 支持/主机名:这些设置只是为了可以把 DNS 主机名自动分配给在 VPC 中创建的 EC2 实例。关于 DNS 的详细信息这里就不做过多介绍了,这些设置将提供一个名称,通过它我们可以到达 EC2 实例,而不仅仅是一个 IP 地址。不是必需的,但通常有用。
Tags / Key / Value:我们的 VPC 需要一个名称。奇怪的是,这里没有“name”属性要设置。因此,我们将使用一个标签,其“Key”为“Name”,“Value”为我们想要使用的任意 VPC 名。该值将在许多(但不是所有)情况下用作显示名称。
这个值很有意思。我们可以简单地硬编码一个名称,但是如果我们在相同的区域使用这个模板创建两个堆栈,我们就会拥有两个同名的 VPC(这实际上是可以的,可以试一下!)这可能会造成不必要的混乱,因此,我们将采取额外的步骤来动态分配名称。
!Join:”!Join”是 CloudFormation 中称为内部函数(intrinsic function)的一种东西。CloudFormation 大约有 15 个这样的函数,我们将在本文中用到几个。简单地说,!Join 用于将文本字符串连接在一起。第一个中括号中的’'标识是要放在连接值之间的字符。我们不想要,所以就放了一个空字符串。
**!:**在阅读文档时,请注意,函数有一个长格式(fn::Join)和一个可在 YAML 中作为替代使用的短格式( !Join)。你会看到,我用的就是这个。但是要注意,在某些情况下,短格式不可用,通常是在将一个函数嵌套到另一个函数中的情况下。
**!Ref:**下一个函数是引用函数。它引用在其他地方定义的东西,通常是在模板中。你将会看到,这个函数用的很多,因为资源之间经常相互引用。
AWS::StackName:这就是 CloudFormation 中所谓的伪参数。当我们运行模板时,它将解析为堆栈的名称。大约有 7 个伪参数可用来动态确定当前区域、当前用户等。使用这些伪参数可以极大地提高模板的灵活性。在这种情况下,VPC 的名称将与它所属的 CloudFormation 堆栈相呼应。当你拥有由许多不同堆栈创建的大量资源时,这非常有用——请相信我。
-VPC:这只是 VPC 名称的后缀。因此,如果我们通过 CloudFormation 以堆栈名“MyNetwork”运行这个模板,我们的 VPC 将被标记为“MyNetwork-VPC”。这个名称将在许多(但不是全部)列出或显示 VPC 的地方使用。
运行模板
把你所做的工作保存,这还远未完成,但是,你可以使用 CloudFormation 运行这个模板来检查你的工作。
从 AWS 管理控制台
从浏览器打开AWS管理控制台。登录,选择任何区域。在菜单中找到 CloudFormation,如果需要的话,使用搜索功能。登入后,点击“创建堆栈”。选择“上传你自己的模板”选项,然后单击“下一步”。
出现什么错误了吗?如果是这样,就是 CloudFormation 遇到了语法问题。在继续之前修复这些问题。通常,错误是由于使用制表符代替空格、不正常地使用空格、=代替:或使用的编辑器嵌入了特殊字符造成的。
进入下一页后,给堆栈起一个名称。在本文的剩余部分,我将使用“MyNetwork”,但是你可以使用任何你喜欢的名字。继续向导,不输入其他任何内容,创建堆栈。
在堆栈运行时,查看“事件(Events)”和“资源(Resources)”选项卡。这些事件将显示你的 VPC 是在什么时候创建的,以及它是在什么时候成功创建的。当堆栈中的最后一个资源被创建时,堆栈的状态将更改为已成功创建。
出现什么错误了吗?堆栈将显示创建失败的状态。如果这里遇到的问题不是语法上的,那么通常就与你试图创建的内容的逻辑有关。使用事件选项卡查找最早出现的错误。我发现,大多数情况下,信息都足够清晰,可以引导我解决问题,并明确标识出出问题的资源。
有一个重要的概念需要注意,如果在创建堆栈时遇到任何错误,整个堆栈(所有资源)将回滚。这种行为可以被重写,但通常这样就行!通常情况下,从头开始执行每一步要容易得多。
如果堆栈失败,它仍然会显示在堆栈列表中,即使堆栈中没有资源。这样做是为了给你时间来调查错误。确定问题后,使用“删除堆栈”操作。
从命令行界面
如果你安装了AWS CLI,并且已经配置了访问密钥和秘密密钥(任何区域),而且命令提示符与你的 YML 文件位于同一个目录,那么你就可以运行以下命令:
–stack-name: 可以是你喜欢的任何名称,但是我在本文中将始终使用“MyNetwork”。
–template-body:你一直在编辑的文件。
如果有任何语法错误,你立马就可以收到反馈。在继续之前修复这些问题。通常,错误是由于使用制表符代替空格、不正常地使用空格、=代替:或使用的编辑器嵌入了特殊字符造成的。
然而,并不是所有的错误都会被 CloudFormation 立即发现。在创建堆栈几分钟后,你可能会遇到非语法错误(例如权限)。发生这种情况时,你就需要通过定期地检查状态来检测问题。另一个方便的选择是等待堆栈完成,或者错误输出,借助下面这个函数:
当堆栈完成后,可以使用下面的命令进行检查:
检查 VPC:从管理控制台转到 VPC 部分。你可以在列表中找到你的 VPC,以及你提供的名称。当然,这个 VPC 中没有任何内容,但我们将在下一篇文章中讨论它。如果你感兴趣,这个 VPC 或 CloudFormation 的使用都是不收费的,不过下面会有所更改(请参阅 NAT 部分)。
更新堆栈
在继续我们的模板之前,让我们先尝试一下更新堆栈。CloudFormation 其中一个奇妙的特性是能够基于对模板的更改修改堆栈。要演示这一点,请返回到模板并进行以下一项或多项更改:
将 VPC“enableDns*”设置改为 false;
将 VPC 的 tag/value 改为 !Join [’’, [!Ref “AWS::StackName”, “-VPC2” ]]。
保存修改。
从 AWS 管理控制台
从列表中选择现有堆栈并选择“更新堆栈”操作。选择“上传你自己的模板”选项,然后单击“下一步”。继续点击“下一步”,一直到要求你创建更改集的界面。更改集实际上就是 CloudFormation 打算应用于你的资源的更改。CloudFormation 可以轻松地进行一些更改,但有些需要删除和重新创建现有资源。选择最后一个选项来执行更改集,并查看正在进行的更改。
从命令行界面
从命令行界面更新堆栈请使用以下命令:
和之前一样,你可以使用 wait 命令来监控这个过程:
我们的堆栈很小,所以更新应该只需要几分钟。你可能会遇到错误,如果出现这种情况,结果会更令人困惑,因为 CloudFormation 会将你的堆栈还原到它之前的形式。由于各种原因,可能会发生错误,例如删除/重新创建资源会影响堆栈外的另一个资源。
删除堆栈
关于原生云,我们需要熟悉的一个概念是,资源用完即弃。现在我们有了模板,可以随时创建和删除堆栈。
从控制台选择堆栈并执行“删除堆栈”操作,或者从命令行界面运行以下命令:
Internet 网关和附件
回到我们的模板:大多数 VPC 需要连接到互联网。我们将通过为 InternetGatway 和 GatewayAttachment 添加资源来实现。在 VPC 资源下复制以下这些行:
AWS::EC2::InternetGateway: 没有它,我们的 VPC 将无法与公网交互。值得注意的是,你不必将 VPC 连接到互联网,许多组织构建的 VPC 完全脱离公共世界,使用 VPN 连接或 AWS Direct Connect 专门连接到现有的本地网络。
VPCGatewayAttachment: AttachGateway 资源更有趣。这是 VPC 和 InternetGateway 之间真正的钩子。注意属性:VpcId 引用了上面定义的 VPC 资源,而 InternetGatewayId 引用我们的 InternetGateway。当 CloudFormation 运行一个堆栈时,它将尝试同时创建所有资源。然而!Ref 函数意味着一个顺序,CloudFormation 将同时创建 VPC 和 InternetGateway,但是在使用它们创建“附件(attachment)”之前,它们都必须完成。
!Ref: 如上所述,这是 CloudFormation 内置的“引用”函数。这是资源相互引用的主要方式。
稍后,在运行此模板时,你会发现,创建前三个资源(尤其是附件)需要几秒钟。稍后,这个延迟会成为问题,在附件完成之前我们无法与公网进行任何交互。稍后请注意使用“DependsOn”来解决这个问题。
某个子网
现在,使用下面的代码创建一个子网:
PublicSubnetA: 同样,任意资源名称,仅适用于堆栈内。你喜欢怎么叫就怎么叫。名称中的“A”是为了表明将这个子网关联到可用区域“A”(如下)。
**!Ref VPC:**子网必须存在于 VPC 中,所以这就是我们将它们关联起来的方式。VPC 会首先创建,然后把这个子网附加到这个 VPC。
CidrBlock:这个 CIDR 是 VPC(10.1.0 /16)的子区间。本质上,它是指这个子网将包含所有以 10.1.10.*开始的地址。这也意味着子网只有 256 个可用地址(实际上是 251 个,因为 AWS 保留 5 个地址自己用,参见说明)。这有点小,而且我正在造成相当数量的浪费,所以在现实场景中,你可能希望调整这个值。想要深入了解 CIDR,请参见 VPC 部分提供的链接。
AvailabilityZone:每个子网的作用域是特定的可用区域。我们可以简单地硬编码一个值,如“us-east-1a”,但这将我们的模板限制在了维吉尼亚州。最佳实践是构建尽可能与区域无关的模板,因此我们动态地确定 AZ。
**!GetAZs:**另一个内置的 CloudFormation 函数。它返回当前区域中所有可用区域的列表(即不管我们在何处运行堆栈)。例如,如果我们在俄勒冈州运行,返回的列表将是{us-west-2a, us-west-2b, us-west-2c}。关键的是,无论何时调用,它都将以相同的顺序显示这些 AZ。
**!Select:**这个内置函数从指定的列表中选择指定的索引。0 是列表中的第一项,1 是第二项,等等。所以这个表达式表示“给我列表中的第一个 AZ”。如果是在俄勒冈州运行,就会得到“us-west-2a”,如果是在维吉尼亚州运行,就会得到“us-east-1a”等等。总之,我们的子网会自动把自己与它在其中运行的区域中的第一个 AZ 关联起来。
Tags:和前面一样,我们想给子网一个简单的名称,但是没有 name 属性可用。使用“Name”标记提供一个在许多(但不是全部)需要列出或显示子网的地方使用的名称。
**!Sub:**这是替换函数。它获取后面的字符串,并动态替换在 ${}标记中找到的任何值。在我们的示例中,由于 AWS::StackName 会被翻译成“MyNetwork”,所以得到的子网名将是“MyNetwork-Public-A”。
你可能已经注意到,这实现了与前面介绍的!Join 函数相同的结果。我这样做是为了向你展示两种可以互相替代的技术。哪个更好?那得由你来决定。如果需要在连接的项之间插入分隔符字符,!Join 通常会更好。
更多子网
让我们遵循上面的模式创建更多的子网:
这些子网之间的区别包括:1)逻辑名称、2)物理名称(通过标记值)、3)CIDR 范围(没有重叠)和 4)指定的可用区域。结果是四个子网,两个是“公共”的,两个是“私有”的,两个在可用区域“A”,两个在可用区域“B”。好了。
如果你愿意,现在可以保存并运行此模板。我建议像这样通过迭代开发来检查错误。
添加路由表
不管名称,子网仅根据其所关联的路由表的定义确定是“公共”或“私有”。路由表定义了可以在子网中将流量路由到哪里。这个主题很复杂,我不会在这里详细介绍,有关子网路由的详细信息,请参阅此信息。
用于子网的部分路由表:
PublicRouteTable:又一个仅在堆栈中使用的任意名称,但这里我们尽量使这个名称具有描述性。路由表必须与 VPC 关联。使用堆栈前缀名进行标记,以便以后更容易识别。
PublicRoute1:这描述了路由表中的一个条目。该条目与公共路由表(!Ref PublicRouteTable)关联,并将任何互联网流量(DestinationCidrBlock: 0.0.0.0/0)路由到互联网网关(!Ref InternetGateway)。每个路由表的第一个条目都是隐含的,我们看不到,这个条目称为“local”路由,所有 10.1.0.0/16 的流量都会留在 VPC 中。关于本地路由的详细信息,请点击这里。
**DependsOn: AttachGateway:**这是一个关键点。如果我们试图构建一个指向未附加网关的路由表条目,就会发生错误。为了消除这个错误,我们让 CloudFormation 知道这个依赖关系。在堆栈创建期间,它就不会尝试构建此路由表条目,直到创建了 AttachGateway 资源之后。它还将等待 VPC 和互联网网关,不过,这些会提前完成。
你可能还是不知道,为什么 CloudFormation 不能自己弄清楚这种依赖关系——这是一个很好的问题。CloudFormation 肯定知道你通过!Ref 或!GetAtt(稍后将介绍)所做的任何显式引用,但是,它确实无法弄清楚这些资源在你引用它们时实际上是否能够工作。CloudFormation 只是代替你进行 API 调用。如果使用 CLI 创建引用未附加网关的路由表条目,会遇到同样的错误。这种情况也出现在其他一两个地方,比如当 EC2 实例需要连接到未完成的 RDS 实例。
你可以保存你的工作并运行该模板,它应该可以成功。你会注意到,大部分创建时间都花在了 AttachGateway 资源上。
此时,你的公共子网是可用的。如果你在其中一个子网中使用公共 IP 地址启动 EC2 实例,就可以从公网访问它。现在,私有子网…
私有路由表
私有路由表在大多数方面都是相似的,除了不引用 InternetGateway:
NatGatewayId:如果你猜到“???”是一个无效的值,那么你猜对了。要解释这一点,我们首先需要岔开下话题,快速地解释下 NAT 的概念(你可以根据需要选择跳过)。
NAT 可以做什么?
NAT 代表网络地址转换。要获得完整的解释,请参阅关于NAT的背景信息。但简单来说:我们不希望私有子网中的实例可以从公网访问。但是,我们确实希望这些实例能够发起出站连接,例如下载。另外,我们希望他们能够在没有公共 IP 地址的情况下做到这一点。
NAT 为此提供了便利。它将拥有一个公共 IP 地址,并与一个公共子网相关联。私有子网中的私有实例将能够使用它发起出站连接。但是,NAT 不允许相反的情况发生,位于公网上的一方不能使用 NAT 连接到我们的私有实例。
在 AWS 中有两种基本的 NATting(这是一个词吗?),一种是配置为 NAT 的 EC2 实例,另一种是相对较新的 AWS 特性,称为NAT网关。我们将使用后者。
创建一个 NAT 网关
添加以下几行创建一个 NAT 网关:
NAT 网关与其中一个公共子网相关联。我们可以(可能也应该)为每个公共子网创建一个 NAT 网关,但现在,单个网关使事情变得简单。名称像以前一样是动态设置的。
AllocationId:NAT 需要一个固定的公共 IP 地址。这是由一个弹性 IP 地址提供的,下面会说明。
**!GetAtt:**又一个隐函数,这里有介绍。这引用了另一个资源的特定属性。这里,使用!Ref 不起作用,NAT 网关资源需要弹性 IP 地址的 allocationId,而不是地址本身。值得注意的是,文档列出了你可以从每个资源“获取”的属性,通常是资源属性的子集。
ElasticIPAddress:EIP 是一个公共 IP 地址,它的值保持不变,不管它附加到什么。这里有完整的说明,足以说明这对于我们的 NAT 是必要的。
价格:到目前为止,堆栈中的每个资源都是免费的。NAT 网关不是。它们是按照小时以及通过的流量收费,见VPC定价。EIP 的不同寻常之处在于,只要使用它们,就不收取任何费用。在没有附加到运行资源的情况下,AWS每小时只收取象征性的费用,这可以防止客户囤积资源。这使得这个堆栈的成本大约是每小时 1 或 2 美分,这取决于你所运行的区域。便宜,但一定要在完成后删除堆栈,以防止成本累积。
现在,我们有了一个 NAT 网关,我们可以在路由表中引用它了。修改后的路由如下所示:
这条路由将互联网流量(DestinationCidrBlock: 0.0.0.0/0)发送到 NAT 网关(!Ref NATGateway)。反过来,由于 NAT 位于公共子网中,它会将其发送到互联网网关。NAT 接收到的响应流量被转发到发出请求的实例。同样,没有显示前面介绍的本地隐式路由。
将路由表附加到子网
最后,我们需要将子网关联到它们的相关路由表。命名为“public”的需要关联到公共路由表,而“private”的需要关联到私有路由表:
AWS::EC2::SubnetRouteTableAssociation:关联资源只是将子网关联到路由表。如果没有这些关联,子网将使用与 VPC 关联的“main”路由表,而这没有包含在我们的模板中。最好有明确的路由表和关联关系。
恭喜!现在,你已经拥有了一个用于构建典型 VPC 的功能完整的 CloudFormation 模板。保存起来以供将来使用,并根据需要扩展它。使用这个模板和前面介绍的指令创建一个堆栈,根据需要更新和删除它。
如果你愿意,可以通过在 VPC 中运行 EC2 实例来测试 VPC。已经把公共 IP 地址附加到公共子网的的实例将可以从公网访问。私有子网中的实例将无法从公网访问,但可以进行出站调用。关于这一点,我建议你阅读其他文章。
记住,NAT 网关每小时要花一到两便士,所以如果你不使用它,就最好删除它。
小结
通过 CloudFormation 创建、修改和删除资源堆栈的能力是基础设施即代码概念的一个有效的示例。现在,我们有了一个更快、可重复、可重用的系统,我们不再需要通过界面或命令来手动设置基础设施。
在下一篇文章中,我将向你展示如何使这个模板更加灵活,使用参数和条件创建数量不同的子网,使私有子网可选,并探讨其他 NAT 选项。我们还将看到,如何把这个堆栈的资源所生成的输出作为其他堆栈的输入供其消费。
关于作者
Ken Krueger 以“通过现代技术的应用,指导组织和个人走向商业成功”作为自己的专业使命。他有超过 30 年的软件开发、项目领导、项目经理、Scrum Master 和导师经验,跨越大型机、客户端-服务器和 Web 时代。他在 Java、Spring、SQL、Web 开发、云和相关技术方面有丰富的经验。行业经验包括电信、金融、房地产、零售、发电、航运、酒店和软件开发。他拥有南佛罗里达大学 MIS 学位,罗林斯学院克鲁默商学院 MBA 学位,以及 Scrum Master、PMP、AWS 和 Java 认证。
查看英文原文:Building a VPC with CloudFormation - Part 1
评论 1 条评论