├── .gitignore ├── README.md ├── packer ├── scripts │ ├── cassandra │ │ ├── cassandra.yaml │ │ ├── download_install.sh │ │ ├── generate_config.sh │ │ └── wait_for_cassandra.sh │ ├── client │ │ ├── install_client.sh │ │ ├── locustfile.py │ │ └── start_client.sh │ └── web │ │ ├── install_web.sh │ │ └── start_web.sh └── template.json ├── service ├── README.md ├── config.yml ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── datastax │ │ └── demo │ │ ├── DemoServiceApplication.java │ │ ├── DemoServiceConfiguration.java │ │ ├── api │ │ ├── CartItem.java │ │ └── DemoEntity.java │ │ ├── core │ │ └── DriverFactory.java │ │ ├── db │ │ ├── DemoDAO.java │ │ ├── SchemaManager.java │ │ └── ShoppingDAO.java │ │ └── resources │ │ ├── DemoResource1.java │ │ ├── HealthCheckResource.java │ │ ├── HomeResource.java │ │ └── ShoppingResource.java │ └── resources │ └── banner.txt └── terraform ├── elb.tf ├── global_accelerator.tf ├── instances_bastion.tf ├── instances_cassandra_nodes.tf ├── instances_cassandra_seeds.tf ├── instances_client.tf ├── instances_web.tf ├── main.tf ├── outputs.tf ├── security_groups.tf ├── subnets.tf ├── variables.tf └── vpcs.tf /.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | 3 | # No backups in repository 4 | *.backup 5 | 6 | # For the purpose of this demo, no state should be checked in 7 | *.tfstate 8 | 9 | # Terraform lock file when running 10 | *.lock.info 11 | 12 | *.iml 13 | service/target/ 14 | dependency-reduced-pom.xml 15 | 16 | *.pyc 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fault Tolerant Applications with Apache Cassandra™ Demo 2 | 3 | Demo project to provision and deploy a multi-tier application architecture that is resilient to infrastructure outages 4 | at Availability Zone and Region level using Cassandra. 5 | 6 | ## Table of contents 7 | 8 | - [Description](#description) 9 | - [Architecture diagram](#architecture-diagram) 10 | - [Schema and Queries](#schema-and-queries) 11 | - [Requirements](#requirements) 12 | - [Provisioning](#provisioning) 13 | - [Load testing](#load-testing) 14 | - [Simulate outages](#simulate-outages) 15 | 16 | ## Description 17 | 18 | This project contains the support files to provision the instances and services on AWS, along with instructions to 19 | simulate Availability Zone and Region level outages. You will be able to deploy a sample application on a cloud 20 | provider and see how it behaves during those outages, while handling incoming load to the system. 21 | 22 | The following services are created as part of this demo: 23 | 24 | - 6 EC2 instances for Cassandra nodes segregated in two data-centers: 25 | - Region us-east-1: 3 EC2 `m5.2xlarge` instances across 3 Availability Zones (AZ). 26 | - Region us-west-2: 3 EC2 `m5.2xlarge` instances across 3 AZs. 27 | - 6 EC2 `m5.large` instances to be used for application services, one in each AZ. 28 | - 2 [Elastic Load Balancers (ELB)][elb], one per region, with health checks enabled. 29 | - 1 [AWS Global Accelerator][gacc] as ELBs anycast frontend, with health checks enabled. 30 | - 2 EC2 `t2.small` instances to be used as clients, one in each region. 31 | 32 | Note that regions used in this demo can be configured if needed. 33 | 34 | Equivalent solutions can be deployed in other public cloud providers and on premise, please refer to the whitepaper 35 | for more information. 36 | 37 | ## Architecture diagram 38 | 39 | ![Architecture diagram](https://i.imgur.com/N2OKUZ2.png) 40 | 41 | _Note that each web instance connects to all the Cassandra nodes within the region, regardless of the AZ._ 42 | 43 | ## Schema and Queries 44 | ### Schema 45 | ``` 46 | CREATE KEYSPACE IF NOT EXISTS shopping WITH REPLICATION = {'class':'NetworkTopologyStrategy','us-east-1': 3,'us-west-2': 3}; 47 | CREATE TABLE IF NOT EXISTS shopping.carts ( 48 | username text, 49 | item_id int, 50 | date_added timestamp, 51 | item_name text, 52 | PRIMARY KEY (username, item_id, date_added)) 53 | ``` 54 | ### Queries 55 | Reads: Consistency Level = LOCAL_QUORUM 56 | ``` 57 | SELECT * FROM shopping.carts WHERE username = ? 58 | ``` 59 | Writes: Consistency Level = LOCAL_QUORUM 60 | ``` 61 | INSERT INTO shopping.carts (username, item_id, date_added, item_name) VALUES (?, ?, toTimestamp(now()), ?) 62 | ``` 63 | 64 | This demo uses OSS Java Driver 4.x 65 | 66 | ## Requirements 67 | 68 | - [Packer][packer] v1.2 or above. 69 | - [Terraform][terraform] v0.12 or above. 70 | - AWS Account. 71 | 72 | ## Provisioning 73 | 74 | Infrastructure is created and provisioned using [Terraform][terraform] and software for Apache Cassandra, microservices 75 | and client load testing tool is deployed using [Packer][packer] images. 76 | 77 | Note that images, instances and services created on AWS have an associated cost. Estimated cost of running this demo 78 | on AWS is around $10 per hour. 79 | 80 | ### Clone the repository 81 | 82 | ```bash 83 | git clone git@github.com:datastax/dc-failover-demo.git 84 | cd dc-failover-demo 85 | ``` 86 | 87 | ### Packer images 88 | 89 | To build the images on the different regions use: 90 | 91 | ```bash 92 | packer build ./packer/template.json 93 | ``` 94 | 95 | Expect building the images to take several minutes. 96 | 97 | ### Terraform 98 | 99 | To create the instances and services use: 100 | 101 | ```bash 102 | terraform apply ./terraform/ 103 | ``` 104 | 105 | ### Verify 106 | 107 | Terraform exports output values that are displayed after creating the infrastructure and provisioning. 108 | 109 | These outputs are AWS Global Accelerator public IP addresses, load tester clients public IPs, bastion public IP and 110 | dev private key to access the bastion. 111 | 112 | Use one of the public ips of the Global Accelerator to access the service: 113 | 114 | ```bash 115 | curl -i http:/// 116 | ``` 117 | 118 | ### Using Packer and Terraform with AWS Vault / AWS Okta 119 | 120 | If you have multiple AWS profiles, we recommend using a tool to manage those credentials, like 121 | [AWS Vault][aws-vault], [AWS Okta][aws-okta] 122 | (if you are using federated login with [Okta][okta]). 123 | 124 | To create and deploy the infrastructure with AWS Vault, use: 125 | 126 | ```bash 127 | aws-vault exec -- packer build ./packer/template.json 128 | aws-vault exec -- terraform apply ./terraform/ 129 | ``` 130 | 131 | With AWS Okta, use: 132 | 133 | ```bash 134 | aws-okta exec -- packer build ./packer/template.json 135 | aws-okta exec -- terraform apply ./terraform/ 136 | ``` 137 | 138 | ## Load testing 139 | 140 | This demo uses [Locust][locust] to put demand on the system and measuring its response. 141 | 142 | Access the two locust instances using a browser with the urls included in the terraform output, for 143 | example: `http://V.X.Y.Z:8089`. 144 | 145 | ## Simulate outages 146 | 147 | While load testing is ongoing, you can simulate outages at different failure domains to see how the application will 148 | respond to those events. 149 | 150 | ### Simulate AZ outage 151 | 152 | To simulate an Availability Zone outage, we remove the security group rule that allows internal TCP connections on 153 | Availability Zone 3 at Region 2. 154 | 155 | ```bash 156 | terraform destroy \ 157 | -target aws_instance.i_cassandra_r2_i3 \ 158 | -target aws_security_group_rule.sg_rule_sg_r2_az3_allow_all_internal ./terraform/ 159 | ``` 160 | 161 | As a result, for new incoming requests the load balancer will route traffic to service instances in the 162 | healthy zones. Application service instances using Apache Cassandra nodes from AZs that were not impacted will not 163 | experience errors derived from the loss of connectivity to the failed nodes and the drivers will attempt to 164 | reconnect in the background while using the set of live nodes as coordinators for the queries while the failed nodes 165 | are offline. 166 | 167 | ### Simulate Region outage 168 | 169 | To simulate a region-level outage, we remove the security group rules for the whole Region 2 and inter 170 | region VPC peering. 171 | 172 | ```bash 173 | terraform destroy \ 174 | -target aws_security_group_rule.sg_rule_elb_r2_allow_http \ 175 | -target aws_security_group_rule.sg_rule_default_r2_allow_all_internal \ 176 | -target aws_security_group_rule.sg_rule_sg_r2_az3_allow_all_internal \ 177 | -target aws_vpc_peering_connection.r1_to_r2_requester \ 178 | ./terraform/ 179 | ``` 180 | 181 | As a result, for new incoming requests AWS Global Accelerator will route traffic to the ELB instance in the second 182 | region. Application services that are healthy can continue querying the database targeting the local data center. 183 | 184 | Note that Global Accelerator will take ten seconds to identify a target as unhealthy. 185 | 186 | ### Cleanup 187 | 188 | You can cleanup all terraform managed resources with: 189 | 190 | ```bash 191 | terraform destroy ./terraform/ 192 | ``` 193 | 194 | ## Notice 195 | 196 | The source code contained in this project is designed for demonstration purposes and it's not intended for production 197 | use. A bastion instance is created along with a private key to access using ssh, in case you want to 198 | verify the state of the instances manually. Note that this private key is included in terraform outputs 199 | and can be a security risk in your deployment, outside this demo use keys generated beforehand and store them 200 | securily. 201 | 202 | The software is provided "as is", without warranty of any kind. Any use by you of the source 203 | code is at your own risk. 204 | 205 | [aws-vault]: https://github.com/99designs/aws-vault 206 | [aws-okta]: https://github.com/segmentio/aws-okta 207 | [okta]: https://www.okta.com/ 208 | [packer]: https://www.packer.io/ 209 | [terraform]: https://www.terraform.io/ 210 | [elb]: https://aws.amazon.com/elasticloadbalancing/ 211 | [gacc]: https://aws.amazon.com/global-accelerator/ 212 | [locust]: https://www.locust.io/ 213 | -------------------------------------------------------------------------------- /packer/scripts/cassandra/cassandra.yaml: -------------------------------------------------------------------------------- 1 | cluster_name: 'DemoCluster' 2 | num_tokens: 4 3 | allocate_tokens_for_local_replication_factor: 3 4 | endpoint_snitch: GossipingPropertyFileSnitch 5 | partitioner: org.apache.cassandra.dht.Murmur3Partitioner 6 | commitlog_sync: periodic 7 | commitlog_sync_period_in_ms: 10000 8 | commitlog_segment_size_in_mb: 32 9 | start_native_transport: true 10 | 11 | -------------------------------------------------------------------------------- /packer/scripts/cassandra/download_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install packages 4 | sudo apt-get update -y -qq 5 | sudo apt-get install -y openjdk-8-jdk-headless 6 | sudo apt-get install -y cloud-utils 7 | sudo apt-get install -y python-dev python-setuptools python-yaml 8 | 9 | # download and uncompress Cassandra 10 | mkdir cassandra; wget -c http://archive.apache.org/dist/cassandra/4.0-alpha4/apache-cassandra-4.0-alpha4-bin.tar.gz -O - | tar -xz -C cassandra --strip-components=1 11 | 12 | -------------------------------------------------------------------------------- /packer/scripts/cassandra/generate_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### 4 | ### Generates cassandra.yaml and other files based on environment variables 5 | ### 6 | 7 | export AZ=$(ec2metadata --availability-zone) 8 | export REGION=$(echo $AZ | sed 's/.$//') 9 | export PRIVATE_IP=$(ec2metadata --local-ipv4) 10 | 11 | # Replace cassandra.yaml file 12 | cp cassandra.yaml cassandra/conf/ 13 | 14 | cd cassandra/conf 15 | 16 | # Set DC and RACK on cassandra-rackdc.properties 17 | printf 'dc=%s\nrack=%s\n' $REGION $AZ > cassandra-rackdc.properties 18 | 19 | # Remove cassandra-topology.properties file 20 | rm -f cassandra/conf/cassandra-topology.properties 21 | 22 | # Set seeds and listen address 23 | printf '\nlisten_address: "%s"' $PRIVATE_IP >> cassandra.yaml 24 | printf '\nrpc_address: "%s"' $PRIVATE_IP >> cassandra.yaml 25 | printf '\nseed_provider:\n - class_name: org.apache.cassandra.locator.SimpleSeedProvider\n parameters:\n - seeds: ' >> cassandra.yaml 26 | printf '"%s"\n' $1 >> cassandra.yaml 27 | 28 | cd -------------------------------------------------------------------------------- /packer/scripts/cassandra/wait_for_cassandra.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PRIVATE_IP=$(ec2metadata --local-ipv4) 4 | 5 | if timeout 240 bash -c 'until (echo > /dev/tcp/$PRIVATE_IP/9042) 2> /dev/null; do echo "Waiting for port 9042..."; sleep 2; done'; then 6 | echo "Cassandra node bootstrapped at $PRIVATE_IP" 7 | else 8 | echo "Failed to start cassandra at $PRIVATE_IP" 9 | exit 1 10 | fi 11 | -------------------------------------------------------------------------------- /packer/scripts/client/install_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install packages 4 | sudo apt-get update -y -qq 5 | sudo apt-get install -y openjdk-8-jdk-headless 6 | sudo apt-get install -y cloud-utils 7 | sudo apt-get install -y python-dev python-setuptools python-yaml python-pip 8 | 9 | # install locust 10 | sudo pip install 'locustio==0.13.5' 11 | -------------------------------------------------------------------------------- /packer/scripts/client/locustfile.py: -------------------------------------------------------------------------------- 1 | import string 2 | import random 3 | import gevent 4 | 5 | from requests.exceptions import ConnectionError, ReadTimeout 6 | from locust import HttpLocust, TaskSet, task 7 | 8 | def random_id(): 9 | return random.randint(1, 2**31) 10 | 11 | def random_string(min_length=4, max_length=16): 12 | return ''.join(random.choice(string.ascii_lowercase) for x in range(random.randint(min_length, max_length))) 13 | 14 | def random_items(): 15 | items=[] 16 | for x in range(1, 42): 17 | items.append({"id": x, "name": random_string()}) 18 | return items 19 | 20 | def random_item(): 21 | global ITEMS 22 | return random.choice(ITEMS) 23 | 24 | class DemoBehavior(TaskSet): 25 | def on_start(self): 26 | self.id = random_id() 27 | self.create() 28 | 29 | @task(1) 30 | def create(self): 31 | self.client.post("/demo/", json={"id": self.id, "content": "stuff"}, ) 32 | 33 | @task(2) 34 | def get(self): 35 | self.client.get("/demo/{}".format(self.id), name="demo/[id]") 36 | 37 | class ShopperBehavior(TaskSet): 38 | MAX_NUM_RETRIES = 2 39 | MAX_NUM_RETRIES_CONNECTION = 25 40 | RETRY_DELAY = 1 41 | RETRY_DELAY_CONNECTION = 0.2 42 | TIMEOUT = 0.6 43 | 44 | def on_start(self): 45 | self.username = random_string() 46 | self.items=random_items() 47 | 48 | @task(1) 49 | def add_item(self): 50 | self.send_request("post", "/cart/{}/add".format(self.username), "/cart/[username]/add", 51 | random.choice(self.items)) 52 | 53 | @task(2) 54 | def get_cart(self): 55 | self.send_request("get", "/cart/{}".format(self.username), "/cart/[username]", None) 56 | 57 | def send_request(self, method, path, name, json): 58 | retry = True 59 | num_retry = 0 60 | num_retry_connection = 0 61 | 62 | while retry: 63 | 64 | try: 65 | with getattr(self.client, method)(path, name=name, json=json, catch_response=True, 66 | timeout=self.TIMEOUT) as response: 67 | retry = False 68 | 69 | if response.ok: 70 | response.success() 71 | elif response.status_code == 504: 72 | # 504 Gateway timeouts occur when the Global Accelerator / ELB timeouts obtaining a response 73 | # from endpoints / target group 74 | if num_retry < self.MAX_NUM_RETRIES: 75 | response.success() 76 | # Retry a few times to simulate a client device obtaining a Gateway timeout as 77 | # part of the normal app flow 78 | num_retry += 1 79 | retry = True 80 | gevent.sleep(self.RETRY_DELAY) 81 | else: 82 | response.failure("504 Gateway timeout failure after {} retries".format(num_retry)) 83 | else: 84 | response.failure("Failed with status code {}: {}".format(response.status_code, response.text)) 85 | 86 | except (ConnectionError, ReadTimeout): 87 | # Connections are dropped from the Global Accelerator to the ELB instance when we are removing 88 | # the security group 89 | if num_retry_connection >= self.MAX_NUM_RETRIES_CONNECTION: 90 | raise 91 | 92 | # Retry part of the normal app flow 93 | num_retry_connection += 1 94 | gevent.sleep(self.RETRY_DELAY_CONNECTION) 95 | 96 | class WebUser(HttpLocust): 97 | task_set = ShopperBehavior 98 | min_wait = 500 99 | max_wait = 1500 100 | -------------------------------------------------------------------------------- /packer/scripts/client/start_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | locust -f locustfile.py --host=http://$1 4 | -------------------------------------------------------------------------------- /packer/scripts/web/install_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install packages 4 | sudo apt-get update -y -qq 5 | sudo apt-get install -y openjdk-8-jdk-headless 6 | sudo apt-get install -y cloud-utils 7 | sudo apt-get install -y maven 8 | 9 | cd service 10 | mvn clean install 11 | -------------------------------------------------------------------------------- /packer/scripts/web/start_web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | AZ1=$2 4 | AZ2=$3 5 | 6 | export CONTACT_POINTS=$1 7 | export LOCAL_DATA_CENTER=$(echo $AZ1 | sed 's/.$//') 8 | export REMOTE_DATA_CENTER=$(echo $AZ2 | sed 's/.$//') 9 | export CREATE_SCHEMA=${4:-false} 10 | 11 | cd service 12 | java -jar target/service-1.0-SNAPSHOT.jar server config.yml -------------------------------------------------------------------------------- /packer/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "region1": "us-east-1", 4 | "region2": "us-west-2" 5 | }, 6 | "builders": [{ 7 | "name": "cassandra", 8 | "type": "amazon-ebs", 9 | "region": "{{user `region1`}}", 10 | "source_ami_filter": { 11 | "filters": { 12 | "virtualization-type": "hvm", 13 | "name": "ubuntu/images/*ubuntu-bionic-18.04-amd64-server-*", 14 | "root-device-type": "ebs" 15 | }, 16 | "owners": ["099720109477"], 17 | "most_recent": true 18 | }, 19 | "instance_type": "m5.2xlarge", 20 | "ssh_username": "ubuntu", 21 | "ami_name": "cassandra-image {{timestamp}}", 22 | "ami_regions": ["{{user `region1`}}", "{{user `region2`}}"] 23 | },{ 24 | "name": "web", 25 | "type": "amazon-ebs", 26 | "region": "{{user `region1`}}", 27 | "source_ami_filter": { 28 | "filters": { 29 | "virtualization-type": "hvm", 30 | "name": "ubuntu/images/*ubuntu-bionic-18.04-amd64-server-*", 31 | "root-device-type": "ebs" 32 | }, 33 | "owners": ["099720109477"], 34 | "most_recent": true 35 | }, 36 | "instance_type": "m5.large", 37 | "ssh_username": "ubuntu", 38 | "ami_name": "demo-web-image {{timestamp}}", 39 | "ami_regions": ["{{user `region1`}}", "{{user `region2`}}"] 40 | },{ 41 | "name": "client", 42 | "type": "amazon-ebs", 43 | "region": "{{user `region1`}}", 44 | "source_ami_filter": { 45 | "filters": { 46 | "virtualization-type": "hvm", 47 | "name": "ubuntu/images/*ubuntu-bionic-18.04-amd64-server-*", 48 | "root-device-type": "ebs" 49 | }, 50 | "owners": ["099720109477"], 51 | "most_recent": true 52 | }, 53 | "instance_type": "m5.large", 54 | "ssh_username": "ubuntu", 55 | "ami_name": "demo-client-image {{timestamp}}", 56 | "ami_regions": ["{{user `region1`}}", "{{user `region2`}}"] 57 | }], 58 | "provisioners": [ 59 | { 60 | "type": "shell", 61 | "inline": "/usr/bin/cloud-init status --wait" 62 | }, 63 | { 64 | "only": ["cassandra"], 65 | "type": "file", 66 | "source": "./packer/scripts/cassandra/cassandra.yaml", 67 | "destination": "/home/ubuntu/" 68 | }, 69 | { 70 | "only": ["cassandra"], 71 | "type": "file", 72 | "source": "./packer/scripts/cassandra/generate_config.sh", 73 | "destination": "/home/ubuntu/" 74 | }, 75 | { 76 | "only": ["cassandra"], 77 | "type": "file", 78 | "source": "./packer/scripts/cassandra/wait_for_cassandra.sh", 79 | "destination": "/home/ubuntu/" 80 | }, 81 | { 82 | "only": ["cassandra"], 83 | "type": "shell", 84 | "script": "./packer/scripts/cassandra/download_install.sh" 85 | }, 86 | { 87 | "only": ["web"], 88 | "type": "file", 89 | "source": "./service", 90 | "destination": "/home/ubuntu/" 91 | }, 92 | { 93 | "only": ["web"], 94 | "type": "shell", 95 | "script": "./packer/scripts/web/install_web.sh" 96 | }, 97 | { 98 | "only": ["web"], 99 | "type": "file", 100 | "source": "./packer/scripts/web/start_web.sh", 101 | "destination": "/home/ubuntu/" 102 | }, 103 | { 104 | "only": ["client"], 105 | "type": "shell", 106 | "script": "./packer/scripts/client/install_client.sh" 107 | }, 108 | { 109 | "only": ["client"], 110 | "type": "file", 111 | "source": "./packer/scripts/client/start_client.sh", 112 | "destination": "/home/ubuntu/" 113 | }, 114 | { 115 | "only": ["client"], 116 | "type": "file", 117 | "source": "./packer/scripts/client/locustfile.py", 118 | "destination": "/home/ubuntu/" 119 | }, 120 | { 121 | "type": "shell", 122 | "inline": "chmod +x *.sh" 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /service/README.md: -------------------------------------------------------------------------------- 1 | # DemoService 2 | 3 | How to start the DemoService application 4 | --- 5 | 6 | 1. Run `mvn clean install` to build your application 7 | 1. Start application with `java -jar target/service-1.0-SNAPSHOT.jar server config.yml` 8 | 1. To check that your application is running enter url `http://localhost:8080` -------------------------------------------------------------------------------- /service/config.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: INFO 3 | loggers: 4 | com.datastax.demo: DEBUG 5 | 6 | driverFactory: 7 | contactPoints: "${CONTACT_POINTS:-127.0.0.1}" 8 | localDataCenter: "${LOCAL_DATA_CENTER:-dc1}" 9 | remoteDataCenter: "${REMOTE_DATA_CENTER:-dc2}" 10 | createSchema: "${CREATE_SCHEMA:-false}" 11 | 12 | server: 13 | applicationConnectors: 14 | - type: http 15 | port: 8080 16 | adminConnectors: [] 17 | -------------------------------------------------------------------------------- /service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 4.0.0 8 | 9 | com.datastax.demo 10 | service 11 | 1.0-SNAPSHOT 12 | jar 13 | 14 | DemoService 15 | 16 | 17 | UTF-8 18 | UTF-8 19 | com.datastax.demo.DemoServiceApplication 20 | 1.3.12 21 | 4.5.1 22 | 23 | 24 | 25 | 26 | 27 | io.dropwizard 28 | dropwizard-bom 29 | ${dropwizard.version} 30 | pom 31 | import 32 | 33 | 34 | 35 | 36 | 37 | 38 | io.dropwizard 39 | dropwizard-core 40 | 41 | 42 | com.datastax.oss 43 | java-driver-core 44 | ${driver.version} 45 | 46 | 47 | 48 | 49 | 50 | 51 | maven-shade-plugin 52 | 2.4.1 53 | 54 | true 55 | 56 | 57 | 58 | ${mainClass} 59 | 60 | 61 | 62 | 63 | 64 | *:* 65 | 66 | META-INF/*.SF 67 | META-INF/*.DSA 68 | META-INF/*.RSA 69 | 70 | 71 | 72 | 73 | 74 | 75 | package 76 | 77 | shade 78 | 79 | 80 | 81 | 82 | 83 | maven-jar-plugin 84 | 3.0.2 85 | 86 | 87 | 88 | true 89 | ${mainClass} 90 | 91 | 92 | 93 | 94 | 95 | maven-compiler-plugin 96 | 3.6.1 97 | 98 | 1.8 99 | 1.8 100 | 101 | 102 | 103 | maven-source-plugin 104 | 3.0.1 105 | 106 | 107 | attach-sources 108 | 109 | jar 110 | 111 | 112 | 113 | 114 | 115 | maven-javadoc-plugin 116 | 3.0.0-M1 117 | 118 | 119 | attach-javadocs 120 | 121 | jar 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/DemoServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo; 2 | 3 | import com.datastax.demo.db.DemoDAO; 4 | import com.datastax.demo.db.SchemaManager; 5 | import com.datastax.demo.db.ShoppingDAO; 6 | import com.datastax.demo.resources.DemoResource1; 7 | import com.datastax.demo.resources.HealthCheckResource; 8 | import com.datastax.demo.resources.HomeResource; 9 | import com.datastax.demo.resources.ShoppingResource; 10 | import com.datastax.oss.driver.api.core.CqlSession; 11 | import io.dropwizard.Application; 12 | import io.dropwizard.configuration.EnvironmentVariableSubstitutor; 13 | import io.dropwizard.configuration.SubstitutingSourceProvider; 14 | import io.dropwizard.jersey.errors.ErrorMessage; 15 | import io.dropwizard.setup.Bootstrap; 16 | import io.dropwizard.setup.Environment; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import javax.ws.rs.core.MediaType; 21 | import javax.ws.rs.core.Response; 22 | import javax.ws.rs.ext.ExceptionMapper; 23 | import java.io.PrintWriter; 24 | import java.io.StringWriter; 25 | 26 | public class DemoServiceApplication extends Application { 27 | private static final Logger logger = LoggerFactory.getLogger(DemoServiceApplication.class); 28 | 29 | private static class DemoExceptionMapper implements ExceptionMapper { 30 | @Override 31 | public Response toResponse(Exception e) { 32 | StringWriter stackTraceWriter = new StringWriter(); 33 | e.printStackTrace(new PrintWriter(stackTraceWriter)); 34 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR) 35 | .entity(new ErrorMessage(e.getMessage())) 36 | .type(MediaType.APPLICATION_JSON) 37 | .build(); 38 | } 39 | } 40 | 41 | public static void main(final String[] args) throws Exception { 42 | new DemoServiceApplication().run(args); 43 | } 44 | 45 | @Override 46 | public String getName() { 47 | return "DemoService"; 48 | } 49 | 50 | @Override 51 | public void initialize(final Bootstrap bootstrap) { 52 | bootstrap.setConfigurationSourceProvider( 53 | new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), 54 | new EnvironmentVariableSubstitutor(false))); 55 | } 56 | 57 | @Override 58 | public void run(final DemoServiceConfiguration config, final Environment environment) throws InterruptedException { 59 | final CqlSession session = config.getDriverFactory().build(environment); 60 | 61 | if (config.getDriverFactory().isCreateSchema()) { 62 | SchemaManager.createSchema(session, config); 63 | } else { 64 | SchemaManager.waitSchema(session); 65 | } 66 | 67 | environment.jersey().register(new DemoExceptionMapper()); 68 | environment.jersey().register(new DemoResource1(new DemoDAO(session))); 69 | environment.jersey().register(new ShoppingResource(new ShoppingDAO(session))); 70 | environment.jersey().register(new HealthCheckResource()); 71 | environment.jersey().register(new HomeResource(environment.jersey().getResourceConfig(), session)); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/DemoServiceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo; 2 | 3 | import io.dropwizard.Configuration; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import javax.validation.Valid; 7 | import javax.validation.constraints.*; 8 | 9 | import com.datastax.demo.core.DriverFactory; 10 | 11 | public class DemoServiceConfiguration extends Configuration { 12 | @Valid 13 | @NotNull 14 | private DriverFactory driverFactory = new DriverFactory(); 15 | 16 | @JsonProperty("driverFactory") 17 | public DriverFactory getDriverFactory() { 18 | return driverFactory; 19 | } 20 | 21 | @JsonProperty("driverFactory") 22 | public void setDriverFactory(DriverFactory factory) { 23 | this.driverFactory = factory; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/api/CartItem.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.api; 2 | 3 | import com.datastax.oss.driver.api.core.cql.Row; 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonFormat; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | 8 | import java.time.Instant; 9 | 10 | public class CartItem { 11 | private int id; 12 | private String name; 13 | private Instant dateAdded; 14 | 15 | public CartItem(Row row) { 16 | this.id = row.getInt("item_id"); 17 | this.name = row.getString("item_name"); 18 | this.dateAdded = row.getInstant("date_added"); 19 | } 20 | 21 | @JsonCreator 22 | public CartItem(@JsonProperty("id") int id, @JsonProperty("name") String name) { 23 | this.id = id; 24 | this.name = name; 25 | } 26 | 27 | @JsonProperty("id") 28 | public int getId() { 29 | return id; 30 | } 31 | 32 | @JsonProperty("name") 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | @JsonProperty("date_added") 38 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS", timezone = "UTC") 39 | public Instant getDateAdded() { 40 | return dateAdded; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/api/DemoEntity.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class DemoEntity { 6 | private int id; 7 | 8 | private String content; 9 | 10 | public DemoEntity() { 11 | // Jackson deserialization 12 | } 13 | 14 | public DemoEntity(int id, String content) { 15 | this.id = id; 16 | this.content = content; 17 | } 18 | 19 | @JsonProperty 20 | public int getId() { 21 | return id; 22 | } 23 | 24 | @JsonProperty 25 | public String getContent() { 26 | return content; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "DemoEntity{" + "id=" + id + ", content='" + content + '\'' + '}'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/core/DriverFactory.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.core; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import io.dropwizard.lifecycle.Managed; 7 | import io.dropwizard.setup.Environment; 8 | import org.hibernate.validator.constraints.NotEmpty; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import com.datastax.oss.driver.api.core.CqlSession; 13 | import com.datastax.oss.driver.api.core.CqlSessionBuilder; 14 | 15 | public class DriverFactory 16 | { 17 | private static final Logger logger = LoggerFactory.getLogger(DriverFactory.class); 18 | private static final int PORT = 9042; 19 | 20 | @NotEmpty 21 | private String contactPoints; 22 | 23 | @NotEmpty 24 | private String localDataCenter; 25 | 26 | @NotEmpty 27 | private String remoteDataCenter; 28 | 29 | private boolean createSchema; 30 | 31 | @JsonProperty 32 | public String getLocalDataCenter() { 33 | return localDataCenter; 34 | } 35 | 36 | @JsonProperty 37 | public void setLocalDataCenter(String localDataCenter) { 38 | this.localDataCenter = localDataCenter; 39 | } 40 | 41 | @JsonProperty 42 | public String getRemoteDataCenter() { 43 | return remoteDataCenter; 44 | } 45 | 46 | @JsonProperty 47 | public void setRemoteDataCenter(String remoteDataCenter) { 48 | this.remoteDataCenter = remoteDataCenter; 49 | } 50 | 51 | @JsonProperty 52 | public String getContactPoints() { 53 | return contactPoints; 54 | } 55 | 56 | @JsonProperty 57 | public void setContactPoints(String contactPoints) { 58 | this.contactPoints = contactPoints; 59 | } 60 | 61 | @JsonProperty 62 | public boolean isCreateSchema() 63 | { 64 | return createSchema; 65 | } 66 | 67 | @JsonProperty 68 | public void setCreateSchema(boolean createSchema) 69 | { 70 | this.createSchema = createSchema; 71 | } 72 | 73 | public CqlSession build(Environment environment) { 74 | final CqlSessionBuilder builder = CqlSession.builder(); 75 | 76 | for (String contactPoint : contactPoints.split("\\s*,\\s*")) { 77 | builder.addContactPoint(new InetSocketAddress(contactPoint, PORT)); 78 | } 79 | 80 | final CqlSession session = builder.withLocalDatacenter(localDataCenter).build(); 81 | environment.lifecycle().manage(new Managed() { 82 | @Override 83 | public void start() { 84 | } 85 | 86 | @Override 87 | public void stop() { 88 | session.close(); 89 | } 90 | }); 91 | 92 | return session; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/db/DemoDAO.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.db; 2 | 3 | import com.datastax.demo.api.DemoEntity; 4 | import com.datastax.oss.driver.api.core.ConsistencyLevel; 5 | import com.datastax.oss.driver.api.core.CqlSession; 6 | import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; 7 | import com.datastax.oss.driver.api.core.cql.BoundStatement; 8 | import com.datastax.oss.driver.api.core.cql.PreparedStatement; 9 | import com.datastax.oss.driver.api.core.cql.Row; 10 | 11 | public class DemoDAO 12 | { 13 | private final CqlSession session; 14 | private final PreparedStatement psSelect; 15 | private final PreparedStatement psInsert; 16 | 17 | private static final ConsistencyLevel READ_CONSISTENCY_LEVEL = DefaultConsistencyLevel.LOCAL_ONE; 18 | private static final ConsistencyLevel WRITE_CONSISTENCY_LEVEL = DefaultConsistencyLevel.LOCAL_ONE; 19 | 20 | public DemoDAO(CqlSession session) 21 | { 22 | this.session = session; 23 | 24 | psSelect = session.prepare("SELECT * FROM ks1.table1 WHERE id = ?"); 25 | psInsert = session.prepare("INSERT INTO ks1.table1 (id) VALUES (?)"); 26 | } 27 | 28 | public DemoEntity get(int id) 29 | { 30 | final BoundStatement statement = psSelect.bind(id).setConsistencyLevel(READ_CONSISTENCY_LEVEL); 31 | 32 | final Row row = session.execute(statement).one(); 33 | if (row == null) { 34 | return null; 35 | } 36 | 37 | return new DemoEntity(row.getInt("id"), "got from db"); 38 | } 39 | 40 | public DemoEntity create(DemoEntity entity) 41 | { 42 | final BoundStatement statement = psInsert.bind(entity.getId()).setConsistencyLevel(WRITE_CONSISTENCY_LEVEL); 43 | session.execute(statement); 44 | return entity; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/db/SchemaManager.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.db; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import com.datastax.demo.DemoServiceApplication; 7 | import com.datastax.demo.DemoServiceConfiguration; 8 | import com.datastax.oss.driver.api.core.CqlSession; 9 | import com.datastax.oss.driver.api.core.cql.SimpleStatement; 10 | 11 | public class SchemaManager 12 | { 13 | private static final Logger logger = LoggerFactory.getLogger(DemoServiceApplication.class); 14 | 15 | private static String createKeyspace(DemoServiceConfiguration config, String name) { 16 | return String.format( 17 | "CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION = {'class':'NetworkTopologyStrategy','%s': 3,'%s': 3}", 18 | name, 19 | config.getDriverFactory().getLocalDataCenter(), 20 | config.getDriverFactory().getRemoteDataCenter() 21 | ); 22 | } 23 | 24 | private static boolean isAlreadyCreated(CqlSession session, String ksName, String tableName) { 25 | return session.getMetadata().getKeyspace(ksName).isPresent() && 26 | session.getMetadata().getKeyspace(ksName).get().getTable(tableName).isPresent() && 27 | session.checkSchemaAgreement(); 28 | } 29 | 30 | public static void createSchema(CqlSession session, DemoServiceConfiguration config) { 31 | // For the purpose of this demo, we check whether the tables already exist. 32 | // On a real world application, we should detach schema creation from the service deployment 33 | 34 | logger.info("Creating demo schema"); 35 | 36 | session.execute(createKeyspace(config, "ks1")); 37 | session.execute("CREATE TABLE IF NOT EXISTS ks1.table1 (id int PRIMARY KEY)"); 38 | session.execute(SimpleStatement.newInstance("INSERT INTO ks1.table1 (id) VALUES (?)", 1)); 39 | 40 | logger.info("Creating shopping schema"); 41 | 42 | session.execute(createKeyspace(config, "shopping")); 43 | session.execute("CREATE TABLE IF NOT EXISTS shopping.carts (" + 44 | " username text," + 45 | " item_id int," + 46 | " date_added timestamp," + 47 | " item_name text," + 48 | " PRIMARY KEY (username, item_id, date_added))"); 49 | } 50 | 51 | public static void waitSchema(CqlSession session) throws InterruptedException 52 | { 53 | int counter = 0; 54 | 55 | while (!isAlreadyCreated(session, "shopping", "carts") && !isAlreadyCreated(session, "ks1", "table1")) { 56 | if (counter++ >= 20) { 57 | throw new IllegalStateException("Schema was not found after waiting"); 58 | } 59 | 60 | logger.info("Waiting for schema to be created..."); 61 | Thread.sleep(5000); 62 | } 63 | 64 | logger.info("Schema is present"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/db/ShoppingDAO.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.db; 2 | 3 | import com.datastax.demo.api.CartItem; 4 | import com.datastax.oss.driver.api.core.ConsistencyLevel; 5 | import com.datastax.oss.driver.api.core.CqlSession; 6 | import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; 7 | import com.datastax.oss.driver.api.core.cql.PreparedStatement; 8 | import com.datastax.oss.driver.api.core.cql.ResultSet; 9 | import com.datastax.oss.driver.api.core.cql.Row; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class ShoppingDAO 15 | { 16 | private final CqlSession session; 17 | 18 | private final PreparedStatement selectCart; 19 | private final PreparedStatement insertItemIntoCart; 20 | 21 | private static final ConsistencyLevel READ_CONSISTENCY_LEVEL = DefaultConsistencyLevel.LOCAL_QUORUM; 22 | private static final ConsistencyLevel WRITE_CONSISTENCY_LEVEL = DefaultConsistencyLevel.LOCAL_QUORUM; 23 | 24 | public ShoppingDAO(CqlSession session) { 25 | this.session = session; 26 | 27 | this.selectCart = session.prepare("SELECT * FROM shopping.carts WHERE username = ?"); 28 | this.insertItemIntoCart = session.prepare("INSERT INTO shopping.carts (username, item_id, date_added, item_name) VALUES (?, ?, toTimestamp(now()), ?)"); 29 | } 30 | 31 | public Iterable getCart(String username) { 32 | ResultSet rs = session.execute(selectCart 33 | .bind(username) 34 | .setConsistencyLevel(READ_CONSISTENCY_LEVEL)); 35 | List items = new ArrayList<>(rs.getAvailableWithoutFetching()); 36 | for (Row row : rs) { 37 | items.add(new CartItem(row)); 38 | } 39 | return items; 40 | } 41 | 42 | public void addItem(String username, CartItem item) { 43 | session.execute(insertItemIntoCart 44 | .bind(username, item.getId(), item.getName()) 45 | .setConsistencyLevel(WRITE_CONSISTENCY_LEVEL)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/resources/DemoResource1.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.resources; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.POST; 5 | import javax.ws.rs.Path; 6 | import javax.ws.rs.PathParam; 7 | import javax.ws.rs.Produces; 8 | import javax.ws.rs.core.MediaType; 9 | import javax.ws.rs.core.Response; 10 | 11 | import io.dropwizard.jersey.errors.ErrorMessage; 12 | import io.dropwizard.jersey.params.IntParam; 13 | 14 | import com.datastax.demo.api.DemoEntity; 15 | import com.datastax.demo.db.DemoDAO; 16 | 17 | @Path("/demo") 18 | @Produces(MediaType.APPLICATION_JSON) 19 | public class DemoResource1 20 | { 21 | 22 | private final DemoDAO demoDAO; 23 | 24 | private Response errorMessage(Response.Status status, String message) 25 | { 26 | return Response.status(status) 27 | .entity(new ErrorMessage(status.getStatusCode(), message)) 28 | .build(); 29 | } 30 | 31 | public DemoResource1(DemoDAO demoDAO) 32 | { 33 | this.demoDAO = demoDAO; 34 | } 35 | 36 | @POST 37 | public DemoEntity create(DemoEntity entity) 38 | { 39 | return demoDAO.create(new DemoEntity(entity.getId(), entity.getContent())); 40 | } 41 | 42 | @GET 43 | @Path("{id}") 44 | public Response get(@PathParam("id") IntParam id ) 45 | { 46 | DemoEntity entity = demoDAO.get(id.get()); 47 | if (entity == null) 48 | { 49 | return errorMessage(Response.Status.NOT_FOUND, "Unable to find demo entity for ID"); 50 | } 51 | return Response.ok(entity).build(); 52 | } 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/resources/HealthCheckResource.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.resources; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | @Path("/status") 9 | public class HealthCheckResource { 10 | 11 | @GET 12 | @Produces(MediaType.TEXT_PLAIN) 13 | public String getStatus() { 14 | return "OK"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/resources/HomeResource.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.resources; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | import java.net.InetAddress; 8 | import java.net.UnknownHostException; 9 | import java.time.Instant; 10 | import java.util.stream.Collectors; 11 | 12 | import io.dropwizard.jersey.DropwizardResourceConfig; 13 | 14 | import com.datastax.oss.driver.api.core.CqlSession; 15 | 16 | @Path("/") 17 | public class HomeResource 18 | { 19 | private final DropwizardResourceConfig resourceConfig; 20 | private final CqlSession session; 21 | 22 | public HomeResource(DropwizardResourceConfig resourceConfig, CqlSession session) { 23 | this.resourceConfig = resourceConfig; 24 | this.session = session; 25 | } 26 | 27 | @GET 28 | @Produces(MediaType.TEXT_PLAIN) 29 | public String get() throws UnknownHostException 30 | { 31 | final String paths = resourceConfig.getEndpointsInfo(); 32 | final String privateIP = InetAddress.getLocalHost().getHostAddress(); 33 | 34 | final StringBuilder builder = new StringBuilder(256); 35 | builder 36 | .append("Welcome to the demo! \n\n") 37 | .append(Instant.now()) 38 | .append("\n\n") 39 | .append("HTTP responded from: ") 40 | .append(privateIP) 41 | .append("\n\nConnected to: ") 42 | .append(session.getMetadata().getNodes().values().stream() 43 | .filter(n -> n.getOpenConnections() > 0) 44 | .map(n -> n.getEndPoint().toString()) 45 | .collect(Collectors.joining(", "))) 46 | .append("\n\n") 47 | .append(paths); 48 | 49 | return builder.toString(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /service/src/main/java/com/datastax/demo/resources/ShoppingResource.java: -------------------------------------------------------------------------------- 1 | package com.datastax.demo.resources; 2 | 3 | import com.datastax.demo.api.CartItem; 4 | import com.datastax.demo.db.ShoppingDAO; 5 | 6 | import javax.ws.rs.*; 7 | import javax.ws.rs.core.MediaType; 8 | 9 | @Path("/cart") 10 | @Produces(MediaType.APPLICATION_JSON) 11 | public class ShoppingResource { 12 | private final ShoppingDAO shoppingDAO; 13 | 14 | public ShoppingResource(ShoppingDAO shoppingDAO) { 15 | this.shoppingDAO = shoppingDAO; 16 | } 17 | 18 | @POST 19 | @Path("{username}/add") 20 | public void addItemToCart(@PathParam("username") String username, CartItem item) { 21 | shoppingDAO.addItem(username, item); 22 | } 23 | 24 | @GET 25 | @Path("{username}") 26 | public Iterable getCart(@PathParam("username") String username) { 27 | return shoppingDAO.getCart(username); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | 3 | DemoService 4 | 5 | ================================================================================ 6 | 7 | -------------------------------------------------------------------------------- /terraform/elb.tf: -------------------------------------------------------------------------------- 1 | # Region 1 ELB 2 | 3 | resource aws_lb lb_r1 { 4 | provider = aws.region1 5 | load_balancer_type = "application" 6 | subnets = [aws_subnet.r1_az1.id, aws_subnet.r1_az2.id, aws_subnet.r1_az3.id] 7 | security_groups = [aws_security_group.sg_default_r1.id, aws_security_group.sg_elb_r1.id] 8 | enable_cross_zone_load_balancing = true 9 | internal = false 10 | } 11 | 12 | resource aws_lb_target_group lb_tg_r1 { 13 | provider = aws.region1 14 | port = 80 15 | protocol = "HTTP" 16 | vpc_id = aws_vpc.r1.id 17 | target_type = "instance" 18 | 19 | health_check { 20 | interval = 5 21 | port = 8080 22 | path = "/status" 23 | healthy_threshold = 2 24 | unhealthy_threshold = 2 25 | timeout = 2 26 | } 27 | } 28 | 29 | resource aws_lb_listener lb_r1_listener { 30 | provider = aws.region1 31 | load_balancer_arn = aws_lb.lb_r1.arn 32 | port = 80 33 | protocol = "HTTP" 34 | 35 | default_action { 36 | type = "forward" 37 | target_group_arn = aws_lb_target_group.lb_tg_r1.arn 38 | } 39 | } 40 | 41 | resource aws_lb_target_group_attachment lb_tga_r1_i1 { 42 | provider = aws.region1 43 | target_group_arn = aws_lb_target_group.lb_tg_r1.arn 44 | target_id = aws_instance.i_web_r1_i1.id 45 | port = 8080 46 | } 47 | 48 | resource aws_lb_target_group_attachment lb_tga_r1_i2 { 49 | provider = aws.region1 50 | target_group_arn = aws_lb_target_group.lb_tg_r1.arn 51 | target_id = aws_instance.i_web_r1_i2.id 52 | port = 8080 53 | } 54 | 55 | resource aws_lb_target_group_attachment lb_tga_r1_i3 { 56 | provider = aws.region1 57 | target_group_arn = aws_lb_target_group.lb_tg_r1.arn 58 | target_id = aws_instance.i_web_r1_i3.id 59 | port = 8080 60 | } 61 | 62 | # Region 2 ELB 63 | 64 | resource aws_lb lb_r2 { 65 | provider = aws.region2 66 | load_balancer_type = "application" 67 | subnets = [aws_subnet.r2_az1.id, aws_subnet.r2_az2.id, aws_subnet.r2_az3.id] 68 | security_groups = [aws_security_group.sg_default_r2.id, aws_security_group.sg_elb_r2.id] 69 | enable_cross_zone_load_balancing = true 70 | internal = false 71 | } 72 | 73 | resource aws_lb_target_group lb_tg_r2 { 74 | provider = aws.region2 75 | port = 80 76 | protocol = "HTTP" 77 | vpc_id = aws_vpc.r2.id 78 | target_type = "instance" 79 | 80 | health_check { 81 | interval = 5 82 | port = 8080 83 | path = "/status" 84 | healthy_threshold = 2 85 | unhealthy_threshold = 2 86 | timeout = 2 87 | } 88 | } 89 | 90 | resource aws_lb_listener lb_r2_listener { 91 | provider = aws.region2 92 | load_balancer_arn = aws_lb.lb_r2.arn 93 | port = 80 94 | protocol = "HTTP" 95 | 96 | default_action { 97 | type = "forward" 98 | target_group_arn = aws_lb_target_group.lb_tg_r2.arn 99 | } 100 | } 101 | 102 | resource aws_lb_target_group_attachment lb_tga_r2_i1 { 103 | provider = aws.region2 104 | target_group_arn = aws_lb_target_group.lb_tg_r2.arn 105 | target_id = aws_instance.i_web_r2_i1.id 106 | port = 8080 107 | } 108 | 109 | resource aws_lb_target_group_attachment lb_tga_r2_i2 { 110 | provider = aws.region2 111 | target_group_arn = aws_lb_target_group.lb_tg_r2.arn 112 | target_id = aws_instance.i_web_r2_i2.id 113 | port = 8080 114 | } 115 | 116 | resource aws_lb_target_group_attachment lb_tga_r2_i3 { 117 | provider = aws.region2 118 | target_group_arn = aws_lb_target_group.lb_tg_r2.arn 119 | target_id = aws_instance.i_web_r2_i3.id 120 | port = 8080 121 | } -------------------------------------------------------------------------------- /terraform/global_accelerator.tf: -------------------------------------------------------------------------------- 1 | resource aws_globalaccelerator_accelerator demo_acc { 2 | name = "demo-accelerator-${random_id.id1.hex}" 3 | ip_address_type = "IPV4" 4 | enabled = true 5 | } 6 | 7 | resource aws_globalaccelerator_listener demo_acc_listener { 8 | accelerator_arn = aws_globalaccelerator_accelerator.demo_acc.id 9 | protocol = "TCP" 10 | 11 | port_range { 12 | from_port = 80 13 | to_port = 80 14 | } 15 | } 16 | 17 | resource aws_globalaccelerator_endpoint_group demo_acc_eg_r1 { 18 | provider = aws.region1 19 | listener_arn = aws_globalaccelerator_listener.demo_acc_listener.id 20 | health_check_interval_seconds = 10 21 | threshold_count = 2 22 | health_check_path = "/status" 23 | health_check_port = 80 24 | health_check_protocol = "HTTP" 25 | 26 | endpoint_configuration { 27 | endpoint_id = aws_lb.lb_r1.arn 28 | weight = 100 29 | } 30 | } 31 | 32 | resource aws_globalaccelerator_endpoint_group demo_acc_eg_r2 { 33 | provider = aws.region2 34 | listener_arn = aws_globalaccelerator_listener.demo_acc_listener.id 35 | health_check_interval_seconds = 10 36 | threshold_count = 2 37 | health_check_path = "/status" 38 | health_check_port = 80 39 | health_check_protocol = "HTTP" 40 | 41 | endpoint_configuration { 42 | endpoint_id = aws_lb.lb_r2.arn 43 | weight = 100 44 | } 45 | } -------------------------------------------------------------------------------- /terraform/instances_bastion.tf: -------------------------------------------------------------------------------- 1 | ################################################## 2 | ### Bastion in Region1 with connectivity 3 | ### to the rest of the nodes in each region 4 | ################################################## 5 | 6 | resource aws_instance bastion_r1 { 7 | provider = aws.region1 8 | ami = data.aws_ami.cassandra_r1.id 9 | instance_type = "t2.small" 10 | subnet_id = aws_subnet.r1_az1.id 11 | associate_public_ip_address = true 12 | key_name = aws_key_pair.key_r1.key_name 13 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id, aws_security_group.sg_bastion_r1.id] 14 | tags = { 15 | Name = "Demo - Bastion", 16 | Purpose = "Demo failover" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /terraform/instances_cassandra_nodes.tf: -------------------------------------------------------------------------------- 1 | ################################## 2 | ### Rest of the Cassandra nodes 3 | ################################## 4 | 5 | resource aws_instance i_cassandra_r1_i2 { 6 | provider = aws.region1 7 | ami = data.aws_ami.cassandra_r1.id 8 | instance_type = "m5.2xlarge" 9 | subnet_id = aws_subnet.r1_az2.id 10 | key_name = aws_key_pair.key_r1.key_name 11 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id] 12 | tags = { 13 | Name = "Demo - Cassandra Node", 14 | Purpose = "Demo failover" 15 | } 16 | 17 | root_block_device { 18 | volume_type = "gp2" 19 | volume_size = 50 20 | delete_on_termination = true 21 | } 22 | 23 | provisioner remote-exec { 24 | connection { 25 | bastion_host = aws_instance.bastion_r1.public_ip 26 | host = self.private_ip 27 | type = "ssh" 28 | user = "ubuntu" 29 | private_key = tls_private_key.dev.private_key_pem 30 | } 31 | 32 | inline = [ 33 | "echo \"${self.private_ip} ${self.private_dns}\" | sudo tee -a /etc/hosts", 34 | "./generate_config.sh ${aws_instance.i_cassandra_r1_i1.private_ip},${aws_instance.i_cassandra_r2_i1.private_ip}", 35 | "nohup cassandra/bin/cassandra -p pid.txt &", 36 | "./wait_for_cassandra.sh" 37 | ] 38 | } 39 | } 40 | 41 | resource aws_instance i_cassandra_r1_i3 { 42 | provider = aws.region1 43 | ami = data.aws_ami.cassandra_r1.id 44 | instance_type = "m5.2xlarge" 45 | subnet_id = aws_subnet.r1_az3.id 46 | key_name = aws_key_pair.key_r1.key_name 47 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id] 48 | tags = { 49 | Name = "Demo - Cassandra Node", 50 | Purpose = "Demo failover" 51 | } 52 | 53 | # Start after r1_i2 54 | depends_on = [ aws_instance.i_cassandra_r1_i2 ] 55 | 56 | root_block_device { 57 | volume_type = "gp2" 58 | volume_size = 50 59 | delete_on_termination = true 60 | } 61 | 62 | provisioner remote-exec { 63 | connection { 64 | bastion_host = aws_instance.bastion_r1.public_ip 65 | host = self.private_ip 66 | type = "ssh" 67 | user = "ubuntu" 68 | private_key = tls_private_key.dev.private_key_pem 69 | } 70 | 71 | inline = [ 72 | "echo \"${self.private_ip} ${self.private_dns}\" | sudo tee -a /etc/hosts", 73 | "./generate_config.sh ${aws_instance.i_cassandra_r1_i1.private_ip},${aws_instance.i_cassandra_r2_i1.private_ip}", 74 | "nohup cassandra/bin/cassandra -p pid.txt &", 75 | "./wait_for_cassandra.sh" 76 | ] 77 | } 78 | } 79 | 80 | resource aws_instance i_cassandra_r2_i2 { 81 | provider = aws.region2 82 | ami = data.aws_ami.cassandra_r2.id 83 | instance_type = "m5.2xlarge" 84 | subnet_id = aws_subnet.r2_az2.id 85 | key_name = aws_key_pair.key_r2.key_name 86 | vpc_security_group_ids = [aws_security_group.sg_default_r2.id] 87 | tags = { 88 | Name = "Demo - Cassandra Node", 89 | Purpose = "Demo failover" 90 | } 91 | 92 | # Start after r1_i3 93 | depends_on = [ aws_instance.i_cassandra_r1_i3 ] 94 | 95 | root_block_device { 96 | volume_type = "gp2" 97 | volume_size = 50 98 | delete_on_termination = true 99 | } 100 | 101 | provisioner remote-exec { 102 | connection { 103 | bastion_host = aws_instance.bastion_r1.public_ip 104 | host = self.private_ip 105 | type = "ssh" 106 | user = "ubuntu" 107 | private_key = tls_private_key.dev.private_key_pem 108 | } 109 | 110 | inline = [ 111 | "echo \"${self.private_ip} ${self.private_dns}\" | sudo tee -a /etc/hosts", 112 | "./generate_config.sh ${aws_instance.i_cassandra_r1_i1.private_ip},${aws_instance.i_cassandra_r2_i1.private_ip}", 113 | "nohup cassandra/bin/cassandra -p pid.txt &", 114 | "./wait_for_cassandra.sh" 115 | ] 116 | } 117 | } 118 | 119 | resource aws_instance i_cassandra_r2_i3 { 120 | provider = aws.region2 121 | ami = data.aws_ami.cassandra_r2.id 122 | instance_type = "m5.2xlarge" 123 | subnet_id = aws_subnet.r2_az3.id 124 | key_name = aws_key_pair.key_r2.key_name 125 | vpc_security_group_ids = [aws_security_group.sg_r2_az3.id] 126 | tags = { 127 | Name = "Demo - Cassandra Node", 128 | Purpose = "Demo failover" 129 | } 130 | 131 | # Start after r2_i2 132 | depends_on = [ aws_instance.i_cassandra_r2_i2 ] 133 | 134 | root_block_device { 135 | volume_type = "gp2" 136 | volume_size = 50 137 | delete_on_termination = true 138 | } 139 | 140 | provisioner remote-exec { 141 | connection { 142 | bastion_host = aws_instance.bastion_r1.public_ip 143 | host = self.private_ip 144 | type = "ssh" 145 | user = "ubuntu" 146 | private_key = tls_private_key.dev.private_key_pem 147 | } 148 | 149 | inline = [ 150 | "echo \"${self.private_ip} ${self.private_dns}\" | sudo tee -a /etc/hosts", 151 | "./generate_config.sh ${aws_instance.i_cassandra_r1_i1.private_ip},${aws_instance.i_cassandra_r2_i1.private_ip}", 152 | "nohup cassandra/bin/cassandra -p pid.txt &", 153 | "./wait_for_cassandra.sh" 154 | ] 155 | } 156 | } -------------------------------------------------------------------------------- /terraform/instances_cassandra_seeds.tf: -------------------------------------------------------------------------------- 1 | ################################## 2 | ### Cassandra seed nodes 3 | ################################## 4 | 5 | resource aws_instance i_cassandra_r1_i1 { 6 | provider = aws.region1 7 | ami = data.aws_ami.cassandra_r1.id 8 | instance_type = "m5.2xlarge" 9 | subnet_id = aws_subnet.r1_az1.id 10 | key_name = aws_key_pair.key_r1.key_name 11 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id] 12 | tags = { 13 | Name = "Demo - Cassandra Node", 14 | Purpose = "Demo failover" 15 | } 16 | 17 | root_block_device { 18 | volume_type = "gp2" 19 | volume_size = 50 20 | delete_on_termination = true 21 | } 22 | 23 | provisioner remote-exec { 24 | connection { 25 | bastion_host = aws_instance.bastion_r1.public_ip 26 | host = self.private_ip 27 | type = "ssh" 28 | user = "ubuntu" 29 | private_key = tls_private_key.dev.private_key_pem 30 | } 31 | 32 | inline = [ 33 | "echo \"${self.private_ip} ${self.private_dns}\" | sudo tee -a /etc/hosts", 34 | "./generate_config.sh ${aws_instance.i_cassandra_r1_i1.private_ip}", 35 | "nohup cassandra/bin/cassandra -p pid.txt &", 36 | "./wait_for_cassandra.sh" 37 | ] 38 | } 39 | } 40 | 41 | resource aws_instance i_cassandra_r2_i1 { 42 | provider = aws.region2 43 | ami = data.aws_ami.cassandra_r2.id 44 | instance_type = "m5.2xlarge" 45 | subnet_id = aws_subnet.r2_az1.id 46 | key_name = aws_key_pair.key_r2.key_name 47 | vpc_security_group_ids = [aws_security_group.sg_default_r2.id] 48 | tags = { 49 | Name = "Demo - Cassandra Node", 50 | Purpose = "Demo failover" 51 | } 52 | 53 | root_block_device { 54 | volume_type = "gp2" 55 | volume_size = 50 56 | delete_on_termination = true 57 | } 58 | 59 | provisioner remote-exec { 60 | connection { 61 | bastion_host = aws_instance.bastion_r1.public_ip 62 | host = self.private_ip 63 | type = "ssh" 64 | user = "ubuntu" 65 | private_key = tls_private_key.dev.private_key_pem 66 | } 67 | 68 | inline = [ 69 | "echo \"${self.private_ip} ${self.private_dns}\" | sudo tee -a /etc/hosts", 70 | "./generate_config.sh ${aws_instance.i_cassandra_r1_i1.private_ip},${aws_instance.i_cassandra_r2_i1.private_ip}", 71 | "nohup cassandra/bin/cassandra -p pid.txt &", 72 | "./wait_for_cassandra.sh" 73 | ] 74 | } 75 | } -------------------------------------------------------------------------------- /terraform/instances_client.tf: -------------------------------------------------------------------------------- 1 | ################################################## 2 | ### Load clients in Regions 1 and 2 3 | ################################################## 4 | 5 | resource aws_instance client_r1 { 6 | provider = aws.region1 7 | ami = data.aws_ami.ami_client_r1.id 8 | instance_type = "m5.large" 9 | subnet_id = aws_subnet.r1_az1.id 10 | associate_public_ip_address = true 11 | key_name = aws_key_pair.key_r1.key_name 12 | vpc_security_group_ids = [aws_security_group.sg_client_r1.id] 13 | tags = { 14 | Name = "Demo - Client", 15 | Purpose = "Demo failover" 16 | } 17 | 18 | provisioner remote-exec { 19 | connection { 20 | host = self.public_ip 21 | type = "ssh" 22 | user = "ubuntu" 23 | private_key = tls_private_key.dev.private_key_pem 24 | } 25 | 26 | inline = [ 27 | "nohup ./start_client.sh ${aws_globalaccelerator_accelerator.demo_acc.ip_sets[0]["ip_addresses"][0]} &", 28 | "sleep 5s" 29 | ] 30 | } 31 | } 32 | 33 | resource aws_instance client_r2 { 34 | provider = aws.region2 35 | ami = data.aws_ami.ami_client_r2.id 36 | instance_type = "m5.large" 37 | subnet_id = aws_subnet.r2_az1.id 38 | associate_public_ip_address = true 39 | key_name = aws_key_pair.key_r2.key_name 40 | vpc_security_group_ids = [aws_security_group.sg_client_r2.id] 41 | tags = { 42 | Name = "Demo - Client", 43 | Purpose = "Demo failover" 44 | } 45 | 46 | provisioner remote-exec { 47 | connection { 48 | host = self.public_ip 49 | type = "ssh" 50 | user = "ubuntu" 51 | private_key = tls_private_key.dev.private_key_pem 52 | } 53 | 54 | inline = [ 55 | "nohup ./start_client.sh ${aws_globalaccelerator_accelerator.demo_acc.ip_sets[0]["ip_addresses"][1]} &", 56 | "sleep 5s" 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /terraform/instances_web.tf: -------------------------------------------------------------------------------- 1 | resource aws_instance i_web_r1_i1 { 2 | provider = aws.region1 3 | ami = data.aws_ami.ami_web_r1.id 4 | instance_type = "m5.large" 5 | subnet_id = aws_subnet.r1_az1.id 6 | key_name = aws_key_pair.key_r1.key_name 7 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id] 8 | tags = { 9 | Name = "Demo - Web", 10 | Purpose = "Demo failover" 11 | } 12 | 13 | # Wait until the Cassandra/DSE nodes are available 14 | depends_on = [ aws_instance.i_cassandra_r1_i1 ] 15 | 16 | provisioner remote-exec { 17 | connection { 18 | bastion_host = aws_instance.bastion_r1.public_ip 19 | host = self.private_ip 20 | type = "ssh" 21 | user = "ubuntu" 22 | private_key = tls_private_key.dev.private_key_pem 23 | } 24 | 25 | inline = [ 26 | "nohup ./start_web.sh ${aws_instance.i_cassandra_r1_i1.private_ip} ${aws_instance.i_cassandra_r1_i1.availability_zone} ${aws_instance.i_cassandra_r2_i1.availability_zone} true &", 27 | "sleep 5s" 28 | ] 29 | } 30 | } 31 | 32 | resource aws_instance i_web_r1_i2 { 33 | provider = aws.region1 34 | ami = data.aws_ami.ami_web_r1.id 35 | instance_type = "m5.large" 36 | subnet_id = aws_subnet.r1_az2.id 37 | key_name = aws_key_pair.key_r1.key_name 38 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id] 39 | tags = { 40 | Name = "Demo - Web", 41 | Purpose = "Demo failover" 42 | } 43 | 44 | # Wait until the Cassandra/DSE nodes are available 45 | depends_on = [ aws_instance.i_cassandra_r1_i1 ] 46 | 47 | provisioner remote-exec { 48 | connection { 49 | bastion_host = aws_instance.bastion_r1.public_ip 50 | host = self.private_ip 51 | type = "ssh" 52 | user = "ubuntu" 53 | private_key = tls_private_key.dev.private_key_pem 54 | } 55 | 56 | inline = [ 57 | "nohup ./start_web.sh ${aws_instance.i_cassandra_r1_i1.private_ip} ${aws_instance.i_cassandra_r1_i1.availability_zone} ${aws_instance.i_cassandra_r2_i1.availability_zone} &", 58 | "sleep 5s" 59 | ] 60 | } 61 | } 62 | 63 | resource aws_instance i_web_r1_i3 { 64 | provider = aws.region1 65 | ami = data.aws_ami.ami_web_r1.id 66 | instance_type = "m5.large" 67 | subnet_id = aws_subnet.r1_az3.id 68 | key_name = aws_key_pair.key_r1.key_name 69 | vpc_security_group_ids = [aws_security_group.sg_default_r1.id] 70 | tags = { 71 | Name = "Demo - Web", 72 | Purpose = "Demo failover" 73 | } 74 | 75 | # Wait until the Cassandra/DSE nodes are available 76 | depends_on = [ aws_instance.i_cassandra_r1_i1 ] 77 | 78 | provisioner remote-exec { 79 | connection { 80 | bastion_host = aws_instance.bastion_r1.public_ip 81 | host = self.private_ip 82 | type = "ssh" 83 | user = "ubuntu" 84 | private_key = tls_private_key.dev.private_key_pem 85 | } 86 | 87 | inline = [ 88 | "nohup ./start_web.sh ${aws_instance.i_cassandra_r1_i1.private_ip} ${aws_instance.i_cassandra_r1_i1.availability_zone} ${aws_instance.i_cassandra_r2_i1.availability_zone} &", 89 | "sleep 5s" 90 | ] 91 | } 92 | } 93 | 94 | resource aws_instance i_web_r2_i1 { 95 | provider = aws.region2 96 | ami = data.aws_ami.ami_web_r2.id 97 | instance_type = "m5.large" 98 | subnet_id = aws_subnet.r2_az1.id 99 | key_name = aws_key_pair.key_r2.key_name 100 | vpc_security_group_ids = [aws_security_group.sg_default_r2.id] 101 | tags = { 102 | Name = "Demo - Web", 103 | Purpose = "Demo failover" 104 | } 105 | 106 | # Wait until the Cassandra/DSE nodes are available 107 | depends_on = [ aws_instance.i_cassandra_r1_i1 ] 108 | 109 | provisioner remote-exec { 110 | connection { 111 | bastion_host = aws_instance.bastion_r1.public_ip 112 | host = self.private_ip 113 | type = "ssh" 114 | user = "ubuntu" 115 | private_key = tls_private_key.dev.private_key_pem 116 | } 117 | 118 | inline = [ 119 | "nohup ./start_web.sh ${aws_instance.i_cassandra_r2_i1.private_ip} ${aws_instance.i_cassandra_r2_i1.availability_zone} ${aws_instance.i_cassandra_r1_i1.availability_zone} &", 120 | "sleep 5s" 121 | ] 122 | } 123 | } 124 | 125 | resource aws_instance i_web_r2_i2 { 126 | provider = aws.region2 127 | ami = data.aws_ami.ami_web_r2.id 128 | instance_type = "m5.large" 129 | subnet_id = aws_subnet.r2_az2.id 130 | key_name = aws_key_pair.key_r2.key_name 131 | vpc_security_group_ids = [aws_security_group.sg_default_r2.id] 132 | tags = { 133 | Name = "Demo - Web", 134 | Purpose = "Demo failover" 135 | } 136 | 137 | # Wait until the Cassandra/DSE nodes are available 138 | depends_on = [ aws_instance.i_cassandra_r1_i1 ] 139 | 140 | provisioner remote-exec { 141 | connection { 142 | bastion_host = aws_instance.bastion_r1.public_ip 143 | host = self.private_ip 144 | type = "ssh" 145 | user = "ubuntu" 146 | private_key = tls_private_key.dev.private_key_pem 147 | } 148 | 149 | inline = [ 150 | "nohup ./start_web.sh ${aws_instance.i_cassandra_r2_i1.private_ip} ${aws_instance.i_cassandra_r2_i1.availability_zone} ${aws_instance.i_cassandra_r1_i1.availability_zone} &", 151 | "sleep 5s" 152 | ] 153 | } 154 | } 155 | 156 | resource aws_instance i_web_r2_i3 { 157 | provider = aws.region2 158 | ami = data.aws_ami.ami_web_r2.id 159 | instance_type = "m5.large" 160 | subnet_id = aws_subnet.r2_az3.id 161 | key_name = aws_key_pair.key_r2.key_name 162 | vpc_security_group_ids = [aws_security_group.sg_r2_az3.id] 163 | tags = { 164 | Name = "Demo - Web", 165 | Purpose = "Demo failover" 166 | } 167 | 168 | # Wait until the Cassandra/DSE nodes are available 169 | depends_on = [ aws_instance.i_cassandra_r1_i1 ] 170 | 171 | provisioner remote-exec { 172 | connection { 173 | bastion_host = aws_instance.bastion_r1.public_ip 174 | host = self.private_ip 175 | type = "ssh" 176 | user = "ubuntu" 177 | private_key = tls_private_key.dev.private_key_pem 178 | } 179 | 180 | inline = [ 181 | "nohup ./start_web.sh ${aws_instance.i_cassandra_r2_i1.private_ip} ${aws_instance.i_cassandra_r2_i1.availability_zone} ${aws_instance.i_cassandra_r1_i1.availability_zone} &", 182 | "sleep 5s" 183 | ] 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | provider aws { 2 | alias = "region1" 3 | region = var.region1 4 | version = "~> 2.16" 5 | } 6 | 7 | provider aws { 8 | alias = "region2" 9 | region = var.region2 10 | version = "~> 2.16" 11 | } 12 | 13 | provider aws { 14 | # Add a default one 15 | region = var.region1 16 | } 17 | 18 | provider tls { 19 | version = "~> 2.0" 20 | } 21 | 22 | data aws_availability_zones r1 { 23 | provider = aws.region1 24 | } 25 | 26 | data aws_availability_zones r2 { 27 | provider = aws.region2 28 | } 29 | 30 | data aws_caller_identity current { 31 | provider = aws.region1 32 | } 33 | 34 | data aws_ami cassandra_r1 { 35 | provider = aws.region1 36 | most_recent = true 37 | owners = ["self"] 38 | filter { 39 | name = "name" 40 | values = ["cassandra-image*"] 41 | } 42 | } 43 | 44 | data aws_ami cassandra_r2 { 45 | provider = aws.region2 46 | most_recent = true 47 | owners = ["self"] 48 | filter { 49 | name = "name" 50 | values = ["cassandra-image*"] 51 | } 52 | } 53 | 54 | data aws_ami ami_web_r1 { 55 | provider = aws.region1 56 | most_recent = true 57 | owners = ["self"] 58 | filter { 59 | name = "name" 60 | values = ["demo-web-image*"] 61 | } 62 | } 63 | 64 | data aws_ami ami_web_r2 { 65 | provider = aws.region2 66 | most_recent = true 67 | owners = ["self"] 68 | filter { 69 | name = "name" 70 | values = ["demo-web-image*"] 71 | } 72 | } 73 | 74 | data aws_ami ami_client_r1 { 75 | provider = aws.region1 76 | most_recent = true 77 | owners = ["self"] 78 | filter { 79 | name = "name" 80 | values = ["demo-client-image*"] 81 | } 82 | } 83 | 84 | data aws_ami ami_client_r2 { 85 | provider = aws.region2 86 | most_recent = true 87 | owners = ["self"] 88 | filter { 89 | name = "name" 90 | values = ["demo-client-image*"] 91 | } 92 | } 93 | 94 | resource tls_private_key dev { 95 | algorithm = "RSA" 96 | rsa_bits = 4096 97 | } 98 | 99 | resource aws_key_pair key_r1 { 100 | provider = aws.region1 101 | key_name = "dev_key_r1" 102 | public_key = tls_private_key.dev.public_key_openssh 103 | } 104 | 105 | resource aws_key_pair key_r2 { 106 | provider = aws.region2 107 | key_name = "dev_key_r2" 108 | public_key = tls_private_key.dev.public_key_openssh 109 | } 110 | 111 | resource random_id id1 { 112 | byte_length = 8 113 | } 114 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "load_client_url_region1" { 2 | value = "http://${aws_instance.client_r1.public_ip}:8089" 3 | } 4 | 5 | output "load_client_url_region2" { 6 | value = "http://${aws_instance.client_r2.public_ip}:8089" 7 | } 8 | 9 | output "accelerator_public_ips" { 10 | value = aws_globalaccelerator_accelerator.demo_acc.ip_sets[0]["ip_addresses"] 11 | } 12 | 13 | output "accelerator_url1" { 14 | value = "http://${aws_globalaccelerator_accelerator.demo_acc.ip_sets[0]["ip_addresses"][0]}" 15 | } 16 | 17 | output "private_key" { 18 | value = tls_private_key.dev.private_key_pem 19 | } 20 | 21 | output "bastion_r1_ip" { 22 | value = "${aws_instance.bastion_r1.public_ip}" 23 | } -------------------------------------------------------------------------------- /terraform/security_groups.tf: -------------------------------------------------------------------------------- 1 | resource aws_security_group sg_default_r1 { 2 | provider = aws.region1 3 | name = "sg_demo_default_r1" 4 | description = "Used in the terraform demo" 5 | vpc_id = aws_vpc.r1.id 6 | 7 | # SSH, HTTP and more from the VPCs 8 | ingress { 9 | from_port = 0 10 | to_port = 65535 11 | protocol = "tcp" 12 | cidr_blocks = ["10.0.0.0/8"] 13 | } 14 | 15 | # outbound internet access 16 | egress { 17 | from_port = 0 18 | to_port = 0 19 | protocol = "-1" 20 | cidr_blocks = ["0.0.0.0/0"] 21 | } 22 | } 23 | 24 | resource aws_security_group sg_default_r2 { 25 | provider = aws.region2 26 | name = "sg_demo_default_r2" 27 | description = "Used in the terraform demo region2" 28 | vpc_id = aws_vpc.r2.id 29 | 30 | # outbound internet access 31 | egress { 32 | from_port = 0 33 | to_port = 0 34 | protocol = "-1" 35 | cidr_blocks = ["0.0.0.0/0"] 36 | } 37 | } 38 | 39 | resource aws_security_group sg_r2_az3 { 40 | provider = aws.region2 41 | name = "sg_demo_r2_az3" 42 | description = "Used in the terraform demo" 43 | vpc_id = aws_vpc.r2.id 44 | 45 | egress { 46 | from_port = 0 47 | to_port = 0 48 | protocol = "-1" 49 | cidr_blocks = ["0.0.0.0/0"] 50 | } 51 | } 52 | 53 | resource aws_security_group sg_bastion_r1 { 54 | provider = aws.region1 55 | name = "sg_demo_bastion_r1" 56 | description = "Used in the terraform demo" 57 | vpc_id = aws_vpc.r1.id 58 | 59 | # SSH from the internet 60 | ingress { 61 | from_port = 22 62 | to_port = 22 63 | protocol = "tcp" 64 | cidr_blocks = ["0.0.0.0/0"] 65 | } 66 | } 67 | 68 | resource aws_security_group sg_client_r1 { 69 | provider = aws.region1 70 | name = "sg_demo_client_r1" 71 | description = "Used in the terraform demo" 72 | vpc_id = aws_vpc.r1.id 73 | 74 | # SSH from the internet 75 | ingress { 76 | from_port = 22 77 | to_port = 22 78 | protocol = "tcp" 79 | cidr_blocks = ["0.0.0.0/0"] 80 | } 81 | 82 | # Locust web interface from the internet 83 | ingress { 84 | from_port = 8089 85 | to_port = 8089 86 | protocol = "tcp" 87 | cidr_blocks = ["0.0.0.0/0"] 88 | } 89 | 90 | egress { 91 | from_port = 0 92 | to_port = 0 93 | protocol = "-1" 94 | cidr_blocks = ["0.0.0.0/0"] 95 | } 96 | } 97 | 98 | resource aws_security_group sg_client_r2 { 99 | provider = aws.region2 100 | name = "sg_demo_client_r2" 101 | description = "Used in the terraform demo" 102 | vpc_id = aws_vpc.r2.id 103 | 104 | # SSH from the internet 105 | ingress { 106 | from_port = 22 107 | to_port = 22 108 | protocol = "tcp" 109 | cidr_blocks = ["0.0.0.0/0"] 110 | } 111 | 112 | # Locust web interface from the internet 113 | ingress { 114 | from_port = 8089 115 | to_port = 8089 116 | protocol = "tcp" 117 | cidr_blocks = ["0.0.0.0/0"] 118 | } 119 | 120 | egress { 121 | from_port = 0 122 | to_port = 0 123 | protocol = "-1" 124 | cidr_blocks = ["0.0.0.0/0"] 125 | } 126 | } 127 | 128 | resource aws_security_group sg_elb_r1 { 129 | provider = aws.region1 130 | name = "sg_elb_r1" 131 | description = "Used in the terraform demo" 132 | vpc_id = aws_vpc.r1.id 133 | 134 | ingress { 135 | from_port = 80 136 | to_port = 80 137 | protocol = "tcp" 138 | cidr_blocks = ["0.0.0.0/0"] 139 | } 140 | 141 | egress { 142 | from_port = 0 143 | to_port = 0 144 | protocol = "-1" 145 | cidr_blocks = ["0.0.0.0/0"] 146 | } 147 | } 148 | 149 | resource aws_security_group sg_elb_r2 { 150 | provider = aws.region2 151 | name = "sg_elb_r2" 152 | description = "Used in the terraform demo" 153 | vpc_id = aws_vpc.r2.id 154 | 155 | egress { 156 | from_port = 0 157 | to_port = 0 158 | protocol = "-1" 159 | cidr_blocks = ["0.0.0.0/0"] 160 | } 161 | } 162 | 163 | # The following rule is created separately from the rest in order to allow to be removed in tests 164 | resource aws_security_group_rule sg_rule_default_r2_allow_all_internal { 165 | provider = aws.region2 166 | type = "ingress" 167 | from_port = 0 168 | to_port = 65535 169 | protocol = "tcp" 170 | cidr_blocks = ["10.0.0.0/8"] 171 | security_group_id = aws_security_group.sg_default_r2.id 172 | } 173 | 174 | # The following rule is created separately from the rest in order to allow to be removed in tests 175 | resource aws_security_group_rule sg_rule_elb_r2_allow_http { 176 | provider = aws.region2 177 | type = "ingress" 178 | from_port = 80 179 | to_port = 80 180 | protocol = "tcp" 181 | cidr_blocks = ["0.0.0.0/0"] 182 | security_group_id = aws_security_group.sg_elb_r2.id 183 | } 184 | 185 | # The following rule is created separately from the rest in order to allow to be dropped in simulations 186 | resource aws_security_group_rule sg_rule_sg_r2_az3_allow_all_internal { 187 | provider = aws.region2 188 | type = "ingress" 189 | from_port = 0 190 | to_port = 65535 191 | protocol = "tcp" 192 | cidr_blocks = ["10.0.0.0/8"] 193 | security_group_id = aws_security_group.sg_r2_az3.id 194 | } 195 | -------------------------------------------------------------------------------- /terraform/subnets.tf: -------------------------------------------------------------------------------- 1 | resource aws_subnet r1_az1 { 2 | provider = aws.region1 3 | vpc_id = aws_vpc.r1.id 4 | cidr_block = "10.0.0.0/24" 5 | availability_zone = data.aws_availability_zones.r1.names[0] 6 | } 7 | 8 | resource aws_subnet r1_az2 { 9 | provider = aws.region1 10 | vpc_id = aws_vpc.r1.id 11 | cidr_block = "10.0.1.0/24" 12 | availability_zone = data.aws_availability_zones.r1.names[1] 13 | } 14 | 15 | resource aws_subnet r1_az3 { 16 | provider = aws.region1 17 | vpc_id = aws_vpc.r1.id 18 | cidr_block = "10.0.2.0/24" 19 | availability_zone = data.aws_availability_zones.r1.names[2] 20 | } 21 | 22 | resource aws_subnet r2_az1 { 23 | provider = aws.region2 24 | vpc_id = aws_vpc.r2.id 25 | cidr_block = "10.1.0.0/24" 26 | availability_zone = data.aws_availability_zones.r2.names[0] 27 | } 28 | 29 | resource aws_subnet r2_az2 { 30 | provider = aws.region2 31 | vpc_id = aws_vpc.r2.id 32 | cidr_block = "10.1.1.0/24" 33 | availability_zone = data.aws_availability_zones.r2.names[1] 34 | } 35 | 36 | resource aws_subnet r2_az3 { 37 | provider = aws.region2 38 | vpc_id = aws_vpc.r2.id 39 | cidr_block = "10.1.2.0/24" 40 | availability_zone = data.aws_availability_zones.r2.names[2] 41 | } -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable region1 { 2 | default = "us-east-1" 3 | } 4 | 5 | variable region2 { 6 | default = "us-west-2" 7 | } 8 | 9 | variable amis { 10 | type = map 11 | default = { 12 | "us-east-1" = "ami-b374d5a5" 13 | "us-west-2" = "ami-4b32be2b" 14 | } 15 | } -------------------------------------------------------------------------------- /terraform/vpcs.tf: -------------------------------------------------------------------------------- 1 | ######################################################## 2 | ### VPCs, VPC peering, route tables and internet gateway 3 | ######################################################## 4 | 5 | resource aws_vpc r1 { 6 | provider = aws.region1 7 | cidr_block = "10.0.0.0/16" 8 | enable_dns_hostnames = true 9 | tags = { 10 | Name = "Demo - VPC R1", 11 | Purpose = "Demo failover" 12 | } 13 | } 14 | 15 | resource aws_vpc r2 { 16 | provider = aws.region2 17 | cidr_block = "10.1.0.0/16" 18 | enable_dns_hostnames = true 19 | tags = { 20 | Name = "Demo - VPC R2", 21 | Purpose = "Demo failover" 22 | } 23 | } 24 | 25 | resource aws_internet_gateway agw_r1 { 26 | provider = aws.region1 27 | vpc_id = aws_vpc.r1.id 28 | tags = { 29 | Name = "Demo - IG R1", 30 | Purpose = "Demo failover" 31 | } 32 | } 33 | 34 | resource aws_route internet_access_r1 { 35 | provider = aws.region1 36 | route_table_id = aws_vpc.r1.main_route_table_id 37 | destination_cidr_block = "0.0.0.0/0" 38 | gateway_id = aws_internet_gateway.agw_r1.id 39 | } 40 | 41 | resource aws_internet_gateway agw_r2 { 42 | provider = aws.region2 43 | vpc_id = aws_vpc.r2.id 44 | tags = { Name = "demo" } 45 | } 46 | 47 | resource aws_route internet_access_r2 { 48 | provider = aws.region2 49 | route_table_id = aws_vpc.r2.main_route_table_id 50 | destination_cidr_block = "0.0.0.0/0" 51 | gateway_id = aws_internet_gateway.agw_r2.id 52 | } 53 | 54 | resource aws_vpc_peering_connection r1_to_r2_requester { 55 | provider = aws.region1 56 | vpc_id = aws_vpc.r1.id 57 | peer_vpc_id = aws_vpc.r2.id 58 | peer_region = var.region2 59 | auto_accept = false 60 | tags = { Side = "Requester" } 61 | } 62 | 63 | resource aws_vpc_peering_connection_accepter r1_to_r2_accepter { 64 | provider = aws.region2 65 | vpc_peering_connection_id = aws_vpc_peering_connection.r1_to_r2_requester.id 66 | auto_accept = true 67 | tags = { Side = "Accepter" } 68 | } 69 | 70 | resource aws_route route_r1 { 71 | provider = aws.region1 72 | route_table_id = aws_vpc.r1.main_route_table_id 73 | destination_cidr_block = "10.1.0.0/16" 74 | vpc_peering_connection_id = aws_vpc_peering_connection.r1_to_r2_requester.id 75 | } 76 | 77 | resource aws_route route_r2 { 78 | provider = aws.region2 79 | route_table_id = aws_vpc.r2.main_route_table_id 80 | destination_cidr_block = "10.0.0.0/16" 81 | vpc_peering_connection_id = aws_vpc_peering_connection.r1_to_r2_requester.id 82 | } --------------------------------------------------------------------------------