Skip to Content

Terraform Remote State

Create S3 bucket to hold remote state for Terraform, using Terraform

Remote State

Although my personal projects are not performed as part of a team, it is still useful to use Terraform remote state. I can switch between computers, as the Terraform configuration is in Git, and the Terraform state is in S3.

S3 Bucket Creation

You could just run aws s3 mb terraform-state and start to use it, but there are recommendations/suggestions for the configuration of the bucket.

  • Bucket versioning
    The documentation for the S3 backend recommends this to handle state recovery.
    While Amazon will happily store all revisions of the state for all time, I’m happy with retention of deleted versions for 14 days.
  • Encryption at rest
    The recommendations for sensitive data include enabling encryption at rest.
  • Require TLS transport
    It’s hard to avoid using a secure transport for S3, but might as well be sure.
  • Prevent public exposure
    Although buckets and objects are private by default, it is possible to override this on a per-object basis. This is why the AWS S3 console reports “Objects can be public”.
    All public access can be blocked by configuration of the bucket.

My setup requirements are now more complicated. This certainly calls for “Infrastructure as Code”.

Terraform Configuration

The obvious tool to configure my Terraform remote state is Terraform. This has to use local state. In this case, there is no sensitive information, so I’m happy to commit this.

The full configuration is available in the terraform-remote-state repository.

S3 Bucket

The S3 bucket resource is as follows. This enables versioning, and a lifecycle rule to remove deleted revisions older than 14 days. I’m also using Terraform lifecycle to try and protect myself from myself.

resource "aws_s3_bucket" "remote_state" {
  bucket = var.bucket_name

  versioning {
    enabled = true
  }

  lifecycle_rule {
    enabled = true
    noncurrent_version_expiration {
      days = 14
    }
  }

  lifecycle {
    prevent_destroy = true
  }
}

Bucket Policy

The bucket policy ensures that encrypted transport is used for all access, and that objects are encrypted as rest.

I’m a fan of Terraform data sources instead of a string literal of a JSON blob using a “heredoc”.

resource "aws_s3_bucket_policy" "remote_state" {
  bucket = aws_s3_bucket.remote_state.id
  policy = data.aws_iam_policy_document.remote_state.json
}

data "aws_iam_policy_document" "remote_state" {
  statement {
    sid       = "RequireEncryptedTransport"
    effect    = "Deny"
    actions   = ["s3:*"]
    resources = ["${aws_s3_bucket.remote_state.arn}/*"]
    condition {
      test     = "Bool"
      variable = "aws:SecureTransport"
      values   = ["false"]
    }
    principals {
      type        = "*"
      identifiers = ["*"]
    }
  }

  statement {
    sid       = "RequireEncryptedStorage"
    effect    = "Deny"
    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.remote_state.arn}/*"]
    condition {
      test     = "StringNotEquals"
      variable = "s3:x-amz-server-side-encryption"
      values   = ["AES256"]
    }
    principals {
      type        = "*"
      identifiers = ["*"]
    }
  }
}

Block Public Access

The public access block configuration is set to ensure that no object in the bucket can me made public.

The depends_on is to wait for application of the bucket policy. Details are available in issue 7628.

resource "aws_s3_bucket_public_access_block" "remote_state" {
  depends_on = [aws_s3_bucket_policy.remote_state]

  bucket = aws_s3_bucket.remote_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Using Remote State

Applying the Terraform configuration will create the S3 bucket for your state. The hard part is finding a memorable name that doesn’t conflict with the memorable name chosen by everyone else.

terraform apply \
  --var aws_region=eu-west-1 \
  --var bucket_name=terraform-state--demo

In your Terraform configurations that want to use remote state, add the following snippet:

terraform {
  backend "s3" {
    encrypt = true
    bucket  = "terraform-state--demo"
    key     = "sample/terraform.tfstate"
  }
}

The value for bucket needs to match the name of the created bucket, and the key needs to be unique for this configuration.

Setting encrypt = true is required, as the bucket policy mandated encryption at rest.