Proof of concept for centralising council APIs under a single custom domain using cross-account AWS architecture.
For the complete case study with detailed technical analysis, see CASE_STUDY.md
Pattern: Account A hosts API Gateway with a custom domain → Account B (or C, D...) hosts Lambda functions. New services are added without DNS changes.
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#386193', 'primaryTextColor': '#fff', 'primaryBorderColor': '#5a8cc8', 'lineColor': '#5a8cc8', 'secondaryColor': '#1a1a2e', 'tertiaryColor': '#16213e', 'background': '#0d1117', 'mainBkg': '#0d1117', 'nodeBorder': '#5a8cc8'}, 'layout': 'elk'}}%%
flowchart TB
Client(["External Client"])
subgraph AccountA["Account A — API Gateway"]
direction TB
Domain["api.council.gov.uk<br/>Custom Domain + ACM"]
APIGW["API Gateway<br/>REST API"]
Role["APIGatewayExecutionRole<br/>Cross-Account IAM"]
end
subgraph AccountB["Account B — Halo ITSM"]
direction TB
LambdaB["Lambda Function<br/>.NET 8 Container"]
PolicyB["Resource Policy<br/>Validates source"]
end
subgraph AccountC["Account C — Future Service"]
direction TB
LambdaC["Lambda Function"]
PolicyC["Resource Policy<br/>Validates source"]
end
Client -->|"HTTPS"| Domain
Domain -->|"Route"| APIGW
APIGW -->|"AssumeRole"| Role
Role -->|"Invoke /halo-itsm/*"| LambdaB
Role -->|"Invoke /future/*"| LambdaC
LambdaB -.->|"Validates"| PolicyB
LambdaC -.->|"Validates"| PolicyC
style AccountA fill:#1a1a2e,stroke:#8b5cf6,stroke-width:3px,color:#fff
style AccountB fill:#1a1a2e,stroke:#2ecc71,stroke-width:2px,color:#fff
style AccountC fill:#1a1a2e,stroke:#f39c12,stroke-width:2px,stroke-dasharray:5 5,color:#fff
style Client fill:#386193,stroke:#5a8cc8,stroke-width:2px,color:#fff
style Domain fill:#2c3e50,stroke:#8b5cf6,stroke-width:2px,color:#fff
style APIGW fill:#2c3e50,stroke:#8b5cf6,stroke-width:2px,color:#fff
style Role fill:#2c3e50,stroke:#f39c12,stroke-width:2px,color:#fff
style LambdaB fill:#2c3e50,stroke:#2ecc71,stroke-width:2px,color:#fff
style PolicyB fill:#16213e,stroke:#2ecc71,stroke-width:1px,color:#aaa
style LambdaC fill:#2c3e50,stroke:#f39c12,stroke-width:2px,stroke-dasharray:5 5,color:#fff
style PolicyC fill:#16213e,stroke:#f39c12,stroke-width:1px,stroke-dasharray:5 5,color:#aaa
linkStyle 0 stroke:#3498db,stroke-width:2px
linkStyle 1 stroke:#8b5cf6,stroke-width:2px
linkStyle 2 stroke:#f39c12,stroke-width:2px
linkStyle 3 stroke:#2ecc71,stroke-width:2px
linkStyle 4 stroke:#f39c12,stroke-width:2px,stroke-dasharray:5 5
linkStyle 5 stroke:#2ecc71,stroke-width:1px,stroke-dasharray:3 3
linkStyle 6 stroke:#f39c12,stroke-width:1px,stroke-dasharray:3 3
- Client calls
https://api.council.gov.uk/service-name/endpoint - DNS resolves to API Gateway in Account A
- API Gateway assumes
APIGatewayExecutionRole - Role invokes Lambda in target account
- Lambda resource policy validates source account and API Gateway ARN
- Response flows back through API Gateway
- Cross-account IAM role with least privilege (only specific Lambda ARNs)
- Lambda resource policy validates both source account AND API Gateway ARN
- CloudWatch logging enabled for full audit trail
- No public Lambda access — invocation only possible via Account A's gateway
- Add new route to API Gateway (e.g.,
/planning/*) - Deploy Lambda in target account
- Add Lambda resource policy allowing Account A
- Update IAM role with new Lambda ARN
- No DNS changes needed
- AWS API Gateway (REST API, regional endpoint)
- AWS Lambda (target functions in separate accounts)
- AWS IAM (cross-account role assumption)
- AWS CloudFormation (Infrastructure as Code)
- Custom Domain via Route 53 + ACM certificate
| Decision | Rationale |
|---|---|
| Regional endpoint (not edge) | Lower latency for UK users |
| IAM role (not resource policy only) | Centralised permission management |
| Separate VPCs per service account | Blast radius isolation |
| CloudFormation for gateway | Reproducible, version-controlled |
├── wncapi/
│ └── account-a-apigateway.yaml # CloudFormation template
├── openapi.yaml # API specification
├── ARCHITECTURE.md # Detailed architecture docs
├── CASE_STUDY.md # Technical case study
└── README.md