Terraform (Hashicorp) 的不同环境

Different environments for Terraform (Hashicorp)

我一直在使用 Terraform 构建我的 AWS 堆栈,并且一直很享受。如果要在商业环境中使用,则需要为不同的环境(例如 QA、STAGING、PROD)重复使用配置。

我怎样才能做到这一点?我是否需要创建一个包装器脚本来调用 terraform 的 cli,同时为每个环境传递不同的状态文件,如下所示?我想知道 Terraform 是否提供了更原生的解决方案。

terraform apply -state=qa.tfstate

无需制作包装脚本。我们所做的是将我们的 env 拆分成一个模块,然后有一个顶级 terraform 文件,我们只需在其中为每个环境导入该模块。只要您的模块设置能够获取足够的变量,通常是 env_name 和其他一些变量,就可以了。举个例子

# project/main.tf
module "dev" {
    source "./env"

    env = "dev"
    aws_ssh_keyname = "dev_ssh"
}

module "stage" {
    source "./env"

    env = "stage"
    aws_ssh_keyname = "stage_ssh"
}

# Then in project/env/main.tf
# All the resources would be defined in here
# along with variables for env and aws_ssh_keyname, etc.

编辑 2020/03/01

这个答案在这一点上已经很老了,但值得更新。对 dev 和 stage 共享同一个状态文件的批评是不好的,这是一个角度问题。对于上面提供的确切代码,它 完全 有效,因为开发和阶段也共享相同的代码。因此 "breaking dev will wreck your stage," 是正确的。我在写这个答案时没有注意到的关键是 source "./env" 也可以写成 source "git::https://example.com/network.git//modules/vpc?ref=v1.2.0"

这样做会使您的整个存储库成为 TF 脚本的子模块,允许您将一个分支拆分为 QA 分支,然后将引用标记为生产环境。这避免了通过更改 dev 破坏您的暂存环境的问题。

下一状态文件共享。我说这是一个角度问题,因为使用一个 运行 就可以更新您的所有环境。在一家小公司中,在推动变更时节省时间可能很有用,如果您小心的话,使用 --target 的一些技巧通常足以加快流程,甚至确实需要这样做。我们发现从一个地方和一个地形 运行 管理所有内容比在不同环境中应用可能略有不同的多种不同配置更不容易出错。将它们全部放在一个状态文件中迫使我们更加严格地了解真正需要成为变量的内容 v.s。对我们的目的来说,这太过分了。它还非常强烈地阻止我们让我们的环境彼此相距太远。当您 terraform plan 输出显示 2k 行时,差异主要是因为 dev 和 stage 看起来一点也不像 prod 挫折因素本身就鼓励我们的团队恢复理智。

一个非常有力的反驳论点是,如果你在一家大公司,那里的各种合规性规则阻止你同时接触开发/阶段/生产。在这种情况下,最好拆分您的状态文件,只需确保如何您运行宁terraform apply是脚本化的。否则你 运行 当有人说 "Oh I just need to --target just this one thing in staging. We'll fix it next sprint, promise." 我已经多次看到这种螺旋式快速上升时,那些状态文件分开的真正风险,充其量使环境之间的任何类型的比较都值得怀疑。

随着您扩大 Terraform 的使用,您将需要共享状态(在开发人员、构建流程和不同项目之间),支持多个环境和区域。 为此,您需要使用远程状态。 在执行地形之前,您需要设置状态。 (我正在使用 powershell)

$environment="devtestexample"
$region     ="eu-west-1"
$remote_state_bucket = "${environment}-terraform-state"
$bucket_key = "yoursharedobject.$region.tfstate"

aws s3 ls "s3://$remote_state_bucket"|out-null
if ($lastexitcode)
{
   aws s3 mb "s3://$remote_state_bucket"
}

terraform remote config -backend S3 -backend-config="bucket=$remote_state_bucket"  -backend-config="key=$bucket_key" -backend-config="region=$region"
#(see here: https://www.terraform.io/docs/commands/remote-config.html)

terraform apply -var='environment=$environment' -var='region=$region'

您的状态现在按区域、按环境存储在 S3 中,然后您可以在其他 tf 项目中访问此状态。

我建议你看看 hashicorp best-practices repo,它有一个很好的设置来处理不同的环境(类似于 James Woolfenden 的建议)。

我们正在使用类似的设置,而且效果很好。但是,此最佳实践存储库假设您使用的是 Atlas,而我们并未使用。我们已经创建了一个相当精细的 Rakefile,它基本上(再次按照最佳实践 repo)获取 /terraform/providers/aws 的所有子文件夹,并使用命名空间将它们公开为不同的构建。所以我们的 rake -T 输出将列出以下任务:

us_east_1_prod:init
us_east_1_prod:plan
us_east_1_prod:apply

us_east_1_staging:init
us_east_1_staging:plan
us_east_1_staging:apply

这种分离可以防止 dev 独有的更改意外影响(或更糟,破坏)prod 中的某些内容,因为它是一个不同的状态文件。它还允许在将 dev/staging 实际应用到产品之前测试 dev/staging 中的更改。

此外,我最近偶然发现了这篇小文章,它基本上展示了如果将所有内容放在一起可能会发生什么: https://charity.wtf/2016/03/30/terraform-vpc-and-why-you-want-a-tfstate-file-per-env/

Paul 的模块解决方案是正确的想法。但是,我强烈建议 against 在同一个 Terraform 文件中定义所有环境(例如 QA、暂存、生产)。如果你这样做了,那么每当你对暂存进行更改时,你也有可能不小心破坏生产,这部分地破坏了首先保持这些环境隔离的意义!请参阅 Terraform, VPC, and why you want a tfstate file per env,了解有关可能出错的内容的丰富多彩的讨论。

我总是建议将每个环境的 Terraform 代码存储在单独的文件夹中。事实上,您甚至可能希望将每个 "component"(例如数据库、VPC、单个应用程序)的 Terraform 代码存储在单独的文件夹中。同样,原因是隔离:在对单个应用程序进行更改时(您每天可能会更改 10 次),您不希望将整个 VPC 置于风险之中(您可能永远不会更改)。

因此,我的典型文件布局如下所示:

stage
  └ vpc
     └ main.tf
     └ vars.tf
     └ outputs.tf
  └ app
  └ db
prod
  └ vpc
  └ app
  └ db
global
  └ s3
  └ iam

staging 环境的所有 Terraform 代码都进入 stage 文件夹,prod 环境的所有代码都进入 prod 文件夹,以及环境之外的所有代码(例如 IAM 用户、S3 存储桶)进入 global 文件夹。

有关详细信息,请查看 How to manage Terraform state. For a deeper look at Terraform best practices, check out the book Terraform: Up & Running

请注意,从版本 0.10.0 开始,Terraform 现在支持工作区的概念(0.9.x 中的环境)。

工作区是 Terraform 状态的命名容器。对于多个工作区,可以使用 Terraform 配置的单个目录来管理多组不同的基础设施资源。

在此处查看更多信息:https://www.terraform.io/docs/state/workspaces.html

这个话题中有很多好的答案。让我也贡献一个对我和其他一些团队有用的想法。

我们的想法是拥有一个包含整个基础结构代码的 "umbrella" 项目。

每个环境的 terraform 文件只包含一个模块 - "main"。

然后"main"将包含资源和其他模块

- terraform_project
- env
  - dev01        <-- Terraform home, run from here 
    - .terraform    <-- git ignored of course
    - dev01.tf  <-- backend, env config, includes always _only_ the main module
  - dev02
    - .terraform
    - dev02.tf 
  - stg01
    - .terraform
    - stg01.tf
  - prd01
    - .terraform
    - prd01.tf 
- main        <-- main umbrella module
   - main.tf
   - variables.tf         
- modules         <-- submodules of the main module
  - module_a
  - module_b
  - module_c

示例环境主文件(例如 dev01.tf)如下所示

provider "azurerm" {
  version = "~>1.42.0"
}

terraform {
  backend "azurerm" {
    resource_group_name  = "tradelens-host-rg"
    storage_account_name = "stterraformstate001"
    container_name       = "terraformstate"
    key                  = "dev.terraform.terraformstate"
  }
}

module "main" {
  source               = "../../main"
  subscription_id      = "000000000-0000-0000-0000-00000000000"
  project_name         = "tlens"
  environment_name     = "dev"
  resource_group_name  = "tradelens-main-dev"
  tenant_id            = "790fd69f-41a3-4b51-8a42-685767c7d8zz"
  location             = "West Europe"
  developers_object_id = "58968a05-dc52-4b69-a7df-ff99f01e12zz"
  terraform_sp_app_id  = "8afb2166-9168-4919-ba27-6f3f9dfad3ff"

  kubernetes_version      = "1.14.8"
  kuberenetes_vm_size     = "Standard_B2ms"
  kuberenetes_nodes_count = 4

  enable_ddos_protection = false
  enable_waf             = false
}

多亏了你:

  • 每个环境的 Terraform 远程状态文件可以有单独的后端
  • 能够针对不同的环境使用单独的系统帐户
  • 能够在每个环境中使用不同版本的提供程序和 Terraform 本身(并逐一升级)
  • 确保为每个环境提供所有必需的属性(如果缺少环境 属性,Terraform 验证将不会通过)
  • 确保始终将所有 resources/modules 添加到所有环境中。不可能 "forget" 整个模块,因为只有一个模块。

Check a source blog post

Form terraform 版本 0.10+ 有一种使用 Workspace 命令维护状态文件的方法

   $ terraform workspace list // The command will list all existing workspaces
   $ terraform workspace new <workspace_name> // The command will create a workspace
   $ terraform workspace select <workspace_name> // The command select a workspace
   $ terraform workspace delete <workspace_name> // The command delete a workspace

您需要做的第一件事是为您的环境创建每个工作区

   $ terraform workspace new dev

已创建并切换到工作区“dev”!

您现在位于一个新的空白工作区中。工作区隔离他们的状态, 所以如果你 运行 “terraform plan” Terraform 将看不到任何现有状态 对于此配置。

  $terraform workspace new test

已创建并切换到工作区“测试”!

您现在位于一个新的空白工作区中。工作区隔离他们的状态, 所以如果你 运行 “terraform plan” Terraform 将看不到任何现有状态 对于此配置。

 $terraform workspace new stage

已创建并切换到工作区“舞台”!

您现在位于一个新的空白工作区中。工作区隔离他们的状态, 所以如果你 运行 “terraform plan” Terraform 将看不到任何现有状态 对于此配置。

将创建后端 terraform.tfstate.d 目录

在它们下面你可以看到 3 个目录 - dev、test、stage,每个目录都会在其工作区下维护其状态文件。

现在您需要做的就是将 env-variable 文件移动到另一个文件夹中

每次执行 terraform plan 只保留一个变量文件,terraform apply

   main.tf
   dev_variable.tfvar
   output.tf

记得切换到正确的工作区以使用正确的环境状态文件

   $terraform workspace select test
   main.tf
   test_variable.tfvar
   output.tf

参考:https://dzone.com/articles/manage-multiple-environments-with-terraform-worksp

绝对没有必要为开发环境和生产环境使用单独的代码库。最佳实践表明(我的意思是 DRY)实际上你最好拥有一个代码库并且 简单地对其进行参数化 就像你在开发软件时通常所做的那样 - 你没有单独的应用程序的开发版本和应用程序的生产版本的文件夹。您只需要确保正确的部署方案。 Terraform 也是如此。考虑这个“Hello world”的想法:

terraform-project
├── etc
│   ├── backend
│   │   ├── dev.conf
│   │   └── prod.conf
│   └── tfvars
│       ├── dev.tfvars
│       └── prod.tfvars
└── src
    └── main.tf

etc/backend/dev.conf

的内容
storage_account_name = "tfremotestates"
container_name       = "tf-state.dev"
key                  = "terraform.tfstate"
access_key           = "****"

etc/backend/prod.conf

的内容
storage_account_name = "tfremotestates"
container_name       = "tf-state.prod"
key                  = "terraform.tfstate"
access_key           = "****"

etc/tfvars/dev.tfvars

的内容
environment = "dev"

etc/tfvars/prod.tfvars

的内容
environment = "prod"

src/main.tf

的内容
terraform {
  backend "azurerm" {
  }
}

provider "azurerm" {
  version = "~> 2.56.0"
  features {}
}

resource "azurerm_resource_group" "rg" {
  name     = "rg-${var.environment}"
  location = "us-east"
}

现在你只需要将适当的值传递给cli调用,例如:

export ENVIRONMENT=dev
terraform init -backend-config=etc/backends/${ENVIRONMENT}.conf
terraform apply -vars-file=etc/tfvars/${ENVIRONMENT}.tfvars

这样:

  • 我们为每个环境都有单独的状态文件(因此它们甚至可以部署在不同的 subscriptions/accounts)
  • 我们有相同的代码库,所以我们确信 dev 和 prod 之间的差异很小,我们可以在上线前依赖 dev 进行测试
  • 我们遵循 DRY 指令
  • 我们遵循 KISS 指令
  • 无需使用晦涩的“工作区”界面!

当然,为了使其完全安全,您应该结合某种 git 流程和代码审查,也许是一些静态或集成测试、自动部署过程等。但我认为这个解决方案是无需重复代码即可拥有多个 Terraform 环境的最佳方法,并且它已经为我们工作了好几年。

Hashicorp 推荐不同的状态文件和文件夹,如下所示:

├── assets
│   ├── index.html
├── prod
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfstate
│   └── terraform.tfvars
└── dev
   ├── main.tf
   ├── variables.tf
   ├── terraform.tfstate
   └── terraform.tfvars

甚至还有关于如何根据最佳实践重构单一配置以支持多个环境的文档。在这里查看: https://learn.hashicorp.com/tutorials/terraform/organize-configuration#variables-tf