Creating Loops in the Terraform Configuration and Scaling Resources

Lab Steps

lock
Creating Loops in the Terraform Configuration and Scaling Resources
Need help? Contact our support team

Here you can find the instructions for this specific Lab Step.

If you are ready for a real environment experience please start the Lab. Keep in mind that you'll need to start from the first step.

Introduction

Using loops in Terraform can give code a cleaner look and follows the DRY (Don't Repeat Yourself)  principles of programming where the same concepts aren't repeated throughout the Terraform configuration. Loops also allow for resources to scale efficiently. 

You will create a module for deploying an EC2 instance. It is recommended to develop configurations with modules in mind. Local modules can be used to organize code. In this lab, the following folder hierarchy will be used:

Copy code
terraformlab
    └──modules 
            └──ec2
                └── main.tf
                └── variables.tf

In this lab step, you will create a module with loops using several methods.

 

Instructions

Before starting this lab, wait for your Cloud Academy IDE environment to provision. You will know when the URL below changes to an address starting with ec2-XXX:

http://

 

​1. In a web browser, navigate to http://:

Copy code
 http://

The web-based CloudAcademy IDE has been configured to listen for inbound connections on port 80 using HTTP. 

Note:

  • It takes approximately 1-2 minutes for the web-based CloudAcademy IDE service to startup - try refreshing the browser page request until access is successful
  • It has been configured to listen on port 80

alt

 

2. Right-click on the ec2 folder and click New File:

alt

 

3. Name the file variables.tf and click OK:

alt

 

4. Copy and paste the terraform variable blocks to the variables.tf file:

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
variable "associate_public_ip_address" {
  description = "Associate a public IP address with EC2 instance"
  type        = bool
  default     = true
}
variable "servername"{
    description = "Name of the server"
    type = string
}

#Variable that will contain a list of map types
variable "ebs_block_device" {
  description = "Additional EBS block devices to attach to the instance"
  type        = list(map(string))
  default     = []
}

Notice the ebs_block_device variable is the type of list(map(string)). This indicates that this variable can hold a list of several maps:

alt

In the next steps, you will create a configuration with a loop that iterates through all specified maps in the  ebs_block_device variable and creates an EBS volume with their values. You can also create a list of objects as well instead of maps, which would be appropriate if each value was of a different type like bool, string, and integer.

 

5. Right-click on the ec2 folder and click New File:

alt

 

6. Name the file main.tf and click OK:

alt

Note: In the lower right corner, you will see some pop-ups from the Terraform Visual Studio Code extension. These can be ignored:

alt

 

7. Copy and paste the terraform resource block to the main.tf file:

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
resource "aws_instance" "server" {
    ami           = "ami-0528a5175983e7f28"
    instance_type = "t2.micro"
    associate_public_ip_address = var.associate_public_ip_address

    #dynamic block with for_each loop
    dynamic "ebs_block_device" {
    for_each = var.ebs_block_device
        content {
        delete_on_termination = lookup(ebs_block_device.value, "delete_on_termination", null)
        device_name           = ebs_block_device.value.device_name
        encrypted             = lookup(ebs_block_device.value, "encrypted", null)
        iops                  = lookup(ebs_block_device.value, "iops", null)
        kms_key_id            = lookup(ebs_block_device.value, "kms_key_id", null)
        snapshot_id           = lookup(ebs_block_device.value, "snapshot_id", null)
        volume_size           = lookup(ebs_block_device.value, "volume_size", null)
        volume_type           = lookup(ebs_block_device.value, "volume_type", null)
        }
    }

    tags = {
        Name = "${var.servername}"
    }
}

In the aws_instance resource block there is a dynamic block referenced for ebs_block_device:

alt

Dynamic blocks can be used for resources that contain repeatable configuration blocks. Instead of repeating several ebs_block_device blocks, a dynamic block is used to simplify the code. This is done by combining the dynamic block with a for_each loop inside. The first line inside the dynamic block is the for_each loop. The loop is iterating through the list of the ebs_block_device variable, which is a list of maps. In the content block, each value of the map is referenced using the lookup function. The logic here is to look for a value in the map variable and if it's not there, set the value to null. The dynamic block will iterate through each map in the list: 

alt

 

8. Add the aws_eip resource block to the main.tf file:

Copy code
1
2
3
4
5
6
7
#public IP address with Count Conditional Expression
resource "aws_eip" "pip" {
  count             = var.associate_public_ip_address ? 1 : 0
  network_interface = aws_instance.server.primary_network_interface_id
  vpc               = true
}

In the aws_eip resource block is the count parameter:

alt

Count allows for creating multiple instances of a resource block. Almost all resource blocks can use the count attribute. It is simply the number of times to create the resource block. It can also be used as conditional logic. In this case, the value of count is a conditional expression. If var.associate_public_ip_address is true set the count value to 1, if false set it to 0. This allows resource blocks to be created conditionally. In this example, a public IP address is not created if var.associate_public_ip_address is set to false.

After adding the aws_eip resource block, the entire main.tf file should look like the following:

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
resource "aws_instance" "server" {
    ami           = "ami-0528a5175983e7f28"
    instance_type = "t2.micro"
    associate_public_ip_address = var.associate_public_ip_address

    #dynamic block with for_each loop
    dynamic "ebs_block_device" {
    for_each = var.ebs_block_device
        content {
        delete_on_termination = lookup(ebs_block_device.value, "delete_on_termination", null)
        device_name           = ebs_block_device.value.device_name
        encrypted             = lookup(ebs_block_device.value, "encrypted", null)
        iops                  = lookup(ebs_block_device.value, "iops", null)
        kms_key_id            = lookup(ebs_block_device.value, "kms_key_id", null)
        snapshot_id           = lookup(ebs_block_device.value, "snapshot_id", null)
        volume_size           = lookup(ebs_block_device.value, "volume_size", null)
        volume_type           = lookup(ebs_block_device.value, "volume_type", null)
        }
    }

    tags = {
        Name = "${var.servername}"
    }
}

#public IP address with Count Conditional Expression
resource "aws_eip" "pip" {
  count             = var.associate_public_ip_address ? 1 : 0
  network_interface = aws_instance.server.primary_network_interface_id
  vpc               = true
}

 

9. Right-click on the terraformlab folder and click New File:

alt

 

10. Name the file main.tf and click OK:

alt

 

11. Copy and paste the terraform resource block to the main.tf file:

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.7"
    }
  }
}

provider "aws" {
  region = "us-west-2"
}


module "server" {
    source = "./modules/ec2"

    servername = "testserver"
    associate_public_ip_address = true

    #The input for the list of maps variable created in the EC2 module
    ebs_block_device = [
        {
        device_name = "/dev/sdh"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        }
    ]

}

 

12. Save the terraform configuration files by clicking File in the upper left corner and click Save All:

alt

 

13. Left-click on the Terminal menu and click New Terminal:

alt

A Terminal window will appear at the bottom.

 

14. Inside the terminal, input the following command to change directory to the terraformlab folder:

Copy code
1
cd terraformlab

 

15. Initialize the directory for Terraform by inputting the terraform init command into the terminal:

Copy code
1
terraform init

alt

The AWS provider is downloaded during the initialization process. Also, notice the module is initialized. 

 

16. Run terraform plan:

Copy code
1
terraform plan

In the execution plan, notice the public IP address resource will be created due to the count conditional logic:

alt

Since only one map was specified, only one ebs_block_device is created:

alt

 

17. Add another ebs block to the module and set associate_public_ip_address to false :

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module "server" {
    source = "./modules/ec2"

    servername = "testserver"
    associate_public_ip_address = false

    ebs_block_device = [
        {
        device_name = "/dev/sdh"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        },
        {
        device_name = "/dev/sdj"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        }
    ]

}

The entire contents of main.tf should look like the following:

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.7"
    }
  }
}

provider "aws" {
  region = "us-west-2"
}

module "server" {
    source = "./modules/ec2"

    servername = "testserver"
    associate_public_ip_address = false

    ebs_block_device = [
        {
        device_name = "/dev/sdh"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        },
        {
        device_name = "/dev/sdj"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        }
    ]

}

 

18. Save the changes to the main.tf file.

 

19. Run terraform apply:

Copy code
1
terraform apply

In the execution plan, the public IP address resource will no longer be created since associate_public_ip_address was set to false. Also, there are now two ebs volumes to be created since an additional map was added to the module configuration:

alt

 

20. Input yes to deploy the config:

alt

The EC2 module has been deployed successfully using the for_each loop to create the ebs volumes.

 

21. Add a count argument to the module block :

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module "server" {
    count = 3 
    source = "./modules/ec2"

    servername = "testserver${count.index}"
    associate_public_ip_address = false

    ebs_block_device = [
        {
        device_name = "/dev/sdh"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        },
        {
        device_name = "/dev/sdj"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        }
    ]

}

The servername argument contains the count.index variable in the value. Count.index becomes available when using count. It represents the index value of the resource being created. It can be used to provide a unique name to resources when they are duplicated with count. In this example, the server name of the 3 servers will be testserver0, testserver1, and testserver2.

The entire contents of main.tf should look like the following:

Copy code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.7"
    }
  }
}

provider "aws" {
  region = "us-west-2"
}

module "server" {
    count = 3 
    source = "./modules/ec2"

    servername = "testserver${count.index}"
    associate_public_ip_address = false

    ebs_block_device = [
        {
        device_name = "/dev/sdh"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        },
        {
        device_name = "/dev/sdj"
        volume_size = "4"
        volume_type = "standard"
        delete_on_termination = "true"
        }
    ]

}

In Terraform v0.13 the count loop can now be used on modules. Now you can create modules and use loops to scale them!

 

22. Save the changes to the main.tf file.

 

23. Run terraform plan:

Copy code
1
terraform plan

In the execution plan, three new servers will be deployed and the existing server will be destroyed:

alt

This is because the count argument turns the module into an indexed resource which forces the existing one to be destroyed:

alt

 

Summary

In this lab step, you created an EC2 Instance module that contains a for_each loop for deploying EBS volumes. You also used count to create conditional logic for the public IP address resource and scale the module to deploy 3 servers. 

Validation checks
1Checks
Created the EC2 instance

Check if the EC2 instance has been created

Amazon EC2