Terraform patterns: Make Terraform programable

Summary

Terraform as a Declarative Language, are smart to achieve the target state without bothering SecDevOps people to keep track of the procedures. You can easily understand from the high level Terraform flow diagram bellow. But only setting up the end-state sometime couldn’t help. There are scenarios where we need to hook Terraform as an interface to expose infrastructure-as-code to Automation stages or E2E testing cases. Making Terraform more programable will tackle these kinds of challenges. Today, I am going to introduce some patterns around this.

Terraform Flow

Make Terraform programable

There are three possible steps to make terraform programmable and functional. This is the high level summary of all the steps. After this summary, I will explain each one of them.Terraform Functional

Input Variables

Use the -var option when running terraform commands. This will enable the parameters to be passed from external, such as config YAML or JSON file or BDD Gherkin File.

Structural Types

A structural type allows multiple values of several distinct types to be grouped together as a single value. After complex type of parameter passed from external, Terraform could resolve those data type by comparing the Structural Types defined in the variable types. For example,

variable "redis_map" {
  type = map(map(object({
                  csam = string,
                  redis_internal_vnet = string,
                  minimum_tls_version = string,
                  enable_non_ssl_port = string,
                  vnet = object({
                    name = string,
                    virtual_network_address_space = string
                  }),
                  subnet = object({
                    name = string,
                    subnet_address_prefix = string
                  })
                }))
            )
default = {
    rg = {
      redis = {
        redis_internal_vnet = "true",
        minimum_tls_version = "1.1",
        enable_non_ssl_port = "true",
        vnet = {
          name = "vent01",
          virtual_network_address_space = "10.0.0.0/16"
        },
        subnet = {
          name = "subnet01",
          subnet_address_prefix = "10.0.1.0/24"
        }
      }
    }
  }
}

Optional arguments

Sometimes the parameters passed in are optional, because there is no need or it would be good enough to leave it as default, or some test cases only check certain parameter instead of all.

Optional arguments in object variable type definition is not available yet. There is issue raised for add this feature. And I put the workaround for this in this feature request as well.

The tricky part is still on the default value of the variable and using local variables. Use the locals variable to verify the optional parameter. Put it into null if not existing. The other resources would be able to refer to those local variables. 

variable "redis_map" {
locals {
  redis_nested_foreach = flatten([
    for resource_group, rg in var.redis_map : [
      for rdsk, rdsv in rg: {
        rg_name = resource_group,
        redis_name = rdsk,
        csam = rdsv.csam,
        enable_non_ssl_port = lookup(rdsv, "enable_non_ssl_port", "false") == "false" ? null : rdsv.enable_non_ssl_port,
        minimum_tls_version = lookup(rdsv, "minimum_tls_version", "1.0") == "1.0" ? null : rdsv.minimum_tls_version,
        redis_internal_vent = rdsv.redis_internal_vnet,
        vnet_name = rdsv.redis_internal_vnet ? rdsv.vnet["name"] : null,
        virtual_network_address_space = rdsv.redis_internal_vnet ? rdsv.vnet["virtual_network_address_space"] : null,
        subnet_name = rdsv.redis_internal_vnet ? rdsv.subnet["name"] : null,
        subnet_address_prefix = rdsv.redis_internal_vnet ? rdsv.subnet["subnet_address_prefix"] : null
      }
    ]
  ])
}

 

 

Leave a comment