Terraform 0.12, a new feature which can be implemented in new projects
Recently HashiCorp published the newest version of Terraform, which has a very interesting feature as a new approach pertaining to loops. Most of us are familiar with the old count expression trick, which works a charm if we want to implement recurrence in our scripts. However, this approach will be complex as we dive deeper; for example, if we want to work with slightly more complicated configuration blocks inside the single resource instead of creating many resources. Right now we can do it because HashiCorp has introduced For and For-Each expressions.
Let’s take as a simple example script to create an S3 bucket from official Terraform documentation:
resource "aws_s3_bucket" "b" { bucket = "my-tf-test-bucket" acl = "private" tags = { Name = "My bucket" Environment = "Dev" } }
There is no place for implementing additional features inside the script.If our case is more complicated, for example, if we want to implement transition property for objects placed in bucket, we will struggle with the lifecycle_rule. First of all, our Terraform friend doesn’t like emptiness, which in contrast is a desireable feature in automation.. Sometimes we want to create an S3 with one set of lifecycle_rule properties, sometimes with another. This is the exact situation where the expression from Terraform steps in. To understand this, let’s expand our script with the lifepolicy_rule:
lifecycle_rule { prefix = "config/" enabled = true noncurrent_version_transition { days = 30 storage_class = "STANDARD_IA" } noncurrent_version_transition { days = 60 storage_class = "GLACIER" } noncurrent_version_expiration { days = 90 }
In this form, such a script is not as reusable as it can be. It will prepare for us lifecycle_rule, which will permanently expect us to provide noncurrent_version_transition details equally for STANDARD_IA as well as GLACIER every time we use this script. As far as we consider our script to be “universal,” we should create it in a universal manner. As universal, I understand that with only a couple changes, preferably only variable changes, we should achieve our goals. So assume that we would like to use this script without GLACIER transition without commenting or leaving empty sections. As I highlighted previously, it is not really possible because Terraform will force you to provide any value, otherwise it will fail. In this case, to be a bit more sophisticated, we could implement the for_each expression, which will allow us to be much more flexible. It will work as a simple ON/OFF switch. Let’s see what it could look like:
resource "aws_s3_bucket" "default" { bucket = var.bucket_name acl = var.acl region = var.aws-region versioning { enabled = var.versioning_enabled } lifecycle_rule { id = var.bucket_name prefix = var.prefix enabled = var.lifecycle_rule_enabled abort_incomplete_multipart_upload_days = var.abort_incomplete_multipart_upload_days noncurrent_version_expiration { days = var.noncurrent_version_expiration_days } dynamic "noncurrent_version_transition" { for_each = var.enable_glacier_transition ? [1] : [] content { days = var.noncurrent_version_transition_days storage_class = "GLACIER" } } transition { days = var.standard_transition_days storage_class = "STANDARD_IA" } dynamic "transition" { for_each = var.enable_glacier_transition ? [1] : [] content { days = var.glacier_transition_days storage_class = "GLACIER" } } } }
In this case, we rely on the most important variable in this example:
var.enable_glacier_transition
This variable determines how our script behaves. Simply put, it will turn on or turn off two dynamic blocks where it is present:
dynamic "noncurrent_version_transition" { for_each = var.enable_glacier_transition ? [1] : [] content { days = var.noncurrent_version_transition_days storage_class = "GLACIER" } } dynamic "transition" { for_each = var.enable_glacier_transition ? [1] : [] content { days = var.glacier_transition_days storage_class = "GLACIER"
Thanks to the combination with dynamic block, we can achieve our goal. If our variable equals to true the block will be present; if false it will be erased from terraform consciousness and S3 bucket will be created without GLACIER class rules.
Another example that will enhance our understanding is to simply create security groups with different settings determined by our variable:
variable "ingress_ports_list" { type = list(number) description = "list of ingress ports to include in allow rule" default = [80, 443] }
Previously, we had to hardcode the same quantity of configuration blocks for as many ports as we would like to open in our Security Group:
resource "aws_security_group" "ec2" { name = "allow_particular_connections" description = "Allow Particular Connections" vpc_id = var.VpcId ingress { from_port = var.portToAllow to_port = var.portToAllow protocol = var.protocol cidr_blocks = var.cidr } ingress { from_port = var.portToAllowSecond to_port = var.portToAllowSecond protocol = var.protocolSecond cidr_blocks = var.cidrSecond } }
We can also try to “inject” ingress group rule into the source Security group, with aws_security_group_rule:
resource "aws_security_group_rule" "example" { type = "ingress" from_port = 0 to_port = 65535 protocol = "tcp" cidr_blocks = aws_vpc.example.cidr_block security_group_id = "sg-123456" }
But in this case we have to struggle with count expression and an additional list of ports, and taking the ports from list with count index and so on. However, we know that the shorter code we produce, the better performance will be achieved. That’s why the for_each expression will change the game:
resource "aws_security_group" "default" { name = "sg_name" description = "sg_description" vpc_id = "vpc_id" dynamic "ingress" { iterator = port for_each = var.ingress_ports content { from_port = port.value to_port = port.value protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } }
This will solve our problem with reusability of the script, because we can make almost every dynamic block dependent on determined variables values. There is no more commenting inside the the script or struggling with changing hardcoded configuration blocks.
This is a very simple example, but you should feel free to implement this principle in almost all cases where reusability is expected, which before the new terraform version, was very difficult to achieve.