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

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 = {
#   "[email protected]" = {
#     "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
}