AWS Deployment (ECS Fargate)
This guide deploys a full Bridge cluster on AWS using ECS Fargate (a service that runs containers without you managing servers). One CloudFormation template creates everything: the database, networking, load balancer, and the containers that make up the cluster.
This guide deploys Bridge v3.0.0. Pass ImageUri="britive/bridge:v3.0.0" when you create the stack; latest moves. If you already run a v2 stack, follow Upgrade to v3 first: stop every v2 role before the first v3 task starts, and keep database and recording backups. v2 and v3 roles cannot run side by side.
What CloudFormation does for you. CloudFormation reads a template file and builds all the AWS resources it describes, in the right order. You run one command; AWS does the rest. To remove everything later, you delete the one “stack” it created.
Overview
The template builds a production-shaped cluster:
- An orchestrator that runs migrations, stays in charge, and keeps workers running.
- Proxy containers that accept connections and serve the web app.
- Session containers that the orchestrator spawns and replaces on demand.
- An export task that converts recordings to video on request. It is
optional; set
ExportEnabled=falseto leave it out. - An Amazon RDS PostgreSQL database (the Bridge’s storage).
- Amazon EFS shared file storage for recordings (so any container can read any recording).
- An internal Network Load Balancer (NLB) by default, exposing the web app over HTTPS and the native protocol ports only to the private networks you allow.
You don’t manage the proxy and session containers directly - the orchestrator creates, replaces, and load-balances them for you.
Architecture
This diagram follows the resources and parameters in
bridge-cluster-aws.cfn.yaml. The NLB can be internal or internet-facing, but
the ECS workers always run with private IPs only. The template encrypts the
load-balancer-to-proxy web connection and probes workers over HTTP on port 9091.
Orchestrator control on TCP 9443 and the broker API on loopback port 8081 stay
private.
flowchart LR
Clients["Approved clients<br/>(browser + native tools)"]
Britive["Britive platform"]
Targets["Target networks<br/>(VPC, peering, TGW, VPN, Direct Connect, NAT)"]
subgraph LbSubnets["Load balancer subnets<br/>LoadBalancerSubnetIds"]
NLB["Network Load Balancer<br/>443 + one listener per enabled protocol"]
end
subgraph WorkerSubnets["Private subnets (NAT-routed)<br/>WorkerSubnetIds"]
Proxy["Proxy workers<br/>ECS Fargate, private IPs"]
Session["Session workers<br/>ECS Fargate, private IPs"]
RDS[("RDS PostgreSQL<br/>checkout + session state")]
EFS[("EFS<br/>recordings")]
Export["Export task<br/>video conversion"]
end
subgraph OrchestratorSubnets["Private subnets<br/>WorkerSubnetIds"]
Orchestrator["Orchestrator<br/>+ Britive Broker when BrokerAuthToken is set<br/>ECS Fargate, private IP"]
end
Clients -->|"HTTPS / native protocols"| NLB
NLB -->|"target groups"| Proxy
Proxy -->|"session control + relay"| Session
Session -->|"protocol connection"| Targets
Proxy --> RDS
Session --> RDS
Orchestrator --> RDS
Proxy --> EFS
Session --> EFS
Export --> EFS
Orchestrator -->|"spawn / replace workers"| Proxy
Orchestrator -->|"spawn / replace workers"| Session
Orchestrator <-->|"outbound TLS"| Britive
Orchestrator -->|"Bridge API via NLB 443"| NLB
Private is the default and recommended deployment model. For nearly all real deployments, keep Bridge behind an internal/private NLB reachable only from your corporate network, VPN, zero-trust network, or private connectivity into AWS. Some native protocols can carry credentials or session data in clear text depending on the client and target configuration, so exposing native listeners on an internet-facing load balancer increases the chance of credential exposure.
The template can still create an internet-facing NLB by setting
LoadBalancerScheme=internet-facing and restricting ClientCidr to a narrow,
reviewed client range. Use that mode only when it is an explicit requirement. In
that model, restrict the deployment to browser-only access over HTTPS and do not
expose native cleartext protocol listeners publicly.
Before You Begin
You need:
- An AWS account and the AWS CLI installed and signed in
(
aws sts get-caller-identityshould show your account). - Permission to create IAM roles, ECS, RDS, EFS, and load balancers
(the deploy uses
CAPABILITY_NAMED_IAM). - An existing VPC with three private, NAT-routed subnets for workers, database, and recording storage.
- Network routes from those private worker subnets to every target Bridge should reach. That can be same-VPC routing, VPC peering, Transit Gateway, VPN, Direct Connect, or NAT egress for internet-reachable targets. Target security groups and firewalls must also allow traffic from the Bridge worker security group or worker subnet CIDRs.
- Subnets for the NLB. Use private subnets for the default internal NLB. Use
public subnets only when you intentionally set
LoadBalancerScheme=internet-facing; the workers still run with private IPs only. - An ACM certificate for the hostname you’ll use (for example
bridge.example.com), in the same region. The load balancer presents this for HTTPS. - The CloudFormation template for the Bridge cluster, downloaded here: bridge-cluster-aws.cfn.yaml. Save it locally - you’ll deploy it in Step 2.
You don’t build anything. The Bridge image is published on Docker Hub as
britive/bridge and the AWS configuration is baked into it; the template selects
it automatically. You set per-deployment values through CloudFormation parameters
(no config file to mount).
Deploy the Cluster
Choose the Image
The Bridge image lives on Docker Hub at britive/bridge. For this release,
use the versioned image:
britive/bridge:v3.0.0ECS Fargate can pull this public image directly. Set ImageUri to
britive/bridge:v3.0.0 to pin this release. The template defaults to
britive/bridge:latest when you omit ImageUri.
Optional - mirror to your own ECR. Some organizations prefer to pull from a
private registry (for Docker Hub rate limits or network policy). If so, copy the
image into your ECR and pass that URI as ImageUri:
REGION=us-west-2
ACCOUNT=111122223333
ECR=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/bridge
aws ecr create-repository --repository-name bridge --region us-west-2 || true
aws ecr get-login-password --region us-west-2 \
| docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com
docker pull --platform linux/arm64 britive/bridge:v3.0.0
docker tag britive/bridge:v3.0.0 $ECR:v3.0.0
docker push $ECR:v3.0.0Prepare the Secrets
The cluster needs a few shared secrets. Generate them once and keep them safe - you’ll pass the same values on every future update.
echo "EncryptionKeyB64: $(openssl rand -base64 32)" # encrypts stored credentials
echo "ClusterToken: $(openssl rand -hex 32)" # secures internal cluster calls
echo "HostKeySeed: $(openssl rand -hex 16)" # stable SSH fingerprint across workers
echo "DbPassword: $(openssl rand -hex 16)" # RDS database passwordThe HostKeySeed makes every session worker present the same SSH host key, so users’ SSH clients don’t warn about a changed fingerprint each time a worker is replaced. Keep it constant.
Deploy the CloudFormation Stack
Run this from the folder where you saved bridge-cluster-aws.cfn.yaml, filling in your own values. This single command creates the entire cluster:
aws cloudformation deploy \
--template-file bridge-cluster-aws.cfn.yaml \
--stack-name bridge \
--region us-west-2 \
--s3-bucket <a bucket you own in this region> \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
ImageUri="britive/bridge:v3.0.0" \
VpcId="vpc-xxxxxxxx" \
WorkerSubnetIds="subnet-private-a,subnet-private-b,subnet-private-c" \
LoadBalancerSubnetIds="subnet-lb-a,subnet-lb-b,subnet-lb-c" \
NatSubnetId="subnet-public-a" \
CertificateArn="arn:aws:acm:us-west-2:111122223333:certificate/xxxx" \
ClientCidr="10.20.0.0/16" \
BridgeUrl="https://bridge.example.com" \
BritiveTenant="https://acme.britive-app.com"That is the whole command. The database password, the cluster token, the encryption key and the SSH host-key seed are generated into Secrets Manager, so there is nothing to invent and nothing to store.
--s3-bucket is required, not optional: the template is larger than
CloudFormation’s 51,200-byte limit for an inline body, so the CLI has to stage it
somewhere. Any bucket you own in the same region will do. The console needs no
bucket, because it stages the upload itself.
Telnet and NETCONF listen on 2323 and 2830 here, not 23 and 830. The container
runs as an unprivileged user, and Fargate does not grant it the capability to bind
a port below 1024 — nor will it: capabilities.add on Fargate accepts only
SYS_PTRACE. So the cluster configs move both, exactly as SSH moves from 22 to
2222. The load balancer listens on the same ports, so this is the port your clients
connect to. A single-container deployment keeps 23 and 830, because Docker does
grant that capability.
CAPABILITY_IAM is sufficient — the template names none of its roles, so
CAPABILITY_NAMED_IAM is not required.
The command pins ImageUri to britive/bridge:v3.0.0. If you mirrored the image,
replace that value with <your-ecr-uri>:v3.0.0. Omitting ImageUri uses the
template’s default, britive/bridge:latest.
LoadBalancerScheme defaults to internal, so you may omit it from the command
if you are using the recommended private deployment. To create an internet-facing
load balancer, set LoadBalancerScheme="internet-facing", set
LoadBalancerSubnetIds to public subnets, and keep SubnetIds as private,
NAT-routed worker/data subnets. Keep ClientCidr limited to the smallest
reviewed client range.
Parameter Reference
| Parameter | Required | Description |
|---|---|---|
VpcId | Yes | The VPC to deploy into. |
WorkerSubnetIds | Yes | Three private subnets. Everything runs here: the orchestrator, the broker, the spawned workers, the database, and the file storage. They get no public IP and reach the internet through the stack’s NAT gateway. They must also route to the targets Bridge connects to, and those targets must allow the worker security group. |
LoadBalancerSubnetIds | Yes | Subnets for the NLB. Private for the default internal scheme; public only with LoadBalancerScheme=internet-facing. |
NatSubnetId | Yes | A public (internet-gateway routed) subnet to host the NAT gateway. It must not be one of WorkerSubnetIds: those route through this NAT, so a NAT inside one of them would have no path out. |
CertificateArn | Yes | ACM certificate for the HTTPS (443) listener. |
ClientCidr | Yes | The only network range allowed to reach the load balancer. For an internal NLB, your routed private or VPN range. For internet-facing, a narrow office or VPN egress CIDR — never a broad internet range with native protocol listeners enabled. |
LoadBalancerScheme | No | Defaults to internal. Use internet-facing only for a reviewed public endpoint, paired with a narrow ClientCidr. |
ImageUri | No | Defaults to britive/bridge:latest. Set britive/bridge:v3.0.0 to pin this release, or use the equivalent URI in your mirrored registry. |
BridgeUrl | No | The URL users reach Bridge at. Used for the OAuth redirect, so it must match your certificate and what Britive has registered. |
BritiveTenant | For Britive login | Your Britive tenant. Empty means Bridge starts and serves sessions but Login with Britive is not offered — see the warning below, which also covers what else changes when you set it. |
BrokerAuthToken | No | Britive Broker pool token. Empty disables the broker. Setting it also lets Bridge retrieve its own license. |
BrokerTenantSubdomain | No | Tenant subdomain the broker registers with. Not a credential. |
BrokerAutoUpdate | No | true fetches the newest Britive Broker from download.britive.com when an orchestrator task starts and runs it. The broker bundled in the image runs if the fetch fails. Default false. Needs HTTPS egress to download.britive.com. |
ExportEnabled | No | Default true: one export task converts recordings to video on request. false omits the task; browser playback does not need it. |
NatEipAllocationId | No | Leave empty and the stack allocates an Elastic IP for the NAT. Supply an eipalloc-… only if the account is at its EIP quota. |
Credentials
None of these is required. Leave one empty and the stack generates it into AWS Secrets Manager; fill one in only to reuse a value a previous deployment already established.
| Parameter | Fill in when | Description |
|---|---|---|
DbPassword | Never, normally | Master password for the RDS database. |
HostKeySeed | Rebuilding a deployment | Seed for the session workers’ SSH host key. Reuse the old value to keep the fingerprint clients already trust; a new one makes every SSH client warn. |
ClusterToken | Rebuilding a deployment | Shared secret for calls between cluster containers. |
EncryptionKeyB64 | Rebuilding a deployment | Key that encrypts stored checkout credentials. Supply the value the old deployment held. Filling this in later, on a stack that generated its own, replaces the key and makes every stored credential unreadable. |
BridgeLicense | No broker token | A signed offline license. Leave empty when BrokerAuthToken is set: Bridge fetches one itself. A license is bound to one tenant and must match BritiveTenant. |
LdapBindPassword | Using LDAP login | Password for the LDAP bind DN. |
AuditExportToken | Streaming to a SIEM | Bearer token for the export destination. |
SshProvisionKeyPem | The broker provisions Linux targets | Privileged SSH key the broker uses. |
No credential is passed to a container as an environment variable. Each one
gets a Secrets Manager secret, and the task definition carries only its ARN — the
ECS agent fetches the value when the container starts. That matters because a task
definition is readable by anyone with ecs:DescribeTaskDefinition, so a credential
placed there is a credential published to every such reader.
Network egress. The stack provisions its own NAT gateway (with an Elastic IP), a
private route table, and an S3 gateway endpoint, so the private worker/orchestrator
subnets reach ECR, S3, CloudWatch Logs, the AWS APIs, and the Britive platform without
you wiring egress yourself. You only supply a public subnet for the NAT
(NatSubnetId) and private subnets for the workers (WorkerSubnetIds). If a worker
subnet can’t reach the internet, tasks fail to start with an ECR GetAuthorizationToken … i/o timeout — check the NAT is healthy and the private route table points 0.0.0.0/0
at it. The NAT’s Elastic IP is exported as the NatEgressIp stack output; allowlist it
on any target that filters by source IP.
The deploy takes several minutes. CloudFormation prints progress and finishes
with Successfully created/updated stack.
Point DNS at the Load Balancer
Get the load balancer’s DNS name from the stack outputs:
aws cloudformation describe-stacks --stack-name bridge --region us-west-2 \
--query 'Stacks[0].Outputs' --output tableCreate a DNS record (a CNAME, or an Alias record in Route 53) for your hostname
(bridge.example.com) pointing at that load balancer DNS name. For the default
internal NLB, publish this record in private DNS or another DNS zone resolvable
only from your approved networks. Your certificate’s hostname must match.
Verify
Check the cluster is running
All containers should be on your new image:
aws ecs list-tasks --cluster bridge --region us-west-2 # the cluster is named after the stackYou should see an orchestrator, one or more proxies, and one or more session
workers, all RUNNING.
Confirm the orchestrator started cleanly and the license loaded
Tail its logs:
aws logs tail /bridge/<stack-name> --follow --region us-west-2Look for log lines showing the database connected, migrations applied, and the
license status (operational=true) if you’ve installed one.
Open the web app
Visit your hostname (https://bridge.example.com/) and log
in.
Updating the Cluster Later
Upgrading to v2.2.0 from an earlier release: set BritiveTenant and use
BridgeUrl in the same update.
Before v2.2.0 the Britive tenant, the OAuth redirect and the license were part of the
image, which meant every deployment built from one image authenticated against the same
Britive instance. They are now supplied by the stack. An update that takes the v2.2.0
image without setting BritiveTenant succeeds and the cluster comes up healthy, but no
tenant is configured anywhere, so Login with Britive disappears from the web app.
The v2.2.0 template also renames the CloudFormation parameter BridgeApiUrl to
BridgeUrl. Replace the old name in your deployment command. BridgeUrl is the
public browser address used for the OAuth callback. It is separate from
BRIDGE_BROKER_API_URL, which the co-located broker uses to call Bridge over loopback.
Review the BridgeConfig protocol switches and load balancer listeners before deployment. Oracle and VNC beta are disabled by default and are for development and testing only. Enable only the protocols you intend to expose.
VNC browser access uses the existing HTTPS listener when enabled in BridgeConfig. Native VNC also needs mounted certificate files and a load balancer listener with matching target-port routing, normally port 5900. The published template does not expose a VNC native listener by default. See VNC beta for viewer and target requirements.
A configuration change reaches warm workers too. The stack keeps Bridge’s
configuration in a Secrets Manager secret, which a task reads when it starts.
Changing a parameter used to build the configuration, such as BritiveTenant or
BridgeUrl, produces new task definition revisions, and the orchestrator service
rolls onto them straight away.
Session and proxy workers follow. The orchestrator compares the task definition each worker was created from against the one it should be using now, so a new revision cycles warm workers whether or not the image changed. You do not need to stop them by hand.
To move to a newer Bridge release, re-run the same deploy command with the new
version in ImageUri (for example ImageUri="britive/bridge:v3.0.0", or your
mirrored ECR equivalent). Pass every other parameter unchanged.
The orchestrator handles the rollout for you: it cycles the proxy containers onto the new image and replaces idle session workers, with no manual container restarts. See Operations for more on upgrades.
If you use the lower-level update-stack command instead of deploy, pass
UsePreviousValue=true for every secret parameter so you don’t have to re-enter
them.
Troubleshoot
The orchestrator waits for PostgreSQL during startup in v3.0.1 and later. A datastore not reachable yet, retrying log means startup is waiting, not that Bridge has exited. Check database reachability and status. Rejected credentials or a missing database still stop startup. See Startup recovery.
| Symptom | Likely Cause | Fix |
|---|---|---|
| Stack fails to create | Missing IAM capability, or a parameter (subnet, cert) is wrong | Read the failure reason in the CloudFormation events; confirm the VPC, subnets, and certificate exist in the same region. |
| Containers crash on start | Invalid configuration, malformed secrets, rejected database credentials, or a missing database. v3.0.0 also exits when RDS is unreachable. | Check /bridge/<stack-name> logs; confirm the database came up and EncryptionKeyB64 is valid base64. |
| Sessions fail to connect to targets | Worker subnets cannot route to the target, or target firewalls block the worker source | Confirm the private worker subnets have a route to the target network and the target allows inbound traffic from TaskSg or the worker subnet CIDRs. |
| Can’t reach the web app | ClientCidr doesn’t include your address, private routing is missing, or DNS not set | Confirm your client or VPN network is within ClientCidr; verify private DNS and routing to the internal load balancer. |
| Footer shows “Unlicensed” / native protocols unavailable | No valid license installed (running in limited mode) | Install a license in Admin → License, or bake one into the config - see Licensing. |
| No “Login with Britive” button, or login fails to authenticate | BritiveTenant is empty or does not match the tenant your users belong to | Set BritiveTenant on the stack and update. Confirm the tenant hostname is exact; the redirect Bridge sends is derived from BridgeUrl, so that must be the URL users actually reach it at. |
| Old version still serving | ImageUri unchanged, or a moving tag was cached | Use a specific version tag (for example britive/bridge:v3.0.0) in ImageUri and redeploy. |
Next Steps
- Configure authentication
- Install and manage your license.
- Review protocols, recording, operations for the Admin console, upgrades, and backups.