Skip to content
AWS Deployment (ECS Fargate)

AWS Deployment (ECS Fargate)

This guide deploys the Gateway on ECS Fargate (containers without servers to manage) behind an Application Load Balancer, using one CloudFormation template.

Because all Gateway state lives in Postgres, the tasks need no storage, no stickiness, and no ordering - this is an ordinary horizontally-scaled web service.

Overview

The template creates:

  • An Application Load Balancer, internal by default, with an HTTPS listener.
  • An ECS cluster and a Fargate service running N Gateway tasks.
  • A target group health-checking /healthz, with stickiness off.
  • Security groups so only the load balancer can reach the tasks.
  • A CloudWatch log group for container output.
  • IAM roles - one to pull the image and read your secrets, one for the task itself.

It deliberately does not create:

Not createdWhy
A VPC or subnetsYou pass existing ones.
A databaseThe Gateway needs Postgres 14+ that you operate. RDS is the expected choice.
The secretsYou create them first, so no credential is ever a CloudFormation parameter in plaintext.
A DNS recordPoint your hostname at the load balancer, then name that hostname in the pool’s Public base URL setting.

Architecture

    flowchart LR
  Clients["MCP clients and agents"]
  Britive["Britive tenant"]
  Backends["Backend MCP servers"]

  subgraph LbSubnets["Load balancer subnets"]
    ALB["Application Load Balancer<br/>HTTPS 443"]
  end

  subgraph TaskSubnets["Private task subnets"]
    T1["Gateway task"]
    T2["Gateway task"]
  end

  RDS[("Postgres<br/>(yours)")]

  Clients -->|"HTTPS"| ALB
  ALB -->|"HTTP 8080"| T1
  ALB -->|"HTTP 8080"| T2
  T1 --> RDS
  T2 --> RDS
  T1 -->|"outbound via NAT"| Britive
  T1 -->|"outbound"| Backends
  

Before You Begin

You need:

  • An existing VPC with two or more subnets for the load balancer, and private subnets for the tasks. The task subnets need outbound access to your Britive tenant and your backend MCP servers, via NAT or VPC endpoints.
  • A Postgres 14+ database the tasks can reach.
  • An ACM certificate in the same region, for the HTTPS listener.
  • A Britive tenant and pool token.
  • Permission to create IAM roles, ECS resources, and load balancers.

Deploy

Download the template

Download mcp-gateway-aws.cfn.yaml - CloudFormation for an ALB plus an ECS Fargate service.

Create the two secrets

The template reads credentials from Secrets Manager rather than taking them as parameters, so they never appear in the stack’s inputs or events.

aws secretsmanager create-secret \
  --name britive/mcp-gateway/pool-token \
  --secret-string '<your gateway pool token>'

aws secretsmanager create-secret \
  --name britive/mcp-gateway/database-url \
  --secret-string 'postgresql://gateway:<password>@your-db-host:5432/gateway'

Note the two ARNs it returns.

There is no master secret to create or rotate. The key that protects stored tokens and sessions is minted by the platform for the gateway pool and delivered with the pool’s settings, so every task agrees on it and it survives every deployment.

Deploy the stack

aws cloudformation deploy \
  --template-file mcp-gateway-aws.cfn.yaml \
  --stack-name britive-mcp-gateway \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides \
      Tenant=acme \
      GatewayPoolTokenSecretArn=<arn> \
      DatabaseUrlSecretArn=<arn> \
      VpcId=vpc-xxxxxxxx \
      LoadBalancerSubnetIds=subnet-aaa,subnet-bbb \
      ServiceSubnetIds=subnet-ccc,subnet-ddd \
      CertificateArn=<acm-arn> \
      AllowedClientCidr=10.0.0.0/8

CAPABILITY_IAM is required because the template creates the two task roles.

Point DNS at the load balancer

aws cloudformation describe-stacks \
  --stack-name britive-mcp-gateway \
  --query 'Stacks[0].Outputs'

Create a DNS record for the hostname clients will use, pointing at LoadBalancerDnsName.

Set the public base URL on the pool

In the Britive tenant portal, open the gateway pool your token belongs to and set Public base URL to that hostname:

https://mcp-gateway.example.internal

It is a pool setting rather than a stack parameter, so the running tasks pick it up on their next sync - within five minutes by default - with no redeploy.

It must match the hostname clients actually use, because OAuth redirect and resource URLs are built from it. If DNS and this value disagree, sign-in fails at the callback. Leave it unset and the tasks fall back to http://localhost:8080 with a warning, which no client can complete a sign-in against.

Confirm the service is healthy

aws ecs describe-services \
  --cluster britive-mcp-gateway \
  --services <service-name> \
  --query 'services[0].{running:runningCount,desired:desiredCount}'

Then, from inside the VPC (or through your VPN if the load balancer is internal):

curl -s https://mcp-gateway.example.internal/healthz

Verify

  • Both tasks show as healthy in the target group.
  • GET /healthz returns 200 through the load balancer.
  • Signing in at the Gateway’s root URL reaches the admin console.
  • Restarting one task leaves you signed in - proof the tasks share state through Postgres.
  • Container logs appear in the CloudWatch log group named in the stack outputs.

Things the Template Sets Deliberately

Two settings exist because the defaults would break real usage.

A 300-second load balancer idle timeout. A Britive checkout that requires human approval can outlast the default 60 seconds, and the connection being cut is what fails the tool call. This is an ingress timeout to raise, not a Gateway defect.

Stickiness off, MinimumHealthyPercent 100. Any task can serve any request, so there is nothing to pin a client to, and a rolling deployment keeps full capacity while it rotates tasks.

Scaling and Upgrades

Change the replica count:

aws cloudformation deploy --template-file mcp-gateway-aws.cfn.yaml \
  --stack-name britive-mcp-gateway \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides DesiredCount=4 <plus your other parameters>

To upgrade the image, force a new deployment - the tasks pull the current image and run migrations on startup:

aws ecs update-service \
  --cluster britive-mcp-gateway \
  --service <service-name> \
  --force-new-deployment

For a repeatable deployment, pin ImageUri to a specific tag rather than latest, so what runs is what you chose.

Troubleshoot

SymptomCauseFix
Tasks start then stopMissing or unreadable secret, or an unreachable databaseCheck the CloudWatch log group; the Gateway names configuration problems explicitly.
Task cannot reach the tenantNo NAT or VPC endpoint from the task subnetsGive the private subnets outbound access.
Target group never healthyHealth check path or port wrong, or the security group blocks the load balancerThe template sets /healthz on 8080; confirm nothing overrode it.
ResourceInitializationError reading secretsExecution role cannot read the ARNs you passedConfirm the ARNs are in the same account and region.
Tasks exit naming the tenantThe tasks cannot reach your tenant, or the pool token was rejectedFetching settings is fatal at startup by design. Check NAT or VPC endpoints from the service subnets, then the token.
OAuth fails at the callbackThe pool’s Public base URL doesn’t match DNSMake them agree in the tenant portal.
A settings change hasn’t taken effectThe next sync hasn’t run, or the last one failedThe console’s Settings tab shows what each task is running and flags stale settings.
Approval-gated calls time outIdle timeout lowered below the approval windowRaise it on the load balancer.

Next Steps

Last updated on