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_instance
、azurerm_resource_group
、google_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.tf
和 main.tf
。其中,variables.tf
包含了 Azure资源组所需的输入变量定义,例如 resource_group_location
、resource_group_name
、resource_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
纳入工作流中试一试,它会大大简化你的模块开发过程。