Skip to content

Commit e584010

Browse files
authored
add local assets bucket (aws-solutions-library-samples#1144)
1 parent cf3c069 commit e584010

File tree

3 files changed

+238
-10
lines changed

3 files changed

+238
-10
lines changed

cfn-templates/cid-admin-policies.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ Resources:
161161
- s3:GetBucketLocation
162162
- s3:AbortMultipartUpload
163163
- s3:ListMultipartUploadParts
164+
- s3:PutBucketTagging
164165
Effect: Allow
165166
Resource:
166167
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-shared
@@ -567,6 +568,7 @@ Resources:
567568
- s3:PutEncryptionConfiguration
568569
- s3:PutLifecycleConfiguration
569570
- s3:PutObject
571+
- s3:PutBucketTagging
570572
Effect: Allow
571573
Resource:
572574
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-shared
@@ -702,13 +704,15 @@ Resources:
702704
- s3:PutLifecycleConfiguration
703705
- s3:PutObject
704706
- s3:PutReplicationConfiguration
707+
- s3:PutBucketTagging
705708
Effect: Allow
706709
Resource:
707710
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-local
708711
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-local/*
709712
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-data-local
710713
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-data-local/*
711-
714+
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-local-assets
715+
- !Sub arn:${AWS::Partition}:s3:::cid-${AWS::AccountId}-local-assets/*
712716
- Sid: ReadOnly
713717
Action:
714718
- cloudformation:GetTemplate

cfn-templates/cid-cfn.yml

Lines changed: 230 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

245260
Conditions:
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

311338
Mappings:
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

335371
Resources:
@@ -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

cfn-templates/tests/test_deploy_with_permissions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,9 @@ def create_cid_as_finops(update):
293293
{"ParameterKey": 'QuickSightUser', "ParameterValue": get_qs_user()},
294294
{"ParameterKey": 'CURVersion', "ParameterValue": '2.0'},
295295
{"ParameterKey": 'DeployCUDOSv5', "ParameterValue": 'yes'},
296-
{"ParameterKey": 'LambdaLayerBucketPrefix', "ParameterValue": TMP_BUCKET_PREFIX},
296+
{"ParameterKey": 'CreateLocalAssetsBucket', "ParameterValue": 'yes'},
297+
{"ParameterKey": 'ReferenceAssetsBucket', "ParameterValue": TMP_BUCKET},
298+
297299
],
298300
Capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
299301
)

0 commit comments

Comments
 (0)