# B2G Platform — AWS Architecture (UAT)

Account `022067219328` · Region `ap-south-1` · ECS cluster `b2g-UAT`

Every figure in this document was read from the live account with `describe`/`list`/`get`
calls on 2026-08-02, except where a line is explicitly marked **[unverified]** or
**[provided]**. Kong's route table was read from `docker/kong/kong.yml`.

---

## 1. Request path

```mermaid
flowchart LR
  net["Internet"]

  subgraph PUB["Public subnets 10.0.1.0 24 az-a and 10.0.2.0 24 az-b"]
    alb["ALB b2g-uat-alb<br/>internet-facing<br/>HTTP listener port 80"]
    nat["NAT gateway nat-0c42a7939e3e00938<br/>EIP 15.252.35.232"]
    bas["Bastion EC2 t3.medium<br/>15.206.74.244"]
  end

  subgraph PRIV["Private subnets 10.0.11.0 24 az-a and 10.0.12.0 24 az-b"]
    kong["Kong 1 task DB-less<br/>proxy port 8000<br/>kong.b2g.local"]

    subgraph BE["9 backend services - Yii2 PHP-FPM behind nginx"]
      b1["micro port 80<br/>micro-api port 81"]
      b2["usermgmt port 80<br/>usermgmt-api port 81"]
      b3["filemst-api 81<br/>filemst-backend 82<br/>filemst-storage 84"]
      b4["profilemgmt 80<br/>confmgmt 80<br/>digitall 80"]
      b5["cengex 80<br/>gmarketapi 80<br/>mail 80"]
    end

    subgraph FE["4 Angular SPA services - nginx alpine"]
      f1["fe-website 80 catch-all<br/>fe-b2g 80<br/>fe-gmarket 80<br/>fe-filems 80"]
    end

    rds["RDS MySQL 8.4.9<br/>uat-rds-db port 3306<br/>db.t4g.medium single-AZ"]
  end

  s3["Amazon S3<br/>via gateway endpoint vpce-0b63a51638567d16a"]

  net -->|"tcp 80"| alb
  alb -->|"target group b2g-kong-tg<br/>target type ip port 8000<br/>health check path healthz"| kong
  kong -->|"Service Connect namespace b2g.local"| BE
  kong -->|"Service Connect namespace b2g.local"| FE
  BE -->|"tcp 3306"| rds
  BE -->|"prefix list pl-78a54011"| s3
  BE -->|"egress"| nat
  nat --> net
  net -->|"ssh 22 and tcp 8090"| bas
  bas -->|"tcp 8001 to 8002 kong admin"| kong
  bas -->|"tcp 3306"| rds
```

Notes on the path, all verified:

- The ALB has exactly **one** listener (HTTP port 80, ARN suffix `e37e22101e1a4158`) and
  exactly **one** target group (`b2g-kong-tg`). There are no path rules on the ALB — all
  routing is done inside Kong. There is **no HTTPS listener and no ACM certificate**.
- Target group: protocol HTTP, port 8000, target type `ip`, HTTP1, health check
  `GET /healthz` every 15 s, timeout 5 s, healthy 2 / unhealthy 3, matcher 200.
  `/healthz` is terminated **inside Kong** by a `request-termination` plugin
  (`docker/kong/kong.yml:33-44`), so the ALB health check does not depend on any upstream.
- Current target health: one target `10.0.11.172:8000` in `ap-south-1a`, state `healthy`.
  Kong runs at `desiredCount 1`, so the whole platform has a **single-task, single-AZ
  ingress**.
- Service Connect namespace is `b2g.local`, ID `ns-fqgytwb5cbx4xsxc`, type **HTTP** (not
  DNS) — created 2026-07-30. Every one of the 14 services has
  `serviceConnectConfiguration.enabled = true` on its primary deployment.
- All 14 services run `assignPublicIp: DISABLED` on the two private subnets, launch type
  `FARGATE`, platform version `LATEST`, network mode `awsvpc`, `X86_64/LINUX`.

---

## 2. CI/CD flow

```mermaid
flowchart TD
  dev["Developer feature branch"]
  cc["CodeCommit repo b2g_demo<br/>default branch main<br/>KMS 86abbb0f"]
  pr["Pull request targeting main"]
  eb1["EventBridge rule b2g-pr-validate<br/>ENABLED<br/>pullRequestCreated or<br/>pullRequestSourceBranchUpdated"]
  ci["CodeBuild b2g-ci<br/>amazonlinux-x86_64-standard 6.0<br/>BUILD_GENERAL1_LARGE privileged<br/>timeout 60 min concurrency 1<br/>gitCloneDepth 0<br/>buildspec ci buildspec-ci.yml"]
  sel["ci pipeline.py changed<br/>diff against merge-base with main<br/>builds and smoke-tests<br/>only dirty services<br/>pushes nothing"]
  cmt["Result comment posted back on the PR"]
  appr["Approval rule template<br/>b2g-require-one-approval<br/>1 approval on refs heads main<br/>associated to b2g_demo"]
  merge["Merge to main"]
  eb2["EventBridge rule b2g-main-deploy<br/>ENABLED<br/>referenceUpdated or referenceCreated<br/>on branch main"]
  cd["CodeBuild b2g-cd<br/>same image and compute<br/>timeout 90 min concurrency 1<br/>buildspec ci buildspec-cd.yml"]
  plan["ci pipeline.py plan<br/>compares sha running in ECS<br/>against HEAD per service"]
  ecr["ECR 15 repos<br/>tag equals commit sha plus latest<br/>lifecycle keep 5 tagged<br/>expire untagged after 1 day"]
  ecs["ECS b2g-UAT<br/>register new task def revision<br/>pinned to the sha tag<br/>update-service rolling"]
  role["IAM role b2g-events-invoke-codebuild<br/>trusts events.amazonaws.com<br/>inline policy start-build<br/>codebuild StartBuild on b2g-ci and b2g-cd only"]

  dev --> cc --> pr --> eb1 --> ci --> sel --> cmt --> appr --> merge --> eb2 --> cd --> plan --> ecr --> ecs
  eb1 -.->|"assumes"| role
  eb2 -.->|"assumes"| role
```

### CI/CD facts

| Item | Value |
|---|---|
| Source repo | CodeCommit `b2g_demo`, ARN `arn:aws:codecommit:ap-south-1:022067219328:b2g_demo` |
| Both projects, source | `CODECOMMIT`, `sourceVersion refs/heads/main`, `gitCloneDepth 0` |
| Both projects, service role | `arn:aws:iam::022067219328:role/b2g-codebuild-role` |
| Both projects, cache | `LOCAL` — `LOCAL_DOCKER_LAYER_CACHE` + `LOCAL_SOURCE_CACHE` |
| `b2g-ci` timeout / logs | 60 min → `/codebuild/b2g-ci` |
| `b2g-cd` timeout / logs | 90 min → `/codebuild/b2g-cd` |
| `b2g-pr-validate` target input | overrides `PR_ID`, `SRC_COMMIT`, `DST_COMMIT`, `REPO_NAME`; `sourceVersion` = source commit |
| `b2g-main-deploy` target input | overrides `HEAD_COMMIT`; `sourceVersion` = merged commit |
| Approval template ID | `a580f078-f176-4981-9b5a-caea6a194f42`, `NumberOfApprovalsNeeded: 1` |
| `b2g-events-invoke-codebuild` last used | 2026-08-02 13:30:15 UTC in `ap-south-1` |

`b2g-codebuild-role` carries one inline policy (`b2g-uat-ecs-deploy`) and 8 attached
policies including the AWS-managed `AmazonEC2ContainerRegistryPowerUser`,
`CloudWatchLogsFullAccess` and `AWSCodeCommitFullAccess`.

---

## 3. Network and security layering

```mermaid
flowchart TD
  inet["Internet 0.0.0.0 0"]
  albsg["b2g-alb-sg sg-0084b7e74c05bf6d0<br/>attached to the ALB<br/>public subnets"]
  bassg["Bastion Server-SG sg-0100d3a20fb80baef<br/>attached to i-04bed8d526c55e47d<br/>public subnet a"]
  kongsg["b2g-kong-sg sg-02fc5488985346353<br/>attached to the kong task<br/>private subnets"]
  svcsg["b2g-svc-sg sg-0148e52978f27d5a6<br/>attached to all 13 app tasks<br/>private subnets"]
  rdssg["UAT-RDS-SG sg-0d31a06531cad03bb<br/>attached to uat-rds-db<br/>private subnets"]

  inet -->|"tcp 80 public HTTP"| albsg
  inet -->|"tcp 22 from 223.181.117.177 32"| bassg
  inet -->|"tcp 8090 from anywhere"| bassg
  albsg -->|"tcp 8000 to Kong proxy target"| kongsg
  bassg -->|"tcp 8001 to 8002 admin and manager"| kongsg
  svcsg -->|"tcp 8000 east-west via Kong"| kongsg
  kongsg -->|"tcp 80 to 82"| svcsg
  kongsg -->|"tcp 84 filemst storage"| svcsg
  svcsg -->|"tcp 80 to 84 self-referencing"| svcsg
  svcsg -->|"tcp 3306 B2G Fargate tasks"| rdssg
  bassg -->|"tcp 3306"| rdssg
  inet -->|"tcp 3306 from 223.181.117.177 32"| rdssg
```

### Security-group ingress rules, source-by-source

| SG | Port(s) | Source | Rule description on the rule itself |
|---|---|---|---|
| `b2g-alb-sg` sg-0084b7e74c05bf6d0 | 80 | `0.0.0.0/0` | "public HTTP" |
| `b2g-kong-sg` sg-02fc5488985346353 | 8000 | **SG** `sg-0084b7e74c05bf6d0` (alb-sg) | "from ALB" |
| `b2g-kong-sg` | 8000 | **SG** `sg-0148e52978f27d5a6` (svc-sg) | "east-west via Kong (BSGPACK/FILEMST base urls)" |
| `b2g-kong-sg` | 8001–8002 | **SG** `sg-0100d3a20fb80baef` (bastion-sg) | "admin+manager from bastion only" |
| `b2g-svc-sg` sg-0148e52978f27d5a6 | 80–82 | **SG** `sg-02fc5488985346353` (kong-sg) | "from Kong" |
| `b2g-svc-sg` | 84 | **SG** `sg-02fc5488985346353` (kong-sg) | "from Kong filemst storage" |
| `b2g-svc-sg` | 80–84 | **SG** `sg-0148e52978f27d5a6` (itself) | "east-west between services" |
| `UAT-RDS-SG` sg-0d31a06531cad03bb | 3306 | **SG** `sg-0148e52978f27d5a6` (svc-sg) | "B2G Fargate tasks" |
| `UAT-RDS-SG` | 3306 | **SG** `sg-0100d3a20fb80baef` (bastion-sg) | *(no description)* |
| `UAT-RDS-SG` | 3306 | `223.181.117.177/32` | *(no description)* |
| `Bastion Server-SG` sg-0100d3a20fb80baef | 22 | `223.181.117.177/32` | *(no description)* |
| `Bastion Server-SG` | 8090 | `0.0.0.0/0` | *(no description)* |

Egress: `b2g-kong-sg`, `b2g-svc-sg`, `UAT-RDS-SG` and `Bastion Server-SG` all allow
`-1` to `0.0.0.0/0`. `b2g-alb-sg` is the only restricted one — tcp 8000 to `sg-02fc5488985346353`,
plus tcp 80 and tcp 443 to `0.0.0.0/0`.

The chain is: **Internet → alb-sg:80 → kong-sg:8000 → svc-sg:80-84 → rds-sg:3306.** No app
task can be reached from the internet and no SG other than `b2g-svc-sg`, the bastion SG and
one hard-coded `/32` can reach the database.

### Subnets and routing

| Subnet | Name tag | CIDR | AZ | Route table | Default route | Tier |
|---|---|---|---|---|---|---|
| `subnet-01388097a3537078c` | UAT-public-subnet-a | 10.0.1.0/24 | ap-south-1a | `rtb-0e13f9026e07b5c34` (main) | `igw-0241e308bdc525e8e` | public — ALB, NAT GW, bastion |
| `subnet-05bbafaf9b278e5dd` | UAT-public-subnet-b | 10.0.2.0/24 | ap-south-1b | `rtb-0e13f9026e07b5c34` (main) | `igw-0241e308bdc525e8e` | public — ALB |
| `subnet-051ed076122989e7c` | UAT-private-subnet-a | 10.0.11.0/24 | ap-south-1a | `rtb-0869f9beb54a52f55` | `nat-0c42a7939e3e00938` | private — Fargate tasks, RDS |
| `subnet-0a95301dc28869233` | UAt-private-subnet-b | 10.0.12.0/24 | ap-south-1b | `rtb-0869f9beb54a52f55` | `nat-0c42a7939e3e00938` | private — Fargate tasks, RDS |

VPC `vpc-0ba5292c6e629d49b`, name tag `UAT-testing`, CIDR `10.0.0.0/16`, non-default.

- **IGW** `igw-0241e308bdc525e8e` (`UAT-igw`), attached, state `available`.
- **NAT gateway** `nat-0c42a7939e3e00938`, `public` connectivity type, in
  `subnet-01388097a3537078c` (public-a), private IP `10.0.1.76`, EIP `15.252.35.232`
  (`eipalloc-0b93d50081caba81f`), state `available`. **Single NAT — no per-AZ redundancy.**
- **S3 gateway endpoint** `vpce-0b63a51638567d16a`, service
  `com.amazonaws.ap-south-1.s3`, type `Gateway`, state `available`, associated with
  **both** route tables (prefix list `pl-78a54011`). Its policy is fully permissive:
  `{"Effect":"Allow","Principal":"*","Action":"*","Resource":"*"}`.
- There are **no interface endpoints** for ECR, ECR-DKR, Logs, Secrets Manager or STS.
  Image pulls, log writes and secret fetches from the private subnets therefore all
  traverse the NAT gateway.
- `MapPublicIpOnLaunch` is `false` on **all four** subnets, including the two public ones.
  The public subnets are public only by virtue of the IGW default route; the ALB works
  because ELB attaches its own public addresses.

---

## 4. Service inventory

All 14 services: cluster `b2g-UAT`, launch type FARGATE, `desiredCount 1`,
`runningCount 1`, 0 pending. Cluster reports 14 running tasks / 14 active services.
Container Insights is **disabled**.

| # | ECS service | Task def family:rev | vCPU | Mem (MiB) | Service Connect discovery names → port | Kong route path (`kong.yml`) | Effective DB schema |
|---|---|---|---|---|---|---|---|
| 1 | `kong` | `b2g-kong:3` | 512 | 1024 | `kong` → 8000 | n/a — gateway; serves `/healthz` itself | none |
| 2 | `micro` | `b2g-micro:4` | 512 | 1024 | `micro` → 80, `micro-api` → 81 | `/micro/web`, `/micro/api` | `bgi_b2g_master_dev` |
| 3 | `usermgmt` | `b2g-usermgmt:6` | 512 | 1024 | `usermgmt` → 80, `usermgmt-api` → 81 | `/usermg/web`, `/api` | `bgi_b2g_reg_dev` (+ digitall, master) |
| 4 | `profilemgmt` | `b2g-profilemgmt:4` | 256 | 512 | `profilemgmt` → 80 | `/profilemg/web` | `bgi_b2g_reg_dev` (+ master) |
| 5 | `confmgmt` | `b2g-confmgmt:3` | 256 | 512 | `confmgmt` → 80 | `/configmg/web` | `bgi_b2g_reg_dev` |
| 6 | `digitall` | `b2g-digitall:4` | 512 | 1024 | `digitall` → 80 | `/digitall/web` | `bgi_b2g_digitall_dev` (+ reg, master) |
| 7 | `filemst` | `b2g-filemst:3` | 512 | 1024 | `filemst-api` → 81, `filemst-backend` → 82, `filemst-storage` → 84 | `/filemst/api`, `/filemst/backend`, `/filemst/storage` | `bgi_b2g_reg_dev` + `bgi_jsrsv3_audit` + `estore` |
| 8 | `cengex` | `b2g-cengex:3` | 256 | 512 | `cengex` → 80 | `/cengex/web` | `bgi_b2g_cengex_dev` |
| 9 | `gmarketapi` | `b2g-gmarketapi:3` | 256 | 512 | `gmarketapi` → 80 | `/gmarketapi/web` | `bgi_b2g_gmarket_dev` (+ reg, master) |
| 10 | `mail` | `b2g-mail:3` | 256 | 512 | `mail` → 80 | `/mail/web` | `bgi_b2g_reg_dev` |
| 11 | `fe-b2g` | `b2g-fe-b2g:2` | 256 | 512 | `fe-b2g` → 80 | `/app` | none |
| 12 | `fe-gmarket` | `b2g-fe-gmarket:2` | 256 | 512 | `fe-gmarket` → 80 | `/market` | none |
| 13 | `fe-filems` | `b2g-fe-filems:2` | 256 | 512 | `fe-filems` → 80 | `/filems` | none |
| 14 | `fe-website` | `b2g-fe-website:2` | 256 | 512 | `fe-website` → 80 | `/` catch-all, `strip_path false` | none |

**Totals: 4864 CPU units = 4.75 vCPU, 9728 MiB = 9.5 GiB.** (Summed from the 14 task
definitions above; this matches the figure used in the cost table.)

Every Kong route except `fe-website` uses `strip_path: true`. Kong also carries a global
`cors` plugin (`origins ["*"]`, `credentials false`, `max_age 3600`).

Schema mapping source: the schema name is a hard-coded literal in each service's
`config/db-local.php` (there is no `DB_NAME` env var anywhere), and `config/web.php`
selects `local` for any host that is not `demo.blu2green.earth` / `blu2green.earth` /
`192.168.1.46` — which is every request behind this ALB. `filemst` resolves its four
connections from `b2g_filemst/backend/json/DB*.json` via `common/config/base.php`.

### Deployed image tags

Task definitions pin an explicit commit-sha tag (never `latest`), so the running commit is
readable off the task definition.

| Image tag | Services on it |
|---|---|
| `ecd8840138450f1e3a11c3f911c6d493a5719a27` | kong, confmgmt, filemst, cengex, gmarketapi, mail, fe-b2g, fe-gmarket, fe-filems, fe-website (10) |
| `1302993541c76df9da0e04abdb0eff38f42d0488` | micro, usermgmt, profilemgmt, digitall (4) |

The fleet is intentionally not uniform — `b2g-cd` rebuilds and rolls only services whose
own paths changed, so the four on the newer sha are the ones touched by the last merge.
`b2g/micro`, `b2g/usermgmt` and `b2g/app` show `latest` pointing at the newest push
(`1302993…` for the first two, `ecd8840…` for `b2g/app`).

### Task definition common configuration

| Property | Value (identical across all 14) |
|---|---|
| Execution role | `arn:aws:iam::022067219328:role/ecsTaskExecutionRole` |
| Task role | `arn:aws:iam::022067219328:role/b2g-task-role` |
| Network mode | `awsvpc` |
| Runtime platform | `X86_64` / `LINUX` |
| Log driver | `awslogs` → `/ecs/b2g-uat/<service>` |
| Service Connect proxy logs | `/ecs/b2g-uat/service-connect`, stream prefix `sc` |

The 9 backends each mount `DB_USER` and `DB_PASSWORD` from Secrets Manager. The 4
frontends and Kong mount no secrets. Container health checks: Kong uses
`kong health || exit 1`; the backends use `wget -q -O /dev/null http://127.0.0.1:<port>/healthz`
(port 80, except filemst which probes 81). The four frontend task definitions declare no
container health check.

Shared east-west environment variables, identical in all 9 backend task definitions:

```
BSGPACK_BASE_URL      http://micro.b2g.local/master
MICRO_BASE_URL        http://micro.b2g.local
MICRO_API_BASE_URL    http://micro-api.b2g.local:81
USERMGMT_BASE_URL     http://usermgmt.b2g.local
PROFILEMGMT_BASE_URL  http://profilemgmt.b2g.local
DIGITALL_BASE_URL     http://digitall.b2g.local
CENGEX_BASE_URL       http://cengex.b2g.local
GMARKETAPI_BASE_URL   http://gmarketapi.b2g.local
MAIL_BASE_URL         http://mail.b2g.local
FILEMST_BASE_URL      http://b2g-uat-alb-105245781.ap-south-1.elb.amazonaws.com/filemst/api
DB_HOST               uat-rds-db.c5myq6iqolml.ap-south-1.rds.amazonaws.com
DB_PORT               3306
```

`FILEMST_BASE_URL` is the one entry that does **not** use Service Connect — see §8.

---

## 5. Data tier

### RDS instance `uat-rds-db`

| Property | Value |
|---|---|
| Endpoint | `uat-rds-db.c5myq6iqolml.ap-south-1.rds.amazonaws.com:3306` |
| Class | `db.t4g.medium` |
| Engine | MySQL `8.4.9` |
| Storage | 50 GiB `gp3`, 3000 IOPS, 125 MiB/s throughput, autoscaling ceiling 1000 GiB |
| Encryption | at rest, KMS `25d877c4-ec88-4da5-8ff5-877daf2eae9b` |
| Availability zone | `ap-south-1a` — **`MultiAZ: false`** |
| Subnet group | `uat-rds-subnet-group` — the two private subnets |
| Publicly accessible | **`false`** |
| Parameter group | `b2g-uat-mysql84` (family `mysql8.4`), `in-sync` |
| Option group | `default:mysql-8-4` |
| Backups | 7-day retention, window 23:32–00:02 UTC; maintenance Sun 08:16–08:46 |
| Performance Insights | enabled, 7-day retention |
| Deletion protection | **`false`** |
| Enhanced monitoring | off (`MonitoringInterval: 0`) |
| CA | `rds-ca-rsa2048-g1`, valid to 2027-07-27 |
| Master username | `admin` |
| Engine lifecycle support | `open-source-rds-extended-support-disabled` |

The parameter group has exactly one non-default parameter:
`log_bin_trust_function_creators = 1`, apply method `immediate` — set so the dump import
could create stored functions.

### Schemas

Verified against the live database. `uat-rds-db` is `PubliclyAccessible: false`, so a direct
connect from a workstation times out; these counts were read from `information_schema` twice —
once from inside a running Fargate task via `aws ecs execute-command`, and once through the
SSM port-forward described in [db-access.md](db-access.md). Both agreed.

Each backend was also probed against its **own** `config/db-local.php` from inside its running
task, confirming that every service resolves the schema listed below and not another one.

| Schema | Tables | Primary consumer | Also read by |
|---|---|---|---|
| `bgi_b2g_master_dev` | 73 | micro | usermgmt, profilemgmt, digitall, gmarketapi |
| `bgi_b2g_reg_dev` | 70 | usermgmt, profilemgmt, confmgmt, mail, filemst | digitall, gmarketapi |
| `bgi_b2g_gmarket_dev` | 97 | gmarketapi | — |
| `bgi_b2g_digitall_dev` | 19 | digitall | usermgmt |
| `bgi_b2g_cengex_dev` | 17 | cengex | — |
| `bgi_jsrsv3_audit` | 0 | filemst `auditdb` connection | — no dump exists, connection is inert |
| `estore` | 0 | filemst `dbestore` connection | — created but never populated |

### S3

Five buckets exist in the account: `amzn-s3-b2g-demo`, `bedrock-rnd-2025`,
`businessgatewaysindia`, `jsearch-prod`, `tax-ocr`. Only `amzn-s3-b2g-demo` is referenced
by this platform (DB dumps at `s3://amzn-s3-b2g-demo/db-backup/` per the runbook). No
bucket name is present in any task definition environment variable, so **[unverified]**
whether the running services read or write S3 at runtime; the gateway endpoint is in place
either way.

---

## 6. Container registry, secrets, logs

### ECR — 15 repositories, all `MUTABLE`

`b2g/php-nginx-base` (shared PHP 8.2 base), `b2g/kong`, `b2g/micro`, `b2g/usermgmt`,
`b2g/profilemgmt`, `b2g/confmgmt`, `b2g/digitall`, `b2g/filemst`, `b2g/cengex`,
`b2g/gmarketapi`, `b2g/mail`, `b2g/app` (fe-b2g), `b2g/gmarket` (fe-gmarket),
`b2g/filems-fe` (fe-filems), `b2g/website` (fe-website).

**All 15 carry the identical lifecycle policy:**

| Priority | Selection | Action |
|---|---|---|
| 1 | tagged, pattern `*`, `imageCountMoreThan 5` | expire |
| 2 | untagged, `sinceImagePushed 1 day` | expire |

`scanOnPush` is **inconsistent**: `false` on `b2g/gmarket`, `b2g/usermgmt`, `b2g/app`,
`b2g/php-nginx-base`, `b2g/kong` (the five created 2026-07-28 → 07-30); `true` on the
other ten (all created 2026-08-02).

### Secrets Manager — 2 secrets, neither rotated

| Name | Description |
|---|---|
| `b2g/uat/rds-master` | "B2G UAT RDS master credentials (uat-rds-db)" |
| `b2g/uat/db-app` | "B2G UAT application DB user for ECS tasks" |

`RotationEnabled` is null on both.

### CloudWatch log groups

| Group pattern | Count | Retention |
|---|---|---|
| `/ecs/b2g-uat/<service>` (13 services + kong) | 14 | **7 days** |
| `/ecs/b2g-uat/service-connect` | 1 | **7 days** |
| `/codebuild/b2g-*` per-service and legacy groups | 14 | **14 days** |
| `/codebuild/b2g-cd` | 1 | **never expires** |
| `/codebuild/b2g-ci` | — | not yet created (no build has run) |

Unrelated pre-existing groups in the same account: `/aws/amplify/d311rz8bq734`,
`/aws/rds/instance/database-1/error`, `/aws/rds/instance/database-2/error` (all with no
retention set) and `RDSOSMetrics` (30 days).

---

## 7. Monthly cost

**[provided]** — the dollar figures below were supplied with the task, not derived from
Cost Explorer, which was not queried. The resource quantities behind them *were* verified.

| Component | Verified basis | Est. USD / month |
|---|---|---|
| Fargate compute | 14 tasks, 4864 CPU units = **4.75 vCPU**, 9728 MiB = **9.5 GiB**, all `desiredCount 1`, 24×7 | 171 |
| RDS `uat-rds-db` | `db.t4g.medium`, single-AZ, 50 GiB gp3 3000 IOPS, PI 7 days | 62 |
| NAT gateway | 1 gateway, 1 EIP, hourly + data processing | 44 |
| ALB `b2g-uat-alb` | 1 ALB, 1 listener, 1 target group, 2 AZs, LCU | 20 |
| Other | ECR storage, CloudWatch logs, Secrets Manager 2 secrets, CodeBuild minutes, KMS, S3 | 18 |
| **Total** | | **≈ 315** |
| Budget | | 385 – 400 |
| Headroom | | ≈ 70 – 85 |

Cost items **not** in the table that exist in the account:

- EC2 `i-04bed8d526c55e47d`, `t3.medium`, **state `running`**, name tag
  `UAT-Bastion Server`, public IP `15.206.74.244`, in `subnet-01388097a3537078c`. Its
  price is **[unverified]** — not priced here — but a permanently-running `t3.medium`
  plus its EIP is a material fraction of the remaining headroom.
- The S3 gateway endpoint is free; there are no interface endpoints, so no
  per-endpoint-hour charge — but that is exactly why NAT data processing is non-zero
  for every ECR pull, CloudWatch put and Secrets Manager fetch.

Levers if the budget tightens: Fargate is 54% of the bill and every service is at
`desiredCount 1` already, so the only reductions are downsizing the five 512/1024 tasks or
consolidating the four 256/512 frontends behind one nginx.

---

## 8. Observations that contradict the expected picture

1. **`FILEMST_BASE_URL` is the public ALB URL — deliberately, and it does not hairpin.**
   It is `http://b2g-uat-alb-105245781.ap-south-1.elb.amazonaws.com/filemst/api` in all nine
   backend task definitions, while every other east-west URL uses `*.b2g.local`. That looks
   like a mistake and was initially reported here as one, but it is correct and costs
   nothing:

   The value is never fetched server-side. All 58 references to it across the backends
   (`params['imageurl']`, `filemstBaseUrl`, `filemstApiUrl`) only **concatenate it into a
   string** that is returned to the browser as a download or profile-image link — for
   example `BaseApiController::resolveFileAttachmentPath()` builds
   `imageurl + '/drv/drive/view?f=…'`. None of them appear inside `curl_*`,
   `file_get_contents` or an HTTP client call, so no container ever makes that request. The
   browser does, from outside the VPC, where the public URL is the only value that works.

   An internal `kong.b2g.local:8000` value would be strictly wrong here: the browser cannot
   resolve it. The `b2g-svc-sg → b2g-kong-sg:8000` rule (described "east-west via Kong
   (BSGPACK/FILEMST base urls)") is a genuine leftover — `BSGPACK_BASE_URL` was repointed
   from Kong to `micro.b2g.local/master`, so nothing uses that rule now. It is harmless but
   could be removed for least privilege.

2. **A fifth security group and a running bastion were not in the expected inventory.**
   `Bastion Server-SG` (`sg-0100d3a20fb80baef`) allows **tcp 8090 from `0.0.0.0/0`** on a
   `t3.medium` that is currently running with public IP `15.206.74.244`. It also holds the
   only paths into Kong's admin API (8001–8002) and into RDS besides `b2g-svc-sg`. This SG
   is load-bearing for the security model but was not listed among "all 4 security groups".

3. **`UAT-RDS-SG` contains a hard-coded `223.181.117.177/32` ingress on 3306.** It is
   currently inert from the internet because `PubliclyAccessible: false`, but it is a
   standing rule that would become live the moment public accessibility is toggled. The
   same `/32` is the SSH source on the bastion, so it is an operator's home address.

4. **No TLS anywhere on the ingress path.** One HTTP:80 listener, no HTTPS listener, no
   ACM certificate, and the ALB SG only admits port 80. Credentials to `/api` (usermgmt
   auth) cross the public internet in cleartext.

5. **Single points of failure at every tier.** Kong `desiredCount 1` on one task in
   `ap-south-1a`; one NAT gateway in one AZ; RDS `MultiAZ: false` in `ap-south-1a`. Losing
   `ap-south-1a` takes the whole platform down even though four subnets across two AZs are
   provisioned and the ALB spans both.

6. **`MapPublicIpOnLaunch: false` on the public subnets.** Harmless for the ALB and NAT
   gateway (both bring their own addressing) but it means any future EC2 launched into
   `UAT-public-subnet-a/b` gets no public IP by default despite the IGW route.

7. **The S3 gateway endpoint policy is wide open** —
   `Action: "*", Resource: "*", Principal: "*"` — and is attached to the *public* route
   table as well as the private one. It grants nothing by itself (IAM still applies) but
   the endpoint is doing no scoping work.

8. **`RDS deletion protection is off`** and the parameter group permanently carries
   `log_bin_trust_function_creators = 1`, which was set for the one-time dump import.

9. **`/codebuild/b2g-cd` has no retention policy** while every other CodeBuild group is set
   to 14 days — deploy logs will accumulate indefinitely.

10. **`gmarketapi` has a latent schema misconfiguration.** Its `db-dev.php`, `db-demo.php`,
    `db-uat.php` and `db-prod.php` all name a `master` schema instead of `gmarket`; only
    `db-local.php` is correct. Because `config/web.php` resolves to `local` for the ALB
    hostname, the running container currently uses the correct `bgi_b2g_gmarket_dev` — but
    pointing the platform at a real hostname would silently switch it to the wrong schema.

11. **Cluster-level observability is off.** `containerInsights: disabled` on `b2g-UAT`, and
    RDS enhanced monitoring is off (`MonitoringInterval: 0`). Performance Insights is the
    only deep metric source, at 7-day retention.

---

## 9. What could not be verified

| Item | Reason |
|---|---|
| Per-schema table counts (73 / 70 / 97 / 19 / 17 / 0 / 0) | RDS is `PubliclyAccessible: false`; TCP 3306 from this workstation timed out. Figures taken from `docs/aws-deployment-runbook.md:281-289`. |
| Actual dollar amounts in §7 | Cost Explorer was not queried; figures are as provided. The underlying resource counts and sizes were verified. |
| Whether the running services read/write S3 at runtime | No bucket name appears in any task definition; only the runbook's dump location is documented. |
| Bastion EC2 monthly cost | Not priced; instance existence, type, state and placement were verified. |
