Backend Azurerm example does not work - Embedded ruby in config/terraform/backend.tf

Hello,

I am testing Terraspace but I faced an issue for dynamically configuring the backend azurerm.
I am trying to apply my company naming convention for resources in the Cloud provider, so I started to use ‘data external_provider’ but it seems it’s not possible in the backend.tf as it is called in early stage of the build. So when I see that we can wrap/embeded ruby code in Terraspace, I would like to give a try but it does not work as expected.
If it is not possible, I will probably use a Makefile to generate the backend config more dynamically before calling terraspace.

I tried this example:
Backend Azurerm - Terraspace

config/terraform/backend.tf:

<%
def resource_group_name
  if ENV['APP']
    expansion("#{ENV['APP']}-:ENV-:LOCATION")
  else
    expansion(":ENV-:LOCATION")
  end
end
%>

terraform {
  backend "azurerm" {
    resource_group_name = "<%= resource_group_name %>"
    storage_account_name = "<%= expansion('ts:SUBSCRIPTION_HASH:LOCATION:ENV') %>"
    container_name = "terraform-state"
    key = "<%= expansion(':LOCATION/:ENV/:BUILD_DIR/terraform.tfstate') %>"
  }
}

But when I do

FULL_BACKTRACE=1 APP=app1 terraspace plan demo

I got the following error:

$> FULL_BACKTRACE=1 APP=app1 terraspace plan demo
NameError: undefined local variable or method `resource_group_name' for #<Terraspace::Compiler::Erb::Context:0x000055cd181660a8 @mod=#<Terraspace::Mod:0x000055cd18072b10 @options={"copy_to_root"=>true, "mod"=>"demo", "args"=>[]}, @name="demo", @consider_stacks=true, @instance=nil, @resolved=true, @_memoized_root="/mnt/c/Users/xxxxx/source/repos/xxxxx/xxx/Projects/terraspace_play/infra_test/app/stacks/demo", @root_module=true, @_memoized_cache_dir="/mnt/c/Users/xxxxx/source/repos/xxxxx/Projects/terraspace_play/infra_test/.terraspace-cache/eastus/dev/stacks/demo">, @options={"copy_to_root"=>true, "mod"=>"demo", "args"=>[]}>
Error evaluating ERB template around line 13 of: /mnt/c/Users/xxxxx/source/repos/xxxxx/Projects/terraspace_play/infra_test/config/terraform/backend.tf:
 8 end
 9 %>
10
11 terraform {
12   backend "azurerm" {
13     resource_group_name = "<%= resource_group_name %>"
14     storage_account_name = "<%= expansion('ts:SUBSCRIPTION_HASH:LOCATION:ENV') %>"
15     container_name = "terraform-state"
16     key = "<%= expansion(':LOCATION/:ENV/:BUILD_DIR/terraform.tfstate') %>"
17   }
18 }

Original backtrace:
/mnt/c/Users/xxxxx/source/repos/xxxxx/Projects/terraspace_play/infra_test/config/terraform/backend.tf:13:in `__tilt_1840'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/tilt-2.0.10/lib/tilt/template.rb:170:in `call'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/tilt-2.0.10/lib/tilt/template.rb:170:in `evaluate'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/tilt-2.0.10/lib/tilt/template.rb:109:in `render'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/render_me_pretty-0.8.4/lib/render_me_pretty/erb.rb:91:in `render'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/render_me_pretty-0.8.4/lib/render_me_pretty.rb:11:in `result'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/erb/render.rb:9:in `build'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/strategy/mod/tf.rb:4:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/strategy/mod.rb:6:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/cleaner/backend_change.rb:40:in `fresh_backend'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/cleaner/backend_change.rb:21:in `purge?'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/cleaner/backend_change.rb:10:in `purge'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/cleaner.rb:16:in `backend_change_purge'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/compiler/cleaner.rb:9:in `clean'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/builder.rb:77:in `clean'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/builder.rb:29:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/cli/commander.rb:9:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/cli.rb:155:in `plan'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor/command.rb:27:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor/invocation.rb:127:in `invoke_command'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor.rb:392:in `dispatch'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/command.rb:61:in `dispatch'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/thor-1.2.1/lib/thor/base.rb:485:in `start'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/lib/terraspace/cli/concern.rb:65:in `start'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/terraspace-1.1.7/exe/terraspace:14:in `<top (required)>'
/opt/terraspace/embedded/bin/terraspace:25:in `load'
/opt/terraspace/embedded/bin/terraspace:25:in `<top (required)>'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/cli/exec.rb:58:in `load'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/cli/exec.rb:58:in `kernel_load'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/cli/exec.rb:23:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/cli.rb:484:in `exec'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/cli.rb:31:in `dispatch'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/cli.rb:25:in `start'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/exe/bundle:48:in `block in <top (required)>'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/lib/bundler/friendly_errors.rb:103:in `with_friendly_errors'
/opt/terraspace/embedded/lib/ruby/gems/3.0.0/gems/bundler-2.3.7/exe/bundle:36:in `<top (required)>'
/opt/terraspace/embedded/bin/bundle:23:in `load'
/opt/terraspace/embedded/bin/bundle:23:in `<main>'

Re-run with FULL_BACKTRACE=1 to see all lines

Interestingly. Dug into this and was able to reproduce it.

Turns out, methods cannot be defined inline with the ERB rendering library that terraspace uses.

Note with normal ERB, methods can be defined inline. Terraspace uses render_me_pretty and tilt since it provides a friendlier interface, though.

Simple Reproduction

Tilt::ERBTemplate#render does not support inline method definitions. Reproduced a simple case with:

tilt_demo.rb

require "tilt"
path = "./template.erb"
tilt_options = {trim: '-', default_encoding: "utf-8"}
template = Tilt::ERBTemplate.new(path, tilt_options)
context = {a: "a test"}
template.render(context)

And template file

template.erb

<%
def foo
  "bar"
end
%>
a is <%= @a %>
foo is <%= foo %>

Results in

$ ruby tilt_demo.rb
./template.erb:7:in `__tilt_60': undefined local variable or method `foo' for {:a=>"a test"}:Hash (NameError)
        from /home/ec2-user/.rvm/gems/ruby-3.1.1/gems/tilt-2.0.10/lib/tilt/template.rb:170:in `call'
        from /home/ec2-user/.rvm/gems/ruby-3.1.1/gems/tilt-2.0.10/lib/tilt/template.rb:170:in `evaluate'
        from /home/ec2-user/.rvm/gems/ruby-3.1.1/gems/tilt-2.0.10/lib/tilt/template.rb:109:in `render'
        from tilt_demo.rb:7:in `<main>'

Solution 1

Instead of defining the method inline, you can define the method as a helper instead:

terraspace new helper resource_group --type project

Then adjust it like so:

config/helpers/resource_group_helper.rb

module Terraspace::Project::ResourceGroupHelper
  def resource_group_name
    if ENV['APP']
      expansion("#{ENV['APP']}-:ENV-:LOCATION")
    else
      expansion(":ENV-:LOCATION")
    end
  end
end

Then the method is available and can be used.

config/terraform/backend.tf

terraform {
  backend "azurerm" {
    resource_group_name  = "<%= resource_group_name %>"
    storage_account_name = "<%= expansion('ts:SUBSCRIPTION_HASH:LOCATION:ENV') %>"
    container_name       = "terraform-state"
    key                  = "<%= expansion(':LOCATION/:ENV/:BUILD_DIR/terraform.tfstate') %>"
  }
}

In some ways, it’s cleaner since the method definition is not in the ERB. Though it would be clearer for a simple one-off method to be able to have the definition right there in the ERB. There are pros and cons.

Solution 2

config/terraform/backend.tf

If you want the defining code right there, use a local variable instead. This also works.

<%
resource_group_name = if ENV['APP']
                        expansion("#{ENV['APP']}-:ENV-:LOCATION")
                      else
                        expansion(":ENV-:LOCATION")
                      end
%>

terraform {
  backend "azurerm" {
    resource_group_name  = "<%= resource_group_name %>"
    storage_account_name = "<%= expansion('ts:SUBSCRIPTION_HASH:LOCATION:ENV') %>"
    container_name       = "terraform-state"
    key                  = "<%= expansion(':LOCATION/:ENV/:BUILD_DIR/terraform.tfstate') %>"
  }
}
1 Like