Skip to content

Commit b4b8eca

Browse files
authored
Recognize Aurora read-replicas in MySQL and PostgreSQL multi-user lambda functions (#156)
1 parent 92f00b3 commit b4b8eca

File tree

2 files changed

+230
-80
lines changed

2 files changed

+230
-80
lines changed

SecretsManagerRDSMySQLRotationMultiUser/lambda_function.py

Lines changed: 115 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ def is_rds_replica_database(replica_dict, master_dict):
558558
559559
Raises:
560560
ValueError: If the new username length would exceed the maximum allowed
561+
561562
"""
562563
# Setup the client
563564
rds_client = boto3.client('rds')
@@ -566,21 +567,123 @@ def is_rds_replica_database(replica_dict, master_dict):
566567
replica_instance_id = replica_dict['host'].split(".")[0]
567568
master_instance_id = master_dict['host'].split(".")[0]
568569

570+
if master_dict['engine'] == 'mysql':
571+
current_instance = get_instance_info_from_rds_api(replica_instance_id, rds_client)
572+
if not current_instance:
573+
return False
574+
return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier')
575+
576+
if master_dict['engine'] == 'aurora-mysql':
577+
replica_info = get_cluster_info_from_master_host(master_dict, rds_client)
578+
if not replica_info:
579+
return False
580+
581+
is_reader_endpoint = (replica_dict['host'] == replica_info['reader_endpoint'])
582+
is_reader_instance = any(replica_instance_id == replica_instance['instance_id'] and not replica_instance['is_writer']
583+
for replica_instance in replica_info['instance_ids'])
584+
return is_reader_endpoint or is_reader_instance
585+
return False
586+
587+
588+
def get_cluster_info_from_master_host(master_dict, rds_client):
589+
"""Fetches replica information from the DescribeDBClusters RDS API using master host/DBClusterIdentifier as a filter.
590+
591+
This helper function fetches replica information from the DescribeDBClusters RDS API using master host/DBClusterIdentifier as a filter.
592+
593+
Agrs:
594+
master_dict (dict): The secret dictionary containing the primary database
595+
596+
rds_client (client): The RDS service client used to query the instance
597+
598+
Returns:
599+
replica_info (dictionary): A replica information dictionary containing the replica details from the master cluster.
600+
601+
"""
602+
master_instance_id = master_dict['host'].split(".")[0]
603+
604+
if 'cluster' in master_dict['host'].split(".")[1]:
605+
# The master host is a writer endpoint
606+
cluster_info = get_cluster_info_from_rds_api(master_instance_id, rds_client)
607+
else:
608+
# The master host is an instance endpoint
609+
current_instance = get_instance_info_from_rds_api(master_instance_id, rds_client)
610+
if not current_instance or 'DBClusterIdentifier' not in current_instance:
611+
return {}
612+
cluster_info = get_cluster_info_from_rds_api(current_instance.get('DBClusterIdentifier'), rds_client)
613+
614+
if not cluster_info:
615+
return {}
616+
return {
617+
'reader_endpoint': cluster_info.get('ReaderEndpoint', ''),
618+
'instance_ids': [{'instance_id': member['DBInstanceIdentifier'], 'is_writer': member['IsClusterWriter']}
619+
for member in cluster_info.get('DBClusterMembers', [])]
620+
}
621+
622+
623+
def get_instance_info_from_rds_api(instance_id, rds_client):
624+
"""Fetches RDS instance infomation from the DescribeDBInstances RDS API using DBInstanceIdentifier as a filter.
625+
626+
This helper function fetches RDS instance infomation from the DescribeDBInstances RDS API using DBInstanceIdentifier as a filter.
627+
628+
Agrs:
629+
instance_id (string): The DBInstanceIdentifier of the RDS instance to describe
630+
631+
rds_client (client): The RDS service client used to query the instance
632+
633+
Returns:
634+
dbInstanceResponse (dict): The DescribeDBInstances RDS API response if found, otherwise None
635+
636+
Rasies:
637+
Exception: If the DescribeDBInstances RDS API returns an error
638+
639+
"""
640+
# Call DescribeDBInstances RDS API
569641
try:
570-
describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id)
642+
describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=instance_id)
571643
except Exception as err:
572-
logger.warning("Encountered error while verifying rds replica status: %s" % err)
573-
return False
644+
logger.error("setSecret: Encountered API error while fetching connection parameters from DescribeDBInstances RDS API: %s" % err)
645+
raise Exception("Encountered API error while fetching connection parameters from DescribeDBInstances RDS API: %s" % err)
646+
# Verify the instance was found
574647
instances = describe_response['DBInstances']
575-
576-
# Host from current secret cannot be found
577-
if not instances:
578-
logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id)
579-
return False
648+
if len(instances) == 0:
649+
logger.error("setSecret: %s is not a valid DB Instance ARN. No Instances found when using DescribeDBInstances RDS API to get connection params." % instance_id)
650+
raise ValueError("%s is not a valid DB Instance ARN. No Instances found when using DescribeDBInstances RDS API to get connection params." % instance_id)
580651

581652
# DB Instance identifiers are unique - can only be one result
582-
current_instance = instances[0]
583-
return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier')
653+
return instances[0]
654+
655+
656+
def get_cluster_info_from_rds_api(cluster_id, rds_client):
657+
"""Fetches RDS cluster infomation from the DescribeDBClusters RDS API using DBClusterIdentifier as a filter.
658+
659+
This helper function fetches RDS cluster infomation from the DescribeDBClusters RDS API using DBClusterIdentifier as a filter.
660+
661+
Agrs:
662+
cluster_id (string): The DBClusterIdentifier of the RDS cluster to describe
663+
664+
rds_client (client): The RDS service client used to query the cluster
665+
666+
Returns:
667+
dbClusterResponse (dict): The DescribeDBClusters RDS API response if found, otherwise None
668+
669+
Rasies:
670+
Exception: If the DescribeDBClusters RDS API returns an error
671+
672+
"""
673+
# Call DescribeDBClusters RDS API
674+
try:
675+
describe_response = rds_client.describe_db_clusters(DBClusterIdentifier=cluster_id)
676+
except Exception as err:
677+
logger.error("setSecret: Encountered API error while fetching connection parameters from DescribeDBClusters RDS API: %s" % err)
678+
raise Exception("Encountered API error while fetching connection parameters from DescribeDBClusters RDS API: %s" % err)
679+
# Verify the instance was found
680+
clusters = describe_response['DBClusters']
681+
if len(clusters) == 0:
682+
logger.error("setSecret: %s is not a valid DB Cluster ARN. No Clusters found when using DescribeDBClusters RDS API to get connection params." % cluster_id)
683+
raise ValueError("%s is not a valid DB Cluster ARN. No Clusters found when using DescribeDBClusters RDS API to get connection params." % cluster_id)
684+
685+
# DB cluster identifiers are unique - can only be one result
686+
return clusters[0]
584687

585688

586689
def fetch_instance_arn_from_system_tags(service_client, secret_arn):
@@ -637,48 +740,20 @@ def get_connection_params_from_rds_api(master_dict, master_instance_info):
637740
Returns:
638741
master_dict (dictionary): An updated master secret dictionary that now contains connection parameters such as `host`, `port`, etc.
639742
640-
Raises:
641-
Exception: If there is some error/throttling when calling the DescribeDBInstances/DescribeClusters RDS API
642-
643-
ValueError: If the DescribeDBInstances/DescribeDBClusters RDS API Response contains no Instances
644743
"""
645744
# Setup the client
646745
rds_client = boto3.client('rds')
647746

648747
if master_instance_info['ARN_SYSTEM_TAG'] == 'aws:rds:primarydbinstancearn':
649-
# Call DescribeDBInstances RDS API
650-
try:
651-
describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=master_instance_info['ARN'])
652-
except Exception as err:
653-
logger.error("setSecret: Encountered API error while fetching connection parameters from DescribeDBInstances RDS API: %s" % err)
654-
raise Exception("Encountered API error while fetching connection parameters from DescribeDBInstances RDS API: %s" % err)
655-
# Verify the instance was found
656-
instances = describe_response['DBInstances']
657-
if len(instances) == 0:
658-
logger.error("setSecret: %s is not a valid DB Instance ARN. No Instances found when using DescribeDBInstances RDS API to get connection params." % master_instance_info['ARN'])
659-
raise ValueError("%s is not a valid DB Instance ARN. No Instances found when using DescribeDBInstances RDS API to get connection params." % master_instance_info['ARN'])
660-
661748
# put connection parameters in master secret dictionary
662-
primary_instance = instances[0]
749+
primary_instance = get_instance_info_from_rds_api(master_instance_info['ARN'], rds_client)
663750
master_dict['host'] = primary_instance['Endpoint']['Address']
664751
master_dict['port'] = primary_instance['Endpoint']['Port']
665752
master_dict['engine'] = primary_instance['Engine']
666753

667754
elif master_instance_info['ARN_SYSTEM_TAG'] == 'aws:rds:primarydbclusterarn':
668-
# Call DescribeDBClusters RDS API
669-
try:
670-
describe_response = rds_client.describe_db_clusters(DBClusterIdentifier=master_instance_info['ARN'])
671-
except Exception as err:
672-
logger.error("setSecret: Encountered API error while fetching connection parameters from DescribeDBClusters RDS API: %s" % err)
673-
raise Exception("Encountered API error while fetching connection parameters from DescribeDBClusters RDS API: %s" % err)
674-
# Verify the instance was found
675-
instances = describe_response['DBClusters']
676-
if len(instances) == 0:
677-
logger.error("setSecret: %s is not a valid DB Cluster ARN. No Instances found when using DescribeDBClusters RDS API to get connection params." % master_instance_info['ARN'])
678-
raise ValueError("%s is not a valid DB Cluster ARN. No Instances found when using DescribeDBClusters RDS API to get connection params." % master_instance_info['ARN'])
679-
680755
# put connection parameters in master secret dictionary
681-
primary_instance = instances[0]
756+
primary_instance = get_cluster_info_from_rds_api(master_instance_info['ARN'], rds_client)
682757
master_dict['host'] = primary_instance['Endpoint']
683758
master_dict['port'] = primary_instance['Port']
684759
master_dict['engine'] = primary_instance['Engine']

0 commit comments

Comments
 (0)