Cloud Connector
Cloud Connector
Section titled “Cloud Connector”Cloud Connector provisions and manages cloud infrastructure across multiple providers. Provisioning happens through direct provider SDK/REST calls, not Infrastructure-as-Code — an earlier CDKTF (Terraform CDK) design was fully removed (ADR-001/M10, dependency cleanup SEC-231). There is no cdktf/constructs dependency in the codebase anymore.
Key Facts
Section titled “Key Facts”- Port: 4321 (API server only — the worker process has no HTTP surface)
- Tech: Express 4, TypeScript, TypeORM + PostgreSQL, amqplib (RabbitMQ), Winston, Axios
- Database: PostgreSQL (
cloud-connector-db, dev port 5434) - Messaging: RabbitMQ,
cloud.eventsexchange (topic) — single canonical exchange per ADR-001 - Provisioning: Always asynchronous — never a synchronous call into a provider API from an HTTP request path
Supported Providers
Section titled “Supported Providers”Adapters live in src/adapters/*.adapter.ts and implement the ICloudProvider interface.
| Provider | Status | Adapter file(s) |
|---|---|---|
| AWS | Fully implemented | aws.adapter.ts, aws-iam.adapter.ts, aws-network.adapter.ts |
| Google Cloud | Fully implemented | gcp.adapter.ts, gcp-iam.adapter.ts |
| Azure | Fully implemented | azure.adapter.ts, azure-iam.adapter.ts, azure-network.adapter.ts (~1256 lines in the main adapter alone) |
| Hetzner | Fully implemented | hetzner.adapter.ts |
| Scaleway | Fully implemented | scaleway.adapter.ts |
| OVHcloud | Fully implemented | ovhcloud.adapter.ts |
| UpCloud | Fully implemented | upcloud.adapter.ts |
| DigitalOcean | Not yet implemented — no adapter file exists in src/adapters/ | — |
Azure was historically listed as “in progress (BYOC)” in older notes; that is stale — the Azure adapter set is complete and on par with AWS/GCP. DigitalOcean, by contrast, has no adapter at all yet, despite appearing as a supported provider value in the job schema (see below) — treat it as reserved/planned, not working.
Provisioning Flow
Section titled “Provisioning Flow”The canonical path (ADR-001) has no Terraform/CDKTF step and no HTTP bridge service:
Strapi: cloud-instance lifecycle hook (create/update/destroy) ↓webhook-publisher (wraps event as { event, data: { jobId, type, cloudInstanceId, credential_id?, payload: { provider, region, config } } }) ↓RabbitMQ: cloud.events exchange (topic) routing key: cloud-instance.provision | cloud-instance.update | cloud-instance.destroy ↓cloud-connector-worker (dist/worker.js, separate Docker Compose service — consumes directly, no HTTP hop) ↓worker.ts unwraps the webhook-publisher envelope, resolves the BYOC credential ↓Provider adapter (direct SDK/REST call — AWS SDK, @google-cloud, Azure SDK, Hetzner REST, …) ↓POST /api/cloud-instances/:documentId/status-callback (Strapi) header: X-Service-Secret: CLOUD_CALLBACK_SECRET ↓Strapi updates CloudInstance status → frontend sees the new stateThere is also an admin/sync path: POST /v1/provision on the API server accepts a provisioning request directly and publishes onto the same cloud.events exchange rather than doing any work inline.
Architecture
Section titled “Architecture”Provider adapters
Section titled “Provider adapters”Directory: src/adapters/
src/adapters/├── aws.adapter.ts├── aws-iam.adapter.ts├── aws-network.adapter.ts├── azure.adapter.ts├── azure-iam.adapter.ts├── azure-network.adapter.ts├── base-resource.adapter.ts├── gcp.adapter.ts├── gcp-iam.adapter.ts├── hetzner.adapter.ts├── multi-region.adapter.ts├── ovhcloud.adapter.ts├── scaleway.adapter.ts├── tier-aware-adapter-factory.ts└── upcloud.adapter.tsEach provider adapter:
- Implements the
ICloudProviderinterface - Calls the provider’s SDK/REST API directly (no generated IaC, no
terraform apply) - Returns instance details (IP, SSH key/credentials, provider-native instance ID)
Database (TypeORM + PostgreSQL)
Section titled “Database (TypeORM + PostgreSQL)”Schema is owned by TypeORM migrations under src/database/migrations/ (there is currently no src/database/entities/ directory in the repository — the job/instance state is modeled in src/models/job.model.ts and applied via migration, e.g. CreateJobsTable). The baseline jobs table tracks provisioning jobs:
jobs├── id (uuid)├── type (enum: provision | update | destroy | heartbeat)├── provider (enum: aws | gcp | azure | digitalocean | scaleway | ovhcloud | hetzner | upcloud)├── region├── config (jsonb)├── status (enum: PENDING | RUNNING | SUCCESS | FAILED)├── createdAt / updatedAt├── startedAt / completedAt├── error (text)├── result (jsonb)├── retryCount / maxRetries├── instanceId└── metadata (jsonb)Confirm exact column/table names against src/database/migrations/ and src/models/ directly before relying on them in code — this reflects the current baseline migration, not a hand-maintained entity file.
Do not hand-edit applied migrations — add a new migration file for schema changes.
Configuration
Section titled “Configuration”.env variables (excerpt):
# API serverPORT=4321DATABASE_HOST=cloud-connector-dbRABBITMQ_URL=amqp://admin:admin@sencai-mq:5672STRAPI_BASE_URL=http://backend:1337STRAPI_API_TOKEN=<token>CLOUD_CALLBACK_SECRET=<shared secret for status-callback>DEPLOYMENT_STRATEGY=single-region
# AWSAWS_REGION=eu-central-1AWS_ACCESS_KEY_ID=<key>AWS_SECRET_ACCESS_KEY=<secret>
# Google CloudGCP_PROJECT_ID=<project>GOOGLE_APPLICATION_CREDENTIALS=/data/gcp-key.json
# AzureAZURE_SUBSCRIPTION_ID=<id>AZURE_TENANT_ID=<id>AZURE_CLIENT_ID=<id>AZURE_CLIENT_SECRET=<secret>
# HetznerHETZNER_TOKEN=<token>
# Scaleway / OVHcloud / UpCloudSCALEWAY_ACCESS_KEY=<key>SCALEWAY_SECRET_KEY=<secret>OVH_APPLICATION_KEY=<key>OVH_APPLICATION_SECRET=<secret>UPCLOUD_USERNAME=<username>UPCLOUD_PASSWORD=<password>Most production credentials are supplied per-tenant as BYOC (bring-your-own-cloud) credentials rather than global .env values — see the BYOC credential material in cloud-connector/CLAUDE.md for the resolution order.
Endpoints
Section titled “Endpoints”The API server (src/index.ts) exposes a larger set of routes than documented here (DNS, CDN, WAF, monitoring, deployment-switch endpoints among them); the following are the ones relevant to the provisioning/Strapi integration:
GET /health # Health checkGET /v1/providers/health # Per-provider health statusPOST /v1/provision # Accept a provisioning job, publish to cloud.eventsGET /v1/jobs/:jobId # Job status lookupGET /v1/jobs/stats # Aggregate job stats
GET /v1/strapi/status # Full status for the Strapi dashboardGET /v1/strapi/health # Health check tailored for StrapiPOST /v1/strapi/webhook # Inbound webhook from StrapiPOST /v1/strapi/force-update # Force a status updatePOST /v1/strapi/cleanup # Clean up stale jobsConfirm the full route list against src/index.ts — additional endpoints exist for DNS zones, CDN distributions, WAF ACLs, DDoS resource tracking, and blue/green deployment switching that are out of scope for this page.
RabbitMQ Events
Section titled “RabbitMQ Events”Cloud Connector consumes from the cloud.events exchange (topic, single canonical exchange per ADR-001):
| Routing key | Queue | Action |
|---|---|---|
cloud-instance.provision | cloud.connector.provision | Provision a new instance |
cloud-instance.update | cloud.connector.update | Update an existing instance |
cloud-instance.destroy | cloud.connector.destroy | Destroy an instance |
inventory.scan | cloud.connector.inventory-scan | Read-only resource discovery (adapter.discoverResources()), batched back to /api/cloud-assets/scan-callback |
Failed jobs are dead-lettered rather than dropped: exchange cloud.connector.dead (topic) + queue cloud.connector.dead.q, with a 14-day TTL.
Job payload shape (unwrapped from the webhook-publisher envelope by worker.ts):
{ "jobId": "uuid", "type": "provision", "cloudInstanceId": "documentId", "credential_id": "optional-byoc-credential-id", "payload": { "provider": "aws", "region": "eu-central-1", "config": { "instanceType": "medium", "os": "ubuntu" } }}Adding a New Provider
Section titled “Adding a New Provider”- Create the adapter:
src/adapters/<provider>.adapter.ts
export class MyProviderAdapter implements ICloudProvider { async provision(config: InstanceConfig): Promise<InstanceDetails> { // Direct SDK/REST call to the provider — no generated IaC // Return { ip, sshKey, credentials, ... } }
async update(instanceId: string, config: Partial<InstanceConfig>): Promise<void> { /* ... */ } async destroy(instanceId: string): Promise<void> { /* ... */ }}- Register the adapter in the provider factory (
src/adapters/tier-aware-adapter-factory.tsor the relevant config module). - Add env vars to
.env.exampleandorchestration/.env.example. - Add the provider value to the
jobs_provider_enumvia a new migration if it isn’t already present. - Test end-to-end: create an instance from the frontend and confirm the job transitions
PENDING → RUNNING → SUCCESSand the status-callback reaches Strapi.
Troubleshooting
Section titled “Troubleshooting”Q: Provisioning stuck in “provisioning”/RUNNING status
A: Check the worker logs (docker logs cloud-connector-worker, not the API server container). Look for provider API errors, timeouts, or a dead-lettered job (cloud.connector.dead.q).
Q: Instance created but Sencai status is still “error”
A: The status-callback to Strapi may have failed (check X-Service-Secret/CLOUD_CALLBACK_SECRET matches on both sides) or the job hit a dead-letter after exhausting retries. Check Strapi logs and the dead-letter queue before manually editing instance status.
Q: Can’t connect to instance via SSH
A: Verify:
- Public IP is correct
- SSH port 22 is open in the provider’s security group/firewall
- SSH key matches the instance
- Instance OS reports ready in the provider console
Q: Provider credentials rejected
A: Verify BYOC credentials resolved correctly for the job (per-tenant credential, not just a global .env fallback), then test manually against the provider CLI/SDK, e.g.:
# AWSaws ec2 describe-instances --region eu-central-1
# Google Cloudgcloud auth listgcloud compute instances list
# Azureaz vm list --output table