Intermediate

Terraform: Managing Multiple Configurations

Data sources, remote state sharing and external data sources for cross-configuration sharing.

Table of Contents

  1. Overview
  2. Module 1 — Data Sources in Terraform
  3. Module 2 — Remote Data Sharing with State
  4. Module 3 — External Data Sources for Cross-configuration Sharing
  5. Architecture Diagrams
  6. Reference Tables
  7. HCL Code Snippets
  8. Best Practices
  9. Summary

Overview

When first adopting Terraform, you typically manage a single configuration that grows over time. At some point, it becomes necessary to split this configuration into distinct blocks for:

  • Separation of concerns
  • Maintainability and ease of debugging
  • Blast radius reduction (limiting the impact of a change)
  • Faster development cycles

This guide focuses on communication between these configurations — how to share information from one configuration to another in a secure, decoupled way.


Module 1 — Data Sources in Terraform

What is a Data Source?

A data source in Terraform is an object defined by a provider that allows querying a platform or service to extract information. Unlike a resource, a data source is read-only — nothing is created, only consulted.

Three ways to inject external information into Terraform:

MethodDescriptionTypical Usage
Input variablesValues passed at runtimeSimple parameterization
File functionsfile(), templatefile()Static content or templates
Data sourcesPlatform queriesDynamic data

Data source use cases:

  • Retrieve available Availability Zones in an AWS region
  • Query an AMI to get the most recent ID
  • Read a TLS certificate from a certificate authority
  • Get the current public IP address for firewall rules
  • Retrieve AWS account info (account ID, etc.)

Dynamic Data Sources

Availability Zones Data Source

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "public-subnet-${count.index + 1}"
  }
}

Dynamic AMI Data Source

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = aws_subnet.public[0].id
}

Generative Data Sources

Some data sources don’t query an external platform but generate an internal document.

aws_iam_policy_document

data "aws_iam_policy_document" "allow_s3" {
  statement {
    effect    = "Allow"
    actions   = ["s3:GetObject", "s3:PutObject"]
    resources = ["arn:aws:s3:::my-bucket/*"]

    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

resource "aws_iam_policy" "s3_access" {
  name   = "s3-access-policy"
  policy = data.aws_iam_policy_document.allow_s3.json
}

cloudinit_config (startup script)

data "cloudinit_config" "web_startup" {
  gzip          = true
  base64_encode = true

  part {
    content_type = "text/x-shellscript"
    content      = templatefile("${path.module}/templates/startup.sh.tpl", {
      environment  = var.environment
      company_name = var.company_name
    })
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  user_data     = data.cloudinit_config.web_startup.rendered
}

Module 2 — Remote Data Sharing with State

Why Split Configurations?

When infrastructure grows, a single terralith (monolithic configuration) becomes unmanageable. Split by:

  • Teams / roles (networking team, app team, DB team)
  • Blast radius — limit the impact of an erroneous terraform apply
  • Deployment cycles — deploy the network and app independently
graph TD
    subgraph Terralith["Terralith (monolithic)"]
        M[main.tf\nVPC + EC2 + DB + IAM + LB]
    end

    subgraph Split["Separate Configurations"]
        NET[network_config\nVPC, Subnets]
        APP[app_config\nEC2, Security Group]
        DB[db_config\nRDS, Parameters]
        IAM[iam_config\nRoles, Policies]
    end

    NET -->|outputs: vpc_id, subnet_ids| APP
    NET -->|outputs: vpc_id| DB
    IAM -->|outputs: role_arns| APP
    DB -->|outputs: db_endpoint| APP

terraform_remote_state

The terraform_remote_state data source allows a configuration to query the state of another configuration, accessing only its root module outputs.

# In app_config/main.tf
data "terraform_remote_state" "networking" {
  backend = "s3"

  config = {
    bucket = var.network_bucket_config.bucket
    key    = var.network_bucket_config.key
    region = var.network_bucket_config.region
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = data.terraform_remote_state.networking.outputs.public_subnet_ids[0]

  vpc_security_group_ids = [aws_security_group.web.id]
}

resource "aws_security_group" "web" {
  name   = "app-web-sg"
  vpc_id = data.terraform_remote_state.networking.outputs.vpc_id
}

Required outputs in the source configuration:

# In network_config/outputs.tf
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "IDs of the public subnets"
  value       = aws_subnet.public[*].id
}

S3 backend configuration:

# In network_config/terraform.tf
terraform {
  backend "s3" {
    bucket = "mycompany-terraform-state"
    key    = "network.tfstate"
    region = "us-east-1"
  }
}

Key Considerations

AspectDetail
CredentialsEach configuration can have distinct credentials
ConnectivityThe calling config must be able to reach the source S3 bucket
Network restrictionsPrivateLink, IP allowlists → verify accessibility
Sensitive dataEntire state is accessible, not just outputs

Module 3 — External Data Sources for Cross-configuration Sharing

Alternatives to terraform_remote_state

HashiCorp officially discourages using terraform_remote_state. Two major problems:

1. Security issue

  • The calling configuration needs full read access to the source state
  • terraform_remote_state ignores the sensitive marker on outputs

2. Tight coupling

  • The calling configuration is directly tied to the internal structure of the source configuration

tfe_outputs Data Source

Secure alternative if the source configuration uses HCP Terraform or Terraform Enterprise as backend.

data "tfe_outputs" "networking" {
  organization = "mycompany"
  workspace    = "network-config"
}

resource "aws_instance" "web" {
  subnet_id = data.tfe_outputs.networking.values.public_subnet_ids[0]
}

# Non-sensitive outputs
resource "aws_db_instance" "main" {
  db_subnet_group_name = data.tfe_outputs.networking.nonsensitive_values.db_subnet_group
}
AttributeContentSensitivity
valuesAll outputsMarked sensitive automatically
nonsensitive_valuesNon-sensitive outputs onlyUsable without unwrapping

Third-Party Storage Services

graph LR
    subgraph Source["Source Configuration"]
        NET[network_config]
    end

    subgraph Store["Storage Service"]
        SSM[AWS SSM\nParameter Store]
        CONSUL[HashiCorp Consul]
        VAULT[HashiCorp Vault\nSecrets]
    end

    subgraph Consumer["Consumer Configuration"]
        APP[app_config]
    end

    NET -->|writes vpc_id, subnet_ids| SSM
    NET -->|writes config data| CONSUL
    SSM -->|data source aws_ssm_parameter| APP
    CONSUL -->|data source consul_keys| APP
CategoryAWSAzureGCPAgnostic
General K-VSSM Parameter StoreApp ConfigCloud DatastoreRedis, Consul
SecretsSecrets ManagerKey VaultSecret ManagerHashiCorp Vault
K8sEKS ConfigMapsAKS ConfigMapsGKE ConfigMapsKubernetes ConfigMaps

Special Data Sources

Data SourceProviderUsage
local_filehashicorp/localRead local files
local_sensitive_filehashicorp/localFiles containing secrets
httphashicorp/httpGeneric HTTP requests to an API
externalhashicorp/externalExecute an arbitrary script
data "external" "network_info" {
  program = ["python3", "${path.module}/scripts/get_network_info.py"]

  query = {
    environment = var.environment
    region      = var.aws_region
  }
}

output "retrieved_vpc_id" {
  value = data.external.network_info.result["vpc_id"]
}

Migrating Network Data to SSM

Source configuration (network_config) — writing to SSM

resource "aws_ssm_parameter" "vpc_id" {
  name           = "/${var.environment}/networking/vpc-id"
  description    = "VPC ID for the application"
  type           = "String"
  insecure_value = aws_vpc.main.id

  tags = {
    Environment = var.environment
  }
}

resource "aws_ssm_parameter" "public_subnet_ids" {
  name        = "/${var.environment}/networking/public-subnet-ids"
  description = "Public subnet IDs"
  type        = "StringList"
  value       = join(",", aws_subnet.public[*].id)
}

Consumer configuration (app_config) — reading from SSM

data "aws_ssm_parameter" "vpc_id" {
  name = "/${var.environment}/networking/vpc-id"
}

data "aws_ssm_parameter" "public_subnet_ids" {
  name = "/${var.environment}/networking/public-subnet-ids"
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = split(",", data.aws_ssm_parameter.public_subnet_ids.value)[0]

  vpc_security_group_ids = [aws_security_group.web.id]
}

resource "aws_security_group" "web" {
  name   = "app-web-sg"
  vpc_id = data.aws_ssm_parameter.vpc_id.value
}

Cross-configuration Dependencies

Scenario 1 — Blocked deletion (best case)

AWS refuses to delete a subnet with attached network interfaces

Scenario 2 — Disconnected resources (dangerous)

Deleting a resource used by another configuration → service disruption

Scenario 3 — Silent renaming (insidious)

Renaming an S3 bucket in one config → breaks logging/caching in another config


Architecture Diagrams

Multi-environment Structure

graph TD
    subgraph Environments
        DEV[dev/]
        STAGING[staging/]
        PROD[prod/]
    end

    subgraph SharedModules["Shared Modules"]
        NET_MOD[module: networking]
        APP_MOD[module: app]
        DB_MOD[module: database]
    end

    DEV -->|dev.tfvars| NET_MOD
    DEV -->|dev.tfvars| APP_MOD
    STAGING -->|staging.tfvars| NET_MOD
    PROD -->|prod.tfvars| NET_MOD
    PROD -->|prod.tfvars| DB_MOD

    subgraph State["Remote State Backends"]
        S3_DEV[(S3: dev.tfstate)]
        S3_STG[(S3: staging.tfstate)]
        S3_PRD[(S3: prod.tfstate)]
    end

    DEV --- S3_DEV
    STAGING --- S3_STG
    PROD --- S3_PRD

Data Sharing Alternatives

flowchart LR
    subgraph S1["Source Configuration"]
        C1[network_config]
    end

    subgraph Methods["Sharing Methods"]
        direction TB
        RM[terraform_remote_state\nFull state access]
        TFE[tfe_outputs\nHCP Terraform/TFE only]
        SSM[AWS SSM\nDecoupled]
        VAULT[HashiCorp Vault\nSecure + Decoupled]
        HTTP[HTTP API\nLast resort]
    end

    subgraph S2["Target Configuration"]
        C2[app_config]
    end

    C1 --> RM
    C1 --> TFE
    C1 --> SSM
    C1 --> VAULT

    RM --> C2
    TFE --> C2
    SSM --> C2
    VAULT --> C2

Reference Tables

Data Sharing Strategies Between Configurations

StrategyCouplingSecurityPrerequisitesRecommendation
terraform_remote_stateTightFull state accessCompatible backendAvoid (discouraged by HashiCorp)
tfe_outputsTightOutputs onlyHCP Terraform / TFEGood if already on HCP/TFE
AWS SSM Parameter StoreLooseGranular IAMAWSRecommended on AWS
Azure App Config / Key VaultLooseGranular RBACAzureRecommended on Azure
HashiCorp ConsulLooseGranular ACLsConsul clusterCloud-agnostic
HashiCorp VaultLooseSecured secretsVault clusterFor sensitive data
external data sourceVariableVariableLocal scriptLast resort

Terralith vs. Separate Configurations

AspectTerralithSeparate Configurations
ComplexityGrowing → unmanageableDivided by domain
Blast radiusHigh (all or nothing)Limited to the affected config
DependenciesManaged automaticallyManual / via data sources
Execution timeLong (full graph)Short per config
CollaborationFrequent conflictsParallel work possible

HCL Code Snippets

tfvars by Environment

# dev.tfvars
environment        = "dev"
instance_type      = "t3.micro"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
vpc_cidr           = "10.0.0.0/16"

# staging.tfvars
environment        = "staging"
instance_type      = "t3.small"
public_subnet_cidrs = ["10.1.1.0/24", "10.1.2.0/24"]
vpc_cidr           = "10.1.0.0/16"

# prod.tfvars
environment        = "prod"
instance_type      = "t3.medium"
public_subnet_cidrs = ["10.2.1.0/24", "10.2.2.0/24"]
vpc_cidr           = "10.2.0.0/16"
project/
├── modules/
│   ├── networking/
│   ├── compute/
│   └── database/
├── environments/
│   ├── dev/
│   │   ├── network/
│   │   │   ├── main.tf
│   │   │   ├── variables.tf
│   │   │   ├── outputs.tf
│   │   │   └── terraform.tfvars
│   │   └── app/
│   │       ├── main.tf
│   │       ├── variables.tf
│   │       └── terraform.tfvars
│   ├── staging/
│   └── prod/
└── README.md

Best Practices

  1. Never hardcode resource IDs between configurations — use data sources
  2. Document every cross-configuration dependency in the README
  3. Define stable outputs in source configurations — avoid breaking renames
  4. Use the sensitive marker on outputs containing secrets
  5. Enable state locking (DynamoDB for S3 backend) to avoid conflicts
  6. Prefer SSM/Consul over terraform_remote_state to reduce coupling
  7. Test changes on non-critical environments before production
  8. Establish change controls for shared resources

Summary

Module 1 — Data Sources

Data sources allow dynamically querying cloud platforms for information like AMI IDs, Availability Zones, or policy documents. Generative data sources (cloudinit_config, aws_iam_policy_document) generate correctly formatted documents without external queries.

Module 2 — Remote State

The terraform_remote_state data source enables sharing outputs between configurations via their state stored in a common backend (S3). Warning: full read access to state, not just outputs.

Module 3 — External Data Sources

To overcome the security and coupling limitations of terraform_remote_state, HashiCorp recommends decoupled alternatives: tfe_outputs (if using HCP Terraform/TFE), or third-party storage services like AWS SSM Parameter Store, HashiCorp Vault, or Consul.

When splitting a terralith into separate configurations, Terraform loses the global view of dependencies. It is the team’s responsibility to document, monitor, and manage these cross-configuration dependencies to prevent unexpected service disruptions.


Search Terms

terraform · managing · configurations · infrastructure · ci/cd · devops · data · sources · source · sharing · ssm · alternatives · configuration · cross-configuration · dynamic · external · remote · state · terraformremotestate

Interested in this course?

Contact us to book it or get a custom training plan for your team.