Completed DynamoDB + DAX Benchmarker with a nice TUI to boot
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
/target
|
||||
/.idea/
|
||||
Cargo.lock
|
||||
/.scannerwork/
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
.idea
|
||||
out/
|
||||
main
|
||||
dynamodb-benchmarker
|
||||
dax-benchmarker
|
||||
*.log
|
||||
*.json
|
||||
@@ -0,0 +1,6 @@
|
||||
run:
|
||||
skip-dirs:
|
||||
- cdk
|
||||
- src
|
||||
- scripts
|
||||
- target
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "dynamodb-benchmarker"
|
||||
version = "0.1.0"
|
||||
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
|
||||
description = "A CLI tool for simulating heavy usage against DynamoDB and publishing metrics to an Elastic Stack for analysis"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.71"
|
||||
aws-config = "0.55.3"
|
||||
aws-sdk-dynamodb = "0.28.0"
|
||||
aws-types = "0.55.3"
|
||||
chrono = { version = "0.4.26", features = ["serde"] }
|
||||
clap = { version = "4.3.14", features = ["derive"] }
|
||||
elasticsearch = "8.5.0-alpha.1"
|
||||
lipsum = "0.9.0"
|
||||
log = "0.4.19"
|
||||
log4rs = { version = "1.2.0", features = ["console_appender"] }
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.171", features = ["derive"] }
|
||||
serde_json = { version = "1.0.102", features = ["arbitrary_precision"] }
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
tokio-util = "0.7.8"
|
||||
uuid = { version = "1.4.0", features = ["v4", "fast-rng"] }
|
||||
@@ -0,0 +1,36 @@
|
||||
#!make
|
||||
|
||||
default: build
|
||||
|
||||
.PHONY: init start-elastic-stack stop-elastic-stack build build-dynamodb-benchmarker build-dax-benchmarker run-dynamodb-benchmarker run-dax-benchmarker clean lint
|
||||
|
||||
init: build
|
||||
@[[ -d ../docker-elk ]] || git clone https://github.com/deviantony/docker-elk.git ..
|
||||
@cd ../docker-elk && docker compose up setup
|
||||
@echo "Default login creds: username=elastic, password=changeme"
|
||||
|
||||
start-elastic-stack:
|
||||
@cd ../docker-elk && docker compose up -d
|
||||
|
||||
stop-elastic-stack:
|
||||
@cd ../docker-elk && docker compose down
|
||||
|
||||
build-dynamodb-benchmarker:
|
||||
@cargo clean && rm -f dynamodb-benchmarker && cargo build --release && mv ./target/release/dynamodb-benchmarker .
|
||||
|
||||
build-dax-benchmarker:
|
||||
@rm -f main && rm -f dax-benchmarker && go build -o dax-benchmarker pkg/app/main.go
|
||||
|
||||
build: build-dynamodb-benchmarker build-dax-benchmarker
|
||||
|
||||
run-dynamodb-benchmarker:
|
||||
@cargo run
|
||||
|
||||
run-dax-benchmarker:
|
||||
@go run pkg/app/main.go
|
||||
|
||||
clean:
|
||||
@cargo clean && rm -f main && rm -f dynamodb-benchmarker && rm -f dax-benchmarker && rm -rf cdk/cdk.out && rm -rf cdk/node_modules
|
||||
|
||||
lint:
|
||||
@cargo clippy && golangci-lint run
|
||||
@@ -1 +1,279 @@
|
||||
# dynamodb-dax-benchmarker
|
||||
# DynamoDB + DAX Benchmarker
|
||||
This project houses the Rust and Go code to benchmark the performance of DynamoDB and DAX by simulating heavy loads.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
* [x] Simulate reads on existing data
|
||||
* [x] Simulate writes
|
||||
* [x] Simulate updates
|
||||
* [x] Simulate deletes
|
||||
* [x] Record the following metrics
|
||||
* [x] The type of operation being simulated
|
||||
* [x] Total simulation time
|
||||
* [x] Read times
|
||||
* [x] Write times
|
||||
* [x] Confirmation of write times (i.e. how long after a write is the item available when performing a read)
|
||||
* [x] Update times
|
||||
* [x] Confirmation of update times (i.e. how long after an update is the item available when performing a read)
|
||||
* [x] Delete times
|
||||
* [x] Confirmation of delete times (i.e. how long after a delete is the item no longer available when performing a read)
|
||||
* [x] Randomized selection of which operation to perform
|
||||
* [x] Multithreaded performance for publishing to a locally running Elasticsearch cluster
|
||||
* [x] Highly performant concurrent operations against DynamoDB - 1,000 concurrent operations
|
||||
* [x] Read-only scenarios for tables that are likely to be hit with mostly reads and very few mutating operations
|
||||
* [x] Randomly generate schemas for DynamoDB with a specified number of attributes and generate random data to query
|
||||
|
||||
**Disclaimer:** This project exists as a proof-of-concept for how to benchmark and evaluate the performance of DynamoDB + DAX. As such,
|
||||
this project does not contain any unit tests, integration tests, or E2E tests, thus regressions with future updates are possible and
|
||||
project stability is _not_ guaranteed in perpetuity.
|
||||
|
||||
## Warning!
|
||||
When making changes to this repository, take extra care to be sure you don't commit the automatically generated variables in
|
||||
the [hosts file](./ansible/inventories/local/hosts.yml) and in the [host_vars](./ansible/inventories/local/host_vars/localhost.yml).
|
||||
|
||||
These files have variables that are populated automatically to make your life easier instead of having to specify variables
|
||||
all the time. You can remove them manually, or you can wipe away everything and have them removed for you:
|
||||
|
||||
* `bastion.hosts.BASTION_HOST_PUBLIC_IP:`
|
||||
* `vpc_id`
|
||||
* `dax_endpoint`
|
||||
|
||||
## Getting Started
|
||||
|
||||
The easiest way to use this project is to use the [benchmarker.sh](./benchmarker.sh) script's TUI. This will automate everything for you and make
|
||||
the use of this project as painless as possible! Just ensure it's executable and run the TUI:
|
||||
|
||||
```shell
|
||||
chmod +x benchmarker.sh
|
||||
./benchmarker.sh
|
||||
```
|
||||
|
||||
This project is broken into several distinct pieces. For more information on each the specific pieces, refer to their respective README's:
|
||||
|
||||
* [Ansible](./ansible/README.md) -- Go here if you're looking to have more control over what's going on across the entire deployment process, local and AWS
|
||||
* [CDK](./cdk/README.md) -- Go here if you're looking to discover and tweak the AWS stack and resources
|
||||
|
||||
The vast majority of this project is designed to be automated; however, it is also designed to allow the user to customize it as needed. Naturally, customization
|
||||
is a more challenging course of action here as you'll need to do some of the automated steps manually. I try to detail those steps below.
|
||||
|
||||
### Prerequisites
|
||||
* The commands are being run in a Debian-based Linux environment (i.e. Ubuntu, WSL, etc.)
|
||||
* [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) is installed and configured
|
||||
* Docker is installed (`sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli`)
|
||||
* The docker compose plugin is installed (`sudo apt-get update && sudo apt-get install docker-compose-plugin`)
|
||||
* Rust is installed via rustup (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`)
|
||||
* `jq` is installed (`sudo apt-get install jq`)
|
||||
* Go is installed ([instructions here](https://go.dev/doc/install))
|
||||
|
||||
### Setting up your own local Elastic Stack
|
||||
Fortunately, setting up the Elastic Stack locally is super easy thanks to [this repository](https://github.com/deviantony/docker-elk).
|
||||
|
||||
This setup is automated for you via the `Makefile` target: `init`
|
||||
|
||||
It will
|
||||
|
||||
* Clone the repository to the folder above the current directory
|
||||
* Go into the folder
|
||||
* Start the `setup` docker compose to initialize the Elastic Stack. Don't worry. This function is supposed to exit once it's done initializing your local Elastic Stack
|
||||
|
||||
### Starting and Stopping the Elastic Stack
|
||||
To start the Elastic Stack, you can either manually `cd` into the [docker-elk](../docker-elk) folder and run `docker compose up -d`, or you can use the `start-elastic-stack` target in the [`Makefile`](./Makefile).
|
||||
Similarly, you can stop the Elastic Stack by either `cd`into the [docker-elk](../docker-elk) folder and running `docker compose down`, or you can use the `stop-elastic-stack` target in the [`Makefile`](./Makefile).
|
||||
|
||||
### Running the Benchmarkers
|
||||
To run the benchmarker, make sure you've done all the following steps so that the AWS SDK can pick up your credentials:
|
||||
|
||||
* You're logged into your desired AWS account via the AWS CLI
|
||||
* Ensure you're properly connected with `aws sts get-caller-identity`
|
||||
* You've exported all the following AWS environment variables, so they can be picked up by the AWS SDK at runtime to authenticate with AWS:
|
||||
* `AWS_ACCESS_KEY_ID`
|
||||
* `AWS_SECRET_ACCESS_KEY`
|
||||
* `AWS_SESSION_TOKEN`
|
||||
* `AWS_REGION` (not typically defined by the CLI)
|
||||
* `AWS_ACCOUNT` (this is a special variable that is not normally defined by the AWS CLI: definition is achieved by running `export AWS_ACCOUNT=$(aws sts get-caller-identity | jq -r .Account)`)
|
||||
* A nifty little shortcut for exporting all but the `AWS_REGION` and `AWS_ACCOUNT` variables is provided with the following command: ```shell eval $(aws configure export-credentials --format env)```
|
||||
|
||||
There's a few ways to run the benchmarkers, but the easiest is to build them and run their binaries.
|
||||
|
||||
It's as simple as `make build` and running whichever binary that corresponds to the benchmarking you wish to perform; e.g.
|
||||
* `./dynamodb-benchmarker`
|
||||
or
|
||||
* `./dax-benchmarker`
|
||||
|
||||
For both the `dynamodb-benchmarker` and the `dax-benchmarker`, additional help and usage flags can be found using `--help`. This way you can tweak your benchmarking experience as necessary.
|
||||
|
||||
**dynamodb-benchmarker help**:
|
||||
```
|
||||
atusa@atusa-thinkpad:~/code/dynamodb-benchmarker$ ./dynamodb-benchmarker --help
|
||||
A CLI tool for simulating heavy usage against DynamoDB and publishing metrics to an Elastic Stack for analysis
|
||||
|
||||
Usage: dynamodb-benchmarker [OPTIONS]
|
||||
|
||||
Options:
|
||||
-c, --concurrent-simulations <CONCURRENT_SIMULATIONS>
|
||||
The number of concurrent simulations to run [default: 1000]
|
||||
-a, --attributes <ATTRIBUTES>
|
||||
The number of attributes to use when populating and querying the DynamoDB table; minimum value of 1 [default: 5]
|
||||
-d, --duration <DURATION>
|
||||
The length of time (in seconds) to run the benchmark for [default: 1800]
|
||||
-b, --buffer <BUFFER>
|
||||
The buffer size of the Elasticsearch thread's MPSC channel [default: 500]
|
||||
-u, --username <USERNAME>
|
||||
Local Elasticsearch cluster username [default: elastic]
|
||||
-p, --password <PASSWORD>
|
||||
Local Elasticsearch cluster password [default: changeme]
|
||||
-i, --index <INDEX>
|
||||
The Elasticsearch Index to insert data into [default: dynamodb]
|
||||
-t, --table-name <TABLE_NAME>
|
||||
The DynamoDB table to perform operations against [default: atusa-high-velocity-table]
|
||||
-r, --read-only
|
||||
Whether to run a read-only scenario for benchmarking
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
```
|
||||
|
||||
**dax-benchmarker help**:
|
||||
```
|
||||
atusa@atusa-thinkpad:~/code/dynamodb-benchmarker$ ./dynamodb-benchmarker --help
|
||||
A CLI tool for simulating heavy usage against DAX and publishing metrics to an Elastic Stack for analysis
|
||||
|
||||
Usage:
|
||||
dax-benchmarker [flags]
|
||||
|
||||
Flags:
|
||||
-a, --attributes int The number of attributes to use when populating and querying the DynamoDB table; minimum value of 1 (default 5)
|
||||
-b, --buffer int The buffer size of the Elasticsearch goroutine's channel (default 500)
|
||||
-c, --concurrent-simulations int The number of concurrent simulations to run (default 1000)
|
||||
-d, --duration int The length of time (in seconds) to run the bechmark for (default 1800)
|
||||
-e, --endpoint string The DAX endpoint to hit when running simulations (assumes secure endpoint, so do not specify port)
|
||||
-h, --help help for dax-benchmarker
|
||||
-i, --index string The Elasticsearch Index to insert data into (default "dax")
|
||||
-p, --password string Local Elasticsearch cluster password (default "changeme")
|
||||
-r, --read-only Whether to run a read-only scenario for benchmarking
|
||||
-t, --table string The DynamoDB table to perform operations against (default "atusa-high-velocity-table")
|
||||
-u, --username string Local Elasticsearch cluster username (default "elastic")
|
||||
```
|
||||
|
||||
#### DAX Benchmarker Gotcha
|
||||
The nature of DAX os that it does not allow external access from outside the VPC it is deployed in. This is due to the
|
||||
fact that DAX uses a proprietary protocol that does not support TLS. So, in order to run the DAX benchmarker, we must
|
||||
push it out and run it from the bastion host that is created in the CDK. This is why the host is created.
|
||||
|
||||
To do this manually, you need to know a few pieces of information:
|
||||
|
||||
* The SSH key used to connect to the host -- if you did not specify a key manually, then the key is `~/.ssh/$USER-dax-pair.pem`
|
||||
* The bastion host's public IP address
|
||||
* The DAX endpoint URI
|
||||
|
||||
The bastion host's public IP and the DAX Endpoint can be obtained using the following commands
|
||||
(You'll need to copy/paste the DAX endpoint once you've SSH'd into the bastion host):
|
||||
|
||||
```shell
|
||||
dax_endpoint=$(aws cloudformation describe-stacks --stack-name "$USER-dax-benchmark-stack" --query "Stacks[0].Outputs[?OutputKey=='DaxEndpoint'].OutputValue" --output text)
|
||||
bastion_host_ip=$(aws cloudformation describe-stacks --stack-name "$USER-dax-benchmark-stack" --query "Stacks[0].Outputs[?OutputKey=='InstancePublicIp'].OutputValue" --output text)
|
||||
```
|
||||
|
||||
Then, you need to upload the `dax-benchmarker` binary to the bastion host:
|
||||
```shell
|
||||
scp -i ~/.ssh/"$USER"-dax-pair.pem dax-benchmarker ec2-user@"$bastion_host_ip":/home/ec2-user/
|
||||
```
|
||||
|
||||
Additionally, you'll need to configure the bastion host to use your current AWS CLI creds; so you'll need to run the following
|
||||
command locally and paste the output exactly into the SSH session:
|
||||
```shell
|
||||
aws configure export-credentials --format env
|
||||
```
|
||||
|
||||
Finally, you need to SSH into the bastion host with remote port forwarding to your local Elasticsearch cluster (port 9200):
|
||||
```shell
|
||||
ssh -i ~/.ssh/"$USER"-dax-pair.pem -R 9200:localhost:9200 ec2-user@"$bastion_host_ip"
|
||||
```
|
||||
|
||||
Once you're SSH'd into the bastion host, you'll need to set the `DAX_ENDPOINT` environment variable using the DAX endpoint spit out
|
||||
by the previous command:
|
||||
|
||||
```shell
|
||||
export DAX_ENDPOINT='PASTE_DAX_ENDPOINT_HERE'
|
||||
```
|
||||
|
||||
Be sure to paste the output of the `aws configure export-credentials --format env` command as well
|
||||
|
||||
Finally, be sure to also export the `AWS_REGION` environment variable that matches the region you deployed your stack into.
|
||||
|
||||
Once you've done all of this, you're ready to run the `dax-benchmarker` from the bastion host and customize the experience however you need using the
|
||||
configuration parameters provided (`./dax-benchmarker -h`).
|
||||
|
||||
### Scenarios
|
||||
By default, for both benchmarkers, they perform CRUD simulations that randomly choose to
|
||||
* Read an existing item
|
||||
* Write a new item and record how long it takes to confirm it's there (deletes the item afterward)
|
||||
* Create a new item and update it, then record how long it takes to confirm the update is reflected in subsequent API calls (deletes the item afterward)
|
||||
|
||||
However, sometimes a more realistic test is to simply run in `read-only` mode; This is supported by both benchmarkers via the `-r, --read-only` flag.
|
||||
|
||||
`read-only` mode, for each concurrent simulation, randomly select a time between 0 and 15 seconds, and then execute a read on an existing item. This simulates more realistic behavior from applications
|
||||
who are only reading from DAX or DynamoDB and not performing any write, update, or delete operations.
|
||||
|
||||
## Accessing the Elastic Stack and analyzing data
|
||||
By default, the Elastic Stack services are at the following URLs when running locally:
|
||||
|
||||
* Elasticsearch -> `http://localhost:9200`
|
||||
* Kibana -> `http://localhost:5601`
|
||||
|
||||
The default credentials for accessing them are
|
||||
|
||||
* Username -> `elastic`
|
||||
* Password -> `changeme`
|
||||
|
||||
Once you're in, you can use Kibana to analyze the data published by the benchmarker.
|
||||
|
||||
This data lives in the `dynamodb` and the `dax` indices of Elasticsearch by default, unless specified otherwise on the clients by the user.
|
||||
|
||||
**Note:** Sometimes the simulations would reach the provisioned throughput thresholds and would be rate-limited by AWS. I set the DynamoDB table to On-Demand
|
||||
to scale out automatically, however this does not always prevent being rate limited. So that is why I also track the Failed Simulations in the Kibana graphs.
|
||||
|
||||
## Populating the DynamoDB benchmarking table with random data
|
||||
By default, the clients and CDK create a DynamoDB table titled `$USER-high-velocity-table`. To run the clients with a different table name, use the `-t, --table` arguments.
|
||||
|
||||
If you wish to populate the table with some data, the easiest way to achieve this is via the [randomly-generate-high-velocity-data](./scripts/randomly-generate-high-velocity-data.sh) script.
|
||||
Simply run it and specify the number of items you wish to populate (rounded to a multiple of 25) via `-i 50`!
|
||||
|
||||
To follow the progress of the script, tail the `/tmp/benchmarker.log` file.
|
||||
|
||||
You can specify different arguments to the script to tweak the settings for the script as necessary:
|
||||
|
||||
```
|
||||
atusa@atusa-thinkpad:~/code/dynamodb-benchmarker$ ./scripts/randomly-generate-high-velocity-data.sh --help
|
||||
randomly-generate-high-velocity-data: A script to randomly generate high-velocity data for some DynamoDB table with random attributes and values for benchmarking purposes.
|
||||
|
||||
USAGE:
|
||||
randomly-generate-high-velocity-data [OPTIONS] [ARGS]...
|
||||
|
||||
-h, --help Show this usage screen
|
||||
|
||||
ARGS:
|
||||
-a, --attributes <ATTRIBUTES> The number of attributes to populate each item in the table with
|
||||
This defaults to 5
|
||||
|
||||
-i, --items <ITEMS> The number of items to populate the table with
|
||||
Items are populated 25 at a time, so whatever number you provide will be rounded to the nearest multiple of 25
|
||||
|
||||
-t, --table <TABLE_NAME> The name of the DynamoDB table to populate
|
||||
This defaults to atusa-high-velocity-table
|
||||
|
||||
```
|
||||
|
||||
These arguments are provided as a convenience to the user if they so wish to populate a table other than the default one created by the CDK.
|
||||
|
||||
## Troubleshooting
|
||||
In the event you need more information about any of the automation, you can check the various log files created throughout the application:
|
||||
|
||||
* `/tmp/ansible-playbook-output.log` -- Generated whenever ansible-playbooks are run from the TUI
|
||||
* `/tmp/benchmarker.log` -- Generated whenever you run the `randomly-generate-high-velocity-data.sh` script outside the TUI
|
||||
* `/tmp/benchmarker-tui.log` -- Generated by events in the TUI
|
||||
* `/tmp/dynamodb-population.log` -- Generated whenever you run the `randomly-generate-high-velocity-data.sh` script from the TUI
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
[defaults]
|
||||
forks = 50
|
||||
gathering = explicit
|
||||
host_key_checking = False
|
||||
nocows = 1
|
||||
retry_files_enabled = False
|
||||
roles_path = ./ansible/roles
|
||||
timeout = 60
|
||||
callback_whitelist = profile_tasks
|
||||
|
||||
[callback_profile_tasks]
|
||||
sort_order = none
|
||||
output_limit = 1000
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# Benchmarking Ansible Automation
|
||||
|
||||
This folder houses all the [Ansible](https://www.ansible.com/) roles to automate the configuration of your local
|
||||
environment and to deploy the necessary DynamoDB and DAX components to AWS. AWS Deployments leverage
|
||||
[AWS CDK](https://aws.amazon.com/cdk/) to automate the provisioning of AWS resources. For more information,
|
||||
navigate to the [CDK directory](../cdk/README.md).
|
||||
|
||||
To just see how to run different plays and their corresponding commands without knowing how it all works together,
|
||||
skip down to the [Plays](#plays) section below.
|
||||
|
||||
Note that if no `ssh_key_name` is provided, the default value is `$USER-dax-pair`
|
||||
|
||||
## Prerequisites
|
||||
* You must be logged into the AWS CLI prior to running the CDK. Ensure you're logged into your target AWS account by running
|
||||
`aws sts get-caller-identity`.
|
||||
* Install pip (Assuming python3 is already installed): `sudo apt-get install python3-pip`
|
||||
* Install the most recent version of Ansible and jmespath from pip: `pip3 install --user ansible jmespath`
|
||||
* Export the local bin path: `export PATH=~/.local/bin:$PATH`
|
||||
* Install curl (`sudo apt-get install curl`)
|
||||
* Install the required Ansible dependencies using Ansible Galaxy (`ansible-galaxy install -r requirements.yml`)
|
||||
|
||||
## Initializing the Stack
|
||||
To initialize the stack (including the local Elastic Stack), run the `deploy_benchmarker.yml` playbook with the `init` tag:
|
||||
```shell
|
||||
ansible-playbook -i inventories/local \
|
||||
--tags init \
|
||||
--ask-become-pass \
|
||||
deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
## Deploying the Stack
|
||||
To deploy the entire benchmarking stack all at once, local and AWS, use the following command:
|
||||
```shell
|
||||
ansible-playbook -i inventories/local \
|
||||
-e vpc_id={{ vpc_id_to_deploy_into }} \
|
||||
deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
The same prerequisites apply to the CDK with the necessary environment or CDK parameters as is defined in the
|
||||
[CDK Parameters](../cdk/README.md#cdk-arguments) section of the CDK README. Ansible will only resolve the following variables
|
||||
for you; all other variables must be supplied by the user a runtime:
|
||||
|
||||
* `localIp`
|
||||
* `awsAccount`
|
||||
|
||||
## Running the benchmarkers
|
||||
To run the benchmarkers, run the following command:
|
||||
```shell
|
||||
ansible-playbook -i inventories/local \
|
||||
-e dax_endpoint={{ the_dax_endpoint_uri }} \
|
||||
run_benchmarkers.yml
|
||||
```
|
||||
|
||||
### Ansible Command Breakdown
|
||||
Let's analyze how an ansible command is formed:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local \
|
||||
-e vpc_id={{ vpc_id_to_deploy_into }} \
|
||||
--ask-become-pass \
|
||||
deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
`ansible-playbook` is the program that runs our playbook, `deploy_benchmarker.yml`. [Playbooks](https://docs.ansible.com/ansible/latest/user_guide/playbooks_intro.html)
|
||||
are the main "blueprints" of automation tasks that Ansible uses.
|
||||
|
||||
`-i inventories/local` tells Ansible that we want to use the hosts and variables associated
|
||||
with the `local` environment. So later in the playbook and
|
||||
[roles](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html), when we're
|
||||
using variables and hosts, we're pulling the corresponding values for this environment. More
|
||||
information about inventories in Ansible can be found
|
||||
[here](https://docs.ansible.com/ansible/2.3/intro_inventory.html). Inventories would be a good place
|
||||
to start learning about Ansible if you're confused by what's happening in this module.
|
||||
|
||||
[This](./inventories/local/host_vars/localhost.yml) is where you'd put variables to persist between runs of this application.
|
||||
By default, they are only provided for you if you follow the steps in the main repository script.
|
||||
|
||||
`-e vpc_id={{ vpc_id_to_deploy_into }}` is setting an extra variable for the playbook to use (fun fact: `-e` is an alias
|
||||
for `--extra-vars`). This variable is not defined by default in your [local host vars](./inventories/local/host_vars/localhost.yml) because
|
||||
we don't know what VPC you want to deploy the stack into. If you're running this using the main TUI script in the root
|
||||
of this repo, then this is handled graphically for you. This will be set on the first run of the CDK deployment, so you do not have to specify
|
||||
the `vpc_id` between subsequent runs. Otherwise, if you wish to change the VPC ID for any reason (including prior to an initial run), and
|
||||
you wish to run this Ansible playbook manually, you can add it to your host vars file.
|
||||
|
||||
`--ask-become-pass` is telling Ansible to prompt you for your sudo password, so it can run installs and other configuration tasks on your behalf.
|
||||
|
||||
`deploy_benchmarker.yml` is the name of our playbook that we want Ansible to run.
|
||||
|
||||
## Using Tags to Control What is Deployed
|
||||
Each part of the `deploy_benchmarker.yml` playbook has
|
||||
[tags](https://docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html) associated with them.
|
||||
These tags allow us to tell Ansible which part(s) of the playbook we want to run. In other words, tags
|
||||
allow us to tell Ansible which parts of the overall Logstash deployment pipeline we want to run.
|
||||
|
||||
They `deploy_benchmarker.yml` playbook (and a couple of roles) has the following tags in it:
|
||||
|
||||
* `init`
|
||||
* `init_elk`
|
||||
* `stop_elk`
|
||||
* `prerequisites`
|
||||
* `elk`
|
||||
* `cdk`
|
||||
* `run`
|
||||
* `deploy`
|
||||
* `destroy`
|
||||
* `destroy_key_pair`
|
||||
* `upload`
|
||||
* `dynamodb`
|
||||
* `dax`
|
||||
* `crud`
|
||||
* `read-only`
|
||||
|
||||
To view all these tags and their associated plays from the `ansible` CLI, run
|
||||
|
||||
```shell
|
||||
ansible-playbook deploy_benchmarker.yml --list-tags
|
||||
```
|
||||
|
||||
Using these tags, we can specify that we only want to run specific parts of the Benchmarking Deployment pipeline that's
|
||||
defined in the `deploy_benchmarker.yml` playbook.
|
||||
|
||||
For example: If we only wanted to start the ELK (Elasticsearch-Logstash-Kibana) stack, we would run this:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags elk deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
Likewise, if we wanted to stop the ELK stack, we'd run this:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags stop_elk deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
Note the `--tags` argument. This allows us to tell Ansible to only run tasks or roles that have the
|
||||
`elk` or `stop_elk` tag on them.
|
||||
|
||||
We can also specify multiple arguments for `--tags` if we wish; for example, if we wanted to simply spin up the local
|
||||
Elastic stack (synonymous with ELK stack), and deploy the CDK, we'd run the following:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local -e vpc_id=vpc-1234567890 --tags 'elk,cdk' deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
## Plays
|
||||
The following plays can be run from these playbooks using the tags with the following commands:
|
||||
|
||||
#### Initialize Your Local Environment and Elastic Stack
|
||||
A sudo password is required to install applications, so we tell Ansible to prompt us for it at the start:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags init deploy_benchmarker.yml --ask-become-pass
|
||||
```
|
||||
|
||||
#### Deploy CDK and Run the Benchmarkers on the Bastion Host
|
||||
This assumes you already know the VPC ID to deploy into and have already created an SSH key pair and have the key pair
|
||||
locally in your `~/.ssh` directory with a `.pem` extension.
|
||||
|
||||
If you did not do this manually, it was done for you automatically and the created pair is under `~/.ssh/$USER-dax-pair.pem`.
|
||||
|
||||
You can either specify the `vpc_id` argument directly via `-e` in the command, or you can hard code
|
||||
it in your [host_vars](./inventories/local/host_vars/localhost.yml). You must also already be logged into the AWS CLI for
|
||||
your target environment, or specify a `profile_id` either in your `host_vars` or via `-e`, along with an `aws_region`. If you're not
|
||||
already logged into AWS, your `profile_id` must be configured to be picked up automatically from your `~/.aws/config` or
|
||||
`~/.aws/credentials` files with no additional login steps in order to deploy to AWS.
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local -e vpc_id=vpc-1234567890 --tags deploy deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
#### Shut Down Your Local Elastic Stack
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags stop_elk deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
#### Wipe Away everything
|
||||
Once more, this assumes you either have the DAX
|
||||
endpoint and the VPC ID hardcoded in your [host vars](./inventories/local/host_vars/localhost.yml), or you provide them via `-e`.
|
||||
|
||||
If you've already run a CDK deploy via Ansible, then you should not need to specify anything.
|
||||
|
||||
**Note:** For safety purposes, this will _not_ wipe away the `ssk_key_name` in your `~/.ssh` directory. If you specified
|
||||
a pre-existing key to use for this deployment, it will not be touched. If you did not specify a key name, the automatically
|
||||
generated key `$USER-dax-pair` will be left in your `~/.ssh` directory. If you wish to delete this pair from your local machine
|
||||
and remove it from AWS, also specify the `destroy_key_pair` tag as well in the below command.
|
||||
|
||||
You can either specify the `vpc_id` argument directly via `-e` in the command, or you can hard code
|
||||
it in your [host_vars](./inventories/local/host_vars/localhost.yml). You must also already be logged into the AWS CLI for
|
||||
your target environment, or specify a `profile_id` either in your `host_vars` or via `-e`, along with an `aws_region`. If you're not
|
||||
already logged into AWS, your `profile_id` must be configured to be picked up automatically from your `~/.aws/config` or
|
||||
`~/.aws/credentials` files with no additional login steps in order to deploy to AWS.
|
||||
|
||||
**Destroy Everything, But Leave the ssh_key_name Key-Pair Alone:**
|
||||
```shell
|
||||
ansible-playbook -i inventories/local -e vpc_id=vpc-1234567890 --tags destroy deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
**Destroy Everything, Including the ssh_key_name Key-Pair**
|
||||
```shell
|
||||
ansible-playbook -i inventories/local -e vpc_id=vpc-1234567890 --tags 'destroy,destroy_key_pair' deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
### Additional Plays You Can Run
|
||||
|
||||
#### Only Install Prerequisites for Local Machine
|
||||
A sudo password is required to install applications, so we tell Ansible to prompt us for it at the start:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags prerequisites deploy_benchmarker.yml --ask-become-pass
|
||||
```
|
||||
|
||||
#### Start Your Local Elastic Stack
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags elk deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
#### Just Deploy the CDK
|
||||
This assumes you already know the VPC ID to deploy into and have already created an SSH key pair and have the key pair
|
||||
locally in your `~/.ssh` directory with a `.pem` extension. If you did not do this manually, it was done for you automatically
|
||||
and the created pair is under `~/.ssh/$USER-dax-pair.pem`. You can either specify the `vpc_id`
|
||||
argument directly via `-e` in the command, or you can hard code it in your [host_vars](./inventories/local/host_vars/localhost.yml).
|
||||
|
||||
If you've already run a CDK deploy via Ansible, then you should not need to specify anything.
|
||||
|
||||
You must also already be logged into the AWS CLI for your target environment, or specify a `profile_id` either in your
|
||||
`host_vars` or via `-e`, along with an `aws_region`. If you're not already logged into AWS, your `profile_id` must be
|
||||
configured to be picked up automatically from your `~/.aws/config` or `~/.aws/credentials` files with no additional
|
||||
login steps in order to deploy to AWS.
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags cdk deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
#### Only Upload the Benchmarkers to the Bastion Host
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags upload deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
#### Run All Benchmarkers and Scenarios
|
||||
This assumes the CDK is already deployed and an EC2 instance already exists. This also assumes you either have the DAX
|
||||
endpoint and the VPC ID hardcoded in your [host vars](./inventories/local/host_vars/localhost.yml), or you provide them via `-e`.
|
||||
If you've already run a CDK deploy via Ansible, then you should not need to specify anything.
|
||||
|
||||
Additionally, You must also already be logged into the AWS CLI for
|
||||
your target environment, or specify a `profile_id` either in your `host_vars` or via `-e`, along with an `aws_region`. If you're not
|
||||
already logged into AWS, your `profile_id` must be configured to be picked up automatically from your `~/.aws/config` or
|
||||
`~/.aws/credentials` files with no additional login steps in order to deploy to AWS:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags run run_benchmarkers.yml
|
||||
```
|
||||
|
||||
#### Only Run the DynamoDB/DAX Benchmarker
|
||||
This assumes the CDK is already deployed and an EC2 instance already exists. This also assumes you either have the DAX
|
||||
endpoint and the VPC ID hardcoded in your [host vars](./inventories/local/host_vars/localhost.yml), or you provide them via `-e`.
|
||||
If you've already run a CDK deploy via Ansible, then you should not need to specify anything.
|
||||
|
||||
Additionally, You must also already be logged into the AWS CLI for
|
||||
your target environment, or specify a `profile_id` either in your `host_vars` or via `-e`, along with an `aws_region`. If you're not
|
||||
already logged into AWS, your `profile_id` must be configured to be picked up automatically from your `~/.aws/config` or
|
||||
`~/.aws/credentials` files with no additional login steps in order to deploy to AWS:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags dynamodb deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags dax deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
Note the difference in tags: `dynamodb` and `dax`
|
||||
|
||||
#### Only Run the Benchmarkers in CRUD/READONLY mode
|
||||
This assumes the CDK is already deployed and an EC2 instance already exists. This also assumes you either have the DAX
|
||||
endpoint and the VPC ID hardcoded in your [host vars](./inventories/local/host_vars/localhost.yml), or you provide them via `-e`.
|
||||
If you've already run a CDK deploy via Ansible, then you should not need to specify anything.
|
||||
|
||||
Additionally, You must also already be logged into the AWS CLI for
|
||||
your target environment, or specify a `profile_id` either in your `host_vars` or via `-e`, along with an `aws_region`. If you're not
|
||||
already logged into AWS, your `profile_id` must be configured to be picked up automatically from your `~/.aws/config` or
|
||||
`~/.aws/credentials` files with no additional login steps in order to deploy to AWS:
|
||||
|
||||
**CRUD:**
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags crud deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
**read-only:**
|
||||
```shell
|
||||
ansible-playbook -i inventories/local --tags read-only deploy_benchmarker.yml
|
||||
```
|
||||
|
||||
## Supported Variables
|
||||
The following variables are supported to be specified via the `-e` argument when running the `deploy_benchmarker.yml`
|
||||
playbook:
|
||||
|
||||
| Variable Name | Description | Required? |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|-----------|
|
||||
| `profile_id` | The name of the AWS CLI profile you wish to deploy with; <br>Defaults to using the `AWS_PROFILE` environment variable | |
|
||||
| `vpc_id` | The ID of the VPC in the AWS account you're deploying to where you want the CDK components created <br>Only required on first run only | * |
|
||||
| `local_ip` | The public IP of your local machine; <br>Defaults to the response from `curl -s -L checkip.amazonaws.com` | |
|
||||
| `ssh_key_name` | The name of the SSH key-pair that will be used when creating the EC2 instance to allow you SSH access to it; <br>Defaults to `$USER-dax-pair` | |
|
||||
| `aws_account` | The account ID of the AWS account you're deploying into; <br>Defaults to the result of `aws sts get-caller-identity \| jq -r .Account` | |
|
||||
| `base_table_name` | The base name to use when creating the DynamoDB table; <br>Defaults to `high-velocity-table` | |
|
||||
| `cdk_action` | The action to perform when deploying the CDK; <br>Defaults to `deploy` | |
|
||||
| `duration` | How long to run each simulation for; <br>Defaults to 1800 seconds | |
|
||||
| `benchmarker` | Which benchmarker to run (i.e. `dynamodb` or `dax`) | |
|
||||
| `dax_endpoint` | The DAX URI to use to hit the DAX cluster; <br>Only required when running the benchmarkers and without an initial CDK deploy) | * |
|
||||
|
||||
## Run Order
|
||||
When first running from scratch, you'll want to run with the `init` tags first to initialize the Elastic Stack and install the prerequisites, then run again without any tags to actually
|
||||
deploy everything and run the benchmarkers. If you only want to run the benchmarkers, run the `run_benchmarkers.yml` playbook, or specify the `run` tag.
|
||||
|
||||
## Troubleshooting
|
||||
You can generally get more information about your problem by adding `-vvv` to the end of your
|
||||
`ansible-playbook` command. The more `v`'s you add, the more verbose the output and the more information
|
||||
you will get. For example:
|
||||
|
||||
```shell
|
||||
ansible-playbook -i inventories/local -e cdk_action=destroy --tags 'elk,cdk' deploy_benchmarker.yml -vvv
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
- name: Deploy the benchmarking components
|
||||
connection: local
|
||||
hosts: local
|
||||
gather_facts: yes
|
||||
roles:
|
||||
- { role: install_prerequisites, tags: [ never, prerequisites, init ] }
|
||||
- { role: configure_elastic_stack, tags: elk }
|
||||
- { role: deploy_cdk, tags: [ cdk, deploy ] }
|
||||
- { role: destroy, tags: [ never, destroy ], cdk_action: destroy }
|
||||
tasks:
|
||||
- name: Populate the DynamoDB table with random data
|
||||
shell:
|
||||
chdir: ../scripts
|
||||
cmd: ./randomly-generate-high-velocity-data.sh -i 5000
|
||||
tags: deploy
|
||||
|
||||
- name: Build the benchmarkers using the Makefile
|
||||
shell:
|
||||
chdir: ../
|
||||
cmd: make build
|
||||
tags: deploy
|
||||
|
||||
- name: Upload the benchmarkers to the bastion host
|
||||
hosts: bastion
|
||||
gather_facts: yes
|
||||
vars:
|
||||
ssh_key_name: "{{ hostvars['localhost']['ssh_key_name'] }}"
|
||||
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}.pem"
|
||||
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
|
||||
remote_user: ec2-user
|
||||
tags: [ upload, deploy ]
|
||||
tasks:
|
||||
- copy:
|
||||
src: "../{{ item }}"
|
||||
dest: .
|
||||
mode: 0777
|
||||
loop:
|
||||
- dynamodb-benchmarker
|
||||
- dax-benchmarker
|
||||
|
||||
- import_playbook: run_benchmarkers.yml
|
||||
@@ -0,0 +1,8 @@
|
||||
user_name: "{{ lookup('env', 'USER') }}"
|
||||
ssh_key_name: "{{ lookup('env', 'USER') }}-dax-pair"
|
||||
profile_id: "{{ lookup('env', 'AWS_PROFILE') }}"
|
||||
aws_region: "{{ lookup('env', 'AWS_REGION') }}"
|
||||
stack_name: "{{ user_name }}-dax-benchmark-stack"
|
||||
vpc_id:
|
||||
base_table_name:
|
||||
dax_endpoint:
|
||||
@@ -0,0 +1,3 @@
|
||||
local:
|
||||
hosts:
|
||||
localhost:
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.general
|
||||
- name: amazon.aws
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,32 @@
|
||||
- name: Clone the docker-elk repo
|
||||
git:
|
||||
repo: https://github.com/deviantony/docker-elk.git
|
||||
dest: ../../docker-elk
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Build the docker-elk stack just in case a pre-existing version of Elasticsearch needs its nodes upgraded
|
||||
shell:
|
||||
chdir: ../../docker-elk
|
||||
cmd: docker compose build
|
||||
|
||||
- name: Start the docker-elk setup container
|
||||
shell:
|
||||
chdir: ../../docker-elk
|
||||
cmd: docker-compose up setup
|
||||
|
||||
- name: Start the docker-elk stack
|
||||
shell:
|
||||
chdir: ../../docker-elk
|
||||
cmd: docker compose up -d
|
||||
|
||||
- name: Wait 20 seconds for the ELK stack to start
|
||||
pause:
|
||||
seconds: 20
|
||||
|
||||
- name: Import the benchmarking dashboards into Kibana
|
||||
shell:
|
||||
cmd: >
|
||||
curl -X POST http://localhost:5601/api/saved_objects/_import?overwrite=true
|
||||
-H 'kbn-xsrf: true'
|
||||
-u 'elastic:changeme'
|
||||
--form file=@roles/configure_elastic_stack/files/benchmarker-dashboards.ndjson
|
||||
@@ -0,0 +1,8 @@
|
||||
- { import_tasks: init_elk_stack.yml, tags: [ never, init, init_elk ] }
|
||||
- { import_tasks: stop_elk_stack.yml, tags: [ never, stop_elk ] }
|
||||
|
||||
- name: Start the docker-elk stack
|
||||
shell:
|
||||
chdir: ../../docker-elk
|
||||
cmd: docker compose up -d
|
||||
tags: deploy
|
||||
@@ -0,0 +1,4 @@
|
||||
- name: Stop the docker-elk stack
|
||||
shell:
|
||||
chdir: ../../docker-elk
|
||||
cmd: docker compose down
|
||||
@@ -0,0 +1,119 @@
|
||||
- name: Check if a key-pair following the specified format already exists
|
||||
stat:
|
||||
path: "{{ ansible_env.HOME }}/.ssh/{{ ssh_key_name }}.pem"
|
||||
register: key_pair
|
||||
changed_when: no
|
||||
when: "'destroy' not in ansible_run_tags"
|
||||
|
||||
- block:
|
||||
- name: Create a new key-pair
|
||||
ec2_key:
|
||||
name: "{{ ssh_key_name }}"
|
||||
register: aws_key_pair
|
||||
|
||||
- name: Create the new pem file
|
||||
file:
|
||||
path: "{{ ansible_env.HOME }}/.ssh/{{ ssh_key_name }}.pem"
|
||||
state: touch
|
||||
mode: '0400'
|
||||
|
||||
- name: Add the generated key-pair to the new file
|
||||
blockinfile:
|
||||
path: "{{ ansible_env.HOME }}/.ssh/{{ ssh_key_name }}.pem"
|
||||
block: "{{ aws_key_pair.key.private_key }}"
|
||||
|
||||
when:
|
||||
- "'destroy' not in ansible_run_tags"
|
||||
- not key_pair.stat.exists
|
||||
|
||||
- name: Fetch the current system's public IP
|
||||
shell:
|
||||
cmd: curl -s -L checkip.amazonaws.com
|
||||
register: public_ip_resp
|
||||
|
||||
- name: Fetch the current AWS account ID
|
||||
shell:
|
||||
cmd: aws sts get-caller-identity | jq -r .Account
|
||||
register: aws_account_resp
|
||||
|
||||
- name: Install CDK dependencies
|
||||
npm:
|
||||
ci: yes
|
||||
path: ../cdk
|
||||
|
||||
- name: Bootstrapping the AWS environment
|
||||
shell:
|
||||
chdir: ../cdk
|
||||
cmd: >
|
||||
npm run build && yes | npm run cdk bootstrap --
|
||||
--no-color --require-approval never
|
||||
--profile {{ profile_id | default("personal") }}
|
||||
-c vpcId={{ vpc_id }}
|
||||
-c localIp={{ public_ip_resp.stdout }}
|
||||
-c sshKeyName={{ ssh_key_name }}
|
||||
-c awsAccount={{ aws_account_resp.stdout }}
|
||||
-c baseTableName={{ base_table_name | default('') }}
|
||||
|
||||
- name: Deploying Benchmarking CDK
|
||||
shell:
|
||||
chdir: ../cdk
|
||||
cmd: >
|
||||
npm run build && yes | npm run cdk {{ cdk_action | default("deploy") }} --
|
||||
--no-color --require-approval never
|
||||
--profile {{ profile_id | default("personal") }}
|
||||
-c vpcId={{ vpc_id }}
|
||||
-c localIp={{ public_ip_resp.stdout }}
|
||||
-c sshKeyName={{ ssh_key_name }}
|
||||
-c awsAccount={{ aws_account_resp.stdout }}
|
||||
-c baseTableName={{ base_table_name | default('') }}
|
||||
register: cdk_response
|
||||
|
||||
- name: Benchmarking CDK deployment summary
|
||||
debug:
|
||||
msg: "{{ cdk_response.stderr_lines }}"
|
||||
|
||||
- block:
|
||||
- name: Fetch the benchmark stack outputs
|
||||
cloudformation_info:
|
||||
stack_name: "{{ stack_name }}"
|
||||
register: benchmark_stack
|
||||
|
||||
- name: Extracting the bastion host IP
|
||||
set_fact:
|
||||
bastion_host_ip: "{{ benchmark_stack.cloudformation[stack_name].stack_outputs['InstancePublicIp'] }}"
|
||||
|
||||
- name: Extracting DAX endpoint
|
||||
set_fact:
|
||||
dax_endpoint: "{{ benchmark_stack.cloudformation[stack_name].stack_outputs['DaxEndpoint'] }}"
|
||||
|
||||
- name: Setting the dax_endpoint variable in the host vars if it doesn't exist already
|
||||
lineinfile:
|
||||
path: inventories/local/host_vars/localhost.yml
|
||||
line: "dax_endpoint: {{ dax_endpoint }}"
|
||||
regexp: '^dax_endpoint:'
|
||||
|
||||
- name: Setting the vpc_id variable in the host vars if it doesn't exist already
|
||||
lineinfile:
|
||||
path: inventories/local/host_vars/localhost.yml
|
||||
line: "vpc_id: {{ vpc_id }}"
|
||||
regexp: '^vpc_id:'
|
||||
|
||||
- block:
|
||||
- name: Setting the bastion host IP if it doesnt exist in the inventory
|
||||
lineinfile:
|
||||
path: inventories/local/hosts.yml
|
||||
line: |
|
||||
bastion:
|
||||
hosts:
|
||||
{{ bastion_host_ip }}:
|
||||
regexp: 'bastion:\n\s*hosts:\n\s*(?:\d{1,3}\.){3}\d{1,3}:'
|
||||
insertafter: EOF
|
||||
|
||||
- name: Add the bastion host to the bastion group
|
||||
add_host:
|
||||
name: "{{ bastion_host_ip }}"
|
||||
groups: bastion
|
||||
when:
|
||||
- "'bastion' not in groups"
|
||||
- "'bastion' not in group_names"
|
||||
when: "'destroy' not in ansible_run_tags"
|
||||
@@ -0,0 +1,54 @@
|
||||
- name: Wipe away local Elastic Stack
|
||||
shell:
|
||||
chdir: ../../docker-elk
|
||||
cmd: docker compose down -v
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Wipe away the ELK directory
|
||||
file:
|
||||
path: ../../docker-elk
|
||||
state: absent
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Run CDK Destroy
|
||||
import_role:
|
||||
name:
|
||||
deploy_cdk
|
||||
|
||||
- name: Delete the key-pair from AWS
|
||||
ec2_key:
|
||||
name: "{{ ssh_key_name }}"
|
||||
state: absent
|
||||
ignore_errors: yes
|
||||
tags: [ never, destroy_key_pair ]
|
||||
|
||||
- name: Delete the key pair from your local machine
|
||||
file:
|
||||
path: "{{ ansible_env.HOME }}/.ssh/{{ ssh_key_name }}.pem"
|
||||
state: absent
|
||||
ignore_errors: yes
|
||||
tags: [ never, destroy_key_pair ]
|
||||
|
||||
- name: Remove the bastion host from the bastion host group
|
||||
replace:
|
||||
path: inventories/local/hosts.yml
|
||||
replace: ''
|
||||
regexp: '^bastion:\n\s*hosts:\n\s*(?:\d{1,3}\.){3}\d{1,3}:'
|
||||
|
||||
- name: Reset the dax_endpoint variable in the host vars
|
||||
lineinfile:
|
||||
path: inventories/local/host_vars/localhost.yml
|
||||
line: 'dax_endpoint:'
|
||||
regexp: '^dax_endpoint:'
|
||||
|
||||
- name: Reset the vpc_id variable in the host vars
|
||||
lineinfile:
|
||||
path: inventories/local/host_vars/localhost.yml
|
||||
line: 'vpc_id:'
|
||||
regexp: '^vpc_id:'
|
||||
|
||||
- name: Clean the repository using the Makefile
|
||||
shell:
|
||||
chdir: ../
|
||||
cmd:
|
||||
make clean
|
||||
@@ -0,0 +1,22 @@
|
||||
- name: Add Docker's official GPG key
|
||||
apt_key:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
keyring: /etc/apt/keyrings/docker.gpg
|
||||
|
||||
- name: Set up docker APT repository
|
||||
apt_repository:
|
||||
repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
|
||||
|
||||
- name: Install the required APT dependencies
|
||||
apt:
|
||||
update_cache: yes
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- docker-compose
|
||||
- containerd.io
|
||||
- docker-compose-plugin
|
||||
- jq
|
||||
- unzip
|
||||
- curl
|
||||
- git
|
||||
@@ -0,0 +1,26 @@
|
||||
- name: Check if AWS CLI is installed
|
||||
shell:
|
||||
cmd: hash aws 2> /dev/null
|
||||
ignore_errors: yes
|
||||
changed_when: no
|
||||
register: awscli_installation_status
|
||||
|
||||
- block:
|
||||
- name: Download the AWS CLI from AWS
|
||||
unarchive:
|
||||
src: https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip
|
||||
dest: "{{ ansible_env.HOME }}/Downloads"
|
||||
group: "{{ user_name }}"
|
||||
owner: "{{ user_name }}"
|
||||
remote_src: yes
|
||||
|
||||
- name: Install the AWS CLI
|
||||
shell:
|
||||
cmd: "{{ ansible_env.HOME }}/Downloads/aws/install"
|
||||
|
||||
- name: Cleanup downloaded AWS installation files
|
||||
file:
|
||||
path: "{{ ansible_env.HOME }}/Downloads/aws/"
|
||||
state: absent
|
||||
|
||||
when: awscli_installation_status.rc | int != 0
|
||||
@@ -0,0 +1,15 @@
|
||||
- name: Check if Go is installed
|
||||
shell:
|
||||
cmd: command -v go 2> /dev/null
|
||||
ignore_errors: yes
|
||||
changed_when: no
|
||||
register: go_installation_status
|
||||
|
||||
- name: Install Go 1.20
|
||||
unarchive:
|
||||
src: https://go.dev/dl/go1.20.5.linux-amd64.tar.gz
|
||||
dest: /usr/local
|
||||
creates: /usr/local/go
|
||||
remote_src: yes
|
||||
become: yes
|
||||
when: go_installation_status.rc | int != 0
|
||||
@@ -0,0 +1,25 @@
|
||||
- { import_tasks: aws_cli.yml, become: yes }
|
||||
- import_tasks: rust.yml
|
||||
- import_tasks: go.yml
|
||||
- import_tasks: node.yml
|
||||
- { import_tasks: apt.yml, become: yes }
|
||||
|
||||
- name: Install CDK
|
||||
npm:
|
||||
name: "{{ item }}"
|
||||
global: yes
|
||||
loop:
|
||||
- aws-cdk
|
||||
- typescript
|
||||
|
||||
- name: Check if golangci-lint is installed
|
||||
shell:
|
||||
cmd: command -v golangci-lint 2> /dev/null
|
||||
ignore_errors: yes
|
||||
changed_when: no
|
||||
register: golangci_lint_installation_status
|
||||
|
||||
- name: Install golangci-lint
|
||||
shell:
|
||||
cmd: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.53.3
|
||||
when: golangci_lint_installation_status.rc | int != 0
|
||||
@@ -0,0 +1,34 @@
|
||||
- name: Check if node is installed
|
||||
shell:
|
||||
cmd: hash node 2> /dev/null
|
||||
ignore_errors: yes
|
||||
changed_when: no
|
||||
register: node_installation_status
|
||||
|
||||
- block:
|
||||
- name: Install nvm
|
||||
shell: >
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
args:
|
||||
creates: "{{ ansible_env.HOME }}/.nvm/nvm.sh"
|
||||
|
||||
- name: Install Node.JS
|
||||
shell:
|
||||
cmd: |
|
||||
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
nvm install node
|
||||
|
||||
- name: Add NVM exports to bashrc
|
||||
lineinfile:
|
||||
path: "{{ ansible_env.HOME }}/.bashrc"
|
||||
line: 'export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"'
|
||||
regexp: '^export NVM_DIR=.+'
|
||||
|
||||
- name: Add NVM script to bashrc
|
||||
lineinfile:
|
||||
path: "{{ ansible_env.HOME }}/.bashrc"
|
||||
line: '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"'
|
||||
regexp: '\[ -s |\$NVM_DIR/nvm\.sh \].+'
|
||||
|
||||
when: node_installation_status.rc | int != 0
|
||||
@@ -0,0 +1,11 @@
|
||||
- name: Check if rustup is installed
|
||||
shell:
|
||||
cmd: command -v rustup 2> /dev/null
|
||||
ignore_errors: yes
|
||||
changed_when: no
|
||||
register: rustup_installation_status
|
||||
|
||||
- name: Install Rust via Rustup
|
||||
shell: >
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
when: rustup_installation_status.rc | int != 0
|
||||
@@ -0,0 +1,110 @@
|
||||
- name: Get AWS Credentials
|
||||
connection: local
|
||||
hosts: local
|
||||
gather_facts: yes
|
||||
tags: [ run, deploy ]
|
||||
tasks:
|
||||
- name: Ensure the user is logged into their AWS CLI
|
||||
assert:
|
||||
that:
|
||||
- aws_region is defined
|
||||
- profile_id is defined
|
||||
- dax_endpoint is defined
|
||||
|
||||
- name: Get the environment variables to set on the bastion host for the current AWS profile
|
||||
shell:
|
||||
cmd: aws configure export-credentials
|
||||
register: aws_creds
|
||||
|
||||
- name: Register the aws_creds as a fact for the benchmarkers playbook to receive
|
||||
set_fact:
|
||||
aws_credentials: "{{ aws_creds.stdout }}"
|
||||
|
||||
- name: Run the benchmarkers
|
||||
hosts: bastion
|
||||
gather_facts: no
|
||||
vars:
|
||||
ssh_key_name: "{{ hostvars['localhost']['ssh_key_name'] }}"
|
||||
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}.pem"
|
||||
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -R 9200:localhost:9200'
|
||||
tags: [ run, deploy ]
|
||||
remote_user: ec2-user
|
||||
tasks:
|
||||
- name: Run the DynamoDB benchmarker in CRUD mode
|
||||
shell:
|
||||
cmd: >
|
||||
export AWS_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('AccessKeyId') }}";
|
||||
export AWS_SECRET_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SecretAccessKey') }}";
|
||||
export AWS_SESSION_TOKEN="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SessionToken') }}";
|
||||
export AWS_CREDENTIAL_EXPIRATION="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('Expiration') }}";
|
||||
export AWS_REGION="{{ hostvars['localhost']['aws_region'] }}";
|
||||
./dynamodb-benchmarker -d "{{ duration | default(1800) | int }}" -t "{{ hostvars['localhost']['user_name'] }}"-high-velocity-table
|
||||
executable: /bin/bash
|
||||
tags:
|
||||
- dynamodb
|
||||
- crud
|
||||
|
||||
- name: Run the DynamoDB benchmarker in read-only mode
|
||||
shell:
|
||||
cmd: >
|
||||
export AWS_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('AccessKeyId') }}";
|
||||
export AWS_SECRET_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SecretAccessKey') }}";
|
||||
export AWS_SESSION_TOKEN="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SessionToken') }}";
|
||||
export AWS_CREDENTIAL_EXPIRATION="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('Expiration') }}";
|
||||
export AWS_REGION="{{ hostvars['localhost']['aws_region'] }}";
|
||||
./dynamodb-benchmarker -d "{{ duration | default(1800) | int }}" -t "{{ hostvars['localhost']['user_name'] }}"-high-velocity-table -r
|
||||
executable: /bin/bash
|
||||
tags:
|
||||
- dynamodb
|
||||
- read-only
|
||||
|
||||
- name: Run the DAX benchmarker in CRUD mode
|
||||
shell:
|
||||
cmd: >
|
||||
export AWS_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('AccessKeyId') }}";
|
||||
export AWS_SECRET_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SecretAccessKey') }}";
|
||||
export AWS_SESSION_TOKEN="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SessionToken') }}";
|
||||
export AWS_CREDENTIAL_EXPIRATION="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('Expiration') }}";
|
||||
export AWS_REGION="{{ hostvars['localhost']['aws_region'] }}";
|
||||
export DAX_ENDPOINT="{{ hostvars['localhost']['dax_endpoint'] }}";
|
||||
unset cmd;
|
||||
basecmd='./dax-benchmarker -c 100
|
||||
-d 115
|
||||
-t "{{ hostvars['localhost']['user_name'] }}"-high-velocity-table
|
||||
-e "{{ hostvars['localhost']['dax_endpoint'] }}"';
|
||||
for i in $(seq 1 9); do
|
||||
cmd+="$basecmd & ";
|
||||
done;
|
||||
cmd+="$basecmd";
|
||||
timeout -s SIGINT "{{ duration | default(1800) | int }}" bash -c "while :; do $cmd; done"
|
||||
executable: /bin/bash
|
||||
ignore_errors: yes
|
||||
tags:
|
||||
- dax
|
||||
- crud
|
||||
|
||||
- name: Run the DAX benchmarker in read-only mode
|
||||
shell:
|
||||
cmd: >
|
||||
export AWS_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('AccessKeyId') }}";
|
||||
export AWS_SECRET_ACCESS_KEY="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SecretAccessKey') }}";
|
||||
export AWS_SESSION_TOKEN="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('SessionToken') }}";
|
||||
export AWS_CREDENTIAL_EXPIRATION="{{ hostvars['localhost']['aws_credentials'] | community.general.json_query('Expiration') }}";
|
||||
export AWS_REGION="{{ hostvars['localhost']['aws_region'] }}";
|
||||
export DAX_ENDPOINT="{{ hostvars['localhost']['dax_endpoint'] }}";
|
||||
unset cmd;
|
||||
basecmd='./dax-benchmarker -c 100
|
||||
-d 115
|
||||
-r
|
||||
-t "{{ hostvars['localhost']['user_name'] }}"-high-velocity-table
|
||||
-e "{{ hostvars['localhost']['dax_endpoint'] }}"';
|
||||
for i in $(seq 1 9); do
|
||||
cmd+="$basecmd & ";
|
||||
done;
|
||||
cmd+="$basecmd";
|
||||
timeout -s SIGINT "{{ duration | default(1800) | int }}" bash -c "while :; do $cmd; done"
|
||||
executable: /bin/bash
|
||||
ignore_errors: yes
|
||||
tags:
|
||||
- dax
|
||||
- read-only
|
||||
Executable
+264
@@ -0,0 +1,264 @@
|
||||
#!/bin/bash
|
||||
export PATH="$HOME"/.local/bin:$PATH
|
||||
|
||||
source scripts/logger.sh /tmp/benchmarker-tui.log
|
||||
source scripts/ui_utils.sh
|
||||
|
||||
ANSIBLE_LOG_FILE=/tmp/ansible-playbook-output.log
|
||||
rm "$ANSIBLE_LOG_FILE" > /dev/null 2>&1 &
|
||||
|
||||
verify-prerequisites() {
|
||||
log-info "Verifying prerequisites"
|
||||
declare prerequisites=(whiptail jq dialog)
|
||||
|
||||
if ! (aws sts get-caller-identity > /dev/null 2>&1); then
|
||||
log-error "Must be logged into AWS CLI to use this script. Log into the target AWS account and run this script again" true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for application in "${prerequisites[@]}"; do
|
||||
if ! (command -v "$application" > /dev/null 2>&1); then
|
||||
log-warn "$application is required to run this script. Installing $application..."
|
||||
sudo apt install "$application"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! (command -v ansible > /dev/null 2>&1); then
|
||||
log-warn "Ansible is required to run this script. Installing Ansible..." true
|
||||
sudo apt install python3-pip
|
||||
pip3 install --user ansible jmespath
|
||||
fi
|
||||
|
||||
if ! (ansible-galaxy collection list | grep -i "community.general\|amazon.aws" > /dev/null 2>&1); then
|
||||
log-warn "Installing Ansible galaxy requirements..." true
|
||||
cd ansible
|
||||
ansible-galaxy install -r requirements.yml
|
||||
cd -
|
||||
fi
|
||||
}
|
||||
|
||||
initialize-environment() {
|
||||
check-sudo-pass "Installing dependencies requires sudo permissions."
|
||||
if [[ "$?" == 0 ]]; then
|
||||
log-info "Sudo pass: $PASSWORD"
|
||||
declare title="Initialize Local Environment"
|
||||
|
||||
if (prompt-yes-no "$title"); then
|
||||
cd ansible
|
||||
|
||||
ansible-playbook -i inventories/local -e "ansible_become_password=$PASSWORD" --tags init deploy_benchmarker.yml > "$ANSIBLE_LOG_FILE" 2>&1 &
|
||||
pid=$!
|
||||
log-info "Running ansible-playbook 'deploy_benchmarker.yml' with the 'init' tag and logging output to file [$ANSIBLE_LOG_FILE]"
|
||||
|
||||
show-tail-box "$title" $pid "$ANSIBLE_LOG_FILE"
|
||||
|
||||
msg-box "Successfully initialized the local environment!"
|
||||
log-info "Successfully initialized the local environment"
|
||||
|
||||
cd -
|
||||
fi
|
||||
fi
|
||||
|
||||
main-menu
|
||||
}
|
||||
|
||||
deploy-and-run-benchmarkers() {
|
||||
declare title="Deploy and Run Benchmarkers"
|
||||
|
||||
if (prompt-yes-no "$title"); then
|
||||
if [[ -z $VPC_ID ]]; then
|
||||
prompt-for-vpc-id
|
||||
fi
|
||||
|
||||
cd ansible
|
||||
|
||||
ansible-playbook -i inventories/local -e vpc_id="$VPC_ID" deploy_benchmarker.yml > "$ANSIBLE_LOG_FILE" 2>&1 &
|
||||
pid=$!
|
||||
log-info "Running ansible-playbook 'deploy_benchmarker.yml' with no tags and logging output to file [$ANSIBLE_LOG_FILE]"
|
||||
|
||||
show-tail-box "$title" $pid "$ANSIBLE_LOG_FILE"
|
||||
|
||||
msg-box "Successfully deployed and ran benchmarkers!"
|
||||
log-info "Successfully deployed and ran benchmarkers"
|
||||
|
||||
cd -
|
||||
fi
|
||||
|
||||
main-menu
|
||||
}
|
||||
|
||||
destroy-all() {
|
||||
declare title="Destroy Everything (Clean Slate)"
|
||||
|
||||
if (prompt-yes-no "$title"); then
|
||||
cd ansible
|
||||
|
||||
ansible-playbook -i inventories/local --tags 'destroy,destroy_key_pair' deploy_benchmarker.yml > "$ANSIBLE_LOG_FILE" 2>&1 &
|
||||
pid=$!
|
||||
log-info "Running ansible-playbook 'deploy_benchmarker.yml' with [destroy,destroy_key_pair] tags and logging output to file [$ANSIBLE_LOG_FILE]"
|
||||
|
||||
show-tail-box "$title" $pid "$ANSIBLE_LOG_FILE"
|
||||
|
||||
msg-box "Successfully destroyed everything!"
|
||||
log-info "Successfully destroyed everything"
|
||||
|
||||
cd -
|
||||
fi
|
||||
|
||||
main-menu
|
||||
}
|
||||
|
||||
randomly-populate-dynamodb() {
|
||||
declare title="Populate DynamoDB with Random Data"
|
||||
|
||||
if (prompt-yes-no "$title"); then
|
||||
./scripts/randomly-generate-high-velocity-data.sh /tmp/dynamodb-population.log &
|
||||
pid=$!
|
||||
log-info "Running randomly-generate-high-velocity-data script and logging to [$ANSIBLE_LOG_FILE]"
|
||||
|
||||
show-tail-box "$title" $pid "$ANSIBLE_LOG_FILE"
|
||||
|
||||
msg-box "Successfully populated DynamoDB with random data!"
|
||||
log-info "Successfully populated DynamoDB with random data"
|
||||
fi
|
||||
|
||||
main-menu
|
||||
}
|
||||
|
||||
custom-selections() {
|
||||
declare title="Customize What to Run (Advanced Mode)"
|
||||
declare choices
|
||||
declare tags=""
|
||||
|
||||
choices=$(whiptail --separate-output --checklist --fb "$title" "$BOX_HEIGHT" "$BOX_WIDTH" 13 \
|
||||
"PREREQUISITES" "Install Prerequisites for Local Machine" OFF \
|
||||
"INITIALIZE_ELK" "Initialize Local Elastic Stack" OFF \
|
||||
"START_ELK" "Start Local Elastic Stack" OFF \
|
||||
"DEPLOY_CDK" "Deploy CDK" OFF \
|
||||
"UPLOAD_BIN" "Upload Benchmarker binaries" OFF \
|
||||
"RUN_BENCHMARKERS" "Run Benchmarkers" OFF \
|
||||
"STOP_ELK" "Stop Local Elastic Stack" OFF \
|
||||
"DESTROY" "Destroy Everything except the SSH key" OFF \
|
||||
"DESTROY_KEY" "Destroy the SSK Key" OFF \
|
||||
"RUN_DYNAMODB" "Run the DynamoDB Benchmarkers" OFF \
|
||||
"RUN_DAX" "Run the DAX Benchmarkers" OFF \
|
||||
"RUN_CRUD" "Run the CRUD benchmarks for both the DynamoDB and DAX benchmarkers" OFF \
|
||||
"RUN_READ_ONLY" "Run the READ-ONLY benchmarks for both the DynamoDB and DAX benchmarkers" OFF 3>&2 2>&1 1>&3)
|
||||
|
||||
if [[ -n $choices ]]; then
|
||||
for choice in $choices; do
|
||||
case "$choice" in
|
||||
"PREREQUISITES")
|
||||
tags+="prerequisites"
|
||||
;;
|
||||
"INITIALIZE_ELK")
|
||||
tags+="init_elk"
|
||||
;;
|
||||
"START_ELK")
|
||||
tags+="elk"
|
||||
;;
|
||||
"DEPLOY_CDK")
|
||||
tags+="cdk"
|
||||
;;
|
||||
"UPLOAD_BIN")
|
||||
tags+="upload"
|
||||
;;
|
||||
"RUN_BENCHMARKERS")
|
||||
tags+="run"
|
||||
if [[ -z $VPC_ID ]]; then
|
||||
prompt-for-vpc-id
|
||||
fi
|
||||
;;
|
||||
"STOP_ELK")
|
||||
tags+="stop_elk"
|
||||
;;
|
||||
"DESTROY")
|
||||
tags+="destroy"
|
||||
;;
|
||||
"DESTROY_KEY")
|
||||
tags+="destroy_key_pair"
|
||||
;;
|
||||
"RUN_DYNAMODB")
|
||||
tags+="dynamodb"
|
||||
;;
|
||||
"RUN_DAX")
|
||||
tags+="dax"
|
||||
;;
|
||||
"RUN_CRUD")
|
||||
tags+="crud"
|
||||
;;
|
||||
"RUN_READ_ONLY")
|
||||
tags+="read-only"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
if (prompt-yes-no "$title"); then
|
||||
cd ansible
|
||||
|
||||
ansible-playbook -i inventories/local --tags "$tags" deploy_benchmarker.yml > "$ANSIBLE_LOG_FILE" 2>&1 &
|
||||
pid=$!
|
||||
log-info "Running ansible-playbook 'deploy_benchmarker.yml' with [$tags] tags and logging output to file [$ANSIBLE_LOG_FILE]"
|
||||
|
||||
show-tail-box "$title" $pid "$ANSIBLE_LOG_FILE"
|
||||
|
||||
msg-box "Successfully ran custom tasks!"
|
||||
log-info "Successfully ran custom tasks"
|
||||
|
||||
cd -
|
||||
fi
|
||||
fi
|
||||
|
||||
main-menu
|
||||
}
|
||||
|
||||
prompt-for-vpc-id() {
|
||||
readarray -t vpc_arr < <(aws ec2 describe-vpcs | jq -r '.Vpcs[] | "\(.VpcId) \((.Tags[]? | select(.Key | contains("Name")) | .Value) // "")"' | awk '{print($1, $2 == "" ? "-" : $2);}')
|
||||
declare prompt=""
|
||||
for item in "${vpc_arr[@]}"; do
|
||||
prompt+="$item OFF "
|
||||
done
|
||||
|
||||
VPC_ID=$(whiptail --fb --title "Select VPC" --radiolist "Select which VPC to use to deploy resources into" "$BOX_HEIGHT" "$BOX_WIDTH" "${vpc_arr[@]}" $prompt 3>&2 2>&1 1>&3)
|
||||
}
|
||||
|
||||
main-menu() {
|
||||
declare choice
|
||||
choice=$(whiptail --fb --title "DynamoDB + DAX Benchmarker" --menu "Select an action" "$BOX_HEIGHT" "$BOX_WIDTH" 6 \
|
||||
"I" "(I)nitialize local environment" \
|
||||
"D" "(D)eploy and Run benchmarkers" \
|
||||
"W" "(W)ipe away everything (Clean Slate)" \
|
||||
"R" "(R)andomly populate DynamoDB" \
|
||||
"C" "(C)ustom (Advanced)" \
|
||||
"X" "E(x)it" 3>&2 2>&1 1>&3)
|
||||
|
||||
case $choice in
|
||||
"I")
|
||||
initialize-environment
|
||||
;;
|
||||
"D")
|
||||
deploy-and-run-benchmarkers
|
||||
;;
|
||||
"W")
|
||||
destroy-all
|
||||
;;
|
||||
"R")
|
||||
randomly-populate-dynamodb
|
||||
;;
|
||||
"C")
|
||||
msg-box "This is for advanced users only! Be sure you know what you're doing, as running some things at the same time can cause problems (like destroy and deploy)!"
|
||||
custom-selections
|
||||
;;
|
||||
"X")
|
||||
clear
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
verify-prerequisites
|
||||
|
||||
while :; do
|
||||
main-menu
|
||||
done
|
||||
@@ -0,0 +1,8 @@
|
||||
*.js
|
||||
!jest.config.js
|
||||
*.d.ts
|
||||
node_modules
|
||||
|
||||
# CDK asset staging directory
|
||||
.cdk.staging
|
||||
cdk.out
|
||||
@@ -0,0 +1,6 @@
|
||||
*.ts
|
||||
!*.d.ts
|
||||
|
||||
# CDK asset staging directory
|
||||
.cdk.staging
|
||||
cdk.out
|
||||
@@ -0,0 +1,88 @@
|
||||
# DynamoDB + DAX Benchmarker CDK
|
||||
|
||||
This CDK project deploys a DynamoDB table with a DAX cluster on top of it, and an EC2 instance to act as a bastion host for running benchmarking tests agasint DAX.
|
||||
|
||||
By default, the name of the DynamoDB table that is created is `$USER-high-velocity-table`.
|
||||
By default, the name of the SSH key that is created for you is `$USER-dax-pair`
|
||||
|
||||
It should be noted that due to a bug in CDK, if you destroy the stack, you'll have to manually delete the SubnetGroup in DAX once everything else is deleted.
|
||||
|
||||
## Prerequisites
|
||||
You must be logged into the AWS CLI prior to running the CDK. Ensure you're logged into your target AWS account by running
|
||||
`aws sts get-caller-identity`.
|
||||
|
||||
## Getting started
|
||||
[NodeJS](https://nodejs.org/en) is required for development. Install NodeJS using the following commands, if it is
|
||||
not already installed:
|
||||
|
||||
### Installing NodeJS
|
||||
|
||||
#### Windows
|
||||
NodeJS can be installed on Windows using the [Chocolatey](https://chocolatey.org) package manager. If Chocolatey is not yet
|
||||
installed on your system, first install it in a privileged PowerShell:
|
||||
```powershell
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force;
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
```
|
||||
|
||||
Then, in a _non-privileged_ PowerShell session, install node:
|
||||
```powershell
|
||||
choco install nodejs
|
||||
```
|
||||
|
||||
#### Linux
|
||||
NodeJS can be installed on Linux using [NVM](https://github.com/nvm-sh/nvm). First, install NVM:
|
||||
```shell
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
```
|
||||
|
||||
**Note:** The installation command was _not_ run with `sudo`. This is intentional, because if you install with `sudo`, then
|
||||
`sudo` permissions will be required to install any and all new dependencies! You should avoid installing Node for the root
|
||||
user!
|
||||
|
||||
|
||||
Then, in order to use NVM to install NodeJS, you need to either restart your current shell session, or run the following:
|
||||
```shell
|
||||
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
```
|
||||
|
||||
Now, install NodeJS:
|
||||
```shell
|
||||
nvm install node
|
||||
```
|
||||
|
||||
### Installing dependent libraries
|
||||
|
||||
Once node is installed, run the following commands to install the NPM libraries:
|
||||
|
||||
```shell
|
||||
cd cdk
|
||||
npm install -g aws-cdk
|
||||
npm install -g typescript --save-dev
|
||||
npm install
|
||||
```
|
||||
|
||||
## CDK Arguments
|
||||
This application depends on a few additional parameters in order to run. They can be specified in one of two ways: environment variables, or via the `-c` argument of the `cdk` command.
|
||||
|
||||
**Important:** Only one environment variable is required by the application, regardless of which parameter specification method you choose: `AWS_REGION`.
|
||||
|
||||
The following is a table of the **required** parameters for running the CDK
|
||||
|
||||
| Parameter Name | Environment Variable Name | Description |
|
||||
|----------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `vpcId` | `VPC_ID` | The VPC ID you wish to deploy all of the stack's components into |
|
||||
| `localIp` | `LOCAL_IP` | Your local IP; Used to allow SSH and Elasticsearch access in the EC2 security group |
|
||||
| `sshKeyName` | `SSH_KEY_NAME` | The key name of your ssh key to allow you access to your EC2 instance. This should only be the name of the `.pem` file, and should not include the `.pem` extension. |
|
||||
| `awsAccount` | `AWS_ACCOUNT` | The account ID of your AWS account. |
|
||||
| `awsRegion` | `AWS_REGION` | The AWS region to deploy this stack and its components into |
|
||||
|
||||
### Optional Parameters
|
||||
It is sometimes necessary to tweak the deployment a bit for different use cases. The CDK can be tweaked with the following parameters:
|
||||
|
||||
| Parameter Name | Default Value | Description |
|
||||
|-----------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| `baseTableName` | `high-velocity-table` | This is the base name for the table. All tables created by the stack will be prefixed with `$USER` to prevent conflicts |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
import 'source-map-support/register';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import {EnvironmentProps} from '../lib/types';
|
||||
import { DaxBenchmarkingStack } from '../lib/dax-benchmarking-stack';
|
||||
|
||||
const app = new cdk.App();
|
||||
const user = process.env.USER || '';
|
||||
let vpcId = app.node.tryGetContext('vpcId');
|
||||
if (!vpcId) {
|
||||
if (!process.env.VPC_ID) {
|
||||
throw new Error('vpcId is a required parameter. Specify it with `-c vpcId=someId`, or by setting the VPC_ID environment variable');
|
||||
} else {
|
||||
vpcId = process.env.VPC_ID
|
||||
}
|
||||
}
|
||||
|
||||
let localIp = app.node.tryGetContext('localIp');
|
||||
if (!localIp) {
|
||||
if (!process.env.LOCAL_IP) {
|
||||
throw new Error('Local IP is a required parameter. Specify it with `-c localIp=XXX.XXX.XXX.XXX`, or by setting the LOCAL_IP environment variable');
|
||||
} else {
|
||||
localIp = process.env.LOCAL_IP
|
||||
}
|
||||
}
|
||||
|
||||
let sshKeyName = app.node.tryGetContext('sshKeyName');
|
||||
if (!sshKeyName) {
|
||||
if (!process.env.SSH_KEY_NAME) {
|
||||
sshKeyName = `${user}-dax-pair`;
|
||||
} else {
|
||||
sshKeyName = process.env.SSH_KEY_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
let awsAccount = app.node.tryGetContext('awsAccount');
|
||||
if (!awsAccount) {
|
||||
if (!process.env.AWS_ACCOUNT) {
|
||||
throw new Error('awsAccount is a required parameter. Specify it with `-c awsAccount=1234567890`, or by setting the AWS_ACCOUNT environment variable.');
|
||||
} else {
|
||||
awsAccount = process.env.AWS_ACCOUNT;
|
||||
}
|
||||
}
|
||||
|
||||
let awsRegion = app.node.tryGetContext('awsRegion');
|
||||
if (!awsRegion) {
|
||||
if (!process.env.AWS_REGION) {
|
||||
throw new Error('The `AWS_REGION` environment variable was not set. It must be set in order to use this application.');
|
||||
} else {
|
||||
awsRegion = process.env.AWS_REGION
|
||||
}
|
||||
}
|
||||
|
||||
let baseTableName = app.node.tryGetContext('baseTableName');
|
||||
if (!baseTableName) {
|
||||
baseTableName = 'high-velocity-table'
|
||||
}
|
||||
|
||||
const environmentProps: EnvironmentProps = {
|
||||
env: { account: awsAccount, region: awsRegion },
|
||||
baseTableName,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
user,
|
||||
vpcId,
|
||||
localIp,
|
||||
sshKeyName
|
||||
};
|
||||
|
||||
new DaxBenchmarkingStack(app, `${user}-dax-benchmark-stack`, environmentProps);
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/test'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Tags } from "aws-cdk-lib";
|
||||
import { Construct } from "constructs";
|
||||
import { EnvironmentProps } from "./types";
|
||||
import { Instance, InstanceClass, InstanceSize, InstanceType, MachineImage, Peer, Port, SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";
|
||||
import { IRole, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
||||
|
||||
export class DaxBastionHost extends Construct {
|
||||
public readonly instanceRole: IRole;
|
||||
public readonly instance: Instance;
|
||||
|
||||
constructor(scope: Construct, id: string, environmentProps: EnvironmentProps, daxSecurityGroup: SecurityGroup) {
|
||||
super(scope, id);
|
||||
|
||||
Tags.of(this).add('Application', 'dynamodb-dax-benchmarker');
|
||||
|
||||
const { removalPolicy, user, vpcId, localIp, sshKeyName } = environmentProps;
|
||||
const localIpCidr = `${localIp}/32`;
|
||||
|
||||
const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId });
|
||||
|
||||
const bastionHostSecurityGroup = new SecurityGroup(this, `${user}-dax-sg`, {
|
||||
vpc,
|
||||
description: `Allow SSH, Elasticsearch, and DAX access for ${user}`,
|
||||
securityGroupName: `${user}-dax-bastion-host-sg`
|
||||
});
|
||||
bastionHostSecurityGroup.applyRemovalPolicy(removalPolicy);
|
||||
bastionHostSecurityGroup.addIngressRule(Peer.ipv4(localIpCidr), Port.tcp(22), "Allow SSH access to this instance from the users public IP");
|
||||
bastionHostSecurityGroup.addIngressRule(Peer.ipv4(localIpCidr), Port.tcp(9200), "Allow the host to communicate with the users locally running Elasticsearch cluster");
|
||||
bastionHostSecurityGroup.addIngressRule(daxSecurityGroup, Port.allTraffic());
|
||||
daxSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.allTraffic());
|
||||
|
||||
this.instanceRole = new Role(this, `${user}-bastion-role`, {
|
||||
roleName: `${user}-bastion-role`,
|
||||
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
|
||||
});
|
||||
this.instanceRole.applyRemovalPolicy(removalPolicy);
|
||||
|
||||
this.instance = new Instance(this, `${user}-dax-bastion-host`, {
|
||||
vpc,
|
||||
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.SMALL),
|
||||
machineImage: MachineImage.latestAmazonLinux2023(),
|
||||
instanceName: `${user}-dax-bastion-host`,
|
||||
keyName: sshKeyName,
|
||||
vpcSubnets: vpc.selectSubnets({ subnetType: SubnetType.PUBLIC }),
|
||||
securityGroup: bastionHostSecurityGroup,
|
||||
role: this.instanceRole
|
||||
});
|
||||
this.instance.applyRemovalPolicy(removalPolicy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Construct } from "constructs";
|
||||
import { EnvironmentProps } from "./types";
|
||||
import { CfnOutput, Stack, Tags } from "aws-cdk-lib";
|
||||
import { CfnCluster, CfnSubnetGroup } from "aws-cdk-lib/aws-dax";
|
||||
import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
||||
import { SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";
|
||||
import { DynamoDbBenchmarkTable } from "./dynamodb";
|
||||
import { DaxBastionHost } from "./bastion-host";
|
||||
|
||||
export class DaxBenchmarkingStack extends Stack {
|
||||
constructor(scope: Construct, id: string, environmentProps: EnvironmentProps) {
|
||||
super(scope, id, environmentProps);
|
||||
|
||||
Tags.of(this).add('Application', 'dynamodb-dax-benchmarker');
|
||||
|
||||
const { user, removalPolicy, vpcId } = environmentProps;
|
||||
const { table } = new DynamoDbBenchmarkTable(this, `${user}-dynamodb-benchmark-table`, environmentProps);
|
||||
|
||||
const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId });
|
||||
|
||||
const daxSecurityGroup = new SecurityGroup(this, `${user}-dax-sg`, {
|
||||
vpc,
|
||||
securityGroupName: `${user}-dax-sg`
|
||||
});
|
||||
daxSecurityGroup.applyRemovalPolicy(removalPolicy);
|
||||
|
||||
const { instanceRole, instance } = new DaxBastionHost(this, `${user}-dax-bastion-host`, environmentProps, daxSecurityGroup);
|
||||
|
||||
const daxClusterName = `${user}-high-velocity`;
|
||||
const daxFullAccessPolicy = new PolicyStatement({
|
||||
effect: Effect.ALLOW,
|
||||
actions: [
|
||||
"dynamodb:BatchGetItem",
|
||||
"dynamodb:GetItem",
|
||||
"dynamodb:Query",
|
||||
"dynamodb:Scan",
|
||||
"dynamodb:BatchWriteItem",
|
||||
"dynamodb:DeleteItem",
|
||||
"dynamodb:PutItem",
|
||||
"dynamodb:UpdateItem",
|
||||
"dynamodb:DescribeLimits",
|
||||
"dynamodb:DescribeTimeToLive",
|
||||
"dynamodb:DescribeTable",
|
||||
"dynamodb:ListTables"
|
||||
],
|
||||
resources: [table.tableArn]
|
||||
});
|
||||
|
||||
const daxServiceRole = new Role(this, `${daxClusterName}-role`, {
|
||||
assumedBy: new ServicePrincipal("dax.amazonaws.com"),
|
||||
inlinePolicies: {
|
||||
DAXFullAccess: new PolicyDocument({
|
||||
statements: [daxFullAccessPolicy]
|
||||
})
|
||||
}
|
||||
});
|
||||
daxServiceRole.applyRemovalPolicy(removalPolicy);
|
||||
|
||||
instanceRole.addToPrincipalPolicy(daxFullAccessPolicy);
|
||||
|
||||
const subnetGroup = new CfnSubnetGroup(this, `${user}-dax-subnet-group`, {
|
||||
subnetIds: vpc.selectSubnets({
|
||||
subnetType: SubnetType.PRIVATE_ISOLATED
|
||||
}).subnetIds,
|
||||
subnetGroupName: `${user}-dax-subnet-group`,
|
||||
});
|
||||
subnetGroup.applyRemovalPolicy(removalPolicy);
|
||||
|
||||
const daxCluster = new CfnCluster(this, daxClusterName, {
|
||||
iamRoleArn: daxServiceRole.roleArn,
|
||||
nodeType: 'dax.r5.large',
|
||||
replicationFactor: 3,
|
||||
securityGroupIds: [daxSecurityGroup.securityGroupId],
|
||||
subnetGroupName: subnetGroup.subnetGroupName,
|
||||
availabilityZones: vpc.availabilityZones,
|
||||
clusterEndpointEncryptionType: 'TLS',
|
||||
clusterName: daxClusterName,
|
||||
sseSpecification: {
|
||||
sseEnabled: true,
|
||||
}
|
||||
});
|
||||
daxCluster.applyRemovalPolicy(removalPolicy);
|
||||
daxCluster.addDependency(subnetGroup);
|
||||
|
||||
new CfnOutput(this, 'DaxEndpoint', { value: daxCluster.attrClusterDiscoveryEndpointUrl });
|
||||
new CfnOutput(this, 'InstanceId', { value: instance.instanceId });
|
||||
new CfnOutput(this, 'InstancePublicIp', { value: instance.instancePublicIp });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {Tags} from "aws-cdk-lib";
|
||||
import {Construct} from "constructs";
|
||||
import {EnvironmentProps} from "./types";
|
||||
import {AttributeType, BillingMode, Table} from "aws-cdk-lib/aws-dynamodb";
|
||||
|
||||
export class DynamoDbBenchmarkTable extends Construct {
|
||||
public readonly table: Table;
|
||||
|
||||
constructor(scope: Construct, id: string, environmentProps: EnvironmentProps) {
|
||||
super(scope, id);
|
||||
|
||||
Tags.of(this).add('Application', 'dynamodb-dax-benchmarker');
|
||||
|
||||
const { baseTableName, removalPolicy, user } = environmentProps;
|
||||
const tableName = `${user}-${baseTableName}`;
|
||||
|
||||
this.table = new Table(this, tableName, {
|
||||
partitionKey: {
|
||||
name: 'id',
|
||||
type: AttributeType.STRING
|
||||
},
|
||||
tableName,
|
||||
removalPolicy,
|
||||
billingMode: BillingMode.PAY_PER_REQUEST
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {RemovalPolicy, StackProps} from "aws-cdk-lib";
|
||||
|
||||
export interface EnvironmentProps extends StackProps {
|
||||
readonly baseTableName: string
|
||||
readonly removalPolicy: RemovalPolicy
|
||||
readonly user: string
|
||||
readonly vpcId: string
|
||||
readonly localIp: string
|
||||
readonly sshKeyName: string
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
module github.com/Dark-Alex-17/dynamodb-benchmarker
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/aws/aws-cdk-go/awscdk/v2 v2.88.0
|
||||
github.com/aws/aws-dax-go v1.2.12
|
||||
github.com/aws/aws-sdk-go v1.44.301
|
||||
github.com/aws/constructs-go/constructs/v10 v10.2.69
|
||||
github.com/aws/jsii-runtime-go v1.85.0
|
||||
github.com/elastic/go-elasticsearch/v8 v8.8.2
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
gopkg.in/loremipsum.v1 v1.1.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.200 // indirect
|
||||
github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2 // indirect
|
||||
github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv5/v2 v2.0.165 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/yuin/goldmark v1.4.13 // indirect
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/tools v0.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antlr/antlr4 v0.0.0-20181218183524-be58ebffde8e // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.3.0 // indirect
|
||||
github.com/gofrs/uuid v3.3.0+incompatible // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/spf13/cobra v1.7.0
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
)
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dark-Alex-17/dynamodb-benchmarker/pkg/models"
|
||||
"github.com/Dark-Alex-17/dynamodb-benchmarker/pkg/simulators"
|
||||
"github.com/Dark-Alex-17/dynamodb-benchmarker/pkg/utils"
|
||||
"github.com/aws/aws-dax-go/dax"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var concurrentSimulations, buffer, attributes, duration int
|
||||
var username, password, index, endpoint, tableName string
|
||||
var readOnly bool
|
||||
|
||||
func main() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "dax-benchmarker",
|
||||
Short: "A CLI tool for simulating heavy usage against DAX and publishing metrics to an Elastic Stack for analysis",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateFlags(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execute()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
rootCmd.PersistentFlags().IntVarP(&concurrentSimulations, "concurrent-simulations", "c", 1000, "The number of concurrent simulations to run")
|
||||
rootCmd.PersistentFlags().IntVarP(&buffer, "buffer", "b", 500, "The buffer size of the Elasticsearch goroutine's channel")
|
||||
rootCmd.PersistentFlags().IntVarP(&attributes, "attributes", "a", 5, "The number of attributes to use when populating and querying the DynamoDB table; minimum value of 1")
|
||||
rootCmd.PersistentFlags().IntVarP(&duration, "duration", "d", 1800, "The length of time (in seconds) to run the benchmark for")
|
||||
rootCmd.PersistentFlags().StringVarP(&username, "username", "u", "elastic", "Local Elasticsearch cluster username")
|
||||
rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "changeme", "Local Elasticsearch cluster password")
|
||||
rootCmd.PersistentFlags().StringVarP(&index, "index", "i", "dax", "The Elasticsearch Index to insert data into")
|
||||
rootCmd.PersistentFlags().StringVarP(&tableName, "table", "t", fmt.Sprintf("%s-high-velocity-table", os.Getenv("USER")), "The DynamoDB table to perform operations against")
|
||||
rootCmd.PersistentFlags().StringVarP(&endpoint, "endpoint", "e", "", "The DAX endpoint to hit when running simulations (assumes secure endpoint, so do not specify port)")
|
||||
rootCmd.PersistentFlags().BoolVarP(&readOnly, "read-only", "r", false, "Whether to run a read-only scenario for benchmarking")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Errorf("Something went wrong parsing CLI args and executing the client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validateFlags() error {
|
||||
if len(endpoint) == 0 {
|
||||
daxEndpointEnvironmentVariable := os.Getenv("DAX_ENDPOINT")
|
||||
if len(daxEndpointEnvironmentVariable) == 0 {
|
||||
return errors.New("a DAX endpoint must be specified either via -e, --endpoint or via the DAX_ENDPOINT environment variable")
|
||||
} else {
|
||||
endpoint = daxEndpointEnvironmentVariable
|
||||
}
|
||||
}
|
||||
|
||||
if attributes < 1 {
|
||||
return errors.New("the number of attributes cannot be lower than 1")
|
||||
}
|
||||
|
||||
if len(os.Getenv("AWS_REGION")) == 0 {
|
||||
return errors.New("an AWS region must be specified using the AWS_REGION environment variable")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func execute() {
|
||||
esChan := make(chan models.DynamoDbSimulationMetrics, buffer)
|
||||
defer close(esChan)
|
||||
daxEndpoint := fmt.Sprintf("%s:9111", endpoint)
|
||||
region := os.Getenv("AWS_REGION")
|
||||
sess := session.Must(session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewChainCredentials([]credentials.Provider{&credentials.EnvProvider{}}),
|
||||
Endpoint: &daxEndpoint,
|
||||
Region: ®ion,
|
||||
}))
|
||||
|
||||
if _, err := sess.Config.Credentials.Get(); err != nil {
|
||||
log.Errorf("credentials were not loaded! %v+", err)
|
||||
}
|
||||
|
||||
client, err := dax.NewWithSession(*sess)
|
||||
if err != nil {
|
||||
log.Errorf("unable to initialize dax client %v", err)
|
||||
}
|
||||
|
||||
partitionKeys, err := scanAllPartitionKeys(client)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to fetch partition keys! Simulation failed! %v+", err)
|
||||
}
|
||||
|
||||
go startElasticsearchPublisher(esChan)
|
||||
|
||||
for i := 0; i < concurrentSimulations; i++ {
|
||||
go simulationLoop(esChan, client, partitionKeys)
|
||||
}
|
||||
|
||||
duration, err := time.ParseDuration(strconv.Itoa(duration) + "s")
|
||||
if err != nil {
|
||||
log.Errorf("Unable to create duration from the provided time: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
<-time.After(duration)
|
||||
}
|
||||
|
||||
func startElasticsearchPublisher(c <-chan models.DynamoDbSimulationMetrics) {
|
||||
config := elasticsearch.Config{
|
||||
Addresses: []string{
|
||||
"http://localhost:9200",
|
||||
},
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
esClient, err := elasticsearch.NewClient(config)
|
||||
if err != nil {
|
||||
log.Errorf("unable to initialize elasticsearch client %v", err)
|
||||
}
|
||||
|
||||
mapping := `{
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
log.Infof("Setting the explicit mappings for the %s index", index)
|
||||
if _, err := esClient.Indices.Create(index); err != nil {
|
||||
log.Warnf("Unable to create the %s index. Encountered the following error: %v", index, err)
|
||||
}
|
||||
|
||||
if _, err := esClient.Indices.PutMapping([]string{index}, strings.NewReader(mapping)); err != nil {
|
||||
log.Errorf("unable to create mapping for the %s index! %v+", index, err)
|
||||
}
|
||||
|
||||
for metric := range c {
|
||||
log.Info("Publishing metrics to Elasticsearch...")
|
||||
|
||||
data, _ := json.Marshal(metric)
|
||||
_, err := esClient.Index(index, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
log.Error("Was unable to publish metrics to Elasticsearch! Received a non 2XX response")
|
||||
} else {
|
||||
log.Info("Successfully published metrics to Elasticsearch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func simulationLoop(c chan<- models.DynamoDbSimulationMetrics, client *dax.Dax, partitionKeys []string) {
|
||||
for {
|
||||
metrics := new(models.DynamoDbSimulationMetrics)
|
||||
metrics.Successful = true
|
||||
metrics.Timestamp = time.Now().UnixNano() / 1e6
|
||||
startTime := time.Now()
|
||||
|
||||
if readOnly {
|
||||
log.Info("Running a read-only simulation...")
|
||||
metrics.Scenario = models.ScenarioReadOnly.String()
|
||||
runReadOnlySimulation(client, metrics, partitionKeys)
|
||||
} else {
|
||||
log.Info("Running a CRUD simulation...")
|
||||
metrics.Scenario = models.ScenarioCrud.String()
|
||||
runCrudSimulation(client, metrics, partitionKeys)
|
||||
}
|
||||
|
||||
log.Info("Simulation completed successfully!")
|
||||
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.SimulationTime = &millisecondDuration
|
||||
|
||||
log.Infof("Metrics: %v+", metrics)
|
||||
|
||||
c <- *metrics
|
||||
}
|
||||
}
|
||||
|
||||
func runReadOnlySimulation(client *dax.Dax, metrics *models.DynamoDbSimulationMetrics, partitionKeys []string) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
time.Sleep(time.Duration(r.Intn(16)))
|
||||
|
||||
metrics.Operation = models.DynamoRead.String()
|
||||
simulators.SimulateReadOperation(client, tableName, partitionKeys, metrics)
|
||||
}
|
||||
|
||||
func runCrudSimulation(client *dax.Dax, metrics *models.DynamoDbSimulationMetrics, partitionKeys []string) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
operation := r.Intn(3)
|
||||
log.Infof("Operation number: %d", operation)
|
||||
|
||||
switch operation {
|
||||
case int(models.DynamoRead):
|
||||
metrics.Operation = models.DynamoRead.String()
|
||||
simulators.SimulateReadOperation(client, tableName, partitionKeys, metrics)
|
||||
case int(models.DynamoWrite):
|
||||
metrics.Operation = models.DynamoWrite.String()
|
||||
simulators.SimulateWriteOperation(client, tableName, attributes, metrics)
|
||||
case int(models.DynamoUpdate):
|
||||
metrics.Operation = models.DynamoUpdate.String()
|
||||
simulators.SimulateUpdateOperation(client, tableName, attributes, metrics)
|
||||
}
|
||||
}
|
||||
|
||||
func scanAllPartitionKeys(client *dax.Dax) ([]string, error) {
|
||||
log.Info("Fetching a large list of partition keys to randomly read...")
|
||||
projectionExpression := "id"
|
||||
var limit int64 = 10000
|
||||
|
||||
response, err := client.Scan(&dynamodb.ScanInput{
|
||||
TableName: &tableName,
|
||||
Limit: &limit,
|
||||
ProjectionExpression: &projectionExpression,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to fetch partition keys! %v", err)
|
||||
return []string{}, err
|
||||
} else {
|
||||
log.Info("Fetched partition keys!")
|
||||
keys := make([]string, 100)
|
||||
|
||||
for _, itemsMap := range response.Items {
|
||||
keys = append(keys, *utils.MapValues(itemsMap)[0].S)
|
||||
}
|
||||
|
||||
log.Infof("Found a total of %d keys", len(keys))
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/loremipsum.v1"
|
||||
)
|
||||
|
||||
type DynamoOperation int
|
||||
|
||||
const (
|
||||
DynamoRead DynamoOperation = iota
|
||||
DynamoWrite
|
||||
DynamoUpdate
|
||||
)
|
||||
|
||||
func (d DynamoOperation) String() string {
|
||||
switch d {
|
||||
case DynamoRead:
|
||||
return "read"
|
||||
case DynamoWrite:
|
||||
return "write"
|
||||
case DynamoUpdate:
|
||||
return "update"
|
||||
default:
|
||||
return "read"
|
||||
}
|
||||
}
|
||||
|
||||
type Scenario int
|
||||
|
||||
const (
|
||||
ScenarioCrud Scenario = iota
|
||||
ScenarioReadOnly
|
||||
)
|
||||
|
||||
func (s Scenario) String() string {
|
||||
switch s {
|
||||
case ScenarioCrud:
|
||||
return "crud"
|
||||
case ScenarioReadOnly:
|
||||
return "readOnly"
|
||||
default:
|
||||
return "crud"
|
||||
}
|
||||
}
|
||||
|
||||
type BenchmarkingItem map[string]*dynamodb.AttributeValue
|
||||
|
||||
func NewBenchmarkingItem(attributes int) BenchmarkingItem {
|
||||
benchmarkingItem := make(map[string]*dynamodb.AttributeValue)
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
loremIpsumGenerator := loremipsum.NewWithSeed(time.Now().UnixNano())
|
||||
id := uuid.New().String()
|
||||
benchmarkingItem["id"] = &dynamodb.AttributeValue{S: &id}
|
||||
|
||||
for i := 0; i < attributes; i++ {
|
||||
switch i % 2 {
|
||||
case 1:
|
||||
float := fmt.Sprintf("%.2f", r.Float64()*32.00)
|
||||
benchmarkingItem[strconv.Itoa(i)] = &dynamodb.AttributeValue{N: &float}
|
||||
default:
|
||||
sentence := loremIpsumGenerator.Sentence()
|
||||
benchmarkingItem[strconv.Itoa(i)] = &dynamodb.AttributeValue{S: &sentence}
|
||||
}
|
||||
}
|
||||
|
||||
return benchmarkingItem
|
||||
}
|
||||
|
||||
type DynamoDbSimulationMetrics struct {
|
||||
Operation string `json:"operation"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Successful bool `json:"successful"`
|
||||
Scenario string `json:"scenario"`
|
||||
SimulationTime *float64 `json:"simulationTime,omitempty"`
|
||||
ReadTime *float64 `json:"readTime,omitempty"`
|
||||
WriteTime *float64 `json:"writeTime,omitempty"`
|
||||
WriteItemConfirmationTime *float64 `json:"writeItemConfirmationTime,omitempty"`
|
||||
UpdateTime *float64 `json:"updateItem,omitempty"`
|
||||
UpdateItemConfirmationTime *float64 `json:"updateItemConfirmationTime,omitempty"`
|
||||
DeleteTime *float64 `json:"deleteTime,omitempty"`
|
||||
DeleteItemConfirmationTime *float64 `json:"deleteItemConfirmationTime,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package simulators
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Dark-Alex-17/dynamodb-benchmarker/pkg/models"
|
||||
"github.com/aws/aws-dax-go/dax"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func ReadItem(client *dax.Dax, tableName string, id dynamodb.AttributeValue, metrics *models.DynamoDbSimulationMetrics, recordMetrics bool) (dynamodb.GetItemOutput, error) {
|
||||
partitionKey := *id.S
|
||||
startTime := time.Now()
|
||||
response, err := client.GetItem(&dynamodb.GetItemInput{
|
||||
TableName: &tableName,
|
||||
Key: map[string]*dynamodb.AttributeValue{
|
||||
"id": {S: id.S},
|
||||
},
|
||||
})
|
||||
|
||||
if recordMetrics {
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.ReadTime = &millisecondDuration
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not fetch item with partition key: %v. %v+", partitionKey, err)
|
||||
metrics.Successful = false
|
||||
return dynamodb.GetItemOutput{}, err
|
||||
}
|
||||
|
||||
if len(response.Item) == 0 {
|
||||
log.Infof("No items found with partition key: %v", partitionKey)
|
||||
return dynamodb.GetItemOutput{}, nil
|
||||
}
|
||||
|
||||
return *response, nil
|
||||
}
|
||||
|
||||
func UpdateItem(client *dax.Dax, tableName string, id dynamodb.AttributeValue, attributes int, metrics *models.DynamoDbSimulationMetrics) {
|
||||
updatedItem := models.NewBenchmarkingItem(attributes)
|
||||
updatedItem["id"] = &id
|
||||
partitionKey := *id.S
|
||||
startTime := time.Now()
|
||||
|
||||
_, err := client.PutItem(&dynamodb.PutItemInput{
|
||||
TableName: &tableName,
|
||||
Item: updatedItem,
|
||||
})
|
||||
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.UpdateTime = &millisecondDuration
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not update item with partition key: %v. %v+", partitionKey, err)
|
||||
metrics.Successful = false
|
||||
} else {
|
||||
log.Infof("Successfully updated item with partition key: %v", partitionKey)
|
||||
}
|
||||
}
|
||||
|
||||
func PutItem(client *dax.Dax, tableName string, attributes int, metrics *models.DynamoDbSimulationMetrics) (models.BenchmarkingItem, error) {
|
||||
newItem := models.NewBenchmarkingItem(attributes)
|
||||
partitionKey := *newItem["id"].S
|
||||
startTime := time.Now()
|
||||
|
||||
_, err := client.PutItem(&dynamodb.PutItemInput{
|
||||
TableName: &tableName,
|
||||
Item: newItem,
|
||||
})
|
||||
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.WriteTime = &millisecondDuration
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not put new item with partition key: %v. %v+", partitionKey, err)
|
||||
metrics.Successful = false
|
||||
return models.BenchmarkingItem{}, err
|
||||
}
|
||||
|
||||
log.Infof("Successfully put new item with partition key: %v", partitionKey)
|
||||
return newItem, nil
|
||||
}
|
||||
|
||||
func DeleteItem(client *dax.Dax, tableName string, id dynamodb.AttributeValue, metrics *models.DynamoDbSimulationMetrics) {
|
||||
partitionKey := *id.S
|
||||
startTime := time.Now()
|
||||
|
||||
_, err := client.DeleteItem(&dynamodb.DeleteItemInput{
|
||||
TableName: &tableName,
|
||||
Key: map[string]*dynamodb.AttributeValue{
|
||||
"charger_id": &id,
|
||||
},
|
||||
})
|
||||
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.DeleteTime = &millisecondDuration
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Could not delete item with partition key: %v. %v+", partitionKey, err)
|
||||
metrics.Successful = false
|
||||
} else {
|
||||
log.Infof("Successfully deleted item with partition key: %v", partitionKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package simulators
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dark-Alex-17/dynamodb-benchmarker/pkg/models"
|
||||
"github.com/aws/aws-dax-go/dax"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func SimulateReadOperation(client *dax.Dax, tableName string, partitionKeys []string, metrics *models.DynamoDbSimulationMetrics) {
|
||||
log.Info("Performing READ operation...")
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
var partitionKey string
|
||||
for {
|
||||
partitionKey = partitionKeys[r.Intn(len(partitionKeys))]
|
||||
if len(strings.TrimSpace(partitionKey)) == 0 {
|
||||
log.Info("Parition key was empty. Trying again to choose a non-empty partition key")
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
id := dynamodb.AttributeValue{S: &partitionKey}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Infof("Attempt %d: Fetching existing item with partition key: %v", i, partitionKey)
|
||||
|
||||
response, _ := ReadItem(client, tableName, id, metrics, true)
|
||||
if response.Item["id"] != nil {
|
||||
log.Infof("Successfully read existing item with partition key: %v", partitionKey)
|
||||
break
|
||||
}
|
||||
|
||||
log.Errorf("Unable to find existing item with partition key: %v", partitionKey)
|
||||
if i == 9 {
|
||||
log.Errorf("All attempts to fetch the existing item with partition key: %v failed!", partitionKey)
|
||||
metrics.Successful = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SimulateWriteOperation(client *dax.Dax, tableName string, attributes int, metrics *models.DynamoDbSimulationMetrics) {
|
||||
log.Info("Performing WRITE operation...")
|
||||
benchmarkingItem, err := PutItem(client, tableName, attributes, metrics)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to complete PUT simulation. %v+", err)
|
||||
metrics.Successful = false
|
||||
return
|
||||
}
|
||||
|
||||
id := *benchmarkingItem["id"]
|
||||
|
||||
AssertItemWasCreated(client, tableName, id, metrics)
|
||||
|
||||
DeleteItem(client, tableName, id, metrics)
|
||||
|
||||
AssertItemWasDeleted(client, tableName, id, metrics)
|
||||
}
|
||||
|
||||
func SimulateUpdateOperation(client *dax.Dax, tableName string, attributes int, metrics *models.DynamoDbSimulationMetrics) {
|
||||
log.Info("Performing UPDATE operation...")
|
||||
newItem, err := PutItem(client, tableName, attributes, metrics)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to complete UPDATE simulation. %v+", err)
|
||||
metrics.Successful = false
|
||||
return
|
||||
}
|
||||
|
||||
id := *newItem["id"]
|
||||
partitionKey := *id.S
|
||||
attemptsExhausted := false
|
||||
|
||||
AssertItemWasCreated(client, tableName, id, metrics)
|
||||
UpdateItem(client, tableName, id, attributes, metrics)
|
||||
|
||||
startTime := time.Now()
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Infof("Attempt %d: Fetching updated item for partition key: %v...", i, partitionKey)
|
||||
|
||||
updatedItem, err := ReadItem(client, tableName, id, metrics, false)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to complete UPDATE simulation. %v+", err)
|
||||
metrics.Successful = false
|
||||
return
|
||||
}
|
||||
|
||||
if *newItem["1"].N != *updatedItem.Item["1"].N {
|
||||
log.Infof("Confirmed update for partition key: %v", partitionKey)
|
||||
break
|
||||
} else {
|
||||
log.Errorf("Update for partition key %v failed! Values are still equal!", partitionKey)
|
||||
if i == 9 {
|
||||
log.Error("Exhausted attempts to fetch updated item!")
|
||||
metrics.Successful = false
|
||||
attemptsExhausted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !attemptsExhausted {
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.UpdateItemConfirmationTime = &millisecondDuration
|
||||
}
|
||||
|
||||
DeleteItem(client, tableName, id, metrics)
|
||||
AssertItemWasDeleted(client, tableName, id, metrics)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package simulators
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Dark-Alex-17/dynamodb-benchmarker/pkg/models"
|
||||
"github.com/aws/aws-dax-go/dax"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func AssertItemWasCreated(client *dax.Dax, tableName string, id dynamodb.AttributeValue, metrics *models.DynamoDbSimulationMetrics) {
|
||||
partitionKey := *id.S
|
||||
attemptsExhausted := false
|
||||
startTime := time.Now()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Infof("Attempt %d: Fetching newly added item with partition key: %v", i, partitionKey)
|
||||
|
||||
newItem, err := ReadItem(client, tableName, id, metrics, false)
|
||||
|
||||
if err != nil || newItem.Item["id"].S == nil {
|
||||
log.Errorf("Unable to find new item with partition key: %v", partitionKey)
|
||||
if i == 9 {
|
||||
log.Errorf("All attempts to fetch the newly added item with partition key: %v failed!", partitionKey)
|
||||
attemptsExhausted = true
|
||||
metrics.Successful = false
|
||||
}
|
||||
} else {
|
||||
log.Infof("Successfully read new item with partition key: %v", partitionKey)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !attemptsExhausted {
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.WriteItemConfirmationTime = &millisecondDuration
|
||||
}
|
||||
}
|
||||
|
||||
func AssertItemWasDeleted(client *dax.Dax, tableName string, id dynamodb.AttributeValue, metrics *models.DynamoDbSimulationMetrics) {
|
||||
partitionKey := *id.S
|
||||
attemptsExhausted := false
|
||||
startTime := time.Now()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Infof("Attempt %d: Fetching deleted item with partition key: %v ...", i, partitionKey)
|
||||
|
||||
deletedItem, _ := ReadItem(client, tableName, id, metrics, false)
|
||||
if deletedItem.Item["id"].S == nil {
|
||||
log.Infof("Item with partition key: %v was successfully deleted.", partitionKey)
|
||||
break
|
||||
} else {
|
||||
log.Errorf("Item with partition key %v was not deleted as expected!", partitionKey)
|
||||
if i == 9 {
|
||||
log.Errorf("All attempts to receive an empty response to verify item with partition key: %v was deleted failed!", partitionKey)
|
||||
attemptsExhausted = true
|
||||
metrics.Successful = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !attemptsExhausted {
|
||||
duration := time.Since(startTime).Microseconds()
|
||||
millisecondDuration := float64(duration) / 1000
|
||||
metrics.DeleteItemConfirmationTime = &millisecondDuration
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package utils
|
||||
|
||||
func MapValues[K comparable, V any](inputMap map[K]V) []V {
|
||||
valuesSlice := make([]V, 0)
|
||||
|
||||
for _, value := range inputMap {
|
||||
valuesSlice = append(valuesSlice, value)
|
||||
}
|
||||
|
||||
return valuesSlice
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
tab_spaces=2
|
||||
edition = "2021"
|
||||
reorder_imports = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
reorder_modules = true
|
||||
merge_derives = true
|
||||
use_field_init_shorthand = true
|
||||
format_macro_matchers = true
|
||||
format_macro_bodies = true
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
BENCHMARK_LOG_FILE="$1"
|
||||
if [[ -z $BENCHMARK_LOG_FILE ]]; then
|
||||
BENCHMARK_LOG_FILE=/tmp/benchmarker.log
|
||||
fi
|
||||
|
||||
red=$(tput setaf 1)
|
||||
green=$(tput setaf 2)
|
||||
gold=$(tput setaf 3)
|
||||
blue=$(tput setaf 4)
|
||||
magenta=$(tput setaf 5)
|
||||
cyan=$(tput setaf 6)
|
||||
default=$(tput sgr0)
|
||||
bold=$(tput bold)
|
||||
|
||||
log-error() {
|
||||
if [[ -z $2 ]]; then
|
||||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}"
|
||||
else
|
||||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}"
|
||||
echo -e "${red}${bold}ERROR:${default}${red} $1${default}" >> "$BENCHMARK_LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
log-warn() {
|
||||
if [[ -z $2 ]]; then
|
||||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}"
|
||||
else
|
||||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}"
|
||||
echo -e "${gold}${bold}WARN:${default}${gold} $1${default}" >> "$BENCHMARK_LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
log-info() {
|
||||
if [[ -z $2 ]]; then
|
||||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}"
|
||||
else
|
||||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}"
|
||||
echo -e "${cyan}${bold}INFO:${default}${cyan} $1${default}" >> "$BENCHMARK_LOG_FILE"
|
||||
fi
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
if [[ $(basename "$(pwd)") == scripts ]]; then
|
||||
source logger.sh
|
||||
else
|
||||
source scripts/logger.sh
|
||||
fi
|
||||
|
||||
trap 'echo Stopping...; exit' SIGINT
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
${blue}${bold}randomly-generate-high-velocity-data${default}: A script to randomly generate high-velocity data for some DynamoDB table with random attributes and values for benchmarking purposes.
|
||||
|
||||
${gold}${bold}USAGE:${default}
|
||||
randomly-generate-high-velocity-data [OPTIONS] [ARGS]...
|
||||
|
||||
${green}-h, --help${default} Show this usage screen
|
||||
|
||||
${gold}${bold}ARGS:${default}
|
||||
${green}-a, --attributes ${magenta}<ATTRIBUTES>${default} The number of attributes to populate each item in the table with
|
||||
This defaults to 5
|
||||
|
||||
${green}-i, --items ${magenta}<ITEMS>${default} The number of items to populate the table with
|
||||
${bold}Note:${default} Items are populated 25 at a time, so whatever number you provide will be rounded to the nearest multiple of 25
|
||||
|
||||
${green}-t, --table ${magenta}<TABLE_NAME>${default} The name of the DynamoDB table to populate
|
||||
This defaults to $USER-high-velocity-table
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure-required-variables-are-set() {
|
||||
declare required_variables=(AWS_PROFILE AWS_REGION ITEMS)
|
||||
|
||||
for variable in "${required_variables[@]}"; do
|
||||
if [[ -z "${!variable}" ]]; then
|
||||
log-error "A required variable environment is not initialized: $variable"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
parse-arguments() {
|
||||
declare parsed_args
|
||||
parsed_args=$(getopt -a -n randomly-generate-high-velocity-data -o :a:hi:t: --long attributes:,help,items:,table: -- "$@")
|
||||
declare valid_arguments=$?
|
||||
|
||||
if [[ $valid_arguments != 0 ]]; then
|
||||
log-error "Invalid arguments passed. See usage below."
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eval set -- "$parsed_args"
|
||||
while :; do
|
||||
case "$1" in
|
||||
"-a" | "--attributes")
|
||||
ATTRIBUTES="$2"
|
||||
shift 2
|
||||
;;
|
||||
"-h" | "--help")
|
||||
usage
|
||||
exit
|
||||
;;
|
||||
"-i" | "--items")
|
||||
ITEMS="$2"
|
||||
shift 2
|
||||
;;
|
||||
"-t" | "--table")
|
||||
TABLE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
log-error "An invalid option was passed, but somehow getopt didn't catch it: $1. Displaying usage and exiting..."
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z $TABLE_NAME ]]; then
|
||||
TABLE_NAME="$USER-high-velocity-table"
|
||||
fi
|
||||
|
||||
if [[ -z $ATTRIBUTES ]]; then
|
||||
ATTRIBUTES=5
|
||||
fi
|
||||
|
||||
ensure-required-variables-are-set
|
||||
|
||||
if [[ $ATTRIBUTES -lt 1 ]]; then
|
||||
log-error "ATTRIBUTES must be a value of at least 1 so that attributes can be added to the table."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! (aws sts get-caller-identity > /dev/null 2>&1); then
|
||||
log-error "You must be logged into the AWS CLI in order to use this script. Please log into the AWS CLI first and then try again."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
show-properties() {
|
||||
log-info "Using the following settings to randomly populate the DynamoDB benchmarking table:"
|
||||
cat <<-EOF
|
||||
${cyan}
|
||||
ATTRIBUTES=$ATTRIBUTES
|
||||
TABLE_NAME=$TABLE_NAME
|
||||
${default}
|
||||
EOF
|
||||
}
|
||||
|
||||
generate-attribute-value() {
|
||||
declare current_val=$1
|
||||
case "$((current_val % 2))" in
|
||||
"1")
|
||||
echo '"'"$current_val"'": {"N": "'"$(seq 0 .01 32 | shuf | head -1)"'"}'
|
||||
;;
|
||||
*)
|
||||
echo '"'"$current_val"'": {"S": "'"$(base64 /dev/urandom | awk '{print(0==NR%100)?"":$1}' | sed 's/[^[:alpha:]]/ /g' | head -1)"'"}'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
generate-put-request() {
|
||||
declare attribute_values
|
||||
attribute_values=$(generate-attribute-value 0)
|
||||
|
||||
for j in $(seq 1 $((ATTRIBUTES-1))); do
|
||||
attribute_values="$attribute_values, $(generate-attribute-value "$j")"
|
||||
done
|
||||
|
||||
|
||||
cat <<-EOF
|
||||
{
|
||||
"PutRequest": {
|
||||
"Item": {
|
||||
"id": {"S": "$(cat /proc/sys/kernel/random/uuid)"},
|
||||
$attribute_values
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
generate-batch-json() {
|
||||
declare batch_request='{ "'"$TABLE_NAME"'": ['
|
||||
batch_request="$batch_request $(generate-put-request)"
|
||||
|
||||
for i in $(seq 0 23); do
|
||||
batch_request="$batch_request, $(generate-put-request)"
|
||||
done
|
||||
|
||||
batch_request="$batch_request ]}"
|
||||
|
||||
echo "$batch_request"
|
||||
}
|
||||
|
||||
if ! (command -v aws > /dev/null 2>&1); then
|
||||
log-error "The AWS CLI must be installed first. Install the CLI first and then try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
parse-arguments "$@"
|
||||
show-properties
|
||||
|
||||
declare -i i=0
|
||||
declare -i items_written=0
|
||||
while [[ $items_written -lt $ITEMS ]]; do
|
||||
log-info "Writing 25 entries to DynamoDB..."
|
||||
aws dynamodb batch-write-item --request-items "$(generate-batch-json)"
|
||||
log-info 'Entries Written!'
|
||||
((i++))
|
||||
((items_written+=25))
|
||||
log-info "Total entries written: $items_written"
|
||||
log-info "Sleeping for 2 seconds to avoid the partition throughput limits..."
|
||||
sleep 2
|
||||
done
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
TERMINAL_HEIGHT=$(tput lines)
|
||||
BOX_HEIGHT=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_HEIGHT * .5" | bc)")
|
||||
|
||||
TERMINAL_WIDTH=$(tput cols)
|
||||
BOX_WIDTH=$(printf "%.0f" "$(echo "scale=2; $TERMINAL_WIDTH * .75" | bc)")
|
||||
|
||||
msg-box() {
|
||||
whiptail --fb --msgbox "$1" "$BOX_HEIGHT" "$BOX_WIDTH"
|
||||
}
|
||||
|
||||
check-sudo-pass() {
|
||||
log-info "Prompting user for sudo password with message: $1"
|
||||
if [[ ! "$PASSWORD" ]]; then
|
||||
PASSWORD=$(whiptail --fb --passwordbox "$1 Enter your sudo password" "$BOX_HEIGHT" "$BOX_WIDTH" 3>&2 2>&1 1>&3)
|
||||
fi
|
||||
}
|
||||
|
||||
show-tail-box() {
|
||||
trap "kill $2 2> /dev/null" EXIT
|
||||
|
||||
while kill -0 "$2" 2> /dev/null; do
|
||||
dialog --title "$1" --exit-label "Finished" --tailbox "$3" "$BOX_HEIGHT" "$BOX_WIDTH"
|
||||
done
|
||||
|
||||
clear
|
||||
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
prompt-yes-no() {
|
||||
declare action="$1"
|
||||
log-info "Prompting user if they wish to proceed with $action"
|
||||
|
||||
whiptail --fb --title "$action?" --yesno "Are you sure you wish to proceed with the specified action: $action?" --defaultno "$BOX_HEIGHT" "$BOX_WIDTH"
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
use std::{env, time::Duration};
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use aws_sdk_dynamodb::Client;
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use elasticsearch::{
|
||||
auth::Credentials,
|
||||
http::{
|
||||
transport::{SingleNodeConnectionPool, TransportBuilder},
|
||||
Url,
|
||||
},
|
||||
indices::IndicesPutMappingParts,
|
||||
Elasticsearch,
|
||||
};
|
||||
use log::{error, info, warn, LevelFilter};
|
||||
use log4rs::{
|
||||
append::console::ConsoleAppender,
|
||||
config::{Appender, Root},
|
||||
encode::pattern::PatternEncoder,
|
||||
};
|
||||
use models::{DynamoDbSimulationMetrics, DynamoOperation};
|
||||
use rand::{
|
||||
rngs::{OsRng, StdRng},
|
||||
Rng, SeedableRng,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::{
|
||||
select,
|
||||
sync::mpsc::{self, Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{models::Scenario, simulators::Simulator};
|
||||
|
||||
mod models;
|
||||
mod simulators;
|
||||
mod timer_utils;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
/// The number of concurrent simulations to run
|
||||
#[arg(short, long, default_value_t = 1000)]
|
||||
concurrent_simulations: u32,
|
||||
/// The number of attributes to use when populating and querying the DynamoDB table; minimum value of 1
|
||||
#[arg(short, long, default_value_t = 5)]
|
||||
attributes: u32,
|
||||
/// The length of time (in seconds) to run the benchmark for
|
||||
#[arg(short, long, default_value_t = 1800)]
|
||||
duration: u64,
|
||||
/// The buffer size of the Elasticsearch thread's MPSC channel
|
||||
#[arg(short, long, default_value_t = 500)]
|
||||
buffer: usize,
|
||||
/// Local Elasticsearch cluster username
|
||||
#[arg(short, long, default_value_t = String::from("elastic"))]
|
||||
username: String,
|
||||
/// Local Elasticsearch cluster password
|
||||
#[arg(short, long, default_value_t = String::from("changeme"))]
|
||||
password: String,
|
||||
/// The Elasticsearch Index to insert data into
|
||||
#[arg(short, long, default_value_t = String::from("dynamodb"))]
|
||||
index: String,
|
||||
/// The DynamoDB table to perform operations against
|
||||
#[arg(short, long, default_value_t = format!("{}-high-velocity-table", env::var("USER").unwrap()))]
|
||||
table_name: String,
|
||||
/// Whether to run a read-only scenario for benchmarking
|
||||
#[arg(short, long)]
|
||||
read_only: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
log4rs::init_config(init_logging_config())?;
|
||||
let cancellation_token = CancellationToken::new();
|
||||
|
||||
let (es_tx, es_rx) = mpsc::channel::<DynamoDbSimulationMetrics>(cli.buffer);
|
||||
std::thread::spawn(move || {
|
||||
start_elasticsearch_publisher(es_rx, cli.username, cli.password, cli.index)
|
||||
});
|
||||
|
||||
let handles: Vec<JoinHandle<_>> = (0..cli.concurrent_simulations)
|
||||
.map(|_| {
|
||||
let tx = es_tx.clone();
|
||||
let token = cancellation_token.clone();
|
||||
let table_name = cli.table_name.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let config = aws_config::load_from_env().await;
|
||||
let dynamodb_client = Client::new(&config);
|
||||
match scan_all_partition_keys(&dynamodb_client, table_name.clone()).await {
|
||||
Ok(partition_keys_vec) => {
|
||||
let simulator = Simulator::new(
|
||||
&dynamodb_client,
|
||||
table_name.clone(),
|
||||
cli.attributes,
|
||||
&partition_keys_vec,
|
||||
);
|
||||
select! {
|
||||
_ = token.cancelled() => {
|
||||
warn!("Task cancelled. Shutting down...");
|
||||
}
|
||||
_ = simulation_loop(simulator, cli.read_only, tx) => ()
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Unable to fetch partition keys: {e:?}"),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
tokio::spawn(async move {
|
||||
info!(
|
||||
"Starting timer task. Executing for {} seconds",
|
||||
cli.duration
|
||||
);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(cli.duration)).await;
|
||||
|
||||
cancellation_token.cancel();
|
||||
});
|
||||
|
||||
for handle in handles {
|
||||
match handle.await {
|
||||
Ok(_) => info!("Task shut down gracefully"),
|
||||
Err(e) => warn!("Task did not shut down gracefully {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn start_elasticsearch_publisher(
|
||||
mut elasticsearch_rx: Receiver<DynamoDbSimulationMetrics>,
|
||||
username: String,
|
||||
password: String,
|
||||
index: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let url = Url::parse("http://localhost:9200")?;
|
||||
let connection_pool = SingleNodeConnectionPool::new(url);
|
||||
let credentials = Credentials::Basic(username, password);
|
||||
let transport = TransportBuilder::new(connection_pool)
|
||||
.auth(credentials)
|
||||
.build()?;
|
||||
let es_client = Elasticsearch::new(transport);
|
||||
|
||||
info!("Setting the explicit mappings for the {index} index");
|
||||
es_client
|
||||
.indices()
|
||||
.put_mapping(IndicesPutMappingParts::Index(&[&index]))
|
||||
.body(json!({
|
||||
"properties": {
|
||||
"timestamp": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
while let Some(metric) = elasticsearch_rx.recv().await {
|
||||
info!("Publishing metrics to Elasticsearch...");
|
||||
|
||||
let es_response = es_client
|
||||
.index(elasticsearch::IndexParts::Index(&index))
|
||||
.body(metric)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match es_response {
|
||||
Ok(resp) => {
|
||||
if resp.status_code().is_success() {
|
||||
info!("Successfully published metrics to Elasticsearch");
|
||||
} else {
|
||||
error!("Was unable to publish metrics to Elasticsearch! Received non 2XX response");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unable to publish metrics to Elasticsearch! {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn simulation_loop(
|
||||
mut simulator: Simulator<'_>,
|
||||
read_only: bool,
|
||||
tx: Sender<DynamoDbSimulationMetrics>,
|
||||
) {
|
||||
let mut rng = StdRng::from_seed(OsRng.gen());
|
||||
loop {
|
||||
let mut metrics = DynamoDbSimulationMetrics::default();
|
||||
metrics.timestamp = Utc::now();
|
||||
|
||||
let simulation_time = time!(match {
|
||||
if read_only {
|
||||
info!("Running a read-only simulation...");
|
||||
metrics.scenario = Scenario::ReadOnly;
|
||||
run_read_only_simulation(&mut simulator, &mut metrics, &mut rng).await
|
||||
} else {
|
||||
info!("Running a CRUD simulation...");
|
||||
metrics.scenario = Scenario::Crud;
|
||||
run_crud_simulation(&mut simulator, &mut metrics, &mut rng).await
|
||||
}
|
||||
} {
|
||||
Ok(_) => {
|
||||
info!("Simulation completed successfully!");
|
||||
metrics.successful = true;
|
||||
}
|
||||
Err(e) => error!("Simulation did not complete. Encountered the following error: {e:?}"),
|
||||
});
|
||||
metrics.simulation_time = Some(simulation_time);
|
||||
info!("Metrics: {metrics:?}");
|
||||
|
||||
match tx.send(metrics).await {
|
||||
Ok(_) => info!("Metrics sent down channel successfully"),
|
||||
Err(e) => error!("Metrics were unable to be sent down the channel! {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_read_only_simulation(
|
||||
simulator: &mut Simulator<'_>,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
rng: &mut StdRng,
|
||||
) -> anyhow::Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(rng.gen_range(0..15))).await;
|
||||
|
||||
metrics.operation = DynamoOperation::Read;
|
||||
simulator.simulate_read_operation(metrics).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_crud_simulation(
|
||||
simulator: &mut Simulator<'_>,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
rng: &mut StdRng,
|
||||
) -> anyhow::Result<()> {
|
||||
match DynamoOperation::from(rng.gen_range(0..3)) {
|
||||
DynamoOperation::Read => {
|
||||
metrics.operation = DynamoOperation::Read;
|
||||
simulator.simulate_read_operation(metrics).await?
|
||||
}
|
||||
DynamoOperation::Write => {
|
||||
metrics.operation = DynamoOperation::Write;
|
||||
simulator.simulate_write_operation(metrics).await?;
|
||||
}
|
||||
DynamoOperation::Update => {
|
||||
metrics.operation = DynamoOperation::Update;
|
||||
simulator.simulate_update_operation(metrics).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn scan_all_partition_keys(
|
||||
dynamodb_client: &Client,
|
||||
table_name: String,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
info!("Fetching a large list of partition keys to randomly read...");
|
||||
let response = dynamodb_client
|
||||
.scan()
|
||||
.table_name(table_name)
|
||||
.limit(10000)
|
||||
.projection_expression("id")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
info!("Fetched partition keys!");
|
||||
let partition_keys = resp
|
||||
.items()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|attribute| {
|
||||
attribute
|
||||
.values()
|
||||
.last()
|
||||
.unwrap()
|
||||
.as_s()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
info!("Found a total of {} keys", partition_keys.len());
|
||||
Ok(partition_keys)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unable to fetch partition keys! {e:?}");
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logging_config() -> log4rs::Config {
|
||||
let stdout = ConsoleAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(
|
||||
"{d(%Y-%m-%d %H:%M:%S%.3f)(utc)} <{i}> [{l}] {f}:{L} - {m}{n}",
|
||||
)))
|
||||
.build();
|
||||
|
||||
log4rs::Config::builder()
|
||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||
.build(Root::builder().appender("stdout").build(LevelFilter::Info))
|
||||
.unwrap()
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
use serde_json::Number;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum DynamoOperation {
|
||||
#[default]
|
||||
Read,
|
||||
Write,
|
||||
Update,
|
||||
}
|
||||
|
||||
impl From<i32> for DynamoOperation {
|
||||
fn from(value: i32) -> Self {
|
||||
match value {
|
||||
0 => DynamoOperation::Read,
|
||||
1 => DynamoOperation::Write,
|
||||
2 => DynamoOperation::Update,
|
||||
_ => DynamoOperation::Read,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Scenario {
|
||||
#[default]
|
||||
Crud,
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BenchmarkingItem(HashMap<String, AttributeValue>);
|
||||
|
||||
impl From<HashMap<String, AttributeValue>> for BenchmarkingItem {
|
||||
fn from(value: HashMap<String, AttributeValue>) -> BenchmarkingItem {
|
||||
BenchmarkingItem(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl BenchmarkingItem {
|
||||
pub fn new(attributes: u32) -> BenchmarkingItem {
|
||||
let mut benchmarking_item = HashMap::<String, AttributeValue>::new();
|
||||
let mut rng = rand::thread_rng();
|
||||
benchmarking_item.insert(
|
||||
"id".to_owned(),
|
||||
AttributeValue::S(Uuid::new_v4().to_string()),
|
||||
);
|
||||
|
||||
(0..attributes).for_each(|i| {
|
||||
if let 0 = i % 2 {
|
||||
benchmarking_item.insert(i.to_string(), AttributeValue::S(lipsum::lipsum_words(15)));
|
||||
} else {
|
||||
benchmarking_item.insert(
|
||||
i.to_string(),
|
||||
AttributeValue::N(rng.gen_range(0.0..=32.0).to_string()),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
BenchmarkingItem(benchmarking_item)
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> AttributeValue {
|
||||
self.0.get("id").cloned().unwrap()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: &str, val: AttributeValue) -> Option<AttributeValue> {
|
||||
self.0.insert(key.to_owned(), val)
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, key: &str) -> Option<&AttributeValue> {
|
||||
self.0.get(key)
|
||||
}
|
||||
|
||||
pub fn extract_map(&self) -> HashMap<String, AttributeValue> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DynamoDbSimulationMetrics {
|
||||
pub operation: DynamoOperation,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub successful: bool,
|
||||
pub scenario: Scenario,
|
||||
pub simulation_time: Option<Number>,
|
||||
pub read_time: Option<Number>,
|
||||
pub write_time: Option<Number>,
|
||||
pub write_item_confirmation_time: Option<Number>,
|
||||
pub update_time: Option<Number>,
|
||||
pub update_item_confirmation_time: Option<Number>,
|
||||
pub delete_time: Option<Number>,
|
||||
pub delete_item_confirmation_time: Option<Number>,
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
use log::{error, info};
|
||||
|
||||
use crate::{models::DynamoDbSimulationMetrics, time};
|
||||
|
||||
use super::{utils, Simulator};
|
||||
|
||||
impl<'a> Simulator<'a> {
|
||||
pub(super) async fn assert_item_was_created(
|
||||
&mut self,
|
||||
id: AttributeValue,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
let partition_key = utils::extract_partition_key(id.clone());
|
||||
let mut attempts_exhausted = false;
|
||||
|
||||
let write_confirmation_time = time!(for i in 0..10 {
|
||||
info!("Attempt {i}: Fetching newly added item with partition key: {partition_key}");
|
||||
|
||||
match self.read_item(id.clone(), metrics, false).await? {
|
||||
Some(_) => {
|
||||
info!("Successfully read new item with partition key: {partition_key}");
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
error!("Unable to find new item with partition key: {partition_key}");
|
||||
if i == 9 {
|
||||
error!("All attempts to fetch the newly added item with partition key: {partition_key} failed!");
|
||||
attempts_exhausted = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if !attempts_exhausted {
|
||||
metrics.write_item_confirmation_time = Some(write_confirmation_time);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn assert_item_was_deleted(
|
||||
&mut self,
|
||||
id: AttributeValue,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
let partition_key = utils::extract_partition_key(id.clone());
|
||||
let mut attempts_exhausted = false;
|
||||
let delete_confirmation_time = time!(for i in 0..10 {
|
||||
info!("Attempt {i}: Fetching deleted item with partition key: {partition_key}...");
|
||||
match self.read_item(id.clone(), metrics, false).await? {
|
||||
Some(_) => {
|
||||
error!("Item with partition key {partition_key} was not deleted as expected!");
|
||||
if i == 9 {
|
||||
error!("All attempts to receive an empty response to verify item with partition key: {partition_key} was deleted failed!");
|
||||
attempts_exhausted = true;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
info!("Item with partition key {partition_key} was successfully deleted.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !attempts_exhausted {
|
||||
metrics.delete_item_confirmation_time = Some(delete_confirmation_time);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use aws_sdk_dynamodb::{types::AttributeValue, Client};
|
||||
use log::{error, info};
|
||||
use rand::{
|
||||
rngs::{OsRng, StdRng},
|
||||
Rng, SeedableRng,
|
||||
};
|
||||
|
||||
use crate::{models::DynamoDbSimulationMetrics, time};
|
||||
|
||||
mod assertions;
|
||||
mod operations;
|
||||
mod utils;
|
||||
|
||||
pub struct Simulator<'a> {
|
||||
dynamodb_client: &'a Client,
|
||||
table_name: String,
|
||||
attributes: u32,
|
||||
partition_keys_vec: &'a [String],
|
||||
rng: StdRng,
|
||||
}
|
||||
|
||||
impl<'a> Simulator<'a> {
|
||||
pub fn new(
|
||||
dynamodb_client: &'a Client,
|
||||
table_name: String,
|
||||
attributes: u32,
|
||||
partition_keys_vec: &'a [String],
|
||||
) -> Simulator<'a> {
|
||||
Simulator {
|
||||
dynamodb_client,
|
||||
table_name,
|
||||
attributes,
|
||||
partition_keys_vec,
|
||||
rng: StdRng::from_seed(OsRng.gen()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn simulate_read_operation(
|
||||
&mut self,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
info!("Performing READ Operation...");
|
||||
let partition_key =
|
||||
self.partition_keys_vec[self.rng.gen_range(0..self.partition_keys_vec.len())].clone();
|
||||
let id = AttributeValue::S(partition_key.clone());
|
||||
|
||||
for i in 0..10 {
|
||||
info!("Attempt {i}: Fetching existing item with partition key: {partition_key}");
|
||||
|
||||
match self.read_item(id.clone(), metrics, true).await? {
|
||||
Some(_) => {
|
||||
info!("Successfully read existing item with partition key: {partition_key}");
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
error!("Unable to find existing item with partition key: {partition_key}");
|
||||
if i == 9 {
|
||||
error!(
|
||||
"All attempts to fetch the existing item with partition key: {partition_key} failed!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn simulate_write_operation(
|
||||
&mut self,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
info!("Performing WRITE operation...");
|
||||
let benchmarking_item = self.put_item(metrics).await?;
|
||||
let id = benchmarking_item.get_id();
|
||||
|
||||
self.assert_item_was_created(id.clone(), metrics).await?;
|
||||
|
||||
self.delete_item(id.clone(), metrics).await?;
|
||||
|
||||
self.assert_item_was_deleted(id, metrics).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn simulate_update_operation(
|
||||
&mut self,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
info!("Performing UPDATE operation...");
|
||||
let new_item = self.put_item(metrics).await?;
|
||||
let id = new_item.get_id();
|
||||
let partition_key = utils::extract_partition_key(id.clone());
|
||||
let mut attempts_exhausted = false;
|
||||
|
||||
self.assert_item_was_created(id.clone(), metrics).await?;
|
||||
self.update_item(id.clone(), metrics).await?;
|
||||
|
||||
let update_confirmation_time = time!(for i in 0..10 {
|
||||
info!("Attempt {i}: Fetching updated item for partition key: {partition_key}...");
|
||||
|
||||
let updated_item = self.read_item(id.clone(), metrics, false).await?.unwrap();
|
||||
|
||||
let new_item_attribute_value = new_item
|
||||
.get("1")
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.as_n()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let updated_item_attribute_value = updated_item
|
||||
.get("1")
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.as_n()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
if new_item_attribute_value != updated_item_attribute_value {
|
||||
info!("Confirmed update for partition key: {partition_key}");
|
||||
break;
|
||||
} else {
|
||||
error!("Update for partition key {partition_key} failed! Values are still equal!");
|
||||
if i == 9 {
|
||||
error!("Exhausted attempts to fetch updated item!");
|
||||
attempts_exhausted = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !attempts_exhausted {
|
||||
metrics.update_item_confirmation_time = Some(update_confirmation_time);
|
||||
}
|
||||
|
||||
self.delete_item(id.clone(), metrics).await?;
|
||||
self.assert_item_was_deleted(id, metrics).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use anyhow::anyhow;
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
use log::{error, info};
|
||||
|
||||
use crate::{
|
||||
models::{BenchmarkingItem, DynamoDbSimulationMetrics},
|
||||
time,
|
||||
};
|
||||
|
||||
use super::{utils::extract_partition_key, Simulator};
|
||||
|
||||
impl<'a> Simulator<'a> {
|
||||
pub async fn read_item(
|
||||
&mut self,
|
||||
id: AttributeValue,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
record_metrics: bool,
|
||||
) -> anyhow::Result<Option<BenchmarkingItem>> {
|
||||
let partition_key = extract_partition_key(id.clone());
|
||||
let (read_time, response) = time!(
|
||||
resp,
|
||||
self
|
||||
.dynamodb_client
|
||||
.get_item()
|
||||
.table_name(self.table_name.clone())
|
||||
.key("id", id)
|
||||
.send()
|
||||
.await
|
||||
);
|
||||
|
||||
if record_metrics {
|
||||
metrics.read_time = Some(read_time);
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
info!("Found item: {}", partition_key);
|
||||
if let Some(item) = resp.item() {
|
||||
info!("Fetched item: {item:?}");
|
||||
Ok(Some(BenchmarkingItem::from(item.clone())))
|
||||
} else {
|
||||
info!("No items found with partition key: {partition_key}");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Could not fetch item with partition key: {partition_key}. {e:?}");
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_item(
|
||||
&mut self,
|
||||
id: AttributeValue,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut updated_item = BenchmarkingItem::new(self.attributes);
|
||||
updated_item.insert("id", id.clone());
|
||||
let partition_key = extract_partition_key(id);
|
||||
let (update_time, response) = time!(
|
||||
resp,
|
||||
self
|
||||
.dynamodb_client
|
||||
.put_item()
|
||||
.table_name(self.table_name.clone())
|
||||
.set_item(Some(updated_item.extract_map()))
|
||||
.send()
|
||||
.await
|
||||
);
|
||||
metrics.update_time = Some(update_time);
|
||||
|
||||
match response {
|
||||
Ok(_) => {
|
||||
info!("Successfully updated item with partition_key: {partition_key}");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Could not update item with partition key: {partition_key}. {e:?}");
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put_item(
|
||||
&mut self,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<BenchmarkingItem> {
|
||||
let new_item = BenchmarkingItem::new(self.attributes);
|
||||
let partition_key = extract_partition_key(new_item.get("id").cloned().unwrap());
|
||||
let (time, response) = time!(
|
||||
resp,
|
||||
self
|
||||
.dynamodb_client
|
||||
.put_item()
|
||||
.table_name(self.table_name.clone())
|
||||
.set_item(Some(new_item.extract_map()))
|
||||
.send()
|
||||
.await
|
||||
);
|
||||
metrics.write_time = Some(time);
|
||||
|
||||
match response {
|
||||
Ok(_) => {
|
||||
info!("Successfully put new item with partition key: {partition_key}");
|
||||
Ok(new_item)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Could not put new item with partition key: {partition_key}. {e:?}");
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_item(
|
||||
&mut self,
|
||||
id: AttributeValue,
|
||||
metrics: &mut DynamoDbSimulationMetrics,
|
||||
) -> anyhow::Result<()> {
|
||||
let partition_key = extract_partition_key(id.clone());
|
||||
let (delete_time, response) = time!(
|
||||
resp,
|
||||
self
|
||||
.dynamodb_client
|
||||
.delete_item()
|
||||
.table_name(self.table_name.clone())
|
||||
.key("id", id)
|
||||
.send()
|
||||
.await
|
||||
);
|
||||
metrics.delete_time = Some(delete_time);
|
||||
|
||||
match response {
|
||||
Ok(_) => {
|
||||
info!("Successfully deleted item with partition key: {partition_key}");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Could not delete item with partition key: {partition_key}. {e:?}");
|
||||
Err(anyhow!(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
|
||||
pub(super) fn extract_partition_key(id: AttributeValue) -> String {
|
||||
id.clone().as_s().unwrap().to_string()
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#[macro_export]
|
||||
macro_rules! time {
|
||||
($x:expr) => {{
|
||||
let start = std::time::Instant::now();
|
||||
let _result = $x;
|
||||
serde_json::Number::from(start.elapsed().as_millis())
|
||||
}};
|
||||
|
||||
($resp:ident, $x:expr) => {{
|
||||
let start = std::time::Instant::now();
|
||||
let $resp = $x;
|
||||
(serde_json::Number::from(start.elapsed().as_millis()), $resp)
|
||||
}};
|
||||
}
|
||||
Reference in New Issue
Block a user