Infrastructure
This is the platform infrastructure — the AWS estate and the deploy control plane that runs it. It complements How deployment works (how a change ships), Environments (which branch reaches what), and Observability & infrastructure (the application integrations — Sentry, GA4, S3, Redis).
Everything runs in one AWS account — 907815021625, region eu-central-1 (Frankfurt). The
account is standalone: it left the outgoing vendor’s AWS Organization, and billing, authentication
and deployment no longer depend on them.
As-built as of 2026-09-06, from a read-only sweep of the account.
The estate on one diagram
Section titled “The estate on one diagram”flowchart TB
subgraph GH["GitHub · OrascomDevelopment"]
REPO[App repos]:::src
GHA[GitHub Actions]:::ci
end
subgraph AWS["AWS · account 907815021625 · eu-central-1 · standalone"]
OIDC{{OIDC · gh-deploy-prod / gh-deploy-uat<br/>no long-lived keys}}:::sec
CD[CodeDeploy · 3 applications, production only<br/>Orascom-Prod-app · ODH-Prod-Supervisor · ODH-Pdf]:::deploy
UATH[UAT host · one EC2 t4g.medium<br/>backend · workers · scheduler · CRM · PDF · MySQL · Redis as containers<br/>stack odh-uat-host]:::compute
S3REV[(S3 orascom-deploy-bucket<br/>revisions)]:::store
ECR[(ECR backend-pdf-service)]:::store
SM[Secrets Manager<br/>portals/<env>/<app> · CMK]:::sec
subgraph SHARED["Shared by prod AND UAT — same subnets, same ALB"]
ALB[Shared ALB + WAF<br/>orascom-prod-ecommerce-lb]:::compute
BEASG[Backend ASG · t4g<br/>blue/green]:::compute
CRMASG[CRM ASG · t4g]:::compute
SUP[Supervisor boxes<br/>workers · scheduler · PDF container]:::compute
end
RDSP[(RDS orascom-prod-rds<br/>MySQL 8.4.11 · db.t3.small · Multi-AZ)]:::data
REDIS[(Redis EC2)]:::data
MEDIA[(S3 media)]:::store
OBS[CloudWatch · CloudTrail · GuardDuty<br/>Access Analyzer · Teams notify Lambda]:::sec
end
AMP[AWS Amplify · 2 apps<br/>outside the VPC · not behind the ALB or WAF]:::compute
CF[CloudFront ×2<br/>prod media · UAT media<br/>no WAF on either]:::store
REPO --> GHA -->|assume via OIDC| OIDC --> CD
GHA -->|build revision| S3REV --> CD
GHA -->|buildx arm64 push| ECR --> SUP
REPO -->|autoBuild| AMP
CD -->|blue/green| BEASG & CRMASG
CD -->|in-place| SUP
ALB --> BEASG & CRMASG
BEASG & CRMASG --> RDSP
SUP --> RDSP
BEASG --> REDIS & MEDIA
CF --> MEDIA
CD -.render env.-> SM
BEASG & CRMASG & SUP -.-> OBS
classDef src fill:#e7eef6,stroke:#2563A8,color:#0b1b2b;
classDef ci fill:#efe7f7,stroke:#7A4FB5,color:#0b1b2b;
classDef deploy fill:#dff0f0,stroke:#0C7C84,color:#0b1b2b;
classDef compute fill:#f7efdc,stroke:#B7791F,color:#0b1b2b;
classDef data fill:#eceff1,stroke:#5B6B7A,color:#0b1b2b;
classDef store fill:#eceff1,stroke:#5B6B7A,color:#0b1b2b;
classDef sec fill:#f6e7dc,stroke:#C4622D,color:#0b1b2b;Compute and network
Section titled “Compute and network”- Backend & CRM run on EC2 Auto Scaling groups (AWS Graviton
t4g) behind one shared Application Load Balancer (orascom-prod-ecommerce-lb) with AWS WAF attached. - The supervisor box (
ODH-Supervisor, production) is a stable pet, not an ASG. It runs, under supervisord: the backend queue workers, the Laravel scheduler (its only home, so scheduled commands cannot duplicate across a scaled-out fleet), thepdf-service-queueworker, and the PDF service Docker container. Deploys to it are in-place. - UAT is one EC2 host (
ODH-UAT-Host,t4g.medium, CloudFormation stackodh-uat-host, since 2026-09-22) running the backend, its workers and scheduler, the CRM middleware, the PDF service, MySQL 8.4 and Redis as Docker Compose containers on an encrypted data volume, behind the shared ALB through theODH-UAT-Host-BackendandODH-UAT-Host-Middlewaretarget groups. It is switched off by an EventBridge schedule outside 08:00–20:00 Cairo, every day (UAT answers 503 then), backs its databases up nightly at 17:17 Cairo to S3odh-uat-host/backups/, and is operated through the SSM documentsUAT-Deploy,UAT-DataandUAT-Logs. The old UAT fleet (ODH-UAT-Backend-ASG,ODH-UAT-Middleware,ODH-UAT-Supervisor,ODH-UAT-Redisandodh-uat-rds) was deleted on 2026-10-01. Runbook:portals-workflows/uat-host/README.md. - Frontend = AWS Amplify — two apps, one per environment. Amplify is AWS-managed serverless hosting: it is not inside the VPC, and it is not behind the ALB or the WAF. If the frontend is down, the load balancer and target groups are not the place to look.
Security groups are clean: only orascom-prod-alb-sg has a 0.0.0.0/0 rule, and only on ports
80/443. No SSH, database or Redis port is exposed to the internet.
orascom-prod-rds |
UAT (on the host) | |
|---|---|---|
| Engine | MySQL 8.4.11 | MySQL 8.4 container |
| Class | db.t3.small (Intel) |
part of the t4g.medium host |
| Multi-AZ | Yes | No |
| Storage | 80 GB gp2 | the host’s encrypted data volume |
There are two databases, not one. Production RDS is the shared relational store for the production
backend and CRM. UAT’s MySQL runs as a container on the UAT host since 2026-09-22 (odh-uat-rds, its
former RDS counterpart, was deleted on 2026-10-01); it is backed up nightly to S3 and restore-drilled
monthly. Production automated backups and point-in-time recovery are on. Production migrations run
once per deploy under a fleet-safe GET_LOCK mutex on the relevant instance.
The 8.0 → 8.4.11 upgrade completed on 2026-08-23, evidenced by pre-upgrade snapshots captured at 8.0.45, and was rehearsed on a throwaway instance the same day before production was touched. It ended a paid Extended Support enrolment that MySQL 8.0 had triggered automatically when it left standard support.
Note that compute is Graviton but the production database is not — it is Intel db.t3.small. Only
UAT is on t4g.
A dedicated Redis EC2 instance per environment serves cache / queues / sessions where an environment routes them to Redis; the app also supports file and database drivers (see Observability & infrastructure).
Storage, CDN and registry
Section titled “Storage, CDN and registry”- S3 (media) → CloudFront — two distributions, one for production media
(
media.my.orascomdh.com) and one for UAT (media.odh-uat.my.orascomdh.com). - Amazon ECR —
backend-pdf-service— the private registry for the PDF service’s arm64 image. GitHub Actions cross-builds the image (buildx --platform linux/arm64) and pushes it here; the supervisor box pulls it during a PDF deploy. Its lifecycle policy expires untagged images — but every image carries a SHA tag, so the rule can never actually fire. - S3 —
orascom-deploy-bucket— holds the CodeDeploy revisions, env-prefixed (uat/,prod/). Astaging/prefix also survives with ~710 MB of dead revisions; no lifecycle rule removes it.
Config and secrets — two planes, not one
Section titled “Config and secrets — two planes, not one”Deploy control plane
Section titled “Deploy control plane”-
GitHub Actions builds every deploy; AWS CodeDeploy rolls it out. No long-lived AWS keys exist in GitHub — Actions assumes an OIDC role (
gh-deploy-prod/gh-deploy-uat), each trust-bound to the exactrepo:…:environment:<env>claim of the deploying repository. -
CodeDeploy applications — there are exactly three, all production (the UAT ones,
ODH-UAT,ODH-UAT-CRMand theuatgroup ofODH-Pdf, were deleted on 2026-10-01; UAT deploys to the host without CodeDeploy):App Groups Style Runs Orascom-Prod-apporascom-backend-deploy-group,orascom-crm-deploy-groupblue/green prod backend + CRM ODH-Prod-Supervisorprodin-place prod queue workers / scheduler ODH-Pdfprodin-place PDF service container swap -
The PDF service has its own rail: push →
buildxarm64 → ECR →ODH-Pdfin-place → a fail-safe container swap (pull the image, render the env, assert the shared token matches, run the new container on a temp port, run a real Chromium render smoke test, and only then swap it onto the live port with--restart=always). A bad image leaves the old container running.
Full detail on How deployment works.
Observability and audit
Section titled “Observability and audit”Seven CloudWatch alarms exist. Four cover the two production target groups (prod-backend-ALB-target-5xx,
prod-backend-ALB-unhealthy-hosts, prod-crm-ALB-target-5xx, prod-crm-ALB-unhealthy-hosts); two cover
backend CPU and memory; the seventh is a legacy low-CPU alarm. All seven notify a single SNS topic whose
name is misleading — it is named after one of the alarms rather than after its purpose.
A legacy low-CPU alarm is in a permanent alarm state, and its action is not a notification.
Oraacom-Backend-CPU-Utilization-lower-20 (note the misspelling in the name) fires below 20% CPU and has
been in ALARM continuously since 2026-09-03. Its alarm action is an Auto Scaling scale-in policy, not
an SNS topic — so it is not paging anyone, it is continuously asking production to scale in, and only the
group’s minimum size is holding capacity. It cannot be fixed by deleting it: CodeDeploy re-creates the
scaling policies and their alarms on every blue/green deployment, so the fix has to be to the
configuration it is re-created from.
A deploy-gating alarm flaps. prod-backend-ALB-unhealthy-hosts cycles into and out of alarm within
60–120 seconds, several times a week. It gates deployment rollback on both production groups, so a deploy
that starts mid-flap can roll back spuriously. A contributing factor is that the production backend
target group health-checks /admin/login — a full application route rather than a dedicated liveness
endpoint — where UAT uses /up, Laravel’s purpose-built health route. A route that renders the admin login
page exercises more of the stack than a liveness check needs to.
Coverage gaps. There are no alarms on RDS, none on the CRM’s CPU or memory, none on the supervisor boxes, none on the NAT gateway, none on any UAT resource, and no billing alarm. Production RDS has no Performance Insights. Given that the single largest cost item in this account appeared automatically on a support calendar, a billing alarm is a notable absence.
Target-group health checks differ between environments, and not in the direction you would expect:
| Target group | Path | Interval | Timeout | Unhealthy threshold |
|---|---|---|---|---|
orascom-prod-backend-target |
/up |
20 s | 10 s | 5 |
orascom-prod-crm-target |
/up |
20 s | 10 s | 5 |
ODH-UAT-Host-Backend (the host, since 2026-09-22) |
/up |
20 s | 5 s | 2 |
ODH-UAT-Host-Middleware (the host) |
/ |
30 s | 5 s | 2 |
(Values read from the target groups on 2026-10-01; the old ODH-UAT-Backend and
ODH-UAT-Middleware groups pointed at the deleted fleet.)
UAT marks the host unhealthy in 40 s (backend) or 60 s (middleware), against 100 s for either
production group — and it uses a 5-second health-check timeout where production allows 10, so a
response taking 5–10 seconds counts as a failure in UAT and passes in production. With a single host
there is nothing to eject to: an unhealthy target means UAT answers 503 until the host recovers,
which is the deliberate trade of one machine against a fleet. slow_start is 0 on all four, so a
target takes full traffic with no ramp.
CloudTrail (multi-region, own encrypted bucket), GuardDuty, and IAM Access Analyzer provide the account-level audit and threat-detection floor.
DNS and certificates
Section titled “DNS and certificates”- Root
orascomdh.comis on Orascom IT’s Cloudflare.my.orascomdh.comis served from a Route 53 zone in a different Orascom AWS account — this account holds zero hosted zones. - ACM provides the TLS certificates for the
*.my.orascomdh.comfamily (ALB and CloudFront). The production media certificatemedia.my.orascomdh.comexpires 2026-10-29 and auto-renews via DNS validation; the validation record was confirmed passing on 2026-09-06. No action is currently required — but this is worth re-checking rather than assuming, because the renewal is automatic only for as long as that record stays resolvable, and the zone is not in this account. - Certificate hygiene: the account also holds several certificates that timed out validation and two that have expired outright, all unused.
What is retired
Section titled “What is retired”- Staging — decommissioned 2026-08-16. The instance was terminated, the CodeDeploy applications deleted, and the Amplify branch removed. See Environments.
- AWS CodePipeline — the old production pipelines were kept, disconnected, as a manual rollback path for about four weeks after the July cutover, then deleted on 2026-08-16 along with their artifact bucket. Only orphaned IAM roles remain. There is no pipeline to fall back to.
- GitLab — no runtime role. The one repository never migrated is
crm-mock, whose.gitlab-ci.ymlis inert (no GitLab, no runner, no staging box). - Vendor-hosted tooling (SonarQube, Dependency-Track, self-hosted Sentry, Grafana, Mattermost) — replaced by GitHub-native checks, CloudWatch, and Orascom’s own Sentry.
Debris still present and worth clearing rather than documenting forever: a retained Elastic IP from
the staging box (deliberately kept — stale DNS records still point at it, some of them in a zone the
outgoing vendor controls); an orphan Amplify domain association with no branch behind it; the IAM roles
odh_staging_role and gh-deploy-staging — the latter still carrying a live GitHub OIDC trust; an
empty staging log group; the staging/ revision prefix; staging entries in the deploy scripts’ group
map; and portals/staging/* secrets scheduled for deletion on 2026-09-21.
