skip to content
Astro Cactus

Terraform Security and Best Practices

/ 6 min read

Series Navigation

Security Best Practices

Secure State Management

backend.tf
terraform {
backend "s3" {
bucket = "terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-west-2"
encrypt = true
kms_key_id = "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
dynamodb_table = "terraform-lock"
}
}
# state-bucket.tf
resource "aws_s3_bucket" "terraform_state" {
bucket = "terraform-state-bucket"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.terraform_state.id
sse_algorithm = "aws:kms"
}
}
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

Secrets Management

# Using AWS Secrets Manager
data "aws_secretsmanager_secret" "database" {
name = "prod/database"
}
data "aws_secretsmanager_secret_version" "database" {
secret_id = data.aws_secretsmanager_secret.database.id
}
locals {
db_creds = jsondecode(data.aws_secretsmanager_secret_version.database.secret_string)
}
resource "aws_db_instance" "main" {
username = local.db_creds.username
password = local.db_creds.password
# Other configuration...
}
# Using HashiCorp Vault
provider "vault" {
address = "https://vault.example.com:8200"
}
data "vault_generic_secret" "database" {
path = "secret/database"
}
resource "aws_db_instance" "main" {
username = data.vault_generic_secret.database.data["username"]
password = data.vault_generic_secret.database.data["password"]
# Other configuration...
}

IAM Best Practices

# Least Privilege IAM Policy
resource "aws_iam_role" "app" {
name = "app-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "app" {
name = "app-policy"
role = aws_iam_role.app.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.app.arn,
"${aws_s3_bucket.app.arn}/*"
]
}
]
})
}

Code Organization Best Practices

Project Structure

.
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── policies/
│ ├── iam/
│ └── security/
└── scripts/
├── deploy.sh
└── cleanup.sh

Module Design Patterns

modules/networking/variables.tf
variable "vpc_config" {
description = "VPC configuration"
type = object({
cidr_block = string
name = string
subnets = list(object({
cidr_block = string
zone = string
public = bool
}))
})
validation {
condition = can(cidrhost(var.vpc_config.cidr_block, 0))
error_message = "VPC CIDR block must be valid."
}
}
# modules/networking/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_config.cidr_block
tags = {
Name = var.vpc_config.name
}
}
resource "aws_subnet" "main" {
for_each = {
for idx, subnet in var.vpc_config.subnets :
"${subnet.zone}-${subnet.public ? "public" : "private"}" => subnet
}
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.zone
tags = {
Name = "${var.vpc_config.name}-${each.key}"
Type = each.value.public ? "public" : "private"
}
}

Resource Management

Resource Naming Convention

locals {
name_prefix = "${var.environment}-${var.project}"
common_tags = {
Environment = var.environment
Project = var.project
ManagedBy = "terraform"
Owner = "infrastructure-team"
}
}
resource "aws_instance" "web" {
count = var.instance_count
ami = var.ami_id
instance_type = var.instance_type
tags = merge(
local.common_tags,
{
Name = "${local.name_prefix}-web-${count.index + 1}"
Role = "web"
}
)
}

Resource Dependencies

# Explicit Dependencies
resource "aws_security_group" "web" {
# Configuration...
}
resource "aws_instance" "web" {
depends_on = [aws_security_group.web]
vpc_security_group_ids = [aws_security_group.web.id]
# Other configuration...
}
# Implicit Dependencies
resource "aws_db_instance" "main" {
# Configuration...
}
resource "aws_ssm_parameter" "db_endpoint" {
name = "/app/database/endpoint"
type = "String"
value = aws_db_instance.main.endpoint
}

Cost Optimization

Resource Scheduling

# Auto Scaling Based on Schedule
resource "aws_autoscaling_schedule" "scale_down" {
scheduled_action_name = "scale-down"
min_size = 1
max_size = 1
desired_capacity = 1
recurrence = "0 18 * * MON-FRI" # 6 PM on weekdays
autoscaling_group_name = aws_autoscaling_group.app.name
}
resource "aws_autoscaling_schedule" "scale_up" {
scheduled_action_name = "scale-up"
min_size = 2
max_size = 4
desired_capacity = 2
recurrence = "0 8 * * MON-FRI" # 8 AM on weekdays
autoscaling_group_name = aws_autoscaling_group.app.name
}

Cost Tagging

# Cost Allocation Tags
locals {
cost_tags = {
CostCenter = var.cost_center
Project = var.project_name
Environment = var.environment
}
}
resource "aws_instance" "app" {
# ... other configuration ...
tags = merge(
local.cost_tags,
{
Name = "${var.environment}-app"
}
)
}

Compliance and Governance

Policy as Code

# AWS Organizations Service Control Policy
resource "aws_organizations_policy" "compliance" {
name = "compliance-policy"
content = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Action = [
"s3:PutBucketPublicAccessBlock",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy"
]
Resource = "*"
Condition = {
StringNotEquals = {
"aws:PrincipalOrgID": aws_organizations_organization.main.id
}
}
}
]
})
}
# AWS Config Rules
resource "aws_config_config_rule" "encrypted_volumes" {
name = "encrypted-volumes"
source {
owner = "AWS"
source_identifier = "ENCRYPTED_VOLUMES"
}
scope {
compliance_resource_types = ["AWS::EC2::Volume"]
}
}

Compliance Monitoring

# CloudWatch Log Metric Filters
resource "aws_cloudwatch_log_metric_filter" "unauthorized_api_calls" {
name = "unauthorized-api-calls"
pattern = "{ $.errorCode = \"*UnauthorizedOperation\" }"
log_group_name = aws_cloudwatch_log_group.cloudtrail.name
metric_transformation {
name = "UnauthorizedAPICalls"
namespace = "CloudTrailMetrics"
value = "1"
}
}
resource "aws_cloudwatch_metric_alarm" "unauthorized_api_calls" {
alarm_name = "unauthorized-api-calls"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "UnauthorizedAPICalls"
namespace = "CloudTrailMetrics"
period = "300"
statistic = "Sum"
threshold = "1"
alarm_actions = [aws_sns_topic.security_alerts.arn]
}

Documentation Best Practices

Module Documentation

/**
* # VPC Module
*
* This module creates a VPC with public and private subnets
* across multiple availability zones.
*
* ## Usage
*
* ```hcl
* module "vpc" {
* source = "./modules/vpc"
*
* vpc_config = {
* cidr_block = "10.0.0.0/16"
* name = "main"
* subnets = [
* {
* cidr_block = "10.0.1.0/24"
* zone = "us-west-2a"
* public = true
* }
* ]
* }
* }
* ```
*/
variable "vpc_config" {
description = "VPC configuration object"
type = object({
cidr_block = string
name = string
subnets = list(object({
cidr_block = string
zone = string
public = bool
}))
})
}

README Templates

# Infrastructure Module
## Overview
This module manages core infrastructure components including VPC, subnets, and security groups.
## Requirements
| Name | Version |
|------|---------|
| terraform | >= 1.0.0 |
| aws | >= 4.0.0 |
## Providers
| Name | Version |
|------|---------|
| aws | >= 4.0.0 |
## Resources
| Name | Type |
|------|------|
| aws_vpc.main | resource |
| aws_subnet.public | resource |
| aws_subnet.private | resource |
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| vpc_cidr | CIDR block for VPC | string | n/a | yes |
| environment | Environment name | string | n/a | yes |
## Outputs
| Name | Description |
|------|-------------|
| vpc_id | ID of the VPC |
| public_subnet_ids | List of public subnet IDs |

Conclusion

This concludes our Terraform series! We’ve covered everything from the basics to advanced concepts, security practices, and best practices for managing infrastructure as code. Here’s a quick recap of what we’ve learned:

  1. Terraform fundamentals and basic concepts
  2. Resource management and state handling
  3. Functions and expressions for dynamic configurations
  4. Variables, outputs, and dependency management
  5. Modules and workspace organization
  6. Remote state and backend configuration
  7. Testing and CI/CD integration
  8. Security and best practices

Remember to always follow security best practices, maintain clean and organized code, and document your infrastructure well. Happy infrastructure coding!

Additional Resources