Terraform tips and tricks

3 minute read Published:

I've spent the last 2 months trying to explore the depths of Terraform (for AWS). Here's a compilation of neat things I discovered along the way.

Warning: many examples in this article are contrived/chopped down versions of the real thing, but they should serve the purpose of demonstration.

Essential links:

Chomp newlines

The chomp interpolation chomps the newline from a file. Useful if you store secrets in a filesystem (e.g. a different access-protected git repo):

db_password = "${chomp(file("/tf-secrets/mysql/db_password"))}"

Before chomp (pre-0.9.4), you would have to remove the newline from the file, or else your variable would be “{file_contents}\n” and cause problems.

Modules

There’s a lot to cover w.r.t. Terraform Modules, but a starting point is how I structure my TF repo:

/repos/tf $ tree -L 1
├── terraform
│   ├── consul-1-stg
│   └── vertica-v2-prd
└── terraform-modules
    ├── consul
    ├── public_vpc
    └── vertica

terraform contains provisioned resources. terraform-modules contains re-usable modules. You can (should?) have your modules in a separate git repo as well.

For now we’ll focus on a mono-repo approach to Terraform. From terraform, you can use modules with relative path imports:

module "vertica-v2-prd" {
  source            = "../../terraform-modules/vertica"

All of my variables are in the terraform/vertica-v2-prd directory, since this represents an instantiation of a Vertica cluster, with specific parameters (VPC, cluster size, etc.) that correspond to the logical stack (v2-prd).

The vertica module has no variables. Every variable it uses should come from the instantiation. Write this one as if you are sharing it with other people. Anything you hardcode will probably have to be painfully refactored at some point.

Variables

Continuing the above example, there are 3 variable files overall:

  • terraform/vertica-v2-prd/variables.tf - defines blank variables that the stack needs
  • terraform/vertica-v2-prd/terraform.tfvars - values for the above blank variables. If something is missing here, it will be prompted for interactively
  • terraform-modules/vertica/variables.tf - defines blank variables that the stack needs

State mv

When refactoring Terraform from monolithic to modular, or changing module names, the terraform state mv command is incredibly important. It lets you move resources to their new name - this may seem simple but Terraform can’t automatically track resources across module renames.

Here’s an example command to take a separate route53-zone resource (that has been instantiated, and contains a terraform.tfstate file) and absorb it as a module inside a larger stack, with the module name my_custom_route53_zone:

terraform state mv -state=./route53-zone/terraform.tfstate -state-out=./terraform.tfstate aws_route53_record.primary-alias module.my_custom_route53_zone.aws_route53_record.primary-alias

Terraform automatically backs up your tfstate files when executing this command - be careful.

Using existing infrastructure

With backends, you can include outputs from other resources:

data "terraform_remote_state" "global_route53_zone" {
  backend = "local"

  config {
    path = "${path.root}/../global-route53/terraform.tfstate"
  }
}
...
module "my_stack" {
...
zone_id           = "${data.terraform_remote_state.global_route53_zone.zone_id}"
domain            = "${data.terraform_remote_state.global_route53_zone.domain}"
...
}

You can go one step further and use a remote backend (Artifactory, Consul, S3, etc.). This is good because it keeps your tfstate files separate from your code.

Null resource

Here’s a way to execute things without consequences:

resource "null_resource" "provision_vertica_1" {
  connection {
    type        = "ssh"
    user        = "dbadmin"
    private_key = "${var.provision_keypair}"
    host        = "${aws_instance.vertica_node.0.public_ip}"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo /opt/vertica/sbin/install_vertica --hosts ${join(",", aws_instance.vertica_node.*.private_ip)} --dba-user-password-disabled --point-to-point --ssh-identity /home/dbadmin/.ssh/vertica_install_key --accept-eula",
    ]
  }
}

Previously I attempted to have the provisioner block inside the aws_instance block defining my Vertica instances. However, there’s a self-referential problem there: the provision command needs the aws_instance.*.private_ip values as parameters.

null_resource solves that by allowing you to depend on the Vertica aws_instance existing.

Conditional actions

$ cat whatever.tf
resource "aws_instance" "optional_machine" {
  count = "${var.with_optional}"
  ...
}
$ cat variables.tf
variable "with_optional" { description = "Create optional instance" }
$ terraform plan
var.with_optional
  Create optional instance

  Enter a value:

Enter 0 or 1 (or more than 1 if you want) to toggle the creation of this instance.

Moving forward

I hope to integrate with Consul as a remote backend and Vault to store secrets. There may be more as I learn.