We previously covered several limitations of Terragrunt managing the creation of the state bucket, log bucket, and lock table used for storing Terraform remote state in AWS. For instance, we often want full control over encryption properties, and Terragrunt does not allow you to specify things like custom encryption keys.
These limitations often drive our team to eschew Terragrunt’s management of the operational infrastructure Terraform requires. Many of our teams have found CloudFormation to be an attractive alternative for managing this operational infrastructure.
Chris, BTI360 engineer and author of thirstydeveloper.io, will show us how to get started using CloudFormation to bootstrap an account for Terraform and Terragrunt.
Terraform State with CloudFormation
We’ll start with a CloudFormation template that exactly mirrors what Terragrunt creates for you so that:
- You can use this template to import resources Terragrunt has already created for you
- We can discuss ways to harden beyond what Terragrunt does for you
Assuming you have Terragrunt’s remote_state configured as follows:
locals {
root_deployments_dir = get_parent_terragrunt_dir()
relative_deployment_path = path_relative_to_include()
deployment_path_components = compact(split("/", local.relative_deployment_path))
tier = local.deployment_path_components[0]
stack = reverse(local.deployment_path_components)[0]
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "bti360-terraform-state"
region = "us-east-1"
encrypt = true
key = "${dirname(local.relative_deployment_path)}/${local.stack}.tfstate"
dynamodb_table = "bti360-terraform-state-locks"
accesslogging_bucket_name = "bti360-terraform-state-logs"
}
}and assuming you’re using Terragrunt 0.26.4, here’s a CloudFormation template that matches what Terragrunt creates with the above configuration:
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy terraform operational infrastructure
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Terraform State Resources
Parameters:
- StateBucketName
- StateLogBucketName
- LockTableName
Parameters:
StateBucketName:
Type: String
Description: Name of the S3 bucket for terraform state
StateLogBucketName:
Type: String
Description: Name of the S3 bucket for terraform state logs
LockTableName:
Type: String
Description: Name of the terraform DynamoDB lock table
Resources:
TerraformStateLogBucket:
Type: 'AWS::S3::Bucket'
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Ref StateLogBucketName
AccessControl: LogDeliveryWrite
TerraformStateBucket:
Type: 'AWS::S3::Bucket'
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Ref StateBucketName
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
LoggingConfiguration:
DestinationBucketName: !Ref StateLogBucketName
LogFilePrefix: TFStateLogs/
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
TerraformStateLockTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
TableName: !Ref LockTableName
AttributeDefinitions:
- AttributeName: LockID
AttributeType: S
KeySchema:
- AttributeName: LockID
KeyType: HASH
BillingMode: PAY_PER_REQUESTPut this YAML into a terraform-bootstrap.cf.yml file. For today’s post, we’re going to assume you’re working from a clean slate. If you already have a state bucket, log bucket, or lock table that Terragrunt created for you, you can use CloudFormation’s resource import feature to bring those under CloudFormation’s control. thirstydeveloper.io has a step-by-step guide on importing Terragrunt created resources into CloudFormation.
Assuming you’re starting fresh, run the following to deploy your CloudFormation stack:
aws --profile bti360 cloudformation deploy \
--template-file terraform-bootstrap.cf.yml
--stack-name terraform-bootstrap \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
StateBucketName=bti360-terraform-state \
StateLogBucketName=bti360-terraform-state-logs \
LockTableName=bti360-terraform-state-locksOnce the stack finishes deployment, you can run terragrunt commands with the above remote_state configuration, and Terragrunt will use the state bucket, log bucket, and lock table CloudFormation created.
Hardening Opportunities
With Terraform’s operational infrastructure managed by CloudFormation, let’s discuss ways to improve Terragrunt’s defaults.
Here are four specific hardening opportunities our teams often exercise:
1. Encryption on the logs bucket
Terragrunt sadly does not enable encryption on the logs bucket by default. We can remedy this in CloudFormation by specifying the BucketEncryption property, just like we do on the state bucket.
2. Block public access on the logs bucket
Similarly, the logs bucket does not explicitly block public access. We can do that with a PublicAccessBlockConfiguration property, again mirroring our state bucket.
3. Encrypt the lock table
We can enable encryption on the DynamoDB lock table using the SSESpecification property.
4. Restrict access to the state bucket
We can use an AWS::S3::BucketPolicy resource to lock down which IAM principals can access our Terraform state. Implementing access control around Terraform state is an excellent idea considering (1) the state almost always contains sensitive information, and (2) losing state can cause tremendous pain to import existing resources back under Terraform’s control.
Conclusion
While we use Terraform for the vast majority of our infrastructure-as-code needs at BTI360, we’ve found CloudFormation quite useful for bootstrapping an AWS account for Terraform management. We heartily recommend CloudFormation if your team has bumped into the limitations of letting Terragrunt manage the creation of Terraform’s operational infrastructure. For more information on this approach, including a fully worked example, see part 5 of Chris’ terraform-skeleton series on thirstydeveloper.io.
Interested in Solving Challenging Problems? Work Here!
Are you a software engineer, interested in joining a software company that invests in its teammates and promotes a strong engineering culture? Then you’re in the right place! Check out our current Career Opportunities. We’re always looking for like-minded engineers to join the BTI360 family.
