Skip to main content
The ADMA infrastructure is managed using Terraform with a modular architecture. This approach provides clean separation of concerns, reusability, and maintainability.

Module Architecture

The Terraform configuration is organized into specialized modules:
infrastructure/terraform/
├── main.tf                  # Root module orchestration
├── variables.tf             # Root variable definitions
├── outputs.tf              # Stack outputs
├── locals.tf               # Common tags and locals
├── terraform.tfvars        # Environment-specific values
└── modules/
    ├── network/            # VPC, subnets, route tables
    ├── security/           # Security groups and rules
    ├── vpc_endpoints/      # Interface endpoints for private subnets
    ├── ecr/                # Container registries
    ├── iam/                # IAM roles and policies
    ├── rds/                # PostgreSQL database
    └── ecs/                # ECS cluster, services, ALB

Root Module Configuration

The root main.tf orchestrates all modules and manages dependencies:
# infrastructure/terraform/main.tf

# Input validation ensures critical requirements are met
resource "terraform_data" "input_validation" {
  lifecycle {
    precondition {
      condition     = !var.enable_https || var.acm_certificate_arn != null
      error_message = "acm_certificate_arn is required when enable_https is true."
    }

    precondition {
      condition = var.create_ecr_repositories || (
        var.existing_frontend_ecr_repository_url != null &&
        var.existing_backend_ecr_repository_url != null
      )
      error_message = "Set existing_frontend_ecr_repository_url and existing_backend_ecr_repository_url when create_ecr_repositories is false."
    }
  }
}

# Network module creates VPC foundation
module "network" {
  source = "./modules/network"

  project_name          = var.project_name
  environment           = var.environment
  vpc_cidr              = var.vpc_cidr
  public_subnet_cidrs   = var.public_subnet_cidrs
  private_subnet_cidrs  = var.private_subnet_cidrs
  availability_zone_cnt = max(length(var.public_subnet_cidrs), length(var.private_subnet_cidrs))
  tags                  = local.common_tags
}

# Security module defines all security groups
module "security" {
  source = "./modules/security"

  project_name          = var.project_name
  environment           = var.environment
  vpc_id                = module.network.vpc_id
  vpc_cidr              = module.network.vpc_cidr
  alb_ingress_cidrs     = var.alb_ingress_cidrs
  frontend_port         = var.frontend_container_port
  backend_port          = var.backend_container_port
  db_port               = var.db_port
  allow_https_from_cidr = var.enable_https
  tags                  = local.common_tags
}

How to Use Modules

Module Inputs

Each module declares its required and optional inputs in variables.tf. Modules are invoked by passing these variables:
module "rds" {
  source = "./modules/rds"

  # Required inputs
  project_name            = var.project_name
  environment             = var.environment
  private_subnet_ids      = module.network.private_subnet_ids
  db_security_group_id    = module.security.db_sg_id
  
  # Database configuration
  db_name                 = var.db_name
  db_username             = var.db_username
  db_port                 = var.db_port
  db_engine_version       = var.db_engine_version
  db_instance_class       = var.db_instance_class
  
  # Storage and backup
  db_allocated_storage    = var.db_allocated_storage
  db_max_allocated_storage = var.db_max_allocated_storage
  db_backup_retention_days = var.db_backup_retention_days
  
  # High availability and safety
  db_multi_az             = var.db_multi_az
  db_deletion_protection  = var.db_deletion_protection
  db_skip_final_snapshot  = var.db_skip_final_snapshot
  
  tags                    = local.common_tags
}

Module Outputs

Modules expose outputs that can be referenced by other modules:
# From modules/network/outputs.tf
output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.this.id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

# Used in root main.tf
module "rds" {
  # ...
  private_subnet_ids = module.network.private_subnet_ids
}

Module Dependencies

Terraform automatically handles most dependencies through references. Explicit dependencies can be declared when needed:
module "ecs" {
  source = "./modules/ecs"
  
  # ... variables ...
  
  depends_on = [
    terraform_data.input_validation,
    module.vpc_endpoints,
    aws_secretsmanager_secret_version.jwt,
  ]
}

Variable Configuration

Variable Types and Validation

The stack uses comprehensive variable validation:
variable "public_subnet_cidrs" {
  description = "Public subnet CIDRs (ALB only)."
  type        = list(string)
  default     = ["10.42.0.0/24", "10.42.1.0/24"]

  validation {
    condition     = length(var.public_subnet_cidrs) >= 2
    error_message = "At least two public subnets are required."
  }
}

variable "db_instance_class" {
  description = "RDS instance class."
  type        = string
  default     = "db.t4g.micro"
}

variable "enable_https" {
  description = "If true, create an HTTPS listener on the public ALB."
  type        = bool
  default     = false
}

Configuration File

Environment-specific values are set in terraform.tfvars:
# terraform.tfvars
aws_region   = "eu-west-1"
project_name = "adma"
environment  = "prod"

# Network
vpc_cidr              = "10.42.0.0/16"
public_subnet_cidrs   = ["10.42.0.0/24", "10.42.1.0/24"]
private_subnet_cidrs  = ["10.42.10.0/24", "10.42.11.0/24"]

# Application sizing
frontend_task_cpu     = 256
frontend_task_memory  = 512
backend_task_cpu      = 512
backend_task_memory   = 1024

# Database
db_instance_class          = "db.t4g.micro"
db_allocated_storage       = 20
db_multi_az                = false
db_deletion_protection     = true

# HTTPS (optional)
enable_https          = true
acm_certificate_arn   = "arn:aws:acm:eu-west-1:123456789012:certificate/xxxxx"

# Application URLs
frontend_public_url   = "https://go.example.com"
app_base_url          = "https://go.example.com"
cors_allowed_origins  = ["https://go.example.com"]
# terraform.tfvars (dev)
environment  = "dev"

# Smaller sizing for cost savings
frontend_task_cpu    = 256
frontend_task_memory = 512
backend_task_cpu     = 256
backend_task_memory  = 512

db_instance_class    = "db.t4g.micro"
db_multi_az          = false

# Safety overrides for dev
db_deletion_protection  = false
db_skip_final_snapshot  = true

enable_https = false

Common Patterns

Local Values

The locals.tf file defines common values used across modules:
locals {
  name_prefix = "${var.project_name}-${var.environment}"
  
  common_tags = merge(var.tags, {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "Terraform"
  })
}

Conditional Resources

Modules can be conditionally created using count:
module "ecr" {
  count  = var.create_ecr_repositories ? 1 : 0
  source = "./modules/ecr"
  
  # ... configuration ...
}

# Reference conditional module outputs
locals {
  frontend_ecr_repository_url = var.create_ecr_repositories ? module.ecr[0].frontend_repository_url : var.existing_frontend_ecr_repository_url
}

Resource Naming Convention

All resources follow a consistent naming pattern:
resource "aws_vpc" "this" {
  cidr_block = var.vpc_cidr
  
  tags = merge(var.tags, {
    Name = "${local.name_prefix}-vpc"
  })
}

# Results in: adma-prod-vpc
Best Practice: Use the name_prefix local for all resource names to ensure consistency and avoid naming conflicts across environments.

Secrets Management in Terraform

Secrets are created and managed by Terraform, then injected into ECS:
# Create secret in Secrets Manager
resource "aws_secretsmanager_secret" "jwt" {
  name                    = "/${var.project_name}/${var.environment}/jwt-secret"
  description             = "JWT signing secret for ${local.name_prefix} backend"
  recovery_window_in_days = 7
}

# Generate secure random value
resource "random_password" "jwt_secret" {
  length           = 64
  special          = true
  override_special = "_-+=@#%"
}

# Store the value
resource "aws_secretsmanager_secret_version" "jwt" {
  secret_id     = aws_secretsmanager_secret.jwt.id
  secret_string = random_password.jwt_secret.result
}

# Pass ARN to ECS module
module "ecs" {
  # ...
  jwt_secret_arn = aws_secretsmanager_secret.jwt.arn
}
Security Note: RDS password is managed by AWS using manage_master_user_password = true, which automatically rotates credentials and stores them in Secrets Manager.

Deployment Workflow

Initial Deployment

# Navigate to Terraform directory
cd infrastructure/terraform

# Copy example configuration
cp terraform.tfvars.example terraform.tfvars

# Edit variables
vim terraform.tfvars

# Initialize Terraform
terraform init

# Review planned changes
terraform plan

# Apply infrastructure
terraform apply

# Save outputs for CI/CD
terraform output -json > outputs.json

Updating Infrastructure

# Make changes to .tf files or terraform.tfvars

# Preview changes
terraform plan

# Apply changes
terraform apply

# Target specific module if needed
terraform apply -target=module.ecs

State Management

For production, configure remote state in providers.tf:
terraform {
  backend "s3" {
    bucket         = "adma-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "adma-terraform-locks"
  }
}
Create the S3 bucket and DynamoDB table first:
aws s3 mb s3://adma-terraform-state --region eu-west-1
aws s3api put-bucket-versioning \
  --bucket adma-terraform-state \
  --versioning-configuration Status=Enabled

aws dynamodb create-table \
  --table-name adma-terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

Module Reference

Each module has a specific responsibility:
ModulePurposeKey Resources
networkVPC foundationVPC, subnets, IGW, route tables
securityNetwork securitySecurity groups, ingress/egress rules
vpc_endpointsPrivate AWS API accessInterface endpoints, S3 gateway
ecrContainer registriesECR repositories, lifecycle policies
iamIAM roles and policiesExecution role, task role, policies
rdsPostgreSQL databaseRDS instance, subnet group, parameter group
ecsContainer orchestrationECS cluster, task definitions, services, ALB
For detailed configuration of each component, see:

Build docs developers (and LLMs) love