def update_deployment_account_output_parameters( deployment_account_region, region, deployment_account_role, cloudformation): """ Update parameters on the deployment account across target regions based on the output of CloudFormation base stacks in the deployment account. """ deployment_account_parameter_store = ParameterStore( deployment_account_region, deployment_account_role ) parameter_store = ParameterStore( region, deployment_account_role ) for key, value in cloudformation.get_stack_regional_outputs().items(): deployment_account_parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value ) parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value )
def configure_generic_account(sts, event, region, role): """ Fetches the kms_arn from the deployment account main region and adds the it plus the deployment_account_id parameter to the target account so it can be consumed in CloudFormation. These are required for the global.yml in all target accounts. """ try: deployment_account_role = sts.assume_cross_account_role( 'arn:aws:iam::{0}:role/{1}'.format( event['deployment_account_id'], event['cross_account_access_role'] ), 'configure_generic' ) parameter_store_deployment_account = ParameterStore( event['deployment_account_region'], deployment_account_role ) parameter_store_target_account = ParameterStore( region, role ) kms_arn = parameter_store_deployment_account.fetch_parameter('/cross_region/kms_arn/{0}'.format(region)) except (ClientError, ParameterNotFoundError): raise GenericAccountConfigureError( 'Account {0} cannot yet be bootstrapped ' 'as the Deployment Account has not yet been bootstrapped. ' 'Have you moved your Deployment account into the deployment OU?'.format(event['account_id']) ) parameter_store_target_account.put_parameter('kms_arn', kms_arn) parameter_store_target_account.put_parameter('deployment_account_id', event['deployment_account_id'])
def update_deployment_account_output_parameters( deployment_account_region, region, kms_and_bucket_dict, deployment_account_role, cloudformation): """ Update parameters on the deployment account across target regions based on the output of CloudFormation base stacks in the deployment account. """ deployment_account_parameter_store = ParameterStore( deployment_account_region, deployment_account_role ) parameter_store = ParameterStore( region, deployment_account_role ) outputs = cloudformation.get_stack_regional_outputs() kms_and_bucket_dict[region] = {} kms_and_bucket_dict[region]['kms'] = outputs['kms_arn'] kms_and_bucket_dict[region]['s3_regional_bucket'] = outputs['s3_regional_bucket'] for key, value in outputs.items(): deployment_account_parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value ) parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value ) return kms_and_bucket_dict
def update_master_account_parameters(parsed_event, parameter_store): """ Update the Master account parameter store in us-east-1 with the deployment_account_id then updates the main deployment region with that same value """ parameter_store.put_parameter('deployment_account_id', parsed_event.account_id) parameter_store = ParameterStore(parsed_event.deployment_account_region, boto3) parameter_store.put_parameter('deployment_account_id', parsed_event.account_id)
def configure_master_account_parameters(event): """ Update the Master account parameter store in us-east-1 with the deployment_account_id then updates the main deployment region with that same value """ parameter_store_master_account_region = ParameterStore(os.environ["AWS_REGION"], boto3) parameter_store_master_account_region.put_parameter('deployment_account_id', event['account_id']) parameter_store_deployment_account_region = ParameterStore(event['deployment_account_region'], boto3) parameter_store_deployment_account_region.put_parameter('deployment_account_id', event['account_id'])
def configure_deployment_account(parsed_event, role): for region in list(set([parsed_event.deployment_account_region] + parsed_event.regions)): parameters = ParameterStore(region, role) if region == parsed_event.deployment_account_region: for key, value in parsed_event.create_deployment_account_parameters().items(): parameters.put_parameter( key, value ) continue parameters.put_parameter('organization_id', os.environ["ORGANIZATION_ID"])
def configure_deployment_account_parameters(event, role): """ Applies the Parameters from adfconfig plus other essential Parameters to the Deployment Account in each region as defined in adfconfig.yml """ for region in list( set([event["deployment_account_region"]] + event["regions"])): parameter_store = ParameterStore(region, role) for key, value in event['deployment_account_parameters'].items(): parameter_store.put_parameter(key, value)
def worker_thread(account_id, sts, config, s3, cache, kms_dict): """ The Worker thread function that is created for each account in which CloudFormation create_stack is called """ LOGGER.debug("%s - Starting new worker thread", account_id) organizations = Organizations(role=boto3, account_id=account_id) ou_id = organizations.get_parent_info().get("ou_parent_id") account_state = is_account_in_invalid_state(ou_id, config.config) if account_state: LOGGER.info("%s %s", account_id, account_state) return account_path = organizations.build_account_path( ou_id, [], # Initial empty array to hold OU Path, cache) try: role = ensure_generic_account_can_be_setup(sts, config, account_id) # Regional base stacks can be updated after global for region in list( set([config.deployment_account_region] + config.target_regions)): # Ensuring the kms_arn on the target account is up-to-date parameter_store = ParameterStore(region, role) parameter_store.put_parameter('kms_arn', kms_dict[region]) cloudformation = CloudFormation( region=region, deployment_account_region=config.deployment_account_region, role=role, wait=True, stack_name=None, s3=s3, s3_key_path=account_path, account_id=account_id) try: cloudformation.create_stack() except GenericAccountConfigureError as error: if 'Unable to fetch parameters' in str(error): LOGGER.error( '%s - Failed to update its base stack due to missing parameters (deployment_account_id or kms_arn), ' 'ensure this account has been bootstrapped correctly by being moved from the root ' 'into an Organizational Unit within AWS Organizations.', account_id) raise Exception from error except GenericAccountConfigureError as generic_account_error: LOGGER.info(generic_account_error) return
def update_deployment_account_output_parameters(deployment_account_region, region, deployment_account_role, cloudformation): deployment_account_parameter_store = ParameterStore( deployment_account_region, deployment_account_role) parameter_store = ParameterStore(region, deployment_account_role) for key, value in cloudformation.get_stack_regional_outputs().items(): deployment_account_parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value) parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value)
def configure_deployment_account(parsed_event, role): """ Applies the Parameters from adfconfig plus other essential Parameters to the Deployment Account in each region as defined in adfconfig.yml """ for region in list( set([parsed_event.deployment_account_region] + parsed_event.regions)): parameters = ParameterStore(region, role) if region == parsed_event.deployment_account_region: for key, value in parsed_event.create_deployment_account_parameters( ).items(): if value: parameters.put_parameter(key, value) parameters.put_parameter('organization_id', os.environ["ORGANIZATION_ID"])
def configure_generic_account(sts, event, region, role): """ Fetches the kms_arn from the deployment account main region and adds the it plus the deployment_account_id parameter to the target account so it can be consumed in CloudFormation. These are required for the global.yml in all target accounts. """ deployment_account_role = sts.assume_cross_account_role( 'arn:aws:iam::{0}:role/{1}'.format(event['deployment_account_id'], event['cross_account_access_role']), 'configure_generic') parameter_store_deployment_account = ParameterStore( event['deployment_account_region'], deployment_account_role) parameter_store_target_account = ParameterStore(region, role) kms_arn = parameter_store_deployment_account.fetch_parameter( '/cross_region/kms_arn/{0}'.format(region)) parameter_store_target_account.put_parameter('kms_arn', kms_arn) parameter_store_target_account.put_parameter( 'deployment_account_id', event['deployment_account_id'])
def prepare_deployment_account(sts, deployment_account_id, config): """ Ensures configuration is up to date on the deployment account and returns the role that can be assumed by the master account to access the deployment account """ deployment_account_role = sts.assume_cross_account_role( 'arn:aws:iam::{0}:role/{1}'.format(deployment_account_id, config.cross_account_access_role), 'master') for region in list( set([config.deployment_account_region] + config.target_regions)): deployment_account_parameter_store = ParameterStore( region, deployment_account_role) deployment_account_parameter_store.put_parameter( 'organization_id', os.environ["ORGANIZATION_ID"]) deployment_account_parameter_store = ParameterStore( config.deployment_account_region, deployment_account_role) deployment_account_parameter_store.put_parameter( 'deployment_account_bucket', DEPLOYMENT_ACCOUNT_S3_BUCKET_NAME) if '@' not in config.notification_endpoint: config.notification_channel = config.notification_endpoint config.notification_endpoint = "arn:aws:lambda:{0}:{1}:function:SendSlackNotification".format( config.deployment_account_region, deployment_account_id) for item in ('cross_account_access_role', 'notification_type', 'notification_endpoint', 'notification_channel'): if getattr(config, item) is not None: deployment_account_parameter_store.put_parameter( '/notification_endpoint/main' if item == 'notification_channel' else item, str(getattr(config, item))) return deployment_account_role
def configure_generic_account(sts, event, region, role): """ Fetches the kms_arn from the deployment account main region and adds the it plus the deployment_account_id parameter to the target account so it can be consumed in CloudFormation. These are required for the global.yml in all target accounts. """ try: deployment_account_id = event['deployment_account_id'] cross_account_access_role = event['cross_account_access_role'] role_arn = f'arn:{PARTITION}:iam::{deployment_account_id}:role/{cross_account_access_role}' deployment_account_role = sts.assume_cross_account_role( role_arn=role_arn, role_session_name='configure_generic') parameter_store_deployment_account = ParameterStore( event['deployment_account_region'], deployment_account_role) parameter_store_target_account = ParameterStore(region, role) kms_arn = parameter_store_deployment_account.fetch_parameter( f'/cross_region/kms_arn/{region}') bucket_name = parameter_store_deployment_account.fetch_parameter( f'/cross_region/s3_regional_bucket/{region}') except (ClientError, ParameterNotFoundError): raise GenericAccountConfigureError( f'Account {event["account_id"]} cannot yet be bootstrapped ' 'as the Deployment Account has not yet been bootstrapped. ' 'Have you moved your Deployment account into the deployment OU?' ) from None parameter_store_target_account.put_parameter('kms_arn', kms_arn) parameter_store_target_account.put_parameter('bucket_name', bucket_name) parameter_store_target_account.put_parameter( 'deployment_account_id', event['deployment_account_id'])
def update_deployment_account_output_parameters(deployment_account_region, region, deployment_account_role, cloudformation): deployment_account_parameter_store = ParameterStore( deployment_account_region, deployment_account_role) regional_parameter_store = ParameterStore(region, deployment_account_role) # Regions needs to know to organization ID for Bucket Policy regional_parameter_store.put_parameter("organization_id", os.environ['ORGANIZATION_ID']) # Regions needs to store its kms arn and s3 bucket in master and regional for key, value in cloudformation.get_stack_regional_outputs().items(): LOGGER.info('Updating %s on deployment account in %s', key, region) deployment_account_parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value) regional_parameter_store.put_parameter( "/cross_region/{0}/{1}".format(key, region), value)
def prepare_deployment_account(sts, deployment_account_id, config): deployment_account_role = sts.assume_cross_account_role( 'arn:aws:iam::{0}:role/{1}'.format(deployment_account_id, config.cross_account_access_role), 'master') for region in list( set([config.deployment_account_region] + config.target_regions)): deployment_account_parameter_store = ParameterStore( region, deployment_account_role) deployment_account_parameter_store.put_parameter( 'organization_id', os.environ["ORGANIZATION_ID"]) deployment_account_parameter_store = ParameterStore( config.deployment_account_region, deployment_account_role) deployment_account_parameter_store.put_parameter( 'cross_account_access_role', str(config.cross_account_access_role)) deployment_account_parameter_store.put_parameter( 'deployment_account_bucket', DEPLOYMENT_ACCOUNT_S3_BUCKET) return deployment_account_role
def prepare_deployment_account(sts, deployment_account_id, config): """ Ensures configuration is up to date on the deployment account and returns the role that can be assumed by the master account to access the deployment account """ deployment_account_role = sts.assume_cross_account_role( f'arn:{PARTITION}:iam::{deployment_account_id}:role/' f'{config.cross_account_access_role}', 'master') for region in list( set([config.deployment_account_region] + config.target_regions)): deployment_account_parameter_store = ParameterStore( region, deployment_account_role) deployment_account_parameter_store.put_parameter( 'organization_id', os.environ["ORGANIZATION_ID"]) deployment_account_parameter_store = ParameterStore( config.deployment_account_region, deployment_account_role) deployment_account_parameter_store.put_parameter('adf_version', ADF_VERSION) deployment_account_parameter_store.put_parameter('adf_log_level', ADF_LOG_LEVEL) deployment_account_parameter_store.put_parameter( 'deployment_account_bucket', DEPLOYMENT_ACCOUNT_S3_BUCKET_NAME) deployment_account_parameter_store.put_parameter( 'default_scm_branch', config.config.get('scm', {}).get( 'default-scm-branch', ADF_DEFAULT_SCM_FALLBACK_BRANCH, )) auto_create_repositories = config.config.get( 'scm', {}).get('auto-create-repositories') if auto_create_repositories is not None: deployment_account_parameter_store.put_parameter( 'auto_create_repositories', str(auto_create_repositories)) if '@' not in config.notification_endpoint: config.notification_channel = config.notification_endpoint config.notification_endpoint = ( f"arn:{PARTITION}:lambda:{config.deployment_account_region}:" f"{deployment_account_id}:function:SendSlackNotification") for item in ('cross_account_access_role', 'notification_type', 'notification_endpoint', 'notification_channel'): if getattr(config, item) is not None: deployment_account_parameter_store.put_parameter( '/notification_endpoint/main' if item == 'notification_channel' else item, str(getattr(config, item))) return deployment_account_role
class Config: """Class used for modeling dfconfig and its properties """ def __init__(self, parameter_store=None, config_path=None): self.parameters_client = parameter_store or ParameterStore( os.environ["AWS_REGION"], boto3) self.config_path = config_path or './adfconfig.yml' self.organization_id = os.environ["ORGANIZATION_ID"] self.client_deployment_region = None self.notification_type = None self.notification_endpoint = None self.config_contents = None self.config = None self.deployment_account_region = None self.notification_channel = None self.protected = None self.target_regions = None self.cross_account_access_role = None self._load_config_file() def store_config(self): self._store_config() self._store_cross_region_config() def _validate(self): """ Validates the adfconfig.yml file """ if None in (self.cross_account_access_role, self.config, self.deployment_account_region, self.organization_id, self.target_regions, self.config.get('moves'), self.config.get('main-notification-endpoint')): raise InvalidConfigException( 'adf_config.yml is missing required properties. ' 'Please see the documentation.') if not len(self.target_regions) >= 1: raise InvalidConfigException( 'ADF requires you to have at least 1 target region ' 'for deployments') if isinstance(self.deployment_account_region, list): if len(self.deployment_account_region) > 1: raise InvalidConfigException( 'ADF currently only supports a single ' 'Deployment Account region') [self.deployment_account_region] = self.deployment_account_region if not isinstance(self.target_regions, list): self.target_regions = [self.target_regions] def _load_config_file(self): """ Loads the adfconfig.yml file and executes _parse_config """ with open(self.config_path) as config: self.config_contents = yaml.load(config, Loader=yaml.FullLoader) self._parse_config() def _parse_config(self): """ Parses the adfconfig.yml file and executes _validate """ self.deployment_account_region = self.config_contents.get( 'regions', None).get('deployment-account', None) self.target_regions = self.config_contents.get('regions', None).get( 'targets', None) self.cross_account_access_role = self.config_contents.get( 'roles', None).get('cross-account-access', None) self.config = self.config_contents.get('config', None) self.protected = self.config.get('protected', []) self.notification_type = 'lambda' if self.config.get( 'main-notification-endpoint')[0].get( 'type') == 'slack' else 'email' self.notification_endpoint = self.config.get( 'main-notification-endpoint')[0].get('target') self.notification_channel = None if self.notification_type == 'email' else self.notification_endpoint self._validate() def _store_cross_region_config(self): """ Stores cross_account_access_role Parameter in Parameter Store on the master account in deployment account main region. """ self.client_deployment_region = ParameterStore( self.deployment_account_region, boto3) self.client_deployment_region.put_parameter( 'cross_account_access_role', self.cross_account_access_role) def _store_config(self): """ Stores the required configuration in Parameter Store on The master account in us-east-1. """ for key, value in self.__dict__.items(): if key not in ("client", "client_deployment_region", "parameters_client", "config_contents", "config_path", "notification_endpoint", "notification_type"): self.parameters_client.put_parameter(key, str(value))
def save_ssm(self, json_string): parameter_store = ParameterStore() resp = parameter_store.put_parameter(self.path, json_string) LOGGER.debug(resp) return resp