Variable default value interpolation from locals

I have asked this question on SO, but it hasn’t received (m)any answers, so I’m hoping TF gurus here might be able to help. If it’s not allowed to cross-post the questions I’m happy to delete this question.

I have a use case where I need two AWS providers for different resources. The default aws provider is configured in the main module which uses another module that defines the additional aws provider which might use different AWS credentials.

By default, I’d like both providers to use the same AWS credentials unless explicitly overridden.

I figured I could do something like this. In the main module:

locals {
  foo_cloud_access_key = aws.access_key
  foo_cloud_secret_key = aws.secret_key
}

variable "foo_cloud_access_key" {
  type        = string
  default     = local.foo_cloud_access_key
}

variable "foo_cloud_secret_key" {
  type        = string
  default     = local.foo_cloud_secret_key
}

where variable s foo_cloud_secret_key and foo_cloud_access_key would be passed down to the child module like this:

module foobar {
...
  foobar_access_key = var.foo_cloud_access_key
  foobar_secret_key = var.foo_cloud_secret_key
...
}

Where module foobar would then configure its additional was provide with these variables:

provider "aws" {
  alias      = "foobar_aws"
  access_key = var.foobar_access_key
  secret_key = var.foobar_secret_key
}

Note, that I need to “extract” the default aws provider config variables and use them elsewhere i.e. the default aws provider will be configured with access_key and secret_key and I need to use those (values) as default values in other variables.

When I run the init terraform spits out this error (for both variables):

Error: Variables not allowed
  on variables.tf line 66, in variable "foo_cloud_access_key":
  66:   default     = local.foo_cloud_access_key

Variables may not be used here.

Is it possible to achieve something like this in terraform or is there any other way to go about this?

As the error states you can’t use another variable within a variable definition block. However you don’t need to. Set the default to null or empty string and then have a local variable which can be either the value of the variable or the value of the local. Something like:

locals {
  foo_cloud_access_key_value = var.foo_cloud_access_key != "" ? var.foo_cloud_access_key : local.foo_cloud_access_key
}

And then use local.foo_cloud_access_key_value in your module instead of the var.foo_cloud_access_key

A non-root module defining a provider is a deprecated pattern because it can cause you to get into a blocked state where you can’t remove instances of the module, because you’d be removing both the resources inside and the provider configurations needed to delete them at the same time.

The usual way to use a different provider configuration for a particular module is to define an additional (aliased) provider configuration in the root module and then pass it down to the child module so that the child will see it as its default configuration:

provider "aws" {
  # the default configuration
}

provider "aws" {
  alias = "foo"

  # alternative configuration for the "foo" module
}

module "foo" {
  providers = {
    aws = aws.foo
  }
  # ...
}

The above means that the root configuration will have two configurations for the AWS provider, but the child module foo will only have a single “default” configuration which refers to the aws.foo configuration from the root.

Building on that to meet your requirement of allowing the module’s credentials to potentially be overridden, you could use some optional variables that default to null as arguments to that second provider configuration:

variable "foobar_access_key_id" {
  type   = string
  default = null
}

variable "foobar_secret_access_key" {
  type   = string
  default = null
}

provider "aws" {
  alias = "foo"

  access_key = var.foobar_access_key_id
  secret_key = var.foobar_secret_access_key
}

Setting a provider configuration argument to null is always the same as leaving it unset, so the default case with the above will not override those arguments at all and will thus leave the provider to discover suitable credentials in the usual ways (credentials files, EC2 metadata, etc).

I would typically recommend against passing credentials directly through variables because that causes them to be fixed statically in the generated plan and thus makes it tricky to use a configuration like this in a situation with dynamically-issued credentials (e.g. AssumeRole or EC2 instance profiles), so I would suggest using some combination of the profile and shared_credentials_file options instead to allow the caller to indirectly specify credentials to use, but the pattern would be the same either way, just with different variable names and different arguments inside that provider "aws" block.