If you’re an infrastructure engineer like I am, and if you use Terraform like I do, then your Terraform state files are some of the most important files you manage. If those files somehow disappeared, Terraform wouldn’t be able to know which resources it should manage, any subsequent terraform apply that you might run would probably fail (in the best case) and would maybe be actively destructive to your business (in the worst case). So you should be really careful to make sure your state files don’t disappear.

You probably host your state file on S3, which means it isn’t managed by version control (eg. git). It’s just a file on S3, and it could be vulnerable in the event that your AWS account gets hacked, it could be vulnerable to sabotage by a disgruntled employee, it could be vulnerable to an errant automation which is meant to clean up your S3 buckets, and so on…

Fortunately, S3 has a feature called S3 Object Lock which can protect your state file from deletion in any of those cases. In fact, it can protect your state file from deletion in every case, because when S3 Object Lock is enabled, it blocks all deletions, even from the root account and even from AWS Support. It can be configured to protect your state file (or anything) from deletion for X amount of time, where X is a user-configurable amount of days (including an infinite amount of days). This is a perfect use case for protecting your Terraform state file.

You can set Object Lock to protect all versions of your Terraform state file for X days (configurable), and then you can use an expiration policy to expire versions which are older than X + 1. If you used a 30 day retention period, for example, and you expect to run terraform apply as part of your normal course of work at least once every 30 days, then you would be able to notice any deletions of your Terraform state within that 30 day period and recover a deleted state file before it’s too late.

Here is how I like to set up my state buckets:

 1locals {
 2  environments = [
 3    "development",
 4    "staging",
 5    "production",
 6  ]
 7}
 8
 9# This is the bucket where you will store your terraform state
10resource "aws_s3_bucket" "state" {
11  for_each = toset(local.environments)
12  bucket   = "somecompany-terraform-state-${each.key}"
13}
14
15# Versioning is recommended in general for terraform state,
16# and versioning is required when using object locks
17resource "aws_s3_bucket_versioning" "state" {
18  for_each = aws_s3_bucket.state
19  bucket   = each.value.bucket
20
21  versioning_configuration {
22    status = "Enabled"
23  }
24}
25
26# Nobody can delete versions of my state file for 30 days,
27# and that really means nobody. Not me, not root, not even AWS.
28resource "aws_s3_bucket_object_lock_configuration" "state" {
29  for_each = aws_s3_bucket.state
30  bucket   = each.value.bucket
31
32  rule {
33    default_retention {
34      mode = "COMPLIANCE"
35      days = 30
36    }
37  }
38}
39
40# Delete non-current versions after 35 days,
41# and this number needs to be higher than the object lock period
42# for this to work as one would expect it to work.
43resource "aws_s3_bucket_lifecycle_configuration" "state" {
44  for_each = aws_s3_bucket.state
45  bucket   = each.value.bucket
46
47  rule {
48    id     = "delete-after-35-days"
49    status = "Enabled"
50
51    noncurrent_version_expiration {
52      noncurrent_days = 35
53    }
54  }
55}
56
57# Encryption is entirely unrelated to object locking,
58# but it's a best practice to enable it and it helps
59# with compliance.
60resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
61  for_each = aws_s3_bucket.state
62  bucket   = each.value.bucket
63
64  rule {
65    apply_server_side_encryption_by_default {
66      sse_algorithm = "AES256"
67    }
68  }
69}
70
71# Public access blocking is entirely unrelated to object locking,
72# but it's a best practice to enable it and it helps
73# with compliance.
74resource "aws_s3_bucket_public_access_block" "state" {
75  for_each = aws_s3_bucket.state
76  bucket   = each.value.bucket
77
78  block_public_acls       = true
79  block_public_policy     = true
80  ignore_public_acls      = true
81  restrict_public_buckets = true
82}
83
84# Ownership controls are entirely unrelated to object locking,
85# but it's a best practice to enable it and it helps
86# with compliance.
87resource "aws_s3_bucket_ownership_controls" "state" {
88  for_each = aws_s3_bucket.state
89  bucket   = each.value.bucket
90
91  rule {
92    object_ownership = "BucketOwnerEnforced"
93  }
94}

When using S3 Object Lock, no other special configuration is needed when setting up your S3 backend config.