mcp-vf-hosted: Migration ECS-Express → Fargate + Tunnel-Sidecar
Ausfuehrbarer Prompt fuer eine frische Claude-Code-Session in ~/source/mcps/mcp-vf-hosted/. Baut den Service vom alten ECS-Express-Stack (gescheitert wegen Custom-Domain-Issue) auf klassisches ECS Fargate + Cloudflare-Tunnel-Sidecar um. Pattern: mcp-hosting-fargate-tunnel. Entscheidung: hosted-mcp-architektur-2026.
Stand bei Session-Start (was schon existiert)
Vom Vortag uebernehmen, nicht neu bauen:
| Resource | Wert |
|---|---|
| Cloudflare Tunnel-ID | ce6dd7c0-d31c-456c-be8a-8705c079982d |
| Cloudflare Tunnel-Name | mcp-vf-hosted |
| Tunnel-Token (Secret) | arn:aws:secretsmanager:eu-central-1:425924867359:secret:mcp-vf-hosted/cloudflared-token-q5MEkS |
| Tunnel-Ingress-Config | mcp-vf.agenticventures.de → http://localhost:8080, sonst http_status:404 |
| Sub-MCP-Tokens (Secret) | arn:aws:secretsmanager:eu-central-1:425924867359:secret:mcp-vf-hosted/upstream-tokens-AHEi6G (6 Keys: papierkram + ticketpay + m365) |
| ECR Repo | 425924867359.dkr.ecr.eu-central-1.amazonaws.com/mcp-vf-hosted (Image :latest mit allen 3 Sub-MCPs + Workflow-Prompts + diagnostic /health) |
| Cloudflare Zone | agenticventures.de aktiv (Zone-ID 772ff674c33fbe17cae49d7234c30e60) |
CF DNS-Record mcp-vf | aktuell CNAME → mc-1a56ed08537b4f9fae11028fe698af47.ecs.eu-central-1.on.aws (gibt 404, wird umgestellt) |
| Alter ECS-Express-Stack | McpVfHosted in CFN, laeuft mit alter Architektur, gibt aktuell den 404 |
Pre-Run-Setup (Mensch, ~3 Min)
export CF_API_TOKEN="<token-aus-cloudflare-dashboard>"
export CF_ACCOUNT_ID="bf395d62cc6a9117564c0712fa9e3ad2"
export CF_ZONE_ID="772ff674c33fbe17cae49d7234c30e60"
export TUNNEL_ID="ce6dd7c0-d31c-456c-be8a-8705c079982d"
# Sanity-Check Cloudflare-API
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID" | jq '.result.status'
# Erwartet: "active"
# Sanity-Check AWS
~/.local/bin/aws sts get-caller-identity --profile av-production --query 'Account' --output text
# Erwartet: 425924867359Der Prompt (Copy-Paste in frische Claude-Code-Session in ~/source/mcps/mcp-vf-hosted/)
# Ziel: mcp-vf-hosted von ECS Express auf klassisches Fargate + Tunnel-Sidecar migrieren
## Kontext
- Repo: `~/source/mcps/mcp-vf-hosted` — FastMCP v2 Wrapper, Code + Image + Tests stehen
- AWS-Account: `av-production` (425924867359), eu-central-1
- Vorgaenger-Pattern (gescheitert): ECS Express Mode mit AWS-managed ALB — Custom-Domain `mcp-vf.agenticventures.de` gab 404 weil ALB nur eigenen Hostname akzeptiert + Sidecar nicht moeglich
- Neues Pattern: klassisches ECS Fargate, 2 Container im Task (vf-mono + cloudflared-Sidecar), KEIN ALB, Tunnel ist einziger Public-Eingang
- Production-Risiko: 0 — Andre nutzt MCP noch nicht aktiv. Wir koennen den alten Stack abreissen.
- Doku-Refs:
- `~/source/agentic-ventures/intern/wissen/entscheidungen/hosted-mcp-architektur-2026.md` (Decision)
- `~/source/agentic-ventures/intern/wissen/prozesse/mcp-hosting-fargate-tunnel.md` (Pattern)
- `~/source/agentic-ventures/intern/capabilities/mcps/mcp-vf-hosted.md` (Capability)
- `~/source/agentic-ventures/intern/projekte/mcp-pipeline-aws/_index.md` (Pipeline-Plan)
## Was bleibt unveraendert
- Source-Code (`src/mcp_vf_hosted/*.py`) — Workflow-Prompts, /health, M365 sind alle drin
- Dockerfile + Image im ECR — `:latest` ist aktuell mit allen 3 Sub-MCPs
- Secret `mcp-vf-hosted/upstream-tokens` (6 Keys) — wiederverwenden
- Secret `mcp-vf-hosted/cloudflared-token` — wiederverwenden
- Cloudflare-Tunnel `ce6dd7c0-d31c-456c-be8a-8705c079982d` + Ingress-Config — wiederverwenden
- Cloudflare-Zone + DNS-Setup ausser dem `mcp-vf` CNAME — wiederverwenden
- Scalekit-Resource fuer Andre — wiederverwenden, Identifier `https://mcp-vf.agenticventures.de/mcp` bleibt
## Was sich aendert
- CDK-Stack `infra/lib/mcp-vf-hosted-stack.ts` wird komplett ersetzt:
- Statt `CfnExpressGatewayService` → `FargateService` + `FargateTaskDefinition`
- Zweiter Container `cloudflared` im Task
- Kein ALB, keine 3 Spezial-IAM-Rollen (nur ExecRole + TaskRole), keine ALB-Sec-Group
- ECR-Repo bleibt (importiert wie vorher, weil RemovalPolicy.RETAIN)
- Secret-Mappings unveraendert (`secrets[]` mit `:KEY::`-Syntax)
- ENV-Vars unveraendert
- CFN-Stack-Name bleibt `McpVfHosted`, aber die Resources darin werden alle ersetzt
## Vorbedingungen (zuerst pruefen, bei Fehler stoppen)
1. `aws sts get-caller-identity --profile av-production` → Account `425924867359`
2. `echo $CF_API_TOKEN`, `echo $CF_ACCOUNT_ID`, `echo $CF_ZONE_ID`, `echo $TUNNEL_ID` alle gesetzt
3. Token-Permissions: `Account:Cloudflare Tunnel:Edit` + `Zone:DNS:Edit` fuer agenticventures.de
4. Tunnel existiert: `curl -s -H "Authorization: Bearer $CF_API_TOKEN" "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID" | jq '.result.name'` → "mcp-vf-hosted"
5. Tunnel-Ingress-Config sitzt: `curl -s -H "Authorization: Bearer $CF_API_TOKEN" ".../cfd_tunnel/$TUNNEL_ID/configurations" | jq '.result.config.ingress'` → muss `mcp-vf.agenticventures.de → http://localhost:8080` enthalten
6. AWS-Secrets existieren: `aws secretsmanager describe-secret --secret-id mcp-vf-hosted/upstream-tokens` und `... mcp-vf-hosted/cloudflared-token`
7. ECR-Image existiert: `aws ecr describe-images --repository-name mcp-vf-hosted --image-ids imageTag=latest`
8. CFN-Stack `McpVfHosted` existiert: `aws cloudformation describe-stacks --stack-name McpVfHosted`
Bei einer Failed-Bedingung: stoppen, Diagnose ausgeben, **nicht** raten.
## Phase 1 — CDK-Stack rewriten
1. Backup vom alten Stack-File:
```
cp infra/lib/mcp-vf-hosted-stack.ts infra/lib/mcp-vf-hosted-stack.ts.ecs-express-backup
```
2. Neuer Stack-Inhalt (`infra/lib/mcp-vf-hosted-stack.ts`):
- Importe: `aws-cdk-lib`, `aws-ecr` (fromRepositoryName, weil Repo retained), `aws-ecs`, `aws-iam`, `aws-secretsmanager`, `aws-logs`
- Klasse `McpVfHostedStack extends cdk.Stack`
- Body:
- `repo = ecr.Repository.fromRepositoryName(this, 'EcrRepo', 'mcp-vf-hosted')`
- `upstreamTokens = secretsmanager.Secret.fromSecretNameV2(this, 'UpstreamTokens', 'mcp-vf-hosted/upstream-tokens')` (importiert das bestehende)
- `cloudflaredToken = secretsmanager.Secret.fromSecretNameV2(this, 'CloudflaredToken', 'mcp-vf-hosted/cloudflared-token')`
- `execRole = new iam.Role(...)` mit `AmazonECSTaskExecutionRolePolicy`, plus `upstreamTokens.grantRead(execRole)` und `cloudflaredToken.grantRead(execRole)`
- `taskRole = new iam.Role(...)` mit nichts (leer)
- `logGroup = new logs.LogGroup(this, 'LogGroup', { logGroupName: '/aws/ecs/default/mcp-vf-hosted', retention: logs.RetentionDays.ONE_MONTH })`
- `taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', { cpu: 1024, memoryLimitMiB: 2048, executionRole: execRole, taskRole: taskRole })`
- **Container 1 (vf-mono)**:
- `image: ecs.ContainerImage.fromEcrRepository(repo, 'latest')`
- `containerName: 'vf-mono'`
- `essential: true`
- `portMappings: [{ containerPort: 8080 }]`
- `environment: { SCALEKIT_ENV_URL: 'https://agenticventures.eu.scalekit.dev', SCALEKIT_RESOURCE_ID: 'res_123571403667539459', PUBLIC_BASE_URL: 'https://mcp-vf.agenticventures.de', AWS_REGION: this.region, LOG_LEVEL: 'INFO', EMERGENCY_DISABLE: 'false', RATE_LIMIT_PER_MINUTE: '60', RATE_LIMIT_PER_HOUR: '1000' }`
- `secrets: { PAPIERKRAM_TOKEN: ecs.Secret.fromSecretsManager(upstreamTokens, 'PAPIERKRAM_TOKEN'), PAPIERKRAM_SUBDOMAIN: ..., TICKETPAY_API_KEY: ..., M365_TENANT_ID: ..., M365_CLIENT_ID: ..., M365_CLIENT_SECRET: ... }` (6 Keys aus dem JSON-Secret)
- `logging: ecs.LogDrivers.awsLogs({ logGroup, streamPrefix: 'vf-mono' })`
- `healthCheck: { command: ['CMD-SHELL', 'curl -f http://localhost:8080/health || exit 1'], interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), retries: 3, startPeriod: cdk.Duration.seconds(60) }`
- **Container 2 (cloudflared)**:
- `image: ecs.ContainerImage.fromRegistry('cloudflare/cloudflared:latest')`
- `containerName: 'cloudflared'`
- `essential: true`
- `command: ['tunnel', '--no-autoupdate', 'run']`
- `secrets: { TUNNEL_TOKEN: ecs.Secret.fromSecretsManager(cloudflaredToken) }` (das ganze Secret, nicht JSON-Key)
- `logging: ecs.LogDrivers.awsLogs({ logGroup, streamPrefix: 'cloudflared' })`
- **Cluster**: `ecs.Cluster.fromClusterAttributes(this, 'Cluster', { clusterName: 'default', vpc: ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true }) })` — oder neuer Cluster wenn `default` Probleme macht
- **Service**: `new ecs.FargateService(this, 'Service', { serviceName: 'mcp-vf-hosted', cluster, taskDefinition: taskDef, desiredCount: 1, assignPublicIp: true, securityGroups: [<eigene-sg-nur-outbound-443>] })`
- Security-Group: `new ec2.SecurityGroup(this, 'EgressOnly', { vpc, allowAllOutbound: true })` — keine Inbound-Regeln
- Outputs:
- `ServiceName: 'mcp-vf-hosted'`
- `EcrUri`
- `LogGroupName: '/aws/ecs/default/mcp-vf-hosted'`
- `TunnelHostname: '<tunnel-id>.cfargotunnel.com'` (Hard-coded weil bekannt)
3. ASCII-Hygiene: `grep -nP '[^\x00-\x7F]' infra/lib/mcp-vf-hosted-stack.ts` — wenn was kommt, ersetzen (Em-Dash → -, etc.). IAM-Role-Descriptions duerfen kein non-ASCII enthalten (CFN-Stolperer von 2026-05-10).
4. `cd infra && npx cdk diff --profile av-production` — sollte zeigen:
- Alle alten Express-Resources: DELETED
- Neue klassische Fargate-Resources: ADDED
- Beide importierten Secrets + ECR: unveraendert (importiert, nicht managed)
## Phase 2 — Alten Stack zerstoeren + neuen Stack deployen
**Achtung:** Wir koennen NICHT einfach `cdk deploy` machen — die Resource-Typen aendern sich komplett (CfnExpressGatewayService → FargateService etc.). Das wuerde CFN versuchen replace-in-place zu machen und scheitern. Wir muessen den Stack zuerst loeschen.
1. Alten Stack loeschen:
```
aws cloudformation delete-stack --stack-name McpVfHosted --profile av-production --region eu-central-1
```
Warten bis `DELETE_COMPLETE`.
Was wird geloescht: ExecRole, InfraRole, TaskRole, IAM-Policy, ExpressGatewayService. NICHT geloescht (weil retained oder importiert): ECR-Repo, beide Secrets (sind RETAIN). ALB + Cert wird von ECS Express-Cleanup mitgeloescht.
2. Pruefen dass ECR + Secrets noch da sind:
```
aws ecr describe-repositories --repository-names mcp-vf-hosted --profile av-production --region eu-central-1
aws secretsmanager describe-secret --secret-id mcp-vf-hosted/upstream-tokens --profile av-production --region eu-central-1
aws secretsmanager describe-secret --secret-id mcp-vf-hosted/cloudflared-token --profile av-production --region eu-central-1
```
3. Neuen Stack deployen:
```
cd infra
npx cdk deploy --profile av-production --require-approval never --outputs-file /tmp/vf-cdk-outputs.json
```
4. Stack-Status auf CREATE_COMPLETE warten (bis ~5 Min). Bei Fehler: Logs anschauen, NICHT raten.
## Phase 3 — Smoke gegen Tunnel-Hostname
Sobald Stack ACTIVE + Task RUNNING:
1. Task-Status pruefen:
```
aws ecs describe-services --cluster default --services mcp-vf-hosted --profile av-production --region eu-central-1 \
--query 'services[0].[status,desiredCount,runningCount,pendingCount]' --output text
```
Erwartet: `ACTIVE 1 1 0`.
2. Cloudflared-Logs pruefen — muss `Registered tunnel connection` enthalten:
```
aws logs tail /aws/ecs/default/mcp-vf-hosted --filter-pattern '?Registered ?cloudflared' --profile av-production --region eu-central-1
```
3. Tunnel-Status in Cloudflare:
```
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID" \
| jq '{status: .result.status, connections: .result.connections}'
```
Erwartet: `status: "healthy"`, 4 Connections sichtbar.
4. Smoke mit Host-Override gegen Tunnel-Hostname:
```
curl -sS -i "https://$TUNNEL_ID.cfargotunnel.com/health" \
-H "Host: mcp-vf.agenticventures.de"
```
Erwartet: `HTTP/2 200`, Body `{"ok":true,"service":"vf-mono","version":"0.1.0","submcps_active":["papierkram","ticketpay","m365"],"uptime_seconds":<n>}`.
## Phase 4 — DNS-Cutover
1. `mcp-vf` Record in Cloudflare auf Tunnel-Hostname umstellen:
```
RECORD_ID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=mcp-vf.agenticventures.de&type=CNAME" \
| jq -r '.result[0].id')
curl -sS -X PUT \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"CNAME\",
\"name\": \"mcp-vf\",
\"content\": \"$TUNNEL_ID.cfargotunnel.com\",
\"proxied\": true,
\"ttl\": 1
}" \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$RECORD_ID"
```
2. ~60s warten (Cloudflare-Edge-Cache-TTL).
3. End-to-End Smoke OHNE Host-Override:
```
curl -sS -i https://mcp-vf.agenticventures.de/health
```
Erwartet: `HTTP/2 200`, Header `server: cloudflare`, `cf-ray: ...`, Body mit drei submcps.
## Phase 5 — Wiki-Updates
Wenn Phase 4 gruen:
1. `~/source/agentic-ventures/intern/capabilities/mcps/mcp-vf-hosted.md` — neue Architektur eintragen (Fargate-Tunnel, kein ALB, ServiceName `mcp-vf-hosted`)
2. `~/source/agentic-ventures/intern/capabilities/repos/mcp-vf-hosted.md` — Frontmatter updaten
3. `~/source/agentic-ventures/intern/projekte/mcp-pipeline-aws/_index.md` — Phase 1B als ✅ DONE, neue Phase „Fargate-Tunnel-Migration" am Ende mit ✅, ECS-Express-Phase als deprecated markieren
4. `~/source/agentic-ventures/intern/firma/produkt-bundle.md` — Compute-Sektion (Punkt 1) auf Fargate-Tunnel-Pattern updaten
5. `~/source/mcps/mcp-vf-hosted/docs/architecture.md` — Mermaid-Diagramm aktualisieren (Tunnel statt ALB)
6. `~/source/mcps/mcp-vf-hosted/README.md` — Architektur-Diagramm + Deploy-Sektion auf Fargate-Tunnel
## Rollback (falls Phase 2 oder Phase 4 bricht)
- Phase 2 bricht: alter Stack ist schon geloescht — neuen Stack debuggen, bei Bedarf in mehreren Iterationen. Production-Risiko = 0 weil Andre nicht aktiv ist.
- Phase 4 bricht: `mcp-vf` Record zurueck auf alten ECS-Express-Hostname → aber der ALB existiert nicht mehr nach Stack-Delete, also kein Sinn. Alternativen:
- Tunnel-Hostname direkt nutzen (`$TUNNEL_ID.cfargotunnel.com`) und Cert-Validation-Browser-Issues akzeptieren
- Stack neu deployen und debuggen
## Wichtige Constraints
- **ALB darf NICHT entstehen.** Wenn der neue Stack einen ALB provisioniert, hast du was falsch gemacht — pruefen.
- **Tunnel-Token-Secret NICHT veraendern.** Wenn du verwirrt bist welcher Tunnel zu welchem Token gehoert: stoppen + fragen, nicht raten.
- **Sub-MCP-Code NICHT anfassen.** Die Migration ist rein Infrastruktur.
- **Andres Scalekit-Resource NICHT loeschen oder veraendern.** Identifier `https://mcp-vf.agenticventures.de/mcp` ist die Audience im JWT.
## Was nach erfolgreicher Migration ansteht (NICHT in dieser Session)
- DSGVO-To-Dos aus [[../../wissen/entscheidungen/cloudflare-dsgvo]] (Subprozessor-Eintrag in Andres AVV)
- Christoph als zweiten Scalekit-User onboarden
- 24h Stabilitaets-Beobachtung
- `mcp-gsuite-hosted` (Phase 1A) auf gleiches Pattern migrierenErwartete Session-Laenge
3-5 Stunden, davon:
- Phase 1 (Stack-Rewrite): ~90 Min
- Phase 2 (Delete + Deploy): ~30 Min davon ~15 Min Warten
- Phase 3 (Tunnel-Smoke): ~10 Min
- Phase 4 (DNS-Cutover + End-to-End-Smoke): ~10 Min
- Phase 5 (Wiki-Updates): ~30-60 Min
Voraussetzungen-Checkliste vor Session-Start
- Cloudflare-API-Token mit
Account:Cloudflare Tunnel:Edit+Zone:DNS:Editfueragenticventures.de(TTL min 24h) - Environment-Vars
CF_API_TOKEN,CF_ACCOUNT_ID=bf395d62cc6a9117564c0712fa9e3ad2,CF_ZONE_ID=772ff674c33fbe17cae49d7234c30e60,TUNNEL_ID=ce6dd7c0-d31c-456c-be8a-8705c079982dexportiert - AWS CLI mit Profil
av-productionfunktioniert (aws sts get-caller-identity) - cd in
~/source/mcps/mcp-vf-hosted/vor Start - Heute keine andere Andre-MCP-Aktivitaet erwartet (Production-Risiko 0)
Related
- hosted-mcp-architektur-2026 — Decision warum dieses Pattern
- mcp-hosting-fargate-tunnel — Pattern-Detail
- mcp-hosting-aws-ecs-express — Vorgaenger (deprecated)
- cloudflare-migration-guide — vorbereitende NS-Migration (durch)
- tunnel-migration-prompt — frueherer Prompt mit ECS-Express-Annahme (durch dieses File ersetzt)
- mcp-vf-hosted — Capability-File