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
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
# terraform.tfvars (prod)
environment = "prod"
# Production sizing
frontend_task_cpu = 512
frontend_task_memory = 1024
backend_task_cpu = 1024
backend_task_memory = 2048
db_instance_class = "db.t4g.small"
db_multi_az = true
# Production safety
db_deletion_protection = true
db_skip_final_snapshot = false
enable_https = true
acm_certificate_arn = "arn:aws:acm:..."
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 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
Remote State Configuration
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:
Module Purpose Key Resources network VPC foundation VPC, subnets, IGW, route tables security Network security Security groups, ingress/egress rules vpc_endpoints Private AWS API access Interface endpoints, S3 gateway ecr Container registries ECR repositories, lifecycle policies iam IAM roles and policies Execution role, task role, policies rds PostgreSQL database RDS instance, subnet group, parameter group ecs Container orchestration ECS cluster, task definitions, services, ALB
For detailed configuration of each component, see: