使用 Terraform 时的最佳实践

Best practices when using Terraform

我正在将我们的基础设施换成 Terraform。 实际管理 Terraform 文件和状态的最佳实践是什么? 我意识到它是基础设施即代码,我会将我的 .tf 文件提交到 git,但我是否也提交 tfstate?它应该驻留在 S3 之类的地方吗?我最终希望 CI 来管理所有这些,但这太牵强了,需要我找出文件的移动部分。

我真的只是想看看人们实际上是如何在生产中使用这种类型的东西

我也正处于将现有 AWS 基础设施迁移到 Terraform 的状态,因此我的目标是在我开发时更新答案。

我一直严重依赖官方 Terraform examples 和多次试验和错误来充实我一直不确定的领域。

.tfstate 个文件

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

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

开发者控制

我们的目标是为开发人员提供对基础架构的更多控制,同时保持完整的审计(git 日志)和完整性检查更改(拉取请求)的能力。考虑到这一点,我的目标是新的基础设施工作流:

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

编辑 1 - 更新当前状态

自从开始回答这个问题以来,我写了很多 TF 代码,对我们的情况感觉更舒服了。我们一路上遇到了错误和限制,但我接受这是使用新的、快速变化的软件的一个特点。

布局

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

模块

下一步是创建一个 git 存储库来存储我们的 Terraform 模块。我们模块的顶级目录结构如下所示:

tree -L 1 .

结果:

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

每个设置一些正常的默认值,但将它们公开为可以被我们的 "glue".

覆盖的变量

胶水

我们的 glue 有第二个存储库,它使用了上述模块。它的布局符合我们的分类文件:

.
├── 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 public 密钥的区域级别;最后在我们的环境(devstgprod 等)中存储了我们的 VPC 设置、实例创建和对等连接等。

旁注: 如您所见,我违背了自己的建议,将 terraform.tfstate 保留在 git 中。这是我迁移到 S3 之前的临时措施,但适合我,因为我目前是唯一的开发人员。

后续步骤

这仍然是一个手动过程,还不是在 Jenkins 中,但我们正在移植一个相当大、复杂的基础设施,到目前为止一切顺利。就像我说的,很少有错误,但进展顺利!

编辑 2 - 更改

自从我写下这个最初的答案已经快一年了,Terraform 和我自己的状态都发生了显着变化。我现在处于使用 Terraform 管理 Azure 集群的新职位,Terraform 现在是 v0.10.7.

人们一再告诉我国家应该 进入 Git - 他们是正确的。我们将此作为一个临时措施,由一个依赖开发人员沟通和纪律的两人团队组成。对于一个更大的分布式团队,我们现在通过 DynamoDB 提供的 locking 充分利用 S3 中的远程状态。理想情况下,这将迁移到 consul,现在它是 v1.0 以减少跨云提供商。

模块

之前我们创建并使用了内部模块。情况仍然如此,但随着 Terraform registry 的出现和发展,我们尝试至少将它们用作基础。

文件结构

新职位的分类要简单得多,只有两个 infx 环境 - devprod。每个都有自己的变量和输出,重用我们上面创建的模块。 remote_state 提供程序还有助于在环境之间共享所创建资源的输出。我们的场景是将不同 Azure 资源组中的子域连接到全球托管的 TLD。

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

计划中

再次面对分布式团队的额外挑战,我们现在总是保存 terraform plan 命令的输出。我们可以检查并知道 运行 会是什么,而不会有 planapply 阶段之间发生某些变化的风险(尽管锁定有助于此)。请记住删除此计划文件,因为它可能包含纯文本 "secret" 变量。

总的来说,我们对 Terraform 非常满意,并会继续学习和改进添加的新功能。

以前 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

详情见docs

我们大量使用 Terraform,我们推荐的设置如下:

文件布局

我们强烈建议将每个环境(例如阶段、生产、质量检查)的 Terraform 代码存储在单独的模板集中(因此,单独的 .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

stage VPC 的所有 Terraform 代码都放在 stage 文件夹中,prod VPC 的所有代码都放在 prod 文件夹中,所有位于 VPC 外部的代码(例如 IAM 用户、SNS 主题、S3 存储桶)进入 global 文件夹。

请注意,按照惯例,我们通常将 Terraform 代码分解为 3 个文件:

  • vars.tf: 输入变量。
  • outputs.tf: 输出变量。
  • main.tf: 实际资源。

模块

通常,我们在两个文件夹中定义基础设施:

  1. infrastructure-modules:此文件夹包含小型、可重用、版本控制的模块。将每个模块视为如何创建单个基础设施(例如 VPC 或数据库)的蓝图。
  2. infrastructure-live:此文件夹包含实际的 运行ning 基础结构,它通过组合 infrastructure-modules 中的模块创建。将此文件夹中的代码视为您根据蓝图建造的实际房屋。

A Terraform module 只是文件夹中的任意一组 Terraform 模板。例如,我们可能在 infrastructure-modules 中有一个名为 vpc 的文件夹,它定义了单个 VPC 的所有路由表、子网、网关、ACL 等:

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

然后我们可以在 infrastructure-live/stageinfrastructure-live/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(参见 module sources)。 Git URL 的优点是我们可以指定特定的 git sha1 或标签 (ref=v0.0.4)。现在,我们不仅将我们的基础设施定义为一堆小模块,而且我们可以对这些模块进行版本控制,并根据需要仔细更新或回滚。

我们已经创建了许多可重用、经过测试和记录的 Infrastructure Packages 用于创建 VPC、Docker 集群、数据库等,在幕后,其中大部分只是版本化 Terraform 模块。

当您使用 Terraform 创建资源(例如 EC2 实例、数据库、VPC)时,它会在 .tfstate 文件中记录有关其创建内容的信息。要更改这些资源,您团队中的每个人都需要访问同一个 .tfstate 文件,但您不应将其签入 Git(参见 )。

相反,我们建议通过在您的 S3 存储桶中启用 Terraform Remote State, which will automatically push/pull the latest files every time you run Terraform. Make sure to enable versioning.tfstate 文件存储在 S3 中,这样您就可以回滚到较旧的 .tfstate 文件,以防您以某种方式损坏最新版本. 但是,重要提示:Terraform 不提供锁定。因此,如果两个团队成员 运行 terraform apply 同时处理同一个 .tfstate 文件,他们最终可能会覆盖彼此的更改。

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

为了解决这个问题,我们创建了一个名为 Terragrunt, which is a thin wrapper for Terraform that uses Amazon DynamoDB to provide locking (which should be completely free for most teams). Check out Add Automatic Remote State Locking and Configuration to Terraform with Terragrunt 的开源工具以获取更多信息。

进一步阅读

我们刚刚开始了一系列名为 A Comprehensive Guide to Terraform 的博客 post,其中详细描述了我们在现实世界中使用 Terraform 所学到的所有最佳实践。

更新:Terraform 博客综合指南 post 系列非常受欢迎,我们将其扩展为一本书,名为 Terraform: Up & Running!

@Yevgeny Brikman 进行了更深入的介绍,但专门回答了 OP 的问题:

What's the best practice for actually managing the terraform files and state?

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

but do I commit tfstate as well?

没有。

Should that reside somewhere like S3?

我知道这里有很多答案,但我的方法完全不同。

⁃   Modules
⁃   Environment management 
⁃   Separation of duties

模块

  1. 为资源的逻辑集合创建模块。 示例:如果您的目标是部署 API,这需要数据库、HA 虚拟机、自动缩放、DNS、PubSub 和对象存储,那么所有这些资源都应该在一个模块中进行模板化。
  2. 避免创建使用单一资源的模块。这可以而且已经完成,注册表中的许多模块都这样做,但这是一种有助于资源可访问性而不是基础设施编排的做法。 示例:AWS EC2 的一个模块通过使复杂的配置更易于调用来帮助用户访问 EC2,但是像 1. 中的示例这样的模块在编排应用程序、组件或服务驱动的基础设施时帮助用户。
    1. 避免在工作区中声明资源。这更多是关于保持代码整洁和有条理。由于模块很容易进行版本控制,因此您可以更好地控制发布。

环境管理

IaC 使 SDLC 过程与基础设施管理相关,期望拥有开发基础设施和开发应用程序环境是不正常的。

  1. 不要使用文件夹来管理您的 IaC 环境。这会导致漂移,因为您的基础设施没有通用模板。
  2. 请使用单个工作区和变量来控制环境规范。 示例:编写您的模块,以便当您更改环境变量(var.stage 很流行)时,计划会改变以满足您的要求。通常情况下,环境的变化应尽可能小,数量、暴露和容量通常是可变配置。 Dev 可能会在专用拓扑中部署 1 个具有 1 个内核和 1GB RAM 的 VM,但生产可能是 3 个具有 2 个内核和 4GB RAM 的 VM 以及额外的 public 拓扑。您当然可以有更多变化:开发可能 运行 数据库进程与应用程序在同一台服务器上以节省成本,但生产可能有专用的数据库实例。所有这些都可以通过更改单个变量、三元语句和插值来管理。

职责分离

如果您在小型组织或 运行宁个人基础设施,这并不适用,但它会帮助您管理您的操作。

  1. 按职责、责任或团队分解您的基础架构。 示例:中央 IT 控制底层共享服务(虚拟网络、子网、public IP 地址、日志组、治理资源、多租户数据库、共享密钥等),而 API 团队仅控制资源他们的服务(VM、LB、PubSub 等)所需的,并通过数据源和远程状态查找使用中央 IT 服务。
    1. 管理团队访问。 示例:中央 IT 可能拥有管理员权限,但 API 团队只能访问一组受限的 public 云 API。

这也有助于解决发布问题,因为您会发现一些资源很少更改,而其他资源则一直在更改。分离消除了风险和复杂性。

该策略与 AWS 的多账户策略相似。阅读更多信息。

CI/CD

这是一个单独的主题,但 Terraform 在良好的管道中运行良好。这里最常见的错误是将 CI 视为灵丹妙药。从技术上讲,Terraform 应该只在组装管道的阶段配置基础设施。这与通常验证和测试模板的 CI 阶段发生的事情是分开的。

N.B。手机写的,如有错误请见谅。

如果您仍在寻找更好的解决方案,请查看可以代替维护不同环境文件夹结构的工作区可以具有特定于工作区的变量。

因为Yevgeniy Brikman 最好有一个模块结构。

Before answers have been very solid and informative, I will try to add my 2 cents here

构建代码的常见建议

  1. 使用较少的资源更容易、更快速:

    • Cmdsterraform planterraform apply 都进行云 API 调用以验证资源状态。
    • 如果您的整个基础架构都在一个组合中,这可能需要很多时间(即使您在同一个文件夹中有多个文件)。
  2. 爆炸半径更小,资源更少:

    • 通过将不相关的资源放置在单独的组合(文件夹)中来将它们彼此隔离可以降低出现问题时的风险。
  3. 使用远程状态启动您的项目:

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

    • 与程序代码一样,Terraform 代码应该首先编写供人们阅读,当六个月后发生变化时,一致性将有所帮助。
    • 可以在 Terraform 状态文件 中移动资源,但如果结构和命名不一致,则可能更难做到。
  5. 资源模块尽可能简单。

  6. 不要 hard-code 可以作为变量传递或使用数据源发现的值。

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

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


示例:

It is easier and faster to work with smaller number of resources so below we present a recommended code layout.

注意:每个项目都有自己的特点,仅供参考,不严格遵循

.
├── 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

我相信在使用 Terraform 编排基础架构时几乎不需要遵循最佳实践

  1. Don't write the same code again ( Reusability)
  2. Keep environment configuration separate to maintain it easily.
  3. Use remote backend s3(encrypted) and dynamo DB to handle the concurrency locking
  4. Create a module and use that module in main infrastructure multiple time, its like a reusable function which can be called multiple time by passing different parameter.

处理多种环境

大多数时候推荐的方法是使用 terraform 'workspace' 来处理多个环境,但我相信工作空间的使用可能会因组织中的工作方式而异。 另一个是为每个环境(例如阶段、生产、QA)存储 Terraform 代码以分离环境状态。然而,在这种情况下,我们只是在许多地方复制了相同的代码。

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

我采用了一些不同的方法来处理和避免重复相同的地形代码,方法是保留在每个环境文件夹中,因为我相信大多数时候所有环境都是 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"
    

有条件地跳过基础设施部分

在 env 特定变量文件中创建配置,并根据该变量决定创建或跳过该部分。这样可以根据需要跳过基础设施的特定部分。

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"
}

需要以下命令来初始化和执行每个环境的基础设施更改,cd 到所需的环境文件夹。

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

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

For reference: https://github.com/mattyait/devops_terraform

我不喜欢子文件夹的想法,因为这会导致每个环境有不同的来源,而且这往往会漂移。

更好的方法是为所有环境(比如开发、预生产和生产)使用一个堆栈。要在单个环境中工作,请使用 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

要遵循的一些 Terraform 最佳实践:

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

    account_number=“123456789012” account_alias="mycompany"

  2. 运行 来自 docker 容器的 Terraform: Terraform 发布了一个官方 Docker 容器,可以让您轻松控制您可以 运行.

  3. 的版本

当您在 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

我想为这个话题做贡献。

  • 这很可能是 AWS S3+DynamoDB,除非您使用的是 Terraform Cloud。
  • 生产和非生产后端的独立基础设施(网络 + RBAC)。
  • 计划禁止从指定网络(例如部署代理池)外部访问状态文件(网络访问和 RBAC)。
  • 不要将 Terraform 后端基础设施与 运行 时代的环境保持在一起。单独使用 帐户。
  • 在您的 Terraform 后端启用对象版本控制以避免丢失更改和状态文件,并维护 Terraform 状态历史记录。

在某些特殊情况下,需要手动访问 Terraform 状态文件。诸如重构、中断更改或修复缺陷之类的事情将需要操作人员 运行ning Terraform 状态操作。对于此类情况,请计划使用堡垒主机、VPN 等对 Terraform 状态进行非常受控的访问。

查看 longer best practices blog 详细介绍此内容,包括 CI/CD 管道指南。

结合上述建议,使用 Terraform Cloud 管理和保存状态。