Skip to content

Instantly share code, notes, and snippets.

@straubt1
Last active March 3, 2026 22:35
Show Gist options
  • Select an option

  • Save straubt1/3592aa6a30425fa58da9cc64672b7b04 to your computer and use it in GitHub Desktop.

Select an option

Save straubt1/3592aa6a30425fa58da9cc64672b7b04 to your computer and use it in GitHub Desktop.
Feature Request: Environment Variable Override for Terraform Provider Source Hostname

Feature Request: Environment Variable Overrides for Terraform Provider Source Hostname and Namespace

Summary

Source: https://github.com/straubt1/terraform-registry-address/tree/provider-source-default-environment-variable

This proposal introduces support for two environment variables, TF_PROVIDER_SOURCE_HOSTNAME and TF_PROVIDER_SOURCE_NAMESPACE, that override the hostname and namespace components of provider source addresses at runtime, without requiring any changes to module source code. Together, these are targeted, low-risk enhancements that eliminate repetitive boilerplate from large-scale enterprise Terraform deployments and improve operational flexibility.


Background: Provider Source Address Anatomy

A Terraform provider source address is composed of three parts:

<hostname>/<namespace>/<name>
Component Default Required
hostname registry.terraform.io No
namespace hashicorp No
name (none) Yes

The current defaults are surfaced clearly in the three canonical forms a source address can take:

# Implicit hostname + implicit namespace
terraform {
  required_providers {
    random = {
      source = "random"
      # hostname  -> registry.terraform.io
      # namespace -> hashicorp
      # name      = "random"
    }
  }
}
# Implicit hostname + explicit namespace
terraform {
  required_providers {
    random = {
      source = "my-org/random"
      # hostname  -> registry.terraform.io
      # namespace -> my-org
      # name      = "random"
    }
  }
}
# Explicit hostname + explicit namespace
terraform {
  required_providers {
    random = {
      source = "tfe.company.com/my-org/random"
      # hostname  -> tfe.company.com
      # namespace -> my-org
      # name      = "random"
    }
  }
}

The hostname and namespace components each already have a well-established default. This proposal extends that model with two environment variables that allow organizations to replace those defaults for their environment. Both follow the same rule: the override applies only when the respective component is implicit in the source address. If a source address includes an explicit hostname, the hostname variable is ignored for that address. If it includes an explicit namespace, the namespace variable is ignored for that address. Explicit values in source addresses are always respected, preserving full backward compatibility and allowing gradual, targeted opt-out on a per-module or per-provider basis.


The Enterprise Reality: Why Private Registries?

Before addressing the technical problem, it is worth establishing why large organizations operate private provider registries in the first place. HCP Terraform is one such option for hosting a private registry; others include self-managed Terraform Enterprise instances and third-party artifact repositories.

Scale Demands a Private Registry

Enterprises with hundreds of Terraform modules and a sprawling cloud footprint, spanning multiple cloud providers, accounts, regions, and environments, cannot afford the operational and compliance risk of allowing Terraform to pull providers directly from the public internet during plan and apply operations.

Private registries solve this by acting as a controlled, internally governed distribution point for provider binaries. The motivations are concrete:

  • Security scanning. Provider binaries are scanned for known vulnerabilities (CVEs) before being made available internally. Only vetted versions are promoted.
  • Air-gapped and restricted environments. Many regulated environments (financial services, government, healthcare, defense) have outbound internet restrictions. A private registry is the only viable path.
  • Provider signing. Providers can be re-signed with internal keys before being published to the private registry, enforcing organizational trust chains.
  • Availability guarantees. Public registry outages have caused real production incidents. An internal registry with SLAs removes that external dependency.

This is not a niche edge case. This is standard practice at the scale these organizations operate.


The Core Problem: Hostname and Namespace as Boilerplate

An organization running a private registry at terraform-registry.company.com under the namespace company must encode both of those values into every required_providers block in every module. A typical module looks like this:

terraform {
  required_providers {
    aws = {
      source = "terraform-registry.company.com/company/aws"
    }
    random = {
      source = "terraform-registry.company.com/company/random"
    }
    tls = {
      source = "terraform-registry.company.com/company/tls"
    }
    null = {
      source = "terraform-registry.company.com/company/null"
    }
    kubernetes = {
      source = "terraform-registry.company.com/company/kubernetes"
    }
  }
}

This block, or one nearly identical to it, lives in every single module across the organization's module library. Looking at each source string, terraform-registry.company.com/company/aws, only one token out of three is actually meaningful at the module level: the provider name. The hostname is a deployment detail. The namespace is an organizational convention. Neither varies from module to module, and neither communicates anything about what the provider does or how the module uses it.

Both are pure boilerplate.

At scale, this means the same hostname and namespace are embedded across potentially hundreds of repositories. Every developer reading a module must visually parse past two repeated tokens to reach the one token that matters. Every code review, every diff, every audit includes this noise.

The hostname and namespace are infrastructure configuration masquerading as application code. Neither belongs hardcoded into module source.


What This Looks Like With the Environment Variables

Step One: Remove the Hostname

With TF_PROVIDER_SOURCE_HOSTNAME=terraform-registry.company.com set in the execution environment, the module above simplifies to:

terraform {
  required_providers {
    aws = {
      source = "company/aws"
    }
    random = {
      source = "company/random"
    }
    tls = {
      source = "company/tls"
    }
    null = {
      source = "company/null"
    }
    kubernetes = {
      source = "company/kubernetes"
    }
  }
}

The hostname is gone. Source addresses now show namespace and name. Already a meaningful reduction, but the namespace is still repeated across every entry.

Step Two: Remove the Namespace

With TF_PROVIDER_SOURCE_NAMESPACE=company also set, the module simplifies further to:

terraform {
  required_providers {
    aws = {
      source = "aws"
    }
    random = {
      source = "random"
    }
    tls = {
      source = "tls"
    }
    null = {
      source = "null"
    }
    kubernetes = {
      source = "kubernetes"
    }
  }
}

The required_providers block now contains only what was always the meaningful part: the provider names. The hostname and namespace live where infrastructure configuration belongs, in the execution environment, set once and applied everywhere.

This is not a new source address form. The source = "aws" shorthand already exists and is valid today, resolving to registry.terraform.io/hashicorp/aws. These environment variables extend that same ergonomic pattern to work for any registry and any namespace, not just HashiCorp's defaults.

Behavior Reference

Source address in code TF_PROVIDER_SOURCE_HOSTNAME TF_PROVIDER_SOURCE_NAMESPACE Resolved address
aws terraform-registry.company.com company terraform-registry.company.com/company/aws
company/aws terraform-registry.company.com other terraform-registry.company.com/company/aws (namespace explicit, not overridden)
terraform-registry.company.com/company/aws new-registry.company.com other terraform-registry.company.com/company/aws (both explicit, neither overridden)
aws (not set) (not set) registry.terraform.io/hashicorp/aws (unchanged defaults)

Secondary Benefit: Operational Flexibility When Registries Change

While the primary case for this feature is eliminating boilerplate, the environment variable approach also provides meaningful flexibility when registry hostnames or namespaces need to change. This is a secondary but valid benefit.

Private registries are not always permanent fixtures. Registries can change for legitimate and sometimes unavoidable reasons: migrating from a self-hosted instance to HCP Terraform, a domain change from corporate rebranding, or consolidating infrastructure after a merger.

In the current model, a hostname or namespace change requires updating source strings across every affected module. With these environment variables in place, both values already live outside of module code. A registry migration becomes a change to one or two environment variables in CI/CD rather than a coordinated, multi-repository code change effort.

This is not the driving argument for this feature, but it is a meaningful compounding benefit of keeping infrastructure configuration in the environment where it belongs.


Lock File Consistency

A critical requirement for any change that affects provider resolution is that the .terraform.lock.hcl file remains consistent and trustworthy. The lock file records the resolved provider source addresses, checksums, and version constraints that were used during terraform init.

Both environment variables handle the lock file correctly, and the behavior is consistent between them:

  • When either variable is set, the fully resolved source address, including any overridden components, is written to the lock file. The lock file always reflects the actual registry and namespace that were used.
  • On subsequent terraform init runs with the same environment variable values, the resolved address matches the lock file entry. No conflict occurs.
  • If either variable's value changes, terraform init detects the mismatch between the previously locked source and the newly resolved source and surfaces a clear error. The operator must acknowledge the change by running terraform init -upgrade or reinitializing.
  • If either variable is unset in an environment where it was previously set, Terraform falls back to the original default (registry.terraform.io for hostname, hashicorp for namespace). The resulting address mismatch with the lock file surfaces immediately as an error, preventing silent misconfiguration.

The lock file acts as a correctness check for both environment variables. Accidental or inconsistent configuration is caught at init time, not silently at runtime. This is consistent with how Terraform already uses the lock file to enforce provider identity across environments.


Adoption Path and Backward Compatibility

These changes are strictly additive and backward compatible:

  • No existing configuration changes. If neither environment variable is set, behavior is identical to today.
  • Explicit source address components are unaffected. A module with a full three-part source address continues to work exactly as before. A module with an explicit namespace but no hostname respects the explicit namespace and still benefits from the hostname variable.
  • Incremental cleanup is supported. Organizations can set one or both variables and progressively remove hardcoded components from modules at their own pace. All source address forms work simultaneously during any transition.
  • Each variable is independent. Setting only TF_PROVIDER_SOURCE_HOSTNAME without TF_PROVIDER_SOURCE_NAMESPACE is fully valid and useful on its own, and vice versa.

Alternatives Considered

.terraformrc provider_installation blocks

The provider_installation stanza in the CLI configuration can redirect provider lookups using network_mirror or filesystem_mirror. However:

  • It operates at the binary/artifact level, not the source address level. The source address in the lock file still reflects the original address.
  • It requires changes to the CLI configuration file, which is per-machine and harder to control in ephemeral CI/CD environments than environment variables.
  • It does not remove the hostname or namespace from module source code. The boilerplate problem remains entirely unaddressed.

Global find-and-replace tooling

Scripted mass replacement is fragile across version-pinned, tagged, release-managed module repositories. It does not address the ongoing boilerplate problem for new modules, and it moves no configuration out of source code.

Requiring modules to use the full three-part source address always

This is the status quo. It is what this proposal is designed to improve.


Conclusion

TF_PROVIDER_SOURCE_HOSTNAME and TF_PROVIDER_SOURCE_NAMESPACE are focused, low-risk additions that directly address a well-documented source of boilerplate in large-scale enterprise Terraform deployments. They align with Terraform's existing philosophy of allowing environment-specific configuration to live in the environment rather than in source code, and they extend the defaulting model that already exists for both components in a natural and consistent way.

The behavior is predictable, the lock file implications are safe and correct, and the changes are fully backward compatible. For organizations managing hundreds of modules against private provider registries, these variables remove the two tokens in every source address that have never belonged in module code, and replace them with a pair of centrally managed environment variables.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment