@@ -39,6 +39,8 @@ Metadata:
3939 - DeployCUDOSDashboard
4040 - DataBucketsKmsKeysArns
4141 - ShareDashboard
42+ - CreateLocalAssetsBucket
43+ - ReferenceAssetsBucket
4244 - Label :
4345 default : ' Legacy'
4446 Parameters :
@@ -98,6 +100,10 @@ Metadata:
98100 default : " Secondary Tag for Compute Optimizer dashboard"
99101 KeepLegacyCURTable :
100102 default : " Keep Legacy CUR Table"
103+ CreateLocalAssetsBucket :
104+ default : " Create Local Assets Bucket"
105+ ReferenceAssetsBucket :
106+ default : " Reference Assets Bucket Name"
101107 cfn-lint :
102108 config :
103109 ignore_checks :
@@ -241,6 +247,15 @@ Parameters:
241247 Description : Choose 'yes' if you want to keep the Legacy CUR table
242248 Default : " no"
243249 AllowedValues : ["yes", "no"]
250+ CreateLocalAssetsBucket :
251+ Type : String
252+ Description : Choose 'yes' if you want to copy reference CID Assets to a local assets buckets (for a set of regions including China and Gov it will be done automatically).
253+ Default : " no"
254+ AllowedValues : ["yes", "no"]
255+ ReferenceAssetsBucket :
256+ Type : String
257+ Description : Source bucket for CID Assets to sync from.
258+ Default : ' aws-managed-cost-intelligence-dashboards-us-east-1'
244259
245260Conditions :
246261 NeedCUDOSDashboard : !Equals [ !Ref DeployCUDOSDashboard, "yes" ]
@@ -307,9 +322,22 @@ Conditions:
307322 - !Equals [!Ref "AWS::Region", "us-gov-east-1"]
308323 - !Equals [!Ref "AWS::Region", "us-gov-west-1"]
309324 LambdaLayerBucketPrefixIsManaged : !Equals [!Ref LambdaLayerBucketPrefix, 'aws-managed-cost-intelligence-dashboards']
325+ NeedLocalAssets :
326+ Fn::Or :
327+ - !Equals [ !Ref CreateLocalAssetsBucket, "yes" ] # explicity requested local assets
328+ - Fn::Or : # or region where CID do not have a public bucket
329+ - !Equals [!Ref "AWS::Region", "af-south-1"]
330+ - !Equals [!Ref "AWS::Region", "ap-southeast-3"]
331+ - !Equals [!Ref "AWS::Region", "eu-central-2"]
332+ - !Equals [!Ref "AWS::Region", "eu-south-1"]
333+ - !Equals [!Ref "AWS::Region", "eu-south-2"]
334+ - !Equals [!Ref "AWS::Region", "cn-north-1"]
335+ - !Equals [!Ref "AWS::Region", "us-gov-east-1"]
336+ - !Equals [!Ref "AWS::Region", "us-gov-west-1"]
310337
311338Mappings :
312- RegionMap : # CID has AWS managed buckets for deploy. Region must support QuickSight ( curl https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonQuickSight/current/region_index.json -s | jq '.regions | keys' )
339+ RegionMap :
340+ # CID has AWS managed buckets for deploy. Region must support QuickSight ( curl https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonQuickSight/current/region_index.json -s | jq '.regions | keys' )
313341 ap-northeast-1 : {BucketName: aws-managed-cost-intelligence-dashboards-ap-northeast-1}
314342 ap-northeast-2 : {BucketName: aws-managed-cost-intelligence-dashboards-ap-northeast-2}
315343 ap-south-1 : {BucketName: aws-managed-cost-intelligence-dashboards-ap-south-1}
@@ -326,10 +354,18 @@ Mappings:
326354 us-east-2 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-2}
327355 us-west-1 : {BucketName: aws-managed-cost-intelligence-dashboards-us-west-1}
328356 us-west-2 : {BucketName: aws-managed-cost-intelligence-dashboards-us-west-2}
329- # todo: add af-south-1
330- # todo: add ap-southeast-3
331- # todo: add eu-south-1
332- # todo: add eu-central-2
357+
358+ # regions without public buckets
359+ af-south-1 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1}
360+ ap-southeast-3 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1}
361+ eu-central-2 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1}
362+ eu-south-1 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1}
363+ eu-south-2 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1}
364+
365+ # specific regions
366+ cn-north-1 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1} # China
367+ us-gov-east-1 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1} # GovCloud (by default must be in commercial region)
368+ us-gov-west-1 : {BucketName: aws-managed-cost-intelligence-dashboards-us-east-1}
333369
334370
335371Resources :
@@ -1836,22 +1872,208 @@ Resources:
18361872 - id : ' W92'
18371873 reason : " No need for reserved concurrency"
18381874
1875+ LocalAssetsBucket :
1876+ Type : AWS::S3::Bucket
1877+ Condition : NeedLocalAssets
1878+ Properties :
1879+ BucketName : !Sub "cid-${AWS::AccountId}-local-assets"
1880+ BucketEncryption :
1881+ ServerSideEncryptionConfiguration :
1882+ - ServerSideEncryptionByDefault :
1883+ SSEAlgorithm : AES256
1884+ AccessControl : BucketOwnerFullControl
1885+ OwnershipControls :
1886+ Rules :
1887+ - ObjectOwnership : BucketOwnerEnforced
1888+ PublicAccessBlockConfiguration :
1889+ BlockPublicAcls : true
1890+ BlockPublicPolicy : true
1891+ IgnorePublicAcls : true
1892+ RestrictPublicBuckets : true
1893+ Metadata :
1894+ cfn-lint :
1895+ config :
1896+ ignore_checks :
1897+ - W3045 # Not needed AWS::S3::BucketPolicy
1898+ cfn_nag :
1899+ rules_to_suppress :
1900+ - id : ' W35'
1901+ reason : " Data buckets would generate too much logs"
1902+ - id : ' W51'
1903+ reason : " No policy needed"
1904+ PullAssetsLambdaRole :
1905+ Type : AWS::IAM::Role
1906+ Condition : NeedLocalAssets
1907+ Properties :
1908+ AssumeRolePolicyDocument :
1909+ Version : ' 2012-10-17'
1910+ Statement :
1911+ - Effect : Allow
1912+ Principal :
1913+ Service : lambda.amazonaws.com
1914+ Action : sts:AssumeRole
1915+ ManagedPolicyArns :
1916+ - !Sub ' arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
1917+ Policies :
1918+ - PolicyName : S3SyncPolicy
1919+ PolicyDocument :
1920+ Version : ' 2012-10-17'
1921+ Statement :
1922+ - Effect : Allow
1923+ Action :
1924+ - s3:GetObject
1925+ - s3:ListBucket
1926+ - s3:ListBucketVersions
1927+ Resource :
1928+ - !Sub ' arn:${AWS::Partition}:s3:::${ReferenceAssetsBucket}'
1929+ - !Sub ' arn:${AWS::Partition}:s3:::${ReferenceAssetsBucket}/*'
1930+ - Effect : Allow
1931+ Action :
1932+ - s3:PutObject
1933+ - s3:DeleteObject
1934+ - s3:DeleteObjectVersion
1935+ - s3:DeleteObjectVersionTagging
1936+ - s3:ListBucket
1937+ - s3:ListBucketVersions
1938+ Resource :
1939+ - !Sub ' arn:${AWS::Partition}:s3:::${LocalAssetsBucket}'
1940+ - !Sub ' arn:${AWS::Partition}:s3:::${LocalAssetsBucket}/*'
1941+
1942+ PullAssetsLambda :
1943+ Type : AWS::Lambda::Function
1944+ Condition : NeedLocalAssets
1945+ Properties :
1946+ FunctionName : !Sub 'CidPullAssets${Suffix}'
1947+ Handler : index.handler
1948+ Role : !GetAtt PullAssetsLambdaRole.Arn
1949+ Runtime : python3.12
1950+ Timeout : 300
1951+ MemorySize : 256
1952+ Code :
1953+ ZipFile : |
1954+ import json
1955+ import logging
1956+ import tempfile
1957+ import urllib.request
1958+ from io import BytesIO
1959+ import boto3
1960+ import cfnresponse
1961+
1962+ logger = logging.getLogger()
1963+ logger.setLevel(logging.INFO)
1964+
1965+ def handler(event, context):
1966+ logger.info(f'Received event: {json.dumps(event)}')
1967+ request_type = event['RequestType']
1968+ resource_props = event.get('ResourceProperties', {})
1969+ old_keys = event.get("OldResourceProperties", {}).get("Keys", [])
1970+ try:
1971+ source_bucket = resource_props['SourceBucket']
1972+ destination_bucket = resource_props['DestinationBucket']
1973+ keys = resource_props.get('Keys', [])
1974+ except KeyError as e:
1975+ logger.error(f"Missing required property: {e}")
1976+ cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': f"Missing required property: {str(e)}"})
1977+ return
1978+ try:
1979+ if request_type == 'Create' or request_type == 'Update':
1980+ sync_buckets(source_bucket, destination_bucket, keys, old_keys)
1981+ cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, destination_bucket)
1982+ elif request_type == 'Delete':
1983+ clean_bucket(destination_bucket)
1984+ cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, destination_bucket)
1985+ else:
1986+ raise ValueError(f"Unsupported request type: {request_type}")
1987+ except Exception as e:
1988+ logger.error(f"Error on {request_type}: {str(e)}")
1989+ cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
1990+
1991+ def get_content(source_bucket, key):
1992+ """ Download the content using public bucket to temporary file
1993+ """
1994+ object_url = f"https://{source_bucket}.s3.amazonaws.com/{key}"
1995+ logger.info(f"Downloading from {object_url}")
1996+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
1997+ with urllib.request.urlopen(object_url) as response:
1998+ while True:
1999+ chunk = response.read(8192)
2000+ if not chunk:
2001+ break
2002+ temp_file.write(chunk)
2003+ return temp_file.name
2004+
2005+ def get_content_from_s3(source_bucket, key):
2006+ s3_client = boto3.client('s3')
2007+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
2008+ s3_client.download_file(
2009+ Bucket=source_bucket,
2010+ Key=key,
2011+ Filename=temp_file.name
2012+ )
2013+ return temp_file.name
2014+
2015+ def sync_buckets(source_bucket, destination_bucket, keys, old_keys):
2016+ """Sync contents from source bucket to destination bucket using HTTPS download and upload
2017+ """
2018+ s3_client = boto3.client('s3')
2019+ # Delete keys that are no longer needed
2020+ for key in old_keys:
2021+ if key not in keys:
2022+ try:
2023+ s3_client.delete_object(Bucket=destination_bucket, Key=key)
2024+ logger.info(f"Deleted {destination_bucket}/{key}")
2025+ except Exception as e:
2026+ logger.info(f"Skipping deletion of {destination_bucket}/{key}: {str(e)}")
2027+ # Download from public URL and upload to destination bucket
2028+ for key in keys:
2029+ try:
2030+ try:
2031+ temp_file_path = get_content_from_s3(source_bucket, key)
2032+ except Exception as e:
2033+ logger.warning(f"Error processing {key}: {str(e)}")
2034+ logger.info(f"Trying public url")
2035+ temp_file_path = get_content(source_bucket, key)
2036+ s3_client.upload_file(temp_file_path, destination_bucket, key)
2037+ logger.info(f"Done uploading to {destination_bucket}/{key}")
2038+ except Exception as e:
2039+ logger.error(f"Error processing {key}: {str(e)}")
2040+ raise e
2041+
2042+ def clean_bucket(bucket_name):
2043+ """Delete all objects in the bucket
2044+ """
2045+ boto3.resource('s3').Bucket(bucket_name).object_versions.delete()
2046+ logger.info(f"Done cleanup {bucket_name}")
2047+ SyncLocalAssets :
2048+ Type : Custom::BucketSync
2049+ Condition : NeedLocalAssets
2050+ Properties :
2051+ ServiceToken : !GetAtt PullAssetsLambda.Arn
2052+ SourceBucket : !Ref ReferenceAssetsBucket
2053+ DestinationBucket : !Ref LocalAssetsBucket
2054+ Keys :
2055+ - ' cid-resource-lambda-layer/cid-4.0.12.zip' # replace version here if needed
2056+
18392057 CidResourceLambdaLayer :
18402058 Type : AWS::Lambda::LayerVersion
18412059 Properties :
18422060 LayerName : !Sub 'CidLambdaLayer${Suffix}'
18432061 Description : An AWS managed layer with a cid-cmd package installed
18442062 Content :
18452063 S3Bucket : !If
1846- - LambdaLayerBucketPrefixIsManaged
1847- - !FindInMap [RegionMap, !Ref 'AWS::Region', BucketName]
1848- - !Sub ' ${LambdaLayerBucketPrefix}-${AWS::Region}' # Region added for backward compatibility
2064+ - NeedLocalAssets
2065+ - !Ref SyncLocalAssets
2066+ - Fn::If :
2067+ - LambdaLayerBucketPrefixIsManaged
2068+ - !FindInMap [RegionMap, !Ref 'AWS::Region', BucketName]
2069+ - !Sub ' ${LambdaLayerBucketPrefix}-${AWS::Region}' # Region added for backward compatibility
18492070 S3Key : ' cid-resource-lambda-layer/cid-4.0.12.zip' # replace version here if needed
18502071 CompatibleRuntimes :
18512072 - python3.10
18522073 - python3.11
18532074 - python3.12
18542075
2076+
18552077 CostIntelligenceDashboard :
18562078 Type : Custom::CidDashboard
18572079 Condition : NeedCostIntelligenceDashboard
0 commit comments