Terraform in 10 commands

Mikael Gibert
Teemo Tech Blog
Published in
8 min readApr 13, 2018

--

At Teemo, we manage all our infrastructure as code using Terraform. This approach allows us to apply software engineering processes to key infrastructure elements (e.g. code linting, testing, code review, versioning, continuous integration, continuous delivery, continuous deployment).

As a cool side-effect, dev and ops are now able to collaborate and define the infrastructure needed to support our features. After an initial discussion about main choices, taking in consideration traffic estimation, scaling rules, sizing, and involved protocols, developers are free to write and adapt Terraform configurations, allowing operations teams to review these files.

Here are ten lessons, commands and definitions we learned by using Terraform over last year.

Artist’s impression of the terraforming of Mars, from its current state to a livable world. Credit: Daein Ballard

1. Code linting

It is easy to lint Terraform code using a built-in fmt command. The update occurs directly on Terraform files but it can be disabled thanks to options.

terraform fmt

This command should be one of the first steps on your CI/CD pipeline to ensure that code style does not diverge in time.

See https://www.terraform.io/docs/commands/fmt.html for more information.

2. Understanding changes

Terraform works by detecting current infrastructure tests and calculating differences between known state and desired state. To avoid blind changes, Terraform outputs by default its migration plan between its current state and target and asks the user for confirmation.

There are two useful commands for supporting this check:

terraform plan

This outputs the migration plan and is particularly useful for letting someone else validate the change or to verify that there are no unwanted changes.

terraform apply

Terraform applies a migration plan. You also can specify the plan to apply to be sure there are no changes between the review and the effective change.

3. State

Terraform stores the resources it manages into a state file. There are two types of state files: remote or local. Where local state is great for an isolated developer, remote state is quite indispensable for a team as each member will need to share the infrastructure state whenever there is a change.

Each time a change is applied, the state is updated with new values (creations, deletions, updates) for both developer-defined values and computed values.

Example: defining a remote GCS state

terraform {
backend "gcs" {
bucket = "my-terraform-states"
prefix = "state-file-prefix"
}
}

Remote state can be updated without applying a change (imagine you deleted a managed resource manually) using Terraform state subcommands

Note that some features depend on the backend (for instance, the workspace feature is not always supported).

For more information, see https://www.terraform.io/docs/state/

4. Workspace

A useful feature that has recently been added to GCS backend is the ability to use multiple workspaces. It enables to have a single directory containing your Terraform configuration files and deploy a different target depending on the workspace.

You can define dev, staging, production environment as workspaces and use the same configuration to deploy each environment. This avoids duplicating all the files into a directory per environment. However, the cost of this flexibility is that your configuration files need to define value depending on the current workspace.

A workspace is created using this command:

terraform workspace new myworkspace

Example: a disk sized 200 GB in production and 10 GB in other environments.

resource "google_compute_disk" "my-disk" {
name = "my-disk"
type = "pd-standard"
zone = "${var.zone}"
disk_size_gb = "${terraform.workspace == "production" ? 200 : 50}"
}

For more information, see https://www.terraform.io/docs/state/workspaces.html

5. Variables

Variables can be defined by the Terraform files and provided when executing a command. They give more flexibility to our configurations and let us deploy the same elements in different zones or with different sizes depending on variable value.

Example: Create a disk in a variable availability zone with GCP provider.

variable "zone" {
default = "europe-west1-a"
}

resource "google_compute_disk" "my-disk" {
name = "my-disk"
type = "pd-standard"
zone = "${var.zone}"
}

If we do not override this variable, it will take the hardcoded default value. But you can also provide a value at runtime by using an environment variable or a command-line parameter.

Environment variables

You have to prefix the variable name with TF_VAR_myvar so that Terraform command will understand you want to define myvar value.

TF_VAR_zone=europe-west1-a terraform apply

Command-line parameter variables

For command-line parameter, you can use the var key like this:

terraform apply -var zone=europe-west1-a

Variables file

Variables can also be defined in a dedicated file (for example, staging.tfvar or production.tfvars).

Example:

terraform apply -var-file=production.tf

For more information, see https://www.terraform.io/docs/configuration/variables.html

6. Resources

Resources are elements you manage from Terraform, which can be an compute engine instance, a DNS record, a firewall rule, an external IP address for cloud providers, or even a table or a database.

Each Terraform provider defines their own resources, so it highly depends on the provider’s business (Github provider defines repository resource, Amazon provider proposes VPC resource, Google provider proposes Bigquery resources, Datadog providers proposes a timeboard resource…).

A resource definition consists of:

  • a resource type (google_compute_instance, datadog_timeboard, github_repository…)
  • a resource name (up to you, just remember that the couple resource type / resource name has to be unique)
  • other properties such as instance size, disk type, repository name…

Example: A health-check resource using GCP provider:

resource "google_compute_health_check" "my-healthcheck" {
name = "my-healthcheck"
check_interval_sec = 1
timeout_sec = 1
healthy_threshold = 2
unhealthy_threshold = 10

http_health_check {
request_path = "/"
port = "80"
}
}

Resources can be linked using this syntax:

my_property = "${google_compute_healthcheck.my-healthcheck.<a_computed_property>}"

7. Using resources created outside Terraform

Terraform is an incredibly useful tool when you want to create new infrastructure elements. But when we introduced it to our toolbelt, we had a lot of existing elements that were created directly from cloud console (e.g. manually).

Import

Fortunately, Terraform is able to import these elements, which basically means adding them to its state. To do this, you have to write the relevant Terraform configuration and then run the “apply” command to let Terraform update the state with the imported elements.

Example: importing a disk using GCP provider

resource "google_compute_disk" "my-disk" {
name = "my-disk"
type = "pd-standard"
zone = "europe-west1-a"
}

Then, use this command to import the existing disk to Terraform state:

terraform import google_compute_disk.my-disk my-disk

More information at https://www.terraform.io/docs/import/index.html Remember that import is resource dependent. Do not forget to look at a specific resource documentation to see if it supports import.

Data sources

Another way for working with resources managed externally (manually, by another tool or by another Terraform configuration) is to use data sources.

You can add a data source in your configuration file, before using and linking it with other resources that you manage.

An example to illustrate how they can be useful (with GCP provider):

data "google_compute_image" "my_image" {
name = "debian-9"
project = "debian-cloud"
}

resource "google_compute_instance" "default" {
# …
boot_disk {
initialize_params {
image = "${data.google_compute_image.my_image.self_link} "
}
}
}

This configuration will retrieve the latest non-deprecated image from debian-9 family and use it to initialize an instance boot disk.

We can also use a remote Terraform state as a data source to directly gain access to all resources managed by another Terraform configuration.

Example:

data "terraform_remote_state" "vpc" {
backend = "atlas"
config {
name = "hashicorp/vpc-prod"
}
}
resource "aws_instance" "foo" {
# …
subnet_id = "${data.terraform_remote_state.vpc.subnet_id}"
}

See https://www.terraform.io/docs/configuration/data-sources.html for more information and remember that data sources are defined at the provider level. Be sure to look directly at the provider documentation to see what data sources are supported.

8. Output computed values

Resources parameters are defined in advance but there are many values that are computed at the creation time (compute engine instance internal ip, service account email, etc…).

Terraform supports the output command and related configuration blocks that allow you to display computed values. These can be useful for debugging or if you have to chain Terraform with another tool (we can imagine a scenario where Terraform outputs an Ansible inventory used to deploy a generic playbook, for example).

Example: output an external ip using GCP provider

resource "google_compute_global_address" "my-address" {
name = "my-address"
}

output "External IP" {
value = "${google_compute_global_address.myaddress.address}"
}

Example: output a data source

data "google_compute_global_address" "my-address" {
name = "my-address"
}
output "External IP" {
value = "${data.google_compute_global_address.my-address.address}"
}

And computed value is then output when you apply your changes or explicitly type the following command:

terraform output

Beware that you need the value to already be in the Terraform state in order to successfully output. If you don’t see your variable, you need to apply your plan, import the variable, or add it manually to your state to make it visible.

See also: https://www.terraform.io/docs/commands/output.html

9. Lifecycle

Terraform lifecycle can be customized on a resource basis, which makes it really flexible and adaptable to any situation.

This allows us to prevent a resource being destroyed, which will lead to an error when any command involving this resource destruction is applied (block is removed and plan is applied, Terraform destroy, or Terraform taint then plan is applied).

Example:

disk {
source_image = "debian-cloud/debian-9"
disk_type = "pd-standard"
disk_size_gb = 20
auto_delete = true

lifecycle {
prevent_destroy = true
}
}

Another feature is to let resources be created before being destroyed. This means that if you trigger a change that implies resource re-creation, then you can override default Terraform behavior (destroy the previous resource and creating it again with new parameters) to first create the new resource and then destroy the previous one.

Example:

disk {
source_image = "debian-cloud/debian-9"
disk_type = "pd-standard"
disk_size_gb = 20
auto_delete = true

lifecycle {
create_before_destroy = true
}
}

Last but not least, we can tell Terraform to ignore some changes if we do not want to re-create resources on the changes of some properties. It is particularly useful to work around some bugs. For instance, waiting for a service account bug to be merged, we had to use the lifecycle to avoid permanent resource destruction/creation.

Example:

disk {
source_image = "debian-cloud/debian-9"
disk_type = "pd-standard"
disk_size_gb = 20
auto_delete = true

lifecycle {
ignore_changes = ["source_image"]
}
}

10. Project structure

We have many small self-supporting projects, from infrastructure to code without forgetting middleware configuration. In that case, we did not feel the pain of large growing projects where Hashicorp recommends the use of many remote state and different directories. We chose to define one Terraform configuration file per service family (one for IAM, one for databases, one for networking…).

Conclusion

For us, Terraform is an incredibly powerful tool. Its customization capabilities let us tackle blocking issues. We were also impressed by the community reactivity (especially the one responsible of the google provider). We plan to use it to perform rolling upgrades and increase our Infrastructure as Code coverage to manage items like DNS records or even Kibana dashboards!

--

--