On the 10th of August Hashicorp released the latest version of their Infrastructure as Code product, Terraform 0.13. This release appears to be not as transformational as their previous release which caused a storm with the breaking changes to the way that the HCL language was formatted. Unfortunately, this meant that the majority of Terraform scripts on GitHub and in day-to-day use had to be re-written before people could utilize the benefits of version 0.12.
This had the effect of slowing down mainstream adoption of this release. This may have appeared to be a mistake on the behalf of Hashicorp but these changes were required to increase the stability and to rationalize and standardize layout standards. To read about the changes in version 0.12 see this article.
Conversely, Terraform 0.13 does not have any real functional or foundational changes. This is more of a new features release and there are some really useful additions to the product.
Required_providers in Terraform 0.13
Prior to version 0.13 we could only use the “required_providers” tag with the default terraform registries. Terraform 0.13 introduces the concept of a hierarchical namespace for providers that are community maintained or internal to a location. So we are able to expand the namespace to allow private registries to be set to a certain version. This is great for those environments that have direct access to the official registry for example enhanced security zones, there will be no requirement to punch a hole in your firewall to download any providers as they can be locally hosted.
terraform { required_providers { azurerm = ">= 2.0” } }
The syntax will change to the following not that presently Terraform will accept code as declared above as the default answer to source will be the default terraform repository, however a later release will enforce the new syntax so it is a good idea to get used to it as soon as possible.
terraform { required_providers { azurerm = { source = "registry.terraform.io/hashicorp/azurerm" version = "2.0" } } }
Now if we wish to add a custom provider to our terraform block
To add a private or alternative repository that is not maintained by HashiCorp, you would use the following code:
terraform { required_providers { my_repository = { source = "my-web-site/my_folder/my_repositoryrandom" version = "1.0" } } }
For_each and Count
The second feature of note is the addition of the use of the for_each and count arguments to modules, these have been available to resource block for a while but the addition of the functions to the module block is a welcome addition.
These are actually very powerful features, that will significantly streamline code. Take the for_each option now instead of having to repeat resource blocks a single module can be used to prepare many pieces of infrastructure. For example, if you use a module to create a Kubernetes cluster across multiple regions; you can now add the for_each option to your module stanza to loop though your regions and create the required resources in each of the declared regions
variable "project_id" { type = string } variable "regions" { type = map(object({ region = string network = string subnetwork = string ip_range_pods = string ip_range_services = string }) ) } module "kubernetes_cluster" { source = "modules/kubernetes-engine/EKS" for_each = var.regions project_id = var.project_id name = each.key region = each.value.region network = each.value.network subnetwork = each.value.subnetwork ip_range_pods = each.value.ip_range_pods ip_range_services = each.value.ip_range_services }
After declaring the object for the map, once we run the necessary code the module will loop through the map until all the entities have been created.
The Count function is for defining a distinct number of objects for example with version 0.12 if we needed to deploy 5 VM’s we would create a resource in the main file and add the count parameter, and for those options that needed uniqueness, we would add a ${count.index} to the value. Well with Terraform 0.13 we can now use module blocks to do the same thing.
variable "bucket_names" { type = type("string") default = ["prod", "qa", "dev"] } module "bucket_deploy" { source = "terraform-aws-modules/s3-bucket/aws" count = length(var.bucket_names) region = var.region bucket = var.bucket_names[count.index] }
This may not seem too big a thing, but when operating at scale this will be a serious time saver.
Depends_on
Another major improvement is the addition of depends_on to modules, In version 0.12 and earlier we would have to do major cartwheels and contortions to set up dependencies when using a module. There were various hacks that just over-complicated deployments. Now with the addition of depends_on, we can keep our code cleaner with modules and set dependencies, for example, if building out a new EKS cluster we need to wait until the S3 buckets are configured before starting to build out the cluster.
variable "bucket_names" { type = type("string") default = ["prod", "test", "dev", “stage”] } module "bucket_deploy" { source = "terraform-aws-modules/s3-bucket/aws" count = length(var.bucket_names) region = var.region bucket = var.bucket_names[count.index] } locals { resources = { wg-prod = "prod-eks" wg-test = "test-eks" wg-dev = "dev-eks" wq-stage = "stage-eks" } } module "my-cluster" { source = "terraform-aws-modules/eks/aws" for_each = local.resources cluster_name = each.value cluster_version = "1.18" subnets = ["subnet-abcde012", "subnet-bcde012a", "subnet-fghi345a"] vpc_id = "vpc-1234556abcdef" worker_groups = [ { name = each.key instance_type = "m4.large" asg_max_size = 3 } ] depends_on = [module.bucket_deploy] }
With this code, we can see that our cluster will not start deploying the cluster until all the S3 buckets have been created.
Variable validation
This is not exactly a new feature but it has reached stability in the code, previously it was in experimental mode, which meant it could be dropped at any time. Now we can set validation checks against variables. In the example below we have checking for a correctly formatted AMI image name, if the format of the image name does not start with ami- the code will error out with the message “Must be an AMI id, starting with “ami-“.”
variable "my_image_id" { type = string description = "The id of the machine image (AMI) to use for the server." validation { # regex(...) fails if it cannot find a match # can(...) returns false if the code it contains produces an error condition = can(regex("^ami-", var.image_id)) error_message = "Must be an AMI id, starting with \"ami-\"." } }
Breaking changes
There are a couple of breaking changes of note with this version, but nothing major like we had with version 0.12. They could still be impactful if you are running older versions of macOS or FreeBSD and this impact would obviously need to be taken into consideration when considering an upgrade strategy.
- The macOS build of Terraform CLI is no longer compatible with macOS 10.10 Yosemite and it requires macOS 10.11 El Capitan. Further, this release is the last major release that will support 10.11 El Capitan, it is recommended that macOS 10.12 Sierra or later is installed if you are.
- The FreeBSD builds of Terraform CLI are no longer compatible with FreeBSD 10.x, which has reached end-of-life. Terraform now requires FreeBSD 11.2 or later.
Summary
This is a release rather than a transformational release. Are these new features enough to drive uptake? Yes. The addition of for_each, count, and depends_on to modules will drive a large amount of code simplification. Also coupled with the validation block they make quite a compelling story for upgrading your legacy 0.11 and earlier code.