Blog

Infrastructure as a Code with Terraform – From declarative language and templates to simple programming language

Infrastructure as a Code with Terraform – From declarative language and templates to simple programming language

Our passion at ITGix is programming and working with Cloud services and tools. With all the different sets of projects we have, our motivation and experience are inspired from technologies like Docker, Kubernetes, Icinga, Ansible, Chef, SaltStack, GitLab, Jenkins, AWS, Google Cloud and many more tools and services. Recently I got keen on Infrastructure as a Code and for the purpose we are using Terraform.

Terraform is a somewhat versatile tool used for automating, codifying your Infrastructure and deploying resources around most of the major Cloud services providers. It uses a declarative language, developed by HashiCorp, called HCL (HashiCorp Configuration Language), which many people think that is a limited and not really innovative programming language. Personally, I cannot share the same opinion, however I can agree that setting conditional logic in Terraform can be a bit tricky and some certain types of tasks become very difficult without access to a full programming language.

For example, since declarative languages usually do not support for-loops, how do we repeat a piece of logic — such as creating multiple similar EC2 Instances and environments without copy and paste? And if the declarative language does not have if-statements, how can we conditionally configure resources, such as not exposing our backend services to the public web and creating a Public DNS record for our frontend service?
Fortunately Terraform provides us with a meta-parameter called “count” and a large number of interpolation functions that allow us to assemble certain types of loops and conditional statements.

Using the “count” parameter and most of the interpolation functions, we managed to create an “all-in-one” module which deploys different sets of resources in an automated way based on the conditions and environment type configured in the variables by the user.
The following code samples show how we have achieved our goal:
  • Setting variables and enumeration 
In the sample below, we use “locals” which are comparable to function’s local variables in other programming languages. Our “local” contains a listing of all possible string inputs.

The variable is the input that Terraform is expecting from the end-user.

“Null_Resource” is used as a workaround to display an error message when the value of our “env_type” variable is not existing in the list of possible input. In other programming languages you can trigger an error manually if something isn't right. In Terraform there is no way to tell someone that the name they used for a variable is wrong, so the trick here is just in the null_resource block, the "count" key needs to be 1 if the assert fails.

locals {
  input_list = ["EC2_Private", "ECS_Cluster", "EC2_Public"]
}

variable "env_type" {
  type = "string"
  default = ""
  description = "Choose environment type. Possible values: EC2_Private, ECS_Cluster, EC2_Public"
}


resource "null_resource" "is_environment_type_valid" {
  count = "${contains(local.environment_list, var.environment_type) == true ? 0 : 1}"
  "ERROR: The environment type value can only be one of the following: EC2_Private, ECS_Cluster, EC2_Public" = true
}
  • Configuring the “conditions” counter 
This is one of the tricky parts in setting the conditionals counter. Let’s take the local “create_cloudfront” variable for example:
Our “create_cloudfront” local needs to have an integer counter value (Boolean can also be used) so we can call the “local” value later in our “count” meta-parameter.

variable "create_cloudfront" {
  type = "string"
  default = "-1"
  description = "Should Cloudfront be created? Only 0 and 1 can be used."
}


locals {
  create_cloudfront = "${var.create_cloudfront == "-1" ? "${var.env_type == "EC2_Public" || var.env_type == "ECS_Cluster" ? "1" : "0" }" : var.create_cloudfront}"

 create_ecs = "${var.create_ecs == "-1" ? "${var.env_type == "ECS_Cluster" ? "1" : "0" }" : var.create_ecs_cluster}"

 is_private = "${var.env_type == "EC2_Private"}"
}


In other programming languages, our “create_cloudfront” function will look something like this:

if ${var.create_cloudfront == "-1"} {
      if ${var.env_type == "EC2_Public" || var.env_type == "ECS_Cluster} {
                   local.create_cloudfront = 1;
      } else { local.create_cloudfront = 0 }
} else { local.create_cloudfront = var.create_cloudfront; }


The variable “create_cloudfront” has a default value “-1”. If we leave the default value, the process goes to the next step where we compare if the variable “env_type” is equal to an existing item in the list, if yes – local “create_cloudfront” takes value 1; if no – our local takes value 0. In other words if “env_type” equals “EC2_Private (not public)”, a Cloudfront resource will not be created.
We allow the end-user to manually override the value for our “local”. If the user inputs a value different than “-1”, the “local” gets the value of the variable inputted from the end-user.

  • Using the “count” parameter – second part of configuring conditionals
This is second of the tricky parts in setting the conditionals. Let’s take again the resource for deploying Cloudfront in AWS as a sample:

resource "aws_cloudfront_distribution" "Cloudfront_Resource" {
  count = "${local.create_cloudfront}"



}

The count parameter takes its value from the local "create_cloudfront" which we have configured above in the previous step. If the count parameter is equal to zero (0), Terraform will not create the resource.


Another interesting sample is when we choose between two possible scenarios:

resource "aws_launch_template" "ec2_launch_template" {
  count = "${1 - local.create_ecs}"



}

This configuration is used when we choose between having an ECS cluster in AWS or only a single EC2 Instance. Value for “local.create_ecs” will always be 0 or 1, if not manually overridden by user, depending on the env_type value (mentioned above). If we want to deploy ECS cluster then “ec2_launch_template” count parameter will receive the equation “1 – 1 = 0”. Following the same logic, if the “local.create_ecs” counter is set to 0, then the equation will be “1-0=1”, which means that the “ec2_launch_template” will be created.

The same logic as above can be configured in a different more versatile way using interpolation syntax and conditionals inside “count” parameter:

resource "aws_launch_template" "ec2_launch_template" {
  count = "${local.create_ecs_cluster == 0 ? 1 : 0}"



}


Another sample with using interpolation syntax inside “count” parameter:

variable "EBS_Volumes {
  type              = "list"
  description  = "Set a list of EBS Volumes in AWS"
  default         = []
}

resource "aws_ebs_volume" "EBS_Volumes" {
  count = "${length(var.EBS_Volumes)}"



}

Sample above will create as many resources as items we have in EBS_Volumes list (array).


  • Limitations
Terraform allows us to use different approaches for setting conditional logic in our Infrastructure as a Code, having in mind that we use declarative language. Sadly, there are some limitations using the “count” parameter. A significant limitation is you cannot use dynamic data in the count parameter. By “dynamic data”, we mean any data that is fetched from a provider (e.g. AWS) or is only available after a resource has been created (e.g. an output attribute of an EC2 Instance). This is caused by Terraform trying to resolve all the count parameters before fetching any dynamic data.

#Conclusion
In conclusion, using the basics of HCL conditionals and the “count” attribute for terraform resources, we can pretty easily set up a dynamic resource configuration. Frankly, even though this moves the focus a little bit from the original idea behind Terraform, we can see how using conditional resources is a useful trick to employ when it comes to finding a way to solve a specific need.
Good news are that HashiCorp are planning to make our work with conditionals and interpolation inside Terraform a little bit easier with presenting us the “Rich Value Types”. HCL has always supported primitive values (strings, numbers, booleans), and Terraform 0.7 added support for simple lists and maps. Unfortunately, due to limitations of the previous generation core of Terraform, lists and maps had a number of surprising limitations, but Terraform 0.12 will remove all of these limitations. In Terraform 0.12, complex objects will be passed to child modules as inputs and returned to parent modules as outputs. HashiCorp also introduced us a powerful type system that module authors can use to specify expected types for input variables. This forces values to be validated and converted properly if possible. This also improves the error messages shown to users of a module.

We are looking forward for all the changes that HashiCorp will introduce to us, so please stay tuned to our blog, as we plan to make a post about comparing the old methods for conditionals prior and after Terraform 0.12.

What do you think? Leave a comment below! ;)