AWSTemplateFormatVersion: "2010-09-09"
Description: >-
  Anywhere Mesh ingress on ECS Fargate with an internet-facing ALB, IAM-authenticated
  WebSocket registration, and Route 53 alias routing. Set Active=false to remove
  billable compute and load balancing while keeping cluster, task definition, and IAM.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Network
        Parameters:
          - VpcId
          - PublicSubnetIds
          - PrivateSubnetIds
          - AssignPublicIp
      - Label:
          default: DNS and TLS
        Parameters:
          - ZoneName
          - DomainName
          - HostedZoneId
          - AlbAliasRecordName
          - CertificateArn
      - Label:
          default: Mesh
        Parameters:
          - ClusterName
          - ImageUri
          - AllowedRoleArns
          - ServiceConnectNamespaceName
          - TaskCpu
          - TaskMemory
          - DesiredCount
          - SessionStore
          - ImageCpuArchitecture
          - Active

Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: VPC for the mesh cluster, ALB, and tasks.

  PublicSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Public subnets (at least two AZs) for the internet-facing ALB when Active=true.

  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Subnets for Fargate tasks (typically private). Use public subnets only if AssignPublicIp=true and you have no NAT.

  AssignPublicIp:
    Type: String
    AllowedValues: ["true", "false"]
    Default: "true"
    Description: Assign a public IP to mesh tasks (required for image pull without NAT/VPC endpoints).

  ZoneName:
    Type: String
    Description: Route 53 zone name (e.g. example.com) used for ACM DNS validation and alias records.

  DomainName:
    Type: String
    Description: Primary hostname for ACM (e.g. mesh.example.com). A wildcard SAN *.DomainName is included.

  HostedZoneId:
    Type: String
    Description: Route 53 hosted zone ID for DNS validation and the ALB alias record.

  AlbAliasRecordName:
    Type: String
    Default: "*"
    Description: >-
      Relative record name in the hosted zone pointing to the ALB (e.g. *.e2e for
      *.e2e.example.com when ZoneName is example.com).

  CertificateArn:
    Type: String
    Default: ""
    Description: Optional existing ACM certificate ARN in this region. If empty, a new DNS-validated cert is created.

  ClusterName:
    Type: String
    Default: anywhere-mesh
    Description: ECS cluster name.

  ImageUri:
    Type: String
    Default: 709825985650.dkr.ecr.us-east-1.amazonaws.com/kloudcover/ecs-anywhere-mesh:v1.0.0
    Description: Container image URI for the mesh server (linux/arm64 or linux/amd64).

  AllowedRoleArns:
    Type: String
    Description: Comma-separated IAM role ARN patterns allowed to register clients (ALLOWED_ROLE_ARNS).

  ServiceConnectNamespaceName:
    Type: String
    Default: mesh.local
    Description: Service Connect private DNS namespace name.

  TaskCpu:
    Type: String
    Default: "256"
    AllowedValues: ["256", "512", "1024", "2048", "4096"]
  TaskMemory:
    Type: String
    Default: "512"
  DesiredCount:
    Type: Number
    Default: 1
    MinValue: 0
    MaxValue: 10
    Description: Fargate desired count when Active=true (ignored when Active=false).

  SessionStore:
    Type: String
    AllowedValues: [memory, dynamodb]
    Default: memory
    Description: >-
      Session registry backend for mesh ingress. dynamodb enables multi-task
      routing (peer forward); memory keeps single-task semantics. DynamoDB table
      is created only when Active=true and SessionStore=dynamodb.

  ImageCpuArchitecture:
    Type: String
    Default: ARM64
    AllowedValues: [ARM64, X86_64]
    Description: Fargate CPU architecture (must match ImageUri).

  Active:
    Type: String
    AllowedValues: ["true", "false"]
    Default: "true"
    Description: When false, no ALB, ECS service, or alias record (minimal ongoing cost).

Conditions:
  IsActive: !Equals [!Ref Active, "true"]
  UseDynamoSessionStore: !Equals [!Ref SessionStore, "dynamodb"]
  CreateSessionTable: !And
    - !Condition IsActive
    - !Condition UseDynamoSessionStore
  CreateCertificate: !Equals [!Ref CertificateArn, ""]
  AssignPublicIpToTasks: !Equals [!Ref AssignPublicIp, "true"]

Resources:
  ExecLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/ecs/${AWS::StackName}/exec-logs"
      RetentionInDays: 7

  MeshLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/ecs/${AWS::StackName}/mesh-server-logs"
      RetentionInDays: 7

  MeshCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref ClusterName
      ClusterSettings:
        - Name: containerInsights
          Value: disabled
      Configuration:
        ExecuteCommandConfiguration:
          Logging: OVERRIDE
          LogConfiguration:
            CloudWatchLogGroupName: !Ref ExecLogGroup
      ServiceConnectDefaults:
        Namespace: !GetAtt MeshServiceConnectNamespace.Arn
  MeshServiceConnectNamespace:
    Type: AWS::ServiceDiscovery::PrivateDnsNamespace
    Properties:
      Name: !Ref ServiceConnectNamespaceName
      Vpc: !Ref VpcId
      Description: !Sub "Service Connect namespace for ${AWS::StackName}"

  MeshSessionTable:
    Type: AWS::DynamoDB::Table
    Condition: CreateSessionTable
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Properties:
      TableName: !Sub "${AWS::StackName}-sessions"
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
        - AttributeName: sk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
        - AttributeName: sk
          KeyType: RANGE
      TimeToLiveSpecification:
        AttributeName: ttl
        Enabled: true

  TaskSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub "Mesh Fargate tasks (${AWS::StackName})"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - !If
          - IsActive
          - IpProtocol: tcp
            FromPort: 8080
            ToPort: 8080
            SourceSecurityGroupId: !Ref AlbSecurityGroup
            Description: HTTP from ALB
          - !Ref AWS::NoValue
        - !If
          - IsActive
          - IpProtocol: tcp
            FromPort: 8082
            ToPort: 8082
            SourceSecurityGroupId: !Ref AlbSecurityGroup
            Description: WebSocket from ALB
          - !Ref AWS::NoValue

  TaskPeerForwardIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Condition: IsActive
    Properties:
      GroupId: !Ref TaskSecurityGroup
      IpProtocol: tcp
      FromPort: 8081
      ToPort: 8081
      SourceSecurityGroupId: !Ref TaskSecurityGroup
      Description: Internal mesh peer forward (same task SG only)

  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: IsActive
    Properties:
      GroupDescription: !Sub "Internet ALB (${AWS::StackName})"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: HTTPS
        - IpProtocol: tcp
          FromPort: 8082
          ToPort: 8082
          CidrIp: 0.0.0.0/0
          Description: WebSocket TLS

  MeshCertificate:
    Type: AWS::CertificateManager::Certificate
    Condition: CreateCertificate
    Properties:
      DomainName: !Ref DomainName
      SubjectAlternativeNames:
        - !Sub "*.${DomainName}"
      ValidationMethod: DNS
      DomainValidationOptions:
        - DomainName: !Ref DomainName
          HostedZoneId: !Ref HostedZoneId

  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

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        # RegisterUsage has no resource-level IAM scope. Credentials must come
        # from this task role, not the execution role or the seller account.
        - PolicyName: MarketplaceRegisterUsage
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: aws-marketplace:RegisterUsage
                Resource: "*"
        - PolicyName: EcsExec
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ssmmessages:CreateControlChannel
                  - ssmmessages:CreateDataChannel
                  - ssmmessages:OpenControlChannel
                  - ssmmessages:OpenDataChannel
                Resource: "*"
        - PolicyName: ServiceDiscovery
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - servicediscovery:DiscoverInstances
                  - servicediscovery:GetInstance
                  - servicediscovery:GetInstancesHealthStatus
                  - servicediscovery:GetService
                  - servicediscovery:ListInstances
                  - servicediscovery:ListServices
                  - servicediscovery:RegisterInstance
                  - servicediscovery:DeregisterInstance
                  - servicediscovery:UpdateInstanceCustomHealthStatus
                Resource: "*"
        - !If
          - CreateSessionTable
          - PolicyName: MeshSessionDynamoDB
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action:
                    - dynamodb:GetItem
                    - dynamodb:PutItem
                    - dynamodb:UpdateItem
                    - dynamodb:DeleteItem
                    - dynamodb:Query
                    - dynamodb:BatchWriteItem
                  Resource:
                    - !GetAtt MeshSessionTable.Arn
                    - !Sub "${MeshSessionTable.Arn}/index/*"
          - !Ref AWS::NoValue

  MeshTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub "${AWS::StackName}-mesh"
      Cpu: !Ref TaskCpu
      Memory: !Ref TaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      RuntimePlatform:
        CpuArchitecture: !Ref ImageCpuArchitecture
        OperatingSystemFamily: LINUX
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      TaskRoleArn: !GetAtt TaskRole.Arn
      ContainerDefinitions:
        - Name: mesh-server
          Image: !Ref ImageUri
          Essential: true
          Command:
            - server
            - --alb-port
            - "8080"
            - --websocket-port
            - "8082"
            - --internal-port
            - "8081"
          Environment:
            - Name: RUST_LOG
              Value: info
            - Name: CLUSTER_NAME
              Value: !Ref ClusterName
            - Name: AWS_REGION
              Value: !Ref AWS::Region
            - Name: ALLOWED_ROLE_ARNS
              Value: !Ref AllowedRoleArns
            - Name: SESSION_STORE
              Value: !Ref SessionStore
            - !If
              - UseDynamoSessionStore
              - Name: MESH_SESSION_TABLE
                Value: !If
                  - CreateSessionTable
                  - !Ref MeshSessionTable
                  - !Sub "${AWS::StackName}-sessions"
              - !Ref AWS::NoValue
            - !If
              - UseDynamoSessionStore
              - Name: MESH_PEER_INTERNAL_BASE_URL
                Value: "http://mesh-internal:8081"
              - !Ref AWS::NoValue
            - !If
              - UseDynamoSessionStore
              - Name: MESH_PEER_ALLOWED_ROLE_ARNS
                Value: !GetAtt TaskRole.Arn
              - !Ref AWS::NoValue
          PortMappings:
            - ContainerPort: 8080
              Protocol: tcp
              Name: mesh-ingress
            - ContainerPort: 8081
              Protocol: tcp
              Name: mesh-internal
            - ContainerPort: 8082
              Protocol: tcp
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref MeshLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: mesh

  MeshLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Condition: IsActive
    Properties:
      Name: !Sub "${AWS::StackName}-alb"
      Scheme: internet-facing
      Type: application
      Subnets: !Ref PublicSubnetIds
      SecurityGroups:
        - !Ref AlbSecurityGroup

  HttpTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Condition: IsActive
    Properties:
      VpcId: !Ref VpcId
      Port: 8080
      Protocol: HTTP
      TargetType: ip
      HealthCheckPath: /health
      HealthCheckIntervalSeconds: 15
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "30"

  WsTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Condition: IsActive
    Properties:
      VpcId: !Ref VpcId
      Port: 8082
      Protocol: HTTP
      TargetType: ip
      HealthCheckPath: /health
      HealthCheckIntervalSeconds: 15
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: "30"

  HttpsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Condition: IsActive
    Properties:
      LoadBalancerArn: !Ref MeshLoadBalancer
      Port: 443
      Protocol: HTTPS
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
      Certificates:
        - CertificateArn: !If
            - CreateCertificate
            - !Ref MeshCertificate
            - !Ref CertificateArn
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref HttpTargetGroup

  WsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Condition: IsActive
    Properties:
      LoadBalancerArn: !Ref MeshLoadBalancer
      Port: 8082
      Protocol: HTTPS
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
      Certificates:
        - CertificateArn: !If
            - CreateCertificate
            - !Ref MeshCertificate
            - !Ref CertificateArn
      DefaultActions:
        - Type: fixed-response
          FixedResponseConfig:
            StatusCode: "503"
            ContentType: text/plain
            MessageBody: WebSocket Service Unavailable

  WsHostRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Condition: IsActive
    Properties:
      ListenerArn: !Ref WsListener
      Priority: 100
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Sub "*.${DomainName}"
      Actions:
        - Type: forward
          TargetGroupArn: !Ref WsTargetGroup

  AlbAliasRecord:
    Type: AWS::Route53::RecordSet
    Condition: IsActive
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Sub "${AlbAliasRecordName}.${ZoneName}"
      Type: A
      AliasTarget:
        DNSName: !GetAtt MeshLoadBalancer.DNSName
        HostedZoneId: !GetAtt MeshLoadBalancer.CanonicalHostedZoneID

  MeshService:
    Type: AWS::ECS::Service
    Condition: IsActive
    DependsOn:
      - HttpsListener
      - WsHostRule
    Properties:
      ServiceName: !Sub "${AWS::StackName}-mesh"
      Cluster: !Ref MeshCluster
      TaskDefinition: !Ref MeshTaskDefinition
      DesiredCount: !Ref DesiredCount
      LaunchType: FARGATE
      EnableExecuteCommand: true
      DeploymentConfiguration:
        MinimumHealthyPercent: 50
        MaximumPercent: 200
        DeploymentCircuitBreaker:
          Enable: true
          Rollback: true
      HealthCheckGracePeriodSeconds: 60
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: !If [AssignPublicIpToTasks, ENABLED, DISABLED]
          SecurityGroups:
            - !Ref TaskSecurityGroup
          Subnets: !Ref PrivateSubnetIds
      ServiceConnectConfiguration:
        Enabled: true
        Namespace: !GetAtt MeshServiceConnectNamespace.Arn
        Services:
          - PortName: mesh-ingress
            DiscoveryName: mesh-ingress
            ClientAliases:
              - Port: 8080
                DnsName: mesh-ingress
          - !If
            - UseDynamoSessionStore
            - PortName: mesh-internal
              DiscoveryName: mesh-internal
              ClientAliases:
                - Port: 8081
                  DnsName: mesh-internal
            - !Ref AWS::NoValue
      LoadBalancers:
        - ContainerName: mesh-server
          ContainerPort: 8080
          TargetGroupArn: !Ref HttpTargetGroup
        - ContainerName: mesh-server
          ContainerPort: 8082
          TargetGroupArn: !Ref WsTargetGroup

Outputs:
  Active:
    Description: Whether billable resources (ALB, service) are deployed.
    Value: !Ref Active

  SessionStore:
    Description: Session registry backend (memory or dynamodb).
    Value: !Ref SessionStore

  MeshSessionTableName:
    Condition: CreateSessionTable
    Description: DynamoDB session table when multi-server mode is active.
    Value: !Ref MeshSessionTable

  ClusterName:
    Description: ECS cluster name for mesh clients.
    Value: !Ref ClusterName

  MeshEndpointHost:
    Description: Example host for client registration and HTTPS routing (wildcard alias).
    Value: !Sub "app.${DomainName}"

  AlbDnsName:
    Condition: IsActive
    Description: DNS name of the application load balancer.
    Value: !GetAtt MeshLoadBalancer.DNSName

  HttpsEndpoint:
    Condition: IsActive
    Description: Base HTTPS URL via the wildcard alias (replace app with your service host label).
    Value: !Sub "https://app.${DomainName}"

  WebSocketEndpoint:
    Condition: IsActive
    Description: WebSocket ingress URL (port 8082, TLS).
    Value: !Sub "wss://app.${DomainName}:8082"

  CertificateArnOut:
    Description: ACM certificate in use (created or supplied).
    Value: !If
      - CreateCertificate
      - !Ref MeshCertificate
      - !Ref CertificateArn
