Skip to content

Instantly share code, notes, and snippets.

@trevorbernard
Created May 2, 2023 12:56
Show Gist options
  • Save trevorbernard/58a9b1a7bcfc665acde8254891a6816c to your computer and use it in GitHub Desktop.
Save trevorbernard/58a9b1a7bcfc665acde8254891a6816c to your computer and use it in GitHub Desktop.
Literate program to create encrypted s3 bucket and provide support for concurrent modification

Terraform S3 Backend

Introduction

This configuration creates an encrypted S3 bucket to store your terraform state with versioning. This bucket is required if you want to store terraform state in the AWS cloud.

Terraform S3 backend

Export Instruction

You can export the source code from this literate program by running the following command line program:

emacs --batch --eval "(require 'org)" --eval '(org-babel-tangle-file "README.org")'

You can tangle the README.org literate program with following shortcut C-c C-v t or M-x org-babel-tangle-file in emacs.

Terraform Versions

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

Terraform Input Variables

variable "region" {
  type        = string
  description = "AWS Region"
  default     = "ca-central-1"
}

variable "environment" {
  type        = string
  description = "Type of environment"
}

Terraform Output Values

Log out important values

output "kms_key" {
  description = "The KMS customer master key to encrypt state buckets."
  value       = aws_kms_key.state_key
}

output "kms_key_alias" {
  description = "The alias of the KMS customer master key used to encrypt state bucket and dynamodb."
  value       = aws_kms_alias.state_key_alias
}

output "state_bucket" {
  description = "The S3 bucket to store the remote state file."
  value       = aws_s3_bucket.state
}

output "dynamodb_table" {
  description = "The DynamoDB table to manage lock states."
  value       = aws_dynamodb_table.state_lock
}

output "terraform_iam_policy" {
  description = "The IAM Policy to access remote state environment."
  value       = aws_iam_policy.terraform_policy
}

Terraform Role

We will follow the principle of least privilege. Create a role that has only the specfic access to modify the s3 bucket as well as the dynamodb lock table.

Note that we didn’t give access for s3:DeleteObject to prevent accidental deletion with terraform apply.

resource "aws_iam_policy" "terraform_policy" {
  name = "terraform-policy"
  policy = data.aws_iam_policy_document.state.json
}

# resource "aws_iam_role" "terraform_role" {
#   name               = "terraform-role"
#   assume_role_policy = data.aws_iam_policy_document.state.json
# }

data "aws_iam_policy_document" "state" {
  statement {
    effect    = "Allow"
    actions   = [
      "s3:ListBucket",
      "s3:GetBucketVersioning",
    ]
    resources = [
      aws_s3_bucket.state.arn
    ]
  }

  statement {
    effect    = "Allow"
    actions = [
      "s3:GetObject",
      "s3:PutObject",
      "s3:DeleteObject",
    ]
    resources = [
      "${aws_s3_bucket.state.arn}/*"
    ]
  }

  statement {
    effect = "Allow"
    actions = [
      "dynamodb:GetItem",
      "dynamodb:PutItem",
      "dynamodb:DeleteItem",
      "dynamodb:DescribeTable",
    ]
    resources = [
      aws_dynamodb_table.state_lock.arn
    ]
  }

  statement {
    effect = "Allow"
    actions = [
      "kms:ListKeys"
    ]
    resources = [
      "*"
    ]
  }

  statement {
    effect = "Allow"
    actions = [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:DescribeKey",
        "kms:GenerateDataKey"
    ]
    resources = [
      aws_kms_key.state_key.arn
    ]
  }
}

Provision KMS Encryption Key

It’s our responsibility to secure our terraform state at rest. We will provision a KMS key and convenience alias so that we may refer back to it at a later time.

resource "aws_kms_key" "state_key" {
  description         = "This key is used to encrypt the terraform state bucket and dynamodb table"
  enable_key_rotation = true
}

resource "aws_kms_alias" "state_key_alias" {
  name          = "alias/state-key-${var.environment}"
  target_key_id = aws_kms_key.state_key.key_id
}

Provision S3 Bucket

Provision the s3 bucket and and associate IAM policies.

Requirements:

  • Must be private and all public access blocked
  • Must be versioned so we can fallback if something goes wrong
  • Must prevent accidential deletion of s3 bucket
  • Must enforce SSL to access contents of the bucket

Create a S3 bucket for logging with Cloudtrail

resource "aws_s3_bucket" "state" {
  bucket = "granola-tfstate-${var.environment}"

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        kms_master_key_id = aws_kms_key.state_key.arn
        sse_algorithm     = "aws:kms"
      }
    }
  }
}

resource "aws_s3_bucket_acl" "state" {
  bucket = aws_s3_bucket.state.id
  acl    = "private"
}

resource "aws_s3_bucket_versioning" "state" {
  bucket = aws_s3_bucket.state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_policy" "state_force_ssl" {
  bucket = aws_s3_bucket.state.id
  policy = data.aws_iam_policy_document.state_force_ssl.json
  depends_on = [aws_s3_bucket_public_access_block.state]
}

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

resource "aws_s3_bucket_public_access_block" "state" {
  bucket                  = aws_s3_bucket.state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Provision DynamoDB Locking Table

Create a DynamoDB table to enable concurrent edits by serializing write with locking.

resource "aws_dynamodb_table" "state_lock" {
  name         = "granola-tfstate-lock-${var.environment}"
  hash_key     = "LockID"

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

  billing_mode = "PAY_PER_REQUEST"

  server_side_encryption {
    enabled     = true
    kms_key_arn = aws_kms_key.state_key.arn
  }

  point_in_time_recovery {
    enabled = true
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment