├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __init__.py ├── events └── event.json ├── images ├── architecture.png ├── artillery.png └── metrics.png ├── install.sh ├── load-no-proxy.yml ├── load-proxy.yml ├── package-lock.json ├── rds-with-proxy.yaml ├── rds ├── __init__.py ├── app.py └── requirements.txt ├── rdsproxy ├── __init__.py ├── app.py └── requirements.txt └── template.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | /.aws-sam/ 246 | /.idea/ 247 | /samconfig.toml 248 | /samconfig-rds.toml 249 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-rds-proxy-demo 2 | 3 | This project demos benefits of using RDS proxy with serverless workload which depends on relational database like RDS Aurora. 4 | Project shows end to end automated setup of RDS Aurora(Mysql) with RDS proxy. Basic serverless architecture is set up 5 | using API gateway HTTP API and Lambda Functions. 6 | 7 | Project sets up two endpoints with HTTP API, one which talks directly to RDS Aurora cluster and the other which talks 8 | via RDS Proxy. It provides load testing setup to measure the benefits of using RDS proxy in terms of connection pooling 9 | and elasticity. 10 | 11 | This project assumes you already have RDS Aurora Mysql cluster up and running. An RDS proxy instance 12 | is also setup with force IAM authentication enabled. You can choose to create rds cluster with proxy following 13 | steps [below](#deploy-rds-aurora-cluster-with-rds-proxy) to have aurora cluster and 14 | RDS proxy setup. 15 | 16 | ## Architecture 17 | 18 | ![img.png](images/architecture.png) 19 | 20 | 21 | ## Deploy the sample application 22 | 23 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. 24 | It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. 25 | 26 | To use the SAM CLI, you need the following tools. 27 | 28 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 29 | * [Python 3.9 installed](https://www.python.org/downloads/) 30 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 31 | 32 | ## Deploy RDS Aurora Cluster with RDS Proxy 33 | 34 | **Note:** If you have already provisioned RDS Aurora cluster with RDS Proxy, you can skip 35 | this step and follow [these steps](#deploy-serverless-workload-using-rds-aurora-as-backend) instead. 36 | 37 | This stack will take care of provisioning RDS Aurora Mysql along with RDS proxy fronting it inside 38 | a VPC with 3 private subnet. Required parameters needed by [next step](#deploy-serverless-workload-using-rds-aurora-as-backend) 39 | is also provided as stack output. 40 | 41 | ```bash 42 | sam build -t rds-with-proxy.yaml --use-container 43 | sam deploy -t rds-with-proxy.yaml --guided 44 | ``` 45 | ## Deploy serverless workload using RDS Aurora as backend 46 | 47 | To build and deploy your application for the first time, run the following in your shell: 48 | Pass required parameters during guided deploy. 49 | 50 | ```bash 51 | sam build --use-container 52 | sam deploy --guided 53 | ``` 54 | 55 | 56 | ## Load testing 57 | 58 | ### Installing artillery 59 | 60 | We will use [artillery](https://artillery.io/docs/guides/overview/welcome.html) to generate some load towards both the apis. 61 | Install Artillery via npm: 62 | 63 | ``` 64 | npm install -g artillery@latest 65 | ``` 66 | 67 | ### Checking your installation 68 | 69 | If you used npm to install Artillery globally, run the following command in your preferred command line interface: 70 | 71 | ``` 72 | artillery dino 73 | ``` 74 | 75 | You should see an ASCII dinosaur printed to the terminal. Something like this: 76 | 77 | ![img.png](images/artillery.png) 78 | 79 | ### Testing 80 | 81 | Before starting load testing, make sure `target` in files `load-no-proxy.yml` and `load-proxy.yml` is update with the 82 | created HTTP API endpoint. The endpoint is also provided as stack output `ApiBasePath` when 83 | executing [above steps](#deploy-serverless-workload-using-rds-aurora-as-backend). You can generate load on both the APIs via: 84 | 85 | ``` 86 | artillery run load-no-proxy.yml 87 | ``` 88 | 89 | ``` 90 | artillery run load-proxy.yml 91 | ``` 92 | 93 | For a sample load which ran 300 seconds with arrival rate for users at 100, i.e. 30000 request over 5 minutes, below is 94 | comparison of number of database connection used when connecting via RDS proxy vs directly to RDS Aurora cluster endpoint. 95 | 96 | ![img.png](images/metrics.png) 97 | 98 | 99 | ## Security 100 | 101 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 102 | 103 | ## License 104 | 105 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-rds-proxy-demo/2686ff51232f98256f013a73593dd141bc1d6bd5/__init__.py -------------------------------------------------------------------------------- /events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"message\": \"hello world\"}", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "requestTime": "09/Apr/2015:12:34:56 +0000", 42 | "requestTimeEpoch": 1428582896000, 43 | "identity": { 44 | "cognitoIdentityPoolId": null, 45 | "accountId": null, 46 | "cognitoIdentityId": null, 47 | "caller": null, 48 | "accessKey": null, 49 | "sourceIp": "127.0.0.1", 50 | "cognitoAuthenticationType": null, 51 | "cognitoAuthenticationProvider": null, 52 | "userArn": null, 53 | "userAgent": "Custom User Agent String", 54 | "user": null 55 | }, 56 | "path": "/prod/path/to/resource", 57 | "resourcePath": "/{proxy+}", 58 | "httpMethod": "POST", 59 | "apiId": "1234567890", 60 | "protocol": "HTTP/1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-rds-proxy-demo/2686ff51232f98256f013a73593dd141bc1d6bd5/images/architecture.png -------------------------------------------------------------------------------- /images/artillery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-rds-proxy-demo/2686ff51232f98256f013a73593dd141bc1d6bd5/images/artillery.png -------------------------------------------------------------------------------- /images/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-rds-proxy-demo/2686ff51232f98256f013a73593dd141bc1d6bd5/images/metrics.png -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | sudo apt install awscli 2 | sudo apt install docker.io 3 | sudo apt install unzip 4 | sudo apt install npm 5 | curl -L https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip -o aws-sam-cli-linux-x86_64.zip 6 | unzip aws-sam-cli-linux-x86_64.zip -d sam-installation 7 | sudo ./sam-installation/install 8 | which sam 9 | sam --version 10 | sudo npm install n -g 11 | sudo n stable 12 | PATH="$PATH" 13 | sudo npm install -g artillery@latest 14 | artillery dino 15 | sudo usermod -aG docker $USER 16 | newgrp docker -------------------------------------------------------------------------------- /load-no-proxy.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "https://jczw4vfi35.execute-api.us-east-1.amazonaws.com" 3 | phases: 4 | - duration: 120 5 | arrivalRate: 100 6 | 7 | scenarios: 8 | - name: "Call Function backed by RDS without using RDS Proxy" 9 | flow: 10 | - get: 11 | url: "/no-proxy" -------------------------------------------------------------------------------- /load-proxy.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "https://jczw4vfi35.execute-api.us-east-1.amazonaws.com" 3 | phases: 4 | - duration: 120 5 | arrivalRate: 100 6 | 7 | scenarios: 8 | - name: "Call Function backed by RDS using RDS Proxy" 9 | flow: 10 | - get: 11 | url: "/proxy" -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-rds-proxy-demo", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /rds-with-proxy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: 2010-09-09 3 | Description: Amazon Aurora MySQL and RDS Proxy Setup 4 | 5 | Mappings: 6 | NetworkSettings: 7 | global: 8 | vpcCidr: 172.31.0.0/16 9 | subPrv1Cidr: 172.31.0.0/24 10 | subPrv2Cidr: 172.31.1.0/24 11 | subPrv3Cidr: 172.31.2.0/24 12 | ClusterSettings: 13 | global: 14 | dbSchema: mylab 15 | dbVersion: 5.7.mysql_aurora.2.09.1 16 | dbEngine: aurora-mysql 17 | dbFamily: aurora-mysql5.7 18 | port: 3306 19 | nodeType: db.r5.large 20 | 21 | Resources: 22 | vpc: 23 | Type: "AWS::EC2::VPC" 24 | Properties: 25 | EnableDnsSupport: true 26 | EnableDnsHostnames: true 27 | InstanceTenancy: default 28 | CidrBlock: !FindInMap [ NetworkSettings, global, vpcCidr ] 29 | Tags: 30 | - Key: Name 31 | Value: !Sub "${AWS::StackName}-vpc" 32 | 33 | sub1Private: 34 | Type: "AWS::EC2::Subnet" 35 | Properties: 36 | VpcId: !Ref vpc 37 | CidrBlock: !FindInMap [ NetworkSettings, global, subPrv1Cidr ] 38 | AvailabilityZone: !Join [ "", [ !Ref "AWS::Region", a ]] 39 | MapPublicIpOnLaunch: false 40 | Tags: 41 | - Key: Name 42 | Value: !Sub "${AWS::StackName}-prv-sub-1" 43 | 44 | sub2Private: 45 | Type: "AWS::EC2::Subnet" 46 | Properties: 47 | VpcId: !Ref vpc 48 | CidrBlock: !FindInMap [ NetworkSettings, global, subPrv2Cidr ] 49 | AvailabilityZone: !Join [ "", [ !Ref "AWS::Region", b ]] 50 | MapPublicIpOnLaunch: false 51 | Tags: 52 | - Key: Name 53 | Value: !Sub "${AWS::StackName}-prv-sub-2" 54 | 55 | sub3Private: 56 | Type: "AWS::EC2::Subnet" 57 | Properties: 58 | VpcId: !Ref vpc 59 | CidrBlock: !FindInMap [ NetworkSettings, global, subPrv3Cidr ] 60 | AvailabilityZone: !Join [ "", [ !Ref "AWS::Region", c ]] 61 | MapPublicIpOnLaunch: false 62 | Tags: 63 | - Key: Name 64 | Value: !Sub "${AWS::StackName}-prv-sub-3" 65 | 66 | lambdaSg: 67 | Type: AWS::EC2::SecurityGroup 68 | Properties: 69 | GroupDescription: Security Groups for the AWS Lambda for accessing RDS/Proxy 70 | GroupName: 'lambda-sg' 71 | SecurityGroupEgress: 72 | - CidrIp: "0.0.0.0/0" 73 | FromPort: 0 74 | ToPort: 65535 75 | IpProtocol: tcp 76 | SecurityGroupIngress: 77 | - CidrIp: "0.0.0.0/0" 78 | FromPort: 0 79 | ToPort: 65535 80 | IpProtocol: tcp 81 | VpcId: !Ref vpc 82 | 83 | dbClusterSecGroup: 84 | Type: "AWS::EC2::SecurityGroup" 85 | Properties: 86 | VpcId: !Ref vpc 87 | GroupName: !Sub "${AWS::StackName}-database-sg" 88 | GroupDescription: "security group (firewall)" 89 | Tags: 90 | - Key: Name 91 | Value: !Sub "${AWS::StackName}-database-sg" 92 | SecurityGroupIngress: 93 | - IpProtocol: tcp 94 | FromPort: !FindInMap [ ClusterSettings, global, port ] 95 | ToPort: !FindInMap [ ClusterSettings, global, port ] 96 | SourceSecurityGroupId: !GetAtt lambdaSg.GroupId 97 | - IpProtocol: -1 98 | FromPort: -1 99 | ToPort: -1 100 | SourceSecurityGroupId: !GetAtt lambdaSg.GroupId 101 | 102 | ruleDbClusterSecGroupIngressSelf: 103 | Type: "AWS::EC2::SecurityGroupIngress" 104 | Properties: 105 | GroupId: !Ref dbClusterSecGroup 106 | IpProtocol: -1 107 | Description: "Allows all inbound access from sources with the same security group" 108 | SourceSecurityGroupId: !Ref dbClusterSecGroup 109 | 110 | secretClusterMasterUser: 111 | Type: "AWS::SecretsManager::Secret" 112 | Properties: 113 | Description: !Sub "Master user credentials for DB cluster '${AWS::StackName}-mysql-cluster'" 114 | GenerateSecretString: 115 | SecretStringTemplate: '{"username": "masteruser"}' 116 | GenerateStringKey: 'password' 117 | PasswordLength: 10 118 | ExcludeCharacters: '"@/\$`&:{}()[]' 119 | Tags: 120 | - Key: Name 121 | Value: !Sub "${AWS::StackName}-cluster-secret" 122 | 123 | secretManagerVpcEndpoint: 124 | Type: "AWS::EC2::VPCEndpoint" 125 | Properties: 126 | VpcId: !Ref vpc 127 | SubnetIds: 128 | - !Ref sub1Private 129 | - !Ref sub2Private 130 | - !Ref sub3Private 131 | PrivateDnsEnabled: true 132 | ServiceName: !Sub "com.amazonaws.${AWS::Region}.secretsmanager" 133 | SecurityGroupIds: 134 | - !Ref dbClusterSecGroup 135 | VpcEndpointType: Interface 136 | PolicyDocument: 137 | Version: 2012-10-17 138 | Statement: 139 | - Principal: "*" 140 | Effect: Allow 141 | Action: 142 | - secretsmanager:GetSecretValue 143 | Resource: 144 | - !Ref secretClusterMasterUser 145 | 146 | roleEnhancedMonitoring: 147 | Type: "AWS::IAM::Role" 148 | Properties: 149 | RoleName: !Sub "${AWS::StackName}-monitor-${AWS::Region}" 150 | Description: "Allows your Aurora DB cluster to deliver Enhanced Monitoring metrics." 151 | AssumeRolePolicyDocument: 152 | Version: 2012-10-17 153 | Statement: 154 | - Effect: Allow 155 | Action: 156 | - "sts:AssumeRole" 157 | Principal: 158 | Service: 159 | - "monitoring.rds.amazonaws.com" 160 | ManagedPolicyArns: 161 | - "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" 162 | Tags: 163 | - Key: Name 164 | Value: !Sub "${AWS::StackName}-monitor-${AWS::Region}" 165 | 166 | pgNodeParams: 167 | Type: "AWS::RDS::DBParameterGroup" 168 | Properties: 169 | Description: !Sub "${AWS::StackName}-mysql-node-params" 170 | Family: !FindInMap [ ClusterSettings, global, dbFamily ] 171 | Parameters: 172 | innodb_stats_persistent_sample_pages: "256" 173 | slow_query_log: "1" 174 | long_query_time: "10" 175 | log_output: FILE 176 | Tags: 177 | - Key: Name 178 | Value: !Sub "${AWS::StackName}-mysql-node-params" 179 | dbSubnets: 180 | Type: "AWS::RDS::DBSubnetGroup" 181 | Properties: 182 | DBSubnetGroupName: !Sub "${AWS::StackName}-db-subnet-group" 183 | DBSubnetGroupDescription: "subnets allowed for deploying DB instances" 184 | SubnetIds: [ !Ref sub1Private, !Ref sub2Private, !Ref sub3Private ] 185 | Tags: 186 | - Key: Name 187 | Value: !Sub "${AWS::StackName}-db-subnet-group" 188 | 189 | dbCluster: 190 | Type: "AWS::RDS::DBCluster" 191 | Properties: 192 | Engine: !FindInMap [ ClusterSettings, global, dbEngine ] 193 | EngineVersion: !FindInMap [ ClusterSettings, global, dbVersion ] 194 | DBSubnetGroupName: !Ref dbSubnets 195 | DBClusterIdentifier: !Sub "${AWS::StackName}-mysql-cluster" 196 | BackupRetentionPeriod: 1 197 | MasterUsername: !Join [ "", [ "{{resolve:secretsmanager:", !Ref secretClusterMasterUser, ":SecretString:username}}" ] ] 198 | MasterUserPassword: !Join [ "", [ "{{resolve:secretsmanager:", !Ref secretClusterMasterUser, ":SecretString:password}}" ] ] 199 | DatabaseName: !FindInMap [ ClusterSettings, global, dbSchema ] 200 | StorageEncrypted: true 201 | VpcSecurityGroupIds: [ !Ref dbClusterSecGroup ] 202 | EnableCloudwatchLogsExports: [ error, slowquery ] 203 | BacktrackWindow: 86400 204 | EnableIAMDatabaseAuthentication: true 205 | Tags: 206 | - Key: Name 207 | Value: !Sub "${AWS::StackName}-mysql-cluster" 208 | 209 | dbNode1: 210 | Type: "AWS::RDS::DBInstance" 211 | Properties: 212 | DBClusterIdentifier: !Ref dbCluster 213 | DBInstanceIdentifier: !Sub "${AWS::StackName}-mysql-node-1" 214 | CopyTagsToSnapshot: true 215 | DBInstanceClass: !FindInMap [ ClusterSettings, global, nodeType ] 216 | DBParameterGroupName: !Ref pgNodeParams 217 | Engine: !FindInMap [ ClusterSettings, global, dbEngine ] 218 | MonitoringInterval: 1 219 | MonitoringRoleArn: !GetAtt roleEnhancedMonitoring.Arn 220 | PubliclyAccessible: false 221 | EnablePerformanceInsights: true 222 | PerformanceInsightsRetentionPeriod: 7 223 | Tags: 224 | - Key: Name 225 | Value: !Sub "${AWS::StackName}-mysql-node-1" 226 | 227 | dbNode2: 228 | Type: "AWS::RDS::DBInstance" 229 | Properties: 230 | DBClusterIdentifier: !Ref dbCluster 231 | DBInstanceIdentifier: !Sub "${AWS::StackName}-mysql-node-2" 232 | CopyTagsToSnapshot: true 233 | DBInstanceClass: !FindInMap [ ClusterSettings, global, nodeType ] 234 | DBParameterGroupName: !Ref pgNodeParams 235 | Engine: !FindInMap [ ClusterSettings, global, dbEngine ] 236 | MonitoringInterval: 1 237 | MonitoringRoleArn: !GetAtt roleEnhancedMonitoring.Arn 238 | PubliclyAccessible: false 239 | EnablePerformanceInsights: true 240 | PerformanceInsightsRetentionPeriod: 7 241 | Tags: 242 | - Key: Name 243 | Value: !Sub "${AWS::StackName}-mysql-node-2" 244 | 245 | dbProxyRole: 246 | Type: AWS::IAM::Role 247 | Properties: 248 | Path: / 249 | AssumeRolePolicyDocument: 250 | Version: '2012-10-17' 251 | Statement: 252 | - Action: [ 'sts:AssumeRole' ] 253 | Effect: Allow 254 | Principal: 255 | Service: [ rds.amazonaws.com ] 256 | Policies: 257 | - PolicyName: DBProxyPolicy 258 | PolicyDocument: 259 | Version: '2012-10-17' 260 | Statement: 261 | - Action: 262 | - secretsmanager:GetSecretValue 263 | Effect: Allow 264 | Resource: 265 | - !Ref secretClusterMasterUser 266 | 267 | dbProxy: 268 | Type: AWS::RDS::DBProxy 269 | Properties: 270 | Auth: 271 | - { AuthScheme: SECRETS, SecretArn: !Ref secretClusterMasterUser, IAMAuth: REQUIRED } 272 | DBProxyName: 'rds-proxy' 273 | EngineFamily: 'MYSQL' 274 | RoleArn: !GetAtt dbProxyRole.Arn 275 | IdleClientTimeout: 120 276 | RequireTLS: true 277 | DebugLogging: false 278 | VpcSubnetIds: 279 | - !Ref sub1Private 280 | - !Ref sub2Private 281 | - !Ref sub3Private 282 | VpcSecurityGroupIds: 283 | - !GetAtt dbClusterSecGroup.GroupId 284 | 285 | proxyTargetGroup: 286 | Type: AWS::RDS::DBProxyTargetGroup 287 | DependsOn: 288 | - dbCluster 289 | - dbNode1 290 | - dbNode2 291 | Properties: 292 | DBProxyName: !Ref dbProxy 293 | DBClusterIdentifiers: [ !Ref dbCluster ] 294 | TargetGroupName: default 295 | ConnectionPoolConfigurationInfo: 296 | MaxConnectionsPercent: 5 297 | MaxIdleConnectionsPercent: 4 298 | ConnectionBorrowTimeout: 120 299 | 300 | Outputs: 301 | vpcId: 302 | Description: "VPC id" 303 | Value: !Ref vpc 304 | sub1Private: 305 | Description: "Private subnet 1" 306 | Value: !Ref sub1Private 307 | sub2Private: 308 | Description: "Private subnet 2" 309 | Value: !Ref sub2Private 310 | sub3Private: 311 | Description: "Private subnet 3" 312 | Value: !Ref sub3Private 313 | clusterEndpoint: 314 | Description: "Cluster Endpoint" 315 | Value: !GetAtt dbCluster.Endpoint.Address 316 | readerEndpoint: 317 | Description: "Aurora Reader Endpoint" 318 | Value: !GetAtt dbCluster.ReadEndpoint.Address 319 | secretArn: 320 | Description: "Database Credentials Secret ARN" 321 | Value: !Ref secretClusterMasterUser 322 | lambdaSgGroupId: 323 | Description: "Security group id to use on lambda" 324 | Value: !GetAtt lambdaSg.GroupId 325 | databasePort: 326 | Description: "Database port" 327 | Value: !FindInMap [ ClusterSettings, global, port ] 328 | rdsProxyEndpoint: 329 | Description: "Proxy writer endpoint" 330 | Value: !GetAtt dbProxy.Endpoint 331 | dbProxyResourceId: 332 | Description: "Proxy Resource ID" 333 | Value: !Select [6, !Split [":", !GetAtt dbProxy.DBProxyArn]] -------------------------------------------------------------------------------- /rds/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-rds-proxy-demo/2686ff51232f98256f013a73593dd141bc1d6bd5/rds/__init__.py -------------------------------------------------------------------------------- /rds/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from os import environ 4 | from aws_lambda_powertools.utilities import parameters 5 | 6 | import pymysql 7 | 8 | 9 | client = boto3.client('rds') # get the rds object 10 | 11 | 12 | def db_ops(): 13 | secret = parameters.get_secret(environ.get('secret_arn'), transform='json', max_age=60) 14 | username = secret.get('username') 15 | password = secret.get('password') 16 | 17 | try: 18 | # create a connection object 19 | connection = pymysql.connect( 20 | host=environ.get('rds_endpoint'), 21 | # getting the rds proxy endpoint from the environment variables 22 | user=username, 23 | password=password, 24 | db=environ.get('database'), 25 | charset='utf8mb4', 26 | cursorclass=pymysql.cursors.DictCursor, 27 | ssl={"use": True} 28 | ) 29 | except pymysql.MySQLError as e: 30 | return e 31 | 32 | return connection 33 | 34 | 35 | def lambda_handler(event, context): 36 | conn = db_ops() 37 | cursor = conn.cursor() 38 | query = "select curdate() from dual" 39 | cursor.execute(query) 40 | results = cursor.fetchmany(1) 41 | 42 | return { 43 | 'statusCode': 200, 44 | 'body': json.dumps(results, default=str) 45 | } 46 | -------------------------------------------------------------------------------- /rds/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql 2 | aws-lambda-powertools -------------------------------------------------------------------------------- /rdsproxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-rds-proxy-demo/2686ff51232f98256f013a73593dd141bc1d6bd5/rdsproxy/__init__.py -------------------------------------------------------------------------------- /rdsproxy/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import boto3 3 | from os import environ 4 | 5 | import pymysql 6 | 7 | 8 | client = boto3.client('rds') # get the rds object 9 | 10 | 11 | def create_proxy_connection_token(username): 12 | # get the required parameters to create a token 13 | region = environ.get('region') # get the region 14 | hostname = environ.get('rds_endpoint') # get the rds proxy endpoint 15 | port = environ.get('port') # get the database port 16 | 17 | # generate the authentication token -- temporary password 18 | token = client.generate_db_auth_token( 19 | DBHostname=hostname, 20 | Port=port, 21 | DBUsername=username, 22 | Region=region 23 | ) 24 | 25 | return token 26 | 27 | 28 | def db_ops(): 29 | username = environ.get('username') 30 | 31 | token = create_proxy_connection_token(username) 32 | 33 | try: 34 | # create a connection object 35 | connection = pymysql.connect( 36 | host=environ.get('rds_endpoint'), 37 | # getting the rds proxy endpoint from the environment variables 38 | user=username, 39 | password=token, 40 | db=environ.get('database'), 41 | charset='utf8mb4', 42 | cursorclass=pymysql.cursors.DictCursor, 43 | ssl={"use": True} 44 | ) 45 | except pymysql.MySQLError as e: 46 | print(e) 47 | return e 48 | 49 | return connection 50 | 51 | 52 | def lambda_handler(event, context): 53 | conn = db_ops() 54 | cursor = conn.cursor() 55 | query = "select curdate() from dual" 56 | cursor.execute(query) 57 | results = cursor.fetchmany(1) 58 | 59 | return { 60 | 'statusCode': 200, 61 | 'body': json.dumps(results, default=str) 62 | } 63 | -------------------------------------------------------------------------------- /rdsproxy/requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | serverless-rds-proxy-demo 5 | 6 | Sample SAM Template for serverless-rds-proxy-demo 7 | 8 | Parameters: 9 | Vpc: 10 | Type: AWS::EC2::VPC::Id 11 | Description: "Vpc where function will be deployed" 12 | Subnets: 13 | Type: List 14 | Description: "Subnets where function will be deployed. Provide at least two" 15 | RdsProxyEndpoint: 16 | Type: String 17 | Description: "RDS Proxy endpoint configured. This should be Read/Write endpoint." 18 | RdsEndpoint: 19 | Type: String 20 | Description: "RDS Aurora Cluster Endpoint." 21 | Port: 22 | Type: Number 23 | Description: "Database port. For Mysql 3306 is default." 24 | Default: 3306 25 | SecretArn: 26 | Type: String 27 | Description: "Secret ARN where database credentials are stored." 28 | ProxyResourceId: 29 | Type: String 30 | Description: "RDS Proxy resource id. This is last part of RDS proxy ARN, ex: prx-. Its required to configure needed permission by the lambda functions." 31 | CreateFunctionSecurityGroup: 32 | Type: String 33 | AllowedValues: ['True', 'False'] 34 | Default: 'True' 35 | Description: "Should a security group for function be created? Set value as True only if you do not already have specified LambdaSecurityGroupId. Make sure traffic from this SG is allowed in database and proxy security group." 36 | LambdaSecurityGroupId: 37 | Type: String 38 | Default: "" 39 | Description: "Security group id for lambda function. Make sure traffic from this SG is allowed in database and proxy security group." 40 | 41 | Conditions: 42 | CreateLambdaSg: !Equals ["True", !Ref CreateFunctionSecurityGroup] 43 | 44 | Globals: 45 | Function: 46 | Timeout: 30 47 | 48 | Resources: 49 | LambdaSg: 50 | Type: AWS::EC2::SecurityGroup 51 | Condition: CreateLambdaSg 52 | Properties: 53 | GroupDescription: Security Groups for the AWS Lambda for accessing RDS/Proxy 54 | GroupName: 'lambda-sg' 55 | SecurityGroupEgress: 56 | - CidrIp: "0.0.0.0/0" 57 | FromPort: 0 58 | ToPort: 65535 59 | IpProtocol: tcp 60 | SecurityGroupIngress: 61 | - CidrIp: "0.0.0.0/0" 62 | FromPort: 0 63 | ToPort: 65535 64 | IpProtocol: tcp 65 | VpcId: !Ref Vpc 66 | 67 | SampleHttpApi: 68 | Type: AWS::Serverless::HttpApi 69 | 70 | RdsFunction: 71 | Type: AWS::Serverless::Function 72 | Properties: 73 | CodeUri: rds/ 74 | Handler: app.lambda_handler 75 | Runtime: python3.9 76 | VpcConfig: 77 | SecurityGroupIds: 78 | - !If [CreateLambdaSg, !Ref LambdaSg, !Ref LambdaSecurityGroupId] 79 | SubnetIds: !Ref Subnets 80 | Policies: 81 | - AWSSecretsManagerGetSecretValuePolicy: 82 | SecretArn: 83 | !Ref SecretArn 84 | Environment: 85 | Variables: 86 | region: !Ref AWS::Region 87 | rds_endpoint: !Ref RdsEndpoint 88 | port: !Ref Port 89 | secret_arn: !Ref SecretArn 90 | Events: 91 | NoProxy: 92 | Type: HttpApi 93 | Properties: 94 | Path: /no-proxy 95 | Method: get 96 | ApiId: !Ref SampleHttpApi 97 | 98 | RdsProxyFunction: 99 | Type: AWS::Serverless::Function 100 | Properties: 101 | CodeUri: rdsproxy/ 102 | Handler: app.lambda_handler 103 | Runtime: python3.9 104 | VpcConfig: 105 | SecurityGroupIds: 106 | - !If [CreateLambdaSg, !Ref LambdaSg, !Ref LambdaSecurityGroupId] 107 | SubnetIds: !Ref Subnets 108 | Policies: 109 | - Statement: 110 | - Sid: AllowDbConnect 111 | Effect: Allow 112 | Action: 113 | - rds-db:connect 114 | Resource: 115 | - !Sub arn:aws:rds-db:${AWS::Region}:${AWS::AccountId}:dbuser:${ProxyResourceId}/* 116 | Environment: 117 | Variables: 118 | region: !Ref AWS::Region 119 | rds_endpoint: !Ref RdsProxyEndpoint 120 | port: !Ref Port 121 | username: !Sub "{{resolve:secretsmanager:${SecretArn}:SecretString:username}}" 122 | Events: 123 | Proxy: 124 | Type: HttpApi 125 | Properties: 126 | Path: /proxy 127 | Method: get 128 | ApiId: !Ref SampleHttpApi 129 | 130 | Outputs: 131 | ApiBasePath: 132 | Description: "API Gateway endpoint URL" 133 | Value: !Sub "https://${SampleHttpApi}.execute-api.${AWS::Region}.amazonaws.com" 134 | 135 | RdsProxyApiPath: 136 | Description: "API Gateway endpoint URL for rds proxy function" 137 | Value: !Sub "https://${SampleHttpApi}.execute-api.${AWS::Region}.amazonaws.com/proxy" 138 | 139 | RdsApiPath: 140 | Description: "API Gateway endpoint URL for rds function" 141 | Value: !Sub "https://${SampleHttpApi}.execute-api.${AWS::Region}.amazonaws.com/no-proxy" 142 | 143 | LambdaSecurityGroupId: 144 | Description: "Security group id attached to lambda functions. Make sure traffic from this SG is allowed in database and proxy security group on db port" 145 | Value: !GetAtt LambdaSg.GroupId 146 | --------------------------------------------------------------------------------