Cloud Integrations¶
This document describes the architecture of the CloudModule for contributors and developers extending Farm's cloud provider support.
Module Location¶
apps/api/src/modules/cloud/
aws/
aws.service.ts — AWS SDK interactions (discovery, cost, ECS, Lambda, Secrets Manager)
aws.service.spec.ts
gcp/
gcp.service.ts — GCP REST API interactions (Cloud Asset, Cloud Run, Secret Manager, Billing)
gcp.service.spec.ts
azure/
azure.service.ts — Azure SDK interactions (ARM, Container Apps, Key Vault, Cost Management)
azure.service.spec.ts
executors/
aws-ecs.executor.ts — Pipeline deploy executor for ECS
aws-ecs.executor.spec.ts
aws-lambda.executor.ts — Pipeline deploy executor for Lambda
aws-lambda.executor.spec.ts
gcp-cloud-run.executor.ts — Pipeline deploy executor for Cloud Run
gcp-cloud-run.executor.spec.ts
azure-container-apps.executor.ts — Pipeline deploy executor for Azure Container Apps
azure-container-apps.executor.spec.ts
dto/
discover-resources.dto.ts
cloud-cost.dto.ts
resolve-secret.dto.ts
interfaces/
cloud-resource.interface.ts — CloudResource interface
cloud.module.ts
cloud-resource.controller.ts — REST endpoints
cloud-resource.service.ts — Aggregation across providers
cloud-cost.service.ts — Cost-specific wrapper
cloud-secrets.service.ts — Secret resolution + pipeline config scanning
Architecture¶
CloudResourceController
├─ GET /api/v1/cloud/resources → CloudResourceService.discoverAll / discoverByProvider
├─ GET /api/v1/cloud/cost → CloudCostService.getAggregatedCost
├─ POST /api/v1/cloud/secrets/resolve → CloudSecretsService.resolve
└─ GET /api/v1/cloud/providers/:orgId → CloudResourceService.listConnectedProviders
CloudResourceService (orchestrator)
├─ AwsService → AWS Resource Groups Tagging API, Cost Explorer
├─ GcpService → Cloud Asset API, Cloud Billing API
└─ AzureService → ARM ResourceManagementClient, Cost Management API
CloudSecretsService (secret resolution)
├─ AwsService.resolveSecret → AWS Secrets Manager
├─ GcpService.resolveSecret → GCP Secret Manager
└─ AzureService.resolveSecret → Azure Key Vault
Pipeline Executors (consumed by PipelineProcessor)
├─ AwsEcsExecutor → AwsService.deployToEcs
├─ AwsLambdaExecutor → AwsService.deployToLambda
├─ GcpCloudRunExecutor → GcpService.deployToCloudRun
└─ AzureContainerAppsExecutor → AzureService.deployToContainerApps
Credential Storage¶
Cloud provider credentials are stored in the integration_credentials table using the IntegrationCredential entity defined in modules/integrations/. The type column distinguishes providers:
IntegrationType enum value | Provider | Encrypted payload shape |
|---|---|---|
aws-iam-role | AWS | { accessKeyId, secretAccessKey, region } |
gcp-service-account | GCP | { serviceAccountJson, projectId } |
azure-service-principal | Azure | { tenantId, clientId, clientSecret, subscriptionId } |
Credentials are encrypted at rest using AES-256-GCM via IntegrationCredentialService.encrypt/decrypt. The encryption key is derived from JWT_SECRET.
Provider services call IntegrationCredentialService.findByType(orgId, type) at request time to resolve credentials. All methods return empty arrays or throw descriptive errors when credentials are absent — they never fail silently.
Resource Discovery¶
Discovery is tag-based. Resources must carry provider-specific tags to be included:
| Provider | Tag Keys |
|---|---|
| AWS | farm:component, farm.io/component |
| GCP | farm_component, farm-component (labels) |
| Azure | farm:component, farm.io/component |
The CloudResource interface returned by all discovery methods:
interface CloudResource {
provider: 'aws' | 'gcp' | 'azure';
resourceId: string; // ARN, GCP full name, or Azure resource ID
resourceType: string; // e.g. "ecs:service", "run.googleapis.com/Service"
name: string;
region: string;
tags: Record<string, string>;
linkedComponentId?: string; // value of the farm:component tag
}
CloudResourceService.discoverAll() calls all three providers in parallel using Promise.allSettled. Individual provider failures are logged and excluded from the result — the method always returns a valid array.
Cost Visibility¶
CloudCostService delegates to CloudResourceService.getAggregatedCost(), which calls:
- AWS —
CostExplorerClient.GetCostAndUsagegrouped byfarm:environmenttag - GCP — Cloud Billing API (returns placeholder data when BigQuery export is not configured)
- Azure —
CostManagementClient.query.usagegrouped byfarm:environmenttag
The response is ProviderCostResult[], one entry per provider that has cost data:
interface ProviderCostResult {
provider: string;
entries: CloudCostEntry[]; // [{ environment, cost, currency, component? }]
}
Pipeline Executors¶
The four cloud executors implement the same execute(config, logFn) pattern used by HelmDeployExecutor. They are injected into PipelineProcessor as @Optional() dependencies and dispatched by the stage.config.engine field:
engine value | Executor | Underlying method |
|---|---|---|
aws-ecs | AwsEcsExecutor | AwsService.deployToEcs |
aws-lambda | AwsLambdaExecutor | AwsService.deployToLambda |
gcp-cloud-run | GcpCloudRunExecutor | GcpService.deployToCloudRun |
azure-container-apps | AzureContainerAppsExecutor | AzureService.deployToContainerApps |
Secret resolution via CloudSecretsService.resolveConfigSecrets is applied to the stage config before the executor receives it.
Secret Resolution¶
CloudSecretsService recognizes three ref formats via static regex patterns:
| Pattern | Provider | Example |
|---|---|---|
^arn:aws:secretsmanager:... | AWS Secrets Manager | arn:aws:secretsmanager:us-east-1:123:secret:prod/db |
^gcp:projects/.../secrets/.../versions/... | GCP Secret Manager | gcp:projects/my-project/secrets/db-pass/versions/latest |
^azure:https://...:... | Azure Key Vault | azure:https://my-vault.vault.azure.net:db-password |
resolveConfigSecrets(config, orgId) scans a pipeline stage config object and replaces any string values matching the patterns with their resolved plain-text values. Unresolved refs are logged as warnings and left unchanged.
Adding a New Cloud Provider¶
- Create
modules/cloud/{provider}/{provider}.service.tsimplementingdiscoverResources,getMonthlyCost, andresolveSecretmethods. - Add a new
IntegrationTypeenum value inmodules/integrations/entities/integration-credential.entity.ts. - Register the service in
CloudModuleproviders and exports. - Inject the service as
@Optional()inCloudResourceServiceandCloudSecretsService. - Extend
CloudResourceService.discoverAll / discoverByProvider / getAggregatedCostto call the new service. - Add executor(s) in
modules/cloud/executors/if the provider supports pipeline deployments. - Add the executor to
PipelineProcessoras an@Optional()dependency and add a dispatch branch. - Write unit tests for the service and executor following the existing mock patterns.
- Update
docs/user-guide/cloud-providers.mdanddocs/api-reference/cloud.md.
Testing¶
Unit tests use jest.fn() mock factories for all SDK clients. The pattern mirrors aws.service.spec.ts:
jest.mock('@aws-sdk/...')(or equivalent) at the top of the spec file — hoisted above imports.- Import the mocked constructors after the
jest.mockcalls to access.mockImplementation. - Re-configure
mockImplementationper test case to simulate specific SDK responses. jest.clearAllMocks()inbeforeEachandafterEach.
All provider services are designed to return empty results (not throw) when credentials are missing — verify this behavior with explicit test cases.