AWSTemplateFormatVersion: "2010-09-09"
Description: >-
  Bedrock Access Gateway (Rust) - OpenAI-compatible RESTful APIs for Amazon
  Bedrock, deployed as an ECS/Fargate service behind an internet-facing
  Application Load Balancer. Supports one-click network creation (a new VPC with
  two public subnets) or bring-your-own existing VPC and subnets. Deploy in any
  AWS commercial region where Amazon Bedrock is available.

Metadata:
  cfn-lint:
    config:
      ignore_checks:
        # VpcId / Subnets carry blank defaults for one-click mode (CreateNetwork=true),
        # where the network is created and these params are intentionally unused. cfn-lint
        # resolves the empty defaults in the CreateNet=false branch of each Fn::If and warns
        # they are not valid vpc-/subnet- ids; that only matters in bring-your-own mode where
        # the operator supplies real ids (validated at deploy time by the AWS::EC2 types the
        # console prompt enforces). These W1030 findings are expected for the dual-mode design.
        - W1030
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Networking
        Parameters:
          - CreateNetwork
          - VpcCidr
          - VpcId
          - Subnets
      - Label:
          default: Gateway
        Parameters:
          - ContainerImageUri
          - ApiKeySecretArn
          - DisableMantle
          - BedrockApiKeySecretArn
          - DefaultModelId
          - DefaultEmbeddingModelId
          - EnablePromptCaching
          - LogLevel
          - DesiredCount

Parameters:
  ContainerImageUri:
    Type: String
    Default: docker.io/sunerpy/bedrock-gateway-rust:latest
    Description: >-
      Container image for the gateway. Defaults to the published Docker Hub image
      (sunerpy/bedrock-gateway-rust). To use your own build, push the repository
      root Dockerfile image to ECR and pass its URI (e.g.,
      123456789012.dkr.ecr.us-east-1.amazonaws.com/bedrock-gateway:latest).
  ApiKeySecretArn:
    Type: String
    Description: >-
      The Secrets Manager secret ARN holding the API key (key "api_key") used to
      authenticate clients of the gateway.
    AllowedPattern: ^arn:aws[a-zA-Z-]*:secretsmanager:.*$
  DisableMantle:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      "true" (default): skip the GPT-5.x mantle backend startup validation so the
      default published image boots without a Bedrock API key (SigV4-only). Set to
      "false" only if you also provide BedrockApiKeySecretArn to enable GPT-5.x
      (gpt-5.4 / gpt-5.5) via the bedrock-mantle upstream.
  BedrockApiKeySecretArn:
    Type: String
    Default: ""
    Description: >-
      (Optional) Secrets Manager secret ARN whose "bedrock_api_key" field holds a
      Bedrock API key (bearer token) for the GPT-5.x mantle backend. Leave blank
      for SigV4-only deployments (the task role already grants Bedrock access).
  DefaultModelId:
    Type: String
    Default: ""
    Description: >-
      (Optional) The default Bedrock model ID. Leave blank to use the gateway
      built-in default. Make sure the model ID is available in this region.
  DefaultEmbeddingModelId:
    Type: String
    Default: ""
    Description: >-
      (Optional) The default embedding model ID. Leave blank to use the gateway
      built-in default (cohere.embed-multilingual-v3).
  EnablePromptCaching:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      Auto-inject prompt cache points for Claude and Nova models. On by default.
  LogLevel:
    Type: String
    Default: info
    AllowedValues:
      - trace
      - debug
      - info
      - warn
      - error
    Description: Log verbosity for the gateway.
  CreateNetwork:
    Type: String
    Default: "true"
    AllowedValues:
      - "true"
      - "false"
    Description: >-
      "true" (one-click): create a new VPC with two public subnets, an internet
      gateway, and public routing - no existing network needed. "false"
      (bring-your-own): reuse an existing network - VpcId and Subnets become
      required in that case.
  VpcCidr:
    Type: String
    Default: 10.240.0.0/16
    Description: >-
      CIDR block for the VPC created in one-click mode (CreateNetwork=true).
      Ignored when CreateNetwork=false. Two /24 public subnets are carved from
      this block.
  VpcId:
    Type: String
    Default: ""
    Description: >-
      Existing VPC ID for the load balancer and Fargate tasks. Required only when
      CreateNetwork=false; leave blank in one-click mode.
  Subnets:
    Type: CommaDelimitedList
    Default: ""
    Description: >-
      At least two existing subnets (in different AZs) for both the ALB and the
      Fargate tasks. Required only when CreateNetwork=false; leave blank in
      one-click mode. Use public subnets, or private subnets with a NAT gateway
      for ECR/Bedrock/Secrets Manager egress.
  DesiredCount:
    Type: Number
    Default: 2
    MinValue: 1
    Description: The desired number of running Fargate tasks.

Conditions:
  HasDefaultModelId: !Not [!Equals [!Ref DefaultModelId, ""]]
  HasDefaultEmbeddingModelId: !Not [!Equals [!Ref DefaultEmbeddingModelId, ""]]
  HasBedrockApiKey: !Not [!Equals [!Ref BedrockApiKeySecretArn, ""]]
  CreateNet: !Equals [!Ref CreateNetwork, "true"]
  UseExistingNet: !Not [!Condition CreateNet]

Resources:
  # ----------------------------------------------------------------
  # One-click networking (created only when CreateNetwork=true)
  # ----------------------------------------------------------------
  Vpc:
    Type: AWS::EC2::VPC
    Condition: CreateNet
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Condition: CreateNet

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Condition: CreateNet
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Condition: CreateNet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: !Select [0, !GetAZs ""]
      CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 4, 8]]
      MapPublicIpOnLaunch: true

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Condition: CreateNet
    Properties:
      VpcId: !Ref Vpc
      AvailabilityZone: !Select [1, !GetAZs ""]
      CidrBlock: !Select [1, !Cidr [!Ref VpcCidr, 4, 8]]
      MapPublicIpOnLaunch: true

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Condition: CreateNet
    Properties:
      VpcId: !Ref Vpc

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    Condition: CreateNet
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: CreateNet
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: CreateNet
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

  # ----------------------------------------------------------------
  # CloudWatch Logs
  # ----------------------------------------------------------------
  GatewayLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 30

  # ----------------------------------------------------------------
  # IAM: Task execution role (ECR pull + CloudWatch logs + secret read)
  # ----------------------------------------------------------------
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: SecretReadForContainer
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource:
                  - !Ref ApiKeySecretArn
                  - !If [HasBedrockApiKey, !Ref BedrockApiKeySecretArn, !Ref AWS::NoValue]

  # ----------------------------------------------------------------
  # IAM: Task role (least-privilege Bedrock + secret read)
  # ----------------------------------------------------------------
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: BedrockGatewayTaskPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: BedrockList
                Effect: Allow
                Action:
                  - bedrock:ListFoundationModels
                  - bedrock:ListInferenceProfiles
                Resource: "*"
              - Sid: BedrockInvoke
                Effect: Allow
                Action:
                  - bedrock:InvokeModel
                  - bedrock:InvokeModelWithResponseStream
                Resource:
                  - !Sub arn:${AWS::Partition}:bedrock:*::foundation-model/*
                  - !Sub arn:${AWS::Partition}:bedrock:*:${AWS::AccountId}:inference-profile/*
                  - !Sub arn:${AWS::Partition}:bedrock:*:${AWS::AccountId}:application-inference-profile/*
              - Sid: SecretRead
                Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource:
                  - !Ref ApiKeySecretArn
                  - !If [HasBedrockApiKey, !Ref BedrockApiKeySecretArn, !Ref AWS::NoValue]

  # ----------------------------------------------------------------
  # Security Groups
  # ----------------------------------------------------------------
  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Bedrock Gateway ALB - allow inbound HTTP from anywhere
      VpcId: !If [CreateNet, !Ref Vpc, !Ref VpcId]
      SecurityGroupIngress:
        - Description: Allow inbound HTTP from anywhere
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  TaskSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Bedrock Gateway Fargate tasks - allow inbound only from ALB
      VpcId: !If [CreateNet, !Ref Vpc, !Ref VpcId]

  AlbToTaskIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Allow ALB to reach the task on the container port
      GroupId: !GetAtt TaskSecurityGroup.GroupId
      IpProtocol: tcp
      FromPort: 8080
      ToPort: 8080
      SourceSecurityGroupId: !GetAtt AlbSecurityGroup.GroupId

  AlbToTaskEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: Allow ALB to forward to the task on the container port
      GroupId: !GetAtt AlbSecurityGroup.GroupId
      IpProtocol: tcp
      FromPort: 8080
      ToPort: 8080
      DestinationSecurityGroupId: !GetAtt TaskSecurityGroup.GroupId

  TaskEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      Description: Allow all outbound traffic (ECR pull, Bedrock, Secrets Manager)
      GroupId: !GetAtt TaskSecurityGroup.GroupId
      IpProtocol: "-1"
      CidrIp: 0.0.0.0/0

  # ----------------------------------------------------------------
  # ECS Cluster + Task Definition + Service
  # ----------------------------------------------------------------
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: bedrock-gateway
      Cpu: "512"
      Memory: "1024"
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      TaskRoleArn: !GetAtt TaskRole.Arn
      RuntimePlatform:
        CpuArchitecture: X86_64
        OperatingSystemFamily: LINUX
      ContainerDefinitions:
        - Name: bedrock-gateway
          Image: !Ref ContainerImageUri
          Essential: true
          StopTimeout: 30
          PortMappings:
            - ContainerPort: 8080
              Protocol: tcp
          Secrets:
            - Name: API_KEY
              ValueFrom: !Sub "${ApiKeySecretArn}:api_key::"
            - !If
              - HasBedrockApiKey
              - Name: AWS_BEARER_TOKEN_BEDROCK
                ValueFrom: !Sub "${BedrockApiKeySecretArn}:bedrock_api_key::"
              - !Ref AWS::NoValue
          Environment:
            - Name: API_KEY_SECRET_ARN
              Value: !Ref ApiKeySecretArn
            - Name: PORT
              Value: "8080"
            - Name: API_ROUTE_PREFIX
              Value: /api/v1
            - Name: AWS_REGION
              Value: !Ref AWS::Region
            - Name: LOG_LEVEL
              Value: !Ref LogLevel
            - Name: ENABLE_PROMPT_CACHING
              Value: !Ref EnablePromptCaching
            - Name: DISABLE_MANTLE
              Value: !Ref DisableMantle
            - !If
              - HasDefaultModelId
              - Name: DEFAULT_MODEL
                Value: !Ref DefaultModelId
              - !Ref AWS::NoValue
            - !If
              - HasDefaultEmbeddingModelId
              - Name: DEFAULT_EMBEDDING_MODEL
                Value: !Ref DefaultEmbeddingModelId
              - !Ref AWS::NoValue
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref GatewayLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: bedrock-gateway

  Service:
    Type: AWS::ECS::Service
    DependsOn:
      - AlbListener
      - AlbToTaskIngress
    Properties:
      Cluster: !Ref Cluster
      LaunchType: FARGATE
      DesiredCount: !Ref DesiredCount
      TaskDefinition: !Ref TaskDefinition
      HealthCheckGracePeriodSeconds: 60
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 50
        DeploymentCircuitBreaker:
          Enable: true
          Rollback: true
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !GetAtt TaskSecurityGroup.GroupId
          Subnets: !If
            - CreateNet
            - [!Ref PublicSubnet1, !Ref PublicSubnet2]
            - !Ref Subnets
      LoadBalancers:
        - ContainerName: bedrock-gateway
          ContainerPort: 8080
          TargetGroupArn: !Ref TargetGroup

  # ----------------------------------------------------------------
  # Application Load Balancer + Listener + Target Group
  # ----------------------------------------------------------------
  BedrockGatewayALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      Type: application
      SecurityGroups:
        - !GetAtt AlbSecurityGroup.GroupId
      Subnets: !If
        - CreateNet
        - [!Ref PublicSubnet1, !Ref PublicSubnet2]
        - !Ref Subnets
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: "600"

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      TargetType: ip
      Protocol: HTTP
      Port: 8080
      VpcId: !If [CreateNet, !Ref Vpc, !Ref VpcId]
      HealthCheckEnabled: true
      HealthCheckProtocol: HTTP
      HealthCheckPath: /api/v1/health
      HealthCheckIntervalSeconds: 30
      HealthCheckTimeoutSeconds: 10
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 5
      Matcher:
        HttpCode: "200"
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "30"

  AlbListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref BedrockGatewayALB
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

Outputs:
  APIBaseUrl:
    Description: Bedrock Gateway API Base URL (OPENAI_API_BASE)
    Value: !Sub "http://${BedrockGatewayALB.DNSName}/api/v1"
  ClusterName:
    Description: ECS cluster name
    Value: !Ref Cluster
  ServiceName:
    Description: ECS service name
    Value: !GetAtt Service.Name
  VpcId:
    Description: VPC used by the gateway (created in one-click mode, or the provided VpcId)
    Value: !If [CreateNet, !Ref Vpc, !Ref VpcId]
  Subnets:
    Description: Subnets used by the ALB and Fargate tasks
    Value: !If
      - CreateNet
      - !Sub "${PublicSubnet1},${PublicSubnet2}"
      - !Join [",", !Ref Subnets]
  NetworkMode:
    Description: Whether the stack created a new network or used an existing one
    Value: !If [UseExistingNet, bring-your-own, one-click]
