RubyTerraform

A simple wrapper around the Terraform binary to allow execution from within a Ruby program, RSpec test or Rakefile.

Installation

Add this line to your application’s Gemfile:

gem 'ruby-terraform'

And then execute:

$ bundle

Or install it yourself as:

$ gem install ruby-terraform

Usage

To require RubyTerraform:

require 'ruby-terraform'

Supported versions and commands

RubyTerraform supports all commands and options up to Terraform 0.15, except terraform console, terraform test and terraform version.

Getting started

There are a couple of ways to call Terraform using RubyTerraform.

Firstly, the RubyTerraform module includes class methods for each of the supported Terraform commands. Each class method takes a parameter hash containing options to pass to Terraform.

For example, to save the plan of changes for a Terraform configuration located under infra/network to a file called network.tfplan whilst providing some vars:

RubyTerraform.plan(
  chdir: 'infra/network',
  out: 'network.tfplan',
  vars: {
    region: 'eu-central'
  },
  var_file: 'defaults.tfvars'
)

To apply the generated plan of changes:

RubyTerraform.apply(
  chdir: 'infra/network',
  plan: 'network.tfplan',
  vars: {
    region: 'eu-central'
  },
  var_file: 'defaults.tfvars'
)

…and to destroy the resulting resources:

RubyTerraform.destroy(
  chdir: 'infra/network',
  vars: {
    region: 'eu-central'
  },
  var_file: 'defaults.tfvars'
)

Each class method also accepts a second hash argument of invocation options to use at command invocation time. Currently, the only supported option is :environment which allows environment variables to be exposed to Terraform.

For example, to apply a configuration with trace level logging:

RubyTerraform.apply(
  {
    chdir: 'infra/network',
    plan: 'network.tfplan',
    vars: {
      region: 'eu-central'
    },
    var_file: 'defaults.tfvars'
  },
  {
    environment: {
      'TF_LOG' => 'trace'
    }
  }
)

Additionally, RubyTerraform allows command instances to be constructed and invoked separately. This is useful when you need to override global configuration on a command by command basis or when you need to pass a command around.

Using the command class approach, the equivalent plan invocation above can be achieved using:

command = RubyTerraform::Commands::Plan.new
command.execute(
  chdir: 'infra/network',
  out: 'network.tfplan',
  vars: {
    region: 'eu-central'
  },
  var_file: 'defaults.tfvars'
)

As with the class methods, the #execute method accepts a second hash argument of invocation options allowing an environment to be specified.

See the API docs for the module[https://infrablocks.github.io/ruby_terraform/RubyTerraform.html] or the module[https://infrablocks.github.io/ruby_terraform/RubyTerraform/Commands.html] more details on the supported commands.

Parameters

The parameter hash passed to each command, whether via the class methods or the #execute method, supports all the options available on the corresponding Terraform command. There are a few different types of options depending on what Terraform expects to receive:

  • Boolean options, accepting true or false, such as :input or :lock;

  • String options, accepting a single string value, such as :state or :target;

  • Array<String> options, accepting an array of strings, such as :var_files or :targets; and

  • Hash<String,Object> options, accepting a hash of key value pairs, where the value might be complex, such as :vars and :backend_config.

For all options that allow multiple values, both a singular and a plural option key are supported. For example, to specify multiple var files during a plan:

RubyTerraform.plan(
  chdir: 'infra/network',
  out: 'network.tfplan',
  var_file: 'defaults.tfvars',
  var_files: %w[environment.tfvars secrets.tfvars]
)

In this case, all three var files are passed to Terraform.

Some options have aliases. For example, the :out option can also be provided as :plan for symmetry with other terraform commands. However, in such situations only one of the aliases should be used in the provided parameters hash.

See the API docs for a more complete listing of available parameter options.

Configuration

RubyTerraform uses sensible defaults for all configuration options. However, there are a couple of ways to override the defaults when they are sufficient.

Binary

By default, RubyTerraform looks for the Terraform binary on the system path. To globally configure a specific binary location:

RubyTerraform.configure do |config|
  config.binary = 'vendor/terraform/bin/terraform'
end

To configure the Terraform binary on a command by command basis, for example for the Plan command:

command = RubyTerraform::Commands::Plan.new(
  binary: 'vendor/terraform/bin/terraform'
)
command.execute(
  # ...
)

Logging

By default, RubyTerraform ‘s own log statements are logged to $stdout with level info.

To globally configure a custom logger:

require 'logger'

logger = Logger.new($stdout)
logger.level = Logger::DEBUG

RubyTerraform.configure do |config|
  config.logger = logger
end

RubyTerraform supports logging to multiple different outputs at once, for example:

require 'logger'

file_device = Logger::LogDevice.new('/foo/bar.log')
stdout_device = Logger::LogDevice.new(STDOUT)
multi_io = RubyTerraform::MultiIO.new(file_device, stdout_device)

logger = Logger.new(multi_io, level: :debug)

RubyTerraform.configure do |config|
  config.binary = '/binary/path/terraform'
  config.logger = logger
  config.stdout = multi_io
  config.stderr = multi_io
end

Creating the Logger with a file this way (using Logger::LogDevice), guarantees that the buffer content will be saved/written, as it sets implicit flushing.

Configured in this way, any logging performed by RubyTerraform will log to both STDOUT and to the specified file.

To configure the logger on a command by command basis, for example for the Show command:

require 'logger'

logger = Logger.new($stdout)
logger.level = Logger::DEBUG

command = RubyTerraform::Commands::Show.new(
  logger: logger
)
command.execute(
  # ...
)

Standard streams

By default, RubyTerraform uses streams $stdin, $stdout and $stderr.

To configure custom output and error streams:

log_file = File.open('path/to/some/ruby_terraform.log', 'a')

RubyTerraform.configure do |config|
  config.stdout = log_file
  config.stderr = log_file
end

In this way, both outputs will be redirected to log_file.

Similarly, a custom input stream can be configured:

require 'stringio'

input = StringIO.new("user\ninput\n")

RubyTerraform.configure do |config|
  config.stdin = input
end

In this way, terraform can be driven by input from somewhere other than interactive input from the terminal.

To configure the standard streams on a command by command basis, for example for the Init command:

require 'logger'

input = StringIO.new("user\ninput\n")
log_file = File.open('path/to/some/ruby_terraform.log', 'a')

command = RubyTerraform::Commands::Init.new(
  stdin: input,
  stdout: log_file,
  stderr: log_file
)
command.execute(
  # ...
)

Documentation

Development

To install dependencies and run the build, run the pre-commit build:

script
./go

This runs all unit tests and other checks including coverage and code linting / formatting.

To run only the unit tests, including coverage:

script
./go test:unit

To attempt to fix any code linting / formatting issues:

script
./go library:fix

To check for code linting / formatting issues without fixing:

script
./go library:check

You can also run bin/console for an interactive prompt that will allow you to experiment.

Managing CircleCI keys

To encrypt a GPG key for use by CircleCI:

openssl aes-256-cbc \
  -e \
  -md sha1 \
  -in ./config/secrets/ci/gpg.private \
  -out ./.circleci/gpg.private.enc \
  -k "<passphrase>"

To check decryption is working correctly:

openssl aes-256-cbc \
  -d \
  -md sha1 \
  -in ./.circleci/gpg.private.enc \
  -k "<passphrase>"

Contributing

Bug reports and pull requests are welcome on GitHub at github.com/infrablocks/ruby_terraform. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.