Deploying to AWS
A CloudFormation template that runs OrbisID on ECS Fargate against an RDS PostgreSQL database, pulling the official orbisid/orbisid-backend and orbisid/orbisid-frontend images directly from Docker Hub — no ECR, no image build step.
Quick launch (no domain required)
If you just want to try OrbisID without owning a domain or an ACM certificate, AWS Marketplace also offers OrbisID as a self-contained EC2 AMI with 1-Click Launch: launch the AMI, wait a few minutes, and browse to the URL shown in the instance's system log. It generates its own database password, encryption key, and a self-signed TLS certificate on first boot — no parameters to fill in. The tradeoffs versus the CloudFormation deployment below: a single EC2 instance rather than ECS/RDS, a self-signed certificate (browser warning) instead of a trusted one, and all data lives on that instance's own EBS volume rather than a managed database. Find it on AWS Marketplace as "OrbisID Community Edition — Quick Launch"; the rest of this page covers the production-oriented CloudFormation/ECS path.
Architecture
This mirrors exactly what nginx.conf does for a Docker Compose install (/api/ and /actuator/ to the backend, everything else to the frontend) — just implemented as ALB listener rules instead of an nginx container, so there's no config file to ship into the container image.
Prerequisites
- AWS CLI installed and configured (
aws configure) - A domain name you control
- An ACM certificate for that domain, already issued and validated, in the same region you're deploying to. OrbisID's session cookies are
Secure-only (seeapplication.yml), so HTTPS at the load balancer isn't optional the way it is for a LAN-only Docker Compose install — request the certificate first:
- Linux
- macOS
- Windows
aws acm request-certificate --domain-name orbisid.example.com --validation-method DNS
aws acm request-certificate --domain-name orbisid.example.com --validation-method DNS
aws acm request-certificate --domain-name orbisid.example.com --validation-method DNS
Then add the returned CNAME validation record to your DNS and wait for Status: ISSUED (aws acm describe-certificate --certificate-arn <arn>).
Deploy
Copy the template below and save it locally as cloudformation.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Description: >
OrbisID on AWS — VPC, RDS PostgreSQL, ECS Fargate (backend + frontend services
pulling the official Docker Hub images), and an Application Load Balancer that
path-routes /api/* and /actuator/* to the backend and everything else to the
frontend (the same split docs-site/nginx.conf does for a Docker Compose install).
See docs-site/docs/installation/cloud-aws.md for the full walkthrough.
Parameters:
EnvironmentName:
Type: String
Default: orbisid
Description: Prefix used to name every resource this stack creates.
OrbisIDVersion:
Type: String
Default: latest
Description: >
Tag to pull from orbisid/orbisid-backend and orbisid/orbisid-frontend on
Docker Hub. Pin to a specific released version (e.g. 2.12.2) for production
— "latest" will change under you on the next image push.
CertificateArn:
Type: String
AllowedPattern: '^arn:aws:acm:.*'
Description: >
ACM certificate ARN for your domain, in the same region as this stack.
OrbisID's session cookies are Secure-only, so HTTPS at the load balancer
is required, not optional — request/validate the certificate before
deploying this stack.
DBUsername:
Type: String
Default: orbisid
Description: Master username for the RDS PostgreSQL instance.
DBPassword:
Type: String
NoEcho: true
MinLength: 8
Description: Master password for the RDS PostgreSQL instance.
DBName:
Type: String
Default: orbisid
DBInstanceClass:
Type: String
Default: db.t4g.micro
Description: See requirements.md — 2 vCPU / 8 GB is the recommended baseline for the whole stack; scale this and the Fargate sizes together.
DBAllocatedStorage:
Type: Number
Default: 20
MinValue: 20
DBMultiAZ:
Type: String
Default: 'false'
AllowedValues: ['true', 'false']
Description: Set to true for production high availability (roughly doubles RDS cost).
EncryptionKey:
Type: String
NoEcho: true
Description: >
AES-256-GCM key OrbisID uses to encrypt stored credentials. Generate with
`openssl rand -base64 32`. Keep this safe outside of AWS too — if it is
lost, encrypted data cannot be recovered.
BackendCpu:
Type: Number
Default: 1024
Description: Fargate CPU units for the backend task (1024 = 1 vCPU).
BackendMemory:
Type: Number
Default: 2048
Description: Fargate memory (MiB) for the backend task.
FrontendCpu:
Type: Number
Default: 512
FrontendMemory:
Type: Number
Default: 1024
DesiredCount:
Type: Number
Default: 1
Description: Number of tasks per service. Increase for HA once Application Auto Scaling is wired up (not included in this base template).
VpcCidr:
Type: String
Default: 10.0.0.0/16
Resources:
# ── Networking ────────────────────────────────────────────────────────────
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-vpc'
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-igw'
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-1'
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [1, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-2'
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [2, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-1'
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: !Select [3, !Cidr [!Ref VpcCidr, 4, 8]]
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-2'
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-public-rt'
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
NatEIP:
Type: AWS::EC2::EIP
DependsOn: VPCGatewayAttachment
Properties:
Domain: vpc
# Single NAT Gateway to keep the base cost down — see docs-site/docs/installation/cloud-aws.md
# for how to add a second one (one per AZ) for NAT high availability.
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEIP.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-nat'
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-private-rt'
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
# ── Security Groups ──────────────────────────────────────────────────────
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} ALB - public HTTP/HTTPS'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-alb-sg'
BackendSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} backend tasks - ALB only'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8080
ToPort: 8080
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-backend-sg'
FrontendSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} frontend tasks - ALB only'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3000
ToPort: 3000
SourceSecurityGroupId: !Ref ALBSecurityGroup
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-frontend-sg'
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub '${EnvironmentName} RDS - backend tasks only'
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref BackendSecurityGroup
Tags:
- Key: Name
Value: !Sub '${EnvironmentName}-db-sg'
# ── Secrets ──────────────────────────────────────────────────────────────
DBPasswordSecret:
Type: AWS::SecretsManager::Secret
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
Name: !Sub '${EnvironmentName}/db-password'
SecretString: !Ref DBPassword
# Retained even if the stack is deleted — losing this key makes all encrypted
# credentials in the database unrecoverable, so an accidental stack deletion
# must not take it with it.
EncryptionKeySecret:
Type: AWS::SecretsManager::Secret
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
Name: !Sub '${EnvironmentName}/encryption-key'
SecretString: !Ref EncryptionKey
# ── Database ─────────────────────────────────────────── ──────────────────
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: !Sub '${EnvironmentName} RDS subnet group (private subnets)'
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
RDSInstance:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
DBInstanceIdentifier: !Sub '${EnvironmentName}-postgres'
Engine: postgres
# No EngineVersion pinned deliberately — RDS's default postgres version for
# new instances changes over time and a hardcoded minor version here would
# eventually be rejected as deprecated. Requires PostgreSQL 15+ (see
# requirements.md); check `aws rds describe-db-engine-versions --engine
# postgres` if you need to pin a specific version instead.
DBInstanceClass: !Ref DBInstanceClass
AllocatedStorage: !Ref DBAllocatedStorage
StorageType: gp3
StorageEncrypted: true
MasterUsername: !Ref DBUsername
MasterUserPassword: !Ref DBPassword
DBName: !Ref DBName
DBSubnetGroupName: !Ref DBSubnetGroup
VPCSecurityGroups:
- !Ref DBSecurityGroup
MultiAZ: !Ref DBMultiAZ
PubliclyAccessible: false
BackupRetentionPeriod: 7
CopyTagsToSnapshot: true
# ── ECS ──────────────────────────────────────────────────────────────────
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${EnvironmentName}-cluster'
ClusterSettings:
- Name: containerInsights
Value: enabled
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${EnvironmentName}'
RetentionInDays: 30
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${EnvironmentName}-ecs-execution-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Policies:
- PolicyName: ReadOrbisIDSecrets
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource:
- !Ref DBPasswordSecret
- !Ref EncryptionKeySecret
BackendTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub '${EnvironmentName}-backend'
Cpu: !Ref BackendCpu
Memory: !Ref BackendMemory
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: backend
Image: !Sub 'orbisid/orbisid-backend:${OrbisIDVersion}'
Essential: true
PortMappings:
- ContainerPort: 8080
Environment:
- Name: SPRING_PROFILES_ACTIVE
Value: docker
- Name: SPRING_DATASOURCE_URL
Value: !Sub 'jdbc:postgresql://${RDSInstance.Endpoint.Address}:${RDSInstance.Endpoint.Port}/${DBName}'
- Name: SPRING_DATASOURCE_USERNAME
Value: !Ref DBUsername
Secrets:
- Name: SPRING_DATASOURCE_PASSWORD
ValueFrom: !Ref DBPasswordSecret
- Name: ORBISID_ENCRYPTION_KEY
ValueFrom: !Ref EncryptionKeySecret
HealthCheck:
Command:
- CMD-SHELL
- wget --quiet --tries=1 --spider http://localhost:8080/actuator/health || exit 1
Interval: 30
Timeout: 10
Retries: 3
StartPeriod: 90
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: backend
FrontendTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub '${EnvironmentName}-frontend'
Cpu: !Ref FrontendCpu
Memory: !Ref FrontendMemory
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
ContainerDefinitions:
- Name: frontend
Image: !Sub 'orbisid/orbisid-frontend:${OrbisIDVersion}'
Essential: true
PortMappings:
- ContainerPort: 3000
Environment:
# Frontend calls the API via a relative path — it and the backend
# are served from the same ALB/domain, routed by path (see the
# listener rule below), so no absolute URL is needed here.
- Name: NEXT_PUBLIC_API_URL
Value: ''
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: frontend
BackendService:
Type: AWS::ECS::Service
DependsOn: HTTPSListener
Properties:
ServiceName: !Sub '${EnvironmentName}-backend'
Cluster: !Ref ECSCluster
TaskDefinition: !Ref BackendTaskDefinition
DesiredCount: !Ref DesiredCount
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref BackendSecurityGroup
LoadBalancers:
- ContainerName: backend
ContainerPort: 8080
TargetGroupArn: !Ref BackendTargetGroup
HealthCheckGracePeriodSeconds: 90
FrontendService:
Type: AWS::ECS::Service
DependsOn: HTTPSListener
Properties:
ServiceName: !Sub '${EnvironmentName}-frontend'
Cluster: !Ref ECSCluster
TaskDefinition: !Ref FrontendTaskDefinition
DesiredCount: !Ref DesiredCount
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref FrontendSecurityGroup
LoadBalancers:
- ContainerName: frontend
ContainerPort: 3000
TargetGroupArn: !Ref FrontendTargetGroup
# ── Load Balancer ────────────────────────────────────────────────────────
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${EnvironmentName}-alb'
Scheme: internet-facing
Type: application
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
BackendTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${EnvironmentName}-backend-tg'
Port: 8080
Protocol: HTTP
TargetType: ip
VpcId: !Ref VPC
HealthCheckPath: /actuator/health
HealthCheckIntervalSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Matcher:
HttpCode: '200'
FrontendTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub '${EnvironmentName}-frontend-tg'
Port: 3000
Protocol: HTTP
TargetType: ip
VpcId: !Ref VPC
HealthCheckPath: /
HealthCheckIntervalSeconds: 30
HealthyThresholdCount: 2
UnhealthyThresholdCount: 3
Matcher:
HttpCode: '200-399'
HTTPSListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref CertificateArn
DefaultActions:
- Type: forward
TargetGroupArn: !Ref FrontendTargetGroup
HTTPListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref ApplicationLoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: '443'
StatusCode: HTTP_301
BackendListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref HTTPSListener
Priority: 10
Conditions:
- Field: path-pattern
Values:
- /api/*
- /actuator/*
Actions:
- Type: forward
TargetGroupArn: !Ref BackendTargetGroup
Outputs:
ApplicationURL:
Description: OrbisID URL (point your domain's DNS at LoadBalancerDNSName, then browse here)
Value: !Sub 'https://${ApplicationLoadBalancer.DNSName}'
LoadBalancerDNSName:
Description: Create a CNAME/ALIAS record for your domain pointing at this
Value: !GetAtt ApplicationLoadBalancer.DNSName
DatabaseEndpoint:
Description: RDS PostgreSQL endpoint (private — only reachable from the backend tasks)
Value: !GetAtt RDSInstance.Endpoint.Address
ECSClusterName:
Value: !Ref ECSCluster
VpcId:
Value: !Ref VPC
Generate a database password and an encryption key, then deploy the stack:
- Linux
- macOS
- Windows
DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9')
ENCRYPTION_KEY=$(openssl rand -base64 32)
echo "Save this — it cannot be recovered if lost: $ENCRYPTION_KEY"
aws cloudformation deploy \
--template-file cloudformation.yaml \
--stack-name orbisid \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx \
DBPassword="$DB_PASSWORD" \
EncryptionKey="$ENCRYPTION_KEY"
DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9')
ENCRYPTION_KEY=$(openssl rand -base64 32)
echo "Save this — it cannot be recovered if lost: $ENCRYPTION_KEY"
aws cloudformation deploy \
--template-file cloudformation.yaml \
--stack-name orbisid \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx \
DBPassword="$DB_PASSWORD" \
EncryptionKey="$ENCRYPTION_KEY"
$bytes = New-Object byte[] 24
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
$DB_PASSWORD = [Convert]::ToBase64String($bytes) -replace '[^A-Za-z0-9]', ''
$bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
$ENCRYPTION_KEY = [Convert]::ToBase64String($bytes)
Write-Host "Save this - it cannot be recovered if lost: $ENCRYPTION_KEY"
aws cloudformation deploy `
--template-file cloudformation.yaml `
--stack-name orbisid `
--capabilities CAPABILITY_NAMED_IAM `
--parameter-overrides `
CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx `
DBPassword="$DB_PASSWORD" `
EncryptionKey="$ENCRYPTION_KEY"
Or deploy through the Console instead: CloudFormation > Create stack > With new resources (standard) > Choose an existing template > Upload a template file, select cloudformation.yaml, and fill in the parameters (CertificateArn is required; generate DBPassword/EncryptionKey the same way as above and paste them in).
It takes 10–15 minutes, mostly for the RDS instance and NAT gateway.
Once the stack reaches CREATE_COMPLETE, fetch the outputs:
- Linux
- macOS
- Windows
aws cloudformation describe-stacks --stack-name orbisid --query 'Stacks[0].Outputs' --output table
aws cloudformation describe-stacks --stack-name orbisid --query 'Stacks[0].Outputs' --output table
aws cloudformation describe-stacks --stack-name orbisid --query 'Stacks[0].Outputs' --output table
When it finishes, LoadBalancerDNSName is the output you need next.
Other parameters worth knowing about (all have defaults except CertificateArn):
| Parameter | Default | Purpose |
|---|---|---|
CertificateArn | required | ACM certificate ARN |
EnvironmentName | orbisid | Prefix for every resource name |
OrbisIDVersion | latest | Docker Hub image tag to deploy — pin this to a specific release (e.g. 2.12.2) for production, since latest moves under you |
DBInstanceClass | db.t4g.micro | RDS instance size |
DBMultiAZ | false | Set true for production high availability |
BackendCpu / BackendMemory | 1024 / 2048 | Fargate sizing for the backend task |
DesiredCount | 1 | Tasks per service |
Post-deployment
- Create a CNAME (or ALIAS, if using Route 53) record for your domain pointing at
LoadBalancerDNSName. - Browse to
https://your-domain. Log in with the default credentials (admin/ChangeMe123!) and change the password immediately — Administration > Users. - Add target systems and configure scanning as usual — see Systems and Scanning.
Cost (rough estimate)
With the template's defaults (single AZ pair, one NAT gateway, db.t4g.micro, one task each for backend/frontend, single-AZ RDS): roughly $120–160/month in us-east-1. This is a rough order of magnitude, not a quote — check the AWS Pricing Calculator for your region and traffic. The main levers if you need to cut cost further or scale up: DBInstanceClass, BackendCpu/BackendMemory, DesiredCount, and DBMultiAZ.
Updating
Redeploy with the same parameters plus a new OrbisIDVersion — aws cloudformation deploy requires every parameter without a default (CertificateArn, DBPassword, EncryptionKey) on every call, so keep those three somewhere you can reuse them (a password manager, or a local, git-ignored parameters file):
- Linux
- macOS
- Windows
aws cloudformation deploy \
--template-file cloudformation.yaml \
--stack-name orbisid \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
CertificateArn="$CERTIFICATE_ARN" \
DBPassword="$DB_PASSWORD" \
EncryptionKey="$ENCRYPTION_KEY" \
OrbisIDVersion=2.13.0
aws cloudformation deploy \
--template-file cloudformation.yaml \
--stack-name orbisid \
--capabilities CAPABILITY_NAMED_IAM \
--parameter-overrides \
CertificateArn="$CERTIFICATE_ARN" \
DBPassword="$DB_PASSWORD" \
EncryptionKey="$ENCRYPTION_KEY" \
OrbisIDVersion=2.13.0
aws cloudformation deploy `
--template-file cloudformation.yaml `
--stack-name orbisid `
--capabilities CAPABILITY_NAMED_IAM `
--parameter-overrides `
CertificateArn="$CERTIFICATE_ARN" `
DBPassword="$DB_PASSWORD" `
EncryptionKey="$ENCRYPTION_KEY" `
OrbisIDVersion=2.13.0
aws cloudformation deploy only touches what changed — the RDS instance and its data are untouched by an image version bump. Database migrations run automatically on backend startup (Flyway), same as the Docker Compose install.
Known limitations
- No Kafka. Endpoint Sensor / Threat Detection event ingestion (
KAFKA_BOOTSTRAP_SERVERS) needs a reachable Kafka broker, which this template doesn't provision — same as the released Docker Compose packages, which also ship without one. If you need that feature in this deployment, add a managed Kafka-compatible service (Amazon MSK Serverless is the natural fit in-VPC) and setKAFKA_BOOTSTRAP_SERVERSon the backend task definition. - No bundled AI runtime. This template doesn't run Ollama — AI features stay off. To enable them, either point Administration > Settings > OrbisAI's base URL at a separately-hosted Ollama instance reachable from the backend's private subnet, or extend the template with an additional Fargate service running
ollama/ollamaplus an EFS volume for model storage (CPU inference is memory-hungry — see Requirements). - No autoscaling.
DesiredCountis fixed; wire up Application Auto Scaling on the ECS services if you need it. - Single NAT gateway. A NAT outage in that AZ takes down internet egress for both private subnets. Add a second NAT gateway (one per AZ) for NAT high availability if this matters to you.
Tearing down
- Linux
- macOS
- Windows
aws cloudformation delete-stack --stack-name orbisid --region <region>
aws cloudformation delete-stack --stack-name orbisid --region <region>
aws cloudformation delete-stack --stack-name orbisid --region <region>
The DBPasswordSecret and EncryptionKeySecret Secrets Manager secrets are retained on stack deletion by design (so an accidental delete can't take your encryption key with it) — delete them manually afterwards if you're done with them for good:
- Linux
- macOS
- Windows
aws secretsmanager delete-secret --secret-id orbisid/db-password --force-delete-without-recovery
aws secretsmanager delete-secret --secret-id orbisid/encryption-key --force-delete-without-recovery
aws secretsmanager delete-secret --secret-id orbisid/db-password --force-delete-without-recovery
aws secretsmanager delete-secret --secret-id orbisid/encryption-key --force-delete-without-recovery
aws secretsmanager delete-secret --secret-id orbisid/db-password --force-delete-without-recovery
aws secretsmanager delete-secret --secret-id orbisid/encryption-key --force-delete-without-recovery
--force-delete-without-recovery skips Secrets Manager's recovery window. The RDS instance is deleted with a final snapshot (DeletionPolicy: Snapshot) — the snapshot itself isn't removed automatically and will keep costing storage until you delete it too.
Troubleshooting
- ECS service stuck, tasks cycling: check
aws logs tail /ecs/orbisid --follow— usually a database connectivity issue (security group) or a badENCRYPTION_KEY/DBPasswordvalue. - ALB health checks failing on the backend target group: the backend takes time to run Flyway migrations on first boot; the target group's grace period (90s) usually covers it, but a large migration backlog on first deploy can exceed that — watch the ECS service events in the console.
- 502/504 from the ALB: almost always the frontend or backend task isn't healthy yet, or the security group rule from the ALB to the task's port is missing (shouldn't happen with the template as shipped, but check first if you customised it).