def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # Create DynamoDB ddb_table = _ddb.Table(self, id='sa-table-cms', table_name='sa_table_cms', partition_key=_ddb.Attribute( name='ID', type=_ddb.AttributeType.STRING)) # Permissions lambda_role = _iam.Role( self, id='sa-cms-role', assumed_by=_iam.ServicePrincipal('lambda.amazonaws.com')) # WARNING: This is an example on how to include AWS managed policy. Don't use this for production. Please use the example of policy document for DynamoDB instead. lambda_role.add_managed_policy( _iam.ManagedPolicy.from_aws_managed_policy_name( 'CloudWatchLogsFullAccess')) policy_statement = _iam.PolicyStatement(effect=_iam.Effect.ALLOW) policy_statement.add_actions("dynamodb:*") policy_statement.add_resources(ddb_table.table_arn) lambda_role.add_to_policy(policy_statement) # Create Lambda Functions fn_lambda_create_post = _lambda.Function( self, "sa-cms-createPost", code=_lambda.AssetCode("../sa-lambda/create-post/"), handler="app.lambda_handler", tracing=_lambda.Tracing.ACTIVE, timeout=core.Duration.seconds(30), role=lambda_role, runtime=_lambda.Runtime.PYTHON_3_8) fn_lambda_list_post = _lambda.Function( self, "sa-cms-listPost", code=_lambda.AssetCode("../sa-lambda/list-post/"), handler="app.lambda_handler", tracing=_lambda.Tracing.ACTIVE, timeout=core.Duration.seconds(30), role=lambda_role, runtime=_lambda.Runtime.PYTHON_3_8) fn_lambda_get_post = _lambda.Function( self, "sa-cms-getPost", code=_lambda.AssetCode("../sa-lambda/get-post/"), handler="app.lambda_handler", tracing=_lambda.Tracing.ACTIVE, timeout=core.Duration.seconds(30), role=lambda_role, runtime=_lambda.Runtime.PYTHON_3_8) fn_lambda_delete_post = _lambda.Function( self, "sa-cms-deletePost", code=_lambda.AssetCode("../sa-lambda/delete-post/"), handler="app.lambda_handler", timeout=core.Duration.seconds(30), tracing=_lambda.Tracing.ACTIVE, role=lambda_role, runtime=_lambda.Runtime.PYTHON_3_8) fn_lambda_create_post.add_environment("DYNAMODB_TABLE", ddb_table.table_name) fn_lambda_get_post.add_environment("DYNAMODB_TABLE", ddb_table.table_name) fn_lambda_list_post.add_environment("DYNAMODB_TABLE", ddb_table.table_name) fn_lambda_delete_post.add_environment("DYNAMODB_TABLE", ddb_table.table_name) api = _ag.RestApi( self, id='sa-api-gateway', # default_cors_preflight_options=_ag.CorsOptions( # allow_methods=['ANY'], # allow_origins=['*'], # allow_headers=['Access-Control-Allow-Origin','Access-Control-Allow-Headers','Content-Type'] # ) ) posts_resource = api.root.add_resource('posts') posts_with_id_resource = posts_resource.add_resource("{id}") posts_resource.add_method('POST', _ag.LambdaIntegration(fn_lambda_create_post)) posts_with_id_resource.add_method( 'GET', _ag.LambdaIntegration(fn_lambda_get_post)) posts_resource.add_method('GET', _ag.LambdaIntegration(fn_lambda_list_post)) posts_with_id_resource.add_method( 'DELETE', _ag.LambdaIntegration(fn_lambda_delete_post))
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) alexa_assets = os.path.dirname( os.path.realpath(__file__)) + "/../skill" asset = assets.Asset(self, 'SkillAsset', path=alexa_assets) # role to access bucket role = Role(self, 'Role', assumed_by=CompositePrincipal( ServicePrincipal('alexa-appkit.amazon.com'), ServicePrincipal('cloudformation.amazonaws.com'))) # Allow the skill resource to access the zipped skill package role.add_to_policy( PolicyStatement( actions=['S3:GetObject'], resources=[ f'arn:aws:s3:::{asset.s3_bucket_name}/{asset.s3_object_key}' ])) # DynamoDB Table users_table = dynamo_db.Table( self, 'Users', partition_key=dynamo_db.Attribute( name='userId', type=dynamo_db.AttributeType.STRING), billing_mode=dynamo_db.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY) # install node dependencies for lambdas lambda_folder = os.path.dirname( os.path.realpath(__file__)) + "/../lambda_fns" subprocess.check_call("npm i".split(), cwd=lambda_folder) subprocess.check_call("npm run build".split(), cwd=lambda_folder) alexa_lambda = _lambda.Function( self, "AlexaLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, code=_lambda.Code.from_asset("lambda_fns"), handler="lambda.handler", environment={"USERS_TABLE": users_table.table_name}) # grant the lambda role read/write permissions to our table users_table.grant_read_write_data(alexa_lambda) # create the skill skill = alexa_ask.CfnSkill( self, 'the-alexa-skill', vendor_id='', authentication_configuration={ 'clientId': '', 'clientSecret': '', 'refreshToken': '' }, skill_package={ 's3Bucket': asset.s3_bucket_name, 's3Key': asset.s3_object_key, 's3BucketRole': role.role_arn, 'overrides': { 'manifest': { 'apis': { 'custom': { 'endpoint': { 'uri': alexa_lambda.function_arn } } } } } }) ### # Allow the Alexa service to invoke the fulfillment Lambda. # In order for the Skill to be created, the fulfillment Lambda # must have a permission allowing Alexa to invoke it, this causes # a circular dependency and requires the first deploy to allow all # Alexa skills to invoke the lambda, subsequent deploys will work # when specifying the eventSourceToken ### alexa_lambda.add_permission( 'AlexaPermission', # eventSourceToken: skill.ref, principal=ServicePrincipal('alexa-appkit.amazon.com'), action='lambda:InvokeFunction')
def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # ugly, but we need to read user_pool_id from amplify's aws-exports.js file with open('../aws-exports.js') as dataFile: data = dataFile.read() obj = data[data.find('{'):data.rfind('}') + 1] aws_exports = json.loads(obj) user_pool_id = aws_exports["aws_user_pools_id"] # use that user_pool_id to define a cdk struct representing it user_pool = aws_cognito.UserPool.from_user_pool_id( self, 'amplify_user_pool', user_pool_id) api = aws_appsync.GraphqlApi( self, 'ring-it-up-api', name='ring-it-up-api', schema=aws_appsync.Schema.from_asset('graphql/schema.graphql'), authorization_config=aws_appsync.AuthorizationConfig( default_authorization=aws_appsync.AuthorizationMode( authorization_type=aws_appsync.AuthorizationType.API_KEY, api_key_config=aws_appsync.ApiKeyConfig( expires=core.Expiration.after(core.Duration.days( 365)))), additional_authorization_modes=[ aws_appsync.AuthorizationMode( authorization_type=aws_appsync.AuthorizationType. USER_POOL, user_pool_config=aws_appsync.UserPoolConfig( user_pool=user_pool)) ])) # Prints out the AppSync GraphQL API URL to the ternminal core.CfnOutput(self, "aws_appsync_graphqlEndpoint", value=api.graphql_url) # Prints out the AppSync GraphQL API key to the terminal core.CfnOutput(self, "aws_appsync_apiKey", value=api.api_key) # Prints out the base authentication type for API core.CfnOutput(self, "aws_appsync_authenticationType", value=str(aws_appsync.AuthorizationType.API_KEY)) api_lambda = aws_lambda.Function( self, 'AppSyncBlogHandler', runtime=aws_lambda.Runtime.NODEJS_12_X, handler='main.handler', code=aws_lambda.Code.from_asset('lambda-fns'), memory_size=1024) # Set the new Lambda function as a data source for the AppSync API lambda_datasource = api.add_lambda_data_source('lambdaDatasource', api_lambda) lambda_datasource.create_resolver(type_name="Query", field_name="getActivityById") lambda_datasource.create_resolver(type_name="Query", field_name="listActivities") lambda_datasource.create_resolver(type_name="Mutation", field_name="createActivity") lambda_datasource.create_resolver(type_name="Mutation", field_name="deleteActivity") lambda_datasource.create_resolver(type_name="Mutation", field_name="updateActivity") activity_table = aws_dynamodb.Table( self, 'CDKPostTable', removal_policy=core.RemovalPolicy.DESTROY, billing_mode=aws_dynamodb.BillingMode.PAY_PER_REQUEST, partition_key={ "name": 'id', "type": aws_dynamodb.AttributeType.STRING, }) # enable the Lambda function to access the DynamoDB table (using IAM) activity_table.grant_full_access(api_lambda) # Create an environment variable that we will use in the function code api_lambda.add_environment('ACTIVITY_TABLE', activity_table.table_name)
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) ################################################################################ # VPC vpc = ec2.Vpc(self, "Monitoring VPC", max_azs=3) ################################################################################ # Amazon OpenSearch Service domain es_sec_grp = ec2.SecurityGroup( self, 'OpenSearchSecGrpMonitoring', vpc=vpc, allow_all_outbound=True, security_group_name='OpenSearchSecGrpMonitoring') es_sec_grp.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(80)) es_sec_grp.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(443)) domain = opensearch.Domain( self, 'opensearch-service-monitor', version=opensearch.EngineVersion. OPENSEARCH_1_0, # Upgrade when CDK upgrades domain_name=DOMAIN_NAME, removal_policy=core.RemovalPolicy.DESTROY, capacity=opensearch.CapacityConfig( data_node_instance_type=DOMAIN_DATA_NODE_INSTANCE_TYPE, data_nodes=DOMAIN_DATA_NODE_INSTANCE_COUNT, master_node_instance_type=DOMAIN_MASTER_NODE_INSTANCE_TYPE, master_nodes=DOMAIN_MASTER_NODE_INSTANCE_COUNT, warm_instance_type=DOMAIN_UW_NODE_INSTANCE_TYPE, warm_nodes=DOMAIN_UW_NODE_INSTANCE_COUNT), ebs=opensearch.EbsOptions(enabled=True, volume_size=DOMAIN_INSTANCE_VOLUME_SIZE, volume_type=ec2.EbsDeviceVolumeType.GP2), vpc=vpc, vpc_subnets=[ec2.SubnetType.PUBLIC], security_groups=[es_sec_grp], zone_awareness=opensearch.ZoneAwarenessConfig( enabled=True, availability_zone_count=DOMAIN_AZ_COUNT), enforce_https=True, node_to_node_encryption=True, encryption_at_rest={"enabled": True}, use_unsigned_basic_auth=True, fine_grained_access_control={ "master_user_name": DOMAIN_ADMIN_UNAME, "master_user_password": core.SecretValue.plain_text(DOMAIN_ADMIN_PW) }) core.CfnOutput( self, "MasterUser", value=DOMAIN_ADMIN_UNAME, description="Master User Name for Amazon OpenSearch Service") core.CfnOutput( self, "MasterPW", value=DOMAIN_ADMIN_PW, description="Master User Password for Amazon OpenSearch Service") ################################################################################ # Dynamo DB table for time stamp tracking table = ddb.Table( self, 'opensearch-monitor-lambda-timestamp', table_name=TABLE_NAME, partition_key=ddb.Attribute(name="domain", type=ddb.AttributeType.STRING), sort_key=ddb.Attribute(name='region', type=ddb.AttributeType.STRING), removal_policy=core.RemovalPolicy.DESTROY) ################################################################################ # Lambda monitoring function lambda_func = lambda_.Function( self, 'CWMetricsToOpenSearch', function_name="CWMetricsToOpenSearch_monitoring", runtime=lambda_.Runtime.PYTHON_3_8, code=lambda_.Code.asset('CWMetricsToOpenSearch'), handler='handler.handler', memory_size=1024, timeout=core.Duration.minutes(10), vpc=vpc) table.grant_read_data(lambda_func) table.grant_write_data(lambda_func) lambda_func.add_environment('TABLE', table.table_name) lambda_func.add_environment('DOMAIN_ENDPOINT', 'https://' + domain.domain_endpoint) lambda_func.add_environment('DOMAIN_ADMIN_UNAME', DOMAIN_ADMIN_UNAME) lambda_func.add_environment('DOMAIN_ADMIN_PW', DOMAIN_ADMIN_PW) lambda_func.add_environment('REGIONS', REGIONS_TO_MONITOR) # When the domain is created here, restrict access lambda_func.add_to_role_policy( iam.PolicyStatement(actions=['es:*'], resources=['*'])) # The function needs to read CW events. Restrict lambda_func.add_to_role_policy( iam.PolicyStatement(actions=['cloudwatch:*'], resources=['*'])) lambda_schedule = events.Schedule.rate( core.Duration.seconds(LAMBDA_INTERVAL)) event_lambda_target = targets.LambdaFunction(handler=lambda_func) events.Rule(self, "Monitoring", enabled=True, schedule=lambda_schedule, targets=[event_lambda_target]) ################################################################################ # Lambda for CW Logs lambda_func_cw_logs = lambda_.Function( self, 'CWLogsToOpenSearch', function_name="CWLogsToOpenSearch_monitoring", runtime=lambda_.Runtime.NODEJS_12_X, code=lambda_.Code.asset('CWLogsToOpenSearch'), handler='index.handler', vpc=vpc) # # Load Amazon OpenSearch Service Domain to env variable lambda_func_cw_logs.add_environment('DOMAIN_ENDPOINT', domain.domain_endpoint) # # When the domain is created here, restrict access lambda_func_cw_logs.add_to_role_policy( iam.PolicyStatement(actions=['es:*'], resources=['*'])) # # The function needs to read CW Logs. Restrict lambda_func_cw_logs.add_to_role_policy( iam.PolicyStatement(actions=['logs:*'], resources=['*'])) # Add permission to create CW logs trigger for all specified region and current account, as region does not have an option to be wildcard account_id = boto3.client("sts").get_caller_identity()["Account"] for region in json.loads(REGIONS_TO_MONITOR): lambda_func_cw_logs.add_permission( id="lambda-cw-logs-permission-" + region, principal=iam.ServicePrincipal("logs.amazonaws.com"), action="lambda:InvokeFunction", source_arn="arn:aws:logs:" + region + ":" + account_id + ":*:*:*") ################################################################################ # Jump host for SSH tunneling and direct access sn_public = ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC) amzn_linux = ec2.MachineImage.latest_amazon_linux( generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, edition=ec2.AmazonLinuxEdition.STANDARD, virtualization=ec2.AmazonLinuxVirt.HVM, storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE) # Instance Role and SSM Managed Policy role = iam.Role(self, "InstanceSSM", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com")) role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name( "service-role/AmazonEC2RoleforSSM")) role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonSSMManagedInstanceCore")) instance = ec2.Instance( self, 'instance', instance_type=ec2.InstanceType(EC2_INSTANCE_TYPE), vpc=vpc, machine_image=amzn_linux, vpc_subnets=sn_public, key_name=EC2_KEY_NAME, role=role, ) instance.connections.allow_from_any_ipv4(ec2.Port.tcp(22), 'SSH') instance.connections.allow_from_any_ipv4(ec2.Port.tcp(443), 'HTTPS') stmt = iam.PolicyStatement(actions=['es:*'], resources=[domain.domain_arn]) instance.add_to_role_policy(stmt) # Create SNS topic, subscription, IAM roles, Policies sns_topic = sns.Topic(self, "cdk_monitoring_topic") sns_topic.add_subscription( subscriptions.EmailSubscription(SNS_NOTIFICATION_EMAIL)) sns_policy_statement = iam.PolicyStatement( actions=["sns:publish"], resources=[sns_topic.topic_arn], effect=iam.Effect.ALLOW) sns_policy = iam.ManagedPolicy(self, "cdk_monitoring_policy") sns_policy.add_statements(sns_policy_statement) sns_role = iam.Role( self, "cdk_monitoring_sns_role", assumed_by=iam.ServicePrincipal("es.amazonaws.com")) sns_role.add_managed_policy(sns_policy) dirname = os.path.dirname(__file__) dashboards_asset = Asset( self, "DashboardsAsset", path=os.path.join(dirname, 'export_opensearch_dashboards_V1_0.ndjson')) dashboards_asset.grant_read(instance.role) dashboards_asset_path = instance.user_data.add_s3_download_command( bucket=dashboards_asset.bucket, bucket_key=dashboards_asset.s3_object_key, ) nginx_asset = Asset(self, "NginxAsset", path=os.path.join(dirname, 'nginx_opensearch.conf')) nginx_asset.grant_read(instance.role) nginx_asset_path = instance.user_data.add_s3_download_command( bucket=nginx_asset.bucket, bucket_key=nginx_asset.s3_object_key, ) alerting_asset = Asset(self, "AlertingAsset", path=os.path.join(dirname, 'create_alerts.sh')) alerting_asset.grant_read(instance.role) alerting_asset_path = instance.user_data.add_s3_download_command( bucket=alerting_asset.bucket, bucket_key=alerting_asset.s3_object_key, ) instance.user_data.add_commands( "yum update -y", "yum install jq -y", "amazon-linux-extras install nginx1.12", "cd /tmp/assets", "mv {} export_opensearch_dashboards_V1_0.ndjson".format( dashboards_asset_path), "mv {} nginx_opensearch.conf".format(nginx_asset_path), "mv {} create_alerts.sh".format(alerting_asset_path), "openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt -subj /C=US/ST=./L=./O=./CN=.\n" "cp nginx_opensearch.conf /etc/nginx/conf.d/", "sed -i 's/DEFAULT_DOMAIN_NAME/" + DOMAIN_NAME + "/g' /tmp/assets/export_opensearch_dashboards_V1_0.ndjson", "sed -i 's/DOMAIN_ENDPOINT/" + domain.domain_endpoint + "/g' /etc/nginx/conf.d/nginx_opensearch.conf", "sed -i 's/DOMAIN_ENDPOINT/" + domain.domain_endpoint + "/g' /tmp/assets/create_alerts.sh", "sed -i 's=LAMBDA_CW_LOGS_ROLE_ARN=" + lambda_func_cw_logs.role.role_arn + "=g' /tmp/assets/create_alerts.sh", "sed -i 's=SNS_ROLE_ARN=" + sns_role.role_arn + "=g' /tmp/assets/create_alerts.sh", "sed -i 's/SNS_TOPIC_ARN/" + sns_topic.topic_arn + "/g' /tmp/assets/create_alerts.sh", "sed -i 's=DOMAIN_ADMIN_UNAME=" + DOMAIN_ADMIN_UNAME + "=g' /tmp/assets/create_alerts.sh", "sed -i 's=DOMAIN_ADMIN_PW=" + DOMAIN_ADMIN_PW + "=g' /tmp/assets/create_alerts.sh", "systemctl restart nginx.service", "chmod 500 create_alerts.sh", "sleep 5", "bash --verbose create_alerts.sh", ) core.CfnOutput(self, "Dashboards URL (via Jump host)", value="https://" + instance.instance_public_ip, description="Dashboards URL via Jump host") core.CfnOutput( self, "SNS Subscription Alert Message", value=SNS_NOTIFICATION_EMAIL, description="Please confirm your SNS subscription receievedt at")
def __init__(self, scope: core.Construct, id: str, vpc_id: str, subnet_ids, rds_secret_arn: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) vpc = ec2.Vpc.from_vpc_attributes(self, vpc_id, vpc_id=vpc_id, availability_zones= [ 'eu-west-1c'], public_subnet_ids= subnet_ids) # creating vote table for dynamodb resolver vote_table = ddb.Table(self, 'votes', table_name='votes', partition_key={ "name": "productid", "type": ddb.AttributeType.STRING }, # Sortkey structure is like : UP#20200902T12:34:00 - DOWN#20201030T10:45:12 sort_key={ "name": "votesortkey", "type": ddb.AttributeType.STRING }, read_capacity=5, write_capacity=5 ) # creating API with GraphQL schema api = appsync.GraphqlApi(self, 'example_appsync_api', name="example_appsync_api", log_config=appsync.LogConfig(field_log_level=appsync.FieldLogLevel.ALL), schema=appsync.Schema.from_asset(file_path="../appsync-conf/schema.graphql") ) # Authentication done with API key - for development purposes only appsync.CfnApiKey(self, 'examplegraphqlapi', api_id=api.api_id ) # create security group for lambda # this will need to be added to your RDS inbound lambda_security_group = ec2.SecurityGroup(self, "Example-AppSyncResolverLambdaSG", security_group_name="Example-AppSyncResolverLambdaSG", vpc=vpc, allow_all_outbound=True ) # getting the code from local directory lambda_rds_code = aws_lambda.Code.asset("../lambda-rds") lambda_rds_resolver = aws_lambda.Function(self, "LambdaAppSyncSQLResolver", function_name=f"LambdaAppSyncSQLResolver", code=lambda_rds_code, handler="index.handler", runtime=aws_lambda.Runtime.NODEJS_12_X, memory_size=512, timeout=core.Duration.seconds(60), log_retention=logs.RetentionDays.ONE_MONTH, vpc=vpc, vpc_subnets={ "subnet_type": ec2.SubnetType.PUBLIC }, allow_public_subnet=True, security_group=lambda_security_group, ) # env parameters for rds lambda to perform SQL calls lambda_rds_resolver.add_environment("SECRET_ARN", rds_secret_arn) # allow lambda to read secret lambda_rds_resolver.add_to_role_policy(iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ 'secretsmanager:GetSecretValue' ], resources=[ rds_secret_arn ] )) # adding the product datasource as lamda resolver products_ds = api.add_lambda_data_source('Products', lambda_rds_resolver) # creates resolver for query getProduct products_ds.create_resolver( type_name='Query', field_name='getProduct', request_mapping_template=appsync.MappingTemplate.from_file("../appsync-conf/vtl/getProduct.vtl"), response_mapping_template=appsync.MappingTemplate.from_file("../appsync-conf/vtl/getProduct_output_template.vtl"), ) # adding lamda resolver for vote fields in product model lambda_dynamodb_code = aws_lambda.Code.asset("../lambda-dynamodb") lambda_dynamodb_votes_resolver = aws_lambda.Function(self, "LambdaAppSyncVotesResolver", function_name=f"LambdaAppSyncVotesResolver", code=lambda_dynamodb_code, handler="index.handler", runtime=aws_lambda.Runtime.NODEJS_12_X, memory_size=512, timeout=core.Duration.seconds(60), ) # allow lambda to query dynamodb lambda_dynamodb_votes_resolver.add_to_role_policy(iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "dynamodb:GetItem", "dynamodb:Query", ], resources=[ vote_table.table_arn, vote_table.table_arn + "/*" ] )); # create lambda datasource for dynamodb queries votes_ds = api.add_lambda_data_source('Votes', lambda_dynamodb_votes_resolver) votes_ds.create_resolver( type_name='Product', field_name='ups', request_mapping_template=appsync.MappingTemplate.from_file("../appsync-conf/vtl/fields/votes_up.vtl"), response_mapping_template=appsync.MappingTemplate.from_file("../appsync-conf/vtl/fields/votes_up_output_template.vtl"), ) votes_ds.create_resolver( type_name='Product', field_name='downs', request_mapping_template=appsync.MappingTemplate.from_file("../appsync-conf/vtl/fields/votes_down.vtl"), response_mapping_template=appsync.MappingTemplate.from_file("../appsync-conf/vtl/fields/votes_down_output_template.vtl"), )
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # DynamoDB Table # This will store our error records # TTL Docs - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-how-to.html table = dynamo_db.Table(self, "CircuitBreaker", partition_key=dynamo_db.Attribute( name="RequestID", type=dynamo_db.AttributeType.STRING), sort_key=dynamo_db.Attribute( name="ExpirationTime", type=dynamo_db.AttributeType.NUMBER), time_to_live_attribute='ExpirationTime') # Add an index that lets us query on site url and Expiration Time table.add_global_secondary_index( index_name='UrlIndex', partition_key=dynamo_db.Attribute( name="SiteUrl", type=dynamo_db.AttributeType.STRING), sort_key=dynamo_db.Attribute(name="ExpirationTime", type=dynamo_db.AttributeType.NUMBER)) # defines an Integration Lambda to call our failing web service integration_lambda = _lambda.Function( self, "WebserviceIntegrationLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, handler="lambda.handler", code=_lambda.Code.from_asset("lambda_fns/webservice"), timeout=core.Duration.seconds(20), environment=dict(TABLE_NAME=table.table_name)) # grant the lambda role read/write permissions to our table table.grant_read_data(integration_lambda) # We need to give your lambda permission to put events on our EventBridge event_policy = iam.PolicyStatement(effect=iam.Effect.ALLOW, resources=['*'], actions=['events:PutEvents']) integration_lambda.add_to_role_policy(event_policy) # defines a lambda to insert errors into dynamoDB error_lambda = _lambda.Function( self, "ErrorLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, handler="lambda.handler", code=_lambda.Code.from_asset("lambda_fns/error"), timeout=core.Duration.seconds(3), environment=dict(TABLE_NAME=table.table_name)) table.grant_write_data(error_lambda) # Create EventBridge rule to route failures error_rule = events.Rule( self, 'webserviceErrorRule', description='Failed Webservice Call', event_pattern=events.EventPattern( source=['cdkpatterns.eventbridge.circuitbreaker'], detail_type=['httpcall'], detail={"status": ["fail"]})) error_rule.add_target(targets.LambdaFunction(handler=error_lambda)) # defines an API Gateway REST API resource backed by our "integration_lambda" function api_gw.LambdaRestApi(self, 'CircuitBreakerGateway', handler=integration_lambda)
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # create dynamo table template_table = aws_dynamodb.Table( self, TABLE_TEMPLATE_NAME, table_name=TABLE_TEMPLATE_NAME, billing_mode=aws_dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy. DESTROY, #开发阶段设置为 DESTROY, 正式环境设置为.RETAIN partition_key=aws_dynamodb.Attribute( name="template_type", type=aws_dynamodb.AttributeType.STRING), sort_key=aws_dynamodb.Attribute( name='create_time', type=aws_dynamodb.AttributeType.STRING)) field_table = aws_dynamodb.Table( self, TABLE_FIELD_NAME, table_name=TABLE_FIELD_NAME, billing_mode=aws_dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy. DESTROY, #开发阶段设置为 DESTROY, 正式环境设置为.RETAIN partition_key=aws_dynamodb.Attribute( name="template_id", type=aws_dynamodb.AttributeType.STRING), sort_key=aws_dynamodb.Attribute( name='business_field', type=aws_dynamodb.AttributeType.STRING)) # create producer lambda function producer_lambda = aws_lambda.Function( self, "producer_lambda_function", function_name="OcrData", runtime=aws_lambda.Runtime.PYTHON_3_6, handler="lambda_function.lambda_handler", code=aws_lambda.Code.asset("./lambda/producer")) producer_lambda.add_environment("TABLE_TEMPLATE_NAME", template_table.table_name) producer_lambda.add_environment("TABLE_FIELD_NAME", field_table.table_name) template_table.grant_read_write_data(producer_lambda) field_table.grant_read_write_data(producer_lambda) # ---------------------api-gateway GET -------------------- # integration_responses = [{ 'statusCode': '200', 'responseParameters': { 'method.response.header.Access-Control-Allow-Origin': "'*'", } }] method_responses = [{ 'statusCode': '200', 'responseParameters': { 'method.response.header.Access-Control-Allow-Origin': True, } }] producer_api = aws_apigateway.RestApi( self, 'OcrApi', endpoint_types=[aws_apigateway.EndpointType.REGIONAL], rest_api_name='OcrApi') method_post_data = "ocr" producer_entity = producer_api.root.add_resource(method_post_data) producer_entity_lambda_integration = aws_apigateway.LambdaIntegration( producer_lambda, proxy=False, integration_responses=integration_responses) producer_entity.add_method('POST', producer_entity_lambda_integration, method_responses=method_responses) core.CfnOutput(self, "OCR-Data", value=producer_api.url + method_post_data, description="ocr url") self.add_cors_options(producer_entity)
def __init__(self, scope: cdk.Stack, id: str, git_hash, sla_zip='sla-monitor.zip', **kwargs): super().__init__(scope, id, **kwargs) self.sla_zip = git_hash + "-" + sla_zip self.sla_monitor_dynamo_table = aws_dynamodb.Table( self, "DynamoTable{}".format("SLAMonitor"), table_name=self.stack_name, billing_mode=aws_dynamodb.BillingMode.PayPerRequest, stream_specification=aws_dynamodb.StreamViewType.NewImage, partition_key={ "name": "service_name", "type": aws_dynamodb.AttributeType.String }, sort_key={ "name": "last_updated_date", "type": aws_dynamodb.AttributeType.String }, ) self.sla_monitor_lambda_function = aws_lambda.Function( self, "SLAMonitorLambdaFunction", function_name=self.stack_name, code=aws_lambda.AssetCode(self.sla_zip), handler="main.lambda_handler", runtime=aws_lambda.Runtime.PYTHON37, tracing=aws_lambda.Tracing.Active, #layers=[self.dynamodb_lambda_layer], description= "Monitors AWS SLA Pages and updates DynamoDB Table when SLAs update", environment={ "STACK_NAME": self.stack_name, "LOCAL_MODE": "False", "DYNAMO_TABLE_NAME": self.stack_name }, memory_size=128, timeout=90, ) # Permissions to access sla_monitor dynamo table self.sla_monitor_dynamo_table.grant_read_write_data( self.sla_monitor_lambda_function.role) self.sla_monitor_cw_event_rule = aws_events.Rule( self, "LambdaCWEventRuleSLAMonitor", description="Scheduled event to trigger AWS SLA monitor", enabled=True, #schedule_expression='cron(0 22 */3 * ? *)', schedule_expression='cron(*/8 * * * ? *)', targets=[ aws_events_targets.LambdaFunction( handler=self.sla_monitor_lambda_function) ], )
def __init__(self, scope: core.App, name: str, **kwargs) -> None: super().__init__(scope, name, **kwargs) # <1> # dynamoDB table to store questions and answers table = dynamodb.Table( self, "EcsClusterQaBot-Table", partition_key=dynamodb.Attribute( name="item_id", type=dynamodb.AttributeType.STRING), billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=core.RemovalPolicy.DESTROY) # <2> vpc = ec2.Vpc( self, "EcsClusterQaBot-Vpc", max_azs=1, subnet_configuration=[ ec2.SubnetConfiguration( name="public", subnet_type=ec2.SubnetType.PUBLIC, ) ], nat_gateways=0, ) # <3> cluster = ecs.Cluster( self, "EcsClusterQaBot-Cluster", vpc=vpc, ) # <4> taskdef = ecs.FargateTaskDefinition( self, "EcsClusterQaBot-TaskDef", cpu=1024, # 1 CPU memory_limit_mib=4096, # 4GB RAM ) # grant permissions table.grant_read_write_data(taskdef.task_role) taskdef.add_to_task_role_policy( iam.PolicyStatement(effect=iam.Effect.ALLOW, resources=["*"], actions=["ssm:GetParameter"])) # <5> container = taskdef.add_container( "EcsClusterQaBot-Container", image=ecs.ContainerImage.from_registry( "registry.gitlab.com/tomomano/intro-aws/handson03:latest"), logging=ecs.LogDrivers.aws_logs( stream_prefix="EcsClusterQaBot", log_retention=aws_logs.RetentionDays.ONE_DAY), ) # Store parameters in SSM ssm.StringParameter( self, "ECS_CLUSTER_NAME", parameter_name="ECS_CLUSTER_NAME", string_value=cluster.cluster_name, ) ssm.StringParameter(self, "ECS_TASK_DEFINITION_ARN", parameter_name="ECS_TASK_DEFINITION_ARN", string_value=taskdef.task_definition_arn) ssm.StringParameter(self, "ECS_TASK_VPC_SUBNET_1", parameter_name="ECS_TASK_VPC_SUBNET_1", string_value=vpc.public_subnets[0].subnet_id) ssm.StringParameter(self, "CONTAINER_NAME", parameter_name="CONTAINER_NAME", string_value=container.container_name) ssm.StringParameter(self, "TABLE_NAME", parameter_name="TABLE_NAME", string_value=table.table_name) core.CfnOutput(self, "ClusterName", value=cluster.cluster_name) core.CfnOutput(self, "TaskDefinitionArn", value=taskdef.task_definition_arn)
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # DDB table to store the Long and Short URLs with Short URL as the partition key url_mapping_table = ddb.Table( self, "url_shortener_mapping_table", partition_key=ddb.Attribute(name="short_url", type=ddb.AttributeType.STRING), read_capacity=10, write_capacity=10, removal_policy=core.RemovalPolicy.DESTROY, ) # AutoScaling of RCUs with a Target Utilization of 70% url_mapping_table.auto_scale_read_capacity( min_capacity=10, max_capacity=40000).scale_on_utilization( target_utilization_percent=70) # AutoScaling of WCUs with a Target Utilization of 70% url_mapping_table.auto_scale_write_capacity( min_capacity=10, max_capacity=40000).scale_on_utilization( target_utilization_percent=70) # DDB table to keep track of an Atomic Counter used for generating Short URLs url_counter_table = ddb.Table( self, "url_shortener_counter_table", partition_key=ddb.Attribute(name="id", type=ddb.AttributeType.STRING), read_capacity=10, write_capacity=10, removal_policy=core.RemovalPolicy.DESTROY, ) # AutoScaling of RCUs with a Target Utilization of 70% url_counter_table.auto_scale_read_capacity( min_capacity=10, max_capacity=40000).scale_on_utilization( target_utilization_percent=70) # AutoScaling of WCUs with a Target Utilization of 70% url_counter_table.auto_scale_write_capacity( min_capacity=10, max_capacity=40000).scale_on_utilization( target_utilization_percent=70) # Lambda function with custom code to handle shortening/unshortening logic url_lambda = _lambda.Function( self, "url_shortener_lambda", code=_lambda.Code.asset("lambda_proxy"), handler="lambda_function.lambda_handler", runtime=_lambda.Runtime.PYTHON_3_8, timeout=core.Duration.seconds(10), environment={ "BACKOFF": "25", "HASH_DIGEST_SIZE": "8", "MAX_RETRIES": "3", "URL_SHORTENER_MAPPING_TABLE": url_mapping_table.table_name, "URL_SHORTENER_COUNTER_TABLE": url_counter_table.table_name, }, log_retention=logs.RetentionDays.ONE_MONTH, ) # A Custom IAM Policy statement to grant DDB access to the Lambda function ddb_policy_statement = iam.PolicyStatement( actions=[ "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem" ], effect=iam.Effect.ALLOW, resources=[ url_mapping_table.table_arn, url_counter_table.table_arn ], ) # Attaching DDB Policy statement with the Lambda IAM Role url_lambda.add_to_role_policy(ddb_policy_statement) # Including X-Requested-With to the default CORS headers list headers = apigw.Cors.DEFAULT_HEADERS headers.append('X-Requested-With') # API Gateway endpoint to serve Shorten/Unshorten APIs url_rest_api = apigw.RestApi( self, "url_shortener_API", default_cors_preflight_options=apigw.CorsOptions( allow_origins=apigw.Cors.ALL_ORIGINS, allow_headers=headers, allow_methods=["POST", "GET", "OPTIONS"], status_code=200, ), ) # Shorten API using POST and Lambda proxy url_rest_api.root.add_resource(path_part="shorten", ).add_method( http_method="POST", request_models={ "application/json": apigw.Model.EMPTY_MODEL, }, integration=apigw.LambdaIntegration( handler=url_lambda, proxy=True, allow_test_invoke=True, ), ) # Unshorten API using GET and Lambda proxy url_rest_api.root.add_resource(path_part="unshorten", ).add_resource( path_part="{shorturl}").add_method( http_method="GET", request_models={ "application/json": apigw.Model.EMPTY_MODEL, }, integration=apigw.LambdaIntegration( handler=url_lambda, proxy=True, allow_test_invoke=True, ), ) # S3 bucket to host the URL Shortener Static Website s3_web_hosting = s3.Bucket( self, "url_shortener_web_hosting_bucket", website_index_document="index.html", ) # Uploading HTML and ICO files from local directory to S3 Static Website bucket s3_deploy = s3deploy.BucketDeployment( self, "website_source_files", sources=[s3deploy.Source.asset(path="website", )], destination_bucket=s3_web_hosting, ) # Lambda function to integrate the API GW Shorten endpoint with the HTML file stored in S3 cr_provider = _lambda.Function( self, "cr_provider", code=_lambda.Code.asset("custom_resource"), handler="lambda_function.lambda_handler", runtime=_lambda.Runtime.PYTHON_3_8, timeout=core.Duration.minutes(1), ) # A Custom IAM Policy statement to grant S3 access to the Lambda function lambda_cr_statement = iam.PolicyStatement( actions=["s3:List*", "s3:Get*", "s3:Put*"], effect=iam.Effect.ALLOW, resources=[ s3_web_hosting.bucket_arn, s3_web_hosting.bucket_arn + "/*" ]) cr_provider.add_to_role_policy(lambda_cr_statement) # CFN Custom Resource backed by Lambda lambda_cr = core.CustomResource( self, "lambda_cr", service_token=cr_provider.function_arn, properties={ "S3_BUCKET": s3_web_hosting.bucket_name, "S3_KEY": "index.html", "POST_URL": url_rest_api.url + "shorten", }, removal_policy=core.RemovalPolicy.DESTROY, ) # Adding dependency so that Custom Resource creation happens after files are uploaded to S3 lambda_cr.node.add_dependency(s3_deploy) # CloudFront Distribution with S3 and APIGateway origins url_cf_distribution = cf.CloudFrontWebDistribution( self, "url_shortener_distribution", origin_configs=[ cf.SourceConfiguration(s3_origin_source=cf.S3OriginConfig( s3_bucket_source=s3_web_hosting, origin_access_identity=cf.OriginAccessIdentity( self, id="OAI", comment= "OAI that allows CloudFront to access the S3 bucket"), ), behaviors=[ cf.Behavior( is_default_behavior=False, path_pattern="/index.html", ), cf.Behavior( is_default_behavior=False, path_pattern="/favicon.ico", ), ]), cf.SourceConfiguration( custom_origin_source=cf.CustomOriginConfig( domain_name=url_rest_api.url.lstrip("https://").split( "/")[0], ), origin_path="/" + url_rest_api.deployment_stage.stage_name + "/unshorten", behaviors=[ cf.Behavior( is_default_behavior=True, allowed_methods=cf.CloudFrontAllowedMethods. GET_HEAD_OPTIONS, ) ]) ], price_class=cf.PriceClass.PRICE_CLASS_ALL, default_root_object="index.html", ) # Adding the CloudFront Distribution endpoint to CFN Output core.CfnOutput( self, "URLShortenerWebsite", value=url_cf_distribution.domain_name, )
def __init__(self, scope: core.Construct, _id: str, **kwargs) -> None: super().__init__(scope, _id, **kwargs) # Setup SSM parameter of credentials, bucket_para, ignore_list ssm_credential_para = ssm.StringParameter.from_secure_string_parameter_attributes( self, "ssm_parameter_credentials", parameter_name=ssm_parameter_credentials, version=1) ssm_bucket_para = ssm.StringParameter(self, "s3bucket_serverless", string_value=json.dumps( bucket_para, indent=4)) ssm_parameter_ignore_list = ssm.StringParameter( self, "s3_migrate_ignore_list", string_value=ignore_list) # Setup DynamoDB ddb_file_list = ddb.Table(self, "s3migrate_serverless", partition_key=ddb.Attribute( name="Key", type=ddb.AttributeType.STRING), billing_mode=ddb.BillingMode.PAY_PER_REQUEST) ddb_file_list.add_global_secondary_index( partition_key=ddb.Attribute(name="desBucket", type=ddb.AttributeType.STRING), index_name="desBucket-index", projection_type=ddb.ProjectionType.INCLUDE, non_key_attributes=["desKey", "versionId"]) # Setup SQS sqs_queue_DLQ = sqs.Queue(self, "s3migrate_serverless_Q_DLQ", visibility_timeout=core.Duration.minutes(15), retention_period=core.Duration.days(14)) sqs_queue = sqs.Queue(self, "s3migrate_serverless_Q", visibility_timeout=core.Duration.minutes(15), retention_period=core.Duration.days(14), dead_letter_queue=sqs.DeadLetterQueue( max_receive_count=60, queue=sqs_queue_DLQ)) # Setup API for Lambda to get IP address (for debug networking routing purpose) checkip = api.RestApi( self, "lambda-checkip-api", cloud_watch_role=True, deploy=True, description="For Lambda get IP address", default_integration=api.MockIntegration( integration_responses=[ api.IntegrationResponse(status_code="200", response_templates={ "application/json": "$context.identity.sourceIp" }) ], request_templates={"application/json": '{"statusCode": 200}'}), endpoint_types=[api.EndpointType.REGIONAL]) checkip.root.add_method("GET", method_responses=[ api.MethodResponse( status_code="200", response_models={ "application/json": api.Model.EMPTY_MODEL }) ]) # Setup Lambda functions handler = lam.Function(self, "s3-migrate-worker", code=lam.Code.asset("./lambda"), handler="lambda_function_worker.lambda_handler", runtime=lam.Runtime.PYTHON_3_8, memory_size=1024, timeout=core.Duration.minutes(15), tracing=lam.Tracing.ACTIVE, environment={ 'table_queue_name': ddb_file_list.table_name, 'Des_bucket_default': Des_bucket_default, 'Des_prefix_default': Des_prefix_default, 'StorageClass': StorageClass, 'checkip_url': checkip.url, 'ssm_parameter_credentials': ssm_parameter_credentials, 'JobType': JobType, 'MaxRetry': MaxRetry, 'MaxThread': MaxThread, 'MaxParallelFile': MaxParallelFile, 'JobTimeout': JobTimeout, 'UpdateVersionId': UpdateVersionId, 'GetObjectWithVersionId': GetObjectWithVersionId }) handler_jobsender = lam.Function( self, "s3-migrate-jobsender", code=lam.Code.asset("./lambda"), handler="lambda_function_jobsender.lambda_handler", runtime=lam.Runtime.PYTHON_3_8, memory_size=1024, timeout=core.Duration.minutes(15), tracing=lam.Tracing.ACTIVE, environment={ 'table_queue_name': ddb_file_list.table_name, 'StorageClass': StorageClass, 'checkip_url': checkip.url, 'sqs_queue': sqs_queue.queue_name, 'ssm_parameter_credentials': ssm_parameter_credentials, 'ssm_parameter_ignore_list': ssm_parameter_ignore_list.parameter_name, 'ssm_parameter_bucket': ssm_bucket_para.parameter_name, 'JobType': JobType, 'MaxRetry': MaxRetry, 'JobsenderCompareVersionId': JobsenderCompareVersionId }) # Allow lambda read/write DDB, SQS ddb_file_list.grant_read_write_data(handler) ddb_file_list.grant_read_write_data(handler_jobsender) sqs_queue.grant_send_messages(handler_jobsender) # SQS trigger Lambda worker handler.add_event_source(SqsEventSource(sqs_queue, batch_size=1)) # Option1: Create S3 Bucket, all new objects in this bucket will be transmitted by Lambda Worker s3bucket = s3.Bucket(self, "s3_new_migrate") s3bucket.grant_read(handler) s3bucket.add_event_notification(s3.EventType.OBJECT_CREATED, s3n.SqsDestination(sqs_queue)) # Option2: Allow Exist S3 Buckets to be read by Lambda functions. # Lambda Jobsender will scan and compare the these buckets and trigger Lambda Workers to transmit bucket_name = '' for b in bucket_para: if bucket_name != b['src_bucket']: # 如果列了多个相同的Bucket,就跳过 bucket_name = b['src_bucket'] s3exist_bucket = s3.Bucket.from_bucket_name( self, bucket_name, # 用这个做id bucket_name=bucket_name) s3exist_bucket.grant_read(handler_jobsender) s3exist_bucket.grant_read(handler) # Allow Lambda read ssm parameters ssm_bucket_para.grant_read(handler_jobsender) ssm_credential_para.grant_read(handler) ssm_credential_para.grant_read(handler_jobsender) ssm_parameter_ignore_list.grant_read(handler_jobsender) # Schedule cron event to trigger Lambda Jobsender per hour: event.Rule(self, 'cron_trigger_jobsender', schedule=event.Schedule.rate(core.Duration.hours(1)), targets=[target.LambdaFunction(handler_jobsender)]) # Create Lambda logs filter to create network traffic metric handler.log_group.add_metric_filter( "Complete-bytes", metric_name="Complete-bytes", metric_namespace="s3_migrate", metric_value="$bytes", filter_pattern=logs.FilterPattern.literal( '[info, date, sn, p="--->Complete", bytes, key]')) handler.log_group.add_metric_filter( "Uploading-bytes", metric_name="Uploading-bytes", metric_namespace="s3_migrate", metric_value="$bytes", filter_pattern=logs.FilterPattern.literal( '[info, date, sn, p="--->Uploading", bytes, key]')) handler.log_group.add_metric_filter( "Downloading-bytes", metric_name="Downloading-bytes", metric_namespace="s3_migrate", metric_value="$bytes", filter_pattern=logs.FilterPattern.literal( '[info, date, sn, p="--->Downloading", bytes, key]')) lambda_metric_Complete = cw.Metric(namespace="s3_migrate", metric_name="Complete-bytes", statistic="Sum", period=core.Duration.minutes(1)) lambda_metric_Upload = cw.Metric(namespace="s3_migrate", metric_name="Uploading-bytes", statistic="Sum", period=core.Duration.minutes(1)) lambda_metric_Download = cw.Metric(namespace="s3_migrate", metric_name="Downloading-bytes", statistic="Sum", period=core.Duration.minutes(1)) handler.log_group.add_metric_filter( "ERROR", metric_name="ERROR-Logs", metric_namespace="s3_migrate", metric_value="1", filter_pattern=logs.FilterPattern.literal('"ERROR"')) handler.log_group.add_metric_filter( "WARNING", metric_name="WARNING-Logs", metric_namespace="s3_migrate", metric_value="1", filter_pattern=logs.FilterPattern.literal('"WARNING"')) # Task timed out handler.log_group.add_metric_filter( "TIMEOUT", metric_name="TIMEOUT-Logs", metric_namespace="s3_migrate", metric_value="1", filter_pattern=logs.FilterPattern.literal('"Task timed out"')) log_metric_ERROR = cw.Metric(namespace="s3_migrate", metric_name="ERROR-Logs", statistic="Sum", period=core.Duration.minutes(1)) log_metric_WARNING = cw.Metric(namespace="s3_migrate", metric_name="WARNING-Logs", statistic="Sum", period=core.Duration.minutes(1)) log_metric_TIMEOUT = cw.Metric(namespace="s3_migrate", metric_name="TIMEOUT-Logs", statistic="Sum", period=core.Duration.minutes(1)) # Dashboard to monitor SQS and Lambda board = cw.Dashboard(self, "s3_migrate_serverless") board.add_widgets( cw.GraphWidget(title="Lambda-NETWORK", left=[ lambda_metric_Download, lambda_metric_Upload, lambda_metric_Complete ]), # TODO: here monitor all lambda concurrency not just the working one. Limitation from CDK # Lambda now supports monitor single lambda concurrency, will change this after CDK support cw.GraphWidget(title="Lambda-all-concurrent", left=[ handler.metric_all_concurrent_executions( period=core.Duration.minutes(1)) ]), cw.GraphWidget( title="Lambda-invocations/errors/throttles", left=[ handler.metric_invocations( period=core.Duration.minutes(1)), handler.metric_errors(period=core.Duration.minutes(1)), handler.metric_throttles(period=core.Duration.minutes(1)) ]), cw.GraphWidget( title="Lambda-duration", left=[ handler.metric_duration(period=core.Duration.minutes(1)) ]), ) board.add_widgets( cw.GraphWidget( title="SQS-Jobs", left=[ sqs_queue.metric_approximate_number_of_messages_visible( period=core.Duration.minutes(1)), sqs_queue. metric_approximate_number_of_messages_not_visible( period=core.Duration.minutes(1)) ]), cw.GraphWidget( title="SQS-DeadLetterQueue", left=[ sqs_queue_DLQ. metric_approximate_number_of_messages_visible( period=core.Duration.minutes(1)), sqs_queue_DLQ. metric_approximate_number_of_messages_not_visible( period=core.Duration.minutes(1)) ]), cw.GraphWidget(title="ERROR/WARNING Logs", left=[log_metric_ERROR], right=[log_metric_WARNING, log_metric_TIMEOUT]), cw.SingleValueWidget( title="Running/Waiting and Dead Jobs", metrics=[ sqs_queue. metric_approximate_number_of_messages_not_visible( period=core.Duration.minutes(1)), sqs_queue.metric_approximate_number_of_messages_visible( period=core.Duration.minutes(1)), sqs_queue_DLQ. metric_approximate_number_of_messages_not_visible( period=core.Duration.minutes(1)), sqs_queue_DLQ. metric_approximate_number_of_messages_visible( period=core.Duration.minutes(1)) ], height=6)) # Alarm for queue - DLQ alarm_DLQ = cw.Alarm( self, "SQS_DLQ", metric=sqs_queue_DLQ.metric_approximate_number_of_messages_visible( ), threshold=0, comparison_operator=cw.ComparisonOperator.GREATER_THAN_THRESHOLD, evaluation_periods=1, datapoints_to_alarm=1) alarm_topic = sns.Topic(self, "SQS queue-DLQ has dead letter") alarm_topic.add_subscription( subscription=sub.EmailSubscription(alarm_email)) alarm_DLQ.add_alarm_action(action.SnsAction(alarm_topic)) core.CfnOutput(self, "Dashboard", value="CloudWatch Dashboard name s3_migrate_serverless")
def __init__(self, scope: core.Construct, id: str, emr_launch_stack, artifact_bucket, output_bucket, **kwargs): super().__init__(scope, id, **kwargs) launch_function = emr_launch_stack.launch_function # Create DynamoDB table for tracking dynamo_table = dynamo.Table( self, "dynamotable", partition_key=dynamo.Attribute(name="BatchId", type=dynamo.AttributeType.STRING), sort_key=dynamo.Attribute(name="Name", type=dynamo.AttributeType.STRING), billing_mode=dynamo.BillingMode.PAY_PER_REQUEST) emr_role = aws_iam.Role.from_role_arn( self, "emr_role_iam", role_arn=emr_launch_stack.instance_role_arn) emr_role.add_to_policy( aws_iam.PolicyStatement(actions=["dynamodb:*"], resources=[dynamo_table.table_arn])) emr_role.add_to_policy( aws_iam.PolicyStatement(actions=[ "logs:CreateLogStream", "logs:DescribeLogStreams", "logs:CreateLogGroup", "logs:PutLogEvents", "ec2:DescribeTags" ], resources=["*"])) # SNS Topics for Success/Failures messages from our Pipeline self.success_topic = sns.Topic(self, 'SuccessTopic') self.failure_topic = sns.Topic(self, 'FailureTopic') # Upload artifacts to S3 step_code = s3d.BucketDeployment( self, id='sparkscript', destination_bucket=artifact_bucket, destination_key_prefix='steps', sources=[ s3d.Source.asset('infrastructure/emr_orchestration/steps/') ]) # Create a Chain to receive Failure messages fail = emr_chains.Fail(self, 'FailChain', message=sfn.TaskInput.from_data_at('$.Error'), subject='Pipeline Failure', topic=self.failure_topic) # # Define a Task to Terminate the Cluster on failure terminate_failed_cluster = emr_tasks.TerminateClusterBuilder.build( self, 'TerminateFailedCluster', name='Terminate Failed Cluster', cluster_id=sfn.TaskInput.from_data_at( '$.LaunchClusterResult.ClusterId').value, result_path='$.TerminateResult').add_catch(fail, errors=['States.ALL'], result_path='$.Error') terminate_failed_cluster.next(fail) # Use a NestedStateMachine to launch the cluster launch_cluster = emr_chains.NestedStateMachine( self, 'NestedStateMachine', name='Launch Cluster StateMachine', state_machine=launch_function.state_machine, fail_chain=fail) pyspark_step = emr_chains.AddStepWithArgumentOverrides( self, 'PySparkDataIngestion', emr_step=emr_code.EMRStep( name=f'Data Ingestion - PySpark Job', jar='command-runner.jar', args=[ 'spark-submit', '--master', 'yarn', '--deploy-mode', 'client', '--packages', 'com.audienceproject:spark-dynamodb_2.12:1.1.2', os.path.join(f's3://{artifact_bucket.bucket_name}', 'steps', 'data_ingestion.py'), '--batch-id', 'DynamoDB.BatchId', '--batch-metadata-table-name', dynamo_table.table_name, '--output-bucket', output_bucket.bucket_name, '--region', os.environ["CDK_DEFAULT_REGION"] ]), cluster_id=sfn.TaskInput.from_data_at( '$.LaunchClusterResult.ClusterId').value, result_path='$.PySparkResult', fail_chain=terminate_failed_cluster) pyspark_example_step = emr_chains.AddStepWithArgumentOverrides( self, 'PySparkDataPreparation', emr_step=emr_code.EMRStep( name=f'Data Preparation - PySpark Job', jar='command-runner.jar', args=[ 'spark-submit', '--master', 'yarn', '--deploy-mode', 'client', os.path.join(f's3://{artifact_bucket.bucket_name}', 'steps', 'data_preparation.py'), '--batch-id', 'DynamoDB.BatchId', '--batch-metadata-table-name', dynamo_table.table_name, '--input-bucket', output_bucket.bucket_name, '--region', os.environ["CDK_DEFAULT_REGION"] ]), cluster_id=sfn.TaskInput.from_data_at( '$.LaunchClusterResult.ClusterId').value, result_path='$.PySparkResult', fail_chain=terminate_failed_cluster) # Define a Task to Terminate the Cluster terminate_cluster = emr_tasks.TerminateClusterBuilder.build( self, 'TerminateCluster', name='Terminate Cluster', cluster_id=sfn.TaskInput.from_data_at( '$.LaunchClusterResult.ClusterId').value, result_path='$.TerminateResult').add_catch(fail, errors=['States.ALL'], result_path='$.Error') # A Chain for Success notification when the pipeline completes success = emr_chains.Success( self, 'SuccessChain', message=sfn.TaskInput.from_data_at('$.TerminateResult'), subject='Pipeline Succeeded', topic=self.success_topic) # Assemble the Pipeline definition = sfn.Chain \ .start(launch_cluster) \ .next(pyspark_step) \ .next(pyspark_example_step) \ .next(terminate_cluster) \ .next(success) # Create the State Machine self.state_machine = sfn.StateMachine( self, 'PySparkExampleStateMachine', state_machine_name='pyspark-example-pipeline', definition=definition) self.dynamo_table = dynamo_table
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) vpc = aws_ec2.Vpc( self, "OctemberVPC", max_azs=2, # subnet_configuration=[{ # "cidrMask": 24, # "name": "Public", # "subnetType": aws_ec2.SubnetType.PUBLIC, # }, # { # "cidrMask": 24, # "name": "Private", # "subnetType": aws_ec2.SubnetType.PRIVATE # }, # { # "cidrMask": 28, # "name": "Isolated", # "subnetType": aws_ec2.SubnetType.ISOLATED, # "reserved": True # } # ], gateway_endpoints={ "S3": aws_ec2.GatewayVpcEndpointOptions( service=aws_ec2.GatewayVpcEndpointAwsService.S3) }) dynamo_db_endpoint = vpc.add_gateway_endpoint( "DynamoDbEndpoint", service=aws_ec2.GatewayVpcEndpointAwsService.DYNAMODB) s3_bucket = s3.Bucket( self, "s3bucket", bucket_name="octember-bizcard-{region}-{account}".format( region=core.Aws.REGION, account=core.Aws.ACCOUNT_ID)) api = apigw.RestApi( self, "BizcardImageUploader", rest_api_name="BizcardImageUploader", description="This service serves uploading bizcard images into s3.", endpoint_types=[apigw.EndpointType.REGIONAL], binary_media_types=["image/png", "image/jpg"], deploy=True, deploy_options=apigw.StageOptions(stage_name="v1")) rest_api_role = aws_iam.Role( self, "ApiGatewayRoleForS3", role_name="ApiGatewayRoleForS3FullAccess", assumed_by=aws_iam.ServicePrincipal("apigateway.amazonaws.com"), managed_policies=[ aws_iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonS3FullAccess") ]) list_objects_responses = [ apigw.IntegrationResponse( status_code="200", #XXX: https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_apigateway/IntegrationResponse.html#aws_cdk.aws_apigateway.IntegrationResponse.response_parameters # The response parameters from the backend response that API Gateway sends to the method response. # Use the destination as the key and the source as the value: # - The destination must be an existing response parameter in the MethodResponse property. # - The source must be an existing method request parameter or a static value. response_parameters={ 'method.response.header.Timestamp': 'integration.response.header.Date', 'method.response.header.Content-Length': 'integration.response.header.Content-Length', 'method.response.header.Content-Type': 'integration.response.header.Content-Type' }), apigw.IntegrationResponse(status_code="400", selection_pattern="4\d{2}"), apigw.IntegrationResponse(status_code="500", selection_pattern="5\d{2}") ] list_objects_integration_options = apigw.IntegrationOptions( credentials_role=rest_api_role, integration_responses=list_objects_responses) get_s3_integration = apigw.AwsIntegration( service="s3", integration_http_method="GET", path='/', options=list_objects_integration_options) api.root.add_method( "GET", get_s3_integration, authorization_type=apigw.AuthorizationType.IAM, api_key_required=False, method_responses=[ apigw.MethodResponse( status_code="200", response_parameters={ 'method.response.header.Timestamp': False, 'method.response.header.Content-Length': False, 'method.response.header.Content-Type': False }, response_models={'application/json': apigw.EmptyModel()}), apigw.MethodResponse(status_code="400"), apigw.MethodResponse(status_code="500") ], request_parameters={'method.request.header.Content-Type': False}) get_s3_folder_integration_options = apigw.IntegrationOptions( credentials_role=rest_api_role, integration_responses=list_objects_responses, #XXX: https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_apigateway/IntegrationOptions.html#aws_cdk.aws_apigateway.IntegrationOptions.request_parameters # Specify request parameters as key-value pairs (string-to-string mappings), with a destination as the key and a source as the value. # The source must be an existing method request parameter or a static value. request_parameters={ "integration.request.path.bucket": "method.request.path.folder" }) get_s3_folder_integration = apigw.AwsIntegration( service="s3", integration_http_method="GET", path="{bucket}", options=get_s3_folder_integration_options) s3_folder = api.root.add_resource('{folder}') s3_folder.add_method( "GET", get_s3_folder_integration, authorization_type=apigw.AuthorizationType.IAM, api_key_required=False, method_responses=[ apigw.MethodResponse( status_code="200", response_parameters={ 'method.response.header.Timestamp': False, 'method.response.header.Content-Length': False, 'method.response.header.Content-Type': False }, response_models={'application/json': apigw.EmptyModel()}), apigw.MethodResponse(status_code="400"), apigw.MethodResponse(status_code="500") ], request_parameters={ 'method.request.header.Content-Type': False, 'method.request.path.folder': True }) get_s3_item_integration_options = apigw.IntegrationOptions( credentials_role=rest_api_role, integration_responses=list_objects_responses, request_parameters={ "integration.request.path.bucket": "method.request.path.folder", "integration.request.path.object": "method.request.path.item" }) get_s3_item_integration = apigw.AwsIntegration( service="s3", integration_http_method="GET", path="{bucket}/{object}", options=get_s3_item_integration_options) s3_item = s3_folder.add_resource('{item}') s3_item.add_method( "GET", get_s3_item_integration, authorization_type=apigw.AuthorizationType.IAM, api_key_required=False, method_responses=[ apigw.MethodResponse( status_code="200", response_parameters={ 'method.response.header.Timestamp': False, 'method.response.header.Content-Length': False, 'method.response.header.Content-Type': False }, response_models={'application/json': apigw.EmptyModel()}), apigw.MethodResponse(status_code="400"), apigw.MethodResponse(status_code="500") ], request_parameters={ 'method.request.header.Content-Type': False, 'method.request.path.folder': True, 'method.request.path.item': True }) put_s3_item_integration_options = apigw.IntegrationOptions( credentials_role=rest_api_role, integration_responses=[ apigw.IntegrationResponse(status_code="200"), apigw.IntegrationResponse(status_code="400", selection_pattern="4\d{2}"), apigw.IntegrationResponse(status_code="500", selection_pattern="5\d{2}") ], request_parameters={ "integration.request.header.Content-Type": "method.request.header.Content-Type", "integration.request.path.bucket": "method.request.path.folder", "integration.request.path.object": "method.request.path.item" }) put_s3_item_integration = apigw.AwsIntegration( service="s3", integration_http_method="PUT", path="{bucket}/{object}", options=put_s3_item_integration_options) s3_item.add_method( "PUT", put_s3_item_integration, authorization_type=apigw.AuthorizationType.IAM, api_key_required=False, method_responses=[ apigw.MethodResponse( status_code="200", response_parameters={ 'method.response.header.Content-Type': False }, response_models={'application/json': apigw.EmptyModel()}), apigw.MethodResponse(status_code="400"), apigw.MethodResponse(status_code="500") ], request_parameters={ 'method.request.header.Content-Type': False, 'method.request.path.folder': True, 'method.request.path.item': True }) ddb_table = dynamodb.Table( self, "BizcardImageMetaInfoDdbTable", table_name="OctemberBizcardImgMeta", partition_key=dynamodb.Attribute( name="image_id", type=dynamodb.AttributeType.STRING), billing_mode=dynamodb.BillingMode.PROVISIONED, read_capacity=15, write_capacity=5) img_kinesis_stream = kinesis.Stream( self, "BizcardImagePath", stream_name="octember-bizcard-image") # create lambda function trigger_textract_lambda_fn = _lambda.Function( self, "TriggerTextExtractorFromImage", runtime=_lambda.Runtime.PYTHON_3_7, function_name="TriggerTextExtractorFromImage", handler="trigger_text_extract_from_s3_image.lambda_handler", description="Trigger to extract text from an image in S3", code=_lambda.Code.asset( "./src/main/python/TriggerTextExtractFromS3Image"), environment={ 'REGION_NAME': core.Aws.REGION, 'DDB_TABLE_NAME': ddb_table.table_name, 'KINESIS_STREAM_NAME': img_kinesis_stream.stream_name }, timeout=core.Duration.minutes(5)) ddb_table_rw_policy_statement = aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, resources=[ddb_table.table_arn], actions=[ "dynamodb:BatchGetItem", "dynamodb:Describe*", "dynamodb:List*", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchWriteItem", "dynamodb:DeleteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dax:Describe*", "dax:List*", "dax:GetItem", "dax:BatchGetItem", "dax:Query", "dax:Scan", "dax:BatchWriteItem", "dax:DeleteItem", "dax:PutItem", "dax:UpdateItem" ]) trigger_textract_lambda_fn.add_to_role_policy( ddb_table_rw_policy_statement) trigger_textract_lambda_fn.add_to_role_policy( aws_iam.PolicyStatement(effect=aws_iam.Effect.ALLOW, resources=[img_kinesis_stream.stream_arn], actions=[ "kinesis:Get*", "kinesis:List*", "kinesis:Describe*", "kinesis:PutRecord", "kinesis:PutRecords" ])) # assign notification for the s3 event type (ex: OBJECT_CREATED) s3_event_filter = s3.NotificationKeyFilter(prefix="bizcard-raw-img/", suffix=".jpg") s3_event_source = S3EventSource(s3_bucket, events=[s3.EventType.OBJECT_CREATED], filters=[s3_event_filter]) trigger_textract_lambda_fn.add_event_source(s3_event_source) #XXX: https://github.com/aws/aws-cdk/issues/2240 # To avoid to create extra Lambda Functions with names like LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a # if log_retention=aws_logs.RetentionDays.THREE_DAYS is added to the constructor props log_group = aws_logs.LogGroup( self, "TriggerTextractLogGroup", log_group_name="/aws/lambda/TriggerTextExtractorFromImage", retention=aws_logs.RetentionDays.THREE_DAYS) log_group.grant_write(trigger_textract_lambda_fn) text_kinesis_stream = kinesis.Stream( self, "BizcardTextData", stream_name="octember-bizcard-txt") textract_lambda_fn = _lambda.Function( self, "GetTextFromImage", runtime=_lambda.Runtime.PYTHON_3_7, function_name="GetTextFromImage", handler="get_text_from_s3_image.lambda_handler", description="extract text from an image in S3", code=_lambda.Code.asset("./src/main/python/GetTextFromS3Image"), environment={ 'REGION_NAME': core.Aws.REGION, 'DDB_TABLE_NAME': ddb_table.table_name, 'KINESIS_STREAM_NAME': text_kinesis_stream.stream_name }, timeout=core.Duration.minutes(5)) textract_lambda_fn.add_to_role_policy(ddb_table_rw_policy_statement) textract_lambda_fn.add_to_role_policy( aws_iam.PolicyStatement(effect=aws_iam.Effect.ALLOW, resources=[text_kinesis_stream.stream_arn], actions=[ "kinesis:Get*", "kinesis:List*", "kinesis:Describe*", "kinesis:PutRecord", "kinesis:PutRecords" ])) textract_lambda_fn.add_to_role_policy( aws_iam.PolicyStatement( **{ "effect": aws_iam.Effect.ALLOW, "resources": [ s3_bucket.bucket_arn, "{}/*".format( s3_bucket.bucket_arn) ], "actions": [ "s3:AbortMultipartUpload", "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:PutObject" ] })) textract_lambda_fn.add_to_role_policy( aws_iam.PolicyStatement(effect=aws_iam.Effect.ALLOW, resources=["*"], actions=["textract:*"])) img_kinesis_event_source = KinesisEventSource( img_kinesis_stream, batch_size=100, starting_position=_lambda.StartingPosition.LATEST) textract_lambda_fn.add_event_source(img_kinesis_event_source) log_group = aws_logs.LogGroup( self, "GetTextFromImageLogGroup", log_group_name="/aws/lambda/GetTextFromImage", retention=aws_logs.RetentionDays.THREE_DAYS) log_group.grant_write(textract_lambda_fn) sg_use_bizcard_es = aws_ec2.SecurityGroup( self, "BizcardSearchClientSG", vpc=vpc, allow_all_outbound=True, description= 'security group for octember bizcard elasticsearch client', security_group_name='use-octember-bizcard-es') core.Tag.add(sg_use_bizcard_es, 'Name', 'use-octember-bizcard-es') sg_bizcard_es = aws_ec2.SecurityGroup( self, "BizcardSearchSG", vpc=vpc, allow_all_outbound=True, description='security group for octember bizcard elasticsearch', security_group_name='octember-bizcard-es') core.Tag.add(sg_bizcard_es, 'Name', 'octember-bizcard-es') sg_bizcard_es.add_ingress_rule(peer=sg_bizcard_es, connection=aws_ec2.Port.all_tcp(), description='octember-bizcard-es') sg_bizcard_es.add_ingress_rule(peer=sg_use_bizcard_es, connection=aws_ec2.Port.all_tcp(), description='use-octember-bizcard-es') sg_ssh_access = aws_ec2.SecurityGroup( self, "BastionHostSG", vpc=vpc, allow_all_outbound=True, description='security group for bastion host', security_group_name='octember-bastion-host-sg') core.Tag.add(sg_ssh_access, 'Name', 'octember-bastion-host') sg_ssh_access.add_ingress_rule(peer=aws_ec2.Peer.any_ipv4(), connection=aws_ec2.Port.tcp(22), description='ssh access') bastion_host = aws_ec2.BastionHostLinux( self, "BastionHost", vpc=vpc, instance_type=aws_ec2.InstanceType('t3.nano'), security_group=sg_ssh_access, subnet_selection=aws_ec2.SubnetSelection( subnet_type=aws_ec2.SubnetType.PUBLIC)) bastion_host.instance.add_security_group(sg_use_bizcard_es) #XXX: aws cdk elastsearch example - https://github.com/aws/aws-cdk/issues/2873 es_cfn_domain = aws_elasticsearch.CfnDomain( self, 'BizcardSearch', elasticsearch_cluster_config={ "dedicatedMasterCount": 3, "dedicatedMasterEnabled": True, "dedicatedMasterType": "t2.medium.elasticsearch", "instanceCount": 2, "instanceType": "t2.medium.elasticsearch", "zoneAwarenessEnabled": True }, ebs_options={ "ebsEnabled": True, "volumeSize": 10, "volumeType": "gp2" }, domain_name="octember-bizcard", elasticsearch_version="7.1", encryption_at_rest_options={"enabled": False}, access_policies={ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": ["es:Describe*", "es:List*", "es:Get*", "es:ESHttp*"], "Resource": self.format_arn(service="es", resource="domain", resource_name="octember-bizcard/*") }] }, snapshot_options={"automatedSnapshotStartHour": 17}, vpc_options={ "securityGroupIds": [sg_bizcard_es.security_group_id], "subnetIds": vpc.select_subnets( subnet_type=aws_ec2.SubnetType.PRIVATE).subnet_ids }) core.Tag.add(es_cfn_domain, 'Name', 'octember-bizcard-es') s3_lib_bucket_name = self.node.try_get_context("lib_bucket_name") #XXX: https://github.com/aws/aws-cdk/issues/1342 s3_lib_bucket = s3.Bucket.from_bucket_name(self, id, s3_lib_bucket_name) es_lib_layer = _lambda.LayerVersion( self, "ESLib", layer_version_name="es-lib", compatible_runtimes=[_lambda.Runtime.PYTHON_3_7], code=_lambda.Code.from_bucket(s3_lib_bucket, "var/octember-es-lib.zip")) redis_lib_layer = _lambda.LayerVersion( self, "RedisLib", layer_version_name="redis-lib", compatible_runtimes=[_lambda.Runtime.PYTHON_3_7], code=_lambda.Code.from_bucket(s3_lib_bucket, "var/octember-redis-lib.zip")) #XXX: Deploy lambda in VPC - https://github.com/aws/aws-cdk/issues/1342 upsert_to_es_lambda_fn = _lambda.Function( self, "UpsertBizcardToES", runtime=_lambda.Runtime.PYTHON_3_7, function_name="UpsertBizcardToElasticSearch", handler="upsert_bizcard_to_es.lambda_handler", description="Upsert bizcard text into elasticsearch", code=_lambda.Code.asset("./src/main/python/UpsertBizcardToES"), environment={ 'ES_HOST': es_cfn_domain.attr_domain_endpoint, 'ES_INDEX': 'octember_bizcard', 'ES_TYPE': 'bizcard' }, timeout=core.Duration.minutes(5), layers=[es_lib_layer], security_groups=[sg_use_bizcard_es], vpc=vpc) text_kinesis_event_source = KinesisEventSource( text_kinesis_stream, batch_size=99, starting_position=_lambda.StartingPosition.LATEST) upsert_to_es_lambda_fn.add_event_source(text_kinesis_event_source) log_group = aws_logs.LogGroup( self, "UpsertBizcardToESLogGroup", log_group_name="/aws/lambda/UpsertBizcardToElasticSearch", retention=aws_logs.RetentionDays.THREE_DAYS) log_group.grant_write(upsert_to_es_lambda_fn) firehose_role_policy_doc = aws_iam.PolicyDocument() firehose_role_policy_doc.add_statements( aws_iam.PolicyStatement( **{ "effect": aws_iam.Effect.ALLOW, "resources": [ s3_bucket.bucket_arn, "{}/*".format( s3_bucket.bucket_arn) ], "actions": [ "s3:AbortMultipartUpload", "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:PutObject" ] })) firehose_role_policy_doc.add_statements( aws_iam.PolicyStatement(effect=aws_iam.Effect.ALLOW, resources=["*"], actions=[ "glue:GetTable", "glue:GetTableVersion", "glue:GetTableVersions" ])) firehose_role_policy_doc.add_statements( aws_iam.PolicyStatement(effect=aws_iam.Effect.ALLOW, resources=[text_kinesis_stream.stream_arn], actions=[ "kinesis:DescribeStream", "kinesis:GetShardIterator", "kinesis:GetRecords" ])) firehose_log_group_name = "/aws/kinesisfirehose/octember-bizcard-txt-to-s3" firehose_role_policy_doc.add_statements( aws_iam.PolicyStatement( effect=aws_iam.Effect.ALLOW, #XXX: The ARN will be formatted as follows: # arn:{partition}:{service}:{region}:{account}:{resource}{sep}}{resource-name} resources=[ self.format_arn(service="logs", resource="log-group", resource_name="{}:log-stream:*".format( firehose_log_group_name), sep=":") ], actions=["logs:PutLogEvents"])) firehose_role = aws_iam.Role( self, "FirehoseDeliveryRole", role_name="FirehoseDeliveryRole", assumed_by=aws_iam.ServicePrincipal("firehose.amazonaws.com"), #XXX: use inline_policies to work around https://github.com/aws/aws-cdk/issues/5221 inline_policies={"firehose_role_policy": firehose_role_policy_doc}) bizcard_text_to_s3_delivery_stream = aws_kinesisfirehose.CfnDeliveryStream( self, "BizcardTextToS3", delivery_stream_name="octember-bizcard-txt-to-s3", delivery_stream_type="KinesisStreamAsSource", kinesis_stream_source_configuration={ "kinesisStreamArn": text_kinesis_stream.stream_arn, "roleArn": firehose_role.role_arn }, extended_s3_destination_configuration={ "bucketArn": s3_bucket.bucket_arn, "bufferingHints": { "intervalInSeconds": 60, "sizeInMBs": 1 }, "cloudWatchLoggingOptions": { "enabled": True, "logGroupName": firehose_log_group_name, "logStreamName": "S3Delivery" }, "compressionFormat": "GZIP", "prefix": "bizcard-text/", "roleArn": firehose_role.role_arn }) sg_use_bizcard_es_cache = aws_ec2.SecurityGroup( self, "BizcardSearchCacheClientSG", vpc=vpc, allow_all_outbound=True, description= 'security group for octember bizcard search query cache client', security_group_name='use-octember-bizcard-es-cache') core.Tag.add(sg_use_bizcard_es_cache, 'Name', 'use-octember-bizcard-es-cache') sg_bizcard_es_cache = aws_ec2.SecurityGroup( self, "BizcardSearchCacheSG", vpc=vpc, allow_all_outbound=True, description= 'security group for octember bizcard search query cache', security_group_name='octember-bizcard-es-cache') core.Tag.add(sg_bizcard_es_cache, 'Name', 'octember-bizcard-es-cache') sg_bizcard_es_cache.add_ingress_rule( peer=sg_use_bizcard_es_cache, connection=aws_ec2.Port.tcp(6379), description='use-octember-bizcard-es-cache') es_query_cache_subnet_group = aws_elasticache.CfnSubnetGroup( self, "QueryCacheSubnetGroup", description="subnet group for octember-bizcard-es-cache", subnet_ids=vpc.select_subnets( subnet_type=aws_ec2.SubnetType.PRIVATE).subnet_ids, cache_subnet_group_name='octember-bizcard-es-cache') es_query_cache = aws_elasticache.CfnCacheCluster( self, "BizcardSearchQueryCache", cache_node_type="cache.t3.small", num_cache_nodes=1, engine="redis", engine_version="5.0.5", auto_minor_version_upgrade=False, cluster_name="octember-bizcard-es-cache", snapshot_retention_limit=3, snapshot_window="17:00-19:00", preferred_maintenance_window="mon:19:00-mon:20:30", #XXX: Do not use referece for "cache_subnet_group_name" - https://github.com/aws/aws-cdk/issues/3098 #cache_subnet_group_name=es_query_cache_subnet_group.cache_subnet_group_name, # Redis cluster goes to wrong VPC cache_subnet_group_name='octember-bizcard-es-cache', vpc_security_group_ids=[sg_bizcard_es_cache.security_group_id]) #XXX: If you're going to launch your cluster in an Amazon VPC, you need to create a subnet group before you start creating a cluster. # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticache-cache-cluster.html#cfn-elasticache-cachecluster-cachesubnetgroupname es_query_cache.add_depends_on(es_query_cache_subnet_group) #XXX: add more than 2 security groups # https://github.com/aws/aws-cdk/blob/ea10f0d141a48819ec0000cd7905feda993870a9/packages/%40aws-cdk/aws-lambda/lib/function.ts#L387 # https://github.com/aws/aws-cdk/issues/1555 # https://github.com/aws/aws-cdk/pull/5049 bizcard_search_lambda_fn = _lambda.Function( self, "BizcardSearchServer", runtime=_lambda.Runtime.PYTHON_3_7, function_name="BizcardSearchProxy", handler="es_search_bizcard.lambda_handler", description="Proxy server to search bizcard text", code=_lambda.Code.asset("./src/main/python/SearchBizcard"), environment={ 'ES_HOST': es_cfn_domain.attr_domain_endpoint, 'ES_INDEX': 'octember_bizcard', 'ES_TYPE': 'bizcard', 'ELASTICACHE_HOST': es_query_cache.attr_redis_endpoint_address }, timeout=core.Duration.minutes(1), layers=[es_lib_layer, redis_lib_layer], security_groups=[sg_use_bizcard_es, sg_use_bizcard_es_cache], vpc=vpc) #XXX: create API Gateway + LambdaProxy search_api = apigw.LambdaRestApi( self, "BizcardSearchAPI", handler=bizcard_search_lambda_fn, proxy=False, rest_api_name="BizcardSearch", description="This service serves searching bizcard text.", endpoint_types=[apigw.EndpointType.REGIONAL], deploy=True, deploy_options=apigw.StageOptions(stage_name="v1")) bizcard_search = search_api.root.add_resource('search') bizcard_search.add_method( "GET", method_responses=[ apigw.MethodResponse( status_code="200", response_models={'application/json': apigw.EmptyModel()}), apigw.MethodResponse(status_code="400"), apigw.MethodResponse(status_code="500") ]) sg_use_bizcard_graph_db = aws_ec2.SecurityGroup( self, "BizcardGraphDbClientSG", vpc=vpc, allow_all_outbound=True, description='security group for octember bizcard graph db client', security_group_name='use-octember-bizcard-neptune') core.Tag.add(sg_use_bizcard_graph_db, 'Name', 'use-octember-bizcard-neptune') sg_bizcard_graph_db = aws_ec2.SecurityGroup( self, "BizcardGraphDbSG", vpc=vpc, allow_all_outbound=True, description='security group for octember bizcard graph db', security_group_name='octember-bizcard-neptune') core.Tag.add(sg_bizcard_graph_db, 'Name', 'octember-bizcard-neptune') sg_bizcard_graph_db.add_ingress_rule( peer=sg_bizcard_graph_db, connection=aws_ec2.Port.tcp(8182), description='octember-bizcard-neptune') sg_bizcard_graph_db.add_ingress_rule( peer=sg_use_bizcard_graph_db, connection=aws_ec2.Port.tcp(8182), description='use-octember-bizcard-neptune') bizcard_graph_db_subnet_group = aws_neptune.CfnDBSubnetGroup( self, "NeptuneSubnetGroup", db_subnet_group_description= "subnet group for octember-bizcard-neptune", subnet_ids=vpc.select_subnets( subnet_type=aws_ec2.SubnetType.PRIVATE).subnet_ids, db_subnet_group_name='octember-bizcard-neptune') bizcard_graph_db = aws_neptune.CfnDBCluster( self, "BizcardGraphDB", availability_zones=vpc.availability_zones, db_subnet_group_name=bizcard_graph_db_subnet_group. db_subnet_group_name, db_cluster_identifier="octember-bizcard", backup_retention_period=1, preferred_backup_window="08:45-09:15", preferred_maintenance_window="sun:18:00-sun:18:30", vpc_security_group_ids=[sg_bizcard_graph_db.security_group_id]) bizcard_graph_db.add_depends_on(bizcard_graph_db_subnet_group) bizcard_graph_db_instance = aws_neptune.CfnDBInstance( self, "BizcardGraphDBInstance", db_instance_class="db.r5.large", allow_major_version_upgrade=False, auto_minor_version_upgrade=False, availability_zone=vpc.availability_zones[0], db_cluster_identifier=bizcard_graph_db.db_cluster_identifier, db_instance_identifier="octember-bizcard", preferred_maintenance_window="sun:18:00-sun:18:30") bizcard_graph_db_instance.add_depends_on(bizcard_graph_db) bizcard_graph_db_replica_instance = aws_neptune.CfnDBInstance( self, "BizcardGraphDBReplicaInstance", db_instance_class="db.r5.large", allow_major_version_upgrade=False, auto_minor_version_upgrade=False, availability_zone=vpc.availability_zones[-1], db_cluster_identifier=bizcard_graph_db.db_cluster_identifier, db_instance_identifier="octember-bizcard-replica", preferred_maintenance_window="sun:18:00-sun:18:30") bizcard_graph_db_replica_instance.add_depends_on(bizcard_graph_db) bizcard_graph_db_replica_instance.add_depends_on( bizcard_graph_db_instance) gremlinpython_lib_layer = _lambda.LayerVersion( self, "GremlinPythonLib", layer_version_name="gremlinpython-lib", compatible_runtimes=[_lambda.Runtime.PYTHON_3_7], code=_lambda.Code.from_bucket( s3_lib_bucket, "var/octember-gremlinpython-lib.zip")) #XXX: https://github.com/aws/aws-cdk/issues/1342 upsert_to_neptune_lambda_fn = _lambda.Function( self, "UpsertBizcardToGraphDB", runtime=_lambda.Runtime.PYTHON_3_7, function_name="UpsertBizcardToNeptune", handler="upsert_bizcard_to_graph_db.lambda_handler", description="Upsert bizcard into neptune", code=_lambda.Code.asset( "./src/main/python/UpsertBizcardToGraphDB"), environment={ 'REGION_NAME': core.Aws.REGION, 'NEPTUNE_ENDPOINT': bizcard_graph_db.attr_endpoint, 'NEPTUNE_PORT': bizcard_graph_db.attr_port }, timeout=core.Duration.minutes(5), layers=[gremlinpython_lib_layer], security_groups=[sg_use_bizcard_graph_db], vpc=vpc) upsert_to_neptune_lambda_fn.add_event_source(text_kinesis_event_source) log_group = aws_logs.LogGroup( self, "UpsertBizcardToGraphDBLogGroup", log_group_name="/aws/lambda/UpsertBizcardToNeptune", retention=aws_logs.RetentionDays.THREE_DAYS) log_group.grant_write(upsert_to_neptune_lambda_fn) sg_use_bizcard_neptune_cache = aws_ec2.SecurityGroup( self, "BizcardNeptuneCacheClientSG", vpc=vpc, allow_all_outbound=True, description= 'security group for octember bizcard recommendation query cache client', security_group_name='use-octember-bizcard-neptune-cache') core.Tag.add(sg_use_bizcard_neptune_cache, 'Name', 'use-octember-bizcard-es-cache') sg_bizcard_neptune_cache = aws_ec2.SecurityGroup( self, "BizcardNeptuneCacheSG", vpc=vpc, allow_all_outbound=True, description= 'security group for octember bizcard recommendation query cache', security_group_name='octember-bizcard-neptune-cache') core.Tag.add(sg_bizcard_neptune_cache, 'Name', 'octember-bizcard-neptune-cache') sg_bizcard_neptune_cache.add_ingress_rule( peer=sg_use_bizcard_neptune_cache, connection=aws_ec2.Port.tcp(6379), description='use-octember-bizcard-neptune-cache') recomm_query_cache_subnet_group = aws_elasticache.CfnSubnetGroup( self, "RecommQueryCacheSubnetGroup", description="subnet group for octember-bizcard-neptune-cache", subnet_ids=vpc.select_subnets( subnet_type=aws_ec2.SubnetType.PRIVATE).subnet_ids, cache_subnet_group_name='octember-bizcard-neptune-cache') recomm_query_cache = aws_elasticache.CfnCacheCluster( self, "BizcardRecommQueryCache", cache_node_type="cache.t3.small", num_cache_nodes=1, engine="redis", engine_version="5.0.5", auto_minor_version_upgrade=False, cluster_name="octember-bizcard-neptune-cache", snapshot_retention_limit=3, snapshot_window="17:00-19:00", preferred_maintenance_window="mon:19:00-mon:20:30", #XXX: Do not use referece for "cache_subnet_group_name" - https://github.com/aws/aws-cdk/issues/3098 #cache_subnet_group_name=recomm_query_cache_subnet_group.cache_subnet_group_name, # Redis cluster goes to wrong VPC cache_subnet_group_name='octember-bizcard-neptune-cache', vpc_security_group_ids=[ sg_bizcard_neptune_cache.security_group_id ]) recomm_query_cache.add_depends_on(recomm_query_cache_subnet_group) bizcard_recomm_lambda_fn = _lambda.Function( self, "BizcardRecommender", runtime=_lambda.Runtime.PYTHON_3_7, function_name="BizcardRecommender", handler="neptune_recommend_bizcard.lambda_handler", description="This service serves PYMK(People You May Know).", code=_lambda.Code.asset("./src/main/python/RecommendBizcard"), environment={ 'REGION_NAME': core.Aws.REGION, 'NEPTUNE_ENDPOINT': bizcard_graph_db.attr_read_endpoint, 'NEPTUNE_PORT': bizcard_graph_db.attr_port, 'ELASTICACHE_HOST': recomm_query_cache.attr_redis_endpoint_address }, timeout=core.Duration.minutes(1), layers=[gremlinpython_lib_layer, redis_lib_layer], security_groups=[ sg_use_bizcard_graph_db, sg_use_bizcard_neptune_cache ], vpc=vpc) #XXX: create API Gateway + LambdaProxy recomm_api = apigw.LambdaRestApi( self, "BizcardRecommendAPI", handler=bizcard_recomm_lambda_fn, proxy=False, rest_api_name="BizcardRecommend", description="This service serves PYMK(People You May Know).", endpoint_types=[apigw.EndpointType.REGIONAL], deploy=True, deploy_options=apigw.StageOptions(stage_name="v1")) bizcard_recomm = recomm_api.root.add_resource('pymk') bizcard_recomm.add_method( "GET", method_responses=[ apigw.MethodResponse( status_code="200", response_models={'application/json': apigw.EmptyModel()}), apigw.MethodResponse(status_code="400"), apigw.MethodResponse(status_code="500") ]) sagemaker_notebook_role_policy_doc = aws_iam.PolicyDocument() sagemaker_notebook_role_policy_doc.add_statements( aws_iam.PolicyStatement( **{ "effect": aws_iam.Effect.ALLOW, "resources": [ "arn:aws:s3:::aws-neptune-notebook", "arn:aws:s3:::aws-neptune-notebook/*" ], "actions": ["s3:GetObject", "s3:ListBucket"] })) sagemaker_notebook_role_policy_doc.add_statements( aws_iam.PolicyStatement( **{ "effect": aws_iam.Effect.ALLOW, "resources": [ "arn:aws:neptune-db:{region}:{account}:{cluster_id}/*". format(region=core.Aws.REGION, account=core.Aws.ACCOUNT_ID, cluster_id=bizcard_graph_db. attr_cluster_resource_id) ], "actions": ["neptune-db:connect"] })) sagemaker_notebook_role = aws_iam.Role( self, 'SageMakerNotebookForNeptuneWorkbenchRole', role_name='AWSNeptuneNotebookRole-OctemberBizcard', assumed_by=aws_iam.ServicePrincipal('sagemaker.amazonaws.com'), #XXX: use inline_policies to work around https://github.com/aws/aws-cdk/issues/5221 inline_policies={ 'AWSNeptuneNotebook': sagemaker_notebook_role_policy_doc }) neptune_wb_lifecycle_content = '''#!/bin/bash sudo -u ec2-user -i <<'EOF' echo "export GRAPH_NOTEBOOK_AUTH_MODE=DEFAULT" >> ~/.bashrc echo "export GRAPH_NOTEBOOK_HOST={NeptuneClusterEndpoint}" >> ~/.bashrc echo "export GRAPH_NOTEBOOK_PORT={NeptuneClusterPort}" >> ~/.bashrc echo "export NEPTUNE_LOAD_FROM_S3_ROLE_ARN=''" >> ~/.bashrc echo "export AWS_REGION={AWS_Region}" >> ~/.bashrc aws s3 cp s3://aws-neptune-notebook/graph_notebook.tar.gz /tmp/graph_notebook.tar.gz rm -rf /tmp/graph_notebook tar -zxvf /tmp/graph_notebook.tar.gz -C /tmp /tmp/graph_notebook/install.sh EOF '''.format(NeptuneClusterEndpoint=bizcard_graph_db.attr_endpoint, NeptuneClusterPort=bizcard_graph_db.attr_port, AWS_Region=core.Aws.REGION) neptune_wb_lifecycle_config_prop = aws_sagemaker.CfnNotebookInstanceLifecycleConfig.NotebookInstanceLifecycleHookProperty( content=core.Fn.base64(neptune_wb_lifecycle_content)) neptune_wb_lifecycle_config = aws_sagemaker.CfnNotebookInstanceLifecycleConfig( self, 'NpetuneWorkbenchLifeCycleConfig', notebook_instance_lifecycle_config_name= 'AWSNeptuneWorkbenchOctemberBizcardLCConfig', on_start=[neptune_wb_lifecycle_config_prop]) neptune_workbench = aws_sagemaker.CfnNotebookInstance( self, 'NeptuneWorkbench', instance_type='ml.t2.medium', role_arn=sagemaker_notebook_role.role_arn, lifecycle_config_name=neptune_wb_lifecycle_config. notebook_instance_lifecycle_config_name, notebook_instance_name='OctemberBizcard-NeptuneWorkbench', root_access='Disabled', security_group_ids=[sg_use_bizcard_graph_db.security_group_name], subnet_id=bizcard_graph_db_subnet_group.subnet_ids[0])
def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # If left unchecked this pattern could "fan out" on the transform and load # lambdas to the point that it consumes all resources on the account. This is # why we are limiting concurrency to 2 on all 3 lambdas. Feel free to raise this. lambda_throttle_size = 2 #### # DynamoDB Table # This is where our transformed data ends up #### table = dynamo_db.Table(self, "TransformedData", partition_key=dynamo_db.Attribute( name="id", type=dynamo_db.AttributeType.STRING)) #### # S3 Landing Bucket # This is where the user uploads the file to be transformed #### bucket = s3.Bucket(self, "LandingBucket") #### # Queue that listens for S3 Bucket events #### queue = sqs.Queue(self, 'newObjectInLandingBucketEventQueue', visibility_timeout=core.Duration.seconds(300)) bucket.add_event_notification(s3.EventType.OBJECT_CREATED, s3n.SqsDestination(queue)) # EventBridge Permissions event_bridge_put_policy = iam.PolicyStatement( effect=iam.Effect.ALLOW, resources=['*'], actions=['events:PutEvents']) #### # Fargate ECS Task Creation to pull data from S3 # # Fargate is used here because if you had a seriously large file, # you could stream the data to fargate for as long as needed before # putting the data onto eventbridge or up the memory/storage to # download the whole file. Lambda has limitations on runtime and # memory/storage #### vpc = ec2.Vpc(self, "Vpc", max_azs=2) logging = ecs.AwsLogDriver(stream_prefix='TheEventBridgeETL', log_retention=logs.RetentionDays.ONE_WEEK) cluster = ecs.Cluster(self, 'Ec2Cluster', vpc=vpc) task_definition = ecs.TaskDefinition( self, 'FargateTaskDefinition', memory_mib="512", cpu="256", compatibility=ecs.Compatibility.FARGATE) # We need to give our fargate container permission to put events on our EventBridge task_definition.add_to_task_role_policy(event_bridge_put_policy) # Grant fargate container access to the object that was uploaded to s3 bucket.grant_read(task_definition.task_role) container = task_definition.add_container( 'AppContainer', image=ecs.ContainerImage.from_asset( 'container/s3DataExtractionTask'), logging=logging, environment={ 'S3_BUCKET_NAME': bucket.bucket_name, 'S3_OBJECT_KEY': '' }) #### # Lambdas # # These are used for 4 phases: # # Extract - kicks of ecs fargate task to download data and splinter to eventbridge events # Transform - takes the two comma separated strings and produces a json object # Load - inserts the data into dynamodb # Observe - This is a lambda that subscribes to all events and logs them centrally #### subnet_ids = [] for subnet in vpc.private_subnets: subnet_ids.append(subnet.subnet_id) #### # Extract # defines an AWS Lambda resource to trigger our fargate ecs task #### extract_lambda = _lambda.Function( self, "extractLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, handler="s3SqsEventConsumer.handler", code=_lambda.Code.from_asset("lambda_fns/extract"), reserved_concurrent_executions=lambda_throttle_size, environment={ "CLUSTER_NAME": cluster.cluster_name, "TASK_DEFINITION": task_definition.task_definition_arn, "SUBNETS": json.dumps(subnet_ids), "CONTAINER_NAME": container.container_name }) queue.grant_consume_messages(extract_lambda) extract_lambda.add_event_source(_event.SqsEventSource(queue=queue)) extract_lambda.add_to_role_policy(event_bridge_put_policy) run_task_policy_statement = iam.PolicyStatement( effect=iam.Effect.ALLOW, resources=[task_definition.task_definition_arn], actions=['ecs:RunTask']) extract_lambda.add_to_role_policy(run_task_policy_statement) task_execution_role_policy_statement = iam.PolicyStatement( effect=iam.Effect.ALLOW, resources=[ task_definition.obtain_execution_role().role_arn, task_definition.task_role.role_arn ], actions=['iam:PassRole']) extract_lambda.add_to_role_policy(task_execution_role_policy_statement) #### # Transform # defines a lambda to transform the data that was extracted from s3 #### transform_lambda = _lambda.Function( self, "TransformLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, handler="transform.handler", code=_lambda.Code.from_asset("lambda_fns/transform"), reserved_concurrent_executions=lambda_throttle_size, timeout=core.Duration.seconds(3)) transform_lambda.add_to_role_policy(event_bridge_put_policy) # Create EventBridge rule to route extraction events transform_rule = events.Rule( self, 'transformRule', description='Data extracted from S3, Needs transformed', event_pattern=events.EventPattern( source=['cdkpatterns.the-eventbridge-etl'], detail_type=['s3RecordExtraction'], detail={"status": ["extracted"]})) transform_rule.add_target( targets.LambdaFunction(handler=transform_lambda)) #### # Load # load the transformed data in dynamodb #### load_lambda = _lambda.Function( self, "LoadLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, handler="load.handler", code=_lambda.Code.from_asset("lambda_fns/load"), reserved_concurrent_executions=lambda_throttle_size, timeout=core.Duration.seconds(3), environment={"TABLE_NAME": table.table_name}) load_lambda.add_to_role_policy(event_bridge_put_policy) table.grant_read_write_data(load_lambda) load_rule = events.Rule( self, 'loadRule', description='Data transformed, Needs loaded into dynamodb', event_pattern=events.EventPattern( source=['cdkpatterns.the-eventbridge-etl'], detail_type=['transform'], detail={"status": ["transformed"]})) load_rule.add_target(targets.LambdaFunction(handler=load_lambda)) #### # Observe # Watch for all cdkpatterns.the-eventbridge-etl events and log them centrally #### observe_lambda = _lambda.Function( self, "ObserveLambdaHandler", runtime=_lambda.Runtime.NODEJS_12_X, handler="observe.handler", code=_lambda.Code.from_asset("lambda_fns/observe"), reserved_concurrent_executions=lambda_throttle_size, timeout=core.Duration.seconds(3)) observe_rule = events.Rule( self, 'observeRule', description='all events are caught here and logged centrally', event_pattern=events.EventPattern( source=['cdkpatterns.the-eventbridge-etl'])) observe_rule.add_target(targets.LambdaFunction(handler=observe_lambda))