Learnings

Providers and shared modules

  • A module intended to be called by one or more other modules must not contain any provider blocks.
  • Provider configurations can be defined only in a root Terraform module.
  • If a module contains its own provider configurations, it is considered “legacy”, and is prevented from being used with the count, for_each and depends_on arguments.

Development tricks

Set up mitmproxy to debug requests

If you’re working with a provider that doesn’t provide an easy way to see the requests it’s making, then install a proxy (like mitmproxy) and route all your requests through it:

mitmproxy

sudo cp ~/.mitmproxy/mitmproxy-ca-cert.cer /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

export HTTP_PROXY=localhost:8080 && export HTTPS_PROXY=localhost:8080

terraform apply ...

A note about error timings

  • If some errors happen in a Terraform job, they will be logged at the end of the process.
  • But the actual time of the error can be much earlier than the time that the error is logged.
  • This can be confusing if you’re troubleshooting a problem, and you need to check some corresponding system logs (e.g. the proxy logs, as configured above) for the specific time/date of the error.
  • Solution: enable debug logging TF_LOG=DEBUG, which should show the original timestamp of the error, so you can correlate this with whatever other system logs you’re using to investigate.

Deploying only parts of a configuration

To deploy only certain parts of a Terraform configuration, use the -target flag, e.g.:

terraform apply -target module.my_module

# With a resource defined as: `resource "local_file" "foo" { }`
terraform apply -target=local_file.foo

Debugging a provider with CLI overrides

If you want to debug a provider, you can download its source code and then tell Terraform to use your local copy by creating an overrides file:

For example:

git clone https://github.com/grafana/terraform-provider-grafana
cd terraform-provider-grafana
git checkout v1.35.0
go build

cat > ~/.terraformrc <<EOF
provider_installation {
  dev_overrides {
    "grafana/grafana" = "/home/tdonohue/repos/terraform-provider-grafana"
  }
}
EOF

cd path/to/your/terraform/config
terraform apply

Debugging a provider with Delve

Activate the provider’s debug mode (check the provider’s source code for the correct environment variable to set) and start a debugging session:

cd ~/repos/terraform-provider-grafana
$HOME/go/bin/dlv debug . -- --debug

(dlv) break theMethodYouWantToBreakOn
(dlv) continue

# This will print a line like TF_REATTACH_PROVIDERS...

And then, in another window, paste the env var declaration and run Terraform as normal:

TF_REATTACH_PROVIDERS=...
terraform apply

Debugging a provider with JetBrains GoLand

git clone https://github.com/grafana/terraform-provider-grafana
cd terraform-provider-grafana
go build -o terraform-provider-grafana .

Create the file ~/.tofurc:

provider_installation {
  dev_overrides {
    "grafana/grafana" = "/path/to/your/terraform-provider-grafana"
  }

  direct {}
}

Start the provider in debug mode:

go run . -debug

Attach GoLand to the process:

  1. Run -> Attach to Process
  2. Find the debug provider by its process ID (PID)
  3. GoLand attaches to the provider and it is paused, waiting for the first Terraform connect
  4. Set breakpoints now

Cookbook

Collections

Convert a map to a multi-line string with line delimiters

Convert a map of usernames and passwords into a multiline string in the format username:password..

First you’ll need to define the variable in a variables.tf file (You can’t define variables within Terraform console):

terminal_users = [
  {
    username = "dave"
    password = "dav381919"
  },
  {
    username = "lucy"
    password = "lucy123211"
  },
]

Then:

join("\r\n", [for user in var.terminal_users : "${user.username}:${user.password}"])

Convert a map of usernames into a space-separated list

join(" ", [for user in var.terminal_users : user.username])

Convert a map of usernames into a for_each input

{for user in var.terminal_users : user.username => user}

Looping

A poor round-robin

  • Use the element function to get the element at a given index in a list
  • Use the index function to get the index of a given key in a map
  • Use the values function to get a list of values from a map

This allows us to loop around input structures and do a “round-robin” of the values. For example:

  • assign each server to a network, from a known list of networks
  • assign each user to a different server, from a known list of servers
module "some_module" {
  for_each = { for idx, user in keys(var.servers) : server => var.servers[server] }

  # Now let's pick a network for this server by doing a round-robin of the networks,
  # using the index of the server in the map (0, 1, 2, 3, 4, 5, etc...)
  # Assuming that module.vpc.networks returns something like:
  # [
  #   {
  #     name = "network-01"
  #   },
  #   {
  #     name = "network-02"
  #   },
  # ]
  # And the var.servers map looks like:
  # {
  #   "server-01" = {
  #     ...
  #   },
  #   "server-02" = {
  #     ...
  #   },
  # }

  # We use 'each.key' to get the current key in the map
  # We then use this to get the index of the current key in the map
  # And then use that index to get the element at that index, in another list
  # The 'element' function returns the element at the given index in a list
  # (and wraps around to the beginning of the list if the index is greater than
  # the length of the list) - e.g. 0, 1, 2, 3, 0, 1, 2, 3...
  # e.g. "network-01", "network-02", etc.
  server_network_name = element(
    values(module.vpc.networks),
    index(keys(var.servers), each.key) 
  ).name
}

Outputs

Output a map of maps

# e.g.:
# app_url = {
#   "mydave" = "https://trcgotestjoe.example.com"
#   "myjoe"  = "https://trcgotestsusan.example.com"
# }
output "app_url" {
  value = {
    for key, stack in module.my_custom_module : key => stack.stacks.url
  }
}

Output a map of maps

# e.g.:
# email_fields = {
#   "joe@example.com" = {
#     "url"         = "https://trcgotestjoe.example.com"
#     "username"    = "joe"
#     "password"    = "hiyaaa"
#     "webterminal" = "https://example.com"
#   }
output "email_fields" {
  value = {
    for user in var.my_users : user.email => {
      "url"         = module.my_custom_module[user.id].stacks.url
      "username"    = user.username
      "password"    = user.password
      "webterminal" = "https://example.com"
    }
  }
}

Output an entire submodule’s output

output "stacks" {
  value     = module.my_custom_module
  sensitive = true
}