Fixing Terraform 'Provider Configuration Not Known Until Apply' with data.aws_ami
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins
TL;DR
- What broke: Terraform cannot resolve the
providerargument ondata.aws_amiat plan time because it references a dynamic or computed value — the plan is dead before it starts. - How to fix it: Use only statically-defined provider aliases in
providerarguments on data sources; never pass a computed resource attribute or module output as a provider reference. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your failing
.tfblock and get corrected HCL without sending your ARNs or account IDs to any server.
The Incident (What Does the Error Mean?)
You hit this during terraform plan or terraform apply:
╷
│ Error: Provider configuration not known until apply
│
│ on main.tf line 12, in data "aws_ami" "base":
│ 12: provider = aws.${var.target_region}
│
│ The provider configuration for this data source depends on a value that
│ cannot be determined until apply. Data sources that depend on unknown
│ provider configurations cannot be read during the plan.
╵
Immediate consequence: Terraform's two-phase execution model (plan → apply) requires that every provider reference be fully resolved during the graph construction phase — before a single API call is made. If the provider meta-argument on data.aws_ami points to anything computed (a for_each key derived from a resource, a dynamic local, a module output that isn't yet known), the entire plan aborts. You get zero diff output. No resources are evaluated. CI/CD pipeline dies here.
The Attack Vector / Blast Radius
This is not just an inconvenience — it is a pipeline blocker with cascading consequences:
Multi-region AMI lookups break entirely. The most common trigger is attempting to fan out
data.aws_amiacross regions using a dynamic provider map. The moment that map key is computed rather than literal, every downstreamaws_instance,aws_launch_template, andaws_autoscaling_groupdepending on that AMI ID is also unresolvable.Module consumers inherit the blast radius. If this pattern lives inside a reusable module and the calling root module passes a computed
providersmap, every environment (dev, staging, prod) that calls that module is broken simultaneously.State drift risk. Engineers under pressure sometimes run
terraform apply -targetto work around this. Partial applies against autoscaling infrastructure leave state files inconsistent with reality — a ticking clock for the next deploy.Security implication of the workaround: Hardcoding AMI IDs to escape the error (instead of fixing the provider reference) means you stop tracking the latest hardened/patched AMI. That AMI ID rots. Six months later you're launching instances from an AMI with unpatched CVEs.
How to Fix It
Root Cause
Terraform's provider reference in a data block must be a literal reference to a statically declared provider alias — not a string interpolation, not a for_each key, not a computed local. The Terraform core graph walker resolves provider edges before it evaluates resource or data source configurations.
Basic Fix — Static Provider Aliases
Stop interpolating the provider alias. Declare explicit aliases and reference them literally.
# BROKEN: dynamic provider reference
- variable "target_region" {
- default = "us-west-2"
- }
-
- provider "aws" {
- alias = "dynamic"
- region = var.target_region
- }
-
- data "aws_ami" "base" {
- provider = aws.${var.target_region} # computed — ILLEGAL
- most_recent = true
- owners = ["amazon"]
- filter {
- name = "name"
- values = ["amzn2-ami-hvm-*-x86_64-gp2"]
- }
- }
# FIXED: static alias, resolved at graph construction time
+ provider "aws" {
+ alias = "us_west_2"
+ region = "us-west-2"
+ }
+
+ provider "aws" {
+ alias = "us_east_1"
+ region = "us-east-1"
+ }
+
+ data "aws_ami" "base_us_west_2" {
+ provider = aws.us_west_2 # static literal alias — valid
+ most_recent = true
+ owners = ["amazon"]
+ filter {
+ name = "name"
+ values = ["amzn2-ami-hvm-*-x86_64-gp2"]
+ }
+ }
+
+ data "aws_ami" "base_us_east_1" {
+ provider = aws.us_east_1
+ most_recent = true
+ owners = ["amazon"]
+ filter {
+ name = "name"
+ values = ["amzn2-ami-hvm-*-x86_64-gp2"]
+ }
+ }
Enterprise Best Practice — Module-Level Provider Passing
For multi-region infrastructure at scale, pass providers explicitly into modules using the providers map at the module call site — where aliases are always statically known.
# Root module — provider aliases are static here
+ provider "aws" {
+ alias = "primary"
+ region = "us-east-1"
+ }
+
+ provider "aws" {
+ alias = "dr"
+ region = "eu-west-1"
+ }
# Module call — providers map uses static alias references
- module "ami_lookup" {
- source = "./modules/ami"
- region_provider = "aws.${local.computed_region}" # ILLEGAL
- }
+ module "ami_lookup_primary" {
+ source = "./modules/ami"
+ providers = {
+ aws = aws.primary # static — Terraform resolves this at graph time
+ }
+ }
+
+ module "ami_lookup_dr" {
+ source = "./modules/ami"
+ providers = {
+ aws = aws.dr
+ }
+ }
# Inside ./modules/ami/main.tf
+ terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ configuration_aliases = [aws]
+ }
+ }
+ }
+
+ data "aws_ami" "base" {
+ most_recent = true
+ owners = ["amazon"]
+ filter {
+ name = "name"
+ values = ["amzn2-ami-hvm-*-x86_64-gp2"]
+ }
+ }
The module receives whichever regional provider the caller injects. The data.aws_ami inside the module inherits it without any dynamic reference. This is the only pattern that scales cleanly to N regions without hitting the plan-time resolution wall.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. terraform validate as a Pre-Commit Gate
terraform validate catches static provider reference errors before a plan is ever attempted. Wire it into pre-commit:
# .pre-commit-config.yaml
- repo: https://github.com/antonbabenko/pre-commit-terraform
hooks:
- id: terraform_validate
- id: terraform_tflint
2. TFLint — terraform_required_providers Rule
Enable the ruleset that flags dynamic provider meta-arguments:
# .tflint.hcl
plugin "terraform" {
enabled = true
preset = "recommended"
}
rule "terraform_required_providers" {
enabled = true
}
3. Checkov Policy — Flag Computed Provider References
Checkov's CKV_TF_1 and custom graph policies can be extended to detect provider = "${...}" interpolation patterns in data sources. Add a custom check:
# checkov custom check skeleton
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
class CheckStaticProviderAlias(BaseResourceCheck):
# Flag any data source where provider value contains interpolation markers
...
4. OPA / Conftest Policy
# policy/no_dynamic_provider.rego
package terraform
deny[msg] {
data_source := input.configuration.root_module.resources[_]
data_source.mode == "data"
# provider_config_key contains a computed reference pattern
contains(data_source.provider_config_key, "${")
msg := sprintf("Data source '%s' uses a dynamic provider reference. Use static aliases only.", [data_source.address])
}
Run in CI: conftest test plan.json --policy policy/
5. Sentinel (Terraform Cloud/Enterprise)
For TFC/TFE shops, enforce at the policy-as-code layer so no run with dynamic provider references on data sources ever reaches the apply queue.
Bottom line: Every provider meta-argument on every data block must be a compile-time constant. If you need dynamic regional dispatch, the answer is always explicit module instantiation with static providers maps — not string interpolation.