Ocasionally when developing cloud serverless functions there’s a need to integrate with other cloud services such as databases, queues or vaults.
While create an Azure Function that interacts with a CosmosDB database I found the need to constantly change the the connection string on the function settings.json to match the database created at the time, as I was creating and deleting databases multiple times during development.
The solution
The solution is to trigger a command line execution to set a CosmosDB connection string as an environment variable during the plans deployment. It should only be triggered when there’s a change to the connection string and if the plan if terraform is applying the plan in a development machine.
This way, when testing the Azure Function locally, it would always be able to access the CosmosDB database running on Azure cloud.
Provisioners
Terraform introduces the concept of provisioners as a way of allowing terraform plans to perform specific actions on a local or remote environment. Provisioner blocks can be:
local-exec- execute commands on the local machineremote-exec- execute commands on remote machine (can be a resource recently created)file- copies files or directories from a the local machine to a 21resource through ssh or winrm
For this particular scenario, local-exec provisioner is enough to execute a script or command-line to set the environment variables, like so:
provisioner "local-exec" {
command = <<EOT
[Environment]::SetEnvironmentVariable("PRIMARY_CONNECTION_STRING", ${azurerm_cosmosdb_account.db_account.connection_strings[0]}, "User")
[Environment]::SetEnvironmentVariable("SECONDARY_CONNECTION_STRING", ${azurerm_cosmosdb_account.db_account.connection_strings[1]}, "User")
EOT
interpreter = ["PowerShell", "-Command"]
}
Provisioners still need to be declared on the context of a resource preferably running indenpendently from other resources while being triggered whenever needed.
Terraform_data
terraform_data is treated as a normal resource but doesn’t do anything by itself, which fits perfectly for the scenario of triggering a provisioner.
It introduces a new lifecycle argument (apart from lifecycle block properties) called triggeres_replace, that triggers whatever action is defined on the terraform_data block. This argument should include all the properties that will trigger the terraform_data resource to be applied.
Joining terraform_data with local-exec:
resource "terraform_data" "set_db_vars" {
triggers_replace = [
azurerm_cosmosdb_account.db_account.connection_strings,
]
provisioner "local-exec" {
...
}
}
If there’s a change in the primary connection string of our CosmosDB database, its value will be saved on the machines environment variables.
Note
Terraform v1.4.0 introduces terraform_data as a replacement of null_resource. Although similar, terraform_data has the advantage of not requiring to install another provider (hashicorp/null).
Developer Machines
This only makes sense during development, hence, only on developer machines so it makes sense to only apply the resource when running in a specific context. We can pass a variable indicating that the plan is being applied from a developer machine like dev_profile = true.
Provisioners do have a when argument, however this only supports two possible values create and destroy which doesn’t make sense for what we intend to achieve because the resource is never created/destroyed.
Using a precondition to validate the if dev_profile = true could be an alternative if it didn’t halt the whole plan when the condition is not met.
The only option is to pass on the variable value to the script that is going to execute and let it evaluate if it should do something or not.
We can use directives to conditionally check the variable value and execute the commands or nop if otherwise, resulting in the following command:
...
command = <<EOT
%{if var.dev_profile}
[Environment]::SetEnvironmentVariable("PRIMARY_CONNECTION_STRING", ${azurerm_cosmosdb_account.db_account.connection_strings[0]}, "User")
[Environment]::SetEnvironmentVariable("SECONDARY_CONNECTION_STRING", ${azurerm_cosmosdb_account.db_account.connection_strings[1]}, "User")
%{endif}
EOT
...
To run the plan and set the environment variables on the local machine you can use terraform apply -var="dev_profile=true" or have the value set on a .tfvars file.
Full Configuration
resource "terraform_data" "set_db_vars" {
triggers_replace = [
azurerm_cosmosdb_account.db_account.connection_strings,
]
provisioner "local-exec" {
command = <<EOT
%{if var.dev_profile}
[Environment]::SetEnvironmentVariable("PRIMARY_CONNECTION_STRING", ${azurerm_cosmosdb_account.db_account.connection_strings[0]}, "User")
[Environment]::SetEnvironmentVariable("SECONDARY_CONNECTION_STRING", ${azurerm_cosmosdb_account.db_account.connection_strings[1]}, "User")
%{endif}
EOT
interpreter = ["PowerShell", "-Command"]
}
}