AWS ECS
Deploy Openfuse on AWS ECS with Fargate, ALB hostname-based routing, Secrets Manager, and CDK examples for the API, Keycloak, UI, and Config Importer.
This guide covers deploying Openfuse on AWS ECS (Fargate). For the single-server Docker Compose setup, see Docker Compose.
Prerequisites
- An AWS account with ECS, ALB, RDS, and Secrets Manager access
- A managed PostgreSQL 17+ instance (RDS) with two databases:
openfuseandkeycloak - A domain with wildcard DNS records pointing to your ALB
- ACM certificates for your domain (wildcard recommended)
Architecture
| Component | ECS Resource | Notes |
|---|---|---|
| API | Fargate Service | Stateless, scale freely |
| Keycloak | Fargate Service | Single task, or clustered for HA |
| UI | Fargate Service | Static SPA, or use S3 + CloudFront |
| Config Importer | RunTask (one-off) | Execute during deployments |
ALB routing
Configure hostname-based routing rules on your Application Load Balancer:
| Rule | Host pattern | Target group |
|---|---|---|
| 1 | *.api.<ROOT_DOMAIN> | API (port 3000) |
| 2 | sso.<ROOT_DOMAIN> | Keycloak (port 8080) |
| 3 | Default | UI (port 80) |
TLS terminates at the ALB using ACM certificates.
Secrets Manager
Store all secrets in AWS Secrets Manager and reference them in task definitions:
const apiSecrets = new secretsmanager.Secret(this, 'OpenfuseApiSecrets', {
secretObjectValue: {
DATABASE_PASSWORD: SecretValue.unsafePlainText('generate-me'),
SESSION_SECRET: SecretValue.unsafePlainText('generate-me'),
KC_STAFF_BACKEND_CLIENT_SECRET: SecretValue.unsafePlainText('generate-me'),
KC_STAFF_BFF_CLIENT_SECRET: SecretValue.unsafePlainText('generate-me'),
KC_TENANTS_BACKEND_CLIENT_SECRET: SecretValue.unsafePlainText('generate-me'),
KC_TENANTS_BFF_CLIENT_SECRET: SecretValue.unsafePlainText('generate-me'),
KC_TENANTS_SDK_CLIENT_SECRET: SecretValue.unsafePlainText('generate-me'),
},
})Reference in task definitions:
secrets: {
DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(apiSecrets, 'DATABASE_PASSWORD'),
SESSION_SECRET: ecs.Secret.fromSecretsManager(apiSecrets, 'SESSION_SECRET'),
KC_STAFF_BACKEND_CLIENT_SECRET: ecs.Secret.fromSecretsManager(apiSecrets, 'KC_STAFF_BACKEND_CLIENT_SECRET'),
// ...
}Config Importer
Use RunTask to execute the Config Importer as a one-off Fargate task before updating the API service.
const configImporter = new ecs.FargateTaskDefinition(this, 'ConfigImporter', {
memoryLimitMiB: 512,
cpu: 256,
})
configImporter.addContainer('config-importer', {
image: ecs.ContainerImage.fromRegistry(
'ghcr.io/openfuseio/openfuse-config-importer:1.0.0'
),
secrets: {
KEYCLOAK_PASSWORD: ecs.Secret.fromSecretsManager(apiSecrets, 'KC_BOOTSTRAP_ADMIN_PASSWORD'),
KC_STAFF_BACKEND_CLIENT_SECRET: ecs.Secret.fromSecretsManager(apiSecrets, 'KC_STAFF_BACKEND_CLIENT_SECRET'),
// ... remaining secrets
},
environment: {
KEYCLOAK_URL: 'http://keycloak.internal:8080',
IMPORT_VARSUBSTITUTION_ENABLED: 'true',
IMPORT_VARSUBSTITUTION_UNDEFINEDISTERROR: 'true',
// ... remaining env vars
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'config-importer' }),
})In CDK, use a Custom Resource to trigger RunTask during deployments. The importer waits up to 120s for Keycloak to be ready before importing.
Run on first deployment and on upgrades that include realm config changes.
API service
const apiTask = new ecs.FargateTaskDefinition(this, 'ApiTask', {
memoryLimitMiB: 1024,
cpu: 512,
})
apiTask.addContainer('api', {
image: ecs.ContainerImage.fromRegistry('ghcr.io/openfuseio/openfuse-api:1.0.0'),
portMappings: [{ containerPort: 3000 }],
secrets: {
DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(apiSecrets, 'DATABASE_PASSWORD'),
SESSION_SECRET: ecs.Secret.fromSecretsManager(apiSecrets, 'SESSION_SECRET'),
// ... remaining secrets
},
environment: {
ROOT_DOMAIN: 'openfuse.example.com',
DATABASE_HOST: rds.instanceEndpoint.hostname,
DATABASE_SSL: 'true',
KEYCLOAK_URL: 'http://keycloak.internal:8080',
KEYCLOAK_EXTERNAL_URL: 'https://sso.openfuse.example.com',
// ... remaining env vars
},
healthCheck: {
command: ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'],
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'api' }),
})
const apiService = new ecs.FargateService(this, 'ApiService', {
cluster,
taskDefinition: apiTask,
desiredCount: 2,
})The API is stateless — scale freely. All replicas must share the same SESSION_SECRET.
Static UI alternative
Instead of running the UI container on ECS, serve the SPA from S3 + CloudFront:
- Extract static files from the
openfuse-uiDocker image - Upload to an S3 bucket
- Serve a
config.jsat the root with runtime configuration - Configure CloudFront with SPA fallback (custom error page →
index.html)
This reduces cost and improves global performance via CDN caching.
Next steps
Kubernetes
Deploy Openfuse on Kubernetes with Deployments, Jobs, Secrets, and Ingress. Includes manifests for the API, Keycloak, UI, and the Config Importer as a Helm hook.
Google Cloud Run
Deploy Openfuse on Google Cloud Run with Cloud SQL, Cloud Run Jobs for the Config Importer, and Cloud Load Balancing for hostname-based routing.