├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── locust-master-pod.json.template ├── locust-slave-replicationcontroller.json.template └── locust-web-service.json ├── script ├── cluster └── gke_functions └── test └── locustfile.py /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | *.log 3 | .~* 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hakobera/locust 2 | 3 | ADD ./test /test 4 | ENV SCENARIO_FILE /test/locustfile.py 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kazuyuki Honda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-locust-gke 2 | 3 | Sample of locust cluster for Google Container Engine using [hakober/locust](https://github.com/hakobera/docker-locust) image. 4 | 5 | ## Push image 6 | 7 | First, you should push image into your private repository. 8 | 9 | ``` 10 | $ export IMAGE_ID=locust-gke 11 | $ ./script/cluster push 12 | ``` 13 | 14 | ## Cluster management 15 | 16 | ## Set common environment value 17 | 18 | ``` 19 | $ export GKE_CLUSTER= 20 | $ export GKE_ZONE= 21 | ``` 22 | 23 | ### Start cluster 24 | 25 | ``` 26 | $ IMAGE_ID=locust-gke \ 27 | GKE_NETWORK= ] 28 | TARGET_URL= \ 29 | LOCUST_SLAVE_COUNT=2 \ 30 | ./script/cluster start 31 | ``` 32 | 33 | ### Stop cluster 34 | 35 | ``` 36 | $ ./script/cluster stop 37 | ``` 38 | 39 | ### Show cluster status 40 | 41 | ``` 42 | $ ./script/cluster status 43 | ``` 44 | 45 | ### Open kubernetes web console 46 | 47 | ``` 48 | $ ./script/cluster open-kubernetes 49 | ``` 50 | 51 | ### Open locust web console 52 | 53 | ``` 54 | $ ./script/cluster open-locust 55 | ``` 56 | -------------------------------------------------------------------------------- /config/locust-master-pod.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "id": "locust-master-pod", 3 | "apiVersion": "v1beta1", 4 | "kind": "Pod", 5 | "desiredState": { 6 | "manifest": { 7 | "version": "v1beta1", 8 | "id": "locust-master-pod", 9 | "containers": [ 10 | { 11 | "name": "locust-master", 12 | "image": "{{IMAGE_TAG}}", 13 | "ports": [ 14 | { 15 | "name": "locust-web-port", 16 | "containerPort": 8089, 17 | "hostPort": 8089 18 | }, 19 | { 20 | "name": "locust-master-port", 21 | "containerPort": 5557, 22 | "hostPort": 5557 23 | }, 24 | { 25 | "name": "locust-master-bind-port", 26 | "containerPort": 5558, 27 | "hostPort": 5558 28 | } 29 | ], 30 | "env": [ 31 | { 32 | "name": "LOCUST_MODE", 33 | "value": "master" 34 | }, 35 | { 36 | "name": "TARGET_URL", 37 | "value": "{{TARGET_URL}}" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "labels": { 45 | "name": "locust-master" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/locust-slave-replicationcontroller.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "id": "locust-slave-replication-controller", 3 | "apiVersion": "v1beta1", 4 | "kind": "ReplicationController", 5 | "desiredState": { 6 | "replicas": {{LOCUST_SLAVE_COUNT}}, 7 | "replicaSelector": { 8 | "name": "locust-slave" 9 | }, 10 | "podTemplate": { 11 | "desiredState": { 12 | "manifest": { 13 | "version": "v1beta1", 14 | "id": "locust-slave-pod", 15 | "containers": [ 16 | { 17 | "name": "locust-slave", 18 | "image": "{{IMAGE_TAG}}", 19 | "ports": [], 20 | "env": [ 21 | { 22 | "name": "LOCUST_MODE", 23 | "value": "slave" 24 | }, 25 | { 26 | "name": "MASTER_HOST", 27 | "value": "{{MASTER_HOST}}" 28 | }, 29 | { 30 | "name": "TARGET_URL", 31 | "value": "{{TARGET_URL}}" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | }, 38 | "labels": { 39 | "name": "locust-slave" 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config/locust-web-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "locust-web-service", 3 | "apiVersion": "v1beta1", 4 | "kind": "Service", 5 | "port": 8089, 6 | "containerPort": "locust-web-port", 7 | "selector": { 8 | "name": "locust-master" 9 | }, 10 | "createExternalLoadBalancer": true 11 | } 12 | -------------------------------------------------------------------------------- /script/cluster: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASEDIR=$(dirname $0) 4 | 5 | # source gke function library 6 | source $BASEDIR/gke_functions 7 | 8 | NETWORK_RANGE=${NETWORK_RANGE:-10.241.0.0/16} 9 | SOURCE_RANGES=${SOURCE_RANGES:-0.0.0.0/0} 10 | REGISTRY_PORT=${REGISTRY_PORT:-5000} 11 | LOCUST_WEB_PORT=8089 12 | LOCUST_SLAVE_COUNT=${LOCUST_SLAVE_COUNT:-2} 13 | 14 | WORK_DIR=./tmp 15 | 16 | if [ ! -d "$WORK_DIR" ]; then 17 | mkdir $WORK_DIR 18 | fi 19 | 20 | # check required env 21 | check_and_export_cluster_envs() { 22 | if [ "$GKE_CLUSTER" = "" ]; then 23 | echo 'Environment value named GKE_CLUSTER must be set' 24 | exit 1 25 | fi 26 | GKE_ZONE=${GKE_ZONE:-us-central1-a} 27 | 28 | export GKE_CLUSTER 29 | export GKE_ZONE 30 | } 31 | 32 | check_and_export_image_id() { 33 | if [ "$IMAGE_ID" = "" ]; then 34 | echo 'Environment value named IMAGE_ID must be set' 35 | return 1 36 | fi 37 | 38 | GKE_PROJECT=$(gcloud config list --format json | jq -r ".core.project") 39 | IMAGE_TAG="gcr.io/$(echo $GKE_PROJECT | sed -e "s/-/_/g")/$IMAGE_ID" 40 | 41 | export GKE_PROJECT 42 | export IMAGE_ID 43 | export IMAGE_TAG 44 | } 45 | 46 | push_image() { 47 | set -e 48 | check_and_export_image_id 49 | 50 | echo "Pushing image: $IMAGE_ID" 51 | 52 | echo "=== env ===" 53 | echo "GKE_PROJECT: $GKE_PROJECT" 54 | echo "IMAGE_ID: $IMAGE_ID" 55 | echo "IMAGE_TAG: $IMAGE_TAG" 56 | echo 57 | 58 | echo "=== Build docker image ===" 59 | docker pull hakobera/locust 60 | docker build -t tmp-$IMAGE_ID . 61 | docker tag -f tmp-$IMAGE_ID $IMAGE_TAG 62 | echo 63 | 64 | echo "=== Push docker image ===" 65 | gcloud docker push $IMAGE_TAG 66 | echo 67 | } 68 | 69 | start() { 70 | check_and_export_cluster_envs 71 | check_and_export_image_id 72 | 73 | echo "Starting GKE cluster: $GKE_CLUSTER in $GKE_ZONE" 74 | 75 | if [ "$GKE_NETWORK" = "" ]; then 76 | echo 'Environment value named GKE_NETWORK must be set' 77 | exit 1 78 | fi 79 | if [ "$TARGET_URL" = "" ]; then 80 | echo 'Environment value named TARGET_URL must be set' 81 | return 1 82 | fi 83 | 84 | echo "=== env ===" 85 | echo "GKE_PROJECT: $GKE_PROJECT" 86 | echo "IMAGE_ID: $IMAGE_ID" 87 | echo "IMAGE_TAG: $IMAGE_TAG" 88 | echo "TARGET_URL: $TARGET_URL" 89 | echo 90 | 91 | local configdir=./config 92 | local image_tag=$(echo $IMAGE_TAG | sed -e 's/\//\\\//g') 93 | local target_url=$(echo -n $TARGET_URL | sed -e 's/[]\/$*.^|[]/\\&/g') 94 | local locust_master_config_file=locust-master-pod.json 95 | local locust_web_config_file=locust-web-service.json 96 | local locust_slave_config_file=locust-slave-replicationcontroller.json 97 | 98 | echo "=== Create network ===" 99 | create_network $GKE_NETWORK $NETWORK_RANGE 100 | add_firewall_rule $GKE_NETWORK ssh 22 $SOURCE_RANGES 101 | add_firewall_rule $GKE_NETWORK locust-web 8089 $SOURCE_RANGES 102 | echo 103 | 104 | set -e 105 | 106 | echo "=== Create cluster ===" 107 | create_cluster $GKE_NETWORK $LOCUST_SLAVE_COUNT 108 | if [ "$?" != "0" ]; then 109 | echo "Create cluster failed: $GKE_CLUSTER in $GKE_ZONE" 110 | return 1 111 | fi 112 | echo 113 | 114 | echo "=== Create locust master pod ===" 115 | sed -e "s/{{IMAGE_TAG}}/$image_tag/" \ 116 | -e "s/{{TARGET_URL}}/$target_url/" \ 117 | $configdir/$locust_master_config_file.template \ 118 | > $WORK_DIR/$locust_master_config_file 119 | cat $WORK_DIR/$locust_master_config_file 120 | create_pod locust-master-pod $WORK_DIR/$locust_master_config_file 121 | echo 122 | 123 | echo "=== Create locust web service ===" 124 | cat $configdir/$locust_web_config_file 125 | create_service locust-web-service $configdir/$locust_web_config_file 126 | echo 127 | 128 | sleep 30s 129 | local locust_master_ip=$(get_pod_ip locust-master-pod $GKE_CLUSTER $GKE_ZONE) 130 | echo "MASTER_HOST: $locust_master_ip" 131 | 132 | echo "=== Create locust slave replication controller ===" 133 | sed -e "s/{{IMAGE_TAG}}/$image_tag/" \ 134 | -e "s/{{TARGET_URL}}/$target_url/" \ 135 | -e "s/{{MASTER_HOST}}/$locust_master_ip/" \ 136 | -e "s/{{LOCUST_SLAVE_COUNT}}/$LOCUST_SLAVE_COUNT/" \ 137 | $configdir/$locust_slave_config_file.template \ 138 | > $WORK_DIR/$locust_slave_config_file 139 | cat $WORK_DIR/$locust_slave_config_file 140 | create_replication_controller locust-slave $WORK_DIR/$locust_slave_config_file 141 | echo 142 | 143 | sleep 30s 144 | echo "=== Results ===" 145 | describe_cluster 146 | } 147 | 148 | stop() { 149 | check_and_export_cluster_envs 150 | echo "Stopping GKE cluster: $GKE_CLUSTER in $GKE_ZONE" 151 | 152 | echo "=== Delete services expternal load balancer ===" 153 | delete_service locust-web-service 154 | 155 | echo "=== Delete cluster ===" 156 | delete_cluster 157 | echo 158 | 159 | #echo "=== Delete network ===" 160 | #delete_network $GKE_NETWORK 161 | } 162 | 163 | status() { 164 | check_and_export_cluster_envs 165 | echo "Show cluster status: $GKE_CLUSTER in $GKE_ZONE" 166 | describe_cluster 167 | } 168 | 169 | open_kubernetes() { 170 | check_and_export_cluster_envs 171 | echo "Open kubernetes web: $GKE_CLUSTER in $GKE_ZONE" 172 | open_kubernetes_web 173 | } 174 | 175 | open_locust_web() { 176 | check_and_export_cluster_envs 177 | echo "Open locust web: $GKE_CLUSTER in $GKE_ZONE" 178 | open http://$(get_forwading_rule_ip locust-web-service):$LOCUST_WEB_PORT 179 | } 180 | 181 | case "$1" in 182 | start) 183 | start 184 | ;; 185 | stop) 186 | stop 187 | ;; 188 | status) 189 | status 190 | ;; 191 | open-kubernetes) 192 | open_kubernetes 193 | ;; 194 | open-locust) 195 | open_locust_web 196 | ;; 197 | push) 198 | push_image 199 | ;; 200 | *) 201 | echo "Usage: cluster {start|stop|status|open-kubernetes|open-locust}" >&2 202 | exit 1 203 | ;; 204 | esac 205 | 206 | exit 0 207 | -------------------------------------------------------------------------------- /script/gke_functions: -------------------------------------------------------------------------------- 1 | ####################################################### 2 | # Google Container Engine helper functions 3 | # 4 | # Prerequisities 5 | # - gcloud 6 | # - jq 7 | ####################################################### 8 | 9 | GCE_CMD="gcloud compute" 10 | GKE_CMD="gcloud preview container" 11 | 12 | function create_network() { 13 | local network=$1 14 | local range=$2 15 | 16 | echo "Create network: $network" 17 | $GCE_CMD networks describe $network 18 | if [ "$?" = "0" ]; then 19 | echo "[skip] $network is already exists." 20 | return 0 21 | fi 22 | 23 | $GCE_CMD networks create $network --range $range 24 | } 25 | 26 | function delete_network() { 27 | local network=$1 28 | 29 | echo "Delete network: $network" 30 | $GCE_CMD networks describe $network 31 | if [ "$?" != "0" ]; then 32 | return 1 33 | fi 34 | echo 35 | 36 | echo "Delete following firewall rules used by $network" 37 | $GCE_CMD firewall-rules list --regexp "^$network-.*$" 38 | echo 39 | 40 | for rule in $($GCE_CMD firewall-rules list --regexp "^$network-.*$" --format json | jq -r ".[].name" | tr '\n' ' ') 41 | do 42 | delete_firewall_rule $rule 43 | echo 44 | done 45 | $GCE_CMD networks delete $network 46 | } 47 | 48 | function add_firewall_rule() { 49 | local network=$1 50 | local name=$2 51 | local port=$3 52 | local range=$4 53 | 54 | echo "Add firewall rule: $1-allow-$name" 55 | $GCE_CMD firewall-rules create $1-allow-$name \ 56 | --allow tcp:$3 \ 57 | --network $1 \ 58 | --source-ranges $4 59 | } 60 | 61 | function delete_firewall_rule() { 62 | local rule=$1 63 | 64 | echo "Delete firewall rule: $rule" 65 | echo 66 | $GCE_CMD firewall-rules delete $rule 67 | } 68 | 69 | function create_cluster() { 70 | local network=$1 71 | local num_nodes=${2:-1} 72 | local machine_type=${3:-n1-standard-1} 73 | local cluster=${4:-$GKE_CLUSTER} 74 | local zone=${5:-$GKE_ZONE} 75 | 76 | echo "Create cluster: $cluster" 77 | $GKE_CMD clusters create $cluster \ 78 | --zone $zone \ 79 | --network $network \ 80 | --num-nodes $num_nodes \ 81 | --machine-type $machine_type \ 82 | --no-set-default 83 | } 84 | 85 | function delete_cluster() { 86 | local cluster=${1:-$GKE_CLUSTER} 87 | local zone=${2:-$GKE_ZONE} 88 | 89 | echo "Delete cluster: $cluster" 90 | echo 91 | $GKE_CMD clusters describe $cluster --zone $zone 92 | if [ "$?" != "0" ]; then 93 | return 1 94 | fi 95 | echo 96 | 97 | $GKE_CMD clusters delete $cluster --zone $zone 98 | } 99 | 100 | function describe_cluster() { 101 | local cluster=${1:-$GKE_CLUSTER} 102 | local zone=${2:-$GKE_ZONE} 103 | 104 | echo "[Cluster]" 105 | $GKE_CMD clusters describe $cluster --zone $zone 106 | echo 107 | echo "[Pods]" 108 | $GKE_CMD pods list --cluster $cluster --zone $zone 109 | echo 110 | echo "[Replication Controllers]" 111 | $GKE_CMD replicationcontrollers list --cluster $cluster --zone $zone 112 | echo 113 | echo "[Services]" 114 | $GKE_CMD services list --cluster $cluster --zone $zone 115 | } 116 | 117 | function create_pod() { 118 | local pod=$1 119 | local config=$2 120 | local cluster=${3:-$GKE_CLUSTER} 121 | local zone=${4:-$GKE_ZONE} 122 | 123 | echo "Create pod: $pod" 124 | $GKE_CMD pods create \ 125 | --name $pod \ 126 | --cluster $cluster \ 127 | --zone $zone \ 128 | --config-file $config 129 | } 130 | 131 | function delete_pod() { 132 | local pod=$1 133 | local cluster=${2:-$GKE_CLUSTER} 134 | local zone=${3:-$GKE_ZONE} 135 | 136 | echo "Delete pod: $pod" 137 | echo 138 | $GKE_CMD pods delete $pod \ 139 | --cluster $cluster \ 140 | --zone $zone 141 | } 142 | 143 | function get_pod_ip() { 144 | local pod=$1 145 | local cluster=${2:-$GKE_CLUSTER} 146 | zone=${3:-$GKE_ZONE} 147 | 148 | $GKE_CMD kubectl --cluster $cluster --zone $zone get pods -o json \ 149 | | jq -r ".items[] | select(.id == \"$pod\") | .currentState.podIP" 150 | } 151 | 152 | function get_pod_host_ip() { 153 | local pod=$1 154 | local cluster=${2:-$GKE_CLUSTER} 155 | local zone=${3:-$GKE_ZONE} 156 | 157 | $GKE_CMD kubectl --cluster $cluster --zone $zone get pods -o json \ 158 | | jq -r ".items[] | select(.id == \"$pod\") | .currentState.hostIP" 159 | } 160 | 161 | function create_replication_controller() { 162 | local replication_controller=$1 163 | local config=$2 164 | local cluster=${3:-$GKE_CLUSTER} 165 | local zone=${4:-$GKE_ZONE} 166 | 167 | echo "Create replication controllers: $replication_controller" 168 | $GKE_CMD replicationcontrollers create \ 169 | --cluster $cluster \ 170 | --zone $zone \ 171 | --config-file $config 172 | } 173 | 174 | function delete_replication_controller() { 175 | local replication_controller=$1 176 | local cluster=${2:-$GKE_CLUSTER} 177 | local zone=${3:-$GKE_ZONE} 178 | 179 | echo "Delete replication controller: $replication_controller" 180 | echo 181 | $GKE_CMD replicationcontrollers delete $replication_controller \ 182 | --cluster $cluster \ 183 | --zone $zone 184 | } 185 | 186 | function create_service() { 187 | local service=$1 188 | local config=$2 189 | local cluster=${3:-$GKE_CLUSTER} 190 | local zone=${4:-$GKE_ZONE} 191 | 192 | echo "Create service: $service" 193 | $GKE_CMD services create \ 194 | --cluster $cluster \ 195 | --zone $zone \ 196 | --config-file $config 197 | } 198 | 199 | function delete_service() { 200 | local service=$1 201 | local cluster=${2:-$GKE_CLUSTER} 202 | local zone=${3:-$GKE_ZONE} 203 | 204 | echo "Delete service: $service" 205 | echo 206 | $GKE_CMD services delete $service \ 207 | --cluster $cluster \ 208 | --zone $zone 209 | } 210 | 211 | function get_service_portal_ip() { 212 | local service=$1 213 | local cluster=${2:-$GKE_CLUSTER} 214 | local zone=${3:-$GKE_ZONE} 215 | 216 | $GKE_CMD kubectl --cluster $cluster --zone $zone get services -o json \ 217 | | jq -r ".items[] | select(.id == \"$service\") | .portalIP" 218 | } 219 | 220 | function get_forwading_rule_ip() { 221 | local name=$1 222 | 223 | $GCE_CMD forwarding-rules list --format json \ 224 | | jq -r ".[] | select(.name == \"$name\") | .IPAddress" 225 | } 226 | 227 | function open_kubernetes_web() { 228 | local cluster=${1:-$GKE_CLUSTER} 229 | local zone=${2:-$GKE_ZONE} 230 | 231 | open $($GKE_CMD clusters describe $cluster --zone $zone --format json | jq '{endpoint, user: .masterAuth.user, password: .masterAuth.password}' | jq -r '"https:////" + .user + ":" + .password + "@" + .endpoint + "/static/#/groups/host/selector/"') 232 | } 233 | 234 | -------------------------------------------------------------------------------- /test/locustfile.py: -------------------------------------------------------------------------------- 1 | from locust import HttpLocust, TaskSet 2 | 3 | def login(self): 4 | self.client.post("/login", {"username":"ellen_key", "password":"education"}) 5 | 6 | def index(self): 7 | self.client.get("/") 8 | 9 | def profile(self): 10 | self.client.get("/profile") 11 | 12 | class UserBehavior(TaskSet): 13 | tasks = {index:2, profile:1} 14 | 15 | def on_start(self): 16 | login(self) 17 | 18 | class WebsiteUser(HttpLocust): 19 | task_set = UserBehavior 20 | min_wait=5000 21 | max_wait=9000 22 | --------------------------------------------------------------------------------