def convert_yaml_node_to_troposphere(node): if node.tag == '!Sub': if type(node.value) == type(str()): # ScalarNode - single argument only return troposphere.Sub(node.value) else: values = {} for map_node in node.value[1:]: if map_node.tag != yaml_map_tag: raise TroposphereConversionError( "Substitue variables for !Sub must be mappings.") values[map_node.value[0][0].value] = map_node.value[0][1].value return troposphere.Sub(node.value[0].value, **values) elif node.tag == '!Ref': return troposphere.Ref(node.value) elif node.tag == '!Join': delimiter = node.value[0].value values = [] for node in node.value[1].value: values.append(convert_yaml_node_to_troposphere(node)) return troposphere.Join(delimiter, values) elif node.tag == yaml_str_tag: return node.value else: raise TroposphereConversionError( "Unknown YAML to convert to Troposphere")
def register_type_project_template(cls, project, template): """Registers into the project stack a S3 bucket where all lambdas code will be stored, as well as an output so any subsequent template can have a reference to this resource.""" bucket_name = troposphere.Join( "-", [ utils.validate_code_bucket(project.settings['code-bucket']), troposphere.Ref(troposphere.AWS_REGION), troposphere.Ref('Stage') ] ) code_bucket = s3.Bucket( "CodeBucket", BucketName=bucket_name, AccessControl=s3.Private, VersioningConfiguration=s3.VersioningConfiguration( Status='Enabled' ) ) template.add_resource(code_bucket) template.add_output([ troposphere.Output( "CodeBucket", Description="CodeBucket name", Value=bucket_name, ) ])
def queue_policy(self): return ts.sqs.QueuePolicy( self._get_logical_id('QueuePolicy'), Queues=[ts.Ref(self.queue)], PolicyDocument={ 'Version': '2012-10-17', 'Statement': [{ 'Sid': 'allow-sns-notifications', 'Effect': 'Allow', 'Principal': { 'AWS': '*', }, 'Action': ['sqs:SendMessage'], 'Resource': '*', 'Condition': { # todo: allow all topics? # 'ArnEquals': {'aws:SourceArn': self.config['SourceTopics']} 'ArnLike': { 'aws:SourceArn': ts.Join(':', [ 'arn:aws:sns', ts.Ref('AWS::Region'), ts.Ref('AWS::AccountId'), '*', ]) }, }, }], }, )
def create_lambda_invoke_properties(self, stage, action, info): function_arn_param = self.create_cfn_parameter( param_type='String', name=self.create_cfn_logical_id('Lambda' + stage.name + action.name), description='The name of the Lambda for stage {} and action {}'. format(stage.name, action.name), value=action.target_lambda + '.arn', ) user_parameters_param = self.create_cfn_parameter( param_type='String', name=self.create_cfn_logical_id('UserParameters' + stage.name + action.name), description='The UserParameters for stage {} and action {}'.format( stage.name, action.name), value=action.user_parameters, ) lambda_function_name = troposphere.Join('', [ troposphere.Select( 6, troposphere.Split(':', troposphere.Ref(function_arn_param))) ]) return { 'Configuration': { 'FunctionName': lambda_function_name, 'UserParameters': troposphere.Ref(user_parameters_param), }, }
def ec2_user_data(self): return ts.Base64( ts.Join( '', [ # -x print commands as they're executed # -e exit on error '#!/bin/bash -xe', '\n', # todo: figure out what these commands do 'yum install -y aws-cfn-bootstrap', '\n', '/opt/aws/bin/cfn-init -v ', ' --stack ', ts.Ref('AWS::StackName'), ' --resource ', self.launch_config_logical_id, ' --region ', ts.Ref('AWS::Region'), '\n', '/opt/aws/bin/cfn-signal -e $? ', ' --stack ', ts.Ref('AWS::StackName'), ' --resource ', self.auto_scaling_group_logical_id, ' --region ', ts.Ref('AWS::Region'), '\n', ]))
def register_resources_template(self, template): extra = defaultdict(list) for notification_id, notification in six.iteritems( self._notifications): notification.register_destination_publish_permission(template) extra[notification.api_property].append( NotificationConfiguration( Id=troposphere.Join('-', ['gordon', notification.id]), DestinationArn=notification.get_destination_arn(), Events=[e for e, _, _ in notification.events], KeyFilters=[ KeyFilter(Name=name, Value=value) for name, value in notification.filters ])) bucket_notification_configuration_lambda = 'lambda:contrib_s3:bucket_notification_configuration:current' template.add_resource( S3BucketNotificationConfiguration.create_with( utils.valid_cloudformation_name(self.name), DependsOn=[ self.project.reference( bucket_notification_configuration_lambda) ], lambda_arn=troposphere.Ref( self.project.reference( bucket_notification_configuration_lambda)), Bucket=self.get_bucket_name(), **dict([[k, v] for k, v in six.iteritems(extra) if v])))
def profile_role_statements(self): return collections.ChainMap( { # allow pulling s3 config 'access-secrets-bucket': awacs.aws.Statement( Effect='Allow', Action=[awacs.s3.Action('*')], Resource=[ ts.Join('', [ 'arn:aws:s3:::', ts.Ref(self.secrets_bucket), '*' ]) ], ), # allow decrypting s3 secrets 'use-secrets-kms-key': awacs.aws.Statement( Effect='Allow', Action=[awacs.kms.Action('*')], Resource=[self.secrets_key_arn], ), }, super().profile_role_statements, )
def get_default_bucket_policy_statements(self, pipeline_bucket): bucket_policy_statements = [ awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[ awacs.s3.ListBucket, awacs.s3.GetBucketVersioning, ], Resource=[ troposphere.Join('', [ awacs.s3.ARN(), pipeline_bucket, ]), ], ), awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[ awacs.s3.HeadBucket, ], Resource=[ '*' ] ), awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[ awacs.s3.GetObject, awacs.s3.GetObjectVersion, awacs.s3.PutObject, awacs.s3.ListObjects, awacs.s3.ListBucketMultipartUploads, awacs.s3.AbortMultipartUpload, awacs.s3.ListMultipartUploadParts, awacs.aws.Action("s3", "Get*"), ], Resource=[ troposphere.Join('', [ awacs.s3.ARN(), pipeline_bucket, '/*' ]), ], ) ] return bucket_policy_statements
def __init__(self, paco_ctx, account_ctx, aws_region, stack_group, stack_tags, zone_config, config_ref): super().__init__(paco_ctx, account_ctx, aws_region, enabled=zone_config.is_enabled(), config_ref=config_ref, iam_capabilities=["CAPABILITY_NAMED_IAM"], stack_group=stack_group, stack_tags=stack_tags) self.set_aws_name('HostedZone', zone_config.name) self.init_template('Route53 Hosted Zone: ' + zone_config.domain_name) self.paco_ctx.log_action_col("Init", "Route53", "Hosted Zone", "{}".format(zone_config.domain_name)) if zone_config.external_resource != None and zone_config.external_resource.is_enabled( ): hosted_zone_id_output_value = zone_config.external_resource.hosted_zone_id nameservers_output_value = ','.join( zone_config.external_resource.nameservers) else: hosted_zone_res = troposphere.route53.HostedZone( title='HostedZone', template=self.template, Name=zone_config.domain_name) hosted_zone_id_output_value = troposphere.Ref(hosted_zone_res) nameservers_output_value = troposphere.Join( ',', troposphere.GetAtt(hosted_zone_res, 'NameServers')) self.create_output(title='HostedZoneId', value=hosted_zone_id_output_value, ref=config_ref + '.id') self.create_output(title='HostedZoneNameServers', value=nameservers_output_value, ref=config_ref + '.name_servers') if len(zone_config.record_sets) > 0: record_set_list = [] for record_set_config in zone_config.record_sets: record_set_res = troposphere.route53.RecordSet( Name=record_set_config.record_name, Type=record_set_config.type, TTL=record_set_config.ttl, ResourceRecords=record_set_config.resource_records) record_set_list.append(record_set_res) group_res = troposphere.route53.RecordSetGroup( title='RecordSetGroup', template=self.template, HostedZoneId=troposphere.Ref(hosted_zone_res), RecordSets=record_set_list) group_res.DependsOn = hosted_zone_res self.set_template()
def __init__(self, stack, paco_ctx, awslambda): super().__init__(stack, paco_ctx) self.set_aws_name('ApiGatewayLamdaPermission', self.resource_group_name, self.resource_name) self.init_template('Cross-account Api Gateway Lambda Permission') apigateway = self.resource api_gateway_id_param = self.create_cfn_parameter( name=self.create_cfn_logical_id('ApiGatewayRestApiId'), param_type='String', description='API Gateway Rest API Id', value=apigateway.paco_ref + '.id', ) lambda_arn_param = self.create_cfn_parameter( name=self.create_cfn_logical_id('LambdaArn'), param_type='String', description='Lambda Arn', value=awslambda.paco_ref + '.arn', ) # Lambda Permission for cross-account API Gateway invocation for method in apigateway.methods.values(): if method.integration != None and method.integration.integration_lambda != None: if awslambda.paco_ref == method.integration.integration_lambda: if apigateway.get_account().name != awslambda.get_account().name: # Grant Cross-Account API Gateway permission path_part = '' if method.resource_name: name_parts = method.resource_name.split('.') resource = method.get_resource() if len(name_parts) > 1: # child resource last_resource = resource while schemas.IApiGatewayResource.providedBy(resource): last_resource = resource resource = resource.__parent__.__parent__ path_part = last_resource.path_part + '/*' # add /* to match all child resource else: # parent resource path_part = resource.path_part lambda_permission_resource = troposphere.awslambda.Permission( title='ApiGatewayRestApiMethod' + md5sum(str_data=method.paco_ref), Action="lambda:InvokeFunction", FunctionName=troposphere.Ref(lambda_arn_param), Principal='apigateway.amazonaws.com', SourceArn=troposphere.Join('', [ "arn:aws:execute-api:", awslambda.region_name, # lambda region ":", apigateway.get_account().account_id, # account id ":", troposphere.Ref(api_gateway_id_param), f"/*/{method.http_method}/{path_part}", ]) ) self.template.add_resource(lambda_permission_resource)
def create_template(self): t = self.template t.add_description("Acceptance Tests for cumulus scaling groups") the_chain = chain.Chain() application_port = "8000" the_chain.add( launch_config.LaunchConfig(prefix='websitesimple', vpc_id=Ref('VpcId'), meta_data=self.get_metadata(), bucket_name=self.context.bucket_name)) the_chain.add( ingress_rule.IngressRule(port_to_open="22", cidr="10.0.0.0/8")) the_chain.add( ingress_rule.IngressRule(port_to_open=application_port, cidr="10.0.0.0/8")) the_chain.add( block_device_data.BlockDeviceData( ec2.BlockDeviceMapping( DeviceName="/dev/xvda", Ebs=ec2.EBSBlockDevice(VolumeSize="40")))) the_chain.add( target_group.TargetGroup(port=application_port, vpc_id=Ref("VpcId"))) the_chain.add(scaling_group.ScalingGroup()) the_chain.add( dns.Dns(base_domain=Ref("BaseDomain"), hosted_zone_id=Ref("AlbCanonicalHostedZoneID"), target=Ref("AlbDNSName"), dns_name=troposphere.Join('', [ self.context.namespace, "-websitesimple", ]))) the_chain.add(alb_port.AlbPort(port_to_open=application_port, )) the_chain.add( listener_rule.ListenerRule(base_domain_name=Ref("BaseDomain"), alb_listener_rule=Ref("IAlbListener"), path_pattern="/*", priority="2")) chain_context = chaincontext.ChainContext(template=t, ) the_chain.run(chain_context)
def to_userdata(userdata_str, base64_encode=True): splitted = re.split('(%s)' % ('|'.join(map(lambda e: e.split_exp, SUPPORTED_ELEMENTS))), userdata_str) def check(value): for e in SUPPORTED_ELEMENTS: try: return e.do(value) except NotMatchError: ... return value userdata = troposphere.Join('', list(map(check, splitted))) return troposphere.Base64(userdata) if base64_encode else userdata
def get_integration_uri(self, resource): integration_type = self._get_integration_type(resource) if integration_type == LAMBDA_INTEGRATION: return troposphere.Join('', [ 'arn:aws:apigateway:', troposphere.Ref(troposphere.AWS_REGION), ':lambda:path/2015-03-31/functions/', troposphere.Ref( self.get_function_name(resource)), '/invocations' ]) elif integration_type == HTTP_INTEGRATION: return resource['integration']['uri'] elif integration_type == MOCK_INTEGRATION: return troposphere.Ref(troposphere.AWS_NO_VALUE)
def get_destination_arn(self): destination = self.settings['topic'] region = troposphere.Ref(troposphere.AWS_REGION) if isinstance(destination, six.string_types): if destination.startswith('arn:aws:'): return destination account = troposphere.Ref(troposphere.AWS_ACCOUNT_ID) elif isinstance(destination, dict): account = destination['account_id'] destination = destination['name'] else: return destination return troposphere.Join(":", ["arn:aws:sns", region, account, destination])
def get_destination_url(self): destination = self.settings['queue'] region = troposphere.Ref(troposphere.AWS_REGION) if isinstance(destination, six.string_types): account = troposphere.Ref(troposphere.AWS_ACCOUNT_ID) elif isinstance(destination, dict): account = destination['account_id'] destination = destination['name'] else: return destination return troposphere.Join("", [ "https://sqs.", region, ".amazonaws.com/", account, "/", destination ])
def create_record(awsclient, name_prefix, instance_reference, type="A", host_zone_name=None): """ Builds route53 record entries enabling DNS names for services Note: gcdt.route53 create_record(awsclient, ...) is used in dataplatform cloudformation.py templates! :param name_prefix: The sub domain prefix to use :param instance_reference: The EC2 troposphere reference which's private IP should be linked to :param type: The type of the record A or CNAME (default: A) :param host_zone_name: The host zone name to use (like preprod.ds.glomex.cloud. - DO NOT FORGET THE DOT!) :return: RecordSetType """ # Only fetch the host zone from the COPS stack if nessary if host_zone_name is None: host_zone_name = _retrieve_stack_host_zone_name(awsclient) if not (type == "A" or type == "CNAME"): raise Exception("Record set type is not supported!") name_of_record = name_prefix \ .replace('.', '') \ .replace('-', '') \ .title() + "HostRecord" # Reference EC2 instance automatically to their private IP if isinstance(instance_reference, Instance): resource_record = troposphere.GetAtt(instance_reference, "PrivateIp") else: resource_record = instance_reference return RecordSetType( name_of_record, HostedZoneName=host_zone_name, Name=troposphere.Join("", [ name_prefix + ".", host_zone_name, ]), Type=type, TTL=TTL_DEFAULT, ResourceRecords=[resource_record], )
def get_template( puppet_version, all_regions, source, is_caching_enabled, is_manual_approvals: bool, scm_skip_creation_of_repo: bool, should_validate: bool, ) -> t.Template: is_codecommit = source.get("Provider", "").lower() == "codecommit" is_github = source.get("Provider", "").lower() == "github" is_codestarsourceconnection = (source.get( "Provider", "").lower() == "codestarsourceconnection") is_custom = (source.get("Provider", "").lower() == "custom") is_s3 = source.get("Provider", "").lower() == "s3" description = f"""Bootstrap template used to bring up the main ServiceCatalog-Puppet AWS CodePipeline with dependencies {{"version": "{puppet_version}", "framework": "servicecatalog-puppet", "role": "bootstrap-master"}}""" template = t.Template(Description=description) version_parameter = template.add_parameter( t.Parameter("Version", Default=puppet_version, Type="String")) org_iam_role_arn_parameter = template.add_parameter( t.Parameter("OrgIamRoleArn", Default="None", Type="String")) with_manual_approvals_parameter = template.add_parameter( t.Parameter( "WithManualApprovals", Type="String", AllowedValues=["Yes", "No"], Default="No", )) puppet_code_pipeline_role_permission_boundary_parameter = template.add_parameter( t.Parameter( "PuppetCodePipelineRolePermissionBoundary", Type="String", Description= "IAM Permission Boundary to apply to the PuppetCodePipelineRole", Default=awscs_iam.ARN(resource="policy/AdministratorAccess").data, )) source_role_permissions_boundary_parameter = template.add_parameter( t.Parameter( "SourceRolePermissionsBoundary", Type="String", Description="IAM Permission Boundary to apply to the SourceRole", Default=awscs_iam.ARN(resource="policy/AdministratorAccess").data, )) puppet_generate_role_permission_boundary_parameter = template.add_parameter( t.Parameter( "PuppetGenerateRolePermissionBoundary", Type="String", Description= "IAM Permission Boundary to apply to the PuppetGenerateRole", Default=awscs_iam.ARN(resource="policy/AdministratorAccess").data, )) puppet_deploy_role_permission_boundary_parameter = template.add_parameter( t.Parameter( "PuppetDeployRolePermissionBoundary", Type="String", Description= "IAM Permission Boundary to apply to the PuppetDeployRole", Default=awscs_iam.ARN(resource="policy/AdministratorAccess").data, )) puppet_provisioning_role_permissions_boundary_parameter = template.add_parameter( t.Parameter( "PuppetProvisioningRolePermissionsBoundary", Type="String", Description= "IAM Permission Boundary to apply to the PuppetProvisioningRole", Default=awscs_iam.ARN(resource="policy/AdministratorAccess").data, )) cloud_formation_deploy_role_permissions_boundary_parameter = template.add_parameter( t.Parameter( "CloudFormationDeployRolePermissionsBoundary", Type="String", Description= "IAM Permission Boundary to apply to the CloudFormationDeployRole", Default=awscs_iam.ARN(resource="policy/AdministratorAccess").data, )) deploy_environment_compute_type_parameter = template.add_parameter( t.Parameter( "DeployEnvironmentComputeType", Type="String", Description="The AWS CodeBuild Environment Compute Type", Default="BUILD_GENERAL1_SMALL", )) spoke_deploy_environment_compute_type_parameter = template.add_parameter( t.Parameter( "SpokeDeployEnvironmentComputeType", Type="String", Description= "The AWS CodeBuild Environment Compute Type for spoke execution mode", Default="BUILD_GENERAL1_SMALL", )) deploy_num_workers_parameter = template.add_parameter( t.Parameter( "DeployNumWorkers", Type="Number", Description= "Number of workers that should be used when running a deploy", Default=10, )) puppet_role_name_parameter = template.add_parameter( t.Parameter("PuppetRoleName", Type="String", Default="PuppetRole")) puppet_role_path_template_parameter = template.add_parameter( t.Parameter("PuppetRolePath", Type="String", Default="/servicecatalog-puppet/")) template.add_condition( "ShouldUseOrgs", t.Not(t.Equals(t.Ref(org_iam_role_arn_parameter), "None"))) template.add_condition( "HasManualApprovals", t.Equals(t.Ref(with_manual_approvals_parameter), "Yes")) template.add_resource( s3.Bucket( "StacksRepository", BucketName=t.Sub("sc-puppet-stacks-repository-${AWS::AccountId}"), VersioningConfiguration=s3.VersioningConfiguration( Status="Enabled"), BucketEncryption=s3.BucketEncryption( ServerSideEncryptionConfiguration=[ s3.ServerSideEncryptionRule( ServerSideEncryptionByDefault=s3. ServerSideEncryptionByDefault(SSEAlgorithm="AES256")) ]), PublicAccessBlockConfiguration=s3.PublicAccessBlockConfiguration( BlockPublicAcls=True, BlockPublicPolicy=True, IgnorePublicAcls=True, RestrictPublicBuckets=True, ), Tags=t.Tags({"ServiceCatalogPuppet:Actor": "Framework"}), )) manual_approvals_param = template.add_resource( ssm.Parameter( "ManualApprovalsParam", Type="String", Name="/servicecatalog-puppet/manual-approvals", Value=t.Ref(with_manual_approvals_parameter), )) template.add_resource( ssm.Parameter( "SpokeDeployEnvParameter", Type="String", Name=constants.SPOKE_EXECUTION_MODE_DEPLOY_ENV_PARAMETER_NAME, Value=t.Ref(spoke_deploy_environment_compute_type_parameter), )) param = template.add_resource( ssm.Parameter( "Param", Type="String", Name="service-catalog-puppet-version", Value=t.Ref(version_parameter), )) partition_parameter = template.add_resource( ssm.Parameter( "PartitionParameter", Type="String", Name="/servicecatalog-puppet/partition", Value=t.Ref("AWS::Partition"), )) puppet_role_name_parameter = template.add_resource( ssm.Parameter( "PuppetRoleNameParameter", Type="String", Name="/servicecatalog-puppet/puppet-role/name", Value=t.Ref(puppet_role_name_parameter), )) puppet_role_path_parameter = template.add_resource( ssm.Parameter( "PuppetRolePathParameter", Type="String", Name="/servicecatalog-puppet/puppet-role/path", Value=t.Ref(puppet_role_path_template_parameter), )) share_accept_function_role = template.add_resource( iam.Role( "ShareAcceptFunctionRole", RoleName="ShareAcceptFunctionRole", ManagedPolicyArns=[ t.Sub( "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ) ], Path=t.Ref(puppet_role_path_template_parameter), Policies=[ iam.Policy( PolicyName="ServiceCatalogActions", PolicyDocument={ "Version": "2012-10-17", "Statement": [{ "Action": ["sts:AssumeRole"], "Resource": { "Fn::Sub": "arn:${AWS::Partition}:iam::*:role${PuppetRolePath}${PuppetRoleName}" }, "Effect": "Allow", }], }, ) ], AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [{ "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "Service": ["lambda.amazonaws.com"] }, }], }, )) provisioning_role = template.add_resource( iam.Role( "ProvisioningRole", RoleName="PuppetProvisioningRole", AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [ { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "Service": ["codebuild.amazonaws.com"] }, }, { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "AWS": { "Fn::Sub": "${AWS::AccountId}" } }, }, ], }, ManagedPolicyArns=[ t.Sub( "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" ) ], PermissionsBoundary=t.Ref( puppet_provisioning_role_permissions_boundary_parameter), Path=t.Ref(puppet_role_path_template_parameter), )) cloud_formation_deploy_role = template.add_resource( iam.Role( "CloudFormationDeployRole", RoleName="CloudFormationDeployRole", AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [ { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "Service": ["cloudformation.amazonaws.com"] }, }, { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "AWS": { "Fn::Sub": "${AWS::AccountId}" } }, }, ], }, ManagedPolicyArns=[ t.Sub( "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" ) ], PermissionsBoundary=t.Ref( cloud_formation_deploy_role_permissions_boundary_parameter), Path=t.Ref(puppet_role_path_template_parameter), )) pipeline_role = template.add_resource( iam.Role( "PipelineRole", RoleName="PuppetCodePipelineRole", AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [{ "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "Service": ["codepipeline.amazonaws.com"] }, }], }, ManagedPolicyArns=[ t.Sub( "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" ) ], PermissionsBoundary=t.Ref( puppet_code_pipeline_role_permission_boundary_parameter), Path=t.Ref(puppet_role_path_template_parameter), )) source_role = template.add_resource( iam.Role( "SourceRole", RoleName="PuppetSourceRole", AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [ { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "Service": ["codepipeline.amazonaws.com"] }, }, { "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "AWS": { "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" } }, }, ], }, ManagedPolicyArns=[ t.Sub( "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" ) ], PermissionsBoundary=t.Ref( source_role_permissions_boundary_parameter), Path=t.Ref(puppet_role_path_template_parameter), )) dry_run_notification_topic = template.add_resource( sns.Topic( "DryRunNotificationTopic", DisplayName="service-catalog-puppet-dry-run-approvals", TopicName="service-catalog-puppet-dry-run-approvals", Condition="HasManualApprovals", )) deploy_role = template.add_resource( iam.Role( "DeployRole", RoleName="PuppetDeployRole", AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [{ "Action": ["sts:AssumeRole"], "Effect": "Allow", "Principal": { "Service": ["codebuild.amazonaws.com"] }, }], }, ManagedPolicyArns=[ t.Sub( "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" ) ], PermissionsBoundary=t.Ref( puppet_deploy_role_permission_boundary_parameter), Path=t.Ref(puppet_role_path_template_parameter), )) num_workers_ssm_parameter = template.add_resource( ssm.Parameter( "NumWorkersSSMParameter", Type="String", Name="/servicecatalog-puppet/deploy/num-workers", Value=t.Sub("${DeployNumWorkers}"), )) parameterised_source_bucket = template.add_resource( s3.Bucket( "ParameterisedSourceBucket", PublicAccessBlockConfiguration=s3.PublicAccessBlockConfiguration( IgnorePublicAcls=True, BlockPublicPolicy=True, BlockPublicAcls=True, RestrictPublicBuckets=True, ), BucketEncryption=s3.BucketEncryption( ServerSideEncryptionConfiguration=[ s3.ServerSideEncryptionRule( ServerSideEncryptionByDefault=s3. ServerSideEncryptionByDefault(SSEAlgorithm="AES256")) ]), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), BucketName=t.Sub("sc-puppet-parameterised-runs-${AWS::AccountId}"), VersioningConfiguration=s3.VersioningConfiguration( Status="Enabled"), )) source_stage = codepipeline.Stages( Name="Source", Actions=[ codepipeline.Actions( RunOrder=1, RoleArn=t.GetAtt("SourceRole", "Arn"), ActionTypeId=codepipeline.ActionTypeId( Category="Source", Owner="AWS", Version="1", Provider="S3", ), OutputArtifacts=[ codepipeline.OutputArtifacts(Name="ParameterisedSource") ], Configuration={ "S3Bucket": t.Ref(parameterised_source_bucket), "S3ObjectKey": "parameters.zip", "PollForSourceChanges": True, }, Name="ParameterisedSource", ) ], ) install_spec = { "runtime-versions": dict(python="3.7"), "commands": [ f"pip install {puppet_version}" if "http" in puppet_version else f"pip install aws-service-catalog-puppet=={puppet_version}", ], } deploy_env_vars = [ { "Type": "PLAINTEXT", "Name": "PUPPET_ACCOUNT_ID", "Value": t.Ref("AWS::AccountId"), }, { "Type": "PLAINTEXT", "Name": "PUPPET_REGION", "Value": t.Ref("AWS::Region"), }, { "Type": "PARAMETER_STORE", "Name": "PARTITION", "Value": t.Ref(partition_parameter), }, { "Type": "PARAMETER_STORE", "Name": "PUPPET_ROLE_NAME", "Value": t.Ref(puppet_role_name_parameter), }, { "Type": "PARAMETER_STORE", "Name": "PUPPET_ROLE_PATH", "Value": t.Ref(puppet_role_path_parameter), }, ] if is_codecommit: template.add_resource( codecommit.Repository( "CodeRepo", RepositoryName=source.get("Configuration").get( "RepositoryName"), RepositoryDescription= "Repo to store the servicecatalog puppet solution", DeletionPolicy="Retain", )) source_stage.Actions.append( codepipeline.Actions( RunOrder=1, RoleArn=t.GetAtt("SourceRole", "Arn"), ActionTypeId=codepipeline.ActionTypeId( Category="Source", Owner="AWS", Version="1", Provider="CodeCommit", ), OutputArtifacts=[codepipeline.OutputArtifacts(Name="Source")], Configuration={ "RepositoryName": source.get("Configuration").get("RepositoryName"), "BranchName": source.get("Configuration").get("BranchName"), "PollForSourceChanges": source.get("Configuration").get("PollForSourceChanges", True), }, Name="Source", )) if is_github: source_stage.Actions.append( codepipeline.Actions( RunOrder=1, ActionTypeId=codepipeline.ActionTypeId( Category="Source", Owner="ThirdParty", Version="1", Provider="GitHub", ), OutputArtifacts=[codepipeline.OutputArtifacts(Name="Source")], Configuration={ "Owner": source.get("Configuration").get("Owner"), "Repo": source.get("Configuration").get("Repo"), "Branch": source.get("Configuration").get("Branch"), "OAuthToken": t.Join( "", [ "{{resolve:secretsmanager:", source.get("Configuration").get( "SecretsManagerSecret"), ":SecretString:OAuthToken}}", ], ), "PollForSourceChanges": source.get("Configuration").get("PollForSourceChanges"), }, Name="Source", )) if is_custom: source_stage.Actions.append( codepipeline.Actions( RunOrder=1, ActionTypeId=codepipeline.ActionTypeId( Category="Source", Owner="Custom", Version=source.get("Configuration").get( "CustomActionTypeVersion"), Provider=source.get("Configuration").get( "CustomActionTypeProvider"), ), OutputArtifacts=[codepipeline.OutputArtifacts(Name="Source")], Configuration={ "GitUrl": source.get("Configuration").get("GitUrl"), "Branch": source.get("Configuration").get("Branch"), "PipelineName": t.Sub("${AWS::StackName}-pipeline"), }, Name="Source", )) webhook = codepipeline.Webhook( "Webhook", Authentication="IP", TargetAction="Source", AuthenticationConfiguration=codepipeline.WebhookAuthConfiguration( AllowedIPRange=source.get("Configuration").get( "GitWebHookIpAddress")), Filters=[ codepipeline.WebhookFilterRule( JsonPath="$.changes[0].ref.id", MatchEquals="refs/heads/{Branch}") ], TargetPipelineVersion=1, TargetPipeline=t.Sub("${AWS::StackName}-pipeline"), ) template.add_resource(webhook) values_for_sub = { "GitUrl": source.get("Configuration").get("GitUrl"), "WebhookUrl": t.GetAtt(webhook, "Url"), } output_to_add = t.Output("WebhookUrl") output_to_add.Value = t.Sub("${GitUrl}||${WebhookUrl}", **values_for_sub) output_to_add.Export = t.Export(t.Sub("${AWS::StackName}-pipeline")) template.add_output(output_to_add) if is_codestarsourceconnection: source_stage.Actions.append( codepipeline.Actions( RunOrder=1, RoleArn=t.GetAtt("SourceRole", "Arn"), ActionTypeId=codepipeline.ActionTypeId( Category="Source", Owner="AWS", Version="1", Provider="CodeStarSourceConnection", ), OutputArtifacts=[codepipeline.OutputArtifacts(Name="Source")], Configuration={ "ConnectionArn": source.get("Configuration").get("ConnectionArn"), "FullRepositoryId": source.get("Configuration").get("FullRepositoryId"), "BranchName": source.get("Configuration").get("BranchName"), "OutputArtifactFormat": source.get("Configuration").get("OutputArtifactFormat"), }, Name="Source", )) if is_s3: bucket_name = source.get("Configuration").get("S3Bucket") if not scm_skip_creation_of_repo: template.add_resource( s3.Bucket( bucket_name, PublicAccessBlockConfiguration=s3. PublicAccessBlockConfiguration( IgnorePublicAcls=True, BlockPublicPolicy=True, BlockPublicAcls=True, RestrictPublicBuckets=True, ), BucketEncryption=s3.BucketEncryption( ServerSideEncryptionConfiguration=[ s3.ServerSideEncryptionRule( ServerSideEncryptionByDefault=s3. ServerSideEncryptionByDefault( SSEAlgorithm="AES256")) ]), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), BucketName=bucket_name, VersioningConfiguration=s3.VersioningConfiguration( Status="Enabled"), )) source_stage.Actions.append( codepipeline.Actions( RunOrder=1, ActionTypeId=codepipeline.ActionTypeId( Category="Source", Owner="AWS", Version="1", Provider="S3", ), OutputArtifacts=[codepipeline.OutputArtifacts(Name="Source")], Configuration={ "S3Bucket": bucket_name, "S3ObjectKey": source.get("Configuration").get("S3ObjectKey"), "PollForSourceChanges": source.get("Configuration").get("PollForSourceChanges"), }, Name="Source", )) single_account_run_project_build_spec = dict( version=0.2, phases=dict( install=install_spec, build={ "commands": [ 'echo "single_account: \\"${SINGLE_ACCOUNT_ID}\\"" > parameters.yaml', "cat parameters.yaml", "zip parameters.zip parameters.yaml", "aws s3 cp parameters.zip s3://sc-puppet-parameterised-runs-${PUPPET_ACCOUNT_ID}/parameters.zip", ] }, post_build={ "commands": [ "servicecatalog-puppet wait-for-parameterised-run-to-complete", ] }, ), artifacts=dict( name="DeployProject", files=[ "ServiceCatalogPuppet/manifest.yaml", "ServiceCatalogPuppet/manifest-expanded.yaml", "results/*/*", "output/*/*", "exploded_results/*/*", "tasks.log", ], ), ) single_account_run_project_args = dict( Name="servicecatalog-puppet-single-account-run", Description="Runs puppet for a single account - SINGLE_ACCOUNT_ID", ServiceRole=t.GetAtt(deploy_role, "Arn"), Tags=t.Tags.from_dict(**{"ServiceCatalogPuppet:Actor": "Framework"}), Artifacts=codebuild.Artifacts(Type="NO_ARTIFACTS", ), TimeoutInMinutes=480, Environment=codebuild.Environment( ComputeType=t.Ref(deploy_environment_compute_type_parameter), Image="aws/codebuild/standard:4.0", Type="LINUX_CONTAINER", EnvironmentVariables=[ { "Type": "PLAINTEXT", "Name": "SINGLE_ACCOUNT_ID", "Value": "CHANGE_ME", }, ] + deploy_env_vars, ), Source=codebuild.Source( Type="NO_SOURCE", BuildSpec=yaml.safe_dump(single_account_run_project_build_spec), ), ) single_account_run_project = template.add_resource( codebuild.Project("SingleAccountRunProject", **single_account_run_project_args)) single_account_run_project_build_spec["phases"]["post_build"]["commands"] = [ "servicecatalog-puppet wait-for-parameterised-run-to-complete --on-complete-url $CALLBACK_URL" ] single_account_run_project_args[ "Name"] = "servicecatalog-puppet-single-account-run-with-callback" single_account_run_project_args[ "Description"] = "Runs puppet for a single account - SINGLE_ACCOUNT_ID and then does a http put" single_account_run_project_args.get( "Environment").EnvironmentVariables.append({ "Type": "PLAINTEXT", "Name": "CALLBACK_URL", "Value": "CHANGE_ME", }) single_account_run_project_args["Source"] = codebuild.Source( Type="NO_SOURCE", BuildSpec=yaml.safe_dump(single_account_run_project_build_spec), ) single_account_run_project_with_callback = template.add_resource( codebuild.Project("SingleAccountRunWithCallbackProject", **single_account_run_project_args)) stages = [source_stage] if should_validate: template.add_resource( codebuild.Project( "ValidateProject", Name="servicecatalog-puppet-validate", ServiceRole=t.GetAtt("DeployRole", "Arn"), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), Artifacts=codebuild.Artifacts(Type="CODEPIPELINE"), TimeoutInMinutes=60, Environment=codebuild.Environment( ComputeType="BUILD_GENERAL1_SMALL", Image="aws/codebuild/standard:4.0", Type="LINUX_CONTAINER", ), Source=codebuild.Source( BuildSpec=yaml.safe_dump( dict( version="0.2", phases={ "install": { "runtime-versions": { "python": "3.7", }, "commands": [ f"pip install {puppet_version}" if "http" in puppet_version else f"pip install aws-service-catalog-puppet=={puppet_version}", ], }, "build": { "commands": [ "servicecatalog-puppet validate manifest.yaml" ] }, }, )), Type="CODEPIPELINE", ), Description="Validate the manifest.yaml file", )) stages.append( codepipeline.Stages( Name="Validate", Actions=[ codepipeline.Actions( InputArtifacts=[ codepipeline.InputArtifacts(Name="Source"), ], Name="Validate", ActionTypeId=codepipeline.ActionTypeId( Category="Build", Owner="AWS", Version="1", Provider="CodeBuild", ), OutputArtifacts=[ codepipeline.OutputArtifacts( Name="ValidateProject") ], Configuration={ "ProjectName": t.Ref("ValidateProject"), "PrimarySource": "Source", }, RunOrder=1, ), ], )) if is_manual_approvals: deploy_stage = codepipeline.Stages( Name="Deploy", Actions=[ codepipeline.Actions( InputArtifacts=[ codepipeline.InputArtifacts(Name="Source"), codepipeline.InputArtifacts( Name="ParameterisedSource"), ], Name="DryRun", ActionTypeId=codepipeline.ActionTypeId( Category="Build", Owner="AWS", Version="1", Provider="CodeBuild", ), OutputArtifacts=[ codepipeline.OutputArtifacts(Name="DryRunProject") ], Configuration={ "ProjectName": t.Ref("DryRunProject"), "PrimarySource": "Source", }, RunOrder=1, ), codepipeline.Actions( ActionTypeId=codepipeline.ActionTypeId( Category="Approval", Owner="AWS", Version="1", Provider="Manual", ), Configuration={ "NotificationArn": t.Ref("DryRunNotificationTopic"), "CustomData": "Approve when you are happy with the dry run.", }, Name="DryRunApproval", RunOrder=2, ), codepipeline.Actions( InputArtifacts=[ codepipeline.InputArtifacts(Name="Source"), codepipeline.InputArtifacts( Name="ParameterisedSource"), ], Name="Deploy", ActionTypeId=codepipeline.ActionTypeId( Category="Build", Owner="AWS", Version="1", Provider="CodeBuild", ), OutputArtifacts=[ codepipeline.OutputArtifacts(Name="DeployProject") ], Configuration={ "ProjectName": t.Ref("DeployProject"), "PrimarySource": "Source", }, RunOrder=3, ), ], ) else: deploy_stage = codepipeline.Stages( Name="Deploy", Actions=[ codepipeline.Actions( InputArtifacts=[ codepipeline.InputArtifacts(Name="Source"), codepipeline.InputArtifacts( Name="ParameterisedSource"), ], Name="Deploy", ActionTypeId=codepipeline.ActionTypeId( Category="Build", Owner="AWS", Version="1", Provider="CodeBuild", ), OutputArtifacts=[ codepipeline.OutputArtifacts(Name="DeployProject") ], Configuration={ "ProjectName": t.Ref("DeployProject"), "PrimarySource": "Source", "EnvironmentVariables": '[{"name":"EXECUTION_ID","value":"#{codepipeline.PipelineExecutionId}","type":"PLAINTEXT"}]', }, RunOrder=1, ), ], ) stages.append(deploy_stage) pipeline = template.add_resource( codepipeline.Pipeline( "Pipeline", RoleArn=t.GetAtt("PipelineRole", "Arn"), Stages=stages, Name=t.Sub("${AWS::StackName}-pipeline"), ArtifactStore=codepipeline.ArtifactStore( Type="S3", Location=t.Sub( "sc-puppet-pipeline-artifacts-${AWS::AccountId}-${AWS::Region}" ), ), RestartExecutionOnUpdate=True, )) if is_github: template.add_resource( codepipeline.Webhook( "Webhook", AuthenticationConfiguration=codepipeline. WebhookAuthConfiguration(SecretToken=t.Join( "", [ "{{resolve:secretsmanager:", source.get("Configuration").get( "SecretsManagerSecret"), ":SecretString:SecretToken}}", ], )), Filters=[ codepipeline.WebhookFilterRule( JsonPath="$.ref", MatchEquals="refs/heads/" + source.get("Configuration").get("Branch"), ) ], Authentication="GITHUB_HMAC", TargetPipeline=t.Ref(pipeline), TargetAction="Source", Name=t.Sub("${AWS::StackName}-webhook"), TargetPipelineVersion=t.GetAtt(pipeline, "Version"), RegisterWithThirdParty="true", )) deploy_project_build_spec = dict( version=0.2, phases=dict( install={ "runtime-versions": dict(python="3.7"), "commands": [ f"pip install {puppet_version}" if "http" in puppet_version else f"pip install aws-service-catalog-puppet=={puppet_version}", ], }, pre_build={ "commands": [ "servicecatalog-puppet --info expand --parameter-override-file $CODEBUILD_SRC_DIR_ParameterisedSource/parameters.yaml manifest.yaml", ] }, build={ "commands": [ "servicecatalog-puppet --info deploy --num-workers ${NUM_WORKERS} manifest-expanded.yaml", ] }, ), artifacts=dict( name="DeployProject", files=[ "manifest-expanded.yaml", "results/*/*", "output/*/*", "exploded_results/*/*", "tasks.log", ], ), ) deploy_project_args = dict( Name="servicecatalog-puppet-deploy", ServiceRole=t.GetAtt(deploy_role, "Arn"), Tags=t.Tags.from_dict(**{"ServiceCatalogPuppet:Actor": "Framework"}), Artifacts=codebuild.Artifacts(Type="CODEPIPELINE", ), TimeoutInMinutes=480, Environment=codebuild.Environment( ComputeType=t.Ref(deploy_environment_compute_type_parameter), Image="aws/codebuild/standard:4.0", Type="LINUX_CONTAINER", EnvironmentVariables=[ { "Type": "PARAMETER_STORE", "Name": "NUM_WORKERS", "Value": t.Ref(num_workers_ssm_parameter), }, { "Type": "PARAMETER_STORE", "Name": "SPOKE_EXECUTION_MODE_DEPLOY_ENV", "Value": constants.SPOKE_EXECUTION_MODE_DEPLOY_ENV_PARAMETER_NAME, }, ] + deploy_env_vars, ), Source=codebuild.Source( Type="CODEPIPELINE", BuildSpec=yaml.safe_dump(deploy_project_build_spec), ), Description="deploys out the products to be deployed", ) deploy_project = template.add_resource( codebuild.Project("DeployProject", **deploy_project_args)) deploy_project_build_spec["phases"]["build"]["commands"] = [ "servicecatalog-puppet --info dry-run manifest-expanded.yaml" ] deploy_project_build_spec["artifacts"]["name"] = "DryRunProject" deploy_project_args["Name"] = "servicecatalog-puppet-dryrun" deploy_project_args[ "Description"] = "dry run of servicecatalog-puppet-dryrun" deploy_project_args["Source"] = codebuild.Source( Type="CODEPIPELINE", BuildSpec=yaml.safe_dump(deploy_project_build_spec), ) dry_run_project = template.add_resource( codebuild.Project("DryRunProject", **deploy_project_args)) bootstrap_project = template.add_resource( codebuild.Project( "BootstrapProject", Name="servicecatalog-puppet-bootstrap-spokes-in-ou", ServiceRole=t.GetAtt("DeployRole", "Arn"), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), Artifacts=codebuild.Artifacts(Type="NO_ARTIFACTS"), TimeoutInMinutes=60, Environment=codebuild.Environment( ComputeType="BUILD_GENERAL1_SMALL", Image="aws/codebuild/standard:4.0", Type="LINUX_CONTAINER", EnvironmentVariables=[ { "Type": "PLAINTEXT", "Name": "OU_OR_PATH", "Value": "CHANGE_ME" }, { "Type": "PLAINTEXT", "Name": "IAM_ROLE_NAME", "Value": "OrganizationAccountAccessRole", }, { "Type": "PLAINTEXT", "Name": "IAM_ROLE_ARNS", "Value": "" }, ], ), Source=codebuild.Source( BuildSpec= "version: 0.2\nphases:\n install:\n runtime-versions:\n python: 3.7\n commands:\n - pip install aws-service-catalog-puppet\n build:\n commands:\n - servicecatalog-puppet bootstrap-spokes-in-ou $OU_OR_PATH $IAM_ROLE_NAME $IAM_ROLE_ARNS\nartifacts:\n files:\n - results/*/*\n - output/*/*\n name: BootstrapProject\n", Type="NO_SOURCE", ), Description="Bootstrap all the accounts in an OU", )) template.add_resource( codebuild.Project( "BootstrapASpokeProject", Name="servicecatalog-puppet-bootstrap-spoke", ServiceRole=t.GetAtt("DeployRole", "Arn"), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), Artifacts=codebuild.Artifacts(Type="NO_ARTIFACTS"), TimeoutInMinutes=60, Environment=codebuild.Environment( ComputeType="BUILD_GENERAL1_SMALL", Image="aws/codebuild/standard:4.0", Type="LINUX_CONTAINER", EnvironmentVariables=[ { "Type": "PLAINTEXT", "Name": "PUPPET_ACCOUNT_ID", "Value": t.Sub("${AWS::AccountId}"), }, { "Type": "PLAINTEXT", "Name": "ORGANIZATION_ACCOUNT_ACCESS_ROLE_ARN", "Value": "CHANGE_ME", }, { "Type": "PLAINTEXT", "Name": "ASSUMABLE_ROLE_IN_ROOT_ACCOUNT", "Value": "CHANGE_ME", }, ], ), Source=codebuild.Source( BuildSpec=yaml.safe_dump( dict( version=0.2, phases=dict( install=install_spec, build={ "commands": [ "servicecatalog-puppet bootstrap-spoke-as ${PUPPET_ACCOUNT_ID} ${ASSUMABLE_ROLE_IN_ROOT_ACCOUNT} ${ORGANIZATION_ACCOUNT_ACCESS_ROLE_ARN}" ] }, ), )), Type="NO_SOURCE", ), Description="Bootstrap given account as a spoke", )) cloud_formation_events_queue = template.add_resource( sqs.Queue( "CloudFormationEventsQueue", QueueName="servicecatalog-puppet-cloudformation-events", Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), )) cloud_formation_events_queue_policy = template.add_resource( sqs.QueuePolicy( "CloudFormationEventsQueuePolicy", Queues=[t.Ref(cloud_formation_events_queue)], PolicyDocument={ "Id": "AllowSNS", "Version": "2012-10-17", "Statement": [{ "Sid": "allow-send-message", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": ["sqs:SendMessage"], "Resource": "*", "Condition": { "ArnEquals": { "aws:SourceArn": t.Sub( "arn:${AWS::Partition}:sns:*:${AWS::AccountId}:servicecatalog-puppet-cloudformation-regional-events" ) } }, }], }, )) spoke_deploy_bucket = template.add_resource( s3.Bucket( "SpokeDeployBucket", PublicAccessBlockConfiguration=s3.PublicAccessBlockConfiguration( IgnorePublicAcls=True, BlockPublicPolicy=True, BlockPublicAcls=True, RestrictPublicBuckets=True, ), BucketEncryption=s3.BucketEncryption( ServerSideEncryptionConfiguration=[ s3.ServerSideEncryptionRule( ServerSideEncryptionByDefault=s3. ServerSideEncryptionByDefault(SSEAlgorithm="AES256")) ]), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), BucketName=t.Sub("sc-puppet-spoke-deploy-${AWS::AccountId}"), VersioningConfiguration=s3.VersioningConfiguration( Status="Enabled"), )) caching_bucket = template.add_resource( s3.Bucket( "CachingBucket", PublicAccessBlockConfiguration=s3.PublicAccessBlockConfiguration( BlockPublicAcls=True, BlockPublicPolicy=True, IgnorePublicAcls=True, RestrictPublicBuckets=True, ), BucketEncryption=s3.BucketEncryption( ServerSideEncryptionConfiguration=[ s3.ServerSideEncryptionRule( ServerSideEncryptionByDefault=s3. ServerSideEncryptionByDefault(SSEAlgorithm="AES256")) ]), Tags=t.Tags.from_dict( **{"ServiceCatalogPuppet:Actor": "Framework"}), BucketName=t.Sub( "sc-puppet-caching-bucket-${AWS::AccountId}-${AWS::Region}"), VersioningConfiguration=s3.VersioningConfiguration( Status="Enabled"), )) template.add_output( t.Output( "CloudFormationEventsQueueArn", Value=t.GetAtt(cloud_formation_events_queue, "Arn"), )) template.add_output(t.Output("Version", Value=t.GetAtt(param, "Value"))) template.add_output( t.Output("ManualApprovalsParam", Value=t.GetAtt(manual_approvals_param, "Value"))) template.add_resource( ssm.Parameter( "DefaultTerraformVersion", Type="String", Name=constants.DEFAULT_TERRAFORM_VERSION_PARAMETER_NAME, Value=constants.DEFAULT_TERRAFORM_VERSION_VALUE, )) return template
def handle(self, chain_context): """ This step adds in the shell of a pipeline. * s3 bucket * policies for the bucket and pipeline * your next step in the chain MUST be a source stage :param chain_context: :return: """ if self.create_bucket: pipeline_bucket = Bucket( "PipelineBucket%s" % chain_context.instance_name, BucketName=self.bucket_name, VersioningConfiguration=VersioningConfiguration( Status="Enabled")) chain_context.template.add_resource(pipeline_bucket) default_bucket_policies = self.get_default_bucket_policy_statements( self.bucket_name) if self.bucket_policy_statements: bucket_access_policy = self.get_bucket_policy( pipeline_bucket=self.bucket_name, bucket_policy_statements=self.bucket_policy_statements, ) chain_context.template.add_resource(bucket_access_policy) pipeline_bucket_access_policy = iam.ManagedPolicy( "PipelineBucketAccessPolicy", Path='/managed/', PolicyDocument=awacs.aws.PolicyDocument( Version="2012-10-17", Id="bucket-access-policy%s" % chain_context.instance_name, Statement=default_bucket_policies)) chain_context.metadata[cumulus.steps.dev_tools. META_PIPELINE_BUCKET_NAME] = self.bucket_name chain_context.metadata[ cumulus.steps.dev_tools.META_PIPELINE_BUCKET_POLICY_REF] = Ref( pipeline_bucket_access_policy) # TODO: this can be cleaned up by using a policytype and passing in the pipeline role it should add itself to. pipeline_policy = iam.Policy( PolicyName="%sPolicy" % self.name, PolicyDocument=awacs.aws.PolicyDocument( Version="2012-10-17", Id="PipelinePolicy", Statement=[ awacs.aws.Statement( Effect=awacs.aws.Allow, # TODO: actions here could be limited more Action=[awacs.aws.Action("s3", "*")], Resource=[ troposphere.Join( '', [awacs.s3.ARN(), self.bucket_name, "/*"]), troposphere.Join('', [ awacs.s3.ARN(), self.bucket_name, ]), ], ), awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[awacs.aws.Action("kms", "*")], Resource=['*'], ), awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[ awacs.aws.Action("cloudformation", "*"), awacs.aws.Action("codebuild", "*"), ], # TODO: restrict more accurately Resource=["*"]), awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[ awacs.codecommit.GetBranch, awacs.codecommit.GetCommit, awacs.codecommit.UploadArchive, awacs.codecommit.GetUploadArchiveStatus, awacs.codecommit.CancelUploadArchive ], Resource=["*"]), awacs.aws.Statement(Effect=awacs.aws.Allow, Action=[awacs.iam.PassRole], Resource=["*"]), awacs.aws.Statement( Effect=awacs.aws.Allow, Action=[awacs.aws.Action("lambda", "*")], Resource=["*"]) ], )) pipeline_service_role = iam.Role( "PipelineServiceRole", Path="/", AssumeRolePolicyDocument=awacs.aws.Policy(Statement=[ awacs.aws.Statement(Effect=awacs.aws.Allow, Action=[awacs.sts.AssumeRole], Principal=awacs.aws.Principal( 'Service', "codepipeline.amazonaws.com")) ]), Policies=[pipeline_policy] + self.pipeline_policies) generic_pipeline = codepipeline.Pipeline( "Pipeline", RoleArn=troposphere.GetAtt(pipeline_service_role, "Arn"), Stages=[], ArtifactStore=codepipeline.ArtifactStore( Type="S3", Location=self.bucket_name, ) # TODO: optionally add kms key here ) if self.bucket_kms_key_arn: encryption_config = codepipeline.EncryptionKey( "ArtifactBucketKmsKey", Id=self.bucket_kms_key_arn, Type='KMS', ) generic_pipeline.ArtifactStore.EncryptionKey = encryption_config pipeline_output = troposphere.Output( "PipelineName", Description="Code Pipeline", Value=Ref(generic_pipeline), ) pipeline_bucket_output = troposphere.Output( "PipelineBucket", Description="Name of the input artifact bucket for the pipeline", Value=self.bucket_name, ) chain_context.template.add_resource(pipeline_bucket_access_policy) chain_context.template.add_resource(pipeline_service_role) chain_context.template.add_resource(generic_pipeline) chain_context.template.add_output(pipeline_output) chain_context.template.add_output(pipeline_bucket_output)
"Allow", "Principal": { "Service": "s3.amazonaws.com" }, "Action": "SQS:SendMessage", "Resource": troposphere.GetAtt(ManifestQueue, "Arn"), "Condition": troposphere.If( "NeedsArchiveBucket", { "ForAllValues:ArnLike": { "aws:SourceArn": troposphere.Join("", [ "arn:aws:s3:::", troposphere.Ref(troposphere.AWS_STACK_NAME), "-archivebucket-*" ]) } }, { "ForAllValues:ArnEquals": { "aws:SourceArn": troposphere.Join("", [ "arn:aws:s3:::", troposphere.Ref(S3ArchiveBucket) ]) } }) }] }, DeletionPolicy=troposphere.Retain,
def __init__(self, stack, paco_ctx, role): super().__init__(stack, paco_ctx) self.set_aws_name('IoTAnalyticsPipeline', self.resource_group_name, self.resource_name) iotap = self.resource # Init Troposphere template self.init_template('IoT Analytics pipeline') if not iotap.is_enabled(): return # Role ARN for IoT role_arn_param = self.create_cfn_parameter( param_type='String', name='IoTRoleArn', description='IoT Topic Rule Service Role ARN', value=role.get_arn(), ) # Channel Resource iotchannel_logical_id = 'IoTAnalyticsChannel' cfn_export_dict = {} if iotap.channel_storage.bucket == None: channel_storage_dict = {'ServiceManagedS3': {}} cfn_export_dict['RetentionPeriod'] = convert_expire_to_cfn_dict( iotap.channel_storage.expire_events_after_days) else: channel_bucket_param = self.create_cfn_parameter( param_type='String', name='IoTAnalyticsChannelBucketName', description='IoT Analytics Channel storage bucket name', value=iotap.channel_storage.bucket + '.name', ) channel_storage_dict = { 'CustomerManagedS3': { 'Bucket': troposphere.Ref(channel_bucket_param), 'KeyPrefix': iotap.channel_storage.key_prefix, 'RoleArn': troposphere.Ref(role_arn_param), } } cfn_export_dict['ChannelStorage'] = channel_storage_dict iot_channel_resource = troposphere.iotanalytics.Channel.from_dict( iotchannel_logical_id, cfn_export_dict) self.template.add_resource(iot_channel_resource) self.create_output( title='ChannelName', description='IoT Analytics Channel name', value=troposphere.Ref(iot_channel_resource), ref=self.resource.paco_ref_parts + '.channel.name', ) # Datastore Resource iotchannel_logical_id = 'IoTAnalyticsDatastore' cfn_export_dict = {} if iotap.datastore_storage.bucket == None: datastore_storage_dict = {'ServiceManagedS3': {}} cfn_export_dict['RetentionPeriod'] = convert_expire_to_cfn_dict( iotap.datastore_storage.expire_events_after_days) else: datastore_bucket_param = self.create_cfn_parameter( param_type='String', name='IoTAnalyticsDatastoreBucketName', description='IoT Analytics Datastore storage bucket name', value=iotap.datastore_storage.bucket + '.name', ) datastore_storage_dict = { 'CustomerManagedS3': { 'Bucket': troposphere.Ref(datastore_bucket_param), 'KeyPrefix': iotap.datastore_storage.key_prefix, 'RoleArn': troposphere.Ref(role_arn_param), } } cfn_export_dict['DatastoreStorage'] = datastore_storage_dict if iotap.datastore_name != None: cfn_export_dict['DatastoreName'] = iotap.datastore_name iotap_datastore_resource = troposphere.iotanalytics.Datastore.from_dict( iotchannel_logical_id, cfn_export_dict) iotap_datastore_resource.DependsOn = iot_channel_resource self.template.add_resource(iotap_datastore_resource) self.create_output( title='DatastoreName', description='IoT Analytics Datastore name', value=troposphere.Ref(iotap_datastore_resource), ref=self.resource.paco_ref_parts + '.datastore.name', ) # Pipeline Resource iotpipeline_logical_id = 'IoTAnalyticsPipeline' cfn_export_dict = {} cfn_export_dict['PipelineActivities'] = [] idx = 0 activity_list = list(iotap.pipeline_activities.values()) # start with a Channel activity if len(activity_list) == 0: next_name = "DatastoreActivity" else: next_name = activity_list[idx].name + "Activity" cfn_export_dict['PipelineActivities'].append({ 'Channel': { 'Name': "ChannelActivity", 'ChannelName': troposphere.Ref(iot_channel_resource), 'Next': next_name, } }) for activity in iotap.pipeline_activities.values(): if len(activity_list) == idx + 1: next_name = 'DatastoreActivity' else: next_name = activity_list[idx + 1].name + "Activity" if activity.activity_type == 'lambda': lambda_param = self.create_cfn_parameter( param_type='String', name=f'LambdaFunction{idx}', description=f'IoT Analytics Lambda for Activity {idx}', value=activity.function + '.arn', ) if not activity.batch_size: activity.batch_size = 1 activity_dict = { 'Lambda': { 'LambdaName': troposphere.Join('', [ '', troposphere.Select( 6, troposphere.Split( ':', troposphere.Ref(lambda_param))) ]), 'BatchSize': activity.batch_size, 'Name': activity.name + "Activity", 'Next': next_name, } } elif activity.activity_type == 'add_attributes': activity_dict = { 'AddAttributes': { 'Name': activity.name + "Activity", 'Attributes': activity.attributes, 'Next': next_name, } } elif activity.activity_type == 'remove_attributes': activity_dict = { 'RemoveAttributes': { 'Name': activity.name + "Activity", 'Attributes': activity.attribute_list, 'Next': next_name, } } elif activity.activity_type == 'select_attributes': activity_dict = { 'SelectAttributes': { 'Name': activity.name + "Activity", 'Attributes': activity.attribute_list, 'Next': next_name, } } elif activity.activity_type == 'filter': activity_dict = { 'Filter': { 'Name': activity.name + "Activity", 'Filter': activity.filter, 'Next': next_name, } } elif activity.activity_type == 'math': activity_dict = { 'Math': { 'Name': activity.name + "Activity", 'Attribute': activity.attribute, 'Math': activity.math, 'Next': next_name, } } elif activity.activity_type == 'device_registry_enrich': activity_dict = { 'DeviceRegistryEnrich': { 'Name': activity.name + "Activity", 'Attribute': activity.attribute, 'ThingName': activity.thing_name, 'Next': next_name, } } elif activity.activity_type == 'device_shadow_enrich': activity_dict = { 'DeviceShadowEnrich': { 'Name': activity.name + "Activity", 'Attribute': activity.attribute, 'ThingName': activity.thing_name, 'Next': next_name, } } cfn_export_dict['PipelineActivities'].append(activity_dict) idx += 1 # finish with a Datastore activity cfn_export_dict['PipelineActivities'].append({ 'Datastore': { 'Name': "DatastoreActivity", 'DatastoreName': troposphere.Ref(iotap_datastore_resource), } }) iotpipeline_resource = troposphere.iotanalytics.Pipeline.from_dict( iotpipeline_logical_id, cfn_export_dict, ) iotpipeline_resource.DependsOn = [ iot_channel_resource, iotap_datastore_resource ] self.template.add_resource(iotpipeline_resource) self.create_output( title='PipelineName', description='IoT Analytics Pipeline name', value=troposphere.Ref(iotpipeline_resource), ref=self.resource.paco_ref_parts + '.pipeline.name', ) # Datasets for dataset in iotap.datasets.values(): iotdataset_logical_id = self.create_cfn_logical_id( f'IoTDataset{dataset.name}') cfn_export_dict = {} cfn_export_dict['Actions'] = [] if dataset.query_action != None: cfn_export_dict['Actions'].append({ 'ActionName': dataset.name, 'QueryAction': { 'Filters': dataset.query_action.filters, 'SqlQuery': dataset.query_action.sql_query, } }) else: # ToDo: container_action pass cfn_export_dict['ContentDeliveryRules'] = [] for delivery_rule in dataset.content_delivery_rules.values(): delivery_dict = { 'Destination': {}, # 'EntryName': delivery_rule.name, } if delivery_rule.s3_destination != None: bucket = get_model_obj_from_ref( delivery_rule.s3_destination.bucket, self.paco_ctx.project) delivery_dict['Destination'][ 'S3DestinationConfiguration'] = { 'Bucket': bucket.get_aws_name(), 'Key': delivery_rule.s3_destination.key, 'RoleArn': troposphere.Ref(role_arn_param), } cfn_export_dict['ContentDeliveryRules'].append(delivery_dict) cfn_export_dict['RetentionPeriod'] = convert_expire_to_cfn_dict( dataset.expire_events_after_days) if dataset.version_history != None: if dataset.version_history == 0: cfn_export_dict['VersioningConfiguration'] = { 'Unlimited': True } else: cfn_export_dict['VersioningConfiguration'] = { 'MaxVersions': dataset.version_history, 'Unlimited': False } iot_dataset_resource = troposphere.iotanalytics.Dataset.from_dict( iotdataset_logical_id, cfn_export_dict) iot_dataset_resource.DependsOn = iotap_datastore_resource self.template.add_resource(iot_dataset_resource) self.create_output( title=f'{dataset.name}DatasetName', description=f'IoT Analytics Dataset {dataset.name}', value=troposphere.Ref(iot_dataset_resource), ref=self.resource.paco_ref_parts + '.dataset.' + dataset.name + '.name', )
def __init__(self, stack, paco_ctx): super().__init__(stack, paco_ctx, iam_capabilities=["CAPABILITY_IAM"]) self.apigatewayrestapi = apigatewayrestapi = stack.resource self.set_aws_name('ApiGatewayRestApi', self.resource_group_name, self.resource.name) self.init_template('ApiGateway: {}'.format(apigatewayrestapi.title)) template = self.template if not self.apigatewayrestapi.is_enabled(): return # Parameters method_params = [] for method in self.apigatewayrestapi.methods.values(): param_name = 'MethodArn' + self.create_cfn_logical_id(method.name) lambda_arn_param = self.create_cfn_parameter( name=param_name, param_type='String', description='Lambda ARN parameter.', value=method.integration.integration_lambda + '.arn', ) method.parameter_arn_ref = troposphere.Ref(param_name) # Resources restapi_logical_id = 'ApiGatewayRestApi' restapi_resource = troposphere.apigateway.RestApi.from_dict( restapi_logical_id, self.apigatewayrestapi.cfn_export_dict ) template.add_resource(restapi_resource) # Model for model in self.apigatewayrestapi.models.values(): model.logical_id = self.create_cfn_logical_id('ApiGatewayModel' + model.name) cfn_export_dict = model.cfn_export_dict cfn_export_dict['RestApiId'] = troposphere.Ref(restapi_resource) if 'Schema' not in cfn_export_dict: cfn_export_dict['Schema'] = {} model_resource = troposphere.apigateway.Model.from_dict(model.logical_id, cfn_export_dict) model.resource = model_resource template.add_resource(model_resource) # Resource for resource in self.apigatewayrestapi.resources.values(): resource_id = 'ApiGatewayResource' + self.create_cfn_logical_id(resource.name) cfn_export_dict = resource.cfn_export_dict if resource.parent_id == "RootResourceId": cfn_export_dict["ParentId"] = troposphere.GetAtt(restapi_resource, "RootResourceId") cfn_export_dict["RestApiId"] = troposphere.Ref(restapi_resource) else: raise NotImplemented("ToDo: handle nested resources") resource_resource = troposphere.apigateway.Resource.from_dict(resource_id, cfn_export_dict) resource.resource = resource_resource resource_resource.DependsOn = restapi_logical_id template.add_resource(resource_resource) # Method for method in self.apigatewayrestapi.methods.values(): method_id = 'ApiGatewayMethod' + self.create_cfn_logical_id(method.name) method.logical_id = method_id cfn_export_dict = method.cfn_export_dict for resource in self.apigatewayrestapi.resources.values(): if resource.name == method.resource_id: cfn_export_dict["ResourceId"] = troposphere.Ref(resource.resource) if 'ResourceId' not in cfn_export_dict: cfn_export_dict["ResourceId"] = troposphere.GetAtt(restapi_resource, 'RootResourceId') cfn_export_dict["RestApiId"] = troposphere.Ref(restapi_resource) uri = troposphere.Join('', ["arn:aws:apigateway:", method.region_name, ":lambda:path/2015-03-31/functions/", method.parameter_arn_ref, "/invocations"]) cfn_export_dict["Integration"]["Uri"] = uri if method.integration.integration_type == 'AWS_PROXY': # IAM Role - allows API Gateway to invoke Lambda # ToDo: enable Api Gateway to invoke things other than Lambda ... iam_role_resource = troposphere.iam.Role( self.create_cfn_logical_id('ApiGatewayIamRole' + self.apigatewayrestapi.name + method.name), Path='/', AssumeRolePolicyDocument=Policy( Version='2012-10-17', Statement=[ Statement( Effect=Allow, Action=[awacs.sts.AssumeRole], Principal=Principal('Service',['apigateway.amazonaws.com']) ) ], ), Policies=[ troposphere.iam.Policy( PolicyName=self.create_cfn_logical_id('LambdaAccessApiGateway' + self.apigatewayrestapi.name + method.name), PolicyDocument=Policy( Version='2012-10-17', Statement=[ Statement( Effect=Allow, Action=[awacs.awslambda.InvokeFunction], Resource=[method.parameter_arn_ref], ) ] ) ) ] ) template.add_resource(iam_role_resource) cfn_export_dict["Integration"]["Credentials"] = troposphere.GetAtt(iam_role_resource, "Arn") elif method.integration.integration_type == 'AWS': # Enable Lambda (custom) integration # When send to a Lambda (Custom) the HTTP Method must always be POST regardless of # the HttpMethod cfn_export_dict["Integration"]["IntegrationHttpMethod"] = "POST" lambda_permission_resource = troposphere.awslambda.Permission( self.create_cfn_logical_id('LambdaPermissionApiGateway' + method.name), Action = 'lambda:InvokeFunction', FunctionName = method.parameter_arn_ref, Principal = 'apigateway.amazonaws.com', SourceArn = troposphere.Sub( "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${%s}/*/%s/" % ( restapi_logical_id, method.http_method ) ) ) template.add_resource(lambda_permission_resource) # look-up the method_names and assign a Ref to the model resource # ToDo: validate model_names in the model responses = [] for method_response in method.method_responses: response_dict = {"StatusCode": method_response.status_code} if method_response.response_models: response_dict["ResponseModels"] = {} for response_model in method_response.response_models: for model in self.apigatewayrestapi.models.values(): if model.name == response_model.model_name: response_dict["ResponseModels"][response_model.content_type] = troposphere.Ref(model.resource) responses.append(response_dict) cfn_export_dict["MethodResponses"] = responses method_resource = troposphere.apigateway.Method.from_dict(method_id, cfn_export_dict) method_resource.DependsOn = restapi_logical_id template.add_resource(method_resource) # Deployment deployment_resource = troposphere.apigateway.Deployment.from_dict( 'ApiGatewayDeployment', {'Description': 'Deployment', 'RestApiId': troposphere.Ref(restapi_resource) } ) # ToDo: Deployment depends upon all Methods for method in self.apigatewayrestapi.methods.values(): deployment_resource.DependsOn = method.logical_id template.add_resource(deployment_resource) # Stage for stage in self.apigatewayrestapi.stages.values(): stage_id = self.create_cfn_logical_id('ApiGatewayStage' + stage.name) cfn_export_dict = stage.cfn_export_dict cfn_export_dict["RestApiId"] = troposphere.Ref(restapi_resource) cfn_export_dict["DeploymentId"] = troposphere.Ref(deployment_resource) stage_resource = troposphere.apigateway.Stage.from_dict(stage_id, cfn_export_dict) template.add_resource(stage_resource)
def __init__(self, stack, paco_ctx,): rds_aurora = stack.resource super().__init__(stack, paco_ctx, iam_capabilities=["CAPABILITY_IAM"]) self.set_aws_name('RDSAurora', self.resource_group_name, self.resource.name) self.init_template('RDSAurora') if not rds_aurora.is_enabled(): return rds_cluster_logical_id = 'DBCluster' db_cluster_dict = rds_aurora.cfn_export_dict self.notification_groups = {} # DB Subnet Group db_subnet_id_list_param = self.create_cfn_parameter( param_type='List<AWS::EC2::Subnet::Id>', name='DBSubnetIdList', description='The list of subnet IDs where this database will be provisioned.', value=rds_aurora.segment + '.subnet_id_list', ) db_subnet_group_resource = troposphere.rds.DBSubnetGroup( title='DBSubnetGroup', template=self.template, DBSubnetGroupDescription=troposphere.Ref('AWS::StackName'), SubnetIds=troposphere.Ref(db_subnet_id_list_param), ) db_cluster_dict['DBSubnetGroupName'] = troposphere.Ref(db_subnet_group_resource) # DB Cluster Parameter Group if rds_aurora.cluster_parameter_group == None: # If no Cluster Parameter Group supplied then create one param_group_family = gen_vocabulary.rds_engine_versions[rds_aurora.engine][rds_aurora.engine_version]['param_group_family'] cluster_parameter_group_ref = troposphere.rds.DBClusterParameterGroup( "DBClusterParameterGroup", template=self.template, Family=param_group_family, Description=troposphere.Ref('AWS::StackName') ) else: # Use existing Parameter Group cluster_parameter_group_ref = self.create_cfn_parameter( name='DBClusterParameterGroupName', param_type='String', description='DB Cluster Parameter Group Name', value=rds_aurora.cluster_parameter_group + '.name', ) db_cluster_dict['DBClusterParameterGroupName'] = troposphere.Ref(cluster_parameter_group_ref) # Default DB Parameter Group need_db_pg = False default_instance = rds_aurora.default_instance for db_instance in rds_aurora.db_instances.values(): if default_instance.parameter_group == None and db_instance.parameter_group == None: need_db_pg = True if need_db_pg: # create default DB Parameter Group param_group_family = gen_vocabulary.rds_engine_versions[rds_config.engine][rds_config.engine_version]['param_group_family'] default_dbparametergroup_resource = troposphere.rds.DBParameterGroup( "DBParameterGroup", template=self.template, Family=param_group_family, Description=troposphere.Ref('AWS::StackName') ) # Enhanced Monitoring Role need_monitoring_role = False for db_instance in rds_aurora.db_instances.values(): enhanced_monitoring_interval = db_instance.get_value_or_default('enhanced_monitoring_interval_in_seconds') if enhanced_monitoring_interval != 0: need_monitoring_role = True if need_monitoring_role: enhanced_monitoring_role_resource = troposphere.iam.Role( title='MonitoringIAMRole', template=self.template, AssumeRolePolicyDocument=PolicyDocument( Statement=[ Statement( Effect=Allow, Action=[Action("sts", "AssumeRole")], Principal=Principal("Service", "monitoring.rds.amazonaws.com") ) ] ), ManagedPolicyArns=["arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"], Path="/", ) # DB Snapshot Identifier if rds_aurora.db_snapshot_identifier == '' or rds_aurora.db_snapshot_identifier == None: db_snapshot_id_enabled = False else: db_snapshot_id_enabled = True if db_snapshot_id_enabled == True: db_cluster_dict['SnapshotIdentifier'] = rds_aurora.db_snapshot_identifier # KMS-CMK key encryption if rds_aurora.enable_kms_encryption == True and db_snapshot_id_enabled == False: key_policy = Policy( Version='2012-10-17', Statement=[ Statement( Effect=Allow, Action=[Action('kms', '*'),], Principal=Principal("AWS", [f'arn:aws:iam::{self.stack.account_ctx.id}:root']), Resource=['*'], ), Statement( Effect=Allow, Action=[ awacs.kms.Encrypt, awacs.kms.Decrypt, Action('kms', 'ReEncrypt*'), Action('kms', 'GenerateDataKey*'), awacs.kms.CreateGrant, awacs.kms.ListGrants, awacs.kms.DescribeKey, ], Principal=Principal('AWS',['*']), Resource=['*'], Condition=Condition([ StringEquals({ 'kms:CallerAccount': f'{self.stack.account_ctx.id}', 'kms:ViaService': f'rds.{self.stack.aws_region}.amazonaws.com' }) ]), ), ], ) kms_key_resource = troposphere.kms.Key( title='AuroraKMSCMK', template=self.template, KeyPolicy=key_policy, ) db_cluster_dict['StorageEncrypted'] = True db_cluster_dict['KmsKeyId'] = troposphere.Ref(kms_key_resource) kms_key_alias_resource = troposphere.kms.Alias( title="AuroraKMSCMKAlias", template=self.template, AliasName=troposphere.Sub('alias/${' + rds_cluster_logical_id + '}'), TargetKeyId=troposphere.Ref(kms_key_resource), ) kms_key_alias_resource.DependsOn = rds_cluster_logical_id # Username and Passsword - only if there is no DB Snapshot Identifier if db_snapshot_id_enabled == False: db_cluster_dict['MasterUsername'] = rds_aurora.master_username if rds_aurora.secrets_password: # Password from Secrets Manager sta_logical_id = 'SecretTargetAttachmentRDS' secret_arn_param = self.create_cfn_parameter( param_type='String', name='RDSSecretARN', description='The ARN for the secret for the RDS master password.', value=rds_aurora.secrets_password + '.arn', ) secret_target_attachment_resource = troposphere.secretsmanager.SecretTargetAttachment( title=sta_logical_id, template=self.template, SecretId=troposphere.Ref(secret_arn_param), TargetId=troposphere.Ref(rds_cluster_logical_id), TargetType='AWS::RDS::DBCluster' ) secret_target_attachment_resource.DependsOn = rds_cluster_logical_id db_cluster_dict['MasterUserPassword'] = troposphere.Join( '', ['{{resolve:secretsmanager:', troposphere.Ref(secret_arn_param), ':SecretString:password}}' ] ) else: master_password_param = self.create_cfn_parameter( param_type='String', name='MasterUserPassword', description='The master user password.', value=rds_aurora.master_user_password, noecho=True, ) db_cluster_dict['MasterUserPassword'] = troposphere.Ref(master_password_param) db_cluster_res = troposphere.rds.DBCluster.from_dict( rds_cluster_logical_id, db_cluster_dict ) self.template.add_resource(db_cluster_res) # Cluster Event Notifications if hasattr(rds_aurora, 'cluster_event_notifications'): for group in rds_aurora.cluster_event_notifications.groups: notif_param = self.create_notification_param(group) event_subscription_resource = troposphere.rds.EventSubscription( title=self.create_cfn_logical_id(f"ClusterEventSubscription{group}"), template=self.template, EventCategories=rds_aurora.cluster_event_notifications.event_categories, SourceIds=[troposphere.Ref(db_cluster_res)], SnsTopicArn=troposphere.Ref(notif_param), SourceType='db-cluster', ) # DB Instance(s) for db_instance in rds_aurora.db_instances.values(): logical_name = self.create_cfn_logical_id(db_instance.name) db_instance_dict = { 'DBClusterIdentifier': troposphere.Ref(db_cluster_res), 'DBInstanceClass': db_instance.get_value_or_default('db_instance_type'), 'DBSubnetGroupName': troposphere.Ref(db_subnet_group_resource), 'EnablePerformanceInsights': db_instance.get_value_or_default('enable_performance_insights'), 'Engine': rds_aurora.engine, 'PubliclyAccessible': db_instance.get_value_or_default('publicly_accessible'), 'AllowMajorVersionUpgrade': db_instance.get_value_or_default('allow_major_version_upgrade'), 'AutoMinorVersionUpgrade': db_instance.get_value_or_default('auto_minor_version_upgrade'), } enhanced_monitoring_interval = db_instance.get_value_or_default('enhanced_monitoring_interval_in_seconds') if enhanced_monitoring_interval != 0: db_instance_dict['MonitoringInterval'] = enhanced_monitoring_interval db_instance_dict['MonitoringRoleArn'] = troposphere.GetAtt(enhanced_monitoring_role_resource, "Arn") if db_instance.availability_zone != None: subnet_id_ref = f'{rds_aurora.segment}.az{db_instance.availability_zone}.availability_zone' db_instance_subnet_param = self.create_cfn_parameter( param_type='String', name=f'DBInstanceAZ{logical_name}', description=f'Subnet where DB Instance {logical_name} is provisioned', value=subnet_id_ref, ) db_instance_dict['AvailabilityZone'] = troposphere.Ref(db_instance_subnet_param) # DB Parameter Group if default_instance.parameter_group == None and db_instance.parameter_group == None: dbparametergroup_resource = default_dbparametergroup_resource elif db_instance.parameter_group != None: # Use instance-specific DB Parameter Group dbparametergroup_resource = self.create_cfn_parameter( name=f'DBParameterGroupName{logical_name}', param_type='String', description='DB Parameter Group Name', value=db_instance.parameter_group + '.name', ) else: # Use default DB Parameter Group dbparametergroup_resource = self.create_cfn_parameter( name=f'DBParameterGroupName{logical_name}', param_type='String', description='DB Parameter Group Name', value=default_instance.parameter_group + '.name', ) db_instance_dict['DBParameterGroupName'] = troposphere.Ref(dbparametergroup_resource) db_instance_resource = troposphere.rds.DBInstance.from_dict( f'DBInstance{logical_name}', db_instance_dict ) self.template.add_resource(db_instance_resource) # DB Event Notifications event_notifications = db_instance.get_value_or_default('event_notifications') if event_notifications != None: for group in event_notifications.groups: notif_param = self.create_notification_param(group) event_subscription_resource = troposphere.rds.EventSubscription( title=self.create_cfn_logical_id(f"DBEventSubscription{logical_name}{group}"), template=self.template, EventCategories=event_notifications.event_categories, SourceIds=[troposphere.Ref(db_instance_resource)], SnsTopicArn=troposphere.Ref(notif_param), SourceType='db-instance', ) # DB Instance Outputs self.create_output( title=f'DBInstanceName{logical_name}', description=f'DB Instance Name for {logical_name}', value=troposphere.Ref(db_instance_resource), ref=db_instance.paco_ref_parts + ".name", ) # DB Cluster Outputs self.create_output( title='DBClusterName', description='DB Cluster Name', value=troposphere.Ref(db_cluster_res), ref=self.resource.paco_ref_parts + ".name", ) self.create_output( title='ClusterEndpointAddress', description='Cluster Endpoint Address', value=troposphere.GetAtt(db_cluster_res, 'Endpoint.Address'), ref=self.resource.paco_ref_parts + ".endpoint.address", ) self.create_output( title='ClusterEndpointPort', description='Cluster Endpoint Port', value=troposphere.GetAtt(db_cluster_res, 'Endpoint.Port'), ref=self.resource.paco_ref_parts + ".endpoint.port", ) self.create_output( title='ClusterReadEndpointAddress', description='Cluster ReadEndpoint Address', value=troposphere.GetAtt(db_cluster_res, 'ReadEndpoint.Address'), ref=self.resource.paco_ref_parts + ".readendpoint.address", ) # DNS - Route53 Record Set if rds_aurora.is_dns_enabled() == True: route53_ctl = self.paco_ctx.get_controller('route53') for dns in rds_aurora.dns: route53_ctl.add_record_set( self.account_ctx, self.aws_region, rds_aurora, enabled=rds_aurora.is_enabled(), dns=dns, record_set_type='CNAME', resource_records=[rds_aurora.paco_ref + '.endpoint.address'], stack_group=self.stack.stack_group, ) for read_dns in rds_aurora.read_dns: route53_ctl.add_record_set( self.account_ctx, self.aws_region, rds_aurora, enabled=rds_aurora.is_enabled(), dns=read_dns, record_set_type='CNAME', resource_records=[rds_aurora.paco_ref + '.readendpoint.address'], stack_group=self.stack.stack_group, )
def __init__(self, stack, paco_ctx,): rds_config = stack.resource config_ref = rds_config.paco_ref_parts super().__init__(stack, paco_ctx) self.set_aws_name('RDS', self.resource_group_name, self.resource.name) self.init_template('RDS') template = self.template if not rds_config.is_enabled(): return rds_logical_id = 'PrimaryDBInstance' # DB Subnet Group db_subnet_id_list_param = self.create_cfn_parameter( param_type='List<AWS::EC2::Subnet::Id>', name='DBSubnetIdList', description='The list of subnet IDs where this database will be provisioned.', value=rds_config.segment+'.subnet_id_list', ) db_subnet_group_res = troposphere.rds.DBSubnetGroup( title='DBSubnetGroup', template =template, DBSubnetGroupDescription=troposphere.Ref('AWS::StackName'), SubnetIds=troposphere.Ref(db_subnet_id_list_param), ) # DB Parameter Group if rds_config.parameter_group == None: # No Parameter Group supplied, create one engine_major_version = '.'.join(rds_config.engine_version.split('.')[0:2]) param_group_family = gen_vocabulary.rds_engine_versions[rds_config.engine][rds_config.engine_version]['param_group_family'] dbparametergroup_ref = troposphere.rds.DBParameterGroup( "DBParameterGroup", template = template, Family=param_group_family, Description=troposphere.Ref('AWS::StackName') ) else: # Use an existing Parameter Group dbparametergroup_ref = self.create_cfn_parameter( name='DBParameterGroupName', param_type='String', description='DB Parameter Group Name', value=rds_config.parameter_group + '.name', ) # Option Group option_group_res = None if len(rds_config.option_configurations) > 0: option_group_dict = { 'EngineName': rds_config.engine, 'MajorEngineVersion': engine_major_version, 'OptionGroupDescription': troposphere.Ref('AWS::StackName') } if len(rds_config.option_configurations) > 0: option_config_list = [] for option_config in rds_config.option_configurations: option_config_dict = { 'OptionName': option_config.option_name, } if len(option_config.option_settings) > 0: option_config_dict['OptionSettings'] = [] for option_setting in option_config.option_settings: option_setting_dict = { 'Name': option_setting.name, 'Value': option_setting.value } option_config_dict['OptionSettings'].append(option_setting_dict) option_config_list.append(option_config_dict) option_group_dict['OptionConfigurations'] = option_config_list option_group_res = troposphere.rds.OptionGroup.from_dict( 'OptionGroup', option_group_dict ) template.add_resource(option_group_res) # RDS MultiAZ (Mysql, Postgresql) if schemas.IRDSMultiAZ.providedBy(rds_config): sg_param_ref_list = [] for sg_ref in rds_config.security_groups: sg_hash = utils.md5sum(str_data=sg_ref) sg_param = self.create_cfn_parameter( param_type='AWS::EC2::SecurityGroup::Id', name=self.create_cfn_logical_id('SecurityGroup'+sg_hash), description='VPC Security Group to attach to the RDS.', value=sg_ref+'.id', ) sg_param_ref_list.append(troposphere.Ref(sg_param)) db_instance_dict = { 'Engine': rds_config.engine, 'EngineVersion': rds_config.engine_version, 'DBInstanceIdentifier': troposphere.Ref('AWS::StackName'), 'DBInstanceClass': rds_config.db_instance_type, 'DBSubnetGroupName': troposphere.Ref(db_subnet_group_res), 'DBParameterGroupName': troposphere.Ref(dbparametergroup_ref), 'CopyTagsToSnapshot': True, 'AllowMajorVersionUpgrade': rds_config.allow_major_version_upgrade, 'AutoMinorVersionUpgrade': rds_config.auto_minor_version_upgrade, 'MultiAZ': rds_config.multi_az, 'AllocatedStorage': rds_config.storage_size_gb, 'StorageType': rds_config.storage_type, 'BackupRetentionPeriod': rds_config.backup_retention_period, 'Port': rds_config.port, 'PreferredBackupWindow': rds_config.backup_preferred_window, 'PreferredMaintenanceWindow': rds_config.maintenance_preferred_window, 'VPCSecurityGroups': sg_param_ref_list } # License Model if rds_config.license_model: db_instance_dict['LicenseModel'] = rds_config.license_model # Deletion Protection if rds_config.deletion_protection: db_instance_dict['DeletionProtection'] = rds_config.deletion_protection # CloudWatch Logs Exports if len(rds_config.cloudwatch_logs_exports) > 0: db_instance_dict['EnableCloudwatchLogsExports'] = rds_config.cloudwatch_logs_exports # Option Group if option_group_res != None: db_instance_dict['OptionGroupName'] = troposphere.Ref(option_group_res) # DB Snapshot Identifier if rds_config.db_snapshot_identifier == '' or rds_config.db_snapshot_identifier == None: db_snapshot_id_enabled = False else: db_snapshot_id_enabled = True if db_snapshot_id_enabled == True: db_instance_dict['DBSnapshotIdentifier'] = rds_config.db_snapshot_identifier # Encryption if rds_config.kms_key_id == '' or rds_config.kms_key_id == None: encryption_enabled = False else: encryption_enabled = True if db_snapshot_id_enabled == False: db_instance_dict['StorageEncrypted'] = encryption_enabled if encryption_enabled: db_instance_dict['KmsKeyId'] = rds_config.kms_key_id # Username and Passsword if db_snapshot_id_enabled == False: db_instance_dict['MasterUsername'] = rds_config.master_username if rds_config.secrets_password: # Password from Secrets Manager sta_logical_id = 'SecretTargetAttachmentRDS' secret_arn_param = self.create_cfn_parameter( param_type='String', name='RDSSecretARN', description='The ARN for the secret for the RDS master password.', value=rds_config.secrets_password + '.arn', ) secret_target_attachment_resource = troposphere.secretsmanager.SecretTargetAttachment( title=sta_logical_id, SecretId=troposphere.Ref(secret_arn_param), TargetId=troposphere.Ref(rds_logical_id), TargetType='AWS::RDS::DBInstance' ) template.add_resource(secret_target_attachment_resource) db_instance_dict['MasterUserPassword'] = troposphere.Join( '', ['{{resolve:secretsmanager:', troposphere.Ref(secret_arn_param), ':SecretString:password}}' ] ) else: master_password_param = self.create_cfn_parameter( param_type='String', name='MasterUserPassword', description='The master user password.', value=rds_config.master_user_password, noecho=True, ) db_instance_dict['MasterUserPassword'] = troposphere.Ref(master_password_param) db_instance_res = troposphere.rds.DBInstance.from_dict( rds_logical_id, db_instance_dict ) template.add_resource(db_instance_res) # Outputs self.create_output( title='DBInstanceName', description='DB Instance Name', value=troposphere.Ref(db_instance_res), ref=config_ref + ".name", ) self.create_output( title='RDSEndpointAddress', description='RDS Endpoint URL', value=troposphere.GetAtt(db_instance_res, 'Endpoint.Address'), ref=config_ref + ".endpoint.address", ) # Legacy Route53 Record Set if self.paco_ctx.legacy_flag('route53_record_set_2019_10_16') == True: if rds_config.is_dns_enabled() == True: for dns_config in rds_config.dns_config: dns_hash = utils.md5sum(str_data=(rds_config.hosted_zone+rds_config.domain_name)) primary_hosted_zone_id_param = self.create_cfn_parameter( param_type='String', name='DNSHostedZoneId'+dns_hash, description='The hosted zone id to create the Route53 record set.', value=rds_config.primary_hosted_zone+'.id', ) record_set_res = troposphere.route53.RecordSetType( title = 'RecordSet'+dns_hash, template = template, Comment = 'RDS Primary DNS', HostedZoneId = troposphere.Ref(primary_hosted_zone_id_param), Name = rds_config.primary_domain_name, Type = 'CNAME', TTL = dns_config.ttl, ResourceRecords = [ troposphere.GetAtt(db_instance_res, 'Endpoint.Address')] ) record_set_res.DependsOn = db_instance_res # DNS - Route53 Record Set if self.paco_ctx.legacy_flag('route53_record_set_2019_10_16') == False: if rds_config.is_dns_enabled() == True: route53_ctl = self.paco_ctx.get_controller('route53') for dns_config in rds_config.dns: route53_ctl.add_record_set( self.account_ctx, self.aws_region, rds_config, enabled=rds_config.is_enabled(), dns=dns_config, record_set_type='CNAME', resource_records=['paco.ref ' + config_ref + '.endpoint.address'], stack_group=self.stack.stack_group, config_ref=rds_config.paco_ref_parts + '.dns' )
def create_policy_statments(policies, s3bucket_arn_param=None, s3bucket_arn=None): policy_statements = [] for policy_statement in policies: statement_dict = { 'Effect': policy_statement.effect, 'Action': [Action(*action.split(':')) for action in policy_statement.action], } # Sid if policy_statement.sid != None and len(policy_statement.sid) > 0: statement_dict['Sid'] = policy_statement.sid # Principal if policy_statement.principal != None and len( policy_statement.principal) > 0: # ToDo: awacs only allows one type of Principal ... is there a use-case where # multiple principal types are needed? for key, value in policy_statement.principal.items(): statement_dict['Principal'] = Principal(key, value) elif policy_statement.aws != None and len(policy_statement.aws) > 0: statement_dict['Principal'] = Principal('AWS', policy_statement.aws) # Condition if policy_statement.condition != {}: conditions = [] for condition_key, condition_value in policy_statement.condition.items( ): # Conditions can be simple: # StringEquals # Or prefixed with ForAnyValue or ForAllValues # ForAnyValue:StringEquals condition_key = condition_key.replace(':', '') condition_class = globals()[condition_key] conditions.append(condition_class(condition_value)) statement_dict['Condition'] = Condition(conditions) # Resource # S3BucketPolicy uses Parameters if s3bucket_arn_param != None: if policy_statement.resource_suffix and len( policy_statement.resource_suffix) > 0: statement_dict['Resource'] = [] for res_suffix in policy_statement.resource_suffix: if res_suffix == '': statement_dict['Resource'].append( troposphere.Ref(s3bucket_arn_param)) else: statement_dict['Resource'].append( troposphere.Join('', [ troposphere.Ref(s3bucket_arn_param), res_suffix ])) else: statement_dict['Resource'] = [ troposphere.Ref(s3bucket_arn_param) ] # S3Bucket uses embdedded strings else: if policy_statement.resource_suffix and len( policy_statement.resource_suffix) > 0: statement_dict['Resource'] = [ s3bucket_arn + res_suffix for res_suffix in policy_statement.resource_suffix ] else: statement_dict['Resource'] = [s3bucket_arn] policy_statements.append(Statement(**statement_dict)) return policy_statements
def get_bucket_arn(self): bucket = self.settings.get('bucket') if bucket.startswith('arn:aws:'): return bucket return troposphere.Join("", ["arn:aws:s3:::", bucket])
def create_template(self): t = self.template t.add_description("Acceptance Tests for cumulus pipelines") instance = self.name + self.context.environment['env'] # TODO: give to builder the_chain = chain.Chain() # bucket becomes: cumulus-acceptance-tests-123123-namespace pipeline_bucket_name = troposphere.Join('', [ self.context.namespace, "-", troposphere.Ref("AWS::AccountId"), "-", "automatedtests" ]) the_chain.add( pipeline.Pipeline( name=self.name, bucket_name=pipeline_bucket_name, )) source_stage_name = "SourceStage" deploy_stage_name = "DeployStage" service_artifact = "ServiceArtifact" the_chain.add( pipeline_stage.PipelineStage(stage_name=source_stage_name)) the_chain.add( pipeline_source_action.PipelineSourceAction( action_name="MicroserviceSource", output_artifact_name=service_artifact, s3_bucket_name=pipeline_bucket_name, s3_object_key="artifact.tar.gz")) the_chain.add( pipeline_stage.PipelineStage(stage_name=deploy_stage_name, ), ) inline_ls_url_spec = """version: 0.2 phases: build: commands: - ls -lah - env - curl $URL """ test_env = troposphere.codebuild.Environment( ComputeType='BUILD_GENERAL1_SMALL', Image='aws/codebuild/golang:1.10', Type='LINUX_CONTAINER', EnvironmentVariables=[{ 'Name': 'URL', 'Value': "https://google.ca" }], ) deploy_test = code_build_action.CodeBuildAction( action_name="DeployMyStuff", stage_name_to_add=deploy_stage_name, input_artifact_name=service_artifact, environment=test_env, buildspec=inline_ls_url_spec, ) the_chain.add(deploy_test) lambda1 = lambda_action.LambdaAction( action_name="TriggerLambda", input_artifact_name=service_artifact, # TODO make optional ? stage_name_to_add=deploy_stage_name, function_name="bswift-mock-function-mock-createUser") the_chain.add(lambda1) # the_chain.add(code_build_action.CodeBuildAction( # action_name="NotificationSmokeTest", # stage_name_to_add=deploy_stage_name, # input_artifact_name=service_artifact, # environment=test_env, # buildspec='buildspec_smoke_test.yml', # )) # TODO: integration tests don't confirm the below.. yet. Do it. destroy_stage_name = "EchoAURL" the_chain.add( pipeline_stage.PipelineStage(stage_name=destroy_stage_name, ), ) the_chain.add( ApprovalAction(action_name="ApproveDestruction", stage_name_to_add=destroy_stage_name)) the_chain.add( code_build_action.CodeBuildAction( action_name="DestroyService", stage_name_to_add=destroy_stage_name, input_artifact_name=service_artifact, )) chain_context = chaincontext.ChainContext(template=t, instance_name=instance) the_chain.run(chain_context)
def get_bucket_arn(self): bucket_name = self.get_bucket_name() return troposphere.Join("", ["arn:aws:s3:::", bucket_name])
def __init__(self, stack, paco_ctx,): rds_config = stack.resource config_ref = rds_config.paco_ref_parts super().__init__(stack, paco_ctx, iam_capabilities=["CAPABILITY_IAM"]) self.set_aws_name('RDS', self.resource_group_name, self.resource.name) self.init_template('RDS') template = self.template if not rds_config.is_enabled(): return rds_logical_id = 'PrimaryDBInstance' # DB Subnet Group db_subnet_id_list_param = self.create_cfn_parameter( param_type='List<AWS::EC2::Subnet::Id>', name='DBSubnetIdList', description='The list of subnet IDs where this database will be provisioned.', value=rds_config.segment+'.subnet_id_list', ) db_subnet_group_res = troposphere.rds.DBSubnetGroup( title='DBSubnetGroup', template =template, DBSubnetGroupDescription=troposphere.Ref('AWS::StackName'), SubnetIds=troposphere.Ref(db_subnet_id_list_param), ) # DB Parameter Group engine_major_version = None if rds_config.parameter_group == None: # No Parameter Group supplied, create one engine_major_version = '.'.join(rds_config.engine_version.split('.')[0:2]) param_group_family = gen_vocabulary.rds_engine_versions[rds_config.engine][rds_config.engine_version]['param_group_family'] dbparametergroup_ref = troposphere.rds.DBParameterGroup( "DBParameterGroup", template = template, Family=param_group_family, Description=troposphere.Ref('AWS::StackName') ) else: # Use an existing Parameter Group dbparametergroup_ref = self.create_cfn_parameter( name='DBParameterGroupName', param_type='String', description='DB Parameter Group Name', value=rds_config.parameter_group + '.name', ) # Option Group option_group_res = None if len(rds_config.option_configurations) > 0 or (hasattr(rds_config, 'backup_restore_bucket') and rds_config.backup_restore_bucket != None): option_group_dict = { 'EngineName': rds_config.engine, 'MajorEngineVersion': engine_major_version, 'OptionGroupDescription': troposphere.Ref('AWS::StackName') } option_config_list = [] if len(rds_config.option_configurations) > 0: for option_config in rds_config.option_configurations: option_config_dict = { 'OptionName': option_config.option_name, } if len(option_config.option_settings) > 0: option_config_dict['OptionSettings'] = [] idx = 0 for option_setting in option_config.option_settings: option_value = option_setting.value if references.is_ref(option_setting.value): # Use an existing Parameter Group option_setting_value_param = self.create_cfn_parameter( name=f'OptionsGroupValue{idx}', param_type='String', description=f'DB Option Settings Value {idx}', value=option_setting.value ) option_value = troposphere.Ref(option_setting_value_param) option_setting_dict = { 'Name': option_setting.name, 'Value': option_value } option_config_dict['OptionSettings'].append(option_setting_dict) option_config_list.append(option_config_dict) if hasattr(rds_config, 'backup_restore_bucket') and rds_config.backup_restore_bucket != None: option_config_dict = { 'OptionName': 'SQLSERVER_BACKUP_RESTORE', 'OptionSettings': [] } # S3 Bucket Arn Param backup_restore_bucket_arn_param = self.create_cfn_parameter( name='SQLServerBackupRestoreBucketArn', param_type='String', description=f'DB Option Setting SQLServer Backup Restore Bucket ARN', value=f'{rds_config.backup_restore_bucket}.arn' ) # Create Role for SQLServer Bucket sqlserver_backup_restore_role = troposphere.iam.Role( title='SQLServerBackupRestoreRole', template=self.template, AssumeRolePolicyDocument=PolicyDocument( Statement=[ Statement( Effect=Allow, Action=[Action("sts", "AssumeRole")], Principal=Principal("Service", "rds.amazonaws.com") ) ] ), Policies=[ troposphere.iam.Policy( PolicyName="S3BucketAccess", PolicyDocument=Policy( Version='2012-10-17', Statement=[ Statement( Effect=Allow, Action=[ awacs.s3.ListBucket, awacs.s3.GetBucketLocation ], Resource=[troposphere.Ref(backup_restore_bucket_arn_param)], ), Statement( Effect=Allow, Action=[ Action('s3', 'GetObjectMetaData'), awacs.s3.GetObject, awacs.s3.PutObject, awacs.s3.ListMultipartUploadParts, awacs.s3.AbortMultipartUpload ], Resource=[troposphere.Sub("${SQLServerBackupRestoreBucketArn}/*")] ) ] ) ) ], Path="/", ) option_value = troposphere.GetAtt(sqlserver_backup_restore_role, 'Arn') option_setting_dict = { 'Name': 'IAM_ROLE_ARN', 'Value': option_value } option_config_dict['OptionSettings'].append(option_setting_dict) option_config_list.append(option_config_dict) option_group_dict['OptionConfigurations'] = option_config_list option_group_res = troposphere.rds.OptionGroup.from_dict( 'OptionGroup', option_group_dict ) template.add_resource(option_group_res) # RDS MultiAZ (Mysql, Postgresql) sg_param_ref_list = [] for sg_ref in rds_config.security_groups: sg_hash = utils.md5sum(str_data=sg_ref) sg_param = self.create_cfn_parameter( param_type='AWS::EC2::SecurityGroup::Id', name=self.create_cfn_logical_id('SecurityGroup'+sg_hash), description='VPC Security Group to attach to the RDS.', value=sg_ref+'.id', ) sg_param_ref_list.append(troposphere.Ref(sg_param)) db_instance_dict = { 'Engine': rds_config.engine, 'EngineVersion': rds_config.engine_version, 'DBInstanceIdentifier': troposphere.Ref('AWS::StackName'), 'DBInstanceClass': rds_config.db_instance_type, 'DBSubnetGroupName': troposphere.Ref(db_subnet_group_res), 'DBParameterGroupName': troposphere.Ref(dbparametergroup_ref), 'CopyTagsToSnapshot': True, 'AllowMajorVersionUpgrade': rds_config.allow_major_version_upgrade, 'AutoMinorVersionUpgrade': rds_config.auto_minor_version_upgrade, 'AllocatedStorage': rds_config.storage_size_gb, 'StorageType': rds_config.storage_type, 'BackupRetentionPeriod': rds_config.backup_retention_period, 'Port': rds_config.port, 'PreferredBackupWindow': rds_config.backup_preferred_window, 'PreferredMaintenanceWindow': rds_config.maintenance_preferred_window, 'VPCSecurityGroups': sg_param_ref_list } if schemas.IRDSMultiAZ.providedBy(rds_config): db_instance_dict['MultiAZ'] = rds_config.multi_az # License Model if rds_config.license_model: db_instance_dict['LicenseModel'] = rds_config.license_model # Deletion Protection if rds_config.deletion_protection: db_instance_dict['DeletionProtection'] = rds_config.deletion_protection # CloudWatch Logs Exports if len(rds_config.cloudwatch_logs_exports) > 0: db_instance_dict['EnableCloudwatchLogsExports'] = rds_config.cloudwatch_logs_exports # Option Group if option_group_res != None: db_instance_dict['OptionGroupName'] = troposphere.Ref(option_group_res) # DB Snapshot Identifier if rds_config.db_snapshot_identifier == '' or rds_config.db_snapshot_identifier == None: db_snapshot_id_enabled = False else: db_snapshot_id_enabled = True if db_snapshot_id_enabled == True: db_instance_dict['DBSnapshotIdentifier'] = rds_config.db_snapshot_identifier # To restore an existing DB from a Snapshot, RDS will need to replace the RDS # resource, in which case the DBInstanceIdentifier name CAN NOT be set # del db_instance_dict['DBInstanceIdentifier'] # Username and Passsword if db_snapshot_id_enabled == False: # Encryption if rds_config.storage_encrypted == True: db_instance_dict['StorageEncrypted'] = True if rds_config.kms_key_id and rds_config.kms_key_id != '': db_instance_dict['KmsKeyId'] = rds_config.kms_key_id # Username & Password db_instance_dict['MasterUsername'] = rds_config.master_username if rds_config.secrets_password: # Password from Secrets Manager sta_logical_id = 'SecretTargetAttachmentRDS' secret_arn_param = self.create_cfn_parameter( param_type='String', name='RDSSecretARN', description='The ARN for the secret for the RDS master password.', value=rds_config.secrets_password + '.arn', ) secret_target_attachment_resource = troposphere.secretsmanager.SecretTargetAttachment( title=sta_logical_id, SecretId=troposphere.Ref(secret_arn_param), TargetId=troposphere.Ref(rds_logical_id), TargetType='AWS::RDS::DBInstance' ) template.add_resource(secret_target_attachment_resource) db_instance_dict['MasterUserPassword'] = troposphere.Join( '', ['{{resolve:secretsmanager:', troposphere.Ref(secret_arn_param), ':SecretString:password}}' ] ) else: master_password_param = self.create_cfn_parameter( param_type='String', name='MasterUserPassword', description='The master user password.', value=rds_config.master_user_password, noecho=True, ) db_instance_dict['MasterUserPassword'] = troposphere.Ref(master_password_param) db_instance_res = troposphere.rds.DBInstance.from_dict( rds_logical_id, db_instance_dict ) template.add_resource(db_instance_res) # Outputs self.create_output( title='DBInstanceName', description='DB Instance Name', value=troposphere.Ref(db_instance_res), ref=config_ref + ".name", ) self.create_output( title='RDSEndpointAddress', description='RDS Endpoint URL', value=troposphere.GetAtt(db_instance_res, 'Endpoint.Address'), ref=config_ref + ".endpoint.address", ) # Legacy Route53 Record Set if self.paco_ctx.legacy_flag('route53_record_set_2019_10_16') == True: if rds_config.is_dns_enabled() == True: for dns_config in rds_config.dns_config: dns_hash = utils.md5sum(str_data=(rds_config.hosted_zone+rds_config.domain_name)) primary_hosted_zone_id_param = self.create_cfn_parameter( param_type='String', name='DNSHostedZoneId'+dns_hash, description='The hosted zone id to create the Route53 record set.', value=rds_config.primary_hosted_zone+'.id', ) record_set_res = troposphere.route53.RecordSetType( title = 'RecordSet'+dns_hash, template = template, Comment = 'RDS Primary DNS', HostedZoneId = troposphere.Ref(primary_hosted_zone_id_param), Name = rds_config.primary_domain_name, Type = 'CNAME', TTL = dns_config.ttl, ResourceRecords = [ troposphere.GetAtt(db_instance_res, 'Endpoint.Address')] ) record_set_res.DependsOn = db_instance_res # DNS - Route53 Record Set if self.paco_ctx.legacy_flag('route53_record_set_2019_10_16') == False: if rds_config.is_dns_enabled() == True: route53_ctl = self.paco_ctx.get_controller('route53') for dns_config in rds_config.dns: route53_ctl.add_record_set( self.account_ctx, self.aws_region, rds_config, enabled=rds_config.is_enabled(), dns=dns_config, record_set_type='CNAME', resource_records=['paco.ref ' + config_ref + '.endpoint.address'], stack_group=self.stack.stack_group, async_stack_provision=True, config_ref=rds_config.paco_ref_parts + '.dns' )
def __init__( self, stack, paco_ctx, ): super().__init__( stack, paco_ctx, iam_capabilities=["CAPABILITY_NAMED_IAM"], ) account_ctx = stack.account_ctx aws_region = stack.aws_region self.set_aws_name('Lambda', self.resource_group_name, self.resource_name) awslambda = self.awslambda = self.stack.resource self.init_template('Lambda Function') # if not enabled finish with only empty placeholder if not awslambda.is_enabled(): return # Parameters sdb_cache_param = self.create_cfn_parameter( name='EnableSDBCache', param_type='String', description='Boolean indicating whether an SDB Domain will be created to be used as a cache.', value=awslambda.sdb_cache ) function_description_param = self.create_cfn_parameter( name='FunctionDescription', param_type='String', description='A description of the Lamdba Function.', value=awslambda.description ) handler_param = self.create_cfn_parameter( name='Handler', param_type='String', description='The name of the function to call upon execution.', value=awslambda.handler ) runtime_param = self.create_cfn_parameter( name='Runtime', param_type='String', description='The name of the runtime language.', value=awslambda.runtime ) role_arn_param = self.create_cfn_parameter( name='RoleArn', param_type='String', description='The execution role for the Lambda Function.', value=awslambda.iam_role.get_arn() ) role_name_param = self.create_cfn_parameter( name='RoleName', param_type='String', description='The execution role name for the Lambda Function.', value=awslambda.iam_role.resolve_ref_obj.role_name ) memory_size_param = self.create_cfn_parameter( name='MemorySize', param_type='Number', description="The amount of memory that your function has access to. Increasing the function's" + \ " memory also increases its CPU allocation. The default value is 128 MB. The value must be a multiple of 64 MB.", value=awslambda.memory_size ) reserved_conc_exec_param = self.create_cfn_parameter( name='ReservedConcurrentExecutions', param_type='Number', description='The number of simultaneous executions to reserve for the function.', value=awslambda.reserved_concurrent_executions ) timeout_param = self.create_cfn_parameter( name='Timeout', param_type='Number', description='The amount of time that Lambda allows a function to run before stopping it. ', value=awslambda.timeout ) layers_param = self.create_cfn_parameter( name='Layers', param_type='CommaDelimitedList', description='List of up to 5 Lambda Layer ARNs.', value=','.join(awslambda.layers) ) # create the Lambda resource cfn_export_dict = { 'Description': troposphere.Ref(function_description_param), 'Handler': troposphere.Ref(handler_param), 'MemorySize': troposphere.Ref(memory_size_param), 'Runtime': troposphere.Ref(runtime_param), 'Role': troposphere.Ref(role_arn_param), 'Timeout': troposphere.Ref(timeout_param), } if awslambda.reserved_concurrent_executions: cfn_export_dict['ReservedConcurrentExecutions'] = troposphere.Ref(reserved_conc_exec_param), if len(awslambda.layers) > 0: cfn_export_dict['Layers'] = troposphere.Ref(layers_param), # Lambda VPC if awslambda.vpc_config != None: vpc_security_group = self.create_cfn_ref_list_param( name='VpcSecurityGroupIdList', param_type='List<AWS::EC2::SecurityGroup::Id>', description='VPC Security Group Id List', value=awslambda.vpc_config.security_groups, ref_attribute='id', ) # Segment SubnetList is a Segment stack Output based on availability zones segment_ref = awslambda.vpc_config.segments[0] + '.subnet_id_list' subnet_list_param = self.create_cfn_parameter( name='VpcSubnetIdList', param_type='List<AWS::EC2::Subnet::Id>', description='VPC Subnet Id List', value=segment_ref ) cfn_export_dict['VpcConfig'] = { 'SecurityGroupIds': troposphere.Ref(vpc_security_group), 'SubnetIds': troposphere.Ref(subnet_list_param), } # Code object: S3 Bucket, inline ZipFile or deploy artifact? if awslambda.code.s3_bucket: if awslambda.code.s3_bucket.startswith('paco.ref '): value = awslambda.code.s3_bucket + ".name" else: value = awslambda.code.s3_bucket s3bucket_param = self.create_cfn_parameter( name='CodeS3Bucket', description="An Amazon S3 bucket in the same AWS Region as your function. The bucket can be in a different AWS account.", param_type='String', value=value ) s3key_param = self.create_cfn_parameter( name='CodeS3Key', description="The Amazon S3 key of the deployment package.", param_type='String', value=awslambda.code.s3_key ) cfn_export_dict['Code'] = { 'S3Bucket': troposphere.Ref(s3bucket_param), 'S3Key': troposphere.Ref(s3key_param), } else: zip_path = Path(awslambda.code.zipfile) if zip_path.is_file(): cfn_export_dict['Code'] = { 'ZipFile': zip_path.read_text() } elif zip_path.is_dir(): # get S3Bucket/S3Key or if it does not exist, it will create the bucket and artifact # and then upload the artifact bucket_name, artifact_name = init_lambda_code( self.paco_ctx.paco_buckets, self.stack.resource, awslambda.code.zipfile, self.stack.account_ctx, self.stack.aws_region, ) s3bucket_param = self.create_cfn_parameter( name='CodeS3Bucket', description="The Paco S3 Bucket for configuration", param_type='String', value=bucket_name ) s3key_param = self.create_cfn_parameter( name='CodeS3Key', description="The Lambda code artifact S3 Key.", param_type='String', value=artifact_name ) cfn_export_dict['Code'] = { 'S3Bucket': troposphere.Ref(s3bucket_param), 'S3Key': troposphere.Ref(s3key_param), } # Environment variables var_export = {} if awslambda.environment != None and awslambda.environment.variables != None: for var in awslambda.environment.variables: name = var.key.replace('_','') env_param = self.create_cfn_parameter( name='EnvVar{}'.format(name), param_type='String', description='Env var for {}'.format(name), value=var.value, ) var_export[var.key] = troposphere.Ref(env_param) if awslambda.sdb_cache == True: var_export['SDB_CACHE_DOMAIN'] = troposphere.Ref('LambdaSDBCacheDomain') if len(awslambda.log_group_names) > 0: # Add PACO_LOG_GROUPS Environment Variable paco_log_groups = [ prefixed_name(awslambda, loggroup_name, self.paco_ctx.legacy_flag) for loggroup_name in awslambda.log_group_names ] paco_log_groups_param = self.create_cfn_parameter( name='EnvVariablePacoLogGroups', param_type='String', description='Env var for Paco Log Groups', value=','.join(paco_log_groups), ) var_export['PACO_LOG_GROUPS'] = troposphere.Ref(paco_log_groups_param) cfn_export_dict['Environment'] = { 'Variables': var_export } # Lambda resource self.awslambda_resource = troposphere.awslambda.Function.from_dict( 'Function', cfn_export_dict ) self.template.add_resource(self.awslambda_resource) # SDB Cache with SDB Domain and SDB Domain Policy resources if awslambda.sdb_cache == True: sdb_domain_resource = troposphere.sdb.Domain( title='LambdaSDBCacheDomain', template=self.template, Description="Lambda Function Domain" ) sdb_policy = troposphere.iam.Policy( title='LambdaSDBCacheDomainPolicy', template=self.template, PolicyName='SDBDomain', PolicyDocument=Policy( Version='2012-10-17', Statement=[ Statement( Effect=Allow, Action=[Action("sdb","*")], Resource=[ troposphere.Sub( 'arn:aws:sdb:${AWS::Region}:${AWS::AccountId}:domain/${DomainName}', DomainName=troposphere.Ref('LambdaSDBCacheDomain') ) ], ) ], Roles=troposphere.Ref(role_arn_param) ) ) sdb_policy.DependsOn = sdb_domain_resource self.awslambda_resource.DependsOn = sdb_domain_resource # Permissions # SNS Topic Lambda permissions and subscription idx = 1 for sns_topic_ref in awslambda.sns_topics: # SNS Topic Arn parameters param_name = 'SNSTopicArn%d' % idx self.create_cfn_parameter( name=param_name, param_type='String', description='An SNS Topic ARN to grant permission to.', value=sns_topic_ref + '.arn' ) # Lambda permission troposphere.awslambda.Permission( title=param_name + 'Permission', template=self.template, Action="lambda:InvokeFunction", FunctionName=troposphere.GetAtt(self.awslambda_resource, 'Arn'), Principal='sns.amazonaws.com', SourceArn=troposphere.Ref(param_name), ) # SNS Topic subscription sns_topic = get_model_obj_from_ref(sns_topic_ref, self.paco_ctx.project) troposphere.sns.SubscriptionResource( title=param_name + 'Subscription', template=self.template, Endpoint=troposphere.GetAtt(self.awslambda_resource, 'Arn'), Protocol='lambda', TopicArn=troposphere.Ref(param_name), Region=sns_topic.region_name ) idx += 1 # Lambda permissions for connected Paco resources app = get_parent_by_interface(awslambda, schemas.IApplication) for obj in get_all_nodes(app): # S3 Bucket notification permission(s) if schemas.IS3Bucket.providedBy(obj): seen = {} if hasattr(obj, 'notifications'): if hasattr(obj.notifications, 'lambdas'): for lambda_notif in obj.notifications.lambdas: if lambda_notif.function == awslambda.paco_ref: # yes, this Lambda gets notification from this S3Bucket group = get_parent_by_interface(obj, schemas.IResourceGroup) s3_logical_name = self.gen_cf_logical_name(group.name + obj.name, '_') if s3_logical_name not in seen: troposphere.awslambda.Permission( title='S3Bucket' + s3_logical_name, template=self.template, Action="lambda:InvokeFunction", FunctionName=troposphere.GetAtt(self.awslambda_resource, 'Arn'), Principal='s3.amazonaws.com', SourceArn='arn:aws:s3:::' + obj.get_bucket_name(), ) seen[s3_logical_name] = True # Events Rule permission(s) if schemas.IEventsRule.providedBy(obj): seen = {} for target in obj.targets: target_ref = Reference(target.target) target_ref.set_account_name(account_ctx.get_name()) target_ref.set_region(aws_region) lambda_ref = Reference(awslambda.paco_ref) if target_ref.raw == lambda_ref.raw: # yes, the Events Rule has a Target that is this Lambda group = get_parent_by_interface(obj, schemas.IResourceGroup) eventsrule_logical_name = self.gen_cf_logical_name(group.name + obj.name, '_') if eventsrule_logical_name not in seen: rule_name = create_event_rule_name(obj) # rule_name = self.create_cfn_logical_id("EventsRule" + obj.paco_ref) # rule_name = hash_smaller(rule_name, 64) source_arn = 'arn:aws:events:{}:{}:rule/{}'.format( aws_region, account_ctx.id, rule_name ) troposphere.awslambda.Permission( title='EventsRule' + eventsrule_logical_name, template=self.template, Action="lambda:InvokeFunction", FunctionName=troposphere.GetAtt(self.awslambda_resource, 'Arn'), Principal='events.amazonaws.com', SourceArn=source_arn, ) seen[eventsrule_logical_name] = True # IoT Analytics permission(s) if schemas.IIoTAnalyticsPipeline.providedBy(obj): seen = {} for activity in obj.pipeline_activities.values(): if activity.activity_type == 'lambda': target_ref = Reference(activity.function) target_ref.set_account_name(account_ctx.get_name()) target_ref.set_region(aws_region) lambda_ref = Reference(awslambda.paco_ref) if target_ref.raw == lambda_ref.raw: # yes, the IoT Analytics Lambda Activity has a ref to this Lambda group = get_parent_by_interface(obj, schemas.IResourceGroup) iotap_logical_name = self.gen_cf_logical_name(group.name + obj.name, '_') if iotap_logical_name not in seen: rule_name = create_event_rule_name(obj) troposphere.awslambda.Permission( title='IoTAnalyticsPipeline' + iotap_logical_name, template=self.template, Action="lambda:InvokeFunction", FunctionName=troposphere.GetAtt(self.awslambda_resource, 'Arn'), Principal='iotanalytics.amazonaws.com', ) seen[iotap_logical_name] = True # Log group(s) loggroup_function_name = troposphere.Join( '', [ '/aws/lambda/', troposphere.Select( 6, troposphere.Split(':', troposphere.GetAtt(self.awslambda_resource, 'Arn')) ) ] ) loggroup_resources = [] loggroup_resources.append( self.add_log_group(loggroup_function_name, 'lambda') ) if len(awslambda.log_group_names) > 0: # Additional App-specific LogGroups for loggroup_name in awslambda.log_group_names: # Add LogGroup to the template prefixed_loggroup_name = prefixed_name(awslambda, loggroup_name, self.paco_ctx.legacy_flag) loggroup_resources.append( self.add_log_group(prefixed_loggroup_name) ) # LogGroup permissions log_group_arns = [ troposphere.Join(':', [ f'arn:aws:logs:{self.aws_region}:{account_ctx.id}:log-group', loggroup_function_name, '*' ]) ] log_stream_arns = [ troposphere.Join(':', [ f'arn:aws:logs:{self.aws_region}:{account_ctx.id}:log-group', loggroup_function_name, 'log-stream', '*' ]) ] for loggroup_name in awslambda.log_group_names: prefixed_loggroup_name = prefixed_name(awslambda, loggroup_name, self.paco_ctx.legacy_flag) log_group_arns.append( f'arn:aws:logs:{self.aws_region}:{account_ctx.id}:log-group:{prefixed_loggroup_name}:*' ) log_stream_arns.append( f'arn:aws:logs:{self.aws_region}:{account_ctx.id}:log-group:{prefixed_loggroup_name}:log-stream:*' ) loggroup_policy_resource = troposphere.iam.ManagedPolicy( title='LogGroupManagedPolicy', PolicyDocument=Policy( Version='2012-10-17', Statement=[ Statement( Sid='AllowLambdaModifyLogStreams', Effect=Allow, Action=[ Action("logs","CreateLogStream"), Action("logs","DescribeLogStreams"), ], Resource=log_group_arns, ), Statement( Sid='AllowLambdaPutLogEvents', Effect=Allow, Action=[ Action("logs","PutLogEvents"), ], Resource=log_stream_arns, ), ], ), Roles=[troposphere.Ref(role_name_param)], ) loggroup_policy_resource.DependsOn = loggroup_resources self.template.add_resource(loggroup_policy_resource) # Outputs self.create_output( title='FunctionName', value=troposphere.Ref(self.awslambda_resource), ref=awslambda.paco_ref_parts + '.name', ) self.create_output( title='FunctionArn', value=troposphere.GetAtt(self.awslambda_resource, 'Arn'), ref=awslambda.paco_ref_parts + '.arn', )
def register_resources_template(self, template): deployment_resources = [] api = RestApi( self.in_project_cf_name, Name=troposphere.Join("-", [self.name, troposphere.Ref('Stage')]), Description=self.settings.get('description', '') ) template.add_resource(api) deployment_resources.append(api) invoke_lambda_role = troposphere.iam.Role( utils.valid_cloudformation_name(self.name, 'Role'), AssumeRolePolicyDocument={ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Service": ["apigateway.amazonaws.com"] }, "Action": ["sts:AssumeRole"] }] }, Policies=[ troposphere.iam.Policy( PolicyName=utils.valid_cloudformation_name(self.name, 'Role', 'Policy'), PolicyDocument={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": [ "*" ] } ] } ) ] ) template.add_resource(invoke_lambda_role) deployment_resources.append(invoke_lambda_role) deployment_dependencies = [] for path, resource in six.iteritems(self.settings.get('resources', {})): resource_reference = self.get_or_create_resource(path, api, template) methods = resource['methods'] if isinstance(methods, six.string_types): methods = [methods] if not isinstance(methods, dict): method_properties = copy.deepcopy(resource) method_properties.pop('methods', None) methods = dict([[method, method_properties] for method in methods]) for method, configuration in six.iteritems(methods): method_name = [self.name] method_name.extend(path.split('/')) method_name.append(method) extra = {} if 'parameters' in configuration: extra['RequestParameters'] = configuration['parameters'] m = Method( utils.valid_cloudformation_name(*method_name), HttpMethod=method, AuthorizationType=self.get_authorization_type(configuration), ApiKeyRequired=self.get_api_key_required(configuration), Integration=self.get_integration(configuration, invoke_lambda_role), MethodResponses=self.get_method_responses(configuration), ResourceId=resource_reference, RestApiId=troposphere.Ref(api), **extra ) template.add_resource(m) deployment_dependencies.append(m.name) deployment_resources.append(m) deploy_hash = hashlib.sha1(six.text_type(uuid.uuid4()).encode('utf-8')).hexdigest() deploy = Deployment( utils.valid_cloudformation_name(self.name, "Deployment", deploy_hash[:8]), DependsOn=sorted(deployment_dependencies), StageName=troposphere.Ref('Stage'), RestApiId=troposphere.Ref(api) ) template.add_resource(deploy) if self._get_true_false('cli-output', 't'): template.add_output([ troposphere.Output( utils.valid_cloudformation_name("Clioutput", self.in_project_name), Value=troposphere.Join( "", [ "https://", troposphere.Ref(api), ".execute-api.", troposphere.Ref(troposphere.AWS_REGION), ".amazonaws.com/", troposphere.Ref('Stage') ] ), ) ])