1.6.10.1. newres

newres 是一个用于 自动生成 Terraform 配置文件 的命令行工具。简单来说,给定一个特定的云资源类型(例如 AWS 的 EC2 实例、Azure 的虚拟机等),newres 会帮我们创建出该资源的 Terraform 模块代码框架,包括 variables.tf(输入变量定义)和 main.tf(资源定义)两个文件。借助 newres,我们无需手动编写繁琐的变量声明和资源块,它会依据提供的资源类型 自动生成所需的变量、属性以及嵌套块,并附带官方文档中的说明,使我们能更快捷地开始编写 Terraform 配置。这一特性极大减少了手工配置的时间开销和出错几率。

newres 支持主流的 Terraform 提供商(providers),例如 AWS、Azure、Google Cloud 等等。无论是公有云(如 AWS)还是第三方提供商(如 Kubernetes、Helm),它都可以根据资源类型生成对应的 Terraform 模块代码。目前 newres 已内置支持十多个官方提供商,包括阿里云、AWS、AzureRM、AzureAD、GCP、Kubernetes、Helm 等。这意味着不论团队使用哪种云平台,newres 都能适配并生成相应资源的配置框架,大大提高了工具的通用性。

1.6.10.1.1. 为什么需要这个工具

对于 Terraform 模块的开发者来说,没有 newres 之前,添加新资源往往是一个繁琐且容易出错的过程。我们来看看在实际项目中手工创建资源目录和结构时遇到的典型问题:

  • 重复劳动繁重:每当需要在模块中增加一个新资源,开发者都必须新建/修改 variables.tf 来添加对应的变量,并在 main.tf 中编写资源块。对于每个输入变量,需要手动写类型、默认值、描述说明等;对于资源块,需要填写所有必需参数、处理可选参数的默认值,甚至嵌套块的动态迭代。这种机械式的重复劳动既耗费时间,又容易让人产生疏漏。
  • 容易遗漏和配置不当:云资源通常有许多参数,其中有些是必填的,有些是可选的(带默认值或可为空)。手动编写时,稍有不慎就可能漏掉某个必要参数,或误把必填当可选反而没提供,导致部署时报错。另外,不同人对哪些可选参数应该显式配置看法不同,可能甲开发者觉得不需要管的属性,乙开发者却认为非常重要。如果没有统一规范,手工过程很难保证不遗漏关键配置。
  • 命名和结构不一致:团队协作中,不同工程师习惯不同,可能变量命名各式各样,资源块内部组织顺序也不同。例如,同一类型的资源,A工程师定义变量用短名称,B工程师则用带前缀的长名称;或者对嵌套块,有人直接在资源内展开写,有人偏好用 dynamic。这些不一致在代码评审和后期运维中会造成困扰,需要花额外精力调整为一致风格。
  • 参考文档成本高:正确编写 Terraform 代码离不开查阅官方文档。每次增加资源都要翻阅提供商文档,找出每个参数的意义和默认值,确定哪些是必需的。这本身非常耗时,而且如果开发者偷懒不写注释,模块使用者以后也得再去查文档理解参数含义,知识无法在代码中沉淀下来。
  • 规模效应的问题:在大型 Terraform 项目中,这些问题会被放大。假如一个模块需要陆续支持几十种资源,手工管理变量和资源文件将难以为继。“人为复制粘贴+修改”的方式不仅效率低,还可能在某次修改中破坏已有的格式或变量定义,埋下隐患。

newres 的出现正是为了解决上述问题,让 Terraform 模块开发事半功倍。它通过自动化来消除手工流程的痛点

  • 自动完成样板代码newres 会根据资源类型自动生成一整套变量定义和资源块配置,省去了人工重复书写的工作。开发者再也不用一个个地敲出 variable "" {} 块,也不用担心资源块的基本结构怎么写——这些样板代码工具都替你准备好了。这极大降低了新建资源时的心智负担,你可以把精力更多放在资源配置的业务逻辑上,而不是样板细节上。
  • 不遗漏关键参数:由于 newres 直接利用 Terraform 提供商的 Schema 数据和文档进行生成,它能确保所有必需参数都被生成为输入变量,没有缺失。可选参数则通常会赋予合理的默认值(比如 null)以保持 Terraform 配置的完整性。这意味着用 newres 创建的资源块“开箱即用”,至少在语法和基本必需属性上是正确完整的,不会因为少写了某个属性而导致 Terraform 执行错误。当然,有些高级场景下仍需手工调整,但借助工具产出的起点,开发者不太会漏掉关键配置
  • 统一规范newres 对变量命名、有默认值的可选参数处理、嵌套块展开方式等都有固定模式。例如上文提到,它倾向于在变量名前加上资源前缀并使用下划线分隔,这自然形成了跨模块的一致风格。另外,对于嵌套块,newres 会采用通用的 dynamic 语法来处理可选的子配置,哪怕该嵌套块实际上是必需的,它也统一这样生成以简化代码结构。这些模式一旦形成共识,大家使用 newres 就相当于遵循了同一套编码规范,减少了风格不一致的问题。在大型团队中,工具即规范,有了工具自动输出的标准格式,代码评审时就不再纠结风格,而能专注于逻辑本身。
  • 内置文档和提示:每个由 newres 生成的变量都会带有描述字符串,说明其用途、是否必需以及默认值含义。这些描述直接来源于官方文档或提供商代码注释,准确而详细。这相当于在代码里自带了文档,既帮助开发者自己理解,也方便日后他人维护时快速了解变量作用。手工编写时往往容易忽略写描述或者一笔带过,而 newres 自动填充的说明使模块的自描述性大大提高。对于初学者来说,看到这些说明可以更容易明白 Terraform 配置中的每个部分是做什么用的,从而学习规范的写法。
  • 提升开发速度,减少出错:综合来说,newres 把一个原本可能需要几十分钟的小型“开发任务”压缩到了一条命令。尤其在处理有上百个参数的大型资源时,人工一个个敲变量既枯燥又容易出错,而使用工具生成可以几秒钟完成初稿。这样的效率提升在迭代节奏快的项目中非常宝贵。此外,由于生成的是机器整理过的配置,语法和基本配置项都会正确无误,减少了低级失误的可能。这对于CI/CD流程也是有利的——更少的拼写错误或遗漏意味着更顺畅的流水线执行。

总而言之,我们需要 newres,因为它针对 Terraform 模块开发中“创建新资源”这一高频且容易出问题的环节提供了完善的解决方案。它让开发者从重复劳动中解放出来,确保代码符合规范且完整,为模块治理保驾护航。在大规模 Terraform 项目中,newres 不仅是一个提高效率的工具,更是保障规范落地的利器。

(需要注意的是,尽管 newres 自动化程度高,我们依然应当对生成的代码进行复核。一些 Terraform 提供商的文档结构不够统一,可能导致 newres 无法百分之百正确解析所有字段。因此在使用生成结果时,还是要结合实际需求和最新文档检查,以确保配置准确无误。)

1.6.10.1.2. 使用方法

介绍了这么多原理和好处,下面我们来看一下 newres 的实际使用方法。总体而言,它的使用非常简洁:通过 Go 工具安装,随后运行一个命令即可生成代码骨架。

安装方式:由于 newres 是用 Go 语言编写的,安装时需要系统已有 Go 开发环境。假设已经安装了 Go,可以直接通过下面的命令获取 newres 的最新版本:

go install github.com/lonegunmanb/newres/v3@latest

newres 提供了两种生成模式

  • MultipleVariables 模式(默认):为资源的每个参数和子块分别生成独立的 variable 输入变量。这种模式下,会在 variables.tf 中列出多个变量块,每个变量对应 Terraform 资源的一个属性或嵌套配置。对于复杂资源,可能会生成相当多的变量,但优点是清晰、直观,各参数可以独立传值。
  • UniVariable 模式:顾名思义,只生成单一变量,将整个资源的所有配置都嵌入到一个复合类型的变量中(包括可能的嵌套块作为变量的子属性)。这种模式更像是把资源的配置封装成一个对象,通过一个变量传入,适合某些希望简化接口的场景。

通过这两种模式的选择,newres 可以适应不同团队的 Terraform 模块设计风格:如果喜欢细粒度的参数就用默认的多变量模式,如果偏好单一对象输入则可以使用 -u 参数开启 UniVariable 模式。

基本用法:newres 的命令行参数非常简单,主要有三个参数需要关注:

  • -dir [目录路径]:必需参数,指定生成的 Terraform 配置文件存放的目录路径。例如写 -dir ./myresource 则表示将在当前目录下的 “myresource” 子目录生成文件。如果目录不存在,需先手动创建。经常我们会将目标目录指向某个 Terraform 模块目录。
  • -r [资源类型]:必需参数,指定要生成配置的 资源类型。资源类型的写法和在 Terraform 配置中的资源块类型一致,例如 aws_instanceazurerm_resource_groupgoogle_compute_instance 等。通过这个参数,newres 知道我们想为哪个具体资源生成模板。
  • -u:可选参数,表示是否使用 UniVariable 模式。加上此参数则使用单变量模式生成,不加则默认为多变量模式生成(即每个属性各自一个变量)。

除了以上核心参数,newres 对某些特定提供商资源还有附加选项。例如针对 Azure 的 AzAPI 提供商资源(azapi_resource),需要额外使用 --azapi-resource-type 参数提供具体的资源类型字符串(如 "Microsoft.Resources/resourceGroups@2021-04-01")。大多数常规资源无需这个选项,只有在生成 AzAPI 通用资源时才用得到。

使用示例:假设我们正在编写一个 Azure 资源组模块,希望快速生成资源组的基础 Terraform 模板。在终端进入该模块目录后,可以运行以下命令:

newres -dir . -r azurerm_resource_group

这里我们指定了当前目录 (./) 为输出目录,资源类型为 azurerm_resource_group(Azure Resource Manager 提供商的资源组)。执行后,newres 将自动在当前目录生成(或更新)两个文件:variables.tfmain.tf。其中,variables.tf 包含了 Azure资源组所需的输入变量定义,例如 resource_group_locationresource_group_nameresource_group_tags 等,每个变量都带有类型、默认值(如果适用)和描述。而 main.tf 则包含了对应的资源块:

resource "azurerm_resource_group" "this" {
  location   = var.resource_group_location
  name       = var.resource_group_name
  managed_by = var.resource_group_managed_by
  tags       = var.resource_group_tags

  dynamic "timeouts" {
    for_each = var.resource_group_timeouts == null ? [] : [var.resource_group_timeouts]

    content {
      create = timeouts.value.create
      delete = timeouts.value.delete
      read   = timeouts.value.read
      update = timeouts.value.update
    }
  }
}

从结果可以看到,基本的属性如区域(location)、名称(name)、标签(tags)都已经绑定到了相应变量,关联关系和语法都已经就绪。

同时,variables.tf 文件中:

variable "resource_group_location" {
  type        = string
  description = "(Required) The Azure Region where the Resource Group should exist. Changing this forces a new Resource Group to be created."
  nullable    = false
}

variable "resource_group_name" {
  type        = string
  description = "(Required) The Name which should be used for this Resource Group. Changing this forces a new Resource Group to be created."
  nullable    = false
}

variable "resource_group_managed_by" {
  type        = string
  default     = null
  description = "(Optional) The ID of the resource or application that manages this Resource Group."
}

variable "resource_group_tags" {
  type        = map(string)
  default     = null
  description = "(Optional) A mapping of tags which should be assigned to the Resource Group."
}

variable "resource_group_timeouts" {
  type = object({
    create = optional(string)
    delete = optional(string)
    read   = optional(string)
    update = optional(string)
  })
  default     = null
  description = <<-EOT
 - `create` - (Defaults to 90 minutes) Used when creating the Resource Group.
 - `delete` - (Defaults to 90 minutes) Used when deleting the Resource Group.
 - `read` - (Defaults to 5 minutes) Used when retrieving the Resource Group.
 - `update` - (Defaults to 90 minutes) Used when updating the Resource Group.
EOT
}

开发者此时只需检查变量默认值或描述是否需要调整,并根据需求补充任何模块特有的逻辑(比如添加 output.tf 导出一些值)即可。整个过程省去了手动查文档、写样板的过程。

需要强调的是,如果我们多次运行 newres 命令生成不同资源类型且目标目录相同,newres 会将新生成的配置追加到已有文件中,而不是覆盖。也就是说,第一次运行可能生成了资源组相关内容,第二次运行如果指定了另一个资源(例如 azurerm_storage_account 存储账户),newres 会在原来的 variables.tf 里再添加存储账户需要的变量,在原来的 main.tf 里追加存储账户的资源块。如此一来,我们可以通过多次调用一步步累积生成一个模块支持多个资源的完整配置而无需手工合并,非常适合逐步拓展模块功能的场景。当然,在追加生成后,建议通读一遍合并后的文件,确保不同资源的变量命名没有冲突,顺序符合团队习惯。

其他示范: 除了 Azure 的例子,我们来看看另一个场景。如果要创建 AWS 的 EC2实例模块,可以执行:

newres -dir ./aws_ec2_module -r aws_instance

这会在指定目录生成 EC2实例的 Terraform 资源块:

resource "aws_instance" "this" {
  ami                                  = var.instance_ami
  associate_public_ip_address          = var.instance_associate_public_ip_address
  availability_zone                    = var.instance_availability_zone
  cpu_core_count                       = var.instance_cpu_core_count
  cpu_threads_per_core                 = var.instance_cpu_threads_per_core
  disable_api_stop                     = var.instance_disable_api_stop
  disable_api_termination              = var.instance_disable_api_termination
  ebs_optimized                        = var.instance_ebs_optimized
  enable_primary_ipv6                  = var.instance_enable_primary_ipv6
  get_password_data                    = var.instance_get_password_data
  hibernation                          = var.instance_hibernation
  host_id                              = var.instance_host_id
  host_resource_group_arn              = var.instance_host_resource_group_arn
  iam_instance_profile                 = var.instance_iam_instance_profile
  instance_initiated_shutdown_behavior = var.instance_instance_initiated_shutdown_behavior
  instance_type                        = var.instance_instance_type
  ipv6_address_count                   = var.instance_ipv6_address_count
  ipv6_addresses                       = var.instance_ipv6_addresses
  key_name                             = var.instance_key_name
  monitoring                           = var.instance_monitoring
  placement_group                      = var.instance_placement_group
  placement_partition_number           = var.instance_placement_partition_number
  private_ip                           = var.instance_private_ip
  secondary_private_ips                = var.instance_secondary_private_ips
  security_groups                      = var.instance_security_groups
  source_dest_check                    = var.instance_source_dest_check
  subnet_id                            = var.instance_subnet_id
  tags                                 = var.instance_tags
  tags_all                             = var.instance_tags_all
  tenancy                              = var.instance_tenancy
  user_data                            = var.instance_user_data
  user_data_base64                     = var.instance_user_data_base64
  user_data_replace_on_change          = var.instance_user_data_replace_on_change
  volume_tags                          = var.instance_volume_tags
  vpc_security_group_ids               = var.instance_vpc_security_group_ids

  dynamic "capacity_reservation_specification" {
    for_each = var.instance_capacity_reservation_specification == null ? [] : [var.instance_capacity_reservation_specification]

    content {
      capacity_reservation_preference = capacity_reservation_specification.value.capacity_reservation_preference

      dynamic "capacity_reservation_target" {
        for_each = capacity_reservation_specification.value.capacity_reservation_target == null ? [] : [capacity_reservation_specification.value.capacity_reservation_target]

        content {
          capacity_reservation_id                 = capacity_reservation_target.value.capacity_reservation_id
          capacity_reservation_resource_group_arn = capacity_reservation_target.value.capacity_reservation_resource_group_arn
        }
      }
    }
  }
  dynamic "cpu_options" {
    for_each = var.instance_cpu_options == null ? [] : [var.instance_cpu_options]

    content {
      amd_sev_snp      = cpu_options.value.amd_sev_snp
      core_count       = cpu_options.value.core_count
      threads_per_core = cpu_options.value.threads_per_core
    }
  }
  dynamic "credit_specification" {
    for_each = var.instance_credit_specification == null ? [] : [var.instance_credit_specification]

    content {
      cpu_credits = credit_specification.value.cpu_credits
    }
  }
  dynamic "ebs_block_device" {
    for_each = var.instance_ebs_block_device == null ? [] : var.instance_ebs_block_device

    content {
      device_name           = ebs_block_device.value.device_name
      delete_on_termination = ebs_block_device.value.delete_on_termination
      encrypted             = ebs_block_device.value.encrypted
      iops                  = ebs_block_device.value.iops
      kms_key_id            = ebs_block_device.value.kms_key_id
      snapshot_id           = ebs_block_device.value.snapshot_id
      tags                  = ebs_block_device.value.tags
      tags_all              = ebs_block_device.value.tags_all
      throughput            = ebs_block_device.value.throughput
      volume_size           = ebs_block_device.value.volume_size
      volume_type           = ebs_block_device.value.volume_type
    }
  }
  dynamic "enclave_options" {
    for_each = var.instance_enclave_options == null ? [] : [var.instance_enclave_options]

    content {
      enabled = enclave_options.value.enabled
    }
  }
  dynamic "ephemeral_block_device" {
    for_each = var.instance_ephemeral_block_device == null ? [] : var.instance_ephemeral_block_device

    content {
      device_name  = ephemeral_block_device.value.device_name
      no_device    = ephemeral_block_device.value.no_device
      virtual_name = ephemeral_block_device.value.virtual_name
    }
  }
  dynamic "instance_market_options" {
    for_each = var.instance_instance_market_options == null ? [] : [var.instance_instance_market_options]

    content {
      market_type = instance_market_options.value.market_type

      dynamic "spot_options" {
        for_each = instance_market_options.value.spot_options == null ? [] : [instance_market_options.value.spot_options]

        content {
          instance_interruption_behavior = spot_options.value.instance_interruption_behavior
          max_price                      = spot_options.value.max_price
          spot_instance_type             = spot_options.value.spot_instance_type
          valid_until                    = spot_options.value.valid_until
        }
      }
    }
  }
  dynamic "launch_template" {
    for_each = var.instance_launch_template == null ? [] : [var.instance_launch_template]

    content {
      id      = launch_template.value.id
      name    = launch_template.value.name
      version = launch_template.value.version
    }
  }
  dynamic "maintenance_options" {
    for_each = var.instance_maintenance_options == null ? [] : [var.instance_maintenance_options]

    content {
      auto_recovery = maintenance_options.value.auto_recovery
    }
  }
  dynamic "metadata_options" {
    for_each = var.instance_metadata_options == null ? [] : [var.instance_metadata_options]

    content {
      http_endpoint               = metadata_options.value.http_endpoint
      http_protocol_ipv6          = metadata_options.value.http_protocol_ipv6
      http_put_response_hop_limit = metadata_options.value.http_put_response_hop_limit
      http_tokens                 = metadata_options.value.http_tokens
      instance_metadata_tags      = metadata_options.value.instance_metadata_tags
    }
  }
  dynamic "network_interface" {
    for_each = var.instance_network_interface == null ? [] : var.instance_network_interface

    content {
      device_index          = network_interface.value.device_index
      network_interface_id  = network_interface.value.network_interface_id
      delete_on_termination = network_interface.value.delete_on_termination
      network_card_index    = network_interface.value.network_card_index
    }
  }
  dynamic "private_dns_name_options" {
    for_each = var.instance_private_dns_name_options == null ? [] : [var.instance_private_dns_name_options]

    content {
      enable_resource_name_dns_a_record    = private_dns_name_options.value.enable_resource_name_dns_a_record
      enable_resource_name_dns_aaaa_record = private_dns_name_options.value.enable_resource_name_dns_aaaa_record
      hostname_type                        = private_dns_name_options.value.hostname_type
    }
  }
  dynamic "root_block_device" {
    for_each = var.instance_root_block_device == null ? [] : [var.instance_root_block_device]

    content {
      delete_on_termination = root_block_device.value.delete_on_termination
      encrypted             = root_block_device.value.encrypted
      iops                  = root_block_device.value.iops
      kms_key_id            = root_block_device.value.kms_key_id
      tags                  = root_block_device.value.tags
      tags_all              = root_block_device.value.tags_all
      throughput            = root_block_device.value.throughput
      volume_size           = root_block_device.value.volume_size
      volume_type           = root_block_device.value.volume_type
    }
  }
  dynamic "timeouts" {
    for_each = var.instance_timeouts == null ? [] : [var.instance_timeouts]

    content {
      create = timeouts.value.create
      delete = timeouts.value.delete
      read   = timeouts.value.read
      update = timeouts.value.update
    }
  }
}

同理,variables.tf 中会出现诸如 instance_ami(AMI ID)、instance_type(实例类型)、instance_tags 等变量,main.tf 则有一个 aws_instance 资源块,属性都映射到前述变量。通过这些变量名称,我们也能体会到 newres 的命名风格:<资源>_<属性>的形式,使变量含义清晰且避免冲突。如果希望把所有输入打包成一个变量对象,也可以在命令中加入 -u 参数试试看效果。

对 AzAPI 的特殊处理

AzAPI 是微软近年来开发和维护的一种新的调用 Azure Resource Management API 的 Terraform Provider,不同于 AzureRM Provider 的是,AzAPI 主要封装了对 Azure API 的调用,它更类似于 Bicep,更加的动态,不需要等待 Provider 开发团队开发对应的 Resource 逻辑,通过阅读 API 定义就可以自行构造资源块。newres 针对 AzAPI 做了特殊处理,例如运行以下命令:

newres -r azapi_resource --azapi-resource-type="Microsoft.CognitiveServices/accounts@2024-10-01" -dir .

得到的 Terraform 代码是:

resource "azapi_resource" "this" {
  location  = var.resource_location
  name      = var.resource_name
  parent_id = var.resource_parent_id
  type      = "Microsoft.CognitiveServices/accounts@2024-10-01"
  body      = var.resource_body
  tags      = var.resource_tags

  dynamic "identity" {
    for_each = var.resource_identity == null ? [] : [var.resource_identity]

    content {
      type = identity.value.type

      dynamic "userAssignedIdentities" {
        for_each = identity.value.userAssignedIdentities == null ? {} : identity.value.userAssignedIdentities

        content {}
      }
    }
  }
}

重点是生成的 var.resource_body

variable "resource_body" {
  type = object({
    kind = optional(string)
    properties = optional(object({
      allowedFqdnList               = optional(list(string))
      customSubDomainName           = optional(string)
      disableLocalAuth              = optional(bool)
      dynamicThrottlingEnabled      = optional(bool)
      migrationToken                = optional(string)
      publicNetworkAccess           = optional(string)
      restore                       = optional(bool)
      restrictOutboundNetworkAccess = optional(bool)
      amlWorkspace = optional(object({
        identityClientId = optional(string)
        resourceId       = optional(string)
      }))
      apiProperties = optional(object({
        aadClientId                    = optional(string)
        aadTenantId                    = optional(string)
        eventHubConnectionString       = optional(string)
        qnaAzureSearchEndpointId       = optional(string)
        qnaAzureSearchEndpointKey      = optional(string)
        qnaRuntimeEndpoint             = optional(string)
        statisticsEnabled              = optional(bool)
        storageAccountConnectionString = optional(string)
        superUser                      = optional(string)
        websiteName                    = optional(string)
      }))
      encryption = optional(object({
        keySource = optional(string)
        keyVaultProperties = optional(object({
          identityClientId = optional(string)
          keyName          = optional(string)
          keyVaultUri      = optional(string)
          keyVersion       = optional(string)
        }))
      }))
      locations = optional(object({
        routingMethod = optional(string)
        regions = optional(list(object({
          customsubdomain = string
          name            = string
          value           = number
        })))
      }))
      networkAcls = optional(object({
        bypass        = optional(string)
        defaultAction = optional(string)
        ipRules = optional(list(object({
          value = string
        })))
        virtualNetworkRules = optional(list(object({
          id                               = string
          ignoreMissingVnetServiceEndpoint = bool
          state                            = string
        })))
      }))
      raiMonitorConfig = optional(object({
        adxStorageResourceId = optional(string)
        identityClientId     = optional(string)
      }))
      userOwnedStorage = optional(list(object({
        identityClientId = string
        resourceId       = string
      })))
    }))
    sku = optional(object({
      capacity = optional(number)
      family   = optional(string)
      name     = string
      size     = optional(string)
      tier     = optional(string)
    }))
  })
  nullable    = false
}

newres 会把 API 中该资源的请求结构映射成 object 类型的输入变量块,极大地加速了用户编写正确的 azapi_resource 块的流程。

1.6.10.1.3. 小结

总结: 使用 newres 非常直观:安装→运行命令→查看生成结果。对于Terraform的初学者来说,这个工具能帮助快速了解一个资源完整的配置需要哪些要素;对于有经验的工程师,它能节省大量机械工作,把注意力集中在架构设计和参数抉择上。在大规模模块治理的实践中,newres 已经展示出它的价值——既充当了标准的制定者(因为工具输出即规范),也扮演了高效助手的角色。正因如此,如果你正在编写 Terraform 模块,特别是涉及很多资源和变量的场景,不妨将 newres 纳入工作流中试一试,它会大大简化你的模块开发过程。

results matching ""

    No results matching ""