├── .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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
470 |
471 |
472 |
473 | On the Amazon RDS console, verify if the MySQL instance was created and is available.
474 |
475 | 
476 |
477 |
478 |
479 | On the ElastiCache console, verify if the Redis cluster is available.
480 |
481 | 
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 | 
502 |
503 |
504 |
505 | 3. On the **Session Manager** tab, choose **Connect**.
506 |
507 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
35 | Please query the MySQL database or cache by clicking on the menu options above.
36 |
37 |
38 |
39 |
40 |
43 |
44 | The first time you query the cache, it will get populated from MySQL, hence taking the same time as a standard MySQL query. The subsequent queries should be faster.
45 |