在 Terraform 0.12中,如何通过列表(对象)实现 for_each

我需要部署一个 GCP 计算实例列表。如何循环 for _ each 通过对象列表中的“ vms”,如下所示:

    "gcp_zone": "us-central1-a",
"image_name": "centos-cloud/centos-7",
"vms": [
{
"hostname": "test1-srfe",
"cpu": 1,
"ram": 4,
"hdd": 15,
"log_drive": 300,
"template": "Template-New",
"service_types": [
"sql",
"db01",
"db02"
]
},
{
"hostname": "test1-second",
"cpu": 1,
"ram": 4,
"hdd": 15,
"template": "APPs-Template",
"service_types": [
"configs"
]
}
]
}
172854 次浏览

Using the for_each block is pretty new and there's not too much documentation. Some of the best info comes from their announcement blog post: https://www.hashicorp.com/blog/hashicorp-terraform-0-12-preview-for-and-for-each/

Also make sure to check out the Dynamic Blocks section of their documentation: https://www.terraform.io/docs/configuration/expressions.html#dynamic-blocks

From what your example looks like you need to have a set of values for each instance that is created so you'll have a map of maps:

Below is an example I created using Terraform 0.12.12:

variable "hostnames" {
default = {
"one" = {
"name" = "one",
"machine" = "n1-standard-1",
"os" = "projects/coreos-cloud/global/images/coreos-stable-2247-5-0-v20191016",
"zone" = "us-central1-a"
},
"two" = {
"name" = "two",
"machine" = "n1-standard-2",
"os" = "projects/centos-cloud/global/images/centos-8-v20191018",
"zone" = "us-central1-b"
}
}
}


resource "google_compute_instance" "default" {
for_each = var.hostnames
name         = each.value.name
machine_type = each.value.machine
zone         = each.value.zone


boot_disk {
initialize_params {
image = each.value.os
}
}


scratch_disk {
}


network_interface {
network = "default"
}
}

Terraform plan output:

Terraform will perform the following actions:


# google_compute_instance.default["one"] will be created
+ resource "google_compute_instance" "default" {
+ can_ip_forward       = false
+ cpu_platform         = (known after apply)
+ deletion_protection  = false
+ guest_accelerator    = (known after apply)
+ id                   = (known after apply)
+ instance_id          = (known after apply)
+ label_fingerprint    = (known after apply)
+ machine_type         = "n1-standard-1"
+ metadata_fingerprint = (known after apply)
+ name                 = "one"
+ project              = (known after apply)
+ self_link            = (known after apply)
+ tags_fingerprint     = (known after apply)
+ zone                 = "us-central1-a"


+ boot_disk {
+ auto_delete                = true
+ device_name                = (known after apply)
+ disk_encryption_key_sha256 = (known after apply)
+ kms_key_self_link          = (known after apply)
+ mode                       = "READ_WRITE"
+ source                     = (known after apply)


+ initialize_params {
+ image  = "projects/coreos-cloud/global/images/coreos-stable-2247-5-0-v20191016"
+ labels = (known after apply)
+ size   = (known after apply)
+ type   = (known after apply)
}
}


+ network_interface {
+ address            = (known after apply)
+ name               = (known after apply)
+ network            = "default"
+ network_ip         = (known after apply)
+ subnetwork         = (known after apply)
+ subnetwork_project = (known after apply)
}


+ scheduling {
+ automatic_restart   = (known after apply)
+ on_host_maintenance = (known after apply)
+ preemptible         = (known after apply)


+ node_affinities {
+ key      = (known after apply)
+ operator = (known after apply)
+ values   = (known after apply)
}
}


+ scratch_disk {
+ interface = "SCSI"
}
}


# google_compute_instance.default["two"] will be created
+ resource "google_compute_instance" "default" {
+ can_ip_forward       = false
+ cpu_platform         = (known after apply)
+ deletion_protection  = false
+ guest_accelerator    = (known after apply)
+ id                   = (known after apply)
+ instance_id          = (known after apply)
+ label_fingerprint    = (known after apply)
+ machine_type         = "n1-standard-2"
+ metadata_fingerprint = (known after apply)
+ name                 = "two"
+ project              = (known after apply)
+ self_link            = (known after apply)
+ tags_fingerprint     = (known after apply)
+ zone                 = "us-central1-b"


+ boot_disk {
+ auto_delete                = true
+ device_name                = (known after apply)
+ disk_encryption_key_sha256 = (known after apply)
+ kms_key_self_link          = (known after apply)
+ mode                       = "READ_WRITE"
+ source                     = (known after apply)


+ initialize_params {
+ image  = "projects/centos-cloud/global/images/centos-8-v20191018"
+ labels = (known after apply)
+ size   = (known after apply)
+ type   = (known after apply)
}
}


+ network_interface {
+ address            = (known after apply)
+ name               = (known after apply)
+ network            = "default"
+ network_ip         = (known after apply)
+ subnetwork         = (known after apply)
+ subnetwork_project = (known after apply)
}


+ scheduling {
+ automatic_restart   = (known after apply)
+ on_host_maintenance = (known after apply)
+ preemptible         = (known after apply)


+ node_affinities {
+ key      = (known after apply)
+ operator = (known after apply)
+ values   = (known after apply)
}
}


+ scratch_disk {
+ interface = "SCSI"
}
}


Plan: 2 to add, 0 to change, 0 to destroy.

Seem's like I found what to do. If you pass not the maps of maps but the list of maps you can use such code

resource "google_compute_instance" "node" {
for_each = {for vm in var.vms:  vm.hostname => vm}


name         = "${each.value.hostname}"
machine_type = "custom-${each.value.cpu}-${each.value.ram*1024}"
zone         = "${var.gcp_zone}"


boot_disk {
initialize_params {
image = "${var.image_name}"
size = "${each.value.hdd}"
}
}


network_interface {
network = "${var.network}"
}


metadata = {
env_id = "${var.env_id}"
service_types = "${join(",",each.value.service_types)}"
}
}

It will create actual number of instance and when you remove for example middle one of three(if you create three:)), terraform will remove what we asked.

From Terraform 0.12, you can use the for_each with modules like the following:

modules/google_compute_instance/variables.tf

variable "hosts" {
type = map(object({
hostname        = string
cpu             = number
ram             = number
hdd             = number
log_drive       = number
template        = string
service_types   = list(string)
}))
}

modules/google_compute_instance/main.tf

resource "google_compute_instance" "gcp_instance" {
for_each = var.hosts


hostname      = each.value.repository_name
cpu           = each.value.cpu
ram           = each.value.ram
hdd           = each.value.hdd
log_drive     = each.value.log_drive
template      = each.value.template
service_types = each.value.service_types
}

#servers.tf

module "gcp_instances" {
source = "./modules/google_compute_instance"


hosts = {
"test1-srfe" = {
hostname        = "test1-srfe",
cpu             = 1,
ram             = 4,
hdd             = 15,
log_drive       = 300,
template        = "Template-New",
service_types   = ["sql", "db01", "db02"]
},
"test1-second" = {
hostname        = "test1-second",
cpu             = 1,
ram             = 4,
hdd             = 15,
log_drive       = 300,
template        = "APPs-Template",
service_types   = ["configs"]
},
}
}

Of course, you can add as many variables as needed and use them in the module.

You can do the following:

for_each = toset(keys({for i, r in var.vms:  i => r}))
cpu = var.vms[each.value]["cpu"]

Assuming you had the following:

variable "vms" {
type = list(object({
hostname        = string
cpu             = number
ram             = number
hdd             = number
log_drive       = number
template        = string
service_types   = list(string)
}))
default = [
{
cpu: 1
...
}
]
}

I work a lot with iterators in Terraform, they always gave me bad headaches. Therefore I identified three of the most common iterator patterns (code examples are given below), which helped me construct a lot of nice modules (source).

  • Using for_each on a list of strings
  • Using for_each on a list of objects
  • Using for_each as a conditional

Using for_each and a list of strings is the easiest to understand, you can always use the toset() function. When working with a list of objects you need to convert it to a map where the key is a unique value. The alternative is to put a map inside your Terraform configuration. Personally, I think it looks cleaner to have a list of objects instead of a map in your configuration. The key usually doesn't have a purpose other than to identify unique items in a map, which can thus be constructed dynamically. I also use iterators to conditionally deploy a resource or resource block, especially when constructing more complex modules.

Using for_each on a list of strings

locals {
ip_addresses = ["10.0.0.1", "10.0.0.2"]
}


resource "example" "example" {
for_each   = toset(local.ip_addresses)
ip_address = each.key
}

Using for_each on a list of objects

locals {
virtual_machines = [
{
ip_address = "10.0.0.1"
name       = "vm-1"
},
{
ip_address = "10.0.0.1"
name       = "vm-2"
}
]
}


resource "example" "example" {
for_each   = {
for index, vm in local.virtual_machines:
vm.name => vm # Perfect, since VM names also need to be unique
# OR: index => vm (unique but not perfect, since index will change frequently)
# OR: uuid() => vm (do NOT do this! gets recreated everytime)
}
name       = each.value.name
ip_address = each.value.ip_address
}

Using for_each as a conditional

variable "deploy_something" {
type        = bool
description = "Indicates whether to deploy something."
default     = true
}


# Using count and a conditional, for_each is also possible here.
# See the next solution using a for_each with a conditional.
resource "example" "example" {
count      = var.deploy_example ? 0 : 1
name       = ...
ip_address = ...
}


variable "enable_something" {
type        = bool
description = "Indicates whether to enable something."
default     = false
}


resource "example" "example" {
name       = ...
ip_address = ...


# Note: dynamic blocks cannot use count!
# Using for_each with an empty list and list(1) as a readable alternative.
dynamic "logs" {
for_each = var.enable_logs ? [] : [1]
content {
name     = "logging"
}
}
}

I took reference from the for_each example above and used below. This did not work for me, link below has details. Terraform for_each on custom registry

module "az"{
source="./modules/az"
vpc_id = module.vpc.vpc_id
for_each = toset(keys({for i,v in var.az_sub: i => v}))
availability_zone = var.az_sub[each.value]["az"]
public_cidr_block = var.az_sub[each.value]["public_cidr_block"]
private_cidr_block  =var.az_sub[each.value]["private_cidr_block"]
}

Error:module.az is object with 2 attributes If I replace for_each with actual values, the module is working perfectly.