├── code ├── extras │ └── TestHarness_Input.json ├── source │ ├── LambdaRDS_TestHarness.py │ ├── LambdaRDS_Test.py │ ├── LambdaRDS_CFNInit.py │ └── LambdaRDS_Demo.yaml └── lib │ └── LambdaRDS_ManageConnections.py ├── .gitignore ├── images ├── Metrics.png ├── CloudWatch-Metrics.png ├── CloudWatch-Metrics1.png └── Solution-Architecture.png ├── NOTICE ├── .github └── PULL_REQUEST_TEMPLATE.md ├── Commands.txt ├── README.md └── LICENSE /code/extras/TestHarness_Input.json: -------------------------------------------------------------------------------- 1 | { 2 | "iterations": 1000, 3 | "operation": "load" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | lib/ 3 | code/PyMySQL-0.7.11.dist-info/ 4 | code/pymysql/ 5 | LambdaRDS_Demo_output.yaml 6 | 7 | -------------------------------------------------------------------------------- /images/Metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-manage-rds-connections/HEAD/images/Metrics.png -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Lambda Manage RDS Connections 2 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /images/CloudWatch-Metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-manage-rds-connections/HEAD/images/CloudWatch-Metrics.png -------------------------------------------------------------------------------- /images/CloudWatch-Metrics1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-manage-rds-connections/HEAD/images/CloudWatch-Metrics1.png -------------------------------------------------------------------------------- /images/Solution-Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-manage-rds-connections/HEAD/images/Solution-Architecture.png -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /Commands.txt: -------------------------------------------------------------------------------- 1 | aws s3 mb s3://rds-lambda-demo-2018-us-west-2 --region us-west-2 2 | 3 | aws cloudformation package --template-file LambdaRDS_Demo.yaml --output-template-file LambdaRDS_Demo_output.yaml --s3-bucket rds-lambda-demo-2018-us-west-2 --region us-west-2 4 | 5 | aws cloudformation deploy --template-file LambdaRDS_Demo_output.yaml --stack-name RDSLambdaDemoStack --capabilities CAPABILITY_IAM --parameter-overrides RDSUserName=DemoUser RDSPassword=Tester123 RDSDBName=TestDB --region us-west-2 -------------------------------------------------------------------------------- /code/source/LambdaRDS_TestHarness.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import boto3 3 | import botocore 4 | import json 5 | import os 6 | 7 | 8 | # Get the service resource. 9 | lambdaClient = boto3.client('lambda') 10 | 11 | testFunctionARN = os.environ['TEST_FUNCTION_ARN'] 12 | 13 | 14 | def invokeTestLambda(functionARN, operation, iterations): 15 | 16 | if operation == 'unit': 17 | iterations = 1 18 | for i in range(1, iterations): 19 | print ('Invoking test function: iteration ' + str(i)) 20 | lambdaClient.invoke( 21 | FunctionName=functionARN, 22 | InvocationType='Event' 23 | ) 24 | return True 25 | 26 | 27 | def lambda_handler(event, context): 28 | invokeTestLambda(testFunctionARN, str(event['operation']), int(event['iterations'])) 29 | print('Test Succeeded') 30 | return True 31 | -------------------------------------------------------------------------------- /code/source/LambdaRDS_Test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, '/opt') 3 | import pymysql 4 | import boto3 5 | import botocore 6 | import json 7 | import random 8 | import time 9 | import os 10 | from LambdaRDS_ManageConnections import * 11 | 12 | 13 | # rds settings 14 | rds_host = os.environ['RDS_HOST'] 15 | name = os.environ['RDS_USERNAME'] 16 | password = os.environ['RDS_PASSWORD'] 17 | db_name = os.environ['RDS_DB_NAME'] 18 | #helperFunctionARN = os.environ['HELPER_FUNCTION_ARN'] 19 | table = dynamodb.Table(os.environ['DDB_TABLE_NAME']) 20 | 21 | conn = None 22 | 23 | # Get the service resource. 24 | lambdaClient = boto3.client('lambda') 25 | 26 | 27 | def invokeConnCountManager(incrementCounter): 28 | result = False 29 | if (incrementCounter == True): 30 | result = checkConnectionCount("Prod_MySQL", table) 31 | else: 32 | result = returnConnectionToPool("Prod_MySQL", table) 33 | 34 | return result 35 | 36 | def openConnection(): 37 | global conn 38 | try: 39 | print("Opening Connection") 40 | if(conn is None): 41 | conn = pymysql.connect( 42 | rds_host, user=name, passwd=password, db=db_name, connect_timeout=5) 43 | elif (not conn.open): 44 | # print(conn.open) 45 | conn = pymysql.connect( 46 | rds_host, user=name, passwd=password, db=db_name, connect_timeout=5) 47 | 48 | except Exception as e: 49 | print (e) 50 | print("ERROR: Unexpected error: Could not connect to MySql instance.") 51 | raise e 52 | 53 | 54 | def lambda_handler(event, context): 55 | if invokeConnCountManager(True) == False: 56 | print ("Not enough Connections available.") 57 | return False 58 | 59 | item_count = 0 60 | try: 61 | openConnection() 62 | # Introducing artificial random delay to mimic actual DB query time. Remove this code for actual use. 63 | time.sleep(random.randint(1, 3)) 64 | with conn.cursor() as cur: 65 | cur.execute("select * from Employees") 66 | for row in cur: 67 | item_count += 1 68 | print(row) 69 | # print(row) 70 | except Exception as e: 71 | # Error while opening connection or processing 72 | print(e) 73 | finally: 74 | print("Closing Connection") 75 | if(conn is not None and conn.open): 76 | conn.close() 77 | invokeConnCountManager(False) 78 | 79 | return "Selected %d items from RDS MySQL table" % (item_count) 80 | -------------------------------------------------------------------------------- /code/lib/LambdaRDS_ManageConnections.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import boto3 3 | import botocore 4 | from datetime import datetime 5 | import os 6 | 7 | # Get the service resource. 8 | dynamodb = boto3.resource('dynamodb') 9 | cloudWatch = boto3.client('cloudwatch') 10 | table = dynamodb.Table(os.environ['DDB_TABLE_NAME']) 11 | 12 | 13 | def publishMetrics(connectionCount, errorCount, RDBMSName): 14 | #return 15 | cloudWatch.put_metric_data( 16 | Namespace='RDSLambda', 17 | MetricData=[ 18 | { 19 | 'MetricName': 'Remaining Connections', 20 | 'Dimensions': [ 21 | { 22 | 'Name': 'DBName', 23 | 'Value': RDBMSName 24 | }, 25 | ], 26 | 'Timestamp': datetime.now(), 27 | 'Value': connectionCount, 28 | 'Unit': 'Count', 29 | 'StorageResolution': 1 30 | }, 31 | { 32 | 'MetricName': 'NoConnLeftError', 33 | 'Dimensions': [ 34 | { 35 | 'Name': 'DBName', 36 | 'Value': RDBMSName 37 | }, 38 | ], 39 | 'Timestamp': datetime.now(), 40 | 'Value': errorCount, 41 | 'Unit': 'Count', 42 | 'StorageResolution': 1 43 | } 44 | ]) 45 | 46 | 47 | def checkConnectionCount(RDBMSName, table): 48 | allowConnection = True 49 | try: 50 | 51 | item = table.update_item( 52 | Key={ 53 | 'RDBMSName': RDBMSName 54 | }, 55 | UpdateExpression='SET RemainingConnections = RemainingConnections - :count', 56 | ConditionExpression='RemainingConnections > :minCount', 57 | ExpressionAttributeValues={ 58 | ':count': 1, 59 | ':minCount': 0 60 | }, 61 | ReturnValues='UPDATED_NEW' 62 | ) 63 | # Publish custom metrics 64 | publishMetrics(int(item['Attributes']['RemainingConnections']), 0, RDBMSName) 65 | 66 | # connection found, report no error 67 | # publishErrorMetric(0) 68 | # print ('RemainingConnections: ' + str(item['Attributes']['RemainingConnections'])) 69 | # print ("Borrow Connection: Total Connections remaining:{}".format(item['Attributes']['RemainingConnections'])) 70 | # print (item) 71 | except botocore.exceptions.ClientError as e: 72 | if e.response['Error']['Code'] == 'ConditionalCheckFailedException': 73 | # no connections left, publish 0 74 | # Publish Error Metric 75 | publishMetrics(0, 1, RDBMSName) 76 | allowConnection = False 77 | else: 78 | raise e 79 | return allowConnection 80 | 81 | 82 | def returnConnectionToPool(RDBMSName, table): 83 | connectionReturned = True 84 | try: 85 | item = table.update_item( 86 | Key={ 87 | 'RDBMSName': RDBMSName 88 | }, 89 | UpdateExpression='SET RemainingConnections = RemainingConnections + :count', 90 | ConditionExpression='RemainingConnections < MaxConnections', 91 | ExpressionAttributeValues={ 92 | ':count': 1 93 | }, 94 | ReturnValues='UPDATED_NEW' 95 | ) 96 | # Publish custom metric and no error 97 | publishMetrics(int(item['Attributes']['RemainingConnections']), 0, RDBMSName) 98 | # print ("Return Connection: Total Connections remaining:{}".format(item['Attributes']['RemainingConnections'])) 99 | # print (item) 100 | except botocore.exceptions.ClientError as e: 101 | if e.response['Error']['Code'] == 'ConditionalCheckFailedException': 102 | # All connections remaining, publish max 103 | # Publish Error 104 | publishMetrics(20, 1, RDBMSName) 105 | connectionReturned = False 106 | else: 107 | raise e 108 | return connectionReturned 109 | 110 | -------------------------------------------------------------------------------- /code/source/LambdaRDS_CFNInit.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | sys.path.insert(0, '/opt') 4 | import pymysql 5 | import boto3 6 | import botocore 7 | import json 8 | import os 9 | from botocore.vendored import requests 10 | 11 | # rds settings 12 | rds_host = os.environ['RDS_HOST'] 13 | name = os.environ['RDS_USERNAME'] 14 | password = os.environ['RDS_PASSWORD'] 15 | db_name = os.environ['RDS_DB_NAME'] 16 | 17 | SUCCESS = "SUCCESS" 18 | FAILED = "FAILED" 19 | 20 | 21 | conn = None 22 | 23 | # Get the service resource. 24 | lambdaClient = boto3.client('lambda') 25 | dynamodb = boto3.resource('dynamodb') 26 | table = dynamodb.Table(os.environ['DDB_TABLE_NAME']) 27 | 28 | 29 | def openConnection(): 30 | global conn 31 | try: 32 | #print("Opening Connection") 33 | if(conn is None): 34 | conn = pymysql.connect( 35 | rds_host, user=name, passwd=password, db=db_name, connect_timeout=5) 36 | elif (not conn.open): 37 | # print(conn.open) 38 | conn = pymysql.connect( 39 | rds_host, user=name, passwd=password, db=db_name, connect_timeout=5) 40 | 41 | except Exception as e: 42 | print (e) 43 | print("ERROR: Unexpected error: Could not connect to MySql instance.") 44 | raise e 45 | 46 | 47 | # CloudFormation uses a pre-signed S3 URL to receive the response back from the custom resources managed by it. This is a simple function 48 | # which shall be used to send the response back to CFN custom resource by performing PUT request to the pre-signed S3 URL. 49 | def sendResponse(event, context, responseStatus, responseData, physicalResourceId): 50 | responseUrl = event['ResponseURL'] 51 | 52 | # print responseUrl 53 | 54 | responseBody = {} 55 | responseBody['Status'] = responseStatus 56 | responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + \ 57 | context.log_stream_name 58 | responseBody['PhysicalResourceId'] = context.log_stream_name 59 | responseBody['StackId'] = event['StackId'] 60 | responseBody['RequestId'] = event['RequestId'] 61 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 62 | responseBody['Data'] = responseData 63 | 64 | json_responseBody = json.dumps(responseBody) 65 | 66 | print ("Response body:\n" + json_responseBody) 67 | 68 | headers = { 69 | 'content-type': '', 70 | 'content-length': str(len(json_responseBody)) 71 | } 72 | 73 | try: 74 | response = requests.put(responseUrl, 75 | data=json_responseBody, 76 | headers=headers) 77 | print ("Status code: " + response.reason) 78 | except Exception as e: 79 | print ("send(..) failed executing requests.put(..): " + str(e)) 80 | 81 | 82 | def lambda_handler(event, context): 83 | 84 | 85 | #print (event) 86 | 87 | json_requestBody = json.dumps(event) 88 | print ("Request body:\n" + json_requestBody) 89 | 90 | responseData = {} 91 | responseStatus = SUCCESS 92 | 93 | # For Delete requests, immediately send a SUCCESS response. 94 | if event['RequestType'] == 'Delete': 95 | sendResponse(event, context, responseStatus, responseData, None) 96 | return True 97 | try: 98 | 99 | # insert test data in RDS instance 100 | openConnection() 101 | 102 | with conn.cursor() as cur: 103 | # create table 104 | cur.execute( 105 | "Create Table if not exists Employees (EmployeeID int AUTO_INCREMENT Primary Key, FirstName varchar(50), LastName varchar(50))") 106 | 107 | # insert sample data 108 | cur.execute( 109 | "Insert into Employees (EmployeeID, FirstName, LastName) Values (null, \"John\", \"Smith\")") 110 | cur.execute( 111 | "Insert into Employees (EmployeeID, FirstName, LastName) Values (null, \"Jane\", \"Doe\")") 112 | cur.execute( 113 | "Insert into Employees (EmployeeID, FirstName, LastName) Values (null, \"Bob\", \"Rogers\")") 114 | conn.commit() 115 | 116 | print ('Created table Employees and inserted 3 rows.') 117 | 118 | # insert a row into DynamoDB 119 | table.put_item( 120 | Item={ 121 | 'RDBMSName': 'Prod_MySQL', 122 | 'MaxConnections': 50, 123 | 'RemainingConnections': 50 124 | }, 125 | ConditionExpression='attribute_not_exists(RDBMSName)' 126 | ) 127 | print ('Insert one item in ' + os.environ['DDB_TABLE_NAME']) 128 | except Exception as e: 129 | # Error while opening connection or processing 130 | print(e) 131 | responseStatus = FAILED 132 | finally: 133 | #print("Closing Connection") 134 | if(conn is not None and conn.open): 135 | conn.close() 136 | # send response back to CFN 137 | sendResponse(event, context, responseStatus, responseData, None) 138 | return True 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Connections Management for RDS/RDBMS solutions 2 | 3 | A connections management solution when using Lambda with RDS/RDBMS solutions. 4 | 5 | ## Background 6 | 7 | There are some inherent challenges with using AWS Lambda with RDS/RDBMS solutions, associated with connections management. The Lambda container model manages the containers lifecycle, which host lambda function invocations. Some of the features of this container model are: 8 | 1. Lambda containers lifecycle are managed by the AWS Lambda service. It doesn't provide developers any control or indication over creation and destruction of containers. 9 | 2. One container can only host one Lambda function invocation at a time, although they can be reused to execute multiple invocations of the same lambda function, serially. 10 | 3. Containers are neither shared across Lambda functions nor across AWS accounts. 11 | 4. Lambda has a 'global' section, which is executed once for the life time of the container. The 'handler' section is executed once per function invocation. 12 | 13 | The above features mean using traditional connection pooling with Lambda, to access RDS/RDBMS solutions, doesn't make too much sense. If the connection pool is initialized in the global section, with multiple connections, only one connection (per container) will actually be used and rest will just count towards the DB connections quota, unused. Also, there is no hook to tap into and destroy these connection pools, once the container reaches end of life. 14 | 15 | As a result, the best practice is to just use one connection per lambda container, initialized in the global section of the lambda function and relying on the DB server TTL to reclaim it. 16 | 17 | Lambda concurrency limits also help mitigate some of the challenges, but some still remain: 18 | 1. Using a global connection variable inside the handler ensures that any new connections stay open for subsequent calls. But in the case of MySQL and PostgreSQL there is risk of session leakage across the open connection that can result in locking issues. 19 | 2. Since the concurrency limit is applied to 'Account' level, you might have to create separate AWS accounts to segregate different serverless applications, which have different throttling requirements. 20 | 3. You can apply concurrency limits at the functional level too. In this case, concurrency limits should correspond to 'peak' invocations, which might not be known, and one might end up in overprovisioning / under provisioning of DB connections. 21 | 4. Lambda hotspots: From the DB perspective, if multiple lambda functions (or serverless applications) are accessing the DB, it becomes a challenge to dynamically allocate connections to a Lambda which is currently running 'hot', due to some external factors like time of the day/year, seasonal promotions, application dynamics etc. 22 | 23 | 24 | Dynamic Connections Management tries to address these challenges. 25 | 26 | ## Contents 27 | 28 | I have chosen Python as the language of choice and will be using MySQL RDS as a RDBMS solution. 29 | 30 | This repository contains the sample code for test and helper Lambda functions, DynamoDB table, MySQL RDS instance as well as a SAM template to deploy the resources in an AWS region of your choice. It also leverages CloudFormation custom resources to prepare the MySQL and DynamoDB instance with test data. 31 | 32 | # Solution Architecture 33 | 34 | 35 | ![Solution Architecture](images/Solution-Architecture.png) 36 | 37 | The solution consists of maintaining a row in a DynamoDB table, which keeps track of the 'Maximum allowed connections' and 'Connections in use' for a given DB. A helper function, packaged as a [Lambda Layer](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-lambda-now-supports-custom-runtimes-and-layers/), is used to manipulate this count. This helper function is called by the parent lambda(s), which wants to talk to the DB in question. The parent lambda function calls the helper once when it opens the connection and once when it closes the connection. To avoid any session leakage issues / locking issues, we open and close the connection in in each handler call, as opposed to maintaining a global connection variable. 38 | 39 | Depending on the response from the helper function (connections are available or not), the parent lambda function decides its course of action. The helper function also publishes 2 metrics to Cloudwatch: 'Remaining Connections' and 'No Connections Left Error', which can then be used to create an alarm and do something interesting, like backing off the load on the DB or providing an alternate source for querying the same data. 40 | 41 | While manipulating the count in DynamoDB, we make sure that we are using strongly consistent reads/writes by using [DynamoDB conditional writes](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.ConditionalUpdate). This means that multiple lambda functions can concurrently manipulate the count. We make sure that we are getting strongly consistent reads by setting the 'ReturnValues' set to 'UPDATED_NEW'. More details [here](https://docs.aws.amazon.com/cli/latest/reference/dynamodb/update-item.html). 42 | 43 | 44 | Below is a snapshot of a test run, blue shows the 'Remaining Connections' (left Y Axis) and orange shows the 'No Connection Left Error' (right Y axis). 45 | 46 | ![Solution Architecture](images/CloudWatch-Metrics1.png) 47 | 48 | You can now create alarms on these metrics and use them to do something interesting. 49 | 50 | # Deployment and Execution 51 | 52 | ## Prerequisites 53 | 54 | 1. Download and install the latest version of Python for your OS from [here](https://www.python.org/downloads/). We shall be using Python 3.6 and above. 55 | 56 | 2. You will be needing [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) as well. If you already have AWS CLI, please upgrade to a minimum version of 1.16.67. 57 | 1. ```bash 58 | pip install awscli --upgrade --user 59 | ``` 60 | Note: If you are using the Windows version of AWS CLI, please use the [Windows msi](https://docs.aws.amazon.com/cli/latest/userguide/install-windows.html#install-msi-on-windows) to upgrade your installation. 61 | 62 | ## Instructions 63 | 64 | This code depends on a bunch of libraries (not included in this distribution) which you will have to install yourself. The code comes with a SAM template, which you can use to deploy the entire solution. 65 | 66 | 1. Download the contents of this repository on your local machine (say: project-directory) 67 | 2. The solution is implemented in python, so make sure you have a working python environment on your local machine. 68 | 3. Open a command prompt, navigate to the project directory. Navigate to the /code/lib sub directory and install the following libraries: 69 | 1. ```bash 70 | pip install pymysql --target . 71 | ``` 72 | 73 | 4. Create a S3 bucket for deployment (note: use the same region throughout the following steps, I have used us-west-2, you can replace it with the region of your choice. Refer to the [region table](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) for service availability.) 74 | 1. ```bash 75 | aws s3 mb s3://rds-lambda-demo-2018-us-west-2 --region us-west-2 76 | ``` 77 | 78 | 5. Navigate to the /code/source sub directory. Package the contents and prepare deployment package using the following command 79 | 1. ```bash 80 | aws cloudformation package --template-file LambdaRDS_Demo.yaml --output-template-file LambdaRDS_Demo_output.yaml --s3-bucket rds-lambda-demo-2018-us-west-2 --region us-west-2 81 | ``` 82 | 6. The SAM template will also create a new MySQL RDS instance and auto-populate it with some test data. Replace the placeholders in the below command with username, password and dbname, and deploy the package: 83 | 1. ```bash 84 | aws cloudformation deploy --template-file LambdaRDS_Demo_output.yaml --stack-name RDSLambdaDemoStack --capabilities CAPABILITY_IAM --parameter-overrides RDSUserName=DemoUser RDSPassword=Tester123 RDSDBName=TestDB --region us-west-2 85 | ``` 86 | 7. If you want to make changes to the Lambda functions, you can do so on your local machine and redeploy them using the steps 5 through 7 above. The package and deploy commands take care of zipping up the new Lambda files (along with the dependencies) and uploading them to AWS for execution. 87 | 88 | ## Outputs 89 | Following are the outputs from the SAM template 90 | 91 | 1. **RDS MySQL Server:** You can use this endpoint to login to your RDS SQL Server. Please note, you might have to modify the security groups associated with your MySQL instance. 92 | 93 | ## Execution 94 | The TestHarness_Input.json file contains sample invocations that you can use to trigger the load test. Navigate to your Lambda console, look for the LambdaRDS_TestHarness function and create a test event. You can modify the test iterations count to your choice. 95 | 96 | Please note, the LambdaRDS_Test function has a 'sleep' function to simulate some random querying times for the query. You should comment this out for production deployments. 97 | 98 | Once the test finishes, you can navigate to your CloudWatch console and use 'RDSLambda' Dashboard (deployed as a part of the SAM template) to look at the metrics. You can then use these metrics to create an alarm. 99 | 100 | ![Metrics](images/Metrics.png) 101 | 102 | 103 | # Code Walkthrough 104 | 105 | 1. **LambdaRDS_Demo**: SAM template 106 | 2. **LambdaRDS_Test**: Test Lambda function to simulate opening and closing of DB connections. 107 | 3. **LambdaRDS_ManageConnections**: Helper library: Used to maintain an atomic counter in DynamoDB table, along with publishing metrics to cloudwatch. Should be called by #2 above before opening a connection and after closing a connection. 108 | 4. **LambdaRDSLayer**: Lambda Layer, containing the code for 'LambdaRDS_ManageConnections' helper function. 109 | 5. **LambdaRDS_TestHarness**: Test harness used to simulate load on LambdaRDS_Test function. 110 | 6. **TestHarness_Input**: Input for test harness function. 111 | 7. **LambdaRDS_CFNInit**: Custom resource lambda function used to insert test data into RDS and DynamoDB. Executed when the CloudFormation template is created, updated or deleted. 112 | 113 | ## Further Reading: 114 | 1. AWS re:Invent 2018 Chalktalk: [Best Practices for Using AWS Lambda with RDS/RDBMS Solutions (SRV301)](https://www.slideshare.net/AmazonWebServices/best-practices-for-using-aws-lambda-with-rdsrdbms-solutions-srv301r1-aws-reinvent-2018) 115 | 2. AWS Database Blog: [Query your AWS database from your serverless application](https://aws.amazon.com/blogs/database/query-your-aws-database-from-your-serverless-application/) 116 | 3. Whitepaper: [Security Overview of AWS Lambda](https://aws.amazon.com/whitepapers/#serverless) 117 | 118 | ## License 119 | 120 | This library is licensed under the Apache 2.0 License. 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /code/source/LambdaRDS_Demo.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Parameters: 5 | RDSUserName: 6 | Type: String 7 | RDSPassword: 8 | Type: String 9 | RDSDBName: 10 | Type: String 11 | 12 | Resources: 13 | 14 | # create VPC 15 | RDSVPC: 16 | Type: AWS::EC2::VPC 17 | Properties: 18 | CidrBlock: 172.31.0.0/16 19 | EnableDnsSupport: 'true' 20 | EnableDnsHostnames: 'true' 21 | Tags: 22 | - Key: Name 23 | Value: RDSVPC 24 | 25 | # create subnets 26 | privateDBSubnet1: 27 | Type: AWS::EC2::Subnet 28 | Properties: 29 | VpcId: !Ref RDSVPC 30 | CidrBlock: 172.31.0.0/20 31 | AvailabilityZone: !Select 32 | - 0 33 | - !GetAZs 34 | Ref: 'AWS::Region' 35 | Tags: 36 | - Key: Name 37 | Value: privateDBSubnet1 38 | 39 | privateDBSubnet2: 40 | Type: AWS::EC2::Subnet 41 | Properties: 42 | VpcId: !Ref RDSVPC 43 | CidrBlock: 172.31.16.0/20 44 | AvailabilityZone: !Select 45 | - 1 46 | - !GetAZs 47 | Ref: 'AWS::Region' 48 | Tags: 49 | - Key: Name 50 | Value: privateDBSubnet2 51 | 52 | privateLambdaSubnet1: 53 | Type: AWS::EC2::Subnet 54 | Properties: 55 | VpcId: !Ref RDSVPC 56 | CidrBlock: 172.31.32.0/20 57 | AvailabilityZone: !Select 58 | - 0 59 | - !GetAZs 60 | Ref: 'AWS::Region' 61 | Tags: 62 | - Key: Name 63 | Value: privateLambdaSubnet1 64 | 65 | privateLambdaSubnet2: 66 | Type: AWS::EC2::Subnet 67 | Properties: 68 | VpcId: !Ref RDSVPC 69 | CidrBlock: 172.31.48.0/20 70 | AvailabilityZone: !Select 71 | - 1 72 | - !GetAZs 73 | Ref: 'AWS::Region' 74 | Tags: 75 | - Key: Name 76 | Value: privateLambdaSubnet2 77 | 78 | publicSubnet1: 79 | Type: AWS::EC2::Subnet 80 | Properties: 81 | VpcId: !Ref RDSVPC 82 | CidrBlock: 172.31.64.0/20 83 | AvailabilityZone: !Select 84 | - 0 85 | - !GetAZs 86 | Ref: 'AWS::Region' 87 | Tags: 88 | - Key: Name 89 | Value: publicSubnet1 90 | 91 | publicSubnet2: 92 | Type: AWS::EC2::Subnet 93 | Properties: 94 | VpcId: !Ref RDSVPC 95 | CidrBlock: 172.31.80.0/20 96 | AvailabilityZone: !Select 97 | - 1 98 | - !GetAZs 99 | Ref: 'AWS::Region' 100 | Tags: 101 | - Key: Name 102 | Value: publicSubnet2 103 | 104 | # create and attach internet gateway 105 | # Internet gatewat and NAT are required so that Lambdas running inside VPC can invoke ManageConnections lambda function (running out of VPC) 106 | myInternetGateway: 107 | Type: AWS::EC2::InternetGateway 108 | Properties: 109 | Tags: 110 | - Key: Name 111 | Value: myInternetGateway 112 | 113 | AttachInternetGateway: 114 | Type: AWS::EC2::VPCGatewayAttachment 115 | Properties: 116 | VpcId: !Ref RDSVPC 117 | InternetGatewayId: !Ref myInternetGateway 118 | 119 | # create route tables 120 | CustomRouteTable: 121 | Type: AWS::EC2::RouteTable 122 | Properties: 123 | VpcId: !Ref RDSVPC 124 | Tags: 125 | - Key: Name 126 | Value: CustomRouteTable 127 | 128 | myRouteTable: 129 | Type: AWS::EC2::RouteTable 130 | Properties: 131 | VpcId: !Ref RDSVPC 132 | Tags: 133 | - Key: Name 134 | Value: myRouteTable 135 | 136 | # attach route tables 137 | mySubnetRouteTableAssociation1: 138 | Type: AWS::EC2::SubnetRouteTableAssociation 139 | Properties: 140 | SubnetId: !Ref privateDBSubnet1 141 | RouteTableId: !Ref myRouteTable 142 | 143 | mySubnetRouteTableAssociation2: 144 | Type: AWS::EC2::SubnetRouteTableAssociation 145 | Properties: 146 | SubnetId: !Ref privateDBSubnet2 147 | RouteTableId: !Ref myRouteTable 148 | 149 | mySubnetRouteTableAssociation3: 150 | Type: AWS::EC2::SubnetRouteTableAssociation 151 | Properties: 152 | SubnetId: !Ref privateLambdaSubnet1 153 | RouteTableId: !Ref myRouteTable 154 | 155 | mySubnetRouteTableAssociation4: 156 | Type: AWS::EC2::SubnetRouteTableAssociation 157 | Properties: 158 | SubnetId: !Ref privateLambdaSubnet2 159 | RouteTableId: !Ref myRouteTable 160 | 161 | # public subnets get attached to CustomRouteTable 162 | mySubnetRouteTableAssociation5: 163 | Type: AWS::EC2::SubnetRouteTableAssociation 164 | Properties: 165 | SubnetId: !Ref publicSubnet1 166 | RouteTableId: !Ref CustomRouteTable 167 | 168 | mySubnetRouteTableAssociation6: 169 | Type: AWS::EC2::SubnetRouteTableAssociation 170 | Properties: 171 | SubnetId: !Ref publicSubnet2 172 | RouteTableId: !Ref CustomRouteTable 173 | 174 | # create and attach NAT gateway 175 | myNAT: 176 | DependsOn: RDSVPC 177 | Type: AWS::EC2::NatGateway 178 | Properties: 179 | AllocationId: !GetAtt [myEIP,AllocationId] 180 | SubnetId: !Ref publicSubnet1 181 | 182 | myEIP: 183 | DependsOn: AttachInternetGateway 184 | Type: AWS::EC2::EIP 185 | Properties: 186 | Domain: vpc 187 | 188 | # create routes 189 | 190 | RouteToNAT: 191 | Type: AWS::EC2::Route 192 | Properties: 193 | RouteTableId: !Ref myRouteTable 194 | DestinationCidrBlock: 0.0.0.0/0 195 | NatGatewayId: !Ref myNAT 196 | 197 | RouteToInternet: 198 | Type: AWS::EC2::Route 199 | Properties: 200 | RouteTableId: !Ref CustomRouteTable 201 | DestinationCidrBlock: 0.0.0.0/0 202 | GatewayId: !Ref myInternetGateway 203 | 204 | # create security groups 205 | 206 | RDSSecurityGroup: 207 | Type: AWS::EC2::SecurityGroup 208 | Properties: 209 | GroupDescription: Allow My SQL access from lambda subnets 210 | VpcId: 211 | Ref: RDSVPC 212 | SecurityGroupIngress: 213 | - IpProtocol: tcp 214 | FromPort: '3306' 215 | ToPort: '3306' 216 | SourceSecurityGroupId : !Ref LambdaSecurityGroup 217 | Tags: 218 | - Key: Name 219 | Value: RDSSecurityGroup 220 | 221 | LambdaSecurityGroup: 222 | Type: AWS::EC2::SecurityGroup 223 | Properties: 224 | GroupDescription: Security group for Lambda ENIs 225 | VpcId: 226 | Ref: RDSVPC 227 | Tags: 228 | - Key: Name 229 | Value: LambdaSecurityGroup 230 | 231 | # Create Db subnet groups for RDS instance 232 | myDBSubnetGroup: 233 | Type: "AWS::RDS::DBSubnetGroup" 234 | Properties: 235 | DBSubnetGroupDescription: "description" 236 | SubnetIds: 237 | - !Ref privateDBSubnet1 238 | - !Ref privateDBSubnet2 239 | Tags: 240 | - 241 | Key: "Name" 242 | Value: "myDBSubnetGroup" 243 | 244 | # create IAM roles 245 | 246 | # Will be assumed by LambdaRDSTest and LambdaRDSTestHarness Lambda functions 247 | RDSLambdaTestRole: 248 | Type: "AWS::IAM::Role" 249 | Properties: 250 | AssumeRolePolicyDocument: 251 | Version: "2012-10-17" 252 | Statement: 253 | - 254 | Effect: "Allow" 255 | Principal: 256 | Service: 257 | - lambda.amazonaws.com 258 | Action: "sts:AssumeRole" 259 | Path: "/" 260 | Policies: 261 | - 262 | PolicyName: "AccessDDB" 263 | PolicyDocument: 264 | Version: "2012-10-17" 265 | Statement: 266 | - 267 | Effect: "Allow" 268 | Action: "dynamodb:*" 269 | Resource: [!GetAtt ConnectionsCounter.Arn] 270 | - 271 | PolicyName: "AllowMetricAdd" 272 | PolicyDocument: 273 | Version: "2012-10-17" 274 | Statement: 275 | - 276 | Effect: "Allow" 277 | Action: "cloudwatch:PutMetricData" 278 | Resource: "*" 279 | - 280 | PolicyName: "AllowInvoke" 281 | PolicyDocument: 282 | Version: "2012-10-17" 283 | Statement: 284 | - 285 | Effect: "Allow" 286 | Action: "lambda:InvokeFunction" 287 | Resource: "*" 288 | #- !GetAtt LambdaRDSManageConnections.Arn 289 | #- !GetAtt LambdaRDSTest.Arn 290 | ManagedPolicyArns: 291 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 292 | - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole 293 | 294 | # Will be assumed by RDSLambdaCFNInit Lambda function 295 | RDSLambdaCFNInitRole: 296 | Type: "AWS::IAM::Role" 297 | Properties: 298 | AssumeRolePolicyDocument: 299 | Version: "2012-10-17" 300 | Statement: 301 | - 302 | Effect: "Allow" 303 | Principal: 304 | Service: 305 | - lambda.amazonaws.com 306 | Action: "sts:AssumeRole" 307 | Path: "/" 308 | Policies: 309 | - 310 | PolicyName: "AccessDDB" 311 | PolicyDocument: 312 | Version: "2012-10-17" 313 | Statement: 314 | - 315 | Effect: "Allow" 316 | Action: "dynamodb:*" 317 | Resource: [!GetAtt ConnectionsCounter.Arn] 318 | ManagedPolicyArns: 319 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 320 | - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole 321 | 322 | 323 | ConnectionsCounter: 324 | Type: AWS::DynamoDB::Table 325 | Properties: 326 | ProvisionedThroughput: 327 | ReadCapacityUnits: 10 328 | WriteCapacityUnits: 10 329 | AttributeDefinitions: 330 | - 331 | AttributeName: "RDBMSName" 332 | AttributeType: "S" 333 | KeySchema: 334 | - 335 | AttributeName: "RDBMSName" 336 | KeyType: "HASH" 337 | 338 | LambdaRDSLayer: 339 | Type: AWS::Serverless::LayerVersion 340 | Properties: 341 | LayerName: MyLayer 342 | Description: Layer description 343 | ContentUri: '../lib' 344 | CompatibleRuntimes: 345 | - python3.6 346 | LicenseInfo: 'Available under the Apache 2.0 license.' 347 | RetentionPolicy: Retain 348 | 349 | LambdaRDSTest: 350 | Type: AWS::Serverless::Function 351 | Properties: 352 | Handler: LambdaRDS_Test.lambda_handler 353 | Description: "Test Lambda function to access a RDS Database and read sample data" 354 | Runtime: python3.6 355 | Role: !GetAtt RDSLambdaTestRole.Arn 356 | MemorySize: 128 357 | Timeout: 60 358 | Layers: 359 | - !Ref LambdaRDSLayer 360 | VpcConfig: # For accessing RDS instance 361 | SecurityGroupIds: 362 | - !Ref LambdaSecurityGroup 363 | SubnetIds: 364 | - !Ref privateLambdaSubnet1 365 | - !Ref privateLambdaSubnet2 366 | Environment: 367 | Variables: 368 | RDS_HOST: !GetAtt RDSMySQL.Endpoint.Address 369 | RDS_USERNAME: !Ref RDSUserName 370 | RDS_PASSWORD: !Ref RDSPassword 371 | RDS_DB_NAME: !Ref RDSDBName 372 | #HELPER_FUNCTION_ARN: !GetAtt LambdaRDSManageConnections.Arn 373 | DDB_TABLE_NAME: !Ref ConnectionsCounter 374 | 375 | LambdaRDSTestHarness: 376 | Type: AWS::Serverless::Function 377 | Properties: 378 | Handler: LambdaRDS_TestHarness.lambda_handler 379 | Description: "Provides a simple framework for conducting various tests of your Lambda functions" 380 | Runtime: python3.6 381 | Role: !GetAtt RDSLambdaTestRole.Arn 382 | MemorySize: 1024 383 | Timeout: 180 384 | Environment: 385 | Variables: 386 | TEST_FUNCTION_ARN: !GetAtt LambdaRDSTest.Arn 387 | 388 | LambdaRDSCFNInit: 389 | DependsOn: 390 | - myEIP # for deletion, this lambda function requires access to S3 bucket, hence this dependency. (chained to NAT and internet gateway) 391 | - mySubnetRouteTableAssociation1 392 | - mySubnetRouteTableAssociation2 393 | - mySubnetRouteTableAssociation3 394 | - mySubnetRouteTableAssociation4 395 | - mySubnetRouteTableAssociation5 396 | - mySubnetRouteTableAssociation6 397 | - RouteToInternet 398 | - RouteToNAT 399 | - RDSSecurityGroup 400 | Type: AWS::Serverless::Function 401 | Properties: 402 | Handler: LambdaRDS_CFNInit.lambda_handler 403 | Description: "Lambda function which will execute when this CFN template is created, updated or deleted" 404 | Runtime: python3.6 405 | Role: !GetAtt RDSLambdaCFNInitRole.Arn 406 | MemorySize: 128 407 | Timeout: 60 408 | Layers: 409 | - !Ref LambdaRDSLayer 410 | VpcConfig: # For accessing RDS instance 411 | SecurityGroupIds: 412 | - !Ref LambdaSecurityGroup 413 | SubnetIds: 414 | - !Ref privateLambdaSubnet1 415 | - !Ref privateLambdaSubnet2 416 | Environment: 417 | Variables: 418 | RDS_HOST: !GetAtt RDSMySQL.Endpoint.Address 419 | RDS_USERNAME: !Ref RDSUserName 420 | RDS_PASSWORD: !Ref RDSPassword 421 | RDS_DB_NAME: !Ref RDSDBName 422 | DDB_TABLE_NAME: !Ref ConnectionsCounter 423 | 424 | # Wire up the lambda function LambdaRDSCFNInit to execute on stack create, update or delete 425 | LambdaRDSCFnTrigger: 426 | Type: Custom::LambdaRDS 427 | Version: 1.0 428 | Properties: 429 | ServiceToken: !GetAtt [ LambdaRDSCFNInit, Arn ] 430 | 431 | 432 | RDSMySQL: 433 | Type: AWS::RDS::DBInstance 434 | Properties: 435 | AllocatedStorage: 5 436 | DBInstanceClass: db.m4.xlarge 437 | DBName: !Ref RDSDBName 438 | Engine: mysql 439 | MasterUsername: !Ref RDSUserName 440 | MasterUserPassword: !Ref RDSPassword 441 | MultiAZ: False 442 | PubliclyAccessible: False 443 | StorageType: gp2 444 | DBSubnetGroupName: !Ref myDBSubnetGroup 445 | VPCSecurityGroups: 446 | - !Ref RDSSecurityGroup 447 | DeletionPolicy: Delete 448 | 449 | RDSLambdaDashboard: 450 | Type: AWS::CloudWatch::Dashboard 451 | Properties: 452 | # DashboardName: 'RDSLambda' 453 | DashboardBody: !Sub '{ "widgets": [ {"type":"metric", "x":1, "y":0, "width":24, "height":12, "properties": { "metrics": [ [ "RDSLambda", "Remaining Connections", "DBName", "Prod_MySQL", { "stat": "Maximum" } ], [ ".", "NoConnLeftError", ".", ".", { "yAxis": "right" } ] ], "view": "timeSeries", "stacked": false, "region": "${AWS::Region}", "stat": "Sum", "period": 1, "annotations": { "horizontal": [ { "label": "Max Connections Available", "value": 50, "fill": "below" } ] }, "title": "RDS Lambda", "yAxis": { "left": { "label": "Remaining Connections" }, "right": { "label": "No Connections Error" } }, "start": "-PT1M", "end": "P0D" } }] }' 454 | 455 | 456 | Outputs: 457 | RDSMySQLEndPoint: 458 | Description: The endpoint of the RDS MySQL instance 459 | Value: !GetAtt RDSMySQL.Endpoint.Address 460 | 461 | --------------------------------------------------------------------------------