AWSTemplateFormatVersion: "2010-09-09"

# =============================================================================
# Britive MCP Gateway — AWS ECS Fargate deployment
# =============================================================================
# Creates an Application Load Balancer and an ECS Fargate service running N
# stateless Gateway tasks. 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.
#
# WHAT THIS TEMPLATE DOES NOT CREATE ----------------------------------------
#   * A VPC or subnets. Pass existing ones.
#   * A database. The Gateway needs Postgres 14+ that you operate; RDS is the
#     expected choice. Pass its connection string as a Secrets Manager secret.
#   * The two secrets. Create them first (see below) and pass their ARNs, so no
#     credential is ever a CloudFormation parameter in plaintext.
#   * A DNS record. Point your hostname at the load balancer's DNS name.
#
# CREATE THE SECRETS FIRST ---------------------------------------------------
#   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:pw@db.internal:5432/gateway'
#
# THEN SET THE SETTINGS ON THE GATEWAY POOL ---------------------------------
# The tasks take their entire configuration from the gateway pool the token
# belongs to, fetched from your tenant at startup and re-fetched on a poll. In
# the Britive tenant portal, open that pool and set "Public base URL" to the
# hostname you point at the load balancer — OAuth redirect and resource URLs are
# built from it, so it must match what the browser reaches. Audit sinks,
# inspection, rate limits and backend policy live there too, and a change reaches
# a running task within five minutes with no redeploy.
#
# There is no master secret to create. The key protecting stored tokens and
# sessions is minted by the platform per pool and delivered with the settings, so
# every task agrees on it and it survives every deployment.
# =============================================================================

Description: Britive MCP Gateway on ECS Fargate behind an Application Load Balancer.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: { default: Britive tenant }
        Parameters: [Tenant, GatewayPoolTokenSecretArn]
      - Label: { default: Database }
        Parameters: [DatabaseUrlSecretArn]
      - Label: { default: Networking }
        Parameters: [VpcId, LoadBalancerSubnetIds, ServiceSubnetIds, InternetFacing, AllowedClientCidr]
      - Label: { default: Endpoint }
        Parameters: [CertificateArn]
      - Label: { default: Service }
        Parameters: [ImageUri, DesiredCount, TaskCpu, TaskMemory, LogRetentionDays]
      - Label: { default: Logging }
        Parameters: [LogRetentionDays, LogLevel]

Parameters:
  Tenant:
    Type: String
    Description: Britive tenant - a bare subdomain such as "acme", or a full https:// URL.

  GatewayPoolTokenSecretArn:
    Type: String
    Description: >-
      Secrets Manager ARN holding the gateway pool token. It authenticates the
      tasks to the platform and decides which gateway pool - and so which
      settings - they run with.

  DatabaseUrlSecretArn:
    Type: String
    Description: >-
      Secrets Manager ARN holding the Postgres connection string, e.g.
      postgresql://user:pass@host:5432/gateway

  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: VPC to deploy into.

  LoadBalancerSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Two or more subnets for the load balancer.

  ServiceSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: >-
      Private subnets for the Fargate tasks. They need outbound access to your
      Britive tenant and to your backend MCP servers, via NAT or VPC endpoints.

  InternetFacing:
    Type: String
    Default: "false"
    AllowedValues: ["true", "false"]
    Description: >-
      Whether the load balancer is internet-facing. Keep it internal unless
      clients genuinely reach the Gateway from the public internet.

  AllowedClientCidr:
    Type: String
    Default: 10.0.0.0/8
    Description: CIDR permitted to reach the load balancer on 443.

  CertificateArn:
    Type: String
    Description: >-
      ACM certificate ARN for the HTTPS listener, in this region. TLS is
      required: OAuth tokens and checked-out credentials cross this connection.

  ImageUri:
    Type: String
    Default: britive/mcp-gateway:latest
    Description: Container image to run.

  DesiredCount:
    Type: Number
    Default: 2
    MinValue: 1
    Description: Number of Gateway tasks. Two for availability rather than throughput.

  TaskCpu:
    Type: String
    Default: "512"
    AllowedValues: ["256", "512", "1024", "2048"]

  TaskMemory:
    Type: String
    Default: "1024"
    AllowedValues: ["512", "1024", "2048", "4096"]

  LogRetentionDays:
    Type: Number
    Default: 30
    Description: >-
      CloudWatch retention for container logs. Note this is the Gateway's stdout,
      not its audit trail, which lives in Postgres.

  LogLevel:
    Type: String
    Default: info
    AllowedValues: [debug, info, warning, error]
    Description: >-
      Log level during startup, before the pool's settings arrive. The pool's
      own "Log level" takes over once they do.

Conditions:
  IsInternetFacing: !Equals [!Ref InternetFacing, "true"]

Resources:
  # --- networking ------------------------------------------------------------
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Britive MCP Gateway load balancer
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref AllowedClientCidr
          Description: HTTPS from approved clients
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-alb

  ServiceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Britive MCP Gateway tasks
      VpcId: !Ref VpcId
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-service

  # Only the load balancer may reach the tasks.
  ServiceIngressFromLoadBalancer:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref ServiceSecurityGroup
      IpProtocol: tcp
      FromPort: 8080
      ToPort: 8080
      SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
      Description: Gateway HTTP from the load balancer

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: application
      Scheme: !If [IsInternetFacing, internet-facing, internal]
      Subnets: !Ref LoadBalancerSubnetIds
      SecurityGroups: [!Ref LoadBalancerSecurityGroup]
      LoadBalancerAttributes:
        # A Britive checkout that needs human approval can outlast a default 60s
        # idle timeout, and the connection being cut is what fails the tool call.
        - Key: idle_timeout.timeout_seconds
          Value: "300"

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VpcId
      Port: 8080
      Protocol: HTTP
      TargetType: ip
      HealthCheckPath: /healthz
      HealthCheckIntervalSeconds: 15
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3
      TargetGroupAttributes:
        # No stickiness: every task shares one database, so any task can serve
        # any request.
        - Key: stickiness.enabled
          Value: "false"
        - Key: deregistration_delay.timeout_seconds
          Value: "30"

  HttpsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 443
      Protocol: HTTPS
      Certificates:
        - CertificateArn: !Ref CertificateArn
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

  # --- logging and identity --------------------------------------------------
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /britive/${AWS::StackName}
      RetentionInDays: !Ref LogRetentionDays

  # Pulls the image and reads the secrets at task start.
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      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: read-gateway-secrets
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  - !Ref GatewayPoolTokenSecretArn
                  - !Ref DatabaseUrlSecretArn

  # The Gateway calls your tenant and your backends over the network and needs
  # no AWS API access of its own, so this role stays empty.
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole

  # --- the service -----------------------------------------------------------
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${AWS::StackName}
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${AWS::StackName}
      Cpu: !Ref TaskCpu
      Memory: !Ref TaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities: [FARGATE]
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      TaskRoleArn: !GetAtt TaskRole.Arn
      ContainerDefinitions:
        - Name: gateway
          Image: !Ref ImageUri
          Essential: true
          PortMappings:
            - ContainerPort: 8080
              Protocol: tcp
          # Three values, and every other setting comes from the gateway pool
          # on your Britive tenant.
          Environment:
            - Name: TENANT
              Value: !Ref Tenant
            - Name: LOG_LEVEL
              Value: !Ref LogLevel
          Secrets:
            - Name: GATEWAY_POOL_TOKEN
              ValueFrom: !Ref GatewayPoolTokenSecretArn
            - Name: DATABASE_URL
              ValueFrom: !Ref DatabaseUrlSecretArn
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: gateway

  Service:
    Type: AWS::ECS::Service
    DependsOn: HttpsListener
    Properties:
      Cluster: !Ref Cluster
      TaskDefinition: !Ref TaskDefinition
      DesiredCount: !Ref DesiredCount
      LaunchType: FARGATE
      # Schema migrations run at startup, so a rolling deployment needs no
      # migration step. minimumPercent 100 keeps capacity during the roll.
      DeploymentConfiguration:
        MinimumHealthyPercent: 100
        MaximumPercent: 200
      HealthCheckGracePeriodSeconds: 60
      NetworkConfiguration:
        AwsvpcConfiguration:
          Subnets: !Ref ServiceSubnetIds
          SecurityGroups: [!Ref ServiceSecurityGroup]
          AssignPublicIp: DISABLED
      LoadBalancers:
        - ContainerName: gateway
          ContainerPort: 8080
          TargetGroupArn: !Ref TargetGroup

Outputs:
  LoadBalancerDnsName:
    Description: >-
      Point your hostname at this, then set "Public base URL" on the gateway pool
      in the Britive tenant portal to https://<that hostname>.
    Value: !GetAtt LoadBalancer.DNSName

  McpEndpoint:
    Description: >-
      Where MCP clients connect, once DNS points at the load balancer and the
      pool's "Public base URL" names the same hostname.
    Value: !Sub https://${LoadBalancer.DNSName}/mcp

  ClusterName:
    Value: !Ref Cluster

  LogGroupName:
    Value: !Ref LogGroup
