AWSTemplateFormatVersion: 2010-09-09

Description: |
  nOps.io integration role and policy (updated June 22 2025)
  For more information visit https://help.nops.io/docs/getting-started/Onboarding/onboarding-aws-with-automatic-setup

Mappings:
 attributes:
  Nops:
   AuthURL: https://app.nops.io/

Parameters:
  ApiKey:
    Type: String
    AllowedPattern: ^\d+\.[a-fA-F0-9]{32}$
    MinLength: 34
    Description: |
      nOps API Key

  PayerAccount:
    Type: String
    Default: 'true'
    AllowedValues:
    - true
    - false
    Description: |
      Leave as true if the template is being applied to onboard a payer account, this will create resources only required for payer accounts.

  MinRequiredPermissions:
    Type: String
    Default: 'false'
    AllowedValues:
    - true
    - false
    Description: |
      If true, IAM policies with the min base permissions for nOps to get cost and usage data will be created. Some platform features will not be available.

  OnboardChildAccounts:
    Type: String
    Default: 'true'
    AllowedValues:
    - true
    - false
    Description: |
      If true and PayerAccount is true, the template will create a CloudFormation StackSet that will automatically onboard child accounts to nOps.

Conditions:
  IsPayerAccount: !Equals [!Ref PayerAccount, 'true']
  MinRequiredPermissions: !Equals [!Ref MinRequiredPermissions, 'true']
  FullPermissions: !Equals [!Ref MinRequiredPermissions, 'false']
  OnboardChildAccounts: !Equals [!Ref OnboardChildAccounts, 'true']
  ShouldCreateChildStackSet:
    Fn::And:
      - Condition: OnboardChildAccounts
      - Condition: IsPayerAccount

Resources:
  NopsIntegrationRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns: !If [ FullPermissions, ["arn:aws:iam::aws:policy/ReadOnlyAccess"], []]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::202279780353:root
            Action:
              - sts:AssumeRole
            Condition:
              StringEquals:
                "sts:ExternalId":
                  Fn::Base64:
                    Fn::Join:
                      - "-"
                      - - !Ref ApiKey
                        - Fn::Select:
                          - 2
                          - Fn::Split:
                            - "/"
                            - !Ref AWS::StackId
      Tags:
        - Key: version
          Value: 1.2.2
        - Key: createdat
          Value: 20250701

  NopsEssentialsPolicy:
    Type: AWS::IAM::Policy
    DependsOn:
      - NopsIntegrationRole
    Condition: MinRequiredPermissions
    Properties:
      PolicyName: NopsEssentialsPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Resource: "*"
            Effect: Allow
            Action:
              - "cloudwatch:ListMetrics"
              - "events:CreateEventBus"
              - "scheduler:GetSchedule"
              - "scheduler:GetScheduleGroup"
              - "scheduler:ListScheduleGroups"
              - "scheduler:ListSchedules"
              - "scheduler:ListTagsForResource"              
      Roles: [!Ref NopsIntegrationRole]

  NopsComputeCopilotPolicy:
    Type: AWS::IAM::Policy
    DependsOn:
      - NopsIntegrationRole
    Condition: MinRequiredPermissions
    Properties:
      PolicyName: NopsComputeCopilotPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Resource: "*"
            Effect: Allow
            Action:
              - "ec2:DescribeLaunchTemplateVersions"
              - "ec2:DescribeLaunchConfigurations"
              - "ec2:DescribeImages"
              - "ec2:DescribeInstances"
              - "ec2:DescribeInstanceTypes"
              - "ec2:DescribeReservedInstances"
              - "lambda:InvokeFunction"
              - "cloudformation:ListStacks"
              - "cloudformation:DescribeStacks"
              - "savingsplans:DescribeSavingsPlans"
      Roles: [!Ref NopsIntegrationRole]

  NopsIntegrationMinimumPolicy:
    Type: AWS::IAM::Policy
    DependsOn:
      - NopsIntegrationRole
    Condition: MinRequiredPermissions
    Properties:
      PolicyName: NopsIntegrationPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
            - autoscaling:DescribeAutoScalingGroups
            - autoscaling:DescribeAutoScalingInstances
            - autoscaling-plans:DescribeScalingPlans
            - autoscaling-plans:GetScalingPlanResourceForecastData
            - autoscaling-plans:DescribeScalingPlanResources
            - autoscaling:DescribePolicies
            - autoscaling:DescribeLaunchConfigurations
            - autoscaling:DescribeLoadBalancers
            - autoscaling:DescribeScalingActivities
            - autoscaling:DescribeAccountLimits
            - apigateway:GET
            - ce:DescribeCostCategoryDefinition
            - ce:DescribeNotificationSubscription
            - ce:DescribeReport
            - ce:GetAnomalies
            - ce:GetAnomalyMonitors
            - ce:GetAnomalySubscriptions
            - ce:GetApproximateUsageRecords
            - ce:GetCostAndUsage
            - ce:GetCostAndUsageWithResources
            - ce:GetCostCategories
            - ce:GetCostForecast
            - ce:GetDimensionValues
            - ce:GetPreferences
            - ce:GetReservationCoverage
            - ce:GetReservationPurchaseRecommendation
            - ce:GetReservationUtilization
            - ce:GetRightsizingRecommendation
            - ce:GetSavingsPlanPurchaseRecommendationDetails
            - ce:GetSavingsPlansCoverage
            - ce:GetSavingsPlansPurchaseRecommendation
            - ce:GetSavingsPlansUtilization
            - ce:GetSavingsPlansUtilizationDetails
            - ce:GetTags
            - ce:GetUsageForecast
            - ce:ListCostAllocationTagBackfillHistory
            - ce:ListCostAllocationTags
            - ce:ListCostCategoryDefinitions
            - ce:ListSavingsPlansPurchaseRecommendationGeneration
            - ce:ListTagsForResource
            - ce:StartSavingsPlansPurchaseRecommendationGeneration
            - ce:UpdateCostAllocationTagsStatus
            - cloudtrail:DescribeTrails
            - cloudtrail:LookupEvents
            - cloudwatch:GetMetricStatistics
            - compute-optimizer:DescribeRecommendationExportJobs
            - compute-optimizer:GetAutoScalingGroupRecommendations
            - compute-optimizer:GetEBSVolumeRecommendations
            - compute-optimizer:GetEC2InstanceRecommendations
            - compute-optimizer:GetEC2RecommendationProjectedMetrics
            - compute-optimizer:GetECSServiceRecommendationProjectedMetrics
            - compute-optimizer:GetECSServiceRecommendations
            - compute-optimizer:GetEffectiveRecommendationPreferences
            - compute-optimizer:GetEnrollmentStatus
            - compute-optimizer:GetEnrollmentStatusesForOrganization
            - compute-optimizer:GetLambdaFunctionRecommendations
            - compute-optimizer:GetLicenseRecommendations
            - compute-optimizer:GetRDSDatabaseRecommendationProjectedMetrics
            - compute-optimizer:GetRDSDatabaseRecommendations
            - compute-optimizer:GetRecommendationPreferences
            - compute-optimizer:GetRecommendationSummaries
            - config:DescribeConfigurationRecorders
            - consolidatedbilling:GetAccountBillingRole
            - cost-optimization-hub:GetPreferences
            - cost-optimization-hub:GetRecommendation
            - cost-optimization-hub:ListEnrollmentStatuses
            - cost-optimization-hub:ListRecommendations
            - cost-optimization-hub:ListRecommendationSummaries
            - cost-optimization-hub:UpdateEnrollmentStatus
            - cost-optimization-hub:UpdatePreferences
            - consolidatedbilling:ListLinkedAccounts
            - cur:GetClassicReport
            - cur:GetClassicReportPreferences
            - cur:GetUsageReport
            - cur:DescribeReportDefinitions
            - cur:PutReportDefinition
            - dynamodb:ListTables
            - ec2:DescribeImages
            - ec2:DescribeInstances
            - ec2:DescribeNatGateways
            - ec2:DescribeNetworkInterfaces
            - ec2:DescribeRegions
            - ec2:DescribeReservedInstances
            - ec2:DescribeVolumes
            - ec2:DescribeVpcs
            - ec2:DescribeAvailabilityZones
            - ec2:DescribeInstanceStatus
            - ec2:DescribeSnapshots
            - ec2:DescribeSecurityGroups
            - ecs:ListClusters
            - eks:ListTagsForResource
            - eks:ListClusters
            - eks:ListNodegroups
            - eks:DescribeCluster
            - eks:DescribeNodegroup
            - elasticache:DescribeCacheClusters
            - elasticache:DescribeCacheSubnetGroups
            - elasticfilesystem:DescribeFileSystems
            - elasticloadbalancing:DescribeLoadBalancers
            - es:DescribeElasticsearchDomains
            - es:ListDomainNames
            - events:ListRules
            - guardduty:ListDetectors
            - iam:GetRole
            - iam:GetAccountSummary
            - iam:ListAccountAliases
            - iam:ListAttachedUserPolicies
            - iam:ListRoles
            - iam:ListUsers
            - kms:Decrypt
            - lambda:GetFunction
            - lambda:GetPolicy
            - lambda:ListFunctions
            - rds:DescribeDBClusters
            - rds:DescribeDBInstances
            - rds:DescribeDBSnapshots
            - rds:DescribeDBClusterSnapshotAttributes
            - rds:DescribeDBClusterSnapshots
            - rds:DescribeDBRecommendations
            - rds:DescribeDBSecurityGroups
            - rds:ListTagsForResource
            - redshift:DescribeClusters
            - s3:ListAllMyBuckets
            - s3:GetBucketVersioning
            - savingsplans:DescribeSavingsPlanRates
            - savingsplans:DescribeSavingsPlans
            - savingsplans:DescribeSavingsPlansOfferingRates
            - savingsplans:DescribeSavingsPlansOfferings
            - savingsplans:ListTagsForResource
            - support:CreateCase
            - support:DescribeCases
            - tag:GetResources
            - tag:GetTagValues
            - tag:DescribeReportCreation
            - tag:GetTagKeys
            - tag:GetComplianceSummary
            - organizations:ListAccounts
            - organizations:DescribeOrganization
            - organizations:ListRoots
            Effect: Allow
            Resource: "*"
      Roles: [!Ref NopsIntegrationRole]

  NopsIntegrationPolicy:
    Type: AWS::IAM::Policy
    DependsOn:
      - NopsIntegrationRole
    Condition: FullPermissions
    Properties:
      PolicyName: NopsIntegrationPolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - cur:DescribeReportDefinitions
              - cur:DeleteReportDefinition
              - cur:PutReportDefinition
              - s3:ListBucket
              - support:CreateCase
              - events:CreateEventBus
              - ce:*
            Effect: Allow
            Resource: "*"
          - Action:
              - acm-pca:Describe*
              - acm-pca:Get*
              - acm-pca:List*
              - acm:Describe*
              - acm:Get*
              - acm:List*
              - apigateway:GET
              - appconfig:GetConfiguration*
              - appflow:DescribeConnector*
              - appflow:ListConnector*
              - appstream:DescribeDirectoryConfigs
              - appstream:DescribeUsers
              - appstream:DescribeSessions
              - appsync:Get*
              - appsync:List*
              - athena:Get*
              - athena:List*
              - backup:GetBackupVaultAccessPolicy
              - cassandra:Select
              - chime:Describe*
              - chime:Get*
              - chime:List*
              - cloud9:Describe*
              - cloud9:Get*
              - cloud9:List*
              - clouddirectory:Get*
              - clouddirectory:List*
              - cloudfront:GetCloudFrontOriginAccessIdentity 
              - cloudfront:GetFieldLevelEncryption*
              - cloudfront:GetKeyGroupConfig
              - cloudwatch:GetMetricData
              - cloudwatch:GetMetricStream
              - cloudwatch:ListMetricStreams
              - codeartifact:GetAuthorizationToken
              - codeartifact:ReadFromRepository 
              - codebuild:BatchGet*
              - codebuild:ListSourceCredentials
              - codecommit:BatchGet*
              - codecommit:Get*
              - codecommit:GitPull
              - codedeploy:BatchGet*
              - codedeploy:Get*
              - codestar:DescribeUserProfile
              - codestar:ListUserProfiles
              - cognito-identity:*
              - cognito-idp:*
              - cognito-sync:*
              - comprehend:Describe*
              - comprehend:List*
              - config:BatchGetAggregateResourceConfig
              - config:BatchGetResourceConfig
              - config:SelectAggregateResourceConfig
              - config:SelectResourceConfig
              - connect:Describe*
              - connect:Get*
              - connect:List*
              - datapipeline:DescribeObjects
              - datapipeline:EvaluateExpression
              - datapipeline:QueryObjects
              - dax:BatchGetItem
              - dax:GetItem
              - dax:Query
              - deepcomposer:Get*
              - deepcomposer:List*
              - devicefarm:GetRemoteAccessSession
              - devicefarm:ListRemoteAccessSessions
              - directconnect:Describe*
              - directconnect:List*
              - discovery:Describe*
              - discovery:Get*
              - discovery:List*
              - dms:Describe*
              - dms:List*
              - ds:Get*
              - dynamodb:GetItem
              - dynamodb:BatchGetItem
              - dynamodb:Query
              - dynamodb:Scan
              - ec2:GetConsoleScreenshot
              - ecr:BatchGetImage
              - ecr:GetAuthorizationToken
              - ecr:GetDownloadUrlForLayer
              - ecr-public:GetAuthorizationToken
              - eks:DescribeIdentityProviderConfig
              - elasticbeanstalk:DescribeConfigurationOptions
              - elasticbeanstalk:DescribeConfigurationSettings
              - es:ESHttpGet*
              - fis:GetExperimentTemplate
              - fms:GetAdminAccount
              - frauddetector:BatchGetVariable
              - frauddetector:Get*
              - gamelift:GetGameSessionLogUrl
              - gamelift:GetInstanceAccess
              - geo:ListDevicePositions
              - glue:GetSecurityConfiguration*
              - glue:SearchTables
              - glue:GetTable*
              - guardduty:GetIPSet
              - guardduty:GetMasterAccount
              - guardduty:GetMembers
              - guardduty:ListMembers
              - guardduty:ListOrganizationAdminAccounts
              - inspector2:GetConfiguration
              - imagebuilder:GetImage
              - iotsitewise:ListAccessPolicies
              - ivs:GetPlaybackKeyPair
              - ivs:GetStreamSession
              - kafka:GetBootstrapBrokers
              - kendra:Query*
              - kinesis:Get*
              - kms:DescribeKey
              - kms:GetPublicKey
              - lex:Get*
              - lambda:GetFunctionConfiguration
              - license-manager:GetGrant
              - license-manager:GetLicense
              - license-manager:ListTokens
              - lightsail:GetBucketAccessKeys
              - lightsail:GetCertificates
              - lightsail:GetContainerImages
              - lightsail:GetKeyPair
              - lightsail:GetRelationalDatabaseLogStreams
              - logs:GetLogEvents
              - logs:StartQuery
              - machinelearning:GetMLModel
              - macie2:GetAdministratorAccount
              - macie2:GetMember
              - macie2:GetMacieSession
              - macie2:SearchResources
              - macie2:GetSensitiveDataOccurrences
              - nimble:GetStreamingSession
              - polly:SynthesizeSpeech
              - proton:GetEnvironmentTemplate
              - proton:GetServiceTemplate
              - proton:ListServiceTemplates
              - proton:ListEnvironmentTemplates
              - qldb:GetBlock
              - qldb:GetDigest
              - rds:Download*
              - rekognition:CompareFaces
              - rekognition:Detect*
              - rekognition:Search*
              - resiliencehub:DescribeAppVersionTemplate
              - resiliencehub:ListRecommendationTemplates
              - robomaker:GetWorldTemplateBody
              - s3-object-lambda:GetObject
              - sagemaker:Search
              - schemas:GetDiscoveredSchema
              - sdb:Get*
              - sdb:Select*
              - secretsmanager:*
              - securityhub:GetFindings
              - securityhub:GetMembers
              - securityhub:ListMembers
              - ses:GetTemplate
              - ses:GetEmailTemplate
              - ses:GetContact
              - ses:GetContactList
              - ses:ListTemplates
              - ses:ListEmailTemplates
              - ses:ListVerifiedEmailAddresses
              - signer:GetSigningProfile
              - signer:ListProfilePermissions
              - signer:ListSigningProfiles
              - sms-voice:DescribeKeywords
              - sms-voice:DescribeOptedOutNumbers
              - sms-voice:DescribePhoneNumbers
              - sms-voice:DescribePools
              - snowball:Describe*
              - sqs:Receive*
              - ssm-contacts:*
              - ssm:DescribeParameters*
              - ssm:GetParameter*
              - sso:Describe*
              - sso:Get*
              - sso:List*
              - storagegateway:DescribeChapCredentials
              - support:DescribeCommunications
              - timestream:ListDatabases
              - timestream:ListTables
              - transcribe:Get*
              - transcribe:List*
              - transfer:Describe*
              - transfer:List*
              - waf-regional:GetChangeToken
              - workmail:DescribeUser
              - workmail:ListUsers
            Effect: Deny
            Resource: "*"
      Roles: [!Ref NopsIntegrationRole]

  NopsSystemS3Bucket:
    Type: AWS::S3::Bucket
    Condition: IsPayerAccount
    DependsOn:
      - NopsIntegrationRole
    Properties:
      BucketName:
        Fn::If:
          - IsPayerAccount
          - Fn::Join:
            - "-"
            - - "nops"
              - !Ref AWS::AccountId
              - Fn::Select:
                - 0
                - Fn::Split:
                  - "."
                  - !Ref ApiKey
          - "na"
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256

  NopsSystemS3BucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Condition: IsPayerAccount
    DependsOn:
      - NopsSystemS3Bucket
    Properties:
      Bucket:
        Fn::If:
          - IsPayerAccount
          - Fn::Join:
            - "-"
            - - "nops"
              - !Ref AWS::AccountId
              - Fn::Select:
                - 0
                - Fn::Split:
                  - "."
                  - !Ref ApiKey
          - "na"
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Deny
          Principal: "*"
          Action: s3:*
          Resource:
          - !GetAtt [NopsSystemS3Bucket, Arn]
          - !Sub "${NopsSystemS3Bucket.Arn}/*"
          Condition:
            Bool:
              aws:SecureTransport: 'false'

  NopsSystemBucketPolicy:
    Type: AWS::IAM::Policy
    Condition: IsPayerAccount
    DependsOn:
      - NopsIntegrationRole
      - NopsSystemS3Bucket
    Properties:
      PolicyName: NopsSystemBucketPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
            -  s3:ListBucket
            -  s3:GetBucketPolicy
            -  s3:GetEncryptionConfiguration
            -  s3:GetBucketVersioning
            -  s3:GetBucketPolicyStatus
            -  s3:GetBucketLocation
            -  s3:GetBucketAcl
            -  s3:GetBucketLogging
            -  s3:GetObject
            -  s3:PutBucketPolicy
            -  s3:PutObject
            Resource:
              - !GetAtt [NopsSystemS3Bucket, Arn]
              - !Sub "${NopsSystemS3Bucket.Arn}/*"
      Roles: [!Ref NopsIntegrationRole]

  WaitForS3Handle:
    Condition: IsPayerAccount
    DependsOn: 
      - NopsSystemS3Bucket
      - NopsIntegrationRole
    Type: AWS::CloudFormation::WaitConditionHandle

  WaitForResourcesHandle:
    DependsOn:
      - NopsIntegrationRole
    Type: AWS::CloudFormation::WaitConditionHandle

  WaitForResources:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Handle: !If [IsPayerAccount, !Ref WaitForS3Handle, !Ref WaitForResourcesHandle]
      Timeout: '15'
      Count: 0

  NopsCallBackLambdaExecutionRole:
    Type: AWS::IAM::Role
    DependsOn:
    - WaitForResources
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
        Version: 2012-10-17
      Path: /
      Policies:
        - PolicyDocument:
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource: arn:aws:logs:*:*:*
              - Action:
                  - cloudformation:CreateStackInstances
                  - cloudformation:CreateStackSet
                  - cloudformation:TagResource
                  - organizations:DescribeOrganization
                  - organizations:ListRoots
                Effect: Allow
                Resource: "*"
            Version: 2012-10-17
          PolicyName: root

  NopsCallBack:
    Type: AWS::Lambda::Function
    DependsOn:
    - WaitForResources
    - NopsCallBackLambdaExecutionRole
    Properties:
      Code:
        ZipFile: !Sub |
          import boto3
          import cfnresponse
          import urllib3
          import json

          def lambda_handler(event, context):
              """Handles stack events"""
              print("Received event: ", event)

              stack_response = dict()

              if event["RequestType"] != "Create":
                  cfnresponse.send(event, context, "SUCCESS", stack_response)
                  return

              try:
                  http = urllib3.PoolManager()
                  properties = event["ResourceProperties"]

                  api_key = properties["ApiKey"]
                  aws_account_number = properties["AWSAccountID"]
                  is_master_payer = str(properties["IsPayerAccount"]) == "true"
                  onboard_child_accounts = str(properties["OnboardChildAccounts"]) == "true"

                  organizations_client = boto3.client("organizations")

                  organization_metadata = organizations_client.describe_organization()
                  master_account_id = organization_metadata["Organization"]["MasterAccountId"]
                  if is_master_payer and aws_account_number != master_account_id:
                      raise Exception("Applying in child account while PayerAccount is set to true, please fix this parameter")

                  nops_auth_url = properties["NopsAuthURL"]
                  nops_project_url = f"{nops_auth_url}c/admin/projectaws/"
                  nops_integration_metadata_url = f"{nops_auth_url}c/aws/integration/"

                  print("Getting nOps project ID")
                  headers = {
                      "Accept": "application/json",
                      "X-Nops-Api-Key": api_key,
                  }
                  response = http.request("GET", nops_project_url, headers=headers)
                  print("nOps projects response: ", response.data)
                  project_data = json.loads(response.data)

                  project_id = ""
                  found_project = False

                  for project in project_data:
                      if project["account_number"] is None:
                          # Case where this is onboarding the master account for the first time
                          print("Found project with no account number, using it as a project")
                          project_id = project["id"]
                          found_project = True
                          break
                      if aws_account_number == project["account_number"]:
                          # For any other account that were autodiscovered
                          print(f"Found project for account {aws_account_number}, using it as a project")
                          project_id = project["id"]
                          found_project = True
                          break

                  if not found_project:
                      # Case that account wasn't autodiscovered, we create the project from here
                      print("Project for this account not found, creating new one...")
                      body = json.dumps({
                          "name" : f"AWS Account {aws_account_number}",
                          "account_number": aws_account_number,
                          "master_payer_account_number": master_account_id,
                      })
                      headers = {
                          "Content-Type": "application/json",
                          "X-Nops-Api-Key": api_key,
                      }
                      create_response = http.request("POST", nops_project_url, headers=headers, body=body)
                      print(f"nOps create project response: ", create_response.data)
                      project_data = json.loads(create_response.data)
                      project_id = project_data["id"]

                  print("Getting nOps project integration metadata")
                  headers = {
                      "Content-Type": "application/json",
                      "X-Nops-Api-Key": api_key,
                  }
                  integration_query_params = {"project_id": project_id}
                  integration_response = http.request("GET", nops_integration_metadata_url, headers=headers, fields=integration_query_params)
                  print("nOps integration metadata response: ", integration_response.data)
                  integration_data = json.loads(integration_response.data)
                  active_token = integration_data["active_token"]

                  nops_auth_url = properties["NopsAuthURL"]
                  nops_integration_complete_url = nops_integration_metadata_url + '?active_token=' + active_token

                  print("Sending nOps integration callback")
                  request_data = {
                      "ResourceProperties": {
                          "AWSAccountID": aws_account_number,
                          "ServiceBucket": properties["ServiceBucket"],
                          "RoleArn": properties["RoleArn"],
                          "ExternalID": properties["ExternalID"],
                      },
                      "RequestType": event["RequestType"],
                  }
                  request_data = json.dumps(request_data).encode('utf-8')
                  headers = {
                      "Content-Type": "application/json",
                      "X-Aws-Account-Number": aws_account_number,
                  }
                  response = http.request("POST", nops_integration_complete_url, body=request_data, headers=headers)
                  print("nOps callback response: ", response.data)

                  if is_master_payer and onboard_child_accounts:
                      print("Creating all child ProjectAWS in nOps...")
                      nops_project_organization_url = f"{nops_auth_url}c/admin/projectaws/organization_accounts/"
                      headers = {
                          "Accept": "application/json",
                          "X-Nops-Api-Key": api_key,
                      }
                      response = http.request("GET", nops_project_organization_url, headers=headers)
                      print("nOps projects organization accounts response: ", response.data)

                      print("Getting organization ID")
                      roots = organizations_client.list_roots()
                      root_organization_ids = ",".join([root["Id"] for root in roots["Roots"]])

                      print("Creating StackSet for child accounts")
                      active_url = integration_data["active_url"]
                      stack_template_url = active_url.split('templateURL=')[1].split('&')[0]

                      cloudformation_client = boto3.client("cloudformation")
                      client_id = api_key.split('.')[0]
                      stack_set_name = f"nOps-Integration-{root_organization_ids}-{client_id}"
                      response = http.request("GET", stack_template_url)
                      template_body = response.data.decode('utf-8')
                      response = cloudformation_client.create_stack_set(
                          StackSetName=stack_set_name,
                          TemplateBody=template_body,
                          Parameters=[
                              {"ParameterKey": "ApiKey", "ParameterValue": api_key},
                              {"ParameterKey": "PayerAccount", "ParameterValue": "false"},
                              {"ParameterKey": "MinRequiredPermissions", "ParameterValue": str(properties["MinRequiredPermissions"])},
                              {"ParameterKey": "OnboardChildAccounts", "ParameterValue": "false"},
                          ],
                          Capabilities=["CAPABILITY_IAM"],
                          Tags=[{"Key": "createdby", "Value": "nOps"}],
                          PermissionModel="SERVICE_MANAGED",
                          AutoDeployment={
                              "Enabled": True,
                              "RetainStacksOnAccountRemoval": False
                          }
                      )
                      print("StackSet creation response: ", response)

                      response = cloudformation_client.create_stack_instances(
                          StackSetName=stack_set_name,
                          DeploymentTargets={
                              "AccountFilterType": "DIFFERENCE",
                              "Accounts": [aws_account_number],
                              "OrganizationalUnitIds": [root_organization_ids]
                          },
                          Regions=[properties["Region"]],
                          OperationPreferences={
                              "FailureTolerancePercentage": 25,
                              "MaxConcurrentPercentage": 25
                          }
                      )
                      print("StackSet instances created successfully: ", response)

                  stack_response["Message"] = "Resource creation successful!"
                  cfnresponse.send(event, context, "SUCCESS", stack_response)
              except Exception as e:
                  print("ERROR: ", e)
                  stack_response["Message"] = f"Resource creation failed: {e}"
                  cfnresponse.send(event, context, "FAILED", stack_response)

      Runtime: python3.12
      Handler: index.lambda_handler
      Role: !GetAtt [NopsCallBackLambdaExecutionRole, Arn]
      Timeout: 45
      Description: |
        Lambda function to update nOps infra with role creation output.

  NopsLambdaTrigger:
    Type: Custom::CustomResource
    DependsOn:
      - NopsIntegrationRole
      - NopsCallBack
      - WaitForResources
    Properties:
      IsPayerAccount: !If [IsPayerAccount, 'true', 'false']
      RoleArn: !GetAtt [NopsIntegrationRole, Arn]
      ServiceToken: !GetAtt [NopsCallBack, Arn]
      ServiceBucket:
        Fn::If:
          - IsPayerAccount
          - Fn::Join:
            - "-"
            - - "nops"
              - !Ref AWS::AccountId
              - Fn::Select:
                - 0
                - Fn::Split:
                  - "."
                  - !Ref ApiKey
          - "na"
      NopsAuthURL: !FindInMap [attributes, Nops, AuthURL]
      AWSAccountID: !Sub ${AWS::AccountId}
      ExternalID:
        Fn::Base64:
          Fn::Join:
            - "-"
            - - !Ref ApiKey
              - Fn::Select:
                - 2
                - Fn::Split:
                  - "/"
                  - !Ref AWS::StackId
      ApiKey: !Sub ${ApiKey}
      MinRequiredPermissions: !If [MinRequiredPermissions, 'true', 'false']
      OnboardChildAccounts: !If [OnboardChildAccounts, 'true', 'false']
      Region: !Ref AWS::Region

Outputs:
  RoleArn:
    Description: The ARN of the IAM role
    Value: !GetAtt [NopsIntegrationRole, Arn]

  Message:
    Description: The message from the lambda trigger.
    Value: !GetAtt [NopsLambdaTrigger, Message]

  LambdaLookupStackFunctionARN:
    Description: The arn of the lambda function.
    Value: !GetAtt [NopsCallBack, Arn]
