使用Terraform时的最佳实践

134
我正在将我们的基础架构转移至terraform,我想了解实际管理terraform文件和状态的最佳实践。 我意识到它是基础设施即代码,我会将我的.tf文件提交到git,但是我是否应该提交tfstate?它应该驻留在像S3之类的地方吗?最终,我希望CI管理所有这些内容,但这需要我弄清楚文件的组成部分。我只是想看看人们如何在生产中使用这种类型的东西。
12个回答

109

我正在将现有的AWS基础架构迁移到Terraform上,因此在开发过程中会更新答案。

我一直在依赖官方的Terraform 示例和多次尝试来解决我不确定的领域。

.tfstate文件

Terraform配置可以用于在不同的基础架构上提供许多盒子,每个盒子都可能有不同的状态。由于它也可以由多个人运行,因此这个状态应该位于集中位置(如S3),但是不要放在git中。

这可以通过查看Terraform .gitignore来确认。

开发者控制

我们的目标是为开发人员提供更多的基础架构控制权,同时保持完整的审计(git log)和检查更改的能力(pull requests)。考虑到这一点,我正在努力实现新的基础架构工作流程:

  1. 常见AMI的基础,包括可重用模块例如puppet。
  2. DevOps使用Terraform提供核心基础设施。
  3. 开发人员根据需要在Git中更改Terraform配置(实例数量;新VPC;增加区域/可用区等)。
  4. Git配置被推送并提交拉取请求以由DevOps小组成员进行检查。
  5. 如果获得批准,则调用Webhook到CI进行构建和部署(目前不确定如何分区多个环境)。

编辑1-当前状态更新

自开始回答以来,我已经编写了大量TF代码,并对我们的情况感到更加舒适。我们一路上遇到了错误和限制,但我接受这是使用新的、快速变化的软件的特点。

布局

我们有一个复杂的AWS基础架构,每个VPC都有多个子网。方便管理的关键是定义一个灵活的分类法,包括区域、环境、服务和所有者,我们可以用它来组织我们的基础设施代码(包括terraform和puppet)。

模块

下一步是创建一个单独的git存储库来存储我们的terraform模块。我们的模块顶层目录结构如下:

tree -L 1 .

结果:

├── README.md
├── aws-asg
├── aws-ec2
├── aws-elb
├── aws-rds
├── aws-sg
├── aws-vpc
└── templates

每个设置都有一些合理的默认值,但将它们作为变量公开,可以由我们的“粘合剂”覆盖。
粘合剂
我们有一个第二个存储库,其中包含我们的“粘合剂”,它利用上述模块。它按照我们的分类法文件布局:
.
├── README.md
├── clientA
│   ├── eu-west-1
│   │   └── dev
│   └── us-east-1
│       └── dev
├── clientB
│   ├── eu-west-1
│   │   ├── dev
│   │   ├── ec2-keys.tf
│   │   ├── prod
│   │   └── terraform.tfstate
│   ├── iam.tf
│   ├── terraform.tfstate
│   └── terraform.tfstate.backup
└── clientC
    ├── eu-west-1
    │   ├── aws.tf
    │   ├── dev
    │   ├── iam-roles.tf
    │   ├── ec2-keys.tf
    │   ├── prod
    │   ├── stg
    │   └── terraform.tfstate
    └── iam.tf

在客户端级别,我们有特定于AWS帐户的.tf文件来提供全局资源(如IAM角色);接下来是区域级别,包括EC2 SSH公钥;最后,在我们的环境中(例如devstgprod等),存储了我们的VPC设置、实例创建和对等连接等内容。

附注:正如您所看到的,我违背了自己的建议,将terraform.tfstate保存在git中。这只是一个临时措施,直到我转移到S3,但它适合我,因为我目前是唯一的开发人员。

下一步

这仍然是一个手动过程,尚未在Jenkins中实现,但我们正在移植一个相当大而复杂的基础架构,目前还不错。像我说的那样,有一些错误,但进展顺利!

编辑2-更改

距离我写下这个初始答案已经快一年了,Terraform和我自己的状况都发生了很大的变化。我现在在一个新职位上使用Terraform来管理Azure集群,而Terraform现在是v0.10.7

状态

人们一再告诉我,状态不应该进入Git - 他们是正确的。我们将其用作两个人团队的临时措施,这个团队依赖于开发者的沟通和纪律性。现在,我们正在使用由DynamoDB提供的锁定完全利用S3中的远程状态来管理一个更大的分布式团队。理想情况下,现在它已经是v1.0,我们将把它迁移到consul以削减跨云提供商的成本。 模块 以前,我们创建并使用内部模块。虽然仍然如此,但随着Terraform注册表的出现和增长,我们尝试使用它们至少作为基础。 文件结构 新的职位有一个更简单的分类法,只有两个infrastructure环境-devprod。每个环境都有自己的变量和输出,重复使用我们上面创建的模块。remote_state提供程序还有助于在环境之间共享所创建资源的输出。我们的方案是将不同Azure资源组中的子域添加到全球管理的TLD中。
├── main.tf
├── dev
│   ├── main.tf
│   ├── output.tf
│   └── variables.tf
└── prod
    ├── main.tf
    ├── output.tf
    └── variables.tf

规划

考虑到分布式团队的额外挑战,我们现在总是保存terraform plan命令的输出。我们可以检查并知道将要运行什么,而不会在planapply阶段之间存在一些变化(尽管锁定有助于解决这个问题)。请记得删除此计划文件,因为它可能包含明文“秘密”变量。

总体而言,我们对Terraform非常满意,并继续学习和改进新功能。


4
您认为 tfstate 文件不应存储在 Git 中的原因是什么?这是因为旧状态不值得保存,还是有其他问题? - agbodike
5
当作为单个开发人员或非常小的团队工作时,如果tfstate被定期提交并推送以避免冲突,则可以将其保存在git中。我的下一步是按照它们在S3上的远程状态文档进行设置(该文档还指出:“由于它经常导致合并冲突,因此使团队中的Terraform工作复杂。远程状态有助于缓解这些问题。”)。然而,像大多数事情一样,良好的团队沟通可以帮助缓解大部分/所有问题,无论采用何种策略来保存状态 :-) - Ewan
1
@the0ther - 很抱歉,我的主要代码库是专有的,但我目前正在开发一个个人代码库,很快将公开发布。 - Ewan
2
@Ewan,你的Git仓库有进展了吗?我很想看看你在做什么。 - David
添加.tfstate文件是一个非常糟糕的想法,应该避免。如果部署并忘记检查您的.tfstate文件,可能会破坏整个基础架构。您的状态文件应存储在S3或官方文档中描述的其他后端中。 - jottr
显示剩余10条评论

94
我们在编程中广泛使用 Terraform,并推荐以下设置:
文件布局
我们强烈建议将每个环境(例如 stage、prod、qa)的 Terraform 代码存储在单独的模板集(因此,单独的 .tfstate 文件)中。这很重要,这样您的不同环境实际上是相互隔离的,同时进行更改。否则,在暂存过程中搞砸一些代码时,太容易在生产环境中出现问题了。请参见 Terraform、VPC 和为什么您需要每个 env 的 tfstate 文件 的Terraform, VPC, and why you want a tfstate file per env,以获得有关此内容的详细讨论。
因此,我们的典型文件布局如下:
stage
  └ main.tf
  └ vars.tf
  └ outputs.tf
prod
  └ main.tf
  └ vars.tf
  └ outputs.tf
global
  └ main.tf
  └ vars.tf
  └ outputs.tf

所有阶段VPC的Terraform代码都放在stage文件夹中,所有生产VPC的代码都放在prod文件夹中,而所有不属于VPC的代码(例如IAM用户、SNS主题、S3存储桶)都放在global文件夹中。
请注意,按照惯例,我们通常将Terraform代码分成三个文件:
  • vars.tf:输入变量。
  • outputs.tf:输出变量。
  • main.tf:实际资源。

模块

通常,我们将基础设施定义在两个文件夹中:
  1. infrastructure-modules:此文件夹包含小型、可重用、有版本控制的模块。将每个模块视为创建单个基础设施部件(如VPC或数据库)的蓝图。
  2. infrastructure-live:此文件夹包含实际正在运行的基础设施,它通过组合infrastructure-modules中的模块来创建。将此文件夹中的代码视为从蓝图建造的实际房屋。

一个Terraform模块就是指包含在文件夹中的一组Terraform模板。例如,我们可能有一个名为vpc的文件夹在infrastructure-modules中,用于定义单个VPC的所有路由表、子网、网关、ACL等内容:

infrastructure-modules
  └ vpc
    └ main.tf
    └ vars.tf
    └ outputs.tf

我们可以在infrastructure-live/stageinfrastructure-live/prod中使用该模块来创建stage和prod VPC。例如,这是infrastructure-live/stage/main.tf的示例:
module "stage_vpc" {
  source = "git::git@github.com:gruntwork-io/module-vpc.git//modules/vpc-app?ref=v0.0.4"

  vpc_name         = "stage"
  aws_region       = "us-east-1"
  num_nat_gateways = 3
  cidr_block       = "10.2.0.0/18"
}

要使用模块,您需要使用module资源,并将其source字段指向本地硬盘上的路径(例如source = "../infrastructure-modules/vpc"),或者如上例所示,指向Git URL(请参见模块来源)。 Git URL的优点是我们可以指定特定的git sha1或标签(ref=v0.0.4)。现在,我们不仅将基础设施定义为一堆小模块,而且可以对这些模块进行版本控制,并根据需要进行仔细的更新或回滚。
我们已经创建了许多可重用、经过测试和文档化的基础设施包,用于创建VPC、Docker集群、数据库等等,在幕后,它们大多数只是有版本控制的Terraform模块。
状态
当你使用Terraform创建资源(例如EC2实例,数据库,VPC),它会记录在.tfstate文件中创建的信息。要对这些资源进行更改,团队中的每个人都需要访问同一个.tfstate文件,但是你不应该将其提交到Git中(请参见此处的解释)。
相反,我们建议启用Terraform远程状态,通过将.tfstate文件存储在S3中,每次运行Terraform时都会自动推送/拉取最新的文件。确保在S3存储桶中启用版本控制,以便在某种情况下损坏最新版本时可以回滚到旧的.tfstate文件。 然而,需要注意的是:Terraform不提供锁定功能。因此,如果两个团队成员同时在同一个.tfstate文件上运行terraform apply,他们可能会覆盖彼此的更改。

编辑2020年:Terraform现在支持锁定功能:https://www.terraform.io/docs/state/locking.html

为了解决这个问题,我们创建了一个名为Terragrunt的开源工具,它是Terraform的轻量级封装,使用Amazon DynamoDB提供锁定功能(对于大多数团队来说应该完全免费)。查看Add Automatic Remote State Locking and Configuration to Terraform with Terragrunt以获取更多信息。
进一步阅读
我们刚刚开始了一系列名为A Comprehensive Guide to Terraform的博客文章,详细描述了我们在实际使用Terraform中学到的所有最佳实践。
更新:Terraform: Up & Running博客文章系列非常受欢迎,我们将其扩展成了一本书!

我认为这是正确的答案。使用模块,对它们进行版本控制,并保持环境分离。 - wrangler
不使用terragrunt或其他包装器,如果要在不同的terraform组件/环境/模块/任何内容上工作,是否需要每次重新运行远程配置步骤? - jmreicha
@jmreicha:如果你刚刚检查了你的Terraform配置文件或者想要更改之前的远程配置, 那么你需要运行remote config. Terraform 0.9将会引入“后端”这一概念,这将大大简化操作。有关更多详细信息,请参见此PR. - Yevgeniy Brikman
那很有帮助。谢谢。 - jmreicha
我开始使用 inputs.tf 来处理输入变量。但是,在一个项目中保持一致性是很好的,这样当你在模块之间跳转时,你就知道它们得到了什么,并且暴露了什么。 - Aldo 'xoen' Giambelluca
显示剩余2条评论

10

以前,remote config 允许这样做,但现在已被 "backends" 取代,因此 terraform remote 不再可用。

terraform remote config -backend-config="bucket=<s3_bucket_to_store_tfstate>" -backend-config="key=terraform.tfstate" -backend=s3
terraform remote pull
terraform apply
terraform remote push

请参阅文档了解详情。


远程源每次想要在不同的terraform组件/环境/模块/任何内容上工作时,需要重新配置吗? - jmreicha

6

在@ Yevgeny Brikman的更深入介绍中已经涉及,但是特别回答OP的问题:

管理terraform文件和状态的最佳实践是什么?

使用git来处理TF文件。但不要检查State文件(即tfstate)。而是使用Terragrunt将状态文件同步/锁定到S3。

我需要提交tfstate吗?

不需要。

它应该保存在类似于S3的地方吗?

是的


3
我知道这里有很多答案,但我的方法有所不同。
   Modules
   Environment management 
   Separation of duties

模块

  1. 创建逻辑资源集合的模块。 例如:如果您的目标是部署 API,需要使用 DB、HA VM、自动扩展、DNS、PubSub和对象存储等所有这些资源,则应在单个模块中对所有这些资源进行模板化。
  2. 避免创建只使用单个资源的模块。尽管注册表中的许多模块采用了这种做法,但这只有助于资源可访问性,而不是基础设施编排。 例如:AWS EC2 的模块帮助用户通过使复杂配置更简单来访问 EC2,但像第 1 条示例中的模块则可以协助用户编排应用程序、组件或服务驱动的基础架构。
  3. 避免在工作区中声明资源。这更多是为了保持代码整洁和有条理。由于模块易于版本控制,因此您可以更好地控制发布。

环境管理

IaC 已经使得 SDLC 过程与基础设施管理相关联,因此我们希望拥有开发基础设施和开发应用程序环境已经变得正常。

  1. 不要使用文件夹来管理您的 IaC 环境。这会导致漂移,因为没有公共模板来管理您的基础设施。
  2. 使用单个工作区和变量来控制环境规范。 例如:编写您的模块,以便当您更改环境变量(var.stage 可选)时,计划会相应地进行调整。通常情况下,环境应尽可能少地变化,而数量、暴露和容量通常是可变配置。Dev 可能在私人拓扑中部署 1 个具有 1 核心和 1GB RAM 的 VM,但生产环境可能需要增加到 3 个具有 2 核心和 4GB RAM 的 VM,同时还需要其他公共拓扑。当然,您可以有更多变化:开发环境可能在同一服务器上运行数据库进程以节省成本,但生产环境可能具有专用的 DB 实例。所有这些都可以通过更改单个变量、三元语句和插值进行管理。

职责分离

如果您在一个小组织或运行个人基础设施,则不太适用,但它将有助于您管理操作。

  1. 按职责、责任或团队拆分您的基础设施。 例如:中央 IT 控制基础共享服务(虚拟网络、子网、公共 IP 地址、日志组、治理资源、多租户 DB 等),而 API 团队仅控制其服务所需的资源(VM、LB、PubSub 等),并通过数据源和远程状态查找使用中央 IT 的服务。
  2. 管理团队访问权限。 例如:中央 IT 可能具有管理员权限,但 API 团队只能访问一组受限制的公共云 API。

这也有助于释放关注点,因为您会发现一些资源很少更改,而其他资源则经常更改。职责分离可消除风险和复杂性。

这种策略与 AWS 的多帐号策略相似。阅读更多信息。

这是一个独立的主题,但Terraform在良好的流水线内运作得非常好。最常见的错误是将CI视为一种万能解决方案。从技术上讲,Terraform只应在组装流水线的阶段中提供基础架构。这将与CI阶段中发生的内容分开,通常在这些阶段中验证和测试模板。

N.B. 在手机上编写,请原谅任何错误。


0

我不喜欢子文件夹的想法,因为这会导致每个环境有不同的源代码,很容易出现偏差。

更好的方法是为所有环境(比如开发、预生产和生产)使用单一堆栈。要在单个环境中工作,请使用terraform workspace

terraform workspace new dev

这将创建一个新的工作区。其中包括一个专用状态文件和变量terraform.workspace,您可以在代码中使用。

resource "aws_s3_bucket" "bucket" {
  bucket = "my-tf-test-bucket-${terraform.workspace}"
}

这样,您将获得以下名称的存储桶:

  • my-tf-test-bucket-dev
  • my-tf-test-bucket-preprod
  • my-tf-test-bucket-prod

在应用于上述工作区后(使用terraform workspace select <WORKSPACE>更改环境),为了使代码更具多区域适用性,请按照以下方式操作:

data "aws_region" "current" {}

resource "aws_s3_bucket" "bucket" {
  bucket = "my-tf-test-bucket-${data.aws_region.current.name}-${terraform.workspace}"
}

获取(针对us-east-1地区)

  • my-tf-test-bucket-us-east-1-dev
  • my-tf-test-bucket-us-east-1-preprod
  • my-tf-test-bucket-us-east-1-prod

0

我想要为这个主题做出贡献。

  • 除非使用 Terraform Cloud,否则最有可能使用 AWS S3+DynamoDB。
  • 将生产和非生产后端的基础设施(网络+RBAC)分离。
  • 计划禁用外部网络(如部署代理池)以外的状态文件(网络访问和RBAC)的访问。
  • 不要将 Terraform 后端基础设施与运行时环境放在一起。使用独立的账户。
  • 在 Terraform 后端启用对象版本控制,以避免丢失更改和状态文件,并维护 Terraform 状态历史记录。

在某些特殊情况下,需要手动访问 Terraform 状态文件。例如重构、重大变更或修复缺陷将需要操作人员通过 Terraform 状态操作来运行。对于这种情况,计划使用堡垒机、VPN 等进行特别控制的 Terraform 状态访问。

查看更长的最佳实践博客,其中详细介绍了 CI/CD 流水线的指导方针。


0
在回答非常扎实和有信息量之前,我会尝试在这里添加我的两分钱。
常见的代码结构建议:
  1. 使用更少的资源可以更轻松、更快速地工作:

    • terraform planterraform apply都会对云API进行调用以验证资源的状态。
    • 如果您的整个基础架构在一个单一的组合中,这可能需要很多分钟(即使您有几个文件在同一个文件夹中)。
  2. 较少的资源意味着更小的影响范围:

    • 通过将不相关的资源放置在单独的组合(文件夹)中相互隔离,可以降低出现问题的风险。
  3. 使用远程状态开始项目:

  4. 尝试使用一致的结构和命名约定:

    • 像过程式代码一样,Terraform代码应首先为人们编写,一致性将有助于在六个月后进行更改时。
    • 可以在Terraform状态文件中移动资源,但如果您的结构和命名不一致,则可能更难实现。
  5. 尽可能保持资源模块的简洁。

  6. 不要硬编码可以作为变量传递或使用数据源发现的值。

  7. 特别是使用data源和terraform_remote_state作为组合内基础设施模块之间的粘合剂。

(参考文章:https://www.terraform-best-practices.com/code-structure)


示例:

使用更少的资源工作更容易和更快,因此我们在下面提供了推荐的代码布局。

注意:仅作为参考,不必严格遵循,因为每个项目都有其自身的特点。

.
├── 1_tf-backend #remote AWS S3 + Dynamo Lock tfstate 
│   ├── main.tf
│   ├── ...
├── 2_secrets
│   ├── main.tf
│   ├── ...
├── 3_identities
│   ├── account.tf
│   ├── roles.tf
│   ├── group.tf
│   ├── users.tf
│   ├── ...
├── 4_security
│   ├── awscloudtrail.tf
│   ├── awsconfig.tf
│   ├── awsinspector.tf
│   ├── awsguarduty.tf
│   ├── awswaf.tf
│   └── ...
├── 5_network
│   ├── account.tf
│   ├── dns_remote_zone_auth.tf
│   ├── dns.tf
│   ├── network.tf
│   ├── network_vpc_peering_dev.tf
│   ├── ...
├── 6_notifications
│   ├── ...
├── 7_containers
│   ├── account.tf
│   ├── container_registry.tf
│   ├── ...
├── config
│   ├── backend.config
│   └── main.config
└── readme.md

0

遵循一些 Terraform 的最佳实践:

  1. 避免硬编码: 有时开发人员会直接手动创建资源。您需要标记这些资源并使用 terraform import 将它们包含在代码中。 一个示例:

    account_number="123456789012" account_alias="mycompany"

  2. 从 Docker 容器中运行 Terraform: Terraform 发布了一个官方的 Docker 容器,让您轻松控制可以运行哪个版本。

建议在 CI/CD 流水线中设置构建作业时运行 Terraform Docker 容器。

TERRAFORM_IMAGE=hashicorp/terraform:0.11.7
TERRAFORM_CMD="docker run -ti --rm -w /app -v ${HOME}/.aws:/root/.aws -v ${HOME}/.ssh:/root/.ssh -v `pwd`:/app $TERRAFORM_IMAGE"

更多信息,请参考我的博客:https://medium.com/tech-darwinbox/how-darwinbox-manages-infrastructure-at-scale-with-terraform-371e2c5f04d3


0

我相信在使用terraform编排基础架构时需要遵循一些最佳实践:

  1. 不要重复编写相同的代码(可重用性)
  2. 将环境配置保持分离以便于维护。
  3. 使用远程后端s3(加密)和dynamo DB来处理并发锁定
  4. 创建一个模块并在主基础架构中多次使用该模块,就像可重用函数一样,可以通过传递不同的参数多次调用它。

处理多个环境

大多数情况下,推荐的方法是使用terraform的“工作区”来处理多个环境,但我认为工作区的使用可能会因组织的工作方式而异。 另一种方法是将每个环境(例如stage、prod、QA)的Terraform代码存储到单独的环境状态中。然而,在这种情况下,我们只是在许多地方复制相同的代码。

├── main.tf
├── dev
│   ├── main.tf
│   ├── output.tf
│   └── variables.tf
└── prod
├── main.tf
├── output.tf
└── variables.tf

我采用了一些不同的方法来处理和避免重复使用相同的terraform代码,通过在每个环境文件夹中保留代码,因为我相信大多数情况下所有环境都会有90%的相似之处。

├── deployment
│ ├── 01-network.tf
│ ├── 02-ecs_cluster.tf
│ ├── 03-ecs_service.tf
│ ├── 04-eks_infra.tf
│ ├── 05-db_infra.tf
│ ├── 06-codebuild-k8s.tf
│ ├── 07-aws-secret.tf
│ ├── backend.tf
│ ├── provider.tf
│ └── variables.tf
├── env
│ ├── dev
│ │ ├── dev.backend.tfvar
│ │ └── dev.variables.tfvar
│ └── prod
│ ├── prod.backend.tfvar
│ └── prod.variables.tfvar
├── modules
│ └── aws
│ ├── compute
│ │ ├── alb_loadbalancer
│ │ ├── alb_target_grp
│ │ ├── ecs_cluster
│ │ ├── ecs_service
│ │ └── launch_configuration
│ ├── database
│ │ ├── db_main
│ │ ├── db_option_group
│ │ ├── db_parameter_group
│ │ └── db_subnet_group
│ ├── developertools
│ ├── network
│ │ ├── internet_gateway
│ │ ├── nat_gateway
│ │ ├── route_table
│ │ ├── security_group
│ │ ├── subnet
│ │ ├── vpc
│ └── security
│ ├── iam_role
│ └── secret-manager
└── templates

与环境相关的配置

将与环境相关的配置和参数分别保存在一个变量文件中,并将该值传递以配置基础设施。例如:

  • dev.backend.tfvar

      region = "ap-southeast-2"
      bucket = "dev-samplebackendterraform"
      key = "dev/state.tfstate"
      dynamo_db_lock = "dev-terraform-state-lock"
    
  • dev.variable.tfvar

    environment                     =   "dev"
    vpc_name                        =   "demo"
    vpc_cidr_block                  =   "10.20.0.0/19"
    private_subnet_1a_cidr_block    =   "10.20.0.0/21"
    private_subnet_1b_cidr_block    =   "10.20.8.0/21"
    public_subnet_1a_cidr_block     =   "10.20.16.0/21"
    public_subnet_1b_cidr_block     =   "10.20.24.0/21"
    

基础设施部分的条件跳过

在特定环境变量文件中创建配置,并根据该变量决定是否创建或跳过该部分。通过这种方式,可以根据需要跳过基础设施的特定部分。

variable vpc_create {
   default = "true"
}

module "vpc" {
  source = "../modules/aws/network/vpc"
  enable = "${var.vpc_create}"
  vpc_cidr_block = "${var.vpc_cidr_block}"
  name = "${var.vpc_name}"
 }

 resource "aws_vpc" "vpc" {
    count                = "${var.enable == "true" ? 1 : 0}"
    cidr_block           = "${var.vpc_cidr_block}"
    enable_dns_support   = "true"
   enable_dns_hostnames = "true"
}

以下命令用于初始化和执行每个环境的基础设施更改,请切换到所需环境文件夹。

  terraform init -var-file=dev.variables.tfvar -backend-config=dev.backend.tfvar ../../deployment/

  terraform apply -var-file=dev.variables.tfvar ../../deployment

参考链接:https://github.com/mattyait/devops_terraform

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接