# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 Parameters: S3Bucket: Type: String Description: S3 bucket name for CUR data S3Prefix: Type: String Description: S3 prefix for bucket storing CUR data Resources: costBucket0209400C: Type: AWS::S3::Bucket Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "Bucket provided as sample code for blog, server access log configuration should be provided by user if needed in their own implementation" Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 BucketName: Fn::Join: - "" - - cost-data- - Ref: AWS::Region - "-" - Ref: AWS::AccountId OwnershipControls: Rules: - ObjectOwnership: BucketOwnerPreferred PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true UpdateReplacePolicy: Delete DeletionPolicy: Delete costBucketPolicy96575D14: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: costBucket0209400C PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - costBucket0209400C - Arn - Fn::Join: - "" - - Fn::GetAtt: - costBucket0209400C - Arn - /* - Action: - s3:GetBucketAcl - s3:GetBucketPolicy Condition: StringEquals: aws:SourceAccount: Ref: AWS::AccountId aws:SourceArn: Fn::Join: - "" - - "arn:aws:cur:us-east-1:" - Ref: AWS::AccountId - :definition/* Effect: Allow Principal: Service: billingreports.amazonaws.com Resource: Fn::GetAtt: - costBucket0209400C - Arn - Action: s3:PutObject Condition: StringEquals: aws:SourceAccount: Ref: AWS::AccountId aws:SourceArn: Fn::Join: - "" - - "arn:aws:cur:us-east-1:" - Ref: AWS::AccountId - :definition/* Effect: Allow Principal: Service: billingreports.amazonaws.com Resource: Fn::Join: - "" - - Fn::GetAtt: - costBucket0209400C - Arn - /* Version: "2012-10-17" athenaResultBucketB25A9F01: Type: AWS::S3::Bucket Metadata: cfn_nag: rules_to_suppress: - id: W35 reason: "Bucket provided as sample code for blog, server access log configuration should be provided by user if needed in their own implementation" Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 BucketName: Fn::Join: - "" - - aws-athena-query-results-cur- - Ref: AWS::Region - "-" - Ref: AWS::AccountId OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true UpdateReplacePolicy: Delete DeletionPolicy: Delete athenaResultBucketPolicyFE38132E: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: athenaResultBucketB25A9F01 PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - athenaResultBucketB25A9F01 - Arn - Fn::Join: - "" - - Fn::GetAtt: - athenaResultBucketB25A9F01 - Arn - /* Version: "2012-10-17" curWorkGroup: Type: AWS::Athena::WorkGroup Properties: Name: emr-eks-cost-analysis WorkGroupConfiguration: EnforceWorkGroupConfiguration: true PublishCloudWatchMetricsEnabled: true ResultConfiguration: EncryptionConfiguration: EncryptionOption: SSE_S3 OutputLocation: Fn::Join: - "" - - s3:// - Ref: athenaResultBucketB25A9F01 DependsOn: - athenaResultBucketPolicyFE38132E - athenaResultBucketB25A9F01 kubecostpolicyamp: DependsOn: - kubecostAmp Type: AWS::IAM::ManagedPolicy Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "Name provided part of the blog for better understanding" Properties: PolicyDocument: Statement: - Action: - aps:RemoteWrite - aps:GetLabels - aps:GetMetricMetadata - aps:GetSeries - aps:QueryMetrics Effect: Allow Resource: - !Ref kubecostAmp Sid: AllowReadWriteToAmp Version: "2012-10-17" Description: Policy use by kubecost to read write to amp ManagedPolicyName: kubecost-amp-policy kubecostpolicy1D54E940: Type: AWS::IAM::ManagedPolicy Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "Name provided part of the blog for better understanding" Properties: PolicyDocument: Statement: - Action: - glue:BatchGetPartition - glue:GetDatabase* - glue:GetPartition* - glue:GetTable* - glue:GetUserDefinedFunction Effect: Allow Resource: - arn:aws:glue:*:*:catalog - arn:aws:glue:*:*:database/athenacurcfn* - arn:aws:glue:*:*:table/athenacurcfn*/* Sid: ReadAccessToAthenaCurDataViaGlue - Action: - s3:AbortMultipartUpload - s3:GetBucketLocation - s3:GetObject - s3:ListBucket - s3:ListBucketMultipartUploads - s3:ListMultipartUploadParts - s3:PutObject Effect: Allow Resource: - Fn::GetAtt: - athenaResultBucketB25A9F01 - Arn - Fn::Join: - "" - - Fn::GetAtt: - athenaResultBucketB25A9F01 - Arn - /* Sid: AthenaQueryResultsOutput - Action: - s3:ListAllMyBuckets - s3:ListBucket - s3:HeadBucket - s3:HeadObject - s3:List* - s3:Get* Effect: Allow Resource: - Fn::GetAtt: - costBucket0209400C - Arn - Fn::Join: - "" - - Fn::GetAtt: - costBucket0209400C - Arn - /* Sid: S3ReadAccessToAwsBillingData - Action: - athena:GetQueryExecution - athena:GetQueryResults - athena:StartQueryExecution Effect: Allow Resource: Fn::Join: - "" - - "arn:aws:athena:" - Ref: AWS::Region - ":" - Ref: AWS::AccountId - :workgroup/emr-eks-cost-analysis Sid: AthenaAccess Version: "2012-10-17" Description: Policy use by kubecost to reconcile with CUR data ManagedPolicyName: kubecost-cur-policy kubecostExporterF87D7003: Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "This is required to be on \"RETAIN\" as the ECR is not able to delete with images in it" Type: AWS::ECR::Repository Properties: ImageScanningConfiguration: ScanOnPush: true RepositoryName: emreks-compute-cost-exporter UpdateReplacePolicy: Retain DeletionPolicy: Retain AWSCURDatabase: Type: 'AWS::Glue::Database' Properties: DatabaseInput: Name: 'athenacurcfn_c_u_r' CatalogId: !Ref AWS::AccountId AWSCURCrawlerComponentFunction: Type: 'AWS::IAM::Role' Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: "Wild card is allowed on the rest of the path on a S3 bucket resource and cloudwatch log" Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole' Policies: - PolicyName: AWSCURCrawlerComponentFunction PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:UpdateDatabase' - 'glue:UpdatePartition' - 'glue:CreateTable' - 'glue:UpdateTable' - 'glue:ImportCatalogToGlue' Resource: '*' - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' Resource: !Sub - 'arn:${AWS::Partition}:s3:::${S3Bucket}/${S3Prefix}/${S3Prefix}/${S3Prefix}*' - S3Bucket: !Ref S3Bucket S3Prefix: !Ref S3Prefix - PolicyName: AWSCURKMSDecryption PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'kms:Decrypt' Resource: '*' AWSComputeCostCrawlerComponent: Type: 'AWS::IAM::Role' Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: "Wild card is allowed on the rest of the path on a S3 bucket resource and cloudwatch log" Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - glue.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSGlueServiceRole' Policies: - PolicyName: AWSCURCrawlerComponentFunction PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:UpdateDatabase' - 'glue:UpdatePartition' - 'glue:CreateTable' - 'glue:UpdateTable' - 'glue:ImportCatalogToGlue' Resource: '*' - Effect: Allow Action: - 's3:GetObject' - 's3:PutObject' Resource: !Sub - 'arn:${AWS::Partition}:s3:::${S3Bucket}/compute_cost*' - S3Bucket: !Ref S3Bucket - PolicyName: AWSCURKMSDecryption PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'kms:Decrypt' Resource: '*' AWSCURCrawlerLambdaExecutor: Type: 'AWS::IAM::Role' Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: "Wild card is allowed on the rest of the path on a S3 bucket resource and cloudwatch log" Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: AWSCURCrawlerLambdaExecutor PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 'glue:StartCrawler' Resource: '*' AWSCURCrawler: Type: 'AWS::Glue::Crawler' DependsOn: - AWSCURDatabase - AWSCURCrawlerComponentFunction Properties: Name: AWSCURCrawler-CUR Description: A recurring crawler that keeps your CUR table in Athena up-to-date. Role: !GetAtt AWSCURCrawlerComponentFunction.Arn DatabaseName: !Ref AWSCURDatabase Targets: S3Targets: - Path: !Sub - 's3://${S3Bucket}/${S3Prefix}/${S3Prefix}/${S3Prefix}' - S3Bucket: !Ref S3Bucket S3Prefix: !Ref S3Prefix Exclusions: - '**.json' - '**.yml' - '**.sql' - '**.csv' - '**.gz' - '**.zip' SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE DeleteBehavior: DELETE_FROM_DATABASE AWSComputeCrawler: Type: 'AWS::Glue::Crawler' DependsOn: - AWSCURDatabase Properties: Name: ComputeCostCrawler Description: A crawler to discover an update the compute cost data table Role: !GetAtt AWSComputeCostCrawlerComponent.Arn DatabaseName: !Ref AWSCURDatabase Targets: S3Targets: - Path: !Sub - 's3://${S3Bucket}/compute_cost' - S3Bucket: !Ref S3Bucket SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE DeleteBehavior: DELETE_FROM_DATABASE AWSCURInitializer: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: "Lambda does not need a VPC and is used as S3 event trigger" DependsOn: AWSCURCrawler Properties: Code: ZipFile: > const AWS = require('aws-sdk'); const response = require('./cfn-response'); exports.handler = function(event, context, callback) { if (event.RequestType === 'Delete') { response.send(event, context, response.SUCCESS); } else { const glue = new AWS.Glue(); glue.startCrawler({ Name: 'AWSCURCrawler-CUR' }, function(err, data) { if (err) { const responseData = JSON.parse(this.httpResponse.body); if (responseData['__type'] == 'CrawlerRunningException') { callback(null, responseData.Message); } else { const responseString = JSON.stringify(responseData); if (event.ResponseURL) { response.send(event, context, response.FAILED,{ msg: responseString }); } else { callback(responseString); } } } else { if (event.ResponseURL) { response.send(event, context, response.SUCCESS); } else { callback(null, response.SUCCESS); } } }); } }; Handler: 'index.handler' Timeout: 30 Runtime: nodejs16.x ReservedConcurrentExecutions: 1 Role: !GetAtt AWSCURCrawlerLambdaExecutor.Arn AWSStartCURCrawler: Type: 'Custom::AWSStartCURCrawler' Properties: ServiceToken: !GetAtt AWSCURInitializer.Arn AWSS3CUREventLambdaPermission: Type: AWS::Lambda::Permission Properties: Action: 'lambda:InvokeFunction' FunctionName: !GetAtt AWSCURInitializer.Arn Principal: 's3.amazonaws.com' SourceAccount: !Ref AWS::AccountId SourceArn: !Sub - 'arn:${AWS::Partition}:s3:::${S3Bucket}' - S3Bucket: !Ref S3Bucket AWSS3CURLambdaExecutor: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / Policies: - PolicyName: AWSS3CURLambdaExecutor PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:*:*' - Effect: Allow Action: - 's3:PutBucketNotification' Resource: !Sub - 'arn:${AWS::Partition}:s3:::${S3Bucket}' - S3Bucket: !Ref S3Bucket AWSS3CURNotification: Type: 'AWS::Lambda::Function' Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: "Lambda does not need a VPC and is used as S3 event trigger" DependsOn: - AWSCURInitializer - AWSS3CUREventLambdaPermission - AWSS3CURLambdaExecutor Properties: Code: ZipFile: > const AWS = require('aws-sdk'); const response = require('./cfn-response'); exports.handler = function(event, context, callback) { const s3 = new AWS.S3(); const putConfigRequest = function(notificationConfiguration) { return new Promise(function(resolve, reject) { s3.putBucketNotificationConfiguration({ Bucket: event.ResourceProperties.BucketName, NotificationConfiguration: notificationConfiguration }, function(err, data) { if (err) reject({ msg: this.httpResponse.body.toString(), error: err, data: data }); else resolve(data); }); }); }; const newNotificationConfig = {}; if (event.RequestType !== 'Delete') { newNotificationConfig.LambdaFunctionConfigurations = [{ Events: [ 's3:ObjectCreated:*' ], LambdaFunctionArn: event.ResourceProperties.TargetLambdaArn || 'missing arn', Filter: { Key: { FilterRules: [ { Name: 'prefix', Value: event.ResourceProperties.ReportKey } ] } } }]; } putConfigRequest(newNotificationConfig).then(function(result) { response.send(event, context, response.SUCCESS, result); callback(null, result); }).catch(function(error) { response.send(event, context, response.FAILED, error); console.log(error); callback(error); }); }; Handler: 'index.handler' Timeout: 30 Runtime: nodejs16.x ReservedConcurrentExecutions: 1 Role: !GetAtt AWSS3CURLambdaExecutor.Arn AWSPutS3CURNotification: Type: 'Custom::AWSPutS3CURNotification' Properties: ServiceToken: !GetAtt AWSS3CURNotification.Arn TargetLambdaArn: !GetAtt AWSCURInitializer.Arn BucketName: !Ref S3Bucket ReportKey: !Sub - '${S3Prefix}/${S3Prefix}/${S3Prefix}' - S3Prefix: !Ref S3Prefix AWSCURReportStatusTable: Type: 'AWS::Glue::Table' DependsOn: AWSCURDatabase Properties: DatabaseName: athenacurcfn_c_u_r CatalogId: !Ref AWS::AccountId TableInput: Name: 'cost_and_usage_data_status' TableType: 'EXTERNAL_TABLE' StorageDescriptor: Columns: - Name: status Type: 'string' InputFormat: 'org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat' OutputFormat: 'org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat' SerdeInfo: SerializationLibrary: 'org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe' Location: !Sub - 's3://${S3Bucket}/${S3Prefix}/${S3Prefix}/cost_and_usage_data_status/' - S3Bucket: !Ref S3Bucket S3Prefix: !Ref S3Prefix kubecostAmp: Type: AWS::APS::Workspace Properties: Alias: kubecost-amp kubecostexporterpolicyE6776D30: Type: AWS::IAM::ManagedPolicy Metadata: cfn_nag: rules_to_suppress: - id: W28 reason: "Name provided part of the blog for better understanding" Properties: PolicyDocument: Statement: - Action: s3:PutObject Effect: Allow Resource: Fn::Join: - "" - - Fn::GetAtt: - costBucket0209400C - Arn - /compute_cost/* Sid: AllowS3PutObject Version: "2012-10-17" Description: Policy used to export kubecost data to S3 ManagedPolicyName: kubecost-s3-exporter-policy Outputs: amp: Value: Fn::GetAtt: - kubecostAmp - WorkspaceId kubecostexporter: Value: Fn::Join: - "" - - Fn::Select: - 4 - Fn::Split: - ":" - Fn::GetAtt: - kubecostExporterF87D7003 - Arn - .dkr.ecr. - Fn::Select: - 3 - Fn::Split: - ":" - Fn::GetAtt: - kubecostExporterF87D7003 - Arn - "." - Ref: AWS::URLSuffix - / - Ref: kubecostExporterF87D7003