Simplify Terraform By Generating Configurations

Terraform is an awesome tool. To make it more awesome though we have wrapped it with some custom Ruby ERB templating to generate our terraform configurations from Yaml configurations.

Terraform uses a declarative language. You describe the state you want and it figures out how to get there. The declarative nature of Terraform does not afford us the same control that a language like Ruby can provide, which is fine, but I have found that I end up managing _massive_ Terraform configurations. It's easy to make mistakes. Generating our Terraform configurations allows us to create more robust services in a shorter amount of time. It's much easier to edit an 80 line yaml file than a 5000 line Terraform configuration. 

We have a script, let's call it "Terraform Generator", or `tg`. We pass to `tg` an environment configuration like `develop`. 

tg develop


This will generate the terraform configuration `terraform/develop/main.yml` from a yaml file at `configs/develop.yml`

The code to generate the file is pretty simple. By running the generated plan through `terraform fmt` we can also do some validation to ensure it's not broken! Simply put we read in the environments yaml file, parse an erb template with the content from that yaml file, output a terraform configuration file, then validate that file. 

`tg` does basically the following:


def output_terraform
    renderer = ERB.new(File.open("templates/#{@tf_template}").read)
    rendered = renderer.result(@context)
out_file = File.open("#{@output_directory}/main.tf", 'w')
out_file.puts(rendered)

format_terraform

end

def format_terraform system “terraform fmt #{@output_directory}” end

def create_output_directory directory = “terraform/#{@config[‘config_src’]}”

unless File.directory?(directory)
  FileUtils.mkdir_p(directory)
end

directory

end

def main(*argv) config_src = argv.last

@config           = YAML.load_file("configs/#{config_src}.yaml")
@tf_template      = 'main.tf.erb'
@output_directory = create_output_directory
@context          = binding

output_terraform

end

main(*ARGV)


The yaml file may look something like:


version: 1

region: us-west-1

tfstate: develop

cluster: name: DEVELOP

load_balancers: frontend: ssl_certs: - example.com backend: ssl_certs: - example.com

ssl_certs: default: domain_name: ‘example.com’ subject_alternative_names: - ‘*.example.com’

services:

my_service: task_count: 2 cpu: 256 memory: 512 https: load_balancer: frontend url: my_service.example.com health_check: path: / autoscaling: min: 2 max: 8 cpu: 75

my_other_service: task_count: 2 cpu: 256 memory: 512 https: load_balancer: backend url: my_other_service.example.com health_check: path: / autoscaling: min: 2 max: 4 cpu: 60


The ERB template:


terraform {
  required_version = ">= 0.14.5"

backend “s3” { bucket = “terraform-state” key = “<%= @config[‘tfstate’] %>/terraform.tfstate” region = “<%= @config[‘region’] %>”

# Force encryption
encrypt = true

}

required_providers { aws = { version = “~> 3.24.1” } } }

provider “aws” { region = “<%= @config[‘region’] %>” } <% @config[‘services’].each do |service, values| %> … <% end %>

… etc …