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.
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 {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
variable "region" {
type = string
description = "AWS Region"
default = "ca-central-1"
}
variable "environment" {
type = string
description = "Type of environment"
}
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
}
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
]
}
}
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 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
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
}
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
}
}