AWSTemplateFormatVersion: "2010-09-09"

Description: Monte Carlo's OpenTelemetry Collector Service

Metadata:
  License: >
    Copyright 2023 Monte Carlo Data, Inc.

    The Software contained herein (the "Software") is the intellectual property of Monte Carlo Data, Inc. ("Licensor"),
    and Licensor retains all intellectual property rights in the Software, including any and all derivatives, changes and
    improvements thereto. Only customers who have entered into a commercial agreement with Licensor for use or
    purchase of the Software ("Licensee") are licensed or otherwise authorized to use the Software, and any Licensee
    agrees that it obtains no copyright or other intellectual property rights to the Software, except for the license
    expressly granted below or in accordance with the terms of their commercial agreement with Licensor (the
    "Agreement"). Subject to the terms and conditions of the Agreement, Licensor grants Licensee a non-exclusive,
    non-transferable, non-sublicensable, revocable, limited right and license to use the Software, in each case solely
    internally within Licensee's organization for non-commercial purposes and only in connection with the service
    provided by Licensor pursuant to the Agreement, and in object code form only. Without Licensor's express prior
    written consent, Licensee may not, directly or indirectly, (i) distribute the Software, any portion thereof, or any
    modifications, enhancements, or derivative works of any of the foregoing (collectively, the "Derivatives") to any
    third party, (ii) license, market, sell, offer for sale or otherwise attempt to commercialize any Software, Derivatives,
    or portions thereof, (iii) use the Software, Derivatives, or any portion thereof for the benefit of any third party, (iv)
    use the Software, Derivatives, or any portion thereof in any manner or with respect to any commercial activity
    which competes, or is reasonably likely to compete, with any business that Licensor conducts, proposes to conduct
    or demonstrably anticipates conducting, at any time; or (v) seek any patent or other intellectual property rights or
    protections over or in connection with any Software of Derivatives.
Parameters:
  ExistingVpcId:
    AllowedPattern: '^(vpc[e]?-[0-9a-f]*)$'
    Description: VPC ID to deploy the OpenTelemetry Collector into.
    Type: String
  ExistingSubnetIds:
    Description: >
      Comma-delimited list of private subnet IDs (at least 2) for deploying the OpenTelemetry Collector.
    Type: CommaDelimitedList
  ExistingSecurityGroupId:
    Description: Optional additional security group ID to attach to the OpenTelemetry Collector resources. If provided, this security group will be added to the Network Load Balancer and ECS Service.
    Type: String
    Default: "N/A"
    AllowedPattern: '^(|N/A|sg-[0-9a-f]*)$'
    ConstraintDescription: Must be either empty, N/A, or a valid security group ID (sg-xxxxxxxxx)
  TelemetryDataBucketArn:
    Description: ARN of the S3 bucket to store OpenTelemetry data such as traces, metrics, and logs.
    Type: String
  GRPCPort:
    Description: Port for OTLP gRPC receiver
    Type: Number
    Default: 4317
    MinValue: 1024
    MaxValue: 65535
  HTTPPort:
    Description: Port for OTLP HTTP receiver
    Type: Number
    Default: 4318
    MinValue: 1024
    MaxValue: 65535
  TaskDesiredCount:
    Description: Desired number of running tasks for the OpenTelemetry Collector service
    Type: Number
    Default: 2
    MinValue: 1
    MaxValue: 10
  TaskCpu:
    Description: CPU units for the task (1024 = 1 vCPU)
    Type: Number
    Default: 1024
    AllowedValues: [256, 512, 1024, 2048, 4096]
  TaskMemory:
    Description: Memory for the task in MB
    Type: Number
    Default: 2048
    AllowedValues: [512, 1024, 2048, 3072, 4096, 5120, 6144, 7168, 8192]
  ContainerImage:
    Description: OpenTelemetry Collector container image
    Type: String
    Default: "otel/opentelemetry-collector-contrib:latest"
  BatchTimeout:
    Description: Timeout for batch processor in seconds
    Type: String
    Default: "10s"
  BatchSize:
    Description: Batch size for sending telemetry data
    Type: Number
    Default: 1024
    MinValue: 100
    MaxValue: 10000
  MemoryLimitMiB:
    Description: Memory limit for the collector in MiB
    Type: Number
    Default: 1500
    MinValue: 512
    MaxValue: 4096
  MemorySpikeLimitMiB:
    Description: Memory spike limit for the collector in MiB
    Type: Number
    Default: 512
    MinValue: 256
    MaxValue: 2048
  ExternalID:
    Description: External ID to access the S3 bucket. Update this value later after the CF stack is created.
    Type: String
    Default: "N/A"
  ExternalAccessPrincipal:
    Description: Principal (AWS ARN/account ID or Federated identifier) allowed to assume the external access role. If left empty, will use the current AWS account ID. Update this value later after the CF stack is created.
    Type: String
    Default: "N/A"
  ExternalAccessPrincipalType:
    Description: Type of principal for external access role
    Type: String
    Default: "AWS"
    AllowedValues: ["AWS", "Federated"]
  ExternalAccessRoleName:
    Description: Custom name of the external access role. If left empty, will use the default name of "{AWS::StackName}-EAR".
    Type: String
    Default: "N/A"
Conditions:
  UseCurrentAccount: !Equals [!Ref ExternalAccessPrincipal, "N/A"]
  UseAWSPrincipal: !Equals [!Ref ExternalAccessPrincipalType, "AWS"]
  HasAdditionalSecurityGroup: !Not [!Equals [!Ref ExistingSecurityGroupId, "N/A"]]
  UseCustomExternalAccessRoleName: !Not [!Equals [!Ref ExternalAccessRoleName, "N/A"]]
Resources:
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 14
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  NetworkLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internal
      Type: network
      Subnets: !Ref ExistingSubnetIds
      SecurityGroups: !If
        - HasAdditionalSecurityGroup
        - - !Ref SecurityGroup
          - !Ref ExistingSecurityGroupId
        - - !Ref SecurityGroup
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  TargetGroupGRPC:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: !Ref GRPCPort
      Protocol: TCP
      TargetType: ip
      VpcId: !Ref ExistingVpcId
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: traffic-port
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  TargetGroupHTTP:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: !Ref HTTPPort
      Protocol: TCP
      TargetType: ip
      VpcId: !Ref ExistingVpcId
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPort: traffic-port
      HealthCheckProtocol: TCP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  ListenerGRPC:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroupGRPC
      LoadBalancerArn: !Ref NetworkLoadBalancer
      Port: !Ref GRPCPort
      Protocol: TCP
  ListenerHTTP:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroupHTTP
      LoadBalancerArn: !Ref NetworkLoadBalancer
      Port: !Ref HTTPPort
      Protocol: TCP
  EcsCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterSettings:
        - Name: containerInsights
          Value: enabled
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
      Path: /
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - s3:PutObject
                  - s3:PutObjectAcl
                  - s3:GetBucketLocation
                Effect: Allow
                Resource:
                  - !Sub "${TelemetryDataBucketArn}/mcd/otel-collector/*"
                  - !Ref TelemetryDataBucketArn
          PolicyName: S3Export
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  ExternalAccessS3ReadOnlyPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: OpenTelemetryS3ExternalAccessReadOnly
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - s3:GetBucketLocation
              - s3:GetObject
              - s3:ListBucket
              - s3:GetObjectVersion
            Resource:
              - !Ref TelemetryDataBucketArn
              - !Sub "${TelemetryDataBucketArn}/*"
      Roles:
        - !Ref ExternalAccessRole
  ExternalAccessRole:
    # This role is used to access the S3 bucket containing the telemetry data
    # by an external warehouse, such as Snowflake.
    Type: AWS::IAM::Role
    Properties:
      RoleName: !If
        - UseCustomExternalAccessRoleName
        - !Ref ExternalAccessRoleName
        - !Sub "${AWS::StackName}-EAR"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: !If
              - UseCurrentAccount
              - AWS: !Ref AWS::AccountId
              - !If
                - UseAWSPrincipal
                - AWS:
                  - !Ref ExternalAccessPrincipal
                  - !If
                    - UseCustomExternalAccessRoleName
                    - !Sub "arn:aws:iam::${AWS::AccountId}:role/${ExternalAccessRoleName}"
                    - !Sub "arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-EAR"
                - Federated: !Ref ExternalAccessPrincipal
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref ExternalID
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: otel-collector
          Image: !Ref ContainerImage
          Essential: true
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: otel-collector
          PortMappings:
            - ContainerPort: !Ref GRPCPort
              Protocol: tcp
            - ContainerPort: !Ref HTTPPort
              Protocol: tcp
          Command:
            - --config
            - env:OTEL_CONFIG_CONTENT
          Environment:
            - Name: AWS_REGION
              Value: !Ref AWS::Region
            - Name: OTEL_CONFIG_CONTENT
              Value: !Sub 
                - |
                  receivers:
                    otlp:
                      protocols:
                        grpc:
                          endpoint: 0.0.0.0:${GRPCPort}
                        http:
                          endpoint: 0.0.0.0:${HTTPPort}

                  processors:
                    batch:
                      timeout: ${BatchTimeout}
                      send_batch_size: ${BatchSize}
                    memory_limiter:
                      check_interval: 1s
                      limit_mib: ${MemoryLimitMiB}
                      spike_limit_mib: ${MemorySpikeLimitMiB}

                  exporters:
                    debug:
                      verbosity: detailed
                    awss3/traces:
                      s3uploader:
                        region: ${AWS::Region}
                        s3_bucket: ${ExternalS3BucketName}
                        s3_base_prefix: 'mcd/otel-collector/traces'
                        file_prefix: traces
                      resource_attrs_to_s3:
                        s3_prefix: "service.name"
                    awss3/metrics:
                      s3uploader:
                        region: ${AWS::Region}
                        s3_bucket: ${ExternalS3BucketName}
                        s3_base_prefix: 'mcd/otel-collector/metrics'
                        file_prefix: metrics
                      resource_attrs_to_s3:
                        s3_prefix: "service.name"
                    awss3/logs:
                      s3uploader:
                        region: ${AWS::Region}
                        s3_bucket: ${ExternalS3BucketName}
                        s3_base_prefix: 'mcd/otel-collector/logs'
                        file_prefix: logs
                      resource_attrs_to_s3:
                        s3_prefix: "service.name"

                  service:
                    pipelines:
                      traces:
                        receivers: [otlp]
                        processors: [memory_limiter, batch]
                        exporters: [debug, awss3/traces]
                      metrics:
                        receivers: [otlp]
                        processors: [memory_limiter, batch]
                        exporters: [debug, awss3/metrics]
                      logs:
                        receivers: [otlp]
                        processors: [memory_limiter, batch]
                        exporters: [debug, awss3/logs]
                - ExternalS3BucketName: !Select [5, !Split [":", !Ref TelemetryDataBucketArn]]
                  GRPCPort: !Ref GRPCPort
                  HTTPPort: !Ref HTTPPort
      Cpu: !Ref TaskCpu
      Memory: !Ref TaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      TaskRoleArn: !GetAtt TaskRole.Arn
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  EcsService:
    Type: AWS::ECS::Service
    DependsOn:
      - ListenerGRPC
      - ListenerHTTP
    Properties:
      Cluster: !Ref EcsCluster
      DesiredCount: !Ref TaskDesiredCount
      EnableECSManagedTags: true
      LaunchType: FARGATE
      LoadBalancers:
        - ContainerName: otel-collector
          ContainerPort: !Ref GRPCPort
          TargetGroupArn: !Ref TargetGroupGRPC
        - ContainerName: otel-collector
          ContainerPort: !Ref HTTPPort
          TargetGroupArn: !Ref TargetGroupHTTP
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups: !If
            - HasAdditionalSecurityGroup
            - - !Ref SecurityGroup
              - !Ref ExistingSecurityGroupId
            - - !Ref SecurityGroup
          Subnets: !Ref ExistingSubnetIds
      TaskDefinition: !Ref TaskDefinition
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for OpenTelemetry Collector containers
      VpcId: !Ref ExistingVpcId
      Tags:
        - Key: Service
          Value: "mcd-otel-collector"
        - Key: Provider
          Value: "monte-carlo"
  SecurityGroupIngressRule:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref SecurityGroup
      IpProtocol: tcp
      FromPort: 4317
      ToPort: 4318
      SourceSecurityGroupId: !Ref SecurityGroup
      Description: Allow TCP ingress on ports 4317 and 4318 from other resources associated with the security group within the VPC
Outputs:
  OpenTelemetryCollectorGRPCEndpoint:
    Description: The gRPC endpoint for the OpenTelemetry Collector
    Value: !Sub "${NetworkLoadBalancer.DNSName}:${GRPCPort}"
    Export:
      Name: !Sub "${AWS::StackName}:OpenTelemetryCollectorGRPCEndpoint"
  OpenTelemetryCollectorHTTPEndpoint:
    Description: The HTTP endpoint for the OpenTelemetry Collector
    Value: !Sub "http://${NetworkLoadBalancer.DNSName}:${HTTPPort}"
    Export:
      Name: !Sub "${AWS::StackName}:OpenTelemetryCollectorHTTPEndpoint"
  OpenTelemetryCollectorExternalAccessRoleArn:
    Description: The ARN of the IAM role for external access to the OpenTelemetry S3 bucket
    Value: !GetAtt ExternalAccessRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}:OpenTelemetryCollectorExternalAccessRoleArn"
  OpenTelemetryCollectorSecurityGroupId:
    Description: The ID of the security group for the OpenTelemetry Collector
    Value: !Ref SecurityGroup
    Export:
      Name: !Sub "${AWS::StackName}:OpenTelemetryCollectorSecurityGroupId"
Rules:
  ValidatePortConfiguration:
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Equals:
                - Ref: GRPCPort
                - Ref: HTTPPort
        AssertDescription: GRPCPort and HTTPPort must be different values to avoid conflicts
