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:
- Connect to Consul: It uses the
syncconfiguration to establish a connection to your Consul agent. - Watch for Services: It continuously monitors Consul for services that match the
tags = ["load-balancer-target"]. - Query Services: When a matching service is found (and is healthy, due to the
query), it executes the Consul queryservices tagged 'load-balancer-target' and service.checks.critical = 0. - Populate Variables: It extracts
service_address,service_port,service_id, andderegistration_timeout_secondsfrom the Consul service data. Thederegistration_timeout_secondswill be set to300if the service’s metadata doesn’t specify aderegistration_timeoutkey. - Apply Terraform Variables: It takes the populated
deregistration_timeout_secondsvalue and applies it to the Terraform configuration, specifically targeting theaws_lb_target_group_attributeresource namedderegistration_delay_timeby setting itsvalueto the dynamic timeout. - Run Terraform: If there are any changes detected by Terraform (in this case, the
valueof theaws_lb_target_group_attributeresource), it will runterraform applywith 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.