Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions testenv/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Terraform working files (keep .terraform.lock.hcl tracked)
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
crash.log
47 changes: 47 additions & 0 deletions testenv/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions testenv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# testenv — free-tier private topology

A small, **fully private** topology (Terraform) that gives `reachr` real ENIs, a
three-tier security-group chain, and a VPC endpoint to design the `topology.json`
schema against and to seed the golden fixture. Not part of the shipped tool —
it's a development/test environment.

```
Custom VPC 10.20.0.0/16 (no IGW / no NAT)
└─ 2 private subnets (2 AZs)
├─ internal ALB [alb-sg] :80 ─▶ target group :8080
│ └─ 2× EC2 t2.micro [app-sg] (stands in for ECS)
├─ RDS db.t3.micro Postgres [db-sg]
└─ S3 gateway endpoint (free) → prefix-list route
SG chain: alb-sg ─:8080▶ app-sg ─:5432▶ db-sg (SG-references-SG)
```

## Cost

Designed for the **12-month AWS Free Tier**: EC2 `t2.micro`, RDS `db.t3.micro`,
and one ALB are each within the 750 hrs/month allowance; the S3 gateway endpoint
is always free. There is **no NAT gateway or interface endpoint** (the usual cost
drivers). Still — **run `terraform destroy` once you've captured the fixture.**
Two app instances share the 750-hr EC2 allowance, so don't leave it running.

## Usage

```sh
cd testenv
terraform init
terraform apply # ~10 min (RDS is the slow part)

# Scan it (from the repo root, using the scan --raw command from Slice 1):
cd ..
go run . scan --raw --region ap-southeast-2 --vpc "$(terraform -chdir=testenv output -raw vpc_id)" > raw.json

# When done:
terraform -chdir=testenv destroy
```

`terraform output scan_command` prints the exact scan command with the VPC id
filled in.

## Notes

- Apps don't serve traffic; ALB targets will show unhealthy. That's fine — reachr
scans **structure**, and v1 doesn't evaluate target health.
- The RDS master password is generated (`random_password`) and never needed; the
DB is not reachable and exists only for its network shape.
84 changes: 84 additions & 0 deletions testenv/compute.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}

# App tier: two instances (one per AZ) standing in for ECS tasks. They don't need
# to serve traffic — reachr scans structure (ENIs/SGs/target associations), and
# v1 does not evaluate target health.
resource "aws_instance" "app" {
count = 2
ami = data.aws_ami.al2023.id
instance_type = var.instance_type
subnet_id = aws_subnet.private[count.index].id
vpc_security_group_ids = [aws_security_group.app.id]
associate_public_ip_address = false
tags = { Name = "reachr-testenv-app-${count.index}" }
}

resource "aws_lb" "app" {
name = "reachr-testenv-alb"
internal = true
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.private[*].id
tags = { Name = "reachr-testenv-alb" }
}

resource "aws_lb_target_group" "app" {
name = "reachr-testenv-app"
port = var.app_port
protocol = "HTTP"
vpc_id = aws_vpc.this.id
target_type = "instance"
tags = { Name = "reachr-testenv-app" }
}

resource "aws_lb_target_group_attachment" "app" {
count = length(aws_instance.app)
target_group_arn = aws_lb_target_group.app.arn
target_id = aws_instance.app[count.index].id
port = var.app_port
}

resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.app.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}

# DB tier: RDS Postgres (db.t3.micro is RDS free-tier eligible).
resource "random_password" "db" {
length = 20
special = false
}

resource "aws_db_subnet_group" "this" {
name = "reachr-testenv"
subnet_ids = aws_subnet.private[*].id
tags = { Name = "reachr-testenv" }
}

resource "aws_db_instance" "this" {
identifier = "reachr-testenv"
engine = "postgres"
instance_class = "db.t3.micro"
allocated_storage = 20
db_name = "reachr"
username = "reachr"
password = random_password.db.result
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.db.id]
publicly_accessible = false
multi_az = false
skip_final_snapshot = true
tags = { Name = "reachr-testenv-db" }
}
44 changes: 44 additions & 0 deletions testenv/network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
data "aws_availability_zones" "available" {
state = "available"
}

locals {
azs = slice(data.aws_availability_zones.available.names, 0, 2)
}

resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "reachr-testenv" }
}

# Private subnets only — no IGW, no NAT. Fully private topology.
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = local.azs[count.index]
tags = { Name = "reachr-testenv-private-${local.azs[count.index]}" }
}

resource "aws_route_table" "private" {
vpc_id = aws_vpc.this.id
tags = { Name = "reachr-testenv-private" }
}

resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}

# Free S3 gateway endpoint: adds a prefix-list route to the private route table
# (exercises the route engine's gateway-endpoint / prefix-list path).
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.this.id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
tags = { Name = "reachr-testenv-s3" }
}
14 changes: 14 additions & 0 deletions testenv/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "vpc_id" {
description = "The test VPC id"
value = aws_vpc.this.id
}

output "scan_command" {
description = "Ready-to-run exploratory scan for this VPC (run from the repo root)"
value = "go run . scan --raw --region ${var.region} --vpc ${aws_vpc.this.id} > raw.json"
}

output "alb_dns_name" {
description = "Internal ALB DNS name"
value = aws_lb.app.dns_name
}
71 changes: 71 additions & 0 deletions testenv/security.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Three-tier SG chain: alb -> app -> db, using SG-references-SG rules (the
# rung-1 reachability engine's key case).

resource "aws_security_group" "alb" {
name = "reachr-testenv-alb"
description = "ALB tier"
vpc_id = aws_vpc.this.id
tags = { Name = "reachr-testenv-alb" }
}

resource "aws_security_group" "app" {
name = "reachr-testenv-app"
description = "App tier (stands in for ECS)"
vpc_id = aws_vpc.this.id
tags = { Name = "reachr-testenv-app" }
}

resource "aws_security_group" "db" {
name = "reachr-testenv-db"
description = "RDS tier"
vpc_id = aws_vpc.this.id
tags = { Name = "reachr-testenv-db" }
}

# ALB: HTTP in from within the VPC (no external client in a fully private env).
resource "aws_vpc_security_group_ingress_rule" "alb_http" {
security_group_id = aws_security_group.alb.id
description = "HTTP from within VPC"
cidr_ipv4 = var.vpc_cidr
from_port = 80
to_port = 80
ip_protocol = "tcp"
}

resource "aws_vpc_security_group_egress_rule" "alb_to_app" {
security_group_id = aws_security_group.alb.id
description = "To app tier"
referenced_security_group_id = aws_security_group.app.id
from_port = var.app_port
to_port = var.app_port
ip_protocol = "tcp"
}

# App: in from ALB on app_port (SG-references-SG).
resource "aws_vpc_security_group_ingress_rule" "app_from_alb" {
security_group_id = aws_security_group.app.id
description = "From ALB"
referenced_security_group_id = aws_security_group.alb.id
from_port = var.app_port
to_port = var.app_port
ip_protocol = "tcp"
}

resource "aws_vpc_security_group_egress_rule" "app_to_db" {
security_group_id = aws_security_group.app.id
description = "To DB"
referenced_security_group_id = aws_security_group.db.id
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
}

# DB: Postgres in from app (SG-references-SG).
resource "aws_vpc_security_group_ingress_rule" "db_from_app" {
security_group_id = aws_security_group.db.id
description = "Postgres from app"
referenced_security_group_id = aws_security_group.app.id
from_port = 5432
to_port = 5432
ip_protocol = "tcp"
}
29 changes: 29 additions & 0 deletions testenv/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
variable "region" {
description = "AWS region for the test environment"
type = string
default = "ap-southeast-2"
}

variable "project_tag" {
description = "Value of the project tag (used by reachr's scope selector and for teardown)"
type = string
default = "reachr-testenv"
}

variable "vpc_cidr" {
description = "CIDR block for the test VPC"
type = string
default = "10.20.0.0/16"
}

variable "instance_type" {
description = "App-tier EC2 instance type (t2.micro is free-tier in ap-southeast-2)"
type = string
default = "t2.micro"
}

variable "app_port" {
description = "Port the app tier listens on behind the ALB"
type = number
default = 8080
}
26 changes: 26 additions & 0 deletions testenv/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.5"
}
}
}

provider "aws" {
region = var.region

# Everything is tagged so reachr's scope selector has real data and teardown
# is trivial (filter/destroy by project=reachr-testenv).
default_tags {
tags = {
project = var.project_tag
managedBy = "terraform"
}
}
}