Arquitetura¶
Visão de alto nível da plataforma TELECOM TOWER POWER em produção.
Topologia geral¶
flowchart TB
subgraph Clients
Web[Web SPA · curl/SDK]
end
subgraph Edge["Edge / Ingress"]
R53[Route 53]
ALB[AWS ALB · sa-east-1]
Caddy[Caddy reverse-proxy<br/>EC2 EIP 18.229.14.122]
RailwayRouter[Railway router]
end
subgraph Compute
ECS[ECS Fargate<br/>prod-of-record]
EC2[EC2 Docker Compose<br/>frontend-react · grafana · prometheus]
Railway[Railway service web<br/>warm failover]
Lambda[AWS Lambda<br/>SQS batch consumer]
end
subgraph App["FastAPI app · telecom_tower_power_api.py"]
Auth[Auth · Rate-limit · Audit · Metrics]
Towers[/towers · /towers/nearest/]
Analyze[/analyze · Fresnel · LOS · RSSI/]
Plan[/plan_repeater Dijkstra/]
Predict[/coverage/predict ridge-v1/]
Batch[/batch_reports · /jobs/]
Bedrock[/bedrock chat · compare/]
end
subgraph Data
RDS[(PostgreSQL 18.3<br/>Railway prod / SQLite dev)]
Redis[(ElastiCache Redis<br/>hop cache · jobs · rate-limits)]
S3[(S3 telecom-tower-power-results<br/>models · reports · backups)]
SRTM[SRTM tiles<br/>local + Redis L2]
end
subgraph External
Bedrock2[AWS Bedrock]
Stripe[Stripe billing]
Cognito[AWS Cognito OIDC]
OpenCellID[OpenCelliD / ANATEL]
end
subgraph Observability
Prom[Prometheus<br/>13 alert rules]
Graf[Grafana]
AM[Alertmanager → Slack · PagerDuty]
Loki[Loki]
end
Web --> R53
R53 --> ALB
R53 --> Caddy
R53 --> RailwayRouter
ALB --> ECS
Caddy --> EC2
RailwayRouter --> Railway
ECS --> Auth
EC2 --> Auth
Railway --> Auth
Auth --> Towers & Analyze & Plan & Predict & Batch & Bedrock
Towers & Analyze & Plan & Predict & Batch --> RDS
Plan & Predict --> Redis
Predict --> SRTM
Predict -. boot eager refresh_from_s3 .-> S3
Batch --> S3
Batch -. enterprise tier .-> Lambda
Lambda --> S3
Lambda --> RDS
Bedrock --> Bedrock2
Auth --> Stripe
Auth --> Cognito
ECS & EC2 --> Prom
Prom --> Graf
Prom --> AM
EC2 --> Loki
classDef ext fill:#fef3c7,stroke:#d97706
class Bedrock2,Stripe,Cognito,OpenCellID ext
Pipeline de ML — terrain-aware signal predictor¶
flowchart LR
subgraph CI["Nightly CI · retrain_coverage_model.py"]
Synth[Synthetic generator<br/>n=10 000, seed=42]
Real[(Postgres link_observations<br/>real measurements via<br/>POST /coverage/observations<br/>or scripts/seed_observations.py)]
Train[train_model<br/>l2=1.0, ridge-v1<br/>real rows up-weighted 3×]
end
Synth --> Train
Real --> Train
Train -->|np.savez| NPZ[coverage_model.npz<br/>17 features · ~1.8 KB]
NPZ -->|aws s3 cp| S3M[(s3://telecom-tower-power-results/<br/>models/coverage_model.npz)]
NPZ -->|git commit| Repo[(repo baseline)]
subgraph Boot["Container boot · entrypoint.sh"]
Refresh[refresh_from_s3]
Load[CoverageModel.load<br/>np.load allow_pickle=False]
Log["log: Coverage model active:<br/>version=ridge-v1 (out-of-envelope fallback;<br/>live metrics: /coverage/model/info)"]
end
S3M --> Refresh
Refresh --> Load
Load --> Log
subgraph Serve["Request /coverage/predict"]
Feat[Build 17 features<br/>SRTM profile · log d · fresnel ratio · terrain σ]
Inf[Ridge inference<br/>w·x + bias]
Conf["confidence = clip(1 - (rmse_db - 8)/20, 0.3, 0.9)"]
end
Load -.cached.-> Inf
Feat --> Inf --> Conf
Ciclo de vida de uma requisição /coverage/predict¶
sequenceDiagram
autonumber
participant C as Client
participant R53 as Route 53
participant ALB
participant API as FastAPI<br/>(ECS task)
participant K as key_store_db<br/>(Redis cache)
participant SRTM as srtm_elevation
participant ML as CoverageModel<br/>(in-process)
participant Aud as audit_log
C->>R53: POST api.telecomtowerpower.com.br/coverage/predict
R53->>ALB: route by host
ALB->>API: HTTP/2
API->>K: verify_api_key (X-API-Key / Bearer)
K-->>API: tier=pro, owner=...
API->>API: rate-limit check (6 req/min pro)
API->>SRTM: terrain profile along link
SRTM-->>API: elevation samples
API->>ML: predict(features[17])
ML-->>API: signal_dbm, rmse_db
API->>API: confidence = clip(1 - (rmse_db-8)/20, .3, .9)
API->>Aud: row(action=predict, tenant, ts)
API-->>C: {model_source:"local-model", model_version:"ridge-v1",<br/>signal_dbm:-31.4, confidence:0.75}
Camadas¶
Verified summary (Apr 2026)¶
| Layer | Implementation |
|---|---|
| Primary API | FastAPI (Python 3.13) — api.telecomtowerpower.com.br resolves to the sa-east-1 ALB which round-robins between two targets: ECS Fargate (FastAPI direct, task-def rev 44) and an EC2 t3.small running Caddy that reverse-proxies to Railway. Local stack: 18-service Docker Compose. |
| Database | PostgreSQL 18.3 on Railway (managed) — 140,498 towers (verified from nightly dump). |
| Cache & Queue | Redis 8.6.2 (SRTM cache, hop cache, jobs, rate-limits). |
| Batch | Hybrid: ≤100 rows sync; >100 rows async via SQS → Lambda → S3. |
| AI & ML | AWS Bedrock (Claude / Titan / Llama) for chat; ridge-v1 (coverage_predict.py, 17 features). |
| Frontend | React PWA served by Nginx 1.30 + MkDocs (Material). |
| Monitoring | Prometheus v3.11.2 + Grafana 13.0.1 + Alertmanager v0.32.0 + Jaeger 1.76.0 (OTLP, head sampling 5% via ParentBased(TraceIdRatioBased(0.05)) in tracing.py; tunable via OTEL_TRACES_SAMPLER_ARG). |
| Failover | Route 53 Failover routing (PRIMARY=ALB sa-east-1, SECONDARY=Railway edge) driven by ALB health checks; Railway kept warm for api.*; drift detected by failover-drift-check.yml. |
| Backups | Nightly: Grafana volume → S3 (~23.05 MB), Railway Postgres → S3 (~1.78 MB gzip, weekly verified restore). |
| CI/CD | 19 GitHub Actions workflows (deploy, backup, drift, failover, retrain, secrets sync, …). |
| TLS | ACM on ALB (sa-east-1) terminates HTTPS; Caddy on EC2 serves :80 origin only. |
| Layer | Components | Function |
|---|---|---|
| Edge | ALB · Caddy · Railway router · Route 53 (DNS failover) | TLS termination, host routing, health checks |
| Compute | ECS Fargate (primary) · EC2 + Docker Compose · Railway · AWS Lambda (sqs_lambda_worker.py) |
API + workers + bursty batch consumer |
| Application | FastAPI (telecom_tower_power_api.py) + React SPA |
HTTP / WebSocket / SSE surfaces |
| Data | Railway PostgreSQL 18.3 · ElastiCache Redis · S3 (artifacts + backups) · SRTM cache (hop_cache.py, srtm_elevation.py) |
Persistent state, hot caches, terrain |
| ML | ridge-v1 in .npz · S3 hot-pull · nightly retrain in CI · Bedrock for scenarios |
Terrain-aware signal prediction + GenAI |
| Async | SQS priority queue · Lambda consumer · batch_worker.py · repeater_jobs_store.py (Redis) |
Long PDF batches and ≥4-hop planning |
| Auth | API keys (key_store_db.py) · Cognito OIDC + Bearer · per-tier rate limits · audit log |
OWASP-Top-10 hardening |
| Observability | Prometheus (13 rules) · Grafana · Alertmanager · OpenTelemetry · Loki | Metrics, dashboards, paging (Slack + PagerDuty) |
| CI/CD | 19 GitHub Actions workflows · BuildKit cache · secret sync via SSM · weekly restore drill | Push-to-deploy, nightly retrain, restore drill |
| Backups | Postgres + Grafana volume → S3 nightly (14d retention) · weekly verified restore | DR, RPO ≈ 24h |
🧠 Key Algorithms¶
| Feature | Implementation |
|---|---|
| Link budget | Free-space path loss + Fresnel zone + earth curvature (effective radius k=4/3). See pdf_generator.py (_free_space_path_loss, first-zone envelope, earth_bulge). |
| Repeater planning | Bottleneck-shortest-path Dijkstra (min-max) over candidate towers; relaxation new_bottleneck = max(bottleneck, effective_loss) with terrain-scored effective_loss (telecom_tower_power_api.py#L731). |
| PDF reports | ReportLab for tables/layout + Matplotlib for the terrain + Fresnel-zone plot (pdf_generator.py). |
| ML signal prediction | Ridge regression on 17 engineered features (SRTM profiles, slope, obstruction count, min Fresnel ratio, log/interaction terms). Trained on synthetic physics (_physics_signal) + log-normal shadow fading; real point-to-point measurements posted to POST /coverage/observations (or bulk-loaded via scripts/seed_observations.py / workflow seed-observations.yml) land in link_observations and are up-weighted 3× once ≥ 1 000 fresh rows accumulate (nightly retrain-coverage-model.yml). Current state: 0 real measurements ingested → model is 100 % synthetic and transitions automatically as customers submit data. Fallback chain: SageMaker endpoint → local .npz model → deterministic physics (coverage_predict.py — _FEATURE_NAMES, predict_signal). |
🗄️ Data Pipeline¶
Tower sources
- ANATEL (official) — 105,240 unique stations (Postgres prod count). Geocoded via IBGE municipality centroids + small random jitter (~800 m) so same-city towers don't stack (load_anatel.py).
- OpenCelliD (crowdsourced) — 35,248 GPS-tagged cells (load_opencellid.py).
Geocoding
- Pre-built lookup table of ~5,570 IBGE municipalities in
municipios_brasileiros.csv→ centroid + ±jitter. - Cache misses fall back to Nominatim (rate-limited to 1.1 req/s).
- ANATEL→OpenCelliD snap pass (
snap_anatel.py):
for every
ANATEL_*tower, find the closestOCID_*tower of the same operator within a configurable radius (default 5 km) using a 0.05° spatial bucket index + haversine distance; rewritelat/lonto the candidate's, keeping theid. 3×3 bucket lookup, O(N) overall. CLI:python snap_anatel.py [--max-km 5.0] [--dry-run].
SRTM elevation tiles (90 m)
- Local
.hgtfiles in./srtm_data/(L1 in-process cache). - Optional Redis L2 cache: raw
.hgtblobs, 7-day TTL (srtm_elevation.py — keysrtm:<tile>). - No Open-Elevation API fallback today; missing tile →
ValueError.
Nightly sync (AWS RDS → Railway)
- .github/workflows/sync-towers.yml,
cron
05:00 UTC. - SSM port-forward via EC2 bastion (no SG ingress) →
localhost:15432→RDS:5432; runsimport_towers.py --source-env AWS --target-env RAILWAY --delete-missing.
S3 — single source of truth¶
s3://telecom-tower-power-results/
├── models/coverage_model.npz ← ML artifact (ridge-v1, 1850 B)
├── reports/{tenant}/{job_id}.zip ← async batch outputs
├── backups/postgres/YYYY-MM-DD.sql.gz ← nightly pg_dump
└── backups/grafana/YYYY-MM-DD.tar.gz ← nightly Grafana volume snapshot