AWSTemplateFormatVersion: 2010-09-09
Description: Newrelic AWS Integration
Mappings:
  GraphqlAPIUrlMap:
    NewRelicRegion:
      US: 'https://api.newrelic.com/graphql'
      EU: 'https://api.eu.newrelic.com/graphql'
Parameters:
  IAMRoleName:
    Description: Customize the base name of IAM role for New relic AWS integration (the final role name will contain some id at the end)
    Type: String
    Default: NewRelicInfrastructure-Integrations
  NewRelicAccountId:
    Type: String
    Description: Id of the account in New relic
    AllowedPattern: "[0-9]+"
    ConstraintDescription: must only contain numbers
  NewRelicRegion:
    Type: String
    Description: Datacenter where the data will be sent (US/EU), DO NOT TOUCH
    AllowedValues: [US, EU]
  IntegrationName:
    Type: String
    Description: Name of the AWS integration that will be created in New relic
    MinLength: 1
    MaxLength: 255
    AllowedPattern: "[a-zA-Z0-9_-]+"
    ConstraintDescription: must only container letters (uppercase and lowercase), numbers, and characters '_', and '-' with max length of 255 total characters
  NewRelicAPIKey:
    Type: String
    Description: New relic user key to use NerdGraph, our GraphQL API, create or get an existing key in https://one.newrelic.com/launcher/api-keys-ui.api-keys-launcher
    NoEcho: true
    MaxLength: 40
    AllowedPattern: "^[^\\s]+$"
    ConstraintDescription: "The parameter value cannot be empty, contain spaces, and must be alphanumeric and can contain symbols."
  PollingIntegrationSlugs:
    Type: CommaDelimitedList
    Description: "Comma-delimited list of the polling integrations to configure"
  MetricCollectionMode:
    Type: String
    Description: "Metrics Collection Mode to be chosen by customer between PUSH or PULL"
    AllowedValues: [ PUSH, PULL ]
  NewRelicLicenseKey:
    Type: String
    Description: New relic license key, create or get and existing key in https://one.newrelic.com/launcher/api-keys-ui.api-keys-launcher
    NoEcho: true
    MaxLength: 40
    AllowedPattern: "^[^\\s]+$"
    ConstraintDescription: "The parameter value cannot be empty, contain spaces, and must be alphanumeric and can contain symbols."
  FirehoseStreamName:
    Type: String
    Description: Name of new Kinesis Firehose Delivery Stream (must be unique per AWS account in the same AWS Region)
    Default: NewRelic-Delivery-Stream
    MaxLength: 64
    AllowedPattern: "[a-zA-Z0-9_.-]+"
    ConstraintDescription: must only container letters (uppercase and lowercase), numbers, and characters '.', '_', and '-' with max length of 64 total characters
  CloudWatchMetricStreamName:
    Type: String
    Description: Name of new CloudWatch Metric Stream (must be unique per AWS account in the same AWS Region)
    Default: NewRelic-Metric-Stream
    MaxLength: 255
    AllowedPattern: "[a-zA-Z0-9_-]+"
    ConstraintDescription: must only container letters (uppercase and lowercase), numbers, and characters '_', and '-' with max length of 255 total characters
  OutputFormat:
    Type: String
    Description: Choose the OpenTelemetry output format for the CloudWatch Metric Stream
    Default: opentelemetry0.7
    AllowedValues: [ opentelemetry0.7, opentelemetry1.0 ]
  S3BackupBucketName:
    Type: String
    Description: S3 Bucket Destination for failed events (must be globally unique across all AWS accounts in all AWS Regions within a partition)
    Default: firehose-backup
    MinLength: 3
    MaxLength: 63
    AllowedPattern: (?!(^xn--|-s3alias$))^[a-z0-9.][a-z0-9-.]{1,61}[a-z0-9.]$
    ConstraintDescription: must adhere to the S3 bucket naming rules - https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
  CreateConfigService:
    Type: String
    Description: Enable and configure AWS Config to track resource changes. This enables additional discovery and monitoring capabilities. (Optional)
    Default: false
    AllowedValues: [ true, false ]
  S3ConfigBucketName:
    Type: String
    Description: S3 Bucket Destination for delivery channel configuration (must be globally unique across all AWS accounts in all AWS Regions within a partition)
    Default: config-backup
    MinLength: 3
    MaxLength: 63
    AllowedPattern: (?!(^xn--|-s3alias$))^[a-z0-9.][a-z0-9-.]{1,61}[a-z0-9.]$
    ConstraintDescription: must adhere to the S3 bucket naming rules - https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
Conditions:
  # Condition to check if aws_auto_discovery is present in the provided pollingIntegrationSlugs
  # Algorithm - Convert the pollingIntegrationSlugs (CommaDelimitedList) to String and split with the keyword "aws_auto_discovery"
  # and then compare the split string with original string. Incase the keyword is not present, the original string and split string
  # would be same and the condition will return false, otherwise true.
  containsAutoDiscovery:
    Fn::Not:
      - Fn::Equals:
          - !Join [",", !Ref PollingIntegrationSlugs]
          - Fn::Select:
              - 0
              - Fn::Split:
                  - 'aws_auto_discovery'
                  - !Join [",", !Ref PollingIntegrationSlugs]
Resources:
  # A Macro used to generate Role and link account in NewRelic
  NewrelicIntegrationRoleStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://nr-downloads-main.s3.amazonaws.com/cloud_integrations/aws/cloudformation/newrelic-integration-role.yml"
      Parameters:
        IAMRoleName: !Join [ "-", [ !Ref IAMRoleName, !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]] ] ]
        NewRelicAccountId: !Ref NewRelicAccountId
        PollingIntegrationSlugs: !Join [ ",", !Ref PollingIntegrationSlugs ]
  NewrelicIntegrationLinkAccountRoleStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://nr-downloads-main.s3.amazonaws.com/cloud_integrations/aws/cloudformation/newrelic-link-account-role.yml"
  # The Graphql API call to link account in NewRelic
  NewrelicLinkAccountStackV1:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://nr-downloads-main.s3.amazonaws.com/cloud_integrations/aws/cloudformation/newrelic-link-account.yml"
      Parameters:
        NewRelicAccountId: !Ref NewRelicAccountId
        IntegrationName: !Ref IntegrationName
        NewRelicAPIKey: !Ref NewRelicAPIKey
        GraphqlAPIUrl: !FindInMap [GraphqlAPIUrlMap, NewRelicRegion, !Ref NewRelicRegion]
        RoleARN: !GetAtt NewrelicIntegrationRoleStack.Outputs.IntegrationRoleArn
        LinkAccountRoleARN: !GetAtt NewrelicIntegrationLinkAccountRoleStack.Outputs.IntegrationRoleArn
        MetricCollectionMode: !Ref MetricCollectionMode
        LogGroupManagerFunctionArn: !GetAtt LogGroupManagerFunction.Arn
  # The Graphql API call to create polling integrations
  NewrelicConfigureIntegrationsStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://nr-downloads-main.s3.amazonaws.com/cloud_integrations/aws/cloudformation/newrelic-configure-integrations.yml"
      Parameters:
        NewRelicAccountId: !Ref NewRelicAccountId
        PollingLinkedAccountId: !GetAtt NewrelicLinkAccountStackV1.Outputs.PollingLinkedAccountId
        PollingIntegrationSlugs: !Join [ ",", !Ref PollingIntegrationSlugs ]
        NewRelicAPIKey: !Ref NewRelicAPIKey
        GraphqlAPIUrl: !FindInMap [ GraphqlAPIUrlMap, NewRelicRegion, !Ref NewRelicRegion ]
        LogGroupManagerFunctionArn: !GetAtt LogGroupManagerFunction.Arn
  NewrelicConfigureIntegrationsStackAutoDiscovery:
    Type: AWS::CloudFormation::Stack
    Condition: containsAutoDiscovery
    Properties:
      TemplateURL: "https://nr-downloads-main.s3.amazonaws.com/cloud_integrations/aws/cloudformation/newrelic-configure-integrations.yml"
      Parameters:
        NewRelicAccountId: !Ref NewRelicAccountId
        PollingLinkedAccountId: !GetAtt NewrelicLinkAccountStackV1.Outputs.PushLinkedAccountId
        PollingIntegrationSlugs: !Join [ ",", !Ref PollingIntegrationSlugs ]
        NewRelicAPIKey: !Ref NewRelicAPIKey
        GraphqlAPIUrl: !FindInMap [ GraphqlAPIUrlMap, NewRelicRegion, !Ref NewRelicRegion ]
  # Metric stream stack
  NewrelicMetricStreamsStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: "https://nr-downloads-main.s3.amazonaws.com/cloud_integrations/aws/cloudformation/newrelic-metric-streams.yml"
      Parameters:
        NewRelicLicenseKey: !Ref NewRelicLicenseKey
        NewRelicDatacenter: !Ref NewRelicRegion
        FirehoseStreamName: !Ref FirehoseStreamName
        CloudWatchMetricStreamName: !Ref CloudWatchMetricStreamName
        OutputFormat: !Ref OutputFormat
        S3BackupBucketName: !Ref S3BackupBucketName
        CreateConfigService: !Ref CreateConfigService
        S3ConfigBucketName: !Ref S3ConfigBucketName
  LogGroupManagerRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: [lambda.amazonaws.com]
            Action: ["sts:AssumeRole"]
      Path: "/"
      Policies:
        - PolicyName: LogGroupManagerPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:PutRetentionPolicy
                  - logs:DescribeLogGroups
                Resource: "*"

  LogGroupManagerFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: "index.handler"
      Role: !GetAtt LogGroupManagerRole.Arn
      Runtime: python3.13
      Timeout: 30
      Code:
        ZipFile: |
          import boto3
          import json
          import logging
          import urllib.request
          import urllib.parse

          LOG = logging.getLogger()
          LOG.setLevel(logging.INFO)
          logs = boto3.client('logs')

          def send_response(event, context, status, data):
              response_body = json.dumps({
                  "Status": status,
                  "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name,
                  "PhysicalResourceId": context.log_stream_name,
                  "StackId": event['StackId'],
                  "RequestId": event['RequestId'],
                  "LogicalResourceId": event['LogicalResourceId'],
                  "Data": data
              })
              req = urllib.request.Request(event['ResponseURL'], data=response_body.encode('utf-8'), method='PUT')
              req.add_header('Content-Type', 'application/json')
              urllib.request.urlopen(req)

          def handler(event, context):
              props = event['ResourceProperties']
              log_group_name = props['LogGroupName']
              retention = int(props.get('RetentionInDays', 0))
              try:
                  if event['RequestType'] == 'Create':
                      try:
                          logs.create_log_group(logGroupName=log_group_name)
                          LOG.info(f"Created log group: {log_group_name}")
                      except logs.exceptions.ResourceAlreadyExistsException:
                          LOG.info(f"Log group {log_group_name} already exists, treating as success.")
                      if retention > 0:
                          logs.put_retention_policy(logGroupName=log_group_name, retentionInDays=retention)
                  elif event['RequestType'] == 'Update':
                      if retention > 0:
                          logs.put_retention_policy(logGroupName=log_group_name, retentionInDays=retention)
                  # On Delete: do nothing
                  send_response(event, context, "SUCCESS", {})
              except Exception as e:
                  LOG.error(f"Error managing log group: {e}")
                  send_response(event, context, "FAILED", {"Message": str(e)})

  LogGroupManagerFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LogGroupManagerFunction}"
      RetentionInDays: 30
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: 'AWS integration configuration'
        Parameters:
          - IntegrationName
          - NewRelicAccountId
          - NewRelicRegion
          - IAMRoleName
          - NewRelicAPIKey
          - MetricCollectionMode
          - PollingIntegrationSlugs
      -
        Label:
          default: 'AWS Metric Stream Configuration'
        Parameters:
          - NewRelicLicenseKey
          - CloudWatchMetricStreamName
          - FirehoseStreamName
          - S3BackupBucketName
          - OutputFormat
      -
        Label:
          default: 'AWS Config Service Configuration (Optional)'
        Parameters:
          - CreateConfigService
          - S3ConfigBucketName
    ParameterLabels:
      IntegrationName:
        default: 'Name of integration in NewRelic'
      NewRelicAccountId:
        default: 'NewRelic account ID'
      IAMRoleName:
        default: 'Role name for NewRelic'
      NewRelicAPIKey:
        default: 'NewRelic API Key'
      MetricCollectionMode:
        default: 'Metric collection mode'
      CloudWatchMetricStreamName:
        default: 'CloudWatch Metric Stream name'
      FirehoseStreamName:
        default: 'Kinesis Data Firehose name'
      NewRelicRegion:
        default: 'New Relic Datacenter Region'
      NewRelicLicenseKey:
        default: 'New Relic Ingest License Key'
      S3BackupBucketName:
        default: 'Firehose S3 backup bucket name'
      CreateConfigService:
        default: 'Enrich metrics with resource metadata from AWS Config?'
      S3ConfigBucketName:
        default: 'Config S3 backup bucket name'
      OutputFormat:
        default: 'OpenTelemetry output format for CloudWatch Metric Stream'
