Terraform state in the Cloud

Terraform provisions cloud resources and keeps a record of the work it’s done. That record is held in the terraform.tfstate file.

Having those files locally makes it difficult to scale up from a single DevOps engineer working in isolation to a team of engineers collaborating.

Set up the S3 bucket

The solution is to store it in AWS S3 with a lock maintained in AWS DynamoDB. Here’s setup.tf:

# terraform state file setup
# create an S3 bucket to store the state file in
resource "aws_s3_bucket" "terraform-state-storage-s3" {
  bucket = "my-terraform-state-s3"
  region = "eu-west-2"

  versioning {
    # enable with caution, makes deleting S3 buckets tricky
    enabled = false
  }

  lifecycle {
    prevent_destroy = true
  }

  tags {
    name = "S3 Remote Terraform State Store"
    proj = "example-iac"
    env = "prod"
  }
}

# create a DynamoDB table for locking the state file
resource "aws_dynamodb_table" "dynamodb-terraform-state-lock" {
  name = "example-iac-terraform-state-lock-dynamo"
  hash_key = "LockID"
  read_capacity = 20
  write_capacity = 20

  attribute {
    name = "LockID"
    type = "S"
  }

  tags {
    name = "DynamoDB Terraform State Lock Table"
    proj = "example-iac"
    env = "prod"
  }
}

Bootstrapping problem

If you skip ahead and simply copy all the code from this tutorial (including the backend bit below) then do one big run with a terraform init && terraform apply, you’ll get this error:

[lightenn@do7 aws-background {master}]$ terraform apply
Error loading state: NoSuchBucket: The specified bucket does not exist
        status code: 404, request id: 716AE642035C23D4, host id: LR4R+2PWWXe217qGThkw9/c4FcNDxAjan0ogipubvHl2f5e+gFb/otXRx5tFjjHWx4nENt5iAGw=
[lightenn@do7 aws-background {master}]$ terraform init

Initializing the backend...
Error loading state: NoSuchBucket: The specified bucket does not exist
        status code: 404, request id: 542A0B8F18D21884, host id: glZaVv4mnpq2t3lo7toVrSHxsCCBqykk7f4Wp6zhu8GxeTZgsfsJdGd+dykNH0TexNVYosOgO78=

We need to create the S3 bucket and DynamoDB table before relying on them.

terraform init then terraform apply to create the resources.

That first Terraform run creates state itself and it’s stored locally. Now we want to transfer that state to the Cloud.

Start using S3 bucket for storing state

Create another file main.tf in the terraform-s3 module:

provider "aws" {
  region = "eu-west-2"
}

# store tfstate in s3 and locking information in DynamoDB
terraform {
  backend "s3" {
    encrypt = true
    # cannot contain interpolations
    # bucket = "${aws_s3_bucket.terraform-state-storage-s3.bucket}"
    bucket = "my-terraform-state-s3"
    # region = "${aws_s3_bucket.terraform-state-storage-s3.region}"
    region = "eu-west-2"
    # dynamodb_table = "example-iac-terraform-state-lock-dynamo"
    key = "terraform-state/terraform.tfstate"
  }
}

Note that you can’t use interpolations "${}" otherwise you’ll get an error like this:

Failed to load backend: Error loading backend config: 1 error(s) occurred:

* terraform.backend: configuration cannot contain interpolations

Also note that the DynamoDB line is commented. You might ask why. The problem is a race condition. First of all we need to get Terraform storing its state in the Cloud (S3 bucket), then locking it.

So again terraform init, but this time it asks us what we want to do with our local state.

[lightenn@do7 terraform-s3 {master}]$ terraform init

Initializing the backend...
Do you want to copy state from "local" to "s3"?
  Pre-existing state was found in "local" while migrating to "s3". No existing
  state was found in "s3". Do you want to copy the state from "local" to
  "s3"? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Answer yes because that transfers the local state, which includes information about the S3 bucket and DynamoDB table that we’ve created, to the Cloud.

Do a terraform apply to check everything is still working smoothly.

Enable locking

Once that’s sorted we can enable locking by uncommenting the DynamoDB table reference:

    region = "eu-west-2"
    dynamodb_table = "example-iac-terraform-state-lock-dynamo"
    key = "terraform-state/terraform.tfstate"

Again terraform init and Terraform detects the change then prompt us to confirm what we want to do:

[lightenn@do7 terraform-s3 {master}]$ terraform init

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now reconfigure for this backend. If you didn't
intend to reconfigure your backend please undo any changes to the "backend"
section in your Terraform configuration.


Do you want to copy the state from "s3"?
  Would you like to copy the state from your prior backend "s3" to the
  newly configured "s3" backend? If you're reconfiguring the same backend,
  answering "yes" or "no" shouldn't make a difference. Please answer exactly
  "yes" or "no".

  Enter a value: yes

Answer yes again because the state we’ve built up to this point is important to avoid the bootstrapping problem I mentioned earlier.

Finally terraform apply and we’ve got a complete working manifest with state stored in the Cloud.

Leave a comment