Terraform AWS Infrastructure Example
This example demonstrates how to create a secure AWS infrastructure using Terraform with Cloudgeni monitoring for compliance and security best practices.What You'll Build
- VPC with public and private subnets
- Application Load Balancer with HTTPS
- ECS Fargate cluster for containers
- RDS PostgreSQL with encryption
- S3 bucket for static assets
- All resources compliant with SOC 2 and ISO 27001
Architecture Overview
Copy
┌─────────────────────────────────────────────────────────────┐
│ VPC │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Public Subnet │ │ Public Subnet │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ ALB │ │ │ │ ALB │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Private Subnet │ │ Private Subnet │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ ECS Fargate │ │ │ │ ECS Fargate │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ RDS │ │ │ │ RDS │ │ │
│ │ └─────────────┘ │ │ └─────────────┘ │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Project Structure
Copy
infrastructure/
├── main.tf # Provider and backend configuration
├── variables.tf # Input variables
├── outputs.tf # Output values
├── vpc.tf # VPC and networking
├── alb.tf # Application Load Balancer
├── ecs.tf # ECS cluster and services
├── rds.tf # RDS database
├── s3.tf # S3 bucket
├── security.tf # Security groups
├── iam.tf # IAM roles and policies
└── kms.tf # KMS keys for encryption
Configuration Files
main.tf
Copy
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "mycompany-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "terraform"
}
}
}
variables.tf
Copy
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "project_name" {
description = "Project name for resource tagging"
type = string
default = "myapp"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "db_instance_class" {
description = "RDS instance class"
type = string
default = "db.t3.medium"
}
vpc.tf
Copy
# VPC with DNS support
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
# Public subnets for ALB
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
# Cloudgeni: Avoid auto-assigning public IPs
map_public_ip_on_launch = false
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
Type = "public"
}
}
# Private subnets for ECS and RDS
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.project_name}-private-${count.index + 1}"
Type = "private"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# NAT Gateway for private subnets
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.project_name}-nat"
}
}
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.project_name}-nat-eip"
}
}
# VPC Flow Logs - Required for compliance
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flow_logs.arn
log_destination = aws_cloudwatch_log_group.flow_logs.arn
tags = {
Name = "${var.project_name}-flow-logs"
}
}
resource "aws_cloudwatch_log_group" "flow_logs" {
name = "/aws/vpc/${var.project_name}/flow-logs"
retention_in_days = 365 # SOC 2 requirement
tags = {
Name = "${var.project_name}-flow-logs"
}
}
security.tf
Copy
# ALB Security Group - Only HTTPS from internet
resource "aws_security_group" "alb" {
name = "${var.project_name}-alb-sg"
description = "Security group for Application Load Balancer"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Cloudgeni: Redirect HTTP to HTTPS, don't allow direct HTTP
ingress {
description = "HTTP redirect only"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-alb-sg"
}
}
# ECS Security Group - Only from ALB
resource "aws_security_group" "ecs" {
name = "${var.project_name}-ecs-sg"
description = "Security group for ECS tasks"
vpc_id = aws_vpc.main.id
ingress {
description = "Allow traffic from ALB"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-ecs-sg"
}
}
# RDS Security Group - Only from ECS
resource "aws_security_group" "rds" {
name = "${var.project_name}-rds-sg"
description = "Security group for RDS"
vpc_id = aws_vpc.main.id
ingress {
description = "PostgreSQL from ECS"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.ecs.id]
}
# Cloudgeni: No egress needed for RDS
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-rds-sg"
}
}
kms.tf
Copy
# KMS key for RDS encryption
resource "aws_kms_key" "rds" {
description = "KMS key for RDS encryption"
deletion_window_in_days = 30
enable_key_rotation = true # SOC 2 / ISO 27001 requirement
tags = {
Name = "${var.project_name}-rds-key"
}
}
resource "aws_kms_alias" "rds" {
name = "alias/${var.project_name}-rds"
target_key_id = aws_kms_key.rds.key_id
}
# KMS key for S3 encryption
resource "aws_kms_key" "s3" {
description = "KMS key for S3 encryption"
deletion_window_in_days = 30
enable_key_rotation = true
tags = {
Name = "${var.project_name}-s3-key"
}
}
resource "aws_kms_alias" "s3" {
name = "alias/${var.project_name}-s3"
target_key_id = aws_kms_key.s3.key_id
}
rds.tf
Copy
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet"
subnet_ids = aws_subnet.private[*].id
tags = {
Name = "${var.project_name}-db-subnet"
}
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
engine = "postgres"
engine_version = "15.4"
instance_class = var.db_instance_class
allocated_storage = 100
max_allocated_storage = 1000
storage_type = "gp3"
db_name = "myapp"
username = "admin"
password = random_password.db_password.result
# Security - Cloudgeni checks
storage_encrypted = true # CKV_AWS_16
kms_key_id = aws_kms_key.rds.arn
publicly_accessible = false # CKV_AWS_17
# High availability
multi_az = true
# Networking
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
# Backup - SOC 2 requirement
backup_retention_period = 30
backup_window = "03:00-04:00"
# Maintenance
maintenance_window = "Mon:04:00-Mon:05:00"
auto_minor_version_upgrade = true
# Deletion protection
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-db-final"
# Logging - Compliance requirement
enabled_cloudwatch_logs_exports = [
"postgresql",
"upgrade"
]
# Performance Insights
performance_insights_enabled = true
performance_insights_retention_period = 7
tags = {
Name = "${var.project_name}-db"
}
}
resource "random_password" "db_password" {
length = 32
special = true
}
# Store password in Secrets Manager
resource "aws_secretsmanager_secret" "db_password" {
name = "${var.project_name}/db-password"
description = "RDS database password"
kms_key_id = aws_kms_key.rds.arn
tags = {
Name = "${var.project_name}-db-password"
}
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db_password.result
}
s3.tf
Copy
resource "aws_s3_bucket" "assets" {
bucket = "${var.project_name}-assets-${data.aws_caller_identity.current.account_id}"
tags = {
Name = "${var.project_name}-assets"
}
}
# Block public access - CKV_AWS_19, CKV_AWS_20, CKV_AWS_21
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Enable versioning - CKV_AWS_21
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
# Server-side encryption - CKV_AWS_19
resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
bucket = aws_s3_bucket.assets.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.s3.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
# Access logging - CKV_AWS_18
resource "aws_s3_bucket_logging" "assets" {
bucket = aws_s3_bucket.assets.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "s3-access-logs/"
}
# Lifecycle rules
resource "aws_s3_bucket_lifecycle_configuration" "assets" {
bucket = aws_s3_bucket.assets.id
rule {
id = "transition-to-ia"
status = "Enabled"
transition {
days = 90
storage_class = "STANDARD_IA"
}
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "STANDARD_IA"
}
noncurrent_version_expiration {
noncurrent_days = 365
}
}
}
# Logs bucket
resource "aws_s3_bucket" "logs" {
bucket = "${var.project_name}-logs-${data.aws_caller_identity.current.account_id}"
tags = {
Name = "${var.project_name}-logs"
}
}
resource "aws_s3_bucket_public_access_block" "logs" {
bucket = aws_s3_bucket.logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Cloudgeni Integration
Scanning This Infrastructure
- Connect your repository to Cloudgeni
- Cloudgeni automatically scans on push
- Review findings in the dashboard
Expected Passing Checks
With this configuration, you should pass:| Check | Description |
|---|---|
| CKV_AWS_16 | RDS encryption enabled |
| CKV_AWS_17 | RDS not publicly accessible |
| CKV_AWS_18 | S3 access logging enabled |
| CKV_AWS_19 | S3 encryption enabled |
| CKV_AWS_20 | S3 block public ACLs |
| CKV_AWS_21 | S3 versioning enabled |
| CKV_AWS_23 | Security groups have descriptions |
| CKV_AWS_79 | VPC flow logs enabled |