# Terraform styleguide

Terraform is tool for managing with infrastructure using declarative configuration files in conjunction with 'provider' interfaces. Although primarily used to manage Cloud resources such as VMs and networks, providers also exist to configure Github repositories, Datadog dashboards and [many more](https://www.terraform.io/docs/providers/index.html) infrastructure types.

## Installing and versioning Terraform

Since our configurations are not consistent when it comes to version-compatibility, the easiest way to install terraform is through the [tfswitch](https://warrensbox.github.io/terraform-switcher/) tool.

Once a terraform configuration is applied using a particular version, no operators will be able to apply the same configuration using a lower version. To help manage this more concretely, in all modules and root .tf files \(see below\), we should ensure that [required\_versions](https://www.terraform.io/docs/configuration/terraform.html#specifying-a-required-terraform-version) are set. This is especially important as we attempt to migrate between versions 0.11 and 0.12, the latter of which contains several breaking syntactic changes.

## Linting

The terraform subcommand `fmt` should be used before committing any PRs - most modern text editors have packages that will run `fmt` every time the file is saved. As with other programming languages, syntax and formatting changes should be separated from functional changes as far as possible.

## Directory Structure

When the `terraform` executable is run, it will scan the current working directory for all `*.tf` files, and perform a plan/application using the sum of all resources declared in these files. These resources are known collectively as the "root module". A nice convention is to split resources within the directory into well-named files, with a few reserved names as well, specifically:

* `variables.tf` for [input variables](https://www.terraform.io/docs/configuration/variables.html)
* `outputs.tf` for [output variables](https://www.terraform.io/docs/configuration/outputs.html)
* `main.tf` for [terraform settings](https://www.terraform.io/docs/configuration/terraform.html) and [provider declarations](https://www.terraform.io/docs/configuration/providers.html)
* `versions.tf` for [terraform](https://www.terraform.io/docs/configuration/terraform.html#specifying-a-required-terraform-version) and [provider](https://www.terraform.io/docs/configuration/terraform.html#specifying-a-required-terraform-version) versioning. Note that in configurations pre version 0.11, provider versioning is handled inside provider declarations.

## Modules

Terraform modules are a way of grouping together resources that are likely to be deployed in similar configurations in multiple places. A good example at Orchard is the `terraform-fargate` module, which creates a fargate cluster, task definition, an optional load balancer and autoscaling policies, as well as any necessary security groups and IAM resources.

Modules source code can be referenced in a few ways, such as local file paths, git repositories or the [terraform registry](https://www.terraform.io/docs/modules/sources.html#terraform-registry). While local paths can be useful for configurations where the module code lives in the same repository as the root, for most cases it makes more sense for us to take advantage of our multi-repo structure and reference remote Github repository. For modules that are used in many places, another best practice is to pin the module to a specific git revision, to ensure that major upgrades to a module have a smaller blast radius.

Another option to explore in the future would be the use of a private terraform registry, in order to ensure more granular version control.

## Resource Ids

Whenever we need to pass some attribute of a resource to another, with both resources created within the same terraform configuration, we always reference the initial resource, e.g.

```text
resource "aws_instance" "example" {
  ami           = "ami-xxxxxxxx"
  instance_type = "t2.micro"
}

resource "aws_eip" "ip" {
    vpc = true
    instance = aws_instance.example.id
}
```

We do this because the aws instance id \(e.g. `i-1234567890abcdef0`\) is random, ephemeral, and cannot be known before apply-time. On the other hand, if we had a dependency on a resource that was created within a different terraform configuration, by a different tool, or even manually in the console, this would not be possible. Instead, one should a data source:

```text
data "aws_instance" "example" {
  filter {
    name   = "tag:Name"
    values = ["instance-name-tag"]
  }
}

resource "aws_eip" "ip" {
    vpc = true
    instance = data.aws_instance.example.id
}
```

This way, if the AWS instance in question gets recreated with a different id for any reason, we can simply re-run terraform, and terraform will re-create the necessary resources as we have declared. In addition, this format is far more readable - the purpose of a given resource id in a configuration is completely opaque without a trip to the console, whereas a data source can reference a human-readable tag.

## Count vs for-each

Prior to terraform 0.11, if one wanted to create a number of similar instances of the same resource type, the recommended method was to use the `count` meta-parameter, e.g.

```text
variable "instance_names" {
  default = ["frontend", "backend"]
}
resource "aws_instance" "example" {
  count         = ${length(var.instance_names)}
  ami           = "ami-xxxxxxxx"
  instance_type = "t2.micro"
  tags = {
    Name = "${var.instance_names[count.index]}"
  }
}
```

which would create 2 instances with the Name tags `frontend`, and `backend`. Let's say we wanted to add a 3rd instance called `db.` The problem with `count` is that the instances are created, they exist in the state file as elements of a list with fixed indexes. Therefore, adding a new element to the list of instances _anywhere but the end of the list_ will result in the instances being renamed in state, and thus destroyed and recreated. For critical infrastructure, e.g. Github repositories, this is extremely dangerous behaviour.

Fortunately, in version 0.12 of terraform, a new meta-parameter called `for_each` was created, resulting in configurations like this:

```text
variable "instance_names" {
  default = ["frontend", "backend"]
  type    = set(string)
}

resource "aws_instance" "example" {
  for_each      = var.instance_names
  ami           = "ami-xxxxxxxx"
  instance_type = "t2.micro"
  tags = {
    "Name" = "instance-${each.key}"
  }
}
```

The result in state of using `for_each` is that created resource form an _unordered map_, rather than a list, with the values of the map being keyed by a unique resource identifier. Therefore, adding another string to `var.instance_names` will just create a new key in the state map, leaving the existing instances alone.

The `for_each` parameter can also be a map, in which case one can use `each.key` and `each.value` to fetch from it.

