Terraform Provider Mismanagement
Overview
Terraform changed how provider requirements of a module are handle in Terraform version 0.11.
In versions <= 0.10
, the providers within modules were handled implicitly. In
other words, there was no explicit way to use a different cofiguration of the
same provider, in different modules. The workaround, though not ideal in
practice, was to define providers indside the module itself. (Yours truly still
has flashbacks of being confused as to how this works!)
Fast forward to 0.13
, provider definitions are still allowed in modules, for
backwards compatibility. However, it will take away your ability to use
for_each
, count
and depends_on
with modules.
Copy Pasta
Fast forward further to the age of.. checks notes Terraform version 1.9.x
,
and I still see these ancient practices copy pasted through generations of
code. Copying over working code and chaging until a semblance of 'working'
appears is a much more wide spread practice than acknowledged.
Warnings
Terraform however adds warnings about provider "mishandling". For better or worse, no one really cares about warnings! "But wait..", you exclaim. "How could it be better?". Consider the following scenario.
Like any fine day, I create a module (copy paste code of course) and think how I'm gonna use it. I was just kidding, taking time to think is for losers, I just copy paste some code for module call/inilization.
Directory structure
.
├── main.tf
└── modules
└── things-doer
└── main.tf
Main file at the sub-module. In the module, I feel like North Verginia.
provider "aws" {
region = "us-east-1"
}
variable "input" {}
data "aws_region" "current" {}
output "user_input" {
value = var.input
}
output "aws_region" {
value = data.aws_region.current.name
}
Main file at the root module. Whoever wrote the code I copy pasted, had different plans.
provider "aws" {
region = "us-east-1"
}
provider "aws" {
region = "ap-southeast-1"
alias = "singapore"
}
module "things_doer" {
source = "./modules/things-doer"
input = "hello"
providers = {
aws.singapore = aws
}
}
output "module_output" {
value = module.things_doer.aws_region
}
Now you terraform init
, just to be greeted with the following warning.
│ Warning: Reference to undefined provider
│
│ on main.tf line 10, in module "things_doer":
│ 10: aws.singapore = aws
│
│ There is no explicit declaration for local provider name "aws.singapore" in module.things_doer, so Terraform is assuming you mean to pass a configuration for "hashicorp/aws".
│
│ If you also control the child module, add a required_providers entry named "aws.singapore" with the source address "hashicorp/aws".
╵
As someone who copy pastes things but still tries to get rid of the
warnings, I might just go and do what Terraform tells me to do and add
required_providers
config that's totally irrelevant to what I might have been
doing. However, it's easier to just ignore the warning and let the unfortunate
who get to work on this code base in the future be confused/annoyed.
This scenario is not that difficult to digest because it's just a few lines of code and the provider naming is still sane. Now imagine a case where the code base is quite big and provider naming is out of whack! Things can get pretty hard to reason about pretty quickly.
Take home
- Be aware of the Terraform version you are using and try to keep up with the newer versions.
- Do not define/initialize providers in modules (sub-module) if you are using
Terraform
>= 0.11
or unless you really know what you are doing. >= 0.11
: If your module needs providers of different configuration, use required_providers directive to define it in the module (and pass down the necessary providers from root module).- If you really have to worry a lot about providers of different configuration, double check your design.
- Take time to read the documentaion.
To wrap up, if I re-did my bad example above correctly, it would look something like the following.
Main of the root module
provider "aws" {
region = "us-east-1"
}
module "things_doer" {
source = "./modules/things-doer"
input = "hello"
}
output "module_output" {
value = module.things_doer.aws_region
}
Main of the sub-module
variable "input" {}
data "aws_region" "current" {}
output "user_input" {
value = var.input
}
output "aws_region" {
value = data.aws_region.current.name
}