TerraformPilot

Terraform

Terraform Consul Backend and Provider

Use HashiCorp Consul with Terraform for state storage, service discovery, and KV configuration: Consul backend, provider resources, and mesh intentions.

LLuca Berton1 min read

Quick Answer

#
terraform {
  backend "consul" {
    address = "consul.example.com:8500"
    scheme  = "https"
    path    = "terraform/myapp/state"
  }
}

Consul Backend Configuration

#
terraform {
  backend "consul" {
    address      = "consul.example.com:8500"
    scheme       = "https"
    path         = "terraform/${var.project}/${var.environment}"
    access_token = var.consul_token  # Or CONSUL_HTTP_TOKEN env var
    lock         = true
    gzip         = true  # Compress state
  }
}

Environment Variables

#
export CONSUL_HTTP_ADDR="consul.example.com:8500"
export CONSUL_HTTP_TOKEN="your-acl-token"
export CONSUL_HTTP_SSL=true
# Minimal config with env vars
terraform {
  backend "consul" {
    path = "terraform/myapp/production"
  }
}

Consul Provider

#
provider "consul" {
  address    = "consul.example.com:8500"
  scheme     = "https"
  token      = var.consul_token
  datacenter = "dc1"
}

KV Store Management

#
# Write configuration values
resource "consul_keys" "app_config" {
  key {
    path  = "config/myapp/database_url"
    value = "postgres://${aws_db_instance.main.address}:5432/myapp"
  }
 
  key {
    path  = "config/myapp/redis_url"
    value = "redis://${aws_elasticache_replication_group.main.primary_endpoint_address}:6379"
  }
 
  key {
    path  = "config/myapp/environment"
    value = var.environment
  }
}
 
# Read configuration values
data "consul_keys" "shared" {
  key {
    name = "vpc_id"
    path = "terraform/networking/vpc_id"
  }
 
  key {
    name    = "domain"
    path    = "config/shared/domain"
    default = "example.com"
  }
}
 
resource "aws_instance" "web" {
  subnet_id = data.consul_keys.shared.var.vpc_id
  # ...
}

Service Registration

#
resource "consul_service" "web" {
  name    = "web"
  node    = "web-server-1"
  port    = 8080
  address = aws_instance.web.private_ip
 
  tags = ["production", "v2"]
 
  check {
    check_id = "web-health"
    name     = "HTTP Health Check"
    http     = "http://${aws_instance.web.private_ip}:8080/health"
    interval = "10s"
    timeout  = "5s"
  }
}

Service Discovery (Data Source)

#
data "consul_service" "database" {
  name = "postgres"
  tag  = "primary"
}
 
# Use discovered service address
locals {
  db_address = data.consul_service.database.service[0].address
  db_port    = data.consul_service.database.service[0].port
}

ACL Policies

#
resource "consul_acl_policy" "app" {
  name  = "app-policy"
  rules = <<-RULES
    key_prefix "config/myapp/" {
      policy = "read"
    }
    service "web" {
      policy = "write"
    }
    node_prefix "" {
      policy = "read"
    }
  RULES
}
 
resource "consul_acl_token" "app" {
  description = "App service token"
  policies    = [consul_acl_policy.app.name]
}

Intentions (Service Mesh)

#
resource "consul_config_entry" "web_to_db" {
  kind = "service-intentions"
  name = "postgres"
 
  config_json = jsonencode({
    Sources = [{
      Name       = "web"
      Action     = "allow"
      Precedence = 9
      Type       = "consul"
    }]
  })
}

Consul vs S3/GCS Backend

#
FeatureConsulS3/GCS
State lockingBuilt-inRequires DynamoDB/native
Service discovery✅❌
KV config store✅❌
Health checks✅❌
Setup complexityHigherLower
Best forMulti-service appsSimple state storage
#

Conclusion

#

Use the Consul backend when you're already running Consul for service discovery. It provides state locking without extra infrastructure (unlike S3+DynamoDB). The Consul provider is powerful for managing KV configuration, service registration, ACL policies, and service mesh intentions alongside your infrastructure.

#Terraform#Consul#HashiCorp#State Management#Infrastructure as Code

Share this article