Learning Terraform – Part 4: State Management

This post is a continuation of my series of blog posts on learning Terraform.  In my previous post I discussed provisioners and modules.  In this post, I want to look at Terraform state and password management. 

State Management

Terraform records information about the infrastructure created in a Terraform state file (terraform.tfstate).  This file uses JSON to map the resources defined in the template to the real resources created.

For example, running a terraform apply on the following resource:

resource "azurerm_resource_group" "rg-terraform" {
  name     = "rg-terraform"
  location = "westus2"
}

Would result in a terraform.tfstate file that looks similar to this:

{
  "version": 4,
  "terraform_version": "0.13.2",
  "serial": 1,
  "lineage": "0488b052-626e-6ff0-45d6-e1afdc6c10a1",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "azurerm_resource_group",
      "name": "rg-terraform",
      "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "/subscriptions/.../resourceGroups/rg-terraform",
            "location": "westus2",
            "name": "rg-terraform",
            "tags": null,
            "timeouts": null
          },
          "private": "..."
        }
      ]
    }
  ]
}

When you run the terraform plan command, Terraform uses the JSON in the terraform.tfstate file to compare what’s in the configuration file vs. what’s been deployed to determine what changes need to be made.

Modifying Terraform State Files

Terraform state files should not be modified directly, instead they should be modified using the terraform state command.

terraform state <subcommand> [options] [args]

The terraform state command can be used on both local and remote state.  There are several Terraform state subcommands available to modify the state file.  Below is a high level overview of each sub command.

list:  Lists all resources within a Terraform state file.

$ terraform state list
azurerm_resource_group.rg-terraform

mv:  Moves items in the Terraform state file.  Used when you want to rename an existing resource without destroying it first.

$ terraform state mv azurerm_resource_group.rg-terraform azurerm_resource_group.rg-terraform
Move "azurerm_resource_group.rg-terraform" to "azurerm_resource_group.rg-example"
Successfully moved 1 object(s).

pull:  Manually download and output the state from remote state.

$ terraform state pull
{
  "version": 4,
  "terraform_version": "0.13.2",
  "serial": 2,
  "lineage": "b6a1608a-a22f-c565-f5b9-cc939ed88ba2",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "azurerm_resource_group",
      "name": "rg-terraform",
      "provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "/subscriptions/.../resourceGroups/rg-terraform",
            "location": "westus2",
            "name": "rg-terraform",
            "tags": null,
            "timeouts": null
          },
          "private": "..."
        }
      ]
    }
  ]
}

push:  Manually upload a local state file to remote state.  This should rarely be used.

rm:  Remove items from the Terraform state file.

$ terraform state rm azurerm_resource_group.rg-terraform
Removed azurerm_resource_group.rg-terraform
Successfully removed 1 resource instance(s)

show:  Shows the attributes of a single resource in state.

$ terraform state show azurerm_resource_group.rg-terraform
azurerm_resource_group.rg-terraform:
resource "azurerm_resource_group" "rg-terraform" {
id = "/subscriptions/.../resourceGroups/rg-terraform"
location = "westus2"
name = "rg-terraform"
}

Terraform Import

There are times when manually created resources need to be brought under Terraform management. Terraform supports this by using its import functionality. The Terraform documentation has a great tutorial that will walk you through the steps of importing existing infrastructure into Terraform.

Terraform Backends

A Terraform backend determines how state is loaded and how the terraform apply command is executed. 

Listed below are some benefits of using backends:

  • If you are working with a team, a remote backend can store state remotely and protect it with locking to prevent corruption.
  • State is retrieved from backends on demand and only stored in memory. This is valuable if you need to keep sensitive data from being written to disk.
  • If the infrastructure being deployed is large, terraform apply may take some time to apply the configuration changes.  Some backends support remote operations which allows the terraform apply operation to be executed remotely instead of from the local machine.

Local Backend

By default, Terraform uses a local backend.  A local backend stores state and performs operations on the terraform.tfstate file that resides on the local filesystem.  State is managed and locked using system APIs.

terraform {
  backend "local" {
    path = "relative/path/to/terraform.tfstate"
  }
}

When using a local backend, Terraform performs a state lock on the .tfstate file during the terraform plan and apply processes.. this prevents other users from updating the terraform.tfstate file at the same time which could result in corrupting the file.

Remote Backend

A remote backend allows you to store Terraform state files in a central repository such as an AWS S3 Bucket or Azure Blob Storage.  In order to do this, you need to create a backend.tf file which tells Terraform where the terraform.tfstate file will be stored. 

Below are examples of a backend.tf file for AWS and Azure..

AWS:

terraform {
    backend "s3"{
        bucket = "s3_bucket_name"
        key = "prod_terraform.tfstate" //name of .tfstate file
        region = "uswest1"
        access_key = "ACCESS_KEY"
        secret_key = "SECRET_KEY"
    }
}

Azure:

terraform {
  backend "azurerm" {
    storage_account_name = "storage_account_name"
    container_name = "container_name"
    key = "prod.terraform.tfstate" //name of .tfstate file
    access_key = "storage account access key"
  }
}

Note:  Azure authentication can be configured with the following:

  •             Managed Service Identity (MSI)
  •             Access Key associated with storage account
  •             SAS Token Associated with storage account

Once the backend.tf file is defined with the appropriate authentication, you can run the terraform init command and Terraform will use the .tfstate file in the remote location.

Not all remote backend types support state locking.  Each backend type describes its state locking functionality.

It’s important to review the documentation to address any specifics regarding remote state locking. 

Password Management

When using Terraform to manage infrastructure, it’s important to make sure passwords are not being stored in the .tf files or .tfvars files.

resource "azurerm_sql_server" "prod_sql" {
name = "prodsqlserver"
resource_group_name = azurerm_resource_group.example.name
location = "westus2"
version = "12.0"
# Don't do this!
administrator_login = "admin"
administrator_login_password = "Password"
}

There are few different options available to secure your passwords.

  • Submit variables at the command line using -var option
# Define variables in .tfvars file
variable "admin" {
description = "Admin Password"
type = string
}

variable "password" {
description = "DB Password"
type = string
}
#  Reference variables in resource block
resource "azurerm_sql_server" "prod_sql" {
name = "prodsqlserver"
resource_group_name = azurerm_resource_group.example.name
location = "westus2"
version = "12.0"
administrator_login = var.admin
administrator_login_password = var.password"
}
#  Set variables at the command line
$ export TF_VAR_admin=admin_username
$ export TF_VAR_password=password 
  • Use a secrets manager. Examples of these include Azure Key Vault, AWS Key Management Service or HashiCorp vault.

It’s important to note that even if you follow either of these password management techniques your passwords will still appear in the terraform.tfstate file in plain text. To address this you can store the terraform.tfstate file in a remote backend that supports encryption such as Azure Blob Storage, AWS S3 or Google Cloud Storage.

If you are using a local terraform.tfstate file, be sure you add it to the .gitignore file before committing to source control. Other files that you may want to add as well…

  •             .terraform (created during the terraform init)
  •             terraform.tfvars (potentially contains usernames and passwords)
  •             crash.log (terraform crash logs)

You can find the .gitignore template for Terraform at the link below: https://github.com/github/gitignore/blob/master/Terraform.gitignore

Summary

In this blog post I discussed Terraform state and password management. It’s important to secure not only usernames, passwords, API Keys, etc., but to make sure that the state file is secured as well.

Thank you for reading!