Skip to content

Commit e2acaca

Browse files
authored
Merge pull request #32 from aws-samples/feature/refactor-extras
Refactoring extras to align with contributing guidelines
2 parents f6e02c1 + 601a6b3 commit e2acaca

File tree

5 files changed

+225
-119
lines changed

5 files changed

+225
-119
lines changed

extras/aws-control-tower/helper-scripts/list-config-recorder-status.py

Lines changed: 105 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
# SPDX-License-Identifier: MIT-0
44
########################################################################
55
import boto3
6-
from botocore.exceptions import ClientError
76
import logging
7+
from botocore.exceptions import ClientError
8+
from concurrent.futures import ThreadPoolExecutor, as_completed
89

910
"""
1011
The purpose of this script is to check if AWS Config is enabled in each AWS account and region within an AWS Control
@@ -17,17 +18,21 @@
1718
python3 list-config-recorder-status.py
1819
"""
1920

20-
# Setup Default Logger
21-
logger = logging.getLogger(__name__)
22-
logger.setLevel(logging.INFO)
21+
# Logging Settings
22+
LOGGER = logging.getLogger()
23+
logging.getLogger("boto3").setLevel(logging.CRITICAL)
24+
logging.getLogger("botocore").setLevel(logging.CRITICAL)
25+
logging.getLogger("s3transfer").setLevel(logging.CRITICAL)
26+
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
2327

2428
SESSION = boto3.Session()
2529
STS_CLIENT = boto3.client('sts')
2630
AWS_PARTITION = "aws"
2731
ASSUME_ROLE_NAME = "AWSControlTowerExecution"
32+
MAX_THREADS = 16
2833

2934

30-
def assume_role(aws_account_number, role_name, session_name):
35+
def assume_role(aws_account_number: str, role_name: str, session_name: str):
3136
"""
3237
Assumes the provided role in the provided account and returns a session
3338
:param aws_account_number: AWS Account Number
@@ -46,12 +51,12 @@ def assume_role(aws_account_number, role_name, session_name):
4651
aws_secret_access_key=response["Credentials"]["SecretAccessKey"],
4752
aws_session_token=response["Credentials"]["SessionToken"],
4853
)
49-
logger.debug(f"Assumed session for {aws_account_number}")
54+
LOGGER.debug(f"...Assumed session for {aws_account_number}")
5055

5156
return session
5257
except Exception as exc:
53-
print(f"Unexpected error: {exc}")
54-
raise ValueError("Error assuming role")
58+
LOGGER.error(f"Unexpected error: {exc}")
59+
exit(1)
5560

5661

5762
def get_all_organization_accounts(account_info: bool, exclude_account_id: str):
@@ -76,11 +81,11 @@ def get_all_organization_accounts(account_info: bool, exclude_account_id: str):
7681
accounts.append(account_record)
7782
account_ids.append(acct["Id"])
7883
except ClientError as ce:
79-
print(f"get_all_organization_accounts error: {ce}")
84+
LOGGER.error(f"get_all_organization_accounts error: {ce}")
8085
raise ValueError("Error getting accounts")
8186
except Exception as exc:
82-
print(f"get_all_organization_accounts error: {exc}")
83-
raise ValueError("Unexpected error getting accounts")
87+
LOGGER.error(f"get_all_organization_accounts error: {exc}")
88+
exit(1)
8489

8590
if account_info:
8691
return accounts
@@ -100,13 +105,14 @@ def is_region_available(region):
100105
return True
101106
except ClientError as error:
102107
if "InvalidClientTokenId" in str(error):
103-
print(f"Region: {region} is not available")
108+
LOGGER.error(f"Region: {region} is not available")
104109
return False
105110
else:
106-
print(f"{error}")
111+
LOGGER.error(f"{error}")
107112

108113

109-
def get_available_service_regions(user_regions: str, aws_service: str, control_tower_regions_only: bool = False) -> list:
114+
def get_available_service_regions(user_regions: str, aws_service: str,
115+
control_tower_regions_only: bool = False) -> list:
110116
"""
111117
Get the available regions for the AWS service
112118
:param: user_regions
@@ -115,9 +121,10 @@ def get_available_service_regions(user_regions: str, aws_service: str, control_t
115121
:return: available region list
116122
"""
117123
available_regions = []
124+
service_regions = []
118125
try:
119126
if user_regions.strip():
120-
print(f"USER REGIONS: {str(user_regions)}")
127+
LOGGER.info(f"USER REGIONS: {user_regions}")
121128
service_regions = [value.strip() for value in user_regions.split(",") if value != '']
122129
elif control_tower_regions_only:
123130
cf_client = SESSION.client('cloudformation')
@@ -130,19 +137,17 @@ def get_available_service_regions(user_regions: str, aws_service: str, control_t
130137
region_set.add(summary["Region"])
131138
service_regions = list(region_set)
132139
else:
133-
service_regions = boto3.session.Session().get_available_regions(
134-
aws_service
135-
)
136-
print(f"SERVICE REGIONS: {service_regions}")
140+
service_regions = boto3.session.Session().get_available_regions(aws_service)
141+
LOGGER.info(f"SERVICE REGIONS: {service_regions}")
137142
except ClientError as ce:
138-
print(f"get_available_service_regions error: {ce}")
139-
raise ValueError("Error getting service regions")
143+
LOGGER.error(f"get_available_service_regions error: {ce}")
144+
exit(1)
140145

141146
for region in service_regions:
142147
if is_region_available(region):
143148
available_regions.append(region)
144149

145-
print(f"AVAILABLE REGIONS: {available_regions}")
150+
LOGGER.info(f"AVAILABLE REGIONS: {available_regions}")
146151
return available_regions
147152

148153

@@ -167,29 +172,84 @@ def get_service_client(aws_service: str, aws_region: str, session=None):
167172
return service_client
168173

169174

170-
if __name__ == "__main__":
171-
account_ids = get_all_organization_accounts(False, "")
172-
available_regions = get_available_service_regions("", "config", True)
173-
account_set = set()
174-
for account_id in account_ids:
175-
try:
176-
session = assume_role(account_id, ASSUME_ROLE_NAME, "ConfigRecorderCheck")
177-
except Exception as error:
178-
print(f"Unable to assume {ASSUME_ROLE_NAME} in {account_id} {error}")
179-
continue
180-
181-
for region in available_regions:
182-
try:
183-
session_config = get_service_client("config", region, session)
184-
response = session_config.describe_configuration_recorders()
185-
if "ConfigurationRecorders" in response and response["ConfigurationRecorders"]:
186-
# print(f"{account_id} {region} - CONFIG ENABLED")
175+
def get_account_config(account_id, regions):
176+
"""
177+
get_account_config
178+
:param account_id:
179+
:param regions:
180+
:return:
181+
"""
182+
region_count = 0
183+
config_recorder_count = 0
184+
all_regions_enabled = False
185+
enabled_regions = []
186+
not_enabled_regions = []
187+
188+
session = assume_role(account_id, ASSUME_ROLE_NAME, "ConfigRecorderCheck")
189+
190+
for region in regions:
191+
region_count += 1
192+
session_config = get_service_client("config", region, session)
193+
config_recorders = session_config.describe_configuration_recorders()
194+
195+
if config_recorders.get("ConfigurationRecorders", ""):
196+
LOGGER.debug(f"{account_id} {region} - CONFIG ENABLED")
197+
config_recorder_count += 1
198+
enabled_regions.append(region)
199+
else:
200+
LOGGER.debug(f"{account_id} {region} - CONFIG NOT ENABLED")
201+
not_enabled_regions.append(region)
202+
203+
if region_count == config_recorder_count:
204+
all_regions_enabled = True
205+
206+
return account_id, all_regions_enabled, enabled_regions, not_enabled_regions
207+
208+
209+
def get_config_recorder_status():
210+
"""
211+
get_config_recorder_status
212+
:return:
213+
"""
214+
try:
215+
account_ids = get_all_organization_accounts(False, "")
216+
available_regions = get_available_service_regions("", "config", True)
217+
account_set = set()
218+
processes = []
219+
220+
if MAX_THREADS > len(account_ids):
221+
thread_cnt = len(account_ids) - 2
222+
else:
223+
thread_cnt = MAX_THREADS
224+
225+
with ThreadPoolExecutor(max_workers=thread_cnt) as executor:
226+
for account_id in account_ids:
227+
try:
228+
processes.append(executor.submit(
229+
get_account_config,
230+
account_id,
231+
available_regions
232+
))
233+
except Exception as error:
234+
LOGGER.error(f"{error}")
187235
continue
188-
else:
189-
print(f"{account_id} {region} - CONFIG NOT ENABLED")
190-
account_set.add(account_id)
191-
except ClientError as error:
192-
print(f"Client Error - {error}")
193-
print(f'Accounts to exclude from Organization Conformance Pack: {",".join(list(account_set))}')
194236

237+
for task in as_completed(processes, timeout=300):
238+
account_id, all_regions_enabled, enabled_regions, not_enabled_regions = task.result()
239+
LOGGER.info(f"Account ID: {account_id}")
240+
LOGGER.info(f"Regions Enabled = {enabled_regions}")
241+
LOGGER.info(f"Regions Not Enabled = {not_enabled_regions}\n")
242+
if not all_regions_enabled:
243+
account_set.add(account_id)
244+
245+
LOGGER.info(f'!!! Accounts to exclude from Organization Conformance Packs: {",".join(list(account_set))}')
246+
except Exception as error:
247+
LOGGER.error(f"{error}")
248+
exit(1)
249+
250+
251+
if __name__ == "__main__":
252+
# Set Log Level
253+
logging.basicConfig(level=logging.INFO, format="%(message)s")
195254

255+
get_config_recorder_status()

extras/aws-control-tower/prerequisites/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-
66

77
1. Deploy the [Customizations for AWS Control Tower](https://aws.amazon.com/solutions/implementations/customizations-for-aws-control-tower/)
88
Solution
9-
2. Required steps to deploy resources into the AWS Control Tower management account (e.g. Primary Account)
10-
1. Create an Organizational Unit (e.g. Management) for the Primary account
9+
2. Required steps to deploy resources into the AWS Control Tower Management account (e.g. Management Account)
10+
1. Create an Organizational Unit (e.g. Management) for the Management account
1111
1. Review the [Manage Accounts Through AWS Organizations](https://docs.aws.amazon.com/controltower/latest/userguide/organizations.html)
1212
documentation
13-
2. Move the Primary account into the new Organizational Unit
14-
3. Create the AWSControlTowerExecution IAM role in the Primary account
13+
2. Move the Management account into the new Organizational Unit
14+
3. Create the AWSControlTowerExecution IAM role in the Management account
1515
1. Use the [prereq-controltower-execution-role.yaml](prereq-controltower-execution-role.yaml) template to
16-
create a CloudFormation stack in the Primary account.
16+
create a CloudFormation stack in the Management account.
1717
3. Create an S3 bucket for the Lambda source code
1818
1. Use the [prereq-lambda-s3-bucket.yaml](prereq-lambda-s3-bucket.yaml) template to create a CloudFormation
19-
StackSet in the Primary account for each region that will deploy custom resources.
19+
StackSet in the Management account for each region that will deploy custom resources.
2020
4. Package the Lambda code and required libraries (e.g. solution/code/src) into a zip file and upload it to the
2121
Lambda source S3 bucket.
2222
1. Use the [packaging script](../../packaging-scripts/package-lambda.sh) to download the required libraries,
2323
create a zip file, and upload it to a provided S3 bucket. Usage details are at the top of the script.
2424
5. (Optional) Create SSM parameters for the AWS Account IDs and AWS Organizations ID
2525
1. Use the [prereq-ssm-account-params.yaml](prereq-ssm-account-params.yaml) template to create a CloudFormation
26-
stack in the Primary account.
26+
stack in the Management account.
2727

extras/aws-control-tower/prerequisites/prereq-controltower-execution-role.yaml

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
########################################################################
2+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
# SPDX-License-Identifier: MIT-0
4+
########################################################################
15
AWSTemplateFormatVersion: 2010-09-09
26
Description: AWS Control Tower Execution IAM Role Creation
37

@@ -8,39 +12,51 @@ Metadata:
812
default: Control Tower Role Attributes
913
Parameters:
1014
- pAWSControlTowerExecutionRoleName
11-
- pOrgPrimaryAccountId
1215
- pIAMTagKey
1316
- pIAMTagValue
17+
- pOrgPrimaryAccountId
1418

1519
ParameterLabels:
16-
pOrgPrimaryAccountId:
17-
default: AWS Organizations Primary Account ID
1820
pAWSControlTowerExecutionRoleName:
1921
default: AWS Control Tower Execution Role Name
2022
pIAMTagKey:
2123
default: IAM Tag Key
2224
pIAMTagValue:
2325
default: IAM Tag Value
26+
pOrgPrimaryAccountId:
27+
default: AWS Organizations Primary Account ID
2428

2529
Parameters:
2630
pOrgPrimaryAccountId:
27-
Type: String
31+
AllowedPattern: '^\d{12}$'
32+
ConstraintDescription: Must be 12 digits
2833
Description: Organization primary account ID
34+
Type: String
2935

3036
pAWSControlTowerExecutionRoleName:
31-
Type: String
32-
Description: AWS Control Tower execution role name
37+
AllowedPattern: '^[\w+=,.@-]{1,64}$'
38+
ConstraintDescription: Max 64 alphanumeric characters. Also special characters supported [+, =, ., @, -]
3339
Default: AWSControlTowerExecution
40+
Description: AWS Control Tower execution role name
41+
Type: String
3442

3543
pIAMTagKey:
36-
Type: String
44+
AllowedPattern: '^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$'
45+
ConstraintDescription:
46+
The string value can be Unicode characters and cannot be prefixed with "aws:".
47+
The string can contain only the set of Unicode letters, digits, white-space, '_', '.', '/', '=', '+', '-''
48+
Default: cfct
3749
Description: IAM tag key
38-
Default: control-tower
50+
Type: String
3951

4052
pIAMTagValue:
41-
Type: String
53+
AllowedPattern: '^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$'
54+
ConstraintDescription:
55+
The string value can be Unicode characters.
56+
The string can contain only the set of Unicode letters, digits, white-space, '_', '.', '/', '=', '+', '-'
57+
Default: managed-by-cfct
4258
Description: IAM tag key value
43-
Default: managed-by-control-tower
59+
Type: String
4460

4561
Resources:
4662
rAWSControlTowerRole:
@@ -57,11 +73,12 @@ Resources:
5773
AssumeRolePolicyDocument:
5874
Version: "2012-10-17"
5975
Statement:
60-
- Effect: Allow
76+
- Action: sts:AssumeRole
77+
Effect: Allow
6178
Principal:
6279
AWS:
6380
- !Sub arn:${AWS::Partition}:iam::${pOrgPrimaryAccountId}:root
64-
Action: sts:AssumeRole
81+
6582
Path: "/"
6683
ManagedPolicyArns:
6784
- !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess

0 commit comments

Comments
 (0)