原文:
annas-archive.org/md5/55f0ee1b5d0f6f58bdd7da1ffd9f7954译者:飞龙
第十二章:利用基础设施即代码
在当今的数字化环境中,管理和部署基础设施是一个复杂且耗时的过程。传统上,基础设施部署涉及手动配置每个服务器、网络和存储设备。这个过程不仅耗时,而且容易出错和产生不一致性。基础设施即代码(IaC)解决方案提供了一种自动化的方式来管理和部署基础设施。IaC 解决方案使开发人员能够将基础设施视为代码,从而以与代码相同的方式定义、管理和配置基础设施。
在本章中,我们将探索 IaC 解决方案,重点关注 Terraform。从 2.0 到 1.1。BSL 允许你自由使用 Terraform,并可以访问其源代码,因此对最终用户没有变化。
使用 Terraform,开发人员可以编写代码来定义他们的基础设施需求,而 Terraform 将负责所需资源的配置和部署。近年来,由于其简便性、灵活性以及对多个云服务提供商的支持,Terraform 变得越来越流行。在接下来的章节中,我们将讨论 Terraform 的主要特性和优势,以及如何使用它在流行的云服务提供商上配置基础设施。
本章我们将学习以下内容:
-
什么是 IaC?
-
IaC 与配置即代码的区别
-
值得了解的 IaC 项目
-
Terraform
-
深入了解 HCL
-
使用 AWS 的 Terraform 示例
技术要求
本章中,您需要一台能够运行 Terraform 的系统。Terraform 是一个用 Go 编程语言编写的单一二进制程序。其安装过程简单易懂,并在 HashiCorp Terraform 项目页面上有详细说明(developer.hashicorp.com/terraform/downloads)。HashiCorp 是 Terraform 以及其他云管理工具的背后公司,这些工具已成为 DevOps 领域的事实标准。您还需要一个 AWS 账户。AWS 提供有限时间的免费服务。我们使用的服务在写本书时具有免费层。在运行示例之前,请查阅 AWS 免费层清单,以避免不必要的费用。
什么是 IaC?
基础设施即代码(IaC)是一种软件开发实践,它通过代码定义和管理基础设施。实质上,这意味着将基础设施视为软件,并通过相同的流程和工具进行管理。IaC 解决方案使开发人员能够通过代码定义、配置和管理基础设施,而无需手动配置服务器、网络和存储设备。这种基础设施管理方法高度自动化、可扩展且高效,能够帮助组织减少部署时间,提高一致性和可靠性。
IaC 解决方案有不同的形式,包括配置管理工具、配置工具和云编排工具。配置管理工具,如Ansible和Chef,用于管理单个服务器或服务器组的配置。配置工具,如 Terraform 和CloudFormation,用于配置和管理基础设施资源。云编排工具,如Kubernetes和OpenShift,用于管理容器化的应用程序及其相关基础设施。无论使用何种具体工具,IaC 解决方案都能提供多个好处,包括可重复性和一致性。
基础设施即代码与配置即代码
你可能会想,难道我们在第十一章中已经讲过这个内容了吗?我们谈到的是 Ansible?答案是否定的,我们并没有讲过。基础设施即代码(IaC)和配置即代码(CaC)之间有着非常明显的区别。IaC 工具关注的正是这一点:基础设施。这意味着网络、DNS 名称、路由以及服务器(虚拟机或物理机),一直到操作系统的安装。而 CaC 关注的是操作系统内部的内容。人们常常试图用一个工具做所有事情,因此你会看到 Ansible 有一些模块可以配置交换机和路由器,但该工具最擅长的还是它原本设计的用途。如果你把这两者混在一起,虽然不会出什么大问题,但你的工作会变得更加困难。
值得了解的 IaC 项目
自从公共云的崛起,尤其是 AWS 之后,对于一种可重复和可靠的方式来设置基础设施并配置云服务的需求也开始增长。从那时起,许多工具应运而生,且越来越多的工具正在开发中。在本节中,我们将回顾一些最流行和最具创新性的工具。
AWS CloudFormation
AWS CloudFormation 是由Amazon Web Services(AWS)提供的一个流行的 IaC 工具,用于自动化 AWS 资源的配置。它首次发布于 2011 年,并迅速成为云端管理基础设施的广泛使用工具。
CloudFormation 允许你使用声明性语言(如 YAML 或 JSON)来定义基础设施,然后根据这些定义创建、更新或删除资源堆栈。这不仅能实现一致且可重复的基础设施部署,还能方便地进行回滚和版本控制。不过,并非所有情况都是一帆风顺的——有时,你可能会在进行未经测试的更改后卡在回滚循环中。例如,假设你正在更改 AWS Lambda 的环境版本。不幸的是,由于你当前使用的版本不再受支持,因此更改失败。现在它被卡在回滚状态,显示为UPDATE_ROLLBACK_FAILED。你需要手动解决这个问题,因为没有自动化的方式来处理这一问题。
CloudFormation 与其他 AWS 服务(如 AWS 身份与访问管理(IAM)、AWS 弹性负载均衡(ELB)和 AWS 自动扩展)集成,轻松实现复杂架构的创建。
下面是一个用 YAML 编写的 CloudFormation 堆栈示例,它在默认 VPC 的公共子网中创建一个名为t4g.small的 EC2 实例:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
KeyName:
Type: AWS::EC2::KeyPair::KeyName
Default: admin-key
InstanceType:
Type: String
Default: t4g.small
SSHCIDR:
Type: String
MinLength: 9
MaxLength: 18
Default: 0.0.0.0/0
AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
LatestAmiId:
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/canonical/ubuntu/server/jammy/stable/current/amd6/hvm/ebs-gp2/ami-id'
Resources:
EC2Instance:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref 'InstanceType'
SecurityGroups: [!Ref 'InstanceSecurityGroup']
KeyName: !Ref 'KeyName'
ImageId: !Ref 'LatestAmiId'
InstanceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Enable SSH access
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: !Ref 'SSHCIDR'
Outputs:
InstanceId:
Description: InstanceId of the newly created EC2 instance
Value: !Ref 'EC2Instance'
PublicDNS:
Description: Public DNSName of the newly created EC2 instance
Value: !GetAtt [EC2Instance, PublicDnsName]
PublicIP:
Description: Public IP address of the newly created EC2 instance
Value: !GetAtt [EC2Instance, PublicIp]
在这个堆栈中,我们创建了两个资源:一个 EC2 实例和一个附加到该实例的安全组。CloudFormation 堆栈可以获取四个参数:
-
KeyName:已经在 AWS EC2 服务中创建的 SSH 密钥名称。默认值为admin-key。 -
InstanceType:我们要启动的实例类型。默认值为t4g.small。 -
SSHCIDR:22。默认值为0.0.0.0/0。在这里,我们验证提供的输入是否符合正则表达式,并检查变量的长度。 -
LatestAmiId:用于启动 EC2 实例的基础系统 AMI ID。默认值为 Ubuntu Linux22.04的最新 AMI。
接下来是Resources部分。在这里,EC2 实例是使用AWS::EC2::Instance资源类型创建的,安全组是使用AWS::EC2::SecurityGroup资源创建的。
最后一部分称为Outputs;在这里,我们可以显示已创建资源的 ID 和其他属性。在这里,我们公开实例 ID、其公共 DNS 名称和公共 IP 地址。
可以将这些输出值作为另一个 CloudFormation 堆栈的输入,这将使 CloudFormation 代码的 YAML 文件大大减小,并更易于维护。
AWS 云开发工具包
AWS 云开发工具包(CDK)是一个开源软件开发框架,用于在代码中定义云基础设施。通过 CDK,开发人员可以使用熟悉的编程语言,如 TypeScript、Python、Java、C#和 JavaScript,来创建和管理 AWS 上的云资源。
AWS CDK 于 2018 年 7 月首次作为开源项目发布。它旨在简化构建和部署云基础设施的过程,让开发人员能够使用现有的编程语言技能和工具。通过 CDK,开发人员可以定义基础设施即代码(IaC),并利用版本控制、自动化测试和持续集成/持续部署(CI/CD)管道的好处。自发布以来,CDK 已成为在 AWS 上构建基础设施的流行选择,并持续更新和新增功能。
下面是一些 AWS CDK Python 代码的示例,用于创建 EC2 实例:
from aws_cdk import core
import aws_cdk.aws_ec2 as ec2
class MyStack(core.Stack):
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
# VPC
vpc = ec2.Vpc(self, "VPC",
nat_gateways=0,
subnet_configuration=[ec2.SubnetConfiguration(name="public",subnet_type=ec2.SubnetType.PUBLIC)]
)
# Get AMI
amzn_linux = ec2.MachineImage.latest_amazon_linux(
generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
edition=ec2.AmazonLinuxEdition.STANDARD,
virtualization=ec2.AmazonLinuxVirt.HVM,
storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE
)
# Create an EC2 instance
instance = ec2.Instance(self, "Instance",
instance_type=ec2.InstanceType("t4g.small"),
machine_image=amzn_linux,
vpc = vpc
)
这段代码创建了一个新的 VPC 和一个 EC2 实例,实例类型为t4g.small,操作系统为安装了 Amazon Linux 的 EC2 实例。请注意,运行此代码之前需要先安装并配置 AWS CDK。
Terraform
Terraform 是一个流行的开源工具,用于基础设施自动化,特别是用于创建、管理和配置云资源。它使开发者能够定义基础设施即代码(IaC),并自动化在多个云平台上部署基础设施的过程。通过 Terraform,用户可以编写声明性配置文件,这些文件使用简单直观的语言,可以进行版本管理、共享和重用。这种基础设施管理方式确保了系统的一致性和可扩展性,并减少了手动错误的风险。Terraform 支持多种云服务提供商,包括 AWS、Azure、Google Cloud 等,这使得它成为拥有复杂云基础设施需求的组织的热门选择。
Terraform 由 HashiCorp 创建,HashiCorp 是由 Mitchell Hashimoto 和 Armon Dadgar 于 2012 年创立的公司。该公司以开发流行的基础设施自动化开源工具而闻名,包括 Vagrant、Consul、Nomad 和 Vault。Terraform 于 2014 年 7 月首次发布,随后成为业内最广泛采用的 IaC 工具之一。HashiCorp 继续维护和开发 Terraform,并定期发布更新,解决新的云服务提供商特性、安全漏洞和社区反馈。该工具拥有一个庞大且活跃的贡献者社区,进一步增强了其功能并支持新的使用场景。
Terraform 也是本章的一个主要话题,稍后我们将在 Terraform 部分深入研究其代码和内部实现。
Terraform 云开发工具包
Terraform 云开发工具包(CDKTF)是一个开源软件开发框架,用于以代码形式定义云基础设施。它允许用户使用熟悉的编程语言(如 TypeScript、JavaScript、Python 和 C#)来定义基础设施。这为开发者提供了更大的灵活性和控制力,因为他们可以利用现有的编程技能和工具来定义复杂的基础设施。CDKTF 于 2019 年首次发布,是 AWS 和 HashiCorp 的合作成果。从那时起,它作为一个强大的工具,在使用 Terraform 定义和部署基础设施方面获得了广泛的关注。
CDKTF 支持多种编程语言,使开发者可以轻松使用自己熟悉的语言。它使用构造函数,这些构造函数是可重用的构建块,代表 AWS 资源,用来创建基础设施。用户可以为每个要创建的资源定义构造函数,并将其组合形成更复杂的基础设施。这使得用户能够以模块化和可重用的方式定义基础设施,从而简化了创建和维护基础设施的过程。
以下是使用 Python 在 AWS 中创建 EC2 实例的 CDKTF 示例代码:
from constructs import Construct
from cdktf import App, TerraformStack
from imports.aws import AwsProvider, Instance, SecurityGroup
class MyStack(TerraformStack):
def __init__(self, scope: Construct, ns: str):
super().__init__(scope, ns)
# Configure AWS provider
aws_provider = AwsProvider(self, 'aws', region='us-east-1')
# Create a security group
security_group = SecurityGroup(self, 'web-server-sg',
name='web-server-sg',
ingress=[
{
'from_port': 22,
'to_port': 22,
'protocol': 'tcp',
'cidr_blocks': ['0.0.0.0/0'],
},
{
'from_port': 80,
'to_port': 80,
'protocol': 'tcp',
'cidr_blocks': ['0.0.0.0/0'],
},
],
)
# Create an EC2 instance
Instance(self, 'web-server',
ami='ami-0c55b159cbfafe1f0',
instance_type='t4g.small',
security_groups=[security_group.id],
user_data="""
#!/bin/bash
echo "Hello, DevOps People!" > index.xhtml
nohup python -m SimpleHTTPServer 80 &
"""
)
app = App()
MyStack(app, "my-stack")
app.synth()
App 和 TerraformStack 类从 cdktf 包导入,而 AWS 资源则从 imports.aws 模块导入。上面的代码创建了一个带有安全组的 EC2 实例,并带有一个基本的用户数据脚本,用于启动一个简单的 HTTP 服务器。生成的基础设施可以使用 cdktf deploy 命令进行部署,该命令会生成 Terraform 配置文件并执行 Terraform CLI。
你可以在 developer.hashicorp.com/terraform/cdktf 阅读更多关于 CDKTF 的信息。
Pulumi
Pulumi 是一个开源的 IaC 工具,允许开发人员使用熟悉的编程语言构建、部署和管理云基础设施。与依赖声明性语言如 YAML 或 JSON 的传统 IaC 工具不同,Pulumi 使用真实的编程语言,如 Python、TypeScript、Go 和 .NET,来定义和管理基础设施。这使得开发人员能够利用他们现有的技能和经验,使用构建应用程序时所用的相同工具和流程来创建基础设施。使用 Pulumi,开发人员可以像进行代码更改一样创建、测试和部署基础设施更改——即通过使用版本控制和 CI/CD 工具。
Pulumi 的首次发布是在 2018 年 5 月,旨在简化管理云基础设施的过程。Pulumi 由 Joe Duffy 创立,他是前微软工程师,曾参与 .NET 运行时和编译器的开发。Duffy 看到了一个机会,利用编程语言来管理基础设施,提供了一种比传统的 IaC 工具更灵活、更强大的方法。自发布以来,Pulumi 在开发者中获得了广泛的关注,尤其是在云原生环境中工作或使用多个云服务提供商的开发者。
Pulumi 支持多种编程语言,包括 Python、TypeScript、Go、.NET 和 Node.js。Pulumi 还提供了一套丰富的库和工具,用于处理云资源,包括对 AWS、Azure、Google Cloud 和 Kubernetes 等流行云服务提供商的支持。此外,Pulumi 还与流行的 CI/CD 工具集成,如 Jenkins、CircleCI 和 GitLab,使开发人员能够轻松地将基础设施更改融入现有的工作流程中。
以下是一个 Pulumi 使用 Python 创建 AWS EC2 实例的示例代码:
import pulumi
from pulumi_aws import ec2
# Create a new security group for the EC2 instance
web_server_sg = ec2.SecurityGroup('web-server-sg',
ingress=[
ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=22,
to_port=22,
cidr_blocks=['0.0.0.0/0'],
),
],
)
# Create the EC2 instance
web_server = ec2.Instance('web-server',
instance_type='t4g.small',
ami='ami-06dd92ecc74fdfb36', # Ubuntu 22.04 LTS
security_groups=[web_server_sg.name],
tags={
'Name': 'web-server',
'Environment': 'production',
},
)
# Export the instance public IP address
pulumi.export('public_ip', web_server.public_ip)
这段代码定义了一个 AWS 安全组,允许通过端口 22(SSH)进行入站流量,然后创建一个 t4g.small 类型的 EC2 实例,使用 Ubuntu 22.04 LTS AMI。该实例与我们之前创建的安全组关联,并带有名称和环境标签。最后,实例的公共 IP 地址作为 Pulumi 堆栈输出被导出,可以供堆栈中的其他资源使用或由用户访问。
在这一部分中,我们介绍了几种 IaC 解决方案:CDK、CDKTF、Terraform 和 Pulumi。它们中的一些针对特定的云提供商,而另一些则允许我们配置不同的云环境。
在接下来的部分中,我们将回到 Terraform,深入探讨它的工作原理,并学习如何在实践中使用基础设施即代码(IaC)。这将为我们快速理解其他解决方案奠定基础,包括我们之前提到的 CDK。
Terraform
在这一部分中,我们将介绍 Terraform,这是目前最广泛使用的 IaC 解决方案之一。
Terraform 是一个由 HashiCorp 开发的 IaC 工具。使用它的原理类似于使用 Ansible 配置系统:基础设施配置保存在文本文件中。它们不像 Ansible 那样是 YAML 格式的,而是采用 HashiCorp 开发的特殊配置语言:HashiCorp 配置语言(HCL)。文本文件容易版本化,这意味着基础设施变更可以存储在如 Git 之类的版本控制系统中。
Terraform 执行的操作比你在 Ansible 中看到的更复杂。一个简单的 HCL 语句可能意味着设置一堆虚拟服务器以及它们之间的路由。因此,尽管 Terraform 和 Ansible 一样是声明式的,但它比其他工具更高层次。此外,与 Ansible 相反,Terraform 是状态感知的。Ansible 有一个待执行操作的列表,每次运行时,它会检查哪些操作已经执行。而 Terraform 则记录系统最后一次的状态,并确保每次执行时系统都会与代码中的状态一致。
为了实现这一点,Terraform 创建并维护一个状态文件。它是一个扩展名为.tfstate的文本文件,记录了工具所知道的基础设施的最后已知状态。状态文件在内部是有版本控制的;Terraform 维护一个特殊的计数器,允许它知道文件是否是最新的。状态文件对于 Terraform 正常工作至关重要。你绝不能损坏或丢失状态文件。如果丢失了该文件,Terraform 会尝试创建已经存在的资源,并可能删除不该删除的内容。
有几种方法可以确保状态文件的安全。其中一种方法是将其存储在经过适当配置的对象存储中(例如 S3),这样状态文件就无法被删除。为了增强安全性,您可以确保该文件是有版本控制的,这意味着存储将保留文件的旧副本以供以后使用。
有一件关于.tfstate的重要事情需要注意:它将包含与您的基础设施相关的所有信息,以及明文密码、登录凭证、访问密钥等。保护该文件的隐私至关重要,并且应将其排除在版本控制系统的提交之外(在 Git 中,可以将其添加到.gitignore文件中)。
代码在以.tf扩展名的文本文件中开发。与 Ansible 不同,您在文件中放置指令的顺序并不重要。在执行之前,Terraform 会分析当前目录下的所有.tf文件,创建配置元素之间的依赖关系图,并正确地排列它们。通常情况下,代码会被分解成更小的.tf文件,这些文件将相关的配置指令分组。然而,您也可以将所有代码保存在一个巨大的文件中,尽管它很快会变得庞大,不利于使用。
尽管您可以自由命名文件,只要它们的扩展名是.tf,但还是有一些最佳实践需要遵守:
-
main.tf:这是您开发配置代码的主要文件。它将包含资源、模块和其他重要信息。 -
variables.tf:此文件将包含您希望在main.tf文件中使用的所有变量的声明。 -
outputs.tf:如果您的main.tf文件中的资源产生任何输出,它们将在此处声明。 -
versions.tf:此文件声明 Terraform 二进制文件本身和提供者所需的版本。最好声明已知能够正常工作的最低版本。 -
providers.tf:如果任何提供者需要额外的配置,您应该将它们放在这个文件中。 -
backend.tf:此文件包含 Terraform 应将状态文件存储的位置的配置。状态文件是 Terraform 中基础设施即代码(IaC)的一个重要组成部分。我们将在Terraform 状态小节中更深入地讨论这一点。
在 Ansible 中,重活是由名为模块的 Python 程序完成的。而在 Terraform 中,这项工作是由提供者(Providers)完成的。提供者是小型的 Golang 程序,它们消耗由 Terraform 准备的配置计划,通过这些服务的 API 连接云端、设备等服务,并执行配置。你可以把它们看作是插件。提供者提供一组资源类型,并最终提供所需的数据源,以便为提供者所连接的 API 编写配置。官方解释是,提供者“是上游 API 的逻辑抽象”。提供者通常发布在 Terraform Registry 上,这是由 HashiCorp 维护的公共插件库。你可以使用其他注册表,但发布在 Terraform Registry 上的提供者通常已通过测试并且被信任能够正常工作。每个发布在该注册表上的提供者都有详细的文档和良好注释的示例。每当你使用一个新提供者时,应该访问该注册表(registry.terraform.io/)。一个例子是 AWS 提供者。这个提供者公开了大量资源,你可以用来与 AWS 服务交互,以配置和部署它们。记住:配置仅限于基础设施。你可以将 Terraform(例如,用来配置虚拟机)与 Ansible(用于在虚拟机中安装软件并进行配置)结合,体验完整的工作流。
让我们看一个来自 Terraform Registry AWS 提供者文档的示例(registry.terraform.io/providers/hashicorp/aws/latest/docs):
terraform {
required_providers {
aws = {
source = "hashicorp/aws" version = "~> 4.0"
}
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
}
# Create a VPC
resource "aws_vpc" "example" {
cidr_block = "10.0.0.0/16"
}
在上述代码片段中,我们声明需要从 Terraform Registry 下载 AWS 提供者。它的版本应该不低于 4.0。然后,我们配置要使用的区域(us-east-1)。最后,我们创建一个虚拟私有网络(虚拟私有云(VPC))并为其声明一个 IP 地址块。
单个目录中的 .tf 文件集合称为 模块。如果你在包含模块文件的目录中运行 Terraform 命令,那么这个目录就被称为 根模块。
Terraform 模块
Terraform 的一个关键概念是模块。Terraform 模块是一个资源集合及其依赖关系,用于构建基础设施的特定组件。模块提供了一种组织代码的方式,并使其能够在多个项目之间复用。模块可以与其他用户和团队共享,甚至发布到像 Terraform Registry 这样的公共注册表中。
在使用 Terraform 时,确保使用兼容的基础设施提供者版本非常重要。提供者负责管理您云环境中的资源,不同版本可能具有不同的功能或行为。为了避免基础设施发生意外变化,您可以在 Terraform 配置中固定您使用的提供者版本。通过在提供者块中指定版本约束,使用 Terraform 版本约束语法,您可以实现这一点。当您运行 Terraform 时,它将下载并使用指定版本的提供者,确保您的基础设施保持一致和可预测。
这是一个示例versions.tf文件,它将 AWS 提供者固定到最新版本,并要求至少版本 1.0.0 的 Terraform:
terraform {
required_providers {
aws = ">= 3.0.0"
}
required_version = ">= 1.0.0"
}
在这个示例中,我们使用required_providers块来指定我们至少需要版本 3.0.0 的 AWS 提供者。通过使用>=操作符,我们允许 Terraform 使用任何版本的提供者,只要它等于或大于 3.0.0,包括最新版本。
当我们运行terraform init时,Terraform 将自动下载并使用提供者的最新版本。此命令还会更新或下载您可能在主模块(或根模块)中使用的其他模块。然而,使用大量依赖其他模块的模块是不推荐的,因为这可能会导致依赖冲突(例如,一些旧模块可能需要 AWS 提供者版本 1.23,而根模块需要版本 3.0 或更高)。我们将在本章的Terraform CLI小节中再次回到命令行界面(CLI)。
要引用另一个模块,您可以使用module代码块。假设我们在根模块相对路径./module/aws_ec2目录中有一个简单的模块。aws_ec2模块需要传入ami、subnet、vpc和security_group变量:
module "aws_ec2_instance" {
source = "./modules/aws_ec2"
ami = "ami-06dd92ecc74fdfb36"
subnet_id = "subnet-12345678"
vpc_id = "vpc-12345678"
security_group = "sg-12345678"
}
如果一个模块公开了某些输出(您可以将其用作资源或其他模块的输入),您可以通过module.NAME.OUTPUT_NAME来引用它们。在这种情况下,我们可以公开 EC2 实例的 ID,您可以通过名称module.aws_ec2_instance.instance_id来引用它。
除了使用本地路径外,还有几种其他方法可以指定 Terraform 中引用模块时的源参数:
-
该模块可以存储在 Git 仓库中并检索:
module "example" {source = "git::https://github.com/example-org/example-module.git"}使用 Git 仓库时,您还可以引用提交 ID、分支或标签:
module "example" {source = "git::https://github.com/example-org/example-module.git?ref=branch_name"}对于私有仓库,您需要使用 SSH 而不是 HTTPS 来将其克隆到本地:
module "example" {source = "git::ssh://github.com/example-org/example-module.git?ref=branch_name"} -
该模块可以发布并从 Terraform 注册表中检索:
module "example" {source = "hashicorp/example-module/aws"}在这种情况下,您可以使用
version属性指定模块版本,如下所示:module "example" {source = "hashicorp/example-module/aws"version = "1.0.0"} -
该模块可以存储在 S3 桶中并检索:
module "example" {source = "s3::https://s3-eu-cental-1.amazonaws.com/example-bucket/example-module.zip"}
您可以在官方文档中找到其他可能的来源:developer.hashicorp.com/terraform/language/modules/sources。
Terraform 状态
Terraform 的一个基本概念是状态文件。它是一个 JSON 文件,描述了你基础设施的当前状态。这个文件用于跟踪 Terraform 已创建、更新或删除的资源,并且还存储每个资源的配置。
状态文件的目的是使 Terraform 能够一致可靠地管理你的基础设施。通过跟踪 Terraform 已创建或修改的资源,状态文件确保 Terraform 的后续运行能够了解基础设施的当前状态,并根据需要进行更改。如果没有状态文件,Terraform 将无法知道当前部署了哪些资源,并且无法做出关于如何进行后续更改的明智决策。
状态文件还被用作 Terraform 的plan和apply操作的事实来源。当你运行terraform plan或terraform apply时,Terraform 将当前基础设施的状态与 Terraform 代码中定义的目标状态进行比较。状态文件用于确定需要进行哪些更改,以使你的基础设施达到所需状态。总体而言,状态文件是 Terraform 基础设施管理功能的关键组成部分,确保 Terraform 能够保证基础设施的一致性和可靠性。
虽然 Terraform 状态文件是该工具的关键组成部分,但使用它也存在一些缺点和挑战。
状态文件是一个集中式文件,用于存储有关基础设施的信息。虽然这很方便,但在团队协作时也可能会带来问题,特别是当多个用户同时对同一基础设施进行更改时。这可能会导致冲突,使得保持状态文件的最新状态变得具有挑战性。通过使用分布式锁机制可以缓解这一问题。在 AWS 环境中,它实际上只是一个 DynamoDB 表,包含一个状态为0或1的锁条目。
Terraform 状态的另一个缺点是,状态文件包含有关基础设施的敏感信息,如密码、密钥和 IP 地址。因此,必须保护状态文件以防止未经授权的访问。如果状态文件被泄露,攻击者可能会获得对基础设施或敏感数据的访问权限。在 AWS 内部,状态文件通常保存在一个 S3 桶中,并且需要启用加密并阻止公共访问。
随着时间的推移,状态文件可能会变得庞大且难以管理,尤其是在你管理着大量资源的基础设施时。这可能会使管理和维护状态文件变得具有挑战性,进而导致错误和不一致性。
我们可能遇到的下一个关于状态文件的挑战是,Terraform 状态文件是版本特定的。这意味着你必须使用与创建状态文件时相同版本的 Terraform 来管理该文件。这可能会在升级到新版 Terraform 时带来问题,因为你可能需要将状态文件迁移到新的格式。
最后,Terraform 的状态文件有一些局限性,例如无法管理外部资源或处理资源之间复杂依赖关系的困难。这在处理某些类型的基础设施或应对复杂部署时可能会带来挑战。
状态文件的另一个功能是强制执行 Terraform 管理的资源的配置。如果有人手动进行了更改,你将在下次执行 terraform plan 或 terraform apply 时看到这些更改,并且这些更改将被回滚。
考虑到所有这些,Terraform 仍然是最好的解决方案之一,而且大多数这些挑战在规划基础设施时都可以轻松解决。
这是一个示例 backend.tf 文件,配置 Terraform 使用名为 state-files 的 S3 存储桶来存储状态文件,并使用名为 terraform 的 DynamoDB 表进行状态锁定:
terraform {
backend "s3" {
bucket = "state-files"
key = "terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform"
}
}
在此配置中,后端块指定了我们希望使用 s3 后端类型,该类型旨在将状态文件存储在 S3 存储桶中。bucket 参数指定了状态文件应存储的存储桶名称,而 key 参数指定了存储桶中状态文件的名称。
region 参数指定了存储桶所在的 AWS 区域。你应该将其设置为最符合你使用场景的区域。
最后,dynamodb_table 参数指定了将用于状态锁定的 DynamoDB 表的名称。这是 S3 后端的一个重要特性,因为它确保一次只有一个用户可以对基础设施进行更改。
这是 Terraform 状态文件的一个示例:
{
"version": 3,
"serial": 1,
"lineage": "f763e45d-ba6f-9951-3498-cf5927bc35c7",
"backend": {
"type": "s3",
"config": {
"access_key": null,
"acl": null,
"assume_role_policy": null,
"bucket": "terraform-states",
"dynamodb_endpoint": null,
"dynamodb_table": "terraform-state-lock",
"encrypt": true,
"endpoint": null,
"external_id": null,
"force_path_style": null,
"iam_endpoint": null,
"key": "staging/terraform.tfstate",
"kms_key_id": null,
"lock_table": null,
"max_retries": null,
"profile": null,
"region": "eu-central-1",
"role_arn": null,
"secret_key": null,
"session_name": null,
"shared_credentials_file": null,
"skip_credentials_validation": null,
"skip_get_ec2_platforms": null,
"skip_metadata_api_check": null,
"skip_region_validation": null,
"skip_requesting_account_id": null,
"sse_customer_key": null,
"sts_endpoint": null,
"token": null,
"workspace_key_prefix": null
},
"hash": 1619020936
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}
通过使用 S3 后端和 DynamoDB 状态锁定,你可以确保 Terraform 部署在团队环境中是安全且一致的,即使多个用户可能同时对相同基础设施进行更改。
在下一个小节中,我们将讨论如何使用 Terraform CLI 与我们的基础设施和状态文件进行交互。
Terraform CLI
Terraform 的核心是其命令行工具,恰如其分地被称为 terraform。我们在介绍 Terraform 时已链接了安装指南。虽然也有工具可以自动化工作流程,省去了使用 CLI 的必要,但该工具的使用非常简单,并且从与它一起工作中可以获得许多有用的知识。在这一节中,我们将介绍 terraform 命令的最常见选项和工作流程。
初始化工作环境
您将使用的第一个 terraform 子命令是 terraform init。在编写完 main.tf 文件的第一部分(如果您遵循建议的模块结构)后,您将运行 terraform init 来下载所需的插件并创建一些重要的目录和帮助文件。
让我们来看一下之前使用的第一段代码的一部分:
terraform {
required_providers {
aws = {
source = "hashicorp/aws" version = "~> 4.0"
}
}
}
这段代码告诉 Terraform 需要下载的插件及其最低版本。现在,让我们运行 terraform init 命令:
admin@myhome:~$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Installing hashicorp/aws v4.58.0...
- Installed hashicorp/aws v4.58.0 (signed by HashiCorp)
Terraform has been successfully initialized!
我们已简化了输出以提高简洁性,但最重要的部分仍然存在。您将看到 Terraform 告诉您已执行了哪些操作。后端是用于存储 .tfstate 文件的存储区域。如果您没有指定存储区域,.tfstate 文件将保存在本地目录中的 terraform.tfstate 文件中。还有一个新的子目录 .terraform,其中安装了所需的插件。最后,存在一个 .terraform.lock.hcl 文件,Terraform 会记录已使用的提供程序版本,以便您可以出于兼容性原因保留这些版本。
terraform init 命令是一个安全命令。您可以根据需要多次运行它;它不会破坏任何内容。
规划更改
接下来要运行的命令是 terraform fmt。此命令将根据现有的最佳实践格式化您的 .tf 文件。使用它可以提高代码的可读性和可维护性,使所有源文件在您将看到的所有 Terraform 项目中遵循相同的格式化策略。在我们的示例上运行 terraform fmt 将产生以下输出:
admin@myhome:~$ terraform fmt
│ Error: Missing attribute separator
│
│ on main.tf line 4, in terraform:
│ 3: aws = {
│ 4: source = "hashicorp/aws" version = "~> 4.0"
│
│ Expected a newline or comma to mark the beginning of the next attribute.
您会注意到 fmt 在我的 main.tf 文件中发现了一个明显的错误。这不仅是一个可读性问题;它还可能在某些提供程序中引入代码解析错误。我将两个属性写在了同一行。将其编辑成如下所示就可以解决问题:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
通过这个更改,fmt 满意了,我们可以继续进行下一步。
通过使用 terraform plan 命令来构建操作计划。它将您基础设施的最后已知记录状态(terraform.tfstate)与目录中的代码进行比较,并准备出步骤以使其匹配。在我们之前的示例代码上运行 terraform plan 会产生以下输出:
admin@myhome:~$ terraform plan
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
由于我们没有引入任何可以创建资源的代码,Terraform 告知我们没有计划进行任何更改。
然而,这并不太有趣。因此,我们将展示一些将在 AWS 中创建资源的内容。
注意
在遵循此示例之前,请先了解您在 AWS 免费套餐服务中的责任。运行这些示例可能会产生费用,如果发生费用,本文的作者和出版商不对其承担任何责任。
如果你想跟随这些示例,你需要拥有一个 AWS 账户(在写本书时是免费的)。然后,你需要创建一个角色并生成 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY。这样做超出了本章的范围。
我们稍微修改了前面的示例。required_providers 块已被移动到 providers.tf 文件中。我们还在其中添加了另一个提供程序块。文件如下所示:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-west-2"
}
请注意,新的块正在配置一个名为 aws 的新提供程序资源。名称(aws)实际上由我们决定,可以是任何名称。记得给它们起有意义的名字,能帮助你以后理解代码。我们为这个提供程序提供了最低限度的配置,指定了我们的资源将启动的区域。
我们在新创建的空 main.tf 文件中进行实际操作:
resource "aws_instance" "vm_example" {
ami = "ami-830c94e3"
instance_type = "t2.micro"
tags = {
Name = "DevOpsGuideTerraformExample"
}
在这里,我们告诉 Terraform 我们想要创建一个新的 aws_instance 类型的资源。我们将其命名为 vm_example。接下来,我们告诉工具使用名为 ami-830c94e3 的虚拟机镜像(AMI)。该实例的类型(它将拥有多少 RAM、多少 CPU 核心、系统驱动器的大小等等)是 t2.micro。最后,我们添加了一个标签,帮助我们识别和查找这个实例。
让我们调用 terraform plan 并应用它:
admin@myhome:~$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.vm_example will be created
+ resource "aws_instance" "vm_example" {
+ ami = "ami-830c94e3"
[...]
+ tags = {
+ "Name" = "DevOpsGuideTerraformExample"
}
+ tags_all = {
+ "Name" = "DevOpsGuideTerraformExample"
}
+ tenancy = (known after apply)
[...]
+ vpc_security_group_ids = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
────────────────────────────────────────────────────────────────────
注意
你没有使用 -out 选项来保存这个计划,因此 Terraform 无法保证如果你现在运行 terraform apply,它会准确执行这些操作。
我们已经省略了计划中的大量输出。然而,你可以看到它与之前的示例有所不同。Terraform 注意到我们没有具有指定参数的虚拟机(记住,它是与 .tfstate 文件进行比较的)。因此,它将创建一个。我们始终可以在以 Plan 开头的行中看到摘要。在计划中,所有以 +(加号)开头的属性将被创建。所有以 -(减号)开头的属性将被销毁,而所有以 ~(波浪号)开头的属性将被修改。
在更改已创建资源的属性时要小心使用 Terraform。通常情况下,它会将其视为新资源,特别是当你更改名称时。这将导致销毁旧名称的虚拟机,并创建一个新名称的虚拟机。这可能不是你想要的结果。
应用更改
计划通过调用 terraform apply 来实施。如果你的 .tf 文件与 .tfstate 文件不同,这个命令将会对你的环境进行更改。此外,如果你的实际运行基础设施与 .tfstate 文件不同,terraform apply 将尽最大努力使实时基础设施与 Terraform 状态文件重新对齐:
admin@myhome:~$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.vm_example will be created
+ resource "aws_instance" "vm_example" {
+ ami = "ami-830c94e3"
[...]
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "DevOpsGuideTerraformExample"
}
+ tags_all = {
+ "Name" = "DevOpsGuideTerraformExample"
}
+ tenancy = (known after apply)
[...]
+ vpc_security_group_ids = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.vm_example: Creating...
aws_instance.vm_example: Still creating... [10s elapsed]
aws_instance.vm_example: Still creating... [20s elapsed]
aws_instance.vm_example: Still creating... [30s elapsed]
aws_instance.vm_example: Still creating... [40s elapsed]
[...]
aws_instance.vm_example: Still creating... [1m20s elapsed]
aws_instance.vm_example: Creation complete after 1m29s [id=i-0a8bee7070b7129e5]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
再次,出于简洁性,很多输出被省略了。
terraform apply 命令再次创建了一个计划。我们可以通过将 terraform plan 记录到文件中,然后将文件输入到 apply 步骤中来避免这种情况。
有趣的部分是确认步骤,在这个步骤中,Terraform 会要求你输入yes才能继续。然后,它将每 10 秒钟打印一次已执行操作的摘要。经过一段时间与 Terraform 的工作,你通常可以根据操作完成所花费的时间来猜测该操作是否成功。
在 AWS 控制台的实例菜单中,我们可以看到虚拟机已经创建完成:
图 12.1 – 通过 Terraform 创建的新虚拟机实例
我们可以通过运行terraform destroy来删除我们刚刚创建的所有基础设施。
有趣的一点是,在我们的工作流程中,我们没有告诉 Terraform 它应该解释哪些文件。这是因为,正如之前提到的,Terraform 会读取当前目录下的所有.tf文件,并创建一个正确的执行计划。
如果你有兴趣查看步骤的层次结构,Terraform 提供了terraform graph命令,它会为你打印出该层次结构:
admin@myhome:~$ terraform graph
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] aws_instance.vm_example (expand)" [label = "aws_instance.vm_example", shape = "box"]
"[root] provider[\"registry.terraform.io/hashicorp/aws\"]" [label = "provider[\"registry.terraform.io/hashicorp/aws\"]", shape = "diamond"]
"[root] aws_instance.vm_example (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"]"
"[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)" -> "[root] aws_instance.vm_example (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/aws\"] (close)"
}
}
有一些工具可以创建 Terraform 生成的图形的良好可视化表示。
修改 Terraform 状态
有时,需要修改状态文件中的资源。在 Terraform 的旧版本中,这必须手动完成,并且容易出错。幸运的是,Terraform 开发者添加了一些命令行工具,可以帮助我们完成这项工作。
最有用的命令如下:
-
Terraform state rm:该命令从状态中移除资源。当我们手动删除了资源并从 Terraform 代码中移除它,但它仍然存在于状态中时,这个命令非常有用。 -
terraform state mv:该命令用于更改资源的名称。这在我们更改资源名称时很有用,以防止删除并创建一个新的资源,这通常不是我们想要的行为。 -
terraform taint:该命令强制重新创建资源。
导入现有资源
将现有资源导入到 Terraform 中,可以将这些资源纳入到 Terraform 状态中,Terraform 状态是一个由 Terraform 管理的资源快照。
terraform import命令用于将现有资源添加到你的 Terraform 状态中。该命令将现有资源映射到 Terraform 代码中的配置块,从而允许你使用 Terraform 管理该资源。
terraform import命令的语法如下:
terraform import [options] resource_in_code resource_identifier
terraform import命令的两个重要参数如下:
-
resource_in_code:在 Terraform 代码中资源的地址。 -
resource_identifier:你想导入的资源的唯一标识符。
举个例子,假设你有一个现有的 AWS S3 存储桶,ARN 为arn:aws:s3:::devopsy-bucket。要将这个资源导入到 Terraform 状态中,可以运行以下命令:
terraform import aws_s3_bucket.devopsy_bucket arn:aws:s3:::devopsy-bucket
导入资源对于你有现有基础设施并希望使用 Terraform 进行管理时非常有用。当你开始在现有项目中使用 Terraform 或者有一些在 Terraform 之外创建的资源时,通常会遇到这种情况。导入资源可以让你将这些资源纳入 Terraform 管理中,从而将来可以使用 Terraform 对其进行修改。
并非所有资源都可以导入到 Terraform 中。你打算导入的资源必须有一个唯一标识符,Terraform 可以利用它在远程服务中找到该资源。此外,资源还必须得到你在 Terraform 中使用的提供程序的支持。
工作区
Terraform 有一个工作区的概念。工作区类似于状态的版本。工作区允许你为相同的代码存储不同的状态。为了能够使用工作区,必须将状态文件存储在支持工作区的后端中。支持工作区的后端列表非常长,涵盖了大多数流行的云服务提供商。
这些工作区可以通过 .tf 文件中的 ${terraform.workspace} 序列来访问。结合条件表达式,这使得你能够创建不同的环境。例如,您可以根据工作区使用不同的 IP 地址,从而区分测试环境和生产环境。
总是存在一个工作区:default。它无法被删除。工作区操作可以通过 terraform workspace 命令来完成。
我们可以使用 terraform list 命令轻松查看当前有哪些工作区以及哪个是活动工作区:
admin@myhome:~$ terraform workspace list
* default
前面带星号的是当前工作区。如果我们只关心查看当前工作区而不是整个列表,可以运行 terraform show 命令:
admin@myhome:~$ terraform workspace show
default
每个工作区都会有一个状态文件。让我们做个实验:我们将创建一个名为 testing 的新工作区,并对测试工作区应用 Terraform。
首先,我们必须调用 terraform workspace new 来创建工作区:
admin@myhome:~$ terraform workspace new testing
Created and switched to workspace "testing"!
You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.
现在,我们必须确认自己确实处于新的工作区中,并使用我们之前的示例在其中运行 terraform apply:
admin@myhome:~$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.vm_example will be created
+ resource "aws_instance" "vm_example" {
+ ami = "ami-830c94e3"
[...]
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.vm_example: Creating...
aws_instance.vm_example: Still creating... [10s elapsed]
aws_instance.vm_example: Still creating... [20s elapsed]
aws_instance.vm_example: Still creating... [30s elapsed]
aws_instance.vm_example: Still creating... [40s elapsed]
aws_instance.vm_example: Still creating... [50s elapsed]
aws_instance.vm_example: Creation complete after 57s [id=i-06cf29fde369218e2]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
如你所见,我们成功创建了虚拟机。然而,当我们将工作区切换回默认工作区时,Terraform 又会要求重新创建它:
admin@myhome:~$ terraform workspace switch default
Switched to workspace "default".
admin@myhome:~$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.vm_example will be created
+ resource "aws_instance" "vm_example" {
[...]
即使资源的创建已经成功,Terraform 仍然会希望重新创建它。当我们检查存放 .tf 文件的目录时,我们会发现与默认工作区关联的 .tfstate 文件,以及一个名为 terraform.tfstate.d/ 的新目录,里面存放着 .tfstate 文件,每个文件都会存放在一个以工作区名称命名的子目录中。因此,对于测试工作区,状态文件将存储在 terraform.tfstate.d/testing 中:
admin@myhome:~$ ll
total 40
-rw-r--r-- 1 trochej staff 159B Mar 21 13:11 main.tf
-rw-r--r-- 1 trochej staff 158B Mar 21 12:27 providers.tf
-rw-r--r-- 1 trochej staff 4.4K Mar 21 21:17 terraform.tfstate
-rw-r--r-- 1 trochej staff 180B Mar 21 21:15 terraform.tfstate.backup
drwxr-xr-x 3 trochej staff 96B Mar 21 21:07 terraform.tfstate.d
admin@myhome:~$ ll terraform.tfstate.d
total 0
drwxr-xr-x 3 trochej staff 96B Mar 21 21:20 testing
admin@myhome:~$ ll terraform.tfstate.d/testing
total 8
-rw-r--r-- 1 trochej staff 180B Mar 21 21:18 terraform.tfstate
我们如何在 Terraform 代码中利用这一点呢?正如我们提到的,有一个特殊的序列(我们称之为变量),它会扩展为当前工作区的名称:
resource "aws_instance" "vm_example" {
ami = "ami-830c94e3"
instance_type = terraform.workspace == "default" ? "t2.micro" : "t2.nano"
tags = {
Name = "DevOpsGuideTerraformExample"
}
通过这个小的修改(如果 terraform.workspace 是默认值,则实例将是 t2.micro;否则,它将是 t2.nano),我们引入了与启动虚拟机的工作空间相关的条件变化。
让我们通过 terraform plan 快速确认一下:
admin@myhome:~$ terraform workspace show
default
admin@myhome:~$ terraform plan | grep instance_type
+ instance_type = "t2.micro"
admin@myhome:~$ terraform workspace select testing
Switched to workspace "testing".
admin@myhome:~$ terraform plan | grep instance_type
+ instance_type = "t2.nano"
如前面的输出所示,取决于我们选择的工作空间,将创建不同类型的实例。
在本节中,我们深入探讨了 Terraform IaC 工具。我们解释了提供者和模块的概念,以及状态文件的作用。我们还演示了简单的 Terraform 配置及其与 AWS 云的互动。
在下一节中,我们将更详细地介绍 HashiCorp 配置语言(HCL),它是专门用于编写这些配置的。
HCL 深入解析
HCL 是一种配置语言,由多个 HashiCorp 工具使用,包括 Terraform,用于定义和管理基础设施即代码(IaC)。
HCL 旨在让人类和机器都能轻松阅读和编写。它使用的语法简单,类似于 JSON,但结构更为宽松,并且支持注释。HCL 文件通常具有 .hcl 或 .tf 文件扩展名。
HCL 使用花括号来定义代码块,每个代码块都有一个标签,用于标识其类型。在每个代码块内,我们使用 key-value 语法定义属性,其中键是属性名,值是属性值。我们还可以使用花括号定义对象,如示例中所示的 tags 对象。
变量
在 HCL 中,变量使用 variable 块来定义。以下是如何在 HCL 中定义变量的示例:
variable "region" {
type = string
default = "eu-central-1"
}
在这个示例中,我们定义了一个名为 region 的变量,类型为 string,并指定了默认值 us-west-2。我们可以在代码中使用 ${var.region} 语法引用该变量。
HCL 支持多种数据类型的变量,包括 string、number、boolean、list、map 和 object。我们还可以使用 variable 块中的 description 参数为变量指定描述。
变量可以通过多种方式赋值,包括默认值、命令行参数或环境变量。在使用 Terraform 时,我们还可以在单独的文件中定义变量,并通过 .tfvars 文件扩展名(例如 variables.tfvars)或命令行参数在执行过程中传入。
一旦定义了变量,它们就不能更改,但 HCL 还允许在 locals 块内定义局部变量。局部变量对于简化模块或资源块中的复杂表达式或计算非常有用,因为它们可以将逻辑分解为更小、更易管理的部分。它们还可以使我们更容易维护代码,因为我们可以为那些可能频繁变化或需要跨多个资源或模块更新的值定义局部变量。
以下是一个locals块示例,它定义了eu-central-1区域并在每个 AZ 中生成子网:
locals {
azs = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
cidr_block = "10.0.0.0/16"
subnet_bits = 8
subnets = {
for idx, az in local.azs : az => {
name = "${var.environment}-subnet-${idx}"
cidr_block = cidrsubnet(local.cidr_block, local.subnet_bits, idx)
availability_zone = az
}
}
}
在此示例中,我们定义了一个locals块,其中包括以下变量:
-
azs:eu-central-1区域中的可用区列表 -
cidr_block:VPC 的 CIDR 块 -
subnet_bits:在 CIDR 块内为子网分配的位数 -
subnets:一个映射,使用for表达式为azs列表中的每个可用区生成子网
subnets映射中的for表达式会为azs列表中的每个可用区生成一个子网。子网的名称包括环境变量(可以作为变量传递)和可用区在列表中的索引。cidrsubnet函数用于根据cidr_block变量和subnet_bits变量计算每个子网的 CIDR 块。
生成的subnets映射将包含azs列表中每个可用区的键值对,其中键是可用区名称,值是一个映射,包含子网名称、CIDR 块和可用区。
注释
HCL 中的注释可以通过两种方式编写:单行注释和多行注释。单行注释以#符号开头,直到行末。多行注释则以/*开头,以*/结尾。多行注释可以跨越多行,通常用于提供更长的解释或暂时禁用代码段。
以下是一个单行注释的示例:
# This is a single-line comment in HCL
以下是一个多行注释的示例:
/*
This is a multi-line comment in HCL
It can span multiple lines and is often used
to provide longer explanations or to temporarily disable sections of code.
*/
Terraform 元参数
在 Terraform 中,元参数是可以用来修改资源块行为的特殊参数。之所以称其为元参数,是因为它们作用于整个资源块,而不是资源块中的特定属性。
元参数用于配置诸如资源实例数量(count)、资源名称(name)、资源之间的依赖关系(depends_on)等内容。
count
count允许你基于数字值创建多个资源实例。这在不重复整个代码块的情况下创建多个资源实例(例如 AWS 中的 EC2 实例)时非常有用。
例如,假设你想在 AWS 账户中创建三个 EC2 实例。你可以使用count元参数来创建多个相同的aws_ec2_instance资源块,而不是创建三个单独的资源块。以下是一个示例:
resource "aws_ec2_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
count = 3
}
在此示例中,我们使用相同的ami和instance_type创建三个 EC2 实例。count元参数设置为3,意味着 Terraform 将创建三个aws_ec2_instance资源块的实例。每个实例将被赋予唯一的标识符,例如aws_ec2_instance.example[0]、aws_ec2_instance.example[1]和aws_ec2_instance.example[2]。
for_each
for_each元参数类似于count元参数,它允许你创建多个资源实例。然而,for_each比count更灵活,因为它允许你根据一个映射或值集合来创建实例,而不仅仅是基于一个数值。
例如,假设你有一个 AWS 安全组的映射,并希望在 Terraform 代码中创建它们。你可以使用for_each在一个块中创建所有安全组,而不是创建多个aws_security_group资源块。下面是一个示例:
variable "security_groups" {
type = map(object({
name = string
description = string
ingress = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
}))
}
resource "aws_security_group" "example" {
for_each = var.security_groups
name_prefix = each.value.name
description = each.value.description
ingress {
from_port = each.value.ingress[0].from_port
to_port = each.value.ingress[0].to_port
protocol = each.value.ingress[0].protocol
cidr_blocks = each.value.ingress[0].cidr_blocks
}
}
在这个示例中,我们使用for_each元参数根据security_groups变量(这是一个对象的映射)创建多个aws_security_group资源块的实例。每个实例将根据映射的键生成一个唯一的标识符。我们还使用name_prefix属性来设置每个安全组的名称,使用description属性来设置描述。最后,我们使用ingress块定义每个安全组的入站流量规则。
使用for_each可以简化你的 Terraform 代码,并使其更具可重用性,特别是在处理映射或集合值时。然而,需要注意实例之间可能存在的依赖关系,并确保代码结构能够正确处理多个实例。
lifecycle
lifecycle元参数用于定义创建、更新和删除资源的自定义行为。它允许你比默认行为更精细地控制资源及其依赖关系的生命周期。
lifecycle元参数可用于定义以下属性:
-
create_before_destroy:如果设置为true,Terraform 将在销毁旧资源之前创建新资源,这在某些情况下可以防止停机。 -
prevent_destroy:如果设置为true,Terraform 将防止资源被销毁。这对于保护关键资源免受意外删除非常有用。 -
ignore_changes:Terraform 在判断是否需要更新资源时,应该忽略的一些属性名称列表。 -
replace_triggered_by:一个依赖项列表,当这些依赖项发生变化时,资源将被重新创建。
下面是使用lifecycle元参数来防止销毁 S3 存储桶的示例:
resource "aws_s3_bucket" "example" {
bucket = "example-bucket"
acl = "private"
lifecycle {
prevent_destroy = true
}
}
在这个示例中,lifecycle块用于将prevent_destroy属性设置为true,这意味着 Terraform 将防止aws_s3_bucket资源被销毁。这对于保护关键资源免于被意外删除非常有用。
depends_on
depends_on元参数用于定义资源之间的依赖关系。它允许你指定一个资源依赖于另一个资源,这意味着 Terraform 将在依赖的资源创建之后创建依赖资源。
然而,重要的是要注意,在大多数情况下,Terraform 可以通过分析你的资源配置自动创建依赖树。这意味着除非绝对必要,否则应避免使用 depends_on,因为它可能导致依赖循环,从而引发错误并使你的 Terraform 代码更难以管理。
如果你确实需要使用 depends_on,那么重要的是要意识到可能出现的依赖循环,并以避免它们的方式来组织代码。这可能涉及将资源拆分成更小的模块,或者使用其他技术来减少复杂性并避免循环依赖。
下面是一个使用 depends_on 来指定 EC2 实例与安全组之间依赖关系的示例:
resource "aws_security_group" "example" {
name_prefix = "example"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
depends_on = [aws_security_group.example]
}
在这个示例中,我们使用 depends_on 来指定 aws_instance 资源依赖于 aws_security_group 资源。这意味着 Terraform 会在创建实例之前先创建安全组。
你可以通过阅读官方文档来了解更多关于 HCL 语言的信息:developer.hashicorp.com/terraform/language。
Terraform 与 AWS 示例
在本节中,我们将创建两个示例模块,以演示如何创建一个模块以及在选择创建资源的方式时需要考虑的事项。我们将要创建的模块将能够创建一个或多个 EC2 实例,一个附加的安全组以及其他所需的资源,如实例配置文件。它将做几乎所有我们在第十章中讨论的内容,但会使用 AWS CLI。
EC2 实例模块
让我们创建一个能够创建 EC2 实例的模块。考虑以下目录结构:
├── aws
│ └── eu-central-1
└── modules
modules 目录是我们放置所有模块的地方,aws 是我们存放 AWS 基础设施的地方,eu-central-1 是法兰克福 AWS 区域的基础设施代码。因此,让我们开始创建 EC2 模块。我们先创建一个目录来存放它和我们将需要的基本文件,如前所述:
admin@myhome:~$ cd modules
admin@myhome:~/modules$ mkdir aws_ec2
admin@myhome:~/modules$ cd aws_ec2
admin@myhome:~/modules/aws_ec2$ touch versions.tf main.tf variables.tf outputs.tf providers.tf
admin@myhome:~/modules/aws_ec2$ ls -l
total 0
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 main.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 outputs.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 providers.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 variables.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 versions.tf
admin@myhome:~/modules/aws_ec2$
注意,我们没有创建后端配置文件。这是因为后端将在根模块中配置。模块没有状态文件,因为由模块创建的资源将使用根(或主)模块的状态文件。让我们开始配置提供程序。在这种情况下,我们此时只需要 AWS 提供程序。在我们的示例中,我们将使用 eu-central-1 区域:
provider "aws" {
region = "eu-central-1"
}
接下来,让我们在 versions.tf 文件中配置我们将使用的 Terraform 和 AWS 提供程序的版本:
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 3.0.0"
}
}
}
在这个示例中,required_version 属性设置为 >= 1.0.0,要求使用 Terraform 1.0.0 或更高版本。required_providers 属性用于指定 AWS 提供者,source 属性设置为 hashicorp/aws,并且 version 属性设置为 >= 3.0.0,要求使用最新版本的 AWS 提供者。
现在,我们可以做一些更有趣的事情,比如添加一个实际的 aws_instance 资源。为此,我们将开始填写这个资源所需的变量:
resource "aws_instance" "test_instance" {
ami = "ami-1234567890"
instance_type = "t3.micro"
tags = {
Name = "TestInstance"
}
}
保存所有模块文件的更改后,我们可以回到 aws/eu-central-1 目录,并创建一个与模块中类似的文件集:
admin@myhome:~/modules/aws_ec2$ cd ../../aws/eu-central-1
admin@myhome:~/aws/eu-central-1$ touch versions.tf main.tf variables.tf providers.tf
admin@myhome:~/aws/eu-central-1$ ls -l
total 0
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 main.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 providers.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 variables.tf
-rw-r--r-- 1 admin admin 0 Mar 16 13:02 versions.tf
admin@myhome:~/aws/eu-central-1$
这次,我们只需要 main.ft、providers.tf、variables.tf 和 versions.tf。为了简化,我们可以直接复制 providers 和 versions 文件的内容:
admin@myhome:~/aws/eu-central-1$ cp ../../modules/aws_ec2/providers.tf .
admin@myhome:~/aws/eu-central-1$ cp ../../modules/aws_ec2/versions.tf .
现在,我们可以集中精力在 main.tf 文件中,在这里我们将尝试使用模块的第一个版本。main.tf 文件将如下所示:
module "test_instance" {
source = "../../modules/aws_ec2"
}
我们创建的模块不需要任何变量,所以在这个文件中这就是我们所需要的全部内容。
由于这是我们的根模块,我们还需要配置 Terraform 状态文件的位置。为了简化,我们将使用本地状态文件,但在实际环境中,我们建议使用配置了分布式锁的 S3 桶。如果没有后端块,Terraform 将创建一个本地文件。我们已经准备好测试我们的模块(输出已缩短以便简洁):
admin@myhome:~/aws/eu-central-1$ terraform init
Initializing modules...
- test_instance in ../../modules/aws_ec2
Initializing the backend...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
一旦你运行了 terraform init(只有在更新模块或后端配置时才需要重新运行),你可以执行 terraform plan 来查看需要应用的更改:
admin@myhome:~/aws/eu-central-1$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.test_instance.aws_instance.test_instance will be created
+ resource "aws_instance" "test_instance" {
+ ami = "ami-1234567890"
# Some of the output removed for readability
Plan: 1 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
注意
你没有使用 -out 选项保存这个计划,因此如果你现在运行 terraform apply,Terraform 无法保证会执行完全相同的操作。
在这个计划中,Terraform 确认我们的模块将为我们创建一个 EC2 实例。不幸的是,这个计划并不理想,因为它没有检查 AMI 是否实际存在,或者子网是否存在。这些错误将在我们运行 terraform apply 时出现。例如,我们提供的 AMI 是假的,因此 Terraform 在创建实例时会失败。让我们回到模块并改进它,通过自动获取正确的 Ubuntu Linux AMI。为此,Terraform AWS 提供者提供了一个数据资源。这个特殊资源使我们能够通过其 API 请求 AWS 提供各种资源。让我们在 modules 目录的 main.tf 文件中添加一个 AMI 数据资源:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "test_instance" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "TestInstance"
}
}
(aws_ami) 代码块使用 aws_ami 数据源从 AWS 市场获取 Canonical 所拥有的最新 Ubuntu AMI。它通过将 most_recent 参数设置为 true,并使用 AMI 的 name 属性来过滤结果。它寻找一个具有特定名称模式的 AMI:ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*。
第二段代码使用在第一段代码中获取的 AMI 创建了一个 AWS EC2 实例。它将实例类型设置为t3.micro,这是一种适合测试目的的小型实例类型。
它还为 EC2 实例添加了一个标签,键名为Name,值为TestInstance,这样它就能在 AWS 管理控制台中轻松识别。
您可以在文档中阅读更多关于aws_ami数据资源的内容:registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami。
做了这个修改后,我们可以运行terraform plan并查看是否有所变化:
admin@myhome:~/aws/eu-central-1$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.test_instance.aws_instance.test_instance will be created
+ resource "aws_instance" "test_instance" {
+ ami = "ami-050096f31d010b533"
# Rest of the output removed for readability
计划成功执行,看起来它找到了一个最近的适用于 Ubuntu Linux 22.04 的 AMI。我们需要考虑一些其他问题,尤其是如果我们想确保能够连接到这个新的 EC2 实例。 当然,如果我们应用更改,它会被创建,但我们目前没有办法连接到它。首先,让我们将 EC2 实例连接到正确的网络:我们将使用一个默认的 VPC 和一个公有子网,这样我们就能直接连接到这个实例。
为了找出默认 VPC 和公有子网的 ID,我们将再次使用数据资源:
-
VPC 文档:
registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc -
子网 文档:
registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnets
问题是我们是否希望将所有创建的实例自动放入默认的 VPC(和公有子网)。通常,这个问题的答案是否。在这种情况下,我们需要添加一些变量并将其传递给这个模块。
让我们向根模块中添加另一个文件,在这个文件中放置所有的数据资源,命名为data.tf:
data "aws_vpc" "default" {
filter {
name = "isDefault"
values = ["true"]
}
}
data "aws_subnets" "public" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
filter {
name = "map-public-ip-on-launch"
values = ["true"]
}
}
现在,我们可以在模块中创建一个输入变量。返回到modules/aws_ec2目录并编辑variables.tf文件:
variable "public_subnet_id" {
description = "Subnet ID we will run our EC2 instance"
type = string
}
现在,当你运行terraform plan时,你会看到以下错误:
│ Error: Missing required argument
│
│ on main.tf line 1, in module "test_instance":
│ 1: module "test_instance" {
│
│ The argument "public_subnet_id" is required, but no definition was found.
在这里,我们创建了一个必需的变量,我们需要将其提供给模块。让我们通过编辑根模块中的main.tf文件(aws/eu-central-1目录)来完成:
module "test_instance" {
source = "../../modules/aws_ec2"
public_subnet_id = data.aws_subnets.public.ids[0]
}
注意data.aws_subnets.public.ids[0]。我们使用了列表表示法,在这里我们选择了列表中的第一个元素(它是一个字符串,因为模块期望它是字符串)。这是因为有多个子网,aws_subnets为我们返回了这些子网的列表。
再次运行计划应该会给我们添加一个资源。太好了!现在,我们的实例将获得一个可以连接的公网 IP 地址。但我们仍然缺少一个防火墙规则,这个规则允许我们连接到端口22(SSH)。让我们创建一个安全 组(SG)。
再次提醒,我们可以选择在根模块中创建一个安全组,这样我们就可以在不更改 EC2 模块的情况下修改它。或者,我们可以将安全组添加到 EC2 模块中,这意味着该模块将完全控制它,但会缺少一些灵活性。还可以创建一个同时执行两者的模块:从根模块注入一个安全组并使用模块中预定义的安全组,但这超出了本章的范围。在本例中,为了简化起见,我们将在模块内部创建一个安全组。
要创建一个安全组(SG),我们将使用aws_security_group资源,它需要一个 VPC ID。有两种可能性:我们需要向 EC2 模块引入另一个变量,或者使用另一个数据资源从提供的子网自动获取 VPC ID。这次,采用更优雅的解决方案是使用数据资源。让我们将其添加到模块中的main.tf:
data "aws_subnet" "current" {
id = var.public_subnet_id
}
有了这个配置,我们现在可以添加一个安全组了:
resource "aws_security_group" "allow_ssh" {
name = "TestInstanceSG"
description = "Allow SSH traffic"
vpc_id = data.aws_subnet.current.vpc_id
ingress {
description = "SSH from the Internet"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "TestInstanceSG"
}
}
上述代码创建了一个允许 SSH 流量通过的 AWS 安全组(SG),以连接我们之前创建的 EC2 实例。
aws_security_group资源用于为 EC2 实例创建一个安全组。它将安全组的名称设置为TestInstanceSG,并提供简短的描述。
vpc_id属性被设置为当前子网的 VPC ID。它使用名为current的aws_subnet数据源来获取当前子网的 VPC ID。
ingress块定义了安全组的入站规则。在本例中,它通过指定from_port、to_port、protocol和cidr_blocks,允许来自任何 IP 地址(0.0.0.0/0)的 SSH 流量。
egress块定义了安全组的出站规则。在本例中,它通过指定from_port、to_port、protocol和cidr_blocks允许所有出站流量。它还通过指定ipv6_cidr_blocks来允许所有 IPv6 流量。
tags属性为安全组设置一个标签,键为Name,值为TestInstanceSG,这样可以方便地在 AWS 管理控制台中进行识别。
现在,我们准备将此安全组附加到我们的实例。我们需要在aws_instance资源中使用security_groups选项:
resource "aws_instance" "test_instance" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
security_groups = [aws_security_group.allow_ssh.id]
tags = {
Name = "TestInstance"
}
}
现在,运行terraform plan后,您将看到两个需要添加的资源:
Plan: 2 to add, 0 to change, 0 to destroy.
在此时,我们需要将我们的公有 SSH 密钥添加到 AWS 并配置 EC2 使用它作为默认的 Ubuntu Linux 用户(ubuntu)。假设您已经生成了 SSH 密钥,我们将创建一个包含该密钥的变量,创建一个资源使该密钥可用于 EC2 实例,最后将其添加到实例配置中。
重要说明
在某些 AWS 区域中,要求使用旧版 RSA 密钥格式。只要可用,我们建议根据最新的推荐使用最新的格式。在撰写本书时,推荐使用ED25519密钥。
让我们在根模块中添加一个变量:
variable "ssh_key" {
description = "SSH key attached to the instance"
type = string
default = "ssh-rsa AAASomeRSAKEY""
}
让我们为 EC2 模块添加一个类似的安全组:
variable "ssh_key" {
description = "SSH key attached to the instance"
type = string
}
这是没有默认值的,以使得这个变量对每个模块使用时都是必需的。现在,让我们在 EC2 模块(main.tf)中添加密钥到 AWS 中:
resource "aws_key_pair" "deployer" {
key_name = "ssh_deployer_key"
public_key = var.ssh_key
}
然后,我们可以在aws_instance资源中使用它:
resource "aws_instance" "test_instance" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
security_groups = [aws_security_group.allow_ssh.id]
key_name = aws_key_pair.deployer.key_name
tags = {
Name = "TestInstance"
}
}
我们需要在根模块中的main.tf里使用这个新变量:
module "test_instance" {
source = "../../modules/aws_ec2"
public_subnet_id = data.aws_subnets.public.ids[0]
ssh_key = var.ssh_key
}
最后,运行terraform plan将给我们显示三个资源:
Plan: 3 to add, 0 to change, 0 to destroy.
太棒了!运行terraform apply并接受任何更改将会在公共子网中部署 EC2 实例,并使用我们的密钥。但是,除非我们去 AWS 控制台手动检查,否则我们仍然无法知道实例的 IP 地址。
为了获取这些信息,我们需要从 EC2 模块中导出这些变量,然后再次在根模块中使用。为此,我们有另一个代码块叫output。它的语法与variable语法非常相似,但你还可以将output变量标记为敏感信息,这样在运行terraform plan或terraform apply命令时,默认情况下不会显示它。
让我们定义输出,显示 EC2 实例的公共 IP 地址。在 EC2 模块的outputs.tf文件中,放入以下代码:
output "instance_public_ip" {
value = aws_instance.test_instance.public_ip
description = "Public IP address of the EC2 instance"
}
在根模块中创建outputs.tf文件,并在那里放入以下代码:
output "instance_public_ip" {
value = module.test_instance.instance_public_ip
description = "Public IP address of the instance"
}
现在,当你运行terraform plan时,你会看到输出结果有所变化:
Plan: 3 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_public_ip = (known after apply)
这样,我们通过创建一个单独的 EC2 实例创建了一个简单的模块。如果我们运行terraform apply,实例将被创建,并且输出结果会显示我们该实例的 IP 地址。
从这里开始,接下来的步骤将涉及为模块添加更多功能,通过使用count元参数来创建多个实例,或者通过使用for_each元参数来创建一组不同的 EC2 实例。
总结
在本章中,我们介绍了 IaC 的概念。我们解释了为什么它是管理和开发基础设施的重要方法。我们还介绍了一些在这种工作方式中相当流行的工具。作为首选工具,我们讲解了 Terraform——可能是最广泛使用的工具。
在下一章,我们将展示如何利用一些在线工具和自动化来构建CI和CD的管道。
练习
尝试以下练习,测试你在本章中学到的内容:
-
创建一个模块,用于创建启用服务器端加密的 S3 存储桶。
-
向我们创建的模块添加一个实例配置文件,并使用我们在第十章中使用的相同 IAM 策略。
-
使用
count元参数创建两个实例。
第十三章:使用 Terraform、GitHub 和 Atlantis 实现 CI/CD
本章我们将在前几章的基础上,介绍持续集成(CI)和持续部署(CD)的管道。市场上有许多可用的 CI 和 CD 工具,包括开源和闭源工具、自托管和 软件即服务(SaaS)工具。我们将演示一个示例管道,从将源代码提交到存储 Terraform 代码的仓库,到在基础设施中应用更改。我们将自动完成这一过程,但会经过团队的审查。
在本章中,我们将覆盖以下主题:
-
什么是 CI/CD?
-
持续集成和部署你的基础设施
-
使用 Atlantis 实现 CI/CD
技术要求
本章需要以下内容:
-
一个 Linux 服务器
-
GitHub 或类似平台(GitLab 或 Bitbucket)上的免费账户
-
最新版本的 Terraform
-
AWS CLI
-
Git
什么是 CI/CD?
CI/CD 是一套实践、工具和流程,帮助软件开发团队自动化构建、测试和部署应用程序,从而更频繁地发布软件,并且更有信心其质量。
持续集成(CI)是一种实践,开发人员定期将代码更改集成到代码仓库中,每次集成都触发自动构建和测试过程。这有助于尽早发现错误,并确保应用程序可以可靠地构建和测试。
例如,使用 Docker,开发人员可以设置一个 CI 管道,该管道在每次将更改推送到代码仓库时自动构建和测试应用程序。管道可以包括构建 Docker 镜像、运行自动化测试以及将镜像发布到 Docker 仓库的步骤。
持续交付是指在成功集成过程后,使软件可以随时进行部署的实践。例如,使用 Docker 镜像,交付过程是将镜像推送到 Docker 仓库,之后可以由部署管道提取。
最后,持续部署(CD)是将经过测试和验证的持续交付过程代码工件(如 Docker 镜像、Java JAR 文件、ZIP 压缩包等)自动部署到生产环境中的实践。这样消除了部署过程中的人工干预需求。
让我们来看一下几种常见的部署策略:
-
滚动部署:这种策略涉及一次只将更改部署到一部分服务器,逐步将更改推广到整个基础设施。这使得团队能够监控更改,并在出现问题时迅速回滚。
-
蓝绿部署:在这种策略中,设置两个相同的生产环境,一个处于激活状态(蓝色),另一个处于非激活状态(绿色)。代码变更被部署到非激活环境,并在切换流量到新环境之前进行测试。这种方式可以实现零停机部署。
-
金丝雀部署:这种策略涉及将变更部署到一小部分用户,并保持大多数用户使用当前版本。这样,团队可以监控这些变更并收集反馈,在将这些变更推广到所有用户之前,进行充分测试。
-
功能开关/特性开关:通过这种策略,变更被部署到生产环境,但会被隐藏在功能开关后面。这个开关随后会逐步对特定用户或环境启用,允许团队在将新功能提供给所有人之前,控制功能的发布并收集反馈。
所有这些策略都可以通过 CD 工具 来实现自动化,如 Jenkins、CircleCI、GitHub 和 GitLab Actions、Travis CI 等众多工具。
在讨论应用程序的部署时,我们至少需要提到 GitOps。GitOps 是一种新的基础设施和应用程序部署方法,它使用 Git 作为声明性基础设施和应用程序规格的唯一真理来源。其核心思想是在 Git 仓库中定义基础设施和应用程序的期望状态,并使用 GitOps 工具自动将这些变更应用到目标环境。
在 GitOps 中,对基础设施或应用程序的每一次更改都是通过 Git 提交来完成的,这会触发一个流水线,将这些更改应用到目标环境。这提供了完整的变更审计跟踪,并确保基础设施始终处于期望的状态。
一些有助于启用 GitOps 的工具包括以下几种:
-
FluxCD:这是一个流行的 GitOps 工具,可以通过 Git 作为唯一的真理来源,自动化应用程序和基础设施的部署和扩展。它与 Kubernetes、Helm 和其他工具集成,提供完整的 GitOps 工作流。
-
ArgoCD:这是另一款流行的 GitOps 工具,使用 Git 作为真理来源来部署和管理应用程序和基础设施。它提供基于网页的 UI 和 CLI 来管理 GitOps 流水线,并与 Kubernetes、Helm 以及其他工具集成。
-
Jenkins X:这是一个包含 GitOps 工作流的 CI/CD 平台,用于构建、测试和将应用程序部署到 Kubernetes 集群。它利用 GitOps 来管理整个流水线,从源代码到生产环境的部署。
既然我们已经了解了 CI/CD 的概念,接下来我们可以探索一些工具,这些工具可以用来构建这样的流水线。在下一节中,我们将为您提供一些流水线示例,包含克隆最新版本的代码库、构建 Docker 镜像以及运行一些测试。
CI/CD 流水线示例
让我们看一些自动化流水线的示例,这些流水线将我们的 Terraform 更改应用到不同的 CD 工具中。
Jenkins
Jenkins 是最流行的开源 CI/CD 工具之一。它从点击式配置转变为在用户批准更改后运行apply:
pipeline {
agent any
environment {
AWS_ACCESS_KEY_ID = credentials('aws-key-id')
AWS_SECRET_ACCESS_KEY = credentials('aws-secret-key')
}
上述代码开启了一个新的流水线定义。它被设置为在任何可用的 Jenkins 代理上运行。接下来,我们设置了将在该流水线中使用的环境变量。这些环境变量的文本是从aws-key-id和aws-secret-key Jenkins 凭证中提取的。在运行该流水线之前,这些凭证需要被定义。
接下来,我们将在stages块中定义每个步骤的流水线:
stages {
stage('Checkout') {
steps {
checkout scm
}
}
首先,我们将克隆我们的 Git 仓库;执行此操作的步骤是checkout scm。URL 将在 Jenkins UI 中直接配置:
stage('TF Plan') {
steps {
dir('terraform') {
sh 'terraform init'
sh 'terraform plan -out terraform.tfplan'
}
}
}
接下来,我们将运行terraform init来初始化 Terraform 环境。在这里,我们运行计划并将输出保存到terraform.tfplan文件中,最后一步将使用此文件运行apply:
stage('Approval') {
steps {
script {
def userInput = input(id: 'confirm', message: 'Run apply?', parameters: [ [$class: 'BooleanParameterDefinition', defaultValue: false, description: 'Running apply', name: 'confirm'] ])
}
}
}
这个步骤定义了用户输入。我们需要在审查计划的输出后通过运行apply来确认这一点。我们将默认值定义为false。流水线将在此步骤等待您的输入:
stage('TF Apply') {
steps {
dir('terraform') {
sh 'terraform apply -auto-approve -input=false terraform.tfplan'
sh 'rm -f terraform.tfplan'
}
}
}
}
}
最后,如果您已确认流水线将运行apply而无需进一步的用户输入(-input=false选项),并且apply运行没有任何错误,它将删除在计划步骤中创建的terraform.tfplan文件。
GitHub Actions 基础
有可能创建一个类似的流水线,使用workflow_dispatch选项,但它会在 Action 运行之前要求用户输入(请参阅官方文档作为参考:github.blog/changelog/2021-11-10-github-actions-input-types-for-manual-workflows/)。因此,我们改为创建一个将运行plan和apply的 Action:
name: Terraform Apply
on:
push:
branches: [ main ]
上述代码定义了 GitHub Action 将仅在更改主分支时触发:
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
在这里,我们以类似于 Jenkins 流水线的方式定义了环境变量。AWS 的访问密钥和密钥从存储在 GitHub 中的秘密中提取,这些秘密需要我们事先添加。如果我们的 GitHub 运行器在 AWS 环境中运行或我们使用 GitHub OpenID Connect,则无需此操作。您可以通过查看 GitHub 文档了解后者:docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services。
接下来,我们可以在jobs块中定义 GitHub Action 的步骤:
jobs:
terraform_apply:
runs-on: ubuntu-latest
在这里,我们定义了一个名为terraform_apply的作业,它将在 GitHub Actions 中可用的最新版本的 Ubuntu 运行器上运行:
steps:
- name: Checkout code
uses: actions/checkout@v2
该步骤检出代码。我们使用 GitHub 中可用的预定义 Action,而不是创建一个运行 Git 命令行的脚本。它将执行的确切代码可在github.com/actions/checkout找到:
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
Setup Terraform步骤将为我们下载 Terraform。默认情况下,它将下载最新的可用版本,但如果需要,我们可以固定某个特定版本。该步骤的代码可在github.com/hashicorp/setup-terraform找到:
- name: Terraform Plan
working-directory: terraform/
run: |
terraform init
terraform plan -out terraform.tfplan
在Terraform Plan步骤中,我们初始化 Terraform 并以与 Jenkins 管道相同的方式运行计划:
- name: Terraform Apply
working-directory: terraform/
run: |
terraform apply -auto-approve –input=false terraform.tfplan
rm -f terraform.tfplan
最后,Terraform Apply步骤将从之前保存的 Terraform 计划文件terraform.tfplan中应用基础设施变更,并删除计划文件。
如果您希望创建一段可以在任何 CI/CD 工具中工作的更强大的代码,可以创建一个Bash 脚本来完成繁重的工作。使用 Bash 脚本,您还可以在运行计划之前嵌入一些测试。以下是一个示例 Bash 脚本,它将为您运行 Terraform 计划并应用它:
#!/usr/bin/env bash
set -u
set -e
在这里,我们将 Bash 设置为运行此脚本的默认 shell。在接下来的几行中,我们将修改脚本的默认设置,使其在遇到任何未绑定的变量或错误时停止执行:
# Check if Terraform binary is in PATH
if command -v terraform &> /dev/null; then
TERRAFORM_BIN="$(command -v terraform)"
else
echo "Terraform not installed?"
exit 1
fi
这个代码块检查系统中是否可用 Terraform,并将其完整路径保存到TERRAFORM_BIN变量中,我们稍后将使用它:
# Init terraform backend
$TERRAFORM_BIN init -input=false
在运行计划之前,初始化 Terraform 环境:
# Plan changes
echo "Running plan..."
$TERRAFORM_BIN plan -input=false -out=./terraform.tfplan
运行plan并将其保存到文件中以供后续使用:
echo "Running Terraform now"
if $TERRAFORM_BIN apply -input=false ./terraform.tfplan; then
echo "Terraform finished successfully"
RETCODE=0
else
echo "Failed!" fi
fi
上面的代码块执行Terraform apply并检查命令的返回代码。它还会显示相应的信息。
其他主要的 CI/CD 解决方案使用类似的方法。最大的区别在于 Jenkins、企业工具和开源解决方案之间,其中YAML配置最为常见。在下一节中,我们将更深入地探讨管道的每个阶段,重点关注 Terraform 的集成测试以及基础设施变更的部署。
持续集成和部署您的基础设施
测试应用程序代码现在已经成为事实上的标准,特别是在测试驱动开发(TDD)被采纳之后。TDD 是一种软件开发过程,在这个过程中,开发人员在编写代码之前先编写自动化测试。
这些测试最初是故意失败的,开发人员随后编写代码使其通过。代码会不断重构,以确保高效、可维护,并且能够通过所有测试。这种方法有助于减少漏洞并提高软件的可靠性。
测试基础设施并不像看起来那么简单,因为很难在不实际启动实例的情况下检查 Amazon 弹性计算云(EC2)是否会成功启动。虽然可以模拟对 AWS 的 API 调用,但这并不能保证实际的 API 会返回与测试代码相同的结果。使用 AWS 时,这也意味着测试会很慢(我们需要等待该 EC2 实例启动),并且可能会产生额外的云端费用。
有多个基础设施测试工具,既有与 Terraform 集成的,也有第三方软件(这也是开源软件)。
集成测试
我们在 CI 流水线中可以运行多种基本测试。我们可以检测实际代码与云端运行代码之间的偏差,我们可以根据推荐的格式对代码进行 lint 检查,还可以测试代码是否符合我们的合规政策。我们还可以从 Terraform 代码中估算 AWS 成本。更复杂且耗时的过程包括单元测试和代码的端到端测试。让我们来看一下在流水线中可以使用的可用工具。
我们可以从一开始运行的大多数基本测试仅涉及运行 terraform validate 和 terraform fmt。前者将检查 Terraform 代码的语法是否有效(意味着资源和/或变量没有拼写错误、所有必需的变量都存在等)。fmt 检查将根据 Terraform 标准更新代码的格式,这意味着所有多余的空格将被删除,或者某些空格可能会被添加以对齐 = 符号以增强可读性。这对于更简单的基础设施来说可能已经足够,我们建议从一开始就添加这些测试,因为这样做非常简单。你可以重用我们之前提供的代码片段来启动你现有代码的过程。
基础设施成本
基础设施成本不是你可以在测试流水线中运行的功能性、静态或单元测试。虽然监控这一方面对你的基础设施非常有用,但你的经理也会很高兴知道 AWS 预算是否符合预测。
Infracost.io 是一个云成本估算工具,允许你通过提供实时的云资源成本分析来监控和管理基础设施成本。使用 Infracost.io,你可以估算基础设施变更的成本,并在开发周期的每个阶段提供成本反馈,避免任何意外的费用。
将 Infracost.io 集成到 GitHub Actions 中是一个简单的过程,包括创建一个 Infracost.io 账户并生成一个 API 密钥,允许你访问成本估算数据。
接下来,你需要在本地机器上安装 Infracost.io 的命令行工具(CLI)。CLI 是一个命令行工具,允许你计算和比较基础设施成本。
安装 CLI 后,您可以通过创建一个新的操作文件(例如 .github/workflows/infracost.yml)来将 Infracost.io 操作添加到您的 GitHub 工作流中。
在 Infracost.io 的操作文件中,您需要指定 Infracost.io API 密钥以及 Terraform 配置文件的路径:
name: Infracost
on:
push:
branches:
- main
jobs:
infracost:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run Infracost
uses: infracost/infracost-action@v1
with:
api_key: ${{ secrets.INFRACOST_API_KEY }}
terraform_dir: ./terraform
最后,将更改提交并推送到您的 GitHub 仓库。每当新的 Terraform 配置文件被推送到仓库时,Infracost.io 操作会自动计算成本估算,并在 GitHub Actions 页面上提供反馈。
对于开源项目,Infracost 是免费的,但您也可以创建自己的服务来监控云成本。Infracost 的 GitHub 仓库可以在github.com/infracost/infracost找到。
通过将其集成到您的 CI 流水线中,您可以主动监控和管理基础设施成本,并在将其部署到云账户之前,基于这些信息做出有关基础设施更改的明智决策。
漂移测试
使用 Terraform 管理基础设施的一个挑战是确保基础设施的实际状态与 Terraform 配置文件中定义的期望状态相匹配。这就是 漂移 概念的由来。
漂移发生在基础设施的期望状态与实际状态之间存在差异时。例如,如果使用 Terraform 创建的资源被手动修改而不通过 Terraform,基础设施的实际状态将与 Terraform 配置文件中定义的期望状态不同。这可能导致基础设施的不一致,并可能引发操作问题。
为了检测基础设施中的漂移,Terraform 提供了一个名为 terraform plan 的命令。当运行此命令时,Terraform 会将配置文件中定义的期望状态与基础设施的实际状态进行比较,并生成一份计划,列出将基础设施恢复到期望状态所需的更改。如果期望状态和实际状态之间存在任何差异,Terraform 会在计划输出中显示它们。
还可以使用第三方工具扩展 Terraform 的这一功能。其中一个工具就是 Driftctl。
Driftctl 是一个开源工具,帮助检测由 Terraform 管理的云基础设施中的漂移。它扫描基础设施资源的实际状态,并将其与 Terraform 配置文件中定义的期望状态进行比较,以识别任何差异或不一致。Driftctl 支持广泛的云服务提供商,包括 AWS、Google Cloud、Microsoft Azure 和 Kubernetes。
Driftctl 可以以多种方式使用来检测基础设施中的漂移。它可以与 CI/CD 流水线集成,自动检测漂移并触发修正措施。也可以手动运行以按需检查漂移。
这是一个使用Driftctl工具来检测基础设施漂移的 GitHub pipeline 示例:
name: Detect Infrastructure Drift
on:
push:
branches:
- main
上述代码表明,此 pipeline 将仅在main分支上运行。
在这里,我们定义了一个名为detect-drift的作业,它将在最新的 Ubuntu Linux runner 上运行:
jobs:
detect-drift:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
接下来,我们开始定义 pipeline 中每个步骤要做的事情——首先,我们将使用一个预定义的动作,在 runner 上运行git clone:
- name: Install Terraform
run: |
sudo apt-get update
sudo apt-get install -y unzip
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install terraform
接下来的步骤是定义一个 shell 脚本,用于从 HashiCorp 发布的公共仓库安装、解压并下载 Terraform:
- name: Install Driftctl
run: |
curl https://github.com/cloudskiff/driftctl/releases/download/v0.8.2/driftctl_0.8.2_linux_amd64.tar.gz -sSLo driftctl.tar.gz
tar -xzf driftctl.tar.gz
sudo mv driftctl /usr/local/bin/
在这一步中,我们将通过从 GitHub 上的公共发布下载归档来安装Driftctl工具。我们将提取文件并将二进制文件移动到/usr/local/bin目录中:
- name: Initialize Terraform
run: terraform init
- name: Check Terraform Configuration
run: terraform validate
上述步骤仅涉及运行terraform init和terraform validate,以验证我们是否可以访问 Terraform 后端,以及我们打算在接下来的几步中检查的代码是否在语法上有效:
- name: Detect Drift with Driftctl
run: |
driftctl scan –from tfstate://./terraform.tfstate –output json > drift.json
- name: Upload Drift Report to GitHub
uses: actions/upload-artifact@v2
with:
name: drift-report
path: drift.json
最后两步是运行Driftctl工具,并将其发现结果保存在driftctl.json文件中,该文件将作为drift-report上传到 GitHub 工件中。
总结一下,这个 pipeline 在main分支上运行,并执行以下步骤:
-
从 GitHub 仓库检出代码。
-
安装
terraform和driftctl。 -
初始化 Terraform 并验证 Terraform 配置文件。
-
使用
driftctl扫描基础设施资源的实际状态,并将其与 Terraform 配置文件中定义的期望状态进行比较,以检测任何漂移。此扫描的输出将保存到名为drift.json的 JSON 文件中。 -
将
drift.json文件作为工件上传到 GitHub,供后续分析使用。
此外,这个 pipeline 还可以根据特定需求进行自定义,例如集成到 CI/CD pipeline 中或按计划定期检查基础设施中的漂移。
每次 pipeline 运行时都需要安装driftctl和terraform并不是我们期望的做法,因此我们建议您准备一个包含这些工具预安装的 Docker 镜像,并使用该镜像。这还将提升您的安全性。
您可以在网站driftctl.com/上找到有关该项目的更多信息。
安全测试
测试基础设施的安全性是维护安全稳定系统的一个重要方面。由于现代基础设施通常作为代码进行定义和管理,因此有必要像测试其他代码一样对其进行测试。基础设施即代码(IaC)测试有助于发现安全漏洞、配置问题以及可能导致系统被攻破的其他缺陷。
有几种自动化工具可以帮助进行基础设施安全测试。这些工具可以帮助识别潜在的安全问题,例如配置错误的安全组、未使用的安全规则和不安全的敏感数据。
我们可以使用几个工具来作为 CI 管道外的单独过程进行安全性测试:
-
Prowler:这是一个开源工具,用于扫描 AWS 基础设施中的安全漏洞。它可以检查诸如 AWS 身份与访问管理(IAM)配置错误、开放的安全组和S3 存储桶权限问题等。
-
CloudFormation Guard:这是一个工具,用于根据一组预定义的安全规则验证AWS CloudFormation模板。它可以帮助识别诸如开放安全组、未使用的 IAM 策略和未加密的 S3 存储桶等问题。
-
OpenSCAP:这是一个为基于 Linux 的基础设施提供自动化安全合规性测试的工具。它可以扫描系统是否符合各种安全标准,如支付卡行业数据安全标准(PCI DSS)或国家标准与技术研究院特别出版物 800-53(NIST SP 800-53)。
-
InSpec:这是另一个开源测试框架,可用于测试基础设施的合规性和安全性。它内置支持多种平台,并可以用于针对不同的安全标准进行测试,例如健康保险流通与责任法案(HIPAA)和互联网安全中心(CIS)。
在这里,我们重点关注在持续集成(CI)中集成一些安全测试。我们可以在此处集成的工具如下:
-
tfsec 是一个开源的 Terraform 代码静态分析工具。它扫描 Terraform 配置,以检测潜在的安全问题,并提供修复建议。它内置支持多种云服务提供商,可以帮助识别诸如弱身份验证、不安全的网络配置和未加密的数据存储等问题。其 GitHub 仓库可以在
github.com/aquasecurity/tfsec找到。 -
Terrascan 是一个用于基础设施即代码(IaC)文件的静态代码分析开源工具。它支持多种 IaC 文件格式,包括 Terraform、Kubernetes YAML 和 Helm 图表,并扫描这些文件中的安全漏洞、合规性违规和其他问题。Terrascan 可以集成到 CI/CD 管道中,并帮助确保基础设施部署是安全的,并符合行业标准。其 GitHub 仓库可以在
github.com/tenable/terrascan找到。 -
CloudQuery 是一个开源工具,使用户能够在不同的云平台上测试安全策略和合规性,包括 AWS、Google Cloud Platform(GCP)和 Microsoft Azure。它提供了统一的查询语言和接口来访问云资源,允许用户分析配置并检测潜在的安全漏洞。CloudQuery 集成了各种 CI/CD 流水线,使自动化安全策略和合规性测试变得更加容易。用户还可以根据他们的特定需求和标准自定义查询和规则。你可以在他们的博客文章中阅读更多相关内容:
www.cloudquery.io/how-to-guides/open-source-cspm。
让我们来看一下这个示例 GitHub 流水线,它集成了 terrascan 工具:
name: Terrascan Scan
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Terrascan
run: |
wget https://github.com/accurics/terrascan/releases/latest/download/terrascan_linux_amd64.zip
unzip terrascan_linux_amd64.zip
rm -f terrascan_linux_amd64.zip
sudo mv terrascan /usr/local/bin/
- name: Run Terrascan
run: |
terrascan scan -f ./path/to/infrastructure/code
在这个工作流中,on: [push] 行指定每当有更改推送到仓库时,工作流应该被触发。
jobs 部分包含一个名为 scan 的单个作业。runs-on 键指定该作业应在 Ubuntu 机器上运行。
steps 部分包含三个步骤:
-
它使用
actions/checkout操作从仓库中检出代码。 -
它使用
wget和unzip命令在机器上下载并安装 Terrascan。请注意,这一步假设你在 Linux 机器上运行工作流。 -
它运行 Terrascan 来扫描基础设施代码。你需要将
./path/to/infrastructure/code替换为你实际的基础设施代码路径。
一旦你创建了这个工作流并将其推送到你的 GitHub 仓库,GitHub Actions 会在每次有更改推送到仓库时自动运行该工作流。你可以在工作流日志中查看 Terrascan 扫描的结果。
让我们继续讨论 基础设施单元测试。目前通常有两种选择:直接测试 HCL 代码时使用 Terratest,或者如果你想使用更先进的编程语言来维护你的基础设施即代码(IaC),可以选择 CDKTF/Pulumi。
使用 Terratest 进行测试
Terratest 是一个开源测试框架,用于基础设施代码的测试,包括 Terraform 的 HCL 代码。它于 2017 年由专注于基础设施自动化的公司 Gruntwork 发布,该公司还提供一套预构建的基础设施模块,称为 Gruntwork IaC 库。
Terratest 旨在通过提供一套辅助函数和库来简化基础设施代码的测试,使用户能够编写使用 Go(一种流行的基础设施自动化编程语言)编写的自动化测试。Terratest 不仅可以用于测试 Terraform 代码,还可以用于测试使用其他工具构建的基础设施,如 Ansible、Packer 和 Docker。
Terratest 的一个关键优势是,它允许开发人员在类似生产环境的环境中测试他们的基础设施代码,而无需专门的测试环境。这可以通过使用 Docker 和 Terraform 等工具来创建临时基础设施资源进行测试。
Terratest 还提供了一系列测试类型,包括 单元测试、集成测试 和 端到端测试,允许用户在不同的抽象层次测试他们的基础设施代码。这有助于确保在将代码部署到生产环境之前,进行充分的测试,减少停机或其他问题的风险。
测试我们在上一章中创建的 aws_instance 资源的示例如下所示:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestAwsInstance(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../path/to/terraform/module",
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
instanceID := terraform.Output(t, terraformOptions, "aws_instance_id")
instance := aws.GetEc2Instance(t, "us-west-2", instanceID)
assert.Equal(t, "t3.micro", instance.InstanceType)
assert.Equal(t, "TestInstance", instance.Tags["Name"])
}
在此示例中,我们首先定义一个名为 TestAwsInstance 的测试函数,使用标准的 terraformOptions 对象,该对象指定我们的 Terraform 模块的目录。
然后,我们使用 terraform.InitAndApply 函数来初始化和应用 Terraform 配置,创建 AWS EC2 实例资源。
接下来,我们使用 Terratest AWS 模块 中的 aws.GetEc2Instance 函数,通过实例 ID 获取有关已创建实例的信息。
最后,我们使用 testify 包中的 assert 库编写断言,验证实例的属性,例如其实例类型和标签。如果任何断言失败,测试将失败。
要运行此示例,您需要确保已安装 Terratest 和 AWS Go 模块,并在您的环境中设置了有效的 AWS 凭据。
使用 CDKTF 进行单元测试
AWS Cloud Development Kit for Terraform (CDKTF) 是一个开源框架,允许开发人员使用编程语言(如TypeScript、Python 和 C#)在 Terraform 支持的任何云解决方案中定义基础设施和服务。它通过使用高级面向对象抽象来实现基础设施即代码(IaC)的创建,减少了编写和维护基础设施代码的复杂性。
CDKTF 最初于 2020 年 3 月发布,是 AWS 和 Terraform 背后的公司 HashiCorp 之间的合作。CDKTF 融合了两者的优势:现代编程语言的熟悉性和表现力,以及 Terraform 的声明式、多云能力。
TypeScript 是与 CDKTF 一起使用的最流行的语言,它提供了一个类型安全的开发体验,具有静态类型检查、代码补全和重构等功能。
作为示例,让我们重用 第十二章 中的 Terraform 代码:
resource "aws_instance" "vm_example" {
ami = "ami-830c94e3"
instance_type = "t2.micro"
tags = {
Name = "DevOpsGuideTerraformExample"
}
在 Python 中,CDKTF 的等效代码如下所示:
#!/usr/bin/env python
from constructs import Construct
from cdktf import App, TerraformStack
from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.instance import Instance
from cdktf_cdktf_provider_aws.data_aws_ami import DataAwsAmi, DataAwsAmiFilter
这里,我们导入了所有必需的模块。脚本的第一行表示该脚本的默认解释器应该是系统中可用的 Python 解释器:
class MyStack(TerraformStack):
def __init__(self, scope: Construct, id: str):
super().__init__(scope, id)
AwsProvider(self, "AWS", region="eu-central-1")
在前述代码行中,我们正在配置一个默认提供程序来使用 eu-central-1 区域。接下来让我们看看:
ec2_instance = Instance(
self,
id_="ec2instanceName",
instance_type="t2.micro"
ami="ami-830c94e3",
)
app = App()
MyStack(app, "ec2instance")
app.synth()
以下是一个使用 unittest Python 模块并遵循 Python 中 TDD 通常语法的单元测试示例:
import unittest
from your_cdk_module import MyStack # Replace 'your_cdk_module' with the actual module name containing the MyStack class
class TestMyStack(unittest.TestCase):
def test_ec2_instance(self):
app = App()
stack = MyStack(app, "test-stack")
# Get the EC2 instance resource created in the stack
ec2_instance = stack.node.try_find_child("ec2instance")
# Assert EC2 instance properties
self.assertEqual(ec2_instance.ami, "ubuntuAMI")
self.assertEqual(ec2_instance.instance_type, "t3.micro")
self.assertEqual(ec2_instance.key_name, "admin_key")
self.assertEqual(ec2_instance.subnet_id, "subnet-1234567890")
if __name__ == '__main__':
unittest.main()
如前所述,不幸的是,实例必须在 AWS 中运行,才能测试我们是否拥有通过 CDKTF 获取的期望标签和其他属性。该实例由 setUp() 函数创建,并通过 tearDown() 函数终止。在这里,我们使用的是一个 免费套餐适用 的小型实例,但对于更大的实例,会产生一定的费用。
实验性 Terraform 测试模块
最后但非常有趣的选择是使用测试 Terraform 模块。这个模块允许你编写 Terraform(HCL)代码测试,也是在相同的语言中进行测试。这将可能大大简化测试编写,因为我们之前介绍的现有选项需要使用 Golang 或 CTKTF 来编写测试。
在撰写本文时,该模块被认为是高度实验性的,但值得关注它未来的发展。
该模块的网站可以在 developer.hashicorp.com/terraform/language/modules/testing-experiment 找到。
其他值得一提的集成工具
市面上有许多其他测试工具,随着你阅读本文,更多工具还在开发中。以下是一些值得一提的工具清单,由于篇幅限制,无法对其进行详细描述:
-
Checkov (https://www.checkov.io/):这是一个开源的 IaC 静态分析工具,帮助开发人员和 DevOps 团队在开发生命周期早期识别并修复安全和合规性问题。
-
Super-linter (https://github.com/github/super-linter):这是一个开源的代码检查工具,能够自动检测和标记各种编程语言中的问题,帮助维护一致的代码质量。
-
Trivy (https://github.com/aquasecurity/trivy):这是一个容器镜像漏洞扫描工具,帮助开发人员和 DevOps 团队识别并修复其容器化应用中的漏洞。
-
Kitchen-Terraform (https://github.com/newcontext-oss/kitchen-terraform):此工具是 Test Kitchen 插件集合的一部分,允许系统利用 Test Kitchen 来应用和验证 Terraform 配置,并使用 InSpec 控制。
-
RSpec-Terraform (https://github.com/bsnape/rspec-terraform):此工具为 Terraform 模块提供 RSpec 测试。RSpec 是一个 行为驱动开发(BDD)测试框架,专为 Ruby 设计,使开发人员能够用 领域特定语言(DSL)编写富有表现力且易读的测试。
-
Terraform-Compliance (https://github.com/terraform-compliance/cli):这是一个专为 Terraform 文件设计的 BDD 测试工具。
-
Clarity (https://github.com/xchapter7x/clarity):这是一个声明式的 Terraform 测试框架,专门用于单元测试。
总结来说,我们有很多可用的测试选项。对于一些人来说,这可能会感到不知所措。Terraform 代码的测试仍在开发中,类似于我们提到的其他 IaC 解决方案。在实现 CI 流水线时,最好一开始专注于简单的任务(格式检查、运行terraform plan等),然后在开发过程中逐步增加更多测试。我们意识到这是一个艰巨的任务,但我们相信,投资这些工作将帮助我们以更高的信心进行基础设施更改,并避免无意的停机。
在下一节中,我们将重点介绍各种 CD 解决方案,包括 SaaS 和自托管版本。
部署
terraform apply。这可以通过一个简单的 Bash 脚本自动完成,但难点在于如何将其集成到 CD 工具中,以确保我们不会无意中删除数据,并且可以有信心地执行。假设我们已经完成了出色的集成测试,并且有足够的信心在没有进一步用户交互的情况下执行它,我们可以启用自动运行。
之前,我们展示了使用 Jenkins、GitHub Actions,甚至一个可以嵌入到流程中的 Bash 脚本来自动化这一过程的示例。我们可以成功地使用这些解决方案将更改部署到基础设施中;然而,已经有专门的解决方案来完成这项工作。让我们从 Terraform 背后的公司——HashiCorp 的 SaaS 产品开始,来看看最流行的几种。
HashiCorp Cloud 与 Terraform Cloud
HashiCorp 是一家提供各种基础设施自动化和管理工具的软件公司。HashiCorp 提供的两个最受欢迎的产品是HashiCorp Cloud和Terraform Cloud。
HashiCorp Cloud 是一个基于云的服务,提供一套用于基础设施自动化和管理的工具。它包括 HashiCorp 的流行工具,如 Terraform、Vault、Consul和Nomad。使用 HashiCorp Cloud,用户可以使用与本地相同的工具来创建和管理他们的基础设施。
另一方面,Terraform Cloud 是 HashiCorp 专门推出的一款产品,专注于 IaC 工具 Terraform。Terraform Cloud 为团队提供了一个集中的地方,协作管理基础设施代码、存储配置状态以及自动化基础设施工作流。它提供了多个功能,如工作区管理、版本控制和协作工具,使团队更容易在大规模基础设施项目中协作。
HashiCorp Cloud 与 Terraform Cloud 之间的主要区别在于,前者提供一整套用于基础设施自动化和管理的工具,而后者则专注于 Terraform。
Scalr
Scalr 是一个云管理平台,为云基础架构的自动化和管理提供企业级解决方案。它由 Sebastian Stadil 于 2007 年创立,旨在简化管理多个云环境的流程。
Scalr 是一个多云管理平台。它有两种版本:商业版作为 SaaS 解决方案由母公司托管,开源版则可由您自行部署。它可以运行您的 Terraform 代码,但它还具有更多功能,例如成本分析,用于展示您正在部署的基础设施的云提供商的预估账单。它配备了 Web UI,抽象了在处理 IaC 时需要完成的大部分工作。正如我们之前提到的,它是一个多云解决方案,并且配备了集中式的单点登录(SSO),可让您从一个地方查看和管理所有的云环境。它提供角色、模块注册表等功能。如果您需要的不仅仅是一个集中化的 IaC 工具,那么它是一个很好的选择。
Spacelift
Spacelift 是一个云原生的 IaC 平台,帮助开发团队通过 Terraform、Pulumi 或CloudFormation自动化和管理其基础架构。它还支持使用kubectl和Ansible CaaC自动化 Kubernetes。
该平台提供多种功能,如版本控制、自动化测试和持续交付,使团队能加快基础设施部署周期并降低错误风险。Spacelift 还提供实时监控和警报,帮助轻松识别和解决可能导致停机或影响用户体验的问题。
Spacelift 由一支经验丰富的 DevOps 和基础架构工程师团队于 2020 年创立,他们意识到需要更好的方式来管理 IaC。公司自那时以来迅速发展,吸引了来自医疗保健、金融和电子商务等各行业的客户。
官方网站位于 https://spacelift.com。
Env0
Env0 是一个 SaaS 平台,使团队能够通过 Terraform 自动化其基础架构和应用程序交付工作流程。它由一支经验丰富的 DevOps 工程师团队于 2018 年创立,他们意识到需要一个简化且易于使用的解决方案来管理 IaC。
Env0 提供各种功能和集成,帮助团队管理其 Terraform 环境,包括自动环境配置、与流行的 CI/CD 工具(如 Jenkins 和CircleCI)集成,以及支持 AWS、Azure 和 Google Cloud 等多个云提供商。
作为一家私人公司,Env0 不公开其财务信息。然而,他们从风险投资公司获得了重要资金支持,包括 2020 年由 Boldstart Ventures 和 Grove Ventures 领投的 350 万美元种子轮融资。
Env0 迅速确立了自己作为管理 Terraform 环境和简化 DevOps 工作流的领先 SaaS 提供商,看起来在你的环境中是一个非常有趣的选择。
官方网站可以在www.env0.com/找到。
Atlantis
Atlantis是一个开源项目,旨在通过提供简化的工作流来管理 Terraform 基础设施代码,帮助创建、审查和合并拉取请求。Atlantis 的首次发布是在 2018 年,随后在使用 Terraform 作为 IaC 工具的开发者和 DevOps 团队中获得了广泛的关注。
Atlantis 通过与现有的版本控制系统(如 GitHub 或 GitLab)集成来工作,持续监控包含 Terraform 代码更改的拉取请求。当新的拉取请求被打开时,Atlantis 会自动为这些更改创建一个新的环境,并在拉取请求中发布一个链接,指向该环境。这样,审阅者可以快速且轻松地在实时环境中查看更改并提供反馈。一旦更改被审阅并批准,Atlantis 可以自动合并拉取请求并将更改应用到目标基础设施中。
这个开源工具是我们将深入研究的对象。由于其源代码是免费的,你可以自己下载并将其部署到本地环境或公有云中。让我们在 AWS 中部署 Atlantis,并配置一个简单的基础设施来进行管理。
使用 Atlantis 进行 CI/CD
掌握 CI/CD(包括交付和部署)工具和原则的相关知识后,我们将使用 Git 和开源工具 Atlantis 创建一个 CI/CD 管道。我们将利用它自动测试并部署对 AWS 基础设施的更改,并在此过程中进行基本测试。
将 Atlantis 部署到 AWS
我们将使用 Anton Bobenko 在 GitHub 上的terraform-aws-modules项目中创建的 Terraform 模块。以下是该模块的 Terraform 注册表链接:registry.terraform.io/modules/terraform-aws-modules/atlantis/aws/latest。
你可以通过两种方式使用这个模块。第一种方式是最常见的,将其用于你现有的 Terraform 代码中,将其部署到 AWS。第二种方式是我们在这个演示中将要使用的,即将这个模块作为独立项目来使用。该模块还会在eu-west AWS 区域为你创建一个新的虚拟私有云(VPC),Atlantis 将在 AWS ECS 服务中运行。这将产生一些基础设施费用。
为了做到这一点,我们需要克隆 GitHub 仓库:
git clone git@github.com:terraform-aws-modules/terraform-aws-atlantis.git
Cloning into 'terraform-aws-atlantis'...
Host key fingerprint is SHA256:+Aze234876JhhddE
remote: Enumerating objects: 1401, done.
remote: Counting objects: 100% (110/110), done.
remote: Compressing objects: 100% (101/101), done.
remote: Total 1400 (delta 71), reused 81 (delta 52), pack-reused 1282
Receiving objects: 100% (1401/1401), 433.19 KiB | 1.12 MiB/s, done.
Resolving deltas: 100% (899/899), done.
接下来,我们需要创建一个 Terraform 变量文件。我们在terraform.tfvars.sample文件中有一些模板代码。让我们复制它:
cp terraform.tfvars.sample terraform.tfvars
在继续之前,确保你已经创建了一个 GitHub 仓库来存放所有的 Terraform 代码。我们将在部署 Atlantis 时为这个仓库创建一个 Webhook,但你需要在应用之前将它添加到terraform.tfvars文件中。
让我们来看一下在terraform.tfvars文件中可以更改的变量:
cidr = "10.10.0.0/16"
azs = ["eu-west-1a", "eu-west-1b"]
private_subnets = ["10.10.1.0/24", "10.10.2.0/24"]
public_subnets = ["10.10.11.0/24", "10.10.12.0/24"]
route53_zone_name = "example.com"
ecs_service_assign_public_ip = true
atlantis_repo_allowlist = ["github.com/terraform-aws-modules/*"]
atlantis_github_user = ""
atlantis_github_user_token = ""
tags = {
Name = "atlantis"
}
atlantis_repo_allowlist是你需要更新的第一个变量,它指定了 Atlantis 能够使用的仓库。确保它指向你的仓库。route53_zone_name也应更改为类似的内容,如automation.yourorganisation.tld。请注意,它需要是一个公共域名—GitHub 将使用它来发送 Webhook 到 Atlantis 以触发构建。你需要在 Terraform 代码中创建Route53托管 DNS 区域,或者使用 Web 控制台。
你还需要更新另外两个变量,分别是atlantis_github_user和atlantis_github_user_token。第一个变量不言而喻,第二个变量,你需要访问 GitHub 网站并生成你的个人访问令牌(PAT)。这将允许 Atlantis 访问你想使用的仓库。为此,你需要按照 GitHub 文档页面上的指南操作:docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token。
更新terraform.tfvars文件后,我们准备运行terraform init和terraform plan:
admin@myhome~/aws$ terraform init
Initializing modules...
# output truncated for readability- Installed hashicorp/random v3.4.3 (signed by HashiCorp)
Terraform has been successfully initialized!
Terraform 已经创建了一个名为.terraform.lock.hcl的锁文件,用于记录它所做的提供者选择。将此文件包含在你的版本控制仓库中,这样下次运行terraform init时,Terraform 会默认做出相同的选择。
现在,我们可以运行以下的terraform plan命令:
admin@myhome~/aws$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
<= read (data resources)
Terraform will perform the following actions:
# Output truncated for readability
# aws_cloudwatch_log_group.atlantis will be created
+ resource "aws_cloudwatch_log_group" "atlantis" {
+ arn = (known after apply)
+ id = (known after apply)
+ name = "atlantis"
+ name_prefix = (known after apply)
+ retention_in_days = 7
+ skip_destroy = false
+ tags = {
+ "Name" = "atlantis"
}
+ tags_all = {
+ "Name" = "atlantis"
}
}
# Removed some output for readability
Plan: 49 to add, 0 to change, 0 to destroy.
如果你看到类似的输出,你可以应用它。该模块还会返回关于已创建资源的大量信息,值得关注。
在运行terraform apply之后(这会花费几分钟),你将看到类似下面的输出:
Apply complete! Resources: 49 added, 0 changed, 0 destroyed.
Outputs:
alb_arn = "arn:aws:elasticloadbalancing:eu-central-1:673522028003:loadbalancer/app/atlantis/8e6a5c314c2936bb"
# Output truncated for readabilityatlantis_url = "https://atlantis.atlantis.devopsfury.com"
atlantis_url_events = "https://atlantis.atlantis.devopsfury.com/events"
public_subnet_ids = [
"subnet-08a96bf6a15a65a20",
"subnet-0bb98459f42567bdb",
]
webhook_secret = <sensitive>
如果你一切操作正确,你应该能够访问我们之前创建的atlantis.automation.yourorganisation.tld域名下的 Atlantis 网站。该模块已将所有必要的记录添加到 Route53 区域。
如果到目前为止一切顺利,当你访问atlantis.automation.yourorganisation.tld时,你将看到以下 Atlantis 面板:
图 13.1 – 使用 Terraform 模块成功部署后的 Atlantis 网站
在前述输出中标记为敏感的webhook_secret输出将用于在 GitHub 仓库端设置 Webhook。要查看它,你需要运行以下命令:
admin@myhome:~/aws$ terraform output webhook_secret
"bf3b20b285c91c741eeff34621215ce241cb62594298a4cec44a19ac3c70ad3333cc97d9e8b24c06909003e5a879683e4d07d29efa750c47cdbeef3779b3eaef"
我们也可以通过 Terraform 自动化这一过程,使用与 Atlantis 同一仓库中提供的模块。
这是模块的完整 URL:github.com/terraform-aws-modules/terraform-aws-atlantis/tree/master/examples/github-repository-webhook。
或者,你可以通过访问 GitHub 网站并按照文档创建 Webhook 来手动进行测试:docs.github.com/en/webhooks-and-events/webhooks/creating-webhooks。
记得使用 Terraform 自动生成的机密,该机密位于输出变量中——即webhook_secret。
创建 Webhook 的文档在 Atlantis 文档中也有很好的描述:www.runatlantis.io/docs/configuring-webhooks.xhtml#github-github-enterprise。
你可能会遇到 Atlantis 没有按预期启动,并且在访问网页面板时看到HTTP 500 error错误。为了排查与该服务相关的问题,例如 Atlantis 仍然不可用或对 GitHub webhook 响应错误,你可以进入 AWS 控制台,找到 ECS 服务。在这里,你应该能看到一个名为atlantis的集群。如果点击它,你将看到集群的配置和状态,如下图所示:
图 13.2 – Amazon Elastic Container Service (ECS) Atlantis 集群信息
如果你进入任务标签页(如前述截图所示),并点击任务 ID(例如,8ecf5f9ced3246e5b2bf16d7485e981c),你将看到以下信息:
图 13.3 – Atlantis ECS 集群内任务的详细信息
日志标签页将显示所有最近的事件。
你可以在CloudWatch服务中查看更详细的日志信息,进入日志 | 日志组部分并找到atlantis日志组。在这里,你可以看到包含任务所有日志的日志流。如果已经有多个日志流,你可以通过任务 ID快速跟踪正确的日志流:
图 13.4 – 包含运行 Atlantis 的 ECS 任务日志流的 CloudWatch 日志
如果到目前为止一切正常,我们准备测试 Atlantis 是否能运行terraform plan和terraform apply。让我们回到代码中。
使用 Atlantis 运行 Terraform
要执行 terraform plan,我们需要创建一个新的 main.tf 文件,内容如下:
# Configure the AWS Provider
provider "aws" {
region = var.region
}
上述代码配置了 AWS 提供程序,以使用 region 变量中指定的区域。
这个 Terraform 代码块配置了所需的 Terraform 版本以及 Terraform 状态文件的位置。在这个示例中,我们使用的是本地存储,但在生产环境中,我们应该使用远程状态位置。
terraform {
required_version = ">=1.0"
backend "local" {
path = "tfstate/terraform.local-tfstate"
}
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "terraform-states"
acl = "private"
force_destroy = false
versioning {
enabled = true
}
上述代码块定义了一个 S3 桶,用于存储 Terraform 状态。它是一个启用了版本控制的私有 S3 桶。这是存储状态文件的推荐设置。
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
上述代码还为 S3 桶配置了服务器端加密(SSE)。
resource "aws_dynamodb_table" "dynamodb-terraform-state-lock" {
name = "terraform-state-lock"
hash_key = "LockID"
read_capacity = 1
write_capacity = 1
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "DynamoDB Terraform State Lock Table"
}
}
上述代码块定义了一个 DynamoDB 表,用于 Terraform 状态锁定。variables.tf 文件将只包含一个变量:
variable "region" {
type = string
default = "eu-central-1"
}
在将这些文件添加到 Git 并提交更改后,创建一个新的 GitHub 拉取请求,你将能够请求 Atlantis 为你运行计划并应用更改。如果你运行计划,Atlantis 将锁定你修改的模块,其他人将无法对其应用任何更改,除非你解锁或应用自己的更改。
要为你的新拉取请求触发计划,只需在拉取请求中添加评论,内容为 atlantis plan。稍后,根据模块的大小,你将得到类似下面屏幕截图中的计划输出:
图 13.5 – 在 GitHub 上与 Atlantis 的交互
在撰写本文时,Atlantis 不支持自动应用更改。然而,可以在 CI 级别实现自动化。例如,使用 GitHub 时,可以创建一个 GitHub Action,在测试成功后,添加 atlantis apply 评论,触发 Atlantis 应用更改并返回状态。
到这时,我们可以赋予开发人员修改基础设施的权限,而不需要直接允许他们在本地机器上运行 Terraform。同时,我们也消除了多个用户同时应用更改的可能性,未使用分布式锁机制时,这种做法可能会非常破坏性。此外,使用 Terraform 将变得更加简单,因为不需要在本地机器上安装它,也不需要直接访问我们的 AWS 账户,我们将获得对基础设施更改的更多可视性。
使用 Terraform 构建 CI/CD 仍然任重道远。基础设施即代码(IaC)在测试功能方面仍然落后于其他编程语言,但许多开发人员正在为此努力。我们期待着让每个人都能更轻松地测试基础设施。
总结
本章中,我们探讨了使用 Terraform 进行基础设施即代码(IaC)的好处,并讨论了在 Terraform 工作流中纳入 CI/CD 流程的重要性。我们涵盖了基础设施测试以及各种自动化部署的工具。
在最后一节中,我们解释了如何将 Atlantis(一个用于自动化 Terraform 拉取请求预览的开源工具)部署到 AWS,并配置 GitHub 以触发terraform plan和terraform apply。通过 Atlantis,Terraform 用户可以通过 GitHub 拉取请求协作进行基础设施变更,使得在将变更应用到生产环境之前可以进行审核和批准。通过将 Atlantis 融入 Terraform 工作流,你可以改善协作,减少错误,并实现更快速、更安全的基础设施变更。
在最后一章中,我们将放慢节奏,讨论 DevOps 的误解和反模式,以及如何避免它们。
练习
尝试以下练习来测试你对本章内容的理解:
-
尝试通过遵循
www.runatlantis.io/guide/testing-locally.xhtml上的文档来本地部署 Atlantis。 -
创建一个仓库并为自己配置 Webhook 和 PAT。为你的新仓库运行计划(提示:你可以使用null 资源进行测试,而不是 AWS 资源)。
-
在其中一个 CD 解决方案网站上创建一个帐户,尝试使用该 SaaS 运行计划。通常,公共仓库有免费的计划。
第十四章:避免 DevOps 中的陷阱
本章的重点是 DevOps 中的陷阱和反模式,这些问题可能会妨碍 DevOps 实践的成功实施。我们将强调采用协作文化的重要性,优先考虑持续改进,并讨论各种常见的陷阱,如忽视测试和质量保证(QA)、过度依赖自动化、监控和反馈环节不足、安全和合规措施不充分、以及缺乏可扩展性和灵活性。
我们还将强调适当的文档和知识共享的重要性,并讨论克服变革抵抗的策略。通过突出这些常见的陷阱和反模式,本章旨在为组织提供指导,帮助它们成功实施 DevOps 实践并避免常见的错误。这些也是组织在技术部分之外最常遇到的挑战。
本章将涵盖以下主题:
-
自动化过多或过少
-
不理解技术
-
没有采用协作文化
-
忽视测试和 QA
-
监控和反馈环节不足
-
安全和合规措施不充分
-
缺乏可扩展性和灵活性
-
缺乏适当的文档和知识共享
-
克服对变革的抵触
技术要求
本章没有技术要求。它更像是一种讨论,并没有提供需要在系统上执行的具体指令。
自动化过多或过少
自动化是 DevOps 的核心原则。说实话,自动化是使我们的工作更轻松、更高效和更有趣的最佳方式。
但是,组织有时可能会过度依赖自动化,从而导致缺乏人类的监督和责任。如果你自动化了太多的内容,你就会错过一些可以通过人工审核发现的错误,尤其是在流程中没有嵌入这些检查时。这也是为什么我们有同行评审流程来确保我们不会错过任何测试或任何集成测试工具没能发现的问题。这也是为什么许多组织更愿意在terraform apply过程真正部署之前手动签署它。
另一方面,如果你什么都不自动化,你就会暴露于意外错误中,因为我们人类在处理枯燥且可重复的任务时并不擅长。而这正是重点:识别可重复的任务 以进行自动化。
为了识别可以自动化的任务,我们建议先关注那些易于实现的低挂果任务。考虑到这一点,让我们识别出最容易自动化的任务,以确保成功的自动化策略。
请考虑以下清单,它按从最简单到最困难的策略排列,帮助你绕过自动化问题:
-
重复性任务
-
耗时的任务
-
手动且容易出错的任务
-
集成版本控制的任务
-
具有可重复模式的任务
-
拥有明确定义 API 或接口的任务
-
拥有明确且定义清晰需求的任务
现在让我们来看看这些策略。
重复性任务
寻找那些重复性强并且可以通过一组一致步骤执行的任务。这些任务是自动化的理想候选,因为它们可以节省时间并减少人为错误的风险。这些任务包括使用 Terraform 配置和管理 AWS 资源、备份作业、创建和管理 GitHub 仓库,或者通过GitHub Actions设置构建和部署流水线,这些任务都可以自动化,从而简化 DevOps 工作流程。
耗时的任务
寻找那些耗时且能从自动化中受益的任务。这些任务包括长时间运行的任务,如数据同步、编译、构建 Docker 镜像以及安全审计,这些都可以通过许多工具完成,包括商业的 SaaS 模式工具或开源工具(例如,Prowler就是一个例子)。
手动且容易出错的任务
确定在手动执行时容易出错的任务。这些任务通常涉及多个步骤或配置,可能既繁琐又容易出错。使用 Python 脚本或基础设施即代码(IaC)工具,如 Terraform,来自动化这些任务可以帮助减少人为错误,并确保环境之间的一致性。用清晰的代码自动化这些任务还具有文档化的好处。你常常听说代码即文档,尤其在这种情况下,这句话是真的。
与版本控制系统集成的任务
确定那些可以与版本控制系统(如 GitHub)集成的任务。例如,GitHub Actions 提供了一个强大的自动化框架,可以根据 GitHub 仓库中的事件(如代码推送、拉取请求或问题更新)自动触发工作流。这使得你能够自动化诸如构建和部署应用程序、运行测试或创建文档等任务,作为 DevOps 工作流程的一部分。
具有重复模式的任务
确定那些遵循可重复模式或可以模板化的任务。这些任务可以通过 Python 脚本、Terraform 模块或 GitHub Actions 模板来自动化。例如,创建多个环境(如开发、测试、生产)中的相似 AWS 资源、管理具有相似设置的多个 GitHub 仓库,或者将相同的应用部署到多个 AWS 账户等任务,可以通过模板或脚本进行自动化,从而减少重复性工作并提高效率。
拥有明确 API 或接口的任务
确定那些有良好文档的 API 或接口的任务。这些任务可以通过 Python 库、Terraform 提供程序、AWS SDK 或 GitHub API 轻松自动化。例如,AWS 为多种编程语言提供了全面的 SDK,包括 Python,这使得自动化诸如管理 AWS 资源、配置 AWS 服务或监控 AWS 资源等任务变得容易。
具有明确且明确定义要求的任务
寻找那些有明确且定义良好的需求、输入和预期输出的任务。这些任务更容易自动化,因为它们可以在脚本、模板或配置中被精确定义。例如,使用 Terraform 配置 AWS 资源、设置 AWS CloudFormation 堆栈或配置 GitHub 仓库设置等任务,可以通过声明性 IaC 模板或配置文件来实现自动化。
通过考虑这些标准,你可以识别出在组织中哪些任务最容易实现自动化。请记住,这通常是一个过程,可能需要几个月甚至几年时间。作为经验法则,尽量创建涉及许多小的(原子化)步骤的解决方案,以便构建一个更复杂的系统,而不是一开始就构建复杂的解决方案。例如,部署过程可以拆分为更小的步骤,如设置环境、构建、测试、上传工件以及创建更新应用程序版本的清单,最终实现服务器端的部署。
自动化常见任务将使你的工作更轻松、更令人满意,但如果不了解背后的技术以及你正在使用的技术如何运作,可能会导致一些无法预见的问题。
不理解技术
你不需要知道电视如何工作就能使用它。但了解其背后的原理、输入接口、输出是什么等内容是必要的。同样,面对任何其他技术,你不需要 100% 理解它是如何运作的,但你需要了解其核心用例是什么,它的目的是什么。
作为一名 DevOps 专业人士,深入理解支持现代软件开发和运维的底层技术至关重要。从数据库到消息队列,从通知到块存储和对象存储,每一项技术都在构建和维护可靠、可扩展的软件系统中发挥着至关重要的作用。然而,DevOps 中最常见的陷阱之一就是没有完全掌握这些常见任务背后的技术。
为什么理解技术如此重要? 答案很简单——DevOps 不仅仅是通过工具和自动化来简化软件开发和部署过程。它是关于理解这些工具如何工作,它们做了什么,以及它们如何相互作用。没有这种理解,DevOps 实践可能变得肤浅和低效,导致次优结果和更高的故障和停机风险。记住:DevOps 是一种工作方式。工具只是工具——帮助你日常工作的工具。理解你的工具将使你能够高效、有效地使用它们,并根据你的需求进行调整。
举个例子,假设你被指派为一个新应用程序设置一个高可用和高性能的数据库。如果没有对数据库技术的深入理解,团队可能仅依赖默认配置或沿用过时的做法,导致性能差、数据丢失,甚至系统崩溃。另一方面,一个对数据库原理有深入理解的团队则能做出明智的决策,关于数据建模、索引、缓存、复制等关键方面,进而实现一个强大且可扩展的数据库架构。
同样,理解消息队列、通知、块存储和对象存储等技术,对于设计和实现可靠高效的通信模式、数据处理管道和存储策略至关重要。这让你能够优化系统性能、确保数据完整性,并为未来的增长做好规划。
另一方面,你不一定需要了解实现的细节。对于数据库,你不必担心其源代码。
那么,如何能快速学习新的和未知的概念呢?以下是一些你可以遵循的建议:
-
保持好奇心和积极性
-
从基础开始
-
动手学习
-
协作与分享知识
-
保持更新
让我们详细看看这些建议。
保持好奇心和积极性
拥抱成长型思维,并主动寻找学习的机会。不要等到问题出现才去深入了解新技术。保持好奇心,探索文档、教程和在线资源,主动在安全可控的环境中尝试不同的工具和技术。我们无法强调这一点——实验是非常重要的。作为 DevOps,说“总是这么做”的说法是最糟糕的。没有沙箱环境和测试新配置、新工作流、新工具,你无法改进基础设施和管道。
从基础开始
不要被复杂的概念吓倒。从基础开始,并逐步建立你的理解。熟悉你想学习的技术的基本原理、术语和概念。一旦你有了扎实的基础,你就可以逐渐深入了解更高级的话题。
动手学习
理论很重要,但动手实践是无价的。建立一个沙箱环境,尝试不同的配置,构建小项目或原型,将所学应用到实践中。通过实践学习将帮助你获得实际技能,并加深对技术的理解。
协作与分享知识
DevOps 是一个协作领域,从同事那里学习是非常宝贵的。与团队互动,参与在线社区,参加聚会或会议,并与他人分享你的知识。教授他人是加深自己理解和从不同角度学习的有效方法。
保持更新
技术不断发展,保持对最新趋势、最佳实践和领域更新的了解至关重要。你可以关注行业博客,订阅时事通讯,参与相关论坛或社区,及时掌握最新动态。核心技术通常保持不变,但使用案例、使用方式以及与技术的互动始终在变化。你知道吗,你可以使用 RESTful API (postgrest.org/en/stable/) 使 PostgreSQL 数据库可用?或者,使用 Multicorn 扩展(multicorn.org/),你可以通过一个端点查询(甚至连接多个数据源,如 Twitter)?
我们喜欢把技术看作是创新和提高团队生产力的最终游乐场。如果它不有趣,为什么要做呢?说到这一点,想想与一个只处理自己任务碎片的孤岛团队合作的情景,这肯定不会有趣。在 DevOps 中,协作是基本原则之一,协作也是你组织文化的一部分。
未能采纳协作文化
DevOps 完全是关于协作并打破团队之间的孤岛。然而,许多组织在采纳协作文化方面存在困难,导致沟通不畅、延误,最终项目失败。讨论 DevOps 中协作的重要性,以及缺乏协作如何导致项目偏离轨道,显然是另一个章节的好主题。
孤岛效应指的是各个团队或部门之间缺乏有效沟通与合作,导致协调不畅,从而影响整体的生产力和效率。组织未能在 DevOps 中建立协作文化的几个可能原因包括:缺乏领导力、从一开始就存在的孤岛结构、缺乏信任以及沟通不足。我们来逐一看看这些原因,并尝试找到一个好的解决方案。
缺乏领导力
一个常见的挑战是领导层没有优先考虑或积极推动协作文化。这可能导致团队只专注于各自的任务,而忽略更广泛的组织目标。为了解决这一问题,必须确保领导层支持 DevOps 实践,包括促进协作文化的建立。领导者应该通过积极推动协作、确立共同目标,并为跨团队合作提供必要的资源和支持来定下基调。
优秀的领导者稀缺,因此在组织内识别潜在领导者并支持他们的成长与发展作为个人及领导者是一个好主意。仅仅提拔一个之前只是贡献者的人,会导致他们失败,因为他们还没有获得执行这项工作的必要工具。
孤岛式组织结构
拥有层级化和信息孤岛结构的组织可能会阻碍协作。团队可能会在孤立的信息孤岛中运作,拥有自己的目标、流程和工具,导致跨团队的可见性和协调性不足。为了克服这一点,组织应重新结构,以促进拥有端到端应用或服务的跨功能团队。创建由不同部门(如开发、运营和质量保证)代表组成的多学科团队可以促进协作,实现更好的沟通和协调。
另一个非常有效的策略是建立协作文化。这涉及促进重视开放沟通、透明度和团队合作的思维方式。鼓励跨功能团队共同工作、分享信息并在项目上进行协作。认可和奖励协作,并为跨不同团队和部门的知识分享和学习创建论坛。
您还可以创建共享的目标和度量标准。让团队定义与整体业务目标一致,并要求跨团队协作的共享目标和度量标准。这将鼓励他们共同努力实现共同的结果,并帮助他们看到超越个人信息孤岛的更大图景。定期在跨功能会议上审查向这些共享目标和度量标准的进展,以促进责任和对齐。
此外,领导在打破信息孤岛中扮演着至关重要的角色。领导者应该以身作则,积极促进团队之间和部门之间的协作、沟通和对齐。这包括为协作设定明确的期望,认可和奖励协作行为,以及提供支持和资源以促进跨功能的协作。
缺乏信任和沟通
缺乏信任和有效沟通,协作可能会受到影响。团队可能因为害怕批评或竞争而不愿分享信息或想法,从而导致信息孤岛的产生。建立信任和开放沟通文化至关重要。这可以通过定期团队会议、跨团队研讨会以及促进团队成员在不受评判恐惧的环境中分享其观点和想法来实现。鼓励开放和透明的沟通渠道,如聊天平台或协作文档工具,也可以促进沟通和协作。
在团队和团队成员之间建立信任对于有效的协作和沟通至关重要。信任是建立健康关系和成功团队工作的基础。在其他书籍中学习的不同策略中,最有效的是:建立明确的期望、促进开放式沟通、提升透明度、分享知识和建立个人联系。
让我们来详细分析一下:
-
明确定义每个团队和团队成员的角色、责任和期望:这有助于避免误解,促进责任感。确保期望是现实的、可实现的,并与组织的整体目标一致。根据需要定期审查和更新期望。
-
开放和包容的沟通:创造一个安全和包容的环境,让团队成员能够舒适地表达他们的想法、意见和担忧,而不必担心被评判或报复。鼓励积极倾听并尊重多样化的观点。避免在问题出现时归咎或指责,专注于协作解决问题。
-
培养透明文化:在团队和团队成员之间公开、一致地共享信息,包括与项目、流程和目标相关的更新、进展和挑战。透明的沟通通过确保每个人都能访问相同的信息并保持一致,从而建立信任。
-
协作思维:为了鼓励协作和知识共享,你需要培养一种协作思维,促使团队及其成员积极协作并相互分享知识。鼓励跨职能协作、结对编程和跨培训机会。创造工作之外的空间,赞助活动,并鼓励团队成员分享他们的知识和经验。认可并奖励协作行为,以强化其重要性。
-
主动解决冲突:冲突在任何团队或组织中都是不可避免的,但如果不及时处理,冲突会削弱信任。鼓励团队成员以建设性和及时的方式解决冲突。提供冲突解决工具和资源,例如调解或主持讨论,帮助团队解决冲突并重建信任。
-
领导者的角色:领导者在建立信任方面发挥着至关重要的作用。领导者应以身作则,展示开放和公正的沟通方式,积极倾听团队成员的意见,并通过自己的行动和决策展现可信度。领导者还应鼓励建立信任的行为,并对在团队内部及跨团队之间建立信任负责。
-
定期提供反馈和认可:公开表扬他人的努力和成功,并在私下给予建设性反馈以帮助他们改进。这有助于建立积极的反馈循环,促进信任,并鼓励开放的沟通。
还有许多策略可以用来促进团队成员之间的信任,从而改善沟通。
还有一个常见的陷阱,最终会在你的组织中形成壁垒。它发生在你忽视组织的文化方面,单纯专注于工具时。
以工具为中心的方式
组织可能过于侧重于实施 DevOps 工具,而忽视了其背后的文化因素。虽然工具很重要,但它们不能替代协作文化。采用以工具为中心的方法可能导致团队孤立工作,仅依赖自动化流程,这会妨碍有效的协作。为了克服这一点,组织应首先优先构建协作文化,然后选择与其文化契合并促进协作的工具。提供培训和支持,确保团队能够熟练使用所选工具来促进协作,是至关重要的。
为了纠正这一点,你可以使用已经讨论过的策略,并额外提供共享的沟通渠道,鼓励并且自己组织跨团队的会议和活动,提供培训和资源(特别是时间)。
一个最终的优秀纠正方法是促进跨团队的角色和责任。定义并鼓励那些促进协作的跨团队角色和责任。这可以包括像联络员或大使这样的角色,帮助促进团队之间的沟通和协调。这些角色可以帮助弥合团队之间的差距,通过作为信息共享的渠道来推动协作。
软件质量保证(QA)也常常被忽视。与文化一样,质量需要在组织层面和团队层面有意识地发展和鼓励。
最好通过一个例子来解释前述方法,而在 Linux 世界中有一个完美的例子:Linux 内核项目。
它在 1991 年 8 月 25 日以一篇帖子在新闻组中开始,帖子内容如下:
“我正在为 386(486) AT 克隆机做一个(免费的)操作系统(只是一个爱好,不会像 GNU 那样大规模和专业)。这个项目自四月以来一直在酝酿,现在开始准备好了。我希望能收到关于人们在 minix 中喜欢/不喜欢的反馈,因为我的操作系统在某些方面与它相似(例如文件系统的物理布局(出于实际原因)等)。目前我已经移植了 bash(1.08)和 gcc(1.40),目前看起来一切正常。这意味着我将在几个月内做出一些实际的成果[…] 是的——它完全没有使用任何 minix 代码,并且拥有一个多线程文件系统。它并不是可移植的[原文如此](使用 386 任务切换等),而且它可能永远只支持 AT 硬盘,因为我只有这一种硬盘 😦。”
正如你所见,从一开始,Linux 的创始人林纳斯·托瓦兹就邀请其他爱好者加入他的这个小项目,并帮助开发它。这种合作精神从第一天就展现出来。每个人都可以加入该项目,他们的贡献会在技术层面上进行评估。沟通的方式是一个开放的公共邮件列表,名字非常贴切——Linux 内核邮件列表(LKML),在这里讨论着项目相关的路线图、补丁、新点子以及所有内容。每个人都可以阅读列表的档案,加入邮件列表和讨论。
虽然讨论几乎对所有人开放,补丁(或拉取请求)可以由任何人提交(尽管是否接受是另外一回事,因为项目必须遵循代码质量、法律和许可要求,我们将在此不做讨论),但仍然存在一种领导层等级,尽管它相当扁平。内核子系统有维护者,这些人负责决定新代码是否会被接受或拒绝。最终的代码合并到内核是由林纳斯·托瓦兹完成的,他在代码接受过程中拥有最终发言权,尽管他通常依赖子系统的维护者来做出决定。
上述结构本质上使 Linux 内核项目免于孤立的管理结构,因为管理层级不多。所有的知识都是公开和自由获取的;项目管理链中的每个人都可以轻松联系。
Linux 内核的源代码公开存放在一个 Git 仓库中。任何人都可以克隆和修改内核,只要他们不违反 Linux 内核发布所使用的许可协议。
沟通和信任是项目采用开放沟通模型和开源代码库的直接结果。没有“幕后交易”的沟通;决策是基于技术的,因此可以信任开发人员和领导。
忽视测试和质量保证
测试和质量保证是任何 DevOps 工作流程中的关键组成部分,然而许多组织未能优先考虑这些因素,导致了软件的缺陷、用户的不满以及收入的损失。在 DevOps 的世界里,软件开发与运维紧密结合,测试和质量保证是开发过程中的关键组成部分。忽视这些方面可能会导致各种问题,这些问题会对软件开发项目产生严重后果。让我们探索忽视测试和质量保证可能带来的一些陷阱,并提出解决方案:
-
增加了软件缺陷
-
部署失败
-
安全漏洞
-
缺乏文档
-
测试覆盖不足
-
缺乏持续改进
让我们详细检查这些陷阱。
增加了软件缺陷
没有适当的测试和质量保证,软件缺陷可能——而且通常会——被忽视,导致质量较差的软件进入生产环境。这可能导致客户投诉增多、用户满意度下降以及收入损失。
实施全面的测试过程至关重要,包括单元测试、集成测试和端到端(E2E)测试,以在开发生命周期的不同阶段识别和修复缺陷。从基本的代码格式检查(linting)、静态代码分析开始,逐步在工作流中添加更多测试。除非你愿意为你的应用程序编写所有测试,否则与开发人员合作是非常必要的。
部署失败
如果没有彻底的测试和质量保证,软件部署可能会失败,导致系统停机并干扰业务运营。这可能导致财务损失、声誉损害和客户流失。为了避免部署失败,至关重要的是建立自动化测试和部署管道,在将代码发布到生产环境之前进行严格的测试和质量保证检查。这有助于及早发现问题,并确保只有稳定可靠的软件被部署。
安全漏洞
忽视质量保证和测试可能使软件面临安全威胁,例如代码注入、跨站脚本攻击(XSS)以及其他类型的攻击。这可能导致数据泄露、合规性违规和法律责任。为了解决这个问题,安全测试应作为测试和质量保证过程的一个核心部分。包括漏洞评估、渗透测试和其他安全测试技术,以识别和修复软件中的安全缺陷。
缺乏文档
适当的文档对于维护软件质量、促进故障排除、维护以及未来开发至关重要。忽视质量保证(QA)和测试可能导致文档不完整或过时,从而使理解和维护软件变得困难。为了缓解这一问题,文档应该被视为测试和质量保证过程中的一个重要交付物。
文档应定期更新,以反映开发和测试过程中所做的更改,并且应易于开发和运维团队访问。为了实现这一点,文档应尽可能靠近代码,以便在更新代码时能够轻松更新文档。技术文档(例如类、代码接口等)应当实现自动化,并且对所有相关人员开放。
测试覆盖不充分
如果没有适当的测试和 QA,可能会出现测试覆盖范围的空白,导致未经过测试或测试不充分的代码。这可能导致预料之外的问题和缺陷进入生产环境。为了解决这个问题,必须建立明确的测试目标,定义测试覆盖标准,并使用代码覆盖率分析工具,确保所有关键代码路径都得到彻底测试。
缺乏持续改进
忽视测试和质量保证(QA)可能导致一种自满的文化,其中质量未被优先考虑。这可能导致软件开发过程中缺乏持续改进,从而随着时间的推移,软件质量下降。为了解决这个问题,必须建立一种持续改进的文化,其中测试和 QA 的反馈被用来识别和解决过程中的空白,改进测试实践,并提升整体软件质量。
为了避免这些陷阱,必须实施全面和自动化的测试流程,优先进行安全测试,保持文档的最新性,确保充足的测试覆盖率,并培养持续改进的文化。通过解决这些挑战,组织可以确保交付符合客户期望的高质量软件,推动业务成功,并带来愉快的团队——我们可不能忘了这一点。
虽然质量保证(QA)会尽力捕捉进入生产系统的任何错误和缺陷,但没有什么能比得上良好的监控和告警。
差的监控和反馈回路
监控和反馈回路对于识别问题并改进 DevOps 工作流程至关重要,但许多组织未能实施有效的监控和反馈机制。
在 DevOps 环境中,反馈回路是指软件开发和运维生命周期中不同阶段之间信息的持续交换。它涉及数据收集、分析,并提供推动开发和运维过程改进的见解。
反馈回路在帮助团队早期识别和纠正软件交付生命周期中的问题方面发挥着至关重要的作用,这将导致更快的开发周期、提高的质量以及增加的值班人员满意度,因为他们不会在夜间被叫醒。
良好监控的特点是能够提供及时、准确且相关的信息,关于系统的健康状况、性能和行为。良好监控的关键特点包括多个方面,如下所示:
-
实时
-
全面
-
可扩展
-
可操作
-
持续改进
让我们来拆解一下。
实时
良好的监控提供了对系统状态的实时可见性,使团队能够在问题升级为关键问题之前快速检测和解决问题。实时监控有助于识别异常、趋势和模式,这些可能表明潜在的问题或瓶颈,从而使得主动排查和解决问题成为可能。
综合性
良好的监控涵盖系统的所有关键组件,包括基础设施、应用程序、服务和依赖关系。它提供了整个系统的全面视图,帮助团队理解不同组件之间的关系和依赖性,以及它们对系统性能和可用性的影响。
此外,监控需要能够仅提供相关信息,而不仅仅是枯燥的警报数据。例如,如果服务器宕机,良好的监控不会发送关于 CPU 或 RAM 使用率、服务宕机等的警报。核心问题是服务器无法响应——将不相关的信息发送给值班团队会导致响应时间变慢。
可扩展的
良好的监控能够处理大量数据,并且能够横向扩展以适应系统不断增长的需求。它可以从多个来源收集和处理数据,并将其与不同的工具和技术进行集成,提供系统健康状况的统一和综合视图。
可操作的
良好的监控提供可操作的见解,使团队能够做出明智的决策并采取及时的行动。它包括丰富的可视化、报告和分析功能,帮助团队识别趋势、关联和异常,并采取适当的措施来优化系统性能和可用性。
持续改进
除了我们在本节中讨论的内容,还需要补充的是,从良好监控到卓越监控的转变需要持续审查当前状态并实施改进。
如果您的组织变化迅速,应每月进行此审查;如果您已经建立了良好的监控系统并且它正常运作,则每季度进行一次。此审查包括以下内容:
-
审查上一个周期内最常触发的警报,并将警报与之前的周期进行比较
-
审查新增的监控指标,以检查其相关性
-
审查长时间没有触发的警报(3-4 个审查周期)
有了这些数据,你可以决定哪些警报增加了噪音,确保新的指标确实是你期望被监控的内容,并审查那些没有触发的警报,这将让你意识到可能根本不需要监控的指标。
尽管在讨论监控时,我们通常关注响应时间或内存消耗等方面,但它不仅仅是这些。你可以追踪软件代码中类之间的交互、系统和数据库之间的延迟,或者可以衡量安全性。如果你不监控你的安全态势,会发生什么?系统中会出现漏洞,而你甚至不知道它们的存在。让我们来看看安全和合规性措施。
安全性和合规性措施不足
安全性和合规性是任何团队的重要关注点,但许多组织未能充分解决这些问题。在 DevOps 的世界中,安全性是一个至关重要的方面,必须将其集成到软件开发生命周期的每一个环节。然而,许多组织仍在安全性和合规性措施上存在不足,这可能导致严重后果,如数据泄露、监管罚款和声誉损害。本章将探讨 DevOps 中关于安全措施的常见误解和陷阱,并讨论组织应努力实现的良好安全措施的特征。
我们将讨论以下方面:
-
什么是安全措施?
-
良好安全措施的特征
什么是安全措施?
安全措施指的是用于保护软件系统、应用程序和数据免受未经授权访问、泄露或其他安全威胁的实践、流程和工具。在 DevOps 的背景下,安全措施贯穿整个软件开发流程,从代码创建和测试到部署和操作。
DevOps 中的常见安全措施包括以下内容:
-
身份验证和授权确保只有授权用户可以访问系统,并且他们拥有执行任务所需的适当权限。
-
加密敏感数据,防止未经授权的用户拦截或访问
-
定期扫描软件组件中的漏洞,并应用补丁或更新以修复它们
-
监控和记录系统内的活动,以便发现和调查安全事件
-
网络安全,包括实施防火墙、入侵检测系统(IDS)和/或入侵防御系统(IPS),以及虚拟私人网络(VPN)以保护网络免受外部威胁
-
审查代码中的安全漏洞,并使用静态分析工具识别潜在的弱点,防止泄露密码和访问令牌等敏感信息
在实施了一些安全措施后,我们需要确保这些措施的质量尽可能高,同时满足组织需要遵守的法律合规性要求。让我们来看一下安全措施的特征。
良好安全措施的特征
DevOps 中有效的安全措施应具备某些特征,以确保它们能为软件系统和数据提供足够的保护。
良好的安全措施是主动的而非被动的,这意味着它们的设计目的是防止安全漏洞,而不仅仅是在事后发现并减轻这些漏洞。主动的安全措施可能包括定期的漏洞评估、代码审查和自动化测试,以在漏洞成为关键问题之前发现并修复安全问题。
安全措施还应涵盖软件开发生命周期的各个方面,从代码创建和测试到部署和操作。这包括保护开发环境、代码库、构建和部署过程,以及部署和操作软件的生产环境。
可扩展性是良好安全措施的另一个特征,意味着它们可以应用于不同类型的软件应用程序、环境和技术。它们应该足够灵活,以适应组织不断变化的需求和不断发展的威胁格局。
利用自动化实现快速且一致的安全检查和响应是谈论 DevOps 实践时始终重复的主题。自动化可以帮助及时高效地识别安全漏洞、应用补丁并强制执行安全策略,从而减少人为错误的风险。
许多组织需要遵守行业法规、标准和最佳实践。遵守相关法规,如通用数据保护条例(GDPR)、健康保险流通与责任法案(HIPAA)或支付卡行业数据安全标准(PCI DSS),对于避免与不合规相关的法律和财务风险至关重要。即使你的组织未受这些标准的监管,选择最合适的标准并在合适时遵循它,仍然是一个好主意。这将使你的安全级别高于不遵循任何标准的情况。
最后,所有安全措施都需要不断更新和改进,以应对新出现的安全威胁和技术。威胁格局在不断变化,安全措施必须具有敏捷性和适应性,才能有效防范新的漏洞和攻击。
一如既往,选择最简单的任务开始,建立团队中的良好安全措施。要经常回顾并逐步改进。
缺乏可扩展性和灵活性
许多组织未能设计出可扩展和灵活的 DevOps 工作流,导致项目在规模或复杂性增长时出现问题,尽管这两者是 DevOps 至关重要的方面,因为它们使组织能够响应不断变化的业务需求并高效处理日益增加的工作负载。然而,你可能会忽视这些因素,从而导致严重的误解和陷阱。让我们深入探讨 DevOps 中可扩展性和灵活性的重要性,并探讨一些常见的误解和陷阱,例如以下几点:
-
DevOps 只适用于小型团队或项目
-
无法扩展基础设施
-
灵活性妥协了稳定性
-
发布管理中缺乏灵活性
让我们详细看看这些误解和陷阱。
DevOps 只适用于小型团队或项目
一个常见的误解是 DevOps 只适用于小团队或小项目。一些组织认为,大团队或大项目不需要 DevOps 实践,因为他们认为传统的开发和运维方式可以处理规模问题。
实际上,DevOps 不仅限于团队或项目的规模。它是一套可以应用于各种规模组织的原则和实践。事实上,随着团队和项目的发展,DevOps 的需求变得更加关键,以确保顺畅的协作、更快速的交付和高效的运营。
无法扩展基础设施
很容易忽视基础设施的可扩展性,这可能导致系统故障、性能问题和未计划的停机时间。
低估 DevOps 过程对基础设施的需求将导致未来出现许多问题,从糟糕的用户体验到错失组织盈利机会。例如,在资源有限或容量不足的环境中部署应用程序,当工作负载增加时,可能会导致性能问题和系统故障。同样,未规划未来的增长或业务需求变化可能会导致需要进行昂贵且耗时的基础设施升级或迁移。这对本地部署和云基础设施都适用。此外,由于全球大流行导致的电子设备短缺,AWS 和其他云服务商有时可能缺少硬件资源。当你尝试为基础设施添加更多资源时,这可能会影响你的组织。对于本地部署,你可以更紧密地控制硬件资源。
为了避免这个陷阱,团队应该仔细评估其应用程序和基础设施的可扩展性需求,规划未来的增长,并确保基础设施被设计和配置为有效应对增加的工作负载。这可能涉及采用 IaC、自动化配置和水平扩展等实践,这些可以使团队迅速且轻松地扩展其基础设施,以应对变化的需求。对于云部署,你可能需要预先支付一些容量来保留资源。
灵活性妥协稳定性
另一个关于 DevOps 的误解是灵活性妥协稳定性。一些组织担心,在开发和运维过程中引入灵活性可能导致不稳定的环境,从而增加风险和漏洞。因此,他们可能会采取僵化的 DevOps 方法,强调稳定性而非灵活性。
然而,这种误解可能会妨碍 DevOps 旨在实现的敏捷性和创新性。灵活性在 DevOps 中至关重要,因为它使团队能够快速响应不断变化的业务需求,尝试新想法,并对应用程序和基础设施进行迭代。事实上,DevOps 实践如持续集成和持续部署(CI/CD)以及自动化测试,旨在确保在部署到生产环境之前,所有的变更都经过充分的测试和验证,从而在确保稳定性的同时,能够实现灵活性。
发布管理缺乏灵活性
一个常见的陷阱是发布管理中的灵活性不足。发布管理涉及将变更部署到生产环境的过程,如果你采用一种僵化的方式来处理发布,可能会导致延迟、复杂性增加以及风险增加。
例如,遵循固定的发布计划或僵化的变更管理流程可能会妨碍快速响应业务需求或客户反馈的能力,导致错失机会或客户满意度下降。同样,不允许进行实验或快速回滚选项会限制对变更进行迭代的能力,并且无法迅速解决生产环境中可能出现的问题。
为了避免这种情况,你应该专注于建立灵活且敏捷的发布管理流程。这可能涉及实施一些实践,如功能开关、暗启动、金丝雀部署和蓝绿部署,它们可以实现渐进式和可控的变更推出,并在出现问题时提供回滚选项。此外,采用自动化发布管道、版本控制和监控可以帮助团队获得发布过程的可见性和控制。
构建灵活且可扩展的系统并非易事。更重要的是,你还需要考虑来自业务角度的变更,这些变更会影响你当前的流程。如果你的流程难以变更,或者你无法根据高流量对系统进行扩展,你将分别遇到延迟和系统不稳定的问题。
为了理解并识别当前流程中的薄弱环节,你需要合适的文档和可视化工具,如网络图或工作流图。在下一节中,我们将讨论你所建立的这些流程部分。
缺乏适当的文档和知识共享
文档和知识共享对于维护一致性并避免 DevOps 工作流中的错误至关重要,然而许多组织未能优先考虑这些活动。
在任何软件开发项目中,文档都起着至关重要的作用,确保项目的成功。它作为参考指南,提供关于项目架构、设计和实现细节的见解,并帮助维护和故障排除软件。DevOps 中的一个常见问题是缺乏适当且最新的文档,这可能导致混乱、延误和错误。为了解决这个问题,必须理解软件项目中不同类型的文档以及它们的目标受众。这里列出了这些文档:
-
技术文档
-
API 文档
-
用户文档
-
流程文档
-
运维文档
-
发布说明和变更日志
让我们详细探讨一下它们。
技术文档
技术文档是面向开发人员、运维团队和其他参与软件开发与部署过程的技术相关人员的。它包括与系统架构、代码库、API、数据库模式、部署脚本、配置文件以及其他技术细节相关的文档。技术文档有助于理解软件的内部运作,从而更容易进行维护、故障排除和系统增强。
一些文档内容,如代码库和 API 的文档,可以通过专业软件实现自动化。你可以确保你的开发团队能够编写自文档代码,并且还可以使用Doxygen (www.doxygen.nl/)、Swimm (swimm.io/) 或 Redoc (github.com/Redocly/redoc)等软件自动化生成代码文档。要记录你的 API 文档,你可以使用基于 OpenAPI 的项目,比如Swagger (github.com/swagger-api/swagger-ui)。
API 文档
API 文档专注于记录软件暴露的 API,这些 API 用于与其他系统集成或构建扩展或插件。它包括与 API 端点、请求和响应格式、身份验证和授权机制、错误处理及其他与 API 相关的细节的文档。API 文档帮助开发者理解如何通过编程方式与软件交互,从而实现与其他系统的无缝集成。
用户文档
用户文档面向软件的最终用户,包括客户、客户以及其他与软件互动的相关人员。它包括用户手册、指南、教程以及其他解释如何有效安装、配置和使用软件的资源。用户文档应使用简明清晰的语言编写,避免技术性语言,涵盖软件的所有必要功能和特点。
确保最终用户能够轻松地通过文档联系到你的支持团队。
流程文档
流程文档侧重于记录软件开发和部署生命周期中遵循的工作流程、过程和程序。它包括与编码标准、版本控制、构建和部署过程、测试方法、发布管理以及其他开发实践相关的文档。流程文档有助于保持一致性、可重复性和效率,确保团队始终如一地遵循最佳实践。
运维文档
运维文档是为负责在生产环境中部署、配置和管理软件的运维团队准备的。它包括与安装说明、配置指南、监控和故障排除程序、灾难恢复(DR)计划以及其他运维任务相关的文档。运维文档帮助运维团队有效地管理和维护生产环境中的软件,确保其可用性、性能和可靠性。
发布说明和更新日志
发布说明和更新日志记录了每个版本发布中对软件所做的更改和更新。它们提供了新功能、修复的漏洞以及其他更改的摘要,并提供如何升级或迁移到最新版本的说明。发布说明和更新日志有助于让利益相关者了解软件的进展,并作为对软件随时间变化的历史记录。
如你所见,文档的组织可能会很棘手,因为你首先需要了解你的目标受众和目的。结合我们在这一部分讨论的信息,你应该能很快识别出这些内容。此外,值得补充的是,文档永远不是一成不变的,需要定期更新,以反映你应用程序当前的状况。
在下一部分,我们将讨论抵制变化的问题。组织越大,惯性越强,变化就越困难。让我们从 DevOps 的角度来探讨这个问题。
克服对变化的抵制
DevOps 要求许多组织进行重大文化转型,抵制变化可能是成功实施的一个重要障碍。抵制变化是实施新流程、新工具和其他文化变革时的一个常见因素,在任何组织中都起着重要作用。这就是为什么我们在几页前说过 “一直都是这么做的” 是最糟糕的说法之一。改进需要改变,而改变需要开放的心态和准备好摧毁现状的勇气。
改变的抵制有多个来源。其中之一是对变化的恐惧。改变组织会带来困难:一个新流程增加了失败的可能性。它还需要学习新事物,放弃已经熟悉并经过验证的解决方案。对大多数人来说,这是超出舒适区的领域。
另一个变革抗拒因素是组织惯性。通常,任何变革的引入都需要大量的文书工作和高层管理的认可。阅读前一段关于变革恐惧的内容。在公司中,绩效的一个指标是完成的工作量。任何接受会导致延误的变革的人都会处于压力之下。
有一些策略可以克服这种抗拒情绪。所有策略的基础是双向透明的沟通。任何引入变革的人都必须以清晰的方式进行沟通,并提前提供通知。那些将受到变革影响的人必须有时间考虑将要发生的事情以及他们在其中的角色。他们必须能够表达自己的意见,并感受到自己被听见。
变革实施失败的最大原因是如果变革看起来被强加,且让人们在整个过程中觉得自己毫无意义。
摘要
本书的最后一章探讨了可能妨碍 DevOps 实践成功实施的潜在陷阱和误解。我们强调了培养协作文化和优先考虑持续改进以实现预期结果的重要性。
我们已经讨论了各种常见的陷阱,包括忽视测试和质量保证、过度依赖自动化、忽略适当的监控和反馈循环、未能妥善处理安全和合规措施、未能实现可扩展性和灵活性,以及未与业务目标对齐等问题。
本章的一个重点是文档和知识共享的重要性,以及如何克服变革抗拒的策略。许多组织在实施 DevOps 时,在这些非技术性方面面临困难,本章提供了如何有效应对这些问题的实用指导。
本章中强调的另一个关键方面是需要建立强大的监控和反馈循环,以便及时了解 DevOps 流水线的性能和稳定性。如果没有适当的监控,及时识别和修复问题将变得十分困难,可能导致长时间的停机和生产力下降。
我们希望你能够在 DevOps 旅程中,对你可能遇到的一些问题产生影响,并在你之后成功地为其他人开辟加入组织的道路。通过本出版物中所包含的知识,你将具备充足的准备,迎接挑战,并拥有扎实的知识基础。
祝你好运!
1752

被折叠的 条评论
为什么被折叠?



