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.