Consul-Terraform-Sync is the glue that lets your Terraform infrastructure-as-code talk to your dynamic network environment.

Let’s see it in action. Imagine we have a service registered in Consul called webserver. This service has an IP address and port. We want to provision a load balancer in front of it using Terraform.

First, we need a Terraform configuration that knows how to provision a load balancer. This is typically a module that takes inputs like the service address and port.

# modules/loadbalancer/main.tf
resource "aws_lb" "main" {
  name               = "webserver-lb"
  internal           = false
  load_balancer_type = "application"
  subnets            = ["subnet-xxxxxxxxxxxxxxxxx", "subnet-yyyyyyyyyyyyyyyyy"]
  security_groups    = ["sg-zzzzzzzzzzzzzzzzz"]
}

resource "aws_lb_target_group" "main" {
  name     = "webserver-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = "vpc-aaaaaaaaaaaaaaaa"

  health_check {
    path = "/"
    protocol = "HTTP"
    interval = 30
    timeout  = 5
    healthy_threshold = 2
    unhealthy_threshold = 2
  }
}

resource "aws_lb_listener" "main" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

# This resource will be updated by Consul-Terraform-Sync
resource "aws_lb_target_group_attribute" "deregistration_delay" {
  target_group_arn = aws_lb_target_group.main.arn
  key              = "deregistration_delay.connection_termination.enabled"
  value            = "true"
}

resource "aws_lb_target_group_attribute" "deregistration_delay_time" {
  target_group_arn = aws_lb_target_group.main.arn
  key              = "deregistration_delay.connection_termination.timeout_seconds"
  value            = "300" # Default to 5 minutes
}

output "load_balancer_dns_name" {
  value = aws_lb.main.dns_name
}

Now, we need to tell Consul-Terraform-Sync how to use this Terraform configuration and how to get the service information from Consul. This is done via a consul-terraform-sync.hcl configuration file.

# consul-terraform-sync.hcl
log_level = "INFO"

sync {
  method = "consul"
  config {
    address = "127.0.0.1:8500" # Address of your Consul agent
    scheme  = "http"
  }
}

terraform {
  # Path to your Terraform configuration directory
  config-dir = "./modules/loadbalancer"
}

# This is where the magic happens. We're telling CTS to watch for services
# tagged with 'load-balancer-target' and to use that information to
# configure Terraform.
#
# The 'service-address' variable will be populated with the IP address
# of the service found in Consul.
#
# The 'service-port' variable will be populated with the port of the service.
#
# The 'service-id' variable will be populated with the ID of the service.
#
# We're also mapping these Consul variables to Terraform variables.
#
# The 'deregistration_delay.connection_termination.timeout_seconds' attribute
# is dynamically set based on the Consul service metadata.
#
# This allows us to automatically adjust the load balancer's deregistration
# timeout when a service instance is unhealthy or being updated.
auto-approve = true

services {
  name = "webserver-loadbalancer" # A descriptive name for this sync configuration
  # This is the tag Consul-Terraform-Sync will look for on services.
  # When a service has this tag, CTS will process it.
  tags = ["load-balancer-target"]

  # This block defines how Consul service information is mapped to Terraform variables.
  # The 'consul' section specifies which Consul service attributes to retrieve.
  # The 'terraform' section specifies how these attributes map to Terraform variables.
  consul {
    # This query will find all services that have the 'load-balancer-target' tag.
    # It will also filter for services that are currently healthy.
    query = "services tagged 'load-balancer-target' and service.checks.critical = 0"
  }

  # This block defines how Consul service data is translated into Terraform variables.
  # The key is the name of the Terraform variable, and the value is how it's
  # derived from Consul data.
  variables {
    # We're using the default behaviour here: the IP address of the service.
    # For more complex scenarios, you might access specific service metadata.
    service_address = "${consul.service.address}"
    service_port    = "${consul.service.port}"
    service_id      = "${consul.service.id}"

    # Dynamically set the deregistration delay based on a metadata key in Consul.
    # If the 'deregistration_timeout' metadata key exists, use its value.
    # Otherwise, default to 300 seconds.
    deregistration_timeout_seconds = "${consul.service.meta.deregistration_timeout | 300}"
  }

  # This block specifies how to apply the Terraform variables to the Terraform
  # configuration.
  #
  # The 'Terraform variable name' must match a variable defined in your Terraform
  # configuration (e.g., via a .tfvars file or directly in the .tf files).
  #
  # Here, we're mapping the 'deregistration_timeout_seconds' variable we defined
  # in the 'variables' block above to the 'deregistration_delay.connection_termination.timeout_seconds'
  # attribute in our Terraform HCL.
  #
  # This allows Consul-Terraform-Sync to dynamically update the Terraform state
  # with the correct timeout value based on Consul service metadata.
  #
  # Note: For simpler variable mappings, you could also use a .tfvars file.
  # However, for dynamic updates based on Consul data, this approach is more powerful.
  terraform-variables {
    # This maps the Terraform variable 'deregistration_delay.connection_termination.timeout_seconds'
    # to the value of the 'deregistration_timeout_seconds' variable we defined above.
    # Consul-Terraform-Sync will automatically update the corresponding resource attribute
    # in your Terraform state.
    "deregistration_delay.connection_termination.timeout_seconds" = "${deregistration_timeout_seconds}"
  }
}

When Consul-Terraform-Sync starts, it will:

  1. Connect to Consul: It uses the sync configuration to establish a connection to your Consul agent.
  2. Watch for Services: It continuously monitors Consul for services that match the tags = ["load-balancer-target"].
  3. Query Services: When a matching service is found (and is healthy, due to the query), it executes the Consul query services tagged 'load-balancer-target' and service.checks.critical = 0.
  4. Populate Variables: It extracts service_address, service_port, service_id, and deregistration_timeout_seconds from the Consul service data. The deregistration_timeout_seconds will be set to 300 if the service’s metadata doesn’t specify a deregistration_timeout key.
  5. Apply Terraform Variables: It takes the populated deregistration_timeout_seconds value and applies it to the Terraform configuration, specifically targeting the aws_lb_target_group_attribute resource named deregistration_delay_time by setting its value to the dynamic timeout.
  6. Run Terraform: If there are any changes detected by Terraform (in this case, the value of the aws_lb_target_group_attribute resource), it will run terraform apply with the necessary variables.

The most surprising true thing about this setup is that Consul-Terraform-Sync doesn’t just provision infrastructure; it actively manages it by reacting to changes in your service catalog.

Here’s how the aws_lb_target_group_attribute.deregistration_delay_time resource in your Terraform configuration would be updated when the deregistration_timeout_seconds variable changes:

If your webserver service in Consul has metadata like this:

{
  "ID": "webserver-instance-12345",
  "Name": "webserver",
  "Address": "10.0.1.10",
  "Port": 80,
  "Meta": {
    "deregistration_timeout": "60"
  },
  "Checks": [
    {
      "Name": "service",
      "Status": "passing",
      "ID": "service:webserver-instance-12345"
    }
  ]
}

Consul-Terraform-Sync will see the deregistration_timeout: "60" metadata and set deregistration_timeout_seconds = 60. Then, it will execute terraform apply, which will update the aws_lb_target_group_attribute resource to:

resource "aws_lb_target_group_attribute" "deregistration_delay_time" {
  target_group_arn = aws_lb_target_group.main.arn
  key              = "deregistration_delay.connection_termination.timeout_seconds"
  value            = "60" # This value is now dynamically set
}

This means that when a webserver instance is about to be deregistered (e.g., during a deployment or if it fails a health check), the load balancer will wait for 60 seconds before terminating existing connections, giving active requests more time to complete. If the metadata was absent, it would default to 300 seconds.

The core idea is that your infrastructure (managed by Terraform) is now aware of and responsive to the state of your application services (registered in Consul). When a service is added, removed, or its health status changes, Consul-Terraform-Sync can automatically adjust the underlying infrastructure.

A key detail often missed is how Consul-Terraform-Sync handles the resolution of multiple service instances. When your Consul query returns more than one service instance, Consul-Terraform-Sync will iterate through each one, creating a separate Terraform apply for each. This means if you have three webserver instances tagged load-balancer-target, CTS will run terraform apply three times, each time potentially updating a different part of your infrastructure if your Terraform code is designed to handle multiple targets (e.g., iterating over a list of IPs to add to a target group). If your Terraform configuration is designed to manage a single target group and doesn’t explicitly handle multiple service instances from Consul directly, it might only pick up the first one or fail depending on how the variables are structured. The consul.service.address is specific to one service instance at a time during the iteration.

The next concept you’ll likely explore is using Consul-Terraform-Sync for more complex scenarios, such as managing firewall rules or DNS records based on service registrations.

Want structured learning?

Take the full Consul course →