Skip to content

Commit 3f69c66

Browse files
committed
Move ECS tasks to private subnets with VPC endpoints
1 parent 99fdfb2 commit 3f69c66

10 files changed

Lines changed: 784 additions & 65 deletions

README.md

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This repository is intended to demonstrate backend/platform engineering depth, n
1818
- PostgreSQL-backed business data and audit persistence
1919
- structured runtime and PDP audit logging
2020
- Docker Compose local development workflow
21-
- AWS deployment with ECR, ECS/Fargate, ALB, RDS PostgreSQL, Secrets Manager, IAM, and CloudWatch
21+
- AWS deployment with ECR, ECS/Fargate, ALB, private ECS networking, VPC endpoints, RDS PostgreSQL, Secrets Manager, IAM, and CloudWatch
2222
- rerunnable SQL migrations
2323
- one-off ECS operational tasks for RDS migrations and dev credential seeding
2424
- local and AWS MCP smoke-test helpers
@@ -278,7 +278,11 @@ Implemented AWS infrastructure:
278278
- HTTP listener
279279
- target group registration for ECS tasks
280280
- VPC, public subnets, private app subnets, and private DB subnets
281-
- security groups for ALB, app tasks, and RDS
281+
- private app route table for private ECS task subnets
282+
- VPC endpoints for private AWS service access:
283+
- interface endpoints for ECR API, ECR Docker registry, CloudWatch Logs, and Secrets Manager
284+
- S3 gateway endpoint associated with the private app route table
285+
- security groups for ALB, app tasks, AWS service interface endpoints, and RDS
282286
- private RDS PostgreSQL instance
283287
- RDS-managed database password secret
284288
- manually-created Secrets Manager secret for `AGENT_CREDENTIAL_HASH_SECRET`
@@ -309,16 +313,21 @@ Verified AWS smoke tests:
309313
- deployed `docs_tool` denies `doc2` with `DEFAULT_DENY`
310314
- the deployed tool path resolves a DB-backed registered-agent credential through `X-Agent-Api-Key`
311315

312-
Current AWS development limitation:
316+
Current AWS networking posture:
313317

314-
- ECS app tasks currently run in public subnets with `assignPublicIp=ENABLED`.
315-
- This avoids NAT Gateway or VPC endpoints during the first runnable AWS slice.
316-
- Inbound access remains restricted through security groups:
317-
- Internet -> ALB on port `80`
318-
- ALB -> ECS app task on port `8000`
319-
- ECS app task -> RDS on port `5432`
318+
- ALB nodes remain in public subnets and provide the public HTTP entry point.
319+
- ECS/Fargate app tasks run in private app subnets with `assignPublicIp=DISABLED`.
320+
- Running app tasks have private IPs only; they are registered with the ALB target group by private task IP.
321+
- RDS PostgreSQL remains in private DB subnets.
322+
- Private app task access to required AWS services uses VPC endpoints rather than a NAT Gateway:
323+
- interface endpoints for ECR API, ECR Docker registry, CloudWatch Logs, and Secrets Manager
324+
- S3 gateway endpoint associated with the private app route table
325+
- App task security group egress is limited to required paths:
326+
- RDS PostgreSQL on port `5432`
327+
- AWS service interface endpoint security group on port `443`
328+
- S3 endpoint prefix list on port `443`
320329

321-
This is a deliberate development-stage trade-off, not the intended production networking posture.
330+
No NAT Gateway is currently deployed. That is intentional for this slice because the app does not yet need general outbound internet access to third-party APIs or arbitrary external services.
322331

323332
## Operational helper scripts
324333

@@ -512,7 +521,7 @@ It is not currently trying to be:
512521
- IdP integration
513522
- database-backed policy authoring/storage
514523
- production credential registry UI
515-
- production-grade AWS networking hardening
524+
- full production-grade AWS hardening beyond the current portfolio/dev deployment
516525

517526
The emphasis is on doing a smaller set of backend/platform concerns properly:
518527

@@ -538,8 +547,7 @@ Credible next improvements include:
538547
- extend immutable image tagging consistently across manual and Terraform-driven deployment paths
539548
- HTTPS listener with ACM certificate
540549
- optional HTTP-to-HTTPS redirect
541-
- private ECS task networking without public task IPs
542-
- NAT Gateway or VPC endpoints for outbound AWS service access
550+
- optional NAT Gateway or controlled egress path only if future app behaviour requires general external access
543551
- Terraform remote state backend
544552
- migration version tracking
545553
- production-grade registered-agent credential registration and rotation workflow
@@ -564,6 +572,8 @@ Current status:
564572
- local Docker/PostgreSQL path works
565573
- local and CI tests pass
566574
- AWS ECS/RDS/ALB deployment path works
575+
- ECS app tasks run in private app subnets with no public IP
576+
- VPC endpoints provide private AWS service access for ECR, CloudWatch Logs, Secrets Manager, and S3
567577
- AWS RDS migrations run through one-off ECS tasks
568578
- AWS dev registered-agent credential seeding/rotation works
569579
- deployed MCP allow and deny paths have been smoke-tested

docs/TRACKER.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ The supporting engineering story is backend/platform implementation depth:
2525
- Docker-based local development
2626
- SQL migrations and seed data
2727
- AWS ECS/Fargate deployment
28+
- private ECS/Fargate task networking
29+
- VPC endpoints for private AWS service access
2830
- RDS PostgreSQL runtime configuration
2931
- Secrets Manager runtime secret injection
3032
- CloudWatch log collection
@@ -215,10 +217,18 @@ Implemented AWS infrastructure includes:
215217
- private DB subnets
216218
- internet gateway
217219
- public route table
220+
- private app route table
218221
- DB subnet group
222+
- VPC endpoints for:
223+
- ECR API
224+
- ECR Docker registry
225+
- CloudWatch Logs
226+
- Secrets Manager
227+
- S3
219228
- security groups for:
220229
- ALB
221230
- ECS app task
231+
- AWS service interface endpoints
222232
- RDS PostgreSQL
223233
- private RDS PostgreSQL instance
224234
- RDS-managed database password secret
@@ -257,26 +267,27 @@ Verified AWS checks:
257267
- The deployed ECS task definition uses the Git commit SHA image tag, not `latest`.
258268
- The CD workflow waits for ECS service stability and checks `/health` after deployment.
259269

260-
Current intentional AWS development limitation:
270+
Current AWS networking posture:
261271

262-
- ECS app tasks currently run in public subnets with `assignPublicIp=ENABLED`.
263-
- This avoids adding NAT Gateway or VPC endpoints during the first runnable AWS vertical slice.
264-
- Inbound access is still controlled by security groups:
265-
- Internet -> ALB on port `80`
266-
- ALB -> ECS app task on port `8000`
267-
- ECS app task -> RDS on port `5432`
272+
- ALB nodes run in public subnets and provide the public HTTP entry point.
273+
- ECS/Fargate app tasks run in private app subnets with `assignPublicIp=DISABLED`.
274+
- Running app tasks have no public IP.
275+
- RDS PostgreSQL runs in private DB subnets.
276+
- Private app task access to required AWS services is provided by VPC endpoints:
277+
- interface endpoints for ECR API, ECR Docker registry, CloudWatch Logs, and Secrets Manager
278+
- S3 gateway endpoint associated with the private app route table
279+
- App task egress is restricted to RDS, the AWS service interface endpoint security group, and the S3 endpoint prefix list.
280+
- No NAT Gateway is currently deployed; add one later only if the app needs general outbound access to external/non-AWS services.
268281

269282
Deferred AWS hardening:
270283

271284
- HTTPS listener with ACM certificate
272285
- optional HTTP-to-HTTPS redirect
273-
- private ECS task networking without public task IPs
274-
- NAT Gateway or VPC endpoints for outbound AWS service access
275-
- immutable image tags instead of deploying `latest`
276286
- Terraform remote state backend
277287
- migration version tracking
278288
- production-grade credential registration/rotation workflow
279-
- CI/CD deployment workflow
289+
- CI-before-deploy safety clarification and deployment guardrails
290+
- Terraform image tag handling alignment with SHA-based CD
280291

281292
---
282293

@@ -366,7 +377,7 @@ The project is not currently trying to implement:
366377
- SQLAlchemy/Alembic unless direct SQL becomes a real limitation
367378
- broad AI governance platform features
368379
- production credential registry UI
369-
- production-grade AWS networking hardening
380+
- full production-grade AWS hardening beyond the current portfolio/dev deployment
370381

371382
These are deliberate scope boundaries, not forgotten requirements.
372383

@@ -384,7 +395,7 @@ Good next candidates:
384395
- keep README, tracker, and AWS deployment docs aligned with the implemented runtime
385396
- extend immutable image tagging consistently across manual and Terraform-driven deployment paths
386397
- add HTTPS/ACM support for the ALB
387-
- add private ECS task networking using NAT Gateway or VPC endpoints
398+
- add NAT Gateway or another explicit egress path only if future external API access requires it
388399
- add Terraform remote state
389400
- add migration version tracking if migration reruns become harder to reason about
390401
- formalize production-style registered-agent credential registration and rotation
@@ -410,6 +421,8 @@ The current stable implementation demonstrates:
410421
- structured runtime/audit logging
411422
- local Docker/PostgreSQL runtime
412423
- AWS ECS/Fargate/RDS/ALB runtime
424+
- private ECS task networking with no public task IP
425+
- VPC endpoint-based AWS service access without NAT Gateway
413426
- RDS-backed registered-agent identity resolution
414427
- HMAC-hashed API-key identity adapter
415428
- one-off ECS operational tasks

docs/aws_deployment_target.md

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
## Purpose
44

5-
Define the AWS runtime shape for `aws-python-service-platform` before implementing Terraform.
5+
Define the implemented AWS runtime shape for `aws-python-service-platform`.
66

77
## Target runtime path
88

99
```text
1010
Client / MCP caller
1111
-> Application Load Balancer
12-
-> ECS Fargate service
12+
-> ECS Fargate service in private app subnets
1313
-> FastAPI + FastMCP app
14-
-> RDS PostgreSQL
15-
-> CloudWatch logs
14+
-> RDS PostgreSQL in private DB subnets
15+
-> CloudWatch logs via VPC endpoint
1616
```
1717

1818
## AWS services
@@ -24,6 +24,7 @@ Client / MCP caller
2424
| Database | RDS PostgreSQL |
2525
| Secrets | Secrets Manager or SSM Parameter Store |
2626
| Logs | CloudWatch Logs |
27+
| Private AWS service access | VPC endpoints for ECR, CloudWatch Logs, Secrets Manager, and S3 |
2728
| Runtime permissions | ECS task role |
2829
| Infrastructure | Terraform |
2930

@@ -43,9 +44,9 @@ The AWS deployment should keep the same application configuration contract used
4344

4445
The application code should continue reading configuration through the existing settings module. Terraform and ECS are responsible for supplying the correct runtime values.
4546

46-
## Initial deployment scope
47+
## Implemented deployment scope
4748

48-
The first AWS deployment will run the existing service using RDS-backed configuration and CloudWatch logging.
49+
The current AWS deployment runs the existing service using RDS-backed configuration, CloudWatch logging, private ECS task networking, and VPC endpoints for required AWS-service access.
4950

5051
## Deferred scope
5152

@@ -74,15 +75,18 @@ flowchart TB
7475
ECSService["ECS Service<br/>desired task count"]
7576
TaskDef["Task Definition<br/>image + env + CPU/memory"]
7677
ECR["ECR Repository<br/>container image"]
78+
Logs["CloudWatch Logs<br/>container logs"]
79+
Secrets["Secrets Manager<br/>runtime secrets"]
80+
S3["S3<br/>ECR image layers"]
7781
end
7882
7983
subgraph VPC["VPC: private network boundary"]
8084
8185
ALB["Logical Application Load Balancer"]
8286
Listener["ALB Listener<br/>HTTP/HTTPS"]
83-
TG["Target Group<br/>registered task IPs + health state"]
87+
TG["Target Group<br/>registered private task IPs + health state"]
8488
85-
subgraph PublicA["Public Subnet A"]
89+
subgraph PublicA["Public subnet A"]
8690
ALBNodeA["ALB node / network interface<br/>AZ: eu-west-2a<br/>public-facing IP"]
8791
end
8892
@@ -91,11 +95,19 @@ flowchart TB
9195
end
9296
9397
subgraph PrivateAppA["Private app subnet A"]
94-
TaskA["Fargate task<br/>FastAPI container<br/>AZ: eu-west-2a<br/>private IP: 10.0.11.x:8000"]
98+
TaskA["Fargate task<br/>FastAPI container<br/>AZ: eu-west-2a<br/>private IP only<br/>no public IP"]
9599
end
96100
97101
subgraph PrivateAppB["Private app subnet B"]
98-
TaskB["Fargate task<br/>FastAPI container<br/>AZ: eu-west-2b<br/>private IP: 10.0.12.x:8000"]
102+
TaskB["Fargate task<br/>FastAPI container<br/>AZ: eu-west-2b<br/>private IP only<br/>no public IP"]
103+
end
104+
105+
subgraph VPCEndpoints["VPC endpoints for AWS service access"]
106+
EcrApiVpce["Interface endpoint<br/>ECR API"]
107+
EcrDkrVpce["Interface endpoint<br/>ECR Docker registry"]
108+
LogsVpce["Interface endpoint<br/>CloudWatch Logs"]
109+
SecretsVpce["Interface endpoint<br/>Secrets Manager"]
110+
S3GatewayVpce["Gateway endpoint<br/>S3 via private app route table"]
99111
end
100112
101113
subgraph PrivateDB["Private DB subnets"]
@@ -111,15 +123,33 @@ flowchart TB
111123
ALBNodeA --> Listener
112124
ALBNodeB --> Listener
113125
Listener --> TG
114-
TG -->|"healthy target"| TaskA
115-
TG -->|"healthy target"| TaskB
126+
TG -->|"healthy private target"| TaskA
127+
TG -->|"healthy private target"| TaskB
116128
117129
ECSCluster --> ECSService
118130
ECSService --> TaskDef
119131
TaskDef --> ECR
120-
ECSService -->|"starts/registers tasks"| TaskA
121-
ECSService -->|"starts/registers tasks"| TaskB
132+
ECSService -->|"starts/registers private tasks"| TaskA
133+
ECSService -->|"starts/registers private tasks"| TaskB
122134
123135
TaskA --> RDS
124136
TaskB --> RDS
137+
138+
TaskA -->|"HTTPS 443"| EcrApiVpce
139+
TaskA -->|"HTTPS 443"| EcrDkrVpce
140+
TaskA -->|"HTTPS 443"| LogsVpce
141+
TaskA -->|"HTTPS 443"| SecretsVpce
142+
TaskA -->|"S3 route"| S3GatewayVpce
143+
144+
TaskB -->|"HTTPS 443"| EcrApiVpce
145+
TaskB -->|"HTTPS 443"| EcrDkrVpce
146+
TaskB -->|"HTTPS 443"| LogsVpce
147+
TaskB -->|"HTTPS 443"| SecretsVpce
148+
TaskB -->|"S3 route"| S3GatewayVpce
149+
150+
EcrApiVpce --> ECR
151+
EcrDkrVpce --> ECR
152+
LogsVpce --> Logs
153+
SecretsVpce --> Secrets
154+
S3GatewayVpce --> S3
125155
```

0 commit comments

Comments
 (0)