├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app.py ├── cdk.json ├── elasticache_demo_cdk_app ├── __init__.py ├── elasticache_demo_cdk_app_stack.py └── user_data.sh ├── images ├── 00_architecture.png ├── 01_deploy.png ├── 02_deploy.png ├── 03_deploy.png ├── 04_check_cfn.png ├── 05_check_cfn_outputs.png ├── 06_check_ec2.png ├── 07_check_rds.png ├── 08_check_redis.png ├── 09-ssm.png ├── 10-ssm.png ├── 11-ssm.png ├── 12-ssm.png ├── 13-ssm.png ├── 14_app_home.png ├── 15_app_query_mysql.png ├── 16_app_query_cache1.png └── 17_app_query_cache2.png ├── requirements-dev.txt ├── requirements.txt ├── source.bat └── web-app ├── cacheLib.py ├── configs.json ├── static ├── css │ └── custom.css └── img │ └── redis.png ├── templates ├── delete_cache.html ├── index.html ├── libraries.html ├── nav.html ├── query_cache.html └── query_mysql.html └── webApp.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS Finder folder custom attributes 2 | .DS_Store 3 | 4 | # ssh pem files 5 | *.pem 6 | 7 | # CDK outputs 8 | cdk.out/ 9 | 10 | #CDK Context 11 | cdk.context.json 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | pip-wheel-metadata/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # IPython 93 | profile_default/ 94 | ipython_config.py 95 | 96 | # pyenv 97 | .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | -------------------------------------------------------------------------------- /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 | # Deploy Amazon ElastiCache for Redis using AWS CDK 2 | 3 | ***Need for Speed*** - No, it's not the video game, but rather a critical requirement for the success of your website in this competitive world. 4 | 5 | Although we might think that subsecond delay is acceptable, the New York Times noted in [For Impatient Web Users, an Eye Blink Is Just Too Long to Wait](http://www.nytimes.com/2012/03/01/technology/impatient-web-users-flee-slow-loading-sites.html?pagewanted=all&_r=0) that humans can notice a 250-millisecond (a quarter of a second) difference between competing sites. In fact, users tend to opt out of slower websites in favor of faster ones. In the study done at Amazon, [How Webpage Load Time Is Related to Visitor Loss](http://pearanalytics.com/blog/2009/how-webpage-load-time-related-to-visitor-loss/), it’s revealed that for every 100-millisecond (one-tenth of a second) increase in load time, sales decrease 1%. 6 | 7 | If someone wants data, you can deliver that data much faster if it's cached. That's true whether it's for a webpage or a report that drives business decisions. Can your business afford to not cache your webpages so as to deliver them with the shortest latency possible? 8 | 9 | Of course, content delivery networks like [Amazon CloudFront](https://aws.amazon.com/cloudfront/) can cache part of your website's content, for example static objects like images, CSS files, and HTML files. However, dynamic data (for example, the product catalog of an ecommerce website) typically resides in a database. So we have to look at caching for databases as well. 10 | 11 | [Amazon ElastiCache](https://aws.amazon.com/elasticache/) is a fully managed, in-memory caching service supporting flexible, real-time use cases. You can use ElastiCache to accelerate application and database performance, or as a primary data store for use cases that don't require durability like session stores, gaming leaderboards, streaming, and analytics. ElastiCache is compatible with Redis and Memcached. 12 | 13 | In this post, we show you how to deploy [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/) using [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) (AWS CDK). The AWS CDK is an open-source software development framework to define your cloud application resources using familiar programming languages like Python. 14 | 15 | 16 | 17 | ## Solution Overview 18 | 19 | We host our web application using [Amazon Elastic Compute Cloud](https://aws.amazon.com/ec2/) (Amazon EC2). We load a large dataset into a MySQL database hosted on [Amazon Relational Database Service](https://aws.amazon.com/rds/) (Amazon RDS). To cache queries, we use ElastiCache for Redis. The following architecture diagram shows the solution components and how they interact. 20 | 21 | The application queries data from both the [Amazon RDS for MySQL](https://aws.amazon.com/rds/mysql/) database and ElastiCache, showing you the respective runtime. The following diagram illustrates this process. 22 | 23 | ![00_architecture](./images/00_architecture.png) 24 | 25 | 26 | 27 | In this post, we walk you through the following steps: 28 | 29 | 1. Install the [AWS Command Line Interface](http://aws.amazon.com/cli) (AWS CLI) and [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) on your local machine. 30 | 31 | 2. Clone and set up the AWS CDK application. 32 | 33 | 3. Run the AWS CDK application. 34 | 35 | 4. Verify the resources created. 36 | 37 | 5. Connect to the web server EC2 instance. 38 | 39 | 6. Start the web application. 40 | 41 | 7. Use the web application. 42 | 43 | 44 | 45 | So, let's begin. 46 | 47 | 48 | 49 | ## Prerequisites 50 | 51 | - An [AWS account](https://signin.aws.amazon.com/signin) 52 | - [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) 53 | - Python 3.6 or later 54 | - node.js 14.x or later 55 | - [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) 56 | 57 | The estimated cost to complete this post is $3, assuming you leave the resources running for 8 hours. Make sure you delete the resources you create in this post to avoid ongoing charges. 58 | 59 | 60 | 61 | ## Install the AWS CLI and AWS CDK on your local machine 62 | 63 | If you do not have AWS CLI already on your local machine, install it using this [install guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and configure using this [configuration guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). 64 | 65 | Install the AWS CDK Toolkit globally using the following node package manager command: 66 | 67 | ```bash 68 | npm install -g aws-cdk-lib@latest 69 | ``` 70 | 71 | 72 | 73 | Run the following command to verify the correct installation and print the version number of the AWS CDK. 74 | 75 | ```bash 76 | cdk --version 77 | ``` 78 | 79 | 80 | 81 | 82 | 83 | ## Clone and set up the AWS CDK application 84 | 85 | On your local machine, clone the AWS CDK application with the following command: 86 | 87 | ```shell 88 | git clone https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk.git 89 | ``` 90 | 91 | 92 | 93 | Navigate into the project folder: 94 | 95 | ```shell 96 | cd amazon-elasticache-demo-using-aws-cdk 97 | ``` 98 | 99 | 100 | 101 | Before we deploy the application, let's review the directory structure: 102 | 103 | ```shell 104 | . 105 | ├── CODE_OF_CONDUCT.md 106 | ├── CONTRIBUTING.md 107 | ├── LICENSE 108 | ├── README.md 109 | ├── app.py 110 | ├── cdk.json 111 | ├── elasticache_demo_cdk_app 112 | │ ├── __init__.py 113 | │ ├── elasticache_demo_cdk_app_stack.py 114 | │ └── user_data.sh 115 | ├── images 116 | │ ├── ... 117 | ├── requirements-dev.txt 118 | ├── requirements.txt 119 | ├── source.bat 120 | └── web-app 121 | ├── cacheLib.py 122 | ├── configs.json 123 | ├── static 124 | │ ├── ... 125 | ├── templates 126 | │ ├── ... 127 | └── webApp.py 128 | ``` 129 | 130 | The repository also contains the web application located under the subfolder web-app, which is installed on an EC2 instance at deployment. 131 | 132 | The cdk.json file tells the AWS CDK Toolkit how to run your application. 133 | 134 | 135 | 136 | #### Setup a virtual environment 137 | 138 | This project is set up like a standard Python project. Create a Python virtual environment using the following code: 139 | 140 | ```shell 141 | python3 -m venv .venv 142 | ``` 143 | 144 | 145 | 146 | Use the following step to activate the virtual environment: 147 | 148 | ```shell 149 | source .venv/bin/activate 150 | ``` 151 | 152 | 153 | 154 | If you’re on a Windows platform, activate the virtual environment as follows: 155 | 156 | ```shell 157 | .venv\Scripts\activate.bat 158 | ``` 159 | 160 | 161 | 162 | After the virtual environment is activated, upgrade pip to the latest version: 163 | 164 | ```shell 165 | python3 -m pip install --upgrade pip 166 | ``` 167 | 168 | 169 | 170 | Install the required dependencies: 171 | 172 | ```shell 173 | pip install -r requirements.txt 174 | ``` 175 | 176 | 177 | 178 | Before you deploy any AWS CDK application, you need to bootstrap a space in your account and the Region you’re deploying into. To bootstrap in your default Region, issue the following command: 179 | 180 | ```bash 181 | cdk bootstrap 182 | ``` 183 | 184 | 185 | 186 | If you want to deploy into a specific account and region, issue the following command: 187 | 188 | ```bash 189 | cdk bootstrap aws://ACCOUNT-NUMBER/REGION 190 | ``` 191 | 192 | For more information about this setup, visit [Getting started with the AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) 193 | 194 | 195 | 196 | You can now synthesize the [AWS CloudFormation](https://aws.amazon.com/cloudformation/) template for this code: 197 | 198 | ```shell 199 | cdk synth 200 | ``` 201 | 202 | 203 | 204 | #### Other useful AWS CDK commands 205 | 206 | * `cdk ls` List all stacks in the app 207 | * `cdk synth` Emits the synthesized CloudFormation template 208 | * `cdk deploy` Deploy this stack to your default AWS account or Region 209 | * `cdk diff` Compare the deployed stack with the current state 210 | * `cdk docs` Open the AWS CDK documentation 211 | 212 | 213 | 214 | 215 | 216 | ## Run the AWS CDK application 217 | 218 | At this point, you can deploy the AWS CDK application: 219 | 220 | ```shell 221 | cdk deploy 222 | ``` 223 | 224 | 225 | 226 | You should see a list of AWS resources that will be provisioned in the stack. Enter 'y' to proceed with the deployment. 227 | 228 | ![04_deploy](./images/01_deploy.png) 229 | 230 | 231 | 232 | You can see the progress of the deployment on the terminal. It takes around 10 to 15 minutes to deploy the stack. 233 | 234 | ![05_deploy](./images/02_deploy.png) 235 | 236 | 237 | 238 | Once deployment is complete, you can see the total deployment time and AWS CloudFormation *Outputs* on the terminal. Take note of the web server public URL. 239 | 240 | ![06_deploy](./images/03_deploy.png) 241 | 242 | These outputs are also available on the AWS console. Navigate to the AWS CloudFormation console and choose the `ElasticacheDemoCdkAppStack` stack to see the details. 243 | 244 | ![07_check_cfn](./images/04_check_cfn.png) 245 | 246 | 247 | 248 | Review the resources under the **Outputs** tab. These outputs should match the outputs from the terminal when the CDK application completes. 249 | 250 | ![08_check_cfn_outputs](./images/05_check_cfn_outputs.png) 251 | 252 | 253 | 254 | The web application retrieves these outputs automatically using the [AWS SDK for Python (Boto3)](https://aws.amazon.com/sdk-for-python/), see function `get_stack_outputs` in [cacheLib.py](https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/blob/main/web-app/cacheLib.py). However, take note of the web server public IP or URL. We need it to connect to the web server later. 255 | 256 | 257 | 258 | #### AWS CDK application code 259 | 260 | The main AWS CDK application is in the app stack file, see [elasticache_demo_cdk_app_stack.py](https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/blob/main/elasticache_demo_cdk_app/elasticache_demo_cdk_app_stack.py), and the whole infrastructure is defined as the `ElasticacheDemoCdkAppStack` class. Read through the comments to see what each block is doing. 261 | 262 | First, we import the necessary libraries needed to construct the stack: 263 | 264 | ```python 265 | from aws_cdk import ( 266 | # Duration, 267 | Stack, 268 | aws_rds as rds, 269 | aws_ec2 as ec2, 270 | aws_iam as iam, 271 | aws_elasticache as elasticache, 272 | RemovalPolicy, 273 | CfnOutput 274 | ) 275 | 276 | from constructs import Construct 277 | ``` 278 | 279 | 280 | 281 | Then in the class `ElasticacheDemoCdkAppStack`, we define the stack, starting with the virtual private network and security groups: 282 | 283 | ```python 284 | class ElasticacheDemoCdkAppStack(Stack): 285 | 286 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 287 | super().__init__(scope, construct_id, **kwargs) 288 | 289 | # VPC 290 | vpc = ec2.Vpc(self, "VPC", 291 | nat_gateways=1, 292 | cidr="10.0.0.0/16", 293 | subnet_configuration=[ 294 | ec2.SubnetConfiguration(name="public",subnet_type=ec2.SubnetType.PUBLIC,cidr_mask=24), 295 | ec2.SubnetConfiguration(name="private",subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT,cidr_mask=24) 296 | ] 297 | ) 298 | 299 | 300 | # Security Groups 301 | db_sec_group = ec2.SecurityGroup( 302 | self, "db-sec-group",security_group_name="db-sec-group", vpc=vpc, allow_all_outbound=True, 303 | ) 304 | webserver_sec_group = ec2.SecurityGroup( 305 | self, "webserver_sec_group",security_group_name="webserver_sec_group", vpc=vpc, allow_all_outbound=True, 306 | ) 307 | redis_sec_group = ec2.SecurityGroup( 308 | self, "redis-sec-group",security_group_name="redis-sec-group", vpc=vpc, allow_all_outbound=True, 309 | ) 310 | 311 | private_subnets_ids = [ps.subnet_id for ps in vpc.private_subnets] 312 | 313 | redis_subnet_group = elasticache.CfnSubnetGroup( 314 | scope=self, 315 | id="redis_subnet_group", 316 | subnet_ids=private_subnets_ids, # todo: add list of subnet ids here 317 | description="subnet group for redis" 318 | ) 319 | 320 | # Add ingress rules to security group 321 | webserver_sec_group.add_ingress_rule( 322 | peer=ec2.Peer.ipv4("0.0.0.0/0"), 323 | description="Flask Application", 324 | connection=ec2.Port.tcp(app_port), 325 | ) 326 | 327 | db_sec_group.add_ingress_rule( 328 | peer=webserver_sec_group, 329 | description="Allow MySQL connection", 330 | connection=ec2.Port.tcp(3306), 331 | ) 332 | 333 | redis_sec_group.add_ingress_rule( 334 | peer=webserver_sec_group, 335 | description="Allow Redis connection", 336 | connection=ec2.Port.tcp(6379), 337 | ) 338 | ``` 339 | 340 | 341 | 342 | Then we define the data stores used in the application, that is, Amazon RDS for MySQL and Amazon ElastiCache: 343 | 344 | ```python 345 | # RDS MySQL Database 346 | rds_instance = rds.DatabaseInstance( 347 | self, id='RDS-MySQL-Demo-DB', 348 | database_name='covid', 349 | engine=rds.DatabaseInstanceEngine.mysql( 350 | version=rds.MysqlEngineVersion.VER_8_0_28 351 | ), 352 | vpc=vpc, 353 | port=3306, 354 | instance_type= ec2.InstanceType.of( 355 | ec2.InstanceClass.BURSTABLE3, 356 | ec2.InstanceSize.MEDIUM, 357 | ), 358 | removal_policy=RemovalPolicy.DESTROY, 359 | deletion_protection=False, 360 | iam_authentication=True, 361 | security_groups=[db_sec_group], 362 | storage_encrypted=True, 363 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT) 364 | ) 365 | 366 | # Elasticache for Redis cluster 367 | redis_cluster = elasticache.CfnCacheCluster( 368 | scope=self, 369 | id="redis_cluster", 370 | engine="redis", 371 | cache_node_type="cache.t3.small", 372 | num_cache_nodes=1, 373 | cache_subnet_group_name=redis_subnet_group.ref, 374 | vpc_security_group_ids=[redis_sec_group.security_group_id], 375 | ) 376 | ``` 377 | 378 | 379 | 380 | Then we define the EC2 instance for the web server as well as the required [AWS Identity and Access Management](http://aws.amazon.com/iam) (IAM) role and policies for the web server to access the data stores and [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) to retrieve the database credentials: 381 | 382 | ```python 383 | # AMI definition 384 | amzn_linux = ec2.MachineImage.latest_amazon_linux( 385 | generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 386 | edition=ec2.AmazonLinuxEdition.STANDARD, 387 | virtualization=ec2.AmazonLinuxVirt.HVM, 388 | storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE 389 | ) 390 | 391 | # Instance Role and SSM Managed Policy 392 | role = iam.Role(self, "ElasticacheDemoInstancePolicy", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com")) 393 | role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSSMManagedInstanceCore")) 394 | role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AWSCloudFormationReadOnlyAccess")) 395 | 396 | # The following inline policy makes sure we allow only retrieving the secret value, provided the secret is already known. 397 | # It does not allow listing of all secrets. 398 | role.attach_inline_policy(iam.Policy(self, "secret-read-only", 399 | statements=[iam.PolicyStatement( 400 | actions=["secretsmanager:GetSecretValue"], 401 | resources=["arn:aws:secretsmanager:*"], 402 | effect=iam.Effect.ALLOW 403 | )] 404 | )) 405 | 406 | # EC2 Instance for Web Server 407 | instance = ec2.Instance(self, "WebServer", 408 | instance_type=ec2.InstanceType("t3.small"), 409 | machine_image=amzn_linux, 410 | vpc = vpc, 411 | role = role, 412 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), 413 | security_group=webserver_sec_group, 414 | user_data=ec2.UserData.custom(user_data) 415 | ) 416 | ``` 417 | 418 | 419 | 420 | Lastly, we capture all CloudFormation stack outputs generated from the AWS CDK application stack: 421 | 422 | ```python 423 | # Generate CloudFormation Outputs 424 | CfnOutput(scope=self,id="secret_name",value=rds_instance.secret.secret_name) 425 | CfnOutput(scope=self,id="mysql_endpoint",value=rds_instance.db_instance_endpoint_address) 426 | CfnOutput(scope=self,id="redis_endpoint",value=redis_cluster.attr_redis_endpoint_address) 427 | CfnOutput(scope=self,id="webserver_public_ip",value=instance.instance_public_ip) 428 | CfnOutput(scope=self,id="webserver_public_url",value='http://' + instance.instance_public_dns_name + ':' + str(app_port)) 429 | ``` 430 | 431 | 432 | 433 | As described in the preceding code, the web server `userdata` is stored in the [user_data.sh](https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/blob/main/elasticache_demo_cdk_app/user_data.sh) file. The content of `user_data.sh` is as follows: 434 | 435 | ```sh 436 | #!/usr/bin/sh 437 | 438 | yum update -y 439 | yum install mariadb -y 440 | yum install git -y 441 | yum install tree -y 442 | yum install wget -y 443 | yum install jq -y 444 | 445 | pip3 install flask redis pymysql boto3 requests 446 | pip3 uninstall urllib3 447 | pip3 install 'urllib3<2.0' 448 | 449 | cd /home/ec2-user 450 | git clone https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk.git 451 | cd amazon-elasticache-demo-using-aws-cdk 452 | wget https://aws-blogs-artifacts-public.s3.amazonaws.com/artifacts/DBBLOG-1922/sample-dataset.zip 453 | unzip sample-dataset.zip 454 | rm sample-dataset.zip 455 | 456 | chown -R ec2-user:ec2-user /home/ec2-user/* 457 | ``` 458 | 459 | 460 | 461 | The web server essentially clones the same Git repository of the main AWS CDK application. The script also downloads a sample dataset from located under the sub-folder `sample-data` under the main folder. The dataset is from [Kaggle](https://www.kaggle.com/charlieharper/spatial-data-for-cord19-covid19-ordc/version/2) and is licensed under [Creative Commons](https://creativecommons.org/licenses/by/4.0/). 462 | 463 | 464 | 465 | ## Verify the resources created by AWS CDK in the console 466 | 467 | On the Amazon EC2 console, verify if the EC2 instance was created and is running. 468 | 469 | ![09_check_ec2](./images/06_check_ec2.png) 470 | 471 | 472 | 473 | On the Amazon RDS console, verify if the MySQL instance was created and is available. 474 | 475 | ![10_check_rds](./images/07_check_rds.png) 476 | 477 | 478 | 479 | On the ElastiCache console, verify if the Redis cluster is available. 480 | 481 | ![11_check_redis](./images/08_check_redis.png) 482 | 483 | 484 | 485 | The Amazon RDS CDK construct also automatically creates a secret in Secrets Manager. You can view the secret name in the CloudFormation stack outputs. The secret contains the MySQL user admin, automatically generated password, database host, and database name. 486 | 487 | The web application retrieves this information automatically from Secrets Manager, so you don’t need to note down these values. 488 | 489 | 490 | 491 | ## Connect to the web server EC2 instance 492 | 493 | We connect to the web server instance using [AWS Systems Manager Session Manager](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html) through the [AWS Management Console](http://aws.amazon.com/console). Leaving inbound SSH ports and remote PowerShell ports open on your managed nodes greatly increases the risk of entities running unauthorized or malicious commands on the managed nodes. Session Manager helps you improve your security posture by letting you close these inbound ports, freeing you from managing SSH keys and certificates, bastion hosts, and jump boxes. 494 | 495 | 1. On the Amazon EC2 console, choose **Instances** in the navigation pane. 496 | 497 | 2. Select the web server and choose **Connect**. 498 | 499 | 500 | 501 | ![ssm-02](./images/10-ssm.png) 502 | 503 | 504 | 505 | 3. On the **Session Manager** tab, choose **Connect**. 506 | 507 | ![ssm-03](./images/11-ssm.png) 508 | 509 | You log in as `ssm-user`. However, the web server's user data script is run for `ec2-user`. 510 | 511 | 4. Switch to `ec2-user` using the following command. 512 | 513 | ```shell 514 | sudo su - ec2-user 515 | ``` 516 | 517 | 518 | 519 | You should land in the `ec2-user` home directory `/home/ec2-user`, which should contain the `elasticache-demo-cdk-application` sub-folder. This is the same repository that you cloned on your local machine. It contains the sample data that is inserted into the MySQL database as well as the web application. You can find the sample dataset and the web application in the `sample-dataset` and `web-app` subfolders, respectively. 520 | 521 | 522 | 523 | Let's navigate to the web application subfolder from the home directory and see the content: 524 | 525 | ```shell 526 | ls -l 527 | cd amazon-elasticache-demo-using-aws-cdk 528 | ls -l 529 | cd web-app 530 | ls -l 531 | ``` 532 | 533 | 534 | 535 | ![ssm-04](./images/12-ssm.png) 536 | 537 | 538 | 539 | ### **Overview of the web application** 540 | 541 | The web application is a Python [Flask](https://www.fullstackpython.com/flask.html) application. The file [webApp.py](https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/blob/main/web-app/webApp.py) is the main application, which contains the Flask routes for each operation, for example, query MySQL database and query cache. The following snippet for the query cache function: 542 | 543 | 544 | 545 | ```python 546 | @app.route("/query_cache") 547 | def query_cache_endpoint(): 548 | data = None 549 | start_time = datetime.now() 550 | result = query_mysql_and_cache(sql,configs['db_host'], configs['db_username'], configs['db_password'], configs['db_name']) 551 | delta = (datetime.now() - start_time).total_seconds() 552 | 553 | if isinstance(result['data'], list): 554 | data = result['data'] 555 | else: 556 | data = json.loads(result['data']) 557 | 558 | return render_template('query_cache.html', delta=delta, data=data, records_in_cache=result['records_in_cache'], 559 | TTL=Cache.ttl(sql), sql=sql, fields=db_tbl_fields) 560 | 561 | ``` 562 | 563 | 564 | 565 | The `webApp.py` file imports `cacheLib` . The file `cacheLib.py` contains the key procedures to perform the following actions: 566 | 567 | - Retrieve the MySQL and ElastiCache for Redis endpoints from the CloudFormation stack outputs 568 | - Load the sample data into the MySQL database 569 | - Store all configurations in `config.json` which is automatically created when the application is run for the first time 570 | - Query the MySQL database 571 | - Query Amazon ElastiCache 572 | 573 | 574 | 575 | Refer to the [cacheLib.py](https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/blob/main/web-app/cacheLib.py) file to understand the respective functions. One of these functions is called `query_mysql_and_cache` , which is triggered by the web application under the `@app.route("/query_cache")`. This function checks if the dataset is in the cache first. If there is a cache hit, the dataset is served by ElastiCache at low latency. Otherwise, the dataset is retrieved from MySQL and then cached into ElastiCache for future queries. The following is the `query_mysql_and_cache` code snippet: 576 | 577 | ```python 578 | def query_mysql_and_cache(sql,db_host, db_username, db_password, db_name): 579 | ''' 580 | This function retrieves records from the cache if it exists, or else gets it from the MySQL database. 581 | ''' 582 | 583 | res = Cache.get(sql) 584 | 585 | if res: 586 | print ('Records in cache...') 587 | return ({'records_in_cache': True, 'data' : res}) 588 | 589 | res = mysql_fetch_data(sql, db_host, db_username, db_password, db_name) 590 | 591 | if res: 592 | print ('Cache was empty. Now populating cache...') 593 | Cache.setex(sql, ttl, json.dumps(res)) 594 | return ({'records_in_cache': False, 'data' : res}) 595 | else: 596 | return None 597 | ``` 598 | 599 | 600 | 601 | 602 | 603 | ## Start the Web Application 604 | 605 | The necessary runtimes and modules were already installed at instance creation using the **userdata** contained in the `user_data.sh` file in the AWS CDK application. 606 | 607 | 608 | 609 | Let's start the web application with the following command: 610 | 611 | ```shell 612 | sudo python3 webApp.py 613 | ``` 614 | 615 | 616 | 617 | ![ssm-05](./images/13-ssm.png) 618 | 619 | 620 | 621 | ## Use the web application 622 | 623 | From a browser, access the web server's public IP address or URL with port 8008 (or any other ports if you changed it), for example, http://WEBSERVER-PUBLIC-IP:8008. This should take you to the landing page of the web application, as shown in the following screenshot: 624 | 625 | ![12_app_home](./images/14_app_home.png) 626 | 627 | If you cannot access the site using port 8008, you can be behind a VPN or firewall that is blocking that port. Try disconnecting your VPN or switching network. 628 | 629 | Now let's query the MySQL database. Choose **Query MySQL** on the navigation bar. 630 | 631 | ![13_app_query_mysql](./images/15_app_query_mysql.png) 632 | 633 | 634 | 635 | You can review the time it took to run the query. Now let's try to query the cache by choosing **Query Cache**. 636 | 637 | ![14_app_query_cache1](./images/16_app_query_cache1.png) 638 | 639 | 640 | 641 | Notice that it took almost the same time to run. This is because the first time you access the Redis cache, it’s empty. So the application gets the data from the MySQL database and then caches it into the Redis cache. 642 | 643 | Now, try querying the cache again and observe the new runtime. You should see a value in the order of milliseconds. This is very fast! 644 | 645 | ![15_app_query_cache2](./images/17_app_query_cache2.png) 646 | 647 | 648 | 649 | If you look at your terminal, you can see the HTTP requests made from the browser. This can be helpful if you need to troubleshoot. 650 | 651 | ```shell 652 | [ec2-user@ip-10-0-0-96 web-app]$ python3 webApp.py 653 | Local config file found...Initializing MySQL Database... 654 | * Serving Flask app 'webApp' (lazy loading) 655 | * Environment: production 656 | WARNING: This is a development server. Do not use it in a production deployment. 657 | Use a production WSGI server instead. 658 | * Debug mode: off * Running on all addresses (0.0.0.0) 659 | WARNING: This is a development server. Do not use it in a production deployment. * Running on http://127.0.0.1:8008 660 | * Running on http://10.0.0.96:8008 (Press CTRL+C to quit) 661 | 151.192.221.101 - - [14/Apr/2022 16:36:41] "GET / HTTP/1.1" 200 - 662 | 151.192.221.101 - - [14/Apr/2022 16:36:41] "GET /static/css/custom.css HTTP/1.1" 200 -151.192.221.101 - - [14/Apr/2022 16:36:41] "GET /static/img/redis.png HTTP/1.1" 200 - 663 | 151.192.221.101 - - [14/Apr/2022 16:36:42] "GET /favicon.ico HTTP/1.1" 404 - 664 | Cache was empty. Now populating cache...151.192.221.101 - - [14/Apr/2022 16:36:46] "GET /query_cache HTTP/1.1" 200 - 665 | 151.192.221.101 - - [14/Apr/2022 16:36:46] "GET /static/css/custom.css HTTP/1.1" 304 -151.192.221.101 - - [14/Apr/2022 16:36:46] "GET /static/img/redis.png HTTP/1.1" 304 - 666 | Records in cache...151.192.221.101 - - [14/Apr/2022 16:36:47] "GET /query_cache HTTP/1.1" 200 - 667 | 151.192.221.101 - - [14/Apr/2022 16:36:47] "GET /static/css/custom.css HTTP/1.1" 304 -151.192.221.101 - - [14/Apr/2022 16:36:47] "GET /static/img/redis.png HTTP/1.1" 304 - 668 | Records in cache... 669 | 151.192.221.101 - - [14/Apr/2022 16:36:49] "GET /query_cache HTTP/1.1" 200 - 670 | 151.192.221.101 - - [14/Apr/2022 16:36:49] "GET /static/css/custom.css HTTP/1.1" 304 - 671 | 151.192.221.101 - - [14/Apr/2022 16:36:49] "GET /static/img/redis.png HTTP/1.1" 304 - 672 | Records in database... 673 | ``` 674 | 675 | 676 | 677 | ## Clean up 678 | 679 | To avoid unnecessary cost, clean up all the infrastructure created with the following command on your workstation: 680 | 681 | ```shell 682 | (.venv) [~/amazon-elasticache-demo-using-aws-cdk] $ cdk destroy 683 | Are you sure you want to delete: ElasticacheDemoCdkAppStack (y/n)? y 684 | ElasticacheDemoCdkAppStack: destroying... 685 | 686 | 687 | ✅ ElasticacheDemoCdkAppStack: destroyed 688 | ``` 689 | 690 | 691 | 692 | ## Conclusion 693 | 694 | As demonstrated in this post, you can use AWS CDK to create the infrastructure for an Amazon ElastiCache application. We showed the difference in runtime between ElastiCache and an Amazon RDS with MySQL engine. 695 | 696 | You can now build your own infrastructure and application using the caching capability of Amazon ElastiCache to accelerate performance for a better user experience. 697 | 698 | 699 | 700 | ## License summary 701 | 702 | This sample code is made available under a modified MIT license. See the LICENSE file for more information. 703 | 704 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from elasticache_demo_cdk_app.elasticache_demo_cdk_app_stack import ElasticacheDemoCdkAppStack 7 | 8 | app = cdk.App() 9 | ElasticacheDemoCdkAppStack(app, "ElasticacheDemoCdkAppStack", 10 | # If you don't specify 'env', this stack will be environment-agnostic. 11 | # Account/Region-dependent features and context lookups will not work, 12 | # but a single synthesized template can be deployed anywhere. 13 | 14 | # Uncomment the next line to specialize this stack for the AWS Account 15 | # and Region that are implied by the current CLI configuration. 16 | 17 | #env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')), 18 | 19 | # Uncomment the next line if you know exactly what Account and Region you 20 | # want to deploy the stack to. */ 21 | 22 | #env=cdk.Environment(account='123456789012', region='us-east-1'), 23 | 24 | # For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html 25 | ) 26 | 27 | app.synth() 28 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 25 | "@aws-cdk/core:checkSecretUsage": true, 26 | "@aws-cdk/aws-iam:minimizePolicies": true, 27 | "@aws-cdk/core:target-partitions": [ 28 | "aws", 29 | "aws-cn" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /elasticache_demo_cdk_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/elasticache_demo_cdk_app/__init__.py -------------------------------------------------------------------------------- /elasticache_demo_cdk_app/elasticache_demo_cdk_app_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | # Duration, 3 | Stack, 4 | aws_rds as rds, 5 | aws_ec2 as ec2, 6 | aws_iam as iam, 7 | aws_elasticache as elasticache, 8 | RemovalPolicy, 9 | CfnOutput 10 | ) 11 | 12 | from constructs import Construct 13 | 14 | # Load user data for the Web Server EC2 instance 15 | with open("./elasticache_demo_cdk_app/user_data.sh") as f: 16 | user_data = f.read() 17 | 18 | app_port = 8008 19 | 20 | class ElasticacheDemoCdkAppStack(Stack): 21 | 22 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 23 | super().__init__(scope, construct_id, **kwargs) 24 | 25 | # VPC 26 | vpc = ec2.Vpc(self, "VPC", 27 | nat_gateways=1, 28 | cidr="10.0.0.0/16", 29 | subnet_configuration=[ 30 | ec2.SubnetConfiguration(name="public",subnet_type=ec2.SubnetType.PUBLIC,cidr_mask=24), 31 | ec2.SubnetConfiguration(name="private",subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT,cidr_mask=24) 32 | ] 33 | ) 34 | 35 | 36 | # Security Groups 37 | db_sec_group = ec2.SecurityGroup( 38 | self, "db-sec-group",security_group_name="db-sec-group", vpc=vpc, allow_all_outbound=True, 39 | ) 40 | webserver_sec_group = ec2.SecurityGroup( 41 | self, "webserver_sec_group",security_group_name="webserver_sec_group", vpc=vpc, allow_all_outbound=True, 42 | ) 43 | redis_sec_group = ec2.SecurityGroup( 44 | self, "redis-sec-group",security_group_name="redis-sec-group", vpc=vpc, allow_all_outbound=True, 45 | ) 46 | 47 | private_subnets_ids = [ps.subnet_id for ps in vpc.private_subnets] 48 | 49 | redis_subnet_group = elasticache.CfnSubnetGroup( 50 | scope=self, 51 | id="redis_subnet_group", 52 | subnet_ids=private_subnets_ids, # todo: add list of subnet ids here 53 | description="subnet group for redis" 54 | ) 55 | 56 | # Add ingress rules to security group 57 | webserver_sec_group.add_ingress_rule( 58 | peer=ec2.Peer.ipv4("0.0.0.0/0"), 59 | description="Flask Application", 60 | connection=ec2.Port.tcp(app_port), 61 | ) 62 | 63 | db_sec_group.add_ingress_rule( 64 | peer=webserver_sec_group, 65 | description="Allow MySQL connection", 66 | connection=ec2.Port.tcp(3306), 67 | ) 68 | 69 | redis_sec_group.add_ingress_rule( 70 | peer=webserver_sec_group, 71 | description="Allow Redis connection", 72 | connection=ec2.Port.tcp(6379), 73 | ) 74 | 75 | # RDS MySQL Database 76 | rds_instance = rds.DatabaseInstance( 77 | self, id='RDS-MySQL-Demo-DB', 78 | database_name='covid', 79 | engine=rds.DatabaseInstanceEngine.mysql( 80 | version=rds.MysqlEngineVersion.VER_8_0_28 81 | ), 82 | vpc=vpc, 83 | port=3306, 84 | instance_type= ec2.InstanceType.of( 85 | ec2.InstanceClass.BURSTABLE3, 86 | ec2.InstanceSize.MEDIUM, 87 | ), 88 | removal_policy=RemovalPolicy.DESTROY, 89 | deletion_protection=False, 90 | iam_authentication=True, 91 | security_groups=[db_sec_group], 92 | storage_encrypted=True, 93 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT) 94 | ) 95 | 96 | # Elasticache for Redis cluster 97 | redis_cluster = elasticache.CfnCacheCluster( 98 | scope=self, 99 | id="redis_cluster", 100 | engine="redis", 101 | cache_node_type="cache.t3.small", 102 | num_cache_nodes=1, 103 | cache_subnet_group_name=redis_subnet_group.ref, 104 | vpc_security_group_ids=[redis_sec_group.security_group_id], 105 | ) 106 | 107 | # AMI definition 108 | amzn_linux = ec2.MachineImage.latest_amazon_linux( 109 | generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, 110 | edition=ec2.AmazonLinuxEdition.STANDARD, 111 | virtualization=ec2.AmazonLinuxVirt.HVM, 112 | storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE 113 | ) 114 | 115 | # Instance Role and SSM Managed Policy 116 | role = iam.Role(self, "ElasticacheDemoInstancePolicy", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com")) 117 | role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSSMManagedInstanceCore")) 118 | role.add_managed_policy(iam.ManagedPolicy.from_aws_managed_policy_name("AWSCloudFormationReadOnlyAccess")) 119 | 120 | # The following inline policy makes sure we allow only retrieving the secret value, provided the secret is already known. 121 | # It does not allow listing of all secrets. 122 | role.attach_inline_policy(iam.Policy(self, "secret-read-only", 123 | statements=[iam.PolicyStatement( 124 | actions=["secretsmanager:GetSecretValue"], 125 | resources=["arn:aws:secretsmanager:*"], 126 | effect=iam.Effect.ALLOW 127 | )] 128 | )) 129 | 130 | # EC2 Instance for Web Server 131 | instance = ec2.Instance(self, "WebServer", 132 | instance_type=ec2.InstanceType("t3.small"), 133 | machine_image=amzn_linux, 134 | vpc = vpc, 135 | role = role, 136 | vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), 137 | security_group=webserver_sec_group, 138 | user_data=ec2.UserData.custom(user_data) 139 | ) 140 | 141 | # Generate CloudFormation Outputs 142 | CfnOutput(scope=self,id="secret_name",value=rds_instance.secret.secret_name) 143 | CfnOutput(scope=self,id="mysql_endpoint",value=rds_instance.db_instance_endpoint_address) 144 | CfnOutput(scope=self,id="redis_endpoint",value=redis_cluster.attr_redis_endpoint_address) 145 | CfnOutput(scope=self,id="webserver_public_ip",value=instance.instance_public_ip) 146 | CfnOutput(scope=self,id="webserver_public_url",value=instance.instance_public_dns_name) 147 | -------------------------------------------------------------------------------- /elasticache_demo_cdk_app/user_data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | yum update -y 4 | yum install mariadb -y 5 | yum install git -y 6 | yum install tree -y 7 | yum install wget -y 8 | yum install jq -y 9 | 10 | pip3 install flask redis pymysql boto3 requests 11 | pip3 uninstall urllib3 12 | pip3 install 'urllib3<2.0' 13 | 14 | cd /home/ec2-user 15 | git clone https://github.com/aws-samples/amazon-elasticache-demo-using-aws-cdk.git 16 | cd amazon-elasticache-demo-using-aws-cdk 17 | wget https://aws-blogs-artifacts-public.s3.amazonaws.com/artifacts/DBBLOG-1922/sample-dataset.zip 18 | unzip sample-dataset.zip 19 | rm sample-dataset.zip 20 | 21 | chown -R ec2-user:ec2-user /home/ec2-user/* 22 | 23 | -------------------------------------------------------------------------------- /images/00_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/00_architecture.png -------------------------------------------------------------------------------- /images/01_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/01_deploy.png -------------------------------------------------------------------------------- /images/02_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/02_deploy.png -------------------------------------------------------------------------------- /images/03_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/03_deploy.png -------------------------------------------------------------------------------- /images/04_check_cfn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/04_check_cfn.png -------------------------------------------------------------------------------- /images/05_check_cfn_outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/05_check_cfn_outputs.png -------------------------------------------------------------------------------- /images/06_check_ec2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/06_check_ec2.png -------------------------------------------------------------------------------- /images/07_check_rds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/07_check_rds.png -------------------------------------------------------------------------------- /images/08_check_redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/08_check_redis.png -------------------------------------------------------------------------------- /images/09-ssm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/09-ssm.png -------------------------------------------------------------------------------- /images/10-ssm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/10-ssm.png -------------------------------------------------------------------------------- /images/11-ssm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/11-ssm.png -------------------------------------------------------------------------------- /images/12-ssm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/12-ssm.png -------------------------------------------------------------------------------- /images/13-ssm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/13-ssm.png -------------------------------------------------------------------------------- /images/14_app_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/14_app_home.png -------------------------------------------------------------------------------- /images/15_app_query_mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/15_app_query_mysql.png -------------------------------------------------------------------------------- /images/16_app_query_cache1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/16_app_query_cache1.png -------------------------------------------------------------------------------- /images/17_app_query_cache2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/images/17_app_query_cache2.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.22.0 2 | constructs>=10.0.0,<11.0.0 -------------------------------------------------------------------------------- /source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /web-app/cacheLib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import redis 4 | import pymysql 5 | import boto3 6 | import sys 7 | import os.path 8 | import requests 9 | 10 | def store_configs (config_file, configs): 11 | ''' 12 | This function stores configurations in a json file. 13 | ''' 14 | with open(config_file, 'w') as fp: 15 | json.dump(configs, fp) 16 | 17 | 18 | def load_configs (config_file): 19 | ''' 20 | This function loads configurations from a json file. 21 | ''' 22 | data = {} 23 | with open(config_file) as fp: 24 | data = json.load(fp) 25 | return data 26 | 27 | def get_secret(secret_name,region_name): 28 | ''' 29 | This function retrieves information from Secrets Manager. 30 | ''' 31 | 32 | # Create a Secrets Manager client 33 | session = boto3.session.Session() 34 | client = session.client( 35 | service_name='secretsmanager', 36 | region_name=region_name 37 | ) 38 | 39 | # In this sample we only handle the specific exceptions for the 'GetSecretValue' API. 40 | # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 41 | # We rethrow the exception by default. 42 | 43 | try: 44 | get_secret_value_response = client.get_secret_value( 45 | SecretId=secret_name 46 | ) 47 | except ClientError as e: 48 | if e.response['Error']['Code'] == 'DecryptionFailureException': 49 | # Secrets Manager can't decrypt the protected secret text using the provided KMS key. 50 | # Deal with the exception here, and/or rethrow at your discretion. 51 | raise e 52 | elif e.response['Error']['Code'] == 'InternalServiceErrorException': 53 | # An error occurred on the server side. 54 | # Deal with the exception here, and/or rethrow at your discretion. 55 | raise e 56 | elif e.response['Error']['Code'] == 'InvalidParameterException': 57 | # You provided an invalid value for a parameter. 58 | # Deal with the exception here, and/or rethrow at your discretion. 59 | raise e 60 | elif e.response['Error']['Code'] == 'InvalidRequestException': 61 | # You provided a parameter value that is not valid for the current state of the resource. 62 | # Deal with the exception here, and/or rethrow at your discretion. 63 | raise e 64 | elif e.response['Error']['Code'] == 'ResourceNotFoundException': 65 | # We can't find the resource that you asked for. 66 | # Deal with the exception here, and/or rethrow at your discretion. 67 | raise e 68 | else: 69 | # Decrypts secret using the associated KMS CMK. 70 | # Depending on whether the secret is a string or binary, one of these fields will be populated. 71 | if 'SecretString' in get_secret_value_response: 72 | secret = json.loads(get_secret_value_response['SecretString']) 73 | else: 74 | secret = json.loads(base64.b64decode(get_secret_value_response['SecretBinary'])) 75 | return secret 76 | 77 | 78 | def get_stack_outputs(stack_name,region_name): 79 | ''' 80 | This function retrieves all outputs of a stack from CloudFormation. 81 | ''' 82 | stack_outputs = {} 83 | cf_client = boto3.client('cloudformation',region_name=region_name) 84 | response = cf_client.describe_stacks(StackName=stack_name) 85 | outputs = response["Stacks"][0]["Outputs"] 86 | for output in outputs: 87 | stack_outputs[output["OutputKey"]] = output["OutputValue"] 88 | 89 | response = get_secret(stack_outputs['secretname'],region_name) 90 | 91 | stack_outputs['db_password'] = response['password'] 92 | stack_outputs['db_name'] = response['dbname'] 93 | stack_outputs['db_port'] = response['port'] 94 | stack_outputs['db_username'] = response['username'] 95 | stack_outputs['db_host'] = response['host'] 96 | 97 | return stack_outputs 98 | 99 | 100 | def mysql_execute_command(sql, db_host, db_username, db_password): 101 | ''' 102 | This function excutes the sql statement, does not return any value. 103 | ''' 104 | try: 105 | con = pymysql.connect(host=db_host, 106 | user=db_username, 107 | password=db_password, 108 | autocommit=True, 109 | local_infile=1) 110 | # Create cursor and execute SQL statement 111 | cursor = con.cursor() 112 | cursor.execute(sql) 113 | con.close() 114 | 115 | except Exception as e: 116 | print('Error: {}'.format(str(e))) 117 | sys.exit(1) 118 | 119 | 120 | def mysql_fetch_data(sql, db_host, db_username, db_password, db_name): 121 | ''' 122 | This function excutes the sql query and returns dataset. 123 | ''' 124 | try: 125 | con = pymysql.connect(host=db_host, 126 | user=db_username, 127 | password=db_password, 128 | database=db_name, 129 | autocommit=True, 130 | local_infile=1, 131 | charset='utf8mb4', 132 | cursorclass=pymysql.cursors.DictCursor) 133 | # Create cursor and execute SQL statement 134 | cursor = con.cursor() 135 | cursor.execute(sql) 136 | data_set = cursor.fetchall() 137 | con.close() 138 | return data_set 139 | 140 | except Exception as e: 141 | print('Error: {}'.format(str(e))) 142 | sys.exit(1) 143 | 144 | def flush_cache(): 145 | ''' 146 | This function flushes all records from the cache. 147 | ''' 148 | 149 | Cache.flushall() 150 | 151 | 152 | def query_mysql_and_cache(sql,db_host, db_username, db_password, db_name): 153 | ''' 154 | This function retrieves records from the cache if it exists, or else gets it from the MySQL database. 155 | ''' 156 | 157 | res = Cache.get(sql) 158 | 159 | if res: 160 | print ('Records in cache...') 161 | return ({'records_in_cache': True, 'data' : res}) 162 | 163 | res = mysql_fetch_data(sql, db_host, db_username, db_password, db_name) 164 | 165 | if res: 166 | print ('Cache was empty. Now populating cache...') 167 | Cache.setex(sql, ttl, json.dumps(res)) 168 | return ({'records_in_cache': False, 'data' : res}) 169 | else: 170 | return None 171 | 172 | 173 | def query_mysql(sql,db_host, db_username, db_password, db_name): 174 | ''' 175 | This function retrieve records from the database. 176 | ''' 177 | 178 | res = mysql_fetch_data(sql, db_host, db_username, db_password, db_name) 179 | 180 | if res: 181 | print ('Records in database...') 182 | return res 183 | else: 184 | return None 185 | 186 | def initialize_database(configs): 187 | ''' 188 | This function initialize the MySQL database if not already done so and generates 189 | all configurations needed for the application. 190 | ''' 191 | 192 | # Initialize Database 193 | print ('Initializing MySQL Database...') 194 | 195 | #Drop table if exists 196 | sql_command = "DROP TABLE IF EXISTS covid.articles;" 197 | mysql_execute_command(sql_command, configs['db_host'], configs['db_username'], configs['db_password']) 198 | 199 | #Create table 200 | sql_command = "CREATE TABLE covid.articles (OBJECTID INT, SHA TEXT, PossiblePlace TEXT, Sentence TEXT, MatchedPlace TEXT, DOI TEXT, Title TEXT, Abstract TEXT, PublishedDate TEXT, Authors TEXT, Journal TEXT, Source TEXT, License TEXT, PRIMARY KEY (OBJECTID));" 201 | mysql_execute_command(sql_command, configs['db_host'], configs['db_username'], configs['db_password']) 202 | 203 | #Load CSV file into mysql 204 | sql_command = """ 205 | LOAD DATA LOCAL INFILE '{0}' 206 | INTO TABLE covid.articles 207 | FIELDS TERMINATED BY ',' 208 | ENCLOSED BY '"' 209 | LINES TERMINATED BY '\n' 210 | IGNORE 1 ROWS; 211 | """.format(configs['dataset_file']) 212 | mysql_execute_command(sql_command, configs['db_host'], configs['db_username'], configs['db_password']) 213 | 214 | 215 | # Load configurations from config file 216 | config_file = 'configs.json' 217 | 218 | if os.path.exists(config_file): 219 | configs = load_configs (config_file) 220 | print('Local config file found...') 221 | else: 222 | print ('Missing config file...') 223 | exit 224 | 225 | stack_name = configs['stack_name'] 226 | ttl = configs['ttl'] 227 | app_port = configs['app_port'] 228 | max_rows = configs['max_rows'] #max # of rows to query from database 229 | dataset_file = configs['dataset_file'] 230 | region_name = requests.get('http://169.254.169.254/latest/dynamic/instance-identity/document').json()['region'] 231 | configs['region_name'] = region_name 232 | 233 | # If datbase is not populated, retrieve endpoints for the database, cache and compute instance from CloudFormation and populate the database 234 | if configs['database_populated'] is False: 235 | 236 | # Get additional configurations from CloudFormation and save on disk 237 | stack_outputs = get_stack_outputs(stack_name,region_name) 238 | for key in stack_outputs.keys(): 239 | configs[key] = stack_outputs[key] 240 | 241 | # Get all configs. If database was not initialized, it will be populated with sample data. 242 | initialize_database(configs) 243 | configs['database_populated'] = True 244 | store_configs (config_file, configs) 245 | 246 | # Initialize the cache 247 | Cache = redis.Redis.from_url('redis://' + configs['redisendpoint'] + ':6379') 248 | 249 | db_table = 'articles' 250 | db_tbl_fields = ['OBJECTID', 'Sentence', 'Title', 'Source'] 251 | sql_fields = ', '.join(db_tbl_fields) 252 | 253 | sql = "select SQL_NO_CACHE " + sql_fields + " from " + db_table + " where Sentence like '%delta%' order by OBJECTID limit " + str(max_rows) 254 | -------------------------------------------------------------------------------- /web-app/configs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ttl": 60, 3 | "app_port": 8008, 4 | "max_rows": 500, 5 | "stack_name": "ElasticacheDemoCdkAppStack", 6 | "dataset_file" : "../sample-dataset/data.csv", 7 | "database_populated" : false 8 | } -------------------------------------------------------------------------------- /web-app/static/css/custom.css: -------------------------------------------------------------------------------- 1 | body{ 2 | font-size: 0.8rem; 3 | } 4 | 5 | .dropdown-item{ 6 | font-size: 0.9rem; 7 | } 8 | 9 | .dropdown-menu { 10 | font-size: 0.8rem 11 | } 12 | 13 | .imgbordered { 14 | border: 1px solid slategrey; 15 | } 16 | 17 | /* borderless table */ 18 | .table.table-borderless td, 19 | .table.table-borderless th, 20 | .table.table-borderless tr { 21 | border: 0 !important; 22 | } 23 | 24 | .table.table-borderless { 25 | margin - bottom: 0px; 26 | } 27 | 28 | a { 29 | color: inherit; 30 | text-decoration: none; 31 | } 32 | 33 | .table-responsive { 34 | display: table; 35 | } 36 | 37 | .chart-container { 38 | position: relative; 39 | margin: auto; 40 | height: 40vh; 41 | width: 90vw; 42 | } 43 | 44 | 45 | .card-body img{ 46 | height: 110px !important; 47 | } 48 | 49 | .card.no-border { 50 | border-width: 0; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /web-app/static/img/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-elasticache-demo-using-aws-cdk/ffc85b21801f7492c85cc0b258de00637da33507/web-app/static/img/redis.png -------------------------------------------------------------------------------- /web-app/templates/delete_cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% include 'libraries.html' %} 9 | 10 | 11 | 12 | {% include 'nav.html' %} 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Redis cache deleted...
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web-app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% include 'libraries.html' %} 9 | 10 | 11 | 12 | {% include 'nav.html' %} 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Welcome to the Elasticache for Redis Demo
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | 38 |
39 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /web-app/templates/libraries.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Elasticache for Redis Demo 12 | -------------------------------------------------------------------------------- /web-app/templates/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-app/templates/query_cache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% include 'libraries.html' %} 9 | 10 | 11 | 12 | {% include 'nav.html' %} 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Querying Redis Cache...
21 |
22 |
23 | 26 | {% if records_in_cache == true %} 27 | 35 | {% else %} 36 | 44 | {% endif %} 45 |
46 |
47 |
48 |
49 |
50 |
Execution time: {{ delta }} seconds
51 | {% if TTL >= 0 %} 52 |
TTL: {{ TTL }} seconds
53 | {% else %} 54 |
TTL: Expired
55 | {% endif %} 56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | {% if data %} 64 |
Dataset:
65 | {% set rows = data | count %} 66 | {% set columns = fields | count %} 67 | 68 | 69 | 70 | 71 | {% for column in range(columns) %} 72 | 73 | {% endfor %} 74 | 75 | 76 | 77 | {% for num in range(rows) %} 78 | 79 | 80 | {% for column in range(columns) %} 81 | 82 | {% endfor %} 83 | 84 | {% endfor %} 85 | 86 |
# {{ fields[column] }}
{{ num + 1 }}{{ data[num][fields[column]] }}
87 | {% else %} 88 |
Dataset: {{ data }}
89 | {% endif %} 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /web-app/templates/query_mysql.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% include 'libraries.html' %} 9 | 10 | 11 | 12 | {% include 'nav.html' %} 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Querying MySQL Database
21 |
22 |
23 |
24 | 27 |
28 | 29 |
30 |
31 |
32 |
Execution time: {{ delta }} seconds
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {% if data %} 41 |
Dataset:
42 | {% set rows = data | count %} 43 | {% set columns = fields | count %} 44 | 45 | 46 | 47 | 48 | {% for column in range(columns) %} 49 | 50 | {% endfor %} 51 | 52 | 53 | 54 | {% for num in range(rows) %} 55 | 56 | 57 | {% for column in range(columns) %} 58 | 59 | {% endfor %} 60 | 61 | {% endfor %} 62 | 63 |
# {{ fields[column] }}
{{ num + 1 }}{{ data[num][fields[column]] }}
64 | {% else %} 65 |
Dataset: {{ data }}
66 | {% endif %} 67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /web-app/webApp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from datetime import datetime 6 | from flask import Flask, render_template 7 | 8 | from cacheLib import * 9 | 10 | app = Flask(__name__) 11 | 12 | 13 | ############################### Flask Routes ################################# 14 | 15 | @app.route("/") 16 | def index(): 17 | return render_template('index.html', rows=max_rows, sql=sql) 18 | 19 | @app.route("/query_mysql") 20 | def query_mysql_endpoint(): 21 | start_time = datetime.now() 22 | data = query_mysql(sql,configs['db_host'], configs['db_username'], configs['db_password'], configs['db_name']) 23 | delta = (datetime.now() - start_time).total_seconds() 24 | return render_template('query_mysql.html', delta=delta, data=data, sql=sql, fields=db_tbl_fields) 25 | 26 | @app.route("/query_cache") 27 | def query_cache_endpoint(): 28 | data = None 29 | start_time = datetime.now() 30 | result = query_mysql_and_cache(sql,configs['db_host'], configs['db_username'], configs['db_password'], configs['db_name']) 31 | delta = (datetime.now() - start_time).total_seconds() 32 | 33 | if isinstance(result['data'], list): 34 | data = result['data'] 35 | else: 36 | data = json.loads(result['data']) 37 | 38 | return render_template('query_cache.html', delta=delta, data=data, records_in_cache=result['records_in_cache'], 39 | TTL=Cache.ttl(sql), sql=sql, fields=db_tbl_fields) 40 | 41 | @app.route("/delete_cache") 42 | def delete_cache_endpoint(): 43 | flush_cache() 44 | return render_template('delete_cache.html') 45 | 46 | if __name__ == "__main__": 47 | app.run(debug=False, use_reloader=False, host='0.0.0.0', port=app_port) 48 | --------------------------------------------------------------------------------