Skip to content

Commit

Permalink
added SNS topic to fanout the Lambda function
Browse files Browse the repository at this point in the history
  • Loading branch information
ievgeniia ieromenko committed Jan 17, 2024
1 parent b354401 commit c3d8e63
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 63 deletions.
10 changes: 10 additions & 0 deletions aws_sra_examples/easy_setup/templates/sra-easy-setup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,16 @@ Rules:
AssertDescription:
"'Security Full Name', 'Security Title', 'Security Email' and 'Security Phone' parameters are required if the 'Security Alternate Contact
Action' parameter is set to 'add'."
EnabledRegionValidation:
RuleCondition: !Equals [!Ref pCommonPrerequisitesRegionsOnly, 'false']
Assertions:
- Assert: !Not [!Equals [!Ref pConfigEnabledRegions, '']]
AssertDescription: "'Enabled Regions' parameter has to have a value if 'Common Prerequisites Regions Only' parameter is set to 'false'."
ResourceTypesValidation:
RuleCondition: !Equals [!Ref pAllSupported, 'false']
Assertions:
- AssertDescription: "'Resource Types' parameter is required if 'All Supported' parameter is set to 'false'."
Assert: !Not [!Equals [!Ref pResourceTypes, '']]

Conditions:
cUsingKmsKey: !Not [!Equals [!Ref pLambdaLogGroupKmsKey, '']]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
159 changes: 125 additions & 34 deletions aws_sra_examples/solutions/config/config_org/lambda/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from mypy_boto3_config.type_defs import DeliveryChannelTypeDef
from mypy_boto3_organizations import OrganizationsClient
from mypy_boto3_secretsmanager import SecretsManagerClient
from mypy_boto3_sns import SNSClient
from mypy_boto3_sns.type_defs import PublishBatchResponseTypeDef

LOGGER = logging.getLogger("sra")
log_level: str = os.environ.get("LOG_LEVEL", "ERROR")
Expand All @@ -35,12 +37,14 @@
UNEXPECTED = "Unexpected!"
SERVICE_NAME = "config.amazonaws.com"
SLEEP_SECONDS = 60
SNS_PUBLISH_BATCH_MAX = 10

helper = CfnResource(json_logging=True, log_level=log_level, boto_level="CRITICAL", sleep_on_delete=120)

try:
MANAGEMENT_ACCOUNT_SESSION = boto3.Session()
ORG_CLIENT: OrganizationsClient = MANAGEMENT_ACCOUNT_SESSION.client("organizations")
SNS_CLIENT: SNSClient = MANAGEMENT_ACCOUNT_SESSION.client("sns")
except Exception:
LOGGER.exception(UNEXPECTED)
raise ValueError("Unexpected error executing Lambda function. Review CloudWatch logs for details.") from None
Expand All @@ -57,40 +61,9 @@ def process_add_update_event(params: dict, regions: list, accounts: list) -> Non
LOGGER.info("...process_add_update_event")

if params["action"] in ["Add", "Update"]:
LOGGER.info("...Configure Config")
accounts = common.get_active_organization_accounts()
regions = common.get_enabled_regions(params["ENABLED_REGIONS"], params["CONTROL_TOWER_REGIONS_ONLY"] == "true")
resource_types = build_resource_types_param(params)

if params["ALL_SUPPORTED"] == "true":
all_supported = True
else:
all_supported = False
if params["INCLUDE_GLOBAL_RESOURCE_TYPES"] == "true":
include_global_resource_types = True
else:
include_global_resource_types = False

for account in accounts:
config.create_service_linked_role(account["AccountId"], params["CONFIGURATION_ROLE_NAME"])

for account in accounts:
for region in regions:
role_arn = (
f"arn:{params['AWS_PARTITION']}:iam::{account['AccountId']}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig"
)
config.set_config_in_org(
account["AccountId"],
region,
params["CONFIGURATION_ROLE_NAME"],
params["RECORDER_NAME"],
role_arn,
resource_types,
all_supported,
include_global_resource_types,
)
delivery_channel = set_delivery_channel_params(params, region)
config.set_delivery_channel(account["AccountId"], region, params["CONFIGURATION_ROLE_NAME"], delivery_channel)
setup_config_global(params, regions, accounts)

LOGGER.info("...ADD_UPDATE_NO_EVENT")

Expand Down Expand Up @@ -209,6 +182,7 @@ def get_validated_parameters(event: Dict[str, Any]) -> dict:
actions = {"Create": "Add", "Update": "Update", "Delete": "Remove"}
params["action"] = actions[event.get("RequestType", "Create")]
true_false_pattern = r"^true|false$"
sns_topic_pattern = r"^arn:(aws[a-zA-Z-]*){1}:sns:[a-z0-9-]+:\d{12}:[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$"

# Required Parameters
params.update(
Expand Down Expand Up @@ -307,6 +281,7 @@ def get_validated_parameters(event: Dict[str, Any]) -> dict:
pattern=r"^$|[a-z0-9-, ]+$",
)
)
params.update(parameter_pattern_validator("SNS_TOPIC_ARN_FANOUT", os.environ.get("SNS_TOPIC_ARN_FANOUT"), pattern=sns_topic_pattern))

# Optional Parameters
params.update(
Expand Down Expand Up @@ -391,6 +366,121 @@ def set_delivery_channel_params(params: dict, region: str) -> DeliveryChannelTyp
return delivery_channel


def setup_config_global(params: dict, regions: list, accounts: list) -> None:
"""Enable the Config service and configure its global settings.
Args:
params: Configuration Parameters
regions: list of regions
accounts: list of accounts
"""
for account in accounts:
config.create_service_linked_role(account["AccountId"], params["CONFIGURATION_ROLE_NAME"])

create_sns_messages(accounts, regions, params["SNS_TOPIC_ARN_FANOUT"], "configure")


def create_sns_messages(accounts: list, regions: list, sns_topic_arn_fanout: str, action: str) -> None:
"""Create SNS Message.
Args:
accounts: Account List
regions: list of AWS regions
sns_topic_arn_fanout: SNS Topic ARN
action: Action
"""
sns_messages = []
for region in regions:
sns_message = {"Accounts": accounts, "Region": region, "Action": action}
sns_messages.append(
{
"Id": region,
"Message": json.dumps(sns_message),
"Subject": "Config Configuration",
}
)

process_sns_message_batches(sns_messages, sns_topic_arn_fanout)


def publish_sns_message_batch(message_batch: list, sns_topic_arn_fanout: str) -> None:
"""Publish SNS Message Batches.
Args:
message_batch: Batch of SNS messages
sns_topic_arn_fanout: SNS Topic ARN
"""
LOGGER.info("Publishing SNS Message Batch")
LOGGER.info({"SNSMessageBatch": message_batch})
response: PublishBatchResponseTypeDef = SNS_CLIENT.publish_batch(TopicArn=sns_topic_arn_fanout, PublishBatchRequestEntries=message_batch)
api_call_details = {"API_Call": "sns:PublishBatch", "API_Response": response}
LOGGER.info(api_call_details)


def process_sns_message_batches(sns_messages: list, sns_topic_arn_fanout: str) -> None:
"""Process SNS Message Batches for Publishing.
Args:
sns_messages: SNS messages to be batched.
sns_topic_arn_fanout: SNS Topic ARN
"""
message_batches = []
for i in range(
SNS_PUBLISH_BATCH_MAX,
len(sns_messages) + SNS_PUBLISH_BATCH_MAX,
SNS_PUBLISH_BATCH_MAX,
):
message_batches.append(sns_messages[i - SNS_PUBLISH_BATCH_MAX : i])

for batch in message_batches:
publish_sns_message_batch(batch, sns_topic_arn_fanout)


def process_event_sns(event: dict) -> None:
"""Process SNS event to complete the setup process.
Args:
event: event data
"""
params = get_validated_parameters({})
for record in event["Records"]:
record["Sns"]["Message"] = json.loads(record["Sns"]["Message"])
LOGGER.info({"SNS Record": record})
message = record["Sns"]["Message"]
if message["Action"] == "configure":
LOGGER.info("Continuing process to enable Config (sns event)")
resource_types = build_resource_types_param(params)

if params["ALL_SUPPORTED"] == "true":
all_supported = True
else:
all_supported = False
if params["INCLUDE_GLOBAL_RESOURCE_TYPES"] == "true":
include_global_resource_types = True
else:
include_global_resource_types = False

for account in message["Accounts"]:
role_arn = (
f"arn:{params['AWS_PARTITION']}:iam::{account['AccountId']}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig"
)
config.set_config_in_org(
account["AccountId"],
message["Region"],
params["CONFIGURATION_ROLE_NAME"],
params["RECORDER_NAME"],
role_arn,
resource_types,
all_supported,
include_global_resource_types,
)
delivery_channel = set_delivery_channel_params(params, message["Region"])
for account in message["Accounts"]:
config.set_delivery_channel(account["AccountId"], message["Region"], params["CONFIGURATION_ROLE_NAME"], delivery_channel)

LOGGER.info("...ADD_UPDATE_NO_EVENT")


@helper.create
@helper.update
@helper.delete
Expand Down Expand Up @@ -435,8 +525,9 @@ def orchestrator(event: Dict[str, Any], context: Any) -> None:
if event.get("RequestType"):
LOGGER.info("...calling helper...")
helper(event, context)
elif event.get("source") == "aws.organizations":
process_event_organizations(event)
elif event.get("Records") and event["Records"][0]["EventSource"] == "aws:sns":
LOGGER.info("...aws:sns record...")
process_event_sns(event)
else:
LOGGER.info("...else...just calling process_event...")
process_event(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
if TYPE_CHECKING:
from mypy_boto3_iam.client import IAMClient
from mypy_boto3_organizations import OrganizationsClient
from mypy_boto3_sts.client import STSClient
from mypy_boto3_ssm.client import SSMClient
from mypy_boto3_sts.client import STSClient

# Setup Default Logger
LOGGER = logging.getLogger("sra")
Expand Down Expand Up @@ -120,7 +120,7 @@ def get_enabled_regions(customer_regions: str, control_tower_regions_only: bool
Returns:
Enabled regions
"""
if customer_regions.strip():
if customer_regions.strip() and not control_tower_regions_only:
LOGGER.info({"CUSTOMER PROVIDED REGIONS": customer_regions})
region_list = []
for region in customer_regions.split(","):
Expand Down
26 changes: 10 additions & 16 deletions aws_sra_examples/solutions/config/config_org/lambda/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def set_config_in_org(
"""Create Config recorder.
Args:
account_id: Account dd
account_id: Account id
region: AWS Region
configuration_role_name: IAM configuration role name
recorder_name: Name for Config recorder
Expand All @@ -86,8 +86,8 @@ def set_config_in_org(
all_supported: All supported
include_global_resource_types: Include global resource types
"""
LOGGER.info(f"Checking config for {account_id} in {region}")
account_session: boto3.Session = common.assume_role(configuration_role_name, "sra-configure-config", account_id)
LOGGER.info(f"Checking config for {account_id} in {region}") # update
config_client: ConfigServiceClient = account_session.client("config", region_name=region)
configuration_recorder: ConfigurationRecorderTypeDef = {
"name": recorder_name,
Expand All @@ -99,9 +99,13 @@ def set_config_in_org(
},
}
if len(config_client.describe_configuration_recorders()["ConfigurationRecorders"]):
LOGGER.info(f"Updating config recorder in {account_id} account in {region} region")
response = config_client.put_configuration_recorder(ConfigurationRecorder=configuration_recorder)
LOGGER.info(f"Config recorder updated for {account_id} account in {region} region. Configurations: {configuration_recorder}")
response = config_client.describe_configuration_recorders()["ConfigurationRecorders"]
if response.pop(0) == configuration_recorder:
LOGGER.info(f"Config recorder is up to update in {account_id} in {region} region. Configurations: {configuration_recorder}")
else:
LOGGER.info(f"Updating config recorder in {account_id} account in {region} region")
response = config_client.put_configuration_recorder(ConfigurationRecorder=configuration_recorder)
LOGGER.info(f"Config recorder updated for {account_id} account in {region} region. Configurations: {configuration_recorder}")

if not len(config_client.describe_configuration_recorders()["ConfigurationRecorders"]):
LOGGER.info(f"Creating config recorder in {account_id} account in {region} region")
Expand All @@ -112,16 +116,6 @@ def set_config_in_org(
LOGGER.info(f"Config recorder is already started in {region}")
LOGGER.info(config_client.describe_configuration_recorder_status())

if len(config_client.describe_delivery_channels()["DeliveryChannels"]):
try:
LOGGER.info("Starting config recorder")
response = config_client.start_configuration_recorder(
ConfigurationRecorderName=config_client.describe_configuration_recorder_status()["ConfigurationRecordersStatus"][0]["name"]
)
LOGGER.info(response)
except ClientError as e:
LOGGER.info(f"Error {repr(e)} starting configuration recorder for account {account_id} in region {region}")


def set_delivery_channel(
account_id: str,
Expand All @@ -145,7 +139,7 @@ def set_delivery_channel(
config_client.start_configuration_recorder(
ConfigurationRecorderName=config_client.describe_configuration_recorder_status()["ConfigurationRecordersStatus"][0]["name"]
)
LOGGER.info(f"Config delivery channel set for account {account_id} in {region} region")
LOGGER.info(f"Config delivery channel set for account {account_id} in {region} region. Configurations: {delivery_channel}")
except ClientError as e:
LOGGER.info(f"Error {repr(e)} enabling Config on account {account_id}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,10 @@ Parameters:

Rules:
ResourceTypesValidation:
RuleCondition: !Equals [!Ref pResourceTypes, '']
RuleCondition: !Equals [!Ref pAllSupported, 'false']
Assertions:
- AssertDescription: "'Resource Types' parameter is required if the 'All Supported' parameter is set to 'true'."
Assert: !Equals [!Ref pAllSupported, 'true']
- AssertDescription: "'Resource Types' parameter is required if the 'All Supported' parameter is set to 'false'."
Assert: !Not [!Equals [!Ref pResourceTypes, '']]

Conditions:
cComplianceFrequencySingleDay: !Equals [!Ref pComplianceFrequency, 1]
Expand Down Expand Up @@ -379,6 +379,17 @@ Resources:
Resource:
- !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/sra*"

- PolicyName: sra-config-org-policy-sns
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: SNSPublish
Effect: Allow
Action:
- sns:Publish
- sns:PublishBatch
Resource: !Ref rConfigOrgTopic

- PolicyName: sra-config-org-policy-iam
PolicyDocument:
Version: 2012-10-17
Expand Down Expand Up @@ -500,6 +511,7 @@ Resources:
RECORDER_NAME: !Ref pRecorderName
KMS_KEY_SECRET_NAME: !Ref pKMSKeyArnSecretName
HOME_REGION: !Ref pHomeRegion
SNS_TOPIC_ARN_FANOUT: !Ref rConfigOrgTopic

Tags:
- Key: sra-solution
Expand Down Expand Up @@ -528,6 +540,32 @@ Resources:
RECORDER_NAME: !Ref pRecorderName
KMS_KEY_SECRET_NAME: !Ref pKMSKeyArnSecretName
HOME_REGION: !Ref pHomeRegion
SNS_TOPIC_ARN_FANOUT: !Ref rConfigOrgTopic

rConfigOrgTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: !Sub ${pSRASolutionName}-configuration
KmsMasterKeyId: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/sns
Tags:
- Key: sra-solution
Value: !Ref pSRASolutionName

rConfigOrgTopicLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt rConfigOrgLambdaFunction.Arn
Principal: sns.amazonaws.com
SourceArn: !Ref rConfigOrgTopic

rConfigOrgTopicSubscription:
Type: AWS::SNS::Subscription
Properties:
Endpoint: !GetAtt rConfigOrgLambdaFunction.Arn
Protocol: lambda
TopicArn: !Ref rConfigOrgTopic


rConfigOrgDLQ:
Type: AWS::SQS::Queue
Expand Down
Loading

0 comments on commit c3d8e63

Please sign in to comment.