Infrastructure — Detailed Reference
This document describes every resource provisioned by this Terraform project.
Overview
The project currently deploys a staging environment for the Tracker REST API (tracker-restapi) on AWS in the eu-west-2 (London) region. The public endpoint is tracker.staging.glimpse.technology.
This document reflects the staging implementation. The production account is separate from staging and already contains the existing production VPC, TimescaleDB cluster, and Valkey. The tracker production stack should create its own VPC in that same production account and connect to the existing production VPC for database access.
Terraform state is stored remotely in an S3 bucket (bucket name supplied at init time via the backend "s3" partial configuration).
The stack is organised as one environment (envs/staging/) that composes eleven reusable modules (modules/*). Every resource is tagged with Project, Environment, and ManagedBy = "terraform" plus any caller-supplied extra tags.
Architecture at a Glance
Internet
│
▼
WAF WebACL
│
▼
Application Load Balancer (public subnets, HTTPS with HTTP redirect)
│ │ │
▼ ▼ ▼
api:8000 frontend:80 admin:80 ← ECS Fargate services (private subnets)
│
┌───────────────────┤
▼ ▼
PostgreSQL 16 Valkey
+ TimescaleDB (Redis-compat cache)
+ PostGIS on same EC2 host
(single EC2 instance, private subnet)
Module Descriptions
1. KMS (modules/kms)
Creates a single Customer-Managed Key (CMK) that is used as the encryption key for every other service in the stack.
| Resource | Detail |
|---|---|
aws_kms_key |
CMK with automatic annual key rotation enabled and a 7-day deletion window. The key policy grants full access only to the AWS account root principal. |
aws_kms_alias |
Human-readable alias alias/<project>-<env> pointing to the key. |
The KMS key ARN is passed into every other module so that S3, EBS, CloudWatch Logs, ECR, Secrets Manager, and MemoryDB all encrypt data with the same project key.
2. Network (modules/network)
Builds the Virtual Private Cloud and all routing primitives.
VPC
- CIDR:
10.40.0.0/24(staging default) - DNS hostnames and DNS support both enabled
- Default security group is locked down — all ingress and egress rules removed
Subnets
The /24 is divided into four /26 blocks across two Availability Zones:
| Tier | AZ 0 | AZ 1 |
|---|---|---|
| Public | .0.0/26 |
.64.0/26 |
| Private | .128.0/26 |
.192.0/26 |
map_public_ip_on_launch = false on all subnets; no instance gets a public IP automatically.
Internet connectivity
aws_internet_gateway— attached to the VPC, used by the public route table.aws_eip+aws_nat_gateway— a single NAT Gateway in the first public subnet gives private-subnet resources outbound internet access without a public IP.- Public route table:
0.0.0.0/0 → IGW, associated with both public subnets. - Private route table:
0.0.0.0/0 → NAT GW, associated with both private subnets.
VPC Flow Logs
- All traffic (
ALL) captured to a CloudWatch log group/aws/vpc/<prefix>-flow-logs - Log retention: 365 days; logs encrypted with the project KMS key
- Dedicated IAM role grants
logs:CreateLogStream,logs:PutLogEvents,logs:DescribeLogStreamsscoped to that log group
3. Security Groups (modules/security)
Four security groups implement a strict, least-privilege network perimeter. All groups are in the project VPC.
ALB security group (edge tier)
- Inbound: TCP 80 and TCP 443 from
0.0.0.0/0 - Outbound: TCP 80 to the ECS security group (frontend/admin containers) and TCP 8000 to the ECS security group (API container)
ECS security group (application tier)
- Inbound: TCP 80 and TCP 8000 from the ALB security group only
- Outbound: TCP 5432 to the database security group; TCP 6379 to the MemoryDB/Valkey security group
Database security group (data tier)
- Inbound: TCP 5432 (PostgreSQL) from the ECS security group only
- Outbound: none
MemoryDB/Cache security group (data tier)
- Inbound: TCP 6379 (Valkey) from the ECS security group only
- Outbound: none
4. ECR (modules/ecr)
Creates four Elastic Container Registry repositories for the application images:
| Key | Repository name |
|---|---|
api |
tracker-api |
frontend |
tracker-frontend |
admin |
tracker-admin |
anisette |
tracker-anisette |
Each repository is configured with:
- Image tag immutability — existing tags cannot be overwritten
- KMS encryption using the project CMK
- Scan on push — vulnerability scanning runs automatically on every
docker push - Lifecycle policy — once more than 30 images exist (any tag status), the oldest are expired automatically
5. ACM (modules/acm)
Requests an ACM TLS certificate for tracker.staging.glimpse.technology using DNS validation.
The aws_acm_certificate_validation resource waits until all DNS CNAME records are present before marking the certificate as valid. The required CNAME values are surfaced via the acm_validation_records output so they can be added to Cloudflare (or another DNS provider) manually or via a separate DNS pipeline.
create_before_destroy = true ensures certificate renewals do not interrupt the ALB listener.
6. WAF (modules/waf)
Deploys a regional WAFv2 Web ACL attached to the ALB.
| Rule | Priority | Purpose |
|---|---|---|
AWSManagedRulesCommonRuleSet |
10 | Blocks common web exploits (SQLi, XSS, bad user-agents, etc.) |
AWSManagedRulesKnownBadInputsRuleSet |
20 | Blocks known malicious request patterns and Log4Shell/SSRF probes |
Default action is allow (traffic not matching a blocking rule passes through).
CloudWatch metrics and sampled requests are enabled for both rules. WAF logs are written to /aws/waf/<project>-<env> (365-day retention, KMS-encrypted).
7. ALB (modules/alb)
Provisions the internet-facing Application Load Balancer and all of its listeners, target groups, and access-log storage.
S3 access-log bucket
- Bucket name:
glimpse-<project>-<env>-alb-logs-<account-id> - All public access blocked
- Server-side encryption with the project KMS key
- Versioning enabled
- Lifecycle rule: expire objects after 90 days; abort incomplete multipart uploads after 7 days
- Bucket policy allows the ELB log-delivery service to write under the configured prefix
Load balancer
- Internet-facing,
applicationtype - Placed in both public subnets
- Attached to the ALB security group
drop_invalid_header_fields = true- Deletion protection enabled (prevents accidental
terraform destroyof the ALB) - WAF WebACL attached via
aws_wafv2_web_acl_association
Listeners and routing
| Listener | Port | Action |
|---|---|---|
| HTTP | 80 | Permanent 301 redirect → HTTPS:443 |
| HTTPS | 443 | Forward to frontend target group (default) |
HTTPS listener rules (evaluated in priority order):
| Priority | Path pattern | Target group |
|---|---|---|
| 10 | /api/*, /api/v1/*, /docs, /redoc, /openapi.json |
api (port 8000) |
| 20 | /admin*, /health* |
admin (port 80) |
| — | Everything else | frontend (port 80) |
Target groups
- all use
iptarget type (required for Fargate):
| Group | Port | Health-check path | Thresholds |
|---|---|---|---|
api |
8000 | /api/v1/health |
2 healthy / 2 unhealthy, 5 s timeout, 30 s interval |
frontend |
80 | / |
Same |
admin |
80 | / |
Same |
8. Database (modules/database)
Provisions a single EC2 instance running both PostgreSQL 16 and Valkey inside a private subnet. This is the data tier for staging; no managed RDS or ElastiCache is used at this tier.
EC2 instance configuration
- AMI: latest Debian 12 (amd64 HVM, EBS-backed) from the official Debian AWS account (
136693071363) - Instance type:
t3.medium(default) - Placed in a single private subnet with no public IP
- Root volume: 50 GiB gp3, KMS-encrypted, deleted on termination
- IMDSv2 enforced (
http_tokens = "required", hop limit 2) - Detailed CloudWatch monitoring enabled
IAM role
AmazonSSMManagedInstanceCorepolicy attachment — allows AWS Systems Manager Session Manager access (no SSH bastion required)- Inline policy:
secretsmanager:GetSecretValueandsecretsmanager:DescribeSecreton the postgres and redis/valkey secrets;kms:Decryptandkms:DescribeKeyon the project CMK
Bootstrap user-data script (user-data.sh.tftpl)
Runs once on first boot:
- Installs the official PostgreSQL 16 apt repository and TimescaleDB repository
- Installs:
postgresql-16,postgresql-contrib-16,timescaledb-2-postgresql-16,postgresql-16-postgis-3 - Fetches
postgres_passwordandredis_passwordfrom Secrets Manager usingaws secretsmanager get-secret-value - Configures PostgreSQL:
listen_addresses = '*'shared_preload_libraries = 'timescaledb,pg_stat_statements'max_connections = 100,shared_buffers = 256MB,effective_cache_size = 768MBpg_hba.confrestricted toscram-sha-256auth within the VPC CIDR only- Creates the
trackerrole and thetrackerdatabase - Enables extensions:
timescaledb,postgis,postgis_topology,pg_stat_statements - Installs Valkey (falls back to redis-server if the Valkey package is unavailable)
- Configures Valkey: binds
0.0.0.0, password authentication, AOF persistence enabled
9. ECS (modules/ecs)
Creates an ECS Fargate cluster and runs three container services.
Cluster
- Container Insights enabled (enhanced CloudWatch metrics)
IAM roles
- Execution role — used by the ECS agent to pull images from ECR and fetch secrets from Secrets Manager. Has
AmazonECSTaskExecutionRolePolicyplus an inline policy granting KMS decrypt for the project CMK. - Task role — attached to running containers (currently grants no extra permissions; extend as needed).
Services deployed
| Service | Image | CPU | Memory | Port |
|---|---|---|---|---|
api |
tracker-api:<git-sha> from ECR |
512 | 1024 MB | 8000 |
frontend |
tracker-frontend:<git-sha> from ECR |
256 | 512 MB | 80 |
admin |
tracker-admin:<git-sha> from ECR |
256 | 512 MB | 80 |
services |
tracker-services:<git-sha> from ECR |
256 | 512 MB | - |
anisette |
tracker-anisette:<git-sha> from ECR |
256 | 512 MB | 6969 |
The api, frontend, and admin services run on Fargate, in private subnets, with assign_public_ip = false. Each service has desired_count = 1. Rolling deployments allow 50 % minimum healthy / 200 % maximum.
The shared services image runs the TaskiQ worker processes. ECS should start separate services from the same image with different command overrides rather than building one image per worker role.
In staging, those worker services are scaffolded behind the enable_workers Terraform flag so the image and runtime shape can be validated before they are turned on. Only tracker-fetcher-2 needs the shared /data EFS mount and ANISETTE_SERVER; the other worker services remain stateless.
Anisette is also deployed on Fargate as a private-only service. It is registered in private service discovery as anisette-v3.anisette-v3.local and is reachable only from inside the VPC on port 6969.
It mounts an EFS-backed persistent volume at /data so its config/state survives task replacement.
EFS is a managed network filesystem for live runtime storage; it is not S3-backed object storage.
API container environment variables (injected at task launch)
| Variable | Value |
|---|---|
POSTGRES_SERVER |
EC2 private DNS of the database host |
POSTGRES_USER |
tracker |
POSTGRES_DB |
tracker |
POSTGRES_PORT |
5432 |
REDIS_HOST |
EC2 private DNS of the database host |
REDIS_PORT |
6379 |
REDIS_CLUSTER_MODE |
false |
REDIS_TLS_ENABLED |
false |
REDIS_CACHE_TTL |
3600 |
REDIS_CACHE_DB / TASKS_DB / HEALTH_DB / NOTIFICATIONS_DB |
0 / 1 / 2 / 3 |
ANISETTE_SERVER |
http://anisette-v3.anisette-v3.local:6969 |
Secrets injected via Secrets Manager (not stored in environment variables)
| Variable | Secret |
|---|---|
POSTGRES_PASSWORD |
postgres_password secret |
SECRET_KEY |
secret_key secret |
REDIS_PASSWORD |
redis_password secret |
Logging
— all containers use the awslogs driver, writing to the CloudWatch log groups created by the logs module.
10. CloudWatch Logs (modules/logs)
Creates one CloudWatch log group per service:
| Key | Log group path |
|---|---|
api |
/aws/ecs/<project>-<env>/api |
frontend |
/aws/ecs/<project>-<env>/frontend |
admin |
/aws/ecs/<project>-<env>/admin |
anisette |
/aws/ecs/<project>-<env>/anisette |
All groups are KMS-encrypted with the project CMK. Retention defaults to 90 days (configurable via the retention_in_days variable).
11. Secrets Manager (modules/secrets)
Creates three Secrets Manager secrets, each encrypted with the project CMK:
| Logical name | Purpose |
|---|---|
postgres_password |
PostgreSQL tracker user password |
secret_key |
Application signing/JWT secret key |
redis_password |
Valkey authentication password |
For each secret, Terraform generates a 32-character random password (mixed case, digits, and special characters from !@#$%^&*()-_=+[]{}:,.?) and stores it as the initial secret value. Rotation is not automated (noted in a code comment as a future enhancement).
12. MemoryDB (modules/memorydb) — module present, not used in staging
The codebase includes a full MemoryDB module (Valkey-compatible managed cluster). It is not instantiated in envs/staging/main.tf; the staging data tier uses Valkey running on the database EC2 host instead. The module is available for environments that require a managed, cluster-mode cache.
Dependency Graph (simplified)
kms
├── network (flow log encryption)
├── ecr (image encryption)
├── waf (log encryption)
├── secrets (secret encryption)
├── logs (log encryption)
├── alb (S3 log bucket encryption)
├── database (EBS encryption + secret access)
└── ecs (execution role KMS decrypt)
network → security → alb → ecs
network → database
secrets → database (secret ARNs in user-data)
secrets → ecs (secret ARNs injected into containers)
acm → alb (TLS certificate)
waf → alb (WebACL association)
logs → ecs (log group names passed to task definitions)
ecr → ecs (image URLs)
alb → ecs (target group ARNs)
Key Variables (staging defaults)
| Variable | Default | Description |
|---|---|---|
project_name |
tracker-restapi |
Prefix for all resource names |
environment |
staging |
Environment label |
aws_region |
eu-west-2 |
AWS region (London) |
vpc_cidr |
10.40.0.0/24 |
VPC address space |
public_hostname |
tracker.staging.glimpse.technology |
Public DNS name |
enable_anisette |
false |
Toggle for the optional private Anisette ECS service |
secret_names |
(required) | Secrets Manager path strings for postgres, secret_key, redis |
Notable Security Properties
- No SSH access — the database host is managed exclusively through SSM Session Manager.
- IMDSv2 enforced — instance metadata requires a signed token; hop limit 2 supports containers.
- No public IPs on ECS or database — all application traffic enters via the ALB only.
- KMS encryption everywhere — EBS, ECR, CloudWatch Logs, S3, Secrets Manager all use the same project CMK with annual rotation.
- WAF in front of ALB — common exploit patterns and known bad inputs are blocked before reaching application code.
- Immutable ECR tags — once an image tag is pushed it cannot be overwritten, preventing tag-hijack attacks.
- ALB deletion protection — prevents accidental teardown of the load balancer.
- Default VPC security group locked — prevents any resource from accidentally inheriting the permissive default rules.