在 Terraform Cloud 上使用 GitOps 构建 CI/CD 管道
目录
我叫寺冈,是一名基础设施工程师。
正如标题所示,本文是关于 Terraform Cloud 的。
我想一步步总结一下,从概述到如何实际使用它。
Terraform 执行环境
运行 Terraform 时使用什么环境?
我觉得最基本的是每个地方的环境。
下载页面( *1 下载二进制文件
直接从本地运行 terraform apply 。
一个人测试时这还好,但实际施工时,往往需要多人操作。
这时候如果每个人在自己的本地环境中运行程序,可能会出现以下问题。
问题
- 如果您忘记推送到 Git,每个人的本地代码可能会出现差异。
- 无法共享 tfstate 文件
- 没有审查书面代码的机制
- 任何人都可以自由申请
- 云平台的访问密钥必须在本地管理
这些问题可以通过使用 Terraform Cloud 来解决。
什么是 Terraform 云?
我引用了Terraform 官方网站的部分内容( *2
Terraform Cloud 是一款帮助团队一起使用 Terraform 的应用程序,它在一致且可靠的环境中管理 Terraform 运行,包括轻松访问共享状态和秘密数据、用于批准基础设施更改的访问控制、用于共享 Terraform 模块的私有注册表、用于管理 Terraform 配置内容等的详细策略控制。
综上所述,Terraform Cloud是一个帮助团队一起使用Terraform的应用程序
,是一个SaaS,提供了团队使用Terraform时所需的功能,例如:
主要特点
- 一致且可靠的 Terraform 执行环境
- 共享状态文件 (tfstate) 和访问密钥等机密
- 用于批准基础设施变更的访问控制
- 用于共享 Terraform 模块的私有存储库
- 用于管理 Terraform 配置内容的策略控制
Terraform Cloud 基本上可以免费使用,但某些功能只能通过付费套餐使用。
确定您的团队需要哪些功能并选择正确的计划。
*3 总结了每个计划的功能和价格
下面描述的使用 Terraform Cloud 的所有方法都可以在免费套餐中进行配置,因此
最好从免费套餐开始,然后在缺少任何功能时切换到付费计划。
配置图
总结配置图上的特征和工作流程。
人物
A队
这是参与项目A所使用的系统开发的成员(以下简称“PRJ A”)所属的团队。
我们将在 PRJ A 的 AWS 账户上构建它,同时使用由下述 SRE 编写的 Terraform 模块。
B队
该团队包括参与项目 B(以下简称“PRJ B”)中使用的系统开发的成员。
我们将在 PRJ B 的 AWS 账户中构建它,同时使用由下述 SRE 编写的 Terraform 模块。
SRE
这是一个SRE(站点可靠性工程师)所属的团队,其目的是协助其他团队进行系统开发。
我认为根据角色也可以称为平台团队。
团队 A 和团队 B 将编写将在 PRJ 中使用的 Terraform 模块。
它还负责管理Terraform Cloud本身的设置,例如稍后将介绍的WorkSpace。
GitLab
管理由团队 A、团队 B 和 SRE 编写的 Terraform 代码。
在 Terraform Cloud 中,管理此类源代码的服务称为 VCS Provider。
这次我使用的是 Gitlab 的社区版。
当然,它也支持企业版和Github。 ( *4 )
存储库模块 VPC
这是一个存储库,用于管理由 SRE 编写的用于构建 AWS VPC 的 Terraform 模块。
在 Terraform Cloud 中,VCS Provider 中的存储库称为 VCS Repository。
存储库 PRJ A
这是一个存储库,用于管理 A 团队为 PRJ A 编写的 Terraform 代码。
概述与存储库模块 VPC 相同。
存储库 PRJ B
这是一个存储库,用于管理团队 A 为 PRJ B 编写的 Terraform 代码。
概述与存储库模块 VPC 相同。
工作空间 PRJ A
这是 Terraform Cloud 中 PRJ A 的工作区。
WorkSpace 是一种逻辑分组,用于将 Terraform 代码中描述的配置划分
为有意义的单元,例如按 PRJ 或按服务。 ( *5 )
工作空间 PRJ B
Terraform Cloud 中 PRJ B 的工作空间。
概要与 WorkSpace PRJ A 相同。
私有模块注册表
Terraform 注册表 ( *6 几乎相同的功能
由 SRE 编写的 Terraform 模块在这里进行管理。
AWS 云(PRJ A)
这是 PRJ A 的 AWS 账户。
A 团队将在此基础上继续发展。
AWS云(PRJ B)
PRJ B 的 AWS 账户。
B 队将在此基础上继续发展。
工作流程
执行该工作的人或工具位于括号中。
1. 推送模块代码(SRE)
在 SRE 上本地编写 Terraoform 模块并将其推送到 Git。
通过模块化代码,可以在 SRE 和 Team 之间适当划分工作范围。
2. 导入模块代码(SRE)
将推送的模块导入 Terraform Cloud 的私有模块注册表。
3.创建工作空间(SRE)
创建一个 Terraform Cloud WorkSpace 供每个团队使用。
4.推送Prj Infra代码(A队或B队)
每个团队将使用 SRE 编写的模块编写的 Terraform 代码推送到 Git。
5. VCS 提供商和自动计划(Terraform Cloud)
检测 Terraform Cloud 上的 Git 推送事件,并自动对推送的代码执行 terraform 计划。
通过这种机制,您可以从 GitOps 中受益,它根据 Git 的更改自动执行 CI/CD 流程。
6. 代码审查和批准(A组或B组)
terraform计划完成后,Terraform Cloud将等待Apply。
在应用设置之前,请检查计划结果,
如果更改符合预期,则批准“应用”。
7. 申请(Terraform Cloud)
这些变化实际上会反映在目标环境中。
8. 通知(Terraform Cloud)
当 Terraform Cloud 中执行某些处理时通知 Slack 等。
审查实施和工作流程
,我想检查
从 SRE 推送 Terraform 模块这将在创建 Terraoform Cloud 帐户和组织后进行描述。
请在注册页面( *7
Terraform 模块描述 (SRE)
这次我会写一个构建VPC的模块。
目录层次结构如下。
$ 树 . ├── README.md ├── 示例 │ └── vpc │ ├── main.tf │ ├── 输出.tf │ ├──provider.tf │ ├── terraform.tfstate │ ├── terraform.tfstate.backup │ ├── terraform.tfvars │ └──variables.tf ├──main.tf ├──outputs.tf └──variables.tf 2个目录,11个文件
目录根部的三个文件是模块本身。
这个会从A组或B组写的代码中读取,所以最好
在README.md中明确说明该模块的概述和规格,并
留下具体的使用示例作为下面的代码,方便以后使用。得到它。
Main.tf 描述创建 VPC 的资源。
主.tf
资源“aws_vpc”“vpc”{cidr_block = var.vpc_config.cidr_block enable_dns_support = var.vpc_config.enable_dns_support enable_dns_hostnames = var.vpc_config.enable_dns_hostnames标签={名称= var.vpc_config.name}}资源“aws_subnet”“public”{for_each = var.public_subnet_config.subnets vpc_id = aws_vpc.vpc.idavailability_zone = every.key cidr_block = every.value map_public_ip_on_launch = true 标签 = { Name = "${var.public_subnet_config.name}-${substr(each.key, - 2, 0)}" } } 资源 "aws_subnet" "dmz" { for_each = var.dmz_subnet_config.subnets vpc_id = aws_vpc.vpc.idavailability_zone =each.key cidr_block =each.value map_public_ip_on_launch = false 标签 = { Name = "$ {var.dmz_subnet_config.name}-${substr(each.key, -2, 0)}" } } 资源 "aws_subnet" "private" { for_each = var.private_subnet_config.subnets vpc_id = aws_vpc.vpc.idavailability_zone =each .key cidr_block = every.value map_public_ip_on_launch = false 标签 = { Name = "${var.private_subnet_config.name}-${substr(each.key, -2, 0)}" } } 资源 "aws_route_table" "public" {计数 = var.public_subnet_config.route_table_name != "" ? 1 : 0 vpc_id = aws_vpc.vpc.id 标签 = { 名称 = var.public_subnet_config.route_table_name } } 资源 "aws_route_table" "dmz" { count = var.dmz_subnet_config.route_table_name ! = "" ? 1 : 0 vpc_id = aws_vpc.vpc.id 标签 = { 名称 = var.dmz_subnet_config.route_table_name } } 资源 "aws_route_table" "private" { count = var.private_subnet_config.route_table_name != "" ? = aws_vpc.vpc.id 标签 = { 名称 = var.private_subnet_config.route_table_name } } 资源 "aws_internet_gateway" "igw" { count = var.public_subnet_config.internet_gateway_name != "" ? 1 : 0 vpc_id = aws_vpc.vpc.id 标签 = { Name = var.public_subnet_config.internet_gateway_name } } 资源 "aws_route" "public" { count = var.public_subnet_config.route_table_name != "" ? 1 : 0 route_table_id = aws_route_table.public[0].id destination_cidr_block = "0.0.0.0/ 0" gateway_id = aws_internet_gateway.igw[0].id dependent_on = [aws_route_table.public] } 资源 "aws_route" "dmz" { count = var.dmz_subnet_config.route_table_name != "" ? 1 : 0 destination_cidr_block = "0.0.0.0/ 0"route_table_id = aws_route_table.dmz[0].id nat_gateway_id = aws_nat_gateway.natgw[0].id dependent_on = [aws_route_table.dmz] } 资源 "aws_route_table_association" "public" { for_each = aws_subnet.publicsubnet_id = every.value.id Route_table_id = aws_route_table.public[0].id } 资源 "aws_route_table_association" "dmz" { for_each = aws_subnet.dmzsubnet_id = every.value.id Route_table_id = aws_route_table.dmz[0].id } 资源 "aws_route_table_association" "private" { for_each = aws_subnet.privatesubnet_id =each.value.idroute_table_id = aws_route_table.private[0].id } 资源 "aws_eip" "natgw" { count = var.dmz_subnet_config.route_table_name != "" ? 1 : 0 vpc = true 标签= { 名称 = var.dmz_subnet_config.nat_gateway_name } } 资源 "aws_nat_gateway" "natgw" { 计数 = var.dmz_subnet_config.route_table_name != "" ? 1 : 0 分配_id = aws_eip.natgw[0].id 子网_id = aws_subnet.public[键(aws_subnet.public)[0]].id 标签 = { 名称 = var.dmz_subnet_config.nat_gateway_name } dependent_on = [aws_internet_gateway.igw] }
在outputs.tf中,写入Output,输出Module创建的Resource的信息。
输出.tf
输出“vpc” { value = aws_vpc.vpc } 输出“public_subnet” { value = aws_subnet.public } 输出“dmz_subnet” { value = aws_subnet.dmz } 输出“private_subnet” { value = aws_subnet.private }
Variables.tf 描述了模块接收到的变量的结构。
请务必编写变量的描述和默认值。
稍后会解释原因。
变量.tf
变量“vpc_config”{描述=“VPC配置”类型=对象({名称=字符串cidr_block=字符串enable_dns_support=boolenable_dns_hostnames=bool})默认={名称=“”cidr_block=“”enable_dns_support=falseenable_dns_hostnames=false}}变量"public_subnet_config" { description = "公共子网配置" type = object({ name = string route_table_name = string internet_gateway_name = stringsubnets = map(string) }) default = { name = ""route_table_name = "" internet_gateway_name = "" 子网= {} } } 变量 "dmz_subnet_config" { 描述 = "DMZ 的子网配置" type = object({ name = string route_table_name = string nat_gateway_name = stringsubnets = map(string) }) default = { name = ""route_table_name = " " nat_gateway_name = "" 子网 = {} } } 变量 "private_subnet_config" { 描述 = "私有子网配置" type = object({ name = string route_table_name = stringsubnets = map(string) }) default = { name = ""路由表名称 = "" 子网 = {} } }
示例 下面,我们将把具体的使用示例作为代码留下。
这是关于如何加载模块和如何传递变量的部分。
从环境变量而不是 terraform.tfvars 加载 AWS 访问密钥等。
示例/provider.tf
提供商“aws” { access_key = var.access_key Secret_key = var.secret_key 区域 = var.region Should_role { role_arn = var.role_arn } }
示例/变量.tf
变量“project”{描述=“项目名称”}变量“环境”{描述=“环境”}变量“access_key”{描述=“AWS访问密钥”}变量“secret_key”{描述=“AWS密钥”}变量"role_arn" {description = "担任角色的 AWS 角色 ARN" } 变量 "region" {description = "AWS 区域" }
示例/terraform.tfvars
########################## # 项目 ##################### # ##### 项目 = “terraform-vpc-module” 环境 = “本地” 区域 = “ap-northeast-1”
示例/main.tf
模块“vpc”{源=“../../”vpc_config={name=“vpc-${var.project}-${var.environment}”cidr_block=“10.0.0.0/16”enable_dns_support=trueenable_dns_hostnames = true } public_subnet_config = { name =“子网-${var.project}-${var.environment}-public”route_table_name =“路由-${var.project}-${var.environment}-public”internet_gateway_name = "igw-${var.project}-${var.environment}" 子网 = { ap-northeast-1a = "10.0.10.0/24" ap-northeast-1c = "10.0.11.0/24" ap-northeast- 1d = "10.0.12.0/24" } } dmz_subnet_config = { name = "子网-${var.project}-${var.environment}-dmz" Route_table_name = "路由-${var.project}-${var .environment}-dmz" nat_gateway_name = "nat-${var.project}-${var.environment}" 子网 = { ap-northeast-1a = "10.0.20.0/24" ap-northeast-1c = "10.0. 21.0/24" ap-northeast-1d = "10.0.22.0/24" } } private_subnet_config = { name = "子网-${var.project}-${var.environment}-private" route_table_name = "route-${ var.project}-${var.environment}-private" 子网 = { ap-northeast-1a = "10.0.30.0/24" ap-northeast-1c = "10.0.31.0/24" ap-northeast-1d = " 10.0.32.0/24" } } }
示例/输出.tf
输出“vpc_id”{值= module.vpc.vpc.id}
编写完此内容后,在 GitLab 上创建一个存储库并将其推送到 Master 分支。
这次,我提前创建了一个名为 Terraform AWS Module VPC 的存储库。
向 Git 提交添加标签。
Terraform Cloud 允许您根据此标签对模块进行版本控制。
$ git tag v1.0.0 $ git Push origin v1.0.0
将模块 (SRE) 导入 Terraform Cloud
要从 GitLab 导入模块,
您需要将 VCS 提供程序设置添加到 Terraform Cloud。
由于这次我们将使用 GitLab,所以让我们参考
*8 使用其他 VCS 提供商时也有一些流程,因此请参阅适用的流程。 ( *9 )
之后,它将被添加到 Terraform Cloud 控制台的“设置”>“VCS 提供商”项目中。
可以使用设置 > 模块 > 添加模块从 Terraform Cloud 控制台添加导入。
将显示您之前添加的 VCS 提供程序,因此选择它。
选择它时,将显示 VCS Repository,因此请选择之前将模块推送到的存储库。
单击确认屏幕上的发布模块。
Publish 完成后,可以看到 Module 的 README.md 和带有 Git 标记的版本已经加载。
您还可以看到需要传递给模块的变量列表。
如果你在写变量的时候写了描述和默认值,你
可以在这个屏幕上查看详细信息,很方便。
您还可以看到运行它时创建的资源列表,这很棒。
为 PRJ B (SRE) 创建 WordSpace
创建一个 Terraform Cloud WordSpace 并将其交给 B 团队。
您可以通过选择“Wordspaces”>“新工作区”从 Terraform Cloud 控制台创建一个工作区。
首先,我只想创建一个工作区并稍后添加设置,因此选择“无 VCS 连接”。
输入工作区的名称,然后单击创建工作区。
名称可以是任何格式,但类似于 team-name_prj-name_environment 的名称会更易于管理。
创建后,它将显示在列表中,如下所示。
为 PRJ B 创建存储库并推送 Terraform 代码(B 团队)
为 PRJ B 编写 Terraform 代码。
B 队将使用 SRE 预先编写的模块。
目录结构
$tree . ├── backend.tf ├── main.tf ├──outputs.tf ├──providers.tf └──variables.tf 0个目录,5个文件
首先,backend.tf 在这里很重要。
这次运行 Terraform 后的状态文件(tfstate)
指定在远程后端创建的工作区将在 Terraform Cloud 上进行管理。
后端.tf
terraform { 后端 "remote" { 主机名 = "app.terraform.io" 组织 = "Org-Name" 工作区 { prefix = "team_b_prj_b_prod" } } }
提供商.tf
提供商“aws” { access_key = var.access_key Secret_key = var.secret_key 区域 = var.region Should_role { role_arn = var.aws_role_arn } }
我们来写变量。
如果要在变量中存储值,可以使用 terraform.tfvars 或环境变量,但
这次值本身将在 Terraform Cloud 上管理,因此都不会在本地准备。
变量.tf
#################### # 项目 ###################### 变量 "project" { 描述= "项目名称" } 变量 "环境" { 描述 = "环境" } ##################### # AWS Common ######### ############ 变量 "access_key" { 描述 = "AWS 访问密钥" } 变量 "secret_key" { 描述 = "AWS 密钥" } 变量 "role_arn" { 描述 = "AWS 角色 ARN承担角色" } 变量"区域" { 描述 = "AWS 区域" }
main.tf 指定导入到私有模块注册表中的模块作为源。
主.tf
模块“vpc”{源=“app.terraform.io/Org-Name/module-vpc/aws”版本=“1.0.0”vpc_config={名称=“vpc-${var.project}-${var.环境}” cidr_block =“10.0.0.0/16”enable_dns_support = true enable_dns_hostnames = true} public_subnet_config = { name =“子网-$ {var.project}-$ {var.environment}-public”route_table_name =“route-$ { var.project}-${var.environment}-public" internet_gateway_name = "igw-${var.project}-${var.environment}" 子网 = { ap-northeast-1a = "10.0.10.0/24" ap -northeast-1c = "10.0.11.0/24" ap-northeast-1d = "10.0.12.0/24" } } dmz_subnet_config = { name = "子网-${var.project}-${var.environment}-dmz "route_table_name = "route-${var.project}-${var.environment}-dmz" nat_gateway_name = "nat-${var.project}-${var.environment}" 子网 = { ap-northeast-1a = "10.0.20.0/24" ap-northeast-1c = "10.0.21.0/24" ap-northeast-1d = "10.0.22.0/24" } } private_subnet_config = { name = "subnet-${var.project}- ${var.environment}-private"route_table_name = "route-${var.project}-${var.environment}-private"子网 = { ap-northeast-1a = "10.0.30.0/24" ap-northeast- 1c = "10.0.31.0/24" ap-northeast-1d = "10.0.32.0/24" } } }
输出.tf
输出“vpc_id”{值= module.vpc.vpc.id}
编写完此内容后,我们将其推送到 Git。
将变量添加到 PRJ B(B 团队)的工作区
当您从列表中选择您创建的工作区时,您将看到一个名为“变量”的项目,
您可以在其中管理工作区中使用的变量的值。
其独特之处在于 AWS 访问密钥等机密信息被存储为敏感值。
如果这样做,您可以编辑该值,但它不会显示在屏幕上或 API 结果中,因此
在添加要保持隐藏的值时非常方便。
更改 PRJ B(B 团队)的工作区设置
从工作区设置更改以下两项设置。
将您想要发送通知的 Slack 通道的设置添加到“通知”设置
参考
Terraform CLoud步骤( *8 由于我们将使用 WebHook,因此我们提前在 Slack 端进行设置。
添加版本控制设置
将 PRJ B 的存储库注册为要在 WorkSpace 中读取的 VCS 存储库。
从工作区屏幕中,转至设置 > 版本控制。
选择您预先注册的VCS提供商。
选择适当的存储库。
单击更新 VCS 设置。
一段时间后,从存储库加载 Terraform 代码将完成。
由于变量设置已经完成,我们点击队列计划。
Terraform 计划仍然在 Terraform Cloud 上手动执行。
Plan完成后,Apply不会被执行,会一直处于等待批准的状态,如下图,所以
如果要执行Apply,需要检查Plan的结果,并从Confirm & Apply进行批准。
一旦批准,将执行如下所示的Apply,并且设置将反映在目标环境中。
状态文件 (tfstate) 也在 Terraform Cloud 上进行严格管理。
我在通知设置中指定了 Slack 通道,因此通知非常完美。
参考网址
我通过手动运行确认没有问题。
这次,我们希望自动执行它以响应 Git 的推送。
让我们对 Terraform 代码进行一些更改,看看它是如何工作的。
让我们更改 VPC 公有子网的一些名称并将其推送到 Git。
计划开始自动执行以响应 Git 推送。
当然,这次也是在Apply之前停止,并且仅将更改的部分作为差异输出。
没有问题,那么我们就批准并申请吧。
申请成功,看起来没有什么问题。
概括
你觉得怎么样?
在团队中使用 Terraform 时,您需要考虑开头描述的问题。
Terraform Cloud 为此提供了有用的功能,并为团队的使用提供了强有力的支持。
即使在免费套餐中也提供了许多功能,因此请尝试一下。
参考网址
*1 https://www.terraform.io/downloads.html
*2 https://www.terraform.io/docs/cloud/index.html
*3 https://www.hashicorp.com/products/terraform/定价/
*4 https://www.terraform.io/docs/cloud/vcs/index.html
*5 https://www.terraform.io/docs/cloud/workspaces/index.html
*6 https:// registry.terraform.io
*7 https://app.terraform.io/signup/account
*8 https://www.terraform.io/docs/cloud/vcs/gitlab-eece.html
*9 https://www .terraform.io/docs/cloud/vcs/index.html
*10 https://www.terraform.io/docs/cloud/workspaces/notifications.html#slack