• HashiCorp Cloud Platform
    • Terraform
    • Packer
    • Consul
    • Vault
    • Boundary
    • Nomad
    • Waypoint
    • Vagrant
  • Sign up
Operational Excellence Labs

Well-Architected Framework

Skip to main content
  • Validate Modules with Custom Conditions

  • Resources

  • Tutorial Library
  • Community Forum
    (opens in new tab)
  • Support
    (opens in new tab)
  • GitHub
    (opens in new tab)
  1. Developer
  2. Well-Architected Framework
  3. Operational Excellence Labs
  4. Validate Modules with Custom Conditions

Validate Modules with Custom Conditions

  • 16min

  • TerraformTerraform

Terraform lets you define custom conditions in your module configuration to validate resources, data sources, and outputs. When planning and applying changes to your infrastructure, Terraform evaluates these condition blocks and reports an error if a condition fails. Terraform supports preconditions, which it evaluates before it provisions the enclosing block, and postconditions, which it evaluates afterward.

In this tutorial, you will provision resources using a local module that represents an application deployment, including a load balancer and EC2 instances. While this module includes variable validation, it is still possible for users of the module to misconfigure the application. You will add condition blocks to the module to ensure that users configure the VPC and EC2 instances correctly.

Prerequisites

You can complete this tutorial using the same workflow with either Terraform OSS or Terraform Cloud. Terraform Cloud is a platform that you can use to manage and execute your Terraform projects. It includes features like remote state and execution, structured plan output, workspace resource summaries, and more.

Select the Terraform Cloud tab to complete this tutorial using Terraform Cloud.

This tutorial assumes that you are familiar with the standard Terraform workflow. If you are new to Terraform, complete the Get Started tutorials first.

For this tutorial, you will need:

  • Terraform 1.2+ installed locally.
  • An AWS account with credentials configured for Terraform.

This tutorial assumes that you are familiar with the Terraform and Terraform Cloud workflows. If you are new to Terraform, complete Get Started tutorials first. If you are new to Terraform Cloud, complete the Terraform Cloud Get Started tutorials first.

In order to complete this tutorial, you will need the following:

  • Terraform 1.2+ installed locally.
  • An AWS account with credentials configured for Terraform.
  • A Terraform Cloud account with Terraform Cloud locally authenticated.
  • A Terraform Cloud variable set configured with your AWS credentials.

Note: Some of the infrastructure in this tutorial does not qualify for the AWS free tier. Destroy the infrastructure at the end of the guide to avoid unnecessary charges. We are not responsible for any charges that you incur.

Clone the example repository

Clone the example repository for this tutorial, which contains Terraform configuration that uses a local module to deploy an application hosted on AWS.

$ git clone https://github.com/hashicorp/learn-terraform-conditions.git

Change into the repository directory.

$ cd learn-terraform-conditions

Review example configuration

The example configuration defines a VPC to host your application, selects an AWS AMI using a data source, and uses a local module to deploy EC2 instances and a load balancer into the VPC.

Open main.tf to review the initial configuration. The module.app block in main.tf configures the example-app-deployment module with several arguments.

main.tf
module "app" {
  source = "./modules/example-app-deployment"

  aws_instance_count = var.aws_instance_count

  aws_instance_type = var.aws_instance_type
  aws_ami_id        = data.aws_ami.amazon_linux.id
  aws_vpc_id        = module.vpc.vpc_id

  aws_public_subnet_ids  = module.vpc.public_subnets
  aws_private_subnet_ids = module.vpc.private_subnets
}

The module configuration in modules/example-app-deployment/main.tf defines the infrastructure that will host your example application, which consists of a load balancer, security groups, and EC2 instances. This local module uses public modules from the Terraform registry to provision your security groups and load balancer.

The EC2 instance configuration references input variables passed from the root module to set the number of instances, the instance type, AMI ID, and private subnets to provision the instances in.

modules/example-app-deployment/main.tf
resource "aws_instance" "app" {
  count = var.aws_instance_count

  instance_type = var.aws_instance_type
  ami           = var.aws_ami_id

  subnet_id              = var.aws_private_subnet_ids[count.index % length(var.aws_private_subnet_ids)]
  vpc_security_group_ids = [module.app_security_group.security_group_id]
}

The variables.tf file defines several input variables for your module, including the VPC ID and subnets to deploy the application in.

modules/example-app-deployment/variables.tf
# Input variables

variable "aws_vpc_id" {
  description = "ID of the VPC to deploy in. DNS support must be enabled on this VPC."
  type        = string
}

variable "aws_private_subnet_ids" {
  description = "VPC private subnet ids."
  type        = list(string)

  validation {
    condition     = length(var.aws_private_subnet_ids) > 1
    error_message = "This application requires at least two private subnets."
  }
}

variable "aws_public_subnet_ids" {
  description = "VPC public subnet ids."
  type        = list(string)
}

variable "aws_ami_id" {
  description = "EC2 instance AMI ID."
  type        = string
}

variable "aws_instance_count" {
  description = "Number of AWS instances to deploy. This number must be evenly divisible by the number of private subnets."
  type        = number

  validation {
    condition     = var.aws_instance_count > 1
    error_message = "This application requires at least two EC2 instances."
  }
}

variable "aws_instance_type" {
  description = "EC2 instance type."
  type        = string
}

Plan changes

Terraform configuration can be syntactically valid and deployable, but still not satisfy other constraints such as application-specific requirements. When you maintain a module, you can use custom conditions in your configuration to enforce these requirements.

In the root module, rename the terraform.tfvars.example file to terraform.tfvars, so that Terraform will detect the file with end-user-configured variables in it.

$ mv terraform.tfvars.example terraform.tfvars

This file sets values for three of the variables used by the example configuration. Terraform can deploy your infrastructure with these values, but they do not meet the hypothetical requirements of the example application, which needs an EC2 instance that supports EBS optimization, and a VPC that has DNS support enabled.

terraform.tfvars
aws_instance_type = "t2.micro"

aws_instance_count = 3

enable_dns = false

If you were deploying a real application with these requirements, the application would fail on the configured infrastructure with little indication of what might be wrong. A developer familiar with the application requirements would have to diagnose the issues once the application was already deployed, and would have trace the cause to these misconfigured variables.

In this tutorial, you will add conditions to the module to ensure that:

  • Each private subnet has the same number of EC2 instances.
  • The EC2 instance type supports EBS optimization.
  • The VPC has DNS support enabled.

By adding these conditions, you will ensure that users cannot deploy the application on infrastructure that does not meet the application's requirements.

Initialize the configuration.

$ terraform init
Initializing modules...
- app in modules/example-app-deployment
Downloading registry.terraform.io/terraform-aws-modules/security-group/aws 4.9.0 for app.app_security_group...
- app.app_security_group in .terraform/modules/app.app_security_group/modules/web
- app.app_security_group.sg in .terraform/modules/app.app_security_group
Downloading registry.terraform.io/terraform-aws-modules/elb/aws 3.0.1 for app.elb_http...
- app.elb_http in .terraform/modules/app.elb_http
- app.elb_http.elb in .terraform/modules/app.elb_http/modules/elb
- app.elb_http.elb_attachment in .terraform/modules/app.elb_http/modules/elb_attachment
Downloading registry.terraform.io/terraform-aws-modules/security-group/aws 4.9.0 for app.lb_security_group...
- app.lb_security_group in .terraform/modules/app.lb_security_group/modules/web
- app.lb_security_group.sg in .terraform/modules/app.lb_security_group
Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 3.14.0 for vpc...
- vpc in .terraform/modules/vpc

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Reusing previous version of hashicorp/random from the dependency lock file
- Installing hashicorp/aws v4.10.0...
- Installed hashicorp/aws v4.10.0 (signed by HashiCorp)
- Installing hashicorp/random v3.1.3...
- Installed hashicorp/random v3.1.3 (signed by HashiCorp)

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Open your terraform.tf file and uncomment the cloud block. Replace the organization name with your own Terraform Cloud organization.

terraform.tf
terraform {
  cloud {
    organization = "organization-name"
    workspaces {
      name = "learn-terraform-conditions"
    }
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.10.0"
    }
  }

  required_version = "~> 1.2.0"
}

Initialize your configuration. Terraform will automatically create the learn-terraform-conditions workspace in your Terraform Cloud organization.

$ terraform init
Initializing Terraform Cloud...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v4.10.0...
- Installed hashicorp/aws v4.10.0 (signed by HashiCorp)
Terraform Cloud has been successfully initialized!
You may now begin working with Terraform Cloud. Try running "terraform plan" to
see any changes that are required for your infrastructure.
If you ever set or change modules or Terraform Settings, run "terraform init"
again to reinitialize your working directory.

Note: This tutorial assumes that you are using a tutorial-specific Terraform Cloud organization with a global variable set of your AWS credentials. Review the Create a Credential Variable Set for detailed guidance. If you are using a scoped variable set, assign it to your new workspace now.

Before you add conditions to the example module, execute a plan to review the resources that Terraform will deploy.

$ terraform plan
data.aws_availability_zones.available: Reading...
data.aws_ami.amazon_linux: Reading...
data.aws_availability_zones.available: Read complete after 0s [id=us-west-2]
data.aws_ami.amazon_linux: Read complete after 2s [id=ami-00af37d1144686454]

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # module.app.data.aws_subnet.public[0] will be read during apply

##...

Plan: 42 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

The plan reports that Terraform is ready to apply your configuration. Before you do so, add conditions to your module to ensure that your users configure the application correctly.

Add preconditions

Terraform allows you to add preconditions and postconditions to the lifecycle of resource, data source, or output blocks. Terraform evaluates preconditions before the enclosing block, validating that your configuration is compliant before it applies it. Terraform evaluates post conditions after the enclosing block, letting you confirm that the results of applied changes are compliant before it applies the rest of your configuration.

Update modules/example-app-deployment/main.tf to include a data source that looks up the instance type. Add two preconditions to the the aws_instance.app block for your EC2 instances to check the number of instances per subnet, and instance type.

modules/example-app-deployment/main.tf
data "aws_ec2_instance_type" "app" {
  instance_type = var.aws_instance_type
}

resource "aws_instance" "app" {
  count = var.aws_instance_count

  instance_type = var.aws_instance_type
  ami           = var.aws_ami_id

  subnet_id              = var.aws_private_subnet_ids[count.index % length(var.aws_private_subnet_ids)]
  vpc_security_group_ids = [module.app_security_group.security_group_id]

  lifecycle {
    precondition {
      condition     = var.aws_instance_count % length(var.aws_private_subnet_ids) == 0
      error_message = "The number of instances (${var.aws_instance_count}) must be evenly divisible by the number of private subnets (${length(var.aws_private_subnet_ids)})."
    }

    precondition {
      condition     = data.aws_ec2_instance_type.app.ebs_optimized_support != "unsupported"
      error_message = "The EC2 instance type (${var.aws_instance_type}) must support EBS optimization."      
    }
  }
}

The first precondition verifies that each private subnet contains the same number of instances. It does so by dividing the number of instances by the number of subnets, and checking that the remainder is 0. This condition ensures that application traffic is spread evenly across the subnets used by your application.

The second precondition verifies that the chosen EC2 instance type supports EBS optimization. In order to do so, it accesses the instance type's ebs_optimized_support attribute from the data source.

Trigger a condition failure

Attempt to plan this configuration, and Terraform will report that the preconditions failed.

$ terraform plan

##...
â•·
│ Error: Resource precondition failed
│
│   on modules/example-app-deployment/main.tf line 93, in resource "aws_instance" "app":
│   93:       condition     = var.aws_instance_count % length(var.aws_private_subnet_ids) == 0
│     ├────────────────
│     │ var.aws_instance_count is 3
│     │ var.aws_private_subnet_ids is list of string with 2 elements
│
│ The number of instances (3) must be evenly divisible by the number of private
│ subnets (2).
╵

##...

â•·
│ Error: Resource precondition failed
│
│   on modules/example-app-deployment/main.tf line 98, in resource "aws_instance" "app":
│   98:       condition     = data.aws_ec2_instance_type.app.ebs_optimized_support != "unsupported"
│     ├────────────────
│     │ data.aws_ec2_instance_type.app.ebs_optimized_support is "unsupported"
│
│ The EC2 instance type (t2.micro) must support EBS optimization.
╵

Note: The configuration uses the count meta-argument to create a number of EC2 instances equal to the value of the aws_instance_count variable, currently set to 3. Terraform reports errors for both preconditions for each instance.

Terraform reports errors whenever a condition fails, and will not continue to plan or apply your configuration. You must resolve the errors before you can successfully deploy this configuration.

Plan configuration with correct values

Earlier in this tutorial, you set the number of instances and the instance type in terraform.tfvars. Update these to values that are compatible with the conditions you added to the example module.

First, update the instance type to one that supports EBS optimization (t3.micro). Second, update the aws_instance_count variable to deploy four instances, so that the number of instances is evenly divisible by the number of private subnets.

terraform.tfvars
aws_instance_type = "t3.micro"

aws_instance_count = 4

enable_dns = false

Plan this configuration again, and verify that it satisfies the preconditions.

$ terraform plan
data.aws_ami.amazon_linux: Reading...
data.aws_availability_zones.available: Reading...
module.app.data.aws_ec2_instance_type.app: Reading...
data.aws_availability_zones.available: Read complete after 1s [id=us-west-2]
module.app.data.aws_ec2_instance_type.app: Read complete after 1s [id=t3.micro]
data.aws_ami.amazon_linux: Read complete after 2s [id=ami-00af37d1144686454]

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # module.app.data.aws_subnet.public[0] will be read during apply

##...

Plan: 44 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Both of the preconditions checks now pass, and Terraform is ready to apply your configuration. Before you do so, add a postcondition to the example module.

Add a postcondition

The root configuration in this project creates a VPC using a public module. The example-app-deployment module expects DNS support to be enabled on the VPC. Add a data source to the application module that looks up the created VPC by its ID and uses a postcondition to verify that DNS support is enabled.

Add the following data source to modules/example-app-deployment/main.tf.

modules/example-app-deployment/main.tf
data "aws_vpc" "app" {
  id = var.aws_vpc_id

  lifecycle {
    postcondition {
      condition     = self.enable_dns_support == true
      error_message = "The selected VPC must have DNS support enabled."      
    }
  }
}

The postcondition refers to the data source using the self value. Terraform will not create the VPC until you apply the example configuration, so it cannot validate this condition until after it has begun provisioning your infrastructure. When you run terraform apply, Terraform will start applying the configuration, and will create the VPC before it reads its attributes from the data source. After it does so, it will evaluate the postcondition and report an error if it fails.

Apply configuration

Apply your configuration now. Respond to the confirmation prompt with a yes, and Terraform will begin applying your changes, and then report an error when the postcondition on your data source fails.

$ terraform apply
data.aws_availability_zones.available: Reading...
data.aws_ami.amazon_linux: Reading...
module.app.data.aws_ec2_instance_type.app: Reading...
data.aws_availability_zones.available: Read complete after 0s [id=us-west-2]
module.app.data.aws_ec2_instance_type.app: Read complete after 0s [id=t3.micro]
data.aws_ami.amazon_linux: Read complete after 2s [id=ami-00af37d1144686454]

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

##...

Plan: 44 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.app.random_string.lb_id: Creating...

##...

module.vpc.aws_route.private_nat_gateway[1]: Creation complete after 2m46s [id=r-rtb-033b9945bd9cd2e2f1080289494]
â•·
│ Error: Resource postcondition failed
│
│   on modules/example-app-deployment/main.tf line 8, in data "aws_vpc" "app":
│    8:       condition     = self.enable_dns_support == true
│     ├────────────────
│     │ self.enable_dns_support is false
│
│ The selected VPC must have DNS support enabled.
╵

Resolve this error by enabling DNS support on your VPC. Update the value for the enable_dns variable in terraform.tfvars.

terraform.tfvars
aws_instance_type = "t3.micro"

aws_instance_count = 4

enable_dns = true

Apply the configuration again. Respond to the confirmation prompt with a yes.

$ terraform apply
module.app.random_string.lb_id: Refreshing state... [id=fYjWkKeE]
data.aws_availability_zones.available: Reading...
data.aws_ami.amazon_linux: Reading...
module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-01395e9cd6796be23]
module.app.data.aws_ec2_instance_type.app: Reading...

##...

Terraform will perform the following actions:

  # module.app.data.aws_vpc.app will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_vpc" "app" {
      + arn                     = (known after apply)
      + cidr_block              = (known after apply)
      + cidr_block_associations = (known after apply)
      + default                 = (known after apply)
      + dhcp_options_id         = (known after apply)
      + enable_dns_hostnames    = (known after apply)
      + enable_dns_support      = (known after apply)
      + id                      = "vpc-01395e9cd6796be23"
      + instance_tenancy        = (known after apply)
      + ipv6_association_id     = (known after apply)
      + ipv6_cidr_block         = (known after apply)
      + main_route_table_id     = (known after apply)
      + owner_id                = (known after apply)
      + state                   = (known after apply)
      + tags                    = (known after apply)
    }

  # module.vpc.aws_vpc.this[0] will be updated in-place
  ~ resource "aws_vpc" "this" {
      ~ enable_dns_support               = false -> true
        id                               = "vpc-01395e9cd6796be23"
        tags                             = {
            "Name" = ""
        }
        # (15 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.vpc.aws_vpc.this[0]: Modifying... [id=vpc-01395e9cd6796be23]
module.vpc.aws_vpc.this[0]: Still modifying... [id=vpc-01395e9cd6796be23, 10s elapsed]
module.vpc.aws_vpc.this[0]: Modifications complete after 11s [id=vpc-01395e9cd6796be23]
module.app.data.aws_vpc.app: Reading...
module.app.data.aws_vpc.app: Read complete after 0s [id=vpc-01395e9cd6796be23]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

After updating your VPC, Terraform read the new value for enable_dns_support from the aws_vpc.app data source inside the example-app-deployment module and evaluated the postcondition. Since DNS support is now enabled, the postcondition succeeded.

Destroy infrastructure

Destroy the infrastructure you created in this tutorial. Respond to the confirmation prompt with a yes.

$ terraform destroy
##...
Plan: 0 to add, 0 to change, 44 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

module.vpc.aws_route_table_association.private[1]: Destroying... [id=rtbassoc-05384faafb2410975]
##...

Destroy complete! Resources: 44 destroyed.

If you used Terraform Cloud for this tutorial, after destroying your resources, delete the learn-terraform-conditions workspace from your Terraform Cloud organization.

Next steps

In this tutorial you learned about the behavior and benefits of preconditions and postconditions. Conditions allow module authors to write configuration that is easier for other people to use successfully, by validating multiple conditions either before or after resource provisioning.

For more information on topics covered in this tutorial, check out the following resources:

  • Read the Terraform custom conditions documentation.

  • Follow the Customize Terraform Configuration with Variables tutorial to learn how to create Terraform variables and how to validate the values of individual variables.

  • Complete the Reuse Configuration with Modules to learn how to create and publish custom Terraform modules.

  • Learn how to configure run tasks in Terraform Cloud.

 Back to Collection
 Next Collection

This tutorial also appears in:

  •  
    18 tutorials
    Write Terraform Configuration
    Learn Terraform configuration language by example. Write configurations to manage multiple pieces of infrastructure and iterate over structured data. Deploy and manage related infrastructure by referring to resources in other configurations.
    • Terraform

On this page

  1. Validate Modules with Custom Conditions
  2. Prerequisites
  3. Clone the example repository
  4. Review example configuration
  5. Plan changes
  6. Add preconditions
  7. Trigger a condition failure
  8. Plan configuration with correct values
  9. Add a postcondition
  10. Apply configuration
  11. Destroy infrastructure
  12. Next steps
Give Feedback(opens in new tab)
  • Certifications
  • System Status
  • Terms of Use
  • Security
  • Privacy
  • Trademark Policy
  • Trade Controls
  • Give Feedback(opens in new tab)