HashiCorp Learn
Infrastructure
  • TerraformTerraformLearn terraformDocs
  • PackerPackerLearn packerDocs
  • VagrantVagrantLearn vagrantDocs
Security
  • VaultVaultLearn vaultDocs
  • BoundaryBoundaryLearn boundaryDocs
Networking
  • ConsulConsulLearn consulDocs
Applications
  • NomadNomadLearn nomadDocs
  • WaypointWaypointLearn waypointDocs
  • HashiCorp Cloud Platform (HCP) LogoHashiCorp Cloud Platform (HCP)HashiCorp Cloud Platform (HCP)Docs
Type '/' to Search
Loading account...
  • Bookmarks
  • Manage Account
  • Overview
  • Prerequisites
  • Apply initial configuration
  • Define a map to configure each project
  • Add for_each to the VPC
  • Update the load balancer and its security group
  • Move EC2 instance to a module
  • Apply scalable configuration
  • Clean up resources
  • Next steps
DocsForum
Back to terraform
Configuration LanguageView Collection
    Define Infrastructure with Terraform ResourcesCustomize Terraform Configuration with VariablesProtect Sensitive Input VariablesSimplify Terraform configuration with localsOutput Data From TerraformCreate Resource DependenciesManage Similar Resources with CountManage Similar Resources with For EachPerform Dynamic Operations with FunctionsCreate Dynamic ExpressionsLock and Upgrade Provider Versions

Manage Similar Resources with For Each

  • 8 min
  • Products Usedterraform
  • This tutorial also appears in: 0.13 Release.

In this tutorial, you will provision a VPC, load balancer, and EC2 instances on AWS. Then you will refactor your configuration to provision multiple projects with the for_each argument and a data structure.

The for_each argument will iterate over a data structure to configure resources or modules with each item in turn. It works best when the duplicate resources need to be configured differently but share the same lifecycle.

Tip: Terraform 0.13 supports the for_each argument on both resource and module blocks. Prior versions only supported it on resource blocks.

»Prerequisites

  • The Terraform CLI, version 0.13 or later.
  • AWS Credentials configured for use with Terraform.
  • The git CLI.

»Apply initial configuration

Clone the example GitHub repository.

$ git clone https://github.com/hashicorp/learn-terraform-count-foreach.git

Change into the new directory.

$ cd learn-terraform-count-foreach

Check out the initial configuration.

$ git checkout tags/foreach-initial-configuration -b foreach-initial-configuration

The configuration in main.tf will provision a VPC with public and private subnets, a load balancer, and EC2 instances in each private subnet. The variables located in variables.tf allow you to configure the VPC. For instance, the private_subnets_per_vpc variable controls the number of private subnets the configuration will create.

Initialize Terraform in this directory. Terraform will install the AWS provider and the vpc, app_security_group, lb_security_group, and elb_http modules.

$ terraform init

Once your directory has been initialized, apply the configuration, and remember to confirm with a yes.

$ terraform apply

Note: The load balancer’s domain name is part of the output. It may take a few minutes after the apply step before you can visit this domain name. Be sure to connect via HTTP, not HTTPS.

Refactor the VPC and related configuration so that Terraform can deploy multiple projects at the same time, each with their own VPC and related resources.

Note: Use separate Terraform projects or workspaces instead of for_each to manage resources’ lifecycles independently. For example, if production and development environments share the same Terraform project running terraform destroy will destroy both. You can either implement the changes below manually, or check out the foreach-multiple-projects branch for the completed configuration.

$ git checkout tags/foreach-multiple-projects -b foreach-multiple-projects

»Define a map to configure each project

Define a map for project configuration in variables.tf that for_each will iterate over to configure each resource.

variable project {
  description = "Map of project names to configuration."
  type        = map
  default     = {
    client-webapp = {
      public_subnets_per_vpc  = 2,
      private_subnets_per_vpc = 2,
      instances_per_subnet    = 2,
      instance_type           = "t2.micro",
      environment             = "dev"
    },
    internal-webapp = {
      public_subnets_per_vpc  = 1,
      private_subnets_per_vpc = 1,
      instances_per_subnet    = 2,
      instance_type           = "t2.nano",
      environment             = "test"
    }
  }
}

Note: The for_each argument also supports lists and sets.

Since the project variable includes most of the options that were configured by individual variables, comment out or remove these variables from variables.tf.

-variable project_name {
-  description = "Name of the project. Used in resource names and tags."
-  type        = string
-  default     = "client-webapp"
-}
-
-variable environment {
-  description = "Value of the 'Environment' tag."
-  type        = string
-  default     = "dev"
-}
-
-variable public_subnets_per_vpc {
-  description = "Number of public subnets. Maximum of 16."
-  type        = number
-  default     = 2
-}
-
-variable private_subnets_per_vpc {
-  description = "Number of private subnets. Maximum of 16."
-  type        = number
-  default     = 2
-}
-
-variable instance_type {
-  description = "Type of EC2 instance to use."
-  type        = string
-  default     = "t2.micro"
-}
-
-variable instances_per_subnet {
-  description = "Number of EC2 instances in each private subnet"
-  type        = number
-  default     = 2
-}

»Add for_each to the VPC

Now use for_each to iterate over the project map in the VPC module block of main.tf, which will create one VPC for each key/value pair in the map.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "2.44.0"
+
+ for_each = var.project

# ...truncated...
}

Terraform will provision multiple VPCs, assigning each key/value pair in the var.project map to each.key and each.value respectively. With a list or set, each.key will be the index of the item in the collection, and each.value will be the value of the item.

In this example, the project map includes values for the number of private and public subnets in each VPC. Update the subnet configuration in the vpc module block in main.tf to use each.value to refer to these values.

  azs             = data.aws_availability_zones.available.names
- private_subnets = slice(var.private_subnet_cidr_blocks, 0, var.private_subnets_per_vpc)
- public_subnets  = slice(var.public_subnet_cidr_blocks, 0, var.public_subnets_per_vpc)
+ private_subnets = slice(var.private_subnet_cidr_blocks, 0, each.value.private_subnets_per_vpc)
+ public_subnets  = slice(var.public_subnet_cidr_blocks, 0, each.value.public_subnets_per_vpc)

Update the app_security_group module to iterate over the project variable to get the security group name, VPC ID, and CIDR blocks for each project.

module "app_security_group" {
  source  = "terraform-aws-modules/security-group/aws//modules/web"
  version = "3.12.0"

+ for_each = var.project

- name        = "web-server-sg-${var.project_name}-${var.environment}"
+ name        = "web-server-sg-${each.key}-${each.value.environment}"
  description = "Security group for web-servers with HTTP ports open within VPC"
- vpc_id      = module.vpc.vpc_id
+ vpc_id      = module.vpc[each.key].vpc_id

- ingress_cidr_blocks = module.vpc.public_subnets_cidr_blocks
+ ingress_cidr_blocks = module.vpc[each.key].public_subnets_cidr_blocks
}

You can differentiate between instances of resources and modules configured with for_each by using the keys of the map you use. In this example, using module.vpc[each.key].vpc_id to define the VPC means that the security group for a given project will be assigned to the corresponding VPC.

»Update the load balancer and its security group

Update the configuration for the load balancer security groups to iterate over the project variable to get their names and VPC IDs.

module "lb_security_group" {
  source  = "terraform-aws-modules/security-group/aws//modules/web"
  version = "3.12.0"

+ for_each = var.project

- name = "load-balancer-sg-${var.project_name}-${var.environment}"
+ name = "load-balancer-sg-${each.key}-${each.value.environment}"

  description = "Security group for load balancer with HTTP ports open within VPC"
- vpc_id      = module.vpc.vpc_id
+ vpc_id      = module.vpc[each.key].vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
}

Update the elb_http block so that each VPC’s load balancer name will also include the name of the project, the environment, and will use the corresponding security groups and subnets.

module "elb_http" {
  source  = "terraform-aws-modules/elb/aws"
  version = "2.4.0"

+ for_each = var.project
+
  # Comply with ELB name restrictions
  # https://docs.aws.amazon.com/elasticloadbalancing/2012-06-01/APIReference/API_CreateLoadBalancer.html
- name = trimsuffix(substr(replace(join("-", ["lb", random_string.lb_id.result, var.project_name, var.environment]), "/[^a-zA-Z0-9-]/", ""), 0, 32), "-")
+ name = trimsuffix(substr(replace(join("-", ["lb", random_string.lb_id.result, each.key, each.value.environment]), "/[^a-zA-Z0-9-]/", ""), 0, 32), "-")
  internal = false

- security_groups = [module.lb_security_group.this_security_group_id]
- subnets         = module.vpc.public_subnets
+ security_groups = [module.lb_security_group[each.key].this_security_group_id]
+ subnets         = module.vpc[each.key].public_subnets

»Move EC2 instance to a module

You will also need to update the instance resource block to assign EC2 instances to each VPC. However, the block already uses count. You cannot use both count and for_each in the same block.

To solve this, you will move the aws_instance resource into a module, including the count argument, and then use for_each when referring to the module in your main.tf file. The example repository includes a module with this configuration in the modules/aws-instance directory. For a detailed example on how to move a configuration to a local module, try the Create a Terraform Module tutorial.

Remove the resource "aws_instance" "app" and data "aws_ami" "amazon_linux" blocks from your root module's main.tf file, and replace them with a reference to the aws-instance module.

module "ec2_instances" {
  source = "./modules/aws-instance"

  for_each = var.project

  instance_count     = each.value.instances_per_subnet * length(module.vpc[each.key].private_subnets)
  instance_type      = each.value.instance_type
  subnet_ids         = module.vpc[each.key].private_subnets[*]
  security_group_ids = [module.app_security_group[each.key].this_security_group_id]

  project_name = each.key
  environment  = each.value.environment
}

Note: You cannot include a provider block in modules that use count or for_each. They must inherit provider configuration from the root module. Resources created by the module will all use the same provider configuration.

Next, replace the references to the EC2 instances in the module "elb_http" block with references to the new module.

- number_of_instances = length(aws_instance.app)
- instances           = aws_instance.app.*.id
+ number_of_instances = length(module.ec2_instances[each.key].instance_ids)
+ instances           = module.ec2_instances[each.key].instance_ids

Finally, replace the entire contents of outputs.tf in your root module with the following.

output public_dns_names {
  description = "Public DNS names of the load balancers for each project"
  value       = { for p in sort(keys(var.project)) : p => module.elb_http[p].this_elb_dns_name }
}

output vpc_arns {
  description = "ARNs of the vpcs for each project"
  value       = { for p in sort(keys(var.project)) : p => module.vpc[p].vpc_arn }
}

output instance_ids {
  description = "IDs of EC2 instances"
  value       = { for p in sort(keys(var.project)) : p => module.ec2_instances[p].instance_ids }
}

The for expressions used here will map the project names to the corresponding values in the Terraform output.

Note: for and for_each are different features. for_each provisions similar resources in module and resource blocks. for creates a list or map by iterating over a collection, such as another list or map. You can read more about for expressions in the Terraform documentation.

»Apply scalable configuration

Initialize the new module.

$ terraform init

Now apply the changes. Remember to respond to the confirmation prompt with yes.

$ terraform apply

Terraform will list the outputs for each project.

# ...truncated...

Outputs:

instance_ids = {
  "client-webapp" = [
    "i-0e11fcc341e6ce292",
    "i-0b7ddd178c0590116",
    "i-0c570628d3997874b",
    "i-0a1642d7cc173f329",
  ]
  "internal-webapp" = [
    "i-0e65c8569f2d2c6f5",
    "i-0c62e911e9446c53b",
  ]
}
public_dns_names = {
  "client-webapp" = "lb-l9Vr-client-webapp-dev-215632309.us-east-2.elb.amazonaws.com"
  "internal-webapp" = "lb-l9Vr-internal-webapp-test-80535664.us-east-2.elb.amazonaws.com"
}
vpc_arns = {
  "client-webapp" = "arn:aws:ec2:us-east-2:130490850807:vpc/vpc-00bd9888322925dc2"
  "internal-webapp" = "arn:aws:ec2:us-east-2:130490850807:vpc/vpc-01aa642055624f109"
}

This configuration creates separate VPCs for each project defined in variables.tf. count and for_each allow you to create more flexible configurations, and reduce duplicate resource and module blocks.

»Clean up resources

After verifying that the projects deployed successfully, run terraform destroy to destroy them. Remember to respond to the confirmation prompt with yes.

$ terraform destroy

»Next steps

Now that you have used for_each in your configuration, explore the following resources.

  • Read the Terraform documentation for the for_each meta-argument.
  • Learn how to create and use Terraform modules.


Back to Collection
HashiCorp
  • System Status
  • Terms of Use
  • Security
  • Privacy
stdin: is not a tty